feat: 배치 관리 시스템 구현
✨ 주요 기능: - 배치 설정 관리 (생성/수정/삭제/실행) - 배치 실행 로그 관리 및 모니터링 - 배치 스케줄러 자동 실행 (cron 기반) - 외부 DB 연결을 통한 데이터 동기화 - Oracle, MSSQL, MariaDB 커넥터 지원 🔧 백엔드 구현: - BatchManagementController: 배치 설정 CRUD - BatchExecutionLogController: 실행 로그 관리 - BatchSchedulerService: 자동 스케줄링 - BatchExternalDbService: 외부 DB 연동 - 배치 관련 테이블 스키마 추가 🎨 프론트엔드 구현: - 배치 관리 대시보드 UI - 배치 생성/수정 폼 - 실행 로그 모니터링 화면 - 수동 실행 및 상태 관리 🛡️ 안전성: - 기존 시스템과 독립적 구현 - 트랜잭션 기반 안전한 데이터 처리 - 에러 핸들링 및 로깅 강화
This commit is contained in:
@@ -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: "배치 설정 삭제에 실패했습니다."
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user