# 웹훅 문서 실물 동기화 지침

작성 2026-08-18 · 대상 `cryptoments-admin/guide-ui/webhook.html` (배포처 https://docs.cryptoments.cc/webhook)
근거: 백엔드 코드 전수 조사 (2026-08-18). **아래 "확정 사실"이 정본이다 — 문서가 틀렸고 코드가 맞다.**

---

## 이 작업의 성격

파트너가 연동 근거로 삼는 문서다. **지금 문서는 오지 않을 이벤트 6종을 기다리라고 가르치고 있고,
실제로 나가는 신규 이벤트 1종(29필드)을 아예 안 알려준다.** 틀린 문장을 지우는 것이 추가보다 중요하다.

파일 구조: 한 파일에 EN 본문(HTML) + KO 사전(`window.I18N.init({...})` 인라인, :1935~).
`data-i18n` / `data-i18n-html` 키로 연결된다. **양쪽을 반드시 함께 고쳐라.** 한쪽만 고치면 언어 전환 시 옛 내용이 나온다.

---

# 확정 사실 (코드 근거)

## F1. 실제 발송 이벤트는 5종뿐

| eventType | 트리거 |
|---|---|
| `DEPOSIT_CONFIRMED` | 온체인 입금 확정 · 폰페이 입금 완료 |
| `WITHDRAWAL_REQUESTED` | 출금 요청 생성 직후 (status = `REQUESTED` 또는 `PENDING_APPROVAL`) |
| `WITHDRAWAL_COMPLETED` | ① 온체인 확정 ② P2P 전액 정산 종결 ③ P2P 전환·취소·강제정산 종결 |
| `WITHDRAWAL_FAILED` | 거부·취소·재시도소진·온체인실패 — `reason` 으로 구분 |
| `P2P_ORDER_COMPLETE` | P2P 입금 주문 종결 시 1회 |

## F2. 문서가 주장하지만 **발송되지 않는** 이벤트 6종 — 전부 삭제

```
WITHDRAWAL_APPROVED     코드에 문자열조차 없음. 승인은 웹훅이 없다
WITHDRAWAL_REJECTED     → WITHDRAWAL_FAILED + reason=REJECTED
WITHDRAWAL_CANCELLED    → WITHDRAWAL_FAILED + reason=CANCELLED_BY_ADMIN / CANCELLED_BY_PARTNER
WITHDRAWAL_EXHAUSTED    → WITHDRAWAL_FAILED + reason=EXHAUSTED
WITHDRAWAL_P2P_PENDING  해당 이벤트 자체가 존재하지 않음
WITHDRAWAL_CONFIRMED    enum 상수일 뿐 웹훅 eventType 아님. 온체인 확정도 WITHDRAWAL_COMPLETED 다
```

⚠️ **`WITHDRAWAL_CONFIRMED` 는 문서 전반(예제 페이로드·상태표·수신서버 예제·문제해결)에 12회 등장한다.
전부 잡아야 한다.** 남기면 파트너가 오지 않는 이벤트로 분기한다.

## F3. `WITHDRAWAL_FAILED` 의 `reason` 값 5종

| reason | 의미 |
|---|---|
| `REJECTED` | 관리자 거부 |
| `CANCELLED_BY_ADMIN` | 관리자 취소 |
| `CANCELLED_BY_PARTNER` | 파트너 자가 취소 |
| `EXHAUSTED` | 자동 재시도 상한 초과 종결 |
| `FAILED` | 온체인 TX 실패 |

`reasonDetail` — 자유 텍스트 상세. 둘 다 서명 대상 아님.

## F4. `settlementMethod` — 전 이벤트 공통 신규 필드

| 이벤트 | 값 |
|---|---|
| `DEPOSIT_CONFIRMED` | `ONCHAIN` · `INTERNAL` · `FIAT` |
| `WITHDRAWAL_COMPLETED` | `ONCHAIN` · `P2P` |
| `WITHDRAWAL_REQUESTED` / `WITHDRAWAL_FAILED` | `null` (키는 존재) |
| `P2P_ORDER_COMPLETE` | `P2P` 고정 |

## F5. `P2P_ORDER_COMPLETE` 전체 명세 (신규 섹션)

**의미**: P2P 입금 주문이 종결됐다. 주문은 여러 매칭 레그(P2P·TORQ·PARTNER)로 쪼개져 체결되며,
**레그 단위로는 입금 웹훅이 나가지 않는다.** 이 이벤트 하나가 주문 전체의 확정 결과다.
파트너는 **이 이벤트를 받고 충전/결제를 완료**해야 한다.

**eventId**: `evt_p2po_{orderCode}_{result}` (예: `evt_p2po_pdo_22f37cbe04a0_FULL`) — 결정론적. 멱등키로 쓸 것.

| 필드 | 타입 | 설명 |
|---|---|---|
| `eventType` | String | `P2P_ORDER_COMPLETE` 고정 |
| `eventId` | String | 멱등키 |
| `settlementMethod` | String | `P2P` 고정 |
| `result` | String | **`FULL`**(주문 전액 확정) \| **`PARTIAL`**(일부만 확정하고 종결) |
| `transactionId` | Long | **P2P 입금 주문 ID** (일반 입금의 deposit ID 와 다른 공간) |
| `partnerId` / `userId` | String | 파트너 ID · 파트너측 사용자 ID |
| `orderId` | String | 파트너가 넘긴 주문 ID (없으면 키 없음) |
| `orderCode` | String | 내부 주문 코드 |
| `transactionHash` | String | **항상 `""`** — 여러 레그를 묶은 결과라 단일 체인 해시가 없다 |
| `settledAmount` | String | **실제 확정 USDT (수수료 차감 후)** — 파트너가 사용자에게 반영할 값 |
| `settledCurrency` | String | `USDT` |
| `settledFeeAmount` | String | 구매자 부담 수수료 합 (USDT) |
| `settledGrossAmount` | String | 수수료 차감 전 합 (USDT) |
| `settledAmountKrw` | String | 확정 원화 합 |
| `orderAmount` | String | 주문 액면 원화 |
| `orderCurrency` | String | `KRW` |
| `unsettledAmountKrw` | String | 미확정 원화 = `orderAmount − settledAmountKrw` |
| `amount` | String | = `settledAmount` (공통 필드 관례 호환) |
| `currencyType` | String | `USDT` |
| `krwAmount` | String | = `settledAmountKrw` |
| `usdAmount` | String | = `settledAmount` |
| `legCount` | int | 전체 매칭 레그 수 |
| `settledLegCount` | int | 확정된 레그 수 |
| `status` | String | 주문 상태 (`COMPLETED` · `PARTIALLY_SETTLED` · `CANCELLED` · `EXPIRED`) |
| `closeReason` | String | 종결 사유 |
| `closedAt` | String | 종결 시각 `yyyy-MM-dd'T'HH:mm:ss` |
| `timestamp` / `signature` | String | 공통 |

**없는 필드(의도)**: `tokenKrwPrice` · `tokenUsdPrice` · `chainType` — 레그마다 환율·체인이 달라 주문 단위 값이 없다.

**발송되지 않는 경우**: 확정된 레그가 0건일 때(입금이 전혀 없는 만료·전부취소). 즉 **`EXPIRED`·`CANCELLED` 중 정산 0건은 아무 이벤트도 안 나간다.**

⚠️ **`transactionId` 는 일반 입금과 ID 공간이 다르다.** 멱등 테이블을 `eventType + transactionId` 로 잡으면
자연히 분리되지만, `transactionId` 만으로 조인하면 충돌한다. 문서에 명시할 것.

## F6. `depositMethod` — 실제 8종 (문서 6종)

기존 6종에 더해:

| 값 | 설명 |
|---|---|
| `TORQ` | TORQ LP 를 통한 원화→USDT 온램프 입금 |
| `P2P` | P2P 매칭(개인 출금자 ↔ 구매자) 입금 |

## F7. 문서에 없는 필드

| 필드 | 이벤트 | 설명 |
|---|---|---|
| `settlementMethod` | 전부 | F4 |
| `orderCode` | DEPOSIT_CONFIRMED, P2P_ORDER_COMPLETE | 내부 코드 |
| `grossAmount` | DEPOSIT_CONFIRMED | 수수료 차감 전 온체인 전송액 |
| `reason` / `reasonDetail` | 출금 3종 (FAILED 만 값 있음) | F3 |
| `eventId` | P2P_ORDER_COMPLETE | 멱등키 |

## F8. `amount` 의 의미가 바뀌었다 ⚠️

`DEPOSIT_CONFIRMED` 의 `amount` 는 **net(수수료 차감 후)** 이다. 온체인 전송액은 `grossAmount`.
문서는 이를 "transaction amount" 로만 적어 오해를 부른다. `krwAmount`/`usdAmount` 환산도 net 기준.

## F9. 입금 웹훅이 나가지 않는 경우 (신설 안내 필요)

```
P2P 주문의 개별 레그(P2P·TORQ·PARTNER)  → 레그마다 DEPOSIT_CONFIRMED 안 나감.
                                           주문 종결 시 P2P_ORDER_COMPLETE 1건으로 갈음
단독 TORQ 온램프 입금(P2P 주문 무관)      → DEPOSIT_CONFIRMED 나감 (depositMethod=TORQ)
```

## F10. 출금 웹훅이 나가지 않는 경우 (신설 안내 필요)

```
파생 출금 (P2P 잔여를 USDT 로 전환해 내보내는 출금)
  → 성공·실패 모두 웹훅 없음.
    원 출금이 이미 WITHDRAWAL_COMPLETED 로 통지됐으므로 중복 통지를 막는다
파트너 정산 쉐어 출금 (SETTLEMENT_WITHDRAW)
  → 웹훅 없음. 파트너 자산 이동이 아니라 수익 인출이다
```

## F11. 재시도 정책 — 문서 수치가 틀렸다

| 항목 | 문서 | 실제 |
|---|---|---|
| 재시도 간격 | 즉시 / 30초 / 1분 / 5분 / 15분 / **1시간** | 즉시 / 30초 / 1분 / 5분 / 15분 → 종결. **1시간은 도달하지 않는다** |
| 시도 횟수 | "5회 재시도" | **총 5회 시도** (최초 1 + 재시도 4) |
| 타임아웃 | "5초" | **설정값이 없다.** 프레임워크 기본값을 따른다 — 문서에서 구체 수치를 빼라 |
| 최초 발송 | "즉시" | 발송 잡이 10초 주기 폴링 → **최대 10초 지연** |
| 재시도 실제 지연 | — | 명목 간격 + 최대 60초(재시도 활성화 잡 주기) + 최대 10초 |
| 성공 판정 | — | HTTP **2xx**. 본문은 보지 않는다 |

> 문서에서 "5초 이내 응답이 없으면"을 근거로 삼는 문장들이 여러 곳에 있다. **"타임아웃"을 단정하는
> 수치를 지우고 "즉시 200 으로 응답하고 비동기 처리하라"는 권고만 남겨라.** 없는 보장을 적지 마라.

## F12. 서명 — 규칙 불변

`partnerId|transactionHash|amount|timestamp` HMAC-SHA256 hex. **변경 없음.**
단, `transactionHash` 가 `""` 인 이벤트가 늘었다(P2P_ORDER_COMPLETE 는 항상 `""`).
**수신한 필드값을 그대로 이어붙이면 검증된다**는 점을 명시하라. `settlementMethod`·`reason`·
`reasonDetail`·`metadata` 는 서명 대상이 아니다.

---

# 수정 작업

## W1. 4장 Event Types 전면 개편

- 이벤트 카드: `DEPOSIT_CONFIRMED` / `WITHDRAWAL_REQUESTED` / `WITHDRAWAL_COMPLETED` /
  `WITHDRAWAL_FAILED` / **`P2P_ORDER_COMPLETE`(신규)** 5장
- "1 deposit + 9 withdrawal" 류 문구 전부 교체
- `WITHDRAWAL_COMPLETED` 카드에 `settlementMethod` 로 온체인/P2P 를 구분한다는 설명
- `WITHDRAWAL_FAILED` 카드에 `reason` 5종 표
- **P2P_ORDER_COMPLETE 카드 신설** — F5 전량 + 예제 페이로드 (FULL 1개, PARTIAL 1개)
- 기존 `WITHDRAWAL_CONFIRMED` 예제 카드는 `WITHDRAWAL_COMPLETED` 로 교체

## W2. Field Reference 표 갱신

F4·F7·F8 반영. `feeAmount` 의 "항상 0" 한계는 유지(사실이다).

## W3. eventType 값 표 — 5행으로 축소

## W4. status 표 갱신

출금 상태별 "Webhook sent" 열을 실제에 맞춘다:

```
REQUESTED / PENDING_APPROVAL  → WITHDRAWAL_REQUESTED
APPROVED                      → 없음      ⚠️문서는 WITHDRAWAL_APPROVED 라고 함
REJECTED                      → WITHDRAWAL_FAILED (reason=REJECTED)
CANCELLED                     → WITHDRAWAL_FAILED (reason=CANCELLED_BY_*)
EXHAUSTED                     → WITHDRAWAL_FAILED (reason=EXHAUSTED)
FAILED                        → WITHDRAWAL_FAILED (reason=FAILED)
CONFIRMED / COMPLETED         → WITHDRAWAL_COMPLETED
P2P_PENDING                   → 없음      ⚠️문서는 WITHDRAWAL_P2P_PENDING 이라고 함
PROCESSING / BROADCASTING / STALE → 없음
```

## W5. depositMethod 표에 `TORQ` · `P2P` 추가 + F9 안내 박스

## W6. 5장 재시도 정책 — F11 로 교체

## W7. 7장 멱등성 — P2P_ORDER_COMPLETE 반영

- `eventType + transactionId` 규칙은 그대로 유효
- **P2P_ORDER_COMPLETE 의 `transactionId` 는 다른 ID 공간**임을 경고 박스로
- `eventId` 를 쓰면 더 간단하다는 안내(P2P 이벤트 한정)

## W8. 8장 수신 서버 예제 코드 갱신

JS·Python 예제의 `switch`/`if` 분기를 실제 5종으로. `WITHDRAWAL_CONFIRMED` 분기 제거,
`P2P_ORDER_COMPLETE` 분기 추가. **주석도 함께 고쳐라.**

## W9. 9장 문제해결 — 오지 않는 이벤트 항목 추가

```
증상  출금 승인 웹훅이 안 온다 / P2P 전환 웹훅이 안 온다
원인  그런 이벤트는 존재하지 않는다 (구 문서의 오기)
조치  승인은 통지 없음. 종결만 통지된다 — WITHDRAWAL_COMPLETED / WITHDRAWAL_FAILED 로 분기
```

```
증상  P2P 입금인데 DEPOSIT_CONFIRMED 가 안 온다
원인  P2P 주문의 레그별 입금은 통지하지 않는다
조치  주문 종결 시 오는 P2P_ORDER_COMPLETE 로 처리
```

## W10. 상단 Overview 문구 정정

"deposit confirmation, and every stage of the withdrawal lifecycle (request, approval, rejection,
cancellation, on-chain confirmation, failure, closure)" — **approval·rejection·cancellation 은 별도
이벤트가 아니다.** 실제에 맞게 다시 쓴다.

## W11. 문서 하단 버전·일자

`Last updated` 를 2026-08-18 로. 버전 표기가 있으면 함께 올린다.

---

## 작업 규칙

- **EN 본문과 KO 사전을 반드시 함께 수정.** `data-i18n` 키가 새로 필요하면 양쪽에 추가
- 기존 문서의 톤·구조·CSS 클래스를 그대로 따른다. **디자인을 바꾸지 마라**
- 예제 페이로드는 **F5 의 필드명·타입과 정확히 일치**해야 한다. 값은 그럴듯한 예시로
- 경고 박스(`⚠️`)·정보 박스(`ℹ️`) 는 기존 마크업 패턴 재사용
- **없는 보장을 쓰지 마라** — 타임아웃 수치처럼 코드에 근거가 없으면 적지 않는다
- Vue 앱 구조(이벤트 카드가 `v-if` 지연 렌더 + `I18N.recapture()`)를 깨지 마라 — 카드 추가 시
  기존 카드의 데이터 구조를 그대로 따를 것
- **백엔드 코드를 수정하지 마라.** 이 작업은 문서 전용

## 완료 기준

```
1  webhook.html 에 WITHDRAWAL_APPROVED / REJECTED / CANCELLED / P2P_PENDING /
   EXHAUSTED / CONFIRMED 가 0건 (단, WITHDRAWAL_FAILED 의 reason 설명 문맥은 예외 —
   'reason=REJECTED' 같은 표기는 허용)
2  P2P_ORDER_COMPLETE 카드 + 필드표 + 예제 2종 존재
3  settlementMethod 가 Field Reference 와 각 이벤트 카드에 존재
4  depositMethod 표에 TORQ · P2P 존재
5  재시도 표에 '1시간' 없음, 타임아웃 단정 수치 없음
6  EN/KO 전환 시 양쪽 다 새 내용 (data-i18n 키 누락 0)
7  브라우저에서 열었을 때 JS 에러 없이 렌더 (node 로 HTML 파싱 검증 또는 육안 확인)
```

## 보고

- 섹션별 수정 요약
- **KO 사전에 추가/수정한 키 목록**
- 완료 기준 1의 grep 결과
- 지침이 실제 파일 구조와 다른 지점 — 고치지 말고 보고
