feat(frontend): 포탈·운영·관리자 콘솔 및 매뉴얼·거래 타임라인·전문탭·서비스 제어 UI

- 콘솔을 포탈/운영/관리자 3뷰로 분리(Portal·AdminPanel·AdminLogin·auth·util), ADMIN JWT 로그인 게이트
- 상단 우측 '서비스 매뉴얼'(public/manual.html) 링크, 로그인 화면 불필요 계좌조회 에러 제거
- 거래 조회: 서비스별 처리시각 5단계 타임라인 + pacs.008/pacs.002 전문 탭(/notifications 프록시)
- 관리자: 서비스 제어 패널(기동/중지/재기동/로그, 4초 폴링, 인프라 상태 포함)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
rtgs
2026-07-16 13:41:35 +09:00
parent 1180013506
commit c96664e52d
8 changed files with 933 additions and 167 deletions

334
frontend/public/manual.html Normal file
View File

@@ -0,0 +1,334 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>RTGS 포탈 · 서비스 매뉴얼</title>
<style>
:root {
--gucci: #6b4fbb; --chanel: #0b63c4; --hermes: #c77d0a;
--dior: #8a4fd0; --prada: #b8860b; --lv: #1a7f37; --seq: #444;
--ink: #222; --muted: #666; --line: #e2e2e2; --bg: #f4f7fb;
}
* { box-sizing: border-box; }
body {
font-family: system-ui, "Segoe UI", "Malgun Gothic", sans-serif;
color: var(--ink); line-height: 1.65; margin: 0;
background: #fafbfc;
}
.wrap { max-width: 960px; margin: 0 auto; padding: 32px 24px 80px; }
header.doc {
background: linear-gradient(135deg, #1a2a44, #2c4468);
color: #fff; border-radius: 12px; padding: 28px 30px; margin-bottom: 8px;
}
header.doc h1 { margin: 0 0 6px; font-size: 26px; }
header.doc p { margin: 0; color: #cdd8ea; font-size: 14px; }
.toc {
background: #fff; border: 1px solid var(--line); border-radius: 10px;
padding: 16px 22px; margin: 20px 0 8px; font-size: 14px;
}
.toc b { display: block; margin-bottom: 8px; color: var(--muted); font-size: 12px; letter-spacing: .04em; text-transform: uppercase; }
.toc a { color: var(--chanel); text-decoration: none; margin-right: 16px; white-space: nowrap; line-height: 2; }
.toc a:hover { text-decoration: underline; }
h2 {
font-size: 20px; margin: 40px 0 14px; padding-bottom: 8px;
border-bottom: 2px solid #eceff3;
}
h3 { font-size: 16px; margin: 22px 0 8px; }
p, li { font-size: 15px; }
code {
background: #eef1f5; padding: 1px 6px; border-radius: 4px;
font-family: ui-monospace, "Cascadia Code", Consolas, monospace; font-size: 13px;
}
.lead { color: var(--muted); font-size: 15px; }
/* 상태 범례 */
.legend { display: flex; gap: 20px; flex-wrap: wrap; background: var(--bg); border-radius: 10px; padding: 14px 18px; margin: 12px 0; }
.legend div { font-size: 14px; }
/* 흐름도 */
.flow { display: flex; align-items: stretch; gap: 4px; flex-wrap: wrap; margin: 14px 0; }
.flow .node {
flex: 1; min-width: 92px; border: 2px solid #ccd3dd; border-radius: 8px;
background: #fff; text-align: center; padding: 10px 6px; font-size: 13px;
}
.flow .node small { display: block; color: var(--muted); font-size: 11px; margin-top: 2px; }
.flow .arrow { align-self: center; color: #99a; font-size: 18px; flex: 0 0 auto; }
/* 서비스 카드 */
.svc {
border: 1px solid var(--line); border-top: 5px solid var(--bar, #888);
border-radius: 10px; padding: 20px 22px; margin: 18px 0;
background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,.04);
}
.svc h3 { margin-top: 0; font-size: 18px; display: flex; align-items: center; gap: 8px; }
.svc .tag {
font-size: 11px; font-weight: 600; color: #fff; background: var(--bar, #888);
padding: 2px 9px; border-radius: 20px; margin-left: auto;
}
.svc .sub { color: var(--muted); font-size: 13px; margin: -4px 0 12px; }
.meta-row { display: flex; flex-wrap: wrap; gap: 8px 18px; font-size: 13px; color: #555; margin-bottom: 10px; }
.meta-row span b { color: var(--ink); }
table { border-collapse: collapse; width: 100%; margin: 10px 0; font-size: 14px; }
th, td { border: 1px solid #e6e9ee; padding: 7px 11px; text-align: left; vertical-align: top; }
th { background: #f4f7fb; font-weight: 600; }
td code { white-space: nowrap; }
.metrics th:first-child, .metrics td:first-child { width: 34%; }
.note { background: #fffbe9; border: 1px solid #f0e2a8; border-radius: 8px; padding: 12px 16px; font-size: 14px; margin: 14px 0; }
.btn-chip { display: inline-block; border: 1px solid #ccc; border-radius: 6px; padding: 2px 10px; font-size: 13px; background: #fff; margin: 2px 4px 2px 0; }
footer { margin-top: 50px; padding-top: 18px; border-top: 1px solid var(--line); color: #999; font-size: 13px; }
</style>
</head>
<body>
<div class="wrap">
<header class="doc">
<h1>🏛 RTGS 포탈 서비스 매뉴얼</h1>
<p>소액 RTGS(실시간총액결제) 프로토타입 · 7개 서비스 안내 · Active-Active-Active 복원력 설계</p>
</header>
<nav class="toc">
<b>목차</b>
<a href="#intro">1. 포탈 개요</a>
<a href="#read">2. 화면 읽는 법</a>
<a href="#flow">3. 업무 흐름</a>
<a href="#services">4. 서비스별 설명</a>
<a href="#status">5. 거래 상태(TxSts)</a>
<a href="#ports">6. 포트 일람</a>
<a href="#glossary">7. 용어</a>
</nav>
<h2 id="intro">1. 포탈 개요</h2>
<p class="lead">
본 포탈은 소액 <b>RTGS(Real-Time Gross Settlement, 실시간총액결제)</b> 프로토타입의 운영·관리 콘솔입니다.
거래는 <b>단일 전역 순번기</b>가 입구에서 순번을 부여하고, 각 센터가 그 순번대로 동일하게 재생(결정론적 처리)하며,
<b>원장(PostgreSQL)</b>이 잔액을 강한 일관성으로 확정합니다. 3센터 <b>Active-Active-Active</b> 구성에서
정족수(2/3)와 펜싱으로 split-brain을 차단하는 것이 설계의 핵심입니다.
</p>
<p>
시스템은 명품 브랜드를 코드네임으로 쓰는 <b>7개 서비스</b>로 구성됩니다. 포탈 화면에는 이 중
주요 6개가 카드로 표시되고, 전역 순번기(Sequencer)는 업무 흐름도의 진입 노드로 나타납니다.
각 서비스가 무슨 일을 하고, 카드에 표시되는 숫자가 무엇을 뜻하는지 이 문서에서 설명합니다.
</p>
<h2 id="read">2. 화면 읽는 법</h2>
<h3>상태 표시등</h3>
<div class="legend">
<div>🟢 <b>UP</b> — 정상 가동(최근 하트비트 수신)</div>
<div>🟡 <b>STALE</b> — 하트비트 지연(응답이 오래됨, 점검 필요)</div>
<div>🔴 <b>DOWN</b> — 중단(하트비트 없음/도달 불가)</div>
</div>
<p>
상단 바의 <code>● n/7 UP</code>은 7개 서비스 중 정상 가동 중인 수를 나타냅니다.
각 서비스는 DB의 <code>service_heartbeat</code>에 주기적으로 살아있음을 기록하고(헤드리스 포함),
관리 서비스가 이를 읽어 UP/STALE/DOWN을 판정합니다.
</p>
<h3>자동 갱신 · 새로고침</h3>
<p>
<span class="btn-chip">자동갱신 5s</span> 체크 시 5초마다 상태·지표를 다시 불러옵니다.
<span class="btn-chip">새로고침</span>으로 즉시 갱신할 수도 있습니다.
카드의 처리 건수는 <b>DB 집계(원장)</b> 기준이라 관리자 콘솔에서 초기화하면 자연히 0으로 돌아갑니다.
</p>
<h2 id="flow">3. 업무 흐름</h2>
<p>한 건의 이체는 아래 순서로 처리됩니다. 각 노드는 담당 서비스이며, 포탈 흐름도에서 색 테두리로 실시간 상태를 함께 보여줍니다.</p>
<div class="flow">
<div class="node">🔢 Sequencer<small>전역순번</small></div>
<div class="arrow"></div>
<div class="node">🛡️ Gucci<small>관문 인증</small></div>
<div class="arrow"></div>
<div class="node">📥 Chanel<small>접수</small></div>
<div class="arrow"></div>
<div class="node">🔄 Dior<small>동기화</small></div>
<div class="arrow"></div>
<div class="node">💰 Hermes<small>결제</small></div>
<div class="arrow"></div>
<div class="node">✅ Prada<small>처리동기화</small></div>
<div class="arrow"></div>
<div class="node">📥 Chanel<small>결과통보</small></div>
</div>
<p class="lead">
전역순번(Sequencer) 부여 → 관문 인증(Gucci) → 접수(Chanel) → 저널/센터 동기화(Dior) →
결제·원장(Hermes) → 처리 동기화·완결(Prada) → 결과통보(Chanel).
</p>
<h2 id="services">4. 서비스별 설명</h2>
<!-- Sequencer -->
<div class="svc" style="--bar: var(--seq)">
<h3>🔢 Sequencer <span class="tag">전역 순번기 · Headless</span></h3>
<div class="sub">전역순번 부여 · 결정론적 값 확정 → 저널 발행</div>
<div class="meta-row"><span>논리포트 <b>8090</b></span><span>유형 <b>Headless JVM</b></span><span>Kafka: <code>rtgs.inbound</code><code>rtgs.journal</code></span></div>
<p>
시스템의 <b>단일 진입 순서 결정자</b>입니다. 접수된 요청에 전역순번(<code>global_seq</code>)을 부여해
단일 파티션 저널에 발행하면, 모든 센터가 이 순서를 그대로 재생해 동일한 결과를 만듭니다(결정론적 처리).
재기동 시 <code>MAX(global_seq)</code>에서 이어서 발번하므로 순번 유실이 없습니다.
</p>
<p class="lead">※ 순서 결정만 담당(상태 O). 분산·라우팅은 Gucci/GSLB(상태 X)가 맡아 역할을 분리합니다.
포탈에서는 별도 카드 없이 흐름도의 진입 노드로 표시됩니다.</p>
</div>
<!-- Gucci -->
<div class="svc" style="--bar: var(--gucci)">
<h3>🛡️ Gucci <span class="tag">게이트웨이 · 기관 인증</span></h3>
<div class="sub">외부 경계 관문 — 로그인 · JWT · API인증 · 유량제어 · 재전송차단 · 결과 콜백송부</div>
<div class="meta-row"><span>논리포트 <b>8095</b></span><span>유형 <b>Web(Tomcat)</b></span><span>코드네임 <b>Global User Communication Control Interface</b></span></div>
<p>
참가기관과 시스템 사이의 <b>대문</b>입니다. 로그인 후 JWT(HS256)를 발급하고, 모든 요청의 인증·인가
(역할 + 기관 바인딩)를 검사합니다. 기관별 유량제어로 코어·순번기를 보호하고(백프레셔),
nonce+timestamp로 재전송을 차단합니다. 처리 결과는 기관 콜백 URL로 재시도·ACK 방식으로 송부합니다.
</p>
<table class="metrics">
<tr><th>카드 지표</th><th>의미</th></tr>
<tr><td>코어(샤넬)</td><td>Gucci가 바라본 코어(Chanel) 연결 상태</td></tr>
<tr><td>등록기관</td><td>당좌계좌가 등록된 참가기관 수</td></tr>
</table>
<div>바로가기 <span class="btn-chip">감사로그(Kibana)</span> — 인증·유량·재전송 등 감사 이벤트를 Kibana에서 조회</div>
</div>
<!-- Chanel -->
<div class="svc" style="--bar: var(--chanel)">
<h3>📥 Chanel <span class="tag">접수</span></h3>
<div class="sub">pacs.008 접수 · XSD검증 · 경량화 · 원전문 저장 · 결과통보(pacs.002)</div>
<div class="meta-row"><span>논리포트 <b>8091</b></span><span>유형 <b>Web(Tomcat)</b></span><span>코드네임 <b>Contact Hub · Entry Liaison</b></span></div>
<p>
참가기관의 결제의뢰 전문(<b>ISO 20022 pacs.008</b>)을 받는 <b>접수 창구</b>입니다.
공식 XSD로 검증(XXE 차단)하고 원전문을 SHA-256 해시와 함께 저장한 뒤, 경량 명령문으로 만들어
입구 토픽에 발행합니다. 처리가 끝나면 <b>pacs.002</b> 결과통보 전문을 생성해 아웃박스에 적재합니다.
</p>
<table class="metrics">
<tr><th>카드 지표</th><th>의미</th></tr>
<tr><td>누적 접수</td><td>지금까지 접수된 거래 건수(원장 집계)</td></tr>
<tr><td>접수율</td><td>초당 접수 건수(TPS, 최근 갱신 구간 기준)</td></tr>
</table>
<div>바로가기 <span class="btn-chip">이체 신청/조회</span> — 운영 콘솔에서 pacs.008 신청 및 거래 상태 조회</div>
</div>
<!-- Dior -->
<div class="svc" style="--bar: var(--dior)">
<h3>🔄 Dior <span class="tag">센터간 접수 동기화</span></h3>
<div class="sub">저널 소비 → 원장 PDNG 기록(접수 동기화)</div>
<div class="meta-row"><span>논리포트 <b>8092</b></span><span>유형 <b>Headless JVM</b></span><span>코드네임 <b>Data Integration · Relay</b></span></div>
<p>
전역순번 저널을 소비해 원장에 거래를 <b>PDNG(대기)</b> 상태로 기록합니다. 결제(Hermes)와 병행해
저널을 읽으면서 <b>모든 센터의 접수 상태를 동일하게 맞추는</b> 동기화 역할을 합니다.
</p>
<table class="metrics">
<tr><th>카드 지표</th><th>의미</th></tr>
<tr><td>메모리</td><td>프로세스 사용 메모리(MB) — 런타임 자원 지표</td></tr>
<tr><td>가동</td><td>프로세스 가동 시간(초)</td></tr>
</table>
<div>바로가기 <span class="btn-chip">원장 보기</span> — 관리자 콘솔의 원장/거래 현황</div>
</div>
<!-- Hermes -->
<div class="svc" style="--bar: var(--hermes)">
<h3>💰 Hermes <span class="tag">결제 · 원장 엔진</span></h3>
<div class="sub">저널 순차소비 · 순서교정 · 선(先)저널 · 당좌 차/대변(강한 일관성) · ACSP</div>
<div class="meta-row"><span>논리포트 <b>8093</b></span><span>유형 <b>Headless JVM</b></span><span>코드네임 <b>Rapid Memory Execution · Settlement</b></span></div>
<p>
실제 <b>잔액을 움직이는 결제 엔진</b>입니다. 저널을 순번대로(동시성 1) 소비하며 순서를 교정하고,
잔액 연산 전에 <b>선저널(write-ahead)</b>로 순번을 확정한 뒤 송신·수신 기관 당좌계좌를 차/대변 처리합니다
(PostgreSQL 강한 일관성 트랜잭션 → 이중지급 0·총액 보존). 정산이 반영되면 <b>ACSP</b> 결과를 발행합니다.
선저널 덕분에 엔진이 죽었다 살아나도 Kafka 오프셋 재생으로 무손실 승계됩니다.
</p>
<table class="metrics">
<tr><th>카드 지표</th><th>의미</th></tr>
<tr><td>결제완료(ACCC)</td><td>최종 완결된 거래 건수(원장 집계)</td></tr>
<tr><td>평균 처리</td><td>정산 1건당 평균 소요시간(ms) — 런타임 지표</td></tr>
<tr><td>미결(대기)</td><td>접수됐으나 아직 완결·반려되지 않은 건수(전체 ACCC RJCT)</td></tr>
</table>
<div>바로가기 <span class="btn-chip">원장 보기</span> — 관리자 콘솔의 원장/잔액</div>
</div>
<!-- Prada -->
<div class="svc" style="--bar: var(--prada)">
<h3>✅ Prada <span class="tag">센터간 처리 동기화 · 완결</span></h3>
<div class="sub">결과 소비 · 완결 규율(과반) · ACCC 확정 · 조회사본 · 결과통보 발행</div>
<div class="meta-row"><span>논리포트 <b>8094</b></span><span>유형 <b>Headless JVM</b></span><span>코드네임 <b>Persistent Repository · Aggregation</b></span></div>
<p>
정산 결과를 소비해 <b>완결 규율</b>을 적용합니다. 과반(정족수) 센터가 동일 결과를 확정하면
최종 상태 <b>ACCC(입금처리완료)</b>로 확정하고 조회 전용 사본을 남깁니다. 완결이 커밋되면
결과통보(<code>rtgs.notify</code>)를 발행해 Chanel→Gucci를 통한 기관 통보로 이어집니다.
</p>
<table class="metrics">
<tr><th>카드 지표</th><th>의미</th></tr>
<tr><td>완결(ACCC)</td><td>최종 완결된 거래 건수</td></tr>
<tr><td>반려(RJCT)</td><td>검증 실패·잔액 부족 등으로 반려된 건수</td></tr>
</table>
<div>바로가기 <span class="btn-chip">완결/조회사본</span> — 관리자 콘솔의 완결 현황</div>
</div>
<!-- Louis Vuitton -->
<div class="svc" style="--bar: var(--lv)">
<h3>🛠️ Louis Vuitton <span class="tag">관리자 · 운영</span></h3>
<div class="sub">시스템관리 — DB초기화 · 코드 · 사용자권한 · 대사 대시보드</div>
<div class="meta-row"><span>논리포트 <b>8099</b></span><span>유형 <b>Web(Tomcat)</b></span><span>코드네임 <b>Leading Operations Unified Info Systems</b></span></div>
<p>
시스템 <b>관리·운영</b> 서비스입니다. DB 초기화, 상태코드·기관·사용자 권한 관리, 헬스(하트비트) 집계,
원장=저널=조회사본 <b>대사(對査) 대시보드</b>를 제공합니다. 스키마는 Flyway로 형상 관리하며 기동 시 자동 적용합니다.
(관리자 콘솔은 ADMIN JWT 필수, 테스트 계정 <code>a / 1</code>)
</p>
<table class="metrics">
<tr><th>카드 지표</th><th>의미</th></tr>
<tr><td>총 거래</td><td>원장에 기록된 전체 거래 건수</td></tr>
<tr><td>최대순번</td><td>부여된 전역순번의 최댓값(<code>MAX(global_seq)</code>)</td></tr>
</table>
<div>바로가기 <span class="btn-chip">관리자 콘솔</span> <span class="btn-chip">로그(Kibana)</span></div>
</div>
<h2 id="status">5. 거래 상태(TxSts) 전이</h2>
<p>한 건의 거래는 아래 상태를 따라 진행됩니다.</p>
<table>
<tr><th>상태</th><th>한글</th><th>담당</th><th>설명</th></tr>
<tr><td><code>RCVD</code></td><td>접수</td><td>Chanel</td><td>전문 접수·검증 통과</td></tr>
<tr><td><code>ACTC</code></td><td>순번</td><td>Sequencer</td><td>전역순번 부여</td></tr>
<tr><td><code>PDNG</code></td><td>대기</td><td>Dior</td><td>원장에 대기 기록(접수 동기화)</td></tr>
<tr><td><code>ACSP</code></td><td>정산반영</td><td>Hermes</td><td>당좌 차/대변 반영</td></tr>
<tr><td><code>ACCC</code></td><td>입금처리완료</td><td>Prada</td><td>과반 확정 · 최종 완결</td></tr>
<tr><td><code>RJCT</code></td><td>반려</td><td>Chanel/Hermes</td><td>XSD·업무규칙 실패 또는 잔액부족·미등록기관</td></tr>
</table>
<div class="note">
정상 흐름: <code>RCVD → ACTC → PDNG → ACSP → ACCC</code>.
검증·잔액 문제 시 <code>RJCT</code>로 종료됩니다.
</div>
<h2 id="ports">6. 포트 일람</h2>
<table>
<tr><th>서비스</th><th>포트</th><th>유형</th></tr>
<tr><td>🔢 Sequencer</td><td><code>8090</code></td><td>Headless JVM (논리)</td></tr>
<tr><td>📥 Chanel</td><td><code>8091</code></td><td>Web (Tomcat)</td></tr>
<tr><td>🔄 Dior</td><td><code>8092</code></td><td>Headless JVM (논리)</td></tr>
<tr><td>💰 Hermes</td><td><code>8093</code></td><td>Headless JVM (논리)</td></tr>
<tr><td>✅ Prada</td><td><code>8094</code></td><td>Headless JVM (논리)</td></tr>
<tr><td>🛡️ Gucci</td><td><code>8095</code></td><td>Web (Tomcat)</td></tr>
<tr><td>🛠️ Louis Vuitton</td><td><code>8099</code></td><td>Web (Tomcat)</td></tr>
<tr><td>운영/관리 콘솔 (Frontend)</td><td><code>5174</code></td><td>React + Vite</td></tr>
<tr><td>PostgreSQL (원장)</td><td><code>5433</code></td><td>RDBMS</td></tr>
<tr><td>Kafka (입구/저널/결과/통보)</td><td><code>9092</code></td><td>KRaft</td></tr>
<tr><td>Elasticsearch / Logstash / Kibana</td><td><code>9200 / 5000 / 5601</code></td><td>ELK 로그</td></tr>
</table>
<h2 id="glossary">7. 용어</h2>
<table>
<tr><th>약어</th><th>의미</th></tr>
<tr><td>RTGS</td><td>Real-Time Gross Settlement — 실시간총액결제</td></tr>
<tr><td>A-A-A</td><td>Active-Active-Active — 3센터 동시 가동</td></tr>
<tr><td>BMI</td><td>Business Message Identifier — 거래식별자 22자리(영업일8+기관4+일련10)</td></tr>
<tr><td>저널</td><td>전역순번이 부여된 처리 명단(Kafka 단일 파티션)</td></tr>
<tr><td>정족수</td><td>Quorum(N/2+1). 3센터=2</td></tr>
<tr><td>선저널</td><td>Write-ahead journal — 잔액연산 전 순번 확정(무손실 승계)</td></tr>
<tr><td>pacs.008 / pacs.002</td><td>ISO 20022 결제의뢰 / 결과통보 전문</td></tr>
<tr><td>TPS</td><td>Transactions Per Second — 초당 처리 건수</td></tr>
</table>
<footer>
RTGS 프로토타입 · 운영/관리 콘솔 서비스 매뉴얼 · 작성 루비(AI) · 검토 양희정 팀장(한국은행 RTGS시스템팀)<br>
상세 설계는 <code>docs/RTGS 아키텍처 설계서.md</code> 및 워크플로우 문서를 참조하세요.
</footer>
</div>
</body>
</html>