feat: Phase 3.15 배치 서비스 Raw Query 전환 완료

4개 서비스 24개 Prisma 호출 전환 완료

배치 서비스 전환:
- BatchExternalDbService (8개)
- BatchExecutionLogService (7개)
- BatchManagementService (5개)
- BatchSchedulerService (4개)

주요 기술:
- json_agg + json_build_object
- 동적 WHERE 절
- 동적 UPDATE 쿼리
- PostgreSQL placeholders

Phase 3 완료
문서: PHASE3.15_BATCH_SERVICES_MIGRATION.md
This commit is contained in:
kjs
2025-10-01 13:30:20 +09:00
parent 3d8f70e181
commit 505f656c15
6 changed files with 406 additions and 190 deletions

View File

@@ -1,7 +1,7 @@
// 배치 실행 로그 서비스
// 작성일: 2024-12-24
import prisma from "../config/database";
import { query, queryOne } from "../database/db";
import {
BatchExecutionLog,
CreateBatchExecutionLogRequest,
@@ -32,48 +32,65 @@ export class BatchExecutionLogService {
const take = limit;
// WHERE 조건 구성
const where: any = {};
const whereConditions: string[] = [];
const params: any[] = [];
let paramIndex = 1;
if (batch_config_id) {
where.batch_config_id = batch_config_id;
whereConditions.push(`bel.batch_config_id = $${paramIndex++}`);
params.push(batch_config_id);
}
if (execution_status) {
where.execution_status = execution_status;
whereConditions.push(`bel.execution_status = $${paramIndex++}`);
params.push(execution_status);
}
if (start_date || end_date) {
where.start_time = {};
if (start_date) {
where.start_time.gte = start_date;
}
if (end_date) {
where.start_time.lte = end_date;
}
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 [logs, total] = await Promise.all([
prisma.batch_execution_logs.findMany({
where,
include: {
batch_config: {
select: {
id: true,
batch_name: true,
description: true,
cron_schedule: true,
is_active: true
}
}
},
orderBy: { start_time: 'desc' },
skip,
take
}),
prisma.batch_execution_logs.count({ where })
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<any>(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[],
@@ -101,22 +118,28 @@ export class BatchExecutionLogService {
data: CreateBatchExecutionLogRequest
): Promise<ApiResponse<BatchExecutionLog>> {
try {
const log = await prisma.batch_execution_logs.create({
data: {
batch_config_id: data.batch_config_id,
execution_status: data.execution_status,
start_time: data.start_time || new Date(),
end_time: data.end_time,
duration_ms: data.duration_ms,
total_records: data.total_records || 0,
success_records: data.success_records || 0,
failed_records: data.failed_records || 0,
error_message: data.error_message,
error_details: data.error_details,
server_name: data.server_name || process.env.HOSTNAME || 'unknown',
process_id: data.process_id || process.pid?.toString()
}
});
const log = await queryOne<BatchExecutionLog>(
`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,
@@ -141,19 +164,53 @@ export class BatchExecutionLogService {
data: UpdateBatchExecutionLogRequest
): Promise<ApiResponse<BatchExecutionLog>> {
try {
const log = await prisma.batch_execution_logs.update({
where: { id },
data: {
execution_status: data.execution_status,
end_time: data.end_time,
duration_ms: data.duration_ms,
total_records: data.total_records,
success_records: data.success_records,
failed_records: data.failed_records,
error_message: data.error_message,
error_details: data.error_details
}
});
// 동적 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<BatchExecutionLog>(
`UPDATE batch_execution_logs
SET ${updates.join(', ')}
WHERE id = $${paramIndex}
RETURNING *`,
params
);
return {
success: true,
@@ -175,9 +232,7 @@ export class BatchExecutionLogService {
*/
static async deleteExecutionLog(id: number): Promise<ApiResponse<void>> {
try {
await prisma.batch_execution_logs.delete({
where: { id }
});
await query(`DELETE FROM batch_execution_logs WHERE id = $1`, [id]);
return {
success: true,
@@ -200,14 +255,17 @@ export class BatchExecutionLogService {
batchConfigId: number
): Promise<ApiResponse<BatchExecutionLog | null>> {
try {
const log = await prisma.batch_execution_logs.findFirst({
where: { batch_config_id: batchConfigId },
orderBy: { start_time: 'desc' }
});
const log = await queryOne<BatchExecutionLog>(
`SELECT * FROM batch_execution_logs
WHERE batch_config_id = $1
ORDER BY start_time DESC
LIMIT 1`,
[batchConfigId]
);
return {
success: true,
data: log as BatchExecutionLog | null
data: log || null
};
} catch (error) {
console.error("최신 배치 실행 로그 조회 실패:", error);
@@ -235,30 +293,37 @@ export class BatchExecutionLogService {
total_records_processed: number;
}>> {
try {
const where: any = {};
const whereConditions: string[] = [];
const params: any[] = [];
let paramIndex = 1;
if (batchConfigId) {
where.batch_config_id = batchConfigId;
whereConditions.push(`batch_config_id = $${paramIndex++}`);
params.push(batchConfigId);
}
if (startDate || endDate) {
where.start_time = {};
if (startDate) {
where.start_time.gte = startDate;
}
if (endDate) {
where.start_time.lte = endDate;
}
if (startDate) {
whereConditions.push(`start_time >= $${paramIndex++}`);
params.push(startDate);
}
if (endDate) {
whereConditions.push(`start_time <= $${paramIndex++}`);
params.push(endDate);
}
const logs = await prisma.batch_execution_logs.findMany({
where,
select: {
execution_status: true,
duration_ms: true,
total_records: true
}
});
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;