관계도 조회 구현
This commit is contained in:
parent
7260ad733b
commit
3a24fd3ebd
|
|
@ -62,7 +62,7 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
companyCode,
|
||||
onSave,
|
||||
selectedDiagram,
|
||||
onBackToList,
|
||||
onBackToList, // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
}) => {
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState<Node<TableNodeData>>([]);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([]);
|
||||
|
|
@ -84,7 +84,7 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
};
|
||||
} | null>(null);
|
||||
const [relationships, setRelationships] = useState<TableRelationship[]>([]); // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
const toastShownRef = useRef(false);
|
||||
const toastShownRef = useRef(false); // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
|
||||
// 키보드 이벤트 핸들러 (Del 키로 선택된 노드 삭제)
|
||||
useEffect(() => {
|
||||
|
|
@ -212,6 +212,38 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
}
|
||||
}
|
||||
|
||||
// 연결된 컬럼 정보 계산
|
||||
const connectedColumnsInfo: {
|
||||
[tableName: string]: { [columnName: string]: { direction: "source" | "target" | "both" } };
|
||||
} = {};
|
||||
|
||||
diagramRelationships.forEach((rel) => {
|
||||
const fromTable = rel.from_table_name;
|
||||
const toTable = rel.to_table_name;
|
||||
const fromColumns = rel.from_column_name.split(",").map((col) => col.trim());
|
||||
const toColumns = rel.to_column_name.split(",").map((col) => col.trim());
|
||||
|
||||
// 소스 테이블의 컬럼들을 source로 표시
|
||||
if (!connectedColumnsInfo[fromTable]) connectedColumnsInfo[fromTable] = {};
|
||||
fromColumns.forEach((col) => {
|
||||
if (connectedColumnsInfo[fromTable][col]) {
|
||||
connectedColumnsInfo[fromTable][col].direction = "both";
|
||||
} else {
|
||||
connectedColumnsInfo[fromTable][col] = { direction: "source" };
|
||||
}
|
||||
});
|
||||
|
||||
// 타겟 테이블의 컬럼들을 target으로 표시
|
||||
if (!connectedColumnsInfo[toTable]) connectedColumnsInfo[toTable] = {};
|
||||
toColumns.forEach((col) => {
|
||||
if (connectedColumnsInfo[toTable][col]) {
|
||||
connectedColumnsInfo[toTable][col].direction = "both";
|
||||
} else {
|
||||
connectedColumnsInfo[toTable][col] = { direction: "target" };
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 테이블을 노드로 변환 (자동 레이아웃)
|
||||
const tableNodes = tableDefinitions.map((table, index) => {
|
||||
const x = (index % 3) * 400 + 100; // 3열 배치
|
||||
|
|
@ -234,6 +266,7 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
},
|
||||
onColumnClick: handleColumnClick,
|
||||
selectedColumns: selectedColumns[table.tableName] || [],
|
||||
connectedColumns: connectedColumnsInfo[table.tableName] || {},
|
||||
} as TableNodeData,
|
||||
};
|
||||
});
|
||||
|
|
@ -242,23 +275,47 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
console.log("📍 테이블 노드 상세:", tableNodes);
|
||||
setNodes(tableNodes);
|
||||
|
||||
// 관계를 엣지로 변환하여 표시
|
||||
const relationshipEdges = diagramRelationships.map((rel) => ({
|
||||
id: generateUniqueId("edge", rel.relationship_id),
|
||||
source: `table-${rel.from_table_name}`,
|
||||
target: `table-${rel.to_table_name}`,
|
||||
sourceHandle: "right",
|
||||
targetHandle: "left",
|
||||
type: "default",
|
||||
data: {
|
||||
relationshipId: rel.relationship_id,
|
||||
relationshipType: rel.relationship_type,
|
||||
connectionType: rel.connection_type,
|
||||
label: rel.relationship_name,
|
||||
fromColumn: rel.from_column_name,
|
||||
toColumn: rel.to_column_name,
|
||||
},
|
||||
}));
|
||||
// 관계를 엣지로 변환하여 표시 (컬럼별 연결)
|
||||
const relationshipEdges: Edge[] = [];
|
||||
|
||||
diagramRelationships.forEach((rel) => {
|
||||
const fromTable = rel.from_table_name;
|
||||
const toTable = rel.to_table_name;
|
||||
const fromColumns = rel.from_column_name.split(",").map((col) => col.trim());
|
||||
const toColumns = rel.to_column_name.split(",").map((col) => col.trim());
|
||||
|
||||
// 각 from 컬럼을 각 to 컬럼에 연결 (1:1 매핑이거나 many:many인 경우)
|
||||
const maxConnections = Math.max(fromColumns.length, toColumns.length);
|
||||
|
||||
for (let i = 0; i < maxConnections; i++) {
|
||||
const fromColumn = fromColumns[i] || fromColumns[0]; // 컬럼이 부족하면 첫 번째 컬럼 재사용
|
||||
const toColumn = toColumns[i] || toColumns[0]; // 컬럼이 부족하면 첫 번째 컬럼 재사용
|
||||
|
||||
relationshipEdges.push({
|
||||
id: generateUniqueId("edge", rel.relationship_id),
|
||||
source: `table-${fromTable}`,
|
||||
target: `table-${toTable}`,
|
||||
sourceHandle: `${fromTable}-${fromColumn}-source`,
|
||||
targetHandle: `${toTable}-${toColumn}-target`,
|
||||
type: "smoothstep",
|
||||
animated: false,
|
||||
style: {
|
||||
stroke: "#3b82f6",
|
||||
strokeWidth: 1.5,
|
||||
strokeDasharray: "none",
|
||||
},
|
||||
data: {
|
||||
relationshipId: rel.relationship_id,
|
||||
relationshipType: rel.relationship_type,
|
||||
connectionType: rel.connection_type,
|
||||
fromTable: fromTable,
|
||||
toTable: toTable,
|
||||
fromColumn: fromColumn,
|
||||
toColumn: toColumn,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
console.log("🔗 생성된 관계 에지 수:", relationshipEdges.length);
|
||||
console.log("📍 관계 에지 상세:", relationshipEdges);
|
||||
|
|
@ -268,7 +325,7 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
console.error("선택된 관계도 로드 실패:", error);
|
||||
toast.error("관계도를 불러오는데 실패했습니다.", { id: "load-diagram" });
|
||||
}
|
||||
}, [selectedDiagram, companyCode, setNodes, setEdges, selectedColumns, handleColumnClick]);
|
||||
}, [selectedDiagram, setNodes, setEdges, selectedColumns, handleColumnClick]);
|
||||
|
||||
// 기존 관계 로드 (새 관계도 생성 시)
|
||||
const loadExistingRelationships = useCallback(async () => {
|
||||
|
|
@ -278,23 +335,47 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
const existingRelationships = await DataFlowAPI.getRelationshipsByCompany(companyCode);
|
||||
setRelationships(existingRelationships);
|
||||
|
||||
// 기존 관계를 엣지로 변환하여 표시
|
||||
const existingEdges = existingRelationships.map((rel) => ({
|
||||
id: generateUniqueId("edge", rel.relationship_id),
|
||||
source: `table-${rel.from_table_name}`,
|
||||
target: `table-${rel.to_table_name}`,
|
||||
sourceHandle: "right",
|
||||
targetHandle: "left",
|
||||
type: "default",
|
||||
data: {
|
||||
relationshipId: rel.relationship_id,
|
||||
relationshipType: rel.relationship_type,
|
||||
connectionType: rel.connection_type,
|
||||
label: rel.relationship_name,
|
||||
fromColumn: rel.from_column_name,
|
||||
toColumn: rel.to_column_name,
|
||||
},
|
||||
}));
|
||||
// 기존 관계를 엣지로 변환하여 표시 (컬럼별 연결)
|
||||
const existingEdges: Edge[] = [];
|
||||
|
||||
existingRelationships.forEach((rel) => {
|
||||
const fromTable = rel.from_table_name;
|
||||
const toTable = rel.to_table_name;
|
||||
const fromColumns = rel.from_column_name.split(",").map((col) => col.trim());
|
||||
const toColumns = rel.to_column_name.split(",").map((col) => col.trim());
|
||||
|
||||
// 각 from 컬럼을 각 to 컬럼에 연결
|
||||
const maxConnections = Math.max(fromColumns.length, toColumns.length);
|
||||
|
||||
for (let i = 0; i < maxConnections; i++) {
|
||||
const fromColumn = fromColumns[i] || fromColumns[0];
|
||||
const toColumn = toColumns[i] || toColumns[0];
|
||||
|
||||
existingEdges.push({
|
||||
id: generateUniqueId("edge", rel.relationship_id),
|
||||
source: `table-${fromTable}`,
|
||||
target: `table-${toTable}`,
|
||||
sourceHandle: `${fromTable}-${fromColumn}-source`,
|
||||
targetHandle: `${toTable}-${toColumn}-target`,
|
||||
type: "smoothstep",
|
||||
animated: false,
|
||||
style: {
|
||||
stroke: "#3b82f6",
|
||||
strokeWidth: 1.5,
|
||||
strokeDasharray: "none",
|
||||
},
|
||||
data: {
|
||||
relationshipId: rel.relationship_id,
|
||||
relationshipType: rel.relationship_type,
|
||||
connectionType: rel.connection_type,
|
||||
fromTable: fromTable,
|
||||
toTable: toTable,
|
||||
fromColumn: fromColumn,
|
||||
toColumn: toColumn,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
setEdges(existingEdges);
|
||||
} catch (error) {
|
||||
|
|
@ -488,24 +569,45 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
(relationship: TableRelationship) => {
|
||||
if (!pendingConnection) return;
|
||||
|
||||
const newEdge = {
|
||||
id: generateUniqueId("edge", relationship.relationship_id),
|
||||
source: pendingConnection.fromNode.id,
|
||||
target: pendingConnection.toNode.id,
|
||||
sourceHandle: "right",
|
||||
targetHandle: "left",
|
||||
type: "default",
|
||||
data: {
|
||||
relationshipId: relationship.relationship_id,
|
||||
relationshipType: relationship.relationship_type,
|
||||
connectionType: relationship.connection_type,
|
||||
label: relationship.relationship_name,
|
||||
fromColumn: relationship.from_column_name,
|
||||
toColumn: relationship.to_column_name,
|
||||
},
|
||||
};
|
||||
// 컬럼별 에지 생성
|
||||
const newEdges: Edge[] = [];
|
||||
const fromTable = relationship.from_table_name;
|
||||
const toTable = relationship.to_table_name;
|
||||
const fromColumns = relationship.from_column_name.split(",").map((col) => col.trim());
|
||||
const toColumns = relationship.to_column_name.split(",").map((col) => col.trim());
|
||||
|
||||
setEdges((eds) => [...eds, newEdge]);
|
||||
const maxConnections = Math.max(fromColumns.length, toColumns.length);
|
||||
|
||||
for (let i = 0; i < maxConnections; i++) {
|
||||
const fromColumn = fromColumns[i] || fromColumns[0];
|
||||
const toColumn = toColumns[i] || toColumns[0];
|
||||
|
||||
newEdges.push({
|
||||
id: generateUniqueId("edge", relationship.relationship_id),
|
||||
source: pendingConnection.fromNode.id,
|
||||
target: pendingConnection.toNode.id,
|
||||
sourceHandle: `${fromTable}-${fromColumn}-source`,
|
||||
targetHandle: `${toTable}-${toColumn}-target`,
|
||||
type: "smoothstep",
|
||||
animated: false,
|
||||
style: {
|
||||
stroke: "#3b82f6",
|
||||
strokeWidth: 1.5,
|
||||
strokeDasharray: "none",
|
||||
},
|
||||
data: {
|
||||
relationshipId: relationship.relationship_id,
|
||||
relationshipType: relationship.relationship_type,
|
||||
connectionType: relationship.connection_type,
|
||||
fromTable: fromTable,
|
||||
toTable: toTable,
|
||||
fromColumn: fromColumn,
|
||||
toColumn: toColumn,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
setEdges((eds) => [...eds, ...newEdges]);
|
||||
setRelationships((prev) => [...prev, relationship]);
|
||||
setPendingConnection(null);
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import {
|
|||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { MoreHorizontal, Edit, Trash2, Copy, Eye, Plus, Search, Network, Database, Calendar, User } from "lucide-react";
|
||||
import { MoreHorizontal, Trash2, Copy, Plus, Search, Network, Database, Calendar, User } from "lucide-react";
|
||||
import { DataFlowAPI, DataFlowDiagram } from "@/lib/api/dataflow";
|
||||
import { toast } from "sonner";
|
||||
|
||||
|
|
@ -58,31 +58,10 @@ export default function DataFlowList({ onDiagramSelect, selectedDiagram, onDesig
|
|||
};
|
||||
}, [currentPage, searchTerm]);
|
||||
|
||||
// 관계도 목록 다시 로드
|
||||
const reloadDiagrams = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await DataFlowAPI.getDataFlowDiagrams(currentPage, 20, searchTerm);
|
||||
setDiagrams(response.diagrams || []);
|
||||
setTotal(response.total || 0);
|
||||
setTotalPages(Math.max(1, Math.ceil((response.total || 0) / 20)));
|
||||
} catch (error) {
|
||||
console.error("관계도 목록 조회 실패", error);
|
||||
toast.error("관계도 목록을 불러오는데 실패했습니다.");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDiagramSelect = (diagram: DataFlowDiagram) => {
|
||||
onDiagramSelect(diagram);
|
||||
};
|
||||
|
||||
const handleEdit = (diagram: DataFlowDiagram) => {
|
||||
// 편집 모달 열기
|
||||
console.log("편집:", diagram);
|
||||
};
|
||||
|
||||
const handleDelete = (diagram: DataFlowDiagram) => {
|
||||
if (confirm(`"${diagram.diagramName}" 관계도를 삭제하시겠습니까?`)) {
|
||||
// 삭제 API 호출
|
||||
|
|
@ -96,12 +75,6 @@ export default function DataFlowList({ onDiagramSelect, selectedDiagram, onDesig
|
|||
toast.info("복사 기능은 아직 구현되지 않았습니다.");
|
||||
};
|
||||
|
||||
const handleView = (diagram: DataFlowDiagram) => {
|
||||
// 미리보기 모달 열기
|
||||
console.log("미리보기:", diagram);
|
||||
toast.info("미리보기 기능은 아직 구현되지 않았습니다.");
|
||||
};
|
||||
|
||||
// 연결 타입에 따른 배지 색상
|
||||
const getConnectionTypeBadge = (connectionType: string) => {
|
||||
switch (connectionType) {
|
||||
|
|
@ -268,14 +241,6 @@ export default function DataFlowList({ onDiagramSelect, selectedDiagram, onDesig
|
|||
<Network className="mr-2 h-4 w-4" />
|
||||
관계도 설계
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleView(diagram)}>
|
||||
<Eye className="mr-2 h-4 w-4" />
|
||||
미리보기
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleEdit(diagram)}>
|
||||
<Edit className="mr-2 h-4 w-4" />
|
||||
편집
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleCopy(diagram)}>
|
||||
<Copy className="mr-2 h-4 w-4" />
|
||||
복사
|
||||
|
|
|
|||
|
|
@ -22,10 +22,18 @@ 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 = [] } = data;
|
||||
const {
|
||||
table,
|
||||
onColumnClick,
|
||||
onScrollAreaEnter,
|
||||
onScrollAreaLeave,
|
||||
selectedColumns = [],
|
||||
connectedColumns = {},
|
||||
} = 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,17 +50,47 @@ 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) => {
|
||||
{table.columns.map((column, index) => {
|
||||
const isSelected = selectedColumns.includes(column.name);
|
||||
const connectionInfo = connectedColumns[column.name];
|
||||
const isConnected = !!connectionInfo;
|
||||
|
||||
// 연결된 컬럼에만 핸들 표시
|
||||
const showSourceHandle =
|
||||
isConnected && (connectionInfo.direction === "source" || connectionInfo.direction === "both");
|
||||
const showTargetHandle =
|
||||
isConnected && (connectionInfo.direction === "target" || connectionInfo.direction === "both");
|
||||
|
||||
return (
|
||||
<div
|
||||
key={column.name}
|
||||
onClick={() => onColumnClick(table.tableName, column.name)}
|
||||
className={`cursor-pointer rounded px-2 py-1 text-xs transition-colors ${
|
||||
className={`relative cursor-pointer rounded px-2 py-1 text-xs transition-colors ${
|
||||
isSelected ? "bg-blue-100 text-blue-800 ring-2 ring-blue-500" : "text-gray-700 hover:bg-gray-100"
|
||||
}`}
|
||||
onClick={() => onColumnClick(table.tableName, column.name)}
|
||||
>
|
||||
{/* Target Handle (왼쪽) - 세련된 디자인 */}
|
||||
{showTargetHandle && (
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
id={`${table.tableName}-${column.name}-target`}
|
||||
className="!absolute !left-[-6px] !h-2 !w-2 !rounded-full !border-0 !bg-blue-500 !shadow-sm hover:!bg-blue-600 hover:!shadow-md"
|
||||
style={{ top: "50%", transform: "translateY(-50%)" }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Source Handle (오른쪽) - 세련된 디자인 */}
|
||||
{showSourceHandle && (
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
id={`${table.tableName}-${column.name}-source`}
|
||||
className="!absolute !right-[-6px] !h-2 !w-2 !rounded-full !border-0 !bg-blue-500 !shadow-sm hover:!bg-blue-600 hover:!shadow-md"
|
||||
style={{ top: "50%", transform: "translateY(-50%)" }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-mono font-medium">{column.name}</span>
|
||||
<span className="text-gray-500">{column.type}</span>
|
||||
|
|
|
|||
Loading…
Reference in New Issue