@zalkera/client 0.7.0 → 0.7.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/README.md CHANGED
@@ -7,43 +7,6 @@
7
7
  - 동작: 응답 껍데기(`ApiResponse<T>`)를 벗겨 `data`만 반환, 실패는 `ZalkeraError`로 throw.
8
8
  - 전제: **서버 사이드 전용**(RSC·route handler·server action). 브라우저 직접 호출 미지원 ([서버 사이드 전용](#서버-사이드-전용-중요) 참고).
9
9
 
10
- ## `@oneque/client` 에서 옮겨오기
11
-
12
- 패키지 이름과 공개 심볼이 함께 바뀌었다(브랜드 개명). 옛 패키지는 발행 상태로 남지만 더는 갱신되지 않는다.
13
-
14
- ```diff
15
- - "@oneque/client": "^0.6.0"
16
- + "@zalkera/client": "^0.6.1"
17
- ```
18
-
19
- ```diff
20
- - import {createOnequeClient, OnequeError} from "@oneque/client";
21
- + import {createZalkeraClient, ZalkeraError} from "@zalkera/client";
22
- ```
23
-
24
- | 옛 이름 | 새 이름 |
25
- |---|---|
26
- | `createOnequeClient` | `createZalkeraClient` |
27
- | `OnequeError` | `ZalkeraError` |
28
- | `OnequeClient` | `ZalkeraClient` |
29
- | `OnequeClientOptions` | `ZalkeraClientOptions` |
30
-
31
- **환경변수 이름도 `ZALKERA_*` 로 바뀌었다**(2026-07-26 컷오버). 관리형 서빙(잘커라가 띄우는 사이트)은
32
- 인프라가 새 이름으로 주입하므로 할 일이 없다. **자기 인프라로 돌리는 BYO 사이트는 `.env` 의 이름을 직접
33
- 바꿔야 한다** — 구 이름 폴백은 제거됐다:
34
-
35
- | 옛 이름 | 새 이름 |
36
- |---|---|
37
- | `ONEQUE_API_BASE` | `ZALKERA_API_BASE` |
38
- | `ONEQUE_TENANT` | `ZALKERA_TENANT` |
39
- | `ONEQUE_STOREFRONT_KEY` | `ZALKERA_STOREFRONT_KEY` |
40
- | `ONEQUE_SITE_URL` | `ZALKERA_SITE_URL` |
41
-
42
- 동적 SSR 정당화 마커도 `zalkera-allow-dynamic` 이 새 이름이지만 기존 `oneque-`/`oneq-` 마커를 계속 수용한다.
43
- `x-oneque-revalidate-secret` **헤더 이름은 그대로다** — 백엔드가 보내는 와이어 이름이라 별도 전환이 필요하다.
44
-
45
- 기능·API 동작은 동일하다 — 이름만 바뀌었다.
46
-
47
10
  ## 설치
48
11
 
49
12
  ```bash
@@ -108,6 +71,7 @@ await zalkera.submitInquiry({name, email, subject, message}, {clientIp});
108
71
  | `getPost(slug)` | `GET /api/public/posts/{slug}` | `PostDetail` |
109
72
  | `recordPostView(slug, ctx?)` | `POST /api/public/posts/{slug}/view` | `boolean` |
110
73
  | `getPage(slug, options?)` | `GET /api/public/pages/{slug}` | `PageContent` |
74
+ | `listPages(params?, options?)` | `GET /api/public/pages` | `Paginated<PageSummary>` |
111
75
  | `listMenus(options?)` | `GET /api/public/menus` | `Menu[]` |
112
76
  | `getMediaUrl(id)` | `GET /api/public/media/{id}/url` | `MediaUrl` |
113
77
  | `submitInquiry(input, ctx?)` | `POST /api/public/inquiries` | `InquiryCreated` |
@@ -124,6 +88,7 @@ await zalkera.submitInquiry({name, email, subject, message}, {clientIp});
124
88
  | `getProductReviewSummary(productId, options?)` | `GET /api/public/products/{id}/reviews/summary` | `RatingSummary` |
125
89
  | `createProductReview(productId, input, accessToken)` | `POST /api/shop/products/{id}/reviews` | `Review` |
126
90
 
91
+ - `listPages`는 **열거 전용**(본문 없음 — sitemap용). `size` 상한 100.
127
92
  - 후기 조회는 **숫자 `productId`**(slug 아님) — `getProduct(slug).id`로 획득.
128
93
  - 후기 작성은 로그인 필수 + 구매검증(본인·배송완료 이상·상품 일치).
129
94
 
package/dist/index.cjs CHANGED
@@ -62,7 +62,7 @@ var ZalkeraError = class _ZalkeraError extends Error {
62
62
  }
63
63
  };
64
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.",
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 ZALKERA_STOREFRONT_KEY). \uBE0C\uB77C\uC6B0\uC800 \uBC88\uB4E4\uC5D0 \uB123\uC9C0 \uB9C8\uC138\uC694.",
66
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
67
  };
68
68
  function isApiErrorBody(value) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/errors.ts","../src/client.ts","../src/sections.ts","../src/safeUrl.ts","../src/sectionConfig.ts","../src/theme.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 ListPagesParams,\n ListPostsParams,\n ListReviewsParams,\n MediaUrl,\n Menu,\n OrderDetail,\n OrderSummary,\n PageContent,\n PageSummary,\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 /**\n * 발행된 고정 페이지 **목록**(열거 전용·본문 없음).\n *\n * sitemap 을 위한 API 다 — 이게 없으면 콘솔·AI 로 만든 페이지가 검색엔진에 열거되지 않는다.\n * 목록에 실린 slug 는 [getPage] 가 반드시 200 을 준다(가시성 술어가 서버에서 하나로 공유된다).\n */\n listPages(params?: ListPagesParams, options?: ReadOptions): Promise<Paginated<PageSummary>>;\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 listPages: (params, options) =>\n request<Paginated<PageSummary>>(\"/public/pages\", {\n query: {page: params?.page, size: params?.size},\n ...nextInit(options),\n }),\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","/**\n * 섹션 어휘 계약 — **정본의 코드 표현**(memo102 §6).\n *\n * 정본은 백엔드 레포의 `doc/contracts/section-vocabulary.json` 이고, 이 파일은 그것을 npm 으로\n * 실어 나르는 **운반체**다. 템플릿 렌더러와 콘솔 zod 스키마가 이 상수를 읽어 자기 커버리지를\n * 기계로 검사한다 — 사본이 갈라진 채 조용히 굳는 것을 막는 게 목적이지, 실시간 동일성이 목적은\n * 아니다(계약이 원래 스큐 내성으로 설계돼 있다: 미지 타입은 스킵).\n *\n * 두 레포가 갈라져 있어 상호 CI 강제가 불가능하므로 **사람 이음새가 정확히 한 곳** 남는다 —\n * 백엔드 JSON ↔ 이 파일. [SECTION_CONTRACT_REV] 를 백엔드 스펙의 `contractRev` 와 맞춰 두고,\n * client 발행 전 `scripts/sync-section-contract.mjs` 로 대조한다.\n */\n\n/** 백엔드 스펙 `contractRev` 와 같아야 한다. 어긋나면 동기 스크립트가 잡는다. */\nexport const SECTION_CONTRACT_REV = 1;\n\n/** 업종 분류 — 콘솔 섹션 픽커의 그룹핑에 쓴다(테넌트 업종 필드는 아직 없다·memo102 §5). */\nexport type SectionVertical = \"BEAUTY\" | \"GENERAL\";\n\nexport interface SectionSpec {\n readonly type: string;\n readonly vertical: SectionVertical;\n /** 이 섹션이 산출하는 schema.org 타입. null 이면 구조화 데이터 없음. */\n readonly jsonLd: string | null;\n}\n\n/**\n * 아는 섹션 전량. **순서는 콘솔 픽커의 노출 순서**(관행 아크: 주목→가치→신뢰→행동).\n * 값 추가는 백엔드 스펙을 먼저 고친 뒤 여기로 옮긴다.\n */\nexport const SECTION_CONTRACT = [\n // 뷰티(memo47) — append-only 계약상 불변\n { type: \"SERVICE_MENU\", vertical: \"BEAUTY\", jsonLd: null },\n { type: \"BEFORE_AFTER_GALLERY\", vertical: \"BEAUTY\", jsonLd: null },\n { type: \"BOOKING_CTA\", vertical: \"BEAUTY\", jsonLd: null },\n { type: \"DOCTOR_INTRO\", vertical: \"BEAUTY\", jsonLd: null },\n // 기업 마케팅(memo102 §2)\n { type: \"HERO\", vertical: \"GENERAL\", jsonLd: null },\n { type: \"FEATURE_GRID\", vertical: \"GENERAL\", jsonLd: null },\n { type: \"TEXT_MEDIA\", vertical: \"GENERAL\", jsonLd: null },\n { type: \"LOGO_WALL\", vertical: \"GENERAL\", jsonLd: null },\n { type: \"STATS_BAND\", vertical: \"GENERAL\", jsonLd: null },\n { type: \"TESTIMONIALS\", vertical: \"GENERAL\", jsonLd: null },\n { type: \"FAQ_LIST\", vertical: \"GENERAL\", jsonLd: \"FAQPage\" },\n { type: \"LEAD_CTA\", vertical: \"GENERAL\", jsonLd: null },\n] as const satisfies readonly SectionSpec[];\n\n/** 지금 렌더러가 아는 섹션 타입. 이 밖의 값이 와도 정상이다(스킵). */\nexport type KnownSectionType = (typeof SECTION_CONTRACT)[number][\"type\"];\n\n/** 업종별 필터 — 콘솔 픽커 그룹핑용. */\nexport function sectionsOfVertical(vertical: SectionVertical): readonly SectionSpec[] {\n return SECTION_CONTRACT.filter((s) => s.vertical === vertical);\n}\n","/**\n * 사용자·콘솔 입력 URL 을 링크로 쓰기 전 소독한다 — **저장형 XSS 방어**.\n *\n * 콘솔 메뉴 url·섹션 config 의 href 는 사람이 입력하는 값이라 `javascript:alert(document.cookie)` 가\n * 들어올 수 있고, 백엔드는 스킴을 검증하지 않는다(길이·trim 뿐). react-dom 이 prod 번들에서\n * `javascript:` href 를 막긴 하지만 **dev 에서는 raw 로 실리고**, 프레임워크 내부 동작에 보안을\n * 의존하는 것 자체가 취약하다. 그래서 이건 **심층 방어**다 — 렌더 소스에서 스킴을 거른다.\n *\n * 허용: 내부 절대경로(`/about`)·http(s)·mailto·tel. 그 외(javascript:·data:·vbscript: 등)는 `#` 으로\n * 무력화한다(링크는 남되 스크립트는 안 돈다). 메뉴는 외부 링크가 정당하므로 내부로만 좁히지 않고\n * 스킴 허용목록을 쓴다.\n */\nconst ALLOWED_SCHEMES = new Set([\"http:\", \"https:\", \"mailto:\", \"tel:\"]);\n\nexport function safeLinkUrl(raw: string | null | undefined): string {\n if (!raw) return \"#\";\n const url = raw.trim();\n // 내부 절대경로 — 스킴이 없다. `//host`(스킴 상대)는 외부라 파서로 넘겨 검증한다.\n if (url.startsWith(\"/\") && !url.startsWith(\"//\")) return url;\n try {\n const parsed = new URL(url); // 절대 URL 만 파싱된다(상대면 throw)\n return ALLOWED_SCHEMES.has(parsed.protocol) ? url : \"#\";\n } catch {\n // 파싱 실패 = 스킴 없는 상대경로(anchor#·?query 등) — 내부로 간주해 통과.\n return url.startsWith(\"//\") ? \"#\" : url;\n }\n}\n","/**\n * 섹션 config 파싱 — **절대 throw 하지 않는다.**\n *\n * config 는 백엔드가 검증하지 않는 raw JSON 이다(의도된 설계 — 사이트마다 백엔드를 배포하지\n * 않으려고). 게다가 콘솔의 raw 편집기는 **JSON 문법만** 보고 저장을 허용한다 — 즉 문법은\n * 맞는데 형상이 다른 config 가 **정상 경로로** 들어온다. `{\"productIds\": 5}` 같은 것.\n *\n * 그래서 파싱이 성공했다고 필드를 믿으면 안 된다. `.map`·`.trim` 을 그냥 부르면 그 순간\n * **페이지 전체가 500** 이 난다 — 계약은 \"그 섹션만 사라진다\" 였다. 아래 가드들이 그 계약을\n * 지키는 장치다. [parseThemeColors] 와 같은 사상.\n *\n * **이 패키지에 사는 이유**(memo108 §1): 이건 표현이 아니라 **계약을 안전하게 읽는 법**이다. 마크업은\n * 만드는 쪽 자유지만, '섹션 하나가 사이트를 죽이지 않는다'는 계약은 한 벌이어야 한다 — 사본이 갈라지면\n * 그 계약이 조용히 깨진다(471946c 교훈).\n */\nexport function parseConfig<T>(config: string | null): T | null {\n if (!config) return null;\n try {\n const parsed: unknown = JSON.parse(config);\n return parsed && typeof parsed === \"object\" && !Array.isArray(parsed) ? (parsed as T) : null;\n } catch {\n return null;\n }\n}\n\n/** 양의 정수 id 만 남긴다 — 배열이 아니면 빈 배열. */\nexport function asIdArray(value: unknown): number[] {\n if (!Array.isArray(value)) return [];\n return value.filter((v): v is number => typeof v === \"number\" && Number.isInteger(v) && v > 0);\n}\n\n/** 객체 배열만 — 아니면 빈 배열. */\nexport function asObjectArray(value: unknown): Record<string, unknown>[] {\n if (!Array.isArray(value)) return [];\n return value.filter(\n (v): v is Record<string, unknown> => v != null && typeof v === \"object\" && !Array.isArray(v),\n );\n}\n\n/** 문자열이 아니면 undefined — 숫자에 `.trim()` 을 부르는 사고를 막는다. */\nexport function asString(value: unknown): string | undefined {\n return typeof value === \"string\" ? value : undefined;\n}\n\n/** 양의 정수가 아니면 undefined. */\nexport function asId(value: unknown): number | undefined {\n return typeof value === \"number\" && Number.isInteger(value) && value > 0 ? value : undefined;\n}\n\n/** 미디어는 프록시로만 — presigned URL 을 마크업에 넣지 않는다(만료되면 깨진 이미지가 박제된다). */\nexport function mediaSrc(assetId: number): string {\n return `/media/${assetId}`;\n}\n","/**\n * 테넌트 테마 색 파서 (서버 전용).\n *\n * `SiteConfig.themeColors` 는 파싱하지 않은 raw JSON 문자열이다(백엔드 콘솔 `site.theme.update` 가\n * 넣는다). 같은 JSON 에 `layout` 등 이종 키가 병합돼 오므로 **색 키만 화이트리스트로** 읽는다.\n * 어떤 쓰레기가 와도 죽지 않는다(섹션 parse.ts 와 같은 사상) — 실패하면 빈 오버라이드(기본 테마).\n *\n * 읽어들인 색은 globals.css 의 `@theme` 변수를 `<html>` inline style 로 덮어, `bg-primary` 등\n * 전 유틸리티가 테넌트 색으로 바뀐다(layout.tsx). CSS 주입 방어를 위해 값은 hex 형식 검증 후에만 싣는다.\n *\n * **이 패키지에 사는 이유**(memo108 §1): L1(말로 색 바꾸기)의 기계는 표현이 아니라 계약이다 — 어떤 토큰을\n * 쓸지는 코드가 정하고 값은 config 가 정한다는 규약(memo65 §3)의 집행부이고, 순수 함수라 React 에 안 매인다.\n * 이 함수의 반환을 `<html style={...}>` 에 싣는 **배선은 각 사이트의 몫**이다(validator S8 이 그걸 센다).\n */\n\n/** `#rgb`/`#rrggbb` 만 통과. 임의 문자열을 style 에 싣지 않는다(CSS 주입 방어). */\nconst HEX = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;\n\n/** 콘솔 스키마의 색 키 → globals.css `@theme` 변수명 매핑. 이 키만 읽는다. */\nconst COLOR_KEYS: ReadonlyArray<readonly [string, string]> = [\n [\"primary\", \"--color-primary\"],\n [\"secondary\", \"--color-secondary\"],\n [\"background\", \"--color-background\"],\n [\"text\", \"--color-foreground\"],\n];\n\n/*\n * ── 전역 토큰 knob (memo69 §2.1 · 계약버전 2) ──────────────────────────────\n * 색과 달리 knob 은 **enum**이다. 백엔드 `site.theme.update` 화이트리스트가 허용값만 저장하지만,\n * 여기서도 매핑 테이블 lookup 으로 한 번 더 봉한다: **사용자 문자열은 어떤 경로로도 style 에 실리지\n * 않는다** — enum 은 테이블 key 조회뿐이고, 없는 값(오염·구버전)은 조용히 버려 @theme 기본값을 유지한다.\n * 이 값들은 백엔드 enum(FONTS/RADII/DENSITIES)과 반드시 일치해야 한다(계약).\n */\n\n/** 시스템 폰트 스택(globals.css --font-sans 와 동일 — system knob 의 기본값). */\nconst SYSTEM_STACK =\n 'ui-sans-serif, system-ui, -apple-system, \"Apple SD Gothic Neo\", \"Malgun Gothic\", \"Noto Sans KR\", sans-serif';\n\n/*\n * font: --font-sans 스택 교체.\n * TODO(memo69 §2.1 후속): pretendard·noto-serif-kr 의 self-host `@font-face` woff2 서브셋 자산 번들링.\n * 지금은 폰트 **패밀리명만** 스택 앞에 얹어, 사용자 환경에 설치돼 있으면 쓰고 없으면 폴백한다\n * (외부 CDN 0 유지). 자산이 동봉되면 여기 매핑은 그대로 두고 globals.css 에 @font-face 만 추가하면 된다.\n */\nconst FONTS: Readonly<Record<string, string>> = {\n system: SYSTEM_STACK,\n pretendard: `\"Pretendard\", ${SYSTEM_STACK}`,\n \"noto-serif-kr\": '\"Noto Serif KR\", ui-serif, Georgia, \"Nanum Myeongjo\", serif',\n};\n\n/** radius: --radius-knob 무단위 배수(globals.css radius 스케일이 곱한다). soft=1 이 Tailwind 기본과 일치. */\nconst RADII: Readonly<Record<string, string>> = {sharp: \"0\", soft: \"1\", round: \"2\"};\n\n/** density: --spacing 베이스(Tailwind v4 기본 0.25rem). 전 spacing 유틸리티가 일괄 스케일. */\nconst DENSITIES: Readonly<Record<string, string>> = {compact: \"0.22rem\", cozy: \"0.25rem\"};\n\n/** knob 키 → CSS 변수 → enum 매핑 테이블. */\nconst KNOB_KEYS: ReadonlyArray<readonly [string, string, Readonly<Record<string, string>>]> = [\n [\"font\", \"--font-sans\", FONTS],\n [\"radius\", \"--radius-knob\", RADII],\n [\"density\", \"--spacing\", DENSITIES],\n];\n\nexport interface ParsedTheme {\n /** `<html style={...}>` 에 그대로 얹을 CSS 변수 맵. 오버라이드가 없으면 빈 객체(기본 테마 유지). */\n cssVars: Record<string, string>;\n}\n\nexport function parseThemeColors(raw: string | null | undefined): ParsedTheme {\n const cssVars: Record<string, string> = {};\n if (!raw) return {cssVars};\n\n let obj: unknown;\n try {\n obj = JSON.parse(raw);\n } catch {\n return {cssVars};\n }\n if (obj == null || typeof obj !== \"object\" || Array.isArray(obj)) return {cssVars};\n\n const src = obj as Record<string, unknown>;\n let primaryHex: string | undefined;\n for (const [key, cssVar] of COLOR_KEYS) {\n const value = src[key];\n if (typeof value === \"string\" && HEX.test(value)) {\n cssVars[cssVar] = value;\n if (key === \"primary\") primaryHex = value;\n }\n }\n\n // 콘솔 스키마에 primary-foreground 키가 없다 — primary 가 오버라이드되면 대비가 큰 쪽을\n // 서버가 산출해 함께 주입한다(테넌트가 밝은 액센트를 골라도 CTA 글자가 안 죽는다).\n if (primaryHex) {\n cssVars[\"--color-primary-foreground\"] = onColor(primaryHex);\n }\n\n // 전역 토큰 knob(font·radius·density) — 매핑 테이블 lookup 만. hasOwnProperty 로 own key 만 채택해\n // 프로토타입 오염(constructor 등)·오염 문자열을 차단한다(사용자 문자열이 style 에 실리지 않는다).\n for (const [key, cssVar, table] of KNOB_KEYS) {\n const value = src[key];\n if (typeof value !== \"string\") continue;\n // hasOwnProperty 로 own key 만 채택한다 — 프로토타입 오염(constructor 등)을 차단하는 그 검사다.\n // 다만 그것으로 타입이 좁혀지지는 않으므로(strict), 조회 결과를 직접 확인해 싣는다.\n if (!Object.prototype.hasOwnProperty.call(table, value)) continue;\n const mapped = table[value];\n if (mapped !== undefined) cssVars[cssVar] = mapped;\n }\n\n return {cssVars};\n}\n\n/** primary 위에 얹을 글자색 — WCAG 상대 휘도로 흰색/slate-950 중 대비 큰 쪽. */\nfunction onColor(hex: string): string {\n return relativeLuminance(hex) > 0.4 ? \"#020617\" /* slate-950 */ : \"#ffffff\";\n}\n\n/** WCAG 상대 휘도(0~1). 검증된 hex 만 들어온다(HEX 통과분). */\nfunction relativeLuminance(hex: string): number {\n // `.map` 은 튜플성을 잃어 number[] 가 되고, 그러면 구조분해가 undefined 를 낳는다(strict).\n // 튜플을 유지한 채 채널별로 편다.\n const [r0, g0, b0] = toRgb(hex);\n const lin = (c: number) => {\n const s = c / 255;\n return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;\n };\n return 0.2126 * lin(r0) + 0.7152 * lin(g0) + 0.0722 * lin(b0);\n}\n\nfunction toRgb(hex: string): [number, number, number] {\n // `#rgb` 축약을 `#rrggbb` 로 편다. slice 로 자르는 것은 인덱스 접근과 달리 undefined 가 안 나온다\n // (이 패키지는 noUncheckedIndexedAccess 라 `h[0]` 은 string|undefined 다 — 검증된 hex 만 들어온다는\n // 사실을 타입으로 증명할 수 없으므로, 증명이 필요 없는 방식으로 쓴다).\n let h = hex.slice(1);\n if (h.length === 3) {\n h = h\n .split(\"\")\n .map((c) => c + c)\n .join(\"\");\n }\n return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];\n}\n"]}
1
+ {"version":3,"sources":["../src/errors.ts","../src/client.ts","../src/sections.ts","../src/safeUrl.ts","../src/sectionConfig.ts","../src/theme.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 의 ZALKERA_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 ListPagesParams,\n ListPostsParams,\n ListReviewsParams,\n MediaUrl,\n Menu,\n OrderDetail,\n OrderSummary,\n PageContent,\n PageSummary,\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`(예: `ZALKERA_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 /**\n * 발행된 고정 페이지 **목록**(열거 전용·본문 없음).\n *\n * sitemap 을 위한 API 다 — 이게 없으면 콘솔·AI 로 만든 페이지가 검색엔진에 열거되지 않는다.\n * 목록에 실린 slug 는 [getPage] 가 반드시 200 을 준다(가시성 술어가 서버에서 하나로 공유된다).\n */\n listPages(params?: ListPagesParams, options?: ReadOptions): Promise<Paginated<PageSummary>>;\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 listPages: (params, options) =>\n request<Paginated<PageSummary>>(\"/public/pages\", {\n query: {page: params?.page, size: params?.size},\n ...nextInit(options),\n }),\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","/**\n * 섹션 어휘 계약 — **정본의 코드 표현**(memo102 §6).\n *\n * 정본은 백엔드 레포의 `doc/contracts/section-vocabulary.json` 이고, 이 파일은 그것을 npm 으로\n * 실어 나르는 **운반체**다. 템플릿 렌더러와 콘솔 zod 스키마가 이 상수를 읽어 자기 커버리지를\n * 기계로 검사한다 — 사본이 갈라진 채 조용히 굳는 것을 막는 게 목적이지, 실시간 동일성이 목적은\n * 아니다(계약이 원래 스큐 내성으로 설계돼 있다: 미지 타입은 스킵).\n *\n * 두 레포가 갈라져 있어 상호 CI 강제가 불가능하므로 **사람 이음새가 정확히 한 곳** 남는다 —\n * 백엔드 JSON ↔ 이 파일. [SECTION_CONTRACT_REV] 를 백엔드 스펙의 `contractRev` 와 맞춰 두고,\n * client 발행 전 `scripts/sync-section-contract.mjs` 로 대조한다.\n */\n\n/** 백엔드 스펙 `contractRev` 와 같아야 한다. 어긋나면 동기 스크립트가 잡는다. */\nexport const SECTION_CONTRACT_REV = 1;\n\n/** 업종 분류 — 콘솔 섹션 픽커의 그룹핑에 쓴다(테넌트 업종 필드는 아직 없다·memo102 §5). */\nexport type SectionVertical = \"BEAUTY\" | \"GENERAL\";\n\nexport interface SectionSpec {\n readonly type: string;\n readonly vertical: SectionVertical;\n /** 이 섹션이 산출하는 schema.org 타입. null 이면 구조화 데이터 없음. */\n readonly jsonLd: string | null;\n}\n\n/**\n * 아는 섹션 전량. **순서는 콘솔 픽커의 노출 순서**(관행 아크: 주목→가치→신뢰→행동).\n * 값 추가는 백엔드 스펙을 먼저 고친 뒤 여기로 옮긴다.\n */\nexport const SECTION_CONTRACT = [\n // 뷰티(memo47) — append-only 계약상 불변\n { type: \"SERVICE_MENU\", vertical: \"BEAUTY\", jsonLd: null },\n { type: \"BEFORE_AFTER_GALLERY\", vertical: \"BEAUTY\", jsonLd: null },\n { type: \"BOOKING_CTA\", vertical: \"BEAUTY\", jsonLd: null },\n { type: \"DOCTOR_INTRO\", vertical: \"BEAUTY\", jsonLd: null },\n // 기업 마케팅(memo102 §2)\n { type: \"HERO\", vertical: \"GENERAL\", jsonLd: null },\n { type: \"FEATURE_GRID\", vertical: \"GENERAL\", jsonLd: null },\n { type: \"TEXT_MEDIA\", vertical: \"GENERAL\", jsonLd: null },\n { type: \"LOGO_WALL\", vertical: \"GENERAL\", jsonLd: null },\n { type: \"STATS_BAND\", vertical: \"GENERAL\", jsonLd: null },\n { type: \"TESTIMONIALS\", vertical: \"GENERAL\", jsonLd: null },\n { type: \"FAQ_LIST\", vertical: \"GENERAL\", jsonLd: \"FAQPage\" },\n { type: \"LEAD_CTA\", vertical: \"GENERAL\", jsonLd: null },\n] as const satisfies readonly SectionSpec[];\n\n/** 지금 렌더러가 아는 섹션 타입. 이 밖의 값이 와도 정상이다(스킵). */\nexport type KnownSectionType = (typeof SECTION_CONTRACT)[number][\"type\"];\n\n/** 업종별 필터 — 콘솔 픽커 그룹핑용. */\nexport function sectionsOfVertical(vertical: SectionVertical): readonly SectionSpec[] {\n return SECTION_CONTRACT.filter((s) => s.vertical === vertical);\n}\n","/**\n * 사용자·콘솔 입력 URL 을 링크로 쓰기 전 소독한다 — **저장형 XSS 방어**.\n *\n * 콘솔 메뉴 url·섹션 config 의 href 는 사람이 입력하는 값이라 `javascript:alert(document.cookie)` 가\n * 들어올 수 있고, 백엔드는 스킴을 검증하지 않는다(길이·trim 뿐). react-dom 이 prod 번들에서\n * `javascript:` href 를 막긴 하지만 **dev 에서는 raw 로 실리고**, 프레임워크 내부 동작에 보안을\n * 의존하는 것 자체가 취약하다. 그래서 이건 **심층 방어**다 — 렌더 소스에서 스킴을 거른다.\n *\n * 허용: 내부 절대경로(`/about`)·http(s)·mailto·tel. 그 외(javascript:·data:·vbscript: 등)는 `#` 으로\n * 무력화한다(링크는 남되 스크립트는 안 돈다). 메뉴는 외부 링크가 정당하므로 내부로만 좁히지 않고\n * 스킴 허용목록을 쓴다.\n */\nconst ALLOWED_SCHEMES = new Set([\"http:\", \"https:\", \"mailto:\", \"tel:\"]);\n\nexport function safeLinkUrl(raw: string | null | undefined): string {\n if (!raw) return \"#\";\n const url = raw.trim();\n // 내부 절대경로 — 스킴이 없다. `//host`(스킴 상대)는 외부라 파서로 넘겨 검증한다.\n if (url.startsWith(\"/\") && !url.startsWith(\"//\")) return url;\n try {\n const parsed = new URL(url); // 절대 URL 만 파싱된다(상대면 throw)\n return ALLOWED_SCHEMES.has(parsed.protocol) ? url : \"#\";\n } catch {\n // 파싱 실패 = 스킴 없는 상대경로(anchor#·?query 등) — 내부로 간주해 통과.\n return url.startsWith(\"//\") ? \"#\" : url;\n }\n}\n","/**\n * 섹션 config 파싱 — **절대 throw 하지 않는다.**\n *\n * config 는 백엔드가 검증하지 않는 raw JSON 이다(의도된 설계 — 사이트마다 백엔드를 배포하지\n * 않으려고). 게다가 콘솔의 raw 편집기는 **JSON 문법만** 보고 저장을 허용한다 — 즉 문법은\n * 맞는데 형상이 다른 config 가 **정상 경로로** 들어온다. `{\"productIds\": 5}` 같은 것.\n *\n * 그래서 파싱이 성공했다고 필드를 믿으면 안 된다. `.map`·`.trim` 을 그냥 부르면 그 순간\n * **페이지 전체가 500** 이 난다 — 계약은 \"그 섹션만 사라진다\" 였다. 아래 가드들이 그 계약을\n * 지키는 장치다. [parseThemeColors] 와 같은 사상.\n *\n * **이 패키지에 사는 이유**(memo108 §1): 이건 표현이 아니라 **계약을 안전하게 읽는 법**이다. 마크업은\n * 만드는 쪽 자유지만, '섹션 하나가 사이트를 죽이지 않는다'는 계약은 한 벌이어야 한다 — 사본이 갈라지면\n * 그 계약이 조용히 깨진다(471946c 교훈).\n */\nexport function parseConfig<T>(config: string | null): T | null {\n if (!config) return null;\n try {\n const parsed: unknown = JSON.parse(config);\n return parsed && typeof parsed === \"object\" && !Array.isArray(parsed) ? (parsed as T) : null;\n } catch {\n return null;\n }\n}\n\n/** 양의 정수 id 만 남긴다 — 배열이 아니면 빈 배열. */\nexport function asIdArray(value: unknown): number[] {\n if (!Array.isArray(value)) return [];\n return value.filter((v): v is number => typeof v === \"number\" && Number.isInteger(v) && v > 0);\n}\n\n/** 객체 배열만 — 아니면 빈 배열. */\nexport function asObjectArray(value: unknown): Record<string, unknown>[] {\n if (!Array.isArray(value)) return [];\n return value.filter(\n (v): v is Record<string, unknown> => v != null && typeof v === \"object\" && !Array.isArray(v),\n );\n}\n\n/** 문자열이 아니면 undefined — 숫자에 `.trim()` 을 부르는 사고를 막는다. */\nexport function asString(value: unknown): string | undefined {\n return typeof value === \"string\" ? value : undefined;\n}\n\n/** 양의 정수가 아니면 undefined. */\nexport function asId(value: unknown): number | undefined {\n return typeof value === \"number\" && Number.isInteger(value) && value > 0 ? value : undefined;\n}\n\n/** 미디어는 프록시로만 — presigned URL 을 마크업에 넣지 않는다(만료되면 깨진 이미지가 박제된다). */\nexport function mediaSrc(assetId: number): string {\n return `/media/${assetId}`;\n}\n","/**\n * 테넌트 테마 색 파서 (서버 전용).\n *\n * `SiteConfig.themeColors` 는 파싱하지 않은 raw JSON 문자열이다(백엔드 콘솔 `site.theme.update` 가\n * 넣는다). 같은 JSON 에 `layout` 등 이종 키가 병합돼 오므로 **색 키만 화이트리스트로** 읽는다.\n * 어떤 쓰레기가 와도 죽지 않는다(섹션 parse.ts 와 같은 사상) — 실패하면 빈 오버라이드(기본 테마).\n *\n * 읽어들인 색은 globals.css 의 `@theme` 변수를 `<html>` inline style 로 덮어, `bg-primary` 등\n * 전 유틸리티가 테넌트 색으로 바뀐다(layout.tsx). CSS 주입 방어를 위해 값은 hex 형식 검증 후에만 싣는다.\n *\n * **이 패키지에 사는 이유**(memo108 §1): L1(말로 색 바꾸기)의 기계는 표현이 아니라 계약이다 — 어떤 토큰을\n * 쓸지는 코드가 정하고 값은 config 가 정한다는 규약(memo65 §3)의 집행부이고, 순수 함수라 React 에 안 매인다.\n * 이 함수의 반환을 `<html style={...}>` 에 싣는 **배선은 각 사이트의 몫**이다(validator S8 이 그걸 센다).\n */\n\n/** `#rgb`/`#rrggbb` 만 통과. 임의 문자열을 style 에 싣지 않는다(CSS 주입 방어). */\nconst HEX = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;\n\n/** 콘솔 스키마의 색 키 → globals.css `@theme` 변수명 매핑. 이 키만 읽는다. */\nconst COLOR_KEYS: ReadonlyArray<readonly [string, string]> = [\n [\"primary\", \"--color-primary\"],\n [\"secondary\", \"--color-secondary\"],\n [\"background\", \"--color-background\"],\n [\"text\", \"--color-foreground\"],\n];\n\n/*\n * ── 전역 토큰 knob (memo69 §2.1 · 계약버전 2) ──────────────────────────────\n * 색과 달리 knob 은 **enum**이다. 백엔드 `site.theme.update` 화이트리스트가 허용값만 저장하지만,\n * 여기서도 매핑 테이블 lookup 으로 한 번 더 봉한다: **사용자 문자열은 어떤 경로로도 style 에 실리지\n * 않는다** — enum 은 테이블 key 조회뿐이고, 없는 값(오염·구버전)은 조용히 버려 @theme 기본값을 유지한다.\n * 이 값들은 백엔드 enum(FONTS/RADII/DENSITIES)과 반드시 일치해야 한다(계약).\n */\n\n/** 시스템 폰트 스택(globals.css --font-sans 와 동일 — system knob 의 기본값). */\nconst SYSTEM_STACK =\n 'ui-sans-serif, system-ui, -apple-system, \"Apple SD Gothic Neo\", \"Malgun Gothic\", \"Noto Sans KR\", sans-serif';\n\n/*\n * font: --font-sans 스택 교체.\n * TODO(memo69 §2.1 후속): pretendard·noto-serif-kr 의 self-host `@font-face` woff2 서브셋 자산 번들링.\n * 지금은 폰트 **패밀리명만** 스택 앞에 얹어, 사용자 환경에 설치돼 있으면 쓰고 없으면 폴백한다\n * (외부 CDN 0 유지). 자산이 동봉되면 여기 매핑은 그대로 두고 globals.css 에 @font-face 만 추가하면 된다.\n */\nconst FONTS: Readonly<Record<string, string>> = {\n system: SYSTEM_STACK,\n pretendard: `\"Pretendard\", ${SYSTEM_STACK}`,\n \"noto-serif-kr\": '\"Noto Serif KR\", ui-serif, Georgia, \"Nanum Myeongjo\", serif',\n};\n\n/** radius: --radius-knob 무단위 배수(globals.css radius 스케일이 곱한다). soft=1 이 Tailwind 기본과 일치. */\nconst RADII: Readonly<Record<string, string>> = {sharp: \"0\", soft: \"1\", round: \"2\"};\n\n/** density: --spacing 베이스(Tailwind v4 기본 0.25rem). 전 spacing 유틸리티가 일괄 스케일. */\nconst DENSITIES: Readonly<Record<string, string>> = {compact: \"0.22rem\", cozy: \"0.25rem\"};\n\n/** knob 키 → CSS 변수 → enum 매핑 테이블. */\nconst KNOB_KEYS: ReadonlyArray<readonly [string, string, Readonly<Record<string, string>>]> = [\n [\"font\", \"--font-sans\", FONTS],\n [\"radius\", \"--radius-knob\", RADII],\n [\"density\", \"--spacing\", DENSITIES],\n];\n\nexport interface ParsedTheme {\n /** `<html style={...}>` 에 그대로 얹을 CSS 변수 맵. 오버라이드가 없으면 빈 객체(기본 테마 유지). */\n cssVars: Record<string, string>;\n}\n\nexport function parseThemeColors(raw: string | null | undefined): ParsedTheme {\n const cssVars: Record<string, string> = {};\n if (!raw) return {cssVars};\n\n let obj: unknown;\n try {\n obj = JSON.parse(raw);\n } catch {\n return {cssVars};\n }\n if (obj == null || typeof obj !== \"object\" || Array.isArray(obj)) return {cssVars};\n\n const src = obj as Record<string, unknown>;\n let primaryHex: string | undefined;\n for (const [key, cssVar] of COLOR_KEYS) {\n const value = src[key];\n if (typeof value === \"string\" && HEX.test(value)) {\n cssVars[cssVar] = value;\n if (key === \"primary\") primaryHex = value;\n }\n }\n\n // 콘솔 스키마에 primary-foreground 키가 없다 — primary 가 오버라이드되면 대비가 큰 쪽을\n // 서버가 산출해 함께 주입한다(테넌트가 밝은 액센트를 골라도 CTA 글자가 안 죽는다).\n if (primaryHex) {\n cssVars[\"--color-primary-foreground\"] = onColor(primaryHex);\n }\n\n // 전역 토큰 knob(font·radius·density) — 매핑 테이블 lookup 만. hasOwnProperty 로 own key 만 채택해\n // 프로토타입 오염(constructor 등)·오염 문자열을 차단한다(사용자 문자열이 style 에 실리지 않는다).\n for (const [key, cssVar, table] of KNOB_KEYS) {\n const value = src[key];\n if (typeof value !== \"string\") continue;\n // hasOwnProperty 로 own key 만 채택한다 — 프로토타입 오염(constructor 등)을 차단하는 그 검사다.\n // 다만 그것으로 타입이 좁혀지지는 않으므로(strict), 조회 결과를 직접 확인해 싣는다.\n if (!Object.prototype.hasOwnProperty.call(table, value)) continue;\n const mapped = table[value];\n if (mapped !== undefined) cssVars[cssVar] = mapped;\n }\n\n return {cssVars};\n}\n\n/** primary 위에 얹을 글자색 — WCAG 상대 휘도로 흰색/slate-950 중 대비 큰 쪽. */\nfunction onColor(hex: string): string {\n return relativeLuminance(hex) > 0.4 ? \"#020617\" /* slate-950 */ : \"#ffffff\";\n}\n\n/** WCAG 상대 휘도(0~1). 검증된 hex 만 들어온다(HEX 통과분). */\nfunction relativeLuminance(hex: string): number {\n // `.map` 은 튜플성을 잃어 number[] 가 되고, 그러면 구조분해가 undefined 를 낳는다(strict).\n // 튜플을 유지한 채 채널별로 편다.\n const [r0, g0, b0] = toRgb(hex);\n const lin = (c: number) => {\n const s = c / 255;\n return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;\n };\n return 0.2126 * lin(r0) + 0.7152 * lin(g0) + 0.0722 * lin(b0);\n}\n\nfunction toRgb(hex: string): [number, number, number] {\n // `#rgb` 축약을 `#rrggbb` 로 편다. slice 로 자르는 것은 인덱스 접근과 달리 undefined 가 안 나온다\n // (이 패키지는 noUncheckedIndexedAccess 라 `h[0]` 은 string|undefined 다 — 검증된 hex 만 들어온다는\n // 사실을 타입으로 증명할 수 없으므로, 증명이 필요 없는 방식으로 쓴다).\n let h = hex.slice(1);\n if (h.length === 3) {\n h = h\n .split(\"\")\n .map((c) => c + c)\n .join(\"\");\n }\n return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];\n}\n"]}
package/dist/index.d.cts CHANGED
@@ -552,7 +552,7 @@ interface OrderSummary {
552
552
  * `PaymentSessionResponse` — 결제 시작 결과.
553
553
  *
554
554
  * **벤더가 둘 중 하나다**(테넌트가 자기 PG 를 고른다 — 기본 TOSS):
555
- * - **리다이렉트형**(PayOneQ 등): [widget] 이 없다 → [paymentUrl] 로 고객을 보내면 끝.
555
+ * - **리다이렉트형**: [widget] 이 없다 → [paymentUrl] 로 고객을 보내면 끝.
556
556
  * - **위젯형**(토스): [widget] 값으로 **내 사이트에서** 결제창을 띄우고, 성공 콜백의 파라미터를
557
557
  * [ZalkeraClient.confirmPayment] 로 넘겨 승인을 확정한다.
558
558
  *
@@ -620,7 +620,7 @@ interface ZalkeraClientOptions {
620
620
  * 스토어프론트 서버 시크릿 키(선택·`oqsk_…`). 주면 모든 요청에 `X-Storefront-Key` 헤더로 실려,
621
621
  * 백엔드가 이 키로 테넌트 신원을 증명한다(memo78 — [tenant] 무인증 신뢰의 보안 승격).
622
622
  *
623
- * ⚠️ **진짜 비밀이다.** 오직 서버 `.env`(예: `ONEQUE_STOREFRONT_KEY`)에만 두고, 브라우저 번들에
623
+ * ⚠️ **진짜 비밀이다.** 오직 서버 `.env`(예: `ZALKERA_STOREFRONT_KEY`)에만 두고, 브라우저 번들에
624
624
  * 절대 넣지 마라 — `NEXT_PUBLIC_*` 접두사·클라이언트 컴포넌트 import 금지. 이 클라이언트가 서버
625
625
  * 전용인 이유가 그것이다. 유출 시 콘솔에서 revoke·재발급.
626
626
  *
package/dist/index.d.ts CHANGED
@@ -552,7 +552,7 @@ interface OrderSummary {
552
552
  * `PaymentSessionResponse` — 결제 시작 결과.
553
553
  *
554
554
  * **벤더가 둘 중 하나다**(테넌트가 자기 PG 를 고른다 — 기본 TOSS):
555
- * - **리다이렉트형**(PayOneQ 등): [widget] 이 없다 → [paymentUrl] 로 고객을 보내면 끝.
555
+ * - **리다이렉트형**: [widget] 이 없다 → [paymentUrl] 로 고객을 보내면 끝.
556
556
  * - **위젯형**(토스): [widget] 값으로 **내 사이트에서** 결제창을 띄우고, 성공 콜백의 파라미터를
557
557
  * [ZalkeraClient.confirmPayment] 로 넘겨 승인을 확정한다.
558
558
  *
@@ -620,7 +620,7 @@ interface ZalkeraClientOptions {
620
620
  * 스토어프론트 서버 시크릿 키(선택·`oqsk_…`). 주면 모든 요청에 `X-Storefront-Key` 헤더로 실려,
621
621
  * 백엔드가 이 키로 테넌트 신원을 증명한다(memo78 — [tenant] 무인증 신뢰의 보안 승격).
622
622
  *
623
- * ⚠️ **진짜 비밀이다.** 오직 서버 `.env`(예: `ONEQUE_STOREFRONT_KEY`)에만 두고, 브라우저 번들에
623
+ * ⚠️ **진짜 비밀이다.** 오직 서버 `.env`(예: `ZALKERA_STOREFRONT_KEY`)에만 두고, 브라우저 번들에
624
624
  * 절대 넣지 마라 — `NEXT_PUBLIC_*` 접두사·클라이언트 컴포넌트 import 금지. 이 클라이언트가 서버
625
625
  * 전용인 이유가 그것이다. 유출 시 콘솔에서 revoke·재발급.
626
626
  *
package/dist/index.js CHANGED
@@ -60,7 +60,7 @@ var ZalkeraError = class _ZalkeraError extends Error {
60
60
  }
61
61
  };
62
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.",
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 ZALKERA_STOREFRONT_KEY). \uBE0C\uB77C\uC6B0\uC800 \uBC88\uB4E4\uC5D0 \uB123\uC9C0 \uB9C8\uC138\uC694.",
64
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
65
  };
66
66
  function isApiErrorBody(value) {
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/errors.ts","../src/client.ts","../src/sections.ts","../src/safeUrl.ts","../src/sectionConfig.ts","../src/theme.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 ListPagesParams,\n ListPostsParams,\n ListReviewsParams,\n MediaUrl,\n Menu,\n OrderDetail,\n OrderSummary,\n PageContent,\n PageSummary,\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 /**\n * 발행된 고정 페이지 **목록**(열거 전용·본문 없음).\n *\n * sitemap 을 위한 API 다 — 이게 없으면 콘솔·AI 로 만든 페이지가 검색엔진에 열거되지 않는다.\n * 목록에 실린 slug 는 [getPage] 가 반드시 200 을 준다(가시성 술어가 서버에서 하나로 공유된다).\n */\n listPages(params?: ListPagesParams, options?: ReadOptions): Promise<Paginated<PageSummary>>;\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 listPages: (params, options) =>\n request<Paginated<PageSummary>>(\"/public/pages\", {\n query: {page: params?.page, size: params?.size},\n ...nextInit(options),\n }),\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","/**\n * 섹션 어휘 계약 — **정본의 코드 표현**(memo102 §6).\n *\n * 정본은 백엔드 레포의 `doc/contracts/section-vocabulary.json` 이고, 이 파일은 그것을 npm 으로\n * 실어 나르는 **운반체**다. 템플릿 렌더러와 콘솔 zod 스키마가 이 상수를 읽어 자기 커버리지를\n * 기계로 검사한다 — 사본이 갈라진 채 조용히 굳는 것을 막는 게 목적이지, 실시간 동일성이 목적은\n * 아니다(계약이 원래 스큐 내성으로 설계돼 있다: 미지 타입은 스킵).\n *\n * 두 레포가 갈라져 있어 상호 CI 강제가 불가능하므로 **사람 이음새가 정확히 한 곳** 남는다 —\n * 백엔드 JSON ↔ 이 파일. [SECTION_CONTRACT_REV] 를 백엔드 스펙의 `contractRev` 와 맞춰 두고,\n * client 발행 전 `scripts/sync-section-contract.mjs` 로 대조한다.\n */\n\n/** 백엔드 스펙 `contractRev` 와 같아야 한다. 어긋나면 동기 스크립트가 잡는다. */\nexport const SECTION_CONTRACT_REV = 1;\n\n/** 업종 분류 — 콘솔 섹션 픽커의 그룹핑에 쓴다(테넌트 업종 필드는 아직 없다·memo102 §5). */\nexport type SectionVertical = \"BEAUTY\" | \"GENERAL\";\n\nexport interface SectionSpec {\n readonly type: string;\n readonly vertical: SectionVertical;\n /** 이 섹션이 산출하는 schema.org 타입. null 이면 구조화 데이터 없음. */\n readonly jsonLd: string | null;\n}\n\n/**\n * 아는 섹션 전량. **순서는 콘솔 픽커의 노출 순서**(관행 아크: 주목→가치→신뢰→행동).\n * 값 추가는 백엔드 스펙을 먼저 고친 뒤 여기로 옮긴다.\n */\nexport const SECTION_CONTRACT = [\n // 뷰티(memo47) — append-only 계약상 불변\n { type: \"SERVICE_MENU\", vertical: \"BEAUTY\", jsonLd: null },\n { type: \"BEFORE_AFTER_GALLERY\", vertical: \"BEAUTY\", jsonLd: null },\n { type: \"BOOKING_CTA\", vertical: \"BEAUTY\", jsonLd: null },\n { type: \"DOCTOR_INTRO\", vertical: \"BEAUTY\", jsonLd: null },\n // 기업 마케팅(memo102 §2)\n { type: \"HERO\", vertical: \"GENERAL\", jsonLd: null },\n { type: \"FEATURE_GRID\", vertical: \"GENERAL\", jsonLd: null },\n { type: \"TEXT_MEDIA\", vertical: \"GENERAL\", jsonLd: null },\n { type: \"LOGO_WALL\", vertical: \"GENERAL\", jsonLd: null },\n { type: \"STATS_BAND\", vertical: \"GENERAL\", jsonLd: null },\n { type: \"TESTIMONIALS\", vertical: \"GENERAL\", jsonLd: null },\n { type: \"FAQ_LIST\", vertical: \"GENERAL\", jsonLd: \"FAQPage\" },\n { type: \"LEAD_CTA\", vertical: \"GENERAL\", jsonLd: null },\n] as const satisfies readonly SectionSpec[];\n\n/** 지금 렌더러가 아는 섹션 타입. 이 밖의 값이 와도 정상이다(스킵). */\nexport type KnownSectionType = (typeof SECTION_CONTRACT)[number][\"type\"];\n\n/** 업종별 필터 — 콘솔 픽커 그룹핑용. */\nexport function sectionsOfVertical(vertical: SectionVertical): readonly SectionSpec[] {\n return SECTION_CONTRACT.filter((s) => s.vertical === vertical);\n}\n","/**\n * 사용자·콘솔 입력 URL 을 링크로 쓰기 전 소독한다 — **저장형 XSS 방어**.\n *\n * 콘솔 메뉴 url·섹션 config 의 href 는 사람이 입력하는 값이라 `javascript:alert(document.cookie)` 가\n * 들어올 수 있고, 백엔드는 스킴을 검증하지 않는다(길이·trim 뿐). react-dom 이 prod 번들에서\n * `javascript:` href 를 막긴 하지만 **dev 에서는 raw 로 실리고**, 프레임워크 내부 동작에 보안을\n * 의존하는 것 자체가 취약하다. 그래서 이건 **심층 방어**다 — 렌더 소스에서 스킴을 거른다.\n *\n * 허용: 내부 절대경로(`/about`)·http(s)·mailto·tel. 그 외(javascript:·data:·vbscript: 등)는 `#` 으로\n * 무력화한다(링크는 남되 스크립트는 안 돈다). 메뉴는 외부 링크가 정당하므로 내부로만 좁히지 않고\n * 스킴 허용목록을 쓴다.\n */\nconst ALLOWED_SCHEMES = new Set([\"http:\", \"https:\", \"mailto:\", \"tel:\"]);\n\nexport function safeLinkUrl(raw: string | null | undefined): string {\n if (!raw) return \"#\";\n const url = raw.trim();\n // 내부 절대경로 — 스킴이 없다. `//host`(스킴 상대)는 외부라 파서로 넘겨 검증한다.\n if (url.startsWith(\"/\") && !url.startsWith(\"//\")) return url;\n try {\n const parsed = new URL(url); // 절대 URL 만 파싱된다(상대면 throw)\n return ALLOWED_SCHEMES.has(parsed.protocol) ? url : \"#\";\n } catch {\n // 파싱 실패 = 스킴 없는 상대경로(anchor#·?query 등) — 내부로 간주해 통과.\n return url.startsWith(\"//\") ? \"#\" : url;\n }\n}\n","/**\n * 섹션 config 파싱 — **절대 throw 하지 않는다.**\n *\n * config 는 백엔드가 검증하지 않는 raw JSON 이다(의도된 설계 — 사이트마다 백엔드를 배포하지\n * 않으려고). 게다가 콘솔의 raw 편집기는 **JSON 문법만** 보고 저장을 허용한다 — 즉 문법은\n * 맞는데 형상이 다른 config 가 **정상 경로로** 들어온다. `{\"productIds\": 5}` 같은 것.\n *\n * 그래서 파싱이 성공했다고 필드를 믿으면 안 된다. `.map`·`.trim` 을 그냥 부르면 그 순간\n * **페이지 전체가 500** 이 난다 — 계약은 \"그 섹션만 사라진다\" 였다. 아래 가드들이 그 계약을\n * 지키는 장치다. [parseThemeColors] 와 같은 사상.\n *\n * **이 패키지에 사는 이유**(memo108 §1): 이건 표현이 아니라 **계약을 안전하게 읽는 법**이다. 마크업은\n * 만드는 쪽 자유지만, '섹션 하나가 사이트를 죽이지 않는다'는 계약은 한 벌이어야 한다 — 사본이 갈라지면\n * 그 계약이 조용히 깨진다(471946c 교훈).\n */\nexport function parseConfig<T>(config: string | null): T | null {\n if (!config) return null;\n try {\n const parsed: unknown = JSON.parse(config);\n return parsed && typeof parsed === \"object\" && !Array.isArray(parsed) ? (parsed as T) : null;\n } catch {\n return null;\n }\n}\n\n/** 양의 정수 id 만 남긴다 — 배열이 아니면 빈 배열. */\nexport function asIdArray(value: unknown): number[] {\n if (!Array.isArray(value)) return [];\n return value.filter((v): v is number => typeof v === \"number\" && Number.isInteger(v) && v > 0);\n}\n\n/** 객체 배열만 — 아니면 빈 배열. */\nexport function asObjectArray(value: unknown): Record<string, unknown>[] {\n if (!Array.isArray(value)) return [];\n return value.filter(\n (v): v is Record<string, unknown> => v != null && typeof v === \"object\" && !Array.isArray(v),\n );\n}\n\n/** 문자열이 아니면 undefined — 숫자에 `.trim()` 을 부르는 사고를 막는다. */\nexport function asString(value: unknown): string | undefined {\n return typeof value === \"string\" ? value : undefined;\n}\n\n/** 양의 정수가 아니면 undefined. */\nexport function asId(value: unknown): number | undefined {\n return typeof value === \"number\" && Number.isInteger(value) && value > 0 ? value : undefined;\n}\n\n/** 미디어는 프록시로만 — presigned URL 을 마크업에 넣지 않는다(만료되면 깨진 이미지가 박제된다). */\nexport function mediaSrc(assetId: number): string {\n return `/media/${assetId}`;\n}\n","/**\n * 테넌트 테마 색 파서 (서버 전용).\n *\n * `SiteConfig.themeColors` 는 파싱하지 않은 raw JSON 문자열이다(백엔드 콘솔 `site.theme.update` 가\n * 넣는다). 같은 JSON 에 `layout` 등 이종 키가 병합돼 오므로 **색 키만 화이트리스트로** 읽는다.\n * 어떤 쓰레기가 와도 죽지 않는다(섹션 parse.ts 와 같은 사상) — 실패하면 빈 오버라이드(기본 테마).\n *\n * 읽어들인 색은 globals.css 의 `@theme` 변수를 `<html>` inline style 로 덮어, `bg-primary` 등\n * 전 유틸리티가 테넌트 색으로 바뀐다(layout.tsx). CSS 주입 방어를 위해 값은 hex 형식 검증 후에만 싣는다.\n *\n * **이 패키지에 사는 이유**(memo108 §1): L1(말로 색 바꾸기)의 기계는 표현이 아니라 계약이다 — 어떤 토큰을\n * 쓸지는 코드가 정하고 값은 config 가 정한다는 규약(memo65 §3)의 집행부이고, 순수 함수라 React 에 안 매인다.\n * 이 함수의 반환을 `<html style={...}>` 에 싣는 **배선은 각 사이트의 몫**이다(validator S8 이 그걸 센다).\n */\n\n/** `#rgb`/`#rrggbb` 만 통과. 임의 문자열을 style 에 싣지 않는다(CSS 주입 방어). */\nconst HEX = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;\n\n/** 콘솔 스키마의 색 키 → globals.css `@theme` 변수명 매핑. 이 키만 읽는다. */\nconst COLOR_KEYS: ReadonlyArray<readonly [string, string]> = [\n [\"primary\", \"--color-primary\"],\n [\"secondary\", \"--color-secondary\"],\n [\"background\", \"--color-background\"],\n [\"text\", \"--color-foreground\"],\n];\n\n/*\n * ── 전역 토큰 knob (memo69 §2.1 · 계약버전 2) ──────────────────────────────\n * 색과 달리 knob 은 **enum**이다. 백엔드 `site.theme.update` 화이트리스트가 허용값만 저장하지만,\n * 여기서도 매핑 테이블 lookup 으로 한 번 더 봉한다: **사용자 문자열은 어떤 경로로도 style 에 실리지\n * 않는다** — enum 은 테이블 key 조회뿐이고, 없는 값(오염·구버전)은 조용히 버려 @theme 기본값을 유지한다.\n * 이 값들은 백엔드 enum(FONTS/RADII/DENSITIES)과 반드시 일치해야 한다(계약).\n */\n\n/** 시스템 폰트 스택(globals.css --font-sans 와 동일 — system knob 의 기본값). */\nconst SYSTEM_STACK =\n 'ui-sans-serif, system-ui, -apple-system, \"Apple SD Gothic Neo\", \"Malgun Gothic\", \"Noto Sans KR\", sans-serif';\n\n/*\n * font: --font-sans 스택 교체.\n * TODO(memo69 §2.1 후속): pretendard·noto-serif-kr 의 self-host `@font-face` woff2 서브셋 자산 번들링.\n * 지금은 폰트 **패밀리명만** 스택 앞에 얹어, 사용자 환경에 설치돼 있으면 쓰고 없으면 폴백한다\n * (외부 CDN 0 유지). 자산이 동봉되면 여기 매핑은 그대로 두고 globals.css 에 @font-face 만 추가하면 된다.\n */\nconst FONTS: Readonly<Record<string, string>> = {\n system: SYSTEM_STACK,\n pretendard: `\"Pretendard\", ${SYSTEM_STACK}`,\n \"noto-serif-kr\": '\"Noto Serif KR\", ui-serif, Georgia, \"Nanum Myeongjo\", serif',\n};\n\n/** radius: --radius-knob 무단위 배수(globals.css radius 스케일이 곱한다). soft=1 이 Tailwind 기본과 일치. */\nconst RADII: Readonly<Record<string, string>> = {sharp: \"0\", soft: \"1\", round: \"2\"};\n\n/** density: --spacing 베이스(Tailwind v4 기본 0.25rem). 전 spacing 유틸리티가 일괄 스케일. */\nconst DENSITIES: Readonly<Record<string, string>> = {compact: \"0.22rem\", cozy: \"0.25rem\"};\n\n/** knob 키 → CSS 변수 → enum 매핑 테이블. */\nconst KNOB_KEYS: ReadonlyArray<readonly [string, string, Readonly<Record<string, string>>]> = [\n [\"font\", \"--font-sans\", FONTS],\n [\"radius\", \"--radius-knob\", RADII],\n [\"density\", \"--spacing\", DENSITIES],\n];\n\nexport interface ParsedTheme {\n /** `<html style={...}>` 에 그대로 얹을 CSS 변수 맵. 오버라이드가 없으면 빈 객체(기본 테마 유지). */\n cssVars: Record<string, string>;\n}\n\nexport function parseThemeColors(raw: string | null | undefined): ParsedTheme {\n const cssVars: Record<string, string> = {};\n if (!raw) return {cssVars};\n\n let obj: unknown;\n try {\n obj = JSON.parse(raw);\n } catch {\n return {cssVars};\n }\n if (obj == null || typeof obj !== \"object\" || Array.isArray(obj)) return {cssVars};\n\n const src = obj as Record<string, unknown>;\n let primaryHex: string | undefined;\n for (const [key, cssVar] of COLOR_KEYS) {\n const value = src[key];\n if (typeof value === \"string\" && HEX.test(value)) {\n cssVars[cssVar] = value;\n if (key === \"primary\") primaryHex = value;\n }\n }\n\n // 콘솔 스키마에 primary-foreground 키가 없다 — primary 가 오버라이드되면 대비가 큰 쪽을\n // 서버가 산출해 함께 주입한다(테넌트가 밝은 액센트를 골라도 CTA 글자가 안 죽는다).\n if (primaryHex) {\n cssVars[\"--color-primary-foreground\"] = onColor(primaryHex);\n }\n\n // 전역 토큰 knob(font·radius·density) — 매핑 테이블 lookup 만. hasOwnProperty 로 own key 만 채택해\n // 프로토타입 오염(constructor 등)·오염 문자열을 차단한다(사용자 문자열이 style 에 실리지 않는다).\n for (const [key, cssVar, table] of KNOB_KEYS) {\n const value = src[key];\n if (typeof value !== \"string\") continue;\n // hasOwnProperty 로 own key 만 채택한다 — 프로토타입 오염(constructor 등)을 차단하는 그 검사다.\n // 다만 그것으로 타입이 좁혀지지는 않으므로(strict), 조회 결과를 직접 확인해 싣는다.\n if (!Object.prototype.hasOwnProperty.call(table, value)) continue;\n const mapped = table[value];\n if (mapped !== undefined) cssVars[cssVar] = mapped;\n }\n\n return {cssVars};\n}\n\n/** primary 위에 얹을 글자색 — WCAG 상대 휘도로 흰색/slate-950 중 대비 큰 쪽. */\nfunction onColor(hex: string): string {\n return relativeLuminance(hex) > 0.4 ? \"#020617\" /* slate-950 */ : \"#ffffff\";\n}\n\n/** WCAG 상대 휘도(0~1). 검증된 hex 만 들어온다(HEX 통과분). */\nfunction relativeLuminance(hex: string): number {\n // `.map` 은 튜플성을 잃어 number[] 가 되고, 그러면 구조분해가 undefined 를 낳는다(strict).\n // 튜플을 유지한 채 채널별로 편다.\n const [r0, g0, b0] = toRgb(hex);\n const lin = (c: number) => {\n const s = c / 255;\n return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;\n };\n return 0.2126 * lin(r0) + 0.7152 * lin(g0) + 0.0722 * lin(b0);\n}\n\nfunction toRgb(hex: string): [number, number, number] {\n // `#rgb` 축약을 `#rrggbb` 로 편다. slice 로 자르는 것은 인덱스 접근과 달리 undefined 가 안 나온다\n // (이 패키지는 noUncheckedIndexedAccess 라 `h[0]` 은 string|undefined 다 — 검증된 hex 만 들어온다는\n // 사실을 타입으로 증명할 수 없으므로, 증명이 필요 없는 방식으로 쓴다).\n let h = hex.slice(1);\n if (h.length === 3) {\n h = h\n .split(\"\")\n .map((c) => c + c)\n .join(\"\");\n }\n return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];\n}\n"]}
1
+ {"version":3,"sources":["../src/errors.ts","../src/client.ts","../src/sections.ts","../src/safeUrl.ts","../src/sectionConfig.ts","../src/theme.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 의 ZALKERA_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 ListPagesParams,\n ListPostsParams,\n ListReviewsParams,\n MediaUrl,\n Menu,\n OrderDetail,\n OrderSummary,\n PageContent,\n PageSummary,\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`(예: `ZALKERA_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 /**\n * 발행된 고정 페이지 **목록**(열거 전용·본문 없음).\n *\n * sitemap 을 위한 API 다 — 이게 없으면 콘솔·AI 로 만든 페이지가 검색엔진에 열거되지 않는다.\n * 목록에 실린 slug 는 [getPage] 가 반드시 200 을 준다(가시성 술어가 서버에서 하나로 공유된다).\n */\n listPages(params?: ListPagesParams, options?: ReadOptions): Promise<Paginated<PageSummary>>;\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 listPages: (params, options) =>\n request<Paginated<PageSummary>>(\"/public/pages\", {\n query: {page: params?.page, size: params?.size},\n ...nextInit(options),\n }),\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","/**\n * 섹션 어휘 계약 — **정본의 코드 표현**(memo102 §6).\n *\n * 정본은 백엔드 레포의 `doc/contracts/section-vocabulary.json` 이고, 이 파일은 그것을 npm 으로\n * 실어 나르는 **운반체**다. 템플릿 렌더러와 콘솔 zod 스키마가 이 상수를 읽어 자기 커버리지를\n * 기계로 검사한다 — 사본이 갈라진 채 조용히 굳는 것을 막는 게 목적이지, 실시간 동일성이 목적은\n * 아니다(계약이 원래 스큐 내성으로 설계돼 있다: 미지 타입은 스킵).\n *\n * 두 레포가 갈라져 있어 상호 CI 강제가 불가능하므로 **사람 이음새가 정확히 한 곳** 남는다 —\n * 백엔드 JSON ↔ 이 파일. [SECTION_CONTRACT_REV] 를 백엔드 스펙의 `contractRev` 와 맞춰 두고,\n * client 발행 전 `scripts/sync-section-contract.mjs` 로 대조한다.\n */\n\n/** 백엔드 스펙 `contractRev` 와 같아야 한다. 어긋나면 동기 스크립트가 잡는다. */\nexport const SECTION_CONTRACT_REV = 1;\n\n/** 업종 분류 — 콘솔 섹션 픽커의 그룹핑에 쓴다(테넌트 업종 필드는 아직 없다·memo102 §5). */\nexport type SectionVertical = \"BEAUTY\" | \"GENERAL\";\n\nexport interface SectionSpec {\n readonly type: string;\n readonly vertical: SectionVertical;\n /** 이 섹션이 산출하는 schema.org 타입. null 이면 구조화 데이터 없음. */\n readonly jsonLd: string | null;\n}\n\n/**\n * 아는 섹션 전량. **순서는 콘솔 픽커의 노출 순서**(관행 아크: 주목→가치→신뢰→행동).\n * 값 추가는 백엔드 스펙을 먼저 고친 뒤 여기로 옮긴다.\n */\nexport const SECTION_CONTRACT = [\n // 뷰티(memo47) — append-only 계약상 불변\n { type: \"SERVICE_MENU\", vertical: \"BEAUTY\", jsonLd: null },\n { type: \"BEFORE_AFTER_GALLERY\", vertical: \"BEAUTY\", jsonLd: null },\n { type: \"BOOKING_CTA\", vertical: \"BEAUTY\", jsonLd: null },\n { type: \"DOCTOR_INTRO\", vertical: \"BEAUTY\", jsonLd: null },\n // 기업 마케팅(memo102 §2)\n { type: \"HERO\", vertical: \"GENERAL\", jsonLd: null },\n { type: \"FEATURE_GRID\", vertical: \"GENERAL\", jsonLd: null },\n { type: \"TEXT_MEDIA\", vertical: \"GENERAL\", jsonLd: null },\n { type: \"LOGO_WALL\", vertical: \"GENERAL\", jsonLd: null },\n { type: \"STATS_BAND\", vertical: \"GENERAL\", jsonLd: null },\n { type: \"TESTIMONIALS\", vertical: \"GENERAL\", jsonLd: null },\n { type: \"FAQ_LIST\", vertical: \"GENERAL\", jsonLd: \"FAQPage\" },\n { type: \"LEAD_CTA\", vertical: \"GENERAL\", jsonLd: null },\n] as const satisfies readonly SectionSpec[];\n\n/** 지금 렌더러가 아는 섹션 타입. 이 밖의 값이 와도 정상이다(스킵). */\nexport type KnownSectionType = (typeof SECTION_CONTRACT)[number][\"type\"];\n\n/** 업종별 필터 — 콘솔 픽커 그룹핑용. */\nexport function sectionsOfVertical(vertical: SectionVertical): readonly SectionSpec[] {\n return SECTION_CONTRACT.filter((s) => s.vertical === vertical);\n}\n","/**\n * 사용자·콘솔 입력 URL 을 링크로 쓰기 전 소독한다 — **저장형 XSS 방어**.\n *\n * 콘솔 메뉴 url·섹션 config 의 href 는 사람이 입력하는 값이라 `javascript:alert(document.cookie)` 가\n * 들어올 수 있고, 백엔드는 스킴을 검증하지 않는다(길이·trim 뿐). react-dom 이 prod 번들에서\n * `javascript:` href 를 막긴 하지만 **dev 에서는 raw 로 실리고**, 프레임워크 내부 동작에 보안을\n * 의존하는 것 자체가 취약하다. 그래서 이건 **심층 방어**다 — 렌더 소스에서 스킴을 거른다.\n *\n * 허용: 내부 절대경로(`/about`)·http(s)·mailto·tel. 그 외(javascript:·data:·vbscript: 등)는 `#` 으로\n * 무력화한다(링크는 남되 스크립트는 안 돈다). 메뉴는 외부 링크가 정당하므로 내부로만 좁히지 않고\n * 스킴 허용목록을 쓴다.\n */\nconst ALLOWED_SCHEMES = new Set([\"http:\", \"https:\", \"mailto:\", \"tel:\"]);\n\nexport function safeLinkUrl(raw: string | null | undefined): string {\n if (!raw) return \"#\";\n const url = raw.trim();\n // 내부 절대경로 — 스킴이 없다. `//host`(스킴 상대)는 외부라 파서로 넘겨 검증한다.\n if (url.startsWith(\"/\") && !url.startsWith(\"//\")) return url;\n try {\n const parsed = new URL(url); // 절대 URL 만 파싱된다(상대면 throw)\n return ALLOWED_SCHEMES.has(parsed.protocol) ? url : \"#\";\n } catch {\n // 파싱 실패 = 스킴 없는 상대경로(anchor#·?query 등) — 내부로 간주해 통과.\n return url.startsWith(\"//\") ? \"#\" : url;\n }\n}\n","/**\n * 섹션 config 파싱 — **절대 throw 하지 않는다.**\n *\n * config 는 백엔드가 검증하지 않는 raw JSON 이다(의도된 설계 — 사이트마다 백엔드를 배포하지\n * 않으려고). 게다가 콘솔의 raw 편집기는 **JSON 문법만** 보고 저장을 허용한다 — 즉 문법은\n * 맞는데 형상이 다른 config 가 **정상 경로로** 들어온다. `{\"productIds\": 5}` 같은 것.\n *\n * 그래서 파싱이 성공했다고 필드를 믿으면 안 된다. `.map`·`.trim` 을 그냥 부르면 그 순간\n * **페이지 전체가 500** 이 난다 — 계약은 \"그 섹션만 사라진다\" 였다. 아래 가드들이 그 계약을\n * 지키는 장치다. [parseThemeColors] 와 같은 사상.\n *\n * **이 패키지에 사는 이유**(memo108 §1): 이건 표현이 아니라 **계약을 안전하게 읽는 법**이다. 마크업은\n * 만드는 쪽 자유지만, '섹션 하나가 사이트를 죽이지 않는다'는 계약은 한 벌이어야 한다 — 사본이 갈라지면\n * 그 계약이 조용히 깨진다(471946c 교훈).\n */\nexport function parseConfig<T>(config: string | null): T | null {\n if (!config) return null;\n try {\n const parsed: unknown = JSON.parse(config);\n return parsed && typeof parsed === \"object\" && !Array.isArray(parsed) ? (parsed as T) : null;\n } catch {\n return null;\n }\n}\n\n/** 양의 정수 id 만 남긴다 — 배열이 아니면 빈 배열. */\nexport function asIdArray(value: unknown): number[] {\n if (!Array.isArray(value)) return [];\n return value.filter((v): v is number => typeof v === \"number\" && Number.isInteger(v) && v > 0);\n}\n\n/** 객체 배열만 — 아니면 빈 배열. */\nexport function asObjectArray(value: unknown): Record<string, unknown>[] {\n if (!Array.isArray(value)) return [];\n return value.filter(\n (v): v is Record<string, unknown> => v != null && typeof v === \"object\" && !Array.isArray(v),\n );\n}\n\n/** 문자열이 아니면 undefined — 숫자에 `.trim()` 을 부르는 사고를 막는다. */\nexport function asString(value: unknown): string | undefined {\n return typeof value === \"string\" ? value : undefined;\n}\n\n/** 양의 정수가 아니면 undefined. */\nexport function asId(value: unknown): number | undefined {\n return typeof value === \"number\" && Number.isInteger(value) && value > 0 ? value : undefined;\n}\n\n/** 미디어는 프록시로만 — presigned URL 을 마크업에 넣지 않는다(만료되면 깨진 이미지가 박제된다). */\nexport function mediaSrc(assetId: number): string {\n return `/media/${assetId}`;\n}\n","/**\n * 테넌트 테마 색 파서 (서버 전용).\n *\n * `SiteConfig.themeColors` 는 파싱하지 않은 raw JSON 문자열이다(백엔드 콘솔 `site.theme.update` 가\n * 넣는다). 같은 JSON 에 `layout` 등 이종 키가 병합돼 오므로 **색 키만 화이트리스트로** 읽는다.\n * 어떤 쓰레기가 와도 죽지 않는다(섹션 parse.ts 와 같은 사상) — 실패하면 빈 오버라이드(기본 테마).\n *\n * 읽어들인 색은 globals.css 의 `@theme` 변수를 `<html>` inline style 로 덮어, `bg-primary` 등\n * 전 유틸리티가 테넌트 색으로 바뀐다(layout.tsx). CSS 주입 방어를 위해 값은 hex 형식 검증 후에만 싣는다.\n *\n * **이 패키지에 사는 이유**(memo108 §1): L1(말로 색 바꾸기)의 기계는 표현이 아니라 계약이다 — 어떤 토큰을\n * 쓸지는 코드가 정하고 값은 config 가 정한다는 규약(memo65 §3)의 집행부이고, 순수 함수라 React 에 안 매인다.\n * 이 함수의 반환을 `<html style={...}>` 에 싣는 **배선은 각 사이트의 몫**이다(validator S8 이 그걸 센다).\n */\n\n/** `#rgb`/`#rrggbb` 만 통과. 임의 문자열을 style 에 싣지 않는다(CSS 주입 방어). */\nconst HEX = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;\n\n/** 콘솔 스키마의 색 키 → globals.css `@theme` 변수명 매핑. 이 키만 읽는다. */\nconst COLOR_KEYS: ReadonlyArray<readonly [string, string]> = [\n [\"primary\", \"--color-primary\"],\n [\"secondary\", \"--color-secondary\"],\n [\"background\", \"--color-background\"],\n [\"text\", \"--color-foreground\"],\n];\n\n/*\n * ── 전역 토큰 knob (memo69 §2.1 · 계약버전 2) ──────────────────────────────\n * 색과 달리 knob 은 **enum**이다. 백엔드 `site.theme.update` 화이트리스트가 허용값만 저장하지만,\n * 여기서도 매핑 테이블 lookup 으로 한 번 더 봉한다: **사용자 문자열은 어떤 경로로도 style 에 실리지\n * 않는다** — enum 은 테이블 key 조회뿐이고, 없는 값(오염·구버전)은 조용히 버려 @theme 기본값을 유지한다.\n * 이 값들은 백엔드 enum(FONTS/RADII/DENSITIES)과 반드시 일치해야 한다(계약).\n */\n\n/** 시스템 폰트 스택(globals.css --font-sans 와 동일 — system knob 의 기본값). */\nconst SYSTEM_STACK =\n 'ui-sans-serif, system-ui, -apple-system, \"Apple SD Gothic Neo\", \"Malgun Gothic\", \"Noto Sans KR\", sans-serif';\n\n/*\n * font: --font-sans 스택 교체.\n * TODO(memo69 §2.1 후속): pretendard·noto-serif-kr 의 self-host `@font-face` woff2 서브셋 자산 번들링.\n * 지금은 폰트 **패밀리명만** 스택 앞에 얹어, 사용자 환경에 설치돼 있으면 쓰고 없으면 폴백한다\n * (외부 CDN 0 유지). 자산이 동봉되면 여기 매핑은 그대로 두고 globals.css 에 @font-face 만 추가하면 된다.\n */\nconst FONTS: Readonly<Record<string, string>> = {\n system: SYSTEM_STACK,\n pretendard: `\"Pretendard\", ${SYSTEM_STACK}`,\n \"noto-serif-kr\": '\"Noto Serif KR\", ui-serif, Georgia, \"Nanum Myeongjo\", serif',\n};\n\n/** radius: --radius-knob 무단위 배수(globals.css radius 스케일이 곱한다). soft=1 이 Tailwind 기본과 일치. */\nconst RADII: Readonly<Record<string, string>> = {sharp: \"0\", soft: \"1\", round: \"2\"};\n\n/** density: --spacing 베이스(Tailwind v4 기본 0.25rem). 전 spacing 유틸리티가 일괄 스케일. */\nconst DENSITIES: Readonly<Record<string, string>> = {compact: \"0.22rem\", cozy: \"0.25rem\"};\n\n/** knob 키 → CSS 변수 → enum 매핑 테이블. */\nconst KNOB_KEYS: ReadonlyArray<readonly [string, string, Readonly<Record<string, string>>]> = [\n [\"font\", \"--font-sans\", FONTS],\n [\"radius\", \"--radius-knob\", RADII],\n [\"density\", \"--spacing\", DENSITIES],\n];\n\nexport interface ParsedTheme {\n /** `<html style={...}>` 에 그대로 얹을 CSS 변수 맵. 오버라이드가 없으면 빈 객체(기본 테마 유지). */\n cssVars: Record<string, string>;\n}\n\nexport function parseThemeColors(raw: string | null | undefined): ParsedTheme {\n const cssVars: Record<string, string> = {};\n if (!raw) return {cssVars};\n\n let obj: unknown;\n try {\n obj = JSON.parse(raw);\n } catch {\n return {cssVars};\n }\n if (obj == null || typeof obj !== \"object\" || Array.isArray(obj)) return {cssVars};\n\n const src = obj as Record<string, unknown>;\n let primaryHex: string | undefined;\n for (const [key, cssVar] of COLOR_KEYS) {\n const value = src[key];\n if (typeof value === \"string\" && HEX.test(value)) {\n cssVars[cssVar] = value;\n if (key === \"primary\") primaryHex = value;\n }\n }\n\n // 콘솔 스키마에 primary-foreground 키가 없다 — primary 가 오버라이드되면 대비가 큰 쪽을\n // 서버가 산출해 함께 주입한다(테넌트가 밝은 액센트를 골라도 CTA 글자가 안 죽는다).\n if (primaryHex) {\n cssVars[\"--color-primary-foreground\"] = onColor(primaryHex);\n }\n\n // 전역 토큰 knob(font·radius·density) — 매핑 테이블 lookup 만. hasOwnProperty 로 own key 만 채택해\n // 프로토타입 오염(constructor 등)·오염 문자열을 차단한다(사용자 문자열이 style 에 실리지 않는다).\n for (const [key, cssVar, table] of KNOB_KEYS) {\n const value = src[key];\n if (typeof value !== \"string\") continue;\n // hasOwnProperty 로 own key 만 채택한다 — 프로토타입 오염(constructor 등)을 차단하는 그 검사다.\n // 다만 그것으로 타입이 좁혀지지는 않으므로(strict), 조회 결과를 직접 확인해 싣는다.\n if (!Object.prototype.hasOwnProperty.call(table, value)) continue;\n const mapped = table[value];\n if (mapped !== undefined) cssVars[cssVar] = mapped;\n }\n\n return {cssVars};\n}\n\n/** primary 위에 얹을 글자색 — WCAG 상대 휘도로 흰색/slate-950 중 대비 큰 쪽. */\nfunction onColor(hex: string): string {\n return relativeLuminance(hex) > 0.4 ? \"#020617\" /* slate-950 */ : \"#ffffff\";\n}\n\n/** WCAG 상대 휘도(0~1). 검증된 hex 만 들어온다(HEX 통과분). */\nfunction relativeLuminance(hex: string): number {\n // `.map` 은 튜플성을 잃어 number[] 가 되고, 그러면 구조분해가 undefined 를 낳는다(strict).\n // 튜플을 유지한 채 채널별로 편다.\n const [r0, g0, b0] = toRgb(hex);\n const lin = (c: number) => {\n const s = c / 255;\n return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;\n };\n return 0.2126 * lin(r0) + 0.7152 * lin(g0) + 0.0722 * lin(b0);\n}\n\nfunction toRgb(hex: string): [number, number, number] {\n // `#rgb` 축약을 `#rrggbb` 로 편다. slice 로 자르는 것은 인덱스 접근과 달리 undefined 가 안 나온다\n // (이 패키지는 noUncheckedIndexedAccess 라 `h[0]` 은 string|undefined 다 — 검증된 hex 만 들어온다는\n // 사실을 타입으로 증명할 수 없으므로, 증명이 필요 없는 방식으로 쓴다).\n let h = hex.slice(1);\n if (h.length === 3) {\n h = h\n .split(\"\")\n .map((c) => c + c)\n .join(\"\");\n }\n return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];\n}\n"]}
package/llms.txt CHANGED
@@ -14,9 +14,12 @@
14
14
  ## 1. 설치·초기화
15
15
 
16
16
  ```bash
17
- npm i ./zalkera-client-0.6.0.tgz # 배포 tarball(심볼릭 링크 말고 tarball — Turbopack 이슈)
17
+ npm install @zalkera/client # 공개 npm (MIT)
18
18
  ```
19
19
 
20
+ - 버전 못 박기: tarball 을 `vendor/` 에 넣어 써도 결과는 동일하다.
21
+ - ❌ 디렉터리 심링크(`file:<dir>`) 금지 — Next 16 Turbopack 이 프로젝트 밖을 못 읽어 깨진다.
22
+
20
23
  ```ts
21
24
  // lib/zalkera.ts — 서버 전용 싱글턴
22
25
  import { createZalkeraClient } from "@zalkera/client";
@@ -61,9 +64,15 @@ await zalkera.submitInquiry(input, { clientIp: ip });
61
64
 
62
65
  ### 커머스 — 카탈로그(공개)
63
66
  - `listProducts({productType?, keyword?, page?, size?, sort?})` → `Paginated<ProductSummary>` (카드용: slug·name·priceFrom·inStock)
64
- - `getProduct(slug)` → `ProductDetail { name, productType, variants[] }`
67
+ - `getProduct(slug)` → `ProductDetail { id, slug, name, description, productType, coverAssetId, seo, variants[] }`
68
+ - `seo` 는 **JSON 문자열 패스스루**(백엔드가 검증 안 함·미설정이면 null). 권장 구조는 `{"title","description"}` —
69
+ `SiteConfig.seoDefaults` 와 같은 모양이다. **파싱 실패해도 죽으면 안 된다**(`parseConfig` 로 읽고, 없으면 상품명으로 강하).
65
70
  - `listProductCategories()` → `ProductCategory[]`
66
71
  - **variant 가 판매 단위다.** 가격·재고·장바구니·주문은 전부 `variant.id` 기준. 단순 상품도 variant 1개.
72
+ `ProductVariant { id, sku, optionSignature, price, compareAtPrice, currency, inStock, available }`
73
+ - `optionSignature` 는 옵션 조합 표시용 문자열("레드/XL") — 단순 상품은 **빈 문자열**이라 그때는 안 그린다.
74
+ - `compareAtPrice` 는 정가(취소선용). null 이면 할인 아님 — **0 과 혼동하지 마라**.
75
+ - 금액 표기는 `currency` 를 따른다. 하드코딩하지 마라.
67
76
  - `variant.inStock`(불리언)·`variant.available`(추적 재고 수량, 무한재고면 null)로 재고 표시.
68
77
  - `productType`: `PHYSICAL`(배송)·`DIGITAL`(즉시)·`SERVICE`(예약).
69
78
  **`SERVICE` 는 장바구니·체크아웃을 타지 않는다** — 별도 예약 흐름(§4.6). 담기 시도는 400 이다.
@@ -108,11 +117,65 @@ await zalkera.submitInquiry(input, { clientIp: ip });
108
117
  ### 커머스 — 결제·주문·배송
109
118
  - `checkout(input, session)` → `OrderDetail` (input: `{ buyerName, buyerPhone, buyerEmail?, shipTo? }`)
110
119
  - 주문 생성 시 가격 스냅샷 동결 + 재고 원자적 차감. 재고 부족이면 409. 상태 `PENDING_PAYMENT`.
111
- - `startPayment(orderNo, access)` → `{ paymentUrl }` — 고객을 paymentUrl 로 리다이렉트.
120
+ - `startPayment(orderNo, access)` → `PaymentSession { paymentUrl, pgPaymentId, widget? }`
121
+ - **`widget` 유무가 분기다**: 있으면 위젯형(내 사이트에서 결제창), 없으면 `paymentUrl` 로 리다이렉트(§4.3).
122
+ - `widget` 에는 **브라우저 노출이 전제된 값만** 담긴다(토스: `vendor:"TOSS_PAYMENTS"`·`clientKey`·`orderId`·`amount`·`orderName`). PG secretKey 는 안 들어온다.
123
+ - **키 구성은 벤더마다 다르다** — `widget.vendor` 로 분기해 읽는다.
124
+ - `confirmPayment(orderNo, providerParams, access)` → `void` — **위젯형 전용** 승인 확정.
125
+ 성공 콜백 파라미터(`{ paymentKey }` 등)를 그대로 넘긴다. 금액은 안 넘겨도 된다 —
126
+ 서버가 **저장된 주문 금액**으로 PG 에 직접 묻는다(브라우저 금액 조작 무효).
112
127
  - `getOrder(orderNo, access)` · `listMyOrders(accessToken,{page,size})` · `cancelOrder(orderNo, access)`
113
128
  · `completeOrder(orderNo, access)` · `getShipment(orderNo, access)`
114
129
  - `access = { accessToken? }`(로그인) **또는 `{ phone? }`(게스트, 주문 시 남긴 연락처)**.
115
130
 
131
+ ### 계약 헬퍼 — 직접 짜지 말고 이걸 부른다
132
+
133
+ 이 패키지는 **API 클라이언트이면서 계약 부품의 운반체**다. 아래는 전부 `@zalkera/client` 에서 import 한다 —
134
+ 프로젝트 안에 `lib/safeUrl.ts`·`sections/parse.ts` 를 새로 만들지 마라(사본이 갈라지면 수리가 안 퍼진다).
135
+
136
+ - `safeLinkUrl(raw)` → `string` — **모든 `href` 는 이걸 태운다.** 콘솔 입력이라 `javascript:` 가 들어올 수 있다(저장형 XSS).
137
+ - `parseConfig<T>(config)` → `T | null` — 섹션 `config` JSON 파싱. **절대 throw 하지 않는다**(실패 시 null).
138
+ - `asString(v)` / `asId(v)` — 형이 안 맞으면 `undefined`.
139
+ - `asIdArray(v)` / `asObjectArray(v)` — **빈 배열**로 강하한다(undefined 아님).
140
+ 빈 배열은 truthy 라 `if (items)` 로 결측을 판별하면 안 된다 — `items.length` 을 봐라.
141
+ - 위 가드로 **필수 값이 없으면 그 섹션만 안 그린다**. 페이지 전체가 죽으면 안 된다.
142
+ - `mediaSrc(assetId)` → `/media/{id}` **경로 문자열**. 이미지는 전부 이걸 통한다.
143
+ 전제: 프로젝트에 `app/media/[id]/route.ts` 프록시 라우트가 있어야 한다(스타터에 있다).
144
+ 이 헬퍼는 경로만 만든다 — 라우트를 안 만들면 이미지가 404 다.
145
+ - `SECTION_CONTRACT` / `SECTION_CONTRACT_REV` / `sectionsOfVertical(vertical)` — 아는 섹션 어휘의 코드 표현(§9).
146
+ 타입·키를 **지어내지 말고** 여기서 확인한다.
147
+ - `parseThemeColors(raw)` → `ParsedTheme` — 테마 토큰 파서(§8). 화이트리스트라 임의 CSS 가 안 들어온다.
148
+
149
+ ### ISR 캐시 태그 (`ReadOptions`)
150
+
151
+ 읽기 메서드는 마지막 인자로 `{ tags: [...] }` 를 받는다. 넘기면 그 fetch 에 `next.tags` 가 실려
152
+ **백엔드가 `revalidateTag` 로 온디맨드 무효화**한다 — 테넌트가 콘솔에서 고친 것이 즉시 반영되는 경로다.
153
+
154
+ ```ts
155
+ const config = await zalkera.getSiteConfig({ tags: ["site-config"] });
156
+ const product = await zalkera.getProduct(slug, { tags: ["products", `product:${slug}`] });
157
+ ```
158
+
159
+ **백엔드가 실제로 발화하는 태그는 둘뿐이다**(실측 — `OperationRegistryChangeApplier`):
160
+
161
+ | 콘솔에서 바뀐 것 | 발화 태그 |
162
+ |---|---|
163
+ | 테마·레이아웃·회사정보(`site.*`) | `site-config` |
164
+ | 상품·가격·재고(`product.*`·`inventory.*`) | `products` |
165
+ | 그 밖(예약·게시물·미지의 op) | 태그 없이 `"/"` 경로 무효화로 폴백 |
166
+
167
+ - **`product:{slug}` 는 기대하지 마라.** 백엔드가 일부러 안 붙인다 — 오퍼레이션 페이로드의 상품 참조가
168
+ 고객이 말로 지시한 모호 참조라 slug 와 일치한다는 보장이 없다. 상품 페이지도 `products` 로 받는다.
169
+ - **모든 페이지 fetch 에 `site-config` 를 동승시켜라.** 테마·레이아웃은 전 페이지에 영향인데,
170
+ 발화 태그는 `site-config` 하나뿐이라 이걸 안 달면 그 페이지만 옛 테마로 남는다.
171
+
172
+ ```ts
173
+ // 페이지 라우트 — 자기 태그 + site-config 동승
174
+ const page = await zalkera.getPage(slug, { tags: ["site-config", "pages", `page:${slug}`] });
175
+ ```
176
+
177
+ - **안 넘기면 세그먼트 기본 캐시만 걸린다** — 콘솔에서 고쳐도 화면이 안 바뀐다는 신고가 대개 이것이다.
178
+
116
179
  ## 4. 레시피
117
180
 
118
181
  ### 4.1 상품 목록 페이지 (RSC)
@@ -154,11 +217,11 @@ export async function POST(req: Request) {
154
217
  // → 스타터의 checkout/page.tsx(openWidget)·payment/complete/page.tsx 를 그대로 쓴다
155
218
  await zalkera.confirmPayment(orderNo, { paymentKey }, access); // BFF 경유
156
219
  } else {
157
- redirect(session.paymentUrl); // 리다이렉트형(PayOneQ )
220
+ redirect(session.paymentUrl); // 리다이렉트형(PG 결제창으로 이동)
158
221
  }
159
222
  ```
160
223
  `widget` 유무로만 분기한다 — **벤더 이름으로 분기하지 마라**(벤더가 늘어난다).
161
- 3. 페이원큐 결제창에서 결제. **결과는 서버 웹훅으로 백엔드가 처리**(프론트는 관여 안 함).
224
+ 3. PG 결제창에서 결제. **결과는 서버 웹훅으로 백엔드가 처리**(프론트는 관여 안 함).
162
225
  4. returnUrl 로 돌아오면 `getOrder(orderNo, access)` 로 상태 확인(잠시 PENDING 일 수 있음 → PAID 폴링/새로고침).
163
226
 
164
227
  > ⚠️ returnUrl·successUrl 콜백의 "성공"을 **신뢰하지 말 것**. 진짜 확정은 백엔드가 한다 — 웹훅(리다이렉트형)
@@ -283,7 +346,7 @@ if (booking.orderNo) { // status=PENDING
283
346
  `export const dynamic='force-dynamic'`/`fetch(...,{cache:'no-store'})` **금지**(per-page SSR 유발).
284
347
  실시간·개인화 데이터(라이브 재고·개인화)는 **클라이언트 컴포넌트(아일랜드)**로 가져오고, 상태 변경
285
348
  (장바구니·주문)은 **BFF route handler** 로 한다. 신선도는 **온디맨드 revalidate**(백엔드 데이터 변경 시
286
- `POST /api/revalidate`)로 지킨다. 동적 SSR 이 꼭 필요하면(예: 검색) **정당화 주석**(`// oneque-allow-dynamic:
349
+ `POST /api/revalidate`)로 지킨다. 동적 SSR 이 꼭 필요하면(예: 검색) **정당화 주석**(`// zalkera-allow-dynamic:
287
350
  <이유>`)이 필요하다 — 서버 동적 렌더는 상시 런타임 원가라 CI validator(§5)가 SEO 라우트에서 강제 차단한다.
288
351
 
289
352
  ## 5.1 산출물 규범 — 발견되는 사이트 (필수)
@@ -350,9 +413,12 @@ catch (e) {
350
413
 
351
414
  ## 7. 결제 파트너
352
415
 
353
- 결제·정산은 **페이원큐(PayOneQ)** 실행한다. 프론트는 카드정보를 절대 다루지 않는다(PCI 범위 밖).
416
+ 결제·정산은 **제3자 PG(결제대행사)** 위임한다. 프론트는 카드정보를 절대 다루지 않는다(PCI 범위 밖).
354
417
  `startPayment` 가 준 URL 로 보내고, 결과는 백엔드가 웹훅으로 받아 주문 상태를 바꾼다.
355
418
 
419
+ **어느 PG 인지에 의존하는 분기를 짜지 마라** — PG 는 교체될 수 있다. 프론트가 보는 계약(`startPayment` 가
420
+ 주는 리다이렉트 URL, 웹훅 뒤에 바뀌는 주문 상태)은 그대로다.
421
+
356
422
  ## 8. 스타일 규약 — Tailwind v4 + 테마 토큰 (필수)
357
423
 
358
424
  스타터(템플릿)는 **Tailwind v4** 와 **테마 토큰**으로 스타일한다. 화면을 그릴 때 아래를 지킨다 — 어기면
@@ -440,23 +506,29 @@ CI validator **S6** 가 좌변 잔존을 error 로 막는다.
440
506
 
441
507
  ```ts
442
508
  const page = await zalkera.getPage("home");
443
- page.sections // [{ id, type: "HERO", sortOrder: 0, config: "{\"title\":\"…\"}" }, …]
509
+ page.sections // [{ type: "HERO", sortOrder: 0, config: "{\"title\":\"…\"}" }, …]
444
510
  ```
445
511
 
446
512
  **왜 이 구조인가**: 콘텐츠가 마크업에 박히지 않고 계약을 타므로, 테넌트가 콘솔에서·"말로" 고친 것이 재코딩 없이
447
513
  반영된다("FAQ 에 배송 질문 추가해줘"). **마크업에 문구를 하드코딩하면 이 경로가 죽는다.**
448
514
 
449
515
  **규약**
450
- - `config` 는 **문자열**이다. 파싱은 반드시 실패 허용으로(스타터의 `components/sections/parse.ts` 본보기 —
451
- 절대 throw 하지 않는다). 필수 필드가 없으면 **그 섹션만 안 그린다**. 페이지 전체가 죽으면 안 된다.
516
+ - `config` 는 **문자열**이다. 파싱은 `parseConfig` + `asString`/`asId`/`asIdArray`/`asObjectArray` 한다
517
+ (이 패키지가 export — 절대 throw 하지 않는다). 필수 필드가 없으면 **그 섹션만 안 그린다**.
518
+ 페이지 전체가 죽으면 안 된다.
452
519
  - **미지 타입은 조용히 건너뛴다.** 어휘는 append-only 라 백엔드가 타입을 늘려도 옛 사이트가 깨지지 않아야 한다.
453
520
  `default:` 에서 에러·경고를 내지 마라 — 그게 계약이다.
454
- - 이미지는 `assetId`(숫자)다. `getMediaUrl(id)`(스타터의 `mediaSrc()`)로 프록시 경유해 그린다.
455
- - **모든 href 는 `lib/safeUrl` 을 태운다.** 콘솔 입력이라 `javascript:` 가 들어올 수 있다(저장형 XSS).
521
+ - 이미지는 `assetId`(숫자)다. `mediaSrc(id)`( 패키지 export)로 프록시 경유해 그린다.
522
+ - **모든 href 는 `safeLinkUrl()` 을 태운다.** 콘솔 입력이라 `javascript:` 가 들어올 수 있다(저장형 XSS).
456
523
  - 자유 HTML 은 없다 — plain text + 줄바꿈뿐(`whitespace-pre-wrap`).
457
524
  - 타입·키를 **지어내지 마라.** 계약 정본은 `SECTION_CONTRACT`(이 패키지가 export)이고, 필요한 어휘가 없으면
458
525
  만들지 말고 **보고**한다.
459
526
 
527
+ **디스패치는 직접 짠다** — `type` 으로 컴포넌트를 고르는 `switch` 하나면 된다. 위 규약에 더해 둘을 지켜라:
528
+
529
+ - **`sortOrder` 로 한 번 더 정렬한다.** 서버가 정렬해 주지만 순서가 틀리면 화면에서 바로 티가 나는 종류다.
530
+ - **key 는 `type`+`sortOrder`+index 로 짠다.** `PageSection` 계약에 id 가 없어(실측) 순수 인덱스보다 낫다.
531
+
460
532
  **어휘 (12종)** — `vertical` 은 콘솔 픽커 그룹핑용이지 사용 제한이 아니다(GENERAL 은 뷰티 테넌트도 쓴다).
461
533
 
462
534
  | type | vertical | config (필수는 굵게) | JSON-LD |
package/package.json CHANGED
@@ -1,50 +1,51 @@
1
1
  {
2
- "name": "@zalkera/client",
3
- "version": "0.7.0",
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"
2
+ "name": "@zalkera/client",
3
+ "version": "0.7.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"
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
+ "@types/node": "^22.10.2",
47
+ "tsup": "^8.3.5",
48
+ "typescript": "^5.7.2",
49
+ "vitest": "^2.1.8"
32
50
  }
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
- }
50
51
  }