From ac03f311b0f83cdd2ed3a6aa1996ebbadfda7a75 Mon Sep 17 00:00:00 2001 From: hyeonsu Date: Mon, 8 Sep 2025 18:18:47 +0900 Subject: [PATCH] =?UTF-8?q?=EB=8B=A8=EC=88=9C=20=ED=82=A4=20=EC=97=B0?= =?UTF-8?q?=EA=B2=B0=20=EA=B5=AC=ED=98=84=20=EC=8B=9C=20=EC=A6=9D=EA=B3=84?= =?UTF-8?q?=20=ED=85=8C=EC=9E=85=EB=A5=B4=EC=97=90=20=EB=A0=88=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=83=9D=EC=84=B1=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 | 12 +- .../src/controllers/dataflowController.ts | 79 ++++++-- backend-node/src/routes/dataflowRoutes.ts | 9 + backend-node/src/services/dataflowService.ts | 175 ++++++++++++++---- .../dataflow/ConnectionSetupModal.tsx | 22 --- .../components/dataflow/DataFlowDesigner.tsx | 2 +- frontend/lib/api/dataflow.ts | 56 +++++- 7 files changed, 269 insertions(+), 86 deletions(-) diff --git a/backend-node/prisma/schema.prisma b/backend-node/prisma/schema.prisma index af59bceb..b792f93a 100644 --- a/backend-node/prisma/schema.prisma +++ b/backend-node/prisma/schema.prisma @@ -5139,14 +5139,10 @@ model data_relationship_bridge { // 소스 테이블 정보 from_table_name String @db.VarChar(100) from_column_name String @db.VarChar(100) - from_key_value String? @db.VarChar(500) // 실제 연결 키 값 - from_record_id String? @db.VarChar(100) // 소스 레코드의 Primary Key // 타겟 테이블 정보 to_table_name String @db.VarChar(100) to_column_name String @db.VarChar(100) - to_key_value String? @db.VarChar(500) // 실제 연결 키 값 - to_record_id String? @db.VarChar(100) // 타겟 레코드의 Primary Key // 메타데이터 connection_type String @db.VarChar(20) // 'simple-key', 'data-save', 'external-call' @@ -5164,12 +5160,12 @@ model data_relationship_bridge { relationship table_relationships @relation(fields: [relationship_id], references: [relationship_id], onDelete: Cascade) @@index([relationship_id], map: "idx_data_bridge_relationship") - @@index([from_table_name, from_key_value], map: "idx_data_bridge_from_table") - @@index([to_table_name, to_key_value], map: "idx_data_bridge_to_table") + @@index([from_table_name], map: "idx_data_bridge_from_table") + @@index([to_table_name], map: "idx_data_bridge_to_table") @@index([company_code], map: "idx_data_bridge_company") @@index([is_active], map: "idx_data_bridge_active") @@index([connection_type], map: "idx_data_bridge_connection_type") - @@index([from_table_name, from_column_name, from_key_value], map: "idx_data_bridge_from_lookup") - @@index([to_table_name, to_column_name, to_key_value], map: "idx_data_bridge_to_lookup") + @@index([from_table_name, from_column_name], map: "idx_data_bridge_from_lookup") + @@index([to_table_name, to_column_name], map: "idx_data_bridge_to_lookup") @@index([company_code, is_active], map: "idx_data_bridge_company_active") } diff --git a/backend-node/src/controllers/dataflowController.ts b/backend-node/src/controllers/dataflowController.ts index c2a842aa..93d20f96 100644 --- a/backend-node/src/controllers/dataflowController.ts +++ b/backend-node/src/controllers/dataflowController.ts @@ -366,12 +366,8 @@ export async function createDataLink( relationshipId, fromTableName, fromColumnName, - fromKeyValue, - fromRecordId, toTableName, toColumnName, - toKeyValue, - toRecordId, connectionType, bridgeData, } = req.body; @@ -381,10 +377,8 @@ export async function createDataLink( !relationshipId || !fromTableName || !fromColumnName || - !fromKeyValue || !toTableName || !toColumnName || - !toKeyValue || !connectionType ) { const response: ApiResponse = { @@ -393,7 +387,7 @@ export async function createDataLink( error: { code: "MISSING_REQUIRED_FIELDS", details: - "필수 필드: relationshipId, fromTableName, fromColumnName, fromKeyValue, toTableName, toColumnName, toKeyValue, connectionType", + "필수 필드: relationshipId, fromTableName, fromColumnName, toTableName, toColumnName, connectionType", }, }; res.status(400).json(response); @@ -409,12 +403,8 @@ export async function createDataLink( relationshipId, fromTableName, fromColumnName, - fromKeyValue, - fromRecordId, toTableName, toColumnName, - toKeyValue, - toRecordId, connectionType, companyCode, bridgeData, @@ -551,3 +541,70 @@ export async function deleteDataLink( res.status(500).json(response); } } + +// ==================== 테이블 데이터 조회 ==================== + +/** + * 테이블 실제 데이터 조회 (페이징) + * GET /api/dataflow/table-data/:tableName + */ +export async function getTableData(req: Request, res: Response): Promise { + try { + const { tableName } = req.params; + const { + page = "1", + limit = "10", + search = "", + searchColumn = "", + } = req.query; + + if (!tableName) { + const response: ApiResponse = { + success: false, + message: "테이블명이 필요합니다.", + error: { + code: "MISSING_TABLE_NAME", + details: "테이블명을 제공해주세요.", + }, + }; + res.status(400).json(response); + return; + } + + const pageNum = parseInt(page as string) || 1; + const limitNum = parseInt(limit as string) || 10; + const userInfo = (req as any).user; + const companyCode = userInfo?.company_code || "*"; + + const dataflowService = new DataflowService(); + const result = await dataflowService.getTableData( + tableName, + pageNum, + limitNum, + search as string, + searchColumn as string, + companyCode + ); + + const response: ApiResponse = { + success: true, + message: "테이블 데이터를 성공적으로 조회했습니다.", + data: result, + }; + + res.status(200).json(response); + } catch (error) { + logger.error("테이블 데이터 조회 중 오류 발생:", error); + + const response: ApiResponse = { + success: false, + message: "테이블 데이터 조회 중 오류가 발생했습니다.", + error: { + code: "TABLE_DATA_GET_ERROR", + details: error instanceof Error ? error.message : "Unknown error", + }, + }; + + res.status(500).json(response); + } +} diff --git a/backend-node/src/routes/dataflowRoutes.ts b/backend-node/src/routes/dataflowRoutes.ts index f3dd28c4..75569771 100644 --- a/backend-node/src/routes/dataflowRoutes.ts +++ b/backend-node/src/routes/dataflowRoutes.ts @@ -9,6 +9,7 @@ import { createDataLink, getLinkedDataByRelationship, deleteDataLink, + getTableData, } from "../controllers/dataflowController"; const router = express.Router(); @@ -69,4 +70,12 @@ router.get( */ router.delete("/data-links/:bridgeId", deleteDataLink); +// ==================== 테이블 데이터 조회 라우트 ==================== + +/** + * 테이블 실제 데이터 조회 + * GET /api/dataflow/table-data/:tableName + */ +router.get("/table-data/:tableName", getTableData); + export default router; diff --git a/backend-node/src/services/dataflowService.ts b/backend-node/src/services/dataflowService.ts index 7a884378..a7e45747 100644 --- a/backend-node/src/services/dataflowService.ts +++ b/backend-node/src/services/dataflowService.ts @@ -56,27 +56,63 @@ export class DataflowService { ); } - // 새 관계 생성 - const relationship = await prisma.table_relationships.create({ - data: { - relationship_name: data.relationshipName, - from_table_name: data.fromTableName, - from_column_name: data.fromColumnName, - to_table_name: data.toTableName, - to_column_name: data.toColumnName, - relationship_type: data.relationshipType, - connection_type: data.connectionType, - company_code: data.companyCode, - settings: data.settings, - created_by: data.createdBy, - updated_by: data.createdBy, - }, + // 트랜잭션으로 관계 생성과 단순 키값 연결 처리 + const result = await prisma.$transaction(async (tx) => { + // 1. 새 관계 생성 + const relationship = await tx.table_relationships.create({ + data: { + relationship_name: data.relationshipName, + from_table_name: data.fromTableName, + from_column_name: data.fromColumnName, + to_table_name: data.toTableName, + to_column_name: data.toColumnName, + relationship_type: data.relationshipType, + connection_type: data.connectionType, + company_code: data.companyCode, + settings: data.settings, + created_by: data.createdBy, + updated_by: data.createdBy, + }, + }); + + // 2. 단순 키값 연결인 경우 data_relationship_bridge에도 기본 레코드 생성 + if (data.connectionType === "simple-key") { + logger.info( + `단순 키값 연결이므로 data_relationship_bridge에 기본 연결 레코드 생성 - 관계ID: ${relationship.relationship_id}` + ); + + await tx.data_relationship_bridge.create({ + data: { + relationship_id: relationship.relationship_id, + from_table_name: data.fromTableName, + from_column_name: data.fromColumnName, + to_table_name: data.toTableName, + to_column_name: data.toColumnName, + connection_type: data.connectionType, + company_code: data.companyCode, + bridge_data: { + autoCreated: true, + createdAt: new Date().toISOString(), + notes: "단순 키값 연결 - 테이블과 컬럼 관계만 정의", + connectionInfo: `${data.fromTableName}.${data.fromColumnName} ↔ ${data.toTableName}.${data.toColumnName}`, + settings: data.settings, + }, + created_by: data.createdBy, + }, + }); + + logger.info( + `단순 키값 연결 기본 레코드 생성 완료 - 관계ID: ${relationship.relationship_id}` + ); + } + + return relationship; }); logger.info( - `DataflowService: 테이블 관계 생성 완료 - ID: ${relationship.relationship_id}` + `DataflowService: 테이블 관계 생성 완료 - ID: ${result.relationship_id}` ); - return relationship; + return result; } catch (error) { logger.error("DataflowService: 테이블 관계 생성 실패", error); throw error; @@ -386,12 +422,8 @@ export class DataflowService { relationshipId: number; fromTableName: string; fromColumnName: string; - fromKeyValue: string; - fromRecordId?: string; toTableName: string; toColumnName: string; - toKeyValue: string; - toRecordId?: string; connectionType: string; companyCode: string; bridgeData?: any; @@ -407,12 +439,8 @@ export class DataflowService { relationship_id: linkData.relationshipId, from_table_name: linkData.fromTableName, from_column_name: linkData.fromColumnName, - from_key_value: linkData.fromKeyValue, - from_record_id: linkData.fromRecordId, to_table_name: linkData.toTableName, to_column_name: linkData.toColumnName, - to_key_value: linkData.toKeyValue, - to_record_id: linkData.toRecordId, connection_type: linkData.connectionType, company_code: linkData.companyCode, bridge_data: linkData.bridgeData || {}, @@ -494,13 +522,7 @@ export class DataflowService { is_active: "Y", }; - // 특정 키 값으로 필터링 - if (keyValue) { - whereCondition.OR = [ - { from_table_name: tableName, from_key_value: keyValue }, - { to_table_name: tableName, to_key_value: keyValue }, - ]; - } + // keyValue 파라미터는 더 이상 사용하지 않음 (key_value 필드 제거됨) // 회사코드 필터링 if (companyCode && companyCode !== "*") { @@ -537,10 +559,6 @@ export class DataflowService { async updateDataLink( bridgeId: number, updateData: { - fromKeyValue?: string; - fromRecordId?: string; - toKeyValue?: string; - toRecordId?: string; bridgeData?: any; updatedBy: string; }, @@ -662,4 +680,89 @@ export class DataflowService { throw error; } } + + // ==================== 테이블 데이터 조회 ==================== + + /** + * 테이블 실제 데이터 조회 (페이징) + */ + async getTableData( + tableName: string, + page: number = 1, + limit: number = 10, + search: string = "", + searchColumn: string = "", + companyCode: string = "*" + ) { + try { + logger.info(`DataflowService: 테이블 데이터 조회 시작 - ${tableName}`); + + // 테이블 존재 여부 확인 (정보 스키마 사용) + const tableExists = await prisma.$queryRaw` + SELECT table_name + FROM information_schema.tables + WHERE table_name = ${tableName.toLowerCase()} + AND table_schema = 'public' + `; + + if ( + !tableExists || + (Array.isArray(tableExists) && tableExists.length === 0) + ) { + throw new Error(`테이블 '${tableName}'이 존재하지 않습니다.`); + } + + // 전체 데이터 개수 조회 + let totalCountQuery = `SELECT COUNT(*) as total FROM "${tableName}"`; + let dataQuery = `SELECT * FROM "${tableName}"`; + + // 검색 조건 추가 + if (search && searchColumn) { + const whereCondition = `WHERE "${searchColumn}" ILIKE '%${search}%'`; + totalCountQuery += ` ${whereCondition}`; + dataQuery += ` ${whereCondition}`; + } + + // 페이징 처리 + const offset = (page - 1) * limit; + dataQuery += ` ORDER BY 1 LIMIT ${limit} OFFSET ${offset}`; + + // 실제 쿼리 실행 + const [totalResult, dataResult] = await Promise.all([ + prisma.$queryRawUnsafe(totalCountQuery), + prisma.$queryRawUnsafe(dataQuery), + ]); + + const total = + Array.isArray(totalResult) && totalResult.length > 0 + ? Number((totalResult[0] as any).total) + : 0; + + const data = Array.isArray(dataResult) ? dataResult : []; + + const result = { + data, + pagination: { + page, + limit, + total, + totalPages: Math.ceil(total / limit), + hasNext: page < Math.ceil(total / limit), + hasPrev: page > 1, + }, + }; + + logger.info( + `DataflowService: 테이블 데이터 조회 완료 - ${tableName}, 총 ${total}건 중 ${data.length}건 조회` + ); + + return result; + } catch (error) { + logger.error( + `DataflowService: 테이블 데이터 조회 실패 - ${tableName}`, + error + ); + throw error; + } + } } diff --git a/frontend/components/dataflow/ConnectionSetupModal.tsx b/frontend/components/dataflow/ConnectionSetupModal.tsx index 5c3973cb..79c4553b 100644 --- a/frontend/components/dataflow/ConnectionSetupModal.tsx +++ b/frontend/components/dataflow/ConnectionSetupModal.tsx @@ -47,7 +47,6 @@ interface ConnectionConfig { // 단순 키값 연결 설정 interface SimpleKeySettings { - syncDirection: "unidirectional" | "bidirectional"; notes: string; } @@ -94,7 +93,6 @@ export const ConnectionSetupModal: React.FC = ({ // 연결 종류별 설정 상태 const [simpleKeySettings, setSimpleKeySettings] = useState({ - syncDirection: "bidirectional", notes: "", }); @@ -130,7 +128,6 @@ export const ConnectionSetupModal: React.FC = ({ // 단순 키값 연결 기본값 설정 setSimpleKeySettings({ - syncDirection: "bidirectional", notes: `${fromTableName}과 ${toTableName} 간의 키값 연결`, }); @@ -238,25 +235,6 @@ export const ConnectionSetupModal: React.FC = ({ 단순 키값 연결 설정
-
- - -