diff --git a/frontend/src/features/liberation/pc/Destiny.jsx b/frontend/src/features/liberation/pc/Destiny.jsx index 8a55665..51769b1 100644 --- a/frontend/src/features/liberation/pc/Destiny.jsx +++ b/frontend/src/features/liberation/pc/Destiny.jsx @@ -6,7 +6,7 @@ import { DESTINY_BOSS_IMAGE_BASE, formatDate, } from '../data' -import { useLiberationStore } from '../store' +import { useLiberationStore, makeEmptyDestinyWeekly } from '../store' import { calcWeekPoints } from '../utils' import ProgressBar from './components/ProgressBar' import QuestSelector from './components/QuestSelector' @@ -22,6 +22,9 @@ export default function Destiny() { const setState = (updater) => updateSlot(updater) const weeklyEarn = calcWeekPoints(state.weekly, DESTINY_BOSSES) + const headerWeekly = calcMode === 'weekly' + ? (state.schedulerWeeks || []).reduce((s, w) => s + calcWeekPoints(w.config, DESTINY_BOSSES), 0) + : weeklyEarn return ( <> @@ -34,8 +37,8 @@ export default function Destiny() { }} > {[ - { key: 'simple', label: '단순 계산' }, - { key: 'weekly', label: '주차별 계산' }, + { key: 'simple', label: '일반' }, + { key: 'weekly', label: '주차별' }, ].map((t) => { const active = calcMode === t.key return ( @@ -126,30 +129,19 @@ export default function Destiny() { - {calcMode === 'simple' ? ( - setState((prev) => ({ ...prev, weekly: w }))} - totalWeekly={weeklyEarn} - remaining={0} - mode="simple" - hasScheduler={false} - /> - ) : ( -
-
주차별 계산 준비 중
-
단순 계산 탭을 이용해주세요.
-
- )} + setState((prev) => ({ ...prev, weekly: w }))} + totalWeekly={headerWeekly} + remaining={0} + mode={calcMode} + startDate={state.startDate} + weeks={state.schedulerWeeks} + onChangeWeeks={(w) => setState((prev) => ({ ...prev, schedulerWeeks: w }))} + /> ) } diff --git a/frontend/src/features/liberation/pc/Genesis.jsx b/frontend/src/features/liberation/pc/Genesis.jsx index d3d2be7..9b45394 100644 --- a/frontend/src/features/liberation/pc/Genesis.jsx +++ b/frontend/src/features/liberation/pc/Genesis.jsx @@ -9,7 +9,7 @@ import { LIBERATION_BOSS_IMAGE_BASE, formatDate, } from '../data' -import { useLiberationStore } from '../store' +import { useLiberationStore, makeEmptyWeekly } from '../store' import { bossEarn, calcWeekPoints, @@ -103,8 +103,8 @@ export default function Genesis() { }} > {[ - { key: 'simple', label: '단순 계산' }, - { key: 'weekly', label: '주차별 계산' }, + { key: 'simple', label: '일반' }, + { key: 'weekly', label: '주차별' }, ].map((t) => { const active = calcMode === t.key return ( @@ -198,6 +198,7 @@ export default function Genesis() { bosses={WEEKLY_BOSSES} monthlyBosses={MONTHLY_BOSSES} imageBase={LIBERATION_BOSS_IMAGE_BASE} + makeEmptyConfig={makeEmptyWeekly} weekly={state.weekly} onChange={(w) => setState((prev) => ({ ...prev, weekly: w }))} totalWeekly={headerWeekly} @@ -232,7 +233,7 @@ export default function Genesis() { onClose={() => setResetOpen(false)} onConfirm={doReset} title="전체 초기화" - description={`${calcMode === 'simple' ? '단순 계산' : '주차별 계산'} 모드의 입력을 모두 초기화하시겠습니까?\n\n시작 날짜, 현재 진행 상태, 주간 보스 설정이 모두 초기값으로 되돌아갑니다.\n다른 모드의 값은 유지됩니다.`} + description={`${calcMode === 'simple' ? '일반' : '주차별'} 모드의 입력을 모두 초기화하시겠습니까?\n\n시작 날짜, 현재 진행 상태, 주간 보스 설정이 모두 초기값으로 되돌아갑니다.\n다른 모드의 값은 유지됩니다.`} confirmText="초기화" destructive /> diff --git a/frontend/src/features/liberation/pc/components/WeeklyDefault.jsx b/frontend/src/features/liberation/pc/components/WeeklyDefault.jsx index e4e3bf2..f04bc00 100644 --- a/frontend/src/features/liberation/pc/components/WeeklyDefault.jsx +++ b/frontend/src/features/liberation/pc/components/WeeklyDefault.jsx @@ -61,6 +61,7 @@ export function BossRow({ boss, sel, onChange, imageBase, monthly = false, showD type="button" disabled={disabled} onClick={() => onChange({ done: !sel.done })} + title="이번 주 해당 난이도를 이미 클리어했는지 여부" className="shrink-0 w-20 rounded-md h-8 text-xs font-semibold border" style={disabled ? { borderColor: 'var(--panel-border)', @@ -85,6 +86,7 @@ export default function WeeklyDefault({ bosses, monthlyBosses = [], imageBase, + makeEmptyConfig, weekly, onChange, totalWeekly, @@ -168,6 +170,10 @@ export default function WeeklyDefault({ ) : ( - {boss.name} + {boss.name}
{ onChange({ ...config, bosses: { ...config.bosses, [key]: { ...config.bosses[key], ...patch } } }) } @@ -61,7 +59,7 @@ function WeekEditor({ config, onChange, isCurrent, monthlyLockedByWeek }) { return (
- {WEEKLY_BOSSES.map((boss, i) => ( + {bosses.map((boss, i) => (
0 ? 'border-t' : ''} @@ -71,23 +69,27 @@ function WeekEditor({ config, onChange, isCurrent, monthlyLockedByWeek }) { boss={boss} sel={config.bosses[boss.key]} onChange={(patch) => updateBoss(boss.key, patch)} + imageBase={imageBase} showDone={isCurrent} />
))} -
- -
- {blackmageLocked && ( + {monthlyBoss && ( +
+ +
+ )} + {monthlyBoss && blackmageLocked && (
0 ? weeksProp - : [{ id: 1, config: makeEmptyWeekly() }] + : [{ id: 1, config: makeEmptyConfig() }] const setWeeks = (updater) => { const next = typeof updater === 'function' ? updater(weeks) : updater onChangeWeeks?.(next) @@ -114,13 +124,13 @@ export default function WeeklyScheduler({ startDate, weeks: weeksProp, onChangeW const id = nextId() setWeeks((prev) => { const last = prev[prev.length - 1] - const base = last ? JSON.parse(JSON.stringify(last.config)) : makeEmptyWeekly() + const base = last ? JSON.parse(JSON.stringify(last.config)) : makeEmptyConfig() // done 상태는 복사하지 않음 Object.keys(base.bosses).forEach((k) => { base.bosses[k].done = false }) if (base.blackMage) base.blackMage.done = false - // 새 주차의 달에 이미 검은 마법사가 배정되어 있으면 복사된 검은마법사는 초기화 - if (startDate && base.blackMage?.difficulty && base.blackMage.difficulty !== 'none') { + // 월간 보스가 이미 같은 달에 배정되어 있으면 새 주차의 월간은 초기화 + if (monthlyBoss && startDate && base.blackMage?.difficulty && base.blackMage.difficulty !== 'none') { const newIdx = prev.length + 1 const newMonth = getWeekRange(startDate, newIdx).start.format('YYYY-MM') const existsInSameMonth = prev.some((p, i) => { @@ -146,9 +156,9 @@ export default function WeeklyScheduler({ startDate, weeks: weeksProp, onChangeW setWeeks((prev) => prev.map((w) => (w.id === id ? { ...w, config } : w))) } - // 검은 마법사 월별 슬롯 배정: 각 주차가 겹치는 달 중 하나를 선점 + // 월간 보스 슬롯 배정: 각 주차가 겹치는 달 중 하나를 선점 const monthlyLocks = (() => { - if (!startDate) return {} + if (!monthlyBoss || !startDate) return {} const claimed = {} // month -> weekNum (1-based) weeks.forEach((w, idx) => { const diff = w.config.blackMage?.difficulty @@ -166,9 +176,7 @@ export default function WeeklyScheduler({ startDate, weeks: weeksProp, onChangeW weeks.forEach((w, idx) => { const r = getWeekRange(startDate, idx + 1) const months = [r.start.format('YYYY-MM'), r.end.format('YYYY-MM')] - // 본인이 한 달이라도 차지했으면 잠그지 않음 if (months.some((m) => claimed[m] === idx + 1)) return - // 겹치는 달이 모두 다른 주차에 점유되었으면 잠금 if (months.every((m) => m in claimed)) { locks[idx] = claimed[months[0]] ?? claimed[months[1]] } @@ -181,8 +189,7 @@ export default function WeeklyScheduler({ startDate, weeks: weeksProp, onChangeW {weeks.map((w, idx) => { const n = idx + 1 const isOpen = expanded === w.id - const isCurrent = idx === 0 // 임시: 첫 번째가 현재 주차 (실제 연결 시 날짜 기반) - // 검은마법사 잠금 판정은 아래 사전 계산된 monthlyLocks 사용 + const isCurrent = idx === 0 const monthlyLockedByWeek = monthlyLocks[idx] ?? null return (
- {WEEKLY_BOSSES.map((b) => ( - + {bosses.map((b) => ( + ))} - + {monthlyBoss && ( + + )}
{(() => { - const weeklySum = calcWeeklySum(w.config) - const monthlySum = monthlyLockedByWeek != null ? 0 : bossEarn(MONTHLY_BOSSES[0], w.config.blackMage) + const weeklySum = calcWeeklySum(w.config, bosses) + const monthlySum = !monthlyBoss || monthlyLockedByWeek != null + ? 0 + : bossEarn(monthlyBoss, w.config.blackMage) return (
+{weeklySum}
@@ -284,6 +300,9 @@ export default function WeeklyScheduler({ startDate, weeks: weeksProp, onChangeW onChange={(c) => updateWeek(w.id, c)} isCurrent={isCurrent} monthlyLockedByWeek={monthlyLockedByWeek} + bosses={bosses} + monthlyBoss={monthlyBoss} + imageBase={imageBase} />
diff --git a/frontend/src/features/liberation/store.js b/frontend/src/features/liberation/store.js index 9733091..488ac2b 100644 --- a/frontend/src/features/liberation/store.js +++ b/frontend/src/features/liberation/store.js @@ -38,6 +38,7 @@ function makeInitialDestinySlot() { currentPoints: 0, startDate: dayjs(todayKST()).toISOString(), weekly: makeEmptyDestinyWeekly(), + schedulerWeeks: [{ id: 1, config: makeEmptyDestinyWeekly() }], } } @@ -83,11 +84,18 @@ export const useLiberationStore = create(persist( }), { name: 'maple-liberation', - version: 1, + version: 2, migrate: (persisted) => { if (!persisted) return persisted - // v0→v1: 데스티니 슬롯에 weekly 필드가 없으면 빈 값으로 채움 - const fill = (slot) => slot ? { ...slot, weekly: slot.weekly || makeEmptyDestinyWeekly() } : slot + // 데스티니 슬롯에 weekly/schedulerWeeks 필드가 없으면 빈 값으로 채움 + const fill = (slot) => { + if (!slot) return slot + return { + ...slot, + weekly: slot.weekly || makeEmptyDestinyWeekly(), + schedulerWeeks: slot.schedulerWeeks || [{ id: 1, config: makeEmptyDestinyWeekly() }], + } + } return { ...persisted, destinySimple: fill(persisted.destinySimple),