gc-wing/apps/web/src/shared/hooks/usePersistedState.ts
htlee 16ebf3abca feat: 사용자 설정 영속화 + 지도 뷰 저장
usePersistedState hook으로 대시보드 상태를
localStorage에 자동 저장. 지도 뷰(중심/줌/방위)도
60초 주기 + 언마운트 시 저장하여 새로고침 복원.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 12:18:39 +09:00

104 lines
2.8 KiB
TypeScript

import { useState, useEffect, useRef, type Dispatch, type SetStateAction } from 'react';
const PREFIX = 'wing';
function buildKey(userId: number, name: string): string {
return `${PREFIX}:${userId}:${name}`;
}
function readStorage<T>(key: string, fallback: T): T {
try {
const raw = localStorage.getItem(key);
if (raw === null) return fallback;
return JSON.parse(raw) as T;
} catch {
return fallback;
}
}
function writeStorage<T>(key: string, value: T): void {
try {
localStorage.setItem(key, JSON.stringify(value));
} catch {
// quota exceeded or unavailable — silent
}
}
function resolveDefault<T>(d: T | (() => T)): T {
return typeof d === 'function' ? (d as () => T)() : d;
}
/**
* useState와 동일한 API, localStorage 자동 동기화.
*
* @param userId null이면 일반 useState처럼 동작 (비영속)
* @param name 설정 이름 (e.g. 'typeEnabled')
* @param defaultValue 초기값 또는 lazy initializer
* @param debounceMs localStorage 쓰기 디바운스 (기본 300ms)
*/
export function usePersistedState<T>(
userId: number | null,
name: string,
defaultValue: T | (() => T),
debounceMs = 300,
): [T, Dispatch<SetStateAction<T>>] {
const resolved = resolveDefault(defaultValue);
const [state, setState] = useState<T>(() => {
if (userId == null) return resolved;
return readStorage(buildKey(userId, name), resolved);
});
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const stateRef = useRef(state);
const userIdRef = useRef(userId);
const nameRef = useRef(name);
stateRef.current = state;
userIdRef.current = userId;
nameRef.current = name;
// userId 변경 시 해당 사용자의 저장값 재로드
useEffect(() => {
if (userId == null) return;
const stored = readStorage(buildKey(userId, name), resolved);
setState(stored);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [userId]);
// debounced write
useEffect(() => {
if (userId == null) return;
const key = buildKey(userId, name);
if (timerRef.current != null) clearTimeout(timerRef.current);
timerRef.current = setTimeout(() => {
writeStorage(key, state);
timerRef.current = null;
}, debounceMs);
return () => {
if (timerRef.current != null) {
clearTimeout(timerRef.current);
timerRef.current = null;
}
};
}, [state, userId, name, debounceMs]);
// unmount 시 pending write flush
useEffect(() => {
return () => {
if (timerRef.current != null) {
clearTimeout(timerRef.current);
timerRef.current = null;
}
if (userIdRef.current != null) {
writeStorage(buildKey(userIdRef.current, nameRef.current), stateRef.current);
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return [state, setState];
}