DataFlowDesigner 컴포넌트 정리
This commit is contained in:
parent
934e4d25af
commit
4ccce97eef
File diff suppressed because it is too large
Load Diff
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue