/** * 데이터 매핑 유틸리티 * 화면 간 데이터 전달 시 매핑 규칙 적용 */ import type { MappingRule, Condition, TransformFunction, } from "@/types/screen-embedding"; import { logger } from "./logger"; /** * 매핑 규칙 적용 * @param data 배열 또는 단일 객체 * @param rules 매핑 규칙 배열 * @returns 매핑된 배열 */ export function applyMappingRules(data: any[] | any, rules: MappingRule[]): any[] { // 빈 데이터 처리 if (!data) { return []; } // 🆕 배열이 아닌 경우 배열로 변환 const dataArray = Array.isArray(data) ? data : [data]; if (dataArray.length === 0) { return []; } // 규칙이 없으면 원본 데이터 반환 if (!rules || rules.length === 0) { return dataArray; } // 변환 함수가 있는 규칙 확인 const hasTransform = rules.some((rule) => rule.transform && rule.transform !== "none"); if (hasTransform) { // 변환 함수가 있으면 단일 값 또는 집계 결과 반환 return [applyTransformRules(dataArray, rules)]; } // 일반 매핑 (각 행에 대해 매핑) // 🆕 원본 데이터를 복사한 후 매핑 규칙 적용 (매핑되지 않은 필드도 유지) return dataArray.map((row) => { // 원본 데이터 복사 const mappedRow: any = { ...row }; for (const rule of rules) { // sourceField와 targetField가 모두 있어야 매핑 적용 if (!rule.sourceField || !rule.targetField) { continue; } const sourceValue = getNestedValue(row, rule.sourceField); const targetValue = sourceValue ?? rule.defaultValue; // 소스 필드와 타겟 필드가 다르면 소스 필드 제거 후 타겟 필드에 설정 if (rule.sourceField !== rule.targetField) { delete mappedRow[rule.sourceField]; } setNestedValue(mappedRow, rule.targetField, targetValue); } return mappedRow; }); } /** * 변환 함수 적용 */ function applyTransformRules(data: any[], rules: MappingRule[]): any { const result: any = {}; for (const rule of rules) { const values = data.map((row) => getNestedValue(row, rule.sourceField)); const transformedValue = applyTransform(values, rule.transform || "none"); setNestedValue(result, rule.targetField, transformedValue); } return result; } /** * 변환 함수 실행 */ function applyTransform(values: any[], transform: TransformFunction): any { switch (transform) { case "none": return values; case "sum": return values.reduce((sum, val) => sum + (Number(val) || 0), 0); case "average": const sum = values.reduce((s, val) => s + (Number(val) || 0), 0); return values.length > 0 ? sum / values.length : 0; case "count": return values.length; case "min": return Math.min(...values.map((v) => Number(v) || 0)); case "max": return Math.max(...values.map((v) => Number(v) || 0)); case "first": return values[0]; case "last": return values[values.length - 1]; case "concat": return values.filter((v) => v != null).join(""); case "join": return values.filter((v) => v != null).join(", "); case "custom": // TODO: 커스텀 함수 실행 logger.warn("커스텀 변환 함수는 아직 구현되지 않았습니다."); return values; default: return values; } } /** * 조건에 따른 데이터 필터링 */ export function filterDataByCondition(data: any[], condition: Condition): any[] { return data.filter((row) => { const value = getNestedValue(row, condition.field); return evaluateCondition(value, condition.operator, condition.value); }); } /** * 조건 평가 */ function evaluateCondition(value: any, operator: string, targetValue: any): boolean { switch (operator) { case "equals": return value === targetValue; case "notEquals": return value !== targetValue; case "contains": return String(value).includes(String(targetValue)); case "notContains": return !String(value).includes(String(targetValue)); case "greaterThan": return Number(value) > Number(targetValue); case "lessThan": return Number(value) < Number(targetValue); case "greaterThanOrEqual": return Number(value) >= Number(targetValue); case "lessThanOrEqual": return Number(value) <= Number(targetValue); case "in": return Array.isArray(targetValue) && targetValue.includes(value); case "notIn": return Array.isArray(targetValue) && !targetValue.includes(value); default: logger.warn(`알 수 없는 조건 연산자: ${operator}`); return true; } } /** * 중첩된 객체에서 값 가져오기 * 예: "user.address.city" -> obj.user.address.city */ function getNestedValue(obj: any, path: string): any { if (!obj || !path) { return undefined; } const keys = path.split("."); let value = obj; for (const key of keys) { if (value == null) { return undefined; } value = value[key]; } return value; } /** * 중첩된 객체에 값 설정 * 예: "user.address.city", "Seoul" -> obj.user.address.city = "Seoul" */ function setNestedValue(obj: any, path: string, value: any): void { if (!obj || !path) { return; } const keys = path.split("."); const lastKey = keys.pop()!; let current = obj; for (const key of keys) { if (!(key in current)) { current[key] = {}; } current = current[key]; } current[lastKey] = value; } /** * 매핑 결과 검증 */ export function validateMappingResult( data: any[], rules: MappingRule[] ): { valid: boolean; errors: string[] } { const errors: string[] = []; // 필수 필드 검증 const requiredRules = rules.filter((rule) => rule.required); for (const rule of requiredRules) { const hasValue = data.some((row) => { const value = getNestedValue(row, rule.targetField); return value != null && value !== ""; }); if (!hasValue) { errors.push(`필수 필드 누락: ${rule.targetField}`); } } return { valid: errors.length === 0, errors, }; } /** * 매핑 규칙 미리보기 * 실제 데이터 전달 전에 결과를 미리 확인 */ export function previewMapping( sampleData: any[], rules: MappingRule[] ): { success: boolean; preview: any[]; errors?: string[] } { try { const preview = applyMappingRules(sampleData.slice(0, 5), rules); const validation = validateMappingResult(preview, rules); return { success: validation.valid, preview, errors: validation.errors, }; } catch (error: any) { return { success: false, preview: [], errors: [error.message], }; } }