feat: 배치 관리 시스템 구현
✨ 주요 기능: - 배치 설정 관리 (생성/수정/삭제/실행) - 배치 실행 로그 관리 및 모니터링 - 배치 스케줄러 자동 실행 (cron 기반) - 외부 DB 연결을 통한 데이터 동기화 - Oracle, MSSQL, MariaDB 커넥터 지원 🔧 백엔드 구현: - BatchManagementController: 배치 설정 CRUD - BatchExecutionLogController: 실행 로그 관리 - BatchSchedulerService: 자동 스케줄링 - BatchExternalDbService: 외부 DB 연동 - 배치 관련 테이블 스키마 추가 🎨 프론트엔드 구현: - 배치 관리 대시보드 UI - 배치 생성/수정 폼 - 실행 로그 모니터링 화면 - 수동 실행 및 상태 관리 🛡️ 안전성: - 기존 시스템과 독립적 구현 - 트랜잭션 기반 안전한 데이터 처리 - 에러 핸들링 및 로깅 강화
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
// 배치관리 API 클라이언트
|
||||
// 배치관리 API 클라이언트 (새로운 API로 업데이트)
|
||||
// 작성일: 2024-12-24
|
||||
|
||||
import { apiClient } from "./client";
|
||||
@@ -61,11 +61,18 @@ export interface ColumnInfo {
|
||||
column_default?: string;
|
||||
}
|
||||
|
||||
export interface TableInfo {
|
||||
table_name: string;
|
||||
columns: ColumnInfo[];
|
||||
description?: string | null;
|
||||
}
|
||||
|
||||
export interface BatchMappingRequest {
|
||||
batch_name: string;
|
||||
batchName: string;
|
||||
description?: string;
|
||||
cron_schedule: string;
|
||||
cronSchedule: string;
|
||||
mappings: BatchMapping[];
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
export interface ApiResponse<T> {
|
||||
@@ -76,31 +83,51 @@ export interface ApiResponse<T> {
|
||||
}
|
||||
|
||||
export class BatchAPI {
|
||||
private static readonly BASE_PATH = "/batch-configs";
|
||||
private static readonly BASE_PATH = "/batch-management";
|
||||
|
||||
/**
|
||||
* 배치 설정 목록 조회
|
||||
*/
|
||||
static async getBatchConfigs(filter: BatchConfigFilter = {}): Promise<BatchConfig[]> {
|
||||
static async getBatchConfigs(filter: BatchConfigFilter = {}): Promise<{
|
||||
success: boolean;
|
||||
data: BatchConfig[];
|
||||
pagination?: {
|
||||
page: number;
|
||||
limit: number;
|
||||
total: number;
|
||||
totalPages: number;
|
||||
};
|
||||
message?: string;
|
||||
}> {
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (filter.is_active) params.append("is_active", filter.is_active);
|
||||
if (filter.company_code) params.append("company_code", filter.company_code);
|
||||
if (filter.search) params.append("search", filter.search);
|
||||
if (filter.page) params.append("page", filter.page.toString());
|
||||
if (filter.limit) params.append("limit", filter.limit.toString());
|
||||
|
||||
const response = await apiClient.get<ApiResponse<BatchConfig[]>>(
|
||||
`${this.BASE_PATH}?${params.toString()}`,
|
||||
);
|
||||
const response = await apiClient.get<{
|
||||
success: boolean;
|
||||
data: BatchConfig[];
|
||||
pagination?: {
|
||||
page: number;
|
||||
limit: number;
|
||||
total: number;
|
||||
totalPages: number;
|
||||
};
|
||||
message?: string;
|
||||
}>(`${this.BASE_PATH}/batch-configs?${params.toString()}`);
|
||||
|
||||
if (!response.data.success) {
|
||||
throw new Error(response.data.message || "배치 설정 목록 조회에 실패했습니다.");
|
||||
}
|
||||
|
||||
return response.data.data || [];
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error("배치 설정 목록 조회 오류:", error);
|
||||
throw error;
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
message: error instanceof Error ? error.message : "배치 설정 목록 조회에 실패했습니다."
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,7 +137,7 @@ export class BatchAPI {
|
||||
static async getBatchConfigById(id: number): Promise<BatchConfig> {
|
||||
try {
|
||||
const response = await apiClient.get<ApiResponse<BatchConfig>>(
|
||||
`${this.BASE_PATH}/${id}`,
|
||||
`${this.BASE_PATH}/batch-configs/${id}`,
|
||||
);
|
||||
|
||||
if (!response.data.success) {
|
||||
@@ -134,7 +161,7 @@ export class BatchAPI {
|
||||
static async createBatchConfig(data: BatchMappingRequest): Promise<BatchConfig> {
|
||||
try {
|
||||
const response = await apiClient.post<ApiResponse<BatchConfig>>(
|
||||
this.BASE_PATH,
|
||||
`${this.BASE_PATH}/batch-configs`,
|
||||
data,
|
||||
);
|
||||
|
||||
@@ -162,7 +189,7 @@ export class BatchAPI {
|
||||
): Promise<BatchConfig> {
|
||||
try {
|
||||
const response = await apiClient.put<ApiResponse<BatchConfig>>(
|
||||
`${this.BASE_PATH}/${id}`,
|
||||
`${this.BASE_PATH}/batch-configs/${id}`,
|
||||
data,
|
||||
);
|
||||
|
||||
@@ -187,7 +214,7 @@ export class BatchAPI {
|
||||
static async deleteBatchConfig(id: number): Promise<void> {
|
||||
try {
|
||||
const response = await apiClient.delete<ApiResponse<void>>(
|
||||
`${this.BASE_PATH}/${id}`,
|
||||
`${this.BASE_PATH}/batch-configs/${id}`,
|
||||
);
|
||||
|
||||
if (!response.data.success) {
|
||||
@@ -202,19 +229,32 @@ export class BatchAPI {
|
||||
/**
|
||||
* 사용 가능한 커넥션 목록 조회
|
||||
*/
|
||||
static async getAvailableConnections(): Promise<ConnectionInfo[]> {
|
||||
static async getConnections(): Promise<ConnectionInfo[]> {
|
||||
try {
|
||||
console.log("[BatchAPI] getAvailableConnections 호출 시작");
|
||||
console.log("[BatchAPI] API URL:", `${this.BASE_PATH}/connections`);
|
||||
|
||||
const response = await apiClient.get<ApiResponse<ConnectionInfo[]>>(
|
||||
`${this.BASE_PATH}/connections`,
|
||||
);
|
||||
|
||||
console.log("[BatchAPI] API 응답:", response);
|
||||
console.log("[BatchAPI] 응답 데이터:", response.data);
|
||||
|
||||
if (!response.data.success) {
|
||||
console.error("[BatchAPI] API 응답 실패:", response.data);
|
||||
throw new Error(response.data.message || "커넥션 목록 조회에 실패했습니다.");
|
||||
}
|
||||
|
||||
return response.data.data || [];
|
||||
const result = response.data.data || [];
|
||||
console.log("[BatchAPI] 최종 결과:", result);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error("커넥션 목록 조회 오류:", error);
|
||||
console.error("[BatchAPI] 커넥션 목록 조회 오류:", error);
|
||||
console.error("[BatchAPI] 오류 상세:", {
|
||||
message: error instanceof Error ? error.message : 'Unknown error',
|
||||
stack: error instanceof Error ? error.stack : undefined
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -233,13 +273,15 @@ export class BatchAPI {
|
||||
}
|
||||
url += '/tables';
|
||||
|
||||
const response = await apiClient.get<ApiResponse<string[]>>(url);
|
||||
const response = await apiClient.get<ApiResponse<TableInfo[]>>(url);
|
||||
|
||||
if (!response.data.success) {
|
||||
throw new Error(response.data.message || "테이블 목록 조회에 실패했습니다.");
|
||||
}
|
||||
|
||||
return response.data.data || [];
|
||||
// TableInfo[]에서 table_name만 추출하여 string[]로 변환
|
||||
const tables = response.data.data || [];
|
||||
return tables.map(table => table.table_name);
|
||||
} catch (error) {
|
||||
console.error("테이블 목록 조회 오류:", error);
|
||||
throw error;
|
||||
@@ -251,8 +293,8 @@ export class BatchAPI {
|
||||
*/
|
||||
static async getTableColumns(
|
||||
connectionType: 'internal' | 'external',
|
||||
tableName: string,
|
||||
connectionId?: number
|
||||
connectionId: number | undefined,
|
||||
tableName: string
|
||||
): Promise<ColumnInfo[]> {
|
||||
try {
|
||||
let url = `${this.BASE_PATH}/connections/${connectionType}`;
|
||||
@@ -273,4 +315,38 @@ export class BatchAPI {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 배치 수동 실행
|
||||
*/
|
||||
static async executeBatchConfig(batchId: number): Promise<{
|
||||
success: boolean;
|
||||
message?: string;
|
||||
data?: {
|
||||
batchId: string;
|
||||
totalRecords: number;
|
||||
successRecords: number;
|
||||
failedRecords: number;
|
||||
duration: number;
|
||||
};
|
||||
}> {
|
||||
try {
|
||||
const response = await apiClient.post<{
|
||||
success: boolean;
|
||||
message?: string;
|
||||
data?: {
|
||||
batchId: string;
|
||||
totalRecords: number;
|
||||
successRecords: number;
|
||||
failedRecords: number;
|
||||
duration: number;
|
||||
};
|
||||
}>(`${this.BASE_PATH}/batch-configs/${batchId}/execute`);
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error("배치 실행 오류:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
113
frontend/lib/api/batchManagement.ts
Normal file
113
frontend/lib/api/batchManagement.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
// 배치관리 전용 API 클라이언트 (기존 소스와 완전 분리)
|
||||
// 작성일: 2024-12-24
|
||||
|
||||
import { apiClient } from "./client";
|
||||
|
||||
// 배치관리 전용 타입 정의
|
||||
export interface BatchConnectionInfo {
|
||||
type: 'internal' | 'external';
|
||||
id?: number;
|
||||
name: string;
|
||||
db_type?: string;
|
||||
}
|
||||
|
||||
export interface BatchColumnInfo {
|
||||
column_name: string;
|
||||
data_type: string;
|
||||
is_nullable?: string;
|
||||
column_default?: string | null;
|
||||
}
|
||||
|
||||
export interface BatchTableInfo {
|
||||
table_name: string;
|
||||
columns: BatchColumnInfo[];
|
||||
description?: string | null;
|
||||
}
|
||||
|
||||
export interface BatchApiResponse<T = unknown> {
|
||||
success: boolean;
|
||||
data?: T;
|
||||
message?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export const BatchManagementAPI = {
|
||||
BASE_PATH: "/api/batch-management",
|
||||
|
||||
/**
|
||||
* 사용 가능한 커넥션 목록 조회
|
||||
*/
|
||||
static async getAvailableConnections(): Promise<BatchConnectionInfo[]> {
|
||||
try {
|
||||
const response = await apiClient.get<BatchApiResponse<BatchConnectionInfo[]>>(
|
||||
`${this.BASE_PATH}/connections`
|
||||
);
|
||||
|
||||
if (!response.data.success) {
|
||||
throw new Error(response.data.message || "커넥션 목록 조회에 실패했습니다.");
|
||||
}
|
||||
|
||||
return response.data.data || [];
|
||||
} catch (error) {
|
||||
console.error("커넥션 목록 조회 오류:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 특정 커넥션의 테이블 목록 조회
|
||||
*/
|
||||
static async getTablesFromConnection(
|
||||
connectionType: 'internal' | 'external',
|
||||
connectionId?: number
|
||||
): Promise<string[]> {
|
||||
try {
|
||||
let url = `${this.BASE_PATH}/connections/${connectionType}`;
|
||||
if (connectionType === 'external' && connectionId) {
|
||||
url += `/${connectionId}`;
|
||||
}
|
||||
url += '/tables';
|
||||
|
||||
const response = await apiClient.get<BatchApiResponse<BatchTableInfo[]>>(url);
|
||||
|
||||
if (!response.data.success) {
|
||||
throw new Error(response.data.message || "테이블 목록 조회에 실패했습니다.");
|
||||
}
|
||||
|
||||
// BatchTableInfo[]에서 table_name만 추출하여 string[]로 변환
|
||||
const tables = response.data.data || [];
|
||||
return tables.map(table => table.table_name);
|
||||
} catch (error) {
|
||||
console.error("테이블 목록 조회 오류:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 특정 테이블의 컬럼 정보 조회
|
||||
*/
|
||||
static async getTableColumns(
|
||||
connectionType: 'internal' | 'external',
|
||||
tableName: string,
|
||||
connectionId?: number
|
||||
): Promise<BatchColumnInfo[]> {
|
||||
try {
|
||||
let url = `${this.BASE_PATH}/connections/${connectionType}`;
|
||||
if (connectionType === 'external' && connectionId) {
|
||||
url += `/${connectionId}`;
|
||||
}
|
||||
url += `/tables/${tableName}/columns`;
|
||||
|
||||
const response = await apiClient.get<BatchApiResponse<BatchColumnInfo[]>>(url);
|
||||
|
||||
if (!response.data.success) {
|
||||
throw new Error(response.data.message || "컬럼 정보 조회에 실패했습니다.");
|
||||
}
|
||||
|
||||
return response.data.data || [];
|
||||
} catch (error) {
|
||||
console.error("컬럼 정보 조회 오류:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user