# P2P 구매자 위젯 구현 지침서 — 오입금 방지 재설계

**작성**: 2026-08-24 · **상태**: 구현 대기 · **대상**: 서브에이전트
**범위**: `widget-ui/src/views/p2p.vue` + `widget-ui/src/locales/*`

**선행 문서 (반드시 먼저 읽어라)**
- `P2P_MISDEPOSIT_PREVENTION_UX_SPEC.md` — 원칙 8 + 화면별 상세 + 문구
- `P2P_DEPOSIT_FLOW_MAP.md` — 화면 목록 + 상태 전이 + 분기
- `P2P_DEPOSIT_BACKEND_PREP_GUIDE.md` — 백엔드 계약(§C·§D·§B-5)

> ⚠️ **`P2P_SEQUENTIAL_TRANSFER_GUIDE.md` 는 폐기됐다.** 그 문서의 v2 는 이번 논의 이전 버전이라
> 확정 설계와 어긋난다(매칭 대기 그만두기, 결과 화면, `/start` 호출이 없다). **참고하지 마라.**

---

## 0. 목적 — 이 작업이 무엇인지 착각하지 마라

**오입금을 막는 것이 유일한 목적이다.** 화면을 예쁘게 만드는 작업이 아니라, 사용자가 잘못된
금액을 잘못된 계좌로 보내는 경로를 화면에서 제거하는 작업이다. 모든 판단은 이 기준으로 한다.

실측된 오입금 패턴은 둘이다(상세는 SPEC §1).

1. **전액을 첫 계좌로** — 10만원이 5만/5만로 나뉘면 첫 계좌에 10만원을 다 보낸다. 가장 잦다.
2. **약정보다 적게 보냄**

진짜 범인은 sticky 요약 카드였다. 총액이 20px/700 로 스크롤 내내 상단에 고정되는데 정작 보낼
레그 금액은 15px 였다. **보내면 안 되는 숫자가 보낼 숫자보다 컸다.**

---

## 1. 현재 상태 — 무엇이 이미 되어 있나

`7a735ff`(2026-08-22, **배포 안 됨**)로 순차 이체 1차 재설계가 이미 들어가 있다.
`p2p.vue` 에 `p2p-split-notice` / `p2p-transfer` 가 있고, `p2p-verifying`·`p2p-dispute`·
`p2p-dispute-reviewing` 은 폐지됐다.

**이 작업은 그 위에 확정 설계를 얹는 것이다.** 처음부터 다시 만들지 마라.

### 바꿔야 할 8가지

| # | 지금 | 확정 |
|---|---|---|
| W1 | 이체 화면 보조 버튼이 `전체 보기` | **`그만두기`** — 체크 후에는 사라진다 |
| W2 | `전액 138,000원을 보내면 안 됩니다` | **숫자 제거** — `전액을 보내면 안 됩니다` |
| W3 | 카드마다 분쟁 CTA | **버튼 없음** — 목록 하단 조용한 링크 하나 |
| W4 | `p2p-split-notice` 가 고지 전용 | **진행 현황 화면의 상태 A** 로 통합. 1곳이면 매칭 완료 통지 |
| W5 | 대기 화면 하단 `닫기` 전폭 | **하단 버튼 없음** — `나중에 확인하기` 링크 |
| W6 | 자료 제출이 사진 첨부만 | **필수 4 / 반려 3 명시 + 예시 그림** |
| W7 | 기존 success/failed 유지 | **결과 화면 2종 재설계** (원화 주, 매칭별 분리, 사유 명시) |
| W8 | 매칭 대기에 출구 없음 | **버튼은 두지 않되** "창을 닫아도 됩니다" 안내 |
| **W9** | — | **`POST /order/{code}/start` 호출 신설** (아래 §2, 필수) |

---

## 2. 백엔드 계약 — 이번에 새로 생긴 것

백엔드 커밋 `a36710c`·`7c76990` 로 아래가 추가됐다. **배포 시점은 UI 확인 직전이다** —
구현 중에는 응답에 없을 수 있으니 **없을 때도 화면이 깨지지 않게** 하라.

### 2-1. ☠️ 착수 신고 — 반드시 호출해야 한다

```
POST /widgets/api/p2p/order/{orderCode}/start
→ P2pWidgetMatchResponse (주문 단위 DTO, 취소·조회와 동일)
```

- **언제**: 진행 현황 화면(상태 A)에서 사용자가 `입금 시작` 을 누를 때 **1회**
- **멱등**: 이미 찍혀 있으면 no-op 200
- **비-`MATCHED` 상태면 409** — 에러코드는 `P2P_ORDER_NOT_CANCELLABLE` 를 재사용한다(별도 코드 없음).
  ⚠️ **위젯은 이 409 를 "이미 종결된 거래" 로 해석해 종결 화면으로 보내라.** 원인 불명 에러로
  띄우면 안 된다.

☠️ **이 호출을 빠뜨리면 안 된다.** 서버에 착수 기한 가드가 들어가 있고(현재 스위치 OFF),
켜지는 순간 **시작 신호가 없는 주문은 3분 뒤 전체 취소**된다. 신호를 안 보내는 위젯이 남아 있으면
그 사용자들의 거래가 죽는다.

**모든 진입 경로에서 호출해야 한다** — 일반 위젯, `/widgets/matching-links/*`,
`/widgets/p2p/links/*` 전부. 링크로 들어온 화면에서 빠뜨리기 쉽다.

### 2-2. 레그 응답에 추가된 필드 (`matches[]` 요소)

| 필드 | 타입 | 의미 |
|---|---|---|
| `transferReportedAt` | `String` | 이체 완료 신고 시각. `LocalDateTime#toString()`(오프셋 없음). **`CREATED` 레그는 `null` 이 정상** |
| `transferElapsedSeconds` | `Long` | 신고 후 경과 초 — **서버 시계 기준**. 신고 전이면 `null` |

⚠️ **경과 시간을 클라이언트 타이머로 재지 마라.** 새로고침 때 0 으로 돌아간다.
`transferElapsedSeconds` 를 기준으로 표시하고, 화면에 머무는 동안의 증가만 로컬로 더한다.

병합 카드(`groupMatchCodes`)는 그룹 내 **가장 이른** 신고 시각이 온다.

### 2-3. 주문 응답에 추가된 필드

| 필드 | 타입 | 의미 |
|---|---|---|
| `closeReason` | `String` | 주문 종결 사유 **코드** |

**서버는 코드만 준다. 문구는 위젯 i18n 이 갖는다.** 내부 용어를 그대로 노출하지 마라.

값역 (10개):

| 코드 | 뜻 | 구매자용 문구(예시) |
|---|---|---|
| `ALL_SETTLED` | 전부 정산 | 충전이 완료되었어요 |
| `ALL_CANCELLED` | 전부 취소 | 거래가 취소되었습니다 |
| `PARTIAL_EXPIRE` | 일부만 정산 + 만료 | 일부만 완료되었습니다 |
| `PARTIAL_LATE_SETTLE` | 지각 정산으로 일부만 | 일부만 완료되었습니다 |
| `NO_SETTLEMENT_EXPIRE` | 정산 0 + 만료 | 시간이 지나 거래가 종료되었습니다 |
| `UNFILLED_NO_LIQUIDITY` | 거래 상대 없음 | 거래 상대를 찾지 못했습니다 |
| `BUYER_CANCEL` | 구매자 취소 | 요청하신 대로 취소되었습니다 |
| `ADMIN_CANCEL` | 관리자 취소 | 고객센터 처리로 취소되었습니다 |
| `MATCH_FAILED` | 매칭 실패 | 거래 성립에 실패했습니다 |
| `START_TIMEOUT` | **착수 기한 초과** | 시간 안에 시작하지 않아 취소되었습니다 |

⚠️ **없는 코드가 와도 화면이 깨지면 안 된다.** 매핑에 없으면 일반 종결 문구로 폴백하라.

### 2-4. 이미 있던 것 (재확인)

`disputeWaitingOn`(`DEPOSITOR`/`WITHDRAWER`), `groupMatchCodes`, `legCount`, `usdtAmount`,
`disputeReason`, `disputeSubmittedBy`, `hasEvidence`, `remainingSeconds`.

☠️ **`resolveMemo` 는 응답에 없고 앞으로도 없다** — 관리자 내부 문구다. 화면에 그리려 하지 마라.

---

## 3. 화면 구현

**상세 구성·문구·시각 위계는 `P2P_MISDEPOSIT_PREVENTION_UX_SPEC.md` §3 이 정본이다.**
여기서는 코드로 옮길 때 걸리는 것만 적는다.

### 3-1. 진행 현황(`p2p-home`) — 하나의 화면, 여섯 상태

`p2p-deposit` 을 이 역할로 확장한다. 상태는 **서버 상태에서 파생**한다(원칙 7).

| 상태 | 조건 | 하단 |
|---|---|---|
| 0. 매칭 대기 | 레그 0개 | **버튼 없음** + "창을 닫아도 됩니다" 안내 |
| A. 매칭 직후 | 전 레그 `CREATED` | `그만두기` / **`입금 시작`** ← 여기서 `/start` 호출 |
| B. 일부 이체 | `CREATED` 와 그 외 혼재 | `그만두기` / `이어서 입금` |
| C. 확인 대기 | 전 레그 신고, 미확인 존재 | 버튼 없음 + `나중에 확인하기` |
| D. 분쟁 발생 | `DISPUTED` · 내 차례 존재 | 버튼 없음 + 절차 안내 배너 |
| E. 검토 대기 | `DISPUTED` · 자료 제출됨 | 버튼 없음 + `나중에 확인하기` |

**우선순위** — 겹치면 위에서부터: `D` > `B` > `E` > `C` > `A` > `0`

**상태 0 에 버튼을 두지 않는다 (확정)**. 취소 버튼을 두면 매칭 워커와 경합하는 창이 생긴다
(BACKEND_PREP §B 개정 참조). 실제로 할 수 있는 게 기다리는 것뿐이므로 **안내로 그 사실을 분명히
한다** — "이 창을 닫아도 매칭은 계속됩니다".

⚠️ **상태 B 의 상단 숫자는 남은 금액이 주(主)다.** `남은 금액 88,000원 / 138,000원`.
총액만 크게 두면 중간에 다시 총액을 보내는 사고가 난다.

**1곳 매칭이면 상태 A 의 성격이 바뀐다** — 경고가 아니라 매칭 완료 통지(`거래 상대를 찾았어요`).
진행 표시·분할 경고·합계 줄을 전부 없앤다. **화면을 건너뛰지는 않는다.**

### 3-2. 순차 이체(`p2p-transfer`)

☠️ **계좌번호는 이 화면에만 존재한다.** 홈 카드에도, 매칭 결과 목록에도 넣지 마라.
**이건 미관이 아니라 자금 안전 제약이다** — 착수 기한 가드가 켜지면 시작 전에 계좌를 보고 송금한
구매자는 3분 뒤 주문이 종결되어 **분쟁조차 걸 수 없다**(`submitDispute` 가 종결 주문을 거부).

- 총액 0회. **부정형도 금지** — `전액을 보내면 안 됩니다`(숫자 없이)
- 레그 금액 3회 — 금액 블록 / 복사 버튼 라벨 / 완료 체크 문구
- 다음 레그 계좌 **DOM 부재**
- 하단: 체크 전 `그만두기` + 비활성 `다음 계좌로` / 체크 후 **`다음 계좌로` 전폭**
- 체크 후 하단 문구 교체 — `보냈다고 표시했기 때문에 이 건은 취소할 수 없습니다.`

**`전체 보기` 를 두지 마라.** 진행 위치는 상단 `2 / 3` 과 진행바가 대신한다.

### 3-3. 자료 제출 — 카드 안 펼침

전체화면을 만들지 마라. 카드가 그 자리에서 늘어난다.

필수 4 / 반려 3 을 화면에 명시하고, **반려 예시는 작은 그림으로** 보여준다(글로만 쓰면 안 읽힌다).
`보낸 금액 — 30,000원` 처럼 **해당 레그 금액을 요건 문장에 박아라** — 3건이 걸려 있을 때 맞는
사진을 고르게 한다.

제출 후 **화면 전환 없이** 같은 카드가 `검토 중` 으로 바뀐다.

### 3-4. 결과 — 인정 / 취소 2종

**분쟁 결과는 `CONFIRM` / `CANCEL` 둘뿐이다.** "자료 반려"는 결과가 아니라 진행 중 상태다
(헤더 `status` 가 `WAITING_EVIDENCE` ↔ `WAITING_JUDGMENT` 를 오간다).

공통 규칙 3:
1. **원화가 주, USDT 는 부기** — 원화 결제다
2. **합산하지 않는다** — `완료된 거래` / `취소된 거래` 섹션을 갈라 **매칭별로 나열**
3. **사유를 명시한다** — 상단 사유 블록 + 해당 카드 안 문장, 두 곳

취소 결과 하단은 `닫기` 하나. **`부족분 다시 충전` 같은 후속 액션 버튼을 두지 마라.**
`이 판정은 되돌릴 수 없습니다` 를 명시하고, 이의는 조용한 링크로만.

### 3-5. 분쟁 진입 — 버튼을 두지 않는다

카드에 분쟁 CTA 를 두지 마라. 상시 노출된 버튼은 "이걸 눌러야 진행되나"로 읽히고 설명하기도 어렵다.

- 목록 하단에 작은 회색 글씨 하나 — `입금했는데 확인이 안 되나요?`
- 누르면 안내 후 **한 번 더 확인**하고 분쟁을 연다(누르는 즉시 되돌릴 수 없는 분쟁이 열린다)
- 지연 카드에는 사유 문장만 — `판매자에게 다시 요청했습니다. 계속 확인되지 않으면 저희가 직접 처리합니다.`
- `자료 올리기` 버튼은 **시스템이 분쟁으로 넘긴 뒤에만** 나타난다

---

## 4. 하지 말 것

- 이체 화면에 총액·합계·"총 N원 중" 류 문구 (**부정형 포함**)
- 홈 카드·매칭 결과 목록에 **계좌번호** (자금 안전 제약, §3-2)
- 홈 목록을 덮는 전체화면을 새로 만드는 것 (이체 하나만 예외, 복귀 있음)
- **매칭 대기(상태 0)에 취소 버튼**을 두는 것
- 진행 위치를 `localStorage`·`sessionStorage`·`ref` 에 저장하는 것
- 결과 화면에서 매칭을 **합산**하거나 **USDT 를 원화보다 크게** 쓰는 것
- 체크박스 체크 **후에** `그만두기` 를 남겨두는 것 (실제 보낸 돈이 취소 대상이 된다)
- 경과 시간을 **클라이언트 타이머로만** 재는 것
- `/start` 호출을 **일부 진입 경로에서만** 하는 것
- `resolveMemo` 를 그리려 하는 것 (응답에 없다)
- `disputeMatch` 폴백만 지우고 `groupMatchCodes` 조회를 안 넣는 것 (병합 카드 회귀)

---

## 5. 완료 기준

1. 3곳 매칭에서 상태 A → `p2p-transfer`(1/3 → 3/3) → 상태 C 순서로 진행된다
2. `입금 시작` 이 **`POST /order/{code}/start` 를 호출**한다. 링크 진입 경로에서도 호출된다
3. `/start` 가 409 를 주면 **종결 화면**으로 간다 (원인 불명 에러 아님)
4. `p2p-transfer` 에 **총액이 어디에도 없다** — 숫자 검색으로 확인
5. `p2p-transfer` 에 **다음 레그 계좌가 DOM 에 없다**
6. **홈·매칭 결과 어디에도 계좌번호가 없다**
7. 체크 전후로 하단 버튼과 하단 문구가 바뀐다 (체크 후 `그만두기` 사라짐)
8. 상태 B 상단이 **남은 금액**을 주 숫자로 보여준다
9. 매칭 대기(상태 0)에 **버튼이 없고** "창을 닫아도 됩니다" 안내가 있다
10. 확인 대기 카드에 **경과 시간**이 뜨고, **새로고침해도 0 으로 돌아가지 않는다**
11. 2건 분쟁일 때 **두 건 다 자료 제출이 가능하다** (사고 A 회귀 테스트)
12. 자료 제출 시 **화면 전환이 없고** 같은 카드가 검토 중으로 바뀐다
13. 병합 카드 하위 코드로 분쟁을 열어도 **올바른 카드**가 펼쳐진다
14. 결과 화면이 **매칭별로 분리**되고 **사유**가 뜨며 **원화가 주**다
15. `closeReason` 에 없는 코드가 와도 화면이 깨지지 않는다
16. 새 필드(`transferReportedAt` 등)가 **응답에 없어도** 화면이 깨지지 않는다
17. 재진입 시 떠난 자리로 복원되고 레그 순서가 동일하다
18. ko / en 키가 1:1 이다
19. `npm run build:prod` 통과 (⚠️ `build` 스크립트는 없다)

---

## 6. 검증

**시나리오 프리뷰는 이번 범위에서 제외한다**(리스크·분량 축소).
기존 `/widget-preview` 의 `?previewStep=` mock 은 유지하되, **폐지·신설된 화면에 맞춰 갱신**하라.

실제 흐름 검증은 **백엔드 배포 후 로컬 위젯을 운영 API 에 붙여** 수행한다.

☠️ **`.env.development` 가 운영 API 를 가리킨다** (`VITE_API=https://api.cryptoments.cc/widgets`).
`serve:dev` 로 띄우면 **진짜 주문과 진짜 매칭이 생긴다.** 판매자가 실제로 돈을 기다린다.
검증은 **소액으로**, 검증 항목을 미리 정해두고 하라.

CORS 는 `*` 라 로컬에서 운영 API 호출이 된다(`SecurityConfig.java:45`).

---

## 7. 작업 규칙

- **git 쓰기 명령 금지** — 커밋은 오케스트레이터가 한다
- `widget-ui` 외 모듈을 건드리지 마라. 백엔드는 이미 push·배포 대기 상태다
- 작업트리에 다른 세션의 미커밋 변경이 있으면 **되돌리지 마라**
- ko / en 은 **키 1:1** 유지. 한쪽에만 추가하면 반려
- 기존 코드 관례를 따라라 — 버튼 클래스 조합, 화면 골격(`widget_scroll` > `widget_layout` >
  내용, 하단 `widget_nav`), 색(`#F57C2C` / `#061C3D` / `#5A6480` / `#D83B2D`), i18n 은 `p2p.*`
- 화면 전환 변수는 `currentStep` 하나, setter 는 `goToStep()`
