ERP-node/frontend/hooks/useLayoutHistory.ts

208 lines
5.1 KiB
TypeScript

"use client";
import { useState, useCallback, useRef } from "react";
// ========================================
// 레이아웃 히스토리 훅
// Undo/Redo 기능 제공
// ========================================
interface HistoryState<T> {
past: T[];
present: T;
future: T[];
}
interface UseLayoutHistoryReturn<T> {
// 현재 상태
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<T>(
initialState: T,
maxHistory: number = 50
): UseLayoutHistoryReturn<T> {
const [history, setHistory] = useState<HistoryState<T>>({
past: [],
present: initialState,
future: [],
});
// 배치 업데이트를 위한 타이머
const batchTimerRef = useRef<NodeJS.Timeout | null>(null);
const pendingStateRef = useRef<T | null>(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;