엣지 호버 등 수정사항 반영

This commit is contained in:
hyeonsu 2025-09-10 11:27:05 +09:00
parent 12910c69e8
commit 0a8413ee8c
3 changed files with 210 additions and 111 deletions

View File

@ -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

View File

@ -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>

View File

@ -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;