# 통합 매칭 서비스 설계 (Unified Matching — P2P + TORQ + 파트너)

**버전**: v0.7 (초안 — 매칭 링크 통합 추가)
**작성일**: 2026-06-13
**상태**: 설계 검토 — 구현 전
**관련 문서**: `P2P_MATCHING_ARCHITECTURE.md`, `P2P_FEE_REDESIGN_GUIDE.md`, `CRYPTOMENTS_V2_DDL.sql`

---

## 0. 한 줄 요약

현금으로 USDT를 사는 모든 경로를 **하나의 "매칭 서비스"** 로 통일한다. 한 입금 주문을 **P2P 출금자 계좌 → TORQ 풀 → 파트너 계좌** 우선순위로 혼합 충족하며, 최대 3계좌를 한 화면에 노출한다. 구매자는 방식과 무관하게 **총 2% 구매 수수료**를 낸다.

지금은 입금 1건을 P2P 전액 *또는* TORQ 전액으로 택일 라우팅하고, TORQ fallback·파트너 경로는 미구현이다.

---

## 1. 결정 사항 (2026-06-13 확정)

| 항목 | 결정 | 비고 |
|------|------|------|
| **구매 방식** | **3종 통합** | ① P2P 매칭 ② TORQ 풀 ③ 파트너 계좌. 단일 "매칭 서비스"로 호칭 |
| **구매 수수료** | **총 2% 고정** | 방식 무관 구매자 단일 부담. 각 방식 내부 원가(torq_fee 등)는 시스템이 흡수 |
| **레그 모델링** | **P2pMatch로 흡수** | 세 방식 모두 `p2p_matches` 레코드. `leg_type`(P2P/TORQ/PARTNER)으로 확인·정산만 분기 |
| **우선순위 폭포** | **P2P > TORQ > 파트너** | P2P 최대 2건 → 잔여는 단일 슬롯 |
| **최대 노출 계좌** | **3개 (P2P 최대 2 + 잔여 1)** | 잔여 슬롯은 **1방식만**(TORQ 또는 파트너). 동시 4계좌 없음 |
| **잔여 충족 규칙** | **TORQ는 잔여 전액 가능 시에만** | TORQ가 잔여 전액 못 채우면 그 슬롯을 통째로 파트너가 대체 (잔여 한해 TORQ all-or-nothing) |
| **파트너 레그 레일** | **P2P와 동일** | 파트너 서비스계좌 = CODEF 확인 + 파트너 자체 유동성 정산. 카운터파티만 파트너 |
| **실패 조건** | **P2P+TORQ 미충족 & 파트너 미등록 → 매칭 실패** | 파트너 계좌 등록 시 잔여를 파트너가 흡수하므로 사실상 실패 없음 |
| **매칭 확정 방식** | **점진형 2단계** | P2P leg 먼저 확정·노출("잔여 매칭 중") → 잔여 leg는 잠금 해제 후 2단계로. TORQ 외부호출이 트랜잭션 밖 |
| **파트너 standing** | **자동 무제한** | 서비스계좌+P2P 켜면 한도=파트너 MASTER 잔액인 catch-all standing 출금주문 1건 자동 |
| **잔여 공급자** | **RemainderProvider 체인** | `[TORQ → PARTNER]` 순서·on/off는 system_settings 제어. `tryFill(need)→leg|null` |

---

## 2. 현재 상태 (AS-IS)

### 2.1 라우팅 (택일)

`P2pMatchingService.checkRoute(krw)`:
```
available = 매칭가능 출금 잔여 총액
route = (available >= krw && krw > 0) ? "P2P" : "TORQ"
```
- 위젯(`p2p.vue`/`torq.vue`)이 `p2pRouteApi`로 판정받아 **P2P 분할매칭** 또는 **TORQ 단일 에스크로** 중 하나로 진입.
- 코드 주석의 "70%"는 outdated — 실제는 **전액 커버** 요구.

### 2.2 P2P 분할매칭

`tryMatchDeposit`:
- exact 1:1 → 실패 시 분할(최대 `MAX_SPLIT_COUNT=3`).
- **전액 성립(`need<=0`)일 때만** MATCHED. 미충족이면 매칭 0건 + MATCHING 대기.
- `// TODO(Phase 2): TORQ LP 전액 fallback` — **미구현**.

### 2.3 두 레일의 본질적 차이

| | P2P 레그 | TORQ 레그 |
|---|---|---|
| 계좌 출처 | 출금자 등록 은행계좌(`bank_accounts`) | **외부 TORQ API가 반환하는 LP 계좌** (`torq_trades.seller_*`/`recipient_phone`) |
| 입금 확인 | **우리 CODEF 스크래핑** (출금자 계좌 입금내역) | **TORQ 외부 확인** + 우리는 LP→MASTER 온체인 감지(Webhook) |
| 정산(USDT) | 출금자 MASTER → 구매자파트너 MASTER 온체인 | LP → 우리 MASTER 온체인(Webhook) |
| 수수료 | `p2p_fee_rate`(구매자 부담) + 파트너 deposit_fee | `torq_fee` |
| 엔티티 | `p2p_matches` (withdraw_order_id 필수) | `torq_trades` (escrow_id) |

> **핵심**: TORQ는 내부 풀이 아니라 외부 카운터파티다. "P2pMatch 흡수"는 **위젯·매칭·상태를 단일화**하되, 확인·정산은 `leg_type`으로 내부 분기해야 한다. `p2p_matches.withdraw_order_id`는 현재 정산·계좌조회 7곳에서 하드 의존(`getBankAccountForMatch`, `startSettlementForMatch`, unlock 등)이므로 TORQ 레그는 이 의존을 우회하는 분기가 필수.

---

## 3. 목표 아키텍처 (TO-BE)

### 3.1 통합 매칭 흐름 (3계좌 예시)

```
입금주문 300,000 ─┬─ P2P leg A 120,000 → 출금자A 계좌  (leg_type=P2P,   CODEF 확인,  MASTER→MASTER)
                 ├─ P2P leg B 100,000 → 출금자B 계좌  (leg_type=P2P,   CODEF 확인,  MASTER→MASTER)
                 └─ 잔여 leg    80,000 → 잔여 슬롯 1개
                       ├ TORQ가 80,000 전액 가능 → TORQ leg (leg_type=TORQ,    TORQ 확인, LP→MASTER)
                       └ TORQ 불가      → 파트너 leg (leg_type=PARTNER, CODEF 확인, 파트너 유동성→구매자)
  전 레그 BANK_CONFIRMED → 레그별 정산 → 전건 완료 시 주문 COMPLETED
```

- 위젯은 **단일 플로우**로 최대 3계좌 카드를 노출(이미 `p2p.vue`가 멀티카드 지원). `torq.vue` 택일 진입 제거.
- 각 레그는 **독립 확인·독립 정산**(이미 §7-B per-leg 정산 토대 존재).
- **잔여 슬롯은 1개** — TORQ *또는* 파트너 중 하나만(동시 아님).

### 3.2 매칭 알고리즘 변경 (우선순위 폭포)

`tryMatchDeposit` 재설계:
```
1. [P2P] 출금 풀로 분할 계획 (최대 MAX_P2P_LEGS=2 레그)
     - 기존 사전계획(점검제외 + 큰잔여 우선 + 파트너 가용액 누적검증) 유지
     - plan.size() 상한 2
2. need = krw - (P2P 레그 합)
3. if need == 0:  → MATCHED (P2P 단독, 1~2계좌)
4. if need > 0:  잔여 슬롯 1개 결정 (우선순위 TORQ > 파트너)
     a. [TORQ] TORQ 풀이 need '전액' 가능?
          → createTrade(need) → leg_type=TORQ, torq_escrow_id, withdraw_order_id=NULL
     b. else [파트너] 파트너 서비스계좌 등록됨?
          → leg_type=PARTNER, withdraw_order_id=파트너 서비스 출금주문, 잔여 전액
     c. else → 매칭 실패 (MATCHING 대기 아님 — 명시적 FAIL/안내)
5. 전 레그 생성 성공 → MATCHED
```

- **P2P 우선**: P2P가 전액 커버하면 잔여 슬롯 없음(1~2계좌).
- **잔여 1슬롯**: TORQ는 *잔여 전액 가능할 때만* 사용. 부분만 가능하면 건너뛰고 파트너가 잔여 전액.
- **3계좌 상한**: P2P 2 + 잔여 1. P2P 후보가 3+여도 2개로 제한(잔여는 TORQ/파트너).

### 3.3 레그별 분기 지점

| 지점 | P2P 레그 | TORQ 레그 | 파트너 레그 |
|------|---------|-----------|-----------|
| 계좌 출처 (`getBankAccountForMatch`) | 출금주문 → bank_account | `torq_escrow_id` → LP 계좌 | 파트너 서비스계좌(bank_account, owner_type=PARTNER) |
| 이체확인 | CODEF 스크래핑(출금자 계좌) | TORQ 폴링/Webhook → escrow 확인 | CODEF 스크래핑(파트너 계좌) |
| 정산(`startSettlementForMatch`) | 출금자 MASTER→구매자 MASTER(PUSH, 2단계) | LP→MASTER 입금=정산(1단계, 구매자 p2p ledger CREDIT, 송금 없음) | 파트너 MASTER→구매자 MASTER(PUSH, 2단계) |
| unlock/만료 | 출금주문 잠금 해제 | TORQ escrow 취소 | 파트너 유동성 잠금 해제 |
| 수수료(§6) | 구매수수료 2%(체인 쉐어) | **0** (torq_fee 내장) | 구매수수료 2%(체인 쉐어) |

> **P2P/파트너는 동일 레일** — 둘 다 CODEF 확인 + (출금자|파트너) 유동성 정산. 차이는 카운터파티(출금자 회원 vs 파트너 자신)와 `withdraw_order_id`가 가리키는 출금주문의 owner뿐. TORQ만 외부 확인·LP 정산으로 다름.

### 3.4 핵심 근거 — 레일은 3개가 아니라 2개

| | 유동성 잠금 | 환율 | 정산 | 확인 |
|---|-----------|------|------|------|
| P2P leg | **파트너 MASTER USDT**(`getAvailableForP2p`) | 시스템 환율(`getSystemExchangeRate`) | MASTER→구매자 MASTER | CODEF |
| PARTNER leg | **동일** 파트너 MASTER USDT | **동일** 시스템 환율 | **동일** MASTER→구매자 MASTER | **동일** CODEF |
| TORQ leg | 외부 LP | TORQ 견적 | LP→MASTER(Webhook) | 외부 TORQ |

> P2P 매칭의 잠금은 이미 출금자 회원이 아니라 **그 회원이 속한 파트너의 MASTER 잔액**에서 잠근다(ledger 기반). 따라서 "파트너 계좌" leg는 **파트너 자신을 출금자로 한 standing 출금주문**으로 두면 P2P 레일을 그대로 재사용한다 — 새 정산/확인 코드 불필요. 통합 신규 비용은 **TORQ(외부 레일) 한 곳**에 집중.

### 3.5 통합 실행 흐름 (점진형 2단계)

```
[Phase 1 — 매칭 트랜잭션 (FOR UPDATE)]
  P2P 출금 풀로 ≤2 leg 잠금+생성 → 커밋
  remainder = krw - Σ(P2P leg)
  remainder == 0 → 주문 MATCHED, 종료
  remainder > 0  → 주문 MATCHING("잔여 매칭 중"), Phase 2 예약
  위젯: P2P 카드 1~2개 즉시 노출 + "잔여 매칭 중" placeholder

[Phase 2 — 잔여 해소 (AFTER_COMMIT 비동기, 잠금 미보유)]
  RemainderProvider 체인 순회 [TORQ → PARTNER]:
    TorqProvider.tryFill(remainder):
       TORQ 풀이 remainder 전액 가능? → createTrade(외부, 잠금 밖) → leg_type=TORQ
       부분/실패 → null (다음 provider)
    PartnerProvider.tryFill(remainder):
       파트너 standing 등록 + MASTER 잔액 ≥ remainder? → 잠금 + leg_type=PARTNER
       아니면 → null
  성공 → 주문 MATCHED, 3번째 카드 등장(위젯 폴링이 픽업)
  전 provider 실패 → 안전망: Phase 1 P2P leg 취소+unlock + 주문 FAILED
```

- **외부 I/O가 잠금 트랜잭션 밖**이라 잠금 보유 시간 최소화(점진형 채택 이유).
- **안전망**: 잔여 해소 완전 실패 시에만 P2P leg 롤백. 파트너 standing이 등록·유동적이면 이 경로는 거의 안 탐.
- **RemainderProvider** 추상으로 TORQ/파트너를 플러그인화 — 순서·on/off는 `p2p.torq_fallback_enabled`/`p2p.partner_fallback_enabled`로 제어, 향후 공급원 추가 용이.

---

## 4. 데이터 모델 변경 (DDL)

### 4.1 `p2p_matches` 확장

```sql
ALTER TABLE p2p_matches
  ADD COLUMN leg_type VARCHAR(10) NOT NULL DEFAULT 'P2P'   -- 'P2P' | 'TORQ' | 'PARTNER'
      COMMENT '매칭 레그 유형 — P2P 출금자 / TORQ LP / 파트너 계좌' AFTER deposit_order_id,
  ADD COLUMN torq_escrow_id BIGINT NULL
      COMMENT 'TORQ 레그의 escrow_id (leg_type=TORQ 시)' AFTER leg_type,
  MODIFY COLUMN withdraw_order_id BIGINT NULL
      COMMENT 'P2P/파트너 레그의 출금 주문 ID (TORQ 레그는 NULL)';
-- 인덱스
ALTER TABLE p2p_matches ADD INDEX idx_pm_torq_escrow (torq_escrow_id);
```

> `withdraw_order_id`를 NULL 허용으로 변경(TORQ 레그만 NULL). 기존 P2P 레그는 모두 `leg_type='P2P'` 기본값으로 무변경. **파트너 레그**는 파트너 서비스계좌를 출처로 하는 출금주문을 가리키므로 `withdraw_order_id` 사용(레일이 P2P와 동일).

### 4.2 `torq_trades` 연계

- TORQ 레그가 만든 `torq_trades` 레코드는 그대로 사용(escrow_id 소유).
- 역참조는 `p2p_matches.torq_escrow_id` 로 충분. (필요 시 `torq_trades.p2p_match_id` 추가 검토 — Phase 2.)

### 4.3 `system_settings`

```
p2p.max_p2p_legs = 2               # P2P 레그 상한
p2p.torq_fallback_enabled = true   # 잔여 TORQ on/off (롤백 안전판)
p2p.partner_fallback_enabled = true # 잔여 파트너 계좌 on/off
p2p.purchase_fee_rate = 0.02        # 구매자 통합 수수료 2%
```

### 4.4 파트너 standing 출금주문

- 파트너가 P2P 활성 + 서비스계좌 등록 시, **한도 = 파트너 MASTER 잔액**인 catch-all standing 출금주문을 자동 인식.
- 구현안: 별도 레코드 없이 PartnerProvider가 매칭 시점에 파트너 서비스계좌 + MASTER 가용액을 즉석 조회해 leg 생성(`withdraw_order_id`는 파트너 소유 standing 주문을 가리키도록 on-the-fly 생성 또는 파트너당 1건 유지). → 상세는 구현 Phase 2에서 확정.
- 환율 = 시스템 환율(P2P와 동일), 만료 없음.

---

## 5. 위젯 변경 (`widget-ui/p2p.vue`)

1. **진입 단일화**: `runRouting`의 P2P/TORQ 택일 제거 → 항상 `p2pCreateMatchApi`로 통합 매칭. 결과 레그 목록(P2P n + TORQ 1)을 카드로.
2. **계좌 카드**: 레그별 `leg_type` 배지(P2P 출금자 / TORQ). 계좌·예금주·금액·이체기한 표시(기존 멀티카드 재사용).
3. **TORQ 레그 안내**: TORQ는 LP 계좌로 송금. `recipient_phone`(BanqPipe) vs `seller_account`(BANK_DIRECT) 분기 표시 — 기존 `torq.vue` 송금 안내 컴포넌트 이식.
4. **이체완료/확인**: 레그별 `transfer-done` → 레그별 확인 상태 폴링(P2P=CODEF, TORQ=TORQ확인). 전 레그 확인 시 verifying→success.
5. **분쟁**: P2P 레그만 CODEF 분쟁 흐름(직전 작업 `a22923c`). TORQ 레그는 TORQ 분쟁 경로(기존 torq dispute) — leg_type로 분기.

---

## 6. 수수료 모델 (확정)

매칭 서비스(현금→USDT 구매)의 **유일한 수수료는 구매자 구매수수료 2%** 하나다. 입금수수료(deposit_fee)는 **전 레그 없음**.

| 레그 | DEBIT(현금) | CREDIT(USDT) | 구매수수료 2% | 입금수수료 |
|------|-----------|-------------|-------------|-----------|
| **P2P / 파트너** | 있음 | 있음 | **있음 — 체인 쉐어 분배** | **없음** |
| **TORQ** | 현금→LP | LP가 준 USDT | **없음 — TORQ quote에 내장** | **없음** |

- 원장에 FEE 엔트리가 찍히는 건 **P2P/파트너 레그의 2%** 뿐.
- **TORQ 레그**: 현금이 LP로 가고 USDT도 LP가 준다 — torq_fee가 환율(quote)에 이미 녹아 있으므로 우리 수수료를 또 붙이면 이중 과금. → 우리 측 수수료 0.
- 구매자 체감: 위젯엔 "최종 받는 USDT 합계" 단일 숫자로 표시. 혼합 주문은 레그별 환율(시스템 환율 vs TORQ quote)이 블렌드됨.

### 6.1 수익 분배(체인 쉐어)

현 P2P는 글로벌 단일률 + 체인 쉐어(v3.0, `p2p_order_shares`). 통합 후:
- **P2P/파트너 레그**: 2% 구매수수료가 체인 쉐어 분배 대상.
- **TORQ 레그**: 우리 수수료 0 → `p2p_order_shares` 분배 **대상 아님**(시스템 마진은 TORQ 정책에 종속). 일별집계가 leg_type=TORQ를 제외.

---

## 6-A. TORQ 레그 정산 일원화 (Option B 확정)

TORQ 레그도 **구매자 크레딧·원장을 p2p 정산 ledger로 일원화**한다. 단 물리 USDT 흐름이 P2P와 반대(외부 PULL-in)라 다음 배선이 핵심.

### 현재 TORQ 확정 메커니즘 (재사용 토대)
1. LP가 USDT를 구매자 파트너 MASTER로 온체인 전송(txHash).
2. blockchain-monitor가 입금 감지 → **Deposit 생성**.
3. TORQ 웹훅 COMPLETED(`handleCompleted`) → txHash로 Deposit 매칭 → `USER_DEPOSIT/TORQ` 보정 + partnerUserId 귀속.

### 통합 시 변경 (이중 크레딧 방지)
- **링크**: TORQ 레그 생성 시 `torq_trades.p2p_match_id` ↔ `p2p_matches.torq_escrow_id` 상호 링크.
- **재분류**: 매칭된 torq_trade가 p2p_match에 연결돼 있으면, LP→MASTER Deposit을 **구매자 직접 크레딧이 아니라 "파트너 MASTER 내부 충전(funding)"으로 분류** — 구매자 크레딧은 p2p 정산 ledger 한 곳에서만.
- **1단계 정산**: P2P/파트너는 (현금확인 → 우리가 MASTER→MASTER 송금) 2단계지만, **TORQ 레그는 LP→MASTER 입금 도착이 곧 정산**. 즉 입금 확정 = `confirmBankTransfer(match)` → 그 레그 즉시 SETTLED + 구매자 p2p ledger CREDIT(FEE 0). 별도 on-chain 송금 없음(P2pSettlement 미생성).
- **주문 완료**: P2P/파트너 레그(정산 송금 완료) + TORQ 레그(LP→MASTER 입금 확정)가 **전부 SETTLED → order COMPLETED**.

### 정산 방향 대비

| | 현금 확인 | USDT 이동 | 정산 단계 |
|---|---------|----------|----------|
| P2P/파트너 | CODEF | 우리가 PUSH (MASTER→구매자 MASTER, Relayer) | 2단계 |
| TORQ | TORQ 외부 | 외부가 PULL-in (LP→구매자 MASTER) | 1단계 (입금=정산) |

---

## 6-B. 분쟁 처리 일원화 (확정)

세 leg_type 모두 **동일 분쟁 UI + 동일 DB 저장**. 해결 권한만 분기 — P2P/파트너는 우리 관리자, TORQ는 TORQ 외부 판정.

### UI — 변경 없음
직전 작업(`a22923c`)으로 분쟁 화면이 이미 **matchCode 단위**(`p2p-dispute`/`p2p-dispute-reviewing`)라, 세 leg_type 전부 같은 화면을 그대로 쓴다. 위젯에 leg_type 분기 불필요.

### 데이터 — 전 레그 우리 DB 저장
`submitDispute(matchId, reason, evidenceUrl)`가 `p2p_matches`에 `dispute_reason`/`dispute_evidence_url`/`dispute_submitted_by=DEPOSITOR`/`disputed_at`/status=DISPUTED 저장(현행). **세 leg 동일.**

### 해결 권한 분기

| leg | 분쟁 전달 | 해결 주체 | 결과 반영 |
|-----|----------|----------|----------|
| **P2P/파트너** | 우리 DB만 | **우리 관리자** (admin `resolveDispute`) | `confirmBankTransfer`→SETTLED / FAILED |
| **TORQ** | 우리 DB **+ TORQ 전달** | **TORQ** (외부 판정) | TORQ 웹훅 resolution → p2p_match 반영 |

### 배선 (신규 작업 2곳)
1. **`submitDispute` leg_type 분기** — TORQ 레그면 우리 DB 저장 후 `torqService.submitEvidence(partnerId, torq_escrow_id, reason, evidenceUrl)`로 TORQ에 추가 전달(기존 API 재사용 — `torqClient.submitEvidence` + DB 기록). P2P/파트너는 저장만.
2. **TORQ 웹훅 → p2p_match 전파** — `handleForceReleased`/`handleCompleted`/`handleCancelled`가 연결된 p2p_match(`torq_escrow_id` 링크)를 찾아 resolution 매핑:
   - `RELEASE_TO_BUYER` / `COMPLETED` → 레그 **SETTLED** (구매자 승, LP→MASTER 1단계 정산)
   - `DISPUTE_REJECTED` / `CANCELLED` → 레그 **FAILED**

### 관리자 콘솔
- **P2P/파트너 레그 분쟁**: 관리자 액션(확인/거절) 가능.
- **TORQ 레그 분쟁**: "TORQ 처리 중"(읽기전용) — 우리가 판정하지 않고 TORQ resolution 대기/표시.

### 분쟁이 혼합 주문에 미치는 영향
- 레그 단위 격리(§7-B). TORQ 레그 분쟁 중에도 P2P/파트너 레그는 독립 진행·정산.
- (위젯 분할 per-card 표시 개선은 별도 잔존 — 메모리 `p2p-split-dispute-ui`)

---

## 7. 엣지 케이스

| 케이스 | 처리 |
|--------|------|
| TORQ가 잔여 전액 불가(LP 부족/장애) | TORQ 레그 건너뜀 → 잔여 슬롯을 파트너가 전액. 파트너 미등록이면 매칭 실패 |
| **잔여 < TORQ 최소금액** | TORQ "실패"로 간주 → 파트너가 잔여 전액 흡수(파트너 최소 없음). 파트너 미등록이면 실패 |
| **멀티 레그 라운딩 잔돈** | KRW 정수·USDT 나눗셈 잔차를 마지막(최대) 레그가 흡수 → Σ(레그 KRW)==주문 총액 보장 |
| P2P+TORQ 미충족 & 파트너 미등록 | **매칭 실패** — 구매자에게 안내(현재 가용 한도 표시) |
| P2P 레그 1건 분쟁/실패 | 해당 레그만 격리(§7-B). 나머지 레그 진행. (위젯 분할 per-card 표시 개선은 별도 — 메모리 `p2p-split-dispute-ui`) |
| TORQ 레그 만료/실패 | escrow 만료 → 매칭 FAILED. 잔여를 파트너로 재시도 또는 부분종결 |
| 재매칭(`rematchWaitingDeposits`) | 잔여 슬롯이 이미 채워진 주문은 재매칭 제외. 슬롯 중복생성 방지(주문당 TORQ/PARTNER 레그 1건 unique 가드) |
| 부분 종결(§7-B) | 확인된 레그(방식 무관) 단위로 즉시 정산, 보낸 만큼만 지급 |
| 파트너 레그 = 정상 판매 | 구매자(파트너 사용자)→파트너 서비스계좌 실제 현금 입금(원장 기록), 파트너 MASTER USDT 제공. 자전 아님 — 가드 불필요 |
| 동일 출금계좌 중복 | 기존 파트너+네트워크 가용액 누적검증 유지. TORQ 레그는 무관 |

---

## 8. 확정 결정 (Open Questions 전부 해소, 2026-06-13)

1. **체인 쉐어 분배** ✅ — TORQ 레그는 `p2p_order_shares` 분배에서 **제외, 시스템 전액 귀속**. P2P/파트너 레그만 체인 쉐어 대상. (torq_fee 외부비용으로 순마진 작거나 음수 가능)
2. **TORQ 레그 확인 트리거** ✅ — **확인 진입점은 통합**(`confirmBankTransfer(matchId)` 단일), **트리거 소스는 leg_type별 분기**: P2P/파트너=CODEF 스크래핑 잡, TORQ=TORQ 웹훅(push)+폴백 폴링+blockchain-monitor 온체인. 잡·위젯·웹훅이 같은 진입점으로 깔때기.
3. **잔여 최소/라운딩** ✅ — 입금은 **무조건 KRW 기준**. 잔여가 TORQ 최소금액 미만이면 **TORQ "실패" 처리 → 파트너 계좌가 잔여 전액 흡수**(파트너 catch-all은 최소 없음). 멀티 레그 라운딩 잔돈은 **마지막(또는 최대) 레그가 흡수**하여 Σ(레그 KRW)==주문 총액 보장.
4. **체인 선택** ✅ — **별도 선택 없음**. 구매자의 파트너가 3체인(TRON/BSC/...) 모두 보유하므로 레그가 어느 체인에 정산되든 수령 가능. 기존 네트워크 무관 매칭 유지.
5. **파트너 자전 방지** ✅ — **가드 불필요**. 파트너 계좌 leg = A 파트너의 사용자(구매자)가 A 파트너 서비스계좌로 **실제 현금 입금**(원장 기록) → 파트너가 USDT 제공. 파트너가 자기 사용자를 위한 **최종 유동성 공급자** 역할의 정상 판매이며, 실제 가치 이동·원장 기록되므로 자전이 아니다.

### 이전 확정 항목
- ✅ 수수료: 구매자 통합 2% 고정, 내부 원가 시스템 흡수.
- ✅ 잔여 충족: 단일 슬롯, TORQ 전액 가능 시 TORQ 아니면 파트너(최대 3계좌).
- ✅ 파트너 레일: P2P와 동일(CODEF + 파트너 유동성).
- ✅ 매칭 확정: 점진형 2단계 + RemainderProvider 체인 + 파트너 자동 무제한 standing.
- ✅ eKYC: 통합 후에도 구매자 eKYC 필수 유지(P2pEkycGuard, 변경 없음).

---

## 9. 단계별 구현 계획 (제안)

| Phase | 범위 | 산출물 |
|-------|------|--------|
| **0. 설계 확정** | 이 문서 + Open Q 8개 해소 | 본 문서 v1.0 |
| **1. DDL + 모델** | p2p_matches 확장, system_settings, 엔티티 | DDL 적용 + Entity |
| **2. 매칭 엔진** | tryMatchDeposit 폭포(P2P 2 → TORQ → 파트너), leg_type 생성, TORQ createTrade 연계 | P2pMatchingService |
| **3. 확인/정산 분기** | getBankAccountForMatch / 확인 / 정산 leg_type 3분기, TORQ 레그 확인 트리거 | Service + Job |
| **4. 위젯** | 단일 진입 + 최대 3카드 + 레그별(P2P/TORQ/파트너) 안내/분쟁 분기 | p2p.vue |
| **5. 수수료** | 구매자 통합 2% + 쉐어 분배 시 TORQ 레그 처리 | Settlement |
| **6. E2E** | P2P단독 / P2P+TORQ / P2P+파트너 / 실패(미등록) 4 시나리오 + 부분확정·분쟁·만료 | 테스트 |

---

## 10. 리스크 / 주의

- `withdraw_order_id` NULL 허용 후 **기존 정산·unlock·계좌조회 코드가 NPE 없이 분기**하는지 전수 점검(7곳). leg_type 가드 누락 시 TORQ 레그가 P2P 정산 경로로 빠지면 자금 사고 위험.
- TORQ는 외부 의존 — createTrade 동기 호출이 매칭 트랜잭션을 잡으므로 **타임아웃/실패 시 롤백 경계** 설계 필요(매칭 tx 안에서 외부 API 호출 지양, 또는 TORQ 레그는 매칭 후 AFTER_COMMIT 비동기 생성 검토).
- 롤백 안전판: `torq_fallback_enabled=false`면 즉시 기존 P2P-only 동작으로 복귀.

---

## 11. 매칭 링크 (통합 구매 링크) — 결정 2026-06-14

현재 파트너 콘솔은 "현금→USDT 구매" 링크가 **TORQ 링크(`torq_links`) + P2P 입금 링크(`p2p_deposit_links`)** 로 이원화돼 있다. 통합 매칭 서비스에 맞춰 **단일 "매칭 링크"** 로 합친다.

### 11.1 확정 사항
| 항목 | 결정 |
|------|------|
| **베이스** | **새 "매칭 링크"(`matching_links`) 신설** — P2P 입금 링크 승격이 아니라 신규 도메인(깨끗한 IA, 통합 전용) |
| **TORQ 링크** | **폐기** — 파트너 콘솔 메뉴·신규 생성 제거. TORQ는 별도 링크가 아니라 매칭 내 fallback 레그로만 존재 |
| **범위** | **cash→USDT 구매 링크만**(TORQ+P2P). 결제 링크(`payment_links` 일반 크립토)·폰페이·Axim Pay는 별개 결제수단으로 유지 |
| **흐름** | 매칭 링크 사용 → 통합 deposit 주문 생성 → 통합 매칭(P2P>TORQ>파트너) 폭포 |

### 11.2 데이터 모델 (신규 `matching_links`)
```sql
CREATE TABLE matching_links (
  id BIGINT AUTO_INCREMENT PRIMARY KEY,
  link_code VARCHAR(40) NOT NULL UNIQUE     COMMENT 'mlnk_{random}',
  partner_id BIGINT NOT NULL,
  partner_user_id VARCHAR(255) NULL         COMMENT '파트너 측 사용자 식별자',
  partner_reference VARCHAR(255) NULL        COMMENT '파트너 참조(주문번호 등)',
  title VARCHAR(255) NULL,
  amount BIGINT NULL                         COMMENT 'KRW 고정금액. NULL=구매자 입력',
  -- network_id 없음: 네트워크 무관 매칭(구매자 파트너 3체인 보유)
  deposit_order_id BIGINT NULL              COMMENT '사용 시 생성된 p2p_deposit_orders.id',
  status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE' COMMENT 'ACTIVE/USED/EXPIRED',
  expires_at DATETIME(6) NULL,
  created_at DATETIME(6) DEFAULT CURRENT_TIMESTAMP(6),
  updated_at DATETIME(6) DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
  KEY idx_ml_partner (partner_id), KEY idx_ml_status (status)
) COMMENT '통합 매칭 구매 링크 (TORQ+P2P 통합)';
```
- p2p_deposit_links와 거의 동형이나 **match_mode/network_id 제거**(통합 매칭은 네트워크 무관·폭포 고정). 상태전이는 결제완료 기준(B안) 재사용.

### 11.3 백엔드
- `MatchingLink` 엔티티 + `MatchingLinkRepository` + `MatchingLinkService`(create/get/use/expire/reactivate).
- **use(linkCode)** = eKYC 가드(P2pEkycGuard) → `depositService.createAndMatch(...)` (통합 매칭) → link USED 전이(완료 기준). 사실상 P2pDepositLinkService.useLink 로직 재사용.
- partner-api: `MatchingLinkController`(생성/목록/비활성). open-api widget: 링크 정보 조회(Public) + use(인증).

### 11.4 파트너 콘솔 (partner-ui)
- 신규 메뉴 **"매칭 링크"**(생성 폼: 금액 선택입력·제목·참조·만료 / 목록·상태·복사).
- **"TORQ 링크" 메뉴 제거**, "P2P 입금 링크"는 매칭 링크로 대체(라우트/메뉴 정리).
- 링크 URL = 위젯 통합 진입(`/m/{code}` 또는 위젯 라우트).

### 11.5 위젯
- 매칭 링크 코드 → **단일 진입(Phase 5)**: eKYC → 통합 deposit 주문 → 폭포 매칭 → 최대 3카드(P2P/TORQ/파트너) 노출 → 레그별 이체·확인 → 완료.
- 기존 위젯의 TORQ/P2P 택일 라우팅(`runRouting`) 제거 대상과 동일선상.

### 11.6 마이그레이션 / 폐기
- **TORQ 링크**: 신규 생성 차단(메뉴 제거). 기존 활성 TORQ 링크는 만료까지 레거시 동작(또는 일괄 만료 후 매칭 링크 안내).
- **P2P 입금 링크**: 신규는 매칭 링크로. 기존 활성 링크 동작 유지.
- 게이트(`unified_matching_enabled`)와 연동 — 매칭 링크는 통합 매칭 ON 전제.

### 11.7 구현 순서
```
1. DDL matching_links + 엔티티/Repo
2. MatchingLinkService(use=createAndMatch 재사용) + partner-api Controller + open-api widget EP
3. partner-ui 매칭 링크 메뉴(생성/목록) + TORQ 링크 메뉴 제거
4. 위젯 매칭 링크 진입(Phase 5 단일 진입과 통합)
5. TORQ/P2P 입금 링크 폐기 처리(신규 차단)
```
> 의존: 위젯 단일 진입(Phase 5) + 통합 매칭 게이트. 매칭 링크는 Phase 5와 함께 진행.

---

## 12. 동일 계좌 레그 병합 (확인 단위 그룹핑) — 결정 2026-06-14

### 12.1 문제

한 입금 주문(deposit order)이 **같은 은행계좌로 가는 P2P 레그를 2개 이상** 만들 수 있다.
- 예: 50,000원 주문 → P2P 15,100(pwo_900e) + P2P 4,461(pwo_31d9) + TORQ 30,439. 그런데 pwo_900e·pwo_31d9가 **둘 다 같은 판매자(ryan003)의 동일 계좌**(우리은행 38221667602002).
- 원인: 한 판매자가 같은 계좌로 PENDING 출금주문을 2건 가질 수 있고, 매칭 루프(`tryMatchDepositUnified`, `findMatchableWithdrawOrdersForUpdate`)는 출금주문 단위로 레그를 따로 생성한다(1 leg = 1 withdraw_order).
- 결과: 구매자가 같은 계좌에 15,100·4,461을 **따로 2번** 보내야 함. 자연스럽게 19,561을 한 번에 보내면 스크래핑이 15,100/4,461 어느 쪽과도 금액 불일치 → 둘 다 자동확정 실패 → 자동분쟁. **확인 정합성 붕괴.**

### 12.2 결정 (사용자 확정 2026-06-14)

> "매칭 완료가 끝나기 전에 추가된다면 합쳐지는 게 맞다."

**한 입금 주문 내에서, 매칭이 전액 충족(완료)되기 전에 추가되는 레그가 이미 존재하는 레그와 동일한 `bank_account_id`로 가면, 그 레그들을 구매자 확인(송금) 단위로 하나로 합친다.** → 원칙: **1 은행계좌 = 1 송금 = 1 확인.**

### 12.3 어느 레이어에서 합치는가 — 확인(confirmation) 레이어

병합은 **정산이 아니라 확인 레이어**다. 두 레그는 서로 다른 withdraw order(서로 다른 frozen USDT, 서로 다른 판매자 정산 의무)에서 나오므로 **온체인 정산은 레그별로 분리 유지**해야 한다. 합치는 대상은 "구매자가 은행에 얼마를 한 번에 보내는가 + 스크래핑이 그 입금을 어떻게 대조하는가"뿐이다.

### 12.4 구체 규칙

| 항목 | 규칙 |
|------|------|
| **그룹 키** | (deposit_order_id, bank_account_id) + leg_type ∈ {P2P, PARTNER}. **TORQ 레그 제외**(외부 escrow 계좌 → 항상 별도 카드) |
| **병합 시점** | 매칭 패스 중 leg 생성 시. 동일 그룹 키 leg가 이미 있으면 동일 transfer-group으로 묶음. 재매칭(`rematchWaitingDeposits`)으로 주문 미완료 중 추가되는 레그도 동일 적용 |
| **위젯 표시** | transfer-group 당 **1 카드**. 표시 금액 = 그룹 레그 KRW **합산**(예: 15,100+4,461=19,561). 은행/계좌/예금주 동일 |
| **스크래핑 확인** | 그룹 **합산 금액**으로 대조. 입금자명(buyer_name)·은행 체크는 그룹 공통. 합산액 입금 1건 감지 → 그룹 내 전 레그 **일괄 confirm** → 레그별 정산 트리거 |
| **부분 입금** | 합산보다 적게 보내면 금액 불일치 → 자동확정 안 됨 → 기존 분쟁 흐름. (개선: 기대 금액이 합산 1개로 명확 — 이전엔 어떤 단일 레그와도 불일치) |
| **정산** | **변경 없음** — 레그별로 각 withdraw order 판매자에게 정산 |
| **완료 후 추가 금지** | 그룹의 한 레그가 이미 confirm/정산되었으면, 이후 들어온 동일 계좌 레그는 **병합하지 않음**(확인 취소 불가). → "매칭 완료 전" 조건의 의미 |

### 12.5 구현 영향 (다음 단계 — 결정만 확정, 코드는 미착수)

- **매칭 엔진**(`P2pMatchingService.tryMatchDepositUnified`/`createMatch`): leg 생성 시 동일 (deposit_order, bank_account_id) 그룹 식별·연결.
- **확인잡**(`P2pScrapingVerifyJob`/`P2pScrapingService`): 대조 단위를 leg 단위 → transfer-group 단위(합산 금액)로. confirm 시 그룹 전 레그 일괄 처리.
- **위젯 응답/표시**(`P2pWidgetController.toMatchDetailResponse`, `p2p.vue`): 동일 계좌 레그를 1 카드로 합쳐 합산 금액 노출.
- **정산**: 무변경.
- **그룹 식별 방식(택일, 구현 단계 결정)**: (A) 런타임 도출 = (deposit_order_id, bank_account_id)로 그룹핑(DDL 무변경, 권장) / (B) `p2p_matches.transfer_group_id` 명시 컬럼(인덱스·확인잡 단순, DDL 1컬럼 추가). → 구현 착수 시 확정.

### 12.6 §7 엣지 케이스 갱신

- 기존 "동일 출금계좌 중복(299행)"은 **파트너+네트워크 가용액 누적검증** 맥락. 본 §12는 그와 별개로 **확인 정합성**(구매자 송금/스크래핑 대조)을 다룸 — 둘 다 유효.
