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