refactor(control-center): Scene/Time 중심으로 단순화하고 추천 복원 흐름 추가

맥락:
- Control Center가 설정 패널처럼 무거워 보여 Focus 몰입 흐름에서 탐색 부담이 컸습니다.

변경사항:
- Control Center에서 Sound 섹션과 Preset Packs 섹션을 제거하고 Scene/Time 2개 핵심 섹션만 유지했습니다.
- 추천 정보를 비인터랙션 1줄(추천 사운드 · 추천 타이머)로 노출하도록 구성했습니다.
- 하단에 tertiary 액션 '추천으로 되돌리기'를 추가해 override 초기화 + 추천값 복원 진입점을 만들었습니다.
- 더 이상 사용하지 않는 quick pack 모델 파일을 제거했습니다.

검증:
- npx tsc --noEmit

세션-상태: Control Center가 Scene/Time 중심의 경량 구조로 정리되었습니다.
세션-다음: 우하단 Sound Quick 변경 시 override.sound가 명시적으로 적용되는 경로를 분리합니다.
세션-리스크: 추천 정보는 현재 텍스트 기반이라 향후 다국어/라벨 변경 시 매핑 점검이 필요합니다.
This commit is contained in:
2026-03-05 12:12:52 +09:00
parent f24205f324
commit ac63f94ed8
3 changed files with 18 additions and 159 deletions

View File

@@ -4,11 +4,10 @@ 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 type { TimerPreset } from '@/entities/session';
import { cn } from '@/shared/lib/cn';
import { useReducedMotion } from '@/shared/lib/useReducedMotion';
@@ -17,49 +16,15 @@ interface ControlCenterSheetWidgetProps {
rooms: RoomTheme[];
selectedRoomId: string;
selectedTimerLabel: string;
selectedSoundPresetId: string;
sceneRecommendedSoundLabel: string;
sceneRecommendedTimerLabel: 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;
onResetToRecommended: () => 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">
@@ -82,17 +47,13 @@ export const ControlCenterSheetWidget = ({
rooms,
selectedRoomId,
selectedTimerLabel,
selectedSoundPresetId,
sceneRecommendedSoundLabel: _sceneRecommendedSoundLabel,
sceneRecommendedTimerLabel: _sceneRecommendedTimerLabel,
sceneRecommendedSoundLabel,
sceneRecommendedTimerLabel,
timerPresets,
soundPresets,
onSelectRoom,
onSelectTimer,
onSelectSound,
onApplyPack,
onLockedClick,
onResetToRecommended: _onResetToRecommended,
onResetToRecommended,
}: ControlCenterSheetWidgetProps) => {
const reducedMotion = useReducedMotion();
const isPro = plan === 'pro';
@@ -107,12 +68,8 @@ export const ControlCenterSheetWidget = ({
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-5">
<div className="space-y-4">
<section className="space-y-2.5 rounded-2xl border border-white/12 bg-black/22 p-3.5 backdrop-blur-md">
<SectionTitle title="Scene" description={selectedRoom?.name ?? '공간'} />
<div
@@ -193,72 +150,19 @@ export const ControlCenterSheetWidget = ({
</div>
</section>
<section className="space-y-2.5 rounded-2xl border border-white/12 bg-black/22 p-3.5 backdrop-blur-md">
<SectionTitle title="Sound" description={selectedSound?.label ?? '기본'} />
<div className="grid grid-cols-2 gap-2 sm:grid-cols-3">
{soundPresets.slice(0, 6).map((preset) => {
const selected = preset.id === selectedSoundPresetId;
const locked = !isPro && PRO_LOCKED_SOUND_IDS.includes(preset.id);
return (
<div className="space-y-1.5 rounded-xl border border-white/12 bg-white/[0.03] px-3 py-2.5">
<p className="text-[11px] text-white/58">: {sceneRecommendedSoundLabel} · {sceneRecommendedTimerLabel}</p>
<button
key={preset.id}
type="button"
onClick={() => {
if (locked) {
onLockedClick(`사운드: ${preset.label}`);
return;
}
onSelectSound(preset.id);
}}
onClick={onResetToRecommended}
className={cn(
'rounded-xl border px-3 py-2 text-xs',
colorMotionClass,
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.5 rounded-2xl border border-white/12 bg-black/22 p-3.5 backdrop-blur-md">
<SectionTitle title="Preset Packs" description="원탭 조합" />
<div className="grid gap-2.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={cn(
'relative rounded-xl border border-white/16 bg-white/[0.04] px-3.5 py-2.5 text-left hover:bg-white/[0.1]',
'text-left text-[11px] text-white/72 transition-colors hover:text-white/90',
colorMotionClass,
)}
>
{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>
);
};

View File

@@ -1,31 +0,0 @@
interface ApplyQuickPackParams {
packId: 'balanced' | 'deep-work' | 'gentle';
onTimerSelect: (timerLabel: string) => void;
onSelectPreset: (presetId: string) => void;
onApplied?: (message: string) => void;
}
export const applyQuickPack = ({
packId,
onTimerSelect,
onSelectPreset,
onApplied,
}: ApplyQuickPackParams) => {
if (packId === 'balanced') {
onTimerSelect('25/5');
onSelectPreset('rain-focus');
onApplied?.('Balanced 팩을 적용했어요.');
return;
}
if (packId === 'deep-work') {
onTimerSelect('50/10');
onSelectPreset('deep-white');
onApplied?.('Deep Work 팩을 적용했어요.');
return;
}
onTimerSelect('25/5');
onSelectPreset('silent');
onApplied?.('Gentle 팩을 적용했어요.');
};

View File

@@ -12,7 +12,6 @@ import { cn } from '@/shared/lib/cn';
import { ControlCenterSheetWidget } from '@/widgets/control-center-sheet';
import { SpaceSideSheet } from '@/widgets/space-sheet-shell';
import type { SpaceAnchorPopoverId, SpaceUtilityPanelId } from '../model/types';
import { applyQuickPack } from '../model/applyQuickPack';
import { getQuickSoundPresets } from '../model/getQuickSoundPresets';
import { ANCHOR_ICON, formatThoughtCount, UTILITY_PANEL_TITLE } from './constants';
import { FocusRightRail } from './FocusRightRail';
@@ -246,13 +245,6 @@ export const SpaceToolsDockWidget = ({
openUtilityPanel('control-center');
};
const handleApplyPack = (packId: 'balanced' | 'deep-work' | 'gentle') =>
applyQuickPack({
packId,
onTimerSelect,
onSelectPreset,
});
const showVolumeFeedback = (nextVolume: number) => {
setVolumeFeedback(`${nextVolume}%`);
@@ -419,21 +411,15 @@ export const SpaceToolsDockWidget = ({
rooms={rooms}
selectedRoomId={selectedRoomId}
selectedTimerLabel={selectedTimerLabel}
selectedSoundPresetId={selectedPresetId}
sceneRecommendedSoundLabel={sceneRecommendedSoundLabel}
sceneRecommendedTimerLabel={sceneRecommendedTimerLabel}
timerPresets={timerPresets}
soundPresets={SOUND_PRESETS}
onSelectRoom={(roomId) => {
onRoomSelect(roomId);
}}
onSelectTimer={(label) => {
onTimerSelect(label);
}}
onSelectSound={(presetId) => {
onSelectPreset(presetId);
}}
onApplyPack={handleApplyPack}
onLockedClick={handleLockedClick}
onResetToRecommended={onResetToSceneRecommended}
/>