fix(space): 정리된 intent hud와 리뷰 반영
This commit is contained in:
@@ -1,16 +1,16 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { copy } from '@/shared/i18n';
|
||||
import type { HudStatusLinePayload } from '@/shared/lib/useHudStatusLine';
|
||||
import { SpaceTimerHudWidget } from '@/widgets/space-timer-hud';
|
||||
import { FloatingGoalWidget } from './FloatingGoalWidget';
|
||||
import { GoalCompleteSheet } from './GoalCompleteSheet';
|
||||
import { IntentCapsule } from './IntentCapsule';
|
||||
import { NextMicroStepPrompt } from './NextMicroStepPrompt';
|
||||
import { RefocusSheet } from './RefocusSheet';
|
||||
|
||||
interface SpaceFocusHudWidgetProps {
|
||||
goal: string;
|
||||
microStep?: string | null;
|
||||
timerLabel: string;
|
||||
timeDisplay?: string;
|
||||
visible: boolean;
|
||||
hasActiveSession?: boolean;
|
||||
playbackState?: 'running' | 'paused';
|
||||
sessionPhase?: 'focus' | 'break' | null;
|
||||
@@ -21,7 +21,7 @@ interface SpaceFocusHudWidgetProps {
|
||||
onStartRequested?: () => void;
|
||||
onPauseRequested?: () => void;
|
||||
onRestartRequested?: () => void;
|
||||
onExitRequested?: () => void;
|
||||
onIntentUpdate: (payload: { goal?: string; microStep?: string | null }) => boolean | Promise<boolean>;
|
||||
onGoalUpdate: (nextGoal: string) => boolean | Promise<boolean>;
|
||||
onStatusMessage: (payload: HudStatusLinePayload) => void;
|
||||
}
|
||||
@@ -29,9 +29,7 @@ interface SpaceFocusHudWidgetProps {
|
||||
export const SpaceFocusHudWidget = ({
|
||||
goal,
|
||||
microStep,
|
||||
timerLabel,
|
||||
timeDisplay,
|
||||
visible,
|
||||
hasActiveSession = false,
|
||||
playbackState = 'paused',
|
||||
sessionPhase = 'focus',
|
||||
@@ -42,15 +40,25 @@ export const SpaceFocusHudWidget = ({
|
||||
onStartRequested,
|
||||
onPauseRequested,
|
||||
onRestartRequested,
|
||||
onExitRequested,
|
||||
onIntentUpdate,
|
||||
onGoalUpdate,
|
||||
onStatusMessage,
|
||||
}: SpaceFocusHudWidgetProps) => {
|
||||
const [sheetOpen, setSheetOpen] = useState(false);
|
||||
const [isRefocusOpen, setRefocusOpen] = useState(false);
|
||||
const [isMicroStepPromptOpen, setMicroStepPromptOpen] = useState(false);
|
||||
const [draftGoal, setDraftGoal] = useState('');
|
||||
const [draftMicroStep, setDraftMicroStep] = useState('');
|
||||
const [autoFocusField, setAutoFocusField] = useState<'goal' | 'microStep'>('goal');
|
||||
const [isSavingIntent, setSavingIntent] = useState(false);
|
||||
const [intentError, setIntentError] = useState<string | null>(null);
|
||||
const visibleRef = useRef(false);
|
||||
const playbackStateRef = useRef<'running' | 'paused'>(playbackState);
|
||||
const resumePlaybackStateRef = useRef<'running' | 'paused'>(playbackState);
|
||||
const pausePlaybackStateRef = useRef<'running' | 'paused'>(playbackState);
|
||||
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;
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
@@ -62,44 +70,206 @@ export const SpaceFocusHudWidget = ({
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (visible && !visibleRef.current && playbackState === 'running') {
|
||||
if (!visibleRef.current && playbackState === 'running') {
|
||||
onStatusMessage({
|
||||
message: copy.space.focusHud.goalToast(normalizedGoal),
|
||||
});
|
||||
}
|
||||
|
||||
visibleRef.current = visible;
|
||||
}, [normalizedGoal, onStatusMessage, playbackState, visible]);
|
||||
visibleRef.current = true;
|
||||
}, [normalizedGoal, onStatusMessage, playbackState]);
|
||||
|
||||
useEffect(() => {
|
||||
if (playbackStateRef.current === 'paused' && playbackState === 'running' && visible) {
|
||||
if (resumePlaybackStateRef.current === 'paused' && playbackState === 'running') {
|
||||
onStatusMessage({
|
||||
message: copy.space.focusHud.goalToast(normalizedGoal),
|
||||
});
|
||||
}
|
||||
|
||||
playbackStateRef.current = playbackState;
|
||||
}, [normalizedGoal, onStatusMessage, playbackState, visible]);
|
||||
resumePlaybackStateRef.current = playbackState;
|
||||
}, [normalizedGoal, onStatusMessage, playbackState]);
|
||||
|
||||
if (!visible) {
|
||||
return null;
|
||||
}
|
||||
const openRefocus = useCallback((field: 'goal' | 'microStep' = 'goal') => {
|
||||
setDraftGoal(goal.trim());
|
||||
setDraftMicroStep(normalizedMicroStep ?? '');
|
||||
setAutoFocusField(field);
|
||||
setIntentError(null);
|
||||
setMicroStepPromptOpen(false);
|
||||
setRefocusOpen(true);
|
||||
}, [goal, normalizedMicroStep]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
pausePlaybackStateRef.current === 'running' &&
|
||||
playbackState === 'paused' &&
|
||||
hasActiveSession &&
|
||||
!isRefocusOpen &&
|
||||
!sheetOpen
|
||||
) {
|
||||
openRefocus('microStep');
|
||||
onStatusMessage({
|
||||
message: copy.space.focusHud.refocusOpenOnPause,
|
||||
});
|
||||
}
|
||||
|
||||
pausePlaybackStateRef.current = playbackState;
|
||||
}, [hasActiveSession, isRefocusOpen, onStatusMessage, openRefocus, playbackState, sheetOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (normalizedMicroStep) {
|
||||
return;
|
||||
}
|
||||
|
||||
setMicroStepPromptOpen(false);
|
||||
}, [normalizedMicroStep]);
|
||||
|
||||
const handleOpenCompleteSheet = () => {
|
||||
setIntentError(null);
|
||||
setRefocusOpen(false);
|
||||
setMicroStepPromptOpen(false);
|
||||
setSheetOpen(true);
|
||||
};
|
||||
|
||||
const handleRefocusSubmit = async () => {
|
||||
const trimmedGoal = draftGoal.trim();
|
||||
|
||||
if (!trimmedGoal || isSavingIntent) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSavingIntent(true);
|
||||
setIntentError(null);
|
||||
|
||||
try {
|
||||
const didUpdate = await onIntentUpdate({
|
||||
goal: trimmedGoal,
|
||||
microStep: draftMicroStep.trim() || null,
|
||||
});
|
||||
|
||||
if (!didUpdate) {
|
||||
setIntentError(copy.space.workspace.intentSyncFailed);
|
||||
return;
|
||||
}
|
||||
|
||||
setRefocusOpen(false);
|
||||
onStatusMessage({
|
||||
message: copy.space.focusHud.refocusSaved,
|
||||
});
|
||||
} finally {
|
||||
setSavingIntent(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeepGoalOnly = async () => {
|
||||
if (isSavingIntent) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSavingIntent(true);
|
||||
setIntentError(null);
|
||||
|
||||
try {
|
||||
const didUpdate = await onIntentUpdate({
|
||||
microStep: null,
|
||||
});
|
||||
|
||||
if (!didUpdate) {
|
||||
setIntentError(copy.space.workspace.intentSyncFailed);
|
||||
return;
|
||||
}
|
||||
|
||||
setMicroStepPromptOpen(false);
|
||||
onStatusMessage({
|
||||
message: copy.space.focusHud.microStepCleared,
|
||||
});
|
||||
} finally {
|
||||
setSavingIntent(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDefineNextMicroStep = () => {
|
||||
setDraftGoal(goal.trim());
|
||||
setDraftMicroStep('');
|
||||
setAutoFocusField('microStep');
|
||||
setIntentError(null);
|
||||
setMicroStepPromptOpen(false);
|
||||
setRefocusOpen(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<FloatingGoalWidget
|
||||
goal={goal}
|
||||
microStep={microStep}
|
||||
onGoalCompleteRequest={handleOpenCompleteSheet}
|
||||
hasActiveSession={hasActiveSession}
|
||||
sessionPhase={sessionPhase}
|
||||
/>
|
||||
<div className="pointer-events-none fixed left-6 top-6 z-20 w-[min(26rem,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()}
|
||||
onMicroStepDone={() => {
|
||||
if (!normalizedMicroStep) {
|
||||
openRefocus('microStep');
|
||||
return;
|
||||
}
|
||||
|
||||
setIntentError(null);
|
||||
setRefocusOpen(false);
|
||||
setMicroStepPromptOpen(true);
|
||||
}}
|
||||
onGoalCompleteRequest={handleOpenCompleteSheet}
|
||||
/>
|
||||
<RefocusSheet
|
||||
open={isRefocusOpen}
|
||||
goalDraft={draftGoal}
|
||||
microStepDraft={draftMicroStep}
|
||||
autoFocusField={autoFocusField}
|
||||
isSaving={isSavingIntent}
|
||||
error={intentError}
|
||||
onGoalChange={setDraftGoal}
|
||||
onMicroStepChange={setDraftMicroStep}
|
||||
onClose={() => {
|
||||
if (isSavingIntent) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIntentError(null);
|
||||
setRefocusOpen(false);
|
||||
}}
|
||||
onSubmit={() => {
|
||||
void handleRefocusSubmit();
|
||||
}}
|
||||
/>
|
||||
<NextMicroStepPrompt
|
||||
open={isMicroStepPromptOpen}
|
||||
isSubmitting={isSavingIntent}
|
||||
error={intentError}
|
||||
onKeepGoalOnly={() => {
|
||||
void handleKeepGoalOnly();
|
||||
}}
|
||||
onDefineNext={handleDefineNextMicroStep}
|
||||
/>
|
||||
<GoalCompleteSheet
|
||||
open={sheetOpen}
|
||||
currentGoal={goal}
|
||||
onClose={() => setSheetOpen(false)}
|
||||
onRest={() => {
|
||||
setSheetOpen(false);
|
||||
|
||||
if (restReminderTimerRef.current) {
|
||||
window.clearTimeout(restReminderTimerRef.current);
|
||||
}
|
||||
|
||||
restReminderTimerRef.current = window.setTimeout(() => {
|
||||
onStatusMessage({ message: copy.space.focusHud.restReminder });
|
||||
restReminderTimerRef.current = null;
|
||||
}, 5 * 60 * 1000);
|
||||
}}
|
||||
onConfirm={(nextGoal) => {
|
||||
return Promise.resolve(onGoalUpdate(nextGoal));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<SpaceTimerHudWidget
|
||||
timerLabel={timerLabel}
|
||||
timeDisplay={timeDisplay}
|
||||
isImmersionMode
|
||||
hasActiveSession={hasActiveSession}
|
||||
@@ -114,26 +284,6 @@ export const SpaceFocusHudWidget = ({
|
||||
onPauseClick={onPauseRequested}
|
||||
onResetClick={onRestartRequested}
|
||||
/>
|
||||
<GoalCompleteSheet
|
||||
open={sheetOpen}
|
||||
currentGoal={goal}
|
||||
onClose={() => setSheetOpen(false)}
|
||||
onRest={() => {
|
||||
setSheetOpen(false);
|
||||
|
||||
if (restReminderTimerRef.current) {
|
||||
window.clearTimeout(restReminderTimerRef.current);
|
||||
}
|
||||
|
||||
restReminderTimerRef.current = window.setTimeout(() => {
|
||||
onStatusMessage({ message: copy.space.focusHud.restReminder });
|
||||
restReminderTimerRef.current = null;
|
||||
}, 5 * 60 * 1000);
|
||||
}}
|
||||
onConfirm={(nextGoal) => {
|
||||
return Promise.resolve(onGoalUpdate(nextGoal));
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user