업무화면 고도화: MDI 탭·그리드(정렬/페이징/CSV/합계/배지)·상세팝업·마스터-디테일
- MDI 탭바: 화면 다중 오픈/전환/닫기 (상태 유지, 레거시 업무포털 방식) - 그리드 컴포넌트: 컬럼 정렬, 페이징(1/34), CSV 저장, 금액 합계행, 상태 한글배지 (접수/대사/정산완료/취소/보류), 행 hover·클릭 - 매입 상세 팝업: 행 클릭 → 매입원장 + 승인/정산/원장(복식기표) 연계조회 + 보류(ACQ_HOLD)·취소(ACQ_CANCEL) 실 tpcall XA 액션 (상태별 버튼 활성화) - 가맹점 관리(QR0301): 마스터 그리드 ↔ 상세폼, 신규등록/정보수정/상태변경 실 tpcall(MM_MERCH_REG/UPD/STATUS) + 확장마스터/변경이력 연계 - 매입목록 필터 확장(상태), 해시 딥링크(#q=/#s=), 거래저널 전화면 공유 - 검증: 헤드리스 스크린샷 3종(대시보드/매입목록/가맹점관리) 렌더 확인 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
1fd60a1e49
commit
19acf4ed96
1 changed files with 405 additions and 236 deletions
|
|
@ -4,7 +4,7 @@
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>ACQUIRE-CORE-X 매입업무포털</title>
|
<title>ACQUIRE-CORE-X 매입업무포털</title>
|
||||||
<style>
|
<style>
|
||||||
/* Xplatform(UXStudio) 풍 내부 업무화면 스타일 */
|
/* Xplatform(UXStudio) 풍 내부 업무화면 */
|
||||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
html, body { height: 100%; font-family: "Malgun Gothic", "맑은 고딕", Dotum, sans-serif; font-size: 12px; color: #222; }
|
html, body { height: 100%; font-family: "Malgun Gothic", "맑은 고딕", Dotum, sans-serif; font-size: 12px; color: #222; }
|
||||||
body { display: flex; flex-direction: column; background: #e8ebef; }
|
body { display: flex; flex-direction: column; background: #e8ebef; }
|
||||||
|
|
@ -17,8 +17,7 @@
|
||||||
|
|
||||||
#main { flex: 1; display: flex; min-height: 0; }
|
#main { flex: 1; display: flex; min-height: 0; }
|
||||||
|
|
||||||
/* 좌측 메뉴 트리 */
|
#menuwrap { width: 232px; background: #f4f6f8; border-right: 1px solid #b8c2cc; display: flex; flex-direction: column; flex-shrink: 0; }
|
||||||
#menuwrap { width: 232px; background: #f4f6f8; border-right: 1px solid #b8c2cc; display: flex; flex-direction: column; }
|
|
||||||
#menusearch { padding: 6px; border-bottom: 1px solid #ccd4dc; }
|
#menusearch { padding: 6px; border-bottom: 1px solid #ccd4dc; }
|
||||||
#menusearch input { width: 100%; height: 24px; border: 1px solid #9aa8b5; padding: 0 8px; font-size: 12px; }
|
#menusearch input { width: 100%; height: 24px; border: 1px solid #9aa8b5; padding: 0 8px; font-size: 12px; }
|
||||||
#menu { flex: 1; overflow-y: auto; }
|
#menu { flex: 1; overflow-y: auto; }
|
||||||
|
|
@ -34,9 +33,18 @@
|
||||||
#menu .itm .cd { color: #888; font-size: 10px; margin-right: 5px; font-family: Consolas, monospace; }
|
#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; }
|
#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; flex-shrink: 0; }
|
|
||||||
#tabbar .tab { padding: 5px 16px 4px; background: #fff; border: 1px solid #a9b4bf; border-bottom: none; font-weight: bold; color: #1d4269; }
|
/* MDI 탭바 */
|
||||||
|
#tabbar { min-height: 30px; background: #d5dce3; border-bottom: 1px solid #a9b4bf; display: flex; align-items: flex-end; padding: 4px 8px 0; flex-wrap: wrap; gap: 2px; flex-shrink: 0; }
|
||||||
|
#tabbar .tab { padding: 5px 8px 4px 14px; background: #e9edf1; border: 1px solid #a9b4bf; border-bottom: none; color: #55677a; cursor: pointer; display: flex; align-items: center; max-width: 210px; }
|
||||||
|
#tabbar .tab.on { background: #fff; font-weight: bold; color: #1d4269; }
|
||||||
|
#tabbar .tab .tnm { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
||||||
|
#tabbar .tab .x { margin-left: 8px; color: #98a4b0; font-weight: bold; padding: 0 3px; }
|
||||||
|
#tabbar .tab .x:hover { color: #c22; }
|
||||||
|
|
||||||
#work { flex: 1; overflow: auto; padding: 12px; }
|
#work { flex: 1; overflow: auto; padding: 12px; }
|
||||||
|
.tabpane { display: none; }
|
||||||
|
.tabpane.on { display: block; }
|
||||||
|
|
||||||
.panel { background: #fff; border: 1px solid #b0bac4; margin-bottom: 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 { font-size: 12px; padding: 6px 10px; background: #eef2f6; border-bottom: 1px solid #ccd4dc; color: #1d4269; display: flex; align-items: center; }
|
||||||
|
|
@ -46,34 +54,58 @@
|
||||||
table.frm { border-collapse: collapse; width: 100%; }
|
table.frm { border-collapse: collapse; width: 100%; }
|
||||||
table.frm th { background: #f0f3f6; border: 1px solid #d3dae1; padding: 5px 8px; text-align: right; width: 150px; 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 td { border: 1px solid #d3dae1; padding: 3px 6px; }
|
||||||
table.frm input[type=text] { width: 100%; max-width: 280px; height: 22px; border: 1px solid #9aa8b5; padding: 0 6px; font-size: 12px; }
|
table.frm input[type=text], table.frm select { width: 100%; max-width: 280px; height: 22px; border: 1px solid #9aa8b5; padding: 0 6px; font-size: 12px; background:#fff; }
|
||||||
|
table.frm input[readonly] { background: #f0f3f6; color: #667; }
|
||||||
table.frm .fldcd { color: #98a4b0; font-size: 10px; margin-left: 8px; font-family: Consolas, monospace; }
|
table.frm .fldcd { color: #98a4b0; font-size: 10px; margin-left: 8px; font-family: Consolas, monospace; }
|
||||||
.btnrow { margin-top: 10px; text-align: right; }
|
.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 { height: 26px; padding: 0 14px; 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.pri { background: linear-gradient(#3f76ad, #29578a); color: #fff; border-color: #1d4269; font-weight: bold; }
|
||||||
|
button.warn { background: linear-gradient(#e8a54a, #c8862c); color: #fff; border-color: #a06a1e; }
|
||||||
|
button.dang { background: linear-gradient(#d95f5f, #b23c3c); color: #fff; border-color: #8f2b2b; }
|
||||||
button:hover { filter: brightness(1.06); }
|
button:hover { filter: brightness(1.06); }
|
||||||
|
button:disabled { opacity: .5; cursor: default; }
|
||||||
|
|
||||||
|
/* 그리드 */
|
||||||
|
.gtools { display: flex; align-items: center; gap: 10px; margin-bottom: 6px; color: #556; }
|
||||||
|
.gtools .sp { flex: 1; }
|
||||||
|
.gtools button { height: 22px; padding: 0 10px; margin: 0; font-size: 11px; }
|
||||||
table.grd { border-collapse: collapse; width: 100%; }
|
table.grd { border-collapse: collapse; width: 100%; }
|
||||||
table.grd th { background: #dfe6ec; border: 1px solid #b8c2cc; padding: 4px 6px; color: #2c4a66; }
|
table.grd th { background: #dfe6ec; border: 1px solid #b8c2cc; padding: 4px 6px; color: #2c4a66; cursor: pointer; user-select: none; white-space: nowrap; }
|
||||||
table.grd td { border: 1px solid #d3dae1; padding: 3px 6px; background: #fff; }
|
table.grd th .sa { color: #29578a; margin-left: 3px; }
|
||||||
|
table.grd td { border: 1px solid #d3dae1; padding: 3px 6px; background: #fff; white-space: nowrap; }
|
||||||
table.grd tr:nth-child(even) td { background: #f7fafc; }
|
table.grd tr:nth-child(even) td { background: #f7fafc; }
|
||||||
|
table.grd tr.rw:hover td { background: #e8f2fb; cursor: pointer; }
|
||||||
|
table.grd tr.sum td { background: #eef3f8; font-weight: bold; border-top: 2px solid #a9b4bf; }
|
||||||
.num { text-align: right; font-family: Consolas, monospace; }
|
.num { text-align: right; font-family: Consolas, monospace; }
|
||||||
.ok { color: #0a7d2c; font-weight: bold; }
|
.ok { color: #0a7d2c; font-weight: bold; }
|
||||||
.err { color: #c22; font-weight: bold; }
|
.err { color: #c22; font-weight: bold; }
|
||||||
|
.bdg { display: inline-block; padding: 1px 7px; border-radius: 2px; font-size: 11px; color: #fff; }
|
||||||
|
.bA { background: #3a7bbf; } .bM { background: #d98a2b; } .bS { background: #2b9e4e; }
|
||||||
|
.bC { background: #c24444; } .bH { background: #7c8a99; } .bV { background: #8f2b8f; }
|
||||||
|
|
||||||
#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; }
|
|
||||||
.cards { display: flex; gap: 10px; flex-wrap: wrap; }
|
.cards { display: flex; gap: 10px; flex-wrap: wrap; }
|
||||||
.card { flex: 1; min-width: 128px; border: 1px solid #c6d0da; background: #f7fafd; padding: 8px 10px; }
|
.card { flex: 1; min-width: 128px; border: 1px solid #c6d0da; background: #f7fafd; padding: 8px 10px; }
|
||||||
.card .cl { font-size: 11px; color: #667; }
|
.card .cl { font-size: 11px; color: #667; }
|
||||||
.card .cv { font-size: 16px; font-weight: bold; color: #1d4269; margin-top: 4px; text-align: right; font-family: Consolas, monospace; }
|
.card .cv { font-size: 16px; font-weight: bold; color: #1d4269; margin-top: 4px; text-align: right; font-family: Consolas, monospace; }
|
||||||
|
|
||||||
|
/* 팝업(모달) */
|
||||||
|
#ovl { position: fixed; inset: 0; background: rgba(20,30,40,.45); display: none; z-index: 50; }
|
||||||
|
#ovl.show { display: block; }
|
||||||
|
#pop { position: absolute; top: 6%; left: 50%; transform: translateX(-50%); width: 880px; max-height: 86%;
|
||||||
|
overflow: auto; background: #eef1f4; border: 2px solid #2c5d8f; box-shadow: 0 8px 30px rgba(0,0,0,.35); }
|
||||||
|
#pop .phd { background: linear-gradient(#2c5d8f, #1d4269); color: #fff; padding: 8px 12px; font-weight: bold; display: flex; }
|
||||||
|
#pop .phd .x { margin-left: auto; cursor: pointer; padding: 0 6px; }
|
||||||
|
#pop .pbd { padding: 12px; }
|
||||||
|
|
||||||
|
#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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="titlebar">
|
<div id="titlebar">
|
||||||
<span class="logo">ACQUIRE-CORE-X</span>
|
<span class="logo">ACQUIRE-CORE-X</span>
|
||||||
<span class="sub">글로벌 매입업무포털 (Enduro/X · ECPG/PostgreSQL · XA)</span>
|
<span class="sub">글로벌 매입업무포털 (Enduro/X · ECPG/PostgreSQL · XA)</span>
|
||||||
<span class="right">사용자: <b>매입운영1팀</b> | 영업일 <span id="bizdate"></span> | 화면 <span id="scrno">-</span> | 전체화면 <b id="scrcnt">-</b>종</span>
|
<span class="right">사용자: <b>매입운영1팀</b> | 영업일 <span id="bizdate"></span> | 전체화면 <b id="scrcnt">-</b>종</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="main">
|
<div id="main">
|
||||||
|
|
@ -82,12 +114,14 @@
|
||||||
<div id="menu"></div>
|
<div id="menu"></div>
|
||||||
</div>
|
</div>
|
||||||
<div id="content">
|
<div id="content">
|
||||||
<div id="tabbar"><span class="tab" id="tabname">-</span></div>
|
<div id="tabbar"></div>
|
||||||
<div id="work"></div>
|
<div id="work"></div>
|
||||||
<div id="statusbar"><span class="dot"></span><span id="stmsg">TP 모니터 연결됨 — 게이트웨이 /api/call</span></div>
|
<div id="statusbar"><span class="dot"></span><span id="stmsg">TP 모니터 연결됨 — 게이트웨이 /api/call</span></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="ovl"><div id="pop"><div class="phd"><span id="ptit">상세</span><span class="x" onclick="closePop()">✕</span></div><div class="pbd" id="pbd"></div></div></div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
/* === 화면 매니페스트 (인라인; 재생성: tools/gen_screens.py) === */
|
/* === 화면 매니페스트 (인라인; 재생성: tools/gen_screens.py) === */
|
||||||
/* 자동생성 화면 매니페스트 - tools/gen_screens.py 가 svc 소스에서 추출 */
|
/* 자동생성 화면 매니페스트 - tools/gen_screens.py 가 svc 소스에서 추출 */
|
||||||
|
|
@ -4396,13 +4430,25 @@ const SVC_SCREENS = [
|
||||||
];
|
];
|
||||||
</script>
|
</script>
|
||||||
<script>
|
<script>
|
||||||
window.onerror = function(m, src, line){
|
window.onerror = function(m, src, line){
|
||||||
var e=document.getElementById('stmsg');
|
var e=document.getElementById('stmsg');
|
||||||
if(e){ e.textContent='JS 오류: '+m+' ('+(src||'').split('/').pop()+':'+line+')'; e.style.color='#c22'; }
|
if(e){ e.textContent='JS 오류: '+m+' ('+(src||'').split('/').pop()+':'+line+')'; e.style.color='#c22'; }
|
||||||
};
|
};
|
||||||
/* ───────── 공통: 필드 한글 라벨 / 기본값 ───────── */
|
|
||||||
|
/* ───────── 공통 ───────── */
|
||||||
const today = new Date().toISOString().slice(0,10).replace(/-/g,'');
|
const today = new Date().toISOString().slice(0,10).replace(/-/g,'');
|
||||||
document.getElementById('bizdate').textContent = today;
|
document.getElementById('bizdate').textContent = today;
|
||||||
|
const fmtN = v => (v===null||v===undefined||v==='') ? '' : (/^-?\d+$/.test(String(v)) ? Number(v).toLocaleString('ko-KR') : v);
|
||||||
|
const STMAP = { A:['접수','bA'], M:['대사','bM'], S:['정산완료','bS'], C:['취소','bC'], H:['보류','bH'], V:['VOID','bV'],
|
||||||
|
ACTIVE:['정상','bS'], HOLD:['보류','bH'], PENDING:['심사중','bM'], SETTLED:['지급완료','bS'] };
|
||||||
|
const badge = v => { const m=STMAP[v]; return m? `<span class="bdg ${m[1]}">${m[0]}</span>` : v; };
|
||||||
|
const setStatus = (m, err) => { const e=document.getElementById('stmsg'); e.textContent=m; e.style.color=err?'#c22':'#345'; };
|
||||||
|
async function runQuery(sql){ try { return await (await fetch('api/query?q='+encodeURIComponent(sql))).json(); } catch(e){ return {ok:false,error:String(e)}; } }
|
||||||
|
async function callSvc(svc, fields, tx){
|
||||||
|
const p = new URLSearchParams({svc, _tx: tx?1:0});
|
||||||
|
Object.entries(fields||{}).forEach(([k,v])=>{ if(v!=='') p.append(k,v); });
|
||||||
|
try { return await (await fetch('api/call?'+p.toString())).json(); } catch(e){ return {ok:false,error:String(e)}; }
|
||||||
|
}
|
||||||
|
|
||||||
const LBL = {
|
const LBL = {
|
||||||
T_MERCHANT:"가맹점번호", T_AMOUNT:"금액(원)", T_FEE:"수수료", T_NET:"정산액", T_STATUS:"상태",
|
T_MERCHANT:"가맹점번호", T_AMOUNT:"금액(원)", T_FEE:"수수료", T_NET:"정산액", T_STATUS:"상태",
|
||||||
|
|
@ -4414,20 +4460,347 @@ const LBL = {
|
||||||
};
|
};
|
||||||
for (let i=1;i<=8;i++){ LBL['T_ARG'+i]='인자'+i; LBL['T_STR'+i]='문자'+i; }
|
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; }
|
for (let i=1;i<=4;i++){ LBL['T_AMT'+i]='금액'+i; }
|
||||||
|
|
||||||
const DEF = {
|
const DEF = {
|
||||||
T_MERCHANT:"M0001", T_AMOUNT:"1000000", T_BIZDATE:today, T_PURCHASE_ID:"1", T_SETTLE_ID:"1",
|
T_MERCHANT:"M0001", T_AMOUNT:"1000000", T_BIZDATE:today, T_PURCHASE_ID:"1001", T_SETTLE_ID:"3001",
|
||||||
T_CANCEL_ID:"1", T_ADJ:"1", T_ID1:"1", T_ID2:"1", T_INSTALL_N:"3", T_COUNT:"10",
|
T_CANCEL_ID:"1", T_ADJ:"1", T_ID1:"5001", 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_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_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",
|
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" : "");
|
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 READONLY = /(_INQ|_CHK|STATUS|SUMMARY|STATS|HISTORY|REPORT|_SUM|BALCHK|LUHN|CRC|WEEKDAY|MONTHEND|BIZDAY|CODENAME|VALIDATE|PROGRESS|VERIFY|DIFFCHK|INQUIRY|TRIALBAL)/;
|
||||||
|
|
||||||
/* ───────── 즐겨찾기(주요 업무) ───────── */
|
/* ───────── 그리드 컴포넌트 (정렬·페이징·CSV·합계·행클릭) ───────── */
|
||||||
|
const __G = {}; let __gid = 0;
|
||||||
|
function grid(el, data, opt={}){
|
||||||
|
if (!data.ok) { el.innerHTML = `<span class="err">${data.error||'조회 실패'}</span>`; return; }
|
||||||
|
const id = 'g' + (++__gid);
|
||||||
|
const g = { el, data, opt, st: { sort: null, dir: 1, page: 0, psz: opt.psz || 15 } };
|
||||||
|
__G[id] = g;
|
||||||
|
const sorted = () => {
|
||||||
|
let view = [...g.data.rows];
|
||||||
|
if (g.st.sort !== null) {
|
||||||
|
const i = g.st.sort;
|
||||||
|
view.sort((a,b) => { const x=a[i], y=b[i];
|
||||||
|
return (/^-?\d+$/.test(x) && /^-?\d+$/.test(y) ? Number(x)-Number(y) : String(x).localeCompare(String(y))) * g.st.dir; });
|
||||||
|
}
|
||||||
|
return view;
|
||||||
|
};
|
||||||
|
g.render = () => {
|
||||||
|
const { cols, rows } = g.data, st = g.st;
|
||||||
|
const view = sorted();
|
||||||
|
const np = Math.max(1, Math.ceil(view.length / st.psz));
|
||||||
|
if (st.page >= np) st.page = np - 1;
|
||||||
|
const pg = view.slice(st.page*st.psz, (st.page+1)*st.psz);
|
||||||
|
const isNum = cols.map((_,i) => rows.length > 0 && rows.every(r => r[i]==='' || /^-?\d+$/.test(r[i])));
|
||||||
|
const doSum = cols.map((c,i) => isNum[i] && /금액|수수료|정산액|지급액|건수|합계|세액/.test(c));
|
||||||
|
let h = `<div class="gtools"><span>총 <b>${rows.length.toLocaleString()}</b>건</span><span class="sp"></span>
|
||||||
|
<button onclick="__G['${id}'].csv()">CSV 저장</button>
|
||||||
|
<button onclick="__G['${id}'].pg(-1)">◀ 이전</button><span>${st.page+1} / ${np}</span><button onclick="__G['${id}'].pg(1)">다음 ▶</button></div>`;
|
||||||
|
h += '<table class="grd"><tr>' + cols.map((c,i) =>
|
||||||
|
`<th onclick="__G['${id}'].sort(${i})">${c}${st.sort===i ? `<span class="sa">${st.dir>0?'▲':'▼'}</span>`:''}</th>`).join('') + '</tr>';
|
||||||
|
h += pg.map((r,ri) => `<tr class="${opt.onRow?'rw':''}" ${opt.onRow?`onclick="__G['${id}'].row(${st.page*st.psz+ri})"`:''}>` +
|
||||||
|
r.map((v,i) => `<td class="${isNum[i]?'num':''}">${cols[i]==='상태'?badge(v):fmtN(v)}</td>`).join('') + '</tr>').join('');
|
||||||
|
if (doSum.some(Boolean) && rows.length > 1)
|
||||||
|
h += '<tr class="sum">' + cols.map((c,i) => doSum[i]
|
||||||
|
? `<td class="num">${fmtN(rows.reduce((s,r)=>s+Number(r[i]||0),0))}</td>`
|
||||||
|
: `<td>${i===0?'합계':''}</td>`).join('') + '</tr>';
|
||||||
|
h += '</table>';
|
||||||
|
el.innerHTML = h;
|
||||||
|
};
|
||||||
|
g.sort = i => { g.st.dir = (g.st.sort===i) ? -g.st.dir : 1; g.st.sort = i; g.render(); };
|
||||||
|
g.pg = d => { g.st.page = Math.max(0, g.st.page + d); g.render(); };
|
||||||
|
g.row = i => { opt.onRow(sorted()[i]); };
|
||||||
|
g.csv = () => {
|
||||||
|
const csv = '' + [g.data.cols.join(','), ...g.data.rows.map(r=>r.map(v=>`"${String(v).replace(/"/g,'""')}"`).join(','))].join('\n');
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = URL.createObjectURL(new Blob([csv], {type:'text/csv;charset=utf-8'}));
|
||||||
|
a.download = (opt.name||'grid') + '_' + today + '.csv'; a.click();
|
||||||
|
};
|
||||||
|
g.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ───────── MDI 탭 ───────── */
|
||||||
|
const TABS = {}; let activeTab = null;
|
||||||
|
const tabbar = document.getElementById('tabbar'), work = document.getElementById('work');
|
||||||
|
function openTab(key, title, renderFn){
|
||||||
|
if (TABS[key]) return focusTab(key);
|
||||||
|
const pane = document.createElement('div'); pane.className = 'tabpane'; work.appendChild(pane);
|
||||||
|
const tab = document.createElement('span'); tab.className = 'tab';
|
||||||
|
tab.innerHTML = `<span class="tnm">${title}</span><span class="x">✕</span>`;
|
||||||
|
tab.onclick = e => { if (!e.target.classList.contains('x')) focusTab(key); };
|
||||||
|
tab.querySelector('.x').onclick = e => { e.stopPropagation(); closeTab(key); };
|
||||||
|
tabbar.appendChild(tab);
|
||||||
|
TABS[key] = { pane, tab, title };
|
||||||
|
renderFn(pane);
|
||||||
|
focusTab(key);
|
||||||
|
}
|
||||||
|
function focusTab(key){
|
||||||
|
Object.entries(TABS).forEach(([k,t]) => { t.pane.classList.toggle('on', k===key); t.tab.classList.toggle('on', k===key); });
|
||||||
|
activeTab = key;
|
||||||
|
}
|
||||||
|
function closeTab(key){
|
||||||
|
const t = TABS[key]; if (!t) return;
|
||||||
|
t.pane.remove(); t.tab.remove(); delete TABS[key];
|
||||||
|
if (activeTab === key) { const ks = Object.keys(TABS); if (ks.length) focusTab(ks[ks.length-1]); else activeTab = null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ───────── 매입 상세 팝업 (연계조회 + 보류/취소 액션) ───────── */
|
||||||
|
function closePop(){ document.getElementById('ovl').classList.remove('show'); }
|
||||||
|
async function openPurchaseDetail(pid){
|
||||||
|
const ovl = document.getElementById('ovl'), bd = document.getElementById('pbd');
|
||||||
|
document.getElementById('ptit').textContent = `매입 상세조회 — 매입번호 ${pid}`;
|
||||||
|
bd.innerHTML = '조회 중 ...'; ovl.classList.add('show');
|
||||||
|
const [p, ap, st, lg] = await Promise.all([
|
||||||
|
runQuery(`select p.purchase_id 매입번호, p.biz_date 영업일, p.merchant_id 가맹점번호, m.name 상호, p.amount 매입금액, p.fee 수수료, p.net 정산액, p.status 상태, p.channel 채널, p.issuer 발급사, p.category 업종, to_char(p.created_at,'YYYY-MM-DD HH24:MI:SS') 접수시각 from purchase p join merchant m on m.merchant_id=p.merchant_id where p.purchase_id=${pid}`),
|
||||||
|
runQuery(`select approval_id 승인번호, case when approved then '승인' else '거절' end 결과, to_char(approved_at,'YYYY-MM-DD HH24:MI:SS') 승인시각 from approval where purchase_id=${pid}`),
|
||||||
|
runQuery(`select settlement_id 정산번호, net 지급액, status 상태, to_char(settled_at,'YYYY-MM-DD HH24:MI') 지급시각 from settlement where purchase_id=${pid}`),
|
||||||
|
runQuery(`select ledger_id 전표번호, entry_type 유형, amount 금액, biz_date 영업일 from ledger where purchase_id=${pid} order by ledger_id`),
|
||||||
|
]);
|
||||||
|
let info = '<span class="err">매입 없음</span>', stv = '';
|
||||||
|
if (p.ok && p.rows.length) {
|
||||||
|
stv = p.rows[0][p.cols.indexOf('상태')];
|
||||||
|
info = '<table class="frm">' + p.cols.map((c,i) =>
|
||||||
|
`<tr><th>${c}</th><td>${c==='상태'?badge(p.rows[0][i]):fmtN(p.rows[0][i])}</td></tr>`).join('') + '</table>';
|
||||||
|
}
|
||||||
|
bd.innerHTML = `
|
||||||
|
<div style="display:flex;gap:12px">
|
||||||
|
<div class="panel" style="flex:1"><h3>매입 원장</h3><div class="bd">${info}</div></div>
|
||||||
|
<div style="flex:1.2">
|
||||||
|
<div class="panel"><h3>승인 연계</h3><div class="bd" id="pp_ap"></div></div>
|
||||||
|
<div class="panel"><h3>정산 연계</h3><div class="bd" id="pp_st"></div></div>
|
||||||
|
<div class="panel"><h3>원장 기표 (복식)</h3><div class="bd" id="pp_lg"></div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="btnrow">
|
||||||
|
<button class="warn" ${['A','M'].includes(stv)?'':'disabled'} onclick="actHold(${pid})">보류 처리 (ACQ_HOLD)</button>
|
||||||
|
<button class="dang" ${['A','M','H'].includes(stv)?'':'disabled'} onclick="actCancel(${pid})">매입 취소 (ACQ_CANCEL·XA)</button>
|
||||||
|
<button onclick="closePop()">닫기</button>
|
||||||
|
</div>`;
|
||||||
|
grid(document.getElementById('pp_ap'), ap, {psz:3});
|
||||||
|
grid(document.getElementById('pp_st'), st, {psz:3});
|
||||||
|
grid(document.getElementById('pp_lg'), lg, {psz:5});
|
||||||
|
}
|
||||||
|
async function actHold(pid){
|
||||||
|
const j = await callSvc('ACQ_HOLD', {T_PURCHASE_ID: pid}, true);
|
||||||
|
jlog('POP', 'ACQ_HOLD', true, j);
|
||||||
|
alert(j.ok ? `보류 처리 완료 (반영 ${(j.fields||{}).T_RC||0}건)` : `실패: ${j.error}`);
|
||||||
|
openPurchaseDetail(pid);
|
||||||
|
}
|
||||||
|
async function actCancel(pid){
|
||||||
|
const reason = prompt('취소 사유를 입력하세요', '가맹점 요청');
|
||||||
|
if (reason === null) return;
|
||||||
|
const j = await callSvc('ACQ_CANCEL', {T_PURCHASE_ID: pid, T_BIZDATE: today, T_REASON: reason}, true);
|
||||||
|
jlog('POP', 'ACQ_CANCEL', true, j);
|
||||||
|
alert(j.ok ? `취소 접수 완료 (취소번호 ${(j.fields||{}).T_CANCEL_ID||'-'})` : `실패: ${j.error}`);
|
||||||
|
openPurchaseDetail(pid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ───────── 거래 저널 ───────── */
|
||||||
|
const journal = [];
|
||||||
|
function jlog(cd, svc, tx, j){
|
||||||
|
journal.unshift({ t:new Date().toLocaleTimeString('ko-KR'), cd, svc, tx: tx?'Y':'N', ok: j.ok,
|
||||||
|
sum: j.ok ? Object.entries(j.fields||{}).slice(0,4).map(([k,v])=>`${k}=${v}`).join(' ') : (j.error||'').slice(0,70) });
|
||||||
|
document.querySelectorAll('.jgrid').forEach(renderJournalIn);
|
||||||
|
}
|
||||||
|
function renderJournalIn(g){
|
||||||
|
g.innerHTML = '<tr><th>시각</th><th>화면</th><th>서비스</th><th>XA</th><th>결과</th><th>요약</th></tr>' +
|
||||||
|
journal.slice(0,30).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('');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ───────── 조회/현황 화면 정의 ───────── */
|
||||||
|
const QSCREENS = [
|
||||||
|
{cd:'HM0001', nm:'운영 대시보드', dash:true},
|
||||||
|
{cd:'QR0101', nm:'매입 목록 조회', filters:[['from','시작일','2026-07-01'],['to','종료일','2026-07-19'],['merch','가맹점번호(선택)',''],['st','상태(선택)','']],
|
||||||
|
detail:true,
|
||||||
|
sql:f=>`select p.purchase_id 매입번호, p.biz_date 영업일, p.merchant_id 가맹점번호, m.name 상호, p.amount 매입금액, p.fee 수수료, p.net 정산액, p.status 상태, p.channel 채널, p.issuer 발급사 from purchase p join merchant m on m.merchant_id=p.merchant_id where p.biz_date between '${f.from}' and '${f.to}' ${f.merch?`and p.merchant_id='${f.merch}'`:''} ${f.st?`and p.status='${f.st}'`:''} order by p.purchase_id desc limit 500`},
|
||||||
|
{cd:'QR0201', nm:'정산 목록 조회', filters:[['from','시작일','2026-07-01'],['to','종료일','2026-07-19']],
|
||||||
|
sql:f=>`select s.settlement_id 정산번호, s.biz_date 영업일, s.merchant_id 가맹점번호, m.name 상호, s.net 지급액, s.status 상태, to_char(s.settled_at,'MM-DD HH24:MI') 지급시각 from settlement s join merchant m on m.merchant_id=s.merchant_id where s.biz_date between '${f.from}' and '${f.to}' order by s.settlement_id desc limit 500`},
|
||||||
|
{cd:'QR0301', nm:'가맹점 관리', merch:true},
|
||||||
|
{cd:'QR0401', nm:'일자별 실적 집계', filters:[['from','시작일','2026-06-20'],['to','종료일','2026-07-19']],
|
||||||
|
sql:f=>`select biz_date 영업일, count(*) 건수, sum(amount) 매입금액, sum(fee) 수수료, sum(net) 정산액, count(*) filter (where status='S') 정산완료, count(*) filter (where status in ('A','M')) 진행중 from purchase where biz_date between '${f.from}' and '${f.to}' group by biz_date order by biz_date desc limit 200`},
|
||||||
|
{cd:'QR0501', nm:'원장 조회', filters:[['from','시작일','2026-07-10'],['to','종료일','2026-07-19']],
|
||||||
|
sql:f=>`select l.ledger_id 전표번호, l.biz_date 영업일, l.entry_type 유형, l.purchase_id 매입번호, l.amount 금액 from ledger l where l.biz_date between '${f.from}' and '${f.to}' order by l.ledger_id desc limit 500`},
|
||||||
|
{cd:'QR0601', nm:'채널·발급사 분석', filters:[['from','시작일','2026-07-01'],['to','종료일','2026-07-19']],
|
||||||
|
sql:f=>`select channel 채널, issuer 발급사, count(*) 건수, sum(amount) 매입금액, round(avg(amount)) 평균금액 from purchase where biz_date between '${f.from}' and '${f.to}' group by channel, issuer order by 4 desc limit 200`},
|
||||||
|
];
|
||||||
|
|
||||||
|
function renderQueryIn(pane, q){
|
||||||
|
const fid = 'qf' + q.cd;
|
||||||
|
const f = (q.filters||[]).map(([id,label,dv]) =>
|
||||||
|
`<tr><th>${label}</th><td><input type="text" id="${fid}_${id}" value="${dv}"></td></tr>`).join('');
|
||||||
|
pane.innerHTML = `
|
||||||
|
<div class="panel"><h3>${q.cd} · ${q.nm}${q.detail?' — <span style="font-weight:normal;color:#667">행 클릭 = 상세 팝업(승인·정산·원장 연계, 보류/취소 처리)</span>':''}</h3><div class="bd">
|
||||||
|
<table class="frm">${f||'<tr><th>조건</th><td style="color:#889">전체 조회</td></tr>'}</table>
|
||||||
|
<div class="btnrow"><button class="pri" id="${fid}_run">조회</button></div>
|
||||||
|
</div></div>
|
||||||
|
<div class="panel"><h3>조회 결과</h3><div class="bd" id="${fid}_grid" style="overflow:auto">조회 중 ...</div></div>`;
|
||||||
|
const run = async () => {
|
||||||
|
const fv = {}; (q.filters||[]).forEach(([id]) => { fv[id] = document.getElementById(`${fid}_${id}`).value || ''; });
|
||||||
|
setStatus('조회 중 ...');
|
||||||
|
const j = await runQuery(q.sql(fv));
|
||||||
|
grid(document.getElementById(`${fid}_grid`), j, {
|
||||||
|
name: q.cd, psz: 15,
|
||||||
|
onRow: q.detail ? (r => openPurchaseDetail(r[0])) : null,
|
||||||
|
});
|
||||||
|
setStatus(j.ok ? `${q.nm} — ${j.rows.length}건 조회` : '조회 실패', !j.ok);
|
||||||
|
};
|
||||||
|
document.getElementById(`${fid}_run`).onclick = run;
|
||||||
|
run();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ───────── 운영 대시보드 ───────── */
|
||||||
|
async function renderDashIn(pane){
|
||||||
|
pane.innerHTML = `
|
||||||
|
<div class="panel"><h3>HM0001 · 운영 대시보드 — 글로벌 매입 현황</h3><div class="bd"><div class="cards" id="dcards">로딩...</div></div></div>
|
||||||
|
<div class="panel"><h3>최근 매입 거래 <span style="font-weight:normal;color:#667">— 행 클릭 시 상세</span></h3><div class="bd" id="drecent" style="overflow:auto">로딩...</div></div>
|
||||||
|
<div style="display:flex; gap:12px">
|
||||||
|
<div class="panel" style="flex:1"><h3>당월 가맹점 TOP 7</h3><div class="bd" id="dtop">로딩...</div></div>
|
||||||
|
<div class="panel" style="flex:1"><h3>최근 7영업일 실적</h3><div class="bd" id="ddaily">로딩...</div></div>
|
||||||
|
</div>`;
|
||||||
|
const k = await runQuery(`select to_char((select max(biz_date) from purchase),'YYYY-MM-DD') 기준일,(select count(*) from purchase where biz_date=(select max(biz_date) from purchase)) 오늘건수,(select coalesce(sum(amount),0) from purchase where biz_date=(select max(biz_date) from purchase)) 오늘매입액,(select count(*) from purchase where date_trunc('month',biz_date)=date_trunc('month',(select max(biz_date) from purchase))) 당월건수,(select coalesce(sum(amount),0) from purchase where date_trunc('month',biz_date)=date_trunc('month',(select max(biz_date) from purchase))) 당월매입액,(select count(*) from merchant where status='ACTIVE') 활성가맹점,(select count(*) from purchase where status in ('A','M','H')) 미정산건`);
|
||||||
|
document.getElementById('dcards').innerHTML = k.ok
|
||||||
|
? k.cols.map((n,i)=>`<div class="card"><div class="cl">${n}</div><div class="cv">${fmtN(k.rows[0][i])}</div></div>`).join('')
|
||||||
|
: `<span class="err">${k.error}</span>`;
|
||||||
|
const r = await runQuery(`select p.purchase_id 매입번호, p.biz_date 영업일, m.name 가맹점, p.amount 금액, p.fee 수수료, p.status 상태, p.channel 채널, p.issuer 발급사 from purchase p join merchant m on m.merchant_id=p.merchant_id order by p.purchase_id desc limit 40`);
|
||||||
|
grid(document.getElementById('drecent'), r, {psz:10, name:'recent', onRow: rr => openPurchaseDetail(rr[0])});
|
||||||
|
const t = await runQuery(`select m.name 가맹점, count(*) 건수, sum(p.amount) 매입금액 from purchase p join merchant m on m.merchant_id=p.merchant_id where date_trunc('month',p.biz_date)=date_trunc('month',(select max(biz_date) from purchase)) group by m.name order by 3 desc limit 7`);
|
||||||
|
grid(document.getElementById('dtop'), t, {psz:7});
|
||||||
|
const d = await runQuery(`select biz_date 영업일, count(*) 건수, sum(amount) 매입금액, sum(net) 정산액 from purchase group by biz_date order by biz_date desc limit 7`);
|
||||||
|
grid(document.getElementById('ddaily'), d, {psz:7});
|
||||||
|
setStatus('대시보드 로드 완료 — 운영 데이터 기준');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ───────── 가맹점 관리 (마스터-디테일 + CRUD tpcall) ───────── */
|
||||||
|
async function renderMerchIn(pane){
|
||||||
|
pane.innerHTML = `
|
||||||
|
<div style="display:flex; gap:12px">
|
||||||
|
<div class="panel" style="flex:1.3"><h3>QR0301 · 가맹점 목록 <span style="font-weight:normal;color:#667">— 행 클릭 = 상세</span></h3>
|
||||||
|
<div class="bd" id="mgrid" style="overflow:auto">로딩...</div></div>
|
||||||
|
<div style="flex:1">
|
||||||
|
<div class="panel"><h3>가맹점 상세 / 등록·수정 (tpcall · XA)</h3><div class="bd">
|
||||||
|
<table class="frm">
|
||||||
|
<tr><th>가맹점번호<span class="fldcd">T_MERCHANT</span></th><td><input type="text" id="mf_id" value=""></td></tr>
|
||||||
|
<tr><th>상호<span class="fldcd">T_STR1</span></th><td><input type="text" id="mf_nm" value=""></td></tr>
|
||||||
|
<tr><th>업종/구분<span class="fldcd">T_STR2</span></th><td><input type="text" id="mf_cat" value="일반"></td></tr>
|
||||||
|
<tr><th>정산계좌<span class="fldcd">T_STR3</span></th><td><input type="text" id="mf_acct" value="110-234-567890"></td></tr>
|
||||||
|
<tr><th>상태<span class="fldcd">T_STATUS</span></th><td><select id="mf_st"><option>ACTIVE</option><option>HOLD</option><option>PENDING</option><option>CLOSED</option></select></td></tr>
|
||||||
|
</table>
|
||||||
|
<div class="btnrow">
|
||||||
|
<button class="pri" onclick="merchReg()">신규등록 (MM_MERCH_REG)</button>
|
||||||
|
<button onclick="merchUpd()">정보수정 (MM_MERCH_UPD)</button>
|
||||||
|
<button class="warn" onclick="merchSt()">상태변경 (MM_MERCH_STATUS)</button>
|
||||||
|
</div>
|
||||||
|
</div></div>
|
||||||
|
<div class="panel"><h3>확장 마스터 / 변경 이력</h3><div class="bd" id="mhist">가맹점을 선택하세요</div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel"><h3>거래 저널</h3><div class="bd"><table class="grd jgrid"></table></div></div>`;
|
||||||
|
renderJournalIn(pane.querySelector('.jgrid'));
|
||||||
|
loadMerchGrid();
|
||||||
|
}
|
||||||
|
async function loadMerchGrid(){
|
||||||
|
const j = await runQuery(`select m.merchant_id 가맹점번호, m.name 상호, m.mdr_bps "MDR(bp)", m.daily_limit 일한도, m.status 상태, coalesce(t.cnt,0) 누적건수, coalesce(t.amt,0) 누적매입액 from merchant m left join (select merchant_id, count(*) cnt, sum(amount) amt from purchase group by merchant_id) t on t.merchant_id=m.merchant_id order by m.merchant_id limit 300`);
|
||||||
|
const el = document.getElementById('mgrid');
|
||||||
|
if (el) grid(el, j, {psz:14, name:'merchants', onRow: r => selectMerch(r)});
|
||||||
|
}
|
||||||
|
async function selectMerch(r){
|
||||||
|
document.getElementById('mf_id').value = r[0];
|
||||||
|
document.getElementById('mf_nm').value = r[1];
|
||||||
|
document.getElementById('mf_st').value = STMAP[r[4]] ? r[4] : 'ACTIVE';
|
||||||
|
const h = await runQuery(`select 'EXT' 구분, coalesce(biz_name,'-') 내용1, coalesce(settle_acct,'-') 내용2, coalesce(to_char(updated_at,'MM-DD HH24:MI'),'') 시각 from mm_merchant_ext where merchant_id='${r[0]}' union all select 'HIST', coalesce(chg_type,'-'), coalesce(chg_detail,'-'), to_char(chg_at,'MM-DD HH24:MI') from mm_merch_hist where merchant_id='${r[0]}' limit 20`);
|
||||||
|
const el = document.getElementById('mhist');
|
||||||
|
if (h.ok && h.rows.length) grid(el, h, {psz:8});
|
||||||
|
else el.innerHTML = '<span style="color:#889">확장 마스터/이력 없음 — 신규등록으로 생성</span>';
|
||||||
|
}
|
||||||
|
const mf = k => document.getElementById('mf_'+k).value.trim();
|
||||||
|
async function merchReg(){
|
||||||
|
const j = await callSvc('MM_MERCH_REG', { T_MERCHANT: mf('id'), T_STR1: mf('nm'), T_STR2: mf('cat'), T_STR3: mf('acct') }, true);
|
||||||
|
jlog('QR0301','MM_MERCH_REG',true,j); alert(j.ok?'등록 완료':'실패: '+j.error); loadMerchGrid(); selectMerch([mf('id'), mf('nm'),'','','ACTIVE']);
|
||||||
|
}
|
||||||
|
async function merchUpd(){
|
||||||
|
const j = await callSvc('MM_MERCH_UPD', { T_MERCHANT: mf('id'), T_STR1: mf('nm'), T_STR3: mf('acct') }, true);
|
||||||
|
jlog('QR0301','MM_MERCH_UPD',true,j); alert(j.ok?'수정 완료':'실패: '+j.error); loadMerchGrid(); selectMerch([mf('id'), mf('nm'),'','','ACTIVE']);
|
||||||
|
}
|
||||||
|
async function merchSt(){
|
||||||
|
const j = await callSvc('MM_MERCH_STATUS', { T_MERCHANT: mf('id'), T_STATUS: document.getElementById('mf_st').value }, true);
|
||||||
|
jlog('QR0301','MM_MERCH_STATUS',true,j); alert(j.ok?'상태변경 완료':'실패: '+j.error); loadMerchGrid();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ───────── 서비스 실행 화면 (330종) ───────── */
|
||||||
|
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 + QSCREENS.length;
|
||||||
|
function scrCode(s){ const i = byMod[s.mod].indexOf(s); return s.mod.toUpperCase() + String(i+1).padStart(2,'0') + '01'; }
|
||||||
|
|
||||||
|
function renderSvcIn(pane, s, nm, note){
|
||||||
|
const cd = scrCode(s), fid = 'sf' + s.svc;
|
||||||
|
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="${fid}_${i}" data-fld="${f}" value="${dflt(f)}"></td></tr>`).join('')
|
||||||
|
|| `<tr><th>입력</th><td style="color:#889">입력 필드 없음 — 즉시 실행</td></tr>`;
|
||||||
|
pane.innerHTML = `
|
||||||
|
<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">
|
||||||
|
<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="${fid}_tx" ${useTx?'checked':''}> tpbegin / tpcommit (2PC)</label></td></tr>
|
||||||
|
</table>
|
||||||
|
<div style="margin-top:6px;color:#889">응답 필드: ${s.out.length ? s.out.map(f=>LBL[f]||f).join(', ') : '-'}</div>
|
||||||
|
<div class="btnrow"><button class="pri" id="${fid}_run">실행 (tpcall)</button></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel"><h3>처리 결과 (응답 전문 UBF)</h3>
|
||||||
|
<div class="bd"><table class="grd" id="${fid}_res"><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><div class="bd"><table class="grd jgrid"></table></div></div>`;
|
||||||
|
renderJournalIn(pane.querySelector('.jgrid'));
|
||||||
|
document.getElementById(`${fid}_run`).onclick = async () => {
|
||||||
|
const fields = {};
|
||||||
|
s.in.forEach((f,i) => { const v = document.getElementById(`${fid}_${i}`).value; if (v!=='') fields[f]=v; });
|
||||||
|
const tx = document.getElementById(`${fid}_tx`).checked;
|
||||||
|
setStatus(`tpcall("${s.svc}") 호출 중 ...`);
|
||||||
|
const j = await callSvc(s.svc, fields, tx);
|
||||||
|
const g = document.getElementById(`${fid}_res`);
|
||||||
|
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>${LBL[k.replace('#','')]||k} <span class="fldcd">${k}</span></td><td class="num">${k==='T_STATUS'?badge(v):fmtN(v)}</td></tr>`;
|
||||||
|
setStatus(`tpcall("${s.svc}") 정상 처리${tx?' · XA tpcommit(2PC)':''}`);
|
||||||
|
} else {
|
||||||
|
g.innerHTML += `<tr><td>오류</td><td class="err">${j.error||'unknown'}</td></tr>`;
|
||||||
|
setStatus(`tpcall("${s.svc}") 실패`, true);
|
||||||
|
}
|
||||||
|
jlog(cd, s.svc, tx, j);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ───────── 메뉴 ───────── */
|
||||||
|
const menuEl = document.getElementById('menu');
|
||||||
|
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(({cd, nm, key, open: openFn}) => {
|
||||||
|
const it = document.createElement('div'); it.className = 'itm';
|
||||||
|
it.innerHTML = `<span class="cd">${cd}</span>${nm}`;
|
||||||
|
it.dataset.k = (cd+' '+nm+' '+key).toLowerCase();
|
||||||
|
it.onclick = e => { e.stopPropagation(); openFn(); };
|
||||||
|
wrap.appendChild(it);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const openQueryTab = q => openTab('q:'+q.cd, q.nm, pane =>
|
||||||
|
q.dash ? renderDashIn(pane) : q.merch ? renderMerchIn(pane) : renderQueryIn(pane, q));
|
||||||
|
const openSvcTab = (s, nm, note) => openTab('s:'+s.svc, nm || s.svc, pane => renderSvcIn(pane, s, nm, note));
|
||||||
|
|
||||||
|
addGroup('조회/현황 (운영)', QSCREENS.map(q => ({cd:q.cd, nm:q.nm, key:q.cd, open:()=>openQueryTab(q)})), true);
|
||||||
const FAVS = [
|
const FAVS = [
|
||||||
{ svc:"ACQUIRE", nm:"매입 접수(정산체인)", note:"글로벌 XA: ACQUIRE→RECONCILE→SETTLE 체인 후 2PC 커밋" },
|
{ svc:"ACQUIRE", nm:"매입 접수(정산체인)", note:"글로벌 XA: ACQUIRE→RECONCILE→SETTLE 체인 후 2PC 커밋" },
|
||||||
{ svc:"ACQ_STATUS", nm:"매입 상태조회" },
|
{ svc:"ACQ_STATUS", nm:"매입 상태조회" },
|
||||||
|
|
@ -4436,137 +4809,12 @@ const FAVS = [
|
||||||
{ svc:"ST_MDR", nm:"MDR 수수료 산출" },
|
{ svc:"ST_MDR", nm:"MDR 수수료 산출" },
|
||||||
{ svc:"LG_BALCHK", nm:"원장 잔액검증" },
|
{ svc:"LG_BALCHK", nm:"원장 잔액검증" },
|
||||||
{ svc:"CL_DAILY", nm:"일마감 처리" },
|
{ svc:"CL_DAILY", nm:"일마감 처리" },
|
||||||
{ svc:"MM_MERCH_INQ", nm:"가맹점 조회" },
|
|
||||||
{ svc:"CM_LUHN", nm:"카드번호 검증(Luhn)" },
|
{ svc:"CM_LUHN", nm:"카드번호 검증(Luhn)" },
|
||||||
];
|
];
|
||||||
|
addGroup('★ 주요 업무', FAVS.filter(f=>bySvc[f.svc]).map(f=>({cd:scrCode(bySvc[f.svc]), nm:f.nm, key:f.svc, open:()=>openSvcTab(bySvc[f.svc], f.nm, f.note)})), true);
|
||||||
|
Object.keys(byMod).sort().forEach(m =>
|
||||||
|
addGroup(`${byMod[m][0].modnm} (${m}) 업무`, byMod[m].map(s=>({cd:scrCode(s), nm:s.svc, key:s.svc+' '+s.desc, open:()=>openSvcTab(s)})), false));
|
||||||
|
|
||||||
/* ───────── 메뉴 구성: 즐겨찾기 + 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;
|
|
||||||
|
|
||||||
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';
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* ───────── 조회/현황 화면 (운영 데이터 그리드 — /api/query) ───────── */
|
|
||||||
let CURQ = null;
|
|
||||||
const fmtN = v => (v===null||v===undefined||v==='') ? '' : (/^-?\d+$/.test(String(v)) ? Number(v).toLocaleString('ko-KR') : v);
|
|
||||||
async function runQuery(sql){
|
|
||||||
try { return await (await fetch('api/query?q='+encodeURIComponent(sql))).json(); }
|
|
||||||
catch(e){ return {ok:false, error:String(e)}; }
|
|
||||||
}
|
|
||||||
const QSCREENS = [
|
|
||||||
{cd:'HM0001', nm:'운영 대시보드', dash:true},
|
|
||||||
{cd:'QR0101', nm:'매입 목록 조회', filters:[['from','시작일','2026-07-01'],['to','종료일','2026-07-19'],['merch','가맹점번호(선택)','']],
|
|
||||||
sql:f=>`select p.purchase_id 매입번호, p.biz_date 영업일, p.merchant_id 가맹점, m.name 상호, p.amount 매입금액, p.fee 수수료, p.net 정산액, p.status 상태, p.channel 채널, p.issuer 발급사 from purchase p join merchant m on m.merchant_id=p.merchant_id where p.biz_date between '${f.from}' and '${f.to}' ${f.merch?`and p.merchant_id='${f.merch}'`:''} order by p.purchase_id desc limit 200`},
|
|
||||||
{cd:'QR0201', nm:'정산 목록 조회', filters:[['from','시작일','2026-07-01'],['to','종료일','2026-07-19']],
|
|
||||||
sql:f=>`select s.settlement_id 정산번호, s.biz_date 영업일, s.merchant_id 가맹점, m.name 상호, s.net 지급액, s.status 상태, to_char(s.settled_at,'MM-DD HH24:MI') 지급시각 from settlement s join merchant m on m.merchant_id=s.merchant_id where s.biz_date between '${f.from}' and '${f.to}' order by s.settlement_id desc limit 200`},
|
|
||||||
{cd:'QR0301', nm:'가맹점 현황', filters:[],
|
|
||||||
sql:f=>`select m.merchant_id 가맹점번호, m.name 상호, m.mdr_bps "MDR(bp)", m.daily_limit 일한도, m.status 상태, coalesce(t.cnt,0) 누적건수, coalesce(t.amt,0) 누적매입액 from merchant m left join (select merchant_id, count(*) cnt, sum(amount) amt from purchase group by merchant_id) t on t.merchant_id=m.merchant_id order by m.merchant_id limit 200`},
|
|
||||||
{cd:'QR0401', nm:'일자별 실적 집계', filters:[['from','시작일','2026-06-20'],['to','종료일','2026-07-19']],
|
|
||||||
sql:f=>`select biz_date 영업일, count(*) 건수, sum(amount) 매입금액, sum(fee) 수수료, sum(net) 정산액, count(*) filter (where status='S') 정산완료, count(*) filter (where status in ('A','M')) 진행중 from purchase where biz_date between '${f.from}' and '${f.to}' group by biz_date order by biz_date desc limit 200`},
|
|
||||||
{cd:'QR0501', nm:'원장 조회', filters:[['from','시작일','2026-07-10'],['to','종료일','2026-07-19']],
|
|
||||||
sql:f=>`select l.ledger_id 전표번호, l.biz_date 영업일, l.entry_type 유형, l.purchase_id 매입번호, l.amount 금액 from ledger l where l.biz_date between '${f.from}' and '${f.to}' order by l.ledger_id desc limit 200`},
|
|
||||||
{cd:'QR0601', nm:'채널·발급사 분석', filters:[['from','시작일','2026-07-01'],['to','종료일','2026-07-19']],
|
|
||||||
sql:f=>`select channel 채널, issuer 발급사, count(*) 건수, sum(amount) 매입금액, round(avg(amount)) 평균금액 from purchase where biz_date between '${f.from}' and '${f.to}' group by channel, issuer order by 4 desc limit 200`},
|
|
||||||
];
|
|
||||||
function gridHtml(j){
|
|
||||||
if (!j.rows.length) return '<span style="color:#889">데이터 없음</span>';
|
|
||||||
return '<table class="grd"><tr>' + j.cols.map(c=>`<th>${c}</th>`).join('') + '</tr>' +
|
|
||||||
j.rows.map(r=>'<tr>'+r.map(v=>`<td class="${/^-?\d+$/.test(v)?'num':''}">${fmtN(v)}</td>`).join('')+'</tr>').join('') + '</table>';
|
|
||||||
}
|
|
||||||
async function openQuery(q, itEl){
|
|
||||||
CUR = null; CURQ = q;
|
|
||||||
document.querySelectorAll('#menu .itm').forEach(e=>e.classList.remove('on'));
|
|
||||||
if (itEl) itEl.classList.add('on');
|
|
||||||
document.getElementById('tabname').textContent = q.nm;
|
|
||||||
document.getElementById('scrno').textContent = q.cd;
|
|
||||||
if (q.dash) return renderDash();
|
|
||||||
const f = (q.filters||[]).map(([id,label,dv]) =>
|
|
||||||
`<tr><th>${label}</th><td><input type="text" id="qf_${id}" value="${dv}"></td></tr>`).join('');
|
|
||||||
work.innerHTML = `
|
|
||||||
<div class="panel"><h3>${q.cd} · ${q.nm}</h3><div class="bd">
|
|
||||||
<table class="frm">${f||'<tr><th>조건</th><td style="color:#889">전체 조회</td></tr>'}</table>
|
|
||||||
<div class="btnrow"><button class="pri" onclick="runQ()">조회</button></div>
|
|
||||||
</div></div>
|
|
||||||
<div class="panel"><h3>조회 결과</h3><div class="bd" id="qgrid" style="overflow:auto">조회 중 ...</div></div>`;
|
|
||||||
runQ();
|
|
||||||
}
|
|
||||||
async function runQ(){
|
|
||||||
const q = CURQ, f = {};
|
|
||||||
(q.filters||[]).forEach(([id]) => { f[id] = (document.getElementById('qf_'+id)||{}).value || ''; });
|
|
||||||
setStatus('조회 중 ...');
|
|
||||||
const j = await runQuery(q.sql(f));
|
|
||||||
const g = document.getElementById('qgrid');
|
|
||||||
if (!j.ok) { g.innerHTML = `<span class="err">${j.error}</span>`; setStatus('조회 실패', true); return; }
|
|
||||||
g.innerHTML = gridHtml(j);
|
|
||||||
setStatus(`조회 완료 — ${j.rows.length}건`);
|
|
||||||
}
|
|
||||||
async function renderDash(){
|
|
||||||
work.innerHTML = `
|
|
||||||
<div class="panel"><h3>HM0001 · 운영 대시보드 — 글로벌 매입 현황</h3><div class="bd"><div class="cards" id="cards">로딩...</div></div></div>
|
|
||||||
<div class="panel"><h3>최근 매입 거래</h3><div class="bd" id="d_recent" style="overflow:auto">로딩...</div></div>
|
|
||||||
<div style="display:flex; gap:12px">
|
|
||||||
<div class="panel" style="flex:1"><h3>당월 가맹점 TOP 7</h3><div class="bd" id="d_top">로딩...</div></div>
|
|
||||||
<div class="panel" style="flex:1"><h3>최근 7영업일 실적</h3><div class="bd" id="d_daily">로딩...</div></div>
|
|
||||||
</div>`;
|
|
||||||
const k = await runQuery(`select to_char((select max(biz_date) from purchase),'YYYY-MM-DD') 기준일,(select count(*) from purchase where biz_date=(select max(biz_date) from purchase)) 오늘건수,(select coalesce(sum(amount),0) from purchase where biz_date=(select max(biz_date) from purchase)) 오늘매입액,(select count(*) from purchase where date_trunc('month',biz_date)=date_trunc('month',(select max(biz_date) from purchase))) 당월건수,(select coalesce(sum(amount),0) from purchase where date_trunc('month',biz_date)=date_trunc('month',(select max(biz_date) from purchase))) 당월매입액,(select count(*) from merchant where status='ACTIVE') 활성가맹점,(select count(*) from purchase where status in ('A','M','H')) 미정산건`);
|
|
||||||
if (k.ok) {
|
|
||||||
document.getElementById('cards').innerHTML =
|
|
||||||
k.cols.map((n,i)=>`<div class="card"><div class="cl">${n}</div><div class="cv">${fmtN(k.rows[0][i])}</div></div>`).join('');
|
|
||||||
} else document.getElementById('cards').innerHTML = `<span class="err">${k.error}</span>`;
|
|
||||||
const r = await runQuery(`select p.purchase_id 매입번호, p.biz_date 영업일, m.name 가맹점, p.amount 금액, p.fee 수수료, p.status 상태, p.channel 채널, p.issuer 발급사 from purchase p join merchant m on m.merchant_id=p.merchant_id order by p.purchase_id desc limit 12`);
|
|
||||||
if (r.ok) document.getElementById('d_recent').innerHTML = gridHtml(r);
|
|
||||||
const t = await runQuery(`select m.name 가맹점, count(*) 건수, sum(p.amount) 매입금액 from purchase p join merchant m on m.merchant_id=p.merchant_id where date_trunc('month',p.biz_date)=date_trunc('month',(select max(biz_date) from purchase)) group by m.name order by 3 desc limit 7`);
|
|
||||||
if (t.ok) document.getElementById('d_top').innerHTML = gridHtml(t);
|
|
||||||
const d = await runQuery(`select biz_date 영업일, count(*) 건수, sum(amount) 매입금액, sum(net) 정산액 from purchase group by biz_date order by biz_date desc limit 7`);
|
|
||||||
if (d.ok) document.getElementById('d_daily').innerHTML = gridHtml(d);
|
|
||||||
setStatus('대시보드 로드 완료 — 운영 데이터 기준');
|
|
||||||
}
|
|
||||||
function addQueryGroup(){
|
|
||||||
const gd = document.createElement('div'); gd.className = 'grp open';
|
|
||||||
gd.innerHTML = `조회/현황 (운영)<span class="cnt">${QSCREENS.length}</span>`;
|
|
||||||
const wrap = document.createElement('div'); wrap.className = 'itms';
|
|
||||||
gd.onclick = () => gd.classList.toggle('open');
|
|
||||||
menuEl.appendChild(gd); menuEl.appendChild(wrap);
|
|
||||||
QSCREENS.forEach(q => {
|
|
||||||
const it = document.createElement('div'); it.className = 'itm';
|
|
||||||
it.innerHTML = `<span class="cd">${q.cd}</span>${q.nm}`;
|
|
||||||
it.dataset.k = (q.cd+' '+q.nm).toLowerCase();
|
|
||||||
it.onclick = e => { e.stopPropagation(); openQuery(q, it); };
|
|
||||||
wrap.appendChild(it);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
addQueryGroup();
|
|
||||||
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
setTimeout(()=>openQuery(QSCREENS[0], document.querySelector('#menu .itms .itm')), 0);
|
|
||||||
|
|
||||||
/* 검색 */
|
|
||||||
document.getElementById('q').addEventListener('input', e => {
|
document.getElementById('q').addEventListener('input', e => {
|
||||||
const q = e.target.value.trim().toLowerCase();
|
const q = e.target.value.trim().toLowerCase();
|
||||||
document.querySelectorAll('#menu .itms').forEach(w => w.classList.toggle('show', !!q));
|
document.querySelectorAll('#menu .itms').forEach(w => w.classList.toggle('show', !!q));
|
||||||
|
|
@ -4574,92 +4822,13 @@ document.getElementById('q').addEventListener('input', e => {
|
||||||
it.style.display = (!q || it.dataset.k.includes(q)) ? (q ? 'block' : '') : 'none');
|
it.style.display = (!q || it.dataset.k.includes(q)) ? (q ? 'block' : '') : 'none');
|
||||||
});
|
});
|
||||||
|
|
||||||
/* ───────── 화면 렌더 ───────── */
|
/* 시작: 대시보드 (+ 해시로 특정 화면: #q=QR0101 / #s=ACQUIRE / #m=1 가맹점관리) */
|
||||||
const work = document.getElementById('work');
|
setTimeout(() => {
|
||||||
const journal = [];
|
openQueryTab(QSCREENS[0]);
|
||||||
|
const h = location.hash;
|
||||||
function openScreen(s, itEl, nm, note) {
|
if (h.startsWith('#q=')) { const q = QSCREENS.find(x=>x.cd===h.slice(3)); if (q) openQueryTab(q); }
|
||||||
CUR = { s, nm: nm || s.svc };
|
if (h.startsWith('#s=')) { const s = bySvc[h.slice(3)]; if (s) openSvcTab(s); }
|
||||||
document.querySelectorAll('#menu .itm').forEach(e=>e.classList.remove('on'));
|
}, 0);
|
||||||
if (itEl) itEl.classList.add('on');
|
|
||||||
const cd = scrCode(s);
|
|
||||||
document.getElementById('tabname').textContent = `${nm || s.svc}`;
|
|
||||||
document.getElementById('scrno').textContent = cd;
|
|
||||||
|
|
||||||
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>${cd} · ${nm || s.svc} — <span style="font-weight:normal;color:#667">${s.desc || note || ''}</span></h3>
|
|
||||||
<div class="bd">
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="panel"><h3>처리 결과 (응답 전문 UBF)</h3>
|
|
||||||
<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>
|
|
||||||
<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(){ const on = document.querySelector('#menu .itm.on'); openScreen(CUR.s, on, CUR.nm); }
|
|
||||||
|
|
||||||
async function execute() {
|
|
||||||
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("${s.svc}") 호출 중 ...`);
|
|
||||||
let j;
|
|
||||||
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: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>${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("${s.svc}") 실패`, true);
|
|
||||||
}
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
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>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue