#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ generate.py - acquire-core-full 레거시 C 코퍼스 결정론적 생성기 ================================================================ 카드 매입·정산 도메인의 TxCore(사내 TP 프레임워크) 기반 레거시 C 시스템을 아키타입별 템플릿 × 파라미터(module, program-id, difficulty tier)로 생성한다. - 각 프로그램은 self-contained: framework/txcore(공유 라이브러리)만 의존하며 독립적으로 .o 로 컴파일된다. - 산출: framework/, app/{online,batch,dbio,common,include,shared}, fml/, scripts/, mk/, config/, db/, Makefile, inventory.json 사용법: python3 generate.py [--fraction F] # F=1.0 full(~2050 files), 0.05 quick test """ import argparse import json import os import shutil ROOT = os.path.dirname(os.path.abspath(__file__)) # 11개 업무 모듈: code -> (한글명, 영문 transform 태그) MODULES = [ ("mg", "전문게이트웨이", "msg-gateway"), ("au", "승인한도", "authorization"), ("ac", "매입", "acquire"), ("rc", "대사", "reconciliation"), ("vl", "정합성", "validation"), ("st", "정산수수료", "settlement"), ("py", "지급", "payment"), ("lg", "원장", "ledger"), ("mm", "마스터", "master"), ("cm", "공통", "common"), ("cl", "마감", "closing"), ] MOD_CODES = [m[0] for m in MODULES] MOD_KNAME = {m[0]: m[1] for m in MODULES} MOD_XFORM = {m[0]: m[2] for m in MODULES} # 파일 타입별 목표 건수 (fraction=1.0 기준) TARGETS = { "online": 350, "batch": 180, "common": 120, "dbio": 300, # 이 중 11개는 모듈 표준 dbio_main "dbio_sql": 50, "headers": 600, # core(11)+io헤더+cu헤더+standalone "fml": 120, "sh": 180, "mk": 90, "config": 60, } # 데모 링크 대상 (생성 산출물로 링크 검증) ONLINE_DEMOS = ["mg", "ac", "rc", "st"] BATCH_DEMOS = ["rc", "cl"] def tier_for(i): """~55% easy, ~30% medium, ~15% hard (결정론적).""" r = i % 20 if r < 11: return "easy" if r < 17: return "medium" return "hard" def render(tpl, **kw): out = tpl for k, v in kw.items(): out = out.replace("@" + k + "@", str(v)) return out # ====================================================================== # 공유 프레임워크 소스 (검증된 슬라이스에서 그대로 이식) - raw 문자열 # ====================================================================== TXCORE_H = r'''/* * txcore.h - TxCore 공통 프레임워크 API * * TxCore 는 사내 표준 공통 프레임워크로, Tuxedo/ProFrame 계열 TP 모니터의 * ATMI(tpcall/tpreturn/tpbegin) 및 FML/UBF(Fchg/Fget) 관용구를 얇게 감싼 * 자체 구현 계층이다. 외부 TP 미들웨어 설치 없이 단독 빌드/링크된다. */ #ifndef TXCORE_H #define TXCORE_H #include #include #ifdef __cplusplus extern "C" { #endif #define TX_OK 0 #define TX_FAIL (-1) #define TX_ENOENT (-2) #define TX_EINVAL (-3) #define TX_EDB (-4) #define TX_ENOMEM (-5) #define TX_ENOKEY (-6) #define TX_LOG_DEBUG 0 #define TX_LOG_INFO 1 #define TX_LOG_WARN 2 #define TX_LOG_ERROR 3 #define TX_MAX_SLOTS 64 #define TX_KEY_LEN 32 #define TX_VAL_LEN 256 typedef struct { char key[TX_KEY_LEN]; char val[TX_VAL_LEN]; int len; int used; } TXFIELD; typedef struct { int count; TXFIELD slots[TX_MAX_SLOTS]; } TXBUF; typedef struct { char name[TX_KEY_LEN]; TXBUF *in; TXBUF *out; int rcode; long xid; } TXSVCINFO; typedef void (*tx_service_fn)(TXSVCINFO *ctx); #define TX_SERVICE(name, ctx) void name(TXSVCINFO *ctx) TXBUF *tx_buf_alloc(void); void tx_buf_free(TXBUF *buf); void tx_buf_reset(TXBUF *buf); int tx_buf_set(TXBUF *buf, const char *key, const char *val, int len); int tx_buf_sets(TXBUF *buf, const char *key, const char *val); int tx_buf_setlong(TXBUF *buf, const char *key, long val); int tx_buf_get(TXBUF *buf, const char *key, char *out, int outlen); int tx_buf_getlong(TXBUF *buf, const char *key, long *out); int tx_register(const char *name, tx_service_fn fn); int tx_call(const char *svc, TXBUF *in, TXBUF *out); int tx_return(TXSVCINFO *ctx, int rc, TXBUF *out); int tx_service_count(void); int tx_begin(void); int tx_commit(void); int tx_abort(void); long tx_current_xid(void); void tx_log(int level, const char *fmt, ...); void tx_set_loglevel(int level); const char *tx_strerror(int rc); #ifdef __cplusplus } #endif #endif /* TXCORE_H */ ''' TXCORE_DBIO_H = r'''/* * txcore_dbio.h - TxCore DBIO 규약 (ECPG/Pro*C 공통) */ #ifndef TXCORE_DBIO_H #define TXCORE_DBIO_H #include "txcore.h" #define TX_SQL_OK 0 #define TX_SQL_NOTFOUND 100 #define TX_DBIO_RESULT(sc) \ ((sc) == TX_SQL_OK ? TX_OK : \ (sc) == TX_SQL_NOTFOUND ? TX_ENOENT : TX_EDB) #define TX_DBIO_LOG_ERR(tag) \ tx_log(TX_LOG_ERROR, "DBIO 오류 [%s] sqlcode=%ld msg=%.*s", \ (tag), (long)sqlca.sqlcode, \ (int)sqlca.sqlerrm.sqlerrml, sqlca.sqlerrm.sqlerrmc) #endif /* TXCORE_DBIO_H */ ''' TXCORE_C = r'''/* * txcore.c - TxCore 공통 프레임워크 구현 */ #include "txcore.h" #include #include #include #include #define TX_MAX_SERVICES 512 typedef struct { char name[TX_KEY_LEN]; tx_service_fn fn; } tx_entry_t; static tx_entry_t g_registry[TX_MAX_SERVICES]; static int g_registry_count = 0; static int g_loglevel = TX_LOG_INFO; static long g_xid_seq = 0; static long g_cur_xid = 0; static const char *level_name(int level) { switch (level) { case TX_LOG_DEBUG: return "DEBUG"; case TX_LOG_INFO: return "INFO "; case TX_LOG_WARN: return "WARN "; case TX_LOG_ERROR: return "ERROR"; default: return "?????"; } } void tx_set_loglevel(int level) { g_loglevel = level; } void tx_log(int level, const char *fmt, ...) { va_list ap; time_t now; struct tm tmv; char ts[20]; if (level < g_loglevel) return; now = time(NULL); localtime_r(&now, &tmv); strftime(ts, sizeof(ts), "%Y-%m-%d %H:%M:%S", &tmv); fprintf(stderr, "[%s] %s ", ts, level_name(level)); va_start(ap, fmt); vfprintf(stderr, fmt, ap); va_end(ap); fputc('\n', stderr); } const char *tx_strerror(int rc) { switch (rc) { case TX_OK: return "정상"; case TX_FAIL: return "일반 실패"; case TX_ENOENT: return "서비스 미등록"; case TX_EINVAL: return "파라미터 오류"; case TX_EDB: return "DB 오류"; case TX_ENOMEM: return "자원 부족"; case TX_ENOKEY: return "버퍼 키 없음"; default: return "알 수 없는 오류"; } } TXBUF *tx_buf_alloc(void) { return (TXBUF *)calloc(1, sizeof(TXBUF)); } void tx_buf_free(TXBUF *buf) { if (buf) free(buf); } void tx_buf_reset(TXBUF *buf) { if (buf) memset(buf, 0, sizeof(*buf)); } static TXFIELD *find_slot(TXBUF *buf, const char *key) { int i; for (i = 0; i < TX_MAX_SLOTS; i++) { if (buf->slots[i].used && strncmp(buf->slots[i].key, key, TX_KEY_LEN) == 0) return &buf->slots[i]; } return NULL; } static TXFIELD *alloc_slot(TXBUF *buf, const char *key) { int i; TXFIELD *f = find_slot(buf, key); if (f) return f; for (i = 0; i < TX_MAX_SLOTS; i++) { if (!buf->slots[i].used) { f = &buf->slots[i]; memset(f, 0, sizeof(*f)); strncpy(f->key, key, TX_KEY_LEN - 1); f->used = 1; buf->count++; return f; } } return NULL; } int tx_buf_set(TXBUF *buf, const char *key, const char *val, int len) { TXFIELD *f; if (!buf || !key || !val) return TX_EINVAL; if (len < 0 || len >= TX_VAL_LEN) return TX_ENOMEM; f = alloc_slot(buf, key); if (!f) return TX_ENOMEM; memcpy(f->val, val, len); f->val[len] = '\0'; f->len = len; return TX_OK; } int tx_buf_sets(TXBUF *buf, const char *key, const char *val) { if (!val) return TX_EINVAL; return tx_buf_set(buf, key, val, (int)strlen(val)); } int tx_buf_setlong(TXBUF *buf, const char *key, long val) { char tmp[32]; snprintf(tmp, sizeof(tmp), "%ld", val); return tx_buf_set(buf, key, tmp, (int)strlen(tmp)); } int tx_buf_get(TXBUF *buf, const char *key, char *out, int outlen) { TXFIELD *f; if (!buf || !key || !out || outlen <= 0) return TX_EINVAL; f = find_slot(buf, key); if (!f) return TX_ENOKEY; if (f->len >= outlen) return TX_ENOMEM; memcpy(out, f->val, f->len); out[f->len] = '\0'; return TX_OK; } int tx_buf_getlong(TXBUF *buf, const char *key, long *out) { TXFIELD *f; char *end; long v; if (!buf || !key || !out) return TX_EINVAL; f = find_slot(buf, key); if (!f) return TX_ENOKEY; v = strtol(f->val, &end, 10); if (end == f->val) return TX_EINVAL; *out = v; return TX_OK; } int tx_register(const char *name, tx_service_fn fn) { int i; if (!name || !fn) return TX_EINVAL; if (g_registry_count >= TX_MAX_SERVICES) return TX_ENOMEM; for (i = 0; i < g_registry_count; i++) { if (strncmp(g_registry[i].name, name, TX_KEY_LEN) == 0) { g_registry[i].fn = fn; return TX_OK; } } strncpy(g_registry[g_registry_count].name, name, TX_KEY_LEN - 1); g_registry[g_registry_count].fn = fn; g_registry_count++; tx_log(TX_LOG_DEBUG, "서비스 등록: %s", name); return TX_OK; } int tx_service_count(void) { return g_registry_count; } static tx_service_fn lookup(const char *name) { int i; for (i = 0; i < g_registry_count; i++) { if (strncmp(g_registry[i].name, name, TX_KEY_LEN) == 0) return g_registry[i].fn; } return NULL; } int tx_call(const char *svc, TXBUF *in, TXBUF *out) { tx_service_fn fn; TXSVCINFO ctx; if (!svc || !in || !out) return TX_EINVAL; fn = lookup(svc); if (!fn) { tx_log(TX_LOG_ERROR, "tx_call: 서비스 미등록 svc=%s", svc); return TX_ENOENT; } memset(&ctx, 0, sizeof(ctx)); strncpy(ctx.name, svc, TX_KEY_LEN - 1); ctx.in = in; ctx.out = out; ctx.rcode = TX_OK; ctx.xid = g_cur_xid; fn(&ctx); return ctx.rcode; } int tx_return(TXSVCINFO *ctx, int rc, TXBUF *out) { if (!ctx) return TX_EINVAL; ctx->rcode = rc; if (out && ctx->out && out != ctx->out) memcpy(ctx->out, out, sizeof(TXBUF)); return rc; } int tx_begin(void) { if (g_cur_xid != 0) { tx_log(TX_LOG_WARN, "tx_begin: 이미 진행중 xid=%ld", g_cur_xid); return TX_FAIL; } g_cur_xid = ++g_xid_seq; tx_log(TX_LOG_INFO, "tx_begin xid=%ld", g_cur_xid); return TX_OK; } int tx_commit(void) { if (g_cur_xid == 0) { tx_log(TX_LOG_WARN, "tx_commit: 트랜잭션 없음"); return TX_FAIL; } tx_log(TX_LOG_INFO, "tx_commit xid=%ld", g_cur_xid); g_cur_xid = 0; return TX_OK; } int tx_abort(void) { if (g_cur_xid == 0) { tx_log(TX_LOG_WARN, "tx_abort: 트랜잭션 없음"); return TX_FAIL; } tx_log(TX_LOG_WARN, "tx_abort xid=%ld", g_cur_xid); g_cur_xid = 0; return TX_OK; } long tx_current_xid(void) { return g_cur_xid; } ''' MSG_LAYOUT_H = r'''/* * msg_layout.h - 카드 매입 전문(電文) 고정길이 레이아웃 (공유) */ #ifndef MSG_LAYOUT_H #define MSG_LAYOUT_H #define MSG_TYPE_RECV "0200" #define MSG_TYPE_RESP "0210" #define MSG_TYPE_RECON "0500" #define RESP_OK "0000" #define RESP_INVALID "9001" #define RESP_NOMERCH "9002" #define RESP_SYSERR "9999" typedef struct { char msg_type[4]; char trans_code[6]; char merch_id[15]; char card_no[16]; char approval_no[9]; char amount[12]; char txn_date[8]; char txn_time[6]; char inst_month[2]; char resp_code[4]; char filler[18]; } acq_msg_t; #define MSG_ACQ_LEN ((int)sizeof(acq_msg_t)) #define FLD_MSG_TYPE_LEN 4 #define FLD_TRANS_CODE_LEN 6 #define FLD_MERCH_ID_LEN 15 #define FLD_CARD_NO_LEN 16 #define FLD_APPROVAL_LEN 9 #define FLD_AMOUNT_LEN 12 #define FLD_TXN_DATE_LEN 8 #define FLD_TXN_TIME_LEN 6 #define FLD_INST_LEN 2 #define FLD_RESP_LEN 4 #endif /* MSG_LAYOUT_H */ ''' ACQ_UTIL_H = r'''/* * acq_util.h - 공통 유틸리티 (일자/금액/전문) 선언 (공유) */ #ifndef ACQ_UTIL_H #define ACQ_UTIL_H #include "msg_layout.h" int date_is_valid(const char *yyyymmdd); int date_weekday(const char *yyyymmdd); int date_is_weekend(const char *yyyymmdd); int date_next_business(const char *yyyymmdd, char *out); void date_today(char *out); long amount_parse(const char *field, int len); int amount_format(long amount, char *out, int len); void amount_format_won(long amount, char *out, int outlen); int amount_is_valid(long amount); void msg_field_copy(char *out, const char *field, int len); void msg_field_set(char *field, const char *src, int len); void msg_field_set_num(char *field, const char *src, int len); int msg_unpack(acq_msg_t *msg, const char *raw, int rawlen); int msg_pack(const acq_msg_t *msg, char *raw, int rawlen); int msg_validate(const acq_msg_t *msg); #endif /* ACQ_UTIL_H */ ''' UTIL_DATE_C = r'''/* * util_date.c - 일자/영업일 유틸리티 (plain C, 공유) */ #include "acq_util.h" #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; } 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; } 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_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; } 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; } 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'; 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); } ''' UTIL_AMOUNT_C = r'''/* * util_amount.c - 금액/통화 유틸리티 (plain C, 공유) */ #include "acq_util.h" #include #include #include #define AMOUNT_MAX 100000000L 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; } int amount_format(long amount, char *out, int len) { char tmp[32]; int n; 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); 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; 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; } ''' UTIL_MSG_C = r'''/* * util_msg.c - 고정길이 전문 pack/unpack 유틸리티 (plain C, 공유) */ #include "acq_util.h" #include #include void msg_field_copy(char *out, const char *field, int len) { int end; 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'; } 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++) 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; return 1; } 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; } ''' # ====================================================================== # 모듈 core 헤더 (표준 DBIO API + 레코드 구조 + 상태코드) # ====================================================================== CORE_H_TPL = r'''/* * @mod@_core.h - @KMOD@ 모듈 핵심 계약 (레코드/상태/표준 DBIO API) * * 이 모듈의 온라인/배치 프로그램은 SQL 을 직접 다루지 않고 아래 표준 * DBIO API 만 호출한다. 구현은 app/dbio/@mod@_dbio_main.pgc (ECPG). */ #ifndef @MOD@_CORE_H #define @MOD@_CORE_H #include "txcore.h" /* @KMOD@ 처리 상태코드 */ #define @MOD@_ST_RECV "R" /* 접수/수신 */ #define @MOD@_ST_DONE "M" /* 처리완료 */ #define @MOD@_ST_FAIL "U" /* 불일치/실패 */ #define @MOD@_ST_SETTLED "S" /* 정산완료 */ /* @KMOD@ 원장 레코드 (1행) */ typedef struct { char key[16]; /* 처리키(승인/거래 번호) PK */ char merch_id[16]; /* 가맹점번호 */ char card_no[20]; /* 카드번호(마스킹) */ long amount; /* 거래금액(원) */ char biz_date[9]; /* 영업일 YYYYMMDD */ char status[2]; /* 상태코드 */ long fee; /* 수수료(원) */ } @mod@_rec_t; /* 표준 DBIO API (구현: @mod@_dbio_main.pgc) */ int @mod@_dbio_connect(const char *target); int @mod@_dbio_disconnect(void); int @mod@_rec_insert(const @mod@_rec_t *rec); int @mod@_rec_select(const char *key, @mod@_rec_t *out); int @mod@_rec_update_status(const char *key, const char *status); long @mod@_rec_count(const char *biz_date, const char *status); int @mod@_rec_sum(const char *biz_date, long *out_sum); #endif /* @MOD@_CORE_H */ ''' DBIO_MAIN_TPL = r'''/* @tier medium @module @mod@ @transform @XFORM@-dbio */ /* * @mod@_dbio_main.pgc - @KMOD@ 표준 DBIO (ECPG 임베디드 SQL) * * @mod@_core.h 의 표준 API 를 @mod@_ledger 테이블에 대해 구현한다. * 온라인/배치 프로그램은 이 함수들만 호출한다 (self-contained). */ #include #include #include "txcore.h" #include "txcore_dbio.h" #include "@mod@_core.h" EXEC SQL INCLUDE sqlca; int @mod@_dbio_connect(const char *target) { EXEC SQL BEGIN DECLARE SECTION; const char *h_target; EXEC SQL END DECLARE SECTION; h_target = (target && target[0]) ? target : "acquire@localhost"; EXEC SQL CONNECT TO :h_target; if (sqlca.sqlcode != TX_SQL_OK) { TX_DBIO_LOG_ERR("@mod@ CONNECT"); return TX_EDB; } tx_log(TX_LOG_INFO, "[@mod@] DB 접속 완료 target=%s", h_target); return TX_OK; } int @mod@_dbio_disconnect(void) { EXEC SQL DISCONNECT CURRENT; return TX_OK; } int @mod@_rec_insert(const @mod@_rec_t *rec) { EXEC SQL BEGIN DECLARE SECTION; char h_key[16]; char h_merch_id[16]; char h_card_no[20]; long h_amount; char h_biz_date[9]; char h_status[2]; long h_fee; EXEC SQL END DECLARE SECTION; if (!rec) return TX_EINVAL; strncpy(h_key, rec->key, sizeof(h_key) - 1); h_key[sizeof(h_key) - 1] = '\0'; strncpy(h_merch_id, rec->merch_id, sizeof(h_merch_id) - 1); h_merch_id[sizeof(h_merch_id) - 1] = '\0'; strncpy(h_card_no, rec->card_no, sizeof(h_card_no) - 1); h_card_no[sizeof(h_card_no) - 1] = '\0'; strncpy(h_biz_date, rec->biz_date, sizeof(h_biz_date) - 1); h_biz_date[sizeof(h_biz_date) - 1] = '\0'; strncpy(h_status, rec->status, sizeof(h_status) - 1); h_status[sizeof(h_status) - 1] = '\0'; h_amount = rec->amount; h_fee = rec->fee; EXEC SQL INSERT INTO @mod@_ledger (key, merch_id, card_no, amount, biz_date, status, fee) VALUES (:h_key, :h_merch_id, :h_card_no, :h_amount, :h_biz_date, :h_status, :h_fee); if (sqlca.sqlcode != TX_SQL_OK) { TX_DBIO_LOG_ERR("@mod@_rec_insert"); return TX_EDB; } return TX_OK; } int @mod@_rec_select(const char *key, @mod@_rec_t *out) { EXEC SQL BEGIN DECLARE SECTION; char h_key[16]; char h_merch_id[16]; char h_card_no[20]; long h_amount; char h_biz_date[9]; char h_status[2]; long h_fee; EXEC SQL END DECLARE SECTION; if (!key || !out) return TX_EINVAL; strncpy(h_key, key, sizeof(h_key) - 1); h_key[sizeof(h_key) - 1] = '\0'; EXEC SQL SELECT merch_id, card_no, amount, biz_date, status, fee INTO :h_merch_id, :h_card_no, :h_amount, :h_biz_date, :h_status, :h_fee FROM @mod@_ledger WHERE key = :h_key; if (sqlca.sqlcode == TX_SQL_NOTFOUND) return TX_ENOENT; if (sqlca.sqlcode != TX_SQL_OK) { TX_DBIO_LOG_ERR("@mod@_rec_select"); return TX_EDB; } memset(out, 0, sizeof(*out)); strncpy(out->key, h_key, sizeof(out->key) - 1); strncpy(out->merch_id, h_merch_id, sizeof(out->merch_id) - 1); strncpy(out->card_no, h_card_no, sizeof(out->card_no) - 1); strncpy(out->biz_date, h_biz_date, sizeof(out->biz_date) - 1); strncpy(out->status, h_status, sizeof(out->status) - 1); out->amount = h_amount; out->fee = h_fee; return TX_OK; } int @mod@_rec_update_status(const char *key, const char *status) { EXEC SQL BEGIN DECLARE SECTION; char h_key[16]; char h_status[2]; EXEC SQL END DECLARE SECTION; if (!key || !status) return TX_EINVAL; strncpy(h_key, key, sizeof(h_key) - 1); h_key[sizeof(h_key) - 1] = '\0'; strncpy(h_status, status, sizeof(h_status) - 1); h_status[sizeof(h_status) - 1] = '\0'; EXEC SQL UPDATE @mod@_ledger SET status = :h_status, upd_ts = now() WHERE key = :h_key; if (sqlca.sqlcode != TX_SQL_OK) { TX_DBIO_LOG_ERR("@mod@_rec_update_status"); return TX_EDB; } if (sqlca.sqlerrd[2] == 0) return TX_ENOENT; return TX_OK; } long @mod@_rec_count(const char *biz_date, const char *status) { EXEC SQL BEGIN DECLARE SECTION; char h_biz_date[9]; char h_status[2]; int h_cnt; EXEC SQL END DECLARE SECTION; if (!biz_date || !status) return -1; strncpy(h_biz_date, biz_date, sizeof(h_biz_date) - 1); h_biz_date[sizeof(h_biz_date) - 1] = '\0'; strncpy(h_status, status, sizeof(h_status) - 1); h_status[sizeof(h_status) - 1] = '\0'; EXEC SQL SELECT count(*) INTO :h_cnt FROM @mod@_ledger WHERE biz_date = :h_biz_date AND status = :h_status; if (sqlca.sqlcode != TX_SQL_OK) { TX_DBIO_LOG_ERR("@mod@_rec_count"); return -1; } return (long)h_cnt; } int @mod@_rec_sum(const char *biz_date, long *out_sum) { EXEC SQL BEGIN DECLARE SECTION; char h_biz_date[9]; long h_sum; EXEC SQL END DECLARE SECTION; if (!biz_date || !out_sum) return TX_EINVAL; strncpy(h_biz_date, biz_date, sizeof(h_biz_date) - 1); h_biz_date[sizeof(h_biz_date) - 1] = '\0'; EXEC SQL SELECT coalesce(sum(amount), 0) INTO :h_sum FROM @mod@_ledger WHERE biz_date = :h_biz_date; if (sqlca.sqlcode != TX_SQL_OK) { TX_DBIO_LOG_ERR("@mod@_rec_sum"); return TX_EDB; } *out_sum = h_sum; return TX_OK; } ''' # ====================================================================== # 온라인 서비스 (.pgc) - easy / medium / hard # ====================================================================== ONLINE_HEAD = r'''/* @tier @TIER@ @module @mod@ @transform @XFORM@-online */ /* * @ID@.pgc - @KMOD@ 온라인 서비스 (@SVC@) [tier=@TIER@] * * TxCore TX_SERVICE 엔트리. 요청 TXBUF 를 검증하고 @mod@ 표준 DBIO 를 * 통해 처리한 뒤 응답 TXBUF 를 구성한다. (ATMI void SVC(TPSVCINFO*) 대응) */ #include #include #include "txcore.h" #include "acq_util.h" #include "@mod@_core.h" EXEC SQL INCLUDE sqlca; ''' ONLINE_EASY = r'''TX_SERVICE(@SVC@, ctx) { @mod@_rec_t rec; char key[16]; int rc; tx_log(TX_LOG_INFO, "[@SVC@] @KMOD@ 단건조회 서비스 진입"); if (tx_buf_get(ctx->in, "KEY", key, sizeof(key)) != TX_OK) { tx_log(TX_LOG_ERROR, "[@SVC@] KEY 누락"); tx_return(ctx, TX_EINVAL, ctx->out); return; } memset(&rec, 0, sizeof(rec)); rc = @mod@_rec_select(key, &rec); if (rc != TX_OK) { tx_log(TX_LOG_WARN, "[@SVC@] 조회 실패 key=%s rc=%d(%s)", key, rc, tx_strerror(rc)); tx_return(ctx, rc, ctx->out); return; } tx_buf_reset(ctx->out); tx_buf_sets(ctx->out, "KEY", rec.key); tx_buf_sets(ctx->out, "MERCHID", rec.merch_id); tx_buf_setlong(ctx->out, "AMOUNT", rec.amount); tx_buf_sets(ctx->out, "STATUS", rec.status); tx_log(TX_LOG_INFO, "[@SVC@] 조회완료 key=%s amount=%ld", key, rec.amount); tx_return(ctx, TX_OK, ctx->out); } ''' ONLINE_MEDIUM = r'''TX_SERVICE(@SVC@, ctx) { @mod@_rec_t rec; char key[16], merch[16], bizdate[16]; long amount = 0, cnt; int rc; tx_log(TX_LOG_INFO, "[@SVC@] @KMOD@ 등록 서비스 진입"); if (tx_buf_get(ctx->in, "KEY", key, sizeof(key)) != TX_OK || tx_buf_get(ctx->in, "MERCHID", merch, sizeof(merch)) != TX_OK || tx_buf_get(ctx->in, "BIZDATE", bizdate, sizeof(bizdate)) != TX_OK) { tx_log(TX_LOG_ERROR, "[@SVC@] 필수 필드 누락"); tx_return(ctx, TX_EINVAL, ctx->out); return; } tx_buf_getlong(ctx->in, "AMOUNT", &amount); /* 업무검증: 금액 범위 + 영업일 형식 */ if (!amount_is_valid(amount)) { tx_log(TX_LOG_WARN, "[@SVC@] 금액 범위 오류 amount=%ld", amount); tx_return(ctx, TX_EINVAL, ctx->out); return; } if (!date_is_valid(bizdate)) { tx_log(TX_LOG_WARN, "[@SVC@] 영업일 오류 date=%s", bizdate); tx_return(ctx, TX_EINVAL, ctx->out); return; } memset(&rec, 0, sizeof(rec)); strncpy(rec.key, key, sizeof(rec.key) - 1); strncpy(rec.merch_id, merch, sizeof(rec.merch_id) - 1); strncpy(rec.biz_date, bizdate, sizeof(rec.biz_date) - 1); strncpy(rec.status, @MOD@_ST_RECV, sizeof(rec.status) - 1); rec.amount = amount; rec.fee = amount / 100; /* 수수료 1% 가정 */ rc = @mod@_rec_insert(&rec); if (rc != TX_OK) { tx_log(TX_LOG_ERROR, "[@SVC@] 등록 실패 rc=%d(%s)", rc, tx_strerror(rc)); tx_return(ctx, rc, ctx->out); return; } cnt = @mod@_rec_count(bizdate, @MOD@_ST_RECV); tx_buf_reset(ctx->out); tx_buf_sets(ctx->out, "RESPCODE", RESP_OK); tx_buf_sets(ctx->out, "KEY", key); tx_buf_setlong(ctx->out, "DAYCNT", (cnt < 0) ? 0 : cnt); tx_log(TX_LOG_INFO, "[@SVC@] 등록완료 key=%s 당일건수=%ld", key, cnt); tx_return(ctx, TX_OK, ctx->out); } ''' ONLINE_HARD = r'''TX_SERVICE(@SVC@, ctx) { @mod@_rec_t rec; char key[16], merch[16], bizdate[16], nextbiz[9]; long amount = 0, daysum = 0; int rc; TXBUF *sub_in; TXBUF *sub_out; tx_log(TX_LOG_INFO, "[@SVC@] @KMOD@ 트랜잭션 오케스트레이션 진입"); if (tx_buf_get(ctx->in, "KEY", key, sizeof(key)) != TX_OK || tx_buf_get(ctx->in, "MERCHID", merch, sizeof(merch)) != TX_OK || tx_buf_get(ctx->in, "BIZDATE", bizdate, sizeof(bizdate)) != TX_OK) { tx_log(TX_LOG_ERROR, "[@SVC@] 필수 필드 누락"); tx_return(ctx, TX_EINVAL, ctx->out); return; } tx_buf_getlong(ctx->in, "AMOUNT", &amount); if (!amount_is_valid(amount) || !date_is_valid(bizdate)) { tx_log(TX_LOG_WARN, "[@SVC@] 입력 검증 실패 amount=%ld date=%s", amount, bizdate); tx_return(ctx, TX_EINVAL, ctx->out); return; } /* 트랜잭션 시작 → 원장 반영 → 후속 서비스 연계 → 커밋 */ if (tx_begin() != TX_OK) { tx_return(ctx, TX_FAIL, ctx->out); return; } memset(&rec, 0, sizeof(rec)); strncpy(rec.key, key, sizeof(rec.key) - 1); strncpy(rec.merch_id, merch, sizeof(rec.merch_id) - 1); strncpy(rec.biz_date, bizdate, sizeof(rec.biz_date) - 1); strncpy(rec.status, @MOD@_ST_RECV, sizeof(rec.status) - 1); rec.amount = amount; rec.fee = amount / 100; rc = @mod@_rec_insert(&rec); if (rc != TX_OK) { tx_log(TX_LOG_ERROR, "[@SVC@] 원장반영 실패 rc=%d", rc); tx_abort(); tx_return(ctx, rc, ctx->out); return; } /* 정산 영업일 = 거래일의 다음 영업일 */ if (date_next_business(bizdate, nextbiz) != 0) { memcpy(nextbiz, bizdate, 8); nextbiz[8] = '\0'; } /* 후속 정산집계 서비스 연계 (레지스트리 문자열 디스패치) */ sub_in = tx_buf_alloc(); sub_out = tx_buf_alloc(); if (sub_in && sub_out) { tx_buf_sets(sub_in, "MERCHID", merch); tx_buf_sets(sub_in, "BIZDATE", nextbiz); tx_buf_setlong(sub_in, "AMOUNT", amount); rc = tx_call("@NEXTSVC@", sub_in, sub_out); if (rc != TX_OK) tx_log(TX_LOG_WARN, "[@SVC@] 후속연계 경고 rc=%d", rc); } tx_buf_free(sub_in); tx_buf_free(sub_out); /* 교차 검증: 당일 합계 재계산 */ if (@mod@_rec_sum(bizdate, &daysum) != TX_OK) daysum = 0; tx_commit(); tx_buf_reset(ctx->out); tx_buf_sets(ctx->out, "RESPCODE", RESP_OK); tx_buf_sets(ctx->out, "KEY", key); tx_buf_setlong(ctx->out, "DAYSUM", daysum); tx_log(TX_LOG_INFO, "[@SVC@] 완료 key=%s 당일합계=%ld", key, daysum); tx_return(ctx, TX_OK, ctx->out); } ''' # ====================================================================== # 배치 (.pgc) - 커서 DECLARE/OPEN/FETCH 루프 + main() # ====================================================================== BATCH_TPL = r'''/* @tier @TIER@ @module @mod@ @transform @XFORM@-batch */ /* * @ID@.pgc - @KMOD@ 배치 (커서 순회 + 상태전이) [tier=@TIER@] * * 지정 영업일의 접수(status='R') 건을 커서로 순회하며 업무규칙에 따라 * 완료('M')/불일치('U') 로 상태를 전이한다. 배치 main 엔트리 포함. * * 실행: @ID@ [db@host] */ #include #include #include #include "txcore.h" #include "txcore_dbio.h" #include "acq_util.h" #include "@mod@_core.h" EXEC SQL INCLUDE sqlca; static int run_@ID@(const char *biz_date) { EXEC SQL BEGIN DECLARE SECTION; char h_biz_date[9]; char h_key[16]; char h_merch_id[16]; long h_amount; char h_new_status[2]; EXEC SQL END DECLARE SECTION; long total = 0, done = 0, unmatch = 0, errcnt = 0; int rc; strncpy(h_biz_date, biz_date, sizeof(h_biz_date) - 1); h_biz_date[sizeof(h_biz_date) - 1] = '\0'; EXEC SQL DECLARE cur_@ID@ CURSOR FOR SELECT key, merch_id, amount FROM @mod@_ledger WHERE biz_date = :h_biz_date AND status = 'R' ORDER BY key; EXEC SQL OPEN cur_@ID@; if (sqlca.sqlcode != TX_SQL_OK) { TX_DBIO_LOG_ERR("OPEN cur_@ID@"); return TX_EDB; } if (tx_begin() != TX_OK) tx_log(TX_LOG_WARN, "[@ID@] tx_begin 경고"); for (;;) { EXEC SQL FETCH cur_@ID@ INTO :h_key, :h_merch_id, :h_amount; if (sqlca.sqlcode == TX_SQL_NOTFOUND) break; if (sqlca.sqlcode != TX_SQL_OK) { TX_DBIO_LOG_ERR("FETCH cur_@ID@"); errcnt++; break; } total++; /* 업무규칙: 금액 유효성으로 완료/불일치 판정 */ if (amount_is_valid(h_amount)) { strncpy(h_new_status, @MOD@_ST_DONE, sizeof(h_new_status)); done++; } else { strncpy(h_new_status, @MOD@_ST_FAIL, sizeof(h_new_status)); unmatch++; } rc = @mod@_rec_update_status(h_key, h_new_status); if (rc != TX_OK) { tx_log(TX_LOG_ERROR, "[@ID@] 상태전이 실패 key=%s rc=%d", h_key, rc); errcnt++; } } EXEC SQL CLOSE cur_@ID@; if (errcnt == 0) tx_commit(); else tx_abort(); tx_log(TX_LOG_INFO, "[@ID@] 요약 date=%s 대상=%ld 완료=%ld 불일치=%ld 오류=%ld", biz_date, total, done, unmatch, errcnt); printf("========== @KMOD@ 배치 결과 [@ID@] ==========\n"); printf(" 영업일 : %s\n", biz_date); printf(" 대상 건수 : %ld\n", total); printf(" 완료(M) : %ld\n", done); printf(" 불일치(U) : %ld\n", unmatch); printf(" 오류 건수 : %ld\n", errcnt); printf("=============================================\n"); return (errcnt == 0) ? TX_OK : TX_FAIL; } int main(int argc, char **argv) { const char *biz_date; const char *target; int rc; tx_set_loglevel(TX_LOG_INFO); if (argc < 2) { fprintf(stderr, "사용법: %s [db@host]\n", argv[0]); return 2; } biz_date = argv[1]; target = (argc >= 3) ? argv[2] : NULL; if (!date_is_valid(biz_date)) { fprintf(stderr, "오류: 유효하지 않은 영업일 '%s'\n", biz_date); return 2; } tx_log(TX_LOG_INFO, "[@ID@] 배치 시작 date=%s", biz_date); rc = @mod@_dbio_connect(target); if (rc != TX_OK) { tx_log(TX_LOG_ERROR, "[@ID@] DB 접속 실패 rc=%d", rc); return 1; } rc = run_@ID@(biz_date); @mod@_dbio_disconnect(); tx_log(TX_LOG_INFO, "[@ID@] 배치 종료 rc=%d(%s)", rc, tx_strerror(rc)); return (rc == TX_OK) ? 0 : 1; } ''' # ====================================================================== # 추가 DBIO (.pgc) - 자체 헤더에 선언된 함수 구현 (self-contained) # ====================================================================== DBIO_EXTRA_H = r'''/* * @ID@.h - @KMOD@ 보조 DBIO 선언 (@ID@.pgc 구현) */ #ifndef @IDU@_H #define @IDU@_H #include "txcore.h" /* @ID@ 전용 조회/갱신 (테이블 @ID@_t) */ int @ID@_fetch(const char *key, long *out_amount); int @ID@_touch(const char *key, long amount); long @ID@_total(const char *biz_date); #endif /* @IDU@_H */ ''' DBIO_EXTRA_TPL = r'''/* @tier @TIER@ @module @mod@ @transform @XFORM@-dbio */ /* * @ID@.pgc - @KMOD@ 보조 DBIO (ECPG) [tier=@TIER@] */ #include #include #include "txcore.h" #include "txcore_dbio.h" #include "@ID@.h" EXEC SQL INCLUDE sqlca; int @ID@_fetch(const char *key, long *out_amount) { EXEC SQL BEGIN DECLARE SECTION; char h_key[16]; long h_amount; EXEC SQL END DECLARE SECTION; if (!key || !out_amount) return TX_EINVAL; strncpy(h_key, key, sizeof(h_key) - 1); h_key[sizeof(h_key) - 1] = '\0'; EXEC SQL SELECT amount INTO :h_amount FROM @ID@_t WHERE key = :h_key; if (sqlca.sqlcode == TX_SQL_NOTFOUND) return TX_ENOENT; if (sqlca.sqlcode != TX_SQL_OK) { TX_DBIO_LOG_ERR("@ID@_fetch"); return TX_EDB; } *out_amount = h_amount; return TX_OK; } int @ID@_touch(const char *key, long amount) { EXEC SQL BEGIN DECLARE SECTION; char h_key[16]; long h_amount; EXEC SQL END DECLARE SECTION; if (!key) return TX_EINVAL; strncpy(h_key, key, sizeof(h_key) - 1); h_key[sizeof(h_key) - 1] = '\0'; h_amount = amount; EXEC SQL UPDATE @ID@_t SET amount = :h_amount, upd_ts = now() WHERE key = :h_key; if (sqlca.sqlcode != TX_SQL_OK) { TX_DBIO_LOG_ERR("@ID@_touch"); return TX_EDB; } if (sqlca.sqlerrd[2] == 0) return TX_ENOENT; return TX_OK; } long @ID@_total(const char *biz_date) { EXEC SQL BEGIN DECLARE SECTION; char h_biz_date[9]; long h_sum; EXEC SQL END DECLARE SECTION; if (!biz_date) return -1; strncpy(h_biz_date, biz_date, sizeof(h_biz_date) - 1); h_biz_date[sizeof(h_biz_date) - 1] = '\0'; EXEC SQL SELECT coalesce(sum(amount), 0) INTO :h_sum FROM @ID@_t WHERE biz_date = :h_biz_date; if (sqlca.sqlcode != TX_SQL_OK) { TX_DBIO_LOG_ERR("@ID@_total"); return -1; } return h_sum; } ''' # ====================================================================== # 공통 유틸 (.c) - plain C # ====================================================================== COMMON_H = r'''/* * @ID@.h - @KMOD@ 공통 유틸 선언 (@ID@.c 구현) */ #ifndef @IDU@_H #define @IDU@_H long @ID@_fee(long amount, int rate_bp); long @ID@_round_unit(long amount, long unit); int @ID@_classify(long amount); #endif /* @IDU@_H */ ''' COMMON_TPL = r'''/* @tier @TIER@ @module @mod@ @transform @XFORM@-util */ /* * @ID@.c - @KMOD@ 공통 유틸리티 (plain C) [tier=@TIER@] */ #include "@ID@.h" #include /* 수수료 계산: rate_bp 는 basis point(1bp=0.01%) */ long @ID@_fee(long amount, int rate_bp) { long fee; if (amount <= 0 || rate_bp < 0) return 0; fee = (amount * (long)rate_bp) / 10000L; if (fee < 0) fee = 0; return fee; } /* 절사(내림) 단위 정규화 */ long @ID@_round_unit(long amount, long unit) { if (unit <= 0) return amount; return (amount / unit) * unit; } /* 금액 구간 분류: 0=소액 1=일반 2=고액 */ int @ID@_classify(long amount) { if (amount < 10000L) return 0; if (amount < 1000000L) return 1; return 2; } ''' # ====================================================================== # 독립 헤더 (전문 struct / 상수 / 레코드) - 4종 로테이션 # ====================================================================== HDR_REC = r'''/* * @ID@.h - @KMOD@ 레코드/뷰 구조 정의 */ #ifndef @IDU@_H #define @IDU@_H /* @KMOD@ 처리 뷰 레코드 */ typedef struct { char key[16]; char merch_id[16]; long amount; long fee; char biz_date[9]; char status[2]; char reserved[16]; } @ID@_view_t; #define @IDU@_STATUS_OPEN "O" #define @IDU@_STATUS_HOLD "H" #define @IDU@_STATUS_CLOSE "C" #endif /* @IDU@_H */ ''' HDR_MSG = r'''/* * @ID@.h - @KMOD@ 고정길이 전문 레이아웃 (positional, 재배치 금지) */ #ifndef @IDU@_H #define @IDU@_H typedef struct { char msg_type[4]; /* 전문구분 4 */ char merch_id[15]; /* 가맹점번호 15 */ char key[9]; /* 처리키 9 */ char amount[12]; /* 금액 12 zero-pad */ char biz_date[8]; /* 영업일 8 */ char resp_code[4]; /* 응답코드 4 */ char filler[20]; /* 예비 20 */ } @ID@_msg_t; #define @IDU@_MSG_LEN ((int)sizeof(@ID@_msg_t)) #endif /* @IDU@_H */ ''' HDR_CONST = r'''/* * @ID@.h - @KMOD@ 코드/상수 테이블 */ #ifndef @IDU@_H #define @IDU@_H #define @IDU@_RC_OK 0 #define @IDU@_RC_RETRY 1 #define @IDU@_RC_SKIP 2 #define @IDU@_RC_ERROR 9 #define @IDU@_LIMIT_DAILY 100000000L #define @IDU@_LIMIT_SINGLE 50000000L #define @IDU@_FEE_RATE_BP 250 /* 2.50% */ #define @IDU@_CHAN_ONLINE "01" #define @IDU@_CHAN_BATCH "02" #define @IDU@_CHAN_RECON "03" #endif /* @IDU@_H */ ''' HDR_DECL = r'''/* * @ID@.h - @KMOD@ 내부 헬퍼 선언 */ #ifndef @IDU@_H #define @IDU@_H #include "txcore.h" int @ID@_check(const char *key, long amount); int @ID@_apply(TXBUF *in, TXBUF *out); long @ID@_eval(long amount, int factor); #endif /* @IDU@_H */ ''' HDR_ROTATION = [HDR_REC, HDR_MSG, HDR_CONST, HDR_DECL] # ====================================================================== # 데모 서버 main (온라인 데모 링크용, 생성 산출물 검증) # ====================================================================== DEMO_MAIN_TPL = r'''/* * demo_@mod@_main.c - @KMOD@ 온라인 데모 부트스트랩 (링크 검증) * * 생성된 플래그십 서비스(@SVC@)를 TxCore 레지스트리에 등록하여 * 등록/디스패치/DBIO 링크 경로가 실제로 연결됨을 증명한다. */ #include #include "txcore.h" void @SVC@(TXSVCINFO *ctx); int main(void) { tx_set_loglevel(TX_LOG_INFO); tx_log(TX_LOG_INFO, "@KMOD@ 온라인 데모 기동 (TxCore)"); tx_register("@SVC@", @SVC@); tx_log(TX_LOG_INFO, "등록 서비스 수 = %d", tx_service_count()); printf("demo_@mod@_server 준비완료: 서비스 %d개 등록\n", tx_service_count()); return 0; } ''' # ====================================================================== # 비컴파일 아티팩트 템플릿 # ====================================================================== FML_TPL = '''# # @ID@.fml - @KMOD@ FML 필드 테이블 (UBF 필드 정의) # *base 1@BASE@0 # # name rel-number type flags comments @ID@_KEY 1 string - 처리키 @ID@_MERCHID 2 string - 가맹점번호 @ID@_CARDNO 3 string - 카드번호(마스킹) @ID@_AMOUNT 4 long - 거래금액 @ID@_BIZDATE 5 string - 영업일 @ID@_STATUS 6 char - 상태코드 @ID@_FEE 7 long - 수수료 @ID@_RESPCODE 8 string - 응답코드 ''' SH_TPL = '''#!/bin/sh # @ID@.sh - @KMOD@ 실행 러너 (@TIER@) # 사용법: @ID@.sh set -eu BIZDATE="${1:-$(date +%Y%m%d)}" APP_HOME="${APP_HOME:-/app/acquire-core}" BIN="${APP_HOME}/bin" LOG="${APP_HOME}/log/@ID@_${BIZDATE}.log" echo "[@ID@] @KMOD@ 처리 시작 date=${BIZDATE}" if [ -x "${BIN}/@RUNBIN@" ]; then "${BIN}/@RUNBIN@" "${BIZDATE}" "acquire@localhost" >> "${LOG}" 2>&1 RC=$? else echo "[@ID@] 실행 파일 없음: ${BIN}/@RUNBIN@" >&2 RC=0 fi echo "[@ID@] 종료 rc=${RC}" exit ${RC} ''' MK_TPL = '''# @ID@.mk - @KMOD@ 빌드 조각 (참고용 include 단편) # 이 조각은 상위 Makefile 이 아카이브/배포 시 참조하는 메타데이터다. @IDU@_MODULE := @mod@ @IDU@_TIER := @TIER@ @IDU@_SRC := app/@SUBDIR@/@REF@ @IDU@_DESC := @KMOD@ @XFORM@ 처리 단위 .PHONY: @ID@-info @ID@-info: @echo "@ID@ module=@mod@ tier=@TIER@ transform=@XFORM@" ''' UBB_TPL = '''# # @ID@.ubb - @KMOD@ TP 도메인 구성 (Tuxedo UBBCONFIG 형식, 참고) # *RESOURCES IPCKEY @IPCKEY@ MASTER SITE1 MODEL SHM LDBAL Y *MACHINES "acqhost" LMID=SITE1 APPDIR="/app/acquire-core" TUXCONFIG="/app/acquire-core/tuxconfig" *GROUPS GRP_@MOD@ LMID=SITE1 GRPNO=@GRPNO@ *SERVERS @MOD@_SVR SRVGRP=GRP_@MOD@ SRVID=@GRPNO@ CLOPT="-A" *SERVICES @MOD@_SVC LOAD=50 PRIO=50 ''' CFG_TPL = '''# @ID@.cfg - @KMOD@ 런타임 설정 (@TIER@) [general] module = @mod@ transform = @XFORM@ tier = @TIER@ [db] dsn = acquire@localhost schema = public fetch_size = 1000 [limits] daily_cap = 100000000 single_cap = 50000000 fee_rate_bp = 250 ''' DAT_TPL = '''# @ID@.dat - @KMOD@ 코드 매핑 데이터 (파이프 구분) # code|label|value 01|@KMOD@ 접수|R 02|@KMOD@ 완료|M 03|@KMOD@ 불일치|U 04|@KMOD@ 정산|S ''' SQL_TPL = '''-- @ID@.sql - @KMOD@ DBIO 스키마/뷰 정의 (@TIER@) -- 런타임 PostgreSQL 에 적용. 컴파일에는 불필요. CREATE TABLE IF NOT EXISTS @mod@_ledger ( key VARCHAR(15) NOT NULL, merch_id VARCHAR(15) NOT NULL, card_no VARCHAR(19) NOT NULL, amount BIGINT NOT NULL DEFAULT 0, biz_date CHAR(8) NOT NULL, status CHAR(1) NOT NULL DEFAULT 'R', fee BIGINT NOT NULL DEFAULT 0, reg_ts TIMESTAMP NOT NULL DEFAULT now(), upd_ts TIMESTAMP, CONSTRAINT pk_@mod@_ledger PRIMARY KEY (key) ); CREATE INDEX IF NOT EXISTS ix_@ID@_date_status ON @mod@_ledger (biz_date, status); CREATE OR REPLACE VIEW @ID@_daily_v AS SELECT biz_date, status, count(*) AS cnt, sum(amount) AS total FROM @mod@_ledger GROUP BY biz_date, status; ''' SCHEMA_SQL = '''-- schema.sql - acquire-core-full 공통 스키마 루트 -- 모듈별 원장 테이블은 db/_*.sql 참조. CREATE TABLE IF NOT EXISTS merchant ( merch_id VARCHAR(15) NOT NULL, merch_name VARCHAR(60) NOT NULL, biz_no VARCHAR(12), status CHAR(1) NOT NULL DEFAULT 'A', CONSTRAINT pk_merchant PRIMARY KEY (merch_id) ); CREATE TABLE IF NOT EXISTS approval ( appr_no VARCHAR(9) NOT NULL, merch_id VARCHAR(15) NOT NULL, amount BIGINT NOT NULL, appr_date CHAR(8) NOT NULL, status CHAR(1) NOT NULL DEFAULT 'A', CONSTRAINT pk_approval PRIMARY KEY (appr_no) ); ''' # ====================================================================== # 생성 로직 # ====================================================================== def main(): ap = argparse.ArgumentParser() ap.add_argument("--fraction", type=float, default=1.0, help="목표 건수 대비 생성 비율 (테스트용)") args = ap.parse_args() frac = args.fraction def scaled(name, floor): return max(floor, int(round(TARGETS[name] * frac))) n_online = scaled("online", 11) n_batch = scaled("batch", 11) n_common = scaled("common", 11) n_dbio = scaled("dbio", 22) # 11 main + extras n_dbsql = scaled("dbio_sql", 11) n_fml = scaled("fml", 11) n_sh = scaled("sh", 11) n_mk = scaled("mk", 11) n_config = scaled("config", 11) n_mods = len(MOD_CODES) n_dbio_extra = max(0, n_dbio - n_mods) # main 은 모듈당 1개 # 헤더: core(11) + io헤더(extra dbio 수) + cu헤더(common 수) + standalone n_hdr_target = scaled("headers", n_mods + 4) n_standalone = max(0, n_hdr_target - n_mods - n_dbio_extra - n_common) # ---- 디렉토리 초기화 ------------------------------------------------- for sub in ["framework", "app", "fml", "scripts", "mk", "config", "db", "build", "bin"]: p = os.path.join(ROOT, sub) if os.path.isdir(p): shutil.rmtree(p) for f in ["Makefile", "inventory.json"]: p = os.path.join(ROOT, f) if os.path.isfile(p): os.remove(p) def wr(relpath, content): p = os.path.join(ROOT, relpath) os.makedirs(os.path.dirname(p), exist_ok=True) with open(p, "w", encoding="utf-8") as fh: fh.write(content) inventory = [] counts = {} def bump(k, n=1): counts[k] = counts.get(k, 0) + n def loc_of(s): return s.count("\n") + (0 if s.endswith("\n") else 1) def sqlcount(s): return s.count("EXEC SQL") def add_inv(pid, module, archetype, tier, path, content): inventory.append({ "id": pid, "module": module, "archetype": archetype, "tier": tier, "path": path, "loc": loc_of(content), "sqlCount": sqlcount(content), }) # ---- 프레임워크 (공유) ---------------------------------------------- wr("framework/txcore/include/txcore.h", TXCORE_H) wr("framework/txcore/dbio/txcore_dbio.h", TXCORE_DBIO_H) wr("framework/txcore/src/txcore.c", TXCORE_C) wr("app/shared/include/msg_layout.h", MSG_LAYOUT_H) wr("app/shared/include/acq_util.h", ACQ_UTIL_H) wr("app/shared/common/util_date.c", UTIL_DATE_C) wr("app/shared/common/util_amount.c", UTIL_AMOUNT_C) wr("app/shared/common/util_msg.c", UTIL_MSG_C) bump("framework", 3) bump("shared_hdr", 2) bump("shared_c", 3) # ---- 모듈 core 헤더 + dbio_main ------------------------------------- first_online = {} # mod -> flagship online id first_batch = {} # mod -> flagship batch id for mod, kname, xform in MODULES: core = render(CORE_H_TPL, mod=mod, MOD=mod.upper(), KMOD=kname, XFORM=xform) wr("app/include/%s_core.h" % mod, core) bump("headers") dm = render(DBIO_MAIN_TPL, mod=mod, MOD=mod.upper(), KMOD=kname, XFORM=xform) pid = "%s_dbio_main" % mod path = "app/dbio/%s.pgc" % pid wr(path, dm) add_inv(pid, mod, "dbio", "medium", path, dm) bump("dbio") # ---- 온라인 서비스 --------------------------------------------------- seq = {m: 0 for m in MOD_CODES} for i in range(n_online): mod = MOD_CODES[i % n_mods] seq[mod] += 1 pid = "%s_ol_%04d" % (mod, seq[mod]) svc = pid.upper() tier = tier_for(i) kname = MOD_KNAME[mod] xform = MOD_XFORM[mod] if mod not in first_online: first_online[mod] = (pid, svc) body = {"easy": ONLINE_EASY, "medium": ONLINE_MEDIUM, "hard": ONLINE_HARD}[tier] content = render(ONLINE_HEAD + body, mod=mod, MOD=mod.upper(), ID=pid, SVC=svc, TIER=tier, KMOD=kname, XFORM=xform, NEXTSVC="%s_POST" % mod.upper()) path = "app/online/%s.pgc" % pid wr(path, content) add_inv(pid, mod, "online", tier, path, content) bump("online") # ---- 배치 ------------------------------------------------------------ bseq = {m: 0 for m in MOD_CODES} for i in range(n_batch): mod = MOD_CODES[i % n_mods] bseq[mod] += 1 pid = "%s_bt_%04d" % (mod, bseq[mod]) tier = tier_for(i) kname = MOD_KNAME[mod] xform = MOD_XFORM[mod] if mod not in first_batch: first_batch[mod] = pid content = render(BATCH_TPL, mod=mod, MOD=mod.upper(), ID=pid, TIER=tier, KMOD=kname, XFORM=xform) path = "app/batch/%s.pgc" % pid wr(path, content) add_inv(pid, mod, "batch", tier, path, content) bump("batch") # ---- 추가 DBIO ------------------------------------------------------- ioseq = {m: 0 for m in MOD_CODES} for i in range(n_dbio_extra): mod = MOD_CODES[i % n_mods] ioseq[mod] += 1 pid = "%s_io_%04d" % (mod, ioseq[mod]) tier = tier_for(i) kname = MOD_KNAME[mod] xform = MOD_XFORM[mod] hdr = render(DBIO_EXTRA_H, ID=pid, IDU=pid.upper(), KMOD=kname) wr("app/include/%s.h" % pid, hdr) bump("headers") content = render(DBIO_EXTRA_TPL, mod=mod, ID=pid, TIER=tier, KMOD=kname, XFORM=xform) path = "app/dbio/%s.pgc" % pid wr(path, content) add_inv(pid, mod, "dbio", tier, path, content) bump("dbio") # ---- 공통 유틸 ------------------------------------------------------- cseq = {m: 0 for m in MOD_CODES} for i in range(n_common): mod = MOD_CODES[i % n_mods] cseq[mod] += 1 pid = "%s_cu_%04d" % (mod, cseq[mod]) tier = tier_for(i) kname = MOD_KNAME[mod] xform = MOD_XFORM[mod] hdr = render(COMMON_H, ID=pid, IDU=pid.upper(), KMOD=kname) wr("app/include/%s.h" % pid, hdr) bump("headers") content = render(COMMON_TPL, mod=mod, ID=pid, TIER=tier, KMOD=kname, XFORM=xform) path = "app/common/%s.c" % pid wr(path, content) add_inv(pid, mod, "common", tier, path, content) bump("common") # ---- 독립 헤더 ------------------------------------------------------- hseq = {m: 0 for m in MOD_CODES} for i in range(n_standalone): mod = MOD_CODES[i % n_mods] hseq[mod] += 1 pid = "%s_h_%04d" % (mod, hseq[mod]) kname = MOD_KNAME[mod] tpl = HDR_ROTATION[i % len(HDR_ROTATION)] content = render(tpl, ID=pid, IDU=pid.upper(), KMOD=kname) wr("app/include/%s.h" % pid, content) bump("headers") # ---- 데모 서버 main (온라인 데모) ----------------------------------- for mod in ONLINE_DEMOS: if mod not in first_online: continue _pid, svc = first_online[mod] content = render(DEMO_MAIN_TPL, mod=mod, SVC=svc, KMOD=MOD_KNAME[mod]) wr("app/common/demo_%s_main.c" % mod, content) bump("demo_main") # ---- FML ------------------------------------------------------------- for i in range(n_fml): mod = MOD_CODES[i % n_mods] pid = "%s_fml_%04d" % (mod, i // n_mods + 1) content = render(FML_TPL, ID=pid.upper(), KMOD=MOD_KNAME[mod], BASE=(i % 9) + 1) wr("fml/%s.fml" % pid, content) bump("fml") # ---- 셸 러너 --------------------------------------------------------- for i in range(n_sh): mod = MOD_CODES[i % n_mods] pid = "%s_run_%04d" % (mod, i // n_mods + 1) tier = tier_for(i) runbin = "demo_%s_batch" % mod if mod in BATCH_DEMOS else \ first_batch.get(mod, "%s_bt_0001" % mod) content = render(SH_TPL, ID=pid, TIER=tier, KMOD=MOD_KNAME[mod], RUNBIN=runbin) wr("scripts/%s.sh" % pid, content) os.chmod(os.path.join(ROOT, "scripts/%s.sh" % pid), 0o755) bump("sh") # ---- Makefile 조각 --------------------------------------------------- for i in range(n_mk): mod = MOD_CODES[i % n_mods] pid = "%s_frag_%04d" % (mod, i // n_mods + 1) tier = tier_for(i) content = render(MK_TPL, ID=pid, IDU=pid.upper(), mod=mod, TIER=tier, KMOD=MOD_KNAME[mod], XFORM=MOD_XFORM[mod], SUBDIR="online", REF="%s_ol_0001.pgc" % mod) wr("mk/%s.mk" % pid, content) bump("mk") # ---- 설정 (.ubb/.cfg/.dat) ------------------------------------------ cfg_exts = ["ubb", "cfg", "dat"] for i in range(n_config): mod = MOD_CODES[i % n_mods] ext = cfg_exts[i % 3] pid = "%s_cfg_%04d" % (mod, i // n_mods + 1) if ext == "ubb": content = render(UBB_TPL, ID=pid, MOD=mod.upper(), KMOD=MOD_KNAME[mod], IPCKEY=str(70000 + i), GRPNO=str((i % 30) + 1)) elif ext == "cfg": content = render(CFG_TPL, ID=pid, mod=mod, XFORM=MOD_XFORM[mod], TIER=tier_for(i)) else: content = render(DAT_TPL, ID=pid, KMOD=MOD_KNAME[mod]) wr("config/%s.%s" % (pid, ext), content) bump("config") # ---- DBIO SQL 스키마 ------------------------------------------------- wr("db/schema.sql", SCHEMA_SQL) for i in range(n_dbsql): mod = MOD_CODES[i % n_mods] pid = "%s_ddl_%04d" % (mod, i // n_mods + 1) content = render(SQL_TPL, ID=pid, mod=mod, KMOD=MOD_KNAME[mod], TIER=tier_for(i)) wr("db/%s.sql" % pid, content) bump("dbio_sql") # ---- Makefile -------------------------------------------------------- wr("Makefile", build_makefile(first_online, first_batch)) # ---- inventory.json -------------------------------------------------- tier_hist = {} arche_hist = {} for r in inventory: tier_hist[r["tier"]] = tier_hist.get(r["tier"], 0) + 1 arche_hist[r["archetype"]] = arche_hist.get(r["archetype"], 0) + 1 inv_doc = { "corpus": "acquire-core-full", "domain": "card acquiring / settlement (카드 매입·정산)", "framework": "TxCore (ECPG + 고정길이 전문 + TP 관용구)", "generatedBy": "generate.py", "fraction": frac, "fileCounts": counts, "programCount": len(inventory), "byTier": tier_hist, "byArchetype": arche_hist, "modules": {m[0]: m[1] for m in MODULES}, "programs": sorted(inventory, key=lambda r: r["path"]), } with open(os.path.join(ROOT, "inventory.json"), "w", encoding="utf-8") as fh: json.dump(inv_doc, fh, ensure_ascii=False, indent=2) # ---- 요약 출력 ------------------------------------------------------- total_files = 0 for _root, _dirs, files in os.walk(ROOT): if "/.git" in _root: continue for f in files: total_files += 1 print("=== acquire-core-full 생성 완료 (fraction=%.3f) ===" % frac) for k in sorted(counts): print(" %-12s %5d" % (k, counts[k])) print(" programs(inv) %5d" % len(inventory)) print(" tier ", tier_hist) print(" 총 파일수 ", total_files) def build_makefile(first_online, first_batch): lines = [] A = lines.append A("# ==========================================================================") A("# acquire-core-full / TxCore Makefile (생성기 산출물)") A("#") A("# .pgc --ecpg--> .c --gcc--> build/*.o --ar--> libtxcore.a --ld--> bin/*") A("#") A("# ~1000개 .c/.pgc 를 build/*.o 로 컴파일하는 것이 빌드 검증의 핵심.") A("# 와일드카드로 전체 소스를 수집하고, 대표 데모 바이너리 몇 개를 링크한다.") A("# ==========================================================================") A("CC := gcc") A("ECPG := ecpg") A("AR := ar") A("PG_INCDIR := $(shell pg_config --includedir 2>/dev/null)") A("") A("INCLUDES := -Iframework/txcore/include \\") A(" -Iframework/txcore/dbio \\") A(" -Iapp/include \\") A(" -Iapp/shared/include \\") A(" -I$(PG_INCDIR)") A("") A("# 고정길이 관용구 경고만 억제 (-Werror 없음)") A("CFLAGS := -g -O2 -Wall -Wextra -Wno-unused-parameter \\") A(" -Wno-stringop-truncation -Wno-format-truncation \\") A(" -Wno-unused-variable -Wno-unused-but-set-variable $(INCLUDES)") A("ECPGFLAGS := -Iframework/txcore/include -Iframework/txcore/dbio \\") A(" -Iapp/include -Iapp/shared/include") A("LDLIBS := -lecpg -lpq") A("") A("BUILD := build") A("BIN := bin") A("") A("# ---- 소스 수집 (와일드카드) ----------------------------------------------") A("ONLINE_PGC := $(wildcard app/online/*.pgc)") A("BATCH_PGC := $(wildcard app/batch/*.pgc)") A("DBIO_PGC := $(wildcard app/dbio/*.pgc)") A("COMMON_C := $(wildcard app/common/*.c)") A("SHARED_C := $(wildcard app/shared/common/*.c)") A("") A("PGC_ALL := $(ONLINE_PGC) $(BATCH_PGC) $(DBIO_PGC)") A("GEN_C := $(PGC_ALL:.pgc=.c)") A("") A("PGC_OBJ := $(patsubst %,$(BUILD)/%.o,$(basename $(notdir $(PGC_ALL))))") A("COMMON_OBJ := $(patsubst %,$(BUILD)/%.o,$(basename $(notdir $(COMMON_C))))") A("SHARED_OBJ := $(patsubst %,$(BUILD)/%.o,$(basename $(notdir $(SHARED_C))))") A("ALL_OBJ := $(PGC_OBJ) $(COMMON_OBJ) $(SHARED_OBJ)") A("") A("LIBTXCORE := $(BUILD)/libtxcore.a") A("") # 데모 바이너리 목록 demo_bins = [] for mod in ONLINE_DEMOS: if mod in first_online: demo_bins.append("$(BIN)/demo_%s_server" % mod) for mod in BATCH_DEMOS: if mod in first_batch: demo_bins.append("$(BIN)/demo_%s_batch" % mod) A("DEMO_BINS := " + " ".join(demo_bins)) A("") A(".SECONDARY:") A(".PHONY: all clean dirs gen objs demos") A("") A("all: dirs $(LIBTXCORE) objs demos") A("\t@echo \"==> 빌드 완료\"") A("\t@echo \"오브젝트: $(words $(ALL_OBJ)) 개\"") A("") A("dirs:") A("\t@mkdir -p $(BUILD) $(BIN)") A("") A("gen: $(GEN_C)") A("objs: $(ALL_OBJ)") A("demos: $(DEMO_BINS)") A("") A("# ---- ecpg: .pgc -> .c (디렉토리별) ---------------------------------------") A("app/online/%.c: app/online/%.pgc") A("\t$(ECPG) $(ECPGFLAGS) -o $@ $<") A("app/batch/%.c: app/batch/%.pgc") A("\t$(ECPG) $(ECPGFLAGS) -o $@ $<") A("app/dbio/%.c: app/dbio/%.pgc") A("\t$(ECPG) $(ECPGFLAGS) -o $@ $<") A("") A("# ---- 컴파일: build/.o (gen 선행 보장) --------------------------") A("$(BUILD)/%.o: app/online/%.c | gen") A("\t$(CC) $(CFLAGS) -c $< -o $@") A("$(BUILD)/%.o: app/batch/%.c | gen") A("\t$(CC) $(CFLAGS) -c $< -o $@") A("$(BUILD)/%.o: app/dbio/%.c | gen") A("\t$(CC) $(CFLAGS) -c $< -o $@") A("$(BUILD)/%.o: app/common/%.c") A("\t$(CC) $(CFLAGS) -c $< -o $@") A("$(BUILD)/%.o: app/shared/common/%.c") A("\t$(CC) $(CFLAGS) -c $< -o $@") A("") A("# ---- TxCore 정적 라이브러리 ----------------------------------------------") A("$(BUILD)/txcore.o: framework/txcore/src/txcore.c") A("\t$(CC) $(CFLAGS) -c $< -o $@") A("$(LIBTXCORE): $(BUILD)/txcore.o") A("\t$(AR) rcs $@ $^") A("") A("# ---- 대표 데모 바이너리 (링크 검증) --------------------------------------") for mod in ONLINE_DEMOS: if mod not in first_online: continue flag_id, _svc = first_online[mod] objs = "$(BUILD)/demo_%s_main.o $(BUILD)/%s.o $(BUILD)/%s_dbio_main.o $(SHARED_OBJ)" % ( mod, flag_id, mod) A("$(BIN)/demo_%s_server: %s $(LIBTXCORE)" % (mod, objs)) A("\t$(CC) $(CFLAGS) -o $@ %s -L$(BUILD) -ltxcore $(LDLIBS)" % objs) A("") for mod in BATCH_DEMOS: if mod not in first_batch: continue bid = first_batch[mod] objs = "$(BUILD)/%s.o $(BUILD)/%s_dbio_main.o $(SHARED_OBJ)" % (bid, mod) A("$(BIN)/demo_%s_batch: %s $(LIBTXCORE)" % (mod, objs)) A("\t$(CC) $(CFLAGS) -o $@ %s -L$(BUILD) -ltxcore $(LDLIBS)" % objs) A("") A("# ==========================================================================") A("clean:") A("\trm -rf $(BUILD) $(BIN)") A("\trm -f $(GEN_C)") A("") return "\n".join(lines) + "\n" if __name__ == "__main__": main()