@sayren/storefront-sdk 0.5.0 → 0.7.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.
@@ -1,5 +1,28 @@
1
- import { a as AnonymousSession, c as TokenPair, l as WithdrawRequest, o as LoginRequest, s as SignupRequest, t as ApiError, u as WithdrawalBlockedDetails } from "../http-CxqZxTfE.mjs";
1
+ import { a as AnonymousSession, d as SignupRequest, f as SocialProvider, h as WithdrawalBlockedDetails, l as LoginRequest, m as WithdrawRequest, p as TokenPair, t as ApiError, u as MemberIdentities } from "../http-BbFHVGBf.mjs";
2
2
  //#region src/auth/index.d.ts
3
+ /** 소셜 로그인 시작 결과 — `codeVerifier`와 `state`는 돌아올 때까지 서버 세션(httpOnly 쿠키 등)에 보관한다 */
4
+ export interface IdpStart {
5
+ /** 구매자를 보낼 공급자 로그인 주소 */
6
+ url: string;
7
+ state: string;
8
+ codeVerifier: string;
9
+ }
10
+ export interface IdpCallbackInput {
11
+ /** 돌아온 주소의 `code`(성공) — 실패면 `error`가 온다 */
12
+ code: string | null;
13
+ /** 돌아온 주소의 `state` */
14
+ state: string | null;
15
+ /** `idp()`가 준 값 — 보관해 둔 것 */
16
+ expectedState: string;
17
+ codeVerifier: string;
18
+ /** 비회원 장바구니 토큰 — 주면 회원 장바구니로 합친다 */
19
+ cartToken?: string;
20
+ }
21
+ /** 소셜 로그인 실패 — 구매자가 취소했거나(`access_denied`) state가 맞지 않는다 */
22
+ export declare class IdpCallbackError extends Error {
23
+ readonly reason: "access_denied" | "state_mismatch" | "missing_code" | string;
24
+ constructor(reason: "access_denied" | "state_mismatch" | "missing_code" | string);
25
+ }
3
26
  export interface StorefrontAuthOptions {
4
27
  /** 예: https://api.sayren.app/storefront/v1 */
5
28
  baseUrl: string;
@@ -8,7 +31,7 @@ export interface StorefrontAuthOptions {
8
31
  fetch?: typeof globalThis.fetch;
9
32
  }
10
33
  /**
11
- * 구매자 인증 — 화면은 스토어프런트가 그리고, 계정 저장과 인증 처리는 sayren이 한다.
34
+ * 구매자 인증 — 화면은 스토어프론트가 그리고, 계정 저장과 인증 처리는 sayren이 한다.
12
35
  * 어느 방식으로 로그인해도 결과는 같은 구매자 토큰(`TokenPair`)이고 `/me/*`·장바구니·주문 API에 그대로 쓴다.
13
36
  *
14
37
  * 토큰 보관은 앱 몫이다. 서버(loader·action)에서 호출하고 httpOnly 쿠키에 두기를 권한다.
@@ -30,6 +53,39 @@ export declare function createStorefrontAuth(options: StorefrontAuthOptions): {
30
53
  session(body: LoginRequest): Promise<TokenPair>;
31
54
  /** 회원가입 — 가입과 동시에 로그인 토큰을 받는다. `cartToken`을 주면 장바구니를 병합한다 */
32
55
  signUp(body: SignupRequest): Promise<TokenPair>;
56
+ /**
57
+ * 소셜 로그인 시작 — PKCE(S256)와 state를 만들고 공급자 로그인 주소를 받는다. 반환한 `state`·`codeVerifier`를
58
+ * 서버 세션에 보관하고 구매자를 `url`로 보낸다. 로그인을 마치면 `redirectUri`로 `?code=&state=`가 붙어 돌아온다.
59
+ * `redirectUri`는 스토어프론트 도메인 목록 안이어야 한다
60
+ */
61
+ idp(provider: SocialProvider, options: {
62
+ redirectUri: string;
63
+ }): Promise<IdpStart>;
64
+ /**
65
+ * 소셜 로그인 마무리 — 돌아온 `code`를 구매자 토큰으로 바꾼다. state가 다르거나 구매자가 취소했으면
66
+ * `IdpCallbackError`. code는 1회용이고 1분 안에 바꿔야 한다
67
+ */
68
+ idpCallback(input: IdpCallbackInput & {
69
+ error?: string | null;
70
+ }): Promise<TokenPair>;
71
+ /** 로그인 수단 — 비밀번호가 있는지, 어떤 소셜 계정이 연결됐는지 */
72
+ identities(accessToken: string): Promise<MemberIdentities>;
73
+ /**
74
+ * 소셜 계정 연결 시작 — 로그인한 구매자에 공급자 계정을 붙인다. `idp()`처럼 `state`·`codeVerifier`를 보관하고
75
+ * 구매자를 `url`로 보낸다. 돌아오면 `linkIdpCallback()`으로 마무리한다. 연결은 흐름을 시작한 구매자에게만 된다
76
+ */
77
+ linkIdp(accessToken: string, provider: SocialProvider, options: {
78
+ redirectUri: string;
79
+ }): Promise<IdpStart>;
80
+ /**
81
+ * 소셜 계정 연결 마무리 — state를 비교하고 code를 바꿔 연결한다. 다른 구매자에 이미 연결된 계정이면
82
+ * 409 `IDENTITY_IN_USE`, 같은 공급자의 다른 계정을 이미 연결했으면 409 `PROVIDER_ALREADY_LINKED`
83
+ */
84
+ linkIdpCallback(accessToken: string, provider: SocialProvider, input: Omit<IdpCallbackInput, "cartToken"> & {
85
+ error?: string | null;
86
+ }): Promise<MemberIdentities>;
87
+ /** 소셜 계정 연결 해제 — 마지막 로그인 수단이면 409 `LAST_LOGIN_METHOD` */
88
+ unlinkIdp(accessToken: string, provider: SocialProvider): Promise<MemberIdentities>;
33
89
  /** 토큰 갱신 — refreshToken은 한 번 쓰면 폐기되고 새 쌍이 온다(회전) */
34
90
  refresh(refreshToken: string): Promise<TokenPair>;
35
91
  /**
@@ -46,4 +102,4 @@ export declare function createStorefrontAuth(options: StorefrontAuthOptions): {
46
102
  };
47
103
  export type StorefrontAuth = ReturnType<typeof createStorefrontAuth>;
48
104
  //#endregion
49
- export { type AnonymousSession, ApiError, type LoginRequest, type SignupRequest, type TokenPair, type WithdrawRequest, type WithdrawalBlockedDetails };
105
+ export { type AnonymousSession, ApiError, type LoginRequest, type MemberIdentities, type SignupRequest, type SocialProvider, type TokenPair, type WithdrawRequest, type WithdrawalBlockedDetails };
@@ -1,7 +1,28 @@
1
- import { it as ApiError, t as createStorefrontClient } from "../client-D-w3LIU2.mjs";
1
+ import { gt as ApiError, t as createStorefrontClient } from "../client-B1uXGefY.mjs";
2
2
  //#region src/auth/index.ts
3
+ /** 소셜 로그인 실패 — 구매자가 취소했거나(`access_denied`) state가 맞지 않는다 */
4
+ var IdpCallbackError = class extends Error {
5
+ reason;
6
+ constructor(reason) {
7
+ super(`소셜 로그인을 마치지 못했어요 (${reason})`);
8
+ this.reason = reason;
9
+ this.name = "IdpCallbackError";
10
+ }
11
+ };
12
+ function base64url(bytes) {
13
+ let binary = "";
14
+ for (const byte of bytes) binary += String.fromCharCode(byte);
15
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
16
+ }
17
+ function randomToken(bytes = 32) {
18
+ return base64url(globalThis.crypto.getRandomValues(new Uint8Array(bytes)));
19
+ }
20
+ async function s256(verifier) {
21
+ const digest = await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
22
+ return base64url(new Uint8Array(digest));
23
+ }
3
24
  /**
4
- * 구매자 인증 — 화면은 스토어프런트가 그리고, 계정 저장과 인증 처리는 sayren이 한다.
25
+ * 구매자 인증 — 화면은 스토어프론트가 그리고, 계정 저장과 인증 처리는 sayren이 한다.
5
26
  * 어느 방식으로 로그인해도 결과는 같은 구매자 토큰(`TokenPair`)이고 `/me/*`·장바구니·주문 API에 그대로 쓴다.
6
27
  *
7
28
  * 토큰 보관은 앱 몫이다. 서버(loader·action)에서 호출하고 httpOnly 쿠키에 두기를 권한다.
@@ -36,6 +57,78 @@ function createStorefrontAuth(options) {
36
57
  signUp(body) {
37
58
  return client().auth.signup(body);
38
59
  },
60
+ /**
61
+ * 소셜 로그인 시작 — PKCE(S256)와 state를 만들고 공급자 로그인 주소를 받는다. 반환한 `state`·`codeVerifier`를
62
+ * 서버 세션에 보관하고 구매자를 `url`로 보낸다. 로그인을 마치면 `redirectUri`로 `?code=&state=`가 붙어 돌아온다.
63
+ * `redirectUri`는 스토어프론트 도메인 목록 안이어야 한다
64
+ */
65
+ async idp(provider, options) {
66
+ const state = randomToken(24);
67
+ const codeVerifier = randomToken(32);
68
+ const { url } = await client().auth.idpAuthorize(provider, {
69
+ redirectUri: options.redirectUri,
70
+ codeChallenge: await s256(codeVerifier),
71
+ state
72
+ });
73
+ return {
74
+ url,
75
+ state,
76
+ codeVerifier
77
+ };
78
+ },
79
+ /**
80
+ * 소셜 로그인 마무리 — 돌아온 `code`를 구매자 토큰으로 바꾼다. state가 다르거나 구매자가 취소했으면
81
+ * `IdpCallbackError`. code는 1회용이고 1분 안에 바꿔야 한다
82
+ */
83
+ idpCallback(input) {
84
+ if (input.error) return Promise.reject(new IdpCallbackError(input.error));
85
+ if (!input.state || input.state !== input.expectedState) return Promise.reject(new IdpCallbackError("state_mismatch"));
86
+ if (!input.code) return Promise.reject(new IdpCallbackError("missing_code"));
87
+ return client().auth.idpToken({
88
+ code: input.code,
89
+ codeVerifier: input.codeVerifier,
90
+ cartToken: input.cartToken
91
+ });
92
+ },
93
+ /** 로그인 수단 — 비밀번호가 있는지, 어떤 소셜 계정이 연결됐는지 */
94
+ identities(accessToken) {
95
+ return client(accessToken).auth.identities();
96
+ },
97
+ /**
98
+ * 소셜 계정 연결 시작 — 로그인한 구매자에 공급자 계정을 붙인다. `idp()`처럼 `state`·`codeVerifier`를 보관하고
99
+ * 구매자를 `url`로 보낸다. 돌아오면 `linkIdpCallback()`으로 마무리한다. 연결은 흐름을 시작한 구매자에게만 된다
100
+ */
101
+ async linkIdp(accessToken, provider, options) {
102
+ const state = randomToken(24);
103
+ const codeVerifier = randomToken(32);
104
+ const { url } = await client(accessToken).auth.linkIdentityAuthorize(provider, {
105
+ redirectUri: options.redirectUri,
106
+ codeChallenge: await s256(codeVerifier),
107
+ state
108
+ });
109
+ return {
110
+ url,
111
+ state,
112
+ codeVerifier
113
+ };
114
+ },
115
+ /**
116
+ * 소셜 계정 연결 마무리 — state를 비교하고 code를 바꿔 연결한다. 다른 구매자에 이미 연결된 계정이면
117
+ * 409 `IDENTITY_IN_USE`, 같은 공급자의 다른 계정을 이미 연결했으면 409 `PROVIDER_ALREADY_LINKED`
118
+ */
119
+ linkIdpCallback(accessToken, provider, input) {
120
+ if (input.error) return Promise.reject(new IdpCallbackError(input.error));
121
+ if (!input.state || input.state !== input.expectedState) return Promise.reject(new IdpCallbackError("state_mismatch"));
122
+ if (!input.code) return Promise.reject(new IdpCallbackError("missing_code"));
123
+ return client(accessToken).auth.linkIdentity(provider, {
124
+ code: input.code,
125
+ codeVerifier: input.codeVerifier
126
+ });
127
+ },
128
+ /** 소셜 계정 연결 해제 — 마지막 로그인 수단이면 409 `LAST_LOGIN_METHOD` */
129
+ unlinkIdp(accessToken, provider) {
130
+ return client(accessToken).auth.unlinkIdentity(provider);
131
+ },
39
132
  /** 토큰 갱신 — refreshToken은 한 번 쓰면 폐기되고 새 쌍이 온다(회전) */
40
133
  refresh(refreshToken) {
41
134
  return client().auth.refresh(refreshToken);
@@ -63,4 +156,4 @@ function createStorefrontAuth(options) {
63
156
  };
64
157
  }
65
158
  //#endregion
66
- export { ApiError, createStorefrontAuth };
159
+ export { ApiError, IdpCallbackError, createStorefrontAuth };
@@ -155,7 +155,12 @@ const anonymousSessionSchema = z.object({
155
155
  * 진행 중인 주문(결제 대기~배송 중)·클레임·결제가 있으면 409 `WITHDRAWAL_BLOCKED`, 비밀번호가 틀리면
156
156
  * 403 `PASSWORD_MISMATCH`다(로그인과 같은 5회 잠금).
157
157
  */
158
- const withdrawRequestSchema = z.object({ password: z.string().min(1) });
158
+ const withdrawRequestSchema = z.object({
159
+ /**
160
+ * 비밀번호로 가입한 구매자는 필수. 소셜 로그인으로만 가입한 구매자는 비밀번호가 없어 생략하고, 대신 최근 10분 안에
161
+ * 로그인한 액세스 토큰이어야 한다(아니면 403 `REAUTH_REQUIRED` — 다시 로그인한 뒤 호출)
162
+ */
163
+ password: z.string().min(1).optional() });
159
164
  /** `WITHDRAWAL_BLOCKED`의 `error.details` — 탈퇴를 막는 항목 수 */
160
165
  const withdrawalBlockedDetailsSchema = z.object({
161
166
  /** 결제 대기~배송 중인 주문 상품 */
@@ -165,6 +170,48 @@ const withdrawalBlockedDetailsSchema = z.object({
165
170
  /** 승인 중이거나 결제창이 열려 있을 수 있는 결제(결제창은 만료되면 풀린다) */
166
171
  pendingPayments: z.number().int()
167
172
  });
173
+ /** 소셜 로그인 공급자 */
174
+ const socialProviderSchema = z.enum([
175
+ "kakao",
176
+ "naver",
177
+ "google"
178
+ ]);
179
+ /** `POST /auth/idp/{provider}/authorize` 본문 — PKCE(S256)와 state는 SDK가 만든다 */
180
+ const idpAuthorizeRequestSchema = z.object({
181
+ /** 로그인을 마치고 돌아올 스토어프론트 주소. 스토어프론트 도메인 목록 안이어야 한다 */
182
+ redirectUri: z.url(),
183
+ /** base64url(SHA-256(codeVerifier)) */
184
+ codeChallenge: z.string().regex(/^[A-Za-z0-9_-]{43}$/),
185
+ /** 스토어프론트가 만든 값 — 돌아올 때 그대로 붙어 온다. 세션에 저장해 두고 비교한다 */
186
+ state: z.string().min(16).max(128)
187
+ });
188
+ const idpAuthorizeResponseSchema = z.object({
189
+ /** 구매자를 보낼 공급자 로그인 주소 */
190
+ url: z.url() });
191
+ /** `POST /auth/idp/token` 본문 — 돌아온 1회용 code를 구매자 토큰으로 바꾼다 */
192
+ const idpTokenRequestSchema = z.object({
193
+ code: z.string().min(1),
194
+ codeVerifier: z.string().regex(/^[A-Za-z0-9._~-]{43,128}$/),
195
+ /** 비회원 장바구니 토큰 — 주면 회원 장바구니로 합친다 */
196
+ cartToken: z.string().optional()
197
+ });
198
+ /** `GET /me/identities` — 구매자의 로그인 수단(비밀번호 여부와 연결된 소셜 계정) */
199
+ const memberIdentitiesSchema = z.object({
200
+ hasPassword: z.boolean(),
201
+ /** 셀러 자체 회원 연동(구매자 위임)으로 들어온 구매자 — 그 로그인도 로그인 수단으로 센다 */
202
+ hasExternalAuth: z.boolean(),
203
+ identities: z.array(z.object({
204
+ provider: socialProviderSchema,
205
+ /** 공급자가 알려 준 이메일(참고값). 없을 수 있다 */
206
+ email: z.string().nullable(),
207
+ linkedAt: z.string()
208
+ }))
209
+ });
210
+ /** `POST /me/identities/{provider}` 본문 — 연결 흐름에서 돌아온 1회용 code와 PKCE verifier */
211
+ const linkIdentityRequestSchema = z.object({
212
+ code: z.string().min(1),
213
+ codeVerifier: z.string().regex(/^[A-Za-z0-9._~-]{43,128}$/)
214
+ });
168
215
  //#endregion
169
216
  //#region src/schemas/cart.ts
170
217
  const cartItemSchema = z.object({
@@ -202,6 +249,38 @@ const updateCartItemRequestSchema = z.object({
202
249
  optionId: z.string().optional()
203
250
  }).refine((value) => Object.values(value).some((v) => v !== void 0), { message: "최소 1개 필드가 필요합니다" });
204
251
  //#endregion
252
+ //#region src/schemas/common.ts
253
+ const pageInfoSchema = z.object({
254
+ page: z.number().int(),
255
+ size: z.number().int(),
256
+ totalElements: z.number().int(),
257
+ totalPages: z.number().int()
258
+ });
259
+ function pageSchema(item) {
260
+ return pageInfoSchema.extend({ contents: z.array(item) });
261
+ }
262
+ const orderItemStatusSchema = z.enum([
263
+ "PAYMENT_WAITING",
264
+ "PAID",
265
+ "CONFIRMED",
266
+ "DISPATCHED",
267
+ "DELIVERING",
268
+ "DELIVERED",
269
+ "PURCHASE_DECIDED",
270
+ "CANCELED"
271
+ ]);
272
+ const claimStatusSchema = z.enum([
273
+ "REQUESTED",
274
+ "HELD",
275
+ "COLLECTING",
276
+ "COLLECTED",
277
+ "REDELIVERING",
278
+ "APPROVED",
279
+ "REJECTED",
280
+ "COMPLETED",
281
+ "WITHDRAWN"
282
+ ]);
283
+ //#endregion
205
284
  //#region src/schemas/catalog.ts
206
285
  const categoryNodeSchema = z.lazy(() => z.object({
207
286
  categoryId: z.string(),
@@ -213,9 +292,9 @@ const productCardSchema = z.object({
213
292
  productId: z.string(),
214
293
  name: z.string(),
215
294
  thumbnailUrl: z.url(),
216
- salePrice: z.number().int(),
217
- discountedPrice: z.number().int().nullable(),
218
- discountRate: z.number().int().nullable(),
295
+ salePrice: z.number().int().describe("판매가. 원(KRW) 단위 정수"),
296
+ discountedPrice: z.number().int().nullable().describe("즉시 할인 적용가. 원 단위. 할인이 없으면 null"),
297
+ discountRate: z.number().int().nullable().describe("할인율(%). 할인이 없으면 null"),
219
298
  freeShipping: z.boolean(),
220
299
  averageRating: z.number(),
221
300
  reviewCount: z.number().int(),
@@ -282,6 +361,8 @@ const productFacetsSchema = z.object({
282
361
  max: z.number().int()
283
362
  }).optional()
284
363
  });
364
+ /** `GET /products` 응답 — 상품 카드 페이지와 검색 패싯 */
365
+ const productPageSchema = pageSchema(productCardSchema).extend({ facets: productFacetsSchema.optional() });
285
366
  //#endregion
286
367
  //#region src/schemas/checkout.ts
287
368
  const createCheckoutRequestSchema = z.object({
@@ -390,38 +471,6 @@ const paymentStatusSchema = z.object({
390
471
  expiresAt: z.string().describe("결제 요청 만료 시각")
391
472
  });
392
473
  //#endregion
393
- //#region src/schemas/common.ts
394
- const pageInfoSchema = z.object({
395
- page: z.number().int(),
396
- size: z.number().int(),
397
- totalElements: z.number().int(),
398
- totalPages: z.number().int()
399
- });
400
- function pageSchema(item) {
401
- return pageInfoSchema.extend({ contents: z.array(item) });
402
- }
403
- const orderItemStatusSchema = z.enum([
404
- "PAYMENT_WAITING",
405
- "PAID",
406
- "CONFIRMED",
407
- "DISPATCHED",
408
- "DELIVERING",
409
- "DELIVERED",
410
- "PURCHASE_DECIDED",
411
- "CANCELED"
412
- ]);
413
- const claimStatusSchema = z.enum([
414
- "REQUESTED",
415
- "HELD",
416
- "COLLECTING",
417
- "COLLECTED",
418
- "REDELIVERING",
419
- "APPROVED",
420
- "REJECTED",
421
- "COMPLETED",
422
- "WITHDRAWN"
423
- ]);
424
- //#endregion
425
474
  //#region src/schemas/claim.ts
426
475
  const claimTypeSchema = z.enum([
427
476
  "CANCEL",
@@ -508,11 +557,17 @@ const createInquiryRequestSchema = z.object({
508
557
  content: z.string().min(1).max(1e3),
509
558
  secret: z.boolean().default(false)
510
559
  });
560
+ /** `POST /products/{productId}/inquiries` 응답 */
561
+ const createInquiryResultSchema = z.object({
562
+ inquiryId: z.string(),
563
+ createdAt: z.string()
564
+ });
511
565
  //#endregion
512
566
  //#region src/schemas/member.ts
513
567
  const memberSchema = z.object({
514
568
  memberId: z.string(),
515
- email: z.email(),
569
+ /** 소셜 가입에서 공급자가 이메일을 주지 않았으면 null */
570
+ email: z.email().nullable(),
516
571
  name: z.string(),
517
572
  phone: z.string().nullable(),
518
573
  point: z.number().int(),
@@ -543,6 +598,11 @@ const memberAddressRequestSchema = z.object({
543
598
  address2: z.string().optional(),
544
599
  isDefault: z.boolean().default(false)
545
600
  });
601
+ /** `POST /me/wishlist` 응답 — 이미 찜한 상품이어도 같은 응답이다 */
602
+ const wishlistAddResultSchema = z.object({
603
+ productId: z.string(),
604
+ wished: z.literal(true)
605
+ });
546
606
  //#endregion
547
607
  //#region src/schemas/order.ts
548
608
  const myOrderItemSchema = z.object({
@@ -599,6 +659,12 @@ const deliveryTrackingSchema = z.object({
599
659
  occurredAt: z.string()
600
660
  }))
601
661
  });
662
+ /** `POST /me/order-items/{orderItemId}/purchase-decision` 응답 */
663
+ const purchaseDecisionResultSchema = z.object({
664
+ orderItemId: z.string(),
665
+ status: z.literal("PURCHASE_DECIDED"),
666
+ decidedAt: z.string()
667
+ });
602
668
  //#endregion
603
669
  //#region src/schemas/review.ts
604
670
  const publicReviewSchema = z.object({
@@ -641,10 +707,17 @@ const updateReviewRequestSchema = z.object({
641
707
  content: z.string().min(10).max(2e3).optional(),
642
708
  images: z.array(z.url()).max(5).optional()
643
709
  }).refine((value) => Object.values(value).some((v) => v !== void 0), { message: "최소 1개 필드가 필요합니다" });
710
+ /** `GET /products/{productId}/reviews` 응답 — 리뷰 페이지와 평점 분포 */
711
+ const reviewPageSchema = pageSchema(publicReviewSchema).extend({ summary: reviewSummarySchema.optional() });
712
+ /** `PUT /me/reviews/{reviewId}` 응답 */
713
+ const updateReviewResultSchema = z.object({
714
+ reviewId: z.string(),
715
+ updatedAt: z.string()
716
+ });
644
717
  //#endregion
645
718
  //#region src/schemas/store.ts
646
719
  /**
647
- * 스토어 공개 설정 — `GET /store`. 스토어프런트가 화면을 그릴 때 읽는 값만 담는다.
720
+ * 스토어 공개 설정 — `GET /store`. 스토어프론트가 화면을 그릴 때 읽는 값만 담는다.
648
721
  */
649
722
  const storefrontStoreSchema = z.object({
650
723
  storeCode: z.string(),
@@ -656,12 +729,19 @@ const storefrontStoreSchema = z.object({
656
729
  * 비회원 주문 허용. false면 로그인하지 않은 구매자의 주문서 생성·결제 요청이 403 `GUEST_CHECKOUT_DISABLED`다.
657
730
  * 장바구니 담기는 된다 — 주문 버튼을 로그인 안내로 바꾸면 된다
658
731
  */
659
- guestCheckout: z.boolean()
732
+ guestCheckout: z.boolean(),
733
+ /**
734
+ * 켜진 로그인 방법 — 화면에 이 순서대로 버튼을 그리면 된다. `password`가 없으면 이메일·비밀번호 가입·로그인이 꺼져 있다
735
+ */
736
+ loginMethods: z.array(z.enum([
737
+ "password",
738
+ "kakao",
739
+ "naver",
740
+ "google"
741
+ ]))
660
742
  });
661
743
  //#endregion
662
744
  //#region src/client.ts
663
- const productPageSchema = pageSchema(productCardSchema).extend({ facets: productFacetsSchema.optional() });
664
- const reviewPageSchema = pageSchema(publicReviewSchema).extend({ summary: reviewSummarySchema.optional() });
665
745
  function createStorefrontClient(options) {
666
746
  const { session } = options;
667
747
  const accessTokenSource = options.accessToken ?? options.auth?.accessToken ?? (session ? () => session.accessToken : void 0);
@@ -712,8 +792,20 @@ function createStorefrontClient(options) {
712
792
  login: (body) => http.request("POST", "/auth/login", tokenPairSchema, { body: loginRequestSchema.parse(body) }),
713
793
  refresh: (refreshToken) => http.request("POST", "/auth/token/refresh", tokenPairSchema, { body: { refreshToken } }),
714
794
  logout: () => http.requestVoid("POST", "/auth/logout"),
795
+ /** 소셜 로그인 시작 — 공급자 로그인 주소를 받는다. PKCE·state는 `@sayren/storefront-sdk/auth`의 `idp()`가 만든다 */
796
+ idpAuthorize: (provider, body) => http.request("POST", path`/auth/idp/${provider}/authorize`, idpAuthorizeResponseSchema, { body: idpAuthorizeRequestSchema.parse(body) }),
797
+ /** 소셜 로그인 마무리 — 돌아온 1회용 code + codeVerifier를 구매자 토큰으로 바꾼다 */
798
+ idpToken: (body) => http.request("POST", "/auth/idp/token", tokenPairSchema, { body: idpTokenRequestSchema.parse(body) }),
715
799
  /** 구매자 탈퇴 — 로그인 상태(accessToken)에서 비밀번호로 본인 확인. 성공하면 토큰은 모두 무효 */
716
- withdraw: (body) => http.requestVoid("POST", "/auth/withdraw", { body: withdrawRequestSchema.parse(body) })
800
+ withdraw: (body) => http.requestVoid("POST", "/auth/withdraw", { body: withdrawRequestSchema.parse(body) }),
801
+ /** 로그인 수단 — 비밀번호 여부와 연결된 소셜 계정(로그인 필요) */
802
+ identities: () => http.request("GET", "/me/identities", memberIdentitiesSchema),
803
+ /** 소셜 계정 연결 시작(로그인 필요) — 공급자 로그인 주소를 받는다. 본문은 `idpAuthorize`와 같다 */
804
+ linkIdentityAuthorize: (provider, body) => http.request("POST", path`/me/identities/${provider}/authorize`, idpAuthorizeResponseSchema, { body: idpAuthorizeRequestSchema.parse(body) }),
805
+ /** 소셜 계정 연결 마무리 — 돌아온 code + codeVerifier로 이 구매자에 연결한다 */
806
+ linkIdentity: (provider, body) => http.request("POST", path`/me/identities/${provider}`, memberIdentitiesSchema, { body: linkIdentityRequestSchema.parse(body) }),
807
+ /** 소셜 계정 연결 해제 — 마지막 로그인 수단이면 409 `LAST_LOGIN_METHOD` */
808
+ unlinkIdentity: (provider) => http.request("DELETE", path`/me/identities/${provider}`, memberIdentitiesSchema)
717
809
  },
718
810
  store: {
719
811
  /** 스토어 공개 설정 — 이름·고객센터·비회원 주문 허용 여부 */
@@ -810,4 +902,4 @@ getStatus: (paymentId) => http.request("GET", path`/payments/${paymentId}`, paym
810
902
  };
811
903
  }
812
904
  //#endregion
813
- export { loginRequestSchema as $, pageSchema as A, pgParamsSchema as B, claimTypeSchema as C, claimStatusSchema as D, myClaimSchema as E, createCheckoutRequestSchema as F, productDetailSchema as G, shippingAddressInputSchema as H, guestInfoSchema as I, addCartItemRequestSchema as J, productFacetsSchema as K, paymentMethodSchema as L, checkoutSessionSchema as M, confirmPaymentRequestSchema as N, orderItemStatusSchema as O, confirmPaymentResultSchema as P, anonymousSessionSchema as Q, paymentParamsSchema as R, claimReasonSchema as S, createClaimResultSchema as T, categoryNodeSchema as U, requestPaymentRequestSchema as V, productCardSchema as W, cartSchema as X, cartItemSchema as Y, updateCartItemRequestSchema as Z, createInquiryRequestSchema as _, publicReviewSchema as a, buildQuery as at, customerInquiryCategorySchema as b, writableReviewSchema as c, myOrderSchema as d, signupRequestSchema as et, orderShippingAddressSchema as f, updateProfileRequestSchema as g, memberSchema as h, createReviewResultSchema as i, ApiError as it, PAYMENT_STATUS_VALUES as j, pageInfoSchema as k, deliveryTrackingSchema as l, memberAddressSchema as m, storefrontStoreSchema as n, withdrawRequestSchema as nt, reviewSummarySchema as o, toApiError as ot, memberAddressRequestSchema as p, productSortSchema as q, createReviewRequestSchema as r, withdrawalBlockedDetailsSchema as rt, updateReviewRequestSchema as s, unwrapData as st, createStorefrontClient as t, tokenPairSchema as tt, myOrderItemSchema as u, publicInquirySchema as v, createClaimRequestSchema as w, customerInquirySchema as x, createCustomerInquiryRequestSchema as y, paymentStatusSchema as z };
905
+ export { pageSchema as $, createClaimResultSchema as A, paymentStatusSchema as B, publicInquirySchema as C, claimReasonSchema as D, customerInquirySchema as E, confirmPaymentResultSchema as F, productCardSchema as G, requestPaymentRequestSchema as H, createCheckoutRequestSchema as I, productPageSchema as J, productDetailSchema as K, guestInfoSchema as L, PAYMENT_STATUS_VALUES as M, checkoutSessionSchema as N, claimTypeSchema as O, confirmPaymentRequestSchema as P, pageInfoSchema as Q, paymentMethodSchema as R, createInquiryResultSchema as S, customerInquiryCategorySchema as T, shippingAddressInputSchema as U, pgParamsSchema as V, categoryNodeSchema as W, claimStatusSchema as X, productSortSchema as Y, orderItemStatusSchema as Z, memberAddressSchema as _, buildQuery as _t, publicReviewSchema as a, idpAuthorizeRequestSchema as at, wishlistAddResultSchema as b, updateReviewRequestSchema as c, linkIdentityRequestSchema as ct, deliveryTrackingSchema as d, signupRequestSchema as dt, addCartItemRequestSchema as et, myOrderItemSchema as f, socialProviderSchema as ft, memberAddressRequestSchema as g, ApiError as gt, purchaseDecisionResultSchema as h, withdrawalBlockedDetailsSchema as ht, createReviewResultSchema as i, anonymousSessionSchema as it, myClaimSchema as j, createClaimRequestSchema as k, updateReviewResultSchema as l, loginRequestSchema as lt, orderShippingAddressSchema as m, withdrawRequestSchema as mt, storefrontStoreSchema as n, cartSchema as nt, reviewPageSchema as o, idpAuthorizeResponseSchema as ot, myOrderSchema as p, tokenPairSchema as pt, productFacetsSchema as q, createReviewRequestSchema as r, updateCartItemRequestSchema as rt, reviewSummarySchema as s, idpTokenRequestSchema as st, createStorefrontClient as t, cartItemSchema as tt, writableReviewSchema as u, memberIdentitiesSchema as ut, memberSchema as v, toApiError as vt, createCustomerInquiryRequestSchema as w, createInquiryRequestSchema as x, updateProfileRequestSchema as y, unwrapData as yt, paymentParamsSchema as z };
@@ -41,7 +41,7 @@ type AnonymousSession = z.infer<typeof anonymousSessionSchema>;
41
41
  * 403 `PASSWORD_MISMATCH`다(로그인과 같은 5회 잠금).
42
42
  */
43
43
  declare const withdrawRequestSchema: z.ZodObject<{
44
- password: z.ZodString;
44
+ password: z.ZodOptional<z.ZodString>;
45
45
  }, z.core.$strip>;
46
46
  type WithdrawRequest = z.infer<typeof withdrawRequestSchema>;
47
47
  /** `WITHDRAWAL_BLOCKED`의 `error.details` — 탈퇴를 막는 항목 수 */
@@ -51,6 +51,51 @@ declare const withdrawalBlockedDetailsSchema: z.ZodObject<{
51
51
  pendingPayments: z.ZodNumber;
52
52
  }, z.core.$strip>;
53
53
  type WithdrawalBlockedDetails = z.infer<typeof withdrawalBlockedDetailsSchema>;
54
+ /** 소셜 로그인 공급자 */
55
+ declare const socialProviderSchema: z.ZodEnum<{
56
+ kakao: "kakao";
57
+ naver: "naver";
58
+ google: "google";
59
+ }>;
60
+ type SocialProvider = z.infer<typeof socialProviderSchema>;
61
+ /** `POST /auth/idp/{provider}/authorize` 본문 — PKCE(S256)와 state는 SDK가 만든다 */
62
+ declare const idpAuthorizeRequestSchema: z.ZodObject<{
63
+ redirectUri: z.ZodURL;
64
+ codeChallenge: z.ZodString;
65
+ state: z.ZodString;
66
+ }, z.core.$strip>;
67
+ type IdpAuthorizeRequest = z.infer<typeof idpAuthorizeRequestSchema>;
68
+ declare const idpAuthorizeResponseSchema: z.ZodObject<{
69
+ url: z.ZodURL;
70
+ }, z.core.$strip>;
71
+ /** `POST /auth/idp/token` 본문 — 돌아온 1회용 code를 구매자 토큰으로 바꾼다 */
72
+ declare const idpTokenRequestSchema: z.ZodObject<{
73
+ code: z.ZodString;
74
+ codeVerifier: z.ZodString;
75
+ cartToken: z.ZodOptional<z.ZodString>;
76
+ }, z.core.$strip>;
77
+ type IdpTokenRequest = z.infer<typeof idpTokenRequestSchema>;
78
+ /** `GET /me/identities` — 구매자의 로그인 수단(비밀번호 여부와 연결된 소셜 계정) */
79
+ declare const memberIdentitiesSchema: z.ZodObject<{
80
+ hasPassword: z.ZodBoolean;
81
+ hasExternalAuth: z.ZodBoolean;
82
+ identities: z.ZodArray<z.ZodObject<{
83
+ provider: z.ZodEnum<{
84
+ kakao: "kakao";
85
+ naver: "naver";
86
+ google: "google";
87
+ }>;
88
+ email: z.ZodNullable<z.ZodString>;
89
+ linkedAt: z.ZodString;
90
+ }, z.core.$strip>>;
91
+ }, z.core.$strip>;
92
+ type MemberIdentities = z.infer<typeof memberIdentitiesSchema>;
93
+ /** `POST /me/identities/{provider}` 본문 — 연결 흐름에서 돌아온 1회용 code와 PKCE verifier */
94
+ declare const linkIdentityRequestSchema: z.ZodObject<{
95
+ code: z.ZodString;
96
+ codeVerifier: z.ZodString;
97
+ }, z.core.$strip>;
98
+ type LinkIdentityRequest = z.infer<typeof linkIdentityRequestSchema>;
54
99
  //#endregion
55
100
  //#region src/http.d.ts
56
101
  declare class ApiError extends Error {
@@ -68,4 +113,4 @@ type TokenSource = string | null | undefined | (() => string | null | undefined)
68
113
  type Query = Record<string, string | number | boolean | undefined>;
69
114
  declare function buildQuery(query?: Query): string;
70
115
  //#endregion
71
- export { AnonymousSession as a, TokenPair as c, anonymousSessionSchema as d, loginRequestSchema as f, withdrawalBlockedDetailsSchema as g, withdrawRequestSchema as h, buildQuery as i, WithdrawRequest as l, tokenPairSchema as m, Query as n, LoginRequest as o, signupRequestSchema as p, TokenSource as r, SignupRequest as s, ApiError as t, WithdrawalBlockedDetails as u };
116
+ export { signupRequestSchema as C, withdrawalBlockedDetailsSchema as D, withdrawRequestSchema as E, memberIdentitiesSchema as S, tokenPairSchema as T, idpAuthorizeRequestSchema as _, AnonymousSession as a, linkIdentityRequestSchema as b, LinkIdentityRequest as c, SignupRequest as d, SocialProvider as f, anonymousSessionSchema as g, WithdrawalBlockedDetails as h, buildQuery as i, LoginRequest as l, WithdrawRequest as m, Query as n, IdpAuthorizeRequest as o, TokenPair as p, TokenSource as r, IdpTokenRequest as s, ApiError as t, MemberIdentities as u, idpAuthorizeResponseSchema as v, socialProviderSchema as w, loginRequestSchema as x, idpTokenRequestSchema as y };
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as AnonymousSession, c as TokenPair, d as anonymousSessionSchema, f as loginRequestSchema, g as withdrawalBlockedDetailsSchema, h as withdrawRequestSchema, i as buildQuery, l as WithdrawRequest, m as tokenPairSchema, n as Query, o as LoginRequest, p as signupRequestSchema, r as TokenSource, s as SignupRequest, t as ApiError, u as WithdrawalBlockedDetails } from "./http-CxqZxTfE.mjs";
1
+ import { C as signupRequestSchema, D as withdrawalBlockedDetailsSchema, E as withdrawRequestSchema, S as memberIdentitiesSchema, T as tokenPairSchema, _ as idpAuthorizeRequestSchema, a as AnonymousSession, b as linkIdentityRequestSchema, c as LinkIdentityRequest, d as SignupRequest, f as SocialProvider, g as anonymousSessionSchema, h as WithdrawalBlockedDetails, i as buildQuery, l as LoginRequest, m as WithdrawRequest, n as Query, o as IdpAuthorizeRequest, p as TokenPair, r as TokenSource, s as IdpTokenRequest, t as ApiError, u as MemberIdentities, v as idpAuthorizeResponseSchema, w as socialProviderSchema, x as loginRequestSchema, y as idpTokenRequestSchema } from "./http-BbFHVGBf.mjs";
2
2
  import { z } from "zod";
3
3
  //#region src/analytics-ids.d.ts
4
4
  /**
@@ -213,6 +213,37 @@ export declare const productFacetsSchema: z.ZodObject<{
213
213
  }, z.core.$strip>>;
214
214
  }, z.core.$strip>;
215
215
  export type ProductFacets = z.infer<typeof productFacetsSchema>;
216
+ /** `GET /products` 응답 — 상품 카드 페이지와 검색 패싯 */
217
+ export declare const productPageSchema: z.ZodObject<{
218
+ page: z.ZodNumber;
219
+ size: z.ZodNumber;
220
+ totalElements: z.ZodNumber;
221
+ totalPages: z.ZodNumber;
222
+ contents: z.ZodArray<z.ZodObject<{
223
+ productId: z.ZodString;
224
+ name: z.ZodString;
225
+ thumbnailUrl: z.ZodURL;
226
+ salePrice: z.ZodNumber;
227
+ discountedPrice: z.ZodNullable<z.ZodNumber>;
228
+ discountRate: z.ZodNullable<z.ZodNumber>;
229
+ freeShipping: z.ZodBoolean;
230
+ averageRating: z.ZodNumber;
231
+ reviewCount: z.ZodNumber;
232
+ soldOut: z.ZodBoolean;
233
+ }, z.core.$strip>>;
234
+ facets: z.ZodOptional<z.ZodObject<{
235
+ categories: z.ZodOptional<z.ZodArray<z.ZodObject<{
236
+ categoryId: z.ZodString;
237
+ name: z.ZodString;
238
+ count: z.ZodNumber;
239
+ }, z.core.$strip>>>;
240
+ priceRange: z.ZodOptional<z.ZodObject<{
241
+ min: z.ZodNumber;
242
+ max: z.ZodNumber;
243
+ }, z.core.$strip>>;
244
+ }, z.core.$strip>>;
245
+ }, z.core.$strip>;
246
+ export type ProductPage = z.infer<typeof productPageSchema>;
216
247
  //#endregion
217
248
  //#region src/schemas/checkout.d.ts
218
249
  export declare const createCheckoutRequestSchema: z.ZodObject<{
@@ -485,11 +516,17 @@ export declare const createInquiryRequestSchema: z.ZodObject<{
485
516
  secret: z.ZodDefault<z.ZodBoolean>;
486
517
  }, z.core.$strip>;
487
518
  export type CreateInquiryRequest = z.infer<typeof createInquiryRequestSchema>;
519
+ /** `POST /products/{productId}/inquiries` 응답 */
520
+ export declare const createInquiryResultSchema: z.ZodObject<{
521
+ inquiryId: z.ZodString;
522
+ createdAt: z.ZodString;
523
+ }, z.core.$strip>;
524
+ export type CreateInquiryResult = z.infer<typeof createInquiryResultSchema>;
488
525
  //#endregion
489
526
  //#region src/schemas/member.d.ts
490
527
  export declare const memberSchema: z.ZodObject<{
491
528
  memberId: z.ZodString;
492
- email: z.ZodEmail;
529
+ email: z.ZodNullable<z.ZodEmail>;
493
530
  name: z.ZodString;
494
531
  phone: z.ZodNullable<z.ZodString>;
495
532
  point: z.ZodNumber;
@@ -524,6 +561,12 @@ export declare const memberAddressRequestSchema: z.ZodObject<{
524
561
  isDefault: z.ZodDefault<z.ZodBoolean>;
525
562
  }, z.core.$strip>;
526
563
  export type MemberAddressRequest = z.infer<typeof memberAddressRequestSchema>;
564
+ /** `POST /me/wishlist` 응답 — 이미 찜한 상품이어도 같은 응답이다 */
565
+ export declare const wishlistAddResultSchema: z.ZodObject<{
566
+ productId: z.ZodString;
567
+ wished: z.ZodLiteral<true>;
568
+ }, z.core.$strip>;
569
+ export type WishlistAddResult = z.infer<typeof wishlistAddResultSchema>;
527
570
  //#endregion
528
571
  //#region src/schemas/review.d.ts
529
572
  export declare const publicReviewSchema: z.ZodObject<{
@@ -572,6 +615,38 @@ export declare const updateReviewRequestSchema: z.ZodObject<{
572
615
  images: z.ZodOptional<z.ZodArray<z.ZodURL>>;
573
616
  }, z.core.$strip>;
574
617
  export type UpdateReviewRequest = z.infer<typeof updateReviewRequestSchema>;
618
+ /** `GET /products/{productId}/reviews` 응답 — 리뷰 페이지와 평점 분포 */
619
+ export declare const reviewPageSchema: z.ZodObject<{
620
+ page: z.ZodNumber;
621
+ size: z.ZodNumber;
622
+ totalElements: z.ZodNumber;
623
+ totalPages: z.ZodNumber;
624
+ contents: z.ZodArray<z.ZodObject<{
625
+ reviewId: z.ZodString;
626
+ rating: z.ZodNumber;
627
+ content: z.ZodString;
628
+ images: z.ZodArray<z.ZodURL>;
629
+ optionName: z.ZodNullable<z.ZodString>;
630
+ writerMaskedName: z.ZodString;
631
+ sellerReply: z.ZodNullable<z.ZodObject<{
632
+ content: z.ZodString;
633
+ repliedAt: z.ZodString;
634
+ }, z.core.$strip>>;
635
+ createdAt: z.ZodString;
636
+ }, z.core.$strip>>;
637
+ summary: z.ZodOptional<z.ZodObject<{
638
+ averageRating: z.ZodNumber;
639
+ totalCount: z.ZodNumber;
640
+ distribution: z.ZodRecord<z.ZodString, z.ZodNumber>;
641
+ }, z.core.$strip>>;
642
+ }, z.core.$strip>;
643
+ export type ReviewPage = z.infer<typeof reviewPageSchema>;
644
+ /** `PUT /me/reviews/{reviewId}` 응답 */
645
+ export declare const updateReviewResultSchema: z.ZodObject<{
646
+ reviewId: z.ZodString;
647
+ updatedAt: z.ZodString;
648
+ }, z.core.$strip>;
649
+ export type UpdateReviewResult = z.infer<typeof updateReviewResultSchema>;
575
650
  //#endregion
576
651
  //#region src/session.d.ts
577
652
  /**
@@ -672,7 +747,7 @@ export declare function createStorefrontClient(options: StorefrontClientOptions)
672
747
  /** session.userinfo() 위임 — session 미전달 시 에러 */
673
748
  userinfo(): Promise<{
674
749
  memberId: string;
675
- email: string;
750
+ email: string | null;
676
751
  name: string;
677
752
  phone: string | null;
678
753
  point: number;
@@ -701,8 +776,52 @@ export declare function createStorefrontClient(options: StorefrontClientOptions)
701
776
  expiresIn: number;
702
777
  }>;
703
778
  logout: () => Promise<void>;
779
+ /** 소셜 로그인 시작 — 공급자 로그인 주소를 받는다. PKCE·state는 `@sayren/storefront-sdk/auth`의 `idp()`가 만든다 */
780
+ idpAuthorize: (provider: SocialProvider, body: IdpAuthorizeRequest) => Promise<{
781
+ url: string;
782
+ }>;
783
+ /** 소셜 로그인 마무리 — 돌아온 1회용 code + codeVerifier를 구매자 토큰으로 바꾼다 */
784
+ idpToken: (body: IdpTokenRequest) => Promise<{
785
+ accessToken: string;
786
+ refreshToken: string;
787
+ expiresIn: number;
788
+ }>;
704
789
  /** 구매자 탈퇴 — 로그인 상태(accessToken)에서 비밀번호로 본인 확인. 성공하면 토큰은 모두 무효 */
705
790
  withdraw: (body: WithdrawRequest) => Promise<void>;
791
+ /** 로그인 수단 — 비밀번호 여부와 연결된 소셜 계정(로그인 필요) */
792
+ identities: () => Promise<{
793
+ hasPassword: boolean;
794
+ hasExternalAuth: boolean;
795
+ identities: {
796
+ provider: "kakao" | "naver" | "google";
797
+ email: string | null;
798
+ linkedAt: string;
799
+ }[];
800
+ }>;
801
+ /** 소셜 계정 연결 시작(로그인 필요) — 공급자 로그인 주소를 받는다. 본문은 `idpAuthorize`와 같다 */
802
+ linkIdentityAuthorize: (provider: SocialProvider, body: IdpAuthorizeRequest) => Promise<{
803
+ url: string;
804
+ }>;
805
+ /** 소셜 계정 연결 마무리 — 돌아온 code + codeVerifier로 이 구매자에 연결한다 */
806
+ linkIdentity: (provider: SocialProvider, body: LinkIdentityRequest) => Promise<{
807
+ hasPassword: boolean;
808
+ hasExternalAuth: boolean;
809
+ identities: {
810
+ provider: "kakao" | "naver" | "google";
811
+ email: string | null;
812
+ linkedAt: string;
813
+ }[];
814
+ }>;
815
+ /** 소셜 계정 연결 해제 — 마지막 로그인 수단이면 409 `LAST_LOGIN_METHOD` */
816
+ unlinkIdentity: (provider: SocialProvider) => Promise<{
817
+ hasPassword: boolean;
818
+ hasExternalAuth: boolean;
819
+ identities: {
820
+ provider: "kakao" | "naver" | "google";
821
+ email: string | null;
822
+ linkedAt: string;
823
+ }[];
824
+ }>;
706
825
  };
707
826
  store: {
708
827
  /** 스토어 공개 설정 — 이름·고객센터·비회원 주문 허용 여부 */
@@ -713,6 +832,7 @@ export declare function createStorefrontClient(options: StorefrontClientOptions)
713
832
  customerCenterPhone: string | null;
714
833
  businessHours: string | null;
715
834
  guestCheckout: boolean;
835
+ loginMethods: ("password" | "kakao" | "naver" | "google")[];
716
836
  }>;
717
837
  };
718
838
  catalog: {
@@ -1140,7 +1260,7 @@ export declare function createStorefrontClient(options: StorefrontClientOptions)
1140
1260
  member: {
1141
1261
  me: () => Promise<{
1142
1262
  memberId: string;
1143
- email: string;
1263
+ email: string | null;
1144
1264
  name: string;
1145
1265
  phone: string | null;
1146
1266
  point: number;
@@ -1495,10 +1615,17 @@ export declare const deliveryTrackingSchema: z.ZodObject<{
1495
1615
  }, z.core.$strip>>;
1496
1616
  }, z.core.$strip>;
1497
1617
  export type DeliveryTracking = z.infer<typeof deliveryTrackingSchema>;
1618
+ /** `POST /me/order-items/{orderItemId}/purchase-decision` 응답 */
1619
+ export declare const purchaseDecisionResultSchema: z.ZodObject<{
1620
+ orderItemId: z.ZodString;
1621
+ status: z.ZodLiteral<"PURCHASE_DECIDED">;
1622
+ decidedAt: z.ZodString;
1623
+ }, z.core.$strip>;
1624
+ export type PurchaseDecisionResult = z.infer<typeof purchaseDecisionResultSchema>;
1498
1625
  //#endregion
1499
1626
  //#region src/schemas/store.d.ts
1500
1627
  /**
1501
- * 스토어 공개 설정 — `GET /store`. 스토어프런트가 화면을 그릴 때 읽는 값만 담는다.
1628
+ * 스토어 공개 설정 — `GET /store`. 스토어프론트가 화면을 그릴 때 읽는 값만 담는다.
1502
1629
  */
1503
1630
  export declare const storefrontStoreSchema: z.ZodObject<{
1504
1631
  storeCode: z.ZodString;
@@ -1507,7 +1634,13 @@ export declare const storefrontStoreSchema: z.ZodObject<{
1507
1634
  customerCenterPhone: z.ZodNullable<z.ZodString>;
1508
1635
  businessHours: z.ZodNullable<z.ZodString>;
1509
1636
  guestCheckout: z.ZodBoolean;
1637
+ loginMethods: z.ZodArray<z.ZodEnum<{
1638
+ password: "password";
1639
+ kakao: "kakao";
1640
+ naver: "naver";
1641
+ google: "google";
1642
+ }>>;
1510
1643
  }, z.core.$strip>;
1511
1644
  export type StorefrontStore = z.infer<typeof storefrontStoreSchema>;
1512
1645
  //#endregion
1513
- export { AnonymousSession, ApiError, LoginRequest, type Query, type SessionStorage, SignupRequest, type StorefrontClient, type StorefrontClientOptions, type StorefrontSession, type StorefrontSessionOptions, TokenPair, type TokenSource, WithdrawRequest, WithdrawalBlockedDetails, anonymousSessionSchema, buildQuery, loginRequestSchema, signupRequestSchema, tokenPairSchema, withdrawRequestSchema, withdrawalBlockedDetailsSchema };
1646
+ export { AnonymousSession, ApiError, IdpAuthorizeRequest, IdpTokenRequest, LinkIdentityRequest, LoginRequest, MemberIdentities, type Query, type SessionStorage, SignupRequest, SocialProvider, type StorefrontClient, type StorefrontClientOptions, type StorefrontSession, type StorefrontSessionOptions, TokenPair, type TokenSource, WithdrawRequest, WithdrawalBlockedDetails, anonymousSessionSchema, buildQuery, idpAuthorizeRequestSchema, idpAuthorizeResponseSchema, idpTokenRequestSchema, linkIdentityRequestSchema, loginRequestSchema, memberIdentitiesSchema, signupRequestSchema, socialProviderSchema, tokenPairSchema, withdrawRequestSchema, withdrawalBlockedDetailsSchema };
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { a as ANALYTICS_VISITOR_HEADER, c as analyticsClientEventSchema, i as ANALYTICS_VISITOR_COOKIE, l as analyticsIdSchema, n as ANALYTICS_SESSION_COOKIE, o as analyticsBatchSchema, r as ANALYTICS_SESSION_HEADER, s as analyticsClientEventNameSchema, t as ANALYTICS_MAX_BATCH_BYTES, u as analyticsServerEventNameSchema } from "./analytics-CgD70OqH.mjs";
2
2
  import { t as analyticsIdsFromCookie } from "./analytics-ids-COElZiI3.mjs";
3
- import { $ as loginRequestSchema, A as pageSchema, B as pgParamsSchema, C as claimTypeSchema, D as claimStatusSchema, E as myClaimSchema, F as createCheckoutRequestSchema, G as productDetailSchema, H as shippingAddressInputSchema, I as guestInfoSchema, J as addCartItemRequestSchema, K as productFacetsSchema, L as paymentMethodSchema, M as checkoutSessionSchema, N as confirmPaymentRequestSchema, O as orderItemStatusSchema, P as confirmPaymentResultSchema, Q as anonymousSessionSchema, R as paymentParamsSchema, S as claimReasonSchema, T as createClaimResultSchema, U as categoryNodeSchema, V as requestPaymentRequestSchema, W as productCardSchema, X as cartSchema, Y as cartItemSchema, Z as updateCartItemRequestSchema, _ as createInquiryRequestSchema, a as publicReviewSchema, at as buildQuery, b as customerInquiryCategorySchema, c as writableReviewSchema, d as myOrderSchema, et as signupRequestSchema, f as orderShippingAddressSchema, g as updateProfileRequestSchema, h as memberSchema, i as createReviewResultSchema, it as ApiError, j as PAYMENT_STATUS_VALUES, k as pageInfoSchema, l as deliveryTrackingSchema, m as memberAddressSchema, n as storefrontStoreSchema, nt as withdrawRequestSchema, o as reviewSummarySchema, ot as toApiError, p as memberAddressRequestSchema, q as productSortSchema, r as createReviewRequestSchema, rt as withdrawalBlockedDetailsSchema, s as updateReviewRequestSchema, st as unwrapData, t as createStorefrontClient, tt as tokenPairSchema, u as myOrderItemSchema, v as publicInquirySchema, w as createClaimRequestSchema, x as customerInquirySchema, y as createCustomerInquiryRequestSchema, z as paymentStatusSchema } from "./client-D-w3LIU2.mjs";
3
+ import { $ as pageSchema, A as createClaimResultSchema, B as paymentStatusSchema, C as publicInquirySchema, D as claimReasonSchema, E as customerInquirySchema, F as confirmPaymentResultSchema, G as productCardSchema, H as requestPaymentRequestSchema, I as createCheckoutRequestSchema, J as productPageSchema, K as productDetailSchema, L as guestInfoSchema, M as PAYMENT_STATUS_VALUES, N as checkoutSessionSchema, O as claimTypeSchema, P as confirmPaymentRequestSchema, Q as pageInfoSchema, R as paymentMethodSchema, S as createInquiryResultSchema, T as customerInquiryCategorySchema, U as shippingAddressInputSchema, V as pgParamsSchema, W as categoryNodeSchema, X as claimStatusSchema, Y as productSortSchema, Z as orderItemStatusSchema, _ as memberAddressSchema, _t as buildQuery, a as publicReviewSchema, at as idpAuthorizeRequestSchema, b as wishlistAddResultSchema, c as updateReviewRequestSchema, ct as linkIdentityRequestSchema, d as deliveryTrackingSchema, dt as signupRequestSchema, et as addCartItemRequestSchema, f as myOrderItemSchema, ft as socialProviderSchema, g as memberAddressRequestSchema, gt as ApiError, h as purchaseDecisionResultSchema, ht as withdrawalBlockedDetailsSchema, i as createReviewResultSchema, it as anonymousSessionSchema, j as myClaimSchema, k as createClaimRequestSchema, l as updateReviewResultSchema, lt as loginRequestSchema, m as orderShippingAddressSchema, mt as withdrawRequestSchema, n as storefrontStoreSchema, nt as cartSchema, o as reviewPageSchema, ot as idpAuthorizeResponseSchema, p as myOrderSchema, pt as tokenPairSchema, q as productFacetsSchema, r as createReviewRequestSchema, rt as updateCartItemRequestSchema, s as reviewSummarySchema, st as idpTokenRequestSchema, t as createStorefrontClient, tt as cartItemSchema, u as writableReviewSchema, ut as memberIdentitiesSchema, v as memberSchema, vt as toApiError, w as createCustomerInquiryRequestSchema, x as createInquiryRequestSchema, y as updateProfileRequestSchema, yt as unwrapData, z as paymentParamsSchema } from "./client-B1uXGefY.mjs";
4
4
  //#region src/session.ts
5
5
  /** 세션 키 상수 — SDK가 소유한다. store-sdk와 겹치지 않는 별도 네임스페이스. */
6
6
  const STOREFRONT_SESSION_KEYS = {
@@ -124,4 +124,4 @@ function createStorefrontSession(options) {
124
124
  };
125
125
  }
126
126
  //#endregion
127
- export { ANALYTICS_MAX_BATCH_BYTES, ANALYTICS_SESSION_COOKIE, ANALYTICS_SESSION_HEADER, ANALYTICS_VISITOR_COOKIE, ANALYTICS_VISITOR_HEADER, ApiError, PAYMENT_STATUS_VALUES, STOREFRONT_SESSION_KEYS, addCartItemRequestSchema, analyticsBatchSchema, analyticsClientEventNameSchema, analyticsClientEventSchema, analyticsIdSchema, analyticsIdsFromCookie, analyticsServerEventNameSchema, anonymousSessionSchema, buildQuery, cartItemSchema, cartSchema, categoryNodeSchema, checkoutSessionSchema, claimReasonSchema, claimStatusSchema, claimTypeSchema, confirmPaymentRequestSchema, confirmPaymentResultSchema, createCheckoutRequestSchema, createClaimRequestSchema, createClaimResultSchema, createCustomerInquiryRequestSchema, createInquiryRequestSchema, createReviewRequestSchema, createReviewResultSchema, createStorefrontClient, createStorefrontSession, customerInquiryCategorySchema, customerInquirySchema, deliveryTrackingSchema, guestInfoSchema, loginRequestSchema, memberAddressRequestSchema, memberAddressSchema, memberSchema, myClaimSchema, myOrderItemSchema, myOrderSchema, orderItemStatusSchema, orderShippingAddressSchema, pageInfoSchema, pageSchema, paymentMethodSchema, paymentParamsSchema, paymentStatusSchema, pgParamsSchema, productCardSchema, productDetailSchema, productFacetsSchema, productSortSchema, publicInquirySchema, publicReviewSchema, requestPaymentRequestSchema, reviewSummarySchema, shippingAddressInputSchema, signupRequestSchema, storefrontStoreSchema, tokenPairSchema, updateCartItemRequestSchema, updateProfileRequestSchema, updateReviewRequestSchema, withdrawRequestSchema, withdrawalBlockedDetailsSchema, writableReviewSchema };
127
+ export { ANALYTICS_MAX_BATCH_BYTES, ANALYTICS_SESSION_COOKIE, ANALYTICS_SESSION_HEADER, ANALYTICS_VISITOR_COOKIE, ANALYTICS_VISITOR_HEADER, ApiError, PAYMENT_STATUS_VALUES, STOREFRONT_SESSION_KEYS, addCartItemRequestSchema, analyticsBatchSchema, analyticsClientEventNameSchema, analyticsClientEventSchema, analyticsIdSchema, analyticsIdsFromCookie, analyticsServerEventNameSchema, anonymousSessionSchema, buildQuery, cartItemSchema, cartSchema, categoryNodeSchema, checkoutSessionSchema, claimReasonSchema, claimStatusSchema, claimTypeSchema, confirmPaymentRequestSchema, confirmPaymentResultSchema, createCheckoutRequestSchema, createClaimRequestSchema, createClaimResultSchema, createCustomerInquiryRequestSchema, createInquiryRequestSchema, createInquiryResultSchema, createReviewRequestSchema, createReviewResultSchema, createStorefrontClient, createStorefrontSession, customerInquiryCategorySchema, customerInquirySchema, deliveryTrackingSchema, guestInfoSchema, idpAuthorizeRequestSchema, idpAuthorizeResponseSchema, idpTokenRequestSchema, linkIdentityRequestSchema, loginRequestSchema, memberAddressRequestSchema, memberAddressSchema, memberIdentitiesSchema, memberSchema, myClaimSchema, myOrderItemSchema, myOrderSchema, orderItemStatusSchema, orderShippingAddressSchema, pageInfoSchema, pageSchema, paymentMethodSchema, paymentParamsSchema, paymentStatusSchema, pgParamsSchema, productCardSchema, productDetailSchema, productFacetsSchema, productPageSchema, productSortSchema, publicInquirySchema, publicReviewSchema, purchaseDecisionResultSchema, requestPaymentRequestSchema, reviewPageSchema, reviewSummarySchema, shippingAddressInputSchema, signupRequestSchema, socialProviderSchema, storefrontStoreSchema, tokenPairSchema, updateCartItemRequestSchema, updateProfileRequestSchema, updateReviewRequestSchema, updateReviewResultSchema, wishlistAddResultSchema, withdrawRequestSchema, withdrawalBlockedDetailsSchema, writableReviewSchema };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sayren/storefront-sdk",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "main": "./dist/index.mjs",
5
5
  "types": "./dist/index.d.mts",
6
6
  "exports": {
package/src/auth/index.ts CHANGED
@@ -3,7 +3,9 @@ import { ApiError } from "../http";
3
3
  import type {
4
4
  AnonymousSession,
5
5
  LoginRequest,
6
+ MemberIdentities,
6
7
  SignupRequest,
8
+ SocialProvider,
7
9
  TokenPair,
8
10
  WithdrawRequest,
9
11
  } from "../schemas/auth";
@@ -13,12 +15,62 @@ export { ApiError } from "../http";
13
15
  export type {
14
16
  AnonymousSession,
15
17
  LoginRequest,
18
+ MemberIdentities,
16
19
  SignupRequest,
20
+ SocialProvider,
17
21
  TokenPair,
18
22
  WithdrawalBlockedDetails,
19
23
  WithdrawRequest,
20
24
  } from "../schemas/auth";
21
25
 
26
+ /** 소셜 로그인 시작 결과 — `codeVerifier`와 `state`는 돌아올 때까지 서버 세션(httpOnly 쿠키 등)에 보관한다 */
27
+ export interface IdpStart {
28
+ /** 구매자를 보낼 공급자 로그인 주소 */
29
+ url: string;
30
+ state: string;
31
+ codeVerifier: string;
32
+ }
33
+
34
+ export interface IdpCallbackInput {
35
+ /** 돌아온 주소의 `code`(성공) — 실패면 `error`가 온다 */
36
+ code: string | null;
37
+ /** 돌아온 주소의 `state` */
38
+ state: string | null;
39
+ /** `idp()`가 준 값 — 보관해 둔 것 */
40
+ expectedState: string;
41
+ codeVerifier: string;
42
+ /** 비회원 장바구니 토큰 — 주면 회원 장바구니로 합친다 */
43
+ cartToken?: string;
44
+ }
45
+
46
+ /** 소셜 로그인 실패 — 구매자가 취소했거나(`access_denied`) state가 맞지 않는다 */
47
+ export class IdpCallbackError extends Error {
48
+ constructor(
49
+ public readonly reason: "access_denied" | "state_mismatch" | "missing_code" | string,
50
+ ) {
51
+ super(`소셜 로그인을 마치지 못했어요 (${reason})`);
52
+ this.name = "IdpCallbackError";
53
+ }
54
+ }
55
+
56
+ function base64url(bytes: Uint8Array): string {
57
+ let binary = "";
58
+ for (const byte of bytes) binary += String.fromCharCode(byte);
59
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
60
+ }
61
+
62
+ function randomToken(bytes = 32): string {
63
+ return base64url(globalThis.crypto.getRandomValues(new Uint8Array(bytes)));
64
+ }
65
+
66
+ async function s256(verifier: string): Promise<string> {
67
+ const digest = await globalThis.crypto.subtle.digest(
68
+ "SHA-256",
69
+ new TextEncoder().encode(verifier),
70
+ );
71
+ return base64url(new Uint8Array(digest));
72
+ }
73
+
22
74
  export interface StorefrontAuthOptions {
23
75
  /** 예: https://api.sayren.app/storefront/v1 */
24
76
  baseUrl: string;
@@ -28,7 +80,7 @@ export interface StorefrontAuthOptions {
28
80
  }
29
81
 
30
82
  /**
31
- * 구매자 인증 — 화면은 스토어프런트가 그리고, 계정 저장과 인증 처리는 sayren이 한다.
83
+ * 구매자 인증 — 화면은 스토어프론트가 그리고, 계정 저장과 인증 처리는 sayren이 한다.
32
84
  * 어느 방식으로 로그인해도 결과는 같은 구매자 토큰(`TokenPair`)이고 `/me/*`·장바구니·주문 API에 그대로 쓴다.
33
85
  *
34
86
  * 토큰 보관은 앱 몫이다. 서버(loader·action)에서 호출하고 httpOnly 쿠키에 두기를 권한다.
@@ -65,6 +117,82 @@ export function createStorefrontAuth(options: StorefrontAuthOptions) {
65
117
  signUp(body: SignupRequest): Promise<TokenPair> {
66
118
  return client().auth.signup(body);
67
119
  },
120
+ /**
121
+ * 소셜 로그인 시작 — PKCE(S256)와 state를 만들고 공급자 로그인 주소를 받는다. 반환한 `state`·`codeVerifier`를
122
+ * 서버 세션에 보관하고 구매자를 `url`로 보낸다. 로그인을 마치면 `redirectUri`로 `?code=&state=`가 붙어 돌아온다.
123
+ * `redirectUri`는 스토어프론트 도메인 목록 안이어야 한다
124
+ */
125
+ async idp(provider: SocialProvider, options: { redirectUri: string }): Promise<IdpStart> {
126
+ const state = randomToken(24);
127
+ const codeVerifier = randomToken(32);
128
+ const { url } = await client().auth.idpAuthorize(provider, {
129
+ redirectUri: options.redirectUri,
130
+ codeChallenge: await s256(codeVerifier),
131
+ state,
132
+ });
133
+ return { url, state, codeVerifier };
134
+ },
135
+ /**
136
+ * 소셜 로그인 마무리 — 돌아온 `code`를 구매자 토큰으로 바꾼다. state가 다르거나 구매자가 취소했으면
137
+ * `IdpCallbackError`. code는 1회용이고 1분 안에 바꿔야 한다
138
+ */
139
+ idpCallback(input: IdpCallbackInput & { error?: string | null }): Promise<TokenPair> {
140
+ if (input.error) return Promise.reject(new IdpCallbackError(input.error));
141
+ if (!input.state || input.state !== input.expectedState) {
142
+ return Promise.reject(new IdpCallbackError("state_mismatch"));
143
+ }
144
+ if (!input.code) return Promise.reject(new IdpCallbackError("missing_code"));
145
+ return client().auth.idpToken({
146
+ code: input.code,
147
+ codeVerifier: input.codeVerifier,
148
+ cartToken: input.cartToken,
149
+ });
150
+ },
151
+ /** 로그인 수단 — 비밀번호가 있는지, 어떤 소셜 계정이 연결됐는지 */
152
+ identities(accessToken: string): Promise<MemberIdentities> {
153
+ return client(accessToken).auth.identities();
154
+ },
155
+ /**
156
+ * 소셜 계정 연결 시작 — 로그인한 구매자에 공급자 계정을 붙인다. `idp()`처럼 `state`·`codeVerifier`를 보관하고
157
+ * 구매자를 `url`로 보낸다. 돌아오면 `linkIdpCallback()`으로 마무리한다. 연결은 흐름을 시작한 구매자에게만 된다
158
+ */
159
+ async linkIdp(
160
+ accessToken: string,
161
+ provider: SocialProvider,
162
+ options: { redirectUri: string },
163
+ ): Promise<IdpStart> {
164
+ const state = randomToken(24);
165
+ const codeVerifier = randomToken(32);
166
+ const { url } = await client(accessToken).auth.linkIdentityAuthorize(provider, {
167
+ redirectUri: options.redirectUri,
168
+ codeChallenge: await s256(codeVerifier),
169
+ state,
170
+ });
171
+ return { url, state, codeVerifier };
172
+ },
173
+ /**
174
+ * 소셜 계정 연결 마무리 — state를 비교하고 code를 바꿔 연결한다. 다른 구매자에 이미 연결된 계정이면
175
+ * 409 `IDENTITY_IN_USE`, 같은 공급자의 다른 계정을 이미 연결했으면 409 `PROVIDER_ALREADY_LINKED`
176
+ */
177
+ linkIdpCallback(
178
+ accessToken: string,
179
+ provider: SocialProvider,
180
+ input: Omit<IdpCallbackInput, "cartToken"> & { error?: string | null },
181
+ ): Promise<MemberIdentities> {
182
+ if (input.error) return Promise.reject(new IdpCallbackError(input.error));
183
+ if (!input.state || input.state !== input.expectedState) {
184
+ return Promise.reject(new IdpCallbackError("state_mismatch"));
185
+ }
186
+ if (!input.code) return Promise.reject(new IdpCallbackError("missing_code"));
187
+ return client(accessToken).auth.linkIdentity(provider, {
188
+ code: input.code,
189
+ codeVerifier: input.codeVerifier,
190
+ });
191
+ },
192
+ /** 소셜 계정 연결 해제 — 마지막 로그인 수단이면 409 `LAST_LOGIN_METHOD` */
193
+ unlinkIdp(accessToken: string, provider: SocialProvider): Promise<MemberIdentities> {
194
+ return client(accessToken).auth.unlinkIdentity(provider);
195
+ },
68
196
  /** 토큰 갱신 — refreshToken은 한 번 쓰면 폐기되고 새 쌍이 온다(회전) */
69
197
  refresh(refreshToken: string): Promise<TokenPair> {
70
198
  return client().auth.refresh(refreshToken);
package/src/client.ts CHANGED
@@ -4,7 +4,16 @@ import { ANALYTICS_SESSION_HEADER, ANALYTICS_VISITOR_HEADER } from "./schemas/an
4
4
  import type { LoginRequest, SignupRequest } from "./schemas/auth";
5
5
  import {
6
6
  anonymousSessionSchema,
7
+ type IdpAuthorizeRequest,
8
+ type IdpTokenRequest,
9
+ idpAuthorizeRequestSchema,
10
+ idpAuthorizeResponseSchema,
11
+ idpTokenRequestSchema,
12
+ type LinkIdentityRequest,
13
+ linkIdentityRequestSchema,
7
14
  loginRequestSchema,
15
+ memberIdentitiesSchema,
16
+ type SocialProvider,
8
17
  signupRequestSchema,
9
18
  tokenPairSchema,
10
19
  type WithdrawRequest,
@@ -22,7 +31,7 @@ import {
22
31
  type ProductSearchParams,
23
32
  productCardSchema,
24
33
  productDetailSchema,
25
- productFacetsSchema,
34
+ productPageSchema,
26
35
  } from "./schemas/catalog";
27
36
  import {
28
37
  type CreateCheckoutRequest,
@@ -67,8 +76,7 @@ import {
67
76
  type CreateReviewRequest,
68
77
  createReviewRequestSchema,
69
78
  createReviewResultSchema,
70
- publicReviewSchema,
71
- reviewSummarySchema,
79
+ reviewPageSchema,
72
80
  type UpdateReviewRequest,
73
81
  updateReviewRequestSchema,
74
82
  writableReviewSchema,
@@ -111,14 +119,6 @@ export interface StorefrontClientOptions {
111
119
  fetch?: typeof globalThis.fetch;
112
120
  }
113
121
 
114
- const productPageSchema = pageSchema(productCardSchema).extend({
115
- facets: productFacetsSchema.optional(),
116
- });
117
-
118
- const reviewPageSchema = pageSchema(publicReviewSchema).extend({
119
- summary: reviewSummarySchema.optional(),
120
- });
121
-
122
122
  export function createStorefrontClient(options: StorefrontClientOptions) {
123
123
  const { session } = options;
124
124
 
@@ -184,9 +184,37 @@ export function createStorefrontClient(options: StorefrontClientOptions) {
184
184
  refresh: (refreshToken: string) =>
185
185
  http.request("POST", "/auth/token/refresh", tokenPairSchema, { body: { refreshToken } }),
186
186
  logout: () => http.requestVoid("POST", "/auth/logout"),
187
+ /** 소셜 로그인 시작 — 공급자 로그인 주소를 받는다. PKCE·state는 `@sayren/storefront-sdk/auth`의 `idp()`가 만든다 */
188
+ idpAuthorize: (provider: SocialProvider, body: IdpAuthorizeRequest) =>
189
+ http.request("POST", path`/auth/idp/${provider}/authorize`, idpAuthorizeResponseSchema, {
190
+ body: idpAuthorizeRequestSchema.parse(body),
191
+ }),
192
+ /** 소셜 로그인 마무리 — 돌아온 1회용 code + codeVerifier를 구매자 토큰으로 바꾼다 */
193
+ idpToken: (body: IdpTokenRequest) =>
194
+ http.request("POST", "/auth/idp/token", tokenPairSchema, {
195
+ body: idpTokenRequestSchema.parse(body),
196
+ }),
187
197
  /** 구매자 탈퇴 — 로그인 상태(accessToken)에서 비밀번호로 본인 확인. 성공하면 토큰은 모두 무효 */
188
198
  withdraw: (body: WithdrawRequest) =>
189
199
  http.requestVoid("POST", "/auth/withdraw", { body: withdrawRequestSchema.parse(body) }),
200
+ /** 로그인 수단 — 비밀번호 여부와 연결된 소셜 계정(로그인 필요) */
201
+ identities: () => http.request("GET", "/me/identities", memberIdentitiesSchema),
202
+ /** 소셜 계정 연결 시작(로그인 필요) — 공급자 로그인 주소를 받는다. 본문은 `idpAuthorize`와 같다 */
203
+ linkIdentityAuthorize: (provider: SocialProvider, body: IdpAuthorizeRequest) =>
204
+ http.request(
205
+ "POST",
206
+ path`/me/identities/${provider}/authorize`,
207
+ idpAuthorizeResponseSchema,
208
+ { body: idpAuthorizeRequestSchema.parse(body) },
209
+ ),
210
+ /** 소셜 계정 연결 마무리 — 돌아온 code + codeVerifier로 이 구매자에 연결한다 */
211
+ linkIdentity: (provider: SocialProvider, body: LinkIdentityRequest) =>
212
+ http.request("POST", path`/me/identities/${provider}`, memberIdentitiesSchema, {
213
+ body: linkIdentityRequestSchema.parse(body),
214
+ }),
215
+ /** 소셜 계정 연결 해제 — 마지막 로그인 수단이면 409 `LAST_LOGIN_METHOD` */
216
+ unlinkIdentity: (provider: SocialProvider) =>
217
+ http.request("DELETE", path`/me/identities/${provider}`, memberIdentitiesSchema),
190
218
  },
191
219
 
192
220
  store: {
@@ -61,7 +61,11 @@ export type AnonymousSession = z.infer<typeof anonymousSessionSchema>;
61
61
  * 403 `PASSWORD_MISMATCH`다(로그인과 같은 5회 잠금).
62
62
  */
63
63
  export const withdrawRequestSchema = z.object({
64
- password: z.string().min(1),
64
+ /**
65
+ * 비밀번호로 가입한 구매자는 필수. 소셜 로그인으로만 가입한 구매자는 비밀번호가 없어 생략하고, 대신 최근 10분 안에
66
+ * 로그인한 액세스 토큰이어야 한다(아니면 403 `REAUTH_REQUIRED` — 다시 로그인한 뒤 호출)
67
+ */
68
+ password: z.string().min(1).optional(),
65
69
  });
66
70
 
67
71
  export type WithdrawRequest = z.infer<typeof withdrawRequestSchema>;
@@ -77,3 +81,55 @@ export const withdrawalBlockedDetailsSchema = z.object({
77
81
  });
78
82
 
79
83
  export type WithdrawalBlockedDetails = z.infer<typeof withdrawalBlockedDetailsSchema>;
84
+
85
+ /** 소셜 로그인 공급자 */
86
+ export const socialProviderSchema = z.enum(["kakao", "naver", "google"]);
87
+ export type SocialProvider = z.infer<typeof socialProviderSchema>;
88
+
89
+ /** `POST /auth/idp/{provider}/authorize` 본문 — PKCE(S256)와 state는 SDK가 만든다 */
90
+ export const idpAuthorizeRequestSchema = z.object({
91
+ /** 로그인을 마치고 돌아올 스토어프론트 주소. 스토어프론트 도메인 목록 안이어야 한다 */
92
+ redirectUri: z.url(),
93
+ /** base64url(SHA-256(codeVerifier)) */
94
+ codeChallenge: z.string().regex(/^[A-Za-z0-9_-]{43}$/),
95
+ /** 스토어프론트가 만든 값 — 돌아올 때 그대로 붙어 온다. 세션에 저장해 두고 비교한다 */
96
+ state: z.string().min(16).max(128),
97
+ });
98
+ export type IdpAuthorizeRequest = z.infer<typeof idpAuthorizeRequestSchema>;
99
+
100
+ export const idpAuthorizeResponseSchema = z.object({
101
+ /** 구매자를 보낼 공급자 로그인 주소 */
102
+ url: z.url(),
103
+ });
104
+
105
+ /** `POST /auth/idp/token` 본문 — 돌아온 1회용 code를 구매자 토큰으로 바꾼다 */
106
+ export const idpTokenRequestSchema = z.object({
107
+ code: z.string().min(1),
108
+ codeVerifier: z.string().regex(/^[A-Za-z0-9._~-]{43,128}$/),
109
+ /** 비회원 장바구니 토큰 — 주면 회원 장바구니로 합친다 */
110
+ cartToken: z.string().optional(),
111
+ });
112
+ export type IdpTokenRequest = z.infer<typeof idpTokenRequestSchema>;
113
+
114
+ /** `GET /me/identities` — 구매자의 로그인 수단(비밀번호 여부와 연결된 소셜 계정) */
115
+ export const memberIdentitiesSchema = z.object({
116
+ hasPassword: z.boolean(),
117
+ /** 셀러 자체 회원 연동(구매자 위임)으로 들어온 구매자 — 그 로그인도 로그인 수단으로 센다 */
118
+ hasExternalAuth: z.boolean(),
119
+ identities: z.array(
120
+ z.object({
121
+ provider: socialProviderSchema,
122
+ /** 공급자가 알려 준 이메일(참고값). 없을 수 있다 */
123
+ email: z.string().nullable(),
124
+ linkedAt: z.string(),
125
+ }),
126
+ ),
127
+ });
128
+ export type MemberIdentities = z.infer<typeof memberIdentitiesSchema>;
129
+
130
+ /** `POST /me/identities/{provider}` 본문 — 연결 흐름에서 돌아온 1회용 code와 PKCE verifier */
131
+ export const linkIdentityRequestSchema = z.object({
132
+ code: z.string().min(1),
133
+ codeVerifier: z.string().regex(/^[A-Za-z0-9._~-]{43,128}$/),
134
+ });
135
+ export type LinkIdentityRequest = z.infer<typeof linkIdentityRequestSchema>;
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import type { PageParams } from "./common";
2
+ import { type PageParams, pageSchema } from "./common";
3
3
 
4
4
  export interface CategoryNode {
5
5
  categoryId: string;
@@ -21,9 +21,13 @@ export const productCardSchema = z.object({
21
21
  productId: z.string(),
22
22
  name: z.string(),
23
23
  thumbnailUrl: z.url(),
24
- salePrice: z.number().int(),
25
- discountedPrice: z.number().int().nullable(),
26
- discountRate: z.number().int().nullable(),
24
+ salePrice: z.number().int().describe("판매가. 원(KRW) 단위 정수"),
25
+ discountedPrice: z
26
+ .number()
27
+ .int()
28
+ .nullable()
29
+ .describe("즉시 할인 적용가. 원 단위. 할인이 없으면 null"),
30
+ discountRate: z.number().int().nullable().describe("할인율(%). 할인이 없으면 null"),
27
31
  freeShipping: z.boolean(),
28
32
  averageRating: z.number(),
29
33
  reviewCount: z.number().int(),
@@ -99,3 +103,10 @@ export const productFacetsSchema = z.object({
99
103
  });
100
104
 
101
105
  export type ProductFacets = z.infer<typeof productFacetsSchema>;
106
+
107
+ /** `GET /products` 응답 — 상품 카드 페이지와 검색 패싯 */
108
+ export const productPageSchema = pageSchema(productCardSchema).extend({
109
+ facets: productFacetsSchema.optional(),
110
+ });
111
+
112
+ export type ProductPage = z.infer<typeof productPageSchema>;
@@ -23,3 +23,11 @@ export const createInquiryRequestSchema = z.object({
23
23
  });
24
24
 
25
25
  export type CreateInquiryRequest = z.infer<typeof createInquiryRequestSchema>;
26
+
27
+ /** `POST /products/{productId}/inquiries` 응답 */
28
+ export const createInquiryResultSchema = z.object({
29
+ inquiryId: z.string(),
30
+ createdAt: z.string(),
31
+ });
32
+
33
+ export type CreateInquiryResult = z.infer<typeof createInquiryResultSchema>;
@@ -2,7 +2,8 @@ import { z } from "zod";
2
2
 
3
3
  export const memberSchema = z.object({
4
4
  memberId: z.string(),
5
- email: z.email(),
5
+ /** 소셜 가입에서 공급자가 이메일을 주지 않았으면 null */
6
+ email: z.email().nullable(),
6
7
  name: z.string(),
7
8
  phone: z.string().nullable(),
8
9
  point: z.number().int(),
@@ -48,3 +49,11 @@ export const memberAddressRequestSchema = z.object({
48
49
  });
49
50
 
50
51
  export type MemberAddressRequest = z.infer<typeof memberAddressRequestSchema>;
52
+
53
+ /** `POST /me/wishlist` 응답 — 이미 찜한 상품이어도 같은 응답이다 */
54
+ export const wishlistAddResultSchema = z.object({
55
+ productId: z.string(),
56
+ wished: z.literal(true),
57
+ });
58
+
59
+ export type WishlistAddResult = z.infer<typeof wishlistAddResultSchema>;
@@ -74,3 +74,12 @@ export const deliveryTrackingSchema = z.object({
74
74
  });
75
75
 
76
76
  export type DeliveryTracking = z.infer<typeof deliveryTrackingSchema>;
77
+
78
+ /** `POST /me/order-items/{orderItemId}/purchase-decision` 응답 */
79
+ export const purchaseDecisionResultSchema = z.object({
80
+ orderItemId: z.string(),
81
+ status: z.literal("PURCHASE_DECIDED"),
82
+ decidedAt: z.string(),
83
+ });
84
+
85
+ export type PurchaseDecisionResult = z.infer<typeof purchaseDecisionResultSchema>;
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { pageSchema } from "./common";
2
3
 
3
4
  export const publicReviewSchema = z.object({
4
5
  reviewId: z.string(),
@@ -63,3 +64,18 @@ export const updateReviewRequestSchema = z
63
64
  });
64
65
 
65
66
  export type UpdateReviewRequest = z.infer<typeof updateReviewRequestSchema>;
67
+
68
+ /** `GET /products/{productId}/reviews` 응답 — 리뷰 페이지와 평점 분포 */
69
+ export const reviewPageSchema = pageSchema(publicReviewSchema).extend({
70
+ summary: reviewSummarySchema.optional(),
71
+ });
72
+
73
+ export type ReviewPage = z.infer<typeof reviewPageSchema>;
74
+
75
+ /** `PUT /me/reviews/{reviewId}` 응답 */
76
+ export const updateReviewResultSchema = z.object({
77
+ reviewId: z.string(),
78
+ updatedAt: z.string(),
79
+ });
80
+
81
+ export type UpdateReviewResult = z.infer<typeof updateReviewResultSchema>;
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
2
 
3
3
  /**
4
- * 스토어 공개 설정 — `GET /store`. 스토어프런트가 화면을 그릴 때 읽는 값만 담는다.
4
+ * 스토어 공개 설정 — `GET /store`. 스토어프론트가 화면을 그릴 때 읽는 값만 담는다.
5
5
  */
6
6
  export const storefrontStoreSchema = z.object({
7
7
  storeCode: z.string(),
@@ -14,6 +14,10 @@ export const storefrontStoreSchema = z.object({
14
14
  * 장바구니 담기는 된다 — 주문 버튼을 로그인 안내로 바꾸면 된다
15
15
  */
16
16
  guestCheckout: z.boolean(),
17
+ /**
18
+ * 켜진 로그인 방법 — 화면에 이 순서대로 버튼을 그리면 된다. `password`가 없으면 이메일·비밀번호 가입·로그인이 꺼져 있다
19
+ */
20
+ loginMethods: z.array(z.enum(["password", "kakao", "naver", "google"])),
17
21
  });
18
22
 
19
23
  export type StorefrontStore = z.infer<typeof storefrontStoreSchema>;