"use client"; import { useState, useCallback, useRef } from "react"; // ======================================== // 레이아웃 히스토리 훅 // Undo/Redo 기능 제공 // ======================================== interface HistoryState { past: T[]; present: T; future: T[]; } interface UseLayoutHistoryReturn { // 현재 상태 state: T; // 상태 설정 (히스토리에 기록) setState: (newState: T | ((prev: T) => T)) => void; // Undo undo: () => void; // Redo redo: () => void; // Undo 가능 여부 canUndo: boolean; // Redo 가능 여부 canRedo: boolean; // 히스토리 초기화 (새 레이아웃 로드 시) reset: (initialState: T) => void; // 히스토리 크기 historySize: number; } /** * 레이아웃 히스토리 관리 훅 * @param initialState 초기 상태 * @param maxHistory 최대 히스토리 개수 (기본 50) */ export function useLayoutHistory( initialState: T, maxHistory: number = 50 ): UseLayoutHistoryReturn { const [history, setHistory] = useState>({ past: [], present: initialState, future: [], }); // 배치 업데이트를 위한 타이머 const batchTimerRef = useRef(null); const pendingStateRef = useRef(null); /** * 상태 설정 (히스토리에 기록) * 연속적인 드래그 등의 작업 시 배치 처리 */ const setState = useCallback( (newState: T | ((prev: T) => T)) => { setHistory((prev) => { const resolvedState = typeof newState === "function" ? (newState as (prev: T) => T)(prev.present) : newState; // 같은 상태면 무시 if (JSON.stringify(resolvedState) === JSON.stringify(prev.present)) { console.log("[History] 상태 동일, 무시"); return prev; } // 히스토리에 현재 상태 추가 const newPast = [...prev.past, prev.present]; // 최대 히스토리 개수 제한 if (newPast.length > maxHistory) { newPast.shift(); } console.log("[History] 상태 저장, past 크기:", newPast.length); return { past: newPast, present: resolvedState, future: [], // Redo 히스토리 초기화 }; }); }, [maxHistory] ); /** * 배치 상태 설정 (드래그 중 연속 업데이트용) * 마지막 상태만 히스토리에 기록 */ const setStateBatched = useCallback( (newState: T | ((prev: T) => T), batchDelay: number = 300) => { // 현재 상태 업데이트 (히스토리에는 바로 기록하지 않음) setHistory((prev) => { const resolvedState = typeof newState === "function" ? (newState as (prev: T) => T)(prev.present) : newState; pendingStateRef.current = prev.present; return { ...prev, present: resolvedState, }; }); // 배치 타이머 리셋 if (batchTimerRef.current) { clearTimeout(batchTimerRef.current); } // 일정 시간 후 히스토리에 기록 batchTimerRef.current = setTimeout(() => { if (pendingStateRef.current !== null) { setHistory((prev) => { const newPast = [...prev.past, pendingStateRef.current as T]; if (newPast.length > maxHistory) { newPast.shift(); } pendingStateRef.current = null; return { ...prev, past: newPast, future: [], }; }); } }, batchDelay); }, [maxHistory] ); /** * Undo - 이전 상태로 복원 */ const undo = useCallback(() => { console.log("[History] Undo 호출"); setHistory((prev) => { console.log("[History] Undo 실행, past 크기:", prev.past.length); if (prev.past.length === 0) { console.log("[History] Undo 불가 - past 비어있음"); return prev; } const newPast = [...prev.past]; const previousState = newPast.pop()!; console.log("[History] Undo 성공, 남은 past 크기:", newPast.length); return { past: newPast, present: previousState, future: [prev.present, ...prev.future], }; }); }, []); /** * Redo - 되돌린 상태 다시 적용 */ const redo = useCallback(() => { setHistory((prev) => { if (prev.future.length === 0) { return prev; } const newFuture = [...prev.future]; const nextState = newFuture.shift()!; return { past: [...prev.past, prev.present], present: nextState, future: newFuture, }; }); }, []); /** * 히스토리 초기화 (새 레이아웃 로드 시) */ const reset = useCallback((initialState: T) => { setHistory({ past: [], present: initialState, future: [], }); }, []); return { state: history.present, setState, undo, redo, canUndo: history.past.length > 0, canRedo: history.future.length > 0, reset, historySize: history.past.length, }; } export default useLayoutHistory;