From 1163f77266be68189046efc80acf9d6058ec049e Mon Sep 17 00:00:00 2001 From: caadiq Date: Tue, 14 Apr 2026 11:52:44 +0900 Subject: [PATCH] =?UTF-8?q?=ED=95=B4=EB=B0=A9=20=EC=99=84=EB=A3=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=8B=9C=EB=AE=AC=EB=A0=88=EC=9D=B4=EC=85=98=20?= =?UTF-8?q?=EA=B8=B0=EB=B0=98=20=EB=A1=9C=EC=A7=81=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EA=B5=90=EC=B2=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 포인트 이월(캐스케이드) 및 주간/월간 리셋을 정확히 반영하기 위해 weeksNeeded 공식 대신 이벤트 시뮬레이션으로 완료일을 계산. - 시작일 당일: (주간 - 완료된 주간 몫) + (이번 달 월간, 검은 마법사 미완료 시) - 이후 매주 목요일에 주간, 매월 1일에 월간 적립 - 누적이 잔여 흔적을 처음 넘는 이벤트 날짜가 해방일 메이플로드/츄츄지지 계산기 결과와 동일하게 동작함을 확인. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/features/liberation/Liberation.jsx | 91 +++++++++++-------- 1 file changed, 51 insertions(+), 40 deletions(-) diff --git a/frontend/src/features/liberation/Liberation.jsx b/frontend/src/features/liberation/Liberation.jsx index 5252762..15f9d40 100644 --- a/frontend/src/features/liberation/Liberation.jsx +++ b/frontend/src/features/liberation/Liberation.jsx @@ -146,59 +146,70 @@ export default function Liberation() { return result }, [state]) - const currentChapterCap = GENESIS_CHAPTERS[state.startChapter]?.required ?? 0 - const currentChapterFilled = Math.min(state.currentPoints, currentChapterCap) - const initialAccumulated = GENESIS_CHAPTERS + // 포인트 이월 계산: 현재 퀘스트의 required를 초과하면 자동으로 다음 퀘스트로 넘어감 + const priorConsumed = GENESIS_CHAPTERS .slice(0, state.startChapter) - .reduce((s, c) => s + c.required, 0) + currentChapterFilled + .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 monthlyDoneEarn = calcMonthlyDoneEarn(state.weekly) + const monthlyDoneThisMonth = !!state.weekly.blackMage?.done - function monthResetsBetween(start, end) { - let count = 0 - let cursor = start.add(1, 'month').startOf('month') - while (!cursor.isAfter(end)) { - count++ - cursor = cursor.add(1, 'month').startOf('month') + // 날짜 이벤트 시뮬레이션으로 해방일 계산 + 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 }) + + // 다음 목요일부터 매주 주간 적립 + 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') } - return count - } - const startKST = dayjs(state.startDate).tz('Asia/Seoul') - const currentMonthBMAvailable = monthlyEarn > 0 && monthlyDoneEarn === 0 ? monthlyEarn : 0 - const adjustedRemaining = remaining + doneEarn + monthlyDoneEarn - let weeksNeeded = null - if (!alreadyDone && (weeklyEarn > 0 || monthlyEarn > 0)) { - for (let N = 1; N <= 520; N++) { - const weeklyCum = N * weeklyEarn - const endKST = startKST.add(N, 'week') - const monthlyCum = currentMonthBMAvailable + monthResetsBetween(startKST, endKST) * monthlyEarn - if (weeklyCum + monthlyCum >= adjustedRemaining) { - weeksNeeded = N - break + // 다음 달 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') } } + + events.sort((a, b) => a.date.diff(b.date)) + let cumulative = 0 + for (const e of events) { + cumulative += e.amount + if (cumulative >= remaining) return e.date.toDate() + } + return null } - // 시작 날짜 기준 다음(또는 당일) 목요일 - const startDay = dayjs(state.startDate).tz('Asia/Seoul') - const dow = startDay.day() // 0=일 ... 4=목 - const daysToThu = dow <= 4 ? 4 - dow : 11 - dow - const upcomingThursday = startDay.add(daysToThu, 'day').startOf('day') - - const isDone = alreadyDone || weeksNeeded !== null - // 시작일의 주 = 1주차, 다음 목요일 = 2주차 시작 - // 완료일 = N주차의 목요일 = upcomingThursday + (weeksNeeded - 2) * 7일 - const completionDate = alreadyDone - ? todayKST() - : weeksNeeded !== null - ? upcomingThursday.add(weeksNeeded - 2, 'week').toDate() - : null + const completionDate = computeCompletionDate() + const isDone = completionDate !== null const updateWeek = (idx, newWeekData) => { setState((prev) => ({