From 72b0d2ee98550ccb30443b7e7709e2e18c049261 Mon Sep 17 00:00:00 2001 From: hyeonsu Date: Wed, 10 Sep 2025 17:48:55 +0900 Subject: [PATCH] =?UTF-8?q?=ED=85=8C=EC=9D=B4=EB=B8=94=20=EB=85=B8?= =?UTF-8?q?=EB=93=9C=20=EC=9C=84=EC=B9=98=EC=A0=95=EB=B3=B4=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend-node/prisma/schema.prisma | 17 ++-- .../controllers/dataflowDiagramController.ts | 82 +++++++++++++++---- .../src/services/dataflowDiagramService.ts | 6 ++ .../components/dataflow/DataFlowDesigner.tsx | 25 +++++- frontend/lib/api/dataflow.ts | 12 +++ 5 files changed, 116 insertions(+), 26 deletions(-) diff --git a/backend-node/prisma/schema.prisma b/backend-node/prisma/schema.prisma index 4202ff2e..5d0aa77b 100644 --- a/backend-node/prisma/schema.prisma +++ b/backend-node/prisma/schema.prisma @@ -5324,14 +5324,15 @@ model data_relationship_bridge { // 데이터플로우 관계도 - JSON 구조로 저장 model dataflow_diagrams { - diagram_id Int @id @default(autoincrement()) - diagram_name String @db.VarChar(255) - relationships Json // 모든 관계 정보를 JSON으로 저장 - company_code String @db.VarChar(50) - created_at DateTime? @default(now()) @db.Timestamp(6) - updated_at DateTime? @default(now()) @updatedAt @db.Timestamp(6) - created_by String? @db.VarChar(50) - updated_by String? @db.VarChar(50) + diagram_id Int @id @default(autoincrement()) + diagram_name String @db.VarChar(255) + relationships Json // 모든 관계 정보를 JSON으로 저장 + node_positions Json? // 테이블 노드의 캔버스 위치 정보 (JSON 형태) + company_code String @db.VarChar(50) + created_at DateTime? @default(now()) @db.Timestamp(6) + updated_at DateTime? @default(now()) @updatedAt @db.Timestamp(6) + created_by String? @db.VarChar(50) + updated_by String? @db.VarChar(50) @@unique([company_code, diagram_name], map: "unique_diagram_name_per_company") @@index([company_code], map: "idx_dataflow_diagrams_company") diff --git a/backend-node/src/controllers/dataflowDiagramController.ts b/backend-node/src/controllers/dataflowDiagramController.ts index eb7f1567..20634d64 100644 --- a/backend-node/src/controllers/dataflowDiagramController.ts +++ b/backend-node/src/controllers/dataflowDiagramController.ts @@ -17,9 +17,17 @@ export const getDataflowDiagrams = async (req: Request, res: Response) => { const page = parseInt(req.query.page as string) || 1; const size = parseInt(req.query.size as string) || 20; const searchTerm = req.query.searchTerm as string; - const companyCode = (req.query.companyCode as string) || req.headers["x-company-code"] as string || "*"; + const companyCode = + (req.query.companyCode as string) || + (req.headers["x-company-code"] as string) || + "*"; - const result = await getDataflowDiagramsService(companyCode, page, size, searchTerm); + const result = await getDataflowDiagramsService( + companyCode, + page, + size, + searchTerm + ); res.json({ success: true, @@ -41,7 +49,10 @@ export const getDataflowDiagrams = async (req: Request, res: Response) => { export const getDataflowDiagramById = async (req: Request, res: Response) => { try { const diagramId = parseInt(req.params.diagramId); - const companyCode = (req.query.companyCode as string) || req.headers["x-company-code"] as string || "*"; + const companyCode = + (req.query.companyCode as string) || + (req.headers["x-company-code"] as string) || + "*"; if (isNaN(diagramId)) { return res.status(400).json({ @@ -78,9 +89,24 @@ export const getDataflowDiagramById = async (req: Request, res: Response) => { */ export const createDataflowDiagram = async (req: Request, res: Response) => { try { - const { diagram_name, relationships, company_code, created_by, updated_by } = req.body; - const companyCode = company_code || (req.query.companyCode as string) || req.headers["x-company-code"] as string || "*"; - const userId = created_by || updated_by || req.headers["x-user-id"] as string || "SYSTEM"; + const { + diagram_name, + relationships, + node_positions, + company_code, + created_by, + updated_by, + } = req.body; + const companyCode = + company_code || + (req.query.companyCode as string) || + (req.headers["x-company-code"] as string) || + "*"; + const userId = + created_by || + updated_by || + (req.headers["x-user-id"] as string) || + "SYSTEM"; if (!diagram_name || !relationships) { return res.status(400).json({ @@ -92,6 +118,7 @@ export const createDataflowDiagram = async (req: Request, res: Response) => { const newDiagram = await createDataflowDiagramService({ diagram_name, relationships, + node_positions, company_code: companyCode, created_by: userId, updated_by: userId, @@ -104,7 +131,7 @@ export const createDataflowDiagram = async (req: Request, res: Response) => { }); } catch (error) { logger.error("관계도 생성 실패:", error); - + // 중복 이름 에러 처리 if (error instanceof Error && error.message.includes("unique constraint")) { return res.status(409).json({ @@ -128,8 +155,12 @@ export const updateDataflowDiagram = async (req: Request, res: Response) => { try { const diagramId = parseInt(req.params.diagramId); const { updated_by } = req.body; - const companyCode = (req.query.companyCode as string) || req.headers["x-company-code"] as string || "*"; - const userId = updated_by || req.headers["x-user-id"] as string || "SYSTEM"; + const companyCode = + (req.query.companyCode as string) || + (req.headers["x-company-code"] as string) || + "*"; + const userId = + updated_by || (req.headers["x-user-id"] as string) || "SYSTEM"; if (isNaN(diagramId)) { return res.status(400).json({ @@ -143,7 +174,11 @@ export const updateDataflowDiagram = async (req: Request, res: Response) => { updated_by: userId, }; - const updatedDiagram = await updateDataflowDiagramService(diagramId, updateData, companyCode); + const updatedDiagram = await updateDataflowDiagramService( + diagramId, + updateData, + companyCode + ); if (!updatedDiagram) { return res.status(404).json({ @@ -173,7 +208,10 @@ export const updateDataflowDiagram = async (req: Request, res: Response) => { export const deleteDataflowDiagram = async (req: Request, res: Response) => { try { const diagramId = parseInt(req.params.diagramId); - const companyCode = (req.query.companyCode as string) || req.headers["x-company-code"] as string || "*"; + const companyCode = + (req.query.companyCode as string) || + (req.headers["x-company-code"] as string) || + "*"; if (isNaN(diagramId)) { return res.status(400).json({ @@ -211,9 +249,18 @@ export const deleteDataflowDiagram = async (req: Request, res: Response) => { export const copyDataflowDiagram = async (req: Request, res: Response) => { try { const diagramId = parseInt(req.params.diagramId); - const { new_name, companyCode: bodyCompanyCode, userId: bodyUserId } = req.body; - const companyCode = bodyCompanyCode || (req.query.companyCode as string) || req.headers["x-company-code"] as string || "*"; - const userId = bodyUserId || req.headers["x-user-id"] as string || "SYSTEM"; + const { + new_name, + companyCode: bodyCompanyCode, + userId: bodyUserId, + } = req.body; + const companyCode = + bodyCompanyCode || + (req.query.companyCode as string) || + (req.headers["x-company-code"] as string) || + "*"; + const userId = + bodyUserId || (req.headers["x-user-id"] as string) || "SYSTEM"; if (isNaN(diagramId)) { return res.status(400).json({ @@ -222,7 +269,12 @@ export const copyDataflowDiagram = async (req: Request, res: Response) => { }); } - const copiedDiagram = await copyDataflowDiagramService(diagramId, companyCode, new_name, userId); + const copiedDiagram = await copyDataflowDiagramService( + diagramId, + companyCode, + new_name, + userId + ); if (!copiedDiagram) { return res.status(404).json({ diff --git a/backend-node/src/services/dataflowDiagramService.ts b/backend-node/src/services/dataflowDiagramService.ts index 070823e6..16524e4a 100644 --- a/backend-node/src/services/dataflowDiagramService.ts +++ b/backend-node/src/services/dataflowDiagramService.ts @@ -7,6 +7,7 @@ const prisma = new PrismaClient(); interface CreateDataflowDiagramData { diagram_name: string; relationships: any; // JSON 데이터 + node_positions?: any; // JSON 데이터 (노드 위치 정보) company_code: string; created_by: string; updated_by: string; @@ -15,6 +16,7 @@ interface CreateDataflowDiagramData { interface UpdateDataflowDiagramData { diagram_name?: string; relationships?: any; // JSON 데이터 + node_positions?: any; // JSON 데이터 (노드 위치 정보) updated_by: string; } @@ -116,6 +118,7 @@ export const createDataflowDiagram = async ( data: { diagram_name: data.diagram_name, relationships: data.relationships, + node_positions: data.node_positions || null, company_code: data.company_code, created_by: data.created_by, updated_by: data.updated_by, @@ -164,6 +167,9 @@ export const updateDataflowDiagram = async ( data: { ...(data.diagram_name && { diagram_name: data.diagram_name }), ...(data.relationships && { relationships: data.relationships }), + ...(data.node_positions !== undefined && { + node_positions: data.node_positions, + }), updated_by: data.updated_by, updated_at: new Date(), }, diff --git a/frontend/components/dataflow/DataFlowDesigner.tsx b/frontend/components/dataflow/DataFlowDesigner.tsx index dbb7f615..9c4f614e 100644 --- a/frontend/components/dataflow/DataFlowDesigner.tsx +++ b/frontend/components/dataflow/DataFlowDesigner.tsx @@ -24,6 +24,7 @@ import { DataFlowDiagram, JsonRelationship, CreateDiagramRequest, + NodePositions, } from "@/lib/api/dataflow"; import SaveDiagramModal from "./SaveDiagramModal"; import { useAuth } from "@/hooks/useAuth"; @@ -313,10 +314,16 @@ export const DataFlowDesigner: React.FC = ({ console.log("🔌 연결된 컬럼 정보:", connectedColumnsInfo); - // 테이블을 노드로 변환 (자동 레이아웃) + // 저장된 노드 위치 정보 가져오기 + const savedNodePositions = jsonDiagram.node_positions || {}; + console.log("📍 저장된 노드 위치:", savedNodePositions); + + // 테이블을 노드로 변환 (저장된 위치 우선 사용, 없으면 자동 레이아웃) const tableNodes = tableDefinitions.map((table, index) => { - const x = (index % 3) * 400 + 100; // 3열 배치 - const y = Math.floor(index / 3) * 300 + 100; + // 저장된 위치가 있으면 사용, 없으면 자동 배치 + const savedPosition = savedNodePositions[table.tableName]; + const x = savedPosition ? savedPosition.x : (index % 3) * 400 + 100; // 3열 배치 + const y = savedPosition ? savedPosition.y : Math.floor(index / 3) * 300 + 100; return { id: `table-${table.tableName}`, @@ -782,6 +789,17 @@ export const DataFlowDesigner: React.FC = ({ new Set([...tempRelationships.map((rel) => rel.fromTable), ...tempRelationships.map((rel) => rel.toTable)]), ).sort(); + // 현재 노드 위치 추출 + const nodePositions: NodePositions = {}; + nodes.forEach((node) => { + if (node.data?.table?.tableName) { + nodePositions[node.data.table.tableName] = { + x: node.position.x, + y: node.position.y, + }; + } + }); + // 저장 요청 데이터 생성 const createRequest: CreateDiagramRequest = { diagram_name: diagramName, @@ -789,6 +807,7 @@ export const DataFlowDesigner: React.FC = ({ relationships: tempRelationships, tables: connectedTables, }, + node_positions: nodePositions, }; let savedDiagram; diff --git a/frontend/lib/api/dataflow.ts b/frontend/lib/api/dataflow.ts index 55c46414..ad099ccc 100644 --- a/frontend/lib/api/dataflow.ts +++ b/frontend/lib/api/dataflow.ts @@ -115,6 +115,16 @@ export interface DataFlowDiagramsResponse { hasPrev: boolean; } +// 노드 위치 정보 타입 +export interface NodePosition { + x: number; + y: number; +} + +export interface NodePositions { + [tableName: string]: NodePosition; +} + // 새로운 JSON 기반 타입들 export interface JsonDataFlowDiagram { diagram_id: number; @@ -123,6 +133,7 @@ export interface JsonDataFlowDiagram { relationships: JsonRelationship[]; tables: string[]; }; + node_positions?: NodePositions; company_code: string; created_at?: string; updated_at?: string; @@ -147,6 +158,7 @@ export interface CreateDiagramRequest { relationships: JsonRelationship[]; tables: string[]; }; + node_positions?: NodePositions; } export interface JsonDataFlowDiagramsResponse {