되돌리기 기능 추가
This commit is contained in:
@@ -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,
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user