pay-rehearsal 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.kr.md +295 -0
- package/README.md +191 -254
- package/dist/index.d.ts +19 -29
- package/dist/index.js +85 -143
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/README.ko.md +0 -308
package/README.kr.md
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
# Pay Rehearsal React
|
|
2
|
+
|
|
3
|
+
[English](https://github.com/Librariann/pay-rehearsal/blob/main/README.md)
|
|
4
|
+
|
|
5
|
+
React와 Next.js에서 함수 하나로 테스트 결제창을 여는 TypeScript Mock 결제 UI SDK입니다. Provider, 훅, 결제사 인증정보, 별도의 CSS import가 필요하지 않습니다.
|
|
6
|
+
|
|
7
|
+
> Pay Rehearsal은 실제 승인이나 청구를 수행하지 않고 실제 카드정보도 수집하지 않습니다. 운영 환경에서는 결제사가 제공하는 공식 SDK와 서버 측 결제 검증으로 반드시 교체해야 합니다.
|
|
8
|
+
|
|
9
|
+
## 제공 기능
|
|
10
|
+
|
|
11
|
+
- 호출 시 모달을 자동으로 마운트하고 정리하는 `requestPayment()` API
|
|
12
|
+
- React Provider와 별도 스타일시트 import가 필요 없는 구성
|
|
13
|
+
- 카드, 계좌이체, 가상계좌, 휴대폰 및 PayPal 방식의 결제 흐름
|
|
14
|
+
- 성공, 실패, 취소, 입금 대기 및 무작위 시나리오
|
|
15
|
+
- 카드사, 할부 개월 및 은행 선택
|
|
16
|
+
- PayPal 방식의 주문 생성, 구매자 승인 및 결제 캡처 단계
|
|
17
|
+
- 포커스 트랩, Escape 닫기, 스크롤 잠금 및 포커스 복원
|
|
18
|
+
- Promise 기반 타입 안전 결과값
|
|
19
|
+
- 테마와 결제수단 노출 순서 설정
|
|
20
|
+
- React 18/19 및 Next.js App Router 지원
|
|
21
|
+
|
|
22
|
+
## 설치
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install pay-rehearsal
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
패키지를 import하면 필요한 스타일도 자동으로 적용됩니다.
|
|
29
|
+
|
|
30
|
+
## React에서 사용하기
|
|
31
|
+
|
|
32
|
+
브라우저 이벤트 핸들러에서 `requestPayment()`를 호출합니다. 라이브러리가 필요한 순간에 결제 모달을 마운트하고 완료 후 자동으로 제거합니다.
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
import { useState } from "react";
|
|
36
|
+
import { requestPayment, type PaymentResult } from "pay-rehearsal";
|
|
37
|
+
|
|
38
|
+
export function CheckoutButton() {
|
|
39
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
40
|
+
const [result, setResult] = useState<PaymentResult | null>(null);
|
|
41
|
+
|
|
42
|
+
const checkout = async () => {
|
|
43
|
+
try {
|
|
44
|
+
setIsOpen(true);
|
|
45
|
+
setResult(
|
|
46
|
+
await requestPayment(
|
|
47
|
+
{
|
|
48
|
+
orderId: `ORDER-${Date.now()}`,
|
|
49
|
+
orderName: "프로 플랜 1개월",
|
|
50
|
+
amount: 29_000,
|
|
51
|
+
currency: "KRW",
|
|
52
|
+
customer: { email: "developer@example.com" },
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
result: "success",
|
|
56
|
+
paymentMethods: ["card", "virtual-account", "paypal"],
|
|
57
|
+
theme: { accentColor: "#4f46e5", borderRadius: 24 },
|
|
58
|
+
},
|
|
59
|
+
),
|
|
60
|
+
);
|
|
61
|
+
} finally {
|
|
62
|
+
setIsOpen(false);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<>
|
|
68
|
+
<button type="button" disabled={isOpen} onClick={checkout}>
|
|
69
|
+
결제하기
|
|
70
|
+
</button>
|
|
71
|
+
{result && <output>{result.status}</output>}
|
|
72
|
+
</>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
전체 Vite 예제는 다음과 같이 실행합니다.
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
cd examples/react
|
|
81
|
+
npm install
|
|
82
|
+
npm run dev
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Next.js App Router에서 사용하기
|
|
86
|
+
|
|
87
|
+
`requestPayment()`는 브라우저 전용 API입니다. Client Component에서 호출하면 되며 루트 레이아웃을 Provider로 감쌀 필요가 없습니다.
|
|
88
|
+
|
|
89
|
+
```tsx
|
|
90
|
+
// app/checkout-button.tsx
|
|
91
|
+
"use client";
|
|
92
|
+
|
|
93
|
+
import { requestPayment } from "pay-rehearsal";
|
|
94
|
+
|
|
95
|
+
export function CheckoutButton() {
|
|
96
|
+
const checkout = async () => {
|
|
97
|
+
const result = await requestPayment(
|
|
98
|
+
{
|
|
99
|
+
orderId: `ORDER-${Date.now()}`,
|
|
100
|
+
orderName: "글로벌 프로 플랜",
|
|
101
|
+
amount: 29,
|
|
102
|
+
currency: "USD",
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
result: "success",
|
|
106
|
+
paymentMethod: "paypal",
|
|
107
|
+
},
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
if (result.status === "success") {
|
|
111
|
+
console.log(result.paymentId, result.approvedAt);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
return <button onClick={checkout}>테스트 결제창 열기</button>;
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
전체 Next.js 예제는 다음과 같이 실행합니다.
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
cd examples/nextjs
|
|
123
|
+
npm install
|
|
124
|
+
npm run dev
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## 호출 API
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
const result = await requestPayment(payment, options);
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### 결제 요청
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
const payment = {
|
|
137
|
+
orderId: "ORDER-1001",
|
|
138
|
+
orderName: "프로 플랜",
|
|
139
|
+
amount: 29_000,
|
|
140
|
+
currency: "KRW",
|
|
141
|
+
customer: {
|
|
142
|
+
id: "USER-42",
|
|
143
|
+
name: "홍길동",
|
|
144
|
+
email: "developer@example.com",
|
|
145
|
+
},
|
|
146
|
+
metadata: { planId: "pro-monthly" },
|
|
147
|
+
};
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
지원 통화는 `KRW`, `USD`, `JPY`, `EUR`입니다.
|
|
151
|
+
|
|
152
|
+
### 옵션
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
requestPayment(payment, {
|
|
156
|
+
result: "failure",
|
|
157
|
+
delayMs: 1_200,
|
|
158
|
+
failureCode: "CARD_DECLINED",
|
|
159
|
+
failureMessage: "카드 승인이 거절되었습니다.",
|
|
160
|
+
paymentMethods: ["card", "bank-transfer", "virtual-account", "paypal"],
|
|
161
|
+
defaultPaymentMethod: "card",
|
|
162
|
+
theme: {
|
|
163
|
+
accentColor: "#4f46e5",
|
|
164
|
+
borderRadius: 24,
|
|
165
|
+
fontFamily: "Pretendard, sans-serif",
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
| 옵션 | 설명 |
|
|
171
|
+
| --- | --- |
|
|
172
|
+
| `result` | `success`, `failure`, `cancelled`, `random` 중 하나 |
|
|
173
|
+
| `delayMs` | Mock 어댑터의 처리 지연 시간 |
|
|
174
|
+
| `paymentMethods` | 화면에 표시할 결제수단과 표시 순서 |
|
|
175
|
+
| `defaultPaymentMethod` | 처음 선택할 결제수단 |
|
|
176
|
+
| `paymentMethod` | 결제수단을 하나로 고정하고 선택 화면 생략 |
|
|
177
|
+
| `randomSuccessRate` | `random` 시나리오의 성공 확률 |
|
|
178
|
+
| `virtualAccountHolder` | 테스트 가상계좌 예금주 |
|
|
179
|
+
| `virtualAccountDueHours` | 테스트 가상계좌 입금기한 |
|
|
180
|
+
| `theme` | 강조 색상, 모서리 및 글꼴 설정 |
|
|
181
|
+
| `adapter` | 선택적으로 사용할 커스텀 `PaymentAdapter` |
|
|
182
|
+
|
|
183
|
+
한 번에 하나의 결제창만 열 수 있습니다. 결제가 진행 중일 때 다시 호출하면 Promise가 reject됩니다.
|
|
184
|
+
|
|
185
|
+
## 반환값
|
|
186
|
+
|
|
187
|
+
`requestPayment()`는 `status`로 구분되는 `PaymentResult`를 반환합니다.
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
const result = await requestPayment(payment);
|
|
191
|
+
|
|
192
|
+
switch (result.status) {
|
|
193
|
+
case "success":
|
|
194
|
+
console.log(result.paymentId, result.approvedAt);
|
|
195
|
+
break;
|
|
196
|
+
case "pending":
|
|
197
|
+
console.log(result.virtualAccount, result.pendingReason);
|
|
198
|
+
break;
|
|
199
|
+
case "failed":
|
|
200
|
+
console.log(result.code, result.message, result.retryable);
|
|
201
|
+
break;
|
|
202
|
+
case "cancelled":
|
|
203
|
+
console.log(result.reason);
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
모든 결과에는 `orderId`, `amount`, `method`, `testMode`가 포함됩니다. 선택한 결제수단에 따라 `cardIssuer`, `installmentMonths`, `bank`도 포함됩니다.
|
|
209
|
+
|
|
210
|
+
테스트 시나리오 설정값은 `failure`지만 반환되는 상태값은 `failed`라는 점에 주의하세요.
|
|
211
|
+
|
|
212
|
+
## 백엔드로 결과 보내기
|
|
213
|
+
|
|
214
|
+
결제 요청과 결과를 JSON으로 일반 백엔드 API에 전송합니다. 이 예제는 특정 DB나 ORM에 의존하지 않습니다.
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
const result = await requestPayment(payment, { result: "success" });
|
|
218
|
+
|
|
219
|
+
await fetch("/api/payments", {
|
|
220
|
+
method: "POST",
|
|
221
|
+
headers: { "content-type": "application/json" },
|
|
222
|
+
body: JSON.stringify({ payment, result }),
|
|
223
|
+
});
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
테스트 레코드는 `testMode: true`로 명확히 구분하세요. `PaymentResult`에는 `orderName`, `currency`, `customer`, `metadata`가 반복해서 포함되지 않으므로 해당 값이 필요하면 원래 요청 객체를 결과와 함께 저장해야 합니다.
|
|
227
|
+
|
|
228
|
+
운영 환경에서는 브라우저가 반환한 결과를 신뢰하면 안 됩니다. 서버에서 실제 결제사를 통해 주문번호, 금액, 통화, 승인 상태와 웹훅 서명을 검증해야 합니다.
|
|
229
|
+
|
|
230
|
+
## PayPal 흐름 테스트하기
|
|
231
|
+
|
|
232
|
+
개별 결제를 PayPal로 고정할 수 있습니다.
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
const result = await requestPayment(
|
|
236
|
+
{
|
|
237
|
+
orderId: "ORDER-PAYPAL-1001",
|
|
238
|
+
orderName: "글로벌 프로 플랜",
|
|
239
|
+
amount: 29,
|
|
240
|
+
currency: "USD",
|
|
241
|
+
},
|
|
242
|
+
{ paymentMethod: "paypal" },
|
|
243
|
+
);
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
모달은 다음 단계를 재현합니다.
|
|
247
|
+
|
|
248
|
+
1. Mock PayPal 주문 생성
|
|
249
|
+
2. 구매자 승인
|
|
250
|
+
3. 결제 캡처와 검증
|
|
251
|
+
4. 설정된 최종 결과 반환
|
|
252
|
+
|
|
253
|
+
실제 PayPal SDK를 불러오지 않으며 인증정보나 실제 금액도 사용하지 않습니다. 운영 환경에서는 서버에서 PayPal 주문을 생성하고 캡처한 뒤 최종 상태를 검증해야 합니다.
|
|
254
|
+
|
|
255
|
+
## PaymentButton
|
|
256
|
+
|
|
257
|
+
선택적으로 제공하는 `PaymentButton`도 Provider 없이 사용할 수 있습니다.
|
|
258
|
+
|
|
259
|
+
```tsx
|
|
260
|
+
import { PaymentButton } from "pay-rehearsal";
|
|
261
|
+
|
|
262
|
+
<PaymentButton
|
|
263
|
+
request={{
|
|
264
|
+
orderId: "ORDER-1001",
|
|
265
|
+
orderName: "프로 플랜",
|
|
266
|
+
amount: 29_000,
|
|
267
|
+
}}
|
|
268
|
+
options={{ result: "success" }}
|
|
269
|
+
onResult={(result) => console.log(result)}
|
|
270
|
+
>
|
|
271
|
+
29,000원 결제하기
|
|
272
|
+
</PaymentButton>;
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
## 운영 전 확인사항
|
|
276
|
+
|
|
277
|
+
- 카드번호, 비밀번호 또는 신원정보를 이 UI에서 수집하거나 저장하지 마세요.
|
|
278
|
+
- 서버에서 생성한 고유한 `orderId`를 사용하세요.
|
|
279
|
+
- 서버에서 예상 금액과 최종 승인 상태를 검증하세요.
|
|
280
|
+
- 중복 승인을 막기 위해 결제 승인 API에 멱등성을 적용하세요.
|
|
281
|
+
- 모의 인증 화면을 결제사의 공식 결제 화면으로 교체하세요.
|
|
282
|
+
- Mock 결과로 운영 주문을 변경할 수 없도록 분리하세요.
|
|
283
|
+
|
|
284
|
+
## 개발 명령어
|
|
285
|
+
|
|
286
|
+
```bash
|
|
287
|
+
npm install
|
|
288
|
+
npm run typecheck
|
|
289
|
+
npm test
|
|
290
|
+
npm run build
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
## 고지
|
|
294
|
+
|
|
295
|
+
Pay Rehearsal은 어떠한 PG사, 카드사 또는 금융기관에서 제공하거나 후원하거나 보증하는 제품이 아닙니다. 금융기관명은 모의 결제 흐름을 설명하기 위한 용도로만 사용됩니다.
|