# T1-d — 표시용 잔액을 원장으로 전환

작성 2026-08-18 · repo `cryptoments`(core, partner-api, open-api, admin-api) + `cryptoments-admin`(p2p-ui, partner-ui, admin-ui)
선행 완료: T1-b 백필 217행, T1-c 판정 경로 원장 전환

---

## 배경 — 지금 화면이 거짓말을 한다

잔액 정본은 원장 `p2p_withdraw_entries`(잔액 = `SUM(amount_krw)`)인데 **표시만 구 누적 컬럼**을 본다.
그 결과 종결·환불된 주문에 잔여가 남은 것처럼 보인다. 운영 실측:

```
구 컬럼(krw − confirmed) vs 원장 불일치   11건 — 전부 원장이 정답
  예) 주문 28  액면 3,000,000 · 전액 환불 완료 · 원장 0
      그런데 화면엔 "잔여 3,000,000" 으로 보인다  ← 구 컬럼은 환불을 모른다

원장 음수 잔액 0건 · CANCELLED 1건도 원장 0  → 데이터는 건강하다
```

**전환하면 이 11건이 "잔여 있음" → "잔여 0" 으로 바뀐다. 그게 정정이다.**

---

## A. 선행 — `cancelOrder` 의 RELEASE 누락을 먼저 막는다 ⚠️

**이걸 먼저 하지 않으면 표시 전환이 곧바로 유령 잔여를 만든다.**

`core/.../p2p/P2pWithdrawService.java` 의 `cancelOrder` 는 `applyRemainderResolution` 을 거치지
않고 상태만 `CANCELLED` 로 바꾼다(약 :284-287). 같은 파일의 `forceSettle`(약 :327)과
`completeDirectWithdrawal`(약 :413)은 `applyRemainderResolution` 을 태워 RELEASE 를 적는다.

지금까지 안 터진 이유는 취소가 1건뿐이었고 그건 T1-b 백필이 덮었기 때문이다.
**다음 취소부터 원장 잔액이 영구히 양수로 남는다.**

**수정**: 취소 시에도 잔여를 원장에서 해소한다. 기존 `applyRemainderResolution` 경로를 재사용하고
**새 해소 로직을 만들지 마라.** 잔여 처리 종류는 기존 `P2pRemainderResolution` 값역을 따른다.

주의:
- 잔여가 0이면 RELEASE 를 적지 마라 (0원 엔트리는 노이즈다)
- 이미 해소된 주문(재취소)에 두 번 적히지 않게 하라 — 기존 `refund_type` 가드(약 :544)의
  의도를 확인하고 같은 방식으로 막아라
- **취소는 자금이 실제로 나가는 경로가 아니다.** 원장 정리만 하고 지급 로직을 건드리지 마라

---

## B. 표시 경로 전환

정본 산식은 **이미 있는 것을 쓴다**:

```
common/.../mapper/P2pMatchingMapper.java  의  LEDGER_BALANCE 상수
common/.../mapper/P2pWithdrawLedgerMapper.java
core/.../p2p/P2pWithdrawLedgerService.java  의  balances()
```

⚠️ **`LEDGER_BALANCE` SQL 을 손으로 복사해 다시 쓰지 마라.** 매퍼 주석이 이미 경고한다 —
복사하면 게이트 조건이 빠진다. 상수를 참조하라.

### B1. 파트너 주문 잔여

**파일** `partner-api/.../dto/p2p/P2pWithdrawOrderResponse.java` (약 :105, :120)

```
현재   remainingAmount = max(0, krw_amount − confirmed_amount)
이후   원장 잔액
```

DTO 가 계산하고 있다면 계산을 매퍼/서비스로 올려 원장 값을 주입하라.
**같은 파트너 콘솔의 풀 현황(`PartnerP2pPoolMapper`)은 이미 원장 기준이다** — 두 화면의 잔여가
지금 서로 다르다. 이 전환으로 하나가 된다.

### B2. 관리자 주문 잔여

**파일** `admin-api/.../mapper/P2pOrderSearchMapper.java` (약 :153)

```
현재   (wo.krw_amount - COALESCE(wo.confirmed_amount, 0)) AS remaining_amount
이후   LEDGER_BALANCE
```

### B3. 관리자 회원 금액 집계

**파일** `admin-api/.../mapper/P2pMemberSearchMapper.java` (약 :89-94)

`remainingKrw` 가 `SUM(krw_amount − matched_amount)` 를 **PENDING/PARTIALLY_MATCHED 만** 합산한다.
원장 잔액으로 바꾸면 상태 필터를 그대로 두어야 값이 안 튄다 — **필터를 임의로 넓히지 마라.**
`matchedKrw`/`confirmedKrw` 는 누적 컬럼 그대로 둔다(별개 지표이고 이번 범위 밖).

### B4. 회원 4분류 ★

**파일** `open-api/.../controller/p2p/P2pWithdrawPageController.java` (약 :185-198, :270-287)

원장 엔트리만으로 전부 산출된다. **매칭 상태 조인이 필요 없다** — 확정을 `UNLOCK + SETTLE`
한 벌로 적는 설계 덕분에 미해소 LOCK 이 곧 "확인 대기"다.

| 표시 | 현재(구 컬럼) | 원장 산식 |
|---|---|---|
| 입금 확인됨 `receivedKrw` | `confirmed_amount` | `-Σ SETTLE` |
| 입금 확인 대기 `incomingKrw` | `max(0, matched − confirmed)` | `-(Σ LOCK + Σ UNLOCK)` |
| 매칭 대기 `waitingKrw` | `max(0, krw − matched − convertReq)` | `max(0, 원장잔액 − convertReq)` |
| USDT 전환 요청 중 | `min(convertReq, krw − matched)` | `min(convertReq, 원장잔액)` |
| 히어로 `pendingKrw` | `krw − matched − convertReq` | `max(0, 원장잔액 − convertReq)` |

- `usdt_convert_requested_krw` / `_usdt` 는 **누적 컬럼이 아니라 요청 스냅샷**이다. 그대로 쓴다
- 타입별 합계를 한 번에 얻는 조회를 `P2pWithdrawLedgerMapper` 에 추가해도 좋다.
  **단 주문마다 5번 질의하는 N+1 을 만들지 마라** — 대시보드는 활성 주문을 순회한다
- `manualPending` 건수 집계는 매칭 상태가 필요하다. **그건 금액이 아니므로 손대지 마라**

### B5. 파트너 풀의 `matchedAmount`

**파일** `partner-api/.../mapper/PartnerP2pPoolMapper.java` (약 :102)

같은 행에서 `remaining_krw` 는 원장인데 `matched_amount` 는 누적 컬럼이라
`krw_amount − matched_amount ≠ remaining_krw` 가 화면에 그대로 보인다.
**표시 목적이 무엇인지 확인하고**, 진행률 표시용이라면 원장 기준(`액면 − 원장잔액`)으로 통일하라.

---

## C. 프론트 — 폴백 계산 제거

**백엔드만 바꾸면 목록 화면은 계속 구 컬럼으로 계산한다.** 프론트가 같은 공식을 재구현해 뒀다.

| 파일 | 위치 | 내용 |
|---|---|---|
| `p2p-ui/src/views/DashboardView.vue` | 약 :51-53 | 서버 `activeOrders` 없을 때 폴백 계산 |
| `p2p-ui/src/views/OrdersView.vue` | 약 :32-34 | **항상** 프론트가 계산 (목록 응답에 summary 없음) |
| `p2p-ui/src/views/OrderDetailView.vue` | 약 :47, :113-119, :127-129 | 받음/잔여/들어오는 중/매칭 대기 |
| `partner-ui/src/views/partner/p2p/P2pMemberDetailView.vue` | 약 :50-52 | `krwAmount − confirmedAmount` 재구현 |

**원칙: 금액 산식을 프론트에 두지 않는다.** 서버가 준 값을 그리기만 한다.
`OrdersView` 처럼 응답에 값이 없으면 **응답에 필드를 추가**하고 프론트 계산을 지워라.

---

## D. 절대 손대지 말 것 ⚠️

아래는 **돈이 실제로 나가는 금액**을 정하거나 이중지급을 막는 코드다. 이번 범위 밖이다.

```
computeRemainder()                    P2pWithdrawService 약 :560-573
   krw − confirmed 로 실제 지급 USDT 액을 정한다. 여기를 바꾸면 지급액이 즉시 달라진다

USDT 전환 게이트 matched != confirmed  P2pWithdrawService 약 :451-456, :519-524
초과 확인 가드 confirmed + krw > krw    P2pMatchingService 약 :1559-1560
refund_type 재실행 차단 가드           P2pWithdrawService 약 :544
```

이 넷은 **읽는 값이 구 컬럼이어도 그대로 둔다.** 표시 전환과 분리해서 별도 검증 후에 다룬다.

---

## 코딩 규칙

- Java 17 · Lombok(`@Data` 금지) · DTO 멤버 JavaDoc 필수
- MyBatis 는 `@Mapper` + `@Select` interface. **XML 매퍼 금지**
- `<script>` 안에서 `<`, `<=`, `<>` 직접 사용 금지 → `&lt;` / `!=` (부팅 시 SAXParseException = 전 서비스 다운)
- ORDER BY / LIMIT / COUNT 직접 작성 금지
- **DDL 을 만들지 마라. DB 에 접속하지 마라.** 컬럼 제거(T1-e)는 이번 범위가 아니다
- Vue 는 기존 스타일. 새 라이브러리 금지

## 완료 기준

```
1  ./gradlew :core:compileJava :partner-api:compileJava :open-api:compileJava :admin-api:compileJava 통과
2  p2p-ui · partner-ui · admin-ui 빌드 통과
3  cancelOrder 가 잔여 > 0 일 때 RELEASE 를 적재한다 (0이면 적재 안 함)
4  파트너 콘솔의 두 화면(풀 현황 · 출금주문 상세) 잔여가 같은 산식을 쓴다
5  회원 4분류가 원장 기준으로 산출된다 (매칭 상태 조인 없이)
6  프론트에 금액 산식이 남아 있지 않다 (C의 4개 파일)
7  D의 네 곳은 변경되지 않았다 (diff 로 증명)
8  대시보드 조회에 N+1 이 없다
```

## 보고 형식

- 작업별 수정 파일:라인 + 한 줄
- **A 에서 재취소 이중 적재를 어떻게 막았는지**
- B4 의 원장 합계 조회 방식과 N+1 회피 방법
- B5 에서 `matchedAmount` 를 어떻게 처리했는지(유지/전환)와 근거
- D 네 곳이 무변경임을 grep/diff 로 증명
- 지침이 실제 코드와 어긋난 지점 — **고치지 말고 먼저 보고**
- 빌드 결과

## 착수 전 필수 확인

```
core/.../p2p/P2pWithdrawService.java            (cancelOrder · applyRemainderResolution · computeRemainder)
core/.../p2p/P2pWithdrawLedgerService.java       (charge/lock/unlock/settle/release · balances)
common/.../mapper/P2pWithdrawLedgerMapper.java
common/.../mapper/P2pMatchingMapper.java         (LEDGER_BALANCE 상수 — 복사 금지, 참조)
common/.../enums/P2pWithdrawEntryType.java       (타입별 부호 규약)
partner-api/.../mapper/PartnerP2pPoolMapper.java (이미 전환된 정본 예시)
open-api/.../controller/p2p/P2pWithdrawPageController.java (4분류 현재 계산)
```

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