diff --git a/frontend/package-lock.json b/frontend/package-lock.json index fabdfd9..0a03a0c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -12,6 +12,7 @@ "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", "@tanstack/react-query": "^5.91.0", + "dayjs": "^1.11.20", "framer-motion": "^12.23.22", "react": "^19.2.4", "react-dom": "^19.2.4", @@ -1522,6 +1523,12 @@ "dev": true, "license": "MIT" }, + "node_modules/dayjs": { + "version": "1.11.20", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz", + "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", diff --git a/frontend/package.json b/frontend/package.json index d942e4a..ad3957c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,6 +14,7 @@ "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", "@tanstack/react-query": "^5.91.0", + "dayjs": "^1.11.20", "framer-motion": "^12.23.22", "react": "^19.2.4", "react-dom": "^19.2.4", diff --git a/frontend/src/features/liberation/Liberation.jsx b/frontend/src/features/liberation/Liberation.jsx index 7f5c7d3..7df661b 100644 --- a/frontend/src/features/liberation/Liberation.jsx +++ b/frontend/src/features/liberation/Liberation.jsx @@ -1,4 +1,5 @@ import { useState, useMemo, useEffect } from 'react' +import dayjs from 'dayjs' import { GENESIS_CHAPTERS, GENESIS_TOTAL, @@ -7,6 +8,7 @@ import { calcPoints, addWeeks, formatDate, + todayKST, } from './data' import WeekCard from './components/WeekCard' import QuestSelector from './components/QuestSelector' @@ -26,7 +28,7 @@ function makeEmptyWeek(startDate) { } }) return { - startDate: startDate.toISOString(), + startDate: dayjs(startDate).toISOString(), bosses, blackMage: { enabled: false, @@ -62,7 +64,7 @@ export default function Liberation() { return { startChapter: 0, currentPoints: 0, - weeks: [makeEmptyWeek(new Date())], + weeks: [makeEmptyWeek(todayKST())], } }) @@ -76,7 +78,9 @@ export default function Liberation() { const startConsumedBefore = GENESIS_CHAPTERS .slice(0, state.startChapter) .reduce((s, c) => s + c.required, 0) - let totalAccumulated = startConsumedBefore + state.currentPoints + const currentChapterCap = GENESIS_CHAPTERS[state.startChapter]?.required ?? 0 + const clampedCurrent = Math.min(state.currentPoints, currentChapterCap) + let totalAccumulated = startConsumedBefore + clampedCurrent for (const week of state.weeks) { const earned = calcWeekPoints(week) @@ -108,11 +112,19 @@ export default function Liberation() { return result }, [state]) + const initialCap = GENESIS_CHAPTERS[state.startChapter]?.required ?? 0 + const initialClamped = Math.min(state.currentPoints, initialCap) + const initialAccumulated = GENESIS_CHAPTERS + .slice(0, state.startChapter) + .reduce((s, c) => s + c.required, 0) + initialClamped + const alreadyDone = initialAccumulated >= GENESIS_TOTAL const completedWeekIdx = progressByWeek.findIndex((w) => w.completed) - const isDone = completedWeekIdx >= 0 - const completionDate = isDone - ? addWeeks(new Date(state.weeks[completedWeekIdx].startDate), 1) - : null + const isDone = alreadyDone || completedWeekIdx >= 0 + const completionDate = alreadyDone + ? todayKST() + : completedWeekIdx >= 0 + ? addWeeks(state.weeks[completedWeekIdx].startDate, 1) + : null const updateWeek = (idx, newWeekData) => { setState((prev) => ({ @@ -124,10 +136,10 @@ export default function Liberation() { const addWeek = () => { setState((prev) => { const lastWeek = prev.weeks[prev.weeks.length - 1] - const nextStart = addWeeks(new Date(lastWeek.startDate), 1) + const nextStart = addWeeks(lastWeek.startDate, 1) return { ...prev, - weeks: [...prev.weeks, { ...lastWeek, startDate: nextStart.toISOString() }], + weeks: [...prev.weeks, { ...lastWeek, startDate: dayjs(nextStart).toISOString() }], } }) } @@ -141,16 +153,15 @@ export default function Liberation() { setState({ startChapter: 0, currentPoints: 0, - weeks: [makeEmptyWeek(new Date())], + weeks: [makeEmptyWeek(todayKST())], }) } const setFirstWeekDate = (dateStr) => { - const d = new Date(dateStr) setState((prev) => { const weeks = prev.weeks.map((w, i) => ({ ...w, - startDate: addWeeks(d, i).toISOString(), + startDate: dayjs(addWeeks(dateStr, i)).toISOString(), })) return { ...prev, weeks } }) @@ -163,9 +174,43 @@ export default function Liberation() { return (
+ + {/* 현재 진행 상태 입력 */} +
+
현재 진행 상태
+ +
+
+ + +
+ +
+ + setState((prev) => ({ ...prev, startChapter: idx }))} + /> +
+ +
+ + setState((prev) => ({ ...prev, currentPoints: n }))} + className="w-full h-12 rounded-lg border border-white/10 bg-gray-950 px-3 text-base text-right tabular-nums outline-none focus:border-emerald-500/50 hover:border-white/20 transition" + /> +
+
+
) } diff --git a/frontend/src/features/liberation/components/ProgressBar.jsx b/frontend/src/features/liberation/components/ProgressBar.jsx index 7954d73..4b83726 100644 --- a/frontend/src/features/liberation/components/ProgressBar.jsx +++ b/frontend/src/features/liberation/components/ProgressBar.jsx @@ -5,20 +5,15 @@ function formatKoreanDate(s) { return `${y}년 ${m}월 ${d}일` } -export default function ProgressBar({ totalAccumulated, completionDate }) { - const chapterStates = [] - let remaining = totalAccumulated - for (const c of GENESIS_CHAPTERS) { - if (remaining >= c.required) { - chapterStates.push({ chapter: c, status: 'done', current: c.required }) - remaining -= c.required - } else if (remaining > 0) { - chapterStates.push({ chapter: c, status: 'active', current: remaining }) - remaining = 0 - } else { - chapterStates.push({ chapter: c, status: 'pending', current: 0 }) +export default function ProgressBar({ startChapter, currentPoints, completionDate }) { + const chapterStates = GENESIS_CHAPTERS.map((c) => { + if (c.idx < startChapter) return { chapter: c, status: 'done', current: c.required } + if (c.idx === startChapter) { + const filled = Math.min(currentPoints, c.required) + return { chapter: c, status: filled > 0 ? 'active' : 'active', current: filled } } - } + return { chapter: c, status: 'pending', current: 0 } + }) const renderSegment = ({ chapter, status, current }) => { const pct = (current / chapter.required) * 100 diff --git a/frontend/src/features/liberation/components/QuestSelector.jsx b/frontend/src/features/liberation/components/QuestSelector.jsx index b5775f0..c547768 100644 --- a/frontend/src/features/liberation/components/QuestSelector.jsx +++ b/frontend/src/features/liberation/components/QuestSelector.jsx @@ -1,9 +1,9 @@ import { useState, useEffect, useRef } from 'react' -import { GENESIS_CHAPTERS, QUEST_BTBOSS_IMAGE_BASE } from '../data' +import { GENESIS_CHAPTERS, QUEST_BOSS_IMAGE_BASE } from '../data' /** * 진행 중인 퀘스트 드롭다운 - * - 선택된 옵션과 옵션 리스트 모두 btboss 이미지로 표시 + * - 보스 초상화 + 이름 텍스트 */ export default function QuestSelector({ value, onChange }) { const [open, setOpen] = useState(false) @@ -25,25 +25,30 @@ export default function QuestSelector({ value, onChange }) { {open && ( -
+
{GENESIS_CHAPTERS.map((chapter) => { const isSelected = chapter.idx === value return ( @@ -51,15 +56,22 @@ export default function QuestSelector({ value, onChange }) { key={chapter.idx} type="button" onClick={() => { onChange(chapter.idx); setOpen(false) }} - className={`relative transition ${ - isSelected ? 'scale-105' : 'opacity-60 hover:opacity-100' + className={`w-full flex items-center gap-3 px-2 py-1.5 transition ${ + isSelected ? 'bg-emerald-500/10' : 'hover:bg-white/5' }`} > - {chapter.boss} +
+ +
+ + {chapter.boss} + ) })} diff --git a/frontend/src/features/liberation/data.js b/frontend/src/features/liberation/data.js index 5f25f60..bdbc50a 100644 --- a/frontend/src/features/liberation/data.js +++ b/frontend/src/features/liberation/data.js @@ -96,25 +96,29 @@ export function calcPoints(basePoints, partySize) { // 목요일 기준 주차 계산 (KST) // 이번 주 목요일 자정 = 이번 주의 시작 export function getThursdayOfWeek(date) { - const d = new Date(date) - const day = d.getDay() // 0=일, 4=목 - // 직전 목요일 찾기 (오늘이 목요일이면 오늘) + const d = dayjs(date).tz(KST) + const day = d.day() // 0=일, 4=목 const diff = (day - 4 + 7) % 7 - d.setDate(d.getDate() - diff) - d.setHours(0, 0, 0, 0) - return d + return d.subtract(diff, 'day').startOf('day').toDate() } +import dayjs from 'dayjs' +import utc from 'dayjs/plugin/utc' +import timezone from 'dayjs/plugin/timezone' + +dayjs.extend(utc) +dayjs.extend(timezone) + +const KST = 'Asia/Seoul' + export function formatDate(date) { - const d = new Date(date) - const y = d.getFullYear() - const m = String(d.getMonth() + 1).padStart(2, '0') - const day = String(d.getDate()).padStart(2, '0') - return `${y}-${m}-${day}` + return dayjs(date).tz(KST).format('YYYY-MM-DD') } export function addWeeks(date, weeks) { - const d = new Date(date) - d.setDate(d.getDate() + weeks * 7) - return d + return dayjs(date).tz(KST).add(weeks, 'week').toDate() +} + +export function todayKST() { + return dayjs().tz(KST).startOf('day').toDate() }