데이터 저장까지 구현 #30

Merged
hyeonsu merged 11 commits from dataflowMng into dev 2025-09-16 17:57:45 +09:00
8 changed files with 1079 additions and 1481 deletions
Showing only changes of commit 4ccce97eef - Show all commits

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,108 @@
"use client";
import React from "react";
import { TableSelector } from "./TableSelector";
import { TableDefinition } from "@/lib/api/dataflow";
import { ExtendedJsonRelationship } from "@/types/dataflowTypes";
interface DataFlowSidebarProps {
companyCode: string;
nodes: Array<{ id: string; data: { table: { tableName: string } } }>;
edges: Array<{ id: string }>;
tempRelationships: ExtendedJsonRelationship[];
hasUnsavedChanges: boolean;
currentDiagramId: number | null;
currentDiagramCategory: string;
onTableAdd: (table: TableDefinition) => void;
onRemoveOrphanedNodes: () => void;
onClearAll: () => void;
onOpenSaveModal: () => void;
getSelectedTableNames: () => string[];
}
export const DataFlowSidebar: React.FC<DataFlowSidebarProps> = ({
companyCode,
nodes,
edges,
tempRelationships,
hasUnsavedChanges,
currentDiagramId,
currentDiagramCategory,
onTableAdd,
onRemoveOrphanedNodes,
onClearAll,
onOpenSaveModal,
getSelectedTableNames,
}) => {
return (
<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>
{/* 테이블 선택기 */}
<TableSelector companyCode={companyCode} onTableAdd={onTableAdd} selectedTables={getSelectedTableNames()} />
{/* 컨트롤 버튼들 */}
<div className="space-y-3">
<button
onClick={onRemoveOrphanedNodes}
className="w-full rounded-lg bg-orange-500 p-3 font-medium text-white transition-colors hover:bg-orange-600"
disabled={nodes.length === 0}
>
🧹
</button>
<button
onClick={onClearAll}
className="w-full rounded-lg bg-red-500 p-3 font-medium text-white transition-colors hover:bg-red-600"
>
🗑
</button>
<button
onClick={onOpenSaveModal}
className={`w-full rounded-lg bg-green-500 p-3 font-medium text-white transition-colors hover:bg-green-600 ${
hasUnsavedChanges ? "animate-pulse" : ""
}`}
>
💾 {tempRelationships.length > 0 && `(${tempRelationships.length})`}
</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 className="flex justify-between">
<span> :</span>
<span className="font-medium text-orange-600">{tempRelationships.length}</span>
</div>
<div className="flex justify-between">
<span> ID:</span>
<span className="font-medium">{currentDiagramId || "미설정"}</span>
</div>
<div className="flex justify-between">
<span> :</span>
<span className="font-medium">
{currentDiagramCategory === "simple-key" && "단순 키값"}
{currentDiagramCategory === "data-save" && "데이터 저장"}
{currentDiagramCategory === "external-call" && "외부 호출"}
</span>
</div>
{hasUnsavedChanges && (
<div className="mt-2 text-xs font-medium text-orange-600"> </div>
)}
</div>
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,127 @@
"use client";
import React from "react";
import { SelectedEdgeInfo } from "@/types/dataflowTypes";
interface EdgeInfoPanelProps {
isOpen: boolean;
edgeInfo: SelectedEdgeInfo | null;
position: { x: number; y: number };
onClose: () => void;
onEdit: () => void;
onDelete: () => void;
}
export const EdgeInfoPanel: React.FC<EdgeInfoPanelProps> = ({
isOpen,
edgeInfo,
position,
onClose,
onEdit,
onDelete,
}) => {
if (!isOpen || !edgeInfo) return null;
return (
<div
className="fixed z-50 rounded-xl border border-gray-200 bg-white shadow-2xl"
style={{
left: position.x - 160,
top: position.y - 100,
minWidth: "320px",
maxWidth: "380px",
}}
>
{/* 헤더 */}
<div className="flex items-center justify-between rounded-t-xl border-b border-gray-200 bg-blue-600 p-4">
<div className="flex items-center gap-3">
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-white/20 backdrop-blur-sm">
<span className="text-sm text-white">🔗</span>
</div>
<div>
<div className="text-sm font-bold text-white">{edgeInfo.relationshipName}</div>
<div className="text-xs text-blue-100"> </div>
</div>
</div>
<button
onClick={onClose}
className="flex h-6 w-6 items-center justify-center rounded-full text-white/80 transition-all hover:bg-white/20 hover:text-white"
>
<span className="text-sm"></span>
</button>
</div>
{/* 관계 정보 요약 */}
<div className="border-b border-gray-100 bg-gray-50 p-3">
<div className="flex items-center justify-center gap-4">
<div className="text-center">
<div className="text-xs font-medium tracking-wide text-gray-500 uppercase"> </div>
<div className="mt-1 inline-flex items-center rounded-full bg-indigo-100 px-2 py-0.5 text-xs font-semibold text-indigo-800">
{edgeInfo.connectionType}
</div>
</div>
</div>
</div>
{/* 연결 정보 */}
<div className="space-y-3 p-4">
{/* From 테이블 */}
<div className="rounded-lg border-l-4 border-emerald-400 bg-emerald-50 p-3">
<div className="mb-2 text-xs font-bold tracking-wide text-emerald-700 uppercase">FROM</div>
<div className="mb-2 text-base font-bold text-gray-800">{edgeInfo.fromTable}</div>
<div className="space-y-1">
<div className="flex flex-wrap gap-2">
{edgeInfo.fromColumns.map((column, index) => (
<span
key={index}
className="inline-flex items-center rounded-md bg-emerald-100 px-2.5 py-0.5 text-xs font-medium text-emerald-800 ring-1 ring-emerald-200"
>
{column}
</span>
))}
</div>
</div>
</div>
{/* 관계 화살표 */}
<div className="flex justify-center">
<span className="text-l text-gray-600"></span>
</div>
{/* To 테이블 */}
<div className="rounded-lg border-l-4 border-blue-400 bg-blue-50 p-3">
<div className="mb-2 text-xs font-bold tracking-wide text-blue-700 uppercase">TO</div>
<div className="mb-2 text-base font-bold text-gray-800">{edgeInfo.toTable}</div>
<div className="space-y-1">
<div className="flex flex-wrap gap-2">
{edgeInfo.toColumns.map((column, index) => (
<span
key={index}
className="inline-flex items-center rounded-md bg-blue-100 px-2.5 py-0.5 text-xs font-medium text-blue-800 ring-1 ring-blue-200"
>
{column}
</span>
))}
</div>
</div>
</div>
</div>
{/* 액션 버튼 */}
<div className="flex gap-2 border-t border-gray-200 bg-gray-50 p-3">
<button
onClick={onEdit}
className="flex flex-1 items-center justify-center gap-1 rounded-lg bg-blue-600 px-3 py-2 text-xs font-semibold text-white shadow-sm transition-all hover:bg-blue-700 hover:shadow-md"
>
<span></span>
</button>
<button
onClick={onDelete}
className="flex flex-1 items-center justify-center gap-1 rounded-lg bg-red-600 px-3 py-2 text-xs font-semibold text-white shadow-sm transition-all hover:bg-red-700 hover:shadow-md"
>
<span></span>
</button>
</div>
</div>
);
};

View File

@ -0,0 +1,211 @@
"use client";
import React from "react";
import { ExtendedJsonRelationship, TableNodeData } from "@/types/dataflowTypes";
import { DataFlowAPI } from "@/lib/api/dataflow";
interface RelationshipListModalProps {
isOpen: boolean;
relationships: ExtendedJsonRelationship[];
nodes: Array<{ id: string; data: TableNodeData }>;
diagramId?: number;
companyCode: string;
editingRelationshipId: string | null;
onClose: () => void;
onEdit: (relationship: ExtendedJsonRelationship) => void;
onDelete: (relationshipId: string) => void;
onSetEditingId: (id: string | null) => void;
onSetSelectedColumns: (columns: { [tableName: string]: string[] }) => void;
onSetPendingConnection: (connection: any) => void;
}
export const RelationshipListModal: React.FC<RelationshipListModalProps> = ({
isOpen,
relationships,
nodes,
diagramId,
companyCode,
editingRelationshipId,
onClose,
onEdit,
onDelete,
onSetEditingId,
onSetSelectedColumns,
onSetPendingConnection,
}) => {
if (!isOpen) return null;
const handleEdit = async (relationship: ExtendedJsonRelationship) => {
// 관계 선택 시 수정 모드로 전환
onSetEditingId(relationship.id);
// 관련 컬럼 하이라이트
const newSelectedColumns: { [tableName: string]: string[] } = {};
if (relationship.fromTable && relationship.fromColumns) {
newSelectedColumns[relationship.fromTable] = [...relationship.fromColumns];
}
if (relationship.toTable && relationship.toColumns) {
newSelectedColumns[relationship.toTable] = [...relationship.toColumns];
}
onSetSelectedColumns(newSelectedColumns);
// 🔥 수정: 데이터베이스에서 관계 설정 정보 로드
let relationshipSettings = {};
if (diagramId && diagramId > 0) {
try {
const jsonDiagram = await DataFlowAPI.getJsonDataFlowDiagramById(diagramId, companyCode);
if (jsonDiagram && relationship.connectionType === "data-save") {
const control = jsonDiagram.control?.find((c) => c.id === relationship.id);
const plan = jsonDiagram.plan?.find((p) => p.id === relationship.id);
relationshipSettings = {
control: control
? {
triggerType: control.triggerType,
conditionTree: control.conditions || [],
}
: undefined,
actions: plan ? plan.actions || [] : [],
};
}
} catch (error) {
console.error("관계 설정 정보 로드 실패:", error);
}
}
// 연결 설정 모달 열기
const fromTable = nodes.find((node) => node.data?.table?.tableName === relationship.fromTable);
const toTable = nodes.find((node) => node.data?.table?.tableName === relationship.toTable);
if (fromTable && toTable) {
onSetPendingConnection({
fromNode: {
id: fromTable.id,
tableName: relationship.fromTable,
displayName: fromTable.data?.table?.displayName || relationship.fromTable,
},
toNode: {
id: toTable.id,
tableName: relationship.toTable,
displayName: toTable.data?.table?.displayName || relationship.toTable,
},
selectedColumnsData: {
[relationship.fromTable]: {
displayName: fromTable.data?.table?.displayName || relationship.fromTable,
columns: relationship.fromColumns || [],
},
[relationship.toTable]: {
displayName: toTable.data?.table?.displayName || relationship.toTable,
columns: relationship.toColumns || [],
},
},
existingRelationship: {
relationshipName: relationship.relationshipName,
connectionType: relationship.connectionType,
settings: relationshipSettings,
},
});
}
// 모달 닫기
onClose();
};
const handleDelete = (relationship: ExtendedJsonRelationship) => {
onDelete(relationship.id);
// 선택된 컬럼 초기화
onSetSelectedColumns({});
// 편집 모드 해제
if (editingRelationshipId === relationship.id) {
onSetEditingId(null);
}
// 모달 닫기
onClose();
};
return (
<div className="pointer-events-auto absolute top-4 right-4 z-40 w-80 rounded-xl border border-blue-200 bg-white shadow-lg">
{/* 헤더 */}
<div className="flex items-center justify-between rounded-t-xl border-b border-blue-100 bg-gradient-to-r from-blue-50 to-indigo-50 p-3">
<div className="flex items-center gap-2">
<div className="rounded-full bg-blue-100 p-1">
<span className="text-sm text-blue-600">🔗</span>
</div>
<div className="text-sm font-semibold text-gray-800"> </div>
</div>
<button
onClick={onClose}
className="flex h-6 w-6 items-center justify-center rounded-full text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-600"
>
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
{/* 관계 목록 */}
<div className="p-3">
<div className="max-h-96 space-y-2 overflow-y-auto">
{relationships.map((relationship) => (
<div
key={relationship.id}
className="rounded-lg border border-gray-200 p-3 transition-all hover:border-blue-300 hover:bg-blue-50"
>
<div className="mb-1 flex items-center justify-between">
<h4 className="text-sm font-medium text-gray-900">
{relationship.fromTable} {relationship.toTable}
</h4>
<div className="flex items-center gap-1">
{/* 편집 버튼 */}
<button
onClick={(e) => {
e.stopPropagation();
handleEdit(relationship);
}}
className="flex h-6 w-6 items-center justify-center rounded text-gray-400 hover:bg-blue-100 hover:text-blue-600"
title="관계 편집"
>
<svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
/>
</svg>
</button>
{/* 삭제 버튼 */}
<button
onClick={(e) => {
e.stopPropagation();
handleDelete(relationship);
}}
className="flex h-6 w-6 items-center justify-center rounded text-gray-400 hover:bg-red-100 hover:text-red-600"
title="관계 삭제"
>
<svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
/>
</svg>
</button>
</div>
</div>
<div className="space-y-1 text-xs text-gray-600">
<p>: {relationship.connectionType}</p>
<p>From: {relationship.fromTable}</p>
<p>To: {relationship.toTable}</p>
</div>
</div>
))}
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,130 @@
"use client";
import React from "react";
import { TableNodeData } from "@/types/dataflowTypes";
interface SelectedTablesPanelProps {
selectedNodes: string[];
nodes: Array<{
id: string;
data: TableNodeData;
}>;
onClose: () => void;
onOpenConnectionModal: () => void;
onClear: () => void;
canCreateConnection: boolean;
}
export const SelectedTablesPanel: React.FC<SelectedTablesPanelProps> = ({
selectedNodes,
nodes,
onClose,
onOpenConnectionModal,
onClear,
canCreateConnection,
}) => {
return (
<div className="pointer-events-auto absolute top-4 left-4 z-40 w-80 rounded-xl border border-blue-200 bg-white shadow-lg">
{/* 헤더 */}
<div className="flex items-center justify-between rounded-t-xl border-b border-blue-100 bg-gradient-to-r from-blue-50 to-indigo-50 p-3">
<div className="flex items-center gap-2">
<div className="flex h-6 w-6 items-center justify-center rounded-full bg-blue-100">
<span className="text-sm text-blue-600">📋</span>
</div>
<div>
<div className="text-sm font-semibold text-gray-800"> </div>
<div className="text-xs text-gray-500">
{selectedNodes.length === 1
? "FROM 테이블 선택됨"
: selectedNodes.length === 2
? "FROM → TO 연결 준비"
: `${selectedNodes.length}개 테이블`}
</div>
</div>
</div>
<button
onClick={onClose}
className="flex h-5 w-5 items-center justify-center rounded-full text-gray-400 hover:bg-gray-100 hover:text-gray-600"
>
</button>
</div>
{/* 컨텐츠 */}
<div className="max-h-80 overflow-y-auto p-3">
<div className="space-y-3">
{selectedNodes.map((nodeId, index) => {
const node = nodes.find((n) => n.id === nodeId);
if (!node) return null;
const { tableName, displayName } = node.data.table;
return (
<div key={`selected-${nodeId}-${index}`}>
{/* 테이블 정보 */}
<div
className={`rounded-lg p-2 ${
index === 0
? "border-l-4 border-emerald-400 bg-emerald-50"
: index === 1
? "border-l-4 border-blue-400 bg-blue-50"
: "bg-gray-50"
}`}
>
<div className="mb-1 flex items-center justify-between">
<div
className={`text-xs font-medium ${
index === 0 ? "text-emerald-700" : index === 1 ? "text-blue-700" : "text-gray-700"
}`}
>
{displayName}
</div>
{selectedNodes.length === 2 && (
<div
className={`rounded-full px-2 py-0.5 text-xs font-bold ${
index === 0 ? "bg-emerald-200 text-emerald-800" : "bg-blue-200 text-blue-800"
}`}
>
{index === 0 ? "FROM" : "TO"}
</div>
)}
</div>
<div className="text-xs text-gray-600">{tableName}</div>
</div>
{/* 연결 화살표 (마지막이 아닌 경우) */}
{index < selectedNodes.length - 1 && (
<div className="flex justify-center py-1">
<div className="text-gray-400"></div>
</div>
)}
</div>
);
})}
</div>
</div>
{/* 액션 버튼 */}
<div className="flex gap-2 border-t border-blue-100 p-3">
<button
onClick={onOpenConnectionModal}
disabled={!canCreateConnection}
className={`flex flex-1 items-center justify-center gap-1 rounded-lg px-3 py-2 text-xs font-medium transition-colors ${
canCreateConnection
? "bg-blue-500 text-white hover:bg-blue-600"
: "cursor-not-allowed bg-gray-300 text-gray-500"
}`}
>
<span>🔗</span>
<span> </span>
</button>
<button
onClick={onClear}
className="flex flex-1 items-center justify-center gap-1 rounded-lg bg-gray-200 px-3 py-2 text-xs font-medium text-gray-600 hover:bg-gray-300"
>
<span>🗑</span>
<span></span>
</button>
</div>
</div>
);
};

View File

@ -0,0 +1,93 @@
"use client";
import { useState, useCallback, useRef } from "react";
import { Node, Edge, useNodesState, useEdgesState } from "@xyflow/react";
import { TableNodeData, ExtendedJsonRelationship, ConnectionInfo, SelectedEdgeInfo } from "@/types/dataflowTypes";
export const useDataFlowDesigner = () => {
const [nodes, setNodes, onNodesChange] = useNodesState<Node<TableNodeData>>([]);
const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([]);
// 상태 관리
const [selectedColumns, setSelectedColumns] = useState<{ [tableName: string]: string[] }>({});
const [selectedNodes, setSelectedNodes] = useState<string[]>([]);
const [pendingConnection, setPendingConnection] = useState<ConnectionInfo | null>(null);
const [relationships, setRelationships] = useState<any[]>([]); // eslint-disable-line @typescript-eslint/no-unused-vars
const [currentDiagramId, setCurrentDiagramId] = useState<number | null>(null);
const [selectedEdgeInfo, setSelectedEdgeInfo] = useState<SelectedEdgeInfo | null>(null);
// 메모리 기반 상태들
const [tempRelationships, setTempRelationships] = useState<ExtendedJsonRelationship[]>([]);
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
const [showSaveModal, setShowSaveModal] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [currentDiagramName, setCurrentDiagramName] = useState<string>("");
const [currentDiagramCategory, setCurrentDiagramCategory] = useState<string>("simple-key");
const [selectedEdgeForEdit, setSelectedEdgeForEdit] = useState<Edge | null>(null);
const [showEdgeActions, setShowEdgeActions] = useState(false);
const [edgeActionPosition] = useState<{ x: number; y: number }>({ x: 0, y: 0 });
const [editingRelationshipId, setEditingRelationshipId] = useState<string | null>(null);
const [showRelationshipListModal, setShowRelationshipListModal] = useState(false);
const [selectedTablePairRelationships, setSelectedTablePairRelationships] = useState<ExtendedJsonRelationship[]>([]);
const toastShownRef = useRef(false); // eslint-disable-line @typescript-eslint/no-unused-vars
return {
// Node & Edge states
nodes,
setNodes,
onNodesChange,
edges,
setEdges,
onEdgesChange,
// Selection states
selectedColumns,
setSelectedColumns,
selectedNodes,
setSelectedNodes,
// Connection states
pendingConnection,
setPendingConnection,
relationships,
setRelationships,
// Diagram states
currentDiagramId,
setCurrentDiagramId,
currentDiagramName,
setCurrentDiagramName,
currentDiagramCategory,
setCurrentDiagramCategory,
// Memory-based states
tempRelationships,
setTempRelationships,
hasUnsavedChanges,
setHasUnsavedChanges,
// Modal states
showSaveModal,
setShowSaveModal,
isSaving,
setIsSaving,
showRelationshipListModal,
setShowRelationshipListModal,
selectedTablePairRelationships,
setSelectedTablePairRelationships,
// Edge states
selectedEdgeInfo,
setSelectedEdgeInfo,
selectedEdgeForEdit,
setSelectedEdgeForEdit,
showEdgeActions,
setShowEdgeActions,
edgeActionPosition,
editingRelationshipId,
setEditingRelationshipId,
// Refs
toastShownRef,
};
};

View File

@ -0,0 +1,117 @@
import { JsonRelationship, TableRelationship, DataFlowDiagram } from "@/lib/api/dataflow";
// 테이블 노드 데이터 타입 정의
export interface TableNodeData extends Record<string, unknown> {
table: {
tableName: string;
displayName: string;
description: string;
columns: Array<{
name: string;
type: string;
description: string;
}>;
};
onColumnClick: (tableName: string, columnName: string) => void;
selectedColumns: string[];
connectedColumns?: {
[columnName: string]: { direction: "source" | "target" | "both" };
};
}
// 내부에서 사용할 확장된 JsonRelationship 타입 (connectionType 포함)
export interface ExtendedJsonRelationship extends JsonRelationship {
connectionType: "simple-key" | "data-save" | "external-call";
settings?: {
control?: {
triggerType?: "insert" | "update" | "delete";
conditionTree?: Array<{
id: string;
type: string;
field?: string;
operator?: string;
value?: unknown;
logicalOperator?: string;
groupId?: string;
groupLevel?: number;
}>;
};
actions?: Array<{
id: string;
name: string;
actionType: "insert" | "update" | "delete" | "upsert";
conditions?: Array<{
id: string;
type: string;
field?: string;
operator?: string;
value?: unknown;
logicalOperator?: string;
groupId?: string;
groupLevel?: number;
}>;
fieldMappings: Array<{
sourceTable?: string;
sourceField: string;
targetTable?: string;
targetField: string;
defaultValue?: string;
}>;
splitConfig?: {
sourceField: string;
delimiter: string;
targetField: string;
};
}>;
notes?: string;
apiCall?: {
url: string;
method: string;
headers: Array<{ key: string; value: string }>;
body: string;
successCriteria: string;
};
};
}
// DataFlowDesigner Props 타입
export interface DataFlowDesignerProps {
companyCode?: string;
onSave?: (relationships: TableRelationship[]) => void;
selectedDiagram?: DataFlowDiagram | string | null;
diagramId?: number;
relationshipId?: string; // 하위 호환성 유지
onBackToList?: () => void;
onDiagramNameUpdate?: (diagramName: string) => void;
}
// 연결 정보 타입
export interface ConnectionInfo {
fromNode: { id: string; tableName: string; displayName: string };
toNode: { id: string; tableName: string; displayName: string };
fromColumn?: string;
toColumn?: string;
selectedColumnsData?: {
[tableName: string]: {
displayName: string;
columns: string[];
};
};
existingRelationship?: {
relationshipName: string;
connectionType: string;
settings?: Record<string, unknown>;
};
}
// 선택된 엣지 정보 타입
export interface SelectedEdgeInfo {
relationshipId: string;
relationshipName: string;
fromTable: string;
toTable: string;
fromColumns: string[];
toColumns: string[];
connectionType: string;
connectionInfo: string;
}

View File

@ -0,0 +1,104 @@
// 데이터플로우 관련 유틸리티 함수들
/**
* ID
*/
export const generateUniqueId = (prefix: string, diagramId?: number): string => {
const timestamp = Date.now();
const random = Math.random().toString(36).substr(2, 9);
return `${prefix}-${diagramId || timestamp}-${random}`;
};
/**
*
*/
export const calculateTableRelationshipCount = (relationships: Array<{ fromTable: string; toTable: string }>) => {
const tableRelationshipCount: { [key: string]: number } = {};
relationships.forEach((rel) => {
const tableKey = [rel.fromTable, rel.toTable].sort().join("-");
tableRelationshipCount[tableKey] = (tableRelationshipCount[tableKey] || 0) + 1;
});
return tableRelationshipCount;
};
/**
*
*/
export const calculateConnectedColumns = (
relationships: Array<{
fromTable: string;
toTable: string;
fromColumns: string[];
toColumns: string[];
}>,
) => {
const connectedColumnsInfo: {
[tableName: string]: { [columnName: string]: { direction: "source" | "target" | "both" } };
} = {};
relationships.forEach((rel) => {
const { fromTable, toTable, fromColumns, toColumns } = rel;
// 소스 테이블의 컬럼들을 source로 표시
if (!connectedColumnsInfo[fromTable]) connectedColumnsInfo[fromTable] = {};
fromColumns.forEach((col: string) => {
if (connectedColumnsInfo[fromTable][col]) {
connectedColumnsInfo[fromTable][col].direction = "both";
} else {
connectedColumnsInfo[fromTable][col] = { direction: "source" };
}
});
// 타겟 테이블의 컬럼들을 target으로 표시
if (!connectedColumnsInfo[toTable]) connectedColumnsInfo[toTable] = {};
toColumns.forEach((col: string) => {
if (connectedColumnsInfo[toTable][col]) {
connectedColumnsInfo[toTable][col].direction = "both";
} else {
connectedColumnsInfo[toTable][col] = { direction: "target" };
}
});
});
return connectedColumnsInfo;
};
/**
*
*/
export const extractNodePositions = (
nodes: Array<{
data: { table: { tableName: string } };
position: { x: number; y: number };
}>,
): { [tableName: string]: { x: number; y: number } } => {
const nodePositions: { [tableName: string]: { x: number; y: number } } = {};
nodes.forEach((node) => {
if (node.data?.table?.tableName) {
nodePositions[node.data.table.tableName] = {
x: node.position.x,
y: node.position.y,
};
}
});
return nodePositions;
};
/**
*
*/
export const extractTableNames = (
nodes: Array<{
id: string;
data: { table: { tableName: string } };
}>,
): string[] => {
return nodes
.filter((node) => node.id.startsWith("table-"))
.map((node) => node.data.table.tableName)
.sort();
};