데이터 저장까지 구현 #30
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