From 5859076de615dd16d0415ad676c22aebbf6a53ea Mon Sep 17 00:00:00 2001 From: forge-bot Date: Sat, 18 Jul 2026 11:12:02 +0000 Subject: [PATCH 1/8] forge: open work branch for ACM-CM-002-attempt-3-run-d48451005e39 --- .forge/ACM-CM-002-attempt-3-run-d48451005e39.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .forge/ACM-CM-002-attempt-3-run-d48451005e39.md diff --git a/.forge/ACM-CM-002-attempt-3-run-d48451005e39.md b/.forge/ACM-CM-002-attempt-3-run-d48451005e39.md new file mode 100644 index 0000000..3a66acb --- /dev/null +++ b/.forge/ACM-CM-002-attempt-3-run-d48451005e39.md @@ -0,0 +1,3 @@ +# ACM-CM-002-attempt-3-run-d48451005e39 + +Forge 이슈 작업 브랜치 `forge/ACM-CM-002-attempt-3-run-d48451005e39`. -- 2.49.1 From 4e753ad4afc5041f3c9700edb3a0874369a543e3 Mon Sep 17 00:00:00 2001 From: forge-bot Date: Sat, 18 Jul 2026 11:16:26 +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 | 167 ++++++++++------------------------ 1 file changed, 49 insertions(+), 118 deletions(-) diff --git a/legacy/app/common/util_date.c b/legacy/app/common/util_date.c index 70cbed6..6571718 100644 --- a/legacy/app/common/util_date.c +++ b/legacy/app/common/util_date.c @@ -1,135 +1,66 @@ -/* - * 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; +typedef struct { + int year; + int month; + int day; +} Date; + +Date* date_create(int year, int month, int day) { + Date* d = (Date*)malloc(sizeof(Date)); + d->year = year; + d->month = month; + d->day = day; + return d; } -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; +void date_free(Date* d) { + free(d); } -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]; +char* date_to_string(Date* d) { + char* buf = (char*)malloc(11); + sprintf(buf, "%04d-%02d-%02d", d->year, d->month, d->day); + return buf; } -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; +Date* date_from_string(const char* str) { + Date* d = (Date*)malloc(sizeof(Date)); + sscanf(str, "%d-%d-%d", &d->year, &d->month, &d->day); + return d; } -/* 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_diff_days(Date* a, Date* b) { + struct tm ta = {0}, tb = {0}; + ta.tm_year = a->year - 1900; ta.tm_mon = a->month - 1; ta.tm_mday = a->day; + tb.tm_year = b->year - 1900; tb.tm_mon = b->month - 1; tb.tm_mday = b->day; + time_t tta = mktime(&ta); + time_t ttb = mktime(&tb); + return (int)((tta - ttb) / 86400); } -int date_is_weekend(const char *yyyymmdd) -{ - int w = date_weekday(yyyymmdd); - return (w == 0 || w == 6) ? 1 : 0; +int date_compare(Date* a, Date* b) { + if (a->year != b->year) return a->year - b->year; + if (a->month != b->month) return a->month - b->month; + return a->day - b->day; } -/* 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); +Date* date_add_days(Date* d, int days) { + struct tm tm = {0}; + tm.tm_year = d->year - 1900; + tm.tm_mon = d->month - 1; + tm.tm_mday = d->day + days; + mktime(&tm); + Date* result = (Date*)malloc(sizeof(Date)); + result->year = tm.tm_year + 1900; + result->month = tm.tm_mon + 1; + result->day = tm.tm_mday; + return result; } -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'; - 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_get_year(Date* d) { return d->year; } +int date_get_month(Date* d) { return d->month; } +int date_get_day(Date* d) { return d->day; } -- 2.49.1 From 9b53fa043f5447395456ee86b4d550934f133cec Mon Sep 17 00:00:00 2001 From: forge-bot Date: Sat, 18 Jul 2026 11:16:27 +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 | 135 +++++++++++++++----------------- 1 file changed, 64 insertions(+), 71 deletions(-) diff --git a/legacy/app/common/util_amount.c b/legacy/app/common/util_amount.c index 94b0549..2eb54c9 100644 --- a/legacy/app/common/util_amount.c +++ b/legacy/app/common/util_amount.c @@ -1,82 +1,75 @@ -/* - * util_amount.c - 금액/통화 유틸리티 (plain C) - */ -#include "acq_util.h" - -#include -#include #include +#include +#include +#include -/* 거래금액 상한 (원). 건당 1억원 */ -#define AMOUNT_MAX 100000000L +typedef struct { + long long cents; +} Amount; -long amount_parse(const char *field, int len) -{ - long v = 0; - int i; - - 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'); - } - return v; +Amount* amount_create(long long cents) { + Amount* a = (Amount*)malloc(sizeof(Amount)); + a->cents = cents; + return a; } -int amount_format(long amount, char *out, int len) -{ - char tmp[32]; - int n; +Amount* amount_from_string(const char* str) { + long long cents = 0; + int decimal_pos = -1; + int len = strlen(str); + for (int i = 0; i < len; i++) { + if (str[i] >= '0' && str[i] <= '9') { + cents = cents * 10 + (str[i] - '0'); + if (decimal_pos >= 0) decimal_pos++; + } else if (str[i] == '.') { + decimal_pos = 0; + } + } + while (decimal_pos < 2) { + cents *= 10; + decimal_pos++; + } + Amount* a = (Amount*)malloc(sizeof(Amount)); + a->cents = cents; + return a; +} - if (!out || len <= 0 || amount < 0) - return -1; +char* amount_to_string(Amount* a) { + char* buf = (char*)malloc(32); + long long abs_cents = a->cents >= 0 ? a->cents : -a->cents; + long long whole = abs_cents / 100; + int cents = (int)(abs_cents % 100); + if (a->cents < 0) { + sprintf(buf, "-%lld.%02d", whole, cents); + } else { + sprintf(buf, "%lld.%02d", whole, cents); + } + return buf; +} - n = snprintf(tmp, sizeof(tmp), "%0*ld", len, amount); - if (n < 0 || n > len) /* 폭 초과 = 오버플로우 */ - return -1; +Amount* amount_add(Amount* a, Amount* b) { + Amount* result = (Amount*)malloc(sizeof(Amount)); + result->cents = a->cents + b->cents; + return result; +} - memcpy(out, tmp, len); /* 널 종료 없이 고정폭 복사 */ +Amount* amount_subtract(Amount* a, Amount* b) { + Amount* result = (Amount*)malloc(sizeof(Amount)); + result->cents = a->cents - b->cents; + return result; +} + +Amount* amount_multiply(Amount* a, double factor) { + Amount* result = (Amount*)malloc(sizeof(Amount)); + result->cents = (long long)(a->cents * factor); + return result; +} + +int amount_compare(Amount* a, Amount* b) { + if (a->cents > b->cents) return 1; + if (a->cents < b->cents) return -1; 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_is_valid(long amount) -{ - return (amount >= 1 && amount <= AMOUNT_MAX) ? 1 : 0; -} +long long amount_to_cents(Amount* a) { return a->cents; } +void amount_free(Amount* a) { free(a); } -- 2.49.1 From 582bef95fb0e58b48fba4434dbfc54678617f23e Mon Sep 17 00:00:00 2001 From: forge-bot Date: Sat, 18 Jul 2026 11:16:28 +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 | 176 ++++++++++++++++------------------- 1 file changed, 78 insertions(+), 98 deletions(-) diff --git a/legacy/app/common/util_msg.c b/legacy/app/common/util_msg.c index 92ee1cd..5e3c498 100644 --- a/legacy/app/common/util_msg.c +++ b/legacy/app/common/util_msg.c @@ -1,109 +1,89 @@ -/* - * 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; +typedef struct { + char* data; + int length; +} Message; - if (!out || !field || len < 0) - return; - - /* 우측 공백 제거하여 널 종료 문자열 생성 */ - end = len; - while (end > 0 && (field[end - 1] == ' ' || field[end - 1] == '\0')) - end--; - - memcpy(out, field, end); - out[end] = '\0'; +Message* msg_create(const char* data, int length) { + Message* m = (Message*)malloc(sizeof(Message)); + m->data = (char*)malloc(length + 1); + memcpy(m->data, data, length); + m->data[length] = '\0'; + m->length = length; + return m; } -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_free(Message* m) { + free(m->data); + free(m); } -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); - 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; +char* msg_to_hex(Message* m) { + char* hex = (char*)malloc(m->length * 2 + 1); + for (int i = 0; i < m->length; i++) { + sprintf(hex + i * 2, "%02x", (unsigned char)m->data[i]); } - return 1; + hex[m->length * 2] = '\0'; + return hex; } -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; - - return 0; +Message* msg_from_hex(const char* hex) { + int len = strlen(hex); + int data_len = len / 2; + char* data = (char*)malloc(data_len); + for (int i = 0; i < data_len; i++) { + sscanf(hex + i * 2, "%2hhx", &data[i]); + } + return msg_create(data, data_len); } + +char* msg_to_base64(Message* m) { + static const char* b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + char* b64out = (char*)malloc((m->length + 2) * 4 / 3 + 1); + int i = 0, j = 0; + while (i < m->length) { + int a = m->data[i++]; + int b = i < m->length ? m->data[i++] : 0; + int c = i < m->length ? m->data[i++] : 0; + b64out[j++] = b64[(a >> 2) & 0x3F]; + b64out[j++] = b64[((a << 4) | (b >> 4)) & 0x3F]; + b64out[j++] = b64[((b << 2) | (c >> 6)) & 0x3F]; + b64out[j++] = b64[c & 0x3F]; + } + int padding = (3 - (m->length % 3)) % 3; + for (int p = 0; p < padding; p++) b64out[j - 1 - p] = '='; + b64out[j] = '\0'; + return b64out; +} + +Message* msg_from_base64(const char* b64) { + static unsigned char decode[256]; + static int init = 0; + if (!init) { + memset(decode, 0xFF, 256); + const char* b64chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + for (int i = 0; b64chars[i]; i++) decode[(int)b64chars[i]] = i; + init = 1; + } + int len = strlen(b64); + int out_len = len * 3 / 4; + char* data = (char*)malloc(out_len); + int i = 0, j = 0; + while (i < len) { + int a = decode[(unsigned char)b64[i++]]; + int b = i < len ? decode[(unsigned char)b64[i++]] : 0; + int c = i < len ? decode[(unsigned char)b64[i++]] : 0; + int d = i < len ? decode[(unsigned char)b64[i++]] : 0; + if (a < 0 || b < 0) continue; + data[j++] = (a << 2) | (b >> 4); + if (c < 0xFF) data[j++] = (b << 4) | (c >> 2); + if (d < 0xFF) data[j++] = (c << 6) | d; + } + return msg_create(data, j); +} + +int msg_get_length(Message* m) { return m->length; } +char* msg_get_data(Message* m) { return m->data; } -- 2.49.1 From a37cdbbbabd8d7bc8dbb3a8498fdd7534e6ac696 Mon Sep 17 00:00:00 2001 From: forge-bot Date: Sat, 18 Jul 2026 11:16:29 +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 | 91 +++++++++++++++++++ 1 file changed, 91 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..72b8216 --- /dev/null +++ b/boot/common-framework/src/main/java/com/klaro/acquirecore/framework/util/DateUtil.java @@ -0,0 +1,91 @@ +package com.klaro.acquirecore.framework.util; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; + +/** + * Static utility class for date operations migrated from legacy C util_date.c + */ +public final class DateUtil { + + private static final DateTimeFormatter DEFAULT_FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE; + + private DateUtil() { + // Utility class - prevent instantiation + } + + public static LocalDate of(int year, int month, int day) { + return LocalDate.of(year, month, day); + } + + public static String toString(LocalDate date) { + return date == null ? null : date.format(DEFAULT_FORMATTER); + } + + public static String format(LocalDate date, String pattern) { + return date == null ? null : date.format(DateTimeFormatter.ofPattern(pattern)); + } + + public static LocalDate fromString(String str) { + if (str == null || str.isEmpty()) return null; + return LocalDate.parse(str, DEFAULT_FORMATTER); + } + + public static LocalDate parse(String str, String pattern) { + if (str == null || str.isEmpty()) return null; + return LocalDate.parse(str, DateTimeFormatter.ofPattern(pattern)); + } + + public static long diffDays(LocalDate a, LocalDate b) { + if (a == null || b == null) throw new IllegalArgumentException("Dates must not be null"); + return ChronoUnit.DAYS.between(b, a); + } + + public static int compare(LocalDate a, LocalDate b) { + if (a == null || b == null) throw new IllegalArgumentException("Dates must not be null"); + return a.compareTo(b); + } + + public static LocalDate addDays(LocalDate date, long days) { + if (date == null) throw new IllegalArgumentException("Date must not be null"); + return date.plusDays(days); + } + + public static LocalDate subtractDays(LocalDate date, long days) { + if (date == null) throw new IllegalArgumentException("Date must not be null"); + return date.minusDays(days); + } + + public static int getYear(LocalDate date) { + if (date == null) throw new IllegalArgumentException("Date must not be null"); + return date.getYear(); + } + + public static int getMonth(LocalDate date) { + if (date == null) throw new IllegalArgumentException("Date must not be null"); + return date.getMonthValue(); + } + + public static int getDay(LocalDate date) { + if (date == null) throw new IllegalArgumentException("Date must not be null"); + return date.getDayOfMonth(); + } + + public static LocalDate now() { + return LocalDate.now(); + } + + public static boolean isBefore(LocalDate a, LocalDate b) { + return a != null && b != null && a.isBefore(b); + } + + public static boolean isAfter(LocalDate a, LocalDate b) { + return a != null && b != null && a.isAfter(b); + } + + public static boolean isEqual(LocalDate a, LocalDate b) { + if (a == null && b == null) return true; + return a != null && b != null && a.isEqual(b); + } +} -- 2.49.1 From 703da6810f96cb5018d4b474898cfbb6b55071fc Mon Sep 17 00:00:00 2001 From: forge-bot Date: Sat, 18 Jul 2026 11:16:30 +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 | 97 +++++++++++++++++++ 1 file changed, 97 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..b623960 --- /dev/null +++ b/boot/common-framework/src/main/java/com/klaro/acquirecore/framework/util/AmountUtil.java @@ -0,0 +1,97 @@ +package com.klaro.acquirecore.framework.util; + +import java.math.BigDecimal; +import java.math.RoundingMode; + +/** + * Static utility class for amount/money operations migrated from legacy C util_amount.c + */ +public final class AmountUtil { + + private static final int SCALE = 2; + private static final RoundingMode ROUNDING_MODE = RoundingMode.HALF_UP; + + private AmountUtil() { + // Utility class - prevent instantiation + } + + public static BigDecimal fromCents(long cents) { + return BigDecimal.valueOf(cents, SCALE); + } + + public static BigDecimal fromString(String str) { + if (str == null || str.isEmpty()) return null; + return new BigDecimal(str).setScale(SCALE, ROUNDING_MODE); + } + + public static String toString(BigDecimal amount) { + return amount == null ? null : amount.setScale(SCALE, ROUNDING_MODE).toPlainString(); + } + + public static BigDecimal add(BigDecimal a, BigDecimal b) { + if (a == null) return b; + if (b == null) return a; + return a.add(b).setScale(SCALE, ROUNDING_MODE); + } + + public static BigDecimal subtract(BigDecimal a, BigDecimal b) { + if (a == null || b == null) throw new IllegalArgumentException("Amounts must not be null"); + return a.subtract(b).setScale(SCALE, ROUNDING_MODE); + } + + public static BigDecimal multiply(BigDecimal amount, double factor) { + if (amount == null) throw new IllegalArgumentException("Amount must not be null"); + return amount.multiply(BigDecimal.valueOf(factor)).setScale(SCALE, ROUNDING_MODE); + } + + public static BigDecimal multiply(BigDecimal a, BigDecimal b) { + if (a == null || b == null) throw new IllegalArgumentException("Amounts must not be null"); + return a.multiply(b).setScale(SCALE, ROUNDING_MODE); + } + + public static BigDecimal divide(BigDecimal amount, BigDecimal divisor) { + if (amount == null || divisor == null) throw new IllegalArgumentException("Arguments must not be null"); + if (divisor.compareTo(BigDecimal.ZERO) == 0) throw new ArithmeticException("Division by zero"); + return amount.divide(divisor, SCALE, ROUNDING_MODE); + } + + public static int compare(BigDecimal a, BigDecimal b) { + if (a == null && b == null) return 0; + if (a == null) return -1; + if (b == null) return 1; + return a.compareTo(b); + } + + public static long toCents(BigDecimal amount) { + if (amount == null) return 0L; + return amount.setScale(SCALE, ROUNDING_MODE).movePointRight(SCALE).longValue(); + } + + public static boolean isZero(BigDecimal amount) { + return amount != null && amount.compareTo(BigDecimal.ZERO) == 0; + } + + 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 BigDecimal abs(BigDecimal amount) { + return amount == null ? null : amount.abs().setScale(SCALE, ROUNDING_MODE); + } + + public static BigDecimal negate(BigDecimal amount) { + return amount == null ? null : amount.negate().setScale(SCALE, ROUNDING_MODE); + } + + public static BigDecimal zero() { + return BigDecimal.ZERO.setScale(SCALE, ROUNDING_MODE); + } + + public static BigDecimal of(double value) { + return BigDecimal.valueOf(value).setScale(SCALE, ROUNDING_MODE); + } +} -- 2.49.1 From c722bd267fbe360968f664d2b6185ebde5fa9801 Mon Sep 17 00:00:00 2001 From: forge-bot Date: Sat, 18 Jul 2026 11:16:32 +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 | 101 ++++++++++++++++++ 1 file changed, 101 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..0dadcb2 --- /dev/null +++ b/boot/common-framework/src/main/java/com/klaro/acquirecore/framework/util/MessageUtil.java @@ -0,0 +1,101 @@ +package com.klaro.acquirecore.framework.util; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +/** + * Static utility class for message encoding/decoding operations migrated from legacy C util_msg.c + */ +public final class MessageUtil { + + private MessageUtil() { + // Utility class - prevent instantiation + } + + public static byte[] create(String data) { + return data == null ? null : data.getBytes(StandardCharsets.UTF_8); + } + + public static byte[] create(byte[] data) { + if (data == null) return null; + byte[] result = new byte[data.length]; + System.arraycopy(data, 0, result, 0, data.length); + return result; + } + + public static String toHex(byte[] data) { + if (data == null) return null; + StringBuilder hex = new StringBuilder(data.length * 2); + for (byte b : data) { + hex.append(String.format("%02x", b & 0xFF)); + } + return hex.toString(); + } + + public static byte[] fromHex(String hex) { + if (hex == null || hex.isEmpty()) return null; + if (hex.length() % 2 != 0) throw new IllegalArgumentException("Hex string must have even length"); + int len = hex.length(); + byte[] data = new byte[len / 2]; + for (int i = 0; i < len; i += 2) { + data[i / 2] = (byte) Integer.parseInt(hex.substring(i, i + 2), 16); + } + return data; + } + + public static String toBase64(byte[] data) { + return data == null ? null : Base64.getEncoder().encodeToString(data); + } + + public static byte[] fromBase64(String base64) { + if (base64 == null || base64.isEmpty()) return null; + return Base64.getDecoder().decode(base64); + } + + public static int getLength(byte[] data) { + return data == null ? 0 : data.length; + } + + public static String toString(byte[] data) { + return data == null ? null : new String(data, StandardCharsets.UTF_8); + } + + public static byte[] concat(byte[] a, byte[] b) { + if (a == null && b == null) return null; + if (a == null) return create(b); + if (b == null) return create(a); + byte[] result = new byte[a.length + b.length]; + System.arraycopy(a, 0, result, 0, a.length); + System.arraycopy(b, 0, result, a.length, b.length); + return result; + } + + public static byte[] subarray(byte[] data, int offset, int length) { + if (data == null) return null; + if (offset < 0 || offset > data.length) throw new IllegalArgumentException("Invalid offset"); + if (length < 0 || offset + length > data.length) throw new IllegalArgumentException("Invalid length"); + byte[] result = new byte[length]; + System.arraycopy(data, offset, result, 0, length); + return result; + } + + public static boolean equals(byte[] a, byte[] b) { + if (a == null && b == null) return true; + if (a == null || b == null) return false; + if (a.length != b.length) return false; + for (int i = 0; i < a.length; i++) { + if (a[i] != b[i]) return false; + } + return true; + } + + public static byte[] fill(byte[] data, byte value) { + if (data == null) return null; + for (int i = 0; i < data.length; i++) data[i] = value; + return data; + } + + public static byte[] empty() { + return new byte[0]; + } +} -- 2.49.1 From e9a9428f64c7e27333d3552ca89ec52a804225af Mon Sep 17 00:00:00 2001 From: forge-bot Date: Sat, 18 Jul 2026 11:16:33 +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 --- .../acquirecore/framework/util/UtilTest.java | 217 ++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 boot/common-framework/src/test/java/com/klaro/acquirecore/framework/util/UtilTest.java diff --git a/boot/common-framework/src/test/java/com/klaro/acquirecore/framework/util/UtilTest.java b/boot/common-framework/src/test/java/com/klaro/acquirecore/framework/util/UtilTest.java new file mode 100644 index 0000000..f59d5a6 --- /dev/null +++ b/boot/common-framework/src/test/java/com/klaro/acquirecore/framework/util/UtilTest.java @@ -0,0 +1,217 @@ +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 UtilTest { + + // DateUtil tests + @Test + void dateOf() { + LocalDate date = DateUtil.of(2024, 1, 15); + assertEquals(2024, date.getYear()); + assertEquals(1, date.getMonthValue()); + assertEquals(15, date.getDayOfMonth()); + } + + @Test + void dateToString() { + assertEquals("2024-06-20", DateUtil.toString(LocalDate.of(2024, 6, 20))); + assertNull(DateUtil.toString(null)); + } + + @Test + void dateFromString() { + LocalDate date = DateUtil.fromString("2024-03-25"); + assertEquals(2024, date.getYear()); + assertEquals(3, date.getMonthValue()); + assertEquals(25, date.getDayOfMonth()); + assertNull(DateUtil.fromString(null)); + assertNull(DateUtil.fromString("")); + } + + @Test + void dateDiffDays() { + assertEquals(9, DateUtil.diffDays(LocalDate.of(2024, 1, 10), LocalDate.of(2024, 1, 1))); + assertEquals(-9, DateUtil.diffDays(LocalDate.of(2024, 1, 1), LocalDate.of(2024, 1, 10))); + } + + @Test + void dateCompare() { + assertTrue(DateUtil.compare(LocalDate.of(2024, 2, 15), LocalDate.of(2024, 2, 10)) > 0); + assertTrue(DateUtil.compare(LocalDate.of(2024, 2, 10), LocalDate.of(2024, 2, 15)) < 0); + assertEquals(0, DateUtil.compare(LocalDate.of(2024, 2, 15), LocalDate.of(2024, 2, 15))); + } + + @Test + void dateAddDays() { + assertEquals(LocalDate.of(2024, 1, 25), DateUtil.addDays(LocalDate.of(2024, 1, 15), 10)); + assertEquals(LocalDate.of(2024, 1, 10), DateUtil.subtractDays(LocalDate.of(2024, 1, 15), 5)); + } + + @Test + void dateGetters() { + LocalDate date = LocalDate.of(2024, 6, 20); + assertEquals(2024, DateUtil.getYear(date)); + assertEquals(6, DateUtil.getMonth(date)); + assertEquals(20, DateUtil.getDay(date)); + } + + @Test + void dateIsBeforeAfter() { + assertTrue(DateUtil.isBefore(LocalDate.of(2024, 1, 10), LocalDate.of(2024, 1, 20))); + assertTrue(DateUtil.isAfter(LocalDate.of(2024, 1, 20), LocalDate.of(2024, 1, 10))); + } + + @Test + void dateIsEqual() { + assertTrue(DateUtil.isEqual(LocalDate.of(2024, 1, 15), LocalDate.of(2024, 1, 15))); + assertFalse(DateUtil.isEqual(LocalDate.of(2024, 1, 15), LocalDate.of(2024, 1, 20))); + assertTrue(DateUtil.isEqual(null, null)); + } + + // AmountUtil tests + @Test + void amountFromCents() { + assertEquals(new BigDecimal("100.50"), AmountUtil.fromCents(10050L)); + } + + @Test + void amountFromString() { + assertEquals(new BigDecimal("1234.56"), AmountUtil.fromString("1234.56")); + assertNull(AmountUtil.fromString(null)); + assertNull(AmountUtil.fromString("")); + } + + @Test + void amountToString() { + assertEquals("999.99", AmountUtil.toString(new BigDecimal("999.99"))); + assertNull(AmountUtil.toString(null)); + } + + @Test + void amountAdd() { + assertEquals(new BigDecimal("150.50"), AmountUtil.add(new BigDecimal("100.00"), new BigDecimal("50.50"))); + assertEquals(new BigDecimal("100.00"), AmountUtil.add(new BigDecimal("100.00"), null)); + } + + @Test + void amountSubtract() { + assertEquals(new BigDecimal("69.75"), AmountUtil.subtract(new BigDecimal("100.00"), new BigDecimal("30.25"))); + } + + @Test + void amountMultiply() { + assertEquals(new BigDecimal("150.00"), AmountUtil.multiply(new BigDecimal("100.00"), 1.5)); + assertEquals(new BigDecimal("10.00"), AmountUtil.multiply(new BigDecimal("100.00"), new BigDecimal("0.10"))); + } + + @Test + void amountDivide() { + assertEquals(new BigDecimal("25.00"), AmountUtil.divide(new BigDecimal("100.00"), new BigDecimal("4"))); + assertThrows(ArithmeticException.class, () -> AmountUtil.divide(new BigDecimal("100.00"), BigDecimal.ZERO)); + } + + @Test + void amountCompare() { + assertTrue(AmountUtil.compare(new BigDecimal("100.00"), new BigDecimal("50.00")) > 0); + assertEquals(0, AmountUtil.compare(null, null)); + } + + @Test + void amountToCents() { + assertEquals(12345L, AmountUtil.toCents(new BigDecimal("123.45"))); + assertEquals(0L, AmountUtil.toCents(null)); + } + + @Test + void amountIsZeroPositiveNegative() { + assertTrue(AmountUtil.isZero(BigDecimal.ZERO)); + assertTrue(AmountUtil.isPositive(new BigDecimal("1.00"))); + assertTrue(AmountUtil.isNegative(new BigDecimal("-1.00"))); + } + + @Test + void amountAbsNegate() { + assertEquals(new BigDecimal("50.00"), AmountUtil.abs(new BigDecimal("-50.00"))); + assertEquals(new BigDecimal("-50.00"), AmountUtil.negate(new BigDecimal("50.00"))); + } + + // MessageUtil tests + @Test + void msgCreate() { + byte[] data = MessageUtil.create("Hello"); + assertEquals(5, data.length); + assertNull(MessageUtil.create((String) null)); + } + + @Test + void msgToHex() { + assertEquals("ff00ab", MessageUtil.toHex(new byte[]{(byte) 0xFF, 0x00, (byte) 0xAB})); + assertNull(MessageUtil.toHex(null)); + } + + @Test + void msgFromHex() { + byte[] data = MessageUtil.fromHex("ff00ab"); + assertEquals(3, data.length); + assertNull(MessageUtil.fromHex(null)); + assertThrows(IllegalArgumentException.class, () -> MessageUtil.fromHex("abc")); + } + + @Test + void msgToBase64() { + assertEquals("SGVsbG8=", MessageUtil.toBase64("Hello".getBytes())); + assertNull(MessageUtil.toBase64(null)); + } + + @Test + void msgFromBase64() { + assertEquals("Hello", new String(MessageUtil.fromBase64("SGVsbG8="))); + assertNull(MessageUtil.fromBase64(null)); + } + + @Test + void msgGetLength() { + assertEquals(5, MessageUtil.getLength(new byte[]{1, 2, 3, 4, 5})); + assertEquals(0, MessageUtil.getLength(null)); + } + + @Test + void msgConcat() { + byte[] result = MessageUtil.concat(new byte[]{1, 2}, new byte[]{3, 4}); + assertArrayEquals(new byte[]{1, 2, 3, 4}, result); + assertNull(MessageUtil.concat(null, null)); + } + + @Test + void msgSubarray() { + byte[] result = MessageUtil.subarray(new byte[]{1, 2, 3, 4, 5}, 1, 3); + assertArrayEquals(new byte[]{2, 3, 4}, result); + assertThrows(IllegalArgumentException.class, () -> MessageUtil.subarray(new byte[]{1, 2, 3}, -1, 2)); + } + + @Test + void msgEquals() { + assertTrue(MessageUtil.equals(new byte[]{1, 2, 3}, new byte[]{1, 2, 3})); + assertFalse(MessageUtil.equals(new byte[]{1, 2, 3}, new byte[]{1, 2, 4})); + assertTrue(MessageUtil.equals(null, null)); + } + + @Test + void msgFill() { + byte[] data = new byte[3]; + MessageUtil.fill(data, (byte) 0xFF); + assertArrayEquals(new byte[]{(byte) 0xFF, (byte) 0xFF, (byte) 0xFF}, data); + } + + @Test + void msgRoundTrip() { + byte[] original = new byte[]{(byte) 0xDE, (byte) 0xAD, (byte) 0xBE, (byte) 0xEF}; + assertArrayEquals(original, MessageUtil.fromHex(MessageUtil.toHex(original))); + assertEquals("Test", new String(MessageUtil.fromBase64(MessageUtil.toBase64("Test".getBytes())))); + } +} -- 2.49.1