@sayren/store-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.
@@ -3,7 +3,7 @@ import { z } from "zod";
3
3
  /**
4
4
  * 방문 분석 조회 (관리 API `/v1/analytics/*`, 스코프 `analytics:r`).
5
5
  *
6
- * 스토어프런트가 `@sayren/storefront-sdk`로 수집한 이벤트를 서버가 날짜(Asia/Seoul)별로 집계한 값이다. 집계는 주기적으로
6
+ * 스토어프론트가 `@sayren/storefront-sdk`로 수집한 이벤트를 서버가 날짜(Asia/Seoul)별로 집계한 값이다. 집계는 주기적으로
7
7
  * 갱신되므로 오늘 값은 `lastComputedAt` 시점까지 반영돼 있다. 봇·자동화 브라우저의 세션은 모든 집계에서 빠진다.
8
8
  */
9
9
 
@@ -96,6 +96,10 @@ export const analyticsOverviewSchema = z.object({
96
96
  rows: z
97
97
  .array(analyticsTotalsSchema.extend({ date: z.string().describe("일자 (YYYY-MM-DD)") }))
98
98
  .describe("날짜별 값. 세션이 없는 날은 생략된다"),
99
+ excludedBotSessions: z
100
+ .number()
101
+ .int()
102
+ .describe("집계에서 뺀 봇 세션 수 — 봇 표시 이벤트가 하나라도 있는 세션은 모든 값에서 빠진다"),
99
103
  });
100
104
  export type AnalyticsOverview = z.infer<typeof analyticsOverviewSchema>;
101
105
 
@@ -132,6 +136,9 @@ export const analyticsProductRowSchema = z.object({
132
136
  "이 상품의 구매 금액 — 주문 상품 금액(할인 반영, 배송비 제외) 합계. 이후 취소·환불은 빼지 않는다",
133
137
  ),
134
138
  purchaseRate: rate("구매율 = 구매 / 조회 세션. 조회 세션이 없으면 null"),
139
+ clickThroughRate: rate(
140
+ "목록 → 상세 전환율 = 상세 조회 / 목록 노출. 노출이 없으면 null(검색·외부 링크로 바로 들어온 조회는 노출 없이 잡힌다)",
141
+ ),
135
142
  });
136
143
  export type AnalyticsProductRow = z.infer<typeof analyticsProductRowSchema>;
137
144
 
@@ -198,3 +205,141 @@ export const analyticsSourcesSchema = z.object({
198
205
  rows: z.array(analyticsSourceRowSchema).describe("세션 수 내림차순"),
199
206
  });
200
207
  export type AnalyticsSources = z.infer<typeof analyticsSourcesSchema>;
208
+
209
+ // ─── 2차(#6): 실시간·기기별 퍼널·첫/이탈 페이지·검색어·결제 단계 이탈 ─────────────────────
210
+
211
+ export const analyticsRealtimeSchema = z.object({
212
+ asOf: z.string().describe("기준 시각(ISO 8601)"),
213
+ windowMinutes: z.literal(5).describe("최근 몇 분 동안 활동한 방문을 세는가"),
214
+ visitors: z.number().int().describe("최근 5분 안에 이벤트가 있는 방문자 수(봇 제외)"),
215
+ sessions: z.number().int(),
216
+ pageViews: z.number().int(),
217
+ pages: z
218
+ .array(z.object({ path: z.string(), visitors: z.number().int(), pageViews: z.number().int() }))
219
+ .describe("최근 5분 페이지뷰가 있는 경로. 방문자 수 내림차순 최대 10개"),
220
+ });
221
+ export type AnalyticsRealtime = z.infer<typeof analyticsRealtimeSchema>;
222
+
223
+ export const analyticsDeviceRowSchema = z.object({
224
+ device: z.enum(["desktop", "mobile", "tablet", "unknown"]),
225
+ sessions: z.number().int(),
226
+ productViewSessions: z.number().int(),
227
+ addToCartSessions: z.number().int(),
228
+ beginCheckoutSessions: z.number().int(),
229
+ purchaseSessions: z.number().int(),
230
+ conversionRate: rate("전환율 = 구매 세션 / 세션"),
231
+ });
232
+ export type AnalyticsDeviceRow = z.infer<typeof analyticsDeviceRowSchema>;
233
+
234
+ export const analyticsDevicesSchema = z.object({
235
+ ...rangeMeta,
236
+ rows: z.array(analyticsDeviceRowSchema).describe("기기별 세션 기준 퍼널. 세션 수 내림차순"),
237
+ });
238
+ export type AnalyticsDevices = z.infer<typeof analyticsDevicesSchema>;
239
+
240
+ const limitParam = z.coerce
241
+ .number()
242
+ .int()
243
+ .min(1)
244
+ .max(100)
245
+ .optional()
246
+ .describe("최대 행 수. 기본 20, 최대 100");
247
+
248
+ export const analyticsPagesQuerySchema = analyticsRangeQuerySchema.extend({ limit: limitParam });
249
+ export type AnalyticsPagesParams = z.infer<typeof analyticsPagesQuerySchema>;
250
+
251
+ export const analyticsPagesSchema = z.object({
252
+ ...rangeMeta,
253
+ landings: z
254
+ .array(
255
+ z.object({
256
+ path: z.string(),
257
+ entrances: z.number().int().describe("이 페이지로 시작한 세션 수"),
258
+ bounces: z.number().int().describe("이 페이지로 시작해 참여하지 않고 끝난 세션 수"),
259
+ bounceRate: rate("반송률 = 반송 / 진입"),
260
+ }),
261
+ )
262
+ .describe("첫 페이지. 진입 수 내림차순"),
263
+ exits: z
264
+ .array(
265
+ z.object({
266
+ path: z.string(),
267
+ pageViews: z.number().int(),
268
+ exits: z.number().int().describe("이 페이지가 세션의 마지막 페이지뷰였던 수"),
269
+ exitRate: rate("이탈률 = 이탈 / 페이지뷰"),
270
+ }),
271
+ )
272
+ .describe("이탈 페이지. 이탈 수 내림차순"),
273
+ });
274
+ export type AnalyticsPages = z.infer<typeof analyticsPagesSchema>;
275
+
276
+ export const analyticsSearchQuerySchema = analyticsRangeQuerySchema.extend({ limit: limitParam });
277
+ export type AnalyticsSearchParams = z.infer<typeof analyticsSearchQuerySchema>;
278
+
279
+ export const analyticsSearchSchema = z.object({
280
+ ...rangeMeta,
281
+ summary: z.object({
282
+ searches: z.number().int(),
283
+ zeroResultSearches: z.number().int().describe("결과가 0개였던 검색 수"),
284
+ searchSessions: z.number().int().describe("검색한 세션 수(세션당 한 번)"),
285
+ purchaseSessions: z
286
+ .number()
287
+ .int()
288
+ .describe(
289
+ "검색한 뒤 같은 날 구매한 세션 수(세션당 한 번). 검색 전에 끝난 구매는 세지 않는다",
290
+ ),
291
+ conversionRate: rate("검색 후 구매율 = 구매 세션 / 검색 세션"),
292
+ }),
293
+ terms: z
294
+ .array(
295
+ z.object({
296
+ term: z.string().describe("검색어 — 소문자, 공백 정리"),
297
+ searches: z.number().int(),
298
+ zeroResultSearches: z.number().int(),
299
+ searchSessions: z.number().int().describe("이 검색어로 검색한 세션 수"),
300
+ purchaseSessions: z.number().int().describe("이 검색어로 검색한 뒤 같은 날 구매한 세션 수"),
301
+ conversionRate: rate("검색 후 구매율 = 구매 세션 / 검색 세션"),
302
+ }),
303
+ )
304
+ .describe(
305
+ "검색 수 내림차순. 세션 수는 검색어마다 세므로 여러 검색어를 쓴 세션은 검색어마다 잡힌다(더하면 요약보다 크다)",
306
+ ),
307
+ zeroResultTerms: z
308
+ .array(
309
+ z.object({
310
+ term: z.string(),
311
+ searches: z.number().int(),
312
+ zeroResultSearches: z.number().int(),
313
+ }),
314
+ )
315
+ .describe(
316
+ "결과가 0개였던 적이 있는 검색어. 결과 없음 수 내림차순 — 검색 수 상위(`terms`)에 들지 않는 드문 검색어도 여기 잡힌다",
317
+ ),
318
+ });
319
+ export type AnalyticsSearch = z.infer<typeof analyticsSearchSchema>;
320
+
321
+ export const analyticsCheckoutOutcomeSchema = z
322
+ .enum([
323
+ "COMPLETED",
324
+ "NOT_OPENED",
325
+ "USER_CANCELED",
326
+ "DECLINED",
327
+ "PREPARE_FAILURE",
328
+ "OTHER",
329
+ "IN_PROGRESS",
330
+ ])
331
+ .describe(
332
+ "결제 세션의 최종 결과 — `COMPLETED`(결제 완료), `NOT_OPENED`(결제창을 열지 않고 끝남), `USER_CANCELED`(결제창에서 취소), " +
333
+ "`DECLINED`(카드 거절 등 승인 거절), `PREPARE_FAILURE`(결제창을 띄우기 전 장애), `OTHER`(그 밖의 이탈·만료), " +
334
+ "`IN_PROGRESS`(아직 진행 중 — 만료 전 대기·승인 중). 실패 분류는 세션의 마지막 결제 시도 기준이다",
335
+ );
336
+ export type AnalyticsCheckoutOutcome = z.infer<typeof analyticsCheckoutOutcomeSchema>;
337
+
338
+ export const analyticsCheckoutSchema = z.object({
339
+ ...rangeMeta,
340
+ sessions: z.number().int().describe("기간 안에 시작한 결제 세션 수(결제 요청 단위)"),
341
+ outcomes: z
342
+ .array(z.object({ outcome: analyticsCheckoutOutcomeSchema, sessions: z.number().int() }))
343
+ .describe("결과별 결제 세션 수. 결과마다 한 행"),
344
+ });
345
+ export type AnalyticsCheckout = z.infer<typeof analyticsCheckoutSchema>;
@@ -53,13 +53,18 @@ export const claimSchema = z.object({
53
53
  .nullable(),
54
54
  requestedAt: z.string(),
55
55
  completedAt: z.string().nullable(),
56
+ /** 보류 중이면 자동 해제 시각(ISO 8601), 아니면 null */
57
+ holdExpiresAt: z.string().nullable(),
58
+ /** 보류를 이미 썼는지 — 보류는 클레임당 1회라 해제·만료 뒤에도 true */
59
+ holdUsed: z.boolean(),
56
60
  });
57
61
 
58
62
  export type Claim = z.infer<typeof claimSchema>;
59
63
 
60
64
  export interface ClaimListParams extends PageParams {
61
65
  type?: ClaimType;
62
- status?: ClaimStatus;
66
+ /** 여러 상태를 한 번에 거를 수 있다(처리 대기 목록 등) */
67
+ status?: ClaimStatus | ClaimStatus[];
63
68
  orderItemId?: string;
64
69
  requestedFrom?: string;
65
70
  requestedTo?: string;
@@ -68,7 +73,10 @@ export interface ClaimListParams extends PageParams {
68
73
  /** `GET /v1/claims` 쿼리 — OpenAPI 문서용 형태(값은 쿼리 문자열) */
69
74
  export const claimListQuerySchema = z.object({
70
75
  type: claimTypeSchema.optional().describe("클레임 유형 필터"),
71
- status: claimStatusSchema.optional().describe("클레임 상태 필터"),
76
+ status: z
77
+ .string()
78
+ .optional()
79
+ .describe("클레임 상태 필터. 쉼표로 여러 개를 줄 수 있다 (예: `REQUESTED,HELD,COLLECTED`)"),
72
80
  orderItemId: z.string().optional().describe("주문상품 id 필터"),
73
81
  requestedFrom: z.string().optional().describe("요청 시각 조회 시작 (ISO 8601, 포함)"),
74
82
  requestedTo: z.string().optional().describe("요청 시각 조회 끝 (ISO 8601, 포함)"),
@@ -115,7 +115,7 @@ export const withdrawCustomerRequestSchema = z.object({
115
115
  });
116
116
  export type WithdrawCustomerRequest = z.infer<typeof withdrawCustomerRequestSchema>;
117
117
 
118
- /** `WITHDRAWAL_BLOCKED`의 `error.details` — 탈퇴를 막는 항목 수(스토어프런트 탈퇴와 같다) */
118
+ /** `WITHDRAWAL_BLOCKED`의 `error.details` — 탈퇴를 막는 항목 수(스토어프론트 탈퇴와 같다) */
119
119
  export const customerWithdrawalBlockedDetailsSchema = z.object({
120
120
  inProgressOrderItems: z.number().int(),
121
121
  openClaims: z.number().int(),
@@ -8,7 +8,7 @@ export type LoginProvider = z.infer<typeof loginProviderSchema>;
8
8
  export const loginProviderStatusSchema = z.object({
9
9
  provider: loginProviderSchema,
10
10
  connected: z.boolean(),
11
- /** 스토어프런트에서 사용 중인지. 연결 안 됐으면 false */
11
+ /** 스토어프론트에서 사용 중인지. 연결 안 됐으면 false */
12
12
  enabled: z.boolean(),
13
13
  clientIdPrefix: z.string().nullable().describe("클라이언트 ID(카카오 REST API 키) 앞 4자리"),
14
14
  secretPrefix: z.string().nullable(),
@@ -13,10 +13,14 @@ export const productSummarySchema = z.object({
13
13
  productId: z.string(),
14
14
  name: z.string(),
15
15
  status: productStatusSchema,
16
- salePrice: z.number().int(),
17
- discountedPrice: z.number().int().nullable(),
16
+ salePrice: z.number().int().describe("판매가. 원(KRW) 단위 정수"),
17
+ discountedPrice: z
18
+ .number()
19
+ .int()
20
+ .nullable()
21
+ .describe("즉시 할인 적용가. 원 단위. 할인이 없으면 null"),
18
22
  /** usable variant 재고 합계 (서버 계산) */
19
- stockQuantity: z.number().int(),
23
+ stockQuantity: z.number().int().describe("판매 가능한 옵션 재고의 합계. 서버가 계산한다"),
20
24
  categoryId: z.string().nullable(),
21
25
  thumbnailUrl: z.url().optional(),
22
26
  /** optionGroups.length > 0 */
@@ -199,8 +203,9 @@ export const productCreateRequestSchema = z
199
203
  .number()
200
204
  .int()
201
205
  .min(10)
202
- .multipleOf(10, "판매가는 10원 단위여야 합니다 (INVALID_PRICE_UNIT)"),
203
- originalPrice: z.number().int().optional(),
206
+ .multipleOf(10, "판매가는 10원 단위여야 합니다 (INVALID_PRICE_UNIT)")
207
+ .describe("판매가. 원(KRW) 단위 정수, 최소 10원, 10원 단위"),
208
+ originalPrice: z.number().int().optional().describe("정가(할인 전 표시 가격). 원 단위"),
204
209
  description: z.string().optional(),
205
210
  /**
206
211
  * 상세설명 에디터(@avarlabs/editor)의 직렬화 상태. 재편집 때 이 값으로 문서를 복원한다.
@@ -209,11 +214,17 @@ export const productCreateRequestSchema = z
209
214
  descriptionJson: z.string().nullable().optional(),
210
215
  images: z.array(z.url()).min(1).max(10).optional(),
211
216
  attributes: z.array(z.object({ attributeId: z.string(), value: z.string() })).optional(),
212
- deliveryType: deliveryTypeSchema.default("PAID"),
213
- deliveryFee: z.number().int().default(0),
214
- conditionalFreeAmount: z.number().int().optional(),
215
- returnFee: z.number().int().default(0),
216
- exchangeFee: z.number().int().default(0),
217
+ deliveryType: deliveryTypeSchema
218
+ .default("PAID")
219
+ .describe("배송비 유형. FREE 무료, PAID 유료, CONDITIONAL_FREE 조건부 무료"),
220
+ deliveryFee: z.number().int().default(0).describe("배송비. 원 단위"),
221
+ conditionalFreeAmount: z
222
+ .number()
223
+ .int()
224
+ .optional()
225
+ .describe("CONDITIONAL_FREE일 때 무료 배송이 되는 주문 금액. 원 단위"),
226
+ returnFee: z.number().int().default(0).describe("반품 배송비. 원 단위"),
227
+ exchangeFee: z.number().int().default(0).describe("교환 배송비. 원 단위"),
217
228
  shippingAddressId: z.string().optional(),
218
229
  returnAddressId: z.string().optional(),
219
230
  })
@@ -91,13 +91,13 @@ export function isWebOrigin(value: string): boolean {
91
91
  }
92
92
  }
93
93
 
94
- /** 방문 분석을 보낼 수 있는 스토어프런트 오리진 — 스킴·호스트(·포트)만. 서버가 소문자·끝 슬래시 제거로 정규화한다 */
94
+ /** 방문 분석을 보낼 수 있는 스토어프론트 오리진 — 스킴·호스트(·포트)만. 서버가 소문자·끝 슬래시 제거로 정규화한다 */
95
95
  export const analyticsOriginSchema = z
96
96
  .string()
97
97
  .trim()
98
98
  .max(255)
99
99
  .refine(isWebOrigin, "https://shop.example.com 형식(경로 없이)으로 입력해 주세요")
100
- .describe("스토어프런트 오리진. 예: `https://shop.example.com`");
100
+ .describe("스토어프론트 오리진. 예: `https://shop.example.com`");
101
101
 
102
102
  /** 허용 오리진 상한 */
103
103
  export const MAX_ANALYTICS_ALLOWED_ORIGINS = 20;
@@ -114,7 +114,7 @@ export const updateStoreRequestSchema = z
114
114
  .max(MAX_ANALYTICS_ALLOWED_ORIGINS)
115
115
  .optional()
116
116
  .describe(
117
- "@deprecated `storefrontOrigins`를 쓴다(같은 목록). 방문 분석 수집을 받을 스토어프런트 오리진(최대 20개). 비우면 모든 오리진에서 받는다. " +
117
+ "@deprecated `storefrontOrigins`를 쓴다(같은 목록). 방문 분석 수집을 받을 스토어프론트 오리진(최대 20개). 비우면 모든 오리진에서 받는다. " +
118
118
  "넣으면 목록에 없는 오리진의 수집 요청은 403 `ORIGIN_NOT_ALLOWED`이고, 페이지 URL이 목록 밖인 이벤트는 버린다",
119
119
  ),
120
120
  storefrontOrigins: z
@@ -122,7 +122,7 @@ export const updateStoreRequestSchema = z
122
122
  .max(MAX_ANALYTICS_ALLOWED_ORIGINS)
123
123
  .optional()
124
124
  .describe(
125
- "스토어프런트 도메인(최대 20개) — 소셜 로그인 뒤 돌아갈 주소와 방문 분석 수집을 함께 제한한다. " +
125
+ "스토어프론트 도메인(최대 20개) — 소셜 로그인 뒤 돌아갈 주소와 방문 분석 수집을 함께 제한한다. " +
126
126
  "`analyticsAllowedOrigins`와 같은 목록이다(둘 다 주면 이 값이 이긴다)",
127
127
  ),
128
128
  guestCheckoutEnabled: z
@@ -149,11 +149,11 @@ export const storeSchema = z.object({
149
149
  customerCenterPhone: z.string().nullable(),
150
150
  analyticsAllowedOrigins: z
151
151
  .array(z.string())
152
- .describe("방문 분석 수집을 받는 스토어프런트 오리진. 비어 있으면 모든 오리진에서 받는다"),
152
+ .describe("방문 분석 수집을 받는 스토어프론트 오리진. 비어 있으면 모든 오리진에서 받는다"),
153
153
  storefrontOrigins: z
154
154
  .array(z.string())
155
155
  .describe(
156
- "스토어프런트 도메인 — 소셜 로그인 리디렉트와 방문 분석 수집이 함께 쓴다(`analyticsAllowedOrigins`와 같은 값)",
156
+ "스토어프론트 도메인 — 소셜 로그인 리디렉트와 방문 분석 수집이 함께 쓴다(`analyticsAllowedOrigins`와 같은 값)",
157
157
  ),
158
158
  guestCheckoutEnabled: z.boolean().describe("비회원 주문 허용. false면 회원만 주문할 수 있다"),
159
159
  memberCount: z.number().int(),
package/src/server.ts CHANGED
@@ -3,6 +3,12 @@
3
3
  * 브라우저 번들에서 import하지 않는다.
4
4
  */
5
5
 
6
+ export {
7
+ type DelegatedMemberToken,
8
+ delegateMemberByEmail,
9
+ delegateMemberByUid,
10
+ MemberDelegationError,
11
+ } from "./member-delegation";
6
12
  export {
7
13
  WEBHOOK_DEFAULT_TOLERANCE_SEC,
8
14
  WEBHOOK_DELIVERY_ID_HEADER,