업무화면 포털 + HTTP→tpcall 게이트웨이 (검증용 화면 계층)
- app/src/clients/acq_httpgw.c: C 경량 HTTP 게이트웨이(:8090, libatmiclt만 링크) /api/call?svc=..&FLD=.. → UBF(CBchg) → (옵션 XA tpbegin/tpcommit) tpcall → JSON 레거시 화면(Xplatform)→Webtier→Tuxedo 구조의 Webtier 대응 - app/ui/index.html: Xplatform풍 한국어 업무포털 (좌측 메뉴트리·폼·결과그리드·거래저널) 화면 9종: 매입접수(XA체인)/상태조회/가맹점집계/한도조회/MDR/잔액검증/일마감/가맹점/Luhn + 333 서비스 자유호출 화면 - entrypoint: 게이트웨이 자동 기동, compose: 8090 포트 노출 - 검증: HTML 서빙, ACQUIRE 체인 HTTP 호출 → XA 커밋(purchase status=S), Luhn T_RC=0, pg_prepared_xacts=0 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7139fdd2fd
commit
5c819239ad
4 changed files with 489 additions and 0 deletions
|
|
@ -39,9 +39,15 @@ xadmin psc
|
|||
echo "--- xadmin ppm (processes) ---"
|
||||
xadmin ppm
|
||||
|
||||
echo "--- acq_httpgw (업무화면 HTTP 게이트웨이 :8090 — 브라우저 → tpcall) ---"
|
||||
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 "=================================================================="
|
||||
echo " acquire-core-x : UP. Run the 매입 chain driver with:"
|
||||
echo " docker compose -f docker/docker-compose.yml exec app /app/run-driver.sh"
|
||||
echo " 업무화면(포털): http://localhost:8090/"
|
||||
echo "=================================================================="
|
||||
|
||||
# keep the container (and the ndrxd daemon) alive
|
||||
|
|
|
|||
254
app/src/clients/acq_httpgw.c
Normal file
254
app/src/clients/acq_httpgw.c
Normal file
|
|
@ -0,0 +1,254 @@
|
|||
/*
|
||||
* acq_httpgw.c - 경량 HTTP -> tpcall 게이트웨이 (레거시 Webtier 포지션).
|
||||
*
|
||||
* 브라우저 업무화면(app/ui)과 Enduro/X TP 모니터 사이의 브릿지:
|
||||
* GET /api/call?svc=ACQUIRE&_tx=1&T_MERCHANT=M0001&T_AMOUNT=1000000
|
||||
* -> UBF 버퍼 구성(CBchg, 문자열->필드타입 자동변환)
|
||||
* -> (옵션 _tx=1: tpbegin 글로벌 XA) tpcall(svc) (tpcommit/tpabort)
|
||||
* -> 응답 UBF 전 필드를 JSON 으로 직렬화해 반환
|
||||
* GET / -> /app/ui/index.html (업무화면)
|
||||
* GET /ui/<file> -> 정적 파일
|
||||
*
|
||||
* 의존성: libatmiclt + libubf 뿐 (외부 HTTP 라이브러리 없음, 단일 스레드 accept 루프).
|
||||
* 실제 레거시의 화면(Xplatform)->Webtier/WebLogic->Tuxedo 구조에서 Webtier 대응.
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <signal.h>
|
||||
#include <sys/socket.h>
|
||||
#include <netinet/in.h>
|
||||
#include <netinet/tcp.h>
|
||||
|
||||
#include <atmi.h>
|
||||
#include <ubf.h>
|
||||
#include <userlog.h>
|
||||
#include <acq.fd.h>
|
||||
|
||||
#define GW_PORT 8090
|
||||
#define REQ_MAX 16384
|
||||
#define VAL_MAX 2048
|
||||
|
||||
/* ---- acq.fd 필드 이름 <-> ID 정적 매핑 (필드테이블 env 불요) ---------------- */
|
||||
typedef struct { const char *name; BFLDID id; } fld_map_t;
|
||||
static const fld_map_t FMAP[] = {
|
||||
{"T_MERCHANT", T_MERCHANT}, {"T_AMOUNT", T_AMOUNT}, {"T_FEE", T_FEE},
|
||||
{"T_NET", T_NET}, {"T_STATUS", T_STATUS}, {"T_PURCHASE_ID", T_PURCHASE_ID},
|
||||
{"T_BIZDATE", T_BIZDATE}, {"T_SETTLE_ID", T_SETTLE_ID}, {"T_MSG", T_MSG},
|
||||
{"T_CANCEL_ID", T_CANCEL_ID}, {"T_INSTALL_N", T_INSTALL_N}, {"T_TAX", T_TAX},
|
||||
{"T_FX_AMT", T_FX_AMT}, {"T_COUNT", T_COUNT}, {"T_REASON", T_REASON},
|
||||
{"T_CATEGORY", T_CATEGORY}, {"T_ISSUER", T_ISSUER}, {"T_CHANNEL", T_CHANNEL},
|
||||
{"T_CCY", T_CCY}, {"T_ADJ", T_ADJ}, {"T_DELTA", T_DELTA},
|
||||
{"T_SVCNAME", T_SVCNAME}, {"T_RC", T_RC}, {"T_GROSS", T_GROSS},
|
||||
{"T_DOCNO", T_DOCNO},
|
||||
{"T_ARG1", T_ARG1}, {"T_ARG2", T_ARG2}, {"T_ARG3", T_ARG3}, {"T_ARG4", T_ARG4},
|
||||
{"T_ARG5", T_ARG5}, {"T_ARG6", T_ARG6}, {"T_ARG7", T_ARG7}, {"T_ARG8", T_ARG8},
|
||||
{"T_STR1", T_STR1}, {"T_STR2", T_STR2}, {"T_STR3", T_STR3}, {"T_STR4", T_STR4},
|
||||
{"T_STR5", T_STR5}, {"T_STR6", T_STR6}, {"T_STR7", T_STR7}, {"T_STR8", T_STR8},
|
||||
{"T_AMT1", T_AMT1}, {"T_AMT2", T_AMT2}, {"T_AMT3", T_AMT3}, {"T_AMT4", T_AMT4},
|
||||
{"T_KEY1", T_KEY1}, {"T_KEY2", T_KEY2}, {"T_ID1", T_ID1}, {"T_ID2", T_ID2},
|
||||
};
|
||||
#define NFLD (sizeof(FMAP)/sizeof(FMAP[0]))
|
||||
|
||||
static BFLDID fld_by_name(const char *n)
|
||||
{
|
||||
size_t i;
|
||||
for (i = 0; i < NFLD; i++)
|
||||
if (0 == strcmp(FMAP[i].name, n)) return FMAP[i].id;
|
||||
return BBADFLDID;
|
||||
}
|
||||
static const char *fld_name(BFLDID id)
|
||||
{
|
||||
size_t i;
|
||||
for (i = 0; i < NFLD; i++)
|
||||
if (FMAP[i].id == id) return FMAP[i].name;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* ---- 유틸 ------------------------------------------------------------------ */
|
||||
static void url_decode(char *s)
|
||||
{
|
||||
char *o = s;
|
||||
while (*s) {
|
||||
if (*s == '+') { *o++ = ' '; s++; }
|
||||
else if (*s == '%' && s[1] && s[2]) {
|
||||
int hi = (s[1] >= 'a') ? s[1]-'a'+10 : (s[1] >= 'A') ? s[1]-'A'+10 : s[1]-'0';
|
||||
int lo = (s[2] >= 'a') ? s[2]-'a'+10 : (s[2] >= 'A') ? s[2]-'A'+10 : s[2]-'0';
|
||||
*o++ = (char)(hi*16+lo); s += 3;
|
||||
} else *o++ = *s++;
|
||||
}
|
||||
*o = '\0';
|
||||
}
|
||||
|
||||
static void json_escape(const char *in, char *out, size_t cap)
|
||||
{
|
||||
size_t o = 0;
|
||||
for (; *in && o + 6 < cap; in++) {
|
||||
unsigned char c = (unsigned char)*in;
|
||||
if (c == '"' || c == '\\') { out[o++]='\\'; out[o++]=c; }
|
||||
else if (c == '\n') { out[o++]='\\'; out[o++]='n'; }
|
||||
else if (c == '\r') { out[o++]='\\'; out[o++]='r'; }
|
||||
else if (c == '\t') { out[o++]='\\'; out[o++]='t'; }
|
||||
else if (c < 0x20) { o += snprintf(out+o, cap-o, "\\u%04x", c); }
|
||||
else out[o++] = c;
|
||||
}
|
||||
out[o] = '\0';
|
||||
}
|
||||
|
||||
static void http_send(int fd, int code, const char *ctype, const char *body, long blen)
|
||||
{
|
||||
char hdr[512];
|
||||
const char *msg = (code == 200) ? "OK" : (code == 404) ? "Not Found" : "Bad Request";
|
||||
int hl = snprintf(hdr, sizeof(hdr),
|
||||
"HTTP/1.1 %d %s\r\nContent-Type: %s\r\nContent-Length: %ld\r\n"
|
||||
"Cache-Control: no-store\r\nConnection: close\r\n\r\n", code, msg, ctype, blen);
|
||||
if (write(fd, hdr, hl) < 0) return;
|
||||
if (blen > 0 && write(fd, body, blen) < 0) return;
|
||||
}
|
||||
|
||||
/* ---- /api/call ------------------------------------------------------------- */
|
||||
static void handle_api_call(int fd, char *query)
|
||||
{
|
||||
char svc[64] = "";
|
||||
int use_tx = 0, in_tx = 0;
|
||||
UBFH *b = NULL;
|
||||
static char out[262144];
|
||||
long olen = 0;
|
||||
char val[VAL_MAX], esc[VAL_MAX*2];
|
||||
char *tok, *save = NULL;
|
||||
|
||||
b = (UBFH *)tpalloc("UBF", NULL, 16384);
|
||||
if (!b) { http_send(fd, 500, "application/json", "{\"ok\":false,\"error\":\"tpalloc\"}", 30); return; }
|
||||
|
||||
/* 쿼리 파라미터 -> UBF (CBchg: 문자열 -> 필드 타입 자동 변환) */
|
||||
for (tok = strtok_r(query, "&", &save); tok; tok = strtok_r(NULL, "&", &save)) {
|
||||
char *eq = strchr(tok, '=');
|
||||
if (!eq) continue;
|
||||
*eq = '\0';
|
||||
strncpy(val, eq + 1, sizeof(val) - 1); val[sizeof(val)-1] = '\0';
|
||||
url_decode(tok); url_decode(val);
|
||||
if (0 == strcmp(tok, "svc")) { strncpy(svc, val, sizeof(svc)-1); continue; }
|
||||
if (0 == strcmp(tok, "_tx")) { use_tx = atoi(val); continue; }
|
||||
BFLDID id = fld_by_name(tok);
|
||||
if (id == BBADFLDID) continue;
|
||||
BFLDOCC occ = Boccur(b, id); /* 같은 필드 반복 = 다음 occurrence */
|
||||
if (CBchg(b, id, occ, val, 0, BFLD_STRING) < 0)
|
||||
userlog("acq_httpgw: CBchg(%s) 실패: %s", tok, Bstrerror(Berror));
|
||||
}
|
||||
if (!svc[0]) {
|
||||
const char *e = "{\"ok\":false,\"error\":\"svc required\"}";
|
||||
tpfree((char *)b); http_send(fd, 400, "application/json", e, (long)strlen(e)); return;
|
||||
}
|
||||
|
||||
if (use_tx) {
|
||||
if (tpbegin(60, 0) < 0) {
|
||||
snprintf(out, sizeof(out), "{\"ok\":false,\"svc\":\"%s\",\"error\":\"tpbegin: %s\"}", svc, tpstrerror(tperrno));
|
||||
tpfree((char *)b); http_send(fd, 500, "application/json", out, (long)strlen(out)); return;
|
||||
}
|
||||
in_tx = 1;
|
||||
}
|
||||
|
||||
long rlen = 0;
|
||||
int rc = tpcall(svc, (char *)b, 0L, (char **)&b, &rlen, 0L);
|
||||
|
||||
if (in_tx) {
|
||||
if (rc >= 0) { if (tpcommit(0) < 0) rc = -1; }
|
||||
else tpabort(0);
|
||||
}
|
||||
|
||||
if (rc < 0) {
|
||||
json_escape(tpstrerror(tperrno), esc, sizeof(esc));
|
||||
olen = snprintf(out, sizeof(out), "{\"ok\":false,\"svc\":\"%s\",\"error\":\"%s\"}", svc, esc);
|
||||
http_send(fd, 200, "application/json", out, olen);
|
||||
tpfree((char *)b);
|
||||
return;
|
||||
}
|
||||
|
||||
/* 응답 UBF -> JSON */
|
||||
olen = snprintf(out, sizeof(out), "{\"ok\":true,\"svc\":\"%s\",\"tx\":%d,\"fields\":{", svc, use_tx);
|
||||
BFLDID id = BFIRSTFLDID; BFLDOCC occ;
|
||||
int first = 1;
|
||||
while (1 == Bnext(b, &id, &occ, NULL, NULL)) {
|
||||
BFLDLEN vl = sizeof(val);
|
||||
if (CBget(b, id, occ, val, &vl, BFLD_STRING) < 0) continue;
|
||||
json_escape(val, esc, sizeof(esc));
|
||||
const char *nm = fld_name(id);
|
||||
char nbuf[32];
|
||||
if (!nm) { snprintf(nbuf, sizeof(nbuf), "F_%d", (int)id); nm = nbuf; }
|
||||
olen += snprintf(out + olen, sizeof(out) - olen, "%s\"%s%s\":\"%s\"",
|
||||
first ? "" : ",", nm, occ > 0 ? "#" : "", esc);
|
||||
first = 0;
|
||||
if ((size_t)olen > sizeof(out) - 4096) break;
|
||||
}
|
||||
olen += snprintf(out + olen, sizeof(out) - olen, "}}");
|
||||
http_send(fd, 200, "application/json", out, olen);
|
||||
tpfree((char *)b);
|
||||
}
|
||||
|
||||
/* ---- 정적 파일 ------------------------------------------------------------- */
|
||||
static void handle_static(int fd, const char *path)
|
||||
{
|
||||
char full[512];
|
||||
if (strstr(path, "..")) { http_send(fd, 400, "text/plain", "bad path", 8); return; }
|
||||
if (0 == strcmp(path, "/") || 0 == strcmp(path, "/index.html"))
|
||||
snprintf(full, sizeof(full), "/app/ui/index.html");
|
||||
else if (0 == strncmp(path, "/ui/", 4))
|
||||
snprintf(full, sizeof(full), "/app/ui/%s", path + 4);
|
||||
else { http_send(fd, 404, "text/plain", "not found", 9); return; }
|
||||
|
||||
FILE *fp = fopen(full, "rb");
|
||||
if (!fp) { http_send(fd, 404, "text/plain", "not found", 9); return; }
|
||||
fseek(fp, 0, SEEK_END); long sz = ftell(fp); fseek(fp, 0, SEEK_SET);
|
||||
char *buf = malloc(sz > 0 ? sz : 1);
|
||||
if (fread(buf, 1, sz, fp) != (size_t)sz) { fclose(fp); free(buf); http_send(fd, 500, "text/plain", "read", 4); return; }
|
||||
fclose(fp);
|
||||
const char *ct = strstr(full, ".css") ? "text/css" :
|
||||
strstr(full, ".js") ? "application/javascript" :
|
||||
"text/html; charset=utf-8";
|
||||
http_send(fd, 200, ct, buf, sz);
|
||||
free(buf);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
signal(SIGPIPE, SIG_IGN);
|
||||
|
||||
if (tpinit(NULL) < 0) { fprintf(stderr, "tpinit FAIL: %s\n", tpstrerror(tperrno)); return 1; }
|
||||
if (tpopen() < 0) { fprintf(stderr, "tpopen FAIL: %s\n", tpstrerror(tperrno)); return 1; }
|
||||
|
||||
int srv = socket(AF_INET, SOCK_STREAM, 0);
|
||||
int on = 1;
|
||||
setsockopt(srv, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
|
||||
struct sockaddr_in a; memset(&a, 0, sizeof(a));
|
||||
a.sin_family = AF_INET; a.sin_addr.s_addr = htonl(INADDR_ANY); a.sin_port = htons(GW_PORT);
|
||||
if (bind(srv, (struct sockaddr *)&a, sizeof(a)) < 0) { perror("bind"); return 1; }
|
||||
if (listen(srv, 16) < 0) { perror("listen"); return 1; }
|
||||
userlog("acq_httpgw: 업무화면 게이트웨이 기동 :%d (HTTP -> tpcall)", GW_PORT);
|
||||
fprintf(stderr, "acq_httpgw listening on :%d\n", GW_PORT);
|
||||
|
||||
while (1) {
|
||||
int fd = accept(srv, NULL, NULL);
|
||||
if (fd < 0) continue;
|
||||
static char req[REQ_MAX];
|
||||
ssize_t n = read(fd, req, sizeof(req) - 1);
|
||||
if (n <= 0) { close(fd); continue; }
|
||||
req[n] = '\0';
|
||||
|
||||
/* "GET /path?query HTTP/1.1" 만 처리 */
|
||||
char *sp1 = strchr(req, ' ');
|
||||
char *sp2 = sp1 ? strchr(sp1 + 1, ' ') : NULL;
|
||||
if (!sp1 || !sp2 || strncmp(req, "GET ", 4) != 0) { http_send(fd, 400, "text/plain", "bad req", 7); close(fd); continue; }
|
||||
*sp2 = '\0';
|
||||
char *path = sp1 + 1;
|
||||
char *query = strchr(path, '?');
|
||||
if (query) *query++ = '\0';
|
||||
|
||||
if (0 == strcmp(path, "/api/call") && query)
|
||||
handle_api_call(fd, query);
|
||||
else
|
||||
handle_static(fd, path);
|
||||
close(fd);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
227
app/ui/index.html
Normal file
227
app/ui/index.html
Normal file
|
|
@ -0,0 +1,227 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>ACQUIRE-CORE-X 매입업무포털</title>
|
||||
<style>
|
||||
/* Xplatform(UXStudio) 풍 내부 업무화면 스타일 */
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
html, body { height: 100%; font-family: "Malgun Gothic", "맑은 고딕", Dotum, sans-serif; font-size: 12px; color: #222; }
|
||||
body { display: flex; flex-direction: column; background: #e8ebef; }
|
||||
|
||||
/* 상단 타이틀바 */
|
||||
#titlebar { height: 44px; background: linear-gradient(#2c5d8f, #1d4269); color: #fff; display: flex; align-items: center; padding: 0 14px; }
|
||||
#titlebar .logo { font-size: 16px; font-weight: bold; letter-spacing: 1px; }
|
||||
#titlebar .sub { margin-left: 12px; font-size: 11px; color: #bcd3e8; }
|
||||
#titlebar .right { margin-left: auto; font-size: 11px; color: #d6e4f2; }
|
||||
#titlebar .right b { color: #ffd27f; }
|
||||
|
||||
#main { flex: 1; display: flex; min-height: 0; }
|
||||
|
||||
/* 좌측 메뉴 트리 */
|
||||
#menu { width: 208px; background: #f4f6f8; border-right: 1px solid #b8c2cc; overflow-y: auto; }
|
||||
#menu .grp { padding: 7px 10px; background: #dbe3ea; border-bottom: 1px solid #c3cdd6; font-weight: bold; color: #2c4a66; cursor: default; }
|
||||
#menu .itm { padding: 6px 10px 6px 22px; border-bottom: 1px solid #e6eaee; cursor: pointer; color: #333; }
|
||||
#menu .itm:hover { background: #e8f0f8; }
|
||||
#menu .itm.on { background: #cfe0f0; color: #14487c; font-weight: bold; }
|
||||
#menu .itm .cd { color: #888; font-size: 10px; margin-right: 5px; }
|
||||
|
||||
/* 컨텐츠 */
|
||||
#content { flex: 1; display: flex; flex-direction: column; min-width: 0; }
|
||||
#tabbar { height: 30px; background: #d5dce3; border-bottom: 1px solid #a9b4bf; display: flex; align-items: flex-end; padding: 0 8px; }
|
||||
#tabbar .tab { padding: 5px 16px 4px; background: #fff; border: 1px solid #a9b4bf; border-bottom: none; font-weight: bold; color: #1d4269; }
|
||||
#work { flex: 1; overflow: auto; padding: 12px; }
|
||||
|
||||
.panel { background: #fff; border: 1px solid #b0bac4; margin-bottom: 12px; }
|
||||
.panel h3 { font-size: 12px; padding: 6px 10px; background: #eef2f6; border-bottom: 1px solid #ccd4dc;
|
||||
color: #1d4269; display: flex; align-items: center; }
|
||||
.panel h3::before { content: ""; width: 4px; height: 12px; background: #2c5d8f; margin-right: 6px; }
|
||||
.panel .bd { padding: 10px; }
|
||||
|
||||
/* 폼 */
|
||||
table.frm { border-collapse: collapse; width: 100%; }
|
||||
table.frm th { background: #f0f3f6; border: 1px solid #d3dae1; padding: 5px 8px; text-align: right; width: 130px; font-weight: normal; color: #445; }
|
||||
table.frm td { border: 1px solid #d3dae1; padding: 3px 6px; }
|
||||
table.frm input { width: 100%; max-width: 260px; height: 22px; border: 1px solid #9aa8b5; padding: 0 6px; font-size: 12px; }
|
||||
.btnrow { margin-top: 10px; text-align: right; }
|
||||
button { height: 26px; padding: 0 16px; margin-left: 6px; background: linear-gradient(#fdfdfd, #dfe5ea); border: 1px solid #8fa0b0; cursor: pointer; font-size: 12px; }
|
||||
button.pri { background: linear-gradient(#3f76ad, #29578a); color: #fff; border-color: #1d4269; font-weight: bold; }
|
||||
button:hover { filter: brightness(1.06); }
|
||||
|
||||
/* 그리드 */
|
||||
table.grd { border-collapse: collapse; width: 100%; }
|
||||
table.grd th { background: #dfe6ec; border: 1px solid #b8c2cc; padding: 4px 6px; color: #2c4a66; }
|
||||
table.grd td { border: 1px solid #d3dae1; padding: 3px 6px; background: #fff; }
|
||||
table.grd tr:nth-child(even) td { background: #f7fafc; }
|
||||
.num { text-align: right; font-family: Consolas, monospace; }
|
||||
.ok { color: #0a7d2c; font-weight: bold; }
|
||||
.err { color: #c22; font-weight: bold; }
|
||||
|
||||
#statusbar { height: 24px; background: #d5dce3; border-top: 1px solid #a9b4bf; display: flex; align-items: center;
|
||||
padding: 0 10px; font-size: 11px; color: #345; }
|
||||
#statusbar .dot { width: 8px; height: 8px; border-radius: 50%; background: #23a54a; margin-right: 6px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="titlebar">
|
||||
<span class="logo">ACQUIRE-CORE-X</span>
|
||||
<span class="sub">글로벌 매입업무포털 (Enduro/X · ECPG/PostgreSQL · XA)</span>
|
||||
<span class="right">사용자: <b>매입운영1팀</b> | 영업일 <span id="bizdate"></span> | 화면번호 <span id="scrno">AC0101</span></span>
|
||||
</div>
|
||||
|
||||
<div id="main">
|
||||
<div id="menu"></div>
|
||||
<div id="content">
|
||||
<div id="tabbar"><span class="tab" id="tabname">매입 접수</span></div>
|
||||
<div id="work"></div>
|
||||
<div id="statusbar"><span class="dot"></span><span id="stmsg">TP 모니터 연결됨 — 게이트웨이 /api/call</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/* ───────── 화면 정의: 실제 tpservice 와 1:1 매핑 ───────── */
|
||||
const today = new Date().toISOString().slice(0,10).replace(/-/g,'');
|
||||
document.getElementById('bizdate').textContent = today;
|
||||
|
||||
const SCREENS = [
|
||||
{ grp: "매입 (ac)", items: [
|
||||
{ cd:"AC0101", nm:"매입 접수(승인체인)", svc:"ACQUIRE", tx:1,
|
||||
note:"글로벌 XA: ACQUIRE→RECONCILE→SETTLE 체인 후 tpcommit(2PC)",
|
||||
flds:[ ["T_MERCHANT","가맹점번호","M0001"], ["T_AMOUNT","매입금액(원)","1000000"], ["T_BIZDATE","영업일자",today] ] },
|
||||
{ cd:"AC0201", nm:"매입 상태조회", svc:"ACQ_STATUS", tx:0,
|
||||
flds:[ ["T_PURCHASE_ID","매입번호","1"] ] },
|
||||
{ cd:"AC0301", nm:"가맹점 매입집계", svc:"ACQ_MERCHSUM", tx:0,
|
||||
flds:[ ["T_MERCHANT","가맹점번호","M0001"], ["T_BIZDATE","영업일자",today] ] },
|
||||
]},
|
||||
{ grp: "승인/한도 (au)", items: [
|
||||
{ cd:"AU0101", nm:"한도 조회", svc:"LIMIT_INQ", tx:0,
|
||||
flds:[ ["T_KEY1","카드번호","4111111111111111"] ] },
|
||||
]},
|
||||
{ grp: "정산/수수료 (st)", items: [
|
||||
{ cd:"ST0101", nm:"MDR 수수료 산출", svc:"ST_MDR", tx:0,
|
||||
flds:[ ["T_PURCHASE_ID","매입번호","1"], ["T_FEE","기준수수료","0"] ] },
|
||||
]},
|
||||
{ grp: "원장 (lg)", items: [
|
||||
{ cd:"LG0101", nm:"원장 잔액검증", svc:"LG_BALCHK", tx:0,
|
||||
flds:[ ["T_ID1","전표번호","1"] ] },
|
||||
]},
|
||||
{ grp: "마감 (cl)", items: [
|
||||
{ cd:"CL0101", nm:"일마감 처리", svc:"CL_DAILY", tx:1,
|
||||
flds:[ ["T_BIZDATE","마감영업일",today] ] },
|
||||
]},
|
||||
{ grp: "마스터 (mm)", items: [
|
||||
{ cd:"MM0101", nm:"가맹점 조회", svc:"MM_MERCH_INQ", tx:0,
|
||||
flds:[ ["T_MERCHANT","가맹점번호","M0001"] ] },
|
||||
]},
|
||||
{ grp: "공통 (cm)", items: [
|
||||
{ cd:"CM0101", nm:"카드번호 검증(Luhn)", svc:"CM_LUHN", tx:0,
|
||||
flds:[ ["T_STR5","카드번호","4111111111111111"] ] },
|
||||
]},
|
||||
{ grp: "자유 호출", items: [
|
||||
{ cd:"ZZ0101", nm:"서비스 직접호출(333종)", svc:"", tx:0, free:true,
|
||||
note:"advertise 된 333 개 tpservice 아무거나 호출 (필드 직접 지정)",
|
||||
flds:[ ["T_MERCHANT","","M0001"] ] },
|
||||
]},
|
||||
];
|
||||
|
||||
/* ───────── 메뉴 렌더 ───────── */
|
||||
const menuEl = document.getElementById('menu');
|
||||
let CUR = null;
|
||||
SCREENS.forEach(g => {
|
||||
const gd = document.createElement('div'); gd.className='grp'; gd.textContent = g.grp; menuEl.appendChild(gd);
|
||||
g.items.forEach(s => {
|
||||
const it = document.createElement('div'); it.className='itm';
|
||||
it.innerHTML = `<span class="cd">${s.cd}</span>${s.nm}`;
|
||||
it.onclick = () => openScreen(s, it);
|
||||
menuEl.appendChild(it);
|
||||
if (s.cd === 'AC0101') setTimeout(()=>openScreen(s, it), 0);
|
||||
});
|
||||
});
|
||||
|
||||
/* ───────── 화면 렌더 ───────── */
|
||||
const work = document.getElementById('work');
|
||||
const journal = []; /* 호출이력 */
|
||||
|
||||
function openScreen(s, itEl) {
|
||||
CUR = s;
|
||||
document.querySelectorAll('#menu .itm').forEach(e=>e.classList.remove('on'));
|
||||
if (itEl) itEl.classList.add('on');
|
||||
document.getElementById('tabname').textContent = s.nm;
|
||||
document.getElementById('scrno').textContent = s.cd;
|
||||
|
||||
let rows = s.flds.map(([f,label,dv],i) =>
|
||||
`<tr><th>${label||f}</th><td><input id="f_${i}" data-fld="${f}" value="${dv}">
|
||||
${s.free?`<input id="fn_${i}" data-name value="${f}" style="max-width:120px;margin-right:6px" placeholder="필드명">`:''}
|
||||
</td></tr>`).join('');
|
||||
const svcRow = s.free
|
||||
? `<tr><th>서비스명</th><td><input id="svcname" value="ACQ_DUPCHK" style="max-width:200px"></td></tr>`
|
||||
: `<tr><th>서비스명</th><td><b>${s.svc}</b>${s.tx?` <span style="color:#a60">[글로벌 XA 트랜잭션]</span>`:''}</td></tr>`;
|
||||
|
||||
work.innerHTML = `
|
||||
<div class="panel"><h3>${s.cd} · ${s.nm}${s.note?` — <span style="font-weight:normal;color:#667">${s.note}</span>`:''}</h3>
|
||||
<div class="bd">
|
||||
<table class="frm">${svcRow}${rows}</table>
|
||||
<div class="btnrow">
|
||||
<button onclick="resetForm()">초기화</button>
|
||||
<button class="pri" onclick="execute()">실행 (tpcall)</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel"><h3>처리 결과 (응답 전문 UBF)</h3>
|
||||
<div class="bd"><table class="grd" id="resgrid"><tr><th style="width:180px">필드</th><th>값</th></tr>
|
||||
<tr><td colspan="2" style="color:#999">실행 전</td></tr></table></div>
|
||||
</div>
|
||||
<div class="panel"><h3>거래 저널 (금일 호출이력)</h3>
|
||||
<div class="bd"><table class="grd" id="jgrid">
|
||||
<tr><th>시각</th><th>화면</th><th>서비스</th><th>XA</th><th>결과</th><th>요약</th></tr></table></div>
|
||||
</div>`;
|
||||
renderJournal();
|
||||
}
|
||||
|
||||
function resetForm(){ openScreen(CUR, document.querySelector('#menu .itm.on')); }
|
||||
|
||||
async function execute() {
|
||||
const s = CUR;
|
||||
const svc = s.free ? document.getElementById('svcname').value.trim() : s.svc;
|
||||
const p = new URLSearchParams({ svc, _tx: s.tx || 0 });
|
||||
s.flds.forEach((_,i) => {
|
||||
const fld = s.free ? document.getElementById('fn_'+i).value.trim() : document.getElementById('f_'+i).dataset.fld;
|
||||
const v = document.getElementById('f_'+i).value;
|
||||
if (fld && v !== '') p.append(fld, v);
|
||||
});
|
||||
setStatus(`tpcall("${svc}") 호출 중 ...`);
|
||||
let j;
|
||||
try {
|
||||
const r = await fetch('/api/call?' + p.toString());
|
||||
j = await r.json();
|
||||
} catch (e) { j = { ok:false, error: String(e) }; }
|
||||
|
||||
const g = document.getElementById('resgrid');
|
||||
g.innerHTML = '<tr><th style="width:180px">필드</th><th>값</th></tr>';
|
||||
if (j.ok) {
|
||||
const f = j.fields || {};
|
||||
if (!Object.keys(f).length) g.innerHTML += '<tr><td colspan="2">(응답 필드 없음 — 정상 처리)</td></tr>';
|
||||
for (const [k,v] of Object.entries(f))
|
||||
g.innerHTML += `<tr><td>${k}</td><td class="num">${fmt(k,v)}</td></tr>`;
|
||||
setStatus(`tpcall("${svc}") 정상 처리${s.tx?' · XA tpcommit 완료(2PC)':''}`);
|
||||
} else {
|
||||
g.innerHTML += `<tr><td>오류</td><td class="err">${j.error||'unknown'}</td></tr>`;
|
||||
setStatus(`tpcall("${svc}") 실패`, true);
|
||||
}
|
||||
journal.unshift({ t:new Date().toLocaleTimeString('ko-KR'), cd:s.cd, svc, tx:s.tx?'Y':'N',
|
||||
ok:j.ok, sum: j.ok ? summarize(j.fields) : (j.error||'').slice(0,60) });
|
||||
renderJournal();
|
||||
}
|
||||
|
||||
function fmt(k, v){ return /AMOUNT|FEE|NET|GROSS|AMT|TAX|DELTA/.test(k) && /^\-?\d+$/.test(v) ? Number(v).toLocaleString('ko-KR') : v; }
|
||||
function summarize(f){ if(!f) return ''; return Object.entries(f).slice(0,4).map(([k,v])=>`${k}=${v}`).join(' '); }
|
||||
function renderJournal(){
|
||||
const g = document.getElementById('jgrid'); if (!g) return;
|
||||
g.innerHTML = '<tr><th>시각</th><th>화면</th><th>서비스</th><th>XA</th><th>결과</th><th>요약</th></tr>' +
|
||||
journal.map(r=>`<tr><td>${r.t}</td><td>${r.cd}</td><td>${r.svc}</td><td>${r.tx}</td>
|
||||
<td class="${r.ok?'ok':'err'}">${r.ok?'정상':'오류'}</td><td>${r.sum}</td></tr>`).join('');
|
||||
}
|
||||
function setStatus(m, err){ const e=document.getElementById('stmsg'); e.textContent=m; e.style.color=err?'#c22':'#345'; }
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -33,6 +33,8 @@ services:
|
|||
ulimits:
|
||||
msgqueue: 2147483648
|
||||
nofile: 65536
|
||||
ports:
|
||||
- "8090:8090" # 업무화면 게이트웨이 (브라우저 → tpcall)
|
||||
volumes:
|
||||
- ../app:/app
|
||||
working_dir: /app
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue