feat: 배치 관리 시스템 구현

 주요 기능:
- 배치 설정 관리 (생성/수정/삭제/실행)
- 배치 실행 로그 관리 및 모니터링
- 배치 스케줄러 자동 실행 (cron 기반)
- 외부 DB 연결을 통한 데이터 동기화
- Oracle, MSSQL, MariaDB 커넥터 지원

🔧 백엔드 구현:
- BatchManagementController: 배치 설정 CRUD
- BatchExecutionLogController: 실행 로그 관리
- BatchSchedulerService: 자동 스케줄링
- BatchExternalDbService: 외부 DB 연동
- 배치 관련 테이블 스키마 추가

🎨 프론트엔드 구현:
- 배치 관리 대시보드 UI
- 배치 생성/수정 폼
- 실행 로그 모니터링 화면
- 수동 실행 및 상태 관리

🛡️ 안전성:
- 기존 시스템과 독립적 구현
- 트랜잭션 기반 안전한 데이터 처리
- 에러 핸들링 및 로깅 강화
This commit is contained in:
2025-09-25 11:04:16 +09:00
parent 4abf5b31c0
commit 949aab0b73
33 changed files with 8549 additions and 961 deletions

View File

@@ -3,7 +3,7 @@
import { Request, Response } from "express";
import { BatchService } from "../services/batchService";
import { BatchConfigFilter, BatchMappingRequest } from "../types/batchTypes";
import { BatchConfigFilter, CreateBatchConfigRequest, UpdateBatchConfigRequest } from "../types/batchTypes";
export interface AuthenticatedRequest extends Request {
user?: {
@@ -20,172 +20,27 @@ export class BatchController {
*/
static async getBatchConfigs(req: AuthenticatedRequest, res: Response) {
try {
const { page = 1, limit = 10, search, isActive } = req.query;
const filter: BatchConfigFilter = {
is_active: req.query.is_active as string,
company_code: req.query.company_code as string,
search: req.query.search as string,
page: Number(page),
limit: Number(limit),
search: search as string,
is_active: isActive as string
};
// 빈 값 제거
Object.keys(filter).forEach((key) => {
if (!filter[key as keyof BatchConfigFilter]) {
delete filter[key as keyof BatchConfigFilter];
}
});
const result = await BatchService.getBatchConfigs(filter);
if (result.success) {
return res.status(200).json(result);
} else {
return res.status(400).json(result);
}
res.json({
success: true,
data: result.data,
pagination: result.pagination
});
} catch (error) {
console.error("배치 설정 목록 조회 오류:", error);
return res.status(500).json({
res.status(500).json({
success: false,
message: "서버 내부 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "알 수 없는 오류",
});
}
}
/**
* 특정 배치 설정 조회
* GET /api/batch-configs/:id
*/
static async getBatchConfigById(req: AuthenticatedRequest, res: Response) {
try {
const id = parseInt(req.params.id);
if (isNaN(id)) {
return res.status(400).json({
success: false,
message: "유효하지 않은 배치 설정 ID입니다.",
});
}
const result = await BatchService.getBatchConfigById(id);
if (result.success) {
return res.status(200).json(result);
} else {
return res.status(404).json(result);
}
} catch (error) {
console.error("배치 설정 조회 오류:", error);
return res.status(500).json({
success: false,
message: "서버 내부 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "알 수 없는 오류",
});
}
}
/**
* 배치 설정 생성
* POST /api/batch-configs
*/
static async createBatchConfig(req: AuthenticatedRequest, res: Response) {
try {
const data: BatchMappingRequest = req.body;
// 필수 필드 검증
if (!data.batch_name || !data.cron_schedule || !data.mappings) {
return res.status(400).json({
success: false,
message: "필수 필드가 누락되었습니다. (batch_name, cron_schedule, mappings)",
});
}
const result = await BatchService.createBatchConfig(
data,
req.user?.userId
);
if (result.success) {
return res.status(201).json(result);
} else {
return res.status(400).json(result);
}
} catch (error) {
console.error("배치 설정 생성 오류:", error);
return res.status(500).json({
success: false,
message: "서버 내부 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "알 수 없는 오류",
});
}
}
/**
* 배치 설정 수정
* PUT /api/batch-configs/:id
*/
static async updateBatchConfig(req: AuthenticatedRequest, res: Response) {
try {
const id = parseInt(req.params.id);
const data: Partial<BatchMappingRequest> = req.body;
if (isNaN(id)) {
return res.status(400).json({
success: false,
message: "유효하지 않은 배치 설정 ID입니다.",
});
}
const result = await BatchService.updateBatchConfig(
id,
data,
req.user?.userId
);
if (result.success) {
return res.status(200).json(result);
} else {
return res.status(400).json(result);
}
} catch (error) {
console.error("배치 설정 수정 오류:", error);
return res.status(500).json({
success: false,
message: "서버 내부 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "알 수 없는 오류",
});
}
}
/**
* 배치 설정 삭제
* DELETE /api/batch-configs/:id
*/
static async deleteBatchConfig(req: AuthenticatedRequest, res: Response) {
try {
const id = parseInt(req.params.id);
if (isNaN(id)) {
return res.status(400).json({
success: false,
message: "유효하지 않은 배치 설정 ID입니다.",
});
}
const result = await BatchService.deleteBatchConfig(
id,
req.user?.userId
);
if (result.success) {
return res.status(200).json(result);
} else {
return res.status(404).json(result);
}
} catch (error) {
console.error("배치 설정 삭제 오류:", error);
return res.status(500).json({
success: false,
message: "서버 내부 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "알 수 없는 오류",
message: "배치 설정 목록 조회에 실패했습니다."
});
}
}
@@ -197,116 +52,230 @@ export class BatchController {
static async getAvailableConnections(req: AuthenticatedRequest, res: Response) {
try {
const result = await BatchService.getAvailableConnections();
if (result.success) {
return res.status(200).json(result);
res.json(result);
} else {
return res.status(400).json(result);
res.status(500).json(result);
}
} catch (error) {
console.error("커넥션 목록 조회 오류:", error);
return res.status(500).json({
res.status(500).json({
success: false,
message: "서버 내부 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "알 수 없는 오류",
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 connectionType = req.params.type as 'internal' | 'external';
const connectionId = req.params.id ? parseInt(req.params.id) : undefined;
if (connectionType !== 'internal' && connectionType !== 'external') {
const { type, id } = req.params;
if (!type || (type !== 'internal' && type !== 'external')) {
return res.status(400).json({
success: false,
message: "유효하지 않은 커넥션 타입입니다. (internal 또는 external)",
message: "올바른 연결 타입을 지정해주세요. (internal 또는 external)"
});
}
if (connectionType === 'external' && (!connectionId || isNaN(connectionId))) {
return res.status(400).json({
success: false,
message: "외부 커넥션의 경우 유효한 커넥션 ID가 필요합니다.",
});
}
const result = await BatchService.getTablesFromConnection(
connectionType,
connectionId
);
const connectionId = type === 'external' ? Number(id) : undefined;
const result = await BatchService.getTablesFromConnection(type, connectionId);
if (result.success) {
return res.status(200).json(result);
return res.json(result);
} else {
return res.status(400).json(result);
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 : "알 수 없는 오류",
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 connectionType = req.params.type as 'internal' | 'external';
const connectionId = req.params.id ? parseInt(req.params.id) : undefined;
const tableName = req.params.tableName;
if (connectionType !== 'internal' && connectionType !== 'external') {
const { type, id, tableName } = req.params;
if (!type || !tableName) {
return res.status(400).json({
success: false,
message: "유효하지 않은 커넥션 타입입니다. (internal 또는 external)",
message: "연결 타입과 테이블명을 모두 지정해주세요."
});
}
if (connectionType === 'external' && (!connectionId || isNaN(connectionId))) {
if (type !== 'internal' && type !== 'external') {
return res.status(400).json({
success: false,
message: "외부 커넥션의 경우 유효한 커넥션 ID가 필요합니다.",
message: "올바른 연결 타입을 지정해주세요. (internal 또는 external)"
});
}
if (!tableName) {
return res.status(400).json({
success: false,
message: "테이블명이 필요합니다.",
});
}
const result = await BatchService.getTableColumns(
connectionType,
tableName,
connectionId
);
const connectionId = type === 'external' ? Number(id) : undefined;
const result = await BatchService.getTableColumns(type, connectionId, tableName);
if (result.success) {
return res.status(200).json(result);
return res.json(result);
} else {
return res.status(400).json(result);
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 : "알 수 없는 오류",
message: "컬럼 정보 조회에 실패했습니다."
});
}
}
}
/**
* 특정 배치 설정 조회
* GET /api/batch-configs/:id
*/
static async getBatchConfigById(req: AuthenticatedRequest, res: Response) {
try {
const { id } = req.params;
const batchConfig = await BatchService.getBatchConfigById(Number(id));
if (!batchConfig) {
return res.status(404).json({
success: false,
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)"
});
}
const batchConfig = await BatchService.createBatchConfig({
batchName,
description,
cronSchedule,
mappings
} 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: "배치 설정 생성에 실패했습니다."
});
}
}
/**
* 배치 설정 수정
* PUT /api/batch-configs/:id
*/
static async updateBatchConfig(req: AuthenticatedRequest, res: Response) {
try {
const { id } = req.params;
const { batchName, description, cronSchedule, mappings, isActive } = req.body;
if (!batchName || !cronSchedule) {
return res.status(400).json({
success: false,
message: "필수 필드가 누락되었습니다. (batchName, cronSchedule)"
});
}
const batchConfig = await BatchService.updateBatchConfig(Number(id), {
batchName,
description,
cronSchedule,
mappings,
isActive
} as UpdateBatchConfigRequest);
if (!batchConfig) {
return res.status(404).json({
success: false,
message: "배치 설정을 찾을 수 없습니다."
});
}
return res.json({
success: true,
data: batchConfig,
message: "배치 설정이 성공적으로 수정되었습니다."
});
} catch (error) {
console.error("배치 설정 수정 오류:", error);
return res.status(500).json({
success: false,
message: "배치 설정 수정에 실패했습니다."
});
}
}
/**
* 배치 설정 삭제 (논리 삭제)
* DELETE /api/batch-configs/:id
*/
static async deleteBatchConfig(req: AuthenticatedRequest, res: Response) {
try {
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,
message: "배치 설정이 성공적으로 삭제되었습니다."
});
} catch (error) {
console.error("배치 설정 삭제 오류:", error);
return res.status(500).json({
success: false,
message: "배치 설정 삭제에 실패했습니다."
});
}
}
}