From 698c124ade3ac823d4763e4dae3279bb140ddc03 Mon Sep 17 00:00:00 2001 From: corpi Date: Wed, 11 Mar 2026 16:21:44 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=EC=98=A4=EB=94=94=EC=98=A4=EA=B0=80=20?= =?UTF-8?q?=EC=9E=AC=EC=83=9D=EB=90=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SpaceWorkspaceWidget 내 순환 참조를 피하기 위해 하드코딩되었던 `shouldPlay: false` 문제 해결 - 핵심 UI 상태(workspaceMode, previewPlaybackState 등)를 SpaceWorkspaceWidget 최상단으로 끌어올림(Lifting State Up) - useHudStatusLine의 중복 호출 제거 --- .../model/useSpaceWorkspaceSessionControls.ts | 31 ++++++---- .../ui/SpaceWorkspaceWidget.tsx | 57 ++++++++++--------- 2 files changed, 48 insertions(+), 40 deletions(-) diff --git a/src/widgets/space-workspace/model/useSpaceWorkspaceSessionControls.ts b/src/widgets/space-workspace/model/useSpaceWorkspaceSessionControls.ts index 12baf6d..6b0a742 100644 --- a/src/widgets/space-workspace/model/useSpaceWorkspaceSessionControls.ts +++ b/src/widgets/space-workspace/model/useSpaceWorkspaceSessionControls.ts @@ -1,6 +1,6 @@ 'use client'; -import { useCallback, useEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useRef } from 'react'; import type { FocusSession } from '@/features/focus-session'; import { copy } from '@/shared/i18n'; import type { HudStatusLinePayload } from '@/shared/lib/useHudStatusLine'; @@ -8,6 +8,12 @@ import { resolveTimerPresetIdFromLabel } from './workspaceSelection'; import type { SessionEntryPoint, WorkspaceMode } from './types'; interface UseSpaceWorkspaceSessionControlsParams { + workspaceMode: WorkspaceMode; + setWorkspaceMode: (mode: WorkspaceMode) => void; + previewPlaybackState: 'running' | 'paused'; + setPreviewPlaybackState: (state: 'running' | 'paused') => void; + pendingSessionEntryPoint: SessionEntryPoint; + setPendingSessionEntryPoint: (entryPoint: SessionEntryPoint) => void; canStart: boolean; currentSession: FocusSession | null; goalInput: string; @@ -39,6 +45,12 @@ interface UseSpaceWorkspaceSessionControlsParams { } export const useSpaceWorkspaceSessionControls = ({ + workspaceMode, + setWorkspaceMode, + previewPlaybackState, + setPreviewPlaybackState, + pendingSessionEntryPoint, + setPendingSessionEntryPoint, canStart, currentSession, goalInput, @@ -59,10 +71,6 @@ export const useSpaceWorkspaceSessionControls = ({ setSelectedGoalId, setShowResumePrompt, }: UseSpaceWorkspaceSessionControlsParams) => { - const [workspaceMode, setWorkspaceMode] = useState('setup'); - const [previewPlaybackState, setPreviewPlaybackState] = useState<'running' | 'paused'>('paused'); - const [pendingSessionEntryPoint, setPendingSessionEntryPoint] = - useState('space-setup'); const queuedFocusStatusMessageRef = useRef(null); const lastSoundPlaybackErrorRef = useRef(null); @@ -87,7 +95,7 @@ export const useSpaceWorkspaceSessionControls = ({ setPreviewPlaybackState('paused'); setWorkspaceMode('focus'); queuedFocusStatusMessageRef.current = copy.space.workspace.readyToStart; - }, [setShowResumePrompt]); + }, [setPendingSessionEntryPoint, setPreviewPlaybackState, setShowResumePrompt, setWorkspaceMode]); const startFocusFlow = useCallback(async () => { const trimmedGoal = goalInput.trim(); @@ -121,6 +129,7 @@ export const useSpaceWorkspaceSessionControls = ({ selectedPresetId, selectedSceneId, selectedTimerLabel, + setPreviewPlaybackState, startSession, ]); @@ -175,7 +184,7 @@ export const useSpaceWorkspaceSessionControls = ({ setPreviewPlaybackState('paused'); setPendingSessionEntryPoint('space-setup'); setWorkspaceMode('setup'); - }, [abandonSession, pushStatusLine]); + }, [abandonSession, pushStatusLine, setPendingSessionEntryPoint, setPreviewPlaybackState, setWorkspaceMode]); const handlePauseRequested = useCallback(async () => { if (!currentSession) { @@ -190,7 +199,7 @@ export const useSpaceWorkspaceSessionControls = ({ message: copy.space.workspace.pauseFailed, }); } - }, [currentSession, pauseSession, pushStatusLine]); + }, [currentSession, pauseSession, pushStatusLine, setPreviewPlaybackState]); const handleRestartRequested = useCallback(async () => { if (!currentSession) { @@ -239,7 +248,7 @@ export const useSpaceWorkspaceSessionControls = ({ pushStatusLine({ message: copy.space.workspace.nextGoalReady, }); - }, [completeSession, currentSession, goalInput, pushStatusLine, setGoalInput, setSelectedGoalId]); + }, [completeSession, currentSession, goalInput, pushStatusLine, setGoalInput, setPendingSessionEntryPoint, setPreviewPlaybackState, setSelectedGoalId]); useEffect(() => { const previousBodyOverflow = document.body.style.overflow; @@ -267,7 +276,7 @@ export const useSpaceWorkspaceSessionControls = ({ return () => { window.cancelAnimationFrame(rafId); }; - }, [currentSession]); + }, [currentSession, setPreviewPlaybackState, setWorkspaceMode]); useEffect(() => { if (!isFocusMode || !queuedFocusStatusMessageRef.current) { @@ -298,9 +307,7 @@ export const useSpaceWorkspaceSessionControls = ({ }, [pushStatusLine, soundPlaybackError]); return { - workspaceMode, isFocusMode, - previewPlaybackState, resolvedPlaybackState, canStartSession, canPauseSession, diff --git a/src/widgets/space-workspace/ui/SpaceWorkspaceWidget.tsx b/src/widgets/space-workspace/ui/SpaceWorkspaceWidget.tsx index 9d4a95c..3a43a87 100644 --- a/src/widgets/space-workspace/ui/SpaceWorkspaceWidget.tsx +++ b/src/widgets/space-workspace/ui/SpaceWorkspaceWidget.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useEffect, useMemo } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { useSearchParams } from 'next/navigation'; import { getSceneById, SCENE_THEMES } from '@/entities/scene'; import { @@ -20,6 +20,7 @@ import { useHudStatusLine } from '@/shared/lib/useHudStatusLine'; import { SpaceFocusHudWidget } from '@/widgets/space-focus-hud'; import { SpaceSetupDrawerWidget } from '@/widgets/space-setup-drawer'; import { SpaceToolsDockWidget } from '@/widgets/space-tools-dock'; +import type { SessionEntryPoint, WorkspaceMode } from '../model/types'; import { useSpaceWorkspaceSelection } from '../model/useSpaceWorkspaceSelection'; import { useSpaceWorkspaceSessionControls } from '../model/useSpaceWorkspaceSessionControls'; import { @@ -72,6 +73,10 @@ export const SpaceWorkspaceWidget = () => { initialScene.recommendedTimerPresetId, ), [initialScene.recommendedTimerPresetId, timerQuery]); + const [workspaceMode, setWorkspaceMode] = useState('setup'); + const [previewPlaybackState, setPreviewPlaybackState] = useState<'running' | 'paused'>('paused'); + const [pendingSessionEntryPoint, setPendingSessionEntryPoint] = useState('space-setup'); + const { selectedPresetId, setSelectedPresetId, @@ -96,14 +101,18 @@ export const SpaceWorkspaceWidget = () => { abandonSession, } = useFocusSessionEngine(); - const isFocusModeRef = { current: false }; // Temporary placeholder for isFocusMode value used in useHudStatusLine if needed early + const isFocusMode = workspaceMode === 'focus'; + const resolvedPlaybackState = currentSession?.state ?? previewPlaybackState; + const shouldPlaySound = isFocusMode && resolvedPlaybackState === 'running'; + + const { activeStatus, pushStatusLine, runActiveAction } = useHudStatusLine(isFocusMode); const { error: soundPlaybackError, unlockPlayback } = useSoundPlayback({ selectedPresetId, soundAsset: soundAssetMap[selectedPresetId], masterVolume, isMuted, - shouldPlay: false, // Will be updated by hook or effect + shouldPlay: shouldPlaySound, }); const resolveSoundPlaybackUrl = (presetId: string) => { @@ -114,8 +123,6 @@ export const SpaceWorkspaceWidget = () => { return asset?.loopUrl ?? asset?.fallbackLoopUrl ?? null; }; - const { activeStatus, pushStatusLine, runActiveAction } = useHudStatusLine(false); // Initial value - const selection = useSpaceWorkspaceSelection({ initialSceneId, initialGoal: goalQuery, @@ -129,7 +136,7 @@ export const SpaceWorkspaceWidget = () => { sceneAssetMap, selectedPresetId, setSelectedPresetId, - shouldPlaySound: false, // We'll handle this in the component for now to avoid circular dependency or complex prop drilling + shouldPlaySound, unlockPlayback, resolveSoundPlaybackUrl, pushStatusLine, @@ -140,6 +147,12 @@ export const SpaceWorkspaceWidget = () => { }); const controls = useSpaceWorkspaceSessionControls({ + workspaceMode, + setWorkspaceMode, + previewPlaybackState, + setPreviewPlaybackState, + pendingSessionEntryPoint, + setPendingSessionEntryPoint, canStart: selection.canStart, currentSession, goalInput: selection.goalInput, @@ -161,18 +174,6 @@ export const SpaceWorkspaceWidget = () => { setShowResumePrompt: selection.setShowResumePrompt, }); - // Connect shouldPlaySound to useHudStatusLine and useSoundPlayback - const shouldPlaySound = controls.isFocusMode && controls.resolvedPlaybackState === 'running'; - - // We need to re-initialize useHudStatusLine with the correct mode - const { activeStatus: focusActiveStatus, pushStatusLine: focusPushStatusLine, runActiveAction: focusRunActiveAction } = useHudStatusLine(controls.isFocusMode); - - // Sync selection shouldPlaySound - useEffect(() => { - // This is a bit tricky since useSpaceWorkspaceSelection uses shouldPlaySound. - // For now, let's assume it's handled. - }, [shouldPlaySound]); - useEffect(() => { const preferMobile = typeof window !== 'undefined' ? window.matchMedia('(max-width: 767px)').matches : false; @@ -194,7 +195,7 @@ export const SpaceWorkspaceWidget = () => { { goal={selection.goalInput.trim()} timerLabel={selection.selectedTimerLabel} timeDisplay={resolvedTimeDisplay} - visible={controls.isFocusMode} + visible={isFocusMode} hasActiveSession={Boolean(currentSession)} - playbackState={controls.resolvedPlaybackState} + playbackState={resolvedPlaybackState} sessionPhase={phase ?? 'focus'} isSessionActionPending={isSessionMutating} canStartSession={controls.canStartSession} @@ -253,19 +254,19 @@ export const SpaceWorkspaceWidget = () => { onRestartRequested={() => { void controls.handleRestartRequested(); }} - onStatusMessage={focusPushStatusLine} + onStatusMessage={pushStatusLine} onGoalUpdate={controls.handleGoalAdvance} /> { onRestoreThought={restoreThought} onRestoreThoughts={restoreThoughts} onClearInbox={clearThoughts} - onStatusMessage={focusPushStatusLine} + onStatusMessage={pushStatusLine} onExitRequested={() => { void controls.handleExitRequested(); }}