feat: 배치 관리 시스템 구현
✨ 주요 기능: - 배치 설정 관리 (생성/수정/삭제/실행) - 배치 실행 로그 관리 및 모니터링 - 배치 스케줄러 자동 실행 (cron 기반) - 외부 DB 연결을 통한 데이터 동기화 - Oracle, MSSQL, MariaDB 커넥터 지원 🔧 백엔드 구현: - BatchManagementController: 배치 설정 CRUD - BatchExecutionLogController: 실행 로그 관리 - BatchSchedulerService: 자동 스케줄링 - BatchExternalDbService: 외부 DB 연동 - 배치 관련 테이블 스키마 추가 🎨 프론트엔드 구현: - 배치 관리 대시보드 UI - 배치 생성/수정 폼 - 실행 로그 모니터링 화면 - 수동 실행 및 상태 관리 🛡️ 안전성: - 기존 시스템과 독립적 구현 - 트랜잭션 기반 안전한 데이터 처리 - 에러 핸들링 및 로깅 강화
This commit is contained in:
370
backend-node/src/services/batchManagementService.ts
Normal file
370
backend-node/src/services/batchManagementService.ts
Normal file
@@ -0,0 +1,370 @@
|
||||
// 배치관리 전용 서비스 (기존 소스와 완전 분리)
|
||||
// 작성일: 2024-12-24
|
||||
|
||||
import prisma from "../config/database";
|
||||
import { PasswordEncryption } from "../utils/passwordEncryption";
|
||||
import { DatabaseConnectorFactory } from "../database/DatabaseConnectorFactory";
|
||||
|
||||
// 배치관리 전용 타입 정의
|
||||
export interface BatchConnectionInfo {
|
||||
type: 'internal' | 'external';
|
||||
id?: number;
|
||||
name: string;
|
||||
db_type?: string;
|
||||
}
|
||||
|
||||
export interface BatchTableInfo {
|
||||
table_name: string;
|
||||
columns: BatchColumnInfo[];
|
||||
description?: string | null;
|
||||
}
|
||||
|
||||
export interface BatchColumnInfo {
|
||||
column_name: string;
|
||||
data_type: string;
|
||||
is_nullable?: string;
|
||||
column_default?: string | null;
|
||||
}
|
||||
|
||||
export interface BatchApiResponse<T = unknown> {
|
||||
success: boolean;
|
||||
data?: T;
|
||||
message?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export class BatchManagementService {
|
||||
/**
|
||||
* 배치관리용 연결 목록 조회
|
||||
*/
|
||||
static async getAvailableConnections(): Promise<BatchApiResponse<BatchConnectionInfo[]>> {
|
||||
try {
|
||||
const connections: BatchConnectionInfo[] = [];
|
||||
|
||||
// 내부 DB 추가
|
||||
connections.push({
|
||||
type: 'internal',
|
||||
name: '내부 데이터베이스 (PostgreSQL)',
|
||||
db_type: 'postgresql'
|
||||
});
|
||||
|
||||
// 활성화된 외부 DB 연결 조회
|
||||
const externalConnections = await prisma.external_db_connections.findMany({
|
||||
where: { is_active: 'Y' },
|
||||
select: {
|
||||
id: true,
|
||||
connection_name: true,
|
||||
db_type: true,
|
||||
description: true
|
||||
},
|
||||
orderBy: { connection_name: 'asc' }
|
||||
});
|
||||
|
||||
// 외부 DB 연결 추가
|
||||
externalConnections.forEach(conn => {
|
||||
connections.push({
|
||||
type: 'external',
|
||||
id: conn.id,
|
||||
name: `${conn.connection_name} (${conn.db_type?.toUpperCase()})`,
|
||||
db_type: conn.db_type || undefined
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: connections,
|
||||
message: `${connections.length}개의 연결을 조회했습니다.`
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("배치관리 연결 목록 조회 실패:", error);
|
||||
return {
|
||||
success: false,
|
||||
message: "연결 목록 조회 중 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "알 수 없는 오류"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 배치관리용 테이블 목록 조회
|
||||
*/
|
||||
static async getTablesFromConnection(
|
||||
connectionType: 'internal' | 'external',
|
||||
connectionId?: number
|
||||
): Promise<BatchApiResponse<BatchTableInfo[]>> {
|
||||
try {
|
||||
let tables: BatchTableInfo[] = [];
|
||||
|
||||
if (connectionType === 'internal') {
|
||||
// 내부 DB 테이블 조회
|
||||
const result = await prisma.$queryRaw<Array<{ table_name: string }>>`
|
||||
SELECT table_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
AND table_type = 'BASE TABLE'
|
||||
ORDER BY table_name
|
||||
`;
|
||||
|
||||
tables = result.map(row => ({
|
||||
table_name: row.table_name,
|
||||
columns: []
|
||||
}));
|
||||
} else if (connectionType === 'external' && connectionId) {
|
||||
// 외부 DB 테이블 조회
|
||||
const tablesResult = await this.getExternalTables(connectionId);
|
||||
if (tablesResult.success && tablesResult.data) {
|
||||
tables = tablesResult.data;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: tables,
|
||||
message: `${tables.length}개의 테이블을 조회했습니다.`
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("배치관리 테이블 목록 조회 실패:", error);
|
||||
return {
|
||||
success: false,
|
||||
message: "테이블 목록 조회 중 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "알 수 없는 오류"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 배치관리용 테이블 컬럼 정보 조회
|
||||
*/
|
||||
static async getTableColumns(
|
||||
connectionType: 'internal' | 'external',
|
||||
connectionId: number | undefined,
|
||||
tableName: string
|
||||
): Promise<BatchApiResponse<BatchColumnInfo[]>> {
|
||||
try {
|
||||
console.log(`[BatchManagementService] getTableColumns 호출:`, {
|
||||
connectionType,
|
||||
connectionId,
|
||||
tableName
|
||||
});
|
||||
|
||||
let columns: BatchColumnInfo[] = [];
|
||||
|
||||
if (connectionType === 'internal') {
|
||||
// 내부 DB 컬럼 조회
|
||||
console.log(`[BatchManagementService] 내부 DB 컬럼 조회 시작: ${tableName}`);
|
||||
|
||||
const result = await prisma.$queryRaw<Array<{
|
||||
column_name: string;
|
||||
data_type: string;
|
||||
is_nullable: string;
|
||||
column_default: string | null
|
||||
}>>`
|
||||
SELECT
|
||||
column_name,
|
||||
data_type,
|
||||
is_nullable,
|
||||
column_default
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = ${tableName}
|
||||
ORDER BY ordinal_position
|
||||
`;
|
||||
|
||||
console.log(`[BatchManagementService] 내부 DB 컬럼 조회 결과:`, result);
|
||||
|
||||
columns = result.map(row => ({
|
||||
column_name: row.column_name,
|
||||
data_type: row.data_type,
|
||||
is_nullable: row.is_nullable,
|
||||
column_default: row.column_default,
|
||||
}));
|
||||
} else if (connectionType === 'external' && connectionId) {
|
||||
// 외부 DB 컬럼 조회
|
||||
console.log(`[BatchManagementService] 외부 DB 컬럼 조회 시작: connectionId=${connectionId}, tableName=${tableName}`);
|
||||
|
||||
const columnsResult = await this.getExternalTableColumns(connectionId, tableName);
|
||||
|
||||
console.log(`[BatchManagementService] 외부 DB 컬럼 조회 결과:`, columnsResult);
|
||||
|
||||
if (columnsResult.success && columnsResult.data) {
|
||||
columns = columnsResult.data;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[BatchManagementService] 최종 컬럼 목록:`, columns);
|
||||
return {
|
||||
success: true,
|
||||
data: columns,
|
||||
message: `${columns.length}개의 컬럼을 조회했습니다.`
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("[BatchManagementService] 컬럼 정보 조회 오류:", error);
|
||||
return {
|
||||
success: false,
|
||||
message: "컬럼 정보 조회 중 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "알 수 없는 오류"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 외부 DB 테이블 목록 조회 (내부 구현)
|
||||
*/
|
||||
private static async getExternalTables(connectionId: number): Promise<BatchApiResponse<BatchTableInfo[]>> {
|
||||
try {
|
||||
// 연결 정보 조회
|
||||
const connection = await prisma.external_db_connections.findUnique({
|
||||
where: { id: connectionId }
|
||||
});
|
||||
|
||||
if (!connection) {
|
||||
return {
|
||||
success: false,
|
||||
message: "연결 정보를 찾을 수 없습니다."
|
||||
};
|
||||
}
|
||||
|
||||
// 비밀번호 복호화
|
||||
const decryptedPassword = PasswordEncryption.decrypt(connection.password);
|
||||
if (!decryptedPassword) {
|
||||
return {
|
||||
success: false,
|
||||
message: "비밀번호 복호화에 실패했습니다."
|
||||
};
|
||||
}
|
||||
|
||||
// 연결 설정 준비
|
||||
const config = {
|
||||
host: connection.host,
|
||||
port: connection.port,
|
||||
database: connection.database_name,
|
||||
user: connection.username,
|
||||
password: decryptedPassword,
|
||||
connectionTimeoutMillis: connection.connection_timeout != null ? connection.connection_timeout * 1000 : undefined,
|
||||
queryTimeoutMillis: connection.query_timeout != null ? connection.query_timeout * 1000 : undefined,
|
||||
ssl: connection.ssl_enabled === "Y" ? { rejectUnauthorized: false } : false
|
||||
};
|
||||
|
||||
// DatabaseConnectorFactory를 통한 테이블 목록 조회
|
||||
const connector = await DatabaseConnectorFactory.createConnector(connection.db_type, config, connectionId);
|
||||
const tables = await connector.getTables();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "테이블 목록을 조회했습니다.",
|
||||
data: tables
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("외부 DB 테이블 목록 조회 오류:", error);
|
||||
return {
|
||||
success: false,
|
||||
message: "테이블 목록 조회 중 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "알 수 없는 오류"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 외부 DB 테이블 컬럼 정보 조회 (내부 구현)
|
||||
*/
|
||||
private static async getExternalTableColumns(connectionId: number, tableName: string): Promise<BatchApiResponse<BatchColumnInfo[]>> {
|
||||
try {
|
||||
console.log(`[BatchManagementService] getExternalTableColumns 호출: connectionId=${connectionId}, tableName=${tableName}`);
|
||||
|
||||
// 연결 정보 조회
|
||||
const connection = await prisma.external_db_connections.findUnique({
|
||||
where: { id: connectionId }
|
||||
});
|
||||
|
||||
if (!connection) {
|
||||
console.log(`[BatchManagementService] 연결 정보를 찾을 수 없음: connectionId=${connectionId}`);
|
||||
return {
|
||||
success: false,
|
||||
message: "연결 정보를 찾을 수 없습니다."
|
||||
};
|
||||
}
|
||||
|
||||
console.log(`[BatchManagementService] 연결 정보 조회 성공:`, {
|
||||
id: connection.id,
|
||||
connection_name: connection.connection_name,
|
||||
db_type: connection.db_type,
|
||||
host: connection.host,
|
||||
port: connection.port,
|
||||
database_name: connection.database_name
|
||||
});
|
||||
|
||||
// 비밀번호 복호화
|
||||
const decryptedPassword = PasswordEncryption.decrypt(connection.password);
|
||||
|
||||
// 연결 설정 준비
|
||||
const config = {
|
||||
host: connection.host,
|
||||
port: connection.port,
|
||||
database: connection.database_name,
|
||||
user: connection.username,
|
||||
password: decryptedPassword,
|
||||
connectionTimeoutMillis: connection.connection_timeout != null ? connection.connection_timeout * 1000 : undefined,
|
||||
queryTimeoutMillis: connection.query_timeout != null ? connection.query_timeout * 1000 : undefined,
|
||||
ssl: connection.ssl_enabled === "Y" ? { rejectUnauthorized: false } : false
|
||||
};
|
||||
|
||||
console.log(`[BatchManagementService] 커넥터 생성 시작: db_type=${connection.db_type}`);
|
||||
|
||||
// 데이터베이스 타입에 따른 커넥터 생성
|
||||
const connector = await DatabaseConnectorFactory.createConnector(connection.db_type, config, connectionId);
|
||||
|
||||
console.log(`[BatchManagementService] 커넥터 생성 완료, 컬럼 조회 시작: tableName=${tableName}`);
|
||||
|
||||
// 컬럼 정보 조회
|
||||
console.log(`[BatchManagementService] connector.getColumns 호출 전`);
|
||||
const columns = await connector.getColumns(tableName);
|
||||
|
||||
console.log(`[BatchManagementService] 원본 컬럼 조회 결과:`, columns);
|
||||
console.log(`[BatchManagementService] 원본 컬럼 개수:`, columns ? columns.length : 'null/undefined');
|
||||
|
||||
// 각 데이터베이스 커넥터의 반환 구조가 다르므로 통일된 구조로 변환
|
||||
const standardizedColumns: BatchColumnInfo[] = columns.map((col: any) => {
|
||||
console.log(`[BatchManagementService] 컬럼 변환 중:`, col);
|
||||
|
||||
// MySQL/MariaDB 구조: {name, dataType, isNullable, defaultValue} (MySQLConnector만)
|
||||
if (col.name && col.dataType !== undefined) {
|
||||
const result = {
|
||||
column_name: col.name,
|
||||
data_type: col.dataType,
|
||||
is_nullable: col.isNullable ? 'YES' : 'NO',
|
||||
column_default: col.defaultValue || null,
|
||||
};
|
||||
console.log(`[BatchManagementService] MySQL/MariaDB 구조로 변환:`, result);
|
||||
return result;
|
||||
}
|
||||
// PostgreSQL/Oracle/MSSQL/MariaDB 구조: {column_name, data_type, is_nullable, column_default}
|
||||
else {
|
||||
const result = {
|
||||
column_name: col.column_name || col.COLUMN_NAME,
|
||||
data_type: col.data_type || col.DATA_TYPE,
|
||||
is_nullable: col.is_nullable || col.IS_NULLABLE || (col.nullable === 'Y' ? 'YES' : 'NO'),
|
||||
column_default: col.column_default || col.COLUMN_DEFAULT || null,
|
||||
};
|
||||
console.log(`[BatchManagementService] 표준 구조로 변환:`, result);
|
||||
return result;
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`[BatchManagementService] 표준화된 컬럼 목록:`, standardizedColumns);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: standardizedColumns,
|
||||
message: "컬럼 정보를 조회했습니다."
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("[BatchManagementService] 외부 DB 컬럼 정보 조회 오류:", error);
|
||||
console.error("[BatchManagementService] 오류 스택:", error instanceof Error ? error.stack : 'No stack trace');
|
||||
return {
|
||||
success: false,
|
||||
message: "컬럼 정보 조회 중 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "알 수 없는 오류"
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user