chore(state): /space 선택 상태와 override 로컬 저장/복원 추가

맥락:
- 새로고침 시 Scene/Timer/Sound와 override가 초기화되면 추천 자동화 체감이 끊겨 사용성이 떨어졌습니다.

변경사항:
- /space workspace에 localStorage 기반 상태 저장/복원 키(viberoom:workspace-selection:v1)를 추가했습니다.
- sceneId, timerPresetId, soundPresetId, override.sound/timer를 저장하고 진입 시 복원하도록 반영했습니다.
- 복원 우선순위는 쿼리 파라미터 > 저장 상태 > Scene 추천값 순으로 정리했습니다.

검증:
- npx tsc --noEmit

세션-상태: 새로고침 후에도 마지막 Scene/Timer/Sound 및 override 상태가 유지됩니다.
세션-다음: 세션 문서(90_current_state/session_brief) 업데이트와 최종 정리를 진행합니다.
세션-리스크: 저장 포맷 변경 시 이전 localStorage 데이터와의 호환성 점검이 필요합니다.
This commit is contained in:
2026-03-05 12:22:12 +09:00
parent 4556b64bbd
commit 478159f787

View File

@@ -26,20 +26,64 @@ type SelectionOverride = {
sound: boolean;
timer: boolean;
};
interface StoredWorkspaceSelection {
sceneId?: string;
timerPresetId?: string;
soundPresetId?: string;
override?: Partial<SelectionOverride>;
}
const resolveInitialRoomId = (roomIdFromQuery: string | null) => {
const WORKSPACE_SELECTION_STORAGE_KEY = 'viberoom:workspace-selection:v1';
const readStoredWorkspaceSelection = (): StoredWorkspaceSelection => {
if (typeof window === 'undefined') {
return {};
}
const raw = window.localStorage.getItem(WORKSPACE_SELECTION_STORAGE_KEY);
if (!raw) {
return {};
}
try {
const parsed = JSON.parse(raw);
if (!parsed || typeof parsed !== 'object') {
return {};
}
return parsed as StoredWorkspaceSelection;
} catch {
return {};
}
};
const resolveInitialRoomId = (roomIdFromQuery: string | null, storedSceneId?: string) => {
if (roomIdFromQuery && getRoomById(roomIdFromQuery)) {
return roomIdFromQuery;
}
if (storedSceneId && getRoomById(storedSceneId)) {
return storedSceneId;
}
return ROOM_THEMES[0].id;
};
const resolveInitialSoundPreset = (presetIdFromQuery: string | null, recommendedPresetId?: string) => {
const resolveInitialSoundPreset = (
presetIdFromQuery: string | null,
storedPresetId: string | undefined,
recommendedPresetId?: string,
) => {
if (presetIdFromQuery && SOUND_PRESETS.some((preset) => preset.id === presetIdFromQuery)) {
return presetIdFromQuery;
}
if (storedPresetId && SOUND_PRESETS.some((preset) => preset.id === storedPresetId)) {
return storedPresetId;
}
if (recommendedPresetId && SOUND_PRESETS.some((preset) => preset.id === recommendedPresetId)) {
return recommendedPresetId;
}
@@ -66,11 +110,26 @@ const resolveTimerLabelFromPresetId = (presetId?: string) => {
return preset.label;
};
const resolveInitialTimerLabel = (timerLabelFromQuery: string | null, recommendedPresetId?: string) => {
const resolveTimerPresetIdFromLabel = (timerLabel: string) => {
const preset = TIMER_SELECTION_PRESETS.find((candidate) => candidate.label === timerLabel);
return preset?.id;
};
const resolveInitialTimerLabel = (
timerLabelFromQuery: string | null,
storedPresetId?: string,
recommendedPresetId?: string,
) => {
if (timerLabelFromQuery && TIMER_SELECTION_PRESETS.some((preset) => preset.label === timerLabelFromQuery)) {
return timerLabelFromQuery;
}
const storedLabel = resolveTimerLabelFromPresetId(storedPresetId);
if (storedLabel) {
return storedLabel;
}
const recommendedLabel = resolveTimerLabelFromPresetId(recommendedPresetId);
if (recommendedLabel) {
@@ -82,6 +141,7 @@ const resolveInitialTimerLabel = (timerLabelFromQuery: string | null, recommende
export const SpaceWorkspaceWidget = () => {
const searchParams = useSearchParams();
const storedSelection = useMemo(() => readStoredWorkspaceSelection(), []);
const {
thoughts,
thoughtCount,
@@ -92,15 +152,17 @@ export const SpaceWorkspaceWidget = () => {
setThoughtCompleted,
} = useThoughtInbox();
const initialRoomId = resolveInitialRoomId(searchParams.get('room'));
const initialRoomId = resolveInitialRoomId(searchParams.get('room'), storedSelection.sceneId);
const initialRoom = getRoomById(initialRoomId) ?? ROOM_THEMES[0];
const initialGoal = searchParams.get('goal')?.trim() ?? '';
const initialSoundPresetId = resolveInitialSoundPreset(
searchParams.get('sound'),
storedSelection.soundPresetId,
initialRoom.recommendedSoundPresetId,
);
const initialTimerLabel = resolveInitialTimerLabel(
searchParams.get('timer'),
storedSelection.timerPresetId,
initialRoom.recommendedTimerPresetId,
);
@@ -109,7 +171,10 @@ export const SpaceWorkspaceWidget = () => {
const [selectedTimerLabel, setSelectedTimerLabel] = useState(initialTimerLabel);
const [goalInput, setGoalInput] = useState(initialGoal);
const [selectedGoalId, setSelectedGoalId] = useState<string | null>(null);
const [selectionOverride, setSelectionOverride] = useState<SelectionOverride>({ sound: false, timer: false });
const [selectionOverride, setSelectionOverride] = useState<SelectionOverride>({
sound: Boolean(storedSelection.override?.sound),
timer: Boolean(storedSelection.override?.timer),
});
const {
selectedPresetId,
@@ -258,6 +323,24 @@ export const SpaceWorkspaceWidget = () => {
};
}, []);
useEffect(() => {
if (typeof window === 'undefined') {
return;
}
const timerPresetId = resolveTimerPresetIdFromLabel(selectedTimerLabel);
window.localStorage.setItem(
WORKSPACE_SELECTION_STORAGE_KEY,
JSON.stringify({
sceneId: selectedRoomId,
timerPresetId,
soundPresetId: selectedPresetId,
override: selectionOverride,
}),
);
}, [selectedRoomId, selectedTimerLabel, selectedPresetId, selectionOverride]);
return (
<div className="relative h-dvh overflow-hidden text-white">
<div