# HQ 콘솔 Phase 5 — 「거래」 명시 화면 (입금 · 출금 · P2P) 기능 정의

> **상태**: 기능 정의 (구현 착수 전)
> **작성**: 2026-08-28
> **범위**: 무엇을 만드는가 · 데이터 원천 · API 목록 · 승계 규칙 · 선행 확인까지.
> **비범위**: 화면 마크업, DTO 필드 세부, 구현 지침. → 승인 후 `HQ_PHASE5_TRANSACTIONS_GUIDE.md` 로 분리.

---

## 0. 확정된 전제 (오너 결정, 2026-08-28)

| # | 항목 | 결정 |
|---|---|---|
| E1 | 탐색기와의 관계 | **둘 다 유지** — 「거래」는 업무용(합계·상세·프리셋), 탐색기는 발견용. 탐색기 8종은 **하나도 빼지 않는다** |
| E2 | 단건 상세 깊이 | **풀 타임라인** — 입금·출금·P2P 각각 "왜 이 상태인가"에 화면이 답한다 |
| E3 | P2P 범위 | **주문 + 매칭 + 정산 관점** — 정산은 별도 탭이 아니라 **매칭의 한 관점**이다 (§3.3 정정, 2026-08-28). 분쟁은 매칭 행의 **상태 배지로만** |
| E4 | 요약 합계 축 | **통화별 분리만** — USDT 환산 총액 한 줄은 **두지 않는다** |
| E5 | 메뉴 | **「거래」·「P2P」 두 섹션 신설**, 통계 다음·원시 데이터 앞. 통계와 거래는 **둘 다 유지하고 서로 링크**. 대시보드 위치는 건드리지 않는다 |
| E6 | P2P 화면 | admin 콘솔의 **3화면을 HQ 로 가져온다** — 출금 주문 · 거래(구매 주문) · 출금 풀. 매칭 목록은 **유지**(정산 실패 탐지가 매칭 축에만 있다) |
| E7 | 쓰기 권한 | **취소까지만** — 강제정산 · 직접전환 · USDT 전환 승인/거부는 HQ 에 두지 않는다 |
| E8 | 순서 | **1차는 뷰어 전부 조회 전용.** 취소는 화면이 선 뒤 **2차(5-F)** 로 분리 (2026-08-28) |

### E4 를 그렇게 정한 이유 (지우지 말 것)

환산 총액은 "어느 시점 시세냐"가 항상 논쟁이 되고, 같은 과거 기간을 다시 조회했을 때 **값이 계속 바뀐다**.
바뀌는 숫자는 대사에 쓸 수 없다. 통화별로 나눠 내면 틀릴 수가 없다.

`formatKrw`(정수 원)와 `formatAmount`(소수 18자리)가 **받는 타입이 다른 것**이 이 규칙의 방어선이다 —
KRW 를 `sumAmounts` 에 넣을 수 없다. 화면에서도 두 축을 같은 행에 놓지 않는다.

---

## 1. 왜 만드는가 — 탐색기가 의도적으로 안 하는 것

「모든 입금 / 모든 출금 / P2P」 표 **자체는 이미 탐색기에 있다**(`deposits` · `withdrawals` ·
`p2p-orders` · `p2p-matches`). 그러므로 이 작업은 **새 데이터를 뚫는 일이 아니라**, 탐색기가
설계상 하지 않는 3가지를 갖춘 업무 화면을 세우는 일이다.

| # | 탐색기의 현재 | 거래 화면이 하는 것 |
|---|---|---|
| 1 | `총 N건` 뿐. 화면에 *"서로 다른 자산이라 합계 행을 두지 않았습니다"* 라고 적혀 있다 | **조건 전체 합계를 통화축별로** 낸다 (E4) |
| 2 | 표의 한 줄이 끝. 단건 상세 없음 | **풀 타임라인 상세** (E2) |
| 3 | 상태 필터가 **문자열 1개**. 기간 프리셋 없음 | 상태 **다중** · 타입 · 방식 · 기간 프리셋 · 딥링크 |

그리고 **P2P 는 정산이 안 보인다.** 매칭 행에 `settlement_tx_hash` 만 있고, **왜 그 TX 가
안 나갔는지**는 어디에도 없다. `p2p_matches.settle_retry_count` · `settle_failure_reason` 이
그 답을 들고 있는데 아무도 읽지 않는다.

---

## 2. 메뉴 (IA)

현재 **9개** → **15개**. 신설은 「거래」 2개 + 「P2P」 4개다.

| 섹션 | 메뉴 | 힌트 | 경로 | |
|---|---|---|---|---|
| **수익** | 대시보드 | 귀속 축 | `/hq/dashboard` | |
| | 수익 | 징수~인출 | `/hq/revenue` | |
| | 실현 내역 | 귀속 축 | `/hq/revenue/realizations` | |
| **통계** | 파트너별 비교 | 주력 | `/hq/stats/partners` | |
| | 입금 통계 | 충전 분리 | `/hq/stats/deposits` | |
| | 출금 통계 | 성공률 | `/hq/stats/withdrawals` | |
| | 원화 채널 | KRW 축 | `/hq/stats/krw` | |
| **거래** | 입금 내역 | 건 단위 | `/hq/transactions/deposits` · 상세 `/:id` | 🆕 |
| | 출금 내역 | 건 단위 | `/hq/transactions/withdrawals` · 상세 `/:id` | 🆕 |
| **P2P** | P2P 출금 주문 | 공급 | `/hq/p2p/withdraw-orders` · 상세 `/:id` | 🆕 |
| | P2P 거래 | 구매 주문 | `/hq/p2p/orders` · 상세 `/:id` | 🆕 |
| | P2P 매칭 | 매칭 축 · 정산 관점 | `/hq/p2p/matches` · 상세 `/:id` | 🆕 |
| | P2P 출금 풀 | **재고 현황** | `/hq/p2p/pool` | 🆕 |
| **원시 데이터** | 데이터 탐색 | 행 원본 | `/hq/explorer/:dataset` | |
| **관리** | 파트너 관리 | 쓰기 | `/hq/partners` | |

### 2.0 「P2P」를 별도 섹션으로 둔 이유

**출금 풀은 거래 목록이 아니라 재고(공급) 현황이다.** 「거래」 안에 넣으면 성격이 어긋나고,
"기간을 걸어 조회하는 표"로 오해된다 — 풀은 **지금 이 순간**의 상태다(화면에 `마지막 업데이트` 가
찍히는 이유다).

⚠️ **1차(5-A~5-E)에서 이 섹션은 조회 전용이다**(E8). 2차에서 행 단위 취소가 들어오면
조회와 쓰기가 한 섹션에 섞이는데, 이는 기존 레이아웃 주석의 원칙
(*"섹션을 나눠 두지 않으면 조회하러 들어왔다가 저장을 누르는 동선이 생긴다"*)과 부딪힌다.
그때 **OTP 확인**이 오조작 경로를 끊는 방어선이 된다(§4.1).

### 2.1 배치 — 통계 다음, 원시 데이터 앞

해상도가 내려가는 순서다: **집계(통계) → 건(거래) → 원본(탐색)**. 통계 화면 행에서
`?partnerId=&from=&to=` 딥링크로 거래로 내려가는 동선이 메뉴에서도 아래로 흐른다.

☠️ **거래를 통계 위로 올리지 않는다.** 기존 섹션 분리에 이유가 박혀 있다 —
*"섹션을 나눠 두지 않으면 두 영역의 금액이 같은 것을 센다고 오해된다. 수수료 집계와 거래 원본은
기준일도 다르다."* 「거래 → 통계 → 수익」 순이 되면 위에서 아래로 읽는 사람이 **거래액과 수수료를
같은 축으로 이어 읽는다.**

### 2.2 명명 — '통계' 와 '내역'

모바일은 섹션 라벨 없이 **평면 가로 스크롤 탭**이다(`NAV_ITEMS` 는 `NAV_SECTIONS` 를 flat 한 것).
「입금 통계」와 「입금 내역」이 나란히 스크롤되므로 **구분이 라벨에만 걸린다.**

☠️ 탭이 **9개 → 15개**가 된다. 평면 가로 스크롤로는 감당이 안 되는 수다 —
**모바일 내비를 섹션 드롭다운으로 바꾸는 작업이 이 Phase 에 딸려온다.**
지금도 9개라 이미 잘려 있으므로, 늘리기만 하고 두면 절반이 도달 불가가 된다.

### 2.3 활성 표시 (`matchPrefix`)

| 메뉴 | `matchPrefix` | 이유 |
|---|---|---|
| 입금 내역 · 출금 내역 | `true` | 상세 `/:id` 에서도 부모 메뉴가 활성이어야 한다 |
| P2P 출금 주문 · P2P 거래 · P2P 매칭 | `true` | 각 상세 `/:id` 동일 |
| P2P 출금 풀 | `false` | 하위 경로가 없다 |

⚠️ P2P 4경로는 서로 접두사가 **아니다**(`/p2p/withdraw-orders` · `/p2p/orders` · `/p2p/matches` ·
`/p2p/pool`). 기존 `/hq/revenue` ↔ `/hq/revenue/realizations` 같은 동시 활성 문제는 생기지 않는다.

### 2.4 통계 ↔ 거래 상호 링크 (E5)

중복을 없애지 않고 **잇는다**. 대신 양쪽에 같은 경고를 박는다.

- 통계 행 클릭 → 같은 조건으로 거래 목록 (`?partnerId=&from=&to=&currencyId=`)
- 거래 요약 → 해당 통계 화면으로 올라가는 링크
- ☠️ **두 화면의 숫자가 다를 수 있다.** 통계는 일별 스냅샷(`settlement_date`), 거래는 온디맨드
  (`created_at`) 라 **기준일이 다르다**(D4 하이브리드). 링크 옆에 이 사실을 적는다 —
  적지 않으면 사용자가 둘을 대사하려 들고, 안 맞는 이유를 아무도 설명하지 못한다.

---

## 3. 화면 정의

### 3.1 거래 > 입금

**목록** — 그룹 전체(`root_partner_id = 나`) 입금. 컬럼은 탐색기 `deposits` 와 동일하게 시작한다
(같은 것을 다르게 부르지 않는다).

**요약 바** — 조건 전체 기준. **통화축(=`currency_id`)마다 한 행**:

```
USDT (BSC)     1,284건   입금액 402,118.500000   수수료 1,608.474000   순액 400,510.026000
USDT (TRON)      377건   입금액 118,004.000000   수수료   472.016000   순액 117,531.984000
KRW               28건   입금액       2,800,000   수수료         5,600   순액       2,794,400
```

- ☠️ **가로 합계 행을 두지 않는다** (E4).
- ⚠️ 요약은 **현재 페이지가 아니라 조건 전체**다. 페이지 합계로 오독되지 않도록 라벨에 "조건 전체"를 적는다.
- ⚠️ 자기 충전(`PARTNER_CHARGE`)은 **거래액이 아니다.** 요약에서 별도 행으로 분리하거나 타입 필터를 강제한다 — 뭉뚱그리면 통계 화면과 숫자가 어긋나 보고가 깨진다.

**필터** — 기간 프리셋(오늘 / 7일 / 30일 / 이번 달 / 직접), 파트너(서브트리 포함), 통화, 네트워크,
**상태 다중**, 입금 타입(`USER_DEPOSIT` / `PARTNER_CHARGE` / `UNIDENTIFIED`),
입금 방식(HD / EXTERNAL / DECIMAL), 회원(`partner_user_id`), 키워드(입금 코드 · TX · 주문 코드 접두).

> 신규는 **상태 다중 · 타입 · 방식** 셋이다. 나머지는 탐색기 필터와 같은 축이므로 서버 로직을 공유한다.

**상세** (`/hq/transactions/deposits/:id`)

| 블록 | 원천 |
|---|---|
| 기본 정보 · 금액 · 주소 · TX | `deposits` |
| **상태 타임라인** | `transaction_status_history` (`tx_type='DEPOSIT'`, `tx_id`) |
| 집금 | `collection_queue` (`deposit_id`) — 상태 · 재시도 · 실패 사유 |
| 원장 반영 | `ledger_entries` (`reference_type='DEPOSIT'`, `reference_id`) |
| 수수료 분배 | **`settlement_daily_fees`** — 이 입금이 속한 **일자·파트너**의 분배 행 |

☠️ **`settlement_balances` 가 아니다** (2026-08-28 운영 DB 확인, 초판 오류 정정).
그 테이블은 `UNIQUE (participant_type, participant_partner_id, currency_id, network_id)` 의
**참여자별 누적 잔액 30행**이라 입금·일자 단위 분해가 **존재하지 않는다.**
일자 × 발생 파트너 × 참여자 분해는 `settlement_daily_fees` 에 있고, 탐색기 「일별 수수료」가
이미 그것을 쓴다(`HqExplorerMapper` 참조).

⚠️ **입금 1건 ↔ 분배 행의 FK 는 없다.** 일별 집계라 입금 단위로 쪼갤 수 없다. 화면에
"이 입금의 분배"라고 쓰지 말고 **"이 입금이 속한 일자·파트너의 분배"** 로 라벨링하고
주의사항에 적는다. 안 적으면 이 금액을 입금 1건에 귀속시켜 읽는다.
☠️ `share_rate` 는 **퍼센트 단위**(`0.3` = 0.3%) — `formatRate`.

### 3.2 거래 > 출금

**목록 · 요약** — 입금과 같은 골격. 요약에 **상태군**을 얹는다:
`진행중`(REQUESTED·PENDING_APPROVAL·APPROVED·BALANCE_PENDING·PROCESSING·BROADCASTING·P2P_PENDING) /
`성공`(**CONFIRMED + COMPLETED 둘 다**) / `실패`(FAILED·EXHAUSTED) / `종료`(REJECTED·CANCELLED).

- ☠️ **성공을 CONFIRMED 하나로 세지 않는다.** `COMPLETED` 는 P2P 원화 출금의 종결 상태다.
  탐색기 노트에 이미 적혀 있는 규칙이고, 여기서 어기면 P2P 출금이 통째로 실패로 집계된다.
- ⚠️ `FAILED` 는 터미널이다(자동 재시도 폐지, 2026-08-21). `retry_count` 는 과거 행에만 값이 있다.
- ⚠️ 파트너 자기 인출(`PARTNER_WITHDRAW`)을 회원 출금과 뭉뚱그리지 않는다 — 타입 필터를 기본 노출한다.

**필터** — 입금과 동일 + 출금 타입, 요청 경로(`request_source`).

**상세** (`/hq/transactions/withdrawals/:id`)

| 블록 | 원천 |
|---|---|
| 기본 정보 · 금액 · 주소 · TX | `withdrawals` |
| **상태 타임라인 (= 승인 이력)** | `transaction_status_history` (`tx_type='WITHDRAWAL'`) |
| 실패 · 재시도 | `error_message` · `retry_count` · `failed_finalized_at` · 이력의 `retry_seq` |
| 분할/부모 관계 | `parent_withdrawal_id` |
| P2P 전환 건 | 매칭으로 점프 |

> ☠️ **`withdrawal_approval_logs` 는 없는 테이블이다.** DDL 에서 제거됐고
> (`-- [제거됨] withdrawal_approval_logs (→ transaction_status_history)`) 승인 이력은
> `transaction_status_history` 로 통합됐다. admin-api 의 `WithdrawalManagementService` 가 이미
> 이 테이블로 타임라인을 그린다 — **그 구현을 참조**한다. (CLAUDE.md 의 4장 테이블 목록이 이 지점에서 낡았다.)

### 3.3 P2P — 화면 4개

> ☠️ **`p2p_settlements` 로 화면을 만들지 않는다.** 이 테이블은 **v2.10 제거 대상**이고
> (`P2P_SETTLEMENT_RESTRUCTURE.md` §2 — ①원장 참조 통일 → ②검증 → ③`reference_type` 개명 →
> ④DROP), 역할이 이미 셋으로 이관됐다:
>
> ```
> 거래 사실   → p2p_matches            금액 · 환율 · 상태 · settled_at · settlement_tx_hash(정본)
> 온체인 실행 → p2p_transfer_requests   크로스 파트너일 때만 · settlement_code UNIQUE 로 멱등
> 자금 기록   → ledger_entries          CREDIT / FEE / DEBIT
> ```
>
> 커버리지로도 틀린 선택이다 — (아래는 **활성화 전** 실측, §6) 정산 행은 **45건**(INNER 34 / ONCHAIN 1 / 유령 10)뿐인데
> **TORQ 레그 1,247건은 정산 행이 아예 없다.** DDL 도 못을 박아 뒀다 —
> *"정산 단위는 매칭이고 실패도 매칭의 상태다."* 매칭 기반으로 지으면 **④DROP 이 와도
> 화면이 안 깨진다.**

admin 콘솔에 **이미 있는 화면 3개를 HQ 로 가져온다**(E6). 새로 발명하지 않는다 — 컬럼·상태·용어를
그대로 승계하고 **그룹 스코프와 마스킹만 씌운다.**

| 화면 | admin 원본 | 백엔드 원본 | 원천 |
|---|---|---|---|
| P2P 출금 주문 | `P2pWithdrawOrderListView.vue` | `GET /admin/p2p/orders/withdraw` | `p2p_withdraw_orders` |
| P2P 거래 | `P2pOrderListView.vue` | `GET /admin/p2p/orders/deposit` | `p2p_deposit_orders` |
| P2P 출금 풀 | `P2pWithdrawPoolView.vue` | `GET /admin/p2p/withdraw-pool` | 집계 (아래 참고) |
| P2P 매칭 | `P2pMatchListView.vue` | `GET /admin/p2p/matches` | `p2p_matches` |

#### 3.3.1 P2P 출금 주문 (공급 · 판매)

컬럼: ID · 주문코드 · 파트너 · 네트워크 · KRW 금액 · **진행상황(매칭/확정/잔여 3분할 바)** ·
잔여처리/전환(강제정산 배지) · 상태 · 등록일.

- ⚠️ **PENDING 은 자동 만료되지 않는다**(2026-06-11 정책). 종료는 회원·파트너의 취소 결정으로만
  일어난다 — "오래된 대기"를 이상 징후로 그리지 않는다.
- ⚠️ 진행상황 바의 **잔여**는 출금 원장 합계가 정본이다. 누적 컬럼과 어긋난 이력이 있어
  탐색기는 아예 표시하지 않는다 — 여기서는 admin 과 **같은 계산식**을 쓴다(두 콘솔이 다른 수를
  내면 안 된다).

#### 3.3.2 P2P 거래 (구매 주문)

컬럼: ID · 주문코드 · 파트너 · 사용자 ID · KRW 금액 · **매칭 N건** · 시도 · 만료일 · 상태 · 등록일.

- 매칭 N건 → 매칭 목록으로 드릴다운.
- ⚠️ 사용자 ID(`partner_user_id`)는 파트너가 정하는 값이다. 마스킹 대상은 아니지만
  **회원 실명이 들어온 행이 실재**하므로 CSV 반출 시 감사 로그가 특히 중요하다.

#### 3.3.3 P2P 출금 풀 (재고 현황)

가용 KRW · 가용 USDT · 활성 주문 수 · 잠긴 USDT · 더스트 제외 / 네트워크별 / 파트너별 /
풀 구성(매칭가능 주문).

☠️ **partner-api 에 이미 있다** — `PartnerP2pPoolController` → `GET /pool`,
`PartnerP2pPoolService.getPoolStatus()`. 그리고 그 서비스는 **이미 그룹 스코프로 동작한다**:

```java
P2pGroupResolver.GroupScope scope = groupResolver.resolveScope(partnerId);
// 파트너×네트워크 집계 = 그룹 범위 내, crossAllowed 반영
```

- 새로 만들지 않는다. **"내 주문"(`countMyActiveOrders` · `sumMyActiveRemainingKrw` ·
  `findMyActiveOrders`) 부분만 그룹 전체로 넓힌다.**
- ⚠️ 이 화면은 **기간 필터가 없다.** 지금 이 순간의 재고다 — 화면에 `마지막 업데이트` 시각을 찍는다.
- ⚠️ 가용 KRW 와 가용 USDT 를 **더하지 않는다**(E4).

#### 3.3.4 P2P 매칭 + 정산 관점

컬럼: 매칭 코드 · 레그 · 상태 · 구매측/판매측 파트너 · 원화 · USDT · 환율 · 요율 · 분쟁 배지.

**정산 관점** — 별도 메뉴가 아니라 이 화면의 **프리셋 필터 + 컬럼 세트**다(`?view=settlement`).

- 컬럼: `settled_at` · `settlement_tx_hash` · `settle_retry_count` · `settle_failure_reason` · `leg_type`
- 크로스 파트너 온체인 건은 `p2p_transfer_requests` 로 실행 결과를 조인한다
  (`settlement_code` 기준. ⚠️ **INNER 레그는 이 테이블에 행이 없다** — 온체인 전송 자체가 없다.
  운영 47건 중 44건이 INNER — **활성화 전 기준이다**(§6.1). 활성화되면 크로스 파트너
  ONCHAIN 이 기본이 된다. 어느 쪽이든 **행이 없는 것을 "실행 실패"로 그리면 안 된다**)
- ☠️ 정산 실패의 판별자는 **상태가 아니라 `settle_failure_reason` 의 유무**다.
  DDL 이 명시한다 — *"정산 실패는 매칭 상태를 바꾸지 않는다(SETTLING 유지)."*
  상태로 세면 실패 건이 **0 으로 나온다.**

**매칭 상세** — 한 거래의 생애를 한 줄로:

```
구매 주문 ─→ 판매 주문 ─→ 매칭 ─→ 은행 입금 확인 ─→ (분쟁) ─→ 정산 ─→ 자금 기록
p2p_deposit_  p2p_withdraw_  p2p_    matches.         matches.  matches +    ledger_
orders        orders         matches bank_confirmed_at disputed_ transfer_    entries
                                                       at 등     requests
```

☠️ **운영 DB 실측 (2026-08-28) — 초판 표의 테이블 3개를 정정한다.**

| 테이블 | 운영 상태 | 결론 |
|---|---|---|
| `p2p_bank_confirmations` | **미생성** | 조인 불가. 은행 확인은 `p2p_matches.bank_confirmed_at` 시각만 |
| `p2p_incidents` | **미생성** | 쓰지 않는다 |
| `p2p_disputes` | **실재 (56행)** | 다만 `p2p_matches.disputed_at` 이 **56/56 전부 커버**한다. 분쟁 배지·라운드는 매칭 컬럼만으로 충분하고, 사유·메모는 어차피 안 내린다 |
| `settlement_balances` | 실재하나 **누적 잔액** | 매칭 단위 분해 없음 → 자금 기록은 `ledger_entries` (`reference_type IN ('P2P_SETTLEMENT','P2P_MATCH')` — 개명 과도기라 둘 다 본다) |

⚠️ 미생성 테이블을 조인하면 **컴파일은 통과하고 조회 시점에 죽는다.**

- 요율 스냅샷(`fee_rate` · `rebate_rate` · `withdraw_fee_rate`)은 **퍼센트 단위**다 —
  `formatRate` 로 그린다(`formatPercent` 는 ×100 이다, 2026-08-27 수정 건).
- ⚠️ 상대가 그룹 밖이면 이름은 `외부 그룹`, 그 쪽 요율은 **비운다**. 빈 요율은 "0%" 가 아니라
  "남의 상업 조건이라 보여 주지 않는다"는 뜻이다.
- ⚠️ TORQ 레그는 판매 주문이 **아예 없다**. 정산 블록도 마찬가지다 — LP 가 직접 전송하므로
  **우리 실행 기록이 없는 것이 정상**이다.
- ⚠️ `INNER_ps_...` 형태의 **합성 tx_hash 가 과거 행에 실재한다**(체인에서 조회되지 않는다).
  TX 링크를 걸기 전에 실제 해시인지 판별한다 — 안 그러면 익스플로러 404 로 이어진다.
- 분쟁은 **상태·시각·라운드만**. `dispute_reason` · `resolve_memo` · `dispute_evidence_url` 은
  내려보내지 않는다 — 실명과 계좌가 그대로 적힌 행이 실재한다.

---

## 4. 승계하는 규칙 (새로 정하지 않음)

| 규칙 | 출처 |
|---|---|
| 그룹 스코프 `root_partner_id = 나` 고정. 타 그룹은 어떤 경로로도 노출 금지 | HQ_CONSOLE_DESIGN §1.1 |
| PII 마스킹(예금주 `홍*동`, 계좌 뒤 4자리) — **조회·CSV 동일** | D3 · `HqPiiMasker` |
| 자유 텍스트 메모 · 증빙 URL · `partner_metadata` 는 **DTO 에 필드를 두지 않는다** | Phase 4 |
| KRW(정수 원)와 USDT(소수 18) 절대 합산 금지 | `formatKrw` / `formatAmount` 타입 분리 |
| 네이티브 코인(BNB · POL · TRX)은 서버에서 제외 | `currencies.currency_type` |
| CSV 반출 1회 = `partner_audit_logs` 1행. **기록 실패 시 파일도 나가지 않는다** | Phase 4.5 |
| CSV 상한 10,000행, 초과 시 **잘린 파일 대신 400** | `MAX_EXPORT_ROWS` |
| 페이지 상한 100건 / 기본 20건 | `HqExplorerService` |
| **1차는 조회 전용.** 쓰기는 파트너 관리뿐 — P2P 취소는 2차(5-F) | D8 · D9 · E7 · E8 |

### 4.1 새로 정하는 규칙 — P2P 주문 취소 (E7 · **적용은 2차 5-F**)

> ⚠️ **1차 범위가 아니다.** 아래는 2차에 들어갈 때의 규칙을 미리 확정해 둔 것이다 —
> 화면을 뷰어로 지을 때 취소 버튼 자리를 비워 두되, **상태 게이트에 필요한 필드**
> (`status`)는 목록 응답에 넣어 둔다. 나중에 컬럼을 다시 뚫지 않기 위해서다.

HQ 가 산하 파트너의 P2P 주문을 **취소**할 수 있다. 강제정산 · 직접전환 · USDT 전환 승인/거부는
**가져오지 않는다** — 자금 이동 방향을 바꾸는 결정이라 총판이 대신 누를 자리가 아니다.

| 항목 | 규칙 |
|---|---|
| 대상 | 출금 주문 · 구매 주문 |
| 가능 상태 | 출금 주문은 **`PENDING` · `PARTIALLY_MATCHED` 만**(admin 화면과 동일 게이트). 그 외에는 버튼을 **렌더하지 않는다** |
| 권한 | 대상 주문의 파트너가 **내 그룹(`root_partner_id = 나`)인지 서버에서 재검증**. 화면 필터를 신뢰하지 않는다 |
| 확인 | **OTP 필수** (`OtpVerificationAspect` 재사용) |
| 기록 | `partner_audit_logs` 1행 (`HqAuditAction` 에 취소 액션 추가) |
| 사유 | admin 은 `'Admin 취소'` 를 고정 문자열로 넣는다. HQ 는 **`'HQ 취소'` + 파트너 코드**로 구분되게 남긴다 — 누가 눌렀는지 로그에서 갈려야 한다 |

⚠️ 취소는 **락 해제·잔여 환급**을 동반한다. 기존 취소 API 를 호출해 그 로직을 재사용한다.
HQ 가 직접 상태를 쓰지 않는다 — 상태 전이 규칙을 두 벌로 만들면 반드시 어긋난다.

### 4.2 새로 정하는 규칙 — 타임라인의 `changed_by`

`transaction_status_history.changed_by` 에는 **관리자 이메일이 그대로 들어간다**
(주석: *"시스템: SYSTEM, 관리자: admin email, API: partner_id"*).
HQ 는 그룹 총판이지 내부 운영자가 아니다. **원본을 그대로 내리지 않고 `SYSTEM` / `ADMIN` / `PARTNER` /
`SCHEDULER` 수준으로 환원**해서 내린다. `note` · `metadata` 도 화이트리스트한 키만 통과시킨다.

---

## 5. API 목록 (신규)

```
GET /api/hq/transactions/deposits              목록 (XPage)
GET /api/hq/transactions/deposits/summary      통화축별 합계
GET /api/hq/transactions/deposits/{id}         상세 (타임라인·집금·원장·분배 포함)
GET /api/hq/transactions/deposits/export       CSV

GET /api/hq/transactions/withdrawals           목록
GET /api/hq/transactions/withdrawals/summary   통화축별 합계 + 상태군 건수
GET /api/hq/transactions/withdrawals/{id}      상세
GET /api/hq/transactions/withdrawals/export    CSV

GET  /api/hq/p2p/withdraw-orders                출금 주문 목록
GET  /api/hq/p2p/withdraw-orders/{id}           출금 주문 상세
GET  /api/hq/p2p/orders                         구매 주문 목록
GET  /api/hq/p2p/orders/{id}                    구매 주문 상세
GET  /api/hq/p2p/matches                        매칭 목록 — view=settlement 로 정산 관점
GET  /api/hq/p2p/matches/{id}                   매칭 상세 (생애 타임라인)
GET  /api/hq/p2p/pool                           출금 풀 — partner-api /pool 을 그룹 스코프로
GET  /api/hq/p2p/summary                        매칭·원화·USDT·분쟁·정산 실패
GET  /api/hq/p2p/{withdraw-orders|orders|matches}/export   CSV

── 2차 (5-F · 액션) ─────────────────────────────────
POST /api/hq/p2p/withdraw-orders/{id}/cancel    취소 (OTP · 그룹 검증 · 감사 로그)
POST /api/hq/p2p/orders/{id}/cancel             취소 (동일 조건)

☠️ /p2p/settlements 는 만들지 않는다 — p2p_settlements 는 제거 대상이다 (§3.3)
```

**공통 파라미터**: `from` · `to` · `partnerId` · `currencyId` · `networkId` · `status`(다중) · `keyword`
+ 데이터셋별 추가(`depositType` · `depositMethod` · `withdrawalType` · `requestSource` · `orderType`).

**재사용**: 그룹 스코프 해석 · 마스킹 · CSV 작성 · 감사 로그는 Phase 4 의
`HqExplorerService` · `HqPiiMasker` · `HqCsvWriter` 를 **그대로 쓴다**. 새로 만들지 않는다 —
두 벌이 되면 한쪽만 마스킹이 빠지는 날이 온다.

---

## 6. 규모 — 실측 (2026-08-28, 운영 DB)

> ☠️ **이 숫자는 전부 「P2P 매칭 활성화 이전」 기준이다. 예측치로 쓰지 마라.**
> 지금 준비하는 것이 바로 그 활성화다 (오너 확인, 2026-08-28).

### 6.1 지금 P2P 는 사실상 TORQ 폴백만 돌고 있다

| 레그 | 건수 | 비중 |
|---|---|---|
| `TORQ` (LP 전액 대체) | 1,681 | **93%** |
| `P2P` (크로스 파트너) | 88 | 5% |
| `PARTNER` (동일 파트너) | 32 | 2% |

```
월별 P2P 레그:  2026-06  71건  →  07  28건  →  08  21건     (줄고 있다)
월별 TORQ 레그: 2026-06 127건  →  07 776건  →  08 779건     (전부를 먹었다)
파트너 62개 중 p2p_withdraw_enabled = 60. 그런데 p2p_withdraw_orders 는 50건뿐 —
공급(판매 주문)이 없어서 전부 LP 로 흘렀다. 「출금 풀」 화면이 0 으로 뜨는 이유가 이것이다.
```

**활성화되면 이 비율이 뒤집힌다.** 그때 비로소 실사용에 들어가는 것들:

- 크로스 파트너 **온체인 정산** → `p2p_transfer_requests` 행이 예외가 아니라 기본이 된다
  (§3.3.4 의 "운영 47건 중 44건이 INNER" 는 **활성화 전** 숫자다)
- **분쟁 · 은행 입금 확인** 경로
- **정산 실패 지표**(`settle_failure_reason`) — 지금은 거의 안 잡히지만 이 화면의 존재 이유다

### 6.2 현재 행 수 (기준선일 뿐, 예측 아님)

| 테이블 | 행 수 | 기간 |
|---|---|---|
| `deposits` | 4,033 | 2025-08-05 ~ |
| `withdrawals` | 1,255 | 2025-09-01 ~ |
| `p2p_matches` | 1,801 | 2026-06-12 ~ |
| `p2p_deposit_orders` | 1,768 | 2026-06-11 ~ |
| `p2p_withdraw_orders` | **50** | 2026-06-09 ~ |
| `transaction_status_history` | 16,619 | 2026-04-04 ~ |

### 6.3 `EXPLAIN` — 입금 요약은 정상, `p2p_matches` 는 풀스캔

```
입금 요약 (그룹 × 기간 × 통화축)
  p : ref  idx_root_partner    rows=1     Using index; Using temporary
  d : ref  idx_partner_status  rows=115   Using index condition        → 정상

P2P 매칭 (기간 정렬)
  m : ALL  possible_keys=NULL  rows=1725  Using where; Using filesort  → 풀스캔
```

### 6.4 결론 — `p2p_matches(created_at)` 인덱스를 **활성화 전에** 추가할 것

☠️ **"지금 1,725행이라 싸다"는 근거로 미루면 안 된다.** 그 숫자가 곧 무의미해지기 때문이다.
판단 근거는 행 수가 아니라 **비대칭**이다:

| | 지금 추가 | 활성화 후 추가 |
|---|---|---|
| 대상 | 1,801행 | 미지수 (그때는 뜨거운 테이블) |
| 소요 | 즉시 | 운영 이벤트 (락 · 타이밍 조율) |
| 실패 시 | 되돌리기 쉬움 | 트래픽 중 대응 |

그리고 `created_at` 은 **단조 증가**라 인덱스 유지 비용이 거의 없다(페이지 분할이 사실상 없다).
"쓰기 잦은 테이블에 인덱스는 비용"이라는 일반론이 이 컬럼에는 약하게 적용된다.

**적용 완료** (2026-08-28, 오너 승인 후 운영 + 개발 DB + `CRYPTOMENTS_V2_DDL.sql`):

```sql
ALTER TABLE p2p_matches ADD INDEX idx_pm_created (created_at), ALGORITHM=INPLACE, LOCK=NONE;
-- 롤백: ALTER TABLE p2p_matches DROP INDEX idx_pm_created;
```

☠️ `ALGORITHM=INPLACE, LOCK=NONE` 을 **명시**했다. 없으면 MySQL 이 테이블 복사·쓰기 잠금 경로로
조용히 빠질 수 있다 — 명시하면 그 경우 **에러로 거부**되므로 "괜찮겠지"가 아니라 보장이 된다.

실행 계획 실측:

| | type | 스캔 행 | 정렬 |
|---|---|---|---|
| 전 | `ALL` | 1,839 | `Using filesort` |
| 후 | `range` (`idx_pm_created`) | **810** | **`Backward index scan`** (정렬 소멸) |

`deposits` · `withdrawals` 는 **추가하지 않는다.** 요약 쿼리가 이미 인덱스를 타고 있고,
활성화가 늘리는 것은 P2P 축이지 이쪽이 아니다.

### 6.5 화면 설계에 반영할 것 (활성화 전제)

- ☠️ **기간 필터에 기본값을 반드시 건다.** "전체 기간" 기본 조회를 만들지 않는다 —
  지금은 멀쩡하고 활성화 후에 죽는다.
- 요약 쿼리도 **기간 없이 호출될 수 없게** 한다 (서버에서 기본 기간을 강제).
- 파일럿 데이터로는 **정산 실패 · 분쟁 · 크로스 파트너 정산 경로를 실검증할 수 없다.**
  구현 단계에서 이 한계를 보고에 남긴다.

---

## 7. 단계 분할

**1차 = 뷰어 전부 조회 전용. 2차 = 액션** (E8).

| 단계 | 내용 | 성격 | 근거 |
|---|---|---|---|
| **5-A** | **P2P 필수 3화면** — 출금 주문 · 거래 · 출금 풀 | 뷰어 | 오너가 지목한 필수 화면. 출금 풀은 `PartnerP2pPoolService` 재사용이라 가장 싸다 |
| **5-B** | 모바일 내비 섹션 드롭다운 전환 | 뷰어 | 5-A 만으로 메뉴가 **9 → 12개**가 된다. 평면 탭이 이미 잘려 있어 더 미룰 수 없다 (§2.2) |
| **5-C** | 입금 · 출금 목록 + 요약 + 필터 | 뷰어 | 통화축별 합계가 이 Phase 의 실제 신규 가치다 |
| **5-D** | 입금 · 출금 상세 (타임라인 · 집금 · 원장 · 분배) | 뷰어 | 조인 쿼리와 `changed_by` 환원이 여기 몰려 있다 |
| **5-E** | P2P 매칭 + 정산 관점 + 매칭 상세 | 뷰어 | 정산 실패(`settle_failure_reason`)가 새로 드러나는 값이다 · 인덱스 판정 결과 반영 |
| **5-F** | **P2P 주문 취소** (OTP · 그룹 재검증 · 감사 로그) | **액션** | 화면이 선 뒤에 붙인다. 규칙은 §4.1 에 이미 확정 |

**5-A 를 먼저 두는 이유** — 오너가 지목한 3화면이고, 셋 다 **admin·partner-api 에 이미 있는 것을
그룹 스코프로 옮기는 일**이라 새로 설계할 것이 가장 적다. 입금·출금 요약(5-C)은 신규 집계 쿼리라
인덱스 판정(§6)이 선행돼야 하는데, 그 사이에 5-A 가 나간다.

⚠️ 5-A 를 지을 때 **취소 버튼 자리는 비워 두되 `status` 는 응답에 넣는다.** 5-F 에서 상태 게이트
(`PENDING` · `PARTIALLY_MATCHED`)에 필요한 값이라, 빠뜨리면 컬럼을 다시 뚫게 된다.

각 단계는 **빌드 검증 → 리뷰 → 커밋** 사이클로 끊는다(Work Orchestration Rules).

---

## 8. 비범위 (명시적으로 하지 않는 것)

- **1차(5-A~5-E)의 모든 쓰기 액션** — 취소 포함. 2차 5-F 로 분리한다(E8).
- **강제정산 · 직접전환 · USDT 전환 승인/거부 · 분쟁 판정 · 출금 승인** — 2차에서도 넣지 않는다(E7).
- P2P 회원 관리 · 수수료 설정 · 정책 화면 (admin 에 있으나 HQ 범위 밖).
- 탐색기 데이터셋 축소 (E1).
- **`p2p_settlements` 를 원천으로 쓰는 화면·API** — 제거 대상이고 커버리지도 45건뿐이다 (§3.3).
- USDT 환산 총액 · 실효 요율 재표기 (E4 · D1).
- 회원 개인정보 원본 노출 — 마스킹 해제 경로는 만들지 않는다.
- 실시간 갱신(WebSocket · 폴링). 조회 시점 스냅샷이다.
