fix: 오디오가 재생되지 않는 문제 수정

- SpaceWorkspaceWidget 내 순환 참조를 피하기 위해 하드코딩되었던 `shouldPlay: false` 문제 해결
- 핵심 UI 상태(workspaceMode, previewPlaybackState 등)를 SpaceWorkspaceWidget 최상단으로 끌어올림(Lifting State Up)
- useHudStatusLine의 중복 호출 제거
This commit is contained in:
2026-03-11 16:21:44 +09:00
parent 972be117cb
commit 698c124ade
2 changed files with 48 additions and 40 deletions

View File

@@ -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<WorkspaceMode>('setup');
const [previewPlaybackState, setPreviewPlaybackState] = useState<'running' | 'paused'>('paused');
const [pendingSessionEntryPoint, setPendingSessionEntryPoint] =
useState<SessionEntryPoint>('space-setup');
const queuedFocusStatusMessageRef = useRef<string | null>(null);
const lastSoundPlaybackErrorRef = useRef<string | null>(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,

View File

@@ -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<WorkspaceMode>('setup');
const [previewPlaybackState, setPreviewPlaybackState] = useState<'running' | 'paused'>('paused');
const [pendingSessionEntryPoint, setPendingSessionEntryPoint] = useState<SessionEntryPoint>('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 = () => {
</div>
<SpaceSetupDrawerWidget
open={!controls.isFocusMode}
open={!isFocusMode}
scenes={selection.setupScenes}
sceneAssetMap={sceneAssetMap}
selectedSceneId={selection.selectedScene.id}
@@ -236,9 +237,9 @@ 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}
/>
<FocusTopToast
visible={controls.isFocusMode && Boolean(focusActiveStatus)}
message={focusActiveStatus?.message ?? ''}
actionLabel={focusActiveStatus?.action?.label}
onAction={focusRunActiveAction}
visible={isFocusMode && Boolean(activeStatus)}
message={activeStatus?.message ?? ''}
actionLabel={activeStatus?.action?.label}
onAction={runActiveAction}
/>
<SpaceToolsDockWidget
isFocusMode={controls.isFocusMode}
isFocusMode={isFocusMode}
scenes={selection.setupScenes}
sceneAssetMap={sceneAssetMap}
selectedSceneId={selection.selectedScene.id}
@@ -289,7 +290,7 @@ export const SpaceWorkspaceWidget = () => {
onRestoreThought={restoreThought}
onRestoreThoughts={restoreThoughts}
onClearInbox={clearThoughts}
onStatusMessage={focusPushStatusLine}
onStatusMessage={pushStatusLine}
onExitRequested={() => {
void controls.handleExitRequested();
}}