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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user