ERP-node/frontend/lib/utils/dataMapping.ts

285 lines
6.7 KiB
TypeScript

/**
* 데이터 매핑 유틸리티
* 화면 간 데이터 전달 시 매핑 규칙 적용
*/
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],
};
}
}