import { useState } from 'react' import { useMutation } from '@tanstack/react-query' import { Reorder, useDragControls } from 'framer-motion' import { api } from '../../../api/client' import ConfirmDialog from '../../../components/ConfirmDialog' import Tooltip from '../../../components/Tooltip' import { useFitText } from '../../../hooks/useFitText' import { DIFFICULTIES, formatMeso, getDifficultyBadgeStyle } from '../admin/constants' const MAX_PER_CHARACTER = 12 const MAX_PER_ACCOUNT = 90 function CharacterContent({ char, selections, bosses }) { const selectedBosses = Object.entries(selections || {}) .filter(([, sel]) => sel) .map(([bossId, sel]) => { const boss = bosses.find((b) => b.id === Number(bossId)) if (!boss) return null const bd = boss.difficulties.find((d) => d.difficulty === sel.difficulty) if (!bd) return null return { boss, difficulty: sel.difficulty, revenue: Math.floor(bd.crystal_price / sel.party), } }) .filter(Boolean) .sort((a, b) => b.revenue - a.revenue) const visibleBosses = selectedBosses.slice(0, MAX_PER_CHARACTER) const totalRevenue = visibleBosses.reduce((s, x) => s + x.revenue, 0) const count = selectedBosses.length return (
{char.character_image ? ( ) : ( ? )}
{char.character_name} Lv.{char.character_level} · {char.job_name}
{visibleBosses.length > 0 ? (
{visibleBosses.map((item) => { const diff = DIFFICULTIES.find((d) => d.key === item.difficulty) return (
{diff?.initial}
) })}
) : (
보스 미선택
)}
0 ? 'text-amber-300' : 'text-gray-600'}`}>{count} / {MAX_PER_CHARACTER}
0 ? 'text-emerald-300' : 'text-gray-700'}`}> {count > 0 ? formatMeso(totalRevenue) : '-'}
) } function CharacterItem({ char, isSelected, selections, bosses, onSelect, onRemove }) { const [dragged, setDragged] = useState(false) const dragControls = useDragControls() return ( setDragged(true)} onDragEnd={() => { // 다음 click 이벤트 후에 reset setTimeout(() => setDragged(false), 0) }} onClick={(e) => { if (dragged) return if (e.target.closest('button')) return onSelect(char.character_name) }} className={`group relative rounded-xl border cursor-pointer select-none ${ isSelected ? 'border-emerald-500/40 bg-emerald-500/[0.08]' : 'border-white/5 hover:border-white/15 bg-gray-950/40 hover:bg-gray-950/60' }`} > {/* 드래그 핸들 */}
{ e.preventDefault(); dragControls.start(e) }} className="absolute left-0 top-0 bottom-0 w-8 flex items-center justify-center text-gray-600 hover:text-gray-400 cursor-grab active:cursor-grabbing" style={{ touchAction: 'none' }} >
) } export default function CharacterPanel({ characters, selectedName, allSelections, bosses, onSelect, onAdd, onRemove, onReorder, }) { const [name, setName] = useState('') const [error, setError] = useState('') const [confirmRemove, setConfirmRemove] = useState(null) const searchMutation = useMutation({ mutationFn: (n) => api(`/api/character/search?name=${encodeURIComponent(n)}`), onSuccess: (data) => { if (characters.find((c) => c.character_name === data.character_name)) { setError('이미 추가된 캐릭터입니다') return } onAdd(data) setName('') setError('') }, onError: (err) => setError(err.message), }) const handleSubmit = (e) => { e.preventDefault() if (!name.trim()) return setError('') searchMutation.mutate(name.trim()) } // 총합 계산 const charResults = characters.map((char) => { const charSel = allSelections[char.character_name] || {} const items = Object.entries(charSel) .filter(([, sel]) => sel) .map(([bossId, sel]) => { const boss = bosses.find((b) => b.id === Number(bossId)) if (!boss) return null const bd = boss.difficulties.find((d) => d.difficulty === sel.difficulty) if (!bd) return null return Math.floor(bd.crystal_price / sel.party) }) .filter(Boolean) .sort((a, b) => b - a) .slice(0, MAX_PER_CHARACTER) return { count: items.length, revenue: items.reduce((s, v) => s + v, 0) } }) const totalCount = charResults.reduce((s, r) => s + r.count, 0) const totalRevenue = charResults.reduce((s, r) => s + r.revenue, 0) const accountUsage = Math.min(totalCount, MAX_PER_ACCOUNT) const usagePct = Math.min((accountUsage / MAX_PER_ACCOUNT) * 100, 100) const totalText = formatMeso(totalRevenue) const { containerRef: totalContainerRef, textRef: totalTextRef } = useFitText({ maxFontSize: 32, minFontSize: 14, value: totalText, }) return (
{/* 총 수익 카드 (고정) */}
총 주간 수익
{totalText}
총 결정 개수
MAX_PER_ACCOUNT ? 'bg-amber-500' : 'bg-emerald-500'}`} style={{ width: `${usagePct}%` }} />
MAX_PER_ACCOUNT ? 'text-red-400' : 'text-amber-300'}`}> {accountUsage} MAX_PER_ACCOUNT ? 'text-red-400/40' : 'text-amber-300/40'}`}> / {MAX_PER_ACCOUNT}
{totalCount > MAX_PER_ACCOUNT && (

⚠ 한도 {totalCount - MAX_PER_ACCOUNT}개 초과

)}
{/* 캐릭터 추가 (고정) */}
{ setName(e.target.value); if (error) setError('') }} placeholder="캐릭터 닉네임 검색" className="w-full rounded-lg border-2 border-white/10 bg-gray-950 pl-10 pr-3 py-2.5 text-sm outline-none focus:border-emerald-500/60 hover:border-white/20 transition" />
{error &&

{error}

}
{/* 캐릭터 목록 (스크롤) */} {characters.length > 0 && (
{characters.map((char) => ( ))}
)} setConfirmRemove(null)} onConfirm={() => { onRemove(confirmRemove.character_name) setConfirmRemove(null) }} title="캐릭터 삭제" description={confirmRemove ? `"${confirmRemove.character_name}" 캐릭터를 목록에서 삭제하시겠습니까?\n\n저장된 보스 선택도 함께 삭제됩니다.` : ''} confirmText="삭제" destructive />
) }