import { query } from "../database/db"; import { DataMappingConfig, InboundMapping, OutboundMapping, FieldMapping, DataMappingResult, MappingValidationResult, FieldTransform, DataType, } from "../types/dataMappingTypes"; export class DataMappingService { constructor() { // No prisma instance needed } /** * Inbound 데이터 매핑 처리 (외부 → 내부) */ async processInboundData( externalData: any, mapping: InboundMapping ): Promise { 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 { 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> { const mappedRecord: Record = {}; 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> { const mappedRecord: Record = {}; 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 { 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 { // 추후 확장 가능한 커스텀 함수들 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, mapping: InboundMapping ): Promise { 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 { 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}`); const result = await query(sql, []); return result; } catch (error) { console.error( `❌ [DataMappingService] 소스 데이터 조회 실패 (${tableName}):`, error ); throw error; } } /** * INSERT 실행 */ private async executeInsert( tableName: string, data: Record ): Promise { 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})`; console.log(`📝 [DataMappingService] INSERT 실행:`, { table: tableName, columns, query: sql, }); await query(sql, values); } /** * UPSERT 실행 */ private async executeUpsert( tableName: string, data: Record, keyFields: string[] ): Promise { 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 = ` INSERT INTO ${tableName} (${columns.join(", ")}) VALUES (${placeholders}) ON CONFLICT (${keyFields.join(", ")}) DO UPDATE SET ${updateClauses} `; console.log(`🔄 [DataMappingService] UPSERT 실행:`, { table: tableName, keyFields, query: sql, }); await query(sql, values); } /** * UPDATE 실행 */ private async executeUpdate( tableName: string, data: Record, keyFields: string[] ): Promise { 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}`; console.log(`✏️ [DataMappingService] UPDATE 실행:`, { table: tableName, keyFields, query: sql, }); await query(sql, values); } /** * 매핑 설정 검증 */ 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 { // No disconnect needed for raw queries } }