@sayren/store-sdk 0.19.0 → 0.21.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/index.d.mts +2010 -1314
- package/dist/index.mjs +1283 -1028
- package/package.json +1 -1
- package/src/client.ts +165 -93
- package/src/index.ts +1 -2
- package/src/schemas/claim.ts +1 -1
- package/src/schemas/fulfillment.ts +26 -275
- package/src/schemas/misc.ts +1 -1
- package/src/schemas/order.ts +25 -61
- package/src/schemas/product.ts +234 -7
- package/src/schemas/promotion.ts +3 -1
- package/src/schemas/saved-option-set.ts +168 -0
- package/src/schemas/store.ts +2 -30
- package/src/schemas/storefront-hosting.ts +576 -3
- package/src/schemas/upload.ts +2 -48
- package/src/schemas/webhook-event.ts +28 -14
- package/src/schemas/digital-asset.ts +0 -65
- package/src/schemas/fulfillment-code.ts +0 -143
package/src/schemas/product.ts
CHANGED
|
@@ -76,10 +76,188 @@ export const productVariantSchema = z.object({
|
|
|
76
76
|
weightGram: z.number().int().nullable(),
|
|
77
77
|
sellerCode: z.string().nullable(),
|
|
78
78
|
usable: z.boolean(),
|
|
79
|
+
hasOrders: z
|
|
80
|
+
.boolean()
|
|
81
|
+
.optional()
|
|
82
|
+
.describe(
|
|
83
|
+
"주문에 담긴 적이 있는 조합인가(이슈 #59). true인 조합은 옵션 교체에서 뺄 수 없고(409 `OPTION_HAS_ORDERS`) `usable: false`로 판매를 끈다. 서버는 항상 싣는다. 요청에는 쓰지 않는다",
|
|
84
|
+
),
|
|
79
85
|
});
|
|
80
86
|
|
|
81
87
|
export type ProductVariant = z.infer<typeof productVariantSchema>;
|
|
82
88
|
|
|
89
|
+
/** 옵션 축(그룹) 최대 개수 — 스키마 상한의 단일 원천 */
|
|
90
|
+
export const MAX_OPTION_GROUPS = 3;
|
|
91
|
+
/** 축 하나가 가질 수 있는 값의 최대 개수 */
|
|
92
|
+
export const MAX_VALUES_PER_GROUP = 100;
|
|
93
|
+
/** 상품 하나가 가질 수 있는 variant(SKU) 최대 개수 */
|
|
94
|
+
export const MAX_VARIANTS = 500;
|
|
95
|
+
|
|
96
|
+
// ─── 추가 선택 옵션·직접 입력 항목 (이슈 #59) ─────────────────────
|
|
97
|
+
//
|
|
98
|
+
// 옵션명은 두 방식이다. **조합**(`optionGroups`)은 값의 조합이 판매 단위(variant)가 되고 재고·SKU·추가금이 조합에 붙는다.
|
|
99
|
+
// **추가 선택**(`addonGroups`)은 재고가 없고 값마다 추가금만 있다 — 구매자가 조합(옵션이 없으면 기본 판매 단위)을 고른 뒤
|
|
100
|
+
// 옵션명마다 값 하나를 더 고르고, 재고는 고른 조합에서 빠진다. 직접 입력 항목(`customInputs`)은 구매자가 글자를 적는 칸이다.
|
|
101
|
+
|
|
102
|
+
/** 추가 선택 옵션명 최대 개수 */
|
|
103
|
+
export const MAX_ADDON_GROUPS = 5;
|
|
104
|
+
/** 직접 입력 항목 최대 개수 */
|
|
105
|
+
export const MAX_CUSTOM_INPUTS = 5;
|
|
106
|
+
/** 직접 입력 항목 최대 글자 수의 상한 */
|
|
107
|
+
export const MAX_CUSTOM_INPUT_LENGTH = 200;
|
|
108
|
+
/** 조합 수가 이 값 이상이면 콘솔이 경고한다(상한은 `MAX_VARIANTS`) */
|
|
109
|
+
export const VARIANT_COUNT_WARNING = 100;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* 옵션 값 정렬 순서 — 등록순·가나다순·낮은 추가금순·높은 추가금순. 서버가 저장할 때 이 순서로 값의 `sortOrder`를 매긴다.
|
|
113
|
+
* 추가금순은 추가 선택 값이면 값의 추가금, 조합 값이면 그 값이 든 조합 추가금의 최솟값으로 비교하고 같으면 등록순이다.
|
|
114
|
+
*/
|
|
115
|
+
export const optionSortSchema = z.enum(["REGISTERED", "NAME", "PRICE_ASC", "PRICE_DESC"]);
|
|
116
|
+
|
|
117
|
+
export type OptionSort = z.infer<typeof optionSortSchema>;
|
|
118
|
+
|
|
119
|
+
/** 추가 선택 값 — 예: 옵션명 "선물 포장"의 값 "포장함"(+2,000원) */
|
|
120
|
+
export const addonValueSchema = z.object({
|
|
121
|
+
valueId: z.string(),
|
|
122
|
+
name: z.string(),
|
|
123
|
+
sortOrder: z.number().int(),
|
|
124
|
+
additionalPrice: z.number().int().describe("이 값을 고르면 단가에 더하는 금액(원, 0 이상)"),
|
|
125
|
+
usable: z.boolean().describe("사용 여부. false면 구매자에게 보이지 않고 고를 수 없다"),
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
export type AddonValue = z.infer<typeof addonValueSchema>;
|
|
129
|
+
|
|
130
|
+
/** 추가 선택 옵션명 */
|
|
131
|
+
export const addonGroupSchema = z.object({
|
|
132
|
+
groupId: z.string(),
|
|
133
|
+
name: z.string(),
|
|
134
|
+
sortOrder: z.number().int(),
|
|
135
|
+
required: z.boolean().describe("구매자가 꼭 골라야 하는가. false면 고르지 않아도 된다"),
|
|
136
|
+
values: z.array(addonValueSchema),
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
export type AddonGroup = z.infer<typeof addonGroupSchema>;
|
|
140
|
+
|
|
141
|
+
/** 직접 입력 항목 — 예: 각인 문구 */
|
|
142
|
+
export const customInputSchema = z.object({
|
|
143
|
+
inputId: z.string(),
|
|
144
|
+
label: z.string().describe("항목명"),
|
|
145
|
+
placeholder: z.string().nullable().describe("입력 안내(구매자 입력 칸의 안내 문구). 없으면 null"),
|
|
146
|
+
maxLength: z.number().int().describe("최대 글자 수"),
|
|
147
|
+
required: z.boolean(),
|
|
148
|
+
usable: z.boolean().describe("사용 여부. false면 구매자에게 보이지 않는다"),
|
|
149
|
+
sortOrder: z.number().int(),
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
export type CustomInput = z.infer<typeof customInputSchema>;
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* 직접 입력 항목명에 쓸 수 없는 개인정보성 단어 — 콘솔 검사와 서버 400(`CUSTOM_INPUT_LABEL_FORBIDDEN`)이 이 목록 하나를 쓴다.
|
|
156
|
+
* 구매자 개인정보는 주문 정보로 받는다. 직접 입력 항목으로 받으면 보관·파기 규칙 밖에 남는다.
|
|
157
|
+
*/
|
|
158
|
+
export const CUSTOM_INPUT_FORBIDDEN_WORDS = [
|
|
159
|
+
"연락처",
|
|
160
|
+
"전화",
|
|
161
|
+
"휴대폰",
|
|
162
|
+
"핸드폰",
|
|
163
|
+
"폰번호",
|
|
164
|
+
"메일",
|
|
165
|
+
"주소",
|
|
166
|
+
"주민",
|
|
167
|
+
"생년월일",
|
|
168
|
+
"생일",
|
|
169
|
+
"실명",
|
|
170
|
+
"계좌",
|
|
171
|
+
"카드번호",
|
|
172
|
+
"비밀번호",
|
|
173
|
+
"여권",
|
|
174
|
+
"통관",
|
|
175
|
+
"phone",
|
|
176
|
+
"mobile",
|
|
177
|
+
"mail",
|
|
178
|
+
"address",
|
|
179
|
+
] as const;
|
|
180
|
+
|
|
181
|
+
/** 금지어 비교용 정규화 — NFKC(전각·호환 문자를 보통 문자로)·소문자 뒤 문자·숫자만 남긴다(공백·기호·제로폭 문자로 쪼갠 우회를 막는다) */
|
|
182
|
+
function forbiddenWordKeyOf(text: string): string {
|
|
183
|
+
return text
|
|
184
|
+
.normalize("NFKC")
|
|
185
|
+
.toLowerCase()
|
|
186
|
+
.replace(/[^\p{L}\p{N}]/gu, "");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* 직접 입력 항목명·입력 안내에 든 개인정보성 금지어. 없으면 null. 공백·기호·대소문자·전각을 무시한다.
|
|
191
|
+
* 서버는 항목명과 입력 안내 둘 다 이 함수로 검사한다(안내에 「연락처를 적어 주십시오」라고 쓰는 우회를 막는다).
|
|
192
|
+
*/
|
|
193
|
+
export function customInputLabelForbiddenWord(label: string): string | null {
|
|
194
|
+
const normalized = forbiddenWordKeyOf(label);
|
|
195
|
+
return (
|
|
196
|
+
CUSTOM_INPUT_FORBIDDEN_WORDS.find((word) => normalized.includes(forbiddenWordKeyOf(word))) ??
|
|
197
|
+
null
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** 옵션명·값 이름 — 앞뒤 공백을 지우고 빈 값은 받지 않는다 */
|
|
202
|
+
const optionNameSchema = z.string().trim().min(1).max(50);
|
|
203
|
+
|
|
204
|
+
export const addonValueRequestSchema = z.object({
|
|
205
|
+
/** 전달 시 기존 값 행을 유지(UPDATE). 생략하면 신규 발급 */
|
|
206
|
+
valueId: z.string().optional(),
|
|
207
|
+
name: optionNameSchema,
|
|
208
|
+
additionalPrice: z.number().int().min(0).default(0).describe("추가금(원, 0 이상)"),
|
|
209
|
+
usable: z.boolean().default(true),
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
export type AddonValueRequest = z.infer<typeof addonValueRequestSchema>;
|
|
213
|
+
|
|
214
|
+
export const addonGroupRequestSchema = z.object({
|
|
215
|
+
/** 전달 시 기존 옵션명 행을 유지(UPDATE). 조합 옵션명의 id는 줄 수 없다(방식을 바꾸려면 새 옵션명으로 보낸다) */
|
|
216
|
+
groupId: z.string().optional(),
|
|
217
|
+
name: optionNameSchema,
|
|
218
|
+
required: z.boolean().default(false),
|
|
219
|
+
values: z.array(addonValueRequestSchema).min(1).max(MAX_VALUES_PER_GROUP),
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
export type AddonGroupRequest = z.infer<typeof addonGroupRequestSchema>;
|
|
223
|
+
|
|
224
|
+
export const customInputRequestSchema = z.object({
|
|
225
|
+
/** 전달 시 기존 항목 행을 유지(UPDATE). 생략하면 신규 발급 */
|
|
226
|
+
inputId: z.string().optional(),
|
|
227
|
+
label: z.string().trim().min(1).max(30),
|
|
228
|
+
placeholder: z.string().trim().max(100).nullish(),
|
|
229
|
+
maxLength: z.number().int().min(1).max(MAX_CUSTOM_INPUT_LENGTH),
|
|
230
|
+
required: z.boolean().default(false),
|
|
231
|
+
usable: z.boolean().default(true),
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
export type CustomInputRequest = z.infer<typeof customInputRequestSchema>;
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* 주문상품·장바구니·주문서 줄의 옵션 선택 스냅샷 한 칸 — 주문 시점 이름이다(상품을 고쳐도 바뀌지 않는다).
|
|
238
|
+
* 조합 옵션은 옵션명마다 한 칸이고 추가금은 조합 단위라 0이다(조합 추가금은 단가에 들어 있다). 추가 선택은 값의 추가금이다.
|
|
239
|
+
*/
|
|
240
|
+
export const optionSelectionSchema = z.object({
|
|
241
|
+
kind: z
|
|
242
|
+
.string()
|
|
243
|
+
.describe(
|
|
244
|
+
"옵션명 방식. 현재 값은 `combination`·`addon`이고 값이 추가될 수 있어 문자열로 받는다",
|
|
245
|
+
),
|
|
246
|
+
groupName: z.string(),
|
|
247
|
+
valueName: z.string(),
|
|
248
|
+
additionalPrice: z.number().int(),
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
export type OptionSelection = z.infer<typeof optionSelectionSchema>;
|
|
252
|
+
|
|
253
|
+
/** 구매자가 적은 직접 입력값 — 주문 시점 항목명과 값 */
|
|
254
|
+
export const customInputValueSchema = z.object({
|
|
255
|
+
label: z.string(),
|
|
256
|
+
value: z.string(),
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
export type CustomInputValue = z.infer<typeof customInputValueSchema>;
|
|
260
|
+
|
|
83
261
|
export const discountSchema = z.object({
|
|
84
262
|
productId: z.string(),
|
|
85
263
|
discountType: z.enum(["RATE", "AMOUNT"]),
|
|
@@ -104,6 +282,22 @@ export const productSchema = productSummarySchema.extend({
|
|
|
104
282
|
optionGroups: z.array(optionGroupSchema),
|
|
105
283
|
/** 모든 상품이 최소 1개 variant를 갖는다 (옵션 없으면 default variant 1개) */
|
|
106
284
|
variants: z.array(productVariantSchema),
|
|
285
|
+
addonGroups: z
|
|
286
|
+
.array(addonGroupSchema)
|
|
287
|
+
.optional()
|
|
288
|
+
.default([])
|
|
289
|
+
.describe("추가 선택 옵션명(이슈 #59). 없으면 빈 배열. 서버는 항상 싣는다"),
|
|
290
|
+
customInputs: z
|
|
291
|
+
.array(customInputSchema)
|
|
292
|
+
.optional()
|
|
293
|
+
.default([])
|
|
294
|
+
.describe(
|
|
295
|
+
"직접 입력 항목(이슈 #59). 사용하지 않는 항목도 싣는다. 없으면 빈 배열. 서버는 항상 싣는다",
|
|
296
|
+
),
|
|
297
|
+
optionSort: optionSortSchema
|
|
298
|
+
.optional()
|
|
299
|
+
.default("REGISTERED")
|
|
300
|
+
.describe("옵션 값 정렬 순서(이슈 #59). 서버는 항상 싣는다"),
|
|
107
301
|
fulfillment: productFulfillmentResponseSchema,
|
|
108
302
|
fulfillmentTypeLocked: z
|
|
109
303
|
.boolean()
|
|
@@ -187,13 +381,6 @@ export function productImageRejection(url: string): ProductImageRejection | null
|
|
|
187
381
|
return null;
|
|
188
382
|
}
|
|
189
383
|
|
|
190
|
-
/** 옵션 축(그룹) 최대 개수 — 스키마 상한의 단일 원천 */
|
|
191
|
-
export const MAX_OPTION_GROUPS = 3;
|
|
192
|
-
/** 축 하나가 가질 수 있는 값의 최대 개수 */
|
|
193
|
-
export const MAX_VALUES_PER_GROUP = 100;
|
|
194
|
-
/** 상품 하나가 가질 수 있는 variant(SKU) 최대 개수 */
|
|
195
|
-
export const MAX_VARIANTS = 500;
|
|
196
|
-
|
|
197
384
|
/**
|
|
198
385
|
* 식별 코드 — 빈 문자열·공백만 있는 값은 거부한다.
|
|
199
386
|
* `""`를 통과시키면 서버 중복 사전 검사(falsy skip)와 DB partial unique
|
|
@@ -244,6 +431,25 @@ export type ProductVariantRequest = z.infer<typeof productVariantRequestSchema>;
|
|
|
244
431
|
export const optionModelRequestSchema = z.object({
|
|
245
432
|
optionGroups: z.array(optionGroupRequestSchema).max(MAX_OPTION_GROUPS).default([]),
|
|
246
433
|
variants: z.array(productVariantRequestSchema).min(1).max(MAX_VARIANTS),
|
|
434
|
+
addonGroups: z
|
|
435
|
+
.array(addonGroupRequestSchema)
|
|
436
|
+
.max(MAX_ADDON_GROUPS)
|
|
437
|
+
.optional()
|
|
438
|
+
.describe(
|
|
439
|
+
"추가 선택 옵션명 전체(이슈 #59). 주면 통째로 바꾸고, 생략하면 등록은 없음·수정은 현재 값을 그대로 둔다. 끄려면 빈 배열을 보낸다. 기능이 열리기 전에는 비어 있지 않은 값이 409 `OPTION_ADDONS_UNAVAILABLE`이다",
|
|
440
|
+
),
|
|
441
|
+
customInputs: z
|
|
442
|
+
.array(customInputRequestSchema)
|
|
443
|
+
.max(MAX_CUSTOM_INPUTS)
|
|
444
|
+
.optional()
|
|
445
|
+
.describe(
|
|
446
|
+
"직접 입력 항목 전체(이슈 #59). 배열 순서가 표시 순서다. 주면 통째로 바꾸고, 생략하면 등록은 없음·수정은 현재 값을 그대로 둔다. 항목명에 개인정보성 단어(`CUSTOM_INPUT_FORBIDDEN_WORDS`)가 있으면 400 `CUSTOM_INPUT_LABEL_FORBIDDEN`, 기능이 열리기 전에는 비어 있지 않은 값이 409 `OPTION_ADDONS_UNAVAILABLE`이다",
|
|
447
|
+
),
|
|
448
|
+
optionSort: optionSortSchema
|
|
449
|
+
.optional()
|
|
450
|
+
.describe(
|
|
451
|
+
"옵션 값 정렬 순서(이슈 #59). 서버가 이 순서로 값의 `sortOrder`를 다시 매긴다. 생략하면 등록은 `REGISTERED`, 수정은 현재 값이다",
|
|
452
|
+
),
|
|
247
453
|
});
|
|
248
454
|
|
|
249
455
|
export type OptionModelRequest = z.infer<typeof optionModelRequestSchema>;
|
|
@@ -252,6 +458,20 @@ export type OptionModelRequest = z.infer<typeof optionModelRequestSchema>;
|
|
|
252
458
|
export const optionModelSchema = z.object({
|
|
253
459
|
optionGroups: z.array(optionGroupSchema),
|
|
254
460
|
variants: z.array(productVariantSchema),
|
|
461
|
+
addonGroups: z
|
|
462
|
+
.array(addonGroupSchema)
|
|
463
|
+
.optional()
|
|
464
|
+
.default([])
|
|
465
|
+
.describe("추가 선택 옵션명(이슈 #59). 없으면 빈 배열. 서버는 항상 싣는다"),
|
|
466
|
+
customInputs: z
|
|
467
|
+
.array(customInputSchema)
|
|
468
|
+
.optional()
|
|
469
|
+
.default([])
|
|
470
|
+
.describe("직접 입력 항목(이슈 #59). 없으면 빈 배열. 서버는 항상 싣는다"),
|
|
471
|
+
optionSort: optionSortSchema
|
|
472
|
+
.optional()
|
|
473
|
+
.default("REGISTERED")
|
|
474
|
+
.describe("옵션 값 정렬 순서(이슈 #59). 서버는 항상 싣는다"),
|
|
255
475
|
});
|
|
256
476
|
|
|
257
477
|
export type OptionModel = z.infer<typeof optionModelSchema>;
|
|
@@ -322,6 +542,13 @@ const productWriteShape = {
|
|
|
322
542
|
"반품·교환 배송비(`returnDeliveryFee`·`exchangeDeliveryFee`)는 배송 상품에만 줄 수 있다(다른 유형은 400)",
|
|
323
543
|
),
|
|
324
544
|
...optionModelRequestSchema.shape,
|
|
545
|
+
sourceOptionSetIds: z
|
|
546
|
+
.array(z.string())
|
|
547
|
+
.max(10)
|
|
548
|
+
.optional()
|
|
549
|
+
.describe(
|
|
550
|
+
"쓰기 전용(이슈 #59) — 이 저장으로 옵션을 불러온 저장 옵션 id. 상품 저장과 같은 트랜잭션에서 불러오기 기록을 남겨 저장 옵션의 `importedProductCount`를 센다. 없는 id는 무시한다",
|
|
551
|
+
),
|
|
325
552
|
};
|
|
326
553
|
|
|
327
554
|
export const productCreateRequestSchema = z.strictObject({
|
package/src/schemas/promotion.ts
CHANGED
|
@@ -158,6 +158,7 @@ export const couponRejectReasonSchema = z
|
|
|
158
158
|
"NO_ELIGIBLE_ITEMS",
|
|
159
159
|
"MIN_ORDER_AMOUNT",
|
|
160
160
|
"LINE_NOT_ELIGIBLE",
|
|
161
|
+
"LINE_AMBIGUOUS",
|
|
161
162
|
"NO_DISCOUNT",
|
|
162
163
|
])
|
|
163
164
|
.describe(
|
|
@@ -165,7 +166,8 @@ export const couponRejectReasonSchema = z
|
|
|
165
166
|
"`STACK_LIMIT`=같은 종류의 쿠폰이 이미 적용됨(주문 쿠폰·배송비 쿠폰은 주문당 1장, 상품 쿠폰은 주문상품 한 줄에 1장), " +
|
|
166
167
|
"`NOT_STARTED`=사용 기간 전, `EXPIRED`=사용 기간 지남, `MEMBER_ONLY`=회원 전용, `FIRST_ORDER_ONLY`=첫 구매 전용, " +
|
|
167
168
|
"`NO_ELIGIBLE_ITEMS`=대상 상품이 주문에 없음, `MIN_ORDER_AMOUNT`=최소 주문 금액 미달, " +
|
|
168
|
-
"`LINE_NOT_ELIGIBLE`=지정한 주문상품이 대상이 아니거나 이미 상품 쿠폰이 있음,
|
|
169
|
+
"`LINE_NOT_ELIGIBLE`=지정한 주문상품이 대상이 아니거나 이미 상품 쿠폰이 있음, " +
|
|
170
|
+
"`LINE_AMBIGUOUS`=지정한 `optionId`의 줄이 여럿이라 줄을 정할 수 없음(`lineId`로 지목한다), `NO_DISCOUNT`=할인할 금액이 없음",
|
|
169
171
|
);
|
|
170
172
|
export type CouponRejectReason = z.infer<typeof couponRejectReasonSchema>;
|
|
171
173
|
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { type PageParams, pageQueryShape } from "./common";
|
|
3
|
+
import {
|
|
4
|
+
MAX_ADDON_GROUPS,
|
|
5
|
+
MAX_CUSTOM_INPUT_LENGTH,
|
|
6
|
+
MAX_CUSTOM_INPUTS,
|
|
7
|
+
MAX_OPTION_GROUPS,
|
|
8
|
+
MAX_VALUES_PER_GROUP,
|
|
9
|
+
optionSortSchema,
|
|
10
|
+
} from "./product";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 저장 옵션 (이슈 #59) — 자주 쓰는 옵션 구성을 이름 붙여 두고 상품 등록에서 불러온다.
|
|
14
|
+
*
|
|
15
|
+
* **불러오기는 복사다.** 저장 옵션을 고치거나 지워도 이미 불러온 상품의 옵션은 그대로다. 재고·SKU·사용 여부는 상품마다
|
|
16
|
+
* 달라 저장하지 않는다. 추가금은 조합별이 아니라 **값별**로 둔다 — 조합 옵션으로 불러오면 조합 추가금은 고른 값 추가금의 합이다.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** 스토어당 저장 옵션 최대 개수 */
|
|
20
|
+
export const MAX_SAVED_OPTION_SETS = 200;
|
|
21
|
+
/** 저장 옵션 이름 최대 글자 수 */
|
|
22
|
+
export const MAX_SAVED_OPTION_SET_NAME = 50;
|
|
23
|
+
|
|
24
|
+
/** 옵션명 방식 — `combination`(조합: 값의 조합이 판매 단위가 된다)·`addon`(추가 선택: 재고 없이 추가금만) */
|
|
25
|
+
export const optionGroupKindSchema = z.enum(["combination", "addon"]);
|
|
26
|
+
|
|
27
|
+
export type OptionGroupKind = z.infer<typeof optionGroupKindSchema>;
|
|
28
|
+
|
|
29
|
+
const nameSchema = z.string().trim().min(1).max(50);
|
|
30
|
+
|
|
31
|
+
export const savedOptionValueSchema = z.object({
|
|
32
|
+
name: z.string(),
|
|
33
|
+
additionalPrice: z.number().int().describe("값별 추가금(원, 0 이상)"),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export const savedOptionGroupSchema = z.object({
|
|
37
|
+
name: z.string(),
|
|
38
|
+
kind: z
|
|
39
|
+
.string()
|
|
40
|
+
.describe(
|
|
41
|
+
"옵션명 방식. 현재 값은 `combination`·`addon`이고 값이 추가될 수 있어 문자열로 받는다",
|
|
42
|
+
),
|
|
43
|
+
required: z.boolean().describe("추가 선택을 구매자가 꼭 고르는가. 조합 옵션명은 항상 false다"),
|
|
44
|
+
values: z.array(savedOptionValueSchema),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
export type SavedOptionGroup = z.infer<typeof savedOptionGroupSchema>;
|
|
48
|
+
|
|
49
|
+
export const savedCustomInputSchema = z.object({
|
|
50
|
+
label: z.string(),
|
|
51
|
+
placeholder: z.string().nullable(),
|
|
52
|
+
maxLength: z.number().int(),
|
|
53
|
+
required: z.boolean(),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
export type SavedCustomInput = z.infer<typeof savedCustomInputSchema>;
|
|
57
|
+
|
|
58
|
+
export const savedOptionSetSchema = z.object({
|
|
59
|
+
optionSetId: z.string(),
|
|
60
|
+
name: z.string(),
|
|
61
|
+
optionSort: optionSortSchema,
|
|
62
|
+
groups: z.array(savedOptionGroupSchema).describe("옵션명. 배열 순서가 입력 순서다"),
|
|
63
|
+
customInputs: z.array(savedCustomInputSchema),
|
|
64
|
+
importedProductCount: z
|
|
65
|
+
.number()
|
|
66
|
+
.int()
|
|
67
|
+
.describe("이 저장 옵션을 불러온 상품 수(중복 없이, 지운 상품 제외). 불러오기 기록으로 센다"),
|
|
68
|
+
createdAt: z.string(),
|
|
69
|
+
updatedAt: z.string(),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
export type SavedOptionSet = z.infer<typeof savedOptionSetSchema>;
|
|
73
|
+
|
|
74
|
+
export const savedOptionValueRequestSchema = z.object({
|
|
75
|
+
name: nameSchema,
|
|
76
|
+
additionalPrice: z.number().int().min(0).default(0),
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
export const savedOptionGroupRequestSchema = z.object({
|
|
80
|
+
name: nameSchema,
|
|
81
|
+
kind: optionGroupKindSchema,
|
|
82
|
+
required: z
|
|
83
|
+
.boolean()
|
|
84
|
+
.default(false)
|
|
85
|
+
.describe("추가 선택만 쓴다. 조합 옵션명에 true를 주면 400이다"),
|
|
86
|
+
values: z.array(savedOptionValueRequestSchema).min(1).max(MAX_VALUES_PER_GROUP),
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
export type SavedOptionGroupRequest = z.infer<typeof savedOptionGroupRequestSchema>;
|
|
90
|
+
|
|
91
|
+
export const savedCustomInputRequestSchema = z.object({
|
|
92
|
+
label: z.string().trim().min(1).max(30),
|
|
93
|
+
placeholder: z.string().trim().max(100).nullish(),
|
|
94
|
+
maxLength: z.number().int().min(1).max(MAX_CUSTOM_INPUT_LENGTH),
|
|
95
|
+
required: z.boolean().default(false),
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 저장 옵션 만들기·수정 본문. 조합 옵션명은 최대 `MAX_OPTION_GROUPS`개, 추가 선택은 최대 `MAX_ADDON_GROUPS`개이고
|
|
100
|
+
* 옵션명과 직접 입력 항목이 모두 비면 400이다. 수정(PUT)은 통째로 바꾼다.
|
|
101
|
+
*/
|
|
102
|
+
export const savedOptionSetRequestSchema = z.object({
|
|
103
|
+
name: z.string().trim().min(1).max(MAX_SAVED_OPTION_SET_NAME),
|
|
104
|
+
optionSort: optionSortSchema.default("REGISTERED"),
|
|
105
|
+
groups: z
|
|
106
|
+
.array(savedOptionGroupRequestSchema)
|
|
107
|
+
.max(MAX_OPTION_GROUPS + MAX_ADDON_GROUPS)
|
|
108
|
+
.default([]),
|
|
109
|
+
customInputs: z.array(savedCustomInputRequestSchema).max(MAX_CUSTOM_INPUTS).default([]),
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
export type SavedOptionSetRequest = z.infer<typeof savedOptionSetRequestSchema>;
|
|
113
|
+
export type SavedOptionSetRequestInput = z.input<typeof savedOptionSetRequestSchema>;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 상품에서 저장 (`POST /v1/saved-option-sets/from-product`) — 상품의 현재 옵션을 저장 옵션으로 만든다.
|
|
117
|
+
*
|
|
118
|
+
* 조합 추가금이 값별 추가금의 합으로 되돌려지면 값별로 저장하고, 조합마다 제각각이면 추가금을 빼고
|
|
119
|
+
* 응답 `additionalPriceDropped: true`로 알린다. `replaceOptionSetId`를 주면 그 저장 옵션을 덮어쓴다(이름 중복의 덮어쓰기).
|
|
120
|
+
*/
|
|
121
|
+
export const savedOptionSetFromProductRequestSchema = z.object({
|
|
122
|
+
productId: z.string(),
|
|
123
|
+
name: z.string().trim().min(1).max(MAX_SAVED_OPTION_SET_NAME),
|
|
124
|
+
includeOptions: z.boolean().default(true).describe("옵션명(조합·추가 선택)을 저장하는가"),
|
|
125
|
+
includeCustomInputs: z.boolean().default(true).describe("직접 입력 항목을 저장하는가"),
|
|
126
|
+
replaceOptionSetId: z.string().optional().describe("덮어쓸 저장 옵션 id. 생략하면 새로 만든다"),
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
export type SavedOptionSetFromProductRequest = z.infer<
|
|
130
|
+
typeof savedOptionSetFromProductRequestSchema
|
|
131
|
+
>;
|
|
132
|
+
export type SavedOptionSetFromProductRequestInput = z.input<
|
|
133
|
+
typeof savedOptionSetFromProductRequestSchema
|
|
134
|
+
>;
|
|
135
|
+
|
|
136
|
+
export const savedOptionSetFromProductResultSchema = z.object({
|
|
137
|
+
optionSet: savedOptionSetSchema,
|
|
138
|
+
additionalPriceDropped: z
|
|
139
|
+
.boolean()
|
|
140
|
+
.describe("조합 추가금을 값별 추가금으로 되돌릴 수 없어 추가금을 빼고 저장했는가"),
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
export type SavedOptionSetFromProductResult = z.infer<typeof savedOptionSetFromProductResultSchema>;
|
|
144
|
+
|
|
145
|
+
/** 구성 필터 — 조합 옵션명·추가 선택 옵션명·직접 입력 항목을 가진 저장 옵션 */
|
|
146
|
+
export const savedOptionSetCompositionSchema = z.enum(["combination", "addon", "customInput"]);
|
|
147
|
+
|
|
148
|
+
export const savedOptionSetSortSchema = z.enum([
|
|
149
|
+
"updatedAt.desc",
|
|
150
|
+
"name.asc",
|
|
151
|
+
"importedProductCount.desc",
|
|
152
|
+
]);
|
|
153
|
+
|
|
154
|
+
export type SavedOptionSetSort = z.infer<typeof savedOptionSetSortSchema>;
|
|
155
|
+
|
|
156
|
+
export interface SavedOptionSetListParams extends PageParams {
|
|
157
|
+
keyword?: string;
|
|
158
|
+
composition?: z.infer<typeof savedOptionSetCompositionSchema>;
|
|
159
|
+
sort?: SavedOptionSetSort;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** `GET /v1/saved-option-sets` 쿼리 — OpenAPI 문서용 형태(값은 쿼리 문자열) */
|
|
163
|
+
export const savedOptionSetListQuerySchema = z.object({
|
|
164
|
+
keyword: z.string().optional().describe("이름 검색 (포함, 대소문자 무시)"),
|
|
165
|
+
composition: savedOptionSetCompositionSchema.optional().describe("구성 필터"),
|
|
166
|
+
sort: savedOptionSetSortSchema.optional().describe("정렬 (기본 updatedAt.desc)"),
|
|
167
|
+
...pageQueryShape({ defaultSize: 20, maxSize: 200 }),
|
|
168
|
+
});
|
package/src/schemas/store.ts
CHANGED
|
@@ -166,22 +166,6 @@ export const updateStoreRequestSchema = z
|
|
|
166
166
|
"비회원 주문 허용. false(회원만)면 로그인하지 않은 구매자의 주문서 생성·결제 요청이 403 `GUEST_CHECKOUT_DISABLED`다. " +
|
|
167
167
|
"장바구니 담기와 이미 받은 비회원 주문의 조회·취소는 막지 않는다",
|
|
168
168
|
),
|
|
169
|
-
fulfillmentMailEnabled: z
|
|
170
|
-
.boolean()
|
|
171
|
-
.optional()
|
|
172
|
-
.describe(
|
|
173
|
-
"비실물 상품 제공 안내 메일(이슈 #48). 켜면(기본) 제공이 끝났을 때 구매자에게 안내 메일을 보낸다. 코드·파일 주소는 싣지 않는다",
|
|
174
|
-
),
|
|
175
|
-
withdrawalNotice: z
|
|
176
|
-
.string()
|
|
177
|
-
.trim()
|
|
178
|
-
.min(1)
|
|
179
|
-
.max(1000)
|
|
180
|
-
.nullable()
|
|
181
|
-
.optional()
|
|
182
|
-
.describe(
|
|
183
|
-
"청약철회 제한 고지 문구(이슈 #48). 제한 상품이 있는 주문서에 보인다. null이면 플랫폼 기본 문구를 쓴다",
|
|
184
|
-
),
|
|
185
169
|
autoDeliverDays: z
|
|
186
170
|
.number()
|
|
187
171
|
.int()
|
|
@@ -247,17 +231,6 @@ export const storeSchema = z.object({
|
|
|
247
231
|
"스토어프론트 도메인 — 소셜 로그인 리디렉트와 방문 분석 수집이 함께 쓴다(`analyticsAllowedOrigins`와 같은 값)",
|
|
248
232
|
),
|
|
249
233
|
guestCheckoutEnabled: z.boolean().describe("비회원 주문 허용. false면 회원만 주문할 수 있다"),
|
|
250
|
-
fulfillmentMailEnabled: z
|
|
251
|
-
.boolean()
|
|
252
|
-
.optional()
|
|
253
|
-
.default(true)
|
|
254
|
-
.describe("비실물 상품 제공 안내 메일(이슈 #48). 서버는 항상 싣는다"),
|
|
255
|
-
withdrawalNotice: z
|
|
256
|
-
.string()
|
|
257
|
-
.nullable()
|
|
258
|
-
.optional()
|
|
259
|
-
.default(null)
|
|
260
|
-
.describe("청약철회 제한 고지 문구(이슈 #48). null이면 플랫폼 기본 문구다. 서버는 항상 싣는다"),
|
|
261
234
|
autoDeliverDays: z.number().int().describe(autoDeliverDaysDescription),
|
|
262
235
|
autoConfirmDays: z.number().int().describe(autoConfirmDaysDescription),
|
|
263
236
|
refundPolicy: refundPolicySchema.describe(
|
|
@@ -361,10 +334,9 @@ export const storeMemberRoleUpdatedSchema = z.object({
|
|
|
361
334
|
export type StoreMemberRoleUpdated = z.infer<typeof storeMemberRoleUpdatedSchema>;
|
|
362
335
|
|
|
363
336
|
/**
|
|
364
|
-
* 주소 유형 —
|
|
365
|
-
* (이 값을 모르는 옛 SDK의 목록 파싱이 깨지지 않게).
|
|
337
|
+
* 주소 유형 — 출고지·반품지.
|
|
366
338
|
*/
|
|
367
|
-
export const addressTypeSchema = z.enum(["SHIPPING", "RETURN"
|
|
339
|
+
export const addressTypeSchema = z.enum(["SHIPPING", "RETURN"]);
|
|
368
340
|
|
|
369
341
|
export type AddressType = z.infer<typeof addressTypeSchema>;
|
|
370
342
|
|