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

@ -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<any>(
`INSERT INTO batch_execution_logs
@ -167,6 +246,7 @@ const log = await queryOne<any>(
### 예시 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<any>(
@ -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<any>(
## 🔧 기술적 고려사항
### 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<any>(
@ -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 연동, 스케줄링, 트랜잭션 처리 포함
**⚠️ 주의**: 배치 시스템의 핵심 기능이므로 신중한 테스트 필수!

View File

@ -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개) - ✅ **전환 완료**

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;

View File

@ -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<Array<{ table_name: string }>>`
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<Array<{
const result = await query<{
column_name: string;
data_type: string;
is_nullable: string;
column_default: string | null
}>>`
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<ApiResponse<TableInfo[]>> {
try {
// 연결 정보 조회
const connection = await prisma.external_db_connections.findUnique({
where: { id: connectionId }
});
const connection = await queryOne<any>(
`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<any>(
`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<any>(
`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<any>(
`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<any>(
`SELECT * FROM external_db_connections WHERE id = $1`,
[connectionId]
);
if (!connection) {
return {

View File

@ -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<Array<{ table_name: string }>>`
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<Array<{
const result = await query<{
column_name: string;
data_type: string;
is_nullable: string;
column_default: string | null
}>>`
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<BatchApiResponse<BatchTableInfo[]>> {
try {
// 연결 정보 조회
const connection = await prisma.external_db_connections.findUnique({
where: { id: connectionId }
});
const connection = await queryOne<any>(
`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<any>(
`SELECT * FROM external_db_connections WHERE id = $1`,
[connectionId]
);
if (!connection) {
console.log(`[BatchManagementService] 연결 정보를 찾을 수 없음: connectionId=${connectionId}`);

View File

@ -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<any>(
`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<any>(
`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<any>(
`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) {