@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.mjs CHANGED
@@ -975,7 +975,7 @@ const claimSchema = z.object({
975
975
  carrierCode: z.string().nullable(),
976
976
  trackingNumber: z.string().nullable()
977
977
  }).nullable(),
978
- fulfillmentType: z.string().optional().default("SHIPPING").describe("클레임 대상 주문상품의 상품 유형(이슈 #48). 수거가 없는 비실물(`DOWNLOAD`·`CODE`·`SERVICE`) 반품은 수거 등록 없이 접수·보류에서 바로 승인한다. 값이 추가될 수 있어 문자열로 받는다. 서버는 항상 싣는다"),
978
+ fulfillmentType: z.string().optional().default("SHIPPING").describe("클레임 대상 주문상품의 상품 유형(이슈 #48). 배송 없는 상품(`MANUAL`) 반품은 수거 등록 없이 접수·보류에서 바로 승인한다. 값이 추가될 수 있어 문자열로 받는다. 서버는 항상 싣는다"),
979
979
  requestedAt: z.string(),
980
980
  completedAt: z.string().nullable(),
981
981
  /** 보류 중이면 자동 해제 시각(ISO 8601), 아니면 null */
@@ -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
  /**
@@ -2031,140 +2032,6 @@ const customerWithdrawalBlockedDetailsSchema = z.object({
2031
2032
  pendingPayments: z.number().int()
2032
2033
  });
2033
2034
  //#endregion
2034
- //#region src/schemas/digital-asset.ts
2035
- /**
2036
- * 파일 다운로드 상품의 판매 파일 (이슈 #48 단계 3).
2037
- *
2038
- * 파일은 CDN **비공개 객체**다. `POST /v1/uploads`(`purpose: "digital-asset"`)로 자리를 받아 응답의 `headers`와 함께 PUT하고,
2039
- * 응답의 `key`로 상품에 연결한다. 객체 키와 파일 주소는 어떤 응답에도 싣지 않는다 — 구매자는 제공된 주문상품에서
2040
- * 수명 300초의 서명 주소로만 받는다. 주문은 주문 시점 파일을 스냅샷하므로 파일을 바꿔도 이미 산 구매자는 옛 파일을 받는다.
2041
- */
2042
- const digitalAssetAttachRequestSchema = z.object({
2043
- key: z.string().min(1).max(512).describe("업로드 자리 발급 응답의 `key`. 이 스토어의 비공개 업로드 키여야 한다"),
2044
- fileName: z.string().trim().min(1).max(200).describe("구매자에게 보일 파일 이름(확장자 포함)"),
2045
- sizeBytes: z.number().int().positive().optional().describe("파일 크기(바이트). 안내용이다"),
2046
- contentType: z.string().max(200).optional().describe("파일 형식(MIME). 안내용이다"),
2047
- variantId: z.string().optional().describe("이 파일을 줄 옵션(variant). 생략하면 모든 옵션의 구매자가 받는다")
2048
- });
2049
- /** 판매 파일 — 객체 키·주소는 싣지 않는다 */
2050
- const digitalAssetSchema = z.object({
2051
- assetId: z.string(),
2052
- productId: z.string(),
2053
- variantId: z.string().nullable().describe("옵션 전용 파일이면 그 옵션, 모든 옵션 공통이면 null"),
2054
- fileName: z.string(),
2055
- contentType: z.string().nullable(),
2056
- sizeBytes: z.number().int().nullable(),
2057
- status: z.string().describe("`ACTIVE`(새 주문에 제공)·`REMOVED`(해제 — 이미 산 구매자는 계속 받는다). 값이 추가될 수 있어 문자열로 받는다"),
2058
- createdAt: z.string()
2059
- });
2060
- /** 셀러 확인용 서명 주소 — `POST /v1/products/{productId}/digital-assets/{assetId}/preview-url` */
2061
- const digitalAssetPreviewSchema = z.object({
2062
- url: z.url().describe("수명 300초의 서명 주소"),
2063
- expiresAt: z.string()
2064
- });
2065
- /** 주문상품의 다운로드 제공 정보 — 주문 응답의 `fulfillment.download` */
2066
- const orderItemDownloadSchema = z.object({
2067
- count: z.number().int().describe("지금까지 받은 횟수"),
2068
- limit: z.number().int().describe("받을 수 있는 횟수"),
2069
- expiresAt: z.string().nullable().describe("받을 수 있는 기한 (ISO 8601)"),
2070
- firstDownloadedAt: z.string().nullable().describe("처음 받은 시각 — 제공 개시 증거다"),
2071
- files: z.array(z.object({
2072
- assetId: z.string(),
2073
- fileName: z.string(),
2074
- sizeBytes: z.number().int().nullable()
2075
- }))
2076
- });
2077
- //#endregion
2078
- //#region src/schemas/fulfillment-code.ts
2079
- /**
2080
- * 코드 발급 상품의 코드 풀 (이슈 #48 단계 2) — 라이선스 키·기프트 코드처럼 결제 확정과 함께 한 개씩 배정하는 코드다.
2081
- *
2082
- * **평문 코드는 두 경로로만 나간다** — 구매자 열람(`POST /storefront/v1/me/order-items/{id}/fulfillment/reveal`)과 셀러 콘솔
2083
- * 열람(`POST /v1/fulfillment-codes/{codeId}/reveal`, 콘솔 전용). 목록·주문·웹훅·API 로그에는 가린 값(`hint`)만 싣는다.
2084
- * 코드는 결제 자격 증명과 같은 키링·AES-256-GCM으로 암호화해 저장한다.
2085
- */
2086
- /**
2087
- * 코드 상태.
2088
- * - `AVAILABLE` 판매 가능 — 재고로 센다
2089
- * - `ASSIGNED` 주문상품에 배정됨
2090
- * - `REVOKED` 환불로 회수됨(구매자 화면에서 숨긴다. 코드 자체의 무효화는 셀러 시스템 몫이다)
2091
- * - `EXPIRED` 유효기간이 지나 배정하지 않음
2092
- * - `VOID` 셀러가 폐기함
2093
- */
2094
- const fulfillmentCodeStatusSchema = z.enum([
2095
- "AVAILABLE",
2096
- "ASSIGNED",
2097
- "REVOKED",
2098
- "EXPIRED",
2099
- "VOID"
2100
- ]);
2101
- /** 한 번에 올릴 수 있는 코드 수 */
2102
- const MAX_CODES_PER_UPLOAD = 1e4;
2103
- /** 코드 한 개의 최대 길이 */
2104
- const MAX_CODE_LENGTH = 200;
2105
- /**
2106
- * 코드 일괄 업로드 요청 — `POST /v1/products/{productId}/codes`. `codes`(배열)와 `csv`(본문) 중 하나를 보낸다.
2107
- * 앞뒤 공백은 지우고 빈 줄은 건너뛴다. 같은 상품에 이미 있는 코드와 요청 안의 중복은 받지 않는다(`duplicates`).
2108
- */
2109
- const fulfillmentCodeUploadRequestSchema = z.object({
2110
- variantId: z.string().optional().describe("코드를 채울 옵션(variant) ID. 옵션 없는 상품은 생략한다(기본 옵션)"),
2111
- codes: z.array(z.string()).max(MAX_CODES_PER_UPLOAD).optional().describe(`코드 목록. 한 번에 ${MAX_CODES_PER_UPLOAD}개까지. \`csv\`와 둘 중 하나만 보낸다`),
2112
- csv: z.string().max(4e6).optional().describe(`CSV 본문 — 줄마다 첫 칸이 코드다. 첫 줄의 첫 칸이 \`code\`면 머리글로 보고 건너뛴다. 한 번에 ${MAX_CODES_PER_UPLOAD}줄까지`),
2113
- expiresAt: z.iso.datetime({ offset: true }).optional().describe("이 업로드 코드의 유효기간 (ISO 8601). 지나면 배정하지 않고 만료로 바꾼다")
2114
- }).refine((value) => value.codes === void 0 !== (value.csv === void 0), { message: "codes와 csv 중 하나만 보내야 합니다" });
2115
- /** 받지 않은 줄 */
2116
- const fulfillmentCodeRejectionSchema = z.object({
2117
- line: z.number().int().describe("요청 안의 위치 (1부터). CSV는 머리글을 포함한 줄 번호다"),
2118
- reason: z.string().describe("사유. `DUPLICATE`(이미 있는 코드이거나 요청 안에서 겹침)·`TOO_LONG`(200자 초과)·`INVALID_CHARACTER`(제어 문자). 값이 추가될 수 있어 문자열로 받는다")
2119
- });
2120
- /** 업로드 결과 — 배치 요약 */
2121
- const fulfillmentCodeBatchSchema = z.object({
2122
- batchId: z.string(),
2123
- productId: z.string(),
2124
- variantId: z.string(),
2125
- total: z.number().int().describe("요청에서 읽은 코드 수(빈 줄·머리글 제외)"),
2126
- accepted: z.number().int().describe("받은 코드 수 — 재고가 이만큼 는다"),
2127
- duplicates: z.number().int().describe("중복이라 받지 않은 코드 수"),
2128
- rejected: z.array(fulfillmentCodeRejectionSchema).describe("받지 않은 줄(중복 포함). 최대 1,000개까지 싣는다"),
2129
- expiresAt: z.string().nullable(),
2130
- createdAt: z.string()
2131
- });
2132
- /** 코드 목록의 한 줄 — **가린 값만** 싣는다 */
2133
- const fulfillmentCodeSchema = z.object({
2134
- codeId: z.string(),
2135
- productId: z.string(),
2136
- variantId: z.string(),
2137
- batchId: z.string().nullable().describe("업로드 배치. 수동 제공으로 셀러가 넘긴 코드는 null"),
2138
- hint: z.string().describe("가린 코드 — 끝 4자만 보인다(예: `****AB12`)"),
2139
- status: z.string().describe("코드 상태. `AVAILABLE`·`ASSIGNED`·`REVOKED`·`EXPIRED`·`VOID`. 값이 추가될 수 있어 문자열로 받는다"),
2140
- orderItemId: z.string().nullable().describe("배정된 주문상품. 배정 전이면 null"),
2141
- assignedAt: z.string().nullable(),
2142
- revealedAt: z.string().nullable().describe("구매자가 처음 열람한 시각 — 제공 개시 증거다"),
2143
- expiresAt: z.string().nullable(),
2144
- createdAt: z.string()
2145
- });
2146
- const fulfillmentCodePageSchema = pageSchema(fulfillmentCodeSchema).extend({ available: z.number().int().describe("판매 가능한 코드 수(필터와 무관한 상품 전체)") });
2147
- /** `GET /v1/products/{productId}/codes` 쿼리 */
2148
- const fulfillmentCodeListQuerySchema = z.object({
2149
- status: fulfillmentCodeStatusSchema.optional().describe("상태 필터"),
2150
- variantId: z.string().optional().describe("옵션 필터"),
2151
- batchId: z.string().optional().describe("업로드 배치 필터"),
2152
- orderItemId: z.string().optional().describe("주문상품 필터"),
2153
- ...pageQueryShape()
2154
- });
2155
- /** 셀러 콘솔 코드 열람 응답 — `POST /v1/fulfillment-codes/{codeId}/reveal` */
2156
- const fulfillmentCodeRevealSchema = z.object({
2157
- codeId: z.string(),
2158
- code: z.string().describe("코드 평문. 응답은 캐시하지 않는다(`Cache-Control: no-store`)")
2159
- });
2160
- /** 주문상품에 배정된 코드의 가린 값 — 주문 응답의 `fulfillment.codes` */
2161
- const orderItemCodeSchema = z.object({
2162
- codeId: z.string(),
2163
- hint: z.string().describe("가린 코드 — 끝 4자만 보인다"),
2164
- status: z.string().describe("`ASSIGNED`(유효)·`REVOKED`(환불로 회수). 값이 추가될 수 있어 문자열로 받는다"),
2165
- revealedAt: z.string().nullable().describe("구매자가 처음 열람한 시각. 열람 전이면 null")
2166
- });
2167
- //#endregion
2168
2035
  //#region src/schemas/login.ts
2169
2036
  /** 소셜 로그인 공급자 */
2170
2037
  const loginProviderSchema = z.enum([
@@ -2507,83 +2374,43 @@ const reviewReportResultSchema = z.object({
2507
2374
  //#endregion
2508
2375
  //#region src/schemas/fulfillment.ts
2509
2376
  /**
2510
- * 상품 유형과 제공 방식 (이슈 #48) — 배송이 필요 없는 상품을 팔기 위한 축이다.
2377
+ * 상품 유형 (이슈 #48·#57) — 배송이 필요한 상품과 필요 없는 상품을 가르는 축이다.
2511
2378
  *
2512
- * **상품 유형은 제공 방식 하나로 표현한다.** `fulfillmentType` 한 필드가 배송 필요 여부, 이행 흐름, 재고 의미를 정한다.
2379
+ * **상품 유형은 이행 방식 하나로 표현한다.** `fulfillment.type`이 배송 필요 여부, 이행 흐름, 클레임 규칙을 정한다.
2513
2380
  * 코드 곳곳에서 유형 이름으로 분기하지 않고 이 파일의 판정 함수(`requiresShipping`·`fulfillmentRuleOf`)를 지난다.
2514
- * 유형이 늘어나도 판정표 한 곳만 고치면 배송비 계산·결제 시작·클레임·콘솔 버튼이 같이 따라온다.
2515
2381
  *
2516
2382
  * 주문상품 상태 enum은 바꾸지 않는다. 비실물의 **제공 완료는 `DELIVERED`를 재사용한다** — 새 상태를 더하면 기존 소비자의
2517
- * 상태 분기(배송완료 뒤 구매확정 대기, 반품 가능 판정)가 새 값을 모른다. 표시는 `fulfillmentType`으로 가른다.
2383
+ * 상태 분기(배송완료 뒤 구매확정 대기, 반품 가능 판정)가 새 값을 모른다. 표시는 유형으로 가른다.
2518
2384
  */
2519
2385
  /**
2520
2386
  * 상품 유형.
2521
- * - `SHIPPING` 배송 상품(기본, 이 필드 이전의 상품 전부)
2522
- * - `DOWNLOAD` 파일 다운로드
2523
- * - `CODE` 코드 발급(라이선스 키·기프트 코드)
2524
- * - `SERVICE` 이용권·상품권·서비스(예약 없음)
2525
- * - `PICKUP` 방문 수령 실물
2387
+ * - `SHIPPING` 배송 상품. 택배로 발송하고 송장·배송 추적으로 배송완료가 된다
2388
+ * - `MANUAL` 배송 없는 상품(비실물). 셀러가 제공 처리(`POST /v1/order-items/{orderItemId}/fulfill`)로 제공 완료를 기록한다
2389
+ *
2390
+ * 비실물의 세부 구분(파일 다운로드·코드 발급·이용권·방문 수령)은 2026-09-28에 제품에서 뺐다(이슈 #48 범위 축소).
2391
+ * 이름은 이행 방식이다 — `SHIPPING`이 택배 발송이듯 `MANUAL`은 셀러가 직접 제공한다는 뜻이다.
2526
2392
  */
2527
- const fulfillmentTypeSchema = z.enum([
2528
- "SHIPPING",
2529
- "DOWNLOAD",
2530
- "CODE",
2531
- "SERVICE",
2532
- "PICKUP"
2533
- ]);
2534
- /** 제공 방식 — `AUTO`는 결제 확정과 함께 제공하고, `MANUAL`은 셀러가 제공 처리한다 */
2535
- const fulfillmentModeSchema = z.enum(["AUTO", "MANUAL"]);
2393
+ const fulfillmentTypeSchema = z.enum(["SHIPPING", "MANUAL"]);
2536
2394
  /**
2537
2395
  * 응답 스키마용 유형 — 서버가 나중에 유형을 더해도 옛 SDK가 응답 파싱에서 깨지지 않게 문자열로 받는다.
2538
2396
  * 요청은 `fulfillmentTypeSchema`(닫힌 enum)로 검증한다.
2539
2397
  */
2540
- const fulfillmentTypeResponseSchema = z.string().describe("상품 유형. 현재 값은 `SHIPPING`(배송 상품)·`DOWNLOAD`(파일 다운로드)·`CODE`(코드 발급)·`SERVICE`(이용권·서비스)·`PICKUP`(방문 수령)이고 값이 추가될 수 있어 문자열로 받는다");
2541
- /**
2542
- * 유형별 규칙표 — **유일한 원천이다.**
2543
- *
2544
- * `PICKUP`은 실물이라 반품 수거와 교환을 받지만 택배 배송은 없다. `DOWNLOAD`는 결제 즉시 권한을 발급하므로 `AUTO`만,
2545
- * `PICKUP`은 셀러가 수령 완료를 누르므로 `MANUAL`만 둔다.
2546
- */
2398
+ const fulfillmentTypeResponseSchema = z.string().describe("상품 유형. 현재 값은 `SHIPPING`(배송 상품)·`MANUAL`(배송 없는 상품, 셀러가 제공 처리)이고 값이 추가될 수 있어 문자열로 받는다");
2399
+ /** 유형별 규칙표 — **유일한 원천이다.** */
2547
2400
  const FULFILLMENT_RULES = {
2548
2401
  SHIPPING: {
2549
2402
  requiresShipping: true,
2550
- modes: [],
2551
- defaultMode: null,
2552
2403
  requiresCollection: true,
2553
2404
  exchangeSupported: true
2554
2405
  },
2555
- DOWNLOAD: {
2556
- requiresShipping: false,
2557
- modes: ["AUTO"],
2558
- defaultMode: "AUTO",
2559
- requiresCollection: false,
2560
- exchangeSupported: false
2561
- },
2562
- CODE: {
2563
- requiresShipping: false,
2564
- modes: ["AUTO", "MANUAL"],
2565
- defaultMode: "AUTO",
2566
- requiresCollection: false,
2567
- exchangeSupported: false
2568
- },
2569
- SERVICE: {
2406
+ MANUAL: {
2570
2407
  requiresShipping: false,
2571
- modes: ["MANUAL", "AUTO"],
2572
- defaultMode: "MANUAL",
2573
2408
  requiresCollection: false,
2574
2409
  exchangeSupported: false
2575
- },
2576
- PICKUP: {
2577
- requiresShipping: false,
2578
- modes: ["MANUAL"],
2579
- defaultMode: "MANUAL",
2580
- requiresCollection: true,
2581
- exchangeSupported: true
2582
2410
  }
2583
2411
  };
2584
2412
  /**
2585
- * 저장된 값 → 유형. null·모르는 값은 `SHIPPING`이다 — 이 필드 이전의 주문상품(`order_items.fulfillment_type` null)과
2586
- * 상품은 모두 배송 상품이었다.
2413
+ * 저장된 값 → 유형. null·모르는 값은 `SHIPPING`이다 — 이 필드 이전의 상품·주문상품은 모두 배송 상품이었다.
2587
2414
  */
2588
2415
  function fulfillmentTypeOf(value) {
2589
2416
  const parsed = fulfillmentTypeSchema.safeParse(value);
@@ -2602,16 +2429,6 @@ function requiresCollection(type) {
2602
2429
  return fulfillmentRuleOf(type).requiresCollection;
2603
2430
  }
2604
2431
  /**
2605
- * 유효 제공 방식 — 요청 값이 유형에 허용되면 그 값, 생략이면 유형 기본값이다. 배송 상품은 null이다.
2606
- * 허용되지 않는 값은 `null`을 돌려주지 않고 호출부가 거부하도록 `undefined`를 돌려준다.
2607
- */
2608
- function resolveFulfillmentMode(type, mode) {
2609
- const rule = FULFILLMENT_RULES[type];
2610
- if (rule.modes.length === 0) return null;
2611
- if (mode === null || mode === void 0) return rule.defaultMode;
2612
- return rule.modes.includes(mode) ? mode : void 0;
2613
- }
2614
- /**
2615
2432
  * 배송이 없는 주문상품의 배송 묶음 키 (`order_items.delivery_bundle_key`).
2616
2433
  *
2617
2434
  * null로 두면 환불 정산이 #29 이전 주문의 레거시 폴백(「묶음 = 같은 상품」, 상품의 **현재** 배송비)을 탄다.
@@ -2625,16 +2442,10 @@ const deliveryTypeSchema = z.enum([
2625
2442
  "PAID",
2626
2443
  "CONDITIONAL_FREE"
2627
2444
  ]);
2628
- /** 파일 다운로드 기본값 — 횟수 5회·기간 30일(#48 결정 8) */
2629
- const DOWNLOAD_DEFAULTS = {
2630
- limit: 5,
2631
- days: 30
2632
- };
2633
2445
  const KNOWN_FULFILLMENT_TYPES = fulfillmentTypeSchema.options;
2634
2446
  const ADDRESS_ID_DESCRIPTIONS = {
2635
2447
  shipping: "출고지 주소 id(`GET /v1/seller-addresses?type=SHIPPING`). 등록에서 생략하면 그 시점 스토어 기본 출고지를 저장한다. `null`이면 비워 둔다(스토어 기본 출고지를 따른다). 이 스토어의 `SHIPPING` 주소가 아니면 거부한다",
2636
- return: "반품지 주소 id(`GET /v1/seller-addresses?type=RETURN`). 등록에서 생략하면 그 시점 스토어 기본 반품지를 저장한다. `null`이면 비워 둔다(스토어 기본 반품지를 따른다). 이 스토어의 `RETURN` 주소가 아니면 거부한다",
2637
- pickup: "방문 수령지 주소 id(`GET /v1/seller-addresses?type=PICKUP`). 등록에서 생략하면 그 시점 스토어 기본 방문 수령지를 저장하고, 기본 방문 수령지도 없으면 400 `PICKUP_ADDRESS_REQUIRED`다. 이 스토어의 `PICKUP` 주소가 아니면 거부한다"
2448
+ return: "반품지 주소 id(`GET /v1/seller-addresses?type=RETURN`). 등록에서 생략하면 그 시점 스토어 기본 반품지를 저장한다. `null`이면 비워 둔다(스토어 기본 반품지를 따른다). 이 스토어의 `RETURN` 주소가 아니면 거부한다"
2638
2449
  };
2639
2450
  /**
2640
2451
  * 배송비 필드 사이의 일관성 — FREE의 배송비, 조건부 무료가 아닌 상품의 무료 기준을 조용히 0·null로 바꿔 저장하지 않고 400으로 돌려준다
@@ -2667,51 +2478,13 @@ const shippingCreateSchema = z.strictObject({
2667
2478
  shippingAddressId: z.string().nullable().optional().describe(ADDRESS_ID_DESCRIPTIONS.shipping),
2668
2479
  returnAddressId: z.string().nullable().optional().describe(ADDRESS_ID_DESCRIPTIONS.return)
2669
2480
  }).superRefine(refineShippingConsistency);
2670
- const downloadCreateSchema = z.strictObject({
2671
- limit: z.number().int().min(1).max(100).default(DOWNLOAD_DEFAULTS.limit).describe("다운로드 횟수 한도(수량 1개당). 생략하면 5회"),
2672
- days: z.number().int().min(1).max(3650).default(DOWNLOAD_DEFAULTS.days).describe("다운로드 기간(제공일부터 일수). 생략하면 30일"),
2673
- stockTracking: z.boolean().default(false).describe("재고를 세는가. 생략하면 false(재고와 무관하게 판다)"),
2674
- withdrawalRestricted: z.boolean().default(false).describe("청약철회 제한 대상인가. true면 주문서가 제한 고지 동의를 받고, 첫 다운로드 뒤 구매자 단순 변심 취소·반품을 받지 않는다")
2675
- });
2676
- const codeCreateSchema = z.strictObject({
2677
- mode: fulfillmentModeSchema.default("AUTO").describe("제공 방식. `AUTO`는 코드 풀에서 결제 확정과 함께 배정하고, `MANUAL`은 셀러가 제공 처리 때 코드를 넘긴다"),
2678
- withdrawalRestricted: z.boolean().default(false).describe("청약철회 제한 대상인가. true면 첫 열람 뒤 구매자 단순 변심 취소·반품을 받지 않는다")
2679
- });
2680
- const serviceCreateSchema = z.strictObject({ mode: fulfillmentModeSchema.default("MANUAL").describe("제공 방식. `AUTO`는 결제 확정과 함께 제공 완료, `MANUAL`은 셀러가 제공 처리한다") });
2681
- const pickupCreateSchema = z.strictObject({ addressId: z.string().optional().describe(ADDRESS_ID_DESCRIPTIONS.pickup) });
2682
2481
  /**
2683
- * 상품 이행 — 등록(POST). 유형 키를 생략하면 그 유형의 기본값이다(`{ "type": "DOWNLOAD" }`만으로 등록할 수 있다).
2482
+ * 상품 이행 — 등록(POST). 배송 상품의 `shipping`을 생략하면 기본값(유료 배송·스토어 기본 배송비·기본 출고지·반품지)이다.
2684
2483
  */
2685
- const productFulfillmentCreateSchema = z.discriminatedUnion("type", [
2686
- z.strictObject({
2687
- type: z.literal("SHIPPING"),
2688
- shipping: shippingCreateSchema.optional()
2689
- }),
2690
- z.strictObject({
2691
- type: z.literal("DOWNLOAD"),
2692
- download: downloadCreateSchema.default({
2693
- limit: DOWNLOAD_DEFAULTS.limit,
2694
- days: DOWNLOAD_DEFAULTS.days,
2695
- stockTracking: false,
2696
- withdrawalRestricted: false
2697
- })
2698
- }),
2699
- z.strictObject({
2700
- type: z.literal("CODE"),
2701
- code: codeCreateSchema.default({
2702
- mode: "AUTO",
2703
- withdrawalRestricted: false
2704
- })
2705
- }),
2706
- z.strictObject({
2707
- type: z.literal("SERVICE"),
2708
- service: serviceCreateSchema.default({ mode: "MANUAL" })
2709
- }),
2710
- z.strictObject({
2711
- type: z.literal("PICKUP"),
2712
- pickup: pickupCreateSchema.default({})
2713
- })
2714
- ]);
2484
+ const productFulfillmentCreateSchema = z.discriminatedUnion("type", [z.strictObject({
2485
+ type: z.literal("SHIPPING"),
2486
+ shipping: shippingCreateSchema.optional()
2487
+ }), z.strictObject({ type: z.literal("MANUAL") })]);
2715
2488
  const shippingUpdateSchema = z.strictObject({
2716
2489
  deliveryType: deliveryTypeSchema.describe(deliveryTypeDescription),
2717
2490
  deliveryFee: z.number().int().min(0).describe("배송비. 원 단위. FREE면 0"),
@@ -2723,76 +2496,24 @@ const shippingUpdateSchema = z.strictObject({
2723
2496
  * 상품 이행 — 수정(PUT·PATCH). 통째 교체이고 유형 키와 그 안의 필드가 **모두 필수**다. 서버는 현재 값과 병합하지 않는다.
2724
2497
  * `fulfillment` 자체를 생략하면 이행을 바꾸지 않는다.
2725
2498
  */
2726
- const productFulfillmentUpdateSchema = z.discriminatedUnion("type", [
2727
- z.strictObject({
2728
- type: z.literal("SHIPPING"),
2729
- shipping: shippingUpdateSchema
2730
- }),
2731
- z.strictObject({
2732
- type: z.literal("DOWNLOAD"),
2733
- download: z.strictObject({
2734
- limit: z.number().int().min(1).max(100),
2735
- days: z.number().int().min(1).max(3650),
2736
- stockTracking: z.boolean(),
2737
- withdrawalRestricted: z.boolean()
2738
- })
2739
- }),
2740
- z.strictObject({
2741
- type: z.literal("CODE"),
2742
- code: z.strictObject({
2743
- mode: fulfillmentModeSchema,
2744
- withdrawalRestricted: z.boolean()
2745
- })
2746
- }),
2747
- z.strictObject({
2748
- type: z.literal("SERVICE"),
2749
- service: z.strictObject({ mode: fulfillmentModeSchema })
2750
- }),
2751
- z.strictObject({
2752
- type: z.literal("PICKUP"),
2753
- pickup: z.strictObject({ addressId: z.string().describe(ADDRESS_ID_DESCRIPTIONS.pickup) })
2754
- })
2755
- ]);
2499
+ const productFulfillmentUpdateSchema = z.discriminatedUnion("type", [z.strictObject({
2500
+ type: z.literal("SHIPPING"),
2501
+ shipping: shippingUpdateSchema
2502
+ }), z.strictObject({ type: z.literal("MANUAL") })]);
2756
2503
  /**
2757
2504
  * 저장된 상품 이행 — 알려진 유형의 닫힌 유니온. 서버의 상품 엔티티와 응답이 이 모양을 쓴다.
2758
2505
  * 응답 파싱은 `productFulfillmentResponseSchema`(열린 유니온)를 쓴다.
2759
2506
  */
2760
- const productFulfillmentSchema = z.discriminatedUnion("type", [
2761
- z.object({
2762
- type: z.literal("SHIPPING"),
2763
- shipping: z.object({
2764
- deliveryType: deliveryTypeSchema,
2765
- deliveryFee: z.number().int().describe("배송비. 원 단위. FREE면 0"),
2766
- conditionalFreeAmount: z.number().int().nullable().describe("조건부 무료 기준 묶음 소계. CONDITIONAL_FREE가 아니면 null"),
2767
- shippingAddressId: z.string().nullable().describe("출고지 주소 id. null이면 스토어 기본 출고지를 따른다"),
2768
- returnAddressId: z.string().nullable().describe("반품지 주소 id. null이면 스토어 기본 반품지를 따른다")
2769
- })
2770
- }),
2771
- z.object({
2772
- type: z.literal("DOWNLOAD"),
2773
- download: z.object({
2774
- limit: z.number().int().describe("다운로드 횟수 한도(수량 1개당)"),
2775
- days: z.number().int().describe("다운로드 기간(제공일부터 일수)"),
2776
- stockTracking: z.boolean().describe("재고를 세는가"),
2777
- withdrawalRestricted: z.boolean().describe("청약철회 제한 대상인가")
2778
- })
2779
- }),
2780
- z.object({
2781
- type: z.literal("CODE"),
2782
- code: z.object({
2783
- mode: fulfillmentModeSchema.describe("제공 방식"),
2784
- withdrawalRestricted: z.boolean().describe("청약철회 제한 대상인가")
2785
- })
2786
- }),
2787
- z.object({
2788
- type: z.literal("SERVICE"),
2789
- service: z.object({ mode: fulfillmentModeSchema.describe("제공 방식") })
2790
- }),
2791
- z.object({
2792
- type: z.literal("PICKUP"),
2793
- pickup: z.object({ addressId: z.string().nullable().describe("방문 수령지 주소 id. null은 수령지가 지워진 상품이다(수령지를 다시 지정할 때까지 팔 수 없다)") })
2507
+ const productFulfillmentSchema = z.discriminatedUnion("type", [z.object({
2508
+ type: z.literal("SHIPPING"),
2509
+ shipping: z.object({
2510
+ deliveryType: deliveryTypeSchema,
2511
+ deliveryFee: z.number().int().describe("배송비. 원 단위. FREE면 0"),
2512
+ conditionalFreeAmount: z.number().int().nullable().describe("조건부 무료 기준 묶음 소계. CONDITIONAL_FREE가 아니면 null"),
2513
+ shippingAddressId: z.string().nullable().describe("출고지 주소 id. null이면 스토어 기본 출고지를 따른다"),
2514
+ returnAddressId: z.string().nullable().describe("반품지 주소 id. null이면 스토어 기본 반품지를 따른다")
2794
2515
  })
2795
- ]);
2516
+ }), z.object({ type: z.literal("MANUAL") })]);
2796
2517
  /**
2797
2518
  * 열린 응답 유니온 — 알려진 유형은 판별 유니온으로 검사하고, **모르는 유형만** `{ type: "OTHER", rawType }`로 읽는다.
2798
2519
  *
@@ -2806,72 +2527,18 @@ function openFulfillmentUnion(known) {
2806
2527
  }))]);
2807
2528
  }
2808
2529
  const productFulfillmentResponseSchema = openFulfillmentUnion(productFulfillmentSchema).describe("상품 이행. `type`이 유형이고 같은 이름의 키가 유형별 속성이다. 값이 추가될 수 있어 SDK는 모르는 유형을 `{ type: \"OTHER\", rawType }`로 읽는다");
2809
- /** 제공 방식 — CODE·SERVICE는 저장된 값, DOWNLOAD는 AUTO, PICKUP은 MANUAL, 배송 상품은 null */
2810
- function fulfillmentModeOf(fulfillment) {
2811
- switch (fulfillment.type) {
2812
- case "CODE": return fulfillment.code.mode;
2813
- case "SERVICE": return fulfillment.service.mode;
2814
- default: return FULFILLMENT_RULES[fulfillment.type].defaultMode;
2815
- }
2816
- }
2817
- /** 재고를 세는가 — 파일 다운로드만 끌 수 있다 */
2818
- function stockTrackingOf(fulfillment) {
2819
- return fulfillment.type === "DOWNLOAD" ? fulfillment.download.stockTracking : true;
2820
- }
2821
- /** 청약철회 제한 대상인가 — 파일 다운로드·코드 발급만 켤 수 있다(전자상거래법 제17조 제2항 제5호, 디지털 콘텐츠) */
2822
- function withdrawalRestrictedOf(fulfillment) {
2823
- if (fulfillment.type === "DOWNLOAD") return fulfillment.download.withdrawalRestricted;
2824
- if (fulfillment.type === "CODE") return fulfillment.code.withdrawalRestricted;
2825
- return false;
2826
- }
2827
- /** 코드 풀을 쓰는가 — 코드 발급 + 자동 제공. 재고가 판매 가능 코드 수다 */
2828
- function usesCodePool(fulfillment) {
2829
- return fulfillment.type === "CODE" && fulfillment.code.mode === "AUTO";
2830
- }
2831
- /** 결제 확정과 함께 제공까지 끝내는가 — 비실물 + 자동 제공 */
2832
- function autoFulfillableOf(fulfillment) {
2833
- return !FULFILLMENT_RULES[fulfillment.type].requiresShipping && fulfillmentModeOf(fulfillment) === "AUTO";
2834
- }
2835
2530
  /**
2836
2531
  * 주문상품의 주문 시점 이행 — 결제 확정이 주문서 줄에서 굳힌다. 상품의 현재 값이 아니다. 서버는 주문상품 컬럼
2837
- * (`fulfillment_type`·`delivery_bundle_key`·`stock_tracked`·`withdrawal_restricted`)과 jsonb(`fulfillment_snapshot`)를 합쳐 이 모양을 만든다.
2838
- *
2839
- * `mode`는 **주문 시점 상품의 방식**이다. 실제 제공 방식은 제공 기록(`fulfillment.mode`)에 있고 다를 수 있다(자동 제공 상품의 수동 복구).
2532
+ * (`fulfillment_type`·`delivery_bundle_key`)과 jsonb(`fulfillment_snapshot`)를 합쳐 이 모양을 만든다.
2840
2533
  */
2841
- const orderItemFulfillmentSnapshotSchema = z.discriminatedUnion("type", [
2842
- z.object({
2843
- type: z.literal("SHIPPING"),
2844
- shipping: z.object({
2845
- bundleKey: z.string().nullable().describe("배송 묶음 키. 같은 키의 주문상품이 배송비를 한 번 낸 한 묶음이다. #29 이전 주문은 null"),
2846
- returnAddressId: z.string().nullable().describe("주문 시점 반품지 주소 id. null이거나 지워진 주소면 스토어 기본 반품지로 안내한다")
2847
- })
2848
- }),
2849
- z.object({
2850
- type: z.literal("DOWNLOAD"),
2851
- download: z.object({
2852
- limit: z.number().int().describe("주문 시점 다운로드 횟수 한도(수량 1개당)"),
2853
- days: z.number().int().describe("주문 시점 다운로드 기간(제공일부터 일수)"),
2854
- stockTracked: z.boolean().describe("주문 시점에 재고를 뺐는가"),
2855
- withdrawalRestricted: z.boolean().describe("구매자가 제한 고지에 동의한 청약철회 제한 주문상품인가")
2856
- })
2857
- }),
2858
- z.object({
2859
- type: z.literal("CODE"),
2860
- code: z.object({
2861
- mode: z.string().describe("주문 시점 상품의 제공 방식. 현재 값은 `AUTO`·`MANUAL`"),
2862
- withdrawalRestricted: z.boolean().describe("구매자가 제한 고지에 동의한 청약철회 제한 주문상품인가")
2863
- })
2864
- }),
2865
- z.object({
2866
- type: z.literal("SERVICE"),
2867
- service: z.object({ mode: z.string().describe("주문 시점 상품의 제공 방식. 현재 값은 `AUTO`·`MANUAL`") })
2868
- }),
2869
- z.object({
2870
- type: z.literal("PICKUP"),
2871
- pickup: z.object({ addressId: z.string().nullable().describe("주문 시점 방문 수령지 주소 id") })
2534
+ const orderItemFulfillmentSnapshotSchema = z.discriminatedUnion("type", [z.object({
2535
+ type: z.literal("SHIPPING"),
2536
+ shipping: z.object({
2537
+ bundleKey: z.string().nullable().describe("배송 묶음 키. 같은 키의 주문상품이 배송비를 한 번 낸 한 묶음이다. #29 이전 주문은 null"),
2538
+ returnAddressId: z.string().nullable().describe("주문 시점 반품지 주소 id. null이거나 지워진 주소면 스토어 기본 반품지로 안내한다")
2872
2539
  })
2873
- ]);
2874
- const orderItemFulfillmentSnapshotResponseSchema = openFulfillmentUnion(orderItemFulfillmentSnapshotSchema).describe("주문 시점 이행 스냅샷. 상품을 나중에 고쳐도 바뀌지 않는다. 실제 제공 기록은 `fulfillment`에 있다. 값이 추가될 수 있어 SDK는 모르는 유형을 `OTHER`로 읽는다");
2540
+ }), z.object({ type: z.literal("MANUAL") })]);
2541
+ const orderItemFulfillmentSnapshotResponseSchema = openFulfillmentUnion(orderItemFulfillmentSnapshotSchema).describe("주문 시점 이행 스냅샷. 상품을 나중에 고쳐도 바뀌지 않는다. 제공 기록은 `fulfillment`에 있다. 값이 추가될 수 있어 SDK는 모르는 유형을 `OTHER`로 읽는다");
2875
2542
  //#endregion
2876
2543
  //#region src/schemas/payment.ts
2877
2544
  /**
@@ -3221,507 +2888,327 @@ const paymentListQuerySchema = z.object({
3221
2888
  size: z.string().optional().describe("페이지 크기 (기본 20, 최대 100)")
3222
2889
  });
3223
2890
  //#endregion
3224
- //#region src/schemas/order.ts
3225
- const orderSummarySchema = z.object({
3226
- orderId: z.string(),
3227
- ordererName: z.string(),
3228
- itemCount: z.number().int(),
3229
- totalAmount: z.number().int(),
3230
- representativeStatus: orderItemStatusSchema,
3231
- orderedAt: z.string(),
3232
- testPayment: z.boolean().describe("테스트 결제(샌드박스 결제) 주문인가. true면 실제로 돈이 오가지 않았습니다. 상품을 발송하지 않도록 주의하십시오")
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("파일 바이트 크기. 주면 용도별 상한을 발급 전에 검사한다")
3233
2969
  });
3234
- const shippingAddressSchema = z.object({
3235
- receiverName: z.string(),
3236
- phone: z.string(),
3237
- zipCode: z.string(),
3238
- address1: z.string(),
3239
- address2: z.string().nullish(),
3240
- deliveryMemo: z.string().nullish(),
3241
- entranceCode: z.string().nullish()
2970
+ const uploadTicketSchema = z.object({
2971
+ /** 업로드 완료 후 공개될 주소 */
2972
+ url: z.url(),
2973
+ /** 파일 바이트를 PUT할 presigned URL. 1시간 유효 */
2974
+ uploadUrl: z.url()
3242
2975
  });
3243
- /** 비실물 주문상품의 제공 정보 (이슈 #48) */
3244
- const orderItemFulfillmentSchema = z.object({
3245
- status: z.string().describe("제공 상태. `FULFILLED`(제공함)·`REVOKED`(환불 등으로 회수함). 값이 추가될 수 있어 문자열로 받는다"),
3246
- mode: z.string().nullable().describe("제공 방식. `AUTO`·`MANUAL`"),
3247
- fulfilledAt: z.string().describe("제공 시각 (ISO 8601)"),
3248
- note: z.string().nullable().describe("셀러가 남긴 이용 안내. 없으면 null"),
3249
- codes: z.array(orderItemCodeSchema).optional().default([]).describe("코드 발급 상품에 배정한 코드의 가린 값. 평문은 싣지 않는다. 코드 상품이 아니면 빈 배열. 서버는 항상 싣는다"),
3250
- download: orderItemDownloadSchema.nullable().optional().default(null).describe("파일 다운로드 상품의 횟수·기한·파일 목록. 파일 주소는 싣지 않는다. 다른 유형은 null. 서버는 항상 싣는다"),
3251
- mail: z.object({
3252
- status: z.string().describe("제공 안내 메일 상태. 현재 값은 `PENDING`(보낼 예정)·`SENDING`·`SENT`·`FAILED`·`SKIPPED`(보내지 않음: 스토어 설정 꺼짐·받을 주소 없음)이고 값이 추가될 수 있다"),
3253
- sentAt: z.string().nullable(),
3254
- errorCode: z.string().nullable().describe("실패·건너뜀 사유 코드")
3255
- }).nullable().optional().default(null).describe("구매자에게 보낸 제공 안내 메일(이슈 #48). 기록이 없으면 null. 서버는 항상 싣는다")
3256
- });
3257
- /** 제공 안내 메일 재발송 결과 */
3258
- const fulfillmentMailResendSchema = z.object({ mailStatus: z.string().describe("재발송 뒤 메일 상태 — `SENT`·`PENDING`(곧 다시 보낸다)·`FAILED`·`SKIPPED`") });
3259
- const orderItemSchema = z.object({
3260
- orderItemId: z.string(),
3261
- orderId: z.string(),
3262
- status: orderItemStatusSchema,
3263
- claimStatus: z.enum([
3264
- "CANCEL_REQUESTED",
3265
- "RETURN_REQUESTED",
3266
- "EXCHANGE_REQUESTED"
3267
- ]).nullable(),
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({
3268
2985
  productId: z.string(),
3269
- productName: z.string(),
3270
- optionId: z.string().nullable(),
3271
- optionName: z.string().nullable(),
3272
- sellerCode: z.string().nullable(),
3273
- fulfillmentSnapshot: orderItemFulfillmentSnapshotResponseSchema.describe("주문 시점 이행 스냅샷(이슈 #57). `type`이 주문 시점 상품 유형이다. 배송 상품이 아니면 발송·배송완료 대신 제공 처리를 하고, `DELIVERED`는 제공 완료(방문 수령은 수령 완료)다. 상품을 나중에 고쳐도 바뀌지 않는다. 실제 제공 기록은 `fulfillment`에 있다"),
3274
- quantity: z.number().int().describe("주문 수량"),
3275
- canceledQuantity: z.number().int().describe("취소·반품으로 환불된 누적 수량. 수량 단위 부분 취소가 없었으면 0이다"),
3276
- activeQuantity: z.number().int().describe("살아 있는 수량 (`quantity` − `canceledQuantity`). 0이면 주문상품이 전량 취소됐다"),
3277
- unitPrice: z.number().int(),
3278
- discountAmount: z.number().int(),
3279
- totalPrice: z.number().int().describe("주문상품 금액 (즉시할인 반영 단가 × 주문 수량). 쿠폰 할인 전 금액이다"),
3280
- couponDiscountAmount: z.number().int().optional().default(0).describe("이 주문상품에 배분된 쿠폰 할인 — 상품 쿠폰 + 주문 쿠폰 배분. 실결제 금액은 `totalPrice − couponDiscountAmount`이고 취소·반품 환불은 실결제 금액을 수량 비율로 나눈다. 쿠폰이 없으면 0. 서버는 항상 싣는다"),
3281
- hold: z.object({
3282
- reason: z.string(),
3283
- expectedDispatchDate: z.string()
3284
- }).nullable(),
3285
- fulfillment: orderItemFulfillmentSchema.nullable().optional().default(null).describe("비실물 상품의 제공 정보(이슈 #48). 제공 전이거나 배송 상품이면 null이다. 코드 평문·파일 주소는 싣지 않는다. 서버는 항상 싣는다"),
3286
- autoDecisionDate: z.string().nullable(),
3287
- sellerMemo: z.string().nullable(),
3288
- orderedAt: z.string(),
3289
- lastChangedAt: z.string(),
3290
- lastChangedType: changeTypeSchema,
3291
- testPayment: z.boolean().describe("소속 주문이 테스트 결제(샌드박스 결제) 주문인가. true면 실제로 돈이 오가지 않았습니다")
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()
3292
2998
  });
3293
- /** 주문이 받은 쿠폰 한 장 (이슈 #47) */
3294
- const orderDiscountSchema = z.object({
3295
- couponId: z.string().describe("쿠폰 id"),
3296
- name: z.string().describe("주문 시점 쿠폰 이름"),
3297
- kind: z.string().describe("쿠폰 종류. 현재 값은 `ORDER`(주문 쿠폰)·`PRODUCT`(상품 쿠폰)·`DELIVERY`(배송비 쿠폰)이고 값이 추가될 수 있어 문자열로 받는다"),
3298
- discountAmount: z.number().int().describe("이 쿠폰의 할인액 (원)"),
3299
- issueId: z.string().nullable().describe("발급 쿠폰 id. 코드 입력형은 null")
2999
+ /** 옵션 축(그룹)이 가지는 값 하나 — 예: 축 "색상"의 값 "블랙" */
3000
+ const optionValueSchema = z.object({
3001
+ valueId: z.string(),
3002
+ name: z.string(),
3003
+ sortOrder: z.number().int()
3300
3004
  });
3301
- const orderSchema = z.object({
3302
- orderId: z.string(),
3303
- orderedAt: z.string(),
3304
- orderer: z.object({
3305
- name: z.string(),
3306
- phone: z.string(),
3307
- email: z.email().optional()
3308
- }),
3309
- requiresShipping: z.boolean().optional().default(true).describe("택배 배송이 필요한 주문상품이 있는가. false면 `shippingAddress`는 주문자 이름·연락처만 있고 주소는 빈 문자열이다. 서버는 항상 싣는다"),
3310
- shippingAddress: shippingAddressSchema,
3311
- payment: z.object({
3312
- method: z.enum([
3313
- "CARD",
3314
- "BANK_TRANSFER",
3315
- "VIRTUAL_ACCOUNT",
3316
- "MOBILE",
3317
- "POINT",
3318
- "MIXED"
3319
- ]).describe("method는 호환용이며 간편결제를 CARD로 표시한다. 실제 수단은 approvedMethod를 본다"),
3320
- approvedMethod: z.string().nullable().optional().default(null).describe("실제 결제수단. method는 호환용이며 간편결제를 CARD로 표시한다. 실제 수단은 approvedMethod를 본다. 현재 값은 `pgPaymentMethodSchema`(`PgPaymentMethod`: CARD·BANK_TRANSFER·VIRTUAL_ACCOUNT·MOBILE·EASY_PAY)이고 매출 리포트·스토어프론트·환불 원장과 같은 값이다. 값이 추가될 수 있어 문자열로 받는다. PG가 알려 주지 않았으면 요청 수단, 시도 기록이 없는 오래된 주문은 null. 서버는 항상 싣는다"),
3321
- easyPayProvider: easyPayProviderResponseSchema.nullable().optional().default(null).describe("승인된 간편결제사. `approvedMethod`가 `EASY_PAY`일 때만 값이 있고, PG가 알려 주지 않았으면 요청한 간편결제사다. 그 밖의 결제수단과 간편결제 이전 주문은 null. 서버는 항상 싣는다. 현재 값은 `NAVERPAY`·`KAKAOPAY`·`TOSSPAY`·`PAYCO`이고 값이 추가될 수 있어 문자열로 받는다"),
3322
- productAmount: z.number().int().describe("상품 금액 합 (즉시할인 반영, 쿠폰 전)"),
3323
- deliveryFee: z.number().int().describe("배송비 (쿠폰 전)"),
3324
- discountAmount: z.number().int().describe("즉시할인 합 (정가 − 할인 단가) × 수량. 쿠폰 할인은 `couponDiscountAmount`로 따로 온다"),
3325
- couponDiscountAmount: z.number().int().optional().default(0).describe("상품·주문 쿠폰 할인 합계. 쿠폰이 없으면 0. 서버는 항상 싣는다"),
3326
- deliveryDiscountAmount: z.number().int().optional().default(0).describe("배송비 쿠폰 할인. 쿠폰이 없으면 0. 서버는 항상 싣는다"),
3327
- discounts: z.array(orderDiscountSchema).optional().default([]).describe("주문이 받은 쿠폰. 쿠폰이 없으면 빈 배열. 서버는 항상 싣는다"),
3328
- totalAmount: z.number().int().describe("결제 금액 = productAmount + deliveryFee − couponDiscountAmount − deliveryDiscountAmount. 쿠폰이 없는 주문은 productAmount + deliveryFee다"),
3329
- paidAt: z.string().nullable(),
3330
- cashReceipt: cashReceiptSchema.nullable().optional().default(null).describe("현금영수증. 구매자가 계좌이체·가상계좌 결제에서 신청했을 때만 있고 그 밖에는 null. 서버는 항상 싣는다")
3331
- }),
3332
- withdrawalAgreedAt: z.string().nullable().optional().default(null).describe("청약철회 제한 안내에 동의한 시각(이슈 #48). 제한 상품이 없던 주문은 null. 서버는 항상 싣는다"),
3333
- testPayment: z.boolean().describe("테스트 결제(샌드박스 결제) 주문인가. true면 실제로 돈이 오가지 않았습니다. 상품을 발송하지 않도록 주의하십시오"),
3334
- items: z.array(orderItemSchema)
3005
+ /** 옵션 축(그룹) — 예: "색상", "사이즈" */
3006
+ const optionGroupSchema = z.object({
3007
+ groupId: z.string(),
3008
+ name: z.string(),
3009
+ sortOrder: z.number().int(),
3010
+ values: z.array(optionValueSchema)
3335
3011
  });
3336
- /** 주문 목록 응답 — `GET /v1/orders` */
3337
- const orderSummaryPageSchema = pageSchema(orderSummarySchema);
3338
- /** 주문상품 변경 목록 응답 — `GET /v1/order-items` */
3339
- const orderItemPageSchema = pageSchema(orderItemSchema);
3340
- const testPaymentQuery = z.enum(["true", "false"]).optional().describe("테스트 결제(샌드박스 결제) 필터. `true`면 테스트 결제만, `false`면 실결제만, 생략하면 전부입니다");
3341
- const pageQuery = { page: z.string().optional().describe("페이지 번호 (1부터, 기본 1)") };
3342
3012
  /**
3343
- * `GET /v1/orders` 쿼리 — OpenAPI 문서용 형태(값은 쿼리 문자열). 검증 규칙은 서버가 적용한다:
3344
- * `orderedFrom` 누락 400 `MISSING_PARAMETER`, 날짜 형식 오류 400 `INVALID_PARAMETER`, 31일 초과 400 `RANGE_TOO_WIDE`,
3345
- * `testPayment`가 `true`·`false`가 아니면 400 `INVALID_PARAMETER`
3013
+ * 판매 단위(SKU). variantId는 기존 optionId 값 공간을 승계하므로
3014
+ * 카트·주문·리뷰·클레임의 optionId가 곧 variantId다.
3346
3015
  */
3347
- const orderListQuerySchema = z.object({
3348
- orderedFrom: z.string().describe("조회 시작 시각 (ISO 8601 날짜 또는 일시, 포함). 필수"),
3349
- orderedTo: z.string().optional().describe("조회 끝 시각 (ISO 8601, 포함). 생략하면 orderedFrom부터 31일. 기간은 최대 31일"),
3350
- status: orderItemStatusSchema.optional().describe("주문상품 상태 필터 — 이 상태의 주문상품이 하나라도 있는 주문"),
3351
- keyword: z.string().optional().describe("주문번호(일치)·주문자명·수령인명(포함) 검색"),
3352
- fulfillmentType: z.string().optional().describe("상품 유형 필터(이슈 #48) — 이 유형(`SHIPPING`·`DOWNLOAD`·`CODE`·`SERVICE`·`PICKUP`)의 주문상품이 하나라도 있는 주문"),
3353
- testPayment: testPaymentQuery,
3354
- ...pageQuery,
3355
- size: z.string().optional().describe("페이지 크기 (기본 20, 최대 100)")
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`로 판매를 끈다. 서버는 항상 싣는다. 요청에는 쓰지 않는다")
3356
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;
3357
3046
  /**
3358
- * `GET /v1/order-items` 쿼리 — OpenAPI 문서용 형태. `lastChangedFrom` 누락 400 `MISSING_PARAMETER`, 형식 오류 400
3359
- * `INVALID_PARAMETER`, 24시간 초과 400 `RANGE_TOO_WIDE`, `testPayment` 값 오류 400 `INVALID_PARAMETER`
3047
+ * 옵션 값 정렬 순서 — 등록순·가나다순·낮은 추가금순·높은 추가금순. 서버가 저장할 때 이 순서로 값의 `sortOrder`를 매긴다.
3048
+ * 추가금순은 추가 선택 값이면 값의 추가금, 조합 값이면 그 값이 든 조합 추가금의 최솟값으로 비교하고 같으면 등록순이다.
3360
3049
  */
3361
- const orderItemListQuerySchema = z.object({
3362
- lastChangedFrom: z.string().describe("마지막 변경 시각 조회 시작 (ISO 8601, 포함). 필수"),
3363
- lastChangedTo: z.string().optional().describe("마지막 변경 시각 조회 끝 (ISO 8601, 포함). 생략하면 시작부터 24시간. 기간은 최대 24시간"),
3364
- lastChangedType: changeTypeSchema.optional().describe("마지막 변경 유형 필터"),
3365
- testPayment: testPaymentQuery,
3366
- ...pageQuery,
3367
- size: z.string().optional().describe("페이지 크기 (기본 100, 최대 300)")
3368
- });
3369
- const deliveryMethodSchema = z.enum([
3370
- "COURIER",
3371
- "DIRECT",
3372
- "QUICK",
3373
- "NOTHING"
3050
+ const optionSortSchema = z.enum([
3051
+ "REGISTERED",
3052
+ "NAME",
3053
+ "PRICE_ASC",
3054
+ "PRICE_DESC"
3374
3055
  ]);
3375
- const dispatchRequestSchema = z.object({
3376
- deliveryMethod: deliveryMethodSchema,
3377
- carrierCode: z.string().optional(),
3378
- trackingNumber: z.string().optional(),
3379
- /**
3380
- * 발송 시각. ISO 8601이고 시간대(`Z` 또는 `+09:00`)를 포함해야 한다. 생략하면 현재 시각이다.
3381
- *
3382
- * 전에는 `z.string()`이라 아무 문자열이나 그대로 저장됐다. 이 값은 배송 정보 jsonb에 들어가
3383
- * 자동 배송완료·추적 동기화 잡의 대상 선별 조건이 된다 — 잘못된 한 행이 스토어 구분 없이 도는
3384
- * 잡 전체를 멈출 수 있어 입력에서 좁혔다. 검증을 통과한 값도 저장 전에 UTC ISO로 정규화한다.
3385
- */
3386
- dispatchedAt: z.iso.datetime({ offset: true }).optional()
3387
- }).refine((value) => value.deliveryMethod === "DIRECT" || value.deliveryMethod === "NOTHING" ? true : !!value.carrierCode && !!value.trackingNumber, { message: "COURIER/QUICK 발송은 carrierCode와 trackingNumber가 필수입니다" });
3388
- const deliveryInfoSchema = z.object({
3389
- orderItemId: z.string(),
3390
- deliveryMethod: deliveryMethodSchema,
3391
- carrierCode: z.string().nullable(),
3392
- carrierName: z.string().nullable(),
3393
- trackingNumber: z.string().nullable(),
3394
- trackingUrl: z.url().nullable(),
3395
- status: z.enum([
3396
- "DISPATCHED",
3397
- "DELIVERING",
3398
- "DELIVERED"
3399
- ]),
3400
- dispatchedAt: z.string(),
3401
- deliveredAt: z.string().nullable()
3402
- });
3403
- /**
3404
- * 택배 추적 단계. 응답에서는 문자열로 받는다(값이 추가될 수 있다) — 현재 값은 `ACCEPTED`(접수)·`IN_TRANSIT`(이동 중)·
3405
- * `OUT_FOR_DELIVERY`(배송 출발)·`DELIVERED`(배송완료)다.
3406
- */
3407
- const trackingStageResponseSchema = z.string();
3408
- /** `GET /v1/order-items/{orderItemId}/delivery` 응답 — 배송 정보 + 배송 추적 이벤트 */
3409
- const deliveryStatusSchema = deliveryInfoSchema.extend({
3410
- trackingEvents: z.array(z.object({
3411
- status: z.string().describe("택배사가 알려 준 처리 내용(예: 집화처리, 배송출발)"),
3412
- location: z.string().describe("처리 장소. 모르면 빈 문자열"),
3413
- occurredAt: z.string(),
3414
- stage: trackingStageResponseSchema.nullable().optional().default(null).describe("이 이벤트의 추적 단계. 배달 실패처럼 단계로 나눌 수 없는 이벤트는 null. 서버는 항상 싣는다. 현재 값은 `ACCEPTED`·`IN_TRANSIT`·`OUT_FOR_DELIVERY`·`DELIVERED`이고 값이 추가될 수 있어 문자열로 받는다")
3415
- })).optional().describe("배송 추적 이력. 시각 오름차순(오래된 것부터)"),
3416
- tracking: z.object({
3417
- result: z.string().nullable().describe("마지막 추적 조회 결과. 조회에 성공한 적이 없으면 null. 현재 값은 `FOUND`(조회됨)·`NOT_FOUND`(택배사 전산에 송장 없음)·`UNSUPPORTED_CARRIER`(추적을 지원하지 않는 택배사)이고 값이 추가될 수 있어 문자열로 받는다"),
3418
- stage: trackingStageResponseSchema.nullable().describe("현재 추적 단계. 조회되지 않았으면 null. 현재 값은 `ACCEPTED`·`IN_TRANSIT`·`OUT_FOR_DELIVERY`·`DELIVERED`이고 값이 추가될 수 있어 문자열로 받는다"),
3419
- checkedAt: z.string().nullable().describe("택배사에서 추적 정보를 마지막으로 받아 온 시각(ISO 8601)"),
3420
- carrierDeliveredAt: z.string().nullable().describe("택배사가 알려 준 배송완료 시각(ISO 8601). 추적으로 배송완료가 반영되면 주문상품의 `deliveredAt`은 반영 시각이라 이 값과 다를 수 있다")
3421
- }).nullable().optional().default(null).describe("택배 추적 요약. 택배 발송이 아니거나 추적을 쓰지 않으면 null. 추적이 배송완료면 주문상품이 자동으로 배송완료된다. 서버는 항상 싣는다")
3422
- });
3423
- /** `PUT /v1/order-items/{orderItemId}/dispatch/tracking` 응답 — 바뀐 송장 */
3424
- const trackingUpdateResultSchema = z.object({
3425
- orderItemId: z.string(),
3426
- carrierCode: z.string(),
3427
- trackingNumber: z.string()
3428
- });
3429
- const carrierSchema = z.object({
3430
- carrierCode: z.string(),
3056
+ /** 추가 선택 값 — 예: 옵션명 "선물 포장"의 값 "포장함"(+2,000원) */
3057
+ const addonValueSchema = z.object({
3058
+ valueId: z.string(),
3431
3059
  name: z.string(),
3432
- trackingUrlTemplate: z.string()
3433
- });
3434
- const holdOrderItemRequestSchema = z.object({
3435
- reason: z.enum([
3436
- "OUT_OF_STOCK",
3437
- "CUSTOMER_REQUEST",
3438
- "CUSTOMS",
3439
- "ETC"
3440
- ]),
3441
- detail: z.string().max(500).optional(),
3442
- expectedDispatchDate: z.string()
3060
+ sortOrder: z.number().int(),
3061
+ additionalPrice: z.number().int().describe("이 값을 고르면 단가에 더하는 금액(원, 0 이상)"),
3062
+ usable: z.boolean().describe("사용 여부. false면 구매자에게 보이지 않고 고를 수 없다")
3443
3063
  });
3444
- const cancelBySellerRequestSchema = z.object({
3445
- reason: z.enum([
3446
- "OUT_OF_STOCK",
3447
- "PRICE_ERROR",
3448
- "DELIVERY_IMPOSSIBLE",
3449
- "ETC"
3450
- ]),
3451
- detail: z.string().max(500).optional(),
3452
- quantity: z.number().int().min(1).optional().describe("취소할 수량. 생략하면 남은 수량 전체다. 남은 수량보다 크면 400 `INVALID_QUANTITY`. 일부만 취소하면 주문상품은 상태를 유지한 채 `canceledQuantity`가 늘고, 전 수량이 소진되면 `CANCELED`가 된다")
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()
3453
3081
  });
3454
3082
  /**
3455
- * 제공 처리 요청 (이슈 #48) — `POST /v1/order-items/{orderItemId}/fulfill`. 수동 제공 비실물 주문상품(이용권·방문 수령 등)을
3456
- * 제공 완료(`DELIVERED`)로 바꾼다.
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
+ * 서버는 항목명과 입력 안내 둘 다 이 함수로 검사한다(안내에 「연락처를 적어 주십시오」라고 쓰는 우회를 막는다).
3457
3115
  */
3458
- const fulfillRequestSchema = z.object({
3459
- note: z.string().trim().max(1e3).optional().describe("구매자에게 보일 이용 안내(예: 예약 방법, 수령 장소). 주문 상세에 실린다"),
3460
- codes: z.array(z.string().trim().min(1).max(200)).min(1).max(100).optional().describe("수동 제공 코드 발급 상품의 코드 — 셀러 시스템이 발급한 코드를 넘긴다. 남은 수량만큼 보낸다(`CODE_COUNT_MISMATCH`). 암호화해 저장하고 구매자가 열람할 때만 평문으로 나간다. 다른 유형은 보내지 않는다")
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)
3461
3128
  });
3462
- /** 제공 처리 일괄 요청 — `POST /v1/order-items/fulfill` */
3463
- const fulfillBulkRequestSchema = z.object({
3464
- orderItemIds: z.array(z.string()).min(1).max(100),
3465
- note: fulfillRequestSchema.shape.note
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)
3466
3144
  });
3467
- /** `POST /v1/order-items/{orderItemId}/cancel` 응답 — 셀러 직권취소 환불 결과 */
3468
- const cancelBySellerResultSchema = z.object({
3469
- orderItemId: z.string(),
3470
- refundAmount: z.number().int().describe("실제 환불한 금액 — 수량 비율 상품 실결제 금액(쿠폰 배분을 뺀 금액) + 배송비 환불 − 조건부 무료배송 미달 차감 + 되돌린 쿠폰 차감(`couponChargeRefundAmount`)"),
3471
- quantity: z.number().int().describe("취소한 수량"),
3472
- canceledQuantity: z.number().int().describe("취소 후 누적 취소 수량"),
3473
- deliveryFeeRefundAmount: z.number().int().describe("환불에 포함된 배송비. 배송 묶음(같은 상품)이 모두 취소됐고 환불 정책이 켜져 있을 때만 0보다 크다"),
3474
- deliveryFeeChargeAmount: z.number().int().describe("조건부 무료배송 기준 미달로 환불액에서 뺀 배송비. 정책이 꺼져 있으면 항상 0이다"),
3475
- couponChargeAmount: z.number().int().optional().default(0).describe("쿠폰 조건 미달 차감. 판매자 취소는 차감하지 않으므로 늘 0이다. 서버는 항상 싣는다"),
3476
- couponChargeRefundAmount: z.number().int().optional().default(0).describe("환불액에 더한, 앞선 환불에서 뺀 쿠폰 차감을 되돌린 몫. 서버는 항상 싣는다"),
3477
- refundedAt: z.string()
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()
3478
3154
  });
3479
- /** `PUT /v1/order-items/{orderItemId}/memo` 응답 — 빈 문자열로 저장하면 메모가 지워져 `null`이다 */
3480
- const orderItemMemoResultSchema = z.object({
3481
- orderItemId: z.string(),
3482
- sellerMemo: z.string().nullable()
3155
+ /** 구매자가 적은 직접 입력값 — 주문 시점 항목명과 값 */
3156
+ const customInputValueSchema = z.object({
3157
+ label: z.string(),
3158
+ value: z.string()
3483
3159
  });
3484
- /** `POST /v1/order-items/{orderItemId}/purchase-decision/request` 응답 */
3485
- const purchaseDecisionRequestResultSchema = z.object({
3486
- orderItemId: z.string(),
3487
- requestedAt: z.string()
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()
3488
3167
  });
3489
- /** `POST /v1/order-items/{orderItemId}/purchase-decision/extend` 응답 — 연장은 최대 2회 */
3490
- const purchaseDecisionExtendResultSchema = z.object({
3491
- orderItemId: z.string(),
3492
- autoDecisionDate: z.string(),
3493
- extendCount: z.number().int().max(2)
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()
3494
3196
  });
3197
+ /** 상품 하나에 등록할 수 있는 이미지 최대 장수 — 첫 장이 대표(썸네일)다 */
3198
+ const MAX_PRODUCT_IMAGES = 10;
3495
3199
  /**
3496
- * `GET /v1/orders/{orderId}/address-change-requests` 응답 — 배송지 변경 요청 플로우는 아직 제공하지 않아
3497
- * 현재는 항상 빈 배열이다
3200
+ * 상품 이미지 주소의 와이어 형식 — URL이고 2048자 이하다.
3201
+ *
3202
+ * https·확장자 규칙은 여기서 강제하지 않고 `productImageRejection`이 판정한다. 서버가
3203
+ * **새로 추가된 주소에만** 그 규칙을 적용해야 하기 때문이다(규칙이 생기기 전에 저장된
3204
+ * 주소 하나로 상품 수정 전체가 막히면 셀러가 빠져나갈 길이 없다).
3205
+ *
3206
+ * 응답 스키마(`productSchema.images`)도 넓게 둔다. 과거에 저장된 주소를 파싱 오류로
3207
+ * 만들면 상품 조회 자체가 막힌다.
3498
3208
  */
3499
- const addressChangeRequestListSchema = z.array(z.record(z.string(), z.unknown())).describe("배송지 변경 요청 목록 (현재 항상 빈 배열)");
3500
- //#endregion
3501
- //#region src/schemas/upload.ts
3209
+ const productImageUrlSchema = z.url().max(2048).describe("상품 이미지 주소. https만 허용하고, 확장자가 있으면 이미지 형식이어야 한다");
3502
3210
  /**
3503
- * 업로드 용도. 허용 확장자와 크기 상한이 용도별로 다르다.
3504
- *
3505
- * - `product-image`: 상품 목록·상세의 상품 이미지. 목록 썸네일로 쓰이므로 정지 이미지만 받는다.
3506
- * - `description`: 상세설명 본문에 삽입하는 미디어. 동영상과 svg도 받는다.
3507
- * - `digital-asset`: 파일 다운로드 상품의 판매 파일(이슈 #48). **비공개 객체**로 올라가고 공개 주소가 없다 — 구매자는 제공된
3508
- * 주문상품에서 수명 300초의 서명 주소로만 받는다.
3509
- */
3510
- const uploadPurposeSchema = z.enum([
3511
- "product-image",
3512
- "description",
3513
- "digital-asset"
3514
- ]);
3515
- const MB = 1048576;
3516
- /**
3517
- * 용도별 업로드 제한 — 서버(`POST /v1/uploads`)와 클라이언트가 공유하는 단일 원천이다.
3518
- *
3519
- * 크기는 **요청이 스스로 신고한 값**으로만 검사한다. 바이트는 api를 거치지 않고 CDN으로
3520
- * 직접 PUT되므로 이 검사는 셀러가 올리기 전에 알게 하는 장치이고, 실제 상한은 CDN이 강제한다.
3521
- */
3522
- const UPLOAD_PURPOSE_LIMITS = {
3523
- "product-image": {
3524
- extensions: [
3525
- "jpg",
3526
- "jpeg",
3527
- "png",
3528
- "webp",
3529
- "avif",
3530
- "gif"
3531
- ],
3532
- maxBytes: 10 * MB
3533
- },
3534
- description: {
3535
- extensions: [
3536
- "jpg",
3537
- "jpeg",
3538
- "png",
3539
- "gif",
3540
- "webp",
3541
- "avif",
3542
- "svg",
3543
- "mp4",
3544
- "webm",
3545
- "mov"
3546
- ],
3547
- maxBytes: 50 * MB
3548
- },
3549
- "digital-asset": {
3550
- extensions: [
3551
- "pdf",
3552
- "epub",
3553
- "zip",
3554
- "mp3",
3555
- "wav",
3556
- "flac",
3557
- "m4a",
3558
- "mp4",
3559
- "mov",
3560
- "jpg",
3561
- "jpeg",
3562
- "png",
3563
- "webp",
3564
- "psd",
3565
- "ai",
3566
- "txt",
3567
- "csv",
3568
- "xlsx",
3569
- "docx",
3570
- "pptx"
3571
- ],
3572
- maxBytes: 200 * MB
3573
- }
3574
- };
3575
- /** 비공개 업로드(판매 파일)의 PUT 필수 헤더 — CDN이 이 값으로 공개 조회를 막는다 */
3576
- const PRIVATE_UPLOAD_HEADERS = { "x-amz-meta-visibility": "private" };
3577
- /** 파일명의 소문자 확장자. 확장자가 없으면 `""` */
3578
- function uploadExtensionOf(filename) {
3579
- const dot = filename.lastIndexOf(".");
3580
- if (dot < 0 || dot === filename.length - 1) return "";
3581
- return filename.slice(dot + 1).toLowerCase();
3582
- }
3583
- /**
3584
- * 용도 제한 위반 여부. 통과하면 `null`.
3585
- * 서버가 던지는 오류와 콘솔이 먼저 띄우는 안내가 갈리지 않도록 한 함수로 판정한다.
3586
- */
3587
- function uploadRejection(purpose, filename, size) {
3588
- const limit = UPLOAD_PURPOSE_LIMITS[purpose];
3589
- if (!limit.extensions.includes(uploadExtensionOf(filename))) return {
3590
- status: 400,
3591
- code: "UNSUPPORTED_FILE_TYPE",
3592
- message: `지원하지 않는 파일 형식입니다. ${limit.extensions.join(", ")}만 올릴 수 있습니다`
3593
- };
3594
- if (size !== void 0 && size > limit.maxBytes) return {
3595
- status: 413,
3596
- code: "FILE_TOO_LARGE",
3597
- message: `파일이 너무 큽니다. ${Math.floor(limit.maxBytes / MB)}MB까지 올릴 수 있습니다`
3598
- };
3599
- return null;
3600
- }
3601
- /**
3602
- * 미디어 업로드 자리 발급 — `POST /v1/uploads`.
3603
- *
3604
- * 파일 바이트는 api를 거치지 않는다. 응답의 `uploadUrl`로 클라이언트가 직접 PUT하고,
3605
- * 끝나면 `url`이 공개 주소가 된다. 서버가 객체 키를 정하므로 요청은 파일명만 보낸다
3606
- * (확장자만 쓰이고 이름 자체는 저장되지 않는다).
3607
- */
3608
- const uploadRequestSchema = z.object({
3609
- filename: z.string().min(1).max(255),
3610
- purpose: uploadPurposeSchema.default("description").describe("업로드 용도. 허용 확장자와 크기 상한이 용도별로 다르다"),
3611
- size: z.number().int().positive().optional().describe("파일 바이트 크기. 주면 용도별 상한을 발급 전에 검사한다")
3612
- });
3613
- const uploadTicketSchema = z.object({
3614
- /** 업로드 완료 후 공개될 주소. 비공개 업로드(`digital-asset`)는 이 주소로 받을 수 없다 */
3615
- url: z.url(),
3616
- /** 파일 바이트를 PUT할 presigned URL. 1시간 유효 */
3617
- uploadUrl: z.url(),
3618
- key: z.string().optional().describe("객체 키. 비공개 업로드(`digital-asset`)는 이 값으로 상품에 파일을 연결한다(`POST /v1/products/{productId}/digital-assets`). 공개 업로드에는 없다"),
3619
- headers: z.record(z.string(), z.string()).optional().default({}).describe("PUT에 반드시 실을 헤더. 비공개 업로드는 `x-amz-meta-visibility: private`이고 빠뜨리면 공개 객체가 된다. 공개 업로드는 빈 객체다")
3620
- });
3621
- //#endregion
3622
- //#region src/schemas/product.ts
3623
- const productStatusSchema = z.enum([
3624
- "SALE",
3625
- "SUSPENDED",
3626
- "OUT_OF_STOCK",
3627
- "CLOSED"
3628
- ]);
3629
- const productSummarySchema = z.object({
3630
- productId: z.string(),
3631
- name: z.string(),
3632
- status: productStatusSchema,
3633
- salePrice: z.number().int().describe("판매가. 원(KRW) 단위 정수"),
3634
- discountedPrice: z.number().int().nullable().describe("즉시 할인 적용가. 원 단위. 할인이 없으면 null"),
3635
- /** usable variant 재고 합계 (서버 계산) */
3636
- stockQuantity: z.number().int().describe("판매 가능한 옵션 재고의 합계. 서버가 계산한다"),
3637
- categoryId: z.string().nullable(),
3638
- thumbnailUrl: z.url().optional(),
3639
- /** optionGroups.length > 0 */
3640
- hasOptions: z.boolean(),
3641
- fulfillment: z.object({ type: fulfillmentTypeResponseSchema }).describe("상품 이행의 유형. 유형별 속성은 단건 조회(`GET /v1/products/{productId}`)의 `fulfillment`에 있다"),
3642
- updatedAt: z.string()
3643
- });
3644
- /** 옵션 축(그룹)이 가지는 값 하나 — 예: 축 "색상"의 값 "블랙" */
3645
- const optionValueSchema = z.object({
3646
- valueId: z.string(),
3647
- name: z.string(),
3648
- sortOrder: z.number().int()
3649
- });
3650
- /** 옵션 축(그룹) — 예: "색상", "사이즈" */
3651
- const optionGroupSchema = z.object({
3652
- groupId: z.string(),
3653
- name: z.string(),
3654
- sortOrder: z.number().int(),
3655
- values: z.array(optionValueSchema)
3656
- });
3657
- /**
3658
- * 판매 단위(SKU). variantId는 기존 optionId 값 공간을 승계하므로
3659
- * 카트·주문·리뷰·클레임의 optionId가 곧 variantId다.
3660
- */
3661
- const productVariantSchema = z.object({
3662
- variantId: z.string(),
3663
- /** 축 sortOrder 순서와 1:1 대응. 옵션 없는 상품은 [] */
3664
- valueIds: z.array(z.string()),
3665
- /** 값 이름을 " / "로 이은 파생 표시명. 옵션 없으면 "" */
3666
- name: z.string(),
3667
- additionalPrice: z.number().int(),
3668
- stockQuantity: z.number().int(),
3669
- sku: z.string().nullable(),
3670
- barcode: z.string().nullable(),
3671
- costPrice: z.number().int().nullable(),
3672
- weightGram: z.number().int().nullable(),
3673
- sellerCode: z.string().nullable(),
3674
- usable: z.boolean()
3675
- });
3676
- const discountSchema = z.object({
3677
- productId: z.string(),
3678
- discountType: z.enum(["RATE", "AMOUNT"]),
3679
- discountValue: z.number().int(),
3680
- discountedPrice: z.number().int(),
3681
- startAt: z.string().nullable(),
3682
- endAt: z.string().nullable()
3683
- });
3684
- const productSchema = productSummarySchema.extend({
3685
- description: z.string().optional(),
3686
- /**
3687
- * 상세설명 에디터(@avarlabs/editor)의 직렬화 상태. 재편집 때 이 값으로 문서를 복원한다.
3688
- * 표시는 `description`(HTML)이 맡는다 — 소비자는 이 필드를 몰라도 된다.
3689
- */
3690
- descriptionJson: z.string().nullable().optional(),
3691
- originalPrice: z.number().int().nullable().optional(),
3692
- images: z.array(z.url()).optional(),
3693
- attributes: z.array(z.object({
3694
- attributeId: z.string(),
3695
- value: z.string()
3696
- })).optional(),
3697
- optionGroups: z.array(optionGroupSchema),
3698
- /** 모든 상품이 최소 1개 variant를 갖는다 (옵션 없으면 default variant 1개) */
3699
- variants: z.array(productVariantSchema),
3700
- fulfillment: productFulfillmentResponseSchema,
3701
- fulfillmentTypeLocked: z.boolean().describe("판매 이력(주문상품)이 있어 상품 유형(`fulfillment.type`)을 바꿀 수 없는가. true면 다른 유형으로 수정하는 요청이 409 `FULFILLMENT_TYPE_LOCKED`다. 같은 유형 안의 속성은 바꿀 수 있다"),
3702
- refundPolicy: productRefundPolicyPatchSchema.nullable().describe("상품별 환불 정책 덮어쓰기(상품이 정한 항목만). null이면 스토어 기본 환불 정책을 쓴다. 반품·교환 배송비(`returnDeliveryFee`·`exchangeDeliveryFee`)는 배송 상품에만 있다"),
3703
- /**
3704
- * 설정된 즉시할인. `GET /products/{productId}/discount`의 응답과 같은 형태다(이슈 #51).
3705
- * 할인이 없으면 null이다. 이 필드를 싣기 전 서버의 응답에는 없으므로 optional이다.
3706
- */
3707
- discount: discountSchema.nullable().optional().describe("설정된 즉시할인. GET /v1/products/{productId}/discount 응답과 같은 형태다. 할인이 없으면 null"),
3708
- createdAt: z.string().optional()
3709
- });
3710
- /** 상품 하나에 등록할 수 있는 이미지 최대 장수 — 첫 장이 대표(썸네일)다 */
3711
- const MAX_PRODUCT_IMAGES = 10;
3712
- /**
3713
- * 상품 이미지 주소의 와이어 형식 — URL이고 2048자 이하다.
3714
- *
3715
- * https·확장자 규칙은 여기서 강제하지 않고 `productImageRejection`이 판정한다. 서버가
3716
- * **새로 추가된 주소에만** 그 규칙을 적용해야 하기 때문이다(규칙이 생기기 전에 저장된
3717
- * 주소 하나로 상품 수정 전체가 막히면 셀러가 빠져나갈 길이 없다).
3718
- *
3719
- * 응답 스키마(`productSchema.images`)도 넓게 둔다. 과거에 저장된 주소를 파싱 오류로
3720
- * 만들면 상품 조회 자체가 막힌다.
3721
- */
3722
- const productImageUrlSchema = z.url().max(2048).describe("상품 이미지 주소. https만 허용하고, 확장자가 있으면 이미지 형식이어야 한다");
3723
- /**
3724
- * 상품에 **새로 붙이는** 이미지 주소의 규칙. 통과하면 `null`.
3211
+ * 상품에 **새로 붙이는** 이미지 주소의 규칙. 통과하면 `null`.
3725
3212
  *
3726
3213
  * - https만 받는다. http는 https 스토어프론트에서 혼합 콘텐츠로 차단되고 `data:`·`javascript:`는
3727
3214
  * 상세 화면에 박히는 주입 경로가 된다.
@@ -3755,139 +3242,511 @@ function productImageRejection(url) {
3755
3242
  };
3756
3243
  return null;
3757
3244
  }
3758
- /** 옵션 축(그룹) 최대 개수 — 스키마 상한의 단일 원천 */
3759
- const MAX_OPTION_GROUPS = 3;
3760
- /** 축 하나가 가질 수 있는 값의 최대 개수 */
3761
- const MAX_VALUES_PER_GROUP = 100;
3762
- /** 상품 하나가 가질 수 있는 variant(SKU) 최대 개수 */
3763
- const MAX_VARIANTS = 500;
3764
3245
  /**
3765
- * 식별 코드 — 빈 문자열·공백만 있는 값은 거부한다.
3766
- * `""`를 통과시키면 서버 중복 사전 검사(falsy skip)와 DB partial unique
3767
- * (`WHERE ... is not null`)의 판정이 갈려 409 대신 500이 나간다.
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
3381
+ //#region src/schemas/order.ts
3382
+ const orderSummarySchema = z.object({
3383
+ orderId: z.string(),
3384
+ ordererName: z.string(),
3385
+ itemCount: z.number().int(),
3386
+ totalAmount: z.number().int(),
3387
+ representativeStatus: orderItemStatusSchema,
3388
+ orderedAt: z.string(),
3389
+ testPayment: z.boolean().describe("테스트 결제(샌드박스 결제) 주문인가. true면 실제로 돈이 오가지 않았습니다. 상품을 발송하지 않도록 주의하십시오")
3390
+ });
3391
+ const shippingAddressSchema = z.object({
3392
+ receiverName: z.string(),
3393
+ phone: z.string(),
3394
+ zipCode: z.string(),
3395
+ address1: z.string(),
3396
+ address2: z.string().nullish(),
3397
+ deliveryMemo: z.string().nullish(),
3398
+ entranceCode: z.string().nullish()
3399
+ });
3400
+ /** 배송 없는 주문상품(`MANUAL`)의 제공 기록 (이슈 #48) — 셀러의 제공 처리가 만든다 */
3401
+ const orderItemFulfillmentSchema = z.object({
3402
+ status: z.string().describe("제공 상태. `FULFILLED`(제공함)·`REVOKED`(전량 취소·환불로 회수함). 값이 추가될 수 있어 문자열로 받는다"),
3403
+ fulfilledAt: z.string().describe("제공 시각 (ISO 8601)"),
3404
+ note: z.string().nullable().describe("셀러가 제공 처리 때 남긴 이용 안내. 없으면 null"),
3405
+ mode: z.string().optional().describe("**Deprecated** — 항상 `MANUAL`이다. 자동 제공을 없앴고(이슈 #48) store-sdk 0.19 이하 호환으로만 싣는다. 게시된 연동이 store-sdk 0.20 이상으로 넘어간 뒤 다음 릴리스에서 뺀다")
3406
+ });
3407
+ const orderItemSchema = z.object({
3408
+ orderItemId: z.string(),
3409
+ orderId: z.string(),
3410
+ status: orderItemStatusSchema,
3411
+ claimStatus: z.enum([
3412
+ "CANCEL_REQUESTED",
3413
+ "RETURN_REQUESTED",
3414
+ "EXCHANGE_REQUESTED"
3415
+ ]).nullable(),
3416
+ productId: z.string(),
3417
+ productName: z.string(),
3418
+ optionId: z.string().nullable(),
3419
+ optionName: z.string().nullable(),
3420
+ optionSelections: z.array(optionSelectionSchema).optional().default([]).describe("주문 시점 옵션 선택(이슈 #59) — 조합 옵션(옵션명마다 한 칸, 추가금 0)과 추가 선택(값의 추가금). 옵션이 없거나 이 필드 이전 주문은 빈 배열. 서버는 항상 싣는다"),
3421
+ customInputs: z.array(customInputValueSchema).optional().default([]).describe("구매자가 적은 직접 입력값(주문 시점 항목명과 값, 이슈 #59). 없으면 빈 배열. 서버는 항상 싣는다"),
3422
+ sellerCode: z.string().nullable(),
3423
+ fulfillmentSnapshot: orderItemFulfillmentSnapshotResponseSchema.describe("주문 시점 이행 스냅샷(이슈 #57). `type`이 주문 시점 상품 유형이다. 배송 상품이 아니면 발송·배송완료 대신 제공 처리를 하고, `DELIVERED`는 제공 완료다. 상품을 나중에 고쳐도 바뀌지 않는다. 제공 기록은 `fulfillment`에 있다"),
3424
+ quantity: z.number().int().describe("주문 수량"),
3425
+ canceledQuantity: z.number().int().describe("취소·반품으로 환불된 누적 수량. 수량 단위 부분 취소가 없었으면 0이다"),
3426
+ activeQuantity: z.number().int().describe("살아 있는 수량 (`quantity` − `canceledQuantity`). 0이면 주문상품이 전량 취소됐다"),
3427
+ unitPrice: z.number().int(),
3428
+ discountAmount: z.number().int(),
3429
+ totalPrice: z.number().int().describe("주문상품 금액 (즉시할인 반영 단가 × 주문 수량). 쿠폰 할인 전 금액이다"),
3430
+ couponDiscountAmount: z.number().int().optional().default(0).describe("이 주문상품에 배분된 쿠폰 할인 — 상품 쿠폰 + 주문 쿠폰 배분. 실결제 금액은 `totalPrice − couponDiscountAmount`이고 취소·반품 환불은 실결제 금액을 수량 비율로 나눈다. 쿠폰이 없으면 0. 서버는 항상 싣는다"),
3431
+ hold: z.object({
3432
+ reason: z.string(),
3433
+ expectedDispatchDate: z.string()
3434
+ }).nullable(),
3435
+ fulfillment: orderItemFulfillmentSchema.nullable().optional().default(null).describe("배송 없는 상품의 제공 기록(이슈 #48). 제공 전이거나 배송 상품이면 null이다. 서버는 항상 싣는다"),
3436
+ autoDecisionDate: z.string().nullable(),
3437
+ sellerMemo: z.string().nullable(),
3438
+ orderedAt: z.string(),
3439
+ lastChangedAt: z.string(),
3440
+ lastChangedType: changeTypeSchema,
3441
+ testPayment: z.boolean().describe("소속 주문이 테스트 결제(샌드박스 결제) 주문인가. true면 실제로 돈이 오가지 않았습니다")
3442
+ });
3443
+ /** 주문이 받은 쿠폰 한 장 (이슈 #47) */
3444
+ const orderDiscountSchema = z.object({
3445
+ couponId: z.string().describe("쿠폰 id"),
3446
+ name: z.string().describe("주문 시점 쿠폰 이름"),
3447
+ kind: z.string().describe("쿠폰 종류. 현재 값은 `ORDER`(주문 쿠폰)·`PRODUCT`(상품 쿠폰)·`DELIVERY`(배송비 쿠폰)이고 값이 추가될 수 있어 문자열로 받는다"),
3448
+ discountAmount: z.number().int().describe("이 쿠폰의 할인액 (원)"),
3449
+ issueId: z.string().nullable().describe("발급 쿠폰 id. 코드 입력형은 null")
3450
+ });
3451
+ const orderSchema = z.object({
3452
+ orderId: z.string(),
3453
+ orderedAt: z.string(),
3454
+ orderer: z.object({
3455
+ name: z.string(),
3456
+ phone: z.string(),
3457
+ email: z.email().optional()
3458
+ }),
3459
+ requiresShipping: z.boolean().optional().default(true).describe("택배 배송이 필요한 주문상품이 있는가. false면 `shippingAddress`는 주문자 이름·연락처만 있고 주소는 빈 문자열이다. 서버는 항상 싣는다"),
3460
+ shippingAddress: shippingAddressSchema,
3461
+ payment: z.object({
3462
+ method: z.enum([
3463
+ "CARD",
3464
+ "BANK_TRANSFER",
3465
+ "VIRTUAL_ACCOUNT",
3466
+ "MOBILE",
3467
+ "POINT",
3468
+ "MIXED"
3469
+ ]).describe("method는 호환용이며 간편결제를 CARD로 표시한다. 실제 수단은 approvedMethod를 본다"),
3470
+ approvedMethod: z.string().nullable().optional().default(null).describe("실제 결제수단. method는 호환용이며 간편결제를 CARD로 표시한다. 실제 수단은 approvedMethod를 본다. 현재 값은 `pgPaymentMethodSchema`(`PgPaymentMethod`: CARD·BANK_TRANSFER·VIRTUAL_ACCOUNT·MOBILE·EASY_PAY)이고 매출 리포트·스토어프론트·환불 원장과 같은 값이다. 값이 추가될 수 있어 문자열로 받는다. PG가 알려 주지 않았으면 요청 수단, 시도 기록이 없는 오래된 주문은 null. 서버는 항상 싣는다"),
3471
+ easyPayProvider: easyPayProviderResponseSchema.nullable().optional().default(null).describe("승인된 간편결제사. `approvedMethod`가 `EASY_PAY`일 때만 값이 있고, PG가 알려 주지 않았으면 요청한 간편결제사다. 그 밖의 결제수단과 간편결제 이전 주문은 null. 서버는 항상 싣는다. 현재 값은 `NAVERPAY`·`KAKAOPAY`·`TOSSPAY`·`PAYCO`이고 값이 추가될 수 있어 문자열로 받는다"),
3472
+ productAmount: z.number().int().describe("상품 금액 합 (즉시할인 반영, 쿠폰 전)"),
3473
+ deliveryFee: z.number().int().describe("배송비 (쿠폰 전)"),
3474
+ discountAmount: z.number().int().describe("즉시할인 합 (정가 − 할인 단가) × 수량. 쿠폰 할인은 `couponDiscountAmount`로 따로 온다"),
3475
+ couponDiscountAmount: z.number().int().optional().default(0).describe("상품·주문 쿠폰 할인 합계. 쿠폰이 없으면 0. 서버는 항상 싣는다"),
3476
+ deliveryDiscountAmount: z.number().int().optional().default(0).describe("배송비 쿠폰 할인. 쿠폰이 없으면 0. 서버는 항상 싣는다"),
3477
+ discounts: z.array(orderDiscountSchema).optional().default([]).describe("주문이 받은 쿠폰. 쿠폰이 없으면 빈 배열. 서버는 항상 싣는다"),
3478
+ totalAmount: z.number().int().describe("결제 금액 = productAmount + deliveryFee − couponDiscountAmount − deliveryDiscountAmount. 쿠폰이 없는 주문은 productAmount + deliveryFee다"),
3479
+ paidAt: z.string().nullable(),
3480
+ cashReceipt: cashReceiptSchema.nullable().optional().default(null).describe("현금영수증. 구매자가 계좌이체·가상계좌 결제에서 신청했을 때만 있고 그 밖에는 null. 서버는 항상 싣는다")
3481
+ }),
3482
+ testPayment: z.boolean().describe("테스트 결제(샌드박스 결제) 주문인가. true면 실제로 돈이 오가지 않았습니다. 상품을 발송하지 않도록 주의하십시오"),
3483
+ items: z.array(orderItemSchema)
3484
+ });
3485
+ /** 주문 목록 응답 — `GET /v1/orders` */
3486
+ const orderSummaryPageSchema = pageSchema(orderSummarySchema);
3487
+ /** 주문상품 변경 목록 응답 — `GET /v1/order-items` */
3488
+ const orderItemPageSchema = pageSchema(orderItemSchema);
3489
+ const testPaymentQuery = z.enum(["true", "false"]).optional().describe("테스트 결제(샌드박스 결제) 필터. `true`면 테스트 결제만, `false`면 실결제만, 생략하면 전부입니다");
3490
+ const pageQuery = { page: z.string().optional().describe("페이지 번호 (1부터, 기본 1)") };
3491
+ /**
3492
+ * `GET /v1/orders` 쿼리 — OpenAPI 문서용 형태(값은 쿼리 문자열). 검증 규칙은 서버가 적용한다:
3493
+ * `orderedFrom` 누락 400 `MISSING_PARAMETER`, 날짜 형식 오류 400 `INVALID_PARAMETER`, 31일 초과 400 `RANGE_TOO_WIDE`,
3494
+ * `testPayment`가 `true`·`false`가 아니면 400 `INVALID_PARAMETER`
3495
+ */
3496
+ const orderListQuerySchema = z.object({
3497
+ orderedFrom: z.string().describe("조회 시작 시각 (ISO 8601 날짜 또는 일시, 포함). 필수"),
3498
+ orderedTo: z.string().optional().describe("조회 끝 시각 (ISO 8601, 포함). 생략하면 orderedFrom부터 31일. 기간은 최대 31일"),
3499
+ status: orderItemStatusSchema.optional().describe("주문상품 상태 필터 — 이 상태의 주문상품이 하나라도 있는 주문"),
3500
+ keyword: z.string().optional().describe("주문번호(일치)·주문자명·수령인명(포함) 검색"),
3501
+ fulfillmentType: z.string().optional().describe("상품 유형 필터(이슈 #48) — 이 유형(`SHIPPING`·`MANUAL`)의 주문상품이 하나라도 있는 주문"),
3502
+ testPayment: testPaymentQuery,
3503
+ ...pageQuery,
3504
+ size: z.string().optional().describe("페이지 크기 (기본 20, 최대 100)")
3505
+ });
3506
+ /**
3507
+ * `GET /v1/order-items` 쿼리 — OpenAPI 문서용 형태. `lastChangedFrom` 누락 400 `MISSING_PARAMETER`, 형식 오류 400
3508
+ * `INVALID_PARAMETER`, 24시간 초과 400 `RANGE_TOO_WIDE`, `testPayment` 값 오류 400 `INVALID_PARAMETER`
3509
+ */
3510
+ const orderItemListQuerySchema = z.object({
3511
+ lastChangedFrom: z.string().describe("마지막 변경 시각 조회 시작 (ISO 8601, 포함). 필수"),
3512
+ lastChangedTo: z.string().optional().describe("마지막 변경 시각 조회 끝 (ISO 8601, 포함). 생략하면 시작부터 24시간. 기간은 최대 24시간"),
3513
+ lastChangedType: changeTypeSchema.optional().describe("마지막 변경 유형 필터"),
3514
+ testPayment: testPaymentQuery,
3515
+ ...pageQuery,
3516
+ size: z.string().optional().describe("페이지 크기 (기본 100, 최대 300)")
3517
+ });
3518
+ const deliveryMethodSchema = z.enum([
3519
+ "COURIER",
3520
+ "DIRECT",
3521
+ "QUICK",
3522
+ "NOTHING"
3523
+ ]);
3524
+ const dispatchRequestSchema = z.object({
3525
+ deliveryMethod: deliveryMethodSchema,
3526
+ carrierCode: z.string().optional(),
3527
+ trackingNumber: z.string().optional(),
3528
+ /**
3529
+ * 발송 시각. ISO 8601이고 시간대(`Z` 또는 `+09:00`)를 포함해야 한다. 생략하면 현재 시각이다.
3530
+ *
3531
+ * 전에는 `z.string()`이라 아무 문자열이나 그대로 저장됐다. 이 값은 배송 정보 jsonb에 들어가
3532
+ * 자동 배송완료·추적 동기화 잡의 대상 선별 조건이 된다 — 잘못된 한 행이 스토어 구분 없이 도는
3533
+ * 잡 전체를 멈출 수 있어 입력에서 좁혔다. 검증을 통과한 값도 저장 전에 UTC ISO로 정규화한다.
3534
+ */
3535
+ dispatchedAt: z.iso.datetime({ offset: true }).optional()
3536
+ }).refine((value) => value.deliveryMethod === "DIRECT" || value.deliveryMethod === "NOTHING" ? true : !!value.carrierCode && !!value.trackingNumber, { message: "COURIER/QUICK 발송은 carrierCode와 trackingNumber가 필수입니다" });
3537
+ const deliveryInfoSchema = z.object({
3538
+ orderItemId: z.string(),
3539
+ deliveryMethod: deliveryMethodSchema,
3540
+ carrierCode: z.string().nullable(),
3541
+ carrierName: z.string().nullable(),
3542
+ trackingNumber: z.string().nullable(),
3543
+ trackingUrl: z.url().nullable(),
3544
+ status: z.enum([
3545
+ "DISPATCHED",
3546
+ "DELIVERING",
3547
+ "DELIVERED"
3548
+ ]),
3549
+ dispatchedAt: z.string(),
3550
+ deliveredAt: z.string().nullable()
3551
+ });
3552
+ /**
3553
+ * 택배 추적 단계. 응답에서는 문자열로 받는다(값이 추가될 수 있다) — 현재 값은 `ACCEPTED`(접수)·`IN_TRANSIT`(이동 중)·
3554
+ * `OUT_FOR_DELIVERY`(배송 출발)·`DELIVERED`(배송완료)다.
3555
+ */
3556
+ const trackingStageResponseSchema = z.string();
3557
+ /** `GET /v1/order-items/{orderItemId}/delivery` 응답 — 배송 정보 + 배송 추적 이벤트 */
3558
+ const deliveryStatusSchema = deliveryInfoSchema.extend({
3559
+ trackingEvents: z.array(z.object({
3560
+ status: z.string().describe("택배사가 알려 준 처리 내용(예: 집화처리, 배송출발)"),
3561
+ location: z.string().describe("처리 장소. 모르면 빈 문자열"),
3562
+ occurredAt: z.string(),
3563
+ stage: trackingStageResponseSchema.nullable().optional().default(null).describe("이 이벤트의 추적 단계. 배달 실패처럼 단계로 나눌 수 없는 이벤트는 null. 서버는 항상 싣는다. 현재 값은 `ACCEPTED`·`IN_TRANSIT`·`OUT_FOR_DELIVERY`·`DELIVERED`이고 값이 추가될 수 있어 문자열로 받는다")
3564
+ })).optional().describe("배송 추적 이력. 시각 오름차순(오래된 것부터)"),
3565
+ tracking: z.object({
3566
+ result: z.string().nullable().describe("마지막 추적 조회 결과. 조회에 성공한 적이 없으면 null. 현재 값은 `FOUND`(조회됨)·`NOT_FOUND`(택배사 전산에 송장 없음)·`UNSUPPORTED_CARRIER`(추적을 지원하지 않는 택배사)이고 값이 추가될 수 있어 문자열로 받는다"),
3567
+ stage: trackingStageResponseSchema.nullable().describe("현재 추적 단계. 조회되지 않았으면 null. 현재 값은 `ACCEPTED`·`IN_TRANSIT`·`OUT_FOR_DELIVERY`·`DELIVERED`이고 값이 추가될 수 있어 문자열로 받는다"),
3568
+ checkedAt: z.string().nullable().describe("택배사에서 추적 정보를 마지막으로 받아 온 시각(ISO 8601)"),
3569
+ carrierDeliveredAt: z.string().nullable().describe("택배사가 알려 준 배송완료 시각(ISO 8601). 추적으로 배송완료가 반영되면 주문상품의 `deliveredAt`은 반영 시각이라 이 값과 다를 수 있다")
3570
+ }).nullable().optional().default(null).describe("택배 추적 요약. 택배 발송이 아니거나 추적을 쓰지 않으면 null. 추적이 배송완료면 주문상품이 자동으로 배송완료된다. 서버는 항상 싣는다")
3571
+ });
3572
+ /** `PUT /v1/order-items/{orderItemId}/dispatch/tracking` 응답 — 바뀐 송장 */
3573
+ const trackingUpdateResultSchema = z.object({
3574
+ orderItemId: z.string(),
3575
+ carrierCode: z.string(),
3576
+ trackingNumber: z.string()
3577
+ });
3578
+ const carrierSchema = z.object({
3579
+ carrierCode: z.string(),
3580
+ name: z.string(),
3581
+ trackingUrlTemplate: z.string()
3582
+ });
3583
+ const holdOrderItemRequestSchema = z.object({
3584
+ reason: z.enum([
3585
+ "OUT_OF_STOCK",
3586
+ "CUSTOMER_REQUEST",
3587
+ "CUSTOMS",
3588
+ "ETC"
3589
+ ]),
3590
+ detail: z.string().max(500).optional(),
3591
+ expectedDispatchDate: z.string()
3592
+ });
3593
+ const cancelBySellerRequestSchema = z.object({
3594
+ reason: z.enum([
3595
+ "OUT_OF_STOCK",
3596
+ "PRICE_ERROR",
3597
+ "DELIVERY_IMPOSSIBLE",
3598
+ "ETC"
3599
+ ]),
3600
+ detail: z.string().max(500).optional(),
3601
+ quantity: z.number().int().min(1).optional().describe("취소할 수량. 생략하면 남은 수량 전체다. 남은 수량보다 크면 400 `INVALID_QUANTITY`. 일부만 취소하면 주문상품은 상태를 유지한 채 `canceledQuantity`가 늘고, 전 수량이 소진되면 `CANCELED`가 된다")
3602
+ });
3603
+ /**
3604
+ * 제공 처리 요청 (이슈 #48) — `POST /v1/order-items/{orderItemId}/fulfill`. 배송 없는 주문상품(`MANUAL`)을
3605
+ * 제공 완료(`DELIVERED`)로 바꾼다.
3768
3606
  */
3769
- const codeSchema = z.string().trim().min(1).max(64);
3770
- const optionValueRequestSchema = z.object({
3771
- /** 전달 시 기존 값 행을 유지(UPDATE). 생략하면 신규 발급 */
3772
- valueId: z.string().optional(),
3773
- name: z.string().min(1).max(50)
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
3774
3612
  });
3775
- const optionGroupRequestSchema = z.object({
3776
- /** 전달 시 기존 축 행을 유지(UPDATE). 생략하면 신규 발급 */
3777
- groupId: z.string().optional(),
3778
- name: z.string().min(1).max(50),
3779
- values: z.array(optionValueRequestSchema).min(1).max(100)
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()
3780
3624
  });
3781
- const productVariantRequestSchema = z.object({
3782
- /** 기존 variant 유지 시 전달. 생략하면 신규 생성 */
3783
- variantId: z.string().optional(),
3784
- /** optionGroups 순서대로 각 축에서 고른 값 이름. 서버가 이름 → valueId를 해석한다 */
3785
- valueNames: z.array(z.string()).default([]),
3786
- additionalPrice: z.number().int().default(0),
3787
- /**
3788
- * 생략 = 기존 재고 유지 (신규 variant는 0).
3789
- * 구조 교체(PUT)가 폼 로드 시점의 재고를 되쓰지 않게 하는 옵트인 필드다 —
3790
- * 재고만 바꿀 때는 PATCH /products/{id}/variants/{variantId}/stock을 쓴다.
3791
- */
3792
- stockQuantity: z.number().int().min(0).optional(),
3793
- sku: codeSchema.optional(),
3794
- barcode: codeSchema.optional(),
3795
- costPrice: z.number().int().min(0).optional(),
3796
- weightGram: z.number().int().min(0).optional(),
3797
- sellerCode: codeSchema.optional(),
3798
- usable: z.boolean().default(true)
3625
+ /** `PUT /v1/order-items/{orderItemId}/memo` 응답 — 빈 문자열로 저장하면 메모가 지워져 `null`이다 */
3626
+ const orderItemMemoResultSchema = z.object({
3627
+ orderItemId: z.string(),
3628
+ sellerMemo: z.string().nullable()
3799
3629
  });
3800
- /** 축 × 값 → variant 전체 교체 요청 (PUT /products/{productId}/options) */
3801
- const optionModelRequestSchema = z.object({
3802
- optionGroups: z.array(optionGroupRequestSchema).max(3).default([]),
3803
- variants: z.array(productVariantRequestSchema).min(1).max(500)
3630
+ /** `POST /v1/order-items/{orderItemId}/purchase-decision/request` 응답 */
3631
+ const purchaseDecisionRequestResultSchema = z.object({
3632
+ orderItemId: z.string(),
3633
+ requestedAt: z.string()
3804
3634
  });
3805
- /** GET /products/{productId}/options 응답 */
3806
- const optionModelSchema = z.object({
3807
- optionGroups: z.array(optionGroupSchema),
3808
- variants: z.array(productVariantSchema)
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)
3809
3640
  });
3810
3641
  /**
3811
- * 재고 변경 택일 위반 문구 — SDK `.refine`과 서버 DomainException이 공유하는 단일 원천.
3812
- * 경로에 따라 셀러가 다른 문구를 받지 않게 한다.
3642
+ * `GET /v1/orders/{orderId}/address-change-requests` 응답 — 배송지 변경 요청 플로우는 아직 제공하지 않아
3643
+ * 현재는 항상 빈 배열이다
3813
3644
  */
3814
- const VARIANT_STOCK_EXCLUSIVE_MESSAGE = "stockQuantity 또는 adjustment 중 하나만 전달해야 합니다";
3645
+ const addressChangeRequestListSchema = z.array(z.record(z.string(), z.unknown())).describe("배송지 변경 요청 목록 (현재 항상 빈 배열)");
3646
+ //#endregion
3647
+ //#region src/schemas/saved-option-set.ts
3815
3648
  /**
3816
- * variant 재고 변경 요청 — 절대값(`stockQuantity`) 또는 증감(`adjustment`) 택일.
3817
- * 서버도 같은 규칙을 400 VALIDATION_FAILED로 검증한다(이중 방어).
3649
+ * 저장 옵션 (이슈 #59) — 자주 쓰는 옵션 구성을 이름 붙여 두고 상품 등록에서 불러온다.
3650
+ *
3651
+ * **불러오기는 복사다.** 저장 옵션을 고치거나 지워도 이미 불러온 상품의 옵션은 그대로다. 재고·SKU·사용 여부는 상품마다
3652
+ * 달라 저장하지 않는다. 추가금은 조합별이 아니라 **값별**로 둔다 — 조합 옵션으로 불러오면 조합 추가금은 고른 값 추가금의 합이다.
3818
3653
  */
3819
- const variantStockRequestSchema = z.object({
3820
- stockQuantity: z.number().int().min(0).optional(),
3821
- adjustment: z.number().int().optional()
3822
- }).refine((v) => v.stockQuantity !== void 0 !== (v.adjustment !== void 0), { message: VARIANT_STOCK_EXCLUSIVE_MESSAGE });
3823
- /** PATCH /products/{productId}/variants/{variantId}/stock 응답 */
3824
- const variantStockSchema = z.object({
3825
- variantId: z.string(),
3826
- stockQuantity: z.number().int()
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 이상)")
3664
+ });
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()
3686
+ });
3687
+ const savedOptionValueRequestSchema = z.object({
3688
+ name: nameSchema,
3689
+ additionalPrice: z.number().int().min(0).default(0)
3690
+ });
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)
3696
+ });
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)
3827
3702
  });
3828
3703
  /**
3829
- * 상품 쓰기 요청의 공통 필드 — 등록(POST)과 전체 수정(PUT)이 같다. 이행(`fulfillment`)만 두 요청의 스키마가 다르다.
3830
- *
3831
- * 최상위가 `strictObject`다. 모르는 필드는 400이다 — 이슈 #57 이전의 평면 필드(`deliveryFee`·`fulfillmentType`·`returnFee` 등)를
3832
- * 보낸 요청이 조용히 무시되지 않게 한다. 옛 필드는 `fulfillment.{유형}.…`과 `refundPolicy`로 옮겼다.
3704
+ * 저장 옵션 만들기·수정 본문. 조합 옵션명은 최대 `MAX_OPTION_GROUPS`개, 추가 선택은 최대 `MAX_ADDON_GROUPS`개이고
3705
+ * 옵션명과 직접 입력 항목이 모두 비면 400이다. 수정(PUT)은 통째로 바꾼다.
3833
3706
  */
3834
- const productWriteShape = {
3835
- name: z.string().min(1).max(100),
3836
- categoryId: z.string().min(1, "카테고리를 선택해주십시오"),
3837
- salePrice: z.number().int().min(10).multipleOf(10, "판매가는 10원 단위여야 합니다 (INVALID_PRICE_UNIT)").describe("판매가. 원(KRW) 단위 정수, 최소 10원, 10원 단위"),
3838
- originalPrice: z.number().int().optional().describe("정가(할인 전 표시 가격). 원 단위"),
3839
- description: z.string().optional(),
3840
- /**
3841
- * 상세설명 에디터(@avarlabs/editor)의 직렬화 상태. 재편집 때 이 값으로 문서를 복원한다.
3842
- * 표시는 `description`(HTML)이 맡는다 — 소비자는 이 필드를 몰라도 된다.
3843
- */
3844
- descriptionJson: z.string().nullable().optional(),
3845
- images: z.array(productImageUrlSchema).min(1).max(10).optional().describe("상품 이미지 주소 목록. 첫 장이 대표 이미지(썸네일)다"),
3846
- attributes: z.array(z.object({
3847
- attributeId: z.string(),
3848
- value: z.string()
3849
- })).optional(),
3850
- refundPolicy: productRefundPolicyPatchSchema.nullish().describe("상품별 환불 정책 — 준 항목만 스토어 기본을 덮어쓴다. `null`이나 생략은 스토어 기본을 그대로 쓴다는 뜻이다. 반품·교환 배송비(`returnDeliveryFee`·`exchangeDeliveryFee`)는 배송 상품에만 줄 수 있다(다른 유형은 400)"),
3851
- ...optionModelRequestSchema.shape
3852
- };
3853
- const productCreateRequestSchema = z.strictObject({
3854
- ...productWriteShape,
3855
- fulfillment: productFulfillmentCreateSchema.optional().describe("상품 이행. `type`이 유형이고 같은 이름의 키가 유형별 속성이다. 생략하면 배송 상품(기본값)이다. 배송 상품이 아닌 유형은 판매가 열린 유형만 등록할 수 있다(409 `FULFILLMENT_TYPE_NOT_AVAILABLE`)")
3856
- });
3857
- const fulfillmentUpdateDescription = "상품 이행. 생략하면 바꾸지 않는다. 주면 유형 안 필드를 모두 담아 통째로 바꾼다(현재 값과 병합하지 않는다). 판매 이력이 있으면 유형을 바꿀 수 없다(409 `FULFILLMENT_TYPE_LOCKED`)";
3858
- /** 전체 수정(PUT) — 공통 필드는 등록과 같고, 이행은 생략하면 현재 값 유지, 주면 필드가 모두 필수다 */
3859
- const productReplaceRequestSchema = z.strictObject({
3860
- ...productWriteShape,
3861
- fulfillment: productFulfillmentUpdateSchema.optional().describe(fulfillmentUpdateDescription)
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([])
3862
3712
  });
3863
- const productPatchRequestSchema = z.strictObject({
3864
- name: z.string().max(100).optional(),
3865
- salePrice: z.number().int().multipleOf(10).optional(),
3866
- status: productStatusSchema.optional(),
3867
- fulfillment: productFulfillmentUpdateSchema.optional().describe(fulfillmentUpdateDescription),
3868
- refundPolicy: productRefundPolicyPatchSchema.nullable().optional().describe("상품별 환불 정책. 주면 통째로 바꾼다(준 항목만 스토어 기본을 덮어쓴다). `null`은 덮어쓰기를 지워 스토어 기본을 따른다. 생략하면 바꾸지 않는다")
3869
- }).refine((value) => Object.values(value).some((v) => v !== void 0), { message: "최소 1개 필드가 필요합니다" });
3870
- const productSortSchema = z.enum([
3871
- "createdAt.desc",
3872
- "createdAt.asc",
3873
- "salePrice.asc",
3874
- "salePrice.desc",
3875
- "updatedAt.desc"
3713
+ /**
3714
+ * 상품에서 저장 (`POST /v1/saved-option-sets/from-product`) — 상품의 현재 옵션을 저장 옵션으로 만든다.
3715
+ *
3716
+ * 조합 추가금이 값별 추가금의 합으로 되돌려지면 값별로 저장하고, 조합마다 제각각이면 추가금을 빼고
3717
+ * 응답 `additionalPriceDropped: true`로 알린다. `replaceOptionSetId`를 주면 그 저장 옵션을 덮어쓴다(이름 중복의 덮어쓰기).
3718
+ */
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"
3876
3735
  ]);
3877
- /** `GET /v1/products` 쿼리 — OpenAPI 문서용 형태(값은 쿼리 문자열) */
3878
- const productListQuerySchema = z.object({
3879
- status: productStatusSchema.optional().describe("판매 상태 필터"),
3880
- categoryId: z.string().optional().describe("카테고리 필터 — 하위 카테고리의 상품도 포함"),
3881
- keyword: z.string().optional().describe("상품명 검색 (포함)"),
3882
- sellerCode: z.string().optional().describe("판매자 상품코드 — 이 코드가 붙은 variant를 가진 상품"),
3883
- sort: productSortSchema.optional().describe("정렬 (기본 createdAt.desc)"),
3884
- ...pageQueryShape()
3885
- });
3886
- const discountRequestSchema = z.object({
3887
- discountType: z.enum(["RATE", "AMOUNT"]),
3888
- discountValue: z.number().int().min(1),
3889
- startAt: z.string().optional(),
3890
- 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
+ })
3891
3750
  });
3892
3751
  //#endregion
3893
3752
  //#region src/schemas/shipping-policy.ts
@@ -4427,8 +4286,6 @@ const updateStoreRequestSchema = z.object({
4427
4286
  analyticsAllowedOrigins: z.array(analyticsOriginSchema).max(20).optional().describe("@deprecated `storefrontOrigins`를 쓴다(같은 목록). 방문 분석 수집을 받을 스토어프론트 오리진(최대 20개). 비우면 모든 오리진에서 받는다. 넣으면 목록에 없는 오리진의 수집 요청은 403 `ORIGIN_NOT_ALLOWED`이고, 페이지 URL이 목록 밖인 이벤트는 버린다"),
4428
4287
  storefrontOrigins: z.array(analyticsOriginSchema).max(20).optional().describe("스토어프론트 도메인(최대 20개) — 소셜 로그인 뒤 돌아갈 주소와 방문 분석 수집을 함께 제한한다. `analyticsAllowedOrigins`와 같은 목록이다(둘 다 주면 이 값이 이긴다)"),
4429
4288
  guestCheckoutEnabled: z.boolean().optional().describe("비회원 주문 허용. false(회원만)면 로그인하지 않은 구매자의 주문서 생성·결제 요청이 403 `GUEST_CHECKOUT_DISABLED`다. 장바구니 담기와 이미 받은 비회원 주문의 조회·취소는 막지 않는다"),
4430
- fulfillmentMailEnabled: z.boolean().optional().describe("비실물 상품 제공 안내 메일(이슈 #48). 켜면(기본) 제공이 끝났을 때 구매자에게 안내 메일을 보낸다. 코드·파일 주소는 싣지 않는다"),
4431
- withdrawalNotice: z.string().trim().min(1).max(1e3).nullable().optional().describe("청약철회 제한 고지 문구(이슈 #48). 제한 상품이 있는 주문서에 보인다. null이면 플랫폼 기본 문구를 쓴다"),
4432
4289
  autoDeliverDays: z.number().int().min(0).max(30).optional().describe(autoDeliverDaysDescription),
4433
4290
  autoConfirmDays: z.number().int().min(0).max(30).optional().describe(autoConfirmDaysDescription),
4434
4291
  refundPolicy: refundPolicyPatchSchema.optional().describe("스토어 기본 환불 정책 — 준 항목만 바꾼다. 상품별 정책(`products.refundPolicy`)이 없는 상품에 적용된다"),
@@ -4454,8 +4311,6 @@ const storeSchema = z.object({
4454
4311
  analyticsAllowedOrigins: z.array(z.string()).describe("방문 분석 수집을 받는 스토어프론트 오리진. 비어 있으면 모든 오리진에서 받는다"),
4455
4312
  storefrontOrigins: z.array(z.string()).describe("스토어프론트 도메인 — 소셜 로그인 리디렉트와 방문 분석 수집이 함께 쓴다(`analyticsAllowedOrigins`와 같은 값)"),
4456
4313
  guestCheckoutEnabled: z.boolean().describe("비회원 주문 허용. false면 회원만 주문할 수 있다"),
4457
- fulfillmentMailEnabled: z.boolean().optional().default(true).describe("비실물 상품 제공 안내 메일(이슈 #48). 서버는 항상 싣는다"),
4458
- withdrawalNotice: z.string().nullable().optional().default(null).describe("청약철회 제한 고지 문구(이슈 #48). null이면 플랫폼 기본 문구다. 서버는 항상 싣는다"),
4459
4314
  autoDeliverDays: z.number().int().describe(autoDeliverDaysDescription),
4460
4315
  autoConfirmDays: z.number().int().describe(autoConfirmDaysDescription),
4461
4316
  refundPolicy: refundPolicySchema.describe("스토어 기본 환불 정책. 스토어가 정하지 않은 항목은 플랫폼 기본값으로 채워 항상 전부 싣는다"),
@@ -4527,14 +4382,9 @@ const storeMemberRoleUpdatedSchema = z.object({
4527
4382
  role: z.enum(["ADMIN", "STAFF"])
4528
4383
  });
4529
4384
  /**
4530
- * 주소 유형 — 출고지·반품지·방문 수령지(이슈 #48). 방문 수령지(`PICKUP`)는 목록에서 `type=PICKUP`으로 조회할 때만 나온다
4531
- * (이 값을 모르는 옛 SDK의 목록 파싱이 깨지지 않게).
4385
+ * 주소 유형 — 출고지·반품지.
4532
4386
  */
4533
- const addressTypeSchema = z.enum([
4534
- "SHIPPING",
4535
- "RETURN",
4536
- "PICKUP"
4537
- ]);
4387
+ const addressTypeSchema = z.enum(["SHIPPING", "RETURN"]);
4538
4388
  /** `GET /v1/seller-addresses` 쿼리 — OpenAPI 문서용 형태. 기본 주소 우선, 그다음 등록순이다 */
4539
4389
  const sellerAddressListQuerySchema = z.object({ type: addressTypeSchema.optional().describe("주소 유형 필터") });
4540
4390
  const sellerAddressSchema = z.object({
@@ -4579,10 +4429,28 @@ const sellerAddressRequestSchema = z.object({
4579
4429
  * - 409 `STOREFRONT_SITE_CREATE_LIMIT` — 사이트는 스토어당 30일에 3번까지 만든다
4580
4430
  * - 409 `STOREFRONT_SITE_CHANGED` — 읽은 상태(`expected*`)와 현재 상태가 다르다. 다시 읽고 시도한다
4581
4431
  * - 409 `STOREFRONT_SITE_NOT_ACTIVE` — 정지(`SUSPENDED`)된 사이트는 바꾸거나 끌 수 없다
4432
+ * - 409 `STORE_SUSPENDED` — 정지된 스토어에는 사이트를 만들 수 없다
4582
4433
  * - 409 `STOREFRONT_VERSION_NOT_READY` — 빌드가 끝나지 않았거나 실패한 버전은 발행할 수 없다
4583
4434
  * - 409 `STOREFRONT_NO_ROLLBACK_TARGET` — 되돌릴 이전 발행 버전이 없다
4584
4435
  * - 400 `STOREFRONT_SLUG_CONFIRM_MISMATCH` — 호스팅 끄기 확인 입력이 현재 슬러그와 다르다
4585
4436
  * - 503 `STOREFRONT_PREVIEW_NOT_CONFIGURED` — 미리보기 서명 키가 설정되지 않았다
4437
+ *
4438
+ * 편집(M2b, `/v1/storefront/site/editing`·`files/**`·`checks/**`·`verify`·`builds/**`, 버전 만들기):
4439
+ * - 403 `STOREFRONT_EDITING_DISABLED` — 플랫폼 편집 스위치가 꺼져 있다
4440
+ * - 503 `STOREFRONT_EDITING_NOT_CONFIGURED` — 편집 Sandbox가 설정되지 않았다
4441
+ * - 409 `EDIT_CAPACITY_FULL` — 플랫폼 전체 동시 편집 상한에 걸렸다. `error.details.retryAfterSeconds` 뒤에 다시 한다
4442
+ * - 409 `EDIT_SESSION_REQUIRED` — 편집 미리보기 링크: 편집 환경이 없다
4443
+ * - 409 `CHECK_RUNNING` — 이 사이트에서 다른 종류의 검사가 돌고 있다(`error.details.checkId`)
4444
+ * - 404 `STOREFRONT_CHECK_NOT_FOUND`
4445
+ * - 409 `FILE_CHANGED` — 해시 조건이 어긋났다. 아무것도 쓰지 않았다(`error.details.conflicts: [{ path, currentHash }]`)
4446
+ * - 409 `FILE_EXISTS` — 옮길 자리에 파일이 있다(`overwrite: true`로 덮는다)
4447
+ * - 404 `FILE_NOT_FOUND` — 파일이 없다
4448
+ * - 400 `INVALID_FILE_PATH` — 경로 형식이 틀렸거나 편집할 수 없는 경로다(`node_modules`·`dist`·`.env*` 등). 쓸 수 있는 곳은
4449
+ * `src/**`·`public/**`와 루트 설정 파일 몇 개이고, `package.json`·`pnpm-lock.yaml`은 읽기만 된다
4450
+ * - 413 `FILE_TOO_LARGE`·`WORKSPACE_TOO_LARGE` — 파일 하나 1MiB, 작업 트리 전체 20MiB·2,000개를 넘는다
4451
+ * - 409 `STOREFRONT_BUILD_IN_PROGRESS` — 이 사이트에 끝나지 않은 빌드가 있다(사이트당 하나)
4452
+ * - 404 `STOREFRONT_BUILD_NOT_FOUND`
4453
+ * - 502 `STOREFRONT_SANDBOX_UNAVAILABLE` — 편집 Sandbox 호출이 실패했다. 잠시 뒤 다시 한다
4586
4454
  */
4587
4455
  /**
4588
4456
  * 슬러그 형식 — 스토어 코드와 같은 DNS 라벨 규칙(소문자·숫자·하이픈 3~30자, 하이픈으로 시작·끝 불가)에
@@ -4607,8 +4475,22 @@ const RESERVED_STOREFRONT_SLUGS = [
4607
4475
  "control",
4608
4476
  "outbound"
4609
4477
  ];
4478
+ /**
4479
+ * 편집 미리보기 호스트 접미사 — 편집 미리보기 주소는 `{사이트 id 정규화}-preview.{호스팅 도메인}`이다(M2b). 사이트 주소와 같은
4480
+ * 1단계 이름이라 슬러그가 이 모양이면 남의 편집 미리보기 주소와 겹칠 수 있어 슬러그로 쓰지 못한다.
4481
+ */
4482
+ const STOREFRONT_EDIT_PREVIEW_SUFFIX = "-preview";
4610
4483
  function isReservedStorefrontSlug(slug) {
4611
- return RESERVED_STOREFRONT_SLUGS.includes(slug.toLowerCase());
4484
+ const lower = slug.toLowerCase();
4485
+ return RESERVED_STOREFRONT_SLUGS.includes(lower) || lower.endsWith("-preview");
4486
+ }
4487
+ /**
4488
+ * 편집 미리보기 호스트 이름의 라벨 — 사이트 id의 `_`를 `-`로 바꾸고 `-preview`를 붙인다(`sfs_ab12cd34` → `sfs-ab12cd34-preview`).
4489
+ * 편집 Sandbox Worker(`apps/hosting-sandbox/src/preview-host.ts`)가 같은 규칙의 복제본을 들고 있고 api
4490
+ * `test/storefront-hosting-parity.spec.ts`가 둘을 대조한다. 규칙을 바꾸면 두 곳을 같이 고친다.
4491
+ */
4492
+ function storefrontEditPreviewLabelOf(siteId) {
4493
+ return `${siteId.replaceAll("_", "-")}${STOREFRONT_EDIT_PREVIEW_SUFFIX}`;
4612
4494
  }
4613
4495
  const storefrontSlugSchema = z.string().regex(STOREFRONT_SLUG_PATTERN, "소문자·숫자·하이픈 3~30자여야 하고 하이픈으로 시작하거나 끝날 수 없습니다").refine((slug) => !slug.includes("--"), "하이픈을 연달아 쓸 수 없습니다").refine((slug) => !isReservedStorefrontSlug(slug), "플랫폼이 쓰는 이름이라 쓸 수 없습니다").describe("주소 슬러그. `{slug}.{호스팅 도메인}`이 사이트 주소가 된다");
4614
4496
  /** 바꾼 슬러그를 다른 사이트가 가져가지 못하게 막는 기간(일). 보류 동안 옛 주소는 새 주소로 301한다 */
@@ -4629,8 +4511,10 @@ const storefrontTemplateSchema = z.object({
4629
4511
  isDefault: z.boolean().describe("기본 템플릿인가 (스토어 생성 시 자동 생성에 쓴다)")
4630
4512
  });
4631
4513
  /**
4632
- * 버전 상태 — M1은 템플릿 빌드 산출물을 그대로 쓰므로 만들 때 곧바로 `READY`다. `BUILDING`·`FAILED`는
4633
- * 셀러 편집 빌드(M2)가 쓴다. 발행 여부는 상태가 아니라 사이트의 발행 포인터가 원천이다(`published`).
4514
+ * 버전 상태 — 템플릿 버전(`source: TEMPLATE`)은 템플릿 산출물을 그대로 쓰므로 만들 때 곧바로 `READY`다. 편집 버전
4515
+ * (`source: EDIT`, M2b)은 작업 트리 스냅샷을 빌드하는 동안 `BUILDING`이고, 빌드·규칙 검증을 통과하면 `READY`, 실패하면
4516
+ * `FAILED`다(실패 단계·로그 꼬리는 `buildId`의 빌드). 발행할 수 있는 것은 `READY`뿐이다. 발행 여부는 상태가 아니라 사이트의
4517
+ * 발행 포인터가 원천이다(`published`).
4634
4518
  */
4635
4519
  const storefrontVersionStatusSchema = z.enum([
4636
4520
  "BUILDING",
@@ -4646,6 +4530,8 @@ const storefrontVersionSchema = z.object({
4646
4530
  published: z.boolean().describe("지금 발행된 버전인가"),
4647
4531
  publishedAt: z.string().nullable().describe("마지막으로 발행한 시각 (ISO 8601). 발행한 적 없으면 null"),
4648
4532
  message: z.string().nullable().describe("버전 메모"),
4533
+ source: z.enum(["TEMPLATE", "EDIT"]).describe("`TEMPLATE` 템플릿 산출물 그대로 · `EDIT` 편집 작업 트리를 빌드함"),
4534
+ buildId: z.string().nullable().describe("편집 버전의 빌드 (`GET /v1/storefront/site/builds/{buildId}`). 템플릿 버전은 null"),
4649
4535
  createdByType: z.string().describe("만든 행위자 유형 (`USER`·`AGENT`·`APP`·`TOKEN`·`SYSTEM`). 늘어날 수 있다"),
4650
4536
  createdAt: z.string().describe("만든 시각 (ISO 8601)")
4651
4537
  });
@@ -4676,6 +4562,19 @@ const storefrontProvisioningSchema = z.object({
4676
4562
  lastErrorCode: z.string().nullable().describe("마지막 반영 실패 코드. 없으면 null"),
4677
4563
  syncedAt: z.string().nullable().describe("마지막으로 반영한 시각 (ISO 8601)")
4678
4564
  });
4565
+ /**
4566
+ * 편집 미리보기 주소(M2b) — 사이트마다 `{사이트 id 정규화}-preview.{호스팅 도메인}` 호스트 하나다. 호스팅 도메인의 와일드카드
4567
+ * 인증서가 덮으므로 따로 준비할 것이 없다. 이 주소는 편집 미리보기 링크(콘솔 전용)로 받은 쿠키가 있어야 열린다.
4568
+ */
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
+ });
4679
4578
  const storefrontSiteSchema = z.object({
4680
4579
  siteId: z.string(),
4681
4580
  slug: z.string().describe("주소 슬러그"),
@@ -4688,7 +4587,9 @@ const storefrontSiteSchema = z.object({
4688
4587
  visibility: storefrontVisibilitySchema,
4689
4588
  publishedVersion: storefrontVersionSchema.nullable().describe("지금 발행된 버전"),
4690
4589
  provisioning: storefrontProvisioningSchema,
4590
+ editPreview: storefrontEditPreviewSchema,
4691
4591
  slugChangesRemaining: z.number().int().min(0).describe(`최근 30일 기준 남은 슬러그 변경 횟수`),
4592
+ suspension: storefrontSuspensionSchema.nullable().optional().describe("정지 정보. `status`가 `SUSPENDED`일 때만 있고 그 밖은 null"),
4692
4593
  createdAt: z.string(),
4693
4594
  updatedAt: z.string()
4694
4595
  });
@@ -4696,6 +4597,7 @@ const storefrontSiteSchema = z.object({
4696
4597
  const storefrontHostingSchema = z.object({
4697
4598
  enabled: z.boolean().describe("새 사이트를 만들 수 있는가 (플랫폼 스위치)"),
4698
4599
  domain: z.string().describe("호스팅 도메인 (예: `sayren.co`). 사이트 주소는 `{slug}.{domain}`"),
4600
+ editingEnabled: z.boolean().describe("파일 편집·버전 만들기를 쓸 수 있는가 (플랫폼 편집 스위치와 Sandbox 설정)"),
4699
4601
  site: storefrontSiteSchema.nullable()
4700
4602
  });
4701
4603
  const storefrontSlugAvailabilitySchema = z.object({
@@ -4745,6 +4647,330 @@ const storefrontPreviewLinkSchema = z.object({
4745
4647
  url: z.string().describe("미리보기 링크 — 5분 안에 연다"),
4746
4648
  expiresAt: z.string().describe("링크 만료 시각 (ISO 8601)")
4747
4649
  });
4650
+ /** 플랫폼 전체 동시 편집 환경 상한 */
4651
+ const STOREFRONT_EDIT_CAPACITY = 20;
4652
+ /** 활동이 없으면 편집 환경이 잠드는 시간(분). 작업 트리는 보존된다 */
4653
+ const STOREFRONT_EDIT_IDLE_MINUTES = 15;
4654
+ /**
4655
+ * 편집 작업(파일 쓰기·검사·버전 만들기·편집 미리보기 링크 발급)이 없으면 서버가 편집 환경을 닫는 시간(분). 미리보기 탭만 열어 두면
4656
+ * 유휴 잠들기(`STOREFRONT_EDIT_IDLE_MINUTES`)가 걸리지 않으므로 이 한도가 편집 환경을 끝낸다. 작업 트리는 보존된다
4657
+ */
4658
+ const STOREFRONT_EDIT_ACTIVITY_TIMEOUT_MINUTES = 30;
4659
+ /** 파일 하나의 최대 크기(바이트) */
4660
+ const STOREFRONT_FILE_MAX_BYTES = 1048576;
4661
+ /** 작업 트리 전체 최대 크기(바이트) — 파일 수는 2,000개까지 */
4662
+ const STOREFRONT_WORKSPACE_MAX_BYTES = 20971520;
4663
+ /** 일괄 쓰기 한 번의 최대 파일 수 */
4664
+ const STOREFRONT_FILE_BATCH_MAX = 50;
4665
+ /** 편집할 수 없는 경로 조각 — 설치물·산출물 폴더 */
4666
+ const STOREFRONT_BLOCKED_PATH_SEGMENTS = [
4667
+ "node_modules",
4668
+ "dist",
4669
+ ".output",
4670
+ ".tanstack",
4671
+ ".wrangler",
4672
+ ".git",
4673
+ ".nitro",
4674
+ ".vinxi"
4675
+ ];
4676
+ /** 경로 조각에 쓸 수 있는 문자 — 편집 Sandbox Worker의 경로 규칙(sandbox-api.md §2.1)과 같다. TanStack Router 파일명(`$id.tsx`·`_layout`·`(group)`·`[.]`)이 들어간다 */
4677
+ const STOREFRONT_PATH_SEGMENT_PATTERN = /^[A-Za-z0-9$_.()[\]@+=,-]+$/;
4678
+ /**
4679
+ * 보이지 않거나 표시 방향을 바꾸는 문자 — 제로폭(U+200B~U+200D, U+FEFF)과 bidi 제어(U+202A~U+202E, U+2066~U+2069). 비슷해 보이는
4680
+ * 경로로 규칙 검사를 피하거나 리뷰어를 속이는 데 쓰인다. 문자 규칙이 이미 막지만 이유를 따로 드러내려고 먼저 본다.
4681
+ */
4682
+ const INVISIBLE_OR_BIDI = /[\u200B-\u200D\uFEFF\u202A-\u202E\u2066-\u2069]/;
4683
+ /** 경로 정규화 — NFC. 비교·저장은 이 값으로 한다(같아 보이는 두 경로가 다른 파일이 되지 않게) */
4684
+ function normalizeStorefrontPath(path) {
4685
+ return path.normalize("NFC");
4686
+ }
4687
+ /**
4688
+ * 작업 트리 상대 경로인가(정규화한 값 기준) — `/`로 구분하고 `/`로 시작하지 않는다. `.`·`..`·빈 조각, 조각 문자 규칙
4689
+ * (`STOREFRONT_PATH_SEGMENT_PATTERN`, 공백·역슬래시·제어·제로폭·bidi 문자 불가) 밖, 설치물·산출물 폴더
4690
+ * (`STOREFRONT_BLOCKED_PATH_SEGMENTS`), `.env`로 시작하는 조각(파일·폴더)은 받지 않는다. 최대 256자·깊이 16.
4691
+ * 어디에 쓸 수 있는지(`src/**`·`public/**`·루트 설정 파일)는 편집 Sandbox가 판정한다(400 `INVALID_FILE_PATH`).
4692
+ * 편집 Sandbox Worker가 같은 규칙의 복제본을 들고 있다(api `test/storefront-hosting-parity.spec.ts`가 대조한다).
4693
+ */
4694
+ function isEditableStorefrontPath(path) {
4695
+ if (path.length === 0 || path.length > 256) return false;
4696
+ if (INVISIBLE_OR_BIDI.test(path)) return false;
4697
+ if (path !== normalizeStorefrontPath(path)) return false;
4698
+ if (path.startsWith("/")) return false;
4699
+ const segments = path.split("/");
4700
+ if (segments.length > 16) return false;
4701
+ for (const segment of segments) {
4702
+ if (segment === "" || segment === "." || segment === "..") return false;
4703
+ if (!STOREFRONT_PATH_SEGMENT_PATTERN.test(segment)) return false;
4704
+ if (STOREFRONT_BLOCKED_PATH_SEGMENTS.includes(segment)) return false;
4705
+ if (segment.startsWith(".env")) return false;
4706
+ }
4707
+ return true;
4708
+ }
4709
+ /** 경로 입력 — NFC로 정규화한 뒤 규칙을 본다 */
4710
+ const storefrontFilePathSchema = z.string().transform(normalizeStorefrontPath).refine(isEditableStorefrontPath, "편집할 수 없는 경로입니다").describe("작업 트리 상대 경로 (예: `src/routes/index.tsx`). NFC로 정규화한다");
4711
+ /** 파일 해시 — 내용 바이트의 SHA-256 hex(소문자 64자) */
4712
+ const storefrontFileHashSchema = z.string().regex(/^[0-9a-f]{64}$/, "SHA-256 hex여야 합니다").describe("내용 바이트의 SHA-256 (소문자 hex)");
4713
+ const storefrontFileEncodingSchema = z.enum(["utf8", "base64"]).describe("`utf8` 텍스트 · `base64` 바이너리(이미지 등)");
4714
+ /**
4715
+ * 편집 환경 상태 — `STARTING`은 컨테이너를 띄우고 작업 트리를 복원해 dev 서버를 올리는 중이다(보통 30초 안, 최대
4716
+ * 90초). `RUNNING`이면 미리보기와 파일 쓰기가 바로 반영된다(HMR).
4717
+ */
4718
+ const storefrontEditSessionStatusSchema = z.enum(["STARTING", "RUNNING"]).describe("`STARTING` 띄우는 중 · `RUNNING` 사용 중");
4719
+ /** 편집 환경을 띄우는 단계 — 콘솔 진행 표시용이다. 값은 늘어날 수 있다 */
4720
+ const STOREFRONT_EDIT_START_STAGES = [
4721
+ "CONTAINER",
4722
+ "RESTORE",
4723
+ "DEV_SERVER"
4724
+ ];
4725
+ const storefrontEditSessionSchema = z.object({
4726
+ sessionId: z.string(),
4727
+ status: storefrontEditSessionStatusSchema,
4728
+ startStage: z.string().nullable().describe("`STARTING`일 때 지금 단계 (`CONTAINER` 컨테이너 기동 · `RESTORE` 작업 트리 복원 · `DEV_SERVER` 개발 서버 기동). 늘어날 수 있다. `RUNNING`이면 null"),
4729
+ startedAt: z.string().describe("연 시각 (ISO 8601)"),
4730
+ lastEditedAt: z.string().nullable().describe("이 편집 환경에서 마지막으로 파일을 바꾼 시각. 없으면 null"),
4731
+ lastActiveAt: z.string().describe("마지막 활동 시각 (파일 도구·검사·미리보기)"),
4732
+ idleExpiresAt: z.string().describe(`이 시각까지 활동이 없으면 잠든다 (마지막 활동 + 15분). 잠들어도 작업 트리는 보존된다`),
4733
+ closesAt: z.string().describe(`이 시각까지 편집 작업(파일 쓰기·검사·버전 만들기·미리보기 링크 발급)이 없으면 서버가 편집 환경을 닫는다 (마지막 편집 작업 + 30분). 미리보기만 보고 있어도 닫힌다. 작업 트리는 보존된다`)
4734
+ });
4735
+ /** `GET·POST·DELETE /v1/storefront/site/editing` — 편집 환경 상태 */
4736
+ const storefrontEditingSchema = z.object({
4737
+ enabled: z.boolean().describe("편집을 쓸 수 있는가 (플랫폼 스위치와 Sandbox 설정)"),
4738
+ session: storefrontEditSessionSchema.nullable().describe("열린 편집 환경. 잠들었거나 닫았으면 null"),
4739
+ capacity: z.object({
4740
+ limit: z.number().int().describe("플랫폼 전체 동시 편집 환경 상한"),
4741
+ inUse: z.number().int().describe("지금 열린 편집 환경 수")
4742
+ }),
4743
+ preview: storefrontEditPreviewSchema.describe("편집 미리보기 주소 (사이트의 `editPreview`와 같다)"),
4744
+ workingTree: z.object({
4745
+ modified: z.boolean().describe("템플릿에서 한 번이라도 고쳤는가 (사이트 `sourceMode`가 `CUSTOM`)"),
4746
+ lastEditedAt: z.string().nullable().describe("마지막으로 파일을 바꾼 시각")
4747
+ })
4748
+ });
4749
+ const storefrontFileEntrySchema = z.object({
4750
+ path: z.string(),
4751
+ size: z.number().int().min(0).describe("바이트 수"),
4752
+ hash: storefrontFileHashSchema
4753
+ });
4754
+ /** `GET /v1/storefront/site/files?prefix=` — 작업 트리 파일 목록(편집 환경이 없어도 된다. 편집 환경을 깨우지 않는다) */
4755
+ const storefrontFileListSchema = z.object({
4756
+ files: z.array(storefrontFileEntrySchema).describe("경로 사전순"),
4757
+ totalBytes: z.number().int().min(0).describe("작업 트리 전체 바이트 수")
4758
+ });
4759
+ const storefrontFileListQuerySchema = z.object({ prefix: z.string().max(256).optional().describe("이 접두사로 시작하는 경로만 (예: `src/routes/`)") });
4760
+ const storefrontFileContentQuerySchema = z.object({ path: z.string().describe("읽을 파일 경로") });
4761
+ /** `GET /v1/storefront/site/files/content?path=` — 편집 환경이 없어도 된다 */
4762
+ const storefrontFileContentSchema = z.object({
4763
+ path: z.string(),
4764
+ content: z.string().describe("내용 (`encoding`에 따라 텍스트 또는 base64)"),
4765
+ encoding: storefrontFileEncodingSchema,
4766
+ hash: storefrontFileHashSchema,
4767
+ size: z.number().int().min(0)
4768
+ });
4769
+ /**
4770
+ * `POST /v1/storefront/site/files/batch` — 여러 파일을 한 번에 쓴다. 편집 환경이 없어도 된다(잠든 작업 트리에 바로 쓰고,
4771
+ * 편집 환경이 떠 있으면 미리보기에 곧바로 반영된다). `expectedHash`: 생략하면 조건 없이 쓴다 · `null`이면 파일이 없어야 한다(새 파일) · 값이면 지금
4772
+ * 해시가 같아야 한다. 조건을 **모두** 확인한 뒤 쓰고, 하나라도 어긋나면 아무것도 쓰지 않는다(409 `FILE_CHANGED`,
4773
+ * `error.details.conflicts`).
4774
+ */
4775
+ const writeStorefrontFilesRequestSchema = z.object({ files: z.array(z.object({
4776
+ path: storefrontFilePathSchema,
4777
+ content: z.string().describe("내용 (`encoding`에 따라 텍스트 또는 base64)"),
4778
+ encoding: storefrontFileEncodingSchema.optional().describe("기본 `utf8`"),
4779
+ expectedHash: storefrontFileHashSchema.nullable().optional().describe("쓰기 조건. 생략 = 조건 없음 · null = 새 파일 · 값 = 지금 해시가 같을 때만")
4780
+ })).min(1).max(50).refine((files) => new Set(files.map((file) => file.path)).size === files.length, { message: "같은 경로를 두 번 실을 수 없습니다" }) });
4781
+ const storefrontFileWriteResultSchema = z.object({ files: z.array(storefrontFileEntrySchema).describe("쓴 파일의 새 해시·크기 (요청 순서)") });
4782
+ /** 409 `FILE_CHANGED`의 `error.details` */
4783
+ const storefrontFileConflictDetailsSchema = z.object({ conflicts: z.array(z.object({
4784
+ path: z.string(),
4785
+ currentHash: storefrontFileHashSchema.nullable().describe("지금 해시. 파일이 없으면 null")
4786
+ })) });
4787
+ /** `POST /v1/storefront/site/files/delete` — 없는 파일이면 `deleted: false`(조건을 주면 409 `FILE_CHANGED`) */
4788
+ const deleteStorefrontFileRequestSchema = z.object({
4789
+ path: storefrontFilePathSchema,
4790
+ expectedHash: storefrontFileHashSchema.optional().describe("주면 지금 해시가 같을 때만 지운다")
4791
+ });
4792
+ const storefrontFileDeleteResultSchema = z.object({
4793
+ path: z.string(),
4794
+ deleted: z.boolean()
4795
+ });
4796
+ /** `POST /v1/storefront/site/files/move` — 이름 바꾸기·옮기기 */
4797
+ const moveStorefrontFileRequestSchema = z.object({
4798
+ from: storefrontFilePathSchema,
4799
+ to: storefrontFilePathSchema,
4800
+ expectedHash: storefrontFileHashSchema.optional().describe("주면 `from`의 지금 해시가 같을 때만"),
4801
+ overwrite: z.boolean().optional().describe("`to`에 파일이 있으면 덮는가 (기본 false → 409 `FILE_EXISTS`)")
4802
+ });
4803
+ const storefrontFileMoveResultSchema = z.object({
4804
+ from: z.string(),
4805
+ to: z.string(),
4806
+ hash: storefrontFileHashSchema,
4807
+ size: z.number().int().min(0)
4808
+ });
4809
+ /** `POST /v1/storefront/site/files/search` — 텍스트 파일 내용 검색(편집 환경이 없어도 된다) */
4810
+ /** 정규식 검색의 패턴 길이 상한 — 편집 Sandbox가 실행 비용을 막으려고 더 좁게 받는다(sandbox-api §4.6) */
4811
+ const STOREFRONT_SEARCH_REGEX_MAX_LENGTH = 100;
4812
+ const searchStorefrontFilesRequestSchema = z.object({
4813
+ query: z.string().min(1).max(200),
4814
+ regex: z.boolean().optional().describe("`query`를 정규식으로 본다 (기본 false = 글자 그대로). 정규식은 100자까지이고 중첩 수량자(`(a+)+`)·반복되는 선택(`(a|b)+`)·역참조·뒤 보기, 가변 수량자(`*`·`+`·`?`·`{n,m}`, 넓은 범위는 2로 센다) 차수 4 초과는 400 `INVALID_REQUEST`다. 차수가 클수록 긴 줄은 건너뛰고 비용이 크면 `truncated`로 끊는다"),
4815
+ caseSensitive: z.boolean().optional().describe("대소문자를 가리는가 (기본 false)"),
4816
+ pathPrefix: z.string().max(256).optional(),
4817
+ maxResults: z.number().int().min(1).max(200).optional().describe("기본 50")
4818
+ }).refine((body) => !body.regex || body.query.length <= 100, {
4819
+ message: `정규식은 100자까지입니다`,
4820
+ path: ["query"]
4821
+ });
4822
+ const storefrontFileSearchResultSchema = z.object({
4823
+ matches: z.array(z.object({
4824
+ path: z.string(),
4825
+ line: z.number().int().min(1),
4826
+ column: z.number().int().min(1).optional(),
4827
+ text: z.string().describe("일치한 줄 (일치 앞뒤 200자까지)")
4828
+ })),
4829
+ truncated: z.boolean().describe("`maxResults`에서 끊었는가")
4830
+ });
4831
+ /**
4832
+ * `POST /v1/storefront/site/checks` (202) — 작업 트리에 명령 하나를 돌린다(`typecheck` 타입 검사 · `lint` · `test`). 편집 환경이
4833
+ * 없어도 된다(일회용 검사 컨테이너에서 돈다). 검사는 30초를 넘길 수 있어 **항상 202로 검사 id만 주고** 결과는
4834
+ * `GET /v1/storefront/site/checks/{checkId}`로 조회한다(권장 2~3초 간격). 사이트당 끝나지 않은 검사는 하나다 — 같은 종류가 돌고
4835
+ * 있으면 그 검사를 다시 돌려주고, 다른 종류면 409 `CHECK_RUNNING`(`error.details.checkId`)이다. 출력은 꼬리만 준다.
4836
+ */
4837
+ const runStorefrontCheckRequestSchema = z.object({ check: z.enum([
4838
+ "typecheck",
4839
+ "lint",
4840
+ "test"
4841
+ ]) });
4842
+ const storefrontCheckStatusSchema = z.enum([
4843
+ "QUEUED",
4844
+ "RUNNING",
4845
+ "PASSED",
4846
+ "FAILED",
4847
+ "TIMED_OUT",
4848
+ "UNKNOWN"
4849
+ ]).describe("`QUEUED` 대기 · `RUNNING` 실행 중 · `PASSED` 통과 · `FAILED` 실패(명령 실패 또는 `errorCode`) · `TIMED_OUT` 마감(150초)에 걸려 끊김 · `UNKNOWN` 결과를 모른다(다시 실행하지 않는다, 새 검사를 요청한다)");
4850
+ /** 끝난 검사 상태 */
4851
+ const STOREFRONT_CHECK_TERMINAL_STATUSES = [
4852
+ "PASSED",
4853
+ "FAILED",
4854
+ "TIMED_OUT",
4855
+ "UNKNOWN"
4856
+ ];
4857
+ const storefrontCheckSchema = z.object({
4858
+ checkId: z.string(),
4859
+ check: z.string().describe("`typecheck`·`lint`·`test`"),
4860
+ status: storefrontCheckStatusSchema,
4861
+ exitCode: z.number().int().nullable().describe("명령 종료 코드. 명령이 끝나지 않았으면 null"),
4862
+ durationMs: z.number().int().min(0).nullable().describe("명령 시간. 끝나지 않았으면 null"),
4863
+ outputTail: z.string().nullable().describe("출력 꼬리 (최대 16KiB). 끝나지 않았으면 null"),
4864
+ errorCode: z.string().nullable().describe("명령 밖의 실패 이유 (`CHECK_BUSY`·`SANDBOX_UNAVAILABLE`·`RESULT_UNKNOWN`, 늘어날 수 있다). 명령 결과로 끝났으면 null"),
4865
+ message: z.string().nullable().describe("`errorCode`가 있을 때 사용자용 안내"),
4866
+ createdAt: z.string(),
4867
+ startedAt: z.string().nullable(),
4868
+ finishedAt: z.string().nullable()
4869
+ });
4870
+ /** 규칙 심각도 — `error`는 버전 발행을 막는다(빌드 `VERIFY` 실패), `warning`은 알리기만 한다. 없으면 `error`로 본다 */
4871
+ const storefrontRuleSeveritySchema = z.enum(["error", "warning"]);
4872
+ /** 규칙 하나의 검사 결과 — MCP 계측(`mcpRuleResultSchema`)과 같은 모양에 심각도를 더한다 */
4873
+ const storefrontRuleResultSchema = z.object({
4874
+ ruleId: z.string(),
4875
+ status: z.enum(["pass", "violation"]),
4876
+ count: z.number().int().min(0),
4877
+ severity: storefrontRuleSeveritySchema.optional()
4878
+ });
4879
+ /**
4880
+ * 규칙 검증 보고서 — `@sayren/mcp`의 `verify_storefront`와 같은 모양이다(원천 `apps/mcp/src/verify.ts`). 위반마다
4881
+ * 고칠 자리(`file`·`line`)와 방법(`fix`·`example`)을 싣는다. `POST /v1/storefront/site/verify`(편집 환경이 잠들어 있어도
4882
+ * 된다)와 빌드의 `verify`가 이 모양이다.
4883
+ */
4884
+ const storefrontVerifyReportSchema = z.object({
4885
+ ok: z.boolean().describe("차단(`error`) 위반이 없는가"),
4886
+ checkedFiles: z.number().int().min(0),
4887
+ summary: z.object({
4888
+ rules: z.number().int(),
4889
+ passed: z.number().int(),
4890
+ violated: z.number().int(),
4891
+ findings: z.number().int()
4892
+ }),
4893
+ results: z.array(storefrontRuleResultSchema),
4894
+ findings: z.array(z.object({
4895
+ ruleId: z.string(),
4896
+ rule: z.string(),
4897
+ file: z.string(),
4898
+ line: z.number().int().optional(),
4899
+ detail: z.string(),
4900
+ cause: z.string(),
4901
+ fix: z.string(),
4902
+ example: z.string(),
4903
+ doc: z.string(),
4904
+ docUrl: z.string(),
4905
+ templateFile: z.string().optional(),
4906
+ severity: storefrontRuleSeveritySchema.optional()
4907
+ })),
4908
+ message: z.string(),
4909
+ nextSteps: z.array(z.string())
4910
+ });
4911
+ const storefrontBuildStatusSchema = z.enum([
4912
+ "QUEUED",
4913
+ "RUNNING",
4914
+ "SUCCEEDED",
4915
+ "FAILED",
4916
+ "TIMED_OUT"
4917
+ ]).describe("`QUEUED` 대기 · `RUNNING` 빌드 중 · `SUCCEEDED` 성공(버전 `READY`) · `FAILED` 실패 · `TIMED_OUT` 시간 초과(10분)");
4918
+ /** 끝난 빌드 상태 */
4919
+ const STOREFRONT_BUILD_TERMINAL_STATUSES = [
4920
+ "SUCCEEDED",
4921
+ "FAILED",
4922
+ "TIMED_OUT"
4923
+ ];
4924
+ /**
4925
+ * 빌드 단계 — 이 순서로 돈다. 첫 실패에서 멈추지만 규칙 검증(`VERIFY`)은 실패해도 타입 검사까지 이어서 해 한 번에 알려 준다.
4926
+ * `UPLOAD`는 사이트 버전 Worker 업로드다(성공하면 발행할 수 있다). 값은 늘어날 수 있다.
4927
+ */
4928
+ const STOREFRONT_BUILD_STAGES = [
4929
+ "VERIFY",
4930
+ "RESTORE",
4931
+ "INSTALL",
4932
+ "TYPECHECK",
4933
+ "BUILD",
4934
+ "BUNDLE",
4935
+ "PACK",
4936
+ "UPLOAD"
4937
+ ];
4938
+ const storefrontBuildSchema = z.object({
4939
+ buildId: z.string(),
4940
+ versionId: z.string(),
4941
+ versionNumber: z.number().int().min(1),
4942
+ status: storefrontBuildStatusSchema,
4943
+ failureStage: z.string().nullable().describe("실패한 단계 (`VERIFY`·`RESTORE`·`INSTALL`·`TYPECHECK`·`BUILD`·`BUNDLE`·`PACK`·`UPLOAD`, 늘어날 수 있다). 실패가 아니면 null"),
4944
+ errorCode: z.string().nullable().describe("실패 코드 (`RULES_VIOLATED`·`INSTALL_FAILED`·`TYPECHECK_FAILED`·`BUILD_FAILED`·`BUNDLE_FAILED`·`ARTIFACT_INVALID`·`UPLOAD_FAILED`·`SOURCE_NOT_FOUND`·`CAPACITY_EXHAUSTED`·`INTERNAL`, 늘어날 수 있다). 실패가 아니면 null"),
4945
+ logTail: z.string().nullable().describe("실패 단계 출력 꼬리 (최대 8KB). 없으면 null"),
4946
+ stages: z.array(z.object({
4947
+ stage: z.string(),
4948
+ ok: z.boolean(),
4949
+ durationMs: z.number().int().min(0)
4950
+ })).describe("끝난 단계 (도는 순서)"),
4951
+ verify: storefrontVerifyReportSchema.nullable().describe("규칙 검증 보고서(위반 상세는 50건까지). 그 단계까지 가지 못했으면 null"),
4952
+ queuedAt: z.string(),
4953
+ startedAt: z.string().nullable(),
4954
+ finishedAt: z.string().nullable(),
4955
+ durationMs: z.number().int().nullable().describe("빌드 시간 (시작 → 끝)")
4956
+ });
4957
+ const storefrontBuildListQuerySchema = z.object({ limit: z.coerce.number().int().min(1).max(50).optional().describe("기본 20, 최신 먼저") });
4958
+ /**
4959
+ * `POST /v1/storefront/site/versions` — 지금 작업 트리를 고정해 버전을 만든다(빌드 시작). 사이트당 끝나지 않은 빌드는
4960
+ * 하나다(409 `STOREFRONT_BUILD_IN_PROGRESS`). 편집 환경이 없어도 된다.
4961
+ */
4962
+ const createStorefrontVersionRequestSchema = z.object({ message: z.string().max(200).optional().describe("버전 메모 (에이전트는 바꾼 내용 요약)") });
4963
+ /** 버전 만들기 응답 (201) — 버전은 `BUILDING`, 빌드는 `QUEUED`로 시작한다. 결과는 `GET builds/{buildId}`로 본다 */
4964
+ const storefrontVersionCreatedSchema = z.object({
4965
+ version: storefrontVersionSchema,
4966
+ build: storefrontBuildSchema
4967
+ });
4968
+ /**
4969
+ * `POST /v1/storefront/site/editing/preview-link` — 편집 미리보기(dev 서버) 링크. 콘솔에서 로그인한 OWNER·ADMIN만
4970
+ * 받는다(PAT·에이전트 불가). 5분 안에 열면 그 브라우저에 미리보기 호스트 전용 쿠키(12시간)가 생긴다. 편집 환경이
4971
+ * 없으면 409 `EDIT_SESSION_REQUIRED`다(편집 환경이 `STARTING`이면 링크는 주고, 미리보기 화면이 기동 대기를 보여 준다).
4972
+ */
4973
+ const createStorefrontEditPreviewLinkRequestSchema = createStorefrontPreviewLinkRequestSchema;
4748
4974
  //#endregion
4749
4975
  //#region src/client.ts
4750
4976
  function formBody(fields) {
@@ -4895,10 +5121,7 @@ function createStoreClient(options) {
4895
5121
  }) });
4896
5122
  const put = await fetchFn(ticket.uploadUrl, {
4897
5123
  method: "PUT",
4898
- headers: {
4899
- ...ticket.headers,
4900
- "content-type": file.type || "application/octet-stream"
4901
- },
5124
+ headers: { "content-type": file.type || "application/octet-stream" },
4902
5125
  body: file
4903
5126
  });
4904
5127
  if (!put.ok) throw new ApiError(put.status, "UPLOAD_PUT_FAILED", `업로드에 실패했습니다: ${filename}`);
@@ -4933,37 +5156,23 @@ function createStoreClient(options) {
4933
5156
  updateVariantStock: (productId, variantId, body) => storeHttp.request("PATCH", path`/products/${productId}/variants/${variantId}/stock`, variantStockSchema, { body: variantStockRequestSchema.parse(body) }),
4934
5157
  getDiscount: (productId) => storeHttp.request("GET", path`/products/${productId}/discount`, discountSchema),
4935
5158
  setDiscount: (productId, body) => storeHttp.request("PUT", path`/products/${productId}/discount`, discountSchema, { body: discountRequestSchema.parse(body) }),
4936
- removeDiscount: (productId) => storeHttp.requestVoid("DELETE", path`/products/${productId}/discount`),
4937
- /**
4938
- * 코드 일괄 업로드 (이슈 #48) — 코드 발급·자동 제공 상품의 코드 풀에 올린다. `codes`와 `csv` 중 하나만 보낸다.
4939
- * 받은 코드 수만큼 재고가 는다. 에러: 409 `NOT_CODE_PRODUCT`·`CODE_POOL_NOT_USED`·`CODE_UPLOAD_CONFLICT`, 400 `TOO_MANY_CODES`
4940
- */
4941
- uploadCodes: (productId, body, idempotencyKey) => storeHttp.request("POST", path`/products/${productId}/codes`, fulfillmentCodeBatchSchema, {
4942
- body: fulfillmentCodeUploadRequestSchema.parse(body),
4943
- headers: idempotency(idempotencyKey)
4944
- }),
4945
- /** 코드 목록 — 가린 값(`hint`)만 싣는다. `available`은 필터와 무관한 판매 가능 코드 수다 */
4946
- listCodes: (productId, params) => storeHttp.request("GET", path`/products/${productId}/codes`, fulfillmentCodePageSchema, { query: params }),
4947
- /** 판매 가능 코드 폐기 — 재고가 1 준다. 배정된 코드는 409 `CODE_NOT_AVAILABLE` */
4948
- voidCode: (productId, codeId) => storeHttp.request("POST", path`/products/${productId}/codes/${codeId}/void`, fulfillmentCodeSchema),
4949
- /**
4950
- * 판매 파일 연결 (이슈 #48) — `uploads.create({ purpose: "digital-asset" })`로 받은 `key`에 파일을 올린 뒤 연결한다.
4951
- * 이후 주문부터 이 파일을 준다. 에러: 400 `INVALID_ASSET_KEY`, 409 `NOT_DOWNLOAD_PRODUCT`·`ASSET_ALREADY_ATTACHED`
4952
- */
4953
- attachDigitalAsset: (productId, body) => storeHttp.request("POST", path`/products/${productId}/digital-assets`, digitalAssetSchema, { body: digitalAssetAttachRequestSchema.parse(body) }),
4954
- /** 판매 파일 목록 — 해제한 파일도 싣는다. 객체 키·주소는 없다 */
4955
- listDigitalAssets: (productId) => storeHttp.request("GET", path`/products/${productId}/digital-assets`, z.array(digitalAssetSchema)),
4956
- /** 판매 파일 해제 — 새 주문에 주지 않는다. 이미 산 구매자는 계속 받는다 */
4957
- removeDigitalAsset: (productId, assetId) => storeHttp.request("DELETE", path`/products/${productId}/digital-assets/${assetId}`, digitalAssetSchema),
4958
- /** 판매 파일 확인 주소 — 수명 300초의 서명 주소 */
4959
- previewDigitalAsset: (productId, assetId) => storeHttp.request("POST", path`/products/${productId}/digital-assets/${assetId}/preview-url`, digitalAssetPreviewSchema)
5159
+ removeDiscount: (productId) => storeHttp.requestVoid("DELETE", path`/products/${productId}/discount`)
4960
5160
  },
4961
- fulfillmentCodes: {
4962
5161
  /**
4963
- * 코드 평문 열람 — 콘솔에서 로그인한 OWNER·ADMIN만 된다(403 `INSUFFICIENT_ROLE`). 열람 기록과 감사 로그가 남는다.
4964
- * 평문은 화면에 보인 뒤 저장하지 않는다
5162
+ * 저장 옵션 (이슈 #59) — 자주 쓰는 옵션 구성. 불러오기는 복사라 저장 옵션을 고쳐도 불러온 상품은 바뀌지 않는다.
5163
+ * 권한은 상품과 같다(`product:r`·`product:rw`).
4965
5164
  */
4966
- reveal: (codeId) => storeHttp.request("POST", path`/fulfillment-codes/${codeId}/reveal`, fulfillmentCodeRevealSchema) },
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
+ },
4967
5176
  categories: {
4968
5177
  list: (params) => storeHttp.request("GET", "/categories", z.array(categorySchema), { query: params }),
4969
5178
  tree: () => storeHttp.request("GET", "/categories", z.array(categoryTreeNodeSchema), { query: { tree: true } }),
@@ -5030,12 +5239,7 @@ reveal: (codeId) => storeHttp.request("POST", path`/fulfillment-codes/${codeId}/
5030
5239
  headers: idempotency(idempotencyKey)
5031
5240
  }),
5032
5241
  /**
5033
- * 제공 안내 메일 재발송 (이슈 #48) — 제공된 주문상품의 안내 메일을 다시 보낸다. 주문상품당 하루 3회(429).
5034
- * 코드·파일 주소는 메일에 없다
5035
- */
5036
- resendFulfillmentMail: (orderItemId) => storeHttp.request("POST", path`/order-items/${orderItemId}/fulfillment/mail`, fulfillmentMailResendSchema),
5037
- /**
5038
- * 제공 처리 (이슈 #48) — 수동 제공 비실물 주문상품(이용권·방문 수령)을 `PAID`·`CONFIRMED`에서 제공 완료(`DELIVERED`)로
5242
+ * 제공 처리 (이슈 #48) — 배송 없는 주문상품(`MANUAL`)을 `PAID`·`CONFIRMED`에서 제공 완료(`DELIVERED`)로
5039
5243
  * 바꾼다. `note`는 구매자 주문 상세에 이용 안내로 실린다. 배송 상품은 409 `SHIPPING_REQUIRED`
5040
5244
  */
5041
5245
  fulfill: (orderItemId, body = {}, idempotencyKey) => storeHttp.request("POST", path`/order-items/${orderItemId}/fulfill`, orderItemSchema, {
@@ -5380,7 +5584,54 @@ list: (params) => storeHttp.request("GET", "/audit-logs", auditLogPageSchema, {
5380
5584
  */
5381
5585
  createPreviewLink: (body = {}) => storeHttp.request("POST", "/storefront/site/preview-link", storefrontPreviewLinkSchema, { body: createStorefrontPreviewLinkRequestSchema.parse(body) }),
5382
5586
  /** `POST /v1/storefront/site/disable` — 호스팅을 끈다(사이트 `CLOSED`, 슬러그 90일 보류). 다시 만들 수 있다 */
5383
- disable: (body) => storeHttp.request("POST", "/storefront/site/disable", storefrontSiteSchema, { body: disableStorefrontHostingRequestSchema.parse(body) })
5587
+ disable: (body) => storeHttp.request("POST", "/storefront/site/disable", storefrontSiteSchema, { body: disableStorefrontHostingRequestSchema.parse(body) }),
5588
+ /** `GET /v1/storefront/site/editing` — 편집 환경 상태(열린 환경·동시 상한·미리보기 인증서) */
5589
+ getEditing: () => storeHttp.request("GET", "/storefront/site/editing", storefrontEditingSchema),
5590
+ /**
5591
+ * `POST /v1/storefront/site/editing` — 편집 환경을 연다(이미 열려 있으면 그대로). 플랫폼 동시 상한에 걸리면 409
5592
+ * `EDIT_CAPACITY_FULL`(`details.retryAfterSeconds`). 띄우는 데 보통 30초가 걸린다(`session.status: STARTING`)
5593
+ */
5594
+ startEditing: () => storeHttp.request("POST", "/storefront/site/editing", storefrontEditingSchema, { body: {} }),
5595
+ /** `DELETE /v1/storefront/site/editing` — 편집 환경을 닫는다(작업 트리는 보존된다) */
5596
+ stopEditing: () => storeHttp.request("DELETE", "/storefront/site/editing", storefrontEditingSchema),
5597
+ /**
5598
+ * `POST /v1/storefront/site/editing/preview-link` — 편집 미리보기(dev 서버) 5분짜리 링크.
5599
+ * 콘솔에서 로그인한 OWNER·ADMIN만 받는다(에이전트 불가)
5600
+ */
5601
+ createEditPreviewLink: (body = {}) => storeHttp.request("POST", "/storefront/site/editing/preview-link", storefrontPreviewLinkSchema, { body: createStorefrontEditPreviewLinkRequestSchema.parse(body) }),
5602
+ /** `GET /v1/storefront/site/files?prefix=` — 작업 트리 파일 목록 */
5603
+ listFiles: (params = {}) => storeHttp.request("GET", "/storefront/site/files", storefrontFileListSchema, { query: params }),
5604
+ /** `GET /v1/storefront/site/files/content?path=` — 파일 하나 읽기 */
5605
+ readFile: (filePath) => storeHttp.request("GET", "/storefront/site/files/content", storefrontFileContentSchema, { query: { path: filePath } }),
5606
+ /**
5607
+ * `POST /v1/storefront/site/files/batch` — 여러 파일 쓰기(해시 조건). 조건이 하나라도 어긋나면 아무것도 쓰지 않고 409
5608
+ * `FILE_CHANGED`(`details.conflicts`). 편집 환경이 없으면 연다
5609
+ */
5610
+ writeFiles: (body) => storeHttp.request("POST", "/storefront/site/files/batch", storefrontFileWriteResultSchema, { body: writeStorefrontFilesRequestSchema.parse(body) }),
5611
+ /** `POST /v1/storefront/site/files/delete` */
5612
+ deleteFile: (body) => storeHttp.request("POST", "/storefront/site/files/delete", storefrontFileDeleteResultSchema, { body: deleteStorefrontFileRequestSchema.parse(body) }),
5613
+ /** `POST /v1/storefront/site/files/move` */
5614
+ moveFile: (body) => storeHttp.request("POST", "/storefront/site/files/move", storefrontFileMoveResultSchema, { body: moveStorefrontFileRequestSchema.parse(body) }),
5615
+ /** `POST /v1/storefront/site/files/search` — 내용 검색 */
5616
+ searchFiles: (body) => storeHttp.request("POST", "/storefront/site/files/search", storefrontFileSearchResultSchema, { body: searchStorefrontFilesRequestSchema.parse(body) }),
5617
+ /**
5618
+ * `POST /v1/storefront/site/checks` (202) — 타입 검사·lint·test 하나를 시작한다. 결과는 `getCheck`로 끝날 때까지 조회한다
5619
+ * (`STOREFRONT_CHECK_TERMINAL_STATUSES`)
5620
+ */
5621
+ runCheck: (body) => storeHttp.request("POST", "/storefront/site/checks", storefrontCheckSchema, { body: runStorefrontCheckRequestSchema.parse(body) }),
5622
+ /** `GET /v1/storefront/site/checks/{checkId}` — 검사 상태·종료 코드·출력 꼬리 */
5623
+ getCheck: (checkId) => storeHttp.request("GET", path`/storefront/site/checks/${checkId}`, storefrontCheckSchema),
5624
+ /** `POST /v1/storefront/site/verify` — 작업 트리 규칙 검증(`verify_storefront`와 같은 보고서) */
5625
+ verify: () => storeHttp.request("POST", "/storefront/site/verify", storefrontVerifyReportSchema, { body: {} }),
5626
+ /**
5627
+ * `POST /v1/storefront/site/versions` (201) — 지금 작업 트리로 버전을 만든다(빌드 시작). 버전은 `BUILDING`, 빌드가
5628
+ * 끝나면 `READY` 또는 `FAILED`다
5629
+ */
5630
+ createVersion: (body = {}) => storeHttp.request("POST", "/storefront/site/versions", storefrontVersionCreatedSchema, { body: createStorefrontVersionRequestSchema.parse(body) }),
5631
+ /** `GET /v1/storefront/site/builds?limit=` — 빌드 목록(최신 먼저) */
5632
+ listBuilds: (params = {}) => storeHttp.request("GET", "/storefront/site/builds", z.array(storefrontBuildSchema), { query: params }),
5633
+ /** `GET /v1/storefront/site/builds/{buildId}` — 빌드 상태·실패 단계·로그 꼬리·규칙 결과 */
5634
+ getBuild: (buildId) => storeHttp.request("GET", path`/storefront/site/builds/${buildId}`, storefrontBuildSchema)
5384
5635
  },
5385
5636
  store: {
5386
5637
  get: () => storeHttp.request("GET", "/store", storeSchema),
@@ -5621,20 +5872,24 @@ const orderWebhookEventDataSchema = z.object({
5621
5872
  status: orderItemStatusSchema.describe("이벤트 발생 시점의 주문 대표 상태"),
5622
5873
  testPayment: z.boolean().optional().default(false).describe("테스트 결제(샌드박스 결제) 주문인가. true면 실제로 돈이 오가지 않았다. 이 필드가 없는 이전 이벤트는 false로 읽는다"),
5623
5874
  couponDiscountAmount: z.number().int().optional().default(0).describe("주문이 받은 상품·주문 쿠폰 할인 합계 (원). 즉시할인(`discountAmount`)과 별개다. 쿠폰이 없거나 이 필드가 없는 이전 이벤트는 0으로 읽는다"),
5624
- orderItemId: z.string().optional().describe("이벤트를 만든 주문상품 ID. 비실물 상품 제공으로 보내는 `ORDER.DELIVERED`에만 실린다. 택배 배송완료와 그 밖의 주문 이벤트에는 없다"),
5625
- fulfillmentSnapshot: orderItemFulfillmentSnapshotResponseSchema.optional().describe("이벤트를 만든 주문상품의 주문 시점 이행 스냅샷(주문상품 응답의 `fulfillmentSnapshot`과 같다, 이슈 #57). 비실물 상품의 제공 완료는 `ORDER.DELIVERED`로도 오고 `type`이 `SHIPPING`이 아니다. `orderItemId`가 있는 이벤트에만 실린다")
5875
+ orderItemId: z.string().optional().describe("이벤트를 만든 주문상품 ID. 배송 없는 상품 제공으로 보내는 `ORDER.DELIVERED`에만 실린다. 택배 배송완료와 그 밖의 주문 이벤트에는 없다"),
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`에만 실리고 그런 주문상품이 없으면 없다")
5626
5882
  });
5627
5883
  /**
5628
- * `ORDER.FULFILLED` — 비실물 주문상품(파일·코드·이용권·방문 수령)을 제공했다(이슈 #48). 자동 제공은 결제 확정과 함께,
5629
- * 수동 제공은 셀러의 제공 처리(`POST /v1/order-items/{orderItemId}/fulfill`)로 온다. 기존 구독자를 위해 같은 순간
5630
- * `ORDER.DELIVERED`도 함께 보낸다. **코드·파일 주소는 싣지 않는다** — 관리 API로 조회한다.
5884
+ * `ORDER.FULFILLED` — 배송 없는 주문상품(`MANUAL`)을 제공했다(이슈 #48). 셀러의 제공 처리
5885
+ * (`POST /v1/order-items/{orderItemId}/fulfill`)로 온다. 기존 구독자를 위해 같은 순간 `ORDER.DELIVERED`도 함께 보낸다.
5631
5886
  */
5632
5887
  const orderFulfilledWebhookEventDataSchema = z.object({
5633
5888
  orderId: z.string().describe("주문 ID"),
5634
5889
  orderItemId: z.string().describe("제공한 주문상품 ID"),
5635
5890
  status: orderItemStatusSchema.describe("이벤트 발생 시점의 주문 대표 상태"),
5636
5891
  fulfillmentSnapshot: orderItemFulfillmentSnapshotResponseSchema.describe("제공한 주문상품의 주문 시점 이행 스냅샷(주문상품 응답의 `fulfillmentSnapshot`과 같다, 이슈 #57)"),
5637
- fulfillment: z.object({ mode: z.string().describe("실제 제공 방식. `AUTO`(결제 확정과 함께)·`MANUAL`(셀러 제공 처리). 주문상품 제공 기록의 `mode`와 같다") }).describe("제공 기록"),
5892
+ fulfillment: z.object({ mode: z.string() }).optional().describe("**Deprecated** — 항상 `{ mode: \"MANUAL\" }`이다. 자동 제공을 없앴고(이슈 #48) store-sdk 0.19 이하 호환으로만 싣는다. 게시된 연동이 store-sdk 0.20 이상으로 넘어간 뒤 다음 릴리스에서 뺀다"),
5638
5893
  fulfilledAt: z.string().describe("제공 시각 (ISO 8601). 주문상품의 `deliveredAt`과 같다"),
5639
5894
  testPayment: z.boolean().describe("테스트 결제(샌드박스 결제) 주문인가. true면 실제로 돈이 오가지 않았다")
5640
5895
  });
@@ -5893,4 +6148,4 @@ function createStoreSession(options) {
5893
6148
  };
5894
6149
  }
5895
6150
  //#endregion
5896
- 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, DOWNLOAD_DEFAULTS, FULFILLMENT_RULES, MAX_ANALYTICS_ALLOWED_ORIGINS, MAX_AUTO_TRANSITION_DAYS, MAX_CODES_PER_UPLOAD, MAX_CODE_LENGTH, 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, PRIVATE_UPLOAD_HEADERS, REFUND_POLICY_DEFAULTS, REFUND_POLICY_DELIVERY_FEE_KEYS, REMOTE_AREA_ZIP_RANGES, RESERVED_STOREFRONT_SLUGS, RESERVED_STORE_CODES, SHIPPING_POLICY_DEFAULTS, STOREFRONT_SITE_CREATE_LIMIT, STOREFRONT_SLUG_CHANGE_LIMIT, STOREFRONT_SLUG_CHANGE_WINDOW_DAYS, STOREFRONT_SLUG_HOLD_DAYS, STOREFRONT_SLUG_PATTERN, 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, autoFulfillableOf, 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, createStorefrontPreviewLinkRequestSchema, createStorefrontSiteRequestSchema, customerAddressSchema, customerConsentListQuerySchema, customerConsentSchema, customerConsentStateSchema, customerConsentsSchema, customerCouponSchema, customerDetailSchema, customerInquiryCategorySchema, customerInquiryListQuerySchema, customerListQuerySchema, customerOrderSchema, customerSortSchema, customerStatsSchema, customerSummarySchema, customerWithdrawalBlockedDetailsSchema, deliveryBundleKeyOf, deliveryFeeWaiverSchema, deliveryInfoSchema, deliveryMethodSchema, deliveryStatusSchema, deliveryTypeSchema, digitalAssetAttachRequestSchema, digitalAssetPreviewSchema, digitalAssetSchema, disableStorefrontHostingRequestSchema, discountRequestSchema, discountSchema, dispatchRequestSchema, easyPayProviderResponseSchema, easyPayProviderSchema, eventTypeSchema, exchangeAuthorizationCode, exchangeStoreTokenViaOAuth, fulfillBulkRequestSchema, fulfillRequestSchema, fulfillmentCodeBatchSchema, fulfillmentCodeListQuerySchema, fulfillmentCodePageSchema, fulfillmentCodeRejectionSchema, fulfillmentCodeRevealSchema, fulfillmentCodeSchema, fulfillmentCodeStatusSchema, fulfillmentCodeUploadRequestSchema, fulfillmentMailResendSchema, fulfillmentModeOf, fulfillmentModeSchema, fulfillmentRuleOf, fulfillmentTypeOf, fulfillmentTypeResponseSchema, fulfillmentTypeSchema, holdOrderItemRequestSchema, inquiryListQuerySchema, inquirySchema, inquiryWebhookEventDataSchema, inquiryWebhookEventSchema, invitationDeliveryStatusSchema, invitationListQuerySchema, invitationSchema, inviteMemberRequestSchema, isCouponTargetLine, isReservedStoreCode, isReservedStorefrontSlug, isStoreScope, isWebOrigin, issueCouponRequestSchema, issueCouponResultSchema, liveGateConditionCodeSchema, liveGateConditionSchema, liveGateErrorDetailsSchema, liveGateSchema, loginProviderSchema, loginProviderStatusSchema, loginProviderTestSchema, loginSettingsSchema, maskedSecretSchema, mcpRuleResultSchema, mcpToolEventSchema, myAccountSchema, myStoreSchema, normalizeCouponCode, oauthClientRegistrationRequestSchema, oauthClientRegistrationResponseSchema, oauthTokenResponseSchema, openFulfillmentUnion, optionGroupRequestSchema, optionGroupSchema, optionModelRequestSchema, optionModelSchema, optionValueRequestSchema, optionValueSchema, orderCanceledWebhookEventDataSchema, orderCanceledWebhookEventSchema, orderDiscountSchema, orderFulfilledWebhookEventDataSchema, orderFulfilledWebhookEventSchema, orderItemCodeSchema, orderItemDownloadSchema, 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, resolveFulfillmentMode, resolveRefundPolicy, resolveShippingPolicy, reviewListQuerySchema, reviewReportResultSchema, reviewSchema, reviewSortSchema, reviewWebhookEventDataSchema, reviewWebhookEventSchema, rollbackStorefrontRequestSchema, salesReportGroupBySchema, salesReportQuerySchema, salesReportRowSchema, salesReportSchema, salesTotalsSchema, saveLoginProviderRequestSchema, scopeAccessSchema, sellerAddressListQuerySchema, sellerAddressRequestSchema, sellerAddressSchema, sellerCustomerInquirySchema, setStorefrontVisibilityRequestSchema, settleCouponShortfall, settlementDetailQuerySchema, settlementDetailSchema, settlementListQuerySchema, settlementStatusSchema, settlementSummarySchema, settlementWithDetailsSchema, shippingAddressSchema, shippingPolicyPatchSchema, shippingPolicySchema, stockTrackingOf, storeCodeSchema, storeMemberRoleUpdatedSchema, storeMemberSchema, storeRoleSchema, storeSchema, storeScopeResourceSchema, storeTokenResponseSchema, storefrontDomainSchema, storefrontHostingSchema, storefrontPreviewLinkSchema, storefrontProvisioningSchema, storefrontSiteSchema, storefrontSiteStatusSchema, storefrontSlugAvailabilityQuerySchema, storefrontSlugAvailabilitySchema, storefrontSlugSchema, storefrontSourceModeSchema, storefrontTemplateSchema, storefrontVersionSchema, storefrontVersionStatusSchema, storefrontVisibilitySchema, tossPaymentsCredentialsInputSchema, trackingStageResponseSchema, trackingUpdateResultSchema, updateCouponRequestSchema, updateLoginProviderRequestSchema, updateLoginSettingsRequestSchema, updateMemberRoleRequestSchema, updatePaymentProviderRequestSchema, updatePaymentSettingsRequestSchema, updateStoreRequestSchema, uploadExtensionOf, uploadPurposeSchema, uploadRejection, uploadRequestSchema, uploadTicketSchema, upsertPortOneProviderRequestSchema, upsertTossPaymentsProviderRequestSchema, userTokenResponseSchema, usesCodePool, variantStockRequestSchema, variantStockSchema, verifyPaymentProviderRequestSchema, webhookCreatedSchema, webhookDeliveryDetailSchema, webhookDeliveryErrorCodeSchema, webhookDeliveryListQuerySchema, webhookDeliveryResourceSchema, webhookDeliverySchema, webhookDeliveryStatusSchema, webhookEventSchema, webhookFailureReasonSchema, webhookRedeliverFailedResultSchema, webhookRequestHeaderSchema, webhookRequestSchema, webhookResourceTypeSchema, webhookSchema, webhookSecretRotatedSchema, webhookStatusSchema, webhookTestRequestSchema, webhookTestResultSchema, withdrawCustomerRequestSchema, withdrawalRestrictedOf, writeDryRunResultSchema, writePreviewSchema };
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 };