import { useState, useLayoutEffect, useMemo } from 'react' import { useQuery } from '@tanstack/react-query' import dayjs from 'dayjs' import { api } from '../../../api/client' import { GENESIS_CHAPTERS, GENESIS_TOTAL, MONTHLY_BOSSES, formatDate, } from '../data' import { useLiberationStore } from '../store' import { bossEarn, calcWeekPoints, calcDoneEarn, calcMonthlyEarn, getSchedulerWeekRange, computeCompletionDate, } from '../utils' import QuestSelector from './components/QuestSelector' import PointsInput from './components/PointsInput' import ProgressBar from './components/ProgressBar' import WeeklyDefault from './components/WeeklyDefault' import DatePicker from '../../../components/common/DatePicker' import ConfirmDialog from '../../../components/common/ConfirmDialog' import { useLayout } from '../../../components/pc/Layout' export default function Liberation() { const { setFullscreen } = useLayout() useLayoutEffect(() => { setFullscreen(true) return () => setFullscreen(false) }, [setFullscreen]) const [liberationType, setLiberationType] = useState('genesis') // 'genesis' | 'destiny' const genesisImg = useQuery({ queryKey: ['image', '제네시스 스태프'], queryFn: () => api('/api/images/' + encodeURIComponent('제네시스 스태프')).catch(() => null), staleTime: Infinity, }) const destinyImg = useQuery({ queryKey: ['image', '데스티니 스태프'], queryFn: () => api('/api/images/' + encodeURIComponent('데스티니 스태프')).catch(() => null), staleTime: Infinity, }) const calcMode = useLiberationStore((s) => s.calcMode) const state = useLiberationStore((s) => s[s.calcMode]) const setCalcMode = useLiberationStore((s) => s.setCalcMode) const updateSlot = useLiberationStore((s) => s.updateSlot) const resetSlot = useLiberationStore((s) => s.resetSlot) const setState = (updater) => updateSlot(updater) // 포인트 이월: 현재 퀘스트 required를 초과하면 자동으로 다음 퀘스트로 넘어감 const priorConsumed = GENESIS_CHAPTERS .slice(0, state.startChapter) .reduce((s, c) => s + c.required, 0) let cascadeIdx = state.startChapter let cascadeRemain = state.currentPoints let cascadeConsumed = 0 while (cascadeIdx < GENESIS_CHAPTERS.length && cascadeRemain >= GENESIS_CHAPTERS[cascadeIdx].required) { cascadeConsumed += GENESIS_CHAPTERS[cascadeIdx].required cascadeRemain -= GENESIS_CHAPTERS[cascadeIdx].required cascadeIdx++ } const initialAccumulated = priorConsumed + cascadeConsumed + cascadeRemain const alreadyDone = initialAccumulated >= GENESIS_TOTAL const weeklyEarn = calcWeekPoints(state.weekly) const remaining = Math.max(GENESIS_TOTAL - initialAccumulated, 0) const doneEarn = calcDoneEarn(state.weekly) const monthlyEarn = calcMonthlyEarn(state.weekly) const monthlyDoneThisMonth = !!state.weekly.blackMage?.done // 주차별 모드 헤더 합산 (검은 마법사는 월별 슬롯 1회만 카운트) const headerWeekly = calcMode === 'weekly' ? (state.schedulerWeeks || []).reduce((s, w) => s + calcWeekPoints(w.config), 0) : weeklyEarn const headerMonthly = (() => { if (calcMode !== 'weekly') return monthlyEarn const sw = state.schedulerWeeks || [] if (!state.startDate) return 0 const claimed = {} sw.forEach((w, idx) => { const diff = w.config.blackMage?.difficulty if (!diff || diff === 'none') return const r = getSchedulerWeekRange(state.startDate, idx + 1) const months = [r.start.format('YYYY-MM'), r.end.format('YYYY-MM')] for (const m of months) { if (!(m in claimed)) { claimed[m] = bossEarn(MONTHLY_BOSSES[0], w.config.blackMage) return } } }) return Object.values(claimed).reduce((s, v) => s + v, 0) })() const completionDate = useMemo( () => computeCompletionDate({ calcMode, state, alreadyDone, remaining, weeklyEarn, doneEarn, monthlyEarn, monthlyDoneThisMonth, }), [calcMode, state, alreadyDone, remaining, weeklyEarn, doneEarn, monthlyEarn, monthlyDoneThisMonth], ) const isDone = completionDate !== null const [resetOpen, setResetOpen] = useState(false) const doReset = () => { resetSlot() setResetOpen(false) } return (