되돌리기 기능 추가
This commit is contained in:
parent
2849f7e116
commit
8046c2a2e0
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue