usePersistedState hook으로 대시보드 상태를 localStorage에 자동 저장. 지도 뷰(중심/줌/방위)도 60초 주기 + 언마운트 시 저장하여 새로고침 복원. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
104 lines
2.8 KiB
TypeScript
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];
|
|
}
|