/** * 데이터 매핑 유틸리티 * 화면 간 데이터 전달 시 매핑 규칙 적용 */ import type { MappingRule, Condition, TransformFunction, } from "@/types/screen-embedding"; import { logger } from "./logger"; /** * 매핑 규칙 적용 */ export function applyMappingRules(data: any[], rules: MappingRule[]): any[] { if (!data || data.length === 0) { return []; } // 변환 함수가 있는 규칙 확인 const hasTransform = rules.some((rule) => rule.transform && rule.transform !== "none"); if (hasTransform) { // 변환 함수가 있으면 단일 값 또는 집계 결과 반환 return [applyTransformRules(data, rules)]; } // 일반 매핑 (각 행에 대해 매핑) return data.map((row) => { const mappedRow: any = {}; for (const rule of rules) { const sourceValue = getNestedValue(row, rule.sourceField); const targetValue = sourceValue ?? rule.defaultValue; 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], }; } }