feat(resume): 지난 한 조각 이어서 시작하는 진입 플로우 추가

맥락:
- /space 재진입 시 마지막 목표를 다시 쓰게 되어 시작 마찰이 컸다.
- work.md 작업 2 요구사항에 맞춰 목표 기반 Resume CTA를 진입 의식 안에 추가했다.

변경사항:
- workspace localStorage 스키마에 goal 필드를 추가하고 저장/복원에 반영했다.
- Setup Ritual에 지난 한 조각 이어서 블록을 추가했다.
- 이어서 시작은 저장 목표로 즉시 Focus 전환, 새로 시작은 목표 초기화 후 새 세션 입력으로 전환하도록 연결했다.
- session 문서 docs/session_brief.md, docs/90_current_state.md를 최신 상태로 갱신했다.

검증:
- npx tsc --noEmit

세션-상태: Resume CTA와 목표 복원 흐름이 /space 진입에 반영됨
세션-다음: Goal Complete 루프와 Recover(Notes→Inbox) 플로우 마감
세션-리스크: localStorage 기반 복원이라 다중 탭/스토리지 초기화 시 세션 연속성이 약할 수 있음
This commit is contained in:
2026-03-05 18:18:13 +09:00
parent 8917cd8e77
commit 5f7ca99f44
4 changed files with 88 additions and 14 deletions

View File

@@ -31,6 +31,7 @@ interface StoredWorkspaceSelection {
sceneId?: string;
timerPresetId?: string;
soundPresetId?: string;
goal?: string;
override?: Partial<SelectionOverride>;
}
@@ -143,6 +144,13 @@ const resolveInitialTimerLabel = (
export const SpaceWorkspaceWidget = () => {
const searchParams = useSearchParams();
const storedSelection = useMemo(() => readStoredWorkspaceSelection(), []);
const roomQuery = searchParams.get('room');
const goalQuery = searchParams.get('goal');
const soundQuery = searchParams.get('sound');
const timerQuery = searchParams.get('timer');
const storedGoal = storedSelection.goal?.trim() ?? '';
const hasQueryOverrides = Boolean(roomQuery || goalQuery || soundQuery || timerQuery);
const canOfferResume = storedGoal.length > 0 && !hasQueryOverrides;
const {
thoughts,
thoughtCount,
@@ -154,16 +162,16 @@ export const SpaceWorkspaceWidget = () => {
setThoughtCompleted,
} = useThoughtInbox();
const initialRoomId = resolveInitialRoomId(searchParams.get('room'), storedSelection.sceneId);
const initialRoomId = resolveInitialRoomId(roomQuery, storedSelection.sceneId);
const initialRoom = getRoomById(initialRoomId) ?? ROOM_THEMES[0];
const initialGoal = searchParams.get('goal')?.trim() ?? '';
const initialGoal = goalQuery?.trim() ?? '';
const initialSoundPresetId = resolveInitialSoundPreset(
searchParams.get('sound'),
soundQuery,
storedSelection.soundPresetId,
initialRoom.recommendedSoundPresetId,
);
const initialTimerLabel = resolveInitialTimerLabel(
searchParams.get('timer'),
timerQuery,
storedSelection.timerPresetId,
initialRoom.recommendedTimerPresetId,
);
@@ -173,6 +181,7 @@ export const SpaceWorkspaceWidget = () => {
const [selectedTimerLabel, setSelectedTimerLabel] = useState(initialTimerLabel);
const [goalInput, setGoalInput] = useState(initialGoal);
const [selectedGoalId, setSelectedGoalId] = useState<string | null>(null);
const [showResumePrompt, setShowResumePrompt] = useState(canOfferResume);
const [selectionOverride, setSelectionOverride] = useState<SelectionOverride>({
sound: Boolean(storedSelection.override?.sound),
timer: Boolean(storedSelection.override?.timer),
@@ -266,11 +275,16 @@ export const SpaceWorkspaceWidget = () => {
};
const handleGoalChipSelect = (chip: GoalChip) => {
setShowResumePrompt(false);
setSelectedGoalId(chip.id);
setGoalInput(chip.label);
};
const handleGoalChange = (value: string) => {
if (showResumePrompt) {
setShowResumePrompt(false);
}
setGoalInput(value);
if (value.trim().length === 0) {
@@ -283,6 +297,7 @@ export const SpaceWorkspaceWidget = () => {
return;
}
setShowResumePrompt(false);
setWorkspaceMode('focus');
};
@@ -309,6 +324,11 @@ export const SpaceWorkspaceWidget = () => {
}
const timerPresetId = resolveTimerPresetIdFromLabel(selectedTimerLabel);
const normalizedGoal = goalInput.trim().length > 0
? goalInput.trim()
: showResumePrompt
? storedGoal
: '';
window.localStorage.setItem(
WORKSPACE_SELECTION_STORAGE_KEY,
@@ -316,10 +336,11 @@ export const SpaceWorkspaceWidget = () => {
sceneId: selectedRoomId,
timerPresetId,
soundPresetId: selectedPresetId,
goal: normalizedGoal,
override: selectionOverride,
}),
);
}, [selectedRoomId, selectedTimerLabel, selectedPresetId, selectionOverride]);
}, [goalInput, selectedRoomId, selectedTimerLabel, selectedPresetId, selectionOverride, showResumePrompt, storedGoal]);
return (
<div className="relative h-dvh overflow-hidden text-white">
@@ -351,6 +372,24 @@ export const SpaceWorkspaceWidget = () => {
onGoalChange={handleGoalChange}
onGoalChipSelect={handleGoalChipSelect}
onStart={handleStart}
resumeHint={
showResumePrompt
? {
goal: storedGoal,
onResume: () => {
setGoalInput(storedGoal);
setSelectedGoalId(null);
setShowResumePrompt(false);
setWorkspaceMode('focus');
},
onStartFresh: () => {
setGoalInput('');
setSelectedGoalId(null);
setShowResumePrompt(false);
},
}
: undefined
}
/>
<SpaceFocusHudWidget