371 lines
13 KiB
TypeScript
371 lines
13 KiB
TypeScript
|
|
// 배치관리 전용 서비스 (기존 소스와 완전 분리)
|
||
|
|
// 작성일: 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 : "알 수 없는 오류"
|
||
|
|
};
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|