데스티니 해방에 현재 진행 상태 섹션 추가
QuestSelector를 chapters/imageBase prop 받도록 일반화한 뒤, Destiny에 시작 날짜/진행 중인 결전/현재 결의 입력 섹션 구현. 결의 입력 max는 20,000 (6챕터 요구량 15,000 여유 포함). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0448b0bfc8
commit
d506c022ca
3 changed files with 77 additions and 9 deletions
|
|
@ -1,11 +1,17 @@
|
||||||
import { DESTINY_CHAPTERS, DESTINY_QUEST_IMAGE_BASE } from '../data'
|
import dayjs from 'dayjs'
|
||||||
|
import { DESTINY_CHAPTERS, DESTINY_QUEST_IMAGE_BASE, formatDate } from '../data'
|
||||||
import { useLiberationStore } from '../store'
|
import { useLiberationStore } from '../store'
|
||||||
import ProgressBar from './components/ProgressBar'
|
import ProgressBar from './components/ProgressBar'
|
||||||
|
import QuestSelector from './components/QuestSelector'
|
||||||
|
import PointsInput from './components/PointsInput'
|
||||||
|
import DatePicker from '../../../components/common/DatePicker'
|
||||||
|
|
||||||
export default function Destiny() {
|
export default function Destiny() {
|
||||||
const calcMode = useLiberationStore((s) => s.destinyCalcMode)
|
const calcMode = useLiberationStore((s) => s.destinyCalcMode)
|
||||||
const setCalcMode = useLiberationStore((s) => s.setDestinyCalcMode)
|
const setCalcMode = useLiberationStore((s) => s.setDestinyCalcMode)
|
||||||
const state = useLiberationStore((s) => s.destinyCalcMode === 'weekly' ? s.destinyWeekly : s.destinySimple)
|
const state = useLiberationStore((s) => s.destinyCalcMode === 'weekly' ? s.destinyWeekly : s.destinySimple)
|
||||||
|
const updateSlot = useLiberationStore((s) => s.updateDestinySlot)
|
||||||
|
const setState = (updater) => updateSlot(updater)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -49,6 +55,66 @@ export default function Destiny() {
|
||||||
completionDate={null}
|
completionDate={null}
|
||||||
completionColor="var(--destiny-date)"
|
completionColor="var(--destiny-date)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* 현재 진행 상태 입력 */}
|
||||||
|
<div
|
||||||
|
className="max-w-3xl mx-auto rounded-2xl border p-6 space-y-4"
|
||||||
|
style={{
|
||||||
|
background: 'var(--panel-bg)',
|
||||||
|
borderColor: 'var(--panel-border)',
|
||||||
|
boxShadow: 'var(--panel-shadow)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="text-lg font-semibold" style={{ color: 'var(--accent-bright)' }}>현재 진행 상태</div>
|
||||||
|
|
||||||
|
<div className="grid gap-3 grid-cols-3">
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<label className="block text-xs" style={{ color: 'var(--text-muted)' }}>시작 날짜</label>
|
||||||
|
<DatePicker
|
||||||
|
value={formatDate(state.startDate)}
|
||||||
|
onChange={(d) => setState((prev) => ({ ...prev, startDate: dayjs(d).toISOString() }))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<label className="block text-xs" style={{ color: 'var(--text-muted)' }}>진행 중인 퀘스트</label>
|
||||||
|
<QuestSelector
|
||||||
|
chapters={DESTINY_CHAPTERS}
|
||||||
|
imageBase={DESTINY_QUEST_IMAGE_BASE}
|
||||||
|
value={state.startChapter}
|
||||||
|
onChange={(idx) => setState((prev) => ({ ...prev, startChapter: idx }))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<label className="block text-xs" style={{ color: 'var(--text-muted)' }}>현재 결의</label>
|
||||||
|
<div
|
||||||
|
className="flex items-stretch rounded-lg border focus-within:border-[var(--input-border-focus)] hover:border-[var(--input-border-hover)]"
|
||||||
|
style={{
|
||||||
|
background: 'var(--input-bg)',
|
||||||
|
borderColor: 'var(--input-border)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PointsInput
|
||||||
|
value={state.currentPoints}
|
||||||
|
max={20000}
|
||||||
|
onChange={(n) => setState((prev) => ({ ...prev, currentPoints: n }))}
|
||||||
|
className="flex-1 min-w-0 bg-transparent px-3 h-12 text-base text-right tabular-nums outline-none"
|
||||||
|
style={{ color: 'var(--text-strong)' }}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className="flex items-center px-3 text-base border-l select-none tabular-nums"
|
||||||
|
style={{
|
||||||
|
borderColor: 'var(--input-border)',
|
||||||
|
color: 'var(--text-dim)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
/ {(DESTINY_CHAPTERS[state.startChapter]?.required ?? 0).toLocaleString()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -155,6 +155,8 @@ export default function Genesis() {
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<label className="block text-xs" style={{ color: 'var(--text-muted)' }}>진행 중인 퀘스트</label>
|
<label className="block text-xs" style={{ color: 'var(--text-muted)' }}>진행 중인 퀘스트</label>
|
||||||
<QuestSelector
|
<QuestSelector
|
||||||
|
chapters={GENESIS_CHAPTERS}
|
||||||
|
imageBase={QUEST_BOSS_IMAGE_BASE}
|
||||||
value={state.startChapter}
|
value={state.startChapter}
|
||||||
onChange={(idx) => setState((prev) => ({ ...prev, startChapter: idx }))}
|
onChange={(idx) => setState((prev) => ({ ...prev, startChapter: idx }))}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
import { useState, useEffect, useRef } from 'react'
|
import { useState, useEffect, useRef } from 'react'
|
||||||
import { motion, AnimatePresence } from 'framer-motion'
|
import { motion, AnimatePresence } from 'framer-motion'
|
||||||
import { GENESIS_CHAPTERS, QUEST_BOSS_IMAGE_BASE } from '../../data'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 진행 중인 퀘스트 드롭다운
|
* 진행 중인 퀘스트 드롭다운 (보스 초상화 + 이름)
|
||||||
* - 보스 초상화 + 이름 텍스트
|
* @param {Array} chapters - { idx, boss, ... }[]
|
||||||
|
* @param {string} imageBase - 보스 초상화 S3 base URL
|
||||||
*/
|
*/
|
||||||
export default function QuestSelector({ value, onChange }) {
|
export default function QuestSelector({ chapters, imageBase, value, onChange }) {
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
const ref = useRef(null)
|
const ref = useRef(null)
|
||||||
|
|
||||||
|
|
@ -19,7 +19,7 @@ export default function QuestSelector({ value, onChange }) {
|
||||||
return () => document.removeEventListener('mousedown', handler)
|
return () => document.removeEventListener('mousedown', handler)
|
||||||
}, [open])
|
}, [open])
|
||||||
|
|
||||||
const selected = GENESIS_CHAPTERS[value]
|
const selected = chapters[value]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={ref} className="relative">
|
<div ref={ref} className="relative">
|
||||||
|
|
@ -38,7 +38,7 @@ export default function QuestSelector({ value, onChange }) {
|
||||||
style={{ background: 'var(--surface-nested)' }}
|
style={{ background: 'var(--surface-nested)' }}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={`${QUEST_BOSS_IMAGE_BASE}/${selected.boss}.webp`}
|
src={`${imageBase}/${selected.boss}.webp`}
|
||||||
alt=""
|
alt=""
|
||||||
className="w-full h-full object-cover"
|
className="w-full h-full object-cover"
|
||||||
/>
|
/>
|
||||||
|
|
@ -69,7 +69,7 @@ export default function QuestSelector({ value, onChange }) {
|
||||||
boxShadow: 'var(--popup-shadow)',
|
boxShadow: 'var(--popup-shadow)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{GENESIS_CHAPTERS.map((chapter) => {
|
{chapters.map((chapter) => {
|
||||||
const isSelected = chapter.idx === value
|
const isSelected = chapter.idx === value
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
|
@ -86,7 +86,7 @@ export default function QuestSelector({ value, onChange }) {
|
||||||
style={{ background: 'var(--surface-nested)' }}
|
style={{ background: 'var(--surface-nested)' }}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={`${QUEST_BOSS_IMAGE_BASE}/${chapter.boss}.webp`}
|
src={`${imageBase}/${chapter.boss}.webp`}
|
||||||
alt=""
|
alt=""
|
||||||
className="w-full h-full object-cover"
|
className="w-full h-full object-cover"
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue