@zalkera/client 0.6.1 → 0.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -16,7 +16,7 @@ interface ApiErrorBody {
16
16
  error: string;
17
17
  /**
18
18
  * 기계 판독 코드(`OUT_OF_STOCK`·`IDEMPOTENCY_CONFLICT`…). 같은 상태코드의 여러 원인을 가른다.
19
- * 구버전 백엔드 응답엔 없다 — `OnequeError.code` 가 `error` 로 폴백한다.
19
+ * 구버전 백엔드 응답엔 없다 — `ZalkeraError.code` 가 `error` 로 폴백한다.
20
20
  */
21
21
  errorCode?: string;
22
22
  message: string;
@@ -115,6 +115,25 @@ interface ListPostsParams {
115
115
  /** Spring 정렬 표현. 예: `"publishedAt,desc"`. */
116
116
  sort?: string;
117
117
  }
118
+ /**
119
+ * `PublicPageSummaryResponse` — 페이지 **목록 카드**. 열거 전용이라 본문·섹션·seo 가 없다.
120
+ *
121
+ * 이 표면의 존재 이유는 sitemap 이다: 상세([PageContent])로 100개를 열거하면 섹션·본문까지 딸려와
122
+ * 크롤러 한 번에 카탈로그 전체를 붓게 된다. 상세가 필요하면 slug 로 [ZalkeraClient.getPage] 를 부른다.
123
+ */
124
+ interface PageSummary {
125
+ slug: string;
126
+ title: string;
127
+ publishedAt: string | null;
128
+ /** 마지막 수정 시각 — sitemap `lastModified` 의 입력. 없으면 소비자가 필드를 생략한다. */
129
+ modified: string | null;
130
+ }
131
+ /** 페이지 목록 질의. page 는 0-based. 정렬은 서버 고정(제목 오름차순)이라 `sort` 가 없다. */
132
+ interface ListPagesParams {
133
+ page?: number;
134
+ /** 서버 상한 100 — 초과 요청은 100 으로 잘린다. */
135
+ size?: number;
136
+ }
118
137
  /**
119
138
  * 페이지 섹션 — 구조화된 페이지 구성 요소.
120
139
  *
@@ -126,20 +145,18 @@ interface ListPostsParams {
126
145
  * 검증하지 않는다 — 사이트 하나 고칠 때마다 백엔드를 배포하지 않으려는 의도다. 파싱은 소비자
127
146
  * 몫이고, **깨진 config 는 그 섹션만 건너뛰어야지 페이지를 죽이면 안 된다.**
128
147
  *
129
- * 타입별 config 형상(정본은 백엔드 `SectionType` enum KDoc — 바뀌면 세 곳을 같이 고친다):
130
- * - `SERVICE_MENU` `{ productIds: number[] }` (`{ categorySlug }` 변형은 공개 상품 API 에
131
- * 카테고리 필터가 아직 없어 미지원)
132
- * - `BEFORE_AFTER_GALLERY` — `{ items: [{ beforeAssetId, afterAssetId, caption }] }`
133
- * - `BOOKING_CTA` `{ productId, label }`
134
- * - `DOCTOR_INTRO` `{ name, title, photoAssetId, bio }`
148
+ * 타입별 config 형상의 **정본은 백엔드 레포 `doc/contracts/section-vocabulary.json`** 이다.
149
+ * 여기에 형상을 다시 적지 않는다 벌이면 갈라진다(사본 넷이 주석 규약으로 갈라진 사고가 실재).
150
+ * 아는 타입 목록은 [SECTION_CONTRACT] 로 제공한다.
151
+ *
152
+ * `SERVICE_MENU` `{ categorySlug }` 변형만 예외적으로 여기 적어 둔다 — 공개 상품 API 에 카테고리
153
+ * 필터가 없어 **스펙에는 있으나 소비자가 아직 지원하지 않는** 상태라, 스펙만 봐서는 알 수 없다.
135
154
  */
136
155
  interface PageSection {
137
156
  type: string;
138
157
  sortOrder: number;
139
158
  config: string | null;
140
159
  }
141
- /** 지금 렌더러가 아는 섹션 타입. 이 밖의 값이 와도 정상이다(스킵). */
142
- type KnownSectionType = "SERVICE_MENU" | "BEFORE_AFTER_GALLERY" | "BOOKING_CTA" | "DOCTOR_INTRO";
143
160
  /** `PublicPageResponse` — 회사 소개 같은 고정 페이지. */
144
161
  interface PageContent {
145
162
  id: number;
@@ -537,7 +554,7 @@ interface OrderSummary {
537
554
  * **벤더가 둘 중 하나다**(테넌트가 자기 PG 를 고른다 — 기본 TOSS):
538
555
  * - **리다이렉트형**(PayOneQ 등): [widget] 이 없다 → [paymentUrl] 로 고객을 보내면 끝.
539
556
  * - **위젯형**(토스): [widget] 값으로 **내 사이트에서** 결제창을 띄우고, 성공 콜백의 파라미터를
540
- * [OnequeClient.confirmPayment] 로 넘겨 승인을 확정한다.
557
+ * [ZalkeraClient.confirmPayment] 로 넘겨 승인을 확정한다.
541
558
  *
542
559
  * 분기는 `session.widget ? 위젯 : redirect(session.paymentUrl)` 한 줄이면 된다.
543
560
  */
@@ -582,7 +599,7 @@ interface ReadOptions {
582
599
  /** Next ISR 캐시 태그. 이 태그로 백엔드가 온디맨드 revalidate 한다. */
583
600
  tags?: string[];
584
601
  }
585
- interface OnequeClientOptions {
602
+ interface ZalkeraClientOptions {
586
603
  /**
587
604
  * 백엔드 베이스 URL — `/api` 접두사는 붙이지 않는다. 예: `http://localhost:8100`.
588
605
  * 클라이언트가 경로에 `/api/public/...` 를 붙인다.
@@ -594,11 +611,29 @@ interface OnequeClientOptions {
594
611
  * 공개 API 는 이 헤더를 그대로 믿는다(비인증). 그래서 이 클라이언트는 **서버 사이드**
595
612
  * (RSC·route handler·server action)에서 쓰는 것을 전제로 한다 — 브라우저에서 직접 부르면
596
613
  * baseUrl 이 노출되고 CORS 를 열어야 한다. README 참고.
614
+ *
615
+ * [secretKey] 를 함께 주면 백엔드는 키로 테넌트를 결정하고(키가 정본), 이 값은 대조용으로만
616
+ * 쓰인다. 이행기(dual)에는 둘 다 보내도 무방하며 일치해야 한다(불일치 시 403 `TENANT_MISMATCH`).
597
617
  */
598
618
  tenant: string;
619
+ /**
620
+ * 스토어프론트 서버 시크릿 키(선택·`oqsk_…`). 주면 모든 요청에 `X-Storefront-Key` 헤더로 실려,
621
+ * 백엔드가 이 키로 테넌트 신원을 증명한다(memo78 — [tenant] 무인증 신뢰의 보안 승격).
622
+ *
623
+ * ⚠️ **진짜 비밀이다.** 오직 서버 `.env`(예: `ONEQUE_STOREFRONT_KEY`)에만 두고, 브라우저 번들에
624
+ * 절대 넣지 마라 — `NEXT_PUBLIC_*` 접두사·클라이언트 컴포넌트 import 금지. 이 클라이언트가 서버
625
+ * 전용인 이유가 그것이다. 유출 시 콘솔에서 revoke·재발급.
626
+ *
627
+ * 안 주면 종전대로 [tenant](`X-Tenant`)만으로 동작한다(dual 이행기 하위호환). 백엔드가 `required`
628
+ * 모드인데 키가 없으면 401 `STOREFRONT_KEY_REQUIRED` → [ZalkeraError].
629
+ */
630
+ secretKey?: string;
599
631
  /**
600
632
  * `fetch` 구현 주입(선택). 기본은 전역 `fetch`(Node 18+·브라우저). 테스트·커스텀 에이전트·
601
633
  * Next.js 의 `fetch` 캐시 옵션을 감싸는 래퍼를 넣을 때 쓴다.
634
+ *
635
+ * (내부 `request` 전송부는 이 주입점을 데이터 소스 seam 으로 유지한다 — 미래의 로컬 픽스처(mock)
636
+ * 모드를 갈아엎지 않고 얹기 위한 여지다. mock 구현은 현재 없다 — memo78 §14 후속.)
602
637
  */
603
638
  fetch?: typeof fetch;
604
639
  /** 모든 요청에 추가할 헤더(선택). */
@@ -607,27 +642,27 @@ interface OnequeClientOptions {
607
642
  timeoutMs?: number;
608
643
  }
609
644
  /**
610
- * Oneque 공개 API 클라이언트.
645
+ * zalkera 공개 API 클라이언트.
611
646
  *
612
647
  * 테넌트 사이트(credium 등)가 백엔드의 공개 엔드포인트를 타입 안전하게 부르기 위한 얇은 래퍼다.
613
648
  * 런타임 의존성이 없다 — 전역 `fetch` 만 쓴다.
614
649
  *
615
650
  * ```ts
616
- * const cms = createOnequeClient({ baseUrl: process.env.API_BASE_URL!, tenant: "credium" });
651
+ * const cms = createZalkeraClient({ baseUrl: process.env.API_BASE_URL!, tenant: "credium" });
617
652
  * const posts = await cms.listPosts({ size: 10, sort: "publishedAt,desc" });
618
653
  * await cms.submitInquiry({ name, email, subject, message });
619
654
  * ```
620
655
  *
621
- * 모든 메서드는 성공 시 envelope 안쪽 `data` 를 돌려주고, 실패 시 [OnequeError] 를 던진다.
656
+ * 모든 메서드는 성공 시 envelope 안쪽 `data` 를 돌려주고, 실패 시 [ZalkeraError] 를 던진다.
622
657
  */
623
- interface OnequeClient {
658
+ interface ZalkeraClient {
624
659
  /** 회사 정보·테마·SEO 기본값. ISR 페이지는 [ReadOptions.tags] 로 캐시 태그를 실을 수 있다. */
625
660
  getSiteConfig(options?: ReadOptions): Promise<SiteConfig>;
626
661
  /** 살아 있는 카테고리 전부(페이징 없음). */
627
662
  listCategories(): Promise<Category[]>;
628
663
  /** 발행된 글 목록(페이징). */
629
664
  listPosts(params?: ListPostsParams): Promise<Paginated<PostSummary>>;
630
- /** slug 로 글 상세(본문 포함). 없으면 404 → [OnequeError]. */
665
+ /** slug 로 글 상세(본문 포함). 없으면 404 → [ZalkeraError]. */
631
666
  getPost(slug: string): Promise<PostDetail>;
632
667
  /**
633
668
  * 조회 비콘 — 같은 뷰어의 같은 날 재조회는 집계되지 않는다(반환 false).
@@ -639,11 +674,18 @@ interface OnequeClient {
639
674
  recordPostView(slug: string, context?: RequestContext): Promise<boolean>;
640
675
  /** slug 로 고정 페이지. */
641
676
  /**
642
- * slug 로 고정 페이지(섹션 포함). 없으면 404 → [OnequeError].
677
+ * slug 로 고정 페이지(섹션 포함). 없으면 404 → [ZalkeraError].
643
678
  * ISR 페이지는 [ReadOptions.tags] 로 캐시 태그를 실을 수 있다 — 백엔드가 발행 시 그 태그만
644
679
  * 콕 집어 revalidate 한다.
645
680
  */
646
681
  getPage(slug: string, options?: ReadOptions): Promise<PageContent>;
682
+ /**
683
+ * 발행된 고정 페이지 **목록**(열거 전용·본문 없음).
684
+ *
685
+ * sitemap 을 위한 API 다 — 이게 없으면 콘솔·AI 로 만든 페이지가 검색엔진에 열거되지 않는다.
686
+ * 목록에 실린 slug 는 [getPage] 가 반드시 200 을 준다(가시성 술어가 서버에서 하나로 공유된다).
687
+ */
688
+ listPages(params?: ListPagesParams, options?: ReadOptions): Promise<Paginated<PageSummary>>;
647
689
  /** 메뉴 트리(HEADER·FOOTER). */
648
690
  listMenus(options?: ReadOptions): Promise<Menu[]>;
649
691
  /** 미디어 presigned 다운로드 URL(만료 있음). */
@@ -810,10 +852,10 @@ interface RequestContext {
810
852
  /** 원 방문자 IP — route handler 에서 요청 헤더(x-forwarded-for·x-real-ip)로 뽑아 넘긴다. */
811
853
  clientIp?: string;
812
854
  }
813
- declare function createOnequeClient(options: OnequeClientOptions): OnequeClient;
855
+ declare function createZalkeraClient(options: ZalkeraClientOptions): ZalkeraClient;
814
856
 
815
857
  /**
816
- * Oneque API 호출 실패.
858
+ * zalkera API 호출 실패.
817
859
  *
818
860
  * 백엔드의 `ErrorResponse` 를 그대로 담는다.
819
861
  *
@@ -829,7 +871,7 @@ declare function createOnequeClient(options: OnequeClientOptions): OnequeClient;
829
871
  *
830
872
  * 네트워크 자체가 실패했거나 응답이 JSON 이 아니면 [status] 가 0 이고 [body] 가 null 이다.
831
873
  */
832
- declare class OnequeError extends Error {
874
+ declare class ZalkeraError extends Error {
833
875
  /** HTTP 상태. 네트워크 실패·비JSON 응답이면 0. */
834
876
  readonly status: number;
835
877
  /**
@@ -854,8 +896,94 @@ declare class OnequeError extends Error {
854
896
  });
855
897
  /** IP 레이트리밋(429) — 문의 폼에서 "잠시 후 다시" 를 띄울 때 쓴다. */
856
898
  get isRateLimited(): boolean;
899
+ /**
900
+ * 스토어프론트 시크릿 키 문제(memo78) — 개발자 오배선 신호. `true` 면 `secretKey` 옵션을
901
+ * 점검하라(미설정·불일치·폐기). [code] 로 정밀 분기: `STOREFRONT_KEY_REQUIRED`(401·키 필요)·
902
+ * `TENANT_MISMATCH`(403·키↔tenant 불일치).
903
+ */
904
+ get isStorefrontKeyError(): boolean;
857
905
  /** 응답 본문(파싱된 것)으로부터 에러를 만든다. */
858
- static fromBody(status: number, body: unknown): OnequeError;
906
+ static fromBody(status: number, body: unknown): ZalkeraError;
907
+ }
908
+
909
+ /**
910
+ * 섹션 어휘 계약 — **정본의 코드 표현**(memo102 §6).
911
+ *
912
+ * 정본은 백엔드 레포의 `doc/contracts/section-vocabulary.json` 이고, 이 파일은 그것을 npm 으로
913
+ * 실어 나르는 **운반체**다. 템플릿 렌더러와 콘솔 zod 스키마가 이 상수를 읽어 자기 커버리지를
914
+ * 기계로 검사한다 — 사본이 갈라진 채 조용히 굳는 것을 막는 게 목적이지, 실시간 동일성이 목적은
915
+ * 아니다(계약이 원래 스큐 내성으로 설계돼 있다: 미지 타입은 스킵).
916
+ *
917
+ * 두 레포가 갈라져 있어 상호 CI 강제가 불가능하므로 **사람 이음새가 정확히 한 곳** 남는다 —
918
+ * 백엔드 JSON ↔ 이 파일. [SECTION_CONTRACT_REV] 를 백엔드 스펙의 `contractRev` 와 맞춰 두고,
919
+ * client 발행 전 `scripts/sync-section-contract.mjs` 로 대조한다.
920
+ */
921
+ /** 백엔드 스펙 `contractRev` 와 같아야 한다. 어긋나면 동기 스크립트가 잡는다. */
922
+ declare const SECTION_CONTRACT_REV = 1;
923
+ /** 업종 분류 — 콘솔 섹션 픽커의 그룹핑에 쓴다(테넌트 업종 필드는 아직 없다·memo102 §5). */
924
+ type SectionVertical = "BEAUTY" | "GENERAL";
925
+ interface SectionSpec {
926
+ readonly type: string;
927
+ readonly vertical: SectionVertical;
928
+ /** 이 섹션이 산출하는 schema.org 타입. null 이면 구조화 데이터 없음. */
929
+ readonly jsonLd: string | null;
859
930
  }
931
+ /**
932
+ * 아는 섹션 전량. **순서는 콘솔 픽커의 노출 순서**(관행 아크: 주목→가치→신뢰→행동).
933
+ * 값 추가는 백엔드 스펙을 먼저 고친 뒤 여기로 옮긴다.
934
+ */
935
+ declare const SECTION_CONTRACT: readonly [{
936
+ readonly type: "SERVICE_MENU";
937
+ readonly vertical: "BEAUTY";
938
+ readonly jsonLd: null;
939
+ }, {
940
+ readonly type: "BEFORE_AFTER_GALLERY";
941
+ readonly vertical: "BEAUTY";
942
+ readonly jsonLd: null;
943
+ }, {
944
+ readonly type: "BOOKING_CTA";
945
+ readonly vertical: "BEAUTY";
946
+ readonly jsonLd: null;
947
+ }, {
948
+ readonly type: "DOCTOR_INTRO";
949
+ readonly vertical: "BEAUTY";
950
+ readonly jsonLd: null;
951
+ }, {
952
+ readonly type: "HERO";
953
+ readonly vertical: "GENERAL";
954
+ readonly jsonLd: null;
955
+ }, {
956
+ readonly type: "FEATURE_GRID";
957
+ readonly vertical: "GENERAL";
958
+ readonly jsonLd: null;
959
+ }, {
960
+ readonly type: "TEXT_MEDIA";
961
+ readonly vertical: "GENERAL";
962
+ readonly jsonLd: null;
963
+ }, {
964
+ readonly type: "LOGO_WALL";
965
+ readonly vertical: "GENERAL";
966
+ readonly jsonLd: null;
967
+ }, {
968
+ readonly type: "STATS_BAND";
969
+ readonly vertical: "GENERAL";
970
+ readonly jsonLd: null;
971
+ }, {
972
+ readonly type: "TESTIMONIALS";
973
+ readonly vertical: "GENERAL";
974
+ readonly jsonLd: null;
975
+ }, {
976
+ readonly type: "FAQ_LIST";
977
+ readonly vertical: "GENERAL";
978
+ readonly jsonLd: "FAQPage";
979
+ }, {
980
+ readonly type: "LEAD_CTA";
981
+ readonly vertical: "GENERAL";
982
+ readonly jsonLd: null;
983
+ }];
984
+ /** 지금 렌더러가 아는 섹션 타입. 이 밖의 값이 와도 정상이다(스킵). */
985
+ type KnownSectionType = (typeof SECTION_CONTRACT)[number]["type"];
986
+ /** 업종별 필터 — 콘솔 픽커 그룹핑용. */
987
+ declare function sectionsOfVertical(vertical: SectionVertical): readonly SectionSpec[];
860
988
 
861
- export { type ApiErrorBody, type AuthTokens, type AvailabilityParams, type AvailabilitySlot, type Booking, type BookingStatus, type BusinessType, type Cart, type CartLine, type Category, type CheckoutInput, type ConsentInput, type ConsentStatus, type ConsentType, type CreateBookingInput, type CreateReviewInput, type CustomerSummary, type InquiryCreated, type InquiryInput, type KnownSectionType, type LeadCreated, type LeadInput, type LeadTracking, type ListPostsParams, type ListProductsParams, type ListReviewsParams, type MediaUrl, type Menu, type MenuPosition, type OnequeClient, type OnequeClientOptions, OnequeError, type OrderAccess, type OrderDetail, type OrderHistoryEntry, type OrderItemLine, type OrderStatus, type OrderSummary, type PageContent, type PageSection, type Paginated, type PaymentSession, type PostDetail, type PostSummary, type ProductCategory, type ProductDetail, type ProductSummary, type ProductType, type ProductVariant, type RatingSummary, type ReadOptions, type RequestContext, type Review, type ShipToInput, type ShipmentEvent, type ShipmentInfo, type ShipmentStatus, type ShopSession, type SiteConfig, type SocialLoginInput, type SocialProvider, type ValidationError, createOnequeClient };
989
+ export { type ApiErrorBody, type AuthTokens, type AvailabilityParams, type AvailabilitySlot, type Booking, type BookingStatus, type BusinessType, type Cart, type CartLine, type Category, type CheckoutInput, type ConsentInput, type ConsentStatus, type ConsentType, type CreateBookingInput, type CreateReviewInput, type CustomerSummary, type InquiryCreated, type InquiryInput, type KnownSectionType, type LeadCreated, type LeadInput, type LeadTracking, type ListPagesParams, type ListPostsParams, type ListProductsParams, type ListReviewsParams, type MediaUrl, type Menu, type MenuPosition, type OrderAccess, type OrderDetail, type OrderHistoryEntry, type OrderItemLine, type OrderStatus, type OrderSummary, type PageContent, type PageSection, type PageSummary, type Paginated, type PaymentSession, type PostDetail, type PostSummary, type ProductCategory, type ProductDetail, type ProductSummary, type ProductType, type ProductVariant, type RatingSummary, type ReadOptions, type RequestContext, type Review, SECTION_CONTRACT, SECTION_CONTRACT_REV, type SectionSpec, type SectionVertical, type ShipToInput, type ShipmentEvent, type ShipmentInfo, type ShipmentStatus, type ShopSession, type SiteConfig, type SocialLoginInput, type SocialProvider, type ValidationError, type ZalkeraClient, type ZalkeraClientOptions, ZalkeraError, createZalkeraClient, sectionsOfVertical };
package/dist/index.d.ts CHANGED
@@ -16,7 +16,7 @@ interface ApiErrorBody {
16
16
  error: string;
17
17
  /**
18
18
  * 기계 판독 코드(`OUT_OF_STOCK`·`IDEMPOTENCY_CONFLICT`…). 같은 상태코드의 여러 원인을 가른다.
19
- * 구버전 백엔드 응답엔 없다 — `OnequeError.code` 가 `error` 로 폴백한다.
19
+ * 구버전 백엔드 응답엔 없다 — `ZalkeraError.code` 가 `error` 로 폴백한다.
20
20
  */
21
21
  errorCode?: string;
22
22
  message: string;
@@ -115,6 +115,25 @@ interface ListPostsParams {
115
115
  /** Spring 정렬 표현. 예: `"publishedAt,desc"`. */
116
116
  sort?: string;
117
117
  }
118
+ /**
119
+ * `PublicPageSummaryResponse` — 페이지 **목록 카드**. 열거 전용이라 본문·섹션·seo 가 없다.
120
+ *
121
+ * 이 표면의 존재 이유는 sitemap 이다: 상세([PageContent])로 100개를 열거하면 섹션·본문까지 딸려와
122
+ * 크롤러 한 번에 카탈로그 전체를 붓게 된다. 상세가 필요하면 slug 로 [ZalkeraClient.getPage] 를 부른다.
123
+ */
124
+ interface PageSummary {
125
+ slug: string;
126
+ title: string;
127
+ publishedAt: string | null;
128
+ /** 마지막 수정 시각 — sitemap `lastModified` 의 입력. 없으면 소비자가 필드를 생략한다. */
129
+ modified: string | null;
130
+ }
131
+ /** 페이지 목록 질의. page 는 0-based. 정렬은 서버 고정(제목 오름차순)이라 `sort` 가 없다. */
132
+ interface ListPagesParams {
133
+ page?: number;
134
+ /** 서버 상한 100 — 초과 요청은 100 으로 잘린다. */
135
+ size?: number;
136
+ }
118
137
  /**
119
138
  * 페이지 섹션 — 구조화된 페이지 구성 요소.
120
139
  *
@@ -126,20 +145,18 @@ interface ListPostsParams {
126
145
  * 검증하지 않는다 — 사이트 하나 고칠 때마다 백엔드를 배포하지 않으려는 의도다. 파싱은 소비자
127
146
  * 몫이고, **깨진 config 는 그 섹션만 건너뛰어야지 페이지를 죽이면 안 된다.**
128
147
  *
129
- * 타입별 config 형상(정본은 백엔드 `SectionType` enum KDoc — 바뀌면 세 곳을 같이 고친다):
130
- * - `SERVICE_MENU` `{ productIds: number[] }` (`{ categorySlug }` 변형은 공개 상품 API 에
131
- * 카테고리 필터가 아직 없어 미지원)
132
- * - `BEFORE_AFTER_GALLERY` — `{ items: [{ beforeAssetId, afterAssetId, caption }] }`
133
- * - `BOOKING_CTA` `{ productId, label }`
134
- * - `DOCTOR_INTRO` `{ name, title, photoAssetId, bio }`
148
+ * 타입별 config 형상의 **정본은 백엔드 레포 `doc/contracts/section-vocabulary.json`** 이다.
149
+ * 여기에 형상을 다시 적지 않는다 벌이면 갈라진다(사본 넷이 주석 규약으로 갈라진 사고가 실재).
150
+ * 아는 타입 목록은 [SECTION_CONTRACT] 로 제공한다.
151
+ *
152
+ * `SERVICE_MENU` `{ categorySlug }` 변형만 예외적으로 여기 적어 둔다 — 공개 상품 API 에 카테고리
153
+ * 필터가 없어 **스펙에는 있으나 소비자가 아직 지원하지 않는** 상태라, 스펙만 봐서는 알 수 없다.
135
154
  */
136
155
  interface PageSection {
137
156
  type: string;
138
157
  sortOrder: number;
139
158
  config: string | null;
140
159
  }
141
- /** 지금 렌더러가 아는 섹션 타입. 이 밖의 값이 와도 정상이다(스킵). */
142
- type KnownSectionType = "SERVICE_MENU" | "BEFORE_AFTER_GALLERY" | "BOOKING_CTA" | "DOCTOR_INTRO";
143
160
  /** `PublicPageResponse` — 회사 소개 같은 고정 페이지. */
144
161
  interface PageContent {
145
162
  id: number;
@@ -537,7 +554,7 @@ interface OrderSummary {
537
554
  * **벤더가 둘 중 하나다**(테넌트가 자기 PG 를 고른다 — 기본 TOSS):
538
555
  * - **리다이렉트형**(PayOneQ 등): [widget] 이 없다 → [paymentUrl] 로 고객을 보내면 끝.
539
556
  * - **위젯형**(토스): [widget] 값으로 **내 사이트에서** 결제창을 띄우고, 성공 콜백의 파라미터를
540
- * [OnequeClient.confirmPayment] 로 넘겨 승인을 확정한다.
557
+ * [ZalkeraClient.confirmPayment] 로 넘겨 승인을 확정한다.
541
558
  *
542
559
  * 분기는 `session.widget ? 위젯 : redirect(session.paymentUrl)` 한 줄이면 된다.
543
560
  */
@@ -582,7 +599,7 @@ interface ReadOptions {
582
599
  /** Next ISR 캐시 태그. 이 태그로 백엔드가 온디맨드 revalidate 한다. */
583
600
  tags?: string[];
584
601
  }
585
- interface OnequeClientOptions {
602
+ interface ZalkeraClientOptions {
586
603
  /**
587
604
  * 백엔드 베이스 URL — `/api` 접두사는 붙이지 않는다. 예: `http://localhost:8100`.
588
605
  * 클라이언트가 경로에 `/api/public/...` 를 붙인다.
@@ -594,11 +611,29 @@ interface OnequeClientOptions {
594
611
  * 공개 API 는 이 헤더를 그대로 믿는다(비인증). 그래서 이 클라이언트는 **서버 사이드**
595
612
  * (RSC·route handler·server action)에서 쓰는 것을 전제로 한다 — 브라우저에서 직접 부르면
596
613
  * baseUrl 이 노출되고 CORS 를 열어야 한다. README 참고.
614
+ *
615
+ * [secretKey] 를 함께 주면 백엔드는 키로 테넌트를 결정하고(키가 정본), 이 값은 대조용으로만
616
+ * 쓰인다. 이행기(dual)에는 둘 다 보내도 무방하며 일치해야 한다(불일치 시 403 `TENANT_MISMATCH`).
597
617
  */
598
618
  tenant: string;
619
+ /**
620
+ * 스토어프론트 서버 시크릿 키(선택·`oqsk_…`). 주면 모든 요청에 `X-Storefront-Key` 헤더로 실려,
621
+ * 백엔드가 이 키로 테넌트 신원을 증명한다(memo78 — [tenant] 무인증 신뢰의 보안 승격).
622
+ *
623
+ * ⚠️ **진짜 비밀이다.** 오직 서버 `.env`(예: `ONEQUE_STOREFRONT_KEY`)에만 두고, 브라우저 번들에
624
+ * 절대 넣지 마라 — `NEXT_PUBLIC_*` 접두사·클라이언트 컴포넌트 import 금지. 이 클라이언트가 서버
625
+ * 전용인 이유가 그것이다. 유출 시 콘솔에서 revoke·재발급.
626
+ *
627
+ * 안 주면 종전대로 [tenant](`X-Tenant`)만으로 동작한다(dual 이행기 하위호환). 백엔드가 `required`
628
+ * 모드인데 키가 없으면 401 `STOREFRONT_KEY_REQUIRED` → [ZalkeraError].
629
+ */
630
+ secretKey?: string;
599
631
  /**
600
632
  * `fetch` 구현 주입(선택). 기본은 전역 `fetch`(Node 18+·브라우저). 테스트·커스텀 에이전트·
601
633
  * Next.js 의 `fetch` 캐시 옵션을 감싸는 래퍼를 넣을 때 쓴다.
634
+ *
635
+ * (내부 `request` 전송부는 이 주입점을 데이터 소스 seam 으로 유지한다 — 미래의 로컬 픽스처(mock)
636
+ * 모드를 갈아엎지 않고 얹기 위한 여지다. mock 구현은 현재 없다 — memo78 §14 후속.)
602
637
  */
603
638
  fetch?: typeof fetch;
604
639
  /** 모든 요청에 추가할 헤더(선택). */
@@ -607,27 +642,27 @@ interface OnequeClientOptions {
607
642
  timeoutMs?: number;
608
643
  }
609
644
  /**
610
- * Oneque 공개 API 클라이언트.
645
+ * zalkera 공개 API 클라이언트.
611
646
  *
612
647
  * 테넌트 사이트(credium 등)가 백엔드의 공개 엔드포인트를 타입 안전하게 부르기 위한 얇은 래퍼다.
613
648
  * 런타임 의존성이 없다 — 전역 `fetch` 만 쓴다.
614
649
  *
615
650
  * ```ts
616
- * const cms = createOnequeClient({ baseUrl: process.env.API_BASE_URL!, tenant: "credium" });
651
+ * const cms = createZalkeraClient({ baseUrl: process.env.API_BASE_URL!, tenant: "credium" });
617
652
  * const posts = await cms.listPosts({ size: 10, sort: "publishedAt,desc" });
618
653
  * await cms.submitInquiry({ name, email, subject, message });
619
654
  * ```
620
655
  *
621
- * 모든 메서드는 성공 시 envelope 안쪽 `data` 를 돌려주고, 실패 시 [OnequeError] 를 던진다.
656
+ * 모든 메서드는 성공 시 envelope 안쪽 `data` 를 돌려주고, 실패 시 [ZalkeraError] 를 던진다.
622
657
  */
623
- interface OnequeClient {
658
+ interface ZalkeraClient {
624
659
  /** 회사 정보·테마·SEO 기본값. ISR 페이지는 [ReadOptions.tags] 로 캐시 태그를 실을 수 있다. */
625
660
  getSiteConfig(options?: ReadOptions): Promise<SiteConfig>;
626
661
  /** 살아 있는 카테고리 전부(페이징 없음). */
627
662
  listCategories(): Promise<Category[]>;
628
663
  /** 발행된 글 목록(페이징). */
629
664
  listPosts(params?: ListPostsParams): Promise<Paginated<PostSummary>>;
630
- /** slug 로 글 상세(본문 포함). 없으면 404 → [OnequeError]. */
665
+ /** slug 로 글 상세(본문 포함). 없으면 404 → [ZalkeraError]. */
631
666
  getPost(slug: string): Promise<PostDetail>;
632
667
  /**
633
668
  * 조회 비콘 — 같은 뷰어의 같은 날 재조회는 집계되지 않는다(반환 false).
@@ -639,11 +674,18 @@ interface OnequeClient {
639
674
  recordPostView(slug: string, context?: RequestContext): Promise<boolean>;
640
675
  /** slug 로 고정 페이지. */
641
676
  /**
642
- * slug 로 고정 페이지(섹션 포함). 없으면 404 → [OnequeError].
677
+ * slug 로 고정 페이지(섹션 포함). 없으면 404 → [ZalkeraError].
643
678
  * ISR 페이지는 [ReadOptions.tags] 로 캐시 태그를 실을 수 있다 — 백엔드가 발행 시 그 태그만
644
679
  * 콕 집어 revalidate 한다.
645
680
  */
646
681
  getPage(slug: string, options?: ReadOptions): Promise<PageContent>;
682
+ /**
683
+ * 발행된 고정 페이지 **목록**(열거 전용·본문 없음).
684
+ *
685
+ * sitemap 을 위한 API 다 — 이게 없으면 콘솔·AI 로 만든 페이지가 검색엔진에 열거되지 않는다.
686
+ * 목록에 실린 slug 는 [getPage] 가 반드시 200 을 준다(가시성 술어가 서버에서 하나로 공유된다).
687
+ */
688
+ listPages(params?: ListPagesParams, options?: ReadOptions): Promise<Paginated<PageSummary>>;
647
689
  /** 메뉴 트리(HEADER·FOOTER). */
648
690
  listMenus(options?: ReadOptions): Promise<Menu[]>;
649
691
  /** 미디어 presigned 다운로드 URL(만료 있음). */
@@ -810,10 +852,10 @@ interface RequestContext {
810
852
  /** 원 방문자 IP — route handler 에서 요청 헤더(x-forwarded-for·x-real-ip)로 뽑아 넘긴다. */
811
853
  clientIp?: string;
812
854
  }
813
- declare function createOnequeClient(options: OnequeClientOptions): OnequeClient;
855
+ declare function createZalkeraClient(options: ZalkeraClientOptions): ZalkeraClient;
814
856
 
815
857
  /**
816
- * Oneque API 호출 실패.
858
+ * zalkera API 호출 실패.
817
859
  *
818
860
  * 백엔드의 `ErrorResponse` 를 그대로 담는다.
819
861
  *
@@ -829,7 +871,7 @@ declare function createOnequeClient(options: OnequeClientOptions): OnequeClient;
829
871
  *
830
872
  * 네트워크 자체가 실패했거나 응답이 JSON 이 아니면 [status] 가 0 이고 [body] 가 null 이다.
831
873
  */
832
- declare class OnequeError extends Error {
874
+ declare class ZalkeraError extends Error {
833
875
  /** HTTP 상태. 네트워크 실패·비JSON 응답이면 0. */
834
876
  readonly status: number;
835
877
  /**
@@ -854,8 +896,94 @@ declare class OnequeError extends Error {
854
896
  });
855
897
  /** IP 레이트리밋(429) — 문의 폼에서 "잠시 후 다시" 를 띄울 때 쓴다. */
856
898
  get isRateLimited(): boolean;
899
+ /**
900
+ * 스토어프론트 시크릿 키 문제(memo78) — 개발자 오배선 신호. `true` 면 `secretKey` 옵션을
901
+ * 점검하라(미설정·불일치·폐기). [code] 로 정밀 분기: `STOREFRONT_KEY_REQUIRED`(401·키 필요)·
902
+ * `TENANT_MISMATCH`(403·키↔tenant 불일치).
903
+ */
904
+ get isStorefrontKeyError(): boolean;
857
905
  /** 응답 본문(파싱된 것)으로부터 에러를 만든다. */
858
- static fromBody(status: number, body: unknown): OnequeError;
906
+ static fromBody(status: number, body: unknown): ZalkeraError;
907
+ }
908
+
909
+ /**
910
+ * 섹션 어휘 계약 — **정본의 코드 표현**(memo102 §6).
911
+ *
912
+ * 정본은 백엔드 레포의 `doc/contracts/section-vocabulary.json` 이고, 이 파일은 그것을 npm 으로
913
+ * 실어 나르는 **운반체**다. 템플릿 렌더러와 콘솔 zod 스키마가 이 상수를 읽어 자기 커버리지를
914
+ * 기계로 검사한다 — 사본이 갈라진 채 조용히 굳는 것을 막는 게 목적이지, 실시간 동일성이 목적은
915
+ * 아니다(계약이 원래 스큐 내성으로 설계돼 있다: 미지 타입은 스킵).
916
+ *
917
+ * 두 레포가 갈라져 있어 상호 CI 강제가 불가능하므로 **사람 이음새가 정확히 한 곳** 남는다 —
918
+ * 백엔드 JSON ↔ 이 파일. [SECTION_CONTRACT_REV] 를 백엔드 스펙의 `contractRev` 와 맞춰 두고,
919
+ * client 발행 전 `scripts/sync-section-contract.mjs` 로 대조한다.
920
+ */
921
+ /** 백엔드 스펙 `contractRev` 와 같아야 한다. 어긋나면 동기 스크립트가 잡는다. */
922
+ declare const SECTION_CONTRACT_REV = 1;
923
+ /** 업종 분류 — 콘솔 섹션 픽커의 그룹핑에 쓴다(테넌트 업종 필드는 아직 없다·memo102 §5). */
924
+ type SectionVertical = "BEAUTY" | "GENERAL";
925
+ interface SectionSpec {
926
+ readonly type: string;
927
+ readonly vertical: SectionVertical;
928
+ /** 이 섹션이 산출하는 schema.org 타입. null 이면 구조화 데이터 없음. */
929
+ readonly jsonLd: string | null;
859
930
  }
931
+ /**
932
+ * 아는 섹션 전량. **순서는 콘솔 픽커의 노출 순서**(관행 아크: 주목→가치→신뢰→행동).
933
+ * 값 추가는 백엔드 스펙을 먼저 고친 뒤 여기로 옮긴다.
934
+ */
935
+ declare const SECTION_CONTRACT: readonly [{
936
+ readonly type: "SERVICE_MENU";
937
+ readonly vertical: "BEAUTY";
938
+ readonly jsonLd: null;
939
+ }, {
940
+ readonly type: "BEFORE_AFTER_GALLERY";
941
+ readonly vertical: "BEAUTY";
942
+ readonly jsonLd: null;
943
+ }, {
944
+ readonly type: "BOOKING_CTA";
945
+ readonly vertical: "BEAUTY";
946
+ readonly jsonLd: null;
947
+ }, {
948
+ readonly type: "DOCTOR_INTRO";
949
+ readonly vertical: "BEAUTY";
950
+ readonly jsonLd: null;
951
+ }, {
952
+ readonly type: "HERO";
953
+ readonly vertical: "GENERAL";
954
+ readonly jsonLd: null;
955
+ }, {
956
+ readonly type: "FEATURE_GRID";
957
+ readonly vertical: "GENERAL";
958
+ readonly jsonLd: null;
959
+ }, {
960
+ readonly type: "TEXT_MEDIA";
961
+ readonly vertical: "GENERAL";
962
+ readonly jsonLd: null;
963
+ }, {
964
+ readonly type: "LOGO_WALL";
965
+ readonly vertical: "GENERAL";
966
+ readonly jsonLd: null;
967
+ }, {
968
+ readonly type: "STATS_BAND";
969
+ readonly vertical: "GENERAL";
970
+ readonly jsonLd: null;
971
+ }, {
972
+ readonly type: "TESTIMONIALS";
973
+ readonly vertical: "GENERAL";
974
+ readonly jsonLd: null;
975
+ }, {
976
+ readonly type: "FAQ_LIST";
977
+ readonly vertical: "GENERAL";
978
+ readonly jsonLd: "FAQPage";
979
+ }, {
980
+ readonly type: "LEAD_CTA";
981
+ readonly vertical: "GENERAL";
982
+ readonly jsonLd: null;
983
+ }];
984
+ /** 지금 렌더러가 아는 섹션 타입. 이 밖의 값이 와도 정상이다(스킵). */
985
+ type KnownSectionType = (typeof SECTION_CONTRACT)[number]["type"];
986
+ /** 업종별 필터 — 콘솔 픽커 그룹핑용. */
987
+ declare function sectionsOfVertical(vertical: SectionVertical): readonly SectionSpec[];
860
988
 
861
- export { type ApiErrorBody, type AuthTokens, type AvailabilityParams, type AvailabilitySlot, type Booking, type BookingStatus, type BusinessType, type Cart, type CartLine, type Category, type CheckoutInput, type ConsentInput, type ConsentStatus, type ConsentType, type CreateBookingInput, type CreateReviewInput, type CustomerSummary, type InquiryCreated, type InquiryInput, type KnownSectionType, type LeadCreated, type LeadInput, type LeadTracking, type ListPostsParams, type ListProductsParams, type ListReviewsParams, type MediaUrl, type Menu, type MenuPosition, type OnequeClient, type OnequeClientOptions, OnequeError, type OrderAccess, type OrderDetail, type OrderHistoryEntry, type OrderItemLine, type OrderStatus, type OrderSummary, type PageContent, type PageSection, type Paginated, type PaymentSession, type PostDetail, type PostSummary, type ProductCategory, type ProductDetail, type ProductSummary, type ProductType, type ProductVariant, type RatingSummary, type ReadOptions, type RequestContext, type Review, type ShipToInput, type ShipmentEvent, type ShipmentInfo, type ShipmentStatus, type ShopSession, type SiteConfig, type SocialLoginInput, type SocialProvider, type ValidationError, createOnequeClient };
989
+ export { type ApiErrorBody, type AuthTokens, type AvailabilityParams, type AvailabilitySlot, type Booking, type BookingStatus, type BusinessType, type Cart, type CartLine, type Category, type CheckoutInput, type ConsentInput, type ConsentStatus, type ConsentType, type CreateBookingInput, type CreateReviewInput, type CustomerSummary, type InquiryCreated, type InquiryInput, type KnownSectionType, type LeadCreated, type LeadInput, type LeadTracking, type ListPagesParams, type ListPostsParams, type ListProductsParams, type ListReviewsParams, type MediaUrl, type Menu, type MenuPosition, type OrderAccess, type OrderDetail, type OrderHistoryEntry, type OrderItemLine, type OrderStatus, type OrderSummary, type PageContent, type PageSection, type PageSummary, type Paginated, type PaymentSession, type PostDetail, type PostSummary, type ProductCategory, type ProductDetail, type ProductSummary, type ProductType, type ProductVariant, type RatingSummary, type ReadOptions, type RequestContext, type Review, SECTION_CONTRACT, SECTION_CONTRACT_REV, type SectionSpec, type SectionVertical, type ShipToInput, type ShipmentEvent, type ShipmentInfo, type ShipmentStatus, type ShopSession, type SiteConfig, type SocialLoginInput, type SocialProvider, type ValidationError, type ZalkeraClient, type ZalkeraClientOptions, ZalkeraError, createZalkeraClient, sectionsOfVertical };