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}
+
)
})}
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()
}