@sayren/store-sdk 0.33.0 → 0.34.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.
@@ -0,0 +1,190 @@
1
+ import { z } from "zod";
2
+
3
+ /**
4
+ * 상품정보제공 고시·인증 정보·안내 섹션 (이슈 #135).
5
+ *
6
+ * **코드 값은 문자열이다.** 품목군(`schemaId`)·인증 종류·대상·면제 사유는 플랫폼 코드 표(서버 DB)가 원천이고, 코드가 늘어도 이 SDK의
7
+ * 계약이 깨지지 않게 zod enum으로 막지 않는다. 서버가 코드 표에 있는지 검사한다(없으면 400 `INVALID_INFO_CODE`).
8
+ * enum으로 두는 값은 서버가 그 값으로 분기하는 인증 상태(`CERTIFICATION_STATUSES`)뿐이다.
9
+ */
10
+
11
+ // ── 플랫폼 코드 표 ────────────────────────────────────────────────
12
+
13
+ /**
14
+ * 칸 값의 출처 — 셀러가 비운 칸을 서버가 채운다. 서버가 분기하는 값이라 형태가 정해져 있지만, 새 출처가 늘 수 있어 응답은 열린 객체다.
15
+ * - `legal`: 법정 문구(`text`). 셀러가 적지 않는다
16
+ * - `option`: 이름이 `names` 중 하나인 조합 옵션명의 값 목록
17
+ * - `store`: 상점 필드(`field`, 예: `customerCenterPhone`)
18
+ */
19
+ export const infoFieldSourceSchema = z.looseObject({
20
+ type: z.string(),
21
+ text: z.string().optional(),
22
+ names: z.array(z.string()).optional(),
23
+ field: z.string().optional(),
24
+ });
25
+
26
+ export type InfoFieldSource = z.infer<typeof infoFieldSourceSchema>;
27
+
28
+ export const infoSchemaFieldSchema = z.object({
29
+ key: z.string(),
30
+ label: z.string(),
31
+ required: z
32
+ .boolean()
33
+ .describe("법정 필수 칸인가. 서버는 비어 있어도 막지 않는다(표시는 「상품상세 참조」)"),
34
+ maxLength: z.number().int(),
35
+ source: infoFieldSourceSchema.nullable().optional().describe("값 출처. 없으면 셀러가 적는 칸"),
36
+ });
37
+
38
+ export type InfoSchemaField = z.infer<typeof infoSchemaFieldSchema>;
39
+
40
+ /** 정형 정보 스키마 — 고시 품목군 하나(`kind: "notice"`). 칸 구성은 버전마다 고정이고 상품은 저장한 버전으로 표시된다 */
41
+ export const infoSchemaSchema = z.object({
42
+ schemaId: z.string().describe("품목군 코드(예: `WEAR`)"),
43
+ version: z.number().int(),
44
+ kind: z.string().describe("스키마 종류. 지금은 `notice`(상품정보제공 고시)"),
45
+ name: z.string(),
46
+ fallback: z.string().describe("비운 칸의 표시 문구(예: 「상품상세 참조」)"),
47
+ fields: z.array(infoSchemaFieldSchema).describe("칸. 배열 순서가 표시 순서다"),
48
+ });
49
+
50
+ export type InfoSchema = z.infer<typeof infoSchemaSchema>;
51
+
52
+ /** 플랫폼 코드 하나 — 코드 묶음(`setId`) 안에서 유일하다. `attrs`는 묶음마다 다른 부가 정보다 */
53
+ export const platformCodeSchema = z.object({
54
+ code: z.string(),
55
+ label: z.string(),
56
+ attrs: z
57
+ .record(z.string(), z.unknown())
58
+ .describe(
59
+ "부가 정보. `certification.subject`는 `kinds`(이 대상을 충족하는 인증 종류)·`exemptions`(허용 면제 사유)를 싣는다",
60
+ ),
61
+ });
62
+
63
+ export type PlatformCode = z.infer<typeof platformCodeSchema>;
64
+
65
+ /** 코드 묶음 — `GET /v1/codes/{setId}` */
66
+ export const PLATFORM_CODE_SETS = {
67
+ certificationKind: "certification.kind",
68
+ certificationSubject: "certification.subject",
69
+ certificationExemption: "certification.exemption",
70
+ } as const;
71
+
72
+ // ── 상품 고시 ────────────────────────────────────────────────────
73
+
74
+ export const productNoticeSchema = z.object({
75
+ schemaId: z.string(),
76
+ version: z.number().int(),
77
+ values: z.record(z.string(), z.string()).describe("칸 키 → 셀러가 적은 값. 비운 칸은 없다"),
78
+ });
79
+
80
+ export type ProductNotice = z.infer<typeof productNoticeSchema>;
81
+
82
+ export const MAX_NOTICE_VALUE_LENGTH = 1500;
83
+
84
+ export const productNoticeRequestSchema = z.object({
85
+ schemaId: z.string().trim().min(1).max(50).describe("품목군 코드(`GET /v1/info-schemas`)"),
86
+ version: z.number().int().positive().optional().describe("품목군 버전. 생략하면 현재 버전"),
87
+ values: z
88
+ .record(z.string().max(50), z.string().max(MAX_NOTICE_VALUE_LENGTH))
89
+ .default({})
90
+ .describe(
91
+ "칸 키 → 값. 출처가 `legal`인 칸은 받지 않는다(400). 빈 문자열은 비운 칸으로 저장하지 않는다. 칸 길이는 스키마 `maxLength`",
92
+ ),
93
+ });
94
+
95
+ export type ProductNoticeRequest = z.infer<typeof productNoticeRequestSchema>;
96
+
97
+ // ── 상품 인증 ────────────────────────────────────────────────────
98
+
99
+ /** 인증 대상 상태 — 서버가 분기한다(대상이면 인증서 필수, 면제면 사유 필수) */
100
+ export const CERTIFICATION_STATUSES = ["REQUIRED", "NOT_REQUIRED", "EXEMPT"] as const;
101
+
102
+ export type CertificationStatus = (typeof CERTIFICATION_STATUSES)[number];
103
+
104
+ export const MAX_CERTIFICATION_ITEMS = 20;
105
+
106
+ export const productCertificationsSchema = z.object({
107
+ subjects: z
108
+ .array(
109
+ z.object({
110
+ subject: z.string().describe("인증 대상 코드(`certification.subject`)"),
111
+ status: z
112
+ .string()
113
+ .describe("`REQUIRED`·`NOT_REQUIRED`·`EXEMPT`. 값이 늘 수 있어 문자열이다"),
114
+ exemption: z.string().nullable().describe("면제 사유 코드. `EXEMPT`일 때만 있다"),
115
+ }),
116
+ )
117
+ .describe("인증 대상 여부. 없는 대상은 `NOT_REQUIRED`다"),
118
+ items: z
119
+ .array(
120
+ z.object({
121
+ kind: z.string().describe("인증 종류 코드(`certification.kind`)"),
122
+ agency: z.string().nullable().describe("인증 기관"),
123
+ number: z.string().describe("인증 번호"),
124
+ }),
125
+ )
126
+ .describe("인증서. 배열 순서가 표시 순서다"),
127
+ });
128
+
129
+ export type ProductCertifications = z.infer<typeof productCertificationsSchema>;
130
+
131
+ export const productCertificationsRequestSchema = z.object({
132
+ subjects: z
133
+ .array(
134
+ z.object({
135
+ subject: z.string().trim().min(1).max(50),
136
+ status: z.enum(CERTIFICATION_STATUSES),
137
+ exemption: z.string().trim().min(1).max(50).nullish(),
138
+ }),
139
+ )
140
+ .max(20)
141
+ .default([])
142
+ .describe(
143
+ "인증 대상 여부. `REQUIRED`인데 그 대상의 인증 종류(`attrs.kinds`) 인증서가 `items`에 없으면 400 `CERTIFICATION_REQUIRED`, " +
144
+ "`EXEMPT`인데 사유가 없거나 대상이 허용하지 않는 사유면 400 `CERTIFICATION_EXEMPTION_INVALID`",
145
+ ),
146
+ items: z
147
+ .array(
148
+ z.object({
149
+ kind: z.string().trim().min(1).max(50),
150
+ agency: z.string().trim().max(100).nullish(),
151
+ number: z.string().trim().min(1).max(100),
152
+ }),
153
+ )
154
+ .max(MAX_CERTIFICATION_ITEMS)
155
+ .default([]),
156
+ });
157
+
158
+ export type ProductCertificationsRequest = z.infer<typeof productCertificationsRequestSchema>;
159
+ export type ProductCertificationsRequestInput = z.input<typeof productCertificationsRequestSchema>;
160
+
161
+ // ── 안내 섹션 ────────────────────────────────────────────────────
162
+
163
+ /** 상품 하나에 연결할 수 있는 안내 섹션 수 */
164
+ export const MAX_PRODUCT_INFO_SECTIONS = 10;
165
+ export const MAX_INFO_SECTION_TITLE = 100;
166
+
167
+ /**
168
+ * 안내 섹션 — 사이즈 가이드·세탁 안내처럼 여러 상품이 같이 쓰는 글이다. **상품은 연결만 한다**(복사가 아니다).
169
+ * 섹션을 고치면 연결된 모든 상품의 표시가 바뀐다.
170
+ */
171
+ export const infoSectionSchema = z.object({
172
+ infoSectionId: z.string(),
173
+ title: z.string(),
174
+ body: z.string().describe("본문 HTML(상세설명과 같은 정화 규칙)"),
175
+ bodyJson: z.string().nullable().describe("에디터 복원용 직렬화 상태"),
176
+ assignedProductCount: z.number().int().describe("이 섹션을 연결한 상품 수"),
177
+ createdAt: z.string(),
178
+ updatedAt: z.string(),
179
+ });
180
+
181
+ export type InfoSection = z.infer<typeof infoSectionSchema>;
182
+
183
+ export const infoSectionRequestSchema = z.object({
184
+ title: z.string().trim().min(1).max(MAX_INFO_SECTION_TITLE),
185
+ body: z.string().default(""),
186
+ bodyJson: z.string().nullable().optional(),
187
+ });
188
+
189
+ export type InfoSectionRequest = z.infer<typeof infoSectionRequestSchema>;
190
+ export type InfoSectionRequestInput = z.input<typeof infoSectionRequestSchema>;
@@ -14,6 +14,13 @@ import {
14
14
  productFulfillmentUpdateSchema,
15
15
  } from "./fulfillment";
16
16
  import { pointEarnRateBpSchema } from "./points";
17
+ import {
18
+ MAX_PRODUCT_INFO_SECTIONS,
19
+ productCertificationsRequestSchema,
20
+ productCertificationsSchema,
21
+ productNoticeRequestSchema,
22
+ productNoticeSchema,
23
+ } from "./product-info";
17
24
  import { productRefundPolicyPatchSchema } from "./refund-policy";
18
25
  import { inventoryModeSchema } from "./stay";
19
26
  import { UPLOAD_PURPOSE_LIMITS, uploadExtensionOf } from "./upload";
@@ -445,6 +452,19 @@ export const productSchema = productSummarySchema.extend({
445
452
  .describe(
446
453
  "설정된 즉시할인. GET /v1/products/{productId}/discount 응답과 같은 형태다. 할인이 없으면 null",
447
454
  ),
455
+ notice: productNoticeSchema
456
+ .nullable()
457
+ .optional()
458
+ .describe("상품정보제공 고시(이슈 #135). 없으면 null. 서버는 항상 싣는다"),
459
+ certifications: productCertificationsSchema
460
+ .nullable()
461
+ .optional()
462
+ .describe("인증 정보(이슈 #135). 없으면 null. 서버는 항상 싣는다"),
463
+ infoSectionIds: z
464
+ .array(z.string())
465
+ .optional()
466
+ .default([])
467
+ .describe("연결한 안내 섹션 id(이슈 #135). 배열 순서가 표시 순서다. 서버는 항상 싣는다"),
448
468
  createdAt: z.string().optional(),
449
469
  });
450
470
 
@@ -569,14 +589,14 @@ export const optionModelRequestSchema = z.object({
569
589
  .max(MAX_ADDON_GROUPS)
570
590
  .optional()
571
591
  .describe(
572
- "추가 선택 옵션명 전체(이슈 #59). 주면 통째로 바꾸고, 생략하면 등록은 없음·수정은 현재 값을 그대로 둔다. 끄려면 빈 배열을 보낸다. 기능이 열리기 전에는 비어 있지 않은 값이 409 `OPTION_ADDONS_UNAVAILABLE`이다",
592
+ "추가 선택 옵션명 전체(이슈 #59). 주면 통째로 바꾸고, 생략하면 등록은 없음·수정은 현재 값을 그대로 둔다. 끄려면 빈 배열을 보낸다",
573
593
  ),
574
594
  customInputs: z
575
595
  .array(customInputRequestSchema)
576
596
  .max(MAX_CUSTOM_INPUTS)
577
597
  .optional()
578
598
  .describe(
579
- "직접 입력 항목 전체(이슈 #59). 배열 순서가 표시 순서다. 주면 통째로 바꾸고, 생략하면 등록은 없음·수정은 현재 값을 그대로 둔다. 항목명에 개인정보성 단어(`CUSTOM_INPUT_FORBIDDEN_WORDS`)가 있으면 400 `CUSTOM_INPUT_LABEL_FORBIDDEN`, 기능이 열리기 전에는 비어 있지 않은 값이 409 `OPTION_ADDONS_UNAVAILABLE`이다",
599
+ "직접 입력 항목 전체(이슈 #59). 배열 순서가 표시 순서다. 주면 통째로 바꾸고, 생략하면 등록은 없음·수정은 현재 값을 그대로 둔다. 항목명에 개인정보성 단어(`CUSTOM_INPUT_FORBIDDEN_WORDS`)가 있으면 400 `CUSTOM_INPUT_LABEL_FORBIDDEN`이다",
580
600
  ),
581
601
  optionSort: optionSortSchema
582
602
  .optional()
@@ -694,6 +714,25 @@ const productWriteShape = {
694
714
  "상품별 환불 정책 — 준 항목만 상점 기본을 덮어쓴다. `null`이나 생략은 상점 기본을 그대로 쓴다는 뜻이다. " +
695
715
  "반품·교환 배송비(`returnDeliveryFee`·`exchangeDeliveryFee`)는 배송 상품에만 줄 수 있다(다른 유형은 400)",
696
716
  ),
717
+ notice: productNoticeRequestSchema
718
+ .nullable()
719
+ .optional()
720
+ .describe(
721
+ "상품정보제공 고시(이슈 #135). `null`은 지운다. 등록에서 생략하면 없음, 전체 수정(PUT)에서 생략하면 지금 값을 유지한다",
722
+ ),
723
+ certifications: productCertificationsRequestSchema
724
+ .nullable()
725
+ .optional()
726
+ .describe(
727
+ "인증 정보(이슈 #135). `null`은 지운다. 등록에서 생략하면 없음, 전체 수정(PUT)에서 생략하면 지금 값을 유지한다",
728
+ ),
729
+ infoSectionIds: z
730
+ .array(z.string().min(1))
731
+ .max(MAX_PRODUCT_INFO_SECTIONS)
732
+ .optional()
733
+ .describe(
734
+ "연결할 안내 섹션 id(이슈 #135). 배열 순서가 표시 순서다. 이 상점의 섹션이 아니면 400 `INFO_SECTION_NOT_FOUND`. 전체 수정(PUT)에서 생략하면 지금 연결을 유지한다",
735
+ ),
697
736
  ...optionModelRequestSchema.shape,
698
737
  sourceOptionSetIds: z
699
738
  .array(z.string())
@@ -13,7 +13,6 @@ import { RESERVED_STORE_CODES } from "./store";
13
13
  * 공개 전환·슬러그 변경·호스팅 끄기는 승인(`APPROVAL_REQUIRED`)이다 — 구매자가 보는 화면을 바꾸는 쓰기다.
14
14
  *
15
15
  * 오류 코드:
16
- * - 403 `STOREFRONT_HOSTING_DISABLED` — 플랫폼 스위치가 꺼져 새 사이트를 만들 수 없다
17
16
  * - 404 `STOREFRONT_SITE_NOT_FOUND` · `STOREFRONT_TEMPLATE_NOT_FOUND` · `STOREFRONT_VERSION_NOT_FOUND`
18
17
  * - 409 `STOREFRONT_SITE_EXISTS` — 이미 사이트가 있다(상점당 1개)
19
18
  * - 409 `STOREFRONT_SLUG_TAKEN` — 다른 사이트가 쓰거나 보류 중인 슬러그다
@@ -30,7 +29,7 @@ import { RESERVED_STORE_CODES } from "./store";
30
29
  * - 503 `STOREFRONT_PREVIEW_NOT_CONFIGURED` — 미리보기 서명 키가 설정되지 않았다
31
30
  *
32
31
  * 편집(M2b, `/v1/storefront/site/editing`·`files/**`·`checks/**`·`verify`·`builds/**`, 버전 만들기):
33
- * - 403 `STOREFRONT_EDITING_DISABLED` — 플랫폼 편집 스위치가 꺼져 있다
32
+ * - 403 `STOREFRONT_EDITING_DISABLED` — 편집을 쓸 수 없어 셀러 템플릿으로 사이트를 만들 수 없다(사이트 만들기)
34
33
  * - 503 `STOREFRONT_EDITING_NOT_CONFIGURED` — 편집 Sandbox가 설정되지 않았다
35
34
  * - 409 `EDIT_CAPACITY_FULL` — 플랫폼 전체 동시 편집 상한에 걸렸다. `error.details.retryAfterSeconds` 뒤에 다시 한다
36
35
  * - 409 `EDIT_SESSION_REQUIRED` — 편집 미리보기 링크: 편집 환경이 없다
@@ -392,13 +391,11 @@ export const storefrontSiteSchema = z.object({
392
391
  });
393
392
  export type StorefrontSite = z.infer<typeof storefrontSiteSchema>;
394
393
 
395
- /** `GET /v1/storefront/site` — 스위치 상태와 현재 사이트(없으면 null. 끈 사이트는 싣지 않는다) */
394
+ /** `GET /v1/storefront/site` — 호스팅 설정과 현재 사이트(없으면 null. 끈 사이트는 싣지 않는다) */
396
395
  export const storefrontHostingSchema = z.object({
397
- enabled: z.boolean().describe("새 사이트를 만들 수 있는가 (플랫폼 스위치)"),
396
+ enabled: z.boolean().describe("새 사이트를 만들 수 있는가 (항상 true)"),
398
397
  domain: z.string().describe("호스팅 도메인 (예: `sayren.co`). 사이트 주소는 `{slug}.{domain}`"),
399
- editingEnabled: z
400
- .boolean()
401
- .describe("파일 편집·버전 만들기를 쓸 수 있는가 (플랫폼 편집 스위치와 Sandbox 설정)"),
398
+ editingEnabled: z.boolean().describe("파일 편집·버전 만들기를 쓸 수 있는가 (Sandbox 설정)"),
402
399
  site: storefrontSiteSchema.nullable(),
403
400
  });
404
401
  export type StorefrontHosting = z.infer<typeof storefrontHostingSchema>;
@@ -574,7 +571,6 @@ export type StorefrontPreviewLink = z.infer<typeof storefrontPreviewLinkSchema>;
574
571
  // - 결제 복귀·방문 분석·소셜 로그인 허용 목록은 연결된(`ACTIVE`) 도메인을 자동으로 더한다.
575
572
  //
576
573
  // 오류 코드:
577
- // - 403 `STOREFRONT_CUSTOM_DOMAINS_DISABLED` — 플랫폼 스위치가 꺼져 도메인을 추가할 수 없다
578
574
  // - 400 `STOREFRONT_DOMAIN_INVALID` — 형식이 틀렸거나 연결할 수 없는 도메인이다(`error.details.reason`)
579
575
  // - 404 `STOREFRONT_DOMAIN_NOT_FOUND`
580
576
  // - 409 `STOREFRONT_DOMAIN_EXISTS` — 이 상점에 이미 추가한 도메인이다
@@ -798,7 +794,7 @@ export type StorefrontSiteDomain = z.infer<typeof storefrontSiteDomainSchema>;
798
794
 
799
795
  /** `GET /v1/storefront/site/domains` — 사이트의 주소 목록(기본 주소 + 내 도메인)과 연결 설정 */
800
796
  export const storefrontSiteDomainsSchema = z.object({
801
- enabled: z.boolean().describe("내 도메인을 추가할 수 있는가 (플랫폼 스위치)"),
797
+ enabled: z.boolean().describe("내 도메인을 추가할 수 있는가 (항상 true)"),
802
798
  limit: z.number().int().min(0).describe("상점당 내 도메인 수 상한 (해제한 도메인은 세지 않는다)"),
803
799
  target: z.string().describe("CNAME 대상 (예: `sites.sayren.co`)"),
804
800
  domains: z.array(storefrontSiteDomainSchema).describe("대표 주소 먼저, 그다음 추가한 순서"),
@@ -971,7 +967,7 @@ export const storefrontDevLogsSchema = z.object({
971
967
  export type StorefrontDevLogs = z.infer<typeof storefrontDevLogsSchema>;
972
968
 
973
969
  export const storefrontEditingSchema = z.object({
974
- enabled: z.boolean().describe("편집을 쓸 수 있는가 (플랫폼 스위치와 Sandbox 설정)"),
970
+ enabled: z.boolean().describe("편집을 쓸 수 있는가 (Sandbox 설정)"),
975
971
  session: storefrontEditSessionSchema
976
972
  .nullable()
977
973
  .describe("열린 편집 환경. 잠들었거나 닫았으면 null"),