해방 계산기 UI 다듬기

- 섹션 폭 max-w-3xl로 통일
- ProgressBar 초상화 테두리 제거, 세그먼트/초상화 간격 gap-2
- 1차 해방 라벨 색상을 에메랄드와 구분되는 보라(#a78bfa)로
- 예상 해방 날짜 텍스트 크기 키우고 요일 표시
- DatePicker 선택 날짜에 요일 표시
- Select 드롭다운이 아래 공간 부족하면 위로 펼침
- Select 옵션 패딩 py-2.5로 키움
- 주간 보스 설정 보스 초상화(w-10)·이름(text-base)·행 높이(h-16) 키움

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
caadiq 2026-04-14 14:22:20 +09:00
parent e418e651b8
commit d3fda95d04
5 changed files with 47 additions and 32 deletions

View file

@ -69,10 +69,12 @@ export default function DatePicker({ value, onChange, placeholder = '날짜 선
const selectYear = (y) => setViewDate(new Date(y, month, 1))
const selectMonth = (m) => { setViewDate(new Date(year, m, 1)); setViewMode('days') }
const DOW = ['일', '월', '화', '수', '목', '금', '토']
const formatDisplay = (s) => {
if (!s) return ''
const [y, m, d] = s.split('-')
return `${y}${parseInt(m)}${parseInt(d)}`
const dow = DOW[new Date(`${s}T00:00:00+09:00`).getDay()]
return `${y}${parseInt(m)}${parseInt(d)}일 (${dow})`
}
const isSelected = (day) => {

View file

@ -7,7 +7,9 @@ import { motion, AnimatePresence } from 'framer-motion'
*/
export default function Select({ value, onChange, options, disabled, className = '', placeholder = '선택', align = 'left' }) {
const [open, setOpen] = useState(false)
const [flipUp, setFlipUp] = useState(false)
const ref = useRef(null)
const buttonRef = useRef(null)
useEffect(() => {
if (!open) return
@ -18,11 +20,21 @@ export default function Select({ value, onChange, options, disabled, className =
return () => document.removeEventListener('mousedown', handler)
}, [open])
useEffect(() => {
if (!open || !buttonRef.current) return
const rect = buttonRef.current.getBoundingClientRect()
const estHeight = Math.min(options.length * 44 + 8, 240)
const spaceBelow = window.innerHeight - rect.bottom
const spaceAbove = rect.top
setFlipUp(spaceBelow < estHeight && spaceAbove > spaceBelow)
}, [open, options.length])
const selected = options.find((o) => o.value === value)
return (
<div ref={ref} className={`relative ${className}`}>
<button
ref={buttonRef}
type="button"
disabled={disabled}
onClick={() => !disabled && setOpen((v) => !v)}
@ -41,13 +53,13 @@ export default function Select({ value, onChange, options, disabled, className =
<AnimatePresence>
{open && (
<motion.div
initial={{ opacity: 0, y: -6, scale: 0.98 }}
initial={{ opacity: 0, y: flipUp ? 6 : -6, scale: 0.98 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: -6, scale: 0.98 }}
exit={{ opacity: 0, y: flipUp ? 6 : -6, scale: 0.98 }}
transition={{ duration: 0.15 }}
className={`absolute top-full mt-1 z-20 min-w-full rounded-lg border border-white/10 bg-gray-900 shadow-xl overflow-hidden origin-top ${
align === 'right' ? 'right-0' : 'left-0'
}`}
className={`absolute z-20 min-w-full rounded-lg border border-white/10 bg-gray-900 shadow-xl overflow-hidden ${
flipUp ? 'bottom-full mb-1 origin-bottom' : 'top-full mt-1 origin-top'
} ${align === 'right' ? 'right-0' : 'left-0'}`}
>
<div className="max-h-60 overflow-y-auto py-1">
{options.map((opt) => (
@ -55,7 +67,7 @@ export default function Select({ value, onChange, options, disabled, className =
key={opt.value}
type="button"
onClick={() => { onChange(opt.value); setOpen(false) }}
className={`w-full text-left px-3 py-1.5 text-sm transition flex items-center gap-2 ${
className={`w-full text-left px-3 py-2.5 text-sm transition flex items-center gap-2 ${
opt.value === value
? 'bg-emerald-500/10 text-emerald-300'
: 'hover:bg-white/5'

View file

@ -288,7 +288,7 @@ export default function Liberation() {
return (
<div className="space-y-6 pb-10">
{/* 해방 종류 탭 */}
<div className="max-w-2xl mx-auto flex gap-2">
<div className="max-w-3xl mx-auto flex gap-2">
{[
{ key: 'genesis', label: '제네시스 해방', img: genesisImg.data?.url },
{ key: 'destiny', label: '데스티니 해방', img: destinyImg.data?.url },
@ -310,7 +310,7 @@ export default function Liberation() {
</div>
{liberationType === 'destiny' ? (
<div className="max-w-2xl 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 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 className="text-2xl font-bold text-gray-300">구현 예정</div>
<div className="text-sm text-gray-500">데스티니 해방 계산기는 준비 중입니다.</div>
</div>
@ -322,7 +322,7 @@ export default function Liberation() {
/>
{/* 현재 진행 상태 입력 */}
<div className="max-w-2xl 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 border-white/10 bg-gray-900/60 p-6 space-y-4">
<div className="text-lg font-semibold text-emerald-300">현재 진행 상태</div>
<div className="grid gap-3" style={{ gridTemplateColumns: '1.2fr 1.2fr 0.7fr' }}>
@ -361,7 +361,7 @@ export default function Liberation() {
totalMonthly={monthlyEarn}
/>
<div className="max-w-2xl mx-auto flex justify-end">
<div className="max-w-3xl mx-auto flex justify-end">
<button
type="button"
onClick={() => setResetOpen(true)}

View file

@ -1,8 +1,10 @@
import { GENESIS_CHAPTERS, GENESIS_TOTAL, QUEST_BOSS_IMAGE_BASE } from '../data'
const DOW = ['일', '월', '화', '수', '목', '금', '토']
function formatKoreanDate(s) {
const [y, m, d] = s.split('-')
return `${y}${m}${d}`
const dow = DOW[new Date(`${s}T00:00:00+09:00`).getDay()]
return `${y}${m}${d}일 (${dow})`
}
export default function ProgressBar({ startChapter, currentPoints, completionDate }) {
@ -29,16 +31,15 @@ export default function ProgressBar({ startChapter, currentPoints, completionDat
}
const renderPortrait = ({ chapter, status }) => (
<div key={`p-${chapter.idx}`} className="flex-1 flex flex-col items-center gap-1.5">
<div className={`w-14 h-14 rounded-lg overflow-hidden border transition ${
status === 'done' ? 'border-emerald-500/40' :
status === 'active' ? 'border-amber-400/60 shadow-lg shadow-amber-500/20' :
'border-white/5 opacity-50'
<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 ${
status === 'active' ? 'shadow-lg shadow-amber-500/20' :
status === 'pending' ? 'opacity-50' : ''
}`}>
<img
src={`${QUEST_BOSS_IMAGE_BASE}/${chapter.boss}.png`}
alt={chapter.boss}
className={`w-full h-full object-cover ${status === 'pending' ? 'grayscale' : ''}`}
className={`block w-full h-full object-cover ${status === 'pending' ? 'grayscale' : ''}`}
/>
</div>
<div className={`text-sm font-medium ${
@ -51,37 +52,37 @@ export default function ProgressBar({ startChapter, currentPoints, completionDat
)
return (
<div className="max-w-2xl 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 border-white/10 bg-gray-900/60 p-6 space-y-5">
{/* 섹션 제목 */}
<div className="text-lg font-semibold text-emerald-300">퀘스트 진행 상황</div>
{/* 1차 / 2차 라벨 + 세그먼트 바 */}
<div className="space-y-3">
<div className="flex items-center gap-1">
<div className="flex items-center gap-2">
<div className="flex-1 flex flex-col items-center gap-2">
<span className="text-base font-bold" style={{ color: '#5eead4' }}>1 해방</span>
<div style={{ width: '100%', height: 3, background: 'rgba(94, 234, 212, 0.5)', borderRadius: 999 }} />
<span className="text-base font-bold" style={{ color: '#a78bfa' }}>1 해방</span>
<div style={{ width: '100%', height: 3, background: 'rgba(167, 139, 250, 0.5)', borderRadius: 999 }} />
</div>
<div className="flex-1 flex flex-col items-center gap-2">
<span className="text-base font-bold" style={{ color: '#fda4af' }}>2 해방</span>
<div style={{ width: '100%', height: 3, background: 'rgba(253, 164, 175, 0.5)', borderRadius: 999 }} />
</div>
</div>
<div className="flex gap-1">
<div className="flex gap-2">
{chapterStates.map(renderSegment)}
</div>
</div>
{/* 초상화 (붙어있음) */}
<div className="flex gap-1">
<div className="flex gap-2">
{chapterStates.map(renderPortrait)}
</div>
{/* 예상 해방 날짜 */}
<div className="flex items-center justify-center gap-2 pt-4 border-t border-white/5 text-base">
<span className="text-emerald-300/80">예상 해방 날짜</span>
<div className="flex items-center justify-center gap-3 pt-4 border-t border-white/5">
<span className="text-lg font-semibold text-white">예상 해방 날짜</span>
<span className="text-gray-600">·</span>
<span className="font-semibold tabular-nums text-amber-400">
<span className="text-xl font-bold tabular-nums text-amber-400">
{completionDate ? formatKoreanDate(completionDate) : <span className="text-gray-500 font-normal">미정</span>}
</span>
</div>

View file

@ -22,13 +22,13 @@ function BossRow({ boss, sel, onChange, monthly = false }) {
.map((d) => ({ value: d.key, label: diffLabel(d, sel.party) }))
return (
<div className="flex items-center gap-3 rounded-lg px-3 h-14 transition">
<div className="flex items-center gap-3 rounded-lg px-3 h-16 transition">
<Tooltip text={boss.name}>
<img src={`${LIBERATION_BOSS_IMAGE_BASE}/${boss.image}`} alt="" className="w-8 h-8 rounded 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>
<span className="text-sm font-medium flex-1 truncate">
<span className="text-base font-semibold flex-1 truncate">
{boss.name}
{monthly && <span className="ml-1.5 text-[10px] text-amber-400/80">월간</span>}
{monthly && <span className="ml-1.5 text-[11px] text-amber-400/80 font-medium">월간</span>}
</span>
<div className="w-36">
@ -78,7 +78,7 @@ export default function WeeklyDefault({ weekly, onChange, totalWeekly, totalMont
}
return (
<div className="max-w-2xl 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 border-white/10 bg-gray-900/60 p-6 space-y-4">
<div className="flex items-center justify-between">
<div className="text-lg font-semibold text-emerald-300">주간 보스 설정</div>
<div className="inline-flex rounded-lg border border-white/10 bg-gray-950 p-0.5">