rest api get 파라미터 설정 개발중

This commit is contained in:
hjjeong 2025-09-29 16:55:37 +09:00
parent 9dfd0cb40f
commit 1be8771e01
5 changed files with 162 additions and 228 deletions

View File

@ -192,9 +192,9 @@ export class BatchController {
mappings mappings
} as CreateBatchConfigRequest); } as CreateBatchConfigRequest);
// 생성된 배치가 활성화 상태라면 스케줄러에 등록 // 생성된 배치가 활성화 상태라면 스케줄러에 등록 (즉시 실행 비활성화)
if (batchConfig.data && batchConfig.data.is_active === 'Y' && batchConfig.data.id) { if (batchConfig.data && batchConfig.data.is_active === 'Y' && batchConfig.data.id) {
await BatchSchedulerService.updateBatchSchedule(batchConfig.data.id); await BatchSchedulerService.updateBatchSchedule(batchConfig.data.id, false);
} }
return res.status(201).json({ return res.status(201).json({
@ -242,8 +242,8 @@ export class BatchController {
}); });
} }
// 스케줄러에서 배치 스케줄 업데이트 (활성화 시 즉시 스케줄 등록) // 스케줄러에서 배치 스케줄 업데이트 (즉시 실행 비활성화)
await BatchSchedulerService.updateBatchSchedule(Number(id)); await BatchSchedulerService.updateBatchSchedule(Number(id), false);
return res.json({ return res.json({
success: true, success: true,

View File

@ -224,18 +224,15 @@ export class BatchManagementController {
} }
const batchConfig = batchConfigResult.data as BatchConfig; const batchConfig = batchConfigResult.data as BatchConfig;
// 배치 실행 로직 (간단한 버전)
const startTime = new Date(); const startTime = new Date();
let totalRecords = 0;
let successRecords = 0; console.log(`배치 수동 실행 시작: ${batchConfig.batch_name} (ID: ${id})`);
let failedRecords = 0;
let executionLog: any = null;
try { try {
console.log(`배치 실행 시작: ${batchConfig.batch_name} (ID: ${id})`);
// 실행 로그 생성 // 실행 로그 생성
const executionLog = await BatchService.createExecutionLog({ executionLog = await BatchService.createExecutionLog({
batch_config_id: Number(id), batch_config_id: Number(id),
execution_status: 'RUNNING', execution_status: 'RUNNING',
start_time: startTime, start_time: startTime,
@ -244,205 +241,74 @@ export class BatchManagementController {
failed_records: 0 failed_records: 0
}); });
// 실제 배치 실행 (매핑이 있는 경우) // BatchSchedulerService의 executeBatchConfig 메서드 사용 (중복 로직 제거)
if (batchConfig.batch_mappings && batchConfig.batch_mappings.length > 0) { const { BatchSchedulerService } = await import('../services/batchSchedulerService');
// 테이블별로 매핑을 그룹화 const result = await BatchSchedulerService.executeBatchConfig(batchConfig);
const tableGroups = new Map<string, typeof batchConfig.batch_mappings>();
for (const mapping of batchConfig.batch_mappings) { // result가 undefined인 경우 처리
const key = `${mapping.from_connection_type}:${mapping.from_connection_id || 'internal'}:${mapping.from_table_name}`; if (!result) {
if (!tableGroups.has(key)) { throw new Error('배치 실행 결과를 받을 수 없습니다.');
tableGroups.set(key, []);
}
tableGroups.get(key)!.push(mapping);
} }
// 각 테이블 그룹별로 처리 const endTime = new Date();
for (const [tableKey, mappings] of tableGroups) { const duration = endTime.getTime() - startTime.getTime();
try {
const firstMapping = mappings[0];
console.log(`테이블 처리 시작: ${tableKey} -> ${mappings.length}개 컬럼 매핑`);
let fromData: any[] = [];
// FROM 데이터 조회 (DB 또는 REST API)
if (firstMapping.from_connection_type === 'restapi') {
// REST API에서 데이터 조회
console.log(`REST API에서 데이터 조회: ${firstMapping.from_api_url}${firstMapping.from_table_name}`);
console.log(`API 설정:`, {
url: firstMapping.from_api_url,
key: firstMapping.from_api_key ? '***' : 'null',
method: firstMapping.from_api_method,
endpoint: firstMapping.from_table_name
});
try {
const apiResult = await BatchExternalDbService.getDataFromRestApi(
firstMapping.from_api_url!,
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),
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 조회 결과:`, {
success: apiResult.success,
dataCount: apiResult.data ? apiResult.data.length : 0,
message: apiResult.message
});
if (apiResult.success && apiResult.data) {
fromData = apiResult.data;
} else {
throw new Error(`REST API 데이터 조회 실패: ${apiResult.message}`);
}
} catch (error) {
console.error(`REST API 조회 오류:`, error);
throw error;
}
} else {
// DB에서 데이터 조회
const fromColumns = mappings.map(m => m.from_column_name);
fromData = await BatchService.getDataFromTableWithColumns(
firstMapping.from_table_name,
fromColumns,
firstMapping.from_connection_type as 'internal' | 'external',
firstMapping.from_connection_id || undefined
);
}
totalRecords += fromData.length;
// 컬럼 매핑 적용하여 TO 테이블 형식으로 변환
const mappedData = fromData.map(row => {
const mappedRow: any = {};
for (const mapping of mappings) {
// DB → REST API 배치인지 확인
if (firstMapping.to_connection_type === 'restapi' && mapping.to_api_body) {
// DB → REST API: 원본 컬럼명을 키로 사용 (템플릿 처리용)
mappedRow[mapping.from_column_name] = row[mapping.from_column_name];
} else {
// 기존 로직: to_column_name을 키로 사용
mappedRow[mapping.to_column_name] = row[mapping.from_column_name];
}
}
return mappedRow;
});
// TO 테이블에 데이터 삽입 (DB 또는 REST API)
let insertResult: { successCount: number; failedCount: number };
if (firstMapping.to_connection_type === 'restapi') {
// REST API로 데이터 전송
console.log(`REST API로 데이터 전송: ${firstMapping.to_api_url}${firstMapping.to_table_name}`);
// DB → REST API 배치인지 확인 (to_api_body가 있으면 템플릿 기반)
const hasTemplate = mappings.some(m => m.to_api_body);
if (hasTemplate) {
// 템플릿 기반 REST API 전송 (DB → REST API 배치)
const templateBody = firstMapping.to_api_body || '{}';
console.log(`템플릿 기반 REST API 전송, Request Body 템플릿: ${templateBody}`);
// URL 경로 컬럼 찾기 (PUT/DELETE용)
const urlPathColumn = mappings.find(m => m.to_column_name === 'URL_PATH_PARAM')?.from_column_name;
const apiResult = await BatchExternalDbService.sendDataToRestApiWithTemplate(
firstMapping.to_api_url!,
firstMapping.to_api_key!,
firstMapping.to_table_name,
firstMapping.to_api_method as 'POST' | 'PUT' | 'DELETE' || 'POST',
templateBody,
mappedData,
urlPathColumn
);
if (apiResult.success && apiResult.data) {
insertResult = apiResult.data;
} else {
throw new Error(`템플릿 기반 REST API 데이터 전송 실패: ${apiResult.message}`);
}
} else {
// 기존 REST API 전송 (REST API → DB 배치)
const apiResult = await BatchExternalDbService.sendDataToRestApi(
firstMapping.to_api_url!,
firstMapping.to_api_key!,
firstMapping.to_table_name,
firstMapping.to_api_method as 'POST' | 'PUT' || 'POST',
mappedData
);
if (apiResult.success && apiResult.data) {
insertResult = apiResult.data;
} else {
throw new Error(`REST API 데이터 전송 실패: ${apiResult.message}`);
}
}
} else {
// DB에 데이터 삽입
insertResult = await BatchService.insertDataToTable(
firstMapping.to_table_name,
mappedData,
firstMapping.to_connection_type as 'internal' | 'external',
firstMapping.to_connection_id || undefined
);
}
successRecords += insertResult.successCount;
failedRecords += insertResult.failedCount;
console.log(`테이블 처리 완료: ${insertResult.successCount}개 성공, ${insertResult.failedCount}개 실패`);
} catch (error) {
console.error(`테이블 처리 실패: ${tableKey}`, error);
failedRecords += 1;
}
}
} else {
console.log("매핑이 없어서 데이터 처리를 건너뜁니다.");
}
// 실행 로그 업데이트 (성공) // 실행 로그 업데이트 (성공)
await BatchService.updateExecutionLog(executionLog.id, { await BatchService.updateExecutionLog(executionLog.id, {
execution_status: 'SUCCESS', execution_status: 'SUCCESS',
end_time: new Date(), end_time: endTime,
duration_ms: Date.now() - startTime.getTime(), duration_ms: duration,
total_records: totalRecords, total_records: result.totalRecords,
success_records: successRecords, success_records: result.successRecords,
failed_records: failedRecords failed_records: result.failedRecords
}); });
return res.json({ return res.json({
success: true, success: true,
message: "배치가 성공적으로 실행되었습니다.",
data: { data: {
batchId: id, batchName: batchConfig.batch_name,
totalRecords, totalRecords: result.totalRecords,
successRecords, successRecords: result.successRecords,
failedRecords, failedRecords: result.failedRecords,
duration: Date.now() - startTime.getTime() executionTime: duration
} },
message: "배치가 성공적으로 실행되었습니다."
}); });
} catch (error) {
console.error(`배치 실행 실패: ${batchConfig.batch_name}`, error); } catch (batchError) {
console.error(`배치 실행 실패: ${batchConfig.batch_name}`, batchError);
// 실행 로그 업데이트 (실패) - executionLog가 생성되었을 경우에만
try {
const endTime = new Date();
const duration = endTime.getTime() - startTime.getTime();
// executionLog가 정의되어 있는지 확인
if (typeof executionLog !== 'undefined') {
await BatchService.updateExecutionLog(executionLog.id, {
execution_status: 'FAILED',
end_time: endTime,
duration_ms: duration,
error_message: batchError instanceof Error ? batchError.message : "알 수 없는 오류"
});
}
} catch (logError) {
console.error('실행 로그 업데이트 실패:', logError);
}
return res.status(500).json({ return res.status(500).json({
success: false, success: false,
message: "배치 실행에 실패했습니다.", message: "배치 실행에 실패했습니다.",
error: error instanceof Error ? error.message : "알 수 없는 오류" error: batchError instanceof Error ? batchError.message : "알 수 없는 오류"
}); });
} }
} catch (error) { } catch (error) {
console.error("배치 실행 오류:", error); console.error(`배치 실행 오류 (ID: ${req.params.id}):`, error);
return res.status(500).json({ return res.status(500).json({
success: false, success: false,
message: "배치 실행 중 오류가 발생했습니다.", message: "배치 실행 중 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "알 수 없는 오류" error: error instanceof Error ? error.message : "Unknown error"
}); });
} }
} }
@ -465,8 +331,8 @@ export class BatchManagementController {
const batchConfig = await BatchService.updateBatchConfig(Number(id), updateData); const batchConfig = await BatchService.updateBatchConfig(Number(id), updateData);
// 스케줄러에서 배치 스케줄 업데이트 // 스케줄러에서 배치 스케줄 업데이트 (즉시 실행 비활성화)
await BatchSchedulerService.updateBatchSchedule(Number(id)); await BatchSchedulerService.updateBatchSchedule(Number(id), false);
return res.json({ return res.json({
success: true, success: true,

View File

@ -27,7 +27,7 @@ export class RestApiConnector implements DatabaseConnector {
timeout: config.timeout || 30000, timeout: config.timeout || 30000,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'X-API-Key': config.apiKey, 'Authorization': `Bearer ${config.apiKey}`,
'Accept': 'application/json' 'Accept': 'application/json'
} }
}); });

View File

@ -10,19 +10,18 @@ import { logger } from '../utils/logger';
export class BatchSchedulerService { export class BatchSchedulerService {
private static scheduledTasks: Map<number, cron.ScheduledTask> = new Map(); private static scheduledTasks: Map<number, cron.ScheduledTask> = new Map();
private static isInitialized = false; private static isInitialized = false;
private static executingBatches: Set<number> = new Set(); // 실행 중인 배치 추적
/** /**
* *
*/ */
static async initialize() { static async initialize() {
if (this.isInitialized) {
logger.info('배치 스케줄러가 이미 초기화되었습니다.');
return;
}
try { try {
logger.info('배치 스케줄러 초기화 시작...'); logger.info('배치 스케줄러 초기화 시작...');
// 기존 모든 스케줄 정리 (중복 방지)
this.clearAllSchedules();
// 활성화된 배치 설정들을 로드하여 스케줄 등록 // 활성화된 배치 설정들을 로드하여 스케줄 등록
await this.loadActiveBatchConfigs(); await this.loadActiveBatchConfigs();
@ -34,6 +33,27 @@ export class BatchSchedulerService {
} }
} }
/**
*
*/
private static clearAllSchedules() {
logger.info(`기존 스케줄 ${this.scheduledTasks.size}개 정리 중...`);
for (const [id, task] of this.scheduledTasks) {
try {
task.stop();
task.destroy();
logger.info(`스케줄 정리 완료: ID ${id}`);
} catch (error) {
logger.error(`스케줄 정리 실패: ID ${id}`, error);
}
}
this.scheduledTasks.clear();
this.isInitialized = false;
logger.info('모든 스케줄 정리 완료');
}
/** /**
* *
*/ */
@ -80,8 +100,23 @@ export class BatchSchedulerService {
// 새로운 스케줄 등록 // 새로운 스케줄 등록
const task = cron.schedule(cron_schedule, async () => { const task = cron.schedule(cron_schedule, async () => {
// 중복 실행 방지 체크
if (this.executingBatches.has(id)) {
logger.warn(`⚠️ 배치가 이미 실행 중입니다. 건너뜀: ${batch_name} (ID: ${id})`);
return;
}
logger.info(`🔄 스케줄 배치 실행 시작: ${batch_name} (ID: ${id})`); logger.info(`🔄 스케줄 배치 실행 시작: ${batch_name} (ID: ${id})`);
// 실행 중 플래그 설정
this.executingBatches.add(id);
try {
await this.executeBatchConfig(config); await this.executeBatchConfig(config);
} finally {
// 실행 완료 후 플래그 제거
this.executingBatches.delete(id);
}
}); });
// 스케줄 시작 (기본적으로 시작되지만 명시적으로 호출) // 스케줄 시작 (기본적으로 시작되지만 명시적으로 호출)
@ -149,7 +184,7 @@ export class BatchSchedulerService {
/** /**
* *
*/ */
private static async executeBatchConfig(config: any) { static async executeBatchConfig(config: any) {
const startTime = new Date(); const startTime = new Date();
let executionLog: any = null; let executionLog: any = null;
@ -168,7 +203,11 @@ export class BatchSchedulerService {
if (!executionLogResponse.success || !executionLogResponse.data) { if (!executionLogResponse.success || !executionLogResponse.data) {
logger.error(`배치 실행 로그 생성 실패: ${config.batch_name}`, executionLogResponse.message); logger.error(`배치 실행 로그 생성 실패: ${config.batch_name}`, executionLogResponse.message);
return; return {
totalRecords: 0,
successRecords: 0,
failedRecords: 1
};
} }
executionLog = executionLogResponse.data; executionLog = executionLogResponse.data;
@ -187,6 +226,10 @@ export class BatchSchedulerService {
}); });
logger.info(`배치 실행 완료: ${config.batch_name} (처리된 레코드: ${result.totalRecords})`); logger.info(`배치 실행 완료: ${config.batch_name} (처리된 레코드: ${result.totalRecords})`);
// 성공 결과 반환
return result;
} catch (error) { } catch (error) {
logger.error(`배치 실행 실패: ${config.batch_name}`, error); logger.error(`배치 실행 실패: ${config.batch_name}`, error);
@ -200,6 +243,13 @@ export class BatchSchedulerService {
error_details: error instanceof Error ? error.stack : String(error) error_details: error instanceof Error ? error.stack : String(error)
}); });
} }
// 실패 시에도 결과 반환
return {
totalRecords: 0,
successRecords: 0,
failedRecords: 1
};
} }
} }

View File

@ -722,39 +722,57 @@ export class BatchService {
const updateColumns = columns.filter(col => col !== primaryKeyColumn); const updateColumns = columns.filter(col => col !== primaryKeyColumn);
const updateSet = updateColumns.map(col => `${col} = EXCLUDED.${col}`).join(', '); const updateSet = updateColumns.map(col => `${col} = EXCLUDED.${col}`).join(', ');
// 트랜잭션 내에서 처리하여 연결 관리 최적화
const result = await prisma.$transaction(async (tx) => {
// 먼저 해당 레코드가 존재하는지 확인 // 먼저 해당 레코드가 존재하는지 확인
const checkQuery = `SELECT COUNT(*) as count FROM ${tableName} WHERE ${primaryKeyColumn} = $1`; const checkQuery = `SELECT COUNT(*) as count FROM ${tableName} WHERE ${primaryKeyColumn} = $1`;
const existsResult = await prisma.$queryRawUnsafe(checkQuery, record[primaryKeyColumn]); const existsResult = await tx.$queryRawUnsafe(checkQuery, record[primaryKeyColumn]);
const exists = (existsResult as any)[0]?.count > 0; const exists = (existsResult as any)[0]?.count > 0;
let query: string; let operationResult = 'no_change';
if (exists && updateSet) { if (exists && updateSet) {
// 기존 레코드가 있으면 UPDATE (값이 다른 경우에만) // 기존 레코드가 있으면 UPDATE (값이 다른 경우에만)
const whereConditions = updateColumns.map((col, index) => const whereConditions = updateColumns.map((col, index) => {
`${col} IS DISTINCT FROM $${index + 2}` // 날짜/시간 컬럼에 대한 타입 캐스팅 처리
).join(' OR '); if (col.toLowerCase().includes('date') ||
col.toLowerCase().includes('time') ||
col.toLowerCase().includes('created') ||
col.toLowerCase().includes('updated') ||
col.toLowerCase().includes('reg')) {
return `${col} IS DISTINCT FROM $${index + 2}::timestamp`;
}
return `${col} IS DISTINCT FROM $${index + 2}`;
}).join(' OR ');
query = `UPDATE ${tableName} SET ${updateSet.replace(/EXCLUDED\./g, '')} const query = `UPDATE ${tableName} SET ${updateSet.replace(/EXCLUDED\./g, '')}
WHERE ${primaryKeyColumn} = $1 AND (${whereConditions})`; WHERE ${primaryKeyColumn} = $1 AND (${whereConditions})`;
// 파라미터: [primaryKeyValue, ...updateValues] // 파라미터: [primaryKeyValue, ...updateValues]
const updateValues = [record[primaryKeyColumn], ...updateColumns.map(col => record[col])]; const updateValues = [record[primaryKeyColumn], ...updateColumns.map(col => record[col])];
const updateResult = await prisma.$executeRawUnsafe(query, ...updateValues); const updateResult = await tx.$executeRawUnsafe(query, ...updateValues);
if (updateResult > 0) { if (updateResult > 0) {
console.log(`[BatchService] 레코드 업데이트: ${primaryKeyColumn}=${record[primaryKeyColumn]}`); console.log(`[BatchService] 레코드 업데이트: ${primaryKeyColumn}=${record[primaryKeyColumn]}`);
operationResult = 'updated';
} else { } else {
console.log(`[BatchService] 레코드 변경사항 없음: ${primaryKeyColumn}=${record[primaryKeyColumn]}`); console.log(`[BatchService] 레코드 변경사항 없음: ${primaryKeyColumn}=${record[primaryKeyColumn]}`);
operationResult = 'no_change';
} }
} else if (!exists) { } else if (!exists) {
// 새 레코드 삽입 // 새 레코드 삽입
query = `INSERT INTO ${tableName} (${columns.join(', ')}) VALUES (${placeholders})`; const query = `INSERT INTO ${tableName} (${columns.join(', ')}) VALUES (${placeholders})`;
await prisma.$executeRawUnsafe(query, ...values); await tx.$executeRawUnsafe(query, ...values);
console.log(`[BatchService] 새 레코드 삽입: ${primaryKeyColumn}=${record[primaryKeyColumn]}`); console.log(`[BatchService] 새 레코드 삽입: ${primaryKeyColumn}=${record[primaryKeyColumn]}`);
operationResult = 'inserted';
} else { } else {
console.log(`[BatchService] 레코드 이미 존재 (변경사항 없음): ${primaryKeyColumn}=${record[primaryKeyColumn]}`); console.log(`[BatchService] 레코드 이미 존재 (변경사항 없음): ${primaryKeyColumn}=${record[primaryKeyColumn]}`);
operationResult = 'no_change';
} }
return operationResult;
});
successCount++; successCount++;
} catch (error) { } catch (error) {
console.error(`레코드 UPSERT 실패:`, error); console.error(`레코드 UPSERT 실패:`, error);