Wave L: 외부 연동·전문처리 (ISO8583 와이어·소켓 왕복·클리어링 파일, 패리티→~89%)

외부 카드망 연동(전문처리)을 실제 프로토콜로 구현 — 마이그레이션 최난이도 계층
(소켓·바이너리 전문·프레이밍·상태처리)이 실제로 동작.

■ ISO 8583 와이어 코덱 (common/acq_iso8583.c)
- 1차 비트맵(64bit) + DE 인코딩/파싱(PAN LLVAR·금액·STAN·RRN·응답코드·MID/TID 등).
- iso_build/iso_parse 왕복 무손실 검증(비트맵 7220000000C08000 정확).

■ 외부 카드망 시뮬레이터 (clients/acq_extsw.c, :9500)
- TCP 서버: 길이접두 프레이밍 + ISO 전문. 0200 수신→승인판정→0210 응답 조립
  (DE38 승인번호·DE39 응답코드). 금액임계/STAN으로 일부 거절(05).

■ 전문 왕복 드라이버 (clients/acq_netdrv.c)
- 실매입건→0200 조립→소켓 전송→0210 수신·파싱→mg_netlog 적재(RTT·응답코드).
- 실측: 200건 왕복 승인198/거절2/오류0, 0200→0210, 평균 RTT 0.3ms.

■ 클리어링 파일 교환 (clients/acq_clrfile.c)
- cl_presentment→고정폭 정산파일(/app/out/CLR_BC_YYYYMMDD.dat) HD/DT/TR+체크섬 생성,
  cl_clearing_file 등록. 실제 SFTP 배치 파일교환 대역. 샘플 파일 커밋.

■ 스키마·부팅·화면
- 74-netio.sql: mg_netlog(왕복로그)·cl_clearing_file(파일 레지스트리).
- entrypoint.sh: acq_extsw 부팅 자동기동(:9500).
- build.sh: 클라이언트에 libacqcommon.a(ISO 코덱) 링크.
- 시스템현황에 "외부망 전문 왕복"·"클리어링 파일 교환" 패널.

패리티 재채점 ≈89/100. 남은 ~11%는 상용회선·HSM·다중센터 DR 등 배포 인프라.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
hyeongwoo 2026-07-23 14:59:36 +09:00
parent bba433308a
commit 12812a6c33
12 changed files with 525 additions and 4 deletions

View file

@ -156,7 +156,8 @@ NCLI=0
for c in "$SRC"/clients/*.c; do
name="$(basename "${c%.c}")"
log "client $name"
buildclient -o "$BIN/$name" -f "$c" -a "-L$PGLIB -lpq"
# libacqcommon.a 링크: ISO 8583 코덱(acq_iso8583) 등 공통 C 함수 사용 가능.
buildclient -o "$BIN/$name" -f "$c" -a "$LIB/libacqcommon.a -L$PGLIB -lpq"
NCLI=$((NCLI+1))
done

View file

@ -44,6 +44,11 @@ nohup /app/bin/acq_httpgw > /app/log/acq_httpgw.log 2>&1 &
sleep 1
grep -q "listening" /app/log/acq_httpgw.log && echo "acq_httpgw: UP (:8090)" || tail -3 /app/log/acq_httpgw.log
echo "--- acq_extsw (외부 카드망 시뮬레이터 :9500 — ISO 8583 전문 왕복 상대망) ---"
nohup /app/bin/acq_extsw 9500 > /app/log/acq_extsw.log 2>&1 &
sleep 1
grep -q "listening" /app/log/acq_extsw.log && echo "acq_extsw: UP (:9500)" || tail -2 /app/log/acq_extsw.log
echo "=================================================================="
echo " acquire-core-x : UP. Run the 매입 chain driver with:"
echo " docker compose -f docker/docker-compose.yml exec app /app/run-driver.sh"

View file

@ -0,0 +1,14 @@
HDCLEARINGFILE BC 20260719PRESENTMENT HEADER
DT000000707549000000009154BC 00000000002862320260719FIRST_PRESENT CLEARED
DT000000707550000000009155BC 00000000024378920260719FIRST_PRESENT CLEARED
DT000000707551000000009158BC 00000000027063620260719FIRST_PRESENT CLEARED
DT000000707552000000009162BC 00000000019592120260719FIRST_PRESENT CLEARED
DT000000707553000000009173BC 00000000073770120260719FIRST_PRESENT CLEARED
DT000000707554000000009177BC 00000000003109120260719FIRST_PRESENT CLEARED
DT000000707555000000009179BC 00000000000746720260719FIRST_PRESENT CLEARED
DT000000707556000000009183BC 00000000023480820260719FIRST_PRESENT CLEARED
DT000000707557000000009184BC 00000000004905420260719FIRST_PRESENT CLEARED
DT000000707558000000009187BC 00000000143387820260719FIRST_PRESENT CLEARED
DT000000707559000000009188BC 00000000000562620260719FIRST_PRESENT CLEARED
DT000000715120000000009188BC 00000000000562620260719REPRESENT CLEARED
TR000000000012000000003244220B79BBF32

View file

@ -0,0 +1,77 @@
/*
* acq_clrfile.c - () ( ).
* . cl_presentment
* /app/out/
* cl_clearing_file . SFTP .
* ( 80): TYPE(2) PRESENT_ID(12) PURCHASE_ID(12) SCHEME(4)
* CLEARING_AMT(15) PRESENT_DATE(8) MSG_CLASS(15) STATUS(12)
* : acq_clrfile <biz_date YYYY-MM-DD> [outdir]
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <libpq-fe.h>
int main(int argc, char **argv) {
const char *bizdate = (argc > 1) ? argv[1] : "2026-07-19";
const char *outdir = (argc > 2) ? argv[2] : "/app/out";
const char *ci = getenv("PGCONN");
PGconn *c = PQconnectdb(ci ? ci : "host=db user=acq password=acq dbname=acq");
if (PQstatus(c) != CONNECTION_OK) { fprintf(stderr, "PG 연결실패: %s\n", PQerrorMessage(c)); return 1; }
char q[512];
snprintf(q, sizeof(q),
"SELECT presentment_id, purchase_id, scheme, clearing_amt, "
"to_char(present_date,'YYYYMMDD'), msg_class, status "
"FROM cl_presentment WHERE present_date = '%s' ORDER BY presentment_id", bizdate);
PGresult *r = PQexec(c, q);
if (PQresultStatus(r) != PGRES_TUPLES_OK) { fprintf(stderr, "조회실패: %s\n", PQerrorMessage(c)); return 1; }
int n = PQntuples(r);
char path[512];
char dcompact[16]; int j = 0;
for (const char *p = bizdate; *p; p++) if (*p != '-') dcompact[j++] = *p;
dcompact[j] = 0;
snprintf(path, sizeof(path), "%s/CLR_BC_%s.dat", outdir, dcompact);
/* outdir 보장 */
char mkd[600]; snprintf(mkd, sizeof(mkd), "mkdir -p %s", outdir); if (system(mkd) != 0) {}
FILE *f = fopen(path, "w");
if (!f) { fprintf(stderr, "파일열기 실패: %s\n", path); return 1; }
/* 헤더 레코드 */
fprintf(f, "HD%-12s%-12s%-4s%15s%-8s%-15s%-12s\n",
"CLEARINGFILE", "", "BC", "", dcompact, "PRESENTMENT", "HEADER");
long total = 0; unsigned long chk = 0;
for (int i = 0; i < n; i++) {
long pres = atol(PQgetvalue(r, i, 0));
long pid = atol(PQgetvalue(r, i, 1));
const char *sc = PQgetvalue(r, i, 2);
long amt = atol(PQgetvalue(r, i, 3));
const char *pd = PQgetvalue(r, i, 4);
const char *mc = PQgetvalue(r, i, 5);
const char *st = PQgetvalue(r, i, 6);
fprintf(f, "DT%012ld%012ld%-4s%015ld%-8s%-15s%-12s\n", pres, pid, sc, amt, pd, mc, st);
total += amt; chk = (chk * 131 + (unsigned long)amt) & 0xFFFFFFFF;
}
/* 트레일러: 건수·합계·체크섬 */
fprintf(f, "TR%012d%015ld%08lX%43s\n", n, total, chk, "");
fclose(f);
/* 레지스트리 등록 */
char reg[900];
snprintf(reg, sizeof(reg),
"INSERT INTO cl_clearing_file(file_id,direction,filename,scheme,biz_date,"
"rec_count,total_amount,checksum,status) VALUES "
"(nextval('cl_file_seq'),'OUT','CLR_BC_%s.dat','BC','%s',%d,%ld,'%08lX','GENERATED') "
"ON CONFLICT DO NOTHING", dcompact, bizdate, n, total, chk);
PGresult *ir = PQexec(c, reg); PQclear(ir);
PQclear(r);
printf(">>> 클리어링 파일 생성: %s (레코드 %d · 합계 %ld · 체크섬 %08lX)\n", path, n, total, chk);
PQfinish(c);
return 0;
}

102
app/src/clients/acq_extsw.c Normal file
View file

@ -0,0 +1,102 @@
/*
* acq_extsw.c - (/VAN/ ).
* ISO 8583 endpoint
* . : 4-hex + ISO 8583 (hex).
* 0200 0210 (DE38 , DE39 ).
* / (05)· . ··
* Spring .
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include "acq_iso8583.h"
static int read_n(int fd, char *buf, int n) {
int got = 0;
while (got < n) {
int r = read(fd, buf + got, n - got);
if (r <= 0) return -1;
got += r;
}
return got;
}
/* 승인번호 6자리 생성(STAN 기반 결정적) */
static void gen_approval(const char *stan, char *out) {
unsigned h = 5381; const char *p = stan;
while (*p) h = ((h << 5) + h) + (unsigned char)*p++;
snprintf(out, 7, "%06u", h % 1000000);
}
static void handle(int cfd) {
char lenhex[5], wire[4096];
if (read_n(cfd, lenhex, 4) != 4) return;
lenhex[4] = 0;
int len = (int)strtol(lenhex, NULL, 16);
if (len <= 0 || len >= (int)sizeof(wire)) return;
if (read_n(cfd, wire, len) != len) return;
wire[len] = 0;
iso_msg_t req; int c = iso_parse(wire, &req);
if (c < 0) return;
/* 승인판정: 금액 파싱, 임계 초과·STAN 말미로 일부 거절 */
long amt = atol(iso_get(&req, 4));
const char *stan = iso_get(&req, 11);
int decline = (amt > 490000000) || ((stan[strlen(stan) > 0 ? strlen(stan) - 1 : 0] == '7') && amt > 3000000);
iso_msg_t rsp; iso_init(&rsp);
/* 응답 MTI = 요청 MTI + 0x10 (0200->0210, 0800->0810, 0420->0430) */
strncpy(rsp.mti, req.mti, 4); rsp.mti[3] = (req.mti[3] == '0') ? '0' : req.mti[3];
if (req.mti[2] == '0') rsp.mti[2] = '1'; /* 20x0 -> 21x0 */
else rsp.mti[2] = (req.mti[2] == '2') ? '3' : req.mti[2];
rsp.mti[4] = 0;
/* 응답 필드: 요청 에코 + 응답코드/승인번호 */
iso_set(&rsp, 2, iso_get(&req, 2));
iso_set(&rsp, 3, iso_get(&req, 3));
iso_set(&rsp, 4, iso_get(&req, 4));
iso_set(&rsp, 7, iso_get(&req, 7));
iso_set(&rsp, 11, stan);
iso_set(&rsp, 41, iso_get(&req, 41));
iso_set(&rsp, 42, iso_get(&req, 42));
iso_set(&rsp, 49, iso_get(&req, 49));
if (decline) {
iso_set(&rsp, 39, "05"); /* 거절 */
} else {
char appr[8]; gen_approval(stan, appr);
iso_set(&rsp, 38, appr);
iso_set(&rsp, 39, "00"); /* 승인 */
}
/* RRN(DE37) = 전송일시+STAN 조합 */
char rrn[16]; snprintf(rrn, sizeof(rrn), "%.6s%.6s", iso_get(&req, 7), stan);
iso_set(&rsp, 37, rrn);
char out[4096]; int ol = iso_build(&rsp, out, sizeof(out));
if (ol < 0) return;
char frame[4200]; int fl = snprintf(frame, sizeof(frame), "%04X%s", ol, out);
write(cfd, frame, fl);
}
int main(int argc, char **argv) {
int port = (argc > 1) ? atoi(argv[1]) : 9500;
int sfd = socket(AF_INET, SOCK_STREAM, 0);
int opt = 1; setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in addr; memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET; addr.sin_addr.s_addr = INADDR_ANY; addr.sin_port = htons(port);
if (bind(sfd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { perror("bind"); return 1; }
if (listen(sfd, 32) < 0) { perror("listen"); return 1; }
fprintf(stderr, "acq_extsw: 외부 카드망 시뮬레이터 listening on :%d\n", port);
for (;;) {
int cfd = accept(sfd, NULL, NULL);
if (cfd < 0) continue;
handle(cfd);
close(cfd);
}
return 0;
}

View file

@ -0,0 +1,116 @@
/*
* acq_netdrv.c - ( /).
* ISO 8583 0200 (acq_extsw)
* 0210 · (RTT)· mg_netlog .
* : acq_netdrv <count> [extsw_host] [extsw_port]
* (··· = ).
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <libpq-fe.h>
#include "acq_iso8583.h"
static long now_ms(void) {
struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts);
return ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
}
/* extsw 로 프레임 왕복. 반환 0=성공, -1=오류 */
static int roundtrip(const char *host, int port, const char *reqwire, char *rspwire, int cap) {
int fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr; memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET; addr.sin_port = htons(port);
struct hostent *he = gethostbyname(host);
if (!he) { close(fd); return -1; }
memcpy(&addr.sin_addr, he->h_addr_list[0], he->h_length);
if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { close(fd); return -1; }
char frame[4200]; int fl = snprintf(frame, sizeof(frame), "%04X%s", (int)strlen(reqwire), reqwire);
if (write(fd, frame, fl) != fl) { close(fd); return -1; }
char lenhex[5]; int got = 0;
while (got < 4) { int r = read(fd, lenhex + got, 4 - got); if (r <= 0) { close(fd); return -1; } got += r; }
lenhex[4] = 0; int rlen = (int)strtol(lenhex, NULL, 16);
if (rlen <= 0 || rlen >= cap) { close(fd); return -1; }
got = 0;
while (got < rlen) { int r = read(fd, rspwire + got, rlen - got); if (r <= 0) { close(fd); return -1; } got += r; }
rspwire[rlen] = 0;
close(fd);
return 0;
}
int main(int argc, char **argv) {
int count = (argc > 1) ? atoi(argv[1]) : 10;
const char *host = (argc > 2) ? argv[2] : "localhost";
int port = (argc > 3) ? atoi(argv[3]) : 9500;
const char *ci = getenv("PGCONN");
PGconn *c = PQconnectdb(ci ? ci : "host=db user=acq password=acq dbname=acq");
if (PQstatus(c) != CONNECTION_OK) { fprintf(stderr, "PG 연결실패: %s\n", PQerrorMessage(c)); return 1; }
PGresult *r = PQexec(c,
"SELECT purchase_id, amount, merchant_id, to_char(created_at,'MMDDHH24MISS'), "
" channel FROM purchase WHERE purchase_id < 10000000 ORDER BY random() LIMIT 200");
int nrows = PQntuples(r);
if (nrows == 0) { fprintf(stderr, "매입건 없음\n"); return 1; }
int ok = 0, decl = 0, err = 0; long tot_rtt = 0;
for (int i = 0; i < count; i++) {
int ri = i % nrows;
long pid = atol(PQgetvalue(r, ri, 0));
long amt = atol(PQgetvalue(r, ri, 1));
const char *mid = PQgetvalue(r, ri, 2);
const char *dt = PQgetvalue(r, ri, 3); /* MMDDhhmmss */
char stan[7]; snprintf(stan, sizeof(stan), "%06ld", (pid + i) % 1000000);
iso_msg_t req; iso_init(&req); strcpy(req.mti, "0200");
iso_set(&req, 2, "4000123412341234");
iso_set(&req, 3, "000000");
char amtbuf[16]; snprintf(amtbuf, sizeof(amtbuf), "%ld", amt); iso_set(&req, 4, amtbuf);
iso_set(&req, 7, dt);
iso_set(&req, 11, stan);
iso_set(&req, 41, "TERM0001");
iso_set(&req, 42, mid);
iso_set(&req, 49, "410");
char reqwire[4096], rspwire[4096];
int rc = iso_build(&req, reqwire, sizeof(reqwire));
if (rc < 0) { err++; continue; }
long t0 = now_ms();
int rt = roundtrip(host, port, reqwire, rspwire, sizeof(rspwire));
long rtt = now_ms() - t0;
tot_rtt += rtt;
const char *status = "OK", *rcode = "", *appr = "", *rmti = "", *rrn = "";
iso_msg_t rsp;
if (rt < 0) { status = "TIMEOUT"; err++; }
else if (iso_parse(rspwire, &rsp) < 0) { status = "ERROR"; err++; }
else {
rmti = rsp.mti; rcode = iso_get(&rsp, 39); appr = iso_get(&rsp, 38); rrn = iso_get(&rsp, 37);
if (strcmp(rcode, "00") == 0) ok++; else decl++;
}
char q[9000];
snprintf(q, sizeof(q),
"INSERT INTO mg_netlog(net_id,purchase_id,channel,peer,req_mti,rsp_mti,stan,rrn,"
"resp_code,approval_no,amount,rtt_ms,req_wire,rsp_wire,status) VALUES "
"(nextval('mg_netlog_seq'),%ld,'CH01','BC-NET','0200','%s','%s','%s','%s','%s',%ld,%ld,'%s','%s','%s')",
pid, rmti, stan, rrn, rcode, appr, amt, rtt, reqwire,
(rt < 0 ? "" : rspwire), status);
PGresult *ir = PQexec(c, q);
PQclear(ir);
}
PQclear(r);
printf(">>> NETDRV 완료: 요청 %d · 승인 %d · 거절 %d · 오류 %d · 평균RTT %ldms\n",
count, ok, decl, err, count ? tot_rtt / count : 0);
PQfinish(c);
return 0;
}

View file

@ -0,0 +1,112 @@
/*
* acq_iso8583.c - ISO 8583 (1 + DE ).
* DE ( ):
* n= , an/ans= , LLVAR=2 .
* : MTI(4 ASCII) + (16 hex = 64bit) + .
* i(1-base) = DE i . DE1(1)=2().
*/
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include "acq_iso8583.h"
/* DE 포맷 테이블: len>0 고정길이, len<0 = LLVAR(최대 -len). type: 0=n,1=ans */
static const struct { int len; int type; } DEF[ISO_MAX_DE + 1] = {
[2] = {-19, 0}, /* PAN (LLVAR) */
[3] = {6, 0}, /* 처리코드 */
[4] = {12, 0}, /* 거래금액 */
[7] = {10, 0}, /* 전송일시 MMDDhhmmss */
[11] = {6, 0}, /* STAN */
[12] = {6, 0}, /* 로컬시각 hhmmss */
[13] = {4, 0}, /* 로컬일자 MMDD */
[18] = {4, 0}, /* MCC */
[22] = {3, 0}, /* POS entry mode */
[37] = {12, 1}, /* RRN(참조번호) */
[38] = {6, 1}, /* 승인번호 */
[39] = {2, 1}, /* 응답코드 */
[41] = {8, 1}, /* 단말ID(TID) */
[42] = {15, 1}, /* 가맹점ID(MID) */
[43] = {-40, 1}, /* 가맹점명(LLVAR) */
[49] = {3, 0}, /* 통화코드 */
[55] = {-99, 1}, /* EMV 데이터(LLVAR) */
};
void iso_init(iso_msg_t *m) {
memset(m, 0, sizeof(*m));
}
void iso_set(iso_msg_t *m, int de, const char *val) {
if (de < 2 || de > ISO_MAX_DE || !val) return;
strncpy(m->de[de], val, sizeof(m->de[de]) - 1);
m->de[de][sizeof(m->de[de]) - 1] = 0;
m->present[de] = 1;
}
const char *iso_get(const iso_msg_t *m, int de) {
if (de < 2 || de > ISO_MAX_DE || !m->present[de]) return "";
return m->de[de];
}
/* 고정 숫자필드 좌0패딩 / 영숫자 우공백패딩 */
static void fmt_fixed(const char *v, int len, int type, char *out) {
int vl = (int)strlen(v);
if (vl >= len) { memcpy(out, v, len); out[len] = 0; return; }
if (type == 0) { /* n: 좌 0패딩 */
int pad = len - vl;
memset(out, '0', pad); memcpy(out + pad, v, vl); out[len] = 0;
} else { /* ans: 우 공백패딩 */
memcpy(out, v, vl); memset(out + vl, ' ', len - vl); out[len] = 0;
}
}
int iso_build(const iso_msg_t *m, char *out, int outcap) {
unsigned char bm[8] = {0};
char body[ISO_MAXLEN]; int bl = 0; int de;
for (de = 2; de <= 64; de++) {
if (!m->present[de] || DEF[de].len == 0) continue;
bm[(de - 1) / 8] |= (unsigned char)(0x80 >> ((de - 1) % 8));
const char *v = m->de[de];
if (DEF[de].len < 0) { /* LLVAR */
int vl = (int)strlen(v); int maxl = -DEF[de].len;
if (vl > maxl) vl = maxl;
bl += snprintf(body + bl, sizeof(body) - bl, "%02d", vl);
memcpy(body + bl, v, vl); bl += vl; body[bl] = 0;
} else { /* 고정 */
char f[160]; fmt_fixed(v, DEF[de].len, DEF[de].type, f);
bl += snprintf(body + bl, sizeof(body) - bl, "%s", f);
}
if (bl >= (int)sizeof(body) - 200) return -1;
}
/* MTI + 비트맵(hex 16) + body */
int n = snprintf(out, outcap, "%.4s", m->mti);
for (de = 0; de < 8; de++) n += snprintf(out + n, outcap - n, "%02X", bm[de]);
n += snprintf(out + n, outcap - n, "%s", body);
return (n < outcap) ? n : -1;
}
static int hexbyte(const char *p) {
int hi = (p[0] <= '9') ? p[0] - '0' : (p[0] & 0xDF) - 'A' + 10;
int lo = (p[1] <= '9') ? p[1] - '0' : (p[1] & 0xDF) - 'A' + 10;
return (hi << 4) | lo;
}
int iso_parse(const char *wire, iso_msg_t *m) {
iso_init(m);
int wl = (int)strlen(wire);
if (wl < 4 + 16) return -1;
memcpy(m->mti, wire, 4); m->mti[4] = 0;
unsigned char bm[8];
for (int i = 0; i < 8; i++) bm[i] = (unsigned char)hexbyte(wire + 4 + i * 2);
int pos = 4 + 16, cnt = 0;
for (int de = 2; de <= 64; de++) {
if (!(bm[(de - 1) / 8] & (0x80 >> ((de - 1) % 8)))) continue;
if (DEF[de].len == 0) return -1; /* 정의없는 필드 켜짐 = 오류 */
int flen;
if (DEF[de].len < 0) { /* LLVAR: 앞 2자리 길이 */
if (pos + 2 > wl) return -1;
flen = (wire[pos] - '0') * 10 + (wire[pos + 1] - '0'); pos += 2;
} else flen = DEF[de].len;
if (pos + flen > wl || flen >= (int)sizeof(m->de[de])) return -1;
memcpy(m->de[de], wire + pos, flen); m->de[de][flen] = 0;
m->present[de] = 1; pos += flen; cnt++;
}
return cnt;
}

View file

@ -0,0 +1,31 @@
/*
* acq_iso8583.h - ISO 8583 ( + ).
* : MTI(4) + 1 (16 hex) + DE ().
* (/VAN) ·.
* Pro*C/Tuxedo .
*/
#ifndef ACQ_ISO8583_H
#define ACQ_ISO8583_H
#define ISO_MAX_DE 64
#define ISO_MAXLEN 2048
/* 지원 DE (부분집합, 실무 핵심 필드) */
typedef struct {
char mti[5]; /* 0200/0210/0420/0800/0810 ... */
char de[ISO_MAX_DE + 1][128]; /* de[2]=PAN, de[3]=처리코드, de[4]=금액 ... */
unsigned char present[ISO_MAX_DE + 1]; /* 필드 존재 플래그 */
} iso_msg_t;
/* 초기화 */
void iso_init(iso_msg_t *m);
/* DE 설정(문자열) */
void iso_set(iso_msg_t *m, int de, const char *val);
/* DE 조회(없으면 "") */
const char *iso_get(const iso_msg_t *m, int de);
/* 조립: iso_msg_t -> 와이어 hex 문자열(out). 반환=길이, 실패 -1 */
int iso_build(const iso_msg_t *m, char *out, int outcap);
/* 파싱: 와이어 hex 문자열 -> iso_msg_t. 반환=파싱 DE 수, 실패 -1 */
int iso_parse(const char *wire, iso_msg_t *m);
#endif /* ACQ_ISO8583_H */

File diff suppressed because one or more lines are too long

View file

@ -1,2 +1,2 @@
/* 자동생성: tools/gen_metrics.py — 소스 실측 마이그레이션 난이도 지표. 손대지 말 것. */
window.METRICS={"SCALE":[{"k":"소스 파일 (.pgc/.c/.h)","v":4656,"u":"개"},{"k":"총 코드 라인","v":333079,"u":"LOC"},{"k":"온라인 서비스","v":1478,"u":"본"},{"k":"배치 잡","v":506,"u":"본"},{"k":"임베디드 SQL(EXEC SQL)","v":23879,"u":"블록"},{"k":"Tuxedo tpcall 호출","v":985,"u":"곳"},{"k":"전역 트랜잭션 tpbegin","v":1234,"u":"곳"},{"k":"UBF 버퍼 필드접근","v":47295,"u":"곳"},{"k":"DB 테이블","v":99,"u":"개"},{"k":"카피북 헤더(.h)","v":2222,"u":"개"}],"HAZARDS":[{"t":"Tuxedo ATMI (tpcall/tpservice/tpreturn)","n":985,"u":"호출","why":"Java·Spring에 1:1 대응 API가 없다. 서비스 호출 모델 자체를 REST/메시지로 재설계해야 하며, 문법 치환으로는 불가능."},{"t":"XA 2PC 전역 트랜잭션 (최대 6단 중첩)","n":1234,"u":"tpbegin","why":"형제 XA 브랜치 격리·2단계 커밋 의미론이 Spring @Transactional 경계와 다르다. 잘못 옮기면 교착·이중전기가 그대로 재현된다."},{"t":"ECPG 임베디드 SQL (호스트변수·커서·sqlca)","n":23879,"u":"블록","why":"EXEC SQL 블록마다 호스트변수 바인딩·sqlcode 분기·커서 수명을 JPA/MyBatis 의미로 옮겨야 한다. 자동 변환기가 가장 많이 깨지는 지점."},{"t":"UBF(FML32) 타입리스 버퍼","n":47295,"u":"필드접근","why":"필드ID 기반 동적 버퍼라 타입·필수여부가 컴파일타임에 없다. Java DTO로 옮기려면 47,000여 접근점의 계약을 사람이 복원해야 한다."},{"t":"카피북 공유(#include) 팬아웃","n":6921,"u":"참조","why":"한 카피북을 수십 프로그램이 공유해 구조체 변경이 전방위로 파급된다. 경계 없는 결합이라 모듈 단위 절단이 어렵다."},{"t":"전처리 매크로(#define)","n":2264,"u":"정의","why":"매크로가 코드 생성·분기를 숨겨 정적 분석을 방해한다. 전개 후에야 실제 로직이 드러난다."}],"TRACE":[{"root":"ACQUIRE","tree":{"svc":"ACQUIRE","mod":"ac","wr":["cm_log"],"children":[{"svc":"RECONCILE","mod":"rc","wr":["cm_log","ledger"],"children":[{"svc":"SETTLE","mod":"st","wr":["ledger","settlement"],"children":[{"svc":"ST_MDR","mod":"st","wr":["ledger"],"children":[{"svc":"LG_SETTLEPOST","mod":"lg","wr":["cm_log"],"children":[{"svc":"LG_BALCHK","mod":"lg","wr":["cm_log"],"children":[]}]}]}]}]}]},"stat":{"programs":6,"tables":3,"xa_branches":6,"tablelist":["cm_log","ledger","settlement"]}},{"root":"PAY_APPROVE","tree":{"svc":"PAY_APPROVE","mod":"py","wr":["cm_log","py_payout"],"children":[{"svc":"PAY_FILEGEN","mod":"py","wr":["cm_log","py_file","py_payout"],"children":[{"svc":"PAY_ACCTCHK","mod":"py","wr":["cm_log"],"children":[{"svc":"LG_POST","mod":"lg","wr":["cm_log"],"children":[{"svc":"LG_BALCHK","mod":"lg","wr":["cm_log"],"children":[]}]}]}]}]},"stat":{"programs":5,"tables":3,"xa_branches":5,"tablelist":["cm_log","py_file","py_payout"]}},{"root":"CL_DAILY","tree":{"svc":"CL_DAILY","mod":"cl","wr":["cm_log"],"children":[{"svc":"VL_SALES","mod":"vl","wr":[],"children":[{"svc":"RC_EXCEPTION","mod":"rc","wr":["cm_log"],"children":[{"svc":"CM_LOG","mod":"cm","wr":[],"children":[]}]}]},{"svc":"LG_CLOSE_LINK","mod":"lg","wr":["cm_log","lg_ledger_line"],"children":[{"svc":"CL_LOCK","mod":"cl","wr":["SET","cl_close_log","cl_period_lock","cm_log"],"children":[]}]}]},"stat":{"programs":6,"tables":5,"xa_branches":6,"tablelist":["SET","cl_close_log","cl_period_lock","cm_log","lg_ledger_line"]}}],"LEGACY":{"copybook_total":2222,"copybook_shared_max":138,"copybook_shared_name":"acq_common.h","macro_defs":2264,"goto":64,"userlog":17267,"sqlca_checks":18877,"embedded_sql_density":9.8},"ORACLE":{"asis_files":9,"asis_proc":7,"asis_plsql":2,"asis_loc":1827,"dialects":[{"t":"PL/SQL 패키지 호출 (pkg_x.proc)","n":18},{"t":"NVL / NVL2 널가드","n":60},{"t":"DECODE 다분기","n":16},{"t":"SYSDATE / SYSTIMESTAMP","n":42},{"t":"FROM DUAL","n":15},{"t":"시퀀스 .NEXTVAL","n":27},{"t":"ROWNUM 의사컬럼","n":6},{"t":"구식 외부조인 (+)","n":8},{"t":"CONNECT BY 계층질의","n":3},{"t":"WHENEVER SQLERROR 라벨분기","n":16},{"t":"TO_DATE / TO_CHAR 포맷","n":35},{"t":"RAISE_APPLICATION_ERROR","n":13}]}};
window.METRICS={"SCALE":[{"k":"소스 파일 (.pgc/.c/.h)","v":4661,"u":"개"},{"k":"총 코드 라인","v":333517,"u":"LOC"},{"k":"온라인 서비스","v":1478,"u":"본"},{"k":"배치 잡","v":506,"u":"본"},{"k":"임베디드 SQL(EXEC SQL)","v":23879,"u":"블록"},{"k":"Tuxedo tpcall 호출","v":985,"u":"곳"},{"k":"전역 트랜잭션 tpbegin","v":1234,"u":"곳"},{"k":"UBF 버퍼 필드접근","v":47295,"u":"곳"},{"k":"DB 테이블","v":101,"u":"개"},{"k":"카피북 헤더(.h)","v":2223,"u":"개"}],"HAZARDS":[{"t":"Tuxedo ATMI (tpcall/tpservice/tpreturn)","n":985,"u":"호출","why":"Java·Spring에 1:1 대응 API가 없다. 서비스 호출 모델 자체를 REST/메시지로 재설계해야 하며, 문법 치환으로는 불가능."},{"t":"XA 2PC 전역 트랜잭션 (최대 6단 중첩)","n":1234,"u":"tpbegin","why":"형제 XA 브랜치 격리·2단계 커밋 의미론이 Spring @Transactional 경계와 다르다. 잘못 옮기면 교착·이중전기가 그대로 재현된다."},{"t":"ECPG 임베디드 SQL (호스트변수·커서·sqlca)","n":23879,"u":"블록","why":"EXEC SQL 블록마다 호스트변수 바인딩·sqlcode 분기·커서 수명을 JPA/MyBatis 의미로 옮겨야 한다. 자동 변환기가 가장 많이 깨지는 지점."},{"t":"UBF(FML32) 타입리스 버퍼","n":47295,"u":"필드접근","why":"필드ID 기반 동적 버퍼라 타입·필수여부가 컴파일타임에 없다. Java DTO로 옮기려면 47,000여 접근점의 계약을 사람이 복원해야 한다."},{"t":"카피북 공유(#include) 팬아웃","n":6921,"u":"참조","why":"한 카피북을 수십 프로그램이 공유해 구조체 변경이 전방위로 파급된다. 경계 없는 결합이라 모듈 단위 절단이 어렵다."},{"t":"전처리 매크로(#define)","n":2267,"u":"정의","why":"매크로가 코드 생성·분기를 숨겨 정적 분석을 방해한다. 전개 후에야 실제 로직이 드러난다."}],"TRACE":[{"root":"ACQUIRE","tree":{"svc":"ACQUIRE","mod":"ac","wr":["cm_log"],"children":[{"svc":"RECONCILE","mod":"rc","wr":["cm_log","ledger"],"children":[{"svc":"SETTLE","mod":"st","wr":["ledger","settlement"],"children":[{"svc":"ST_MDR","mod":"st","wr":["ledger"],"children":[{"svc":"LG_SETTLEPOST","mod":"lg","wr":["cm_log"],"children":[{"svc":"LG_BALCHK","mod":"lg","wr":["cm_log"],"children":[]}]}]}]}]}]},"stat":{"programs":6,"tables":3,"xa_branches":6,"tablelist":["cm_log","ledger","settlement"]}},{"root":"PAY_APPROVE","tree":{"svc":"PAY_APPROVE","mod":"py","wr":["cm_log","py_payout"],"children":[{"svc":"PAY_FILEGEN","mod":"py","wr":["cm_log","py_file","py_payout"],"children":[{"svc":"PAY_ACCTCHK","mod":"py","wr":["cm_log"],"children":[{"svc":"LG_POST","mod":"lg","wr":["cm_log"],"children":[{"svc":"LG_BALCHK","mod":"lg","wr":["cm_log"],"children":[]}]}]}]}]},"stat":{"programs":5,"tables":3,"xa_branches":5,"tablelist":["cm_log","py_file","py_payout"]}},{"root":"CL_DAILY","tree":{"svc":"CL_DAILY","mod":"cl","wr":["cm_log"],"children":[{"svc":"VL_SALES","mod":"vl","wr":[],"children":[{"svc":"RC_EXCEPTION","mod":"rc","wr":["cm_log"],"children":[{"svc":"CM_LOG","mod":"cm","wr":[],"children":[]}]}]},{"svc":"LG_CLOSE_LINK","mod":"lg","wr":["cm_log","lg_ledger_line"],"children":[{"svc":"CL_LOCK","mod":"cl","wr":["SET","cl_close_log","cl_period_lock","cm_log"],"children":[]}]}]},"stat":{"programs":6,"tables":5,"xa_branches":6,"tablelist":["SET","cl_close_log","cl_period_lock","cm_log","lg_ledger_line"]}}],"LEGACY":{"copybook_total":2223,"copybook_shared_max":138,"copybook_shared_name":"acq_common.h","macro_defs":2267,"goto":64,"userlog":17267,"sqlca_checks":18877,"embedded_sql_density":9.8},"ORACLE":{"asis_files":9,"asis_proc":7,"asis_plsql":2,"asis_loc":1827,"dialects":[{"t":"PL/SQL 패키지 호출 (pkg_x.proc)","n":18},{"t":"NVL / NVL2 널가드","n":60},{"t":"DECODE 다분기","n":16},{"t":"SYSDATE / SYSTIMESTAMP","n":42},{"t":"FROM DUAL","n":15},{"t":"시퀀스 .NEXTVAL","n":27},{"t":"ROWNUM 의사컬럼","n":6},{"t":"구식 외부조인 (+)","n":8},{"t":"CONNECT BY 계층질의","n":3},{"t":"WHENEVER SQLERROR 라벨분기","n":16},{"t":"TO_DATE / TO_CHAR 포맷","n":35},{"t":"RAISE_APPLICATION_ERROR","n":13}]}};