# HQ 콘솔 Phase 3 — 그룹 거래 통계 (백엔드 + UI)

> 설계 원본: `HQ_CONSOLE_DESIGN.md` §4-C
> 선행: Phase 2 수익 (`HQ_PHASE2_REVENUE_GUIDE.md`, `HQ_PHASE2_UI_GUIDE.md`) — **완료**
> 대상: `partner-api` (백엔드) + `cryptoments-admin/hq-ui` (프론트)
> 범위: **C-1 입금 / C-2 출금 / C-3 원화·P2P·LP / C-4 파트너별 비교**

수익(Phase 2)이 "얼마 벌었나"라면, 통계는 **"그 돈이 어디서 나왔나"** 다.

---

## 0. 실측 (2026-08-25 운영 DB) — 설계를 바꾸는 숫자들

### 0-1. ☠️ `PARTNER_CHARGE` 가 그룹 거래액의 **96%** 다

qndk11(51) 그룹 입금 전량:

| status | type | method | 건수 | 금액 | 수수료 |
|---|---|---|---|---|---|
| SETTLED | **PARTNER_CHARGE** | DIRECT | 11 | **4,325.5141** | 17.302057 |
| SETTLED | USER_DEPOSIT | TORQ | 4 | 133.7905 | 0.000000 |
| SETTLED | USER_DEPOSIT | P2P | 1 | 36.3629 | 0.326545 |

`PARTNER_CHARGE` = 파트너 자기 운영자금 충전. **매출이 아니다**(2026-08-24 오너 결정).
전체로는 금액 0.3%(321건/276,154)지만 **이 그룹에서는 96%** 다.

→ **거래액에 섞으면 실효 요율이 완전히 왜곡된다.** 반드시 분리해 별도 행으로 표시한다.
⚠️ 과거 징수분(전체 152.82)은 환급하지 않으므로 **과거 구간 데이터에는 수수료가 남아 있다.**
"자기 충전인데 수수료가 있다"는 오류가 아니라 소급 미적용의 결과다.

### 0-2. ☠️ 출금 status 는 7종이고 `COMPLETED` 를 빼면 이 그룹의 67% 가 사라진다

전체 실측: `CONFIRMED` 961 · `COMPLETED` 48 · `CANCELLED` 21 · `EXHAUSTED` 13 ·
`REJECTED` 7 · `P2P_PENDING` 2 · `FAILED` 1

**qndk11 그룹**: `COMPLETED` 4건(200.698) + `CONFIRMED` 2건(376.428) — **`COMPLETED` 가 다수다.**

`CONFIRMED` 만 성공으로 치면 이 그룹 출금의 3분의 2가 통계에서 증발한다.
(`COMPLETED` 는 P2P 원화 출금의 종결 상태다.)

### 0-3. 입금 status 는 `CONFIRMED` + `SETTLED` 둘 다

전체: `SETTLED` 3,149 · `CONFIRMED` 187 · `COLLECTING` 1 · `FAILED` 1.
`CONFIRMED` 단독 필터로 P2P 입금이 0 으로 보였던 실사고가 있다 — **둘 다 포함**한다.

### 0-4. `p2p_disputes` 는 **실재한다** (설계 리뷰 R6 정정)

`p2p_disputes` **51행**, `dispute_events` **19행** — 둘 다 있다.
DDL 헤더의 "v2.10 설계, 미생성" 주석은 stale 이다. **실물 조회로 확인했다.**

### 0-5. qndk11 그룹 원화·P2P 규모

| 테이블 | 건수 |
|---|---|
| `p2p_deposit_orders` | 7 |
| `p2p_withdraw_orders` | 4 |
| `torq_trades` | 5 |
| `p2p_members` | 16 |

**작다.** 화면이 비어 보여도 정상이며, "무거래 숨기기" 필터가 필수다(그룹 9곳 중 5곳이 거래 0).

### 0-6. 통화 마스터 (축 라벨)

`1 USDT(BSC)` · `2 USDT(Polygon)` · `3 USDT(TRON)` · `4 BNB` · `5 POL` · `6 TRX` · **`99 KRW`**

⚠️ `symbol` 만으로는 구분이 안 된다(USDT 가 3개). **반드시 `name` 또는 network 를 병기**한다.

---

## 1. 공통 규칙 (Phase 2 와 동일 — 재확인)

| 규칙 | 비고 |
|---|---|
| 그룹 스코프 `p.root_partner_id = #{hqId}` | 예외 없이. `hqId` 는 세션에서만 |
| 통화·네트워크 **합산 금지** | 축 분리 |
| 금액 `PlainBigDecimalSerializer` (문자열) | Phase 2 에서 신설. `ToStringSerializer` 쓰지 마라 — 지수표기가 나온다 |
| `<script>` 내 `<`/`<=`/`<>` 금지 | `&lt;` / CDATA / `!=`. 위반 시 **기동 시 전 서비스 다운** |
| MyBatis 3규칙 | `XPagination` 첫 / `Class<?>` 마지막 / `XPage<T>` 반환. ORDER BY·LIMIT·COUNT 직접 작성 금지 |
| DTO 멤버 JavaDoc / `@Data` 금지 / XML 매퍼 금지 | |
| **응답 `meta` 에 포함 상태를 명시** | 화면이 "무엇을 셌는지" 알 수 있어야 문의가 안 온다 |

---

## 2. 엔드포인트

`HqStatsController` (`/api/hq/stats`) + `HqStatsService` + `HqStatsMapper`.

### 2-1. `GET /api/hq/stats/deposits?from&to&partnerId&currencyId&networkId&groupBy`

`groupBy` ∈ `DAY` | `PARTNER` | `METHOD` (기본 `DAY`)

| 지표 | 산식 |
|---|---|
| `depositCount` / `depositAmount` | `status IN ('CONFIRMED','SETTLED')` **AND `deposit_type != 'PARTNER_CHARGE'`** |
| `netAmount` | `SUM(net_amount)` — 총액/순액 축 분리 |
| `feeAmount` | `SUM(fee_amount)` |
| **`partnerChargeCount` / `partnerChargeAmount`** | `deposit_type = 'PARTNER_CHARGE'` — **별도 행** (§0-1) |
| `unidentifiedCount` | `deposit_type = 'UNIDENTIFIED'` — ≠0 이면 매칭 실패 신호 |
| `byMethod[]` | `deposit_method` 분포 (HD_WALLET/EXTERNAL_WALLET/DECIMAL_MATCH/DIRECT/MANUAL/PHONEPAY/TORQ/P2P) |

`meta.includedStatuses = ["CONFIRMED","SETTLED"]`, `meta.excludedTypes = ["PARTNER_CHARGE"]` 를 반드시 내려준다.

⚠️ **회원당 평균·재입금률은 이번 증분에서 만들지 마라.** `DISTINCT partner_user_id` 는 기간 합산으로
복원할 수 없어(일별 UU 를 더해도 기간 UU 가 아니다) 별도 설계가 필요하다.

### 2-2. `GET /api/hq/stats/withdrawals?from&to&partnerId&currencyId&networkId&groupBy`

| 지표 | 산식 |
|---|---|
| `successCount` | `status IN ('CONFIRMED','COMPLETED')` ☠️ **`COMPLETED` 필수** (§0-2) |
| `failureCount` | `status IN ('FAILED','REJECTED','EXHAUSTED','CANCELLED')` |
| `pendingCount` | 그 외 (`P2P_PENDING`, 진행중 상태 전부) |
| `successRate` | `successCount / (successCount + failureCount)` — **pending 은 분모에서 제외** |
| `byStatus[]` | 실제 status 별 건수·금액 (7종 전부. 화이트리스트로 자르지 마라) |

☠️ `meta.successStatuses` / `meta.failureStatuses` 를 **응답에 명시**한다. 분모 정의가 화면에 없으면
"성공률이 왜 이래?" 문의가 반드시 온다. `FAILED` 는 터미널이다(자동 재시도 폐지, 2026-08-21).

⚠️ **승인 대기 적체는 실시간 지표**다 — 기간 필터와 무관하게 **현재** `PENDING_APPROVAL` 건수와
최장 대기시간을 별도 필드로 내려준다.

### 2-3. `GET /api/hq/stats/krw?from&to&partnerId`

원화 채널. **KRW 축은 USDT 축과 절대 합산하지 않는다**(`currency_id=99`).

| 지표 | 소스 |
|---|---|
| `p2pDepositOrders` | `p2p_deposit_orders` (건수·KRW 금액·상태별) |
| `p2pWithdrawOrders` | `p2p_withdraw_orders` |
| `lpTrades` | `torq_trades` — **`lp_provider` 별로 분리** |
| `matchRate` | 매칭 완료 / 입금 주문 |
| `poolLiquidity` | **실시간** — `p2p_withdraw_orders` `PENDING` 잔여액. 0 이면 P2P 사실상 정지 |
| `disputes` | `p2p_disputes` **(51행 실재 — §0-4)** 상태별 + 미해결 건수 |
| `memberCount` | `p2p_members` |

☠️ **`p2p_settlements` 를 읽지 마라** — TORQ 레그를 안 담아 3% 오보고한 이력. P2P 실적은 `p2p_matches`.
☠️ **"오늘 LP 수익" 을 만들지 마라** — LP 리베이트는 **주간 확정**이다. 그리고
**qndk11 은 BARO 그룹(`rebate_enabled=0`)이라 리베이트가 영구 0** 이다. 이 카드는 만들지 않는다.

### 2-4. `GET /api/hq/stats/partners?from&to&currencyId&networkId&includeInactive`

**이 화면이 통계 영역의 주력이다.** 그룹 전원을 세로로 비교.

| 컬럼 | 소스 |
|---|---|
| `partnerId/Code/Name`, `partnerType`, `depth`, `status` | `partners` (`root_partner_id` 스코프) |
| `depositAmount` / `withdrawalAmount` / 건수 | §2-1·2-2 규칙 그대로 |
| `partnerChargeAmount` | 별도 (§0-1) |
| `collectedFeeAmount` | `ledger_entries` `entry_type='FEE'` |
| **`myShareAmount`** | 이 파트너에서 발생해 **HQ 에게** 온 `share_amount` — **기본 정렬 키** |
| `effectiveRate` | `collectedFeeAmount / depositAmount` — **금액에서 역산**(요율 컬럼 읽지 마라) |
| `lastTransactionAt` | 최근 거래일 — N일 무거래면 회색 |
| `hasGap` | 이 파트너의 G1 갭 ≠ 0 (Phase 2 `/reconcile` 재사용) |

`includeInactive=false`(기본)면 **거래 0 파트너를 제외**한다 — 그룹 9곳 중 5곳이 거래 0 이라
안 하면 대부분 빈 행이다(§0-5).

`depth` 는 `root_partner_id` 기준 계산. **재귀 CTE 금지** — 기존 `searchSubPartnerTree` 는
`depth < 5` 제한이 있어 금액이 틀릴 수 있다.

---

## 3. UI (`hq-ui`)

라우트: `/hq/stats/deposits` · `/withdrawals` · `/krw` · `/partners`
`HqLayout` 사이드바에 "통계" 섹션 추가.

### 공통
- Phase 2 의 `lib/amount.ts`(문자열 금액), `lib/hqAxis.ts`(축), `components/hq/MetaBar.vue` 재사용
- ☠️ **`meta` 의 포함 상태를 화면에 표시**한다. 툴팁이 아니라 **표 헤더 옆에 보이게**
- `parseFloat`/`Number` 금지, 목업 금지

### 화면별
| 화면 | 필수 |
|---|---|
| 입금 | **자기 충전을 별도 행**으로. 합계 행에 포함하지 마라 |
| 출금 | 성공률 옆에 **분모 정의**를 그대로 표기 ("CONFIRMED+COMPLETED / (성공+실패)"). 승인 대기는 "현재 시점" 배지 |
| 원화 | KRW 축 분리. 유동성 0 이면 경고. **LP 수익 카드 없음** |
| 파트너 비교 | `myShareAmount` 기본 정렬 · "무거래 숨기기" 기본 ON · 행 클릭 시 해당 파트너로 필터된 다른 통계로 이동 |

---

## 4. 하지 말 것

- `PARTNER_CHARGE` 를 거래액에 합산
- 출금 성공에서 `COMPLETED` 누락
- 입금에 `CONFIRMED` 단독 필터
- 통화·네트워크 합산 (특히 KRW 와 USDT)
- `p2p_settlements` 읽기
- "오늘/일별 LP 수익" 생성
- 회원당 평균·재입금률 (DISTINCT 집계 — 별도 설계 필요)
- 재귀 CTE 로 depth 계산
- `ToStringSerializer` 사용 (지수표기)
- `<script>` 내 raw `<`
- 신규 테이블·DDL
- **커밋·push**

## 5. 검증

```
cd /Users/dudgh/work/Cryptoments/cryptoments && ./gradlew :partner-api:compileJava :partner-api:test --rerun-tasks
cd /Users/dudgh/work/Cryptoments/cryptoments-admin/hq-ui && NODE_ENV=development npm run build
```

**qndk11 기준 기대값**: 입금(자기충전 제외) 5건 / 170.1534 · 자기충전 11건 / 4,325.5141 ·
출금 성공 6건(COMPLETED 4 + CONFIRMED 2) / 577.1261 · P2P 주문 7 · LP 거래 5 · P2P 회원 16

## 6. 참고

| 목적 | 경로 |
|---|---|
| Phase 2 구현 (스타일 원형) | `partner-api/.../controller/HqRevenueController.java`, `service/HqRevenueService.java`, `mapper/HqRevenueMapper.java` |
| 금액 직렬화 | `partner-api/.../config/PlainBigDecimalSerializer.java` |
| 프론트 원형 | `cryptoments-admin/hq-ui/src/views/hq/RevenueBreakdownView.vue` |
| 설계 | `v2-docs/HQ_CONSOLE_DESIGN.md` §4-C |
