Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management

This commit is contained in:
kjs
2025-09-29 13:37:35 +09:00
40 changed files with 10345 additions and 1914 deletions

View File

@@ -1,294 +1,281 @@
// 배치 관리 컨트롤러
// 작성일: 2024-12-23
// 배치관리 컨트롤러
// 작성일: 2024-12-24
import { Request, Response } from 'express';
import { BatchService } from '../services/batchService';
import { BatchJob, BatchJobFilter } from '../types/batchManagement';
import { AuthenticatedRequest } from '../middleware/authMiddleware';
import { Request, Response } from "express";
import { BatchService } from "../services/batchService";
import { BatchConfigFilter, CreateBatchConfigRequest, UpdateBatchConfigRequest } from "../types/batchTypes";
export interface AuthenticatedRequest extends Request {
user?: {
userId: string;
username: string;
companyCode: string;
};
}
export class BatchController {
/**
* 배치 작업 목록 조회
* 배치 설정 목록 조회
* GET /api/batch-configs
*/
static async getBatchJobs(req: AuthenticatedRequest, res: Response): Promise<void> {
static async getBatchConfigs(req: AuthenticatedRequest, res: Response) {
try {
const filter: BatchJobFilter = {
job_name: req.query.job_name as string,
job_type: req.query.job_type as string,
is_active: req.query.is_active as string,
company_code: req.user?.companyCode || '*',
search: req.query.search as string,
const { page = 1, limit = 10, search, isActive } = req.query;
const filter: BatchConfigFilter = {
page: Number(page),
limit: Number(limit),
search: search as string,
is_active: isActive as string
};
const jobs = await BatchService.getBatchJobs(filter);
res.status(200).json({
const result = await BatchService.getBatchConfigs(filter);
res.json({
success: true,
data: jobs,
message: '배치 작업 목록을 조회했습니다.',
data: result.data,
pagination: result.pagination
});
} catch (error) {
console.error('배치 작업 목록 조회 오류:', error);
console.error("배치 설정 목록 조회 오류:", error);
res.status(500).json({
success: false,
message: error instanceof Error ? error.message : '배치 작업 목록 조회에 실패했습니다.',
message: "배치 설정 목록 조회에 실패했습니다."
});
}
}
/**
* 배치 작업 상세 조회
* 사용 가능한 커넥션 목록 조회
* GET /api/batch-configs/connections
*/
static async getBatchJobById(req: AuthenticatedRequest, res: Response): Promise<void> {
static async getAvailableConnections(req: AuthenticatedRequest, res: Response) {
try {
const id = parseInt(req.params.id);
if (isNaN(id)) {
res.status(400).json({
const result = await BatchService.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: "커넥션 목록 조회에 실패했습니다."
});
}
}
/**
* 테이블 목록 조회 (내부/외부 DB)
* GET /api/batch-configs/connections/:type/tables
* GET /api/batch-configs/connections/:type/:id/tables
*/
static async getTablesFromConnection(req: AuthenticatedRequest, res: Response) {
try {
const { type, id } = req.params;
if (!type || (type !== 'internal' && type !== 'external')) {
return res.status(400).json({
success: false,
message: '유효하지 않은 ID입니다.',
message: "올바른 연결 타입을 지정해주세요. (internal 또는 external)"
});
return;
}
const job = await BatchService.getBatchJobById(id);
if (!job) {
res.status(404).json({
const connectionId = type === 'external' ? Number(id) : undefined;
const result = await BatchService.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: "테이블 목록 조회에 실패했습니다."
});
}
}
/**
* 테이블 컬럼 정보 조회 (내부/외부 DB)
* GET /api/batch-configs/connections/:type/tables/:tableName/columns
* GET /api/batch-configs/connections/:type/:id/tables/:tableName/columns
*/
static async getTableColumns(req: AuthenticatedRequest, res: Response) {
try {
const { type, id, tableName } = req.params;
if (!type || !tableName) {
return res.status(400).json({
success: false,
message: '배치 작업을 찾을 수 없습니다.',
message: "연결 타입과 테이블명을 모두 지정해주세요."
});
return;
}
res.status(200).json({
success: true,
data: job,
message: '배치 작업을 조회했습니다.',
});
} catch (error) {
console.error('배치 작업 조회 오류:', error);
res.status(500).json({
success: false,
message: error instanceof Error ? error.message : '배치 작업 조회에 실패했습니다.',
});
}
}
/**
* 배치 작업 생성
*/
static async createBatchJob(req: AuthenticatedRequest, res: Response): Promise<void> {
try {
const data: BatchJob = {
...req.body,
company_code: req.user?.companyCode || '*',
created_by: req.user?.userId,
};
// 필수 필드 검증
if (!data.job_name || !data.job_type) {
res.status(400).json({
if (type !== 'internal' && type !== 'external') {
return res.status(400).json({
success: false,
message: '필수 필드가 누락되었습니다.',
message: "올바른 연결 타입을 지정해주세요. (internal 또는 external)"
});
return;
}
const job = await BatchService.createBatchJob(data);
res.status(201).json({
success: true,
data: job,
message: '배치 작업을 생성했습니다.',
});
const connectionId = type === 'external' ? Number(id) : undefined;
const result = await BatchService.getTableColumns(type, connectionId, tableName);
if (result.success) {
return res.json(result);
} else {
return res.status(500).json(result);
}
} catch (error) {
console.error('배치 작업 생성 오류:', error);
res.status(500).json({
console.error("컬럼 정보 조회 오류:", error);
return res.status(500).json({
success: false,
message: error instanceof Error ? error.message : '배치 작업 생성에 실패했습니다.',
message: "컬럼 정보 조회에 실패했습니다."
});
}
}
/**
* 배치 작업 수정
* 특정 배치 설정 조회
* GET /api/batch-configs/:id
*/
static async updateBatchJob(req: AuthenticatedRequest, res: Response): Promise<void> {
static async getBatchConfigById(req: AuthenticatedRequest, res: Response) {
try {
const id = parseInt(req.params.id);
if (isNaN(id)) {
res.status(400).json({
const { id } = req.params;
const batchConfig = await BatchService.getBatchConfigById(Number(id));
if (!batchConfig) {
return res.status(404).json({
success: false,
message: '유효하지 않은 ID입니다.',
message: "배치 설정을 찾을 수 없습니다."
});
}
return res.json({
success: true,
data: batchConfig
});
} catch (error) {
console.error("배치 설정 조회 오류:", error);
return res.status(500).json({
success: false,
message: "배치 설정 조회에 실패했습니다."
});
}
}
/**
* 배치 설정 생성
* POST /api/batch-configs
*/
static async createBatchConfig(req: AuthenticatedRequest, res: Response) {
try {
const { batchName, description, cronSchedule, mappings } = req.body;
if (!batchName || !cronSchedule || !mappings || !Array.isArray(mappings)) {
return res.status(400).json({
success: false,
message: "필수 필드가 누락되었습니다. (batchName, cronSchedule, mappings)"
});
return;
}
const data: Partial<BatchJob> = {
...req.body,
updated_by: req.user?.userId,
};
const job = await BatchService.updateBatchJob(id, data);
res.status(200).json({
const batchConfig = await BatchService.createBatchConfig({
batchName,
description,
cronSchedule,
mappings
} as CreateBatchConfigRequest);
return res.status(201).json({
success: true,
data: job,
message: '배치 작업을 수정했습니다.',
data: batchConfig,
message: "배치 설정이 성공적으로 생성되었습니다."
});
} catch (error) {
console.error('배치 작업 수정 오류:', error);
res.status(500).json({
console.error("배치 설정 생성 오류:", error);
return res.status(500).json({
success: false,
message: error instanceof Error ? error.message : '배치 작업 수정에 실패했습니다.',
message: "배치 설정 생성에 실패했습니다."
});
}
}
/**
* 배치 작업 삭제
* 배치 설정 수정
* PUT /api/batch-configs/:id
*/
static async deleteBatchJob(req: AuthenticatedRequest, res: Response): Promise<void> {
static async updateBatchConfig(req: AuthenticatedRequest, res: Response) {
try {
const id = parseInt(req.params.id);
if (isNaN(id)) {
res.status(400).json({
const { id } = req.params;
const { batchName, description, cronSchedule, mappings, isActive } = req.body;
if (!batchName || !cronSchedule) {
return res.status(400).json({
success: false,
message: '유효하지 않은 ID입니다.',
message: "필수 필드가 누락되었습니다. (batchName, cronSchedule)"
});
return;
}
await BatchService.deleteBatchJob(id);
res.status(200).json({
success: true,
message: '배치 작업을 삭제했습니다.',
});
} catch (error) {
console.error('배치 작업 삭제 오류:', error);
res.status(500).json({
success: false,
message: error instanceof Error ? error.message : '배치 작업 삭제에 실패했습니다.',
});
}
}
/**
* 배치 작업 수동 실행
*/
static async executeBatchJob(req: AuthenticatedRequest, res: Response): Promise<void> {
try {
const id = parseInt(req.params.id);
if (isNaN(id)) {
res.status(400).json({
const batchConfig = await BatchService.updateBatchConfig(Number(id), {
batchName,
description,
cronSchedule,
mappings,
isActive
} as UpdateBatchConfigRequest);
if (!batchConfig) {
return res.status(404).json({
success: false,
message: '유효하지 않은 ID입니다.',
message: "배치 설정을 찾을 수 없습니다."
});
return;
}
const execution = await BatchService.executeBatchJob(id);
res.status(200).json({
return res.json({
success: true,
data: execution,
message: '배치 작업을 실행했습니다.',
data: batchConfig,
message: "배치 설정이 성공적으로 수정되었습니다."
});
} catch (error) {
console.error('배치 작업 실행 오류:', error);
res.status(500).json({
console.error("배치 설정 수정 오류:", error);
return res.status(500).json({
success: false,
message: error instanceof Error ? error.message : '배치 작업 실행에 실패했습니다.',
message: "배치 설정 수정에 실패했습니다."
});
}
}
/**
* 배치 실행 목록 조회
* 배치 설정 삭제 (논리 삭제)
* DELETE /api/batch-configs/:id
*/
static async getBatchExecutions(req: AuthenticatedRequest, res: Response): Promise<void> {
static async deleteBatchConfig(req: AuthenticatedRequest, res: Response) {
try {
const jobId = req.query.job_id ? parseInt(req.query.job_id as string) : undefined;
const executions = await BatchService.getBatchExecutions(jobId);
res.status(200).json({
const { id } = req.params;
const result = await BatchService.deleteBatchConfig(Number(id));
if (!result) {
return res.status(404).json({
success: false,
message: "배치 설정을 찾을 수 없습니다."
});
}
return res.json({
success: true,
data: executions,
message: '배치 실행 목록을 조회했습니다.',
message: "배치 설정이 성공적으로 삭제되었습니다."
});
} catch (error) {
console.error('배치 실행 목록 조회 오류:', error);
res.status(500).json({
console.error("배치 설정 삭제 오류:", error);
return res.status(500).json({
success: false,
message: error instanceof Error ? error.message : '배치 실행 목록 조회에 실패했습니다.',
message: "배치 설정 삭제에 실패했습니다."
});
}
}
/**
* 배치 모니터링 정보 조회
*/
static async getBatchMonitoring(req: AuthenticatedRequest, res: Response): Promise<void> {
try {
const monitoring = await BatchService.getBatchMonitoring();
res.status(200).json({
success: true,
data: monitoring,
message: '배치 모니터링 정보를 조회했습니다.',
});
} catch (error) {
console.error('배치 모니터링 조회 오류:', error);
res.status(500).json({
success: false,
message: error instanceof Error ? error.message : '배치 모니터링 조회에 실패했습니다.',
});
}
}
/**
* 지원되는 작업 타입 조회
*/
static async getSupportedJobTypes(req: AuthenticatedRequest, res: Response): Promise<void> {
try {
const { BATCH_JOB_TYPE_OPTIONS } = await import('../types/batchManagement');
res.status(200).json({
success: true,
data: {
types: BATCH_JOB_TYPE_OPTIONS,
},
message: '지원하는 작업 타입 목록을 조회했습니다.',
});
} catch (error) {
console.error('작업 타입 조회 오류:', error);
res.status(500).json({
success: false,
message: '작업 타입 조회에 실패했습니다.',
});
}
}
/**
* 스케줄 프리셋 조회
*/
static async getSchedulePresets(req: AuthenticatedRequest, res: Response): Promise<void> {
try {
const { SCHEDULE_PRESETS } = await import('../types/batchManagement');
res.status(200).json({
success: true,
data: {
presets: SCHEDULE_PRESETS,
},
message: '스케줄 프리셋 목록을 조회했습니다.',
});
} catch (error) {
console.error('스케줄 프리셋 조회 오류:', error);
res.status(500).json({
success: false,
message: '스케줄 프리셋 조회에 실패했습니다.',
});
}
}
}
}

View File

@@ -0,0 +1,179 @@
// 배치 실행 로그 컨트롤러
// 작성일: 2024-12-24
import { Request, Response } from "express";
import { AuthenticatedRequest } from "../types/auth";
import { BatchExecutionLogService } from "../services/batchExecutionLogService";
import { BatchExecutionLogFilter, CreateBatchExecutionLogRequest, UpdateBatchExecutionLogRequest } from "../types/batchExecutionLogTypes";
export class BatchExecutionLogController {
/**
* 배치 실행 로그 목록 조회
*/
static async getExecutionLogs(req: AuthenticatedRequest, res: Response) {
try {
const {
batch_config_id,
execution_status,
start_date,
end_date,
page,
limit
} = req.query;
const filter: BatchExecutionLogFilter = {
batch_config_id: batch_config_id ? Number(batch_config_id) : undefined,
execution_status: execution_status as string,
start_date: start_date ? new Date(start_date as string) : undefined,
end_date: end_date ? new Date(end_date as string) : undefined,
page: page ? Number(page) : undefined,
limit: limit ? Number(limit) : undefined
};
const result = await BatchExecutionLogService.getExecutionLogs(filter);
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 createExecutionLog(req: AuthenticatedRequest, res: Response) {
try {
const data: CreateBatchExecutionLogRequest = req.body;
const result = await BatchExecutionLogService.createExecutionLog(data);
if (result.success) {
res.status(201).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 updateExecutionLog(req: AuthenticatedRequest, res: Response) {
try {
const { id } = req.params;
const data: UpdateBatchExecutionLogRequest = req.body;
const result = await BatchExecutionLogService.updateExecutionLog(Number(id), data);
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 deleteExecutionLog(req: AuthenticatedRequest, res: Response) {
try {
const { id } = req.params;
const result = await BatchExecutionLogService.deleteExecutionLog(Number(id));
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 getLatestExecutionLog(req: AuthenticatedRequest, res: Response) {
try {
const { batchConfigId } = req.params;
const result = await BatchExecutionLogService.getLatestExecutionLog(Number(batchConfigId));
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 getExecutionStats(req: AuthenticatedRequest, res: Response) {
try {
const {
batch_config_id,
start_date,
end_date
} = req.query;
const result = await BatchExecutionLogService.getExecutionStats(
batch_config_id ? Number(batch_config_id) : undefined,
start_date ? new Date(start_date as string) : undefined,
end_date ? new Date(end_date as string) : undefined
);
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 : "알 수 없는 오류"
});
}
}
}

View File

@@ -0,0 +1,619 @@
// 배치관리 전용 컨트롤러 (기존 소스와 완전 분리)
// 작성일: 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";
import { BatchExternalDbService } from "../services/batchExternalDbService";
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 : "알 수 없는 오류"
});
}
}
/**
* 특정 배치 설정 조회
* 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 : "알 수 없는 오류"
});
}
}
/**
* 배치 설정 목록 조회
* 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}개 컬럼 매핑`);
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)
);
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, {
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 : "알 수 없는 오류"
});
}
}
/**
* REST API 데이터 미리보기
*/
static async previewRestApiData(req: AuthenticatedRequest, res: Response) {
try {
const { apiUrl, apiKey, endpoint, method = 'GET' } = req.body;
if (!apiUrl || !apiKey || !endpoint) {
return res.status(400).json({
success: false,
message: "API URL, API Key, 엔드포인트는 필수입니다."
});
}
// RestApiConnector 사용하여 데이터 조회
const { RestApiConnector } = await import('../database/RestApiConnector');
const connector = new RestApiConnector({
baseUrl: apiUrl,
apiKey: apiKey,
timeout: 30000
});
// 연결 테스트
await connector.connect();
// 데이터 조회 (최대 5개만) - GET 메서드만 지원
const result = await connector.executeQuery(endpoint, method);
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: "배치 저장 중 오류가 발생했습니다."
});
}
}
}