@sayren/storefront-sdk 0.9.0 → 0.11.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/dist/auth/index.d.mts +12 -3
- package/dist/auth/index.mjs +13 -4
- package/dist/checkout-CHpxo2Ap.mjs +310 -0
- package/dist/{client-D9iQIWnV.mjs → client-CHfCafer.mjs} +134 -13
- package/dist/{http-BbFHVGBf.d.mts → http-CeRVoUGu.d.mts} +19 -1
- package/dist/{index-DvHOQm2-.d.mts → index-OqvbJ6Xi.d.mts} +459 -3
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +3 -3
- package/dist/payments/index.d.mts +5 -1
- package/dist/payments/index.mjs +4 -2
- package/package.json +1 -1
- package/src/auth/index.ts +18 -3
- package/src/client.ts +28 -0
- package/src/index.ts +1 -0
- package/src/payments/index.ts +11 -1
- package/src/schemas/auth.ts +16 -0
- package/src/schemas/checkout.ts +192 -1
- package/src/schemas/claim.ts +63 -3
- package/src/schemas/consent.ts +72 -0
- package/src/schemas/member.ts +6 -0
- package/src/schemas/order.ts +52 -8
- package/dist/checkout-JFk3b8RG.mjs +0 -189
package/dist/auth/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as AnonymousSession, d as
|
|
1
|
+
import { a as AnonymousSession, d as MemberIdentities, f as SignupRequest, g as WithdrawalBlockedDetails, h as WithdrawRequest, m as TokenPair, p as SocialProvider, t as ApiError, u as LoginRequest } from "../http-CeRVoUGu.mjs";
|
|
2
2
|
//#region src/auth/index.d.ts
|
|
3
3
|
/** 소셜 로그인 시작 결과 — `codeVerifier`와 `state`는 돌아올 때까지 서버 세션(httpOnly 쿠키 등)에 보관한다 */
|
|
4
4
|
export interface IdpStart {
|
|
@@ -56,14 +56,23 @@ export declare function createStorefrontAuth(options: StorefrontAuthOptions): {
|
|
|
56
56
|
/**
|
|
57
57
|
* 소셜 로그인 시작 — PKCE(S256)와 state를 만들고 공급자 로그인 주소를 받는다. 반환한 `state`·`codeVerifier`를
|
|
58
58
|
* 서버 세션에 보관하고 구매자를 `url`로 보낸다. 로그인을 마치면 `redirectUri`로 `?code=&state=`가 붙어 돌아온다.
|
|
59
|
-
* `redirectUri`는 스토어프론트 도메인 목록 안이어야
|
|
59
|
+
* `redirectUri`는 스토어프론트 도메인 목록 안이어야 한다.
|
|
60
|
+
*
|
|
61
|
+
* 가입 화면에서 받은 약관 동의는 `agreements`로 함께 보낸다. **이 로그인으로 가입이 되는 경우에만** 이력에
|
|
62
|
+
* 남고(기존 회원의 로그인이면 무시한다), 보내지 않으면 가입은 되지만 필수 동의 이력이 없어
|
|
63
|
+
* `GET /me`의 `reconsentRequired`가 true다(첫 진입에서 `updateConsents()`로 받는다)
|
|
60
64
|
*/
|
|
61
65
|
idp(provider: SocialProvider, options: {
|
|
62
66
|
redirectUri: string;
|
|
67
|
+
agreements?: {
|
|
68
|
+
terms: true;
|
|
69
|
+
privacy: true;
|
|
70
|
+
marketing?: boolean;
|
|
71
|
+
};
|
|
63
72
|
}): Promise<IdpStart>;
|
|
64
73
|
/**
|
|
65
74
|
* 소셜 로그인 마무리 — 돌아온 `code`를 구매자 토큰으로 바꾼다. state가 다르거나 구매자가 취소했으면
|
|
66
|
-
* `IdpCallbackError`. code는 1회용이고 1분 안에 바꿔야
|
|
75
|
+
* `IdpCallbackError`. code는 1회용이고 1분 안에 바꿔야 한다. 약관 동의는 `idp()`에서 받는다
|
|
67
76
|
*/
|
|
68
77
|
idpCallback(input: IdpCallbackInput & {
|
|
69
78
|
error?: string | null;
|
package/dist/auth/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { pt as ApiError, t as createStorefrontClient } from "../client-CHfCafer.mjs";
|
|
2
2
|
//#region src/auth/index.ts
|
|
3
3
|
/** 소셜 로그인 실패 — 구매자가 취소했거나(`access_denied`) state가 맞지 않는다 */
|
|
4
4
|
var IdpCallbackError = class extends Error {
|
|
@@ -60,7 +60,11 @@ function createStorefrontAuth(options) {
|
|
|
60
60
|
/**
|
|
61
61
|
* 소셜 로그인 시작 — PKCE(S256)와 state를 만들고 공급자 로그인 주소를 받는다. 반환한 `state`·`codeVerifier`를
|
|
62
62
|
* 서버 세션에 보관하고 구매자를 `url`로 보낸다. 로그인을 마치면 `redirectUri`로 `?code=&state=`가 붙어 돌아온다.
|
|
63
|
-
* `redirectUri`는 스토어프론트 도메인 목록 안이어야
|
|
63
|
+
* `redirectUri`는 스토어프론트 도메인 목록 안이어야 한다.
|
|
64
|
+
*
|
|
65
|
+
* 가입 화면에서 받은 약관 동의는 `agreements`로 함께 보낸다. **이 로그인으로 가입이 되는 경우에만** 이력에
|
|
66
|
+
* 남고(기존 회원의 로그인이면 무시한다), 보내지 않으면 가입은 되지만 필수 동의 이력이 없어
|
|
67
|
+
* `GET /me`의 `reconsentRequired`가 true다(첫 진입에서 `updateConsents()`로 받는다)
|
|
64
68
|
*/
|
|
65
69
|
async idp(provider, options) {
|
|
66
70
|
const state = randomToken(24);
|
|
@@ -68,7 +72,12 @@ function createStorefrontAuth(options) {
|
|
|
68
72
|
const { url } = await client().auth.idpAuthorize(provider, {
|
|
69
73
|
redirectUri: options.redirectUri,
|
|
70
74
|
codeChallenge: await s256(codeVerifier),
|
|
71
|
-
state
|
|
75
|
+
state,
|
|
76
|
+
agreements: options.agreements && {
|
|
77
|
+
terms: true,
|
|
78
|
+
privacy: true,
|
|
79
|
+
marketing: options.agreements.marketing ?? false
|
|
80
|
+
}
|
|
72
81
|
});
|
|
73
82
|
return {
|
|
74
83
|
url,
|
|
@@ -78,7 +87,7 @@ function createStorefrontAuth(options) {
|
|
|
78
87
|
},
|
|
79
88
|
/**
|
|
80
89
|
* 소셜 로그인 마무리 — 돌아온 `code`를 구매자 토큰으로 바꾼다. state가 다르거나 구매자가 취소했으면
|
|
81
|
-
* `IdpCallbackError`. code는 1회용이고 1분 안에 바꿔야
|
|
90
|
+
* `IdpCallbackError`. code는 1회용이고 1분 안에 바꿔야 한다. 약관 동의는 `idp()`에서 받는다
|
|
82
91
|
*/
|
|
83
92
|
idpCallback(input) {
|
|
84
93
|
if (input.error) return Promise.reject(new IdpCallbackError(input.error));
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
//#region src/schemas/checkout.ts
|
|
3
|
+
const createCheckoutRequestSchema = z.object({
|
|
4
|
+
cartItemIds: z.array(z.string()).min(1).optional(),
|
|
5
|
+
directItem: z.object({
|
|
6
|
+
productId: z.string(),
|
|
7
|
+
optionId: z.string().optional(),
|
|
8
|
+
quantity: z.number().int().min(1)
|
|
9
|
+
}).optional()
|
|
10
|
+
}).refine((value) => value.cartItemIds ? !value.directItem : !!value.directItem, { message: "cartItemIds와 directItem 중 하나만 전달해야 합니다" });
|
|
11
|
+
/** 지역 추가 배송비 구분 — 우편번호로 판정한다 */
|
|
12
|
+
const remoteAreaSchema = z.enum([
|
|
13
|
+
"NONE",
|
|
14
|
+
"JEJU",
|
|
15
|
+
"ISOLATED"
|
|
16
|
+
]);
|
|
17
|
+
/**
|
|
18
|
+
* 배송비 내역 (이슈 #29) — 배송비를 **몇 번, 왜** 부과했는지다.
|
|
19
|
+
*
|
|
20
|
+
* 배송비는 **배송 묶음**마다 붙는다. 같은 출고지 + 같은 배송 정책인 상품은 한 묶음이고 한 번만 낸다.
|
|
21
|
+
* 제주·도서산간 추가 배송비는 묶음마다 더하고, 무료배송이어도 붙는다.
|
|
22
|
+
*
|
|
23
|
+
* 주문서를 만들 때는 배송지를 모르므로 `zipCode`가 null이고 `remoteSurcharge`가 0이다.
|
|
24
|
+
* 배송지를 입력하면 `POST /storefront/v1/checkout/{checkoutId}/delivery-quote`로 다시 받고,
|
|
25
|
+
* **결제 시작이 보낸 배송지로 최종 확정한다** — 그래서 주문서 조회 금액과 실제 결제 금액이 다를 수 있다.
|
|
26
|
+
*/
|
|
27
|
+
const checkoutDeliverySchema = z.object({
|
|
28
|
+
baseFee: z.number().int().describe("기본 배송비 합계. 묶음마다 부과한 금액의 합이다"),
|
|
29
|
+
remoteSurcharge: z.number().int().describe("제주·도서산간 추가 배송비 합계. 묶음마다 붙는다. 배송지를 모르면 0"),
|
|
30
|
+
remoteArea: remoteAreaSchema.describe("배송지 지역 구분. 배송지를 모르면 NONE"),
|
|
31
|
+
remoteAreaLabel: z.string().nullable().describe("지역 안내 이름(예: 제주특별자치도). 추가 배송비 지역이 아니면 null"),
|
|
32
|
+
bundleCount: z.number().int().describe("배송 묶음 수 — 배송비를 부과한 횟수다"),
|
|
33
|
+
freeByThreshold: z.boolean().describe("스토어 전체 무료배송 기준액을 넘겨 기본 배송비를 면제했는가"),
|
|
34
|
+
zipCode: z.string().nullable().describe("이 금액에 반영한 배송지 우편번호. null이면 배송지 미반영(추가 배송비 0)이다")
|
|
35
|
+
});
|
|
36
|
+
const checkoutSessionSchema = z.object({
|
|
37
|
+
checkoutId: z.string(),
|
|
38
|
+
items: z.array(z.object({
|
|
39
|
+
productId: z.string(),
|
|
40
|
+
productName: z.string(),
|
|
41
|
+
/** variantId. 옵션 없는 상품은 default variant id */
|
|
42
|
+
optionId: z.string().nullable(),
|
|
43
|
+
optionName: z.string().nullable(),
|
|
44
|
+
quantity: z.number().int(),
|
|
45
|
+
unitPrice: z.number().int(),
|
|
46
|
+
totalPrice: z.number().int()
|
|
47
|
+
})),
|
|
48
|
+
amounts: z.object({
|
|
49
|
+
productAmount: z.number().int(),
|
|
50
|
+
deliveryFee: z.number().int(),
|
|
51
|
+
totalAmount: z.number().int()
|
|
52
|
+
}),
|
|
53
|
+
delivery: checkoutDeliverySchema.describe("배송비 내역. 배송지를 입력하면 금액이 바뀔 수 있다 — 최종 금액은 결제 시작 응답의 `amounts`다"),
|
|
54
|
+
expiresAt: z.string(),
|
|
55
|
+
paymentOptions: z.lazy(() => paymentOptionListSchema).describe("이 주문서에서 고를 수 있는 결제 옵션, PG 우선순위순. 셀러가 켠 PG·결제수단·간편결제만 담긴다. 비어 있으면 지금 결제할 수 없다"),
|
|
56
|
+
testPayment: z.boolean().describe("지금 결제하면 테스트 결제(샌드박스)인가. true면 실제로 돈이 나가지 않아요 — 주문서에 테스트 결제 안내를 표시해요. 결제 요청 시점에 스토어 결제 설정으로 다시 정해지니 최종 값은 결제 요청 응답의 `testPayment`예요")
|
|
57
|
+
});
|
|
58
|
+
/**
|
|
59
|
+
* 배송비 미리보기 요청 — 배송지 우편번호로 배송비를 다시 계산한다. 주문서를 바꾸지 않는다(읽기 계산이다).
|
|
60
|
+
*
|
|
61
|
+
* 구매자가 배송지를 입력하거나 고칠 때 부르면 제주·도서산간 추가 배송비가 반영된 금액을 미리 보여 줄 수 있다.
|
|
62
|
+
*/
|
|
63
|
+
const deliveryQuoteRequestSchema = z.object({ zipCode: z.string().regex(/^[0-9]{5}$/, "우편번호 5자리를 입력해 주세요").describe("배송지 우편번호 5자리") });
|
|
64
|
+
/** 배송비 미리보기 응답 — 이 배송지로 결제하면 나갈 금액이다 */
|
|
65
|
+
const deliveryQuoteSchema = z.object({
|
|
66
|
+
amounts: checkoutSessionSchema.shape.amounts.describe("이 배송지 기준 금액"),
|
|
67
|
+
delivery: checkoutDeliverySchema
|
|
68
|
+
});
|
|
69
|
+
const paymentMethodSchema = z.enum([
|
|
70
|
+
"CARD",
|
|
71
|
+
"BANK_TRANSFER",
|
|
72
|
+
"VIRTUAL_ACCOUNT",
|
|
73
|
+
"MOBILE",
|
|
74
|
+
"EASY_PAY"
|
|
75
|
+
]);
|
|
76
|
+
/** 결제를 처리하는 PG. 셀러가 직접 계약하고 콘솔 설정 › 결제에서 연결한다 */
|
|
77
|
+
const pgProviderSchema = z.enum(["tosspayments", "portone"]);
|
|
78
|
+
/** 간편결제사. 토스페이먼츠·포트원 모두 같은 코드를 쓴다 */
|
|
79
|
+
const easyPayProviderSchema = z.enum([
|
|
80
|
+
"NAVERPAY",
|
|
81
|
+
"KAKAOPAY",
|
|
82
|
+
"TOSSPAY",
|
|
83
|
+
"PAYCO"
|
|
84
|
+
]);
|
|
85
|
+
/**
|
|
86
|
+
* 응답에 실리는 간편결제사 — 서버가 간편결제사를 추가해도 파싱이 깨지지 않게 문자열로 받는다
|
|
87
|
+
* (`pgProvider`·결제 상태와 같은 방식). 요청과 결제 옵션은 값을 골라 보내는 자리라 enum 그대로다.
|
|
88
|
+
*/
|
|
89
|
+
const easyPayProviderResponseSchema = z.string();
|
|
90
|
+
const pg = pgProviderSchema.describe("결제를 처리할 PG");
|
|
91
|
+
/**
|
|
92
|
+
* 결제 옵션 — 어느 PG로 어떤 결제수단을 쓸지. `method`로 좁히면 필요한 필드가 타입으로 정해진다
|
|
93
|
+
* (`EASY_PAY`만 `provider`가 있다). 주문서의 `paymentOptions` 항목을 그대로 넘기거나 직접 써도 된다.
|
|
94
|
+
* 셀러가 켜지 않은 조합이면 서버가 409 `PAYMENT_OPTION_UNAVAILABLE`로 거절한다.
|
|
95
|
+
*/
|
|
96
|
+
const paymentOptionSchema = z.discriminatedUnion("method", [
|
|
97
|
+
z.object({
|
|
98
|
+
pg,
|
|
99
|
+
method: z.literal("CARD")
|
|
100
|
+
}),
|
|
101
|
+
z.object({
|
|
102
|
+
pg,
|
|
103
|
+
method: z.literal("EASY_PAY"),
|
|
104
|
+
provider: easyPayProviderSchema.describe("간편결제사 — 이 간편결제 결제창을 바로 연다")
|
|
105
|
+
}),
|
|
106
|
+
z.object({
|
|
107
|
+
pg,
|
|
108
|
+
method: z.literal("BANK_TRANSFER")
|
|
109
|
+
}),
|
|
110
|
+
z.object({
|
|
111
|
+
pg,
|
|
112
|
+
method: z.literal("VIRTUAL_ACCOUNT")
|
|
113
|
+
}),
|
|
114
|
+
z.object({
|
|
115
|
+
pg,
|
|
116
|
+
method: z.literal("MOBILE")
|
|
117
|
+
})
|
|
118
|
+
]);
|
|
119
|
+
/** 주문서가 내려주는 결제 옵션 — 결제 옵션 + 표시 이름 */
|
|
120
|
+
const availablePaymentOptionSchema = paymentOptionSchema.and(z.object({
|
|
121
|
+
label: z.string().describe("결제수단 표시 이름 (예: 신용·체크카드, 네이버페이)"),
|
|
122
|
+
pgName: z.string().describe("PG 표시 이름 (예: 토스페이먼츠)")
|
|
123
|
+
}));
|
|
124
|
+
/**
|
|
125
|
+
* 결제 옵션 목록 — 서버가 나중에 추가한 결제수단·PG를 이 SDK가 모르면 **그 항목만 빼고** 읽는다(목록 파싱은 깨지지 않는다).
|
|
126
|
+
*
|
|
127
|
+
* 모르는 항목을 버리는 일은 `preprocess`가 하고 항목 타입은 그대로 남는다 — OpenAPI 문서에 `items` 없는
|
|
128
|
+
* 배열로 나가지 않게 하기 위해서다(`z.array(z.unknown()).transform(...)`은 출력 타입을 표현하지 못한다).
|
|
129
|
+
*/
|
|
130
|
+
const paymentOptionListSchema = z.preprocess((value) => Array.isArray(value) ? value.filter((item) => availablePaymentOptionSchema.safeParse(item).success) : value, z.array(availablePaymentOptionSchema));
|
|
131
|
+
/** 결제 옵션을 화면 키로 — 라디오 value·React key. 예: `tosspayments:CARD`, `portone:EASY_PAY:NAVERPAY` */
|
|
132
|
+
function paymentOptionKey(option) {
|
|
133
|
+
return option.method === "EASY_PAY" ? `${option.pg}:EASY_PAY:${option.provider}` : `${option.pg}:${option.method}`;
|
|
134
|
+
}
|
|
135
|
+
const shippingAddressInputSchema = z.object({
|
|
136
|
+
addressId: z.string().optional(),
|
|
137
|
+
receiverName: z.string().min(1, "수령인명을 입력해주세요").max(50),
|
|
138
|
+
phone: z.string().regex(/^01[0-9]{8,9}$/, "연락처 형식이 아닙니다 (예: 01012345678)"),
|
|
139
|
+
zipCode: z.string().regex(/^[0-9]{5}$/, "우편번호는 5자리 숫자입니다"),
|
|
140
|
+
address1: z.string().min(1, "기본 주소를 입력해주세요"),
|
|
141
|
+
address2: z.string().optional(),
|
|
142
|
+
deliveryMemo: z.string().max(100).optional(),
|
|
143
|
+
entranceCode: z.string().max(20).optional()
|
|
144
|
+
});
|
|
145
|
+
const guestInfoSchema = z.object({
|
|
146
|
+
name: z.string().min(1),
|
|
147
|
+
phone: z.string().regex(/^01[0-9]{8,9}$/),
|
|
148
|
+
email: z.email(),
|
|
149
|
+
orderPassword: z.string().min(6, "주문 조회 비밀번호는 6자 이상이어야 합니다")
|
|
150
|
+
});
|
|
151
|
+
/** 현금영수증을 신청할 수 있는 결제수단 — 계좌이체·가상계좌 */
|
|
152
|
+
const CASH_RECEIPT_METHODS = ["BANK_TRANSFER", "VIRTUAL_ACCOUNT"];
|
|
153
|
+
/** 이 결제수단으로 현금영수증을 신청할 수 있는가 */
|
|
154
|
+
function cashReceiptAvailable(method) {
|
|
155
|
+
return CASH_RECEIPT_METHODS.includes(method);
|
|
156
|
+
}
|
|
157
|
+
/** 현금영수증 용도 — `INCOME_DEDUCTION` 소득공제(개인) · `EXPENSE_PROOF` 지출증빙(사업자) */
|
|
158
|
+
const cashReceiptTypeSchema = z.enum(["INCOME_DEDUCTION", "EXPENSE_PROOF"]);
|
|
159
|
+
/**
|
|
160
|
+
* 주민등록번호·외국인등록번호 형식인가 — 현금영수증 식별 번호로는 받지 않는다.
|
|
161
|
+
*
|
|
162
|
+
* 현금영수증카드 번호의 아래 끝(13자리)이 주민등록번호 길이와 겹친다. 개인 식별 번호는 최소로 받는다는 원칙이라
|
|
163
|
+
* 주민등록번호로 읽히는 값은 카드 번호 자리로도 받지 않는다.
|
|
164
|
+
*
|
|
165
|
+
* **13자리 숫자 전부를 막지 않는다.** 13자리 현금영수증카드를 쓰는 구매자가 신청할 수 없게 되기 때문이다.
|
|
166
|
+
* 대신 주민등록번호 형식 — 생년월일 6자리(월 01~12, 일 01~31) + 성별·세기 코드(1~8, 5~8은 외국인등록번호) — 으로
|
|
167
|
+
* 판정해 오탐을 줄인다(13자리 숫자 중 이 형식에 걸리는 비율은 약 3%다).
|
|
168
|
+
*
|
|
169
|
+
* 검증번호(뒤 1자리 mod 11)까지는 보지 않는다. 한 자리 오타가 난 주민등록번호도 생년월일을 담고 있어 막아야 하고,
|
|
170
|
+
* 2020년 10월 이후 발급된 외국인등록번호에는 검증번호 규칙이 없다.
|
|
171
|
+
*
|
|
172
|
+
* `cashReceiptIdentityTypeOf`와 같이 하이픈·공백은 빼고 판정한다.
|
|
173
|
+
*/
|
|
174
|
+
function residentNumberLike(identityNumber) {
|
|
175
|
+
const digits = identityNumber.replace(/[\s-]/g, "");
|
|
176
|
+
if (!/^[0-9]{13}$/.test(digits)) return false;
|
|
177
|
+
const month = Number(digits.slice(2, 4));
|
|
178
|
+
const day = Number(digits.slice(4, 6));
|
|
179
|
+
const genderCode = Number(digits[6]);
|
|
180
|
+
return month >= 1 && month <= 12 && day >= 1 && day <= 31 && genderCode >= 1 && genderCode <= 8;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* 식별 번호 → 종류. 하이픈·공백은 빼고 판정한다. 용도에 맞지 않는 번호면 null이다.
|
|
184
|
+
* - 소득공제: 휴대폰 번호(01로 시작하는 10~11자리) 또는 현금영수증카드 번호(13~19자리)
|
|
185
|
+
* - 지출증빙: 사업자등록번호(10자리) 또는 현금영수증카드 번호(13~19자리)
|
|
186
|
+
*
|
|
187
|
+
* 자릿수 근거. PG 문서는 식별 번호의 최대 길이만 정하고 종류별 자릿수는 정하지 않는다
|
|
188
|
+
* (토스페이먼츠 `customerIdentityNumber` "최대 길이는 30자", 소득공제는 휴대폰 번호·현금영수증카드 번호,
|
|
189
|
+
* 지출증빙은 사업자등록번호: https://docs.tosspayments.com/common/apis/cash-receipt /
|
|
190
|
+
* 포트원 V2는 종류만 PHONE·CARD·BUSINESS로 나눈다: https://developers.portone.io/api/rest-v2/payment.cashReceipt).
|
|
191
|
+
* 그래서 자릿수는 국세청이 정하는 발급수단 규격을 따른다 — 홈택스 소비자 발급수단 등록의 카드 번호는
|
|
192
|
+
* 13~19자리 숫자이고(https://thisthatbase.com/cash-receipts-card-registration/ 가 옮긴 홈택스 안내,
|
|
193
|
+
* 원본은 https://www.hometax.go.kr/ 소비자 발급수단 관리), 사업자등록번호는 10자리, 휴대폰 번호는 01X + 7~8자리다.
|
|
194
|
+
* 국세청 자진발급 번호(010-000-1234)도 휴대폰 형식으로 통과한다.
|
|
195
|
+
*/
|
|
196
|
+
function cashReceiptIdentityTypeOf(type, identityNumber) {
|
|
197
|
+
const digits = identityNumber.replace(/[\s-]/g, "");
|
|
198
|
+
if (!/^[0-9]+$/.test(digits)) return null;
|
|
199
|
+
if (residentNumberLike(digits)) return null;
|
|
200
|
+
if (/^[0-9]{13,19}$/.test(digits)) return "CARD";
|
|
201
|
+
if (type === "INCOME_DEDUCTION") return /^01[016789][0-9]{7,8}$/.test(digits) ? "PHONE" : null;
|
|
202
|
+
return /^[0-9]{10}$/.test(digits) ? "BUSINESS" : null;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* 현금영수증 신청 — 계좌이체·가상계좌 결제에서만 보낼 수 있다. 결제가 승인되면 sayren이 결제한 PG로 발급하고,
|
|
206
|
+
* 환불하면 그만큼 취소한다. 식별 번호는 암호화해 저장하고 응답에는 가린 값만 나간다.
|
|
207
|
+
*/
|
|
208
|
+
const cashReceiptRequestSchema = z.object({
|
|
209
|
+
type: cashReceiptTypeSchema.describe("용도. `INCOME_DEDUCTION` 소득공제(개인) · `EXPENSE_PROOF` 지출증빙(사업자)"),
|
|
210
|
+
identityNumber: z.string().max(30).describe("식별 번호. 소득공제는 휴대폰 번호(01X + 7~8자리) 또는 현금영수증카드 번호(13~19자리), 지출증빙은 사업자등록번호(10자리) 또는 현금영수증카드 번호(13~19자리). 하이픈은 빼고 읽는다. 주민등록번호는 받지 않는다(`RESIDENT_NUMBER_NOT_ALLOWED`)")
|
|
211
|
+
}).superRefine((value, ctx) => {
|
|
212
|
+
if (residentNumberLike(value.identityNumber)) {
|
|
213
|
+
ctx.addIssue({
|
|
214
|
+
code: "custom",
|
|
215
|
+
path: ["identityNumber"],
|
|
216
|
+
message: "주민등록번호는 현금영수증 식별 번호로 쓸 수 없어요. 휴대폰 번호 또는 현금영수증카드 번호를 입력해주세요 (RESIDENT_NUMBER_NOT_ALLOWED)"
|
|
217
|
+
});
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (!cashReceiptIdentityTypeOf(value.type, value.identityNumber)) ctx.addIssue({
|
|
221
|
+
code: "custom",
|
|
222
|
+
path: ["identityNumber"],
|
|
223
|
+
message: value.type === "INCOME_DEDUCTION" ? "휴대폰 번호 또는 현금영수증카드 번호를 입력해주세요" : "사업자등록번호(10자리) 또는 현금영수증카드 번호를 입력해주세요"
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
const cashReceiptField = cashReceiptRequestSchema.optional().describe("현금영수증 신청. 계좌이체·가상계좌 옵션에서만 보낼 수 있다(그 밖의 옵션이면 400 `CASH_RECEIPT_NOT_AVAILABLE`). 결제가 승인되면 발급한다");
|
|
227
|
+
/**
|
|
228
|
+
* 결제 시작 — 배송지를 확정하고 고른 결제 옵션으로 결제 시도를 연다. 응답의 `payUrl`(결제 서비스)을 팝업(`mode=popup`)이나
|
|
229
|
+
* 전체 페이지로 열면 결제 서비스가 PG 결제창을 띄우고 승인까지 한다. 리다이렉트 결제가 끝나면 구매자는 `returnUrl`로 돌아온다.
|
|
230
|
+
* `returnUrl`의 도메인은 스토어 결제 도메인(콘솔 설정 › 결제)에 등록돼 있어야 한다(테스트 결제는 localhost 허용).
|
|
231
|
+
*/
|
|
232
|
+
const startPaymentRequestSchema = z.object({
|
|
233
|
+
option: paymentOptionSchema,
|
|
234
|
+
returnUrl: z.url().max(2048).describe("결제를 마치면 돌아올 스토어프론트 주소. 스토어 결제 도메인에 등록된 도메인이어야 한다"),
|
|
235
|
+
shippingAddress: shippingAddressInputSchema,
|
|
236
|
+
guest: guestInfoSchema.optional(),
|
|
237
|
+
cashReceipt: cashReceiptField
|
|
238
|
+
});
|
|
239
|
+
/**
|
|
240
|
+
* 같은 결제에서 다른 결제 옵션으로 다시 시도. `returnUrl`을 생략하면 결제 시작 때의 복귀 주소를 쓴다.
|
|
241
|
+
* `cashReceipt`를 보내면 현금영수증 신청을 이 값으로 바꾸고, 생략하면 결제 시작 때의 신청을 그대로 둔다
|
|
242
|
+
* (승인된 결제수단이 계좌이체·가상계좌가 아니면 발급하지 않는다)
|
|
243
|
+
*/
|
|
244
|
+
const retryPaymentRequestSchema = z.object({
|
|
245
|
+
option: paymentOptionSchema,
|
|
246
|
+
returnUrl: startPaymentRequestSchema.shape.returnUrl.optional(),
|
|
247
|
+
cashReceipt: cashReceiptField
|
|
248
|
+
});
|
|
249
|
+
const paymentStartSchema = z.object({
|
|
250
|
+
paymentId: z.string().describe("결제 id. 상태 조회·재시도의 키다"),
|
|
251
|
+
attemptId: z.string().describe("결제 시도 id. PG 주문번호(토스 orderId, 포트원 paymentId)다"),
|
|
252
|
+
option: paymentOptionSchema.describe("이 시도의 결제 옵션"),
|
|
253
|
+
payUrl: z.string().describe("결제 서비스 주소. 팝업으로 열려면 쿼리 `mode=popup`을 붙이고, 그대로 이동하면 리다이렉트 결제다"),
|
|
254
|
+
expiresAt: z.string().describe("결제 요청 만료 시각"),
|
|
255
|
+
testPayment: z.boolean().describe("테스트 결제(샌드박스) 여부. true면 실제로 돈이 나가지 않는다 — 결제 화면에 테스트 결제 안내를 표시한다"),
|
|
256
|
+
amounts: checkoutSessionSchema.shape.amounts.describe("**확정 결제 금액** — 보낸 배송지로 배송비를 다시 계산한 값이다. 주문서 조회 시점의 `amounts`와 다를 수 있다(제주·도서산간 추가 배송비)"),
|
|
257
|
+
delivery: checkoutDeliverySchema.describe("확정 배송비 내역. `zipCode`는 보낸 배송지의 우편번호다")
|
|
258
|
+
});
|
|
259
|
+
/** 결제창에서 결제하지 못한 이유 */
|
|
260
|
+
const paymentFailureCategorySchema = z.enum([
|
|
261
|
+
"PREPARE_FAILURE",
|
|
262
|
+
"DECLINED",
|
|
263
|
+
"USER_CANCELED"
|
|
264
|
+
]);
|
|
265
|
+
/**
|
|
266
|
+
* 결제 상태 조회의 현재 상태 값 — 응답 스키마는 서버가 상태를 추가해도 파싱이 깨지지 않게 문자열로 받는다
|
|
267
|
+
* (`pgProvider`와 같은 방식). 화면 분기는 이 목록으로 하고 모르는 값은 확인 중으로 다룬다.
|
|
268
|
+
*/
|
|
269
|
+
const PAYMENT_STATUS_VALUES = [
|
|
270
|
+
"pending",
|
|
271
|
+
"processing",
|
|
272
|
+
"completed",
|
|
273
|
+
"failed",
|
|
274
|
+
"expired"
|
|
275
|
+
];
|
|
276
|
+
/** 결제 결과 구분 — 결제 서비스가 스토어프론트에 돌려주는 값과 같다 */
|
|
277
|
+
const PAYMENT_RESULT_VALUES = [
|
|
278
|
+
"COMPLETED",
|
|
279
|
+
"PROCESSING",
|
|
280
|
+
"CANCELED",
|
|
281
|
+
"FAILED",
|
|
282
|
+
"EXPIRED",
|
|
283
|
+
"PENDING"
|
|
284
|
+
];
|
|
285
|
+
const paymentStatusSchema = z.object({
|
|
286
|
+
paymentId: z.string(),
|
|
287
|
+
status: z.string().describe("결제 상태. 현재 값은 `pending` 결제 대기 · `processing` 승인 확인 중 · `completed` 결제 완료(주문 생성) · `failed` 실패 · `expired` 만료예요. 상태가 추가될 수 있어 문자열로 받아요 — 모르는 값은 `processing`처럼 다루고 계속 조회하세요"),
|
|
288
|
+
result: z.string().describe("화면 분기용 결과. `COMPLETED` 결제 완료 · `PROCESSING` 결과 확인 중 · `CANCELED` 구매자가 결제창을 닫음 · `FAILED` 결제 실패(다른 결제 옵션으로 재시도 가능) · `EXPIRED` 결제 요청 만료 · `PENDING` 결제 전이에요. 값이 추가될 수 있어 문자열로 받아요 — 모르는 값은 `PROCESSING`처럼 다루세요"),
|
|
289
|
+
orderId: z.string().nullable().describe("결제가 완료됐으면 주문 번호, 그 외에는 null"),
|
|
290
|
+
lastFailure: z.object({
|
|
291
|
+
attemptId: z.string(),
|
|
292
|
+
category: z.string().describe("`PREPARE_FAILURE`·`DECLINED`·`USER_CANCELED`(값이 추가될 수 있다)"),
|
|
293
|
+
code: z.string().nullable().describe("PG가 준 실패 코드. 내부 사유는 null이에요. 화면 분기는 `category`로 하세요"),
|
|
294
|
+
message: z.string().nullable().describe("구매자에게 보여 줄 안내 문구")
|
|
295
|
+
}).nullable().describe("현재 결제 시도가 실패·취소로 끝났으면 그 내용, 아니면 null"),
|
|
296
|
+
testPayment: z.boolean().describe("테스트 결제(샌드박스)인가. true면 실제로 돈이 나가지 않았어요"),
|
|
297
|
+
expiresAt: z.string().describe("결제 요청 만료 시각")
|
|
298
|
+
});
|
|
299
|
+
/**
|
|
300
|
+
* 결제 서비스가 팝업 opener에 보내는 메시지 — `type`으로 거르고 결과의 원천은 결제 상태 조회다.
|
|
301
|
+
* 안내 문구는 싣지 않는다. 화면 문구는 상태 조회의 `lastFailure`를 쓴다
|
|
302
|
+
*/
|
|
303
|
+
const paymentMessageSchema = z.object({
|
|
304
|
+
type: z.literal("sayren:payment"),
|
|
305
|
+
paymentId: z.string(),
|
|
306
|
+
result: z.string(),
|
|
307
|
+
orderId: z.string().optional()
|
|
308
|
+
});
|
|
309
|
+
//#endregion
|
|
310
|
+
export { startPaymentRequestSchema as A, paymentStartSchema as C, residentNumberLike as D, remoteAreaSchema as E, retryPaymentRequestSchema as O, paymentOptionSchema as S, pgProviderSchema as T, paymentFailureCategorySchema as _, cashReceiptAvailable as a, paymentOptionKey as b, cashReceiptTypeSchema as c, createCheckoutRequestSchema as d, deliveryQuoteRequestSchema as f, guestInfoSchema as g, easyPayProviderSchema as h, availablePaymentOptionSchema as i, shippingAddressInputSchema as k, checkoutDeliverySchema as l, easyPayProviderResponseSchema as m, PAYMENT_RESULT_VALUES as n, cashReceiptIdentityTypeOf as o, deliveryQuoteSchema as p, PAYMENT_STATUS_VALUES as r, cashReceiptRequestSchema as s, CASH_RECEIPT_METHODS as t, checkoutSessionSchema as u, paymentMessageSchema as v, paymentStatusSchema as w, paymentOptionListSchema as x, paymentMethodSchema as y };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { a as ANALYTICS_VISITOR_HEADER, r as ANALYTICS_SESSION_HEADER } from "./analytics-CgD70OqH.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { A as startPaymentRequestSchema, C as paymentStartSchema, O as retryPaymentRequestSchema, d as createCheckoutRequestSchema, f as deliveryQuoteRequestSchema, m as easyPayProviderResponseSchema, p as deliveryQuoteSchema, u as checkoutSessionSchema, w as paymentStatusSchema, y as paymentMethodSchema } from "./checkout-CHpxo2Ap.mjs";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
//#region src/http.ts
|
|
5
5
|
/** 표준 엔벨로프의 $.error 상세 */
|
|
@@ -177,6 +177,18 @@ const socialProviderSchema = z.enum([
|
|
|
177
177
|
"naver",
|
|
178
178
|
"google"
|
|
179
179
|
]);
|
|
180
|
+
/**
|
|
181
|
+
* 소셜 가입 화면에서 받은 필수·선택 동의 — 공급자로 보내기 전에 함께 보낸다.
|
|
182
|
+
*
|
|
183
|
+
* **이 흐름으로 회원이 처음 만들어질 때만 쓴다.** 이미 있는 회원의 로그인이면 무시한다(동의는 늘어나지 않는다).
|
|
184
|
+
* 보내지 않으면 필수 동의 이력이 남지 않아 `GET /me`의 `reconsentRequired`가 true가 되고,
|
|
185
|
+
* 스토어프론트가 첫 진입에서 `POST /me/consents`로 받는다(가입 자체는 거부하지 않는다).
|
|
186
|
+
*/
|
|
187
|
+
const idpAgreementsSchema = z.object({
|
|
188
|
+
terms: z.literal(true),
|
|
189
|
+
privacy: z.literal(true),
|
|
190
|
+
marketing: z.boolean().default(false)
|
|
191
|
+
});
|
|
180
192
|
/** `POST /auth/idp/{provider}/authorize` 본문 — PKCE(S256)와 state는 SDK가 만든다 */
|
|
181
193
|
const idpAuthorizeRequestSchema = z.object({
|
|
182
194
|
/** 로그인을 마치고 돌아올 스토어프론트 주소. 스토어프론트 도메인 목록 안이어야 한다 */
|
|
@@ -184,7 +196,9 @@ const idpAuthorizeRequestSchema = z.object({
|
|
|
184
196
|
/** base64url(SHA-256(codeVerifier)) */
|
|
185
197
|
codeChallenge: z.string().regex(/^[A-Za-z0-9_-]{43}$/),
|
|
186
198
|
/** 스토어프론트가 만든 값 — 돌아올 때 그대로 붙어 온다. 세션에 저장해 두고 비교한다 */
|
|
187
|
-
state: z.string().min(16).max(128)
|
|
199
|
+
state: z.string().min(16).max(128),
|
|
200
|
+
/** 가입이 되는 경우 남길 동의. 기존 회원 로그인이면 무시한다 */
|
|
201
|
+
agreements: idpAgreementsSchema.optional()
|
|
188
202
|
});
|
|
189
203
|
const idpAuthorizeResponseSchema = z.object({
|
|
190
204
|
/** 구매자를 보낼 공급자 로그인 주소 */
|
|
@@ -384,13 +398,31 @@ const createClaimRequestSchema = z.object({
|
|
|
384
398
|
reason: claimReasonSchema,
|
|
385
399
|
reasonDetail: z.string().max(1e3).optional(),
|
|
386
400
|
evidenceImages: z.array(z.url()).max(5).optional(),
|
|
387
|
-
exchangeOptionId: z.string().optional()
|
|
401
|
+
exchangeOptionId: z.string().optional(),
|
|
402
|
+
quantity: z.number().int().min(1).optional().describe("취소·반품·교환할 수량. 생략하면 남은 수량 전체다. 남은 수량보다 크면 400 `INVALID_QUANTITY`")
|
|
388
403
|
});
|
|
389
404
|
const createClaimResultSchema = z.object({
|
|
390
405
|
claimId: z.string(),
|
|
391
406
|
status: z.literal("REQUESTED"),
|
|
392
|
-
expectedRefundAmount: z.number().int().nullable(),
|
|
393
|
-
claimDeliveryFee: z.number().int()
|
|
407
|
+
expectedRefundAmount: z.number().int().nullable().describe("예상 환불액 — 수량 비율 상품 금액 − 반품 배송비. **배송비 환불은 여기 포함되지 않고 `deliveryFeeRefundAmount`로 따로 온다**(승인 시점의 배송 묶음 상태로 확정되기 때문이다). 교환은 환불이 없어 null이다"),
|
|
408
|
+
claimDeliveryFee: z.number().int().describe("구매자가 부담하는 반품·교환 배송비. 원 단위"),
|
|
409
|
+
quantity: z.number().int().describe("접수된 수량. 요청에서 생략하면 남은 수량 전체다"),
|
|
410
|
+
deliveryFeeRefundAmount: z.number().int().describe("환불 예정 배송비. 배송 묶음이 모두 취소되거나 판매자 귀책일 때만 0보다 크다")
|
|
411
|
+
});
|
|
412
|
+
/**
|
|
413
|
+
* 반품지 (이슈 #30) — 반품·교환 수거가 안내하는 주소다. 스토어 주소록의 기본 반품지이거나,
|
|
414
|
+
* 상품이 반품지를 따로 지정했으면 그 주소다.
|
|
415
|
+
*
|
|
416
|
+
* 취소 클레임은 보낼 물건이 없어 null이고, 반품지를 등록하지 않은 스토어도 null이다.
|
|
417
|
+
* 구매자가 직접 보내는 수거(`BUYER_SEND`)에 이 주소가 필요하다.
|
|
418
|
+
*/
|
|
419
|
+
const claimReturnAddressSchema = z.object({
|
|
420
|
+
name: z.string().describe("주소 이름(셀러가 붙인 별칭). 예: 본사 물류센터"),
|
|
421
|
+
contactName: z.string().describe("수취인 이름"),
|
|
422
|
+
phone: z.string().describe("연락처"),
|
|
423
|
+
zipCode: z.string(),
|
|
424
|
+
address1: z.string(),
|
|
425
|
+
address2: z.string().nullable()
|
|
394
426
|
});
|
|
395
427
|
const myClaimSchema = z.object({
|
|
396
428
|
claimId: z.string(),
|
|
@@ -398,13 +430,77 @@ const myClaimSchema = z.object({
|
|
|
398
430
|
status: claimStatusSchema,
|
|
399
431
|
orderItemId: z.string(),
|
|
400
432
|
productName: z.string(),
|
|
433
|
+
quantity: z.number().int().describe("접수된 수량"),
|
|
401
434
|
reason: z.string(),
|
|
402
|
-
expectedRefundAmount: z.number().int().nullable(),
|
|
435
|
+
expectedRefundAmount: z.number().int().nullable().describe("예상 환불액 — 수량 비율 상품 금액 − 반품 배송비 − 조건부 무료배송 미달 차감. **배송비 환불은 포함되지 않고 `expectedDeliveryFeeRefundAmount`로 따로 온다.** 교환은 null이다"),
|
|
436
|
+
expectedDeliveryFeeRefundAmount: z.number().int().nullable().describe("예상 배송비 환불액. 배송 묶음이 모두 취소되거나 판매자 귀책일 때만 0보다 크다. 이 필드가 생기기 전에 접수된 클레임은 null이다"),
|
|
403
437
|
rejectReason: z.string().nullable(),
|
|
438
|
+
returnAddress: claimReturnAddressSchema.nullable().describe("반품·교환 물건을 보낼 반품지(이슈 #30). 직접 보내는 수거 안내에 쓴다. 취소 클레임은 null"),
|
|
404
439
|
requestedAt: z.string(),
|
|
405
440
|
completedAt: z.string().nullable()
|
|
406
441
|
});
|
|
407
442
|
//#endregion
|
|
443
|
+
//#region src/schemas/consent.ts
|
|
444
|
+
/**
|
|
445
|
+
* 동의 종류 — 이용약관(`TERMS`)·개인정보 수집이용(`PRIVACY`)은 가입 필수, 마케팅 수신(`MARKETING`)은 선택이다.
|
|
446
|
+
* 필수 동의는 철회할 수 없다(철회 = 탈퇴).
|
|
447
|
+
*/
|
|
448
|
+
const consentKindSchema = z.enum([
|
|
449
|
+
"TERMS",
|
|
450
|
+
"PRIVACY",
|
|
451
|
+
"MARKETING"
|
|
452
|
+
]);
|
|
453
|
+
/** 동의 이력의 한 줄이 무엇인가 — 동의했거나 철회했다 */
|
|
454
|
+
const consentActionSchema = z.enum(["AGREED", "WITHDRAWN"]);
|
|
455
|
+
/**
|
|
456
|
+
* 동의가 일어난 경로. 값이 추가될 수 있어 모르는 값은 그대로 보여 준다.
|
|
457
|
+
* `SIGNUP` 비밀번호 가입 · `SOCIAL_SIGNUP` 소셜 가입 · `DELEGATED` 셀러 자체 회원 연동 ·
|
|
458
|
+
* `RECONSENT` 약관 개정 재동의 · `MEMBER_UPDATE` 구매자가 직접 변경 · `SELLER` 셀러가 처리
|
|
459
|
+
*/
|
|
460
|
+
const consentSourceSchema = z.enum([
|
|
461
|
+
"SIGNUP",
|
|
462
|
+
"SOCIAL_SIGNUP",
|
|
463
|
+
"DELEGATED",
|
|
464
|
+
"RECONSENT",
|
|
465
|
+
"MEMBER_UPDATE",
|
|
466
|
+
"SELLER"
|
|
467
|
+
]);
|
|
468
|
+
/** 종류별 현재 동의 상태 */
|
|
469
|
+
const consentStateSchema = z.object({
|
|
470
|
+
kind: consentKindSchema,
|
|
471
|
+
agreed: z.boolean(),
|
|
472
|
+
/** 동의한 약관 버전. 마케팅은 버전이 없어 null이고, 동의 이력이 없으면 null이다 */
|
|
473
|
+
agreedVersion: z.string().nullable(),
|
|
474
|
+
/** 스토어의 현재 약관 버전. 마케팅은 null */
|
|
475
|
+
currentVersion: z.string().nullable(),
|
|
476
|
+
/**
|
|
477
|
+
* 스토어가 정한 현재 약관 문서 주소. 재동의 화면에서 이 주소를 보여 준다.
|
|
478
|
+
* 스토어가 정하지 않았거나 마케팅이면 null이다(동의한 시점의 주소는 셀러가 보는 이력에 남는다).
|
|
479
|
+
*/
|
|
480
|
+
currentDocumentUrl: z.string().nullable(),
|
|
481
|
+
/** 마지막으로 동의·철회한 시각. 이력이 없으면 null */
|
|
482
|
+
updatedAt: z.string().nullable(),
|
|
483
|
+
/** 약관이 개정돼 다시 받아야 하는가. 마케팅은 항상 false */
|
|
484
|
+
reconsentRequired: z.boolean()
|
|
485
|
+
});
|
|
486
|
+
/** `GET /storefront/v1/me/consents` 응답 */
|
|
487
|
+
const memberConsentsSchema = z.object({
|
|
488
|
+
terms: consentStateSchema,
|
|
489
|
+
privacy: consentStateSchema,
|
|
490
|
+
marketing: consentStateSchema,
|
|
491
|
+
/** 필수 동의 중 하나라도 다시 받아야 하면 true — `POST /me/consents`로 받는다 */
|
|
492
|
+
reconsentRequired: z.boolean()
|
|
493
|
+
});
|
|
494
|
+
/**
|
|
495
|
+
* `POST /storefront/v1/me/consents` 본문 — 바꿀 항목만 보낸다.
|
|
496
|
+
* 필수 동의는 동의(`true`)만 보낼 수 있다. 이미 현재 버전에 동의한 항목은 이력을 더 남기지 않는다.
|
|
497
|
+
*/
|
|
498
|
+
const consentUpdateRequestSchema = z.object({
|
|
499
|
+
terms: z.literal(true).optional().describe("이용약관 재동의"),
|
|
500
|
+
privacy: z.literal(true).optional().describe("개인정보 수집이용 재동의"),
|
|
501
|
+
marketing: z.boolean().optional().describe("마케팅 수신 동의(false면 철회)")
|
|
502
|
+
}).refine((value) => Object.values(value).some((v) => v !== void 0), { message: "최소 1개 항목이 필요합니다" });
|
|
503
|
+
//#endregion
|
|
408
504
|
//#region src/schemas/customer-inquiry.ts
|
|
409
505
|
const customerInquiryCategorySchema = z.enum([
|
|
410
506
|
"DELIVERY",
|
|
@@ -466,7 +562,8 @@ const memberSchema = z.object({
|
|
|
466
562
|
phone: z.string().nullable(),
|
|
467
563
|
point: z.number().int(),
|
|
468
564
|
marketingAgreed: z.boolean(),
|
|
469
|
-
joinedAt: z.string()
|
|
565
|
+
joinedAt: z.string(),
|
|
566
|
+
reconsentRequired: z.boolean().describe("약관이 개정돼 필수 동의를 다시 받아야 하는가. true면 `GET /me/consents`로 어떤 항목인지 확인하고 `POST /me/consents`로 받는다. 받기 전에도 스토어 이용은 막히지 않는다")
|
|
470
567
|
});
|
|
471
568
|
const updateProfileRequestSchema = z.object({
|
|
472
569
|
name: z.string().max(50).optional(),
|
|
@@ -505,7 +602,9 @@ const myOrderItemSchema = z.object({
|
|
|
505
602
|
productName: z.string(),
|
|
506
603
|
thumbnailUrl: z.url().nullable().describe("주문 시점의 대표 이미지. 없었으면 null"),
|
|
507
604
|
optionName: z.string().nullable(),
|
|
508
|
-
quantity: z.number().int(),
|
|
605
|
+
quantity: z.number().int().describe("주문 수량"),
|
|
606
|
+
canceledQuantity: z.number().int().describe("취소·반품으로 환불된 누적 수량. 부분 취소가 없었으면 0이다"),
|
|
607
|
+
activeQuantity: z.number().int().describe("남은 수량 (`quantity` − `canceledQuantity`). 취소·반품·교환 요청 수량의 상한이다"),
|
|
509
608
|
totalPrice: z.number().int(),
|
|
510
609
|
status: orderItemStatusSchema,
|
|
511
610
|
claimStatus: z.string().nullable(),
|
|
@@ -539,6 +638,11 @@ const myOrderSchema = z.object({
|
|
|
539
638
|
}),
|
|
540
639
|
testPayment: z.boolean().describe("테스트 결제(샌드박스 결제) 주문인가. true면 실제로 돈이 나가지 않았어요 — 주문 화면에 테스트 결제 배지를 표시해요")
|
|
541
640
|
});
|
|
641
|
+
/**
|
|
642
|
+
* 택배 추적 단계. 응답에서는 문자열로 받는다(값이 추가될 수 있다) — 현재 값은 `ACCEPTED`(접수)·`IN_TRANSIT`(이동 중)·
|
|
643
|
+
* `OUT_FOR_DELIVERY`(배송 출발)·`DELIVERED`(배송완료)다.
|
|
644
|
+
*/
|
|
645
|
+
const trackingStageResponseSchema = z.string();
|
|
542
646
|
const deliveryTrackingSchema = z.object({
|
|
543
647
|
carrierName: z.string(),
|
|
544
648
|
trackingNumber: z.string(),
|
|
@@ -549,10 +653,13 @@ const deliveryTrackingSchema = z.object({
|
|
|
549
653
|
"DELIVERED"
|
|
550
654
|
]),
|
|
551
655
|
trackingEvents: z.array(z.object({
|
|
552
|
-
status: z.string(),
|
|
553
|
-
location: z.string(),
|
|
554
|
-
occurredAt: z.string()
|
|
555
|
-
|
|
656
|
+
status: z.string().describe("택배사가 알려 준 처리 내용(예: 집화처리, 배송출발)"),
|
|
657
|
+
location: z.string().describe("처리 장소. 모르면 빈 문자열"),
|
|
658
|
+
occurredAt: z.string(),
|
|
659
|
+
stage: trackingStageResponseSchema.nullable().optional().default(null).describe("이 이벤트의 추적 단계. 배달 실패처럼 단계로 나눌 수 없는 이벤트는 null. 서버는 항상 싣는다. 현재 값은 `ACCEPTED`·`IN_TRANSIT`·`OUT_FOR_DELIVERY`·`DELIVERED`이고 값이 추가될 수 있어 문자열로 받는다")
|
|
660
|
+
})).describe("배송 추적 이력. 시각 오름차순(오래된 것부터)"),
|
|
661
|
+
trackingStage: trackingStageResponseSchema.nullable().optional().default(null).describe("현재 추적 단계. 택배 발송이 아니거나, 아직 조회되지 않았거나, 택배사 전산에 송장이 없으면 null. 서버는 항상 싣는다. 현재 값은 `ACCEPTED`(접수)·`IN_TRANSIT`(이동 중)·`OUT_FOR_DELIVERY`(배송 출발)·`DELIVERED`(배송완료)이고 값이 추가될 수 있어 문자열로 받는다"),
|
|
662
|
+
trackingCheckedAt: z.string().nullable().optional().default(null).describe("택배사에서 추적 정보를 마지막으로 받아 온 시각(ISO 8601). 받아 온 적이 없으면 null. 서버는 항상 싣는다")
|
|
556
663
|
});
|
|
557
664
|
/** `POST /me/order-items/{orderItemId}/purchase-decision` 응답 */
|
|
558
665
|
const purchaseDecisionResultSchema = z.object({
|
|
@@ -722,6 +829,16 @@ get: () => http.request("GET", "/store", storefrontStoreSchema) },
|
|
|
722
829
|
checkout: {
|
|
723
830
|
create: (body) => http.request("POST", "/checkout", checkoutSessionSchema, { body: createCheckoutRequestSchema.parse(body) }),
|
|
724
831
|
/**
|
|
832
|
+
* `POST /checkout/{checkoutId}/delivery-quote` — 배송비 미리보기. 배송지 우편번호로 배송비를 다시 계산한다.
|
|
833
|
+
*
|
|
834
|
+
* 주문서를 만들 때는 배송지를 모르므로 제주·도서산간 추가 배송비가 빠져 있다. 구매자가 배송지를 입력하거나
|
|
835
|
+
* 고칠 때 이 메서드로 금액을 갱신하면 결제 직전에 금액이 바뀌는 일을 피할 수 있다. 주문서를 바꾸지 않는
|
|
836
|
+
* 읽기 계산이고, **최종 금액은 결제 시작 응답의 `amounts`다.**
|
|
837
|
+
*
|
|
838
|
+
* 에러(`ApiError.code`): 404 `CHECKOUT_NOT_FOUND`, 409 `CHECKOUT_EXPIRED`, 409 `ALREADY_PAID`
|
|
839
|
+
*/
|
|
840
|
+
quoteDelivery: (checkoutId, body) => http.request("POST", path`/checkout/${checkoutId}/delivery-quote`, deliveryQuoteSchema, { body: deliveryQuoteRequestSchema.parse(body) }),
|
|
841
|
+
/**
|
|
725
842
|
* `POST /checkout/{checkoutId}/payment` — 결제 시작. 배송지와 결제 옵션(`paymentOptions` 항목 또는 직접 쓴 옵션)으로
|
|
726
843
|
* 결제 세션·첫 시도를 만들고 결제 서비스 주소(`payUrl`)를 돌려준다. 그 주소를 열어 결제를 진행하는 것은
|
|
727
844
|
* `@sayren/storefront-sdk/payments`의 `createPayments().start()`다 — 이 메서드는 값만 받는다.
|
|
@@ -765,6 +882,10 @@ get: () => http.request("GET", "/store", storefrontStoreSchema) },
|
|
|
765
882
|
member: {
|
|
766
883
|
me: () => http.request("GET", "/me", memberSchema),
|
|
767
884
|
updateMe: (body) => http.requestVoid("PATCH", "/me", { body: updateProfileRequestSchema.parse(body) }),
|
|
885
|
+
/** 약관·개인정보·마케팅 동의의 현재 상태와 재동의 필요 여부 */
|
|
886
|
+
consents: () => http.request("GET", "/me/consents", memberConsentsSchema),
|
|
887
|
+
/** 재동의·마케팅 동의 변경 — 바꾼 뒤의 상태를 돌려준다 */
|
|
888
|
+
updateConsents: (body) => http.request("POST", "/me/consents", memberConsentsSchema, { body: consentUpdateRequestSchema.parse(body) }),
|
|
768
889
|
listAddresses: () => http.request("GET", "/me/addresses", z.array(memberAddressSchema)),
|
|
769
890
|
createAddress: (body) => http.requestVoid("POST", "/me/addresses", { body: memberAddressRequestSchema.parse(body) }),
|
|
770
891
|
updateAddress: (addressId, body) => http.requestVoid("PUT", path`/me/addresses/${addressId}`, { body: memberAddressRequestSchema.parse(body) }),
|
|
@@ -788,4 +909,4 @@ get: () => http.request("GET", "/store", storefrontStoreSchema) },
|
|
|
788
909
|
};
|
|
789
910
|
}
|
|
790
911
|
//#endregion
|
|
791
|
-
export {
|
|
912
|
+
export { updateCartItemRequestSchema as $, consentSourceSchema as A, categoryNodeSchema as B, createInquiryResultSchema as C, customerInquirySchema as D, customerInquiryCategorySchema as E, claimReturnAddressSchema as F, productSortSchema as G, productDetailSchema as H, claimTypeSchema as I, pageInfoSchema as J, claimStatusSchema as K, createClaimRequestSchema as L, consentUpdateRequestSchema as M, memberConsentsSchema as N, consentActionSchema as O, claimReasonSchema as P, cartSchema as Q, createClaimResultSchema as R, createInquiryRequestSchema as S, createCustomerInquiryRequestSchema as T, productFacetsSchema as U, productCardSchema as V, productPageSchema as W, addCartItemRequestSchema as X, pageSchema as Y, cartItemSchema as Z, memberAddressRequestSchema as _, publicReviewSchema as a, linkIdentityRequestSchema as at, updateProfileRequestSchema as b, updateReviewRequestSchema as c, signupRequestSchema as ct, deliveryTrackingSchema as d, withdrawRequestSchema as dt, anonymousSessionSchema as et, myOrderItemSchema as f, withdrawalBlockedDetailsSchema as ft, trackingStageResponseSchema as g, unwrapData as gt, purchaseDecisionResultSchema as h, toApiError as ht, createReviewResultSchema as i, idpTokenRequestSchema as it, consentStateSchema as j, consentKindSchema as k, updateReviewResultSchema as l, socialProviderSchema as lt, orderShippingAddressSchema as m, buildQuery as mt, storefrontStoreSchema as n, idpAuthorizeRequestSchema as nt, reviewPageSchema as o, loginRequestSchema as ot, myOrderSchema as p, ApiError as pt, orderItemStatusSchema as q, createReviewRequestSchema as r, idpAuthorizeResponseSchema as rt, reviewSummarySchema as s, memberIdentitiesSchema as st, createStorefrontClient as t, idpAgreementsSchema as tt, writableReviewSchema as u, tokenPairSchema as ut, memberAddressSchema as v, publicInquirySchema as w, wishlistAddResultSchema as x, memberSchema as y, myClaimSchema as z };
|