232 lines
6.1 KiB
TypeScript
232 lines
6.1 KiB
TypeScript
|
|
"use client";
|
||
|
|
|
||
|
|
/**
|
||
|
|
* PivotState 훅
|
||
|
|
* 피벗 그리드 상태 저장/복원 관리
|
||
|
|
*/
|
||
|
|
|
||
|
|
import { useState, useEffect, useCallback } from "react";
|
||
|
|
import { PivotFieldConfig, PivotGridState } from "../types";
|
||
|
|
|
||
|
|
// ==================== 타입 ====================
|
||
|
|
|
||
|
|
export interface PivotStateConfig {
|
||
|
|
enabled: boolean;
|
||
|
|
storageKey?: string;
|
||
|
|
storageType?: "localStorage" | "sessionStorage";
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface SavedPivotState {
|
||
|
|
version: string;
|
||
|
|
timestamp: number;
|
||
|
|
fields: PivotFieldConfig[];
|
||
|
|
expandedRowPaths: string[][];
|
||
|
|
expandedColumnPaths: string[][];
|
||
|
|
filterConfig: Record<string, any[]>;
|
||
|
|
sortConfig: {
|
||
|
|
field: string;
|
||
|
|
direction: "asc" | "desc";
|
||
|
|
} | null;
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface UsePivotStateResult {
|
||
|
|
// 상태
|
||
|
|
fields: PivotFieldConfig[];
|
||
|
|
pivotState: PivotGridState;
|
||
|
|
|
||
|
|
// 상태 변경
|
||
|
|
setFields: (fields: PivotFieldConfig[]) => void;
|
||
|
|
setPivotState: (state: PivotGridState | ((prev: PivotGridState) => PivotGridState)) => void;
|
||
|
|
|
||
|
|
// 저장/복원
|
||
|
|
saveState: () => void;
|
||
|
|
loadState: () => boolean;
|
||
|
|
clearState: () => void;
|
||
|
|
hasStoredState: () => boolean;
|
||
|
|
|
||
|
|
// 상태 정보
|
||
|
|
lastSaved: Date | null;
|
||
|
|
isDirty: boolean;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ==================== 상수 ====================
|
||
|
|
|
||
|
|
const STATE_VERSION = "1.0.0";
|
||
|
|
const DEFAULT_STORAGE_KEY = "pivot-grid-state";
|
||
|
|
|
||
|
|
// ==================== 훅 ====================
|
||
|
|
|
||
|
|
export function usePivotState(
|
||
|
|
initialFields: PivotFieldConfig[],
|
||
|
|
config: PivotStateConfig
|
||
|
|
): UsePivotStateResult {
|
||
|
|
const {
|
||
|
|
enabled,
|
||
|
|
storageKey = DEFAULT_STORAGE_KEY,
|
||
|
|
storageType = "localStorage",
|
||
|
|
} = config;
|
||
|
|
|
||
|
|
// 상태
|
||
|
|
const [fields, setFieldsInternal] = useState<PivotFieldConfig[]>(initialFields);
|
||
|
|
const [pivotState, setPivotStateInternal] = useState<PivotGridState>({
|
||
|
|
expandedRowPaths: [],
|
||
|
|
expandedColumnPaths: [],
|
||
|
|
sortConfig: null,
|
||
|
|
filterConfig: {},
|
||
|
|
});
|
||
|
|
const [lastSaved, setLastSaved] = useState<Date | null>(null);
|
||
|
|
const [isDirty, setIsDirty] = useState(false);
|
||
|
|
const [initialStateLoaded, setInitialStateLoaded] = useState(false);
|
||
|
|
|
||
|
|
// 스토리지 가져오기
|
||
|
|
const getStorage = useCallback(() => {
|
||
|
|
if (typeof window === "undefined") return null;
|
||
|
|
return storageType === "localStorage" ? localStorage : sessionStorage;
|
||
|
|
}, [storageType]);
|
||
|
|
|
||
|
|
// 저장된 상태 확인
|
||
|
|
const hasStoredState = useCallback((): boolean => {
|
||
|
|
const storage = getStorage();
|
||
|
|
if (!storage) return false;
|
||
|
|
return storage.getItem(storageKey) !== null;
|
||
|
|
}, [getStorage, storageKey]);
|
||
|
|
|
||
|
|
// 상태 저장
|
||
|
|
const saveState = useCallback(() => {
|
||
|
|
if (!enabled) return;
|
||
|
|
|
||
|
|
const storage = getStorage();
|
||
|
|
if (!storage) return;
|
||
|
|
|
||
|
|
const stateToSave: SavedPivotState = {
|
||
|
|
version: STATE_VERSION,
|
||
|
|
timestamp: Date.now(),
|
||
|
|
fields,
|
||
|
|
expandedRowPaths: pivotState.expandedRowPaths,
|
||
|
|
expandedColumnPaths: pivotState.expandedColumnPaths,
|
||
|
|
filterConfig: pivotState.filterConfig,
|
||
|
|
sortConfig: pivotState.sortConfig,
|
||
|
|
};
|
||
|
|
|
||
|
|
try {
|
||
|
|
storage.setItem(storageKey, JSON.stringify(stateToSave));
|
||
|
|
setLastSaved(new Date());
|
||
|
|
setIsDirty(false);
|
||
|
|
console.log("✅ 피벗 상태 저장됨:", storageKey);
|
||
|
|
} catch (error) {
|
||
|
|
console.error("❌ 피벗 상태 저장 실패:", error);
|
||
|
|
}
|
||
|
|
}, [enabled, getStorage, storageKey, fields, pivotState]);
|
||
|
|
|
||
|
|
// 상태 불러오기
|
||
|
|
const loadState = useCallback((): boolean => {
|
||
|
|
if (!enabled) return false;
|
||
|
|
|
||
|
|
const storage = getStorage();
|
||
|
|
if (!storage) return false;
|
||
|
|
|
||
|
|
try {
|
||
|
|
const saved = storage.getItem(storageKey);
|
||
|
|
if (!saved) return false;
|
||
|
|
|
||
|
|
const parsedState: SavedPivotState = JSON.parse(saved);
|
||
|
|
|
||
|
|
// 버전 체크
|
||
|
|
if (parsedState.version !== STATE_VERSION) {
|
||
|
|
console.warn("⚠️ 저장된 상태 버전이 다름, 무시됨");
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 상태 복원
|
||
|
|
setFieldsInternal(parsedState.fields);
|
||
|
|
setPivotStateInternal({
|
||
|
|
expandedRowPaths: parsedState.expandedRowPaths,
|
||
|
|
expandedColumnPaths: parsedState.expandedColumnPaths,
|
||
|
|
sortConfig: parsedState.sortConfig,
|
||
|
|
filterConfig: parsedState.filterConfig,
|
||
|
|
});
|
||
|
|
setLastSaved(new Date(parsedState.timestamp));
|
||
|
|
setIsDirty(false);
|
||
|
|
|
||
|
|
console.log("✅ 피벗 상태 복원됨:", storageKey);
|
||
|
|
return true;
|
||
|
|
} catch (error) {
|
||
|
|
console.error("❌ 피벗 상태 복원 실패:", error);
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}, [enabled, getStorage, storageKey]);
|
||
|
|
|
||
|
|
// 상태 초기화
|
||
|
|
const clearState = useCallback(() => {
|
||
|
|
const storage = getStorage();
|
||
|
|
if (!storage) return;
|
||
|
|
|
||
|
|
try {
|
||
|
|
storage.removeItem(storageKey);
|
||
|
|
setLastSaved(null);
|
||
|
|
console.log("🗑️ 피벗 상태 삭제됨:", storageKey);
|
||
|
|
} catch (error) {
|
||
|
|
console.error("❌ 피벗 상태 삭제 실패:", error);
|
||
|
|
}
|
||
|
|
}, [getStorage, storageKey]);
|
||
|
|
|
||
|
|
// 필드 변경 (dirty 플래그 설정)
|
||
|
|
const setFields = useCallback((newFields: PivotFieldConfig[]) => {
|
||
|
|
setFieldsInternal(newFields);
|
||
|
|
setIsDirty(true);
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
// 피벗 상태 변경 (dirty 플래그 설정)
|
||
|
|
const setPivotState = useCallback(
|
||
|
|
(newState: PivotGridState | ((prev: PivotGridState) => PivotGridState)) => {
|
||
|
|
setPivotStateInternal(newState);
|
||
|
|
setIsDirty(true);
|
||
|
|
},
|
||
|
|
[]
|
||
|
|
);
|
||
|
|
|
||
|
|
// 초기 로드
|
||
|
|
useEffect(() => {
|
||
|
|
if (!initialStateLoaded && enabled && hasStoredState()) {
|
||
|
|
loadState();
|
||
|
|
setInitialStateLoaded(true);
|
||
|
|
}
|
||
|
|
}, [enabled, hasStoredState, loadState, initialStateLoaded]);
|
||
|
|
|
||
|
|
// 초기 필드 동기화 (저장된 상태가 없을 때)
|
||
|
|
useEffect(() => {
|
||
|
|
if (initialStateLoaded) return;
|
||
|
|
if (!hasStoredState() && initialFields.length > 0) {
|
||
|
|
setFieldsInternal(initialFields);
|
||
|
|
setInitialStateLoaded(true);
|
||
|
|
}
|
||
|
|
}, [initialFields, hasStoredState, initialStateLoaded]);
|
||
|
|
|
||
|
|
// 자동 저장 (변경 시)
|
||
|
|
useEffect(() => {
|
||
|
|
if (!enabled || !isDirty) return;
|
||
|
|
|
||
|
|
const timeout = setTimeout(() => {
|
||
|
|
saveState();
|
||
|
|
}, 1000); // 1초 디바운스
|
||
|
|
|
||
|
|
return () => clearTimeout(timeout);
|
||
|
|
}, [enabled, isDirty, saveState]);
|
||
|
|
|
||
|
|
return {
|
||
|
|
fields,
|
||
|
|
pivotState,
|
||
|
|
setFields,
|
||
|
|
setPivotState,
|
||
|
|
saveState,
|
||
|
|
loadState,
|
||
|
|
clearState,
|
||
|
|
hasStoredState,
|
||
|
|
lastSaved,
|
||
|
|
isDirty,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
export default usePivotState;
|
||
|
|
|