diff --git a/backend-spring/src/main/java/com/erp/batch/MappingTransformer.java b/backend-spring/src/main/java/com/erp/batch/MappingTransformer.java new file mode 100644 index 00000000..a9129a3d --- /dev/null +++ b/backend-spring/src/main/java/com/erp/batch/MappingTransformer.java @@ -0,0 +1,179 @@ +package com.erp.batch; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * 매핑 변환 유틸리티 — vexplor_rps 의 batchSchedulerService L550~617 .map() 로직 1:1 이식. + * + * BatchExecutor 가 FROM 에서 읽은 row 들을 TO 형태로 변환할 때 사용. 의존성 없는 정적 메서드만. + * + * mapping_type 분기: + * - "direct" : row[from_column_name] → row[to_column_name] 그대로 복사 + * from_column_name 은 점 표기법 지원 (예: "response.access_token") + * - "fixed" : from_column_name 자체가 고정값. transformRow 는 fixed 매핑을 건너뛰고, + * 호출측이 partition 한 뒤 mappedRow 에 적용 (vexplor_rps L598-603 패턴). + * - "conditional" : ConditionalConfig.rules 의 when 과 sourceVal 문자열 동등 비교, 매칭 then 반환. + * 매칭 없으면 default. (단순 문자열 lookup. SpEL/JEXL 등 표현식 평가 안 함) + */ +@Slf4j +public final class MappingTransformer { + + private static final ObjectMapper OM = new ObjectMapper(); + + private MappingTransformer() {} + + /** 단일 row 를 매핑 룰에 따라 변환. mapping_type 별 분기 처리. */ + public static Map transformRow( + Map row, + List> nonFixedMappings, + List> fixedMappings, + String toConnectionType, + String companyCode + ) { + Map mappedRow = new LinkedHashMap<>(); + + for (Map mapping : nonFixedMappings) { + String mt = strOr(mapping.get("mapping_type"), "direct"); + String fromCol = str(mapping.get("from_column_name")); + String toCol = str(mapping.get("to_column_name")); + + if ("conditional".equals(mt)) { + ConditionalConfig cfg = parseConditionalConfig(mapping.get("mapping_config")); + String sourceVal = String.valueOf(getValueByPath(row, fromCol)); + if (sourceVal == null || "null".equals(sourceVal)) sourceVal = ""; + mappedRow.put(toCol, evaluateConditional(sourceVal, cfg)); + continue; + } + + // direct 또는 알 수 없는 type — 그대로 복사 + // DB→REST 의 to_api_body 템플릿 처리는 BatchExecutor 측에서 별도 처리 (vexplor_rps L582~595). + // 여기서는 단순 to_column_name 으로 값 흘림. + Object value = getValueByPath(row, fromCol); + mappedRow.put(toCol, value); + } + + // 고정값 매핑 적용 — from_column_name 자체가 저장값 (vexplor_rps L598-603) + if (fixedMappings != null) { + for (Map fm : fixedMappings) { + mappedRow.put(str(fm.get("to_column_name")), fm.get("from_column_name")); + } + } + + // 멀티테넌시: TO 가 DB 일 때 company_code 자동 주입 (vexplor_rps L605-614) + if (!"restapi".equals(toConnectionType) + && companyCode != null + && !mappedRow.containsKey("company_code")) { + mappedRow.put("company_code", companyCode); + } + + return mappedRow; + } + + /** 점 표기법 path 평가 — "response.access_token" 같은 중첩 키 지원 (vexplor_rps L540-548). */ + @SuppressWarnings("unchecked") + public static Object getValueByPath(Map obj, String path) { + if (obj == null || path == null || path.isEmpty()) return null; + if (!path.contains(".")) return obj.get(path); + Object cur = obj; + for (String part : path.split("\\.")) { + if (!(cur instanceof Map)) return null; + cur = ((Map) cur).get(part); + if (cur == null) return null; + } + return cur; + } + + /** ConditionalConfig 단일 평가 — when/then lookup + default. */ + public static Object evaluateConditional(String sourceVal, ConditionalConfig cfg) { + if (cfg == null || cfg.rules == null) return cfg != null ? cfg.defaultValue : null; + for (ConditionalRule r : cfg.rules) { + String when = r.when == null ? "" : r.when; + if (Objects.equals(when, sourceVal)) return r.then; + } + return cfg.defaultValue; + } + + /** + * mapping_config (JSONB) 의 원시 값 → ConditionalConfig. + * - BatchService.attachMappings 가 이미 파싱한 경우 → Map + * - 직접 SELECT 결과 → String(JSON) 가능 + * - null → 빈 cfg + */ + @SuppressWarnings("unchecked") + public static ConditionalConfig parseConditionalConfig(Object raw) { + if (raw == null) return ConditionalConfig.empty(); + Map map; + try { + if (raw instanceof Map) { + map = (Map) raw; + } else if (raw instanceof String) { + String s = ((String) raw).trim(); + if (s.isEmpty()) return ConditionalConfig.empty(); + map = OM.readValue(s, Map.class); + } else { + return ConditionalConfig.empty(); + } + } catch (Exception e) { + log.warn("[conditional 매핑] JSON 파싱 실패: {}", e.getMessage()); + return ConditionalConfig.empty(); + } + + ConditionalConfig cfg = new ConditionalConfig(); + Object rulesRaw = map.get("rules"); + if (rulesRaw instanceof List) { + for (Object r : (List) rulesRaw) { + if (r instanceof Map) { + Map rm = (Map) r; + cfg.rules.add(new ConditionalRule( + rm.get("when") == null ? "" : String.valueOf(rm.get("when")), + rm.get("then") == null ? null : String.valueOf(rm.get("then")) + )); + } + } + } + Object def = map.get("default"); + cfg.defaultValue = def == null ? null : String.valueOf(def); + return cfg; + } + + /** non-fixed / fixed 매핑 분리. vexplor_rps L265~271 partition 패턴. */ + public static Partition partitionFixed(List> mappings) { + Partition p = new Partition(); + if (mappings == null) return p; + for (Map m : mappings) { + String mt = strOr(m.get("mapping_type"), "direct"); + if ("fixed".equals(mt)) p.fixed.add(m); else p.nonFixed.add(m); + } + return p; + } + + public static final class Partition { + public final List> nonFixed = new ArrayList<>(); + public final List> fixed = new ArrayList<>(); + } + + public static final class ConditionalConfig { + public List rules = new ArrayList<>(); + public String defaultValue; + public static ConditionalConfig empty() { return new ConditionalConfig(); } + } + + public static final class ConditionalRule { + public final String when; + public final String then; + public ConditionalRule(String when, String then) { this.when = when; this.then = then; } + } + + private static String str(Object v) { return v == null ? null : v.toString(); } + private static String strOr(Object v, String fallback) { + String s = str(v); + return (s == null || s.isEmpty()) ? fallback : s; + } +}