From 351e57dd3104f6d46dd5a1004cb326af9c82a043 Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Wed, 18 Mar 2026 13:56:03 +0900 Subject: [PATCH] [agent-pipeline] pipe-20260318044621-56k5 round-2 --- .../controllers/batchManagementController.ts | 37 ++++++++++++ .../src/routes/batchManagementRoutes.ts | 6 ++ .../src/services/batchSchedulerService.ts | 60 ++++++++++++++++++- backend-node/src/services/batchService.ts | 25 +++++++- backend-node/src/types/batchTypes.ts | 12 ++++ 5 files changed, 136 insertions(+), 4 deletions(-) diff --git a/backend-node/src/controllers/batchManagementController.ts b/backend-node/src/controllers/batchManagementController.ts index bdd9e869..4d406209 100644 --- a/backend-node/src/controllers/batchManagementController.ts +++ b/backend-node/src/controllers/batchManagementController.ts @@ -768,4 +768,41 @@ export class BatchManagementController { }); } } + + /** + * 노드 플로우 목록 조회 (배치 설정에서 노드 플로우 선택용) + * GET /api/batch-management/node-flows + * 멀티테넌시: 최고 관리자는 전체, 일반 회사는 자기 회사 플로우만 + */ + static async getNodeFlows(req: AuthenticatedRequest, res: Response) { + try { + const companyCode = req.user?.companyCode; + + let queryText: string; + let queryParams: any[] = []; + + if (companyCode === "*") { + queryText = `SELECT flow_id, flow_name, description, created_date + FROM node_flows + ORDER BY flow_name`; + } else { + queryText = `SELECT flow_id, flow_name, description, created_date + FROM node_flows + WHERE company_code = $1 + ORDER BY flow_name`; + queryParams = [companyCode]; + } + + const result = await query(queryText, queryParams); + + return res.json({ success: true, data: result }); + } catch (error) { + console.error("노드 플로우 목록 조회 오류:", error); + return res.status(500).json({ + success: false, + message: "노드 플로우 목록 조회 실패", + error: error instanceof Error ? error.message : "알 수 없는 오류", + }); + } + } } diff --git a/backend-node/src/routes/batchManagementRoutes.ts b/backend-node/src/routes/batchManagementRoutes.ts index 50ee1ea0..d08664de 100644 --- a/backend-node/src/routes/batchManagementRoutes.ts +++ b/backend-node/src/routes/batchManagementRoutes.ts @@ -85,4 +85,10 @@ router.post("/rest-api/save", authenticateToken, BatchManagementController.saveR */ router.get("/auth-services", authenticateToken, BatchManagementController.getAuthServiceNames); +/** + * GET /api/batch-management/node-flows + * 노드 플로우 목록 조회 (배치 설정에서 노드 플로우 선택용) + */ +router.get("/node-flows", authenticateToken, BatchManagementController.getNodeFlows); + export default router; diff --git a/backend-node/src/services/batchSchedulerService.ts b/backend-node/src/services/batchSchedulerService.ts index f6fe56a1..b3aef16d 100644 --- a/backend-node/src/services/batchSchedulerService.ts +++ b/backend-node/src/services/batchSchedulerService.ts @@ -165,8 +165,20 @@ export class BatchSchedulerService { executionLog = executionLogResponse.data; - // 실제 배치 실행 로직 (수동 실행과 동일한 로직 사용) - const result = await this.executeBatchMappings(config); + // 실행 유형에 따라 분기: node_flow면 노드 플로우 실행, 아니면 매핑 배치 실행 + let result: { + totalRecords: number; + successRecords: number; + failedRecords: number; + }; + if ( + config.execution_type === "node_flow" && + config.node_flow_id != null + ) { + result = await this.executeNodeFlow(config); + } else { + result = await this.executeBatchMappings(config); + } // 실행 로그 업데이트 (성공) await BatchExecutionLogService.updateExecutionLog(executionLog.id, { @@ -207,6 +219,50 @@ export class BatchSchedulerService { } } + /** + * 노드 플로우 실행 (execution_type === 'node_flow'일 때) + * node_flows 테이블의 플로우를 NodeFlowExecutionService로 실행하고 결과를 배치 로그 형식으로 반환 + */ + private static async executeNodeFlow(config: any): Promise<{ + totalRecords: number; + successRecords: number; + failedRecords: number; + }> { + const { NodeFlowExecutionService } = await import( + "./nodeFlowExecutionService" + ); + + // 플로우 존재 여부 확인 + const flowCheck = await query<{ flow_id: number; flow_name: string }>( + "SELECT flow_id, flow_name FROM node_flows WHERE flow_id = $1", + [config.node_flow_id] + ); + if (flowCheck.length === 0) { + throw new Error( + `노드 플로우를 찾을 수 없습니다 (flow_id: ${config.node_flow_id})` + ); + } + + const contextData: Record = { + ...(config.node_flow_context || {}), + _batchId: config.id, + _batchName: config.batch_name, + _companyCode: config.company_code, + _executedBy: "batch_system", + }; + + const flowResult = await NodeFlowExecutionService.executeFlow( + config.node_flow_id, + contextData + ); + + return { + totalRecords: flowResult.summary.total, + successRecords: flowResult.summary.success, + failedRecords: flowResult.summary.failed, + }; + } + /** * 배치 매핑 실행 (수동 실행과 동일한 로직) */ diff --git a/backend-node/src/services/batchService.ts b/backend-node/src/services/batchService.ts index 31ee2001..5c4d7def 100644 --- a/backend-node/src/services/batchService.ts +++ b/backend-node/src/services/batchService.ts @@ -176,8 +176,8 @@ export class BatchService { // 배치 설정 생성 const batchConfigResult = await client.query( `INSERT INTO batch_configs - (batch_name, description, cron_schedule, is_active, company_code, save_mode, conflict_key, auth_service_name, data_array_path, created_by, created_date, updated_date) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, NOW(), NOW()) + (batch_name, description, cron_schedule, is_active, company_code, save_mode, conflict_key, auth_service_name, data_array_path, execution_type, node_flow_id, node_flow_context, created_by, created_date, updated_date) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, NOW(), NOW()) RETURNING *`, [ data.batchName, @@ -189,6 +189,11 @@ export class BatchService { data.conflictKey || null, data.authServiceName || null, data.dataArrayPath || null, + data.executionType || "mapping", + data.nodeFlowId ?? null, + data.nodeFlowContext != null + ? JSON.stringify(data.nodeFlowContext) + : null, userId, ] ); @@ -332,6 +337,22 @@ export class BatchService { updateFields.push(`data_array_path = $${paramIndex++}`); updateValues.push(data.dataArrayPath || null); } + if (data.executionType !== undefined) { + updateFields.push(`execution_type = $${paramIndex++}`); + updateValues.push(data.executionType); + } + if (data.nodeFlowId !== undefined) { + updateFields.push(`node_flow_id = $${paramIndex++}`); + updateValues.push(data.nodeFlowId ?? null); + } + if (data.nodeFlowContext !== undefined) { + updateFields.push(`node_flow_context = $${paramIndex++}`); + updateValues.push( + data.nodeFlowContext != null + ? JSON.stringify(data.nodeFlowContext) + : null + ); + } // 배치 설정 업데이트 const batchConfigResult = await client.query( diff --git a/backend-node/src/types/batchTypes.ts b/backend-node/src/types/batchTypes.ts index a6404036..efaca16f 100644 --- a/backend-node/src/types/batchTypes.ts +++ b/backend-node/src/types/batchTypes.ts @@ -91,6 +91,12 @@ export interface BatchConfig { conflict_key?: string; // UPSERT 시 충돌 기준 컬럼명 auth_service_name?: string; // REST API 인증에 사용할 토큰 서비스명 data_array_path?: string; // REST API 응답에서 데이터 배열 경로 (예: response, data.items) + /** 실행 유형: mapping(테이블 매핑) | node_flow(노드 플로우) */ + execution_type?: "mapping" | "node_flow"; + /** 노드 플로우 실행 시 사용할 flow_id (node_flows.flow_id) */ + node_flow_id?: number; + /** 노드 플로우 실행 시 전달할 컨텍스트 (Record) */ + node_flow_context?: Record; created_by?: string; created_date?: Date; updated_by?: string; @@ -150,6 +156,9 @@ export interface CreateBatchConfigRequest { conflictKey?: string; authServiceName?: string; dataArrayPath?: string; // REST API 응답에서 데이터 배열 경로 + executionType?: "mapping" | "node_flow"; + nodeFlowId?: number; + nodeFlowContext?: Record; mappings: BatchMappingRequest[]; } @@ -162,6 +171,9 @@ export interface UpdateBatchConfigRequest { conflictKey?: string; authServiceName?: string; dataArrayPath?: string; // REST API 응답에서 데이터 배열 경로 + executionType?: "mapping" | "node_flow"; + nodeFlowId?: number; + nodeFlowContext?: Record; mappings?: BatchMappingRequest[]; }