# P2P 매칭 아키텍처 — 크로스사이트 출금↔입금 자동 매칭

> **버전**: v0.6 (1:3 매칭 + 파트너 콘솔 + 출금자 페이지)
> **작성일**: 2026-05-28 ~ 2026-06-05
> **상태**: 설계 진행 중

---

## 1. 개요

A 사이트(파트너)의 **출금 고객**과 B 사이트(파트너)의 **입금 고객**을 매칭하여,
법정화폐(KRW) 이체를 P2P로 처리하고, 이후 디지털 자산(USDT)을 A→B로 정산하는 시스템.

**핵심 가치**: 법정화폐 이동 없이 고객 간 직접 이체로 처리하므로, cryptoments의 법정화폐 유동성 부담이 없다.

### 1.1 TORQ 연동 — P2P 우선 매칭

P2P 매칭은 **기존 TORQ 플로우에 통합**된다. 입금 고객이 매칭을 요청하면:

```
1순위: P2P 출금 대기자 (내부 매칭)   ← 우선 탐색
2순위: TORQ LP (외부 유동성)          ← P2P 출금자 없을 때
3순위: 매칭 대기 ("찾는 중입니다...")   ← LP도 없을 때
```

### 1.2 TORQ와의 차이

| 항목 | TORQ | P2P 매칭 |
|------|------|---------|
| LP(유동성 공급자) | 외부 TORQ 서비스 | **cryptoments 자체 매칭 엔진** |
| 매칭 단위 | 1:1 (TORQ 내부) | **1:N 부분 매칭** |
| 입금 확인 | 수동 (송금 완료 버튼) + 스크래핑 | **스크래핑 API 자동 확인 (동일)** |
| 자금 흐름 | KRW→TORQ LP→USDT→고객 | **KRW: A↔B 고객 직접 / USDT: A파트너→B파트너** |
| 파트너 범위 | 단일 파트너 | **크로스 파트너 + 동일 파트너** |
| 분쟁 처리 | TORQ 분쟁 프로세스 | **TORQ와 동일한 분쟁 프로세스 적용** |

### 1.3 MASTER 지갑 통합 (별도 지갑 없음)

P2P 매칭은 파트너의 **기존 MASTER 지갑**을 그대로 사용한다.
별도 P2P 전용 지갑이나 충전 단계가 없다.

```
[지갑 구조]
  기존 그대로: HOT(입금) / MASTER(출금원천+P2P정산) / FEE(가스비) / SYSTEM(Relayer)
  별도 지갑 추가 없음

[P2P 가용 잔액] = MASTER.available_balance - p2p_partner_locks.locked_balance
  → MASTER 잔액이 곧 P2P 출금 가능 한도

[정산] A사이트 MASTER → B사이트 MASTER (온체인 전송)
       → B사이트 입장에서는 일반 사용자 입금(deposit)으로 처리
       → Relayer.transferFrom(MASTER_A → MASTER_B) — 기존 Approve 활용

[잔액 부족] MASTER 가용 잔액 < 매칭 금액 → 해당 사이트 P2P 매칭 제외 → TORQ LP fallback
```

### 1.4 수수료 구조

- **입금 측**: **2%** (TORQ와 동일)
- **출금 측**: 출금 수수료 취득 (총판이 수수료율 책정)
- 양쪽 모두 수수료 부과, 총판(`partners`)별 개별 설정

### 1.5 환율

- **거래 시작(주문 생성) 시점**의 USDT 환율 확정
- 출금 요청이 KRW인 경우: `usdt_amount = krw_amount / exchange_rate`
- 출금 요청이 USDT인 경우: `krw_amount = usdt_amount × exchange_rate`

---

## 2. 핵심 흐름

### 2.1 매칭 라우팅 (1:1 + TORQ fallback)

```
B고객 입금 요청 (입금액 N원)
    │
    ▼
  출금 잔여액 ≥ N원인 대기자가 있는가?
    │
  예 ──→ P2P 1:1 매칭 (출금 잔여 차감, 계좌 전달)
    │
  아니오 ──→ TORQ LP 전액 (기존 플로우 그대로)
              │
            LP 없음 ──→ 매칭 대기 ("찾는 중...")
```

**핵심 규칙:**
- **1:1 매칭만** — 1:N 부분 매칭 없음
- **입금 ≤ 출금 잔여**일 때만 P2P 매칭 (입금 > 출금이면 TORQ fallback)
- 같은 `network_id`끼리만 매칭 (FOR UPDATE 비관적 잠금)
- 동일 계좌로 활성 출금 주문 동시 등록 불가
- 출금 주문 만료: **48시간**

### 2.2 출금자(A사이트) 플로우

```
사용자 출금 요청 (API/Widget/콘솔)
  → withdrawals INSERT (REQUESTED) + freeze(금액)
  → 관리자 승인 시 "직접 출금" 또는 "P2P 전환" 선택
      ├── 직접 출금 → 기존 플로우 (APPROVED → 온체인 → CONFIRMED → debit + unfreeze)
      └── P2P 전환 → withdraw 상태 P2P_PENDING
                    → p2p_withdraw_order 생성 (withdrawal_id 참조)
                    → 스크래핑 빠른조회 등록 (회원 계좌)
                    → 매칭 대기 (48시간 만료)
```

P2P 전환 후 매칭+정산:
```
입금자 30만원 매칭 → P2P 정산 → unfreeze(30) + debit(30)
입금자 60만원 매칭 → P2P 정산 → unfreeze(60) + debit(60)
잔여 10만원 → 파트너가 선택:
  A) 출금 전환: unfreeze(10) → 새 withdrawal(REQUESTED) → 일반 출금 플로우
  B) 강제 정산: unfreeze(10) → 장부 기록 (파트너 직접 지급 완료)
  C) 취소:     unfreeze(10) → 환불 기록 → 사용자 잔액 복원
```

withdraw 레코드 1건으로 완결 (부분 debit):
```
withdrawals #1 (100만원)
  ledger DEBIT 30만원 — P2P 정산 (ref: P2P_SETTLEMENT)
  ledger DEBIT 60만원 — P2P 정산 (ref: P2P_SETTLEMENT)
  잔여 10만원 → A/B/C 중 택1
  → COMPLETED
```

잔여 환불 시 p2p_withdraw_order에 기록:
```
p2p_withdraw_orders:
  refunded_amount: 10만원
  refund_type: CANCEL / FORCE_SETTLE
  refund_memo: "..."
  → 파트너가 자기 시스템에서 사용자 잔액 복원
```

### 2.3 입금자(B사이트 구매자) 플로우

```
eKYC 인증 완료 (Axim — 공통 필수)
  → Widget Controller 매칭 라우팅:
      출금 잔여 ≥ 입금액인 대기자 있는가?
        예 → P2P 1:1 매칭 → 출금자 계좌 정보 전달
        아니오 → TORQ LP 전액 (기존 플로우 그대로)
  → 매칭 후: 출금자 계좌로 KRW 이체
  → 스크래핑 자동 감지 → BANK_CONFIRMED
  → 정산: MASTER(A) → MASTER(B) 온체인
  → B고객 USDT 수령 (수수료 차감 후)
```

### 2.4 상세 단계

| 단계 | 주체 | 행위 | 시스템 반응 |
|------|------|------|-----------|
| W1 | A고객 | 출금 요청 (API/Widget/콘솔) | `withdrawals` INSERT + freeze |
| W2 | 관리자 | P2P 전환 승인 | `p2p_withdraw_orders` INSERT, 스크래핑 등록 |
| W3 | - | *매칭 대기 (48시간)* | 사용자 P2P 페이지에서 현황 확인 |
| D1 | B고객 | eKYC 인증 + 입금 요청 | Widget Controller 매칭 라우팅 |
| D2 | 시스템 | P2P 매칭 (출금 잔여 ≥ 입금) | `p2p_matches` INSERT, 양쪽 상태 업데이트 |
| D2' | 시스템 | (매칭 불가 시) TORQ LP | 기존 TORQ 플로우 |
| D3 | B고객 | A고객 계좌로 KRW 이체 | 스크래핑 빠른조회로 자동 감지 |
| D4 | 시스템 | 입금 확인 → BANK_CONFIRMED | match 상태 업데이트 |
| S1 | 시스템 | 정산: MASTER(A)→MASTER(B) | unfreeze + debit + 온체인 전송 |
| S2 | 시스템 | 알림 | Webhook (withdraw.confirmed, deposit.completed) |
| R1 | 관리자 | 잔여분 처리 (출금전환/강제정산/취소) | 신규 withdrawal 또는 환불 기록 |

---

## 3. 데이터 모델 (DDL)

### 3.0 p2p_site_deposits — 출금 보증 풀

P2P 출금 서비스를 위한 USDT 예치금. 예치 잔액 = 해당 사이트 고객의 P2P 출금 가능 한도.
체인별 별도 지갑(P2P_DEPOSIT 타입)에 온체인 전송으로 충전.

```sql
CREATE TABLE p2p_site_deposits (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    partner_id BIGINT NOT NULL
        COMMENT 'partners.id — 출금 서비스 제공 사이트',
    network_id BIGINT NOT NULL
        COMMENT 'blockchain_networks.id — 체인',
    wallet_address_id BIGINT NOT NULL
        COMMENT 'wallet_addresses.id — P2P_DEPOSIT 지갑 (체인당 1개)',
    
    -- 디파짓 잔액
    total_deposited DECIMAL(36,18) NOT NULL DEFAULT 0
        COMMENT '총 충전 USDT 누적',
    available_balance DECIMAL(36,18) NOT NULL DEFAULT 0
        COMMENT '사용 가능 잔액 (출금 매칭에 사용 가능한 금액)',
    locked_balance DECIMAL(36,18) NOT NULL DEFAULT 0
        COMMENT '매칭 중 잠긴 금액 (은행 이체 확인 전)',
    settled_balance DECIMAL(36,18) NOT NULL DEFAULT 0
        COMMENT '정산 완료 누적 (A→B MASTER로 전송된 총액)',
    
    -- 상태
    status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE'
        COMMENT 'ACTIVE / SUSPENDED / DEPLETED',
    
    created_at DATETIME(6) DEFAULT CURRENT_TIMESTAMP(6),
    updated_at DATETIME(6) DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
    
    UNIQUE KEY uk_partner_network (partner_id, network_id)
) COMMENT 'P2P 출금 보증 풀 — 파트너가 예치한 USDT (체인당 1개)';

CREATE TABLE p2p_site_deposit_history (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    site_deposit_id BIGINT NOT NULL
        COMMENT 'p2p_site_deposits.id',
    partner_id BIGINT NOT NULL
        COMMENT 'partners.id',
    
    type VARCHAR(20) NOT NULL
        COMMENT 'DEPOSIT(충전) / LOCK(매칭잠금) / UNLOCK(매칭해제) / SETTLE(정산차감) / REFUND(환불)',
    amount DECIMAL(36,18) NOT NULL
        COMMENT '변동 금액 (양수)',
    balance_after DECIMAL(36,18) NOT NULL
        COMMENT '변동 후 available_balance',
    
    -- 연관 거래
    reference_type VARCHAR(30)
        COMMENT 'P2P_MATCH / P2P_SETTLEMENT / WEBHOOK / MANUAL',
    reference_id BIGINT
        COMMENT '참조 ID',
    
    memo VARCHAR(500),
    tx_hash VARCHAR(255)
        COMMENT '온체인 TX hash (충전/정산 시)',
    
    created_at DATETIME(6) DEFAULT CURRENT_TIMESTAMP(6),
    
    KEY idx_site_deposit (site_deposit_id),
    KEY idx_partner (partner_id),
    KEY idx_type (type)
) COMMENT 'P2P 디파짓 변동 이력 — 충전/잠금/해제/정산/환불 추적';
```

### 3.1 p2p_withdraw_orders — 출금 주문

A사이트 고객이 출금을 요청할 때 생성. KRW 또는 USDT로 요청 가능.

```sql
CREATE TABLE p2p_withdraw_orders (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    order_code VARCHAR(30) NOT NULL UNIQUE
        COMMENT '주문 코드 (pwo_{nanoid})',
    partner_id BIGINT NOT NULL
        COMMENT 'partners.id — A사이트 파트너',
    partner_user_id VARCHAR(100) NOT NULL
        COMMENT '파트너 측 사용자 ID',
    
    -- 요청 방식
    request_currency VARCHAR(10) NOT NULL DEFAULT 'KRW'
        COMMENT '요청 통화 (KRW / USDT)',
    
    -- 금액
    krw_amount BIGINT NOT NULL
        COMMENT '출금 KRW 금액 (USDT 요청 시 환산)',
    usdt_amount DECIMAL(36,18) NOT NULL
        COMMENT 'USDT 수량 (KRW 요청 시 환산)',
    exchange_rate DECIMAL(20,4) NOT NULL
        COMMENT '거래 시작 시점 확정 환율 (KRW/USDT)',
    
    matched_amount BIGINT NOT NULL DEFAULT 0
        COMMENT '매칭 완료된 KRW 금액 (≤ krw_amount)',
    confirmed_amount BIGINT NOT NULL DEFAULT 0
        COMMENT '입금 확인된 KRW 금액 (≤ matched_amount)',
    
    -- 입금 받을 계좌 (A고객이 입력)
    bank_code VARCHAR(10) NOT NULL
        COMMENT '은행 코드 (004=KB, 088=신한 등)',
    bank_name VARCHAR(50) NOT NULL
        COMMENT '은행명',
    account_number VARCHAR(50) NOT NULL
        COMMENT '계좌번호',
    account_holder VARCHAR(50) NOT NULL
        COMMENT '예금주',
    
    -- 수수료 (총판 설정 기준)
    fee_rate DECIMAL(10,6) DEFAULT 0
        COMMENT '출금 수수료율 (총판 책정)',
    fee_amount BIGINT DEFAULT 0
        COMMENT '출금 수수료 KRW',
    
    -- 상태
    status VARCHAR(20) NOT NULL DEFAULT 'PENDING'
        COMMENT 'PENDING / PARTIALLY_MATCHED / FULLY_MATCHED / SETTLING / COMPLETED / CANCELLED / EXPIRED',
    
    -- 타임라인
    expires_at DATETIME(6) NOT NULL
        COMMENT '만료 시각 (기본 30분)',
    matched_at DATETIME(6)
        COMMENT '완전 매칭 시각',
    settled_at DATETIME(6)
        COMMENT 'USDT 정산 완료 시각',
    completed_at DATETIME(6)
        COMMENT '전체 프로세스 완료 시각',
    cancelled_at DATETIME(6),
    cancel_reason VARCHAR(200),
    
    created_at DATETIME(6) DEFAULT CURRENT_TIMESTAMP(6),
    updated_at DATETIME(6) DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
    
    KEY idx_partner_status (partner_id, status),
    KEY idx_status_amount (status, krw_amount),
    KEY idx_expires (expires_at)
) COMMENT 'P2P 출금 주문 — A사이트 고객이 출금 요청 (KRW or USDT)';
```

### 3.2 p2p_deposit_orders — 입금 주문

B사이트 고객이 KRW 입금을 요청할 때 생성.

```sql
CREATE TABLE p2p_deposit_orders (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    order_code VARCHAR(30) NOT NULL UNIQUE
        COMMENT '주문 코드 (pdo_{nanoid})',
    partner_id BIGINT NOT NULL
        COMMENT 'partners.id — B사이트 파트너',
    partner_user_id VARCHAR(100) NOT NULL
        COMMENT '파트너 측 사용자 ID',
    
    -- 금액
    krw_amount BIGINT NOT NULL
        COMMENT '입금 요청 KRW 금액',
    remaining_amount BIGINT NOT NULL
        COMMENT '미매칭 잔여 금액 (= krw_amount - 매칭된 합계)',
    
    -- 상태
    status VARCHAR(20) NOT NULL DEFAULT 'PENDING'
        COMMENT 'PENDING / MATCHING / MATCHED / COMPLETED / CANCELLED / EXPIRED',
    
    -- 타임라인
    expires_at DATETIME(6) NOT NULL
        COMMENT '만료 시각',
    completed_at DATETIME(6),
    cancelled_at DATETIME(6),
    cancel_reason VARCHAR(200),
    
    created_at DATETIME(6) DEFAULT CURRENT_TIMESTAMP(6),
    updated_at DATETIME(6) DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
    
    KEY idx_partner_status (partner_id, status),
    KEY idx_status_remaining (status, remaining_amount),
    KEY idx_expires (expires_at)
) COMMENT 'P2P 입금 주문 — B사이트 고객이 KRW 입금 요청';
```

### 3.3 p2p_matches — 매칭 레코드

출금 주문과 입금 주문의 매칭 관계. 1:N 부분 매칭이므로 하나의 출금 주문에 여러 매칭 레코드 가능.

```sql
CREATE TABLE p2p_matches (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    match_code VARCHAR(30) NOT NULL UNIQUE
        COMMENT '매칭 코드 (pm_{nanoid})',
    
    withdraw_order_id BIGINT NOT NULL
        COMMENT 'p2p_withdraw_orders.id',
    deposit_order_id BIGINT NOT NULL
        COMMENT 'p2p_deposit_orders.id',
    
    -- 매칭 금액 (부분 매칭)
    krw_amount BIGINT NOT NULL
        COMMENT '이 매칭의 KRW 금액',
    
    -- 상태
    status VARCHAR(20) NOT NULL DEFAULT 'CREATED'
        COMMENT 'CREATED / BANK_PENDING / BANK_CONFIRMED / SETTLING / SETTLED / FAILED / CANCELLED',
    
    -- 은행 이체 확인
    bank_transfer_ref VARCHAR(100)
        COMMENT '은행 API 이체 확인 참조 번호',
    bank_confirmed_at DATETIME(6)
        COMMENT '은행 입금 확인 시각',
    
    -- 정산
    settled_at DATETIME(6)
        COMMENT 'USDT 정산 완료 시각',
    
    -- 만료
    expires_at DATETIME(6) NOT NULL
        COMMENT '이체 기한 (매칭 후 N분)',
    
    created_at DATETIME(6) DEFAULT CURRENT_TIMESTAMP(6),
    updated_at DATETIME(6) DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
    
    KEY idx_withdraw (withdraw_order_id),
    KEY idx_deposit (deposit_order_id),
    KEY idx_status (status),
    KEY idx_expires (expires_at)
) COMMENT 'P2P 매칭 — 출금↔입금 부분 매칭 레코드';
```

### 3.4 p2p_settlements — 정산 레코드

매칭 확인 후 USDT 이체 기록.

```sql
CREATE TABLE p2p_settlements (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    settlement_code VARCHAR(30) NOT NULL UNIQUE
        COMMENT '정산 코드 (ps_{nanoid})',
    
    match_id BIGINT NOT NULL
        COMMENT 'p2p_matches.id',
    
    -- 출금(A) → 입금(B) 정산
    from_partner_id BIGINT NOT NULL
        COMMENT 'A파트너 (출금 측)',
    to_partner_id BIGINT NOT NULL
        COMMENT 'B파트너 (입금 측)',
    
    -- 금액
    krw_amount BIGINT NOT NULL
        COMMENT 'KRW 기준 금액',
    usdt_amount DECIMAL(36,18) NOT NULL
        COMMENT 'USDT 정산 금액',
    exchange_rate DECIMAL(20,4) NOT NULL
        COMMENT '적용 환율',
    fee_amount DECIMAL(36,18) DEFAULT 0
        COMMENT '수수료 (USDT)',
    
    -- 정산 방식
    settlement_type VARCHAR(20) NOT NULL
        COMMENT 'ONCHAIN / LEDGER — 파트너 설정에 따라',
    
    -- 온체인 정산 시
    tx_hash VARCHAR(255)
        COMMENT '온체인 TX hash',
    from_address VARCHAR(255)
        COMMENT '출금 지갑 주소',
    to_address VARCHAR(255)
        COMMENT '입금 지갑 주소',
    
    -- 상태
    status VARCHAR(20) NOT NULL DEFAULT 'PENDING'
        COMMENT 'PENDING / PROCESSING / COMPLETED / FAILED',
    
    completed_at DATETIME(6),
    failed_at DATETIME(6),
    failure_reason VARCHAR(500),
    
    created_at DATETIME(6) DEFAULT CURRENT_TIMESTAMP(6),
    updated_at DATETIME(6) DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
    
    KEY idx_match (match_id),
    KEY idx_from_partner (from_partner_id),
    KEY idx_to_partner (to_partner_id),
    KEY idx_status (status)
) COMMENT 'P2P 정산 — 매칭 확인 후 USDT 이체 기록';
```

---

## 4. 상태 머신

### 4.1 출금 주문 (p2p_withdraw_orders.status)

```
PENDING ────────→ PARTIALLY_MATCHED ────→ FULLY_MATCHED ────→ SETTLING ────→ COMPLETED
   │                     │                                         │
   │                     │ (매칭 일부 실패)                          │
   ▼                     ▼                                         ▼
EXPIRED              PENDING (롤백)                              FAILED
   │
   ▼
CANCELLED
```

- `PENDING`: 매칭 대기 중
- `PARTIALLY_MATCHED`: 일부 금액 매칭됨 (나머지 대기)
- `FULLY_MATCHED`: 전체 금액 매칭 완료, 입금 확인 대기
- `SETTLING`: 모든 매칭의 입금 확인됨, USDT 정산 중
- `COMPLETED`: 정산 완료
- `EXPIRED`: 만료 시간 내 매칭 안 됨
- `CANCELLED`: 사용자/관리자 취소

### 4.2 입금 주문 (p2p_deposit_orders.status)

```
PENDING ────→ MATCHING ────→ MATCHED ────→ COMPLETED
   │                            │
   ▼                            ▼
EXPIRED                      CANCELLED
```

- `PENDING`: 매칭 대기
- `MATCHING`: 매칭 진행 중 (부분 매칭 포함)
- `MATCHED`: 매칭 완료, 은행 이체 진행/완료
- `COMPLETED`: 모든 매칭 건의 이체 + 정산 완료
- `EXPIRED` / `CANCELLED`

### 4.3 매칭 (p2p_matches.status)

```
CREATED ────→ BANK_PENDING ────→ BANK_CONFIRMED ────→ SETTLING ────→ SETTLED
                   │                                                    │
                   ▼                                                    ▼
               FAILED (시간 초과)                                     FAILED
                   │
                   ▼
              CANCELLED
```

- `CREATED`: 매칭 생성, B고객에게 계좌 정보 전달 대기
- `BANK_PENDING`: B고객이 계좌 정보 확인, 이체 대기
- `BANK_CONFIRMED`: 은행 API로 입금 확인됨
- `SETTLING`: USDT 정산 진행 중
- `SETTLED`: 정산 완료
- `FAILED` / `CANCELLED`

---

## 5. 매칭 엔진

### 5.1 매칭 풀 구성

| 풀 | 소스 | 한도 |
|---|---|---|
| 개인 출금자 계좌 | p2p_members → bank_accounts (MEMBER) | 출금 주문별 금액 |
| 파트너 서비스 계좌 | partners → bank_accounts (PARTNER, 1개) | MASTER 지갑 USDT 잔액 |

### 5.2 매칭 전략 — 최대 1:3

입금자 1명에 대해 출금자 최대 3명 매칭. 큰 금액 우선, 이후 정확 매칭.

```
매칭 알고리즘:
  1. 개인 출금자 탐색 (큰 금액 우선, 같은 network_id, 최대 3명)
  2. 3명으로 부족하면 파트너 서비스 계좌로 잔여 충당
  3. 커버리지 판정:
     매칭 합계 ≥ 입금액의 70% → P2P 진행 (잔여 TORQ)
     매칭 합계 < 입금액의 70% → P2P 안 함
```

### 5.3 라우팅 — fallback은 라우팅 단계에서만

```
[라우팅 단계] Widget Controller — 여기서만 P2P/TORQ 분기
  매칭 풀 확인 → P2P 커버리지 ≥ 70%?
    예 → P2P 플로우 진입
    아니오 → TORQ 플로우 진입

[P2P 플로우] 독립 — TORQ fallback 없음
  매칭 성공 → 계좌 정보 + 이체 안내 (최대 3건)
  매칭 실패 (타이밍 경합) → 매칭 실패 → 처음부터 다시 시도

[TORQ 플로우] 독립 — P2P와 무관
  기존 TORQ 플로우 그대로
```

### 5.4 입금자 접근 경로 2가지

| 경로 | 진입 | 매칭 방식 | 실패 시 |
|------|------|---------|--------|
| Widget 경유 | torq.vue → 라우팅 | P2P 또는 TORQ (라우팅 판정) | 처음부터 → 재라우팅 |
| P2P 전용 링크 | p2p.vue 직접 접근 | P2P만 (TORQ 없음) | 처음부터 → P2P 재시도 |

### 5.5 P2P 링크 — 자동/수동 매칭

파트너가 링크 생성 시 매칭 모드 선택:

**자동 매칭 (AUTO):**
- 금액만 지정 → 링크 생성
- 입금자 접속 시 자동 매칭

**수동 매칭 (MANUAL):**
- 금액 + 대기 중인 출금자(계좌) 직접 선택 → 링크 생성
- 링크 생성 시점에 p2p_matches 미리 생성 + 출금자 freeze
- 입금자 접속 시 이미 매칭 완료 → 바로 이체 화면

```sql
-- p2p_links
CREATE TABLE p2p_links (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    link_code VARCHAR(50) NOT NULL UNIQUE COMMENT 'plnk_{nanoid}',
    partner_id BIGINT NOT NULL,
    partner_user_id VARCHAR(100),
    amount BIGINT NOT NULL COMMENT 'KRW',
    network_id BIGINT NOT NULL,
    match_mode VARCHAR(10) NOT NULL DEFAULT 'AUTO' COMMENT 'AUTO / MANUAL',
    expires_minutes INT NOT NULL DEFAULT 30 COMMENT '5 / 10 / 30 / 60',
    status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE' COMMENT 'ACTIVE/USED/EXPIRED/CANCELLED',
    expires_at DATETIME(6) NOT NULL,
    created_at DATETIME(6) DEFAULT CURRENT_TIMESTAMP(6),
    updated_at DATETIME(6) DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
    KEY idx_partner (partner_id),
    KEY idx_status_expires (status, expires_at)
) COMMENT 'P2P 입금 링크 — 자동/수동 매칭';
```

### 5.6 매칭 알고리즘 (의사코드)

```
function tryP2pMatch(depositAmount, networkId, partnerId):
    // 1단계: 개인 출금자 탐색 (큰 금액 우선, 최대 3명)
    candidates = findWithdrawOrders(
        status IN ('PENDING', 'PARTIALLY_MATCHED'),
        network_id = networkId,
        (krw_amount - matched_amount) > 0,
        ORDER BY (krw_amount - matched_amount) DESC, created_at ASC
        FOR UPDATE
    )
    
    matches = []
    remaining = depositAmount
    for wo in candidates:
        if len(matches) >= 3: break
        available = wo.krw_amount - wo.matched_amount
        matchAmount = min(available, remaining)
        if lockMaster(wo.partner_id, matchAmount): 
            matches.add({wo, matchAmount})
            remaining -= matchAmount
        if remaining <= 0: break
    
    // 2단계: 파트너 서비스 계좌로 잔여 충당 (3명 미만이면)
    if remaining > 0 AND len(matches) < 3:
        serviceAccount = getPartnerServiceAccount(partnerId, networkId)
        if serviceAccount AND lockMaster(partnerId, remaining):
            matches.add({serviceAccount, remaining})
            remaining = 0
    
    // 3단계: 커버리지 판정
    matched = depositAmount - remaining
    if matched < depositAmount * 0.7:
        // 70% 미만 → P2P 안 함, 전부 unlock
        unlockAll(matches)
        return null  // 라우팅에서 TORQ로 전환
    
    return matches  // P2P 진행 (잔여 있으면 TORQ)

function routeDeposit(depositAmount, networkId, partnerId, eKycData):
    matches = tryP2pMatch(depositAmount, networkId, partnerId)
    if matches:
        remaining = depositAmount - sum(matches.amounts)
        if remaining > 0:
            torqTrade = torqService.createTrade(remaining, eKycData)
        return "P2P" + matches (+ torqTrade if remaining)
    else:
        torqTrade = torqService.createTrade(depositAmount, eKycData)
        return "TORQ" + torqTrade
```

### 5.7 이체 — 부분 완료 허용

입금자에게 최대 3건의 계좌 정보 전달. 전부 이체하지 않아도 진행 가능.

```
매칭 3건: 40만 + 35만 + 20만

이체 진행:
  ✓ 40만원 스크래핑 확인
  ✓ 35만원 스크래핑 확인
  ✗ 20만원 미이체

→ 입금자 선택:
  "75만원 확인됨 → XX USDT 수령 가능"
  [이대로 진행] → 75만원 정산, 20만원 매칭 취소
  [이체 계속]   → 20만원 대기 유지
```

### 5.8 정산 분기

```
from_partner ≠ to_partner → ONCHAIN (Relayer.transferFrom, 가스비 발생)
from_partner = to_partner → INNER (DB 원장만, 가스비 0, 즉시 완료)
```

### 5.9 동시성 제어

- 매칭 시 출금 주문 `SELECT ... FOR UPDATE`
- 동일 계좌 활성 출금 주문 중복 불가 (bank_accounts UNIQUE)
- 매칭 트랜잭션은 단일 DB 트랜잭션 내에서 완료
- 수동 매칭 링크: 생성 시 freeze, 만료 시 unfreeze

### 5.10 출금자 제약

- 출금 주문 만료: **48시간**
- 동일 계좌로 활성 출금 주문 중복 불가
- 스크래핑 빠른조회: 출금 등록 시 자동 신청 (계좌 단위)

---

## 6. 은행 API 자동 확인 — 스크래핑 API

### 6.1 확인 방식

**스크래핑 API 빠른 조회 서비스** 사용 (TORQ에서 이미 사용 중인 동일 서비스).

```
매칭 후 → B고객에게 계좌 정보 전달
       → 스크래핑 API 빠른 조회 폴링 시작 (A고객 계좌 입금 모니터링)
       → 입금 감지 시:
           - 금액 일치 확인
           - 매칭 상태 BANK_CONFIRMED 전이
           - 정산 프로세스 시작
```

### 6.2 확인 기준

| 항목 | 기준 |
|------|------|
| 금액 | 매칭 금액과 정확히 일치 (원 단위) |
| 계좌 | A고객이 입력한 계좌로 입금 |
| 시간 | 매칭 후 N분 이내 |

### 6.3 분쟁 처리

TORQ와 동일한 분쟁 프로세스 적용:
- B고객이 이체했다고 주장하나 스크래핑 API에서 미확인 → 분쟁 상태 전이
- 관리자가 수동 확인 후 처리 (확인/취소)
- 분쟁 기간 중 해당 매칭의 금액은 양쪽 모두 잠김 상태 유지

---

## 7. 정산 (Settlement)

### 7.1 정산 트리거

출금 주문의 모든 매칭이 `BANK_CONFIRMED`가 되면 정산 시작.

```
if 출금주문.confirmed_amount == 출금주문.krw_amount:
    정산 실행
```

### 7.2 정산 방식 — P2P_DEPOSIT → MASTER (온체인)

매칭의 은행 이체가 확인(BANK_CONFIRMED)되면 **매 거래마다 온체인 전송**으로 정산:

```
A사이트 P2P_DEPOSIT 지갑 ──→ B사이트 MASTER 지갑 (온체인 USDT 전송)
                                │
                                ▼
                         B사이트: 일반 사용자 입금(deposit)으로 처리
                         deposits 테이블에 기록, B고객 잔액 반영

A사이트 디파짓 잔액 변동:
  locked_balance -= usdt_amount   (잠금 해제)
  available_balance -= usdt_amount (차감 — 매칭 시 이미 차감됨)
  settled_balance += usdt_amount   (정산 누적)
```

**Relayer 활용**: 기존 Approve+Relayer 패턴 동일 적용.
P2P_DEPOSIT 지갑도 최초 `approve(relayer, MAX_UINT)` 실행 후,
`Relayer.transferFrom(A_P2P_DEPOSIT → B_MASTER)` 방식으로 정산.

### 7.3 수수료

```
환율: 거래 시작(주문 생성) 시점 확정
정산 USDT = krw_amount / exchange_rate (주문 생성 시 확정)

출금 측 수수료 = 총판이 책정한 출금 수수료율 적용
입금 측 수수료 = 2% (TORQ 동일)

A사이트에서 차감되는 USDT = 정산 USDT (수수료는 별도 정산)
B사이트 MASTER에 입금되는 USDT = 정산 USDT
```

---

## 8. 회원 관리 + 스크래핑 자격증명

### 8.0 데이터 모델 (신규 5개 테이블)

**P2P 회원:**
```sql
p2p_members
  - id, partner_id, partner_user_id
  - member_token (고정 링크 토큰, 불변)
  - pin_hash, pin_set_at (페이지 접근 인증)
  - status (ACTIVE/SUSPENDED — 접근 차단 가능)
  - 계좌는 bank_accounts 테이블에서 관리 (owner_type=MEMBER)
```

**스크래핑 연동 (4개 테이블 — cryptoments DB 직접 관리):**
```
banks                      — 은행 카탈로그 (17개 시드, 패턴별 자격증명 스키마)
bank_maintenance_windows   — 은행 점검 시간 (verify 전 사전 차단)
credentials                — 스크래핑 자격증명 (AES-256-GCM 암호화 + 동의 라이프사이클)
call_logs                  — 스크래핑 호출 기록 (디버깅/SLO 추적, 6개월 보존)
```

상세 스키마: `v2-docs/SCRAPING_DATA_MODEL.md` 참조.

### 8.1 출금자 회원 페이지 — 계좌 등록 플로우

파트너가 회원별 고정 링크를 발급. 회원이 페이지에서 직접:

```
1. 페이지 접속 → 핀코드 설정/입력 (인증)
2. 은행 선택 → banks 테이블에서 패턴 조회
3. 패턴별 입력 폼:
   - A_TYPE0 (14개 은행): 계좌번호 + 예금주 + 계좌비밀번호(4자리) + 주민번호앞6자리
   - B_NO_IDENTITY (iM뱅크): 계좌번호 + 예금주 + 계좌비밀번호(4자리)
   - D_FAST_PLUS_ACCT (KB/신한): 계좌번호 + 예금주 + 계좌비밀번호 + 빠른조회 ID/PW
4. 빠른조회 사전등록 확인 체크 (은행 앱에서 미리 등록 필요)
5. credentials INSERT (AES 암호화) → activateConsent → ACTIVE
6. p2p_members 업데이트 (계좌 + scraping_credential_id)
7. → 출금 주문 등록 가능 상태
```

### 8.2 회원 관리 API + 콘솔

**Partner API:**
```
POST   /api/partner/p2p/members              — 회원 등록 (→ 고정 링크 발급)
GET    /api/partner/p2p/members              — 회원 목록
GET    /api/partner/p2p/members/{token}      — 회원 상세
POST   /api/partner/p2p/members/{token}/reset-pin — 핀코드 리셋
DELETE /api/partner/p2p/members/{token}      — 회원 비활성화
```

**파트너 콘솔:**
- 회원 목록 (token, 이름, 계좌 등록 여부, 활성 출금 건수)
- 회원 등록 폼 → 링크 자동 생성/복사
- 회원 상세 (출금 내역, 스크래핑 상태, 핀코드 리셋)

**회원 페이지 URL:**
```
https://p2p.cryptoments.cc/m/{memberToken}
```
memberToken은 발급 후 고정 — 파트너가 회원에게 준 링크가 영구 유효.

### 8.3 파트너 API (partner-api) — 출금/입금

```
# 출금 주문 생성 (A사이트 서버 → cryptoments)
POST /api/partner/p2p/withdraw-orders
{
    "partnerUserId": "user123",
    "networkId": 3,
    "krwAmount": 500000
}
→ 회원의 등록 계좌 + 스크래핑 자격증명 자동 사용
→ { "orderCode": "pwo_abc123", "status": "PENDING", "expiresAt": "..." }

# 입금 주문 생성 (B사이트 서버 → cryptoments)
POST /api/v1/p2p/deposit-orders
{
    "partnerUserId": "user456",
    "krwAmount": 300000
}
→ { "orderCode": "pdo_def456", "status": "PENDING", "expiresAt": "..." }

# 주문 상태 조회
GET /api/v1/p2p/withdraw-orders/{orderCode}
GET /api/v1/p2p/deposit-orders/{orderCode}

# 매칭 상태 조회 (입금 고객용 — 계좌 정보 포함)
GET /api/v1/p2p/deposit-orders/{orderCode}/matches
→ [{ "matchCode": "pm_...", "krwAmount": 300000,
      "bankName": "KB국민은행", "accountNumber": "123-456-789",
      "accountHolder": "홍길동", "status": "BANK_PENDING", "expiresAt": "..." }]

# 주문 취소
POST /api/v1/p2p/withdraw-orders/{orderCode}/cancel
POST /api/v1/p2p/deposit-orders/{orderCode}/cancel
```

### 8.2 위젯 API (open-api) — 선택적

Widget UI를 통해 직접 입금/출금을 처리하는 경우.

```
# 위젯에서 입금 주문 매칭 정보 폴링
GET /widgets/api/p2p/deposit-orders/{orderCode}/status
→ { "status": "MATCHED", "matches": [...], "totalAmount": 300000 }
```

### 8.3 Webhook 알림

파트너에게 상태 변경 알림:

| 이벤트 | 대상 | 페이로드 |
|--------|------|---------|
| `p2p.withdraw.matched` | A파트너 | 매칭 완료 (부분/전체) |
| `p2p.withdraw.confirmed` | A파트너 | 입금 확인됨 |
| `p2p.withdraw.completed` | A파트너 | 정산 완료 |
| `p2p.deposit.matched` | B파트너 | 매칭 완료 + 계좌 정보 |
| `p2p.deposit.completed` | B파트너 | 이체 확인 + 정산 완료 |
| `p2p.order.expired` | 해당 파트너 | 주문 만료 |
| `p2p.order.cancelled` | 해당 파트너 | 주문 취소 |

---

## 8.5 파트너 콘솔 기능

### 사이드바

```
P2P 관리 (신규 섹션)
  회원 관리
  입금 링크
  출금 주문
  거래 내역
```

기존 메뉴 수정: 출금 내역(P2P 전환 추가), 비즈니스 설정(P2P 설정 추가), 대시보드(P2P 요약)

### 8.5.1 P2P 회원 관리

회원 목록/등록/상세. P2P 서비스 이용을 위한 회원 관리.

- 회원 등록: partner_user_id 입력 → 고정 링크 자동 발급
- 회원 상세: 링크 복사, 계좌 등록 상태, 스크래핑 상태, 핀코드 리셋, 접근 차단/해제
- 회원 페이지(출금자 전용)는 별도 공간 — 모바일핏

### 8.5.2 출금 → P2P 전환

출금 승인 시 "직접 출금" 또는 "P2P 전환" 선택.

**P2P 전환 시:**
- 계좌/스크래핑 미등록이어도 즉시 전환 가능
- p2p_member 없으면 자동 생성 + 링크 발급
- 이미 있으면 기존 링크 표시
- 사용자가 링크 접속 → 핀코드 설정 → 계좌 등록 → 스크래핑 → 매칭 풀 진입

**P2P 출금 주문이 쌓이는 루트 2가지:**

```
루트 A: 기존 출금 → P2P 전환
  withdrawal(REQUESTED) → 파트너 승인 시 P2P 선택
  → withdrawal 상태 P2P_PENDING + p2p_withdraw_order 생성

루트 B: 파트너 콘솔에서 직접 생성 + P2P 동시 전환
  출금 요청 생성 시 "P2P 전환" 선택
  → withdrawal(P2P_PENDING) + p2p_withdraw_order 동시 생성
  → to_address 없이 생성 가능 (P2P는 KRW 수령이므로)
```

### 8.5.3 잔여분 처리

P2P 매칭 후 잔여 금액에 대해 파트너가 선택:

```
A) 출금 전환: unfreeze → 새 withdrawal(REQUESTED) → 일반 출금 플로우
B) 강제 정산: unfreeze → USDT 파트너 잔액 복원 → 기록만 (이미 지급 완료)
C) 취소:     unfreeze → USDT 파트너 잔액 복원 → 환불 기록
```

**USDT 직접 출금 전환 (경로 2가지):**

```
경로 A: 파트너가 직접 전환
  사용자에게 주소를 전달받아 콘솔에서 입력 → 즉시 전환

경로 B: 사용자 요청 → 파트너 승인
  사용자가 P2P 페이지에서 주소 입력 + 전환 요청
  → 파트너 콘솔에 요청 알림
  → 파트너 승인 → 전환
```

### 8.5.4 P2P 입금 링크 관리

AUTO/MANUAL 모드 링크 생성/관리.

- AUTO: 금액 + 만료 설정 → 입금자 접속 시 자동 매칭
- MANUAL: 금액 + 대기 중 출금자 선택 → 사전 매칭 + freeze → 입금자 접속 시 바로 이체 화면
- 만료: 5분 / 10분 / 30분 / 1시간

### 8.5.5 비즈니스 설정 — P2P

- P2P 서비스 ON/OFF
- P2P 수수료율 (p2p_fee_rate, 구매자 부담)
- 파트너 서비스 계좌 등록/수정/삭제 (파트너당 1개)

---

## 8.6 출금자 P2P 페이지 (모바일핏)

별도 공간. URL: `https://p2p.cryptoments.cc/m/{memberToken}`

### 페이지 기능

| 기능 | 설명 |
|------|------|
| 인증 | 핀코드 입력 (최초: 설정, 이후: 입력) |
| 메인 | 출금 대기 금액, 계좌/스크래핑 상태, 최근 거래 |
| 계좌 등록 | 은행 선택 → 패턴별 입력 → 스크래핑 인증 |
| 빠른조회 재인증 | 만료 시 재인증 |
| 거래 내역 | 진행 중/완료/취소 목록 |
| 거래 상세 | 매칭 내역, 정산 상태 |
| 거래 확인증 | 완료 거래 확인증 (저장/인쇄) |
| 분쟁 확인 | 입금 분쟁 상태 확인 |
| USDT 전환 요청 | 잔여분 USDT 출금 전환 (주소 입력 → 파트너 승인 대기) |

### 페이지에서 할 수 없는 것

- 출금 금액 직접 설정 (파트너가 결정)
- 출금 요청 직접 생성 (파트너가 전환)
- 계좌 변경 (진행 중 건 없을 때만, 삭제 후 재등록)

---

## 8.7 Admin 콘솔 P2P 관리

시스템 운영자(cryptoments 관리자)가 전체 파트너의 P2P를 모니터링하고 분쟁을 처리.

```
Admin 사이드바:
  P2P 관리 (신규)
    ├── 대시보드 — 오늘 매칭/정산/분쟁 요약, 파트너별 TOP, 출금 풀 현황
    ├── 매칭 현황 — 전체 매칭 목록/상세, 상태별/파트너별 필터
    ├── 분쟁 관리 — 대기 중 분쟁, 증빙 확인, 판정(확인/취소)
    ├── 정산 현황 — ONCHAIN/INNER, 실패 건 수동 재시도/취소
    └── 회원 조회 — 전체 파트너 회원, 스크래핑 상태, 계좌 확인
```

**분쟁 판정 (핵심 기능):**
- 입금자 증빙(이체확인증) + 스크래핑 조회 이력 확인
- [이체 확인 → 정산 진행] 또는 [이체 미확인 → 매칭 취소]
- 판정 사유 + Admin ID 기록

**정산 실패 처리:**
- 실패 사유 확인 (Relayer timeout, nonce conflict 등)
- [수동 재시도] 또는 [취소 (unlock + 환불)]

---

## 9. 모듈 구조

```
core/src/main/java/com/cryptoments/core/p2p/
├── P2pMatchingService.java       — 매칭 엔진 (1:1, P2P 우선 → TORQ fallback)
├── P2pLockService.java           — MASTER 지갑 P2P 잠금 관리
├── P2pWithdrawService.java       — 출금 주문 관리
├── P2pDepositService.java        — 입금 주문 관리
├── P2pSettlementService.java     — 정산 (MASTER→MASTER 온체인)
├── P2pMemberService.java         — 회원 관리 (링크, 핀코드, 계좌)
└── P2pScrapingService.java          — 스크래핑 자격증명 등록/조회/입금확인

partner-api/src/main/java/.../controller/
├── P2pController.java            — 출금/입금 주문 API (6 EP)
└── P2pMemberController.java      — 회원 관리 API (5 EP)

open-api/src/main/java/.../controller/widget/
└── P2pWithdrawWidgetController.java — 출금자 회원 페이지 API

common/src/main/java/.../entity/
├── P2pPartnerLock.java, P2pLockHistory.java
├── P2pWithdrawOrder.java, P2pDepositOrder.java
├── P2pMatch.java, P2pSettlement.java
├── P2pMember.java
├── Bank.java, BankMaintenanceWindow.java
├── ScrapingCredential.java, ScrapingCallLog.java

scheduler/
├── P2pMatchExpiryJob.java        — 매칭 이체 기한 만료 (20분)
├── P2pScrapingVerifyJob.java        — 스크래핑 입금 확인 폴링 (5초)
├── P2pDepositLinkExpiryJob.java  — P2P 입금 링크 만료
└── ScrapingConsentExpiryJob.java    — 스크래핑 동의 만료 추적
```

---

## 10. 분쟁 처리

### 10.1 분쟁 발생 조건

입금자가 "송금 완료" 클릭 → 스크래핑 확인 시작 (5초 폴링) → **3분 경과 미확인** → 분쟁 제기 가능.

### 10.2 분쟁 해소 경로 2가지

```
경로 1: 출금자가 직접 확인 (대부분 여기서 해결)
  출금자 P2P 페이지 → "입금 확인" 버튼
  → match.status = BANK_CONFIRMED → 정산 진행
  → Admin 개입 불필요

경로 2: 입금자가 분쟁 제기 (출금자가 응답 안 할 때)
  입금자 Widget → 이체 확인증 업로드 + 사유
  → match.status = DISPUTED
  → Admin이 증빙 확인 후 판정:
    → CONFIRMED: 이체 확인 → 정산 진행
    → CANCELLED: 이체 미확인 → 매칭 취소 + unlock
```

### 10.3 분쟁 시 전체 펜딩

3건 매칭 중 1건이라도 분쟁이면 **전체 정산 보류**.

```
① 400,000원 ✓ 확인
② 350,000원 ✓ 확인
③ 200,000원 ⚠ 분쟁

→ ①②도 정산 보류 (전체 펜딩)
→ ③ 해결 후:
    CONFIRMED → 전체(①②③) 정산 진행
    CANCELLED → ①② 정산 + ③ 취소(unlock)
```

### 10.4 분쟁 데이터 (p2p_matches 추가 필드)

```
dispute_evidence_url VARCHAR(500)  — 이체 확인증 파일 URL
dispute_submitted_by VARCHAR(20)   — DEPOSITOR / WITHDRAWER
resolved_by BIGINT                 — 판정 Admin ID
resolve_memo VARCHAR(500)          — 판정 사유
```

---

## 10.5 만료 처리

### 출금 주문 — 만료 없음

파트너 또는 사용자가 직접 취소만 가능. 자동 만료 없음.

### 매칭 이체 기한 — 20분

입금자가 "매칭 시작" 후 20분 내 이체 완료해야 함 (건당 5분 × 3건 + 여유 5분).

```
match.expires_at 경과 시:
  → 미이체 건: match.status = EXPIRED, unlock
  → 출금 주문: matched_amount 차감 → 매칭 풀 복귀
  → 입금자: "이체 기한 초과" 안내
  → 이미 이체 확인된 건은 유지
```

### P2P 입금 링크 — 5/10/30/60분

파트너가 설정한 만료 시간. 만료 시 MANUAL 링크의 사전 매칭 건도 취소 + unlock.

### 스크래핑 동의 — 은행별 (30~365일)

만료 임박 시 D-7/D-3/D-1 알림. 만료 시 출금 주문 생성 불가 (계좌 재인증 필요).

---

## 10.6 리스크

| 리스크 | 대응 |
|--------|------|
| 입금자 미이체 | 매칭 20분 만료 → 출금 주문 복귀 |
| 금액 불일치 이체 | 스크래핑 불일치 → 분쟁 → Admin 판정 |
| 출금자 계좌 오류 | P2P 페이지에서 계좌 변경 (진행 건 없을 때) |
| 동시 매칭 경합 | FOR UPDATE 비관적 잠금 |
| 동일 계좌 중복 | bank_accounts UNIQUE(bank_code, account_number) |
| 온체인 정산 실패 | 재시도 큐 + 관리자 알림 |
| 스크래핑 동의 만료 | D-7/D-3/D-1 알림 + 만료 시 출금 차단 |

### 10.7 한도

| 항목 | 값 | 설정 |
|------|---|------|
| 최소 주문 금액 | 10,000 KRW | 코드 상수 |
| 최대 주문 금액 | 10,000,000 KRW | 파트너별 |
| 매칭 이체 기한 | 20분 | 시스템 상수 |
| 분쟁 제기 가능 | 송금 완료 후 3분 | 시스템 상수 |
| 스크래핑 폴링 주기 | 5초 | 시스템 상수 |
| P2P 링크 만료 | 5/10/30/60분 | 파트너 선택 |
| 스크래핑 동의 기간 | 30~365일 | 은행별 |

---

## 10.8 스케줄러

| Job | 주기 | 역할 |
|-----|------|------|
| P2pMatchExpiryJob | 30초 | 매칭 이체 기한 20분 만료 (EXPIRED 처리 + unlock) |
| P2pScrapingVerifyJob | 5초 | 송금 완료 건 스크래핑 입금 확인 폴링 |
| P2pDepositLinkExpiryJob | 30초 | P2P 입금 링크 만료 + MANUAL 사전매칭 취소 |
| ScrapingConsentExpiryJob | 매일 09:00 | 스크래핑 동의 만료 알림 (D-7/D-3/D-1) + 만료 처리 |

---

## 10.9 Widget 매칭 라우팅

```
POST /widgets/api/p2p/route — 라우팅 판정 (매칭 생성 안 함, 가용 여부만)
  요청: { krwAmount, networkId }
  응답: { route: "P2P" | "TORQ", coverage: 0.95 }

P2P 전용 엔드포인트 (P2pWidgetController):
  POST /widgets/api/p2p/match — 매칭 시작 (실제 매칭 + freeze)
  GET  /widgets/api/p2p/session/{orderCode} — 세션 상태 조회 (폴링)
  POST /widgets/api/p2p/match/{code}/transfer-done/{matchId} — 건별 송금 완료
  POST /widgets/api/p2p/match/{code}/confirm — 이대로 진행
  POST /widgets/api/p2p/match/{code}/cancel — 전체 취소
  POST /widgets/api/p2p/match/{code}/dispute/{matchId} — 분쟁 제기

P2P 링크 (P2pDepositLinkController):
  GET /widgets/p2p/links/{linkCode} — 링크 정보 + 토큰 발급 (Public)
```

P2P와 TORQ는 완전 독립. 연결점은 라우팅 API 한 곳뿐. 데이터 참조 관계 없음.

---

## 11. 구현 우선순위

### Phase 1: DDL + 기반 (✅ 부분 완료)

```
✅ 완료:
  - P2P 6개 테이블 (운영 DB 적용)
  - Entity/Repository/Enum/Mapper
  - Core Services 5개
  - Partner API P2pController 6 EP
  - p2p_fee_rate + 수수료 체계

⬜ 미완료 (신규 DDL):
  - 기존 테이블 수정 (p2p_withdraw_orders, p2p_matches, p2p_settlements, withdrawals)
  - 신규 7개 테이블 (banks, bank_maintenance_windows, credentials, call_logs, bank_accounts, p2p_members, p2p_deposit_links)
  - 은행 17개 시드 데이터
  - 신규 Entity/Repository 생성
```

### Phase 2: 출금 플로우 (파트너 → P2P 전환)

```
  - WithdrawalService P2P 전환 (P2P_PENDING 상태)
  - P2pWithdrawService 수정 (withdrawal_id, bank_account_id)
  - 부분 debit (정산 건별 unfreeze + debit)
  - 잔여분 3옵션 (출금 전환 / 강제 정산 / 취소)
  - USDT 전환 요청 (사용자 요청 → 파트너 승인)
  - Partner API: P2P 전환 + 잔여분 처리 EP
  - 파트너 콘솔: 출금 P2P 전환 UI
```

### Phase 3: 회원 관리 + 스크래핑 + 출금자 페이지

```
  - P2pMemberService (회원 CRUD + 고정 링크 + 핀코드)
  - P2pScrapingService (자격증명 등록/활성화/조회)
  - BankAccountService (공통 계좌 + 중복 검증)
  - Partner API: P2pMemberController 5 EP
  - 파트너 콘솔: P2P 회원 관리
  - 출금자 P2P 페이지 (별도 — 모바일핏)
    핀코드 → 계좌 등록 → 스크래핑 인증 → 현황 → 분쟁 → 확인증
  - open-api: P2pWithdrawPageController
```

### Phase 4: 매칭 엔진 고도화 (1:3 + 라우팅)

```
  - P2pMatchingService 재작성 (1:3, 큰 금액 우선, 70% 커버리지)
  - 파트너 서비스 계좌 매칭 풀
  - InnerTx (동일 파트너 DB 원장만)
  - Widget 라우팅 Controller (POST /widgets/api/p2p/route)
  - P2pWidgetController (매칭/송금완료/확인/취소/분쟁 6 EP)
  - p2p.vue 신규 (입금자 Widget)
  - torq.vue 수정 (라우팅 분기)
```

### Phase 5: P2P 입금 링크 + 스크래핑 확인

```
  - P2pDepositLinkService (AUTO/MANUAL)
  - MANUAL: 사전 매칭 + freeze
  - P2pDepositLinkController (링크 조회 + 토큰)
  - 파트너 콘솔: P2P 입금 링크 관리
  - P2pScrapingVerifyJob (5초 폴링)
  - P2pMatchExpiryJob (30초, 이체 20분 만료)
  - P2pDepositLinkExpiryJob (30초, 링크 만료)
  - ScrapingConsentExpiryJob (매일, 동의 만료)
```

### Phase 6: 정산 + Admin 콘솔

```
  - 온체인 정산 (RelayerApiClient.transferFrom + Webhook)
  - completeSettlement 연동 (debit + FEE + 총판 쉐어)
  - Admin 콘솔 P2P: 대시보드, 매칭, 분쟁 관리, 정산, 회원 조회
  - 분쟁 판정 (Admin)
  - 정산 실패 재시도/취소
```

### 의존 관계

```
Phase 1 → Phase 2, 3 (DDL 기반)
Phase 2 + 3 → Phase 4 (출금 + 회원 → 매칭)
Phase 4 → Phase 5 (매칭 → 링크 + 확인)
Phase 4 + 5 → Phase 6 (매칭 + 확인 → 정산 + Admin)
```

---

## 12. 결정 사항 (v0.6)

| 항목 | 결정 | 비고 |
|------|------|------|
| 매칭 방식 | **1:3 단방향** | 입금자 1명 : 출금자 최대 3명. 입금자가 트리거 |
| 매칭 풀 | **개인 출금자 + 파트너 서비스 계좌** | 서비스 계좌는 MASTER 잔액이 한도, 파트너당 1개 |
| 매칭 알고리즘 | **큰 금액 우선 → 정확 매칭** | 커버리지 ≥ 70% → P2P, < 70% → 전액 TORQ |
| 매칭 라우팅 | **Widget Controller에서 분기** | fallback은 라우팅에서만, 진입 후 독립 |
| 은행 API | **스크래핑 API 빠른 조회** | 4개 테이블 cryptoments DB 직접 관리 |
| 환율 기준 | **출금 주문 생성 시점 확정** | KRW/USDT 입력 기준 통화가 정확값 |
| 수수료 | **구매자: p2p_fee_rate / 파트너: deposit_fee_rate** | 총판 쉐어(aggregateDailyFees) |
| 출금 수수료 | **없음** | — |
| 분쟁 | **출금자 직접 확인 또는 Admin 판정** | 출금자 "입금 확인" 버튼 / 입금자 증빙 → Admin |
| 분쟁 시 | **전체 펜딩** | 1건이라도 분쟁이면 전체 정산 보류 |
| MASTER 지갑 | **통합 (별도 지갑 없음)** | 기존 approve 활용 |
| 정산 | **ONCHAIN(크로스) / INNER(동일파트너)** | 동일 파트너 = 가스비 0 |
| 출금 만료 | **없음** | 파트너/사용자 직접 취소만 |
| 이체 기한 | **20분** | 건당 5분 × 3건 + 여유 5분 |
| 분쟁 제기 | **송금 완료 후 3분** | 스크래핑 미확인 시 |
| 스크래핑 폴링 | **5초** | 송금 완료 클릭 후 해당 건 |
| 동일 계좌 | **중복 등록 불가** | bank_accounts UNIQUE |
| 계좌 관리 | **bank_accounts 공통 테이블** | MEMBER/PARTNER 통합 |
| 회원 관리 | **p2p_members** | 고정 링크 + 핀코드 + 접근 차단 |
| P2P 전환 | **즉시 전환** | 계좌/스크래핑 미등록이어도 OK, 링크 자동 발급 |
| P2P 링크 | **p2p_deposit_links** | AUTO/MANUAL, 만료 5/10/30/60분 |
| USDT 전환 | **파트너 직접 또는 사용자 요청→파트너 승인** | — |
