2026-04-13 19:17:49 +09:00
|
|
|
import { useState, useEffect } from 'react'
|
|
|
|
|
import { useQuery } from '@tanstack/react-query'
|
|
|
|
|
import { api } from '../../api/client'
|
|
|
|
|
import { useLayout } from '../../components/Layout'
|
|
|
|
|
import CharacterPanel from './user/CharacterPanel'
|
|
|
|
|
import BossSelector from './user/BossSelector'
|
|
|
|
|
|
|
|
|
|
const STORAGE_CHARS = 'maple-bc-characters'
|
|
|
|
|
const STORAGE_SELECTIONS = 'maple-bc-selections'
|
|
|
|
|
const MAX_PER_CHARACTER = 12
|
|
|
|
|
|
2026-04-13 15:27:04 +09:00
|
|
|
export default function BossCrystal() {
|
2026-04-13 19:17:49 +09:00
|
|
|
const [characters, setCharacters] = useState(() => {
|
|
|
|
|
const saved = localStorage.getItem(STORAGE_CHARS)
|
|
|
|
|
return saved ? JSON.parse(saved) : []
|
|
|
|
|
})
|
|
|
|
|
const [selectedChar, setSelectedChar] = useState(() => {
|
|
|
|
|
const saved = localStorage.getItem(STORAGE_CHARS)
|
|
|
|
|
const list = saved ? JSON.parse(saved) : []
|
|
|
|
|
return list[0]?.character_name || null
|
|
|
|
|
})
|
|
|
|
|
const [allSelections, setAllSelections] = useState(() => {
|
|
|
|
|
const saved = localStorage.getItem(STORAGE_SELECTIONS)
|
|
|
|
|
return saved ? JSON.parse(saved) : {}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
localStorage.setItem(STORAGE_CHARS, JSON.stringify(characters))
|
|
|
|
|
}, [characters])
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
localStorage.setItem(STORAGE_SELECTIONS, JSON.stringify(allSelections))
|
|
|
|
|
}, [allSelections])
|
|
|
|
|
|
|
|
|
|
// 풀스크린 모드 (푸터 숨김 + 내부 스크롤)
|
|
|
|
|
const { setFullscreen } = useLayout()
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
setFullscreen(true)
|
|
|
|
|
return () => setFullscreen(false)
|
|
|
|
|
}, [setFullscreen])
|
|
|
|
|
|
|
|
|
|
const { data: bosses = [], isLoading } = useQuery({
|
|
|
|
|
queryKey: ['boss-crystal', 'bosses'],
|
|
|
|
|
queryFn: () => api('/api/boss-crystal/bosses').catch(() => []),
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const handleAddCharacter = (char) => {
|
|
|
|
|
setCharacters((prev) => [...prev, char])
|
|
|
|
|
setSelectedChar(char.character_name)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleRemoveCharacter = (name) => {
|
|
|
|
|
setCharacters((prev) => {
|
|
|
|
|
const next = prev.filter((c) => c.character_name !== name)
|
|
|
|
|
if (selectedChar === name) {
|
|
|
|
|
setSelectedChar(next[0]?.character_name || null)
|
|
|
|
|
}
|
|
|
|
|
return next
|
|
|
|
|
})
|
|
|
|
|
setAllSelections((prev) => {
|
|
|
|
|
const next = { ...prev }
|
|
|
|
|
delete next[name]
|
|
|
|
|
return next
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleReorderCharacters = (next) => {
|
|
|
|
|
setCharacters(next)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleBossChange = (bossId, sel) => {
|
|
|
|
|
if (!selectedChar) return
|
|
|
|
|
setAllSelections((prev) => {
|
|
|
|
|
const charSel = { ...(prev[selectedChar] || {}) }
|
|
|
|
|
if (sel === null) {
|
|
|
|
|
delete charSel[bossId]
|
|
|
|
|
} else {
|
|
|
|
|
charSel[bossId] = sel
|
|
|
|
|
}
|
|
|
|
|
return { ...prev, [selectedChar]: charSel }
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const currentSelections = selectedChar ? (allSelections[selectedChar] || {}) : {}
|
|
|
|
|
const currentSelectedCount = Object.values(currentSelections).filter(Boolean).length
|
|
|
|
|
const isMaxReached = currentSelectedCount >= MAX_PER_CHARACTER
|
|
|
|
|
|
|
|
|
|
|
2026-04-13 15:27:04 +09:00
|
|
|
return (
|
2026-04-13 19:17:49 +09:00
|
|
|
<div className="h-full">
|
|
|
|
|
{isLoading ? (
|
|
|
|
|
<div className="rounded-2xl border border-white/5 bg-gray-900/40 p-16 text-center">
|
|
|
|
|
<div className="w-6 h-6 border-2 border-emerald-500 border-t-transparent rounded-full animate-spin mx-auto" />
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="grid gap-4 lg:grid-cols-[420px_1fr] h-full min-h-0">
|
|
|
|
|
{/* 좌측: 캐릭터 + 결과 통합 (총 수익/추가 고정 + 목록 스크롤) */}
|
|
|
|
|
<div className="rounded-2xl border border-white/5 bg-gray-900/40 p-4 min-h-0 max-h-full self-start overflow-hidden flex flex-col">
|
|
|
|
|
<CharacterPanel
|
|
|
|
|
characters={characters}
|
|
|
|
|
selectedName={selectedChar}
|
|
|
|
|
allSelections={allSelections}
|
|
|
|
|
bosses={bosses}
|
|
|
|
|
onSelect={setSelectedChar}
|
|
|
|
|
onAdd={handleAddCharacter}
|
|
|
|
|
onRemove={handleRemoveCharacter}
|
|
|
|
|
onReorder={handleReorderCharacters}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 우측: 보스 선택 (헤더 고정 + 목록 스크롤) */}
|
|
|
|
|
<div className="min-h-0">
|
|
|
|
|
<BossSelector
|
|
|
|
|
characterName={selectedChar}
|
|
|
|
|
bosses={bosses}
|
|
|
|
|
selections={currentSelections}
|
|
|
|
|
onChange={handleBossChange}
|
|
|
|
|
maxReached={isMaxReached}
|
|
|
|
|
selectedCount={currentSelectedCount}
|
|
|
|
|
maxPerCharacter={MAX_PER_CHARACTER}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2026-04-13 15:27:04 +09:00
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|