From f883ab754dbb5c72a510f8503eca30bd33ca4117 Mon Sep 17 00:00:00 2001 From: forge-bot Date: Sat, 18 Jul 2026 11:17:45 +0000 Subject: [PATCH] =?UTF-8?q?[=EB=A7=A4=EC=9E=85]=20mg=5Frecv=5Fsvc=20?= =?UTF-8?q?=E2=86=92=20=EC=A0=84=EB=AC=B8=EC=88=98=EC=8B=A0=20@Service=20(?= =?UTF-8?q?ACM-MG-004)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../acquiring/service/TxMessageCodec.java | 158 ++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 boot/modules/acquiring/src/main/java/com/klaro/acquirecore/acquiring/service/TxMessageCodec.java diff --git a/boot/modules/acquiring/src/main/java/com/klaro/acquirecore/acquiring/service/TxMessageCodec.java b/boot/modules/acquiring/src/main/java/com/klaro/acquirecore/acquiring/service/TxMessageCodec.java new file mode 100644 index 0000000..964ecda --- /dev/null +++ b/boot/modules/acquiring/src/main/java/com/klaro/acquirecore/acquiring/service/TxMessageCodec.java @@ -0,0 +1,158 @@ +package com.klaro.acquirecore.acquiring.service; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.klaro.acquirecore.acquiring.dto.TxMessage; +import org.springframework.stereotype.Component; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.UUID; + +/** + * Codec for TX message packing/unpacking. + * Handles binary protocol ↔ domain object conversion. + */ +@Component +public class TxMessageCodec { + + private static final DateTimeFormatter DT_FORMAT = DateTimeFormatter.ofPattern("yyyyMMddHHmmss"); + private static final String ENCODING = "EUC-KR"; + + private final ObjectMapper objectMapper; + + public TxMessageCodec(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + /** + * Unpack binary message to TxMessage domain object. + * Format: HEADER(20) + BODY(n) + */ + public TxMessage unpack(byte[] raw) { + if (raw == null || raw.length < 20) { + throw new IllegalArgumentException("Invalid message: too short"); + } + ByteBuffer buf = ByteBuffer.wrap(raw); + + // HEADER: 20 bytes + byte[] headerBytes = new byte[20]; + buf.get(headerBytes); + String header = new String(headerBytes, StandardCharsets.ISO_8859_1).trim(); + + // BODY: remaining bytes + byte[] bodyBytes = new byte[raw.length - 20]; + buf.get(bodyBytes); + + TxMessage msg = new TxMessage(); + msg.setHeader(header); + msg.setRawBody(bodyBytes); + + // Parse body as JSON or delimited fields + parseBody(msg, bodyBytes); + + return msg; + } + + private void parseBody(TxMessage msg, byte[] body) { + String bodyStr = new String(body, StandardCharsets.ISO_8859_1); + try { + JsonNode node = objectMapper.readTree(bodyStr); + if (node.isObject()) { + ObjectNode obj = (ObjectNode) node; + obj.fields().forEachRemaining(e -> { + String key = e.getKey(); + JsonNode val = e.getValue(); + if (val.isTextual()) { + setField(msg, key, val.asText()); + } else if (val.isNumber()) { + setNumericField(msg, key, val.asLong()); + } + }); + } + } catch (Exception e) { + // Fallback: delimited parse + parseDelimited(msg, bodyStr); + } + } + + private void parseDelimited(TxMessage msg, String body) { + String[] fields = body.split("\\|"); + for (int i = 0; i < fields.length; i++) { + String[] kv = fields[i].split("=", 2); + if (kv.length == 2) { + setField(msg, kv[0].trim(), kv[1].trim()); + } + } + } + + private void setField(TxMessage msg, String key, String value) { + switch (key.toUpperCase()) { + case "TXID": msg.setTxId(value); break; + case "MERCHANTID": msg.setMerchantId(value); break; + case "TERMINALID": msg.setTerminalId(value); break; + case "AMOUNT": msg.setAmount(value); break; + case "STATUS": msg.setStatus(value); break; + case "TRANDATE": msg.setTranDate(value); break; + case "TRANTIME": msg.setTranTime(value); break; + case "AUTHNO": msg.setAuthNo(value); break; + case "CARDNO": msg.setCardNo(value); break; + case "VANMSG": msg.setVanMsg(value); break; + default: msg.setExtra(key, value); + } + } + + private void setNumericField(TxMessage msg, String key, long value) { + if ("AMOUNT".equalsIgnoreCase(key)) { + msg.setAmount(String.valueOf(value)); + } + } + + /** + * Pack TxMessage to binary format. + */ + public byte[] pack(TxMessage msg) { + byte[] body = packBody(msg); + byte[] header = buildHeader(msg, body.length); + + ByteBuffer buf = ByteBuffer.allocate(header.length + body.length); + buf.put(header); + buf.put(body); + return buf.array(); + } + + private byte[] buildHeader(TxMessage msg, int bodyLen) { + String header = String.format("%-20s", + msg.getTxId() != null ? msg.getTxId() : UUID.randomUUID().toString().substring(0, 8)); + return header.getBytes(StandardCharsets.ISO_8859_1); + } + + private byte[] packBody(TxMessage msg) { + try { + ObjectNode node = objectMapper.createObjectNode(); + if (msg.getTxId() != null) node.put("TXID", msg.getTxId()); + if (msg.getMerchantId() != null) node.put("MERCHANTID", msg.getMerchantId()); + if (msg.getTerminalId() != null) node.put("TERMINALID", msg.getTerminalId()); + if (msg.getAmount() != null) node.put("AMOUNT", msg.getAmount()); + if (msg.getStatus() != null) node.put("STATUS", msg.getStatus()); + if (msg.getTranDate() != null) node.put("TRANDATE", msg.getTranDate()); + if (msg.getTranTime() != null) node.put("TRANTIME", msg.getTranTime()); + if (msg.getAuthNo() != null) node.put("AUTHNO", msg.getAuthNo()); + if (msg.getCardNo() != null) node.put("CARDNO", maskCardNo(msg.getCardNo())); + if (msg.getVanMsg() != null) node.put("VANMSG", msg.getVanMsg()); + + msg.getExtra().forEach(node::put); + return objectMapper.writeValueAsString(node).getBytes(StandardCharsets.ISO_8859_1); + } catch (Exception e) { + throw new RuntimeException("Failed to pack message body", e); + } + } + + private String maskCardNo(String cardNo) { + if (cardNo == null || cardNo.length() < 10) return cardNo; + return cardNo.substring(0, 6) + "****" + cardNo.substring(cardNo.length() - 4); + } +}