데스티니 주차별 계산 UI + 탭 라벨 변경
- WeeklyScheduler를 bosses/monthlyBoss/imageBase/makeEmptyConfig prop 받도록 일반화 (월간 보스 없으면 관련 UI/락 전부 스킵) - WeeklyDefault가 WeeklyScheduler에 props 전달, Destiny에서 주차별 모드 주차 카드 확장/추가/삭제 + 보스 아바타 뱃지 표시 동작 - 탭 '단순 계산/주차별 계산' → '일반/주차별' (ConfirmDialog 문구 포함) - 주간 보스 완료 토글 버튼에 title 툴팁 추가 - store: destiny 슬롯에 schedulerWeeks 추가, migrate v2로 기존 사용자 backfill Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
99500d91af
commit
ee30c87518
5 changed files with 95 additions and 69 deletions
|
|
@ -6,7 +6,7 @@ import {
|
||||||
DESTINY_BOSS_IMAGE_BASE,
|
DESTINY_BOSS_IMAGE_BASE,
|
||||||
formatDate,
|
formatDate,
|
||||||
} from '../data'
|
} from '../data'
|
||||||
import { useLiberationStore } from '../store'
|
import { useLiberationStore, makeEmptyDestinyWeekly } from '../store'
|
||||||
import { calcWeekPoints } from '../utils'
|
import { calcWeekPoints } from '../utils'
|
||||||
import ProgressBar from './components/ProgressBar'
|
import ProgressBar from './components/ProgressBar'
|
||||||
import QuestSelector from './components/QuestSelector'
|
import QuestSelector from './components/QuestSelector'
|
||||||
|
|
@ -22,6 +22,9 @@ export default function Destiny() {
|
||||||
const setState = (updater) => updateSlot(updater)
|
const setState = (updater) => updateSlot(updater)
|
||||||
|
|
||||||
const weeklyEarn = calcWeekPoints(state.weekly, DESTINY_BOSSES)
|
const weeklyEarn = calcWeekPoints(state.weekly, DESTINY_BOSSES)
|
||||||
|
const headerWeekly = calcMode === 'weekly'
|
||||||
|
? (state.schedulerWeeks || []).reduce((s, w) => s + calcWeekPoints(w.config, DESTINY_BOSSES), 0)
|
||||||
|
: weeklyEarn
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -34,8 +37,8 @@ export default function Destiny() {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{[
|
{[
|
||||||
{ key: 'simple', label: '단순 계산' },
|
{ key: 'simple', label: '일반' },
|
||||||
{ key: 'weekly', label: '주차별 계산' },
|
{ key: 'weekly', label: '주차별' },
|
||||||
].map((t) => {
|
].map((t) => {
|
||||||
const active = calcMode === t.key
|
const active = calcMode === t.key
|
||||||
return (
|
return (
|
||||||
|
|
@ -126,30 +129,19 @@ export default function Destiny() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{calcMode === 'simple' ? (
|
|
||||||
<WeeklyDefault
|
<WeeklyDefault
|
||||||
bosses={DESTINY_BOSSES}
|
bosses={DESTINY_BOSSES}
|
||||||
imageBase={DESTINY_BOSS_IMAGE_BASE}
|
imageBase={DESTINY_BOSS_IMAGE_BASE}
|
||||||
|
makeEmptyConfig={makeEmptyDestinyWeekly}
|
||||||
weekly={state.weekly}
|
weekly={state.weekly}
|
||||||
onChange={(w) => setState((prev) => ({ ...prev, weekly: w }))}
|
onChange={(w) => setState((prev) => ({ ...prev, weekly: w }))}
|
||||||
totalWeekly={weeklyEarn}
|
totalWeekly={headerWeekly}
|
||||||
remaining={0}
|
remaining={0}
|
||||||
mode="simple"
|
mode={calcMode}
|
||||||
hasScheduler={false}
|
startDate={state.startDate}
|
||||||
|
weeks={state.schedulerWeeks}
|
||||||
|
onChangeWeeks={(w) => setState((prev) => ({ ...prev, schedulerWeeks: w }))}
|
||||||
/>
|
/>
|
||||||
) : (
|
|
||||||
<div
|
|
||||||
className="max-w-3xl mx-auto rounded-2xl border p-16 text-center"
|
|
||||||
style={{
|
|
||||||
background: 'var(--panel-bg)',
|
|
||||||
borderColor: 'var(--panel-border)',
|
|
||||||
boxShadow: 'var(--panel-shadow)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="text-xl font-bold" style={{ color: 'var(--text-emphasis)' }}>주차별 계산 준비 중</div>
|
|
||||||
<div className="text-sm mt-2" style={{ color: 'var(--text-dim)' }}>단순 계산 탭을 이용해주세요.</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import {
|
||||||
LIBERATION_BOSS_IMAGE_BASE,
|
LIBERATION_BOSS_IMAGE_BASE,
|
||||||
formatDate,
|
formatDate,
|
||||||
} from '../data'
|
} from '../data'
|
||||||
import { useLiberationStore } from '../store'
|
import { useLiberationStore, makeEmptyWeekly } from '../store'
|
||||||
import {
|
import {
|
||||||
bossEarn,
|
bossEarn,
|
||||||
calcWeekPoints,
|
calcWeekPoints,
|
||||||
|
|
@ -103,8 +103,8 @@ export default function Genesis() {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{[
|
{[
|
||||||
{ key: 'simple', label: '단순 계산' },
|
{ key: 'simple', label: '일반' },
|
||||||
{ key: 'weekly', label: '주차별 계산' },
|
{ key: 'weekly', label: '주차별' },
|
||||||
].map((t) => {
|
].map((t) => {
|
||||||
const active = calcMode === t.key
|
const active = calcMode === t.key
|
||||||
return (
|
return (
|
||||||
|
|
@ -198,6 +198,7 @@ export default function Genesis() {
|
||||||
bosses={WEEKLY_BOSSES}
|
bosses={WEEKLY_BOSSES}
|
||||||
monthlyBosses={MONTHLY_BOSSES}
|
monthlyBosses={MONTHLY_BOSSES}
|
||||||
imageBase={LIBERATION_BOSS_IMAGE_BASE}
|
imageBase={LIBERATION_BOSS_IMAGE_BASE}
|
||||||
|
makeEmptyConfig={makeEmptyWeekly}
|
||||||
weekly={state.weekly}
|
weekly={state.weekly}
|
||||||
onChange={(w) => setState((prev) => ({ ...prev, weekly: w }))}
|
onChange={(w) => setState((prev) => ({ ...prev, weekly: w }))}
|
||||||
totalWeekly={headerWeekly}
|
totalWeekly={headerWeekly}
|
||||||
|
|
@ -232,7 +233,7 @@ export default function Genesis() {
|
||||||
onClose={() => setResetOpen(false)}
|
onClose={() => setResetOpen(false)}
|
||||||
onConfirm={doReset}
|
onConfirm={doReset}
|
||||||
title="전체 초기화"
|
title="전체 초기화"
|
||||||
description={`${calcMode === 'simple' ? '단순 계산' : '주차별 계산'} 모드의 입력을 모두 초기화하시겠습니까?\n\n시작 날짜, 현재 진행 상태, 주간 보스 설정이 모두 초기값으로 되돌아갑니다.\n다른 모드의 값은 유지됩니다.`}
|
description={`${calcMode === 'simple' ? '일반' : '주차별'} 모드의 입력을 모두 초기화하시겠습니까?\n\n시작 날짜, 현재 진행 상태, 주간 보스 설정이 모두 초기값으로 되돌아갑니다.\n다른 모드의 값은 유지됩니다.`}
|
||||||
confirmText="초기화"
|
confirmText="초기화"
|
||||||
destructive
|
destructive
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,7 @@ export function BossRow({ boss, sel, onChange, imageBase, monthly = false, showD
|
||||||
type="button"
|
type="button"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onClick={() => onChange({ done: !sel.done })}
|
onClick={() => onChange({ done: !sel.done })}
|
||||||
|
title="이번 주 해당 난이도를 이미 클리어했는지 여부"
|
||||||
className="shrink-0 w-20 rounded-md h-8 text-xs font-semibold border"
|
className="shrink-0 w-20 rounded-md h-8 text-xs font-semibold border"
|
||||||
style={disabled ? {
|
style={disabled ? {
|
||||||
borderColor: 'var(--panel-border)',
|
borderColor: 'var(--panel-border)',
|
||||||
|
|
@ -85,6 +86,7 @@ export default function WeeklyDefault({
|
||||||
bosses,
|
bosses,
|
||||||
monthlyBosses = [],
|
monthlyBosses = [],
|
||||||
imageBase,
|
imageBase,
|
||||||
|
makeEmptyConfig,
|
||||||
weekly,
|
weekly,
|
||||||
onChange,
|
onChange,
|
||||||
totalWeekly,
|
totalWeekly,
|
||||||
|
|
@ -168,6 +170,10 @@ export default function WeeklyDefault({
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<WeeklyScheduler
|
<WeeklyScheduler
|
||||||
|
bosses={bosses}
|
||||||
|
monthlyBoss={monthlyBosses[0] ?? null}
|
||||||
|
imageBase={imageBase}
|
||||||
|
makeEmptyConfig={makeEmptyConfig}
|
||||||
startDate={startDate}
|
startDate={startDate}
|
||||||
weeks={weeks}
|
weeks={weeks}
|
||||||
onChangeWeeks={onChangeWeeks}
|
onChangeWeeks={onChangeWeeks}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { motion, AnimatePresence } from 'framer-motion'
|
import { motion, AnimatePresence } from 'framer-motion'
|
||||||
import { LIBERATION_BOSS_IMAGE_BASE, WEEKLY_BOSSES, MONTHLY_BOSSES } from '../../data'
|
|
||||||
import { makeEmptyWeekly } from '../../store'
|
|
||||||
import { bossEarn, calcWeekPoints as calcWeeklySum, getSchedulerWeekRange as getWeekRange } from '../../utils'
|
import { bossEarn, calcWeekPoints as calcWeeklySum, getSchedulerWeekRange as getWeekRange } from '../../utils'
|
||||||
import { BossRow } from './WeeklyDefault'
|
import { BossRow } from './WeeklyDefault'
|
||||||
|
|
||||||
|
|
@ -18,7 +16,7 @@ const DIFF_BADGE = {
|
||||||
extreme: { label: 'X', color: '#f59e0b', border: 'rgba(245,158,11,0.5)', bg: 'rgba(245,158,11,0.2)' },
|
extreme: { label: 'X', color: '#f59e0b', border: 'rgba(245,158,11,0.5)', bg: 'rgba(245,158,11,0.2)' },
|
||||||
}
|
}
|
||||||
|
|
||||||
function BossAvatar({ boss, difficulty, size = 40 }) {
|
function BossAvatar({ boss, imageBase, difficulty, size = 40 }) {
|
||||||
const badge = DIFF_BADGE[difficulty]
|
const badge = DIFF_BADGE[difficulty]
|
||||||
const enabled = difficulty && difficulty !== 'none'
|
const enabled = difficulty && difficulty !== 'none'
|
||||||
return (
|
return (
|
||||||
|
|
@ -32,7 +30,7 @@ function BossAvatar({ boss, difficulty, size = 40 }) {
|
||||||
borderColor: 'var(--panel-border)',
|
borderColor: 'var(--panel-border)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<img src={`${LIBERATION_BOSS_IMAGE_BASE}/${boss.image}`} alt={boss.name} className="w-full h-full object-cover" />
|
<img src={`${imageBase}/${boss.image}`} alt={boss.name} className="w-full h-full object-cover" />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="text-[10px] font-bold leading-none rounded flex items-center justify-center border"
|
className="text-[10px] font-bold leading-none rounded flex items-center justify-center border"
|
||||||
|
|
@ -49,7 +47,7 @@ function BossAvatar({ boss, difficulty, size = 40 }) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function WeekEditor({ config, onChange, isCurrent, monthlyLockedByWeek }) {
|
function WeekEditor({ config, onChange, isCurrent, monthlyLockedByWeek, bosses, monthlyBoss, imageBase }) {
|
||||||
const updateBoss = (key, patch) => {
|
const updateBoss = (key, patch) => {
|
||||||
onChange({ ...config, bosses: { ...config.bosses, [key]: { ...config.bosses[key], ...patch } } })
|
onChange({ ...config, bosses: { ...config.bosses, [key]: { ...config.bosses[key], ...patch } } })
|
||||||
}
|
}
|
||||||
|
|
@ -61,7 +59,7 @@ function WeekEditor({ config, onChange, isCurrent, monthlyLockedByWeek }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{WEEKLY_BOSSES.map((boss, i) => (
|
{bosses.map((boss, i) => (
|
||||||
<div
|
<div
|
||||||
key={boss.key}
|
key={boss.key}
|
||||||
className={i > 0 ? 'border-t' : ''}
|
className={i > 0 ? 'border-t' : ''}
|
||||||
|
|
@ -71,23 +69,27 @@ function WeekEditor({ config, onChange, isCurrent, monthlyLockedByWeek }) {
|
||||||
boss={boss}
|
boss={boss}
|
||||||
sel={config.bosses[boss.key]}
|
sel={config.bosses[boss.key]}
|
||||||
onChange={(patch) => updateBoss(boss.key, patch)}
|
onChange={(patch) => updateBoss(boss.key, patch)}
|
||||||
|
imageBase={imageBase}
|
||||||
showDone={isCurrent}
|
showDone={isCurrent}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
{monthlyBoss && (
|
||||||
<div
|
<div
|
||||||
className={`border-t ${blackmageLocked ? 'opacity-40 pointer-events-none' : ''}`}
|
className={`border-t ${blackmageLocked ? 'opacity-40 pointer-events-none' : ''}`}
|
||||||
style={{ borderColor: 'var(--row-divider)' }}
|
style={{ borderColor: 'var(--row-divider)' }}
|
||||||
>
|
>
|
||||||
<BossRow
|
<BossRow
|
||||||
boss={MONTHLY_BOSSES[0]}
|
boss={monthlyBoss}
|
||||||
sel={blackmageLocked ? { difficulty: 'none', party: 1, done: false } : config.blackMage}
|
sel={blackmageLocked ? { difficulty: 'none', party: 1, done: false } : config.blackMage}
|
||||||
onChange={updateBlackMage}
|
onChange={updateBlackMage}
|
||||||
|
imageBase={imageBase}
|
||||||
monthly
|
monthly
|
||||||
showDone={isCurrent}
|
showDone={isCurrent}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{blackmageLocked && (
|
)}
|
||||||
|
{monthlyBoss && blackmageLocked && (
|
||||||
<div
|
<div
|
||||||
className="text-[11px] px-3 py-2"
|
className="text-[11px] px-3 py-2"
|
||||||
style={{ color: 'var(--warning-text)' }}
|
style={{ color: 'var(--warning-text)' }}
|
||||||
|
|
@ -99,10 +101,18 @@ function WeekEditor({ config, onChange, isCurrent, monthlyLockedByWeek }) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function WeeklyScheduler({ startDate, weeks: weeksProp, onChangeWeeks }) {
|
export default function WeeklyScheduler({
|
||||||
|
bosses,
|
||||||
|
monthlyBoss = null,
|
||||||
|
imageBase,
|
||||||
|
makeEmptyConfig,
|
||||||
|
startDate,
|
||||||
|
weeks: weeksProp,
|
||||||
|
onChangeWeeks,
|
||||||
|
}) {
|
||||||
const weeks = weeksProp && weeksProp.length > 0
|
const weeks = weeksProp && weeksProp.length > 0
|
||||||
? weeksProp
|
? weeksProp
|
||||||
: [{ id: 1, config: makeEmptyWeekly() }]
|
: [{ id: 1, config: makeEmptyConfig() }]
|
||||||
const setWeeks = (updater) => {
|
const setWeeks = (updater) => {
|
||||||
const next = typeof updater === 'function' ? updater(weeks) : updater
|
const next = typeof updater === 'function' ? updater(weeks) : updater
|
||||||
onChangeWeeks?.(next)
|
onChangeWeeks?.(next)
|
||||||
|
|
@ -114,13 +124,13 @@ export default function WeeklyScheduler({ startDate, weeks: weeksProp, onChangeW
|
||||||
const id = nextId()
|
const id = nextId()
|
||||||
setWeeks((prev) => {
|
setWeeks((prev) => {
|
||||||
const last = prev[prev.length - 1]
|
const last = prev[prev.length - 1]
|
||||||
const base = last ? JSON.parse(JSON.stringify(last.config)) : makeEmptyWeekly()
|
const base = last ? JSON.parse(JSON.stringify(last.config)) : makeEmptyConfig()
|
||||||
// done 상태는 복사하지 않음
|
// done 상태는 복사하지 않음
|
||||||
Object.keys(base.bosses).forEach((k) => { base.bosses[k].done = false })
|
Object.keys(base.bosses).forEach((k) => { base.bosses[k].done = false })
|
||||||
if (base.blackMage) base.blackMage.done = false
|
if (base.blackMage) base.blackMage.done = false
|
||||||
|
|
||||||
// 새 주차의 달에 이미 검은 마법사가 배정되어 있으면 복사된 검은마법사는 초기화
|
// 월간 보스가 이미 같은 달에 배정되어 있으면 새 주차의 월간은 초기화
|
||||||
if (startDate && base.blackMage?.difficulty && base.blackMage.difficulty !== 'none') {
|
if (monthlyBoss && startDate && base.blackMage?.difficulty && base.blackMage.difficulty !== 'none') {
|
||||||
const newIdx = prev.length + 1
|
const newIdx = prev.length + 1
|
||||||
const newMonth = getWeekRange(startDate, newIdx).start.format('YYYY-MM')
|
const newMonth = getWeekRange(startDate, newIdx).start.format('YYYY-MM')
|
||||||
const existsInSameMonth = prev.some((p, i) => {
|
const existsInSameMonth = prev.some((p, i) => {
|
||||||
|
|
@ -146,9 +156,9 @@ export default function WeeklyScheduler({ startDate, weeks: weeksProp, onChangeW
|
||||||
setWeeks((prev) => prev.map((w) => (w.id === id ? { ...w, config } : w)))
|
setWeeks((prev) => prev.map((w) => (w.id === id ? { ...w, config } : w)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 검은 마법사 월별 슬롯 배정: 각 주차가 겹치는 달 중 하나를 선점
|
// 월간 보스 슬롯 배정: 각 주차가 겹치는 달 중 하나를 선점
|
||||||
const monthlyLocks = (() => {
|
const monthlyLocks = (() => {
|
||||||
if (!startDate) return {}
|
if (!monthlyBoss || !startDate) return {}
|
||||||
const claimed = {} // month -> weekNum (1-based)
|
const claimed = {} // month -> weekNum (1-based)
|
||||||
weeks.forEach((w, idx) => {
|
weeks.forEach((w, idx) => {
|
||||||
const diff = w.config.blackMage?.difficulty
|
const diff = w.config.blackMage?.difficulty
|
||||||
|
|
@ -166,9 +176,7 @@ export default function WeeklyScheduler({ startDate, weeks: weeksProp, onChangeW
|
||||||
weeks.forEach((w, idx) => {
|
weeks.forEach((w, idx) => {
|
||||||
const r = getWeekRange(startDate, idx + 1)
|
const r = getWeekRange(startDate, idx + 1)
|
||||||
const months = [r.start.format('YYYY-MM'), r.end.format('YYYY-MM')]
|
const months = [r.start.format('YYYY-MM'), r.end.format('YYYY-MM')]
|
||||||
// 본인이 한 달이라도 차지했으면 잠그지 않음
|
|
||||||
if (months.some((m) => claimed[m] === idx + 1)) return
|
if (months.some((m) => claimed[m] === idx + 1)) return
|
||||||
// 겹치는 달이 모두 다른 주차에 점유되었으면 잠금
|
|
||||||
if (months.every((m) => m in claimed)) {
|
if (months.every((m) => m in claimed)) {
|
||||||
locks[idx] = claimed[months[0]] ?? claimed[months[1]]
|
locks[idx] = claimed[months[0]] ?? claimed[months[1]]
|
||||||
}
|
}
|
||||||
|
|
@ -181,8 +189,7 @@ export default function WeeklyScheduler({ startDate, weeks: weeksProp, onChangeW
|
||||||
{weeks.map((w, idx) => {
|
{weeks.map((w, idx) => {
|
||||||
const n = idx + 1
|
const n = idx + 1
|
||||||
const isOpen = expanded === w.id
|
const isOpen = expanded === w.id
|
||||||
const isCurrent = idx === 0 // 임시: 첫 번째가 현재 주차 (실제 연결 시 날짜 기반)
|
const isCurrent = idx === 0
|
||||||
// 검은마법사 잠금 판정은 아래 사전 계산된 monthlyLocks 사용
|
|
||||||
const monthlyLockedByWeek = monthlyLocks[idx] ?? null
|
const monthlyLockedByWeek = monthlyLocks[idx] ?? null
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
@ -218,15 +225,24 @@ export default function WeeklyScheduler({ startDate, weeks: weeksProp, onChangeW
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex-1 flex items-center gap-2">
|
<div className="flex-1 flex items-center gap-2">
|
||||||
{WEEKLY_BOSSES.map((b) => (
|
{bosses.map((b) => (
|
||||||
<BossAvatar key={b.key} boss={b} difficulty={w.config.bosses[b.key]?.difficulty} size={40} />
|
<BossAvatar key={b.key} boss={b} imageBase={imageBase} difficulty={w.config.bosses[b.key]?.difficulty} size={40} />
|
||||||
))}
|
))}
|
||||||
<BossAvatar boss={MONTHLY_BOSSES[0]} difficulty={monthlyLockedByWeek != null ? 'none' : w.config.blackMage?.difficulty} size={40} />
|
{monthlyBoss && (
|
||||||
|
<BossAvatar
|
||||||
|
boss={monthlyBoss}
|
||||||
|
imageBase={imageBase}
|
||||||
|
difficulty={monthlyLockedByWeek != null ? 'none' : w.config.blackMage?.difficulty}
|
||||||
|
size={40}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{(() => {
|
{(() => {
|
||||||
const weeklySum = calcWeeklySum(w.config)
|
const weeklySum = calcWeeklySum(w.config, bosses)
|
||||||
const monthlySum = monthlyLockedByWeek != null ? 0 : bossEarn(MONTHLY_BOSSES[0], w.config.blackMage)
|
const monthlySum = !monthlyBoss || monthlyLockedByWeek != null
|
||||||
|
? 0
|
||||||
|
: bossEarn(monthlyBoss, w.config.blackMage)
|
||||||
return (
|
return (
|
||||||
<div className="text-right shrink-0 pr-1 tabular-nums leading-tight">
|
<div className="text-right shrink-0 pr-1 tabular-nums leading-tight">
|
||||||
<div className="text-base font-bold" style={{ color: 'var(--accent-bright)' }}>+{weeklySum}</div>
|
<div className="text-base font-bold" style={{ color: 'var(--accent-bright)' }}>+{weeklySum}</div>
|
||||||
|
|
@ -284,6 +300,9 @@ export default function WeeklyScheduler({ startDate, weeks: weeksProp, onChangeW
|
||||||
onChange={(c) => updateWeek(w.id, c)}
|
onChange={(c) => updateWeek(w.id, c)}
|
||||||
isCurrent={isCurrent}
|
isCurrent={isCurrent}
|
||||||
monthlyLockedByWeek={monthlyLockedByWeek}
|
monthlyLockedByWeek={monthlyLockedByWeek}
|
||||||
|
bosses={bosses}
|
||||||
|
monthlyBoss={monthlyBoss}
|
||||||
|
imageBase={imageBase}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ function makeInitialDestinySlot() {
|
||||||
currentPoints: 0,
|
currentPoints: 0,
|
||||||
startDate: dayjs(todayKST()).toISOString(),
|
startDate: dayjs(todayKST()).toISOString(),
|
||||||
weekly: makeEmptyDestinyWeekly(),
|
weekly: makeEmptyDestinyWeekly(),
|
||||||
|
schedulerWeeks: [{ id: 1, config: makeEmptyDestinyWeekly() }],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -83,11 +84,18 @@ export const useLiberationStore = create(persist(
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: 'maple-liberation',
|
name: 'maple-liberation',
|
||||||
version: 1,
|
version: 2,
|
||||||
migrate: (persisted) => {
|
migrate: (persisted) => {
|
||||||
if (!persisted) return persisted
|
if (!persisted) return persisted
|
||||||
// v0→v1: 데스티니 슬롯에 weekly 필드가 없으면 빈 값으로 채움
|
// 데스티니 슬롯에 weekly/schedulerWeeks 필드가 없으면 빈 값으로 채움
|
||||||
const fill = (slot) => slot ? { ...slot, weekly: slot.weekly || makeEmptyDestinyWeekly() } : slot
|
const fill = (slot) => {
|
||||||
|
if (!slot) return slot
|
||||||
|
return {
|
||||||
|
...slot,
|
||||||
|
weekly: slot.weekly || makeEmptyDestinyWeekly(),
|
||||||
|
schedulerWeeks: slot.schedulerWeeks || [{ id: 1, config: makeEmptyDestinyWeekly() }],
|
||||||
|
}
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
...persisted,
|
...persisted,
|
||||||
destinySimple: fill(persisted.destinySimple),
|
destinySimple: fill(persisted.destinySimple),
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue