49b4cdf562
5 viewMode 통합 세번째 단계 — Codex 권고 (Pivot 통째 흡수, 회귀 위험 차단) 그대로 적용. v2-pivot-grid 의 본체 + utils + components + hooks + 보조 타입을 모두 table/ 하위로 이전. 리팩토링 X (V2Date picker 패턴 동일). table/types.ts 보조 타입 흡수 - PivotResult, PivotHeaderNode, PivotFlatRow, PivotFlatColumn, PivotCellValue, PivotCellData, PivotGridState, PivotGridProps - PivotDataSourceType, PivotFilterCondition, PivotJoinConfig, PivotDataSourceConfig, PivotTotalsConfig, PivotFieldChooserConfig, PivotChartConfig, PivotConditionalFormatRule, PivotStyleConfig, PivotExportConfig - PivotFieldConfig 에 filterValues / filterType / isCalculated / calculateFormula 누락 속성 추가 - TableConfig 에 pivot 보조 키 (pivotTotals / pivotStyle / pivotFieldChooser / pivotChart / pivotExportConfig) table/utils/pivot/ 4개 파일 이전 (1505줄) - pivotEngine.ts (812) — processPivotData / pathToKey / 헤더 트리 / 매트릭스 / 10종 displayMode (runningTotal, percentDifferenceFromPrevious 등) - aggregation.ts (180) — sum/count/avg/min/max/countDistinct + 포맷 - conditionalFormat.ts (311) — colorScale/dataBar/iconSet/cellValue 4 종 - exportExcel.ts (202) — Excel 내보내기 (xlsx) - 옛 prefix(AggregationType 등) → Pivot prefix 일괄 정리 table/internals/pivot/components/ 7개 파일 이전 (2347줄) - ContextMenu / DrillDownModal / FieldChooser / FieldPanel / FilterPopup / PivotChart / index table/internals/pivot/hooks/ 3개 파일 이전 (570줄) - usePivotState / useVirtualScroll / index table/views/PivotView.tsx 신규 (PivotGridComponent.tsx 1963줄 통째 흡수) - import 경로 일괄 정정 (../../types → ../types, ./utils → ../utils/pivot, ./components → ../internals/pivot/components, ./hooks → ../internals/pivot/hooks) - 컴포넌트 이름 PivotGridComponent → PivotView - 본체 로직 그대로 (리팩토링 X) TableComponent.switch - pivot 분기 placeholder 제거 → PivotView 호출 - DOM filter 에 pivotTotals/pivotStyle/pivotFieldChooser/pivotChart/ pivotExportConfig 추가 13 files, +5400+ insertions. v2-pivot-grid/ 폴더 자체는 Phase T5 dead code 일괄 삭제에서 정리 예정. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
232 lines
6.2 KiB
TypeScript
232 lines
6.2 KiB
TypeScript
"use client";
|
|
|
|
/**
|
|
* PivotState 훅
|
|
* 피벗 그리드 상태 저장/복원 관리
|
|
*/
|
|
|
|
import { useState, useEffect, useCallback } from "react";
|
|
import { PivotFieldConfig, PivotGridState, PivotSortDirection } 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: PivotSortDirection;
|
|
} | 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;
|
|
|