되돌리기 기능 추가

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

@@ -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,
});
},