# U5 — 원장 타임라인 + 파트너 잔액 분류 (UI 마무리)

작성 2026-08-18 · repo `cryptoments`(admin-api, partner-api) + `cryptoments-admin`(admin-ui, partner-ui)
선행 완료: T1-d(표시 잔액을 원장으로 전환), U1~U4

---

## 배경

T1-d 로 **원장이 잔액의 정본**이 됐다. 그런데:

```
① 원장을 볼 수 있는 화면·API 가 없다
   "이 주문 잔여가 왜 이 값이냐"는 문의에 근거를 댈 수단이 관리자에게 없다

② 회원은 금액을 4분류로 보는데 파트너는 remainingAmount 단일값이다
   파트너가 "잔여가 왜 줄었나"를 이해할 수 없다 — 매칭 중인 돈과 대기 중인 돈이 안 갈린다
```

원장 실적재(운영):

```
LOCK 69 · UNLOCK 69 · CHARGE 38 · SETTLE 37 · RELEASE 11
```

**읽기 전용 작업이다. 원장에 쓰는 코드를 건드리지 마라.**

---

# A. 원장 조회 API (admin-api)

## A1. 엔드포인트

```
GET /api/admin/p2p/orders/withdraw/{id}/ledger
→ { balance, entries: [...] }
```

- 대상 테이블 `p2p_withdraw_entries` — 컬럼: `id`, `withdraw_order_id`, `type`, `amount_krw`, `match_id`, `memo`, `created_at`
- 정렬: **`id` 오름차순** (시간순 = 적재순). `created_at` 은 같은 트랜잭션에서 동일할 수 있으므로 `id` 를 쓴다
- `balance` 는 `SUM(amount_krw)` — **`P2pMatchingMapper.LEDGER_BALANCE` 상수를 참조**하거나
  기존 `P2pWithdrawLedgerMapper` 조회를 재사용하라. **SQL 을 손으로 복사하지 마라**
- 엔트리가 0건이어도 200 + 빈 배열. 404 를 던지지 마라

## A2. 응답 DTO

`P2pWithdrawLedgerResponse` (래퍼) + 중첩 `Entry`. **멤버 변수 JavaDoc 필수.**

각 엔트리에 **누적 잔액(running balance)** 을 담아라 — 타임라인의 핵심 가치다.
`amount_krw` 만 나열하면 운영자가 암산해야 한다. 자바에서 순회하며 누적하라(SQL 윈도우 함수 금지 —
`XResultInterceptor` 규약과 충돌할 수 있다).

```
Entry: id · type · amountKrw · runningBalance · matchId · matchCode · memo · createdAt
```

`matchCode` 는 `p2p_matches` 조인으로. **`match_id` 가 NULL 인 엔트리(CHARGE/RELEASE)가 정상**이므로
LEFT JOIN 을 쓰고, 없으면 null 로 둔다.

## A3. 매퍼

`admin-api` 에 전용 매퍼를 만들되, **잔액 계산은 재사용**한다. `@Mapper` + `@Select` interface,
XML 금지. `<script>` 를 쓰면 `<`·`<=`·`<>` 직접 사용 금지 → `&lt;` / `!=`
(위반 시 부팅 때 전 서비스가 죽는다. 2026-06-11 장애).

---

# B. 원장 타임라인 화면 (admin-ui)

**파일** `admin-ui/src/views/p2p/P2pWithdrawOrderDetailView.vue`

기존 카드들 아래에 "출금 원장" 카드를 추가한다. U4 에서 만든 분쟁 타임라인의 마크업 패턴을 따르라.

- 시간순(오래된 것 위), 타입별 색 마커
- 각 행: 시각 · 타입(한글) · 금액(부호 그대로) · 누적 잔액 · 매칭 코드(있으면 링크) · 메모
- 마지막 행의 누적 잔액이 **화면 상단의 잔여와 일치해야 한다** — 어긋나면 그 자체가 신호다
- 엔트리 0건이면 "원장 기록 없음" (T1-b 백필 이전 주문일 수 있다)

**타입 한글 라벨은 이 화면 로컬 상수로 둔다.** U1 의 `STATUS_LABELS_KO` 에 넣지 마라 — 상태가 아니라 원장 타입이다.

```
CHARGE   충전(주문 등록)
LOCK     매칭 예약
UNLOCK   예약 해제
SETTLE   입금 확정
RELEASE  잔여 회수
```

부호 규약: `CHARGE +` / `LOCK −` / `UNLOCK +` / `SETTLE −` / `RELEASE −`.
**금액을 절대값으로 바꾸지 마라.** 부호가 보여야 흐름이 읽힌다.

---

# C. 파트너 잔액 분류 (partner-api + partner-ui)

## C1. 응답 확장

**파일** `partner-api/.../dto/p2p/P2pWithdrawOrderResponse.java`

현재 `remainingAmount` 하나뿐이다. 회원 페이지와 **같은 분류**를 추가한다.

| 필드 | 의미 | 산식 |
|---|---|---|
| `receivedKrw` | 입금 확인됨 | `-Σ SETTLE` |
| `incomingKrw` | 입금 확인 대기 | `-(Σ LOCK + Σ UNLOCK)` |
| `waitingKrw` | 매칭 대기 | `max(0, 잔액 − 전환요청)` |
| `remainingAmount` | (기존 유지) | 원장 잔액 |

⚠️ **산식을 새로 구현하지 마라.** T1-d 에서 `open-api` 가 쓰는 것과 **같은 계산기를 재사용**하라
(`P2pWithdrawLedgerService` 의 타입별 합계 조회 + 분류 산출). 두 곳에 같은 산식이 따로 있으면
언젠가 갈라진다 — 그게 이번에 고친 문제의 원인이었다.

계산기가 `open-api` 모듈에만 있으면 **`core` 로 올려서 양쪽이 쓰게 하라.** 복사하지 마라.

## C2. N+1 금지

출금 주문 **목록**에도 분류를 넣는다면 반드시 배치 조회를 쓰라
(T1-d 가 만든 `sumByOrderIdsAndType(orderIds)`). 주문마다 질의하면 목록이 느려진다.
빈 목록은 호출 전에 차단하라 — `IN ()` 는 문법 오류다.

**판단이 필요하면**: 목록에는 `remainingAmount` 만 두고 **상세에만 분류**를 넣어도 좋다.
그 편이 단순하면 그렇게 하고, 이유를 보고하라.

## C3. 화면

**파일** `partner-ui/src/views/partner/p2p/P2pWithdrawOrderDetailView.vue`

현재 잔여 한 줄만 보여준다(약 :306). 4분류로 바꾼다.

- 회원 화면(`p2p-ui/src/views/DashboardView.vue`)의 분류·색·문구를 **참고해 톤을 맞추되**,
  파트너 콘솔의 기존 컴포넌트(`AmountDisplay` 등)를 쓰라
- USDT 전환·강제 정산 액션 문구(약 :341, :352)가 "잔여"를 말한다 —
  **어떤 값이 실제 대상인지 확인하고** 문구를 정확히 하라.
  (전환/정산 대상은 `remainingAmount` 이지 `waitingKrw` 가 아닐 수 있다. 백엔드 로직을 읽고 판단하라)
- **금액 산식을 프론트에 만들지 마라.** 서버 값을 그리기만 한다 (T1-d 원칙)

---

## 코딩 규칙

- Java 17 · Lombok(`@Data` 금지) · DTO 멤버 JavaDoc 필수
- MyBatis `@Mapper` + `@Select` interface. XML 금지. ORDER BY/LIMIT/COUNT 직접 작성 금지
- `<script>` 내 `<`·`<=`·`<>` 금지 → `&lt;` / `!=`
- **DDL 을 만들지 마라. DB 에 접속하지 마라**
- **원장에 쓰는 코드(`P2pWithdrawLedgerService` 의 charge/lock/unlock/settle/release)를 변경하지 마라.** 읽기만
- T1-d 에서 손대지 않기로 한 곳은 이번에도 손대지 마라:
  `computeRemainder` · USDT 전환 게이트 · 초과 확인 가드 · `refund_type` 차단
- Vue 는 기존 스타일. 새 라이브러리 금지

## 완료 기준

```
1  ./gradlew :core:compileJava :admin-api:compileJava :partner-api:compileJava :open-api:compileJava 통과
2  admin-ui · partner-ui 빌드 통과
3  원장 API 가 엔트리 0건일 때 200 + 빈 배열 (404 아님)
4  타임라인 마지막 행의 누적 잔액 = 주문 상세 상단의 잔여
5  분류 산식이 한 곳에만 존재한다 (open-api / partner-api 중복 없음 — grep 으로 증명)
6  목록에 분류를 넣었다면 배치 조회를 쓴다 (N+1 없음)
7  프론트에 금액 산식이 없다
8  원장 쓰기 코드 무변경 (diff 로 증명)
```

## 보고 형식

- 작업별 수정 파일:라인 + 한 줄
- **C1 에서 계산기를 어디에 두고 어떻게 공유했는지** (복사하지 않았음을 grep 으로 증명)
- C2 판단(목록에 분류를 넣었는지 / 안 넣었다면 이유)
- C3 에서 USDT 전환·강제 정산의 실제 대상 금액이 무엇인지 (코드 근거)
- 원장 쓰기 무변경 증명
- 지침이 실제 코드와 어긋난 지점 — **고치지 말고 먼저 보고**
- 빌드 결과

## 착수 전 필수 확인

```
common/.../entity/P2pWithdrawEntry.java          (컬럼·부호 규약)
common/.../enums/P2pWithdrawEntryType.java
common/.../mapper/P2pWithdrawLedgerMapper.java   (T1-d 가 만든 배치 조회 포함)
core/.../p2p/P2pWithdrawLedgerService.java       (balances/amounts — 재사용 대상)
open-api/.../dto/p2p/P2pPageAmounts.java         (T1-d 분류 산식 — 이걸 공유해야 한다)
admin-api/.../mapper/P2pOrderSearchMapper.java   (출금 상세 현재 조회)
admin-ui/src/views/p2p/P2pWithdrawOrderDetailView.vue
partner-ui/src/views/partner/p2p/P2pWithdrawOrderDetailView.vue
```

지침과 다르면 **멈추고 보고하라.** 지침을 쓴 사람이 틀렸을 수 있다.
