// 배치관리 전용 컨트롤러 (기존 소스와 완전 분리) // 작성일: 2024-12-24 import { Response } from "express"; import { AuthenticatedRequest } from "../types/auth"; import { BatchManagementService, BatchConnectionInfo, BatchTableInfo, BatchColumnInfo } from "../services/batchManagementService"; import { BatchService } from "../services/batchService"; import { BatchSchedulerService } from "../services/batchSchedulerService"; import { BatchExternalDbService } from "../services/batchExternalDbService"; import { CreateBatchConfigRequest, BatchConfig } from "../types/batchTypes"; export class BatchManagementController { /** * 사용 가능한 커넥션 목록 조회 */ static async getAvailableConnections(req: AuthenticatedRequest, res: Response) { try { const result = await BatchManagementService.getAvailableConnections(); if (result.success) { res.json(result); } else { res.status(500).json(result); } } catch (error) { console.error("커넥션 목록 조회 오류:", error); res.status(500).json({ success: false, message: "커넥션 목록 조회 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "알 수 없는 오류" }); } } /** * 특정 커넥션의 테이블 목록 조회 */ static async getTablesFromConnection(req: AuthenticatedRequest, res: Response) { try { const { type, id } = req.params; if (type !== 'internal' && type !== 'external') { return res.status(400).json({ success: false, message: "올바른 연결 타입을 지정해주세요. (internal 또는 external)" }); } const connectionId = type === 'external' ? Number(id) : undefined; const result = await BatchManagementService.getTablesFromConnection(type, connectionId); if (result.success) { return res.json(result); } else { return res.status(500).json(result); } } catch (error) { console.error("테이블 목록 조회 오류:", error); return res.status(500).json({ success: false, message: "테이블 목록 조회 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "알 수 없는 오류" }); } } /** * 특정 테이블의 컬럼 정보 조회 */ static async getTableColumns(req: AuthenticatedRequest, res: Response) { try { const { type, id, tableName } = req.params; if (type !== 'internal' && type !== 'external') { return res.status(400).json({ success: false, message: "올바른 연결 타입을 지정해주세요. (internal 또는 external)" }); } const connectionId = type === 'external' ? Number(id) : undefined; const result = await BatchManagementService.getTableColumns(type, connectionId, tableName); if (result.success) { return res.json(result); } else { return res.status(500).json(result); } } catch (error) { console.error("컬럼 정보 조회 오류:", error); return res.status(500).json({ success: false, message: "컬럼 정보 조회 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "알 수 없는 오류" }); } } /** * 배치 설정 생성 * POST /api/batch-management/batch-configs */ static async createBatchConfig(req: AuthenticatedRequest, res: Response) { try { const { batchName, description, cronSchedule, mappings, isActive } = req.body; if (!batchName || !cronSchedule || !mappings || !Array.isArray(mappings)) { return res.status(400).json({ success: false, message: "필수 필드가 누락되었습니다. (batchName, cronSchedule, mappings)" }); } const batchConfig = await BatchService.createBatchConfig({ batchName, description, cronSchedule, mappings, isActive: isActive !== undefined ? isActive : true } as CreateBatchConfigRequest); return res.status(201).json({ success: true, data: batchConfig, message: "배치 설정이 성공적으로 생성되었습니다." }); } catch (error) { console.error("배치 설정 생성 오류:", error); return res.status(500).json({ success: false, message: "배치 설정 생성에 실패했습니다.", error: error instanceof Error ? error.message : "알 수 없는 오류" }); } } /** * 특정 배치 설정 조회 * GET /api/batch-management/batch-configs/:id */ static async getBatchConfigById(req: AuthenticatedRequest, res: Response) { try { const { id } = req.params; console.log("🔍 배치 설정 조회 요청:", id); const result = await BatchService.getBatchConfigById(Number(id)); if (!result.success) { return res.status(404).json({ success: false, message: result.message || "배치 설정을 찾을 수 없습니다." }); } console.log("📋 조회된 배치 설정:", result.data); return res.json({ success: true, data: result.data }); } catch (error) { console.error("❌ 배치 설정 조회 오류:", error); return res.status(500).json({ success: false, message: "배치 설정 조회에 실패했습니다.", error: error instanceof Error ? error.message : "알 수 없는 오류" }); } } /** * 배치 설정 목록 조회 * GET /api/batch-management/batch-configs */ static async getBatchConfigs(req: AuthenticatedRequest, res: Response) { try { const { page = 1, limit = 10, search, isActive } = req.query; const filter = { page: Number(page), limit: Number(limit), search: search as string, is_active: isActive as string }; const result = await BatchService.getBatchConfigs(filter); res.json({ success: true, data: result.data, pagination: result.pagination }); } catch (error) { console.error("배치 설정 목록 조회 오류:", error); res.status(500).json({ success: false, message: "배치 설정 목록 조회에 실패했습니다.", error: error instanceof Error ? error.message : "알 수 없는 오류" }); } } /** * 배치 수동 실행 * POST /api/batch-management/batch-configs/:id/execute */ static async executeBatchConfig(req: AuthenticatedRequest, res: Response) { try { const { id } = req.params; if (!id || isNaN(Number(id))) { return res.status(400).json({ success: false, message: "올바른 배치 설정 ID를 제공해주세요." }); } // 배치 설정 조회 const batchConfigResult = await BatchService.getBatchConfigById(Number(id)); if (!batchConfigResult.success || !batchConfigResult.data) { return res.status(404).json({ success: false, message: "배치 설정을 찾을 수 없습니다." }); } const batchConfig = batchConfigResult.data as BatchConfig; // 배치 실행 로직 (간단한 버전) const startTime = new Date(); let totalRecords = 0; let successRecords = 0; let failedRecords = 0; try { console.log(`배치 실행 시작: ${batchConfig.batch_name} (ID: ${id})`); // 실행 로그 생성 const executionLog = await BatchService.createExecutionLog({ batch_config_id: Number(id), execution_status: 'RUNNING', start_time: startTime, total_records: 0, success_records: 0, failed_records: 0 }); // 실제 배치 실행 (매핑이 있는 경우) if (batchConfig.batch_mappings && batchConfig.batch_mappings.length > 0) { // 테이블별로 매핑을 그룹화 const tableGroups = new Map(); for (const mapping of batchConfig.batch_mappings) { const key = `${mapping.from_connection_type}:${mapping.from_connection_id || 'internal'}:${mapping.from_table_name}`; if (!tableGroups.has(key)) { tableGroups.set(key, []); } tableGroups.get(key)!.push(mapping); } // 각 테이블 그룹별로 처리 for (const [tableKey, mappings] of tableGroups) { try { const firstMapping = mappings[0]; console.log(`테이블 처리 시작: ${tableKey} -> ${mappings.length}개 컬럼 매핑`); let fromData: any[] = []; // FROM 데이터 조회 (DB 또는 REST API) if (firstMapping.from_connection_type === 'restapi') { // REST API에서 데이터 조회 console.log(`REST API에서 데이터 조회: ${firstMapping.from_api_url}${firstMapping.from_table_name}`); console.log(`API 설정:`, { url: firstMapping.from_api_url, key: firstMapping.from_api_key ? '***' : 'null', method: firstMapping.from_api_method, endpoint: firstMapping.from_table_name }); try { const apiResult = await BatchExternalDbService.getDataFromRestApi( firstMapping.from_api_url!, firstMapping.from_api_key!, firstMapping.from_table_name, firstMapping.from_api_method as 'GET' | 'POST' | 'PUT' | 'DELETE' || 'GET', mappings.map(m => m.from_column_name) ); console.log(`API 조회 결과:`, { success: apiResult.success, dataCount: apiResult.data ? apiResult.data.length : 0, message: apiResult.message }); if (apiResult.success && apiResult.data) { fromData = apiResult.data; } else { throw new Error(`REST API 데이터 조회 실패: ${apiResult.message}`); } } catch (error) { console.error(`REST API 조회 오류:`, error); throw error; } } else { // DB에서 데이터 조회 const fromColumns = mappings.map(m => m.from_column_name); fromData = await BatchService.getDataFromTableWithColumns( firstMapping.from_table_name, fromColumns, firstMapping.from_connection_type as 'internal' | 'external', firstMapping.from_connection_id || undefined ); } totalRecords += fromData.length; // 컬럼 매핑 적용하여 TO 테이블 형식으로 변환 const mappedData = fromData.map(row => { const mappedRow: any = {}; for (const mapping of mappings) { // DB → REST API 배치인지 확인 if (firstMapping.to_connection_type === 'restapi' && mapping.to_api_body) { // DB → REST API: 원본 컬럼명을 키로 사용 (템플릿 처리용) mappedRow[mapping.from_column_name] = row[mapping.from_column_name]; } else { // 기존 로직: to_column_name을 키로 사용 mappedRow[mapping.to_column_name] = row[mapping.from_column_name]; } } return mappedRow; }); // TO 테이블에 데이터 삽입 (DB 또는 REST API) let insertResult: { successCount: number; failedCount: number }; if (firstMapping.to_connection_type === 'restapi') { // REST API로 데이터 전송 console.log(`REST API로 데이터 전송: ${firstMapping.to_api_url}${firstMapping.to_table_name}`); // DB → REST API 배치인지 확인 (to_api_body가 있으면 템플릿 기반) const hasTemplate = mappings.some(m => m.to_api_body); if (hasTemplate) { // 템플릿 기반 REST API 전송 (DB → REST API 배치) const templateBody = firstMapping.to_api_body || '{}'; console.log(`템플릿 기반 REST API 전송, Request Body 템플릿: ${templateBody}`); // URL 경로 컬럼 찾기 (PUT/DELETE용) const urlPathColumn = mappings.find(m => m.to_column_name === 'URL_PATH_PARAM')?.from_column_name; const apiResult = await BatchExternalDbService.sendDataToRestApiWithTemplate( firstMapping.to_api_url!, firstMapping.to_api_key!, firstMapping.to_table_name, firstMapping.to_api_method as 'POST' | 'PUT' | 'DELETE' || 'POST', templateBody, mappedData, urlPathColumn ); if (apiResult.success && apiResult.data) { insertResult = apiResult.data; } else { throw new Error(`템플릿 기반 REST API 데이터 전송 실패: ${apiResult.message}`); } } else { // 기존 REST API 전송 (REST API → DB 배치) const apiResult = await BatchExternalDbService.sendDataToRestApi( firstMapping.to_api_url!, firstMapping.to_api_key!, firstMapping.to_table_name, firstMapping.to_api_method as 'POST' | 'PUT' || 'POST', mappedData ); if (apiResult.success && apiResult.data) { insertResult = apiResult.data; } else { throw new Error(`REST API 데이터 전송 실패: ${apiResult.message}`); } } } else { // DB에 데이터 삽입 insertResult = await BatchService.insertDataToTable( firstMapping.to_table_name, mappedData, firstMapping.to_connection_type as 'internal' | 'external', firstMapping.to_connection_id || undefined ); } successRecords += insertResult.successCount; failedRecords += insertResult.failedCount; console.log(`테이블 처리 완료: ${insertResult.successCount}개 성공, ${insertResult.failedCount}개 실패`); } catch (error) { console.error(`테이블 처리 실패: ${tableKey}`, error); failedRecords += 1; } } } else { console.log("매핑이 없어서 데이터 처리를 건너뜁니다."); } // 실행 로그 업데이트 (성공) await BatchService.updateExecutionLog(executionLog.id, { execution_status: 'SUCCESS', end_time: new Date(), duration_ms: Date.now() - startTime.getTime(), total_records: totalRecords, success_records: successRecords, failed_records: failedRecords }); return res.json({ success: true, message: "배치가 성공적으로 실행되었습니다.", data: { batchId: id, totalRecords, successRecords, failedRecords, duration: Date.now() - startTime.getTime() } }); } catch (error) { console.error(`배치 실행 실패: ${batchConfig.batch_name}`, error); return res.status(500).json({ success: false, message: "배치 실행에 실패했습니다.", error: error instanceof Error ? error.message : "알 수 없는 오류" }); } } catch (error) { console.error("배치 실행 오류:", error); return res.status(500).json({ success: false, message: "배치 실행 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "알 수 없는 오류" }); } } /** * 배치 설정 업데이트 * PUT /api/batch-management/batch-configs/:id */ static async updateBatchConfig(req: AuthenticatedRequest, res: Response) { try { const { id } = req.params; const updateData = req.body; if (!id || isNaN(Number(id))) { return res.status(400).json({ success: false, message: "올바른 배치 설정 ID를 제공해주세요." }); } const batchConfig = await BatchService.updateBatchConfig(Number(id), updateData); // 스케줄러에서 배치 스케줄 업데이트 await BatchSchedulerService.updateBatchSchedule(Number(id)); return res.json({ success: true, data: batchConfig, message: "배치 설정이 성공적으로 업데이트되었습니다." }); } catch (error) { console.error("배치 설정 업데이트 오류:", error); return res.status(500).json({ success: false, message: "배치 설정 업데이트에 실패했습니다.", error: error instanceof Error ? error.message : "알 수 없는 오류" }); } } /** * REST API 데이터 미리보기 */ static async previewRestApiData(req: AuthenticatedRequest, res: Response) { try { const { apiUrl, apiKey, endpoint, method = 'GET' } = req.body; if (!apiUrl || !apiKey || !endpoint) { return res.status(400).json({ success: false, message: "API URL, API Key, 엔드포인트는 필수입니다." }); } // RestApiConnector 사용하여 데이터 조회 const { RestApiConnector } = await import('../database/RestApiConnector'); const connector = new RestApiConnector({ baseUrl: apiUrl, apiKey: apiKey, timeout: 30000 }); // 연결 테스트 await connector.connect(); // 데이터 조회 (최대 5개만) - GET 메서드만 지원 const result = await connector.executeQuery(endpoint, method); console.log(`[previewRestApiData] executeQuery 결과:`, { rowCount: result.rowCount, rowsLength: result.rows ? result.rows.length : 'undefined', firstRow: result.rows && result.rows.length > 0 ? result.rows[0] : 'no data' }); const data = result.rows.slice(0, 5); // 최대 5개 샘플만 console.log(`[previewRestApiData] 슬라이스된 데이터:`, data); if (data.length > 0) { // 첫 번째 객체에서 필드명 추출 const fields = Object.keys(data[0]); console.log(`[previewRestApiData] 추출된 필드:`, fields); return res.json({ success: true, data: { fields: fields, samples: data, totalCount: result.rowCount || data.length }, message: `${fields.length}개 필드, ${result.rowCount || data.length}개 레코드를 조회했습니다.` }); } else { return res.json({ success: true, data: { fields: [], samples: [], totalCount: 0 }, message: "API에서 데이터를 가져올 수 없습니다." }); } } catch (error) { console.error("REST API 미리보기 오류:", error); return res.status(500).json({ success: false, message: "REST API 데이터 미리보기 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "알 수 없는 오류" }); } } /** * REST API 배치 설정 저장 */ static async saveRestApiBatch(req: AuthenticatedRequest, res: Response) { try { const { batchName, batchType, cronSchedule, description, apiMappings } = req.body; if (!batchName || !batchType || !cronSchedule || !apiMappings || apiMappings.length === 0) { return res.status(400).json({ success: false, message: "필수 필드가 누락되었습니다." }); } console.log("REST API 배치 저장 요청:", { batchName, batchType, cronSchedule, description, apiMappings }); // BatchService를 사용하여 배치 설정 저장 const batchConfig: CreateBatchConfigRequest = { batchName: batchName, description: description || '', cronSchedule: cronSchedule, mappings: apiMappings }; const result = await BatchService.createBatchConfig(batchConfig); if (result.success && result.data) { // 스케줄러에 자동 등록 ✅ try { await BatchSchedulerService.scheduleBatchConfig(result.data); console.log(`✅ 새로운 배치가 스케줄러에 등록되었습니다: ${batchName} (ID: ${result.data.id})`); } catch (schedulerError) { console.error(`❌ 스케줄러 등록 실패: ${batchName}`, schedulerError); // 스케줄러 등록 실패해도 배치 저장은 성공으로 처리 } return res.json({ success: true, message: "REST API 배치가 성공적으로 저장되었습니다.", data: result.data }); } else { return res.status(500).json({ success: false, message: result.message || "배치 저장에 실패했습니다." }); } } catch (error) { console.error("REST API 배치 저장 오류:", error); return res.status(500).json({ success: false, message: "배치 저장 중 오류가 발생했습니다." }); } } }