@sayren/store-sdk 0.20.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.mjs CHANGED
@@ -1258,8 +1258,9 @@ const couponRejectReasonSchema = z.enum([
1258
1258
  "NO_ELIGIBLE_ITEMS",
1259
1259
  "MIN_ORDER_AMOUNT",
1260
1260
  "LINE_NOT_ELIGIBLE",
1261
+ "LINE_AMBIGUOUS",
1261
1262
  "NO_DISCOUNT"
1262
- ]).describe("쿠폰을 적용하지 못한 사유. `DUPLICATE_COUPON`=같은 쿠폰을 두 번 넣음, `STACK_LIMIT`=같은 종류의 쿠폰이 이미 적용됨(주문 쿠폰·배송비 쿠폰은 주문당 1장, 상품 쿠폰은 주문상품 한 줄에 1장), `NOT_STARTED`=사용 기간 전, `EXPIRED`=사용 기간 지남, `MEMBER_ONLY`=회원 전용, `FIRST_ORDER_ONLY`=첫 구매 전용, `NO_ELIGIBLE_ITEMS`=대상 상품이 주문에 없음, `MIN_ORDER_AMOUNT`=최소 주문 금액 미달, `LINE_NOT_ELIGIBLE`=지정한 주문상품이 대상이 아니거나 이미 상품 쿠폰이 있음, `NO_DISCOUNT`=할인할 금액이 없음");
1263
+ ]).describe("쿠폰을 적용하지 못한 사유. `DUPLICATE_COUPON`=같은 쿠폰을 두 번 넣음, `STACK_LIMIT`=같은 종류의 쿠폰이 이미 적용됨(주문 쿠폰·배송비 쿠폰은 주문당 1장, 상품 쿠폰은 주문상품 한 줄에 1장), `NOT_STARTED`=사용 기간 전, `EXPIRED`=사용 기간 지남, `MEMBER_ONLY`=회원 전용, `FIRST_ORDER_ONLY`=첫 구매 전용, `NO_ELIGIBLE_ITEMS`=대상 상품이 주문에 없음, `MIN_ORDER_AMOUNT`=최소 주문 금액 미달, `LINE_NOT_ELIGIBLE`=지정한 주문상품이 대상이 아니거나 이미 상품 쿠폰이 있음, `LINE_AMBIGUOUS`=지정한 `optionId`의 줄이 여럿이라 줄을 정할 수 없음(`lineId`로 지목한다), `NO_DISCOUNT`=할인할 금액이 없음");
1263
1264
  /** 쿠폰 적용 뒤 결제 금액의 하한 — 0원 결제 경로가 없어 이보다 작으면 결제를 시작하지 않는다(400 `PAYMENT_AMOUNT_TOO_LOW`) */
1264
1265
  const MIN_PAYMENT_AMOUNT = 100;
1265
1266
  /**
@@ -2887,6 +2888,496 @@ const paymentListQuerySchema = z.object({
2887
2888
  size: z.string().optional().describe("페이지 크기 (기본 20, 최대 100)")
2888
2889
  });
2889
2890
  //#endregion
2891
+ //#region src/schemas/upload.ts
2892
+ /**
2893
+ * 업로드 용도. 허용 확장자와 크기 상한이 용도별로 다르다.
2894
+ *
2895
+ * - `product-image`: 상품 목록·상세의 상품 이미지. 목록 썸네일로 쓰이므로 정지 이미지만 받는다.
2896
+ * - `description`: 상세설명 본문에 삽입하는 미디어. 동영상과 svg도 받는다.
2897
+ */
2898
+ const uploadPurposeSchema = z.enum(["product-image", "description"]);
2899
+ const MB = 1048576;
2900
+ /**
2901
+ * 용도별 업로드 제한 — 서버(`POST /v1/uploads`)와 클라이언트가 공유하는 단일 원천이다.
2902
+ *
2903
+ * 크기는 **요청이 스스로 신고한 값**으로만 검사한다. 바이트는 api를 거치지 않고 CDN으로
2904
+ * 직접 PUT되므로 이 검사는 셀러가 올리기 전에 알게 하는 장치이고, 실제 상한은 CDN이 강제한다.
2905
+ */
2906
+ const UPLOAD_PURPOSE_LIMITS = {
2907
+ "product-image": {
2908
+ extensions: [
2909
+ "jpg",
2910
+ "jpeg",
2911
+ "png",
2912
+ "webp",
2913
+ "avif",
2914
+ "gif"
2915
+ ],
2916
+ maxBytes: 10 * MB
2917
+ },
2918
+ description: {
2919
+ extensions: [
2920
+ "jpg",
2921
+ "jpeg",
2922
+ "png",
2923
+ "gif",
2924
+ "webp",
2925
+ "avif",
2926
+ "svg",
2927
+ "mp4",
2928
+ "webm",
2929
+ "mov"
2930
+ ],
2931
+ maxBytes: 50 * MB
2932
+ }
2933
+ };
2934
+ /** 파일명의 소문자 확장자. 확장자가 없으면 `""` */
2935
+ function uploadExtensionOf(filename) {
2936
+ const dot = filename.lastIndexOf(".");
2937
+ if (dot < 0 || dot === filename.length - 1) return "";
2938
+ return filename.slice(dot + 1).toLowerCase();
2939
+ }
2940
+ /**
2941
+ * 용도 제한 위반 여부. 통과하면 `null`.
2942
+ * 서버가 던지는 오류와 콘솔이 먼저 띄우는 안내가 갈리지 않도록 한 함수로 판정한다.
2943
+ */
2944
+ function uploadRejection(purpose, filename, size) {
2945
+ const limit = UPLOAD_PURPOSE_LIMITS[purpose];
2946
+ if (!limit.extensions.includes(uploadExtensionOf(filename))) return {
2947
+ status: 400,
2948
+ code: "UNSUPPORTED_FILE_TYPE",
2949
+ message: `지원하지 않는 파일 형식입니다. ${limit.extensions.join(", ")}만 올릴 수 있습니다`
2950
+ };
2951
+ if (size !== void 0 && size > limit.maxBytes) return {
2952
+ status: 413,
2953
+ code: "FILE_TOO_LARGE",
2954
+ message: `파일이 너무 큽니다. ${Math.floor(limit.maxBytes / MB)}MB까지 올릴 수 있습니다`
2955
+ };
2956
+ return null;
2957
+ }
2958
+ /**
2959
+ * 미디어 업로드 자리 발급 — `POST /v1/uploads`.
2960
+ *
2961
+ * 파일 바이트는 api를 거치지 않는다. 응답의 `uploadUrl`로 클라이언트가 직접 PUT하고,
2962
+ * 끝나면 `url`이 공개 주소가 된다. 서버가 객체 키를 정하므로 요청은 파일명만 보낸다
2963
+ * (확장자만 쓰이고 이름 자체는 저장되지 않는다).
2964
+ */
2965
+ const uploadRequestSchema = z.object({
2966
+ filename: z.string().min(1).max(255),
2967
+ purpose: uploadPurposeSchema.default("description").describe("업로드 용도. 허용 확장자와 크기 상한이 용도별로 다르다"),
2968
+ size: z.number().int().positive().optional().describe("파일 바이트 크기. 주면 용도별 상한을 발급 전에 검사한다")
2969
+ });
2970
+ const uploadTicketSchema = z.object({
2971
+ /** 업로드 완료 후 공개될 주소 */
2972
+ url: z.url(),
2973
+ /** 파일 바이트를 PUT할 presigned URL. 1시간 유효 */
2974
+ uploadUrl: z.url()
2975
+ });
2976
+ //#endregion
2977
+ //#region src/schemas/product.ts
2978
+ const productStatusSchema = z.enum([
2979
+ "SALE",
2980
+ "SUSPENDED",
2981
+ "OUT_OF_STOCK",
2982
+ "CLOSED"
2983
+ ]);
2984
+ const productSummarySchema = z.object({
2985
+ productId: z.string(),
2986
+ name: z.string(),
2987
+ status: productStatusSchema,
2988
+ salePrice: z.number().int().describe("판매가. 원(KRW) 단위 정수"),
2989
+ discountedPrice: z.number().int().nullable().describe("즉시 할인 적용가. 원 단위. 할인이 없으면 null"),
2990
+ /** usable variant 재고 합계 (서버 계산) */
2991
+ stockQuantity: z.number().int().describe("판매 가능한 옵션 재고의 합계. 서버가 계산한다"),
2992
+ categoryId: z.string().nullable(),
2993
+ thumbnailUrl: z.url().optional(),
2994
+ /** optionGroups.length > 0 */
2995
+ hasOptions: z.boolean(),
2996
+ fulfillment: z.object({ type: fulfillmentTypeResponseSchema }).describe("상품 이행의 유형. 유형별 속성은 단건 조회(`GET /v1/products/{productId}`)의 `fulfillment`에 있다"),
2997
+ updatedAt: z.string()
2998
+ });
2999
+ /** 옵션 축(그룹)이 가지는 값 하나 — 예: 축 "색상"의 값 "블랙" */
3000
+ const optionValueSchema = z.object({
3001
+ valueId: z.string(),
3002
+ name: z.string(),
3003
+ sortOrder: z.number().int()
3004
+ });
3005
+ /** 옵션 축(그룹) — 예: "색상", "사이즈" */
3006
+ const optionGroupSchema = z.object({
3007
+ groupId: z.string(),
3008
+ name: z.string(),
3009
+ sortOrder: z.number().int(),
3010
+ values: z.array(optionValueSchema)
3011
+ });
3012
+ /**
3013
+ * 판매 단위(SKU). variantId는 기존 optionId 값 공간을 승계하므로
3014
+ * 카트·주문·리뷰·클레임의 optionId가 곧 variantId다.
3015
+ */
3016
+ const productVariantSchema = z.object({
3017
+ variantId: z.string(),
3018
+ /** 축 sortOrder 순서와 1:1 대응. 옵션 없는 상품은 [] */
3019
+ valueIds: z.array(z.string()),
3020
+ /** 값 이름을 " / "로 이은 파생 표시명. 옵션 없으면 "" */
3021
+ name: z.string(),
3022
+ additionalPrice: z.number().int(),
3023
+ stockQuantity: z.number().int(),
3024
+ sku: z.string().nullable(),
3025
+ barcode: z.string().nullable(),
3026
+ costPrice: z.number().int().nullable(),
3027
+ weightGram: z.number().int().nullable(),
3028
+ sellerCode: z.string().nullable(),
3029
+ usable: z.boolean(),
3030
+ hasOrders: z.boolean().optional().describe("주문에 담긴 적이 있는 조합인가(이슈 #59). true인 조합은 옵션 교체에서 뺄 수 없고(409 `OPTION_HAS_ORDERS`) `usable: false`로 판매를 끈다. 서버는 항상 싣는다. 요청에는 쓰지 않는다")
3031
+ });
3032
+ /** 옵션 축(그룹) 최대 개수 — 스키마 상한의 단일 원천 */
3033
+ const MAX_OPTION_GROUPS = 3;
3034
+ /** 축 하나가 가질 수 있는 값의 최대 개수 */
3035
+ const MAX_VALUES_PER_GROUP = 100;
3036
+ /** 상품 하나가 가질 수 있는 variant(SKU) 최대 개수 */
3037
+ const MAX_VARIANTS = 500;
3038
+ /** 추가 선택 옵션명 최대 개수 */
3039
+ const MAX_ADDON_GROUPS = 5;
3040
+ /** 직접 입력 항목 최대 개수 */
3041
+ const MAX_CUSTOM_INPUTS = 5;
3042
+ /** 직접 입력 항목 최대 글자 수의 상한 */
3043
+ const MAX_CUSTOM_INPUT_LENGTH = 200;
3044
+ /** 조합 수가 이 값 이상이면 콘솔이 경고한다(상한은 `MAX_VARIANTS`) */
3045
+ const VARIANT_COUNT_WARNING = 100;
3046
+ /**
3047
+ * 옵션 값 정렬 순서 — 등록순·가나다순·낮은 추가금순·높은 추가금순. 서버가 저장할 때 이 순서로 값의 `sortOrder`를 매긴다.
3048
+ * 추가금순은 추가 선택 값이면 값의 추가금, 조합 값이면 그 값이 든 조합 추가금의 최솟값으로 비교하고 같으면 등록순이다.
3049
+ */
3050
+ const optionSortSchema = z.enum([
3051
+ "REGISTERED",
3052
+ "NAME",
3053
+ "PRICE_ASC",
3054
+ "PRICE_DESC"
3055
+ ]);
3056
+ /** 추가 선택 값 — 예: 옵션명 "선물 포장"의 값 "포장함"(+2,000원) */
3057
+ const addonValueSchema = z.object({
3058
+ valueId: z.string(),
3059
+ name: z.string(),
3060
+ sortOrder: z.number().int(),
3061
+ additionalPrice: z.number().int().describe("이 값을 고르면 단가에 더하는 금액(원, 0 이상)"),
3062
+ usable: z.boolean().describe("사용 여부. false면 구매자에게 보이지 않고 고를 수 없다")
3063
+ });
3064
+ /** 추가 선택 옵션명 */
3065
+ const addonGroupSchema = z.object({
3066
+ groupId: z.string(),
3067
+ name: z.string(),
3068
+ sortOrder: z.number().int(),
3069
+ required: z.boolean().describe("구매자가 꼭 골라야 하는가. false면 고르지 않아도 된다"),
3070
+ values: z.array(addonValueSchema)
3071
+ });
3072
+ /** 직접 입력 항목 — 예: 각인 문구 */
3073
+ const customInputSchema = z.object({
3074
+ inputId: z.string(),
3075
+ label: z.string().describe("항목명"),
3076
+ placeholder: z.string().nullable().describe("입력 안내(구매자 입력 칸의 안내 문구). 없으면 null"),
3077
+ maxLength: z.number().int().describe("최대 글자 수"),
3078
+ required: z.boolean(),
3079
+ usable: z.boolean().describe("사용 여부. false면 구매자에게 보이지 않는다"),
3080
+ sortOrder: z.number().int()
3081
+ });
3082
+ /**
3083
+ * 직접 입력 항목명에 쓸 수 없는 개인정보성 단어 — 콘솔 검사와 서버 400(`CUSTOM_INPUT_LABEL_FORBIDDEN`)이 이 목록 하나를 쓴다.
3084
+ * 구매자 개인정보는 주문 정보로 받는다. 직접 입력 항목으로 받으면 보관·파기 규칙 밖에 남는다.
3085
+ */
3086
+ const CUSTOM_INPUT_FORBIDDEN_WORDS = [
3087
+ "연락처",
3088
+ "전화",
3089
+ "휴대폰",
3090
+ "핸드폰",
3091
+ "폰번호",
3092
+ "메일",
3093
+ "주소",
3094
+ "주민",
3095
+ "생년월일",
3096
+ "생일",
3097
+ "실명",
3098
+ "계좌",
3099
+ "카드번호",
3100
+ "비밀번호",
3101
+ "여권",
3102
+ "통관",
3103
+ "phone",
3104
+ "mobile",
3105
+ "mail",
3106
+ "address"
3107
+ ];
3108
+ /** 금지어 비교용 정규화 — NFKC(전각·호환 문자를 보통 문자로)·소문자 뒤 문자·숫자만 남긴다(공백·기호·제로폭 문자로 쪼갠 우회를 막는다) */
3109
+ function forbiddenWordKeyOf(text) {
3110
+ return text.normalize("NFKC").toLowerCase().replace(/[^\p{L}\p{N}]/gu, "");
3111
+ }
3112
+ /**
3113
+ * 직접 입력 항목명·입력 안내에 든 개인정보성 금지어. 없으면 null. 공백·기호·대소문자·전각을 무시한다.
3114
+ * 서버는 항목명과 입력 안내 둘 다 이 함수로 검사한다(안내에 「연락처를 적어 주십시오」라고 쓰는 우회를 막는다).
3115
+ */
3116
+ function customInputLabelForbiddenWord(label) {
3117
+ const normalized = forbiddenWordKeyOf(label);
3118
+ return CUSTOM_INPUT_FORBIDDEN_WORDS.find((word) => normalized.includes(forbiddenWordKeyOf(word))) ?? null;
3119
+ }
3120
+ /** 옵션명·값 이름 — 앞뒤 공백을 지우고 빈 값은 받지 않는다 */
3121
+ const optionNameSchema = z.string().trim().min(1).max(50);
3122
+ const addonValueRequestSchema = z.object({
3123
+ /** 전달 시 기존 값 행을 유지(UPDATE). 생략하면 신규 발급 */
3124
+ valueId: z.string().optional(),
3125
+ name: optionNameSchema,
3126
+ additionalPrice: z.number().int().min(0).default(0).describe("추가금(원, 0 이상)"),
3127
+ usable: z.boolean().default(true)
3128
+ });
3129
+ const addonGroupRequestSchema = z.object({
3130
+ /** 전달 시 기존 옵션명 행을 유지(UPDATE). 조합 옵션명의 id는 줄 수 없다(방식을 바꾸려면 새 옵션명으로 보낸다) */
3131
+ groupId: z.string().optional(),
3132
+ name: optionNameSchema,
3133
+ required: z.boolean().default(false),
3134
+ values: z.array(addonValueRequestSchema).min(1).max(100)
3135
+ });
3136
+ const customInputRequestSchema = z.object({
3137
+ /** 전달 시 기존 항목 행을 유지(UPDATE). 생략하면 신규 발급 */
3138
+ inputId: z.string().optional(),
3139
+ label: z.string().trim().min(1).max(30),
3140
+ placeholder: z.string().trim().max(100).nullish(),
3141
+ maxLength: z.number().int().min(1).max(200),
3142
+ required: z.boolean().default(false),
3143
+ usable: z.boolean().default(true)
3144
+ });
3145
+ /**
3146
+ * 주문상품·장바구니·주문서 줄의 옵션 선택 스냅샷 한 칸 — 주문 시점 이름이다(상품을 고쳐도 바뀌지 않는다).
3147
+ * 조합 옵션은 옵션명마다 한 칸이고 추가금은 조합 단위라 0이다(조합 추가금은 단가에 들어 있다). 추가 선택은 값의 추가금이다.
3148
+ */
3149
+ const optionSelectionSchema = z.object({
3150
+ kind: z.string().describe("옵션명 방식. 현재 값은 `combination`·`addon`이고 값이 추가될 수 있어 문자열로 받는다"),
3151
+ groupName: z.string(),
3152
+ valueName: z.string(),
3153
+ additionalPrice: z.number().int()
3154
+ });
3155
+ /** 구매자가 적은 직접 입력값 — 주문 시점 항목명과 값 */
3156
+ const customInputValueSchema = z.object({
3157
+ label: z.string(),
3158
+ value: z.string()
3159
+ });
3160
+ const discountSchema = z.object({
3161
+ productId: z.string(),
3162
+ discountType: z.enum(["RATE", "AMOUNT"]),
3163
+ discountValue: z.number().int(),
3164
+ discountedPrice: z.number().int(),
3165
+ startAt: z.string().nullable(),
3166
+ endAt: z.string().nullable()
3167
+ });
3168
+ const productSchema = productSummarySchema.extend({
3169
+ description: z.string().optional(),
3170
+ /**
3171
+ * 상세설명 에디터(@avarlabs/editor)의 직렬화 상태. 재편집 때 이 값으로 문서를 복원한다.
3172
+ * 표시는 `description`(HTML)이 맡는다 — 소비자는 이 필드를 몰라도 된다.
3173
+ */
3174
+ descriptionJson: z.string().nullable().optional(),
3175
+ originalPrice: z.number().int().nullable().optional(),
3176
+ images: z.array(z.url()).optional(),
3177
+ attributes: z.array(z.object({
3178
+ attributeId: z.string(),
3179
+ value: z.string()
3180
+ })).optional(),
3181
+ optionGroups: z.array(optionGroupSchema),
3182
+ /** 모든 상품이 최소 1개 variant를 갖는다 (옵션 없으면 default variant 1개) */
3183
+ variants: z.array(productVariantSchema),
3184
+ addonGroups: z.array(addonGroupSchema).optional().default([]).describe("추가 선택 옵션명(이슈 #59). 없으면 빈 배열. 서버는 항상 싣는다"),
3185
+ customInputs: z.array(customInputSchema).optional().default([]).describe("직접 입력 항목(이슈 #59). 사용하지 않는 항목도 싣는다. 없으면 빈 배열. 서버는 항상 싣는다"),
3186
+ optionSort: optionSortSchema.optional().default("REGISTERED").describe("옵션 값 정렬 순서(이슈 #59). 서버는 항상 싣는다"),
3187
+ fulfillment: productFulfillmentResponseSchema,
3188
+ fulfillmentTypeLocked: z.boolean().describe("판매 이력(주문상품)이 있어 상품 유형(`fulfillment.type`)을 바꿀 수 없는가. true면 다른 유형으로 수정하는 요청이 409 `FULFILLMENT_TYPE_LOCKED`다. 같은 유형 안의 속성은 바꿀 수 있다"),
3189
+ refundPolicy: productRefundPolicyPatchSchema.nullable().describe("상품별 환불 정책 덮어쓰기(상품이 정한 항목만). null이면 스토어 기본 환불 정책을 쓴다. 반품·교환 배송비(`returnDeliveryFee`·`exchangeDeliveryFee`)는 배송 상품에만 있다"),
3190
+ /**
3191
+ * 설정된 즉시할인. `GET /products/{productId}/discount`의 응답과 같은 형태다(이슈 #51).
3192
+ * 할인이 없으면 null이다. 이 필드를 싣기 전 서버의 응답에는 없으므로 optional이다.
3193
+ */
3194
+ discount: discountSchema.nullable().optional().describe("설정된 즉시할인. GET /v1/products/{productId}/discount 응답과 같은 형태다. 할인이 없으면 null"),
3195
+ createdAt: z.string().optional()
3196
+ });
3197
+ /** 상품 하나에 등록할 수 있는 이미지 최대 장수 — 첫 장이 대표(썸네일)다 */
3198
+ const MAX_PRODUCT_IMAGES = 10;
3199
+ /**
3200
+ * 상품 이미지 주소의 와이어 형식 — URL이고 2048자 이하다.
3201
+ *
3202
+ * https·확장자 규칙은 여기서 강제하지 않고 `productImageRejection`이 판정한다. 서버가
3203
+ * **새로 추가된 주소에만** 그 규칙을 적용해야 하기 때문이다(규칙이 생기기 전에 저장된
3204
+ * 주소 하나로 상품 수정 전체가 막히면 셀러가 빠져나갈 길이 없다).
3205
+ *
3206
+ * 응답 스키마(`productSchema.images`)도 넓게 둔다. 과거에 저장된 주소를 파싱 오류로
3207
+ * 만들면 상품 조회 자체가 막힌다.
3208
+ */
3209
+ const productImageUrlSchema = z.url().max(2048).describe("상품 이미지 주소. https만 허용하고, 확장자가 있으면 이미지 형식이어야 한다");
3210
+ /**
3211
+ * 상품에 **새로 붙이는** 이미지 주소의 규칙. 통과하면 `null`.
3212
+ *
3213
+ * - https만 받는다. http는 https 스토어프론트에서 혼합 콘텐츠로 차단되고 `data:`·`javascript:`는
3214
+ * 상세 화면에 박히는 주입 경로가 된다.
3215
+ * - 호스트는 제한하지 않는다 — 셀러가 이미 쓰는 이미지 호스트를 그대로 붙일 수 있어야 한다.
3216
+ * - 경로에 확장자가 있으면 상품 이미지 형식이어야 한다(svg·동영상 거부). **확장자가 없으면 통과시킨다**
3217
+ * — 외부 이미지 CDN은 확장자 없는 키를 쓰는 경우가 흔하고, 주소만으로는 실제 형식을 알 수 없다.
3218
+ */
3219
+ function productImageRejection(url) {
3220
+ if (url.length > 2048) return {
3221
+ code: "INVALID_IMAGE_URL",
3222
+ message: "이미지 주소가 너무 깁니다"
3223
+ };
3224
+ let parsed;
3225
+ try {
3226
+ parsed = new URL(url);
3227
+ } catch {
3228
+ return {
3229
+ code: "INVALID_IMAGE_URL",
3230
+ message: "이미지 주소 형식이 아닙니다"
3231
+ };
3232
+ }
3233
+ if (parsed.protocol !== "https:") return {
3234
+ code: "INVALID_IMAGE_URL",
3235
+ message: "이미지 주소는 https로 시작해야 합니다"
3236
+ };
3237
+ const extension = uploadExtensionOf(parsed.pathname);
3238
+ const allowed = UPLOAD_PURPOSE_LIMITS["product-image"].extensions;
3239
+ if (extension && !allowed.includes(extension)) return {
3240
+ code: "UNSUPPORTED_FILE_TYPE",
3241
+ message: `상품 이미지는 ${allowed.join(", ")}만 쓸 수 있습니다`
3242
+ };
3243
+ return null;
3244
+ }
3245
+ /**
3246
+ * 식별 코드 — 빈 문자열·공백만 있는 값은 거부한다.
3247
+ * `""`를 통과시키면 서버 중복 사전 검사(falsy skip)와 DB partial unique
3248
+ * (`WHERE ... is not null`)의 판정이 갈려 409 대신 500이 나간다.
3249
+ */
3250
+ const codeSchema = z.string().trim().min(1).max(64);
3251
+ const optionValueRequestSchema = z.object({
3252
+ /** 전달 시 기존 값 행을 유지(UPDATE). 생략하면 신규 발급 */
3253
+ valueId: z.string().optional(),
3254
+ name: z.string().min(1).max(50)
3255
+ });
3256
+ const optionGroupRequestSchema = z.object({
3257
+ /** 전달 시 기존 축 행을 유지(UPDATE). 생략하면 신규 발급 */
3258
+ groupId: z.string().optional(),
3259
+ name: z.string().min(1).max(50),
3260
+ values: z.array(optionValueRequestSchema).min(1).max(100)
3261
+ });
3262
+ const productVariantRequestSchema = z.object({
3263
+ /** 기존 variant 유지 시 전달. 생략하면 신규 생성 */
3264
+ variantId: z.string().optional(),
3265
+ /** optionGroups 순서대로 각 축에서 고른 값 이름. 서버가 이름 → valueId를 해석한다 */
3266
+ valueNames: z.array(z.string()).default([]),
3267
+ additionalPrice: z.number().int().default(0),
3268
+ /**
3269
+ * 생략 = 기존 재고 유지 (신규 variant는 0).
3270
+ * 구조 교체(PUT)가 폼 로드 시점의 재고를 되쓰지 않게 하는 옵트인 필드다 —
3271
+ * 재고만 바꿀 때는 PATCH /products/{id}/variants/{variantId}/stock을 쓴다.
3272
+ */
3273
+ stockQuantity: z.number().int().min(0).optional(),
3274
+ sku: codeSchema.optional(),
3275
+ barcode: codeSchema.optional(),
3276
+ costPrice: z.number().int().min(0).optional(),
3277
+ weightGram: z.number().int().min(0).optional(),
3278
+ sellerCode: codeSchema.optional(),
3279
+ usable: z.boolean().default(true)
3280
+ });
3281
+ /** 축 × 값 → variant 전체 교체 요청 (PUT /products/{productId}/options) */
3282
+ const optionModelRequestSchema = z.object({
3283
+ optionGroups: z.array(optionGroupRequestSchema).max(3).default([]),
3284
+ variants: z.array(productVariantRequestSchema).min(1).max(500),
3285
+ addonGroups: z.array(addonGroupRequestSchema).max(5).optional().describe("추가 선택 옵션명 전체(이슈 #59). 주면 통째로 바꾸고, 생략하면 등록은 없음·수정은 현재 값을 그대로 둔다. 끄려면 빈 배열을 보낸다. 기능이 열리기 전에는 비어 있지 않은 값이 409 `OPTION_ADDONS_UNAVAILABLE`이다"),
3286
+ customInputs: z.array(customInputRequestSchema).max(5).optional().describe("직접 입력 항목 전체(이슈 #59). 배열 순서가 표시 순서다. 주면 통째로 바꾸고, 생략하면 등록은 없음·수정은 현재 값을 그대로 둔다. 항목명에 개인정보성 단어(`CUSTOM_INPUT_FORBIDDEN_WORDS`)가 있으면 400 `CUSTOM_INPUT_LABEL_FORBIDDEN`, 기능이 열리기 전에는 비어 있지 않은 값이 409 `OPTION_ADDONS_UNAVAILABLE`이다"),
3287
+ optionSort: optionSortSchema.optional().describe("옵션 값 정렬 순서(이슈 #59). 서버가 이 순서로 값의 `sortOrder`를 다시 매긴다. 생략하면 등록은 `REGISTERED`, 수정은 현재 값이다")
3288
+ });
3289
+ /** GET /products/{productId}/options 응답 */
3290
+ const optionModelSchema = z.object({
3291
+ optionGroups: z.array(optionGroupSchema),
3292
+ variants: z.array(productVariantSchema),
3293
+ addonGroups: z.array(addonGroupSchema).optional().default([]).describe("추가 선택 옵션명(이슈 #59). 없으면 빈 배열. 서버는 항상 싣는다"),
3294
+ customInputs: z.array(customInputSchema).optional().default([]).describe("직접 입력 항목(이슈 #59). 없으면 빈 배열. 서버는 항상 싣는다"),
3295
+ optionSort: optionSortSchema.optional().default("REGISTERED").describe("옵션 값 정렬 순서(이슈 #59). 서버는 항상 싣는다")
3296
+ });
3297
+ /**
3298
+ * 재고 변경 택일 위반 문구 — SDK `.refine`과 서버 DomainException이 공유하는 단일 원천.
3299
+ * 경로에 따라 셀러가 다른 문구를 받지 않게 한다.
3300
+ */
3301
+ const VARIANT_STOCK_EXCLUSIVE_MESSAGE = "stockQuantity 또는 adjustment 중 하나만 전달해야 합니다";
3302
+ /**
3303
+ * variant 재고 변경 요청 — 절대값(`stockQuantity`) 또는 증감(`adjustment`) 택일.
3304
+ * 서버도 같은 규칙을 400 VALIDATION_FAILED로 검증한다(이중 방어).
3305
+ */
3306
+ const variantStockRequestSchema = z.object({
3307
+ stockQuantity: z.number().int().min(0).optional(),
3308
+ adjustment: z.number().int().optional()
3309
+ }).refine((v) => v.stockQuantity !== void 0 !== (v.adjustment !== void 0), { message: VARIANT_STOCK_EXCLUSIVE_MESSAGE });
3310
+ /** PATCH /products/{productId}/variants/{variantId}/stock 응답 */
3311
+ const variantStockSchema = z.object({
3312
+ variantId: z.string(),
3313
+ stockQuantity: z.number().int()
3314
+ });
3315
+ /**
3316
+ * 상품 쓰기 요청의 공통 필드 — 등록(POST)과 전체 수정(PUT)이 같다. 이행(`fulfillment`)만 두 요청의 스키마가 다르다.
3317
+ *
3318
+ * 최상위가 `strictObject`다. 모르는 필드는 400이다 — 이슈 #57 이전의 평면 필드(`deliveryFee`·`fulfillmentType`·`returnFee` 등)를
3319
+ * 보낸 요청이 조용히 무시되지 않게 한다. 옛 필드는 `fulfillment.{유형}.…`과 `refundPolicy`로 옮겼다.
3320
+ */
3321
+ const productWriteShape = {
3322
+ name: z.string().min(1).max(100),
3323
+ categoryId: z.string().min(1, "카테고리를 선택해주십시오"),
3324
+ salePrice: z.number().int().min(10).multipleOf(10, "판매가는 10원 단위여야 합니다 (INVALID_PRICE_UNIT)").describe("판매가. 원(KRW) 단위 정수, 최소 10원, 10원 단위"),
3325
+ originalPrice: z.number().int().optional().describe("정가(할인 전 표시 가격). 원 단위"),
3326
+ description: z.string().optional(),
3327
+ /**
3328
+ * 상세설명 에디터(@avarlabs/editor)의 직렬화 상태. 재편집 때 이 값으로 문서를 복원한다.
3329
+ * 표시는 `description`(HTML)이 맡는다 — 소비자는 이 필드를 몰라도 된다.
3330
+ */
3331
+ descriptionJson: z.string().nullable().optional(),
3332
+ images: z.array(productImageUrlSchema).min(1).max(10).optional().describe("상품 이미지 주소 목록. 첫 장이 대표 이미지(썸네일)다"),
3333
+ attributes: z.array(z.object({
3334
+ attributeId: z.string(),
3335
+ value: z.string()
3336
+ })).optional(),
3337
+ refundPolicy: productRefundPolicyPatchSchema.nullish().describe("상품별 환불 정책 — 준 항목만 스토어 기본을 덮어쓴다. `null`이나 생략은 스토어 기본을 그대로 쓴다는 뜻이다. 반품·교환 배송비(`returnDeliveryFee`·`exchangeDeliveryFee`)는 배송 상품에만 줄 수 있다(다른 유형은 400)"),
3338
+ ...optionModelRequestSchema.shape,
3339
+ sourceOptionSetIds: z.array(z.string()).max(10).optional().describe("쓰기 전용(이슈 #59) — 이 저장으로 옵션을 불러온 저장 옵션 id. 상품 저장과 같은 트랜잭션에서 불러오기 기록을 남겨 저장 옵션의 `importedProductCount`를 센다. 없는 id는 무시한다")
3340
+ };
3341
+ const productCreateRequestSchema = z.strictObject({
3342
+ ...productWriteShape,
3343
+ fulfillment: productFulfillmentCreateSchema.optional().describe("상품 이행. `type`이 유형이고 같은 이름의 키가 유형별 속성이다. 생략하면 배송 상품(기본값)이다. 배송 상품이 아닌 유형은 판매가 열린 유형만 등록할 수 있다(409 `FULFILLMENT_TYPE_NOT_AVAILABLE`)")
3344
+ });
3345
+ const fulfillmentUpdateDescription = "상품 이행. 생략하면 바꾸지 않는다. 주면 유형 안 필드를 모두 담아 통째로 바꾼다(현재 값과 병합하지 않는다). 판매 이력이 있으면 유형을 바꿀 수 없다(409 `FULFILLMENT_TYPE_LOCKED`)";
3346
+ /** 전체 수정(PUT) — 공통 필드는 등록과 같고, 이행은 생략하면 현재 값 유지, 주면 필드가 모두 필수다 */
3347
+ const productReplaceRequestSchema = z.strictObject({
3348
+ ...productWriteShape,
3349
+ fulfillment: productFulfillmentUpdateSchema.optional().describe(fulfillmentUpdateDescription)
3350
+ });
3351
+ const productPatchRequestSchema = z.strictObject({
3352
+ name: z.string().max(100).optional(),
3353
+ salePrice: z.number().int().multipleOf(10).optional(),
3354
+ status: productStatusSchema.optional(),
3355
+ fulfillment: productFulfillmentUpdateSchema.optional().describe(fulfillmentUpdateDescription),
3356
+ refundPolicy: productRefundPolicyPatchSchema.nullable().optional().describe("상품별 환불 정책. 주면 통째로 바꾼다(준 항목만 스토어 기본을 덮어쓴다). `null`은 덮어쓰기를 지워 스토어 기본을 따른다. 생략하면 바꾸지 않는다")
3357
+ }).refine((value) => Object.values(value).some((v) => v !== void 0), { message: "최소 1개 필드가 필요합니다" });
3358
+ const productSortSchema = z.enum([
3359
+ "createdAt.desc",
3360
+ "createdAt.asc",
3361
+ "salePrice.asc",
3362
+ "salePrice.desc",
3363
+ "updatedAt.desc"
3364
+ ]);
3365
+ /** `GET /v1/products` 쿼리 — OpenAPI 문서용 형태(값은 쿼리 문자열) */
3366
+ const productListQuerySchema = z.object({
3367
+ status: productStatusSchema.optional().describe("판매 상태 필터"),
3368
+ categoryId: z.string().optional().describe("카테고리 필터 — 하위 카테고리의 상품도 포함"),
3369
+ keyword: z.string().optional().describe("상품명 검색 (포함)"),
3370
+ sellerCode: z.string().optional().describe("판매자 상품코드 — 이 코드가 붙은 variant를 가진 상품"),
3371
+ sort: productSortSchema.optional().describe("정렬 (기본 createdAt.desc)"),
3372
+ ...pageQueryShape()
3373
+ });
3374
+ const discountRequestSchema = z.object({
3375
+ discountType: z.enum(["RATE", "AMOUNT"]),
3376
+ discountValue: z.number().int().min(1),
3377
+ startAt: z.string().optional(),
3378
+ endAt: z.string().optional()
3379
+ });
3380
+ //#endregion
2890
3381
  //#region src/schemas/order.ts
2891
3382
  const orderSummarySchema = z.object({
2892
3383
  orderId: z.string(),
@@ -2926,6 +3417,8 @@ const orderItemSchema = z.object({
2926
3417
  productName: z.string(),
2927
3418
  optionId: z.string().nullable(),
2928
3419
  optionName: z.string().nullable(),
3420
+ optionSelections: z.array(optionSelectionSchema).optional().default([]).describe("주문 시점 옵션 선택(이슈 #59) — 조합 옵션(옵션명마다 한 칸, 추가금 0)과 추가 선택(값의 추가금). 옵션이 없거나 이 필드 이전 주문은 빈 배열. 서버는 항상 싣는다"),
3421
+ customInputs: z.array(customInputValueSchema).optional().default([]).describe("구매자가 적은 직접 입력값(주문 시점 항목명과 값, 이슈 #59). 없으면 빈 배열. 서버는 항상 싣는다"),
2929
3422
  sellerCode: z.string().nullable(),
2930
3423
  fulfillmentSnapshot: orderItemFulfillmentSnapshotResponseSchema.describe("주문 시점 이행 스냅샷(이슈 #57). `type`이 주문 시점 상품 유형이다. 배송 상품이 아니면 발송·배송완료 대신 제공 처리를 하고, `DELIVERED`는 제공 완료다. 상품을 나중에 고쳐도 바뀌지 않는다. 제공 기록은 `fulfillment`에 있다"),
2931
3424
  quantity: z.number().int().describe("주문 수량"),
@@ -3111,401 +3604,149 @@ const cancelBySellerRequestSchema = z.object({
3111
3604
  * 제공 처리 요청 (이슈 #48) — `POST /v1/order-items/{orderItemId}/fulfill`. 배송 없는 주문상품(`MANUAL`)을
3112
3605
  * 제공 완료(`DELIVERED`)로 바꾼다.
3113
3606
  */
3114
- const fulfillRequestSchema = z.object({ note: z.string().trim().max(1e3).optional().describe("구매자에게 보일 이용 안내(예: 예약 방법, 수령 장소). 주문 상세에 실린다") });
3115
- /** 제공 처리 일괄 요청 — `POST /v1/order-items/fulfill` */
3116
- const fulfillBulkRequestSchema = z.object({
3117
- orderItemIds: z.array(z.string()).min(1).max(100),
3118
- note: fulfillRequestSchema.shape.note
3119
- });
3120
- /** `POST /v1/order-items/{orderItemId}/cancel` 응답 — 셀러 직권취소 환불 결과 */
3121
- const cancelBySellerResultSchema = z.object({
3122
- orderItemId: z.string(),
3123
- refundAmount: z.number().int().describe("실제 환불한 금액 — 수량 비율 상품 실결제 금액(쿠폰 배분을 뺀 금액) + 배송비 환불 − 조건부 무료배송 미달 차감 + 되돌린 쿠폰 차감(`couponChargeRefundAmount`)"),
3124
- quantity: z.number().int().describe("취소한 수량"),
3125
- canceledQuantity: z.number().int().describe("취소 후 누적 취소 수량"),
3126
- deliveryFeeRefundAmount: z.number().int().describe("환불에 포함된 배송비. 배송 묶음(같은 상품)이 모두 취소됐고 환불 정책이 켜져 있을 때만 0보다 크다"),
3127
- deliveryFeeChargeAmount: z.number().int().describe("조건부 무료배송 기준 미달로 환불액에서 뺀 배송비. 정책이 꺼져 있으면 항상 0이다"),
3128
- couponChargeAmount: z.number().int().optional().default(0).describe("쿠폰 조건 미달 차감. 판매자 취소는 차감하지 않으므로 늘 0이다. 서버는 항상 싣는다"),
3129
- couponChargeRefundAmount: z.number().int().optional().default(0).describe("환불액에 더한, 앞선 환불에서 뺀 쿠폰 차감을 되돌린 몫. 서버는 항상 싣는다"),
3130
- refundedAt: z.string()
3131
- });
3132
- /** `PUT /v1/order-items/{orderItemId}/memo` 응답 — 빈 문자열로 저장하면 메모가 지워져 `null`이다 */
3133
- const orderItemMemoResultSchema = z.object({
3134
- orderItemId: z.string(),
3135
- sellerMemo: z.string().nullable()
3136
- });
3137
- /** `POST /v1/order-items/{orderItemId}/purchase-decision/request` 응답 */
3138
- const purchaseDecisionRequestResultSchema = z.object({
3139
- orderItemId: z.string(),
3140
- requestedAt: z.string()
3141
- });
3142
- /** `POST /v1/order-items/{orderItemId}/purchase-decision/extend` 응답 — 연장은 최대 2회 */
3143
- const purchaseDecisionExtendResultSchema = z.object({
3144
- orderItemId: z.string(),
3145
- autoDecisionDate: z.string(),
3146
- extendCount: z.number().int().max(2)
3147
- });
3148
- /**
3149
- * `GET /v1/orders/{orderId}/address-change-requests` 응답 — 배송지 변경 요청 플로우는 아직 제공하지 않아
3150
- * 현재는 항상 빈 배열이다
3151
- */
3152
- const addressChangeRequestListSchema = z.array(z.record(z.string(), z.unknown())).describe("배송지 변경 요청 목록 (현재 항상 빈 배열)");
3153
- //#endregion
3154
- //#region src/schemas/upload.ts
3155
- /**
3156
- * 업로드 용도. 허용 확장자와 크기 상한이 용도별로 다르다.
3157
- *
3158
- * - `product-image`: 상품 목록·상세의 상품 이미지. 목록 썸네일로 쓰이므로 정지 이미지만 받는다.
3159
- * - `description`: 상세설명 본문에 삽입하는 미디어. 동영상과 svg도 받는다.
3160
- */
3161
- const uploadPurposeSchema = z.enum(["product-image", "description"]);
3162
- const MB = 1048576;
3163
- /**
3164
- * 용도별 업로드 제한 — 서버(`POST /v1/uploads`)와 클라이언트가 공유하는 단일 원천이다.
3165
- *
3166
- * 크기는 **요청이 스스로 신고한 값**으로만 검사한다. 바이트는 api를 거치지 않고 CDN으로
3167
- * 직접 PUT되므로 이 검사는 셀러가 올리기 전에 알게 하는 장치이고, 실제 상한은 CDN이 강제한다.
3168
- */
3169
- const UPLOAD_PURPOSE_LIMITS = {
3170
- "product-image": {
3171
- extensions: [
3172
- "jpg",
3173
- "jpeg",
3174
- "png",
3175
- "webp",
3176
- "avif",
3177
- "gif"
3178
- ],
3179
- maxBytes: 10 * MB
3180
- },
3181
- description: {
3182
- extensions: [
3183
- "jpg",
3184
- "jpeg",
3185
- "png",
3186
- "gif",
3187
- "webp",
3188
- "avif",
3189
- "svg",
3190
- "mp4",
3191
- "webm",
3192
- "mov"
3193
- ],
3194
- maxBytes: 50 * MB
3195
- }
3196
- };
3197
- /** 파일명의 소문자 확장자. 확장자가 없으면 `""` */
3198
- function uploadExtensionOf(filename) {
3199
- const dot = filename.lastIndexOf(".");
3200
- if (dot < 0 || dot === filename.length - 1) return "";
3201
- return filename.slice(dot + 1).toLowerCase();
3202
- }
3203
- /**
3204
- * 용도 제한 위반 여부. 통과하면 `null`.
3205
- * 서버가 던지는 오류와 콘솔이 먼저 띄우는 안내가 갈리지 않도록 한 함수로 판정한다.
3206
- */
3207
- function uploadRejection(purpose, filename, size) {
3208
- const limit = UPLOAD_PURPOSE_LIMITS[purpose];
3209
- if (!limit.extensions.includes(uploadExtensionOf(filename))) return {
3210
- status: 400,
3211
- code: "UNSUPPORTED_FILE_TYPE",
3212
- message: `지원하지 않는 파일 형식입니다. ${limit.extensions.join(", ")}만 올릴 수 있습니다`
3213
- };
3214
- if (size !== void 0 && size > limit.maxBytes) return {
3215
- status: 413,
3216
- code: "FILE_TOO_LARGE",
3217
- message: `파일이 너무 큽니다. ${Math.floor(limit.maxBytes / MB)}MB까지 올릴 수 있습니다`
3218
- };
3219
- return null;
3220
- }
3221
- /**
3222
- * 미디어 업로드 자리 발급 — `POST /v1/uploads`.
3223
- *
3224
- * 파일 바이트는 api를 거치지 않는다. 응답의 `uploadUrl`로 클라이언트가 직접 PUT하고,
3225
- * 끝나면 `url`이 공개 주소가 된다. 서버가 객체 키를 정하므로 요청은 파일명만 보낸다
3226
- * (확장자만 쓰이고 이름 자체는 저장되지 않는다).
3227
- */
3228
- const uploadRequestSchema = z.object({
3229
- filename: z.string().min(1).max(255),
3230
- purpose: uploadPurposeSchema.default("description").describe("업로드 용도. 허용 확장자와 크기 상한이 용도별로 다르다"),
3231
- size: z.number().int().positive().optional().describe("파일 바이트 크기. 주면 용도별 상한을 발급 전에 검사한다")
3232
- });
3233
- const uploadTicketSchema = z.object({
3234
- /** 업로드 완료 후 공개될 주소 */
3235
- url: z.url(),
3236
- /** 파일 바이트를 PUT할 presigned URL. 1시간 유효 */
3237
- uploadUrl: z.url()
3238
- });
3239
- //#endregion
3240
- //#region src/schemas/product.ts
3241
- const productStatusSchema = z.enum([
3242
- "SALE",
3243
- "SUSPENDED",
3244
- "OUT_OF_STOCK",
3245
- "CLOSED"
3246
- ]);
3247
- const productSummarySchema = z.object({
3248
- productId: z.string(),
3249
- name: z.string(),
3250
- status: productStatusSchema,
3251
- salePrice: z.number().int().describe("판매가. 원(KRW) 단위 정수"),
3252
- discountedPrice: z.number().int().nullable().describe("즉시 할인 적용가. 원 단위. 할인이 없으면 null"),
3253
- /** usable variant 재고 합계 (서버 계산) */
3254
- stockQuantity: z.number().int().describe("판매 가능한 옵션 재고의 합계. 서버가 계산한다"),
3255
- categoryId: z.string().nullable(),
3256
- thumbnailUrl: z.url().optional(),
3257
- /** optionGroups.length > 0 */
3258
- hasOptions: z.boolean(),
3259
- fulfillment: z.object({ type: fulfillmentTypeResponseSchema }).describe("상품 이행의 유형. 유형별 속성은 단건 조회(`GET /v1/products/{productId}`)의 `fulfillment`에 있다"),
3260
- updatedAt: z.string()
3261
- });
3262
- /** 옵션 축(그룹)이 가지는 값 하나 — 예: 축 "색상"의 값 "블랙" */
3263
- const optionValueSchema = z.object({
3264
- valueId: z.string(),
3265
- name: z.string(),
3266
- sortOrder: z.number().int()
3267
- });
3268
- /** 옵션 축(그룹) — 예: "색상", "사이즈" */
3269
- const optionGroupSchema = z.object({
3270
- groupId: z.string(),
3271
- name: z.string(),
3272
- sortOrder: z.number().int(),
3273
- values: z.array(optionValueSchema)
3274
- });
3275
- /**
3276
- * 판매 단위(SKU). variantId는 기존 optionId 값 공간을 승계하므로
3277
- * 카트·주문·리뷰·클레임의 optionId가 곧 variantId다.
3278
- */
3279
- const productVariantSchema = z.object({
3280
- variantId: z.string(),
3281
- /** 축 sortOrder 순서와 1:1 대응. 옵션 없는 상품은 [] */
3282
- valueIds: z.array(z.string()),
3283
- /** 값 이름을 " / "로 이은 파생 표시명. 옵션 없으면 "" */
3284
- name: z.string(),
3285
- additionalPrice: z.number().int(),
3286
- stockQuantity: z.number().int(),
3287
- sku: z.string().nullable(),
3288
- barcode: z.string().nullable(),
3289
- costPrice: z.number().int().nullable(),
3290
- weightGram: z.number().int().nullable(),
3291
- sellerCode: z.string().nullable(),
3292
- usable: z.boolean()
3607
+ const fulfillRequestSchema = z.object({ note: z.string().trim().max(1e3).optional().describe("구매자에게 보일 이용 안내(예: 예약 방법, 수령 장소). 주문 상세에 실린다") });
3608
+ /** 제공 처리 일괄 요청 — `POST /v1/order-items/fulfill` */
3609
+ const fulfillBulkRequestSchema = z.object({
3610
+ orderItemIds: z.array(z.string()).min(1).max(100),
3611
+ note: fulfillRequestSchema.shape.note
3293
3612
  });
3294
- const discountSchema = z.object({
3295
- productId: z.string(),
3296
- discountType: z.enum(["RATE", "AMOUNT"]),
3297
- discountValue: z.number().int(),
3298
- discountedPrice: z.number().int(),
3299
- startAt: z.string().nullable(),
3300
- endAt: z.string().nullable()
3613
+ /** `POST /v1/order-items/{orderItemId}/cancel` 응답 — 셀러 직권취소 환불 결과 */
3614
+ const cancelBySellerResultSchema = z.object({
3615
+ orderItemId: z.string(),
3616
+ refundAmount: z.number().int().describe("실제 환불한 금액 — 수량 비율 상품 실결제 금액(쿠폰 배분을 뺀 금액) + 배송비 환불 − 조건부 무료배송 미달 차감 + 되돌린 쿠폰 차감(`couponChargeRefundAmount`)"),
3617
+ quantity: z.number().int().describe("취소한 수량"),
3618
+ canceledQuantity: z.number().int().describe("취소 후 누적 취소 수량"),
3619
+ deliveryFeeRefundAmount: z.number().int().describe("환불에 포함된 배송비. 배송 묶음(같은 상품)이 모두 취소됐고 환불 정책이 켜져 있을 때만 0보다 크다"),
3620
+ deliveryFeeChargeAmount: z.number().int().describe("조건부 무료배송 기준 미달로 환불액에서 뺀 배송비. 정책이 꺼져 있으면 항상 0이다"),
3621
+ couponChargeAmount: z.number().int().optional().default(0).describe("쿠폰 조건 미달 차감. 판매자 취소는 차감하지 않으므로 늘 0이다. 서버는 항상 싣는다"),
3622
+ couponChargeRefundAmount: z.number().int().optional().default(0).describe("환불액에 더한, 앞선 환불에서 뺀 쿠폰 차감을 되돌린 몫. 서버는 항상 싣는다"),
3623
+ refundedAt: z.string()
3301
3624
  });
3302
- const productSchema = productSummarySchema.extend({
3303
- description: z.string().optional(),
3304
- /**
3305
- * 상세설명 에디터(@avarlabs/editor)의 직렬화 상태. 재편집 때 이 값으로 문서를 복원한다.
3306
- * 표시는 `description`(HTML)이 맡는다 — 소비자는 이 필드를 몰라도 된다.
3307
- */
3308
- descriptionJson: z.string().nullable().optional(),
3309
- originalPrice: z.number().int().nullable().optional(),
3310
- images: z.array(z.url()).optional(),
3311
- attributes: z.array(z.object({
3312
- attributeId: z.string(),
3313
- value: z.string()
3314
- })).optional(),
3315
- optionGroups: z.array(optionGroupSchema),
3316
- /** 모든 상품이 최소 1개 variant를 갖는다 (옵션 없으면 default variant 1개) */
3317
- variants: z.array(productVariantSchema),
3318
- fulfillment: productFulfillmentResponseSchema,
3319
- fulfillmentTypeLocked: z.boolean().describe("판매 이력(주문상품)이 있어 상품 유형(`fulfillment.type`)을 바꿀 수 없는가. true면 다른 유형으로 수정하는 요청이 409 `FULFILLMENT_TYPE_LOCKED`다. 같은 유형 안의 속성은 바꿀 수 있다"),
3320
- refundPolicy: productRefundPolicyPatchSchema.nullable().describe("상품별 환불 정책 덮어쓰기(상품이 정한 항목만). null이면 스토어 기본 환불 정책을 쓴다. 반품·교환 배송비(`returnDeliveryFee`·`exchangeDeliveryFee`)는 배송 상품에만 있다"),
3321
- /**
3322
- * 설정된 즉시할인. `GET /products/{productId}/discount`의 응답과 같은 형태다(이슈 #51).
3323
- * 할인이 없으면 null이다. 이 필드를 싣기 전 서버의 응답에는 없으므로 optional이다.
3324
- */
3325
- discount: discountSchema.nullable().optional().describe("설정된 즉시할인. GET /v1/products/{productId}/discount 응답과 같은 형태다. 할인이 없으면 null"),
3326
- createdAt: z.string().optional()
3625
+ /** `PUT /v1/order-items/{orderItemId}/memo` 응답 — 빈 문자열로 저장하면 메모가 지워져 `null`이다 */
3626
+ const orderItemMemoResultSchema = z.object({
3627
+ orderItemId: z.string(),
3628
+ sellerMemo: z.string().nullable()
3629
+ });
3630
+ /** `POST /v1/order-items/{orderItemId}/purchase-decision/request` 응답 */
3631
+ const purchaseDecisionRequestResultSchema = z.object({
3632
+ orderItemId: z.string(),
3633
+ requestedAt: z.string()
3634
+ });
3635
+ /** `POST /v1/order-items/{orderItemId}/purchase-decision/extend` 응답 — 연장은 최대 2회 */
3636
+ const purchaseDecisionExtendResultSchema = z.object({
3637
+ orderItemId: z.string(),
3638
+ autoDecisionDate: z.string(),
3639
+ extendCount: z.number().int().max(2)
3327
3640
  });
3328
- /** 상품 하나에 등록할 수 있는 이미지 최대 장수 — 첫 장이 대표(썸네일)다 */
3329
- const MAX_PRODUCT_IMAGES = 10;
3330
3641
  /**
3331
- * 상품 이미지 주소의 와이어 형식 — URL이고 2048자 이하다.
3332
- *
3333
- * https·확장자 규칙은 여기서 강제하지 않고 `productImageRejection`이 판정한다. 서버가
3334
- * **새로 추가된 주소에만** 그 규칙을 적용해야 하기 때문이다(규칙이 생기기 전에 저장된
3335
- * 주소 하나로 상품 수정 전체가 막히면 셀러가 빠져나갈 길이 없다).
3336
- *
3337
- * 응답 스키마(`productSchema.images`)도 넓게 둔다. 과거에 저장된 주소를 파싱 오류로
3338
- * 만들면 상품 조회 자체가 막힌다.
3642
+ * `GET /v1/orders/{orderId}/address-change-requests` 응답 — 배송지 변경 요청 플로우는 아직 제공하지 않아
3643
+ * 현재는 항상 빈 배열이다
3339
3644
  */
3340
- const productImageUrlSchema = z.url().max(2048).describe("상품 이미지 주소. https만 허용하고, 확장자가 있으면 이미지 형식이어야 한다");
3645
+ const addressChangeRequestListSchema = z.array(z.record(z.string(), z.unknown())).describe("배송지 변경 요청 목록 (현재 항상 빈 배열)");
3646
+ //#endregion
3647
+ //#region src/schemas/saved-option-set.ts
3341
3648
  /**
3342
- * 상품에 **새로 붙이는** 이미지 주소의 규칙. 통과하면 `null`.
3649
+ * 저장 옵션 (이슈 #59) — 자주 쓰는 옵션 구성을 이름 붙여 두고 상품 등록에서 불러온다.
3343
3650
  *
3344
- * - https만 받는다. http는 https 스토어프론트에서 혼합 콘텐츠로 차단되고 `data:`·`javascript:`는
3345
- * 상세 화면에 박히는 주입 경로가 된다.
3346
- * - 호스트는 제한하지 않는다 — 셀러가 이미 쓰는 이미지 호스트를 그대로 붙일 수 있어야 한다.
3347
- * - 경로에 확장자가 있으면 상품 이미지 형식이어야 한다(svg·동영상 거부). **확장자가 없으면 통과시킨다**
3348
- * — 외부 이미지 CDN은 확장자 없는 키를 쓰는 경우가 흔하고, 주소만으로는 실제 형식을 알 수 없다.
3349
- */
3350
- function productImageRejection(url) {
3351
- if (url.length > 2048) return {
3352
- code: "INVALID_IMAGE_URL",
3353
- message: "이미지 주소가 너무 깁니다"
3354
- };
3355
- let parsed;
3356
- try {
3357
- parsed = new URL(url);
3358
- } catch {
3359
- return {
3360
- code: "INVALID_IMAGE_URL",
3361
- message: "이미지 주소 형식이 아닙니다"
3362
- };
3363
- }
3364
- if (parsed.protocol !== "https:") return {
3365
- code: "INVALID_IMAGE_URL",
3366
- message: "이미지 주소는 https로 시작해야 합니다"
3367
- };
3368
- const extension = uploadExtensionOf(parsed.pathname);
3369
- const allowed = UPLOAD_PURPOSE_LIMITS["product-image"].extensions;
3370
- if (extension && !allowed.includes(extension)) return {
3371
- code: "UNSUPPORTED_FILE_TYPE",
3372
- message: `상품 이미지는 ${allowed.join(", ")}만 쓸 수 있습니다`
3373
- };
3374
- return null;
3375
- }
3376
- /** 옵션 축(그룹) 최대 개수 — 스키마 상한의 단일 원천 */
3377
- const MAX_OPTION_GROUPS = 3;
3378
- /** 축 하나가 가질 수 있는 값의 최대 개수 */
3379
- const MAX_VALUES_PER_GROUP = 100;
3380
- /** 상품 하나가 가질 수 있는 variant(SKU) 최대 개수 */
3381
- const MAX_VARIANTS = 500;
3382
- /**
3383
- * 식별 코드 — 빈 문자열·공백만 있는 값은 거부한다.
3384
- * `""`를 통과시키면 서버 중복 사전 검사(falsy skip)와 DB partial unique
3385
- * (`WHERE ... is not null`)의 판정이 갈려 409 대신 500이 나간다.
3651
+ * **불러오기는 복사다.** 저장 옵션을 고치거나 지워도 이미 불러온 상품의 옵션은 그대로다. 재고·SKU·사용 여부는 상품마다
3652
+ * 달라 저장하지 않는다. 추가금은 조합별이 아니라 **값별**로 둔다 — 조합 옵션으로 불러오면 조합 추가금은 고른 값 추가금의 합이다.
3386
3653
  */
3387
- const codeSchema = z.string().trim().min(1).max(64);
3388
- const optionValueRequestSchema = z.object({
3389
- /** 전달 시 기존 값 행을 유지(UPDATE). 생략하면 신규 발급 */
3390
- valueId: z.string().optional(),
3391
- name: z.string().min(1).max(50)
3654
+ /** 스토어당 저장 옵션 최대 개수 */
3655
+ const MAX_SAVED_OPTION_SETS = 200;
3656
+ /** 저장 옵션 이름 최대 글자 수 */
3657
+ const MAX_SAVED_OPTION_SET_NAME = 50;
3658
+ /** 옵션명 방식 — `combination`(조합: 값의 조합이 판매 단위가 된다)·`addon`(추가 선택: 재고 없이 추가금만) */
3659
+ const optionGroupKindSchema = z.enum(["combination", "addon"]);
3660
+ const nameSchema = z.string().trim().min(1).max(50);
3661
+ const savedOptionValueSchema = z.object({
3662
+ name: z.string(),
3663
+ additionalPrice: z.number().int().describe("값별 추가금(원, 0 이상)")
3392
3664
  });
3393
- const optionGroupRequestSchema = z.object({
3394
- /** 전달 시 기존 축 행을 유지(UPDATE). 생략하면 신규 발급 */
3395
- groupId: z.string().optional(),
3396
- name: z.string().min(1).max(50),
3397
- values: z.array(optionValueRequestSchema).min(1).max(100)
3665
+ const savedOptionGroupSchema = z.object({
3666
+ name: z.string(),
3667
+ kind: z.string().describe("옵션명 방식. 현재 값은 `combination`·`addon`이고 값이 추가될 수 있어 문자열로 받는다"),
3668
+ required: z.boolean().describe("추가 선택을 구매자가 꼭 고르는가. 조합 옵션명은 항상 false다"),
3669
+ values: z.array(savedOptionValueSchema)
3670
+ });
3671
+ const savedCustomInputSchema = z.object({
3672
+ label: z.string(),
3673
+ placeholder: z.string().nullable(),
3674
+ maxLength: z.number().int(),
3675
+ required: z.boolean()
3676
+ });
3677
+ const savedOptionSetSchema = z.object({
3678
+ optionSetId: z.string(),
3679
+ name: z.string(),
3680
+ optionSort: optionSortSchema,
3681
+ groups: z.array(savedOptionGroupSchema).describe("옵션명. 배열 순서가 입력 순서다"),
3682
+ customInputs: z.array(savedCustomInputSchema),
3683
+ importedProductCount: z.number().int().describe("이 저장 옵션을 불러온 상품 수(중복 없이, 지운 상품 제외). 불러오기 기록으로 센다"),
3684
+ createdAt: z.string(),
3685
+ updatedAt: z.string()
3398
3686
  });
3399
- const productVariantRequestSchema = z.object({
3400
- /** 기존 variant 유지 시 전달. 생략하면 신규 생성 */
3401
- variantId: z.string().optional(),
3402
- /** optionGroups 순서대로 각 축에서 고른 값 이름. 서버가 이름 → valueId를 해석한다 */
3403
- valueNames: z.array(z.string()).default([]),
3404
- additionalPrice: z.number().int().default(0),
3405
- /**
3406
- * 생략 = 기존 재고 유지 (신규 variant는 0).
3407
- * 구조 교체(PUT)가 폼 로드 시점의 재고를 되쓰지 않게 하는 옵트인 필드다 —
3408
- * 재고만 바꿀 때는 PATCH /products/{id}/variants/{variantId}/stock을 쓴다.
3409
- */
3410
- stockQuantity: z.number().int().min(0).optional(),
3411
- sku: codeSchema.optional(),
3412
- barcode: codeSchema.optional(),
3413
- costPrice: z.number().int().min(0).optional(),
3414
- weightGram: z.number().int().min(0).optional(),
3415
- sellerCode: codeSchema.optional(),
3416
- usable: z.boolean().default(true)
3687
+ const savedOptionValueRequestSchema = z.object({
3688
+ name: nameSchema,
3689
+ additionalPrice: z.number().int().min(0).default(0)
3417
3690
  });
3418
- /** 축 × 값 → variant 전체 교체 요청 (PUT /products/{productId}/options) */
3419
- const optionModelRequestSchema = z.object({
3420
- optionGroups: z.array(optionGroupRequestSchema).max(3).default([]),
3421
- variants: z.array(productVariantRequestSchema).min(1).max(500)
3691
+ const savedOptionGroupRequestSchema = z.object({
3692
+ name: nameSchema,
3693
+ kind: optionGroupKindSchema,
3694
+ required: z.boolean().default(false).describe("추가 선택만 쓴다. 조합 옵션명에 true를 주면 400이다"),
3695
+ values: z.array(savedOptionValueRequestSchema).min(1).max(100)
3422
3696
  });
3423
- /** GET /products/{productId}/options 응답 */
3424
- const optionModelSchema = z.object({
3425
- optionGroups: z.array(optionGroupSchema),
3426
- variants: z.array(productVariantSchema)
3697
+ const savedCustomInputRequestSchema = z.object({
3698
+ label: z.string().trim().min(1).max(30),
3699
+ placeholder: z.string().trim().max(100).nullish(),
3700
+ maxLength: z.number().int().min(1).max(200),
3701
+ required: z.boolean().default(false)
3427
3702
  });
3428
3703
  /**
3429
- * 재고 변경 택일 위반 문구 — SDK `.refine`과 서버 DomainException이 공유하는 단일 원천.
3430
- * 경로에 따라 셀러가 다른 문구를 받지 않게 한다.
3431
- */
3432
- const VARIANT_STOCK_EXCLUSIVE_MESSAGE = "stockQuantity 또는 adjustment 중 하나만 전달해야 합니다";
3433
- /**
3434
- * variant 재고 변경 요청 — 절대값(`stockQuantity`) 또는 증감(`adjustment`) 택일.
3435
- * 서버도 같은 규칙을 400 VALIDATION_FAILED로 검증한다(이중 방어).
3704
+ * 저장 옵션 만들기·수정 본문. 조합 옵션명은 최대 `MAX_OPTION_GROUPS`개, 추가 선택은 최대 `MAX_ADDON_GROUPS`개이고
3705
+ * 옵션명과 직접 입력 항목이 모두 비면 400이다. 수정(PUT)은 통째로 바꾼다.
3436
3706
  */
3437
- const variantStockRequestSchema = z.object({
3438
- stockQuantity: z.number().int().min(0).optional(),
3439
- adjustment: z.number().int().optional()
3440
- }).refine((v) => v.stockQuantity !== void 0 !== (v.adjustment !== void 0), { message: VARIANT_STOCK_EXCLUSIVE_MESSAGE });
3441
- /** PATCH /products/{productId}/variants/{variantId}/stock 응답 */
3442
- const variantStockSchema = z.object({
3443
- variantId: z.string(),
3444
- stockQuantity: z.number().int()
3707
+ const savedOptionSetRequestSchema = z.object({
3708
+ name: z.string().trim().min(1).max(50),
3709
+ optionSort: optionSortSchema.default("REGISTERED"),
3710
+ groups: z.array(savedOptionGroupRequestSchema).max(8).default([]),
3711
+ customInputs: z.array(savedCustomInputRequestSchema).max(5).default([])
3445
3712
  });
3446
3713
  /**
3447
- * 상품 쓰기 요청의 공통 필드 — 등록(POST)과 전체 수정(PUT)이 같다. 이행(`fulfillment`)만 두 요청의 스키마가 다르다.
3714
+ * 상품에서 저장 (`POST /v1/saved-option-sets/from-product`) — 상품의 현재 옵션을 저장 옵션으로 만든다.
3448
3715
  *
3449
- * 최상위가 `strictObject`다. 모르는 필드는 400이다 — 이슈 #57 이전의 평면 필드(`deliveryFee`·`fulfillmentType`·`returnFee` 등)를
3450
- * 보낸 요청이 조용히 무시되지 않게 한다. 옛 필드는 `fulfillment.{유형}.…`과 `refundPolicy`로 옮겼다.
3716
+ * 조합 추가금이 값별 추가금의 합으로 되돌려지면 값별로 저장하고, 조합마다 제각각이면 추가금을 빼고
3717
+ * 응답 `additionalPriceDropped: true`로 알린다. `replaceOptionSetId`를 주면 그 저장 옵션을 덮어쓴다(이름 중복의 덮어쓰기).
3451
3718
  */
3452
- const productWriteShape = {
3453
- name: z.string().min(1).max(100),
3454
- categoryId: z.string().min(1, "카테고리를 선택해주십시오"),
3455
- salePrice: z.number().int().min(10).multipleOf(10, "판매가는 10원 단위여야 합니다 (INVALID_PRICE_UNIT)").describe("판매가. 원(KRW) 단위 정수, 최소 10원, 10원 단위"),
3456
- originalPrice: z.number().int().optional().describe("정가(할인 전 표시 가격). 원 단위"),
3457
- description: z.string().optional(),
3458
- /**
3459
- * 상세설명 에디터(@avarlabs/editor)의 직렬화 상태. 재편집 때 이 값으로 문서를 복원한다.
3460
- * 표시는 `description`(HTML)이 맡는다 — 소비자는 이 필드를 몰라도 된다.
3461
- */
3462
- descriptionJson: z.string().nullable().optional(),
3463
- images: z.array(productImageUrlSchema).min(1).max(10).optional().describe("상품 이미지 주소 목록. 첫 장이 대표 이미지(썸네일)다"),
3464
- attributes: z.array(z.object({
3465
- attributeId: z.string(),
3466
- value: z.string()
3467
- })).optional(),
3468
- refundPolicy: productRefundPolicyPatchSchema.nullish().describe("상품별 환불 정책 — 준 항목만 스토어 기본을 덮어쓴다. `null`이나 생략은 스토어 기본을 그대로 쓴다는 뜻이다. 반품·교환 배송비(`returnDeliveryFee`·`exchangeDeliveryFee`)는 배송 상품에만 줄 수 있다(다른 유형은 400)"),
3469
- ...optionModelRequestSchema.shape
3470
- };
3471
- const productCreateRequestSchema = z.strictObject({
3472
- ...productWriteShape,
3473
- fulfillment: productFulfillmentCreateSchema.optional().describe("상품 이행. `type`이 유형이고 같은 이름의 키가 유형별 속성이다. 생략하면 배송 상품(기본값)이다. 배송 상품이 아닌 유형은 판매가 열린 유형만 등록할 수 있다(409 `FULFILLMENT_TYPE_NOT_AVAILABLE`)")
3474
- });
3475
- const fulfillmentUpdateDescription = "상품 이행. 생략하면 바꾸지 않는다. 주면 유형 안 필드를 모두 담아 통째로 바꾼다(현재 값과 병합하지 않는다). 판매 이력이 있으면 유형을 바꿀 수 없다(409 `FULFILLMENT_TYPE_LOCKED`)";
3476
- /** 전체 수정(PUT) — 공통 필드는 등록과 같고, 이행은 생략하면 현재 값 유지, 주면 필드가 모두 필수다 */
3477
- const productReplaceRequestSchema = z.strictObject({
3478
- ...productWriteShape,
3479
- fulfillment: productFulfillmentUpdateSchema.optional().describe(fulfillmentUpdateDescription)
3480
- });
3481
- const productPatchRequestSchema = z.strictObject({
3482
- name: z.string().max(100).optional(),
3483
- salePrice: z.number().int().multipleOf(10).optional(),
3484
- status: productStatusSchema.optional(),
3485
- fulfillment: productFulfillmentUpdateSchema.optional().describe(fulfillmentUpdateDescription),
3486
- refundPolicy: productRefundPolicyPatchSchema.nullable().optional().describe("상품별 환불 정책. 주면 통째로 바꾼다(준 항목만 스토어 기본을 덮어쓴다). `null`은 덮어쓰기를 지워 스토어 기본을 따른다. 생략하면 바꾸지 않는다")
3487
- }).refine((value) => Object.values(value).some((v) => v !== void 0), { message: "최소 1개 필드가 필요합니다" });
3488
- const productSortSchema = z.enum([
3489
- "createdAt.desc",
3490
- "createdAt.asc",
3491
- "salePrice.asc",
3492
- "salePrice.desc",
3493
- "updatedAt.desc"
3719
+ const savedOptionSetFromProductRequestSchema = z.object({
3720
+ productId: z.string(),
3721
+ name: z.string().trim().min(1).max(50),
3722
+ includeOptions: z.boolean().default(true).describe("옵션명(조합·추가 선택)을 저장하는가"),
3723
+ includeCustomInputs: z.boolean().default(true).describe("직접 입력 항목을 저장하는가"),
3724
+ replaceOptionSetId: z.string().optional().describe("덮어쓸 저장 옵션 id. 생략하면 새로 만든다")
3725
+ });
3726
+ const savedOptionSetFromProductResultSchema = z.object({
3727
+ optionSet: savedOptionSetSchema,
3728
+ additionalPriceDropped: z.boolean().describe("조합 추가금을 값별 추가금으로 되돌릴 수 없어 추가금을 빼고 저장했는가")
3729
+ });
3730
+ /** 구성 필터 — 조합 옵션명·추가 선택 옵션명·직접 입력 항목을 가진 저장 옵션 */
3731
+ const savedOptionSetCompositionSchema = z.enum([
3732
+ "combination",
3733
+ "addon",
3734
+ "customInput"
3494
3735
  ]);
3495
- /** `GET /v1/products` 쿼리 — OpenAPI 문서용 형태(값은 쿼리 문자열) */
3496
- const productListQuerySchema = z.object({
3497
- status: productStatusSchema.optional().describe("판매 상태 필터"),
3498
- categoryId: z.string().optional().describe("카테고리 필터 — 하위 카테고리의 상품도 포함"),
3499
- keyword: z.string().optional().describe("상품명 검색 (포함)"),
3500
- sellerCode: z.string().optional().describe("판매자 상품코드 — 이 코드가 붙은 variant를 가진 상품"),
3501
- sort: productSortSchema.optional().describe("정렬 (기본 createdAt.desc)"),
3502
- ...pageQueryShape()
3503
- });
3504
- const discountRequestSchema = z.object({
3505
- discountType: z.enum(["RATE", "AMOUNT"]),
3506
- discountValue: z.number().int().min(1),
3507
- startAt: z.string().optional(),
3508
- endAt: z.string().optional()
3736
+ const savedOptionSetSortSchema = z.enum([
3737
+ "updatedAt.desc",
3738
+ "name.asc",
3739
+ "importedProductCount.desc"
3740
+ ]);
3741
+ /** `GET /v1/saved-option-sets` 쿼리 — OpenAPI 문서용 형태(값은 쿼리 문자열) */
3742
+ const savedOptionSetListQuerySchema = z.object({
3743
+ keyword: z.string().optional().describe("이름 검색 (포함, 대소문자 무시)"),
3744
+ composition: savedOptionSetCompositionSchema.optional().describe("구성 필터"),
3745
+ sort: savedOptionSetSortSchema.optional().describe("정렬 (기본 updatedAt.desc)"),
3746
+ ...pageQueryShape({
3747
+ defaultSize: 20,
3748
+ maxSize: 200
3749
+ })
3509
3750
  });
3510
3751
  //#endregion
3511
3752
  //#region src/schemas/shipping-policy.ts
@@ -4188,6 +4429,7 @@ const sellerAddressRequestSchema = z.object({
4188
4429
  * - 409 `STOREFRONT_SITE_CREATE_LIMIT` — 사이트는 스토어당 30일에 3번까지 만든다
4189
4430
  * - 409 `STOREFRONT_SITE_CHANGED` — 읽은 상태(`expected*`)와 현재 상태가 다르다. 다시 읽고 시도한다
4190
4431
  * - 409 `STOREFRONT_SITE_NOT_ACTIVE` — 정지(`SUSPENDED`)된 사이트는 바꾸거나 끌 수 없다
4432
+ * - 409 `STORE_SUSPENDED` — 정지된 스토어에는 사이트를 만들 수 없다
4191
4433
  * - 409 `STOREFRONT_VERSION_NOT_READY` — 빌드가 끝나지 않았거나 실패한 버전은 발행할 수 없다
4192
4434
  * - 409 `STOREFRONT_NO_ROLLBACK_TARGET` — 되돌릴 이전 발행 버전이 없다
4193
4435
  * - 400 `STOREFRONT_SLUG_CONFIRM_MISMATCH` — 호스팅 끄기 확인 입력이 현재 슬러그와 다르다
@@ -4325,6 +4567,14 @@ const storefrontProvisioningSchema = z.object({
4325
4567
  * 인증서가 덮으므로 따로 준비할 것이 없다. 이 주소는 편집 미리보기 링크(콘솔 전용)로 받은 쿠키가 있어야 열린다.
4326
4568
  */
4327
4569
  const storefrontEditPreviewSchema = z.object({ hostname: z.string().nullable().describe("편집 미리보기 호스트 (예: `sfs-ab12cd34-preview.sayren.co`). 편집을 쓸 수 없으면 null") });
4570
+ /**
4571
+ * 사이트 정지 — 운영 정책에 따라 플랫폼 운영자가 사이트를 정지한다. 정지 중에는 방문자에게 정지 안내가 보이고 발행·되돌리기·템플릿 변경·
4572
+ * 공개 전환·슬러그 변경·끄기·편집이 409 `STOREFRONT_SITE_NOT_ACTIVE`다. 셀러가 풀 수 없다(고객센터로 문의한다).
4573
+ */
4574
+ const storefrontSuspensionSchema = z.object({
4575
+ reason: z.string().nullable().describe("정지 사유. 운영자가 적은 안내이고 없으면 null"),
4576
+ suspendedAt: z.string().nullable().describe("정지한 시각 (ISO 8601)")
4577
+ });
4328
4578
  const storefrontSiteSchema = z.object({
4329
4579
  siteId: z.string(),
4330
4580
  slug: z.string().describe("주소 슬러그"),
@@ -4339,6 +4589,7 @@ const storefrontSiteSchema = z.object({
4339
4589
  provisioning: storefrontProvisioningSchema,
4340
4590
  editPreview: storefrontEditPreviewSchema,
4341
4591
  slugChangesRemaining: z.number().int().min(0).describe(`최근 30일 기준 남은 슬러그 변경 횟수`),
4592
+ suspension: storefrontSuspensionSchema.nullable().optional().describe("정지 정보. `status`가 `SUSPENDED`일 때만 있고 그 밖은 null"),
4342
4593
  createdAt: z.string(),
4343
4594
  updatedAt: z.string()
4344
4595
  });
@@ -4907,6 +5158,21 @@ function createStoreClient(options) {
4907
5158
  setDiscount: (productId, body) => storeHttp.request("PUT", path`/products/${productId}/discount`, discountSchema, { body: discountRequestSchema.parse(body) }),
4908
5159
  removeDiscount: (productId) => storeHttp.requestVoid("DELETE", path`/products/${productId}/discount`)
4909
5160
  },
5161
+ /**
5162
+ * 저장 옵션 (이슈 #59) — 자주 쓰는 옵션 구성. 불러오기는 복사라 저장 옵션을 고쳐도 불러온 상품은 바뀌지 않는다.
5163
+ * 권한은 상품과 같다(`product:r`·`product:rw`).
5164
+ */
5165
+ savedOptionSets: {
5166
+ list: (params) => storeHttp.request("GET", "/saved-option-sets", pageSchema(savedOptionSetSchema), { query: params }),
5167
+ get: (optionSetId) => storeHttp.request("GET", path`/saved-option-sets/${optionSetId}`, savedOptionSetSchema),
5168
+ /** 이름이 겹치면 409 `OPTION_SET_NAME_TAKEN`, 스토어당 200개를 넘으면 409 `OPTION_SET_LIMIT_EXCEEDED` */
5169
+ create: (body) => storeHttp.request("POST", "/saved-option-sets", savedOptionSetSchema, { body: savedOptionSetRequestSchema.parse(body) }),
5170
+ /** 통째로 바꾼다. 불러온 상품은 바뀌지 않는다 */
5171
+ update: (optionSetId, body) => storeHttp.request("PUT", path`/saved-option-sets/${optionSetId}`, savedOptionSetSchema, { body: savedOptionSetRequestSchema.parse(body) }),
5172
+ remove: (optionSetId) => storeHttp.requestVoid("DELETE", path`/saved-option-sets/${optionSetId}`),
5173
+ /** 상품의 현재 옵션을 저장 옵션으로 만든다(또는 `replaceOptionSetId`를 덮어쓴다) */
5174
+ saveFromProduct: (body) => storeHttp.request("POST", "/saved-option-sets/from-product", savedOptionSetFromProductResultSchema, { body: savedOptionSetFromProductRequestSchema.parse(body) })
5175
+ },
4910
5176
  categories: {
4911
5177
  list: (params) => storeHttp.request("GET", "/categories", z.array(categorySchema), { query: params }),
4912
5178
  tree: () => storeHttp.request("GET", "/categories", z.array(categoryTreeNodeSchema), { query: { tree: true } }),
@@ -5607,7 +5873,12 @@ const orderWebhookEventDataSchema = z.object({
5607
5873
  testPayment: z.boolean().optional().default(false).describe("테스트 결제(샌드박스 결제) 주문인가. true면 실제로 돈이 오가지 않았다. 이 필드가 없는 이전 이벤트는 false로 읽는다"),
5608
5874
  couponDiscountAmount: z.number().int().optional().default(0).describe("주문이 받은 상품·주문 쿠폰 할인 합계 (원). 즉시할인(`discountAmount`)과 별개다. 쿠폰이 없거나 이 필드가 없는 이전 이벤트는 0으로 읽는다"),
5609
5875
  orderItemId: z.string().optional().describe("이벤트를 만든 주문상품 ID. 배송 없는 상품 제공으로 보내는 `ORDER.DELIVERED`에만 실린다. 택배 배송완료와 그 밖의 주문 이벤트에는 없다"),
5610
- fulfillmentSnapshot: orderItemFulfillmentSnapshotResponseSchema.optional().describe("이벤트를 만든 주문상품의 주문 시점 이행 스냅샷(주문상품 응답의 `fulfillmentSnapshot`과 같다, 이슈 #57). 배송 없는 상품의 제공 완료는 `ORDER.DELIVERED`로도 오고 `type`이 `MANUAL`이다. `orderItemId`가 있는 이벤트에만 실린다")
5876
+ fulfillmentSnapshot: orderItemFulfillmentSnapshotResponseSchema.optional().describe("이벤트를 만든 주문상품의 주문 시점 이행 스냅샷(주문상품 응답의 `fulfillmentSnapshot`과 같다, 이슈 #57). 배송 없는 상품의 제공 완료는 `ORDER.DELIVERED`로도 오고 `type`이 `MANUAL`이다. `orderItemId`가 있는 이벤트에만 실린다"),
5877
+ itemOptions: z.array(z.object({
5878
+ orderItemId: z.string(),
5879
+ optionSelections: z.array(optionSelectionSchema),
5880
+ customInputs: z.array(customInputValueSchema)
5881
+ })).optional().describe("추가 선택·직접 입력이 있는 주문상품의 주문 시점 선택값(이슈 #59, 주문상품 응답의 `optionSelections`·`customInputs`와 같다). `ORDER.PAID`에만 실리고 그런 주문상품이 없으면 없다")
5611
5882
  });
5612
5883
  /**
5613
5884
  * `ORDER.FULFILLED` — 배송 없는 주문상품(`MANUAL`)을 제공했다(이슈 #48). 셀러의 제공 처리
@@ -5877,4 +6148,4 @@ function createStoreSession(options) {
5877
6148
  };
5878
6149
  }
5879
6150
  //#endregion
5880
- export { APPROVAL_ALREADY_DECIDED_CODE, APPROVAL_REQUIRED_CODE, ApiError, ApprovalPendingError, CASH_RECEIPT_STATUS_VALUES, COUPON_CODE_PATTERN, COUPON_CONDITIONS_DEFAULTS, COUPON_TARGET_ALL, COUPON_TARGET_LIST_MAX, FULFILLMENT_RULES, MAX_ANALYTICS_ALLOWED_ORIGINS, MAX_AUTO_TRANSITION_DAYS, MAX_OPTION_GROUPS, MAX_PRODUCT_IMAGES, MAX_VALUES_PER_GROUP, MAX_VARIANTS, MCP_TELEMETRY_MAX_RESULTS, MIN_PAYMENT_AMOUNT, NO_COUPON_SETTLEMENT, NO_SHIPPING_BUNDLE_KEY, PAYMENT_ROUTING_EXCLUSION_CODES, PORTONE_CHANNEL_NAME_MAX_LENGTH, PORTONE_MAX_CHANNELS, PORTONE_MIN_CHANNELS, REFUND_POLICY_DEFAULTS, REFUND_POLICY_DELIVERY_FEE_KEYS, REMOTE_AREA_ZIP_RANGES, RESERVED_STOREFRONT_SLUGS, RESERVED_STORE_CODES, SHIPPING_POLICY_DEFAULTS, STOREFRONT_BLOCKED_PATH_SEGMENTS, STOREFRONT_BUILD_STAGES, STOREFRONT_BUILD_TERMINAL_STATUSES, STOREFRONT_CHECK_TERMINAL_STATUSES, STOREFRONT_EDIT_ACTIVITY_TIMEOUT_MINUTES, STOREFRONT_EDIT_CAPACITY, STOREFRONT_EDIT_IDLE_MINUTES, STOREFRONT_EDIT_PREVIEW_SUFFIX, STOREFRONT_EDIT_START_STAGES, STOREFRONT_FILE_BATCH_MAX, STOREFRONT_FILE_MAX_BYTES, STOREFRONT_PATH_SEGMENT_PATTERN, STOREFRONT_SEARCH_REGEX_MAX_LENGTH, STOREFRONT_SITE_CREATE_LIMIT, STOREFRONT_SLUG_CHANGE_LIMIT, STOREFRONT_SLUG_CHANGE_WINDOW_DAYS, STOREFRONT_SLUG_HOLD_DAYS, STOREFRONT_SLUG_PATTERN, STOREFRONT_WORKSPACE_MAX_BYTES, STORE_CODE_PATTERN, STORE_ROLE_SCOPES, STORE_SCOPE_RESOURCES, STORE_SESSION_KEYS, TOSS_WIDGET_CLIENT_KEY_PATTERN, TOSS_WIDGET_KEY_MESSAGE, TOSS_WIDGET_SECRET_KEY_PATTERN, UPLOAD_PURPOSE_LIMITS, VARIANT_STOCK_EXCLUSIVE_MESSAGE, WEBHOOK_DEFAULT_TOLERANCE_SEC, WEBHOOK_DELIVERY_ID_HEADER, WEBHOOK_EVENT_ID_HEADER, WEBHOOK_EVENT_TYPE_HEADER, WEBHOOK_FAILURE_REASONS, WEBHOOK_HEADERS, WEBHOOK_PAYLOAD_EXCERPT_MAX_BYTES, WEBHOOK_RESOURCE_TYPES, WEBHOOK_SIGNATURE_HEADER, WEBHOOK_USER_AGENT, addressChangeRequestListSchema, addressTypeSchema, agentPolicyDecisionSchema, allocateByWeight, analyticsChannelSchema, analyticsCheckoutOutcomeSchema, analyticsCheckoutSchema, analyticsDeviceRowSchema, analyticsDevicesSchema, analyticsFunnelStepSchema, analyticsOriginSchema, analyticsOverviewSchema, analyticsPagesQuerySchema, analyticsPagesSchema, analyticsProductRowSchema, analyticsProductSortSchema, analyticsProductsQuerySchema, analyticsProductsSchema, analyticsRangeQuerySchema, analyticsRealtimeSchema, analyticsSearchQuerySchema, analyticsSearchSchema, analyticsSourceGroupBySchema, analyticsSourceRowSchema, analyticsSourcesQuerySchema, analyticsSourcesSchema, analyticsTotalsSchema, apiClientListQuerySchema, apiClientSchema, apiClientSecretRotatedSchema, apiClientStatusSchema, apiClientTypeSchema, apiLogErrorSchema, apiLogListQuerySchema, apiLogPageSchema, apiLogPrincipalSchema, apiLogPrincipalTypeSchema, apiLogSchema, apiLogStatusFilterSchema, apiLogSummarySchema, apiTokenCreateRequestSchema, apiTokenCreatedSchema, apiTokenSchema, approvalDecisionRequestSchema, approvalListQuerySchema, approvalPageSchema, approvalRequestSchema, approvalStatusSchema, approveClaimResultSchema, auditActorSchema, auditActorTypeSchema, auditLogListQuerySchema, auditLogOutcomeSchema, auditLogPageSchema, auditLogSchema, auditLogSourceSchema, buildAuthorizeUrl, buildQuery, bulkResultSchema, cancelBySellerRequestSchema, cancelBySellerResultSchema, carrierSchema, cashReceiptSchema, categoryAttributeSchema, categoryCreateRequestSchema, categoryDeleteQuerySchema, categoryDeleteStrategySchema, categoryListQuerySchema, categoryPatchRequestSchema, categoryProductSortSchema, categorySchema, categorySearchResultSchema, categoryTreeNodeSchema, categoryWithPathSchema, changeStorefrontSlugRequestSchema, changeStorefrontTemplateRequestSchema, changeTypeSchema, claimDetailSchema, claimHoldResultSchema, claimListQuerySchema, claimReasonSchema, claimReturnAddressSchema, claimSchema, claimStatusSchema, claimTypeSchema, claimWebhookEventDataSchema, claimWebhookEventSchema, clientCredentialsTokenResponseSchema, consentActionSchema, consentKindSchema, consentSourceSchema, couponBenefitSchema, couponConditionsInputSchema, couponConditionsSchema, couponDiscountOf, couponIssueListQuerySchema, couponIssueMethodSchema, couponIssueSchema, couponIssueStatusSchema, couponKindSchema, couponListQuerySchema, couponRedemptionListQuerySchema, couponRedemptionSchema, couponRejectReasonSchema, couponRuleSchema, couponSchema, couponStatsSchema, couponStatusSchema, couponTargetInputSchema, couponTargetSchema, createCouponRequestSchema, createPkcePair, createStoreClient, createStoreSession, createStorefrontEditPreviewLinkRequestSchema, createStorefrontPreviewLinkRequestSchema, createStorefrontSiteRequestSchema, createStorefrontVersionRequestSchema, customerAddressSchema, customerConsentListQuerySchema, customerConsentSchema, customerConsentStateSchema, customerConsentsSchema, customerCouponSchema, customerDetailSchema, customerInquiryCategorySchema, customerInquiryListQuerySchema, customerListQuerySchema, customerOrderSchema, customerSortSchema, customerStatsSchema, customerSummarySchema, customerWithdrawalBlockedDetailsSchema, deleteStorefrontFileRequestSchema, deliveryBundleKeyOf, deliveryFeeWaiverSchema, deliveryInfoSchema, deliveryMethodSchema, deliveryStatusSchema, deliveryTypeSchema, disableStorefrontHostingRequestSchema, discountRequestSchema, discountSchema, dispatchRequestSchema, easyPayProviderResponseSchema, easyPayProviderSchema, eventTypeSchema, exchangeAuthorizationCode, exchangeStoreTokenViaOAuth, fulfillBulkRequestSchema, fulfillRequestSchema, fulfillmentRuleOf, fulfillmentTypeOf, fulfillmentTypeResponseSchema, fulfillmentTypeSchema, holdOrderItemRequestSchema, inquiryListQuerySchema, inquirySchema, inquiryWebhookEventDataSchema, inquiryWebhookEventSchema, invitationDeliveryStatusSchema, invitationListQuerySchema, invitationSchema, inviteMemberRequestSchema, isCouponTargetLine, isEditableStorefrontPath, isReservedStoreCode, isReservedStorefrontSlug, isStoreScope, isWebOrigin, issueCouponRequestSchema, issueCouponResultSchema, liveGateConditionCodeSchema, liveGateConditionSchema, liveGateErrorDetailsSchema, liveGateSchema, loginProviderSchema, loginProviderStatusSchema, loginProviderTestSchema, loginSettingsSchema, maskedSecretSchema, mcpRuleResultSchema, mcpToolEventSchema, moveStorefrontFileRequestSchema, myAccountSchema, myStoreSchema, normalizeCouponCode, normalizeStorefrontPath, oauthClientRegistrationRequestSchema, oauthClientRegistrationResponseSchema, oauthTokenResponseSchema, openFulfillmentUnion, optionGroupRequestSchema, optionGroupSchema, optionModelRequestSchema, optionModelSchema, optionValueRequestSchema, optionValueSchema, orderCanceledWebhookEventDataSchema, orderCanceledWebhookEventSchema, orderDiscountSchema, orderFulfilledWebhookEventDataSchema, orderFulfilledWebhookEventSchema, orderItemFulfillmentSchema, orderItemFulfillmentSnapshotResponseSchema, orderItemFulfillmentSnapshotSchema, orderItemListQuerySchema, orderItemMemoResultSchema, orderItemPageSchema, orderItemSchema, orderItemStatusSchema, orderListQuerySchema, orderSchema, orderSummaryPageSchema, orderSummarySchema, orderWebhookEventDataSchema, orderWebhookEventSchema, pageInfoSchema, pageQueryShape, pageSchema, paymentAttemptRecordSchema, paymentAttemptStatusSchema, paymentCredentialSetSchema, paymentListQuerySchema, paymentProviderSchema, paymentProviderVerificationSchema, paymentRecordDetailSchema, paymentRecordPageSchema, paymentRecordSchema, paymentRecordStatusSchema, paymentRefundRecordSchema, paymentSettingsSchema, pgEnvironmentSchema, pgPaymentMethodSchema, pgProviderSchema, pgVerificationResultSchema, policyDocumentUrlSchema, policyVersionSchema, portOneChannelSchema, portOneCredentialsInputSchema, productCreateRequestSchema, productFulfillmentCreateSchema, productFulfillmentResponseSchema, productFulfillmentSchema, productFulfillmentUpdateSchema, productImageRejection, productImageUrlSchema, productListQuerySchema, productOutOfStockWebhookEventDataSchema, productOutOfStockWebhookEventSchema, productPatchRequestSchema, productRefundPolicyOverride, productRefundPolicyPatchSchema, productReplaceRequestSchema, productSchema, productSortSchema, productStatusSchema, productSummarySchema, productVariantRequestSchema, productVariantSchema, proratedPaidRefundBase, proratedRefundBase, publishStorefrontVersionRequestSchema, purchaseDecisionExtendResultSchema, purchaseDecisionRequestResultSchema, quoteDelivery, quotePricing, refreshUserToken, refundPolicyPatchSchema, refundPolicySchema, rejectClaimRequestSchema, remoteAreaLabelOf, remoteAreaOf, remoteAreaSchema, remoteSurchargeOf, requiresCollection, requiresShipping, resolveRefundPolicy, resolveShippingPolicy, reviewListQuerySchema, reviewReportResultSchema, reviewSchema, reviewSortSchema, reviewWebhookEventDataSchema, reviewWebhookEventSchema, rollbackStorefrontRequestSchema, runStorefrontCheckRequestSchema, salesReportGroupBySchema, salesReportQuerySchema, salesReportRowSchema, salesReportSchema, salesTotalsSchema, saveLoginProviderRequestSchema, scopeAccessSchema, searchStorefrontFilesRequestSchema, sellerAddressListQuerySchema, sellerAddressRequestSchema, sellerAddressSchema, sellerCustomerInquirySchema, setStorefrontVisibilityRequestSchema, settleCouponShortfall, settlementDetailQuerySchema, settlementDetailSchema, settlementListQuerySchema, settlementStatusSchema, settlementSummarySchema, settlementWithDetailsSchema, shippingAddressSchema, shippingPolicyPatchSchema, shippingPolicySchema, storeCodeSchema, storeMemberRoleUpdatedSchema, storeMemberSchema, storeRoleSchema, storeSchema, storeScopeResourceSchema, storeTokenResponseSchema, storefrontBuildListQuerySchema, storefrontBuildSchema, storefrontBuildStatusSchema, storefrontCheckSchema, storefrontCheckStatusSchema, storefrontDomainSchema, storefrontEditPreviewLabelOf, storefrontEditPreviewSchema, storefrontEditSessionSchema, storefrontEditSessionStatusSchema, storefrontEditingSchema, storefrontFileConflictDetailsSchema, storefrontFileContentQuerySchema, storefrontFileContentSchema, storefrontFileDeleteResultSchema, storefrontFileEncodingSchema, storefrontFileEntrySchema, storefrontFileHashSchema, storefrontFileListQuerySchema, storefrontFileListSchema, storefrontFileMoveResultSchema, storefrontFilePathSchema, storefrontFileSearchResultSchema, storefrontFileWriteResultSchema, storefrontHostingSchema, storefrontPreviewLinkSchema, storefrontProvisioningSchema, storefrontRuleResultSchema, storefrontRuleSeveritySchema, storefrontSiteSchema, storefrontSiteStatusSchema, storefrontSlugAvailabilityQuerySchema, storefrontSlugAvailabilitySchema, storefrontSlugSchema, storefrontSourceModeSchema, storefrontTemplateSchema, storefrontVerifyReportSchema, storefrontVersionCreatedSchema, storefrontVersionSchema, storefrontVersionStatusSchema, storefrontVisibilitySchema, tossPaymentsCredentialsInputSchema, trackingStageResponseSchema, trackingUpdateResultSchema, updateCouponRequestSchema, updateLoginProviderRequestSchema, updateLoginSettingsRequestSchema, updateMemberRoleRequestSchema, updatePaymentProviderRequestSchema, updatePaymentSettingsRequestSchema, updateStoreRequestSchema, uploadExtensionOf, uploadPurposeSchema, uploadRejection, uploadRequestSchema, uploadTicketSchema, upsertPortOneProviderRequestSchema, upsertTossPaymentsProviderRequestSchema, userTokenResponseSchema, variantStockRequestSchema, variantStockSchema, verifyPaymentProviderRequestSchema, webhookCreatedSchema, webhookDeliveryDetailSchema, webhookDeliveryErrorCodeSchema, webhookDeliveryListQuerySchema, webhookDeliveryResourceSchema, webhookDeliverySchema, webhookDeliveryStatusSchema, webhookEventSchema, webhookFailureReasonSchema, webhookRedeliverFailedResultSchema, webhookRequestHeaderSchema, webhookRequestSchema, webhookResourceTypeSchema, webhookSchema, webhookSecretRotatedSchema, webhookStatusSchema, webhookTestRequestSchema, webhookTestResultSchema, withdrawCustomerRequestSchema, writeDryRunResultSchema, writePreviewSchema, writeStorefrontFilesRequestSchema };
6151
+ export { APPROVAL_ALREADY_DECIDED_CODE, APPROVAL_REQUIRED_CODE, ApiError, ApprovalPendingError, CASH_RECEIPT_STATUS_VALUES, COUPON_CODE_PATTERN, COUPON_CONDITIONS_DEFAULTS, COUPON_TARGET_ALL, COUPON_TARGET_LIST_MAX, CUSTOM_INPUT_FORBIDDEN_WORDS, FULFILLMENT_RULES, MAX_ADDON_GROUPS, MAX_ANALYTICS_ALLOWED_ORIGINS, MAX_AUTO_TRANSITION_DAYS, MAX_CUSTOM_INPUTS, MAX_CUSTOM_INPUT_LENGTH, MAX_OPTION_GROUPS, MAX_PRODUCT_IMAGES, MAX_SAVED_OPTION_SETS, MAX_SAVED_OPTION_SET_NAME, MAX_VALUES_PER_GROUP, MAX_VARIANTS, MCP_TELEMETRY_MAX_RESULTS, MIN_PAYMENT_AMOUNT, NO_COUPON_SETTLEMENT, NO_SHIPPING_BUNDLE_KEY, PAYMENT_ROUTING_EXCLUSION_CODES, PORTONE_CHANNEL_NAME_MAX_LENGTH, PORTONE_MAX_CHANNELS, PORTONE_MIN_CHANNELS, REFUND_POLICY_DEFAULTS, REFUND_POLICY_DELIVERY_FEE_KEYS, REMOTE_AREA_ZIP_RANGES, RESERVED_STOREFRONT_SLUGS, RESERVED_STORE_CODES, SHIPPING_POLICY_DEFAULTS, STOREFRONT_BLOCKED_PATH_SEGMENTS, STOREFRONT_BUILD_STAGES, STOREFRONT_BUILD_TERMINAL_STATUSES, STOREFRONT_CHECK_TERMINAL_STATUSES, STOREFRONT_EDIT_ACTIVITY_TIMEOUT_MINUTES, STOREFRONT_EDIT_CAPACITY, STOREFRONT_EDIT_IDLE_MINUTES, STOREFRONT_EDIT_PREVIEW_SUFFIX, STOREFRONT_EDIT_START_STAGES, STOREFRONT_FILE_BATCH_MAX, STOREFRONT_FILE_MAX_BYTES, STOREFRONT_PATH_SEGMENT_PATTERN, STOREFRONT_SEARCH_REGEX_MAX_LENGTH, STOREFRONT_SITE_CREATE_LIMIT, STOREFRONT_SLUG_CHANGE_LIMIT, STOREFRONT_SLUG_CHANGE_WINDOW_DAYS, STOREFRONT_SLUG_HOLD_DAYS, STOREFRONT_SLUG_PATTERN, STOREFRONT_WORKSPACE_MAX_BYTES, STORE_CODE_PATTERN, STORE_ROLE_SCOPES, STORE_SCOPE_RESOURCES, STORE_SESSION_KEYS, TOSS_WIDGET_CLIENT_KEY_PATTERN, TOSS_WIDGET_KEY_MESSAGE, TOSS_WIDGET_SECRET_KEY_PATTERN, UPLOAD_PURPOSE_LIMITS, VARIANT_COUNT_WARNING, VARIANT_STOCK_EXCLUSIVE_MESSAGE, WEBHOOK_DEFAULT_TOLERANCE_SEC, WEBHOOK_DELIVERY_ID_HEADER, WEBHOOK_EVENT_ID_HEADER, WEBHOOK_EVENT_TYPE_HEADER, WEBHOOK_FAILURE_REASONS, WEBHOOK_HEADERS, WEBHOOK_PAYLOAD_EXCERPT_MAX_BYTES, WEBHOOK_RESOURCE_TYPES, WEBHOOK_SIGNATURE_HEADER, WEBHOOK_USER_AGENT, addonGroupRequestSchema, addonGroupSchema, addonValueRequestSchema, addonValueSchema, addressChangeRequestListSchema, addressTypeSchema, agentPolicyDecisionSchema, allocateByWeight, analyticsChannelSchema, analyticsCheckoutOutcomeSchema, analyticsCheckoutSchema, analyticsDeviceRowSchema, analyticsDevicesSchema, analyticsFunnelStepSchema, analyticsOriginSchema, analyticsOverviewSchema, analyticsPagesQuerySchema, analyticsPagesSchema, analyticsProductRowSchema, analyticsProductSortSchema, analyticsProductsQuerySchema, analyticsProductsSchema, analyticsRangeQuerySchema, analyticsRealtimeSchema, analyticsSearchQuerySchema, analyticsSearchSchema, analyticsSourceGroupBySchema, analyticsSourceRowSchema, analyticsSourcesQuerySchema, analyticsSourcesSchema, analyticsTotalsSchema, apiClientListQuerySchema, apiClientSchema, apiClientSecretRotatedSchema, apiClientStatusSchema, apiClientTypeSchema, apiLogErrorSchema, apiLogListQuerySchema, apiLogPageSchema, apiLogPrincipalSchema, apiLogPrincipalTypeSchema, apiLogSchema, apiLogStatusFilterSchema, apiLogSummarySchema, apiTokenCreateRequestSchema, apiTokenCreatedSchema, apiTokenSchema, approvalDecisionRequestSchema, approvalListQuerySchema, approvalPageSchema, approvalRequestSchema, approvalStatusSchema, approveClaimResultSchema, auditActorSchema, auditActorTypeSchema, auditLogListQuerySchema, auditLogOutcomeSchema, auditLogPageSchema, auditLogSchema, auditLogSourceSchema, buildAuthorizeUrl, buildQuery, bulkResultSchema, cancelBySellerRequestSchema, cancelBySellerResultSchema, carrierSchema, cashReceiptSchema, categoryAttributeSchema, categoryCreateRequestSchema, categoryDeleteQuerySchema, categoryDeleteStrategySchema, categoryListQuerySchema, categoryPatchRequestSchema, categoryProductSortSchema, categorySchema, categorySearchResultSchema, categoryTreeNodeSchema, categoryWithPathSchema, changeStorefrontSlugRequestSchema, changeStorefrontTemplateRequestSchema, changeTypeSchema, claimDetailSchema, claimHoldResultSchema, claimListQuerySchema, claimReasonSchema, claimReturnAddressSchema, claimSchema, claimStatusSchema, claimTypeSchema, claimWebhookEventDataSchema, claimWebhookEventSchema, clientCredentialsTokenResponseSchema, consentActionSchema, consentKindSchema, consentSourceSchema, couponBenefitSchema, couponConditionsInputSchema, couponConditionsSchema, couponDiscountOf, couponIssueListQuerySchema, couponIssueMethodSchema, couponIssueSchema, couponIssueStatusSchema, couponKindSchema, couponListQuerySchema, couponRedemptionListQuerySchema, couponRedemptionSchema, couponRejectReasonSchema, couponRuleSchema, couponSchema, couponStatsSchema, couponStatusSchema, couponTargetInputSchema, couponTargetSchema, createCouponRequestSchema, createPkcePair, createStoreClient, createStoreSession, createStorefrontEditPreviewLinkRequestSchema, createStorefrontPreviewLinkRequestSchema, createStorefrontSiteRequestSchema, createStorefrontVersionRequestSchema, customInputLabelForbiddenWord, customInputRequestSchema, customInputSchema, customInputValueSchema, customerAddressSchema, customerConsentListQuerySchema, customerConsentSchema, customerConsentStateSchema, customerConsentsSchema, customerCouponSchema, customerDetailSchema, customerInquiryCategorySchema, customerInquiryListQuerySchema, customerListQuerySchema, customerOrderSchema, customerSortSchema, customerStatsSchema, customerSummarySchema, customerWithdrawalBlockedDetailsSchema, deleteStorefrontFileRequestSchema, deliveryBundleKeyOf, deliveryFeeWaiverSchema, deliveryInfoSchema, deliveryMethodSchema, deliveryStatusSchema, deliveryTypeSchema, disableStorefrontHostingRequestSchema, discountRequestSchema, discountSchema, dispatchRequestSchema, easyPayProviderResponseSchema, easyPayProviderSchema, eventTypeSchema, exchangeAuthorizationCode, exchangeStoreTokenViaOAuth, fulfillBulkRequestSchema, fulfillRequestSchema, fulfillmentRuleOf, fulfillmentTypeOf, fulfillmentTypeResponseSchema, fulfillmentTypeSchema, holdOrderItemRequestSchema, inquiryListQuerySchema, inquirySchema, inquiryWebhookEventDataSchema, inquiryWebhookEventSchema, invitationDeliveryStatusSchema, invitationListQuerySchema, invitationSchema, inviteMemberRequestSchema, isCouponTargetLine, isEditableStorefrontPath, isReservedStoreCode, isReservedStorefrontSlug, isStoreScope, isWebOrigin, issueCouponRequestSchema, issueCouponResultSchema, liveGateConditionCodeSchema, liveGateConditionSchema, liveGateErrorDetailsSchema, liveGateSchema, loginProviderSchema, loginProviderStatusSchema, loginProviderTestSchema, loginSettingsSchema, maskedSecretSchema, mcpRuleResultSchema, mcpToolEventSchema, moveStorefrontFileRequestSchema, myAccountSchema, myStoreSchema, normalizeCouponCode, normalizeStorefrontPath, oauthClientRegistrationRequestSchema, oauthClientRegistrationResponseSchema, oauthTokenResponseSchema, openFulfillmentUnion, optionGroupKindSchema, optionGroupRequestSchema, optionGroupSchema, optionModelRequestSchema, optionModelSchema, optionSelectionSchema, optionSortSchema, optionValueRequestSchema, optionValueSchema, orderCanceledWebhookEventDataSchema, orderCanceledWebhookEventSchema, orderDiscountSchema, orderFulfilledWebhookEventDataSchema, orderFulfilledWebhookEventSchema, orderItemFulfillmentSchema, orderItemFulfillmentSnapshotResponseSchema, orderItemFulfillmentSnapshotSchema, orderItemListQuerySchema, orderItemMemoResultSchema, orderItemPageSchema, orderItemSchema, orderItemStatusSchema, orderListQuerySchema, orderSchema, orderSummaryPageSchema, orderSummarySchema, orderWebhookEventDataSchema, orderWebhookEventSchema, pageInfoSchema, pageQueryShape, pageSchema, paymentAttemptRecordSchema, paymentAttemptStatusSchema, paymentCredentialSetSchema, paymentListQuerySchema, paymentProviderSchema, paymentProviderVerificationSchema, paymentRecordDetailSchema, paymentRecordPageSchema, paymentRecordSchema, paymentRecordStatusSchema, paymentRefundRecordSchema, paymentSettingsSchema, pgEnvironmentSchema, pgPaymentMethodSchema, pgProviderSchema, pgVerificationResultSchema, policyDocumentUrlSchema, policyVersionSchema, portOneChannelSchema, portOneCredentialsInputSchema, productCreateRequestSchema, productFulfillmentCreateSchema, productFulfillmentResponseSchema, productFulfillmentSchema, productFulfillmentUpdateSchema, productImageRejection, productImageUrlSchema, productListQuerySchema, productOutOfStockWebhookEventDataSchema, productOutOfStockWebhookEventSchema, productPatchRequestSchema, productRefundPolicyOverride, productRefundPolicyPatchSchema, productReplaceRequestSchema, productSchema, productSortSchema, productStatusSchema, productSummarySchema, productVariantRequestSchema, productVariantSchema, proratedPaidRefundBase, proratedRefundBase, publishStorefrontVersionRequestSchema, purchaseDecisionExtendResultSchema, purchaseDecisionRequestResultSchema, quoteDelivery, quotePricing, refreshUserToken, refundPolicyPatchSchema, refundPolicySchema, rejectClaimRequestSchema, remoteAreaLabelOf, remoteAreaOf, remoteAreaSchema, remoteSurchargeOf, requiresCollection, requiresShipping, resolveRefundPolicy, resolveShippingPolicy, reviewListQuerySchema, reviewReportResultSchema, reviewSchema, reviewSortSchema, reviewWebhookEventDataSchema, reviewWebhookEventSchema, rollbackStorefrontRequestSchema, runStorefrontCheckRequestSchema, salesReportGroupBySchema, salesReportQuerySchema, salesReportRowSchema, salesReportSchema, salesTotalsSchema, saveLoginProviderRequestSchema, savedCustomInputRequestSchema, savedCustomInputSchema, savedOptionGroupRequestSchema, savedOptionGroupSchema, savedOptionSetCompositionSchema, savedOptionSetFromProductRequestSchema, savedOptionSetFromProductResultSchema, savedOptionSetListQuerySchema, savedOptionSetRequestSchema, savedOptionSetSchema, savedOptionSetSortSchema, savedOptionValueRequestSchema, savedOptionValueSchema, scopeAccessSchema, searchStorefrontFilesRequestSchema, sellerAddressListQuerySchema, sellerAddressRequestSchema, sellerAddressSchema, sellerCustomerInquirySchema, setStorefrontVisibilityRequestSchema, settleCouponShortfall, settlementDetailQuerySchema, settlementDetailSchema, settlementListQuerySchema, settlementStatusSchema, settlementSummarySchema, settlementWithDetailsSchema, shippingAddressSchema, shippingPolicyPatchSchema, shippingPolicySchema, storeCodeSchema, storeMemberRoleUpdatedSchema, storeMemberSchema, storeRoleSchema, storeSchema, storeScopeResourceSchema, storeTokenResponseSchema, storefrontBuildListQuerySchema, storefrontBuildSchema, storefrontBuildStatusSchema, storefrontCheckSchema, storefrontCheckStatusSchema, storefrontDomainSchema, storefrontEditPreviewLabelOf, storefrontEditPreviewSchema, storefrontEditSessionSchema, storefrontEditSessionStatusSchema, storefrontEditingSchema, storefrontFileConflictDetailsSchema, storefrontFileContentQuerySchema, storefrontFileContentSchema, storefrontFileDeleteResultSchema, storefrontFileEncodingSchema, storefrontFileEntrySchema, storefrontFileHashSchema, storefrontFileListQuerySchema, storefrontFileListSchema, storefrontFileMoveResultSchema, storefrontFilePathSchema, storefrontFileSearchResultSchema, storefrontFileWriteResultSchema, storefrontHostingSchema, storefrontPreviewLinkSchema, storefrontProvisioningSchema, storefrontRuleResultSchema, storefrontRuleSeveritySchema, storefrontSiteSchema, storefrontSiteStatusSchema, storefrontSlugAvailabilityQuerySchema, storefrontSlugAvailabilitySchema, storefrontSlugSchema, storefrontSourceModeSchema, storefrontSuspensionSchema, storefrontTemplateSchema, storefrontVerifyReportSchema, storefrontVersionCreatedSchema, storefrontVersionSchema, storefrontVersionStatusSchema, storefrontVisibilitySchema, tossPaymentsCredentialsInputSchema, trackingStageResponseSchema, trackingUpdateResultSchema, updateCouponRequestSchema, updateLoginProviderRequestSchema, updateLoginSettingsRequestSchema, updateMemberRoleRequestSchema, updatePaymentProviderRequestSchema, updatePaymentSettingsRequestSchema, updateStoreRequestSchema, uploadExtensionOf, uploadPurposeSchema, uploadRejection, uploadRequestSchema, uploadTicketSchema, upsertPortOneProviderRequestSchema, upsertTossPaymentsProviderRequestSchema, userTokenResponseSchema, variantStockRequestSchema, variantStockSchema, verifyPaymentProviderRequestSchema, webhookCreatedSchema, webhookDeliveryDetailSchema, webhookDeliveryErrorCodeSchema, webhookDeliveryListQuerySchema, webhookDeliveryResourceSchema, webhookDeliverySchema, webhookDeliveryStatusSchema, webhookEventSchema, webhookFailureReasonSchema, webhookRedeliverFailedResultSchema, webhookRequestHeaderSchema, webhookRequestSchema, webhookResourceTypeSchema, webhookSchema, webhookSecretRotatedSchema, webhookStatusSchema, webhookTestRequestSchema, webhookTestResultSchema, withdrawCustomerRequestSchema, writeDryRunResultSchema, writePreviewSchema, writeStorefrontFilesRequestSchema };