import { getPool } from "../database/db"; import { logger } from "../utils/logger"; import crypto from "crypto"; export interface ExcelMappingTemplate { id?: number; tableName: string; excelColumns: string[]; excelColumnsHash: string; columnMappings: Record; // { "엑셀컬럼": "시스템컬럼" } companyCode: string; createdDate?: Date; updatedDate?: Date; } class ExcelMappingService { /** * 엑셀 컬럼 목록으로 해시 생성 * 정렬 후 MD5 해시 생성하여 동일한 컬럼 구조 식별 */ generateColumnsHash(columns: string[]): string { // 컬럼 목록을 정렬하여 순서와 무관하게 동일한 해시 생성 const sortedColumns = [...columns].sort(); const columnsString = sortedColumns.join("|"); return crypto.createHash("md5").update(columnsString).digest("hex"); } /** * 엑셀 컬럼 구조로 매핑 템플릿 조회 * 동일한 컬럼 구조가 있으면 기존 매핑 반환 */ async findMappingByColumns( tableName: string, excelColumns: string[], companyCode: string ): Promise { try { const hash = this.generateColumnsHash(excelColumns); logger.info("엑셀 매핑 템플릿 조회", { tableName, excelColumns, hash, companyCode, }); const pool = getPool(); // 회사별 매핑 먼저 조회, 없으면 공통(*) 매핑 조회 let query: string; let params: any[]; if (companyCode === "*") { query = ` SELECT id, table_name as "tableName", excel_columns as "excelColumns", excel_columns_hash as "excelColumnsHash", column_mappings as "columnMappings", company_code as "companyCode", created_date as "createdDate", updated_date as "updatedDate" FROM excel_mapping_template WHERE table_name = $1 AND excel_columns_hash = $2 ORDER BY updated_date DESC LIMIT 1 `; params = [tableName, hash]; } else { query = ` SELECT id, table_name as "tableName", excel_columns as "excelColumns", excel_columns_hash as "excelColumnsHash", column_mappings as "columnMappings", company_code as "companyCode", created_date as "createdDate", updated_date as "updatedDate" FROM excel_mapping_template WHERE table_name = $1 AND excel_columns_hash = $2 AND (company_code = $3 OR company_code = '*') ORDER BY CASE WHEN company_code = $3 THEN 0 ELSE 1 END, updated_date DESC LIMIT 1 `; params = [tableName, hash, companyCode]; } const result = await pool.query(query, params); if (result.rows.length > 0) { logger.info("기존 매핑 템플릿 발견", { id: result.rows[0].id, tableName, }); return result.rows[0]; } logger.info("매핑 템플릿 없음 - 새 구조", { tableName, hash }); return null; } catch (error: any) { logger.error(`매핑 템플릿 조회 실패: ${error.message}`, { error }); throw error; } } /** * 매핑 템플릿 저장 (UPSERT) * 동일한 테이블+컬럼구조+회사코드가 있으면 업데이트, 없으면 삽입 */ async saveMappingTemplate( tableName: string, excelColumns: string[], columnMappings: Record, companyCode: string, userId?: string ): Promise { try { const hash = this.generateColumnsHash(excelColumns); logger.info("엑셀 매핑 템플릿 저장 (UPSERT)", { tableName, excelColumns, hash, columnMappings, companyCode, }); const pool = getPool(); const query = ` INSERT INTO excel_mapping_template ( table_name, excel_columns, excel_columns_hash, column_mappings, company_code, created_date, updated_date ) VALUES ($1, $2, $3, $4, $5, NOW(), NOW()) ON CONFLICT (table_name, excel_columns_hash, company_code) DO UPDATE SET column_mappings = EXCLUDED.column_mappings, updated_date = NOW() RETURNING id, table_name as "tableName", excel_columns as "excelColumns", excel_columns_hash as "excelColumnsHash", column_mappings as "columnMappings", company_code as "companyCode", created_date as "createdDate", updated_date as "updatedDate" `; const result = await pool.query(query, [ tableName, excelColumns, hash, JSON.stringify(columnMappings), companyCode, ]); logger.info("매핑 템플릿 저장 완료", { id: result.rows[0].id, tableName, hash, }); return result.rows[0]; } catch (error: any) { logger.error(`매핑 템플릿 저장 실패: ${error.message}`, { error }); throw error; } } /** * 테이블의 모든 매핑 템플릿 조회 */ async getMappingTemplates( tableName: string, companyCode: string ): Promise { try { logger.info("테이블 매핑 템플릿 목록 조회", { tableName, companyCode }); const pool = getPool(); let query: string; let params: any[]; if (companyCode === "*") { query = ` SELECT id, table_name as "tableName", excel_columns as "excelColumns", excel_columns_hash as "excelColumnsHash", column_mappings as "columnMappings", company_code as "companyCode", created_date as "createdDate", updated_date as "updatedDate" FROM excel_mapping_template WHERE table_name = $1 ORDER BY updated_date DESC `; params = [tableName]; } else { query = ` SELECT id, table_name as "tableName", excel_columns as "excelColumns", excel_columns_hash as "excelColumnsHash", column_mappings as "columnMappings", company_code as "companyCode", created_date as "createdDate", updated_date as "updatedDate" FROM excel_mapping_template WHERE table_name = $1 AND (company_code = $2 OR company_code = '*') ORDER BY updated_date DESC `; params = [tableName, companyCode]; } const result = await pool.query(query, params); logger.info(`매핑 템플릿 ${result.rows.length}개 조회`, { tableName }); return result.rows; } catch (error: any) { logger.error(`매핑 템플릿 목록 조회 실패: ${error.message}`, { error }); throw error; } } /** * 매핑 템플릿 삭제 */ async deleteMappingTemplate( id: number, companyCode: string ): Promise { try { logger.info("매핑 템플릿 삭제", { id, companyCode }); const pool = getPool(); let query: string; let params: any[]; if (companyCode === "*") { query = `DELETE FROM excel_mapping_template WHERE id = $1`; params = [id]; } else { query = `DELETE FROM excel_mapping_template WHERE id = $1 AND company_code = $2`; params = [id, companyCode]; } const result = await pool.query(query, params); if (result.rowCount && result.rowCount > 0) { logger.info("매핑 템플릿 삭제 완료", { id }); return true; } logger.warn("삭제할 매핑 템플릿 없음", { id, companyCode }); return false; } catch (error: any) { logger.error(`매핑 템플릿 삭제 실패: ${error.message}`, { error }); throw error; } } } export default new ExcelMappingService();