2025-09-25 11:04:16 +09:00
|
|
|
// 배치관리 전용 컨트롤러 (기존 소스와 완전 분리)
|
|
|
|
|
// 작성일: 2024-12-24
|
|
|
|
|
|
|
|
|
|
import { Response } from "express";
|
|
|
|
|
import { AuthenticatedRequest } from "../types/auth";
|
|
|
|
|
import { BatchManagementService, BatchConnectionInfo, BatchTableInfo, BatchColumnInfo } from "../services/batchManagementService";
|
|
|
|
|
import { BatchService } from "../services/batchService";
|
|
|
|
|
import { BatchSchedulerService } from "../services/batchSchedulerService";
|
2025-09-26 17:29:20 +09:00
|
|
|
import { BatchExternalDbService } from "../services/batchExternalDbService";
|
2025-09-25 11:04:16 +09:00
|
|
|
import { CreateBatchConfigRequest, BatchConfig } from "../types/batchTypes";
|
|
|
|
|
|
|
|
|
|
export class BatchManagementController {
|
|
|
|
|
/**
|
|
|
|
|
* 사용 가능한 커넥션 목록 조회
|
|
|
|
|
*/
|
|
|
|
|
static async getAvailableConnections(req: AuthenticatedRequest, res: Response) {
|
|
|
|
|
try {
|
|
|
|
|
const result = await BatchManagementService.getAvailableConnections();
|
|
|
|
|
if (result.success) {
|
|
|
|
|
res.json(result);
|
|
|
|
|
} else {
|
|
|
|
|
res.status(500).json(result);
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("커넥션 목록 조회 오류:", error);
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
success: false,
|
|
|
|
|
message: "커넥션 목록 조회 중 오류가 발생했습니다.",
|
|
|
|
|
error: error instanceof Error ? error.message : "알 수 없는 오류"
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 특정 커넥션의 테이블 목록 조회
|
|
|
|
|
*/
|
|
|
|
|
static async getTablesFromConnection(req: AuthenticatedRequest, res: Response) {
|
|
|
|
|
try {
|
|
|
|
|
const { type, id } = req.params;
|
|
|
|
|
|
|
|
|
|
if (type !== 'internal' && type !== 'external') {
|
|
|
|
|
return res.status(400).json({
|
|
|
|
|
success: false,
|
|
|
|
|
message: "올바른 연결 타입을 지정해주세요. (internal 또는 external)"
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const connectionId = type === 'external' ? Number(id) : undefined;
|
|
|
|
|
const result = await BatchManagementService.getTablesFromConnection(type, connectionId);
|
|
|
|
|
|
|
|
|
|
if (result.success) {
|
|
|
|
|
return res.json(result);
|
|
|
|
|
} else {
|
|
|
|
|
return res.status(500).json(result);
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("테이블 목록 조회 오류:", error);
|
|
|
|
|
return res.status(500).json({
|
|
|
|
|
success: false,
|
|
|
|
|
message: "테이블 목록 조회 중 오류가 발생했습니다.",
|
|
|
|
|
error: error instanceof Error ? error.message : "알 수 없는 오류"
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 특정 테이블의 컬럼 정보 조회
|
|
|
|
|
*/
|
|
|
|
|
static async getTableColumns(req: AuthenticatedRequest, res: Response) {
|
|
|
|
|
try {
|
|
|
|
|
const { type, id, tableName } = req.params;
|
|
|
|
|
|
|
|
|
|
if (type !== 'internal' && type !== 'external') {
|
|
|
|
|
return res.status(400).json({
|
|
|
|
|
success: false,
|
|
|
|
|
message: "올바른 연결 타입을 지정해주세요. (internal 또는 external)"
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const connectionId = type === 'external' ? Number(id) : undefined;
|
|
|
|
|
const result = await BatchManagementService.getTableColumns(type, connectionId, tableName);
|
|
|
|
|
|
|
|
|
|
if (result.success) {
|
|
|
|
|
return res.json(result);
|
|
|
|
|
} else {
|
|
|
|
|
return res.status(500).json(result);
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("컬럼 정보 조회 오류:", error);
|
|
|
|
|
return res.status(500).json({
|
|
|
|
|
success: false,
|
|
|
|
|
message: "컬럼 정보 조회 중 오류가 발생했습니다.",
|
|
|
|
|
error: error instanceof Error ? error.message : "알 수 없는 오류"
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 배치 설정 생성
|
|
|
|
|
* POST /api/batch-management/batch-configs
|
|
|
|
|
*/
|
|
|
|
|
static async createBatchConfig(req: AuthenticatedRequest, res: Response) {
|
|
|
|
|
try {
|
|
|
|
|
const { batchName, description, cronSchedule, mappings, isActive } = req.body;
|
|
|
|
|
|
|
|
|
|
if (!batchName || !cronSchedule || !mappings || !Array.isArray(mappings)) {
|
|
|
|
|
return res.status(400).json({
|
|
|
|
|
success: false,
|
|
|
|
|
message: "필수 필드가 누락되었습니다. (batchName, cronSchedule, mappings)"
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const batchConfig = await BatchService.createBatchConfig({
|
|
|
|
|
batchName,
|
|
|
|
|
description,
|
|
|
|
|
cronSchedule,
|
|
|
|
|
mappings,
|
|
|
|
|
isActive: isActive !== undefined ? isActive : true
|
|
|
|
|
} as CreateBatchConfigRequest);
|
|
|
|
|
|
|
|
|
|
return res.status(201).json({
|
|
|
|
|
success: true,
|
|
|
|
|
data: batchConfig,
|
|
|
|
|
message: "배치 설정이 성공적으로 생성되었습니다."
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("배치 설정 생성 오류:", error);
|
|
|
|
|
return res.status(500).json({
|
|
|
|
|
success: false,
|
|
|
|
|
message: "배치 설정 생성에 실패했습니다.",
|
|
|
|
|
error: error instanceof Error ? error.message : "알 수 없는 오류"
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-26 17:29:20 +09:00
|
|
|
/**
|
|
|
|
|
* 특정 배치 설정 조회
|
|
|
|
|
* GET /api/batch-management/batch-configs/:id
|
|
|
|
|
*/
|
|
|
|
|
static async getBatchConfigById(req: AuthenticatedRequest, res: Response) {
|
|
|
|
|
try {
|
|
|
|
|
const { id } = req.params;
|
|
|
|
|
console.log("🔍 배치 설정 조회 요청:", id);
|
|
|
|
|
|
|
|
|
|
const result = await BatchService.getBatchConfigById(Number(id));
|
|
|
|
|
|
|
|
|
|
if (!result.success) {
|
|
|
|
|
return res.status(404).json({
|
|
|
|
|
success: false,
|
|
|
|
|
message: result.message || "배치 설정을 찾을 수 없습니다."
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log("📋 조회된 배치 설정:", result.data);
|
|
|
|
|
|
|
|
|
|
return res.json({
|
|
|
|
|
success: true,
|
|
|
|
|
data: result.data
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("❌ 배치 설정 조회 오류:", error);
|
|
|
|
|
return res.status(500).json({
|
|
|
|
|
success: false,
|
|
|
|
|
message: "배치 설정 조회에 실패했습니다.",
|
|
|
|
|
error: error instanceof Error ? error.message : "알 수 없는 오류"
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-25 11:04:16 +09:00
|
|
|
/**
|
|
|
|
|
* 배치 설정 목록 조회
|
|
|
|
|
* GET /api/batch-management/batch-configs
|
|
|
|
|
*/
|
|
|
|
|
static async getBatchConfigs(req: AuthenticatedRequest, res: Response) {
|
|
|
|
|
try {
|
|
|
|
|
const { page = 1, limit = 10, search, isActive } = req.query;
|
|
|
|
|
|
|
|
|
|
const filter = {
|
|
|
|
|
page: Number(page),
|
|
|
|
|
limit: Number(limit),
|
|
|
|
|
search: search as string,
|
|
|
|
|
is_active: isActive as string
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const result = await BatchService.getBatchConfigs(filter);
|
|
|
|
|
|
|
|
|
|
res.json({
|
|
|
|
|
success: true,
|
|
|
|
|
data: result.data,
|
|
|
|
|
pagination: result.pagination
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("배치 설정 목록 조회 오류:", error);
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
success: false,
|
|
|
|
|
message: "배치 설정 목록 조회에 실패했습니다.",
|
|
|
|
|
error: error instanceof Error ? error.message : "알 수 없는 오류"
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 배치 수동 실행
|
|
|
|
|
* POST /api/batch-management/batch-configs/:id/execute
|
|
|
|
|
*/
|
|
|
|
|
static async executeBatchConfig(req: AuthenticatedRequest, res: Response) {
|
|
|
|
|
try {
|
|
|
|
|
const { id } = req.params;
|
|
|
|
|
|
|
|
|
|
if (!id || isNaN(Number(id))) {
|
|
|
|
|
return res.status(400).json({
|
|
|
|
|
success: false,
|
|
|
|
|
message: "올바른 배치 설정 ID를 제공해주세요."
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 배치 설정 조회
|
|
|
|
|
const batchConfigResult = await BatchService.getBatchConfigById(Number(id));
|
|
|
|
|
if (!batchConfigResult.success || !batchConfigResult.data) {
|
|
|
|
|
return res.status(404).json({
|
|
|
|
|
success: false,
|
|
|
|
|
message: "배치 설정을 찾을 수 없습니다."
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const batchConfig = batchConfigResult.data as BatchConfig;
|
|
|
|
|
|
|
|
|
|
// 배치 실행 로직 (간단한 버전)
|
|
|
|
|
const startTime = new Date();
|
|
|
|
|
let totalRecords = 0;
|
|
|
|
|
let successRecords = 0;
|
|
|
|
|
let failedRecords = 0;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
console.log(`배치 실행 시작: ${batchConfig.batch_name} (ID: ${id})`);
|
|
|
|
|
|
|
|
|
|
// 실행 로그 생성
|
|
|
|
|
const executionLog = await BatchService.createExecutionLog({
|
|
|
|
|
batch_config_id: Number(id),
|
|
|
|
|
execution_status: 'RUNNING',
|
|
|
|
|
start_time: startTime,
|
|
|
|
|
total_records: 0,
|
|
|
|
|
success_records: 0,
|
|
|
|
|
failed_records: 0
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 실제 배치 실행 (매핑이 있는 경우)
|
|
|
|
|
if (batchConfig.batch_mappings && batchConfig.batch_mappings.length > 0) {
|
|
|
|
|
// 테이블별로 매핑을 그룹화
|
|
|
|
|
const tableGroups = new Map<string, typeof batchConfig.batch_mappings>();
|
|
|
|
|
|
|
|
|
|
for (const mapping of batchConfig.batch_mappings) {
|
|
|
|
|
const key = `${mapping.from_connection_type}:${mapping.from_connection_id || 'internal'}:${mapping.from_table_name}`;
|
|
|
|
|
if (!tableGroups.has(key)) {
|
|
|
|
|
tableGroups.set(key, []);
|
|
|
|
|
}
|
|
|
|
|
tableGroups.get(key)!.push(mapping);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 각 테이블 그룹별로 처리
|
|
|
|
|
for (const [tableKey, mappings] of tableGroups) {
|
|
|
|
|
try {
|
|
|
|
|
const firstMapping = mappings[0];
|
|
|
|
|
console.log(`테이블 처리 시작: ${tableKey} -> ${mappings.length}개 컬럼 매핑`);
|
|
|
|
|
|
2025-09-26 17:29:20 +09:00
|
|
|
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',
|
2025-09-29 13:48:59 +09:00
|
|
|
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
|
2025-09-26 17:29:20 +09:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-25 11:04:16 +09:00
|
|
|
totalRecords += fromData.length;
|
|
|
|
|
|
|
|
|
|
// 컬럼 매핑 적용하여 TO 테이블 형식으로 변환
|
|
|
|
|
const mappedData = fromData.map(row => {
|
|
|
|
|
const mappedRow: any = {};
|
|
|
|
|
for (const mapping of mappings) {
|
2025-09-26 17:29:20 +09:00
|
|
|
// 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];
|
|
|
|
|
}
|
2025-09-25 11:04:16 +09:00
|
|
|
}
|
|
|
|
|
return mappedRow;
|
|
|
|
|
});
|
|
|
|
|
|
2025-09-26 17:29:20 +09:00
|
|
|
// 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
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-25 11:04:16 +09:00
|
|
|
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, {
|
|
|
|
|
execution_status: 'SUCCESS',
|
|
|
|
|
end_time: new Date(),
|
|
|
|
|
duration_ms: Date.now() - startTime.getTime(),
|
|
|
|
|
total_records: totalRecords,
|
|
|
|
|
success_records: successRecords,
|
|
|
|
|
failed_records: failedRecords
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return res.json({
|
|
|
|
|
success: true,
|
|
|
|
|
message: "배치가 성공적으로 실행되었습니다.",
|
|
|
|
|
data: {
|
|
|
|
|
batchId: id,
|
|
|
|
|
totalRecords,
|
|
|
|
|
successRecords,
|
|
|
|
|
failedRecords,
|
|
|
|
|
duration: Date.now() - startTime.getTime()
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error(`배치 실행 실패: ${batchConfig.batch_name}`, error);
|
|
|
|
|
|
|
|
|
|
return res.status(500).json({
|
|
|
|
|
success: false,
|
|
|
|
|
message: "배치 실행에 실패했습니다.",
|
|
|
|
|
error: error instanceof Error ? error.message : "알 수 없는 오류"
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("배치 실행 오류:", error);
|
|
|
|
|
return res.status(500).json({
|
|
|
|
|
success: false,
|
|
|
|
|
message: "배치 실행 중 오류가 발생했습니다.",
|
|
|
|
|
error: error instanceof Error ? error.message : "알 수 없는 오류"
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 배치 설정 업데이트
|
|
|
|
|
* PUT /api/batch-management/batch-configs/:id
|
|
|
|
|
*/
|
|
|
|
|
static async updateBatchConfig(req: AuthenticatedRequest, res: Response) {
|
|
|
|
|
try {
|
|
|
|
|
const { id } = req.params;
|
|
|
|
|
const updateData = req.body;
|
|
|
|
|
|
|
|
|
|
if (!id || isNaN(Number(id))) {
|
|
|
|
|
return res.status(400).json({
|
|
|
|
|
success: false,
|
|
|
|
|
message: "올바른 배치 설정 ID를 제공해주세요."
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const batchConfig = await BatchService.updateBatchConfig(Number(id), updateData);
|
|
|
|
|
|
|
|
|
|
// 스케줄러에서 배치 스케줄 업데이트
|
|
|
|
|
await BatchSchedulerService.updateBatchSchedule(Number(id));
|
|
|
|
|
|
|
|
|
|
return res.json({
|
|
|
|
|
success: true,
|
|
|
|
|
data: batchConfig,
|
|
|
|
|
message: "배치 설정이 성공적으로 업데이트되었습니다."
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("배치 설정 업데이트 오류:", error);
|
|
|
|
|
return res.status(500).json({
|
|
|
|
|
success: false,
|
|
|
|
|
message: "배치 설정 업데이트에 실패했습니다.",
|
|
|
|
|
error: error instanceof Error ? error.message : "알 수 없는 오류"
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-09-26 17:29:20 +09:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* REST API 데이터 미리보기
|
|
|
|
|
*/
|
|
|
|
|
static async previewRestApiData(req: AuthenticatedRequest, res: Response) {
|
|
|
|
|
try {
|
2025-09-29 13:48:59 +09:00
|
|
|
const {
|
|
|
|
|
apiUrl,
|
|
|
|
|
apiKey,
|
|
|
|
|
endpoint,
|
|
|
|
|
method = 'GET',
|
|
|
|
|
paramType,
|
|
|
|
|
paramName,
|
|
|
|
|
paramValue,
|
|
|
|
|
paramSource
|
|
|
|
|
} = req.body;
|
2025-09-26 17:29:20 +09:00
|
|
|
|
|
|
|
|
if (!apiUrl || !apiKey || !endpoint) {
|
|
|
|
|
return res.status(400).json({
|
|
|
|
|
success: false,
|
|
|
|
|
message: "API URL, API Key, 엔드포인트는 필수입니다."
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-29 13:48:59 +09:00
|
|
|
console.log("🔍 REST API 미리보기 요청:", {
|
|
|
|
|
apiUrl,
|
|
|
|
|
endpoint,
|
|
|
|
|
paramType,
|
|
|
|
|
paramName,
|
|
|
|
|
paramValue,
|
|
|
|
|
paramSource
|
|
|
|
|
});
|
|
|
|
|
|
2025-09-26 17:29:20 +09:00
|
|
|
// RestApiConnector 사용하여 데이터 조회
|
|
|
|
|
const { RestApiConnector } = await import('../database/RestApiConnector');
|
|
|
|
|
|
|
|
|
|
const connector = new RestApiConnector({
|
|
|
|
|
baseUrl: apiUrl,
|
|
|
|
|
apiKey: apiKey,
|
|
|
|
|
timeout: 30000
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 연결 테스트
|
|
|
|
|
await connector.connect();
|
|
|
|
|
|
2025-09-29 13:48:59 +09:00
|
|
|
// 파라미터가 있는 경우 엔드포인트 수정
|
|
|
|
|
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);
|
|
|
|
|
|
2025-09-26 17:29:20 +09:00
|
|
|
// 데이터 조회 (최대 5개만) - GET 메서드만 지원
|
2025-09-29 13:48:59 +09:00
|
|
|
const result = await connector.executeQuery(finalEndpoint, method);
|
2025-09-26 17:29:20 +09:00
|
|
|
console.log(`[previewRestApiData] executeQuery 결과:`, {
|
|
|
|
|
rowCount: result.rowCount,
|
|
|
|
|
rowsLength: result.rows ? result.rows.length : 'undefined',
|
|
|
|
|
firstRow: result.rows && result.rows.length > 0 ? result.rows[0] : 'no data'
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const data = result.rows.slice(0, 5); // 최대 5개 샘플만
|
|
|
|
|
console.log(`[previewRestApiData] 슬라이스된 데이터:`, data);
|
|
|
|
|
|
|
|
|
|
if (data.length > 0) {
|
|
|
|
|
// 첫 번째 객체에서 필드명 추출
|
|
|
|
|
const fields = Object.keys(data[0]);
|
|
|
|
|
console.log(`[previewRestApiData] 추출된 필드:`, fields);
|
|
|
|
|
|
|
|
|
|
return res.json({
|
|
|
|
|
success: true,
|
|
|
|
|
data: {
|
|
|
|
|
fields: fields,
|
|
|
|
|
samples: data,
|
|
|
|
|
totalCount: result.rowCount || data.length
|
|
|
|
|
},
|
|
|
|
|
message: `${fields.length}개 필드, ${result.rowCount || data.length}개 레코드를 조회했습니다.`
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
return res.json({
|
|
|
|
|
success: true,
|
|
|
|
|
data: {
|
|
|
|
|
fields: [],
|
|
|
|
|
samples: [],
|
|
|
|
|
totalCount: 0
|
|
|
|
|
},
|
|
|
|
|
message: "API에서 데이터를 가져올 수 없습니다."
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("REST API 미리보기 오류:", error);
|
|
|
|
|
return res.status(500).json({
|
|
|
|
|
success: false,
|
|
|
|
|
message: "REST API 데이터 미리보기 중 오류가 발생했습니다.",
|
|
|
|
|
error: error instanceof Error ? error.message : "알 수 없는 오류"
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* REST API 배치 설정 저장
|
|
|
|
|
*/
|
|
|
|
|
static async saveRestApiBatch(req: AuthenticatedRequest, res: Response) {
|
|
|
|
|
try {
|
|
|
|
|
const {
|
|
|
|
|
batchName,
|
|
|
|
|
batchType,
|
|
|
|
|
cronSchedule,
|
|
|
|
|
description,
|
|
|
|
|
apiMappings
|
|
|
|
|
} = req.body;
|
|
|
|
|
|
|
|
|
|
if (!batchName || !batchType || !cronSchedule || !apiMappings || apiMappings.length === 0) {
|
|
|
|
|
return res.status(400).json({
|
|
|
|
|
success: false,
|
|
|
|
|
message: "필수 필드가 누락되었습니다."
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log("REST API 배치 저장 요청:", {
|
|
|
|
|
batchName,
|
|
|
|
|
batchType,
|
|
|
|
|
cronSchedule,
|
|
|
|
|
description,
|
|
|
|
|
apiMappings
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// BatchService를 사용하여 배치 설정 저장
|
|
|
|
|
const batchConfig: CreateBatchConfigRequest = {
|
|
|
|
|
batchName: batchName,
|
|
|
|
|
description: description || '',
|
|
|
|
|
cronSchedule: cronSchedule,
|
|
|
|
|
mappings: apiMappings
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const result = await BatchService.createBatchConfig(batchConfig);
|
|
|
|
|
|
|
|
|
|
if (result.success && result.data) {
|
|
|
|
|
// 스케줄러에 자동 등록 ✅
|
|
|
|
|
try {
|
|
|
|
|
await BatchSchedulerService.scheduleBatchConfig(result.data);
|
|
|
|
|
console.log(`✅ 새로운 배치가 스케줄러에 등록되었습니다: ${batchName} (ID: ${result.data.id})`);
|
|
|
|
|
} catch (schedulerError) {
|
|
|
|
|
console.error(`❌ 스케줄러 등록 실패: ${batchName}`, schedulerError);
|
|
|
|
|
// 스케줄러 등록 실패해도 배치 저장은 성공으로 처리
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return res.json({
|
|
|
|
|
success: true,
|
|
|
|
|
message: "REST API 배치가 성공적으로 저장되었습니다.",
|
|
|
|
|
data: result.data
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
return res.status(500).json({
|
|
|
|
|
success: false,
|
|
|
|
|
message: result.message || "배치 저장에 실패했습니다."
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("REST API 배치 저장 오류:", error);
|
|
|
|
|
return res.status(500).json({
|
|
|
|
|
success: false,
|
|
|
|
|
message: "배치 저장 중 오류가 발생했습니다."
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-09-25 11:04:16 +09:00
|
|
|
}
|