diff --git a/src/main/java/com/eactive/eai/adapter/handler/TemplateAdapterErrorMsgHandler.java b/src/main/java/com/eactive/eai/adapter/handler/TemplateAdapterErrorMsgHandler.java
new file mode 100644
index 0000000..2d1956f
--- /dev/null
+++ b/src/main/java/com/eactive/eai/adapter/handler/TemplateAdapterErrorMsgHandler.java
@@ -0,0 +1,197 @@
+package com.eactive.eai.adapter.handler;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Properties;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.commons.lang3.StringUtils;
+
+import com.eactive.eai.adapter.AdapterPropManager;
+import com.eactive.eai.common.util.Logger;
+import com.eactive.eai.message.StandardItem;
+import com.eactive.eai.message.StandardMessage;
+
+/**
+ * 템플릿 기반 어댑터 에러 메시지 핸들러.
+ *
+ * [프로퍼티 설정]
+ * AdapterPropManager 의 "AdapterErrorMessageHandler" 프로퍼티 그룹에서
+ * "{adapterGroupId}.template" 키로 등록된 템플릿 문자열을 읽어
+ * StandardMessage 의 필드 값으로 치환한 결과를 반환한다.
+ *
+ * [변수 치환 규칙]
+ * 1. "${callprop.키}" → callProp.getProperty("키") 로 치환
+ * 2. "${경로}" → StandardMessage.findItemValue("경로") 로 치환
+ * callprop 접두어가 있으면 callProp 을 우선 참조하고,
+ * 없으면 StandardMessage 경로로 해석한다.
+ *
+ * [배열 반복 문법] (MSG_LIST 등 GRID 타입)
+ * {{#foreach MSG.MSG_LIST}}
+ * { "code":"${outp_msg_cd}", "msg":"${outp_msg_ctnt}" }
+ * {{/foreach}}
+ * → MSG_LIST 각 행을 반복하며 행 내 필드명(접두어 없이)으로 치환,
+ * 반복 결과를 쉼표(,)로 이어 붙임
+ *
+ * [사용 예 – JSON 템플릿]
+ * {
+ * "adapterName": "${callprop.ADAPTER_NAME}",
+ * "resultCode": "${MSG.MAIN_MSG.outp_msg_cd}",
+ * "resultMsg": "${MSG.MAIN_MSG.outp_msg_ctnt}",
+ * "errors": [
+ * {{#foreach MSG.MSG_LIST}}
+ * {
+ * "code": "${outp_msg_cd}",
+ * "msg": "${outp_msg_ctnt}",
+ * "desc": "${outp_msg_desc}",
+ * "field": "${err_occu_item_nm}"
+ * }
+ * {{/foreach}}
+ * ]
+ * }
+ *
+ * [사용 예 – XML 템플릿]
+ *
+ * ${callprop.ADAPTER_NAME}
+ * ${MSG.MAIN_MSG.outp_msg_cd}
+ *
+ * {{#foreach MSG.MSG_LIST}}
+ * ${outp_msg_cd}${outp_msg_ctnt}
+ * {{/foreach}}
+ *
+ *
+ */
+public class TemplateAdapterErrorMsgHandler implements AdapterErrorMessageHandler {
+
+ private static final Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
+
+ /** AdapterPropManager 에서 템플릿을 조회할 프로퍼티 그룹 이름 */
+ static final String PROP_GROUP = "AdapterErrorMessageHandler";
+
+ /** callProp 참조 변수 접두어: ${callprop.키} */
+ static final String CALLPROP_PREFIX = "callprop.";
+
+ /** ${path} 또는 ${callprop.key} 스칼라 변수 패턴 */
+ private static final Pattern VAR_PATTERN =
+ Pattern.compile("\\$\\{([^}]+)\\}");
+
+ /** {{#foreach path}}...{{/foreach}} 배열 반복 블록 패턴 */
+ private static final Pattern FOREACH_PATTERN =
+ Pattern.compile("\\{\\{#foreach\\s+([^}]+)\\}\\}(.*?)\\{\\{/foreach\\}\\}",
+ Pattern.DOTALL);
+
+ @Override
+ public Object generateNonStandardErrorResponseMessage(
+ String inboundAdapterGroupName,
+ String inboundAdapterName,
+ Properties callProp,
+ Object outboundRequestData,
+ StandardMessage resStandardMessage) throws Exception {
+
+ String templateKey = inboundAdapterGroupName + ".template";
+ String template = AdapterPropManager.getInstance().getProperty(PROP_GROUP, templateKey);
+
+ if (StringUtils.isBlank(template)) {
+ if (logger.isWarn()) {
+ logger.warn("TemplateAdapterErrorMsgHandler] template not found. group=" + PROP_GROUP
+ + ", key=" + templateKey);
+ }
+ return null;
+ }
+
+ return render(template, resStandardMessage, callProp);
+ }
+
+ /**
+ * 템플릿에 StandardMessage 및 callProp 값을 치환해 최종 문자열을 반환한다.
+ * package-private: 단위 테스트에서 직접 호출 가능
+ */
+ String render(String template, StandardMessage msg, Properties callProp) {
+ // 1단계: {{#foreach path}}...{{/foreach}} 배열 반복 처리
+ StringBuffer sb = new StringBuffer();
+ Matcher foreachMatcher = FOREACH_PATTERN.matcher(template);
+ while (foreachMatcher.find()) {
+ String arrayPath = foreachMatcher.group(1).trim();
+ String blockContent = foreachMatcher.group(2);
+ String rendered = renderForeach(arrayPath, blockContent, msg);
+ foreachMatcher.appendReplacement(sb, Matcher.quoteReplacement(rendered));
+ }
+ foreachMatcher.appendTail(sb);
+
+ // 2단계: 나머지 ${path} / ${callprop.키} 스칼라 변수 치환
+ String intermediate = sb.toString();
+ StringBuffer result = new StringBuffer();
+ Matcher varMatcher = VAR_PATTERN.matcher(intermediate);
+ while (varMatcher.find()) {
+ String expr = varMatcher.group(1).trim();
+ String value = resolveScalar(expr, msg, callProp);
+ varMatcher.appendReplacement(result, Matcher.quoteReplacement(value));
+ }
+ varMatcher.appendTail(result);
+ return result.toString();
+ }
+
+ /**
+ * 변수 표현식을 해석해 값을 반환한다.
+ * - "callprop.키" → callProp.getProperty("키")
+ * - 그 외 → StandardMessage.findItemValue(expr)
+ */
+ private String resolveScalar(String expr, StandardMessage msg, Properties callProp) {
+ if (expr.startsWith(CALLPROP_PREFIX)) {
+ String key = expr.substring(CALLPROP_PREFIX.length());
+ if (callProp != null) {
+ return StringUtils.defaultString(callProp.getProperty(key));
+ }
+ return "";
+ }
+ return StringUtils.defaultString(msg.findItemValue(expr));
+ }
+
+ /**
+ * GRID 타입 배열 아이템을 반복하며 blockContent 를 각 행으로 치환,
+ * 결과를 쉼표로 이어 반환한다. (foreach 블록 내부는 callProp 참조 미지원)
+ */
+ private String renderForeach(String arrayPath, String blockContent, StandardMessage msg) {
+ StandardItem arrayItem = msg.findItem(arrayPath);
+ if (arrayItem == null) {
+ if (logger.isWarn()) logger.warn("TemplateAdapterErrorMsgHandler] foreach path not found: " + arrayPath);
+ return "";
+ }
+
+ List> rows = arrayItem.getList();
+ if (rows == null || rows.isEmpty()) {
+ return "";
+ }
+
+ List renderedItems = new ArrayList<>();
+ for (LinkedHashMap row : rows) {
+ String rowText = renderRow(blockContent, row);
+ if (StringUtils.isNotBlank(rowText)) {
+ renderedItems.add(rowText);
+ }
+ }
+ return String.join(",", renderedItems);
+ }
+
+ /**
+ * 배열 한 행(LinkedHashMap) 기준으로 blockContent 의 ${fieldName} 을 치환한다.
+ * fieldName 은 경로 없이 해당 행의 컬럼명만 사용한다.
+ */
+ private String renderRow(String blockContent, LinkedHashMap row) {
+ StringBuffer result = new StringBuffer();
+ Matcher varMatcher = VAR_PATTERN.matcher(blockContent);
+ while (varMatcher.find()) {
+ String fieldName = varMatcher.group(1).trim();
+ String value = "";
+ StandardItem item = row.get(fieldName);
+ if (item != null) {
+ value = StringUtils.defaultString(item.getValue());
+ }
+ varMatcher.appendReplacement(result, Matcher.quoteReplacement(value));
+ }
+ varMatcher.appendTail(result);
+ return result.toString().trim();
+ }
+}
diff --git a/src/test/java/com/eactive/eai/adapter/handler/TemplateAdapterErrorMsgHandlerTest.java b/src/test/java/com/eactive/eai/adapter/handler/TemplateAdapterErrorMsgHandlerTest.java
new file mode 100644
index 0000000..7326022
--- /dev/null
+++ b/src/test/java/com/eactive/eai/adapter/handler/TemplateAdapterErrorMsgHandlerTest.java
@@ -0,0 +1,439 @@
+package com.eactive.eai.adapter.handler;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+
+import java.lang.reflect.Field;
+import java.util.LinkedHashMap;
+import java.util.Properties;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+import org.springframework.context.ApplicationContext;
+
+import com.eactive.eai.adapter.AdapterPropManager;
+import com.eactive.eai.common.util.ApplicationContextProvider;
+import com.eactive.eai.message.StandardItem;
+import com.eactive.eai.message.StandardMessage;
+import com.eactive.eai.message.StandardType;
+
+/**
+ * TemplateAdapterErrorMsgHandler 단위테스트
+ *
+ * - render() : package-private 이므로 같은 패키지에서 직접 호출
+ * - generateNonStandardErrorResponseMessage() : ApplicationContextProvider 에
+ * mock ApplicationContext 를 리플렉션으로 주입하여 AdapterPropManager 격리
+ */
+class TemplateAdapterErrorMsgHandlerTest {
+
+ private TemplateAdapterErrorMsgHandler handler;
+
+ @BeforeEach
+ void setUp() {
+ handler = new TemplateAdapterErrorMsgHandler();
+ }
+
+ // ================================================================
+ // StandardMessage 빌더 헬퍼
+ // ================================================================
+
+ /** FIELD 아이템 (type=2) */
+ private static StandardItem field(String name, String value) {
+ return new StandardItem(name, 3, StandardType.FIELD, 1, 0, 300, 1, "", "", value, name);
+ }
+
+ /** GROUP 아이템 (type=3, size=1 → 활성) */
+ private static StandardItem group(String name) {
+ return new StandardItem(name, 2, StandardType.GROUP, 1, 0, 0, 1, "", "", null, name);
+ }
+
+ /**
+ * DJB 표준전문 MSG 영역을 모사한 StandardMessage 생성.
+ *
+ * 구조:
+ * MSG (GROUP)
+ * MAIN_MSG (GROUP)
+ * outp_msg_cd, outp_msg_ctnt, outp_msg_desc
+ * MSG_LIST (GRID)
+ * row[n]: outp_msg_cd, outp_msg_ctnt, outp_msg_desc, err_occu_item_nm
+ *
+ * @param listRows null 또는 빈 배열이면 MSG_LIST 행 없음
+ */
+ private StandardMessage buildMsg(String errCode, String errMsg, String errDesc,
+ String[][] listRows) {
+ StandardMessage msg = new StandardMessage();
+
+ StandardItem msgGroup = group("MSG");
+
+ StandardItem mainMsg = group("MAIN_MSG");
+ mainMsg.addItem(field("outp_msg_cd", errCode));
+ mainMsg.addItem(field("outp_msg_ctnt", errMsg));
+ mainMsg.addItem(field("outp_msg_desc", errDesc));
+ msgGroup.addItem(mainMsg);
+
+ StandardItem msgList = new StandardItem(
+ "MSG_LIST", 2, StandardType.GRID, 0, 0, 0, 1, "", "", null, "MSG_LIST");
+ msgList.addItem(field("outp_msg_cd", ""));
+ msgList.addItem(field("outp_msg_ctnt", ""));
+ msgList.addItem(field("outp_msg_desc", ""));
+ msgList.addItem(field("err_occu_item_nm", ""));
+
+ if (listRows != null) {
+ for (String[] row : listRows) {
+ LinkedHashMap rowMap = new LinkedHashMap<>();
+ rowMap.put("outp_msg_cd", field("outp_msg_cd", get(row, 0)));
+ rowMap.put("outp_msg_ctnt", field("outp_msg_ctnt", get(row, 1)));
+ rowMap.put("outp_msg_desc", field("outp_msg_desc", get(row, 2)));
+ rowMap.put("err_occu_item_nm", field("err_occu_item_nm", get(row, 3)));
+ msgList.addArray(rowMap);
+ }
+ }
+ msgGroup.addItem(msgList);
+ msg.addItem(msgGroup);
+ return msg;
+ }
+
+ private static String get(String[] arr, int idx) {
+ return (arr != null && arr.length > idx) ? arr[idx] : "";
+ }
+
+ private static Properties props(String... keyValues) {
+ Properties p = new Properties();
+ for (int i = 0; i < keyValues.length - 1; i += 2) {
+ p.setProperty(keyValues[i], keyValues[i + 1]);
+ }
+ return p;
+ }
+
+ // ================================================================
+ // 스칼라 변수 치환 ${path}
+ // ================================================================
+
+ @Nested
+ @DisplayName("StandardMessage 스칼라 변수 치환 ${path}")
+ class ScalarSubstitution {
+
+ @Test
+ @DisplayName("단순 JSON 템플릿 치환")
+ void 단순_치환() {
+ StandardMessage msg = buildMsg("E001", "오류발생", "상세설명", null);
+ String template = "{\"code\":\"${MSG.MAIN_MSG.outp_msg_cd}\",\"msg\":\"${MSG.MAIN_MSG.outp_msg_ctnt}\"}";
+
+ String result = handler.render(template, msg, null);
+
+ assertEquals("{\"code\":\"E001\",\"msg\":\"오류발생\"}", result);
+ }
+
+ @Test
+ @DisplayName("동일 변수 중복 치환")
+ void 동일_변수_중복_치환() {
+ StandardMessage msg = buildMsg("E001", "오류", "", null);
+ String template = "${MSG.MAIN_MSG.outp_msg_cd}/${MSG.MAIN_MSG.outp_msg_cd}";
+
+ assertEquals("E001/E001", handler.render(template, msg, null));
+ }
+
+ @Test
+ @DisplayName("존재하지 않는 경로는 빈 문자열로 치환")
+ void 없는_경로_빈_문자열() {
+ StandardMessage msg = buildMsg("E001", "오류", "", null);
+
+ assertEquals("", handler.render("${NONE.PATH}", msg, null));
+ }
+
+ @Test
+ @DisplayName("변수 없는 템플릿은 원본 그대로 반환")
+ void 변수_없는_템플릿_원본() {
+ StandardMessage msg = buildMsg("E001", "오류", "", null);
+
+ assertEquals("{\"static\":\"value\"}", handler.render("{\"static\":\"value\"}", msg, null));
+ }
+
+ @Test
+ @DisplayName("outp_msg_desc 필드 치환")
+ void outp_msg_desc_치환() {
+ StandardMessage msg = buildMsg("E001", "오류", "상세설명입니다", null);
+
+ assertEquals("상세설명입니다", handler.render("${MSG.MAIN_MSG.outp_msg_desc}", msg, null));
+ }
+ }
+
+ // ================================================================
+ // callProp 변수 치환 ${callprop.키}
+ // ================================================================
+
+ @Nested
+ @DisplayName("callProp 변수 치환 ${callprop.키}")
+ class CallPropSubstitution {
+
+ @Test
+ @DisplayName("callProp 값을 템플릿에서 참조")
+ void callProp_단순_참조() {
+ StandardMessage msg = buildMsg("E001", "오류", "", null);
+ Properties callProp = props("ADAPTER_NAME", "MY_ADAPTER", "IF_ID", "SVC001");
+ String template = "{\"adapter\":\"${callprop.ADAPTER_NAME}\",\"ifId\":\"${callprop.IF_ID}\"}";
+
+ String result = handler.render(template, msg, callProp);
+
+ assertEquals("{\"adapter\":\"MY_ADAPTER\",\"ifId\":\"SVC001\"}", result);
+ }
+
+ @Test
+ @DisplayName("callProp 과 StandardMessage 혼합 참조")
+ void callProp_stdmsg_혼합() {
+ StandardMessage msg = buildMsg("E001", "오류발생", "", null);
+ Properties callProp = props("ADAPTER_NAME", "MY_ADAPTER");
+ String template = "{\"adapter\":\"${callprop.ADAPTER_NAME}\",\"code\":\"${MSG.MAIN_MSG.outp_msg_cd}\"}";
+
+ String result = handler.render(template, msg, callProp);
+
+ assertEquals("{\"adapter\":\"MY_ADAPTER\",\"code\":\"E001\"}", result);
+ }
+
+ @Test
+ @DisplayName("callProp 에 없는 키는 빈 문자열")
+ void callProp_없는_키_빈문자열() {
+ StandardMessage msg = buildMsg("E001", "오류", "", null);
+ Properties callProp = new Properties();
+
+ assertEquals("", handler.render("${callprop.NONE_KEY}", msg, callProp));
+ }
+
+ @Test
+ @DisplayName("callProp 이 null 이면 빈 문자열")
+ void callProp_null_빈문자열() {
+ StandardMessage msg = buildMsg("E001", "오류", "", null);
+
+ assertEquals("", handler.render("${callprop.ADAPTER_NAME}", msg, null));
+ }
+ }
+
+ // ================================================================
+ // 배열 반복 {{#foreach path}}...{{/foreach}}
+ // ================================================================
+
+ @Nested
+ @DisplayName("배열 반복 블록 {{#foreach}}")
+ class ForeachBlock {
+
+ @Test
+ @DisplayName("MSG_LIST 1건 렌더링")
+ void 단건_렌더링() {
+ StandardMessage msg = buildMsg("E001", "대표오류", "",
+ new String[][]{{"E001", "메시지A", "설명A", "fieldA"}});
+ String template = "[{{#foreach MSG.MSG_LIST}}{\"c\":\"${outp_msg_cd}\",\"m\":\"${outp_msg_ctnt}\"}{{/foreach}}]";
+
+ assertEquals("[{\"c\":\"E001\",\"m\":\"메시지A\"}]", handler.render(template, msg, null));
+ }
+
+ @Test
+ @DisplayName("MSG_LIST 2건 → 쉼표로 연결")
+ void 다건_쉼표_연결() {
+ StandardMessage msg = buildMsg("E001", "대표오류", "",
+ new String[][]{
+ {"E001", "오류A", "설명A", "fieldA"},
+ {"E002", "오류B", "설명B", "fieldB"}
+ });
+ String template = "[{{#foreach MSG.MSG_LIST}}{\"c\":\"${outp_msg_cd}\"}{{/foreach}}]";
+
+ assertEquals("[{\"c\":\"E001\"},{\"c\":\"E002\"}]", handler.render(template, msg, null));
+ }
+
+ @Test
+ @DisplayName("MSG_LIST 가 비어있으면 foreach 결과는 빈 문자열")
+ void 빈_배열() {
+ StandardMessage msg = buildMsg("E001", "오류", "", new String[][]{});
+ String template = "[{{#foreach MSG.MSG_LIST}}{\"c\":\"${outp_msg_cd}\"}{{/foreach}}]";
+
+ assertEquals("[]", handler.render(template, msg, null));
+ }
+
+ @Test
+ @DisplayName("foreach 경로가 없으면 빈 문자열")
+ void 없는_경로() {
+ StandardMessage msg = buildMsg("E001", "오류", "", null);
+ String template = "[{{#foreach NONE.LIST}}{\"c\":\"${outp_msg_cd}\"}{{/foreach}}]";
+
+ assertEquals("[]", handler.render(template, msg, null));
+ }
+
+ @Test
+ @DisplayName("err_occu_item_nm 필드 포함 렌더링")
+ void err_occu_item_nm_포함() {
+ StandardMessage msg = buildMsg("E001", "오류", "",
+ new String[][]{{"E001", "오류A", "설명A", "accountNo"}});
+ String template = "{{#foreach MSG.MSG_LIST}}{\"field\":\"${err_occu_item_nm}\"}{{/foreach}}";
+
+ assertEquals("{\"field\":\"accountNo\"}", handler.render(template, msg, null));
+ }
+ }
+
+ // ================================================================
+ // 혼합 템플릿 (스칼라 + callProp + 배열)
+ // ================================================================
+
+ @Nested
+ @DisplayName("혼합 템플릿")
+ class MixedTemplate {
+
+ @Test
+ @DisplayName("JSON – MAIN_MSG + callProp + MSG_LIST 2건")
+ void JSON_전체_혼합() {
+ StandardMessage msg = buildMsg("E001", "대표오류", "대표설명",
+ new String[][]{
+ {"E001", "오류A", "설명A", "fieldA"},
+ {"E002", "오류B", "설명B", "fieldB"}
+ });
+ Properties callProp = props("ADAPTER_NAME", "TEST_ADAPTER");
+
+ String template =
+ "{" +
+ "\"adapter\":\"${callprop.ADAPTER_NAME}\"," +
+ "\"resultCode\":\"${MSG.MAIN_MSG.outp_msg_cd}\"," +
+ "\"resultMsg\":\"${MSG.MAIN_MSG.outp_msg_ctnt}\"," +
+ "\"errors\":[{{#foreach MSG.MSG_LIST}}" +
+ "{\"code\":\"${outp_msg_cd}\",\"msg\":\"${outp_msg_ctnt}\",\"field\":\"${err_occu_item_nm}\"}" +
+ "{{/foreach}}]" +
+ "}";
+
+ String result = handler.render(template, msg, callProp);
+
+ assertEquals(
+ "{\"adapter\":\"TEST_ADAPTER\"," +
+ "\"resultCode\":\"E001\",\"resultMsg\":\"대표오류\"," +
+ "\"errors\":[{\"code\":\"E001\",\"msg\":\"오류A\",\"field\":\"fieldA\"}," +
+ "{\"code\":\"E002\",\"msg\":\"오류B\",\"field\":\"fieldB\"}]}",
+ result);
+ }
+
+ @Test
+ @DisplayName("XML – 스칼라 + callProp 치환")
+ void XML_스칼라_callProp() {
+ StandardMessage msg = buildMsg("E001", "오류발생", "", null);
+ Properties callProp = props("IF_ID", "IF_001");
+ String template = "${callprop.IF_ID}${MSG.MAIN_MSG.outp_msg_cd}";
+
+ assertEquals("IF_001E001",
+ handler.render(template, msg, callProp));
+ }
+
+ @Test
+ @DisplayName("XML – foreach 배열")
+ void XML_foreach_배열() {
+ StandardMessage msg = buildMsg("E001", "오류", "",
+ new String[][]{{"E001", "오류A", "", ""}, {"E002", "오류B", "", ""}});
+ String template =
+ "{{#foreach MSG.MSG_LIST}}" +
+ "${outp_msg_cd}${outp_msg_ctnt} " +
+ "{{/foreach}}";
+
+ assertEquals(
+ "" +
+ "E001오류A ," +
+ "E002오류B " +
+ "",
+ handler.render(template, msg, null));
+ }
+ }
+
+ // ================================================================
+ // generateNonStandardErrorResponseMessage – AdapterPropManager mock
+ // ================================================================
+
+ @Nested
+ @DisplayName("generateNonStandardErrorResponseMessage")
+ class GenerateMethod {
+
+ private void injectMockPropManager(AdapterPropManager mockPropManager) throws Exception {
+ ApplicationContext mockCtx = Mockito.mock(ApplicationContext.class);
+ Mockito.when(mockCtx.getBean(AdapterPropManager.class)).thenReturn(mockPropManager);
+ Field ctxField = ApplicationContextProvider.class.getDeclaredField("context");
+ ctxField.setAccessible(true);
+ ctxField.set(null, mockCtx);
+ }
+
+ @Test
+ @DisplayName("템플릿 미설정 시 null 반환")
+ void 템플릿_없으면_null() throws Exception {
+ AdapterPropManager mockProp = Mockito.mock(AdapterPropManager.class);
+ Mockito.when(mockProp.getProperty(anyString(), anyString())).thenReturn(null);
+ injectMockPropManager(mockProp);
+
+ StandardMessage msg = buildMsg("E001", "오류", "", null);
+ Object result = handler.generateNonStandardErrorResponseMessage(
+ "TEST_GROUP", "TEST_ADAPTER", new Properties(), null, msg);
+
+ assertNull(result);
+ }
+
+ @Test
+ @DisplayName("빈 템플릿 시 null 반환")
+ void 빈_템플릿_null() throws Exception {
+ AdapterPropManager mockProp = Mockito.mock(AdapterPropManager.class);
+ Mockito.when(mockProp.getProperty(anyString(), anyString())).thenReturn(" ");
+ injectMockPropManager(mockProp);
+
+ StandardMessage msg = buildMsg("E001", "오류", "", null);
+ Object result = handler.generateNonStandardErrorResponseMessage(
+ "TEST_GROUP", "TEST_ADAPTER", new Properties(), null, msg);
+
+ assertNull(result);
+ }
+
+ @Test
+ @DisplayName("템플릿 설정 시 StandardMessage 치환 결과 반환")
+ void 템플릿_stdmsg_치환() throws Exception {
+ String template = "{\"code\":\"${MSG.MAIN_MSG.outp_msg_cd}\"}";
+ AdapterPropManager mockProp = Mockito.mock(AdapterPropManager.class);
+ Mockito.when(mockProp.getProperty(
+ eq(TemplateAdapterErrorMsgHandler.PROP_GROUP),
+ eq("MY_GROUP.template"))).thenReturn(template);
+ injectMockPropManager(mockProp);
+
+ StandardMessage msg = buildMsg("E999", "테스트오류", "", null);
+ Object result = handler.generateNonStandardErrorResponseMessage(
+ "MY_GROUP", "MY_ADAPTER", new Properties(), null, msg);
+
+ assertEquals("{\"code\":\"E999\"}", result);
+ }
+
+ @Test
+ @DisplayName("callProp 값이 템플릿에 치환되어 반환")
+ void 템플릿_callProp_치환() throws Exception {
+ String template = "{\"adapter\":\"${callprop.ADAPTER_NAME}\",\"code\":\"${MSG.MAIN_MSG.outp_msg_cd}\"}";
+ AdapterPropManager mockProp = Mockito.mock(AdapterPropManager.class);
+ Mockito.when(mockProp.getProperty(
+ eq(TemplateAdapterErrorMsgHandler.PROP_GROUP),
+ eq("MY_GROUP.template"))).thenReturn(template);
+ injectMockPropManager(mockProp);
+
+ StandardMessage msg = buildMsg("E999", "테스트오류", "", null);
+ Properties callProp = props("ADAPTER_NAME", "REAL_ADAPTER");
+
+ Object result = handler.generateNonStandardErrorResponseMessage(
+ "MY_GROUP", "MY_ADAPTER", callProp, null, msg);
+
+ assertEquals("{\"adapter\":\"REAL_ADAPTER\",\"code\":\"E999\"}", result);
+ }
+
+ @Test
+ @DisplayName("템플릿 키는 {adapterGroupId}.template 형식으로 조회")
+ void 템플릿_키_형식_검증() throws Exception {
+ AdapterPropManager mockProp = Mockito.mock(AdapterPropManager.class);
+ Mockito.when(mockProp.getProperty(anyString(), anyString())).thenReturn(null);
+ injectMockPropManager(mockProp);
+
+ StandardMessage msg = buildMsg("E001", "오류", "", null);
+ handler.generateNonStandardErrorResponseMessage(
+ "SAMPLE_GROUP", "ANY_ADAPTER", new Properties(), null, msg);
+
+ Mockito.verify(mockProp).getProperty(
+ TemplateAdapterErrorMsgHandler.PROP_GROUP,
+ "SAMPLE_GROUP.template");
+ }
+ }
+}