@sayren/store-sdk 0.8.0 → 0.10.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/index.d.mts +557 -11
- package/dist/index.mjs +406 -36
- package/package.json +1 -1
- package/src/client.ts +79 -2
- package/src/index.ts +1 -0
- package/src/schemas/claim.ts +38 -4
- package/src/schemas/common.ts +3 -0
- package/src/schemas/customer.ts +72 -0
- package/src/schemas/order.ts +35 -3
- package/src/schemas/product.ts +84 -10
- package/src/schemas/refund-policy.ts +163 -0
- package/src/schemas/store.ts +85 -0
- package/src/schemas/upload.ts +86 -0
- package/src/schemas/webhook-event.ts +40 -0
package/src/schemas/store.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { storeRoleSchema } from "./auth";
|
|
3
|
+
import { refundPolicyPatchSchema, refundPolicySchema } from "./refund-policy";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* 스토어 코드 규칙 — 스토어프론트 API가 테넌트를 가르는 값이다(`X-Store-Code` 헤더).
|
|
@@ -102,6 +103,38 @@ export const analyticsOriginSchema = z
|
|
|
102
103
|
/** 허용 오리진 상한 */
|
|
103
104
|
export const MAX_ANALYTICS_ALLOWED_ORIGINS = 20;
|
|
104
105
|
|
|
106
|
+
/** 자동 배송완료·자동 구매확정 대기일 상한 */
|
|
107
|
+
export const MAX_AUTO_TRANSITION_DAYS = 30;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* 약관 버전 — 스토어가 정하는 문자열이다(`1`·`2`·`2026-09-26`·`v1.2` 등). 형식은 강제하지 않고 값이 다르면 다른 버전이다.
|
|
111
|
+
* 올리면 그 약관에 동의한 구매자의 `reconsentRequired`가 켜지고, 재동의가 이력에 남는다. 되돌리면 재동의 요구도 풀린다.
|
|
112
|
+
*/
|
|
113
|
+
export const policyVersionSchema = z
|
|
114
|
+
.string()
|
|
115
|
+
.trim()
|
|
116
|
+
.min(1)
|
|
117
|
+
.max(20)
|
|
118
|
+
.describe("약관 버전 문자열(1~20자). 예: `1`, `2026-09-26`");
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 약관 문서 주소 — 구매자에게 보여 주는 약관 본문의 주소다. 동의 시점 값이 이력 행에 복사되므로
|
|
122
|
+
* 나중에 주소를 바꿔도 "무엇에 동의했는지"가 남는다. `null`로 지울 수 있다.
|
|
123
|
+
*/
|
|
124
|
+
export const policyDocumentUrlSchema = z
|
|
125
|
+
.url()
|
|
126
|
+
.max(500)
|
|
127
|
+
.refine((value) => value.startsWith("https://"), "https 주소만 쓸 수 있어요")
|
|
128
|
+
.nullable();
|
|
129
|
+
|
|
130
|
+
const autoDeliverDaysDescription =
|
|
131
|
+
"발송 후 자동 배송완료까지의 일수(0~30, 기본 7). 발송(`DISPATCHED`)·배송중(`DELIVERING`) 주문상품을 " +
|
|
132
|
+
"이 기간이 지나면 배송완료(`DELIVERED`)로 바꾼다. `0`이면 자동 배송완료를 하지 않는다";
|
|
133
|
+
|
|
134
|
+
const autoConfirmDaysDescription =
|
|
135
|
+
"배송완료 후 자동 구매확정까지의 일수(0~30, 기본 7). 배송완료 시점에 구매확정 예정일(`autoDecisionDate`)을 이 값으로 잡고, " +
|
|
136
|
+
"그 날짜가 지나면 구매확정(`PURCHASE_DECIDED`)으로 바꾼다. 진행 중인 클레임이 있으면 건너뛴다. `0`이면 자동 구매확정을 하지 않는다";
|
|
137
|
+
|
|
105
138
|
/** `PATCH /v1/store` 본문 — 바꿀 필드만 보낸다 */
|
|
106
139
|
export const updateStoreRequestSchema = z
|
|
107
140
|
.object({
|
|
@@ -132,6 +165,41 @@ export const updateStoreRequestSchema = z
|
|
|
132
165
|
"비회원 주문 허용. false(회원만)면 로그인하지 않은 구매자의 주문서 생성·결제 요청이 403 `GUEST_CHECKOUT_DISABLED`다. " +
|
|
133
166
|
"장바구니 담기와 이미 받은 비회원 주문의 조회·취소는 막지 않는다",
|
|
134
167
|
),
|
|
168
|
+
autoDeliverDays: z
|
|
169
|
+
.number()
|
|
170
|
+
.int()
|
|
171
|
+
.min(0)
|
|
172
|
+
.max(MAX_AUTO_TRANSITION_DAYS)
|
|
173
|
+
.optional()
|
|
174
|
+
.describe(autoDeliverDaysDescription),
|
|
175
|
+
autoConfirmDays: z
|
|
176
|
+
.number()
|
|
177
|
+
.int()
|
|
178
|
+
.min(0)
|
|
179
|
+
.max(MAX_AUTO_TRANSITION_DAYS)
|
|
180
|
+
.optional()
|
|
181
|
+
.describe(autoConfirmDaysDescription),
|
|
182
|
+
refundPolicy: refundPolicyPatchSchema
|
|
183
|
+
.optional()
|
|
184
|
+
.describe(
|
|
185
|
+
"스토어 기본 환불 정책 — 준 항목만 바꾼다. 상품별 정책(`products.refundPolicy`)이 없는 상품에 적용된다",
|
|
186
|
+
),
|
|
187
|
+
termsVersion: policyVersionSchema
|
|
188
|
+
.optional()
|
|
189
|
+
.describe(
|
|
190
|
+
"이용약관 버전. 올리면 이 약관에 동의한 구매자에게 재동의를 요구한다(`GET /storefront/v1/me`의 `reconsentRequired`)",
|
|
191
|
+
),
|
|
192
|
+
privacyVersion: policyVersionSchema
|
|
193
|
+
.optional()
|
|
194
|
+
.describe("개인정보 수집이용 동의 버전. 올리면 구매자에게 재동의를 요구한다"),
|
|
195
|
+
termsUrl: policyDocumentUrlSchema
|
|
196
|
+
.optional()
|
|
197
|
+
.describe(
|
|
198
|
+
"이용약관 문서 주소(https). 동의 시점의 값이 동의 이력에 복사된다. `null`을 주면 지운다",
|
|
199
|
+
),
|
|
200
|
+
privacyUrl: policyDocumentUrlSchema
|
|
201
|
+
.optional()
|
|
202
|
+
.describe("개인정보 수집이용 동의 문서 주소(https). `null`을 주면 지운다"),
|
|
135
203
|
})
|
|
136
204
|
.refine((value) => Object.values(value).some((v) => v !== undefined), {
|
|
137
205
|
message: "최소 1개 필드가 필요합니다",
|
|
@@ -156,6 +224,23 @@ export const storeSchema = z.object({
|
|
|
156
224
|
"스토어프론트 도메인 — 소셜 로그인 리디렉트와 방문 분석 수집이 함께 쓴다(`analyticsAllowedOrigins`와 같은 값)",
|
|
157
225
|
),
|
|
158
226
|
guestCheckoutEnabled: z.boolean().describe("비회원 주문 허용. false면 회원만 주문할 수 있다"),
|
|
227
|
+
autoDeliverDays: z.number().int().describe(autoDeliverDaysDescription),
|
|
228
|
+
autoConfirmDays: z.number().int().describe(autoConfirmDaysDescription),
|
|
229
|
+
refundPolicy: refundPolicySchema.describe(
|
|
230
|
+
"스토어 기본 환불 정책. 스토어가 정하지 않은 항목은 플랫폼 기본값으로 채워 항상 전부 싣는다",
|
|
231
|
+
),
|
|
232
|
+
termsVersion: z.string().describe("현재 이용약관 버전. 가입·재동의가 이 값으로 이력에 남는다"),
|
|
233
|
+
privacyVersion: z.string().describe("현재 개인정보 수집이용 동의 버전"),
|
|
234
|
+
termsVersionUpdatedAt: z
|
|
235
|
+
.string()
|
|
236
|
+
.nullable()
|
|
237
|
+
.describe("이용약관 버전을 마지막으로 바꾼 시각. 바꾼 적이 없으면 null"),
|
|
238
|
+
privacyVersionUpdatedAt: z.string().nullable(),
|
|
239
|
+
termsUrl: z
|
|
240
|
+
.string()
|
|
241
|
+
.nullable()
|
|
242
|
+
.describe("이용약관 문서 주소. 정하지 않았으면 null이고 동의 이력에도 주소가 남지 않는다"),
|
|
243
|
+
privacyUrl: z.string().nullable().describe("개인정보 수집이용 동의 문서 주소"),
|
|
159
244
|
memberCount: z.number().int(),
|
|
160
245
|
createdAt: z.string(),
|
|
161
246
|
});
|
package/src/schemas/upload.ts
CHANGED
|
@@ -1,5 +1,82 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* 업로드 용도. 허용 확장자와 크기 상한이 용도별로 다르다.
|
|
5
|
+
*
|
|
6
|
+
* - `product-image`: 상품 목록·상세의 상품 이미지. 목록 썸네일로 쓰이므로 정지 이미지만 받는다.
|
|
7
|
+
* - `description`: 상세설명 본문에 삽입하는 미디어. 동영상과 svg도 받는다.
|
|
8
|
+
*/
|
|
9
|
+
export const uploadPurposeSchema = z.enum(["product-image", "description"]);
|
|
10
|
+
|
|
11
|
+
export type UploadPurpose = z.infer<typeof uploadPurposeSchema>;
|
|
12
|
+
|
|
13
|
+
export interface UploadPurposeLimit {
|
|
14
|
+
/** 소문자 확장자 목록 */
|
|
15
|
+
extensions: readonly string[];
|
|
16
|
+
maxBytes: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const MB = 1024 * 1024;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 용도별 업로드 제한 — 서버(`POST /v1/uploads`)와 클라이언트가 공유하는 단일 원천이다.
|
|
23
|
+
*
|
|
24
|
+
* 크기는 **요청이 스스로 신고한 값**으로만 검사한다. 바이트는 api를 거치지 않고 CDN으로
|
|
25
|
+
* 직접 PUT되므로 이 검사는 셀러가 올리기 전에 알게 하는 장치이고, 실제 상한은 CDN이 강제한다.
|
|
26
|
+
*/
|
|
27
|
+
export const UPLOAD_PURPOSE_LIMITS: Record<UploadPurpose, UploadPurposeLimit> = {
|
|
28
|
+
// svg는 스크립트를 품을 수 있고, 동영상은 목록 썸네일 자리에 쓸 수 없다
|
|
29
|
+
"product-image": {
|
|
30
|
+
extensions: ["jpg", "jpeg", "png", "webp", "avif", "gif"],
|
|
31
|
+
maxBytes: 10 * MB,
|
|
32
|
+
},
|
|
33
|
+
description: {
|
|
34
|
+
extensions: ["jpg", "jpeg", "png", "gif", "webp", "avif", "svg", "mp4", "webm", "mov"],
|
|
35
|
+
maxBytes: 50 * MB,
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/** 파일명의 소문자 확장자. 확장자가 없으면 `""` */
|
|
40
|
+
export function uploadExtensionOf(filename: string): string {
|
|
41
|
+
const dot = filename.lastIndexOf(".");
|
|
42
|
+
if (dot < 0 || dot === filename.length - 1) return "";
|
|
43
|
+
return filename.slice(dot + 1).toLowerCase();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface UploadRejection {
|
|
47
|
+
/** 400 형식 불일치 / 413 크기 초과 */
|
|
48
|
+
status: 400 | 413;
|
|
49
|
+
code: "UNSUPPORTED_FILE_TYPE" | "FILE_TOO_LARGE";
|
|
50
|
+
message: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 용도 제한 위반 여부. 통과하면 `null`.
|
|
55
|
+
* 서버가 던지는 오류와 콘솔이 먼저 띄우는 안내가 갈리지 않도록 한 함수로 판정한다.
|
|
56
|
+
*/
|
|
57
|
+
export function uploadRejection(
|
|
58
|
+
purpose: UploadPurpose,
|
|
59
|
+
filename: string,
|
|
60
|
+
size?: number,
|
|
61
|
+
): UploadRejection | null {
|
|
62
|
+
const limit = UPLOAD_PURPOSE_LIMITS[purpose];
|
|
63
|
+
if (!limit.extensions.includes(uploadExtensionOf(filename))) {
|
|
64
|
+
return {
|
|
65
|
+
status: 400,
|
|
66
|
+
code: "UNSUPPORTED_FILE_TYPE",
|
|
67
|
+
message: `지원하지 않는 파일 형식입니다. ${limit.extensions.join(", ")}만 올릴 수 있습니다`,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
if (size !== undefined && size > limit.maxBytes) {
|
|
71
|
+
return {
|
|
72
|
+
status: 413,
|
|
73
|
+
code: "FILE_TOO_LARGE",
|
|
74
|
+
message: `파일이 너무 큽니다. ${Math.floor(limit.maxBytes / MB)}MB까지 올릴 수 있습니다`,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
3
80
|
/**
|
|
4
81
|
* 미디어 업로드 자리 발급 — `POST /v1/uploads`.
|
|
5
82
|
*
|
|
@@ -9,6 +86,15 @@ import { z } from "zod";
|
|
|
9
86
|
*/
|
|
10
87
|
export const uploadRequestSchema = z.object({
|
|
11
88
|
filename: z.string().min(1).max(255),
|
|
89
|
+
purpose: uploadPurposeSchema
|
|
90
|
+
.default("description")
|
|
91
|
+
.describe("업로드 용도. 허용 확장자와 크기 상한이 용도별로 다르다"),
|
|
92
|
+
size: z
|
|
93
|
+
.number()
|
|
94
|
+
.int()
|
|
95
|
+
.positive()
|
|
96
|
+
.optional()
|
|
97
|
+
.describe("파일 바이트 크기. 주면 용도별 상한을 발급 전에 검사한다"),
|
|
12
98
|
});
|
|
13
99
|
|
|
14
100
|
export type UploadRequest = z.infer<typeof uploadRequestSchema>;
|
|
@@ -44,6 +44,46 @@ export const claimWebhookEventDataSchema = z.object({
|
|
|
44
44
|
orderItemId: z.string().describe("클레임 대상 주문 상품 ID"),
|
|
45
45
|
claimType: claimTypeSchema.describe("클레임 유형"),
|
|
46
46
|
status: claimStatusSchema.describe("이벤트 발생 시점의 클레임 상태"),
|
|
47
|
+
quantity: z
|
|
48
|
+
.number()
|
|
49
|
+
.int()
|
|
50
|
+
.optional()
|
|
51
|
+
.default(0)
|
|
52
|
+
.describe("클레임 대상 수량. 이 필드가 없는 이전 이벤트는 0으로 읽는다"),
|
|
53
|
+
refundAmount: z
|
|
54
|
+
.number()
|
|
55
|
+
.int()
|
|
56
|
+
.nullable()
|
|
57
|
+
.optional()
|
|
58
|
+
.default(null)
|
|
59
|
+
.describe(
|
|
60
|
+
"환불액 (원). 배송비 환불을 포함하고 배송비 차감·추가 차감을 뺀 실제 금액이다. " +
|
|
61
|
+
"환불이 확정되지 않은 접수 이벤트는 예상 환불액이고, 교환처럼 환불이 없으면 null이다",
|
|
62
|
+
),
|
|
63
|
+
deliveryFeeRefundAmount: z
|
|
64
|
+
.number()
|
|
65
|
+
.int()
|
|
66
|
+
.optional()
|
|
67
|
+
.default(0)
|
|
68
|
+
.describe("환불액에 포함된 배송비 (원). 이 필드가 없는 이전 이벤트는 0으로 읽는다"),
|
|
69
|
+
deliveryFeeChargeAmount: z
|
|
70
|
+
.number()
|
|
71
|
+
.int()
|
|
72
|
+
.optional()
|
|
73
|
+
.default(0)
|
|
74
|
+
.describe(
|
|
75
|
+
"환불액에서 뺀 배송비 (원) — 반품·교환 배송비와 조건부 무료배송 미달 차감의 합. " +
|
|
76
|
+
"셀러가 승인에 넣은 추가 차감은 `deductAmount`로 따로 온다. 이 필드가 없는 이전 이벤트는 0으로 읽는다",
|
|
77
|
+
),
|
|
78
|
+
deductAmount: z
|
|
79
|
+
.number()
|
|
80
|
+
.int()
|
|
81
|
+
.optional()
|
|
82
|
+
.default(0)
|
|
83
|
+
.describe(
|
|
84
|
+
"환불액에서 뺀 추가 차감 (원) — 셀러가 클레임 승인에 넣은 금액이다(상품 훼손 등). " +
|
|
85
|
+
"배송비 차감과 별개이므로 `deliveryFeeChargeAmount`와 더해서 뺀다. 접수 이벤트와 이 필드가 없는 이전 이벤트는 0이다",
|
|
86
|
+
),
|
|
47
87
|
testPayment: z
|
|
48
88
|
.boolean()
|
|
49
89
|
.optional()
|