ERP-node/backend-node/src/services/batchService.ts

808 lines
26 KiB
TypeScript
Raw Normal View History

2025-09-24 10:46:55 +09:00
// 배치관리 서비스
// 작성일: 2024-12-24
2025-09-25 10:13:43 +09:00
import prisma from "../config/database";
2025-09-25 10:13:43 +09:00
import {
2025-09-24 10:46:55 +09:00
BatchConfig,
BatchMapping,
BatchConfigFilter,
BatchMappingRequest,
BatchValidationResult,
ApiResponse,
ConnectionInfo,
TableInfo,
ColumnInfo,
CreateBatchConfigRequest,
UpdateBatchConfigRequest,
2025-09-24 10:46:55 +09:00
} from "../types/batchTypes";
import { BatchExternalDbService } from "./batchExternalDbService";
2025-09-24 10:46:55 +09:00
import { DbConnectionManager } from "./dbConnectionManager";
2025-09-25 10:13:43 +09:00
export class BatchService {
/**
2025-09-24 10:46:55 +09:00
*
2025-09-25 10:13:43 +09:00
*/
2025-09-24 10:46:55 +09:00
static async getBatchConfigs(
filter: BatchConfigFilter
): Promise<ApiResponse<BatchConfig[]>> {
try {
const where: any = {};
// 필터 조건 적용
if (filter.is_active) {
where.is_active = filter.is_active;
}
if (filter.company_code) {
where.company_code = filter.company_code;
}
// 검색 조건 적용
if (filter.search && filter.search.trim()) {
where.OR = [
{
batch_name: {
contains: filter.search.trim(),
mode: "insensitive",
},
},
{
description: {
contains: filter.search.trim(),
mode: "insensitive",
},
},
];
}
2025-09-25 10:13:43 +09:00
const page = filter.page || 1;
const limit = filter.limit || 10;
const skip = (page - 1) * limit;
const [batchConfigs, total] = await Promise.all([
prisma.batch_configs.findMany({
where,
include: {
batch_mappings: true,
},
orderBy: [{ is_active: "desc" }, { batch_name: "asc" }],
skip,
take: limit,
}),
prisma.batch_configs.count({ where }),
]);
2025-09-24 10:46:55 +09:00
return {
success: true,
data: batchConfigs as BatchConfig[],
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit),
},
2025-09-24 10:46:55 +09:00
};
} catch (error) {
console.error("배치 설정 목록 조회 오류:", error);
return {
success: false,
message: "배치 설정 목록 조회에 실패했습니다.",
error: error instanceof Error ? error.message : "알 수 없는 오류",
2025-09-25 10:13:43 +09:00
};
}
2025-09-24 10:46:55 +09:00
}
2025-09-25 10:13:43 +09:00
2025-09-24 10:46:55 +09:00
/**
*
*/
static async getBatchConfigById(
id: number
): Promise<ApiResponse<BatchConfig>> {
try {
const batchConfig = await prisma.batch_configs.findUnique({
where: { id },
include: {
batch_mappings: {
orderBy: [
{ from_table_name: "asc" },
{ from_column_name: "asc" },
{ mapping_order: "asc" },
],
},
},
});
2025-09-25 10:13:43 +09:00
2025-09-24 10:46:55 +09:00
if (!batchConfig) {
return {
success: false,
message: "배치 설정을 찾을 수 없습니다.",
};
}
2025-09-25 10:13:43 +09:00
2025-09-24 10:46:55 +09:00
return {
success: true,
data: batchConfig as BatchConfig,
};
} catch (error) {
console.error("배치 설정 조회 오류:", error);
return {
success: false,
message: "배치 설정 조회에 실패했습니다.",
error: error instanceof Error ? error.message : "알 수 없는 오류",
};
2025-09-25 10:13:43 +09:00
}
2025-09-24 10:46:55 +09:00
}
2025-09-25 10:13:43 +09:00
2025-09-24 10:46:55 +09:00
/**
*
*/
static async createBatchConfig(
data: CreateBatchConfigRequest,
2025-09-24 10:46:55 +09:00
userId?: string
): Promise<ApiResponse<BatchConfig>> {
try {
// 트랜잭션으로 배치 설정과 매핑 생성
const result = await prisma.$transaction(async (tx) => {
// 배치 설정 생성
const batchConfig = await tx.batch_configs.create({
data: {
batch_name: data.batchName,
2025-09-24 10:46:55 +09:00
description: data.description,
cron_schedule: data.cronSchedule,
2025-09-24 10:46:55 +09:00
created_by: userId,
updated_by: userId,
},
});
2025-09-25 10:13:43 +09:00
2025-09-24 10:46:55 +09:00
// 배치 매핑 생성
const mappings = await Promise.all(
data.mappings.map((mapping, index) =>
tx.batch_mappings.create({
data: {
batch_config_id: batchConfig.id,
from_connection_type: mapping.from_connection_type,
from_connection_id: mapping.from_connection_id,
from_table_name: mapping.from_table_name,
from_column_name: mapping.from_column_name,
from_column_type: mapping.from_column_type,
2025-09-26 17:29:20 +09:00
from_api_url: mapping.from_api_url,
from_api_key: mapping.from_api_key,
from_api_method: mapping.from_api_method,
2025-09-24 10:46:55 +09:00
to_connection_type: mapping.to_connection_type,
to_connection_id: mapping.to_connection_id,
to_table_name: mapping.to_table_name,
to_column_name: mapping.to_column_name,
to_column_type: mapping.to_column_type,
2025-09-26 17:29:20 +09:00
to_api_url: mapping.to_api_url,
to_api_key: mapping.to_api_key,
to_api_method: mapping.to_api_method,
// to_api_body: mapping.to_api_body, // Request Body 템플릿 추가 - 임시 주석 처리
2025-09-24 10:46:55 +09:00
mapping_order: mapping.mapping_order || index + 1,
created_by: userId,
},
})
)
);
return {
...batchConfig,
batch_mappings: mappings,
};
});
return {
success: true,
data: result as BatchConfig,
message: "배치 설정이 성공적으로 생성되었습니다.",
};
} catch (error) {
console.error("배치 설정 생성 오류:", error);
return {
success: false,
message: "배치 설정 생성에 실패했습니다.",
error: error instanceof Error ? error.message : "알 수 없는 오류",
};
}
2025-09-25 10:13:43 +09:00
}
/**
2025-09-24 10:46:55 +09:00
*
2025-09-25 10:13:43 +09:00
*/
2025-09-24 10:46:55 +09:00
static async updateBatchConfig(
id: number,
data: UpdateBatchConfigRequest,
2025-09-24 10:46:55 +09:00
userId?: string
): Promise<ApiResponse<BatchConfig>> {
try {
// 기존 배치 설정 확인
const existingConfig = await prisma.batch_configs.findUnique({
where: { id },
include: { batch_mappings: true },
});
if (!existingConfig) {
return {
success: false,
message: "배치 설정을 찾을 수 없습니다.",
};
}
2025-09-25 10:13:43 +09:00
2025-09-24 10:46:55 +09:00
// 트랜잭션으로 업데이트
const result = await prisma.$transaction(async (tx) => {
// 배치 설정 업데이트
const updateData: any = {
updated_by: userId,
};
if (data.batchName) updateData.batch_name = data.batchName;
2025-09-24 10:46:55 +09:00
if (data.description !== undefined) updateData.description = data.description;
if (data.cronSchedule) updateData.cron_schedule = data.cronSchedule;
if (data.isActive !== undefined) updateData.is_active = data.isActive;
2025-09-24 10:46:55 +09:00
const batchConfig = await tx.batch_configs.update({
where: { id },
data: updateData,
});
2025-09-25 10:13:43 +09:00
2025-09-24 10:46:55 +09:00
// 매핑이 제공된 경우 기존 매핑 삭제 후 새로 생성
if (data.mappings) {
await tx.batch_mappings.deleteMany({
where: { batch_config_id: id },
});
const mappings = await Promise.all(
data.mappings.map((mapping, index) =>
tx.batch_mappings.create({
data: {
batch_config_id: id,
from_connection_type: mapping.from_connection_type,
from_connection_id: mapping.from_connection_id,
from_table_name: mapping.from_table_name,
from_column_name: mapping.from_column_name,
from_column_type: mapping.from_column_type,
to_connection_type: mapping.to_connection_type,
to_connection_id: mapping.to_connection_id,
to_table_name: mapping.to_table_name,
to_column_name: mapping.to_column_name,
to_column_type: mapping.to_column_type,
mapping_order: mapping.mapping_order || index + 1,
created_by: userId,
},
})
)
);
return {
...batchConfig,
batch_mappings: mappings,
};
} else {
return {
...batchConfig,
batch_mappings: existingConfig.batch_mappings,
};
}
});
return {
success: true,
data: result as BatchConfig,
message: "배치 설정이 성공적으로 수정되었습니다.",
};
} catch (error) {
console.error("배치 설정 수정 오류:", error);
return {
success: false,
message: "배치 설정 수정에 실패했습니다.",
error: error instanceof Error ? error.message : "알 수 없는 오류",
};
}
2025-09-25 10:13:43 +09:00
}
/**
2025-09-24 10:46:55 +09:00
* ( )
2025-09-25 10:13:43 +09:00
*/
2025-09-24 10:46:55 +09:00
static async deleteBatchConfig(
id: number,
userId?: string
): Promise<ApiResponse<void>> {
try {
const existingConfig = await prisma.batch_configs.findUnique({
where: { id },
});
if (!existingConfig) {
return {
success: false,
message: "배치 설정을 찾을 수 없습니다.",
};
}
2025-09-25 10:13:43 +09:00
2025-09-26 17:29:20 +09:00
// 배치 매핑 먼저 삭제 (외래키 제약)
await prisma.batch_mappings.deleteMany({
where: { batch_config_id: id }
});
// 배치 설정 삭제
await prisma.batch_configs.delete({
where: { id }
2025-09-24 10:46:55 +09:00
});
return {
success: true,
message: "배치 설정이 성공적으로 삭제되었습니다.",
};
} catch (error) {
console.error("배치 설정 삭제 오류:", error);
return {
success: false,
message: "배치 설정 삭제에 실패했습니다.",
error: error instanceof Error ? error.message : "알 수 없는 오류",
};
}
2025-09-25 10:13:43 +09:00
}
/**
2025-09-24 10:46:55 +09:00
*
2025-09-25 10:13:43 +09:00
*/
2025-09-24 10:46:55 +09:00
static async getAvailableConnections(): Promise<ApiResponse<ConnectionInfo[]>> {
try {
const connections: ConnectionInfo[] = [];
// 내부 DB 추가
connections.push({
type: 'internal',
name: 'Internal Database',
db_type: 'postgresql',
});
// 외부 DB 연결 조회
const externalConnections = await BatchExternalDbService.getAvailableConnections();
2025-09-24 10:46:55 +09:00
if (externalConnections.success && externalConnections.data) {
externalConnections.data.forEach((conn) => {
connections.push({
type: 'external',
id: conn.id,
name: conn.name,
2025-09-24 10:46:55 +09:00
db_type: conn.db_type,
});
});
}
2025-09-25 10:13:43 +09:00
2025-09-24 10:46:55 +09:00
return {
success: true,
data: connections,
};
} catch (error) {
console.error("커넥션 목록 조회 오류:", error);
return {
success: false,
message: "커넥션 목록 조회에 실패했습니다.",
error: error instanceof Error ? error.message : "알 수 없는 오류",
};
2025-09-25 10:13:43 +09:00
}
2025-09-24 10:46:55 +09:00
}
2025-09-25 10:13:43 +09:00
2025-09-24 10:46:55 +09:00
/**
*
*/
static async getTablesFromConnection(
connectionType: 'internal' | 'external',
connectionId?: number
): Promise<ApiResponse<TableInfo[]>> {
2025-09-24 10:46:55 +09:00
try {
let tables: TableInfo[] = [];
2025-09-24 10:46:55 +09:00
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: []
}));
2025-09-24 10:46:55 +09:00
} else if (connectionType === 'external' && connectionId) {
// 외부 DB 테이블 조회
const tablesResult = await BatchExternalDbService.getTablesFromConnection(connectionType, connectionId);
2025-09-24 10:46:55 +09:00
if (tablesResult.success && tablesResult.data) {
tables = tablesResult.data;
}
}
2025-09-25 10:13:43 +09:00
2025-09-24 10:46:55 +09:00
return {
success: true,
data: tables,
};
} catch (error) {
console.error("테이블 목록 조회 오류:", error);
return {
success: false,
message: "테이블 목록 조회에 실패했습니다.",
error: error instanceof Error ? error.message : "알 수 없는 오류",
};
}
2025-09-25 10:13:43 +09:00
}
/**
2025-09-24 10:46:55 +09:00
*
2025-09-25 10:13:43 +09:00
*/
2025-09-24 10:46:55 +09:00
static async getTableColumns(
connectionType: 'internal' | 'external',
connectionId: number | undefined,
tableName: string
2025-09-24 10:46:55 +09:00
): Promise<ApiResponse<ColumnInfo[]>> {
try {
console.log(`[BatchService] getTableColumns 호출:`, {
connectionType,
connectionId,
tableName
});
2025-09-24 10:46:55 +09:00
let columns: ColumnInfo[] = [];
2025-09-24 10:46:55 +09:00
if (connectionType === 'internal') {
// 내부 DB 컬럼 조회
console.log(`[BatchService] 내부 DB 컬럼 조회 시작: ${tableName}`);
2025-09-24 10:46:55 +09:00
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(`[BatchService] 내부 DB 컬럼 조회 결과:`, result);
2025-09-24 10:46:55 +09:00
columns = result.map(row => ({
column_name: row.column_name,
data_type: row.data_type,
is_nullable: row.is_nullable,
2025-09-24 10:46:55 +09:00
column_default: row.column_default,
}));
} else if (connectionType === 'external' && connectionId) {
// 외부 DB 컬럼 조회
console.log(`[BatchService] 외부 DB 컬럼 조회 시작: connectionId=${connectionId}, tableName=${tableName}`);
const columnsResult = await BatchExternalDbService.getTableColumns(
connectionType,
2025-09-24 10:46:55 +09:00
connectionId,
tableName
);
console.log(`[BatchService] 외부 DB 컬럼 조회 결과:`, columnsResult);
2025-09-24 10:46:55 +09:00
if (columnsResult.success && columnsResult.data) {
columns = columnsResult.data;
2025-09-24 10:46:55 +09:00
}
console.log(`[BatchService] 외부 DB 컬럼:`, columns);
2025-09-24 10:46:55 +09:00
}
return {
success: true,
data: columns,
};
} catch (error) {
console.error("컬럼 정보 조회 오류:", error);
return {
success: false,
message: "컬럼 정보 조회에 실패했습니다.",
error: error instanceof Error ? error.message : "알 수 없는 오류",
};
}
2025-09-25 10:13:43 +09:00
}
/**
*
2025-09-25 10:13:43 +09:00
*/
static async createExecutionLog(data: {
batch_config_id: number;
execution_status: string;
start_time: Date;
total_records: number;
success_records: number;
failed_records: number;
}): Promise<any> {
try {
const executionLog = await prisma.batch_execution_logs.create({
data: {
batch_config_id: data.batch_config_id,
execution_status: data.execution_status,
start_time: data.start_time,
total_records: data.total_records,
success_records: data.success_records,
failed_records: data.failed_records,
},
});
2025-09-25 10:13:43 +09:00
return executionLog;
} catch (error) {
console.error("배치 실행 로그 생성 오류:", error);
throw error;
2025-09-25 10:13:43 +09:00
}
}
2025-09-25 10:13:43 +09:00
/**
*
*/
static async updateExecutionLog(
id: number,
data: {
execution_status?: string;
end_time?: Date;
duration_ms?: number;
total_records?: number;
success_records?: number;
failed_records?: number;
error_message?: string;
2025-09-25 10:13:43 +09:00
}
): Promise<void> {
try {
await prisma.batch_execution_logs.update({
where: { id },
data,
});
} catch (error) {
console.error("배치 실행 로그 업데이트 오류:", error);
throw error;
}
}
2025-09-25 10:13:43 +09:00
/**
* ( / DB )
*/
static async getDataFromTable(
tableName: string,
connectionType: 'internal' | 'external' = 'internal',
connectionId?: number
): Promise<any[]> {
try {
console.log(`[BatchService] 테이블에서 데이터 조회: ${tableName} (${connectionType}${connectionId ? `:${connectionId}` : ''})`);
if (connectionType === 'internal') {
// 내부 DB에서 데이터 조회
const result = await prisma.$queryRawUnsafe(`SELECT * FROM ${tableName} LIMIT 100`);
console.log(`[BatchService] 내부 DB 데이터 조회 결과: ${Array.isArray(result) ? result.length : 0}개 레코드`);
return result as any[];
} else if (connectionType === 'external' && connectionId) {
// 외부 DB에서 데이터 조회
const result = await BatchExternalDbService.getDataFromTable(connectionId, tableName);
if (result.success && result.data) {
console.log(`[BatchService] 외부 DB 데이터 조회 결과: ${result.data.length}개 레코드`);
return result.data;
} else {
console.error(`외부 DB 데이터 조회 실패: ${result.message}`);
return [];
}
} else {
throw new Error(`잘못된 연결 타입 또는 연결 ID: ${connectionType}, ${connectionId}`);
2025-09-25 10:13:43 +09:00
}
} catch (error) {
console.error(`테이블 데이터 조회 오류 (${tableName}):`, error);
throw error;
}
2025-09-25 10:13:43 +09:00
}
/**
* ( / DB )
2025-09-25 10:13:43 +09:00
*/
static async getDataFromTableWithColumns(
tableName: string,
columns: string[],
connectionType: 'internal' | 'external' = 'internal',
connectionId?: number
): Promise<any[]> {
try {
console.log(`[BatchService] 테이블에서 특정 컬럼 데이터 조회: ${tableName} (${columns.join(', ')}) (${connectionType}${connectionId ? `:${connectionId}` : ''})`);
if (connectionType === 'internal') {
// 내부 DB에서 특정 컬럼만 조회
const columnList = columns.join(', ');
const result = await prisma.$queryRawUnsafe(`SELECT ${columnList} FROM ${tableName} LIMIT 100`);
console.log(`[BatchService] 내부 DB 특정 컬럼 조회 결과: ${Array.isArray(result) ? result.length : 0}개 레코드`);
return result as any[];
} else if (connectionType === 'external' && connectionId) {
// 외부 DB에서 특정 컬럼만 조회
const result = await BatchExternalDbService.getDataFromTableWithColumns(connectionId, tableName, columns);
if (result.success && result.data) {
console.log(`[BatchService] 외부 DB 특정 컬럼 조회 결과: ${result.data.length}개 레코드`);
return result.data;
} else {
console.error(`외부 DB 특정 컬럼 조회 실패: ${result.message}`);
return [];
}
} else {
throw new Error(`잘못된 연결 타입 또는 연결 ID: ${connectionType}, ${connectionId}`);
}
} catch (error) {
console.error(`테이블 특정 컬럼 조회 오류 (${tableName}):`, error);
throw error;
2025-09-25 10:13:43 +09:00
}
}
2025-09-25 10:13:43 +09:00
/**
* ( / DB )
*/
static async insertDataToTable(
tableName: string,
data: any[],
connectionType: 'internal' | 'external' = 'internal',
connectionId?: number
): Promise<{
successCount: number;
failedCount: number;
}> {
try {
console.log(`[BatchService] 테이블에 데이터 삽입: ${tableName} (${connectionType}${connectionId ? `:${connectionId}` : ''}), ${data.length}개 레코드`);
if (!data || data.length === 0) {
return { successCount: 0, failedCount: 0 };
}
2025-09-25 10:13:43 +09:00
if (connectionType === 'internal') {
// 내부 DB에 데이터 삽입
let successCount = 0;
let failedCount = 0;
// 각 레코드를 개별적으로 삽입 (UPSERT 방식으로 중복 처리)
for (const record of data) {
try {
// 동적 UPSERT 쿼리 생성 (PostgreSQL ON CONFLICT 사용)
const columns = Object.keys(record);
const values = Object.values(record).map(value => {
// Date 객체를 ISO 문자열로 변환 (PostgreSQL이 자동으로 파싱)
if (value instanceof Date) {
return value.toISOString();
}
// JavaScript Date 문자열을 Date 객체로 변환 후 ISO 문자열로
if (typeof value === 'string') {
const dateRegex = /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s+(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+\d{2}\s+\d{4}\s+\d{2}:\d{2}:\d{2}/;
if (dateRegex.test(value)) {
return new Date(value).toISOString();
}
// ISO 날짜 문자열 형식 체크 (2025-09-24T06:29:01.351Z)
const isoDateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z?$/;
if (isoDateRegex.test(value)) {
return new Date(value).toISOString();
}
}
return value;
});
// PostgreSQL 타입 캐스팅을 위한 placeholder 생성
const placeholders = columns.map((col, index) => {
// 날짜/시간 관련 컬럼명 패턴 체크
if (col.toLowerCase().includes('date') ||
col.toLowerCase().includes('time') ||
col.toLowerCase().includes('created') ||
col.toLowerCase().includes('updated') ||
col.toLowerCase().includes('reg')) {
return `$${index + 1}::timestamp`;
}
return `$${index + 1}`;
}).join(', ');
// Primary Key 컬럼 추정 (일반적으로 id 또는 첫 번째 컬럼)
const primaryKeyColumn = columns.includes('id') ? 'id' :
columns.includes('user_id') ? 'user_id' :
columns[0];
// UPDATE SET 절 생성 (Primary Key 제외)
const updateColumns = columns.filter(col => col !== primaryKeyColumn);
const updateSet = updateColumns.map(col => `${col} = EXCLUDED.${col}`).join(', ');
let query: string;
if (updateSet) {
// UPSERT: 중복 시 업데이트
query = `INSERT INTO ${tableName} (${columns.join(', ')}) VALUES (${placeholders})
ON CONFLICT (${primaryKeyColumn}) DO UPDATE SET ${updateSet}`;
} else {
// Primary Key만 있는 경우 중복 시 무시
query = `INSERT INTO ${tableName} (${columns.join(', ')}) VALUES (${placeholders})
ON CONFLICT (${primaryKeyColumn}) DO NOTHING`;
}
await prisma.$executeRawUnsafe(query, ...values);
successCount++;
} catch (error) {
console.error(`레코드 UPSERT 실패:`, error);
failedCount++;
}
}
console.log(`[BatchService] 내부 DB 데이터 삽입 완료: 성공 ${successCount}개, 실패 ${failedCount}`);
return { successCount, failedCount };
} else if (connectionType === 'external' && connectionId) {
// 외부 DB에 데이터 삽입
const result = await BatchExternalDbService.insertDataToTable(connectionId, tableName, data);
if (result.success && result.data) {
console.log(`[BatchService] 외부 DB 데이터 삽입 완료: 성공 ${result.data.successCount}개, 실패 ${result.data.failedCount}`);
return result.data;
} else {
console.error(`외부 DB 데이터 삽입 실패: ${result.message}`);
return { successCount: 0, failedCount: data.length };
}
} else {
2025-09-26 17:29:20 +09:00
console.log(`[BatchService] 연결 정보 디버그:`, { connectionType, connectionId });
throw new Error(`잘못된 연결 타입 또는 연결 ID: ${connectionType}, ${connectionId}`);
}
} catch (error) {
console.error(`테이블 데이터 삽입 오류 (${tableName}):`, error);
throw error;
}
2025-09-25 10:13:43 +09:00
}
/**
2025-09-24 10:46:55 +09:00
*
2025-09-25 10:13:43 +09:00
*/
2025-09-24 10:46:55 +09:00
private static async validateBatchMappings(
mappings: BatchMapping[]
): Promise<BatchValidationResult> {
const errors: string[] = [];
const warnings: string[] = [];
if (!mappings || mappings.length === 0) {
errors.push("최소 하나 이상의 매핑이 필요합니다.");
return { isValid: false, errors, warnings };
}
2025-09-25 10:13:43 +09:00
2025-09-24 10:46:55 +09:00
// n:1 매핑 검사 (여러 FROM이 같은 TO로 매핑되는 것 방지)
const toMappings = new Map<string, number>();
mappings.forEach((mapping, index) => {
const toKey = `${mapping.to_connection_type}:${mapping.to_connection_id || 'internal'}:${mapping.to_table_name}:${mapping.to_column_name}`;
if (toMappings.has(toKey)) {
errors.push(
`매핑 ${index + 1}: TO 컬럼 '${mapping.to_table_name}.${mapping.to_column_name}'에 중복 매핑이 있습니다. n:1 매핑은 허용되지 않습니다.`
);
} else {
toMappings.set(toKey, index);
}
2025-09-25 10:13:43 +09:00
});
2025-09-24 10:46:55 +09:00
// 1:n 매핑 경고 (같은 FROM에서 여러 TO로 매핑)
const fromMappings = new Map<string, number[]>();
mappings.forEach((mapping, index) => {
const fromKey = `${mapping.from_connection_type}:${mapping.from_connection_id || 'internal'}:${mapping.from_table_name}:${mapping.from_column_name}`;
if (!fromMappings.has(fromKey)) {
fromMappings.set(fromKey, []);
}
fromMappings.get(fromKey)!.push(index);
2025-09-25 10:13:43 +09:00
});
2025-09-24 10:46:55 +09:00
fromMappings.forEach((indices, fromKey) => {
if (indices.length > 1) {
const [, , tableName, columnName] = fromKey.split(':');
warnings.push(
`FROM 컬럼 '${tableName}.${columnName}'에서 ${indices.length}개의 TO 컬럼으로 매핑됩니다. (1:n 매핑)`
);
}
2025-09-25 10:13:43 +09:00
});
return {
2025-09-24 10:46:55 +09:00
isValid: errors.length === 0,
errors,
warnings,
2025-09-25 10:13:43 +09:00
};
}
}