React Flow 기본 설정 완료
This commit is contained in:
parent
e4e11fa490
commit
0c765921b7
|
|
@ -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 기반 시각적 설계**: 복잡한 데이터 관계를 직관적인 노드와 엣지로 설계
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="h-screen bg-gray-50">
|
||||
<DataFlowDesigner
|
||||
companyCode={user?.companyCode || 'COMP001'}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -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<EdgeProps<CustomEdgeData>> = ({
|
||||
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 (
|
||||
<>
|
||||
<BaseEdge
|
||||
id={id}
|
||||
path={edgePath}
|
||||
markerEnd={markerEnd}
|
||||
style={{
|
||||
stroke: edgeColor,
|
||||
...edgeStyle,
|
||||
}}
|
||||
/>
|
||||
<EdgeLabelRenderer>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
|
||||
background: "white",
|
||||
padding: "8px 12px",
|
||||
borderRadius: "8px",
|
||||
fontSize: "12px",
|
||||
fontWeight: 600,
|
||||
border: `2px solid ${edgeColor}`,
|
||||
boxShadow: "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)",
|
||||
color: edgeColor,
|
||||
minWidth: "80px",
|
||||
textAlign: "center",
|
||||
}}
|
||||
className={`nodrag nopan transition-all duration-200 ${selected ? "scale-110" : "hover:scale-105"}`}
|
||||
>
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="text-sm font-bold">
|
||||
{data?.label || getRelationshipIcon(data?.relationshipType || "one-to-one")}
|
||||
</div>
|
||||
<div className="mt-1 text-xs opacity-75">
|
||||
{getConnectionTypeDescription(data?.connectionType || "simple-key")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</EdgeLabelRenderer>
|
||||
|
||||
{/* 선택된 상태일 때 추가 시각적 효과 */}
|
||||
{selected && (
|
||||
<BaseEdge
|
||||
id={`${id}-glow`}
|
||||
path={edgePath}
|
||||
style={{
|
||||
stroke: edgeColor,
|
||||
strokeWidth: 6,
|
||||
opacity: 0.3,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -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<DataFlowDesignerProps> = ({ 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 (
|
||||
<div className="data-flow-designer h-screen bg-gray-100">
|
||||
<div className="flex h-full">
|
||||
{/* 사이드바 */}
|
||||
<div className="w-80 border-r border-gray-200 bg-white shadow-lg">
|
||||
<div className="p-6">
|
||||
<h2 className="mb-6 text-xl font-bold text-gray-800">데이터 흐름 관리</h2>
|
||||
|
||||
{/* 회사 정보 */}
|
||||
<div className="mb-6 rounded-lg bg-blue-50 p-4">
|
||||
<div className="text-sm font-medium text-blue-600">회사 코드</div>
|
||||
<div className="text-lg font-bold text-blue-800">{companyCode}</div>
|
||||
</div>
|
||||
|
||||
{/* 컨트롤 버튼들 */}
|
||||
<div className="space-y-3">
|
||||
<button
|
||||
onClick={addSampleNode}
|
||||
className="w-full rounded-lg bg-blue-500 p-3 font-medium text-white transition-colors hover:bg-blue-600"
|
||||
>
|
||||
+ 샘플 화면 추가
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={clearNodes}
|
||||
className="w-full rounded-lg bg-red-500 p-3 font-medium text-white transition-colors hover:bg-red-600"
|
||||
>
|
||||
전체 삭제
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => onSave && onSave([])}
|
||||
className="w-full rounded-lg bg-green-500 p-3 font-medium text-white transition-colors hover:bg-green-600"
|
||||
>
|
||||
저장
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 통계 정보 */}
|
||||
<div className="mt-6 rounded-lg bg-gray-50 p-4">
|
||||
<div className="mb-2 text-sm font-semibold text-gray-700">통계</div>
|
||||
<div className="space-y-1 text-sm text-gray-600">
|
||||
<div className="flex justify-between">
|
||||
<span>화면 노드:</span>
|
||||
<span className="font-medium">{nodes.length}개</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>연결:</span>
|
||||
<span className="font-medium">{edges.length}개</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 선택된 필드 정보 */}
|
||||
{selectedField && (
|
||||
<div className="mt-6 rounded-lg border border-yellow-200 bg-yellow-50 p-4">
|
||||
<div className="mb-2 text-sm font-semibold text-yellow-800">선택된 필드</div>
|
||||
<div className="text-sm text-yellow-700">
|
||||
<div>화면: {selectedField.screenId}</div>
|
||||
<div>필드: {selectedField.fieldName}</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setSelectedField(null)}
|
||||
className="mt-2 text-xs text-yellow-600 hover:text-yellow-800"
|
||||
>
|
||||
선택 해제
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* React Flow 캔버스 */}
|
||||
<div className="relative flex-1">
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={onConnect}
|
||||
nodeTypes={nodeTypes}
|
||||
edgeTypes={edgeTypes}
|
||||
fitView
|
||||
attributionPosition="bottom-left"
|
||||
>
|
||||
<Controls />
|
||||
<MiniMap
|
||||
nodeColor={(node) => {
|
||||
switch (node.type) {
|
||||
case "screenNode":
|
||||
return "#3B82F6";
|
||||
default:
|
||||
return "#6B7280";
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Background variant="dots" gap={20} size={1} color="#E5E7EB" />
|
||||
</ReactFlow>
|
||||
|
||||
{/* 안내 메시지 */}
|
||||
{nodes.length === 0 && (
|
||||
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
||||
<div className="text-center text-gray-500">
|
||||
<div className="mb-2 text-2xl">📊</div>
|
||||
<div className="mb-1 text-lg font-medium">데이터 흐름 설계를 시작하세요</div>
|
||||
<div className="text-sm">왼쪽 사이드바에서 "샘플 화면 추가" 버튼을 클릭하세요</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -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 (
|
||||
<div className="max-w-96 min-w-80 rounded-lg border-2 border-gray-300 bg-white shadow-lg transition-shadow hover:shadow-xl">
|
||||
{/* 노드 헤더 */}
|
||||
<div className="rounded-t-lg bg-gradient-to-r from-blue-500 to-blue-600 p-4 text-white">
|
||||
<div className="mb-1 text-base font-bold">{screen.screenName}</div>
|
||||
<div className="mb-1 text-sm opacity-90">ID: {screen.screenCode}</div>
|
||||
<div className="flex items-center text-xs opacity-75">
|
||||
<span className="mr-1">🗃️</span>
|
||||
테이블: {screen.tableName}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 필드 목록 */}
|
||||
<div className="p-4">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<div className="text-sm font-semibold text-gray-700">필드 목록</div>
|
||||
<div className="rounded-full bg-gray-100 px-2 py-1 text-xs text-gray-500">{screen.fields.length}개</div>
|
||||
</div>
|
||||
|
||||
<div className="max-h-64 space-y-2 overflow-y-auto">
|
||||
{screen.fields.map((field, index) => (
|
||||
<div
|
||||
key={field.name}
|
||||
className="flex cursor-pointer items-center justify-between rounded-lg border border-transparent p-3 transition-colors hover:border-gray-200 hover:bg-gray-50"
|
||||
onClick={() => onFieldClick(screen.screenId, field.name)}
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="mb-1 flex items-center">
|
||||
<div className="truncate text-sm font-medium text-gray-900">{field.name}</div>
|
||||
{index === 0 && (
|
||||
<span className="ml-2 rounded bg-yellow-100 px-1.5 py-0.5 text-xs text-yellow-800">PK</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="truncate text-xs text-gray-500">{field.description}</div>
|
||||
</div>
|
||||
<div className={`ml-2 rounded px-2 py-1 font-mono text-xs ${getFieldTypeColor(field.type)}`}>
|
||||
{field.type}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* React Flow 핸들 */}
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
className="h-4 w-4 border-2 border-white bg-blue-500 shadow-md transition-colors hover:bg-blue-600"
|
||||
title="연결 시작점"
|
||||
/>
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
className="h-4 w-4 border-2 border-white bg-green-500 shadow-md transition-colors hover:bg-green-600"
|
||||
title="연결 도착점"
|
||||
/>
|
||||
|
||||
{/* 추가 핸들들 (상하) */}
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Bottom}
|
||||
className="h-4 w-4 border-2 border-white bg-blue-500 shadow-md transition-colors hover:bg-blue-600"
|
||||
id="bottom-source"
|
||||
/>
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Top}
|
||||
className="h-4 w-4 border-2 border-white bg-green-500 shadow-md transition-colors hover:bg-green-600"
|
||||
id="top-target"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -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 <Cog className="h-4 w-4" />;
|
||||
}
|
||||
if (MENU_ICONS.DATAFLOW.some((keyword) => menuName.includes(keyword))) {
|
||||
return <GitBranch className="h-4 w-4" />;
|
||||
}
|
||||
return <FileText className="h-4 w-4" />;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in New Issue