fix: 오디오가 재생되지 않는 문제 수정
- SpaceWorkspaceWidget 내 순환 참조를 피하기 위해 하드코딩되었던 `shouldPlay: false` 문제 해결 - 핵심 UI 상태(workspaceMode, previewPlaybackState 등)를 SpaceWorkspaceWidget 최상단으로 끌어올림(Lifting State Up) - useHudStatusLine의 중복 호출 제거
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef } from 'react';
|
||||||
import type { FocusSession } from '@/features/focus-session';
|
import type { FocusSession } from '@/features/focus-session';
|
||||||
import { copy } from '@/shared/i18n';
|
import { copy } from '@/shared/i18n';
|
||||||
import type { HudStatusLinePayload } from '@/shared/lib/useHudStatusLine';
|
import type { HudStatusLinePayload } from '@/shared/lib/useHudStatusLine';
|
||||||
@@ -8,6 +8,12 @@ import { resolveTimerPresetIdFromLabel } from './workspaceSelection';
|
|||||||
import type { SessionEntryPoint, WorkspaceMode } from './types';
|
import type { SessionEntryPoint, WorkspaceMode } from './types';
|
||||||
|
|
||||||
interface UseSpaceWorkspaceSessionControlsParams {
|
interface UseSpaceWorkspaceSessionControlsParams {
|
||||||
|
workspaceMode: WorkspaceMode;
|
||||||
|
setWorkspaceMode: (mode: WorkspaceMode) => void;
|
||||||
|
previewPlaybackState: 'running' | 'paused';
|
||||||
|
setPreviewPlaybackState: (state: 'running' | 'paused') => void;
|
||||||
|
pendingSessionEntryPoint: SessionEntryPoint;
|
||||||
|
setPendingSessionEntryPoint: (entryPoint: SessionEntryPoint) => void;
|
||||||
canStart: boolean;
|
canStart: boolean;
|
||||||
currentSession: FocusSession | null;
|
currentSession: FocusSession | null;
|
||||||
goalInput: string;
|
goalInput: string;
|
||||||
@@ -39,6 +45,12 @@ interface UseSpaceWorkspaceSessionControlsParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const useSpaceWorkspaceSessionControls = ({
|
export const useSpaceWorkspaceSessionControls = ({
|
||||||
|
workspaceMode,
|
||||||
|
setWorkspaceMode,
|
||||||
|
previewPlaybackState,
|
||||||
|
setPreviewPlaybackState,
|
||||||
|
pendingSessionEntryPoint,
|
||||||
|
setPendingSessionEntryPoint,
|
||||||
canStart,
|
canStart,
|
||||||
currentSession,
|
currentSession,
|
||||||
goalInput,
|
goalInput,
|
||||||
@@ -59,10 +71,6 @@ export const useSpaceWorkspaceSessionControls = ({
|
|||||||
setSelectedGoalId,
|
setSelectedGoalId,
|
||||||
setShowResumePrompt,
|
setShowResumePrompt,
|
||||||
}: UseSpaceWorkspaceSessionControlsParams) => {
|
}: 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 queuedFocusStatusMessageRef = useRef<string | null>(null);
|
||||||
const lastSoundPlaybackErrorRef = useRef<string | null>(null);
|
const lastSoundPlaybackErrorRef = useRef<string | null>(null);
|
||||||
|
|
||||||
@@ -87,7 +95,7 @@ export const useSpaceWorkspaceSessionControls = ({
|
|||||||
setPreviewPlaybackState('paused');
|
setPreviewPlaybackState('paused');
|
||||||
setWorkspaceMode('focus');
|
setWorkspaceMode('focus');
|
||||||
queuedFocusStatusMessageRef.current = copy.space.workspace.readyToStart;
|
queuedFocusStatusMessageRef.current = copy.space.workspace.readyToStart;
|
||||||
}, [setShowResumePrompt]);
|
}, [setPendingSessionEntryPoint, setPreviewPlaybackState, setShowResumePrompt, setWorkspaceMode]);
|
||||||
|
|
||||||
const startFocusFlow = useCallback(async () => {
|
const startFocusFlow = useCallback(async () => {
|
||||||
const trimmedGoal = goalInput.trim();
|
const trimmedGoal = goalInput.trim();
|
||||||
@@ -121,6 +129,7 @@ export const useSpaceWorkspaceSessionControls = ({
|
|||||||
selectedPresetId,
|
selectedPresetId,
|
||||||
selectedSceneId,
|
selectedSceneId,
|
||||||
selectedTimerLabel,
|
selectedTimerLabel,
|
||||||
|
setPreviewPlaybackState,
|
||||||
startSession,
|
startSession,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -175,7 +184,7 @@ export const useSpaceWorkspaceSessionControls = ({
|
|||||||
setPreviewPlaybackState('paused');
|
setPreviewPlaybackState('paused');
|
||||||
setPendingSessionEntryPoint('space-setup');
|
setPendingSessionEntryPoint('space-setup');
|
||||||
setWorkspaceMode('setup');
|
setWorkspaceMode('setup');
|
||||||
}, [abandonSession, pushStatusLine]);
|
}, [abandonSession, pushStatusLine, setPendingSessionEntryPoint, setPreviewPlaybackState, setWorkspaceMode]);
|
||||||
|
|
||||||
const handlePauseRequested = useCallback(async () => {
|
const handlePauseRequested = useCallback(async () => {
|
||||||
if (!currentSession) {
|
if (!currentSession) {
|
||||||
@@ -190,7 +199,7 @@ export const useSpaceWorkspaceSessionControls = ({
|
|||||||
message: copy.space.workspace.pauseFailed,
|
message: copy.space.workspace.pauseFailed,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [currentSession, pauseSession, pushStatusLine]);
|
}, [currentSession, pauseSession, pushStatusLine, setPreviewPlaybackState]);
|
||||||
|
|
||||||
const handleRestartRequested = useCallback(async () => {
|
const handleRestartRequested = useCallback(async () => {
|
||||||
if (!currentSession) {
|
if (!currentSession) {
|
||||||
@@ -239,7 +248,7 @@ export const useSpaceWorkspaceSessionControls = ({
|
|||||||
pushStatusLine({
|
pushStatusLine({
|
||||||
message: copy.space.workspace.nextGoalReady,
|
message: copy.space.workspace.nextGoalReady,
|
||||||
});
|
});
|
||||||
}, [completeSession, currentSession, goalInput, pushStatusLine, setGoalInput, setSelectedGoalId]);
|
}, [completeSession, currentSession, goalInput, pushStatusLine, setGoalInput, setPendingSessionEntryPoint, setPreviewPlaybackState, setSelectedGoalId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const previousBodyOverflow = document.body.style.overflow;
|
const previousBodyOverflow = document.body.style.overflow;
|
||||||
@@ -267,7 +276,7 @@ export const useSpaceWorkspaceSessionControls = ({
|
|||||||
return () => {
|
return () => {
|
||||||
window.cancelAnimationFrame(rafId);
|
window.cancelAnimationFrame(rafId);
|
||||||
};
|
};
|
||||||
}, [currentSession]);
|
}, [currentSession, setPreviewPlaybackState, setWorkspaceMode]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isFocusMode || !queuedFocusStatusMessageRef.current) {
|
if (!isFocusMode || !queuedFocusStatusMessageRef.current) {
|
||||||
@@ -298,9 +307,7 @@ export const useSpaceWorkspaceSessionControls = ({
|
|||||||
}, [pushStatusLine, soundPlaybackError]);
|
}, [pushStatusLine, soundPlaybackError]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
workspaceMode,
|
|
||||||
isFocusMode,
|
isFocusMode,
|
||||||
previewPlaybackState,
|
|
||||||
resolvedPlaybackState,
|
resolvedPlaybackState,
|
||||||
canStartSession,
|
canStartSession,
|
||||||
canPauseSession,
|
canPauseSession,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useMemo } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { useSearchParams } from 'next/navigation';
|
import { useSearchParams } from 'next/navigation';
|
||||||
import { getSceneById, SCENE_THEMES } from '@/entities/scene';
|
import { getSceneById, SCENE_THEMES } from '@/entities/scene';
|
||||||
import {
|
import {
|
||||||
@@ -20,6 +20,7 @@ import { useHudStatusLine } from '@/shared/lib/useHudStatusLine';
|
|||||||
import { SpaceFocusHudWidget } from '@/widgets/space-focus-hud';
|
import { SpaceFocusHudWidget } from '@/widgets/space-focus-hud';
|
||||||
import { SpaceSetupDrawerWidget } from '@/widgets/space-setup-drawer';
|
import { SpaceSetupDrawerWidget } from '@/widgets/space-setup-drawer';
|
||||||
import { SpaceToolsDockWidget } from '@/widgets/space-tools-dock';
|
import { SpaceToolsDockWidget } from '@/widgets/space-tools-dock';
|
||||||
|
import type { SessionEntryPoint, WorkspaceMode } from '../model/types';
|
||||||
import { useSpaceWorkspaceSelection } from '../model/useSpaceWorkspaceSelection';
|
import { useSpaceWorkspaceSelection } from '../model/useSpaceWorkspaceSelection';
|
||||||
import { useSpaceWorkspaceSessionControls } from '../model/useSpaceWorkspaceSessionControls';
|
import { useSpaceWorkspaceSessionControls } from '../model/useSpaceWorkspaceSessionControls';
|
||||||
import {
|
import {
|
||||||
@@ -72,6 +73,10 @@ export const SpaceWorkspaceWidget = () => {
|
|||||||
initialScene.recommendedTimerPresetId,
|
initialScene.recommendedTimerPresetId,
|
||||||
), [initialScene.recommendedTimerPresetId, timerQuery]);
|
), [initialScene.recommendedTimerPresetId, timerQuery]);
|
||||||
|
|
||||||
|
const [workspaceMode, setWorkspaceMode] = useState<WorkspaceMode>('setup');
|
||||||
|
const [previewPlaybackState, setPreviewPlaybackState] = useState<'running' | 'paused'>('paused');
|
||||||
|
const [pendingSessionEntryPoint, setPendingSessionEntryPoint] = useState<SessionEntryPoint>('space-setup');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
selectedPresetId,
|
selectedPresetId,
|
||||||
setSelectedPresetId,
|
setSelectedPresetId,
|
||||||
@@ -96,14 +101,18 @@ export const SpaceWorkspaceWidget = () => {
|
|||||||
abandonSession,
|
abandonSession,
|
||||||
} = useFocusSessionEngine();
|
} = 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({
|
const { error: soundPlaybackError, unlockPlayback } = useSoundPlayback({
|
||||||
selectedPresetId,
|
selectedPresetId,
|
||||||
soundAsset: soundAssetMap[selectedPresetId],
|
soundAsset: soundAssetMap[selectedPresetId],
|
||||||
masterVolume,
|
masterVolume,
|
||||||
isMuted,
|
isMuted,
|
||||||
shouldPlay: false, // Will be updated by hook or effect
|
shouldPlay: shouldPlaySound,
|
||||||
});
|
});
|
||||||
|
|
||||||
const resolveSoundPlaybackUrl = (presetId: string) => {
|
const resolveSoundPlaybackUrl = (presetId: string) => {
|
||||||
@@ -114,8 +123,6 @@ export const SpaceWorkspaceWidget = () => {
|
|||||||
return asset?.loopUrl ?? asset?.fallbackLoopUrl ?? null;
|
return asset?.loopUrl ?? asset?.fallbackLoopUrl ?? null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const { activeStatus, pushStatusLine, runActiveAction } = useHudStatusLine(false); // Initial value
|
|
||||||
|
|
||||||
const selection = useSpaceWorkspaceSelection({
|
const selection = useSpaceWorkspaceSelection({
|
||||||
initialSceneId,
|
initialSceneId,
|
||||||
initialGoal: goalQuery,
|
initialGoal: goalQuery,
|
||||||
@@ -129,7 +136,7 @@ export const SpaceWorkspaceWidget = () => {
|
|||||||
sceneAssetMap,
|
sceneAssetMap,
|
||||||
selectedPresetId,
|
selectedPresetId,
|
||||||
setSelectedPresetId,
|
setSelectedPresetId,
|
||||||
shouldPlaySound: false, // We'll handle this in the component for now to avoid circular dependency or complex prop drilling
|
shouldPlaySound,
|
||||||
unlockPlayback,
|
unlockPlayback,
|
||||||
resolveSoundPlaybackUrl,
|
resolveSoundPlaybackUrl,
|
||||||
pushStatusLine,
|
pushStatusLine,
|
||||||
@@ -140,6 +147,12 @@ export const SpaceWorkspaceWidget = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const controls = useSpaceWorkspaceSessionControls({
|
const controls = useSpaceWorkspaceSessionControls({
|
||||||
|
workspaceMode,
|
||||||
|
setWorkspaceMode,
|
||||||
|
previewPlaybackState,
|
||||||
|
setPreviewPlaybackState,
|
||||||
|
pendingSessionEntryPoint,
|
||||||
|
setPendingSessionEntryPoint,
|
||||||
canStart: selection.canStart,
|
canStart: selection.canStart,
|
||||||
currentSession,
|
currentSession,
|
||||||
goalInput: selection.goalInput,
|
goalInput: selection.goalInput,
|
||||||
@@ -161,18 +174,6 @@ export const SpaceWorkspaceWidget = () => {
|
|||||||
setShowResumePrompt: selection.setShowResumePrompt,
|
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(() => {
|
useEffect(() => {
|
||||||
const preferMobile =
|
const preferMobile =
|
||||||
typeof window !== 'undefined' ? window.matchMedia('(max-width: 767px)').matches : false;
|
typeof window !== 'undefined' ? window.matchMedia('(max-width: 767px)').matches : false;
|
||||||
@@ -194,7 +195,7 @@ export const SpaceWorkspaceWidget = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SpaceSetupDrawerWidget
|
<SpaceSetupDrawerWidget
|
||||||
open={!controls.isFocusMode}
|
open={!isFocusMode}
|
||||||
scenes={selection.setupScenes}
|
scenes={selection.setupScenes}
|
||||||
sceneAssetMap={sceneAssetMap}
|
sceneAssetMap={sceneAssetMap}
|
||||||
selectedSceneId={selection.selectedScene.id}
|
selectedSceneId={selection.selectedScene.id}
|
||||||
@@ -236,9 +237,9 @@ export const SpaceWorkspaceWidget = () => {
|
|||||||
goal={selection.goalInput.trim()}
|
goal={selection.goalInput.trim()}
|
||||||
timerLabel={selection.selectedTimerLabel}
|
timerLabel={selection.selectedTimerLabel}
|
||||||
timeDisplay={resolvedTimeDisplay}
|
timeDisplay={resolvedTimeDisplay}
|
||||||
visible={controls.isFocusMode}
|
visible={isFocusMode}
|
||||||
hasActiveSession={Boolean(currentSession)}
|
hasActiveSession={Boolean(currentSession)}
|
||||||
playbackState={controls.resolvedPlaybackState}
|
playbackState={resolvedPlaybackState}
|
||||||
sessionPhase={phase ?? 'focus'}
|
sessionPhase={phase ?? 'focus'}
|
||||||
isSessionActionPending={isSessionMutating}
|
isSessionActionPending={isSessionMutating}
|
||||||
canStartSession={controls.canStartSession}
|
canStartSession={controls.canStartSession}
|
||||||
@@ -253,19 +254,19 @@ export const SpaceWorkspaceWidget = () => {
|
|||||||
onRestartRequested={() => {
|
onRestartRequested={() => {
|
||||||
void controls.handleRestartRequested();
|
void controls.handleRestartRequested();
|
||||||
}}
|
}}
|
||||||
onStatusMessage={focusPushStatusLine}
|
onStatusMessage={pushStatusLine}
|
||||||
onGoalUpdate={controls.handleGoalAdvance}
|
onGoalUpdate={controls.handleGoalAdvance}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FocusTopToast
|
<FocusTopToast
|
||||||
visible={controls.isFocusMode && Boolean(focusActiveStatus)}
|
visible={isFocusMode && Boolean(activeStatus)}
|
||||||
message={focusActiveStatus?.message ?? ''}
|
message={activeStatus?.message ?? ''}
|
||||||
actionLabel={focusActiveStatus?.action?.label}
|
actionLabel={activeStatus?.action?.label}
|
||||||
onAction={focusRunActiveAction}
|
onAction={runActiveAction}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SpaceToolsDockWidget
|
<SpaceToolsDockWidget
|
||||||
isFocusMode={controls.isFocusMode}
|
isFocusMode={isFocusMode}
|
||||||
scenes={selection.setupScenes}
|
scenes={selection.setupScenes}
|
||||||
sceneAssetMap={sceneAssetMap}
|
sceneAssetMap={sceneAssetMap}
|
||||||
selectedSceneId={selection.selectedScene.id}
|
selectedSceneId={selection.selectedScene.id}
|
||||||
@@ -289,7 +290,7 @@ export const SpaceWorkspaceWidget = () => {
|
|||||||
onRestoreThought={restoreThought}
|
onRestoreThought={restoreThought}
|
||||||
onRestoreThoughts={restoreThoughts}
|
onRestoreThoughts={restoreThoughts}
|
||||||
onClearInbox={clearThoughts}
|
onClearInbox={clearThoughts}
|
||||||
onStatusMessage={focusPushStatusLine}
|
onStatusMessage={pushStatusLine}
|
||||||
onExitRequested={() => {
|
onExitRequested={() => {
|
||||||
void controls.handleExitRequested();
|
void controls.handleExitRequested();
|
||||||
}}
|
}}
|
||||||
|
|||||||
Reference in New Issue
Block a user