diff --git a/backend-node/prisma/schema.prisma b/backend-node/prisma/schema.prisma index b792f93a..1eb7c612 100644 --- a/backend-node/prisma/schema.prisma +++ b/backend-node/prisma/schema.prisma @@ -5108,6 +5108,7 @@ model code_info { // 테이블 간 관계 정의 model table_relationships { relationship_id Int @id @default(autoincrement()) + diagram_id Int // 관계도 그룹 식별자 relationship_name String @db.VarChar(200) from_table_name String @db.VarChar(100) from_column_name String @db.VarChar(100) @@ -5127,8 +5128,10 @@ model table_relationships { bridges data_relationship_bridge[] @@index([company_code], map: "idx_table_relationships_company_code") + @@index([diagram_id], map: "idx_table_relationships_diagram_id") @@index([from_table_name], map: "idx_table_relationships_from_table") @@index([to_table_name], map: "idx_table_relationships_to_table") + @@index([company_code, diagram_id], map: "idx_table_relationships_company_diagram") } // 테이블 간 데이터 관계 중계 테이블 - 실제 데이터 연결 정보 저장 diff --git a/backend-node/src/controllers/dataflowController.ts b/backend-node/src/controllers/dataflowController.ts index 57a0c3ea..c9a4a426 100644 --- a/backend-node/src/controllers/dataflowController.ts +++ b/backend-node/src/controllers/dataflowController.ts @@ -15,6 +15,7 @@ export async function createTableRelationship( logger.info("=== 테이블 관계 생성 시작 ==="); const { + diagramId, relationshipName, fromTableName, fromColumnName, @@ -52,6 +53,7 @@ export async function createTableRelationship( const dataflowService = new DataflowService(); const relationship = await dataflowService.createTableRelationship({ + diagramId: diagramId ? parseInt(diagramId) : undefined, relationshipName, fromTableName, fromColumnName, @@ -828,7 +830,62 @@ export async function deleteDiagram( } /** - * relationship_id로 관계도 관계 조회 + * diagram_id로 관계도 관계 조회 + */ +export async function getDiagramRelationshipsByDiagramId( + req: AuthenticatedRequest, + res: Response +): Promise { + try { + const { diagramId } = req.params; + const companyCode = (req.user as any)?.company_code || "*"; + + if (!diagramId) { + const response: ApiResponse = { + success: false, + message: "관계도 ID가 필요합니다.", + error: { + code: "MISSING_DIAGRAM_ID", + details: "diagramId 파라미터가 필요합니다.", + }, + }; + res.status(400).json(response); + return; + } + + const dataflowService = new DataflowService(); + const relationships = + await dataflowService.getDiagramRelationshipsByDiagramId( + companyCode, + parseInt(diagramId) + ); + + const response: ApiResponse = { + success: true, + message: "관계도 관계 목록을 성공적으로 조회했습니다.", + data: relationships, + }; + + res.status(200).json(response); + } catch (error) { + logger.error("관계도 관계 조회 실패:", error); + const response: ApiResponse = { + success: false, + message: "관계도 관계 조회에 실패했습니다.", + error: { + code: "DIAGRAM_RELATIONSHIPS_FETCH_FAILED", + details: + error instanceof Error + ? error.message + : "알 수 없는 오류가 발생했습니다.", + }, + }; + res.status(500).json(response); + } +} + +/** + * relationship_id로 관계도 관계 조회 (하위 호환성 유지) */ export async function getDiagramRelationshipsByRelationshipId( req: AuthenticatedRequest, @@ -852,10 +909,11 @@ export async function getDiagramRelationshipsByRelationshipId( } const dataflowService = new DataflowService(); - const relationships = await dataflowService.getDiagramRelationshipsByRelationshipId( - companyCode, - parseInt(relationshipId) - ); + const relationships = + await dataflowService.getDiagramRelationshipsByRelationshipId( + companyCode, + parseInt(relationshipId) + ); const response: ApiResponse = { success: true, @@ -871,7 +929,10 @@ export async function getDiagramRelationshipsByRelationshipId( message: "관계도 관계 조회에 실패했습니다.", error: { code: "DIAGRAM_RELATIONSHIPS_FETCH_FAILED", - details: error instanceof Error ? error.message : "알 수 없는 오류가 발생했습니다.", + details: + error instanceof Error + ? error.message + : "알 수 없는 오류가 발생했습니다.", }, }; res.status(500).json(response); diff --git a/backend-node/src/routes/dataflowRoutes.ts b/backend-node/src/routes/dataflowRoutes.ts index ddb87a17..983ac181 100644 --- a/backend-node/src/routes/dataflowRoutes.ts +++ b/backend-node/src/routes/dataflowRoutes.ts @@ -12,6 +12,7 @@ import { getTableData, getDataFlowDiagrams, getDiagramRelationships, + getDiagramRelationshipsByDiagramId, getDiagramRelationshipsByRelationshipId, copyDiagram, deleteDiagram, @@ -92,10 +93,22 @@ router.get("/table-data/:tableName", getTableData); router.get("/diagrams", getDataFlowDiagrams); /** - * 특정 관계도의 모든 관계 조회 - * GET /api/dataflow/diagrams/:diagramName/relationships + * 특정 관계도의 모든 관계 조회 (diagram_id로) + * GET /api/dataflow/diagrams/:diagramId/relationships */ -router.get("/diagrams/:diagramName/relationships", getDiagramRelationships); +router.get( + "/diagrams/:diagramId/relationships", + getDiagramRelationshipsByDiagramId +); + +/** + * 특정 관계도의 모든 관계 조회 (diagramName으로 - 하위 호환성) + * GET /api/dataflow/diagrams/name/:diagramName/relationships + */ +router.get( + "/diagrams/name/:diagramName/relationships", + getDiagramRelationships +); /** * 관계도 복사 @@ -109,7 +122,7 @@ router.post("/diagrams/:diagramName/copy", copyDiagram); */ router.delete("/diagrams/:diagramName", deleteDiagram); -// relationship_id로 관계도 관계 조회 +// relationship_id로 관계도 관계 조회 (하위 호환성) router.get( "/relationships/:relationshipId/diagram", getDiagramRelationshipsByRelationshipId diff --git a/backend-node/src/services/dataflowService.ts b/backend-node/src/services/dataflowService.ts index e188a4cf..0f711335 100644 --- a/backend-node/src/services/dataflowService.ts +++ b/backend-node/src/services/dataflowService.ts @@ -5,6 +5,7 @@ const prisma = new PrismaClient(); // 테이블 관계 생성 데이터 타입 interface CreateTableRelationshipData { + diagramId?: number; // 기존 관계도에 추가하는 경우 relationshipName: string; fromTableName: string; fromColumnName: string; @@ -38,9 +39,31 @@ export class DataflowService { try { logger.info("DataflowService: 테이블 관계 생성 시작", data); - // 중복 관계 확인 + // diagram_id 결정 로직 + let diagramId = data.diagramId; + + if (!diagramId) { + // 새로운 관계도인 경우, 새로운 diagram_id 생성 + // 현재 최대 diagram_id + 1 + const maxDiagramId = await prisma.table_relationships.findFirst({ + where: { + company_code: data.companyCode, + }, + orderBy: { + diagram_id: "desc", + }, + select: { + diagram_id: true, + }, + }); + + diagramId = (maxDiagramId?.diagram_id || 0) + 1; + } + + // 중복 관계 확인 (같은 diagram_id 내에서) const existingRelationship = await prisma.table_relationships.findFirst({ where: { + diagram_id: diagramId, from_table_name: data.fromTableName, from_column_name: data.fromColumnName, to_table_name: data.toTableName, @@ -56,9 +79,10 @@ export class DataflowService { ); } - // 새 관계 생성 (중계 테이블은 별도로 생성하지 않음) + // 새 관계 생성 const relationship = await prisma.table_relationships.create({ data: { + diagram_id: diagramId, relationship_name: data.relationshipName, from_table_name: data.fromTableName, from_column_name: data.fromColumnName, @@ -74,7 +98,7 @@ export class DataflowService { }); logger.info( - `DataflowService: 테이블 관계 생성 완료 - ID: ${relationship.relationship_id}` + `DataflowService: 테이블 관계 생성 완료 - ID: ${relationship.relationship_id}, Diagram ID: ${relationship.diagram_id}` ); return relationship; } catch (error) { @@ -731,7 +755,7 @@ export class DataflowService { } /** - * 관계도 그룹 목록 조회 (관계도 이름별로 그룹화) + * 관계도 그룹 목록 조회 (diagram_id별로 그룹화) */ async getDataFlowDiagrams( companyCode: string, @@ -744,7 +768,7 @@ export class DataflowService { `DataflowService: 관계도 목록 조회 시작 - ${companyCode}, page: ${page}, size: ${size}, search: ${searchTerm}` ); - // 관계도 이름별로 그룹화하여 조회 + // diagram_id별로 그룹화하여 조회 const whereCondition = { company_code: companyCode, is_active: "Y", @@ -772,11 +796,12 @@ export class DataflowService { }), }; - // 관계도별로 그룹화된 데이터 조회 (관계도 이름을 기준으로) + // diagram_id별로 그룹화된 데이터 조회 const relationships = await prisma.table_relationships.findMany({ where: whereCondition, select: { relationship_id: true, + diagram_id: true, relationship_name: true, from_table_name: true, to_table_name: true, @@ -787,19 +812,19 @@ export class DataflowService { updated_date: true, updated_by: true, }, - orderBy: [{ relationship_name: "asc" }, { created_date: "desc" }], + orderBy: [{ diagram_id: "asc" }, { created_date: "desc" }], }); - // 관계도 이름별로 그룹화 - const diagramMap = new Map(); + // diagram_id별로 그룹화 + const diagramMap = new Map(); relationships.forEach((rel) => { - const diagramName = rel.relationship_name; + const diagramId = rel.diagram_id; - if (!diagramMap.has(diagramName)) { - diagramMap.set(diagramName, { - relationshipId: rel.relationship_id, // 첫 번째 관계의 ID를 대표 ID로 사용 - diagramName: diagramName, + if (!diagramMap.has(diagramId)) { + diagramMap.set(diagramId, { + diagramId: diagramId, + diagramName: rel.relationship_name, // 첫 번째 관계의 이름을 사용 connectionType: rel.connection_type, relationshipType: rel.relationship_type, tableCount: new Set(), @@ -812,7 +837,7 @@ export class DataflowService { }); } - const diagram = diagramMap.get(diagramName); + const diagram = diagramMap.get(diagramId); diagram.tableCount.add(rel.from_table_name); diagram.tableCount.add(rel.to_table_name); diagram.relationshipCount++; @@ -893,7 +918,7 @@ export class DataflowService { } /** - * 관계도 복사 + * 관계도 복사 (diagram_id 기반) */ async copyDiagram( companyCode: string, @@ -936,11 +961,27 @@ export class DataflowService { newDiagramName = `${originalDiagramName} (${counter})`; } + // 새로운 diagram_id 생성 + const maxDiagramId = await prisma.table_relationships.findFirst({ + where: { + company_code: companyCode, + }, + orderBy: { + diagram_id: "desc", + }, + select: { + diagram_id: true, + }, + }); + + const newDiagramId = (maxDiagramId?.diagram_id || 0) + 1; + // 트랜잭션으로 모든 관계 복사 const copiedRelationships = await prisma.$transaction( originalRelationships.map((rel) => prisma.table_relationships.create({ data: { + diagram_id: newDiagramId, relationship_name: newDiagramName, from_table_name: rel.from_table_name, from_column_name: rel.from_column_name, @@ -959,7 +1000,7 @@ export class DataflowService { ); logger.info( - `DataflowService: 관계도 복사 완료 - ${originalDiagramName} → ${newDiagramName}, ${copiedRelationships.length}개 관계 복사` + `DataflowService: 관계도 복사 완료 - ${originalDiagramName} → ${newDiagramName} (diagram_id: ${newDiagramId}), ${copiedRelationships.length}개 관계 복사` ); return newDiagramName; @@ -1002,7 +1043,46 @@ export class DataflowService { } /** - * relationship_id로 해당 관계도의 모든 관계 조회 + * diagram_id로 해당 관계도의 모든 관계 조회 + */ + async getDiagramRelationshipsByDiagramId( + companyCode: string, + diagramId: number + ) { + try { + logger.info( + `DataflowService: diagram_id로 관계도 관계 조회 - ${diagramId}` + ); + + // diagram_id로 모든 관계 조회 + const relationships = await prisma.table_relationships.findMany({ + where: { + diagram_id: diagramId, + company_code: companyCode, + is_active: "Y", + }, + orderBy: [{ relationship_id: "asc" }], + }); + + logger.info( + `DataflowService: diagram_id로 관계도 관계 조회 완료 - ${relationships.length}개 관계` + ); + + return relationships.map((rel) => ({ + ...rel, + settings: rel.settings as any, + })); + } catch (error) { + logger.error( + `DataflowService: diagram_id로 관계도 관계 조회 실패 - ${diagramId}`, + error + ); + throw error; + } + } + + /** + * relationship_id로 해당 관계도의 모든 관계 조회 (하위 호환성 유지) */ async getDiagramRelationshipsByRelationshipId( companyCode: string, @@ -1013,7 +1093,7 @@ export class DataflowService { `DataflowService: relationship_id로 관계도 관계 조회 - ${relationshipId}` ); - // 먼저 해당 relationship_id의 관계도명을 찾음 + // 먼저 해당 relationship_id의 diagram_id를 찾음 const targetRelationship = await prisma.table_relationships.findFirst({ where: { relationship_id: relationshipId, @@ -1021,7 +1101,7 @@ export class DataflowService { is_active: "Y", }, select: { - relationship_name: true, + diagram_id: true, }, }); @@ -1029,24 +1109,11 @@ export class DataflowService { throw new Error("해당 관계 ID를 찾을 수 없습니다."); } - // 같은 관계도명을 가진 모든 관계 조회 - const relationships = await prisma.table_relationships.findMany({ - where: { - relationship_name: targetRelationship.relationship_name, - company_code: companyCode, - is_active: "Y", - }, - orderBy: [{ relationship_id: "asc" }], - }); - - logger.info( - `DataflowService: relationship_id로 관계도 관계 조회 완료 - ${relationships.length}개 관계` + // diagram_id로 모든 관계 조회 + return this.getDiagramRelationshipsByDiagramId( + companyCode, + targetRelationship.diagram_id ); - - return relationships.map((rel) => ({ - ...rel, - settings: rel.settings as any, - })); } catch (error) { logger.error( `DataflowService: relationship_id로 관계도 관계 조회 실패 - ${relationshipId}`, diff --git a/frontend/app/(main)/admin/dataflow/edit/[relationshipId]/page.tsx b/frontend/app/(main)/admin/dataflow/edit/[diagramId]/page.tsx similarity index 81% rename from frontend/app/(main)/admin/dataflow/edit/[relationshipId]/page.tsx rename to frontend/app/(main)/admin/dataflow/edit/[diagramId]/page.tsx index ff6fc87b..0e3cf520 100644 --- a/frontend/app/(main)/admin/dataflow/edit/[relationshipId]/page.tsx +++ b/frontend/app/(main)/admin/dataflow/edit/[diagramId]/page.tsx @@ -11,19 +11,19 @@ import { toast } from "sonner"; export default function DataFlowEditPage() { const params = useParams(); const router = useRouter(); - const [relationshipId, setRelationshipId] = useState(""); + const [diagramId, setDiagramId] = useState(0); const [diagramName, setDiagramName] = useState(""); useEffect(() => { - if (params.relationshipId) { - // URL에서 relationship_id 설정 - const id = params.relationshipId as string; - setRelationshipId(id); + if (params.diagramId) { + // URL에서 diagram_id 설정 + const id = parseInt(params.diagramId as string); + setDiagramId(id); - // relationship_id로 관계도명 조회 + // diagram_id로 관계도명 조회 const fetchDiagramName = async () => { try { - const relationships = await DataFlowAPI.getDiagramRelationshipsByRelationshipId(id); + const relationships = await DataFlowAPI.getDiagramRelationshipsByDiagramId(id); if (relationships.length > 0) { setDiagramName(relationships[0].relationship_name); } else { @@ -37,13 +37,13 @@ export default function DataFlowEditPage() { fetchDiagramName(); } - }, [params.relationshipId]); + }, [params.diagramId]); const handleBackToList = () => { router.push("/admin/dataflow"); }; - if (!relationshipId || !diagramName) { + if (!diagramId || !diagramName) { return (
@@ -74,11 +74,7 @@ export default function DataFlowEditPage() { {/* 데이터플로우 디자이너 */}
- +
); diff --git a/frontend/app/(main)/admin/dataflow/page.tsx b/frontend/app/(main)/admin/dataflow/page.tsx index 29a3aea3..9ef77927 100644 --- a/frontend/app/(main)/admin/dataflow/page.tsx +++ b/frontend/app/(main)/admin/dataflow/page.tsx @@ -72,8 +72,10 @@ export default function DataFlowPage() { const handleSave = (relationships: TableRelationship[]) => { console.log("저장된 관계:", relationships); - // 저장 후 목록으로 돌아가기 - goToStep("list"); + // 저장 후 목록으로 돌아가기 - 다음 렌더링 사이클로 지연 + setTimeout(() => { + goToStep("list"); + }, 0); }; const handleDiagramSelect = (diagram: DataFlowDiagram) => { @@ -83,7 +85,7 @@ export default function DataFlowPage() { const handleDesignDiagram = (diagram: DataFlowDiagram | null) => { if (diagram) { // 기존 관계도 편집 - 새로운 URL로 이동 - router.push(`/admin/dataflow/edit/${diagram.relationshipId}`); + router.push(`/admin/dataflow/edit/${diagram.diagramId}`); } else { // 새 관계도 생성 - 현재 페이지에서 처리 setSelectedDiagram(null); diff --git a/frontend/components/dataflow/ConnectionSetupModal.tsx b/frontend/components/dataflow/ConnectionSetupModal.tsx index 2aa98137..b8c3dec9 100644 --- a/frontend/components/dataflow/ConnectionSetupModal.tsx +++ b/frontend/components/dataflow/ConnectionSetupModal.tsx @@ -70,6 +70,7 @@ interface ConnectionSetupModalProps { isOpen: boolean; connection: ConnectionInfo | null; companyCode: string; + diagramId?: number; onConfirm: (relationship: TableRelationship) => void; onCancel: () => void; } @@ -78,6 +79,7 @@ export const ConnectionSetupModal: React.FC = ({ isOpen, connection, companyCode, + diagramId, onConfirm, onCancel, }) => { @@ -188,15 +190,17 @@ export const ConnectionSetupModal: React.FC = ({ toast.loading("관계를 생성하고 있습니다...", { id: "create-relationship" }); // 단일 관계 데이터 준비 (모든 선택된 컬럼 정보 포함) - const relationshipData: Omit = { - relationship_name: config.relationshipName, - from_table_name: connection.fromNode.tableName, - from_column_name: fromColumns.join(","), // 여러 컬럼을 콤마로 구분 - to_table_name: connection.toNode.tableName, - to_column_name: toColumns.join(","), // 여러 컬럼을 콤마로 구분 - relationship_type: config.relationshipType, - connection_type: config.connectionType, - company_code: companyCode, + // API 요청용 데이터 (camelCase) + const apiRequestData = { + ...(diagramId && diagramId > 0 ? { diagramId: diagramId } : {}), // diagramId가 유효할 때만 추가 + relationshipName: config.relationshipName, + fromTableName: connection.fromNode.tableName, + fromColumnName: fromColumns.join(","), // 여러 컬럼을 콤마로 구분 + toTableName: connection.toNode.tableName, + toColumnName: toColumns.join(","), // 여러 컬럼을 콤마로 구분 + relationshipType: config.relationshipType, + connectionType: config.connectionType, + companyCode: companyCode, settings: { ...settings, multiColumnMapping: { @@ -211,11 +215,10 @@ export const ConnectionSetupModal: React.FC = ({ to: toColumns.length, }, }, - is_active: "Y", }; // API 호출 - const createdRelationship = await DataFlowAPI.createRelationship(relationshipData); + const createdRelationship = await DataFlowAPI.createRelationship(apiRequestData as any); toast.success("관계가 성공적으로 생성되었습니다!", { id: "create-relationship" }); diff --git a/frontend/components/dataflow/DataFlowDesigner.tsx b/frontend/components/dataflow/DataFlowDesigner.tsx index 119fd8dd..8a14149b 100644 --- a/frontend/components/dataflow/DataFlowDesigner.tsx +++ b/frontend/components/dataflow/DataFlowDesigner.tsx @@ -20,10 +20,10 @@ import { ConnectionSetupModal } from "./ConnectionSetupModal"; import { TableDefinition, TableRelationship, DataFlowAPI, DataFlowDiagram } from "@/lib/api/dataflow"; // 고유 ID 생성 함수 -const generateUniqueId = (prefix: string, relationshipId?: number): string => { +const generateUniqueId = (prefix: string, diagramId?: number): string => { const timestamp = Date.now(); const random = Math.random().toString(36).substr(2, 9); - return `${prefix}-${relationshipId || timestamp}-${random}`; + return `${prefix}-${diagramId || timestamp}-${random}`; }; // 테이블 노드 데이터 타입 정의 @@ -53,7 +53,8 @@ interface DataFlowDesignerProps { companyCode?: string; onSave?: (relationships: TableRelationship[]) => void; selectedDiagram?: DataFlowDiagram | string | null; - relationshipId?: string; + diagramId?: number; + relationshipId?: string; // 하위 호환성 유지 onBackToList?: () => void; } @@ -61,7 +62,8 @@ interface DataFlowDesignerProps { export const DataFlowDesigner: React.FC = ({ companyCode = "*", - relationshipId, + diagramId, + relationshipId, // 하위 호환성 유지 onSave, selectedDiagram, onBackToList, // eslint-disable-line @typescript-eslint/no-unused-vars @@ -86,6 +88,7 @@ export const DataFlowDesigner: React.FC = ({ }; } | null>(null); const [relationships, setRelationships] = useState([]); // eslint-disable-line @typescript-eslint/no-unused-vars + const [currentDiagramId, setCurrentDiagramId] = useState(null); // 현재 화면의 diagram_id const toastShownRef = useRef(false); // eslint-disable-line @typescript-eslint/no-unused-vars // 키보드 이벤트 핸들러 (Del 키로 선택된 노드 삭제) @@ -169,15 +172,21 @@ export const DataFlowDesigner: React.FC = ({ // 선택된 관계도의 관계 로드 const loadSelectedDiagramRelationships = useCallback(async () => { - if (!relationshipId) return; + const currentDiagramId = diagramId || (relationshipId ? parseInt(relationshipId) : null); + if (!currentDiagramId || isNaN(currentDiagramId)) return; try { - console.log("🔍 관계도 로드 시작 (relationshipId):", relationshipId); + console.log("🔍 관계도 로드 시작 (diagramId):", currentDiagramId); toast.loading("관계도를 불러오는 중...", { id: "load-diagram" }); - // relationshipId로 해당 관계도의 모든 관계 조회 - const diagramRelationships = await DataFlowAPI.getDiagramRelationshipsByRelationshipId(relationshipId); + // diagramId로 해당 관계도의 모든 관계 조회 + const diagramRelationships = await DataFlowAPI.getDiagramRelationshipsByDiagramId(currentDiagramId); console.log("📋 관계도 관계 데이터:", diagramRelationships); + + if (!Array.isArray(diagramRelationships)) { + throw new Error("관계도 데이터 형식이 올바르지 않습니다."); + } + console.log("📋 첫 번째 관계 상세:", diagramRelationships[0]); console.log( "📋 관계 객체 키들:", @@ -185,11 +194,18 @@ export const DataFlowDesigner: React.FC = ({ ); setRelationships(diagramRelationships); + // 현재 diagram_id 설정 (기존 관계도 편집 시) + if (diagramRelationships.length > 0) { + setCurrentDiagramId(diagramRelationships[0].diagram_id || null); + } + // 관계도의 모든 테이블 추출 const tableNames = new Set(); diagramRelationships.forEach((rel) => { - tableNames.add(rel.from_table_name); - tableNames.add(rel.to_table_name); + if (rel && rel.from_table_name && rel.to_table_name) { + tableNames.add(rel.from_table_name); + tableNames.add(rel.to_table_name); + } }); console.log("📊 추출된 테이블 이름들:", Array.from(tableNames)); @@ -222,10 +238,21 @@ export const DataFlowDesigner: React.FC = ({ } = {}; diagramRelationships.forEach((rel) => { + if (!rel || !rel.from_table_name || !rel.to_table_name || !rel.from_column_name || !rel.to_column_name) { + console.warn("⚠️ 관계 데이터가 불완전합니다:", rel); + return; + } + 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()); + const fromColumns = rel.from_column_name + .split(",") + .map((col) => col.trim()) + .filter((col) => col); + const toColumns = rel.to_column_name + .split(",") + .map((col) => col.trim()) + .filter((col) => col); // 소스 테이블의 컬럼들을 source로 표시 if (!connectedColumnsInfo[fromTable]) connectedColumnsInfo[fromTable] = {}; @@ -283,12 +310,28 @@ export const DataFlowDesigner: React.FC = ({ const relationshipEdges: Edge[] = []; diagramRelationships.forEach((rel) => { + if (!rel || !rel.from_table_name || !rel.to_table_name || !rel.from_column_name || !rel.to_column_name) { + console.warn("⚠️ 에지 생성 시 관계 데이터가 불완전합니다:", rel); + return; + } + 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()); + const fromColumns = rel.from_column_name + .split(",") + .map((col) => col.trim()) + .filter((col) => col); + const toColumns = rel.to_column_name + .split(",") + .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++) { @@ -296,7 +339,7 @@ export const DataFlowDesigner: React.FC = ({ const toColumn = toColumns[i] || toColumns[0]; // 컬럼이 부족하면 첫 번째 컬럼 재사용 relationshipEdges.push({ - id: generateUniqueId("edge", rel.relationship_id), + id: generateUniqueId("edge", rel.diagram_id), source: `table-${fromTable}`, target: `table-${toTable}`, sourceHandle: `${fromTable}-${fromColumn}-source`, @@ -329,7 +372,7 @@ export const DataFlowDesigner: React.FC = ({ console.error("선택된 관계도 로드 실패:", error); toast.error("관계도를 불러오는데 실패했습니다.", { id: "load-diagram" }); } - }, [relationshipId, setNodes, setEdges, selectedColumns, handleColumnClick]); + }, [diagramId, relationshipId, setNodes, setEdges, selectedColumns, handleColumnClick]); // 기존 관계 로드 (새 관계도 생성 시) const loadExistingRelationships = useCallback(async () => { @@ -356,7 +399,7 @@ export const DataFlowDesigner: React.FC = ({ const toColumn = toColumns[i] || toColumns[0]; existingEdges.push({ - id: generateUniqueId("edge", rel.relationship_id), + id: generateUniqueId("edge", rel.diagram_id), source: `table-${fromTable}`, target: `table-${toTable}`, sourceHandle: `${fromTable}-${fromColumn}-source`, @@ -391,13 +434,13 @@ export const DataFlowDesigner: React.FC = ({ // 컴포넌트 마운트 시 관계 로드 useEffect(() => { if (companyCode) { - if (relationshipId) { + if (diagramId || relationshipId) { loadSelectedDiagramRelationships(); } else { loadExistingRelationships(); } } - }, [companyCode, relationshipId, loadExistingRelationships, loadSelectedDiagramRelationships]); + }, [companyCode, diagramId, relationshipId, loadExistingRelationships, loadSelectedDiagramRelationships]); // 노드 선택 변경 핸들러 const onSelectionChange = useCallback(({ nodes }: { nodes: Node[] }) => { @@ -561,6 +604,7 @@ export const DataFlowDesigner: React.FC = ({ setSelectedColumns({}); setSelectionOrder([]); setSelectedNodes([]); + setCurrentDiagramId(null); // 현재 diagram_id도 초기화 }, [setNodes, setEdges]); // 현재 추가된 테이블명 목록 가져오기 @@ -587,7 +631,7 @@ export const DataFlowDesigner: React.FC = ({ const toColumn = toColumns[i] || toColumns[0]; newEdges.push({ - id: generateUniqueId("edge", relationship.relationship_id), + id: generateUniqueId("edge", relationship.diagram_id), source: pendingConnection.fromNode.id, target: pendingConnection.toNode.id, sourceHandle: `${fromTable}-${fromColumn}-source`, @@ -615,17 +659,19 @@ export const DataFlowDesigner: React.FC = ({ setRelationships((prev) => [...prev, relationship]); setPendingConnection(null); - console.log("관계 생성 완료:", relationship); - // 저장 콜백 호출 (필요한 경우) - if (onSave) { - // 현재 모든 관계를 수집하여 전달 - setRelationships((currentRelationships) => { - onSave([...currentRelationships, relationship]); - return currentRelationships; - }); + // 첫 번째 관계 생성 시 currentDiagramId 설정 (새 관계도 생성 시) + if (!currentDiagramId && relationship.diagram_id) { + setCurrentDiagramId(relationship.diagram_id); } + + console.log("관계 생성 완료:", relationship); + // 관계 생성 완료 후 자동으로 목록 새로고침을 위한 콜백 (선택적) + // 렌더링 중 상태 업데이트 방지를 위해 제거 + // if (onSave) { + // onSave([...relationships, relationship]); + // } }, - [pendingConnection, setEdges, onSave], + [pendingConnection, setEdges, currentDiagramId], ); // 연결 설정 취소 @@ -684,6 +730,10 @@ export const DataFlowDesigner: React.FC = ({ 연결: {edges.length}개
+
+ 관계도 ID: + {currentDiagramId || "미설정"} +
@@ -806,6 +856,7 @@ export const DataFlowDesigner: React.FC = ({ isOpen={!!pendingConnection} connection={pendingConnection} companyCode={companyCode} + diagramId={currentDiagramId || diagramId || (relationshipId ? parseInt(relationshipId) : undefined)} onConfirm={handleConfirmConnection} onCancel={handleCancelConnection} /> diff --git a/frontend/components/dataflow/DataFlowList.tsx b/frontend/components/dataflow/DataFlowList.tsx index 5aa522fd..df58c209 100644 --- a/frontend/components/dataflow/DataFlowList.tsx +++ b/frontend/components/dataflow/DataFlowList.tsx @@ -232,9 +232,9 @@ export default function DataFlowList({ onDiagramSelect, selectedDiagram, onDesig {diagrams.map((diagram) => ( handleDiagramSelect(diagram)} > diff --git a/frontend/lib/api/client.ts b/frontend/lib/api/client.ts index a9134feb..f33832d9 100644 --- a/frontend/lib/api/client.ts +++ b/frontend/lib/api/client.ts @@ -15,8 +15,8 @@ const getApiBaseUrl = (): string => { port: currentPort, }); - // 로컬 개발환경: localhost:9771 → localhost:8080 - if ((currentHost === "localhost" || currentHost === "127.0.0.1") && currentPort === "9771") { + // 로컬 개발환경: localhost:9771 또는 localhost:3000 → localhost:8080 + if ((currentHost === "localhost" || currentHost === "127.0.0.1") && (currentPort === "9771" || currentPort === "3000")) { console.log("🏠 로컬 개발 환경 감지 → localhost:8080/api"); return "http://localhost:8080/api"; } diff --git a/frontend/lib/api/dataflow.ts b/frontend/lib/api/dataflow.ts index 6678c59d..8b5f5d45 100644 --- a/frontend/lib/api/dataflow.ts +++ b/frontend/lib/api/dataflow.ts @@ -39,6 +39,7 @@ export interface TableInfo { export interface TableRelationship { relationship_id?: number; + diagram_id?: number; // 새 관계도 생성 시에는 optional relationship_name: string; from_table_name: string; from_column_name: string; @@ -89,7 +90,7 @@ export interface TableDataResponse { // 관계도 정보 인터페이스 export interface DataFlowDiagram { - relationshipId: number; + diagramId: number; diagramName: string; connectionType: string; relationshipType: string; @@ -173,7 +174,9 @@ export class DataFlowAPI { /** * 테이블 관계 생성 */ - static async createRelationship(relationship: Omit): Promise { + static async createRelationship( + relationship: any, // 백엔드 API 형식 (camelCase) + ): Promise { try { const response = await apiClient.post>( "/dataflow/table-relationships", @@ -435,11 +438,11 @@ export class DataFlowAPI { } } - // 특정 관계도의 모든 관계 조회 (relationship_id로) - static async getDiagramRelationshipsByRelationshipId(relationshipId: string): Promise { + // 특정 관계도의 모든 관계 조회 (diagram_id로) + static async getDiagramRelationshipsByDiagramId(diagramId: number): Promise { try { const response = await apiClient.get>( - `/dataflow/relationships/${relationshipId}/diagram`, + `/dataflow/diagrams/${diagramId}/relationships`, ); if (!response.data.success) {