@sayren/store-sdk 0.49.0 → 0.51.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 +671 -414
- package/dist/index.mjs +341 -10
- package/dist/server.d.mts +3 -1
- package/dist/server.mjs +2 -1
- package/dist/userinfo-n1JYxWAz.d.mts +90 -0
- package/package.json +1 -1
- package/src/client.ts +29 -0
- package/src/index.ts +1 -0
- package/src/member-delegation.ts +4 -0
- package/src/schemas/condition-tree.ts +103 -3
- package/src/schemas/coupon-rules.ts +178 -0
- package/src/schemas/coupon.ts +92 -5
- package/src/schemas/customer-delegation.ts +6 -0
- package/src/schemas/customer.ts +4 -0
- package/src/schemas/userinfo.ts +118 -0
- package/dist/webhook-headers-D6QmPdEJ.d.mts +0 -22
package/src/schemas/coupon.ts
CHANGED
|
@@ -35,17 +35,34 @@ export const couponStatusSchema = z
|
|
|
35
35
|
export type CouponStatus = z.infer<typeof couponStatusSchema>;
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
|
-
* 발급 방식 — 코드 입력·셀러 지급·다운로드(구매자가 받기)·회원가입 자동 지급(이슈 #110).
|
|
38
|
+
* 발급 방식 — 코드 입력·셀러 지급·다운로드(구매자가 받기)·회원가입 자동 지급(이슈 #110)·흐름 지급(이슈 #264).
|
|
39
39
|
* 응답(`couponSchema.issueMethod`)은 값이 늘 수 있어 문자열로 받는다. 이 enum은 요청 검증용이다.
|
|
40
40
|
*/
|
|
41
41
|
export const couponIssueMethodSchema = z
|
|
42
|
-
.enum(["CODE", "MANUAL", "DOWNLOAD", "SIGNUP"])
|
|
42
|
+
.enum(["CODE", "MANUAL", "DOWNLOAD", "SIGNUP", "FLOW", "VOUCHER"])
|
|
43
43
|
.describe(
|
|
44
44
|
"발급 방식. `CODE`=구매자가 주문서에 코드를 입력한다, `MANUAL`=셀러가 회원을 골라 지급한다, " +
|
|
45
|
-
"`DOWNLOAD`=구매자가 스토어프론트에서 받는다, `SIGNUP`=회원가입 때 자동으로
|
|
45
|
+
"`DOWNLOAD`=구매자가 스토어프론트에서 받는다, `SIGNUP`=회원가입 때 자동으로 지급한다, " +
|
|
46
|
+
"`FLOW`=주문 흐름의 쿠폰 지급 단계가 그 주문의 구매자에게 지급한다, " +
|
|
47
|
+
"`VOUCHER`=셀러가 1회용 쿠폰 번호를 만들어 나눠 주고 구매자가 번호를 등록하면 지급한다",
|
|
46
48
|
);
|
|
47
49
|
export type CouponIssueMethod = z.infer<typeof couponIssueMethodSchema>;
|
|
48
50
|
|
|
51
|
+
/** 쿠폰 번호 접두어 — 영문·숫자 2~10자, 대문자로 저장한다. 상점 안에서 유일하다 */
|
|
52
|
+
export const VOUCHER_PREFIX_PATTERN = /^[A-Za-z0-9]{2,10}$/;
|
|
53
|
+
/** 쿠폰 번호 한 쿠폰의 최대 개수 */
|
|
54
|
+
export const VOUCHER_COUNT_MAX = 100_000;
|
|
55
|
+
|
|
56
|
+
/** 쿠폰 번호 설정(`VOUCHER`, 이슈 #264) — 만든 뒤에는 바꿀 수 없다 */
|
|
57
|
+
export const couponVoucherInputSchema = z
|
|
58
|
+
.strictObject({
|
|
59
|
+
prefix: z.string().regex(VOUCHER_PREFIX_PATTERN).describe("번호 앞부분(영문·숫자 2~10자)"),
|
|
60
|
+
count: z.number().int().min(1).max(VOUCHER_COUNT_MAX).describe("만들 번호 수(1~100,000)"),
|
|
61
|
+
})
|
|
62
|
+
.describe(
|
|
63
|
+
"쿠폰 번호 설정 — 1회용 번호 `count`개를 만든다. 번호는 `GET /v1/coupons/{couponId}/voucher-codes`로 받는다",
|
|
64
|
+
);
|
|
65
|
+
|
|
49
66
|
/** 코드 형식 — 영문·숫자·`-`·`_` 6~30자. 대소문자를 구분하지 않고 서버가 대문자로 저장한다 */
|
|
50
67
|
export const COUPON_CODE_PATTERN = /^[A-Za-z0-9_-]{6,30}$/;
|
|
51
68
|
|
|
@@ -70,6 +87,8 @@ const descriptions = {
|
|
|
70
87
|
totalIssueLimit:
|
|
71
88
|
"총 발급 수 한도. 생략하면 무제한. 다운로드·회원가입 쿠폰은 한도에 닿으면 더 발급하지 않는다",
|
|
72
89
|
totalUseLimit: "총 사용 수 한도. 결제 중인 예약도 센다. 생략하면 무제한",
|
|
90
|
+
dailyIssueLimit:
|
|
91
|
+
"하루 발급 수 한도(이슈 #222) — 한국 시간 0시 기준 하루마다 이 장수까지 발급한다(받기·회원가입·셀러 지급). 생략하면 없다. 코드 입력형에는 두지 않는다",
|
|
73
92
|
perMemberIssueLimit:
|
|
74
93
|
"1인 발급 수 한도(셀러 지급·다운로드). 생략하면 1인 1장. 회원가입 쿠폰은 1장 고정이라 1만 받는다",
|
|
75
94
|
perMemberUseLimit:
|
|
@@ -123,6 +142,7 @@ export function couponRequestRuleIssues(body: {
|
|
|
123
142
|
audience?: CouponRuleTrees["eligibility"];
|
|
124
143
|
stacking?: { perOrderLimit: number };
|
|
125
144
|
perMemberUseLimit?: number | null;
|
|
145
|
+
dailyIssueLimit?: number | null;
|
|
126
146
|
issueStartsAt?: string | null;
|
|
127
147
|
issueEndsAt?: string | null;
|
|
128
148
|
visibleFrom?: string | null;
|
|
@@ -164,6 +184,11 @@ export function couponRequestRuleIssues(body: {
|
|
|
164
184
|
path: ["stacking", "perOrderLimit"],
|
|
165
185
|
message: "한 주문에 두 장 이상은 상품 쿠폰만 쓸 수 있습니다",
|
|
166
186
|
});
|
|
187
|
+
if (body.issueMethod === "CODE" && body.dailyIssueLimit != null)
|
|
188
|
+
issues.push({
|
|
189
|
+
path: ["dailyIssueLimit"],
|
|
190
|
+
message: "하루 발급 한도는 발급형(셀러 지급·받기·회원가입) 쿠폰에만 둡니다",
|
|
191
|
+
});
|
|
167
192
|
if (
|
|
168
193
|
body.issueMethod !== "DOWNLOAD" &&
|
|
169
194
|
(body.issueStartsAt != null || body.issueEndsAt != null || body.visibleFrom != null)
|
|
@@ -232,6 +257,7 @@ export const createCouponRequestSchema = strictRequest(
|
|
|
232
257
|
target: couponTargetInputSchema.optional().describe("대상 상품·카테고리. 생략하면 전 상품"),
|
|
233
258
|
issueMethod: couponIssueMethodSchema,
|
|
234
259
|
code: z.string().regex(COUPON_CODE_PATTERN).optional().describe(descriptions.code),
|
|
260
|
+
voucher: couponVoucherInputSchema.optional(),
|
|
235
261
|
startsAt: couponTimeField(descriptions.startsAt),
|
|
236
262
|
endsAt: couponTimeField(descriptions.endsAt),
|
|
237
263
|
validDaysAfterIssue: z
|
|
@@ -243,6 +269,7 @@ export const createCouponRequestSchema = strictRequest(
|
|
|
243
269
|
.optional()
|
|
244
270
|
.describe(descriptions.validDaysAfterIssue),
|
|
245
271
|
totalIssueLimit: limitField(descriptions.totalIssueLimit),
|
|
272
|
+
dailyIssueLimit: limitField(descriptions.dailyIssueLimit),
|
|
246
273
|
totalUseLimit: limitField(descriptions.totalUseLimit),
|
|
247
274
|
perMemberIssueLimit: limitField(descriptions.perMemberIssueLimit),
|
|
248
275
|
perMemberUseLimit: limitField(descriptions.perMemberUseLimit),
|
|
@@ -261,6 +288,10 @@ export const createCouponRequestSchema = strictRequest(
|
|
|
261
288
|
ctx.addIssue({ code: "custom", path: [path], message });
|
|
262
289
|
if (body.issueMethod === "CODE" && !body.code)
|
|
263
290
|
issue("code", "코드 입력형 쿠폰은 코드가 필요합니다");
|
|
291
|
+
if (body.issueMethod === "VOUCHER" && !body.voucher)
|
|
292
|
+
issue("voucher", "쿠폰 번호 쿠폰은 번호 설정이 필요합니다");
|
|
293
|
+
if (body.issueMethod !== "VOUCHER" && body.voucher)
|
|
294
|
+
issue("voucher", "번호 설정은 쿠폰 번호 쿠폰에만 둡니다");
|
|
264
295
|
if (body.issueMethod !== "CODE" && body.code)
|
|
265
296
|
issue("code", "코드는 코드 입력형 쿠폰에만 둡니다");
|
|
266
297
|
if (body.issueMethod === "CODE" && body.validDaysAfterIssue != null) {
|
|
@@ -315,6 +346,7 @@ export const updateCouponRequestSchema = strictRequest(
|
|
|
315
346
|
endsAt: couponTimeField(descriptions.endsAt),
|
|
316
347
|
validDaysAfterIssue: z.number().int().min(1).max(3650).nullable().optional(),
|
|
317
348
|
totalIssueLimit: limitField(descriptions.totalIssueLimit),
|
|
349
|
+
dailyIssueLimit: limitField(descriptions.dailyIssueLimit),
|
|
318
350
|
totalUseLimit: limitField(descriptions.totalUseLimit),
|
|
319
351
|
perMemberIssueLimit: limitField(descriptions.perMemberIssueLimit),
|
|
320
352
|
perMemberUseLimit: limitField(descriptions.perMemberUseLimit),
|
|
@@ -338,16 +370,29 @@ export const couponSchema = z.object({
|
|
|
338
370
|
issueMethod: z
|
|
339
371
|
.string()
|
|
340
372
|
.describe(
|
|
341
|
-
"발급 방식. 현재 값은 `CODE`·`MANUAL`·`DOWNLOAD`(구매자가 받기)·`SIGNUP`(회원가입 자동 지급)이고 값이 추가될 수 있어 문자열로 받는다",
|
|
373
|
+
"발급 방식. 현재 값은 `CODE`·`MANUAL`·`DOWNLOAD`(구매자가 받기)·`SIGNUP`(회원가입 자동 지급)·`FLOW`(주문 흐름 지급)·`VOUCHER`(쿠폰 번호)이고 값이 추가될 수 있어 문자열로 받는다",
|
|
342
374
|
),
|
|
343
375
|
code: z.string().nullable().describe("쿠폰 코드(대문자). 코드 입력형이 아니면 null"),
|
|
344
376
|
startsAt: z.string().nullable(),
|
|
345
377
|
endsAt: z.string().nullable(),
|
|
346
378
|
validDaysAfterIssue: z.number().int().nullable(),
|
|
347
379
|
totalIssueLimit: z.number().int().nullable(),
|
|
380
|
+
dailyIssueLimit: z
|
|
381
|
+
.number()
|
|
382
|
+
.int()
|
|
383
|
+
.nullable()
|
|
384
|
+
.optional()
|
|
385
|
+
.default(null)
|
|
386
|
+
.describe("하루 발급 수 한도(한국 시간 0시 기준). 없으면 null. 서버는 항상 싣는다"),
|
|
348
387
|
totalUseLimit: z.number().int().nullable(),
|
|
349
388
|
perMemberIssueLimit: z.number().int().nullable(),
|
|
350
389
|
perMemberUseLimit: z.number().int().nullable(),
|
|
390
|
+
voucher: z
|
|
391
|
+
.object({ prefix: z.string(), count: z.number().int() })
|
|
392
|
+
.nullable()
|
|
393
|
+
.optional()
|
|
394
|
+
.default(null)
|
|
395
|
+
.describe("쿠폰 번호 설정(`VOUCHER`만, 이슈 #264). 다른 발급 방식은 null. 서버는 항상 싣는다"),
|
|
351
396
|
issuedCount: z
|
|
352
397
|
.number()
|
|
353
398
|
.int()
|
|
@@ -427,7 +472,7 @@ export const couponIssueSchema = z.object({
|
|
|
427
472
|
.string()
|
|
428
473
|
.optional()
|
|
429
474
|
.describe(
|
|
430
|
-
"발급 경로(이슈 #110). 현재 값은 `MANUAL`(셀러 지급)·`DOWNLOAD`(구매자가 받음)·`SIGNUP`(회원가입 자동 지급)이고 값이 추가될 수 있다. 서버는 항상 싣는다",
|
|
475
|
+
"발급 경로(이슈 #110). 현재 값은 `MANUAL`(셀러 지급)·`DOWNLOAD`(구매자가 받음)·`SIGNUP`(회원가입 자동 지급)·`FLOW`(주문 흐름 지급)·`VOUCHER`(쿠폰 번호 등록)이고 값이 추가될 수 있다. 서버는 항상 싣는다",
|
|
431
476
|
),
|
|
432
477
|
status: couponIssueStatusSchema,
|
|
433
478
|
validFrom: z.string(),
|
|
@@ -443,6 +488,48 @@ export const couponIssueListQuerySchema = z.object({
|
|
|
443
488
|
...pageQueryShape(),
|
|
444
489
|
});
|
|
445
490
|
|
|
491
|
+
/** `GET /v1/coupons/{couponId}/voucher-codes` — 쿠폰 번호 전체(순번 순). 상점 플랫폼이 CSV로 내려받는다 */
|
|
492
|
+
export const couponVoucherCodesSchema = z
|
|
493
|
+
.object({
|
|
494
|
+
prefix: z.string(),
|
|
495
|
+
count: z.number().int(),
|
|
496
|
+
codes: z.array(z.string()).describe("번호 — 순번 1부터. 등록에 쓴 번호도 싣는다"),
|
|
497
|
+
})
|
|
498
|
+
.describe("쿠폰 번호 목록");
|
|
499
|
+
export type CouponVoucherCodes = z.infer<typeof couponVoucherCodesSchema>;
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* 발급 대상 전원 지급(이슈 #264) — `POST /v1/coupons/{couponId}/distributions`(202)가 만들고 서버가 뒤에서 처리한다. 셀러 지급(`MANUAL`) 쿠폰을
|
|
503
|
+
* 발급 대상 조건에 맞는 회원 모두에게 준다(셀러 지급과 같은 한도 판정). 쿠폰마다 진행 중 작업은 하나다
|
|
504
|
+
*/
|
|
505
|
+
export const couponDistributionSchema = z
|
|
506
|
+
.object({
|
|
507
|
+
distributionId: z.string(),
|
|
508
|
+
couponId: z.string(),
|
|
509
|
+
status: z
|
|
510
|
+
.string()
|
|
511
|
+
.describe(
|
|
512
|
+
"작업 상태. 현재 값은 `QUEUED`(대기)·`RUNNING`(지급 중)·`COMPLETED`(끝남)·`FAILED`(실패)이고 값이 추가될 수 있다",
|
|
513
|
+
),
|
|
514
|
+
processedCount: z.number().int().describe("지금까지 살펴본 회원 수"),
|
|
515
|
+
issuedCount: z.number().int().describe("이 작업이 지급한 장수"),
|
|
516
|
+
skippedCount: z
|
|
517
|
+
.number()
|
|
518
|
+
.int()
|
|
519
|
+
.describe("건너뛴 회원 수 — 발급 대상이 아니거나 1인 발급 한도에 닿은 회원"),
|
|
520
|
+
stopCode: z
|
|
521
|
+
.string()
|
|
522
|
+
.nullable()
|
|
523
|
+
.describe(
|
|
524
|
+
"일찍 끝난 사유. `COUPON_EXHAUSTED`(총 발급 한도)·`COUPON_DAILY_EXHAUSTED`(하루 발급 한도)·`COUPON_NOT_ACTIVE`(중지·종료)·`COUPON_EXPIRED`(사용 기간 끝) 등. 끝까지 돌았으면 null",
|
|
525
|
+
),
|
|
526
|
+
createdAt: z.string(),
|
|
527
|
+
startedAt: z.string().nullable(),
|
|
528
|
+
finishedAt: z.string().nullable(),
|
|
529
|
+
})
|
|
530
|
+
.describe("발급 대상 전원 지급 작업");
|
|
531
|
+
export type CouponDistribution = z.infer<typeof couponDistributionSchema>;
|
|
532
|
+
|
|
446
533
|
/** `POST /v1/coupons/{couponId}/issues` — 셀러 지급. 한 번에 1,000명까지 */
|
|
447
534
|
export const issueCouponRequestSchema = z.strictObject({
|
|
448
535
|
customerIds: z
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as z from "zod";
|
|
2
2
|
import { strictRequest } from "./strict";
|
|
3
|
+
import { customerUserinfoInputSchema } from "./userinfo";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* 구매자 위임 REST(`POST /v1/customers/delegations`, #71) — 계정 토큰(또는 관리 API 토큰) + `X-Store-Id`로 부른다(`customer:rw`).
|
|
@@ -31,6 +32,11 @@ export const customerDelegationRequestSchema = strictRequest(
|
|
|
31
32
|
.describe(
|
|
32
33
|
"사용자가 셀러 서비스에서 실제로 로그인한 시각(uid 위임만). 주면 구매자 토큰의 인증 시각이 된다",
|
|
33
34
|
),
|
|
35
|
+
userinfo: customerUserinfoInputSchema
|
|
36
|
+
.optional()
|
|
37
|
+
.describe(
|
|
38
|
+
"OIDC 클레임(이슈 #264) — 위임할 때마다 준 클레임만 바뀌고 null은 지운다. `email_verified`를 생략하면 위임으로 만든 구매자의 이메일은 확인된 것으로 본다",
|
|
39
|
+
),
|
|
34
40
|
})
|
|
35
41
|
.refine((value) => Boolean(value.uid || value.email), {
|
|
36
42
|
message: "uid 또는 email이 필요합니다",
|
package/src/schemas/customer.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as z from "zod";
|
|
2
2
|
import { orderItemStatusSchema, pageQueryShape } from "./common";
|
|
3
3
|
import { strictRequest } from "./strict";
|
|
4
|
+
import { customerUserinfoSchema } from "./userinfo";
|
|
4
5
|
|
|
5
6
|
/** 구매자 목록 정렬 */
|
|
6
7
|
export const customerSortSchema = z
|
|
@@ -169,6 +170,9 @@ export const customerDetailSchema = z.object({
|
|
|
169
170
|
"약관·개인정보·마케팅 동의의 현재 상태. 이력은 `GET /v1/customers/{customerId}/consents`",
|
|
170
171
|
),
|
|
171
172
|
addresses: z.array(customerAddressSchema),
|
|
173
|
+
userinfo: customerUserinfoSchema
|
|
174
|
+
.optional()
|
|
175
|
+
.describe("OIDC userinfo 표준 클레임(이슈 #264, 주소는 기본 배송지). 서버는 항상 싣는다"),
|
|
172
176
|
stats: customerStatsSchema.extend({
|
|
173
177
|
canceledOrReturnedCount: z.number().int().describe("취소된 주문 상품 + 완료된 반품 수"),
|
|
174
178
|
reviewCount: z.number().int(),
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import * as z from "zod";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 구매자 정보의 OIDC userinfo 표준 클레임(OpenID Connect Core 1.0 5.1, 이슈 #264). 이름은 표준 그대로 snake_case다.
|
|
5
|
+
* 값이 없는 클레임은 응답에서 빠진다. 같은 형식이 스토어프론트 API(storefront-sdk `userinfoSchema`)에도 있다.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** 생일 — `YYYY-MM-DD`, 연도를 모르면 `0000-MM-DD`(표준 형식) */
|
|
9
|
+
export const CUSTOMER_BIRTHDATE_PATTERN = /^(\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/;
|
|
10
|
+
|
|
11
|
+
export const customerUserinfoAddressSchema = z.object({
|
|
12
|
+
formatted: z.string().optional(),
|
|
13
|
+
street_address: z.string().optional(),
|
|
14
|
+
locality: z.string().optional(),
|
|
15
|
+
region: z.string().optional(),
|
|
16
|
+
postal_code: z.string().optional(),
|
|
17
|
+
country: z.string().optional().describe("ISO 3166-1 alpha-2"),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export const customerUserinfoSchema = z
|
|
21
|
+
.object({
|
|
22
|
+
sub: z.string().describe("구매자 id(상점 안에서 유일)"),
|
|
23
|
+
name: z.string().optional(),
|
|
24
|
+
given_name: z.string().optional(),
|
|
25
|
+
family_name: z.string().optional(),
|
|
26
|
+
middle_name: z.string().optional(),
|
|
27
|
+
nickname: z.string().optional(),
|
|
28
|
+
preferred_username: z.string().optional(),
|
|
29
|
+
profile: z.string().optional(),
|
|
30
|
+
picture: z.string().optional(),
|
|
31
|
+
website: z.string().optional(),
|
|
32
|
+
email: z.string().optional(),
|
|
33
|
+
email_verified: z
|
|
34
|
+
.boolean()
|
|
35
|
+
.optional()
|
|
36
|
+
.describe("이메일 확인 여부 — 인증된 소셜 가입·셀러 위임만 true다. 비밀번호 가입은 false"),
|
|
37
|
+
gender: z.string().optional().describe("표준 값은 `female`·`male`이고 다른 값도 온다"),
|
|
38
|
+
birthdate: z.string().optional().describe("`YYYY-MM-DD`, 연도를 모르면 `0000-MM-DD`"),
|
|
39
|
+
zoneinfo: z.string().optional().describe("IANA 시간대(예: `Asia/Seoul`)"),
|
|
40
|
+
locale: z.string().optional().describe("BCP 47 언어 태그(예: `ko-KR`)"),
|
|
41
|
+
phone_number: z.string().optional().describe("E.164(예: `+821012345678`)"),
|
|
42
|
+
phone_number_verified: z.boolean().optional(),
|
|
43
|
+
address: customerUserinfoAddressSchema.optional().describe("기본 배송지에서 만든다"),
|
|
44
|
+
updated_at: z.number().int().optional().describe("프로필을 바꾼 시각(epoch 초)"),
|
|
45
|
+
})
|
|
46
|
+
.describe("OIDC userinfo 표준 클레임(이슈 #264)");
|
|
47
|
+
export type CustomerUserinfo = z.infer<typeof customerUserinfoSchema>;
|
|
48
|
+
|
|
49
|
+
const text = (max: number) => z.string().trim().min(1).max(max).nullable().optional();
|
|
50
|
+
const httpsUrl = z
|
|
51
|
+
.url({ protocol: /^https$/ })
|
|
52
|
+
.max(2048)
|
|
53
|
+
.nullable()
|
|
54
|
+
.optional()
|
|
55
|
+
.describe("https 주소");
|
|
56
|
+
|
|
57
|
+
/** 시간대·언어 태그 검사 — 런타임(Intl)이 아는 값만 받는다 */
|
|
58
|
+
const zoneinfo = z
|
|
59
|
+
.string()
|
|
60
|
+
.max(64)
|
|
61
|
+
.refine((value) => {
|
|
62
|
+
try {
|
|
63
|
+
new Intl.DateTimeFormat("en", { timeZone: value });
|
|
64
|
+
return true;
|
|
65
|
+
} catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}, "IANA 시간대가 아닙니다")
|
|
69
|
+
.nullable()
|
|
70
|
+
.optional();
|
|
71
|
+
const locale = z
|
|
72
|
+
.string()
|
|
73
|
+
.max(35)
|
|
74
|
+
.refine((value) => {
|
|
75
|
+
try {
|
|
76
|
+
return Intl.getCanonicalLocales(value).length === 1;
|
|
77
|
+
} catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}, "BCP 47 언어 태그가 아닙니다")
|
|
81
|
+
.nullable()
|
|
82
|
+
.optional();
|
|
83
|
+
const birthdate = z
|
|
84
|
+
.string()
|
|
85
|
+
.regex(CUSTOMER_BIRTHDATE_PATTERN, "생일은 YYYY-MM-DD(연도를 모르면 0000-MM-DD)입니다")
|
|
86
|
+
.refine((value) => {
|
|
87
|
+
const [y, m, d] = value.split("-").map(Number) as [number, number, number];
|
|
88
|
+
// 연도를 모르면 윤년(2000)으로 날짜만 본다
|
|
89
|
+
const day = new Date(Date.UTC(y === 0 ? 2000 : y, m - 1, d));
|
|
90
|
+
return day.getUTCMonth() === m - 1 && day.getUTCDate() === d;
|
|
91
|
+
}, "없는 날짜입니다")
|
|
92
|
+
.nullable()
|
|
93
|
+
.optional();
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 셀러가 위임으로 바꿀 수 있는 클레임 — 준 클레임만 바뀌고 null은 지운다. `sub`·`email`·`phone_number`·`updated_at`·`address`는 받지 않는다. 확인 여부(`*_verified`)는 셀러가 확인한 값을 받는다
|
|
97
|
+
* (이메일·휴대폰은 각자의 칸, 주소는 배송지가 원천이다)
|
|
98
|
+
*/
|
|
99
|
+
export const customerUserinfoInputSchema = z
|
|
100
|
+
.strictObject({
|
|
101
|
+
/** 셀러가 확인한 값 그대로 받는다 — 셀러 서비스가 인증한 회원이다 */
|
|
102
|
+
email_verified: z.boolean().optional(),
|
|
103
|
+
phone_number_verified: z.boolean().optional(),
|
|
104
|
+
given_name: text(50),
|
|
105
|
+
family_name: text(50),
|
|
106
|
+
middle_name: text(50),
|
|
107
|
+
nickname: text(50),
|
|
108
|
+
preferred_username: text(50),
|
|
109
|
+
profile: httpsUrl,
|
|
110
|
+
picture: httpsUrl,
|
|
111
|
+
website: httpsUrl,
|
|
112
|
+
gender: text(30),
|
|
113
|
+
birthdate,
|
|
114
|
+
zoneinfo,
|
|
115
|
+
locale,
|
|
116
|
+
})
|
|
117
|
+
.describe("바꿀 OIDC 클레임 — 준 것만 바뀌고 null은 지운다");
|
|
118
|
+
export type CustomerUserinfoInput = z.infer<typeof customerUserinfoInputSchema>;
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
//#region src/webhook-headers.d.ts
|
|
2
|
-
/** 웹훅 발송 요청 헤더 상수 (공개 계약). zod 의존 없이 서버 엔트리포인트에서도 가볍게 쓴다 */
|
|
3
|
-
/** 서명 헤더. 형식: `t=<unix초>,v1=<hex>[,v1=<hex>]` */
|
|
4
|
-
declare const WEBHOOK_SIGNATURE_HEADER = "Sayren-Signature";
|
|
5
|
-
/** 이벤트 ID 헤더. 본문 `id`와 같다. 중복 제거 키 */
|
|
6
|
-
declare const WEBHOOK_EVENT_ID_HEADER = "Sayren-Event-Id";
|
|
7
|
-
/** 이벤트 타입 헤더. 본문 `type`과 같다 */
|
|
8
|
-
declare const WEBHOOK_EVENT_TYPE_HEADER = "Sayren-Event-Type";
|
|
9
|
-
/** 전송 기록 ID 헤더. 전송 기록의 `deliveryId`와 같다 */
|
|
10
|
-
declare const WEBHOOK_DELIVERY_ID_HEADER = "Sayren-Delivery-Id";
|
|
11
|
-
/** 발송 요청의 `User-Agent` 값 */
|
|
12
|
-
declare const WEBHOOK_USER_AGENT = "Sayren-Webhooks/1.0";
|
|
13
|
-
/** 수신 측 권장 타임스탬프 허용 오차(초) */
|
|
14
|
-
declare const WEBHOOK_DEFAULT_TOLERANCE_SEC = 300;
|
|
15
|
-
declare const WEBHOOK_HEADERS: {
|
|
16
|
-
readonly signature: "Sayren-Signature";
|
|
17
|
-
readonly eventId: "Sayren-Event-Id";
|
|
18
|
-
readonly eventType: "Sayren-Event-Type";
|
|
19
|
-
readonly deliveryId: "Sayren-Delivery-Id";
|
|
20
|
-
};
|
|
21
|
-
//#endregion
|
|
22
|
-
export { WEBHOOK_HEADERS as a, WEBHOOK_EVENT_TYPE_HEADER as i, WEBHOOK_DELIVERY_ID_HEADER as n, WEBHOOK_SIGNATURE_HEADER as o, WEBHOOK_EVENT_ID_HEADER as r, WEBHOOK_USER_AGENT as s, WEBHOOK_DEFAULT_TOLERANCE_SEC as t };
|