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}
+
+
+ {/* 컨트롤 버튼들 */}
+
+
+ + 샘플 화면 추가
+
+
+
+ 전체 삭제
+
+
+ onSave && onSave([])}
+ className="w-full rounded-lg bg-green-500 p-3 font-medium text-white transition-colors hover:bg-green-600"
+ >
+ 저장
+
+
+
+ {/* 통계 정보 */}
+
+
통계
+
+
+ 화면 노드:
+ {nodes.length}개
+
+
+ 연결:
+ {edges.length}개
+
+
+
+
+ {/* 선택된 필드 정보 */}
+ {selectedField && (
+
+
선택된 필드
+
+
화면: {selectedField.screenId}
+
필드: {selectedField.fieldName}
+
+
setSelectedField(null)}
+ className="mt-2 text-xs text-yellow-600 hover:text-yellow-800"
+ >
+ 선택 해제
+
+
+ )}
+
+
+
+ {/* 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",