From 25570111976ab82d5bd28b1aa5fe73fae48f9f33 Mon Sep 17 00:00:00 2001 From: forge-bot Date: Sat, 18 Jul 2026 11:07:26 +0000 Subject: [PATCH 1/8] forge: open work branch for ACM-CM-002-attempt-2-run-87bf2707792a --- .forge/ACM-CM-002-attempt-2-run-87bf2707792a.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .forge/ACM-CM-002-attempt-2-run-87bf2707792a.md diff --git a/.forge/ACM-CM-002-attempt-2-run-87bf2707792a.md b/.forge/ACM-CM-002-attempt-2-run-87bf2707792a.md new file mode 100644 index 0000000..7d71991 --- /dev/null +++ b/.forge/ACM-CM-002-attempt-2-run-87bf2707792a.md @@ -0,0 +1,3 @@ +# ACM-CM-002-attempt-2-run-87bf2707792a + +Forge 이슈 작업 브랜치 `forge/ACM-CM-002-attempt-2-run-87bf2707792a`. -- 2.49.1 From 3cb3b83120d78eba44f0dac167ed0aae3c008c42 Mon Sep 17 00:00:00 2001 From: forge-bot Date: Sat, 18 Jul 2026 11:09:11 +0000 Subject: [PATCH 2/8] =?UTF-8?q?[=EA=B3=B5=ED=86=B5]=20util=20=E2=86=92=20f?= =?UTF-8?q?ramework=20=EC=9C=A0=ED=8B=B8=20(ACM-CM-002)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- legacy/app/common/util_date.c | 161 ++++++++-------------------------- 1 file changed, 39 insertions(+), 122 deletions(-) diff --git a/legacy/app/common/util_date.c b/legacy/app/common/util_date.c index 70cbed6..04aa43f 100644 --- a/legacy/app/common/util_date.c +++ b/legacy/app/common/util_date.c @@ -1,135 +1,52 @@ -/* - * util_date.c - 일자/영업일 유틸리티 (plain C) - */ -#include "acq_util.h" - -#include -#include #include +#include +#include #include -static int is_all_digit(const char *s, int len) -{ - int i; - for (i = 0; i < len; i++) { - if (!isdigit((unsigned char)s[i])) - return 0; - } - return 1; +#define DATE_BUF_SIZE 32 + +typedef struct { + int year; + int month; + int day; +} date_t; + +int date_parse(const char* str, date_t* out) { + if (!str || !out) return -1; + return sscanf(str, "%4d-%2d-%2d", &out->year, &out->month, &out->day); } -static int to_int(const char *s, int len) -{ - int i, v = 0; - for (i = 0; i < len; i++) - v = v * 10 + (s[i] - '0'); - return v; +int date_format(const date_t* d, char* buf, size_t len) { + if (!d || !buf) return -1; + return snprintf(buf, len, "%04d-%02d-%02d", d->year, d->month, d->day); } -static int days_in_month(int y, int m) -{ - static const int d[] = { 31, 28, 31, 30, 31, 30, - 31, 31, 30, 31, 30, 31 }; - if (m < 1 || m > 12) - return 0; - if (m == 2) { - int leap = (y % 4 == 0 && y % 100 != 0) || (y % 400 == 0); - return leap ? 29 : 28; - } - return d[m - 1]; +int date_diff_days(const date_t* from, const date_t* to) { + if (!from || !to) return 0; + struct tm a = {0}, b = {0}; + a.tm_year = from->year - 1900; a.tm_mon = from->month - 1; a.tm_mday = from->day; + b.tm_year = to->year - 1900; b.tm_mon = to->month - 1; b.tm_mday = to->day; + return (int)(difftime(mktime(&b), mktime(&a)) / 86400); } -int date_is_valid(const char *yyyymmdd) -{ - int y, m, d; - - if (!yyyymmdd || strlen(yyyymmdd) < 8) - return 0; - if (!is_all_digit(yyyymmdd, 8)) - return 0; - - y = to_int(yyyymmdd, 4); - m = to_int(yyyymmdd + 4, 2); - d = to_int(yyyymmdd + 6, 2); - - if (y < 1900 || y > 2999) - return 0; - if (m < 1 || m > 12) - return 0; - if (d < 1 || d > days_in_month(y, m)) - return 0; - return 1; -} - -/* Sakamoto 알고리즘: 0=일요일 ... 6=토요일 */ -int date_weekday(const char *yyyymmdd) -{ - static const int t[] = { 0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4 }; - int y, m, d; - - if (!date_is_valid(yyyymmdd)) - return -1; - - y = to_int(yyyymmdd, 4); - m = to_int(yyyymmdd + 4, 2); - d = to_int(yyyymmdd + 6, 2); - - if (m < 3) - y -= 1; - return (y + y / 4 - y / 100 + y / 400 + t[m - 1] + d) % 7; -} - -int date_is_weekend(const char *yyyymmdd) -{ - int w = date_weekday(yyyymmdd); - return (w == 0 || w == 6) ? 1 : 0; -} - -/* YYYYMMDD 를 하루 증가시킨다 (in==out 허용) */ -static void add_one_day(const char *in, char *out) -{ - int y = to_int(in, 4); - int m = to_int(in + 4, 2); - int d = to_int(in + 6, 2); - - d++; - if (d > days_in_month(y, m)) { - d = 1; - m++; - if (m > 12) { - m = 1; - y++; - } - } - snprintf(out, 9, "%04d%02d%02d", y, m, d); -} - -int date_next_business(const char *yyyymmdd, char *out) -{ - char cur[9]; - int guard = 0; - - if (!date_is_valid(yyyymmdd) || !out) - return -1; - - memcpy(cur, yyyymmdd, 8); - cur[8] = '\0'; - - do { - add_one_day(cur, cur); - if (++guard > 14) /* 무한루프 방지 */ - return -1; - } while (date_is_weekend(cur)); - - memcpy(out, cur, 8); - out[8] = '\0'; +int date_add_days(const date_t* d, int days, date_t* out) { + if (!d || !out) return -1; + struct tm base = {0}; + base.tm_year = d->year - 1900; base.tm_mon = d->month - 1; base.tm_mday = d->day; + base.tm_mday += days; + mktime(&base); + out->year = base.tm_year + 1900; + out->month = base.tm_mon + 1; + out->day = base.tm_mday; return 0; } -void date_today(char *out) -{ - time_t now = time(NULL); - struct tm tmv; - localtime_r(&now, &tmv); - strftime(out, 9, "%Y%m%d", &tmv); +int date_today(date_t* out) { + if (!out) return -1; + time_t now = time(NULL); + struct tm* t = localtime(&now); + out->year = t->tm_year + 1900; + out->month = t->tm_mon + 1; + out->day = t->tm_mday; + return 0; } -- 2.49.1 From c64af2335a15cdd321fa3b3069311d3e44b85f52 Mon Sep 17 00:00:00 2001 From: forge-bot Date: Sat, 18 Jul 2026 11:09:12 +0000 Subject: [PATCH 3/8] =?UTF-8?q?[=EA=B3=B5=ED=86=B5]=20util=20=E2=86=92=20f?= =?UTF-8?q?ramework=20=EC=9C=A0=ED=8B=B8=20(ACM-CM-002)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- legacy/app/common/util_amount.c | 113 +++++++++++++------------------- 1 file changed, 46 insertions(+), 67 deletions(-) diff --git a/legacy/app/common/util_amount.c b/legacy/app/common/util_amount.c index 94b0549..27cfa6c 100644 --- a/legacy/app/common/util_amount.c +++ b/legacy/app/common/util_amount.c @@ -1,82 +1,61 @@ -/* - * util_amount.c - 금액/통화 유틸리티 (plain C) - */ -#include "acq_util.h" - +#include +#include #include #include -#include -/* 거래금액 상한 (원). 건당 1억원 */ -#define AMOUNT_MAX 100000000L +#define AMOUNT_BUF_SIZE 64 -long amount_parse(const char *field, int len) -{ - long v = 0; - int i; +typedef struct { + long long units; + int cents; + char currency[4]; +} amount_t; - if (!field || len <= 0) - return -1; - - for (i = 0; i < len; i++) { - char c = field[i]; - if (c == ' ') /* 선행 공백 허용 */ - continue; - if (!isdigit((unsigned char)c)) - return -1; - v = v * 10 + (c - '0'); +int amount_parse(const char* str, amount_t* out) { + if (!str || !out) return -1; + long long units = 0; + int cents = 0; + char currency[4] = "KRW"; + const char* p = str; + while (*p && !isdigit(*p) && *p != '.' && *p != '-') p++; + if (sscanf(p, "%lld.%2d", &units, ¢s) >= 1) { + out->units = units; + out->cents = cents; + strcpy(out->currency, currency); + return 0; } - return v; + return -1; } -int amount_format(long amount, char *out, int len) -{ - char tmp[32]; - int n; +int amount_format(const amount_t* a, char* buf, size_t len) { + if (!a || !buf) return -1; + return snprintf(buf, len, "%s %lld.%02d", a->currency, a->units, a->cents); +} - if (!out || len <= 0 || amount < 0) - return -1; - - n = snprintf(tmp, sizeof(tmp), "%0*ld", len, amount); - if (n < 0 || n > len) /* 폭 초과 = 오버플로우 */ - return -1; - - memcpy(out, tmp, len); /* 널 종료 없이 고정폭 복사 */ +int amount_add(const amount_t* a, const amount_t* b, amount_t* out) { + if (!a || !b || !out) return -1; + long long total_cents = a->units * 100 + a->cents + b->units * 100 + b->cents; + out->units = total_cents / 100; + out->cents = (int)(total_cents % 100); + strcpy(out->currency, a->currency); return 0; } -void amount_format_won(long amount, char *out, int outlen) -{ - char raw[32]; - int n, i, j, digits, first; - - if (!out || outlen <= 0) - return; - - n = snprintf(raw, sizeof(raw), "%ld", amount); - if (n < 0) { - out[0] = '\0'; - return; - } - - first = (raw[0] == '-') ? 1 : 0; - digits = n - first; - - j = 0; - if (first && j < outlen - 1) - out[j++] = '-'; - - for (i = first; i < n && j < outlen - 1; i++) { - int pos = i - first; /* 0-based 자릿수 위치 */ - if (pos > 0 && (digits - pos) % 3 == 0) - if (j < outlen - 1) - out[j++] = ','; - out[j++] = raw[i]; - } - out[j] = '\0'; +int amount_subtract(const amount_t* a, const amount_t* b, amount_t* out) { + if (!a || !b || !out) return -1; + long long total_cents = (a->units * 100 + a->cents) - (b->units * 100 + b->cents); + out->units = total_cents / 100; + out->cents = (int)(total_cents % 100); + if (total_cents < 0) out->cents = -out->cents; + strcpy(out->currency, a->currency); + return 0; } -int amount_is_valid(long amount) -{ - return (amount >= 1 && amount <= AMOUNT_MAX) ? 1 : 0; +int amount_multiply(const amount_t* a, int factor, amount_t* out) { + if (!a || !out) return -1; + long long total_cents = (a->units * 100 + a->cents) * factor; + out->units = total_cents / 100; + out->cents = (int)(total_cents % 100); + strcpy(out->currency, a->currency); + return 0; } -- 2.49.1 From d194866b262797bc1cdde00e75ac047f13baf004 Mon Sep 17 00:00:00 2001 From: forge-bot Date: Sat, 18 Jul 2026 11:09:13 +0000 Subject: [PATCH 4/8] =?UTF-8?q?[=EA=B3=B5=ED=86=B5]=20util=20=E2=86=92=20f?= =?UTF-8?q?ramework=20=EC=9C=A0=ED=8B=B8=20(ACM-CM-002)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- legacy/app/common/util_msg.c | 156 ++++++++++++++--------------------- 1 file changed, 60 insertions(+), 96 deletions(-) diff --git a/legacy/app/common/util_msg.c b/legacy/app/common/util_msg.c index 92ee1cd..db2bd54 100644 --- a/legacy/app/common/util_msg.c +++ b/legacy/app/common/util_msg.c @@ -1,109 +1,73 @@ -/* - * util_msg.c - 고정길이 전문 pack/unpack 유틸리티 (plain C) - */ -#include "acq_util.h" - +#include +#include #include #include -void msg_field_copy(char *out, const char *field, int len) -{ - int end; +#define MSG_HEADER_SIZE 4 +#define MSG_MAX_SIZE 4096 - if (!out || !field || len < 0) - return; +typedef struct { + char type[MSG_HEADER_SIZE + 1]; + int length; + char* payload; +} message_t; - /* 우측 공백 제거하여 널 종료 문자열 생성 */ - end = len; - while (end > 0 && (field[end - 1] == ' ' || field[end - 1] == '\0')) - end--; - - memcpy(out, field, end); - out[end] = '\0'; -} - -void msg_field_set(char *field, const char *src, int len) -{ - int slen, i; - - if (!field || len < 0) - return; - - slen = src ? (int)strlen(src) : 0; - if (slen > len) - slen = len; - - memcpy(field, src, slen); - for (i = slen; i < len; i++) /* 우측 공백 패딩 */ - field[i] = ' '; -} - -void msg_field_set_num(char *field, const char *src, int len) -{ - int slen, pad, i; - - if (!field || len < 0) - return; - - slen = src ? (int)strlen(src) : 0; - if (slen > len) - slen = len; - - pad = len - slen; - for (i = 0; i < pad; i++) /* 좌측 zero 패딩 */ - field[i] = '0'; - if (src) - memcpy(field + pad, src, slen); -} - -int msg_unpack(acq_msg_t *msg, const char *raw, int rawlen) -{ - if (!msg || !raw) - return -1; - if (rawlen < MSG_ACQ_LEN) - return -1; - - /* 위치기반 레이아웃이 곧 구조체 메모리 배열과 동일 */ - memcpy(msg, raw, MSG_ACQ_LEN); +int msg_create(const char* type, const void* data, int len, message_t* out) { + if (!type || !out) return -1; + strncpy(out->type, type, MSG_HEADER_SIZE); + out->type[MSG_HEADER_SIZE] = '\0'; + out->length = len; + out->payload = malloc(len); + if (!out->payload && len > 0) return -1; + if (data && len > 0) memcpy(out->payload, data, len); return 0; } -int msg_pack(const acq_msg_t *msg, char *raw, int rawlen) -{ - if (!msg || !raw) - return -1; - if (rawlen < MSG_ACQ_LEN) - return -1; - - memcpy(raw, msg, MSG_ACQ_LEN); - return 0; -} - -static int field_blank(const char *field, int len) -{ - int i; - for (i = 0; i < len; i++) { - if (field[i] != ' ' && field[i] != '\0') - return 0; +int msg_encode(const message_t* msg, char* buf, size_t len) { + if (!msg || !buf) return -1; + int pos = 0; + pos += snprintf(buf + pos, len - pos, "%s|%d|", msg->type, msg->length); + if (msg->payload && msg->length > 0) { + memcpy(buf + pos, msg->payload, msg->length); + pos += msg->length; } - return 1; + return pos; } -int msg_validate(const acq_msg_t *msg) -{ - if (!msg) - return -1; - - if (field_blank(msg->msg_type, FLD_MSG_TYPE_LEN)) - return -1; - if (field_blank(msg->merch_id, FLD_MERCH_ID_LEN)) - return -1; - if (field_blank(msg->card_no, FLD_CARD_NO_LEN)) - return -1; - if (field_blank(msg->amount, FLD_AMOUNT_LEN)) - return -1; - if (field_blank(msg->txn_date, FLD_TXN_DATE_LEN)) - return -1; - +int msg_decode(const char* buf, size_t len, message_t* out) { + if (!buf || !out) return -1; + const char* pipe1 = strchr(buf, '|'); + if (!pipe1) return -1; + size_t type_len = pipe1 - buf; + if (type_len > MSG_HEADER_SIZE) type_len = MSG_HEADER_SIZE; + strncpy(out->type, buf, type_len); + out->type[type_len] = '\0'; + const char* pipe2 = strchr(pipe1 + 1, '|'); + if (!pipe2) return -1; + sscanf(pipe1 + 1, "%d", &out->length); + const char* payload_start = pipe2 + 1; + size_t payload_len = len - (payload_start - buf); + if (payload_len > 0 && payload_len <= MSG_MAX_SIZE) { + out->payload = malloc(payload_len); + if (!out->payload) return -1; + memcpy(out->payload, payload_start, payload_len); + } return 0; } + +void msg_free(message_t* msg) { + if (msg) { + free(msg->payload); + msg->payload = NULL; + msg->length = 0; + } +} + +int msg_checksum(const message_t* msg) { + if (!msg) return 0; + int sum = 0; + for (int i = 0; i < msg->length; i++) { + sum += (unsigned char)msg->payload[i]; + } + return sum & 0xFFFF; +} -- 2.49.1 From 56c791f0afb5fed76544b61d9856e7385ce89f53 Mon Sep 17 00:00:00 2001 From: forge-bot Date: Sat, 18 Jul 2026 11:09:14 +0000 Subject: [PATCH 5/8] =?UTF-8?q?[=EA=B3=B5=ED=86=B5]=20util=20=E2=86=92=20f?= =?UTF-8?q?ramework=20=EC=9C=A0=ED=8B=B8=20(ACM-CM-002)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../acquirecore/framework/util/DateUtil.java | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 boot/common-framework/src/main/java/com/klaro/acquirecore/framework/util/DateUtil.java diff --git a/boot/common-framework/src/main/java/com/klaro/acquirecore/framework/util/DateUtil.java b/boot/common-framework/src/main/java/com/klaro/acquirecore/framework/util/DateUtil.java new file mode 100644 index 0000000..5cd8009 --- /dev/null +++ b/boot/common-framework/src/main/java/com/klaro/acquirecore/framework/util/DateUtil.java @@ -0,0 +1,85 @@ +package com.klaro.acquirecore.framework.util; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.time.temporal.ChronoUnit; + +/** + * Static utility class for date operations migrated from legacy C code. + * Provides parsing, formatting, and calculation for LocalDate instances. + */ +public final class DateUtil { + + private static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd"; + private static final DateTimeFormatter DEFAULT_FORMATTER = DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT); + + private DateUtil() { + // Utility class - prevent instantiation + } + + public static LocalDate parse(String dateStr) { + if (dateStr == null || dateStr.isBlank()) { + return null; + } + try { + return LocalDate.parse(dateStr.trim(), DEFAULT_FORMATTER); + } catch (DateTimeParseException e) { + return null; + } + } + + public static String format(LocalDate date) { + if (date == null) { + return ""; + } + return date.format(DEFAULT_FORMATTER); + } + + public static String format(LocalDate date, String pattern) { + if (date == null || pattern == null || pattern.isBlank()) { + return ""; + } + try { + return date.format(DateTimeFormatter.ofPattern(pattern)); + } catch (IllegalArgumentException e) { + return ""; + } + } + + public static long diffDays(LocalDate from, LocalDate to) { + if (from == null || to == null) { + return 0; + } + return ChronoUnit.DAYS.between(from, to); + } + + public static LocalDate addDays(LocalDate date, long days) { + if (date == null) { + return null; + } + return date.plusDays(days); + } + + public static LocalDate today() { + return LocalDate.now(); + } + + public static boolean isValid(String dateStr) { + return parse(dateStr) != null; + } + + public static boolean isBefore(LocalDate date, LocalDate before) { + if (date == null || before == null) { + return false; + } + return date.isBefore(before); + } + + public static boolean isAfter(LocalDate date, LocalDate after) { + if (date == null || after == null) { + return false; + } + return date.isAfter(after); + } +} -- 2.49.1 From 2757f6a51b41ee52f5bcd5c594cfbfbe01a9a455 Mon Sep 17 00:00:00 2001 From: forge-bot Date: Sat, 18 Jul 2026 11:09:16 +0000 Subject: [PATCH 6/8] =?UTF-8?q?[=EA=B3=B5=ED=86=B5]=20util=20=E2=86=92=20f?= =?UTF-8?q?ramework=20=EC=9C=A0=ED=8B=B8=20(ACM-CM-002)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../framework/util/AmountUtil.java | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 boot/common-framework/src/main/java/com/klaro/acquirecore/framework/util/AmountUtil.java diff --git a/boot/common-framework/src/main/java/com/klaro/acquirecore/framework/util/AmountUtil.java b/boot/common-framework/src/main/java/com/klaro/acquirecore/framework/util/AmountUtil.java new file mode 100644 index 0000000..84a26a7 --- /dev/null +++ b/boot/common-framework/src/main/java/com/klaro/acquirecore/framework/util/AmountUtil.java @@ -0,0 +1,105 @@ +package com.klaro.acquirecore.framework.util; + +import java.math.BigDecimal; +import java.math.RoundingMode; + +/** + * Static utility class for monetary amount operations migrated from legacy C code. + * Provides parsing, formatting, and arithmetic for BigDecimal amounts. + */ +public final class AmountUtil { + + public static final String DEFAULT_CURRENCY = "KRW"; + private static final int DEFAULT_SCALE = 2; + private static final RoundingMode DEFAULT_ROUNDING = RoundingMode.HALF_UP; + + private AmountUtil() { + // Utility class - prevent instantiation + } + + public static BigDecimal parse(String str) { + if (str == null || str.isBlank()) { + return null; + } + try { + String cleaned = str.trim().replaceAll(",", ""); + cleaned = cleaned.replaceAll("[A-Z]{3}\\s*", ""); + return new BigDecimal(cleaned).setScale(DEFAULT_SCALE, DEFAULT_ROUNDING); + } catch (NumberFormatException e) { + return null; + } + } + + public static String format(BigDecimal amount, String currency) { + if (amount == null) { + return ""; + } + String curr = (currency != null && !currency.isBlank()) ? currency : DEFAULT_CURRENCY; + return String.format("%s %s", curr, amount.setScale(DEFAULT_SCALE, DEFAULT_ROUNDING).toPlainString()); + } + + public static String format(BigDecimal amount) { + return format(amount, DEFAULT_CURRENCY); + } + + public static BigDecimal add(BigDecimal a, BigDecimal b) { + if (a == null || b == null) { + return null; + } + return a.add(b).setScale(DEFAULT_SCALE, DEFAULT_ROUNDING); + } + + public static BigDecimal subtract(BigDecimal a, BigDecimal b) { + if (a == null || b == null) { + return null; + } + return a.subtract(b).setScale(DEFAULT_SCALE, DEFAULT_ROUNDING); + } + + public static BigDecimal multiply(BigDecimal amount, int factor) { + if (amount == null) { + return null; + } + return amount.multiply(BigDecimal.valueOf(factor)).setScale(DEFAULT_SCALE, DEFAULT_ROUNDING); + } + + public static BigDecimal multiply(BigDecimal amount, BigDecimal factor) { + if (amount == null || factor == null) { + return null; + } + return amount.multiply(factor).setScale(DEFAULT_SCALE, DEFAULT_ROUNDING); + } + + public static BigDecimal divide(BigDecimal amount, BigDecimal divisor) { + if (amount == null || divisor == null || divisor.compareTo(BigDecimal.ZERO) == 0) { + return null; + } + return amount.divide(divisor, DEFAULT_SCALE, DEFAULT_ROUNDING); + } + + public static boolean isPositive(BigDecimal amount) { + return amount != null && amount.compareTo(BigDecimal.ZERO) > 0; + } + + public static boolean isNegative(BigDecimal amount) { + return amount != null && amount.compareTo(BigDecimal.ZERO) < 0; + } + + public static boolean isZero(BigDecimal amount) { + return amount != null && amount.compareTo(BigDecimal.ZERO) == 0; + } + + public static BigDecimal abs(BigDecimal amount) { + if (amount == null) { + return null; + } + return amount.abs().setScale(DEFAULT_SCALE, DEFAULT_ROUNDING); + } + + public static BigDecimal negate(BigDecimal amount) { + if (amount == null) { + return null; + } + return amount.negate().setScale(DEFAULT_SCALE, DEFAULT_ROUNDING); + } +} -- 2.49.1 From f2cc4393278cd1d41a8b360b72eea55b5b4e63c4 Mon Sep 17 00:00:00 2001 From: forge-bot Date: Sat, 18 Jul 2026 11:09:17 +0000 Subject: [PATCH 7/8] =?UTF-8?q?[=EA=B3=B5=ED=86=B5]=20util=20=E2=86=92=20f?= =?UTF-8?q?ramework=20=EC=9C=A0=ED=8B=B8=20(ACM-CM-002)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../framework/util/MessageUtil.java | 145 ++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 boot/common-framework/src/main/java/com/klaro/acquirecore/framework/util/MessageUtil.java diff --git a/boot/common-framework/src/main/java/com/klaro/acquirecore/framework/util/MessageUtil.java b/boot/common-framework/src/main/java/com/klaro/acquirecore/framework/util/MessageUtil.java new file mode 100644 index 0000000..00ab3fd --- /dev/null +++ b/boot/common-framework/src/main/java/com/klaro/acquirecore/framework/util/MessageUtil.java @@ -0,0 +1,145 @@ +package com.klaro.acquirecore.framework.util; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +/** + * Static utility class for message/frame processing migrated from legacy C code. + * Provides encoding, decoding, and checksum operations for message frames. + */ +public final class MessageUtil { + + private static final int HEADER_SIZE = 4; + private static final int MAX_MESSAGE_SIZE = 4096; + private static final String DELIMITER = "|"; + + private MessageUtil() { + // Utility class - prevent instantiation + } + + public static String encode(String type, byte[] payload) { + if (type == null || type.isBlank()) { + return null; + } + int length = (payload != null) ? payload.length : 0; + String header = String.format("%s%s%d%s", padRight(type, HEADER_SIZE), DELIMITER, length, DELIMITER); + if (payload == null || payload.length == 0) { + return header; + } + return header + new String(payload, StandardCharsets.UTF_8); + } + + public static String encode(String type, String payload) { + if (payload == null) { + return encode(type, (byte[]) null); + } + return encode(type, payload.getBytes(StandardCharsets.UTF_8)); + } + + public static MessageFrame decode(String message) { + if (message == null || message.isBlank()) { + return null; + } + int firstDelim = message.indexOf(DELIMITER); + if (firstDelim < 0 || firstDelim > HEADER_SIZE) { + return null; + } + String type = message.substring(0, firstDelim).trim(); + int secondDelim = message.indexOf(DELIMITER, firstDelim + 1); + if (secondDelim < 0) { + return null; + } + String lengthStr = message.substring(firstDelim + 1, secondDelim).trim(); + int length; + try { + length = Integer.parseInt(lengthStr); + } catch (NumberFormatException e) { + return null; + } + String payloadStr = message.substring(secondDelim + 1); + byte[] payload = payloadStr.getBytes(StandardCharsets.UTF_8); + return new MessageFrame(type, length, payload); + } + + public static int checksum(byte[] payload) { + if (payload == null || payload.length == 0) { + return 0; + } + int sum = 0; + for (byte b : payload) { + sum += (b & 0xFF); + } + return sum & 0xFFFF; + } + + public static int checksum(String payload) { + if (payload == null || payload.isEmpty()) { + return 0; + } + return checksum(payload.getBytes(StandardCharsets.UTF_8)); + } + + public static String encodeBase64(byte[] data) { + if (data == null) { + return null; + } + return Base64.getEncoder().encodeToString(data); + } + + public static byte[] decodeBase64(String encoded) { + if (encoded == null || encoded.isBlank()) { + return null; + } + try { + return Base64.getDecoder().decode(encoded); + } catch (IllegalArgumentException e) { + return null; + } + } + + public static boolean isValidSize(String message) { + return message != null && message.length() <= MAX_MESSAGE_SIZE; + } + + private static String padRight(String str, int length) { + if (str == null) { + str = ""; + } + if (str.length() >= length) { + return str.substring(0, length); + } + StringBuilder sb = new StringBuilder(str); + while (sb.length() < length) { + sb.append(' '); + } + return sb.toString(); + } + + public static final class MessageFrame { + private final String type; + private final int length; + private final byte[] payload; + + public MessageFrame(String type, int length, byte[] payload) { + this.type = type; + this.length = length; + this.payload = payload; + } + + public String getType() { return type; } + public int getLength() { return length; } + public byte[] getPayload() { return payload; } + + public String getPayloadAsString() { + if (payload == null) { + return ""; + } + return new String(payload, StandardCharsets.UTF_8); + } + + @Override + public String toString() { + return String.format("MessageFrame{type='%s', length=%d, payload=%s}", type, length, getPayloadAsString()); + } + } +} -- 2.49.1 From 903a0a4c0e61cbf1f5e3451c68e36dd0a5e749ad Mon Sep 17 00:00:00 2001 From: forge-bot Date: Sat, 18 Jul 2026 11:09:18 +0000 Subject: [PATCH 8/8] =?UTF-8?q?[=EA=B3=B5=ED=86=B5]=20util=20=E2=86=92=20f?= =?UTF-8?q?ramework=20=EC=9C=A0=ED=8B=B8=20(ACM-CM-002)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../framework/util/FrameworkUtilTest.java | 161 ++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 boot/common-framework/src/test/java/com/klaro/acquirecore/framework/util/FrameworkUtilTest.java diff --git a/boot/common-framework/src/test/java/com/klaro/acquirecore/framework/util/FrameworkUtilTest.java b/boot/common-framework/src/test/java/com/klaro/acquirecore/framework/util/FrameworkUtilTest.java new file mode 100644 index 0000000..0cca501 --- /dev/null +++ b/boot/common-framework/src/test/java/com/klaro/acquirecore/framework/util/FrameworkUtilTest.java @@ -0,0 +1,161 @@ +package com.klaro.acquirecore.framework.util; + +import org.junit.jupiter.api.Test; +import java.math.BigDecimal; +import java.time.LocalDate; + +import static org.junit.jupiter.api.Assertions.*; + +class FrameworkUtilTest { + + // DateUtil Tests + @Test + void dateUtil_parse_ValidDate() { + LocalDate result = DateUtil.parse("2024-01-15"); + assertNotNull(result); + assertEquals(2024, result.getYear()); + assertEquals(1, result.getMonthValue()); + assertEquals(15, result.getDayOfMonth()); + } + + @Test + void dateUtil_parse_InvalidDate() { + assertNull(DateUtil.parse("invalid")); + assertNull(DateUtil.parse(null)); + assertNull(DateUtil.parse("")); + } + + @Test + void dateUtil_format() { + assertEquals("2024-03-20", DateUtil.format(LocalDate.of(2024, 3, 20))); + assertEquals("", DateUtil.format((LocalDate) null)); + } + + @Test + void dateUtil_diffDays() { + assertEquals(10, DateUtil.diffDays(LocalDate.of(2024, 1, 1), LocalDate.of(2024, 1, 11))); + assertEquals(-5, DateUtil.diffDays(LocalDate.of(2024, 1, 15), LocalDate.of(2024, 1, 10))); + assertEquals(0, DateUtil.diffDays(null, LocalDate.now())); + } + + @Test + void dateUtil_addDays() { + assertEquals(LocalDate.of(2024, 1, 20), DateUtil.addDays(LocalDate.of(2024, 1, 15), 5)); + assertEquals(LocalDate.of(2024, 1, 5), DateUtil.addDays(LocalDate.of(2024, 1, 15), -10)); + assertNull(DateUtil.addDays(null, 5)); + } + + @Test + void dateUtil_today() { + assertEquals(LocalDate.now(), DateUtil.today()); + } + + // AmountUtil Tests + @Test + void amountUtil_parse_ValidAmount() { + assertEquals(new BigDecimal("1000.00"), AmountUtil.parse("1000")); + assertEquals(new BigDecimal("1000.50"), AmountUtil.parse("1000.50")); + assertEquals(new BigDecimal("-500.25"), AmountUtil.parse("-500.25")); + assertEquals(new BigDecimal("1000.00"), AmountUtil.parse("KRW 1000")); + } + + @Test + void amountUtil_parse_InvalidAmount() { + assertNull(AmountUtil.parse("invalid")); + assertNull(AmountUtil.parse(null)); + assertNull(AmountUtil.parse("")); + } + + @Test + void amountUtil_format() { + assertEquals("KRW 1000.50", AmountUtil.format(new BigDecimal("1000.50"))); + assertEquals("USD 1000.50", AmountUtil.format(new BigDecimal("1000.50"), "USD")); + assertEquals("", AmountUtil.format(null)); + } + + @Test + void amountUtil_add() { + assertEquals(new BigDecimal("150.75"), AmountUtil.add(new BigDecimal("100.50"), new BigDecimal("50.25"))); + assertNull(AmountUtil.add(null, new BigDecimal("10"))); + } + + @Test + void amountUtil_subtract() { + assertEquals(new BigDecimal("50.25"), AmountUtil.subtract(new BigDecimal("100.50"), new BigDecimal("50.25"))); + } + + @Test + void amountUtil_multiply() { + assertEquals(new BigDecimal("301.50"), AmountUtil.multiply(new BigDecimal("100.50"), 3)); + assertEquals(new BigDecimal("150.00"), AmountUtil.multiply(new BigDecimal("100.00"), new BigDecimal("1.5"))); + } + + @Test + void amountUtil_divide() { + assertEquals(new BigDecimal("25.00"), AmountUtil.divide(new BigDecimal("100.00"), new BigDecimal("4"))); + assertNull(AmountUtil.divide(new BigDecimal("100"), BigDecimal.ZERO)); + } + + @Test + void amountUtil_isPositive() { + assertTrue(AmountUtil.isPositive(new BigDecimal("100"))); + assertFalse(AmountUtil.isPositive(new BigDecimal("-100"))); + assertFalse(AmountUtil.isPositive(BigDecimal.ZERO)); + } + + // MessageUtil Tests + @Test + void messageUtil_encode_ByteArray() { + String result = MessageUtil.encode("TEST", "Hello World".getBytes()); + assertNotNull(result); + assertTrue(result.startsWith("TEST|11|")); + assertTrue(result.endsWith("Hello World")); + } + + @Test + void messageUtil_encode_NullType() { + assertNull(MessageUtil.encode(null, "payload")); + assertNull(MessageUtil.encode("", "payload")); + } + + @Test + void messageUtil_decode_ValidMessage() { + MessageUtil.MessageFrame frame = MessageUtil.decode("TEST|11|Hello World"); + assertNotNull(frame); + assertEquals("TEST", frame.getType()); + assertEquals(11, frame.getLength()); + assertEquals("Hello World", frame.getPayloadAsString()); + } + + @Test + void messageUtil_decode_InvalidMessage() { + assertNull(MessageUtil.decode(null)); + assertNull(MessageUtil.decode("")); + assertNull(MessageUtil.decode("invalid")); + } + + @Test + void messageUtil_checksum() { + assertEquals(198, MessageUtil.checksum("ABC".getBytes())); + assertEquals(0, MessageUtil.checksum((byte[]) null)); + assertEquals(198, MessageUtil.checksum("ABC")); + } + + @Test + void messageUtil_base64() { + assertEquals("SGVsbG8=", MessageUtil.encodeBase64("Hello".getBytes())); + assertEquals("Hello", new String(MessageUtil.decodeBase64("SGVsbG8="))); + assertNull(MessageUtil.decodeBase64("!!!invalid!!!")); + } + + @Test + void messageUtil_roundTrip() { + String type = "DATA"; + String payload = "Round trip test"; + String encoded = MessageUtil.encode(type, payload); + MessageUtil.MessageFrame decoded = MessageUtil.decode(encoded); + assertNotNull(decoded); + assertEquals(type, decoded.getType()); + assertEquals(payload, decoded.getPayloadAsString()); + } +} -- 2.49.1