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

574 lines
15 KiB
TypeScript
Raw Normal View History

import { query } from "../database/db";
2025-09-26 17:52:11 +09:00
import {
DataMappingConfig,
InboundMapping,
OutboundMapping,
FieldMapping,
DataMappingResult,
MappingValidationResult,
FieldTransform,
DataType,
} from "../types/dataMappingTypes";
export class DataMappingService {
constructor() {
// No prisma instance needed
2025-09-26 17:52:11 +09:00
}
/**
* Inbound ( )
*/
async processInboundData(
externalData: any,
mapping: InboundMapping
): Promise<DataMappingResult> {
const startTime = Date.now();
const result: DataMappingResult = {
success: false,
direction: "inbound",
recordsProcessed: 0,
recordsInserted: 0,
recordsUpdated: 0,
recordsSkipped: 0,
errors: [],
executionTime: 0,
timestamp: new Date().toISOString(),
};
try {
console.log(`📥 [DataMappingService] Inbound 매핑 시작:`, {
targetTable: mapping.targetTable,
insertMode: mapping.insertMode,
fieldMappings: mapping.fieldMappings.length,
});
// 데이터 배열로 변환
const dataArray = Array.isArray(externalData)
? externalData
: [externalData];
result.recordsProcessed = dataArray.length;
// 각 레코드 처리
for (const record of dataArray) {
try {
const mappedData = await this.mapInboundRecord(record, mapping);
if (Object.keys(mappedData).length === 0) {
result.recordsSkipped!++;
continue;
}
// 데이터베이스에 저장
await this.saveInboundRecord(mappedData, mapping);
if (mapping.insertMode === "insert") {
result.recordsInserted!++;
} else {
result.recordsUpdated!++;
}
} catch (error) {
console.error(`❌ [DataMappingService] 레코드 처리 실패:`, error);
result.errors!.push(
`레코드 처리 실패: ${error instanceof Error ? error.message : String(error)}`
);
result.recordsSkipped!++;
}
}
result.success =
result.errors!.length === 0 ||
result.recordsInserted! > 0 ||
result.recordsUpdated! > 0;
} catch (error) {
console.error(`❌ [DataMappingService] Inbound 매핑 실패:`, error);
result.errors!.push(
`매핑 처리 실패: ${error instanceof Error ? error.message : String(error)}`
);
}
result.executionTime = Date.now() - startTime;
console.log(`✅ [DataMappingService] Inbound 매핑 완료:`, result);
return result;
}
/**
* Outbound ( )
*/
async processOutboundData(
mapping: OutboundMapping,
filter?: any
): Promise<any> {
console.log(`📤 [DataMappingService] Outbound 매핑 시작:`, {
sourceTable: mapping.sourceTable,
fieldMappings: mapping.fieldMappings.length,
filter,
});
try {
// 소스 데이터 조회
const sourceData = await this.getSourceData(mapping, filter);
if (
!sourceData ||
(Array.isArray(sourceData) && sourceData.length === 0)
) {
console.log(`⚠️ [DataMappingService] 소스 데이터가 없습니다.`);
return null;
}
// 데이터 매핑
const mappedData = Array.isArray(sourceData)
? await Promise.all(
sourceData.map((record) => this.mapOutboundRecord(record, mapping))
)
: await this.mapOutboundRecord(sourceData, mapping);
console.log(`✅ [DataMappingService] Outbound 매핑 완료:`, {
recordCount: Array.isArray(mappedData) ? mappedData.length : 1,
});
return mappedData;
} catch (error) {
console.error(`❌ [DataMappingService] Outbound 매핑 실패:`, error);
throw error;
}
}
/**
* Inbound
*/
private async mapInboundRecord(
sourceRecord: any,
mapping: InboundMapping
): Promise<Record<string, any>> {
const mappedRecord: Record<string, any> = {};
for (const fieldMapping of mapping.fieldMappings) {
try {
const sourceValue = sourceRecord[fieldMapping.sourceField];
// 필수 필드 체크
if (
fieldMapping.required &&
(sourceValue === undefined || sourceValue === null)
) {
if (fieldMapping.defaultValue !== undefined) {
mappedRecord[fieldMapping.targetField] = fieldMapping.defaultValue;
} else {
throw new Error(
`필수 필드 '${fieldMapping.sourceField}'가 누락되었습니다.`
);
}
continue;
}
// 값이 없으면 기본값 사용
if (sourceValue === undefined || sourceValue === null) {
if (fieldMapping.defaultValue !== undefined) {
mappedRecord[fieldMapping.targetField] = fieldMapping.defaultValue;
}
continue;
}
// 데이터 변환 적용
const transformedValue = await this.transformFieldValue(
sourceValue,
fieldMapping.dataType,
fieldMapping.transform
);
mappedRecord[fieldMapping.targetField] = transformedValue;
} catch (error) {
console.error(
`❌ [DataMappingService] 필드 매핑 실패 (${fieldMapping.sourceField}${fieldMapping.targetField}):`,
error
);
throw error;
}
}
return mappedRecord;
}
/**
* Outbound
*/
private async mapOutboundRecord(
sourceRecord: any,
mapping: OutboundMapping
): Promise<Record<string, any>> {
const mappedRecord: Record<string, any> = {};
for (const fieldMapping of mapping.fieldMappings) {
try {
const sourceValue = sourceRecord[fieldMapping.sourceField];
// 데이터 변환 적용
const transformedValue = await this.transformFieldValue(
sourceValue,
fieldMapping.dataType,
fieldMapping.transform
);
mappedRecord[fieldMapping.targetField] = transformedValue;
} catch (error) {
console.error(
`❌ [DataMappingService] 필드 매핑 실패 (${fieldMapping.sourceField}${fieldMapping.targetField}):`,
error
);
throw error;
}
}
return mappedRecord;
}
/**
*
*/
private async transformFieldValue(
value: any,
targetDataType: DataType,
transform?: FieldTransform
): Promise<any> {
let transformedValue = value;
// 1. 변환 함수 적용
if (transform) {
switch (transform.type) {
case "constant":
transformedValue = transform.value;
break;
case "format":
if (targetDataType === "date" && transform.format) {
transformedValue = this.formatDate(value, transform.format);
}
break;
case "function":
if (transform.functionName) {
transformedValue = await this.applyCustomFunction(
value,
transform.functionName
);
}
break;
}
}
// 2. 데이터 타입 변환
return this.convertDataType(transformedValue, targetDataType);
}
/**
*
*/
private convertDataType(value: any, targetType: DataType): any {
if (value === null || value === undefined) return value;
switch (targetType) {
case "string":
return String(value);
case "number":
const num = Number(value);
return isNaN(num) ? null : num;
case "boolean":
if (typeof value === "boolean") return value;
if (typeof value === "string") {
return (
value.toLowerCase() === "true" || value === "1" || value === "Y"
);
}
return Boolean(value);
case "date":
return new Date(value);
case "json":
return typeof value === "string" ? JSON.parse(value) : value;
default:
return value;
}
}
/**
*
*/
private formatDate(value: any, format: string): string {
const date = new Date(value);
if (isNaN(date.getTime())) return value;
// 간단한 날짜 포맷 변환
switch (format) {
case "YYYY-MM-DD":
return date.toISOString().split("T")[0];
case "YYYY-MM-DD HH:mm:ss":
return date
.toISOString()
.replace("T", " ")
.replace(/\.\d{3}Z$/, "");
default:
return date.toISOString();
}
}
/**
*
*/
private async applyCustomFunction(
value: any,
functionName: string
): Promise<any> {
// 추후 확장 가능한 커스텀 함수들
switch (functionName) {
case "upperCase":
return String(value).toUpperCase();
case "lowerCase":
return String(value).toLowerCase();
case "trim":
return String(value).trim();
default:
console.warn(
`⚠️ [DataMappingService] 알 수 없는 함수: ${functionName}`
);
return value;
}
}
/**
* Inbound
*/
private async saveInboundRecord(
mappedData: Record<string, any>,
mapping: InboundMapping
): Promise<void> {
const tableName = mapping.targetTable;
try {
switch (mapping.insertMode) {
case "insert":
await this.executeInsert(tableName, mappedData);
break;
case "upsert":
await this.executeUpsert(
tableName,
mappedData,
mapping.keyFields || []
);
break;
case "update":
await this.executeUpdate(
tableName,
mappedData,
mapping.keyFields || []
);
break;
}
} catch (error) {
console.error(
`❌ [DataMappingService] 데이터 저장 실패 (${tableName}):`,
error
);
throw error;
}
}
/**
*
*/
private async getSourceData(
mapping: OutboundMapping,
filter?: any
): Promise<any> {
const tableName = mapping.sourceTable;
try {
// 동적 테이블 쿼리 (Prisma의 경우 런타임에서 제한적)
// 실제 구현에서는 각 테이블별 모델을 사용하거나 Raw SQL을 사용해야 함
let whereClause = {};
if (mapping.sourceFilter) {
// 간단한 필터 파싱 (실제로는 더 정교한 파싱 필요)
console.log(
`🔍 [DataMappingService] 필터 조건: ${mapping.sourceFilter}`
);
// TODO: 필터 조건 파싱 및 적용
}
if (filter) {
whereClause = { ...whereClause, ...filter };
}
// Raw SQL을 사용한 동적 쿼리
const sql = `SELECT * FROM ${tableName}${mapping.sourceFilter ? ` WHERE ${mapping.sourceFilter}` : ""}`;
console.log(`🔍 [DataMappingService] 쿼리 실행: ${sql}`);
2025-09-26 17:52:11 +09:00
const result = await query<any>(sql, []);
2025-09-26 17:52:11 +09:00
return result;
} catch (error) {
console.error(
`❌ [DataMappingService] 소스 데이터 조회 실패 (${tableName}):`,
error
);
throw error;
}
}
/**
* INSERT
*/
private async executeInsert(
tableName: string,
data: Record<string, any>
): Promise<void> {
const columns = Object.keys(data);
const values = Object.values(data);
const placeholders = values.map((_, i) => `$${i + 1}`).join(", ");
const sql = `INSERT INTO ${tableName} (${columns.join(", ")}) VALUES (${placeholders})`;
2025-09-26 17:52:11 +09:00
console.log(`📝 [DataMappingService] INSERT 실행:`, {
table: tableName,
columns,
query: sql,
2025-09-26 17:52:11 +09:00
});
await query(sql, values);
2025-09-26 17:52:11 +09:00
}
/**
* UPSERT
*/
private async executeUpsert(
tableName: string,
data: Record<string, any>,
keyFields: string[]
): Promise<void> {
if (keyFields.length === 0) {
throw new Error("UPSERT 모드에서는 키 필드가 필요합니다.");
}
const columns = Object.keys(data);
const values = Object.values(data);
const placeholders = values.map((_, i) => `$${i + 1}`).join(", ");
const updateClauses = columns
.filter((col) => !keyFields.includes(col))
.map((col) => `${col} = EXCLUDED.${col}`)
.join(", ");
const sql = `
2025-09-26 17:52:11 +09:00
INSERT INTO ${tableName} (${columns.join(", ")})
VALUES (${placeholders})
ON CONFLICT (${keyFields.join(", ")})
DO UPDATE SET ${updateClauses}
`;
console.log(`🔄 [DataMappingService] UPSERT 실행:`, {
table: tableName,
keyFields,
query: sql,
2025-09-26 17:52:11 +09:00
});
await query(sql, values);
2025-09-26 17:52:11 +09:00
}
/**
* UPDATE
*/
private async executeUpdate(
tableName: string,
data: Record<string, any>,
keyFields: string[]
): Promise<void> {
if (keyFields.length === 0) {
throw new Error("UPDATE 모드에서는 키 필드가 필요합니다.");
}
const updateColumns = Object.keys(data).filter(
(col) => !keyFields.includes(col)
);
const updateClauses = updateColumns
.map((col, i) => `${col} = $${i + 1}`)
.join(", ");
const whereConditions = keyFields
.map((field, i) => `${field} = $${updateColumns.length + i + 1}`)
.join(" AND ");
const values = [
...updateColumns.map((col) => data[col]),
...keyFields.map((field) => data[field]),
];
const sql = `UPDATE ${tableName} SET ${updateClauses} WHERE ${whereConditions}`;
2025-09-26 17:52:11 +09:00
console.log(`✏️ [DataMappingService] UPDATE 실행:`, {
table: tableName,
keyFields,
query: sql,
2025-09-26 17:52:11 +09:00
});
await query(sql, values);
2025-09-26 17:52:11 +09:00
}
/**
*
*/
validateMappingConfig(config: DataMappingConfig): MappingValidationResult {
const result: MappingValidationResult = {
isValid: true,
errors: [],
warnings: [],
};
if (config.direction === "none") {
return result;
}
// Inbound 매핑 검증
if (
(config.direction === "inbound" ||
config.direction === "bidirectional") &&
config.inboundMapping
) {
if (!config.inboundMapping.targetTable) {
result.errors.push("Inbound 매핑에 대상 테이블이 필요합니다.");
}
if (config.inboundMapping.fieldMappings.length === 0) {
result.errors.push("Inbound 매핑에 필드 매핑이 필요합니다.");
}
if (
config.inboundMapping.insertMode !== "insert" &&
(!config.inboundMapping.keyFields ||
config.inboundMapping.keyFields.length === 0)
) {
result.errors.push("UPSERT/UPDATE 모드에서는 키 필드가 필요합니다.");
}
}
// Outbound 매핑 검증
if (
(config.direction === "outbound" ||
config.direction === "bidirectional") &&
config.outboundMapping
) {
if (!config.outboundMapping.sourceTable) {
result.errors.push("Outbound 매핑에 소스 테이블이 필요합니다.");
}
if (config.outboundMapping.fieldMappings.length === 0) {
result.errors.push("Outbound 매핑에 필드 매핑이 필요합니다.");
}
}
result.isValid = result.errors.length === 0;
return result;
}
/**
*
*/
async disconnect(): Promise<void> {
// No disconnect needed for raw queries
2025-09-26 17:52:11 +09:00
}
}