fix: 오디오가 재생되지 않는 문제 수정
- SpaceWorkspaceWidget 내 순환 참조를 피하기 위해 하드코딩되었던 `shouldPlay: false` 문제 해결 - 핵심 UI 상태(workspaceMode, previewPlaybackState 등)를 SpaceWorkspaceWidget 최상단으로 끌어올림(Lifting State Up) - useHudStatusLine의 중복 호출 제거
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user