@sayren/store-sdk 0.1.0 → 0.2.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/src/client.ts CHANGED
@@ -1,6 +1,17 @@
1
1
  import { z } from "zod";
2
2
  import { base64Utf8 } from "./base64";
3
3
  import { createHttp, path, type Query, resolveToken, type TokenSource, unwrapData } from "./http";
4
+ import {
5
+ type AnalyticsProductsParams,
6
+ type AnalyticsRangeParams,
7
+ type AnalyticsSourcesParams,
8
+ analyticsOverviewSchema,
9
+ analyticsProductsQuerySchema,
10
+ analyticsProductsSchema,
11
+ analyticsRangeQuerySchema,
12
+ analyticsSourcesQuerySchema,
13
+ analyticsSourcesSchema,
14
+ } from "./schemas/analytics";
4
15
  import {
5
16
  type ApiClientListParams,
6
17
  apiClientSchema,
@@ -34,8 +45,9 @@ import {
34
45
  import {
35
46
  approveClaimResultSchema,
36
47
  type ClaimListParams,
48
+ claimDetailSchema,
49
+ claimHoldResultSchema,
37
50
  claimSchema,
38
- claimStatusSchema,
39
51
  type RejectClaimRequest,
40
52
  rejectClaimRequestSchema,
41
53
  } from "./schemas/claim";
@@ -47,8 +59,8 @@ import {
47
59
  reviewSchema,
48
60
  type SettlementListParams,
49
61
  sellerCustomerInquirySchema,
50
- settlementDetailSchema,
51
62
  settlementSummarySchema,
63
+ settlementWithDetailsSchema,
52
64
  type WebhookDeliveryListParams,
53
65
  type WebhookRequest,
54
66
  type WebhookTestRequest,
@@ -64,9 +76,11 @@ import {
64
76
  import {
65
77
  type CancelBySellerRequest,
66
78
  cancelBySellerRequestSchema,
79
+ cancelBySellerResultSchema,
67
80
  carrierSchema,
68
81
  type DispatchRequest,
69
82
  deliveryInfoSchema,
83
+ deliveryStatusSchema,
70
84
  dispatchRequestSchema,
71
85
  type HoldOrderItemRequest,
72
86
  holdOrderItemRequestSchema,
@@ -75,6 +89,7 @@ import {
75
89
  orderItemSchema,
76
90
  orderSchema,
77
91
  orderSummarySchema,
92
+ purchaseDecisionExtendResultSchema,
78
93
  } from "./schemas/order";
79
94
  import {
80
95
  type PaymentListParams,
@@ -459,11 +474,7 @@ export function createStoreClient(options: StoreClientOptions) {
459
474
  storeHttp.request(
460
475
  "POST",
461
476
  path`/order-items/${orderItemId}/cancel`,
462
- z.object({
463
- orderItemId: z.string(),
464
- refundAmount: z.number().int(),
465
- refundedAt: z.string(),
466
- }),
477
+ cancelBySellerResultSchema,
467
478
  { body: cancelBySellerRequestSchema.parse(body), headers: idempotency(idempotencyKey) },
468
479
  ),
469
480
  updateMemo: (orderItemId: string, memo: string) =>
@@ -474,11 +485,7 @@ export function createStoreClient(options: StoreClientOptions) {
474
485
  storeHttp.request(
475
486
  "POST",
476
487
  path`/order-items/${orderItemId}/purchase-decision/extend`,
477
- z.object({
478
- orderItemId: z.string(),
479
- autoDecisionDate: z.string(),
480
- extendCount: z.number().int().max(2),
481
- }),
488
+ purchaseDecisionExtendResultSchema,
482
489
  ),
483
490
  dispatch: (orderItemId: string, body: DispatchRequest, idempotencyKey?: string) =>
484
491
  storeHttp.request("POST", path`/order-items/${orderItemId}/dispatch`, deliveryInfoSchema, {
@@ -498,15 +505,7 @@ export function createStoreClient(options: StoreClientOptions) {
498
505
  body: { carrierCode, trackingNumber },
499
506
  }),
500
507
  getDelivery: (orderItemId: string) =>
501
- storeHttp.request(
502
- "GET",
503
- path`/order-items/${orderItemId}/delivery`,
504
- deliveryInfoSchema.extend({
505
- trackingEvents: z
506
- .array(z.object({ status: z.string(), location: z.string(), occurredAt: z.string() }))
507
- .optional(),
508
- }),
509
- ),
508
+ storeHttp.request("GET", path`/order-items/${orderItemId}/delivery`, deliveryStatusSchema),
510
509
  },
511
510
 
512
511
  carriers: {
@@ -517,21 +516,7 @@ export function createStoreClient(options: StoreClientOptions) {
517
516
  list: (params?: ClaimListParams) =>
518
517
  storeHttp.request("GET", "/claims", pageSchema(claimSchema), { query: params as Query }),
519
518
  get: (claimId: string) =>
520
- storeHttp.request(
521
- "GET",
522
- path`/claims/${claimId}`,
523
- claimSchema.extend({
524
- histories: z
525
- .array(
526
- z.object({
527
- status: claimStatusSchema,
528
- actor: z.enum(["BUYER", "SELLER", "SYSTEM"]),
529
- occurredAt: z.string(),
530
- }),
531
- )
532
- .optional(),
533
- }),
534
- ),
519
+ storeHttp.request("GET", path`/claims/${claimId}`, claimDetailSchema),
535
520
  approve: (claimId: string, deductAmount?: number, idempotencyKey?: string) =>
536
521
  storeHttp.request("POST", path`/claims/${claimId}/approve`, approveClaimResultSchema, {
537
522
  body: deductAmount !== undefined ? { deductAmount } : {},
@@ -552,12 +537,9 @@ export function createStoreClient(options: StoreClientOptions) {
552
537
  body: { carrierCode, trackingNumber },
553
538
  }),
554
539
  hold: (claimId: string, reason: string) =>
555
- storeHttp.request(
556
- "POST",
557
- path`/claims/${claimId}/hold`,
558
- z.object({ claimId: z.string(), holdExpiresAt: z.string() }),
559
- { body: { reason } },
560
- ),
540
+ storeHttp.request("POST", path`/claims/${claimId}/hold`, claimHoldResultSchema, {
541
+ body: { reason },
542
+ }),
561
543
  releaseHold: (claimId: string) =>
562
544
  storeHttp.requestVoid("DELETE", path`/claims/${claimId}/hold`),
563
545
  },
@@ -614,12 +596,9 @@ export function createStoreClient(options: StoreClientOptions) {
614
596
  }),
615
597
  /** @deprecated `salesReport.get`을 쓴다. 기존 정산 레코드 조회만 유지된다 */
616
598
  get: (settlementId: string, params?: PageParams) =>
617
- storeHttp.request(
618
- "GET",
619
- path`/settlements/${settlementId}`,
620
- settlementSummarySchema.extend({ details: z.array(settlementDetailSchema).optional() }),
621
- { query: params as Query },
622
- ),
599
+ storeHttp.request("GET", path`/settlements/${settlementId}`, settlementWithDetailsSchema, {
600
+ query: params as Query,
601
+ }),
623
602
  },
624
603
 
625
604
  /**
@@ -638,6 +617,29 @@ export function createStoreClient(options: StoreClientOptions) {
638
617
  }),
639
618
  },
640
619
 
620
+ /**
621
+ * 방문 분석 — 스코프 `analytics:r`. 스토어프런트가 `@sayren/storefront-sdk`로 수집한 이벤트를 Asia/Seoul 날짜별로
622
+ * 집계한 값이다. 집계는 주기적으로 갱신되고 응답 `lastComputedAt`이 마지막 집계 시각이다. 봇 세션은 빠진다.
623
+ * 기간은 양끝 포함 최대 92일이고, 테스트 결제 구매는 `includeTest: "true"`일 때만 포함한다.
624
+ */
625
+ analytics: {
626
+ /** `GET /v1/analytics/overview` — 합계·세션 퍼널·날짜별 값 */
627
+ overview: (params: AnalyticsRangeParams) =>
628
+ storeHttp.request("GET", "/analytics/overview", analyticsOverviewSchema, {
629
+ query: analyticsRangeQuerySchema.parse(params),
630
+ }),
631
+ /** `GET /v1/analytics/products` — 상품별 노출·조회·장바구니·구매 */
632
+ products: (params: AnalyticsProductsParams) =>
633
+ storeHttp.request("GET", "/analytics/products", analyticsProductsSchema, {
634
+ query: analyticsProductsQuerySchema.parse(params) as unknown as Query,
635
+ }),
636
+ /** `GET /v1/analytics/sources` — 채널·소스·캠페인·기기별 세션과 구매 */
637
+ sources: (params: AnalyticsSourcesParams) =>
638
+ storeHttp.request("GET", "/analytics/sources", analyticsSourcesSchema, {
639
+ query: analyticsSourcesQuerySchema.parse(params),
640
+ }),
641
+ },
642
+
641
643
  /**
642
644
  * 결제 내역 — 스코프 `settlement:r`. 결제 건(세션) 단위이고 주문 목록과 다르다. 주문이 만들어지지 않은
643
645
  * 결제(카드 거절·결제창 이탈·승인 결과 확인 중)도 그대로 나온다.
package/src/index.ts CHANGED
@@ -11,6 +11,7 @@ export {
11
11
  oauthTokenResponseSchema,
12
12
  refreshUserToken,
13
13
  } from "./oauth";
14
+ export * from "./schemas/analytics";
14
15
  export * from "./schemas/api-client";
15
16
  export * from "./schemas/api-log";
16
17
  export * from "./schemas/auth";
@@ -0,0 +1,200 @@
1
+ import { z } from "zod";
2
+
3
+ /**
4
+ * 방문 분석 조회 (관리 API `/v1/analytics/*`, 스코프 `analytics:r`).
5
+ *
6
+ * 스토어프런트가 `@sayren/storefront-sdk`로 수집한 이벤트를 서버가 날짜(Asia/Seoul)별로 집계한 값이다. 집계는 주기적으로
7
+ * 갱신되므로 오늘 값은 `lastComputedAt` 시점까지 반영돼 있다. 봇·자동화 브라우저의 세션은 모든 집계에서 빠진다.
8
+ */
9
+
10
+ const reportDate = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "날짜 형식은 YYYY-MM-DD 입니다");
11
+
12
+ const includeTest = z
13
+ .enum(["true", "false"])
14
+ .optional()
15
+ .describe("테스트 결제(샌드박스 결제)로 만든 구매 포함 여부. 기본 `false`");
16
+
17
+ export const analyticsRangeQuerySchema = z.object({
18
+ from: reportDate.describe("시작일 (YYYY-MM-DD, Asia/Seoul 기준, 포함)"),
19
+ to: reportDate.describe("종료일 (YYYY-MM-DD, 포함). from부터 최대 92일"),
20
+ includeTest,
21
+ });
22
+ export type AnalyticsRangeParams = z.infer<typeof analyticsRangeQuerySchema>;
23
+
24
+ const rangeMeta = {
25
+ from: z.string(),
26
+ to: z.string(),
27
+ timezone: z.literal("Asia/Seoul"),
28
+ currency: z.literal("KRW"),
29
+ includeTest: z.boolean().describe("테스트 결제 구매를 포함했는가"),
30
+ lastComputedAt: z
31
+ .string()
32
+ .nullable()
33
+ .describe("기간 안에서 가장 최근에 집계한 시각(ISO 8601). 집계된 날이 없으면 null"),
34
+ };
35
+
36
+ /**
37
+ * 비율(0 이상). 단계가 세션 안에서 꼭 이어지지는 않아 1을 넘을 수 있다 — 예: 조회 없이 장바구니에서 바로 산 구매,
38
+ * 자정을 넘겨 다음 날 확정된 구매.
39
+ */
40
+ const rate = (description: string) => z.number().min(0).nullable().describe(description);
41
+
42
+ export const analyticsTotalsSchema = z.object({
43
+ visitors: z
44
+ .number()
45
+ .int()
46
+ .describe("방문자 수 — 날짜별 순방문자의 합. 여러 날 방문한 사람은 날마다 센다"),
47
+ sessions: z
48
+ .number()
49
+ .int()
50
+ .describe("세션 수. 30분 동안 활동이 없거나 다른 캠페인으로 들어오면 새 세션"),
51
+ engagedSessions: z
52
+ .number()
53
+ .int()
54
+ .describe(
55
+ "참여 세션 수 — 페이지뷰 2개 이상, 체류 10초 이상, 장바구니 담기·결제 시작·구매 중 하나",
56
+ ),
57
+ pageViews: z.number().int(),
58
+ engagementSeconds: z
59
+ .number()
60
+ .int()
61
+ .describe(
62
+ "페이지가 화면에 보인 시간의 합(초). 페이지뷰 하나는 최대 30분, 세션은 세션 길이 + 30분까지 센다",
63
+ ),
64
+ productViews: z.number().int().describe("상품 조회 수"),
65
+ productViewSessions: z.number().int().describe("상품을 조회한 세션 수"),
66
+ addToCartSessions: z.number().int().describe("장바구니에 담은 세션 수"),
67
+ beginCheckoutSessions: z.number().int().describe("주문서를 연 세션 수"),
68
+ purchaseSessions: z.number().int().describe("구매한 세션 수"),
69
+ purchases: z.number().int().describe("구매(주문) 수"),
70
+ revenue: z
71
+ .number()
72
+ .int()
73
+ .describe(
74
+ "구매 금액 합계 — 주문 결제 금액(배송비 포함). 이후 취소·환불은 빼지 않으므로 매출 리포트의 순매출과 다르다",
75
+ ),
76
+ engagementRate: rate("참여율 = 참여 세션 / 세션. 세션이 없으면 null"),
77
+ conversionRate: rate("전환율 = 구매 세션 / 세션. 세션이 없으면 null"),
78
+ });
79
+ export type AnalyticsTotals = z.infer<typeof analyticsTotalsSchema>;
80
+
81
+ export const analyticsFunnelStepSchema = z.object({
82
+ step: z.enum(["SESSION", "PRODUCT_VIEW", "ADD_TO_CART", "BEGIN_CHECKOUT", "PURCHASE"]),
83
+ sessions: z.number().int(),
84
+ rateFromPrevious: rate("직전 단계 대비 비율. 첫 단계이거나 직전 단계가 0이면 null"),
85
+ });
86
+ export type AnalyticsFunnelStep = z.infer<typeof analyticsFunnelStepSchema>;
87
+
88
+ export const analyticsOverviewSchema = z.object({
89
+ ...rangeMeta,
90
+ summary: analyticsTotalsSchema.describe("기간 합계"),
91
+ funnel: z
92
+ .array(analyticsFunnelStepSchema)
93
+ .describe(
94
+ "세션 기준 구매 퍼널. 각 단계는 그 행동을 한 세션 수이고, 앞 단계를 거쳤는지는 따지지 않는다",
95
+ ),
96
+ rows: z
97
+ .array(analyticsTotalsSchema.extend({ date: z.string().describe("일자 (YYYY-MM-DD)") }))
98
+ .describe("날짜별 값. 세션이 없는 날은 생략된다"),
99
+ });
100
+ export type AnalyticsOverview = z.infer<typeof analyticsOverviewSchema>;
101
+
102
+ export const analyticsProductSortSchema = z
103
+ .enum(["VIEWS", "ADD_TO_CARTS", "PURCHASES", "REVENUE", "IMPRESSIONS"])
104
+ .describe("정렬 기준(내림차순). 기본 `VIEWS`");
105
+ export type AnalyticsProductSort = z.infer<typeof analyticsProductSortSchema>;
106
+
107
+ export const analyticsProductsQuerySchema = analyticsRangeQuerySchema.extend({
108
+ sort: analyticsProductSortSchema.optional(),
109
+ limit: z.coerce
110
+ .number()
111
+ .int()
112
+ .min(1)
113
+ .max(100)
114
+ .optional()
115
+ .describe("최대 행 수. 기본 20, 최대 100"),
116
+ });
117
+ export type AnalyticsProductsParams = z.infer<typeof analyticsProductsQuerySchema>;
118
+
119
+ export const analyticsProductRowSchema = z.object({
120
+ productId: z.string(),
121
+ productName: z.string().nullable().describe("현재 상품명. 삭제된 상품이면 null"),
122
+ impressions: z.number().int().describe("목록 노출 수"),
123
+ views: z.number().int().describe("상세 조회 수"),
124
+ viewSessions: z.number().int().describe("상세를 조회한 세션 수"),
125
+ addToCarts: z.number().int().describe("장바구니 담기 수"),
126
+ purchases: z.number().int().describe("이 상품이 포함된 구매(주문) 수"),
127
+ quantity: z.number().int().describe("구매 수량"),
128
+ revenue: z
129
+ .number()
130
+ .int()
131
+ .describe(
132
+ "이 상품의 구매 금액 — 주문 상품 금액(할인 반영, 배송비 제외) 합계. 이후 취소·환불은 빼지 않는다",
133
+ ),
134
+ purchaseRate: rate("구매율 = 구매 / 조회 세션. 조회 세션이 없으면 null"),
135
+ });
136
+ export type AnalyticsProductRow = z.infer<typeof analyticsProductRowSchema>;
137
+
138
+ export const analyticsProductsSchema = z.object({
139
+ ...rangeMeta,
140
+ sort: analyticsProductSortSchema,
141
+ rows: z.array(analyticsProductRowSchema),
142
+ });
143
+ export type AnalyticsProducts = z.infer<typeof analyticsProductsSchema>;
144
+
145
+ export const analyticsChannelSchema = z
146
+ .enum(["DIRECT", "ORGANIC_SEARCH", "SOCIAL", "PAID", "EMAIL", "CAMPAIGN", "REFERRAL", "UNKNOWN"])
147
+ .describe(
148
+ "유입 채널 — 세션 첫 페이지뷰의 utm·리퍼러로 정한다. " +
149
+ "`PAID`(utm_medium이 cpc·ppc·paid·display 등), `EMAIL`(utm_medium email), `SOCIAL`(SNS 리퍼러·utm_source), " +
150
+ "`ORGANIC_SEARCH`(검색 엔진 리퍼러), `CAMPAIGN`(그 밖의 utm), `REFERRAL`(그 밖의 리퍼러), `DIRECT`(없음), " +
151
+ "`UNKNOWN`(페이지뷰 없이 서버 이벤트만 있는 세션)",
152
+ );
153
+ export type AnalyticsChannel = z.infer<typeof analyticsChannelSchema>;
154
+
155
+ export const analyticsSourceGroupBySchema = z
156
+ .enum(["CHANNEL", "SOURCE", "CAMPAIGN", "DEVICE"])
157
+ .describe("행 집계 기준. 기본 `CHANNEL`");
158
+ export type AnalyticsSourceGroupBy = z.infer<typeof analyticsSourceGroupBySchema>;
159
+
160
+ export const analyticsSourcesQuerySchema = analyticsRangeQuerySchema.extend({
161
+ groupBy: analyticsSourceGroupBySchema.optional(),
162
+ });
163
+ export type AnalyticsSourcesParams = z.infer<typeof analyticsSourcesQuerySchema>;
164
+
165
+ export const analyticsSourceRowSchema = z.object({
166
+ channel: analyticsChannelSchema.nullable().describe("groupBy=CHANNEL일 때만 값"),
167
+ source: z
168
+ .string()
169
+ .nullable()
170
+ .describe(
171
+ "groupBy=SOURCE·CAMPAIGN일 때 값. utm_source, 없으면 리퍼러 호스트, 둘 다 없으면 `(direct)`",
172
+ ),
173
+ medium: z
174
+ .string()
175
+ .nullable()
176
+ .describe("groupBy=SOURCE·CAMPAIGN일 때 값. utm_medium, 없으면 `referral`·`(none)`"),
177
+ campaign: z
178
+ .string()
179
+ .nullable()
180
+ .describe("groupBy=CAMPAIGN일 때 값. utm_campaign, 없으면 `(not set)`"),
181
+ device: z
182
+ .enum(["desktop", "mobile", "tablet", "unknown"])
183
+ .nullable()
184
+ .describe("groupBy=DEVICE일 때만 값"),
185
+ sessions: z.number().int(),
186
+ visitors: z.number().int().describe("날짜별 순방문자의 합"),
187
+ engagedSessions: z.number().int(),
188
+ purchaseSessions: z.number().int(),
189
+ purchases: z.number().int(),
190
+ revenue: z.number().int(),
191
+ conversionRate: rate("전환율 = 구매 세션 / 세션"),
192
+ });
193
+ export type AnalyticsSourceRow = z.infer<typeof analyticsSourceRowSchema>;
194
+
195
+ export const analyticsSourcesSchema = z.object({
196
+ ...rangeMeta,
197
+ groupBy: analyticsSourceGroupBySchema,
198
+ rows: z.array(analyticsSourceRowSchema).describe("세션 수 내림차순"),
199
+ });
200
+ export type AnalyticsSources = z.infer<typeof analyticsSourcesSchema>;
@@ -96,3 +96,39 @@ export type CategoryPatchRequest = z.infer<typeof categoryPatchRequestSchema>;
96
96
  export const categoryDeleteStrategySchema = z.enum(["reparent", "cascade"]);
97
97
 
98
98
  export type CategoryDeleteStrategy = z.infer<typeof categoryDeleteStrategySchema>;
99
+
100
+ /**
101
+ * `GET /v1/categories` 쿼리 — OpenAPI 문서용 형태(값은 쿼리 문자열). 응답 형태가 쿼리로 갈린다:
102
+ * `q`가 있으면 검색 결과(`CategorySearchResult[]`), 아니면 `tree=true`일 때 트리(`CategoryTreeNode[]`),
103
+ * 둘 다 아니면 목록(`Category[]`)이다.
104
+ */
105
+ export const categoryListQuerySchema = z.object({
106
+ parentId: z
107
+ .string()
108
+ .optional()
109
+ .describe("이 카테고리의 직계 자식만. 생략하면 최상위 카테고리. `flat=true`면 무시"),
110
+ flat: z
111
+ .enum(["true", "false"])
112
+ .optional()
113
+ .describe("`true`면 전체 카테고리를 깊이·순서대로 평탄화"),
114
+ tree: z
115
+ .enum(["true", "false"])
116
+ .optional()
117
+ .describe("`true`면 최상위부터 `children`을 채운 트리(`CategoryTreeNode[]`)를 돌려준다"),
118
+ q: z
119
+ .string()
120
+ .optional()
121
+ .describe(
122
+ "이름 검색(포함, 대소문자 무시). 주면 `fullPath`·`ancestors`가 붙은 검색 결과를 돌려준다",
123
+ ),
124
+ });
125
+
126
+ /** `DELETE /v1/categories/{categoryId}` 쿼리 — OpenAPI 문서용 형태 */
127
+ export const categoryDeleteQuerySchema = z.object({
128
+ strategy: categoryDeleteStrategySchema
129
+ .optional()
130
+ .describe(
131
+ "하위 카테고리·연결 상품 처리 방식. 생략하면 둘 중 하나라도 있을 때 409 `CATEGORY_NOT_EMPTY`다. " +
132
+ "`reparent`는 직계 자식을 삭제할 카테고리의 부모로 옮기고, `cascade`는 서브트리를 삭제하고 연결 상품을 미분류로 둔다",
133
+ ),
134
+ });
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import type { PageParams } from "./common";
2
+ import { type PageParams, pageQueryShape } from "./common";
3
3
 
4
4
  export const claimTypeSchema = z.enum(["CANCEL", "RETURN", "EXCHANGE"]);
5
5
 
@@ -65,6 +65,36 @@ export interface ClaimListParams extends PageParams {
65
65
  requestedTo?: string;
66
66
  }
67
67
 
68
+ /** `GET /v1/claims` 쿼리 — OpenAPI 문서용 형태(값은 쿼리 문자열) */
69
+ export const claimListQuerySchema = z.object({
70
+ type: claimTypeSchema.optional().describe("클레임 유형 필터"),
71
+ status: claimStatusSchema.optional().describe("클레임 상태 필터"),
72
+ orderItemId: z.string().optional().describe("주문상품 id 필터"),
73
+ requestedFrom: z.string().optional().describe("요청 시각 조회 시작 (ISO 8601, 포함)"),
74
+ requestedTo: z.string().optional().describe("요청 시각 조회 끝 (ISO 8601, 포함)"),
75
+ ...pageQueryShape(),
76
+ });
77
+
78
+ /** `GET /v1/claims/{claimId}` 응답 — 클레임 + 상태 이력 */
79
+ export const claimDetailSchema = claimSchema.extend({
80
+ histories: z
81
+ .array(
82
+ z.object({
83
+ status: claimStatusSchema,
84
+ actor: z.enum(["BUYER", "SELLER", "SYSTEM"]),
85
+ occurredAt: z.string(),
86
+ }),
87
+ )
88
+ .optional(),
89
+ });
90
+
91
+ export type ClaimDetail = z.infer<typeof claimDetailSchema>;
92
+
93
+ /** `POST /v1/claims/{claimId}/hold` 응답 */
94
+ export const claimHoldResultSchema = z.object({ claimId: z.string(), holdExpiresAt: z.string() });
95
+
96
+ export type ClaimHoldResult = z.infer<typeof claimHoldResultSchema>;
97
+
68
98
  export const rejectClaimRequestSchema = z.object({
69
99
  reason: z.enum([
70
100
  "USED_PRODUCT",
@@ -13,6 +13,18 @@ export function pageSchema<T extends z.ZodType>(item: T) {
13
13
  return pageInfoSchema.extend({ contents: z.array(item) });
14
14
  }
15
15
 
16
+ /**
17
+ * 목록 쿼리의 `page`·`size` — OpenAPI 문서용 형태(값은 쿼리 문자열). 서버는 숫자가 아니거나 범위를 벗어난
18
+ * 값을 기본값·상한으로 보정한다.
19
+ */
20
+ export function pageQueryShape(options: { defaultSize?: number; maxSize?: number } = {}) {
21
+ const { defaultSize = 20, maxSize = 100 } = options;
22
+ return {
23
+ page: z.string().optional().describe("페이지 번호 (1부터, 기본 1)"),
24
+ size: z.string().optional().describe(`페이지 크기 (기본 ${defaultSize}, 최대 ${maxSize})`),
25
+ };
26
+ }
27
+
16
28
  export interface PageParams {
17
29
  page?: number;
18
30
  size?: number;
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import type { PageParams } from "./common";
2
+ import { type PageParams, pageQueryShape } from "./common";
3
3
 
4
4
  export const inquirySchema = z.object({
5
5
  inquiryId: z.string(),
@@ -54,6 +54,27 @@ export const sellerCustomerInquirySchema = z.object({
54
54
 
55
55
  export type SellerCustomerInquiry = z.infer<typeof sellerCustomerInquirySchema>;
56
56
 
57
+ const answeredQuery = z
58
+ .enum(["true", "false"])
59
+ .optional()
60
+ .describe("답변 여부 필터. `true`면 답변된 문의만, `false`면 미답변만, 생략하면 전부");
61
+
62
+ /** `GET /v1/inquiries` 쿼리(상품 문의) — OpenAPI 문서용 형태(값은 쿼리 문자열). 최신순이다 */
63
+ export const inquiryListQuerySchema = z.object({
64
+ answered: answeredQuery,
65
+ productId: z.string().optional().describe("상품 id 필터"),
66
+ from: z.string().optional().describe("작성 시각 조회 시작 (ISO 8601, 포함)"),
67
+ ...pageQueryShape(),
68
+ });
69
+
70
+ /** `GET /v1/customer-inquiries` 쿼리(1:1 문의) — OpenAPI 문서용 형태(값은 쿼리 문자열). 최신순이다 */
71
+ export const customerInquiryListQuerySchema = z.object({
72
+ answered: answeredQuery,
73
+ category: customerInquiryCategorySchema.optional().describe("문의 유형 필터"),
74
+ from: z.string().optional().describe("작성 시각 조회 시작 (ISO 8601, 포함)"),
75
+ ...pageQueryShape(),
76
+ });
77
+
57
78
  export const settlementStatusSchema = z.enum(["SCHEDULED", "PAID", "HELD"]);
58
79
 
59
80
  export type SettlementStatus = z.infer<typeof settlementStatusSchema>;
@@ -88,6 +109,29 @@ export const settlementDetailSchema = z.object({
88
109
 
89
110
  export type SettlementDetail = z.infer<typeof settlementDetailSchema>;
90
111
 
112
+ /** `GET /v1/settlements/{settlementId}` 응답 — 정산 요약 + 상세 내역(`size`건까지) */
113
+ export const settlementWithDetailsSchema = settlementSummarySchema.extend({
114
+ details: z.array(settlementDetailSchema).optional(),
115
+ });
116
+
117
+ export type SettlementWithDetails = z.infer<typeof settlementWithDetailsSchema>;
118
+
119
+ /** `GET /v1/settlements` 쿼리 — OpenAPI 문서용 형태(값은 쿼리 문자열). 정산일 최신순이다 */
120
+ export const settlementListQuerySchema = z.object({
121
+ from: z.string().describe("정산일 조회 시작 (ISO 8601, 포함). 필수"),
122
+ to: z
123
+ .string()
124
+ .optional()
125
+ .describe("정산일 조회 끝 (ISO 8601, 포함). 생략하면 from부터 31일. 기간은 최대 31일"),
126
+ status: settlementStatusSchema.optional().describe("정산 상태 필터"),
127
+ ...pageQueryShape(),
128
+ });
129
+
130
+ /** `GET /v1/settlements/{settlementId}` 쿼리 — `size`는 돌려줄 상세 내역 건수다(`page`는 무시) */
131
+ export const settlementDetailQuerySchema = z.object(
132
+ pageQueryShape({ defaultSize: 100, maxSize: 300 }),
133
+ );
134
+
91
135
  export interface SettlementListParams extends PageParams {
92
136
  from: string;
93
137
  to?: string;
@@ -288,11 +332,42 @@ export const reviewSchema = z.object({
288
332
 
289
333
  export type Review = z.infer<typeof reviewSchema>;
290
334
 
335
+ export const reviewSortSchema = z.enum([
336
+ "createdAt.desc",
337
+ "createdAt.asc",
338
+ "rating.asc",
339
+ "rating.desc",
340
+ ]);
341
+
342
+ export type ReviewSort = z.infer<typeof reviewSortSchema>;
343
+
291
344
  export interface ReviewListParams extends PageParams {
292
345
  productId?: string;
293
346
  rating?: number;
294
347
  replied?: boolean;
295
348
  hasImage?: boolean;
296
349
  from?: string;
297
- sort?: "createdAt.desc" | "createdAt.asc" | "rating.asc" | "rating.desc";
350
+ sort?: ReviewSort;
298
351
  }
352
+
353
+ /** `GET /v1/reviews` 쿼리 — OpenAPI 문서용 형태(값은 쿼리 문자열) */
354
+ export const reviewListQuerySchema = z.object({
355
+ productId: z.string().optional().describe("상품 id 필터"),
356
+ rating: z.enum(["1", "2", "3", "4", "5"]).optional().describe("별점 필터 (일치)"),
357
+ replied: z
358
+ .enum(["true", "false"])
359
+ .optional()
360
+ .describe("답글 여부 필터. `true`면 답글 단 리뷰만, `false`면 답글 없는 리뷰만"),
361
+ hasImage: z.enum(["true", "false"]).optional().describe("`true`면 이미지가 있는 리뷰만"),
362
+ from: z.string().optional().describe("작성 시각 조회 시작 (ISO 8601, 포함)"),
363
+ sort: reviewSortSchema.optional().describe("정렬 (기본 createdAt.desc)"),
364
+ ...pageQueryShape(),
365
+ });
366
+
367
+ /** `POST /v1/reviews/{reviewId}/report` 응답(202) — 신고 접수 */
368
+ export const reviewReportResultSchema = z.object({
369
+ reviewId: z.string(),
370
+ status: z.literal("PENDING_REVIEW"),
371
+ });
372
+
373
+ export type ReviewReportResult = z.infer<typeof reviewReportResultSchema>;
@@ -209,6 +209,24 @@ export const deliveryInfoSchema = z.object({
209
209
 
210
210
  export type DeliveryInfo = z.infer<typeof deliveryInfoSchema>;
211
211
 
212
+ /** `GET /v1/order-items/{orderItemId}/delivery` 응답 — 배송 정보 + 배송 추적 이벤트 */
213
+ export const deliveryStatusSchema = deliveryInfoSchema.extend({
214
+ trackingEvents: z
215
+ .array(z.object({ status: z.string(), location: z.string(), occurredAt: z.string() }))
216
+ .optional(),
217
+ });
218
+
219
+ export type DeliveryStatus = z.infer<typeof deliveryStatusSchema>;
220
+
221
+ /** `PUT /v1/order-items/{orderItemId}/dispatch/tracking` 응답 — 바뀐 송장 */
222
+ export const trackingUpdateResultSchema = z.object({
223
+ orderItemId: z.string(),
224
+ carrierCode: z.string(),
225
+ trackingNumber: z.string(),
226
+ });
227
+
228
+ export type TrackingUpdateResult = z.infer<typeof trackingUpdateResultSchema>;
229
+
212
230
  export const carrierSchema = z.object({
213
231
  carrierCode: z.string(),
214
232
  name: z.string(),
@@ -231,3 +249,45 @@ export const cancelBySellerRequestSchema = z.object({
231
249
  });
232
250
 
233
251
  export type CancelBySellerRequest = z.infer<typeof cancelBySellerRequestSchema>;
252
+
253
+ /** `POST /v1/order-items/{orderItemId}/cancel` 응답 — 셀러 직권취소 환불 결과 */
254
+ export const cancelBySellerResultSchema = z.object({
255
+ orderItemId: z.string(),
256
+ refundAmount: z.number().int(),
257
+ refundedAt: z.string(),
258
+ });
259
+
260
+ export type CancelBySellerResult = z.infer<typeof cancelBySellerResultSchema>;
261
+
262
+ /** `PUT /v1/order-items/{orderItemId}/memo` 응답 — 빈 문자열로 저장하면 메모가 지워져 `null`이다 */
263
+ export const orderItemMemoResultSchema = z.object({
264
+ orderItemId: z.string(),
265
+ sellerMemo: z.string().nullable(),
266
+ });
267
+
268
+ export type OrderItemMemoResult = z.infer<typeof orderItemMemoResultSchema>;
269
+
270
+ /** `POST /v1/order-items/{orderItemId}/purchase-decision/request` 응답 */
271
+ export const purchaseDecisionRequestResultSchema = z.object({
272
+ orderItemId: z.string(),
273
+ requestedAt: z.string(),
274
+ });
275
+
276
+ export type PurchaseDecisionRequestResult = z.infer<typeof purchaseDecisionRequestResultSchema>;
277
+
278
+ /** `POST /v1/order-items/{orderItemId}/purchase-decision/extend` 응답 — 연장은 최대 2회 */
279
+ export const purchaseDecisionExtendResultSchema = z.object({
280
+ orderItemId: z.string(),
281
+ autoDecisionDate: z.string(),
282
+ extendCount: z.number().int().max(2),
283
+ });
284
+
285
+ export type PurchaseDecisionExtendResult = z.infer<typeof purchaseDecisionExtendResultSchema>;
286
+
287
+ /**
288
+ * `GET /v1/orders/{orderId}/address-change-requests` 응답 — 배송지 변경 요청 플로우는 아직 제공하지 않아
289
+ * 현재는 항상 빈 배열이다
290
+ */
291
+ export const addressChangeRequestListSchema = z
292
+ .array(z.record(z.string(), z.unknown()))
293
+ .describe("배송지 변경 요청 목록 (현재 항상 빈 배열)");