From 6243dea01e756eb8a42a2ebdd2851fad17bf6450 Mon Sep 17 00:00:00 2001 From: caadiq Date: Tue, 14 Apr 2026 19:33:31 +0900 Subject: [PATCH] =?UTF-8?q?=EC=A3=BC=EC=B0=A8=EB=B3=84=20=EB=AA=A8?= =?UTF-8?q?=EB=93=9C=20=ED=95=B4=EB=B0=A9=EC=9D=BC=20=EA=B3=84=EC=82=B0=20?= =?UTF-8?q?+=20=ED=97=A4=EB=8D=94/=EC=A3=BC=EC=B0=A8=20=ED=96=89=20?= =?UTF-8?q?=ED=91=9C=EC=8B=9C=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - weekly 모드 시뮬레이션: 1주차는 시작일 당일에 (주간-완료) 적립, 2주차 이후 매 목요일에 해당 주차 설정의 주간 합 적립 - 검은 마법사: 슬롯 배정에 따라 1회씩 적립(이미 done이면 제외) - 마지막 주차 이후로는 마지막 주차 설정을 매주/매월 반복 적용 - 헤더: 주간(초록) + 월간(노랑) / 6500 형식, 모드별 합산 - 주차 행 우측: 주간/월간을 두 줄로 색상 분리 표시 (월간은 있을 때만) Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/features/liberation/Liberation.jsx | 154 +++++++++++++++--- .../liberation/components/WeeklyDefault.jsx | 13 +- .../liberation/components/WeeklyScheduler.jsx | 30 +++- 3 files changed, 167 insertions(+), 30 deletions(-) diff --git a/frontend/src/features/liberation/Liberation.jsx b/frontend/src/features/liberation/Liberation.jsx index 8d2086e..66af2b6 100644 --- a/frontend/src/features/liberation/Liberation.jsx +++ b/frontend/src/features/liberation/Liberation.jsx @@ -203,35 +203,137 @@ export default function Liberation() { 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 = (() => { + const start = dayjs(state.startDate).tz('Asia/Seoul').startOf('day') + const dow = start.day() + const daysToNextThu = dow < 4 ? 4 - dow : 11 - dow + const nextThu = start.add(daysToNextThu, 'day') + if (idx + 1 === 1) return { start, end: nextThu.subtract(1, 'day') } + const ws = nextThu.add((idx + 1 - 2) * 7, 'day') + return { start: ws, end: ws.add(6, 'day') } + })() + 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) + })() + // 날짜 이벤트 시뮬레이션으로 해방일 계산 function computeCompletionDate() { if (alreadyDone) return todayKST() - if (weeklyEarn === 0 && monthlyEarn === 0) return null if (remaining <= 0) return dayjs(state.startDate).tz('Asia/Seoul').startOf('day').toDate() const startKST = dayjs(state.startDate).tz('Asia/Seoul').startOf('day') const events = [] - // 시작일 당일: (주간 - 완료된 주간) + (이번 달 월간, 아직 안 잡았을 때) - const day0Weekly = Math.max(weeklyEarn - doneEarn, 0) - const day0Monthly = monthlyEarn > 0 && !monthlyDoneThisMonth ? monthlyEarn : 0 - events.push({ date: startKST, amount: day0Weekly + day0Monthly }) + if (calcMode === 'weekly') { + // 주차별 모드: 각 주차의 설정에 따라 적립 + const sw = state.schedulerWeeks || [] + if (sw.length === 0) return null + // 주간 보스: 시작일 당일 = 1주차 설정, 이후 매 목요일 = 2주차/3주차 설정 + const dow = startKST.day() + const daysToNextThu = dow < 4 ? 4 - dow : 11 - dow + // 1주차: 시작일 당일에 (주간 - done) 적립 + const week1Cfg = sw[0]?.config || makeEmptyWeekly() + const w1Weekly = calcWeekPoints(week1Cfg) + const w1Done = calcDoneEarn(week1Cfg) + events.push({ date: startKST, amount: Math.max(w1Weekly - w1Done, 0) }) + // 2주차 이후: 각 목요일에 해당 주차 설정의 주간 합 적립 + // 마지막 주차 이후로는 마지막 주차 설정을 반복 적용 + let nextThu = startKST.add(daysToNextThu, 'day') + for (let i = 1; i < 520; i++) { + const cfg = sw[i]?.config || sw[sw.length - 1]?.config || makeEmptyWeekly() + events.push({ date: nextThu, amount: calcWeekPoints(cfg) }) + nextThu = nextThu.add(1, 'week') + } - // 다음 목요일부터 매주 주간 적립 - const dow = startKST.day() - const daysToNextThu = dow < 4 ? 4 - dow : 11 - dow - let nextThu = startKST.add(daysToNextThu, 'day') - for (let i = 0; i < 520; i++) { - events.push({ date: nextThu, amount: weeklyEarn }) - nextThu = nextThu.add(1, 'week') - } + // 검은 마법사: 슬롯 배정에 따라 해당 주차의 첫날(or 1주차이면 시작일)에 적립 + const claimed = {} // monthKey -> { weekIdx, earn, doneAlready } + sw.forEach((w, i) => { + const diff = w.config.blackMage?.difficulty + if (!diff || diff === 'none') return + const range = getSchedulerWeekRange(state.startDate, i + 1) + const months = [range.start.format('YYYY-MM'), range.end.format('YYYY-MM')] + for (const m of months) { + if (!(m in claimed)) { + claimed[m] = { + weekIdx: i, + earn: bossEarn(MONTHLY_BOSSES[0], w.config.blackMage), + done: !!w.config.blackMage.done, + } + return + } + } + }) + Object.entries(claimed).forEach(([, info]) => { + if (info.done) return + const wIdx = info.weekIdx + // 1주차면 시작일, 그 외엔 해당 주차의 시작 목요일 + const date = wIdx === 0 + ? startKST + : startKST.add(daysToNextThu + (wIdx - 1) * 7, 'day') + events.push({ date, amount: info.earn }) + }) - // 다음 달 1일부터 매월 월간 적립 - if (monthlyEarn > 0) { - let nextMonth = startKST.add(1, 'month').startOf('month') - for (let i = 0; i < 120; i++) { - events.push({ date: nextMonth, amount: monthlyEarn }) - nextMonth = nextMonth.add(1, 'month') + // 마지막 주차 이후로는 마지막 주차의 검은 마법사 설정을 매월 반복 적용 + const lastCfg = sw[sw.length - 1]?.config + const lastBmEarn = lastCfg ? bossEarn(MONTHLY_BOSSES[0], lastCfg.blackMage) : 0 + if (lastBmEarn > 0) { + const lastWeekStart = sw.length === 1 + ? startKST + : startKST.add(daysToNextThu + (sw.length - 2) * 7, 'day') + const claimedMonths = new Set(Object.keys(claimed)) + let cursor = lastWeekStart.add(1, 'month').startOf('month') + for (let i = 0; i < 120; i++) { + const m = cursor.format('YYYY-MM') + if (!claimedMonths.has(m)) { + events.push({ date: cursor, amount: lastBmEarn }) + } + cursor = cursor.add(1, 'month') + } + } + } else { + // 단순 계산 모드: 매주 동일 설정 + if (weeklyEarn === 0 && monthlyEarn === 0) return null + + // 시작일 당일: (주간 - 완료된 주간) + (이번 달 월간, 아직 안 잡았을 때) + const day0Weekly = Math.max(weeklyEarn - doneEarn, 0) + const day0Monthly = monthlyEarn > 0 && !monthlyDoneThisMonth ? monthlyEarn : 0 + events.push({ date: startKST, amount: day0Weekly + day0Monthly }) + + // 다음 목요일부터 매주 주간 적립 + const dow = startKST.day() + const daysToNextThu = dow < 4 ? 4 - dow : 11 - dow + let nextThu = startKST.add(daysToNextThu, 'day') + for (let i = 0; i < 520; i++) { + events.push({ date: nextThu, amount: weeklyEarn }) + nextThu = nextThu.add(1, 'week') + } + + // 다음 달 1일부터 매월 월간 적립 + if (monthlyEarn > 0) { + let nextMonth = startKST.add(1, 'month').startOf('month') + for (let i = 0; i < 120; i++) { + events.push({ date: nextMonth, amount: monthlyEarn }) + nextMonth = nextMonth.add(1, 'month') + } } } @@ -244,6 +346,16 @@ export default function Liberation() { return null } + function getSchedulerWeekRange(startDateStr, weekIdx) { + const start = dayjs(startDateStr).tz('Asia/Seoul').startOf('day') + const dow = start.day() + const daysToNextThu = dow < 4 ? 4 - dow : 11 - dow + const nextThu = start.add(daysToNextThu, 'day') + if (weekIdx === 1) return { start, end: nextThu.subtract(1, 'day') } + const ws = nextThu.add((weekIdx - 2) * 7, 'day') + return { start: ws, end: ws.add(6, 'day') } + } + const completionDate = computeCompletionDate() const isDone = completionDate !== null @@ -382,8 +494,8 @@ export default function Liberation() { setState((prev) => ({ ...prev, weekly: w }))} - totalWeekly={weeklyEarn} - totalMonthly={monthlyEarn} + totalWeekly={headerWeekly} + totalMonthly={headerMonthly} mode={calcMode} startDate={state.startDate} weeks={state.schedulerWeeks} diff --git a/frontend/src/features/liberation/components/WeeklyDefault.jsx b/frontend/src/features/liberation/components/WeeklyDefault.jsx index d9b411a..c1b11ed 100644 --- a/frontend/src/features/liberation/components/WeeklyDefault.jsx +++ b/frontend/src/features/liberation/components/WeeklyDefault.jsx @@ -82,13 +82,12 @@ export default function WeeklyDefault({ weekly, onChange, totalWeekly, totalMont
주간 보스 설정
-
- - 주간 획득 +{totalWeekly} - - - 월간 획득 +{totalMonthly} - +
+ {totalWeekly} + + + {totalMonthly} + / + 6500
diff --git a/frontend/src/features/liberation/components/WeeklyScheduler.jsx b/frontend/src/features/liberation/components/WeeklyScheduler.jsx index ea72d6c..9556f61 100644 --- a/frontend/src/features/liberation/components/WeeklyScheduler.jsx +++ b/frontend/src/features/liberation/components/WeeklyScheduler.jsx @@ -1,9 +1,22 @@ import { useState } from 'react' import { motion, AnimatePresence } from 'framer-motion' import dayjs from 'dayjs' -import { LIBERATION_BOSS_IMAGE_BASE, WEEKLY_BOSSES, MONTHLY_BOSSES } from '../data' +import { LIBERATION_BOSS_IMAGE_BASE, WEEKLY_BOSSES, MONTHLY_BOSSES, calcPoints } from '../data' import { BossRow } from './WeeklyDefault' +function bossEarn(boss, sel) { + if (!sel || !sel.difficulty || sel.difficulty === 'none') return 0 + const d = boss.difficulties.find((x) => x.key === sel.difficulty) + if (!d) return 0 + return calcPoints(d.points, sel.party) +} + +function calcWeeklySum(config) { + let sum = 0 + WEEKLY_BOSSES.forEach((b) => { sum += bossEarn(b, config.bosses[b.key]) }) + return sum +} + const KST = 'Asia/Seoul' // 주차별 날짜 범위 계산 (목요일 리셋, 1주차는 시작일부터 다음 목요일 전까지) @@ -219,9 +232,22 @@ export default function WeeklyScheduler({ startDate, weeks: weeksProp, onChangeW {WEEKLY_BOSSES.map((b) => ( ))} - +
+ {(() => { + const weeklySum = calcWeeklySum(w.config) + const monthlySum = monthlyLockedByWeek != null ? 0 : bossEarn(MONTHLY_BOSSES[0], w.config.blackMage) + return ( +
+
+{weeklySum}
+ {monthlySum > 0 && ( +
+{monthlySum}
+ )} +
+ ) + })()} +