@zalkera/client 0.6.1 → 0.6.2

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.cjs CHANGED
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  // src/errors.ts
4
- var OnequeError = class _OnequeError extends Error {
4
+ var ZalkeraError = class _ZalkeraError extends Error {
5
5
  /** HTTP 상태. 네트워크 실패·비JSON 응답이면 0. */
6
6
  status;
7
7
  /**
@@ -19,7 +19,7 @@ var OnequeError = class _OnequeError extends Error {
19
19
  body;
20
20
  constructor(message, options) {
21
21
  super(message, { cause: options.cause });
22
- this.name = "OnequeError";
22
+ this.name = "ZalkeraError";
23
23
  this.status = options.status;
24
24
  this.code = options.code ?? null;
25
25
  this.validationErrors = options.validationErrors ?? [];
@@ -29,36 +29,54 @@ var OnequeError = class _OnequeError extends Error {
29
29
  get isRateLimited() {
30
30
  return this.status === 429;
31
31
  }
32
+ /**
33
+ * 스토어프론트 시크릿 키 문제(memo78) — 개발자 오배선 신호. `true` 면 `secretKey` 옵션을
34
+ * 점검하라(미설정·불일치·폐기). [code] 로 정밀 분기: `STOREFRONT_KEY_REQUIRED`(401·키 필요)·
35
+ * `TENANT_MISMATCH`(403·키↔tenant 불일치).
36
+ */
37
+ get isStorefrontKeyError() {
38
+ return this.code === "STOREFRONT_KEY_REQUIRED" || this.code === "TENANT_MISMATCH";
39
+ }
32
40
  /** 응답 본문(파싱된 것)으로부터 에러를 만든다. */
33
41
  static fromBody(status, body) {
34
42
  if (isApiErrorBody(body)) {
35
- return new _OnequeError(body.message, {
36
- // errorCode(기계 판독)를 우선, 없으면 error(HTTP 사유구)로 폴백 — 구버전 백엔드 안전.
37
- status,
38
- code: body.errorCode ?? body.error,
39
- validationErrors: body.errors ?? [],
40
- body
41
- });
43
+ const code = body.errorCode ?? body.error;
44
+ return new _ZalkeraError(
45
+ // 스토어프론트 키 오배선(memo78)은 개발자용 안내로 메시지를 덮는다 — 백엔드는 정보 누출
46
+ // 최소화로 두루뭉술한 401 을 주므로, SDK 가 "무엇을 고쳐야 하는지"를 또렷이 알려준다.
47
+ STOREFRONT_KEY_MESSAGES[code] ?? body.message,
48
+ {
49
+ // errorCode(기계 판독)를 우선, 없으면 error(HTTP 사유구)로 폴백 — 구버전 백엔드 안전.
50
+ status,
51
+ code,
52
+ validationErrors: body.errors ?? [],
53
+ body
54
+ }
55
+ );
42
56
  }
43
57
  const gateway = status === 502 || status === 503 || status === 504;
44
- return new _OnequeError(
58
+ return new _ZalkeraError(
45
59
  gateway ? "\uC11C\uBE44\uC2A4\uC5D0 \uC77C\uC2DC\uC801\uC778 \uBB38\uC81C\uAC00 \uC788\uC2B5\uB2C8\uB2E4. \uC7A0\uC2DC \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD574\uC8FC\uC138\uC694." : `\uC694\uCCAD\uC774 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4 (HTTP ${status})`,
46
60
  { status, code: gateway ? "UPSTREAM_UNAVAILABLE" : null }
47
61
  );
48
62
  }
49
63
  };
64
+ var STOREFRONT_KEY_MESSAGES = {
65
+ STOREFRONT_KEY_REQUIRED: "\uC2A4\uD1A0\uC5B4\uD504\uB860\uD2B8 \uC2DC\uD06C\uB9BF \uD0A4\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4. createZalkeraClient \uC758 secretKey \uC635\uC158\uC744 \uC124\uC815\uD558\uC138\uC694 (\uD30C\uD2B8\uB108 \uCF58\uC194\uC5D0\uC11C \uBC1C\uAE09 \u2192 \uC11C\uBC84 .env \uC758 ONEQUE_STOREFRONT_KEY). \uBE0C\uB77C\uC6B0\uC800 \uBC88\uB4E4\uC5D0 \uB123\uC9C0 \uB9C8\uC138\uC694.",
66
+ TENANT_MISMATCH: "secretKey \uAC00 tenant \uC635\uC158\uACFC \uB2E4\uB978 \uD14C\uB10C\uD2B8\uC758 \uD0A4\uC785\uB2C8\uB2E4. \uB450 \uAC12\uC774 \uAC19\uC740 \uD14C\uB10C\uD2B8\uC778\uC9C0 \uD655\uC778\uD558\uC138\uC694."
67
+ };
50
68
  function isApiErrorBody(value) {
51
69
  return typeof value === "object" && value !== null && "message" in value && typeof value.message === "string";
52
70
  }
53
71
 
54
72
  // src/client.ts
55
73
  var DEFAULT_TIMEOUT_MS = 1e4;
56
- function createOnequeClient(options) {
74
+ function createZalkeraClient(options) {
57
75
  const baseUrl = options.baseUrl.replace(/\/+$/, "");
58
76
  const fetchImpl = options.fetch ?? globalThis.fetch;
59
77
  if (typeof fetchImpl !== "function") {
60
78
  throw new Error(
61
- "@oneque/client: \uC804\uC5ED fetch \uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. Node 18+ \uB97C \uC4F0\uAC70\uB098 options.fetch \uB97C \uC8FC\uC785\uD558\uC138\uC694."
79
+ "@zalkera/client: \uC804\uC5ED fetch \uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. Node 18+ \uB97C \uC4F0\uAC70\uB098 options.fetch \uB97C \uC8FC\uC785\uD558\uC138\uC694."
62
80
  );
63
81
  }
64
82
  const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
@@ -74,6 +92,7 @@ function createOnequeClient(options) {
74
92
  Accept: "application/json",
75
93
  ...options.headers
76
94
  };
95
+ if (options.secretKey) headers["X-Storefront-Key"] = options.secretKey;
77
96
  if (init?.body != null) headers["Content-Type"] = "application/json";
78
97
  if (init?.context?.clientIp) headers["X-Forwarded-For"] = init.context.clientIp;
79
98
  if (init?.bearer) headers["Authorization"] = `Bearer ${init.bearer}`;
@@ -93,7 +112,7 @@ function createOnequeClient(options) {
93
112
  response = await fetchImpl(url.toString(), fetchInit);
94
113
  } catch (cause) {
95
114
  const aborted = cause instanceof Error && cause.name === "AbortError";
96
- throw new OnequeError(
115
+ throw new ZalkeraError(
97
116
  aborted ? `\uC694\uCCAD \uC2DC\uAC04\uC774 \uCD08\uACFC\uB418\uC5C8\uC2B5\uB2C8\uB2E4 (${timeoutMs}ms)` : "\uB124\uD2B8\uC6CC\uD06C \uC694\uCCAD\uC774 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4.",
98
117
  { status: 0, cause }
99
118
  );
@@ -103,11 +122,11 @@ function createOnequeClient(options) {
103
122
  const text = await response.text();
104
123
  const parsed = text ? safeJsonParse(text) : null;
105
124
  if (!response.ok) {
106
- throw OnequeError.fromBody(response.status, parsed);
125
+ throw ZalkeraError.fromBody(response.status, parsed);
107
126
  }
108
127
  if (!text) return void 0;
109
128
  if (parsed === null || typeof parsed !== "object") {
110
- throw new OnequeError(
129
+ throw new ZalkeraError(
111
130
  `\uC11C\uBC84 \uC751\uB2F5 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. \uC7A0\uC2DC \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD574\uC8FC\uC138\uC694. (HTTP ${response.status})`,
112
131
  { status: 502, code: "UPSTREAM_NON_JSON" }
113
132
  );
@@ -241,7 +260,7 @@ function safeJsonParse(text) {
241
260
  }
242
261
  }
243
262
 
244
- exports.OnequeError = OnequeError;
245
- exports.createOnequeClient = createOnequeClient;
263
+ exports.ZalkeraError = ZalkeraError;
264
+ exports.createZalkeraClient = createZalkeraClient;
246
265
  //# sourceMappingURL=index.cjs.map
247
266
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/errors.ts","../src/client.ts"],"names":["options"],"mappings":"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","file":"index.cjs","sourcesContent":["import type {ApiErrorBody, ValidationError} from \"./types\";\n\n/**\n * Oneque API 호출 실패.\n *\n * 백엔드의 `ErrorResponse` 를 그대로 담는다.\n *\n * **원인 분기는 [code] 로 한다** — 한 엔드포인트가 같은 상태코드로 여러 원인을 낸다(checkout 의 409 =\n * `OUT_OF_STOCK`·`ITEM_NOT_PURCHASABLE`·`IDEMPOTENCY_CONFLICT` — 처방이 전부 다르다). `message` 는\n * 사람용 한국어라 **문자열 매칭 금지**.\n *\n * `status` 는 거친 분류에만:\n * - `400` + [validationErrors] — 입력값 문제(폼 필드별 메시지 노출)\n * - `404` — 없는 slug/리소스\n * - `429` — IP 레이트리밋(문의 남발). [isRateLimited] 로 편히 판별\n * - `5xx` — 서버 오류\n *\n * 네트워크 자체가 실패했거나 응답이 JSON 이 아니면 [status] 가 0 이고 [body] 가 null 이다.\n */\nexport class OnequeError extends Error {\n /** HTTP 상태. 네트워크 실패·비JSON 응답이면 0. */\n readonly status: number;\n /**\n * **기계 판독 에러 코드** — `ErrorResponse.errorCode`(백엔드 `ErrorCode` enum 이름).\n * 예: `OUT_OF_STOCK`·`IDEMPOTENCY_CONFLICT`·`CART_NOT_FOUND`. 이걸로 분기한다.\n *\n * 코드를 안 싣는 구버전 백엔드 응답에서는 `error`(HTTP 사유구, \"Conflict\")로 **폴백**한다 —\n * 그때는 원인을 가를 수 없으니, 폴백 값에 의존하는 분기를 짜지 마라.\n * 열린 계약이라 문자열이다(서버가 코드를 추가해도 SDK 릴리스가 필요 없다).\n */\n readonly code: string | null;\n /** 검증 실패 시 필드별 메시지. 그 외엔 빈 배열. */\n readonly validationErrors: ValidationError[];\n /** 파싱된 원본 에러 본문(있으면). */\n readonly body: ApiErrorBody | null;\n\n constructor(\n message: string,\n options: {\n status: number;\n code?: string | null;\n validationErrors?: ValidationError[];\n body?: ApiErrorBody | null;\n cause?: unknown;\n },\n ) {\n super(message, {cause: options.cause});\n this.name = \"OnequeError\";\n this.status = options.status;\n this.code = options.code ?? null;\n this.validationErrors = options.validationErrors ?? [];\n this.body = options.body ?? null;\n }\n\n /** IP 레이트리밋(429) — 문의 폼에서 \"잠시 후 다시\" 를 띄울 때 쓴다. */\n get isRateLimited(): boolean {\n return this.status === 429;\n }\n\n /** 응답 본문(파싱된 것)으로부터 에러를 만든다. */\n static fromBody(status: number, body: unknown): OnequeError {\n if (isApiErrorBody(body)) {\n return new OnequeError(body.message, {\n // errorCode(기계 판독)를 우선, 없으면 error(HTTP 사유구)로 폴백 — 구버전 백엔드 안전.\n status,\n code: body.errorCode ?? body.error,\n validationErrors: body.errors ?? [],\n body,\n });\n }\n // 비JSON 에러 바디(게이트웨이 HTML·빈 바디). 502/503/504 는 상류 장애로 안내 메시지 + 코드 부여.\n const gateway = status === 502 || status === 503 || status === 504;\n return new OnequeError(\n gateway ? \"서비스에 일시적인 문제가 있습니다. 잠시 후 다시 시도해주세요.\" : `요청이 실패했습니다 (HTTP ${status})`,\n {status, code: gateway ? \"UPSTREAM_UNAVAILABLE\" : null},\n );\n }\n}\n\nfunction isApiErrorBody(value: unknown): value is ApiErrorBody {\n return (\n typeof value === \"object\" &&\n value !== null &&\n \"message\" in value &&\n typeof (value as {message: unknown}).message === \"string\"\n );\n}\n","import {OnequeError} from \"./errors\";\nimport type {\n ApiResponse,\n AuthTokens,\n AvailabilityParams,\n AvailabilitySlot,\n Booking,\n Cart,\n Category,\n CheckoutInput,\n CreateBookingInput,\n CreateReviewInput,\n ConsentInput,\n ConsentStatus,\n CustomerSummary,\n InquiryCreated,\n InquiryInput,\n LeadCreated,\n LeadInput,\n ListPostsParams,\n ListReviewsParams,\n MediaUrl,\n Menu,\n OrderDetail,\n OrderSummary,\n PageContent,\n ListProductsParams,\n Paginated,\n PaymentSession,\n PostDetail,\n PostSummary,\n ProductCategory,\n ProductDetail,\n ProductSummary,\n RatingSummary,\n Review,\n ShipmentInfo,\n SiteConfig,\n SocialLoginInput,\n} from \"./types\";\n\n/**\n * ISR 읽기 옵션 — Next.js 캐시 태그(memo31 §0-1). RSC/ISR 페이지에서 읽기 메서드에 넘기면\n * 그 fetch 에 `next.tags` 가 실려, 백엔드가 `revalidateTag(tag)` 로 온디맨드 무효화할 수 있다.\n * 태그 컨벤션: `site-config`(사이트 설정·테마·레이아웃), `products`(카탈로그), `product:{slug}`(특정 상품).\n * 넘기지 않으면 세그먼트 기본 캐시(페이지의 `revalidate` 주기)만 적용된다 — 하위호환 유지.\n */\nexport interface ReadOptions {\n /** Next ISR 캐시 태그. 이 태그로 백엔드가 온디맨드 revalidate 한다. */\n tags?: string[];\n}\n\nexport interface OnequeClientOptions {\n /**\n * 백엔드 베이스 URL — `/api` 접두사는 붙이지 않는다. 예: `http://localhost:8100`.\n * 클라이언트가 경로에 `/api/public/...` 를 붙인다.\n */\n baseUrl: string;\n /**\n * 테넌트 코드. 모든 요청에 `X-Tenant` 헤더로 실린다.\n *\n * 공개 API 는 이 헤더를 그대로 믿는다(비인증). 그래서 이 클라이언트는 **서버 사이드**\n * (RSC·route handler·server action)에서 쓰는 것을 전제로 한다 — 브라우저에서 직접 부르면\n * baseUrl 이 노출되고 CORS 를 열어야 한다. README 참고.\n */\n tenant: string;\n /**\n * `fetch` 구현 주입(선택). 기본은 전역 `fetch`(Node 18+·브라우저). 테스트·커스텀 에이전트·\n * Next.js 의 `fetch` 캐시 옵션을 감싸는 래퍼를 넣을 때 쓴다.\n */\n fetch?: typeof fetch;\n /** 모든 요청에 추가할 헤더(선택). */\n headers?: Record<string, string>;\n /** 요청 타임아웃(ms). 기본 10초. */\n timeoutMs?: number;\n}\n\nconst DEFAULT_TIMEOUT_MS = 10_000;\n\n/**\n * Oneque 공개 API 클라이언트.\n *\n * 테넌트 사이트(credium 등)가 백엔드의 공개 엔드포인트를 타입 안전하게 부르기 위한 얇은 래퍼다.\n * 런타임 의존성이 없다 — 전역 `fetch` 만 쓴다.\n *\n * ```ts\n * const cms = createOnequeClient({ baseUrl: process.env.API_BASE_URL!, tenant: \"credium\" });\n * const posts = await cms.listPosts({ size: 10, sort: \"publishedAt,desc\" });\n * await cms.submitInquiry({ name, email, subject, message });\n * ```\n *\n * 모든 메서드는 성공 시 envelope 안쪽 `data` 를 돌려주고, 실패 시 [OnequeError] 를 던진다.\n */\nexport interface OnequeClient {\n /** 회사 정보·테마·SEO 기본값. ISR 페이지는 [ReadOptions.tags] 로 캐시 태그를 실을 수 있다. */\n getSiteConfig(options?: ReadOptions): Promise<SiteConfig>;\n /** 살아 있는 카테고리 전부(페이징 없음). */\n listCategories(): Promise<Category[]>;\n /** 발행된 글 목록(페이징). */\n listPosts(params?: ListPostsParams): Promise<Paginated<PostSummary>>;\n /** slug 로 글 상세(본문 포함). 없으면 404 → [OnequeError]. */\n getPost(slug: string): Promise<PostDetail>;\n /**\n * 조회 비콘 — 같은 뷰어의 같은 날 재조회는 집계되지 않는다(반환 false).\n * 화면 렌더를 막지 않도록 fire-and-forget 으로 부르길 권한다(실패해도 페이지는 살아야 한다).\n *\n * 조회 dedup 은 뷰어 IP·UA 를 쓴다. 서버 사이드에서 부를 때는 [RequestContext.clientIp] 로\n * 원 방문자 IP 를 넘겨야 방문자별로 집계된다 — 안 넘기면 전부 서버 IP 하나로 뭉친다.\n */\n recordPostView(slug: string, context?: RequestContext): Promise<boolean>;\n /** slug 로 고정 페이지. */\n /**\n * slug 로 고정 페이지(섹션 포함). 없으면 404 → [OnequeError].\n * ISR 페이지는 [ReadOptions.tags] 로 캐시 태그를 실을 수 있다 — 백엔드가 발행 시 그 태그만\n * 콕 집어 revalidate 한다.\n */\n getPage(slug: string, options?: ReadOptions): Promise<PageContent>;\n /** 메뉴 트리(HEADER·FOOTER). */\n listMenus(options?: ReadOptions): Promise<Menu[]>;\n /** 미디어 presigned 다운로드 URL(만료 있음). */\n getMediaUrl(id: number): Promise<MediaUrl>;\n /**\n * 문의 접수. 성공 시 생성된 문의 id. 레이트리밋이면 429 → `error.isRateLimited`.\n *\n * ⚠️ 서버 사이드(테넌트 route handler)에서 부를 때는 [RequestContext.clientIp] 로 **원 방문자\n * IP 를 반드시 넘겨야 한다.** 백엔드의 문의 레이트리밋·IP 기록은 `X-Forwarded-For` 첫 홉을\n * 쓰는데, 안 넘기면 백엔드가 테넌트 서버 IP 하나만 보고 몇 건 뒤 **모든 방문자**를 429 로 막는다.\n */\n submitInquiry(input: InquiryInput, context?: RequestContext): Promise<InquiryCreated>;\n /**\n * 광고 리드 접수. 문의와 달리 이메일이 선택이고 UTM 추적을 함께 보낸다.\n *\n * ⚠️ [submitInquiry] 와 같은 이유로 서버 사이드에서는 [RequestContext.clientIp] 로 원 방문자\n * IP 를 넘겨야 한다 — 백엔드 리드 레이트리밋·IP 기록이 그 값을 본다.\n */\n submitLead(input: LeadInput, context?: RequestContext): Promise<LeadCreated>;\n\n // ── 커머스: 카탈로그(공개) ──────────────────────────────────\n /** slug 로 공개 상품(ACTIVE) 상세 — variant·재고 가용여부 포함. 없으면 404. ISR 태그는 [ReadOptions]. */\n getProduct(slug: string, options?: ReadOptions): Promise<ProductDetail>;\n /** 공개 상품 목록(ACTIVE) — 카드용 요약(최저가·재고). ISR 태그는 [ReadOptions]. */\n listProducts(params?: ListProductsParams, options?: ReadOptions): Promise<Paginated<ProductSummary>>;\n /** 커머스 카테고리 목록(노출 순서). ISR 태그는 [ReadOptions]. */\n listProductCategories(options?: ReadOptions): Promise<ProductCategory[]>;\n\n // ── 커머스: 상품후기(공개) ──────────────────────────────────\n /**\n * 상품 후기 목록(공개·VISIBLE 만, 최신순, 페이징).\n *\n * 상품의 **숫자 id(productId)** 로 조회한다 — slug 가 아니다. `getProduct(slug).id` 로 획득한다(0.4.0).\n */\n listProductReviews(productId: number, params?: ListReviewsParams, options?: ReadOptions): Promise<Paginated<Review>>;\n /** 상품 평점 요약(후기 수·평균 별점). 후기 없으면 count=0·avg=0. 목록 별점 배지에 값싸게 쓴다. */\n getProductReviewSummary(productId: number, options?: ReadOptions): Promise<RatingSummary>;\n /**\n * 후기 작성 — **로그인 필수 + 구매검증**. `input.orderItemId` 는 [OrderItemLine.id](주문 상세에서 획득).\n * 서버가 본인 주문·배송완료(DELIVERED) 이상·상품 일치를 3중 확인한다. 라인당 1개(재작성 시 409 `ALREADY_REVIEWED`).\n */\n createProductReview(productId: number, input: CreateReviewInput, accessToken: string): Promise<Review>;\n\n // ── 커머스: 고객 인증 ───────────────────────────────────────\n /** 소셜 로그인 — authorization code 를 백엔드가 교환하고 고객 토큰을 발급. */\n socialLogin(input: SocialLoginInput): Promise<AuthTokens>;\n /** refresh 토큰으로 새 토큰 회전(옛 refresh 는 폐기). */\n refreshSession(refreshToken: string): Promise<AuthTokens>;\n /** 로그아웃 — 이 고객의 모든 세션 폐기. */\n logout(accessToken: string): Promise<string>;\n /** 내 정보. */\n getMe(accessToken: string): Promise<CustomerSummary>;\n /** 현재 동의 상태 조회(로그인 필수) — 마케팅 수신 토글 화면 등에서 초기값으로 쓴다. */\n getConsents(accessToken: string): Promise<ConsentStatus[]>;\n /**\n * 동의 갱신(로그인 필수, append) — 마케팅 수신 on/off 등. 넘긴 항목만 반영되고 나머지는 유지된다.\n * 성공 시 백엔드 확인 메시지를 돌려준다(최신 상태가 필요하면 [getConsents] 로 다시 읽는다).\n */\n updateConsents(accessToken: string, consents: ConsentInput[]): Promise<string>;\n\n // ── 예약(SERVICE 상품) ──────────────────────────────────────\n /**\n * 예약 가능한 슬롯 조회 — **비로그인 가능**(상품 상세에서 시간표를 보여주는 자리).\n *\n * 미래·잔여정원 있는 OPEN 슬롯만 온다. 가용 판정은 [AvailabilitySlot.availableCount] 로 하고,\n * 카탈로그의 `inStock`/`available` 을 쓰지 마라 — 예약은 슬롯마다 정원이 따로다.\n */\n availability(params: AvailabilityParams, options?: ReadOptions): Promise<AvailabilitySlot[]>;\n /**\n * 예약 생성 — 슬롯 정원을 원자적으로 소비한다. **로그인 필수**(게스트 예약 없음).\n *\n * 무료 예약은 즉시 `CONFIRMED`. **유료·예약금이면 `status=PENDING` + [Booking.orderNo] 가 실려 오고**,\n * 그 orderNo 로 [startPayment] 를 불러 **기존 결제 흐름을 그대로 탄다**(예약 전용 결제 API 는 없다).\n *\n * 정원 소진은 `SLOT_FULL` — 슬롯 목록이 stale 했다는 뜻이니 [availability] 를 다시 읽어 보여줘라.\n */\n createBooking(accessToken: string, input: CreateBookingInput): Promise<Booking>;\n /** 내 예약 목록(최신순). 로그인 필수. */\n myBookings(accessToken: string): Promise<Booking[]>;\n /** 예약 취소 — 시작 전·무료 예약만. 정원이 복원된다. 로그인 필수. */\n cancelBooking(accessToken: string, bookingCode: string): Promise<string>;\n /** 같은 상품의 다른 슬롯으로 이동(신 슬롯 소비·구 슬롯 복원). 로그인 필수. */\n rescheduleBooking(accessToken: string, bookingCode: string, newSlotId: number): Promise<Booking>;\n\n // ── 커머스: 장바구니 ────────────────────────────────────────\n /**\n * 장바구니 조회. 로그인은 accessToken, 게스트는 cartSessionKey 로 식별.\n *\n * ⚠️ **예약(SERVICE) 상품은 장바구니에 담기지 않는다** — [addToCart] 가 400 으로 거부한다.\n * 예약은 [createBooking] 으로 별도 흐름을 탄다(카트에 슬롯을 실으면 체크아웃이 오염된다).\n */\n getCart(session: ShopSession): Promise<Cart>;\n /**\n * 담기(같은 variant 는 수량 누적).\n *\n * ⚠️ **예약(SERVICE) 상품은 400 으로 거부된다** — 예약은 [createBooking] 이다. 상품 상세에서\n * `productType === \"SERVICE\"` 면 담기 버튼 대신 슬롯 선택을 그려라.\n */\n addToCart(variantId: number, quantity: number, session: ShopSession): Promise<Cart>;\n /** 수량 변경. */\n updateCartItem(variantId: number, quantity: number, session: ShopSession): Promise<Cart>;\n /** 항목 삭제. */\n removeFromCart(variantId: number, session: ShopSession): Promise<Cart>;\n /** 비우기. */\n clearCart(session: ShopSession): Promise<Cart>;\n\n // ── 커머스: 결제·주문·배송 ──────────────────────────────────\n /**\n * 결제(주문 생성) — 스냅샷 동결 + 재고 차감. 상태=PENDING_PAYMENT.\n *\n * [idempotencyKey] 를 주면 **같은 키·같은 입력의 재요청이 새 주문을 만들지 않고 원주문을 그대로\n * 반환**한다(더블클릭·네트워크 재시도 안전). 같은 키로 **다른 입력**을 보내면 409.\n *\n * **키는 \"이 체크아웃 1건\"에 고정된 값이어야 한다** — 호출마다 새로 만들면 재시도가 서로 다른\n * 키를 들고 가서 아무것도 막지 못한다(그래서 SDK 가 자동 생성해 주지 않는다). 안정적인 소스를\n * 쓴다: 게스트는 **장바구니 세션 키**(카트 1개 → 주문 1건이라 의미가 정확히 맞는다),\n * 그 외에는 결제 시도 시작 시 한 번 만들어 재시도 동안 보관한 값.\n *\n * 안 주면 종전 동작 — 중복 제출은 카트가 이미 소비돼 404(`CART_NOT_FOUND`)로 떨어진다.\n */\n checkout(input: CheckoutInput, session: ShopSession, idempotencyKey?: string): Promise<OrderDetail>;\n /**\n * 결제 시작. **벤더에 따라 두 갈래**(테넌트가 자기 PG 를 고른다 — 기본 TOSS):\n * `session.widget` 이 있으면 위젯형(내 사이트에서 결제창 → [confirmPayment] 로 승인 확정),\n * 없으면 리다이렉트형(`session.paymentUrl` 로 보내면 끝).\n */\n startPayment(orderNo: string, access: OrderAccess): Promise<PaymentSession>;\n /**\n * **위젯형 벤더의 승인 확정**(토스 등). 결제창이 성공 URL 로 돌려준 파라미터를 그대로 넘긴다\n * (토스: `{paymentKey, orderId, amount}` — URL 쿼리에서 읽어 통째로).\n *\n * **금액을 넘겨도 서버는 안 믿는다** — 서버가 저장한 주문 금액으로 PG 에 직접 묻는다. 그래서\n * 브라우저에서 금액을 조작해도 승인되지 않는다.\n *\n * 이미 승인된 주문(웹훅 선착)이면 조용히 통과한다 — 재호출이 안전하다.\n * 리다이렉트형 벤더에 부르면 404(`PG_CONFIG_NOT_FOUND`).\n */\n confirmPayment(orderNo: string, providerParams: Record<string, string>, access: OrderAccess): Promise<void>;\n /** 주문 조회 — 본인(accessToken) 또는 게스트(phone). */\n getOrder(orderNo: string, access: OrderAccess): Promise<OrderDetail>;\n /** 내 주문 목록(로그인 필수). */\n listMyOrders(accessToken: string, params?: {page?: number; size?: number}): Promise<Paginated<OrderSummary>>;\n /** 주문 취소(미결제만) — 본인 또는 게스트(phone). */\n cancelOrder(orderNo: string, access: OrderAccess): Promise<OrderDetail>;\n /** 구매 확정(배송완료 후) — 본인 또는 게스트(phone). */\n completeOrder(orderNo: string, access: OrderAccess): Promise<OrderDetail>;\n /** 배송 조회 — 본인 또는 게스트(phone). */\n getShipment(orderNo: string, access: OrderAccess): Promise<ShipmentInfo>;\n}\n\n/**\n * 장바구니·결제의 사용자 식별. 로그인 고객은 [accessToken], 게스트는 [cartSessionKey]\n * (브라우저가 만든 안정적 익명 키)를 넘긴다. 로그인 시 게스트 카트→고객 카트 병합은 백엔드 몫.\n */\nexport interface ShopSession {\n accessToken?: string;\n cartSessionKey?: string;\n}\n\n/**\n * 주문 조회·취소·배송조회의 접근 크리덴셜. 로그인 고객은 [accessToken] 으로 소유권을,\n * 게스트는 주문 시 남긴 [phone] 으로 확인한다(주문번호 + 연락처).\n */\nexport interface OrderAccess {\n accessToken?: string;\n phone?: string;\n}\n\n/**\n * IP 민감 엔드포인트(문의·조회 비콘)에서 원 방문자를 백엔드에 알리는 컨텍스트.\n *\n * 테넌트 사이트는 서버 사이드에서 이 클라이언트를 부르므로, 백엔드가 보는 소스 IP 는 방문자가\n * 아니라 테넌트 서버다. 방문자 IP 를 `X-Forwarded-For` 로 실어 백엔드가 방문자별로 판단하게 한다.\n */\nexport interface RequestContext {\n /** 원 방문자 IP — route handler 에서 요청 헤더(x-forwarded-for·x-real-ip)로 뽑아 넘긴다. */\n clientIp?: string;\n}\n\nexport function createOnequeClient(options: OnequeClientOptions): OnequeClient {\n const baseUrl = options.baseUrl.replace(/\\/+$/, \"\");\n const fetchImpl = options.fetch ?? globalThis.fetch;\n if (typeof fetchImpl !== \"function\") {\n throw new Error(\n \"@oneque/client: 전역 fetch 가 없습니다. Node 18+ 를 쓰거나 options.fetch 를 주입하세요.\",\n );\n }\n const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n\n async function request<T>(\n path: string,\n init?: {\n method?: string;\n body?: unknown;\n query?: Record<string, unknown>;\n context?: RequestContext;\n /** 고객 access 토큰 → Authorization: Bearer. */\n bearer?: string;\n /** 게스트 장바구니 키 → X-Cart-Session. */\n cartSession?: string;\n /** 멱등키 → Idempotency-Key. 같은 키·같은 본문의 재요청은 원주문을 재생한다. */\n idempotencyKey?: string;\n /** Next.js ISR 캐시 옵션 — RSC/ISR fetch 에 `next.tags` 를 실어 온디맨드 revalidate 대상으로 만든다. */\n next?: {tags?: string[]};\n },\n ): Promise<T> {\n const url = new URL(`${baseUrl}/api${path}`);\n if (init?.query) {\n for (const [key, value] of Object.entries(init.query)) {\n if (value != null) url.searchParams.set(key, String(value));\n }\n }\n\n const headers: Record<string, string> = {\n \"X-Tenant\": options.tenant,\n Accept: \"application/json\",\n ...options.headers,\n };\n if (init?.body != null) headers[\"Content-Type\"] = \"application/json\";\n // 원 방문자 IP 를 백엔드에 전달 — 문의 레이트리밋·조회 dedup·IP 기록이 이걸 본다.\n if (init?.context?.clientIp) headers[\"X-Forwarded-For\"] = init.context.clientIp;\n // 커머스 인증 — 로그인 고객은 Bearer, 게스트 장바구니는 세션 키.\n if (init?.bearer) headers[\"Authorization\"] = `Bearer ${init.bearer}`;\n if (init?.cartSession) headers[\"X-Cart-Session\"] = init.cartSession;\n if (init?.idempotencyKey) headers[\"Idempotency-Key\"] = init.idempotencyKey;\n\n // 타임아웃 — 행 걸린 연결이 페이지 렌더를 무한정 붙들지 않게.\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n\n let response: Response;\n try {\n // Next.js 는 표준 RequestInit 에 비표준 `next`(캐시 태그/주기)를 확장한다. 표준 DOM 타입엔\n // 없으므로 교차 타입으로 선언해 (있을 때만) 실어 보낸다 — 일반 fetch 는 이 필드를 무시한다.\n const fetchInit: RequestInit & {next?: {tags?: string[]}} = {\n method: init?.method ?? \"GET\",\n headers,\n body: init?.body != null ? JSON.stringify(init.body) : undefined,\n signal: controller.signal,\n };\n if (init?.next) fetchInit.next = init.next;\n response = await fetchImpl(url.toString(), fetchInit);\n } catch (cause) {\n const aborted = cause instanceof Error && cause.name === \"AbortError\";\n throw new OnequeError(\n aborted ? `요청 시간이 초과되었습니다 (${timeoutMs}ms)` : \"네트워크 요청이 실패했습니다.\",\n {status: 0, cause},\n );\n } finally {\n clearTimeout(timer);\n }\n\n // 204 등 본문 없는 응답은 파싱하지 않는다.\n const text = await response.text();\n const parsed = text ? safeJsonParse(text) : null;\n\n if (!response.ok) {\n throw OnequeError.fromBody(response.status, parsed);\n }\n // 빈 바디(204·void 엔드포인트 예: confirmPayment)는 정상 — data 접근 없이 통과.\n if (!text) return undefined as T;\n // 성공(2xx)인데 envelope 가 아니면(HTML·비객체 JSON) 게이트웨이/프록시 오염이다.\n // (null).data TypeError 로 새지 않게 구조화 에러로 변환 — BFF errorResponse 가 502 로 서빙(memo 57 §11-4).\n if (parsed === null || typeof parsed !== \"object\") {\n throw new OnequeError(\n `서버 응답 형식이 올바르지 않습니다. 잠시 후 다시 시도해주세요. (HTTP ${response.status})`,\n {status: 502, code: \"UPSTREAM_NON_JSON\"},\n );\n }\n return (parsed as ApiResponse<T>).data;\n }\n\n return {\n getSiteConfig: (options) => request<SiteConfig>(\"/public/site-config\", nextInit(options)),\n listCategories: () => request<Category[]>(\"/public/categories\"),\n listPosts: (params) =>\n request<Paginated<PostSummary>>(\"/public/posts\", {\n query: {\n category: params?.category,\n page: params?.page,\n size: params?.size,\n sort: params?.sort,\n },\n }),\n getPost: (slug) => request<PostDetail>(`/public/posts/${encodeURIComponent(slug)}`),\n recordPostView: (slug, context) =>\n request<boolean>(`/public/posts/${encodeURIComponent(slug)}/view`, {\n method: \"POST\",\n context,\n }),\n getPage: (slug, options) =>\n request<PageContent>(`/public/pages/${encodeURIComponent(slug)}`, nextInit(options)),\n listMenus: (options) =>\n request<Menu[]>(\"/public/menus\", {next: options?.tags ? {tags: options.tags} : undefined}),\n getMediaUrl: (id) => request<MediaUrl>(`/public/media/${id}/url`),\n submitInquiry: (input, context) =>\n request<InquiryCreated>(\"/public/inquiries\", {\n method: \"POST\",\n body: input,\n context,\n }),\n submitLead: (input, context) =>\n request<LeadCreated>(\"/public/leads\", {\n method: \"POST\",\n body: input,\n context,\n }),\n\n // ── 커머스: 카탈로그 ────────────────────────────────────\n getProduct: (slug, options) =>\n request<ProductDetail>(`/public/products/${encodeURIComponent(slug)}`, nextInit(options)),\n listProducts: (params, options) =>\n request<Paginated<ProductSummary>>(\"/public/products\", {\n query: {\n productType: params?.productType,\n keyword: params?.keyword,\n page: params?.page,\n size: params?.size,\n sort: params?.sort,\n },\n ...nextInit(options),\n }),\n listProductCategories: (options) =>\n request<ProductCategory[]>(\"/public/product-categories\", nextInit(options)),\n\n // ── 커머스: 상품후기(공개) ──────────────────────────────\n listProductReviews: (productId, params, options) =>\n request<Paginated<Review>>(`/public/products/${productId}/reviews`, {\n query: {page: params?.page, size: params?.size},\n next: options?.tags ? {tags: options.tags} : undefined,\n }),\n getProductReviewSummary: (productId, options) =>\n request<RatingSummary>(`/public/products/${productId}/reviews/summary`, {\n next: options?.tags ? {tags: options.tags} : undefined,\n }),\n createProductReview: (productId, input, accessToken) =>\n request<Review>(`/shop/products/${productId}/reviews`, {method: \"POST\", body: input, bearer: accessToken}),\n\n // ── 커머스: 고객 인증 ───────────────────────────────────\n socialLogin: (input) => request<AuthTokens>(\"/shop/auth/social\", {method: \"POST\", body: input}),\n refreshSession: (refreshToken) =>\n request<AuthTokens>(\"/shop/auth/refresh\", {method: \"POST\", body: {refreshToken}}),\n logout: (accessToken) => request<string>(\"/shop/auth/logout\", {method: \"POST\", bearer: accessToken}),\n getMe: (accessToken) => request<CustomerSummary>(\"/shop/me\", {bearer: accessToken}),\n getConsents: (accessToken) => request<ConsentStatus[]>(\"/shop/consents\", {bearer: accessToken}),\n updateConsents: (accessToken, consents) =>\n request<string>(\"/shop/consents\", {method: \"POST\", bearer: accessToken, body: {consents}}),\n\n // ── 예약(SERVICE 상품) ───────────────────────────────────\n availability: (params, options) =>\n request<AvailabilitySlot[]>(\"/public/booking/availability\", {\n query: {productId: params.productId, from: params.from, to: params.to},\n next: options?.tags ? {tags: options.tags} : undefined,\n }),\n createBooking: (accessToken, input) =>\n request<Booking>(\"/shop/booking/bookings\", {method: \"POST\", body: input, bearer: accessToken}),\n myBookings: (accessToken) => request<Booking[]>(\"/shop/booking/bookings\", {bearer: accessToken}),\n cancelBooking: (accessToken, bookingCode) =>\n request<string>(`/shop/booking/bookings/${encodeURIComponent(bookingCode)}/cancel`, {\n method: \"POST\",\n bearer: accessToken,\n }),\n rescheduleBooking: (accessToken, bookingCode, newSlotId) =>\n request<Booking>(`/shop/booking/bookings/${encodeURIComponent(bookingCode)}/reschedule`, {\n method: \"POST\",\n body: {newSlotId},\n bearer: accessToken,\n }),\n\n // ── 커머스: 장바구니 ────────────────────────────────────\n getCart: (session) => request<Cart>(\"/shop/cart\", shopInit(session)),\n addToCart: (variantId, quantity, session) =>\n request<Cart>(\"/shop/cart/items\", shopInit(session, {method: \"POST\", body: {variantId, quantity}})),\n updateCartItem: (variantId, quantity, session) =>\n request<Cart>(`/shop/cart/items/${variantId}`, shopInit(session, {method: \"PATCH\", body: {quantity}})),\n removeFromCart: (variantId, session) =>\n request<Cart>(`/shop/cart/items/${variantId}`, shopInit(session, {method: \"DELETE\"})),\n clearCart: (session) => request<Cart>(\"/shop/cart\", shopInit(session, {method: \"DELETE\"})),\n\n // ── 커머스: 결제·주문·배송 ──────────────────────────────\n checkout: (input, session, idempotencyKey) =>\n request<OrderDetail>(\"/shop/checkout\", shopInit(session, {method: \"POST\", body: input, idempotencyKey})),\n confirmPayment: (orderNo, providerParams, access) =>\n request<void>(\n `/shop/orders/${encodeURIComponent(orderNo)}/payment/confirm`,\n accessInit(access, {method: \"POST\", body: providerParams}),\n ),\n startPayment: (orderNo, access) =>\n request<PaymentSession>(\n `/shop/orders/${encodeURIComponent(orderNo)}/payment/session`,\n accessInit(access, {method: \"POST\"}),\n ),\n getOrder: (orderNo, access) =>\n request<OrderDetail>(`/shop/orders/${encodeURIComponent(orderNo)}`, accessInit(access)),\n listMyOrders: (accessToken, params) =>\n request<Paginated<OrderSummary>>(\"/shop/orders\", {\n bearer: accessToken,\n query: {page: params?.page, size: params?.size},\n }),\n cancelOrder: (orderNo, access) =>\n request<OrderDetail>(\n `/shop/orders/${encodeURIComponent(orderNo)}/cancel`,\n accessInit(access, {method: \"POST\"}),\n ),\n completeOrder: (orderNo, access) =>\n request<OrderDetail>(\n `/shop/orders/${encodeURIComponent(orderNo)}/complete`,\n accessInit(access, {method: \"POST\"}),\n ),\n getShipment: (orderNo, access) =>\n request<ShipmentInfo>(`/shop/orders/${encodeURIComponent(orderNo)}/shipment`, accessInit(access)),\n };\n}\n\n/** [ReadOptions] 를 request init 의 Next 캐시 옵션으로 변환. 태그 없으면 빈 객체(하위호환). */\nfunction nextInit(options?: ReadOptions): {next?: {tags?: string[]}} {\n return options?.tags && options.tags.length > 0 ? {next: {tags: options.tags}} : {};\n}\n\n/** 장바구니·결제 요청의 사용자 식별을 request init 으로 변환. */\nfunction shopInit(\n session: ShopSession,\n extra?: {method?: string; body?: unknown; idempotencyKey?: string},\n): {method?: string; body?: unknown; bearer?: string; cartSession?: string; idempotencyKey?: string} {\n return {...extra, bearer: session.accessToken, cartSession: session.cartSessionKey};\n}\n\n/** 주문 접근(본인 토큰 또는 게스트 phone)을 request init 으로 변환. */\nfunction accessInit(\n access: OrderAccess,\n extra?: {method?: string; body?: unknown},\n): {method?: string; body?: unknown; bearer?: string; query?: Record<string, unknown>} {\n return {\n ...extra,\n bearer: access.accessToken,\n query: access.phone ? {phone: access.phone} : undefined,\n };\n}\n\nfunction safeJsonParse(text: string): unknown {\n try {\n return JSON.parse(text);\n } catch {\n return null;\n }\n}\n"]}
1
+ {"version":3,"sources":["../src/errors.ts","../src/client.ts"],"names":["options"],"mappings":"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","file":"index.cjs","sourcesContent":["import type {ApiErrorBody, ValidationError} from \"./types\";\n\n/**\n * zalkera API 호출 실패.\n *\n * 백엔드의 `ErrorResponse` 를 그대로 담는다.\n *\n * **원인 분기는 [code] 로 한다** — 한 엔드포인트가 같은 상태코드로 여러 원인을 낸다(checkout 의 409 =\n * `OUT_OF_STOCK`·`ITEM_NOT_PURCHASABLE`·`IDEMPOTENCY_CONFLICT` — 처방이 전부 다르다). `message` 는\n * 사람용 한국어라 **문자열 매칭 금지**.\n *\n * `status` 는 거친 분류에만:\n * - `400` + [validationErrors] — 입력값 문제(폼 필드별 메시지 노출)\n * - `404` — 없는 slug/리소스\n * - `429` — IP 레이트리밋(문의 남발). [isRateLimited] 로 편히 판별\n * - `5xx` — 서버 오류\n *\n * 네트워크 자체가 실패했거나 응답이 JSON 이 아니면 [status] 가 0 이고 [body] 가 null 이다.\n */\nexport class ZalkeraError extends Error {\n /** HTTP 상태. 네트워크 실패·비JSON 응답이면 0. */\n readonly status: number;\n /**\n * **기계 판독 에러 코드** — `ErrorResponse.errorCode`(백엔드 `ErrorCode` enum 이름).\n * 예: `OUT_OF_STOCK`·`IDEMPOTENCY_CONFLICT`·`CART_NOT_FOUND`. 이걸로 분기한다.\n *\n * 코드를 안 싣는 구버전 백엔드 응답에서는 `error`(HTTP 사유구, \"Conflict\")로 **폴백**한다 —\n * 그때는 원인을 가를 수 없으니, 폴백 값에 의존하는 분기를 짜지 마라.\n * 열린 계약이라 문자열이다(서버가 코드를 추가해도 SDK 릴리스가 필요 없다).\n */\n readonly code: string | null;\n /** 검증 실패 시 필드별 메시지. 그 외엔 빈 배열. */\n readonly validationErrors: ValidationError[];\n /** 파싱된 원본 에러 본문(있으면). */\n readonly body: ApiErrorBody | null;\n\n constructor(\n message: string,\n options: {\n status: number;\n code?: string | null;\n validationErrors?: ValidationError[];\n body?: ApiErrorBody | null;\n cause?: unknown;\n },\n ) {\n super(message, {cause: options.cause});\n this.name = \"ZalkeraError\";\n this.status = options.status;\n this.code = options.code ?? null;\n this.validationErrors = options.validationErrors ?? [];\n this.body = options.body ?? null;\n }\n\n /** IP 레이트리밋(429) — 문의 폼에서 \"잠시 후 다시\" 를 띄울 때 쓴다. */\n get isRateLimited(): boolean {\n return this.status === 429;\n }\n\n /**\n * 스토어프론트 시크릿 키 문제(memo78) — 개발자 오배선 신호. `true` 면 `secretKey` 옵션을\n * 점검하라(미설정·불일치·폐기). [code] 로 정밀 분기: `STOREFRONT_KEY_REQUIRED`(401·키 필요)·\n * `TENANT_MISMATCH`(403·키↔tenant 불일치).\n */\n get isStorefrontKeyError(): boolean {\n return this.code === \"STOREFRONT_KEY_REQUIRED\" || this.code === \"TENANT_MISMATCH\";\n }\n\n /** 응답 본문(파싱된 것)으로부터 에러를 만든다. */\n static fromBody(status: number, body: unknown): ZalkeraError {\n if (isApiErrorBody(body)) {\n const code = body.errorCode ?? body.error;\n return new ZalkeraError(\n // 스토어프론트 키 오배선(memo78)은 개발자용 안내로 메시지를 덮는다 — 백엔드는 정보 누출\n // 최소화로 두루뭉술한 401 을 주므로, SDK 가 \"무엇을 고쳐야 하는지\"를 또렷이 알려준다.\n STOREFRONT_KEY_MESSAGES[code as string] ?? body.message,\n {\n // errorCode(기계 판독)를 우선, 없으면 error(HTTP 사유구)로 폴백 — 구버전 백엔드 안전.\n status,\n code,\n validationErrors: body.errors ?? [],\n body,\n },\n );\n }\n // 비JSON 에러 바디(게이트웨이 HTML·빈 바디). 502/503/504 는 상류 장애로 안내 메시지 + 코드 부여.\n const gateway = status === 502 || status === 503 || status === 504;\n return new ZalkeraError(\n gateway ? \"서비스에 일시적인 문제가 있습니다. 잠시 후 다시 시도해주세요.\" : `요청이 실패했습니다 (HTTP ${status})`,\n {status, code: gateway ? \"UPSTREAM_UNAVAILABLE\" : null},\n );\n }\n}\n\n/**\n * 스토어프론트 시크릿 키(memo78) 관련 에러코드 → 개발자용 안내 메시지. 백엔드 원문 메시지를 이걸로\n * 덮는다(백엔드는 열거 방지로 두루뭉술하게 답한다 — SDK 가 처방을 또렷이 준다).\n */\nconst STOREFRONT_KEY_MESSAGES: Record<string, string> = {\n STOREFRONT_KEY_REQUIRED:\n \"스토어프론트 시크릿 키가 필요합니다. createZalkeraClient 의 secretKey 옵션을 설정하세요 \" +\n \"(파트너 콘솔에서 발급 → 서버 .env 의 ONEQUE_STOREFRONT_KEY). 브라우저 번들에 넣지 마세요.\",\n TENANT_MISMATCH:\n \"secretKey 가 tenant 옵션과 다른 테넌트의 키입니다. 두 값이 같은 테넌트인지 확인하세요.\",\n};\n\nfunction isApiErrorBody(value: unknown): value is ApiErrorBody {\n return (\n typeof value === \"object\" &&\n value !== null &&\n \"message\" in value &&\n typeof (value as {message: unknown}).message === \"string\"\n );\n}\n","import {ZalkeraError} from \"./errors\";\nimport type {\n ApiResponse,\n AuthTokens,\n AvailabilityParams,\n AvailabilitySlot,\n Booking,\n Cart,\n Category,\n CheckoutInput,\n CreateBookingInput,\n CreateReviewInput,\n ConsentInput,\n ConsentStatus,\n CustomerSummary,\n InquiryCreated,\n InquiryInput,\n LeadCreated,\n LeadInput,\n ListPostsParams,\n ListReviewsParams,\n MediaUrl,\n Menu,\n OrderDetail,\n OrderSummary,\n PageContent,\n ListProductsParams,\n Paginated,\n PaymentSession,\n PostDetail,\n PostSummary,\n ProductCategory,\n ProductDetail,\n ProductSummary,\n RatingSummary,\n Review,\n ShipmentInfo,\n SiteConfig,\n SocialLoginInput,\n} from \"./types\";\n\n/**\n * ISR 읽기 옵션 — Next.js 캐시 태그(memo31 §0-1). RSC/ISR 페이지에서 읽기 메서드에 넘기면\n * 그 fetch 에 `next.tags` 가 실려, 백엔드가 `revalidateTag(tag)` 로 온디맨드 무효화할 수 있다.\n * 태그 컨벤션: `site-config`(사이트 설정·테마·레이아웃), `products`(카탈로그), `product:{slug}`(특정 상품).\n * 넘기지 않으면 세그먼트 기본 캐시(페이지의 `revalidate` 주기)만 적용된다 — 하위호환 유지.\n */\nexport interface ReadOptions {\n /** Next ISR 캐시 태그. 이 태그로 백엔드가 온디맨드 revalidate 한다. */\n tags?: string[];\n}\n\nexport interface ZalkeraClientOptions {\n /**\n * 백엔드 베이스 URL — `/api` 접두사는 붙이지 않는다. 예: `http://localhost:8100`.\n * 클라이언트가 경로에 `/api/public/...` 를 붙인다.\n */\n baseUrl: string;\n /**\n * 테넌트 코드. 모든 요청에 `X-Tenant` 헤더로 실린다.\n *\n * 공개 API 는 이 헤더를 그대로 믿는다(비인증). 그래서 이 클라이언트는 **서버 사이드**\n * (RSC·route handler·server action)에서 쓰는 것을 전제로 한다 — 브라우저에서 직접 부르면\n * baseUrl 이 노출되고 CORS 를 열어야 한다. README 참고.\n *\n * [secretKey] 를 함께 주면 백엔드는 키로 테넌트를 결정하고(키가 정본), 이 값은 대조용으로만\n * 쓰인다. 이행기(dual)에는 둘 다 보내도 무방하며 일치해야 한다(불일치 시 403 `TENANT_MISMATCH`).\n */\n tenant: string;\n /**\n * 스토어프론트 서버 시크릿 키(선택·`oqsk_…`). 주면 모든 요청에 `X-Storefront-Key` 헤더로 실려,\n * 백엔드가 이 키로 테넌트 신원을 증명한다(memo78 — [tenant] 무인증 신뢰의 보안 승격).\n *\n * ⚠️ **진짜 비밀이다.** 오직 서버 `.env`(예: `ONEQUE_STOREFRONT_KEY`)에만 두고, 브라우저 번들에\n * 절대 넣지 마라 — `NEXT_PUBLIC_*` 접두사·클라이언트 컴포넌트 import 금지. 이 클라이언트가 서버\n * 전용인 이유가 그것이다. 유출 시 콘솔에서 revoke·재발급.\n *\n * 안 주면 종전대로 [tenant](`X-Tenant`)만으로 동작한다(dual 이행기 하위호환). 백엔드가 `required`\n * 모드인데 키가 없으면 401 `STOREFRONT_KEY_REQUIRED` → [ZalkeraError].\n */\n secretKey?: string;\n /**\n * `fetch` 구현 주입(선택). 기본은 전역 `fetch`(Node 18+·브라우저). 테스트·커스텀 에이전트·\n * Next.js 의 `fetch` 캐시 옵션을 감싸는 래퍼를 넣을 때 쓴다.\n *\n * (내부 `request` 전송부는 이 주입점을 데이터 소스 seam 으로 유지한다 — 미래의 로컬 픽스처(mock)\n * 모드를 갈아엎지 않고 얹기 위한 여지다. mock 구현은 현재 없다 — memo78 §14 후속.)\n */\n fetch?: typeof fetch;\n /** 모든 요청에 추가할 헤더(선택). */\n headers?: Record<string, string>;\n /** 요청 타임아웃(ms). 기본 10초. */\n timeoutMs?: number;\n}\n\nconst DEFAULT_TIMEOUT_MS = 10_000;\n\n/**\n * zalkera 공개 API 클라이언트.\n *\n * 테넌트 사이트(credium 등)가 백엔드의 공개 엔드포인트를 타입 안전하게 부르기 위한 얇은 래퍼다.\n * 런타임 의존성이 없다 — 전역 `fetch` 만 쓴다.\n *\n * ```ts\n * const cms = createZalkeraClient({ baseUrl: process.env.API_BASE_URL!, tenant: \"credium\" });\n * const posts = await cms.listPosts({ size: 10, sort: \"publishedAt,desc\" });\n * await cms.submitInquiry({ name, email, subject, message });\n * ```\n *\n * 모든 메서드는 성공 시 envelope 안쪽 `data` 를 돌려주고, 실패 시 [ZalkeraError] 를 던진다.\n */\nexport interface ZalkeraClient {\n /** 회사 정보·테마·SEO 기본값. ISR 페이지는 [ReadOptions.tags] 로 캐시 태그를 실을 수 있다. */\n getSiteConfig(options?: ReadOptions): Promise<SiteConfig>;\n /** 살아 있는 카테고리 전부(페이징 없음). */\n listCategories(): Promise<Category[]>;\n /** 발행된 글 목록(페이징). */\n listPosts(params?: ListPostsParams): Promise<Paginated<PostSummary>>;\n /** slug 로 글 상세(본문 포함). 없으면 404 → [ZalkeraError]. */\n getPost(slug: string): Promise<PostDetail>;\n /**\n * 조회 비콘 — 같은 뷰어의 같은 날 재조회는 집계되지 않는다(반환 false).\n * 화면 렌더를 막지 않도록 fire-and-forget 으로 부르길 권한다(실패해도 페이지는 살아야 한다).\n *\n * 조회 dedup 은 뷰어 IP·UA 를 쓴다. 서버 사이드에서 부를 때는 [RequestContext.clientIp] 로\n * 원 방문자 IP 를 넘겨야 방문자별로 집계된다 — 안 넘기면 전부 서버 IP 하나로 뭉친다.\n */\n recordPostView(slug: string, context?: RequestContext): Promise<boolean>;\n /** slug 로 고정 페이지. */\n /**\n * slug 로 고정 페이지(섹션 포함). 없으면 404 → [ZalkeraError].\n * ISR 페이지는 [ReadOptions.tags] 로 캐시 태그를 실을 수 있다 — 백엔드가 발행 시 그 태그만\n * 콕 집어 revalidate 한다.\n */\n getPage(slug: string, options?: ReadOptions): Promise<PageContent>;\n /** 메뉴 트리(HEADER·FOOTER). */\n listMenus(options?: ReadOptions): Promise<Menu[]>;\n /** 미디어 presigned 다운로드 URL(만료 있음). */\n getMediaUrl(id: number): Promise<MediaUrl>;\n /**\n * 문의 접수. 성공 시 생성된 문의 id. 레이트리밋이면 429 → `error.isRateLimited`.\n *\n * ⚠️ 서버 사이드(테넌트 route handler)에서 부를 때는 [RequestContext.clientIp] 로 **원 방문자\n * IP 를 반드시 넘겨야 한다.** 백엔드의 문의 레이트리밋·IP 기록은 `X-Forwarded-For` 첫 홉을\n * 쓰는데, 안 넘기면 백엔드가 테넌트 서버 IP 하나만 보고 몇 건 뒤 **모든 방문자**를 429 로 막는다.\n */\n submitInquiry(input: InquiryInput, context?: RequestContext): Promise<InquiryCreated>;\n /**\n * 광고 리드 접수. 문의와 달리 이메일이 선택이고 UTM 추적을 함께 보낸다.\n *\n * ⚠️ [submitInquiry] 와 같은 이유로 서버 사이드에서는 [RequestContext.clientIp] 로 원 방문자\n * IP 를 넘겨야 한다 — 백엔드 리드 레이트리밋·IP 기록이 그 값을 본다.\n */\n submitLead(input: LeadInput, context?: RequestContext): Promise<LeadCreated>;\n\n // ── 커머스: 카탈로그(공개) ──────────────────────────────────\n /** slug 로 공개 상품(ACTIVE) 상세 — variant·재고 가용여부 포함. 없으면 404. ISR 태그는 [ReadOptions]. */\n getProduct(slug: string, options?: ReadOptions): Promise<ProductDetail>;\n /** 공개 상품 목록(ACTIVE) — 카드용 요약(최저가·재고). ISR 태그는 [ReadOptions]. */\n listProducts(params?: ListProductsParams, options?: ReadOptions): Promise<Paginated<ProductSummary>>;\n /** 커머스 카테고리 목록(노출 순서). ISR 태그는 [ReadOptions]. */\n listProductCategories(options?: ReadOptions): Promise<ProductCategory[]>;\n\n // ── 커머스: 상품후기(공개) ──────────────────────────────────\n /**\n * 상품 후기 목록(공개·VISIBLE 만, 최신순, 페이징).\n *\n * 상품의 **숫자 id(productId)** 로 조회한다 — slug 가 아니다. `getProduct(slug).id` 로 획득한다(0.4.0).\n */\n listProductReviews(productId: number, params?: ListReviewsParams, options?: ReadOptions): Promise<Paginated<Review>>;\n /** 상품 평점 요약(후기 수·평균 별점). 후기 없으면 count=0·avg=0. 목록 별점 배지에 값싸게 쓴다. */\n getProductReviewSummary(productId: number, options?: ReadOptions): Promise<RatingSummary>;\n /**\n * 후기 작성 — **로그인 필수 + 구매검증**. `input.orderItemId` 는 [OrderItemLine.id](주문 상세에서 획득).\n * 서버가 본인 주문·배송완료(DELIVERED) 이상·상품 일치를 3중 확인한다. 라인당 1개(재작성 시 409 `ALREADY_REVIEWED`).\n */\n createProductReview(productId: number, input: CreateReviewInput, accessToken: string): Promise<Review>;\n\n // ── 커머스: 고객 인증 ───────────────────────────────────────\n /** 소셜 로그인 — authorization code 를 백엔드가 교환하고 고객 토큰을 발급. */\n socialLogin(input: SocialLoginInput): Promise<AuthTokens>;\n /** refresh 토큰으로 새 토큰 회전(옛 refresh 는 폐기). */\n refreshSession(refreshToken: string): Promise<AuthTokens>;\n /** 로그아웃 — 이 고객의 모든 세션 폐기. */\n logout(accessToken: string): Promise<string>;\n /** 내 정보. */\n getMe(accessToken: string): Promise<CustomerSummary>;\n /** 현재 동의 상태 조회(로그인 필수) — 마케팅 수신 토글 화면 등에서 초기값으로 쓴다. */\n getConsents(accessToken: string): Promise<ConsentStatus[]>;\n /**\n * 동의 갱신(로그인 필수, append) — 마케팅 수신 on/off 등. 넘긴 항목만 반영되고 나머지는 유지된다.\n * 성공 시 백엔드 확인 메시지를 돌려준다(최신 상태가 필요하면 [getConsents] 로 다시 읽는다).\n */\n updateConsents(accessToken: string, consents: ConsentInput[]): Promise<string>;\n\n // ── 예약(SERVICE 상품) ──────────────────────────────────────\n /**\n * 예약 가능한 슬롯 조회 — **비로그인 가능**(상품 상세에서 시간표를 보여주는 자리).\n *\n * 미래·잔여정원 있는 OPEN 슬롯만 온다. 가용 판정은 [AvailabilitySlot.availableCount] 로 하고,\n * 카탈로그의 `inStock`/`available` 을 쓰지 마라 — 예약은 슬롯마다 정원이 따로다.\n */\n availability(params: AvailabilityParams, options?: ReadOptions): Promise<AvailabilitySlot[]>;\n /**\n * 예약 생성 — 슬롯 정원을 원자적으로 소비한다. **로그인 필수**(게스트 예약 없음).\n *\n * 무료 예약은 즉시 `CONFIRMED`. **유료·예약금이면 `status=PENDING` + [Booking.orderNo] 가 실려 오고**,\n * 그 orderNo 로 [startPayment] 를 불러 **기존 결제 흐름을 그대로 탄다**(예약 전용 결제 API 는 없다).\n *\n * 정원 소진은 `SLOT_FULL` — 슬롯 목록이 stale 했다는 뜻이니 [availability] 를 다시 읽어 보여줘라.\n */\n createBooking(accessToken: string, input: CreateBookingInput): Promise<Booking>;\n /** 내 예약 목록(최신순). 로그인 필수. */\n myBookings(accessToken: string): Promise<Booking[]>;\n /** 예약 취소 — 시작 전·무료 예약만. 정원이 복원된다. 로그인 필수. */\n cancelBooking(accessToken: string, bookingCode: string): Promise<string>;\n /** 같은 상품의 다른 슬롯으로 이동(신 슬롯 소비·구 슬롯 복원). 로그인 필수. */\n rescheduleBooking(accessToken: string, bookingCode: string, newSlotId: number): Promise<Booking>;\n\n // ── 커머스: 장바구니 ────────────────────────────────────────\n /**\n * 장바구니 조회. 로그인은 accessToken, 게스트는 cartSessionKey 로 식별.\n *\n * ⚠️ **예약(SERVICE) 상품은 장바구니에 담기지 않는다** — [addToCart] 가 400 으로 거부한다.\n * 예약은 [createBooking] 으로 별도 흐름을 탄다(카트에 슬롯을 실으면 체크아웃이 오염된다).\n */\n getCart(session: ShopSession): Promise<Cart>;\n /**\n * 담기(같은 variant 는 수량 누적).\n *\n * ⚠️ **예약(SERVICE) 상품은 400 으로 거부된다** — 예약은 [createBooking] 이다. 상품 상세에서\n * `productType === \"SERVICE\"` 면 담기 버튼 대신 슬롯 선택을 그려라.\n */\n addToCart(variantId: number, quantity: number, session: ShopSession): Promise<Cart>;\n /** 수량 변경. */\n updateCartItem(variantId: number, quantity: number, session: ShopSession): Promise<Cart>;\n /** 항목 삭제. */\n removeFromCart(variantId: number, session: ShopSession): Promise<Cart>;\n /** 비우기. */\n clearCart(session: ShopSession): Promise<Cart>;\n\n // ── 커머스: 결제·주문·배송 ──────────────────────────────────\n /**\n * 결제(주문 생성) — 스냅샷 동결 + 재고 차감. 상태=PENDING_PAYMENT.\n *\n * [idempotencyKey] 를 주면 **같은 키·같은 입력의 재요청이 새 주문을 만들지 않고 원주문을 그대로\n * 반환**한다(더블클릭·네트워크 재시도 안전). 같은 키로 **다른 입력**을 보내면 409.\n *\n * **키는 \"이 체크아웃 1건\"에 고정된 값이어야 한다** — 호출마다 새로 만들면 재시도가 서로 다른\n * 키를 들고 가서 아무것도 막지 못한다(그래서 SDK 가 자동 생성해 주지 않는다). 안정적인 소스를\n * 쓴다: 게스트는 **장바구니 세션 키**(카트 1개 → 주문 1건이라 의미가 정확히 맞는다),\n * 그 외에는 결제 시도 시작 시 한 번 만들어 재시도 동안 보관한 값.\n *\n * 안 주면 종전 동작 — 중복 제출은 카트가 이미 소비돼 404(`CART_NOT_FOUND`)로 떨어진다.\n */\n checkout(input: CheckoutInput, session: ShopSession, idempotencyKey?: string): Promise<OrderDetail>;\n /**\n * 결제 시작. **벤더에 따라 두 갈래**(테넌트가 자기 PG 를 고른다 — 기본 TOSS):\n * `session.widget` 이 있으면 위젯형(내 사이트에서 결제창 → [confirmPayment] 로 승인 확정),\n * 없으면 리다이렉트형(`session.paymentUrl` 로 보내면 끝).\n */\n startPayment(orderNo: string, access: OrderAccess): Promise<PaymentSession>;\n /**\n * **위젯형 벤더의 승인 확정**(토스 등). 결제창이 성공 URL 로 돌려준 파라미터를 그대로 넘긴다\n * (토스: `{paymentKey, orderId, amount}` — URL 쿼리에서 읽어 통째로).\n *\n * **금액을 넘겨도 서버는 안 믿는다** — 서버가 저장한 주문 금액으로 PG 에 직접 묻는다. 그래서\n * 브라우저에서 금액을 조작해도 승인되지 않는다.\n *\n * 이미 승인된 주문(웹훅 선착)이면 조용히 통과한다 — 재호출이 안전하다.\n * 리다이렉트형 벤더에 부르면 404(`PG_CONFIG_NOT_FOUND`).\n */\n confirmPayment(orderNo: string, providerParams: Record<string, string>, access: OrderAccess): Promise<void>;\n /** 주문 조회 — 본인(accessToken) 또는 게스트(phone). */\n getOrder(orderNo: string, access: OrderAccess): Promise<OrderDetail>;\n /** 내 주문 목록(로그인 필수). */\n listMyOrders(accessToken: string, params?: {page?: number; size?: number}): Promise<Paginated<OrderSummary>>;\n /** 주문 취소(미결제만) — 본인 또는 게스트(phone). */\n cancelOrder(orderNo: string, access: OrderAccess): Promise<OrderDetail>;\n /** 구매 확정(배송완료 후) — 본인 또는 게스트(phone). */\n completeOrder(orderNo: string, access: OrderAccess): Promise<OrderDetail>;\n /** 배송 조회 — 본인 또는 게스트(phone). */\n getShipment(orderNo: string, access: OrderAccess): Promise<ShipmentInfo>;\n}\n\n/**\n * 장바구니·결제의 사용자 식별. 로그인 고객은 [accessToken], 게스트는 [cartSessionKey]\n * (브라우저가 만든 안정적 익명 키)를 넘긴다. 로그인 시 게스트 카트→고객 카트 병합은 백엔드 몫.\n */\nexport interface ShopSession {\n accessToken?: string;\n cartSessionKey?: string;\n}\n\n/**\n * 주문 조회·취소·배송조회의 접근 크리덴셜. 로그인 고객은 [accessToken] 으로 소유권을,\n * 게스트는 주문 시 남긴 [phone] 으로 확인한다(주문번호 + 연락처).\n */\nexport interface OrderAccess {\n accessToken?: string;\n phone?: string;\n}\n\n/**\n * IP 민감 엔드포인트(문의·조회 비콘)에서 원 방문자를 백엔드에 알리는 컨텍스트.\n *\n * 테넌트 사이트는 서버 사이드에서 이 클라이언트를 부르므로, 백엔드가 보는 소스 IP 는 방문자가\n * 아니라 테넌트 서버다. 방문자 IP 를 `X-Forwarded-For` 로 실어 백엔드가 방문자별로 판단하게 한다.\n */\nexport interface RequestContext {\n /** 원 방문자 IP — route handler 에서 요청 헤더(x-forwarded-for·x-real-ip)로 뽑아 넘긴다. */\n clientIp?: string;\n}\n\nexport function createZalkeraClient(options: ZalkeraClientOptions): ZalkeraClient {\n const baseUrl = options.baseUrl.replace(/\\/+$/, \"\");\n const fetchImpl = options.fetch ?? globalThis.fetch;\n if (typeof fetchImpl !== \"function\") {\n throw new Error(\n \"@zalkera/client: 전역 fetch 가 없습니다. Node 18+ 를 쓰거나 options.fetch 를 주입하세요.\",\n );\n }\n const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n\n async function request<T>(\n path: string,\n init?: {\n method?: string;\n body?: unknown;\n query?: Record<string, unknown>;\n context?: RequestContext;\n /** 고객 access 토큰 → Authorization: Bearer. */\n bearer?: string;\n /** 게스트 장바구니 키 → X-Cart-Session. */\n cartSession?: string;\n /** 멱등키 → Idempotency-Key. 같은 키·같은 본문의 재요청은 원주문을 재생한다. */\n idempotencyKey?: string;\n /** Next.js ISR 캐시 옵션 — RSC/ISR fetch 에 `next.tags` 를 실어 온디맨드 revalidate 대상으로 만든다. */\n next?: {tags?: string[]};\n },\n ): Promise<T> {\n const url = new URL(`${baseUrl}/api${path}`);\n if (init?.query) {\n for (const [key, value] of Object.entries(init.query)) {\n if (value != null) url.searchParams.set(key, String(value));\n }\n }\n\n const headers: Record<string, string> = {\n \"X-Tenant\": options.tenant,\n Accept: \"application/json\",\n ...options.headers,\n };\n // 스토어프론트 서버 시크릿(memo78) — 있으면 전 요청에 붙는다. 키가 테넌트 신원의 정본이고\n // X-Tenant 는 대조용으로 함께 간다(dual). 순수 옵션 전달 — 값은 코드에 박히지 않는다.\n if (options.secretKey) headers[\"X-Storefront-Key\"] = options.secretKey;\n if (init?.body != null) headers[\"Content-Type\"] = \"application/json\";\n // 원 방문자 IP 를 백엔드에 전달 — 문의 레이트리밋·조회 dedup·IP 기록이 이걸 본다.\n if (init?.context?.clientIp) headers[\"X-Forwarded-For\"] = init.context.clientIp;\n // 커머스 인증 — 로그인 고객은 Bearer, 게스트 장바구니는 세션 키.\n if (init?.bearer) headers[\"Authorization\"] = `Bearer ${init.bearer}`;\n if (init?.cartSession) headers[\"X-Cart-Session\"] = init.cartSession;\n if (init?.idempotencyKey) headers[\"Idempotency-Key\"] = init.idempotencyKey;\n\n // 타임아웃 — 행 걸린 연결이 페이지 렌더를 무한정 붙들지 않게.\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n\n let response: Response;\n try {\n // Next.js 는 표준 RequestInit 에 비표준 `next`(캐시 태그/주기)를 확장한다. 표준 DOM 타입엔\n // 없으므로 교차 타입으로 선언해 (있을 때만) 실어 보낸다 — 일반 fetch 는 이 필드를 무시한다.\n const fetchInit: RequestInit & {next?: {tags?: string[]}} = {\n method: init?.method ?? \"GET\",\n headers,\n body: init?.body != null ? JSON.stringify(init.body) : undefined,\n signal: controller.signal,\n };\n if (init?.next) fetchInit.next = init.next;\n response = await fetchImpl(url.toString(), fetchInit);\n } catch (cause) {\n const aborted = cause instanceof Error && cause.name === \"AbortError\";\n throw new ZalkeraError(\n aborted ? `요청 시간이 초과되었습니다 (${timeoutMs}ms)` : \"네트워크 요청이 실패했습니다.\",\n {status: 0, cause},\n );\n } finally {\n clearTimeout(timer);\n }\n\n // 204 등 본문 없는 응답은 파싱하지 않는다.\n const text = await response.text();\n const parsed = text ? safeJsonParse(text) : null;\n\n if (!response.ok) {\n throw ZalkeraError.fromBody(response.status, parsed);\n }\n // 빈 바디(204·void 엔드포인트 예: confirmPayment)는 정상 — data 접근 없이 통과.\n if (!text) return undefined as T;\n // 성공(2xx)인데 envelope 가 아니면(HTML·비객체 JSON) 게이트웨이/프록시 오염이다.\n // (null).data TypeError 로 새지 않게 구조화 에러로 변환 — BFF errorResponse 가 502 로 서빙(memo 57 §11-4).\n if (parsed === null || typeof parsed !== \"object\") {\n throw new ZalkeraError(\n `서버 응답 형식이 올바르지 않습니다. 잠시 후 다시 시도해주세요. (HTTP ${response.status})`,\n {status: 502, code: \"UPSTREAM_NON_JSON\"},\n );\n }\n return (parsed as ApiResponse<T>).data;\n }\n\n return {\n getSiteConfig: (options) => request<SiteConfig>(\"/public/site-config\", nextInit(options)),\n listCategories: () => request<Category[]>(\"/public/categories\"),\n listPosts: (params) =>\n request<Paginated<PostSummary>>(\"/public/posts\", {\n query: {\n category: params?.category,\n page: params?.page,\n size: params?.size,\n sort: params?.sort,\n },\n }),\n getPost: (slug) => request<PostDetail>(`/public/posts/${encodeURIComponent(slug)}`),\n recordPostView: (slug, context) =>\n request<boolean>(`/public/posts/${encodeURIComponent(slug)}/view`, {\n method: \"POST\",\n context,\n }),\n getPage: (slug, options) =>\n request<PageContent>(`/public/pages/${encodeURIComponent(slug)}`, nextInit(options)),\n listMenus: (options) =>\n request<Menu[]>(\"/public/menus\", {next: options?.tags ? {tags: options.tags} : undefined}),\n getMediaUrl: (id) => request<MediaUrl>(`/public/media/${id}/url`),\n submitInquiry: (input, context) =>\n request<InquiryCreated>(\"/public/inquiries\", {\n method: \"POST\",\n body: input,\n context,\n }),\n submitLead: (input, context) =>\n request<LeadCreated>(\"/public/leads\", {\n method: \"POST\",\n body: input,\n context,\n }),\n\n // ── 커머스: 카탈로그 ────────────────────────────────────\n getProduct: (slug, options) =>\n request<ProductDetail>(`/public/products/${encodeURIComponent(slug)}`, nextInit(options)),\n listProducts: (params, options) =>\n request<Paginated<ProductSummary>>(\"/public/products\", {\n query: {\n productType: params?.productType,\n keyword: params?.keyword,\n page: params?.page,\n size: params?.size,\n sort: params?.sort,\n },\n ...nextInit(options),\n }),\n listProductCategories: (options) =>\n request<ProductCategory[]>(\"/public/product-categories\", nextInit(options)),\n\n // ── 커머스: 상품후기(공개) ──────────────────────────────\n listProductReviews: (productId, params, options) =>\n request<Paginated<Review>>(`/public/products/${productId}/reviews`, {\n query: {page: params?.page, size: params?.size},\n next: options?.tags ? {tags: options.tags} : undefined,\n }),\n getProductReviewSummary: (productId, options) =>\n request<RatingSummary>(`/public/products/${productId}/reviews/summary`, {\n next: options?.tags ? {tags: options.tags} : undefined,\n }),\n createProductReview: (productId, input, accessToken) =>\n request<Review>(`/shop/products/${productId}/reviews`, {method: \"POST\", body: input, bearer: accessToken}),\n\n // ── 커머스: 고객 인증 ───────────────────────────────────\n socialLogin: (input) => request<AuthTokens>(\"/shop/auth/social\", {method: \"POST\", body: input}),\n refreshSession: (refreshToken) =>\n request<AuthTokens>(\"/shop/auth/refresh\", {method: \"POST\", body: {refreshToken}}),\n logout: (accessToken) => request<string>(\"/shop/auth/logout\", {method: \"POST\", bearer: accessToken}),\n getMe: (accessToken) => request<CustomerSummary>(\"/shop/me\", {bearer: accessToken}),\n getConsents: (accessToken) => request<ConsentStatus[]>(\"/shop/consents\", {bearer: accessToken}),\n updateConsents: (accessToken, consents) =>\n request<string>(\"/shop/consents\", {method: \"POST\", bearer: accessToken, body: {consents}}),\n\n // ── 예약(SERVICE 상품) ───────────────────────────────────\n availability: (params, options) =>\n request<AvailabilitySlot[]>(\"/public/booking/availability\", {\n query: {productId: params.productId, from: params.from, to: params.to},\n next: options?.tags ? {tags: options.tags} : undefined,\n }),\n createBooking: (accessToken, input) =>\n request<Booking>(\"/shop/booking/bookings\", {method: \"POST\", body: input, bearer: accessToken}),\n myBookings: (accessToken) => request<Booking[]>(\"/shop/booking/bookings\", {bearer: accessToken}),\n cancelBooking: (accessToken, bookingCode) =>\n request<string>(`/shop/booking/bookings/${encodeURIComponent(bookingCode)}/cancel`, {\n method: \"POST\",\n bearer: accessToken,\n }),\n rescheduleBooking: (accessToken, bookingCode, newSlotId) =>\n request<Booking>(`/shop/booking/bookings/${encodeURIComponent(bookingCode)}/reschedule`, {\n method: \"POST\",\n body: {newSlotId},\n bearer: accessToken,\n }),\n\n // ── 커머스: 장바구니 ────────────────────────────────────\n getCart: (session) => request<Cart>(\"/shop/cart\", shopInit(session)),\n addToCart: (variantId, quantity, session) =>\n request<Cart>(\"/shop/cart/items\", shopInit(session, {method: \"POST\", body: {variantId, quantity}})),\n updateCartItem: (variantId, quantity, session) =>\n request<Cart>(`/shop/cart/items/${variantId}`, shopInit(session, {method: \"PATCH\", body: {quantity}})),\n removeFromCart: (variantId, session) =>\n request<Cart>(`/shop/cart/items/${variantId}`, shopInit(session, {method: \"DELETE\"})),\n clearCart: (session) => request<Cart>(\"/shop/cart\", shopInit(session, {method: \"DELETE\"})),\n\n // ── 커머스: 결제·주문·배송 ──────────────────────────────\n checkout: (input, session, idempotencyKey) =>\n request<OrderDetail>(\"/shop/checkout\", shopInit(session, {method: \"POST\", body: input, idempotencyKey})),\n confirmPayment: (orderNo, providerParams, access) =>\n request<void>(\n `/shop/orders/${encodeURIComponent(orderNo)}/payment/confirm`,\n accessInit(access, {method: \"POST\", body: providerParams}),\n ),\n startPayment: (orderNo, access) =>\n request<PaymentSession>(\n `/shop/orders/${encodeURIComponent(orderNo)}/payment/session`,\n accessInit(access, {method: \"POST\"}),\n ),\n getOrder: (orderNo, access) =>\n request<OrderDetail>(`/shop/orders/${encodeURIComponent(orderNo)}`, accessInit(access)),\n listMyOrders: (accessToken, params) =>\n request<Paginated<OrderSummary>>(\"/shop/orders\", {\n bearer: accessToken,\n query: {page: params?.page, size: params?.size},\n }),\n cancelOrder: (orderNo, access) =>\n request<OrderDetail>(\n `/shop/orders/${encodeURIComponent(orderNo)}/cancel`,\n accessInit(access, {method: \"POST\"}),\n ),\n completeOrder: (orderNo, access) =>\n request<OrderDetail>(\n `/shop/orders/${encodeURIComponent(orderNo)}/complete`,\n accessInit(access, {method: \"POST\"}),\n ),\n getShipment: (orderNo, access) =>\n request<ShipmentInfo>(`/shop/orders/${encodeURIComponent(orderNo)}/shipment`, accessInit(access)),\n };\n}\n\n/** [ReadOptions] 를 request init 의 Next 캐시 옵션으로 변환. 태그 없으면 빈 객체(하위호환). */\nfunction nextInit(options?: ReadOptions): {next?: {tags?: string[]}} {\n return options?.tags && options.tags.length > 0 ? {next: {tags: options.tags}} : {};\n}\n\n/** 장바구니·결제 요청의 사용자 식별을 request init 으로 변환. */\nfunction shopInit(\n session: ShopSession,\n extra?: {method?: string; body?: unknown; idempotencyKey?: string},\n): {method?: string; body?: unknown; bearer?: string; cartSession?: string; idempotencyKey?: string} {\n return {...extra, bearer: session.accessToken, cartSession: session.cartSessionKey};\n}\n\n/** 주문 접근(본인 토큰 또는 게스트 phone)을 request init 으로 변환. */\nfunction accessInit(\n access: OrderAccess,\n extra?: {method?: string; body?: unknown},\n): {method?: string; body?: unknown; bearer?: string; query?: Record<string, unknown>} {\n return {\n ...extra,\n bearer: access.accessToken,\n query: access.phone ? {phone: access.phone} : undefined,\n };\n}\n\nfunction safeJsonParse(text: string): unknown {\n try {\n return JSON.parse(text);\n } catch {\n return null;\n }\n}\n"]}
package/dist/index.d.cts CHANGED
@@ -16,7 +16,7 @@ interface ApiErrorBody {
16
16
  error: string;
17
17
  /**
18
18
  * 기계 판독 코드(`OUT_OF_STOCK`·`IDEMPOTENCY_CONFLICT`…). 같은 상태코드의 여러 원인을 가른다.
19
- * 구버전 백엔드 응답엔 없다 — `OnequeError.code` 가 `error` 로 폴백한다.
19
+ * 구버전 백엔드 응답엔 없다 — `ZalkeraError.code` 가 `error` 로 폴백한다.
20
20
  */
21
21
  errorCode?: string;
22
22
  message: string;
@@ -537,7 +537,7 @@ interface OrderSummary {
537
537
  * **벤더가 둘 중 하나다**(테넌트가 자기 PG 를 고른다 — 기본 TOSS):
538
538
  * - **리다이렉트형**(PayOneQ 등): [widget] 이 없다 → [paymentUrl] 로 고객을 보내면 끝.
539
539
  * - **위젯형**(토스): [widget] 값으로 **내 사이트에서** 결제창을 띄우고, 성공 콜백의 파라미터를
540
- * [OnequeClient.confirmPayment] 로 넘겨 승인을 확정한다.
540
+ * [ZalkeraClient.confirmPayment] 로 넘겨 승인을 확정한다.
541
541
  *
542
542
  * 분기는 `session.widget ? 위젯 : redirect(session.paymentUrl)` 한 줄이면 된다.
543
543
  */
@@ -582,7 +582,7 @@ interface ReadOptions {
582
582
  /** Next ISR 캐시 태그. 이 태그로 백엔드가 온디맨드 revalidate 한다. */
583
583
  tags?: string[];
584
584
  }
585
- interface OnequeClientOptions {
585
+ interface ZalkeraClientOptions {
586
586
  /**
587
587
  * 백엔드 베이스 URL — `/api` 접두사는 붙이지 않는다. 예: `http://localhost:8100`.
588
588
  * 클라이언트가 경로에 `/api/public/...` 를 붙인다.
@@ -594,11 +594,29 @@ interface OnequeClientOptions {
594
594
  * 공개 API 는 이 헤더를 그대로 믿는다(비인증). 그래서 이 클라이언트는 **서버 사이드**
595
595
  * (RSC·route handler·server action)에서 쓰는 것을 전제로 한다 — 브라우저에서 직접 부르면
596
596
  * baseUrl 이 노출되고 CORS 를 열어야 한다. README 참고.
597
+ *
598
+ * [secretKey] 를 함께 주면 백엔드는 키로 테넌트를 결정하고(키가 정본), 이 값은 대조용으로만
599
+ * 쓰인다. 이행기(dual)에는 둘 다 보내도 무방하며 일치해야 한다(불일치 시 403 `TENANT_MISMATCH`).
597
600
  */
598
601
  tenant: string;
602
+ /**
603
+ * 스토어프론트 서버 시크릿 키(선택·`oqsk_…`). 주면 모든 요청에 `X-Storefront-Key` 헤더로 실려,
604
+ * 백엔드가 이 키로 테넌트 신원을 증명한다(memo78 — [tenant] 무인증 신뢰의 보안 승격).
605
+ *
606
+ * ⚠️ **진짜 비밀이다.** 오직 서버 `.env`(예: `ONEQUE_STOREFRONT_KEY`)에만 두고, 브라우저 번들에
607
+ * 절대 넣지 마라 — `NEXT_PUBLIC_*` 접두사·클라이언트 컴포넌트 import 금지. 이 클라이언트가 서버
608
+ * 전용인 이유가 그것이다. 유출 시 콘솔에서 revoke·재발급.
609
+ *
610
+ * 안 주면 종전대로 [tenant](`X-Tenant`)만으로 동작한다(dual 이행기 하위호환). 백엔드가 `required`
611
+ * 모드인데 키가 없으면 401 `STOREFRONT_KEY_REQUIRED` → [ZalkeraError].
612
+ */
613
+ secretKey?: string;
599
614
  /**
600
615
  * `fetch` 구현 주입(선택). 기본은 전역 `fetch`(Node 18+·브라우저). 테스트·커스텀 에이전트·
601
616
  * Next.js 의 `fetch` 캐시 옵션을 감싸는 래퍼를 넣을 때 쓴다.
617
+ *
618
+ * (내부 `request` 전송부는 이 주입점을 데이터 소스 seam 으로 유지한다 — 미래의 로컬 픽스처(mock)
619
+ * 모드를 갈아엎지 않고 얹기 위한 여지다. mock 구현은 현재 없다 — memo78 §14 후속.)
602
620
  */
603
621
  fetch?: typeof fetch;
604
622
  /** 모든 요청에 추가할 헤더(선택). */
@@ -607,27 +625,27 @@ interface OnequeClientOptions {
607
625
  timeoutMs?: number;
608
626
  }
609
627
  /**
610
- * Oneque 공개 API 클라이언트.
628
+ * zalkera 공개 API 클라이언트.
611
629
  *
612
630
  * 테넌트 사이트(credium 등)가 백엔드의 공개 엔드포인트를 타입 안전하게 부르기 위한 얇은 래퍼다.
613
631
  * 런타임 의존성이 없다 — 전역 `fetch` 만 쓴다.
614
632
  *
615
633
  * ```ts
616
- * const cms = createOnequeClient({ baseUrl: process.env.API_BASE_URL!, tenant: "credium" });
634
+ * const cms = createZalkeraClient({ baseUrl: process.env.API_BASE_URL!, tenant: "credium" });
617
635
  * const posts = await cms.listPosts({ size: 10, sort: "publishedAt,desc" });
618
636
  * await cms.submitInquiry({ name, email, subject, message });
619
637
  * ```
620
638
  *
621
- * 모든 메서드는 성공 시 envelope 안쪽 `data` 를 돌려주고, 실패 시 [OnequeError] 를 던진다.
639
+ * 모든 메서드는 성공 시 envelope 안쪽 `data` 를 돌려주고, 실패 시 [ZalkeraError] 를 던진다.
622
640
  */
623
- interface OnequeClient {
641
+ interface ZalkeraClient {
624
642
  /** 회사 정보·테마·SEO 기본값. ISR 페이지는 [ReadOptions.tags] 로 캐시 태그를 실을 수 있다. */
625
643
  getSiteConfig(options?: ReadOptions): Promise<SiteConfig>;
626
644
  /** 살아 있는 카테고리 전부(페이징 없음). */
627
645
  listCategories(): Promise<Category[]>;
628
646
  /** 발행된 글 목록(페이징). */
629
647
  listPosts(params?: ListPostsParams): Promise<Paginated<PostSummary>>;
630
- /** slug 로 글 상세(본문 포함). 없으면 404 → [OnequeError]. */
648
+ /** slug 로 글 상세(본문 포함). 없으면 404 → [ZalkeraError]. */
631
649
  getPost(slug: string): Promise<PostDetail>;
632
650
  /**
633
651
  * 조회 비콘 — 같은 뷰어의 같은 날 재조회는 집계되지 않는다(반환 false).
@@ -639,7 +657,7 @@ interface OnequeClient {
639
657
  recordPostView(slug: string, context?: RequestContext): Promise<boolean>;
640
658
  /** slug 로 고정 페이지. */
641
659
  /**
642
- * slug 로 고정 페이지(섹션 포함). 없으면 404 → [OnequeError].
660
+ * slug 로 고정 페이지(섹션 포함). 없으면 404 → [ZalkeraError].
643
661
  * ISR 페이지는 [ReadOptions.tags] 로 캐시 태그를 실을 수 있다 — 백엔드가 발행 시 그 태그만
644
662
  * 콕 집어 revalidate 한다.
645
663
  */
@@ -810,10 +828,10 @@ interface RequestContext {
810
828
  /** 원 방문자 IP — route handler 에서 요청 헤더(x-forwarded-for·x-real-ip)로 뽑아 넘긴다. */
811
829
  clientIp?: string;
812
830
  }
813
- declare function createOnequeClient(options: OnequeClientOptions): OnequeClient;
831
+ declare function createZalkeraClient(options: ZalkeraClientOptions): ZalkeraClient;
814
832
 
815
833
  /**
816
- * Oneque API 호출 실패.
834
+ * zalkera API 호출 실패.
817
835
  *
818
836
  * 백엔드의 `ErrorResponse` 를 그대로 담는다.
819
837
  *
@@ -829,7 +847,7 @@ declare function createOnequeClient(options: OnequeClientOptions): OnequeClient;
829
847
  *
830
848
  * 네트워크 자체가 실패했거나 응답이 JSON 이 아니면 [status] 가 0 이고 [body] 가 null 이다.
831
849
  */
832
- declare class OnequeError extends Error {
850
+ declare class ZalkeraError extends Error {
833
851
  /** HTTP 상태. 네트워크 실패·비JSON 응답이면 0. */
834
852
  readonly status: number;
835
853
  /**
@@ -854,8 +872,14 @@ declare class OnequeError extends Error {
854
872
  });
855
873
  /** IP 레이트리밋(429) — 문의 폼에서 "잠시 후 다시" 를 띄울 때 쓴다. */
856
874
  get isRateLimited(): boolean;
875
+ /**
876
+ * 스토어프론트 시크릿 키 문제(memo78) — 개발자 오배선 신호. `true` 면 `secretKey` 옵션을
877
+ * 점검하라(미설정·불일치·폐기). [code] 로 정밀 분기: `STOREFRONT_KEY_REQUIRED`(401·키 필요)·
878
+ * `TENANT_MISMATCH`(403·키↔tenant 불일치).
879
+ */
880
+ get isStorefrontKeyError(): boolean;
857
881
  /** 응답 본문(파싱된 것)으로부터 에러를 만든다. */
858
- static fromBody(status: number, body: unknown): OnequeError;
882
+ static fromBody(status: number, body: unknown): ZalkeraError;
859
883
  }
860
884
 
861
- export { type ApiErrorBody, type AuthTokens, type AvailabilityParams, type AvailabilitySlot, type Booking, type BookingStatus, type BusinessType, type Cart, type CartLine, type Category, type CheckoutInput, type ConsentInput, type ConsentStatus, type ConsentType, type CreateBookingInput, type CreateReviewInput, type CustomerSummary, type InquiryCreated, type InquiryInput, type KnownSectionType, type LeadCreated, type LeadInput, type LeadTracking, type ListPostsParams, type ListProductsParams, type ListReviewsParams, type MediaUrl, type Menu, type MenuPosition, type OnequeClient, type OnequeClientOptions, OnequeError, type OrderAccess, type OrderDetail, type OrderHistoryEntry, type OrderItemLine, type OrderStatus, type OrderSummary, type PageContent, type PageSection, type Paginated, type PaymentSession, type PostDetail, type PostSummary, type ProductCategory, type ProductDetail, type ProductSummary, type ProductType, type ProductVariant, type RatingSummary, type ReadOptions, type RequestContext, type Review, type ShipToInput, type ShipmentEvent, type ShipmentInfo, type ShipmentStatus, type ShopSession, type SiteConfig, type SocialLoginInput, type SocialProvider, type ValidationError, createOnequeClient };
885
+ export { type ApiErrorBody, type AuthTokens, type AvailabilityParams, type AvailabilitySlot, type Booking, type BookingStatus, type BusinessType, type Cart, type CartLine, type Category, type CheckoutInput, type ConsentInput, type ConsentStatus, type ConsentType, type CreateBookingInput, type CreateReviewInput, type CustomerSummary, type InquiryCreated, type InquiryInput, type KnownSectionType, type LeadCreated, type LeadInput, type LeadTracking, type ListPostsParams, type ListProductsParams, type ListReviewsParams, type MediaUrl, type Menu, type MenuPosition, type OrderAccess, type OrderDetail, type OrderHistoryEntry, type OrderItemLine, type OrderStatus, type OrderSummary, type PageContent, type PageSection, type Paginated, type PaymentSession, type PostDetail, type PostSummary, type ProductCategory, type ProductDetail, type ProductSummary, type ProductType, type ProductVariant, type RatingSummary, type ReadOptions, type RequestContext, type Review, type ShipToInput, type ShipmentEvent, type ShipmentInfo, type ShipmentStatus, type ShopSession, type SiteConfig, type SocialLoginInput, type SocialProvider, type ValidationError, type ZalkeraClient, type ZalkeraClientOptions, ZalkeraError, createZalkeraClient };
package/dist/index.d.ts CHANGED
@@ -16,7 +16,7 @@ interface ApiErrorBody {
16
16
  error: string;
17
17
  /**
18
18
  * 기계 판독 코드(`OUT_OF_STOCK`·`IDEMPOTENCY_CONFLICT`…). 같은 상태코드의 여러 원인을 가른다.
19
- * 구버전 백엔드 응답엔 없다 — `OnequeError.code` 가 `error` 로 폴백한다.
19
+ * 구버전 백엔드 응답엔 없다 — `ZalkeraError.code` 가 `error` 로 폴백한다.
20
20
  */
21
21
  errorCode?: string;
22
22
  message: string;
@@ -537,7 +537,7 @@ interface OrderSummary {
537
537
  * **벤더가 둘 중 하나다**(테넌트가 자기 PG 를 고른다 — 기본 TOSS):
538
538
  * - **리다이렉트형**(PayOneQ 등): [widget] 이 없다 → [paymentUrl] 로 고객을 보내면 끝.
539
539
  * - **위젯형**(토스): [widget] 값으로 **내 사이트에서** 결제창을 띄우고, 성공 콜백의 파라미터를
540
- * [OnequeClient.confirmPayment] 로 넘겨 승인을 확정한다.
540
+ * [ZalkeraClient.confirmPayment] 로 넘겨 승인을 확정한다.
541
541
  *
542
542
  * 분기는 `session.widget ? 위젯 : redirect(session.paymentUrl)` 한 줄이면 된다.
543
543
  */
@@ -582,7 +582,7 @@ interface ReadOptions {
582
582
  /** Next ISR 캐시 태그. 이 태그로 백엔드가 온디맨드 revalidate 한다. */
583
583
  tags?: string[];
584
584
  }
585
- interface OnequeClientOptions {
585
+ interface ZalkeraClientOptions {
586
586
  /**
587
587
  * 백엔드 베이스 URL — `/api` 접두사는 붙이지 않는다. 예: `http://localhost:8100`.
588
588
  * 클라이언트가 경로에 `/api/public/...` 를 붙인다.
@@ -594,11 +594,29 @@ interface OnequeClientOptions {
594
594
  * 공개 API 는 이 헤더를 그대로 믿는다(비인증). 그래서 이 클라이언트는 **서버 사이드**
595
595
  * (RSC·route handler·server action)에서 쓰는 것을 전제로 한다 — 브라우저에서 직접 부르면
596
596
  * baseUrl 이 노출되고 CORS 를 열어야 한다. README 참고.
597
+ *
598
+ * [secretKey] 를 함께 주면 백엔드는 키로 테넌트를 결정하고(키가 정본), 이 값은 대조용으로만
599
+ * 쓰인다. 이행기(dual)에는 둘 다 보내도 무방하며 일치해야 한다(불일치 시 403 `TENANT_MISMATCH`).
597
600
  */
598
601
  tenant: string;
602
+ /**
603
+ * 스토어프론트 서버 시크릿 키(선택·`oqsk_…`). 주면 모든 요청에 `X-Storefront-Key` 헤더로 실려,
604
+ * 백엔드가 이 키로 테넌트 신원을 증명한다(memo78 — [tenant] 무인증 신뢰의 보안 승격).
605
+ *
606
+ * ⚠️ **진짜 비밀이다.** 오직 서버 `.env`(예: `ONEQUE_STOREFRONT_KEY`)에만 두고, 브라우저 번들에
607
+ * 절대 넣지 마라 — `NEXT_PUBLIC_*` 접두사·클라이언트 컴포넌트 import 금지. 이 클라이언트가 서버
608
+ * 전용인 이유가 그것이다. 유출 시 콘솔에서 revoke·재발급.
609
+ *
610
+ * 안 주면 종전대로 [tenant](`X-Tenant`)만으로 동작한다(dual 이행기 하위호환). 백엔드가 `required`
611
+ * 모드인데 키가 없으면 401 `STOREFRONT_KEY_REQUIRED` → [ZalkeraError].
612
+ */
613
+ secretKey?: string;
599
614
  /**
600
615
  * `fetch` 구현 주입(선택). 기본은 전역 `fetch`(Node 18+·브라우저). 테스트·커스텀 에이전트·
601
616
  * Next.js 의 `fetch` 캐시 옵션을 감싸는 래퍼를 넣을 때 쓴다.
617
+ *
618
+ * (내부 `request` 전송부는 이 주입점을 데이터 소스 seam 으로 유지한다 — 미래의 로컬 픽스처(mock)
619
+ * 모드를 갈아엎지 않고 얹기 위한 여지다. mock 구현은 현재 없다 — memo78 §14 후속.)
602
620
  */
603
621
  fetch?: typeof fetch;
604
622
  /** 모든 요청에 추가할 헤더(선택). */
@@ -607,27 +625,27 @@ interface OnequeClientOptions {
607
625
  timeoutMs?: number;
608
626
  }
609
627
  /**
610
- * Oneque 공개 API 클라이언트.
628
+ * zalkera 공개 API 클라이언트.
611
629
  *
612
630
  * 테넌트 사이트(credium 등)가 백엔드의 공개 엔드포인트를 타입 안전하게 부르기 위한 얇은 래퍼다.
613
631
  * 런타임 의존성이 없다 — 전역 `fetch` 만 쓴다.
614
632
  *
615
633
  * ```ts
616
- * const cms = createOnequeClient({ baseUrl: process.env.API_BASE_URL!, tenant: "credium" });
634
+ * const cms = createZalkeraClient({ baseUrl: process.env.API_BASE_URL!, tenant: "credium" });
617
635
  * const posts = await cms.listPosts({ size: 10, sort: "publishedAt,desc" });
618
636
  * await cms.submitInquiry({ name, email, subject, message });
619
637
  * ```
620
638
  *
621
- * 모든 메서드는 성공 시 envelope 안쪽 `data` 를 돌려주고, 실패 시 [OnequeError] 를 던진다.
639
+ * 모든 메서드는 성공 시 envelope 안쪽 `data` 를 돌려주고, 실패 시 [ZalkeraError] 를 던진다.
622
640
  */
623
- interface OnequeClient {
641
+ interface ZalkeraClient {
624
642
  /** 회사 정보·테마·SEO 기본값. ISR 페이지는 [ReadOptions.tags] 로 캐시 태그를 실을 수 있다. */
625
643
  getSiteConfig(options?: ReadOptions): Promise<SiteConfig>;
626
644
  /** 살아 있는 카테고리 전부(페이징 없음). */
627
645
  listCategories(): Promise<Category[]>;
628
646
  /** 발행된 글 목록(페이징). */
629
647
  listPosts(params?: ListPostsParams): Promise<Paginated<PostSummary>>;
630
- /** slug 로 글 상세(본문 포함). 없으면 404 → [OnequeError]. */
648
+ /** slug 로 글 상세(본문 포함). 없으면 404 → [ZalkeraError]. */
631
649
  getPost(slug: string): Promise<PostDetail>;
632
650
  /**
633
651
  * 조회 비콘 — 같은 뷰어의 같은 날 재조회는 집계되지 않는다(반환 false).
@@ -639,7 +657,7 @@ interface OnequeClient {
639
657
  recordPostView(slug: string, context?: RequestContext): Promise<boolean>;
640
658
  /** slug 로 고정 페이지. */
641
659
  /**
642
- * slug 로 고정 페이지(섹션 포함). 없으면 404 → [OnequeError].
660
+ * slug 로 고정 페이지(섹션 포함). 없으면 404 → [ZalkeraError].
643
661
  * ISR 페이지는 [ReadOptions.tags] 로 캐시 태그를 실을 수 있다 — 백엔드가 발행 시 그 태그만
644
662
  * 콕 집어 revalidate 한다.
645
663
  */
@@ -810,10 +828,10 @@ interface RequestContext {
810
828
  /** 원 방문자 IP — route handler 에서 요청 헤더(x-forwarded-for·x-real-ip)로 뽑아 넘긴다. */
811
829
  clientIp?: string;
812
830
  }
813
- declare function createOnequeClient(options: OnequeClientOptions): OnequeClient;
831
+ declare function createZalkeraClient(options: ZalkeraClientOptions): ZalkeraClient;
814
832
 
815
833
  /**
816
- * Oneque API 호출 실패.
834
+ * zalkera API 호출 실패.
817
835
  *
818
836
  * 백엔드의 `ErrorResponse` 를 그대로 담는다.
819
837
  *
@@ -829,7 +847,7 @@ declare function createOnequeClient(options: OnequeClientOptions): OnequeClient;
829
847
  *
830
848
  * 네트워크 자체가 실패했거나 응답이 JSON 이 아니면 [status] 가 0 이고 [body] 가 null 이다.
831
849
  */
832
- declare class OnequeError extends Error {
850
+ declare class ZalkeraError extends Error {
833
851
  /** HTTP 상태. 네트워크 실패·비JSON 응답이면 0. */
834
852
  readonly status: number;
835
853
  /**
@@ -854,8 +872,14 @@ declare class OnequeError extends Error {
854
872
  });
855
873
  /** IP 레이트리밋(429) — 문의 폼에서 "잠시 후 다시" 를 띄울 때 쓴다. */
856
874
  get isRateLimited(): boolean;
875
+ /**
876
+ * 스토어프론트 시크릿 키 문제(memo78) — 개발자 오배선 신호. `true` 면 `secretKey` 옵션을
877
+ * 점검하라(미설정·불일치·폐기). [code] 로 정밀 분기: `STOREFRONT_KEY_REQUIRED`(401·키 필요)·
878
+ * `TENANT_MISMATCH`(403·키↔tenant 불일치).
879
+ */
880
+ get isStorefrontKeyError(): boolean;
857
881
  /** 응답 본문(파싱된 것)으로부터 에러를 만든다. */
858
- static fromBody(status: number, body: unknown): OnequeError;
882
+ static fromBody(status: number, body: unknown): ZalkeraError;
859
883
  }
860
884
 
861
- export { type ApiErrorBody, type AuthTokens, type AvailabilityParams, type AvailabilitySlot, type Booking, type BookingStatus, type BusinessType, type Cart, type CartLine, type Category, type CheckoutInput, type ConsentInput, type ConsentStatus, type ConsentType, type CreateBookingInput, type CreateReviewInput, type CustomerSummary, type InquiryCreated, type InquiryInput, type KnownSectionType, type LeadCreated, type LeadInput, type LeadTracking, type ListPostsParams, type ListProductsParams, type ListReviewsParams, type MediaUrl, type Menu, type MenuPosition, type OnequeClient, type OnequeClientOptions, OnequeError, type OrderAccess, type OrderDetail, type OrderHistoryEntry, type OrderItemLine, type OrderStatus, type OrderSummary, type PageContent, type PageSection, type Paginated, type PaymentSession, type PostDetail, type PostSummary, type ProductCategory, type ProductDetail, type ProductSummary, type ProductType, type ProductVariant, type RatingSummary, type ReadOptions, type RequestContext, type Review, type ShipToInput, type ShipmentEvent, type ShipmentInfo, type ShipmentStatus, type ShopSession, type SiteConfig, type SocialLoginInput, type SocialProvider, type ValidationError, createOnequeClient };
885
+ export { type ApiErrorBody, type AuthTokens, type AvailabilityParams, type AvailabilitySlot, type Booking, type BookingStatus, type BusinessType, type Cart, type CartLine, type Category, type CheckoutInput, type ConsentInput, type ConsentStatus, type ConsentType, type CreateBookingInput, type CreateReviewInput, type CustomerSummary, type InquiryCreated, type InquiryInput, type KnownSectionType, type LeadCreated, type LeadInput, type LeadTracking, type ListPostsParams, type ListProductsParams, type ListReviewsParams, type MediaUrl, type Menu, type MenuPosition, type OrderAccess, type OrderDetail, type OrderHistoryEntry, type OrderItemLine, type OrderStatus, type OrderSummary, type PageContent, type PageSection, type Paginated, type PaymentSession, type PostDetail, type PostSummary, type ProductCategory, type ProductDetail, type ProductSummary, type ProductType, type ProductVariant, type RatingSummary, type ReadOptions, type RequestContext, type Review, type ShipToInput, type ShipmentEvent, type ShipmentInfo, type ShipmentStatus, type ShopSession, type SiteConfig, type SocialLoginInput, type SocialProvider, type ValidationError, type ZalkeraClient, type ZalkeraClientOptions, ZalkeraError, createZalkeraClient };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/errors.ts
2
- var OnequeError = class _OnequeError extends Error {
2
+ var ZalkeraError = class _ZalkeraError extends Error {
3
3
  /** HTTP 상태. 네트워크 실패·비JSON 응답이면 0. */
4
4
  status;
5
5
  /**
@@ -17,7 +17,7 @@ var OnequeError = class _OnequeError extends Error {
17
17
  body;
18
18
  constructor(message, options) {
19
19
  super(message, { cause: options.cause });
20
- this.name = "OnequeError";
20
+ this.name = "ZalkeraError";
21
21
  this.status = options.status;
22
22
  this.code = options.code ?? null;
23
23
  this.validationErrors = options.validationErrors ?? [];
@@ -27,36 +27,54 @@ var OnequeError = class _OnequeError extends Error {
27
27
  get isRateLimited() {
28
28
  return this.status === 429;
29
29
  }
30
+ /**
31
+ * 스토어프론트 시크릿 키 문제(memo78) — 개발자 오배선 신호. `true` 면 `secretKey` 옵션을
32
+ * 점검하라(미설정·불일치·폐기). [code] 로 정밀 분기: `STOREFRONT_KEY_REQUIRED`(401·키 필요)·
33
+ * `TENANT_MISMATCH`(403·키↔tenant 불일치).
34
+ */
35
+ get isStorefrontKeyError() {
36
+ return this.code === "STOREFRONT_KEY_REQUIRED" || this.code === "TENANT_MISMATCH";
37
+ }
30
38
  /** 응답 본문(파싱된 것)으로부터 에러를 만든다. */
31
39
  static fromBody(status, body) {
32
40
  if (isApiErrorBody(body)) {
33
- return new _OnequeError(body.message, {
34
- // errorCode(기계 판독)를 우선, 없으면 error(HTTP 사유구)로 폴백 — 구버전 백엔드 안전.
35
- status,
36
- code: body.errorCode ?? body.error,
37
- validationErrors: body.errors ?? [],
38
- body
39
- });
41
+ const code = body.errorCode ?? body.error;
42
+ return new _ZalkeraError(
43
+ // 스토어프론트 키 오배선(memo78)은 개발자용 안내로 메시지를 덮는다 — 백엔드는 정보 누출
44
+ // 최소화로 두루뭉술한 401 을 주므로, SDK 가 "무엇을 고쳐야 하는지"를 또렷이 알려준다.
45
+ STOREFRONT_KEY_MESSAGES[code] ?? body.message,
46
+ {
47
+ // errorCode(기계 판독)를 우선, 없으면 error(HTTP 사유구)로 폴백 — 구버전 백엔드 안전.
48
+ status,
49
+ code,
50
+ validationErrors: body.errors ?? [],
51
+ body
52
+ }
53
+ );
40
54
  }
41
55
  const gateway = status === 502 || status === 503 || status === 504;
42
- return new _OnequeError(
56
+ return new _ZalkeraError(
43
57
  gateway ? "\uC11C\uBE44\uC2A4\uC5D0 \uC77C\uC2DC\uC801\uC778 \uBB38\uC81C\uAC00 \uC788\uC2B5\uB2C8\uB2E4. \uC7A0\uC2DC \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD574\uC8FC\uC138\uC694." : `\uC694\uCCAD\uC774 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4 (HTTP ${status})`,
44
58
  { status, code: gateway ? "UPSTREAM_UNAVAILABLE" : null }
45
59
  );
46
60
  }
47
61
  };
62
+ var STOREFRONT_KEY_MESSAGES = {
63
+ STOREFRONT_KEY_REQUIRED: "\uC2A4\uD1A0\uC5B4\uD504\uB860\uD2B8 \uC2DC\uD06C\uB9BF \uD0A4\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4. createZalkeraClient \uC758 secretKey \uC635\uC158\uC744 \uC124\uC815\uD558\uC138\uC694 (\uD30C\uD2B8\uB108 \uCF58\uC194\uC5D0\uC11C \uBC1C\uAE09 \u2192 \uC11C\uBC84 .env \uC758 ONEQUE_STOREFRONT_KEY). \uBE0C\uB77C\uC6B0\uC800 \uBC88\uB4E4\uC5D0 \uB123\uC9C0 \uB9C8\uC138\uC694.",
64
+ TENANT_MISMATCH: "secretKey \uAC00 tenant \uC635\uC158\uACFC \uB2E4\uB978 \uD14C\uB10C\uD2B8\uC758 \uD0A4\uC785\uB2C8\uB2E4. \uB450 \uAC12\uC774 \uAC19\uC740 \uD14C\uB10C\uD2B8\uC778\uC9C0 \uD655\uC778\uD558\uC138\uC694."
65
+ };
48
66
  function isApiErrorBody(value) {
49
67
  return typeof value === "object" && value !== null && "message" in value && typeof value.message === "string";
50
68
  }
51
69
 
52
70
  // src/client.ts
53
71
  var DEFAULT_TIMEOUT_MS = 1e4;
54
- function createOnequeClient(options) {
72
+ function createZalkeraClient(options) {
55
73
  const baseUrl = options.baseUrl.replace(/\/+$/, "");
56
74
  const fetchImpl = options.fetch ?? globalThis.fetch;
57
75
  if (typeof fetchImpl !== "function") {
58
76
  throw new Error(
59
- "@oneque/client: \uC804\uC5ED fetch \uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. Node 18+ \uB97C \uC4F0\uAC70\uB098 options.fetch \uB97C \uC8FC\uC785\uD558\uC138\uC694."
77
+ "@zalkera/client: \uC804\uC5ED fetch \uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. Node 18+ \uB97C \uC4F0\uAC70\uB098 options.fetch \uB97C \uC8FC\uC785\uD558\uC138\uC694."
60
78
  );
61
79
  }
62
80
  const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
@@ -72,6 +90,7 @@ function createOnequeClient(options) {
72
90
  Accept: "application/json",
73
91
  ...options.headers
74
92
  };
93
+ if (options.secretKey) headers["X-Storefront-Key"] = options.secretKey;
75
94
  if (init?.body != null) headers["Content-Type"] = "application/json";
76
95
  if (init?.context?.clientIp) headers["X-Forwarded-For"] = init.context.clientIp;
77
96
  if (init?.bearer) headers["Authorization"] = `Bearer ${init.bearer}`;
@@ -91,7 +110,7 @@ function createOnequeClient(options) {
91
110
  response = await fetchImpl(url.toString(), fetchInit);
92
111
  } catch (cause) {
93
112
  const aborted = cause instanceof Error && cause.name === "AbortError";
94
- throw new OnequeError(
113
+ throw new ZalkeraError(
95
114
  aborted ? `\uC694\uCCAD \uC2DC\uAC04\uC774 \uCD08\uACFC\uB418\uC5C8\uC2B5\uB2C8\uB2E4 (${timeoutMs}ms)` : "\uB124\uD2B8\uC6CC\uD06C \uC694\uCCAD\uC774 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4.",
96
115
  { status: 0, cause }
97
116
  );
@@ -101,11 +120,11 @@ function createOnequeClient(options) {
101
120
  const text = await response.text();
102
121
  const parsed = text ? safeJsonParse(text) : null;
103
122
  if (!response.ok) {
104
- throw OnequeError.fromBody(response.status, parsed);
123
+ throw ZalkeraError.fromBody(response.status, parsed);
105
124
  }
106
125
  if (!text) return void 0;
107
126
  if (parsed === null || typeof parsed !== "object") {
108
- throw new OnequeError(
127
+ throw new ZalkeraError(
109
128
  `\uC11C\uBC84 \uC751\uB2F5 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. \uC7A0\uC2DC \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD574\uC8FC\uC138\uC694. (HTTP ${response.status})`,
110
129
  { status: 502, code: "UPSTREAM_NON_JSON" }
111
130
  );
@@ -239,6 +258,6 @@ function safeJsonParse(text) {
239
258
  }
240
259
  }
241
260
 
242
- export { OnequeError, createOnequeClient };
261
+ export { ZalkeraError, createZalkeraClient };
243
262
  //# sourceMappingURL=index.js.map
244
263
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/errors.ts","../src/client.ts"],"names":["options"],"mappings":"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","file":"index.js","sourcesContent":["import type {ApiErrorBody, ValidationError} from \"./types\";\n\n/**\n * Oneque API 호출 실패.\n *\n * 백엔드의 `ErrorResponse` 를 그대로 담는다.\n *\n * **원인 분기는 [code] 로 한다** — 한 엔드포인트가 같은 상태코드로 여러 원인을 낸다(checkout 의 409 =\n * `OUT_OF_STOCK`·`ITEM_NOT_PURCHASABLE`·`IDEMPOTENCY_CONFLICT` — 처방이 전부 다르다). `message` 는\n * 사람용 한국어라 **문자열 매칭 금지**.\n *\n * `status` 는 거친 분류에만:\n * - `400` + [validationErrors] — 입력값 문제(폼 필드별 메시지 노출)\n * - `404` — 없는 slug/리소스\n * - `429` — IP 레이트리밋(문의 남발). [isRateLimited] 로 편히 판별\n * - `5xx` — 서버 오류\n *\n * 네트워크 자체가 실패했거나 응답이 JSON 이 아니면 [status] 가 0 이고 [body] 가 null 이다.\n */\nexport class OnequeError extends Error {\n /** HTTP 상태. 네트워크 실패·비JSON 응답이면 0. */\n readonly status: number;\n /**\n * **기계 판독 에러 코드** — `ErrorResponse.errorCode`(백엔드 `ErrorCode` enum 이름).\n * 예: `OUT_OF_STOCK`·`IDEMPOTENCY_CONFLICT`·`CART_NOT_FOUND`. 이걸로 분기한다.\n *\n * 코드를 안 싣는 구버전 백엔드 응답에서는 `error`(HTTP 사유구, \"Conflict\")로 **폴백**한다 —\n * 그때는 원인을 가를 수 없으니, 폴백 값에 의존하는 분기를 짜지 마라.\n * 열린 계약이라 문자열이다(서버가 코드를 추가해도 SDK 릴리스가 필요 없다).\n */\n readonly code: string | null;\n /** 검증 실패 시 필드별 메시지. 그 외엔 빈 배열. */\n readonly validationErrors: ValidationError[];\n /** 파싱된 원본 에러 본문(있으면). */\n readonly body: ApiErrorBody | null;\n\n constructor(\n message: string,\n options: {\n status: number;\n code?: string | null;\n validationErrors?: ValidationError[];\n body?: ApiErrorBody | null;\n cause?: unknown;\n },\n ) {\n super(message, {cause: options.cause});\n this.name = \"OnequeError\";\n this.status = options.status;\n this.code = options.code ?? null;\n this.validationErrors = options.validationErrors ?? [];\n this.body = options.body ?? null;\n }\n\n /** IP 레이트리밋(429) — 문의 폼에서 \"잠시 후 다시\" 를 띄울 때 쓴다. */\n get isRateLimited(): boolean {\n return this.status === 429;\n }\n\n /** 응답 본문(파싱된 것)으로부터 에러를 만든다. */\n static fromBody(status: number, body: unknown): OnequeError {\n if (isApiErrorBody(body)) {\n return new OnequeError(body.message, {\n // errorCode(기계 판독)를 우선, 없으면 error(HTTP 사유구)로 폴백 — 구버전 백엔드 안전.\n status,\n code: body.errorCode ?? body.error,\n validationErrors: body.errors ?? [],\n body,\n });\n }\n // 비JSON 에러 바디(게이트웨이 HTML·빈 바디). 502/503/504 는 상류 장애로 안내 메시지 + 코드 부여.\n const gateway = status === 502 || status === 503 || status === 504;\n return new OnequeError(\n gateway ? \"서비스에 일시적인 문제가 있습니다. 잠시 후 다시 시도해주세요.\" : `요청이 실패했습니다 (HTTP ${status})`,\n {status, code: gateway ? \"UPSTREAM_UNAVAILABLE\" : null},\n );\n }\n}\n\nfunction isApiErrorBody(value: unknown): value is ApiErrorBody {\n return (\n typeof value === \"object\" &&\n value !== null &&\n \"message\" in value &&\n typeof (value as {message: unknown}).message === \"string\"\n );\n}\n","import {OnequeError} from \"./errors\";\nimport type {\n ApiResponse,\n AuthTokens,\n AvailabilityParams,\n AvailabilitySlot,\n Booking,\n Cart,\n Category,\n CheckoutInput,\n CreateBookingInput,\n CreateReviewInput,\n ConsentInput,\n ConsentStatus,\n CustomerSummary,\n InquiryCreated,\n InquiryInput,\n LeadCreated,\n LeadInput,\n ListPostsParams,\n ListReviewsParams,\n MediaUrl,\n Menu,\n OrderDetail,\n OrderSummary,\n PageContent,\n ListProductsParams,\n Paginated,\n PaymentSession,\n PostDetail,\n PostSummary,\n ProductCategory,\n ProductDetail,\n ProductSummary,\n RatingSummary,\n Review,\n ShipmentInfo,\n SiteConfig,\n SocialLoginInput,\n} from \"./types\";\n\n/**\n * ISR 읽기 옵션 — Next.js 캐시 태그(memo31 §0-1). RSC/ISR 페이지에서 읽기 메서드에 넘기면\n * 그 fetch 에 `next.tags` 가 실려, 백엔드가 `revalidateTag(tag)` 로 온디맨드 무효화할 수 있다.\n * 태그 컨벤션: `site-config`(사이트 설정·테마·레이아웃), `products`(카탈로그), `product:{slug}`(특정 상품).\n * 넘기지 않으면 세그먼트 기본 캐시(페이지의 `revalidate` 주기)만 적용된다 — 하위호환 유지.\n */\nexport interface ReadOptions {\n /** Next ISR 캐시 태그. 이 태그로 백엔드가 온디맨드 revalidate 한다. */\n tags?: string[];\n}\n\nexport interface OnequeClientOptions {\n /**\n * 백엔드 베이스 URL — `/api` 접두사는 붙이지 않는다. 예: `http://localhost:8100`.\n * 클라이언트가 경로에 `/api/public/...` 를 붙인다.\n */\n baseUrl: string;\n /**\n * 테넌트 코드. 모든 요청에 `X-Tenant` 헤더로 실린다.\n *\n * 공개 API 는 이 헤더를 그대로 믿는다(비인증). 그래서 이 클라이언트는 **서버 사이드**\n * (RSC·route handler·server action)에서 쓰는 것을 전제로 한다 — 브라우저에서 직접 부르면\n * baseUrl 이 노출되고 CORS 를 열어야 한다. README 참고.\n */\n tenant: string;\n /**\n * `fetch` 구현 주입(선택). 기본은 전역 `fetch`(Node 18+·브라우저). 테스트·커스텀 에이전트·\n * Next.js 의 `fetch` 캐시 옵션을 감싸는 래퍼를 넣을 때 쓴다.\n */\n fetch?: typeof fetch;\n /** 모든 요청에 추가할 헤더(선택). */\n headers?: Record<string, string>;\n /** 요청 타임아웃(ms). 기본 10초. */\n timeoutMs?: number;\n}\n\nconst DEFAULT_TIMEOUT_MS = 10_000;\n\n/**\n * Oneque 공개 API 클라이언트.\n *\n * 테넌트 사이트(credium 등)가 백엔드의 공개 엔드포인트를 타입 안전하게 부르기 위한 얇은 래퍼다.\n * 런타임 의존성이 없다 — 전역 `fetch` 만 쓴다.\n *\n * ```ts\n * const cms = createOnequeClient({ baseUrl: process.env.API_BASE_URL!, tenant: \"credium\" });\n * const posts = await cms.listPosts({ size: 10, sort: \"publishedAt,desc\" });\n * await cms.submitInquiry({ name, email, subject, message });\n * ```\n *\n * 모든 메서드는 성공 시 envelope 안쪽 `data` 를 돌려주고, 실패 시 [OnequeError] 를 던진다.\n */\nexport interface OnequeClient {\n /** 회사 정보·테마·SEO 기본값. ISR 페이지는 [ReadOptions.tags] 로 캐시 태그를 실을 수 있다. */\n getSiteConfig(options?: ReadOptions): Promise<SiteConfig>;\n /** 살아 있는 카테고리 전부(페이징 없음). */\n listCategories(): Promise<Category[]>;\n /** 발행된 글 목록(페이징). */\n listPosts(params?: ListPostsParams): Promise<Paginated<PostSummary>>;\n /** slug 로 글 상세(본문 포함). 없으면 404 → [OnequeError]. */\n getPost(slug: string): Promise<PostDetail>;\n /**\n * 조회 비콘 — 같은 뷰어의 같은 날 재조회는 집계되지 않는다(반환 false).\n * 화면 렌더를 막지 않도록 fire-and-forget 으로 부르길 권한다(실패해도 페이지는 살아야 한다).\n *\n * 조회 dedup 은 뷰어 IP·UA 를 쓴다. 서버 사이드에서 부를 때는 [RequestContext.clientIp] 로\n * 원 방문자 IP 를 넘겨야 방문자별로 집계된다 — 안 넘기면 전부 서버 IP 하나로 뭉친다.\n */\n recordPostView(slug: string, context?: RequestContext): Promise<boolean>;\n /** slug 로 고정 페이지. */\n /**\n * slug 로 고정 페이지(섹션 포함). 없으면 404 → [OnequeError].\n * ISR 페이지는 [ReadOptions.tags] 로 캐시 태그를 실을 수 있다 — 백엔드가 발행 시 그 태그만\n * 콕 집어 revalidate 한다.\n */\n getPage(slug: string, options?: ReadOptions): Promise<PageContent>;\n /** 메뉴 트리(HEADER·FOOTER). */\n listMenus(options?: ReadOptions): Promise<Menu[]>;\n /** 미디어 presigned 다운로드 URL(만료 있음). */\n getMediaUrl(id: number): Promise<MediaUrl>;\n /**\n * 문의 접수. 성공 시 생성된 문의 id. 레이트리밋이면 429 → `error.isRateLimited`.\n *\n * ⚠️ 서버 사이드(테넌트 route handler)에서 부를 때는 [RequestContext.clientIp] 로 **원 방문자\n * IP 를 반드시 넘겨야 한다.** 백엔드의 문의 레이트리밋·IP 기록은 `X-Forwarded-For` 첫 홉을\n * 쓰는데, 안 넘기면 백엔드가 테넌트 서버 IP 하나만 보고 몇 건 뒤 **모든 방문자**를 429 로 막는다.\n */\n submitInquiry(input: InquiryInput, context?: RequestContext): Promise<InquiryCreated>;\n /**\n * 광고 리드 접수. 문의와 달리 이메일이 선택이고 UTM 추적을 함께 보낸다.\n *\n * ⚠️ [submitInquiry] 와 같은 이유로 서버 사이드에서는 [RequestContext.clientIp] 로 원 방문자\n * IP 를 넘겨야 한다 — 백엔드 리드 레이트리밋·IP 기록이 그 값을 본다.\n */\n submitLead(input: LeadInput, context?: RequestContext): Promise<LeadCreated>;\n\n // ── 커머스: 카탈로그(공개) ──────────────────────────────────\n /** slug 로 공개 상품(ACTIVE) 상세 — variant·재고 가용여부 포함. 없으면 404. ISR 태그는 [ReadOptions]. */\n getProduct(slug: string, options?: ReadOptions): Promise<ProductDetail>;\n /** 공개 상품 목록(ACTIVE) — 카드용 요약(최저가·재고). ISR 태그는 [ReadOptions]. */\n listProducts(params?: ListProductsParams, options?: ReadOptions): Promise<Paginated<ProductSummary>>;\n /** 커머스 카테고리 목록(노출 순서). ISR 태그는 [ReadOptions]. */\n listProductCategories(options?: ReadOptions): Promise<ProductCategory[]>;\n\n // ── 커머스: 상품후기(공개) ──────────────────────────────────\n /**\n * 상품 후기 목록(공개·VISIBLE 만, 최신순, 페이징).\n *\n * 상품의 **숫자 id(productId)** 로 조회한다 — slug 가 아니다. `getProduct(slug).id` 로 획득한다(0.4.0).\n */\n listProductReviews(productId: number, params?: ListReviewsParams, options?: ReadOptions): Promise<Paginated<Review>>;\n /** 상품 평점 요약(후기 수·평균 별점). 후기 없으면 count=0·avg=0. 목록 별점 배지에 값싸게 쓴다. */\n getProductReviewSummary(productId: number, options?: ReadOptions): Promise<RatingSummary>;\n /**\n * 후기 작성 — **로그인 필수 + 구매검증**. `input.orderItemId` 는 [OrderItemLine.id](주문 상세에서 획득).\n * 서버가 본인 주문·배송완료(DELIVERED) 이상·상품 일치를 3중 확인한다. 라인당 1개(재작성 시 409 `ALREADY_REVIEWED`).\n */\n createProductReview(productId: number, input: CreateReviewInput, accessToken: string): Promise<Review>;\n\n // ── 커머스: 고객 인증 ───────────────────────────────────────\n /** 소셜 로그인 — authorization code 를 백엔드가 교환하고 고객 토큰을 발급. */\n socialLogin(input: SocialLoginInput): Promise<AuthTokens>;\n /** refresh 토큰으로 새 토큰 회전(옛 refresh 는 폐기). */\n refreshSession(refreshToken: string): Promise<AuthTokens>;\n /** 로그아웃 — 이 고객의 모든 세션 폐기. */\n logout(accessToken: string): Promise<string>;\n /** 내 정보. */\n getMe(accessToken: string): Promise<CustomerSummary>;\n /** 현재 동의 상태 조회(로그인 필수) — 마케팅 수신 토글 화면 등에서 초기값으로 쓴다. */\n getConsents(accessToken: string): Promise<ConsentStatus[]>;\n /**\n * 동의 갱신(로그인 필수, append) — 마케팅 수신 on/off 등. 넘긴 항목만 반영되고 나머지는 유지된다.\n * 성공 시 백엔드 확인 메시지를 돌려준다(최신 상태가 필요하면 [getConsents] 로 다시 읽는다).\n */\n updateConsents(accessToken: string, consents: ConsentInput[]): Promise<string>;\n\n // ── 예약(SERVICE 상품) ──────────────────────────────────────\n /**\n * 예약 가능한 슬롯 조회 — **비로그인 가능**(상품 상세에서 시간표를 보여주는 자리).\n *\n * 미래·잔여정원 있는 OPEN 슬롯만 온다. 가용 판정은 [AvailabilitySlot.availableCount] 로 하고,\n * 카탈로그의 `inStock`/`available` 을 쓰지 마라 — 예약은 슬롯마다 정원이 따로다.\n */\n availability(params: AvailabilityParams, options?: ReadOptions): Promise<AvailabilitySlot[]>;\n /**\n * 예약 생성 — 슬롯 정원을 원자적으로 소비한다. **로그인 필수**(게스트 예약 없음).\n *\n * 무료 예약은 즉시 `CONFIRMED`. **유료·예약금이면 `status=PENDING` + [Booking.orderNo] 가 실려 오고**,\n * 그 orderNo 로 [startPayment] 를 불러 **기존 결제 흐름을 그대로 탄다**(예약 전용 결제 API 는 없다).\n *\n * 정원 소진은 `SLOT_FULL` — 슬롯 목록이 stale 했다는 뜻이니 [availability] 를 다시 읽어 보여줘라.\n */\n createBooking(accessToken: string, input: CreateBookingInput): Promise<Booking>;\n /** 내 예약 목록(최신순). 로그인 필수. */\n myBookings(accessToken: string): Promise<Booking[]>;\n /** 예약 취소 — 시작 전·무료 예약만. 정원이 복원된다. 로그인 필수. */\n cancelBooking(accessToken: string, bookingCode: string): Promise<string>;\n /** 같은 상품의 다른 슬롯으로 이동(신 슬롯 소비·구 슬롯 복원). 로그인 필수. */\n rescheduleBooking(accessToken: string, bookingCode: string, newSlotId: number): Promise<Booking>;\n\n // ── 커머스: 장바구니 ────────────────────────────────────────\n /**\n * 장바구니 조회. 로그인은 accessToken, 게스트는 cartSessionKey 로 식별.\n *\n * ⚠️ **예약(SERVICE) 상품은 장바구니에 담기지 않는다** — [addToCart] 가 400 으로 거부한다.\n * 예약은 [createBooking] 으로 별도 흐름을 탄다(카트에 슬롯을 실으면 체크아웃이 오염된다).\n */\n getCart(session: ShopSession): Promise<Cart>;\n /**\n * 담기(같은 variant 는 수량 누적).\n *\n * ⚠️ **예약(SERVICE) 상품은 400 으로 거부된다** — 예약은 [createBooking] 이다. 상품 상세에서\n * `productType === \"SERVICE\"` 면 담기 버튼 대신 슬롯 선택을 그려라.\n */\n addToCart(variantId: number, quantity: number, session: ShopSession): Promise<Cart>;\n /** 수량 변경. */\n updateCartItem(variantId: number, quantity: number, session: ShopSession): Promise<Cart>;\n /** 항목 삭제. */\n removeFromCart(variantId: number, session: ShopSession): Promise<Cart>;\n /** 비우기. */\n clearCart(session: ShopSession): Promise<Cart>;\n\n // ── 커머스: 결제·주문·배송 ──────────────────────────────────\n /**\n * 결제(주문 생성) — 스냅샷 동결 + 재고 차감. 상태=PENDING_PAYMENT.\n *\n * [idempotencyKey] 를 주면 **같은 키·같은 입력의 재요청이 새 주문을 만들지 않고 원주문을 그대로\n * 반환**한다(더블클릭·네트워크 재시도 안전). 같은 키로 **다른 입력**을 보내면 409.\n *\n * **키는 \"이 체크아웃 1건\"에 고정된 값이어야 한다** — 호출마다 새로 만들면 재시도가 서로 다른\n * 키를 들고 가서 아무것도 막지 못한다(그래서 SDK 가 자동 생성해 주지 않는다). 안정적인 소스를\n * 쓴다: 게스트는 **장바구니 세션 키**(카트 1개 → 주문 1건이라 의미가 정확히 맞는다),\n * 그 외에는 결제 시도 시작 시 한 번 만들어 재시도 동안 보관한 값.\n *\n * 안 주면 종전 동작 — 중복 제출은 카트가 이미 소비돼 404(`CART_NOT_FOUND`)로 떨어진다.\n */\n checkout(input: CheckoutInput, session: ShopSession, idempotencyKey?: string): Promise<OrderDetail>;\n /**\n * 결제 시작. **벤더에 따라 두 갈래**(테넌트가 자기 PG 를 고른다 — 기본 TOSS):\n * `session.widget` 이 있으면 위젯형(내 사이트에서 결제창 → [confirmPayment] 로 승인 확정),\n * 없으면 리다이렉트형(`session.paymentUrl` 로 보내면 끝).\n */\n startPayment(orderNo: string, access: OrderAccess): Promise<PaymentSession>;\n /**\n * **위젯형 벤더의 승인 확정**(토스 등). 결제창이 성공 URL 로 돌려준 파라미터를 그대로 넘긴다\n * (토스: `{paymentKey, orderId, amount}` — URL 쿼리에서 읽어 통째로).\n *\n * **금액을 넘겨도 서버는 안 믿는다** — 서버가 저장한 주문 금액으로 PG 에 직접 묻는다. 그래서\n * 브라우저에서 금액을 조작해도 승인되지 않는다.\n *\n * 이미 승인된 주문(웹훅 선착)이면 조용히 통과한다 — 재호출이 안전하다.\n * 리다이렉트형 벤더에 부르면 404(`PG_CONFIG_NOT_FOUND`).\n */\n confirmPayment(orderNo: string, providerParams: Record<string, string>, access: OrderAccess): Promise<void>;\n /** 주문 조회 — 본인(accessToken) 또는 게스트(phone). */\n getOrder(orderNo: string, access: OrderAccess): Promise<OrderDetail>;\n /** 내 주문 목록(로그인 필수). */\n listMyOrders(accessToken: string, params?: {page?: number; size?: number}): Promise<Paginated<OrderSummary>>;\n /** 주문 취소(미결제만) — 본인 또는 게스트(phone). */\n cancelOrder(orderNo: string, access: OrderAccess): Promise<OrderDetail>;\n /** 구매 확정(배송완료 후) — 본인 또는 게스트(phone). */\n completeOrder(orderNo: string, access: OrderAccess): Promise<OrderDetail>;\n /** 배송 조회 — 본인 또는 게스트(phone). */\n getShipment(orderNo: string, access: OrderAccess): Promise<ShipmentInfo>;\n}\n\n/**\n * 장바구니·결제의 사용자 식별. 로그인 고객은 [accessToken], 게스트는 [cartSessionKey]\n * (브라우저가 만든 안정적 익명 키)를 넘긴다. 로그인 시 게스트 카트→고객 카트 병합은 백엔드 몫.\n */\nexport interface ShopSession {\n accessToken?: string;\n cartSessionKey?: string;\n}\n\n/**\n * 주문 조회·취소·배송조회의 접근 크리덴셜. 로그인 고객은 [accessToken] 으로 소유권을,\n * 게스트는 주문 시 남긴 [phone] 으로 확인한다(주문번호 + 연락처).\n */\nexport interface OrderAccess {\n accessToken?: string;\n phone?: string;\n}\n\n/**\n * IP 민감 엔드포인트(문의·조회 비콘)에서 원 방문자를 백엔드에 알리는 컨텍스트.\n *\n * 테넌트 사이트는 서버 사이드에서 이 클라이언트를 부르므로, 백엔드가 보는 소스 IP 는 방문자가\n * 아니라 테넌트 서버다. 방문자 IP 를 `X-Forwarded-For` 로 실어 백엔드가 방문자별로 판단하게 한다.\n */\nexport interface RequestContext {\n /** 원 방문자 IP — route handler 에서 요청 헤더(x-forwarded-for·x-real-ip)로 뽑아 넘긴다. */\n clientIp?: string;\n}\n\nexport function createOnequeClient(options: OnequeClientOptions): OnequeClient {\n const baseUrl = options.baseUrl.replace(/\\/+$/, \"\");\n const fetchImpl = options.fetch ?? globalThis.fetch;\n if (typeof fetchImpl !== \"function\") {\n throw new Error(\n \"@oneque/client: 전역 fetch 가 없습니다. Node 18+ 를 쓰거나 options.fetch 를 주입하세요.\",\n );\n }\n const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n\n async function request<T>(\n path: string,\n init?: {\n method?: string;\n body?: unknown;\n query?: Record<string, unknown>;\n context?: RequestContext;\n /** 고객 access 토큰 → Authorization: Bearer. */\n bearer?: string;\n /** 게스트 장바구니 키 → X-Cart-Session. */\n cartSession?: string;\n /** 멱등키 → Idempotency-Key. 같은 키·같은 본문의 재요청은 원주문을 재생한다. */\n idempotencyKey?: string;\n /** Next.js ISR 캐시 옵션 — RSC/ISR fetch 에 `next.tags` 를 실어 온디맨드 revalidate 대상으로 만든다. */\n next?: {tags?: string[]};\n },\n ): Promise<T> {\n const url = new URL(`${baseUrl}/api${path}`);\n if (init?.query) {\n for (const [key, value] of Object.entries(init.query)) {\n if (value != null) url.searchParams.set(key, String(value));\n }\n }\n\n const headers: Record<string, string> = {\n \"X-Tenant\": options.tenant,\n Accept: \"application/json\",\n ...options.headers,\n };\n if (init?.body != null) headers[\"Content-Type\"] = \"application/json\";\n // 원 방문자 IP 를 백엔드에 전달 — 문의 레이트리밋·조회 dedup·IP 기록이 이걸 본다.\n if (init?.context?.clientIp) headers[\"X-Forwarded-For\"] = init.context.clientIp;\n // 커머스 인증 — 로그인 고객은 Bearer, 게스트 장바구니는 세션 키.\n if (init?.bearer) headers[\"Authorization\"] = `Bearer ${init.bearer}`;\n if (init?.cartSession) headers[\"X-Cart-Session\"] = init.cartSession;\n if (init?.idempotencyKey) headers[\"Idempotency-Key\"] = init.idempotencyKey;\n\n // 타임아웃 — 행 걸린 연결이 페이지 렌더를 무한정 붙들지 않게.\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n\n let response: Response;\n try {\n // Next.js 는 표준 RequestInit 에 비표준 `next`(캐시 태그/주기)를 확장한다. 표준 DOM 타입엔\n // 없으므로 교차 타입으로 선언해 (있을 때만) 실어 보낸다 — 일반 fetch 는 이 필드를 무시한다.\n const fetchInit: RequestInit & {next?: {tags?: string[]}} = {\n method: init?.method ?? \"GET\",\n headers,\n body: init?.body != null ? JSON.stringify(init.body) : undefined,\n signal: controller.signal,\n };\n if (init?.next) fetchInit.next = init.next;\n response = await fetchImpl(url.toString(), fetchInit);\n } catch (cause) {\n const aborted = cause instanceof Error && cause.name === \"AbortError\";\n throw new OnequeError(\n aborted ? `요청 시간이 초과되었습니다 (${timeoutMs}ms)` : \"네트워크 요청이 실패했습니다.\",\n {status: 0, cause},\n );\n } finally {\n clearTimeout(timer);\n }\n\n // 204 등 본문 없는 응답은 파싱하지 않는다.\n const text = await response.text();\n const parsed = text ? safeJsonParse(text) : null;\n\n if (!response.ok) {\n throw OnequeError.fromBody(response.status, parsed);\n }\n // 빈 바디(204·void 엔드포인트 예: confirmPayment)는 정상 — data 접근 없이 통과.\n if (!text) return undefined as T;\n // 성공(2xx)인데 envelope 가 아니면(HTML·비객체 JSON) 게이트웨이/프록시 오염이다.\n // (null).data TypeError 로 새지 않게 구조화 에러로 변환 — BFF errorResponse 가 502 로 서빙(memo 57 §11-4).\n if (parsed === null || typeof parsed !== \"object\") {\n throw new OnequeError(\n `서버 응답 형식이 올바르지 않습니다. 잠시 후 다시 시도해주세요. (HTTP ${response.status})`,\n {status: 502, code: \"UPSTREAM_NON_JSON\"},\n );\n }\n return (parsed as ApiResponse<T>).data;\n }\n\n return {\n getSiteConfig: (options) => request<SiteConfig>(\"/public/site-config\", nextInit(options)),\n listCategories: () => request<Category[]>(\"/public/categories\"),\n listPosts: (params) =>\n request<Paginated<PostSummary>>(\"/public/posts\", {\n query: {\n category: params?.category,\n page: params?.page,\n size: params?.size,\n sort: params?.sort,\n },\n }),\n getPost: (slug) => request<PostDetail>(`/public/posts/${encodeURIComponent(slug)}`),\n recordPostView: (slug, context) =>\n request<boolean>(`/public/posts/${encodeURIComponent(slug)}/view`, {\n method: \"POST\",\n context,\n }),\n getPage: (slug, options) =>\n request<PageContent>(`/public/pages/${encodeURIComponent(slug)}`, nextInit(options)),\n listMenus: (options) =>\n request<Menu[]>(\"/public/menus\", {next: options?.tags ? {tags: options.tags} : undefined}),\n getMediaUrl: (id) => request<MediaUrl>(`/public/media/${id}/url`),\n submitInquiry: (input, context) =>\n request<InquiryCreated>(\"/public/inquiries\", {\n method: \"POST\",\n body: input,\n context,\n }),\n submitLead: (input, context) =>\n request<LeadCreated>(\"/public/leads\", {\n method: \"POST\",\n body: input,\n context,\n }),\n\n // ── 커머스: 카탈로그 ────────────────────────────────────\n getProduct: (slug, options) =>\n request<ProductDetail>(`/public/products/${encodeURIComponent(slug)}`, nextInit(options)),\n listProducts: (params, options) =>\n request<Paginated<ProductSummary>>(\"/public/products\", {\n query: {\n productType: params?.productType,\n keyword: params?.keyword,\n page: params?.page,\n size: params?.size,\n sort: params?.sort,\n },\n ...nextInit(options),\n }),\n listProductCategories: (options) =>\n request<ProductCategory[]>(\"/public/product-categories\", nextInit(options)),\n\n // ── 커머스: 상품후기(공개) ──────────────────────────────\n listProductReviews: (productId, params, options) =>\n request<Paginated<Review>>(`/public/products/${productId}/reviews`, {\n query: {page: params?.page, size: params?.size},\n next: options?.tags ? {tags: options.tags} : undefined,\n }),\n getProductReviewSummary: (productId, options) =>\n request<RatingSummary>(`/public/products/${productId}/reviews/summary`, {\n next: options?.tags ? {tags: options.tags} : undefined,\n }),\n createProductReview: (productId, input, accessToken) =>\n request<Review>(`/shop/products/${productId}/reviews`, {method: \"POST\", body: input, bearer: accessToken}),\n\n // ── 커머스: 고객 인증 ───────────────────────────────────\n socialLogin: (input) => request<AuthTokens>(\"/shop/auth/social\", {method: \"POST\", body: input}),\n refreshSession: (refreshToken) =>\n request<AuthTokens>(\"/shop/auth/refresh\", {method: \"POST\", body: {refreshToken}}),\n logout: (accessToken) => request<string>(\"/shop/auth/logout\", {method: \"POST\", bearer: accessToken}),\n getMe: (accessToken) => request<CustomerSummary>(\"/shop/me\", {bearer: accessToken}),\n getConsents: (accessToken) => request<ConsentStatus[]>(\"/shop/consents\", {bearer: accessToken}),\n updateConsents: (accessToken, consents) =>\n request<string>(\"/shop/consents\", {method: \"POST\", bearer: accessToken, body: {consents}}),\n\n // ── 예약(SERVICE 상품) ───────────────────────────────────\n availability: (params, options) =>\n request<AvailabilitySlot[]>(\"/public/booking/availability\", {\n query: {productId: params.productId, from: params.from, to: params.to},\n next: options?.tags ? {tags: options.tags} : undefined,\n }),\n createBooking: (accessToken, input) =>\n request<Booking>(\"/shop/booking/bookings\", {method: \"POST\", body: input, bearer: accessToken}),\n myBookings: (accessToken) => request<Booking[]>(\"/shop/booking/bookings\", {bearer: accessToken}),\n cancelBooking: (accessToken, bookingCode) =>\n request<string>(`/shop/booking/bookings/${encodeURIComponent(bookingCode)}/cancel`, {\n method: \"POST\",\n bearer: accessToken,\n }),\n rescheduleBooking: (accessToken, bookingCode, newSlotId) =>\n request<Booking>(`/shop/booking/bookings/${encodeURIComponent(bookingCode)}/reschedule`, {\n method: \"POST\",\n body: {newSlotId},\n bearer: accessToken,\n }),\n\n // ── 커머스: 장바구니 ────────────────────────────────────\n getCart: (session) => request<Cart>(\"/shop/cart\", shopInit(session)),\n addToCart: (variantId, quantity, session) =>\n request<Cart>(\"/shop/cart/items\", shopInit(session, {method: \"POST\", body: {variantId, quantity}})),\n updateCartItem: (variantId, quantity, session) =>\n request<Cart>(`/shop/cart/items/${variantId}`, shopInit(session, {method: \"PATCH\", body: {quantity}})),\n removeFromCart: (variantId, session) =>\n request<Cart>(`/shop/cart/items/${variantId}`, shopInit(session, {method: \"DELETE\"})),\n clearCart: (session) => request<Cart>(\"/shop/cart\", shopInit(session, {method: \"DELETE\"})),\n\n // ── 커머스: 결제·주문·배송 ──────────────────────────────\n checkout: (input, session, idempotencyKey) =>\n request<OrderDetail>(\"/shop/checkout\", shopInit(session, {method: \"POST\", body: input, idempotencyKey})),\n confirmPayment: (orderNo, providerParams, access) =>\n request<void>(\n `/shop/orders/${encodeURIComponent(orderNo)}/payment/confirm`,\n accessInit(access, {method: \"POST\", body: providerParams}),\n ),\n startPayment: (orderNo, access) =>\n request<PaymentSession>(\n `/shop/orders/${encodeURIComponent(orderNo)}/payment/session`,\n accessInit(access, {method: \"POST\"}),\n ),\n getOrder: (orderNo, access) =>\n request<OrderDetail>(`/shop/orders/${encodeURIComponent(orderNo)}`, accessInit(access)),\n listMyOrders: (accessToken, params) =>\n request<Paginated<OrderSummary>>(\"/shop/orders\", {\n bearer: accessToken,\n query: {page: params?.page, size: params?.size},\n }),\n cancelOrder: (orderNo, access) =>\n request<OrderDetail>(\n `/shop/orders/${encodeURIComponent(orderNo)}/cancel`,\n accessInit(access, {method: \"POST\"}),\n ),\n completeOrder: (orderNo, access) =>\n request<OrderDetail>(\n `/shop/orders/${encodeURIComponent(orderNo)}/complete`,\n accessInit(access, {method: \"POST\"}),\n ),\n getShipment: (orderNo, access) =>\n request<ShipmentInfo>(`/shop/orders/${encodeURIComponent(orderNo)}/shipment`, accessInit(access)),\n };\n}\n\n/** [ReadOptions] 를 request init 의 Next 캐시 옵션으로 변환. 태그 없으면 빈 객체(하위호환). */\nfunction nextInit(options?: ReadOptions): {next?: {tags?: string[]}} {\n return options?.tags && options.tags.length > 0 ? {next: {tags: options.tags}} : {};\n}\n\n/** 장바구니·결제 요청의 사용자 식별을 request init 으로 변환. */\nfunction shopInit(\n session: ShopSession,\n extra?: {method?: string; body?: unknown; idempotencyKey?: string},\n): {method?: string; body?: unknown; bearer?: string; cartSession?: string; idempotencyKey?: string} {\n return {...extra, bearer: session.accessToken, cartSession: session.cartSessionKey};\n}\n\n/** 주문 접근(본인 토큰 또는 게스트 phone)을 request init 으로 변환. */\nfunction accessInit(\n access: OrderAccess,\n extra?: {method?: string; body?: unknown},\n): {method?: string; body?: unknown; bearer?: string; query?: Record<string, unknown>} {\n return {\n ...extra,\n bearer: access.accessToken,\n query: access.phone ? {phone: access.phone} : undefined,\n };\n}\n\nfunction safeJsonParse(text: string): unknown {\n try {\n return JSON.parse(text);\n } catch {\n return null;\n }\n}\n"]}
1
+ {"version":3,"sources":["../src/errors.ts","../src/client.ts"],"names":["options"],"mappings":"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","file":"index.js","sourcesContent":["import type {ApiErrorBody, ValidationError} from \"./types\";\n\n/**\n * zalkera API 호출 실패.\n *\n * 백엔드의 `ErrorResponse` 를 그대로 담는다.\n *\n * **원인 분기는 [code] 로 한다** — 한 엔드포인트가 같은 상태코드로 여러 원인을 낸다(checkout 의 409 =\n * `OUT_OF_STOCK`·`ITEM_NOT_PURCHASABLE`·`IDEMPOTENCY_CONFLICT` — 처방이 전부 다르다). `message` 는\n * 사람용 한국어라 **문자열 매칭 금지**.\n *\n * `status` 는 거친 분류에만:\n * - `400` + [validationErrors] — 입력값 문제(폼 필드별 메시지 노출)\n * - `404` — 없는 slug/리소스\n * - `429` — IP 레이트리밋(문의 남발). [isRateLimited] 로 편히 판별\n * - `5xx` — 서버 오류\n *\n * 네트워크 자체가 실패했거나 응답이 JSON 이 아니면 [status] 가 0 이고 [body] 가 null 이다.\n */\nexport class ZalkeraError extends Error {\n /** HTTP 상태. 네트워크 실패·비JSON 응답이면 0. */\n readonly status: number;\n /**\n * **기계 판독 에러 코드** — `ErrorResponse.errorCode`(백엔드 `ErrorCode` enum 이름).\n * 예: `OUT_OF_STOCK`·`IDEMPOTENCY_CONFLICT`·`CART_NOT_FOUND`. 이걸로 분기한다.\n *\n * 코드를 안 싣는 구버전 백엔드 응답에서는 `error`(HTTP 사유구, \"Conflict\")로 **폴백**한다 —\n * 그때는 원인을 가를 수 없으니, 폴백 값에 의존하는 분기를 짜지 마라.\n * 열린 계약이라 문자열이다(서버가 코드를 추가해도 SDK 릴리스가 필요 없다).\n */\n readonly code: string | null;\n /** 검증 실패 시 필드별 메시지. 그 외엔 빈 배열. */\n readonly validationErrors: ValidationError[];\n /** 파싱된 원본 에러 본문(있으면). */\n readonly body: ApiErrorBody | null;\n\n constructor(\n message: string,\n options: {\n status: number;\n code?: string | null;\n validationErrors?: ValidationError[];\n body?: ApiErrorBody | null;\n cause?: unknown;\n },\n ) {\n super(message, {cause: options.cause});\n this.name = \"ZalkeraError\";\n this.status = options.status;\n this.code = options.code ?? null;\n this.validationErrors = options.validationErrors ?? [];\n this.body = options.body ?? null;\n }\n\n /** IP 레이트리밋(429) — 문의 폼에서 \"잠시 후 다시\" 를 띄울 때 쓴다. */\n get isRateLimited(): boolean {\n return this.status === 429;\n }\n\n /**\n * 스토어프론트 시크릿 키 문제(memo78) — 개발자 오배선 신호. `true` 면 `secretKey` 옵션을\n * 점검하라(미설정·불일치·폐기). [code] 로 정밀 분기: `STOREFRONT_KEY_REQUIRED`(401·키 필요)·\n * `TENANT_MISMATCH`(403·키↔tenant 불일치).\n */\n get isStorefrontKeyError(): boolean {\n return this.code === \"STOREFRONT_KEY_REQUIRED\" || this.code === \"TENANT_MISMATCH\";\n }\n\n /** 응답 본문(파싱된 것)으로부터 에러를 만든다. */\n static fromBody(status: number, body: unknown): ZalkeraError {\n if (isApiErrorBody(body)) {\n const code = body.errorCode ?? body.error;\n return new ZalkeraError(\n // 스토어프론트 키 오배선(memo78)은 개발자용 안내로 메시지를 덮는다 — 백엔드는 정보 누출\n // 최소화로 두루뭉술한 401 을 주므로, SDK 가 \"무엇을 고쳐야 하는지\"를 또렷이 알려준다.\n STOREFRONT_KEY_MESSAGES[code as string] ?? body.message,\n {\n // errorCode(기계 판독)를 우선, 없으면 error(HTTP 사유구)로 폴백 — 구버전 백엔드 안전.\n status,\n code,\n validationErrors: body.errors ?? [],\n body,\n },\n );\n }\n // 비JSON 에러 바디(게이트웨이 HTML·빈 바디). 502/503/504 는 상류 장애로 안내 메시지 + 코드 부여.\n const gateway = status === 502 || status === 503 || status === 504;\n return new ZalkeraError(\n gateway ? \"서비스에 일시적인 문제가 있습니다. 잠시 후 다시 시도해주세요.\" : `요청이 실패했습니다 (HTTP ${status})`,\n {status, code: gateway ? \"UPSTREAM_UNAVAILABLE\" : null},\n );\n }\n}\n\n/**\n * 스토어프론트 시크릿 키(memo78) 관련 에러코드 → 개발자용 안내 메시지. 백엔드 원문 메시지를 이걸로\n * 덮는다(백엔드는 열거 방지로 두루뭉술하게 답한다 — SDK 가 처방을 또렷이 준다).\n */\nconst STOREFRONT_KEY_MESSAGES: Record<string, string> = {\n STOREFRONT_KEY_REQUIRED:\n \"스토어프론트 시크릿 키가 필요합니다. createZalkeraClient 의 secretKey 옵션을 설정하세요 \" +\n \"(파트너 콘솔에서 발급 → 서버 .env 의 ONEQUE_STOREFRONT_KEY). 브라우저 번들에 넣지 마세요.\",\n TENANT_MISMATCH:\n \"secretKey 가 tenant 옵션과 다른 테넌트의 키입니다. 두 값이 같은 테넌트인지 확인하세요.\",\n};\n\nfunction isApiErrorBody(value: unknown): value is ApiErrorBody {\n return (\n typeof value === \"object\" &&\n value !== null &&\n \"message\" in value &&\n typeof (value as {message: unknown}).message === \"string\"\n );\n}\n","import {ZalkeraError} from \"./errors\";\nimport type {\n ApiResponse,\n AuthTokens,\n AvailabilityParams,\n AvailabilitySlot,\n Booking,\n Cart,\n Category,\n CheckoutInput,\n CreateBookingInput,\n CreateReviewInput,\n ConsentInput,\n ConsentStatus,\n CustomerSummary,\n InquiryCreated,\n InquiryInput,\n LeadCreated,\n LeadInput,\n ListPostsParams,\n ListReviewsParams,\n MediaUrl,\n Menu,\n OrderDetail,\n OrderSummary,\n PageContent,\n ListProductsParams,\n Paginated,\n PaymentSession,\n PostDetail,\n PostSummary,\n ProductCategory,\n ProductDetail,\n ProductSummary,\n RatingSummary,\n Review,\n ShipmentInfo,\n SiteConfig,\n SocialLoginInput,\n} from \"./types\";\n\n/**\n * ISR 읽기 옵션 — Next.js 캐시 태그(memo31 §0-1). RSC/ISR 페이지에서 읽기 메서드에 넘기면\n * 그 fetch 에 `next.tags` 가 실려, 백엔드가 `revalidateTag(tag)` 로 온디맨드 무효화할 수 있다.\n * 태그 컨벤션: `site-config`(사이트 설정·테마·레이아웃), `products`(카탈로그), `product:{slug}`(특정 상품).\n * 넘기지 않으면 세그먼트 기본 캐시(페이지의 `revalidate` 주기)만 적용된다 — 하위호환 유지.\n */\nexport interface ReadOptions {\n /** Next ISR 캐시 태그. 이 태그로 백엔드가 온디맨드 revalidate 한다. */\n tags?: string[];\n}\n\nexport interface ZalkeraClientOptions {\n /**\n * 백엔드 베이스 URL — `/api` 접두사는 붙이지 않는다. 예: `http://localhost:8100`.\n * 클라이언트가 경로에 `/api/public/...` 를 붙인다.\n */\n baseUrl: string;\n /**\n * 테넌트 코드. 모든 요청에 `X-Tenant` 헤더로 실린다.\n *\n * 공개 API 는 이 헤더를 그대로 믿는다(비인증). 그래서 이 클라이언트는 **서버 사이드**\n * (RSC·route handler·server action)에서 쓰는 것을 전제로 한다 — 브라우저에서 직접 부르면\n * baseUrl 이 노출되고 CORS 를 열어야 한다. README 참고.\n *\n * [secretKey] 를 함께 주면 백엔드는 키로 테넌트를 결정하고(키가 정본), 이 값은 대조용으로만\n * 쓰인다. 이행기(dual)에는 둘 다 보내도 무방하며 일치해야 한다(불일치 시 403 `TENANT_MISMATCH`).\n */\n tenant: string;\n /**\n * 스토어프론트 서버 시크릿 키(선택·`oqsk_…`). 주면 모든 요청에 `X-Storefront-Key` 헤더로 실려,\n * 백엔드가 이 키로 테넌트 신원을 증명한다(memo78 — [tenant] 무인증 신뢰의 보안 승격).\n *\n * ⚠️ **진짜 비밀이다.** 오직 서버 `.env`(예: `ONEQUE_STOREFRONT_KEY`)에만 두고, 브라우저 번들에\n * 절대 넣지 마라 — `NEXT_PUBLIC_*` 접두사·클라이언트 컴포넌트 import 금지. 이 클라이언트가 서버\n * 전용인 이유가 그것이다. 유출 시 콘솔에서 revoke·재발급.\n *\n * 안 주면 종전대로 [tenant](`X-Tenant`)만으로 동작한다(dual 이행기 하위호환). 백엔드가 `required`\n * 모드인데 키가 없으면 401 `STOREFRONT_KEY_REQUIRED` → [ZalkeraError].\n */\n secretKey?: string;\n /**\n * `fetch` 구현 주입(선택). 기본은 전역 `fetch`(Node 18+·브라우저). 테스트·커스텀 에이전트·\n * Next.js 의 `fetch` 캐시 옵션을 감싸는 래퍼를 넣을 때 쓴다.\n *\n * (내부 `request` 전송부는 이 주입점을 데이터 소스 seam 으로 유지한다 — 미래의 로컬 픽스처(mock)\n * 모드를 갈아엎지 않고 얹기 위한 여지다. mock 구현은 현재 없다 — memo78 §14 후속.)\n */\n fetch?: typeof fetch;\n /** 모든 요청에 추가할 헤더(선택). */\n headers?: Record<string, string>;\n /** 요청 타임아웃(ms). 기본 10초. */\n timeoutMs?: number;\n}\n\nconst DEFAULT_TIMEOUT_MS = 10_000;\n\n/**\n * zalkera 공개 API 클라이언트.\n *\n * 테넌트 사이트(credium 등)가 백엔드의 공개 엔드포인트를 타입 안전하게 부르기 위한 얇은 래퍼다.\n * 런타임 의존성이 없다 — 전역 `fetch` 만 쓴다.\n *\n * ```ts\n * const cms = createZalkeraClient({ baseUrl: process.env.API_BASE_URL!, tenant: \"credium\" });\n * const posts = await cms.listPosts({ size: 10, sort: \"publishedAt,desc\" });\n * await cms.submitInquiry({ name, email, subject, message });\n * ```\n *\n * 모든 메서드는 성공 시 envelope 안쪽 `data` 를 돌려주고, 실패 시 [ZalkeraError] 를 던진다.\n */\nexport interface ZalkeraClient {\n /** 회사 정보·테마·SEO 기본값. ISR 페이지는 [ReadOptions.tags] 로 캐시 태그를 실을 수 있다. */\n getSiteConfig(options?: ReadOptions): Promise<SiteConfig>;\n /** 살아 있는 카테고리 전부(페이징 없음). */\n listCategories(): Promise<Category[]>;\n /** 발행된 글 목록(페이징). */\n listPosts(params?: ListPostsParams): Promise<Paginated<PostSummary>>;\n /** slug 로 글 상세(본문 포함). 없으면 404 → [ZalkeraError]. */\n getPost(slug: string): Promise<PostDetail>;\n /**\n * 조회 비콘 — 같은 뷰어의 같은 날 재조회는 집계되지 않는다(반환 false).\n * 화면 렌더를 막지 않도록 fire-and-forget 으로 부르길 권한다(실패해도 페이지는 살아야 한다).\n *\n * 조회 dedup 은 뷰어 IP·UA 를 쓴다. 서버 사이드에서 부를 때는 [RequestContext.clientIp] 로\n * 원 방문자 IP 를 넘겨야 방문자별로 집계된다 — 안 넘기면 전부 서버 IP 하나로 뭉친다.\n */\n recordPostView(slug: string, context?: RequestContext): Promise<boolean>;\n /** slug 로 고정 페이지. */\n /**\n * slug 로 고정 페이지(섹션 포함). 없으면 404 → [ZalkeraError].\n * ISR 페이지는 [ReadOptions.tags] 로 캐시 태그를 실을 수 있다 — 백엔드가 발행 시 그 태그만\n * 콕 집어 revalidate 한다.\n */\n getPage(slug: string, options?: ReadOptions): Promise<PageContent>;\n /** 메뉴 트리(HEADER·FOOTER). */\n listMenus(options?: ReadOptions): Promise<Menu[]>;\n /** 미디어 presigned 다운로드 URL(만료 있음). */\n getMediaUrl(id: number): Promise<MediaUrl>;\n /**\n * 문의 접수. 성공 시 생성된 문의 id. 레이트리밋이면 429 → `error.isRateLimited`.\n *\n * ⚠️ 서버 사이드(테넌트 route handler)에서 부를 때는 [RequestContext.clientIp] 로 **원 방문자\n * IP 를 반드시 넘겨야 한다.** 백엔드의 문의 레이트리밋·IP 기록은 `X-Forwarded-For` 첫 홉을\n * 쓰는데, 안 넘기면 백엔드가 테넌트 서버 IP 하나만 보고 몇 건 뒤 **모든 방문자**를 429 로 막는다.\n */\n submitInquiry(input: InquiryInput, context?: RequestContext): Promise<InquiryCreated>;\n /**\n * 광고 리드 접수. 문의와 달리 이메일이 선택이고 UTM 추적을 함께 보낸다.\n *\n * ⚠️ [submitInquiry] 와 같은 이유로 서버 사이드에서는 [RequestContext.clientIp] 로 원 방문자\n * IP 를 넘겨야 한다 — 백엔드 리드 레이트리밋·IP 기록이 그 값을 본다.\n */\n submitLead(input: LeadInput, context?: RequestContext): Promise<LeadCreated>;\n\n // ── 커머스: 카탈로그(공개) ──────────────────────────────────\n /** slug 로 공개 상품(ACTIVE) 상세 — variant·재고 가용여부 포함. 없으면 404. ISR 태그는 [ReadOptions]. */\n getProduct(slug: string, options?: ReadOptions): Promise<ProductDetail>;\n /** 공개 상품 목록(ACTIVE) — 카드용 요약(최저가·재고). ISR 태그는 [ReadOptions]. */\n listProducts(params?: ListProductsParams, options?: ReadOptions): Promise<Paginated<ProductSummary>>;\n /** 커머스 카테고리 목록(노출 순서). ISR 태그는 [ReadOptions]. */\n listProductCategories(options?: ReadOptions): Promise<ProductCategory[]>;\n\n // ── 커머스: 상품후기(공개) ──────────────────────────────────\n /**\n * 상품 후기 목록(공개·VISIBLE 만, 최신순, 페이징).\n *\n * 상품의 **숫자 id(productId)** 로 조회한다 — slug 가 아니다. `getProduct(slug).id` 로 획득한다(0.4.0).\n */\n listProductReviews(productId: number, params?: ListReviewsParams, options?: ReadOptions): Promise<Paginated<Review>>;\n /** 상품 평점 요약(후기 수·평균 별점). 후기 없으면 count=0·avg=0. 목록 별점 배지에 값싸게 쓴다. */\n getProductReviewSummary(productId: number, options?: ReadOptions): Promise<RatingSummary>;\n /**\n * 후기 작성 — **로그인 필수 + 구매검증**. `input.orderItemId` 는 [OrderItemLine.id](주문 상세에서 획득).\n * 서버가 본인 주문·배송완료(DELIVERED) 이상·상품 일치를 3중 확인한다. 라인당 1개(재작성 시 409 `ALREADY_REVIEWED`).\n */\n createProductReview(productId: number, input: CreateReviewInput, accessToken: string): Promise<Review>;\n\n // ── 커머스: 고객 인증 ───────────────────────────────────────\n /** 소셜 로그인 — authorization code 를 백엔드가 교환하고 고객 토큰을 발급. */\n socialLogin(input: SocialLoginInput): Promise<AuthTokens>;\n /** refresh 토큰으로 새 토큰 회전(옛 refresh 는 폐기). */\n refreshSession(refreshToken: string): Promise<AuthTokens>;\n /** 로그아웃 — 이 고객의 모든 세션 폐기. */\n logout(accessToken: string): Promise<string>;\n /** 내 정보. */\n getMe(accessToken: string): Promise<CustomerSummary>;\n /** 현재 동의 상태 조회(로그인 필수) — 마케팅 수신 토글 화면 등에서 초기값으로 쓴다. */\n getConsents(accessToken: string): Promise<ConsentStatus[]>;\n /**\n * 동의 갱신(로그인 필수, append) — 마케팅 수신 on/off 등. 넘긴 항목만 반영되고 나머지는 유지된다.\n * 성공 시 백엔드 확인 메시지를 돌려준다(최신 상태가 필요하면 [getConsents] 로 다시 읽는다).\n */\n updateConsents(accessToken: string, consents: ConsentInput[]): Promise<string>;\n\n // ── 예약(SERVICE 상품) ──────────────────────────────────────\n /**\n * 예약 가능한 슬롯 조회 — **비로그인 가능**(상품 상세에서 시간표를 보여주는 자리).\n *\n * 미래·잔여정원 있는 OPEN 슬롯만 온다. 가용 판정은 [AvailabilitySlot.availableCount] 로 하고,\n * 카탈로그의 `inStock`/`available` 을 쓰지 마라 — 예약은 슬롯마다 정원이 따로다.\n */\n availability(params: AvailabilityParams, options?: ReadOptions): Promise<AvailabilitySlot[]>;\n /**\n * 예약 생성 — 슬롯 정원을 원자적으로 소비한다. **로그인 필수**(게스트 예약 없음).\n *\n * 무료 예약은 즉시 `CONFIRMED`. **유료·예약금이면 `status=PENDING` + [Booking.orderNo] 가 실려 오고**,\n * 그 orderNo 로 [startPayment] 를 불러 **기존 결제 흐름을 그대로 탄다**(예약 전용 결제 API 는 없다).\n *\n * 정원 소진은 `SLOT_FULL` — 슬롯 목록이 stale 했다는 뜻이니 [availability] 를 다시 읽어 보여줘라.\n */\n createBooking(accessToken: string, input: CreateBookingInput): Promise<Booking>;\n /** 내 예약 목록(최신순). 로그인 필수. */\n myBookings(accessToken: string): Promise<Booking[]>;\n /** 예약 취소 — 시작 전·무료 예약만. 정원이 복원된다. 로그인 필수. */\n cancelBooking(accessToken: string, bookingCode: string): Promise<string>;\n /** 같은 상품의 다른 슬롯으로 이동(신 슬롯 소비·구 슬롯 복원). 로그인 필수. */\n rescheduleBooking(accessToken: string, bookingCode: string, newSlotId: number): Promise<Booking>;\n\n // ── 커머스: 장바구니 ────────────────────────────────────────\n /**\n * 장바구니 조회. 로그인은 accessToken, 게스트는 cartSessionKey 로 식별.\n *\n * ⚠️ **예약(SERVICE) 상품은 장바구니에 담기지 않는다** — [addToCart] 가 400 으로 거부한다.\n * 예약은 [createBooking] 으로 별도 흐름을 탄다(카트에 슬롯을 실으면 체크아웃이 오염된다).\n */\n getCart(session: ShopSession): Promise<Cart>;\n /**\n * 담기(같은 variant 는 수량 누적).\n *\n * ⚠️ **예약(SERVICE) 상품은 400 으로 거부된다** — 예약은 [createBooking] 이다. 상품 상세에서\n * `productType === \"SERVICE\"` 면 담기 버튼 대신 슬롯 선택을 그려라.\n */\n addToCart(variantId: number, quantity: number, session: ShopSession): Promise<Cart>;\n /** 수량 변경. */\n updateCartItem(variantId: number, quantity: number, session: ShopSession): Promise<Cart>;\n /** 항목 삭제. */\n removeFromCart(variantId: number, session: ShopSession): Promise<Cart>;\n /** 비우기. */\n clearCart(session: ShopSession): Promise<Cart>;\n\n // ── 커머스: 결제·주문·배송 ──────────────────────────────────\n /**\n * 결제(주문 생성) — 스냅샷 동결 + 재고 차감. 상태=PENDING_PAYMENT.\n *\n * [idempotencyKey] 를 주면 **같은 키·같은 입력의 재요청이 새 주문을 만들지 않고 원주문을 그대로\n * 반환**한다(더블클릭·네트워크 재시도 안전). 같은 키로 **다른 입력**을 보내면 409.\n *\n * **키는 \"이 체크아웃 1건\"에 고정된 값이어야 한다** — 호출마다 새로 만들면 재시도가 서로 다른\n * 키를 들고 가서 아무것도 막지 못한다(그래서 SDK 가 자동 생성해 주지 않는다). 안정적인 소스를\n * 쓴다: 게스트는 **장바구니 세션 키**(카트 1개 → 주문 1건이라 의미가 정확히 맞는다),\n * 그 외에는 결제 시도 시작 시 한 번 만들어 재시도 동안 보관한 값.\n *\n * 안 주면 종전 동작 — 중복 제출은 카트가 이미 소비돼 404(`CART_NOT_FOUND`)로 떨어진다.\n */\n checkout(input: CheckoutInput, session: ShopSession, idempotencyKey?: string): Promise<OrderDetail>;\n /**\n * 결제 시작. **벤더에 따라 두 갈래**(테넌트가 자기 PG 를 고른다 — 기본 TOSS):\n * `session.widget` 이 있으면 위젯형(내 사이트에서 결제창 → [confirmPayment] 로 승인 확정),\n * 없으면 리다이렉트형(`session.paymentUrl` 로 보내면 끝).\n */\n startPayment(orderNo: string, access: OrderAccess): Promise<PaymentSession>;\n /**\n * **위젯형 벤더의 승인 확정**(토스 등). 결제창이 성공 URL 로 돌려준 파라미터를 그대로 넘긴다\n * (토스: `{paymentKey, orderId, amount}` — URL 쿼리에서 읽어 통째로).\n *\n * **금액을 넘겨도 서버는 안 믿는다** — 서버가 저장한 주문 금액으로 PG 에 직접 묻는다. 그래서\n * 브라우저에서 금액을 조작해도 승인되지 않는다.\n *\n * 이미 승인된 주문(웹훅 선착)이면 조용히 통과한다 — 재호출이 안전하다.\n * 리다이렉트형 벤더에 부르면 404(`PG_CONFIG_NOT_FOUND`).\n */\n confirmPayment(orderNo: string, providerParams: Record<string, string>, access: OrderAccess): Promise<void>;\n /** 주문 조회 — 본인(accessToken) 또는 게스트(phone). */\n getOrder(orderNo: string, access: OrderAccess): Promise<OrderDetail>;\n /** 내 주문 목록(로그인 필수). */\n listMyOrders(accessToken: string, params?: {page?: number; size?: number}): Promise<Paginated<OrderSummary>>;\n /** 주문 취소(미결제만) — 본인 또는 게스트(phone). */\n cancelOrder(orderNo: string, access: OrderAccess): Promise<OrderDetail>;\n /** 구매 확정(배송완료 후) — 본인 또는 게스트(phone). */\n completeOrder(orderNo: string, access: OrderAccess): Promise<OrderDetail>;\n /** 배송 조회 — 본인 또는 게스트(phone). */\n getShipment(orderNo: string, access: OrderAccess): Promise<ShipmentInfo>;\n}\n\n/**\n * 장바구니·결제의 사용자 식별. 로그인 고객은 [accessToken], 게스트는 [cartSessionKey]\n * (브라우저가 만든 안정적 익명 키)를 넘긴다. 로그인 시 게스트 카트→고객 카트 병합은 백엔드 몫.\n */\nexport interface ShopSession {\n accessToken?: string;\n cartSessionKey?: string;\n}\n\n/**\n * 주문 조회·취소·배송조회의 접근 크리덴셜. 로그인 고객은 [accessToken] 으로 소유권을,\n * 게스트는 주문 시 남긴 [phone] 으로 확인한다(주문번호 + 연락처).\n */\nexport interface OrderAccess {\n accessToken?: string;\n phone?: string;\n}\n\n/**\n * IP 민감 엔드포인트(문의·조회 비콘)에서 원 방문자를 백엔드에 알리는 컨텍스트.\n *\n * 테넌트 사이트는 서버 사이드에서 이 클라이언트를 부르므로, 백엔드가 보는 소스 IP 는 방문자가\n * 아니라 테넌트 서버다. 방문자 IP 를 `X-Forwarded-For` 로 실어 백엔드가 방문자별로 판단하게 한다.\n */\nexport interface RequestContext {\n /** 원 방문자 IP — route handler 에서 요청 헤더(x-forwarded-for·x-real-ip)로 뽑아 넘긴다. */\n clientIp?: string;\n}\n\nexport function createZalkeraClient(options: ZalkeraClientOptions): ZalkeraClient {\n const baseUrl = options.baseUrl.replace(/\\/+$/, \"\");\n const fetchImpl = options.fetch ?? globalThis.fetch;\n if (typeof fetchImpl !== \"function\") {\n throw new Error(\n \"@zalkera/client: 전역 fetch 가 없습니다. Node 18+ 를 쓰거나 options.fetch 를 주입하세요.\",\n );\n }\n const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n\n async function request<T>(\n path: string,\n init?: {\n method?: string;\n body?: unknown;\n query?: Record<string, unknown>;\n context?: RequestContext;\n /** 고객 access 토큰 → Authorization: Bearer. */\n bearer?: string;\n /** 게스트 장바구니 키 → X-Cart-Session. */\n cartSession?: string;\n /** 멱등키 → Idempotency-Key. 같은 키·같은 본문의 재요청은 원주문을 재생한다. */\n idempotencyKey?: string;\n /** Next.js ISR 캐시 옵션 — RSC/ISR fetch 에 `next.tags` 를 실어 온디맨드 revalidate 대상으로 만든다. */\n next?: {tags?: string[]};\n },\n ): Promise<T> {\n const url = new URL(`${baseUrl}/api${path}`);\n if (init?.query) {\n for (const [key, value] of Object.entries(init.query)) {\n if (value != null) url.searchParams.set(key, String(value));\n }\n }\n\n const headers: Record<string, string> = {\n \"X-Tenant\": options.tenant,\n Accept: \"application/json\",\n ...options.headers,\n };\n // 스토어프론트 서버 시크릿(memo78) — 있으면 전 요청에 붙는다. 키가 테넌트 신원의 정본이고\n // X-Tenant 는 대조용으로 함께 간다(dual). 순수 옵션 전달 — 값은 코드에 박히지 않는다.\n if (options.secretKey) headers[\"X-Storefront-Key\"] = options.secretKey;\n if (init?.body != null) headers[\"Content-Type\"] = \"application/json\";\n // 원 방문자 IP 를 백엔드에 전달 — 문의 레이트리밋·조회 dedup·IP 기록이 이걸 본다.\n if (init?.context?.clientIp) headers[\"X-Forwarded-For\"] = init.context.clientIp;\n // 커머스 인증 — 로그인 고객은 Bearer, 게스트 장바구니는 세션 키.\n if (init?.bearer) headers[\"Authorization\"] = `Bearer ${init.bearer}`;\n if (init?.cartSession) headers[\"X-Cart-Session\"] = init.cartSession;\n if (init?.idempotencyKey) headers[\"Idempotency-Key\"] = init.idempotencyKey;\n\n // 타임아웃 — 행 걸린 연결이 페이지 렌더를 무한정 붙들지 않게.\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n\n let response: Response;\n try {\n // Next.js 는 표준 RequestInit 에 비표준 `next`(캐시 태그/주기)를 확장한다. 표준 DOM 타입엔\n // 없으므로 교차 타입으로 선언해 (있을 때만) 실어 보낸다 — 일반 fetch 는 이 필드를 무시한다.\n const fetchInit: RequestInit & {next?: {tags?: string[]}} = {\n method: init?.method ?? \"GET\",\n headers,\n body: init?.body != null ? JSON.stringify(init.body) : undefined,\n signal: controller.signal,\n };\n if (init?.next) fetchInit.next = init.next;\n response = await fetchImpl(url.toString(), fetchInit);\n } catch (cause) {\n const aborted = cause instanceof Error && cause.name === \"AbortError\";\n throw new ZalkeraError(\n aborted ? `요청 시간이 초과되었습니다 (${timeoutMs}ms)` : \"네트워크 요청이 실패했습니다.\",\n {status: 0, cause},\n );\n } finally {\n clearTimeout(timer);\n }\n\n // 204 등 본문 없는 응답은 파싱하지 않는다.\n const text = await response.text();\n const parsed = text ? safeJsonParse(text) : null;\n\n if (!response.ok) {\n throw ZalkeraError.fromBody(response.status, parsed);\n }\n // 빈 바디(204·void 엔드포인트 예: confirmPayment)는 정상 — data 접근 없이 통과.\n if (!text) return undefined as T;\n // 성공(2xx)인데 envelope 가 아니면(HTML·비객체 JSON) 게이트웨이/프록시 오염이다.\n // (null).data TypeError 로 새지 않게 구조화 에러로 변환 — BFF errorResponse 가 502 로 서빙(memo 57 §11-4).\n if (parsed === null || typeof parsed !== \"object\") {\n throw new ZalkeraError(\n `서버 응답 형식이 올바르지 않습니다. 잠시 후 다시 시도해주세요. (HTTP ${response.status})`,\n {status: 502, code: \"UPSTREAM_NON_JSON\"},\n );\n }\n return (parsed as ApiResponse<T>).data;\n }\n\n return {\n getSiteConfig: (options) => request<SiteConfig>(\"/public/site-config\", nextInit(options)),\n listCategories: () => request<Category[]>(\"/public/categories\"),\n listPosts: (params) =>\n request<Paginated<PostSummary>>(\"/public/posts\", {\n query: {\n category: params?.category,\n page: params?.page,\n size: params?.size,\n sort: params?.sort,\n },\n }),\n getPost: (slug) => request<PostDetail>(`/public/posts/${encodeURIComponent(slug)}`),\n recordPostView: (slug, context) =>\n request<boolean>(`/public/posts/${encodeURIComponent(slug)}/view`, {\n method: \"POST\",\n context,\n }),\n getPage: (slug, options) =>\n request<PageContent>(`/public/pages/${encodeURIComponent(slug)}`, nextInit(options)),\n listMenus: (options) =>\n request<Menu[]>(\"/public/menus\", {next: options?.tags ? {tags: options.tags} : undefined}),\n getMediaUrl: (id) => request<MediaUrl>(`/public/media/${id}/url`),\n submitInquiry: (input, context) =>\n request<InquiryCreated>(\"/public/inquiries\", {\n method: \"POST\",\n body: input,\n context,\n }),\n submitLead: (input, context) =>\n request<LeadCreated>(\"/public/leads\", {\n method: \"POST\",\n body: input,\n context,\n }),\n\n // ── 커머스: 카탈로그 ────────────────────────────────────\n getProduct: (slug, options) =>\n request<ProductDetail>(`/public/products/${encodeURIComponent(slug)}`, nextInit(options)),\n listProducts: (params, options) =>\n request<Paginated<ProductSummary>>(\"/public/products\", {\n query: {\n productType: params?.productType,\n keyword: params?.keyword,\n page: params?.page,\n size: params?.size,\n sort: params?.sort,\n },\n ...nextInit(options),\n }),\n listProductCategories: (options) =>\n request<ProductCategory[]>(\"/public/product-categories\", nextInit(options)),\n\n // ── 커머스: 상품후기(공개) ──────────────────────────────\n listProductReviews: (productId, params, options) =>\n request<Paginated<Review>>(`/public/products/${productId}/reviews`, {\n query: {page: params?.page, size: params?.size},\n next: options?.tags ? {tags: options.tags} : undefined,\n }),\n getProductReviewSummary: (productId, options) =>\n request<RatingSummary>(`/public/products/${productId}/reviews/summary`, {\n next: options?.tags ? {tags: options.tags} : undefined,\n }),\n createProductReview: (productId, input, accessToken) =>\n request<Review>(`/shop/products/${productId}/reviews`, {method: \"POST\", body: input, bearer: accessToken}),\n\n // ── 커머스: 고객 인증 ───────────────────────────────────\n socialLogin: (input) => request<AuthTokens>(\"/shop/auth/social\", {method: \"POST\", body: input}),\n refreshSession: (refreshToken) =>\n request<AuthTokens>(\"/shop/auth/refresh\", {method: \"POST\", body: {refreshToken}}),\n logout: (accessToken) => request<string>(\"/shop/auth/logout\", {method: \"POST\", bearer: accessToken}),\n getMe: (accessToken) => request<CustomerSummary>(\"/shop/me\", {bearer: accessToken}),\n getConsents: (accessToken) => request<ConsentStatus[]>(\"/shop/consents\", {bearer: accessToken}),\n updateConsents: (accessToken, consents) =>\n request<string>(\"/shop/consents\", {method: \"POST\", bearer: accessToken, body: {consents}}),\n\n // ── 예약(SERVICE 상품) ───────────────────────────────────\n availability: (params, options) =>\n request<AvailabilitySlot[]>(\"/public/booking/availability\", {\n query: {productId: params.productId, from: params.from, to: params.to},\n next: options?.tags ? {tags: options.tags} : undefined,\n }),\n createBooking: (accessToken, input) =>\n request<Booking>(\"/shop/booking/bookings\", {method: \"POST\", body: input, bearer: accessToken}),\n myBookings: (accessToken) => request<Booking[]>(\"/shop/booking/bookings\", {bearer: accessToken}),\n cancelBooking: (accessToken, bookingCode) =>\n request<string>(`/shop/booking/bookings/${encodeURIComponent(bookingCode)}/cancel`, {\n method: \"POST\",\n bearer: accessToken,\n }),\n rescheduleBooking: (accessToken, bookingCode, newSlotId) =>\n request<Booking>(`/shop/booking/bookings/${encodeURIComponent(bookingCode)}/reschedule`, {\n method: \"POST\",\n body: {newSlotId},\n bearer: accessToken,\n }),\n\n // ── 커머스: 장바구니 ────────────────────────────────────\n getCart: (session) => request<Cart>(\"/shop/cart\", shopInit(session)),\n addToCart: (variantId, quantity, session) =>\n request<Cart>(\"/shop/cart/items\", shopInit(session, {method: \"POST\", body: {variantId, quantity}})),\n updateCartItem: (variantId, quantity, session) =>\n request<Cart>(`/shop/cart/items/${variantId}`, shopInit(session, {method: \"PATCH\", body: {quantity}})),\n removeFromCart: (variantId, session) =>\n request<Cart>(`/shop/cart/items/${variantId}`, shopInit(session, {method: \"DELETE\"})),\n clearCart: (session) => request<Cart>(\"/shop/cart\", shopInit(session, {method: \"DELETE\"})),\n\n // ── 커머스: 결제·주문·배송 ──────────────────────────────\n checkout: (input, session, idempotencyKey) =>\n request<OrderDetail>(\"/shop/checkout\", shopInit(session, {method: \"POST\", body: input, idempotencyKey})),\n confirmPayment: (orderNo, providerParams, access) =>\n request<void>(\n `/shop/orders/${encodeURIComponent(orderNo)}/payment/confirm`,\n accessInit(access, {method: \"POST\", body: providerParams}),\n ),\n startPayment: (orderNo, access) =>\n request<PaymentSession>(\n `/shop/orders/${encodeURIComponent(orderNo)}/payment/session`,\n accessInit(access, {method: \"POST\"}),\n ),\n getOrder: (orderNo, access) =>\n request<OrderDetail>(`/shop/orders/${encodeURIComponent(orderNo)}`, accessInit(access)),\n listMyOrders: (accessToken, params) =>\n request<Paginated<OrderSummary>>(\"/shop/orders\", {\n bearer: accessToken,\n query: {page: params?.page, size: params?.size},\n }),\n cancelOrder: (orderNo, access) =>\n request<OrderDetail>(\n `/shop/orders/${encodeURIComponent(orderNo)}/cancel`,\n accessInit(access, {method: \"POST\"}),\n ),\n completeOrder: (orderNo, access) =>\n request<OrderDetail>(\n `/shop/orders/${encodeURIComponent(orderNo)}/complete`,\n accessInit(access, {method: \"POST\"}),\n ),\n getShipment: (orderNo, access) =>\n request<ShipmentInfo>(`/shop/orders/${encodeURIComponent(orderNo)}/shipment`, accessInit(access)),\n };\n}\n\n/** [ReadOptions] 를 request init 의 Next 캐시 옵션으로 변환. 태그 없으면 빈 객체(하위호환). */\nfunction nextInit(options?: ReadOptions): {next?: {tags?: string[]}} {\n return options?.tags && options.tags.length > 0 ? {next: {tags: options.tags}} : {};\n}\n\n/** 장바구니·결제 요청의 사용자 식별을 request init 으로 변환. */\nfunction shopInit(\n session: ShopSession,\n extra?: {method?: string; body?: unknown; idempotencyKey?: string},\n): {method?: string; body?: unknown; bearer?: string; cartSession?: string; idempotencyKey?: string} {\n return {...extra, bearer: session.accessToken, cartSession: session.cartSessionKey};\n}\n\n/** 주문 접근(본인 토큰 또는 게스트 phone)을 request init 으로 변환. */\nfunction accessInit(\n access: OrderAccess,\n extra?: {method?: string; body?: unknown},\n): {method?: string; body?: unknown; bearer?: string; query?: Record<string, unknown>} {\n return {\n ...extra,\n bearer: access.accessToken,\n query: access.phone ? {phone: access.phone} : undefined,\n };\n}\n\nfunction safeJsonParse(text: string): unknown {\n try {\n return JSON.parse(text);\n } catch {\n return null;\n }\n}\n"]}
package/package.json CHANGED
@@ -1,49 +1,50 @@
1
1
  {
2
- "name": "@zalkera/client",
3
- "version": "0.6.1",
4
- "description": "zalkera 헤드리스 CMS 공개 API 클라이언트 (테넌트 사이트용)",
5
- "license": "MIT",
6
- "author": "Credium Co., Ltd.",
7
- "type": "module",
8
- "publishConfig": {
9
- "access": "public"
10
- },
11
- "keywords": [
12
- "oneque",
13
- "headless",
14
- "commerce",
15
- "cms",
16
- "api-client",
17
- "sdk"
18
- ],
19
- "homepage": "https://zalkera.com",
20
- "files": [
21
- "dist",
22
- "llms.txt"
23
- ],
24
- "main": "./dist/index.cjs",
25
- "module": "./dist/index.js",
26
- "types": "./dist/index.d.ts",
27
- "exports": {
28
- ".": {
29
- "types": "./dist/index.d.ts",
30
- "import": "./dist/index.js",
31
- "require": "./dist/index.cjs"
32
- }
33
- },
34
- "scripts": {
35
- "build": "tsup",
36
- "dev": "tsup --watch",
37
- "typecheck": "tsc --noEmit",
38
- "test": "vitest run",
39
- "test:watch": "vitest"
40
- },
41
- "engines": {
42
- "node": ">=18"
43
- },
44
- "devDependencies": {
45
- "tsup": "^8.3.5",
46
- "typescript": "^5.7.2",
47
- "vitest": "^2.1.8"
2
+ "name": "@zalkera/client",
3
+ "version": "0.6.2",
4
+ "description": "zalkera 헤드리스 CMS 공개 API 클라이언트 (테넌트 사이트용)",
5
+ "license": "MIT",
6
+ "author": "Credium Co., Ltd.",
7
+ "type": "module",
8
+ "publishConfig": {
9
+ "access": "public"
10
+ },
11
+ "keywords": [
12
+ "zalkera",
13
+ "headless",
14
+ "commerce",
15
+ "cms",
16
+ "api-client",
17
+ "sdk"
18
+ ],
19
+ "homepage": "https://zalkera.com",
20
+ "files": [
21
+ "dist",
22
+ "llms.txt"
23
+ ],
24
+ "main": "./dist/index.cjs",
25
+ "module": "./dist/index.js",
26
+ "types": "./dist/index.d.ts",
27
+ "exports": {
28
+ ".": {
29
+ "types": "./dist/index.d.ts",
30
+ "import": "./dist/index.js",
31
+ "require": "./dist/index.cjs"
48
32
  }
33
+ },
34
+ "scripts": {
35
+ "build": "tsup",
36
+ "dev": "tsup --watch",
37
+ "typecheck": "tsc --noEmit",
38
+ "test": "vitest run",
39
+ "test:watch": "vitest",
40
+ "prepublishOnly": "npm run typecheck && npm test && npm run build"
41
+ },
42
+ "engines": {
43
+ "node": ">=18"
44
+ },
45
+ "devDependencies": {
46
+ "tsup": "^8.3.5",
47
+ "typescript": "^5.7.2",
48
+ "vitest": "^2.1.8"
49
+ }
49
50
  }