# U2 — 정산 테이블 읽기 의존 제거 + 매칭 필드 노출

작성 2026-08-18 · repo `cryptoments`(admin-api, partner-api) + `cryptoments-admin`(admin-ui)
선행 완료: T4 참조 통일(원장 99행 이관), 재시도 회계 `p2p_matches` 이관(v2.11)

---

## 배경 — 왜 정산 테이블을 읽으면 안 되는가

`p2p_settlements` 는 **P2P 레그만 온전히 담고 TORQ 레그를 전혀 안 담는다.** 실측:

```
매칭 1,446건 중 정산 행이 있는 것 47건 (3%)

TORQ    SETTLED 1247 · FAILED 50 · CANCELLED 45   → 정산 행 0
P2P     SETTLED   37                              → 37 (전건)
PARTNER SETTLED   21                              → 10 (절반)
```

그 결과 P2P 대시보드가 **6월부터 잘못된 숫자를 보여줬다.**

```
오늘 정산 건수   0    실제 1
실패 정산 건수   0    실제 70
7일 정산 완료    3    실제 170
```

정본은 이미 `p2p_matches` 로 이사했다 — `settle_retry_count`, `settle_failure_reason`,
그리고 상태 자체(`SETTLED`/`FAILED`). **이 작업은 읽는 쪽을 정본으로 옮기는 것이다.**

> ⚠️ **이 작업에서 `p2p_settlements` 에 쓰는 코드는 건드리지 않는다.**
> `core/p2p/P2pSettlementService.java` 의 INSERT 는 그대로 둔다. 읽는 곳을 먼저 전부 없애고,
> 배포·검증이 끝난 뒤 별도 단계에서 쓰기 중단 + DROP 한다. 되돌릴 수 있는 순서를 지킨다.

---

# A. 백엔드 — admin-api

## A1. 매칭 상세 DTO 필드 노출

**파일** `admin-api/.../dto/response/P2pMatchDetailResponse.java`

매퍼가 이미 `SELECT m.*` 이므로 (`P2pMatchSearchMapper.java:21,56`) **DTO에 필드만 추가하면 자동 매핑된다. 매퍼는 손대지 마라.**

엔티티 `common/.../entity/P2pMatch.java` 에서 확인한 미노출 컬럼:

| 컬럼 | Java 필드 | 타입 | 엔티티 라인 |
|---|---|---|---|
| `network_id` | `networkId` | `Long` | :53 |
| `settle_retry_count` | `settleRetryCount` | `Integer` | :206 |
| `settle_failure_reason` | `settleFailureReason` | `String` | :215 |
| `dispute_source` | `disputeSource` | `String` | :137 |
| `dispute_round` | `disputeRound` | `Integer` | :141 |
| `dispute_waiting_on` | `disputeWaitingOn` | `String` | :145 |
| `dispute_due_at` | `disputeDueAt` | `LocalDateTime` | :149 |

**DTO 멤버 변수에는 JavaDoc 주석 필수** (프로젝트 규칙). 각 필드가 무엇인지 한 줄로 적어라.

## A2. 입금 주문 DTO 필드 노출

**파일** `admin-api/.../dto/response/P2pTransactionOrderResponse.java`

매퍼가 `SELECT do2.*` (`P2pOrderSearchMapper.java:57`) 이므로 역시 필드만 추가.

| 컬럼 | 필드 | 엔티티 `P2pDepositOrder.java` |
|---|---|---|
| `closed_at` | `closedAt` | :122 |
| `close_reason` | `closeReason` | :131 |
| `match_attempt_count` | `matchAttemptCount` | :147 |
| `claimed_at` | `claimedAt` | :151 |

> 목록 DTO(`P2pDepositOrderListResponse`)도 같은 `do2.*` 를 쓰는지 확인하고, 쓴다면
> `matchAttemptCount` 만 추가한다(목록에서 워커 적체를 보기 위함). 나머지는 상세 전용.

## A3. 레그 조회에서 `p2p_settlements` JOIN 제거 ⚠️ 핵심

**파일** `admin-api/.../mapper/P2pOrderSearchMapper.java`

두 쿼리가 정산 테이블을 JOIN 한다.

**A3-1. `findTransactionLegs`** (:118-144)

```
제거   LEFT JOIN p2p_settlements ps ON ps.match_id = m.id      (:141)
제거   ps.settlement_code / settlement_type / status / tx_hash_record /
       network_id / completed_at / failed_at / failure_reason / retry_count  (:131-135)
```

대체 — **전부 `m.*` 에 이미 들어 있다.** 별칭 없이 DTO 필드명만 맞추면 된다.

| 기존 (정산 기준) | 대체 (매칭 기준) | 비고 |
|---|---|---|
| `ps.retry_count AS settlement_retry_count` | `m.settle_retry_count` | 정본 (v2.11) |
| `ps.failure_reason AS settlement_failure_reason` | `m.settle_failure_reason` | 정본 |
| `ps.status AS settlement_status` | `m.status` | 이미 레그 DTO 에 `status` 로 있음 → **중복 필드 제거 대상** |
| `ps.tx_hash AS settlement_tx_hash_record` | `m.settlement_tx_hash` | 매칭에 이미 있다 |
| `ps.completed_at AS settlement_completed_at` | `m.settled_at` | 매칭에 이미 있다 |
| `ps.settlement_code` | **대체 없음** | 아래 참조 |
| `ps.settlement_type` | **대체 없음** | 아래 참조 |
| `ps.network_id AS settlement_network_id` | `m.network_id` | A1 에서 노출 |
| `ps.failed_at` | **대체 없음** | 아래 참조 |

**대체 없는 3개(`settlement_code`, `settlement_type`, `failed_at`)의 처리:**
`p2p_transfer_requests` 가 `settlement_code` 를 UNIQUE 로 보유한다고 알려져 있다.
**먼저 그 테이블의 실제 컬럼과 `match_id` 연결 여부를 확인하라.**
- 연결이 있으면 → 그 테이블을 JOIN 해 세 값을 가져온다
- 연결이 없거나 데이터가 비어 있으면 → **DTO 필드를 지우고 화면에서도 뺀다.** 억지로 만들지 마라

**A3-2. `findWithdrawOrderLegs`** (:168-180) — 같은 방식. `ps` JOIN(:177)과 별칭 4개(:172-173) 제거.

**A3-3. `tt.lp_provider` 추가** — `findTransactionLegs` 는 이미 `LEFT JOIN torq_trades tt`(:140) 를 한다. `tt.lp_provider AS lp_provider` 한 줄을 추가하고 레그 DTO 에 `lpProvider` 필드를 만든다. TORQ / TORQ2 구분용.

> ⚠️ **MyBatis `<script>` 주의** — 이 두 쿼리는 `<script>` 가 아니지만, 혹시 수정 중
> `<script>` 로 바꾸게 되면 `<`, `<=`, `<>` 를 직접 쓰지 마라. 부팅 시 SAXParseException 으로
> **전 서비스가 죽는다** (2026-06-11 운영 장애). `&lt;` 또는 `!=` 를 쓴다.

## A4. 대시보드 매퍼 — 정산 카운트를 매칭 기준으로

**파일** `admin-api/.../mapper/P2pDashboardMapper.java`

5개 쿼리가 `p2p_settlements` 를 센다. 아래로 교체한다. **의미가 바뀌므로 메서드 JavaDoc 도 함께 고쳐라.**

| 메서드 | 기존 | 교체 | 실측 검증값 |
|---|---|---|---|
| `countTodaySettlements` (:32) | `COUNT(*) p2p_settlements WHERE DATE(created_at)=CURDATE()` | `COUNT(*) FROM p2p_matches WHERE DATE(settled_at)=CURDATE()` | 오늘 1 |
| `sumTodaySettlementUsdt` (:36) | `SUM(usdt_amount) ... status='COMPLETED'` | `SELECT COALESCE(SUM(usdt_amount),0) FROM p2p_matches WHERE status='SETTLED' AND DATE(settled_at)=CURDATE()` | — |
| `countFailedSettlements` (:44) | `COUNT(*) ... status='FAILED'` | `COUNT(*) FROM p2p_matches WHERE status='FAILED'` | 70 |
| `countSettlementsCompletedSince` (:70) | `status='COMPLETED' AND created_at>=...` | `FROM p2p_matches WHERE status='SETTLED' AND settled_at >= DATE_SUB(CURDATE(), INTERVAL #{days} DAY)` | 7일 170 |
| `countSettlementsFailedSince` (:74) | `status='FAILED' AND created_at>=...` | `FROM p2p_matches WHERE status='FAILED' AND created_at >= DATE_SUB(CURDATE(), INTERVAL #{days} DAY)` | — |

> **완료 건은 `settled_at`, 실패 건은 `created_at` 기준**이다. 실패 매칭은 `settled_at` 이 NULL 이라
> `settled_at` 으로 걸면 전부 사라진다. 이 비대칭은 의도된 것이니 통일하지 마라.

**대시보드 숫자가 크게 오른다 (3 → 170 등). 버그가 아니라 그동안 빠져 있던 TORQ 레그가 들어오는 것이다.** 서비스 계층에 이 사실을 주석으로 남겨라.

## A5. 정산 재시도/취소를 매칭 기준으로

**파일** `admin-api/.../service/P2pSettlementManagementService.java`

현재 `retrySettlement(id)` / `cancelSettlement(id, reason)` 는 **정산 행 id** 를 받는다.
내부를 보면 **실제 효력은 이미 매칭에 있다** — `p2pMatchingMapper.resetSettleRetryCount(matchId)` /
`blockSettleRetry(matchId, ...)`. 정산 행 갱신은 이중 기록일 뿐이다.

**변경:**
- 두 메서드가 **`matchId` 를 직접 받도록** 바꾼다
- `p2pSettlementRepository.findOne` / `update` 호출 제거
- 사전 검증을 정산 상태(`P2pSettlementStatus.FAILED`)에서 **매칭 상태(`P2pMatchStatus.FAILED`)** 로 바꾼다
- 감사 로그(`AuditAction.RETRY_P2P_SETTLEMENT`)의 대상 ID 를 matchId 로. **AuditAction enum 값은 그대로 둔다** (과거 로그와의 연속성)
- 반환 타입을 `P2pSettlement` → `P2pMatchDetailResponse` 로 (화면이 갱신된 매칭을 바로 받도록)

**서비스 클래스명이 실체와 어긋난다.** 이름 변경은 이번 범위 밖이니 **클래스 상단 JavaDoc 에
"정산 행이 아니라 매칭 회계를 다룬다"고 명시**만 하고 리네임은 하지 마라.

## A6. 정산 목록/상세 조회 폐지

- `P2pSettlementManagementService.getSettlements` / `getSettlementDetail` **삭제**
- 대응 컨트롤러 엔드포인트 삭제 (`P2pSettlementManagementController` 를 찾아 확인)
- `P2pSettlementSearchMapper`, `P2pSettlementDetailResponse`, `P2pSettlementSearchRequest` **삭제**
- 재시도/취소 엔드포인트는 **매칭 컨트롤러(`P2pMatchManagementController`)로 옮긴다** — 경로 예:
  `POST /admin/p2p/matches/{id}/settle/retry`, `POST /admin/p2p/matches/{id}/settle/cancel`

> 삭제 전에 **`P2pSettlementDetailResponse` 를 참조하는 다른 곳이 없는지 grep 하라.**
> 있으면 지우지 말고 보고하라.

## A7. partner-api — `tradingPaused` 노출

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

`from(P2pMember member, ...)` 정적 팩토리가 있다 (:121). 빌더에 `.tradingPaused(member.getTradingPaused())` 한 줄과 필드 + JavaDoc 을 추가한다.

> 이번엔 **표시 전용**이다. 파트너가 회원의 거래중지를 **변경**하는 API 는 만들지 마라 —
> 거래중지는 회원 본인이 결정하는 값이고, 파트너 대행 정책은 아직 결정되지 않았다.

---

# B. 프론트 — admin-ui

## B1. 정산 화면 2개 폐지

- `views/p2p/P2pSettlementListView.vue`, `views/p2p/P2pSettlementDetailView.vue` **삭제**
- 라우터에서 해당 route 제거 (`src/router/`)
- 메뉴에서 제거 (`src/utils/constants.ts` 의 `MENU_ITEMS`)
- `api/services/p2p.service.ts` 의 `getSettlements` / `getSettlementDetail` 삭제
- `retrySettlement` / `cancelSettlement` 는 **새 매칭 경로로 시그니처 변경** (A5/A6)
- `api/types/p2p.ts` 에서 정산 전용 타입 삭제

**다른 화면이 정산 라우트로 링크하는지 grep 하라** (`p2p/settlements` 등). 있으면 매칭 상세로 돌린다.

## B2. 매칭 상세에 정산 흡수

**파일** `views/p2p/P2pMatchDetailView.vue`

기존 "정산 정보" 카드(`settledAt`/`settlementTxHash`/`expiresAt`)를 확장한다:

- `settleRetryCount` — 재시도 횟수
- `settleFailureReason` — 실패 사유 (있을 때만)
- `networkId` — 체인 (가능하면 이름으로. 네트워크명 매핑이 이미 있으면 재사용, 없으면 ID 그대로)
- **재시도 / 정산취소 버튼** — `status === 'FAILED'` 일 때만 노출. U1 에서 만든 증빙 재요청 다이얼로그 패턴을 따른다

분쟁 카드에도 A1 의 `disputeRound` / `disputeWaitingOn` / `disputeDueAt` / `disputeSource` 를 추가한다 — 관리자가 "몇 라운드, 누구 응답 대기"를 봐야 한다.

## B3. 나머지 표시

- **입금 주문 상세** — `closeReason`(종결 사유), `closedAt`, `matchAttemptCount`(매칭 시도 횟수) 표시.
  레그 카드의 재시도/실패 표시 출처를 `settlementRetryCount` → `settleRetryCount` 로 교체
- **입금 주문 목록** — `matchAttemptCount` 컬럼 (워커 적체 관측용)
- **레그 카드** — `lpProvider` 를 TORQ 레그에 배지로
- **출금 주문 상세** — 레그의 정산 필드 교체 (A3-2 대응)

## 코딩 규칙

- Java 17 · Lombok `@Getter @Setter @Builder(toBuilder=true) @NoArgsConstructor @AllArgsConstructor` (`@Data` 금지)
- **DTO 멤버 변수에 JavaDoc 필수**
- MyBatis 는 `@Mapper` + `@Select` interface 메서드. **XML 매퍼 금지**
- ORDER BY / LIMIT / COUNT 직접 작성 금지 (`XResultInterceptor` 가 처리)
- Vue 는 기존 파일 스타일을 따르고 새 라이브러리 도입 금지
- **DDL 을 만들거나 실행하지 마라.** 스키마 변경은 이 작업에 없다

## 완료 기준

```
1  ./gradlew :admin-api:compileJava  통과
2  ./gradlew :partner-api:compileJava 통과
3  admin-ui 빌드 통과 (타입 에러 0)
4  admin-api 전체에서 p2p_settlements 를 SELECT 하는 곳 0건 (grep 으로 증명)
5  P2pSettlementSearchMapper / P2pSettlementDetailResponse 삭제됨
6  매칭 상세가 재시도 횟수·실패 사유·체인·분쟁 라운드를 보여준다
7  FAILED 매칭에서 재시도/취소를 실행할 수 있다
8  정산 메뉴·라우트가 사라지고, 죽은 링크가 없다
```

## 보고 형식

- 작업별 수정 파일:라인 + 한 줄 설명
- **A3 의 `settlement_code`/`settlement_type`/`failed_at` 을 어떻게 처리했는지** (JOIN 했는지, 지웠는지, 근거)
- A4 교체 쿼리가 위 실측값(70 / 170 / 1)과 맞는지 — **가능하면 직접 확인하지 말고, 쿼리문이 표와 일치하는지만 대조**
- A6 삭제 전 grep 결과
- 지침이 실제 코드와 어긋난 지점 — **고치지 말고 먼저 보고**
- 빌드 결과

## 착수 전 필수 확인

```
common/.../entity/P2pMatch.java              (A1 컬럼·타입)
common/.../entity/P2pDepositOrder.java       (A2 컬럼·타입)
admin-api/.../mapper/P2pOrderSearchMapper.java   (A3 — SELECT 별칭 전량)
admin-api/.../dto/response/P2pTransactionLegResponse.java
admin-api/.../dto/response/P2pWithdrawOrderLegResponse.java
admin-api/.../service/P2pSettlementManagementService.java  (A5 — 실제 효력 위치)
p2p_transfer_requests 관련 엔티티/매퍼        (A3 settlement_code 대체 가능 여부)
```

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