From c0c3f45d18ebe68843a495a95ab638d7000d0dc3 Mon Sep 17 00:00:00 2001 From: curry772 Date: Tue, 12 May 2026 11:21:58 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=AA=A8=EB=93=A0=20=EB=B3=80=EC=88=98?= =?UTF-8?q?=20=ED=91=9C=ED=98=84=EC=8B=9D=EC=97=90=20:=EA=B8=B0=EB=B3=B8?= =?UTF-8?q?=EA=B0=92=20=EB=AC=B8=EB=B2=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ${callprop.KEY:default} - 직접 참조 기본값 - ${callprop[MSG.path]:default} - 간접 참조 기본값 - ${MSG.path:default} - 표준전문 경로 기본값 키/경로 미존재 또는 callProp null 시 기본값 반환, 기본값 미지정 시 빈 문자열. Co-Authored-By: Claude Sonnet 4.6 --- .../TemplateAdapterErrorMsgHandler.java | 65 ++++++-- .../TemplateAdapterErrorMsgHandlerTest.java | 139 ++++++++++++++++++ 2 files changed, 192 insertions(+), 12 deletions(-) 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 714f99b..26eb500 100644 --- a/src/main/java/com/eactive/eai/adapter/handler/TemplateAdapterErrorMsgHandler.java +++ b/src/main/java/com/eactive/eai/adapter/handler/TemplateAdapterErrorMsgHandler.java @@ -23,10 +23,13 @@ import com.eactive.eai.message.StandardMessage; * StandardMessage 의 필드 값으로 치환한 결과를 반환한다. * * [변수 치환 규칙] - * 1. "${callprop.키}" → callProp.getProperty("키") 로 치환 - * 2. "${경로}" → StandardMessage.findItemValue("경로") 로 치환 - * callprop 접두어가 있으면 callProp 을 우선 참조하고, - * 없으면 StandardMessage 경로로 해석한다. + * 1. "${callprop.키}" → callProp.getProperty("키") 로 치환 + * "${callprop.키:기본값}" → 키 없거나 callProp null 이면 기본값 반환 + * 2. "${callprop[경로]}" → StandardMessage 경로 값을 키로 callProp 간접 조회 + * "${callprop[경로]:기본값}" → 키 결정 실패 또는 키 없으면 기본값 반환 + * 3. "${경로}" → StandardMessage.findItemValue("경로") 로 치환 + * "${경로:기본값}" → 경로 값이 null/공백이면 기본값 반환 + * 모든 형태에서 기본값 미지정 시 빈 문자열을 반환한다. * * [배열 반복 문법] (MSG_LIST 등 GRID 타입) * {{#foreach MSG.MSG_LIST}} @@ -79,6 +82,9 @@ public class TemplateAdapterErrorMsgHandler implements AdapterErrorMessageHandle /** callProp 참조 변수 접두어: ${callprop.키} */ static final String CALLPROP_PREFIX = "callprop."; + /** callProp 간접 참조 접두어: ${callprop[표준전문경로]} */ + static final String CALLPROP_INDIRECT_PREFIX = "callprop["; + /** ${path} 또는 ${callprop.key} 스칼라 변수 패턴 */ private static final Pattern VAR_PATTERN = Pattern.compile("\\$\\{([^}]+)\\}"); @@ -141,17 +147,52 @@ public class TemplateAdapterErrorMsgHandler implements AdapterErrorMessageHandle /** * 변수 표현식을 해석해 값을 반환한다. - * - "callprop.키" → callProp 에서 직접 조회 - * - "callprop.부모.자식" → callProp.get("부모") 가 Properties 이면 중첩 탐색 - * - 그 외 → StandardMessage.findItemValue(expr) + * 모든 형태에서 ':기본값' 접미사를 지원한다. 기본값 미지정 시 빈 문자열. + * - "callprop[경로][:기본값]" → 표준전문 경로 값을 키로 callProp 간접 조회 + * - "callprop.키[:기본값]" → callProp 직접/중첩 조회 + * - "경로[:기본값]" → StandardMessage.findItemValue(경로) */ private String resolveScalar(String expr, StandardMessage msg, Properties callProp) { - if (expr.startsWith(CALLPROP_PREFIX)) { - if (callProp == null) return ""; - String keyPath = expr.substring(CALLPROP_PREFIX.length()); - return StringUtils.defaultString(resolveCallProp(callProp, keyPath)); + if (expr.startsWith(CALLPROP_INDIRECT_PREFIX)) { + return resolveIndirectCallProp(expr, msg, callProp); } - return StringUtils.defaultString(msg.findItemValue(expr)); + if (expr.startsWith(CALLPROP_PREFIX)) { + String rest = expr.substring(CALLPROP_PREFIX.length()); + int colonIdx = rest.indexOf(':'); + String keyPath = colonIdx >= 0 ? rest.substring(0, colonIdx) : rest; + String defaultVal = colonIdx >= 0 ? rest.substring(colonIdx + 1) : ""; + if (callProp == null) return defaultVal; + String value = resolveCallProp(callProp, keyPath); + return value != null ? value : defaultVal; + } + 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); + return StringUtils.isNotEmpty(value) ? value : defaultVal; + } + + /** + * ${callprop[경로]} 또는 ${callprop[경로]:기본값} 처리. + * 1. 표준전문 경로로 callProp 키를 동적으로 결정한다. + * 2. 결정된 키로 callProp 에서 값을 조회한다. + * 3. 키 또는 값을 찾지 못하면 기본값(없으면 빈 문자열)을 반환한다. + */ + private String resolveIndirectCallProp(String expr, StandardMessage msg, Properties callProp) { + int closeIdx = expr.indexOf(']'); + if (closeIdx < 0) return ""; + + String msgPath = expr.substring(CALLPROP_INDIRECT_PREFIX.length(), closeIdx); + String defaultVal = (closeIdx + 1 < expr.length() && expr.charAt(closeIdx + 1) == ':') + ? expr.substring(closeIdx + 2) + : ""; + + String key = msg.findItemValue(msgPath); + if (StringUtils.isBlank(key)) return defaultVal; + if (callProp == null) return defaultVal; + + String value = resolveCallProp(callProp, key); + return value != null ? value : defaultVal; } /** 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 fc9a769..ec7e5e6 100644 --- a/src/test/java/com/eactive/eai/adapter/handler/TemplateAdapterErrorMsgHandlerTest.java +++ b/src/test/java/com/eactive/eai/adapter/handler/TemplateAdapterErrorMsgHandlerTest.java @@ -160,6 +160,30 @@ class TemplateAdapterErrorMsgHandlerTest { assertEquals("상세설명입니다", handler.render("${MSG.MAIN_MSG.outp_msg_desc}", msg, null)); } + + @Test + @DisplayName("경로:기본값 – 경로 존재 시 값 반환") + void 표준전문_기본값_경로존재() { + StandardMessage msg = buildMsg("E001", "오류", "", null); + + assertEquals("E001", handler.render("${MSG.MAIN_MSG.outp_msg_cd:DEFAULT}", msg, null)); + } + + @Test + @DisplayName("경로:기본값 – 경로 없으면 기본값 반환") + void 표준전문_기본값_경로없음() { + StandardMessage msg = buildMsg("E001", "오류", "", null); + + assertEquals("UNKNOWN", handler.render("${NONE.PATH:UNKNOWN}", msg, null)); + } + + @Test + @DisplayName("경로:기본값 – 기본값이 빈 문자열") + void 표준전문_기본값_빈문자열() { + StandardMessage msg = buildMsg("E001", "오류", "", null); + + assertEquals("", handler.render("${NONE.PATH:}", msg, null)); + } } // ================================================================ @@ -273,6 +297,121 @@ class TemplateAdapterErrorMsgHandlerTest { assertEquals("", handler.render("${callprop.SUB.NONE_KEY}", msg, callProp)); } + + @Test + @DisplayName("callprop.키:기본값 – 키 존재 시 값 반환") + void callProp_직접_기본값_키존재() { + StandardMessage msg = buildMsg("E001", "오류", "", null); + Properties callProp = props("ADAPTER_NAME", "MY_ADAPTER"); + + assertEquals("MY_ADAPTER", handler.render("${callprop.ADAPTER_NAME:DEFAULT}", msg, callProp)); + } + + @Test + @DisplayName("callprop.키:기본값 – 키 없으면 기본값 반환") + void callProp_직접_기본값_키없음() { + StandardMessage msg = buildMsg("E001", "오류", "", null); + Properties callProp = new Properties(); + + assertEquals("DEFAULT_VAL", handler.render("${callprop.NONE_KEY:DEFAULT_VAL}", msg, callProp)); + } + + @Test + @DisplayName("callprop.키:기본값 – callProp null 이면 기본값 반환") + void callProp_직접_기본값_null() { + StandardMessage msg = buildMsg("E001", "오류", "", null); + + assertEquals("FALLBACK", handler.render("${callprop.ADAPTER_NAME:FALLBACK}", msg, null)); + } + } + + // ================================================================ + // callProp 간접 참조 ${callprop[표준전문경로]} / ${callprop[경로]:기본값} + // ================================================================ + + @Nested + @DisplayName("callProp 간접 참조 ${callprop[경로]}") + class IndirectCallPropSubstitution { + + /** 표준전문에 cp_key 필드가 있는 메시지 */ + private StandardMessage buildMsgWithKey(String cpKey, String errCode) { + StandardMessage msg = buildMsg(errCode, "오류", "", null); + // MSG 그룹 아래 cp_key 필드 추가 + StandardItem msgGroup = msg.findItem("MSG"); + if (msgGroup != null) { + msgGroup.addItem(field("cp_key", cpKey)); + } + return msg; + } + + @Test + @DisplayName("표준전문 경로 값으로 callProp 키를 결정해 값 조회") + void 간접_참조_정상() { + StandardMessage msg = buildMsgWithKey("ADAPTER_NAME", "E001"); + Properties callProp = props("ADAPTER_NAME", "MY_ADAPTER"); + String template = "${callprop[MSG.cp_key]}"; + + assertEquals("MY_ADAPTER", handler.render(template, msg, callProp)); + } + + @Test + @DisplayName("표준전문 경로 값이 없으면 기본값 반환") + void 표준전문_경로_없으면_기본값() { + StandardMessage msg = buildMsg("E001", "오류", "", null); + Properties callProp = props("ADAPTER_NAME", "MY_ADAPTER"); + String template = "${callprop[MSG.NONE_KEY]:DEFAULT_ADAPTER}"; + + assertEquals("DEFAULT_ADAPTER", handler.render(template, msg, callProp)); + } + + @Test + @DisplayName("callProp 에 해당 키 없으면 기본값 반환") + void callProp_키_없으면_기본값() { + StandardMessage msg = buildMsgWithKey("MISSING_KEY", "E001"); + Properties callProp = props("OTHER_KEY", "OTHER_VALUE"); + String template = "${callprop[MSG.cp_key]:FALLBACK}"; + + assertEquals("FALLBACK", handler.render(template, msg, callProp)); + } + + @Test + @DisplayName("기본값 미지정 시 빈 문자열") + void 기본값_없으면_빈문자열() { + StandardMessage msg = buildMsg("E001", "오류", "", null); + Properties callProp = props("ADAPTER_NAME", "MY_ADAPTER"); + String template = "${callprop[MSG.NONE_KEY]}"; + + assertEquals("", handler.render(template, msg, callProp)); + } + + @Test + @DisplayName("callProp 이 null 이면 기본값 반환") + void callProp_null_기본값() { + StandardMessage msg = buildMsgWithKey("ADAPTER_NAME", "E001"); + String template = "${callprop[MSG.cp_key]:NO_PROP}"; + + assertEquals("NO_PROP", handler.render(template, msg, null)); + } + + @Test + @DisplayName("기본값이 빈 문자열인 경우") + void 기본값이_빈문자열() { + StandardMessage msg = buildMsg("E001", "오류", "", null); + Properties callProp = new Properties(); + String template = "${callprop[MSG.NONE_KEY]:}"; + + assertEquals("", handler.render(template, msg, callProp)); + } + + @Test + @DisplayName("간접 참조와 직접 참조 혼합") + void 간접_직접_혼합() { + StandardMessage msg = buildMsgWithKey("IF_ID_KEY", "E001"); + Properties callProp = props("IF_ID_KEY", "SVC_001", "ADAPTER_NAME", "MY_ADAPTER"); + String template = "{\"adapter\":\"${callprop.ADAPTER_NAME}\",\"ifId\":\"${callprop[MSG.cp_key]}\"}"; + + assertEquals("{\"adapter\":\"MY_ADAPTER\",\"ifId\":\"SVC_001\"}", handler.render(template, msg, callProp)); + } } // ================================================================