From d1ca41ed4a188600dec754971a01e32f0f61d6fb Mon Sep 17 00:00:00 2001 From: caadiq Date: Tue, 14 Apr 2026 18:41:53 +0900 Subject: [PATCH] =?UTF-8?q?=ED=95=B4=EB=B0=A9=20=EC=A3=BC=EC=B0=A8?= =?UTF-8?q?=EB=B3=84=20=EA=B3=84=EC=82=B0=20=ED=83=AD=20=EC=B4=88=EC=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 계산 모드 탭(단순/주차별)을 상단으로 이동, 각 모드 독립 slot 저장 - 초기화 시 현재 모드 slot만 초기화, 다른 모드는 유지 - 주차별 카드 리스트 + 펼침 편집 영역 목업 - 편집 영역에서 기존 BossRow 재사용 (완료 버튼은 현재 주차에만) - 검은 마법사 행 항상 표시, 같은 달 다른 주차 배정 시 비활성 Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/features/liberation/Liberation.jsx | 88 ++++++---- .../liberation/components/WeeklyDefault.jsx | 115 +++++------- .../components/WeeklyDesignMocks.jsx | 166 ++++++++++++++++++ 3 files changed, 265 insertions(+), 104 deletions(-) create mode 100644 frontend/src/features/liberation/components/WeeklyDesignMocks.jsx diff --git a/frontend/src/features/liberation/Liberation.jsx b/frontend/src/features/liberation/Liberation.jsx index 24b9634..425cbe6 100644 --- a/frontend/src/features/liberation/Liberation.jsx +++ b/frontend/src/features/liberation/Liberation.jsx @@ -93,43 +93,46 @@ export default function Liberation() { staleTime: Infinity, }) - const [state, setState] = useState(() => { + const makeInitialSlot = () => ({ + startChapter: 0, + currentPoints: 0, + startDate: dayjs(todayKST()).toISOString(), + weekly: makeEmptyWeekly(), + weekOverrides: {}, + weeks: [makeEmptyWeek(todayKST())], + }) + + const [root, setRoot] = useState(() => { const saved = localStorage.getItem(STORAGE_KEY) if (saved) { try { const parsed = JSON.parse(saved) - if (!parsed.weekly) parsed.weekly = makeEmptyWeekly() - if (!parsed.startDate) parsed.startDate = dayjs(todayKST()).toISOString() - if (!parsed.weekOverrides) parsed.weekOverrides = {} - // enabled/'none' 필드 제거 마이그레이션 - const migrate = (sel, defaultDiff) => { - if (!sel) return sel - if (!sel.difficulty || sel.difficulty === 'none') sel.difficulty = defaultDiff - delete sel.enabled - return sel + // 구버전(단일 slot) → 새 구조로 마이그레이션 + if (!parsed.calcMode) { + if (!parsed.weekly) parsed.weekly = makeEmptyWeekly() + if (!parsed.startDate) parsed.startDate = dayjs(todayKST()).toISOString() + if (!parsed.weekOverrides) parsed.weekOverrides = {} + return { calcMode: 'simple', simple: parsed, weekly: makeInitialSlot() } } - WEEKLY_BOSSES.forEach((b) => { - if (parsed.weekly.bosses?.[b.key]) { - parsed.weekly.bosses[b.key] = migrate(parsed.weekly.bosses[b.key], b.difficulties[0].key) - } - }) - parsed.weekly.blackMage = migrate(parsed.weekly.blackMage, MONTHLY_BOSSES[0].difficulties[0].key) return parsed } catch { /* ignore */ } } - return { - startChapter: 0, - currentPoints: 0, - startDate: dayjs(todayKST()).toISOString(), - weekly: makeEmptyWeekly(), - weekOverrides: {}, - weeks: [makeEmptyWeek(todayKST())], - } + return { calcMode: 'simple', simple: makeInitialSlot(), weekly: makeInitialSlot() } }) + const calcMode = root.calcMode + const state = root[calcMode] + const setState = (updater) => { + setRoot((prev) => ({ + ...prev, + [prev.calcMode]: typeof updater === 'function' ? updater(prev[prev.calcMode]) : updater, + })) + } + const setCalcMode = (mode) => setRoot((prev) => ({ ...prev, calcMode: mode })) + useEffect(() => { - localStorage.setItem(STORAGE_KEY, JSON.stringify(state)) - }, [state]) + localStorage.setItem(STORAGE_KEY, JSON.stringify(root)) + }, [root]) // 주차별 계산 const progressByWeek = useMemo(() => { @@ -260,14 +263,7 @@ export default function Liberation() { const [resetOpen, setResetOpen] = useState(false) const doReset = () => { - setState({ - startChapter: 0, - currentPoints: 0, - startDate: dayjs(todayKST()).toISOString(), - weekly: makeEmptyWeekly(), - weekOverrides: {}, - weeks: [makeEmptyWeek(todayKST())], - }) + setState(makeInitialSlot()) setResetOpen(false) } @@ -315,6 +311,27 @@ export default function Liberation() {
데스티니 해방 계산기는 준비 중입니다.
) : (<> + {/* 계산 모드 탭 */} +
+ {[ + { key: 'simple', label: '단순 계산' }, + { key: 'weekly', label: '주차별 계산' }, + ].map((t) => ( + + ))} +
+ setState((prev) => ({ ...prev, weekly: w }))} totalWeekly={weeklyEarn} totalMonthly={monthlyEarn} + mode={calcMode} />
@@ -380,7 +398,7 @@ export default function Liberation() { onClose={() => setResetOpen(false)} onConfirm={doReset} title="전체 초기화" - description={'입력한 내용을 모두 초기화하시겠습니까?\n\n시작 날짜, 현재 진행 상태, 주간 보스 설정이 모두 초기값으로 되돌아갑니다.'} + description={`${calcMode === 'simple' ? '단순 계산' : '주차별 계산'} 모드의 입력을 모두 초기화하시겠습니까?\n\n시작 날짜, 현재 진행 상태, 주간 보스 설정이 모두 초기값으로 되돌아갑니다.\n다른 모드의 값은 유지됩니다.`} confirmText="초기화" destructive /> diff --git a/frontend/src/features/liberation/components/WeeklyDefault.jsx b/frontend/src/features/liberation/components/WeeklyDefault.jsx index 202889d..d3fccb6 100644 --- a/frontend/src/features/liberation/components/WeeklyDefault.jsx +++ b/frontend/src/features/liberation/components/WeeklyDefault.jsx @@ -1,6 +1,7 @@ import { useState } from 'react' import Select from '../../../components/Select' import Tooltip from '../../../components/Tooltip' +import WeeklyDesignMocks from './WeeklyDesignMocks' import { WEEKLY_BOSSES, MONTHLY_BOSSES, LIBERATION_BOSS_IMAGE_BASE, calcPoints } from '../data' const PARTY_OPTIONS = [1, 2, 3, 4, 5, 6].map((n) => ({ value: n, label: `${n}인` })) @@ -16,7 +17,7 @@ function diffLabel(d, party) { ) } -function BossRow({ boss, sel, onChange, monthly = false }) { +export function BossRow({ boss, sel, onChange, monthly = false, showDone = true }) { const disabled = sel.difficulty === 'none' const difficultyOptions = [NONE_DIFFICULTY, ...boss.difficulties] .map((d) => ({ value: d.key, label: diffLabel(d, sel.party) })) @@ -49,27 +50,27 @@ function BossRow({ boss, sel, onChange, monthly = false }) { disabled={disabled} />
- + {showDone && ( + + )} ) } -export default function WeeklyDefault({ weekly, onChange, totalWeekly, totalMonthly }) { - const [mode, setMode] = useState('simple') // 'simple' | 'weekly' - +export default function WeeklyDefault({ weekly, onChange, totalWeekly, totalMonthly, mode = 'simple' }) { const updateBoss = (key, patch) => { onChange({ ...weekly, bosses: { ...weekly.bosses, [key]: { ...weekly.bosses[key], ...patch } } }) } @@ -81,63 +82,39 @@ export default function WeeklyDefault({ weekly, onChange, totalWeekly, totalMont
주간 보스 설정
-
- setMode('simple')}>단순 계산 - setMode('weekly')}>주차별 계산 +
+ + 주간 획득 +{totalWeekly} + + + 월간 획득 +{totalMonthly} +
{mode === 'simple' ? ( - <> -
- - 주간 획득 +{totalWeekly} - - - 월간 획득 +{totalMonthly} - -
-
- {WEEKLY_BOSSES.map((boss) => ( - updateBoss(boss.key, patch)} - /> - ))} - {MONTHLY_BOSSES.map((boss) => ( - - ))} -
- - ) : ( -
- 주차별 계산 UI 준비 중 +
+ {WEEKLY_BOSSES.map((boss) => ( + updateBoss(boss.key, patch)} + /> + ))} + {MONTHLY_BOSSES.map((boss) => ( + + ))}
+ ) : ( + )}
) } - -function TabButton({ active, onClick, children }) { - return ( - - ) -} diff --git a/frontend/src/features/liberation/components/WeeklyDesignMocks.jsx b/frontend/src/features/liberation/components/WeeklyDesignMocks.jsx new file mode 100644 index 0000000..5cfa053 --- /dev/null +++ b/frontend/src/features/liberation/components/WeeklyDesignMocks.jsx @@ -0,0 +1,166 @@ +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 && ( +
+ +
+ )} +
+ ))} +
+ ) +}