해방 계산기 미사용 코드 정리
- WeekCard, WeeklyDesignMocks 컴포넌트 삭제 (현재 사용처 없음) - progressByWeek/updateWeek/addWeek/removeWeek/setFirstWeekDate/totalCumulative 등 주차별 모드 도입 전 잔여 코드 제거 - 사용하지 않는 export(addWeeks, getThursdayOfWeek) 및 import 정리 - calcMonthlyDoneEarn 미사용 함수 제거 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6243dea01e
commit
aa5db4b4c2
5 changed files with 1 additions and 407 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
import { useState, useMemo, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { useQuery } from '@tanstack/react-query'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { api } from '../../api/client'
|
import { api } from '../../api/client'
|
||||||
|
|
@ -8,11 +8,9 @@ import {
|
||||||
WEEKLY_BOSSES,
|
WEEKLY_BOSSES,
|
||||||
MONTHLY_BOSSES,
|
MONTHLY_BOSSES,
|
||||||
calcPoints,
|
calcPoints,
|
||||||
addWeeks,
|
|
||||||
formatDate,
|
formatDate,
|
||||||
todayKST,
|
todayKST,
|
||||||
} from './data'
|
} from './data'
|
||||||
import WeekCard from './components/WeekCard'
|
|
||||||
import QuestSelector from './components/QuestSelector'
|
import QuestSelector from './components/QuestSelector'
|
||||||
import PointsInput from './components/PointsInput'
|
import PointsInput from './components/PointsInput'
|
||||||
import ProgressBar from './components/ProgressBar'
|
import ProgressBar from './components/ProgressBar'
|
||||||
|
|
@ -23,13 +21,6 @@ import { useLayout } from '../../components/Layout'
|
||||||
|
|
||||||
const STORAGE_KEY = 'maple-liberation'
|
const STORAGE_KEY = 'maple-liberation'
|
||||||
|
|
||||||
function makeEmptyWeek(startDate) {
|
|
||||||
return {
|
|
||||||
startDate: dayjs(startDate).toISOString(),
|
|
||||||
...makeEmptyWeekly(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeEmptyWeekly() {
|
function makeEmptyWeekly() {
|
||||||
const bosses = {}
|
const bosses = {}
|
||||||
WEEKLY_BOSSES.forEach((b) => {
|
WEEKLY_BOSSES.forEach((b) => {
|
||||||
|
|
@ -69,9 +60,6 @@ function calcMonthlyEarn(weekData) {
|
||||||
return bossEarn(MONTHLY_BOSSES[0], weekData.blackMage)
|
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() {
|
export default function Liberation() {
|
||||||
const { setFullscreen } = useLayout()
|
const { setFullscreen } = useLayout()
|
||||||
|
|
@ -98,8 +86,6 @@ export default function Liberation() {
|
||||||
currentPoints: 0,
|
currentPoints: 0,
|
||||||
startDate: dayjs(todayKST()).toISOString(),
|
startDate: dayjs(todayKST()).toISOString(),
|
||||||
weekly: makeEmptyWeekly(),
|
weekly: makeEmptyWeekly(),
|
||||||
weekOverrides: {},
|
|
||||||
weeks: [makeEmptyWeek(todayKST())],
|
|
||||||
schedulerWeeks: [{ id: 1, config: makeEmptyWeekly() }],
|
schedulerWeeks: [{ id: 1, config: makeEmptyWeekly() }],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -112,11 +98,9 @@ export default function Liberation() {
|
||||||
if (!parsed.calcMode) {
|
if (!parsed.calcMode) {
|
||||||
if (!parsed.weekly) parsed.weekly = makeEmptyWeekly()
|
if (!parsed.weekly) parsed.weekly = makeEmptyWeekly()
|
||||||
if (!parsed.startDate) parsed.startDate = dayjs(todayKST()).toISOString()
|
if (!parsed.startDate) parsed.startDate = dayjs(todayKST()).toISOString()
|
||||||
if (!parsed.weekOverrides) parsed.weekOverrides = {}
|
|
||||||
if (!parsed.schedulerWeeks) parsed.schedulerWeeks = [{ id: 1, config: makeEmptyWeekly() }]
|
if (!parsed.schedulerWeeks) parsed.schedulerWeeks = [{ id: 1, config: makeEmptyWeekly() }]
|
||||||
return { calcMode: 'simple', simple: parsed, weekly: makeInitialSlot() }
|
return { calcMode: 'simple', simple: parsed, weekly: makeInitialSlot() }
|
||||||
}
|
}
|
||||||
// 새 구조에서 schedulerWeeks 누락 시 채움
|
|
||||||
;['simple', 'weekly'].forEach((k) => {
|
;['simple', 'weekly'].forEach((k) => {
|
||||||
if (parsed[k] && !parsed[k].schedulerWeeks) {
|
if (parsed[k] && !parsed[k].schedulerWeeks) {
|
||||||
parsed[k].schedulerWeeks = [{ id: 1, config: makeEmptyWeekly() }]
|
parsed[k].schedulerWeeks = [{ id: 1, config: makeEmptyWeekly() }]
|
||||||
|
|
@ -142,46 +126,6 @@ export default function Liberation() {
|
||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(root))
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(root))
|
||||||
}, [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를 초과하면 자동으로 다음 퀘스트로 넘어감
|
// 포인트 이월 계산: 현재 퀘스트의 required를 초과하면 자동으로 다음 퀘스트로 넘어감
|
||||||
const priorConsumed = GENESIS_CHAPTERS
|
const priorConsumed = GENESIS_CHAPTERS
|
||||||
.slice(0, state.startChapter)
|
.slice(0, state.startChapter)
|
||||||
|
|
@ -359,48 +303,12 @@ export default function Liberation() {
|
||||||
const completionDate = computeCompletionDate()
|
const completionDate = computeCompletionDate()
|
||||||
const isDone = completionDate !== null
|
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 [resetOpen, setResetOpen] = useState(false)
|
||||||
const doReset = () => {
|
const doReset = () => {
|
||||||
setState(makeInitialSlot())
|
setState(makeInitialSlot())
|
||||||
setResetOpen(false)
|
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 (
|
return (
|
||||||
<div className="space-y-6 pb-10">
|
<div className="space-y-6 pb-10">
|
||||||
{/* 해방 종류 탭 */}
|
{/* 해방 종류 탭 */}
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
|
||||||
<div className="rounded-xl border border-white/5 bg-gray-900/40 overflow-hidden">
|
|
||||||
{/* 헤더: 주차 번호 + 날짜 + 이번 주 획득 + 누적 */}
|
|
||||||
<div className="flex items-center justify-between gap-3 px-4 py-3 bg-gray-950/60 border-b border-white/5">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="text-sm font-semibold">{weekNumber}주차</div>
|
|
||||||
<div className="text-xs text-gray-500">{formatDate(weekData.startDate)}</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-4 text-sm">
|
|
||||||
<div className="text-gray-400">
|
|
||||||
획득 <span className="text-emerald-300 font-semibold tabular-nums">+{totalThisWeek}</span>
|
|
||||||
</div>
|
|
||||||
<div className="text-gray-400">
|
|
||||||
누적 <span className="text-white font-semibold tabular-nums">{cumulativePoints}</span>
|
|
||||||
</div>
|
|
||||||
{chapterInfo && (
|
|
||||||
<div className="text-xs text-gray-500">
|
|
||||||
{chapterInfo.name} {chapterInfo.current}/{chapterInfo.required}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 보스 그리드 */}
|
|
||||||
<div className="p-3 grid grid-cols-2 sm:grid-cols-4 gap-2">
|
|
||||||
{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 (
|
|
||||||
<div key={boss.key} className={`rounded-lg border p-2 transition ${sel.enabled ? 'border-white/10 bg-gray-950/40' : 'border-white/5 bg-transparent opacity-60'}`}>
|
|
||||||
<div className="flex items-center gap-2 mb-2">
|
|
||||||
<Checkbox
|
|
||||||
checked={sel.enabled}
|
|
||||||
onChange={(v) => updateBoss(boss.key, { enabled: v })}
|
|
||||||
size="sm"
|
|
||||||
/>
|
|
||||||
<Tooltip text={boss.name}>
|
|
||||||
<img src={`${BOSS_IMAGE_BASE}/${boss.image}`} alt={boss.name} className="w-7 h-7 rounded object-cover" />
|
|
||||||
</Tooltip>
|
|
||||||
<span className="text-xs font-medium truncate flex-1">{boss.name}</span>
|
|
||||||
{earned > 0 && (
|
|
||||||
<span className="text-xs text-emerald-300 font-semibold tabular-nums">+{earned}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{sel.enabled && (
|
|
||||||
<div className="flex gap-1.5">
|
|
||||||
<Select
|
|
||||||
value={sel.difficulty}
|
|
||||||
onChange={(v) => updateBoss(boss.key, { difficulty: v })}
|
|
||||||
options={boss.difficulties.map((d) => ({ value: d.key, label: `${d.label} +${d.points}` }))}
|
|
||||||
className="flex-1"
|
|
||||||
/>
|
|
||||||
<Select
|
|
||||||
value={sel.party}
|
|
||||||
onChange={(v) => updateBoss(boss.key, { party: v })}
|
|
||||||
options={PARTY_OPTIONS}
|
|
||||||
className="w-16"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
|
|
||||||
{/* 검은 마법사 (월 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 (
|
|
||||||
<div key={boss.key} className={`rounded-lg border p-2 transition col-span-2 sm:col-span-2 ${
|
|
||||||
sel.enabled ? 'border-amber-500/40 bg-amber-500/[0.05]' : 'border-white/5 bg-transparent opacity-60'
|
|
||||||
}`}>
|
|
||||||
<div className="flex items-center gap-2 mb-2">
|
|
||||||
<Checkbox
|
|
||||||
checked={sel.enabled}
|
|
||||||
onChange={(v) => updateBlackMage({ enabled: v })}
|
|
||||||
size="sm"
|
|
||||||
/>
|
|
||||||
<Tooltip text={`${boss.name} (월 1회)`}>
|
|
||||||
<img src={`${BOSS_IMAGE_BASE}/${boss.image}`} alt={boss.name} className="w-7 h-7 rounded object-cover" />
|
|
||||||
</Tooltip>
|
|
||||||
<span className="text-xs font-medium flex-1">{boss.name} <span className="text-[10px] text-amber-400">월간</span></span>
|
|
||||||
{earned > 0 && (
|
|
||||||
<span className="text-xs text-emerald-300 font-semibold tabular-nums">+{earned}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{sel.enabled && (
|
|
||||||
<div className="flex gap-1.5">
|
|
||||||
<Select
|
|
||||||
value={sel.difficulty}
|
|
||||||
onChange={(v) => updateBlackMage({ difficulty: v })}
|
|
||||||
options={boss.difficulties.map((d) => ({ value: d.key, label: `${d.label} +${d.points}` }))}
|
|
||||||
className="flex-1"
|
|
||||||
/>
|
|
||||||
<Select
|
|
||||||
value={sel.party}
|
|
||||||
onChange={(v) => updateBlackMage({ party: v })}
|
|
||||||
options={PARTY_OPTIONS}
|
|
||||||
className="w-16"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import { useState } from 'react'
|
|
||||||
import Select from '../../../components/Select'
|
import Select from '../../../components/Select'
|
||||||
import Tooltip from '../../../components/Tooltip'
|
import Tooltip from '../../../components/Tooltip'
|
||||||
import WeeklyScheduler from './WeeklyScheduler'
|
import WeeklyScheduler from './WeeklyScheduler'
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
|
||||||
<div className="flex flex-col items-center gap-1">
|
|
||||||
<div
|
|
||||||
className="rounded-md overflow-hidden bg-gray-900 border border-white/5"
|
|
||||||
style={{ width: size, height: size }}
|
|
||||||
>
|
|
||||||
<img src={`${LIBERATION_BOSS_IMAGE_BASE}/${boss.image}`} alt={boss.name} className="w-full h-full object-cover" />
|
|
||||||
</div>
|
|
||||||
{badge && (
|
|
||||||
<div
|
|
||||||
className="text-[10px] font-bold leading-none rounded flex items-center justify-center border"
|
|
||||||
style={{ width: 16, height: 16, color: badge.color, background: badge.bg, borderColor: badge.border }}
|
|
||||||
>
|
|
||||||
{badge.label}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 주차 편집 영역 (실제 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 (
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div className="divide-y divide-white/5">
|
|
||||||
{WEEKLY_BOSSES.map((boss) => (
|
|
||||||
<BossRow
|
|
||||||
key={boss.key}
|
|
||||||
boss={boss}
|
|
||||||
sel={config.bosses[boss.key]}
|
|
||||||
onChange={(patch) => updateBoss(boss.key, patch)}
|
|
||||||
showDone={week.current}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
{/* 검은 마법사는 항상 표시, 같은 달에 다른 주차에 이미 배정된 경우 비활성 */}
|
|
||||||
<div className={monthlyAlreadyAssigned ? 'opacity-40 pointer-events-none' : ''}>
|
|
||||||
<BossRow
|
|
||||||
boss={MONTHLY_BOSSES[0]}
|
|
||||||
sel={monthlyAlreadyAssigned ? { difficulty: 'none', party: 1 } : config.blackMage}
|
|
||||||
onChange={updateBlackMage}
|
|
||||||
monthly
|
|
||||||
showDone={week.current}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{monthlyAlreadyAssigned && (
|
|
||||||
<div className="text-[11px] text-amber-400/80 px-3 py-2">
|
|
||||||
이번 달 검은 마법사는 다른 주차에 배정되어 있습니다.
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{week.custom && (
|
|
||||||
<div className="flex justify-end">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="text-xs text-red-400 hover:text-red-300 transition"
|
|
||||||
>
|
|
||||||
기본 설정으로 되돌리기
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function WeeklyDesignMocks() {
|
|
||||||
const [expanded, setExpanded] = useState(3)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-2">
|
|
||||||
{MOCK_WEEKS.map((w) => (
|
|
||||||
<div
|
|
||||||
key={w.n}
|
|
||||||
className={`rounded-xl border transition ${
|
|
||||||
w.custom ? 'border-emerald-500/30 bg-emerald-500/[0.03]' : 'border-white/5 bg-gray-950/30'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setExpanded(expanded === w.n ? null : w.n)}
|
|
||||||
className="w-full flex items-center gap-4 px-4 py-3.5 hover:bg-white/[0.02] transition text-left"
|
|
||||||
>
|
|
||||||
<div className="w-12 text-center shrink-0">
|
|
||||||
<div className="text-[11px] text-gray-500 leading-tight">주차</div>
|
|
||||||
<div className={`text-xl font-extrabold tabular-nums leading-tight ${w.custom ? 'text-emerald-300' : 'text-gray-200'}`}>
|
|
||||||
{w.n}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="text-sm text-gray-400 tabular-nums w-24 shrink-0">{w.date}</div>
|
|
||||||
|
|
||||||
<div className="flex-1 flex items-center gap-2">
|
|
||||||
{WEEKLY_BOSSES.map((b) => (
|
|
||||||
<BossAvatar key={b.key} boss={b} difficulty={w.diffs[b.key]} size={40} />
|
|
||||||
))}
|
|
||||||
{w.monthly && (
|
|
||||||
<BossAvatar boss={MONTHLY_BOSSES[0]} difficulty="hard" size={40} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-right shrink-0">
|
|
||||||
<div className="text-lg font-bold text-emerald-300 tabular-nums leading-tight">+{w.earn}</div>
|
|
||||||
<div className="text-[11px] text-gray-500 tabular-nums">누적 {w.cumulative.toLocaleString()}</div>
|
|
||||||
</div>
|
|
||||||
<svg
|
|
||||||
width="16" height="16" viewBox="0 0 12 12" fill="none"
|
|
||||||
className={`text-gray-500 transition-transform shrink-0 ${expanded === w.n ? 'rotate-180' : ''}`}
|
|
||||||
>
|
|
||||||
<path d="M3 4.5L6 7.5L9 4.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{expanded === w.n && (
|
|
||||||
<div className="border-t border-white/5 px-3 py-3 bg-gray-950/30">
|
|
||||||
<WeekEditor week={w} monthlyAlreadyAssigned={!w.monthly} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -95,15 +95,6 @@ export function calcPoints(basePoints, partySize) {
|
||||||
return Math.floor(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 dayjs from 'dayjs'
|
||||||
import utc from 'dayjs/plugin/utc'
|
import utc from 'dayjs/plugin/utc'
|
||||||
import timezone from 'dayjs/plugin/timezone'
|
import timezone from 'dayjs/plugin/timezone'
|
||||||
|
|
@ -117,10 +108,6 @@ export function formatDate(date) {
|
||||||
return dayjs(date).tz(KST).format('YYYY-MM-DD')
|
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() {
|
export function todayKST() {
|
||||||
return dayjs().tz(KST).startOf('day').toDate()
|
return dayjs().tz(KST).startOf('day').toDate()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue