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}
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 (
{/* 캐릭터 조회 */}
{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일 (토)
)
}