fix(space): HUD 시작 흐름과 컨트롤 상태를 정리
This commit is contained in:
@@ -8,11 +8,15 @@ interface SpaceFocusHudWidgetProps {
|
|||||||
timerLabel: string;
|
timerLabel: string;
|
||||||
timeDisplay?: string;
|
timeDisplay?: string;
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
|
hasActiveSession?: boolean;
|
||||||
playbackState?: 'running' | 'paused';
|
playbackState?: 'running' | 'paused';
|
||||||
sessionPhase?: 'focus' | 'break' | null;
|
sessionPhase?: 'focus' | 'break' | null;
|
||||||
isSessionActionPending?: boolean;
|
isSessionActionPending?: boolean;
|
||||||
|
canStartSession?: boolean;
|
||||||
|
canPauseSession?: boolean;
|
||||||
|
canRestartSession?: boolean;
|
||||||
|
onStartRequested?: () => void;
|
||||||
onPauseRequested?: () => void;
|
onPauseRequested?: () => void;
|
||||||
onResumeRequested?: () => void;
|
|
||||||
onRestartRequested?: () => void;
|
onRestartRequested?: () => void;
|
||||||
onGoalUpdate: (nextGoal: string) => void | Promise<void>;
|
onGoalUpdate: (nextGoal: string) => void | Promise<void>;
|
||||||
onStatusMessage: (payload: HudStatusLinePayload) => void;
|
onStatusMessage: (payload: HudStatusLinePayload) => void;
|
||||||
@@ -23,11 +27,15 @@ export const SpaceFocusHudWidget = ({
|
|||||||
timerLabel,
|
timerLabel,
|
||||||
timeDisplay,
|
timeDisplay,
|
||||||
visible,
|
visible,
|
||||||
playbackState = 'running',
|
hasActiveSession = false,
|
||||||
|
playbackState = 'paused',
|
||||||
sessionPhase = 'focus',
|
sessionPhase = 'focus',
|
||||||
isSessionActionPending = false,
|
isSessionActionPending = false,
|
||||||
|
canStartSession = false,
|
||||||
|
canPauseSession = false,
|
||||||
|
canRestartSession = false,
|
||||||
|
onStartRequested,
|
||||||
onPauseRequested,
|
onPauseRequested,
|
||||||
onResumeRequested,
|
|
||||||
onRestartRequested,
|
onRestartRequested,
|
||||||
onGoalUpdate,
|
onGoalUpdate,
|
||||||
onStatusMessage,
|
onStatusMessage,
|
||||||
@@ -48,14 +56,14 @@ export const SpaceFocusHudWidget = ({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (visible && !visibleRef.current) {
|
if (visible && !visibleRef.current && playbackState === 'running') {
|
||||||
onStatusMessage({
|
onStatusMessage({
|
||||||
message: `이번 한 조각 · ${normalizedGoal}`,
|
message: `이번 한 조각 · ${normalizedGoal}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
visibleRef.current = visible;
|
visibleRef.current = visible;
|
||||||
}, [normalizedGoal, onStatusMessage, visible]);
|
}, [normalizedGoal, onStatusMessage, playbackState, visible]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (playbackStateRef.current === 'paused' && playbackState === 'running' && visible) {
|
if (playbackStateRef.current === 'paused' && playbackState === 'running' && visible) {
|
||||||
@@ -82,12 +90,16 @@ export const SpaceFocusHudWidget = ({
|
|||||||
goal={goal}
|
goal={goal}
|
||||||
timeDisplay={timeDisplay}
|
timeDisplay={timeDisplay}
|
||||||
isImmersionMode
|
isImmersionMode
|
||||||
|
hasActiveSession={hasActiveSession}
|
||||||
sessionPhase={sessionPhase}
|
sessionPhase={sessionPhase}
|
||||||
playbackState={playbackState}
|
playbackState={playbackState}
|
||||||
isControlsDisabled={isSessionActionPending}
|
isControlsDisabled={isSessionActionPending}
|
||||||
|
canStart={canStartSession}
|
||||||
|
canPause={canPauseSession}
|
||||||
|
canReset={canRestartSession}
|
||||||
className="pr-[4.2rem]"
|
className="pr-[4.2rem]"
|
||||||
onGoalCompleteRequest={handleOpenCompleteSheet}
|
onGoalCompleteRequest={handleOpenCompleteSheet}
|
||||||
onStartClick={onResumeRequested}
|
onStartClick={onStartRequested}
|
||||||
onPauseClick={onPauseRequested}
|
onPauseClick={onPauseRequested}
|
||||||
onResetClick={onRestartRequested}
|
onResetClick={onRestartRequested}
|
||||||
/>
|
/>
|
||||||
@@ -110,9 +122,6 @@ export const SpaceFocusHudWidget = ({
|
|||||||
onConfirm={(nextGoal) => {
|
onConfirm={(nextGoal) => {
|
||||||
void onGoalUpdate(nextGoal);
|
void onGoalUpdate(nextGoal);
|
||||||
setSheetOpen(false);
|
setSheetOpen(false);
|
||||||
onStatusMessage({
|
|
||||||
message: `이번 한 조각 · ${nextGoal}`,
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ export const SpaceSetupDrawerWidget = ({
|
|||||||
<header className="mb-3 space-y-1">
|
<header className="mb-3 space-y-1">
|
||||||
<p className="text-[10px] uppercase tracking-[0.18em] text-white/48">Ritual</p>
|
<p className="text-[10px] uppercase tracking-[0.18em] text-white/48">Ritual</p>
|
||||||
<h1 className="text-[1.45rem] font-semibold leading-tight text-white">이번 한 조각을 정하고 시작해요.</h1>
|
<h1 className="text-[1.45rem] font-semibold leading-tight text-white">이번 한 조각을 정하고 시작해요.</h1>
|
||||||
<p className="text-xs text-white/60">목표만 적으면 바로 Focus 모드로 넘어가요.</p>
|
<p className="text-xs text-white/60">목표를 정한 뒤 HUD의 시작 버튼으로 실제 세션을 시작해요.</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{resumeHint ? (
|
{resumeHint ? (
|
||||||
@@ -174,7 +174,7 @@ export const SpaceSetupDrawerWidget = ({
|
|||||||
onClick={resumeHint.onResume}
|
onClick={resumeHint.onResume}
|
||||||
className="rounded-full border border-sky-200/34 bg-sky-200/14 px-2.5 py-1 text-[11px] text-white/90 transition-colors hover:bg-sky-200/22"
|
className="rounded-full border border-sky-200/34 bg-sky-200/14 px-2.5 py-1 text-[11px] text-white/90 transition-colors hover:bg-sky-200/22"
|
||||||
>
|
>
|
||||||
이어서 시작
|
이어서 준비
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -295,7 +295,7 @@ export const SpaceSetupDrawerWidget = ({
|
|||||||
'h-10 rounded-xl !bg-sky-300/84 !text-slate-900 shadow-[0_8px_16px_rgba(125,211,252,0.24)] hover:!bg-sky-300 disabled:!bg-white/10 disabled:!text-white/42',
|
'h-10 rounded-xl !bg-sky-300/84 !text-slate-900 shadow-[0_8px_16px_rgba(125,211,252,0.24)] hover:!bg-sky-300 disabled:!bg-white/10 disabled:!text-white/42',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
시작하기
|
집중 화면 열기
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -12,10 +12,14 @@ interface SpaceTimerHudWidgetProps {
|
|||||||
goal: string;
|
goal: string;
|
||||||
timeDisplay?: string;
|
timeDisplay?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
hasActiveSession?: boolean;
|
||||||
sessionPhase?: 'focus' | 'break' | null;
|
sessionPhase?: 'focus' | 'break' | null;
|
||||||
playbackState?: 'running' | 'paused' | null;
|
playbackState?: 'running' | 'paused' | null;
|
||||||
isControlsDisabled?: boolean;
|
isControlsDisabled?: boolean;
|
||||||
isImmersionMode?: boolean;
|
isImmersionMode?: boolean;
|
||||||
|
canStart?: boolean;
|
||||||
|
canPause?: boolean;
|
||||||
|
canReset?: boolean;
|
||||||
onStartClick?: () => void;
|
onStartClick?: () => void;
|
||||||
onPauseClick?: () => void;
|
onPauseClick?: () => void;
|
||||||
onResetClick?: () => void;
|
onResetClick?: () => void;
|
||||||
@@ -33,10 +37,14 @@ export const SpaceTimerHudWidget = ({
|
|||||||
goal,
|
goal,
|
||||||
timeDisplay = '25:00',
|
timeDisplay = '25:00',
|
||||||
className,
|
className,
|
||||||
|
hasActiveSession = false,
|
||||||
sessionPhase = 'focus',
|
sessionPhase = 'focus',
|
||||||
playbackState = 'running',
|
playbackState = 'paused',
|
||||||
isControlsDisabled = false,
|
isControlsDisabled = false,
|
||||||
isImmersionMode = false,
|
isImmersionMode = false,
|
||||||
|
canStart = true,
|
||||||
|
canPause = false,
|
||||||
|
canReset = false,
|
||||||
onStartClick,
|
onStartClick,
|
||||||
onPauseClick,
|
onPauseClick,
|
||||||
onResetClick,
|
onResetClick,
|
||||||
@@ -46,6 +54,8 @@ export const SpaceTimerHudWidget = ({
|
|||||||
const normalizedGoal = goal.trim().length > 0 ? goal.trim() : '이번 한 조각을 설정해 주세요.';
|
const normalizedGoal = goal.trim().length > 0 ? goal.trim() : '이번 한 조각을 설정해 주세요.';
|
||||||
const modeLabel = isBreatheMode
|
const modeLabel = isBreatheMode
|
||||||
? RECOVERY_30S_MODE_LABEL
|
? RECOVERY_30S_MODE_LABEL
|
||||||
|
: !hasActiveSession
|
||||||
|
? 'Ready'
|
||||||
: sessionPhase === 'break'
|
: sessionPhase === 'break'
|
||||||
? 'Break'
|
? 'Break'
|
||||||
: 'Focus';
|
: 'Focus';
|
||||||
@@ -110,42 +120,59 @@ export const SpaceTimerHudWidget = ({
|
|||||||
|
|
||||||
<div className="flex items-center gap-2.5">
|
<div className="flex items-center gap-2.5">
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
{HUD_ACTIONS.map((action) => (
|
{HUD_ACTIONS.map((action) => {
|
||||||
|
const isStartAction = action.id === 'start';
|
||||||
|
const isPauseAction = action.id === 'pause';
|
||||||
|
const isResetAction = action.id === 'reset';
|
||||||
|
const isDisabled =
|
||||||
|
isControlsDisabled ||
|
||||||
|
(isStartAction ? !canStart : isPauseAction ? !canPause : !canReset);
|
||||||
|
const isHighlighted =
|
||||||
|
(isStartAction && playbackState !== 'running') ||
|
||||||
|
(isPauseAction && playbackState === 'running');
|
||||||
|
|
||||||
|
return (
|
||||||
<button
|
<button
|
||||||
key={action.id}
|
key={action.id}
|
||||||
type="button"
|
type="button"
|
||||||
title={action.label}
|
title={action.label}
|
||||||
disabled={isControlsDisabled}
|
aria-pressed={isHighlighted}
|
||||||
|
disabled={isDisabled}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (action.id === 'start') {
|
if (isStartAction) {
|
||||||
onStartClick?.();
|
onStartClick?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action.id === 'pause') {
|
if (isPauseAction) {
|
||||||
onPauseClick?.();
|
onPauseClick?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action.id === 'reset') {
|
if (isResetAction) {
|
||||||
onResetClick?.();
|
onResetClick?.();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
'inline-flex h-8 w-8 items-center justify-center rounded-full border text-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-200/80 disabled:cursor-not-allowed disabled:opacity-45',
|
'inline-flex h-9 w-9 items-center justify-center rounded-full border text-sm transition-[transform,background-color,border-color,box-shadow,color,opacity] duration-150 ease-out focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-200/80 active:translate-y-px active:scale-[0.95] disabled:cursor-not-allowed disabled:opacity-38 disabled:shadow-none',
|
||||||
|
'shadow-[inset_0_1px_0_rgba(255,255,255,0.08),0_8px_18px_rgba(2,6,23,0.18)]',
|
||||||
isImmersionMode
|
isImmersionMode
|
||||||
? 'border-white/14 bg-black/26 text-white/82 hover:bg-black/34'
|
? 'border-white/14 bg-black/28 text-white/82 hover:border-white/22 hover:bg-white/[0.09]'
|
||||||
: 'border-white/14 bg-black/26 text-white/84 hover:bg-black/34',
|
: 'border-white/14 bg-black/28 text-white/84 hover:border-white/22 hover:bg-white/[0.09]',
|
||||||
action.id === 'start' && playbackState === 'running'
|
isStartAction && isHighlighted
|
||||||
? 'border-sky-200/42 bg-sky-200/18 text-white'
|
? 'border-sky-200/56 bg-sky-200/20 text-sky-50 shadow-[inset_0_1px_0_rgba(255,255,255,0.12),0_10px_22px_rgba(56,189,248,0.24)]'
|
||||||
: '',
|
: '',
|
||||||
action.id === 'pause' && playbackState === 'paused'
|
isPauseAction && isHighlighted
|
||||||
? 'border-amber-200/42 bg-amber-200/16 text-white'
|
? 'border-amber-200/52 bg-amber-200/18 text-amber-50 shadow-[inset_0_1px_0_rgba(255,255,255,0.12),0_10px_22px_rgba(251,191,36,0.18)]'
|
||||||
|
: '',
|
||||||
|
isResetAction && !isDisabled
|
||||||
|
? 'hover:border-white/26 hover:bg-white/[0.12] hover:text-white'
|
||||||
: '',
|
: '',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span aria-hidden>{action.icon}</span>
|
<span aria-hidden>{action.icon}</span>
|
||||||
<span className="sr-only">{action.label}</span>
|
<span className="sr-only">{action.label}</span>
|
||||||
</button>
|
</button>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
<Restart30sAction
|
<Restart30sAction
|
||||||
onTrigger={triggerRestart}
|
onTrigger={triggerRestart}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useSearchParams } from 'next/navigation';
|
import { useSearchParams } from 'next/navigation';
|
||||||
import {
|
import {
|
||||||
getSceneBackgroundStyle,
|
getSceneBackgroundStyle,
|
||||||
@@ -24,6 +24,7 @@ import { SpaceToolsDockWidget } from '@/widgets/space-tools-dock';
|
|||||||
import { FocusTopToast } from './FocusTopToast';
|
import { FocusTopToast } from './FocusTopToast';
|
||||||
|
|
||||||
type WorkspaceMode = 'setup' | 'focus';
|
type WorkspaceMode = 'setup' | 'focus';
|
||||||
|
type SessionEntryPoint = 'space-setup' | 'goal-complete' | 'resume-restore';
|
||||||
type SelectionOverride = {
|
type SelectionOverride = {
|
||||||
sound: boolean;
|
sound: boolean;
|
||||||
timer: boolean;
|
timer: boolean;
|
||||||
@@ -188,11 +189,14 @@ export const SpaceWorkspaceWidget = () => {
|
|||||||
const [resumeGoal, setResumeGoal] = useState('');
|
const [resumeGoal, setResumeGoal] = useState('');
|
||||||
const [showResumePrompt, setShowResumePrompt] = useState(false);
|
const [showResumePrompt, setShowResumePrompt] = useState(false);
|
||||||
const [hasHydratedSelection, setHasHydratedSelection] = useState(false);
|
const [hasHydratedSelection, setHasHydratedSelection] = useState(false);
|
||||||
const [previewPlaybackState, setPreviewPlaybackState] = useState<'running' | 'paused'>('running');
|
const [previewPlaybackState, setPreviewPlaybackState] = useState<'running' | 'paused'>('paused');
|
||||||
|
const [pendingSessionEntryPoint, setPendingSessionEntryPoint] =
|
||||||
|
useState<SessionEntryPoint>('space-setup');
|
||||||
const [selectionOverride, setSelectionOverride] = useState<SelectionOverride>({
|
const [selectionOverride, setSelectionOverride] = useState<SelectionOverride>({
|
||||||
sound: false,
|
sound: false,
|
||||||
timer: false,
|
timer: false,
|
||||||
});
|
});
|
||||||
|
const queuedFocusStatusMessageRef = useRef<string | null>(null);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
selectedPresetId,
|
selectedPresetId,
|
||||||
@@ -235,6 +239,9 @@ export const SpaceWorkspaceWidget = () => {
|
|||||||
const { activeStatus, pushStatusLine, runActiveAction } = useHudStatusLine(isFocusMode);
|
const { activeStatus, pushStatusLine, runActiveAction } = useHudStatusLine(isFocusMode);
|
||||||
const resolvedPlaybackState = playbackState ?? previewPlaybackState;
|
const resolvedPlaybackState = playbackState ?? previewPlaybackState;
|
||||||
const resolvedTimeDisplay = timeDisplay ?? resolveFocusTimeDisplayFromTimerLabel(selectedTimerLabel);
|
const resolvedTimeDisplay = timeDisplay ?? resolveFocusTimeDisplayFromTimerLabel(selectedTimerLabel);
|
||||||
|
const canStartSession = canStart && (!currentSession || resolvedPlaybackState !== 'running');
|
||||||
|
const canPauseSession = Boolean(currentSession && resolvedPlaybackState === 'running');
|
||||||
|
const canRestartSession = Boolean(currentSession);
|
||||||
|
|
||||||
const applyRecommendedSelections = useCallback((
|
const applyRecommendedSelections = useCallback((
|
||||||
sceneId: string,
|
sceneId: string,
|
||||||
@@ -391,42 +398,75 @@ export const SpaceWorkspaceWidget = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const startFocusFlow = async (
|
const openFocusMode = (
|
||||||
nextGoal: string,
|
nextGoal: string,
|
||||||
entryPoint: 'space-setup' | 'goal-complete' | 'resume-restore' = 'space-setup',
|
entryPoint: SessionEntryPoint = 'space-setup',
|
||||||
) => {
|
) => {
|
||||||
const trimmedGoal = nextGoal.trim();
|
const trimmedGoal = nextGoal.trim();
|
||||||
|
|
||||||
|
if (!trimmedGoal) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setShowResumePrompt(false);
|
||||||
|
setPendingSessionEntryPoint(entryPoint);
|
||||||
|
setPreviewPlaybackState('paused');
|
||||||
|
setWorkspaceMode('focus');
|
||||||
|
queuedFocusStatusMessageRef.current = '준비 완료 · 시작 버튼을 눌러 집중을 시작해요.';
|
||||||
|
};
|
||||||
|
|
||||||
|
const startFocusFlow = async () => {
|
||||||
|
const trimmedGoal = goalInput.trim();
|
||||||
const timerPresetId = resolveTimerPresetIdFromLabel(selectedTimerLabel);
|
const timerPresetId = resolveTimerPresetIdFromLabel(selectedTimerLabel);
|
||||||
|
|
||||||
if (!trimmedGoal || !timerPresetId) {
|
if (!trimmedGoal || !timerPresetId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setShowResumePrompt(false);
|
|
||||||
setPreviewPlaybackState('running');
|
|
||||||
setWorkspaceMode('focus');
|
|
||||||
|
|
||||||
const startedSession = await startSession({
|
const startedSession = await startSession({
|
||||||
sceneId: selectedSceneId,
|
sceneId: selectedSceneId,
|
||||||
goal: trimmedGoal,
|
goal: trimmedGoal,
|
||||||
timerPresetId,
|
timerPresetId,
|
||||||
soundPresetId: selectedPresetId,
|
soundPresetId: selectedPresetId,
|
||||||
entryPoint,
|
entryPoint: pendingSessionEntryPoint,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!startedSession) {
|
if (startedSession) {
|
||||||
pushStatusLine({
|
setPreviewPlaybackState('running');
|
||||||
message: '세션 API 연결 실패 · 로컬 미리보기 모드로 계속해요.',
|
return;
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setPreviewPlaybackState('paused');
|
||||||
|
pushStatusLine({
|
||||||
|
message: '세션을 시작하지 못했어요. 잠시 후 다시 시도해 주세요.',
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleStart = () => {
|
const handleSetupFocusOpen = () => {
|
||||||
if (!canStart) {
|
if (!canStart) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
void startFocusFlow(goalInput, 'space-setup');
|
openFocusMode(goalInput, 'space-setup');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleStartRequested = async () => {
|
||||||
|
if (!canStartSession) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentSession) {
|
||||||
|
await startFocusFlow();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resumedSession = await resumeSession();
|
||||||
|
|
||||||
|
if (!resumedSession) {
|
||||||
|
pushStatusLine({
|
||||||
|
message: '세션을 다시 시작하지 못했어요.',
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleExitRequested = async () => {
|
const handleExitRequested = async () => {
|
||||||
@@ -439,7 +479,8 @@ export const SpaceWorkspaceWidget = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setPreviewPlaybackState('running');
|
setPreviewPlaybackState('paused');
|
||||||
|
setPendingSessionEntryPoint('space-setup');
|
||||||
setWorkspaceMode('setup');
|
setWorkspaceMode('setup');
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -458,26 +499,8 @@ export const SpaceWorkspaceWidget = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleResumeRequested = async () => {
|
|
||||||
if (!currentSession) {
|
|
||||||
setPreviewPlaybackState('running');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const resumedSession = await resumeSession();
|
|
||||||
|
|
||||||
if (!resumedSession) {
|
|
||||||
pushStatusLine({
|
|
||||||
message: '세션을 다시 시작하지 못했어요.',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRestartRequested = async () => {
|
const handleRestartRequested = async () => {
|
||||||
if (!currentSession) {
|
if (!currentSession) {
|
||||||
pushStatusLine({
|
|
||||||
message: '실제 세션이 시작된 뒤에만 다시 시작할 수 있어요.',
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -512,12 +535,17 @@ export const SpaceWorkspaceWidget = () => {
|
|||||||
pushStatusLine({
|
pushStatusLine({
|
||||||
message: '현재 세션 완료를 서버에 반영하지 못했어요.',
|
message: '현재 세션 완료를 서버에 반영하지 못했어요.',
|
||||||
});
|
});
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setGoalInput(trimmedNextGoal);
|
setGoalInput(trimmedNextGoal);
|
||||||
setSelectedGoalId(null);
|
setSelectedGoalId(null);
|
||||||
void startFocusFlow(trimmedNextGoal, 'goal-complete');
|
setPendingSessionEntryPoint('goal-complete');
|
||||||
|
setPreviewPlaybackState('paused');
|
||||||
|
pushStatusLine({
|
||||||
|
message: '다음 한 조각 준비 완료 · 시작 버튼을 눌러 이어가요.',
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -561,6 +589,18 @@ export const SpaceWorkspaceWidget = () => {
|
|||||||
);
|
);
|
||||||
}, [goalInput, hasHydratedSelection, resumeGoal, selectedSceneId, selectedTimerLabel, selectedPresetId, selectionOverride, showResumePrompt]);
|
}, [goalInput, hasHydratedSelection, resumeGoal, selectedSceneId, selectedTimerLabel, selectedPresetId, selectionOverride, showResumePrompt]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isFocusMode || !queuedFocusStatusMessageRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = queuedFocusStatusMessageRef.current;
|
||||||
|
queuedFocusStatusMessageRef.current = null;
|
||||||
|
pushStatusLine({
|
||||||
|
message,
|
||||||
|
});
|
||||||
|
}, [isFocusMode, pushStatusLine]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative h-dvh overflow-hidden text-white">
|
<div className="relative h-dvh overflow-hidden text-white">
|
||||||
<div
|
<div
|
||||||
@@ -590,7 +630,7 @@ export const SpaceWorkspaceWidget = () => {
|
|||||||
onSoundSelect={(presetId) => handleSelectSound(presetId, true)}
|
onSoundSelect={(presetId) => handleSelectSound(presetId, true)}
|
||||||
onGoalChange={handleGoalChange}
|
onGoalChange={handleGoalChange}
|
||||||
onGoalChipSelect={handleGoalChipSelect}
|
onGoalChipSelect={handleGoalChipSelect}
|
||||||
onStart={handleStart}
|
onStart={handleSetupFocusOpen}
|
||||||
resumeHint={
|
resumeHint={
|
||||||
showResumePrompt && resumeGoal
|
showResumePrompt && resumeGoal
|
||||||
? {
|
? {
|
||||||
@@ -599,7 +639,7 @@ export const SpaceWorkspaceWidget = () => {
|
|||||||
setGoalInput(resumeGoal);
|
setGoalInput(resumeGoal);
|
||||||
setSelectedGoalId(null);
|
setSelectedGoalId(null);
|
||||||
setShowResumePrompt(false);
|
setShowResumePrompt(false);
|
||||||
void startFocusFlow(resumeGoal, 'resume-restore');
|
openFocusMode(resumeGoal, 'resume-restore');
|
||||||
},
|
},
|
||||||
onStartFresh: () => {
|
onStartFresh: () => {
|
||||||
setGoalInput('');
|
setGoalInput('');
|
||||||
@@ -616,15 +656,19 @@ export const SpaceWorkspaceWidget = () => {
|
|||||||
timerLabel={selectedTimerLabel}
|
timerLabel={selectedTimerLabel}
|
||||||
timeDisplay={resolvedTimeDisplay}
|
timeDisplay={resolvedTimeDisplay}
|
||||||
visible={isFocusMode}
|
visible={isFocusMode}
|
||||||
|
hasActiveSession={Boolean(currentSession)}
|
||||||
playbackState={resolvedPlaybackState}
|
playbackState={resolvedPlaybackState}
|
||||||
sessionPhase={phase ?? 'focus'}
|
sessionPhase={phase ?? 'focus'}
|
||||||
isSessionActionPending={isSessionMutating}
|
isSessionActionPending={isSessionMutating}
|
||||||
|
canStartSession={canStartSession}
|
||||||
|
canPauseSession={canPauseSession}
|
||||||
|
canRestartSession={canRestartSession}
|
||||||
|
onStartRequested={() => {
|
||||||
|
void handleStartRequested();
|
||||||
|
}}
|
||||||
onPauseRequested={() => {
|
onPauseRequested={() => {
|
||||||
void handlePauseRequested();
|
void handlePauseRequested();
|
||||||
}}
|
}}
|
||||||
onResumeRequested={() => {
|
|
||||||
void handleResumeRequested();
|
|
||||||
}}
|
|
||||||
onRestartRequested={() => {
|
onRestartRequested={() => {
|
||||||
void handleRestartRequested();
|
void handleRestartRequested();
|
||||||
}}
|
}}
|
||||||
|
|||||||
Reference in New Issue
Block a user