@sayren/store-sdk 0.23.0 → 0.28.1

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.
@@ -2,9 +2,9 @@ import { z } from "zod";
2
2
  import { type PageParams, pageSchema } from "./common";
3
3
 
4
4
  /**
5
- * 스토어 결제(PG) 설정과 매출 리포트 계약.
5
+ * 상점 결제(PG) 설정과 매출 리포트 계약.
6
6
  *
7
- * PG는 셀러가 직접 계약한다. 자격 증명은 스토어별로 저장되고, 비밀 값(시크릿 키·API Secret·
7
+ * PG는 셀러가 직접 계약한다. 자격 증명은 상점별로 저장되고, 비밀 값(시크릿 키·API Secret·
8
8
  * 웹훅 시크릿)은 저장 후 다시 조회할 수 없다. 응답에는 앞 4자리 마스킹만 나온다. 공개 값
9
9
  * (클라이언트 키·Store ID·채널 키)은 원래 브라우저 결제창에 노출되는 값이라 그대로 돌려준다.
10
10
  */
@@ -53,7 +53,7 @@ const secretInput = () => z.string().trim().min(8).max(512);
53
53
  /**
54
54
  * 토스페이먼츠 결제위젯 키 접두사. 결제창은 API 개별 연동(`*_ck_`·`*_sk_`)만 쓴다. 위젯 키는 결제창 SDK가
55
55
  * `NOT_SUPPORTED_WIDGET_KEY`로, 서버 API가 404 `NOT_FOUND_MERCHANT`로 거부하므로 저장 전에 막는다.
56
- * 요청 스키마·api 어댑터·콘솔 폼이 같은 상수를 쓴다(원천). `g` 플래그가 없어 `.test()`가 상태를 갖지 않는다.
56
+ * 요청 스키마·api 어댑터·상점 플랫폼 폼이 같은 상수를 쓴다(원천). `g` 플래그가 없어 `.test()`가 상태를 갖지 않는다.
57
57
  */
58
58
  export const TOSS_WIDGET_CLIENT_KEY_PATTERN = /^(test|live)_gck_/;
59
59
  /** 토스페이먼츠 결제위젯 시크릿 키 접두사. {@link TOSS_WIDGET_CLIENT_KEY_PATTERN} 참고 */
@@ -360,7 +360,7 @@ export const paymentProviderSchema = z.object({
360
360
  .optional()
361
361
  .default(null)
362
362
  .describe(
363
- "지금 새 결제의 후보가 아닌 사유. 후보면 null. 판정 원천은 서버 라우팅이다(콘솔이 다시 계산하지 않는다). " +
363
+ "지금 새 결제의 후보가 아닌 사유. 후보면 null. 판정 원천은 서버 라우팅이다(상점 플랫폼이 다시 계산하지 않는다). " +
364
364
  "후보가 아닌 PG에는 항상 사유 코드를 붙인다(연결 안 된 PG 포함) — 모든 PG는 `candidates`에 있거나 이 값이 null이 아니다. 현재 값: " +
365
365
  "`NOT_CONNECTED`(연결 안 됨) · `DISABLED`(사용 중지) · `CREDENTIAL_MISSING`(활성 환경 자격 증명 없음) · " +
366
366
  "`SANDBOX_IN_LIVE_STORE`(다른 PG가 라이브라 이 샌드박스 PG는 실구매자 결제에 쓰지 않음) · " +
@@ -559,7 +559,9 @@ export const cashReceiptSchema = z.object({
559
559
  canceledAt: z.string().nullable().describe("최근 취소 시각 (ISO 8601)"),
560
560
  failCode: z.string().nullable().describe("마지막 실패 코드. 성공하면 비운다"),
561
561
  failMessage: z.string().nullable().describe("마지막 실패 사유"),
562
- reissuable: z.boolean().describe("콘솔 재발급으로 다시 시도할 수 있는가 (`FAILED`·`PENDING`)"),
562
+ reissuable: z
563
+ .boolean()
564
+ .describe("상점 플랫폼 재발급으로 다시 시도할 수 있는가 (`FAILED`·`PENDING`)"),
563
565
  requestedAt: z.string().describe("신청 시각 (ISO 8601)"),
564
566
  updatedAt: z.string().describe("마지막 변경 시각 (ISO 8601)"),
565
567
  });
@@ -35,6 +35,13 @@ export const productSummarySchema = z.object({
35
35
  "상품 이행의 유형. 유형별 속성은 단건 조회(`GET /v1/products/{productId}`)의 `fulfillment`에 있다",
36
36
  ),
37
37
  updatedAt: z.string(),
38
+ sample: z
39
+ .boolean()
40
+ .optional()
41
+ .default(false)
42
+ .describe(
43
+ "상점을 만들 때 넣은 샘플 상품인가(이슈 #70). 상점 플랫폼이 「샘플」 배지를 단다. 스토어프론트에는 싣지 않는다. 서버는 항상 싣는다",
44
+ ),
38
45
  });
39
46
 
40
47
  export type ProductSummary = z.infer<typeof productSummarySchema>;
@@ -105,7 +112,7 @@ export const MAX_ADDON_GROUPS = 5;
105
112
  export const MAX_CUSTOM_INPUTS = 5;
106
113
  /** 직접 입력 항목 최대 글자 수의 상한 */
107
114
  export const MAX_CUSTOM_INPUT_LENGTH = 200;
108
- /** 조합 수가 이 값 이상이면 콘솔이 경고한다(상한은 `MAX_VARIANTS`) */
115
+ /** 조합 수가 이 값 이상이면 상점 플랫폼이 경고한다(상한은 `MAX_VARIANTS`) */
109
116
  export const VARIANT_COUNT_WARNING = 100;
110
117
 
111
118
  /**
@@ -152,7 +159,7 @@ export const customInputSchema = z.object({
152
159
  export type CustomInput = z.infer<typeof customInputSchema>;
153
160
 
154
161
  /**
155
- * 직접 입력 항목명에 쓸 수 없는 개인정보성 단어 — 콘솔 검사와 서버 400(`CUSTOM_INPUT_LABEL_FORBIDDEN`)이 이 목록 하나를 쓴다.
162
+ * 직접 입력 항목명에 쓸 수 없는 개인정보성 단어 — 상점 플랫폼 검사와 서버 400(`CUSTOM_INPUT_LABEL_FORBIDDEN`)이 이 목록 하나를 쓴다.
156
163
  * 구매자 개인정보는 주문 정보로 받는다. 직접 입력 항목으로 받으면 보관·파기 규칙 밖에 남는다.
157
164
  */
158
165
  export const CUSTOM_INPUT_FORBIDDEN_WORDS = [
@@ -307,7 +314,7 @@ export const productSchema = productSummarySchema.extend({
307
314
  refundPolicy: productRefundPolicyPatchSchema
308
315
  .nullable()
309
316
  .describe(
310
- "상품별 환불 정책 덮어쓰기(상품이 정한 항목만). null이면 스토어 기본 환불 정책을 쓴다. " +
317
+ "상품별 환불 정책 덮어쓰기(상품이 정한 항목만). null이면 상점 기본 환불 정책을 쓴다. " +
311
318
  "반품·교환 배송비(`returnDeliveryFee`·`exchangeDeliveryFee`)는 배송 상품에만 있다",
312
319
  ),
313
320
  /**
@@ -538,7 +545,7 @@ const productWriteShape = {
538
545
  refundPolicy: productRefundPolicyPatchSchema
539
546
  .nullish()
540
547
  .describe(
541
- "상품별 환불 정책 — 준 항목만 스토어 기본을 덮어쓴다. `null`이나 생략은 스토어 기본을 그대로 쓴다는 뜻이다. " +
548
+ "상품별 환불 정책 — 준 항목만 상점 기본을 덮어쓴다. `null`이나 생략은 상점 기본을 그대로 쓴다는 뜻이다. " +
542
549
  "반품·교환 배송비(`returnDeliveryFee`·`exchangeDeliveryFee`)는 배송 상품에만 줄 수 있다(다른 유형은 400)",
543
550
  ),
544
551
  ...optionModelRequestSchema.shape,
@@ -587,7 +594,7 @@ export const productPatchRequestSchema = z
587
594
  .nullable()
588
595
  .optional()
589
596
  .describe(
590
- "상품별 환불 정책. 주면 통째로 바꾼다(준 항목만 스토어 기본을 덮어쓴다). `null`은 덮어쓰기를 지워 스토어 기본을 따른다. 생략하면 바꾸지 않는다",
597
+ "상품별 환불 정책. 주면 통째로 바꾼다(준 항목만 상점 기본을 덮어쓴다). `null`은 덮어쓰기를 지워 상점 기본을 따른다. 생략하면 바꾸지 않는다",
591
598
  ),
592
599
  })
593
600
  .refine((value) => Object.values(value).some((v) => v !== undefined), {
@@ -16,7 +16,7 @@ import { proratedRefundBase } from "./refund-policy";
16
16
  * 4. 배송비 쿠폰(`DELIVERY`) — 주문에 한 장. 기본 배송비(지역 추가 배송비 제외)를 기준으로 할인하고
17
17
  * 배송 묶음에 기본 배송비 비율로 나눈다.
18
18
  *
19
- * 무료배송 기준(상품별 조건부 무료·스토어 전체 기준)은 **쿠폰 차감 전 금액**으로 판정한다. 그래서 배송비
19
+ * 무료배송 기준(상품별 조건부 무료·상점 전체 기준)은 **쿠폰 차감 전 금액**으로 판정한다. 그래서 배송비
20
20
  * 견적(`quoteDelivery`)은 쿠폰과 무관하게 먼저 계산되고, 이 함수는 그 결과만 입력으로 받는다.
21
21
  *
22
22
  * 최소 주문 금액은 대상 상품의 **즉시할인 반영 합계**(쿠폰 차감 전, 배송비 제외)로 판정한다.
@@ -34,7 +34,7 @@ const descriptions = {
34
34
  "정률 할인의 최대 할인액(원). null이면 상한이 없다. 정액 할인에는 쓰지 않는다(null)",
35
35
  minOrderAmount:
36
36
  "최소 주문 금액(원). 대상 상품의 즉시할인 반영 합계(배송비 제외)가 이 값 이상이어야 쓸 수 있다. 0이면 조건이 없다",
37
- firstOrderOnly: "이 스토어에서 결제 완료 주문이 없는 회원만 쓸 수 있다",
37
+ firstOrderOnly: "이 상점에서 결제 완료 주문이 없는 회원만 쓸 수 있다",
38
38
  memberOnly:
39
39
  "회원만 쓸 수 있다. false면 비회원도 쓸 수 있다 — 코드 입력형이면서 1인 한도가 없는 쿠폰만 false로 둘 수 있다",
40
40
  stackWithInstantDiscount:
@@ -224,7 +224,7 @@ export interface PricingInput {
224
224
  coupons: readonly PricingCoupon[];
225
225
  /** 배송비 견적. 없으면 배송비 0으로 계산하고 배송비 쿠폰은 할인할 금액이 없다 */
226
226
  delivery?: PricingDelivery | null;
227
- /** 이 스토어에서 결제 완료 주문이 없는 회원인가 */
227
+ /** 이 상점에서 결제 완료 주문이 없는 회원인가 */
228
228
  firstOrder: boolean;
229
229
  /** 회원 주문인가 */
230
230
  isMember: boolean;
@@ -635,7 +635,7 @@ export interface CouponSettlementInput {
635
635
  coupons: readonly AppliedCouponSnapshot[];
636
636
  lines: readonly CouponSettlementLine[];
637
637
  settlements: Readonly<Record<string, Partial<CouponSettlementState> | undefined>>;
638
- /** 스토어 환불 정책 `couponShortfallCharge` */
638
+ /** 상점 환불 정책 `couponShortfallCharge` */
639
639
  shortfallCharge: boolean;
640
640
  /** 판매자 귀책(직권취소, 불량·오배송 반품) — 미달분을 청구하지 않는다 */
641
641
  sellerFault: boolean;
@@ -3,9 +3,9 @@ import { z } from "zod";
3
3
  /**
4
4
  * 환불 정책 (이슈 #25) — 취소·반품·교환에서 **배송비를 누가 부담하는지**를 정한다.
5
5
  *
6
- * 두 층이다. 스토어 설정(`PATCH /v1/store`의 `refundPolicy`)이 기본값이고, 상품별 정책
6
+ * 두 층이다. 상점 설정(`PATCH /v1/store`의 `refundPolicy`)이 기본값이고, 상품별 정책
7
7
  * (`POST/PUT /v1/products`의 `refundPolicy`)이 그 상품에만 덮어쓴다. 상품이 정하지 않은 항목은
8
- * 스토어 기본을 따르고, 스토어도 정하지 않았으면 `REFUND_POLICY_DEFAULTS`다.
8
+ * 상점 기본을 따르고, 상점도 정하지 않았으면 `REFUND_POLICY_DEFAULTS`다.
9
9
  *
10
10
  * 한 주문에 정책이 섞이면 **상품별로 계산한다**. 배송비 묶음도 상품 단위다(주문서·장바구니의
11
11
  * 배송비 합산과 같은 규칙: 같은 상품의 주문상품이 한 묶음이고 묶음마다 배송비가 붙는다).
@@ -30,7 +30,7 @@ const descriptions = {
30
30
  "판매자 귀책(셀러 직권취소, 불량·오배송 반품)은 빼지 않는다. false면 차감 없이 실결제 금액만 나눠 환불한다. 기본 true",
31
31
  } as const;
32
32
 
33
- /** 유효 환불 정책 — 스토어 기본 + 상품별 덮어쓰기를 합친 결과 */
33
+ /** 유효 환불 정책 — 상점 기본 + 상품별 덮어쓰기를 합친 결과 */
34
34
  export const refundPolicySchema = z.object({
35
35
  preShipmentDeliveryFeeRefund: z.boolean().describe(descriptions.preShipmentDeliveryFeeRefund),
36
36
  returnDeliveryFee: z.number().int().min(0).describe(descriptions.returnDeliveryFee),
@@ -49,7 +49,7 @@ export const refundPolicySchema = z.object({
49
49
  export type RefundPolicy = z.infer<typeof refundPolicySchema>;
50
50
 
51
51
  /**
52
- * 스토어가 정하지 않았을 때 서버가 쓰는 값.
52
+ * 상점이 정하지 않았을 때 서버가 쓰는 값.
53
53
  *
54
54
  * `as const`가 아니라 `RefundPolicy`로 타입을 좁힌다 — 리터럴 타입이면 `Partial<typeof …>`로 일부만 바꿀 때
55
55
  * `true` 자리에 `false`를 넣을 수 없다.
@@ -64,7 +64,7 @@ export const REFUND_POLICY_DEFAULTS: RefundPolicy = {
64
64
  };
65
65
 
66
66
  /**
67
- * 부분 정책 — 스토어 설정 요청과 상품별 덮어쓰기가 쓴다. 준 항목만 바뀐다.
67
+ * 부분 정책 — 상점 설정 요청과 상품별 덮어쓰기가 쓴다. 준 항목만 바뀐다.
68
68
  *
69
69
  * `strictObject`라 모르는 키는 400이다. 오타(`returnFee` 등)를 조용히 무시하면 셀러는 저장됐다고
70
70
  * 믿는데 정책은 기본값으로 남는다.
@@ -93,7 +93,7 @@ export type RefundPolicyPatch = z.infer<typeof refundPolicyPatchSchema>;
93
93
 
94
94
  /**
95
95
  * 상품별 덮어쓰기 — 쿠폰 조건 미달 차감(`couponShortfallCharge`)은 주문 단위 규칙이라 상품이 정하지 않는다.
96
- * 스토어 정책만 읽는다(이슈 #47).
96
+ * 상점 정책만 읽는다(이슈 #47).
97
97
  */
98
98
  export const productRefundPolicyPatchSchema = refundPolicyPatchSchema.omit({
99
99
  couponShortfallCharge: true,
@@ -101,12 +101,12 @@ export const productRefundPolicyPatchSchema = refundPolicyPatchSchema.omit({
101
101
 
102
102
  /** 정책 해석에 쓰는 상품 값 — 상품별 덮어쓰기 */
103
103
  export interface RefundPolicyProductInput {
104
- /** 상품별 덮어쓰기. 없으면 스토어 기본을 그대로 쓴다 */
104
+ /** 상품별 덮어쓰기. 없으면 상점 기본을 그대로 쓴다 */
105
105
  refundPolicy?: RefundPolicyPatch | null;
106
106
  }
107
107
 
108
108
  /**
109
- * 유효 정책 해석 — 상품별 덮어쓰기 → 스토어 기본 → `REFUND_POLICY_DEFAULTS` 순이다.
109
+ * 유효 정책 해석 — 상품별 덮어쓰기 → 상점 기본 → `REFUND_POLICY_DEFAULTS` 순이다.
110
110
  *
111
111
  * 반품·교환 배송비의 원천은 정책 하나다(이슈 #57 결정 3). 상품의 레거시 반품비·교환비 필드와의 연결은 없앴고,
112
112
  * 레거시 값은 마이그레이션이 상품별 정책으로 옮겼다.
@@ -122,15 +122,15 @@ export function resolveRefundPolicy(
122
122
  /**
123
123
  * 상품의 **덮어쓰기 자체** — 덮어쓰기가 없으면 null.
124
124
  *
125
- * `resolveRefundPolicy`가 합친 결과로는 "상품이 정한 값"과 "스토어 기본을 물려받은 값"을 가를 수 없다.
126
- * 관리 API 응답의 `refundPolicy`와 콘솔 폼의 덮어쓰기 스위치가 이 함수를 원천으로 쓴다 — 가르지 못하면
127
- * 스토어 기본을 쓰는 상품을 저장할 때 그 순간의 기본값이 상품에 박힌다.
125
+ * `resolveRefundPolicy`가 합친 결과로는 "상품이 정한 값"과 "상점 기본을 물려받은 값"을 가를 수 없다.
126
+ * 관리 API 응답의 `refundPolicy`와 상점 플랫폼 폼의 덮어쓰기 스위치가 이 함수를 원천으로 쓴다 — 가르지 못하면
127
+ * 상점 기본을 쓰는 상품을 저장할 때 그 순간의 기본값이 상품에 박힌다.
128
128
  */
129
129
  export function productRefundPolicyOverride(
130
130
  product?: RefundPolicyProductInput | null,
131
131
  ): RefundPolicyPatch | null {
132
132
  if (product?.refundPolicy == null) return null;
133
- // 쿠폰 조건 미달 차감은 주문 단위라 스토어만 정한다 — 상품 행에 키가 있어도 덮어쓰기로 읽지 않는다(이슈 #47)
133
+ // 쿠폰 조건 미달 차감은 주문 단위라 상점만 정한다 — 상품 행에 키가 있어도 덮어쓰기로 읽지 않는다(이슈 #47)
134
134
  const { couponShortfallCharge: _storeOnly, ...override } = dropUndefined(product.refundPolicy);
135
135
  return override;
136
136
  }
@@ -16,7 +16,7 @@ import {
16
16
  * 달라 저장하지 않는다. 추가금은 조합별이 아니라 **값별**로 둔다 — 조합 옵션으로 불러오면 조합 추가금은 고른 값 추가금의 합이다.
17
17
  */
18
18
 
19
- /** 스토어당 저장 옵션 최대 개수 */
19
+ /** 상점당 저장 옵션 최대 개수 */
20
20
  export const MAX_SAVED_OPTION_SETS = 200;
21
21
  /** 저장 옵션 이름 최대 글자 수 */
22
22
  export const MAX_SAVED_OPTION_SET_NAME = 50;
@@ -2,10 +2,10 @@ import { z } from "zod";
2
2
  import type { StoreRole } from "./auth";
3
3
 
4
4
  /**
5
- * 스토어 스코프 카탈로그 (auth-spec §7) — 형식 `{리소스}:{r|rw}`, rw는 r을 함의한다.
5
+ * 상점 스코프 카탈로그 (auth-spec §7) — 형식 `{리소스}:{r|rw}`, rw는 r을 함의한다.
6
6
  *
7
7
  * 관리 API가 요구하는 스코프와 역할별 자동 부여 스코프의 **단일 원천**이다.
8
- * 서버(apps/api)가 이 상수를 그대로 가져다 역할→스코프를 산출하므로, 콘솔 권한 매트릭스와
8
+ * 서버(apps/api)가 이 상수를 그대로 가져다 역할→스코프를 산출하므로, 상점 플랫폼 권한 매트릭스와
9
9
  * 서버 인가가 갈라지지 않는다.
10
10
  */
11
11
 
@@ -49,8 +49,7 @@ export function isStoreScope(scope: string): boolean {
49
49
 
50
50
  /**
51
51
  * 역할별 자동 부여 스코프 (auth-spec §7).
52
- * storeToken(사람)의 스코프는 이 표로 결정된다. PAT·M2M 클라이언트에는 역할이 없고
53
- * 발급 시 지정한 스코프만 갖는다(발급자 보유 스코프 이내).
52
+ * 계정 토큰으로 상점을 호출할 때의 스코프는 그 상점에서의 역할로 이 표에서 결정된다.
54
53
  */
55
54
  export const STORE_ROLE_SCOPES = {
56
55
  OWNER: [
@@ -9,7 +9,7 @@ import { type DeliveryType, type FulfillmentType, requiresShipping } from "./ful
9
9
  * (`deliveryBundleKeyOf`). 이슈 #25의 「묶음 = 같은 상품」 규칙을 일반화한 것이다 — 키를 상품 id에서
10
10
  * 정책 지문으로 바꿨고, 주문상품이 주문 시점 키를 스냅샷해(`order_items.delivery_bundle_key`)
11
11
  * 환불 정산(`refund-settlement.ts`)과 주문 스냅샷(`orders.delivery_fee_settlements`)이 같은 키를 쓴다.
12
- * 2. **무료 기준**: 상품별 조건부 무료(`CONDITIONAL_FREE`, 묶음 소계 기준)와 스토어 전체 무료 기준
12
+ * 2. **무료 기준**: 상품별 조건부 무료(`CONDITIONAL_FREE`, 묶음 소계 기준)와 상점 전체 무료 기준
13
13
  * (`freeThreshold`, 주문 상품 금액 합 기준)이 있다. 둘 중 하나라도 충족하면 그 배송비는 0이다.
14
14
  * 3. **지역 추가 배송비**: 우편번호가 제주·도서산간이면 **묶음마다** 추가된다. 묶음이 곧 한 번의 발송이라
15
15
  * 두 출고지에서 각각 보내면 추가 운임도 두 번 든다.
@@ -20,21 +20,21 @@ import { type DeliveryType, type FulfillmentType, requiresShipping } from "./ful
20
20
  * 계산은 `quoteDelivery` 하나다. 장바구니·주문서·결제 시작·환불 정산이 모두 이 함수를 쓴다 —
21
21
  * 네 곳이 각자 계산하면 화면 금액과 결제 금액이 갈라진다.
22
22
  *
23
- * **비실물 줄은 배송 계산에서 빠진다**(이슈 #48, `requiresShipping`). 묶음을 만들지 않고, 스토어 전체 무료 기준의
23
+ * **비실물 줄은 배송 계산에서 빠진다**(이슈 #48, `requiresShipping`). 묶음을 만들지 않고, 상점 전체 무료 기준의
24
24
  * 판정 금액(`shippableAmount`)에도 넣지 않는다 — 디지털 상품 금액이 실물 배송비를 면제시키지 않게 한다.
25
25
  */
26
26
 
27
27
  const descriptions = {
28
28
  baseDeliveryFee:
29
- "스토어 기본 배송비. 원 단위. 상품 등록·수정에서 `deliveryFee`를 생략하면 이 값을 쓴다. 기본 3000",
29
+ "상점 기본 배송비. 원 단위. 상품 등록·수정에서 `deliveryFee`를 생략하면 이 값을 쓴다. 기본 3000",
30
30
  freeThreshold:
31
- "스토어 전체 무료배송 기준액. 주문 상품 금액 합이 이 값 이상이면 모든 묶음의 배송비가 0이다. " +
31
+ "상점 전체 무료배송 기준액. 주문 상품 금액 합이 이 값 이상이면 모든 묶음의 배송비가 0이다. " +
32
32
  "null이면 전체 무료배송을 쓰지 않는다(상품별 조건부 무료는 그대로 동작한다). 기본 null",
33
33
  jejuSurcharge: "제주 지역 추가 배송비. 원 단위. **배송 묶음마다** 더한다. 기본 0",
34
34
  isolatedSurcharge: "도서산간 지역 추가 배송비. 원 단위. **배송 묶음마다** 더한다. 기본 0",
35
35
  } as const;
36
36
 
37
- /** 유효 배송비 정책 — 스토어가 정하지 않은 항목은 `SHIPPING_POLICY_DEFAULTS`로 채운 값이다 */
37
+ /** 유효 배송비 정책 — 상점이 정하지 않은 항목은 `SHIPPING_POLICY_DEFAULTS`로 채운 값이다 */
38
38
  export const shippingPolicySchema = z.object({
39
39
  baseDeliveryFee: z.number().int().min(0).describe(descriptions.baseDeliveryFee),
40
40
  freeThreshold: z.number().int().min(1).nullable().describe(descriptions.freeThreshold),
@@ -45,7 +45,7 @@ export const shippingPolicySchema = z.object({
45
45
  export type ShippingPolicy = z.infer<typeof shippingPolicySchema>;
46
46
 
47
47
  /**
48
- * 스토어가 정하지 않았을 때 서버가 쓰는 값.
48
+ * 상점이 정하지 않았을 때 서버가 쓰는 값.
49
49
  *
50
50
  * `as const`가 아니라 `ShippingPolicy`로 좁힌다 — 리터럴 타입이면 `Partial<typeof …>`로 일부만 바꿀 때
51
51
  * `null` 자리에 숫자를 넣을 수 없다(`REFUND_POLICY_DEFAULTS`와 같은 이유).
@@ -72,7 +72,7 @@ export const shippingPolicyPatchSchema = z.strictObject({
72
72
 
73
73
  export type ShippingPolicyPatch = z.infer<typeof shippingPolicyPatchSchema>;
74
74
 
75
- /** 유효 정책 해석 — 스토어가 정한 항목 + `SHIPPING_POLICY_DEFAULTS` */
75
+ /** 유효 정책 해석 — 상점이 정한 항목 + `SHIPPING_POLICY_DEFAULTS` */
76
76
  export function resolveShippingPolicy(patch?: ShippingPolicyPatch | null): ShippingPolicy {
77
77
  if (!patch) return { ...SHIPPING_POLICY_DEFAULTS };
78
78
  const given = Object.fromEntries(
@@ -89,10 +89,10 @@ export const remoteAreaSchema = z.enum(["NONE", "JEJU", "ISOLATED"]);
89
89
  export type RemoteArea = z.infer<typeof remoteAreaSchema>;
90
90
 
91
91
  /**
92
- * 제주·도서산간 우편번호 범위 — **코드 상수다.** 금액은 스토어가 정하고 범위는 플랫폼이 고정한다.
92
+ * 제주·도서산간 우편번호 범위 — **코드 상수다.** 금액은 상점이 정하고 범위는 플랫폼이 고정한다.
93
93
  *
94
94
  * 국내 택배사가 공표하는 제주·도서산간 할증 구간을 5자리 우편번호 범위로 옮긴 표다. 셀러마다
95
- * 범위가 다르면 같은 주소에 다른 금액이 나가고 CS가 셀러 수만큼 갈라진다. 스토어별 범위 편집은
95
+ * 범위가 다르면 같은 주소에 다른 금액이 나가고 CS가 셀러 수만큼 갈라진다. 상점별 범위 편집은
96
96
  * 후속 과제이고, 지금은 이 표가 유일한 원천이다.
97
97
  *
98
98
  * ## 근거
@@ -131,7 +131,7 @@ export type RemoteArea = z.infer<typeof remoteAreaSchema>;
131
131
  * 도로명주소·건물관리번호 단위 판정은 후속 과제다.
132
132
  *
133
133
  * 범위는 **닫힌 구간**(`from` ≤ zip ≤ `to`)이고 겹치지 않는다. 앞에서부터 처음 맞는 항목을 쓴다.
134
- * 순서는 콘솔 「추가 배송비 지역」 표의 행 순서이기도 하다(`remoteAreaGroups`). 제주를 먼저 두고
134
+ * 순서는 상점 플랫폼 「추가 배송비 지역」 표의 행 순서이기도 하다(`remoteAreaGroups`). 제주를 먼저 두고
135
135
  * 도서산간은 우편번호 오름차순으로 둔다.
136
136
  */
137
137
  export const REMOTE_AREA_ZIP_RANGES: ReadonlyArray<{
@@ -141,7 +141,7 @@ export const REMOTE_AREA_ZIP_RANGES: ReadonlyArray<{
141
141
  readonly label: string;
142
142
  }> = [
143
143
  // 제주 전지역. 추자면·우도는 택배사 표에서 더 높은 운임이지만 공개 계약의 지역 구분은
144
- // JEJU 하나이고 금액은 스토어가 정한다.
144
+ // JEJU 하나이고 금액은 상점이 정한다.
145
145
  { area: "JEJU", from: 63_000, to: 63_644, label: "제주특별자치도" },
146
146
  // 무의도·실미도·팔미도. 택배사 표는 「인천 중구 섬지역」이지만 2026-07-01 행정체제 개편으로
147
147
  // 중구가 나뉘면서 무의동은 영종구 소속이다.
@@ -220,7 +220,7 @@ export interface DeliveryBundleProduct {
220
220
  deliveryType: DeliveryType;
221
221
  deliveryFee: number;
222
222
  conditionalFreeAmount: number | null;
223
- /** 출고지 주소 id(`seller_addresses`). null이면 스토어 기본 출고지를 쓰는 상품끼리 한 묶음이다 */
223
+ /** 출고지 주소 id(`seller_addresses`). null이면 상점 기본 출고지를 쓰는 상품끼리 한 묶음이다 */
224
224
  shippingAddressId?: string | null;
225
225
  /**
226
226
  * 상품 유형 (이슈 #48). 생략·null이면 배송 상품이다. 배송이 필요 없는 유형은 묶음을 만들지 않는다.
@@ -265,7 +265,7 @@ export interface DeliveryFeeLine {
265
265
  * 기본 배송비를 면제한 근거 — 면제하지 않았으면 null이다.
266
266
  *
267
267
  * 환불 정산이 근거를 구분해야 한다. 조건부 무료 기준 미달 차감은 **상품별 조건부 무료(`CONDITIONAL`)로
268
- * 면제된 묶음만** 대상이다. 스토어 전체 무료 기준(`STORE_THRESHOLD`)이나 무료배송 상품(`FREE`)은 셀러가
268
+ * 면제된 묶음만** 대상이다. 상점 전체 무료 기준(`STORE_THRESHOLD`)이나 무료배송 상품(`FREE`)은 셀러가
269
269
  * 배송비를 받지 않기로 한 것이므로, 일부 취소로 기준이 깨져도 구매자에게 배송비를 물리지 않는다.
270
270
  *
271
271
  * 세 값이 겹치면 `FREE` → `STORE_THRESHOLD` → `CONDITIONAL` 순으로 센 근거를 남긴다.
@@ -277,7 +277,7 @@ export type DeliveryFeeWaiver = z.infer<typeof deliveryFeeWaiverSchema>;
277
277
  /** 묶음 하나의 견적 */
278
278
  export interface DeliveryBundleQuote {
279
279
  bundleKey: string;
280
- /** 이 묶음에 든 상품 id — 스냅샷 검증과 콘솔 안내에 쓴다 */
280
+ /** 이 묶음에 든 상품 id — 스냅샷 검증과 상점 플랫폼 안내에 쓴다 */
281
281
  productIds: string[];
282
282
  /** 실제 청구한 기본 배송비 — 무료배송·무료 기준 충족이면 0 */
283
283
  fee: number;
@@ -301,10 +301,10 @@ export interface DeliveryQuote {
301
301
  /** 청구할 배송비 총액 = `baseFee` + `remoteSurcharge` */
302
302
  deliveryFee: number;
303
303
  remoteArea: RemoteArea;
304
- /** 스토어 전체 무료 기준(`freeThreshold`) 충족으로 기본 배송비를 면제했는가 */
304
+ /** 상점 전체 무료 기준(`freeThreshold`) 충족으로 기본 배송비를 면제했는가 */
305
305
  freeByThreshold: boolean;
306
306
  /**
307
- * 배송이 필요한 줄의 상품 금액 합 — 스토어 전체 무료 기준의 판정 금액이다(이슈 #48). 비실물 줄은 빠진다.
307
+ * 배송이 필요한 줄의 상품 금액 합 — 상점 전체 무료 기준의 판정 금액이다(이슈 #48). 비실물 줄은 빠진다.
308
308
  * 주문 상품 금액 합(`productAmount`)과 다를 수 있다.
309
309
  */
310
310
  shippableAmount: number;
@@ -325,7 +325,7 @@ export interface DeliveryQuoteInput {
325
325
  * 배송비 계산 — **순수 함수다.** 장바구니 요약, 주문서 금액, 결제 시작의 최종 확정,
326
326
  * 주문 배송비 스냅샷이 모두 이 한 곳을 쓴다.
327
327
  *
328
- * 순서가 규칙이다. 묶음별 조건부 무료를 먼저 보고, 그다음 스토어 전체 무료 기준으로 기본 배송비를
328
+ * 순서가 규칙이다. 묶음별 조건부 무료를 먼저 보고, 그다음 상점 전체 무료 기준으로 기본 배송비를
329
329
  * 한꺼번에 0으로 만들고, 마지막에 지역 추가 배송비를 묶음마다 더한다. 추가 배송비는 무료 기준에
330
330
  * 걸리지 않는다.
331
331
  */
@@ -363,7 +363,7 @@ export function quoteDelivery(input: DeliveryQuoteInput): DeliveryQuote {
363
363
  product.deliveryType === "CONDITIONAL_FREE" && product.conditionalFreeAmount !== null;
364
364
  const freeThreshold = conditionalFree ? (product.conditionalFreeAmount ?? 0) : null;
365
365
  const listFee = product.deliveryType === "FREE" ? 0 : product.deliveryFee;
366
- // 면제 근거는 센 것부터 본다 — 무료배송 상품 > 스토어 전체 무료 기준 > 상품별 조건부 무료.
366
+ // 면제 근거는 센 것부터 본다 — 무료배송 상품 > 상점 전체 무료 기준 > 상품별 조건부 무료.
367
367
  // 환불 정산의 미달 차감은 CONDITIONAL만 대상이므로 근거를 뭉개면 안 된다.
368
368
  const waivedBy: DeliveryFeeWaiver | null =
369
369
  product.deliveryType === "FREE"
@@ -4,7 +4,7 @@ import { refundPolicyPatchSchema, refundPolicySchema } from "./refund-policy";
4
4
  import { shippingPolicyPatchSchema, shippingPolicySchema } from "./shipping-policy";
5
5
 
6
6
  /**
7
- * 스토어 코드 규칙 — 스토어프론트 API가 테넌트를 가르는 값이다(`X-Store-Code` 헤더).
7
+ * 상점 코드 규칙 — 스토어프론트 API가 테넌트를 가르는 값이다(`X-Store-Code` 헤더).
8
8
  * **개설할 때 서버가 랜덤으로 발급하고** 이후 바꿀 수 없다. 이미 나간 링크와 승인된 결제가
9
9
  * 이 값에 묶여 있어서 바꾸면 함께 깨진다.
10
10
  *
@@ -12,7 +12,7 @@ import { shippingPolicyPatchSchema, shippingPolicySchema } from "./shipping-poli
12
12
  */
13
13
  export const STORE_CODE_PATTERN = /^[a-z0-9](?:[a-z0-9-]{1,28}[a-z0-9])$/;
14
14
 
15
- /** 플랫폼이 쓰거나 쓸 호스트 이름 — 스토어 코드로 내주지 않는다 */
15
+ /** 플랫폼이 쓰거나 쓸 호스트 이름 — 상점 코드로 내주지 않는다 */
16
16
  export const RESERVED_STORE_CODES: readonly string[] = [
17
17
  "www",
18
18
  "api",
@@ -72,7 +72,7 @@ export const storeCodeSchema = z
72
72
  )
73
73
  .refine(
74
74
  (code) => !isReservedStoreCode(code),
75
- "플랫폼이 쓰는 이름이라 스토어 코드로 쓸 수 없습니다",
75
+ "플랫폼이 쓰는 이름이라 상점 코드로 쓸 수 없습니다",
76
76
  );
77
77
 
78
78
  /** `https://shop.example.com`처럼 경로·쿼리 없는 http(s) 오리진인가 */
@@ -108,7 +108,7 @@ export const MAX_ANALYTICS_ALLOWED_ORIGINS = 20;
108
108
  export const MAX_AUTO_TRANSITION_DAYS = 30;
109
109
 
110
110
  /**
111
- * 약관 버전 — 스토어가 정하는 문자열이다(`1`·`2`·`2026-09-26`·`v1.2` 등). 형식은 강제하지 않고 값이 다르면 다른 버전이다.
111
+ * 약관 버전 — 상점이 정하는 문자열이다(`1`·`2`·`2026-09-26`·`v1.2` 등). 형식은 강제하지 않고 값이 다르면 다른 버전이다.
112
112
  * 올리면 그 약관에 동의한 구매자의 `reconsentRequired`가 켜지고, 재동의가 이력에 남는다. 되돌리면 재동의 요구도 풀린다.
113
113
  */
114
114
  export const policyVersionSchema = z
@@ -183,12 +183,12 @@ export const updateStoreRequestSchema = z
183
183
  refundPolicy: refundPolicyPatchSchema
184
184
  .optional()
185
185
  .describe(
186
- "스토어 기본 환불 정책 — 준 항목만 바꾼다. 상품별 정책(`products.refundPolicy`)이 없는 상품에 적용된다",
186
+ "상점 기본 환불 정책 — 준 항목만 바꾼다. 상품별 정책(`products.refundPolicy`)이 없는 상품에 적용된다",
187
187
  ),
188
188
  shippingPolicy: shippingPolicyPatchSchema
189
189
  .optional()
190
190
  .describe(
191
- "스토어 배송비 규칙 — 준 항목만 바꾼다. 기본 배송비, 전체 무료배송 기준액, 제주·도서산간 추가 배송비다. " +
191
+ "상점 배송비 규칙 — 준 항목만 바꾼다. 기본 배송비, 전체 무료배송 기준액, 제주·도서산간 추가 배송비다. " +
192
192
  "바꾼 값은 **새 주문서부터** 적용된다(이미 만든 주문서·주문의 배송비는 스냅샷이 원천이다)",
193
193
  ),
194
194
  termsVersion: policyVersionSchema
@@ -234,10 +234,10 @@ export const storeSchema = z.object({
234
234
  autoDeliverDays: z.number().int().describe(autoDeliverDaysDescription),
235
235
  autoConfirmDays: z.number().int().describe(autoConfirmDaysDescription),
236
236
  refundPolicy: refundPolicySchema.describe(
237
- "스토어 기본 환불 정책. 스토어가 정하지 않은 항목은 플랫폼 기본값으로 채워 항상 전부 싣는다",
237
+ "상점 기본 환불 정책. 상점이 정하지 않은 항목은 플랫폼 기본값으로 채워 항상 전부 싣는다",
238
238
  ),
239
239
  shippingPolicy: shippingPolicySchema.describe(
240
- "스토어 배송비 규칙. 스토어가 정하지 않은 항목은 플랫폼 기본값으로 채워 항상 전부 싣는다",
240
+ "상점 배송비 규칙. 상점이 정하지 않은 항목은 플랫폼 기본값으로 채워 항상 전부 싣는다",
241
241
  ),
242
242
  termsVersion: z.string().describe("현재 이용약관 버전. 가입·재동의가 이 값으로 이력에 남는다"),
243
243
  privacyVersion: z.string().describe("현재 개인정보 수집이용 동의 버전"),
@@ -270,7 +270,7 @@ export type StoreMember = z.infer<typeof storeMemberSchema>;
270
270
 
271
271
  /**
272
272
  * 초대 메일 발송 결과 — `SENDING`은 발송 요청 중(결과를 아직 적지 못함), `NOT_CONFIGURED`는 메일 발송이 설정되지 않아
273
- * 보내지 않은 것이다. 발송 실패는 초대를 무효로 만들지 않는다 — 재발송으로 다시 보낸다
273
+ * 보내지 않은 것이다. 상점의 월 메일 한도(요금제)를 다 써서 보내지 않았으면 `FAILED` + `deliveryErrorCode: MAIL_QUOTA_EXCEEDED`다. 발송 실패는 초대를 무효로 만들지 않는다 — 재발송으로 다시 보낸다
274
274
  */
275
275
  export const invitationDeliveryStatusSchema = z.enum([
276
276
  "SENDING",