# Cryptoments Partner Console — 프론트엔드 API 연동 가이드

> **버전**: v1.0
> **작성일**: 2026-03-06
> **대상**: 프론트엔드 개발팀
> **기반**: Partner Console API 명세서 v1.0, OpenAPI 생성 spec (restMetaGenerator), 화면설계서 v1.2
> **openapi.json 원본**: `partner-api/build/docs/openapi.json` (restMetaGenerator로 생성)

---

## 목차

1. [서비스 기본 정보](#1-서비스-기본-정보)
2. [핵심 주의사항 — 명세서 vs 실제 구현 차이](#2-핵심-주의사항--명세서-vs-실제-구현-차이)
3. [구현 상태 요약](#3-구현-상태-요약)
4. [화면별 API 매핑 테이블](#4-화면별-api-매핑-테이블)
5. [인증 (Auth)](#5-인증-auth)
6. [대시보드 (Dashboard)](#6-대시보드-dashboard)
7. [입금 관리 (Deposit)](#7-입금-관리-deposit)
8. [출금 관리 (Withdrawal)](#8-출금-관리-withdrawal)
9. [잔액/원장 (Balance/Ledger)](#9-잔액원장-balanceledger)
10. [정산 (Settlement)](#10-정산-settlement)
11. [가스비 (Gas)](#11-가스비-gas)
12. [하위 파트너 관리 (Sub-Partners) — DISTRIBUTOR 전용](#12-하위-파트너-관리-sub-partners--distributor-전용)
13. [총판 리포트 (Reports) — DISTRIBUTOR 전용](#13-총판-리포트-reports--distributor-전용)
14. [연동 설정 (Integration)](#14-연동-설정-integration)
15. [내 계정 (Account)](#15-내-계정-account)
16. [사용자 컨텍스트 (User Context)](#16-사용자-컨텍스트-user-context)
17. [Webhook 이벤트 정의](#17-webhook-이벤트-정의)
18. [에러 코드 전체 목록](#18-에러-코드-전체-목록)

---

## 1. 서비스 기본 정보

### 1.1 Base URL

```
개발 서버: http://localhost:8081
스테이징:  https://api-staging.cryptoments.cc
프로덕션:  https://api.cryptoments.cc
```

> **주의**: API 명세서에는 Base URL을 `https://api.cryptoments.cc/partner/v2`로 정의했지만,
> 실제 컨트롤러 경로는 `/api/partner/` prefix를 사용합니다.
> **프론트엔드에서 사용할 실제 경로는 본 문서 기준으로 `/api/partner/`입니다.**

### 1.2 인증

모든 인증이 필요한 API에 아래 헤더를 포함해야 합니다:

```http
Authorization: Bearer {access_token}
Content-Type: application/json
Accept-Language: ko
```

- **Access Token**: 세션 토큰 (Axim REST Framework HMAC-SHA256 기반, 15분 유효)
- **갱신**: Access Token 만료 시 `POST /api/partner/auth/token/refresh` 호출
- 로그인 응답의 `token` 필드를 Authorization 헤더에 사용

### 1.3 페이지네이션

```
GET /api/partner/deposits?page=1&size=20&sort=id&order=desc
```

| 파라미터 | 기본값 | 설명 |
|---------|--------|------|
| page | 1 | 페이지 번호 (1부터 시작) |
| size | 20 | 페이지당 건수 (최대 100) |
| sort | id | 정렬 기준 컬럼 |
| order | desc | asc / desc |

> openapi.json에는 `pagination` 파라미터가 `required: true`로 표시되지만, 이는 프레임워크 아티팩트입니다.
> 실제 API 호출 시 `page`, `size`, `sort`, `order`를 각각 개별 파라미터로 전송하세요.

### 1.4 공통 응답 구조

**성공 (단건)**:
```json
{
  "data": { ... }
}
```

**성공 (목록/페이지네이션)**:
```json
{
  "data": {
    "content": [ ... ],
    "page": 1,
    "size": 20,
    "totalCount": 158,
    "totalPages": 8
  }
}
```

> `XPage<T>` 응답은 위 구조로 래핑됩니다. `data.content[]`에서 목록을 추출하세요.

**실패**:
```json
{
  "code": "INVALID_PARAMETER",
  "message": "금액은 0보다 커야 합니다"
}
```

### 1.5 데이터 타입 규칙

| 타입 | 형식 | 예시 |
|------|------|------|
| 금액 | `number` (BigDecimal) | `99.000000` |
| 날짜/시간 | ISO 8601 문자열 | `"2026-03-05T10:30:00+09:00"` |
| ID | `integer (int64)` | `1`, `100` |
| Enum | 문자열 | `"CONFIRMED"`, `"USDT"` |

### 1.6 메뉴 접근 분기

| 메뉴 | MERCHANT | DISTRIBUTOR |
|------|:--------:|:-----------:|
| 대시보드 | O | O |
| 입금 관리 | O | O |
| 출금 관리 | O | O |
| 잔액/원장 | O | O |
| 정산 | O | O |
| 가스비 | O | O |
| **하위 파트너 관리** | X | O |
| **총판 리포트** | X | O |
| 연동 설정 | O | O |
| 내 계정 | O | O |

---

## 2. 핵심 주의사항 — 명세서 vs 실제 구현 차이

> 아래 내용은 API 명세서(CRYPTOMENTS_PARTNER_API_SPEC.md)와 실제 생성된 OpenAPI(openapi.json)의 차이점입니다.
> **프론트엔드 구현 시 본 문서의 내용을 우선 기준으로 사용하세요.**

### [차이 1] 쿼리 파라미터 — snake_case → camelCase

명세서는 snake_case로 표기했지만 실제 API는 **camelCase**를 사용합니다.

| 명세서 (X — 사용 불가) | 실제 API (O — 사용 필요) |
|----------------------|------------------------|
| `currency_id` | `currencyId` |
| `network_id` | `networkId` |
| `partner_user_id` | `partnerUserId` |
| `has_payment_link` | `hasPaymentLink` |
| `deposit_method` | `depositMethod` |
| `withdrawal_type` | `withdrawalType` |
| `is_active` | `isActive` |
| `include_sub_partners` | `includeSubPartners` |
| `entry_type` | `entryType` |
| `partner_type` | `partnerType` |
| `deposit_type` | `depositType` |

### [차이 2] 요청 바디 필드 — snake_case → camelCase

요청 바디(Request Body)도 마찬가지로 **camelCase**를 사용합니다.

| 명세서 (X) | 실제 API (O) |
|------------|------------|
| `current_password` | `currentPassword` |
| `new_password` | `newPassword` |
| `new_password_confirm` | `newPasswordConfirm` |
| `partner_user_id` | `partnerUserId` |
| `currency_id` | `currencyId` |
| `network_id` | `networkId` |
| `deposit_method` | `depositMethod` |
| `amount_currency` | `amountCurrency` |
| `expires_in_minutes` | `expiresInMinutes` |
| `otp_code` | `otpCode` |
| `temp_token` | `tempToken` |
| `refresh_token` | `refreshToken` |
| `whitelist_id` | `whitelistId` |
| `to_address` | `toAddress` |
| `withdrawal_type` | `withdrawalType` |

### [차이 3] 응답 필드 — snake_case → camelCase

모든 응답 필드도 **camelCase**입니다.

```json
// 명세서 (X)
{ "deposit_code": "dep_2603_...", "tx_hash": "0x...", "created_at": "..." }

// 실제 API (O)
{ "depositCode": "dep_2603_...", "txHash": "0x...", "createdAt": "..." }
```

### [차이 4] URL 경로 — 명세서 vs 실제

| 기능 | 명세서 경로 | 실제 경로 |
|------|-----------|---------|
| 연동 설정 | `/settings/api-key` | `/api/partner/integration/api-key` |
| Webhook 설정 | `/settings/webhook` | `/api/partner/integration/webhook` |
| Telegram 설정 | `/settings/telegram` | `/api/partner/integration/telegram` |
| Axim 설정 | `/settings/axim` | `/api/partner/integration/axim` |
| 사용자 컨텍스트 | `/user-context/{id}/...` | `/api/partner/users/{partnerUserId}/...` |

### [차이 5] 대시보드 응답 구조 — 단순화됨

명세서의 풍부한 중첩 구조와 달리 현재 구현은 단순화된 flat 구조입니다.

**현재 구현 응답** (`GET /api/partner/dashboard/summary`):
```json
{
  "todayDepositCount": 15,
  "todayDepositAmount": "12500.000000",
  "todayWithdrawalCount": 3,
  "todayWithdrawalAmount": "5000.000000",
  "pendingWithdrawalCount": 2
}
```

> `today_deposits.by_currency[]`, `balances[]`, `pending_alerts`, `sub_partner_summary` 등은
> **미구현 상태**입니다. 별도 API (`GET /balances`)로 잔액을 조회하세요.

### [차이 6] 대시보드 차트 응답

명세서: `{ "series": [...] }` 래핑 객체 반환
실제: `List<DailyChartResponse>` 직접 배열 반환 (래핑 없음)

```json
// 현재 구현 (Array 직접 반환)
[
  { "date": "2026-03-05", "depositCount": 8, "depositAmount": "5200.000000" },
  { "date": "2026-03-04", "depositCount": 12, "depositAmount": "8100.000000" }
]
```

> 출금 데이터(`withdrawalAmount`, `withdrawalCount`)는 현재 미포함. 추후 추가 예정.

### [차이 7] 잔액 응답 구조 — 단순화됨

명세서: `available`, `frozen`, `unsettled_settlement`, `total` 포함
현재 구현: `currencyId`, `currencyCode`, `totalBalance` 3필드만 포함

```json
// 현재 구현
[
  { "currencyId": 1, "currencyCode": "USDT", "totalBalance": "48730.500000" }
]
```

> `available`, `frozen` 분리는 미구현. 추후 업데이트 예정.

### [차이 8] 2FA 설정 엔드포인트 차이

| 기능 | 명세서 | 실제 API |
|------|--------|---------|
| 2FA 등록 확인(활성화) | `POST /account/2fa/verify` | `POST /api/partner/account/2fa/enable` |
| 2FA 해제 | `POST /account/2fa/disable` | `DELETE /api/partner/account/2fa` |

### [차이 9] 프로필 수정 HTTP Method

- 명세서: `PATCH /account/profile`
- 실제: `PUT /api/partner/account/profile`

### [차이 10] 총판 리포트 — 수수료 엔드포인트 이름

- 명세서: `GET /reports/commission`
- 실제: `GET /api/partner/reports/fees`

---

## 3. 구현 상태 요약

### 완전 구현 (서비스 로직 포함)

| 기능 | 상태 |
|------|------|
| 로그인 / 2FA 인증 / 로그아웃 | O |
| 토큰 갱신 | O |
| 비밀번호 재설정 | O |
| 입금 내역 목록/상세 조회 | O |
| 입금 세션 목록/상세 조회 | O |
| 결제 링크 목록 조회 | O |
| 입금 주소 풀 목록/상세 조회 | O |
| 집금 현황 목록/상세 조회 | O |
| Axim Pay 결제 내역 조회 | O |
| 출금 내역 목록/상세 조회 | O |
| 화이트리스트 조회/추가/수정 | O |
| 정산 - 일별 수수료 조회 | O |
| 정산 - 실현 내역 조회 | O |
| 정산 - 잔액 현황 조회 | O |
| 가스비 기록 조회 | O |
| 가스비 인보이스 목록/상세 조회 | O |
| 연동 설정 (API키/Webhook/Telegram/Axim) | O |
| 내 계정 프로필 조회/수정 | O |
| 내 계정 비밀번호 변경 | O |
| 내 계정 2FA 설정 | O |
| 하위 파트너 목록/상세/등록/수정 | O |
| 하위 파트너 거래 현황 | O |
| 총판 리포트 (매출/수수료/성과) | O |
| 사용자 컨텍스트 입금/세션/출금/결제링크/Axim 조회 | 라우팅 O, 서비스 TODO |

### 미구현 (TODO — null 반환)

| 기능 | 엔드포인트 | 비고 |
|------|----------|------|
| 입금 세션 생성 | `POST /api/partner/deposit-sessions` | 주소 할당 로직 필요 |
| 결제 링크 생성 | `POST /api/partner/payment-links` | |
| Axim 결제 요청 | `POST /api/partner/axim-payments/request` | |
| 잔액 현황 | `GET /api/partner/balances` | 원장 집계 로직 |
| 원장 조회 | `GET /api/partner/ledger` | |
| 정산 쉐어 출금 요청 | `POST /api/partner/settlement/withdraw` | 잔액 검증 + 출금 생성 |
| 수동 출금 요청 | `POST /api/partner/withdrawals` | 잔액 검증 + 주소 검증 |
| 사용자 컨텍스트 (실 서비스) | `GET /api/partner/users/{id}/*` | 서비스 로직 |
| 로그인 이력 조회 | `GET /api/partner/account/login-history` | 테이블 미확정 |
| 대시보드 요약 (풍부한 구조) | `GET /api/partner/dashboard/summary` | 현재 단순화 버전만 |

### 명세서에 있으나 현재 미구현 엔드포인트

| 기능 | 명세서 경로 | 비고 |
|------|-----------|------|
| Webhook 재시도 | `POST /settings/webhook/retry/{id}` | 미생성 |
| Telegram 연결 해제 | `DELETE /settings/telegram` | 미생성 |
| Telegram 구독 설정 | `PUT /settings/telegram/subscriptions` | 미생성 |
| 사용자 컨텍스트 요약 | `GET /user-context/{id}/summary` | 의도적으로 제거됨 |

---

## 4. 화면별 API 매핑 테이블

| 화면코드 | 화면명 | 경로 | API 엔드포인트 | 구현상태 |
|---------|--------|------|--------------|---------|
| PCR-0100 | 로그인 | `/partner/login` | `POST /api/partner/auth/login` | O |
| PCR-0110 | 2FA 인증 | `/partner/login/2fa` | `POST /api/partner/auth/2fa/verify` | O |
| PCR-0120 | 비밀번호 재설정 요청 | `/partner/forgot-password` | `POST /api/partner/auth/password/forgot` | O |
| PCR-0130 | 새 비밀번호 설정 | `/partner/reset-password` | `POST /api/partner/auth/password/reset` | O |
| PCR-1000 | 대시보드 | `/partner/dashboard` | `GET /api/partner/dashboard/summary`<br>`GET /api/partner/dashboard/chart` | 부분 구현 |
| PCR-2000 | 입금 내역 | `/partner/deposits` | `GET /api/partner/deposits`<br>`GET /api/partner/deposits/{depositId}` | O |
| PCR-2010 | 입금 세션 조회 | `/partner/deposit-sessions` | `GET /api/partner/deposit-sessions`<br>`GET /api/partner/deposit-sessions/{sessionId}` | O |
| PCR-2015 | 입금 세션 생성 | `/partner/deposit-sessions/new` | `POST /api/partner/deposit-sessions` | TODO |
| PCR-2020 | 결제 링크 관리 | `/partner/payment-links` | `GET /api/partner/payment-links`<br>`POST /api/partner/payment-links` | 조회 O / 생성 TODO |
| PCR-2030 | 입금 주소 관리 | `/partner/deposit-addresses` | `GET /api/partner/deposit-addresses`<br>`GET /api/partner/deposit-addresses/{addressId}` | O |
| PCR-2040 | 집금 현황 | `/partner/collections` | `GET /api/partner/collections`<br>`GET /api/partner/collections/{collectionId}` | O |
| PCR-2050 | Axim Pay 결제 | `/partner/axim-payments` | `GET /api/partner/axim-payments` | O |
| PCR-3000 | 출금 내역 | `/partner/withdrawals` | `GET /api/partner/withdrawals`<br>`GET /api/partner/withdrawals/{withdrawalId}` | O |
| PCR-3010 | 수동 출금 요청 | `/partner/withdrawals/new` | `POST /api/partner/withdrawals` | TODO |
| PCR-3020 | 출금 화이트리스트 | `/partner/withdrawals/whitelist` | `GET /api/partner/withdrawals/whitelist`<br>`POST /api/partner/withdrawals/whitelist`<br>`PUT /api/partner/withdrawals/whitelist/{id}` | O |
| PCR-4000 | 잔액 현황 | `/partner/balances` | `GET /api/partner/balances` | TODO |
| PCR-4010 | 원장 조회 | `/partner/ledger` | `GET /api/partner/ledger` | TODO |
| PCR-5000 | 수수료 집계 | `/partner/settlement/fees` | `GET /api/partner/settlement/daily-fees` | O |
| PCR-5010 | 정산 실현 | `/partner/settlement/realizations` | `GET /api/partner/settlement/realizations` | O |
| PCR-5020 | 정산 잔액 | `/partner/settlement/balance` | `GET /api/partner/settlement/balance` | O |
| PCR-5030 | 쉐어 출금 요청 | `/partner/settlement/withdraw` | `POST /api/partner/settlement/withdraw` | TODO |
| PCR-6000 | 가스비 기록 | `/partner/gas-costs` | `GET /api/partner/gas-costs` | O |
| PCR-6010 | 인보이스 조회 | `/partner/gas-invoices` | `GET /api/partner/gas-invoices` | O |
| PCR-6011 | 인보이스 상세 | `/partner/gas-invoices/{id}` | `GET /api/partner/gas-invoices/{invoiceId}` | O |
| PCR-7000 | 하위 파트너 목록 | `/partner/sub-partners` | `GET /api/partner/sub-partners` | O |
| PCR-7010 | 하위 파트너 등록 | `/partner/sub-partners/new` | `POST /api/partner/sub-partners` | O (서비스 로직 TODO) |
| PCR-7020 | 하위 파트너 상세 | `/partner/sub-partners/{id}` | `GET /api/partner/sub-partners/{partnerId}`<br>`PATCH /api/partner/sub-partners/{partnerId}` | O |
| PCR-7030 | 하위 파트너 현황 | `/partner/sub-partners/overview` | `GET /api/partner/sub-partners/overview` | O (서비스 TODO) |
| PCR-8000 | 매출 리포트 | `/partner/reports/revenue` | `GET /api/partner/reports/revenue` | O (서비스 TODO) |
| PCR-8010 | 수수료 리포트 | `/partner/reports/commission` | `GET /api/partner/reports/fees` | O (서비스 TODO) |
| PCR-8020 | 성과 리포트 | `/partner/reports/performance` | `GET /api/partner/reports/performance` | O (서비스 TODO) |
| PCR-9000 | API 키 설정 | `/partner/integration/api-key` | `GET /api/partner/integration/api-key`<br>`POST /api/partner/integration/api-key/regenerate` | O (재발급 서비스 TODO) |
| PCR-9010 | Webhook 설정 | `/partner/integration/webhook` | `PUT /api/partner/integration/webhook`<br>`POST /api/partner/integration/webhook/test`<br>`GET /api/partner/integration/webhook/logs` | O |
| PCR-9020 | Telegram 설정 | `/partner/integration/telegram` | `GET /api/partner/integration/telegram`<br>`PUT /api/partner/integration/telegram`<br>`POST /api/partner/integration/telegram/test` | O (서비스 TODO) |
| PCR-9030 | Axim Pay 설정 | `/partner/integration/axim` | `GET /api/partner/integration/axim` | O |
| PCR-A000 | 프로필 | `/partner/account/profile` | `GET /api/partner/account/profile`<br>`PUT /api/partner/account/profile` | O |
| PCR-A010 | 비밀번호 변경 | `/partner/account/password` | `POST /api/partner/account/password` | O |
| PCR-A020 | 2FA 설정 | `/partner/account/2fa` | `GET /api/partner/account/2fa/setup`<br>`POST /api/partner/account/2fa/enable`<br>`DELETE /api/partner/account/2fa` | O (TOTP 검증 TODO) |
| PCR-A030 | 로그인 이력 | `/partner/account/login-history` | `GET /api/partner/account/login-history` | TODO |
| PCR-C100 | 사용자 컨텍스트 | (오버레이 패널) | `GET /api/partner/users/{partnerUserId}/deposits`<br>`GET /api/partner/users/{partnerUserId}/sessions`<br>`GET /api/partner/users/{partnerUserId}/payment-links`<br>`GET /api/partner/users/{partnerUserId}/withdrawals`<br>`GET /api/partner/users/{partnerUserId}/axim-payments` | 라우팅 O, 서비스 TODO |

---

## 5. 인증 (Auth)

### POST /api/partner/auth/login — 로그인 (PCR-0100)

**Request Body**:
```json
{
  "email": "partner@example.com",
  "password": "SecurePass123!"
}
```

**Response 200** (2FA 미사용 — 즉시 로그인):
```json
{
  "token": "eyJ...",
  "partnerId": 1,
  "partnerType": "MERCHANT",
  "requiresTwoFactor": false
}
```

**Response 200** (2FA 사용 — 추가 검증 필요):
```json
{
  "requiresTwoFactor": true,
  "tempToken": "tmp_eyJ...",
  "partnerType": "MERCHANT"
}
```

> 응답에 `requiresTwoFactor: true`가 있으면 `tempToken`을 저장 후 2FA 검증 화면으로 이동

**에러**:
| HTTP | 상황 |
|------|------|
| 401 | 이메일/비밀번호 불일치 |
| 403 | SUSPENDED / TERMINATED 계정 |

---

### POST /api/partner/auth/2fa/verify — 2FA 검증 (PCR-0110)

**Request Body**:
```json
{
  "tempToken": "tmp_eyJ...",
  "otpCode": "123456"
}
```

**Response 200**: 로그인 응답과 동일한 구조 (token 포함)

---

### POST /api/partner/auth/token/refresh — 토큰 갱신

**Request Body**:
```json
{
  "refreshToken": "eyJ..."
}
```

**Response 200**:
```json
{
  "accessToken": "eyJ...",
  "expiresIn": 900
}
```

---

### POST /api/partner/auth/password/forgot — 비밀번호 재설정 요청 (PCR-0120)

**Request Body**: `{ "email": "partner@example.com" }`

**Response 200**: `{ "message": "..." }` (이메일 존재 여부 관계없이 동일 응답)

---

### POST /api/partner/auth/password/reset — 비밀번호 재설정 (PCR-0130)

**Request Body**:
```json
{
  "token": "reset_token_from_email",
  "newPassword": "NewSecurePass456!",
  "newPasswordConfirm": "NewSecurePass456!"
}
```

---

### POST /api/partner/auth/logout — 로그아웃

**Response 200**: `{ "message": "로그아웃 완료" }`

---

## 6. 대시보드 (Dashboard)

### GET /api/partner/dashboard/summary — 요약 조회 (PCR-1000)

**Query Parameters**:
| 파라미터 | 타입 | 설명 |
|---------|------|------|
| includeSubPartners | boolean | DISTRIBUTOR: true 시 하위 합산 포함 |

**현재 구현 Response 200** (단순화 버전):
```json
{
  "todayDepositCount": 15,
  "todayDepositAmount": "12500.000000",
  "todayWithdrawalCount": 3,
  "todayWithdrawalAmount": "5000.000000",
  "pendingWithdrawalCount": 2
}
```

> 잔액, 알림, 하위 파트너 요약은 별도 API 호출:
> - 잔액: `GET /api/partner/balances` (현재 TODO)
> - 미납 인보이스: `GET /api/partner/gas-invoices?status=ISSUED` 건수로 집계

---

### GET /api/partner/dashboard/chart — 차트 데이터 (PCR-1000)

**Query Parameters**:
| 파라미터 | 타입 | 기본값 | 설명 |
|---------|------|--------|------|
| days | integer | 7 | 조회 일수 (최대 30) |

**Response 200** (직접 배열 반환):
```json
[
  { "date": "2026-03-05", "depositCount": 8, "depositAmount": "5200.000000" },
  { "date": "2026-03-04", "depositCount": 12, "depositAmount": "8100.000000" }
]
```

---

## 7. 입금 관리 (Deposit)

### GET /api/partner/deposits — 입금 내역 목록 (PCR-2000)

**Query Parameters**:
| 파라미터 | 타입 | 설명 |
|---------|------|------|
| from | string | 시작 일시 (ISO 8601) |
| to | string | 종료 일시 |
| status | string | DETECTED / CONFIRMING / CONFIRMED / COLLECTING / SETTLED / FAILED |
| currencyId | integer | 통화 ID |
| networkId | integer | 네트워크 ID |
| depositType | string | CRYPTO_PAYMENT / AXIM_PAYMENT 등 |
| partnerUserId | string | 파트너 사용자 ID |
| search | string | deposit_code 또는 tx_hash 검색 |
| page, size, sort, order | — | 페이지네이션 |

**Response Schema**: `XPage<DepositResponse>` (`data.content[]`)

`DepositResponse` 주요 필드 (camelCase):
```json
{
  "id": 1001,
  "depositCode": "dep_2603_X1Y2Z3W4",
  "partnerUserId": "user_12345",
  "amount": "100.000000",
  "feeAmount": "1.000000",
  "netAmount": "99.000000",
  "status": "CONFIRMED",
  "txHash": "0xabc123...",
  "blockConfirmations": 12,
  "createdAt": "2026-03-05T10:30:00+09:00",
  "confirmedAt": "2026-03-05T10:35:00+09:00"
}
```

> `partnerUserId` 값은 **클릭 가능한 링크**로 렌더링 → PCR-C100 사용자 컨텍스트 패널 오픈

---

### GET /api/partner/deposits/{depositId} — 입금 상세 (PCR-2000 드로어)

**Path Parameter**: `depositId` (integer)

**Response Schema**: `DepositResponse` (단건 + 상태 이력 포함)

---

### GET /api/partner/deposit-sessions — 입금 세션 목록 (PCR-2010)

**Query Parameters**:
| 파라미터 | 타입 | 설명 |
|---------|------|------|
| from, to | string | 기간 |
| status | string | CREATED / WAITING / RECEIVED / COMPLETED / EXPIRED 등 |
| depositMethod | string | HD_WALLET / DECIMAL_MATCH / DIRECT / EXTERNAL_WALLET |
| hasPaymentLink | boolean | 결제링크 경유 여부 |
| partnerUserId | string | 파트너 사용자 ID |
| search | string | 세션코드, 주소 |

**Response Schema**: `XPage<DepositSessionResponse>`

`DepositSessionResponse` 주요 필드:
```json
{
  "id": 500,
  "sessionCode": "ses_2603_A1B2C3D4",
  "partnerUserId": "user_12345",
  "depositAddress": "T9yD14Nj9j7xAB4dbGei...",
  "depositMethod": "HD_WALLET",
  "requestSource": "API",
  "status": "COMPLETED",
  "cryptoAmount": "100.000000",
  "expiresAt": "2026-03-05T11:30:00+09:00",
  "createdAt": "2026-03-05T10:30:00+09:00"
}
```

---

### POST /api/partner/deposit-sessions — 입금 세션 생성 (PCR-2015) [TODO]

**Request Body**:
```json
{
  "partnerUserId": "user_12345",
  "currencyId": 1,
  "networkId": 1,
  "depositMethod": "HD_WALLET",
  "amount": 100.0,
  "amountCurrency": "CRYPTO",
  "expiresInMinutes": 60
}
```

> `amountCurrency`: `"CRYPTO"` 또는 `"KRW"` (KRW 선택 시 자동 환율 변환)

**Response 201**: `DepositSessionResponse`

---

### GET /api/partner/deposit-sessions/{sessionId} — 세션 상세

**Response Schema**: `DepositSessionResponse` (단건)

---

### GET /api/partner/payment-links — 결제 링크 목록 (PCR-2020)

**Query Parameters**: `status`, `partnerUserId`, `search`, page/size

**Response Schema**: `XPage<PaymentLink>`

`PaymentLink` 주요 필드:
```json
{
  "id": 200,
  "linkCode": "lnk_2603_P1Q2R3S4",
  "title": "상품 A 결제",
  "partnerUserId": "user_12345",
  "amount": "50.000000",
  "status": "ACTIVE",
  "paymentUrl": "https://pay.cryptoments.cc/lnk_2603_P1Q2R3S4",
  "expiresAt": null,
  "createdAt": "2026-03-05T10:00:00+09:00"
}
```

---

### POST /api/partner/payment-links — 결제 링크 생성 (PCR-2020) [TODO]

**Request Body**:
```json
{
  "title": "상품 A 결제",
  "partnerUserId": "user_12345",
  "partnerReference": "order_9876",
  "currencyId": 1,
  "networkId": null,
  "amount": "50.000000",
  "depositMethod": null,
  "expiresInMinutes": null
}
```

> nullable 필드를 null로 보내면 결제 페이지에서 사용자가 직접 선택

---

### GET /api/partner/deposit-addresses — 입금 주소 풀 (PCR-2030)

**Query Parameters**: `networkId`, `isActive`, page/size

**Response Schema**: `XPage<WalletAddress>`

---

### GET /api/partner/deposit-addresses/{addressId} — 주소 상세 (PCR-2030 드로어)

**Response Schema**: `WalletAddress`

---

### GET /api/partner/collections — 집금 현황 (PCR-2040)

**Query Parameters**: `from`, `to`, `status`, `networkId`, page/size

**Response Schema**: `XPage<CollectionQueue>`

---

### GET /api/partner/collections/{collectionId} — 집금 상세

**Response Schema**: `CollectionQueue`

---

### GET /api/partner/axim-payments — Axim Pay 결제 내역 (PCR-2050)

**Query Parameters**: `from`, `to`, `status`, `partnerUserId`, `search`, page/size

**Response Schema**: `XPage<AximPayment>`

> `partner_axim_settings` 미설정 파트너는 403

---

### POST /api/partner/axim-payments/request — Axim 결제 요청 [TODO]

**Request Body**:
```json
{
  "partnerUserId": "user_12345",
  "amount": "50.000000",
  "currencyId": 1,
  "networkId": 1,
  "partnerReference": "order_9876"
}
```

---

## 8. 출금 관리 (Withdrawal)

### GET /api/partner/withdrawals — 출금 내역 목록 (PCR-3000)

**Query Parameters**:
| 파라미터 | 타입 | 설명 |
|---------|------|------|
| from, to | string | 기간 |
| status | string | REQUESTED / PENDING_APPROVAL / APPROVED / REJECTED / PROCESSING / BROADCASTING / CONFIRMED / FAILED / CANCELLED |
| currencyId | integer | 통화 |
| withdrawalType | string | PARTNER_WITHDRAW / SETTLEMENT_WITHDRAW |
| partnerUserId | string | 파트너 사용자 ID |
| search | string | withdrawal_code, tx_hash |

**Response Schema**: `XPage<WithdrawalResponse>`

`WithdrawalResponse` 주요 필드:
```json
{
  "id": 600,
  "withdrawalCode": "wdr_2603_T5U6V7W8",
  "withdrawalType": "PARTNER_WITHDRAW",
  "amount": "500.000000",
  "feeAmount": "1.000000",
  "netAmount": "499.000000",
  "toAddress": "TRecipient123...",
  "status": "CONFIRMED",
  "txHash": "0xghi789...",
  "createdAt": "2026-03-05T14:00:00+09:00"
}
```

---

### GET /api/partner/withdrawals/{withdrawalId} — 출금 상세 (PCR-3000 드로어)

**Response Schema**: `WithdrawalResponse` (상태 이력 포함)

---

### POST /api/partner/withdrawals — 수동 출금 요청 (PCR-3010) [TODO]

**Request Body**:
```json
{
  "currencyId": 1,
  "networkId": 1,
  "toAddress": "TRecipient123...",
  "whitelistId": 5,
  "amount": "500.000000",
  "partnerUserId": null
}
```

> `whitelistId` 또는 `toAddress` 중 하나 필수. `whitelist_required=TRUE` 파트너는 `whitelistId` 필수.

**에러**:
| HTTP | 코드 | 상황 |
|------|------|------|
| 400 | - | 잔액 부족 / 화이트리스트 오류 |

---

### GET /api/partner/withdrawals/whitelist — 화이트리스트 목록 (PCR-3020)

**Response Schema**: `List<WithdrawalAddressWhitelist>` (배열 직접 반환)

`WithdrawalAddressWhitelist` 주요 필드:
```json
{
  "id": 5,
  "label": "메인 출금 지갑",
  "address": "TRecipient123...",
  "networkId": 1,
  "isActive": true,
  "createdAt": "2026-02-01T00:00:00+09:00"
}
```

---

### POST /api/partner/withdrawals/whitelist — 주소 추가 (PCR-3020)

**Request Body**:
```json
{
  "label": "콜드월렛",
  "address": "TNewAddress456...",
  "networkId": 1
}
```

**Response 201**: `WithdrawalAddressWhitelist`

---

### PUT /api/partner/withdrawals/whitelist/{id} — 주소 수정

**Path Parameter**: `id` (integer)

**Request Body**: `{ "label": "새 라벨", "isActive": false }`

**Response 200**: `WithdrawalAddressWhitelist`

---

## 9. 잔액/원장 (Balance/Ledger)

### GET /api/partner/balances — 잔액 현황 (PCR-4000) [TODO]

**Response 200** (예정 구현 구조):
```json
[
  { "currencyId": 1, "currencyCode": "USDT", "totalBalance": "48730.500000" }
]
```

> 현재 단순화된 3필드 버전. `available`, `frozen` 분리는 추후 업데이트 예정.

---

### GET /api/partner/ledger — 원장 조회 (PCR-4010) [TODO]

**Query Parameters**: `from`, `to`, `currencyId`, `entryType`, `search`, page/size

**Response Schema**: `XPage<LedgerEntry>`

---

## 10. 정산 (Settlement)

### GET /api/partner/settlement/daily-fees — 일별 수수료 집계 (PCR-5000)

**Query Parameters**: `from`, `to`, `currencyId`, page/size

**Response Schema**: `XPage<SettlementDailyFee>`

`SettlementDailyFee` 주요 필드:
```json
{
  "id": 700,
  "feeDate": "2026-03-05",
  "currencyId": 1,
  "shareAmount": "125.000000",
  "txCount": 50,
  "isRealized": false,
  "createdAt": "2026-03-06T00:05:00+09:00"
}
```

---

### GET /api/partner/settlement/realizations — 정산 실현 내역 (PCR-5010)

**Query Parameters**: `from`, `to`, `currencyId`, `status`, page/size

**Response Schema**: `XPage<SettlementRealization>`

---

### GET /api/partner/settlement/balance — 정산 잔액 (PCR-5020)

**Response Schema**: `List<SettlementBalance>` (배열 직접 반환)

`SettlementBalance` 주요 필드:
```json
{
  "id": 1,
  "participantPartnerId": 1,
  "currencyId": 1,
  "unrealizedBalance": "250.000000",
  "realizedBalance": "3500.000000",
  "withdrawnBalance": "2000.000000"
}
```

---

### POST /api/partner/settlement/withdraw — 쉐어 출금 요청 (PCR-5030) [TODO]

**Request Body**:
```json
{
  "currencyId": 1,
  "networkId": 1,
  "toAddress": "TRecipient123...",
  "whitelistId": 5,
  "amount": "1000.000000"
}
```

**Response 201**: `Withdrawal` (출금 요청 결과)

---

## 11. 가스비 (Gas)

### GET /api/partner/gas-costs — 가스비 기록 (PCR-6000)

**Query Parameters**: `from`, `to`, `networkId`, page/size

**Response Schema**: `XPage<GasCostRecord>`

`GasCostRecord` 주요 필드:
```json
{
  "id": 900,
  "txType": "COLLECTION",
  "networkId": 1,
  "feeNative": "14.500000",
  "feeUsd": "2.150000",
  "txHash": "0xgas123...",
  "invoiceId": 100,
  "createdAt": "2026-03-05T11:02:00+09:00"
}
```

---

### GET /api/partner/gas-invoices — 인보이스 목록 (PCR-6010)

**Query Parameters**: `status` (DRAFT/ISSUED/PAID/OVERDUE/CANCELLED), page/size

**Response Schema**: `XPage<GasInvoice>`

`GasInvoice` 주요 필드:
```json
{
  "id": 100,
  "invoiceNumber": "INV-2603-0042",
  "billingMonth": "2026-02",
  "totalAmountUsd": "45.200000",
  "status": "ISSUED",
  "issuedAt": "2026-03-01T00:00:00+09:00",
  "dueDate": "2026-03-15",
  "paidAt": null
}
```

---

### GET /api/partner/gas-invoices/{invoiceId} — 인보이스 상세 (PCR-6011)

**Path Parameter**: `invoiceId` (integer)

**Response Schema**: `GasInvoice`

---

## 12. 하위 파트너 관리 (Sub-Partners) — DISTRIBUTOR 전용

> 모든 엔드포인트 — MERCHANT 호출 시 **403 FORBIDDEN**

### GET /api/partner/sub-partners — 하위 파트너 목록 (PCR-7000)

**Query Parameters**:
| 파라미터 | 타입 | 설명 |
|---------|------|------|
| status | string | PENDING / ACTIVE / SUSPENDED / TERMINATED |
| partnerType | string | MERCHANT / DISTRIBUTOR |
| view | string | `list` (기본) / `tree` |
| search | string | partner_code, partner_name |
| page, size | — | |

**Response Schema**: `XPage<Partner>`

---

### POST /api/partner/sub-partners — 하위 파트너 등록 (PCR-7010)

**Request Body**:
```json
{
  "partnerType": "MERCHANT",
  "partnerName": "새 파트너",
  "businessName": "주식회사 새파트너",
  "contactName": "김담당",
  "contactEmail": "contact@newpartner.com",
  "contactPhone": "010-1234-5678",
  "loginEmail": "login@newpartner.com",
  "depositFeeRate": "0.012000"
}
```

**Response 201**: `Partner`

**에러**:
| HTTP | 상황 |
|------|------|
| 400 | 수수료율 범위 벗어남 |
| 403 | DISTRIBUTOR 권한 없음 |
| 409 | 이메일 중복 |

---

### GET /api/partner/sub-partners/{partnerId} — 하위 파트너 상세 (PCR-7020)

**Response Schema**: `Partner`

---

### PATCH /api/partner/sub-partners/{partnerId} — 하위 파트너 수정

**Request Body**: 부분 업데이트 (수정 필드만 포함)

```json
{ "status": "SUSPENDED" }
```

---

### GET /api/partner/sub-partners/overview — 거래 현황 요약 (PCR-7030)

**Query Parameters**: `period` (today / week / month, 기본값 today)

**Response Schema**: `SubPartnerOverviewResponse`
```json
{
  "totalSubPartners": 12,
  "subPartners": [
    {
      "id": 10, "partnerCode": "PTN_...", "partnerName": "하위 파트너 A"
    }
  ]
}
```

---

## 13. 총판 리포트 (Reports) — DISTRIBUTOR 전용

### GET /api/partner/reports/revenue — 매출 리포트 (PCR-8000)

**Query Parameters**: `from`, `to`, `interval` (daily/monthly, 기본 daily)

**Response Schema**: `List<RevenueReportResponse>`
```json
[
  {
    "period": "2026-03-05",
    "totalShareAmount": "125.000000",
    "totalFeeAmount": "12.500000",
    "totalDepositAmount": "5200.000000"
  }
]
```

---

### GET /api/partner/reports/fees — 수수료 리포트 (PCR-8010)

> 명세서 경로 `/reports/commission`과 다름 — **실제 경로는 `/reports/fees`**

**Query Parameters**: `from`, `to`

**Response Schema**: `List<FeeReportResponse>`
```json
[
  {
    "partnerId": 10,
    "partnerName": "하위 파트너 A",
    "partnerCode": "PTN_...",
    "totalFee": "500.000000",
    "distributorShare": "50.000000"
  }
]
```

---

### GET /api/partner/reports/performance — 성과 비교 (PCR-8020)

**Query Parameters**: `from`, `to`

**Response Schema**: `List<PerformanceReportResponse>`
```json
[
  {
    "partnerId": 10,
    "partnerName": "하위 파트너 A",
    "depositCount": 150,
    "withdrawalCount": 12
  }
]
```

---

## 14. 연동 설정 (Integration)

> 명세서 경로 `/settings/...`는 실제 구현에서 `/api/partner/integration/...`으로 변경됨

### GET /api/partner/integration/api-key — API 키 조회 (PCR-9000)

**Response Schema**: `ApiKeyResponse`
```json
{
  "apiKeyMasked": "cmt_****...****a1b2",
  "webhookUrl": "https://partner.com/webhook/cryptoments"
}
```

---

### POST /api/partner/integration/api-key/regenerate — API 키 재발급

**Response 200**: `ApiKeyResponse`

> 재발급된 키는 한 번만 표시됩니다.

---

### PUT /api/partner/integration/webhook — Webhook URL 설정 (PCR-9010)

**Request Body**:
```json
{ "webhookUrl": "https://partner.com/webhook/v2" }
```

**Response 200**: `WebhookResponse` (`{ "webhookUrl": "..." }`)

---

### POST /api/partner/integration/webhook/test — Webhook 테스트 전송

**Request Body** (기존 body 없는 요청도 호환):
```json
{
  "eventType": "WITHDRAWAL_COMPLETED",
  "payload": {
    "transactionId": 127,
    "userId": "user_002",
    "transactionHash": "0xdef789abc123",
    "amount": "500.000000"
  }
}
```

지원 이벤트: `DEPOSIT_CONFIRMED`, `WITHDRAWAL_REQUESTED`, `WITHDRAWAL_COMPLETED`, `WITHDRAWAL_FAILED`, `P2P_ORDER_COMPLETE`, `P2P_UNSOLD_RECOVERED`. 발송 URL은 저장된 파트너 Webhook URL로 고정되며 `eventType`, `partnerId`, `timestamp`, `signature`는 서버가 실제 테스트 값으로 덮어쓴다.

**Response 200**: `MessageResponse` (`{ "message": "..." }`)

---

### GET /api/partner/integration/webhook/logs — Webhook 전달 로그

**Query Parameters**: page/size

**Response Schema**: `XPage<WebhookDeliveryLog>`

---

### GET /api/partner/integration/telegram — Telegram 설정 조회 (PCR-9020)

**Response Schema**: `TelegramConfigResponse`
```json
{
  "config": {
    "partnerId": 1,
    "botUsername": "@CryptomentsBot",
    "isActive": true
  },
  "subscriptions": [
    { "eventType": "DEPOSIT_CONFIRMED", "isActive": true },
    { "eventType": "WITHDRAWAL_COMPLETED", "isActive": true }
  ]
}
```

---

### PUT /api/partner/integration/telegram — Telegram 설정 수정

**Request Body**: `UpdateTelegramConfigRequest`

**Response 200**: `PartnerTelegramConfig`

---

### POST /api/partner/integration/telegram/test — Telegram 테스트 전송

**Response 200**: `MessageResponse`

---

### GET /api/partner/integration/axim — Axim Pay 설정 조회 (PCR-9030)

**Response Schema**: `PartnerAximSettings`

> Axim 미설정 파트너는 **403**

---

## 15. 내 계정 (Account)

### GET /api/partner/account/profile — 프로필 조회 (PCR-A000)

**Response Schema**: `Partner` (Entity 직접 반환)

`Partner` 주요 필드 (민감 필드 `@JsonIgnore` 처리):
```json
{
  "id": 1,
  "partnerCode": "PTN_2601_A1B2C3D4",
  "partnerName": "테스트 파트너",
  "partnerType": "MERCHANT",
  "businessName": "주식회사 테스트",
  "contactName": "홍길동",
  "contactEmail": "contact@test.com",
  "contactPhone": "010-1234-5678",
  "loginEmail": "login@test.com",
  "status": "ACTIVE",
  "twoFactorEnabled": true,
  "createdAt": "2026-01-01T00:00:00+09:00"
}
```

---

### PUT /api/partner/account/profile — 프로필 수정

> 명세서 `PATCH`와 달리 실제는 `PUT`

**Request Body**:
```json
{
  "contactName": "홍길동 (변경)",
  "businessName": "주식회사 변경",
  "contactEmail": "new@test.com",
  "contactPhone": "010-9876-5432"
}
```

---

### POST /api/partner/account/password — 비밀번호 변경 (PCR-A010)

**Request Body**:
```json
{
  "currentPassword": "OldPass123!",
  "newPassword": "NewPass456!",
  "newPasswordConfirm": "NewPass456!"
}
```

**Response 200**: `MessageResponse` (`{ "message": "비밀번호가 변경되었습니다" }`)

**에러**:
| HTTP | 상황 |
|------|------|
| 400 | 현재 비밀번호 불일치 |

---

### GET /api/partner/account/2fa/setup — 2FA QR 코드 생성 (PCR-A020)

**Response Schema**: `TwoFactorSetupResponse`
```json
{
  "secret": "JBSWY3DPEHPK3PXP",
  "qrUrl": "otpauth://totp/Cryptoments:login@test.com?secret=..."
}
```

> `qrUrl`로 QR 코드 이미지 생성 후 표시 (라이브러리 사용: `qrcode.js` 등)

---

### POST /api/partner/account/2fa/enable — 2FA 활성화

> 명세서 `POST /account/2fa/verify`와 URL 다름 — **실제 경로 `/account/2fa/enable`**

**Request Body**:
```json
{
  "otpCode": "123456",
  "secret": "JBSWY3DPEHPK3PXP"
}
```

**Response 200**: `MessageResponse`

---

### DELETE /api/partner/account/2fa — 2FA 해제

> 명세서 `POST /account/2fa/disable`와 HTTP Method 다름 — **실제는 DELETE**

**Request Body**:
```json
{ "otpCode": "123456" }
```

**Response 200**: `MessageResponse`

---

### GET /api/partner/account/login-history — 로그인 이력 (PCR-A030) [TODO]

**Response 200**: `MessageResponse` (현재 미구현)

---

## 16. 사용자 컨텍스트 (User Context)

> PCR-C100: 테이블의 `partnerUserId` 클릭 시 오버레이 패널로 표시
>
> 명세서 경로 `/user-context/{id}/...`와 달리 **실제 경로는 `/api/partner/users/{partnerUserId}/...`**
>
> **요약 엔드포인트 없음**: 명세서의 `GET /user-context/{id}/summary`는 현재 미구현

### GET /api/partner/users/{partnerUserId}/deposits — 사용자 입금 내역

**Path Parameter**: `partnerUserId` (string)
**Query Parameters**: page/size
**Response Schema**: `XPage<DepositResponse>`

---

### GET /api/partner/users/{partnerUserId}/sessions — 사용자 세션 내역

**Response Schema**: `XPage<DepositSessionResponse>`

---

### GET /api/partner/users/{partnerUserId}/payment-links — 사용자 결제 링크

**Response Schema**: `XPage<PaymentLink>`

---

### GET /api/partner/users/{partnerUserId}/withdrawals — 사용자 출금 내역

**Response Schema**: `XPage<WithdrawalResponse>`

---

### GET /api/partner/users/{partnerUserId}/axim-payments — 사용자 Axim Pay 내역

**Response Schema**: `XPage<AximPayment>`

---

## 17. Webhook 이벤트 정의

파트너 서버가 수신할 Webhook 은 **평면(flat) JSON** 이며, 이벤트는 `eventType` 으로 구분한다.

> **2026-08-17 변경 (호환 기간 없음)** — **원화결제(P2P·TORQ 매칭) 입금**은 `DEPOSIT_CONFIRMED` 를
> 더 이상 보내지 않고 **`P2P_ORDER_COMPLETE`** 로 통지한다. 이 이벤트를 처리하지 않으면 원화결제
> 충전이 **전부 반영되지 않는다.** 아래 "[P2P·TORQ 원화결제 입금](#p2ptorq-원화결제-입금--p2p_order_complete-2026-08-17-신설)" 절 참조.
>
> **2026-08-13 변경 (호환 기간 없음)** — 출금 이벤트를 **9종 → 3종**으로 줄이고, 입금 `amount` 를
> **실 크레딧액(net)** 으로 바꿨다. 아래 "변경 요약" 참조.

**서명 검증**: `HMAC-SHA256("{partnerId}|{transactionHash}|{amount}|{timestamp}", api_secret)` → hex.
수신한 페이로드의 `partnerId`·`transactionHash`·`amount`·`timestamp` 를 그대로 이어 붙이면 재현된다.
`transactionHash` 가 없는 이벤트는 **빈 문자열(`""`)** 로 정규화되어 서명에도 `""` 가 들어간다.
`settlementMethod`·`reason`·`reasonDetail`·`feeAmount`·`grossAmount` 는 **서명 대상이 아니다.**

**재시도 정책**: 실패 시 30초 → 60초 → 5분 → 15분 → 60분 (최대 5회)

### 이벤트 유형

| `eventType` | 설명 |
|---|---|
| `DEPOSIT_CONFIRMED` | 입금 확정 — **온체인 입금 / 폰페이(원화 은행이체)**. 트랜잭션 1건 = 웹훅 1건 |
| `P2P_ORDER_COMPLETE` | **원화결제(P2P·TORQ 매칭) 주문 종결** — 주문 1건 = 웹훅 1건 (2026-08-17 신설) |
| `WITHDRAWAL_REQUESTED` | 출금 요청 생성 |
| `WITHDRAWAL_COMPLETED` | 출금 **종결(성공)** — 온체인 전송 확정 또는 P2P 출금 완료 |
| `WITHDRAWAL_FAILED` | 출금 **종결(실패)** — 거부·취소·TX 실패·재시도 소진 |

출금은 "요청했다 / 성공으로 끝났다 / 실패로 끝났다" 3종만 통지한다. 승인, P2P 전환 등 **중간
상태는 내부 사정이므로 통지하지 않는다.**

### 입금 — 금액 필드

| 필드 | 의미 |
|---|---|
| `amount` | **실 크레딧액(net)** = `grossAmount − feeAmount`. 회원에게 반영할 금액 |
| `feeAmount` | 수수료. 수수료가 없는 건은 `"0"` |
| `grossAmount` | 온체인 전송액. `transactionHash` 로 체인 대조 시 사용 |

```json
{
  "eventType": "DEPOSIT_CONFIRMED",
  "transactionHash": "0x...",
  "amount": "70.780141000000000000",
  "feeAmount": "0.141844000000000000",
  "grossAmount": "70.921985000000000000"
}
```

> ⚠️ `amount` 가 net 으로 바뀌었다. 기존에 `amount` 를 그대로 크레딧하던 파트너는 **수수료가
> 이중 차감되지 않도록** 자체 차감 로직을 제거해야 한다. 서명도 새 `amount`(net) 기준이다.

`krwAmount` / `usdAmount` 환산도 **net 기준**이다 — `amount` 와 같은 금액을 환산한 값이므로
회원에게 원화를 그대로 안내해도 어긋나지 않는다. gross 기준 원화가 필요하면
`grossAmount × tokenKrwPrice` 로 직접 계산할 것.

### 입금 — 경로 구분 필드 (2026-08-16 신설)

`DEPOSIT_CONFIRMED` 도 `settlementMethod` 로 입금 경로를 알린다. 출금 웹훅의 동명 필드와 같은 역할이다.

| `settlementMethod` | 의미 | `transactionHash` |
|---|---|---|
| `ONCHAIN` | 온체인 전송 확정 | 실제 체인 해시 |
| `FIAT` | **원화 은행이체 입금** (폰페이). 체인 자체가 없다 | `""` |
| `INTERNAL` | **내부 정산** — 온체인 이동이 없는 크레딧. 2026-08-17 이후 `DEPOSIT_CONFIRMED` 로는 **더 이상 발생하지 않는다**(해당 건은 `P2P_ORDER_COMPLETE` 로 이동). 과거 이벤트 파싱 호환용 | `""` |

> ⚠️ `transactionHash` 가 `""` 인 것은 **장애가 아니다.** 해당 입금에 대응하는 온체인 트랜잭션이
> 실제로 존재하지 않는다는 뜻이다. 체인 대사는 `settlementMethod == "ONCHAIN"` 인 건만 수행할 것.
> **해시 유무로 경로를 유추하지 말고 `settlementMethod` 로 판단할 것.**

`FIAT` 입금은 `chainType = "FIAT"`, `currencyType = "KRW"` 이며 `amount` 는 **원화 금액**이다
(USDT 입금과 단위가 다르다 — `currencyType` 으로 반드시 구분할 것). `krwAmount` / `usdAmount` 는
경로와 무관하게 항상 원화/달러 환산값이므로 그대로 비교할 수 있다.

---

### P2P·TORQ 원화결제 입금 — `P2P_ORDER_COMPLETE` (2026-08-17 신설)

> **원화결제(P2P 매칭 / TORQ 매칭)를 사용하는 파트너는 이 이벤트를 반드시 구현해야 한다.**
> 구현하지 않으면 원화결제로 들어온 충전이 **한 건도 반영되지 않는다.**

#### 1. 무엇이 바뀌었나

| 입금 경로 | 이벤트 |
|---|---|
| 온체인 입금 (USDT 등) | `DEPOSIT_CONFIRMED` — **변경 없음** |
| 폰페이 (원화 은행이체) | `DEPOSIT_CONFIRMED` — **변경 없음** |
| **원화결제 P2P / TORQ 매칭** | **`P2P_ORDER_COMPLETE`** — 신설 (기존 `DEPOSIT_CONFIRMED` 중단) |

**왜 별도 이벤트인가.** `DEPOSIT_CONFIRMED` 는 파트너에게 세 가지를 약속하는 이벤트인데,
원화결제는 구조적으로 셋 다 지킬 수 없다.

| `DEPOSIT_CONFIRMED` 의 약속 | 원화결제에서 깨지는 이유 |
|---|---|
| **1 트랜잭션 = 1 웹훅** | 한 주문이 여러 상대(P2P 판매자 / TORQ)에 **분할 매칭**된다. 매칭 건수만큼 웹훅이 나가 파트너가 한 주문을 여러 입금으로 오인한다 |
| **`transactionHash` 로 체인 대사 가능** | 같은 파트너 내부에서 처리되는 매칭은 온체인 전송 자체가 없어 해시가 `""` 다 — 대사할 대상이 존재하지 않는다 |
| **온체인 확정 = 종결** | 실제로 일어나는 일은 **구매자의 원화 계좌이체 + 내부 정산**이다. 체인 확정과 시점·의미가 다르다 |

여기에 더 큰 문제가 있었다. 주문이 **일부만 체결되고 종결**되는 경우(예: 30,000원 주문 중
15,000원만 성사), 파트너는 부분 금액에 대한 웹훅 1건만 받고 **"이 주문은 여기서 끝났다"는 통지를
받지 못했다.** 주문이 영원히 열려 있는 것처럼 보여 CS 로 이어졌다.

`P2P_ORDER_COMPLETE` 는 파트너가 실제로 알아야 하는 **"이 주문으로 결국 얼마를 받았나"** 하나만,
**주문 종결 시점에 1회** 통지한다. 중간 과정(분할 매칭, 상대방 이체, 내부 정산)은 Cryptoments 의
내부 사정이며 통지 대상이 아니다.

#### 2. 언제 오는가

```
주문이 종결되는 시점에 정확히 1회
체결분이 1원이라도 있을 때만 발송
```

| 상황 | 통지 시점 |
|---|---|
| **전액 체결 (정상 거래)** | **사실상 즉시** — 마지막 체결이 끝나는 순간 주문이 닫히고 웹훅이 나간다 |
| **일부만 체결되고 끝남** | 주문 만료 시각까지 대기 후 종결. 주문 유효시간은 **생성 후 30분**이며, 만료 처리는 30초 주기로 돌아 **최대 약 30분 30초** 뒤 도착한다 |
| **구매자가 주문을 취소** | 취소 시점. 단 체결분이 있는 경우에만 발송 |
| **문제 신고(분쟁) 진행 중** | 종결 **보류** — 관리자 판정으로 해소된 뒤 종결·발송 |

**발송되지 않는 경우 (정상 동작이다):**

- 한 건도 체결하지 못하고 만료된 주문
- 체결 없이 취소된 주문

> 체결분이 0 이면 반영할 금액도 없으므로 이벤트를 만들지 않는다. **"주문했는데 웹훅이 안 왔다"
> = 한 푼도 체결되지 않았다**는 뜻이다. 파트너 시스템에서 미수신 주문을 실패로 처리하면 된다.

#### 3. 페이로드 전체 필드

`P2P_ORDER_COMPLETE` 는 아래 필드를 **항상 전부** 싣는다(값이 없으면 `null`, 키는 유지).

| 필드 | 타입 | 의미 | 예시 |
|---|---|---|---|
| `eventType` | string | 항상 `"P2P_ORDER_COMPLETE"` | `"P2P_ORDER_COMPLETE"` |
| `eventId` | string | **멱등키.** `evt_p2po_{orderCode}_{result}` 형식. 재전송돼도 값이 변하지 않는다 | `"evt_p2po_pdo_32d94ea4da7b_FULL"` |
| `settlementMethod` | string | 항상 `"P2P"` — 체인 대사 대상이 아님을 뜻한다 | `"P2P"` |
| `result` | string | `"FULL"` (요청 금액 전액 체결) 또는 `"PARTIAL"` (일부만 체결하고 종결) | `"FULL"` |
| `transactionId` | number | Cryptoments **주문** 내부 ID. ⚠️ 입금 건 ID 가 아니다 | `1346` |
| `partnerId` | string | 파트너 ID (문자열) | `"50"` |
| `userId` | string | 파트너 회원 식별자 (주문 시 넘긴 값) | `"mouse777"` |
| `orderId` | string \| null | **파트너 주문 ID** — 주문 생성 시 넘긴 `partnerReference`. 대사 기준 키. 안 넘겼으면 `null` | `"260814164752-NNLNO-mouse777"` |
| `orderCode` | string | Cryptoments 주문 코드 | `"pdo_32d94ea4da7b"` |
| `transactionHash` | string | 항상 빈 문자열 `""` (체인 대사 불가). 서명 재현에 이 값을 그대로 쓴다 | `""` |
| `settledAmount` | string | ⭐ **회원에게 반영할 금액. 단위 USDT.** 수수료를 뺀 실 크레딧액(net) | `"20.806794000000000000"` |
| `settledCurrency` | string | 항상 `"USDT"` | `"USDT"` |
| `settledFeeAmount` | string | 구매자 부담 수수료. 단위 USDT. 수수료가 없는 경로는 `"0"` | `"0"` |
| `settledGrossAmount` | string | 수수료 차감 **전** 체결액. 단위 USDT (`settledAmount + settledFeeAmount`) | `"20.806794000000000000"` |
| `settledAmountKrw` | string | 실제 체결된 **원화** 합계. `orderAmount` 와 같은 단위라 바로 비교된다 | `"30000"` |
| `orderAmount` | string | **주문 요청 금액. 단위 KRW.** ⚠️ `settledAmount` 와 단위가 다르다 | `"30000"` |
| `orderCurrency` | string | 항상 `"KRW"` — 주문은 원화 액면으로만 존재한다 | `"KRW"` |
| `unsettledAmountKrw` | string | 미체결 원화 = `orderAmount − settledAmountKrw`. 전액 체결이면 `"0"` | `"0"` |
| `amount` | string | `settledAmount` 와 **같은 값**(기존 입금 웹훅과 키 이름을 맞춘 호환 필드). 단위 USDT | `"20.806794000000000000"` |
| `currencyType` | string | 항상 `"USDT"` — `amount` 의 단위 | `"USDT"` |
| `krwAmount` | string | `settledAmountKrw` 와 같은 값 | `"30000"` |
| `usdAmount` | string | `settledAmount` 와 같은 값 (USDT 1:1 기준) | `"20.806794000000000000"` |
| `legCount` | number | 이 주문에 생성된 전체 매칭 건수(실패·취소 포함) | `1` |
| `settledLegCount` | number | 그중 실제 체결된 건수 | `1` |
| `status` | string | 주문 종결 상태 — `COMPLETED` / `PARTIALLY_SETTLED` / `CANCELLED` | `"COMPLETED"` |
| `closeReason` | string \| null | 종결 사유 코드 (§ 종결 사유 표) | `"ALL_SETTLED"` |
| `closedAt` | string \| null | 종결 시각 `yyyy-MM-dd'T'HH:mm:ss` (KST) | `"2026-08-14T16:48:12"` |
| `timestamp` | string | 페이로드 생성 시각 (Unix seconds). **서명 입력값** | `"1786693692"` |
| `signature` | string | HMAC-SHA256 hex (§ 서명 검증) | `"9b7e…"` |

> **`DEPOSIT_CONFIRMED` 에는 있고 이 이벤트에는 없는 필드**: `fromAddress`, `toAddress`,
> `chainType`, `depositMethod`, `confirmedAt`, `feeAmount`, `grossAmount`,
> `tokenKrwPrice`, `tokenUsdPrice`, `reservedAmount`.
> 체인·단가가 **매칭 건마다 다르기 때문**에 주문 단위 값이 존재하지 않는다. 실효 환율이 필요하면
> `settledAmountKrw ÷ settledGrossAmount` 로 계산할 것.

##### ⚠️ 단위가 섞여 있다 — 가장 흔한 실수

```
settledAmount     = 20.806794000000000000   ← USDT (회원에게 반영할 금액)
orderAmount       = 30000                   ← KRW  (회원이 결제 요청한 금액)
settledAmountKrw  = 30000                   ← KRW  (실제 체결된 원화)
```

- **회원 잔액에 반영할 값은 `settledAmount` (USDT) 뿐이다.**
- `orderAmount` 를 크레딧하면 **원화 금액이 USDT 로 들어간다** — 30,000 USDT 를 지급하게 된다.
- 원화끼리 비교할 때만 `orderAmount` ↔ `settledAmountKrw` ↔ `unsettledAmountKrw` 를 쓴다.

##### JSON 예시 — `FULL` (30,000원 주문 전액 체결)

```json
{
  "eventType": "P2P_ORDER_COMPLETE",
  "eventId": "evt_p2po_pdo_32d94ea4da7b_FULL",
  "settlementMethod": "P2P",
  "result": "FULL",
  "transactionId": 1346,
  "partnerId": "50",
  "userId": "mouse777",
  "orderId": "260814164752-NNLNO-mouse777",
  "orderCode": "pdo_32d94ea4da7b",
  "transactionHash": "",
  "settledAmount": "20.806794000000000000",
  "settledCurrency": "USDT",
  "settledFeeAmount": "0",
  "settledGrossAmount": "20.806794000000000000",
  "settledAmountKrw": "30000",
  "orderAmount": "30000",
  "orderCurrency": "KRW",
  "unsettledAmountKrw": "0",
  "amount": "20.806794000000000000",
  "currencyType": "USDT",
  "krwAmount": "30000",
  "usdAmount": "20.806794000000000000",
  "legCount": 1,
  "settledLegCount": 1,
  "status": "COMPLETED",
  "closeReason": "ALL_SETTLED",
  "closedAt": "2026-08-14T16:48:12",
  "timestamp": "1786693692",
  "signature": "9b7e4f2c8a1d5e60b3c7a9f184d2e6301fa5c8b7d94e2036af518c7bd3e604a1"
}
```

##### JSON 예시 — `PARTIAL` (30,000원 주문 중 15,000원만 체결)

```json
{
  "eventType": "P2P_ORDER_COMPLETE",
  "eventId": "evt_p2po_pdo_22f37cbe04a0_PARTIAL",
  "settlementMethod": "P2P",
  "result": "PARTIAL",
  "transactionId": 1343,
  "partnerId": "50",
  "userId": "mouse777",
  "orderId": "260814160406-MAELH-mouse777",
  "orderCode": "pdo_22f37cbe04a0",
  "transactionHash": "",
  "settledAmount": "10.403397000000000000",
  "settledCurrency": "USDT",
  "settledFeeAmount": "0",
  "settledGrossAmount": "10.403397000000000000",
  "settledAmountKrw": "15000",
  "orderAmount": "30000",
  "orderCurrency": "KRW",
  "unsettledAmountKrw": "15000",
  "amount": "10.403397000000000000",
  "currencyType": "USDT",
  "krwAmount": "15000",
  "usdAmount": "10.403397000000000000",
  "legCount": 2,
  "settledLegCount": 1,
  "status": "PARTIALLY_SETTLED",
  "closeReason": "PARTIAL_EXPIRE",
  "closedAt": "2026-08-14T16:36:11",
  "timestamp": "1786691771",
  "signature": "c41a8d0f7b62e9354ad18c0be7f236915dc4a80e6b3f27194ca5d80e63b1f472"
}
```

##### `result` 의 의미

| `result` | 뜻 | 파트너 처리 |
|---|---|---|
| `FULL` | 요청 금액 **전액** 체결. `unsettledAmountKrw = "0"` | 주문 성공 처리 + `settledAmount` 크레딧 |
| `PARTIAL` | **일부만** 체결하고 주문이 종결됨. 나머지는 **더 이상 들어오지 않는다** | `settledAmount` 만큼 크레딧 + 주문을 부분완료/종료로 마감. 잔액 회수 요청 금지 |

> `PARTIAL` 에서도 `settledAmount` 는 **이미 파트너 잔고에 반영된 실제 금액**이다. 미체결분
> (`unsettledAmountKrw`)은 회원이 이체하지 않았거나 상대가 취소한 금액이므로 정산 대상이 아니다.

##### 종결 사유 (`closeReason`)

| 값 | 의미 | `result` |
|---|---|---|
| `ALL_SETTLED` | 전액 체결 완료 | `FULL` |
| `PARTIAL_EXPIRE` | 일부 체결 + 나머지 미체결로 만료 | `PARTIAL` |
| `PARTIAL_LATE_SETTLE` | 만료 후 뒤늦게 일부가 체결됨 | `PARTIAL` |
| `BUYER_CANCEL` | 구매자가 남은 부분을 취소 | `PARTIAL` |
| `ALL_CANCELLED` / `NO_SETTLEMENT_EXPIRE` / `UNFILLED_NO_LIQUIDITY` / `MATCH_FAILED` | 체결분 없음 | **웹훅 미발송** |

`closeReason` 은 참고용이다. **처리 분기는 `result` 로 하라** — 사유 코드는 향후 추가될 수 있다.

#### 4. 파트너가 구현할 것

##### (1) 멱등 처리 — 필수

배달 실패 시 **최대 5회 재시도**한다(30초 → 60초 → 5분 → 15분 → 60분). 파트너 서버가
2xx 를 반환하기 전에 타임아웃되면 **같은 이벤트를 두 번 이상 받을 수 있다.**

- 재시도는 **저장된 페이로드를 그대로** 재전송한다 — `eventId`·`timestamp`·`signature` 를 포함해
  모든 값이 처음과 동일하다.
- **`eventId` 를 유니크 키로 저장하고, 이미 처리한 값이면 크레딧하지 말고 2xx 만 반환**할 것.
- 한 주문은 두 번 종결되지 않으므로 `eventId` 는 주문당 1개다.

```
if (exists(eventId)) return 200;   // 중복 — 무시
credit(userId, settledAmount);     // USDT 크레딧
save(eventId);
return 200;
```

> 2xx 이외를 반환하거나 응답이 지연되면 재시도 대상이 된다. **처리 성공 시 반드시 2xx 를 즉시
> 반환**할 것.

##### (2) `result` 별 처리

- `FULL` → 주문 성공 마감.
- `PARTIAL` → 받은 만큼만 반영하고 주문을 **종료** 처리. 추가 입금을 기다리지 말 것.
- 두 경우 모두 **크레딧 금액은 `settledAmount` 하나**다. `result` 로 금액을 바꾸지 않는다.

##### (3) 서명 검증

**계산식은 기존 웹훅과 동일하다 — 검증 로직을 새로 만들 필요가 없다.**

```
HMAC-SHA256("{partnerId}|{transactionHash}|{amount}|{timestamp}", api_secret) → hex
```

- `transactionHash` 는 **항상 `""`** 이므로 두 구분자가 연달아 붙는다.
- `amount` 는 페이로드의 `amount` 필드(= `settledAmount`)를 **문자열 그대로** 쓴다. 숫자로
  파싱했다가 다시 문자열로 만들면 소수점 자릿수가 달라져 검증이 실패한다.

```
검증 데이터 예시: 50||20.806794000000000000|1786693692
                    ↑ transactionHash 가 빈 문자열
```

`settledAmount`·`result`·`orderAmount` 등 나머지 필드는 **서명 대상이 아니다.**

#### 5. 전환 안내 — 병행 기간 없음

| | |
|---|---|
| **적용 시점** | 배포 즉시 |
| **병행 발송** | **없음.** 원화결제 입금에 대한 `DEPOSIT_CONFIRMED` 는 배포 순간부터 발송이 중단된다 |
| **온체인·폰페이 입금** | 영향 없음 — 계속 `DEPOSIT_CONFIRMED` 로 온다 |
| **서명 방식** | 변경 없음 |
| **Webhook URL** | 변경 없음 — 기존 URL 로 `eventType` 만 다르게 도착한다 |

> ⚠️ **`P2P_ORDER_COMPLETE` 를 처리하지 않으면 원화결제 충전이 전부 미반영된다.** 재시도로
> 해결되는 종류의 실패가 아니다(파트너 서버가 200 을 주고 무시하면 재시도조차 없다).
> 원화결제를 쓰지 않는 파트너는 아무 조치도 필요 없다.

전환 확인 방법: 원화결제로 소액 1건을 결제해 `eventType == "P2P_ORDER_COMPLETE"` 수신과
`settledAmount` 크레딧을 확인하면 된다.

---

### 출금 — 종결 구분 필드

`WITHDRAWAL_COMPLETED` 는 `settlementMethod` 로 완료 경로를 알린다.

| `settlementMethod` | 의미 |
|---|---|
| `ONCHAIN` | 온체인 전송 확정. `transactionHash` 채워짐 |
| `P2P` | 원화 P2P 매칭으로 완료. `transactionHash` 는 `""` |

> P2P 출금의 온체인 TX 는 **파트너 간 정산** 트랜잭션이라 회원 출금과 무관하다. 그래서 해시를
> 싣지 않는다. **`transactionHash` 유무로 경로를 유추하지 말고 `settlementMethod` 로 판단할 것.**

`WITHDRAWAL_FAILED` 는 `reason` 으로 실패 원인을 알린다.

| `reason` | 의미 |
|---|---|
| `REJECTED` | 관리자 거부 |
| `CANCELLED_BY_PARTNER` | 파트너가 직접 취소 |
| `CANCELLED_BY_ADMIN` | 관리자 취소 — 회원 안내 필요 |
| `FAILED` | 온체인 TX 실패 |
| `EXHAUSTED` | 재시도 상한 초과 종결 |

`reasonDetail` 은 자유 텍스트이며 없으면 `null` 이다. `CANCELLED_BY_ADMIN` 의 경우 관리자가 취소
시 입력한 사유가 그대로 담기므로, **회원 안내 문구로 그대로 노출 가능**하다(미입력 시 `null`).

```json
{
  "eventType": "WITHDRAWAL_FAILED",
  "reason": "CANCELLED_BY_ADMIN",
  "reasonDetail": "출금 주소 오기재 — 회원 재요청 안내함"
}
```

`settlementMethod`·`reason`·`reasonDetail` 키는 **해당 없는 이벤트에도 항상 존재**하며 값만
`null` 이다(파싱 안정성).

### 출금 웹훅이 오지 않는 경우 — 정산 쉐어 출금

파트너 콘솔에서 **자기 정산 수익을 인출**하는 출금(정산 쉐어 출금)은 웹훅 3종을 **모두 발송하지
않는다.** 회원 거래가 아니라 파트너가 직접 실행한 행위이므로, 자기가 누른 결과를 웹훅으로 되돌려
받을 이유가 없다. 진행 상태는 파트너 콘솔의 정산/출금 화면에서 확인한다.

> 웹훅으로 회원 잔액을 자동 반영하는 파트너는 이 건이 오지 않는 것이 **정상**이다.
> 회원 출금(`USER_PAYOUT`)만 통지 대상이다.

### 변경 요약 (2026-08-17)

| 구분 | 변경 |
|---|---|
| 신설 이벤트 | **`P2P_ORDER_COMPLETE`** — 원화결제(P2P·TORQ 매칭) **주문 종결** 통지. 주문당 최대 1회 |
| 중단 | 원화결제 경로의 `DEPOSIT_CONFIRMED` **발송 중단** (병행 기간 없음) |
| 영향 없음 | 온체인 입금·폰페이 입금의 `DEPOSIT_CONFIRMED`, 출금 웹훅 3종 |
| 발송 제외 | 체결분이 **0** 인 주문(전량 미체결 만료/취소)은 미발송 |
| 멱등키 | `eventId` = `evt_p2po_{orderCode}_{result}` — 재시도 시에도 불변 |
| 서명식 | **변경 없음** (`partnerId\|txHash\|amount\|timestamp`, `txHash` 는 항상 `""`) |
| 파트너 필수 조치 | 이벤트 수신 핸들러 추가 + `settledAmount`(USDT) 크레딧 + `eventId` 중복 차단 |

### 변경 요약 (2026-08-13)

| 구분 | 변경 |
|---|---|
| 폐기된 이벤트 | `WITHDRAWAL_APPROVED`, `WITHDRAWAL_P2P_PENDING`, `WITHDRAWAL_CONFIRMED`, `WITHDRAWAL_REJECTED`, `WITHDRAWAL_CANCELLED`, `WITHDRAWAL_EXHAUSTED` |
| 대체 | 거부·취소·실패·소진 → `WITHDRAWAL_FAILED` + `reason` / 확정 → `WITHDRAWAL_COMPLETED` + `settlementMethod` |
| 입금 `amount` | gross → **net(실 크레딧)** |
| 입금 `krwAmount`/`usdAmount` | gross 환산 → **net 환산** (`amount` 와 동일 기준) |
| 입금 신설 필드 | `feeAmount`, `grossAmount`, `settlementMethod`(2026-08-16) |
| 발송 제외 | **정산 쉐어 출금**은 출금 웹훅 3종 모두 미발송 |
| 서명식 | **변경 없음** (`partnerId\|txHash\|amount\|timestamp`) — 단 입금은 새 `amount`(net) 기준 |

---

## 18. 에러 코드 전체 목록

### 공통 HTTP 에러

| HTTP | 코드 | 설명 |
|------|------|------|
| 400 | INVALID_PARAMETER | 파라미터 유효성 오류 |
| 401 | UNAUTHORIZED | 인증 토큰 없음/만료 |
| 403 | FORBIDDEN | 권한 없음 |
| 403 | ACCOUNT_SUSPENDED | 계정 정지 |
| 404 | NOT_FOUND | 리소스 없음 |
| 409 | CONFLICT | 상태 충돌 |
| 429 | RATE_LIMITED | 요청 한도 초과 |
| 500 | INTERNAL_ERROR | 서버 오류 |

### 도메인별 에러

| 도메인 | 에러 코드 | 설명 |
|--------|---------|------|
| 인증 | INVALID_CREDENTIALS | 이메일/비밀번호 불일치 |
| 인증 | INVALID_OTP | OTP 코드 불일치 |
| 인증 | OTP_EXPIRED | temp_token 만료 |
| 인증 | ACCOUNT_LOCKED | 로그인 연속 실패 잠금 |
| 인증 | WRONG_PASSWORD | 현재 비밀번호 불일치 |
| 인증 | PASSWORD_TOO_WEAK | 비밀번호 정책 미충족 |
| 인증 | SAME_PASSWORD | 현재와 동일한 비밀번호 |
| 입금 세션 | NO_AVAILABLE_ADDRESS | 할당 가능한 입금 주소 없음 |
| 입금 세션 | UNSUPPORTED_NETWORK | 파트너 미활성 네트워크 |
| 입금 세션 | UNSUPPORTED_DEPOSIT_METHOD | 허용되지 않은 입금 방식 |
| 출금 | INSUFFICIENT_BALANCE | 가용 잔액 부족 |
| 출금 | BELOW_MIN_WITHDRAWAL | 최소 출금액 미달 |
| 출금 | WHITELIST_REQUIRED | 화이트리스트 주소만 허용 |
| 출금 | ADDRESS_NOT_IN_WHITELIST | 화이트리스트에 없는 주소 |
| 출금 | INVALID_ADDRESS_FORMAT | 주소 형식 오류 |
| 화이트리스트 | ADDRESS_ALREADY_EXISTS | 동일 주소 이미 등록 |
| 정산 | INSUFFICIENT_SETTLEMENT_BALANCE | 정산 잔액 부족 |
| 하위 파트너 | FEE_RATE_OUT_OF_RANGE | 수수료율 범위 벗어남 |
| 하위 파트너 | CHAIN_NOT_AVAILABLE | 활성화되지 않은 네트워크/통화 |
| 하위 파트너 | EMAIL_ALREADY_EXISTS | 이메일 중복 |
| Axim | AXIM_NOT_CONFIGURED | Axim Pay 미연동 |

---

## 부록: 상태값 정의

### 입금 상태 (Deposit Status)

| 값 | 한글 | 색상 |
|----|------|------|
| DETECTED | 감지됨 | ⚪ 회색 |
| CONFIRMING | 확인 중 | 🔵 파랑 |
| CONFIRMED | 확인 완료 | 🟢 초록 |
| COLLECTING | 집금 중 | 🔵 파랑 |
| SETTLED | 정산 완료 | 🟢 초록 |
| FAILED | 실패 | 🔴 빨강 |

### 출금 상태 (Withdrawal Status)

| 값 | 한글 | 색상 |
|----|------|------|
| REQUESTED | 요청됨 | ⚪ 회색 |
| PENDING_APPROVAL | 승인 대기 | 🟡 노랑 |
| APPROVED | 승인됨 | 🔵 파랑 |
| REJECTED | 거부됨 | 🔴 빨강 |
| PROCESSING | 처리 중 | 🔵 파랑 |
| BROADCASTING | 브로드캐스팅 | 🔵 파랑 |
| CONFIRMED | 완료 | 🟢 초록 |
| FAILED | 실패 | 🔴 빨강 |
| CANCELLED | 취소 | ⚪ 회색 |

### 입금 세션 상태

| 값 | 한글 | 색상 |
|----|------|------|
| CREATED | 생성됨 | ⚪ |
| WAITING | 대기 중 | 🟡 |
| RECEIVED | 수신됨 | 🔵 |
| COMPLETED | 완료 | 🟢 |
| REFUNDED | 환불됨 | ⚪ |
| EXPIRED | 만료 | ⚪ |
| CANCELLED | 취소 | ⚪ |

### 가스비 인보이스 상태

| 값 | 한글 | 색상 |
|----|------|------|
| DRAFT | 초안 | ⚪ |
| ISSUED | 발행됨 | 🔵 |
| PAID | 납부완료 | 🟢 |
| OVERDUE | 연체 | 🟠 |
| CANCELLED | 취소 | ⚪ |

### Axim Pay 결제 상태

| 값 | 한글 | 색상 |
|----|------|------|
| REQUESTED | 요청 | ⚪ |
| PENDING | 대기 | 🟡 |
| CONFIRMED | 확인 | 🟢 |
| CANCELED | 취소 | ⚪ |
| DENIED | 거부 | 🔴 |
| FAILED | 실패 | 🔴 |
| EXPIRED | 만료 | ⚪ |

---

*본 문서는 restMetaGenerator로 생성한 openapi.json (2026-03-06 기준) 기반으로 작성되었습니다.*
*API 구현 완료 후 업데이트 필요 항목은 [TODO] 표시를 확인하세요.*
