해방 계산기 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:
parent
e418e651b8
commit
d3fda95d04
5 changed files with 47 additions and 32 deletions
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue