diff --git a/app/entrypoint.sh b/app/entrypoint.sh index f35cd03..fc81445 100755 --- a/app/entrypoint.sh +++ b/app/entrypoint.sh @@ -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 diff --git a/app/src/clients/acq_httpgw.c b/app/src/clients/acq_httpgw.c new file mode 100644 index 0000000..3c72d6b --- /dev/null +++ b/app/src/clients/acq_httpgw.c @@ -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/ -> 정적 파일 + * + * 의존성: libatmiclt + libubf 뿐 (외부 HTTP 라이브러리 없음, 단일 스레드 accept 루프). + * 실제 레거시의 화면(Xplatform)->Webtier/WebLogic->Tuxedo 구조에서 Webtier 대응. + */ +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#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; +} diff --git a/app/ui/index.html b/app/ui/index.html new file mode 100644 index 0000000..d0096be --- /dev/null +++ b/app/ui/index.html @@ -0,0 +1,227 @@ + + + + +ACQUIRE-CORE-X 매입업무포털 + + + +
+ + 글로벌 매입업무포털 (Enduro/X · ECPG/PostgreSQL · XA) + 사용자: 매입운영1팀 | 영업일 | 화면번호 AC0101 +
+ +
+ +
+
매입 접수
+
+
TP 모니터 연결됨 — 게이트웨이 /api/call
+
+
+ + + + diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 01656bb..3900b88 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -33,6 +33,8 @@ services: ulimits: msgqueue: 2147483648 nofile: 65536 + ports: + - "8090:8090" # 업무화면 게이트웨이 (브라우저 → tpcall) volumes: - ../app:/app working_dir: /app