Merge pull request 'dataflowMng' (#27) from dataflowMng into dev

Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/27
This commit is contained in:
hyeonsu 2025-09-11 11:44:01 +09:00
commit 867fb57741
2 changed files with 153 additions and 145 deletions

View File

@ -784,6 +784,8 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
// 수정 모드였다면 해제
if (editingRelationshipId) {
setEditingRelationshipId(null);
// 편집 모드 취소 시 선택된 컬럼도 초기화
setSelectedColumns({});
}
}, [editingRelationshipId]);
@ -1136,98 +1138,101 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
<Background variant={BackgroundVariant.Dots} gap={20} size={1} color="#E5E7EB" />
</ReactFlow>
{/* 선택된 컬럼 팝업 - 캔버스 좌측 상단 고정 (신규 관계도 생성 시에만 표시) */}
{Object.keys(selectedColumns).length > 0 && !showEdgeActions && !pendingConnection && !diagramId && (
<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">{Object.keys(selectedColumns).length} </div>
{/* 선택된 컬럼 팝업 - 캔버스 좌측 상단 고정 (새 관계 생성 시에만 표시) */}
{Object.keys(selectedColumns).length > 0 &&
!showEdgeActions &&
!pendingConnection &&
!editingRelationshipId && (
<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">{Object.keys(selectedColumns).length} </div>
</div>
</div>
<button
onClick={() => {
setSelectedColumns({});
setSelectionOrder([]);
}}
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>
<button
onClick={() => {
setSelectedColumns({});
setSelectionOrder([]);
}}
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">
{[...new Set(selectionOrder)]
.filter((tableName) => selectedColumns[tableName] && selectedColumns[tableName].length > 0)
.map((tableName, index, filteredOrder) => {
const columns = selectedColumns[tableName];
const node = nodes.find((n) => n.data.table.tableName === tableName);
const displayName = node?.data.table.displayName || tableName;
{/* 컨텐츠 */}
<div className="max-h-80 overflow-y-auto p-3">
<div className="space-y-3">
{[...new Set(selectionOrder)]
.filter((tableName) => selectedColumns[tableName] && selectedColumns[tableName].length > 0)
.map((tableName, index, filteredOrder) => {
const columns = selectedColumns[tableName];
const node = nodes.find((n) => n.data.table.tableName === tableName);
const displayName = node?.data.table.displayName || tableName;
return (
<div key={`selected-${tableName}-${index}`}>
{/* 테이블 정보 */}
<div className="rounded-lg bg-blue-50 p-2">
<div className="mb-1 text-xs font-medium text-blue-700">{displayName}</div>
<div className="flex flex-wrap gap-1">
{columns.map((column, columnIndex) => (
<span
key={`${tableName}-${column}-${columnIndex}`}
className="rounded bg-blue-100 px-2 py-1 text-xs text-blue-800"
title={column}
>
{column}
</span>
))}
return (
<div key={`selected-${tableName}-${index}`}>
{/* 테이블 정보 */}
<div className="rounded-lg bg-blue-50 p-2">
<div className="mb-1 text-xs font-medium text-blue-700">{displayName}</div>
<div className="flex flex-wrap gap-1">
{columns.map((column, columnIndex) => (
<span
key={`${tableName}-${column}-${columnIndex}`}
className="rounded bg-blue-100 px-2 py-1 text-xs text-blue-800"
title={column}
>
{column}
</span>
))}
</div>
</div>
{/* 화살표 */}
{index === 0 && filteredOrder.length > 1 && (
<div className="flex justify-center py-1">
<div className="text-sm text-gray-400"></div>
</div>
)}
</div>
);
})}
</div>
</div>
{/* 화살표 */}
{index === 0 && filteredOrder.length > 1 && (
<div className="flex justify-center py-1">
<div className="text-sm text-gray-400"></div>
</div>
)}
</div>
);
})}
{/* 액션 버튼 */}
<div className="flex gap-2 border-t border-blue-100 p-3">
<button
onClick={openConnectionModal}
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={() => {
setSelectedColumns({});
setSelectionOrder([]);
}}
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>
{/* 액션 버튼 */}
<div className="flex gap-2 border-t border-blue-100 p-3">
<button
onClick={openConnectionModal}
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={() => {
setSelectedColumns({});
setSelectionOrder([]);
}}
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>
)}
)}
{/* 안내 메시지 */}
{nodes.length === 0 && (
@ -1258,23 +1263,23 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
{/* 엣지 정보 및 액션 버튼 */}
{showEdgeActions && selectedEdgeForEdit && selectedEdgeInfo && (
<div
className="fixed z-50 rounded-xl border border-gray-200 bg-white shadow-xl"
className="fixed z-50 rounded-xl border border-gray-200 bg-white shadow-2xl"
style={{
left: edgeActionPosition.x - 160,
top: edgeActionPosition.y - 100,
minWidth: "320px",
maxWidth: "400px",
maxWidth: "380px",
}}
>
{/* 헤더 */}
<div className="flex items-center justify-between rounded-t-xl border-b border-gray-100 bg-gradient-to-r from-blue-50 to-indigo-50 p-4">
<div className="flex items-center gap-2">
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-blue-100">
<span className="text-blue-600">🔗</span>
<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="font-semibold text-gray-800">{selectedEdgeInfo.relationshipName}</div>
<div className="text-xs text-gray-500"> </div>
<div className="text-sm font-bold text-white">{selectedEdgeInfo.relationshipName}</div>
<div className="text-xs text-blue-100"> </div>
</div>
</div>
<button
@ -1284,76 +1289,87 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
setSelectedEdgeForEdit(null);
setSelectedColumns({});
}}
className="flex h-6 w-6 items-center justify-center rounded-full text-gray-400 hover:bg-gray-100 hover:text-gray-600"
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-blue-100 px-2 py-0.5 text-xs font-semibold text-blue-800">
{selectedEdgeInfo.relationshipType}
</div>
</div>
<div className="text-gray-300">|</div>
<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">
{selectedEdgeInfo.connectionType}
</div>
</div>
</div>
</div>
{/* 연결 정보 */}
<div className="space-y-3 p-4">
{/* From 테이블 */}
<div className="rounded-lg bg-green-50 p-3">
<div className="mb-1 text-xs font-medium text-green-700">From </div>
<div className="font-semibold text-gray-800">{selectedEdgeInfo.fromTable}</div>
<div className="mt-1 flex flex-wrap gap-1">
{selectedEdgeInfo.fromColumns.map((column, index) => (
<span key={index} className="rounded bg-green-100 px-2 py-1 text-xs text-green-800">
{column}
</span>
))}
<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">{selectedEdgeInfo.fromTable}</div>
<div className="space-y-1">
<div className="flex flex-wrap gap-2">
{selectedEdgeInfo.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">
<div className="flex items-center gap-2 text-gray-400">
<div className="h-px w-8 bg-gray-300"></div>
<span className="text-lg"></span>
<div className="h-px w-8 bg-gray-300"></div>
</div>
<span className="text-l text-gray-600"></span>
</div>
{/* To 테이블 */}
<div className="rounded-lg bg-blue-50 p-3">
<div className="mb-1 text-xs font-medium text-blue-700">To </div>
<div className="font-semibold text-gray-800">{selectedEdgeInfo.toTable}</div>
<div className="mt-1 flex flex-wrap gap-1">
{selectedEdgeInfo.toColumns.map((column, index) => (
<span key={index} className="rounded bg-blue-100 px-2 py-1 text-xs text-blue-800">
{column}
</span>
))}
</div>
</div>
{/* 관계 정보 */}
<div className="grid grid-cols-2 gap-2">
<div className="rounded-lg bg-gray-50 p-2">
<div className="text-xs font-medium text-gray-600"> </div>
<div className="text-sm font-semibold text-gray-800">{selectedEdgeInfo.relationshipType}</div>
</div>
<div className="rounded-lg bg-gray-50 p-2">
<div className="text-xs font-medium text-gray-600"> </div>
<div className="text-sm font-semibold text-gray-800">{selectedEdgeInfo.connectionType}</div>
<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">{selectedEdgeInfo.toTable}</div>
<div className="space-y-1">
<div className="flex flex-wrap gap-2">
{selectedEdgeInfo.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-100 p-4">
<div className="flex gap-2 border-t border-gray-200 bg-gray-50 p-3">
<button
onClick={handleEditEdge}
className="flex flex-1 items-center justify-center gap-2 rounded-lg bg-blue-500 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-600"
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>
<span></span>
</button>
<button
onClick={handleDeleteEdge}
className="flex flex-1 items-center justify-center gap-2 rounded-lg bg-red-500 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-red-600"
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>
<span></span>
</button>
</div>

View File

@ -22,18 +22,10 @@ interface TableNodeData {
onScrollAreaEnter?: () => void;
onScrollAreaLeave?: () => void;
selectedColumns?: string[]; // 선택된 컬럼 목록
connectedColumns?: { [columnName: string]: { direction: "source" | "target" | "both" } }; // 연결된 컬럼 정보
}
export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => {
const {
table,
onColumnClick,
onScrollAreaEnter,
onScrollAreaLeave,
selectedColumns = [],
connectedColumns = {},
} = data;
const { table, onColumnClick, onScrollAreaEnter, onScrollAreaLeave, selectedColumns = [] } = data;
return (
<div className="relative flex min-w-[280px] flex-col overflow-hidden rounded-lg border-2 border-gray-300 bg-white shadow-lg">
@ -42,7 +34,7 @@ export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => {
<Handle type="source" position={Position.Right} id="right" className="!invisible !h-1 !w-1" />
{/* 테이블 헤더 - 통일된 디자인 */}
<div className="rounded-t-lg bg-blue-600 p-3 text-white">
<div className="bg-blue-600 p-3 text-white">
<h3 className="truncate text-sm font-semibold">{table.displayName}</h3>
{table.description && <p className="mt-1 truncate text-xs opacity-75">{table.description}</p>}
</div>
@ -50,7 +42,7 @@ export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => {
{/* 컬럼 목록 */}
<div className="flex-1 overflow-hidden p-2" onMouseEnter={onScrollAreaEnter} onMouseLeave={onScrollAreaLeave}>
<div className="space-y-1">
{table.columns.map((column, index) => {
{table.columns.map((column) => {
const isSelected = selectedColumns.includes(column.name);
return (