해방 날짜 계산기 + DatePicker 테마 토큰화
- Liberation 루트/ProgressBar/QuestSelector/WeeklyDefault/WeeklyScheduler 전체 이관 - DatePicker 드롭다운(연도/월/일 선택) 모두 semantic 토큰으로 대응 - 세그먼트 바, 배지, 탭, 초기화 버튼, 보스 Row 모두 테마 대응 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f0a04c51ff
commit
fe65c107c8
6 changed files with 367 additions and 180 deletions
|
|
@ -97,14 +97,19 @@ export default function DatePicker({ value, onChange, placeholder = '날짜 선
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={(e) => stop(e, () => setIsOpen(!isOpen))}
|
onClick={(e) => stop(e, () => setIsOpen(!isOpen))}
|
||||||
className={`w-full h-12 rounded-lg border bg-gray-950 px-4 text-base flex items-center justify-between transition ${
|
className="w-full h-12 rounded-lg border px-4 text-base flex items-center justify-between"
|
||||||
isOpen ? 'border-emerald-500/50' : 'border-white/10 hover:border-white/20'
|
style={{
|
||||||
}`}
|
background: 'var(--input-bg)',
|
||||||
|
borderColor: isOpen ? 'var(--input-border-focus)' : 'var(--input-border)',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<span className={value ? 'text-white' : 'text-gray-500'}>
|
<span style={{ color: value ? 'var(--text-strong)' : 'var(--input-placeholder)' }}>
|
||||||
{value ? formatDisplay(value) : placeholder}
|
{value ? formatDisplay(value) : placeholder}
|
||||||
</span>
|
</span>
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" className="text-gray-400">
|
<svg
|
||||||
|
width="16" height="16" viewBox="0 0 24 24" fill="none"
|
||||||
|
style={{ color: 'var(--input-icon)' }}
|
||||||
|
>
|
||||||
<rect x="3" y="4" width="18" height="18" rx="2" stroke="currentColor" strokeWidth="2" />
|
<rect x="3" y="4" width="18" height="18" rx="2" stroke="currentColor" strokeWidth="2" />
|
||||||
<path d="M16 2v4M8 2v4M3 10h18" stroke="currentColor" strokeWidth="2" />
|
<path d="M16 2v4M8 2v4M3 10h18" stroke="currentColor" strokeWidth="2" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|
@ -117,22 +122,29 @@ export default function DatePicker({ value, onChange, placeholder = '날짜 선
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
exit={{ opacity: 0, y: -6 }}
|
exit={{ opacity: 0, y: -6 }}
|
||||||
transition={{ duration: 0.15 }}
|
transition={{ duration: 0.15 }}
|
||||||
className="absolute z-50 mt-2 left-0 rounded-xl border border-white/10 bg-gray-900 shadow-2xl p-5"
|
className="absolute z-50 mt-2 left-0 rounded-xl border p-5"
|
||||||
style={{ width: 420 }}
|
style={{
|
||||||
|
width: 420,
|
||||||
|
background: 'var(--popup-bg)',
|
||||||
|
borderColor: 'var(--popup-border)',
|
||||||
|
boxShadow: 'var(--popup-shadow)',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={(e) => stop(e, viewMode === 'years' ? prevYearRange : prevMonth)}
|
onClick={(e) => stop(e, viewMode === 'years' ? prevYearRange : prevMonth)}
|
||||||
disabled={viewMode === 'years' ? !canGoPrevYearRange : (year === minYear && month === 0)}
|
disabled={viewMode === 'years' ? !canGoPrevYearRange : (year === minYear && month === 0)}
|
||||||
className="p-1.5 rounded hover:bg-white/5 disabled:opacity-30 disabled:cursor-not-allowed text-gray-400"
|
className="p-1.5 rounded hover:bg-[var(--row-hover-bg)] disabled:opacity-30 disabled:cursor-not-allowed"
|
||||||
|
style={{ color: 'var(--text-muted)' }}
|
||||||
>
|
>
|
||||||
<ChevronIcon dir="left" size={18} />
|
<ChevronIcon dir="left" size={18} />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={(e) => stop(e, () => setViewMode(viewMode === 'days' ? 'years' : 'days'))}
|
onClick={(e) => stop(e, () => setViewMode(viewMode === 'days' ? 'years' : 'days'))}
|
||||||
className="flex items-center gap-1 text-sm font-medium text-gray-200 hover:text-emerald-300 transition"
|
className="flex items-center gap-1 text-sm font-medium hover:text-[var(--accent-bright)]"
|
||||||
|
style={{ color: 'var(--text-emphasis)' }}
|
||||||
>
|
>
|
||||||
{viewMode === 'years' ? `${years[0]} - ${years[years.length - 1]}` : `${year}년 ${month + 1}월`}
|
{viewMode === 'years' ? `${years[0]} - ${years[years.length - 1]}` : `${year}년 ${month + 1}월`}
|
||||||
<ChevronIcon dir={viewMode !== 'days' ? 'up' : 'down'} size={14} className="transition-transform" />
|
<ChevronIcon dir={viewMode !== 'days' ? 'up' : 'down'} size={14} className="transition-transform" />
|
||||||
|
|
@ -140,7 +152,8 @@ export default function DatePicker({ value, onChange, placeholder = '날짜 선
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={(e) => stop(e, viewMode === 'years' ? nextYearRange : nextMonth)}
|
onClick={(e) => stop(e, viewMode === 'years' ? nextYearRange : nextMonth)}
|
||||||
className="p-1.5 rounded hover:bg-white/5 text-gray-400"
|
className="p-1.5 rounded hover:bg-[var(--row-hover-bg)]"
|
||||||
|
style={{ color: 'var(--text-muted)' }}
|
||||||
>
|
>
|
||||||
<ChevronIcon dir="right" size={18} />
|
<ChevronIcon dir="right" size={18} />
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -149,43 +162,51 @@ export default function DatePicker({ value, onChange, placeholder = '날짜 선
|
||||||
<AnimatePresence mode="wait">
|
<AnimatePresence mode="wait">
|
||||||
{viewMode === 'years' ? (
|
{viewMode === 'years' ? (
|
||||||
<motion.div key="years" initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ duration: 0.12 }}>
|
<motion.div key="years" initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ duration: 0.12 }}>
|
||||||
<div className="text-center text-xs text-gray-500 mb-2">연도</div>
|
<div className="text-center text-xs mb-2" style={{ color: 'var(--text-dim)' }}>연도</div>
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, minmax(0, 1fr))', gap: '6px', marginBottom: '12px' }}>
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, minmax(0, 1fr))', gap: '6px', marginBottom: '12px' }}>
|
||||||
{years.map((y) => (
|
{years.map((y) => {
|
||||||
<button
|
const isActive = year === y
|
||||||
key={y}
|
const isCurrent = currentYear === y && !isActive
|
||||||
type="button"
|
return (
|
||||||
onClick={(e) => stop(e, () => selectYear(y))}
|
<button
|
||||||
className={`py-2 rounded-lg text-sm transition ${
|
key={y}
|
||||||
year === y
|
type="button"
|
||||||
? 'bg-emerald-500 text-white'
|
onClick={(e) => stop(e, () => selectYear(y))}
|
||||||
: currentYear === y
|
className="py-2 rounded-lg text-sm hover:bg-[var(--row-hover-bg)]"
|
||||||
? 'text-emerald-300 hover:bg-white/5'
|
style={isActive ? {
|
||||||
: 'text-gray-300 hover:bg-white/5'
|
background: 'var(--btn-primary-bg)',
|
||||||
}`}
|
color: 'var(--btn-primary-text)',
|
||||||
>
|
} : {
|
||||||
{y}
|
color: isCurrent ? 'var(--accent-bright)' : 'var(--text-emphasis)',
|
||||||
</button>
|
}}
|
||||||
))}
|
>
|
||||||
|
{y}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center text-xs text-gray-500 mb-2">월</div>
|
<div className="text-center text-xs mb-2" style={{ color: 'var(--text-dim)' }}>월</div>
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, minmax(0, 1fr))', gap: '6px' }}>
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, minmax(0, 1fr))', gap: '6px' }}>
|
||||||
{monthNames.map((m, i) => (
|
{monthNames.map((m, i) => {
|
||||||
<button
|
const isActive = month === i
|
||||||
key={m}
|
const isCurrent = (currentYear === year && currentMonth === i) && !isActive
|
||||||
type="button"
|
return (
|
||||||
onClick={(e) => stop(e, () => selectMonth(i))}
|
<button
|
||||||
className={`py-2 rounded-lg text-sm transition ${
|
key={m}
|
||||||
month === i
|
type="button"
|
||||||
? 'bg-emerald-500 text-white'
|
onClick={(e) => stop(e, () => selectMonth(i))}
|
||||||
: (currentYear === year && currentMonth === i)
|
className="py-2 rounded-lg text-sm hover:bg-[var(--row-hover-bg)]"
|
||||||
? 'text-emerald-300 hover:bg-white/5'
|
style={isActive ? {
|
||||||
: 'text-gray-300 hover:bg-white/5'
|
background: 'var(--btn-primary-bg)',
|
||||||
}`}
|
color: 'var(--btn-primary-text)',
|
||||||
>
|
} : {
|
||||||
{m}
|
color: isCurrent ? 'var(--accent-bright)' : 'var(--text-emphasis)',
|
||||||
</button>
|
}}
|
||||||
))}
|
>
|
||||||
|
{m}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -194,9 +215,11 @@ export default function DatePicker({ value, onChange, placeholder = '날짜 선
|
||||||
{['일', '월', '화', '수', '목', '금', '토'].map((d, i) => (
|
{['일', '월', '화', '수', '목', '금', '토'].map((d, i) => (
|
||||||
<div
|
<div
|
||||||
key={d}
|
key={d}
|
||||||
className={`text-center text-xs font-medium py-1 ${
|
className="text-center text-xs font-medium py-1"
|
||||||
i === 0 ? 'text-red-400/80' : i === 6 ? 'text-sky-400/80' : 'text-gray-500'
|
style={{
|
||||||
}`}
|
color: i === 0 ? 'var(--danger-text)' : i === 6 ? '#60a5fa' : 'var(--text-dim)',
|
||||||
|
opacity: i === 0 || i === 6 ? 0.8 : 1,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{d}
|
{d}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -207,20 +230,25 @@ export default function DatePicker({ value, onChange, placeholder = '날짜 선
|
||||||
const dw = i % 7
|
const dw = i % 7
|
||||||
const selected = isSelected(day)
|
const selected = isSelected(day)
|
||||||
const today = isToday(day)
|
const today = isToday(day)
|
||||||
|
const textColor = today && !selected ? 'var(--accent-bright)'
|
||||||
|
: day && !selected && !today && dw === 0 ? 'var(--danger-text)'
|
||||||
|
: day && !selected && !today && dw === 6 ? '#60a5fa'
|
||||||
|
: day && !selected && !today ? 'var(--text-emphasis)'
|
||||||
|
: undefined
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={i}
|
key={i}
|
||||||
type="button"
|
type="button"
|
||||||
disabled={!day}
|
disabled={!day}
|
||||||
onClick={(e) => day && stop(e, () => selectDate(day))}
|
onClick={(e) => day && stop(e, () => selectDate(day))}
|
||||||
style={{ aspectRatio: '1 / 1' }}
|
style={{
|
||||||
className={`rounded-full text-base font-medium flex items-center justify-center transition-all
|
aspectRatio: '1 / 1',
|
||||||
${!day ? '' : 'hover:bg-white/5'}
|
background: selected ? 'var(--btn-primary-bg)' : undefined,
|
||||||
${selected ? 'bg-emerald-500 text-white hover:bg-emerald-500' : ''}
|
color: selected ? 'var(--btn-primary-text)' : textColor,
|
||||||
${today && !selected ? 'text-emerald-300 font-bold' : ''}
|
fontWeight: today && !selected ? 'bold' : undefined,
|
||||||
${day && !selected && !today && dw === 0 ? 'text-red-400' : ''}
|
}}
|
||||||
${day && !selected && !today && dw === 6 ? 'text-sky-400' : ''}
|
className={`rounded-full text-base font-medium flex items-center justify-center
|
||||||
${day && !selected && !today && dw > 0 && dw < 6 ? 'text-gray-300' : ''}
|
${!day ? '' : 'hover:bg-[var(--row-hover-bg)]'}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
{day}
|
{day}
|
||||||
|
|
|
||||||
|
|
@ -277,48 +277,76 @@ export default function Liberation() {
|
||||||
{[
|
{[
|
||||||
{ key: 'genesis', label: '제네시스 해방', img: genesisImg.data?.url },
|
{ key: 'genesis', label: '제네시스 해방', img: genesisImg.data?.url },
|
||||||
{ key: 'destiny', label: '데스티니 해방', img: destinyImg.data?.url },
|
{ key: 'destiny', label: '데스티니 해방', img: destinyImg.data?.url },
|
||||||
].map((tab) => (
|
].map((tab) => {
|
||||||
<button
|
const active = liberationType === tab.key
|
||||||
key={tab.key}
|
return (
|
||||||
type="button"
|
<button
|
||||||
onClick={() => setLiberationType(tab.key)}
|
key={tab.key}
|
||||||
className={`flex-1 flex items-center justify-center gap-3 rounded-2xl border px-5 py-3 transition ${
|
type="button"
|
||||||
liberationType === tab.key
|
onClick={() => setLiberationType(tab.key)}
|
||||||
? 'border-emerald-500/50 bg-emerald-500/10 text-emerald-200 shadow-lg shadow-emerald-500/10'
|
className="flex-1 flex items-center justify-center gap-3 rounded-2xl border px-5 py-3"
|
||||||
: 'border-white/10 bg-gray-900/40 text-gray-400 hover:border-white/20 hover:text-gray-200'
|
style={active ? {
|
||||||
}`}
|
background: 'var(--selected-bg)',
|
||||||
>
|
borderColor: 'var(--selected-border)',
|
||||||
{tab.img && <img src={tab.img} alt="" className="w-8 h-8 object-contain" />}
|
color: 'var(--accent-bright)',
|
||||||
<span className="text-base font-semibold">{tab.label}</span>
|
boxShadow: 'var(--btn-primary-shadow)',
|
||||||
</button>
|
} : {
|
||||||
))}
|
background: 'var(--panel-bg)',
|
||||||
|
borderColor: 'var(--panel-border)',
|
||||||
|
color: 'var(--text-muted)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tab.img && <img src={tab.img} alt="" className="w-8 h-8 object-contain" />}
|
||||||
|
<span className="text-base font-semibold">{tab.label}</span>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{liberationType === 'destiny' ? (
|
{liberationType === 'destiny' ? (
|
||||||
<div className="max-w-3xl mx-auto rounded-2xl border border-white/10 bg-gray-900/60 p-16 text-center space-y-3 flex flex-col items-center justify-center" style={{ minHeight: 'calc(100vh - 220px)' }}>
|
<div
|
||||||
<div className="text-2xl font-bold text-gray-300">구현 예정</div>
|
className="max-w-3xl mx-auto rounded-2xl border p-16 text-center space-y-3 flex flex-col items-center justify-center"
|
||||||
<div className="text-sm text-gray-500">데스티니 해방 계산기는 준비 중입니다.</div>
|
style={{
|
||||||
|
minHeight: 'calc(100vh - 220px)',
|
||||||
|
background: 'var(--panel-bg)',
|
||||||
|
borderColor: 'var(--panel-border)',
|
||||||
|
boxShadow: 'var(--panel-shadow)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="text-2xl font-bold" style={{ color: 'var(--text-emphasis)' }}>구현 예정</div>
|
||||||
|
<div className="text-sm" style={{ color: 'var(--text-dim)' }}>데스티니 해방 계산기는 준비 중입니다.</div>
|
||||||
</div>
|
</div>
|
||||||
) : (<>
|
) : (<>
|
||||||
{/* 계산 모드 탭 */}
|
{/* 계산 모드 탭 */}
|
||||||
<div className="max-w-3xl mx-auto flex gap-1 p-1 rounded-xl border border-white/10 bg-gray-950/60">
|
<div
|
||||||
|
className="max-w-3xl mx-auto flex gap-1 p-1 rounded-xl border"
|
||||||
|
style={{
|
||||||
|
background: 'var(--surface-3)',
|
||||||
|
borderColor: 'var(--panel-border)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
{[
|
{[
|
||||||
{ key: 'simple', label: '단순 계산' },
|
{ key: 'simple', label: '단순 계산' },
|
||||||
{ key: 'weekly', label: '주차별 계산' },
|
{ key: 'weekly', label: '주차별 계산' },
|
||||||
].map((t) => (
|
].map((t) => {
|
||||||
<button
|
const active = calcMode === t.key
|
||||||
key={t.key}
|
return (
|
||||||
type="button"
|
<button
|
||||||
onClick={() => setCalcMode(t.key)}
|
key={t.key}
|
||||||
className={`flex-1 h-10 rounded-lg text-sm font-semibold transition ${
|
type="button"
|
||||||
calcMode === t.key
|
onClick={() => setCalcMode(t.key)}
|
||||||
? 'bg-emerald-500/20 text-emerald-300'
|
className="flex-1 h-10 rounded-lg text-sm font-semibold"
|
||||||
: 'text-gray-400 hover:text-gray-200 hover:bg-white/5'
|
style={active ? {
|
||||||
}`}
|
background: 'var(--selected-bg)',
|
||||||
>
|
color: 'var(--accent-bright)',
|
||||||
{t.label}
|
} : {
|
||||||
</button>
|
color: 'var(--text-muted)',
|
||||||
))}
|
}}
|
||||||
|
>
|
||||||
|
{t.label}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
|
|
@ -328,12 +356,19 @@ export default function Liberation() {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 현재 진행 상태 입력 */}
|
{/* 현재 진행 상태 입력 */}
|
||||||
<div className="max-w-3xl mx-auto rounded-2xl border border-white/10 bg-gray-900/60 p-6 space-y-4">
|
<div
|
||||||
<div className="text-lg font-semibold text-emerald-300">현재 진행 상태</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="grid gap-3 grid-cols-3">
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<label className="block text-xs text-gray-400">시작 날짜</label>
|
<label className="block text-xs" style={{ color: 'var(--text-muted)' }}>시작 날짜</label>
|
||||||
<DatePicker
|
<DatePicker
|
||||||
value={formatDate(state.startDate)}
|
value={formatDate(state.startDate)}
|
||||||
onChange={(d) => setState((prev) => ({ ...prev, startDate: dayjs(d).toISOString() }))}
|
onChange={(d) => setState((prev) => ({ ...prev, startDate: dayjs(d).toISOString() }))}
|
||||||
|
|
@ -341,7 +376,7 @@ export default function Liberation() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<label className="block text-xs text-gray-400">진행 중인 퀘스트</label>
|
<label className="block text-xs" style={{ color: 'var(--text-muted)' }}>진행 중인 퀘스트</label>
|
||||||
<QuestSelector
|
<QuestSelector
|
||||||
value={state.startChapter}
|
value={state.startChapter}
|
||||||
onChange={(idx) => setState((prev) => ({ ...prev, startChapter: idx }))}
|
onChange={(idx) => setState((prev) => ({ ...prev, startChapter: idx }))}
|
||||||
|
|
@ -349,15 +384,28 @@ export default function Liberation() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<label className="block text-xs text-gray-400">현재 흔적</label>
|
<label className="block text-xs" style={{ color: 'var(--text-muted)' }}>현재 흔적</label>
|
||||||
<div className="flex items-stretch rounded-lg border border-white/10 bg-gray-950 transition focus-within:border-emerald-500/50 hover:border-white/20">
|
<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
|
<PointsInput
|
||||||
value={state.currentPoints}
|
value={state.currentPoints}
|
||||||
max={3000}
|
max={3000}
|
||||||
onChange={(n) => setState((prev) => ({ ...prev, currentPoints: n }))}
|
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"
|
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 text-gray-500 border-l border-white/10 select-none tabular-nums">
|
<span
|
||||||
|
className="flex items-center px-3 text-base border-l select-none tabular-nums"
|
||||||
|
style={{
|
||||||
|
borderColor: 'var(--input-border)',
|
||||||
|
color: 'var(--text-dim)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
/ {(GENESIS_CHAPTERS[state.startChapter]?.required ?? 0).toLocaleString()}
|
/ {(GENESIS_CHAPTERS[state.startChapter]?.required ?? 0).toLocaleString()}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -381,7 +429,12 @@ export default function Liberation() {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setResetOpen(true)}
|
onClick={() => setResetOpen(true)}
|
||||||
className="inline-flex items-center gap-2 rounded-lg border border-red-500/50 bg-red-500/10 hover:bg-red-500/20 text-red-300 hover:text-red-200 px-5 py-2.5 text-sm font-semibold transition shadow-lg shadow-red-500/10"
|
className="inline-flex items-center gap-2 rounded-lg border px-5 py-2.5 text-sm font-semibold hover:bg-[var(--danger-bg-hover)]"
|
||||||
|
style={{
|
||||||
|
borderColor: 'var(--icon-danger-border)',
|
||||||
|
background: 'var(--icon-danger-bg)',
|
||||||
|
color: 'var(--danger-text)',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||||
<path d="M2 3H14M6 3V2C6 1.45 6.45 1 7 1H9C9.55 1 10 1.45 10 2V3M3 3L4 14C4 14.55 4.45 15 5 15H11C11.55 15 12 14.55 12 14L13 3" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" />
|
<path d="M2 3H14M6 3V2C6 1.45 6.45 1 7 1H9C9.55 1 10 1.45 10 2V3M3 3L4 14C4 14.55 4.45 15 5 15H11C11.55 15 12 14.55 12 14L13 3" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,13 @@ export default function ProgressBar({ startChapter, currentPoints, completionDat
|
||||||
|
|
||||||
const renderSegment = ({ chapter, status, current }) => {
|
const renderSegment = ({ chapter, status, current }) => {
|
||||||
const pct = (current / chapter.required) * 100
|
const pct = (current / chapter.required) * 100
|
||||||
const bg = status === 'done' ? '#10b981' : status === 'active' ? '#fbbf24' : 'transparent'
|
const bg = status === 'done' ? 'var(--progress-emerald)' : status === 'active' ? 'var(--progress-amber)' : 'transparent'
|
||||||
return (
|
return (
|
||||||
<div key={`seg-${chapter.idx}`} className="flex-1 h-2 rounded bg-gray-900 overflow-hidden">
|
<div
|
||||||
|
key={`seg-${chapter.idx}`}
|
||||||
|
className="flex-1 h-2 rounded overflow-hidden"
|
||||||
|
style={{ background: 'var(--progress-track)' }}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className="h-full transition-all"
|
className="h-full transition-all"
|
||||||
style={{ width: `${pct}%`, background: bg }}
|
style={{ width: `${pct}%`, background: bg }}
|
||||||
|
|
@ -32,7 +36,7 @@ export default function ProgressBar({ startChapter, currentPoints, completionDat
|
||||||
|
|
||||||
const renderPortrait = ({ chapter, status }) => (
|
const renderPortrait = ({ chapter, status }) => (
|
||||||
<div key={`p-${chapter.idx}`} className="flex-1 flex flex-col items-center gap-1.5 min-w-0">
|
<div key={`p-${chapter.idx}`} className="flex-1 flex flex-col items-center gap-1.5 min-w-0">
|
||||||
<div className={`w-full aspect-square rounded-lg overflow-hidden transition ${
|
<div className={`w-full aspect-square rounded-lg overflow-hidden ${
|
||||||
status === 'active' ? 'shadow-lg shadow-amber-500/20' :
|
status === 'active' ? 'shadow-lg shadow-amber-500/20' :
|
||||||
status === 'pending' ? 'opacity-50' : ''
|
status === 'pending' ? 'opacity-50' : ''
|
||||||
}`}>
|
}`}>
|
||||||
|
|
@ -42,19 +46,29 @@ export default function ProgressBar({ startChapter, currentPoints, completionDat
|
||||||
className={`block w-full h-full object-cover ${status === 'pending' ? 'grayscale' : ''}`}
|
className={`block w-full h-full object-cover ${status === 'pending' ? 'grayscale' : ''}`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={`text-sm font-medium ${
|
<div
|
||||||
status === 'done' ? 'text-emerald-300' :
|
className="text-sm font-medium"
|
||||||
status === 'active' ? 'text-amber-300' : 'text-gray-500'
|
style={{
|
||||||
}`}>
|
color: status === 'done' ? 'var(--accent-bright)' :
|
||||||
|
status === 'active' ? 'var(--warning-text-bright)' : 'var(--text-dim)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
{chapter.boss}
|
{chapter.boss}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-3xl mx-auto rounded-2xl border border-white/10 bg-gray-900/60 p-6 space-y-5">
|
<div
|
||||||
|
className="max-w-3xl mx-auto rounded-2xl border p-6 space-y-5"
|
||||||
|
style={{
|
||||||
|
background: 'var(--panel-bg)',
|
||||||
|
borderColor: 'var(--panel-border)',
|
||||||
|
boxShadow: 'var(--panel-shadow)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
{/* 섹션 제목 */}
|
{/* 섹션 제목 */}
|
||||||
<div className="text-lg font-semibold text-emerald-300">퀘스트 진행 상황</div>
|
<div className="text-lg font-semibold" style={{ color: 'var(--accent-bright)' }}>퀘스트 진행 상황</div>
|
||||||
|
|
||||||
{/* 1차 / 2차 라벨 + 세그먼트 바 */}
|
{/* 1차 / 2차 라벨 + 세그먼트 바 */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
|
|
@ -79,11 +93,17 @@ export default function ProgressBar({ startChapter, currentPoints, completionDat
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 예상 해방 날짜 */}
|
{/* 예상 해방 날짜 */}
|
||||||
<div className="flex items-center justify-center gap-3 pt-4 border-t border-white/5">
|
<div
|
||||||
<span className="text-lg font-semibold text-white">예상 해방 날짜</span>
|
className="flex items-center justify-center gap-3 pt-4 border-t"
|
||||||
<span className="text-gray-600">·</span>
|
style={{ borderColor: 'var(--panel-border)' }}
|
||||||
<span className="text-xl font-bold tabular-nums text-amber-400">
|
>
|
||||||
{completionDate ? formatKoreanDate(completionDate) : <span className="text-gray-500 font-normal">미정</span>}
|
<span className="text-lg font-semibold" style={{ color: 'var(--text-strong)' }}>예상 해방 날짜</span>
|
||||||
|
<span style={{ color: 'var(--text-dim)' }}>·</span>
|
||||||
|
<span
|
||||||
|
className="text-xl font-bold tabular-nums"
|
||||||
|
style={{ color: 'var(--warning-text-bright)' }}
|
||||||
|
>
|
||||||
|
{completionDate ? formatKoreanDate(completionDate) : <span className="font-normal" style={{ color: 'var(--text-dim)' }}>미정</span>}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -26,23 +26,30 @@ export default function QuestSelector({ value, onChange }) {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setOpen((v) => !v)}
|
onClick={() => setOpen((v) => !v)}
|
||||||
className={`w-full h-12 flex items-center gap-3 rounded-lg border bg-gray-950 pl-2 pr-3 transition ${
|
className="w-full h-12 flex items-center gap-3 rounded-lg border pl-2 pr-3"
|
||||||
open ? 'border-emerald-500/50' : 'border-white/10 hover:border-white/20'
|
style={{
|
||||||
}`}
|
background: 'var(--input-bg)',
|
||||||
|
borderColor: open ? 'var(--input-border-focus)' : 'var(--input-border)',
|
||||||
|
color: 'var(--text-strong)',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div className="w-9 h-9 rounded overflow-hidden shrink-0 bg-gray-900">
|
<div
|
||||||
|
className="w-9 h-9 rounded overflow-hidden shrink-0"
|
||||||
|
style={{ background: 'var(--surface-nested)' }}
|
||||||
|
>
|
||||||
<img
|
<img
|
||||||
src={`${QUEST_BOSS_IMAGE_BASE}/${selected.boss}.webp`}
|
src={`${QUEST_BOSS_IMAGE_BASE}/${selected.boss}.webp`}
|
||||||
alt=""
|
alt=""
|
||||||
className="w-full h-full object-cover"
|
className="w-full h-full object-cover"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span className="flex-1 text-left text-sm font-medium text-gray-100">
|
<span className="flex-1 text-left text-sm font-medium">
|
||||||
{selected.boss}
|
{selected.boss}
|
||||||
</span>
|
</span>
|
||||||
<svg
|
<svg
|
||||||
width="14" height="14" viewBox="0 0 12 12" fill="none"
|
width="14" height="14" viewBox="0 0 12 12" fill="none"
|
||||||
className={`text-gray-400 transition-transform ${open ? 'rotate-180' : ''}`}
|
className={`transition-transform ${open ? 'rotate-180' : ''}`}
|
||||||
|
style={{ color: 'var(--input-icon)' }}
|
||||||
>
|
>
|
||||||
<path d="M3 4.5L6 7.5L9 4.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
<path d="M3 4.5L6 7.5L9 4.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|
@ -55,7 +62,12 @@ export default function QuestSelector({ value, onChange }) {
|
||||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||||
exit={{ opacity: 0, y: -6, scale: 0.98 }}
|
exit={{ opacity: 0, y: -6, scale: 0.98 }}
|
||||||
transition={{ duration: 0.15 }}
|
transition={{ duration: 0.15 }}
|
||||||
className="absolute top-full left-0 right-0 mt-1 z-50 rounded-lg border border-white/10 bg-gray-900 shadow-2xl py-1 max-h-72 overflow-y-auto origin-top"
|
className="absolute top-full left-0 right-0 mt-1 z-50 rounded-lg border py-1 max-h-72 overflow-y-auto origin-top"
|
||||||
|
style={{
|
||||||
|
background: 'var(--popup-bg)',
|
||||||
|
borderColor: 'var(--popup-border)',
|
||||||
|
boxShadow: 'var(--popup-shadow)',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{GENESIS_CHAPTERS.map((chapter) => {
|
{GENESIS_CHAPTERS.map((chapter) => {
|
||||||
const isSelected = chapter.idx === value
|
const isSelected = chapter.idx === value
|
||||||
|
|
@ -64,20 +76,25 @@ export default function QuestSelector({ value, onChange }) {
|
||||||
key={chapter.idx}
|
key={chapter.idx}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => { onChange(chapter.idx); setOpen(false) }}
|
onClick={() => { onChange(chapter.idx); setOpen(false) }}
|
||||||
className={`w-full flex items-center gap-3 px-2 py-1.5 transition ${
|
className="w-full flex items-center gap-3 px-2 py-1.5"
|
||||||
isSelected ? 'bg-emerald-500/10' : 'hover:bg-white/5'
|
style={isSelected ? { background: 'var(--option-selected-bg)' } : undefined}
|
||||||
}`}
|
onMouseEnter={(e) => { if (!isSelected) e.currentTarget.style.background = 'var(--row-hover-bg)' }}
|
||||||
|
onMouseLeave={(e) => { if (!isSelected) e.currentTarget.style.background = '' }}
|
||||||
>
|
>
|
||||||
<div className="w-9 h-9 rounded overflow-hidden shrink-0 bg-gray-950">
|
<div
|
||||||
|
className="w-9 h-9 rounded overflow-hidden shrink-0"
|
||||||
|
style={{ background: 'var(--surface-nested)' }}
|
||||||
|
>
|
||||||
<img
|
<img
|
||||||
src={`${QUEST_BOSS_IMAGE_BASE}/${chapter.boss}.webp`}
|
src={`${QUEST_BOSS_IMAGE_BASE}/${chapter.boss}.webp`}
|
||||||
alt=""
|
alt=""
|
||||||
className="w-full h-full object-cover"
|
className="w-full h-full object-cover"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span className={`flex-1 text-left text-sm font-medium ${
|
<span
|
||||||
isSelected ? 'text-emerald-300' : 'text-gray-200'
|
className="flex-1 text-left text-sm font-medium"
|
||||||
}`}>
|
style={{ color: isSelected ? 'var(--option-selected-text)' : 'var(--text-emphasis)' }}
|
||||||
|
>
|
||||||
{chapter.boss}
|
{chapter.boss}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,11 @@ const PARTY_OPTIONS = [1, 2, 3, 4, 5, 6].map((n) => ({ value: n, label: `${n}인
|
||||||
const NONE_DIFFICULTY = { key: 'none', label: '격파 불가', points: 0 }
|
const NONE_DIFFICULTY = { key: 'none', label: '격파 불가', points: 0 }
|
||||||
|
|
||||||
function diffLabel(d, party) {
|
function diffLabel(d, party) {
|
||||||
if (d.key === 'none') return <span className="text-gray-500">격파 불가</span>
|
if (d.key === 'none') return <span style={{ color: 'var(--text-dim)' }}>격파 불가</span>
|
||||||
const earned = calcPoints(d.points, party)
|
const earned = calcPoints(d.points, party)
|
||||||
return (
|
return (
|
||||||
<span>
|
<span>
|
||||||
{d.label} <span className="text-emerald-400">+{earned}</span>
|
{d.label} <span style={{ color: 'var(--accent-bright)' }}>+{earned}</span>
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -22,13 +22,20 @@ export function BossRow({ boss, sel, onChange, monthly = false, showDone = true
|
||||||
.map((d) => ({ value: d.key, label: diffLabel(d, sel.party) }))
|
.map((d) => ({ value: d.key, label: diffLabel(d, sel.party) }))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-3 rounded-lg px-3 h-16 transition">
|
<div className="flex items-center gap-3 rounded-lg px-3 h-16">
|
||||||
<Tooltip text={boss.name}>
|
<Tooltip text={boss.name}>
|
||||||
<img src={`${LIBERATION_BOSS_IMAGE_BASE}/${boss.image}`} alt="" className="w-10 h-10 rounded-md object-cover shrink-0" />
|
<img src={`${LIBERATION_BOSS_IMAGE_BASE}/${boss.image}`} alt="" className="w-10 h-10 rounded-md object-cover shrink-0" />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<span className="text-base font-semibold flex-1 truncate">
|
<span className="text-base font-semibold flex-1 truncate">
|
||||||
{boss.name}
|
{boss.name}
|
||||||
{monthly && <span className="ml-1.5 text-[11px] text-amber-400/80 font-medium">월간</span>}
|
{monthly && (
|
||||||
|
<span
|
||||||
|
className="ml-1.5 text-[11px] font-medium"
|
||||||
|
style={{ color: 'var(--warning-text)' }}
|
||||||
|
>
|
||||||
|
월간
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div className="w-36">
|
<div className="w-36">
|
||||||
|
|
@ -54,13 +61,18 @@ export function BossRow({ boss, sel, onChange, monthly = false, showDone = true
|
||||||
type="button"
|
type="button"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onClick={() => onChange({ done: !sel.done })}
|
onClick={() => onChange({ done: !sel.done })}
|
||||||
className={`shrink-0 w-20 rounded-md h-8 text-xs font-semibold transition border ${
|
className="shrink-0 w-20 rounded-md h-8 text-xs font-semibold border disabled:cursor-not-allowed"
|
||||||
disabled
|
style={disabled ? {
|
||||||
? 'border-white/5 text-gray-700 cursor-not-allowed'
|
borderColor: 'var(--panel-border)',
|
||||||
: sel.done
|
color: 'var(--text-dim)',
|
||||||
? 'bg-emerald-500/20 border-emerald-500/50 text-emerald-300'
|
} : sel.done ? {
|
||||||
: 'border-white/10 text-gray-500 hover:border-white/20 hover:text-gray-300'
|
background: 'var(--selected-bg)',
|
||||||
}`}
|
borderColor: 'var(--selected-border)',
|
||||||
|
color: 'var(--accent-bright)',
|
||||||
|
} : {
|
||||||
|
borderColor: 'var(--btn-border)',
|
||||||
|
color: 'var(--text-dim)',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{sel.done ? '완료' : '미완료'}
|
{sel.done ? '완료' : '미완료'}
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -78,42 +90,59 @@ export default function WeeklyDefault({ weekly, onChange, totalWeekly, totalMont
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-3xl mx-auto rounded-2xl border border-white/10 bg-gray-900/60 p-6 space-y-4">
|
<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="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="text-lg font-semibold text-emerald-300">주간 보스 설정</div>
|
<div className="text-lg font-semibold" style={{ color: 'var(--accent-bright)' }}>주간 보스 설정</div>
|
||||||
<div className="text-sm tabular-nums">
|
<div className="text-sm tabular-nums">
|
||||||
{mode === 'weekly' ? (
|
{mode === 'weekly' ? (
|
||||||
<>
|
<>
|
||||||
<span className="text-emerald-300 font-semibold">{totalWeekly}</span>
|
<span className="font-semibold" style={{ color: 'var(--accent-bright)' }}>{totalWeekly}</span>
|
||||||
<span className="text-gray-500 mx-1">+</span>
|
<span className="mx-1" style={{ color: 'var(--text-dim)' }}>+</span>
|
||||||
<span className="text-amber-300 font-semibold">{totalMonthly}</span>
|
<span className="font-semibold" style={{ color: 'var(--warning-text-bright)' }}>{totalMonthly}</span>
|
||||||
<span className="text-gray-500 mx-1">/</span>
|
<span className="mx-1" style={{ color: 'var(--text-dim)' }}>/</span>
|
||||||
<span className="text-gray-300 font-semibold">{(remaining ?? 0).toLocaleString()}</span>
|
<span className="font-semibold" style={{ color: 'var(--text-emphasis)' }}>{(remaining ?? 0).toLocaleString()}</span>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-emerald-300 font-semibold">+{totalWeekly + totalMonthly}</span>
|
<span className="font-semibold" style={{ color: 'var(--accent-bright)' }}>+{totalWeekly + totalMonthly}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{mode === 'simple' ? (
|
{mode === 'simple' ? (
|
||||||
<div className="divide-y divide-white/5">
|
<div>
|
||||||
{WEEKLY_BOSSES.map((boss) => (
|
{WEEKLY_BOSSES.map((boss, i) => (
|
||||||
<BossRow
|
<div
|
||||||
key={boss.key}
|
key={boss.key}
|
||||||
boss={boss}
|
className={i > 0 ? 'border-t' : ''}
|
||||||
sel={weekly.bosses[boss.key]}
|
style={i > 0 ? { borderColor: 'var(--row-divider)' } : undefined}
|
||||||
onChange={(patch) => updateBoss(boss.key, patch)}
|
>
|
||||||
/>
|
<BossRow
|
||||||
|
boss={boss}
|
||||||
|
sel={weekly.bosses[boss.key]}
|
||||||
|
onChange={(patch) => updateBoss(boss.key, patch)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
{MONTHLY_BOSSES.map((boss) => (
|
{MONTHLY_BOSSES.map((boss) => (
|
||||||
<BossRow
|
<div
|
||||||
key={boss.key}
|
key={boss.key}
|
||||||
boss={boss}
|
className="border-t"
|
||||||
sel={weekly.blackMage}
|
style={{ borderColor: 'var(--row-divider)' }}
|
||||||
onChange={updateBlackMage}
|
>
|
||||||
monthly
|
<BossRow
|
||||||
/>
|
boss={boss}
|
||||||
|
sel={weekly.blackMage}
|
||||||
|
onChange={updateBlackMage}
|
||||||
|
monthly
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
||||||
|
|
@ -63,8 +63,13 @@ function BossAvatar({ boss, difficulty, size = 40 }) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center gap-1">
|
<div className="flex flex-col items-center gap-1">
|
||||||
<div
|
<div
|
||||||
className={`rounded-md overflow-hidden bg-gray-900 border border-white/5 ${enabled ? '' : 'opacity-30 grayscale'}`}
|
className={`rounded-md overflow-hidden border ${enabled ? '' : 'opacity-30 grayscale'}`}
|
||||||
style={{ width: size, height: size }}
|
style={{
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
background: 'var(--surface-nested)',
|
||||||
|
borderColor: 'var(--panel-border)',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<img src={`${LIBERATION_BOSS_IMAGE_BASE}/${boss.image}`} alt={boss.name} className="w-full h-full object-cover" />
|
<img src={`${LIBERATION_BOSS_IMAGE_BASE}/${boss.image}`} alt={boss.name} className="w-full h-full object-cover" />
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -72,9 +77,9 @@ function BossAvatar({ boss, difficulty, size = 40 }) {
|
||||||
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"
|
||||||
style={{
|
style={{
|
||||||
width: 16, height: 16,
|
width: 16, height: 16,
|
||||||
color: badge?.color || '#4b5563',
|
color: badge?.color || 'var(--text-dim)',
|
||||||
background: badge?.bg || 'transparent',
|
background: badge?.bg || 'transparent',
|
||||||
borderColor: badge?.border || 'rgba(255,255,255,0.08)',
|
borderColor: badge?.border || 'var(--panel-border)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{badge?.label || '-'}
|
{badge?.label || '-'}
|
||||||
|
|
@ -94,17 +99,25 @@ function WeekEditor({ config, onChange, isCurrent, monthlyLockedByWeek }) {
|
||||||
const blackmageLocked = monthlyLockedByWeek != null
|
const blackmageLocked = monthlyLockedByWeek != null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="divide-y divide-white/5">
|
<div>
|
||||||
{WEEKLY_BOSSES.map((boss) => (
|
{WEEKLY_BOSSES.map((boss, i) => (
|
||||||
<BossRow
|
<div
|
||||||
key={boss.key}
|
key={boss.key}
|
||||||
boss={boss}
|
className={i > 0 ? 'border-t' : ''}
|
||||||
sel={config.bosses[boss.key]}
|
style={i > 0 ? { borderColor: 'var(--row-divider)' } : undefined}
|
||||||
onChange={(patch) => updateBoss(boss.key, patch)}
|
>
|
||||||
showDone={isCurrent}
|
<BossRow
|
||||||
/>
|
boss={boss}
|
||||||
|
sel={config.bosses[boss.key]}
|
||||||
|
onChange={(patch) => updateBoss(boss.key, patch)}
|
||||||
|
showDone={isCurrent}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
<div className={blackmageLocked ? 'opacity-40 pointer-events-none' : ''}>
|
<div
|
||||||
|
className={`border-t ${blackmageLocked ? 'opacity-40 pointer-events-none' : ''}`}
|
||||||
|
style={{ borderColor: 'var(--row-divider)' }}
|
||||||
|
>
|
||||||
<BossRow
|
<BossRow
|
||||||
boss={MONTHLY_BOSSES[0]}
|
boss={MONTHLY_BOSSES[0]}
|
||||||
sel={blackmageLocked ? { difficulty: 'none', party: 1, done: false } : config.blackMage}
|
sel={blackmageLocked ? { difficulty: 'none', party: 1, done: false } : config.blackMage}
|
||||||
|
|
@ -114,7 +127,10 @@ function WeekEditor({ config, onChange, isCurrent, monthlyLockedByWeek }) {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{blackmageLocked && (
|
{blackmageLocked && (
|
||||||
<div className="text-[11px] text-amber-400/80 px-3 py-2">
|
<div
|
||||||
|
className="text-[11px] px-3 py-2"
|
||||||
|
style={{ color: 'var(--warning-text)' }}
|
||||||
|
>
|
||||||
이번 달 검은 마법사는 {monthlyLockedByWeek}주차에 배정되어 있습니다.
|
이번 달 검은 마법사는 {monthlyLockedByWeek}주차에 배정되어 있습니다.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -210,7 +226,11 @@ export default function WeeklyScheduler({ startDate, weeks: weeksProp, onChangeW
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={w.id}
|
key={w.id}
|
||||||
className="rounded-xl border border-white/5 bg-gray-950/30"
|
className="rounded-xl border"
|
||||||
|
style={{
|
||||||
|
background: 'var(--surface-3)',
|
||||||
|
borderColor: 'var(--panel-border)',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3 pl-4 pr-2 py-3">
|
<div className="flex items-center gap-3 pl-4 pr-2 py-3">
|
||||||
<button
|
<button
|
||||||
|
|
@ -219,11 +239,19 @@ export default function WeeklyScheduler({ startDate, weeks: weeksProp, onChangeW
|
||||||
className="flex items-center gap-4 flex-1 text-left hover:opacity-90 transition"
|
className="flex items-center gap-4 flex-1 text-left hover:opacity-90 transition"
|
||||||
>
|
>
|
||||||
<div className="w-12 text-center shrink-0">
|
<div className="w-12 text-center shrink-0">
|
||||||
<div className="text-[11px] text-gray-500 leading-tight">주차</div>
|
<div className="text-[11px] leading-tight" style={{ color: 'var(--text-dim)' }}>주차</div>
|
||||||
<div className="text-xl font-extrabold tabular-nums leading-tight text-gray-200">{n}</div>
|
<div
|
||||||
|
className="text-xl font-extrabold tabular-nums leading-tight"
|
||||||
|
style={{ color: 'var(--text-emphasis)' }}
|
||||||
|
>
|
||||||
|
{n}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{startDate && (
|
{startDate && (
|
||||||
<div className="text-sm text-gray-400 tabular-nums w-24 shrink-0">
|
<div
|
||||||
|
className="text-sm tabular-nums w-24 shrink-0"
|
||||||
|
style={{ color: 'var(--text-muted)' }}
|
||||||
|
>
|
||||||
{formatRange(getWeekRange(startDate, n))}
|
{formatRange(getWeekRange(startDate, n))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -240,9 +268,9 @@ export default function WeeklyScheduler({ startDate, weeks: weeksProp, onChangeW
|
||||||
const monthlySum = monthlyLockedByWeek != null ? 0 : bossEarn(MONTHLY_BOSSES[0], w.config.blackMage)
|
const monthlySum = monthlyLockedByWeek != null ? 0 : bossEarn(MONTHLY_BOSSES[0], 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 text-emerald-300">+{weeklySum}</div>
|
<div className="text-base font-bold" style={{ color: 'var(--accent-bright)' }}>+{weeklySum}</div>
|
||||||
{monthlySum > 0 && (
|
{monthlySum > 0 && (
|
||||||
<div className="text-sm font-semibold text-amber-300">+{monthlySum}</div>
|
<div className="text-sm font-semibold" style={{ color: 'var(--warning-text-bright)' }}>+{monthlySum}</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
@ -250,7 +278,8 @@ export default function WeeklyScheduler({ startDate, weeks: weeksProp, onChangeW
|
||||||
|
|
||||||
<svg
|
<svg
|
||||||
width="16" height="16" viewBox="0 0 12 12" fill="none"
|
width="16" height="16" viewBox="0 0 12 12" fill="none"
|
||||||
className={`text-gray-500 transition-transform shrink-0 ${isOpen ? 'rotate-180' : ''}`}
|
className={`transition-transform shrink-0 ${isOpen ? 'rotate-180' : ''}`}
|
||||||
|
style={{ color: 'var(--text-dim)' }}
|
||||||
>
|
>
|
||||||
<path d="M3 4.5L6 7.5L9 4.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
<path d="M3 4.5L6 7.5L9 4.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|
@ -260,7 +289,8 @@ export default function WeeklyScheduler({ startDate, weeks: weeksProp, onChangeW
|
||||||
onClick={() => removeWeek(w.id)}
|
onClick={() => removeWeek(w.id)}
|
||||||
disabled={weeks.length <= 1}
|
disabled={weeks.length <= 1}
|
||||||
title={weeks.length <= 1 ? '최소 한 주차는 유지되어야 합니다' : '이 주차 삭제'}
|
title={weeks.length <= 1 ? '최소 한 주차는 유지되어야 합니다' : '이 주차 삭제'}
|
||||||
className="shrink-0 w-8 h-8 rounded-md text-gray-500 hover:text-red-400 hover:bg-red-500/10 disabled:opacity-30 disabled:hover:text-gray-500 disabled:hover:bg-transparent disabled:cursor-not-allowed transition flex items-center justify-center"
|
className="shrink-0 w-8 h-8 rounded-md hover:bg-[var(--danger-bg-hover)] hover:text-[var(--danger-text)] disabled:opacity-30 disabled:hover:bg-transparent disabled:cursor-not-allowed flex items-center justify-center"
|
||||||
|
style={{ color: 'var(--text-dim)' }}
|
||||||
>
|
>
|
||||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
|
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
|
||||||
<path d="M1 1L13 13M13 1L1 13" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" />
|
<path d="M1 1L13 13M13 1L1 13" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" />
|
||||||
|
|
@ -281,7 +311,13 @@ export default function WeeklyScheduler({ startDate, weeks: weeksProp, onChangeW
|
||||||
}}
|
}}
|
||||||
style={{ overflow: 'hidden' }}
|
style={{ overflow: 'hidden' }}
|
||||||
>
|
>
|
||||||
<div className="border-t border-white/5 px-3 py-3 bg-gray-950/40">
|
<div
|
||||||
|
className="border-t px-3 py-3"
|
||||||
|
style={{
|
||||||
|
borderColor: 'var(--row-divider)',
|
||||||
|
background: 'var(--skeleton-bg)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<WeekEditor
|
<WeekEditor
|
||||||
config={w.config}
|
config={w.config}
|
||||||
onChange={(c) => updateWeek(w.id, c)}
|
onChange={(c) => updateWeek(w.id, c)}
|
||||||
|
|
@ -299,7 +335,11 @@ export default function WeeklyScheduler({ startDate, weeks: weeksProp, onChangeW
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={addWeek}
|
onClick={addWeek}
|
||||||
className="w-full rounded-xl border border-dashed border-white/10 hover:border-emerald-500/40 hover:bg-emerald-500/5 text-gray-500 hover:text-emerald-300 py-3 text-sm font-semibold transition flex items-center justify-center gap-2"
|
className="w-full rounded-xl border border-dashed py-3 text-sm font-semibold flex items-center justify-center gap-2 hover:border-[var(--selected-border)] hover:text-[var(--accent-bright)]"
|
||||||
|
style={{
|
||||||
|
borderColor: 'var(--dashed-border)',
|
||||||
|
color: 'var(--text-dim)',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
|
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
|
||||||
<path d="M7 1V13M1 7H13" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
|
<path d="M7 1V13M1 7H13" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue