diff --git a/docs/화면간_데이터_관계_설정_시스템_설계.md b/docs/화면간_데이터_관계_설정_시스템_설계.md index 5806867a..75f4bfdf 100644 --- a/docs/화면간_데이터_관계_설정_시스템_설계.md +++ b/docs/화면간_데이터_관계_설정_시스템_설계.md @@ -613,12 +613,18 @@ PUT /api/external-call-configs/:id ## 📅 구현 계획 -### Phase 1: React Flow 기본 설정 (1주) +### Phase 1: React Flow 기본 설정 (1주) ✅ **완료** -- [ ] React Flow 라이브러리 설치 및 설정 -- [ ] 기본 노드와 엣지 컴포넌트 구현 -- [ ] 화면 노드 컴포넌트 구현 -- [ ] 기본 연결선 그리기 +- [x] React Flow 라이브러리 설치 및 설정 (@xyflow/react 12.8.4) +- [x] 기본 노드와 엣지 컴포넌트 구현 +- [x] 화면 노드 컴포넌트 구현 (ScreenNode.tsx) +- [x] 기본 연결선 그리기 (CustomEdge.tsx) +- [x] 메인 데이터 흐름 관리 컴포넌트 구현 (DataFlowDesigner.tsx) +- [x] /admin/dataflow 페이지 생성 +- [x] 메뉴 시스템 연동 (SQL 스크립트 제공) +- [x] 샘플 노드 추가/삭제 기능 +- [x] 노드 간 드래그앤드롭 연결 기능 +- [x] 줌, 팬, 미니맵 등 React Flow 기본 기능 ### Phase 2: 관계 설정 기능 (2주) @@ -659,6 +665,31 @@ PUT /api/external-call-configs/:id **데이터 흐름 관리 시스템**을 통해 ERP 시스템의 화면들 간 데이터 흐름을 시각적으로 설계하고 관리할 수 있습니다. React Flow 라이브러리를 활용한 직관적인 노드 기반 인터페이스와 회사별 권한 관리, 기존 화면관리 시스템과의 완벽한 연동을 통해 체계적인 데이터 관계 관리가 가능합니다. +## 📊 구현 현황 + +### ✅ Phase 1 완료 (2024-12-19) + +**구현된 기능:** + +- React Flow 12.8.4 기반 시각적 캔버스 +- 화면 노드 컴포넌트 (필드 정보, 타입별 색상 구분) +- 커스텀 엣지 컴포넌트 (관계 타입별 스타일링) +- 드래그앤드롭 노드 배치 및 연결 +- 줌, 팬, 미니맵 등 고급 시각화 기능 +- 샘플 데이터 생성 및 관리 기능 +- /admin/dataflow 경로 설정 +- 메뉴 시스템 연동 준비 완료 + +**구현된 파일:** + +- `frontend/components/dataflow/DataFlowDesigner.tsx` +- `frontend/components/dataflow/ScreenNode.tsx` +- `frontend/components/dataflow/CustomEdge.tsx` +- `frontend/app/(main)/admin/dataflow/page.tsx` +- `docs/add_dataflow_menu.sql` (메뉴 추가 스크립트) + +**다음 단계:** Phase 2 - 관계 설정 기능 구현 + ### 주요 가치 - **React Flow 기반 시각적 설계**: 복잡한 데이터 관계를 직관적인 노드와 엣지로 설계 diff --git a/frontend/app/(main)/admin/dataflow/page.tsx b/frontend/app/(main)/admin/dataflow/page.tsx new file mode 100644 index 00000000..72eb4f8b --- /dev/null +++ b/frontend/app/(main)/admin/dataflow/page.tsx @@ -0,0 +1,23 @@ +'use client'; + +import React from 'react'; +import { DataFlowDesigner } from '@/components/dataflow/DataFlowDesigner'; +import { useAuth } from '@/hooks/useAuth'; + +export default function DataFlowPage() { + const { user } = useAuth(); + + const handleSave = (relationships: any[]) => { + console.log('저장된 관계:', relationships); + // TODO: API 호출로 관계 저장 + }; + + return ( +
+ +
+ ); +} diff --git a/frontend/components/dataflow/CustomEdge.tsx b/frontend/components/dataflow/CustomEdge.tsx new file mode 100644 index 00000000..e87c76c2 --- /dev/null +++ b/frontend/components/dataflow/CustomEdge.tsx @@ -0,0 +1,162 @@ +"use client"; + +import React from "react"; +import { EdgeProps, getBezierPath, EdgeLabelRenderer, BaseEdge } from "@xyflow/react"; + +interface CustomEdgeData { + relationshipType: string; + connectionType: string; + label?: string; +} + +export const CustomEdge: React.FC> = ({ + id, + sourceX, + sourceY, + targetX, + targetY, + sourcePosition, + targetPosition, + data, + markerEnd, + selected, +}) => { + const [edgePath, labelX, labelY] = getBezierPath({ + sourceX, + sourceY, + sourcePosition, + targetX, + targetY, + targetPosition, + }); + + // 연결 타입에 따른 색상 반환 + const getEdgeColor = (connectionType: string) => { + switch (connectionType) { + case "simple-key": + return "#3B82F6"; // 파란색 - 단순 키값 연결 + case "data-save": + return "#10B981"; // 초록색 - 데이터 저장 + case "external-call": + return "#F59E0B"; // 주황색 - 외부 호출 + default: + return "#6B7280"; // 회색 - 기본 + } + }; + + // 연결 타입에 따른 스타일 반환 + const getEdgeStyle = (connectionType: string) => { + switch (connectionType) { + case "simple-key": + return { + strokeWidth: 2, + strokeDasharray: "5,5", + opacity: selected ? 1 : 0.8, + }; + case "data-save": + return { + strokeWidth: 3, + opacity: selected ? 1 : 0.8, + }; + case "external-call": + return { + strokeWidth: 2, + strokeDasharray: "10,5", + opacity: selected ? 1 : 0.8, + }; + default: + return { + strokeWidth: 2, + opacity: selected ? 1 : 0.6, + }; + } + }; + + // 관계 타입에 따른 아이콘 반환 + const getRelationshipIcon = (relationshipType: string) => { + switch (relationshipType) { + case "one-to-one": + return "1:1"; + case "one-to-many": + return "1:N"; + case "many-to-one": + return "N:1"; + case "many-to-many": + return "N:N"; + default: + return "1:1"; + } + }; + + // 연결 타입에 따른 설명 반환 + const getConnectionTypeDescription = (connectionType: string) => { + switch (connectionType) { + case "simple-key": + return "단순 키값"; + case "data-save": + return "데이터 저장"; + case "external-call": + return "외부 호출"; + default: + return "연결"; + } + }; + + const edgeColor = getEdgeColor(data?.connectionType || ""); + const edgeStyle = getEdgeStyle(data?.connectionType || ""); + + return ( + <> + + +
+
+
+ {data?.label || getRelationshipIcon(data?.relationshipType || "one-to-one")} +
+
+ {getConnectionTypeDescription(data?.connectionType || "simple-key")} +
+
+
+
+ + {/* 선택된 상태일 때 추가 시각적 효과 */} + {selected && ( + + )} + + ); +}; diff --git a/frontend/components/dataflow/DataFlowDesigner.tsx b/frontend/components/dataflow/DataFlowDesigner.tsx new file mode 100644 index 00000000..8d70c2b6 --- /dev/null +++ b/frontend/components/dataflow/DataFlowDesigner.tsx @@ -0,0 +1,209 @@ +"use client"; + +import React, { useState, useCallback } from "react"; +import { + ReactFlow, + Node, + Edge, + Controls, + Background, + MiniMap, + useNodesState, + useEdgesState, + addEdge, + Connection, +} from "@xyflow/react"; +import "@xyflow/react/dist/style.css"; +import { ScreenNode } from "./ScreenNode"; +import { CustomEdge } from "./CustomEdge"; + +// 노드 및 엣지 타입 정의 +const nodeTypes = { + screenNode: ScreenNode, +}; + +const edgeTypes = { + customEdge: CustomEdge, +}; + +interface DataFlowDesignerProps { + companyCode: string; + onSave?: (relationships: any[]) => void; +} + +export const DataFlowDesigner: React.FC = ({ companyCode, onSave }) => { + const [nodes, setNodes, onNodesChange] = useNodesState([]); + const [edges, setEdges, onEdgesChange] = useEdgesState([]); + const [selectedField, setSelectedField] = useState<{ + screenId: string; + fieldName: string; + } | null>(null); + + // 노드 연결 처리 + const onConnect = useCallback( + (params: Connection) => { + const newEdge = { + ...params, + type: "customEdge", + data: { + relationshipType: "one-to-one", + connectionType: "simple-key", + label: "1:1 연결", + }, + }; + setEdges((eds) => addEdge(newEdge, eds)); + }, + [setEdges], + ); + + // 필드 클릭 처리 + const handleFieldClick = useCallback((screenId: string, fieldName: string) => { + setSelectedField({ screenId, fieldName }); + }, []); + + // 샘플 화면 노드 추가 + const addSampleNode = useCallback(() => { + const newNode: Node = { + id: `screen-${Date.now()}`, + type: "screenNode", + position: { x: Math.random() * 300, y: Math.random() * 200 }, + data: { + screen: { + screenId: `screen-${Date.now()}`, + screenName: `샘플 화면 ${nodes.length + 1}`, + screenCode: `SCREEN${nodes.length + 1}`, + tableName: `table_${nodes.length + 1}`, + fields: [ + { name: "id", type: "INTEGER", description: "고유 식별자" }, + { name: "name", type: "VARCHAR(100)", description: "이름" }, + { name: "code", type: "VARCHAR(50)", description: "코드" }, + { name: "created_date", type: "TIMESTAMP", description: "생성일시" }, + ], + }, + onFieldClick: handleFieldClick, + }, + }; + + setNodes((nds) => nds.concat(newNode)); + }, [nodes.length, handleFieldClick, setNodes]); + + // 노드 전체 삭제 + const clearNodes = useCallback(() => { + setNodes([]); + setEdges([]); + }, [setNodes, setEdges]); + + return ( +
+
+ {/* 사이드바 */} +
+
+

데이터 흐름 관리

+ + {/* 회사 정보 */} +
+
회사 코드
+
{companyCode}
+
+ + {/* 컨트롤 버튼들 */} +
+ + + + + +
+ + {/* 통계 정보 */} +
+
통계
+
+
+ 화면 노드: + {nodes.length}개 +
+
+ 연결: + {edges.length}개 +
+
+
+ + {/* 선택된 필드 정보 */} + {selectedField && ( +
+
선택된 필드
+
+
화면: {selectedField.screenId}
+
필드: {selectedField.fieldName}
+
+ +
+ )} +
+
+ + {/* React Flow 캔버스 */} +
+ + + { + switch (node.type) { + case "screenNode": + return "#3B82F6"; + default: + return "#6B7280"; + } + }} + /> + + + + {/* 안내 메시지 */} + {nodes.length === 0 && ( +
+
+
📊
+
데이터 흐름 설계를 시작하세요
+
왼쪽 사이드바에서 "샘플 화면 추가" 버튼을 클릭하세요
+
+
+ )} +
+
+
+ ); +}; diff --git a/frontend/components/dataflow/ScreenNode.tsx b/frontend/components/dataflow/ScreenNode.tsx new file mode 100644 index 00000000..00838449 --- /dev/null +++ b/frontend/components/dataflow/ScreenNode.tsx @@ -0,0 +1,109 @@ +"use client"; + +import React from "react"; +import { Handle, Position } from "@xyflow/react"; + +interface ScreenField { + name: string; + type: string; + description: string; +} + +interface Screen { + screenId: string; + screenName: string; + screenCode: string; + tableName: string; + fields: ScreenField[]; +} + +interface ScreenNodeData { + screen: Screen; + onFieldClick: (screenId: string, fieldName: string) => void; +} + +export const ScreenNode: React.FC<{ data: ScreenNodeData }> = ({ data }) => { + const { screen, onFieldClick } = data; + + // 필드 타입에 따른 색상 반환 + const getFieldTypeColor = (type: string) => { + if (type.includes("INTEGER") || type.includes("NUMERIC")) return "text-blue-600 bg-blue-50"; + if (type.includes("VARCHAR") || type.includes("TEXT")) return "text-green-600 bg-green-50"; + if (type.includes("TIMESTAMP") || type.includes("DATE")) return "text-purple-600 bg-purple-50"; + if (type.includes("BOOLEAN")) return "text-orange-600 bg-orange-50"; + return "text-gray-600 bg-gray-50"; + }; + + return ( +
+ {/* 노드 헤더 */} +
+
{screen.screenName}
+
ID: {screen.screenCode}
+
+ 🗃️ + 테이블: {screen.tableName} +
+
+ + {/* 필드 목록 */} +
+
+
필드 목록
+
{screen.fields.length}개
+
+ +
+ {screen.fields.map((field, index) => ( +
onFieldClick(screen.screenId, field.name)} + > +
+
+
{field.name}
+ {index === 0 && ( + PK + )} +
+
{field.description}
+
+
+ {field.type} +
+
+ ))} +
+
+ + {/* React Flow 핸들 */} + + + + {/* 추가 핸들들 (상하) */} + + +
+ ); +}; diff --git a/frontend/components/layout/MainSidebar.tsx b/frontend/components/layout/MainSidebar.tsx index 7fb246fe..97721831 100644 --- a/frontend/components/layout/MainSidebar.tsx +++ b/frontend/components/layout/MainSidebar.tsx @@ -1,4 +1,4 @@ -import { ChevronDown, ChevronRight, Home, FileText, Users, BarChart3, Cog } from "lucide-react"; +import { ChevronDown, ChevronRight, Home, FileText, Users, BarChart3, Cog, GitBranch } from "lucide-react"; import { cn } from "@/lib/utils"; import { MenuItem } from "@/types/menu"; import { MENU_ICONS, MESSAGES } from "@/constants/layout"; @@ -29,6 +29,9 @@ const getMenuIcon = (menuName: string) => { if (MENU_ICONS.SETTINGS.some((keyword) => menuName.includes(keyword))) { return ; } + if (MENU_ICONS.DATAFLOW.some((keyword) => menuName.includes(keyword))) { + return ; + } return ; }; diff --git a/frontend/constants/layout.ts b/frontend/constants/layout.ts index e245b1de..73b9eb22 100644 --- a/frontend/constants/layout.ts +++ b/frontend/constants/layout.ts @@ -30,3 +30,12 @@ export const MESSAGES = { NO_DATA: "데이터가 없습니다.", NO_MENUS: "사용 가능한 메뉴가 없습니다.", } as const; + +export const MENU_ICONS = { + HOME: ["홈", "메인", "대시보드"], + DOCUMENT: ["문서", "게시판", "공지"], + USERS: ["사용자", "회원", "직원", "인사"], + STATISTICS: ["통계", "분석", "리포트", "차트"], + SETTINGS: ["설정", "관리", "시스템"], + DATAFLOW: ["데이터", "흐름", "관계", "연결"], +} as const; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 57519b47..50d96947 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -28,6 +28,7 @@ "@radix-ui/react-tabs": "^1.1.12", "@tanstack/react-query": "^5.85.6", "@tanstack/react-table": "^8.21.3", + "@xyflow/react": "^12.8.4", "axios": "^1.11.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -2559,6 +2560,55 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -3167,6 +3217,38 @@ "win32" ] }, + "node_modules/@xyflow/react": { + "version": "12.8.4", + "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.8.4.tgz", + "integrity": "sha512-bqUu4T5QSHiCFPkoH+b+LROKwQJdLvcjhGbNW9c1dLafCBRjmH1IYz0zPE+lRDXCtQ9kRyFxz3tG19+8VORJ1w==", + "license": "MIT", + "dependencies": { + "@xyflow/system": "0.0.68", + "classcat": "^5.0.3", + "zustand": "^4.4.0" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@xyflow/system": { + "version": "0.0.68", + "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.68.tgz", + "integrity": "sha512-QDG2wxIG4qX+uF8yzm1ULVZrcXX3MxPBoxv7O52FWsX87qIImOqifUhfa/TwsvLdzn7ic2DDBH1uI8TKbdNTYA==", + "license": "MIT", + "dependencies": { + "@types/d3-drag": "^3.0.7", + "@types/d3-interpolate": "^3.0.4", + "@types/d3-selection": "^3.0.10", + "@types/d3-transition": "^3.0.8", + "@types/d3-zoom": "^3.0.8", + "d3-drag": "^3.0.0", + "d3-interpolate": "^3.0.1", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0" + } + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -3686,6 +3768,12 @@ "url": "https://polar.sh/cva" } }, + "node_modules/classcat": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", + "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==", + "license": "MIT" + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", @@ -3804,6 +3892,111 @@ "devOptional": true, "license": "MIT" }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -8290,6 +8483,34 @@ "funding": { "url": "https://github.com/sponsors/colinhacks" } + }, + "node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } } } } diff --git a/frontend/package.json b/frontend/package.json index afae2e6e..d299c50e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -33,6 +33,7 @@ "@radix-ui/react-tabs": "^1.1.12", "@tanstack/react-query": "^5.85.6", "@tanstack/react-table": "^8.21.3", + "@xyflow/react": "^12.8.4", "axios": "^1.11.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1",