되돌리기 기능 추가

This commit is contained in:
kjs 2025-10-13 13:28:20 +09:00
parent 2849f7e116
commit 8046c2a2e0
3 changed files with 181 additions and 4 deletions

View File

@ -60,11 +60,14 @@ function FlowEditorInner() {
onNodesChange,
onEdgesChange,
onConnect,
onNodeDragStart,
addNode,
showPropertiesPanel,
selectNodes,
selectedNodes,
removeNodes,
undo,
redo,
} = useFlowEditorStore();
/**
@ -80,17 +83,37 @@ function FlowEditorInner() {
);
/**
* (Delete/Backspace )
* (Delete/Backspace , Ctrl+Z/Y로 Undo/Redo)
*/
const onKeyDown = useCallback(
(event: React.KeyboardEvent) => {
// Undo: Ctrl+Z (Windows/Linux) or Cmd+Z (Mac)
if ((event.ctrlKey || event.metaKey) && event.key === "z" && !event.shiftKey) {
event.preventDefault();
console.log("⏪ Undo");
undo();
return;
}
// Redo: Ctrl+Y (Windows/Linux) or Cmd+Shift+Z (Mac) or Ctrl+Shift+Z
if (
((event.ctrlKey || event.metaKey) && event.key === "y") ||
((event.ctrlKey || event.metaKey) && event.shiftKey && event.key === "z")
) {
event.preventDefault();
console.log("⏩ Redo");
redo();
return;
}
// Delete: Delete/Backspace 키로 노드 삭제
if ((event.key === "Delete" || event.key === "Backspace") && selectedNodes.length > 0) {
event.preventDefault();
console.log("🗑️ 선택된 노드 삭제:", selectedNodes);
removeNodes(selectedNodes);
}
},
[selectedNodes, removeNodes],
[selectedNodes, removeNodes, undo, redo],
);
/**
@ -170,6 +193,7 @@ function FlowEditorInner() {
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
onNodeDragStart={onNodeDragStart}
onSelectionChange={onSelectionChange}
onDragOver={onDragOver}
onDrop={onDrop}

View File

@ -25,6 +25,10 @@ export function FlowToolbar() {
isSaving,
selectedNodes,
removeNodes,
undo,
redo,
canUndo,
canRedo,
} = useFlowEditorStore();
const [showLoadDialog, setShowLoadDialog] = useState(false);
@ -108,10 +112,10 @@ export function FlowToolbar() {
<div className="h-6 w-px bg-gray-200" />
{/* 실행 취소/다시 실행 */}
<Button variant="ghost" size="sm" title="실행 취소" disabled>
<Button variant="ghost" size="sm" title="실행 취소 (Ctrl+Z)" disabled={!canUndo()} onClick={undo}>
<Undo2 className="h-4 w-4" />
</Button>
<Button variant="ghost" size="sm" title="다시 실행" disabled>
<Button variant="ghost" size="sm" title="다시 실행 (Ctrl+Y)" disabled={!canRedo()} onClick={redo}>
<Redo2 className="h-4 w-4" />
</Button>

View File

@ -13,11 +13,22 @@ interface ExternalConnectionCache {
timestamp: number;
}
// 🔥 히스토리 스냅샷 타입
interface HistorySnapshot {
nodes: FlowNode[];
edges: FlowEdge[];
}
interface FlowEditorState {
// 노드 및 엣지
nodes: FlowNode[];
edges: FlowEdge[];
// 🔥 히스토리 관리
history: HistorySnapshot[];
historyIndex: number;
maxHistorySize: number;
// 선택 상태
selectedNodes: string[];
selectedEdges: string[];
@ -39,12 +50,23 @@ interface FlowEditorState {
// 🔥 외부 커넥션 캐시 (전역 캐싱)
externalConnectionsCache: ExternalConnectionCache | null;
// ========================================================================
// 🔥 히스토리 관리 (Undo/Redo)
// ========================================================================
saveToHistory: () => void;
undo: () => void;
redo: () => void;
canUndo: () => boolean;
canRedo: () => boolean;
// ========================================================================
// 노드 관리
// ========================================================================
setNodes: (nodes: FlowNode[]) => void;
onNodesChange: (changes: NodeChange[]) => void;
onNodeDragStart: () => void; // 노드 드래그 시작 시 히스토리 저장 (변경 전 상태)
addNode: (node: FlowNode) => void;
updateNode: (id: string, data: Partial<FlowNode["data"]>) => void;
removeNode: (id: string) => void;
@ -117,6 +139,9 @@ export const useFlowEditorStore = create<FlowEditorState>((set, get) => ({
// 초기 상태
nodes: [],
edges: [],
history: [{ nodes: [], edges: [] }], // 초기 빈 상태를 히스토리에 저장
historyIndex: 0,
maxHistorySize: 50,
selectedNodes: [],
selectedEdges: [],
flowId: null,
@ -129,6 +154,103 @@ export const useFlowEditorStore = create<FlowEditorState>((set, get) => ({
validationResult: null,
externalConnectionsCache: null, // 🔥 캐시 초기화
// ========================================================================
// 🔥 히스토리 관리 (Undo/Redo)
// ========================================================================
saveToHistory: () => {
const { nodes, edges, history, historyIndex, maxHistorySize } = get();
// 현재 상태를 스냅샷으로 저장
const snapshot: HistorySnapshot = {
nodes: JSON.parse(JSON.stringify(nodes)),
edges: JSON.parse(JSON.stringify(edges)),
};
// historyIndex 이후의 히스토리 제거 (새로운 변경이 발생했으므로)
const newHistory = history.slice(0, historyIndex + 1);
newHistory.push(snapshot);
// 최대 크기 제한
if (newHistory.length > maxHistorySize) {
newHistory.shift();
}
console.log("📸 히스토리 저장:", {
노드수: nodes.length,
엣지수: edges.length,
히스토리크기: newHistory.length,
현재인덱스: newHistory.length - 1,
});
set({
history: newHistory,
historyIndex: newHistory.length - 1,
});
},
undo: () => {
const { history, historyIndex } = get();
console.log("⏪ Undo 시도:", { historyIndex, historyLength: history.length });
if (historyIndex > 0) {
const newIndex = historyIndex - 1;
const snapshot = history[newIndex];
console.log("✅ Undo 실행:", {
이전인덱스: historyIndex,
새인덱스: newIndex,
노드수: snapshot.nodes.length,
엣지수: snapshot.edges.length,
});
set({
nodes: JSON.parse(JSON.stringify(snapshot.nodes)),
edges: JSON.parse(JSON.stringify(snapshot.edges)),
historyIndex: newIndex,
});
} else {
console.log("❌ Undo 불가: 히스토리가 없음");
}
},
redo: () => {
const { history, historyIndex } = get();
console.log("⏩ Redo 시도:", { historyIndex, historyLength: history.length });
if (historyIndex < history.length - 1) {
const newIndex = historyIndex + 1;
const snapshot = history[newIndex];
console.log("✅ Redo 실행:", {
이전인덱스: historyIndex,
새인덱스: newIndex,
노드수: snapshot.nodes.length,
엣지수: snapshot.edges.length,
});
set({
nodes: JSON.parse(JSON.stringify(snapshot.nodes)),
edges: JSON.parse(JSON.stringify(snapshot.edges)),
historyIndex: newIndex,
});
} else {
console.log("❌ Redo 불가: 되돌릴 히스토리가 없음");
}
},
canUndo: () => {
const { historyIndex } = get();
return historyIndex > 0;
},
canRedo: () => {
const { history, historyIndex } = get();
return historyIndex < history.length - 1;
},
// ========================================================================
// 노드 관리
// ========================================================================
@ -141,13 +263,21 @@ export const useFlowEditorStore = create<FlowEditorState>((set, get) => ({
});
},
onNodeDragStart: () => {
// 노드 드래그 시작 시 히스토리 저장 (변경 전 상태)
get().saveToHistory();
console.log("🎯 노드 이동 시작, 변경 전 상태 히스토리 저장");
},
addNode: (node) => {
get().saveToHistory(); // 히스토리에 저장
set((state) => ({
nodes: [...state.nodes, node],
}));
},
updateNode: (id, data) => {
get().saveToHistory(); // 히스토리에 저장
set((state) => ({
nodes: state.nodes.map((node) =>
node.id === id
@ -161,6 +291,7 @@ export const useFlowEditorStore = create<FlowEditorState>((set, get) => ({
},
removeNode: (id) => {
get().saveToHistory(); // 히스토리에 저장
set((state) => ({
nodes: state.nodes.filter((node) => node.id !== id),
edges: state.edges.filter((edge) => edge.source !== id && edge.target !== id),
@ -168,6 +299,7 @@ export const useFlowEditorStore = create<FlowEditorState>((set, get) => ({
},
removeNodes: (ids) => {
get().saveToHistory(); // 히스토리에 저장
set((state) => ({
nodes: state.nodes.filter((node) => !ids.includes(node.id)),
edges: state.edges.filter((edge) => !ids.includes(edge.source) && !ids.includes(edge.target)),
@ -181,12 +313,20 @@ export const useFlowEditorStore = create<FlowEditorState>((set, get) => ({
setEdges: (edges) => set({ edges }),
onEdgesChange: (changes) => {
// 엣지 삭제(remove) 타입이 있으면 히스토리 저장
const hasRemove = changes.some((change) => change.type === "remove");
if (hasRemove) {
get().saveToHistory();
console.log("🔗 엣지 삭제, 변경 전 상태 히스토리 저장");
}
set({
edges: applyEdgeChanges(changes, get().edges) as FlowEdge[],
});
},
onConnect: (connection) => {
get().saveToHistory(); // 히스토리에 저장
// 연결 검증
const validation = validateConnection(connection, get().nodes);
if (!validation.valid) {
@ -210,12 +350,14 @@ export const useFlowEditorStore = create<FlowEditorState>((set, get) => ({
},
removeEdge: (id) => {
get().saveToHistory(); // 히스토리에 저장
set((state) => ({
edges: state.edges.filter((edge) => edge.id !== id),
}));
},
removeEdges: (ids) => {
get().saveToHistory(); // 히스토리에 저장
set((state) => ({
edges: state.edges.filter((edge) => !ids.includes(edge.id)),
}));
@ -253,6 +395,7 @@ export const useFlowEditorStore = create<FlowEditorState>((set, get) => ({
// ========================================================================
loadFlow: (id, name, description, nodes, edges) => {
console.log("📂 플로우 로드:", { id, name, 노드수: nodes.length, 엣지수: edges.length });
set({
flowId: id,
flowName: name,
@ -261,10 +404,14 @@ export const useFlowEditorStore = create<FlowEditorState>((set, get) => ({
edges,
selectedNodes: [],
selectedEdges: [],
// 로드된 상태를 히스토리의 첫 번째 스냅샷으로 저장
history: [{ nodes: JSON.parse(JSON.stringify(nodes)), edges: JSON.parse(JSON.stringify(edges)) }],
historyIndex: 0,
});
},
clearFlow: () => {
console.log("🔄 플로우 초기화");
set({
flowId: null,
flowName: "새 제어 플로우",
@ -274,6 +421,8 @@ export const useFlowEditorStore = create<FlowEditorState>((set, get) => ({
selectedNodes: [],
selectedEdges: [],
validationResult: null,
history: [{ nodes: [], edges: [] }], // 초기 빈 상태를 히스토리에 저장
historyIndex: 0,
});
},