From be16153befc0203e7a911e36f8898193257e571f Mon Sep 17 00:00:00 2001 From: corpi Date: Tue, 3 Mar 2026 17:50:49 +0900 Subject: [PATCH] =?UTF-8?q?fix(space-ui):=20/space=20=ED=8F=AC=EC=BB=A4?= =?UTF-8?q?=EC=8A=A4=20=EC=95=B5=EC=BB=A4=20=EC=9E=98=EB=A6=BC=EA=B3=BC=20?= =?UTF-8?q?=EC=8A=A4=ED=81=AC=EB=A1=A4=20=EB=AC=B8=EC=A0=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/globals.css | 15 +- .../ui/SpaceFocusHudWidget.tsx | 16 +- .../ui/SpaceSetupDrawerWidget.tsx | 288 +++++++--- .../ui/SpaceTimerHudWidget.tsx | 33 +- src/widgets/space-tools-dock/model/types.ts | 3 +- .../ui/SpaceToolsDockWidget.tsx | 495 ++++++++++++------ .../ui/panels/SettingsToolPanel.tsx | 72 ++- .../ui/SpaceWorkspaceWidget.tsx | 119 +++-- 8 files changed, 735 insertions(+), 306 deletions(-) diff --git a/src/app/globals.css b/src/app/globals.css index bcd9eee..65ff209 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -52,10 +52,10 @@ body { @keyframes space-stage-pan { 0% { - transform: scale(1.02) translate3d(-0.4%, -0.25%, 0); + transform: translate3d(-0.55%, -0.35%, 0); } 100% { - transform: scale(1.07) translate3d(0.55%, 0.35%, 0); + transform: translate3d(0.55%, 0.35%, 0); } } @@ -69,3 +69,14 @@ body { opacity: 1; } } + +@keyframes popover-rise { + from { + opacity: 0; + transform: translateY(8px); + } + to { + opacity: 1; + transform: translateY(0); + } +} diff --git a/src/widgets/space-focus-hud/ui/SpaceFocusHudWidget.tsx b/src/widgets/space-focus-hud/ui/SpaceFocusHudWidget.tsx index 10d7eb6..dba47be 100644 --- a/src/widgets/space-focus-hud/ui/SpaceFocusHudWidget.tsx +++ b/src/widgets/space-focus-hud/ui/SpaceFocusHudWidget.tsx @@ -2,13 +2,25 @@ import { SpaceTimerHudWidget } from '@/widgets/space-timer-hud'; interface SpaceFocusHudWidgetProps { goal: string; + timerLabel: string; visible: boolean; } -export const SpaceFocusHudWidget = ({ goal, visible }: SpaceFocusHudWidgetProps) => { +export const SpaceFocusHudWidget = ({ + goal, + timerLabel, + visible, +}: SpaceFocusHudWidgetProps) => { if (!visible) { return null; } - return ; + return ( + + ); }; diff --git a/src/widgets/space-setup-drawer/ui/SpaceSetupDrawerWidget.tsx b/src/widgets/space-setup-drawer/ui/SpaceSetupDrawerWidget.tsx index c21e074..300719d 100644 --- a/src/widgets/space-setup-drawer/ui/SpaceSetupDrawerWidget.tsx +++ b/src/widgets/space-setup-drawer/ui/SpaceSetupDrawerWidget.tsx @@ -1,51 +1,131 @@ 'use client'; -import type { FormEvent } from 'react'; +import { useEffect, useMemo, useRef, useState, type FormEvent } from 'react'; import type { RoomTheme } from '@/entities/room'; -import type { GoalChip, SoundPreset } from '@/entities/session'; +import type { GoalChip, SoundPreset, TimerPreset } from '@/entities/session'; import { SpaceSelectCarousel } from '@/features/space-select'; import { SessionGoalField } from '@/features/session-goal'; import { Button } from '@/shared/ui'; import { cn } from '@/shared/lib/cn'; -import { SpaceSideSheet } from '@/widgets/space-sheet-shell'; + +type RitualPopover = 'space' | 'timer' | 'sound'; interface SpaceSetupDrawerWidgetProps { open: boolean; - dismissible?: boolean; rooms: RoomTheme[]; selectedRoomId: string; + selectedTimerLabel: string; + selectedSoundPresetId: string; goalInput: string; selectedGoalId: string | null; - selectedSoundPresetId: string; goalChips: GoalChip[]; soundPresets: SoundPreset[]; + timerPresets: TimerPreset[]; canStart: boolean; - onClose: () => void; onRoomSelect: (roomId: string) => void; + onTimerSelect: (timerLabel: string) => void; + onSoundSelect: (soundPresetId: string) => void; onGoalChange: (value: string) => void; onGoalChipSelect: (chip: GoalChip) => void; - onSoundSelect: (soundPresetId: string) => void; onStart: () => void; } +interface SummaryChipProps { + label: string; + value: string; + open: boolean; + onClick: () => void; +} + +const SummaryChip = ({ label, value, open, onClick }: SummaryChipProps) => { + return ( + + ); +}; + export const SpaceSetupDrawerWidget = ({ open, - dismissible = true, rooms, selectedRoomId, + selectedTimerLabel, + selectedSoundPresetId, goalInput, selectedGoalId, - selectedSoundPresetId, goalChips, soundPresets, + timerPresets, canStart, - onClose, onRoomSelect, + onTimerSelect, + onSoundSelect, onGoalChange, onGoalChipSelect, - onSoundSelect, onStart, }: SpaceSetupDrawerWidgetProps) => { + const [openPopover, setOpenPopover] = useState(null); + const panelRef = useRef(null); + + const selectedRoom = useMemo(() => { + return rooms.find((room) => room.id === selectedRoomId) ?? rooms[0]; + }, [rooms, selectedRoomId]); + + const selectedSoundLabel = useMemo(() => { + return ( + soundPresets.find((preset) => preset.id === selectedSoundPresetId)?.label ?? + soundPresets[0]?.label ?? + '기본' + ); + }, [selectedSoundPresetId, soundPresets]); + + useEffect(() => { + if (!openPopover) { + return; + } + + const handleEscape = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + setOpenPopover(null); + } + }; + + const handlePointerDown = (event: MouseEvent) => { + const target = event.target as Node; + + if (!panelRef.current?.contains(target)) { + setOpenPopover(null); + } + }; + + document.addEventListener('keydown', handleEscape); + document.addEventListener('mousedown', handlePointerDown); + + return () => { + document.removeEventListener('keydown', handleEscape); + document.removeEventListener('mousedown', handlePointerDown); + }; + }, [openPopover]); + + if (!open) { + return null; + } + + const togglePopover = (popover: RitualPopover) => { + setOpenPopover((current) => (current === popover ? null : popover)); + }; + const handleSubmit = (event: FormEvent) => { event.preventDefault(); @@ -57,44 +137,115 @@ export const SpaceSetupDrawerWidget = ({ }; return ( - - {!canStart ? ( -

목표를 적으면 시작할 수 있어요.

- ) : null} - - - )} +
-
-
-

공간

- -
+
+
+

Ritual

+

이번 한 조각을 정하고 시작해요.

+

목표만 적으면 바로 Focus 모드로 넘어가요.

+
-
-

이번 세션 한 줄(필수)

+
+
+ togglePopover('space')} + /> + togglePopover('timer')} + /> + togglePopover('sound')} + /> +
+ + {openPopover === 'space' ? ( +
+ { + onRoomSelect(roomId); + setOpenPopover(null); + }} + /> +
+ ) : null} + + {openPopover === 'timer' ? ( +
+
+ {timerPresets.slice(0, 3).map((preset) => { + const selected = preset.label === selectedTimerLabel; + + return ( + + ); + })} +
+
+ ) : null} + + {openPopover === 'sound' ? ( +
+
+ {soundPresets.slice(0, 6).map((preset) => { + const selected = preset.id === selectedSoundPresetId; + + return ( + + ); + })} +
+
+ ) : null} +
+ + -
-
-

분위기(선택)

- -
- {soundPresets.slice(0, 6).map((preset) => { - const selected = preset.id === selectedSoundPresetId; - - return ( - - ); - })} +
+ {!canStart ?

목표를 적으면 시작할 수 있어요.

: null} +
-
- - + +
+
); }; diff --git a/src/widgets/space-timer-hud/ui/SpaceTimerHudWidget.tsx b/src/widgets/space-timer-hud/ui/SpaceTimerHudWidget.tsx index 27dbe7a..5a88c75 100644 --- a/src/widgets/space-timer-hud/ui/SpaceTimerHudWidget.tsx +++ b/src/widgets/space-timer-hud/ui/SpaceTimerHudWidget.tsx @@ -36,13 +36,17 @@ export const SpaceTimerHudWidget = ({ )} style={{ bottom: 'calc(env(safe-area-inset-bottom, 0px) + 0.35rem)' }} > -
+
+
@@ -50,29 +54,29 @@ export const SpaceTimerHudWidget = ({ {isBreatheMode ? RECOVERY_30S_MODE_LABEL : 'Focus'} 25:00 - + {timerLabel}
{hintMessage ? ( -

+

{hintMessage}

) : ( -

+

목표: {goal}

)} @@ -88,8 +92,8 @@ export const SpaceTimerHudWidget = ({ className={cn( 'inline-flex h-8 w-8 items-center justify-center rounded-full border text-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-200/80', isImmersionMode - ? 'border-white/10 bg-white/5 text-white/64 hover:bg-white/10' - : 'border-white/15 bg-white/8 text-white/82 hover:bg-white/14', + ? 'border-white/14 bg-black/26 text-white/82 hover:bg-black/34' + : 'border-white/14 bg-black/26 text-white/84 hover:bg-black/34', )} > {action.icon} @@ -97,7 +101,10 @@ export const SpaceTimerHudWidget = ({ ))}
- +
diff --git a/src/widgets/space-tools-dock/model/types.ts b/src/widgets/space-tools-dock/model/types.ts index 2c9c78f..4f0636d 100644 --- a/src/widgets/space-tools-dock/model/types.ts +++ b/src/widgets/space-tools-dock/model/types.ts @@ -1 +1,2 @@ -export type SpaceToolPanelId = 'sound' | 'notes' | 'inbox' | 'stats' | 'settings'; +export type SpaceAnchorPopoverId = 'sound' | 'notes'; +export type SpaceUtilityPanelId = 'settings' | 'inbox' | 'stats'; diff --git a/src/widgets/space-tools-dock/ui/SpaceToolsDockWidget.tsx b/src/widgets/space-tools-dock/ui/SpaceToolsDockWidget.tsx index 7412521..be0df6e 100644 --- a/src/widgets/space-tools-dock/ui/SpaceToolsDockWidget.tsx +++ b/src/widgets/space-tools-dock/ui/SpaceToolsDockWidget.tsx @@ -1,201 +1,369 @@ 'use client'; -import { useState } from 'react'; -import type { RecentThought } from '@/entities/session'; -import type { SoundTrackKey } from '@/features/sound-preset'; +import { useEffect, useMemo, useState } from 'react'; +import type { RoomTheme } from '@/entities/room'; +import { SOUND_PRESETS, type RecentThought, type TimerPreset } from '@/entities/session'; +import { ExitHoldButton } from '@/features/exit-hold'; import { useToast } from '@/shared/ui'; import { cn } from '@/shared/lib/cn'; import { SpaceSideSheet } from '@/widgets/space-sheet-shell'; -import type { SpaceToolPanelId } from '../model/types'; +import type { SpaceAnchorPopoverId, SpaceUtilityPanelId } from '../model/types'; import { InboxToolPanel } from './panels/InboxToolPanel'; -import { NotesToolPanel } from './panels/NotesToolPanel'; import { SettingsToolPanel } from './panels/SettingsToolPanel'; -import { SoundToolPanel } from './panels/SoundToolPanel'; import { StatsToolPanel } from './panels/StatsToolPanel'; interface SpaceToolsDockWidgetProps { isFocusMode: boolean; + rooms: RoomTheme[]; + selectedRoomId: string; + selectedTimerLabel: string; + timerPresets: TimerPreset[]; + selectedPresetId: string; thoughts: RecentThought[]; thoughtCount: number; - selectedPresetId: string; + onRoomSelect: (roomId: string) => void; + onTimerSelect: (timerLabel: string) => void; onSelectPreset: (presetId: string) => void; - isMixerOpen: boolean; - onToggleMixer: () => void; - isMuted: boolean; - onMuteChange: (next: boolean) => void; - masterVolume: number; - onMasterVolumeChange: (next: number) => void; - trackKeys: readonly SoundTrackKey[]; - trackLevels: Record; - onTrackLevelChange: (track: SoundTrackKey, level: number) => void; onCaptureThought: (note: string) => void; onClearInbox: () => void; + onExitRequested: () => void; } -const TOOL_ITEMS: Array<{ - id: SpaceToolPanelId; - label: string; -}> = [ - { id: 'sound', label: 'Sound' }, - { id: 'notes', label: 'Notes' }, - { id: 'inbox', label: 'Inbox' }, - { id: 'stats', label: 'Stats' }, - { id: 'settings', label: 'Settings' }, -]; +const ANCHOR_ICON = { + sound: ( + + + + + ), + notes: ( + + + + + + ), +}; -const PANEL_TITLE_MAP: Record = { - sound: '사운드', - notes: '생각 던지기', +const UTILITY_PANEL_TITLE: Record = { inbox: '인박스', stats: '집중 요약', settings: '설정', }; -const DockIcon = ({ id }: { id: SpaceToolPanelId }) => { - const commonProps = { - viewBox: '0 0 24 24', - fill: 'none', - className: 'h-4 w-4', - stroke: 'currentColor', - strokeWidth: 1.8, - strokeLinecap: 'round' as const, - strokeLinejoin: 'round' as const, - }; - - switch (id) { - case 'sound': - return ( - - - - - - ); - case 'notes': - return ( - - - - - ); - case 'inbox': - return ( - - - - - ); - case 'stats': - return ( - - - - - ); - case 'settings': - return ( - - - - - ); - default: - return null; +const formatThoughtCount = (count: number) => { + if (count < 1) { + return '0'; } + + if (count > 9) { + return '9+'; + } + + return String(count); }; export const SpaceToolsDockWidget = ({ isFocusMode, + rooms, + selectedRoomId, + selectedTimerLabel, + timerPresets, + selectedPresetId, thoughts, thoughtCount, - selectedPresetId, + onRoomSelect, + onTimerSelect, onSelectPreset, - isMixerOpen, - onToggleMixer, - isMuted, - onMuteChange, - masterVolume, - onMasterVolumeChange, - trackKeys, - trackLevels, - onTrackLevelChange, onCaptureThought, onClearInbox, + onExitRequested, }: SpaceToolsDockWidgetProps) => { const { pushToast } = useToast(); - const [activePanel, setActivePanel] = useState(null); + const [openPopover, setOpenPopover] = useState(null); + const [utilityPanel, setUtilityPanel] = useState(null); + const [noteDraft, setNoteDraft] = useState(''); + const [isIdle, setIdle] = useState(false); + + const selectedSoundLabel = useMemo(() => { + return ( + SOUND_PRESETS.find((preset) => preset.id === selectedPresetId)?.label ?? SOUND_PRESETS[0]?.label ?? '기본' + ); + }, [selectedPresetId]); + + useEffect(() => { + if (!openPopover) { + return; + } + + const handleEscape = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + setOpenPopover(null); + } + }; + + document.addEventListener('keydown', handleEscape); + + return () => { + document.removeEventListener('keydown', handleEscape); + }; + }, [openPopover]); + + useEffect(() => { + if (!isFocusMode || openPopover || utilityPanel) { + setIdle(false); + return; + } + + let timerId: number | null = null; + + const armIdleTimer = () => { + if (timerId) { + window.clearTimeout(timerId); + } + + timerId = window.setTimeout(() => { + setIdle(true); + }, 3500); + }; + + const wake = () => { + setIdle(false); + armIdleTimer(); + }; + + armIdleTimer(); + + window.addEventListener('pointermove', wake); + window.addEventListener('keydown', wake); + window.addEventListener('pointerdown', wake); + + return () => { + if (timerId) { + window.clearTimeout(timerId); + } + window.removeEventListener('pointermove', wake); + window.removeEventListener('keydown', wake); + window.removeEventListener('pointerdown', wake); + }; + }, [isFocusMode, openPopover, utilityPanel]); + + const openUtilityPanel = (panel: SpaceUtilityPanelId) => { + setOpenPopover(null); + setUtilityPanel(panel); + }; + + const handleNoteSubmit = () => { + const trimmedNote = noteDraft.trim(); + + if (!trimmedNote) { + return; + } + + onCaptureThought(trimmedNote); + setNoteDraft(''); + pushToast({ title: '메모를 잠깐 주차했어요.' }); + }; return ( <> -
-
- {TOOL_ITEMS.map((item) => { - const selected = activePanel === item.id; + {openPopover ? ( + - ); - })} -
+
+
+ {isFocusMode ? ( + <> +
+
+
+ + + {openPopover === 'notes' ? ( +
+

떠오른 생각을 잠깐 주차해요

+
+ setNoteDraft(event.target.value)} + placeholder="한 줄 메모" + className="h-8 min-w-0 flex-1 rounded-lg border border-white/14 bg-white/[0.04] px-2.5 text-xs text-white placeholder:text-white/38 focus:border-sky-200/42 focus:outline-none" + /> + +
+
    + {thoughts.slice(0, 3).map((thought) => ( +
  • + {thought.text} +
  • + ))} + {thoughts.length === 0 ? ( +
  • + 아직 메모가 없어요. +
  • + ) : null} +
+
+ + + +
+
+ ) : null} +
+
+ +
+
+
+ + + {openPopover === 'sound' ? ( +
+

사운드 프리셋

+
+ {SOUND_PRESETS.slice(0, 6).map((preset) => { + const selected = preset.id === selectedPresetId; + + return ( + + ); + })} +
+
+ +
+
+ ) : null} +
+
+ + ) : null} + setActivePanel(null)} + open={utilityPanel !== null} + title={utilityPanel ? UTILITY_PANEL_TITLE[utilityPanel] : ''} + onClose={() => setUtilityPanel(null)} > - {activePanel === 'sound' ? ( - - ) : null} - - {activePanel === 'notes' ? ( - { - onCaptureThought(note); - pushToast({ title: '인박스에 주차했어요 (더미)' }); - }} - /> - ) : null} - - {activePanel === 'inbox' ? ( + {utilityPanel === 'inbox' ? ( { @@ -205,8 +373,23 @@ export const SpaceToolsDockWidget = ({ /> ) : null} - {activePanel === 'stats' ? : null} - {activePanel === 'settings' ? : null} + {utilityPanel === 'stats' ? : null} + {utilityPanel === 'settings' ? ( + { + onRoomSelect(roomId); + pushToast({ title: '공간을 바꿨어요.' }); + }} + onSelectTimer={(label) => { + onTimerSelect(label); + pushToast({ title: `타이머를 ${label}로 바꿨어요.` }); + }} + /> + ) : null} ); diff --git a/src/widgets/space-tools-dock/ui/panels/SettingsToolPanel.tsx b/src/widgets/space-tools-dock/ui/panels/SettingsToolPanel.tsx index 58767b5..d1ec011 100644 --- a/src/widgets/space-tools-dock/ui/panels/SettingsToolPanel.tsx +++ b/src/widgets/space-tools-dock/ui/panels/SettingsToolPanel.tsx @@ -1,10 +1,28 @@ 'use client'; import { useState } from 'react'; +import type { RoomTheme } from '@/entities/room'; +import type { TimerPreset } from '@/entities/session'; import { DEFAULT_PRESET_OPTIONS } from '@/shared/config/settingsOptions'; import { cn } from '@/shared/lib/cn'; -export const SettingsToolPanel = () => { +interface SettingsToolPanelProps { + rooms: RoomTheme[]; + selectedRoomId: string; + selectedTimerLabel: string; + timerPresets: TimerPreset[]; + onSelectRoom: (roomId: string) => void; + onSelectTimer: (timerLabel: string) => void; +} + +export const SettingsToolPanel = ({ + rooms, + selectedRoomId, + selectedTimerLabel, + timerPresets, + onSelectRoom, + onSelectTimer, +}: SettingsToolPanelProps) => { const [reduceMotion, setReduceMotion] = useState(false); const [defaultPresetId, setDefaultPresetId] = useState< (typeof DEFAULT_PRESET_OPTIONS)[number]['id'] @@ -40,6 +58,58 @@ export const SettingsToolPanel = () => {
+
+

공간

+

몰입 중에도 공간을 바꿀 수 있어요.

+
+ {rooms.slice(0, 4).map((room) => { + const selected = room.id === selectedRoomId; + + return ( + + ); + })} +
+
+ +
+

타이머 프리셋

+

기본 프리셋만 빠르게 고를 수 있어요.

+
+ {timerPresets.slice(0, 3).map((preset) => { + const selected = preset.label === selectedTimerLabel; + + return ( + + ); + })} +
+
+

기본 프리셋

diff --git a/src/widgets/space-workspace/ui/SpaceWorkspaceWidget.tsx b/src/widgets/space-workspace/ui/SpaceWorkspaceWidget.tsx index 843fa99..bfd8dcd 100644 --- a/src/widgets/space-workspace/ui/SpaceWorkspaceWidget.tsx +++ b/src/widgets/space-workspace/ui/SpaceWorkspaceWidget.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useMemo, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { useSearchParams } from 'next/navigation'; import { getRoomBackgroundStyle, @@ -10,8 +10,10 @@ import { import { GOAL_CHIPS, SOUND_PRESETS, + TIMER_PRESETS, useThoughtInbox, type GoalChip, + type TimerPreset, } from '@/entities/session'; import { useSoundPresetSelection } from '@/features/sound-preset'; import { useToast } from '@/shared/ui'; @@ -37,6 +39,19 @@ const resolveInitialSoundPreset = (presetIdFromQuery: string | null) => { return SOUND_PRESETS[0].id; }; +const TIMER_SELECTION_PRESETS = TIMER_PRESETS.filter( + (preset): preset is TimerPreset & { focusMinutes: number; breakMinutes: number } => + typeof preset.focusMinutes === 'number' && typeof preset.breakMinutes === 'number', +).slice(0, 3); + +const resolveInitialTimerLabel = (timerLabelFromQuery: string | null) => { + if (timerLabelFromQuery && TIMER_SELECTION_PRESETS.some((preset) => preset.label === timerLabelFromQuery)) { + return timerLabelFromQuery; + } + + return TIMER_SELECTION_PRESETS[0]?.label ?? '25/5'; +}; + export const SpaceWorkspaceWidget = () => { const searchParams = useSearchParams(); const { pushToast } = useToast(); @@ -45,33 +60,31 @@ export const SpaceWorkspaceWidget = () => { const initialRoomId = resolveInitialRoomId(searchParams.get('room')); const initialGoal = searchParams.get('goal')?.trim() ?? ''; const initialSoundPresetId = resolveInitialSoundPreset(searchParams.get('sound')); + const initialTimerLabel = resolveInitialTimerLabel(searchParams.get('timer')); const [workspaceMode, setWorkspaceMode] = useState('setup'); - const [isSetupDrawerOpen, setSetupDrawerOpen] = useState(true); const [selectedRoomId, setSelectedRoomId] = useState(initialRoomId); + const [selectedTimerLabel, setSelectedTimerLabel] = useState(initialTimerLabel); const [goalInput, setGoalInput] = useState(initialGoal); const [selectedGoalId, setSelectedGoalId] = useState(null); const { selectedPresetId, setSelectedPresetId, - isMixerOpen, - setMixerOpen, - isMuted, - setMuted, - masterVolume, - setMasterVolume, - trackLevels, - setTrackLevel, - trackKeys, } = useSoundPresetSelection(initialSoundPresetId); const selectedRoom = useMemo(() => { return getRoomById(selectedRoomId) ?? ROOM_THEMES[0]; }, [selectedRoomId]); + const setupRooms = useMemo(() => { - const restRooms = ROOM_THEMES.filter((room) => room.id !== selectedRoom.id); - return [selectedRoom, ...restRooms].slice(0, 4); + const visibleRooms = ROOM_THEMES.slice(0, 6); + + if (visibleRooms.some((room) => room.id === selectedRoom.id)) { + return visibleRooms; + } + + return [selectedRoom, ...visibleRooms].slice(0, 6); }, [selectedRoom]); const canStart = goalInput.trim().length > 0; @@ -96,19 +109,32 @@ export const SpaceWorkspaceWidget = () => { } setWorkspaceMode('focus'); - setSetupDrawerOpen(false); pushToast({ title: `목표: ${goalInput.trim()} 시작해요.`, }); }; - const handleOpenSetup = () => { - setSetupDrawerOpen(true); + const handleExitRequested = () => { + setWorkspaceMode('setup'); + pushToast({ title: '준비 모드로 돌아왔어요.' }); }; + useEffect(() => { + const previousBodyOverflow = document.body.style.overflow; + const previousHtmlOverflow = document.documentElement.style.overflow; + + document.body.style.overflow = 'hidden'; + document.documentElement.style.overflow = 'hidden'; + + return () => { + document.body.style.overflow = previousBodyOverflow; + document.documentElement.style.overflow = previousHtmlOverflow; + }; + }, []); + return ( -
+
{ }} /> -
-
- {!isFocusMode ? ( -
-

VibeRoom

-
- ) : ( -
- )} - - {isFocusMode && !isSetupDrawerOpen ? ( - - ) : null} -
- +
{ - if (isFocusMode) { - setSetupDrawerOpen(false); - } - }} onRoomSelect={setSelectedRoomId} + onTimerSelect={setSelectedTimerLabel} + onSoundSelect={setSelectedPresetId} onGoalChange={handleGoalChange} onGoalChipSelect={handleGoalChipSelect} - onSoundSelect={setSelectedPresetId} onStart={handleStart} /> - + setMixerOpen((current) => !current)} - isMuted={isMuted} - onMuteChange={setMuted} - masterVolume={masterVolume} - onMasterVolumeChange={setMasterVolume} - trackKeys={trackKeys} - trackLevels={trackLevels} - onTrackLevelChange={setTrackLevel} onCaptureThought={(note) => addThought(note, selectedRoom.name)} onClearInbox={clearThoughts} + onExitRequested={handleExitRequested} />
);