# 매칭 테스트에서 나온 수정 3건 (2026-08-21)

repo `cryptoments` (+ `cryptoments-admin`) · **DDL 없음**

BARO 매칭 테스트 중 실거래로 재현된 문제다. 두 건 모두 로그·DB 로 원인이 확정됐다.

---

## A. 🔴 롤백된 P2P 레그의 "매칭됨" 알림이 출금 회원에게 발송된다

### 재현 (실거래 `pdo_fea0866c37d0`)

```
02:52:54.158  P2P 레그 1564 생성 14,140 → 출금주문 46 FULLY_MATCHED
                                        → notifyMemberAfterCommit(MATCHED) 등록
02:52:54.217  잔여 860 ≤ dust_floor(10,000) → rollbackP2pLegs
                                        → 레그 CANCELLED · 출금주문 PENDING 복원
02:52:55      BARO 레그 1565 15,000 (전액 LP)
   커밋        → 예약돼 있던 "매칭됨" 알림 발송 ❌
```

출금 회원은 **성립하지 않은 매칭**의 알림을 받았다.

### 원인

`createMatch` (`P2pMatchingService:1681-1684`) 가 `fullyMatched` 일 때 알림을 `afterCommit` 으로
등록하는데, **커밋 시점에 상태를 다시 확인하지 않는다.** 생성과 롤백이 같은 트랜잭션
(`matchPhase1` `@Transactional` :398) 안이라, 콜백이 실행될 때는 이미 레그가 CANCELLED 다.

### 고치는 방향

`notifyMemberAfterCommit` 의 `afterCommit` 안에서 **출금 주문을 다시 읽어** 알림 전제가
여전히 성립할 때만 발송한다.

- `MATCHED` 이벤트는 커밋 시점에도 `FULLY_MATCHED` 여야 발송
- 캡처해 둔 객체의 상태를 믿지 마라 — **DB 를 다시 읽어라**
- 다른 이벤트의 기존 동작을 바꾸지 마라. 재확인이 필요한 이벤트에만 적용한다
- 재조회 실패는 발송하지 않는 쪽으로 (허위 알림 < 알림 누락)

> ⚠️ 롤백 쪽에 "알림 취소" 를 넣는 방식은 쓰지 마라. 등록 지점이 늘어나면 같은 버그가 반복된다.
> **발송 직전에 사실을 확인**하는 한 곳으로 끝낸다.

## B. 🟠 취소된 레그가 화면에서 "혼합 매칭" 으로 보인다

같은 건에서 CANCELLED P2P 레그 1건 + BARO 레그 1건 = 2행이 남아 어드민이 혼합으로 표시했다.
실제 구성은 **BARO 단독**이다.

- **데이터는 남긴다** — 레그가 잡혔다 풀린 것은 이력이다. 지우지 마라
- **화면의 매칭 구성 판정(단독/혼합·레그 수·구성 요약)에서 CANCELLED·FAILED 레그를 제외**한다
- 개별 레그 목록에는 계속 보여주되 취소 상태가 드러나게 (지금도 "취소" 뱃지는 나온다)
- admin 콘솔이 주 대상. 파트너·위젯에도 같은 판정이 있으면 함께 맞춘다

---

## C. 🟠 더스트 잔여 가드가 P2P 유동성을 사실상 봉쇄한다

### 재현 (실거래 `pdo_cffdbb7f07be`)

```
유동성 13,710 (출금주문 50) · 입금 주문 10,000
  legAmount = 10,000 → leftoverAfter = 3,710
  0 < 3,710 < min_amount(10,000)  → legAmount = 13,710 − 10,000 = 3,710
  3,710 < min_amount              → continue (후보 스킵)
  → P2P 0 · 전액 BARO
```

### 사각지대

잔액 `A` 인 출금 주문이 매칭될 수 있는 주문 크기는:

```
need ≥ A          ✓ 전액 소진 (leftover 0)
need ≤ A − min    ✓ 잔여가 min 이상
(A−min, A)        ✗ 스킵          ← 폭이 정확히 min = 10,000원
```

`A = 13,710` 이면 **3,710 초과 ~ 13,710 미만 전부가 사각지대**다. 실제 출금 주문이 1만 원대라
쓸 만한 구간이 거의 다 막힌다. 이 규칙은 주문이 `min` 보다 훨씬 클 때를 가정하고 만들어졌다.

### ☠️ 규칙의 근거가 사실과 다르다

기존 주석: *"더스트 잔여는 이후 매칭 후보가 못 되어 **영구 잔류(출금자 자금 고착)** 한다."*

**아니다.** `P2pWithdrawService.cancelOrder:257` 은 `PENDING` 과 **`PARTIALLY_MATCHED` 둘 다**
취소를 허용한다. USDT 전환 경로도 있다. 잔여는 갇히지 않는다 — P2P 로 재매칭이 안 될 뿐이고,
출금자가 취소하면 회수한다.

두 결과를 비교하면 방향이 분명하다.

| | 출금자 | 구매자 |
|---|---|---|
| 현재(스킵) | **0원 매칭.** 13,710 들고 계속 대기 → 결국 취소해야 함 | 전액 LP |
| 제거 후 | **10,000 매칭.** 잔여 3,710 은 취소로 회수 | P2P 10,000 |

**규칙이 막으려던 정체를 규칙 자신이 만들고 있다.**

### 고치는 방향

`P2pMatchingService:623-627` 의 더스트 잔여 가드(축소 + 스킵)를 **제거한다.**
잔여가 `min_amount` 미만이 되더라도 그대로 매칭한다.

```
제거 대상
  long leftoverAfter = available - legAmount;
  if (leftoverAfter > 0 && leftoverAfter < minAmount) {
      legAmount = available - minAmount;
      if (legAmount < minAmount) continue;
  }
```

- **`available < minAmount → continue` (:616) 는 그대로 둔다** — 더스트 주문을 후보로 올리는 건
  별개 문제이고, 이번 결정 범위가 아니다
- 되돌릴 수 있게 **설정 스위치**를 둔다: `p2p.dust_leftover_guard` (기본 **false** = 가드 없음).
  `getBooleanSetting` 류가 없으면 `getIntSetting(key, 0) != 0` 같은 기존 관례를 따르라 —
  **새 설정 인프라를 만들지 마라**
- 제거 사유를 주석으로 남겨라. 근거(취소 가능 = 자금 고착 아님)를 적지 않으면 누군가 다시 넣는다
- 부분 매칭이 되면 출금 주문은 `PARTIALLY_MATCHED` 가 되고 `fullyMatched=false` 라
  **알림이 나가지 않는다** — A 와 상호작용 없음을 확인하라

---

## 오케스트레이터가 별도로 볼 것 (코드 아님)

`p2p.dust_adjust_rate` 가 **1%** 라 위 A 사례에서 (a) 주문 하향 분기가 발동하지 못했다
(잔여 860 vs 문턱 150). 6% 정도면 이런 건은 **P2P 전액 성립**으로 끝나 LP 로 새지 않는다.
DB 설정이라 배포 불필요 — **서브에이전트는 건드리지 마라.**

---

## 절대 규칙

- **DDL·DML 금지. DB 접속 금지. 운영 서버 접속 금지**
- `available < minAmount` 스킵(:616)은 **유지**
- `rollbackP2pLegs` · `adjustOrderDownForDust` 의 동작을 바꾸지 마라 (A 는 알림만 고친다)
- 매칭 대기(`isWaitingForMatch`) 관련 분기를 건드리지 마라
- CANCELLED 레그 **데이터를 지우지 마라** (B 는 표시만)
- MyBatis `<script>` 안에 `<` `<=` `<>` 금지 — SAXParseException 으로 전 서비스 다운
- `IXRepository.modify()` 는 non-null 필드만 갱신 · `save()` 는 PK(Long) 반환
- 새 라이브러리 금지 · **git commit / push 하지 마라**

## 완료 기준

```
1  ★ afterCommit 이 DB 를 다시 읽어 FULLY_MATCHED 일 때만 MATCHED 알림 — 코드 경로로 증명
2  ★ 롤백 시나리오(레그 생성 → 같은 tx 에서 CANCELLED)에서 알림이 억제됨을 코드로 설명
3  다른 회원 알림 이벤트의 기존 동작 무변경
4  ★ 더스트 잔여 가드 제거 — 잔여가 min 미만이어도 매칭된다
5  p2p.dust_leftover_guard=true 면 기존(가드) 동작으로 되돌아간다
6  available < minAmount 스킵은 그대로 (:616 무변경 — diff 로 증명)
7  화면의 매칭 구성 판정에서 CANCELLED/FAILED 레그 제외. 레그 목록에는 계속 표시
8  ./gradlew 컴파일 통과 · admin-ui 빌드 통과
9  git commit·push 하지 않았다
```

## 보고

- 수정 파일:라인 + 한 줄
- **1·2·4·6 을 코드 경로로 증명**
- 재현 사례 두 건이 수정 후 어떻게 흘러가는지 단계별로 서술
- 지침이 실제 코드와 어긋난 지점 — 고치지 말고 먼저 보고
- 빌드 결과

## 착수 전 필수 확인

```
core/.../p2p/P2pMatchingService.java     :610-640 후보 루프 · :665-700 더스트 분기
                                         :1660-1690 createMatch 알림 · notifyMemberAfterCommit
core/.../p2p/P2pWithdrawService.java     :240-300 cancelOrder (PARTIALLY_MATCHED 허용 근거)
core/.../p2p/P2pMemberNotifier (또는 상당)  발송 구현
admin-ui  매칭 구성/레그 표시 화면 (P2pDepositOrderDetailView 등)
```

지침과 다르면 **멈추고 보고하라.**
