# HQ Phase 5 뷰어 — 구현 지침서 (서브에이전트용)

> **선행 문서**: `v2-docs/HQ_PHASE5_TRANSACTIONS_DESIGN.md` — **먼저 읽을 것.**
> 이 문서는 "어떻게 짜는가"만 담는다. "무엇을 만드는가"는 설계서에 있다.
> **작성**: 2026-08-28 · **범위**: 5-A ~ 5-E (뷰어). 5-F(취소 액션)는 이 범위가 **아니다.**

---

## 0. 작업 위치

| 대상 | 경로 | 브랜치 |
|---|---|---|
| 백엔드 (`partner-api`) | `/data/paseo/worktrees/0l672syo/hq-viewer` | `hq-viewer` |
| 프런트 (`hq-ui`) | `/data/paseo/worktrees/105g8hw8/hq-viewer` | `hq-viewer` |

- 두 repo **브랜치명이 같아야** 프리뷰가 짝을 찾는다. 바꾸지 말 것.
- **커밋·푸시 금지.** 변경은 working tree 에 둔다. 커밋은 오케스트레이터가 사람 확인 후 한다.
- 다른 worktree(`clingy-fox` · `hq-fee-display` 등)를 건드리지 말 것.

---

## 1. 절대 규칙 — 어기면 서비스가 죽는다

### 1.1 MyBatis `<script>` 안에서 `<` 를 쓰지 않는다

```java
// ☠️ 컴파일은 통과하고 기동 시점에 SAXParseException 으로 전 서비스가 죽는다 (2026-06-11 운영 장애)
"<if test='x != null'> AND a < b </if>"          // 금지
"<if test='x != null'> AND a &lt; b </if>"       // 이렇게
"<if test='x != null'> AND a <![CDATA[ < ]]> b </if>"  // 또는 이렇게
```

같지 않음은 `!=`, 미만·이하는 `&lt;` / `&lt;=` 또는 `CDATA`.

### 1.2 페이지네이션 3규칙 — 지키면 자동, 어기면 안 먹는다

```java
@Mapper
public interface HqXxxMapper {
    // (1) 첫 파라미터 XPagination  (2) 마지막 파라미터 Class<?> cls  (3) 반환 XPage<T>
    @Select("<script>SELECT ... <where>...</where></script>")
    XPage<HqXxxRow> search(XPagination pagination, @Param("q") HqXxxQuery q, Class<?> cls);
}
```

- **ORDER BY · LIMIT · COUNT 쿼리를 직접 쓰지 않는다.** `XResultInterceptor` 가 만든다.
- 정렬이 꼭 필요하면 `XPagination` 의 정렬 파라미터를 쓴다. SQL 에 박지 않는다.
- XML 매퍼 파일 금지. **interface + 어노테이션만.**

### 1.3 그룹 스코프는 서버에서 재검증한다

```java
Long hqId = getSession().getPartnerId();   // XSessionController<HqSessionData>
```

- 모든 조회는 `root_partner_id = hqId` 로 **서버가** 좁힌다. 프런트가 보낸 `partnerId` 를 그대로 믿지 않는다.
- 요청의 `partnerId` 가 내 그룹이 아니면 **400/403**. 빈 결과로 조용히 넘기지 않는다
  (조용히 비우면 "데이터가 없다"로 읽혀 타 그룹 조회 시도가 드러나지 않는다).
- 기존 구현을 그대로 따른다: `HqExplorerService` · `HqPartnerMgmtService` 의 스코프 해석 로직.

### 1.4 개인정보

- 마스킹은 **`HqPiiMasker` 하나만** 쓴다. 새로 만들지 않는다.
- **DTO 에 필드를 두지 않는** 것: 분쟁 사유 · 판정 메모 · 증빙 URL · 이체 참조 ·
  `partner_metadata` · eKYC 식별자 · 취소/반환 자유 텍스트.
- `transaction_status_history.changed_by` 는 **관리자 이메일이 원본으로 들어 있다.**
  `SYSTEM` / `ADMIN` / `PARTNER` / `SCHEDULER` 로 **환원해서** 내린다 (설계서 §4.2).

### 1.5 금액·요율

| 축 | 타입 | 서버 직렬화 | 프런트 표시 |
|---|---|---|---|
| USDT 등 `DECIMAL(36,18)` | `BigDecimal` | `@JsonSerialize(using = PlainBigDecimalSerializer.class)` | `formatAmount(str)` |
| KRW `BIGINT` (정수 원) | `Long` | 기본 | `formatKrw(number)` |
| 요율 `DECIMAL(10,6)` | `BigDecimal` | `PlainBigDecimalSerializer` | **`formatRate(str)`** |
| 서버가 나눈 비율 (0~1) | `BigDecimal` | `PlainBigDecimalSerializer` | `formatPercent(str)` |

☠️ **`formatRate` 와 `formatPercent` 를 바꿔 쓰지 마라.** DB 요율 컬럼은 퍼센트 단위(`0.4` = 0.4%)라
`formatPercent`(×100)를 태우면 100배가 된다. 2026-08-27 에 실제로 났던 버그다.

☠️ **KRW 와 USDT 를 더하지 마라.** 합계는 통화축별로 분리한다. 가로 합계 행을 만들지 않는다.

### 1.5b 활성화 전제 — 기간 기본값을 반드시 건다

☠️ **P2P 매칭은 아직 활성화 전이다.** 지금 운영 데이터가 적은 것은 "원래 적은 것"이 아니라
**아직 안 켠 것**이고, 우리가 지금 만드는 화면이 그 활성화를 위한 준비다 (설계서 §6.1).

- **"전체 기간" 기본 조회를 만들지 않는다.** 목록·요약 모두 기간 기본값(예: 최근 30일)을
  서버에서 강제한다. 지금은 멀쩡하고 **활성화 후에 죽는다.**
- 파일럿 데이터로는 정산 실패 · 분쟁 · 크로스 파트너 정산 경로를 **실검증할 수 없다.**
  구현했으나 실데이터로 확인 못 한 것은 보고서에 적는다.

### 1.6 Axim 프레임워크

- Entity: `@XEntity("table")` · `@XColumn("col")` · `@Getter @Setter @Builder(toBuilder = true)
  @NoArgsConstructor @AllArgsConstructor`. **`@Data` 금지.**
- Entity 에 `@XIgnoreColumn` 으로 JOIN 데이터를 붙이지 않는다. JOIN 결과는 **별도 데이터 클래스**.
- `IXRepository.save()` 는 **PK(Long)** 를 반환한다. 엔티티가 아니다.
- 예외는 `ErrorCodes` 상수 + 도메인 예외 (`XRestException` 상속).
- Gradle: `common` 의 `implementation` 의존성은 전파되지 않는다. 직접 쓰는 건 모듈
  `build.gradle` 에도 명시.

---

## 2. 프런트 규칙 (`hq-ui`)

### 2.1 `@/lib/amount` 외의 수치 처리 금지

```ts
Number(value)      // ☠️ 금지
parseFloat(value)  // ☠️ 금지
```

서버가 `DECIMAL(36,18)` 을 **문자열**로 내린다. `number` 로 바꾸는 순간 정밀도가 깨지고
한 번 깨진 값은 화면 어디서도 복구되지 않는다.

### 2.2 널 체크는 `== null` (느슨한 비교)

서버가 `non_null` 직렬화라 값 없는 필드는 `null` 이 아니라 **아예 없다**(`undefined`).
`=== null` 은 성립하지 않는다.

### 2.3 "값 없음"과 "0" 을 구분한다

`-` 와 `0` 은 다른 사실이다. 분모가 0 이라 계산 불가인 것을 `0%` 로 그리지 않는다.

### 2.4 화면 상단 주의사항(`notes`)을 반드시 적는다

기존 화면들이 전부 그렇게 되어 있다. **틀리기 쉬운 것만** 적는다 — 설계서의 ⚠️·☠️ 항목이 재료다.

### 2.5 기존 컴포넌트를 쓴다

`@/components/ui/*` (Card · Badge · Button · Input) · `@/components/hq/*`
(PeriodFilter · PartnerSelect · AxisSelect · StatsMetaBar). 새로 만들기 전에 있는지 본다.

---

## 3. 배치 분할

### 배치 A — 5-A + 5-B

1. **P2P 출금 주문** (`/hq/p2p/withdraw-orders`) — 설계서 §3.3.1
2. **P2P 거래(구매 주문)** (`/hq/p2p/orders`) — §3.3.2
3. **P2P 출금 풀** (`/hq/p2p/pool`) — §3.3.3
4. **메뉴·라우트 골격** — 설계서 §2 표의 **6개 신규 메뉴 전부** 등록.
   아직 안 만든 4개(입금 내역 · 출금 내역 · P2P 매칭)는 **"준비 중" 플레이스홀더 뷰**로 둔다.
   → 배치 B·C 가 그 파일만 갈아끼우면 되므로 공용 파일 충돌이 사라진다.
5. **모바일 내비를 섹션 드롭다운으로 전환** (§2.2) — 메뉴가 12개가 되어 평면 탭이 감당 못 한다.

**원본 참조**(그대로 베끼지 말고 컬럼·상태·용어를 승계):
`/data/paseo/projects/cryptoments-admin/admin-ui/src/views/p2p/P2pWithdrawOrderListView.vue` ·
`P2pOrderListView.vue` · `P2pWithdrawPoolView.vue` 와 백엔드
`admin-api` 의 대응 컨트롤러/매퍼.

**출금 풀은 새로 만들지 않는다** — `PartnerP2pPoolService.getPoolStatus()` 가 이미 그룹 스코프다.
"내 주문" 부분만 그룹 전체로 넓힌 HQ 용 진입점을 만든다.

### 배치 B — 5-C + 5-D

입금·출금 목록 + 통화축별 요약 + 필터, 그리고 상세(타임라인·집금·원장·수수료 분배).
설계서 §3.1 · §3.2.

### 배치 C — 5-E

P2P 매칭 목록 + 정산 관점(`?view=settlement`) + 매칭 상세 타임라인. 설계서 §3.3.4.

---

## 4. 빌드 검증 (완료 기준)

```bash
# 백엔드
cd /data/paseo/worktrees/0l672syo/hq-viewer
./gradlew :partner-api:compileJava --offline

# 프런트 (vue-tsc 타입체크 포함)
cd /data/paseo/worktrees/105g8hw8/hq-viewer/hq-ui
npm run build
```

**둘 다 통과해야 완료다.** 통과하지 못한 채로 "완료"라고 보고하지 않는다.

⚠️ 컴파일 통과 ≠ 동작. `<script>` 안의 `<` 는 컴파일을 통과하고 **기동 때** 죽는다 —
매퍼를 작성했으면 §1.1 을 다시 확인한다.

---

## 5. 보고 형식

작업이 끝나면 다음을 보고한다.

1. 만든 파일 목록 (백엔드 / 프런트 구분)
2. 빌드 결과 두 줄 (백엔드 · 프런트, 실제 종료 코드)
3. **설계서와 다르게 구현한 것 + 그 이유** — 있으면 반드시. 없으면 "없음"
4. 확인하지 못한 것 (예: 실데이터로 검증 못 한 쿼리)

☠️ **모르는 것을 지어내지 않는다.** 스키마가 설계서와 다르면 DDL(`v2-docs/CRYPTOMENTS_V2_DDL.sql`)을
확인하고, 그래도 불분명하면 **보고에 적는다.** 추측으로 컬럼을 만들지 않는다.
