diff --git a/frontend/src/features/liberation/Liberation.jsx b/frontend/src/features/liberation/Liberation.jsx index 66af2b6..eba1c99 100644 --- a/frontend/src/features/liberation/Liberation.jsx +++ b/frontend/src/features/liberation/Liberation.jsx @@ -1,4 +1,4 @@ -import { useState, useMemo, useEffect } from 'react' +import { useState, useEffect } from 'react' import { useQuery } from '@tanstack/react-query' import dayjs from 'dayjs' import { api } from '../../api/client' @@ -8,11 +8,9 @@ import { WEEKLY_BOSSES, MONTHLY_BOSSES, calcPoints, - addWeeks, formatDate, todayKST, } from './data' -import WeekCard from './components/WeekCard' import QuestSelector from './components/QuestSelector' import PointsInput from './components/PointsInput' import ProgressBar from './components/ProgressBar' @@ -23,13 +21,6 @@ import { useLayout } from '../../components/Layout' const STORAGE_KEY = 'maple-liberation' -function makeEmptyWeek(startDate) { - return { - startDate: dayjs(startDate).toISOString(), - ...makeEmptyWeekly(), - } -} - function makeEmptyWeekly() { const bosses = {} WEEKLY_BOSSES.forEach((b) => { @@ -69,9 +60,6 @@ function calcMonthlyEarn(weekData) { return bossEarn(MONTHLY_BOSSES[0], weekData.blackMage) } -function calcMonthlyDoneEarn(weekData) { - return weekData.blackMage?.done ? bossEarn(MONTHLY_BOSSES[0], weekData.blackMage) : 0 -} export default function Liberation() { const { setFullscreen } = useLayout() @@ -98,8 +86,6 @@ export default function Liberation() { currentPoints: 0, startDate: dayjs(todayKST()).toISOString(), weekly: makeEmptyWeekly(), - weekOverrides: {}, - weeks: [makeEmptyWeek(todayKST())], schedulerWeeks: [{ id: 1, config: makeEmptyWeekly() }], }) @@ -112,11 +98,9 @@ export default function Liberation() { if (!parsed.calcMode) { if (!parsed.weekly) parsed.weekly = makeEmptyWeekly() if (!parsed.startDate) parsed.startDate = dayjs(todayKST()).toISOString() - if (!parsed.weekOverrides) parsed.weekOverrides = {} if (!parsed.schedulerWeeks) parsed.schedulerWeeks = [{ id: 1, config: makeEmptyWeekly() }] return { calcMode: 'simple', simple: parsed, weekly: makeInitialSlot() } } - // 새 구조에서 schedulerWeeks 누락 시 채움 ;['simple', 'weekly'].forEach((k) => { if (parsed[k] && !parsed[k].schedulerWeeks) { parsed[k].schedulerWeeks = [{ id: 1, config: makeEmptyWeekly() }] @@ -142,46 +126,6 @@ export default function Liberation() { localStorage.setItem(STORAGE_KEY, JSON.stringify(root)) }, [root]) - // 주차별 계산 - const progressByWeek = useMemo(() => { - const result = [] - const startConsumedBefore = GENESIS_CHAPTERS - .slice(0, state.startChapter) - .reduce((s, c) => s + c.required, 0) - 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) - totalAccumulated += earned - - let temp = totalAccumulated - let chapterIdx = 0 - while (chapterIdx < GENESIS_CHAPTERS.length && temp >= GENESIS_CHAPTERS[chapterIdx].required) { - temp -= GENESIS_CHAPTERS[chapterIdx].required - chapterIdx++ - } - - const isCompleted = totalAccumulated >= GENESIS_TOTAL - const chapterInfo = isCompleted - ? { name: '완료', current: GENESIS_TOTAL, required: GENESIS_TOTAL } - : { - name: GENESIS_CHAPTERS[chapterIdx]?.boss || '', - current: temp, - required: GENESIS_CHAPTERS[chapterIdx]?.required || 0, - } - - result.push({ - points: earned, - cumulative: totalAccumulated, - completed: isCompleted, - chapterInfo, - }) - } - return result - }, [state]) - // 포인트 이월 계산: 현재 퀘스트의 required를 초과하면 자동으로 다음 퀘스트로 넘어감 const priorConsumed = GENESIS_CHAPTERS .slice(0, state.startChapter) @@ -359,48 +303,12 @@ export default function Liberation() { const completionDate = computeCompletionDate() const isDone = completionDate !== null - const updateWeek = (idx, newWeekData) => { - setState((prev) => ({ - ...prev, - weeks: prev.weeks.map((w, i) => (i === idx ? newWeekData : w)), - })) - } - - const addWeek = () => { - setState((prev) => { - const lastWeek = prev.weeks[prev.weeks.length - 1] - const nextStart = addWeeks(lastWeek.startDate, 1) - return { - ...prev, - weeks: [...prev.weeks, { ...lastWeek, startDate: dayjs(nextStart).toISOString() }], - } - }) - } - - const removeWeek = (idx) => { - setState((prev) => ({ ...prev, weeks: prev.weeks.filter((_, i) => i !== idx) })) - } - const [resetOpen, setResetOpen] = useState(false) const doReset = () => { setState(makeInitialSlot()) setResetOpen(false) } - const setFirstWeekDate = (dateStr) => { - setState((prev) => { - const weeks = prev.weeks.map((w, i) => ({ - ...w, - startDate: dayjs(addWeeks(dateStr, i)).toISOString(), - })) - return { ...prev, weeks } - }) - } - - const totalCumulative = progressByWeek[progressByWeek.length - 1]?.cumulative - || (GENESIS_CHAPTERS.slice(0, state.startChapter).reduce((s, c) => s + c.required, 0) + state.currentPoints) - const overallProgress = Math.min((totalCumulative / GENESIS_TOTAL) * 100, 100) - return (
{/* 해방 종류 탭 */} diff --git a/frontend/src/features/liberation/components/WeekCard.jsx b/frontend/src/features/liberation/components/WeekCard.jsx deleted file mode 100644 index 9bd1259..0000000 --- a/frontend/src/features/liberation/components/WeekCard.jsx +++ /dev/null @@ -1,134 +0,0 @@ -import Select from '../../../components/Select' -import Checkbox from '../../../components/Checkbox' -import Tooltip from '../../../components/Tooltip' -import { WEEKLY_BOSSES, MONTHLY_BOSSES, BOSS_IMAGE_BASE, calcPoints, formatDate } from '../data' - -const PARTY_OPTIONS = [1, 2, 3, 4, 5, 6].map((n) => ({ value: n, label: `${n}인` })) - -/** - * week: { startDate, bosses: { [bossKey]: { enabled, difficulty, party } }, includeBlackMage: {enabled, difficulty, party} } - */ -export default function WeekCard({ weekNumber, weekData, cumulativePoints, currentChapter, chapterInfo, onChange, weekProgress }) { - const totalThisWeek = weekProgress.points - const updateBoss = (bossKey, patch) => { - const nextBosses = { ...weekData.bosses, [bossKey]: { ...weekData.bosses[bossKey], ...patch } } - onChange({ ...weekData, bosses: nextBosses }) - } - - const updateBlackMage = (patch) => { - onChange({ ...weekData, blackMage: { ...weekData.blackMage, ...patch } }) - } - - return ( -
- {/* 헤더: 주차 번호 + 날짜 + 이번 주 획득 + 누적 */} -
-
-
{weekNumber}주차
-
{formatDate(weekData.startDate)}
-
-
-
- 획득 +{totalThisWeek} -
-
- 누적 {cumulativePoints} -
- {chapterInfo && ( -
- {chapterInfo.name} {chapterInfo.current}/{chapterInfo.required} -
- )} -
-
- - {/* 보스 그리드 */} -
- {WEEKLY_BOSSES.map((boss) => { - const sel = weekData.bosses[boss.key] || { enabled: false, difficulty: boss.difficulties[0].key, party: 1 } - const diff = boss.difficulties.find((d) => d.key === sel.difficulty) || boss.difficulties[0] - const earned = sel.enabled ? calcPoints(diff.points, sel.party) : 0 - - return ( -
-
- updateBoss(boss.key, { enabled: v })} - size="sm" - /> - - {boss.name} - - {boss.name} - {earned > 0 && ( - +{earned} - )} -
- {sel.enabled && ( -
- updateBoss(boss.key, { party: v })} - options={PARTY_OPTIONS} - className="w-16" - /> -
- )} -
- ) - })} - - {/* 검은 마법사 (월 1회) */} - {MONTHLY_BOSSES.map((boss) => { - const sel = weekData.blackMage || { enabled: false, difficulty: boss.difficulties[0].key, party: 1 } - const diff = boss.difficulties.find((d) => d.key === sel.difficulty) || boss.difficulties[0] - const earned = sel.enabled ? calcPoints(diff.points, sel.party) : 0 - - return ( -
-
- updateBlackMage({ enabled: v })} - size="sm" - /> - - {boss.name} - - {boss.name} 월간 - {earned > 0 && ( - +{earned} - )} -
- {sel.enabled && ( -
- updateBlackMage({ party: v })} - options={PARTY_OPTIONS} - className="w-16" - /> -
- )} -
- ) - })} -
-
- ) -} diff --git a/frontend/src/features/liberation/components/WeeklyDefault.jsx b/frontend/src/features/liberation/components/WeeklyDefault.jsx index c1b11ed..2976f70 100644 --- a/frontend/src/features/liberation/components/WeeklyDefault.jsx +++ b/frontend/src/features/liberation/components/WeeklyDefault.jsx @@ -1,4 +1,3 @@ -import { useState } from 'react' import Select from '../../../components/Select' import Tooltip from '../../../components/Tooltip' import WeeklyScheduler from './WeeklyScheduler' diff --git a/frontend/src/features/liberation/components/WeeklyDesignMocks.jsx b/frontend/src/features/liberation/components/WeeklyDesignMocks.jsx deleted file mode 100644 index 5cfa053..0000000 --- a/frontend/src/features/liberation/components/WeeklyDesignMocks.jsx +++ /dev/null @@ -1,166 +0,0 @@ -import { useState } from 'react' -import { LIBERATION_BOSS_IMAGE_BASE, WEEKLY_BOSSES, MONTHLY_BOSSES } from '../data' -import { BossRow } from './WeeklyDefault' - -const DIFF_BADGE = { - easy: { label: 'E', color: '#22c55e', border: 'rgba(34,197,94,0.4)', bg: 'rgba(34,197,94,0.15)' }, - normal: { label: 'N', color: '#60a5fa', border: 'rgba(96,165,250,0.4)', bg: 'rgba(96,165,250,0.15)' }, - hard: { label: 'H', color: '#f87171', border: 'rgba(248,113,113,0.4)', bg: 'rgba(248,113,113,0.15)' }, - chaos: { label: 'C', color: '#c084fc', border: 'rgba(192,132,252,0.45)', bg: 'rgba(192,132,252,0.15)' }, - extreme: { label: 'X', color: '#f59e0b', border: 'rgba(245,158,11,0.5)', bg: 'rgba(245,158,11,0.2)' }, -} - -// 임시 목업 데이터 -const MOCK_WEEKS = [ - { n: 1, date: '4/14 - 4/16', diffs: { lotus: 'hard', damien: 'hard', lucid: 'hard', will: 'hard', dusk: 'chaos', jinhilla: 'hard', darknell: 'hard' }, monthly: true, earn: 1070, cumulative: 1070, current: true }, - { n: 2, date: '4/16 - 4/23', diffs: { lotus: 'hard', damien: 'hard', lucid: 'hard', will: 'hard', dusk: 'chaos', jinhilla: 'hard', darknell: 'hard' }, monthly: false, earn: 470, cumulative: 1540 }, - { n: 3, date: '4/23 - 4/30', diffs: { lotus: 'hard', damien: 'hard', lucid: 'normal', will: 'normal', dusk: 'normal', jinhilla: 'hard', darknell: 'hard' }, monthly: false, earn: 315, cumulative: 1855, custom: true }, - { n: 4, date: '4/30 - 5/7', diffs: { lotus: 'hard', damien: 'hard', lucid: 'hard', will: 'hard', dusk: 'chaos', jinhilla: 'hard', darknell: 'hard' }, monthly: true, earn: 1070, cumulative: 2925 }, - { n: 5, date: '5/7 - 5/14', diffs: { lotus: 'hard', damien: 'hard', lucid: 'hard', will: 'hard', dusk: 'chaos', jinhilla: 'hard', darknell: 'hard' }, monthly: false, earn: 470, cumulative: 3395 }, - { n: 6, date: '5/14 - 5/21', diffs: { lotus: 'hard', damien: 'hard', lucid: 'hard', will: 'hard', dusk: 'chaos', jinhilla: 'hard', darknell: 'hard' }, monthly: false, earn: 470, cumulative: 3865 }, - { n: 7, date: '5/21 - 5/28', diffs: { lotus: 'hard', damien: 'hard', lucid: 'hard', will: 'hard', dusk: 'chaos', jinhilla: 'hard', darknell: 'hard' }, monthly: false, earn: 470, cumulative: 4335 }, - { n: 8, date: '5/28 - 6/4', diffs: { lotus: 'hard', damien: 'hard', lucid: 'hard', will: 'hard', dusk: 'chaos', jinhilla: 'hard', darknell: 'hard' }, monthly: true, earn: 1070, cumulative: 5405 }, - { n: 9, date: '6/4 - 6/11', diffs: { lotus: 'hard', damien: 'hard', lucid: 'hard', will: 'hard', dusk: 'chaos', jinhilla: 'hard', darknell: 'hard' }, monthly: false, earn: 470, cumulative: 5875 }, - { n: 10, date: '6/11 - 6/18', diffs: { lotus: 'hard', damien: 'hard', lucid: 'hard', will: 'hard', dusk: 'chaos', jinhilla: 'hard', darknell: 'hard' }, monthly: false, earn: 470, cumulative: 6345 }, - { n: 11, date: '6/18 - 6/25', diffs: { lotus: 'hard', damien: 'hard', lucid: 'hard', will: 'hard', dusk: 'chaos', jinhilla: 'hard', darknell: 'hard' }, monthly: false, earn: 470, cumulative: 6500 }, -] - -function BossAvatar({ boss, difficulty, size = 40 }) { - const badge = DIFF_BADGE[difficulty] - return ( -
-
- {boss.name} -
- {badge && ( -
- {badge.label} -
- )} -
- ) -} - -// 주차 편집 영역 (실제 state 바인딩은 이후 연결) -function WeekEditor({ week, monthlyAlreadyAssigned }) { - const initial = () => { - const bosses = {} - WEEKLY_BOSSES.forEach((b) => { - bosses[b.key] = { difficulty: week.diffs[b.key] || 'none', party: 1 } - }) - return { bosses, blackMage: { difficulty: week.monthly ? 'hard' : 'none', party: 1 } } - } - const [config, setConfig] = useState(initial) - - const updateBoss = (key, patch) => { - setConfig((prev) => ({ ...prev, bosses: { ...prev.bosses, [key]: { ...prev.bosses[key], ...patch } } })) - } - const updateBlackMage = (patch) => { - if (monthlyAlreadyAssigned) return - setConfig((prev) => ({ ...prev, blackMage: { ...prev.blackMage, ...patch } })) - } - - return ( -
-
- {WEEKLY_BOSSES.map((boss) => ( - updateBoss(boss.key, patch)} - showDone={week.current} - /> - ))} - {/* 검은 마법사는 항상 표시, 같은 달에 다른 주차에 이미 배정된 경우 비활성 */} -
- -
- {monthlyAlreadyAssigned && ( -
- 이번 달 검은 마법사는 다른 주차에 배정되어 있습니다. -
- )} -
- {week.custom && ( -
- -
- )} -
- ) -} - -export default function WeeklyDesignMocks() { - const [expanded, setExpanded] = useState(3) - - return ( -
- {MOCK_WEEKS.map((w) => ( -
- - - {expanded === w.n && ( -
- -
- )} -
- ))} -
- ) -} diff --git a/frontend/src/features/liberation/data.js b/frontend/src/features/liberation/data.js index fbba23b..e1ddd53 100644 --- a/frontend/src/features/liberation/data.js +++ b/frontend/src/features/liberation/data.js @@ -95,15 +95,6 @@ export function calcPoints(basePoints, partySize) { return Math.floor(basePoints / partySize) } -// 목요일 기준 주차 계산 (KST) -// 이번 주 목요일 자정 = 이번 주의 시작 -export function getThursdayOfWeek(date) { - const d = dayjs(date).tz(KST) - const day = d.day() // 0=일, 4=목 - const diff = (day - 4 + 7) % 7 - return d.subtract(diff, 'day').startOf('day').toDate() -} - import dayjs from 'dayjs' import utc from 'dayjs/plugin/utc' import timezone from 'dayjs/plugin/timezone' @@ -117,10 +108,6 @@ export function formatDate(date) { return dayjs(date).tz(KST).format('YYYY-MM-DD') } -export function addWeeks(date, weeks) { - return dayjs(date).tz(KST).add(weeks, 'week').toDate() -} - export function todayKST() { return dayjs().tz(KST).startOf('day').toDate() }