feat: 배치 관리 시스템 테스트 및 업데이트 기능 개선
- 배치 스케줄러 서비스 안정성 향상 - 외부 DB 연결 서비스 개선 - 배치 컨트롤러 및 관리 컨트롤러 업데이트 - 프론트엔드 배치 관리 페이지 개선 - Prisma 스키마 업데이트
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
|
||||
import { Request, Response } from "express";
|
||||
import { BatchService } from "../services/batchService";
|
||||
import { BatchSchedulerService } from "../services/batchSchedulerService";
|
||||
import { BatchConfigFilter, CreateBatchConfigRequest, UpdateBatchConfigRequest } from "../types/batchTypes";
|
||||
|
||||
export interface AuthenticatedRequest extends Request {
|
||||
@@ -190,6 +191,11 @@ export class BatchController {
|
||||
cronSchedule,
|
||||
mappings
|
||||
} as CreateBatchConfigRequest);
|
||||
|
||||
// 생성된 배치가 활성화 상태라면 스케줄러에 등록
|
||||
if (batchConfig.data && batchConfig.data.is_active === 'Y' && batchConfig.data.id) {
|
||||
await BatchSchedulerService.updateBatchSchedule(batchConfig.data.id);
|
||||
}
|
||||
|
||||
return res.status(201).json({
|
||||
success: true,
|
||||
@@ -235,6 +241,9 @@ export class BatchController {
|
||||
message: "배치 설정을 찾을 수 없습니다."
|
||||
});
|
||||
}
|
||||
|
||||
// 스케줄러에서 배치 스케줄 업데이트 (활성화 시 즉시 스케줄 등록)
|
||||
await BatchSchedulerService.updateBatchSchedule(Number(id));
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
|
||||
@@ -282,7 +282,13 @@ export class BatchManagementController {
|
||||
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)
|
||||
mappings.map(m => m.from_column_name),
|
||||
100, // limit
|
||||
// 파라미터 정보 전달
|
||||
firstMapping.from_api_param_type,
|
||||
firstMapping.from_api_param_name,
|
||||
firstMapping.from_api_param_value,
|
||||
firstMapping.from_api_param_source
|
||||
);
|
||||
|
||||
console.log(`API 조회 결과:`, {
|
||||
@@ -482,7 +488,16 @@ export class BatchManagementController {
|
||||
*/
|
||||
static async previewRestApiData(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const { apiUrl, apiKey, endpoint, method = 'GET' } = req.body;
|
||||
const {
|
||||
apiUrl,
|
||||
apiKey,
|
||||
endpoint,
|
||||
method = 'GET',
|
||||
paramType,
|
||||
paramName,
|
||||
paramValue,
|
||||
paramSource
|
||||
} = req.body;
|
||||
|
||||
if (!apiUrl || !apiKey || !endpoint) {
|
||||
return res.status(400).json({
|
||||
@@ -491,6 +506,15 @@ export class BatchManagementController {
|
||||
});
|
||||
}
|
||||
|
||||
console.log("🔍 REST API 미리보기 요청:", {
|
||||
apiUrl,
|
||||
endpoint,
|
||||
paramType,
|
||||
paramName,
|
||||
paramValue,
|
||||
paramSource
|
||||
});
|
||||
|
||||
// RestApiConnector 사용하여 데이터 조회
|
||||
const { RestApiConnector } = await import('../database/RestApiConnector');
|
||||
|
||||
@@ -503,8 +527,28 @@ export class BatchManagementController {
|
||||
// 연결 테스트
|
||||
await connector.connect();
|
||||
|
||||
// 파라미터가 있는 경우 엔드포인트 수정
|
||||
let finalEndpoint = endpoint;
|
||||
if (paramType && paramName && paramValue) {
|
||||
if (paramType === 'url') {
|
||||
// URL 파라미터: /api/users/{userId} → /api/users/123
|
||||
if (endpoint.includes(`{${paramName}}`)) {
|
||||
finalEndpoint = endpoint.replace(`{${paramName}}`, paramValue);
|
||||
} else {
|
||||
// 엔드포인트에 {paramName}이 없으면 뒤에 추가
|
||||
finalEndpoint = `${endpoint}/${paramValue}`;
|
||||
}
|
||||
} else if (paramType === 'query') {
|
||||
// 쿼리 파라미터: /api/users?userId=123
|
||||
const separator = endpoint.includes('?') ? '&' : '?';
|
||||
finalEndpoint = `${endpoint}${separator}${paramName}=${paramValue}`;
|
||||
}
|
||||
}
|
||||
|
||||
console.log("🔗 최종 엔드포인트:", finalEndpoint);
|
||||
|
||||
// 데이터 조회 (최대 5개만) - GET 메서드만 지원
|
||||
const result = await connector.executeQuery(endpoint, method);
|
||||
const result = await connector.executeQuery(finalEndpoint, method);
|
||||
console.log(`[previewRestApiData] executeQuery 결과:`, {
|
||||
rowCount: result.rowCount,
|
||||
rowsLength: result.rows ? result.rows.length : 'undefined',
|
||||
|
||||
@@ -697,7 +697,12 @@ export class BatchExternalDbService {
|
||||
endpoint: string,
|
||||
method: 'GET' | 'POST' | 'PUT' | 'DELETE' = 'GET',
|
||||
columns?: string[],
|
||||
limit: number = 100
|
||||
limit: number = 100,
|
||||
// 파라미터 정보 추가
|
||||
paramType?: 'url' | 'query',
|
||||
paramName?: string,
|
||||
paramValue?: string,
|
||||
paramSource?: 'static' | 'dynamic'
|
||||
): Promise<ApiResponse<any[]>> {
|
||||
try {
|
||||
console.log(`[BatchExternalDbService] REST API 데이터 조회: ${apiUrl}${endpoint}`);
|
||||
@@ -712,8 +717,33 @@ export class BatchExternalDbService {
|
||||
// 연결 테스트
|
||||
await connector.connect();
|
||||
|
||||
// 파라미터가 있는 경우 엔드포인트 수정
|
||||
const { logger } = await import('../utils/logger');
|
||||
logger.info(`[BatchExternalDbService] 파라미터 정보`, {
|
||||
paramType, paramName, paramValue, paramSource
|
||||
});
|
||||
|
||||
let finalEndpoint = endpoint;
|
||||
if (paramType && paramName && paramValue) {
|
||||
if (paramType === 'url') {
|
||||
// URL 파라미터: /api/users/{userId} → /api/users/123
|
||||
if (endpoint.includes(`{${paramName}}`)) {
|
||||
finalEndpoint = endpoint.replace(`{${paramName}}`, paramValue);
|
||||
} else {
|
||||
// 엔드포인트에 {paramName}이 없으면 뒤에 추가
|
||||
finalEndpoint = `${endpoint}/${paramValue}`;
|
||||
}
|
||||
} else if (paramType === 'query') {
|
||||
// 쿼리 파라미터: /api/users?userId=123
|
||||
const separator = endpoint.includes('?') ? '&' : '?';
|
||||
finalEndpoint = `${endpoint}${separator}${paramName}=${paramValue}`;
|
||||
}
|
||||
|
||||
logger.info(`[BatchExternalDbService] 파라미터 적용된 엔드포인트: ${finalEndpoint}`);
|
||||
}
|
||||
|
||||
// 데이터 조회
|
||||
const result = await connector.executeQuery(endpoint, method);
|
||||
const result = await connector.executeQuery(finalEndpoint, method);
|
||||
let data = result.rows;
|
||||
|
||||
// 컬럼 필터링 (지정된 컬럼만 추출)
|
||||
@@ -734,7 +764,8 @@ export class BatchExternalDbService {
|
||||
data = data.slice(0, limit);
|
||||
}
|
||||
|
||||
console.log(`[BatchExternalDbService] REST API 데이터 조회 완료: ${data.length}개 레코드`);
|
||||
logger.info(`[BatchExternalDbService] REST API 데이터 조회 완료: ${data.length}개 레코드`);
|
||||
logger.info(`[BatchExternalDbService] 조회된 데이터`, { data });
|
||||
|
||||
return {
|
||||
success: true,
|
||||
|
||||
@@ -112,7 +112,7 @@ export class BatchSchedulerService {
|
||||
/**
|
||||
* 배치 설정 업데이트 시 스케줄 재등록
|
||||
*/
|
||||
static async updateBatchSchedule(configId: number) {
|
||||
static async updateBatchSchedule(configId: number, executeImmediately: boolean = true) {
|
||||
try {
|
||||
// 기존 스케줄 제거
|
||||
await this.unscheduleBatchConfig(configId);
|
||||
@@ -132,6 +132,12 @@ export class BatchSchedulerService {
|
||||
if (config.is_active === 'Y') {
|
||||
await this.scheduleBatchConfig(config);
|
||||
logger.info(`배치 스케줄 업데이트 완료: ${config.batch_name} (ID: ${configId})`);
|
||||
|
||||
// 활성화 시 즉시 실행 (옵션)
|
||||
if (executeImmediately) {
|
||||
logger.info(`🚀 배치 활성화 즉시 실행: ${config.batch_name} (ID: ${configId})`);
|
||||
await this.executeBatchConfig(config);
|
||||
}
|
||||
} else {
|
||||
logger.info(`비활성화된 배치 스케줄 제거: ${config.batch_name} (ID: ${configId})`);
|
||||
}
|
||||
@@ -239,7 +245,13 @@ export class BatchSchedulerService {
|
||||
firstMapping.from_api_key!,
|
||||
firstMapping.from_table_name,
|
||||
firstMapping.from_api_method as 'GET' | 'POST' | 'PUT' | 'DELETE' || 'GET',
|
||||
mappings.map((m: any) => m.from_column_name)
|
||||
mappings.map((m: any) => m.from_column_name),
|
||||
100, // limit
|
||||
// 파라미터 정보 전달
|
||||
firstMapping.from_api_param_type,
|
||||
firstMapping.from_api_param_name,
|
||||
firstMapping.from_api_param_value,
|
||||
firstMapping.from_api_param_source
|
||||
);
|
||||
|
||||
if (apiResult.success && apiResult.data) {
|
||||
|
||||
@@ -168,6 +168,10 @@ export class BatchService {
|
||||
from_api_url: mapping.from_api_url,
|
||||
from_api_key: mapping.from_api_key,
|
||||
from_api_method: mapping.from_api_method,
|
||||
from_api_param_type: mapping.from_api_param_type,
|
||||
from_api_param_name: mapping.from_api_param_name,
|
||||
from_api_param_value: mapping.from_api_param_value,
|
||||
from_api_param_source: mapping.from_api_param_source,
|
||||
to_connection_type: mapping.to_connection_type,
|
||||
to_connection_id: mapping.to_connection_id,
|
||||
to_table_name: mapping.to_table_name,
|
||||
@@ -176,7 +180,7 @@ export class BatchService {
|
||||
to_api_url: mapping.to_api_url,
|
||||
to_api_key: mapping.to_api_key,
|
||||
to_api_method: mapping.to_api_method,
|
||||
// to_api_body: mapping.to_api_body, // Request Body 템플릿 추가 - 임시 주석 처리
|
||||
to_api_body: mapping.to_api_body,
|
||||
mapping_order: mapping.mapping_order || index + 1,
|
||||
created_by: userId,
|
||||
},
|
||||
@@ -260,11 +264,22 @@ export class BatchService {
|
||||
from_table_name: mapping.from_table_name,
|
||||
from_column_name: mapping.from_column_name,
|
||||
from_column_type: mapping.from_column_type,
|
||||
from_api_url: mapping.from_api_url,
|
||||
from_api_key: mapping.from_api_key,
|
||||
from_api_method: mapping.from_api_method,
|
||||
from_api_param_type: mapping.from_api_param_type,
|
||||
from_api_param_name: mapping.from_api_param_name,
|
||||
from_api_param_value: mapping.from_api_param_value,
|
||||
from_api_param_source: mapping.from_api_param_source,
|
||||
to_connection_type: mapping.to_connection_type,
|
||||
to_connection_id: mapping.to_connection_id,
|
||||
to_table_name: mapping.to_table_name,
|
||||
to_column_name: mapping.to_column_name,
|
||||
to_column_type: mapping.to_column_type,
|
||||
to_api_url: mapping.to_api_url,
|
||||
to_api_key: mapping.to_api_key,
|
||||
to_api_method: mapping.to_api_method,
|
||||
to_api_body: mapping.to_api_body,
|
||||
mapping_order: mapping.mapping_order || index + 1,
|
||||
created_by: userId,
|
||||
},
|
||||
@@ -707,18 +722,39 @@ export class BatchService {
|
||||
const updateColumns = columns.filter(col => col !== primaryKeyColumn);
|
||||
const updateSet = updateColumns.map(col => `${col} = EXCLUDED.${col}`).join(', ');
|
||||
|
||||
// 먼저 해당 레코드가 존재하는지 확인
|
||||
const checkQuery = `SELECT COUNT(*) as count FROM ${tableName} WHERE ${primaryKeyColumn} = $1`;
|
||||
const existsResult = await prisma.$queryRawUnsafe(checkQuery, record[primaryKeyColumn]);
|
||||
const exists = (existsResult as any)[0]?.count > 0;
|
||||
|
||||
let query: string;
|
||||
if (updateSet) {
|
||||
// UPSERT: 중복 시 업데이트
|
||||
query = `INSERT INTO ${tableName} (${columns.join(', ')}) VALUES (${placeholders})
|
||||
ON CONFLICT (${primaryKeyColumn}) DO UPDATE SET ${updateSet}`;
|
||||
if (exists && updateSet) {
|
||||
// 기존 레코드가 있으면 UPDATE (값이 다른 경우에만)
|
||||
const whereConditions = updateColumns.map((col, index) =>
|
||||
`${col} IS DISTINCT FROM $${index + 2}`
|
||||
).join(' OR ');
|
||||
|
||||
query = `UPDATE ${tableName} SET ${updateSet.replace(/EXCLUDED\./g, '')}
|
||||
WHERE ${primaryKeyColumn} = $1 AND (${whereConditions})`;
|
||||
|
||||
// 파라미터: [primaryKeyValue, ...updateValues]
|
||||
const updateValues = [record[primaryKeyColumn], ...updateColumns.map(col => record[col])];
|
||||
const updateResult = await prisma.$executeRawUnsafe(query, ...updateValues);
|
||||
|
||||
if (updateResult > 0) {
|
||||
console.log(`[BatchService] 레코드 업데이트: ${primaryKeyColumn}=${record[primaryKeyColumn]}`);
|
||||
} else {
|
||||
console.log(`[BatchService] 레코드 변경사항 없음: ${primaryKeyColumn}=${record[primaryKeyColumn]}`);
|
||||
}
|
||||
} else if (!exists) {
|
||||
// 새 레코드 삽입
|
||||
query = `INSERT INTO ${tableName} (${columns.join(', ')}) VALUES (${placeholders})`;
|
||||
await prisma.$executeRawUnsafe(query, ...values);
|
||||
console.log(`[BatchService] 새 레코드 삽입: ${primaryKeyColumn}=${record[primaryKeyColumn]}`);
|
||||
} else {
|
||||
// Primary Key만 있는 경우 중복 시 무시
|
||||
query = `INSERT INTO ${tableName} (${columns.join(', ')}) VALUES (${placeholders})
|
||||
ON CONFLICT (${primaryKeyColumn}) DO NOTHING`;
|
||||
console.log(`[BatchService] 레코드 이미 존재 (변경사항 없음): ${primaryKeyColumn}=${record[primaryKeyColumn]}`);
|
||||
}
|
||||
|
||||
await prisma.$executeRawUnsafe(query, ...values);
|
||||
successCount++;
|
||||
} catch (error) {
|
||||
console.error(`레코드 UPSERT 실패:`, error);
|
||||
|
||||
@@ -37,6 +37,10 @@ export interface BatchMapping {
|
||||
from_api_method?: 'GET' | 'POST' | 'PUT' | 'DELETE'; // REST API 전용
|
||||
from_api_url?: string; // REST API 서버 URL
|
||||
from_api_key?: string; // REST API 키
|
||||
from_api_param_type?: 'url' | 'query'; // API 파라미터 타입
|
||||
from_api_param_name?: string; // API 파라미터명
|
||||
from_api_param_value?: string; // API 파라미터 값 또는 템플릿
|
||||
from_api_param_source?: 'static' | 'dynamic'; // 파라미터 소스 타입
|
||||
|
||||
// TO 정보
|
||||
to_connection_type: 'internal' | 'external' | 'restapi';
|
||||
@@ -92,6 +96,10 @@ export interface BatchMappingRequest {
|
||||
from_api_url?: string;
|
||||
from_api_key?: string;
|
||||
from_api_method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
|
||||
from_api_param_type?: 'url' | 'query'; // API 파라미터 타입
|
||||
from_api_param_name?: string; // API 파라미터명
|
||||
from_api_param_value?: string; // API 파라미터 값 또는 템플릿
|
||||
from_api_param_source?: 'static' | 'dynamic'; // 파라미터 소스 타입
|
||||
to_connection_type: 'internal' | 'external' | 'restapi';
|
||||
to_connection_id?: number;
|
||||
to_table_name: string;
|
||||
|
||||
Reference in New Issue
Block a user