feat: DJBank 차세대 표준전문(R10) 초기 구현

- standard-layout-djb.csv: HEAD(평면구조)/EZDATA/MSG/DATA/전문종료부(@JJ) 레이아웃 정의
- standard-message-mapping-config-djb.properties: 논리키 → 필드경로 매핑 (GUID 40자, guid_prgs_no 별도 필드 등)
- standard-message-config.properties: DJB 클래스로 전환 (CSV 방식)
- GUIDGeneratorDJB: 40자 GUID 생성 (일자+시간+밀리초+시스템코드+노드+세션+Random)
- InterfaceMapperDJB: GUID 40자, guid_prgs_no 독립 필드, 응답코드 S/F 변환
- StandardMessageCoordinatorDJB: 전문 생성/응답/에러 각 단계 후처리
- 단위테스트 45개 전체 통과 (GUIDGenerator/InterfaceMapper/Layout 검증)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
curry772
2026-04-20 11:46:18 +09:00
parent 51724ab0f5
commit 18b3e781b1
9 changed files with 1252 additions and 4 deletions
@@ -0,0 +1,92 @@
package com.eactive.eai.custom.message;
import java.security.SecureRandom;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.atomic.AtomicInteger;
/**
* DJBank(제주은행) 차세대 표준전문 GUID 생성기
*
* GUID 구성 (40자):
* 일자(8) + 시간(6) + 밀리초(3) + 시스템코드(3) + 노드번호(2) + 세션ID(12) + Random(6)
* = 8 + 6 + 3 + 3 + 2 + 12 + 6 = 40자
*/
public final class GUIDGeneratorDJB {
public static final int GUID_LENGTH = 40;
private static final DateTimeFormatter FMT_DATE = DateTimeFormatter.ofPattern("yyyyMMdd");
private static final DateTimeFormatter FMT_TIME = DateTimeFormatter.ofPattern("HHmmss");
private static final DateTimeFormatter FMT_MILLIS = DateTimeFormatter.ofPattern("SSS");
private static final AtomicInteger seq = new AtomicInteger(1);
private static final SecureRandom rnd = new SecureRandom();
private static final String NODE_NO;
private static final String SESSION_ID_BASE;
static {
// 노드번호(2): 서버명 마지막 2자리, 없으면 "00"
String serverName = System.getProperty("server.key", "");
if (serverName.length() >= 2) {
NODE_NO = serverName.substring(serverName.length() - 2);
} else {
NODE_NO = "00";
}
// 세션ID 기반(12): 호스트명 앞 12자 (부족 시 '_'로 우측패딩)
String hostname;
try {
hostname = java.net.InetAddress.getLocalHost().getHostName();
} catch (Exception e) {
hostname = "____________";
}
if (hostname.length() >= 12) {
SESSION_ID_BASE = hostname.substring(0, 12).toUpperCase();
} else {
SESSION_ID_BASE = String.format("%-12s", hostname).toUpperCase().replace(' ', '_');
}
}
private GUIDGeneratorDJB() {
}
/**
* @param systemCode 시스템구분코드 3자 (예: "EAI", "OPA")
*/
public static String getGUID(String systemCode) {
LocalDateTime now = LocalDateTime.now();
String date = now.format(FMT_DATE); // 8자
String time = now.format(FMT_TIME); // 6자
String millis = now.format(FMT_MILLIS); // 3자
// 시스템코드 3자 정규화
String sysCode = normalizeCode(systemCode, 3);
// 노드번호 2자
String nodeNo = NODE_NO;
// 세션ID 12자
String sessionId = SESSION_ID_BASE;
// Random 6자 (숫자)
int seqVal = seq.getAndUpdate(v -> v >= 999999 ? 1 : v + 1);
String random = String.format("%06d", seqVal);
String guid = date + time + millis + sysCode + nodeNo + sessionId + random;
if (guid.length() != GUID_LENGTH) {
throw new IllegalStateException("GUID length error: " + guid.length() + " [" + guid + "]");
}
return guid;
}
private static String normalizeCode(String code, int len) {
if (code == null) code = "";
code = code.toUpperCase();
if (code.length() >= len) return code.substring(0, len);
return String.format("%-" + len + "s", code).replace(' ', '_');
}
}
@@ -0,0 +1,222 @@
package com.eactive.eai.custom.message;
import org.apache.commons.lang3.StringUtils;
import com.eactive.eai.common.server.EAIServerManager;
import com.eactive.eai.message.StandardMessage;
import com.eactive.eai.message.service.DefaultInterfaceMapper;
import com.ext.eai.common.stdmessage.STDMessageKeys;
/**
* DJBank(제주은행) 차세대 표준전문 InterfaceMapper
*
* HEAD 영역이 평면 구조로 변경됨에 따라 필드 접근 경로 및 값 변환 로직을 재정의.
*
* 주요 변경사항:
* - GUID: 40자 (기존 36자)
* - GUID 진행번호: HEAD.guid_prgs_no 별도 필드 (기존 GUID 문자열 내 포함)
* - 처리결과구분코드: S(성공)/F(오류) (기존 NR/ER)
* - 시스템환경구분코드: D/T/P (기존 O/S/D)
*/
@SuppressWarnings("serial")
public class InterfaceMapperDJB extends DefaultInterfaceMapper {
private static final String PATH_MCI_NODE = "HEAD.mci_node_dvcd";
private static final String PATH_EAI_NODE = "HEAD.eai_node_dvcd";
private static final String PATH_MCI_SESN = "HEAD.mci_sesn_id";
// 처리결과구분코드 DJBank 값
private static final String PROCS_RSLT_SUCCESS = "S";
private static final String PROCS_RSLT_FAIL = "F";
@Override
public String getEaiSvcCode(StandardMessage standardMessage) {
String interfaceId = getInterfaceId(standardMessage);
String sendRecvDvcd = getSendRecvDivision(standardMessage); // S | R
String inExDivision = getInExDivision(standardMessage); // 1 | 2
if (interfaceId == null) interfaceId = "";
interfaceId = interfaceId.trim();
return interfaceId + sendRecvDvcd + StringUtils.defaultString(inExDivision);
}
// ----------------------------------------------------------------
// GUID (40자 고정)
// ----------------------------------------------------------------
@Override
public void setGuid(StandardMessage standardMessage, String guid) {
if (guid == null) throw new RuntimeException("guid is null");
if (guid.length() != GUIDGeneratorDJB.GUID_LENGTH) {
throw new RuntimeException("GUID length must be " + GUIDGeneratorDJB.GUID_LENGTH + ", actual=" + guid.length());
}
setItemValue(standardMessage, getPath(GUID), guid);
}
@Override
public String getGuid(StandardMessage standardMessage) {
return StringUtils.defaultString(getItemValue(standardMessage, getPath(GUID)));
}
// ----------------------------------------------------------------
// GUID 진행번호 — HEAD.guid_prgs_no 별도 필드
// ----------------------------------------------------------------
@Override
public String getGuidSeq(StandardMessage standardMessage) {
String val = getItemValue(standardMessage, getPath(GUID_SEQ));
if (StringUtils.isBlank(val)) return "001";
try {
// NUMBER 타입은 내부적으로 선행 0 제거 → 조회 시 3자리로 복원
return String.format("%03d", Integer.parseInt(val.trim()));
} catch (NumberFormatException e) {
return StringUtils.defaultString(val, "001");
}
}
@Override
public void setGuidSeq(StandardMessage standardMessage, String guidSeq) {
setItemValue(standardMessage, getPath(GUID_SEQ), guidSeq);
}
@Override
public String nextGuidSeq(StandardMessage standardMessage) {
String current = getGuidSeq(standardMessage);
int seq = 1;
try {
seq = Integer.parseInt(current.trim()) + 1;
} catch (NumberFormatException e) {
seq = 2;
}
String next = String.format("%03d", seq);
setGuidSeq(standardMessage, next);
return next;
}
// ----------------------------------------------------------------
// 원거래 GUID — HEAD.ortr_guid (40자 그대로)
// ----------------------------------------------------------------
@Override
public String getOrgGuid(StandardMessage standardMessage) {
return StringUtils.defaultString(getItemValue(standardMessage, getPath(GUID_ORG)));
}
@Override
public void setOrgGuid(StandardMessage standardMessage, String orgGuid) {
setItemValue(standardMessage, getPath(GUID_ORG), orgGuid);
}
// ----------------------------------------------------------------
// 처리결과구분코드: 엔진 내부(0=정상, 1=오류) ↔ DJBank(S/F)
// ----------------------------------------------------------------
@Override
public String getResponseType(StandardMessage standardMessage) {
String val = getItemValue(standardMessage, getPath(RESPONSE_TYPE));
if (PROCS_RSLT_SUCCESS.equals(val)) return STDMessageKeys.RESPONSE_TYPE_CODE_N;
if (PROCS_RSLT_FAIL.equals(val)) return STDMessageKeys.RESPONSE_TYPE_CODE_E;
return val;
}
@Override
public void setResponseType(StandardMessage standardMessage, String responseType) {
String siteVal;
if (STDMessageKeys.RESPONSE_TYPE_CODE_N.equals(responseType)) {
siteVal = PROCS_RSLT_SUCCESS;
} else if (STDMessageKeys.RESPONSE_TYPE_CODE_E.equals(responseType)) {
siteVal = PROCS_RSLT_FAIL;
} else {
siteVal = responseType;
}
setItemValue(standardMessage, getPath(RESPONSE_TYPE), siteVal);
}
// ----------------------------------------------------------------
// 원거래복원여부: Y/N 그대로 사용 (엔진 내부 1/0과 변환)
// ----------------------------------------------------------------
@Override
public String getRecoverYn(StandardMessage standardMessage) {
String val = getItemValue(standardMessage, getPath(RECOVER_YN));
return "Y".equals(val) ? "1" : "0";
}
@Override
public void setRecoverYn(StandardMessage standardMessage, String recoverYn) {
setItemValue(standardMessage, getPath(RECOVER_YN), "1".equals(recoverYn) ? "Y" : "N");
}
// ----------------------------------------------------------------
// 인터페이스ID (trim 처리)
// ----------------------------------------------------------------
@Override
public String getInterfaceId(StandardMessage standardMessage) {
String val = getItemValue(standardMessage, getPath(INTERFACE_ID));
return val == null ? "" : val.trim();
}
// ----------------------------------------------------------------
// 세션ID / 인스턴스코드 — MCI 여부에 따라 처리
// ----------------------------------------------------------------
@Override
public String getSessionId(StandardMessage standardMessage) {
EAIServerManager server = EAIServerManager.getInstance();
if (server != null && server.isMCI()) {
return StringUtils.defaultString(getItemValue(standardMessage, PATH_MCI_SESN));
}
return "";
}
@Override
public void setSessionId(StandardMessage standardMessage, String sessionId) {
EAIServerManager server = EAIServerManager.getInstance();
if (server != null && server.isMCI()) {
setItemValue(standardMessage, PATH_MCI_SESN, sessionId);
}
}
@Override
public String getInstCode(StandardMessage standardMessage) {
EAIServerManager server = EAIServerManager.getInstance();
if (server != null && server.isMCI()) {
return StringUtils.defaultString(getItemValue(standardMessage, PATH_MCI_NODE));
}
return StringUtils.defaultString(getItemValue(standardMessage, PATH_EAI_NODE));
}
@Override
public void setInstCode(StandardMessage standardMessage, String instCode) {
EAIServerManager server = EAIServerManager.getInstance();
if (server != null && server.isMCI()) {
setItemValue(standardMessage, PATH_MCI_NODE, instCode);
} else {
setItemValue(standardMessage, PATH_EAI_NODE, instCode);
}
}
// ----------------------------------------------------------------
// 요청시스템코드 — 채널유형코드(chnl_tycd)에서 추출
// ----------------------------------------------------------------
@Override
public String getReqSysCode(StandardMessage standardMessage) {
String chnlTycd = getItemValue(standardMessage, getPath(CHANNEL_TYPE_CD));
if (StringUtils.isNotBlank(chnlTycd)) {
return chnlTycd.trim();
}
// fallback: tx_id(서비스ID) 경로에서 추출
String txId = getItemValue(standardMessage, getPath(SERVICE_ID));
if (txId != null) {
String[] split = txId.split("/");
if (split.length > 1) {
String route = split[1].toUpperCase();
return route.length() > 3 ? route.substring(0, 3) : route;
}
}
return null;
}
}
@@ -0,0 +1,257 @@
package com.eactive.eai.custom.message;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.LinkedHashMap;
import java.util.Properties;
import org.apache.commons.lang3.StringUtils;
import com.eactive.eai.common.server.EAIServerManager;
import com.eactive.eai.common.util.Logger;
import com.eactive.eai.common.util.MessageUtil;
import com.eactive.eai.inbound.processor.ProcessVO;
import com.eactive.eai.message.StandardItem;
import com.eactive.eai.message.StandardMessage;
import com.eactive.eai.message.manager.StandardMessageManager;
import com.eactive.eai.message.service.DefaultStandardMessageCoordinator;
import com.eactive.eai.message.service.InterfaceMapper;
import com.ext.eai.common.stdmessage.STDMessageKeys;
/**
* DJBank(제주은행) 차세대 표준전문 Coordinator
*
* 표준전문 구조 변경에 따른 각 처리 단계 후처리 정의:
* - HEAD 평면 구조, GUID 40자, guid_prgs_no 별도 필드
* - MSG 영역: msg_dvcd(NM/EM) + MAIN_MSG + msg_list
* - 처리결과구분코드: S(성공) / F(오류)
* - 전문종료부: @JJ
*/
public class StandardMessageCoordinatorDJB extends DefaultStandardMessageCoordinator {
private static final Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT);
private static final DateTimeFormatter FMT_DATE = DateTimeFormatter.ofPattern("yyyyMMdd");
private static final DateTimeFormatter FMT_TIME_MILLIS = DateTimeFormatter.ofPattern("HHmmssSSS");
// HEAD 필드 경로
private static final String HEAD_FRST_MESG_DMAN_DT = "HEAD.frst_mesg_dman_dt";
private static final String HEAD_FRST_MESG_DMAN_TIME = "HEAD.frst_mesg_dman_time";
private static final String HEAD_FRST_TRNM_IPAD = "HEAD.frst_trnm_ipad";
private static final String HEAD_FRST_TRNM_IPV4_ADDR = "HEAD.frst_trnm_ipv4_addr";
private static final String HEAD_FRST_TRNM_MAC = "HEAD.frst_trnm_mac";
private static final String HEAD_SYS_ENV_DVCD = "HEAD.sys_env_dvcd";
private static final String HEAD_TRNM_SYS_DVCD = "HEAD.trnm_sys_dvcd";
private static final String HEAD_MESG_RSPN_DT = "HEAD.mesg_rspn_dt";
private static final String HEAD_MESG_RSPN_TIME = "HEAD.mesg_rspn_time";
// MSG 영역 경로
private static final String MSG_DVCD = "MSG.msg_dvcd";
private static final String MSG_SCOP_LEN = "MSG.msg_scop_len";
private static final String MSG_MAIN_MSG = "MSG.MAIN_MSG";
private static final String MSG_OUTP_ATRB_CD = "MSG.MAIN_MSG.outp_atrb_cd";
private static final String MSG_OUTP_MSG_CD = "MSG.MAIN_MSG.outp_msg_cd";
private static final String MSG_OUTP_MSG_CTNT = "MSG.MAIN_MSG.outp_msg_ctnt";
private static final String MSG_LIST_ROWCNT = "MSG.MAIN_MSG.msg_list_rowcnt";
private static final String MSG_LIST = "MSG.msg_list";
// DATA 영역 경로
private static final String DATA_SCOP_LEN = "DATA.data_scop_len";
@Override
public void coordinateAfterParsing(StandardMessage standardMessage, Object paramObject, Properties prop) {
}
@Override
public void coordinateAfterCreateMessageByRule(StandardMessage standardMessage, Object paramObject, Properties prop) {
StandardMessageManager manager = StandardMessageManager.getInstance();
InterfaceMapper mapper = manager.getMapper();
// GUID 생성 (40자) 및 설정
String guid = GUIDGeneratorDJB.getGUID("EAI");
mapper.setGuid(standardMessage, guid);
// GUID 진행번호 초기화 (최초=1)
mapper.setGuidSeq(standardMessage, "001");
// 원거래GUID = GUID 동일값 설정 (복원이 아닌 경우)
mapper.setOrgGuid(standardMessage, guid);
// 최초전문요청일자/시각 설정
LocalDateTime now = LocalDateTime.now();
standardMessage.setData(HEAD_FRST_MESG_DMAN_DT, now.format(FMT_DATE));
standardMessage.setData(HEAD_FRST_MESG_DMAN_TIME, now.format(FMT_TIME_MILLIS));
// 전송시스템구분코드 설정
standardMessage.setData(HEAD_TRNM_SYS_DVCD, "EAI");
// 시스템환경구분코드 설정 (D/T/P)
EAIServerManager eaiServer = EAIServerManager.getInstance();
standardMessage.setData(HEAD_SYS_ENV_DVCD, getSysEnvDvcd(eaiServer));
// 최초전송 IP / IPv4 / MAC 설정
setNetworkInfo(standardMessage);
}
@Override
public void coordinateBeforeStandardMessageSend(StandardMessage standardMessage, String msgType, String charset) {
makeupDataScopLen(standardMessage, charset);
}
@Override
public void coordinateBeforeResponse(StandardMessage standardMessage, ProcessVO processVO, Properties prop, String charset) {
// 응답일자/시각 설정
LocalDateTime now = LocalDateTime.now();
standardMessage.setData(HEAD_MESG_RSPN_DT, now.format(FMT_DATE));
standardMessage.setData(HEAD_MESG_RSPN_TIME, now.format(FMT_TIME_MILLIS));
makeupDataScopLen(standardMessage, charset);
}
@Override
public boolean coordinateSetStandardMessageError(StandardMessage standardMessage, InterfaceMapper mapper,
String errCode, String errMsg) {
// 처리결과 = 오류(F)
mapper.setResponseType(standardMessage, STDMessageKeys.RESPONSE_TYPE_CODE_E);
// 요청응답구분 = 응답(R)
mapper.setSendRecvDivision(standardMessage, STDMessageKeys.SEND_RECV_CD_RECV);
// 에러코드/메시지 변환
String siteErrCode = StringUtils.defaultString(MessageUtil.getTobeCode(errCode));
String siteErrMsg = StringUtils.defaultString(MessageUtil.getTobeMessage(errCode));
// MSG 영역 활성화
StandardItem msgItem = standardMessage.findItem("MSG");
if (msgItem != null) {
msgItem.setHidden(false);
msgItem.setSize(1);
}
// msg_dvcd = EM (에러메시지)
standardMessage.setData(MSG_DVCD, "EM");
// 출력속성코드 = 1(팝업)
standardMessage.setData(MSG_OUTP_ATRB_CD, "1");
// 대표 에러코드/메시지
standardMessage.setData(MSG_OUTP_MSG_CD, siteErrCode);
standardMessage.setData(MSG_OUTP_MSG_CTNT, siteErrMsg);
// msg_list 1건 구성
standardMessage.setData(MSG_LIST_ROWCNT, "00001");
StandardItem msgList = standardMessage.findItem(MSG_LIST);
if (msgList != null) {
msgList.setSize(1);
LinkedHashMap<String, StandardItem> row = msgList.getArrayChilds(0, true);
if (row != null) {
if (row.containsKey("outp_msg_cd")) row.get("outp_msg_cd").setValue(siteErrCode);
if (row.containsKey("outp_msg_ctnt")) row.get("outp_msg_ctnt").setValue(siteErrMsg);
}
}
// mapper 에러코드 설정
mapper.setErrorCode(standardMessage, siteErrCode);
mapper.setErrorMsg(standardMessage, siteErrMsg);
// msg_scop_len 계산
makeMsgScopLen(standardMessage);
// BIZ_DATA는 null 처리 (false 반환)
return false;
}
@Override
public void coordinateAfterRecvNonStdSyncResponse(StandardMessage responseMessage) {
// 처리결과 = 성공(S)
StandardMessageManager manager = StandardMessageManager.getInstance();
InterfaceMapper mapper = manager.getMapper();
mapper.setResponseType(responseMessage, STDMessageKeys.RESPONSE_TYPE_CODE_N);
// MSG 영역 활성화
StandardItem msgItem = responseMessage.findItem("MSG");
if (msgItem != null) {
msgItem.setHidden(false);
msgItem.setSize(1);
}
// msg_dvcd = NM (정상메시지)
responseMessage.setData(MSG_DVCD, "NM");
responseMessage.setData(MSG_OUTP_ATRB_CD, "0");
responseMessage.setData(MSG_OUTP_MSG_CD, "000000000000");
responseMessage.setData(MSG_OUTP_MSG_CTNT, "정상처리");
responseMessage.setData(MSG_LIST_ROWCNT, "00001");
StandardItem msgList = responseMessage.findItem(MSG_LIST);
if (msgList != null) {
msgList.setSize(1);
LinkedHashMap<String, StandardItem> row = msgList.getArrayChilds(0, true);
if (row != null) {
if (row.containsKey("outp_msg_cd")) row.get("outp_msg_cd").setValue("000000000000");
if (row.containsKey("outp_msg_ctnt")) row.get("outp_msg_ctnt").setValue("정상처리");
}
}
makeMsgScopLen(responseMessage);
}
// ----------------------------------------------------------------
// private helpers
// ----------------------------------------------------------------
private String getSysEnvDvcd(EAIServerManager server) {
if (server.isPEAIServer()) return "P"; // 운영
if (server.isSEAIServer()) return "T"; // 검증/테스트
return "D"; // 개발
}
private void setNetworkInfo(StandardMessage standardMessage) {
try {
InetAddress localAddr = InetAddress.getLocalHost();
String ipv4 = localAddr.getHostAddress();
// IPv4 주소 설정
standardMessage.setData(HEAD_FRST_TRNM_IPAD, ipv4);
standardMessage.setData(HEAD_FRST_TRNM_IPV4_ADDR, ipv4);
// MAC 주소 설정
NetworkInterface ni = NetworkInterface.getByInetAddress(localAddr);
if (ni != null) {
byte[] mac = ni.getHardwareAddress();
if (mac != null) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < mac.length; i++) {
if (i > 0) sb.append(":");
sb.append(String.format("%02X", mac[i]));
}
standardMessage.setData(HEAD_FRST_TRNM_MAC, sb.toString());
}
}
} catch (Exception e) {
if (logger.isWarn()) logger.warn("Failed to set network info", e);
}
}
private void makeupDataScopLen(StandardMessage standardMessage, String charset) {
try {
int dataLen = standardMessage.getBizDataBytes(charset).length;
String lenStr = StringUtils.leftPad(String.valueOf(dataLen), 8, '0');
standardMessage.setData(DATA_SCOP_LEN, lenStr);
} catch (Exception e) {
throw new RuntimeException("Failed to calculate data_scop_len", e);
}
}
private void makeMsgScopLen(StandardMessage standardMessage) {
try {
StandardItem mainMsg = standardMessage.findItem(MSG_MAIN_MSG);
if (mainMsg == null) return;
int len = standardMessage.getBytesDataLengthForPath(MSG_MAIN_MSG);
standardMessage.setData(MSG_SCOP_LEN, StringUtils.leftPad(String.valueOf(len), 8, '0'));
} catch (Exception e) {
if (logger.isWarn()) logger.warn("Failed to calculate msg_scop_len", e);
}
}
}