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;
|
sound: boolean;
|
||||||
timer: 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)) {
|
if (roomIdFromQuery && getRoomById(roomIdFromQuery)) {
|
||||||
return roomIdFromQuery;
|
return roomIdFromQuery;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (storedSceneId && getRoomById(storedSceneId)) {
|
||||||
|
return storedSceneId;
|
||||||
|
}
|
||||||
|
|
||||||
return ROOM_THEMES[0].id;
|
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)) {
|
if (presetIdFromQuery && SOUND_PRESETS.some((preset) => preset.id === presetIdFromQuery)) {
|
||||||
return presetIdFromQuery;
|
return presetIdFromQuery;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (storedPresetId && SOUND_PRESETS.some((preset) => preset.id === storedPresetId)) {
|
||||||
|
return storedPresetId;
|
||||||
|
}
|
||||||
|
|
||||||
if (recommendedPresetId && SOUND_PRESETS.some((preset) => preset.id === recommendedPresetId)) {
|
if (recommendedPresetId && SOUND_PRESETS.some((preset) => preset.id === recommendedPresetId)) {
|
||||||
return recommendedPresetId;
|
return recommendedPresetId;
|
||||||
}
|
}
|
||||||
@@ -66,11 +110,26 @@ const resolveTimerLabelFromPresetId = (presetId?: string) => {
|
|||||||
return preset.label;
|
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)) {
|
if (timerLabelFromQuery && TIMER_SELECTION_PRESETS.some((preset) => preset.label === timerLabelFromQuery)) {
|
||||||
return timerLabelFromQuery;
|
return timerLabelFromQuery;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const storedLabel = resolveTimerLabelFromPresetId(storedPresetId);
|
||||||
|
|
||||||
|
if (storedLabel) {
|
||||||
|
return storedLabel;
|
||||||
|
}
|
||||||
|
|
||||||
const recommendedLabel = resolveTimerLabelFromPresetId(recommendedPresetId);
|
const recommendedLabel = resolveTimerLabelFromPresetId(recommendedPresetId);
|
||||||
|
|
||||||
if (recommendedLabel) {
|
if (recommendedLabel) {
|
||||||
@@ -82,6 +141,7 @@ const resolveInitialTimerLabel = (timerLabelFromQuery: string | null, recommende
|
|||||||
|
|
||||||
export const SpaceWorkspaceWidget = () => {
|
export const SpaceWorkspaceWidget = () => {
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
|
const storedSelection = useMemo(() => readStoredWorkspaceSelection(), []);
|
||||||
const {
|
const {
|
||||||
thoughts,
|
thoughts,
|
||||||
thoughtCount,
|
thoughtCount,
|
||||||
@@ -92,15 +152,17 @@ export const SpaceWorkspaceWidget = () => {
|
|||||||
setThoughtCompleted,
|
setThoughtCompleted,
|
||||||
} = useThoughtInbox();
|
} = useThoughtInbox();
|
||||||
|
|
||||||
const initialRoomId = resolveInitialRoomId(searchParams.get('room'));
|
const initialRoomId = resolveInitialRoomId(searchParams.get('room'), storedSelection.sceneId);
|
||||||
const initialRoom = getRoomById(initialRoomId) ?? ROOM_THEMES[0];
|
const initialRoom = getRoomById(initialRoomId) ?? ROOM_THEMES[0];
|
||||||
const initialGoal = searchParams.get('goal')?.trim() ?? '';
|
const initialGoal = searchParams.get('goal')?.trim() ?? '';
|
||||||
const initialSoundPresetId = resolveInitialSoundPreset(
|
const initialSoundPresetId = resolveInitialSoundPreset(
|
||||||
searchParams.get('sound'),
|
searchParams.get('sound'),
|
||||||
|
storedSelection.soundPresetId,
|
||||||
initialRoom.recommendedSoundPresetId,
|
initialRoom.recommendedSoundPresetId,
|
||||||
);
|
);
|
||||||
const initialTimerLabel = resolveInitialTimerLabel(
|
const initialTimerLabel = resolveInitialTimerLabel(
|
||||||
searchParams.get('timer'),
|
searchParams.get('timer'),
|
||||||
|
storedSelection.timerPresetId,
|
||||||
initialRoom.recommendedTimerPresetId,
|
initialRoom.recommendedTimerPresetId,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -109,7 +171,10 @@ export const SpaceWorkspaceWidget = () => {
|
|||||||
const [selectedTimerLabel, setSelectedTimerLabel] = useState(initialTimerLabel);
|
const [selectedTimerLabel, setSelectedTimerLabel] = useState(initialTimerLabel);
|
||||||
const [goalInput, setGoalInput] = useState(initialGoal);
|
const [goalInput, setGoalInput] = useState(initialGoal);
|
||||||
const [selectedGoalId, setSelectedGoalId] = useState<string | null>(null);
|
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 {
|
const {
|
||||||
selectedPresetId,
|
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 (
|
return (
|
||||||
<div className="relative h-dvh overflow-hidden text-white">
|
<div className="relative h-dvh overflow-hidden text-white">
|
||||||
<div
|
<div
|
||||||
|
|||||||
Reference in New Issue
Block a user