// 배치 실행 로그 서비스 // 작성일: 2024-12-24 import { query, queryOne } from "../database/db"; import { BatchExecutionLog, CreateBatchExecutionLogRequest, UpdateBatchExecutionLogRequest, BatchExecutionLogFilter, BatchExecutionLogWithConfig, } from "../types/batchExecutionLogTypes"; import { ApiResponse } from "../types/batchTypes"; export class BatchExecutionLogService { /** * 배치 실행 로그 목록 조회 */ static async getExecutionLogs( filter: BatchExecutionLogFilter = {} ): Promise> { try { const { batch_config_id, execution_status, start_date, end_date, page = 1, limit = 50, } = filter; const skip = (page - 1) * limit; const take = limit; // WHERE 조건 구성 const whereConditions: string[] = []; const params: any[] = []; let paramIndex = 1; if (batch_config_id) { whereConditions.push(`bel.batch_config_id = $${paramIndex++}`); params.push(batch_config_id); } if (execution_status) { whereConditions.push(`bel.execution_status = $${paramIndex++}`); params.push(execution_status); } if (start_date) { whereConditions.push(`bel.start_time >= $${paramIndex++}`); params.push(start_date); } if (end_date) { whereConditions.push(`bel.start_time <= $${paramIndex++}`); params.push(end_date); } const whereClause = whereConditions.length > 0 ? `WHERE ${whereConditions.join(" AND ")}` : ""; // 로그 조회 (batch_config 정보 포함) const sql = ` SELECT bel.*, json_build_object( 'id', bc.id, 'batch_name', bc.batch_name, 'description', bc.description, 'cron_schedule', bc.cron_schedule, 'is_active', bc.is_active ) as batch_config FROM batch_execution_logs bel LEFT JOIN batch_configs bc ON bel.batch_config_id = bc.id ${whereClause} ORDER BY bel.start_time DESC LIMIT $${paramIndex} OFFSET $${paramIndex + 1} `; const countSql = ` SELECT COUNT(*) as count FROM batch_execution_logs bel ${whereClause} `; params.push(take, skip); const [logs, countResult] = await Promise.all([ query(sql, params), query<{ count: number }>(countSql, params.slice(0, -2)), ]); const total = parseInt(countResult[0]?.count?.toString() || "0", 10); return { success: true, data: logs as BatchExecutionLogWithConfig[], pagination: { page, limit, total, totalPages: Math.ceil(total / limit), }, }; } catch (error) { console.error("배치 실행 로그 조회 실패:", error); return { success: false, message: "배치 실행 로그 조회 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "알 수 없는 오류", }; } } /** * 배치 실행 로그 생성 */ static async createExecutionLog( data: CreateBatchExecutionLogRequest ): Promise> { try { const log = await queryOne( `INSERT INTO batch_execution_logs ( batch_config_id, execution_status, start_time, end_time, duration_ms, total_records, success_records, failed_records, error_message, error_details, server_name, process_id ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING *`, [ data.batch_config_id, data.execution_status, data.start_time || new Date(), data.end_time, data.duration_ms, data.total_records || 0, data.success_records || 0, data.failed_records || 0, data.error_message, data.error_details, data.server_name || process.env.HOSTNAME || "unknown", data.process_id || process.pid?.toString(), ] ); return { success: true, data: log as BatchExecutionLog, message: "배치 실행 로그가 생성되었습니다.", }; } catch (error) { console.error("배치 실행 로그 생성 실패:", error); return { success: false, message: "배치 실행 로그 생성 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "알 수 없는 오류", }; } } /** * 배치 실행 로그 업데이트 */ static async updateExecutionLog( id: number, data: UpdateBatchExecutionLogRequest ): Promise> { try { // 동적 UPDATE 쿼리 생성 const updates: string[] = []; const params: any[] = []; let paramIndex = 1; if (data.execution_status !== undefined) { updates.push(`execution_status = $${paramIndex++}`); params.push(data.execution_status); } if (data.end_time !== undefined) { updates.push(`end_time = $${paramIndex++}`); params.push(data.end_time); } if (data.duration_ms !== undefined) { updates.push(`duration_ms = $${paramIndex++}`); params.push(data.duration_ms); } if (data.total_records !== undefined) { updates.push(`total_records = $${paramIndex++}`); params.push(data.total_records); } if (data.success_records !== undefined) { updates.push(`success_records = $${paramIndex++}`); params.push(data.success_records); } if (data.failed_records !== undefined) { updates.push(`failed_records = $${paramIndex++}`); params.push(data.failed_records); } if (data.error_message !== undefined) { updates.push(`error_message = $${paramIndex++}`); params.push(data.error_message); } if (data.error_details !== undefined) { updates.push(`error_details = $${paramIndex++}`); params.push(data.error_details); } params.push(id); const log = await queryOne( `UPDATE batch_execution_logs SET ${updates.join(", ")} WHERE id = $${paramIndex} RETURNING *`, params ); return { success: true, data: log as BatchExecutionLog, message: "배치 실행 로그가 업데이트되었습니다.", }; } catch (error) { console.error("배치 실행 로그 업데이트 실패:", error); return { success: false, message: "배치 실행 로그 업데이트 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "알 수 없는 오류", }; } } /** * 배치 실행 로그 삭제 */ static async deleteExecutionLog(id: number): Promise> { try { await query(`DELETE FROM batch_execution_logs WHERE id = $1`, [id]); return { success: true, message: "배치 실행 로그가 삭제되었습니다.", }; } catch (error) { console.error("배치 실행 로그 삭제 실패:", error); return { success: false, message: "배치 실행 로그 삭제 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "알 수 없는 오류", }; } } /** * 특정 배치의 최신 실행 로그 조회 */ static async getLatestExecutionLog( batchConfigId: number ): Promise> { try { const log = await queryOne( `SELECT * FROM batch_execution_logs WHERE batch_config_id = $1 ORDER BY start_time DESC LIMIT 1`, [batchConfigId] ); return { success: true, data: log || null, }; } catch (error) { console.error("최신 배치 실행 로그 조회 실패:", error); return { success: false, message: "최신 배치 실행 로그 조회 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "알 수 없는 오류", }; } } /** * 배치 실행 통계 조회 */ static async getExecutionStats( batchConfigId?: number, startDate?: Date, endDate?: Date ): Promise< ApiResponse<{ total_executions: number; success_count: number; failed_count: number; success_rate: number; average_duration_ms: number; total_records_processed: number; }> > { try { const whereConditions: string[] = []; const params: any[] = []; let paramIndex = 1; if (batchConfigId) { whereConditions.push(`batch_config_id = $${paramIndex++}`); params.push(batchConfigId); } if (startDate) { whereConditions.push(`start_time >= $${paramIndex++}`); params.push(startDate); } if (endDate) { whereConditions.push(`start_time <= $${paramIndex++}`); params.push(endDate); } const whereClause = whereConditions.length > 0 ? `WHERE ${whereConditions.join(" AND ")}` : ""; const logs = await query<{ execution_status: string; duration_ms: number; total_records: number; }>( `SELECT execution_status, duration_ms, total_records FROM batch_execution_logs ${whereClause}`, params ); const total_executions = logs.length; const success_count = logs.filter( (log: any) => log.execution_status === "SUCCESS" ).length; const failed_count = logs.filter( (log: any) => log.execution_status === "FAILED" ).length; const success_rate = total_executions > 0 ? (success_count / total_executions) * 100 : 0; const validDurations = logs .filter((log: any) => log.duration_ms !== null) .map((log: any) => log.duration_ms!); const average_duration_ms = validDurations.length > 0 ? validDurations.reduce( (sum: number, duration: number) => sum + duration, 0 ) / validDurations.length : 0; const total_records_processed = logs .filter((log: any) => log.total_records !== null) .reduce((sum: number, log: any) => sum + (log.total_records || 0), 0); return { success: true, data: { total_executions, success_count, failed_count, success_rate, average_duration_ms, total_records_processed, }, }; } catch (error) { console.error("배치 실행 통계 조회 실패:", error); return { success: false, message: "배치 실행 통계 조회 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "알 수 없는 오류", }; } } }