refactor(control-center): Quick Controls 재디자인 및 플랜/잠금 결제 동선 정리

This commit is contained in:
2026-03-04 14:36:38 +09:00
parent 60cd093308
commit 3cddd3c1f4
21 changed files with 983 additions and 149 deletions

View File

@@ -0,0 +1 @@
export * from './ui/ControlCenterSheetWidget';

View File

@@ -0,0 +1,237 @@
'use client';
import { useMemo } from 'react';
import type { PlanTier } from '@/entities/plan';
import {
PRO_LOCKED_ROOM_IDS,
PRO_LOCKED_SOUND_IDS,
PRO_LOCKED_TIMER_LABELS,
} from '@/entities/plan';
import { getRoomCardBackgroundStyle, type RoomTheme } from '@/entities/room';
import type { SoundPreset, TimerPreset } from '@/entities/session';
import { cn } from '@/shared/lib/cn';
interface ControlCenterSheetWidgetProps {
plan: PlanTier;
rooms: RoomTheme[];
selectedRoomId: string;
selectedTimerLabel: string;
selectedSoundPresetId: string;
timerPresets: TimerPreset[];
soundPresets: SoundPreset[];
onSelectRoom: (roomId: string) => void;
onSelectTimer: (timerLabel: string) => void;
onSelectSound: (soundPresetId: string) => void;
onApplyPack: (packId: QuickPackId) => void;
onLockedClick: (source: string) => void;
}
type QuickPackId = 'balanced' | 'deep-work' | 'gentle';
interface QuickPack {
id: QuickPackId;
name: string;
combo: string;
locked: boolean;
}
const QUICK_PACKS: QuickPack[] = [
{
id: 'balanced',
name: 'Balanced',
combo: '25/5 + Rain Focus',
locked: false,
},
{
id: 'deep-work',
name: 'Deep Work',
combo: '50/10 + Deep White',
locked: true,
},
{
id: 'gentle',
name: 'Gentle',
combo: '25/5 + Silent',
locked: true,
},
];
const LockBadge = () => {
return (
<span className="absolute right-2 top-2 rounded-full border border-white/20 bg-black/46 px-1.5 py-0.5 text-[9px] font-semibold tracking-[0.08em] text-white/86">
LOCK PRO
</span>
);
};
const SectionTitle = ({ title, description }: { title: string; description: string }) => {
return (
<header className="flex items-end justify-between gap-2">
<h4 className="text-sm font-semibold text-white/90">{title}</h4>
<p className="text-[11px] text-white/56">{description}</p>
</header>
);
};
export const ControlCenterSheetWidget = ({
plan,
rooms,
selectedRoomId,
selectedTimerLabel,
selectedSoundPresetId,
timerPresets,
soundPresets,
onSelectRoom,
onSelectTimer,
onSelectSound,
onApplyPack,
onLockedClick,
}: ControlCenterSheetWidgetProps) => {
const isPro = plan === 'pro';
const selectedRoom = useMemo(() => {
return rooms.find((room) => room.id === selectedRoomId) ?? rooms[0];
}, [rooms, selectedRoomId]);
const selectedSound = useMemo(() => {
return soundPresets.find((preset) => preset.id === selectedSoundPresetId) ?? soundPresets[0];
}, [selectedSoundPresetId, soundPresets]);
return (
<div className="space-y-4">
<section className="space-y-2 rounded-2xl border border-white/14 bg-white/[0.035] p-3">
<SectionTitle title="Scene" description={selectedRoom?.name ?? '공간'} />
<div className="-mx-1 flex gap-2 overflow-x-auto px-1 pb-1">
{rooms.slice(0, 6).map((room) => {
const selected = room.id === selectedRoomId;
const locked = !isPro && PRO_LOCKED_ROOM_IDS.includes(room.id);
return (
<button
key={room.id}
type="button"
onClick={() => {
if (locked) {
onLockedClick(`공간: ${room.name}`);
return;
}
onSelectRoom(room.id);
}}
className={cn(
'relative h-24 w-[130px] shrink-0 overflow-hidden rounded-xl border text-left transition-transform hover:-translate-y-0.5',
selected ? 'border-sky-200/44 shadow-[0_8px_16px_rgba(56,189,248,0.18)]' : 'border-white/16',
)}
>
<div aria-hidden className="absolute inset-0 bg-cover bg-center" style={getRoomCardBackgroundStyle(room)} />
<div aria-hidden className="absolute inset-0 bg-gradient-to-t from-black/56 via-black/18 to-black/6" />
{locked ? <LockBadge /> : null}
<div className="absolute inset-x-2 bottom-2 min-w-0">
<p className="truncate text-sm font-medium text-white/90">{room.name}</p>
<p className="truncate text-[11px] text-white/66">{room.vibeLabel}</p>
</div>
</button>
);
})}
</div>
</section>
<section className="space-y-2 rounded-2xl border border-white/14 bg-white/[0.035] p-3">
<SectionTitle title="Time" description={selectedTimerLabel} />
<div className="grid grid-cols-3 gap-1.5">
{timerPresets.slice(0, 3).map((preset) => {
const selected = preset.label === selectedTimerLabel;
const locked = !isPro && PRO_LOCKED_TIMER_LABELS.includes(preset.label);
return (
<button
key={preset.id}
type="button"
onClick={() => {
if (locked) {
onLockedClick(`타이머: ${preset.label}`);
return;
}
onSelectTimer(preset.label);
}}
className={cn(
'relative rounded-xl border px-2 py-2 text-xs transition-colors',
selected
? 'border-sky-200/42 bg-sky-200/16 text-white'
: 'border-white/18 bg-white/[0.04] text-white/74 hover:bg-white/[0.1]',
)}
>
{preset.label}
{locked ? <span className="ml-1 text-[9px] text-white/66">LOCK PRO</span> : null}
</button>
);
})}
</div>
</section>
<section className="space-y-2 rounded-2xl border border-white/14 bg-white/[0.035] p-3">
<SectionTitle title="Sound" description={selectedSound?.label ?? '기본'} />
<div className="flex flex-wrap gap-1.5">
{soundPresets.slice(0, 6).map((preset) => {
const selected = preset.id === selectedSoundPresetId;
const locked = !isPro && PRO_LOCKED_SOUND_IDS.includes(preset.id);
return (
<button
key={preset.id}
type="button"
onClick={() => {
if (locked) {
onLockedClick(`사운드: ${preset.label}`);
return;
}
onSelectSound(preset.id);
}}
className={cn(
'rounded-full border px-3 py-1.5 text-xs transition-colors',
selected
? 'border-sky-200/42 bg-sky-200/16 text-white'
: 'border-white/18 bg-white/[0.04] text-white/74 hover:bg-white/[0.1]',
)}
>
{preset.label}
{locked ? <span className="ml-1.5 text-[10px] text-white/66">LOCK PRO</span> : null}
</button>
);
})}
</div>
</section>
<section className="space-y-2 rounded-2xl border border-white/14 bg-white/[0.035] p-3">
<SectionTitle title="Preset Packs" description="원탭 조합" />
<div className="grid gap-1.5">
{QUICK_PACKS.map((pack) => {
const locked = !isPro && pack.locked;
return (
<button
key={pack.id}
type="button"
onClick={() => {
if (locked) {
onLockedClick(`프리셋 팩: ${pack.name}`);
return;
}
onApplyPack(pack.id);
}}
className="relative rounded-xl border border-white/16 bg-white/[0.04] px-3 py-2 text-left transition-colors hover:bg-white/[0.1]"
>
{locked ? <LockBadge /> : null}
<p className="text-sm font-medium text-white/90">{pack.name}</p>
<p className="mt-0.5 text-[11px] text-white/62">{pack.combo}</p>
</button>
);
})}
</div>
</section>
</div>
);
};