프론트 전면 구성: 330개 전 서비스 화면 포털 (매니페스트 자동생성)
- tools/gen_screens.py: svc 소스에서 입력(getl/gets_)·출력(setl/sets) 필드와 한글 설명을 추출해 app/ui/screens.js(330 화면 매니페스트) 생성 - index.html 확장: 모듈별 메뉴트리(11그룹×30화면) + 화면검색 + 주요업무 즐겨찾기 9종, 화면별 입력폼 자동구성(한글라벨·기본값), 조회성 화면 XA 자동 off, 결과그리드·거래저널 - README: 화면 계층(4.0) 문서화 - 검증: screens.js 서빙, ACQ_DUPCHK(조회)·RC_STATS(통계)·MM_MERCH_REG(XA 등록) 실호출 정상 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
5c819239ad
commit
844a1d50fc
4 changed files with 4488 additions and 105 deletions
21
README.md
21
README.md
|
|
@ -84,13 +84,16 @@ acquire-core-x/
|
|||
│ ├─ conf/ # app.ini(CCONFIG), ndrxconfig.xml(생성), setapp.sh(XA env)
|
||||
│ ├─ ubftab/acq.fd # UBF 필드테이블 (전 모듈 공용)
|
||||
│ ├─ build.sh # 모듈 자동발견 빌드 + ndrxconfig 생성
|
||||
│ ├─ entrypoint.sh # 빌드 → ndrxd 기동
|
||||
│ ├─ entrypoint.sh # 빌드 → ndrxd 기동 (+ 업무화면 게이트웨이 :8090)
|
||||
│ ├─ ui/ # 업무화면 포털 (index.html + screens.js — 330 화면 매니페스트)
|
||||
│ └─ src/<mod>/ # 11개 모듈 (ac au rc st py lg cl mm vl mg cm)
|
||||
│ ├─ <mod>_svr.pgc # 얇은 모듈 서버(디스패처)
|
||||
│ ├─ svc/*.pgc + *.h # 1서비스=1파일 + 카피북 (모듈당 30)
|
||||
│ ├─ dbio/*.pgc + *.h # DB 접근 계층 (모듈당 ~40)
|
||||
│ ├─ batch/*.pgc # XA 배치 (모듈당 ~22)
|
||||
│ └─ run/*.sh # 운영 기동 스크립트 (모듈당 ~17)
|
||||
├─ tools/
|
||||
│ └─ gen_screens.py # svc 소스 → 화면 매니페스트(screens.js) 재생성기
|
||||
└─ docs/
|
||||
├─ architecture.md # 상세 아키텍처
|
||||
└─ service-catalog.md # 서비스 카탈로그
|
||||
|
|
@ -105,7 +108,20 @@ docker compose -f docker/docker-compose.yml up -d --build # postgres + endur
|
|||
```
|
||||
|
||||
빌드(전 모듈 ecpg→buildserver, ~240 배치 컴파일)는 수 분 걸린다. 완료되면 `ndrxd`가
|
||||
11개 모듈 서버 + `tmsrv`를 부팅한다.
|
||||
11개 모듈 서버 + `tmsrv`를 부팅하고, **업무화면 포털**이 http://localhost:8090/ 에 뜬다.
|
||||
|
||||
### 4.0 업무화면 (프론트 — RFI의 Xplatform 내부화면 포지션)
|
||||
|
||||
```
|
||||
브라우저 업무화면(app/ui, Xplatform풍) → acq_httpgw(C, Webtier 포지션) → tpcall → 모듈서버 → PostgreSQL
|
||||
```
|
||||
- `app/src/clients/acq_httpgw.c` — C 경량 HTTP 게이트웨이(:8090, libatmiclt만 링크).
|
||||
`GET /api/call?svc=<SVC>&_tx=1&T_필드=값...` → UBF 구성(CBchg) → (옵션) 글로벌 XA
|
||||
`tpbegin/tpcommit` → `tpcall` → 응답 UBF 전체를 JSON 직렬화.
|
||||
- `app/ui/index.html` + `screens.js` — 한국어 업무포털. **330개 전 서비스가 화면으로 노출**
|
||||
(모듈별 메뉴트리·검색·주요업무 즐겨찾기 9종). 각 화면의 입력폼은 해당 tpservice 소스에서
|
||||
추출한 실제 입력 필드(getl/gets_)로 자동 구성되며(`tools/gen_screens.py`), 실행 시 실제
|
||||
tpcall 이 돌고 결과 그리드·거래 저널에 표시된다.
|
||||
|
||||
### 4.1 필수 런타임 요건 (없으면 ndrxd 부팅 실패 — 실측으로 규명)
|
||||
|
||||
|
|
@ -218,4 +234,5 @@ $C exec db psql -U acq -d acq -c "select count(*) from pg_prepared_xacts;" # 0
|
|||
- [x] ECPG XA 서비스 체인(매입→대사→정산) + 커밋 row psql 검증 + 롤백(`prepared_xacts=0`)
|
||||
- [x] 2,000본+ 실동작 시스템 (2,052 파일 / `.pgc` 1,015본, 전 모듈 dbio·batch 세분화)
|
||||
- [x] 전 `.pgc` 정규화-고유 (클론 0)
|
||||
- [x] 업무화면 포털(:8090) — 330 화면, HTTP→tpcall 게이트웨이, 화면에서 XA 체인 실행 검증
|
||||
- [ ] Klaro Forge 마이그레이션 → Spring Boot (Phase 3, 예정)
|
||||
|
|
|
|||
|
|
@ -9,8 +9,7 @@
|
|||
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 { height: 44px; background: linear-gradient(#2c5d8f, #1d4269); color: #fff; display: flex; align-items: center; padding: 0 14px; flex-shrink: 0; }
|
||||
#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; }
|
||||
|
|
@ -19,36 +18,41 @@
|
|||
#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; }
|
||||
#menuwrap { width: 232px; background: #f4f6f8; border-right: 1px solid #b8c2cc; display: flex; flex-direction: column; }
|
||||
#menusearch { padding: 6px; border-bottom: 1px solid #ccd4dc; }
|
||||
#menusearch input { width: 100%; height: 24px; border: 1px solid #9aa8b5; padding: 0 8px; font-size: 12px; }
|
||||
#menu { flex: 1; overflow-y: auto; }
|
||||
#menu .grp { padding: 7px 10px; background: #dbe3ea; border-bottom: 1px solid #c3cdd6; font-weight: bold; color: #2c4a66; cursor: pointer; display: flex; }
|
||||
#menu .grp .cnt { margin-left: auto; font-weight: normal; color: #7b8a99; font-size: 10px; }
|
||||
#menu .grp::before { content: "▸ "; color: #7b8a99; }
|
||||
#menu .grp.open::before { content: "▾ "; }
|
||||
#menu .itm { padding: 5px 8px 5px 22px; border-bottom: 1px solid #e6eaee; cursor: pointer; color: #333;
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; display: none; }
|
||||
#menu .grp.open + .itms .itm, #menu .itms.show .itm { display: block; }
|
||||
#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; }
|
||||
#menu .itm .cd { color: #888; font-size: 10px; margin-right: 5px; font-family: Consolas, monospace; }
|
||||
|
||||
/* 컨텐츠 */
|
||||
#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 { height: 30px; background: #d5dce3; border-bottom: 1px solid #a9b4bf; display: flex; align-items: flex-end; padding: 0 8px; flex-shrink: 0; }
|
||||
#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 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; flex-shrink: 0; }
|
||||
.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 th { background: #f0f3f6; border: 1px solid #d3dae1; padding: 5px 8px; text-align: right; width: 150px; 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; }
|
||||
table.frm input[type=text] { width: 100%; max-width: 280px; height: 22px; border: 1px solid #9aa8b5; padding: 0 6px; font-size: 12px; }
|
||||
table.frm .fldcd { color: #98a4b0; font-size: 10px; margin-left: 8px; font-family: Consolas, monospace; }
|
||||
.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; }
|
||||
|
|
@ -57,8 +61,7 @@
|
|||
.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 { height: 24px; background: #d5dce3; border-top: 1px solid #a9b4bf; display: flex; align-items: center; padding: 0 10px; font-size: 11px; color: #345; flex-shrink: 0; }
|
||||
#statusbar .dot { width: 8px; height: 8px; border-radius: 50%; background: #23a54a; margin-right: 6px; }
|
||||
</style>
|
||||
</head>
|
||||
|
|
@ -66,101 +69,133 @@
|
|||
<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>
|
||||
<span class="right">사용자: <b>매입운영1팀</b> | 영업일 <span id="bizdate"></span> | 화면 <span id="scrno">-</span> | 전체화면 <b id="scrcnt">-</b>종</span>
|
||||
</div>
|
||||
|
||||
<div id="main">
|
||||
<div id="menuwrap">
|
||||
<div id="menusearch"><input id="q" placeholder="화면/서비스 검색 (예: 매입, SETTLE)"></div>
|
||||
<div id="menu"></div>
|
||||
</div>
|
||||
<div id="content">
|
||||
<div id="tabbar"><span class="tab" id="tabname">매입 접수</span></div>
|
||||
<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 src="/ui/screens.js"></script>
|
||||
<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 LBL = {
|
||||
T_MERCHANT:"가맹점번호", T_AMOUNT:"금액(원)", T_FEE:"수수료", T_NET:"정산액", T_STATUS:"상태",
|
||||
T_PURCHASE_ID:"매입번호", T_BIZDATE:"영업일자", T_SETTLE_ID:"정산번호", T_MSG:"메시지",
|
||||
T_CANCEL_ID:"취소번호", T_INSTALL_N:"할부개월", T_TAX:"세액", T_FX_AMT:"외화금액", T_COUNT:"건수",
|
||||
T_REASON:"사유", T_CATEGORY:"업종분류", T_ISSUER:"발급사", T_CHANNEL:"채널", T_CCY:"통화",
|
||||
T_ADJ:"조정번호", T_DELTA:"증감액", T_SVCNAME:"서비스명", T_RC:"결과코드", T_GROSS:"총액", T_DOCNO:"문서번호",
|
||||
T_KEY1:"키1(카드/계좌)", T_KEY2:"키2", T_ID1:"번호1", T_ID2:"번호2",
|
||||
};
|
||||
for (let i=1;i<=8;i++){ LBL['T_ARG'+i]='인자'+i; LBL['T_STR'+i]='문자'+i; }
|
||||
for (let i=1;i<=4;i++){ LBL['T_AMT'+i]='금액'+i; }
|
||||
|
||||
const DEF = {
|
||||
T_MERCHANT:"M0001", T_AMOUNT:"1000000", T_BIZDATE:today, T_PURCHASE_ID:"1", T_SETTLE_ID:"1",
|
||||
T_CANCEL_ID:"1", T_ADJ:"1", T_ID1:"1", T_ID2:"1", T_INSTALL_N:"3", T_COUNT:"10",
|
||||
T_CCY:"KRW", T_CHANNEL:"POS", T_ISSUER:"BC", T_CATEGORY:"5411", T_STATUS:"A",
|
||||
T_KEY1:"4111111111111111", T_STR5:"4111111111111111", T_FEE:"2500", T_DOCNO:"D0001",
|
||||
T_SVCNAME:"ACQUIRE", T_GROSS:"0", T_DELTA:"0", T_TAX:"0", T_FX_AMT:"0",
|
||||
};
|
||||
const dflt = f => DEF[f] !== undefined ? DEF[f] : (/T_(ARG|AMT)/.test(f) ? "0" : "");
|
||||
|
||||
/* XA 를 기본으로 켤 화면(갱신성) 판단: 조회성 키워드면 OFF */
|
||||
const READONLY = /(_INQ|_CHK|STATUS|SUMMARY|STATS|HISTORY|REPORT|_SUM|BALCHK|LUHN|CRC|WEEKDAY|MONTHEND|BIZDAY|CODENAME|VALIDATE|PROGRESS|VERIFY|DIFFCHK|INQUIRY|TRIALBAL)/;
|
||||
|
||||
/* ───────── 즐겨찾기(주요 업무) ───────── */
|
||||
const FAVS = [
|
||||
{ svc:"ACQUIRE", nm:"매입 접수(정산체인)", note:"글로벌 XA: ACQUIRE→RECONCILE→SETTLE 체인 후 2PC 커밋" },
|
||||
{ svc:"ACQ_STATUS", nm:"매입 상태조회" },
|
||||
{ svc:"ACQ_MERCHSUM", nm:"가맹점 매입집계" },
|
||||
{ svc:"LIMIT_INQ", nm:"한도 조회" },
|
||||
{ svc:"ST_MDR", nm:"MDR 수수료 산출" },
|
||||
{ svc:"LG_BALCHK", nm:"원장 잔액검증" },
|
||||
{ svc:"CL_DAILY", nm:"일마감 처리" },
|
||||
{ svc:"MM_MERCH_INQ", nm:"가맹점 조회" },
|
||||
{ svc:"CM_LUHN", nm:"카드번호 검증(Luhn)" },
|
||||
];
|
||||
|
||||
/* ───────── 메뉴 렌더 ───────── */
|
||||
/* ───────── 메뉴 구성: 즐겨찾기 + 11개 모듈 × 30 화면 ───────── */
|
||||
const byMod = {};
|
||||
SVC_SCREENS.forEach(s => { (byMod[s.mod] = byMod[s.mod] || []).push(s); });
|
||||
const bySvc = Object.fromEntries(SVC_SCREENS.map(s => [s.svc, s]));
|
||||
document.getElementById('scrcnt').textContent = SVC_SCREENS.length;
|
||||
|
||||
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 => {
|
||||
|
||||
function scrCode(s){ const i = byMod[s.mod].indexOf(s); return s.mod.toUpperCase() + String(i+1).padStart(2,'0') + '01'; }
|
||||
|
||||
function addGroup(title, items, open) {
|
||||
const gd = document.createElement('div'); gd.className = 'grp' + (open?' open':'');
|
||||
gd.innerHTML = `${title}<span class="cnt">${items.length}</span>`;
|
||||
const wrap = document.createElement('div'); wrap.className = 'itms';
|
||||
gd.onclick = () => gd.classList.toggle('open');
|
||||
menuEl.appendChild(gd); menuEl.appendChild(wrap);
|
||||
items.forEach(({s, nm, note}) => {
|
||||
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 cd = scrCode(s);
|
||||
it.innerHTML = `<span class="cd">${cd}</span>${nm || s.svc}`;
|
||||
it.title = (s.desc || s.svc);
|
||||
it.dataset.k = (cd + ' ' + s.svc + ' ' + (nm||'') + ' ' + (s.desc||'')).toLowerCase();
|
||||
it.onclick = (e) => { e.stopPropagation(); openScreen(s, it, nm, note); };
|
||||
wrap.appendChild(it);
|
||||
if (s.svc === 'ACQUIRE' && open) setTimeout(()=>openScreen(s, it, nm, note), 0);
|
||||
});
|
||||
}
|
||||
|
||||
addGroup('★ 주요 업무', FAVS.filter(f=>bySvc[f.svc]).map(f=>({s:bySvc[f.svc], nm:f.nm, note:f.note})), true);
|
||||
Object.keys(byMod).sort().forEach(m => {
|
||||
addGroup(`${byMod[m][0].modnm} (${m}) 업무`, byMod[m].map(s=>({s})), false);
|
||||
});
|
||||
|
||||
/* 검색 */
|
||||
document.getElementById('q').addEventListener('input', e => {
|
||||
const q = e.target.value.trim().toLowerCase();
|
||||
document.querySelectorAll('#menu .itms').forEach(w => w.classList.toggle('show', !!q));
|
||||
document.querySelectorAll('#menu .itm').forEach(it =>
|
||||
it.style.display = (!q || it.dataset.k.includes(q)) ? (q ? 'block' : '') : 'none');
|
||||
});
|
||||
|
||||
/* ───────── 화면 렌더 ───────── */
|
||||
const work = document.getElementById('work');
|
||||
const journal = []; /* 호출이력 */
|
||||
const journal = [];
|
||||
|
||||
function openScreen(s, itEl) {
|
||||
CUR = s;
|
||||
function openScreen(s, itEl, nm, note) {
|
||||
CUR = { s, nm: nm || s.svc };
|
||||
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;
|
||||
const cd = scrCode(s);
|
||||
document.getElementById('tabname').textContent = `${nm || s.svc}`;
|
||||
document.getElementById('scrno').textContent = 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>`;
|
||||
const useTx = !READONLY.test(s.svc);
|
||||
const rows = (s.in.length ? s.in : []).map((f,i) =>
|
||||
`<tr><th>${LBL[f]||f}<span class="fldcd">${f}</span></th>
|
||||
<td><input type="text" id="f_${i}" data-fld="${f}" value="${dflt(f)}"></td></tr>`).join('')
|
||||
|| `<tr><th>입력</th><td style="color:#889">입력 필드 없음 — 즉시 실행</td></tr>`;
|
||||
const outHint = s.out.length ? s.out.map(f=>LBL[f]||f).join(', ') : '-';
|
||||
|
||||
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="panel"><h3>${cd} · ${nm || s.svc} — <span style="font-weight:normal;color:#667">${s.desc || note || ''}</span></h3>
|
||||
<div class="bd">
|
||||
<table class="frm">${svcRow}${rows}</table>
|
||||
<table class="frm">
|
||||
<tr><th>서비스명</th><td><b>${s.svc}</b> <span class="fldcd">(${s.modnm} 모듈 · ${s.mod}_svr)</span></td></tr>
|
||||
${rows}
|
||||
<tr><th>글로벌 XA</th><td><label><input type="checkbox" id="usetx" ${useTx?'checked':''}> tpbegin / tpcommit (2PC)</label></td></tr>
|
||||
</table>
|
||||
<div style="margin-top:6px;color:#889">응답 필드: ${outHint}</div>
|
||||
<div class="btnrow">
|
||||
<button onclick="resetForm()">초기화</button>
|
||||
<button class="pri" onclick="execute()">실행 (tpcall)</button>
|
||||
|
|
@ -168,7 +203,7 @@ function openScreen(s, itEl) {
|
|||
</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>
|
||||
<div class="bd"><table class="grd" id="resgrid"><tr><th style="width:200px">필드</th><th>값</th></tr>
|
||||
<tr><td colspan="2" style="color:#999">실행 전</td></tr></table></div>
|
||||
</div>
|
||||
<div class="panel"><h3>거래 저널 (금일 호출이력)</h3>
|
||||
|
|
@ -178,38 +213,35 @@ function openScreen(s, itEl) {
|
|||
renderJournal();
|
||||
}
|
||||
|
||||
function resetForm(){ openScreen(CUR, document.querySelector('#menu .itm.on')); }
|
||||
function resetForm(){ const on = document.querySelector('#menu .itm.on'); openScreen(CUR.s, on, CUR.nm); }
|
||||
|
||||
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);
|
||||
const { s } = CUR;
|
||||
const useTx = document.getElementById('usetx').checked ? 1 : 0;
|
||||
const p = new URLSearchParams({ svc: s.svc, _tx: useTx });
|
||||
s.in.forEach((f,i) => {
|
||||
const v = document.getElementById('f_'+i)?.value;
|
||||
if (v !== undefined && v !== '') p.append(f, v);
|
||||
});
|
||||
setStatus(`tpcall("${svc}") 호출 중 ...`);
|
||||
setStatus(`tpcall("${s.svc}") 호출 중 ...`);
|
||||
let j;
|
||||
try {
|
||||
const r = await fetch('/api/call?' + p.toString());
|
||||
j = await r.json();
|
||||
} catch (e) { j = { ok:false, error: String(e) }; }
|
||||
try { j = await (await fetch('/api/call?' + p.toString())).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>';
|
||||
g.innerHTML = '<tr><th style="width:200px">필드</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)':''}`);
|
||||
g.innerHTML += `<tr><td>${LBL[k.replace('#','')]||k} <span class="fldcd">${k}</span></td><td class="num">${fmt(k,v)}</td></tr>`;
|
||||
setStatus(`tpcall("${s.svc}") 정상 처리${useTx?' · XA tpcommit 완료(2PC)':''}`);
|
||||
} else {
|
||||
g.innerHTML += `<tr><td>오류</td><td class="err">${j.error||'unknown'}</td></tr>`;
|
||||
setStatus(`tpcall("${svc}") 실패`, true);
|
||||
setStatus(`tpcall("${s.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) });
|
||||
journal.unshift({ t:new Date().toLocaleTimeString('ko-KR'), cd:scrCode(s), svc:s.svc, tx:useTx?'Y':'N',
|
||||
ok:j.ok, sum: j.ok ? summarize(j.fields) : (j.error||'').slice(0,70) });
|
||||
renderJournal();
|
||||
}
|
||||
|
||||
|
|
|
|||
4304
app/ui/screens.js
Normal file
4304
app/ui/screens.js
Normal file
File diff suppressed because it is too large
Load diff
30
tools/gen_screens.py
Executable file
30
tools/gen_screens.py
Executable file
|
|
@ -0,0 +1,30 @@
|
|||
#!/usr/bin/env python3
|
||||
"""app/src/*/svc/*.pgc 에서 화면 매니페스트(app/ui/screens.js)를 재생성한다.
|
||||
각 tpservice 의 입력(getl/gets_)·출력(setl/sets) 필드와 한글 설명을 추출."""
|
||||
import re, os, json, glob
|
||||
|
||||
MOD_NAMES = {'ac':'매입','au':'승인/한도','rc':'대사','st':'정산/수수료','py':'지급',
|
||||
'lg':'원장','cl':'마감','mm':'마스터','vl':'정합성검증','mg':'전문게이트웨이','cm':'공통'}
|
||||
screens=[]
|
||||
for path in sorted(glob.glob('app/src/*/svc/*.pgc')):
|
||||
mod = path.split('/')[2]
|
||||
svc = os.path.basename(path)[:-4]
|
||||
src = open(path, encoding='utf-8', errors='ignore').read()
|
||||
desc=''
|
||||
for line in src.splitlines()[:12]:
|
||||
line=line.strip().lstrip('/*').strip(' *')
|
||||
if re.search(r'[가-힣]', line) and svc not in line[:len(svc)+8]:
|
||||
desc=line.strip(); break
|
||||
if re.search(r'[가-힣]', line) and ':' in line:
|
||||
desc=line.split('-',1)[-1].strip(); break
|
||||
if not desc:
|
||||
m=re.search(r'\*\s*([^\n]*[가-힣][^\n]*)', src[:600])
|
||||
desc=m.group(1).strip() if m else ''
|
||||
dedup=lambda xs: list(dict.fromkeys(xs))
|
||||
screens.append({'mod':mod,'modnm':MOD_NAMES.get(mod,mod),'svc':svc,'desc':desc[:80],
|
||||
'in':dedup(re.findall(r'(?:getl|gets_)\(b,\s*([A-Z_0-9]+)',src)),
|
||||
'out':dedup(re.findall(r'(?:setl|sets|sets_)\(b,\s*([A-Z_0-9]+)',src))})
|
||||
with open('app/ui/screens.js','w',encoding='utf-8') as f:
|
||||
f.write("/* 자동생성: app/src/*/svc/*.pgc 에서 추출한 화면 매니페스트 (재생성: tools/gen_screens.py) */\n")
|
||||
f.write("const SVC_SCREENS = "); json.dump(screens,f,ensure_ascii=False,indent=1); f.write(";\n")
|
||||
print(f"screens: {len(screens)}")
|
||||
Loading…
Add table
Add a link
Reference in a new issue