From 980c929d83237f11c4c01b2c0e25df68d6d19470 Mon Sep 17 00:00:00 2001 From: kjs Date: Thu, 8 Jan 2026 12:28:48 +0900 Subject: [PATCH] =?UTF-8?q?=EB=A6=AC=EB=8F=84,=EC=96=B8=EB=8F=84=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/common/EditableSpreadsheet.tsx | 95 ++++++++++++++++++- 1 file changed, 93 insertions(+), 2 deletions(-) diff --git a/frontend/components/common/EditableSpreadsheet.tsx b/frontend/components/common/EditableSpreadsheet.tsx index 0d4ad3d3..17c8ce94 100644 --- a/frontend/components/common/EditableSpreadsheet.tsx +++ b/frontend/components/common/EditableSpreadsheet.tsx @@ -55,8 +55,91 @@ export const EditableSpreadsheet: React.FC = ({ // 복사된 범위 (점선 애니메이션 표시용) const [copiedRange, setCopiedRange] = useState(null); + // Undo/Redo 히스토리 + interface HistoryState { + columns: string[]; + data: Record[]; + } + const [history, setHistory] = useState([]); + const [historyIndex, setHistoryIndex] = useState(-1); + const [isUndoRedo, setIsUndoRedo] = useState(false); + const inputRef = useRef(null); const tableRef = useRef(null); + + // 히스토리에 현재 상태 저장 + const saveToHistory = useCallback(() => { + if (isUndoRedo) return; + + const newState: HistoryState = { + columns: [...columns], + data: data.map(row => ({ ...row })), + }; + + setHistory(prev => { + // 현재 인덱스 이후의 히스토리는 삭제 (새로운 분기) + const newHistory = prev.slice(0, historyIndex + 1); + newHistory.push(newState); + // 최대 50개까지만 유지 + if (newHistory.length > 50) { + newHistory.shift(); + return newHistory; + } + return newHistory; + }); + setHistoryIndex(prev => Math.min(prev + 1, 49)); + }, [columns, data, historyIndex, isUndoRedo]); + + // 초기 상태 저장 + useEffect(() => { + if (history.length === 0 && (columns.length > 0 || data.length > 0)) { + setHistory([{ columns: [...columns], data: data.map(row => ({ ...row })) }]); + setHistoryIndex(0); + } + }, []); + + // 데이터 변경 시 히스토리 저장 (Undo/Redo가 아닌 경우) + useEffect(() => { + if (!isUndoRedo && historyIndex >= 0) { + const currentState = history[historyIndex]; + if (currentState) { + const columnsChanged = JSON.stringify(columns) !== JSON.stringify(currentState.columns); + const dataChanged = JSON.stringify(data) !== JSON.stringify(currentState.data); + if (columnsChanged || dataChanged) { + saveToHistory(); + } + } + } + setIsUndoRedo(false); + }, [columns, data]); + + // Undo 실행 + const handleUndo = useCallback(() => { + if (historyIndex <= 0) return; + + const prevIndex = historyIndex - 1; + const prevState = history[prevIndex]; + if (prevState) { + setIsUndoRedo(true); + setHistoryIndex(prevIndex); + onColumnsChange([...prevState.columns]); + onDataChange(prevState.data.map(row => ({ ...row }))); + } + }, [history, historyIndex, onColumnsChange, onDataChange]); + + // Redo 실행 + const handleRedo = useCallback(() => { + if (historyIndex >= history.length - 1) return; + + const nextIndex = historyIndex + 1; + const nextState = history[nextIndex]; + if (nextState) { + setIsUndoRedo(true); + setHistoryIndex(nextIndex); + onColumnsChange([...nextState.columns]); + onDataChange(nextState.data.map(row => ({ ...row }))); + } + }, [history, historyIndex, onColumnsChange, onDataChange]); // 범위 정규화 (시작이 끝보다 크면 교환) const normalizeRange = (range: CellRange): CellRange => { @@ -518,7 +601,15 @@ export const EditableSpreadsheet: React.FC = ({ return; } - if ((e.ctrlKey || e.metaKey) && e.key === "c") { + if ((e.ctrlKey || e.metaKey) && e.key === "z") { + // Ctrl+Z: Undo + e.preventDefault(); + handleUndo(); + } else if ((e.ctrlKey || e.metaKey) && e.key === "y") { + // Ctrl+Y: Redo + e.preventDefault(); + handleRedo(); + } else if ((e.ctrlKey || e.metaKey) && e.key === "c") { e.preventDefault(); handleCopy(); } else if ((e.ctrlKey || e.metaKey) && e.key === "v") { @@ -555,7 +646,7 @@ export const EditableSpreadsheet: React.FC = ({ document.addEventListener("keydown", handleGlobalKeyDown); return () => document.removeEventListener("keydown", handleGlobalKeyDown); - }, [editingCell, selection, handleCopy, handlePaste, handleDelete]); + }, [editingCell, selection, handleCopy, handlePaste, handleDelete, handleUndo, handleRedo]); // 행 삭제 const handleDeleteRow = (rowIndex: number) => {