@sayren/store-sdk 0.41.0 → 0.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { a as WEBHOOK_HEADERS, i as WEBHOOK_EVENT_TYPE_HEADER, n as WEBHOOK_DELIVERY_ID_HEADER, o as WEBHOOK_SIGNATURE_HEADER, r as WEBHOOK_EVENT_ID_HEADER, s as WEBHOOK_USER_AGENT, t as WEBHOOK_DEFAULT_TOLERANCE_SEC } from "./webhook-headers-D974dE4T.mjs";
1
+ import { a as WEBHOOK_HEADERS, i as WEBHOOK_EVENT_TYPE_HEADER, n as WEBHOOK_DELIVERY_ID_HEADER, o as WEBHOOK_SIGNATURE_HEADER, r as WEBHOOK_EVENT_ID_HEADER, s as WEBHOOK_USER_AGENT, t as WEBHOOK_DEFAULT_TOLERANCE_SEC } from "./webhook-headers-D0E3wPpS.mjs";
2
2
  //#region src/member-delegation.ts
3
3
  /** 위임 실패 — `error`는 api 오류 코드(엔벨로프 `error.code`, 없으면 `http_{status}`) */
4
4
  var MemberDelegationError = class extends Error {
@@ -1,4 +1,82 @@
1
1
  import { z } from "zod";
2
+ //#region src/schemas/strict.ts
3
+ /**
4
+ * 관리 API 요청 본문을 모르는 키까지 거르는 스키마로 바꾼다(이슈 #193, 2026-10-24). `z.object`는 모르는 키를 조용히 버려서
5
+ * 오타·잘못된 자리의 필드(예: 옵션 값에 둔 추가금)가 400 없이 사라지고 기본값으로 저장됐다. 원본은 응답 스키마와 하위 스키마를
6
+ * 공유하므로 고치지 않고, 안쪽 객체까지 `catchall: never`로 복제한다. `z.looseObject`처럼 catchall을 정한 객체는 그대로 둔다.
7
+ * 설명·예시(전역 등록부의 메타)는 복제본에 옮겨 OpenAPI 문서가 같다
8
+ */
9
+ function strictRequest(schema) {
10
+ return strictOf(schema, /* @__PURE__ */ new Map());
11
+ }
12
+ function strictOf(schema, seen) {
13
+ const done = seen.get(schema);
14
+ if (done) return done;
15
+ const def = schema._zod.def;
16
+ const clone = (patch) => {
17
+ const next = schema.clone({
18
+ ...def,
19
+ ...patch
20
+ });
21
+ const meta = z.globalRegistry.get(schema);
22
+ if (meta) z.globalRegistry.add(next, meta);
23
+ return next;
24
+ };
25
+ let next;
26
+ switch (def.type) {
27
+ case "object":
28
+ next = clone({
29
+ shape: Object.fromEntries(Object.entries(def.shape).map(([key, value]) => [key, strictOf(value, seen)])),
30
+ catchall: def.catchall ?? z.never()
31
+ });
32
+ break;
33
+ case "array":
34
+ next = clone({ element: strictOf(def.element, seen) });
35
+ break;
36
+ case "optional":
37
+ case "nullable":
38
+ case "default":
39
+ case "prefault":
40
+ case "nonoptional":
41
+ case "readonly":
42
+ case "catch":
43
+ next = clone({ innerType: strictOf(def.innerType, seen) });
44
+ break;
45
+ case "union":
46
+ next = clone({ options: def.options.map((option) => strictOf(option, seen)) });
47
+ break;
48
+ case "intersection":
49
+ next = clone({
50
+ left: strictOf(def.left, seen),
51
+ right: strictOf(def.right, seen)
52
+ });
53
+ break;
54
+ case "pipe":
55
+ next = clone({ in: strictOf(def.in, seen) });
56
+ break;
57
+ case "record":
58
+ next = clone({ valueType: strictOf(def.valueType, seen) });
59
+ break;
60
+ case "tuple":
61
+ next = clone({
62
+ items: def.items.map((item) => strictOf(item, seen)),
63
+ rest: def.rest ? strictOf(def.rest, seen) : def.rest
64
+ });
65
+ break;
66
+ case "lazy": {
67
+ let inner;
68
+ next = clone({ getter: () => {
69
+ inner ??= strictOf(def.getter(), seen);
70
+ return inner;
71
+ } });
72
+ break;
73
+ }
74
+ default: next = schema;
75
+ }
76
+ seen.set(schema, next);
77
+ return next;
78
+ }
79
+ //#endregion
2
80
  //#region src/schemas/audit.ts
3
81
  /**
4
82
  * 감사 로그와 승인 요청 — `/v1/audit-logs`, `/v1/approval-requests` (이슈 #36).
@@ -155,7 +233,7 @@ const approvalPageSchema = z.object({
155
233
  items: z.array(approvalRequestSchema),
156
234
  nextCursor: z.string().nullable().describe("다음 페이지 커서. 마지막 페이지면 null")
157
235
  });
158
- const approvalDecisionRequestSchema = z.object({ note: z.string().trim().max(500).optional().describe("승인·거절 메모 (최대 500자)") });
236
+ const approvalDecisionRequestSchema = strictRequest(z.object({ note: z.string().trim().max(500).optional().describe("승인·거절 메모 (최대 500자)") }));
159
237
  //#endregion
160
238
  //#region src/http.ts
161
239
  /** 표준 엔벨로프의 $.error 상세 */
@@ -356,4 +434,4 @@ const WEBHOOK_HEADERS = {
356
434
  deliveryId: WEBHOOK_DELIVERY_ID_HEADER
357
435
  };
358
436
  //#endregion
359
- export { auditLogPageSchema as A, approvalPageSchema as C, auditActorTypeSchema as D, auditActorSchema as E, auditLogSourceSchema as M, writeDryRunResultSchema as N, auditLogListQuerySchema as O, writePreviewSchema as P, approvalListQuerySchema as S, approvalStatusSchema as T, resolveToken as _, WEBHOOK_HEADERS as a, agentPolicyDecisionSchema as b, APPROVAL_ALREADY_DECIDED_CODE as c, ApprovalPendingError as d, SAYREN_API_BASE_URL as f, path as g, createHttp as h, WEBHOOK_EVENT_TYPE_HEADER as i, auditLogSchema as j, auditLogOutcomeSchema as k, APPROVAL_REQUIRED_CODE as l, buildQuery as m, WEBHOOK_DELIVERY_ID_HEADER as n, WEBHOOK_SIGNATURE_HEADER as o, SAYREN_API_ORIGIN as p, WEBHOOK_EVENT_ID_HEADER as r, WEBHOOK_USER_AGENT as s, WEBHOOK_DEFAULT_TOLERANCE_SEC as t, ApiError as u, toApiError as v, approvalRequestSchema as w, approvalDecisionRequestSchema as x, unwrapData as y };
437
+ export { auditLogPageSchema as A, approvalPageSchema as C, auditActorTypeSchema as D, auditActorSchema as E, strictRequest as F, auditLogSourceSchema as M, writeDryRunResultSchema as N, auditLogListQuerySchema as O, writePreviewSchema as P, approvalListQuerySchema as S, approvalStatusSchema as T, resolveToken as _, WEBHOOK_HEADERS as a, agentPolicyDecisionSchema as b, APPROVAL_ALREADY_DECIDED_CODE as c, ApprovalPendingError as d, SAYREN_API_BASE_URL as f, path as g, createHttp as h, WEBHOOK_EVENT_TYPE_HEADER as i, auditLogSchema as j, auditLogOutcomeSchema as k, APPROVAL_REQUIRED_CODE as l, buildQuery as m, WEBHOOK_DELIVERY_ID_HEADER as n, WEBHOOK_SIGNATURE_HEADER as o, SAYREN_API_ORIGIN as p, WEBHOOK_EVENT_ID_HEADER as r, WEBHOOK_USER_AGENT as s, WEBHOOK_DEFAULT_TOLERANCE_SEC as t, ApiError as u, toApiError as v, approvalRequestSchema as w, approvalDecisionRequestSchema as x, unwrapData as y };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sayren/store-sdk",
3
- "version": "0.41.0",
3
+ "version": "0.42.0",
4
4
  "main": "./dist/index.mjs",
5
5
  "types": "./dist/index.d.mts",
6
6
  "exports": {
package/src/schemas/ai.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { storefrontSlugSchema } from "./storefront-hosting";
3
+ import { strictRequest } from "./strict";
3
4
 
4
5
  /**
5
6
  * 상점 플랫폼 AI 채팅(이슈 #124) — 대화·메시지·실행과 실행 이벤트 스트림의 계약.
@@ -388,12 +389,14 @@ export const storeSetupBriefSchema = z.object({
388
389
  });
389
390
  export type StoreSetupBrief = z.infer<typeof storeSetupBriefSchema>;
390
391
 
391
- export const createAiConversationRequestSchema = z.object({
392
- /** 웹사이트에서 남긴 초안(#124 M2) — 연결한 사용자만 쓴다. 주면 첫 메시지로 실행을 시작한다 */
393
- draftId: z.string().max(64).optional(),
394
- /** 질문 화면의 답 — `draftId`와 함께만 쓴다. 초안 앞에 붙어 첫 메시지가 된다 */
395
- brief: storeSetupBriefSchema.optional(),
396
- });
392
+ export const createAiConversationRequestSchema = strictRequest(
393
+ z.object({
394
+ /** 웹사이트에서 남긴 초안(#124 M2) — 연결한 사용자만 쓴다. 주면 첫 메시지로 실행을 시작한다 */
395
+ draftId: z.string().max(64).optional(),
396
+ /** 질문 화면의 답 — `draftId`와 함께만 쓴다. 초안 앞에 붙어 첫 메시지가 된다 */
397
+ brief: storeSetupBriefSchema.optional(),
398
+ }),
399
+ );
397
400
  export type CreateAiConversationRequest = z.infer<typeof createAiConversationRequestSchema>;
398
401
 
399
402
  export const createAiConversationResponseSchema = z.object({
@@ -403,20 +406,22 @@ export const createAiConversationResponseSchema = z.object({
403
406
  });
404
407
  export type CreateAiConversationResponse = z.infer<typeof createAiConversationResponseSchema>;
405
408
 
406
- export const sendAiMessageRequestSchema = z
407
- .object({
408
- text: z.string().trim().max(AI_MESSAGE_MAX_CHARS),
409
- attachments: z.array(aiAttachmentSchema).max(AI_MESSAGE_MAX_ATTACHMENTS).default([]),
410
- /** 선택지 버튼으로 보낸 답이면 그 값(화면 표시는 `text`) */
411
- choice: z.string().max(200).optional(),
412
- /** 편집 미리보기에서 고른 요소(#65) — 모델은 이 요소를 기준으로 고친다 */
413
- selection: aiPreviewSelectionSchema.optional(),
414
- /** 디자인 탭에서 고른 아트보드(2026-10-14) — 모델은 그 아트보드를 기준으로 고친다 */
415
- designSelection: aiDesignSelectionSchema.optional(),
416
- })
417
- .refine((body) => body.text.length > 0 || body.attachments.length > 0, {
418
- message: "내용이나 첨부가 있어야 합니다",
419
- });
409
+ export const sendAiMessageRequestSchema = strictRequest(
410
+ z
411
+ .object({
412
+ text: z.string().trim().max(AI_MESSAGE_MAX_CHARS),
413
+ attachments: z.array(aiAttachmentSchema).max(AI_MESSAGE_MAX_ATTACHMENTS).default([]),
414
+ /** 선택지 버튼으로 보낸 답이면 그 값(화면 표시는 `text`) */
415
+ choice: z.string().max(200).optional(),
416
+ /** 편집 미리보기에서 고른 요소(#65) — 모델은 이 요소를 기준으로 고친다 */
417
+ selection: aiPreviewSelectionSchema.optional(),
418
+ /** 디자인 탭에서 고른 아트보드(2026-10-14) — 모델은 그 아트보드를 기준으로 고친다 */
419
+ designSelection: aiDesignSelectionSchema.optional(),
420
+ })
421
+ .refine((body) => body.text.length > 0 || body.attachments.length > 0, {
422
+ message: "내용이나 첨부가 있어야 합니다",
423
+ }),
424
+ );
420
425
  export type SendAiMessageRequest = z.input<typeof sendAiMessageRequestSchema>;
421
426
 
422
427
  export const sendAiMessageResponseSchema = aiStreamAccessSchema.extend({
@@ -622,18 +627,20 @@ export const STORE_DRAFT_ATTACHMENT_MAX_BYTES = 10 * 1024 * 1024;
622
627
  /** 연결하지 않은 초안을 지우기까지 */
623
628
  export const STORE_DRAFT_RETENTION_DAYS = 7;
624
629
 
625
- export const createStoreDraftRequestSchema = z.object({
626
- text: z.string().trim().min(1).max(AI_MESSAGE_MAX_CHARS),
627
- attachments: z
628
- .array(
629
- z.object({
630
- filename: z.string().trim().min(1).max(200),
631
- size: z.number().int().positive().max(STORE_DRAFT_ATTACHMENT_MAX_BYTES),
632
- }),
633
- )
634
- .max(STORE_DRAFT_MAX_ATTACHMENTS)
635
- .default([]),
636
- });
630
+ export const createStoreDraftRequestSchema = strictRequest(
631
+ z.object({
632
+ text: z.string().trim().min(1).max(AI_MESSAGE_MAX_CHARS),
633
+ attachments: z
634
+ .array(
635
+ z.object({
636
+ filename: z.string().trim().min(1).max(200),
637
+ size: z.number().int().positive().max(STORE_DRAFT_ATTACHMENT_MAX_BYTES),
638
+ }),
639
+ )
640
+ .max(STORE_DRAFT_MAX_ATTACHMENTS)
641
+ .default([]),
642
+ }),
643
+ );
637
644
  export type CreateStoreDraftRequest = z.input<typeof createStoreDraftRequestSchema>;
638
645
 
639
646
  export const createStoreDraftResponseSchema = z.object({
@@ -8,6 +8,7 @@ import {
8
8
  appSettingFieldSchema,
9
9
  } from "./app-manifest";
10
10
  import { webhookDeliveryErrorCodeSchema, webhookDeliveryStatusSchema } from "./misc";
11
+ import { strictRequest } from "./strict";
11
12
 
12
13
  /**
13
14
  * 설치 앱 관리 API 계약 (이슈 #22) — `/v1/apps/installations/**`(상점이 설치한 앱), `/v1/app/installation`(설치 토큰 자기 조회).
@@ -183,21 +184,23 @@ export type AppInstallationActivity = z.infer<typeof appInstallationActivitySche
183
184
  * - `{ result }`: accounts 설치 동의 결과(1회용 JWT). 개발자 포털 [내 상점에 설치]가 이 길로 온다
184
185
  * - `{ appId, settings }`: 상점 플랫폼 앱 목록 › 설치 화면에서 동의하고 설정까지 채워 바로 설치한다
185
186
  */
186
- export const appInstallRequestSchema = z.union([
187
- z.object({
188
- result: z
189
- .string()
190
- .min(1)
191
- .describe("accounts가 발급한 설치 결과 JWT(`typ: sayren-install+jwt`)"),
192
- }),
193
- z.object({
194
- appId: z.string().min(1),
195
- settings: z
196
- .record(z.string(), settingsValueSchema)
197
- .default({})
198
- .describe("설치와 함께 저장할 설정. 비밀 필드도 여기서 넣는다"),
199
- }),
200
- ]);
187
+ export const appInstallRequestSchema = strictRequest(
188
+ z.union([
189
+ z.object({
190
+ result: z
191
+ .string()
192
+ .min(1)
193
+ .describe("accounts가 발급한 설치 결과 JWT(`typ: sayren-install+jwt`)"),
194
+ }),
195
+ z.object({
196
+ appId: z.string().min(1),
197
+ settings: z
198
+ .record(z.string(), settingsValueSchema)
199
+ .default({})
200
+ .describe("설치와 함께 저장할 설정. 비밀 필드도 여기서 넣는다"),
201
+ }),
202
+ ]),
203
+ );
201
204
  export type AppInstallRequest = z.infer<typeof appInstallRequestSchema>;
202
205
 
203
206
  /** 앱 목록 정보 — 현재 버전이 올린 아이콘·README(없으면 매니페스트 `iconUrl`) */
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { strictRequest } from "./strict";
2
3
 
3
4
  /**
4
5
  * 감사 로그와 승인 요청 — `/v1/audit-logs`, `/v1/approval-requests` (이슈 #36).
@@ -261,7 +262,9 @@ export const approvalPageSchema = z.object({
261
262
  });
262
263
  export type ApprovalPage = z.infer<typeof approvalPageSchema>;
263
264
 
264
- export const approvalDecisionRequestSchema = z.object({
265
- note: z.string().trim().max(500).optional().describe("승인·거절 메모 (최대 500자)"),
266
- });
265
+ export const approvalDecisionRequestSchema = strictRequest(
266
+ z.object({
267
+ note: z.string().trim().max(500).optional().describe("승인·거절 메모 (최대 500자)"),
268
+ }),
269
+ );
267
270
  export type ApprovalDecisionRequest = z.infer<typeof approvalDecisionRequestSchema>;
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { strictRequest } from "./strict";
2
3
 
3
4
  export const categoryProductSortSchema = z.enum([
4
5
  "RECENT",
@@ -70,40 +71,44 @@ export const categoryAttributeSchema = z.object({
70
71
 
71
72
  export type CategoryAttribute = z.infer<typeof categoryAttributeSchema>;
72
73
 
73
- export const categoryCreateRequestSchema = z.object({
74
- name: z.string().min(1).max(100),
75
- parentId: z.string().nullable().optional(),
76
- sortOrder: z.number().int().optional(),
77
- visible: z.boolean().optional(),
78
- showPc: z.boolean().optional(),
79
- showMobile: z.boolean().optional(),
80
- productSort: categoryProductSortSchema.optional(),
81
- metaTitle: z.string().max(100).nullable().optional(),
82
- metaDescription: z.string().max(300).nullable().optional(),
83
- metaKeywords: z.string().max(200).nullable().optional(),
84
- });
74
+ export const categoryCreateRequestSchema = strictRequest(
75
+ z.object({
76
+ name: z.string().min(1).max(100),
77
+ parentId: z.string().nullable().optional(),
78
+ sortOrder: z.number().int().optional(),
79
+ visible: z.boolean().optional(),
80
+ showPc: z.boolean().optional(),
81
+ showMobile: z.boolean().optional(),
82
+ productSort: categoryProductSortSchema.optional(),
83
+ metaTitle: z.string().max(100).nullable().optional(),
84
+ metaDescription: z.string().max(300).nullable().optional(),
85
+ metaKeywords: z.string().max(200).nullable().optional(),
86
+ }),
87
+ );
85
88
 
86
89
  export type CategoryCreateRequest = z.infer<typeof categoryCreateRequestSchema>;
87
90
 
88
- export const categoryPatchRequestSchema = z.object({
89
- name: z.string().min(1).max(100).optional(),
90
- parentId: z.string().nullable().optional(),
91
- sortOrder: z.number().int().optional(),
92
- visible: z.boolean().optional(),
93
- showPc: z.boolean().optional(),
94
- showMobile: z.boolean().optional(),
95
- productSort: categoryProductSortSchema.optional(),
96
- metaTitle: z.string().max(100).nullable().optional(),
97
- metaDescription: z.string().max(300).nullable().optional(),
98
- metaKeywords: z.string().max(200).nullable().optional(),
99
- flowId: z
100
- .string()
101
- .nullable()
102
- .optional()
103
- .describe(
104
- "주문 흐름(이슈 #115) — 게시한 흐름 id, null이면 기본 흐름. 맨 아래 카테고리에만 지정한다(400 `CATEGORY_NOT_LEAF`). 게시하지 않은 흐름은 409 `FLOW_NOT_PUBLISHED`",
105
- ),
106
- });
91
+ export const categoryPatchRequestSchema = strictRequest(
92
+ z.object({
93
+ name: z.string().min(1).max(100).optional(),
94
+ parentId: z.string().nullable().optional(),
95
+ sortOrder: z.number().int().optional(),
96
+ visible: z.boolean().optional(),
97
+ showPc: z.boolean().optional(),
98
+ showMobile: z.boolean().optional(),
99
+ productSort: categoryProductSortSchema.optional(),
100
+ metaTitle: z.string().max(100).nullable().optional(),
101
+ metaDescription: z.string().max(300).nullable().optional(),
102
+ metaKeywords: z.string().max(200).nullable().optional(),
103
+ flowId: z
104
+ .string()
105
+ .nullable()
106
+ .optional()
107
+ .describe(
108
+ "주문 흐름(이슈 #115) — 게시한 흐름 id, null이면 기본 흐름. 맨 아래 카테고리에만 지정한다(400 `CATEGORY_NOT_LEAF`). 게시하지 않은 흐름은 409 `FLOW_NOT_PUBLISHED`",
109
+ ),
110
+ }),
111
+ );
107
112
 
108
113
  export type CategoryPatchRequest = z.infer<typeof categoryPatchRequestSchema>;
109
114
 
@@ -1,6 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { commerceNumberField } from "./commerce-number";
3
3
  import { type PageParams, pageQueryShape } from "./common";
4
+ import { strictRequest } from "./strict";
4
5
 
5
6
  export const claimTypeSchema = z.enum(["CANCEL", "RETURN", "EXCHANGE"]);
6
7
 
@@ -164,17 +165,19 @@ export const claimHoldResultSchema = z.object({ claimId: z.string(), holdExpires
164
165
 
165
166
  export type ClaimHoldResult = z.infer<typeof claimHoldResultSchema>;
166
167
 
167
- export const rejectClaimRequestSchema = z.object({
168
- reason: z.enum([
169
- "USED_PRODUCT",
170
- "MISSING_COMPONENT",
171
- "PERIOD_EXPIRED",
172
- "BUYER_FAULT_DAMAGE",
173
- "ETC",
174
- ]),
175
- detail: z.string().min(10).max(1000),
176
- evidenceImages: z.array(z.url()).max(5).optional(),
177
- });
168
+ export const rejectClaimRequestSchema = strictRequest(
169
+ z.object({
170
+ reason: z.enum([
171
+ "USED_PRODUCT",
172
+ "MISSING_COMPONENT",
173
+ "PERIOD_EXPIRED",
174
+ "BUYER_FAULT_DAMAGE",
175
+ "ETC",
176
+ ]),
177
+ detail: z.string().min(10).max(1000),
178
+ evidenceImages: z.array(z.url()).max(5).optional(),
179
+ }),
180
+ );
178
181
 
179
182
  export type RejectClaimRequest = z.infer<typeof rejectClaimRequestSchema>;
180
183
 
@@ -1,6 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { type PageParams, pageQueryShape, pageSchema } from "./common";
3
3
  import { productImageUrlSchema } from "./product";
4
+ import { strictRequest } from "./strict";
4
5
 
5
6
  /**
6
7
  * 컬렉션 (이슈 #83) — 상품을 어디에 보여 줄지 정하는 묶음이다. 카테고리는 상품이 무엇인지를 정하고 상품당 하나지만, 컬렉션은
@@ -432,33 +433,35 @@ const periodRefine = (
432
433
  * 직접 선택(`MANUAL`)은 `productIds`가 처음 담을 상품과 순서다. 조건 자동(`AUTOMATIC`)은 `rules`가 있어야 하고 `productIds`를 받지 않는다.
433
434
  * `productSort`를 생략하면 직접 선택은 `MANUAL`, 조건 자동은 `LATEST`(판매량 상위 조건이 있으면 `BEST_SELLING`)다. 조건 자동은 `MANUAL`을 쓸 수 없다.
434
435
  */
435
- export const collectionCreateRequestSchema = z
436
- .object({
437
- title: z.string().trim().min(1).max(MAX_COLLECTION_TITLE).describe("제목"),
438
- slug: slugField.optional(),
439
- type: z
440
- .enum(COLLECTION_TYPES)
441
- .default("MANUAL")
442
- .describe("유형. `MANUAL`(직접 선택)·`AUTOMATIC`(조건 자동). 만든 뒤 바꿀 수 없다"),
443
- rules: collectionRulesSchema
444
- .optional()
445
- .describe("조건 자동 컬렉션의 조건(AND). 조건 자동이면 필수, 직접 선택이면 줄 수 없다"),
446
- description: commonFields.description.default(""),
447
- imageUrl: commonFields.imageUrl.optional(),
448
- productSort: commonFields.productSort
449
- .optional()
450
- .describe(
451
- "스토어프론트 상품 기본 정렬. 생략하면 직접 선택은 `MANUAL`, 조건 자동은 `LATEST`(판매량 상위 조건이 있으면 `BEST_SELLING`)다. 조건 자동은 `MANUAL`을 쓸 수 없다",
452
- ),
453
- visible: commonFields.visible.default(true),
454
- startsAt: commonFields.startsAt,
455
- endsAt: commonFields.endsAt,
456
- sortOrder: commonFields.sortOrder.optional().describe("목록 순서. 생략하면 맨 뒤"),
457
- metaTitle: commonFields.metaTitle.optional(),
458
- metaDescription: commonFields.metaDescription.optional(),
459
- productIds: productIdsField.optional(),
460
- })
461
- .superRefine(periodRefine);
436
+ export const collectionCreateRequestSchema = strictRequest(
437
+ z
438
+ .object({
439
+ title: z.string().trim().min(1).max(MAX_COLLECTION_TITLE).describe("제목"),
440
+ slug: slugField.optional(),
441
+ type: z
442
+ .enum(COLLECTION_TYPES)
443
+ .default("MANUAL")
444
+ .describe("유형. `MANUAL`(직접 선택)·`AUTOMATIC`(조건 자동). 만든 뒤 바꿀 수 없다"),
445
+ rules: collectionRulesSchema
446
+ .optional()
447
+ .describe("조건 자동 컬렉션의 조건(AND). 조건 자동이면 필수, 직접 선택이면 줄 수 없다"),
448
+ description: commonFields.description.default(""),
449
+ imageUrl: commonFields.imageUrl.optional(),
450
+ productSort: commonFields.productSort
451
+ .optional()
452
+ .describe(
453
+ "스토어프론트 상품 기본 정렬. 생략하면 직접 선택은 `MANUAL`, 조건 자동은 `LATEST`(판매량 상위 조건이 있으면 `BEST_SELLING`)다. 조건 자동은 `MANUAL`을 쓸 수 없다",
454
+ ),
455
+ visible: commonFields.visible.default(true),
456
+ startsAt: commonFields.startsAt,
457
+ endsAt: commonFields.endsAt,
458
+ sortOrder: commonFields.sortOrder.optional().describe("목록 순서. 생략하면 맨 뒤"),
459
+ metaTitle: commonFields.metaTitle.optional(),
460
+ metaDescription: commonFields.metaDescription.optional(),
461
+ productIds: productIdsField.optional(),
462
+ })
463
+ .superRefine(periodRefine),
464
+ );
462
465
 
463
466
  export type CollectionCreateRequest = z.infer<typeof collectionCreateRequestSchema>;
464
467
  export type CollectionCreateRequestInput = z.input<typeof collectionCreateRequestSchema>;
@@ -467,45 +470,51 @@ export type CollectionCreateRequestInput = z.input<typeof collectionCreateReques
467
470
  * `PATCH /v1/collections/{collectionId}` — 준 항목만 바뀐다. `type`을 주면 400 `COLLECTION_TYPE_IMMUTABLE`이다.
468
471
  * `revision`을 주면 그 번호일 때만 바뀌고, 그 사이 다른 수정이 있었으면 409 `COLLECTION_CHANGED`다.
469
472
  */
470
- export const collectionPatchRequestSchema = z
471
- .object({
472
- title: z.string().trim().min(1).max(MAX_COLLECTION_TITLE).optional(),
473
- slug: slugField.optional(),
474
- type: z.string().optional().describe("바꿀 수 없다. 주면 400 `COLLECTION_TYPE_IMMUTABLE`"),
475
- rules: collectionRulesSchema
476
- .optional()
477
- .describe(
478
- "조건 자동 컬렉션의 조건을 통째로 바꾼다. 직접 선택 컬렉션에 주면 400 `COLLECTION_RULES_NOT_ALLOWED`",
479
- ),
480
- description: commonFields.description.optional(),
481
- imageUrl: commonFields.imageUrl.optional(),
482
- productSort: commonFields.productSort.optional(),
483
- visible: commonFields.visible.optional(),
484
- startsAt: commonFields.startsAt,
485
- endsAt: commonFields.endsAt,
486
- sortOrder: commonFields.sortOrder.optional(),
487
- metaTitle: commonFields.metaTitle.optional(),
488
- metaDescription: commonFields.metaDescription.optional(),
489
- revision: z.number().int().min(0).optional().describe("읽은 수정 번호"),
490
- })
491
- .superRefine(periodRefine);
473
+ export const collectionPatchRequestSchema = strictRequest(
474
+ z
475
+ .object({
476
+ title: z.string().trim().min(1).max(MAX_COLLECTION_TITLE).optional(),
477
+ slug: slugField.optional(),
478
+ type: z.string().optional().describe("바꿀 수 없다. 주면 400 `COLLECTION_TYPE_IMMUTABLE`"),
479
+ rules: collectionRulesSchema
480
+ .optional()
481
+ .describe(
482
+ "조건 자동 컬렉션의 조건을 통째로 바꾼다. 직접 선택 컬렉션에 주면 400 `COLLECTION_RULES_NOT_ALLOWED`",
483
+ ),
484
+ description: commonFields.description.optional(),
485
+ imageUrl: commonFields.imageUrl.optional(),
486
+ productSort: commonFields.productSort.optional(),
487
+ visible: commonFields.visible.optional(),
488
+ startsAt: commonFields.startsAt,
489
+ endsAt: commonFields.endsAt,
490
+ sortOrder: commonFields.sortOrder.optional(),
491
+ metaTitle: commonFields.metaTitle.optional(),
492
+ metaDescription: commonFields.metaDescription.optional(),
493
+ revision: z.number().int().min(0).optional().describe("읽은 수정 번호"),
494
+ })
495
+ .superRefine(periodRefine),
496
+ );
492
497
 
493
498
  export type CollectionPatchRequest = z.infer<typeof collectionPatchRequestSchema>;
494
499
 
495
500
  /** `PUT /v1/collections/{collectionId}/products` — 담긴 상품을 통째로 바꾼다(순서 포함) */
496
- export const collectionProductsReplaceRequestSchema = z.object({
497
- productIds: productIdsField,
498
- revision: z.number().int().min(0).optional().describe("읽은 수정 번호"),
499
- });
501
+ export const collectionProductsReplaceRequestSchema = strictRequest(
502
+ z.object({
503
+ productIds: productIdsField,
504
+ revision: z.number().int().min(0).optional().describe("읽은 수정 번호"),
505
+ }),
506
+ );
500
507
 
501
508
  export type CollectionProductsReplaceRequest = z.infer<
502
509
  typeof collectionProductsReplaceRequestSchema
503
510
  >;
504
511
 
505
512
  /** `POST /v1/collections/{collectionId}/products` — 뒤에 붙인다. 이미 담긴 상품은 그 자리에 둔다 */
506
- export const collectionProductsAddRequestSchema = z.object({
507
- productIds: z.array(z.string().min(1)).min(1).max(MAX_COLLECTION_PRODUCTS_PER_ADD),
508
- });
513
+ export const collectionProductsAddRequestSchema = strictRequest(
514
+ z.object({
515
+ productIds: z.array(z.string().min(1)).min(1).max(MAX_COLLECTION_PRODUCTS_PER_ADD),
516
+ }),
517
+ );
509
518
 
510
519
  export type CollectionProductsAddRequest = z.infer<typeof collectionProductsAddRequestSchema>;
511
520
 
@@ -562,14 +571,16 @@ export const collectionProductListQuerySchema = z.object({
562
571
  });
563
572
 
564
573
  /** `POST /v1/collections/preview` — 저장하지 않고 조건으로 나오는 상품을 본다 */
565
- export const collectionPreviewRequestSchema = z.object({
566
- rules: collectionRulesSchema,
567
- productSort: collectionProductSortRequestSchema
568
- .optional()
569
- .describe(
570
- "미리보기 순서. 생략하면 `LATEST`(판매량 상위 조건이 있으면 `BEST_SELLING`)이고, `MANUAL`이면 `LATEST`다",
571
- ),
572
- });
574
+ export const collectionPreviewRequestSchema = strictRequest(
575
+ z.object({
576
+ rules: collectionRulesSchema,
577
+ productSort: collectionProductSortRequestSchema
578
+ .optional()
579
+ .describe(
580
+ "미리보기 순서. 생략하면 `LATEST`(판매량 상위 조건이 있으면 `BEST_SELLING`)이고, `MANUAL`이면 `LATEST`다",
581
+ ),
582
+ }),
583
+ );
573
584
 
574
585
  export type CollectionPreviewRequest = z.infer<typeof collectionPreviewRequestSchema>;
575
586