/** * @module useHistoryManager * @description 컴포넌트 변경 이력을 관리하여 Undo/Redo 기능을 제공한다. * - 최대 50개의 히스토리 스냅샷 유지 (메모리 제한) * - 300ms 디바운스로 연속 변경을 하나의 히스토리 항목으로 묶음 * - Undo/Redo 실행 중에는 히스토리 저장을 건너뜀 (isUndoRedoing 플래그) */ import { useState, useCallback, useEffect } from "react"; import type { ComponentConfig } from "@/types/report"; import type { SetComponentsFn, ToastFunction } from "./internalTypes"; /** 히스토리에 유지할 최대 스냅샷 수 */ const MAX_HISTORY_SIZE = 50; /** 컴포넌트 변경 후 히스토리 저장까지 대기 시간 (ms) */ const HISTORY_DEBOUNCE_MS = 300; export interface HistoryManagerResult { undo: () => void; redo: () => void; canUndo: boolean; canRedo: boolean; /** 외부에서 히스토리 초기화가 필요할 때 사용 (레이아웃 최초 로드 시) */ initHistory: (components: ComponentConfig[]) => void; isUndoRedoing: boolean; } export function useHistoryManager( components: ComponentConfig[], setComponents: SetComponentsFn, isLoading: boolean, toast: ToastFunction, ): HistoryManagerResult { const [history, setHistory] = useState([]); const [historyIndex, setHistoryIndex] = useState(-1); const [isUndoRedoing, setIsUndoRedoing] = useState(false); /** * 현재 컴포넌트 배열을 히스토리에 저장한다. * 현재 인덱스 이후의 미래 이력은 제거하여 새 분기를 시작한다. */ const saveToHistory = useCallback( (newComponents: ComponentConfig[]) => { setHistory((prev) => { const truncated = prev.slice(0, historyIndex + 1); truncated.push(JSON.parse(JSON.stringify(newComponents))); return truncated.slice(-MAX_HISTORY_SIZE); }); setHistoryIndex((prev) => Math.min(prev + 1, MAX_HISTORY_SIZE - 1)); }, [historyIndex], ); /** 최초 레이아웃 로드 후 히스토리를 초기 상태로 설정한다. */ const initHistory = useCallback((initialComponents: ComponentConfig[]) => { setHistory([JSON.parse(JSON.stringify(initialComponents))]); setHistoryIndex(0); }, []); /** 컴포넌트 변경 감지 → 300ms 디바운스 후 히스토리에 저장 */ useEffect(() => { if (components.length === 0 || isLoading || isUndoRedoing) return; const timeoutId = setTimeout(() => { setHistory((prev) => { const lastSnapshot = prev[historyIndex]; const isDifferent = !lastSnapshot || JSON.stringify(lastSnapshot) !== JSON.stringify(components); if (isDifferent) { saveToHistory(components); } return prev; // saveToHistory가 내부에서 setState를 호출함 }); }, HISTORY_DEBOUNCE_MS); return () => clearTimeout(timeoutId); // eslint-disable-next-line react-hooks/exhaustive-deps }, [components, isUndoRedoing, isLoading]); /** 이전 상태로 되돌린다. */ const undo = useCallback(() => { if (historyIndex <= 0) return; setIsUndoRedoing(true); const prevIndex = historyIndex - 1; setHistoryIndex(prevIndex); setComponents(JSON.parse(JSON.stringify(history[prevIndex]))); setTimeout(() => setIsUndoRedoing(false), 100); toast({ title: "실행 취소", description: "이전 상태로 되돌렸습니다." }); }, [historyIndex, history, setComponents, toast]); /** 취소한 작업을 다시 실행한다. */ const redo = useCallback(() => { if (historyIndex >= history.length - 1) return; setIsUndoRedoing(true); const nextIndex = historyIndex + 1; setHistoryIndex(nextIndex); setComponents(JSON.parse(JSON.stringify(history[nextIndex]))); setTimeout(() => setIsUndoRedoing(false), 100); toast({ title: "다시 실행", description: "다음 상태로 이동했습니다." }); }, [historyIndex, history, setComponents, toast]); return { undo, redo, canUndo: historyIndex > 0, canRedo: historyIndex < history.length - 1, initHistory, isUndoRedoing, }; }