From 6a04ae450d29fe0e1cb6967f167217093a858019 Mon Sep 17 00:00:00 2001 From: hyeonsu Date: Mon, 15 Sep 2025 16:15:00 +0900 Subject: [PATCH] =?UTF-8?q?=EC=97=A3=EC=A7=80=20=ED=81=B4=EB=A6=AD=20?= =?UTF-8?q?=EC=8B=9C=20=EC=97=B0=EA=B2=B0=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/dataflow/edit/[diagramId]/page.tsx | 12 +- .../components/dataflow/DataFlowDesigner.tsx | 182 +++++++++++++----- 2 files changed, 145 insertions(+), 49 deletions(-) diff --git a/frontend/app/(main)/admin/dataflow/edit/[diagramId]/page.tsx b/frontend/app/(main)/admin/dataflow/edit/[diagramId]/page.tsx index 25b3f193..ede20c29 100644 --- a/frontend/app/(main)/admin/dataflow/edit/[diagramId]/page.tsx +++ b/frontend/app/(main)/admin/dataflow/edit/[diagramId]/page.tsx @@ -43,6 +43,11 @@ export default function DataFlowEditPage() { router.push("/admin/dataflow"); }; + // 관계도 이름 업데이트 핸들러 + const handleDiagramNameUpdate = (newDiagramName: string) => { + setDiagramName(newDiagramName); + }; + if (!diagramId || !diagramName) { return (
@@ -74,7 +79,12 @@ export default function DataFlowEditPage() { {/* 데이터플로우 디자이너 */}
- +
); diff --git a/frontend/components/dataflow/DataFlowDesigner.tsx b/frontend/components/dataflow/DataFlowDesigner.tsx index 38a33db0..26a6eba6 100644 --- a/frontend/components/dataflow/DataFlowDesigner.tsx +++ b/frontend/components/dataflow/DataFlowDesigner.tsx @@ -66,6 +66,7 @@ interface DataFlowDesignerProps { diagramId?: number; relationshipId?: string; // 하위 호환성 유지 onBackToList?: () => void; + onDiagramNameUpdate?: (diagramName: string) => void; // 관계도 이름 업데이트 콜백 추가 } // TableRelationship 타입은 dataflow.ts에서 import @@ -82,6 +83,7 @@ export const DataFlowDesigner: React.FC = ({ onSave, // eslint-disable-line @typescript-eslint/no-unused-vars selectedDiagram, onBackToList, // eslint-disable-line @typescript-eslint/no-unused-vars + onDiagramNameUpdate, // 관계도 이름 업데이트 콜백 }) => { const { user } = useAuth(); @@ -136,6 +138,8 @@ export const DataFlowDesigner: React.FC = ({ const [showEdgeActions, setShowEdgeActions] = useState(false); // 엣지 액션 버튼 표시 상태 const [edgeActionPosition, setEdgeActionPosition] = useState<{ x: number; y: number }>({ x: 0, y: 0 }); // 액션 버튼 위치 const [editingRelationshipId, setEditingRelationshipId] = useState(null); // 현재 수정 중인 관계 ID + const [showRelationshipListModal, setShowRelationshipListModal] = useState(false); // 관계 목록 모달 표시 상태 + const [selectedTablePairRelationships, setSelectedTablePairRelationships] = useState([]); // 선택된 테이블 쌍의 관계들 const toastShownRef = useRef(false); // eslint-disable-line @typescript-eslint/no-unused-vars // 편집 모드일 때 관계도 이름 로드 @@ -333,8 +337,9 @@ export const DataFlowDesigner: React.FC = ({ console.log("📍 테이블 노드 상세:", tableNodes); setNodes(tableNodes); - // JSON 관계를 엣지로 변환하여 표시 (테이블 간 번들 연결) + // JSON 관계를 엣지로 변환하여 표시 (각 관계마다 개별 엣지 생성) const relationshipEdges: Edge[] = []; + const tableRelationshipCount: { [key: string]: number } = {}; // 테이블 쌍별 관계 개수 normalizedRelationships.forEach((rel: ExtendedJsonRelationship) => { const fromTable = rel.fromTable; @@ -347,9 +352,16 @@ export const DataFlowDesigner: React.FC = ({ return; } - // 테이블 간 하나의 번들 엣지 생성 (컬럼별 개별 엣지 대신) + // 테이블 쌍 키 생성 (양방향 동일하게 처리) + const tableKey = [fromTable, toTable].sort().join("-"); + tableRelationshipCount[tableKey] = (tableRelationshipCount[tableKey] || 0) + 1; + const relationshipIndex = tableRelationshipCount[tableKey]; + + // 각 관계마다 고유한 엣지 생성 (곡선 오프셋으로 구분) + const curveOffset = (relationshipIndex - 1) * 30; // 30px씩 오프셋 + relationshipEdges.push({ - id: generateUniqueId("edge", currentDiagramId), + id: `edge-${rel.id}`, // 관계 ID를 기반으로 고유 ID 생성 source: `table-${fromTable}`, target: `table-${toTable}`, type: "smoothstep", @@ -359,6 +371,17 @@ export const DataFlowDesigner: React.FC = ({ strokeWidth: 2, strokeDasharray: "none", }, + // 여러 관계가 있을 때 곡선 오프셋 적용 + ...(relationshipIndex > 1 && { + style: { + stroke: "#3b82f6", + strokeWidth: 2, + strokeDasharray: "none", + }, + pathOptions: { + offset: curveOffset, + }, + }), data: { relationshipId: rel.id, relationshipName: rel.relationshipName, @@ -367,6 +390,9 @@ export const DataFlowDesigner: React.FC = ({ toTable: toTable, fromColumns: fromColumns, toColumns: toColumns, + // 테이블 쌍의 모든 관계 정보 (엣지 클릭 시 사용) + tableKey: tableKey, + relationshipIndex: relationshipIndex, // 클릭 시 표시할 상세 정보 details: { connectionInfo: `${fromTable}(${fromColumns.join(", ")}) → ${toTable}(${toColumns.join(", ")})`, @@ -486,56 +512,45 @@ export const DataFlowDesigner: React.FC = ({ }, []); // 엣지 클릭 시 연결 정보 표시 및 관련 컬럼 하이라이트 - const onEdgeClick = useCallback((event: React.MouseEvent, edge: Edge) => { - event.stopPropagation(); - const edgeData = edge.data as { - relationshipId: string; - relationshipName: string; - fromTable: string; - toTable: string; - fromColumns: string[]; - toColumns: string[]; - connectionType: string; - details?: { - connectionInfo: string; + const onEdgeClick = useCallback( + (event: React.MouseEvent, edge: Edge) => { + event.stopPropagation(); + const edgeData = edge.data as { + relationshipId: string; + relationshipName: string; + fromTable: string; + toTable: string; + fromColumns: string[]; + toColumns: string[]; connectionType: string; + tableKey: string; + relationshipIndex: number; + details?: { + connectionInfo: string; + connectionType: string; + }; }; - }; - if (edgeData) { - // 엣지 정보 설정 - setSelectedEdgeInfo({ - relationshipId: edgeData.relationshipId, - relationshipName: edgeData.relationshipName || "관계", - fromTable: edgeData.fromTable, - toTable: edgeData.toTable, - fromColumns: edgeData.fromColumns || [], - toColumns: edgeData.toColumns || [], - connectionType: edgeData.connectionType, - connectionInfo: edgeData.details?.connectionInfo || `${edgeData.fromTable} → ${edgeData.toTable}`, - }); + if (edgeData) { + // 해당 테이블 쌍의 모든 관계 찾기 + const fromTable = edgeData.fromTable; + const toTable = edgeData.toTable; - // 관련 컬럼 하이라이트 - const newSelectedColumns: { [tableName: string]: string[] } = {}; + const tablePairRelationships = tempRelationships.filter( + (rel) => + (rel.fromTable === fromTable && rel.toTable === toTable) || + (rel.fromTable === toTable && rel.toTable === fromTable), + ); - // fromTable의 컬럼들 선택 - if (edgeData.fromTable && edgeData.fromColumns) { - newSelectedColumns[edgeData.fromTable] = [...edgeData.fromColumns]; + console.log(`🔗 ${fromTable} ↔ ${toTable} 간의 관계:`, tablePairRelationships); + + // 관계가 1개든 여러 개든 항상 관계 목록 모달 표시 + setSelectedTablePairRelationships(tablePairRelationships); + setShowRelationshipListModal(true); } - - // toTable의 컬럼들 선택 - if (edgeData.toTable && edgeData.toColumns) { - newSelectedColumns[edgeData.toTable] = [...edgeData.toColumns]; - } - - setSelectedColumns(newSelectedColumns); - - // 액션 버튼 표시 - setSelectedEdgeForEdit(edge); - setEdgeActionPosition({ x: event.clientX, y: event.clientY }); - setShowEdgeActions(true); - } - }, []); + }, + [tempRelationships], + ); // 엣지 마우스 엔터 시 색상 변경 const onEdgeMouseEnter = useCallback( @@ -845,6 +860,11 @@ export const DataFlowDesigner: React.FC = ({ setShowSaveModal(false); setCurrentDiagramId(savedDiagram.diagram_id); + // 관계도 이름 업데이트 (편집 모드일 때만) + if (diagramId && diagramId > 0 && onDiagramNameUpdate) { + onDiagramNameUpdate(diagramName); + } + console.log("관계도 저장 완료:", savedDiagram); } catch (error) { console.error("관계도 저장 실패:", error); @@ -853,7 +873,7 @@ export const DataFlowDesigner: React.FC = ({ setIsSaving(false); } }, - [tempRelationships, diagramId, companyCode, user?.userId, nodes], + [tempRelationships, diagramId, companyCode, user?.userId, nodes, onDiagramNameUpdate], ); // 저장 모달 열기 @@ -1136,6 +1156,72 @@ export const DataFlowDesigner: React.FC = ({ > + + {/* 관계 목록 모달 - 캔버스 내부 우측 상단에 배치 */} + {showRelationshipListModal && ( +
+ {/* 헤더 */} +
+
+
+ 🔗 +
+
테이블 간 관계 목록
+
+ +
+ + {/* 관계 목록 */} +
+
+ {selectedTablePairRelationships.map((relationship, index) => ( +
{ + // 관계 선택 시 수정 모드로 전환 + setEditingRelationshipId(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]; + } + setSelectedColumns(newSelectedColumns); + + // 모달 닫기 + setShowRelationshipListModal(false); + }} + className="cursor-pointer rounded-lg border border-gray-200 p-3 transition-all hover:border-blue-300 hover:bg-blue-50" + > +
+

+ {relationship.fromTable} → {relationship.toTable} +

+ + + +
+
+

타입: {relationship.connectionType}

+

From: {relationship.fromTable}

+

To: {relationship.toTable}

+
+
+ ))} +
+
+
+ )} {/* 선택된 테이블 노드 팝업 - 캔버스 좌측 상단 고정 (새 관계 생성 시에만 표시) */}