import { useState, useEffect, useMemo } from 'react' import { useQuery, useMutation } from '@tanstack/react-query' import { api } from '../../api/client' import { useLayout } from '../../components/Layout' import Select from '../../components/Select' const TYPE_ORDER = ['아케인', '어센틱', '그랜드 어센틱'] function CharacterCard({ char, active, onSelect, onRemove }) { return (
{ if (e.target.closest('button')) return onSelect() }} className={`group relative shrink-0 w-36 rounded-xl border cursor-pointer select-none transition ${ active ? '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' }`} > {/* 삭제 (우상단) */} {/* 내용 */}
{char.character_image ? ( ) : ( ? )}
{char.character_name}
Lv.{char.character_level} · {char.job_name}
) } function SymbolCard({ symbol, equipped }) { const [weeklyCount, setWeeklyCount] = useState(3) const [dailyDone, setDailyDone] = useState(false) // 임시 목업 값 (계산 기능 미구현) const level = equipped ? 0 : 0 const growth = 0 const requireGrowth = symbol.levels?.[0]?.required_count || 0 const remainingSymbols = '-' const remainingMeso = '-' const daysLeft = '-' const completeDate = '-' return (
{symbol.image_url && ( {symbol.region} )}
{symbol.region}
Lv.{level} / {symbol.max_level}
{/* 진행도 바 */}
성장치 {growth} / {requireGrowth} {requireGrowth ? Math.min(Math.floor((growth / requireGrowth) * 100), 100) : 0}%
{/* 획득량 입력 */}
{/* 정보 */}
남은 심볼 {remainingSymbols}
필요 메소 {remainingMeso}
체납 메소 -
남은 일수 {typeof daysLeft === 'number' ? `${daysLeft}일` : daysLeft}
예상 완료일 {completeDate}
) } export default function Symbol() { const { setFullscreen } = useLayout() useEffect(() => { setFullscreen(true) return () => setFullscreen(false) }, [setFullscreen]) const STORAGE_KEY = 'maple-symbol' // 심볼 목록 (DB에서 로드) const { data: allSymbols = [] } = useQuery({ queryKey: ['symbol', 'symbols'], queryFn: () => api('/api/symbols').catch(() => []), staleTime: 5 * 60 * 1000, }) const tabs = useMemo(() => { const groups = {} for (const s of allSymbols) { if (!groups[s.type]) groups[s.type] = s } return TYPE_ORDER .filter((t) => groups[t]) .map((t) => ({ key: t, label: `${t} 심볼`, image_url: groups[t].image_url })) }, [allSymbols]) const [tab, setTab] = useState(null) useEffect(() => { if (!tab && tabs.length) setTab(tabs[0].key) }, [tabs, tab]) const [characters, setCharacters] = useState(() => { try { const saved = localStorage.getItem(STORAGE_KEY) if (saved) return JSON.parse(saved).characters || [] } catch { /* ignore */ } return [] }) const [selectedCharId, setSelectedCharId] = useState(() => { try { const saved = localStorage.getItem(STORAGE_KEY) if (saved) return JSON.parse(saved).selectedCharId ?? null } catch { /* ignore */ } return null }) const [addName, setAddName] = useState('') const [addError, setAddError] = useState('') useEffect(() => { localStorage.setItem(STORAGE_KEY, JSON.stringify({ characters, selectedCharId })) }, [characters, selectedCharId]) const symbols = allSymbols.filter((s) => s.type === tab) const tabInfo = tabs.find((t) => t.key === tab) const searchMutation = useMutation({ mutationFn: (name) => api(`/api/character/search?name=${encodeURIComponent(name)}`), onSuccess: (data) => { setCharacters((prev) => { if (prev.find((c) => c.character_name === data.character_name)) { setAddError('이미 추가된 캐릭터입니다') return prev } setAddError('') setAddName('') setSelectedCharId(data.ocid) return [...prev, { ...data, id: data.ocid }] }) }, onError: (err) => setAddError(err.message || '조회 실패'), }) const handleSearch = (e) => { e.preventDefault() const n = addName.trim() if (!n) return setAddError('') searchMutation.mutate(n) } // 임시: 첫 번째 심볼만 장착된 것으로 표시 const isEquipped = (i) => i === 0 return (
{/* 캐릭터 조회 */}
{ setAddName(e.target.value); if (addError) setAddError('') }} placeholder="캐릭터 닉네임으로 장착 심볼 불러오기" className="w-full h-12 rounded-lg border-2 border-white/10 bg-gray-950 pl-10 pr-4 text-base outline-none focus:border-emerald-500/60 hover:border-white/20 transition" />
{addError &&

{addError}

} {/* 캐릭터 목록 */} {characters.length > 0 && (
{characters.map((c) => ( setSelectedCharId(c.id)} onRemove={() => { setCharacters((prev) => prev.filter((x) => x.id !== c.id)) if (selectedCharId === c.id) setSelectedCharId(null) }} /> ))}
)}
{/* 심볼 타입 탭 */}
{tabs.map((t) => ( ))}
{/* 심볼 카드 그리드 */}
{symbols.map((s, i) => ( ))}
{/* 전체 요약 */}
{tabInfo?.label} 전체 만렙 완료 예상일
2026년 09월 12일 (토)
누적 체납 메소
108,000,000
누적 필요 메소
768,000,000
) }