# 폰페이 위젯 개발 가이드

> 외부 위젯 개발자용. 서버 API는 모두 배포 완료.
> Base URL: `https://api.cryptoments.cc`

---

## 전체 흐름 요약

```
고객이 링크 클릭
  https://widget.cryptoments.cc/phonepay/{linkCode}
       │
       ▼
  ① 링크 정보 조회 (인증 불필요)
  GET /widgets/phonepay/links/{linkCode}
  → accessToken, amount, partnerName, phonepayEnabled, axim 정보 수신
  → 응답 분기:
     · aximEnabled = false     → eKYC 스킵, ④ 폰페이 세션 생성으로 직행
     · aximConnected = false   → Axim 지갑 연결 페이지 (siteId, connectId 사용)
     · aximConnected = true    → ② eKYC 확인으로 진행
       │
       ▼
  ② eKYC 인증 확인 (accessToken 필요)
  GET /widgets/api/axim/ekyc/status
  → verified: true → 다음 단계
  → verified: false → eKYC 안내 화면
       │
       ▼
  ③ 송금인 정보 조회
  GET /widgets/api/axim/ekyc/info
  → "홍길동 님 (KB국민은행)" 표시
  → bankMatched: true → 다음 단계
  → bankMatched: false → "지원하지 않는 은행" 안내
       │
       ▼
  ③-1 (선택) 지원 은행 목록 조회
  GET /widgets/api/phonepay/banks
  → 지원 은행 리스트 표시 (bankMatched=false 안내 화면 등에 활용)
       │
       ▼
  ④ 폰페이 세션 생성
  POST /widgets/api/phonepay/sessions
  → { amount, linkCode }
  → 수취인 정보(이름/전화번호/은행) + 타이머 수신
       │
       ▼
  ⑤ 대기 화면 (수취인 정보 + 카운트다운)
  "아래 연락처로 연락처 이체를 실행해주세요"
       │
       ▼
  ⑥ 상태 폴링 (3~5초 간격)
  GET /widgets/api/phonepay/sessions/{id}
  → WAITING: 계속 폴링
  → MATCHED: "매칭됨" 표시, 계속 폴링
  → COMPLETED: ✅ 완료 화면, 폴링 중단
  → FAILED/EXPIRED/CANCELLED: 실패 화면, 폴링 중단
       │
       ▼
  ⑦ (선택) 사용자가 취소 버튼 클릭
  POST /widgets/api/phonepay/sessions/{id}/cancel
  → WAITING 상태에서만 가능
```

---

## API 상세

### ① 링크 정보 조회

```
GET /widgets/phonepay/links/{linkCode}
```

**인증 불필요.** 위젯 페이지 진입 시 최초 호출.

#### 응답 (200)

```json
{
  "linkCode": "plnk_e324daefd85c414d",
  "partnerName": "테스트 파트너",
  "amount": 50000,
  "partnerUserId": "test123",
  "partnerReference": "ORDER-001",
  "status": "ACTIVE",
  "isUsable": true,
  "expiresAt": "2026-04-29T17:00:00",
  "createdAt": "2026-04-29T01:00:00",
  "phonepayEnabled": true,
  "accessToken": "eyJhbGciOiJIUzI1NiJ9...",
  "aximEnabled": true,
  "aximSiteId": "12345",
  "aximConnectId": "31323a7465737431323300",
  "aximConnected": true,
  "aximStatus": "CONNECTED",
  "aximConnectWalletToken": "cwt_xxxxxxxx"
}
```

| 필드 | 타입 | 설명 |
|------|------|------|
| `linkCode` | String | 링크 코드 |
| `partnerName` | String | 파트너명 (화면 상단 표시용) |
| `amount` | Long \| null | 결제 금액 (원). **null이면 고객이 직접 입력** |
| `partnerUserId` | String | 고객 ID |
| `partnerReference` | String \| null | 주문번호 등 참조 코드 |
| `status` | String | `ACTIVE` / `USED` / `EXPIRED` / `CANCELLED` / `DEACTIVATED` |
| `isUsable` | Boolean | **true일 때만 결제 진행 가능** |
| `expiresAt` | String \| null | 링크 만료 시각 (ISO 8601). null이면 무기한 |
| `phonepayEnabled` | Boolean | 파트너의 폰페이 설정 활성화 여부 |
| `accessToken` | String | **이후 모든 API 호출에 사용할 Bearer 토큰** (isUsable=true일 때만 발급) |
| `aximEnabled` | Boolean | Axim 연동 활성화 여부 |
| `aximSiteId` | String \| null | Axim 사이트 ID (eKYC 페이지 이동용) |
| `aximConnectId` | String \| null | Axim 연결 ID (eKYC API 호출용) |
| `aximConnected` | Boolean \| null | Axim 지갑 연결 완료 여부 |
| `aximStatus` | String \| null | Axim 연결 상태 (`CONNECTED` / `INACTIVE`) |
| `aximConnectWalletToken` | String \| null | Axim 지갑 연결 토큰 (연결 완료 시) |

#### 화면 분기

| 조건 | 동작 |
|------|------|
| `isUsable = false` | 상태별 안내: USED → "이미 사용된 링크", EXPIRED → "만료된 링크", 등 |
| `phonepayEnabled = false` | "폰페이를 사용할 수 없습니다" 안내 |
| `aximEnabled = false` | eKYC/지갑 연결 단계 스킵, accessToken 저장 → ④ 폰페이 세션 생성으로 직행 |
| `aximConnected = false` | Axim 지갑 연결 페이지로 이동 (`aximSiteId`, `aximConnectId` 사용) |
| `aximConnected = true` | accessToken 저장 → ② eKYC 확인으로 진행 |

#### 에러

| HTTP | 의미 |
|------|------|
| 404 | 링크 코드가 존재하지 않음 |

---

### ② eKYC 인증 확인

```
GET /widgets/api/axim/ekyc/status
Authorization: Bearer {accessToken}
```

#### 응답 (200)

```json
{
  "verified": true,
  "status": "VERIFIED"
}
```

| 필드 | 타입 | 설명 |
|------|------|------|
| `verified` | Boolean | **이 값만 확인.** true면 인증 완료 |
| `status` | String | `VERIFIED` / `UNVERIFIED` / `VERIFYING` / `REJECTED` / `NOT_CONFIGURED` / `NO_USER_ID` / `API_ERROR` |

#### 화면 분기

| verified | 동작 |
|----------|------|
| `true` | → ③ eKYC 정보 조회 |
| `false` | eKYC 인증 안내 화면. Axim eKYC 페이지로 이동 유도 |

> 이 API는 예외를 던지지 않음. 어떤 상황에서도 200 + `verified: false`로 응답.

---

### ③ 송금인 정보 조회

```
GET /widgets/api/axim/ekyc/info
Authorization: Bearer {accessToken}
```

#### 응답 (200)

```json
{
  "name": "홍길동",
  "phone": "01012345678",
  "bankCode": "004",
  "bankName": "KB국민은행",
  "bankMatched": true
}
```

| 필드 | 타입 | 설명 |
|------|------|------|
| `name` | String | 인증된 실명 |
| `phone` | String | 휴대폰 번호 |
| `bankCode` | String | 금융기관 코드 (3자리) |
| `bankName` | String | 금융기관명 |
| `bankMatched` | Boolean | 송금 가능 은행 여부. eKYC 인증 은행이 BanqPipe 지원 목록에 포함되면 `true` |

#### 화면 분기

| 조건 | 동작 |
|------|------|
| `bankMatched = true` | 정상 진행 → 금액 확인/입력 후 ④ 세션 생성 |
| `bankMatched = false` | **"지원하지 않는 은행입니다"** 경고 표시. 진행 차단 권장 |

> `bankMatched`가 `false`인 경우 세션 생성은 가능하지만, 연락처 이체가 정상 처리되지 않을 수 있습니다. 위젯에서 사전 차단을 권장합니다.

#### 화면 표시

```
홍길동 님 (KB국민은행)
```

링크에 금액이 고정되어 있으면 → 바로 ④ 세션 생성.
금액이 null이면 → 금액 입력 필드 표시 후 ④ 세션 생성.

#### 에러

| HTTP | 코드 | 의미 |
|------|------|------|
| 404 | `AXIM_SETTINGS_NOT_FOUND` | Axim 설정 없음 |
| 404 | `EKYC_NOT_VERIFIED` | eKYC 미인증 (②에서 먼저 확인 권장) |

---

### ③-1 지원 은행 목록 조회 (선택)

```
GET /widgets/api/phonepay/banks
Authorization: Bearer {accessToken}
```

BanqPipe가 연락처 이체를 지원하는 은행 목록을 반환합니다.

#### 응답 (200)

```json
[
  {
    "code": "020",
    "name": "우리은행",
    "category": "시중은행",
    "macroSupported": true
  },
  {
    "code": "088",
    "name": "신한은행",
    "category": "시중은행",
    "macroSupported": true
  },
  {
    "code": "004",
    "name": "KB국민은행",
    "category": "시중은행",
    "macroSupported": true
  }
]
```

| 필드 | 타입 | 설명 |
|------|------|------|
| `code` | String | KFTC 3자리 은행 코드 |
| `name` | String | 은행명 |
| `category` | String | 은행 분류 (시중은행, 특수은행, 지방은행 등) |
| `macroSupported` | Boolean | 매크로 자동 처리 지원 여부 |

#### 활용

| 용도 | 설명 |
|------|------|
| `bankMatched = false` 안내 | "지원하지 않는 은행" 화면에서 지원 은행 리스트 표시 |
| 사전 안내 | 송금 전 지원 은행 확인 UI |

> 이 API는 선택 사항입니다. `bankMatched` 필드(③)만으로도 은행 지원 여부를 판단할 수 있습니다.
> BanqPipe API 장애 시 빈 배열(`[]`)을 반환합니다.

---

### ④ 폰페이 세션 생성

```
POST /widgets/api/phonepay/sessions
Authorization: Bearer {accessToken}
Content-Type: application/json
```

#### 요청

```json
{
  "amount": 50000,
  "linkCode": "plnk_e324daefd85c414d"
}
```

| 필드 | 타입 | 필수 | 설명 |
|------|------|------|------|
| `amount` | Long | ✅ | 입금 금액 (원, KRW). 정수만 |
| `linkCode` | String | 선택 | 링크에서 진입한 경우 전달. 세션-링크 연결용 |

> **송금인 정보는 서버가 eKYC에서 자동 조회** — 위변조 차단 목적.

#### 응답 (200)

```json
{
  "sessionId": 1,
  "banqpipeSessionId": "dp_a1b2c3d4e5f6",
  "status": "WAITING",
  "amount": 50000,
  "senderName": "홍길동",
  "recipientName": "김철수",
  "recipientPhone": "01098765432",
  "recipientBank": "국민은행",
  "expiresAt": "2026-04-29T17:30:00",
  "remainingSeconds": 3600
}
```

| 필드 | 타입 | 설명 |
|------|------|------|
| `sessionId` | Long | **이후 ⑤/⑥/⑦에서 사용할 세션 ID** |
| `banqpipeSessionId` | String | BanqPipe 세션 ID (참고용) |
| `status` | String | 항상 `WAITING` (생성 직후) |
| `amount` | Long | 입금 금액 (원) |
| `senderName` | String | 송금인 이름 (eKYC) |
| `recipientName` | String | **수취인 이름** — 화면 표시 |
| `recipientPhone` | String | **수취인 전화번호** — 연락처 이체 대상 |
| `recipientBank` | String | **수취인 은행명** — 화면 표시 |
| `expiresAt` | String | 만료 시각 (ISO 8601). BanqPipe 기준 1시간 |
| `remainingSeconds` | Long | 남은 시간 (초) |

#### 화면 표시 (대기 화면)

```
수취인: 김철수 (국민은행)
연락처: 010-9876-5432
금액: ₩50,000

위 연락처로 연락처 이체를 실행해주세요.
[타이머: 59:59]  [취소]
```

#### 에러

| HTTP | 코드 | 의미 |
|------|------|------|
| 404 | `AXIM_SETTINGS_NOT_FOUND` | Axim 설정 없음 |
| 404 | `EKYC_NOT_VERIFIED` | eKYC 미인증 |
| 404 | `PHONEPAY_SETTINGS_NOT_FOUND` | 폰페이 미설정 |
| 409 | `PHONEPAY_NOT_CONFIGURED` | 폰페이 비활성화 |
| 404 | `PHONEPAY_LINK_NOT_FOUND` | linkCode가 유효하지 않음 |
| 409 | `PHONEPAY_LINK_NOT_USABLE` | 링크가 만료/취소/비활성 상태 |
| 500 | `PHONEPAY_API_FAILED` | BanqPipe API 호출 실패 |

---

### ⑤ 세션 상태 폴링

```
GET /widgets/api/phonepay/sessions/{sessionId}
Authorization: Bearer {accessToken}
```

#### 응답 (200)

④와 동일한 구조. `remainingSeconds`가 실시간 갱신됨.

```json
{
  "sessionId": 1,
  "banqpipeSessionId": "dp_a1b2c3d4e5f6",
  "status": "WAITING",
  "amount": 50000,
  "senderName": "홍길동",
  "recipientName": "김철수",
  "recipientPhone": "01098765432",
  "recipientBank": "국민은행",
  "expiresAt": "2026-04-29T17:30:00",
  "remainingSeconds": 1800
}
```

#### 상태별 동작

| status | 화면 | 폴링 |
|--------|------|------|
| `WAITING` | 대기 화면 (수취인 정보 + 카운트다운) | **계속** |
| `MATCHED` | "SMS 매칭됨 — 입금 처리 중" 표시 | **계속** |
| `COMPLETED` | ✅ 입금 완료 | **중단** |
| `FAILED` | ❌ 입금 실패 | **중단** |
| `EXPIRED` | ⏰ 시간 만료 | **중단** |
| `CANCELLED` | 취소됨 | **중단** |

> **폴링 중단 조건**: `COMPLETED`, `FAILED`, `EXPIRED`, `CANCELLED` — 이 4개는 종료(terminal) 상태.

#### 에러

| HTTP | 코드 | 의미 |
|------|------|------|
| 404 | `PHONEPAY_SESSION_NOT_FOUND` | 세션 없음 또는 다른 파트너 소유 |

---

### ⑥ 세션 취소

```
POST /widgets/api/phonepay/sessions/{sessionId}/cancel
Authorization: Bearer {accessToken}
```

Body 없음.

#### 응답 (200)

```json
{
  "sessionId": 1,
  "status": "CANCELLED",
  "cancelledAt": "2026-04-29T16:05:00"
}
```

> **WAITING 상태에서만** 취소 가능. MATCHED 이후는 취소 불가.

#### 에러

| HTTP | 코드 | 의미 |
|------|------|------|
| 404 | `PHONEPAY_SESSION_NOT_FOUND` | 세션 없음 |
| 409 | `PHONEPAY_SESSION_NOT_CANCELLABLE` | WAITING이 아닌 상태 |

---

## 에러 응답 형식

모든 에러는 동일한 JSON 구조:

```json
{
  "code": "935",
  "message": "eKYC 인증이 완료되지 않았습니다."
}
```

---

## 인증 방식

| 단계 | 인증 |
|------|------|
| ① 링크 조회 | **불필요** (공개 API) |
| ②~③ eKYC | `Authorization: Bearer {accessToken}` |
| ③-1 은행 목록 | `Authorization: Bearer {accessToken}` |
| ④~⑦ 세션 | `Authorization: Bearer {accessToken}` |

> accessToken은 WidgetSessionData 기반 HMAC-SHA256 토큰.
> 유효기간: 1일. 링크가 ACTIVE일 때만 발급됨.

---

## 구현 참고사항

### 타이머

- `expiresAt`과 `remainingSeconds` 모두 제공됨
- `remainingSeconds`는 서버 시간 기준 → 클라이언트 타이머는 `expiresAt` 기반 자체 카운트다운 권장
- 만료 시 폴링에서 `EXPIRED` 반환

### 금액 입력 (amount = null인 링크)

- 링크의 `amount`가 null이면 ③ 이후 금액 입력 화면 표시
- 정수(원) 단위만 허용. 소수점 불가
- 최소/최대 금액 서버 제한 없음 (BanqPipe 측 제한 있을 수 있으므로 에러 처리 필요)

### 재시도

- `FAILED`, `EXPIRED`, `CANCELLED` 상태에서 "다시 시도" 가능
- ④ 세션 생성을 다시 호출하면 새 세션이 생성됨 (이전 세션과 무관)
- 단, 링크가 USED/EXPIRED/CANCELLED 상태면 ④에서 `PHONEPAY_LINK_NOT_USABLE` 에러

### 연락처 이체 안내

- 수취인 전화번호(`recipientPhone`)로 **연락처 이체**를 안내
- 은행 앱에서 "연락처로 보내기" 기능 사용 유도
- 수취인 이름/은행은 확인용으로 표시

### 폴링 권장 간격

- **3~5초** 간격
- MATCHED 상태 진입 후에도 COMPLETED까지 계속 폴링
- 네트워크 에러 시 폴링 중단하지 말고 재시도 (최대 3회 연속 실패 시 에러 표시)
