feat(batch): Phase 3 — MappingTransformer lookup 엔진
vexplor_rps batchSchedulerService L540~617 의 변환 로직 1:1 이식. 의존성 없는 정적 유틸 — BatchExecutor 가 FROM 읽기 결과를 TO 형태로 변환할 때 사용. - transformRow: mapping_type 별 분기 (direct/conditional/fixed), 멀티테넌시용 company_code 자동 주입, 점 표기법 path 평가 - evaluateConditional: ConditionalConfig.rules 의 when/then lookup + default 폴백. 단순 문자열 동등 비교 (SpEL/JEXL 표현식 평가 안 함) - getValueByPath: "response.access_token" 같은 중첩 키 지원 - parseConditionalConfig: JSONB 가 Map/String/null 셋 다 가능 — 안전 normalize - partitionFixed: non-fixed / fixed 매핑 분리 헬퍼 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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<String, Object> transformRow(
|
||||
Map<String, Object> row,
|
||||
List<Map<String, Object>> nonFixedMappings,
|
||||
List<Map<String, Object>> fixedMappings,
|
||||
String toConnectionType,
|
||||
String companyCode
|
||||
) {
|
||||
Map<String, Object> mappedRow = new LinkedHashMap<>();
|
||||
|
||||
for (Map<String, Object> 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<String, Object> 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<String, Object> 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<String, Object>) 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<String,Object>
|
||||
* - 직접 SELECT 결과 → String(JSON) 가능
|
||||
* - null → 빈 cfg
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static ConditionalConfig parseConditionalConfig(Object raw) {
|
||||
if (raw == null) return ConditionalConfig.empty();
|
||||
Map<String, Object> map;
|
||||
try {
|
||||
if (raw instanceof Map) {
|
||||
map = (Map<String, Object>) 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<Object>) rulesRaw) {
|
||||
if (r instanceof Map) {
|
||||
Map<String, Object> rm = (Map<String, Object>) 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<Map<String, Object>> mappings) {
|
||||
Partition p = new Partition();
|
||||
if (mappings == null) return p;
|
||||
for (Map<String, Object> 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<Map<String, Object>> nonFixed = new ArrayList<>();
|
||||
public final List<Map<String, Object>> fixed = new ArrayList<>();
|
||||
}
|
||||
|
||||
public static final class ConditionalConfig {
|
||||
public List<ConditionalRule> 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user