113 lines
3.8 KiB
TypeScript
113 lines
3.8 KiB
TypeScript
/**
|
|
* 차트 워크스페이스 설정을 DB(백엔드)에 자동 저장·복원하는 훅.
|
|
*
|
|
* 사용법:
|
|
* const { isLoaded, saveNow } = useWorkspacePersist({ layoutId, syncOptions, slots, setLayoutId, setSlots });
|
|
*
|
|
* - 마운트 시 DB에서 설정을 로드하여 상태를 복원한다.
|
|
* - 설정이 변경될 때마다 debounce(2초) 후 자동 저장한다.
|
|
* - saveNow() 를 호출하면 즉시 저장한다.
|
|
*/
|
|
|
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
import {
|
|
loadWorkspace,
|
|
saveWorkspace,
|
|
loadWatchlist,
|
|
loadHoldings,
|
|
WatchlistItem,
|
|
HoldingsItem,
|
|
} from '../utils/backendApi';
|
|
|
|
export interface WorkspaceState {
|
|
layoutId: string;
|
|
syncOptions: unknown;
|
|
slots: unknown[];
|
|
}
|
|
|
|
interface UseWorkspacePersistOptions {
|
|
/** 로그인/로그아웃 시 증가 → DB에서 워크스페이스 재로드 */
|
|
sessionKey?: number;
|
|
/** 현재 워크스페이스 상태 */
|
|
state: WorkspaceState;
|
|
/** DB 로드 후 상태를 반영하는 콜백 */
|
|
onLoad: (data: WorkspaceState) => void;
|
|
/** 관심종목 로드 콜백 */
|
|
onWatchlistLoad?: (items: WatchlistItem[]) => void;
|
|
/** 보유종목 로드 콜백 */
|
|
onHoldingsLoad?: (items: HoldingsItem[]) => void;
|
|
/** 자동 저장 debounce ms (기본 2000) */
|
|
debounceMs?: number;
|
|
/** 자동 저장 비활성화 */
|
|
noAutoSave?: boolean;
|
|
}
|
|
|
|
export function useWorkspacePersist({
|
|
sessionKey = 0,
|
|
state,
|
|
onLoad,
|
|
onWatchlistLoad,
|
|
onHoldingsLoad,
|
|
debounceMs = 2000,
|
|
noAutoSave = false,
|
|
}: UseWorkspacePersistOptions) {
|
|
const [isLoaded, setIsLoaded] = useState(false);
|
|
const saveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
const isFirstRender = useRef(true);
|
|
|
|
// ── 초기 로드 (세션 변경 시 재로드) ─────────────────────────────────────────
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
setIsLoaded(false);
|
|
isFirstRender.current = true;
|
|
|
|
(async () => {
|
|
const [ws, watchlist, holdings] = await Promise.all([
|
|
loadWorkspace(),
|
|
onWatchlistLoad ? loadWatchlist() : Promise.resolve(null),
|
|
onHoldingsLoad ? loadHoldings() : Promise.resolve(null),
|
|
]);
|
|
|
|
if (cancelled) return;
|
|
|
|
if (ws) {
|
|
onLoad({
|
|
layoutId: ws.layoutId,
|
|
syncOptions: ws.syncOptions,
|
|
slots: ws.slots,
|
|
});
|
|
}
|
|
if (watchlist && onWatchlistLoad) onWatchlistLoad(watchlist);
|
|
if (holdings && onHoldingsLoad) onHoldingsLoad(holdings);
|
|
|
|
setIsLoaded(true);
|
|
})();
|
|
|
|
return () => { cancelled = true; };
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [sessionKey]);
|
|
|
|
// ── 자동 저장 (debounce) ──────────────────────────────────────────────────
|
|
useEffect(() => {
|
|
if (!isLoaded || noAutoSave) return;
|
|
if (isFirstRender.current) { isFirstRender.current = false; return; }
|
|
|
|
if (saveTimerRef.current) clearTimeout(saveTimerRef.current);
|
|
saveTimerRef.current = setTimeout(() => {
|
|
saveWorkspace(state as Parameters<typeof saveWorkspace>[0]);
|
|
}, debounceMs);
|
|
|
|
return () => {
|
|
if (saveTimerRef.current) clearTimeout(saveTimerRef.current);
|
|
};
|
|
}, [state, isLoaded, noAutoSave, debounceMs]);
|
|
|
|
// ── 즉시 저장 ──────────────────────────────────────────────────────────────
|
|
const saveNow = useCallback(() => {
|
|
if (saveTimerRef.current) clearTimeout(saveTimerRef.current);
|
|
return saveWorkspace(state as Parameters<typeof saveWorkspace>[0]);
|
|
}, [state]);
|
|
|
|
return { isLoaded, saveNow };
|
|
}
|