diff --git a/backend-node/prisma/schema.prisma b/backend-node/prisma/schema.prisma index 7859e626..afdeb691 100644 --- a/backend-node/prisma/schema.prisma +++ b/backend-node/prisma/schema.prisma @@ -5100,3 +5100,72 @@ model code_info { @@id([code_category, code_value], map: "pk_code_info") @@index([code_category, sort_order], map: "idx_code_info_sort") } + +// 테이블 간 관계 정의 +model table_relationships { + relationship_id Int @id @default(autoincrement()) + relationship_name String @db.VarChar(200) + from_table_name String @db.VarChar(100) + from_column_name String @db.VarChar(100) + to_table_name String @db.VarChar(100) + to_column_name String @db.VarChar(100) + relationship_type String @db.VarChar(20) // 'one-to-one', 'one-to-many', 'many-to-one', 'many-to-many' + connection_type String @db.VarChar(20) // 'simple-key', 'data-save', 'external-call' + company_code String @db.VarChar(50) + settings Json? // 연결 종류별 세부 설정 + is_active String? @default("Y") @db.Char(1) + created_date DateTime? @default(now()) @db.Timestamp(6) + created_by String? @db.VarChar(50) + updated_date DateTime? @default(now()) @db.Timestamp(6) + updated_by String? @db.VarChar(50) + + // 역참조 관계 + bridges data_relationship_bridge[] + + @@index([company_code], map: "idx_table_relationships_company_code") + @@index([from_table_name], map: "idx_table_relationships_from_table") + @@index([to_table_name], map: "idx_table_relationships_to_table") +} + +// 테이블 간 데이터 관계 중계 테이블 - 실제 데이터 연결 정보 저장 +model data_relationship_bridge { + bridge_id Int @id @default(autoincrement()) + relationship_id Int + + // 소스 테이블 정보 + 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' + company_code String @db.VarChar(50) + created_at DateTime @default(now()) @db.Timestamp(6) + created_by String? @db.VarChar(50) + updated_at DateTime @default(now()) @db.Timestamp(6) + updated_by String? @db.VarChar(50) + is_active String @default("Y") @db.Char(1) + + // 추가 설정 (JSON) + bridge_data Json? // 연결 종류별 추가 데이터 + + // 관계 설정 + 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([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([company_code, is_active], map: "idx_data_bridge_company_active") +} diff --git a/backend-node/src/app.ts b/backend-node/src/app.ts index ac92d38b..81b40ddb 100644 --- a/backend-node/src/app.ts +++ b/backend-node/src/app.ts @@ -18,6 +18,7 @@ import commonCodeRoutes from "./routes/commonCodeRoutes"; import dynamicFormRoutes from "./routes/dynamicFormRoutes"; import fileRoutes from "./routes/fileRoutes"; import companyManagementRoutes from "./routes/companyManagementRoutes"; +import dataflowRoutes from "./routes/dataflowRoutes"; // import userRoutes from './routes/userRoutes'; // import menuRoutes from './routes/menuRoutes'; @@ -83,6 +84,7 @@ app.use("/api/common-codes", commonCodeRoutes); app.use("/api/dynamic-form", dynamicFormRoutes); app.use("/api/files", fileRoutes); app.use("/api/company-management", companyManagementRoutes); +app.use("/api/dataflow", dataflowRoutes); // app.use('/api/users', userRoutes); // app.use('/api/menus', menuRoutes); diff --git a/backend-node/src/controllers/dataflowController.ts b/backend-node/src/controllers/dataflowController.ts new file mode 100644 index 00000000..c2a842aa --- /dev/null +++ b/backend-node/src/controllers/dataflowController.ts @@ -0,0 +1,553 @@ +import { Request, Response } from "express"; +import { logger } from "../utils/logger"; +import { AuthenticatedRequest } from "../types/auth"; +import { ApiResponse } from "../types/common"; +import { DataflowService } from "../services/dataflowService"; + +/** + * 테이블 관계 생성 + */ +export async function createTableRelationship( + req: AuthenticatedRequest, + res: Response +): Promise { + try { + logger.info("=== 테이블 관계 생성 시작 ==="); + + const { + relationshipName, + fromTableName, + fromColumnName, + toTableName, + toColumnName, + relationshipType, + connectionType, + settings, + } = req.body; + + // 필수 필드 검증 + if ( + !relationshipName || + !fromTableName || + !fromColumnName || + !toTableName || + !toColumnName + ) { + const response: ApiResponse = { + success: false, + message: "필수 필드가 누락되었습니다.", + error: { + code: "MISSING_REQUIRED_FIELDS", + details: + "relationshipName, fromTableName, fromColumnName, toTableName, toColumnName는 필수입니다.", + }, + }; + res.status(400).json(response); + return; + } + + // 사용자 정보에서 회사 코드 가져오기 + const companyCode = (req.user as any)?.company_code || "*"; + const userId = (req.user as any)?.userId || "system"; + + const dataflowService = new DataflowService(); + const relationship = await dataflowService.createTableRelationship({ + relationshipName, + fromTableName, + fromColumnName, + toTableName, + toColumnName, + relationshipType: relationshipType || "one-to-one", + connectionType: connectionType || "simple-key", + companyCode, + settings: settings || {}, + createdBy: userId, + }); + + logger.info(`테이블 관계 생성 완료: ${relationship.relationship_id}`); + + const response: ApiResponse = { + success: true, + message: "테이블 관계가 성공적으로 생성되었습니다.", + data: relationship, + }; + + res.status(201).json(response); + } catch (error) { + logger.error("테이블 관계 생성 중 오류 발생:", error); + + const response: ApiResponse = { + success: false, + message: "테이블 관계 생성 중 오류가 발생했습니다.", + error: { + code: "TABLE_RELATIONSHIP_CREATE_ERROR", + details: error instanceof Error ? error.message : "Unknown error", + }, + }; + + res.status(500).json(response); + } +} + +/** + * 테이블 관계 목록 조회 (회사별) + */ +export async function getTableRelationships( + req: AuthenticatedRequest, + res: Response +): Promise { + try { + logger.info("=== 테이블 관계 목록 조회 시작 ==="); + + // 사용자 정보에서 회사 코드 가져오기 + const companyCode = (req.user as any)?.company_code || "*"; + + const dataflowService = new DataflowService(); + const relationships = + await dataflowService.getTableRelationships(companyCode); + + logger.info(`테이블 관계 목록 조회 완료: ${relationships.length}개`); + + 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: "TABLE_RELATIONSHIPS_LIST_ERROR", + details: error instanceof Error ? error.message : "Unknown error", + }, + }; + + res.status(500).json(response); + } +} + +/** + * 테이블 관계 수정 + */ +export async function updateTableRelationship( + req: AuthenticatedRequest, + res: Response +): Promise { + try { + logger.info("=== 테이블 관계 수정 시작 ==="); + + const { relationshipId } = req.params; + const updateData = req.body; + + if (!relationshipId) { + const response: ApiResponse = { + success: false, + message: "관계 ID가 필요합니다.", + error: { + code: "MISSING_RELATIONSHIP_ID", + details: "relationshipId 파라미터가 누락되었습니다.", + }, + }; + res.status(400).json(response); + return; + } + + // 사용자 정보에서 회사 코드와 사용자 ID 가져오기 + const companyCode = (req.user as any)?.company_code || "*"; + const userId = (req.user as any)?.userId || "system"; + + const dataflowService = new DataflowService(); + const relationship = await dataflowService.updateTableRelationship( + parseInt(relationshipId), + { + ...updateData, + updatedBy: userId, + }, + companyCode + ); + + if (!relationship) { + const response: ApiResponse = { + success: false, + message: "테이블 관계를 찾을 수 없습니다.", + error: { + code: "TABLE_RELATIONSHIP_NOT_FOUND", + details: `관계 ID ${relationshipId}를 찾을 수 없습니다.`, + }, + }; + res.status(404).json(response); + return; + } + + logger.info(`테이블 관계 수정 완료: ${relationshipId}`); + + const response: ApiResponse = { + success: true, + message: "테이블 관계가 성공적으로 수정되었습니다.", + data: relationship, + }; + + res.status(200).json(response); + } catch (error) { + logger.error("테이블 관계 수정 중 오류 발생:", error); + + const response: ApiResponse = { + success: false, + message: "테이블 관계 수정 중 오류가 발생했습니다.", + error: { + code: "TABLE_RELATIONSHIP_UPDATE_ERROR", + details: error instanceof Error ? error.message : "Unknown error", + }, + }; + + res.status(500).json(response); + } +} + +/** + * 테이블 관계 삭제 + */ +export async function deleteTableRelationship( + req: AuthenticatedRequest, + res: Response +): Promise { + try { + logger.info("=== 테이블 관계 삭제 시작 ==="); + + const { relationshipId } = req.params; + + if (!relationshipId) { + const response: ApiResponse = { + success: false, + message: "관계 ID가 필요합니다.", + error: { + code: "MISSING_RELATIONSHIP_ID", + details: "relationshipId 파라미터가 누락되었습니다.", + }, + }; + res.status(400).json(response); + return; + } + + // 사용자 정보에서 회사 코드 가져오기 + const companyCode = (req.user as any)?.company_code || "*"; + + const dataflowService = new DataflowService(); + const success = await dataflowService.deleteTableRelationship( + parseInt(relationshipId), + companyCode + ); + + if (!success) { + const response: ApiResponse = { + success: false, + message: "테이블 관계를 찾을 수 없습니다.", + error: { + code: "TABLE_RELATIONSHIP_NOT_FOUND", + details: `관계 ID ${relationshipId}를 찾을 수 없습니다.`, + }, + }; + res.status(404).json(response); + return; + } + + logger.info(`테이블 관계 삭제 완료: ${relationshipId}`); + + const response: ApiResponse = { + success: true, + message: "테이블 관계가 성공적으로 삭제되었습니다.", + }; + + res.status(200).json(response); + } catch (error) { + logger.error("테이블 관계 삭제 중 오류 발생:", error); + + const response: ApiResponse = { + success: false, + message: "테이블 관계 삭제 중 오류가 발생했습니다.", + error: { + code: "TABLE_RELATIONSHIP_DELETE_ERROR", + details: error instanceof Error ? error.message : "Unknown error", + }, + }; + + res.status(500).json(response); + } +} + +/** + * 특정 테이블 관계 조회 + */ +export async function getTableRelationship( + req: AuthenticatedRequest, + res: Response +): Promise { + try { + logger.info("=== 테이블 관계 조회 시작 ==="); + + const { relationshipId } = req.params; + + if (!relationshipId) { + const response: ApiResponse = { + success: false, + message: "관계 ID가 필요합니다.", + error: { + code: "MISSING_RELATIONSHIP_ID", + details: "relationshipId 파라미터가 누락되었습니다.", + }, + }; + res.status(400).json(response); + return; + } + + // 사용자 정보에서 회사 코드 가져오기 + const companyCode = (req.user as any)?.company_code || "*"; + + const dataflowService = new DataflowService(); + const relationship = await dataflowService.getTableRelationship( + parseInt(relationshipId), + companyCode + ); + + if (!relationship) { + const response: ApiResponse = { + success: false, + message: "테이블 관계를 찾을 수 없습니다.", + error: { + code: "TABLE_RELATIONSHIP_NOT_FOUND", + details: `관계 ID ${relationshipId}를 찾을 수 없습니다.`, + }, + }; + res.status(404).json(response); + return; + } + + logger.info(`테이블 관계 조회 완료: ${relationshipId}`); + + const response: ApiResponse = { + success: true, + message: "테이블 관계를 성공적으로 조회했습니다.", + data: relationship, + }; + + res.status(200).json(response); + } catch (error) { + logger.error("테이블 관계 조회 중 오류 발생:", error); + + const response: ApiResponse = { + success: false, + message: "테이블 관계 조회 중 오류가 발생했습니다.", + error: { + code: "TABLE_RELATIONSHIP_GET_ERROR", + details: error instanceof Error ? error.message : "Unknown error", + }, + }; + + res.status(500).json(response); + } +} + +// ==================== 데이터 연결 관리 API ==================== + +/** + * 데이터 관계 연결 생성 + */ +export async function createDataLink( + req: Request, + res: Response +): Promise { + try { + const { + relationshipId, + fromTableName, + fromColumnName, + fromKeyValue, + fromRecordId, + toTableName, + toColumnName, + toKeyValue, + toRecordId, + connectionType, + bridgeData, + } = req.body; + + // 필수 필드 검증 + if ( + !relationshipId || + !fromTableName || + !fromColumnName || + !fromKeyValue || + !toTableName || + !toColumnName || + !toKeyValue || + !connectionType + ) { + const response: ApiResponse = { + success: false, + message: "필수 필드가 누락되었습니다.", + error: { + code: "MISSING_REQUIRED_FIELDS", + details: + "필수 필드: relationshipId, fromTableName, fromColumnName, fromKeyValue, toTableName, toColumnName, toKeyValue, connectionType", + }, + }; + res.status(400).json(response); + return; + } + + const userInfo = (req as any).user; + const companyCode = userInfo?.company_code || "*"; + const createdBy = userInfo?.userId || "system"; + + const dataflowService = new DataflowService(); + const bridge = await dataflowService.createDataLink({ + relationshipId, + fromTableName, + fromColumnName, + fromKeyValue, + fromRecordId, + toTableName, + toColumnName, + toKeyValue, + toRecordId, + connectionType, + companyCode, + bridgeData, + createdBy, + }); + + const response: ApiResponse = { + success: true, + message: "데이터 연결이 성공적으로 생성되었습니다.", + data: bridge, + }; + + res.status(201).json(response); + } catch (error) { + logger.error("데이터 연결 생성 중 오류 발생:", error); + + const response: ApiResponse = { + success: false, + message: "데이터 연결 생성 중 오류가 발생했습니다.", + error: { + code: "DATA_LINK_CREATE_ERROR", + details: error instanceof Error ? error.message : "Unknown error", + }, + }; + + res.status(500).json(response); + } +} + +/** + * 관계별 연결된 데이터 조회 + */ +export async function getLinkedDataByRelationship( + req: Request, + res: Response +): Promise { + try { + const relationshipId = parseInt(req.params.relationshipId); + + if (!relationshipId || isNaN(relationshipId)) { + const response: ApiResponse = { + success: false, + message: "유효하지 않은 관계 ID입니다.", + error: { + code: "INVALID_RELATIONSHIP_ID", + details: "관계 ID는 숫자여야 합니다.", + }, + }; + res.status(400).json(response); + return; + } + + const userInfo = (req as any).user; + const companyCode = userInfo?.company_code || "*"; + + const dataflowService = new DataflowService(); + const linkedData = await dataflowService.getLinkedDataByRelationship( + relationshipId, + companyCode + ); + + const response: ApiResponse = { + success: true, + message: "연결된 데이터를 성공적으로 조회했습니다.", + data: linkedData, + }; + + res.status(200).json(response); + } catch (error) { + logger.error("연결된 데이터 조회 중 오류 발생:", error); + + const response: ApiResponse = { + success: false, + message: "연결된 데이터 조회 중 오류가 발생했습니다.", + error: { + code: "LINKED_DATA_GET_ERROR", + details: error instanceof Error ? error.message : "Unknown error", + }, + }; + + res.status(500).json(response); + } +} + +/** + * 데이터 연결 삭제 + */ +export async function deleteDataLink( + req: Request, + res: Response +): Promise { + try { + const bridgeId = parseInt(req.params.bridgeId); + + if (!bridgeId || isNaN(bridgeId)) { + const response: ApiResponse = { + success: false, + message: "유효하지 않은 Bridge ID입니다.", + error: { + code: "INVALID_BRIDGE_ID", + details: "Bridge ID는 숫자여야 합니다.", + }, + }; + res.status(400).json(response); + return; + } + + const userInfo = (req as any).user; + const companyCode = userInfo?.company_code || "*"; + const deletedBy = userInfo?.userId || "system"; + + const dataflowService = new DataflowService(); + await dataflowService.deleteDataLink(bridgeId, companyCode, deletedBy); + + const response: ApiResponse = { + success: true, + message: "데이터 연결이 성공적으로 삭제되었습니다.", + data: null, + }; + + res.status(200).json(response); + } catch (error) { + logger.error("데이터 연결 삭제 중 오류 발생:", error); + + const response: ApiResponse = { + success: false, + message: "데이터 연결 삭제 중 오류가 발생했습니다.", + error: { + code: "DATA_LINK_DELETE_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 new file mode 100644 index 00000000..f3dd28c4 --- /dev/null +++ b/backend-node/src/routes/dataflowRoutes.ts @@ -0,0 +1,72 @@ +import express from "express"; +import { authenticateToken } from "../middleware/authMiddleware"; +import { + createTableRelationship, + getTableRelationships, + getTableRelationship, + updateTableRelationship, + deleteTableRelationship, + createDataLink, + getLinkedDataByRelationship, + deleteDataLink, +} from "../controllers/dataflowController"; + +const router = express.Router(); + +// 모든 라우트에 인증 미들웨어 적용 +router.use(authenticateToken); + +/** + * 테이블 관계 생성 + * POST /api/dataflow/table-relationships + */ +router.post("/table-relationships", createTableRelationship); + +/** + * 테이블 관계 목록 조회 (회사별) + * GET /api/dataflow/table-relationships + */ +router.get("/table-relationships", getTableRelationships); + +/** + * 특정 테이블 관계 조회 + * GET /api/dataflow/table-relationships/:relationshipId + */ +router.get("/table-relationships/:relationshipId", getTableRelationship); + +/** + * 테이블 관계 수정 + * PUT /api/dataflow/table-relationships/:relationshipId + */ +router.put("/table-relationships/:relationshipId", updateTableRelationship); + +/** + * 테이블 관계 삭제 + * DELETE /api/dataflow/table-relationships/:relationshipId + */ +router.delete("/table-relationships/:relationshipId", deleteTableRelationship); + +// ==================== 데이터 연결 관리 라우트 ==================== + +/** + * 데이터 연결 생성 + * POST /api/dataflow/data-links + */ +router.post("/data-links", createDataLink); + +/** + * 관계별 연결된 데이터 조회 + * GET /api/dataflow/data-links/relationship/:relationshipId + */ +router.get( + "/data-links/relationship/:relationshipId", + getLinkedDataByRelationship +); + +/** + * 데이터 연결 삭제 + * DELETE /api/dataflow/data-links/:bridgeId + */ +router.delete("/data-links/:bridgeId", deleteDataLink); + +export default router; diff --git a/backend-node/src/services/dataflowService.ts b/backend-node/src/services/dataflowService.ts new file mode 100644 index 00000000..7a884378 --- /dev/null +++ b/backend-node/src/services/dataflowService.ts @@ -0,0 +1,665 @@ +import { PrismaClient } from "@prisma/client"; +import { logger } from "../utils/logger"; + +const prisma = new PrismaClient(); + +// 테이블 관계 생성 데이터 타입 +interface CreateTableRelationshipData { + relationshipName: string; + fromTableName: string; + fromColumnName: string; + toTableName: string; + toColumnName: string; + relationshipType: string; + connectionType: string; + companyCode: string; + settings: any; + createdBy: string; +} + +// 테이블 관계 수정 데이터 타입 +interface UpdateTableRelationshipData { + relationshipName?: string; + fromTableName?: string; + fromColumnName?: string; + toTableName?: string; + toColumnName?: string; + relationshipType?: string; + connectionType?: string; + settings?: any; + updatedBy: string; +} + +export class DataflowService { + /** + * 테이블 관계 생성 + */ + async createTableRelationship(data: CreateTableRelationshipData) { + try { + logger.info("DataflowService: 테이블 관계 생성 시작", data); + + // 중복 관계 확인 + const existingRelationship = await prisma.table_relationships.findFirst({ + where: { + from_table_name: data.fromTableName, + from_column_name: data.fromColumnName, + to_table_name: data.toTableName, + to_column_name: data.toColumnName, + company_code: data.companyCode, + is_active: "Y", + }, + }); + + if (existingRelationship) { + throw new Error( + `이미 존재하는 관계입니다: ${data.fromTableName}.${data.fromColumnName} → ${data.toTableName}.${data.toColumnName}` + ); + } + + // 새 관계 생성 + 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, + }, + }); + + logger.info( + `DataflowService: 테이블 관계 생성 완료 - ID: ${relationship.relationship_id}` + ); + return relationship; + } catch (error) { + logger.error("DataflowService: 테이블 관계 생성 실패", error); + throw error; + } + } + + /** + * 회사별 테이블 관계 목록 조회 + */ + async getTableRelationships(companyCode: string) { + try { + logger.info( + `DataflowService: 테이블 관계 목록 조회 시작 - 회사코드: ${companyCode}` + ); + + // 관리자는 모든 회사의 관계를 볼 수 있음 + const whereCondition: any = { + is_active: "Y", + }; + + if (companyCode !== "*") { + whereCondition.company_code = companyCode; + } + + const relationships = await prisma.table_relationships.findMany({ + where: whereCondition, + orderBy: { + created_date: "desc", + }, + }); + + logger.info( + `DataflowService: 테이블 관계 목록 조회 완료 - ${relationships.length}개` + ); + return relationships; + } catch (error) { + logger.error("DataflowService: 테이블 관계 목록 조회 실패", error); + throw error; + } + } + + /** + * 특정 테이블 관계 조회 + */ + async getTableRelationship(relationshipId: number, companyCode: string) { + try { + logger.info( + `DataflowService: 테이블 관계 조회 시작 - ID: ${relationshipId}, 회사코드: ${companyCode}` + ); + + const whereCondition: any = { + relationship_id: relationshipId, + is_active: "Y", + }; + + // 관리자가 아닌 경우 회사코드 제한 + if (companyCode !== "*") { + whereCondition.company_code = companyCode; + } + + const relationship = await prisma.table_relationships.findFirst({ + where: whereCondition, + }); + + if (relationship) { + logger.info( + `DataflowService: 테이블 관계 조회 완료 - ID: ${relationshipId}` + ); + } else { + logger.warn( + `DataflowService: 테이블 관계를 찾을 수 없음 - ID: ${relationshipId}` + ); + } + + return relationship; + } catch (error) { + logger.error("DataflowService: 테이블 관계 조회 실패", error); + throw error; + } + } + + /** + * 테이블 관계 수정 + */ + async updateTableRelationship( + relationshipId: number, + updateData: UpdateTableRelationshipData, + companyCode: string + ) { + try { + logger.info( + `DataflowService: 테이블 관계 수정 시작 - ID: ${relationshipId}`, + updateData + ); + + // 기존 관계 확인 + const existingRelationship = await this.getTableRelationship( + relationshipId, + companyCode + ); + if (!existingRelationship) { + return null; + } + + // 관계 수정 + const relationship = await prisma.table_relationships.update({ + where: { + relationship_id: relationshipId, + }, + data: { + ...updateData, + updated_date: new Date(), + }, + }); + + logger.info( + `DataflowService: 테이블 관계 수정 완료 - ID: ${relationshipId}` + ); + return relationship; + } catch (error) { + logger.error("DataflowService: 테이블 관계 수정 실패", error); + throw error; + } + } + + /** + * 테이블 관계 삭제 (소프트 삭제) + */ + async deleteTableRelationship(relationshipId: number, companyCode: string) { + try { + logger.info( + `DataflowService: 테이블 관계 삭제 시작 - ID: ${relationshipId}, 회사코드: ${companyCode}` + ); + + // 기존 관계 확인 + const existingRelationship = await this.getTableRelationship( + relationshipId, + companyCode + ); + if (!existingRelationship) { + return false; + } + + // 소프트 삭제 (is_active = 'N') + await prisma.table_relationships.update({ + where: { + relationship_id: relationshipId, + }, + data: { + is_active: "N", + updated_date: new Date(), + }, + }); + + logger.info( + `DataflowService: 테이블 관계 삭제 완료 - ID: ${relationshipId}` + ); + return true; + } catch (error) { + logger.error("DataflowService: 테이블 관계 삭제 실패", error); + throw error; + } + } + + /** + * 특정 테이블과 관련된 모든 관계 조회 + */ + async getRelationshipsByTable(tableName: string, companyCode: string) { + try { + logger.info( + `DataflowService: 테이블별 관계 조회 시작 - 테이블: ${tableName}, 회사코드: ${companyCode}` + ); + + const whereCondition: any = { + OR: [{ from_table_name: tableName }, { to_table_name: tableName }], + is_active: "Y", + }; + + // 관리자가 아닌 경우 회사코드 제한 + if (companyCode !== "*") { + whereCondition.company_code = companyCode; + } + + const relationships = await prisma.table_relationships.findMany({ + where: whereCondition, + orderBy: { + created_date: "desc", + }, + }); + + logger.info( + `DataflowService: 테이블별 관계 조회 완료 - ${relationships.length}개` + ); + return relationships; + } catch (error) { + logger.error("DataflowService: 테이블별 관계 조회 실패", error); + throw error; + } + } + + /** + * 연결 타입별 관계 조회 + */ + async getRelationshipsByConnectionType( + connectionType: string, + companyCode: string + ) { + try { + logger.info( + `DataflowService: 연결타입별 관계 조회 시작 - 타입: ${connectionType}, 회사코드: ${companyCode}` + ); + + const whereCondition: any = { + connection_type: connectionType, + is_active: "Y", + }; + + // 관리자가 아닌 경우 회사코드 제한 + if (companyCode !== "*") { + whereCondition.company_code = companyCode; + } + + const relationships = await prisma.table_relationships.findMany({ + where: whereCondition, + orderBy: { + created_date: "desc", + }, + }); + + logger.info( + `DataflowService: 연결타입별 관계 조회 완료 - ${relationships.length}개` + ); + return relationships; + } catch (error) { + logger.error("DataflowService: 연결타입별 관계 조회 실패", error); + throw error; + } + } + + /** + * 관계 통계 조회 + */ + async getRelationshipStats(companyCode: string) { + try { + logger.info( + `DataflowService: 관계 통계 조회 시작 - 회사코드: ${companyCode}` + ); + + const whereCondition: any = { + is_active: "Y", + }; + + // 관리자가 아닌 경우 회사코드 제한 + if (companyCode !== "*") { + whereCondition.company_code = companyCode; + } + + // 전체 관계 수 + const totalCount = await prisma.table_relationships.count({ + where: whereCondition, + }); + + // 관계 타입별 통계 + const relationshipTypeStats = await prisma.table_relationships.groupBy({ + by: ["relationship_type"], + where: whereCondition, + _count: { + relationship_id: true, + }, + }); + + // 연결 타입별 통계 + const connectionTypeStats = await prisma.table_relationships.groupBy({ + by: ["connection_type"], + where: whereCondition, + _count: { + relationship_id: true, + }, + }); + + const stats = { + totalCount, + relationshipTypeStats: relationshipTypeStats.map((stat) => ({ + type: stat.relationship_type, + count: stat._count.relationship_id, + })), + connectionTypeStats: connectionTypeStats.map((stat) => ({ + type: stat.connection_type, + count: stat._count.relationship_id, + })), + }; + + logger.info(`DataflowService: 관계 통계 조회 완료`, stats); + return stats; + } catch (error) { + logger.error("DataflowService: 관계 통계 조회 실패", error); + throw error; + } + } + + // ==================== 데이터 중계 관리 ==================== + + /** + * 데이터 관계 연결 생성 + */ + async createDataLink(linkData: { + relationshipId: number; + fromTableName: string; + fromColumnName: string; + fromKeyValue: string; + fromRecordId?: string; + toTableName: string; + toColumnName: string; + toKeyValue: string; + toRecordId?: string; + connectionType: string; + companyCode: string; + bridgeData?: any; + createdBy: string; + }) { + try { + logger.info( + `DataflowService: 데이터 연결 생성 시작 - 관계ID: ${linkData.relationshipId}` + ); + + const bridge = await prisma.data_relationship_bridge.create({ + data: { + 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 || {}, + created_by: linkData.createdBy, + }, + }); + + logger.info( + `DataflowService: 데이터 연결 생성 완료 - Bridge ID: ${bridge.bridge_id}` + ); + return bridge; + } catch (error) { + logger.error("DataflowService: 데이터 연결 생성 실패", error); + throw error; + } + } + + /** + * 관계별 연결된 데이터 조회 + */ + async getLinkedDataByRelationship( + relationshipId: number, + companyCode: string + ) { + try { + logger.info( + `DataflowService: 관계별 연결 데이터 조회 시작 - 관계ID: ${relationshipId}` + ); + + const whereCondition: any = { + relationship_id: relationshipId, + is_active: "Y", + }; + + // 관리자가 아닌 경우 회사코드 제한 + if (companyCode !== "*") { + whereCondition.company_code = companyCode; + } + + const linkedData = await prisma.data_relationship_bridge.findMany({ + where: whereCondition, + orderBy: { created_at: "desc" }, + include: { + relationship: { + select: { + relationship_name: true, + relationship_type: true, + connection_type: true, + }, + }, + }, + }); + + logger.info( + `DataflowService: 관계별 연결 데이터 조회 완료 - ${linkedData.length}건` + ); + return linkedData; + } catch (error) { + logger.error("DataflowService: 관계별 연결 데이터 조회 실패", error); + throw error; + } + } + + /** + * 특정 테이블의 연결된 데이터 조회 + */ + async getLinkedDataByTable( + tableName: string, + keyValue?: string, + companyCode?: string + ) { + try { + logger.info( + `DataflowService: 테이블별 연결 데이터 조회 시작 - 테이블: ${tableName}` + ); + + const whereCondition: any = { + OR: [{ from_table_name: tableName }, { to_table_name: tableName }], + is_active: "Y", + }; + + // 특정 키 값으로 필터링 + if (keyValue) { + whereCondition.OR = [ + { from_table_name: tableName, from_key_value: keyValue }, + { to_table_name: tableName, to_key_value: keyValue }, + ]; + } + + // 회사코드 필터링 + if (companyCode && companyCode !== "*") { + whereCondition.company_code = companyCode; + } + + const linkedData = await prisma.data_relationship_bridge.findMany({ + where: whereCondition, + orderBy: { created_at: "desc" }, + include: { + relationship: { + select: { + relationship_name: true, + relationship_type: true, + connection_type: true, + }, + }, + }, + }); + + logger.info( + `DataflowService: 테이블별 연결 데이터 조회 완료 - ${linkedData.length}건` + ); + return linkedData; + } catch (error) { + logger.error("DataflowService: 테이블별 연결 데이터 조회 실패", error); + throw error; + } + } + + /** + * 데이터 연결 수정 + */ + async updateDataLink( + bridgeId: number, + updateData: { + fromKeyValue?: string; + fromRecordId?: string; + toKeyValue?: string; + toRecordId?: string; + bridgeData?: any; + updatedBy: string; + }, + companyCode: string + ) { + try { + logger.info( + `DataflowService: 데이터 연결 수정 시작 - Bridge ID: ${bridgeId}` + ); + + const whereCondition: any = { + bridge_id: bridgeId, + is_active: "Y", + }; + + // 관리자가 아닌 경우 회사코드 제한 + if (companyCode !== "*") { + whereCondition.company_code = companyCode; + } + + const updatedBridge = await prisma.data_relationship_bridge.update({ + where: whereCondition, + data: { + ...updateData, + updated_at: new Date(), + }, + }); + + logger.info( + `DataflowService: 데이터 연결 수정 완료 - Bridge ID: ${bridgeId}` + ); + return updatedBridge; + } catch (error) { + logger.error("DataflowService: 데이터 연결 수정 실패", error); + throw error; + } + } + + /** + * 데이터 연결 삭제 (소프트 삭제) + */ + async deleteDataLink( + bridgeId: number, + companyCode: string, + deletedBy: string + ) { + try { + logger.info( + `DataflowService: 데이터 연결 삭제 시작 - Bridge ID: ${bridgeId}` + ); + + const whereCondition: any = { + bridge_id: bridgeId, + is_active: "Y", + }; + + // 관리자가 아닌 경우 회사코드 제한 + if (companyCode !== "*") { + whereCondition.company_code = companyCode; + } + + await prisma.data_relationship_bridge.update({ + where: whereCondition, + data: { + is_active: "N", + updated_at: new Date(), + updated_by: deletedBy, + }, + }); + + logger.info( + `DataflowService: 데이터 연결 삭제 완료 - Bridge ID: ${bridgeId}` + ); + return true; + } catch (error) { + logger.error("DataflowService: 데이터 연결 삭제 실패", error); + throw error; + } + } + + /** + * 관계 삭제 시 연결된 모든 데이터도 삭제 + */ + async deleteAllLinkedDataByRelationship( + relationshipId: number, + companyCode: string, + deletedBy: string + ) { + try { + logger.info( + `DataflowService: 관계별 모든 데이터 연결 삭제 시작 - 관계ID: ${relationshipId}` + ); + + const whereCondition: any = { + relationship_id: relationshipId, + is_active: "Y", + }; + + // 관리자가 아닌 경우 회사코드 제한 + if (companyCode !== "*") { + whereCondition.company_code = companyCode; + } + + const result = await prisma.data_relationship_bridge.updateMany({ + where: whereCondition, + data: { + is_active: "N", + updated_at: new Date(), + updated_by: deletedBy, + }, + }); + + logger.info( + `DataflowService: 관계별 모든 데이터 연결 삭제 완료 - ${result.count}건` + ); + return result.count; + } catch (error) { + logger.error("DataflowService: 관계별 모든 데이터 연결 삭제 실패", error); + throw error; + } + } +} diff --git a/frontend/components/dataflow/ConnectionSetupModal.tsx b/frontend/components/dataflow/ConnectionSetupModal.tsx index 2d412309..5c3973cb 100644 --- a/frontend/components/dataflow/ConnectionSetupModal.tsx +++ b/frontend/components/dataflow/ConnectionSetupModal.tsx @@ -6,10 +6,11 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Textarea } from "@/components/ui/textarea"; -import { ArrowRight, Database, Link } from "lucide-react"; +import { ArrowRight, Link, Key, Save, Globe, Plus } from "lucide-react"; +import { DataFlowAPI, TableRelationship } from "@/lib/api/dataflow"; +import toast from "react-hot-toast"; // 연결 정보 타입 interface ConnectionInfo { @@ -40,20 +41,44 @@ interface ConnectionConfig { connectionType: "simple-key" | "data-save" | "external-call"; fromColumnName: string; toColumnName: string; - settings?: Record; + settings?: Record; description?: string; } +// 단순 키값 연결 설정 +interface SimpleKeySettings { + syncDirection: "unidirectional" | "bidirectional"; + notes: string; +} + +// 데이터 저장 설정 +interface DataSaveSettings { + sourceField: string; + targetField: string; + saveConditions: string; +} + +// 외부 호출 설정 +interface ExternalCallSettings { + callType: "rest-api" | "email" | "webhook" | "ftp" | "queue"; + apiUrl?: string; + httpMethod?: "GET" | "POST" | "PUT" | "DELETE"; + headers?: string; + bodyTemplate?: string; +} + interface ConnectionSetupModalProps { isOpen: boolean; connection: ConnectionInfo | null; - onConfirm: (config: ConnectionConfig) => void; + companyCode: string; + onConfirm: (relationship: TableRelationship) => void; onCancel: () => void; } export const ConnectionSetupModal: React.FC = ({ isOpen, connection, + companyCode, onConfirm, onCancel, }) => { @@ -64,6 +89,27 @@ export const ConnectionSetupModal: React.FC = ({ fromColumnName: "", toColumnName: "", description: "", + settings: {}, + }); + + // 연결 종류별 설정 상태 + const [simpleKeySettings, setSimpleKeySettings] = useState({ + syncDirection: "bidirectional", + notes: "", + }); + + const [dataSaveSettings, setDataSaveSettings] = useState({ + sourceField: "", + targetField: "", + saveConditions: "", + }); + + const [externalCallSettings, setExternalCallSettings] = useState({ + callType: "rest-api", + apiUrl: "", + httpMethod: "POST", + headers: "{}", + bodyTemplate: "{}", }); // 모달이 열릴 때 기본값 설정 @@ -79,14 +125,82 @@ export const ConnectionSetupModal: React.FC = ({ fromColumnName: "", toColumnName: "", description: `${fromTableName}과 ${toTableName} 간의 데이터 관계`, + settings: {}, + }); + + // 단순 키값 연결 기본값 설정 + setSimpleKeySettings({ + syncDirection: "bidirectional", + notes: `${fromTableName}과 ${toTableName} 간의 키값 연결`, + }); + + // 데이터 저장 기본값 설정 + setDataSaveSettings({ + sourceField: "", + targetField: "", + saveConditions: "데이터 저장 조건을 입력하세요", + }); + + // 외부 호출 기본값 설정 + setExternalCallSettings({ + callType: "rest-api", + apiUrl: "https://api.example.com/webhook", + httpMethod: "POST", + headers: "{}", + bodyTemplate: "{}", }); } }, [isOpen, connection]); - const handleConfirm = () => { - if (config.relationshipName && config.fromColumnName && config.toColumnName) { - onConfirm(config); + const handleConfirm = async () => { + if (!config.relationshipName || !connection) { + toast.error("필수 정보를 모두 입력해주세요."); + return; + } + + try { + // 연결 종류별 설정을 준비 + let settings = {}; + + switch (config.connectionType) { + case "simple-key": + settings = simpleKeySettings; + break; + case "data-save": + settings = dataSaveSettings; + break; + case "external-call": + settings = externalCallSettings; + break; + } + + // API 호출을 위한 관계 데이터 준비 + const relationshipData: Omit = { + relationshipName: config.relationshipName, + fromTableName: connection.fromNode.tableName, + fromColumnName: connection.fromColumn || "", + toTableName: connection.toNode.tableName, + toColumnName: connection.toColumn || "", + relationshipType: config.relationshipType, + connectionType: config.connectionType, + companyCode: companyCode, + settings: settings, + isActive: "Y", + }; + + toast.loading("관계를 생성하고 있습니다...", { id: "create-relationship" }); + + // API 호출 + const createdRelationship = await DataFlowAPI.createRelationship(relationshipData); + + toast.success("관계가 성공적으로 생성되었습니다!", { id: "create-relationship" }); + + // 성공 콜백 호출 + onConfirm(createdRelationship); handleCancel(); // 모달 닫기 + } catch (error) { + console.error("관계 생성 오류:", error); + toast.error("관계 생성에 실패했습니다. 다시 시도해주세요.", { id: "create-relationship" }); } }; @@ -113,163 +227,344 @@ export const ConnectionSetupModal: React.FC = ({ const fromTableData = selectedColumnsData[fromTable]; const toTableData = selectedColumnsData[toTable]; + // 연결 종류별 설정 패널 렌더링 + const renderConnectionTypeSettings = () => { + switch (config.connectionType) { + case "simple-key": + return ( +
+
+ + 단순 키값 연결 설정 +
+
+
+ + +
+
+ +