# HQ 콘솔 Phase 2 (2/2) — 수익 화면 (hq-ui)

> 설계 원본: `HQ_CONSOLE_DESIGN.md` §4-A · §4-B · 부록 A
> 백엔드: `HQ_PHASE2_REVENUE_GUIDE.md` — **구현 완료** (44 테스트 통과)
> 대상: `cryptoments-admin/hq-ui/`
> 범위: **A 대시보드 + B 수익 분해·대사.** 통계(C)·탐색기(D)·파트너관리(E)는 다음 증분.

---

## 0. 완료 기준

1. `npm run build` (vue-tsc) 통과
2. `/hq/dashboard` 가 자리표시자 → **실제 수익 화면**으로 교체
3. `/hq/revenue` (분해 폭포수), `/hq/revenue/reconcile` (대사) 신설
4. qndk11 계정으로 §5 실측값이 화면에 그대로 표시

---

## 1. 화면에 나와야 하는 실측값 (qndk11, 전체 기간)

| 항목 | 값 |
|---|---|
| 내 수익 (귀속 축) | **9.282311** |
| 시스템 배분 | **8.720778** |
| 분배 총액 | **18.003089** |
| G1 갭 | **0.000000** (갭 없음 → 배너 미표시) |
| 실현 | **0건** |
| 미실현 | 8.668311 (USDT/BSC) + 0.614000 (USDT/TRON) |

⚠️ **축이 3개다** (currency 1/network 1, 3/3, 6/3). 그중 6/3 은 전액 0이라 백엔드가 이미
`axes[]` 에서 제외한다. **프론트가 합산하지 마라** — 통화별로 카드/행을 분리한다.

---

## 2. ☠️ 이 화면이 절대 하면 안 되는 것

| 금지 | 이유 |
|---|---|
| **통화·네트워크 합산** | USDT(BSC) + USDT(TRON) + BNB + TRX 를 더하는 순간 의미 없는 숫자가 된다 |
| **`myRevenue`(대시보드)와 `breakdown.hqAmount` 를 같은 값으로 취급** | **축이 다르다.** 대시보드=귀속 축(내가 받은 것), 분해=발생 축(우리 그룹이 만든 것). 실측 차이 존재. **더하면 이중계상** |
| 시스템 노드에 요율·비중·증감 붙이기 | 설계 D1 — **금액 한 줄만.** 합계 검증용이지 분석 대상이 아니다 |
| `unallocatedApplicable=false` 인데 미분배를 폭포수 형제 노드로 그리기 | 필터 상태에서 **노드 합 ≠ 총액** 이 된다 |
| 실현 0건을 "데이터 없음"으로만 표시 | **"실현 기준 10 USD 미달"** 을 반드시 병기. 없으면 "곧 실현될 것"으로 오인된다 |
| "가용 잔액" 을 `realizedBalance` 외의 값에 쓰기 | 파트너 콘솔에서 반복된 문의가 재현된다 |
| 금액을 `Number` 로 파싱 | 백엔드가 **문자열**로 내려준다(정밀도). 표시·정렬 모두 문자열 기반으로 다뤄라 |

---

## 3. API (전부 구현 완료 — 필드명 그대로 쓸 것)

공통 `meta`: `{ asOf, aggregatedThrough, from, to, feeSource, currencyId, networkId, axis }`
`axis` 는 `"PARTICIPANT"`(귀속) 또는 `"SOURCE"`(발생). **화면에 축을 표기하라.**

### 3-1. `GET /api/hq/dashboard/summary?from&to&currencyId&networkId`
```
meta, myRevenue[], balances[], realizationThresholdUsd,
realizationThresholdScope, groupPartnerCount, reconcile
```
- `myRevenue[]` : `{ currencyId, currencySymbol, networkId, networkSymbol, amount, feeRowCount }`
- `balances[]`  : `{ currencyId, currencySymbol, networkId, networkSymbol, unrealizedBalance, realizedBalance, totalWithdrawn }`

### 3-2. `GET /api/hq/dashboard/trend?from&to&currencyId&networkId`
`rows[]` : `{ settlementDate, bucket, feeSource, currencyId, currencySymbol, networkId, networkSymbol, shareAmount, feeRowCount }`
`bucket` ∈ `HQ` | `SUB` | `EXTERNAL` | `SYSTEM`

### 3-3. `GET /api/hq/revenue/breakdown?from&to&feeSource&currencyId&networkId`
```
meta, axes[], unallocated, unallocatedApplicable
axes[] = { currencyId, currencySymbol, networkId, networkSymbol,
           distributedTotal, hqAmount, subAmount, externalAmount, systemAmount,
           subPartners[]{ partnerId, partnerCode, partnerName, amount, feeRowCount },
           byRole[]{ bucket, feeSource, feeRole, amount, feeRowCount } }
```
**불변식**: `hqAmount + subAmount + externalAmount + systemAmount == distributedTotal` (축 단위).
화면에서 이 검산을 하고 어긋나면 경고를 띄워라 — 조용히 그리지 마라.

`externalAmount` = 그룹 밖 참여자 몫(주로 크로스그룹 리베이트). **파트너 식별정보 없음이 정상**이다.
라벨은 `"외부/귀속불가"` 로 (설계상 "리베이트 유출"보다 넓은 개념).

`feeSource` 파라미터: `DEPOSIT` | `P2P` | 미지정(전체). **탭으로 분리** — 재원마다 분배 산식이 다르다.

### 3-4. `GET /api/hq/revenue/by-partner?from&to&currencyId&networkId`
`rows[]` : `{ partnerId, partnerCode, partnerName, hq, currencyId, currencySymbol, networkId,
networkSymbol, totalShareAmount, depositShareAmount, p2pShareAmount, otherShareAmount, feeRowCount }`
`hq=true` 인 행이 HQ 본인. **귀속 축**이다.

### 3-5. `GET /api/hq/revenue/realizations` (XPage)
`XPagination` 1-based. 파라미터: `page`, `size`, `from`, `to`, `currencyId`, `status`, `sourceType`.
행: `{ id, periodStart, periodEnd, currencyId, currencySymbol, networkId, networkSymbol,
sourcePartnerId, sourcePartnerName, sourceExternal, sourceType, totalFeeAmount,
totalShareAmount, txHash, status, completedAt, failedReason, createdAt }`

`sourceExternal=true` 면 발생 파트너가 그룹 밖 → **이름이 null 이 정상**이다.

### 3-6. `GET /api/hq/revenue/reconcile?from&to`
```
meta, summary, balances[], byPartner[]
summary = { hasGap, negativeGap, gapByAxis[], maxPartnerGapAmount, maxPartnerGapPartnerCode,
            maxPartnerGapCurrencyId/Symbol, maxPartnerGapNetworkId/Symbol,
            todayPendingByAxis[], aggregatedThrough }
gapByAxis[] = { currencyId, currencySymbol, networkId, networkSymbol,
                collectedFeeAmount, distributedShareAmount, gapAmount, negativeGap }
byPartner[] = { partnerId, partnerCode, partnerName, currencyId, currencySymbol,
                networkId, networkSymbol, collectedFeeAmount, distributedShareAmount, gapAmount }
```

**경보 표시 규칙 — 설계 부록 A-5**

| 조건 | 표시 |
|---|---|
| `negativeGap === true` | **적색 즉시 경보.** "분배가 징수보다 많습니다 — 산식상 불가능" |
| `hasGap === true` (양수만) | 주황 경고. 해당 파트너·통화 목록 |
| `hasGap === false` | 녹색 "정합" |
| `todayPendingByAxis[]` | **갭이 아니다.** "오늘 발생 (집계 대기)" 로 별도 표시 |

☠️ `todayPendingByAxis` 를 갭으로 표시하지 마라. 집계 잡이 전일분만 처리하므로 **매일 정상적으로
값이 있다.** 이걸 경보로 만들면 매일 울려서 진짜 사고를 덮는다(설계 부록 A-5 의 핵심 교훈).
`maxPartnerGapAmount` 는 갭이 없으면 `null` 이다 — null 체크 필수.

---

## 4. 화면 구성

### 4-1. `/hq/dashboard` (기존 자리표시자 교체)

| 카드 | 내용 |
|---|---|
| 내 수익 | `myRevenue[]` 통화별. 상단에 **"귀속 축"** 배지 |
| 정산 잔액 | `balances[]` 통화별 — 미실현 / **가용(=`realizedBalance`)** / 누적 인출 |
| 실현 안내 | 미실현 > 0 이고 실현 0 이면 `"실현 기준 ${realizationThresholdUsd} USD 미달 (${realizationThresholdScope})"` |
| 대사 | `reconcile` 요약 → §3-6 규칙. 갭 없으면 녹색 한 줄 |
| 그룹 | `groupPartnerCount` + 수익 화면 링크 |
| 추이 | `trend` — 일별 스택. **버킷별 색 구분**, 통화 셀렉터로 축 하나만 표시 |

⚠️ 차트 라이브러리가 필요하면 `chart.js` + `vue-chartjs` 를 추가한다(partner-ui 와 동일 버전).
**과하면 표로도 충분하다** — 추이는 행이 적다(실측 9행).

### 4-2. `/hq/revenue` — 분해 폭포수

- 상단: 기간 · 재원 탭(`전체 | DEPOSIT | P2P`) · 통화 셀렉터
- 축마다 폭포수 카드:
```
분배 총액                  18.003089 USDT (BSC)
  ├ 내 수익                 9.282311      ← 강조
  ├ 하위 배분               0.000000      ← subPartners[] 펼침
  ├ 외부/귀속불가            0.000000
  ├ 시스템                  8.720778      ← 평문. 요율·비중 없음
  └ ⚠️ 미분배                0.000000      ← unallocatedApplicable 일 때만
```
- 각 노드에 `byRole[]` 을 접이식으로 — **`feeSource` + `feeRole` 을 함께 표시**
  (섞이면 "수수료 0.27인데 수익 0.03"이 오류로 보인다)
- 하단: `by-partner` 표 (귀속 축 — **축이 다르다는 안내 문구 필수**)

### 4-3. `/hq/revenue/reconcile` — 대사

`gapByAxis[]` 표 + `byPartner[]` 표 + `todayPendingByAxis[]` 별도 블록.
`aggregatedThrough` 를 화면에 표시 — "이 날짜까지 집계된 기준" 임을 밝힌다.

### 4-4. `/hq/revenue/realizations` — 실현 내역
`XPage` 목록. **비어 있는 게 현재 정상**이므로 빈 상태 문구에 §4-1 의 임계치 안내를 넣는다.

---

## 5. 구현 규약

- `src/api/hq/revenue.ts` 신설. 타입은 `src/types/hq.ts` 확장
- 기존 `src/api/client.ts` 의 `api.get` 사용 — 헤더·401 처리는 이미 되어 있다
- 사이드바 메뉴를 `HqLayout.vue` 에 추가 (현재 화면 1개라 없음)
- 금액 표시 유틸 하나로 통일 — 문자열 그대로 받아 소수 자릿수만 다듬는다. `parseFloat` 금지
- `vue-tsc` 통과 필수. `any` 남용 금지
- 목업 데이터 금지 — API 실패 시 에러 상태를 표시하라

## 6. 하지 말 것

- §2 표의 금지 항목 전부
- 통계(C)·탐색기(D)·파트너관리(E) 화면 (다음 증분)
- 백엔드 수정 (필요하면 보고하고 멈춰라)
- **커밋·push**

## 7. 참고

| 목적 | 경로 |
|---|---|
| 백엔드 컨트롤러 | `cryptoments/partner-api/.../controller/HqDashboardController.java`, `HqRevenueController.java` |
| 응답 DTO | `cryptoments/partner-api/.../dto/response/Hq*.java` |
| 기존 hq-ui | `cryptoments-admin/hq-ui/src/` |
| 화면 스타일 원형 | `cryptoments-admin/partner-ui/src/views/partner/` |
