From 505f656c1571c69a53e8e4b3ce5878821d596889 Mon Sep 17 00:00:00 2001 From: kjs Date: Wed, 1 Oct 2025 13:30:20 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20Phase=203.15=20=EB=B0=B0=EC=B9=98=20?= =?UTF-8?q?=EC=84=9C=EB=B9=84=EC=8A=A4=20Raw=20Query=20=EC=A0=84=ED=99=98?= =?UTF-8?q?=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- PHASE3.15_BATCH_SERVICES_MIGRATION.md | 129 ++++++++-- PRISMA_TO_RAW_QUERY_MIGRATION_PLAN.md | 10 +- .../src/services/batchExecutionLogService.ts | 239 +++++++++++------- .../src/services/batchExternalDbService.ts | 81 +++--- .../src/services/batchManagementService.ts | 60 +++-- .../src/services/batchSchedulerService.ts | 77 ++++-- 6 files changed, 406 insertions(+), 190 deletions(-) diff --git a/PHASE3.15_BATCH_SERVICES_MIGRATION.md b/PHASE3.15_BATCH_SERVICES_MIGRATION.md index 7fbefec1..b2df870e 100644 --- a/PHASE3.15_BATCH_SERVICES_MIGRATION.md +++ b/PHASE3.15_BATCH_SERVICES_MIGRATION.md @@ -6,16 +6,75 @@ ### 📊 기본 정보 -| 항목 | 내용 | -| --------------- | -------------------------------------------------------------- | -| 대상 서비스 | 4개 (BatchExternalDb, ExecutionLog, Management, Scheduler) | -| 파일 위치 | `backend-node/src/services/batch*.ts` | -| 총 파일 크기 | 2,161 라인 | -| Prisma 호출 | 24개 | -| **현재 진행률** | **0/24 (0%)** 🔄 **진행 예정** | -| 복잡도 | 높음 (외부 DB 연동, 스케줄링, 트랜잭션) | -| 우선순위 | 🔴 높음 (Phase 3.15) | -| **상태** | ⏳ **대기 중** | +| 항목 | 내용 | +| --------------- | ---------------------------------------------------------- | +| 대상 서비스 | 4개 (BatchExternalDb, ExecutionLog, Management, Scheduler) | +| 파일 위치 | `backend-node/src/services/batch*.ts` | +| 총 파일 크기 | 2,161 라인 | +| Prisma 호출 | 0개 (전환 완료) | +| **현재 진행률** | **24/24 (100%)** ✅ **전환 완료** | +| 복잡도 | 높음 (외부 DB 연동, 스케줄링, 트랜잭션) | +| 우선순위 | 🔴 높음 (Phase 3.15) | +| **상태** | ✅ **완료** | + +--- + +## ✅ 전환 완료 내역 + +### 전환된 Prisma 호출 (24개) + +#### 1. BatchExternalDbService (8개) +- `getAvailableConnections()` - findMany → query +- `getTables()` - $queryRaw → query (information_schema) +- `getTableColumns()` - $queryRaw → query (information_schema) +- `getExternalTables()` - findUnique → queryOne (x5) + +#### 2. BatchExecutionLogService (7개) +- `getExecutionLogs()` - findMany + count → query (JOIN + 동적 WHERE) +- `createExecutionLog()` - create → queryOne (INSERT RETURNING) +- `updateExecutionLog()` - update → queryOne (동적 UPDATE) +- `deleteExecutionLog()` - delete → query +- `getLatestExecutionLog()` - findFirst → queryOne +- `getExecutionStats()` - findMany → query (동적 WHERE) + +#### 3. BatchManagementService (5개) +- `getAvailableConnections()` - findMany → query +- `getTables()` - $queryRaw → query (information_schema) +- `getTableColumns()` - $queryRaw → query (information_schema) +- `getExternalTables()` - findUnique → queryOne (x2) + +#### 4. BatchSchedulerService (4개) +- `loadActiveBatchConfigs()` - findMany → query (JOIN with json_agg) +- `updateBatchSchedule()` - findUnique → query (JOIN with json_agg) +- `getDataFromSource()` - $queryRawUnsafe → query +- `insertDataToTarget()` - $executeRawUnsafe → query + +### 주요 기술적 해결 사항 + +1. **외부 DB 연결 조회 반복** + - 5개의 `findUnique` 호출을 `queryOne`으로 일괄 전환 + - 암호화/복호화 로직 유지 + +2. **배치 설정 + 매핑 JOIN** + - Prisma `include` → `json_agg` + `json_build_object` + - `FILTER (WHERE bm.id IS NOT NULL)` 로 NULL 방지 + - 계층적 JSON 데이터 생성 + +3. **동적 WHERE 절 생성** + - 조건부 필터링 (batch_config_id, execution_status, 날짜 범위) + - 파라미터 인덱스 동적 관리 + +4. **동적 UPDATE 쿼리** + - undefined 필드 제외 + - 8개 필드의 조건부 업데이트 + +5. **통계 쿼리 전환** + - 클라이언트 사이드 집계 유지 + - 원본 데이터만 쿼리로 조회 + +### 컴파일 상태 +✅ TypeScript 컴파일 성공 +✅ Linter 오류 없음 --- @@ -24,12 +83,14 @@ ### 1. BatchExternalDbService (8개 호출, 943 라인) **주요 기능**: + - 외부 DB에서 배치 데이터 조회 - 외부 DB로 배치 데이터 저장 - 외부 DB 연결 관리 - 데이터 변환 및 매핑 **예상 Prisma 호출**: + - `getExternalDbConnection()` - 외부 DB 연결 정보 조회 - `fetchDataFromExternalDb()` - 외부 DB 데이터 조회 - `saveDataToExternalDb()` - 외부 DB 데이터 저장 @@ -40,6 +101,7 @@ - `getBatchExecutionStatus()` - 실행 상태 조회 **기술적 고려사항**: + - 다양한 DB 타입 지원 (PostgreSQL, MySQL, Oracle, MSSQL) - 연결 풀 관리 - 트랜잭션 처리 @@ -50,12 +112,14 @@ ### 2. BatchExecutionLogService (7개 호출, 299 라인) **주요 기능**: + - 배치 실행 로그 생성 - 배치 실행 이력 조회 - 배치 실행 통계 - 로그 정리 **예상 Prisma 호출**: + - `createExecutionLog()` - 실행 로그 생성 - `updateExecutionLog()` - 실행 로그 업데이트 - `getExecutionLogs()` - 실행 로그 목록 조회 @@ -65,6 +129,7 @@ - `getFailedExecutions()` - 실패한 실행 조회 **기술적 고려사항**: + - 대용량 로그 처리 - 통계 쿼리 최적화 - 로그 보관 정책 @@ -75,12 +140,14 @@ ### 3. BatchManagementService (5개 호출, 373 라인) **주요 기능**: + - 배치 작업 설정 관리 - 배치 작업 실행 - 배치 작업 중지 - 배치 작업 모니터링 **예상 Prisma 호출**: + - `getBatchJobs()` - 배치 작업 목록 조회 - `getBatchJob()` - 배치 작업 단건 조회 - `createBatchJob()` - 배치 작업 생성 @@ -88,6 +155,7 @@ - `deleteBatchJob()` - 배치 작업 삭제 **기술적 고려사항**: + - JSON 설정 필드 (job_config) - 작업 상태 관리 - 동시 실행 제어 @@ -98,18 +166,21 @@ ### 4. BatchSchedulerService (4개 호출, 546 라인) **주요 기능**: + - 배치 스케줄 설정 - Cron 표현식 관리 - 스케줄 실행 - 다음 실행 시간 계산 **예상 Prisma 호출**: + - `getScheduledBatches()` - 스케줄된 배치 조회 - `createSchedule()` - 스케줄 생성 - `updateSchedule()` - 스케줄 수정 - `deleteSchedule()` - 스케줄 삭제 **기술적 고려사항**: + - Cron 표현식 파싱 - 시간대 처리 - 실행 이력 추적 @@ -120,17 +191,23 @@ ## 💡 통합 전환 전략 ### Phase 1: 핵심 서비스 전환 (12개) + **BatchManagementService (5개) + BatchExecutionLogService (7개)** + - 배치 관리 및 로깅 기능 우선 - 상대적으로 단순한 CRUD ### Phase 2: 스케줄러 전환 (4개) + **BatchSchedulerService (4개)** + - 스케줄 관리 - Cron 표현식 처리 ### Phase 3: 외부 DB 연동 전환 (8개) + **BatchExternalDbService (8개)** + - 가장 복잡한 서비스 - 외부 DB 연결 및 쿼리 @@ -141,6 +218,7 @@ ### 예시 1: 배치 실행 로그 생성 **변경 전**: + ```typescript const log = await prisma.batch_execution_logs.create({ data: { @@ -154,6 +232,7 @@ const log = await prisma.batch_execution_logs.create({ ``` **변경 후**: + ```typescript const log = await queryOne( `INSERT INTO batch_execution_logs @@ -167,6 +246,7 @@ const log = await queryOne( ### 예시 2: 배치 통계 조회 **변경 전**: + ```typescript const stats = await prisma.batch_execution_logs.groupBy({ by: ["status"], @@ -179,6 +259,7 @@ const stats = await prisma.batch_execution_logs.groupBy({ ``` **변경 후**: + ```typescript const stats = await query<{ status: string; count: string }>( `SELECT status, COUNT(*) as count @@ -194,6 +275,7 @@ const stats = await query<{ status: string; count: string }>( ### 예시 3: 외부 DB 연결 및 쿼리 **변경 전**: + ```typescript // 연결 정보 조회 const connection = await prisma.external_db_connections.findUnique({ @@ -205,6 +287,7 @@ const externalData = await externalDbClient.query(sql); ``` **변경 후**: + ```typescript // 연결 정보 조회 const connection = await queryOne( @@ -219,6 +302,7 @@ const externalData = await externalDbClient.query(sql); ### 예시 4: 스케줄 관리 **변경 전**: + ```typescript const schedule = await prisma.batch_schedules.create({ data: { @@ -231,6 +315,7 @@ const schedule = await prisma.batch_schedules.create({ ``` **변경 후**: + ```typescript const nextRun = calculateNextRun(cronExp); @@ -248,6 +333,7 @@ const schedule = await queryOne( ## 🔧 기술적 고려사항 ### 1. 외부 DB 연결 관리 + ```typescript import { DatabaseConnectorFactory } from "../database/connectorFactory"; @@ -264,13 +350,14 @@ try { ``` ### 2. 트랜잭션 처리 + ```typescript await transaction(async (client) => { // 배치 상태 업데이트 - await client.query( - `UPDATE batch_jobs SET status = $1 WHERE id = $2`, - ["running", batchId] - ); + await client.query(`UPDATE batch_jobs SET status = $1 WHERE id = $2`, [ + "running", + batchId, + ]); // 실행 로그 생성 await client.query( @@ -282,6 +369,7 @@ await transaction(async (client) => { ``` ### 3. Cron 표현식 처리 + ```typescript import cron from "node-cron"; @@ -296,6 +384,7 @@ function calculateNextRun(cronExp: string): Date { ``` ### 4. 대용량 데이터 처리 + ```typescript // 스트리밍 방식으로 대용량 데이터 처리 const stream = await query( @@ -313,6 +402,7 @@ for await (const row of stream) { ## 📝 전환 체크리스트 ### BatchExternalDbService (8개) + - [ ] `getExternalDbConnection()` - 연결 정보 조회 - [ ] `fetchDataFromExternalDb()` - 외부 DB 데이터 조회 - [ ] `saveDataToExternalDb()` - 외부 DB 데이터 저장 @@ -323,6 +413,7 @@ for await (const row of stream) { - [ ] `getBatchExecutionStatus()` - 실행 상태 조회 ### BatchExecutionLogService (7개) + - [ ] `createExecutionLog()` - 실행 로그 생성 - [ ] `updateExecutionLog()` - 실행 로그 업데이트 - [ ] `getExecutionLogs()` - 실행 로그 목록 조회 @@ -332,6 +423,7 @@ for await (const row of stream) { - [ ] `getFailedExecutions()` - 실패한 실행 조회 ### BatchManagementService (5개) + - [ ] `getBatchJobs()` - 배치 작업 목록 조회 - [ ] `getBatchJob()` - 배치 작업 단건 조회 - [ ] `createBatchJob()` - 배치 작업 생성 @@ -339,12 +431,14 @@ for await (const row of stream) { - [ ] `deleteBatchJob()` - 배치 작업 삭제 ### BatchSchedulerService (4개) + - [ ] `getScheduledBatches()` - 스케줄된 배치 조회 - [ ] `createSchedule()` - 스케줄 생성 - [ ] `updateSchedule()` - 스케줄 수정 - [ ] `deleteSchedule()` - 스케줄 삭제 ### 공통 작업 + - [ ] import 문 수정 (모든 서비스) - [ ] Prisma import 완전 제거 (모든 서비스) - [ ] 트랜잭션 로직 확인 @@ -355,15 +449,18 @@ for await (const row of stream) { ## 🧪 테스트 계획 ### 단위 테스트 (24개) + - 각 Prisma 호출별 1개씩 ### 통합 테스트 (8개) + - BatchExternalDbService: 외부 DB 연동 테스트 (2개) - BatchExecutionLogService: 로그 생성 및 조회 테스트 (2개) - BatchManagementService: 배치 작업 실행 테스트 (2개) - BatchSchedulerService: 스케줄 실행 테스트 (2개) ### 성능 테스트 + - 대용량 데이터 처리 성능 - 동시 배치 실행 성능 - 외부 DB 연결 풀 성능 @@ -377,7 +474,6 @@ for await (const row of stream) { - 트랜잭션 처리 - 스케줄링 로직 - 대용량 데이터 처리 - - **예상 소요 시간**: 4~5시간 - Phase 1 (BatchManagement + ExecutionLog): 1.5시간 - Phase 2 (Scheduler): 1시간 @@ -389,6 +485,7 @@ for await (const row of stream) { ## ⚠️ 주의사항 ### 중요 체크포인트 + 1. ✅ 외부 DB 연결은 반드시 try-finally에서 해제 2. ✅ 배치 실행 중 에러 시 롤백 처리 3. ✅ Cron 표현식 검증 필수 @@ -396,6 +493,7 @@ for await (const row of stream) { 5. ✅ 동시 실행 제한 확인 ### 성능 최적화 + - 연결 풀 활용 - 배치 쿼리 최적화 - 인덱스 확인 @@ -406,4 +504,3 @@ for await (const row of stream) { **상태**: ⏳ **대기 중** **특이사항**: 외부 DB 연동, 스케줄링, 트랜잭션 처리 포함 **⚠️ 주의**: 배치 시스템의 핵심 기능이므로 신중한 테스트 필수! - diff --git a/PRISMA_TO_RAW_QUERY_MIGRATION_PLAN.md b/PRISMA_TO_RAW_QUERY_MIGRATION_PLAN.md index 2f74db2b..6ca6e9e2 100644 --- a/PRISMA_TO_RAW_QUERY_MIGRATION_PLAN.md +++ b/PRISMA_TO_RAW_QUERY_MIGRATION_PLAN.md @@ -139,11 +139,11 @@ backend-node/ (루트) - `externalCallConfigService.ts` (0개) - ✅ **전환 완료** (Phase 3.12) - [계획서](PHASE3.12_EXTERNAL_CALL_CONFIG_SERVICE_MIGRATION.md) - `entityJoinService.ts` (0개) - ✅ **전환 완료** (Phase 3.13) - [계획서](PHASE3.13_ENTITY_JOIN_SERVICE_MIGRATION.md) - `authService.ts` (0개) - ✅ **전환 완료** (Phase 1.5에서 완료) - [계획서](PHASE3.14_AUTH_SERVICE_MIGRATION.md) -- **배치 관련 서비스 (24개)** - [통합 계획서](PHASE3.15_BATCH_SERVICES_MIGRATION.md) - - `batchExternalDbService.ts` (8개) - 배치 외부DB - - `batchExecutionLogService.ts` (7개) - 배치 실행 로그 - - `batchManagementService.ts` (5개) - 배치 관리 - - `batchSchedulerService.ts` (4개) - 배치 스케줄러 +- **배치 관련 서비스 (0개)** - ✅ **전환 완료** - [통합 계획서](PHASE3.15_BATCH_SERVICES_MIGRATION.md) + - `batchExternalDbService.ts` (0개) - ✅ **전환 완료** + - `batchExecutionLogService.ts` (0개) - ✅ **전환 완료** + - `batchManagementService.ts` (0개) - ✅ **전환 완료** + - `batchSchedulerService.ts` (0개) - ✅ **전환 완료** - **데이터 관리 서비스 (0개)** - ✅ **전환 완료** - [통합 계획서](PHASE3.16_DATA_MANAGEMENT_SERVICES_MIGRATION.md) - `enhancedDynamicFormService.ts` (0개) - ✅ **전환 완료** - `dataMappingService.ts` (0개) - ✅ **전환 완료** diff --git a/backend-node/src/services/batchExecutionLogService.ts b/backend-node/src/services/batchExecutionLogService.ts index 2fee555a..5f7335f4 100644 --- a/backend-node/src/services/batchExecutionLogService.ts +++ b/backend-node/src/services/batchExecutionLogService.ts @@ -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(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> { 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( + `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> { 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( + `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> { 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> { try { - const log = await prisma.batch_execution_logs.findFirst({ - where: { batch_config_id: batchConfigId }, - orderBy: { start_time: 'desc' } - }); + 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 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; diff --git a/backend-node/src/services/batchExternalDbService.ts b/backend-node/src/services/batchExternalDbService.ts index d5670f04..c2c0851e 100644 --- a/backend-node/src/services/batchExternalDbService.ts +++ b/backend-node/src/services/batchExternalDbService.ts @@ -2,7 +2,7 @@ // 기존 ExternalDbConnectionService와 분리하여 배치관리 시스템에 특화된 기능 제공 // 작성일: 2024-12-24 -import prisma from "../config/database"; +import { query, queryOne } from "../database/db"; import { PasswordEncryption } from "../utils/passwordEncryption"; import { DatabaseConnectorFactory } from "../database/DatabaseConnectorFactory"; import { RestApiConnector } from "../database/RestApiConnector"; @@ -34,16 +34,18 @@ export class BatchExternalDbService { }); // 활성화된 외부 DB 연결 조회 - const externalConnections = await prisma.external_db_connections.findMany({ - where: { is_active: 'Y' }, - select: { - id: true, - connection_name: true, - db_type: true, - description: true - }, - orderBy: { connection_name: 'asc' } - }); + const externalConnections = await query<{ + id: number; + connection_name: string; + db_type: string; + description: string; + }>( + `SELECT id, connection_name, db_type, description + FROM external_db_connections + WHERE is_active = 'Y' + ORDER BY connection_name ASC`, + [] + ); // 외부 DB 연결 추가 externalConnections.forEach(conn => { @@ -82,13 +84,14 @@ export class BatchExternalDbService { if (connectionType === 'internal') { // 내부 DB 테이블 조회 - const result = await prisma.$queryRaw>` - SELECT table_name + const result = await query<{ table_name: string }>( + `SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_type = 'BASE TABLE' - ORDER BY table_name - `; + ORDER BY table_name`, + [] + ); tables = result.map(row => ({ table_name: row.table_name, @@ -138,22 +141,23 @@ export class BatchExternalDbService { // 내부 DB 컬럼 조회 console.log(`[BatchExternalDbService] 내부 DB 컬럼 조회 시작: ${tableName}`); - const result = await prisma.$queryRaw>` - SELECT + }>( + `SELECT column_name, data_type, is_nullable, column_default FROM information_schema.columns WHERE table_schema = 'public' - AND table_name = ${tableName} - ORDER BY ordinal_position - `; + AND table_name = $1 + ORDER BY ordinal_position`, + [tableName] + ); console.log(`[BatchExternalDbService] 내부 DB 컬럼 조회 결과:`, result); @@ -198,9 +202,10 @@ export class BatchExternalDbService { private static async getExternalTables(connectionId: number): Promise> { try { // 연결 정보 조회 - const connection = await prisma.external_db_connections.findUnique({ - where: { id: connectionId } - }); + const connection = await queryOne( + `SELECT * FROM external_db_connections WHERE id = $1`, + [connectionId] + ); if (!connection) { return { @@ -257,9 +262,10 @@ export class BatchExternalDbService { console.log(`[BatchExternalDbService] getExternalTableColumns 호출: connectionId=${connectionId}, tableName=${tableName}`); // 연결 정보 조회 - const connection = await prisma.external_db_connections.findUnique({ - where: { id: connectionId } - }); + const connection = await queryOne( + `SELECT * FROM external_db_connections WHERE id = $1`, + [connectionId] + ); if (!connection) { console.log(`[BatchExternalDbService] 연결 정보를 찾을 수 없음: connectionId=${connectionId}`); @@ -418,9 +424,10 @@ export class BatchExternalDbService { console.log(`[BatchExternalDbService] 외부 DB 데이터 조회: connectionId=${connectionId}, tableName=${tableName}`); // 외부 DB 연결 정보 조회 - const connection = await prisma.external_db_connections.findUnique({ - where: { id: connectionId } - }); + const connection = await queryOne( + `SELECT * FROM external_db_connections WHERE id = $1`, + [connectionId] + ); if (!connection) { return { @@ -490,9 +497,10 @@ export class BatchExternalDbService { console.log(`[BatchExternalDbService] 외부 DB 특정 컬럼 조회: connectionId=${connectionId}, tableName=${tableName}, columns=[${columns.join(', ')}]`); // 외부 DB 연결 정보 조회 - const connection = await prisma.external_db_connections.findUnique({ - where: { id: connectionId } - }); + const connection = await queryOne( + `SELECT * FROM external_db_connections WHERE id = $1`, + [connectionId] + ); if (!connection) { return { @@ -569,9 +577,10 @@ export class BatchExternalDbService { } // 외부 DB 연결 정보 조회 - const connection = await prisma.external_db_connections.findUnique({ - where: { id: connectionId } - }); + const connection = await queryOne( + `SELECT * FROM external_db_connections WHERE id = $1`, + [connectionId] + ); if (!connection) { return { diff --git a/backend-node/src/services/batchManagementService.ts b/backend-node/src/services/batchManagementService.ts index 1b082209..6d09178b 100644 --- a/backend-node/src/services/batchManagementService.ts +++ b/backend-node/src/services/batchManagementService.ts @@ -1,7 +1,7 @@ // 배치관리 전용 서비스 (기존 소스와 완전 분리) // 작성일: 2024-12-24 -import prisma from "../config/database"; +import { query, queryOne } from "../database/db"; import { PasswordEncryption } from "../utils/passwordEncryption"; import { DatabaseConnectorFactory } from "../database/DatabaseConnectorFactory"; @@ -49,16 +49,18 @@ export class BatchManagementService { }); // 활성화된 외부 DB 연결 조회 - const externalConnections = await prisma.external_db_connections.findMany({ - where: { is_active: 'Y' }, - select: { - id: true, - connection_name: true, - db_type: true, - description: true - }, - orderBy: { connection_name: 'asc' } - }); + const externalConnections = await query<{ + id: number; + connection_name: string; + db_type: string; + description: string; + }>( + `SELECT id, connection_name, db_type, description + FROM external_db_connections + WHERE is_active = 'Y' + ORDER BY connection_name ASC`, + [] + ); // 외부 DB 연결 추가 externalConnections.forEach(conn => { @@ -97,13 +99,14 @@ export class BatchManagementService { if (connectionType === 'internal') { // 내부 DB 테이블 조회 - const result = await prisma.$queryRaw>` - SELECT table_name + const result = await query<{ table_name: string }>( + `SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_type = 'BASE TABLE' - ORDER BY table_name - `; + ORDER BY table_name`, + [] + ); tables = result.map(row => ({ table_name: row.table_name, @@ -153,22 +156,23 @@ export class BatchManagementService { // 내부 DB 컬럼 조회 console.log(`[BatchManagementService] 내부 DB 컬럼 조회 시작: ${tableName}`); - const result = await prisma.$queryRaw>` - SELECT + }>( + `SELECT column_name, data_type, is_nullable, column_default FROM information_schema.columns WHERE table_schema = 'public' - AND table_name = ${tableName} - ORDER BY ordinal_position - `; + AND table_name = $1 + ORDER BY ordinal_position`, + [tableName] + ); console.log(`[BatchManagementService] 쿼리 결과:`, result); @@ -215,9 +219,10 @@ export class BatchManagementService { private static async getExternalTables(connectionId: number): Promise> { try { // 연결 정보 조회 - const connection = await prisma.external_db_connections.findUnique({ - where: { id: connectionId } - }); + const connection = await queryOne( + `SELECT * FROM external_db_connections WHERE id = $1`, + [connectionId] + ); if (!connection) { return { @@ -274,9 +279,10 @@ export class BatchManagementService { console.log(`[BatchManagementService] getExternalTableColumns 호출: connectionId=${connectionId}, tableName=${tableName}`); // 연결 정보 조회 - const connection = await prisma.external_db_connections.findUnique({ - where: { id: connectionId } - }); + const connection = await queryOne( + `SELECT * FROM external_db_connections WHERE id = $1`, + [connectionId] + ); if (!connection) { console.log(`[BatchManagementService] 연결 정보를 찾을 수 없음: connectionId=${connectionId}`); diff --git a/backend-node/src/services/batchSchedulerService.ts b/backend-node/src/services/batchSchedulerService.ts index ea2f7f89..21166f93 100644 --- a/backend-node/src/services/batchSchedulerService.ts +++ b/backend-node/src/services/batchSchedulerService.ts @@ -2,7 +2,7 @@ // 작성일: 2024-12-24 import * as cron from 'node-cron'; -import prisma from '../config/database'; +import { query, queryOne } from '../database/db'; import { BatchService } from './batchService'; import { BatchExecutionLogService } from './batchExecutionLogService'; import { logger } from '../utils/logger'; @@ -59,14 +59,28 @@ export class BatchSchedulerService { */ private static async loadActiveBatchConfigs() { try { - const activeConfigs = await prisma.batch_configs.findMany({ - where: { - is_active: 'Y' - }, - include: { - batch_mappings: true - } - }); + const activeConfigs = await query( + `SELECT + bc.*, + json_agg( + json_build_object( + 'id', bm.id, + 'batch_config_id', bm.batch_config_id, + 'field_name', bm.field_name, + 'source_field', bm.source_field, + 'field_type', bm.field_type, + 'is_required', bm.is_required, + 'default_value', bm.default_value, + 'transform_function', bm.transform_function, + 'sort_order', bm.sort_order + ) + ) FILTER (WHERE bm.id IS NOT NULL) as batch_mappings + FROM batch_configs bc + LEFT JOIN batch_mappings bm ON bc.id = bm.batch_config_id + WHERE bc.is_active = 'Y' + GROUP BY bc.id`, + [] + ); logger.info(`활성화된 배치 설정 ${activeConfigs.length}개 발견`); @@ -153,10 +167,30 @@ export class BatchSchedulerService { await this.unscheduleBatchConfig(configId); // 업데이트된 배치 설정 조회 - const config = await prisma.batch_configs.findUnique({ - where: { id: configId }, - include: { batch_mappings: true } - }); + const configResult = await query( + `SELECT + bc.*, + json_agg( + json_build_object( + 'id', bm.id, + 'batch_config_id', bm.batch_config_id, + 'field_name', bm.field_name, + 'source_field', bm.source_field, + 'field_type', bm.field_type, + 'is_required', bm.is_required, + 'default_value', bm.default_value, + 'transform_function', bm.transform_function, + 'sort_order', bm.sort_order + ) + ) FILTER (WHERE bm.id IS NOT NULL) as batch_mappings + FROM batch_configs bc + LEFT JOIN batch_mappings bm ON bc.id = bm.batch_config_id + WHERE bc.id = $1 + GROUP BY bc.id`, + [configId] + ); + + const config = configResult[0] || null; if (!config) { logger.warn(`배치 설정을 찾을 수 없습니다: ID ${configId}`); @@ -455,10 +489,11 @@ export class BatchSchedulerService { try { if (mapping.from_connection_type === 'internal') { // 내부 DB에서 조회 - const result = await prisma.$queryRawUnsafe( - `SELECT * FROM ${mapping.from_table_name}` + const result = await query( + `SELECT * FROM ${mapping.from_table_name}`, + [] ); - return result as any[]; + return result; } else { // 외부 DB에서 조회 (구현 필요) logger.warn('외부 DB 조회는 아직 구현되지 않았습니다.'); @@ -485,9 +520,13 @@ export class BatchSchedulerService { // 매핑된 컬럼만 추출 const mappedData = this.mapColumns(record, mapping); - await prisma.$executeRawUnsafe( - `INSERT INTO ${mapping.to_table_name} (${Object.keys(mappedData).join(', ')}) VALUES (${Object.values(mappedData).map(() => '?').join(', ')})`, - ...Object.values(mappedData) + const columns = Object.keys(mappedData); + const values = Object.values(mappedData); + const placeholders = values.map((_, i) => `$${i + 1}`).join(', '); + + await query( + `INSERT INTO ${mapping.to_table_name} (${columns.join(', ')}) VALUES (${placeholders})`, + values ); successCount++; } catch (error) {