feat(core-loop): /app 진입과 /space 복구 흐름 구현

This commit is contained in:
2026-03-14 18:02:50 +09:00
parent bc08a049b6
commit b4ed94cf1b
19 changed files with 2638 additions and 619 deletions

View File

@@ -5,7 +5,9 @@ import { SpaceTimerHudWidget } from '@/widgets/space-timer-hud';
import { GoalCompleteSheet } from './GoalCompleteSheet';
import { IntentCapsule } from './IntentCapsule';
import { NextMicroStepPrompt } from './NextMicroStepPrompt';
import { PauseRefocusPrompt } from './PauseRefocusPrompt';
import { RefocusSheet } from './RefocusSheet';
import { ReturnPrompt } from './ReturnPrompt';
interface SpaceFocusHudWidgetProps {
goal: string;
@@ -18,11 +20,14 @@ interface SpaceFocusHudWidgetProps {
canStartSession?: boolean;
canPauseSession?: boolean;
canRestartSession?: boolean;
returnPromptMode?: 'focus' | 'break' | null;
onStartRequested?: () => void;
onPauseRequested?: () => void;
onRestartRequested?: () => void;
onDismissReturnPrompt?: () => void;
onIntentUpdate: (payload: { goal?: string; microStep?: string | null }) => boolean | Promise<boolean>;
onGoalUpdate: (nextGoal: string) => boolean | Promise<boolean>;
onGoalFinish: () => boolean | Promise<boolean>;
onStatusMessage: (payload: HudStatusLinePayload) => void;
}
@@ -37,16 +42,19 @@ export const SpaceFocusHudWidget = ({
canStartSession = false,
canPauseSession = false,
canRestartSession = false,
returnPromptMode = null,
onStartRequested,
onPauseRequested,
onRestartRequested,
onDismissReturnPrompt,
onIntentUpdate,
onGoalUpdate,
onGoalFinish,
onStatusMessage,
}: SpaceFocusHudWidgetProps) => {
const [sheetOpen, setSheetOpen] = useState(false);
const [isRefocusOpen, setRefocusOpen] = useState(false);
const [isMicroStepPromptOpen, setMicroStepPromptOpen] = useState(false);
const [overlay, setOverlay] = useState<'none' | 'paused' | 'return' | 'refocus' | 'next-beat' | 'complete'>('none');
const [refocusOrigin, setRefocusOrigin] = useState<'manual' | 'pause' | 'next-beat' | 'return'>('manual');
const [completePreferredView, setCompletePreferredView] = useState<'choice' | 'next'>('choice');
const [draftGoal, setDraftGoal] = useState('');
const [draftMicroStep, setDraftMicroStep] = useState('');
const [autoFocusField, setAutoFocusField] = useState<'goal' | 'microStep'>('goal');
@@ -58,7 +66,12 @@ export const SpaceFocusHudWidget = ({
const restReminderTimerRef = useRef<number | null>(null);
const normalizedGoal = goal.trim().length > 0 ? goal.trim() : copy.space.focusHud.goalFallback;
const normalizedMicroStep = microStep?.trim() ? microStep.trim() : null;
const isIntentOverlayOpen = isRefocusOpen || isMicroStepPromptOpen || sheetOpen;
const isPausedPromptOpen = overlay === 'paused';
const isReturnPromptOpen = overlay === 'return';
const isRefocusOpen = overlay === 'refocus';
const isMicroStepPromptOpen = overlay === 'next-beat';
const isCompleteOpen = overlay === 'complete';
const isIntentOverlayOpen = overlay !== 'none';
useEffect(() => {
return () => {
@@ -69,6 +82,32 @@ export const SpaceFocusHudWidget = ({
};
}, []);
useEffect(() => {
if (!hasActiveSession) {
setOverlay('none');
setIntentError(null);
setSavingIntent(false);
setRefocusOrigin('manual');
setCompletePreferredView('choice');
}
}, [hasActiveSession]);
useEffect(() => {
if (!returnPromptMode) {
if (overlay === 'return') {
setOverlay('none');
}
return;
}
if (overlay === 'complete') {
return;
}
setIntentError(null);
setOverlay('return');
}, [overlay, returnPromptMode]);
useEffect(() => {
if (!visibleRef.current && playbackState === 'running') {
onStatusMessage({
@@ -89,13 +128,16 @@ export const SpaceFocusHudWidget = ({
resumePlaybackStateRef.current = playbackState;
}, [normalizedGoal, onStatusMessage, playbackState]);
const openRefocus = useCallback((field: 'goal' | 'microStep' = 'goal') => {
const openRefocus = useCallback((
field: 'goal' | 'microStep' = 'goal',
origin: 'manual' | 'pause' | 'next-beat' | 'return' = 'manual',
) => {
setDraftGoal(goal.trim());
setDraftMicroStep(normalizedMicroStep ?? '');
setAutoFocusField(field);
setIntentError(null);
setMicroStepPromptOpen(false);
setRefocusOpen(true);
setRefocusOrigin(origin);
setOverlay('refocus');
}, [goal, normalizedMicroStep]);
useEffect(() => {
@@ -103,31 +145,42 @@ export const SpaceFocusHudWidget = ({
pausePlaybackStateRef.current === 'running' &&
playbackState === 'paused' &&
hasActiveSession &&
!isRefocusOpen &&
!sheetOpen
overlay === 'none'
) {
openRefocus('microStep');
setIntentError(null);
setOverlay('paused');
onStatusMessage({
message: copy.space.focusHud.refocusOpenOnPause,
});
}
pausePlaybackStateRef.current = playbackState;
}, [hasActiveSession, isRefocusOpen, onStatusMessage, openRefocus, playbackState, sheetOpen]);
}, [hasActiveSession, onStatusMessage, overlay, playbackState]);
useEffect(() => {
if (normalizedMicroStep) {
return;
if (playbackState === 'running' && overlay === 'paused') {
setOverlay('none');
}
}, [overlay, playbackState]);
setMicroStepPromptOpen(false);
}, [normalizedMicroStep]);
useEffect(() => {
if (!normalizedMicroStep && overlay === 'next-beat') {
setOverlay('none');
}
}, [normalizedMicroStep, overlay]);
const handleOpenCompleteSheet = () => {
const handleOpenCompleteSheet = (preferredView: 'choice' | 'next' = 'choice') => {
setIntentError(null);
setRefocusOpen(false);
setMicroStepPromptOpen(false);
setSheetOpen(true);
setCompletePreferredView(preferredView);
setOverlay('complete');
};
const handleDismissReturnPrompt = () => {
onDismissReturnPrompt?.();
if (overlay === 'return') {
setOverlay('none');
}
};
const handleRefocusSubmit = async () => {
@@ -151,10 +204,18 @@ export const SpaceFocusHudWidget = ({
return;
}
setRefocusOpen(false);
setOverlay('none');
onStatusMessage({
message: copy.space.focusHud.refocusSaved,
});
if (refocusOrigin === 'return') {
onDismissReturnPrompt?.();
}
if (refocusOrigin === 'pause' && playbackState === 'paused') {
onStartRequested?.();
}
} finally {
setSavingIntent(false);
}
@@ -178,7 +239,7 @@ export const SpaceFocusHudWidget = ({
return;
}
setMicroStepPromptOpen(false);
setOverlay('none');
onStatusMessage({
message: copy.space.focusHud.microStepCleared,
});
@@ -192,37 +253,70 @@ export const SpaceFocusHudWidget = ({
setDraftMicroStep('');
setAutoFocusField('microStep');
setIntentError(null);
setMicroStepPromptOpen(false);
setRefocusOpen(true);
setRefocusOrigin('next-beat');
setOverlay('refocus');
};
return (
<>
<div className="pointer-events-none fixed left-6 top-6 z-20 w-[min(26rem,calc(100vw-3rem))] md:left-10 md:top-9">
<div className="pointer-events-none fixed left-6 top-6 z-20 w-[min(29rem,calc(100vw-3rem))] md:left-10 md:top-9">
<IntentCapsule
goal={normalizedGoal}
microStep={microStep}
canRefocus={Boolean(hasActiveSession)}
canComplete={hasActiveSession && sessionPhase === 'focus'}
showActions={!isIntentOverlayOpen}
onOpenRefocus={() => openRefocus()}
onOpenRefocus={() => openRefocus('goal', 'manual')}
onMicroStepDone={() => {
if (!normalizedMicroStep) {
openRefocus('microStep');
openRefocus('microStep', 'next-beat');
return;
}
setIntentError(null);
setRefocusOpen(false);
setMicroStepPromptOpen(true);
setOverlay('next-beat');
}}
onGoalCompleteRequest={handleOpenCompleteSheet}
/>
<ReturnPrompt
open={isReturnPromptOpen && Boolean(returnPromptMode)}
mode={returnPromptMode === 'break' ? 'break' : 'focus'}
isBusy={isSavingIntent}
onContinue={() => {
handleDismissReturnPrompt();
}}
onRefocus={() => {
handleDismissReturnPrompt();
openRefocus('microStep', 'return');
}}
onRest={() => {
handleDismissReturnPrompt();
onStatusMessage({ message: copy.space.focusHud.restReminder });
}}
onNextGoal={() => {
handleDismissReturnPrompt();
handleOpenCompleteSheet('next');
}}
/>
<PauseRefocusPrompt
open={isPausedPromptOpen}
isBusy={isSavingIntent}
onRefocus={() => openRefocus('microStep', 'pause')}
onKeepCurrent={() => {
setOverlay('none');
onStartRequested?.();
}}
/>
<RefocusSheet
open={isRefocusOpen}
goalDraft={draftGoal}
microStepDraft={draftMicroStep}
autoFocusField={autoFocusField}
submitLabel={
refocusOrigin === 'pause' && playbackState === 'paused'
? copy.space.focusHud.refocusApplyAndResume
: copy.space.focusHud.refocusApply
}
isSaving={isSavingIntent}
error={intentError}
onGoalChange={setDraftGoal}
@@ -233,7 +327,7 @@ export const SpaceFocusHudWidget = ({
}
setIntentError(null);
setRefocusOpen(false);
setOverlay('none');
}}
onSubmit={() => {
void handleRefocusSubmit();
@@ -241,6 +335,7 @@ export const SpaceFocusHudWidget = ({
/>
<NextMicroStepPrompt
open={isMicroStepPromptOpen}
goal={normalizedGoal}
isSubmitting={isSavingIntent}
error={intentError}
onKeepGoalOnly={() => {
@@ -249,11 +344,13 @@ export const SpaceFocusHudWidget = ({
onDefineNext={handleDefineNextMicroStep}
/>
<GoalCompleteSheet
open={sheetOpen}
open={isCompleteOpen}
currentGoal={goal}
onClose={() => setSheetOpen(false)}
preferredView={completePreferredView}
onClose={() => setOverlay('none')}
onFinish={() => Promise.resolve(onGoalFinish())}
onRest={() => {
setSheetOpen(false);
setOverlay('none');
if (restReminderTimerRef.current) {
window.clearTimeout(restReminderTimerRef.current);