diff --git a/src/main/java/com/eactive/eai/adapter/handler/TemplateAdapterErrorMsgHandler.java b/src/main/java/com/eactive/eai/adapter/handler/TemplateAdapterErrorMsgHandler.java index 26eb500..3e6cd3e 100644 --- a/src/main/java/com/eactive/eai/adapter/handler/TemplateAdapterErrorMsgHandler.java +++ b/src/main/java/com/eactive/eai/adapter/handler/TemplateAdapterErrorMsgHandler.java @@ -10,6 +10,8 @@ import java.util.regex.Pattern; import org.apache.commons.lang3.StringUtils; import com.eactive.eai.common.property.PropManager; +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; import com.eactive.eai.common.util.Logger; import com.eactive.eai.message.StandardItem; import com.eactive.eai.message.StandardMessage; @@ -168,10 +170,55 @@ public class TemplateAdapterErrorMsgHandler implements AdapterErrorMessageHandle int colonIdx = expr.indexOf(':'); String path = colonIdx >= 0 ? expr.substring(0, colonIdx) : expr; String defaultVal = colonIdx >= 0 ? expr.substring(colonIdx + 1) : ""; - String value = msg.findItemValue(path); + String value = resolveMessagePath(msg, path); return StringUtils.isNotEmpty(value) ? value : defaultVal; } + /** + * StandardMessage 경로를 해석한다. + * 1. findItemValue(path) 로 직접 조회한다. + * 2. null 이면 경로를 뒤에서부터 분리하며, 부모 경로 값이 JSON 문자열인 경우 + * 나머지 경로(subPath)를 키로 JSON 필드를 추출한다. + * 예) DATA.BIZDATA.acctNo → findItemValue("DATA.BIZDATA") → JSON 파싱 → acctNo + * DATA.BIZDATA.addr.city → findItemValue("DATA.BIZDATA") → JSON 파싱 → addr.city + */ + private String resolveMessagePath(StandardMessage msg, String path) { + String value = msg.findItemValue(path); + if (value != null) return value; + + int dotIdx = path.lastIndexOf('.'); + while (dotIdx > 0) { + String parentPath = path.substring(0, dotIdx); + String subPath = path.substring(dotIdx + 1); + String parentVal = msg.findItemValue(parentPath); + if (StringUtils.isNotBlank(parentVal)) { + String extracted = extractFromJson(parentVal, subPath); + if (extracted != null) return extracted; + } + dotIdx = parentPath.lastIndexOf('.'); + } + return null; + } + + /** + * JSON 문자열에서 dot 구분 경로로 필드 값을 추출한다. + * 중간 경로가 JSON 오브젝트면 계속 탐색하고, 최종 값이 primitive 면 문자열로, + * 오브젝트/배열이면 JSON 문자열 그대로 반환한다. 파싱 실패 시 null 반환. + */ + private String extractFromJson(String json, String fieldPath) { + try { + JsonElement el = new JsonParser().parse(json); + for (String part : fieldPath.split("\\.", -1)) { + if (!el.isJsonObject()) return null; + el = el.getAsJsonObject().get(part); + if (el == null) return null; + } + return el.isJsonPrimitive() ? el.getAsString() : el.toString(); + } catch (Exception e) { + return null; + } + } + /** * ${callprop[경로]} 또는 ${callprop[경로]:기본값} 처리. * 1. 표준전문 경로로 callProp 키를 동적으로 결정한다. diff --git a/src/test/java/com/eactive/eai/adapter/handler/TemplateAdapterErrorMsgHandlerTest.java b/src/test/java/com/eactive/eai/adapter/handler/TemplateAdapterErrorMsgHandlerTest.java index ec7e5e6..b73811e 100644 --- a/src/test/java/com/eactive/eai/adapter/handler/TemplateAdapterErrorMsgHandlerTest.java +++ b/src/test/java/com/eactive/eai/adapter/handler/TemplateAdapterErrorMsgHandlerTest.java @@ -418,6 +418,90 @@ class TemplateAdapterErrorMsgHandlerTest { // 배열 반복 {{#foreach path}}...{{/foreach}} // ================================================================ + // ================================================================ + // DATA 영역 변수 치환 ${DATA.xxx} + // ================================================================ + + @Nested + @DisplayName("DATA 영역 변수 치환 ${DATA.xxx}") + class DataSectionSubstitution { + + /** + * DATA > BIZDATA(FIELD, JSON String value) 구조의 StandardMessage 생성. + * BIZDATA 는 파싱 없이 JSON 문자열 그대로 세팅되는 형태. + */ + private StandardMessage buildMsgWithBizData(String bizDataJson) { + StandardMessage msg = buildMsg("E001", "오류", "", null); + + StandardItem dataGroup = group("DATA"); + dataGroup.addItem(field("BIZDATA", bizDataJson)); + msg.addItem(dataGroup); + + return msg; + } + + @Test + @DisplayName("DATA.BIZDATA JSON 필드 치환 – 단일 depth") + void DATA_BIZDATA_JSON_필드_치환() { + StandardMessage msg = buildMsgWithBizData("{\"acctNo\":\"1234567890\",\"custNm\":\"홍길동\"}"); + String template = "{\"acctNo\":\"${DATA.BIZDATA.acctNo}\",\"custNm\":\"${DATA.BIZDATA.custNm}\"}"; + + assertEquals("{\"acctNo\":\"1234567890\",\"custNm\":\"홍길동\"}", + handler.render(template, msg, null)); + } + + @Test + @DisplayName("DATA.BIZDATA JSON 필드 치환 – 중첩 depth") + void DATA_BIZDATA_JSON_중첩_필드_치환() { + StandardMessage msg = buildMsgWithBizData("{\"addr\":{\"city\":\"서울\",\"zip\":\"12345\"}}"); + + assertEquals("서울", handler.render("${DATA.BIZDATA.addr.city}", msg, null)); + } + + @Test + @DisplayName("DATA.BIZDATA 원본 JSON 문자열 치환") + void DATA_BIZDATA_원본_치환() { + String rawJson = "{\"acctNo\":\"1234567890\"}"; + StandardMessage msg = buildMsgWithBizData(rawJson); + + assertEquals(rawJson, handler.render("${DATA.BIZDATA}", msg, null)); + } + + @Test + @DisplayName("DATA.BIZDATA 와 MSG 영역 혼합 치환") + void DATA_MSG_혼합_치환() { + StandardMessage msg = buildMsgWithBizData("{\"acctNo\":\"9876543210\"}"); + String template = "{\"code\":\"${MSG.MAIN_MSG.outp_msg_cd}\",\"acctNo\":\"${DATA.BIZDATA.acctNo}\"}"; + + assertEquals("{\"code\":\"E001\",\"acctNo\":\"9876543210\"}", + handler.render(template, msg, null)); + } + + @Test + @DisplayName("DATA.BIZDATA JSON 에 없는 필드는 빈 문자열") + void DATA_BIZDATA_없는_JSON_필드_빈문자열() { + StandardMessage msg = buildMsgWithBizData("{\"acctNo\":\"1234567890\"}"); + + assertEquals("", handler.render("${DATA.BIZDATA.noneField}", msg, null)); + } + + @Test + @DisplayName("DATA.BIZDATA JSON 에 없는 필드에 기본값 지정") + void DATA_BIZDATA_없는_JSON_필드_기본값() { + StandardMessage msg = buildMsgWithBizData("{\"acctNo\":\"1234567890\"}"); + + assertEquals("UNKNOWN", handler.render("${DATA.BIZDATA.noneField:UNKNOWN}", msg, null)); + } + + @Test + @DisplayName("DATA 영역 미설정 시 기본값 반환") + void DATA_없으면_기본값() { + StandardMessage msg = buildMsg("E001", "오류", "", null); + + assertEquals("{}", handler.render("${DATA.BIZDATA:{}}", msg, null)); + } + } + @Nested @DisplayName("배열 반복 블록 {{#foreach}}") class ForeachBlock {