fix(space-ui): /space 포커스 앵커 잘림과 스크롤 문제 수정

This commit is contained in:
2026-03-03 17:50:49 +09:00
parent ef9cc63cc5
commit be16153bef
8 changed files with 735 additions and 306 deletions

View File

@@ -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<WorkspaceMode>('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<string | null>(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 (
<div className="relative min-h-screen overflow-hidden text-white">
<div className="relative h-dvh overflow-hidden text-white">
<div
aria-hidden
className="absolute inset-0 bg-cover bg-center will-change-transform animate-[space-stage-pan_42s_ease-in-out_infinite_alternate] motion-reduce:animate-none"
@@ -135,72 +161,51 @@ export const SpaceWorkspaceWidget = () => {
}}
/>
<div className="relative z-10 flex min-h-screen flex-col pr-[4.25rem]">
<header className="flex items-center justify-between px-4 pt-3.5 sm:px-6">
{!isFocusMode ? (
<div className="rounded-full border border-white/16 bg-slate-950/32 px-3 py-1.5 backdrop-blur-xl">
<p className="text-xs font-semibold tracking-tight text-white/88">VibeRoom</p>
</div>
) : (
<div aria-hidden className="h-7" />
)}
{isFocusMode && !isSetupDrawerOpen ? (
<button
type="button"
onClick={handleOpenSetup}
className="rounded-full border border-white/16 bg-slate-950/24 px-2.5 py-1 text-[11px] text-white/58 transition-colors hover:bg-slate-950/40 hover:text-white/84"
>
Setup
</button>
) : null}
</header>
<div className="relative z-10 flex h-full flex-col">
<main className="relative flex-1" />
</div>
<SpaceSetupDrawerWidget
open={isSetupDrawerOpen}
dismissible={isFocusMode}
open={!isFocusMode}
rooms={setupRooms}
selectedRoomId={selectedRoom.id}
selectedTimerLabel={selectedTimerLabel}
selectedSoundPresetId={selectedPresetId}
goalInput={goalInput}
selectedGoalId={selectedGoalId}
selectedSoundPresetId={selectedPresetId}
goalChips={GOAL_CHIPS}
soundPresets={SOUND_PRESETS}
timerPresets={TIMER_SELECTION_PRESETS}
canStart={canStart}
onClose={() => {
if (isFocusMode) {
setSetupDrawerOpen(false);
}
}}
onRoomSelect={setSelectedRoomId}
onTimerSelect={setSelectedTimerLabel}
onSoundSelect={setSelectedPresetId}
onGoalChange={handleGoalChange}
onGoalChipSelect={handleGoalChipSelect}
onSoundSelect={setSelectedPresetId}
onStart={handleStart}
/>
<SpaceFocusHudWidget goal={goalInput.trim()} visible={isFocusMode} />
<SpaceFocusHudWidget
goal={goalInput.trim()}
timerLabel={selectedTimerLabel}
visible={isFocusMode}
/>
<SpaceToolsDockWidget
isFocusMode={isFocusMode}
rooms={setupRooms}
selectedRoomId={selectedRoom.id}
selectedTimerLabel={selectedTimerLabel}
timerPresets={TIMER_SELECTION_PRESETS}
thoughts={thoughts}
thoughtCount={thoughtCount}
selectedPresetId={selectedPresetId}
onRoomSelect={setSelectedRoomId}
onTimerSelect={setSelectedTimerLabel}
onSelectPreset={setSelectedPresetId}
isMixerOpen={isMixerOpen}
onToggleMixer={() => 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}
/>
</div>
);