엣지 호버 등 수정사항 반영
This commit is contained in:
parent
12910c69e8
commit
0a8413ee8c
|
|
@ -89,6 +89,7 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
} | null>(null);
|
||||
const [relationships, setRelationships] = useState<TableRelationship[]>([]); // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
const [currentDiagramId, setCurrentDiagramId] = useState<number | null>(null); // 현재 화면의 diagram_id
|
||||
const [selectedEdgeInfo, setSelectedEdgeInfo] = useState<any | null>(null); // 선택된 엣지 정보
|
||||
const toastShownRef = useRef(false); // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
|
||||
// 키보드 이벤트 핸들러 (Del 키로 선택된 노드 삭제)
|
||||
|
|
@ -220,12 +221,13 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
if (foundTable) {
|
||||
// 각 테이블의 컬럼 정보를 별도로 가져옴
|
||||
const columns = await DataFlowAPI.getTableColumns(tableName);
|
||||
console.log(`📋 테이블 ${tableName}의 컬럼 수:`, columns.length);
|
||||
const safeColumns = Array.isArray(columns) ? columns : [];
|
||||
console.log(`📋 테이블 ${tableName}의 컬럼 수:`, safeColumns.length);
|
||||
tableDefinitions.push({
|
||||
tableName: foundTable.tableName,
|
||||
displayName: foundTable.displayName,
|
||||
description: foundTable.description,
|
||||
columns: columns,
|
||||
columns: safeColumns,
|
||||
});
|
||||
} else {
|
||||
console.warn(`⚠️ 테이블 ${tableName}을 찾을 수 없습니다`);
|
||||
|
|
@ -275,6 +277,8 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
});
|
||||
});
|
||||
|
||||
console.log("🔌 연결된 컬럼 정보:", connectedColumnsInfo);
|
||||
|
||||
// 테이블을 노드로 변환 (자동 레이아웃)
|
||||
const tableNodes = tableDefinitions.map((table, index) => {
|
||||
const x = (index % 3) * 400 + 100; // 3열 배치
|
||||
|
|
@ -288,15 +292,17 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
table: {
|
||||
tableName: table.tableName,
|
||||
displayName: table.displayName,
|
||||
description: table.description || "",
|
||||
columns: table.columns.map((col) => ({
|
||||
name: col.columnName,
|
||||
type: col.dataType || "varchar",
|
||||
description: col.description || "",
|
||||
})),
|
||||
description: "", // 기존 로드된 노드도 description 없이 통일
|
||||
columns: Array.isArray(table.columns)
|
||||
? table.columns.map((col) => ({
|
||||
name: col.columnName,
|
||||
type: col.dataType || "varchar",
|
||||
description: col.description || "",
|
||||
}))
|
||||
: [],
|
||||
},
|
||||
onColumnClick: handleColumnClick,
|
||||
selectedColumns: selectedColumns[table.tableName] || [],
|
||||
selectedColumns: [], // 관계도 로드 시에는 빈 상태로 시작
|
||||
connectedColumns: connectedColumnsInfo[table.tableName] || {},
|
||||
} as TableNodeData,
|
||||
};
|
||||
|
|
@ -306,7 +312,7 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
console.log("📍 테이블 노드 상세:", tableNodes);
|
||||
setNodes(tableNodes);
|
||||
|
||||
// 관계를 엣지로 변환하여 표시 (컬럼별 연결)
|
||||
// 관계를 엣지로 변환하여 표시 (테이블 간 번들 연결)
|
||||
const relationshipEdges: Edge[] = [];
|
||||
|
||||
diagramRelationships.forEach((rel) => {
|
||||
|
|
@ -326,42 +332,40 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
.map((col) => col.trim())
|
||||
.filter((col) => col);
|
||||
|
||||
// 각 from 컬럼을 각 to 컬럼에 연결 (1:1 매핑이거나 many:many인 경우)
|
||||
if (fromColumns.length === 0 || toColumns.length === 0) {
|
||||
console.warn("⚠️ 컬럼 정보가 없습니다:", { fromColumns, toColumns });
|
||||
return;
|
||||
}
|
||||
|
||||
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.diagram_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,
|
||||
// 테이블 간 하나의 번들 엣지 생성 (컬럼별 개별 엣지 대신)
|
||||
relationshipEdges.push({
|
||||
id: generateUniqueId("edge", rel.diagram_id),
|
||||
source: `table-${fromTable}`,
|
||||
target: `table-${toTable}`,
|
||||
type: "smoothstep",
|
||||
animated: false,
|
||||
style: {
|
||||
stroke: "#3b82f6",
|
||||
strokeWidth: 2,
|
||||
strokeDasharray: "none",
|
||||
},
|
||||
data: {
|
||||
relationshipId: rel.relationship_id,
|
||||
relationshipName: rel.relationship_name,
|
||||
relationshipType: rel.relationship_type,
|
||||
connectionType: rel.connection_type,
|
||||
fromTable: fromTable,
|
||||
toTable: toTable,
|
||||
fromColumns: fromColumns,
|
||||
toColumns: toColumns,
|
||||
// 클릭 시 표시할 상세 정보
|
||||
details: {
|
||||
connectionInfo: `${fromTable}(${fromColumns.join(", ")}) → ${toTable}(${toColumns.join(", ")})`,
|
||||
relationshipType: rel.relationship_type,
|
||||
connectionType: rel.connection_type,
|
||||
fromTable: fromTable,
|
||||
toTable: toTable,
|
||||
fromColumn: fromColumn,
|
||||
toColumn: toColumn,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
console.log("🔗 생성된 관계 에지 수:", relationshipEdges.length);
|
||||
|
|
@ -372,7 +376,7 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
console.error("선택된 관계도 로드 실패:", error);
|
||||
toast.error("관계도를 불러오는데 실패했습니다.", { id: "load-diagram" });
|
||||
}
|
||||
}, [diagramId, relationshipId, setNodes, setEdges, selectedColumns, handleColumnClick]);
|
||||
}, [diagramId, relationshipId, setNodes, setEdges, handleColumnClick]);
|
||||
|
||||
// 기존 관계 로드 (새 관계도 생성 시)
|
||||
const loadExistingRelationships = useCallback(async () => {
|
||||
|
|
@ -448,12 +452,80 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
setSelectedNodes(selectedNodeIds);
|
||||
}, []);
|
||||
|
||||
// 캔버스 클릭 시 엣지 정보 섹션 닫기
|
||||
const onPaneClick = useCallback(() => {
|
||||
if (selectedEdgeInfo) {
|
||||
setSelectedEdgeInfo(null);
|
||||
}
|
||||
}, [selectedEdgeInfo]);
|
||||
|
||||
// 빈 onConnect 함수 (드래그 연결 비활성화)
|
||||
const onConnect = useCallback(() => {
|
||||
// 드래그로 연결하는 것을 방지
|
||||
return;
|
||||
}, []);
|
||||
|
||||
// 엣지 클릭 시 연결 정보 표시
|
||||
const onEdgeClick = useCallback((event: React.MouseEvent, edge: Edge) => {
|
||||
event.stopPropagation();
|
||||
const edgeData = edge.data as any;
|
||||
if (edgeData) {
|
||||
setSelectedEdgeInfo({
|
||||
relationshipId: edgeData.relationshipId,
|
||||
relationshipName: edgeData.relationshipName || "관계",
|
||||
fromTable: edgeData.fromTable,
|
||||
toTable: edgeData.toTable,
|
||||
fromColumns: edgeData.fromColumns || [],
|
||||
toColumns: edgeData.toColumns || [],
|
||||
relationshipType: edgeData.relationshipType,
|
||||
connectionType: edgeData.connectionType,
|
||||
connectionInfo: edgeData.details?.connectionInfo || `${edgeData.fromTable} → ${edgeData.toTable}`,
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 엣지 마우스 엔터 시 색상 변경
|
||||
const onEdgeMouseEnter = useCallback(
|
||||
(event: React.MouseEvent, edge: Edge) => {
|
||||
setEdges((eds) =>
|
||||
eds.map((e) =>
|
||||
e.id === edge.id
|
||||
? {
|
||||
...e,
|
||||
style: {
|
||||
...e.style,
|
||||
stroke: "#1d4ed8", // hover 색상
|
||||
strokeWidth: 3,
|
||||
},
|
||||
}
|
||||
: e,
|
||||
),
|
||||
);
|
||||
},
|
||||
[setEdges],
|
||||
);
|
||||
|
||||
// 엣지 마우스 리브 시 원래 색상으로 복원
|
||||
const onEdgeMouseLeave = useCallback(
|
||||
(event: React.MouseEvent, edge: Edge) => {
|
||||
setEdges((eds) =>
|
||||
eds.map((e) =>
|
||||
e.id === edge.id
|
||||
? {
|
||||
...e,
|
||||
style: {
|
||||
...e.style,
|
||||
stroke: "#3b82f6", // 기본 색상
|
||||
strokeWidth: 2,
|
||||
},
|
||||
}
|
||||
: e,
|
||||
),
|
||||
);
|
||||
},
|
||||
[setEdges],
|
||||
);
|
||||
|
||||
// 선택된 컬럼이 변경될 때마다 기존 노드들 업데이트 및 selectionOrder 정리
|
||||
useEffect(() => {
|
||||
setNodes((prevNodes) =>
|
||||
|
|
@ -548,16 +620,18 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
table: {
|
||||
tableName: table.tableName,
|
||||
displayName: table.displayName || table.tableName,
|
||||
description: table.description || "",
|
||||
columns: table.columns.map((col) => ({
|
||||
name: col.columnName || "unknown",
|
||||
type: col.dataType || col.dbType || "UNKNOWN",
|
||||
description:
|
||||
col.columnLabel || col.displayName || col.description || col.columnName || "No description",
|
||||
})),
|
||||
description: "", // 새로 추가된 노드는 description 없이 통일
|
||||
columns: Array.isArray(table.columns)
|
||||
? table.columns.map((col) => ({
|
||||
name: col.columnName || "unknown",
|
||||
type: col.dataType || "varchar", // 기존과 동일한 기본값 사용
|
||||
description: col.description || "",
|
||||
}))
|
||||
: [],
|
||||
},
|
||||
onColumnClick: handleColumnClick,
|
||||
selectedColumns: selectedColumns[table.tableName] || [],
|
||||
connectedColumns: {}, // 새로 추가된 노드는 연결 정보 없음
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -617,45 +691,48 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
(relationship: TableRelationship) => {
|
||||
if (!pendingConnection) return;
|
||||
|
||||
// 컬럼별 에지 생성
|
||||
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());
|
||||
const fromColumns = relationship.from_column_name
|
||||
.split(",")
|
||||
.map((col) => col.trim())
|
||||
.filter((col) => col);
|
||||
const toColumns = relationship.to_column_name
|
||||
.split(",")
|
||||
.map((col) => col.trim())
|
||||
.filter((col) => col);
|
||||
|
||||
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.diagram_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,
|
||||
const newEdge: Edge = {
|
||||
id: generateUniqueId("edge", relationship.diagram_id),
|
||||
source: pendingConnection.fromNode.id,
|
||||
target: pendingConnection.toNode.id,
|
||||
type: "smoothstep",
|
||||
animated: false,
|
||||
style: {
|
||||
stroke: "#3b82f6",
|
||||
strokeWidth: 2,
|
||||
strokeDasharray: "none",
|
||||
},
|
||||
data: {
|
||||
relationshipId: relationship.relationship_id,
|
||||
relationshipName: relationship.relationship_name,
|
||||
relationshipType: relationship.relationship_type,
|
||||
connectionType: relationship.connection_type,
|
||||
fromTable: fromTable,
|
||||
toTable: toTable,
|
||||
fromColumns: fromColumns,
|
||||
toColumns: toColumns,
|
||||
// 클릭 시 표시할 상세 정보
|
||||
details: {
|
||||
connectionInfo: `${fromTable}(${fromColumns.join(", ")}) → ${toTable}(${toColumns.join(", ")})`,
|
||||
relationshipType: relationship.relationship_type,
|
||||
connectionType: relationship.connection_type,
|
||||
fromTable: fromTable,
|
||||
toTable: toTable,
|
||||
fromColumn: fromColumn,
|
||||
toColumn: toColumn,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
setEdges((eds) => [...eds, ...newEdges]);
|
||||
setEdges((eds) => [...eds, newEdge]);
|
||||
setRelationships((prev) => [...prev, relationship]);
|
||||
setPendingConnection(null);
|
||||
|
||||
|
|
@ -664,6 +741,10 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
setCurrentDiagramId(relationship.diagram_id);
|
||||
}
|
||||
|
||||
// 관계 생성 후 선택된 컬럼들 초기화
|
||||
setSelectedColumns({});
|
||||
setSelectionOrder([]);
|
||||
|
||||
console.log("관계 생성 완료:", relationship);
|
||||
// 관계 생성 완료 후 자동으로 목록 새로고침을 위한 콜백 (선택적)
|
||||
// 렌더링 중 상태 업데이트 방지를 위해 제거
|
||||
|
|
@ -805,18 +886,56 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 선택된 엣지 정보 */}
|
||||
{selectedEdgeInfo && (
|
||||
<div className="mt-6 space-y-4">
|
||||
<div className="rounded-lg border border-green-200 bg-green-50 p-4">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<div className="text-sm font-semibold text-green-800">🔗 연결 정보</div>
|
||||
<button onClick={() => setSelectedEdgeInfo(null)} className="text-green-600 hover:text-green-800">
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-2 text-xs">
|
||||
<div className="rounded bg-white p-2">
|
||||
<div className="font-medium text-gray-700">연결</div>
|
||||
<div className="text-gray-600">
|
||||
{selectedEdgeInfo.fromTable}({selectedEdgeInfo.fromColumns.join(", ")}) →{" "}
|
||||
{selectedEdgeInfo.toTable}({selectedEdgeInfo.toColumns.join(", ")})
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<div className="flex-1 rounded bg-white p-2">
|
||||
<div className="font-medium text-gray-700">관계 유형</div>
|
||||
<div className="text-gray-600">{selectedEdgeInfo.relationshipType}</div>
|
||||
</div>
|
||||
<div className="flex-1 rounded bg-white p-2">
|
||||
<div className="font-medium text-gray-700">연결 유형</div>
|
||||
<div className="text-gray-600">{selectedEdgeInfo.connectionType}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* React Flow 캔버스 */}
|
||||
<div className="relative flex-1">
|
||||
<ReactFlow
|
||||
className="[&_.react-flow\_\_pane]:cursor-default [&_.react-flow\_\_pane.dragging]:cursor-grabbing"
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={onConnect}
|
||||
onSelectionChange={onSelectionChange}
|
||||
onPaneClick={onPaneClick}
|
||||
onEdgeClick={onEdgeClick}
|
||||
onEdgeMouseEnter={onEdgeMouseEnter}
|
||||
onEdgeMouseLeave={onEdgeMouseLeave}
|
||||
nodeTypes={nodeTypes}
|
||||
edgeTypes={edgeTypes}
|
||||
fitView
|
||||
|
|
|
|||
|
|
@ -41,8 +41,8 @@ export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => {
|
|||
<Handle type="target" position={Position.Left} id="left" className="!invisible !h-1 !w-1" />
|
||||
<Handle type="source" position={Position.Right} id="right" className="!invisible !h-1 !w-1" />
|
||||
|
||||
{/* 테이블 헤더 */}
|
||||
<div className="bg-blue-600 p-3 text-white">
|
||||
{/* 테이블 헤더 - 통일된 디자인 */}
|
||||
<div className="rounded-t-lg 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>
|
||||
|
|
@ -52,14 +52,6 @@ export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => {
|
|||
<div className="space-y-1">
|
||||
{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
|
||||
|
|
@ -69,27 +61,7 @@ export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => {
|
|||
}`}
|
||||
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>
|
||||
|
|
|
|||
|
|
@ -139,13 +139,21 @@ export class DataFlowAPI {
|
|||
*/
|
||||
static async getTableColumns(tableName: string): Promise<ColumnInfo[]> {
|
||||
try {
|
||||
const response = await apiClient.get<ApiResponse<ColumnInfo[]>>(`/table-management/tables/${tableName}/columns`);
|
||||
const response = await apiClient.get<
|
||||
ApiResponse<{
|
||||
columns: ColumnInfo[];
|
||||
page: number;
|
||||
total: number;
|
||||
totalPages: number;
|
||||
}>
|
||||
>(`/table-management/tables/${tableName}/columns`);
|
||||
|
||||
if (!response.data.success) {
|
||||
throw new Error(response.data.message || "컬럼 정보 조회에 실패했습니다.");
|
||||
}
|
||||
|
||||
return response.data.data || [];
|
||||
// 페이지네이션된 응답에서 columns 배열만 추출
|
||||
return response.data.data?.columns || [];
|
||||
} catch (error) {
|
||||
console.error("컬럼 정보 조회 오류:", error);
|
||||
throw error;
|
||||
|
|
|
|||
Loading…
Reference in New Issue