feat(report): 통계 현황 보고서 화면(📈 보고서 탭)
- LouisVuitton /admin/report: 전체 거래 집계(요약·대사·금액·처리시간·기관별) - 처리시간은 stage timestamp 기반 접수→완결 avg/p50/p95/max + 단계별 평균(percentile_cont) · 인과 정합(접수≤순번) 행만 집계해 BMI 재사용 시계역전 건 제외 - 대사: transfer=journal_log=settlement_view 건수 일치 + 총액 보존 검증 - 프론트: Report.tsx 신규 + App.tsx 상단 '📈 보고서' 탭(ADMIN 로그인 게이트) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -112,6 +112,70 @@ class AdminController(
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 6) 통계 현황 보고서 — 전체 거래 집계(성능·대사·기관별) ----------
|
||||
@GetMapping("/admin/report")
|
||||
fun report(): Map<String, Any?> {
|
||||
val byStatus = jdbc.queryForList("SELECT status, count(*) AS cnt FROM transfer GROUP BY status ORDER BY status")
|
||||
val transfers = jdbc.queryForObject("SELECT count(*) FROM transfer", Long::class.java) ?: 0
|
||||
val accounts = jdbc.queryForObject("SELECT count(*) FROM account", Long::class.java) ?: 0
|
||||
val maxSeq = jdbc.queryForObject("SELECT COALESCE(max(global_seq),0) FROM transfer", Long::class.java) ?: 0
|
||||
val totalBalance = jdbc.queryForObject("SELECT COALESCE(sum(balance),0) FROM account", Long::class.java) ?: 0
|
||||
|
||||
// 대사(정합성): 원장=선저널=조회사본 건수 일치 + 총액 보존
|
||||
val journalCount = jdbc.queryForObject("SELECT count(*) FROM journal_log", Long::class.java) ?: 0
|
||||
val viewCount = jdbc.queryForObject("SELECT count(*) FROM settlement_view", Long::class.java) ?: 0
|
||||
val initialTotal = accounts * 1_000_000_000L
|
||||
|
||||
// 금액 통계(ACCC 완결 대상)
|
||||
val amt = jdbc.queryForMap(
|
||||
"SELECT count(*) AS count, COALESCE(sum(amount),0) AS sum, COALESCE(round(avg(amount)),0) AS avg, " +
|
||||
"COALESCE(min(amount),0) AS min, COALESCE(max(amount),0) AS max FROM transfer WHERE status='ACCC'")
|
||||
|
||||
// 처리시간(지연, ms) — ACCC & 전 단계 시각 존재 건. created_at=순번, updated_at=완결, received_at=접수(raw_message)
|
||||
val lat = jdbc.queryForMap(
|
||||
"""
|
||||
SELECT count(*) AS count,
|
||||
COALESCE(round(avg(t.updated_at - r.received_at)),0) AS e2e_avg,
|
||||
COALESCE(round(percentile_cont(0.5) WITHIN GROUP (ORDER BY t.updated_at - r.received_at)),0) AS e2e_p50,
|
||||
COALESCE(round(percentile_cont(0.95) WITHIN GROUP (ORDER BY t.updated_at - r.received_at)),0) AS e2e_p95,
|
||||
COALESCE(max(t.updated_at - r.received_at),0) AS e2e_max,
|
||||
COALESCE(round(avg(t.created_at - r.received_at)),0) AS recv_to_actc,
|
||||
COALESCE(round(avg(t.pdng_at - t.created_at)),0) AS actc_to_pdng,
|
||||
COALESCE(round(avg(t.acsp_at - t.pdng_at)),0) AS pdng_to_acsp,
|
||||
COALESCE(round(avg(t.updated_at - t.acsp_at)),0) AS acsp_to_accc
|
||||
FROM transfer t JOIN raw_message r ON r.bmi = t.bmi
|
||||
WHERE t.status='ACCC' AND r.received_at > 0 AND t.pdng_at IS NOT NULL AND t.acsp_at IS NOT NULL
|
||||
AND t.created_at >= r.received_at -- 인과 정합(접수≤순번) 행만: BMI 재사용 등 시계 역전 건 제외
|
||||
""".trimIndent())
|
||||
|
||||
// 기관별 송·수신 집계(ACCC 기준) + 현재 잔액
|
||||
val byInstitution = jdbc.queryForList(
|
||||
"""
|
||||
SELECT a.code, a.name, a.balance,
|
||||
COALESCE(s.cnt,0) AS sent_cnt, COALESCE(s.amt,0) AS sent_amt,
|
||||
COALESCE(v.cnt,0) AS recv_cnt, COALESCE(v.amt,0) AS recv_amt,
|
||||
(COALESCE(v.amt,0) - COALESCE(s.amt,0)) AS net
|
||||
FROM account a
|
||||
LEFT JOIN (SELECT sender_code c, count(*) cnt, sum(amount) amt FROM transfer WHERE status='ACCC' GROUP BY sender_code) s ON s.c = a.code
|
||||
LEFT JOIN (SELECT receiver_code c, count(*) cnt, sum(amount) amt FROM transfer WHERE status='ACCC' GROUP BY receiver_code) v ON v.c = a.code
|
||||
ORDER BY a.code
|
||||
""".trimIndent())
|
||||
|
||||
return mapOf(
|
||||
"generatedAt" to System.currentTimeMillis(),
|
||||
"totals" to mapOf("transfers" to transfers, "accounts" to accounts, "maxSeq" to maxSeq, "byStatus" to byStatus),
|
||||
"reconciliation" to mapOf(
|
||||
"transferCount" to transfers, "journalCount" to journalCount, "viewCount" to viewCount,
|
||||
"consistent" to (transfers == journalCount && transfers == viewCount),
|
||||
"totalBalance" to totalBalance, "initialTotal" to initialTotal,
|
||||
"balancePreserved" to (totalBalance == initialTotal),
|
||||
),
|
||||
"amount" to amt,
|
||||
"latency" to lat,
|
||||
"byInstitution" to byInstitution,
|
||||
)
|
||||
}
|
||||
|
||||
private fun lim(n: Int) = n.coerceIn(1, 200).toString()
|
||||
|
||||
@GetMapping("/admin/ledger/transfers")
|
||||
|
||||
Reference in New Issue
Block a user