@roottale/cms-client 0.69.0 → 0.70.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # @roottale/cms-client
2
2
 
3
+ ## 0.70.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 2d66028: API·DB 장애 때 고객 사이트가 멈추지 않도록 조회 대기 시간에 상한을 둡니다. `createRedirectMiddleware`는 규칙 조회를 기본 1.5초(`timeoutMs`)까지만 기다리고, 기존 규칙이 있으면 갱신을 기다리지 않고 뒤에서 한 번만 갱신하며, 실패 뒤 30초 동안 다시 조회하지 않습니다. cms-client 서버 조회는 호출부가 `signal`을 주지 않으면 10초 제한 시간을 겁니다. 기존 export·응답 형식·캐시 태그는 바뀌지 않습니다.
8
+ - @roottale/cms-core@0.70.1
9
+
10
+ ## 0.70.0
11
+
12
+ ### Patch Changes
13
+
14
+ - Updated dependencies [49145e4]
15
+ - @roottale/cms-core@0.70.0
16
+
3
17
  ## 0.69.0
4
18
 
5
19
  ### Minor Changes
package/dist/server.js CHANGED
@@ -4,7 +4,11 @@ import {
4
4
 
5
5
  // src/server-base.ts
6
6
  var DEFAULT_BASE_URL = "https://api.roottale.com";
7
- var CLIENT_VERSION = true ? "0.69.0" : "dev";
7
+ var DEFAULT_SERVER_FETCH_TIMEOUT_MS = 1e4;
8
+ function requestSignal(signal) {
9
+ return signal ?? AbortSignal.timeout(DEFAULT_SERVER_FETCH_TIMEOUT_MS);
10
+ }
11
+ var CLIENT_VERSION = true ? "0.70.1" : "dev";
8
12
  var CMS_CLIENT_VERSION = CLIENT_VERSION;
9
13
  function authHeaders(apiKey) {
10
14
  return {
@@ -16,7 +20,7 @@ function buildGetInit(apiKey, opts) {
16
20
  const init = {
17
21
  method: "GET",
18
22
  headers: authHeaders(apiKey),
19
- signal: opts.signal
23
+ signal: requestSignal(opts.signal)
20
24
  };
21
25
  if (opts.revalidate !== void 0 || opts.tags && opts.tags.length > 0) {
22
26
  init.next = {
@@ -870,7 +874,7 @@ async function fetchExposureDecision(options) {
870
874
  const response = await fetch(`${base}/v1/cms/public/exposure-decisions?${params}`, {
871
875
  headers: authHeaders(options.apiKey),
872
876
  cache: "no-store",
873
- signal: options.signal
877
+ signal: requestSignal(options.signal)
874
878
  });
875
879
  if (!response.ok) throw await toApiError(response);
876
880
  const payload = object(await response.json());
@@ -984,7 +988,7 @@ async function fetchInquiryForm(options) {
984
988
  headers: authHeaders(options.apiKey),
985
989
  cache: "no-store",
986
990
  redirect: "manual",
987
- signal: options.signal
991
+ signal: requestSignal(options.signal)
988
992
  });
989
993
  if (response.status === 404) return null;
990
994
  if (!response.ok) throw await toApiError(response);
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/server-base.ts","../src/server-post-wire.ts","../src/server-posts-theme.ts","../src/server-blog.ts","../src/server-categories.ts","../src/server-business.ts","../src/server-discovery.ts","../src/server-exposure.ts","../src/server-exposure-decision.ts","../src/server-site-inquiries.ts","../src/server-inquiry-forms.ts","../src/server-preview.ts","../src/server-patterns.ts","../src/server-metadata.ts","../src/metadata-contract-checksum.ts","../src/server-cache-tags.ts","../src/seo.ts","../src/inquiry.ts"],"sourcesContent":["/**\n * `@roottale/cms-client/server` — RootTale CMS Public API server-side fetch client.\n *\n * 외부 고객사 사이트가 RootTale CMS 콘텐츠를 fetch 하기 위한 SSR-only client.\n * Bearer (`rtlk_cust_*`) 인증, 브라우저 직접 호출 금지 (ADR-0023 §5.1 #15).\n *\n * 사용 예 (Next.js server component):\n * ```ts\n * import { fetchPosts } from \"@roottale/cms-client/server\";\n *\n * const posts = await fetchPosts({\n * apiKey: process.env.ROOTTALE_API_KEY!,\n * limit: 10,\n * });\n * ```\n *\n * 데이터 모델 = ADR-0034 `posts` (WP 7.0-aligned, type=post|page).\n * Phase 1 scope: list + get. WP import/export, draft preview = 별도 PR.\n */\n\nexport type CmsPostType = \"post\" | \"page\";\n\nexport type CmsTaxonomyKind = \"category\" | \"tag\" | \"custom\";\n\nexport interface CmsTermRef {\n id: string;\n taxonomy: CmsTaxonomyKind;\n slug: string;\n name: string;\n termOrder?: number;\n parentId?: string | null;\n}\n\nexport interface CmsArchiveRelatedPost {\n id: string;\n title: string;\n slug: string;\n excerpt: string | null;\n collectionKey: string;\n}\n\n/** 관련 콘텐츠 1건 — 카드 하나를 그릴 만큼(제목·주소·유형·요약·대표이미지). */\nexport interface CmsRelatedPost {\n id: string;\n title: string;\n slug: string;\n /** 저장된 정규 공개 경로(ADR-0105). 상세 주소가 없는 유형이면 null. */\n path: string | null;\n type: string;\n modelKey: string | null;\n collectionKey: string | null;\n excerpt: string | null;\n featuredImageUrl: string | null;\n /** ISO 8601 | null. */\n publishedAt: string | null;\n}\n\nexport interface CmsArchiveRelatedGroup {\n collectionKey: string;\n label: string;\n items: CmsArchiveRelatedPost[];\n}\n\n/** W5-2 — `fields` 표시 메타 1건 (서버 계약 `CmsFieldMeta` 대응). */\nexport interface CmsFieldMetaEntry {\n /** `fields` 의 key. */\n name: string;\n /** 필드 그룹에 정의된 사람용 라벨. */\n label: string;\n /** 필드 타입 (text/textarea/select/relationship/…). */\n type: string;\n /** select/radio 계열만 — 저장 값(value)→표시 라벨. */\n choices?: Array<{ value: string; label: string }>;\n}\n\n/** ADR-0052 A안 (W4-6 PR B) — 언어판 참조(locale + 그 언어판의 slug). */\nexport interface CmsPostTranslationRef {\n locale: string;\n slug: string;\n}\n\nexport interface CmsPostContent {\n /** UUIDv7. */\n id: string;\n /** Owning tenant id. */\n tenantId: string;\n /** Owning site id (ADR-0034 P4 dual-key). */\n siteId: string;\n /** WP post type. */\n type: CmsPostType;\n /**\n * ADR-0060 Amendment 1 — 글의 명시 섹션(content collection) key. 라우팅/스트림\n * 판정에 카테고리 파생보다 우선. 구 서버/미설정이면 null.\n */\n collectionKey: string | null;\n /** 페이지·글·정보를 가리키는 안정적인 모델 key. 구 서버에서는 null. */\n modelKey?: string | null;\n /** Lowercase kebab-case slug (1-120 chars). */\n slug: string;\n /**\n * ADR-0052 A안 — 이 글판의 BCP-47 locale. 단일 언어 사이트는 항상 'ko'.\n * 구 서버(마이그레이션 전) 는 미포함 — optional (하위호환).\n */\n locale?: string;\n /**\n * ADR-0052 A안 — 같은 글의 발행된 언어판 목록(이 글 자신 포함). 번역이 없으면\n * 자기 자신 1건만. 구 서버는 미포함 — optional (하위호환).\n */\n translations?: CmsPostTranslationRef[];\n /**\n * 이 글이 예전에 썼던 slug 목록(최신순). 주소 이동 뒤에도 옛 경로에서 파생한\n * 본문 예약 내부 링크 키(`[[internal:…]]`)를 현재 글로 해석할 때 옛 경로도 같은\n * 글로 등록하는 데 쓴다. 구 서버는 미포함 — optional.\n */\n previousSlugs?: string[];\n /**\n * ADR-0105 — 저장된 정규 공개 경로(origin 제외, 예 `/column/my-post`). 링크·사이트맵·\n * 내부 링크 키에 그대로 쓴다. 상세 주소가 없는 글은 null, 구 서버는 미포함.\n */\n path?: string | null;\n title: string;\n excerpt: string | null;\n /** Media id (resolved by renderer / consumer to URL). */\n featuredMediaId: string | null;\n /**\n * featured 이미지 delivery URL (CF Images variant, md 우선). 서버가 `featured_media_id`\n * → `media.variants_json` 를 해석해 제공. 대표 이미지 미설정 시 null.\n */\n featuredImageUrl: string | null;\n /** 사이트 공통 공개 작성자 ID. 구 서버에서는 null. */\n authorProfileId?: string | null;\n /** @deprecated Use authorProfileId. */\n authorId: string | null;\n /** Public display name for the post author. Email is not exposed. */\n authorName: string | null;\n /** Public profile image URL for the post author. */\n authorImageUrl: string | null;\n /** 작성자 사진의 좌우 초점(0~100). 구 서버·레거시 작성자는 null. */\n authorImagePositionX?: number | null;\n /** 작성자 사진의 상하 초점(0~100). 구 서버·레거시 작성자는 null. */\n authorImagePositionY?: number | null;\n /** Public biography text for the post author. */\n authorBio: string | null;\n /**\n * Author archive slug (`/blog/author/{slug}`). null when the author has no\n * assigned slug. 구 서버는 미포함 — 매핑부에서 `?? null` 기본값.\n */\n authorSlug: string | null;\n /** Tiptap / Gutenberg-compatible Block JSON. Sanitization is renderer's responsibility. */\n bodyJson: Record<string, unknown>;\n /**\n * 원문 HTML 정본 — 외부에서 이관된 글처럼 HTML 이 정본인 경우에만 채워진다.\n * 렌더러는 값이 있으면 sanitize 후 우선하고 없으면(null) body_json 으로\n * 폴백한다. 구 서버는 미포함 — 매핑부에서 `?? null` 기본값.\n * Sanitization 책임은 body_json 과 동일하게 렌더러에 있다.\n *\n * `?:` 인 이유 — 이 필드는 순수 가산분이라 기존 소비자가 모르고 지나가도\n * 종전과 동일하게 동작해야 한다(값이 없으면 body_json 폴백). optional 로 두면\n * 외부 사이트의 기존 객체 리터럴도 그대로 타입을 만족한다.\n */\n bodyHtml?: string | null;\n /** Public API only exposes `published`. */\n status: \"published\";\n /** Per-post meta (SEO overrides, canonical, og, ...). */\n metaJson: Record<string, unknown>;\n /**\n * ADR-0041 — 포맷된 커스텀 필드(ACF) 값. 서버가 필드 정의(코드 ⊕ DB 그룹)로\n * `meta_json.acf` 를 포맷 — image/file/gallery 는 media variant URL 객체.\n * 해석된 그룹이 없거나 구 서버면 null.\n */\n fields: Record<string, unknown> | null;\n /**\n * W5-2 — `fields` 의 표시 메타(라벨·타입·choices), 필드 그룹 정의 순서.\n * 서버가 필드 정의로 계산해 동봉 — 렌더러가 vertical 지식 없이 양식 값을\n * 라벨과 함께 표시한다. 해석된 그룹 없거나 구 서버면 빈 배열.\n */\n fieldsMeta: CmsFieldMetaEntry[];\n /**\n * Attached taxonomy terms (categories + tags + custom). Server 가 category →\n * tag → custom 순서로 정렬해 보내며, 각 taxonomy 내부는 name asc. attach 가 0 개면\n * 빈 배열. category 만 추출하려면 `terms.filter(t => t.taxonomy === \"category\")`.\n */\n terms: CmsTermRef[];\n archiveRelated: CmsArchiveRelatedGroup[];\n /**\n * ADR-0065 — 누적 조회수. 어드민에서 \"공개 조회수\"를 켠 사이트에서만 내려온다\n * (꺼져 있으면 `undefined`). \"조회 N\" 표시에 사용. 비콘이 채우므로 신규 글은 0.\n */\n viewCount?: number;\n /**\n * 공통 블록 자리별 블록 key(자리 key → `fetchSitePatterns` 의 블록 key | null). 서버가\n * 어드민 배치 규칙을 이 글의 유형에 맞춰 계산한 결과라 사이트는 규칙을 몰라도 된다.\n * 선언된 자리(예: `post_footer`)는 항상 키로 존재하고 놓을 블록이 없으면 null.\n * 구 서버는 미포함 — 매핑부에서 빈 객체.\n */\n patternSlots?: Record<string, string | null>;\n /**\n * 관련 콘텐츠 — 편집자가 어드민 \"관련 콘텐츠\"에서 고른 글(유형 무관), 고른 순서, 발행 글만.\n * 목록·상세·미리보기 모두 내려온다. 구 서버는 빈 배열(매핑부). 비었을 때\n * \"같은 분류 최신 글\" 자동 추천은 `selectRelatedPosts` 로 사이트가 채운다.\n */\n relatedPosts?: CmsRelatedPost[];\n /** ISO 8601. */\n publishedAt: string;\n /** ISO 8601. */\n createdAt: string;\n /** ISO 8601. */\n updatedAt: string;\n}\n\nexport interface CmsPostContentPage {\n items: CmsPostContent[];\n nextCursor: string | null;\n hasMore: boolean;\n}\n\nexport const DEFAULT_BASE_URL = \"https://api.roottale.com\";\n\n// fleet 가시성 — 자기 버전을 자가 보고 (build-time 주입, tsup define).\n// api-core 가 사이트별 check-in 으로 기록 → internal `GET /v1/internal/sites`.\ndeclare const __CMS_CLIENT_VERSION__: string | undefined;\nconst CLIENT_VERSION =\n typeof __CMS_CLIENT_VERSION__ === \"string\" ? __CMS_CLIENT_VERSION__ : \"dev\";\n\n/** 설치된 cms-client 버전 (build-time 주입) — fleet well-known 라우트 등에서 사용. */\nexport const CMS_CLIENT_VERSION = CLIENT_VERSION;\n\nexport function authHeaders(apiKey: string): Record<string, string> {\n return {\n authorization: `Bearer ${apiKey}`,\n \"x-roottale-client\": `cms-client/${CLIENT_VERSION}`,\n };\n}\n\n/**\n * ISR 시간 기반 fallback(W0-9). 지정 시 fetch 에 Next 의 `next: { revalidate }`\n * 를 붙여 webhook 유실 시에도 이 주기(초) 내에 캐시가 자가 치유된다.\n * **미지정(undefined)이면 아무 것도 붙이지 않아 현행 동작을 그대로 유지**(파괴적\n * 변경 없음) — Next 기본 캐시/정적화 정책을 따른다. `0` 은 항상 재검증(동적).\n */\nexport interface RevalidatableFetchOptions {\n /** Optional AbortSignal. */\n signal?: AbortSignal;\n /**\n * ISR revalidate 초. opt-in — 미지정 시 현행 캐시 동작 유지.\n * (Next.js `next: { revalidate }` 로 전달.)\n */\n revalidate?: number;\n}\n\n/** Next `fetch` 확장 — `next.revalidate`/`next.tags` 지원(런타임 무해, non-Next 무시). */\ntype NextFetchInit = RequestInit & {\n next?: { revalidate?: number; tags?: string[] };\n};\n\n/**\n * GET fetch 용 RequestInit 빌더 — Bearer 헤더 + signal + opt-in ISR revalidate\n * + opt-in cache tags. cms-client 의 모든 공개 콘텐츠/테마 fetch 가 공유해\n * `next.revalidate`/`next.tags` 지원을 일관되게 얻는다.\n *\n * `tags` 는 on-demand 무효화 경로(W0-4): 지정 시 fetch 캐시 엔트리에 tag 를\n * 붙여, 수신 route 의 `revalidateTag(tag)` 가 해당 엔트리(+그 fetch 를 쓴 route)를\n * 무효화한다. 미지정 = 붙이지 않음(현행 동작 유지).\n */\nexport function buildGetInit(\n apiKey: string,\n opts: { signal?: AbortSignal; revalidate?: number; tags?: string[] },\n): NextFetchInit {\n const init: NextFetchInit = {\n method: \"GET\",\n headers: authHeaders(apiKey),\n signal: opts.signal,\n };\n if (opts.revalidate !== undefined || (opts.tags && opts.tags.length > 0)) {\n init.next = {\n ...(opts.revalidate !== undefined ? { revalidate: opts.revalidate } : {}),\n ...(opts.tags && opts.tags.length > 0 ? { tags: opts.tags } : {}),\n };\n }\n return init;\n}\n\nexport interface FetchPostsOptions {\n /** `rtlk_cust_*` API key. Required. */\n apiKey: string;\n /** Override the API base URL. Defaults to `https://api.roottale.com`. */\n baseUrl?: string;\n /** Page size, 1-100, default 20. */\n limit?: number;\n /** Opaque pagination cursor returned from previous call. */\n cursor?: string;\n /** Filter by post type (`post` or `page`). Default = both. */\n type?: CmsPostType;\n /** 작가 slug 필터 — 작가 아카이브(/blog/author/{slug})용. 지정 시 발행 글(post)만. */\n author?: string;\n /** Override the auto-resolved site id (Phase 1 = 1 site/tenant, usually omitted). */\n siteId?: string;\n /**\n * ADR-0060 — 특정 섹션(collection) 소속 글만(포지티브 스코프). `excludeCollections`\n * 와 동시 지정 불가(서버가 400).\n */\n collectionKey?: string;\n /** 특정 콘텐츠 모델의 항목만 조회한다. */\n modelKey?: string;\n /**\n * 네거티브 스코프 — 어느 스트림에도 안 속한(\"미소속\") 글만\n * (`blog-list-collection-scope.md` §2②). `collectionKey` 와 동시 지정 불가.\n */\n excludeCollections?: boolean;\n /**\n * ADR-0052 A안 (W4-6 PR B) — BCP-47 소문자 locale 필터(예: \"en\"). 미지정 시\n * 서버가 사이트 기본 locale(`settingsJson.locales[0]`, 미설정이면 'ko') 만\n * 반환한다 — 하위호환(번역판이 생겨도 이 옵션을 안 쓰는 기존 호출부는 원어\n * 콘텐츠만 계속 받는다).\n */\n locale?: string;\n /** Optional AbortSignal. */\n signal?: AbortSignal;\n /** ISR revalidate 초(opt-in, W0-9). 미지정 시 현행 동작 유지. */\n revalidate?: number;\n /** Next fetch cache tags. Use CONTENT_POLICY_CACHE_TAG for declared policy facts. */\n tags?: string[];\n}\n\nexport interface FetchPostOptions {\n apiKey: string;\n baseUrl?: string;\n /** Slug (kebab-case) or UUIDv7 id. */\n slugOrId: string;\n /** Override the auto-resolved site id. */\n siteId?: string;\n /** ADR-0052 A안 — `FetchPostsOptions.locale` 과 동일 시맨틱. */\n locale?: string;\n signal?: AbortSignal;\n /** ISR revalidate 초(opt-in, W0-9). 미지정 시 현행 동작 유지. */\n revalidate?: number;\n /** Next fetch cache tags. Use CONTENT_POLICY_CACHE_TAG for declared policy facts. */\n tags?: string[];\n}\n\n\nexport class CmsApiError extends Error {\n readonly status: number;\n readonly code: string;\n constructor(message: string, status: number, code: string) {\n super(message);\n this.name = \"CmsApiError\";\n this.status = status;\n this.code = code;\n }\n}\n\nexport async function toApiError(response: Response): Promise<CmsApiError> {\n let code = \"request_failed\";\n let message = `RootTale CMS API error: ${response.status}`;\n try {\n const body = (await response.json()) as { code?: string; message?: string };\n if (body.code) code = body.code;\n if (body.message) message = body.message;\n } catch {\n /* ignore body parse */\n }\n return new CmsApiError(message, response.status, code);\n}\n\nexport function assertServer(): void {\n if (typeof window !== \"undefined\") {\n throw new Error(\n \"@roottale/cms-client/server must not run in the browser. \" +\n \"Move this call to a Server Component, getServerSideProps, or a Route Handler. \" +\n \"Shipping API keys to the browser breaks the security model (ADR-0023 §5.1 #15).\",\n );\n }\n}\n\n// 프레임워크 무관 SEO 헬퍼 (RSS 2.0 + Schema.org JSON-LD) 재노출.\n","import type { CmsPostContent, CmsPostType, CmsRelatedPost, CmsTermRef } from \"./server-base\";\n\n// 공개 API 글 응답(wire) → CmsPostContent 변환. server-posts-theme 와 server-preview 가\n// 함께 쓰는 내부 모듈 — 공개 배럴(server.ts)에서 재노출하지 않는다.\n\nexport type CmsPostWire = {\n id: string;\n tenant_id: string;\n site_id: string;\n type: CmsPostType;\n /** ADR-0060 Amendment 1 — 명시 섹션 key. 구 서버는 미포함 — optional. */\n collection_key?: string | null;\n /** 신규 모델 key. 구 서버는 미포함 — optional. */\n model_key?: string | null;\n slug: string;\n /** ADR-0052 A안 — 구 서버는 미포함 — optional(하위호환). */\n locale?: string;\n /** ADR-0052 A안 — 구 서버는 미포함 — optional(하위호환). */\n translations?: Array<{ locale: string; slug: string }>;\n /** 옛 slug 목록(최신순). 구 서버는 미포함 — optional. */\n previous_slugs?: string[];\n /** 저장된 정규 공개 경로. 구 서버는 미포함 — optional. */\n path?: string | null;\n title: string;\n excerpt: string | null;\n featured_media_id: string | null;\n /** featured 이미지 delivery URL. 구 서버는 미포함 — backward-compat optional. */\n featured_media_url?: string | null;\n author_id: string | null;\n author_profile_id?: string | null;\n author_name?: string | null;\n author_image_url?: string | null;\n author_image_position_x?: number | null;\n author_image_position_y?: number | null;\n author_bio?: string | null;\n author_slug?: string | null;\n body_json: Record<string, unknown>;\n /**\n * 원문 HTML 정본. 구 서버는 미포함 — backward-compat optional.\n * 렌더러가 sanitize 후 우선 사용하고, 없으면 body_json 으로 폴백한다.\n */\n body_html?: string | null;\n status: \"published\";\n meta_json: Record<string, unknown>;\n /** ADR-0041 포맷 필드. 구 서버는 미포함 — backward-compat optional. */\n fields?: Record<string, unknown> | null;\n /** 0.2.0 이전 서버는 미포함 — backward-compat 차원에서 optional. */\n terms?: Array<CmsTermRef & { term_order?: number; parent_id?: string | null }>;\n archive_related?: Array<{\n collection_key: string;\n label: string;\n items: Array<{\n id: string;\n title: string;\n slug: string;\n excerpt: string | null;\n collection_key: string;\n }>;\n }>;\n /** W5-2 — fields 표시 메타. 해석된 그룹 없거나 구 서버는 미포함. */\n fields_meta?: Array<{\n name: string;\n label: string;\n type: string;\n choices?: Array<{ value: string; label: string }>;\n }>;\n /** ADR-0065 — 누적 조회수. 사이트가 공개 조회수를 켠 경우에만 포함. */\n view_count?: number;\n /** 공통 블록 자리별 블록 key. 구 서버는 미포함 — optional. */\n pattern_slots?: Record<string, string | null>;\n /** 관련 콘텐츠(목록·상세·미리보기). 구 서버는 미포함 — optional. */\n related_posts?: Array<{\n id: string;\n title: string;\n slug: string;\n path: string | null;\n type: string;\n model_key: string | null;\n collection_key: string | null;\n excerpt: string | null;\n featured_media_url: string | null;\n published_at: string | null;\n }>;\n published_at: string;\n created_at: string;\n updated_at: string;\n};\n\n/** `pattern_slots` → 자리별 블록 key. 형식이 틀린 항목은 버리고 구버전 응답은 빈 객체. */\nfunction patternSlotsFromWire(raw: unknown): Record<string, string | null> {\n if (!raw || typeof raw !== \"object\" || Array.isArray(raw)) return {};\n const out: Record<string, string | null> = {};\n for (const [slot, key] of Object.entries(raw as Record<string, unknown>)) {\n if (key === null || typeof key === \"string\") out[slot] = key;\n }\n return out;\n}\n\n/** `related_posts` → 관련 콘텐츠. 형식이 틀린 항목은 버리고 구버전 응답은 빈 배열. */\nfunction relatedPostsFromWire(raw: unknown): CmsRelatedPost[] {\n if (!Array.isArray(raw)) return [];\n const out: CmsRelatedPost[] = [];\n for (const item of raw) {\n if (!item || typeof item !== \"object\") continue;\n const rec = item as Record<string, unknown>;\n if (typeof rec.id !== \"string\" || typeof rec.title !== \"string\" || typeof rec.slug !== \"string\") continue;\n out.push({\n id: rec.id,\n title: rec.title,\n slug: rec.slug,\n path: typeof rec.path === \"string\" ? rec.path : null,\n type: typeof rec.type === \"string\" ? rec.type : \"post\",\n modelKey: typeof rec.model_key === \"string\" ? rec.model_key : null,\n collectionKey: typeof rec.collection_key === \"string\" ? rec.collection_key : null,\n excerpt: typeof rec.excerpt === \"string\" ? rec.excerpt : null,\n featuredImageUrl: typeof rec.featured_media_url === \"string\" ? rec.featured_media_url : null,\n publishedAt: typeof rec.published_at === \"string\" ? rec.published_at : null,\n });\n }\n return out;\n}\n\nexport function postFromWire(wire: CmsPostWire): CmsPostContent {\n const imagePosition = (value: unknown): number | null =>\n typeof value === \"number\" && Number.isInteger(value) && value >= 0 && value <= 100\n ? value\n : null;\n return {\n id: wire.id,\n tenantId: wire.tenant_id,\n siteId: wire.site_id,\n type: wire.type,\n collectionKey: wire.collection_key ?? null,\n modelKey: wire.model_key ?? null,\n slug: wire.slug,\n ...(wire.locale !== undefined ? { locale: wire.locale } : {}),\n ...(wire.translations !== undefined ? { translations: wire.translations } : {}),\n ...(wire.previous_slugs !== undefined ? { previousSlugs: wire.previous_slugs } : {}),\n ...(wire.path !== undefined ? { path: wire.path } : {}),\n title: wire.title,\n excerpt: wire.excerpt,\n featuredMediaId: wire.featured_media_id,\n featuredImageUrl: wire.featured_media_url ?? null,\n authorProfileId: wire.author_profile_id ?? null,\n authorId: wire.author_id,\n authorName: wire.author_name ?? null,\n authorImageUrl: wire.author_image_url ?? null,\n authorImagePositionX: imagePosition(wire.author_image_position_x),\n authorImagePositionY: imagePosition(wire.author_image_position_y),\n authorBio: wire.author_bio ?? null,\n authorSlug: wire.author_slug ?? null,\n bodyJson: wire.body_json,\n // 구 서버(필드 없음) → null → body_json 폴백 = 종전 동작 그대로.\n bodyHtml: wire.body_html ?? null,\n status: wire.status,\n metaJson: wire.meta_json,\n fields: wire.fields ?? null,\n fieldsMeta: (wire.fields_meta ?? []).map((entry) => ({\n name: entry.name,\n label: entry.label,\n type: entry.type,\n ...(entry.choices\n ? {\n choices: entry.choices.map((c) => ({\n value: c.value,\n label: c.label,\n })),\n }\n : {}),\n })),\n terms: (wire.terms ?? []).map(({ term_order, parent_id, ...term }) => ({\n ...term,\n ...(term_order !== undefined ? { termOrder: term_order } : {}),\n ...(parent_id !== undefined ? { parentId: parent_id } : {}),\n })),\n archiveRelated: (wire.archive_related ?? []).map((group) => ({\n collectionKey: group.collection_key,\n label: group.label,\n items: group.items.map((item) => ({\n id: item.id,\n title: item.title,\n slug: item.slug,\n excerpt: item.excerpt,\n collectionKey: item.collection_key,\n })),\n })),\n ...(typeof wire.view_count === \"number\" ? { viewCount: wire.view_count } : {}),\n patternSlots: patternSlotsFromWire(wire.pattern_slots),\n relatedPosts: relatedPostsFromWire(wire.related_posts),\n publishedAt: wire.published_at,\n createdAt: wire.created_at,\n updatedAt: wire.updated_at,\n };\n}\n","import {\n type CmsPostContent,\n type CmsPostContentPage,\n type FetchPostOptions,\n type FetchPostsOptions,\n DEFAULT_BASE_URL,\n assertServer,\n buildGetInit,\n toApiError,\n} from \"./server-base\";\nimport { type CmsPostWire, postFromWire } from \"./server-post-wire\";\n\n/**\n * 외부 사이트가 cms-renderer 에 주입할 RootTale 디자인 토큰.\n * admin /design (admin.roottale.com) 에서 저장한 값과 동일한 shape.\n * 미설정 필드는 응답에서 누락 → renderer 의 CSS fallback 이 사용된다.\n */\nexport interface RootTaleTheme {\n colors?: {\n primary?: string;\n primaryForeground?: string;\n foreground?: string;\n background?: string;\n secondary?: string;\n secondaryForeground?: string;\n accent?: string;\n accentForeground?: string;\n muted?: string;\n mutedForeground?: string;\n destructive?: string;\n destructiveForeground?: string;\n border?: string;\n input?: string;\n ring?: string;\n };\n fonts?: {\n body?: string;\n display?: string;\n };\n radius?: {\n sm?: string;\n md?: string;\n lg?: string;\n xl?: string;\n };\n /**\n * W6-3 — `sites.name`(DB SSoT, `rt init --name` 이 저장) 미러. 외부 사이트가\n * `<title>`/OG/헤더·푸터 브랜드명을 site.config.ts 정적 리터럴 대신 런타임\n * 반영하는 데 쓴다. 미설정이면 소비자가 site.config.ts 값으로 fallback.\n */\n siteName?: string;\n announcement?: AnnouncementSettings;\n actionDock?: ActionDockSettings;\n inquiryBoard?: InquiryBoardSettings;\n siteNav?: SiteNavSettings;\n /**\n * ROOT-ADMIN 연동 설정에 입력한 검색엔진 소유 확인 코드. `siteVerificationMetadata`\n * 로 Next metadata `verification` 모양으로 바꿔 루트 layout 에 출력한다.\n */\n siteVerification?: SiteVerificationSettings;\n}\n\n/** 엔진별 소유 확인 코드(고객·RootTale 계정이 각각 확인할 수 있어 여러 개). */\nexport interface SiteVerificationSettings {\n google?: string[];\n naver?: string[];\n}\n\nexport type AnnouncementVariant = \"single\" | \"rotating\" | \"cta\";\nexport type AnnouncementTone = \"default\" | \"primary\";\n\nexport interface AnnouncementItem {\n text: string;\n href?: string;\n ctaLabel?: string;\n}\n\nexport interface AnnouncementSettings {\n enabled: boolean;\n variant: AnnouncementVariant;\n tone: AnnouncementTone;\n items: AnnouncementItem[];\n startsAt?: string;\n endsAt?: string;\n}\n\nexport type ActionDockKind = \"tel\" | \"link\" | \"map\" | \"channel\";\nexport type ActionDockPosition = \"bottom\" | \"side\";\n\nexport interface ActionDockAction {\n label: string;\n href: string;\n kind: ActionDockKind;\n}\n\nexport interface ActionDockSettings {\n enabled: boolean;\n actions: ActionDockAction[];\n position: ActionDockPosition;\n collapseAt?: number;\n}\n\nexport interface InquiryBoardSettings {\n enabled: boolean;\n}\n\nexport interface SiteNavLink {\n label: string;\n href: string;\n description?: string;\n}\n\nexport interface SiteNavColumn {\n title?: string;\n links: SiteNavLink[];\n}\n\nexport interface SiteNavGroup {\n label: string;\n href?: string;\n columns?: SiteNavColumn[];\n}\n\nexport interface SiteNavCta {\n label: string;\n href: string;\n}\n\nexport interface SiteNavSettings {\n navGroups: SiteNavGroup[];\n cta?: SiteNavCta;\n utilityLinks?: SiteNavLink[];\n}\n\n/**\n * 테마 fetch 캐시 tag 의 정본 이름 (W0-4). starter layout 의 `fetchTheme` 호출에\n * `tags: [THEME_CACHE_TAG]` 로 부착하고, revalidate route 가 `theme.updated`\n * 수신 시 `revalidateTag(THEME_CACHE_TAG)` 로 무효화한다. 양쪽이 이 상수를 공유해\n * drift 를 방지한다.\n */\nexport const THEME_CACHE_TAG = \"rt-theme\";\n\nexport interface FetchThemeOptions {\n apiKey: string;\n baseUrl?: string;\n siteId?: string;\n signal?: AbortSignal;\n /** ISR revalidate 초(opt-in, W0-9). 미지정 시 현행 동작 유지. */\n revalidate?: number;\n /**\n * on-demand 무효화용 Next fetch cache tags(opt-in, W0-4). 보통\n * `[THEME_CACHE_TAG]`. 미지정 시 현행 동작 유지.\n */\n tags?: string[];\n}\n\n/**\n * tenant 의 RootTale 디자인 토큰을 가져온다. admin /design 의 마지막 저장값\n * (없으면 빈 객체) — renderer 가 이 값을 CSS 변수로 주입한다.\n *\n * 자동 fetch (renderer 가 theme prop 없을 때) 와 명시 호출 모두 지원.\n * 네트워크/서버 에러는 throw — 호출자가 fallback 처리.\n */\nexport async function fetchTheme(options: FetchThemeOptions): Promise<RootTaleTheme> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams();\n if (options.siteId) params.set(\"site_id\", options.siteId);\n const qs = params.toString();\n const url = `${baseUrl}/v1/cms/public/theme${qs ? `?${qs}` : \"\"}`;\n\n const response = await fetch(url, buildGetInit(options.apiKey, options));\n\n if (!response.ok) {\n throw await toApiError(response);\n }\n\n const json = (await response.json()) as {\n colors?: RootTaleTheme[\"colors\"];\n fonts?: RootTaleTheme[\"fonts\"];\n radius?: RootTaleTheme[\"radius\"];\n site_name?: string;\n announcement?: AnnouncementSettings;\n actionDock?: ActionDockSettings;\n inquiryBoard?: InquiryBoardSettings;\n siteNav?: SiteNavSettings;\n siteVerification?: SiteVerificationSettings;\n };\n const theme: RootTaleTheme = {};\n if (json.colors) theme.colors = json.colors;\n if (json.fonts) theme.fonts = json.fonts;\n if (json.radius) theme.radius = json.radius;\n if (json.site_name) theme.siteName = json.site_name;\n if (json.announcement) theme.announcement = json.announcement;\n if (json.actionDock) theme.actionDock = json.actionDock;\n if (json.inquiryBoard) theme.inquiryBoard = json.inquiryBoard;\n if (json.siteNav) theme.siteNav = json.siteNav;\n if (json.siteVerification) theme.siteVerification = json.siteVerification;\n return theme;\n}\n\nexport type AnalyticsTagProvider = \"ga4\" | \"clarity\" | \"meta_pixel\" | \"naver\";\n\n/** analytics-runtime `TagConfig` 와 구조 호환 — `buildManifest` 입력. */\nexport interface AnalyticsTagConfig {\n provider: AnalyticsTagProvider;\n id: string;\n enabled: boolean;\n}\n\nexport interface SiteAnalyticsTags {\n siteId: string;\n tags: AnalyticsTagConfig[];\n}\n\nexport interface FetchAnalyticsOptions {\n apiKey: string;\n baseUrl?: string;\n siteId?: string;\n signal?: AbortSignal;\n /** ISR revalidate 초(opt-in, W0-9). 미지정 시 현행 동작 유지. */\n revalidate?: number;\n}\n\n/**\n * ADR-0038 plane ① — 외부 사이트가 `<head>` 에 주입할 런타임 태그(GA4 measurement\n * id / Clarity project id 등, 비밀 아님). RootTale 관리용 default + 테넌트 고객용\n * 설정을 머지한 결과를 반환한다. analytics-runtime `buildManifest` 입력으로 쓰고,\n * 동의는 항상 fail-closed(런타임이 처리)이므로 키 노출 위험 없음(public id).\n */\nexport async function fetchAnalyticsConfig(\n options: FetchAnalyticsOptions,\n): Promise<SiteAnalyticsTags> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams();\n if (options.siteId) params.set(\"site_id\", options.siteId);\n const qs = params.toString();\n const url = `${baseUrl}/v1/cms/public/analytics${qs ? `?${qs}` : \"\"}`;\n\n const response = await fetch(url, buildGetInit(options.apiKey, options));\n\n if (!response.ok) {\n throw await toApiError(response);\n }\n\n const json = (await response.json()) as {\n site_id: string;\n tags?: AnalyticsTagConfig[];\n };\n return { siteId: json.site_id, tags: json.tags ?? [] };\n}\n\n\nexport async function fetchPosts(options: FetchPostsOptions): Promise<CmsPostContentPage> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams();\n if (options.limit !== undefined) params.set(\"limit\", String(options.limit));\n if (options.cursor) params.set(\"cursor\", options.cursor);\n if (options.type) params.set(\"type\", options.type);\n if (options.author) params.set(\"author\", options.author);\n if (options.siteId) params.set(\"site_id\", options.siteId);\n if (options.collectionKey) params.set(\"collection_key\", options.collectionKey);\n if (options.modelKey) params.set(\"model_key\", options.modelKey);\n if (options.excludeCollections) params.set(\"exclude_collections\", \"true\");\n if (options.locale) params.set(\"locale\", options.locale);\n const qs = params.toString();\n const url = `${baseUrl}/v1/cms/public/posts${qs ? `?${qs}` : \"\"}`;\n\n const response = await fetch(url, buildGetInit(options.apiKey, options));\n\n if (!response.ok) {\n throw await toApiError(response);\n }\n\n const json = (await response.json()) as {\n items: CmsPostWire[];\n has_more: boolean;\n next_cursor: string | null;\n };\n return {\n items: json.items.map(postFromWire),\n hasMore: json.has_more,\n nextCursor: json.next_cursor,\n };\n}\n\nexport async function fetchPost(options: FetchPostOptions): Promise<CmsPostContent | null> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams();\n if (options.siteId) params.set(\"site_id\", options.siteId);\n if (options.locale) params.set(\"locale\", options.locale);\n const qs = params.toString();\n const url = `${baseUrl}/v1/cms/public/posts/${encodeURIComponent(options.slugOrId)}${qs ? `?${qs}` : \"\"}`;\n\n const response = await fetch(url, buildGetInit(options.apiKey, options));\n\n if (response.status === 404) return null;\n if (!response.ok) {\n throw await toApiError(response);\n }\n\n const json = (await response.json()) as CmsPostWire;\n return postFromWire(json);\n}\n\nexport interface FetchPageBySlugOptions {\n readonly apiKey: string;\n readonly baseUrl?: string;\n readonly slug: string;\n readonly siteId?: string;\n /** ADR-0052 A안 (W4-6 PR C1) — `FetchPostsOptions.locale` 과 동일 시맨틱. */\n readonly locale?: string;\n readonly signal?: AbortSignal;\n readonly revalidate?: number;\n}\n\nexport async function fetchPageBySlug(\n options: FetchPageBySlugOptions,\n): Promise<CmsPostContent | null> {\n let cursor: string | undefined;\n do {\n const page = await fetchPosts({\n apiKey: options.apiKey,\n baseUrl: options.baseUrl,\n limit: 100,\n ...(cursor ? { cursor } : {}),\n type: \"page\",\n siteId: options.siteId,\n ...(options.locale ? { locale: options.locale } : {}),\n signal: options.signal,\n revalidate: options.revalidate,\n });\n const match = page.items.find((item) => item.slug === options.slug);\n if (match) return match;\n cursor = page.nextCursor ?? undefined;\n } while (cursor);\n return null;\n}\n\n","import {\n DEFAULT_BASE_URL,\n assertServer,\n buildGetInit,\n toApiError,\n type CmsPostContent,\n} from \"./server-base\";\n\n/**\n * 블로그 표시 설정 — admin /design 의 \"블로그 표시\" 에서 저장한 사이트 기본값.\n * renderer 가 prop 미지정 시 이 값을 기본으로 적용 (글별 override 는 post 의\n * metaJson.display 가 우선).\n */\n/** @deprecated 전환 흐름은 사이트 공통 블록이나 프로젝트 코드가 소유한다. */\nexport interface RootTaleBlogPostCta {\n title: string | null;\n description: string | null;\n buttonLabel: string;\n buttonHref: string;\n}\n\n/**\n * 사이트 레벨 SEO 기본값 — 외부 사이트 `<head>`(설명·로고·파비콘)와 글별 OG\n * 미설정 시 공유 썸네일 폴백(defaultOgImageUrl)에 쓴다. 모두 nullable.\n */\nexport interface RootTaleSiteProfile {\n siteDescription: string | null;\n /** 메인 홈(`/`) 검색 제목. null 이면 사이트 이름(theme `siteName`)을 쓴다. */\n homeTitle: string | null;\n /** 메인 홈(`/`) 검색 설명. null 이면 siteDescription 을 쓴다. */\n homeDescription: string | null;\n logoUrl: string | null;\n faviconUrl: string | null;\n /** 글별 og:image 미설정 시 폴백(1200×630 권장). */\n defaultOgImageUrl: string | null;\n}\n\n/** @deprecated 사이트맵 구조는 사이트 프로젝트 코드가 소유한다. 응답 호환 전용. */\nexport interface RootTaleSitemapSettings {\n /** 블로그 글 entry 에 대표 이미지(featuredImageUrl)를 `<image:image>` 로 포함. */\n blogImages: boolean;\n /** 다국어 hreflang locales (BCP-47). 빈 배열 = 단일 로케일(alternates 미emit). */\n locales: string[];\n /** authors 하위 사이트맵 포함 (작가 아카이브 라우트 필요). */\n authors: boolean;\n /**\n * 색인 위생(seo-geo-contract v3.1 §1) — \"검색에 이 분류 페이지 노출\" 을 켠\n * 카테고리 slug 목록. 아카이브는 기본 noindex 라서 여기 없는 카테고리의 모음\n * 페이지는 sitemap 에서 빠진다.\n *\n * **`undefined` = 승격 정보를 모름** (구버전 API 응답이거나 admin 설정 fetch 를\n * 생략한 경로). 이때 `createSitemapIndex` 는 정책을 적용하지 않고 기존 동작을\n * 유지한다 — 모른다는 이유로 이미 색인된 URL 을 지우지 않기 위해서다.\n *\n * 이 구분을 소비처가 직접 해석하지 말고 cms-renderer-next 의\n * `resolveCategoryPromotion(slugs, slug)` 에 그대로 넘길 것 — 사이트맵과\n * 아카이브 페이지가 같은 3-state(`promoted`/`not-promoted`/`unknown`)를 써야\n * 두 소스의 결론이 갈리지 않는다.\n */\n promotedCategorySlugs?: string[];\n}\n\nexport interface RootTaleBlogSettings {\n showTableOfContents: boolean;\n showAuthor: boolean;\n showDate: boolean;\n showAuthorCard: boolean;\n /** 목차 상단 제목. null = renderer 기본(\"목차\"). */\n tocTitle: string | null;\n /**\n * 목차 배치. \"inline\"=본문 위 접이식(기본), \"sidebar\"=넓은 화면에서 본문\n * 오른쪽 sticky 사이드바(좁은 화면은 자동 인라인 폴백).\n */\n tocPosition: \"inline\" | \"sidebar\";\n /** 작성자 카드 상단 라벨. null = renderer 기본(\"작성자\"). */\n authorCardEyebrow: string | null;\n /** @deprecated 작성자 소개는 글의 authorBio를 사용한다. 항상 null. */\n authorCardDescription: string | null;\n /** @deprecated 작성자 이름은 글의 authorName을 사용한다. 항상 null. */\n authorProfileName: string | null;\n /** @deprecated 작성자 소개는 글의 authorBio를 사용한다. 항상 null. */\n authorProfileBio: string | null;\n /** @deprecated 작성자 사진은 글의 authorImageUrl을 사용한다. 항상 null. */\n authorProfileImageUrl: string | null;\n /** @deprecated 렌더러가 자동 적용하지 않는다. 사이트 시멘틱 토큰/CSS에서 관리한다. */\n authorProfileImageRadius: \"circle\" | \"rounded\" | \"square\";\n /** @deprecated 렌더러가 자동 적용하지 않는다. 사이트 콘텐츠 데이터에서 관리한다. */\n authorProfileImagePositionX: number;\n /** @deprecated 렌더러가 자동 적용하지 않는다. 사이트 콘텐츠 데이터에서 관리한다. */\n authorProfileImagePositionY: number;\n /** 사이트 레벨 SEO 기본값(설명·로고·파비콘·기본 OG). */\n siteProfile: RootTaleSiteProfile;\n /** @deprecated 렌더러가 자동 적용하지 않는다. 응답 호환 전용. */\n postCta: RootTaleBlogPostCta | null;\n /**\n * @deprecated 사이트맵 정책은 사이트 코드에서 명시한다. optional — 구버전\n * 응답/직접 구성한 객체와의 하위호환을 위해 보존한다.\n */\n sitemap?: RootTaleSitemapSettings;\n}\n\n/**\n * 블로그 표시 설정 fetch 캐시 tag 의 정본 이름 (ADR-0096 Phase C).\n * `fetchBlogSettings` 호출에 `tags: [BLOG_SETTINGS_CACHE_TAG]` 로 부착하고,\n * revalidate route 가 `theme.updated` 수신 시\n * `revalidateTag(BLOG_SETTINGS_CACHE_TAG)` 로 무효화한다. 양쪽이 이 상수를\n * 공유해 drift 를 방지한다.\n */\nexport const BLOG_SETTINGS_CACHE_TAG = \"rt-blog-settings\";\n\nexport interface FetchBlogSettingsOptions {\n apiKey: string;\n baseUrl?: string;\n siteId?: string;\n signal?: AbortSignal;\n /** ISR revalidate 초(opt-in, W0-9). 미지정 시 현행 동작 유지. */\n revalidate?: number;\n /**\n * on-demand 무효화용 Next fetch cache tags(opt-in, ADR-0096 Phase C). 보통\n * `[BLOG_SETTINGS_CACHE_TAG]`. 미지정 시 현행 동작 유지.\n */\n tags?: string[];\n}\n\n/**\n * tenant 의 블로그 표시 기본값을 가져온다. 네트워크/서버 에러는 throw —\n * 호출자(renderer)가 안전 기본값으로 fallback.\n */\nexport async function fetchBlogSettings(\n options: FetchBlogSettingsOptions,\n): Promise<RootTaleBlogSettings> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams();\n if (options.siteId) params.set(\"site_id\", options.siteId);\n const qs = params.toString();\n const url = `${baseUrl}/v1/cms/public/blog-settings${qs ? `?${qs}` : \"\"}`;\n\n const response = await fetch(url, buildGetInit(options.apiKey, options));\n\n if (!response.ok) {\n throw await toApiError(response);\n }\n\n const json = (await response.json()) as {\n show_table_of_contents?: boolean;\n show_author?: boolean;\n show_date?: boolean;\n show_author_card?: boolean;\n toc_title?: string | null;\n toc_position?: string | null;\n author_card_eyebrow?: string | null;\n author_card_description?: string | null;\n author_profile_name?: string | null;\n author_profile_bio?: string | null;\n author_profile_image_url?: string | null;\n author_profile_image_radius?: string | null;\n author_profile_image_position_x?: number;\n author_profile_image_position_y?: number;\n site_profile?: {\n site_description?: string | null;\n home_title?: string | null;\n home_description?: string | null;\n logo_url?: string | null;\n favicon_url?: string | null;\n default_og_image_url?: string | null;\n } | null;\n post_cta?: {\n title?: string | null;\n description?: string | null;\n button_label?: string;\n button_href?: string;\n } | null;\n sitemap?: {\n blog_images?: boolean;\n locales?: string[];\n authors?: boolean;\n promoted_category_slugs?: string[];\n } | null;\n };\n const radius =\n json.author_profile_image_radius === \"rounded\" ||\n json.author_profile_image_radius === \"square\"\n ? json.author_profile_image_radius\n : \"circle\";\n const percent = (x: unknown): number =>\n typeof x === \"number\" && Number.isFinite(x)\n ? Math.max(0, Math.min(100, Math.round(x)))\n : 50;\n const nonEmpty = (x: unknown): string | null =>\n typeof x === \"string\" && x.length > 0 ? x : null;\n return {\n showTableOfContents: json.show_table_of_contents === true,\n showAuthor: json.show_author !== false,\n showDate: json.show_date !== false,\n showAuthorCard: json.show_author_card !== false,\n tocTitle:\n typeof json.toc_title === \"string\" && json.toc_title.length > 0\n ? json.toc_title\n : null,\n tocPosition: json.toc_position === \"sidebar\" ? \"sidebar\" : \"inline\",\n authorCardEyebrow:\n typeof json.author_card_eyebrow === \"string\" &&\n json.author_card_eyebrow.length > 0\n ? json.author_card_eyebrow\n : null,\n authorCardDescription:\n typeof json.author_card_description === \"string\" &&\n json.author_card_description.length > 0\n ? json.author_card_description\n : null,\n authorProfileName:\n typeof json.author_profile_name === \"string\" &&\n json.author_profile_name.length > 0\n ? json.author_profile_name\n : null,\n authorProfileBio:\n typeof json.author_profile_bio === \"string\" &&\n json.author_profile_bio.length > 0\n ? json.author_profile_bio\n : null,\n authorProfileImageUrl:\n typeof json.author_profile_image_url === \"string\" &&\n json.author_profile_image_url.length > 0\n ? json.author_profile_image_url\n : null,\n authorProfileImageRadius: radius,\n authorProfileImagePositionX: percent(json.author_profile_image_position_x),\n authorProfileImagePositionY: percent(json.author_profile_image_position_y),\n siteProfile: {\n siteDescription: nonEmpty(json.site_profile?.site_description),\n homeTitle: nonEmpty(json.site_profile?.home_title),\n homeDescription: nonEmpty(json.site_profile?.home_description),\n logoUrl: nonEmpty(json.site_profile?.logo_url),\n faviconUrl: nonEmpty(json.site_profile?.favicon_url),\n defaultOgImageUrl: nonEmpty(json.site_profile?.default_og_image_url),\n },\n postCta:\n json.post_cta &&\n typeof json.post_cta.button_label === \"string\" &&\n json.post_cta.button_label.length > 0 &&\n typeof json.post_cta.button_href === \"string\" &&\n json.post_cta.button_href.length > 0\n ? {\n title:\n typeof json.post_cta.title === \"string\" &&\n json.post_cta.title.length > 0\n ? json.post_cta.title\n : null,\n description:\n typeof json.post_cta.description === \"string\" &&\n json.post_cta.description.length > 0\n ? json.post_cta.description\n : null,\n buttonLabel: json.post_cta.button_label,\n buttonHref: json.post_cta.button_href,\n }\n : null,\n sitemap: {\n blogImages: json.sitemap?.blog_images !== false,\n locales: Array.isArray(json.sitemap?.locales)\n ? json.sitemap.locales.filter(\n (s): s is string => typeof s === \"string\" && s.length > 0,\n )\n : [],\n authors: json.sitemap?.authors === true,\n // 배열이 아니면(구버전 API 응답) 필드 자체를 생략 = \"승격 정보 모름\".\n // 빈 배열([])은 \"승격된 카테고리가 하나도 없음\" 이라는 확정 정보라 의미가\n // 다르다 — 이 구분이 사이트맵 필터의 fail-soft 근거다.\n ...(Array.isArray(json.sitemap?.promoted_category_slugs)\n ? {\n promotedCategorySlugs: json.sitemap.promoted_category_slugs.filter(\n (s): s is string => typeof s === \"string\" && s.length > 0,\n ),\n }\n : {}),\n },\n };\n}\n\n// ── 콘텐츠 Collections (ADR-0060) ────────────────────────────────────────────\n\n/**\n * 콘텐츠 스트림 1개 (camelCase). cms-core `RouteCollection` 과 구조 호환 —\n * `createSitemap`/`createFeedRoute` 의 `collections` 에 그대로 넘길 수 있다.\n */\nexport interface CmsRouteCollection {\n key: string;\n label: string;\n /** \"\" = 주소 없는 유형(라우트 없음) — 페이지 안에서 불러와 쓰는 콘텐츠. */\n basePath: string;\n layout: string;\n categories: string[];\n feed: boolean;\n archives: boolean;\n og: boolean;\n requireSubcategory: boolean;\n pinning: boolean;\n icon: string | null;\n /** 글별 상세 라우트 존재 여부 (기본 true — 하위 호환). false = 목록 전용 유형. */\n detail: boolean;\n detailPath: \"flat\" | \"category\";\n categoryPath: \"namespaced\" | \"direct\";\n categoryCardinality: \"multiple\" | \"exactly-one\";\n}\n\n/**\n * 콘텐츠 유형(collections) fetch 캐시 tag 의 정본 이름 (ADR-0096 Phase C).\n * `fetchCollections` 호출에 `tags: [COLLECTIONS_CACHE_TAG]` 로 부착하고,\n * revalidate route 가 `theme.updated` 수신 시\n * `revalidateTag(COLLECTIONS_CACHE_TAG)` 로 무효화한다. 양쪽이 이 상수를 공유해\n * drift 를 방지한다.\n */\nexport const COLLECTIONS_CACHE_TAG = \"rt-collections\";\n\nexport interface FetchCollectionsOptions {\n apiKey: string;\n baseUrl?: string;\n siteId?: string;\n signal?: AbortSignal;\n /** ISR revalidate 초(opt-in, W0-9). 미지정 시 현행 동작 유지. */\n revalidate?: number;\n /**\n * on-demand 무효화용 Next fetch cache tags(opt-in, ADR-0096 Phase C). 보통\n * `[COLLECTIONS_CACHE_TAG]`. 미지정 시 현행 동작 유지.\n */\n tags?: string[];\n}\n\n/**\n * 사이트의 콘텐츠 Collections (공지/블로그 등 스트림 분리)를 가져온다. 미설정이면\n * 빈 배열(레거시 단일 /blog). 네트워크/서버 에러는 throw — 호출자가 빈 배열로\n * fail-soft 하도록 한다(렌더가 collections 때문에 죽지 않는 것이 계약).\n */\nexport async function fetchCollections(\n options: FetchCollectionsOptions,\n): Promise<CmsRouteCollection[]> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams();\n if (options.siteId) params.set(\"site_id\", options.siteId);\n const qs = params.toString();\n const url = `${baseUrl}/v1/cms/public/collections${qs ? `?${qs}` : \"\"}`;\n\n const response = await fetch(url, buildGetInit(options.apiKey, options));\n if (!response.ok) {\n throw await toApiError(response);\n }\n\n const json = (await response.json()) as {\n collections?: Array<Record<string, unknown>>;\n };\n const items = Array.isArray(json.collections) ? json.collections : [];\n return items.flatMap((o) => {\n const key = typeof o.key === \"string\" ? o.key : \"\";\n // \"\" = 주소 없는 유형(라우트/사이트맵/피드/아카이브 파생 없음, ADR-0060) — key\n // 만 있으면 유지한다. 소속 판정(`postsNotInAnyCollection` 방어선)의 시맨틱을\n // 서버(declaredKeys=key 기준)와 맞추기 위해 basePath 없다고 드롭하지 않는다.\n const basePath = typeof o.base_path === \"string\" ? o.base_path : \"\";\n if (!key) return [];\n return [\n {\n key,\n basePath,\n label: typeof o.label === \"string\" ? o.label : key,\n layout: typeof o.layout === \"string\" ? o.layout : \"grid\",\n categories: Array.isArray(o.categories)\n ? o.categories.filter((s): s is string => typeof s === \"string\")\n : [],\n feed: o.feed === true,\n archives: o.archives === true,\n og: o.og === true,\n requireSubcategory: o.require_subcategory === true,\n pinning: o.pinning === true,\n icon: typeof o.icon === \"string\" ? o.icon : null,\n // 기본 true(하위 호환) — 명시적으로 false 인 경우만 목록 전용.\n detail: o.detail !== false,\n detailPath: o.detail_path === \"category\" ? \"category\" : \"flat\",\n categoryPath: o.category_path === \"direct\" ? \"direct\" : \"namespaced\",\n categoryCardinality:\n o.category_cardinality === \"exactly-one\" ? \"exactly-one\" : \"multiple\",\n },\n ];\n });\n}\n\n/**\n * fetch 실패/미설정 시 안전 기본값 — 빈 배열(레거시 단일 /blog). 사이트가\n * `fetchCollections().catch(() => fetchCollectionsSafeDefault())` 로 쓴다.\n */\nexport const fetchCollectionsSafeDefault: CmsRouteCollection[] = [];\n\n// ── 콘텐츠 모델 ──────────────────────────────────────────────────────────────\n\nexport interface CmsContentModel {\n key: string;\n label: string;\n cardinality: \"singleton\" | \"collection\";\n preset: \"page\" | \"article\" | \"entity\" | null;\n source: \"code\" | \"site\";\n presentation:\n | { readonly kind: \"data_only\"; readonly listPaths?: readonly string[] }\n | {\n readonly kind: \"fixed_page\";\n readonly path: string;\n readonly templateKey?: string;\n readonly allowedSections: readonly string[];\n }\n | {\n readonly kind: \"detail\";\n readonly detailPath: string;\n readonly templateKey?: string;\n /** 글을 목록으로 보여 주는 고정 화면(홈·회사소개 등). 글 변경 웹훅에 함께 실린다. */\n readonly listPaths?: readonly string[];\n }\n | {\n readonly kind: \"category_tree\";\n readonly basePath: string;\n readonly categoryDepth: number;\n /** `leaf`면 하위 분류가 없는 분류에 단계와 무관하게 글을 붙인다. 생략은 `deepest`. */\n readonly entryCategory?: \"deepest\" | \"leaf\";\n readonly categorySelection?: \"first\";\n readonly categoryFallbackSlug?: string;\n readonly reservedCategorySlugs?: readonly string[];\n readonly templateKey?: string;\n readonly listPaths?: readonly string[];\n };\n}\n\nexport interface FetchContentModelsOptions {\n apiKey: string;\n baseUrl?: string;\n siteId?: string;\n signal?: AbortSignal;\n revalidate?: number;\n tags?: string[];\n}\n\n/** 활성 페이지·글·정보 모델의 안전한 공개 projection을 가져온다. */\nexport async function fetchContentModels(\n options: FetchContentModelsOptions,\n): Promise<CmsContentModel[]> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams();\n if (options.siteId) params.set(\"site_id\", options.siteId);\n const qs = params.toString();\n const response = await fetch(\n `${baseUrl}/v1/cms/public/content-models${qs ? `?${qs}` : \"\"}`,\n buildGetInit(options.apiKey, options),\n );\n if (!response.ok) throw await toApiError(response);\n\n const json = (await response.json()) as {\n models?: Array<Record<string, unknown>>;\n };\n return (Array.isArray(json.models) ? json.models : []).flatMap((model) => {\n if (typeof model.key !== \"string\" || typeof model.label !== \"string\") return [];\n const cardinality =\n model.cardinality === \"singleton\" ? \"singleton\" : \"collection\";\n const preset =\n model.preset === \"page\" || model.preset === \"article\" || model.preset === \"entity\"\n ? model.preset\n : null;\n const rawPresentation =\n model.presentation && typeof model.presentation === \"object\" &&\n !Array.isArray(model.presentation)\n ? model.presentation as Record<string, unknown>\n : {};\n const listPaths = Array.isArray(rawPresentation.listPaths)\n ? rawPresentation.listPaths.filter(\n (item): item is string => typeof item === \"string\" && item.startsWith(\"/\"),\n )\n : [];\n const withListPaths = listPaths.length > 0 ? { listPaths } : {};\n const presentation: CmsContentModel[\"presentation\"] =\n rawPresentation.kind === \"fixed_page\" && typeof rawPresentation.path === \"string\"\n ? {\n kind: \"fixed_page\",\n path: rawPresentation.path,\n allowedSections: Array.isArray(rawPresentation.allowedSections)\n ? rawPresentation.allowedSections.filter(\n (item): item is string => typeof item === \"string\",\n )\n : [],\n ...(typeof rawPresentation.templateKey === \"string\"\n ? { templateKey: rawPresentation.templateKey }\n : {}),\n }\n : rawPresentation.kind === \"detail\" && typeof rawPresentation.detailPath === \"string\"\n ? {\n kind: \"detail\",\n detailPath: rawPresentation.detailPath,\n ...(typeof rawPresentation.templateKey === \"string\"\n ? { templateKey: rawPresentation.templateKey }\n : {}),\n ...withListPaths,\n }\n : rawPresentation.kind === \"category_tree\" &&\n typeof rawPresentation.basePath === \"string\" &&\n typeof rawPresentation.categoryDepth === \"number\"\n ? {\n kind: \"category_tree\",\n basePath: rawPresentation.basePath,\n categoryDepth: rawPresentation.categoryDepth,\n ...(rawPresentation.categorySelection === \"first\" ? { categorySelection: \"first\" as const } : {}),\n ...(typeof rawPresentation.categoryFallbackSlug === \"string\" ? { categoryFallbackSlug: rawPresentation.categoryFallbackSlug } : {}),\n ...(Array.isArray(rawPresentation.reservedCategorySlugs)\n ? { reservedCategorySlugs: rawPresentation.reservedCategorySlugs.filter((slug): slug is string => typeof slug === \"string\") }\n : {}),\n ...(rawPresentation.entryCategory === \"leaf\" ||\n rawPresentation.entryCategory === \"deepest\"\n ? { entryCategory: rawPresentation.entryCategory }\n : {}),\n ...(typeof rawPresentation.templateKey === \"string\"\n ? { templateKey: rawPresentation.templateKey }\n : {}),\n ...withListPaths,\n }\n : { kind: \"data_only\", ...withListPaths };\n return [{\n key: model.key,\n label: model.label,\n cardinality,\n preset,\n source: model.source === \"site\" ? \"site\" : \"code\",\n presentation,\n }];\n });\n}\n\n// ── 작가 아카이브 (blanche식 /blog/author/{slug}) ────────────────────────────\n\n/** 작가 1명 (camelCase) — author_slug 가 있고 발행 글이 1건 이상인 작가만. */\nexport interface RootTaleAuthor {\n /** 아카이브 URL slug (/blog/author/{slug}). */\n slug: string;\n /** 표시 이름 (author_display_name > user.name 폴백). */\n name: string;\n imageUrl: string | null;\n imagePositionX?: number | null;\n imagePositionY?: number | null;\n bio: string | null;\n /** 발행 글(type=post) 수 (≥1). */\n postCount: number;\n /** 작가의 최근 글 수정일 (사이트맵 lastmod). */\n lastUpdatedAt: string | null;\n}\n\nexport interface FetchAuthorsOptions {\n apiKey: string;\n baseUrl?: string;\n siteId?: string;\n signal?: AbortSignal;\n /** ISR revalidate 초(opt-in, W0-9). 미지정 시 현행 동작 유지. */\n revalidate?: number;\n}\n\n/**\n * 사이트의 작가 목록(작가 아카이브·사이트맵 파생용)을 가져온다. author_slug 가 있고\n * 발행 글이 1건 이상인 작가만. 네트워크/서버 에러는 throw — 호출자(라우트/사이트맵\n * 팩토리)가 빈 배열로 fail-soft 한다.\n */\nexport async function fetchAuthors(\n options: FetchAuthorsOptions,\n): Promise<RootTaleAuthor[]> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams();\n if (options.siteId) params.set(\"site_id\", options.siteId);\n const qs = params.toString();\n const url = `${baseUrl}/v1/cms/public/authors${qs ? `?${qs}` : \"\"}`;\n\n const response = await fetch(url, buildGetInit(options.apiKey, options));\n if (!response.ok) {\n throw await toApiError(response);\n }\n\n const json = (await response.json()) as {\n authors?: Array<Record<string, unknown>>;\n };\n const items = Array.isArray(json.authors) ? json.authors : [];\n return items.flatMap((o) => {\n const slug = typeof o.slug === \"string\" ? o.slug : \"\";\n const name = typeof o.name === \"string\" ? o.name : \"\";\n if (!slug || !name) return [];\n return [\n {\n slug,\n name,\n imageUrl: typeof o.image_url === \"string\" ? o.image_url : null,\n imagePositionX:\n typeof o.image_position_x === \"number\" ? o.image_position_x : null,\n imagePositionY:\n typeof o.image_position_y === \"number\" ? o.image_position_y : null,\n bio: typeof o.bio === \"string\" ? o.bio : null,\n postCount: typeof o.post_count === \"number\" ? o.post_count : 0,\n lastUpdatedAt:\n typeof o.last_updated_at === \"string\" ? o.last_updated_at : null,\n },\n ];\n });\n}\n\n/** fetch 실패/미설정 시 사용할 안전 기본값 (기존 렌더러 동작). */\nexport const DEFAULT_BLOG_SETTINGS: RootTaleBlogSettings = {\n showTableOfContents: false,\n showAuthor: true,\n showDate: true,\n showAuthorCard: true,\n tocTitle: null,\n tocPosition: \"inline\",\n authorCardEyebrow: null,\n authorCardDescription: null,\n authorProfileName: null,\n authorProfileBio: null,\n authorProfileImageUrl: null,\n authorProfileImageRadius: \"circle\",\n authorProfileImagePositionX: 50,\n authorProfileImagePositionY: 50,\n siteProfile: {\n siteDescription: null,\n homeTitle: null,\n homeDescription: null,\n logoUrl: null,\n faviconUrl: null,\n defaultOgImageUrl: null,\n },\n postCta: null,\n sitemap: {\n blogImages: true,\n locales: [],\n authors: false,\n // 이 기본값은 **블로그 렌더링 폴백 전용**이다 (cms-renderer-next 의\n // `fetchBlogSettingsFailSoft` = `fetchBlogSettings(...).catch(() => DEFAULT_BLOG_SETTINGS)`,\n // 소비처는 `server/blog-list.tsx`·`server/blog-post.tsx` 뿐 — 둘 다 색인\n // 정책과 무관한 본문 표시 설정만 읽는다).\n //\n // **색인 정책 경로는 이 상수를 쓰지 않는다** — 사이트맵은\n // `resolveSitemapSettings`(cms-renderer-next/routes-sitemap-entries.ts)가\n // 자체 try/catch 로 `remote = {}` 를 쓰고, 아카이브 페이지는 조회 실패 시\n // 설정 객체 자체를 `undefined` 로 넘긴다. 그래서 두 경로 모두 fetch 실패가\n // `promotedCategorySlugs === undefined`(= 승격 정보 모름) 로 전달돼\n // `resolveCategoryPromotion` 이 `unknown` 을 만들고 기존 URL 이 보존된다.\n // 여기 `[]` 는 \"승격 0건\"(확정) 이지만 그 의미가 색인 정책까지 전달되는\n // 경로는 없다 — 새 소비처를 만들 때 이 상수를 승격 판정에 넘기지 말 것.\n promotedCategorySlugs: [],\n },\n};\n\n\nexport interface ResolvedPostDisplay {\n showTableOfContents: boolean;\n showAuthor: boolean;\n showDate: boolean;\n showAuthorCard: boolean;\n tocTitle: string;\n tocPosition: \"inline\" | \"sidebar\";\n authorCardEyebrow: string;\n authorCardDescription: string | null;\n authorProfileName: string | null;\n authorProfileBio: string | null;\n authorProfileImageUrl: string | null;\n authorProfileImageRadius: \"circle\" | \"rounded\" | \"square\";\n authorProfileImagePositionX: number;\n authorProfileImagePositionY: number;\n}\n\n/**\n * 글 1건의 실제 표시 여부 결정. 우선순위:\n * 명시 prop > 글별 override(post.metaJson.display) > 사이트 기본값(settings).\n * tocTitle: 명시 prop > settings.tocTitle > \"목차\".\n */\nexport function resolvePostDisplay(\n post: { metaJson?: Record<string, unknown> | null },\n settings: RootTaleBlogSettings,\n explicit?: { showTableOfContents?: boolean; tableOfContentsTitle?: string },\n): ResolvedPostDisplay {\n const display =\n post.metaJson && typeof post.metaJson === \"object\"\n ? ((post.metaJson as Record<string, unknown>).display as\n | Record<string, unknown>\n | undefined)\n : undefined;\n const ov = (key: string): boolean | undefined =>\n display && typeof display[key] === \"boolean\"\n ? (display[key] as boolean)\n : undefined;\n return {\n showTableOfContents:\n explicit?.showTableOfContents ?? ov(\"toc\") ?? settings.showTableOfContents,\n showAuthor: ov(\"author\") ?? settings.showAuthor,\n showDate: ov(\"date\") ?? settings.showDate,\n showAuthorCard: settings.showAuthorCard,\n tocTitle: explicit?.tableOfContentsTitle ?? settings.tocTitle ?? \"목차\",\n tocPosition: settings.tocPosition ?? \"inline\",\n authorCardEyebrow: settings.authorCardEyebrow ?? \"작성자\",\n authorCardDescription: settings.authorCardDescription,\n authorProfileName: settings.authorProfileName,\n authorProfileBio: settings.authorProfileBio,\n authorProfileImageUrl: settings.authorProfileImageUrl,\n authorProfileImageRadius: settings.authorProfileImageRadius,\n authorProfileImagePositionX: settings.authorProfileImagePositionX,\n authorProfileImagePositionY: settings.authorProfileImagePositionY,\n };\n}\n\n/**\n * 관련 글 선별 — 현재 글과 **같은 카테고리**를 1개 이상 공유하는 후보를\n * 발행일 내림차순으로 골라 최대 `limit` 개 반환 (자기 자신 제외).\n *\n * 순수 함수: 후보(`candidates`)는 호출부가 `fetchPosts` 로 미리 받아 전달한다\n * (서버에 카테고리 필터 파라미터가 없어 최근 글 배치에서 선별). 현재 글에\n * 카테고리가 없으면 빈 배열.\n */\nexport function selectRelatedPosts(\n current: CmsPostContent,\n candidates: CmsPostContent[],\n limit = 3,\n): CmsPostContent[] {\n if (limit <= 0) return [];\n const currentCats = new Set(\n current.terms\n .filter((t) => t.taxonomy === \"category\")\n .map((t) => t.id),\n );\n if (currentCats.size === 0) return [];\n return candidates\n .filter(\n (p) =>\n p.id !== current.id &&\n p.terms.some(\n (t) => t.taxonomy === \"category\" && currentCats.has(t.id),\n ),\n )\n .sort((a, b) => b.publishedAt.localeCompare(a.publishedAt))\n .slice(0, limit);\n}\n","/**\n * 분류(category)별 발행 글 수 — **서버측 집계** 조회.\n *\n * 분류 아카이브의 \"0건 = 404\" 판정과 사이트맵 포함 여부(색인 위생)를 정확히\n * 계산하기 위한 값이다. 이 API 가 없던 시절 렌더러는 최근 글 배치를 받아 세었고,\n * 글이 많은 사이트에서는 배치 한도 밖에만 글이 있는 분류가 페이지·사이트맵에서\n * 함께 사라졌다. 소비처(`@roottale/cms-renderer-next`)는 이 함수를 먼저 쓰고,\n * 실패하면 예전 배치 파생으로 fail-soft 한다.\n */\n\nimport {\n CmsApiError,\n DEFAULT_BASE_URL,\n assertServer,\n buildGetInit,\n toApiError,\n} from \"./server-base\";\n\n/** 분류 1건 + 요청 스코프 안의 발행 글 수. */\nexport interface CmsCategoryCount {\n /** 안정 term id. 계층형 모델에서 post.terms의 id와 연결한다. */\n id?: string | null;\n slug: string;\n name: string;\n /** 부모 category term id. 루트면 null. */\n parentId?: string | null;\n termOrder?: number;\n /** 요청 스코프(미소속/특정 스트림) 안에서 이 분류에 걸린 발행 글 수. 0 가능. */\n publishedPostCount: number;\n /** 이 분류가 귀속된 콘텐츠 유형 key (ADR-0060). 미귀속·구버전 응답이면 null. */\n collectionKey: string | null;\n /**\n * 어드민 \"검색에 이 분류 페이지 노출\" 토글. **승격 판정 입력으로 쓰지 말 것** —\n * 렌더러의 3-state 승격은 `fetchBlogSettings().sitemap.promotedCategorySlugs`\n * 하나에서만 조립한다(두 소스를 섞으면 사이트맵과 페이지가 서로 다른 시점의\n * 값을 볼 수 있다). 진단·어드민 표시용 참고값이다.\n */\n hubPromoted: boolean;\n description: string | null;\n seoTitle: string | null;\n seoDescription: string | null;\n /** 공유 전용 설정. 본문 분류 이미지와 별도로 관리한다. */\n seoOgImage?: string | null;\n seoOgImageAlt?: string | null;\n seoNofollow?: boolean | null;\n imageMediaId?: string | null;\n imageUrl?: string | null;\n imageAlt?: string | null;\n path: string | null;\n}\n\nexport interface CmsCategoryCounts {\n categories: CmsCategoryCount[];\n /** Apply hubPromoted metadata policy only after explicit site activation. */\n categoryIndexActivated?: boolean;\n /** 분류 수가 서버 상한을 넘어 일부가 빠졌으면 true — 소비처는 경고를 남길 것. */\n truncated: boolean;\n}\n\nexport interface FetchCategoryCountsOptions {\n /** `rtlk_cust_*` API key. 블로그 조회와 같은 키. */\n apiKey: string;\n baseUrl?: string;\n siteId?: string;\n /**\n * 그 섹션(collection) 소속 글만 센다 — 스트림 아카이브용. `excludeCollections`\n * 와 동시 지정 불가(서버가 400).\n */\n collectionKey?: string;\n /** 어느 스트림에도 안 속한 글만 센다 — 레거시 `/blog` 계열 아카이브의 모집단. */\n excludeCollections?: boolean;\n /** BCP-47. 미지정이면 사이트 기본 로케일(포스트 목록과 같은 기본 동작). */\n locale?: string;\n signal?: AbortSignal;\n /** ISR revalidate 초(opt-in). 미지정 시 현행 동작 유지. */\n revalidate?: number;\n /** Additional Next.js data-cache tags (for example METADATA_CACHE_TAG). */\n tags?: string[];\n}\n\nfunction toNonNegativeInt(value: unknown): number {\n return typeof value === \"number\" && Number.isInteger(value) && value >= 0\n ? value\n : 0;\n}\n\n/**\n * 사이트의 모든 분류와 발행 글 수를 가져온다. 네트워크/서버 에러는 **throw** —\n * 호출자가 예전 파생 방식으로 fail-soft 할지, 화면을 죽일지 정한다.\n *\n * 이 엔드포인트가 없는 구버전 API 는 404 를 주고 그것도 `CmsApiError` 로 throw\n * 되므로, 호출자는 \"구버전 API\" 와 \"일시 장애\" 를 같은 catch 로 다루면 된다.\n * 응답 shape 이 예상과 다른 경우(`categories` 가 배열이 아님)도 throw 로 취급한다\n * — 빈 목록으로 오해하면 \"분류 0건\" 이라는 **확정 정보**가 되어 아카이브가 전부\n * 404 가 되기 때문이다.\n */\nexport async function fetchCategoryCounts(\n options: FetchCategoryCountsOptions,\n): Promise<CmsCategoryCounts> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams();\n if (options.siteId) params.set(\"site_id\", options.siteId);\n if (options.collectionKey) params.set(\"collection_key\", options.collectionKey);\n if (options.excludeCollections) params.set(\"exclude_collections\", \"true\");\n if (options.locale) params.set(\"locale\", options.locale);\n const qs = params.toString();\n const url = `${baseUrl}/v1/cms/public/categories${qs ? `?${qs}` : \"\"}`;\n\n const response = await fetch(url, buildGetInit(options.apiKey, options));\n if (!response.ok) {\n throw await toApiError(response);\n }\n\n const json = (await response.json()) as {\n categories?: Array<Record<string, unknown>>;\n truncated?: boolean;\n category_index_activated?: boolean;\n };\n if (!Array.isArray(json.categories)) {\n throw new CmsApiError(\n \"RootTale CMS API returned no `categories` array for /v1/cms/public/categories\",\n response.status,\n \"unexpected_response\",\n );\n }\n return {\n categories: json.categories.flatMap((o) => {\n const slug = typeof o.slug === \"string\" ? o.slug : \"\";\n const name = typeof o.name === \"string\" ? o.name : \"\";\n if (!slug || !name) return [];\n return [\n {\n id: typeof o.id === \"string\" ? o.id : null,\n slug,\n name,\n parentId: typeof o.parent_id === \"string\" ? o.parent_id : null,\n ...(typeof o.term_order === \"number\" && Number.isInteger(o.term_order) ? { termOrder: o.term_order } : {}),\n publishedPostCount: toNonNegativeInt(o.published_post_count),\n collectionKey:\n typeof o.collection_key === \"string\" ? o.collection_key : null,\n hubPromoted: o.hub_promoted === true,\n description: typeof o.description === \"string\" ? o.description : null,\n seoTitle: typeof o.seo_title === \"string\" ? o.seo_title : null,\n seoDescription:\n typeof o.seo_description === \"string\" ? o.seo_description : null,\n seoOgImage: typeof o.seo_og_image === \"string\" ? o.seo_og_image : null,\n seoOgImageAlt: typeof o.seo_og_image_alt === \"string\" ? o.seo_og_image_alt : null,\n seoNofollow: typeof o.seo_nofollow === \"boolean\" ? o.seo_nofollow : null,\n imageMediaId:\n typeof o.image_media_id === \"string\" ? o.image_media_id : null,\n imageUrl: typeof o.image_url === \"string\" ? o.image_url : null,\n imageAlt: typeof o.image_alt === \"string\" ? o.image_alt : null,\n path: typeof o.path === \"string\" ? o.path : null,\n },\n ];\n }),\n truncated: json.truncated === true,\n ...(typeof json.category_index_activated === \"boolean\" ? { categoryIndexActivated: json.category_index_activated } : {}),\n };\n}\n","import { DEFAULT_BASE_URL, assertServer, buildGetInit, toApiError } from \"./server-base\";\n\n// ── 비즈니스 프로필 (로컬 SEO) ───────────────────────────────────────────────\n\n/** Schema.org LocalBusiness 하위 타입 (admin \"운영 > 비즈니스 프로필\" 의 업종). */\nexport type BusinessType =\n | \"LocalBusiness\"\n | \"ProfessionalService\"\n | \"AccountingService\"\n | \"LegalService\"\n | \"MedicalClinic\"\n | \"Dentist\"\n | \"RealEstateAgent\"\n | \"Restaurant\"\n | \"BeautySalon\";\n\n/** Schema.org openingHours 요일 약어. */\nexport type BusinessDayCode = \"Mo\" | \"Tu\" | \"We\" | \"Th\" | \"Fr\" | \"Sa\" | \"Su\";\n\nexport interface BusinessOpeningHours {\n days: BusinessDayCode[];\n /** HH:MM (24시간). */\n opens: string;\n /** HH:MM (24시간). */\n closes: string;\n}\n\n/**\n * 비즈니스 프로필 — admin \"운영 > 비즈니스 프로필\" 에서 저장한 사업장 정보.\n * `localBusinessSchema(profile, { url })` 로 LocalBusiness JSON-LD 를 만들어\n * layout 에 1회 렌더한다 (로컬 SEO).\n */\nexport interface RootTaleBusinessProfile {\n /** 사업장 이름 (간판명). */\n name: string;\n /** 사업자등록 상호 — 간판명과 다를 때만 설정. */\n legalName: string | null;\n /** 간판명의 표기 변형(띄어쓰기 등) — JSON-LD alternateName. */\n alternateName: string | null;\n businessType: BusinessType;\n telephone: string | null;\n /** 팩스번호 — JSON-LD faxNumber. */\n faxNumber: string | null;\n email: string | null;\n address: {\n streetAddress: string;\n addressLocality: string;\n addressRegion: string | null;\n postalCode: string | null;\n } | null;\n geo: { latitude: number; longitude: number } | null;\n openingHours: BusinessOpeningHours[];\n /** 가격대 표기 (예: \"₩₩\"). */\n priceRange: string | null;\n /** 서비스 지역 (예: \"서울 강남구\"). */\n areaServed: string[];\n /** 취급 업무 (예: \"기장·신고대리\") — JSON-LD hasOfferCatalog. */\n services: string[];\n /** 외부 프로필 URL — JSON-LD sameAs 로 들어가 동일 사업장임을 연결. */\n profiles: {\n naverPlace: string | null;\n googleBusiness: string | null;\n kakaoChannel: string | null;\n /** 지도 앱 딥링크 — naverPlace 가 없을 때 hasMap 대체값. */\n kakaoMap: string | null;\n instagram: string | null;\n naverBlog: string | null;\n };\n}\n\n/**\n * 사업장 정보 fetch 캐시 tag 의 정본 이름 (ADR-0096 Phase C). 사이트의\n * `fetchBusinessProfile` 호출에 `tags: [BUSINESS_CACHE_TAG]` 로 부착하고,\n * revalidate route 가 `theme.updated` 수신 시 `revalidateTag(BUSINESS_CACHE_TAG)`\n * 로 무효화한다. 양쪽이 이 상수를 공유해 drift 를 방지한다.\n *\n * tag 를 붙이지 않으면 `revalidatePath` 로는 이 fetch 의 Data Cache 엔트리를\n * 지울 수 없어 `revalidate` 초만큼 낡은 주소·전화가 남는다.\n */\nexport const BUSINESS_CACHE_TAG = \"rt-business\";\n\nexport interface FetchBusinessProfileOptions {\n apiKey: string;\n baseUrl?: string;\n siteId?: string;\n signal?: AbortSignal;\n /** ISR revalidate 초(opt-in, W0-9). 미지정 시 현행 동작 유지. */\n revalidate?: number;\n /**\n * on-demand 무효화용 Next fetch cache tags(opt-in, ADR-0096 Phase C). 보통\n * `[BUSINESS_CACHE_TAG]`. 미지정 시 현행 동작 유지.\n */\n tags?: string[];\n}\n\ntype BusinessProfileWire = {\n configured?: boolean;\n name?: string | null;\n legal_name?: string | null;\n alternate_name?: string | null;\n business_type?: string | null;\n telephone?: string | null;\n fax_number?: string | null;\n email?: string | null;\n address?: {\n street_address?: string;\n address_locality?: string;\n address_region?: string | null;\n postal_code?: string | null;\n } | null;\n geo?: { latitude?: number; longitude?: number } | null;\n opening_hours?: {\n days?: BusinessDayCode[];\n opens?: string;\n closes?: string;\n }[];\n price_range?: string | null;\n area_served?: string[];\n services?: string[];\n profiles?: {\n naver_place?: string | null;\n google_business?: string | null;\n kakao_channel?: string | null;\n kakao_map?: string | null;\n instagram?: string | null;\n naver_blog?: string | null;\n } | null;\n};\n\n/** wire(snake_case) → RootTaleBusinessProfile. 미설정이면 null (export for tests). */\nexport function businessProfileFromWire(\n json: BusinessProfileWire,\n): RootTaleBusinessProfile | null {\n // configured=false 또는 이름 없음 = 미설정 — 호출자는 JSON-LD 렌더를 건너뛴다.\n if (json.configured === false) return null;\n if (typeof json.name !== \"string\" || json.name.length === 0) return null;\n const address =\n json.address &&\n typeof json.address.street_address === \"string\" &&\n typeof json.address.address_locality === \"string\"\n ? {\n streetAddress: json.address.street_address,\n addressLocality: json.address.address_locality,\n addressRegion: json.address.address_region ?? null,\n postalCode: json.address.postal_code ?? null,\n }\n : null;\n const geo =\n json.geo &&\n typeof json.geo.latitude === \"number\" &&\n typeof json.geo.longitude === \"number\"\n ? { latitude: json.geo.latitude, longitude: json.geo.longitude }\n : null;\n const openingHours: BusinessOpeningHours[] = [];\n for (const row of json.opening_hours ?? []) {\n if (\n Array.isArray(row.days) &&\n row.days.length > 0 &&\n typeof row.opens === \"string\" &&\n typeof row.closes === \"string\"\n ) {\n openingHours.push({ days: row.days, opens: row.opens, closes: row.closes });\n }\n }\n return {\n name: json.name,\n legalName: json.legal_name ?? null,\n alternateName: json.alternate_name ?? null,\n businessType:\n typeof json.business_type === \"string\" && json.business_type.length > 0\n ? (json.business_type as BusinessType)\n : \"LocalBusiness\",\n telephone: json.telephone ?? null,\n faxNumber: json.fax_number ?? null,\n email: json.email ?? null,\n address,\n geo,\n openingHours,\n priceRange: json.price_range ?? null,\n areaServed: json.area_served ?? [],\n services: json.services ?? [],\n profiles: {\n naverPlace: json.profiles?.naver_place ?? null,\n googleBusiness: json.profiles?.google_business ?? null,\n kakaoChannel: json.profiles?.kakao_channel ?? null,\n kakaoMap: json.profiles?.kakao_map ?? null,\n instagram: json.profiles?.instagram ?? null,\n naverBlog: json.profiles?.naver_blog ?? null,\n },\n };\n}\n\n/**\n * tenant 의 비즈니스 프로필을 가져온다. 미설정이면 null — 호출자는 null 일 때\n * JSON-LD 렌더를 건너뛰면 된다. 네트워크/서버 에러는 throw — 호출자가\n * `.catch(() => null)` 등으로 fallback.\n */\nexport async function fetchBusinessProfile(\n options: FetchBusinessProfileOptions,\n): Promise<RootTaleBusinessProfile | null> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams();\n if (options.siteId) params.set(\"site_id\", options.siteId);\n const qs = params.toString();\n const url = `${baseUrl}/v1/cms/public/business-profile${qs ? `?${qs}` : \"\"}`;\n\n const response = await fetch(url, buildGetInit(options.apiKey, options));\n\n // 구 서버(라우트 미배포)는 404 — 미설정과 동일하게 null.\n if (response.status === 404) return null;\n if (!response.ok) {\n throw await toApiError(response);\n }\n\n const json = (await response.json()) as BusinessProfileWire;\n return businessProfileFromWire(json);\n}\n\n","import {\n type CmsPostType,\n DEFAULT_BASE_URL,\n assertServer,\n buildGetInit,\n toApiError,\n} from \"./server-base\";\nimport type { CmsRouteCollection } from \"./server-blog\";\n\n// ── 공개 검색 (WP `?s=` 패리티) ────────────────────────────────────────────\n\nexport interface SearchPostsOptions {\n apiKey: string;\n baseUrl?: string;\n /** 검색 키워드 (title·excerpt·본문 텍스트 매칭). */\n query: string;\n /** 결과 수 1-50, default 10. */\n limit?: number;\n /** 기본 = `post` 만. page 포함 검색은 명시. */\n type?: CmsPostType | \"all\";\n /** BCP-47 locale. 미지정 시 사이트 기본 언어. */\n locale?: string;\n siteId?: string;\n signal?: AbortSignal;\n /** ISR revalidate 초(opt-in, W0-9). 미지정 시 현행 동작 유지. */\n revalidate?: number;\n}\n\n/** 검색 결과 카드용 슬림 hit — body 미포함. */\nexport interface CmsSearchHit {\n id: string;\n type: CmsPostType;\n collectionKey: string | null;\n modelKey: string | null;\n locale: string;\n title: string;\n slug: string;\n excerpt: string | null;\n featuredImageUrl: string | null;\n /** ISO 8601. */\n publishedAt: string;\n}\n\n/**\n * published 글 키워드 검색 (사이트 내 검색). 관련도순, body 미포함 슬림\n * hit — 상세는 slug 로 `fetchPost`. 구 서버(라우트 미배포)는 404 — 빈 배열.\n */\nexport async function searchPosts(\n options: SearchPostsOptions,\n): Promise<CmsSearchHit[]> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams();\n params.set(\"q\", options.query);\n if (options.limit !== undefined) params.set(\"limit\", String(options.limit));\n if (options.type) params.set(\"type\", options.type);\n if (options.locale) params.set(\"locale\", options.locale);\n if (options.siteId) params.set(\"site_id\", options.siteId);\n const url = `${baseUrl}/v1/cms/public/search?${params.toString()}`;\n\n const response = await fetch(url, buildGetInit(options.apiKey, options));\n\n if (response.status === 404) return [];\n if (!response.ok) {\n throw await toApiError(response);\n }\n\n const json = (await response.json()) as {\n items: {\n id: string;\n type: CmsPostType;\n collection_key: string | null;\n model_key?: string | null;\n locale: string;\n title: string;\n slug: string;\n excerpt: string | null;\n featured_media_url: string | null;\n published_at: string;\n }[];\n };\n return json.items.map((w) => ({\n id: w.id,\n type: w.type,\n collectionKey: w.collection_key ?? null,\n modelKey: w.model_key ?? null,\n locale: w.locale,\n title: w.title,\n slug: w.slug,\n excerpt: w.excerpt,\n featuredImageUrl: w.featured_media_url ?? null,\n publishedAt: w.published_at,\n }));\n}\n\ntype RoutableSearchHit = Pick<\n CmsSearchHit,\n \"type\" | \"slug\" | \"collectionKey\"\n>;\n\n/** 검색 hit을 사이트의 실제 공개 상세 주소로 바꾼다. 주소 없는 유형은 null. */\nexport function resolveSearchHitPath(\n hit: RoutableSearchHit,\n collections: readonly Pick<\n CmsRouteCollection,\n \"key\" | \"basePath\" | \"detail\"\n >[],\n locale?: string,\n): string | null {\n let path: string | null;\n if (hit.type === \"page\") {\n path = `/${hit.slug}`;\n } else {\n const collection = hit.collectionKey\n ? collections.find((candidate) => candidate.key === hit.collectionKey)\n : undefined;\n if (!collection) {\n path = `/blog/${hit.slug}`;\n } else if (!collection.detail || collection.basePath.length === 0) {\n path = null;\n } else {\n const basePath = `/${collection.basePath}`.replace(/\\/{2,}/g, \"/\").replace(/\\/$/, \"\");\n path = `${basePath}/${hit.slug}`;\n }\n }\n\n if (!path || !locale) return path;\n const localePrefix = `/${locale.replace(/^\\/+|\\/+$/g, \"\")}`;\n return `${localePrefix}${path}`;\n}\n\n// ── 네비게이션 메뉴 (WP 외모>메뉴 패리티) ──────────────────────────────────\n\n/** 메뉴 항목 트리 노드 (깊이 ≤ 2). */\nexport interface RootTaleMenuItem {\n id: string;\n label: string;\n /** 상대 경로(`/about`) 또는 절대 http(s) URL — 서버가 sanitize. */\n url: string;\n /** true 면 `target=\"_blank\" rel=\"noopener\"` 로 렌더할 것. */\n newTab?: boolean;\n children?: RootTaleMenuItem[];\n}\n\nexport interface RootTaleMenu {\n id: string;\n /** 사람용 이름 — \"헤더 메뉴\". */\n name: string;\n /** 위치 핸들 — \"primary\" | \"footer\" 등. */\n slug: string;\n items: RootTaleMenuItem[];\n updatedAt: string | null;\n}\n\n/**\n * 메뉴(네비게이션) fetch 캐시 tag 의 정본 이름 (ADR-0096 Phase C).\n * `fetchMenu`·`fetchMenus` 호출에 `tags: [MENUS_CACHE_TAG]` 로 부착하고,\n * revalidate route 가 `theme.updated` 수신 시 `revalidateTag(MENUS_CACHE_TAG)`\n * 로 무효화한다. 양쪽이 이 상수를 공유해 drift 를 방지한다.\n *\n * 메뉴 1개(`fetchMenu`)와 전체(`fetchMenus`)가 같은 tag 를 쓴다 — 어드민에서\n * 메뉴를 저장하면 어느 쪽 캐시든 같이 낡기 때문이다.\n */\nexport const MENUS_CACHE_TAG = \"rt-menus\";\n\nexport interface FetchMenuOptions {\n apiKey: string;\n baseUrl?: string;\n /** 위치 핸들 — \"primary\" | \"footer\" 등. */\n slug: string;\n siteId?: string;\n signal?: AbortSignal;\n /** ISR revalidate 초(opt-in, W0-9). 미지정 시 현행 동작 유지. */\n revalidate?: number;\n /**\n * on-demand 무효화용 Next fetch cache tags(opt-in, ADR-0096 Phase C). 보통\n * `[MENUS_CACHE_TAG]`. 미지정 시 현행 동작 유지.\n */\n tags?: string[];\n}\n\nexport interface FetchMenusOptions {\n apiKey: string;\n baseUrl?: string;\n siteId?: string;\n signal?: AbortSignal;\n /** ISR revalidate 초(opt-in, W0-9). 미지정 시 현행 동작 유지. */\n revalidate?: number;\n /**\n * on-demand 무효화용 Next fetch cache tags(opt-in, ADR-0096 Phase C). 보통\n * `[MENUS_CACHE_TAG]`. 미지정 시 현행 동작 유지.\n */\n tags?: string[];\n}\n\ntype CmsMenuItemWire = {\n id: string;\n label: string;\n url: string;\n new_tab?: boolean;\n children?: CmsMenuItemWire[];\n};\n\ntype CmsMenuWire = {\n id: string;\n name: string;\n slug: string;\n items: CmsMenuItemWire[];\n updated_at: string | null;\n};\n\nfunction menuItemFromWire(w: CmsMenuItemWire): RootTaleMenuItem {\n const item: RootTaleMenuItem = { id: w.id, label: w.label, url: w.url };\n if (w.new_tab) item.newTab = true;\n if (w.children && w.children.length > 0) {\n item.children = w.children.map(menuItemFromWire);\n }\n return item;\n}\n\nfunction menuFromWire(w: CmsMenuWire): RootTaleMenu {\n return {\n id: w.id,\n name: w.name,\n slug: w.slug,\n items: (w.items ?? []).map(menuItemFromWire),\n updatedAt: w.updated_at ?? null,\n };\n}\n\n/**\n * 위치 핸들 slug 로 네비게이션 메뉴 1개를 가져온다. admin \"디자인 > 메뉴\" 의\n * 저장값. 미설정/미존재(404)는 null — 사이트는 자체 fallback 네비를 렌더한다.\n * 구 서버(라우트 미배포)도 404 → null 로 동일하게 fail-soft.\n */\nexport async function fetchMenu(\n options: FetchMenuOptions,\n): Promise<RootTaleMenu | null> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams();\n if (options.siteId) params.set(\"site_id\", options.siteId);\n const qs = params.toString();\n const url = `${baseUrl}/v1/cms/public/menus/${encodeURIComponent(options.slug)}${qs ? `?${qs}` : \"\"}`;\n\n const response = await fetch(url, buildGetInit(options.apiKey, options));\n\n if (response.status === 404) return null;\n if (!response.ok) {\n throw await toApiError(response);\n }\n\n const json = (await response.json()) as CmsMenuWire;\n return menuFromWire(json);\n}\n\n/** 사이트의 네비게이션 메뉴 전체. 구 서버(라우트 미배포)는 404 — 빈 배열. */\nexport async function fetchMenus(\n options: FetchMenusOptions,\n): Promise<RootTaleMenu[]> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams();\n if (options.siteId) params.set(\"site_id\", options.siteId);\n const qs = params.toString();\n const url = `${baseUrl}/v1/cms/public/menus${qs ? `?${qs}` : \"\"}`;\n\n const response = await fetch(url, buildGetInit(options.apiKey, options));\n\n if (response.status === 404) return [];\n if (!response.ok) {\n throw await toApiError(response);\n }\n\n const json = (await response.json()) as { items: CmsMenuWire[] };\n return (json.items ?? []).map(menuFromWire);\n}\n\n// ── 커스텀 리다이렉트 (WP RankMath Redirections 패리티) ─────────────────────\n\nexport interface RootTaleRedirect {\n id: string;\n /** 정규화된 출발 경로 (앞 슬래시, 쿼리 제외). 예: `/old-event`. */\n fromPath: string;\n /** 도착지 — 사이트 내부 경로(`/promo`) 또는 절대 URL. */\n toTarget: string;\n /** 301 영구 / 302 임시. */\n statusCode: 301 | 302;\n}\n\nexport interface FetchRedirectsOptions {\n apiKey: string;\n baseUrl?: string;\n siteId?: string;\n signal?: AbortSignal;\n /** ISR revalidate 초(opt-in, W0-9). 미지정 시 현행 동작 유지. */\n revalidate?: number;\n}\n\ntype CmsRedirectWire = {\n id: string;\n from_path: string;\n to_target: string;\n status_code: number;\n};\n\n/**\n * 사이트의 활성 커스텀 리다이렉트 규칙 전체. 고객 사이트 middleware\n * (`createRedirectMiddleware`)가 요청 경로 매칭에 사용한다. 구 서버(라우트\n * 미배포)는 404 — 빈 배열로 fail-soft (트래픽을 막지 않음).\n */\nexport async function fetchRedirects(\n options: FetchRedirectsOptions,\n): Promise<RootTaleRedirect[]> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams();\n if (options.siteId) params.set(\"site_id\", options.siteId);\n const qs = params.toString();\n const url = `${baseUrl}/v1/cms/public/redirects${qs ? `?${qs}` : \"\"}`;\n\n const response = await fetch(url, buildGetInit(options.apiKey, options));\n\n if (response.status === 404) return [];\n if (!response.ok) {\n throw await toApiError(response);\n }\n\n const json = (await response.json()) as { items: CmsRedirectWire[] };\n return (json.items ?? []).map((w) => ({\n id: w.id,\n fromPath: w.from_path,\n toTarget: w.to_target,\n statusCode: (w.status_code === 302 ? 302 : 301) as 301 | 302,\n }));\n}\n","import { DEFAULT_BASE_URL, assertServer, buildGetInit, toApiError } from \"./server-base\";\n\nexport const EXPOSURE_CACHE_TAG = \"rt-exposures\";\n\nexport interface CmsExposureCampaign {\n id: string;\n slotKey: string;\n kind: \"banner\" | \"popup\";\n content: {\n variant: string;\n eyebrow?: string;\n title: string;\n description?: string;\n assetUrl?: string;\n assetAlt?: string;\n linkLabel?: string;\n linkUrl?: string;\n mobileAssetUrl?: string;\n mobileAssetAlt?: string;\n linkTarget?: \"self\" | \"blank\";\n };\n repeat: \"always\" | \"session\" | \"day\" | \"never\";\n endsAt?: string | null;\n delivery?: {\n version: 2;\n timeZone: string;\n delaySeconds: number;\n scrollPercent: 25 | 50 | 75 | null;\n dismissible: boolean;\n hideForToday: boolean;\n size: \"small\" | \"medium\" | \"large\";\n dayBoundary: \"calendar\" | \"rolling\";\n };\n}\n\nexport interface FetchExposureOptions {\n apiKey: string;\n baseUrl?: string;\n siteId?: string;\n slotKey: string;\n path: string;\n signal?: AbortSignal;\n revalidate?: number;\n tags?: string[];\n}\n\n/** 개발자가 선언한 슬롯과 현재 경로에 맞는 활성 캠페인 하나만 가져온다. */\nexport async function fetchExposure(\n options: FetchExposureOptions,\n): Promise<CmsExposureCampaign | null> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams({ slot_key: options.slotKey, path: options.path });\n if (options.siteId) params.set(\"site_id\", options.siteId);\n const response = await fetch(\n `${baseUrl}/v1/cms/public/exposures?${params.toString()}`,\n buildGetInit(options.apiKey, {\n ...options,\n // 레거시 RSC도 발행 웹훅을 실제로 받으며 긴 ISR에 묶이지 않는다.\n revalidate: Math.min(options.revalidate ?? 60, 60),\n tags: [...new Set([EXPOSURE_CACHE_TAG, ...(options.tags ?? [])])],\n }),\n );\n if (!response.ok) throw await toApiError(response);\n const json = (await response.json()) as {\n campaign?: {\n id: string;\n slot_key: string;\n kind: \"banner\" | \"popup\";\n content: {\n variant: string;\n eyebrow?: string;\n title: string;\n description?: string;\n asset_url?: string;\n asset_alt?: string;\n link_label?: string;\n link_url?: string;\n };\n repeat: CmsExposureCampaign[\"repeat\"];\n } | null;\n };\n const campaign = json.campaign;\n if (!campaign) return null;\n return {\n id: campaign.id,\n slotKey: campaign.slot_key,\n kind: campaign.kind,\n content: {\n variant: campaign.content.variant,\n ...(campaign.content.eyebrow ? { eyebrow: campaign.content.eyebrow } : {}),\n title: campaign.content.title,\n ...(campaign.content.description ? { description: campaign.content.description } : {}),\n ...(campaign.content.asset_url ? { assetUrl: campaign.content.asset_url } : {}),\n ...(campaign.content.asset_alt ? { assetAlt: campaign.content.asset_alt } : {}),\n ...(campaign.content.link_label ? { linkLabel: campaign.content.link_label } : {}),\n ...(campaign.content.link_url ? { linkUrl: campaign.content.link_url } : {}),\n },\n repeat: campaign.repeat,\n };\n}\n","import { DEFAULT_BASE_URL, assertServer, authHeaders, toApiError } from \"./server-base\";\nimport type { CmsExposureCampaign } from \"./server-exposure\";\n\nexport interface CmsExposureDecision {\n campaigns: CmsExposureCampaign[];\n evaluatedAt: string;\n nextChangeAt: string | null;\n validUntil: string;\n contractVersion: number | null;\n supportedFeatures: string[];\n}\n\nexport interface FetchExposureDecisionOptions {\n apiKey: string;\n baseUrl?: string;\n siteId?: string;\n slotKeys: readonly string[];\n path: string;\n device: \"desktop\" | \"mobile\";\n signal?: AbortSignal;\n}\n\nfunction object(value: unknown): Record<string, unknown> {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) throw new Error(\"Invalid exposure response\");\n return value as Record<string, unknown>;\n}\n\nfunction requiredString(value: unknown): string {\n if (typeof value !== \"string\" || value.length === 0) throw new Error(\"Invalid exposure response\");\n return value;\n}\n\nfunction timestamp(value: unknown): string {\n const result = requiredString(value);\n if (!Number.isFinite(Date.parse(result))) throw new Error(\"Invalid exposure timestamp\");\n return result;\n}\n\nfunction safeUrl(value: unknown, internal = true): string | undefined {\n if (value === undefined || value === null) return undefined;\n const text = requiredString(value);\n if (/[\\u0000-\\u0020\\u007f\\\\]/.test(text)) throw new Error(\"Invalid exposure URL\");\n if (internal && text.startsWith(\"/\") && !text.startsWith(\"//\")) return text;\n const url = new URL(text);\n if (url.protocol !== \"https:\" || url.username || url.password) throw new Error(\"Invalid exposure URL\");\n return text;\n}\n\nfunction campaignFromWire(value: unknown): CmsExposureCampaign {\n const row = object(value);\n const content = object(row.content);\n if (row.kind !== \"banner\" && row.kind !== \"popup\") throw new Error(\"Invalid exposure kind\");\n if (![\"always\", \"session\", \"day\", \"never\"].includes(String(row.repeat))) throw new Error(\"Invalid exposure repeat\");\n const result: CmsExposureCampaign = {\n id: requiredString(row.id), slotKey: requiredString(row.slot_key), kind: row.kind,\n repeat: row.repeat as CmsExposureCampaign[\"repeat\"],\n endsAt: row.ends_at == null ? null : timestamp(row.ends_at),\n content: { variant: requiredString(content.variant), title: requiredString(content.title),\n ...(typeof content.eyebrow === \"string\" ? { eyebrow: content.eyebrow } : {}),\n ...(typeof content.description === \"string\" ? { description: content.description } : {}),\n ...(typeof content.asset_alt === \"string\" ? { assetAlt: content.asset_alt } : {}),\n ...(typeof content.mobile_asset_alt === \"string\" ? { mobileAssetAlt: content.mobile_asset_alt } : {}),\n ...(typeof content.link_label === \"string\" ? { linkLabel: content.link_label } : {}),\n assetUrl: safeUrl(content.asset_url, false), mobileAssetUrl: safeUrl(content.mobile_asset_url, false),\n linkUrl: safeUrl(content.link_url), linkTarget: content.link_target === \"blank\" ? \"blank\" : \"self\",\n },\n };\n if (row.delivery != null) {\n const delivery = object(row.delivery);\n if (delivery.version !== 2 || typeof delivery.delay_seconds !== \"number\" ||\n delivery.delay_seconds < 0 || delivery.delay_seconds > 30 ||\n ![null, 25, 50, 75].includes(delivery.scroll_percent as number | null) ||\n typeof delivery.dismissible !== \"boolean\" || typeof delivery.hide_for_today !== \"boolean\" ||\n ![\"small\", \"medium\", \"large\"].includes(String(delivery.size)) ||\n ![\"calendar\", \"rolling\"].includes(String(delivery.day_boundary))) throw new Error(\"Invalid exposure delivery\");\n const timeZone = requiredString(delivery.time_zone);\n new Intl.DateTimeFormat(\"en\", { timeZone });\n result.delivery = { version: 2, timeZone, delaySeconds: delivery.delay_seconds,\n scrollPercent: delivery.scroll_percent as 25 | 50 | 75 | null,\n dismissible: delivery.dismissible, hideForToday: delivery.hide_for_today,\n size: delivery.size as \"small\" | \"medium\" | \"large\", dayBoundary: delivery.day_boundary as \"calendar\" | \"rolling\" };\n }\n return result;\n}\n\n/** 현재 시각·경로·기기의 최종 선택은 ISR/SWR 캐시 없이 매번 판정한다. */\nexport async function fetchExposureDecision(options: FetchExposureDecisionOptions): Promise<CmsExposureDecision> {\n assertServer();\n const params = new URLSearchParams({ slot_keys: options.slotKeys.join(\",\"), path: options.path, device: options.device });\n if (options.siteId) params.set(\"site_id\", options.siteId);\n const base = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const response = await fetch(`${base}/v1/cms/public/exposure-decisions?${params}`, {\n headers: authHeaders(options.apiKey), cache: \"no-store\", signal: options.signal,\n });\n if (!response.ok) throw await toApiError(response);\n const payload = object(await response.json());\n if (!Array.isArray(payload.campaigns) || payload.campaigns.length > 200) throw new Error(\"Invalid exposure campaigns\");\n const evaluatedAt = timestamp(payload.evaluated_at);\n const validUntil = timestamp(payload.valid_until);\n if (Date.parse(validUntil) <= Date.parse(evaluatedAt)) throw new Error(\"Expired exposure response\");\n return { campaigns: payload.campaigns.map(campaignFromWire), evaluatedAt, validUntil,\n nextChangeAt: payload.next_change_at == null ? null : timestamp(payload.next_change_at),\n contractVersion: typeof payload.contract_version === \"number\" ? payload.contract_version : null,\n supportedFeatures: Array.isArray(payload.supported_features) ? payload.supported_features.filter((item): item is string => typeof item === \"string\") : [],\n };\n}\n","import {\n DEFAULT_BASE_URL,\n assertServer,\n authHeaders,\n buildGetInit,\n toApiError,\n} from \"./server-base\";\n\nexport type SiteInquiryStatus = \"pending\" | \"answered\";\n\nexport interface SiteInquiryListItem {\n id: string;\n title: string;\n category: string | null;\n authorMasked: string | null;\n status: SiteInquiryStatus | null;\n isSecret: boolean;\n locked: boolean;\n createdAt: string | null;\n}\n\nexport interface SiteInquiryListPage {\n items: SiteInquiryListItem[];\n hasMore: boolean;\n nextCursor: string | null;\n}\n\nexport interface SiteInquiryDetail {\n id: string;\n title: string;\n category: string | null;\n authorMasked: string;\n status: SiteInquiryStatus;\n isSecret: boolean;\n body: string;\n answerBody: string | null;\n answeredAt: string | null;\n createdAt: string;\n}\n\nexport interface FetchSiteInquiriesOptions {\n apiKey: string;\n baseUrl?: string;\n siteId?: string;\n limit?: number;\n cursor?: string;\n signal?: AbortSignal;\n revalidate?: number;\n}\n\nexport interface FetchSiteInquiryOptions {\n apiKey: string;\n baseUrl?: string;\n siteId?: string;\n id: string;\n password?: string;\n signal?: AbortSignal;\n revalidate?: number;\n}\n\nexport interface SubmitSiteInquiryFields {\n siteId?: string;\n category?: string;\n title: string;\n body: string;\n authorName: string;\n contact?: string;\n isSecret: boolean;\n secretPassword?: string;\n privacyConsent: boolean;\n}\n\nexport interface SubmitSiteInquiryOptions {\n apiKey: string;\n baseUrl?: string;\n fields: SubmitSiteInquiryFields;\n signal?: AbortSignal;\n}\n\nexport interface SubmitSiteInquiryResult {\n id: string;\n status: SiteInquiryStatus;\n}\n\ntype SiteInquiryListItemWire = {\n id: string;\n title: string;\n category: string | null;\n author_masked: string | null;\n status: SiteInquiryStatus | null;\n is_secret: boolean;\n locked: boolean;\n created_at: string | null;\n};\n\ntype SiteInquiryDetailWire = {\n id: string;\n title: string;\n category: string | null;\n author_masked: string;\n status: SiteInquiryStatus;\n is_secret: boolean;\n body: string;\n answer_body: string | null;\n answered_at: string | null;\n created_at: string;\n};\n\nexport async function fetchSiteInquiries(\n options: FetchSiteInquiriesOptions,\n): Promise<SiteInquiryListPage> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams();\n if (options.limit !== undefined) params.set(\"limit\", String(options.limit));\n if (options.cursor) params.set(\"cursor\", options.cursor);\n if (options.siteId) params.set(\"site_id\", options.siteId);\n const qs = params.toString();\n const url = `${baseUrl}/v1/cms/public/inquiries${qs ? `?${qs}` : \"\"}`;\n const response = await fetch(url, buildGetInit(options.apiKey, options));\n if (!response.ok) throw await toApiError(response);\n const json = (await response.json()) as {\n items: SiteInquiryListItemWire[];\n has_more: boolean;\n next_cursor: string | null;\n };\n return {\n items: json.items.map(fromListWire),\n hasMore: json.has_more,\n nextCursor: json.next_cursor,\n };\n}\n\nexport async function fetchSiteInquiry(\n options: FetchSiteInquiryOptions,\n): Promise<SiteInquiryDetail | null> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams();\n if (options.siteId) params.set(\"site_id\", options.siteId);\n if (options.password) params.set(\"password\", options.password);\n const qs = params.toString();\n const url = `${baseUrl}/v1/cms/public/inquiries/${encodeURIComponent(options.id)}${qs ? `?${qs}` : \"\"}`;\n const response = await fetch(url, buildGetInit(options.apiKey, options));\n if (response.status === 404) return null;\n if (!response.ok) throw await toApiError(response);\n return fromDetailWire((await response.json()) as SiteInquiryDetailWire);\n}\n\nexport async function submitSiteInquiry(\n options: SubmitSiteInquiryOptions,\n): Promise<SubmitSiteInquiryResult> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const fields = options.fields;\n const response = await fetch(`${baseUrl}/v1/cms/public/inquiries`, {\n method: \"POST\",\n headers: {\n ...authHeaders(options.apiKey),\n \"content-type\": \"application/json\",\n },\n body: JSON.stringify({\n site_id: fields.siteId,\n category: fields.category,\n title: fields.title,\n body: fields.body,\n author_name: fields.authorName,\n contact: fields.contact,\n is_secret: fields.isSecret,\n secret_password: fields.secretPassword,\n privacy_consent: fields.privacyConsent,\n }),\n signal: options.signal,\n });\n if (!response.ok) throw await toApiError(response);\n return (await response.json()) as SubmitSiteInquiryResult;\n}\n\nfunction fromListWire(item: SiteInquiryListItemWire): SiteInquiryListItem {\n return {\n id: item.id,\n title: item.title,\n category: item.category,\n authorMasked: item.author_masked,\n status: item.status,\n isSecret: item.is_secret,\n locked: item.locked,\n createdAt: item.created_at,\n };\n}\n\nfunction fromDetailWire(item: SiteInquiryDetailWire): SiteInquiryDetail {\n return {\n id: item.id,\n title: item.title,\n category: item.category,\n authorMasked: item.author_masked,\n status: item.status,\n isSecret: item.is_secret,\n body: item.body,\n answerBody: item.answer_body,\n answeredAt: item.answered_at,\n createdAt: item.created_at,\n };\n}\n","import { inquiryFormDefinitionSchema, type InquiryAnswer, type InquiryFormDefinition } from \"@roottale/inquiry-forms\";\n\nimport type { InquiryAttribution } from \"./attribution\";\nimport { CmsApiError, DEFAULT_BASE_URL, assertServer, authHeaders, toApiError } from \"./server-base\";\n\nexport type { InquiryKind, InquiryFormDefinition, InquiryFormField, InquiryAnswer } from \"@roottale/inquiry-forms\";\n\n/** A published form belongs to exactly one site. Versions are immutable. */\nexport interface PublishedInquiryForm {\n formId: string;\n siteId: string;\n formKey: string;\n version: number;\n definition: InquiryFormDefinition;\n}\n\nexport interface FetchInquiryFormOptions {\n apiKey: string;\n baseUrl?: string;\n /** Required for a tenant-wide key; a site-bound key already identifies its site. */\n siteId?: string;\n formId: string;\n signal?: AbortSignal;\n}\n\nexport interface SubmitFormInquiryFields {\n siteId?: string;\n /** Version that the visitor saw, including its consent notice. */\n version: number;\n answers: Record<string, InquiryAnswer>;\n privacyConsent: boolean;\n /** Caller-generated key. Reuse the same key and payload after an uncertain response. */\n idempotencyKey: string;\n placement?: string;\n turnstileToken?: string;\n attribution?: InquiryAttribution | null;\n /** readJourney() array; the API validates entries and enforces the site's privacy mode. */\n journey?: readonly unknown[];\n}\n\nexport interface SubmitFormInquiryOptions {\n apiKey: string;\n baseUrl?: string;\n formId: string;\n fields: SubmitFormInquiryFields;\n signal?: AbortSignal;\n}\n\nexport interface FormInquiryReceipt {\n id: string;\n inquiryNo: number;\n receivedAt: string;\n /** True when this is the receipt of an already saved submission. */\n replayed: boolean;\n}\n\nexport type SubmitFormInquiryResult =\n | { ok: true; receipt: FormInquiryReceipt }\n | {\n ok: false;\n /** Transport/invalid_response can mean the server saved the inquiry before the response was lost. */\n kind: \"api\" | \"transport\" | \"invalid_response\";\n code: string;\n status: number | null;\n message: string;\n fieldErrors: Record<string, string[]>;\n formErrors: string[];\n };\n\n/** Server-only. Always reads the current published form; 404 returns null. */\nexport async function fetchInquiryForm(options: FetchInquiryFormOptions): Promise<PublishedInquiryForm | null> {\n assertServer();\n const params = new URLSearchParams();\n if (options.siteId) params.set(\"site_id\", options.siteId);\n const query = params.toString();\n const response = await fetch(`${formUrl(options)}${query ? `?${query}` : \"\"}`, {\n headers: authHeaders(options.apiKey),\n cache: \"no-store\",\n redirect: \"manual\",\n signal: options.signal,\n });\n if (response.status === 404) return null;\n if (!response.ok) throw await toApiError(response);\n\n const body = await readJson(response);\n if (!isRecord(body) || body.form_id !== options.formId || !isId(body.site_id)\n || (options.siteId !== undefined && body.site_id !== options.siteId)\n || !isNonEmptyString(body.form_key) || !isPositiveInteger(body.version)) {\n throw invalidFormResponse(response.status);\n }\n const definition = inquiryFormDefinitionSchema.safeParse(body.definition);\n if (!definition.success) throw invalidFormResponse(response.status);\n return {\n formId: options.formId,\n siteId: body.site_id,\n formKey: body.form_key,\n version: body.version,\n definition: definition.data,\n };\n}\n\n/**\n * Server-only JSON intake. A successful HTTP status is insufficient without a valid receipt.\n * Does not retry or generate keys; the caller keeps its key until the receipt is confirmed.\n */\nexport async function submitFormInquiry(options: SubmitFormInquiryOptions): Promise<SubmitFormInquiryResult> {\n assertServer();\n const fields = options.fields;\n let response: Response;\n try {\n response = await fetch(`${formUrl(options)}/submissions`, {\n method: \"POST\",\n headers: { ...authHeaders(options.apiKey), \"content-type\": \"application/json\" },\n body: JSON.stringify({\n site_id: fields.siteId,\n version: fields.version,\n answers: fields.answers,\n privacy_consent: fields.privacyConsent,\n idempotency_key: fields.idempotencyKey,\n placement: fields.placement,\n turnstile_token: fields.turnstileToken,\n attribution: fields.attribution ?? undefined,\n journey: fields.journey,\n }),\n cache: \"no-store\",\n redirect: \"manual\",\n signal: options.signal,\n });\n } catch {\n return failure(\"transport\", \"network_error\", null,\n \"접수 결과를 확인하지 못했습니다. 같은 내용으로 다시 시도해주세요.\");\n }\n\n const body = await readJson(response);\n if (!response.ok) {\n const code = isRecord(body) && isNonEmptyString(body.code) ? body.code : codeForStatus(response.status);\n const message = isRecord(body) && isNonEmptyString(body.message)\n ? body.message : \"문의 전송에 실패했습니다. 입력 내용과 연결 상태를 확인해주세요.\";\n const details = isRecord(body) && isRecord(body.details) ? body.details : undefined;\n return {\n ...failure(\"api\", code, response.status, message),\n fieldErrors: parseFieldErrors(details?.field_errors),\n formErrors: stringArray(details?.form_errors),\n };\n }\n\n if (!isRecord(body) || !isId(body.id) || !isPositiveInteger(body.inquiry_no)\n || !isTimestamp(body.received_at) || typeof body.replayed !== \"boolean\") {\n return failure(\"invalid_response\", \"invalid_response\", response.status,\n \"접수증을 확인하지 못했습니다. 같은 내용으로 다시 시도해주세요.\");\n }\n return {\n ok: true,\n receipt: { id: body.id, inquiryNo: body.inquiry_no, receivedAt: body.received_at, replayed: body.replayed },\n };\n}\n\nfunction formUrl(options: { baseUrl?: string; formId: string }): string {\n const base = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n return `${base}/v1/public/inquiry-forms/${encodeURIComponent(options.formId)}`;\n}\n\nfunction failure(\n kind: \"api\" | \"transport\" | \"invalid_response\", code: string, status: number | null, message: string,\n): Extract<SubmitFormInquiryResult, { ok: false }> {\n return { ok: false, kind, code, status, message, fieldErrors: {}, formErrors: [] };\n}\n\nfunction invalidFormResponse(status: number): CmsApiError {\n return new CmsApiError(\"RootTale inquiry form response is invalid\", status, \"invalid_response\");\n}\n\nasync function readJson(response: Response): Promise<unknown> {\n try { return await response.json(); } catch { return null; }\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction isNonEmptyString(value: unknown): value is string {\n return typeof value === \"string\" && value.trim().length > 0;\n}\n\nfunction isId(value: unknown): value is string {\n return typeof value === \"string\" && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);\n}\n\nfunction isPositiveInteger(value: unknown): value is number {\n return typeof value === \"number\" && Number.isSafeInteger(value) && value > 0;\n}\n\nfunction isTimestamp(value: unknown): value is string {\n return typeof value === \"string\"\n && /^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?(?:Z|[+-]\\d{2}:\\d{2})$/.test(value)\n && Number.isFinite(Date.parse(value));\n}\n\nfunction stringArray(value: unknown): string[] {\n return Array.isArray(value) ? value.filter((item): item is string => typeof item === \"string\") : [];\n}\n\nfunction parseFieldErrors(value: unknown): Record<string, string[]> {\n if (!isRecord(value)) return {};\n return Object.fromEntries(Object.entries(value).map(([key, errors]) => [key, stringArray(errors)]));\n}\n\nfunction codeForStatus(status: number): string {\n if (status === 401) return \"invalid_api_key\";\n if (status === 403) return \"insufficient_scope\";\n if (status === 404) return \"not_found\";\n if (status === 429) return \"rate_limited\";\n return \"request_failed\";\n}\n","import {\n type CmsPostContent,\n CmsApiError,\n DEFAULT_BASE_URL,\n assertServer,\n buildGetInit,\n toApiError,\n} from \"./server-base\";\nimport { type CmsPostWire, postFromWire } from \"./server-post-wire\";\n\n/**\n * 글 미리보기 — 어드민 편집기가 발급한 토큰으로 초안을 **발행 글과 같은 모양**으로\n * 받아 온다. 고객 사이트는 글 상세 템플릿을 그대로 재사용해 미리보기를 그리면\n * 된다(어드민이 대신 그리던 미리보기는 사이트 폰트·너비·목차와 달랐다).\n *\n * - 응답은 서버가 `cache-control: no-store` 로 보내고, 이 함수도 `cache: \"no-store\"`\n * 로 요청한다. 미리보기 페이지는 `export const dynamic = \"force-dynamic\"` 으로 두라.\n * - 토큰은 글 1건·1시간짜리. 만료되면 `CmsApiError`(status 410, code\n * `preview_expired`) 를 던진다 — `isPreviewExpiredError` 로 구분해 안내 문구를 보인다.\n */\nexport interface FetchPostPreviewOptions {\n /** `rtlk_cust_*` API key. Required. */\n readonly apiKey: string;\n /** Override the API base URL. Defaults to `https://api.roottale.com`. */\n readonly baseUrl?: string;\n /** 어드민 '미리보기'·'공유 링크' 가 URL `?token=` 으로 넘긴 토큰. */\n readonly token: string;\n readonly siteId?: string;\n readonly signal?: AbortSignal;\n}\n\nexport interface CmsPostPreviewInfo {\n /** 토큰 만료 시각(ISO). */\n readonly expiresAt: string;\n /** 글의 실제 상태. 본문 `status` 는 템플릿 재사용을 위해 항상 `published`. */\n readonly sourceStatus:\n | \"draft\"\n | \"pending\"\n | \"scheduled\"\n | \"published\"\n | \"private\"\n | \"trash\";\n}\n\nexport interface CmsPostPreviewContent extends CmsPostContent {\n readonly preview: CmsPostPreviewInfo;\n}\n\ntype CmsPostPreviewWire = CmsPostWire & {\n preview: { expires_at: string; source_status: CmsPostPreviewInfo[\"sourceStatus\"] };\n};\n\n/** 만료된 미리보기 링크(410 `preview_expired`)인지 구분한다. */\nexport function isPreviewExpiredError(error: unknown): boolean {\n return (\n error instanceof CmsApiError &&\n (error.code === \"preview_expired\" || error.status === 410)\n );\n}\n\n/**\n * 토큰이 없거나 다른 사이트의 것이면 `null`(404). 만료는 throw(410) — 두 경우를\n * 다르게 안내할 수 있게 구분한다.\n */\nexport async function fetchPostPreview(\n options: FetchPostPreviewOptions,\n): Promise<CmsPostPreviewContent | null> {\n assertServer();\n const token = options.token.trim();\n if (!token) return null;\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams({ token });\n if (options.siteId) params.set(\"site_id\", options.siteId);\n const url = `${baseUrl}/v1/cms/public/posts/preview?${params.toString()}`;\n\n const response = await fetch(url, {\n ...buildGetInit(options.apiKey, { signal: options.signal }),\n cache: \"no-store\",\n });\n\n if (response.status === 404) return null;\n if (!response.ok) {\n throw await toApiError(response);\n }\n\n const json = (await response.json()) as CmsPostPreviewWire;\n return {\n ...postFromWire(json),\n preview: {\n expiresAt: json.preview.expires_at,\n sourceStatus: json.preview.source_status,\n },\n };\n}\n","import { DEFAULT_BASE_URL, assertServer, buildGetInit, toApiError } from \"./server-base\";\n\n/** 블록 표시 형태 — 어드민이 정한 모양. 사이트는 값을 받아 그리기만 한다. */\nexport interface CmsSitePatternPresentation {\n /** `plain` = 본문처럼, `card` = 배경색 있는 카드. */\n layout: \"plain\" | \"card\";\n /** 카드 배경색(hex). */\n background: string;\n /** 마지막 문단의 링크를 버튼으로. */\n linkStyle: \"text\" | \"buttons\";\n /** 버튼 배경색 순서대로(글자는 흰색). 링크가 더 많으면 마지막 색 반복. */\n buttonColors: string[];\n}\n\nexport const DEFAULT_SITE_PATTERN_PRESENTATION: CmsSitePatternPresentation = {\n layout: \"plain\",\n background: \"#f8f9fa\",\n linkStyle: \"text\",\n buttonColors: [\"#03c75a\", \"#1a1a1a\", \"#666666\"],\n};\n\n/** 공통 블록 목록 fetch 캐시 tag — `theme.updated` 웹훅이 설정류와 함께 지운다. */\nexport const SITE_PATTERNS_CACHE_TAG = \"rt-site-patterns\";\n\n/** 글 하단 자리 key — 글 응답 `patternSlots` 와 어드민 배치 규칙이 같은 이름을 쓴다. */\nexport const POST_FOOTER_PATTERN_SLOT = \"post_footer\";\n\n/**\n * 공통 블록(Site Pattern) — 어드민 콘텐츠 도구 > 공통 블록에서 발행한 재사용 본문.\n * 어느 글의 어느 자리에 놓을지는 글 응답 `patternSlots` 가 이미 말해 주므로 사이트는\n * `patternSlots[slot]` 로 이 목록에서 key 를 찾아 글 본문과 같은 렌더러로 그리면 된다.\n */\nexport interface CmsSitePattern {\n id: string;\n key: string;\n name: string;\n description: string | null;\n /** Tiptap 문서 JSON. 글 본문(`bodyJson`)과 같은 렌더·정화 경로로 그린다. */\n bodyJson: Record<string, unknown>;\n /** 표시 형태(구 서버는 기본값). */\n presentation: CmsSitePatternPresentation;\n /** ISO 8601. */\n updatedAt: string;\n}\n\nexport interface FetchSitePatternsOptions {\n apiKey: string;\n baseUrl?: string;\n siteId?: string;\n signal?: AbortSignal;\n /** ISR revalidate 초(opt-in). 미지정 시 현행 동작 유지. */\n revalidate?: number;\n /** on-demand 무효화용 Next fetch cache tags(opt-in). 보통 `[SITE_PATTERNS_CACHE_TAG]`. */\n tags?: string[];\n}\n\n/**\n * 발행된 공통 블록 전체를 가져온다. 네트워크/서버 에러는 throw — 호출자가 빈 배열로\n * fail-soft 한다(블록이 없다고 글이 안 열리면 안 된다).\n */\nexport async function fetchSitePatterns(\n options: FetchSitePatternsOptions,\n): Promise<CmsSitePattern[]> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams();\n if (options.siteId) params.set(\"site_id\", options.siteId);\n const qs = params.toString();\n const url = `${baseUrl}/v1/cms/public/patterns${qs ? `?${qs}` : \"\"}`;\n\n const response = await fetch(url, buildGetInit(options.apiKey, options));\n if (!response.ok) throw await toApiError(response);\n\n const json = (await response.json()) as { patterns?: unknown };\n const items = Array.isArray(json.patterns) ? json.patterns : [];\n return items.flatMap((item) => {\n const pattern = sitePatternFromWire(item);\n return pattern ? [pattern] : [];\n });\n}\n\nfunction sitePatternFromWire(raw: unknown): CmsSitePattern | null {\n if (!raw || typeof raw !== \"object\" || Array.isArray(raw)) return null;\n const o = raw as Record<string, unknown>;\n if (typeof o.id !== \"string\" || typeof o.key !== \"string\" || o.key.length === 0) return null;\n if (typeof o.name !== \"string\") return null;\n const bodyJson = o.body_json;\n if (!bodyJson || typeof bodyJson !== \"object\" || Array.isArray(bodyJson)) return null;\n return {\n id: o.id,\n key: o.key,\n name: o.name,\n description: typeof o.description === \"string\" ? o.description : null,\n bodyJson: bodyJson as Record<string, unknown>,\n presentation: presentationFromWire(o.presentation),\n updatedAt: typeof o.updated_at === \"string\" ? o.updated_at : \"\",\n };\n}\n\nconst HEX_RE = /^#[0-9a-f]{6}$/;\n\nfunction presentationFromWire(raw: unknown): CmsSitePatternPresentation {\n const base = DEFAULT_SITE_PATTERN_PRESENTATION;\n if (!raw || typeof raw !== \"object\") return { ...base, buttonColors: [...base.buttonColors] };\n const o = raw as Record<string, unknown>;\n const colors = Array.isArray(o.button_colors)\n ? o.button_colors.filter((c): c is string => typeof c === \"string\" && HEX_RE.test(c))\n : [];\n return {\n layout: o.layout === \"card\" ? \"card\" : \"plain\",\n background: typeof o.background === \"string\" && HEX_RE.test(o.background) ? o.background : base.background,\n linkStyle: o.link_style === \"buttons\" ? \"buttons\" : \"text\",\n buttonColors: colors.length > 0 ? colors : [...base.buttonColors],\n };\n}\n\n/**\n * 표시 형태 → 래퍼 요소에 얹을 data 속성과 CSS 변수. 사이트 CSS 는 이 변수만 읽는다:\n * `[data-pattern-layout=\"card\"]`, `[data-pattern-link-style=\"buttons\"]`, `--rt-pattern-bg`,\n * `--rt-pattern-btn-1..n`(버튼 n번째 색).\n */\nexport function sitePatternPresentationAttributes(presentation: CmsSitePatternPresentation): {\n dataset: Record<string, string>;\n style: Record<string, string>;\n} {\n const style: Record<string, string> = { \"--rt-pattern-bg\": presentation.background };\n presentation.buttonColors.forEach((color, index) => {\n style[`--rt-pattern-btn-${index + 1}`] = color;\n });\n return {\n dataset: {\n \"data-pattern-layout\": presentation.layout,\n \"data-pattern-link-style\": presentation.linkStyle,\n },\n style,\n };\n}\n\n/**\n * 글의 `patternSlots` 와 블록 목록으로 그 자리에 놓을 블록을 고른다. 자리가 비었거나\n * (null·구버전 응답) key 가 목록에 없으면(미발행·삭제) null.\n */\nexport function selectSitePatternForSlot(\n patternSlots: Readonly<Record<string, string | null>> | null | undefined,\n patterns: readonly CmsSitePattern[],\n slot: string = POST_FOOTER_PATTERN_SLOT,\n): CmsSitePattern | null {\n const key = patternSlots?.[slot] ?? null;\n if (!key) return null;\n return patterns.find((pattern) => pattern.key === key) ?? null;\n}\n","import { MetadataRobotsPolicySchema, MetadataValuesSchema, type PublicRouteMetadata } from \"@roottale/cms-core\";\nimport { CmsApiError, DEFAULT_BASE_URL, assertServer, buildGetInit, toApiError } from \"./server-base\";\n\nexport { readMetadataValues, resolveMetadata, formatMetadataDefaults, metadataBodyParagraphs, metadataContentImageUrl, MetadataDefaultRulesSchema } from \"@roottale/cms-core\";\nexport type { MetadataField, MetadataValues, MetadataPatch, PublicRouteMetadata, MetadataResolutionInput, MetadataRobotsPolicy, MetadataDefaultsDeclaration, MetadataDefaultsInput, MetadataDefaultRules, MetadataTextRule } from \"@roottale/cms-core\";\n\nexport const METADATA_CACHE_TAG = \"rt-metadata\";\nexport const SITE_METADATA_DEFAULTS_CACHE_TAG = \"rt-metadata-defaults\";\n/** Add to public post/term fetches whose fields determine a declared content policy. */\nexport const CONTENT_POLICY_CACHE_TAG = \"rt-content-policy\";\n\n/** locale를 생략한 요청은 사이트 기본 locale을 조회한다. */\nexport function routeMetadataCacheTag(routeKey: string, locale?: string): string {\n if (!/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(routeKey) || routeKey.length > 160) throw new Error(\"Invalid metadata route key\");\n const language = locale?.trim().toLowerCase() || \"default\";\n if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(language) || language.length > 40) {\n throw new Error(\"Invalid metadata locale\");\n }\n return `${METADATA_CACHE_TAG}:${routeKey}:${language}`;\n}\n\nexport interface FetchRouteMetadataOptions {\n apiKey: string;\n baseUrl?: string;\n siteId?: string;\n routeKey: string;\n locale?: string;\n signal?: AbortSignal;\n /** 기본 300초. 웹훅 유실 시 재조회하며, 0이면 항상 최신값을 읽는다. */\n revalidate?: number;\n}\n\n/**\n * 고유 route key 직접 조회. 등록되지 않은 경로만 null이며, 연결된 빈 설정은\n * overrides:{}인 객체다. HTTP/네트워크/응답 오류는 throw하여 빈 설정으로 캐시하지 않는다.\n * 공개 읽기는 구독 등급을 검사하지 않아 요금제 하향 후에도 기존 설정을 유지한다.\n */\nexport async function fetchRouteMetadata(options: FetchRouteMetadataOptions): Promise<PublicRouteMetadata | null> {\n assertServer();\n const routeTag = routeMetadataCacheTag(options.routeKey, options.locale);\n const params = new URLSearchParams({ route_key: options.routeKey });\n if (options.locale) params.set(\"locale\", options.locale);\n if (options.siteId) params.set(\"site_id\", options.siteId);\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const response = await fetch(`${baseUrl}/v1/cms/public/metadata?${params}`, buildGetInit(options.apiKey, {\n signal: options.signal,\n revalidate: options.revalidate ?? 300,\n tags: [METADATA_CACHE_TAG, SITE_METADATA_DEFAULTS_CACHE_TAG, routeTag],\n }));\n if (!response.ok) throw await toApiError(response);\n const json: unknown = await response.json();\n if (!json || typeof json !== \"object\" || !(\"metadata\" in json)) throw unexpectedResponse(response.status);\n if (json.metadata === null) return null;\n if (!isRouteMetadata(json.metadata) || json.metadata.routeKey !== options.routeKey) throw unexpectedResponse(response.status);\n return json.metadata;\n}\n\nfunction isValues(value: unknown): value is Record<string, unknown> {\n return MetadataValuesSchema.safeParse(value).success;\n}\n\nfunction isRouteMetadata(value: unknown): value is PublicRouteMetadata {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) return false;\n const row = value as Record<string, unknown>;\n return typeof row.routeKey === \"string\" && typeof row.locale === \"string\" &&\n typeof row.path === \"string\" && row.path.startsWith(\"/\") && !row.path.startsWith(\"//\") &&\n typeof row.version === \"string\" && isValues(row.overrides) && isValues(row.defaults) &&\n isValues(row.effective) && (row.fallback === undefined || isValues(row.fallback)) &&\n (row.robots === undefined || MetadataRobotsPolicySchema.safeParse(row.robots).success) &&\n Array.isArray(row.supportedFields) &&\n row.supportedFields.every((field) => [\"title\", \"description\", \"ogImage\", \"ogImageAlt\", \"noindex\", \"nofollow\"].includes(field));\n}\n\nfunction unexpectedResponse(status: number): CmsApiError {\n return new CmsApiError(\"RootTale CMS API returned invalid route metadata\", status, \"unexpected_response\");\n}\n","function canonicalize(value: unknown): unknown {\n if (Array.isArray(value)) return value.map(canonicalize);\n if (!value || typeof value !== \"object\") return value;\n return Object.fromEntries(Object.entries(value)\n .sort(([left], [right]) => left.localeCompare(right))\n .map(([key, child]) => [key, canonicalize(child)]));\n}\n\n/**\n * 배포 도구가 정규화해 등록한 manifest의 SHA-256(64자리 hex).\n * 이 함수는 스키마 검사·기본값 채우기를 하지 않는다. 원본 JSON을 그대로 해시하면\n * 등록 API가 정규화한 계약과 달라질 수 있으므로 배포 시 정규화한 manifest를 사용한다.\n */\nexport async function metadataContractChecksum(normalizedContract: unknown): Promise<string> {\n const json = JSON.stringify(canonicalize(normalizedContract));\n if (json === undefined) throw new Error(\"Metadata contract must be a normalized JSON value\");\n const digest = await globalThis.crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(json));\n return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, \"0\")).join(\"\");\n}\n","/**\n * 설정류 fetch 캐시 tag 한 벌 (ADR-0096 Phase C).\n *\n * 개별 tag 상수는 각자의 fetch 함수 옆에 산다 — `THEME_CACHE_TAG`\n * (server-posts-theme) · `BUSINESS_CACHE_TAG`(server-business) ·\n * `MENUS_CACHE_TAG`(server-discovery) · `BLOG_SETTINGS_CACHE_TAG` ·\n * `COLLECTIONS_CACHE_TAG`(server-blog). 본 모듈은 그것들을 **묶기만** 한다:\n * 어느 도메인 모듈에 배열을 두어도 나머지 셋을 import 해야 해서 모듈 방향이\n * 꼬이기 때문이다(여기서 한 방향으로만 import 한다).\n */\n\nimport { BLOG_SETTINGS_CACHE_TAG, COLLECTIONS_CACHE_TAG } from \"./server-blog\";\nimport { BUSINESS_CACHE_TAG } from \"./server-business\";\nimport { MENUS_CACHE_TAG } from \"./server-discovery\";\nimport { THEME_CACHE_TAG } from \"./server-posts-theme\";\nimport { EXPOSURE_CACHE_TAG } from \"./server-exposure\";\nimport { SITE_PATTERNS_CACHE_TAG } from \"./server-patterns\";\nimport { METADATA_CACHE_TAG, SITE_METADATA_DEFAULTS_CACHE_TAG } from \"./server-metadata\";\n\n/**\n * 어드민 \"설정\" 저장으로 낡는 fetch 캐시 tag 전부 — 테마·사업장 정보·메뉴·\n * 블로그 표시 설정·콘텐츠 유형·노출 캠페인·공통 블록.\n *\n * 수신측(`createRevalidateRoute`)이 `theme.updated` 한 번에 이 목록을 통째로\n * 무효화한다. 사이트가 이 중 일부 fetch 만 쓰더라도 안 쓰는 tag 의\n * `revalidateTag` 는 no-op 이므로 해가 없다 — 그래서 \"설정을 저장했다\" 신호\n * 하나로 설정류 전체를 지우는 것이 안전한 기본값이다.\n */\nexport const SETTINGS_CACHE_TAGS: readonly string[] = [\n THEME_CACHE_TAG,\n BUSINESS_CACHE_TAG,\n MENUS_CACHE_TAG,\n BLOG_SETTINGS_CACHE_TAG,\n COLLECTIONS_CACHE_TAG,\n EXPOSURE_CACHE_TAG,\n SITE_PATTERNS_CACHE_TAG,\n METADATA_CACHE_TAG,\n SITE_METADATA_DEFAULTS_CACHE_TAG,\n];\n","// 프레임워크 무관 SEO 헬퍼 — RSS 2.0 피드 + Schema.org JSON-LD 빌더.\n// 외부 사이트(Next route handler / Astro endpoint / RSC)가 fetchPosts 결과로\n// /feed.xml 응답이나 <head> JSON-LD 를 만들 때 사용. 순수 함수(네트워크 없음).\n\nimport type {\n CmsPostContent,\n RootTaleBusinessProfile,\n SiteVerificationSettings,\n} from \"./server\";\n\n// ── RSS 2.0 ────────────────────────────────────────────────────────────────\n\nexport interface RssItemInput {\n title: string;\n /** 글 절대 URL. */\n link: string;\n /** 고유 식별자. 미지정 시 link 사용. */\n guid?: string;\n description?: string | null;\n /** 발행 시각. */\n pubDate?: string | Date | null;\n}\n\nexport interface RssFeedInput {\n /** 사이트/피드 제목. */\n title: string;\n /** 사이트 홈 URL. */\n link: string;\n /** 이 피드 자체의 URL (atom:self). */\n feedUrl: string;\n /** WebSub hub 주소. 지정하면 atom:hub discovery link를 낸다. */\n hubUrl?: string;\n description?: string;\n /** 기본 \"ko-KR\". */\n language?: string;\n items: RssItemInput[];\n}\n\n/** RSS 2.0 XML 문자열 생성. */\nexport function generateRssXml(input: RssFeedInput): string {\n const lang = input.language ?? \"ko-KR\";\n const lastBuild =\n input.items.length > 0 ? toRfc822(input.items[0]!.pubDate) : \"\";\n const lines: string[] = [];\n lines.push('<?xml version=\"1.0\" encoding=\"UTF-8\"?>');\n lines.push(\n '<rss version=\"2.0\" xmlns:atom=\"http://www.w3.org/2005/Atom\" xmlns:content=\"http://purl.org/rss/1.0/modules/content/\">',\n );\n lines.push(\"<channel>\");\n lines.push(`<title>${escapeXml(input.title)}</title>`);\n lines.push(`<link>${escapeXml(input.link)}</link>`);\n lines.push(\n `<atom:link href=\"${escapeXml(input.feedUrl)}\" rel=\"self\" type=\"application/rss+xml\" />`,\n );\n if (input.hubUrl) {\n lines.push(\n `<atom:link href=\"${escapeXml(input.hubUrl)}\" rel=\"hub\" />`,\n );\n }\n lines.push(\n `<description>${escapeXml(input.description ?? input.title)}</description>`,\n );\n lines.push(`<language>${escapeXml(lang)}</language>`);\n if (lastBuild) lines.push(`<lastBuildDate>${lastBuild}</lastBuildDate>`);\n for (const item of input.items) {\n const guid = item.guid ?? item.link;\n lines.push(\"<item>\");\n lines.push(`<title>${escapeXml(item.title)}</title>`);\n lines.push(`<link>${escapeXml(item.link)}</link>`);\n lines.push(`<guid isPermaLink=\"false\">${escapeXml(guid)}</guid>`);\n if (item.description) {\n lines.push(`<description>${cdata(item.description)}</description>`);\n }\n const pub = toRfc822(item.pubDate);\n if (pub) lines.push(`<pubDate>${pub}</pubDate>`);\n lines.push(\"</item>\");\n }\n lines.push(\"</channel>\");\n lines.push(\"</rss>\");\n return lines.join(\"\\n\");\n}\n\n/** CmsPostContent[] → RSS item[]. postUrl 로 글 절대 URL 생성. */\nexport function rssItemsFromPosts(\n posts: CmsPostContent[],\n opts: { postUrl: (post: CmsPostContent) => string },\n): RssItemInput[] {\n return posts.map((p) => ({\n title: p.title,\n link: opts.postUrl(p),\n guid: opts.postUrl(p),\n description: p.excerpt,\n pubDate: p.publishedAt,\n }));\n}\n\nfunction toRfc822(value: string | Date | null | undefined): string {\n if (!value) return \"\";\n const d = value instanceof Date ? value : new Date(value);\n return Number.isNaN(d.getTime()) ? \"\" : d.toUTCString();\n}\n\n// ── Schema.org JSON-LD 빌더 ─────────────────────────────────────────────────\n\ntype JsonLd = Record<string, unknown>;\n\nexport interface ArticleSchemaInput {\n headline: string;\n url: string;\n description?: string | null;\n image?: string | null;\n datePublished?: string | Date | null;\n dateModified?: string | Date | null;\n authorName?: string | null;\n publisherName?: string;\n publisherLogo?: string;\n /** Schema.org 타입. 기본 \"Article\" (미지정 시 기존 출력 그대로). */\n type?: \"Article\" | \"BlogPosting\";\n /** 지정 시 `@id` emit (관례: `<url>#article`). */\n id?: string;\n /** 둘 다 있고 image 도 있으면 image 를 ImageObject 로 승격. */\n imageWidth?: number;\n imageHeight?: number;\n /** 저자 페이지 URL — 있으면 author 에 url + `@id`(`<authorUrl>#person`) 추가. */\n authorUrl?: string;\n /** 있으면 publisher 에 `@id` 추가 (name/logo 는 유지). */\n publisherId?: string;\n /** → articleSection. */\n section?: string;\n /** → inLanguage (기본값 없음, 지정 시만). */\n inLanguage?: string;\n}\n\nexport function articleSchema(input: ArticleSchemaInput): JsonLd {\n const schema: JsonLd = {\n \"@context\": \"https://schema.org\",\n \"@type\": input.type ?? \"Article\",\n headline: input.headline,\n mainEntityOfPage: { \"@type\": \"WebPage\", \"@id\": input.url },\n };\n if (input.id) schema[\"@id\"] = input.id;\n if (input.description) schema.description = input.description;\n if (input.image) {\n schema.image =\n input.imageWidth && input.imageHeight\n ? {\n \"@type\": \"ImageObject\",\n url: input.image,\n width: input.imageWidth,\n height: input.imageHeight,\n }\n : input.image;\n }\n const pub = toIso(input.datePublished);\n if (pub) schema.datePublished = pub;\n const mod = toIso(input.dateModified);\n if (mod) schema.dateModified = mod;\n if (input.authorName) {\n schema.author = {\n \"@type\": \"Person\",\n name: input.authorName,\n ...(input.authorUrl\n ? { \"@id\": `${input.authorUrl}#person`, url: input.authorUrl }\n : {}),\n };\n }\n if (input.publisherName) {\n schema.publisher = {\n \"@type\": \"Organization\",\n ...(input.publisherId ? { \"@id\": input.publisherId } : {}),\n name: input.publisherName,\n ...(input.publisherLogo\n ? { logo: { \"@type\": \"ImageObject\", url: input.publisherLogo } }\n : {}),\n };\n }\n if (input.section) schema.articleSection = input.section;\n if (input.inLanguage) schema.inLanguage = input.inLanguage;\n return schema;\n}\n\n/**\n * 저자 프로필 페이지용 ProfilePage JSON-LD.\n * `/blog/author/<slug>` 같은 저자 페이지 `<head>` 에서 사용.\n */\nexport function profilePageSchema(input: {\n url: string;\n name: string;\n image?: string | null;\n description?: string | null;\n}): JsonLd {\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"ProfilePage\",\n mainEntity: {\n \"@type\": \"Person\",\n \"@id\": `${input.url}#person`,\n name: input.name,\n url: input.url,\n ...(input.image ? { image: input.image } : {}),\n ...(input.description ? { description: input.description } : {}),\n },\n };\n}\n\n/**\n * 글 목록·카테고리 허브 페이지용 CollectionPage JSON-LD.\n * `/blog`, `/blog/category/<slug>` 등에서 사용.\n */\nexport function collectionPageSchema(input: {\n url: string;\n name: string;\n description?: string | null;\n siteUrl?: string;\n}): JsonLd {\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"CollectionPage\",\n \"@id\": `${input.url}#collection`,\n url: input.url,\n name: input.name,\n ...(input.description ? { description: input.description } : {}),\n ...(input.siteUrl\n ? { isPartOf: { \"@type\": \"WebSite\", url: input.siteUrl } }\n : {}),\n };\n}\n\nexport function breadcrumbSchema(\n items: { name: string; url: string }[],\n): JsonLd {\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"BreadcrumbList\",\n itemListElement: items.map((it, i) => ({\n \"@type\": \"ListItem\",\n position: i + 1,\n name: it.name,\n item: it.url,\n })),\n };\n}\n\nexport function organizationSchema(input: {\n name: string;\n url: string;\n logo?: string;\n sameAs?: string[];\n}): JsonLd {\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"Organization\",\n name: input.name,\n url: input.url,\n ...(input.logo ? { logo: input.logo } : {}),\n ...(input.sameAs && input.sameAs.length > 0 ? { sameAs: input.sameAs } : {}),\n };\n}\n\n// ── LocalBusiness (로컬 SEO) ────────────────────────────────────────────────\n\n/** Mo~Su 약어 → Schema.org dayOfWeek 전체 이름. */\nconst DAY_OF_WEEK: Record<string, string> = {\n Mo: \"Monday\",\n Tu: \"Tuesday\",\n We: \"Wednesday\",\n Th: \"Thursday\",\n Fr: \"Friday\",\n Sa: \"Saturday\",\n Su: \"Sunday\",\n};\n\n/**\n * `localBusinessSchema` 입력 — `fetchBusinessProfile` 의 반환값\n * (RootTaleBusinessProfile) 을 그대로 받도록 느슨하게 정의 (null/undefined 모두\n * 허용). 수동 구성도 가능.\n */\nexport interface LocalBusinessProfileInput {\n name: string;\n legalName?: string | null;\n /** 간판명의 표기 변형(띄어쓰기 등) — alternateName. */\n alternateName?: string | null;\n /** Schema.org LocalBusiness 하위 타입. 미지정 시 \"LocalBusiness\". */\n businessType?: string | null;\n telephone?: string | null;\n faxNumber?: string | null;\n email?: string | null;\n address?: {\n streetAddress: string;\n addressLocality: string;\n addressRegion?: string | null;\n postalCode?: string | null;\n } | null;\n geo?: { latitude: number; longitude: number } | null;\n openingHours?:\n | { days: string[]; opens: string; closes: string }[]\n | null;\n priceRange?: string | null;\n areaServed?: string[] | null;\n /** 취급 업무 — hasOfferCatalog 의 Offer 목록으로 들어간다. */\n services?: string[] | null;\n /** 외부 프로필 URL — 값들이 sameAs 로 들어간다 (네이버플레이스·구글 등). */\n profiles?: Record<string, string | null | undefined> | null;\n}\n\n/**\n * LocalBusiness JSON-LD — admin \"운영 > 비즈니스 프로필\" 의\n * `fetchBusinessProfile` 결과로 layout 에 1회 렌더한다 (로컬 SEO).\n *\n * - `@type` 은 `[businessType, \"LocalBusiness\"]` 중복 제거 배열.\n * - `profiles` 의 URL 들은 `sameAs` 로 — 검색엔진이 네이버플레이스/구글\n * 비즈니스 프로필과 동일 사업장임을 연결한다.\n */\n/** 취급 업무 상한 — api-contracts / api-core sanitize 와 같은 값이어야 한다. */\nexport const LOCAL_BUSINESS_MAX_SERVICES = 12;\nexport const LOCAL_BUSINESS_SERVICE_MAX_LENGTH = 80;\n\n/** hasMap 으로 빠지고 sameAs 에는 넣지 않는 지도 딥링크 키. */\nconst MAP_PROFILE_KEYS = new Set([\"kakaoMap\", \"kakao_map\"]);\n\nexport function localBusinessSchema(\n profile: LocalBusinessProfileInput | RootTaleBusinessProfile,\n opts: { url: string; image?: string | null },\n): JsonLd {\n const input = profile as LocalBusinessProfileInput;\n const types = Array.from(\n new Set([input.businessType || \"LocalBusiness\", \"LocalBusiness\"]),\n );\n const schema: JsonLd = {\n \"@context\": \"https://schema.org\",\n \"@type\": types,\n name: input.name,\n url: opts.url,\n };\n if (input.legalName) schema.legalName = input.legalName;\n if (input.alternateName) schema.alternateName = input.alternateName;\n if (opts.image) schema.image = opts.image;\n if (input.telephone) schema.telephone = input.telephone;\n if (input.faxNumber) schema.faxNumber = input.faxNumber;\n if (input.email) schema.email = input.email;\n if (input.address) {\n schema.address = {\n \"@type\": \"PostalAddress\",\n streetAddress: input.address.streetAddress,\n addressLocality: input.address.addressLocality,\n ...(input.address.addressRegion\n ? { addressRegion: input.address.addressRegion }\n : {}),\n ...(input.address.postalCode\n ? { postalCode: input.address.postalCode }\n : {}),\n addressCountry: \"KR\",\n };\n }\n if (input.geo) {\n schema.geo = {\n \"@type\": \"GeoCoordinates\",\n latitude: input.geo.latitude,\n longitude: input.geo.longitude,\n };\n }\n const openingHours = (input.openingHours ?? []).filter(\n (row) => row.days.length > 0 && row.opens && row.closes,\n );\n if (openingHours.length > 0) {\n schema.openingHoursSpecification = openingHours.map((row) => ({\n \"@type\": \"OpeningHoursSpecification\",\n dayOfWeek: row.days.map((d) => DAY_OF_WEEK[d] ?? d),\n opens: row.opens,\n closes: row.closes,\n }));\n }\n if (input.priceRange) schema.priceRange = input.priceRange;\n if (input.areaServed && input.areaServed.length > 0) {\n schema.areaServed = input.areaServed;\n }\n // 취급 업무 → OfferCatalog. \"무엇을 하는 곳인가\" 를 검색엔진에 명시하는 신호.\n //\n // 이 함수는 공개 API 라 admin/sanitize 를 거치지 않은 수동 입력도 들어온다.\n // 두 경로의 결과가 갈리지 않도록 여기서도 같은 정규화(공백 정리 → 길이 제한\n // → 중복 제거 → 개수 제한)를 적용한다.\n //\n // Service 단위 areaServed 는 넣지 않는다 — 서비스별 지역 데이터가 없어서\n // 첫 지역만 쓰면 \"이 서비스는 서울에서만 제공\" 이라는 없는 사실을 주장하게 된다.\n // 지역은 최상위 areaServed 가 이미 담고 있다.\n const services: string[] = [];\n for (const raw of input.services ?? []) {\n if (services.length >= LOCAL_BUSINESS_MAX_SERVICES) break;\n if (typeof raw !== \"string\") continue;\n const name = raw.trim().slice(0, LOCAL_BUSINESS_SERVICE_MAX_LENGTH);\n if (name && !services.includes(name)) services.push(name);\n }\n if (services.length > 0) {\n schema.hasOfferCatalog = {\n \"@type\": \"OfferCatalog\",\n name: `${input.name} 서비스`,\n itemListElement: services.map((name) => ({\n \"@type\": \"Offer\",\n itemOffered: { \"@type\": \"Service\", name },\n })),\n };\n }\n const profiles = input.profiles ?? {};\n // 지도 링크는 sameAs 가 아니라 hasMap 이 정확한 자리다. 네이버 플레이스 우선,\n // 없으면 지도 딥링크로 대체한다. `profiles` 는 수동 구성도 허용하는 느슨한\n // Record 라, 지도 키는 camelCase·snake_case 를 모두 인정한다.\n const pickUrl = (key: string): string | null => {\n const value = profiles[key];\n return typeof value === \"string\" && value.length > 0 ? value : null;\n };\n const mapUrl = pickUrl(\"naverPlace\") ?? pickUrl(\"kakaoMap\") ?? pickUrl(\"kakao_map\");\n if (mapUrl) schema.hasMap = mapUrl;\n // sameAs 는 \"같은 사업체를 가리키는 프로필 페이지\" 만 — 지도 딥링크는 프로필이\n // 아니라 위치 링크라 hasMap 으로만 내보내고 여기서는 뺀다.\n const sameAs = Object.entries(profiles)\n .filter(([key]) => !MAP_PROFILE_KEYS.has(key))\n .map(([, value]) => value)\n .filter((u): u is string => typeof u === \"string\" && u.length > 0);\n if (sameAs.length > 0) schema.sameAs = sameAs;\n return schema;\n}\n\nexport function websiteSchema(input: {\n name: string;\n url: string;\n searchUrl?: string;\n}): JsonLd {\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"WebSite\",\n name: input.name,\n url: input.url,\n ...(input.searchUrl\n ? {\n potentialAction: {\n \"@type\": \"SearchAction\",\n target: `${input.searchUrl}{search_term_string}`,\n \"query-input\": \"required name=search_term_string\",\n },\n }\n : {}),\n };\n}\n\nexport function faqSchema(\n qa: { question: string; answer: string }[],\n): JsonLd {\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"FAQPage\",\n mainEntity: qa.map((x) => ({\n \"@type\": \"Question\",\n name: x.question,\n acceptedAnswer: { \"@type\": \"Answer\", text: x.answer },\n })),\n };\n}\n\n/**\n * ADR-0065 — 글 조회수 식별자 meta 태그 HTML.\n *\n * 글 상세 페이지 `<head>` 에 렌더하면, RootTale 비콘이 이 값을 읽어 pageview 에\n * `post_id` 를 동봉한다 → URL/컬렉션 basePath 변경에 무관하게 조회수가 해당 글에\n * 정확히 집계된다. `post.id`(uuidv7) 를 그대로 넘기면 된다.\n *\n * Next App Router 는 `generateMetadata` 의 `other: { \"rt:content-id\": post.id }`\n * 로도 동일 태그를 낼 수 있다 — 그 경우 본 헬퍼 대신 metadata 를 쓴다.\n *\n * @example\n * // 프레임워크 무관(Astro/RSC set:html 등):\n * <head>{contentIdMeta(post.id)}</head>\n */\nexport function contentIdMeta(postId: string): string {\n return `<meta name=\"rt:content-id\" content=\"${escapeXml(postId)}\" />`;\n}\n\n/**\n * JSON-LD 객체 → XSS-safe JSON *문자열* (script 태그 없음). React 소비자가\n * `<script type=\"application/ld+json\" dangerouslySetInnerHTML={{ __html: ... }} />`\n * 로 직접 방출할 때 사용 — `</script>` 주입 방지 escape 의 단일 소스.\n */\nexport function jsonLdJson(schema: JsonLd | JsonLd[]): string {\n // `</script>` 주입 방지를 위해 `<` 이스케이프.\n return JSON.stringify(schema).replace(/</g, \"\\\\u003c\");\n}\n\n/** JSON-LD 객체 → `<script type=\"application/ld+json\">` HTML 문자열 (XSS-safe). */\nexport function jsonLdScript(schema: JsonLd | JsonLd[]): string {\n return `<script type=\"application/ld+json\">${jsonLdJson(schema)}</script>`;\n}\n\nfunction toIso(value: string | Date | null | undefined): string | null {\n if (!value) return null;\n const d = value instanceof Date ? value : new Date(value);\n return Number.isNaN(d.getTime()) ? null : d.toISOString();\n}\n\nfunction escapeXml(value: string): string {\n return value\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\");\n}\n\nfunction cdata(value: string): string {\n return `<![CDATA[${value.replace(/]]>/g, \"]]]]><![CDATA[>\")}]]>`;\n}\n\n// ── 검색엔진 소유 확인 ──────────────────────────────────────────────────────\n\n/** Next.js `Metadata[\"verification\"]` 와 구조 호환. */\nexport interface SiteVerificationMetadata {\n google?: string[];\n other?: Record<string, string[]>;\n}\n\n/**\n * `theme.siteVerification` → 루트 layout metadata 의 `verification`.\n * 코드가 없으면 undefined 를 돌려줘 spread 로 생략할 수 있다.\n */\nexport function siteVerificationMetadata(\n settings: SiteVerificationSettings | undefined,\n): SiteVerificationMetadata | undefined {\n const google = settings?.google?.filter(Boolean) ?? [];\n const naver = settings?.naver?.filter(Boolean) ?? [];\n if (google.length === 0 && naver.length === 0) return undefined;\n return {\n ...(google.length > 0 ? { google } : {}),\n ...(naver.length > 0 ? { other: { \"naver-site-verification\": naver } } : {}),\n };\n}\n","/**\n * `@roottale/cms-client` — 리드(문의) 접수 server-side 헬퍼.\n *\n * 외부 고객사 사이트가 *블로그 조회(`fetchPosts`)와 동일한 API key 하나* 로 리드도\n * 접수하기 위한 SSR-only 클라이언트. 브라우저가 아니라 사이트 서버(Server Action /\n * Route Handler)에서 호출해야 한다 — API key 는 절대 브라우저로 노출 금지\n * (ADR-0023 §5.1 #15). 키가 tenant 를 권위 식별하므로 `tenant_slug` 불필요.\n *\n * 사용 예 (Next.js server action):\n * ```ts\n * \"use server\";\n * import { submitInquiry } from \"@roottale/cms-client/server\";\n *\n * const result = await submitInquiry({\n * apiKey: process.env.ROOTTALE_API_KEY!, // fetchPosts 와 같은 키\n * fields: {\n * vertical: \"tax\",\n * contactName: name,\n * businessName: company,\n * email,\n * phone,\n * message,\n * privacyConsent: true,\n * },\n * });\n * if (result.ok) showSuccess(); // 그 키의 tenant /crm 에 도착\n * else showError(result.message);\n * ```\n *\n * intake API 는 302 `?ok=1` / `?err=*` 로 응답 — 본 헬퍼가 파싱해 구조화 결과 반환.\n */\n\nimport type { InquiryAttribution } from \"./attribution\";\n\nconst DEFAULT_BASE_URL = \"https://api.roottale.com\";\n\nexport type InquiryVertical = \"consulting\" | \"medical\" | \"tax\" | \"legal\";\n\n/** intake API 의 `err` 코드 → 한국어 사용자 메시지. */\nexport const INQUIRY_ERROR_MESSAGES: Record<string, string> = {\n consent_privacy: \"개인정보 수집·이용 동의가 필요합니다.\",\n consent_overseas: \"의료 분야는 국외이전 동의가 필요합니다.\",\n invalid_vertical: \"업종 분류가 유효하지 않습니다.\",\n missing_fields: \"필수 항목이 비어 있습니다.\",\n invalid_email: \"이메일 형식이 올바르지 않습니다.\",\n invalid_api_key: \"연동 인증에 실패했습니다. API 키를 확인해주세요.\",\n rate_limited: \"요청이 너무 많습니다. 잠시 후 다시 시도해주세요.\",\n turnstile_failed: \"보안 확인에 실패했습니다. 새로고침 후 다시 시도해주세요.\",\n internal: \"전송에 실패했습니다. 잠시 후 다시 시도해주세요.\",\n};\n\nexport interface SubmitInquiryFields {\n vertical: InquiryVertical;\n contactName: string;\n businessName: string;\n email: string;\n phone: string;\n /** 자유 문의 내용 (선택). */\n message?: string;\n /** 상담 분야 라벨 등 부가 메타 (선택). */\n consultationField?: string;\n /** 현재 사이트 URL (선택). */\n currentSiteUrl?: string;\n /** 개인정보 수집·이용 동의 (필수 true). */\n privacyConsent: boolean;\n /** 의료 vertical 의 국외이전 동의 (medical 일 때 필수). */\n overseasTransferConsent?: boolean;\n /**\n * \"patient\"(고객사 방문자 문의, 기본) | \"sales\"(RootTale 자체 B2B 영업 리드).\n * sales 는 medical 국외이전 동의 게이트 면제.\n */\n leadKind?: \"patient\" | \"sales\";\n turnstileToken?: string;\n /**\n * 표준 필드 외에 폼이 받은 임의 항목(key→value)을 있는 그대로 전달. intake 가\n * 암호화 보관하고 CRM 상세에서 노출한다. 예약 키(표준 필드)·`_` 접두 키는 무시.\n * 주의: 개인정보가 담길 수 있으므로 폼의 동의 고지에 수집 항목을 반영할 것.\n */\n extras?: Record<string, string>;\n /**\n * 유입 first-touch 어트리뷰션 (ADR-0048 §2, 선택). 브라우저에서\n * `readAttribution()`(`@roottale/cms-client/attribution`)으로 읽어 Server\n * Action 인자로 넘긴 값을 그대로 전달하면 된다. CRM 에 \"이 문의가 어느\n * 글/링크/QR 에서 왔는지\"가 표시된다. null/undefined 면 미동반 제출.\n */\n attribution?: InquiryAttribution | null;\n /**\n * 방문 여정(journey) JSON 원문(설계 v2 §3, 선택) — `@roottale/analytics-runtime`\n * `readJourney()` 로 읽은 sessionStorage `_rt_journey` 링버퍼를 그대로\n * `JSON.stringify` 한 문자열. 파싱·검증은 서버(zod, medical/legal 마일스톤\n * 게이트 재강제)가 한다 — 여기서는 원문 그대로 `attr_journey` FormData 로\n * 전달만. null/undefined/빈 문자열이면 미동반 제출.\n */\n journey?: string;\n}\n\n/** submitInquiry 가 전용으로 매핑하는 키 — extras 에서 충돌 시 무시. */\nconst RESERVED_INQUIRY_KEYS = new Set([\n \"vertical\",\n \"contact_name\",\n \"business_name\",\n \"email\",\n \"phone\",\n \"message\",\n \"consultation_field\",\n \"current_site_url\",\n \"privacy_consent\",\n \"overseas_transfer_consent\",\n \"lead_kind\",\n \"cf-turnstile-response\",\n]);\n\nexport interface SubmitInquiryOptions {\n /** `rtlk_cust_*` API key — `fetchPosts` 와 동일한 키. Required. */\n apiKey: string;\n /** API base URL 오버라이드. 기본 `https://api.roottale.com`. */\n baseUrl?: string;\n fields: SubmitInquiryFields;\n signal?: AbortSignal;\n}\n\nexport type SubmitInquiryResult =\n | { ok: true }\n | { ok: false; code: string | null; message: string };\n\n/** `err` 코드를 사용자 메시지로 (미지의 코드는 generic). */\nexport function inquiryErrorMessage(code: string | null | undefined): string {\n return (code && INQUIRY_ERROR_MESSAGES[code]) || INQUIRY_ERROR_MESSAGES.internal;\n}\n\n/**\n * intake API 의 302 Location (`?ok=1` / `?err=*`) 을 구조화 결과로 파싱.\n * 외부 사이트가 ERROR_MESSAGES·파싱을 각자 재구현하지 않도록 공유.\n */\nexport function parseInquiryRedirect(\n location: string | null,\n base: string = DEFAULT_BASE_URL,\n): SubmitInquiryResult {\n if (!location) {\n return { ok: false, code: null, message: inquiryErrorMessage(null) };\n }\n let url: URL;\n try {\n url = new URL(location, base);\n } catch {\n return { ok: false, code: null, message: inquiryErrorMessage(null) };\n }\n if (url.searchParams.get(\"ok\") === \"1\") return { ok: true };\n const code = url.searchParams.get(\"err\");\n return { ok: false, code, message: inquiryErrorMessage(code) };\n}\n\nexport function formatKoreanPhoneNumber(input: string): string {\n let digits = input.replace(/\\D/g, \"\");\n if (digits.startsWith(\"82\") && digits.length >= 11) {\n digits = `0${digits.slice(2)}`;\n }\n if (digits.length === 11) {\n return `${digits.slice(0, 3)}-${digits.slice(3, 7)}-${digits.slice(7)}`;\n }\n if (digits.length === 10) {\n if (digits.startsWith(\"02\")) {\n return `${digits.slice(0, 2)}-${digits.slice(2, 6)}-${digits.slice(6)}`;\n }\n return `${digits.slice(0, 3)}-${digits.slice(3, 6)}-${digits.slice(6)}`;\n }\n if (digits.length === 8) {\n return `${digits.slice(0, 4)}-${digits.slice(4)}`;\n }\n return input.trim();\n}\n\n/**\n * 리드를 RootTale intake API 로 제출 (server-to-server, Bearer 인증).\n * 키의 tenant 로 라우팅되어 admin(admin.roottale.com) /crm 에서 관리된다.\n */\nexport async function submitInquiry(\n options: SubmitInquiryOptions,\n): Promise<SubmitInquiryResult> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const f = options.fields;\n\n const body = new FormData();\n body.set(\"vertical\", f.vertical);\n body.set(\"contact_name\", f.contactName);\n body.set(\"business_name\", f.businessName);\n body.set(\"email\", f.email);\n body.set(\"phone\", formatKoreanPhoneNumber(f.phone));\n if (f.message) body.set(\"message\", f.message);\n if (f.consultationField) body.set(\"consultation_field\", f.consultationField);\n if (f.currentSiteUrl) body.set(\"current_site_url\", f.currentSiteUrl);\n // 사용자가 명시 동의한 경우에만 전달 (자동 부여 금지).\n if (f.privacyConsent) body.set(\"privacy_consent\", \"on\");\n if (f.overseasTransferConsent) body.set(\"overseas_transfer_consent\", \"on\");\n if (f.leadKind) body.set(\"lead_kind\", f.leadKind);\n if (f.turnstileToken) body.set(\"cf-turnstile-response\", f.turnstileToken);\n // 유입 어트리뷰션 → intake `attr_*` 필드 (허용 키만, 빈 값 생략).\n if (f.attribution) {\n for (const [key, value] of Object.entries(f.attribution)) {\n // ad_click(설계 v2 §2) 은 boolean \"존재 플래그\" — true 일 때만 \"1\" 로\n // 전송(false/미설정은 신호 없음과 동일하므로 생략).\n if (typeof value === \"boolean\") {\n if (value) body.set(`attr_${key}`, \"1\");\n continue;\n }\n if (typeof value !== \"string\" || !value.trim()) continue;\n body.set(`attr_${key}`, value.trim());\n }\n }\n // 방문 여정(설계 v2 §3) — attribution 과 별도 채널(`attr_journey`). 파싱은\n // 서버(zod)가 전담 — 여기서는 원문 그대로 전달.\n if (f.journey && f.journey.trim()) {\n body.set(\"attr_journey\", f.journey.trim());\n }\n // 표준 외 임의 항목 — 예약 키/`_`접두/빈 값/중복은 건너뛰고 원본 그대로 전달.\n if (f.extras) {\n for (const [rawKey, rawValue] of Object.entries(f.extras)) {\n const key = rawKey.trim();\n const value = String(rawValue ?? \"\").trim();\n if (!key || !value) continue;\n if (key.startsWith(\"_\") || RESERVED_INQUIRY_KEYS.has(key)) continue;\n if (key.startsWith(\"attr_\")) continue; // 어트리뷰션 채널과 충돌 방지\n\n if (body.has(key)) continue;\n body.set(key, value);\n }\n }\n\n let response: Response;\n try {\n response = await fetch(`${baseUrl}/v1/public/inquiries`, {\n method: \"POST\",\n headers: { authorization: `Bearer ${options.apiKey}` },\n body,\n // intake 는 302 로 결과 신호 — 자동 추적 금지하고 Location 직접 읽음.\n redirect: \"manual\",\n signal: options.signal,\n });\n } catch {\n return { ok: false, code: \"internal\", message: inquiryErrorMessage(\"internal\") };\n }\n\n // 잘못된/만료된 키 → 라우트가 401.\n if (response.status === 401) {\n return {\n ok: false,\n code: \"invalid_api_key\",\n message: inquiryErrorMessage(\"invalid_api_key\"),\n };\n }\n if (response.status === 429) {\n return {\n ok: false,\n code: \"rate_limited\",\n message: inquiryErrorMessage(\"rate_limited\"),\n };\n }\n\n const location = response.headers.get(\"location\");\n if (location) return parseInquiryRedirect(location, baseUrl);\n\n // redirect 없이 성공 응답이면 ok 로 간주, 그 외엔 generic 실패.\n if (response.ok) return { ok: true };\n return { ok: false, code: \"internal\", message: inquiryErrorMessage(\"internal\") };\n}\n\nfunction assertServer(): void {\n if (typeof window !== \"undefined\") {\n throw new Error(\n \"@roottale/cms-client submitInquiry must not run in the browser. \" +\n \"Call it from a Server Action / Route Handler. \" +\n \"Shipping the API key to the browser breaks the security model (ADR-0023 §5.1 #15).\",\n );\n }\n}\n"],"mappings":"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","names":["DEFAULT_BASE_URL","assertServer"]}
1
+ {"version":3,"sources":["../src/server-base.ts","../src/server-post-wire.ts","../src/server-posts-theme.ts","../src/server-blog.ts","../src/server-categories.ts","../src/server-business.ts","../src/server-discovery.ts","../src/server-exposure.ts","../src/server-exposure-decision.ts","../src/server-site-inquiries.ts","../src/server-inquiry-forms.ts","../src/server-preview.ts","../src/server-patterns.ts","../src/server-metadata.ts","../src/metadata-contract-checksum.ts","../src/server-cache-tags.ts","../src/seo.ts","../src/inquiry.ts"],"sourcesContent":["/**\n * `@roottale/cms-client/server` — RootTale CMS Public API server-side fetch client.\n *\n * 외부 고객사 사이트가 RootTale CMS 콘텐츠를 fetch 하기 위한 SSR-only client.\n * Bearer (`rtlk_cust_*`) 인증, 브라우저 직접 호출 금지 (ADR-0023 §5.1 #15).\n *\n * 사용 예 (Next.js server component):\n * ```ts\n * import { fetchPosts } from \"@roottale/cms-client/server\";\n *\n * const posts = await fetchPosts({\n * apiKey: process.env.ROOTTALE_API_KEY!,\n * limit: 10,\n * });\n * ```\n *\n * 데이터 모델 = ADR-0034 `posts` (WP 7.0-aligned, type=post|page).\n * Phase 1 scope: list + get. WP import/export, draft preview = 별도 PR.\n */\n\nexport type CmsPostType = \"post\" | \"page\";\n\nexport type CmsTaxonomyKind = \"category\" | \"tag\" | \"custom\";\n\nexport interface CmsTermRef {\n id: string;\n taxonomy: CmsTaxonomyKind;\n slug: string;\n name: string;\n termOrder?: number;\n parentId?: string | null;\n}\n\nexport interface CmsArchiveRelatedPost {\n id: string;\n title: string;\n slug: string;\n excerpt: string | null;\n collectionKey: string;\n}\n\n/** 관련 콘텐츠 1건 — 카드 하나를 그릴 만큼(제목·주소·유형·요약·대표이미지). */\nexport interface CmsRelatedPost {\n id: string;\n title: string;\n slug: string;\n /** 저장된 정규 공개 경로(ADR-0105). 상세 주소가 없는 유형이면 null. */\n path: string | null;\n type: string;\n modelKey: string | null;\n collectionKey: string | null;\n excerpt: string | null;\n featuredImageUrl: string | null;\n /** ISO 8601 | null. */\n publishedAt: string | null;\n}\n\nexport interface CmsArchiveRelatedGroup {\n collectionKey: string;\n label: string;\n items: CmsArchiveRelatedPost[];\n}\n\n/** W5-2 — `fields` 표시 메타 1건 (서버 계약 `CmsFieldMeta` 대응). */\nexport interface CmsFieldMetaEntry {\n /** `fields` 의 key. */\n name: string;\n /** 필드 그룹에 정의된 사람용 라벨. */\n label: string;\n /** 필드 타입 (text/textarea/select/relationship/…). */\n type: string;\n /** select/radio 계열만 — 저장 값(value)→표시 라벨. */\n choices?: Array<{ value: string; label: string }>;\n}\n\n/** ADR-0052 A안 (W4-6 PR B) — 언어판 참조(locale + 그 언어판의 slug). */\nexport interface CmsPostTranslationRef {\n locale: string;\n slug: string;\n}\n\nexport interface CmsPostContent {\n /** UUIDv7. */\n id: string;\n /** Owning tenant id. */\n tenantId: string;\n /** Owning site id (ADR-0034 P4 dual-key). */\n siteId: string;\n /** WP post type. */\n type: CmsPostType;\n /**\n * ADR-0060 Amendment 1 — 글의 명시 섹션(content collection) key. 라우팅/스트림\n * 판정에 카테고리 파생보다 우선. 구 서버/미설정이면 null.\n */\n collectionKey: string | null;\n /** 페이지·글·정보를 가리키는 안정적인 모델 key. 구 서버에서는 null. */\n modelKey?: string | null;\n /** Lowercase kebab-case slug (1-120 chars). */\n slug: string;\n /**\n * ADR-0052 A안 — 이 글판의 BCP-47 locale. 단일 언어 사이트는 항상 'ko'.\n * 구 서버(마이그레이션 전) 는 미포함 — optional (하위호환).\n */\n locale?: string;\n /**\n * ADR-0052 A안 — 같은 글의 발행된 언어판 목록(이 글 자신 포함). 번역이 없으면\n * 자기 자신 1건만. 구 서버는 미포함 — optional (하위호환).\n */\n translations?: CmsPostTranslationRef[];\n /**\n * 이 글이 예전에 썼던 slug 목록(최신순). 주소 이동 뒤에도 옛 경로에서 파생한\n * 본문 예약 내부 링크 키(`[[internal:…]]`)를 현재 글로 해석할 때 옛 경로도 같은\n * 글로 등록하는 데 쓴다. 구 서버는 미포함 — optional.\n */\n previousSlugs?: string[];\n /**\n * ADR-0105 — 저장된 정규 공개 경로(origin 제외, 예 `/column/my-post`). 링크·사이트맵·\n * 내부 링크 키에 그대로 쓴다. 상세 주소가 없는 글은 null, 구 서버는 미포함.\n */\n path?: string | null;\n title: string;\n excerpt: string | null;\n /** Media id (resolved by renderer / consumer to URL). */\n featuredMediaId: string | null;\n /**\n * featured 이미지 delivery URL (CF Images variant, md 우선). 서버가 `featured_media_id`\n * → `media.variants_json` 를 해석해 제공. 대표 이미지 미설정 시 null.\n */\n featuredImageUrl: string | null;\n /** 사이트 공통 공개 작성자 ID. 구 서버에서는 null. */\n authorProfileId?: string | null;\n /** @deprecated Use authorProfileId. */\n authorId: string | null;\n /** Public display name for the post author. Email is not exposed. */\n authorName: string | null;\n /** Public profile image URL for the post author. */\n authorImageUrl: string | null;\n /** 작성자 사진의 좌우 초점(0~100). 구 서버·레거시 작성자는 null. */\n authorImagePositionX?: number | null;\n /** 작성자 사진의 상하 초점(0~100). 구 서버·레거시 작성자는 null. */\n authorImagePositionY?: number | null;\n /** Public biography text for the post author. */\n authorBio: string | null;\n /**\n * Author archive slug (`/blog/author/{slug}`). null when the author has no\n * assigned slug. 구 서버는 미포함 — 매핑부에서 `?? null` 기본값.\n */\n authorSlug: string | null;\n /** Tiptap / Gutenberg-compatible Block JSON. Sanitization is renderer's responsibility. */\n bodyJson: Record<string, unknown>;\n /**\n * 원문 HTML 정본 — 외부에서 이관된 글처럼 HTML 이 정본인 경우에만 채워진다.\n * 렌더러는 값이 있으면 sanitize 후 우선하고 없으면(null) body_json 으로\n * 폴백한다. 구 서버는 미포함 — 매핑부에서 `?? null` 기본값.\n * Sanitization 책임은 body_json 과 동일하게 렌더러에 있다.\n *\n * `?:` 인 이유 — 이 필드는 순수 가산분이라 기존 소비자가 모르고 지나가도\n * 종전과 동일하게 동작해야 한다(값이 없으면 body_json 폴백). optional 로 두면\n * 외부 사이트의 기존 객체 리터럴도 그대로 타입을 만족한다.\n */\n bodyHtml?: string | null;\n /** Public API only exposes `published`. */\n status: \"published\";\n /** Per-post meta (SEO overrides, canonical, og, ...). */\n metaJson: Record<string, unknown>;\n /**\n * ADR-0041 — 포맷된 커스텀 필드(ACF) 값. 서버가 필드 정의(코드 ⊕ DB 그룹)로\n * `meta_json.acf` 를 포맷 — image/file/gallery 는 media variant URL 객체.\n * 해석된 그룹이 없거나 구 서버면 null.\n */\n fields: Record<string, unknown> | null;\n /**\n * W5-2 — `fields` 의 표시 메타(라벨·타입·choices), 필드 그룹 정의 순서.\n * 서버가 필드 정의로 계산해 동봉 — 렌더러가 vertical 지식 없이 양식 값을\n * 라벨과 함께 표시한다. 해석된 그룹 없거나 구 서버면 빈 배열.\n */\n fieldsMeta: CmsFieldMetaEntry[];\n /**\n * Attached taxonomy terms (categories + tags + custom). Server 가 category →\n * tag → custom 순서로 정렬해 보내며, 각 taxonomy 내부는 name asc. attach 가 0 개면\n * 빈 배열. category 만 추출하려면 `terms.filter(t => t.taxonomy === \"category\")`.\n */\n terms: CmsTermRef[];\n archiveRelated: CmsArchiveRelatedGroup[];\n /**\n * ADR-0065 — 누적 조회수. 어드민에서 \"공개 조회수\"를 켠 사이트에서만 내려온다\n * (꺼져 있으면 `undefined`). \"조회 N\" 표시에 사용. 비콘이 채우므로 신규 글은 0.\n */\n viewCount?: number;\n /**\n * 공통 블록 자리별 블록 key(자리 key → `fetchSitePatterns` 의 블록 key | null). 서버가\n * 어드민 배치 규칙을 이 글의 유형에 맞춰 계산한 결과라 사이트는 규칙을 몰라도 된다.\n * 선언된 자리(예: `post_footer`)는 항상 키로 존재하고 놓을 블록이 없으면 null.\n * 구 서버는 미포함 — 매핑부에서 빈 객체.\n */\n patternSlots?: Record<string, string | null>;\n /**\n * 관련 콘텐츠 — 편집자가 어드민 \"관련 콘텐츠\"에서 고른 글(유형 무관), 고른 순서, 발행 글만.\n * 목록·상세·미리보기 모두 내려온다. 구 서버는 빈 배열(매핑부). 비었을 때\n * \"같은 분류 최신 글\" 자동 추천은 `selectRelatedPosts` 로 사이트가 채운다.\n */\n relatedPosts?: CmsRelatedPost[];\n /** ISO 8601. */\n publishedAt: string;\n /** ISO 8601. */\n createdAt: string;\n /** ISO 8601. */\n updatedAt: string;\n}\n\nexport interface CmsPostContentPage {\n items: CmsPostContent[];\n nextCursor: string | null;\n hasMore: boolean;\n}\n\nexport const DEFAULT_BASE_URL = \"https://api.roottale.com\";\n\n/**\n * 호출부가 `signal`을 주지 않은 서버 조회의 최대 대기 시간. API·DB 장애 때 페이지\n * 렌더가 끝없이 기다리지 않게 한다. 빌드처럼 동시 조회가 몰려도 정상 응답은 이 안에 끝난다.\n */\nexport const DEFAULT_SERVER_FETCH_TIMEOUT_MS = 10_000;\n\n/** 호출부 `signal`을 우선하고, 없으면 기본 제한 시간을 건다. */\nexport function requestSignal(signal?: AbortSignal): AbortSignal {\n return signal ?? AbortSignal.timeout(DEFAULT_SERVER_FETCH_TIMEOUT_MS);\n}\n\n// fleet 가시성 — 자기 버전을 자가 보고 (build-time 주입, tsup define).\n// api-core 가 사이트별 check-in 으로 기록 → internal `GET /v1/internal/sites`.\ndeclare const __CMS_CLIENT_VERSION__: string | undefined;\nconst CLIENT_VERSION =\n typeof __CMS_CLIENT_VERSION__ === \"string\" ? __CMS_CLIENT_VERSION__ : \"dev\";\n\n/** 설치된 cms-client 버전 (build-time 주입) — fleet well-known 라우트 등에서 사용. */\nexport const CMS_CLIENT_VERSION = CLIENT_VERSION;\n\nexport function authHeaders(apiKey: string): Record<string, string> {\n return {\n authorization: `Bearer ${apiKey}`,\n \"x-roottale-client\": `cms-client/${CLIENT_VERSION}`,\n };\n}\n\n/**\n * ISR 시간 기반 fallback(W0-9). 지정 시 fetch 에 Next 의 `next: { revalidate }`\n * 를 붙여 webhook 유실 시에도 이 주기(초) 내에 캐시가 자가 치유된다.\n * **미지정(undefined)이면 아무 것도 붙이지 않아 현행 동작을 그대로 유지**(파괴적\n * 변경 없음) — Next 기본 캐시/정적화 정책을 따른다. `0` 은 항상 재검증(동적).\n */\nexport interface RevalidatableFetchOptions {\n /** Optional AbortSignal. */\n signal?: AbortSignal;\n /**\n * ISR revalidate 초. opt-in — 미지정 시 현행 캐시 동작 유지.\n * (Next.js `next: { revalidate }` 로 전달.)\n */\n revalidate?: number;\n}\n\n/** Next `fetch` 확장 — `next.revalidate`/`next.tags` 지원(런타임 무해, non-Next 무시). */\ntype NextFetchInit = RequestInit & {\n next?: { revalidate?: number; tags?: string[] };\n};\n\n/**\n * GET fetch 용 RequestInit 빌더 — Bearer 헤더 + signal(기본 제한 시간) + opt-in ISR revalidate\n * + opt-in cache tags. cms-client 의 모든 공개 콘텐츠/테마 fetch 가 공유해\n * `next.revalidate`/`next.tags` 지원을 일관되게 얻는다.\n *\n * `tags` 는 on-demand 무효화 경로(W0-4): 지정 시 fetch 캐시 엔트리에 tag 를\n * 붙여, 수신 route 의 `revalidateTag(tag)` 가 해당 엔트리(+그 fetch 를 쓴 route)를\n * 무효화한다. 미지정 = 붙이지 않음(현행 동작 유지).\n */\nexport function buildGetInit(\n apiKey: string,\n opts: { signal?: AbortSignal; revalidate?: number; tags?: string[] },\n): NextFetchInit {\n const init: NextFetchInit = {\n method: \"GET\",\n headers: authHeaders(apiKey),\n signal: requestSignal(opts.signal),\n };\n if (opts.revalidate !== undefined || (opts.tags && opts.tags.length > 0)) {\n init.next = {\n ...(opts.revalidate !== undefined ? { revalidate: opts.revalidate } : {}),\n ...(opts.tags && opts.tags.length > 0 ? { tags: opts.tags } : {}),\n };\n }\n return init;\n}\n\nexport interface FetchPostsOptions {\n /** `rtlk_cust_*` API key. Required. */\n apiKey: string;\n /** Override the API base URL. Defaults to `https://api.roottale.com`. */\n baseUrl?: string;\n /** Page size, 1-100, default 20. */\n limit?: number;\n /** Opaque pagination cursor returned from previous call. */\n cursor?: string;\n /** Filter by post type (`post` or `page`). Default = both. */\n type?: CmsPostType;\n /** 작가 slug 필터 — 작가 아카이브(/blog/author/{slug})용. 지정 시 발행 글(post)만. */\n author?: string;\n /** Override the auto-resolved site id (Phase 1 = 1 site/tenant, usually omitted). */\n siteId?: string;\n /**\n * ADR-0060 — 특정 섹션(collection) 소속 글만(포지티브 스코프). `excludeCollections`\n * 와 동시 지정 불가(서버가 400).\n */\n collectionKey?: string;\n /** 특정 콘텐츠 모델의 항목만 조회한다. */\n modelKey?: string;\n /**\n * 네거티브 스코프 — 어느 스트림에도 안 속한(\"미소속\") 글만\n * (`blog-list-collection-scope.md` §2②). `collectionKey` 와 동시 지정 불가.\n */\n excludeCollections?: boolean;\n /**\n * ADR-0052 A안 (W4-6 PR B) — BCP-47 소문자 locale 필터(예: \"en\"). 미지정 시\n * 서버가 사이트 기본 locale(`settingsJson.locales[0]`, 미설정이면 'ko') 만\n * 반환한다 — 하위호환(번역판이 생겨도 이 옵션을 안 쓰는 기존 호출부는 원어\n * 콘텐츠만 계속 받는다).\n */\n locale?: string;\n /** Optional AbortSignal. */\n signal?: AbortSignal;\n /** ISR revalidate 초(opt-in, W0-9). 미지정 시 현행 동작 유지. */\n revalidate?: number;\n /** Next fetch cache tags. Use CONTENT_POLICY_CACHE_TAG for declared policy facts. */\n tags?: string[];\n}\n\nexport interface FetchPostOptions {\n apiKey: string;\n baseUrl?: string;\n /** Slug (kebab-case) or UUIDv7 id. */\n slugOrId: string;\n /** Override the auto-resolved site id. */\n siteId?: string;\n /** ADR-0052 A안 — `FetchPostsOptions.locale` 과 동일 시맨틱. */\n locale?: string;\n signal?: AbortSignal;\n /** ISR revalidate 초(opt-in, W0-9). 미지정 시 현행 동작 유지. */\n revalidate?: number;\n /** Next fetch cache tags. Use CONTENT_POLICY_CACHE_TAG for declared policy facts. */\n tags?: string[];\n}\n\n\nexport class CmsApiError extends Error {\n readonly status: number;\n readonly code: string;\n constructor(message: string, status: number, code: string) {\n super(message);\n this.name = \"CmsApiError\";\n this.status = status;\n this.code = code;\n }\n}\n\nexport async function toApiError(response: Response): Promise<CmsApiError> {\n let code = \"request_failed\";\n let message = `RootTale CMS API error: ${response.status}`;\n try {\n const body = (await response.json()) as { code?: string; message?: string };\n if (body.code) code = body.code;\n if (body.message) message = body.message;\n } catch {\n /* ignore body parse */\n }\n return new CmsApiError(message, response.status, code);\n}\n\nexport function assertServer(): void {\n if (typeof window !== \"undefined\") {\n throw new Error(\n \"@roottale/cms-client/server must not run in the browser. \" +\n \"Move this call to a Server Component, getServerSideProps, or a Route Handler. \" +\n \"Shipping API keys to the browser breaks the security model (ADR-0023 §5.1 #15).\",\n );\n }\n}\n\n// 프레임워크 무관 SEO 헬퍼 (RSS 2.0 + Schema.org JSON-LD) 재노출.\n","import type { CmsPostContent, CmsPostType, CmsRelatedPost, CmsTermRef } from \"./server-base\";\n\n// 공개 API 글 응답(wire) → CmsPostContent 변환. server-posts-theme 와 server-preview 가\n// 함께 쓰는 내부 모듈 — 공개 배럴(server.ts)에서 재노출하지 않는다.\n\nexport type CmsPostWire = {\n id: string;\n tenant_id: string;\n site_id: string;\n type: CmsPostType;\n /** ADR-0060 Amendment 1 — 명시 섹션 key. 구 서버는 미포함 — optional. */\n collection_key?: string | null;\n /** 신규 모델 key. 구 서버는 미포함 — optional. */\n model_key?: string | null;\n slug: string;\n /** ADR-0052 A안 — 구 서버는 미포함 — optional(하위호환). */\n locale?: string;\n /** ADR-0052 A안 — 구 서버는 미포함 — optional(하위호환). */\n translations?: Array<{ locale: string; slug: string }>;\n /** 옛 slug 목록(최신순). 구 서버는 미포함 — optional. */\n previous_slugs?: string[];\n /** 저장된 정규 공개 경로. 구 서버는 미포함 — optional. */\n path?: string | null;\n title: string;\n excerpt: string | null;\n featured_media_id: string | null;\n /** featured 이미지 delivery URL. 구 서버는 미포함 — backward-compat optional. */\n featured_media_url?: string | null;\n author_id: string | null;\n author_profile_id?: string | null;\n author_name?: string | null;\n author_image_url?: string | null;\n author_image_position_x?: number | null;\n author_image_position_y?: number | null;\n author_bio?: string | null;\n author_slug?: string | null;\n body_json: Record<string, unknown>;\n /**\n * 원문 HTML 정본. 구 서버는 미포함 — backward-compat optional.\n * 렌더러가 sanitize 후 우선 사용하고, 없으면 body_json 으로 폴백한다.\n */\n body_html?: string | null;\n status: \"published\";\n meta_json: Record<string, unknown>;\n /** ADR-0041 포맷 필드. 구 서버는 미포함 — backward-compat optional. */\n fields?: Record<string, unknown> | null;\n /** 0.2.0 이전 서버는 미포함 — backward-compat 차원에서 optional. */\n terms?: Array<CmsTermRef & { term_order?: number; parent_id?: string | null }>;\n archive_related?: Array<{\n collection_key: string;\n label: string;\n items: Array<{\n id: string;\n title: string;\n slug: string;\n excerpt: string | null;\n collection_key: string;\n }>;\n }>;\n /** W5-2 — fields 표시 메타. 해석된 그룹 없거나 구 서버는 미포함. */\n fields_meta?: Array<{\n name: string;\n label: string;\n type: string;\n choices?: Array<{ value: string; label: string }>;\n }>;\n /** ADR-0065 — 누적 조회수. 사이트가 공개 조회수를 켠 경우에만 포함. */\n view_count?: number;\n /** 공통 블록 자리별 블록 key. 구 서버는 미포함 — optional. */\n pattern_slots?: Record<string, string | null>;\n /** 관련 콘텐츠(목록·상세·미리보기). 구 서버는 미포함 — optional. */\n related_posts?: Array<{\n id: string;\n title: string;\n slug: string;\n path: string | null;\n type: string;\n model_key: string | null;\n collection_key: string | null;\n excerpt: string | null;\n featured_media_url: string | null;\n published_at: string | null;\n }>;\n published_at: string;\n created_at: string;\n updated_at: string;\n};\n\n/** `pattern_slots` → 자리별 블록 key. 형식이 틀린 항목은 버리고 구버전 응답은 빈 객체. */\nfunction patternSlotsFromWire(raw: unknown): Record<string, string | null> {\n if (!raw || typeof raw !== \"object\" || Array.isArray(raw)) return {};\n const out: Record<string, string | null> = {};\n for (const [slot, key] of Object.entries(raw as Record<string, unknown>)) {\n if (key === null || typeof key === \"string\") out[slot] = key;\n }\n return out;\n}\n\n/** `related_posts` → 관련 콘텐츠. 형식이 틀린 항목은 버리고 구버전 응답은 빈 배열. */\nfunction relatedPostsFromWire(raw: unknown): CmsRelatedPost[] {\n if (!Array.isArray(raw)) return [];\n const out: CmsRelatedPost[] = [];\n for (const item of raw) {\n if (!item || typeof item !== \"object\") continue;\n const rec = item as Record<string, unknown>;\n if (typeof rec.id !== \"string\" || typeof rec.title !== \"string\" || typeof rec.slug !== \"string\") continue;\n out.push({\n id: rec.id,\n title: rec.title,\n slug: rec.slug,\n path: typeof rec.path === \"string\" ? rec.path : null,\n type: typeof rec.type === \"string\" ? rec.type : \"post\",\n modelKey: typeof rec.model_key === \"string\" ? rec.model_key : null,\n collectionKey: typeof rec.collection_key === \"string\" ? rec.collection_key : null,\n excerpt: typeof rec.excerpt === \"string\" ? rec.excerpt : null,\n featuredImageUrl: typeof rec.featured_media_url === \"string\" ? rec.featured_media_url : null,\n publishedAt: typeof rec.published_at === \"string\" ? rec.published_at : null,\n });\n }\n return out;\n}\n\nexport function postFromWire(wire: CmsPostWire): CmsPostContent {\n const imagePosition = (value: unknown): number | null =>\n typeof value === \"number\" && Number.isInteger(value) && value >= 0 && value <= 100\n ? value\n : null;\n return {\n id: wire.id,\n tenantId: wire.tenant_id,\n siteId: wire.site_id,\n type: wire.type,\n collectionKey: wire.collection_key ?? null,\n modelKey: wire.model_key ?? null,\n slug: wire.slug,\n ...(wire.locale !== undefined ? { locale: wire.locale } : {}),\n ...(wire.translations !== undefined ? { translations: wire.translations } : {}),\n ...(wire.previous_slugs !== undefined ? { previousSlugs: wire.previous_slugs } : {}),\n ...(wire.path !== undefined ? { path: wire.path } : {}),\n title: wire.title,\n excerpt: wire.excerpt,\n featuredMediaId: wire.featured_media_id,\n featuredImageUrl: wire.featured_media_url ?? null,\n authorProfileId: wire.author_profile_id ?? null,\n authorId: wire.author_id,\n authorName: wire.author_name ?? null,\n authorImageUrl: wire.author_image_url ?? null,\n authorImagePositionX: imagePosition(wire.author_image_position_x),\n authorImagePositionY: imagePosition(wire.author_image_position_y),\n authorBio: wire.author_bio ?? null,\n authorSlug: wire.author_slug ?? null,\n bodyJson: wire.body_json,\n // 구 서버(필드 없음) → null → body_json 폴백 = 종전 동작 그대로.\n bodyHtml: wire.body_html ?? null,\n status: wire.status,\n metaJson: wire.meta_json,\n fields: wire.fields ?? null,\n fieldsMeta: (wire.fields_meta ?? []).map((entry) => ({\n name: entry.name,\n label: entry.label,\n type: entry.type,\n ...(entry.choices\n ? {\n choices: entry.choices.map((c) => ({\n value: c.value,\n label: c.label,\n })),\n }\n : {}),\n })),\n terms: (wire.terms ?? []).map(({ term_order, parent_id, ...term }) => ({\n ...term,\n ...(term_order !== undefined ? { termOrder: term_order } : {}),\n ...(parent_id !== undefined ? { parentId: parent_id } : {}),\n })),\n archiveRelated: (wire.archive_related ?? []).map((group) => ({\n collectionKey: group.collection_key,\n label: group.label,\n items: group.items.map((item) => ({\n id: item.id,\n title: item.title,\n slug: item.slug,\n excerpt: item.excerpt,\n collectionKey: item.collection_key,\n })),\n })),\n ...(typeof wire.view_count === \"number\" ? { viewCount: wire.view_count } : {}),\n patternSlots: patternSlotsFromWire(wire.pattern_slots),\n relatedPosts: relatedPostsFromWire(wire.related_posts),\n publishedAt: wire.published_at,\n createdAt: wire.created_at,\n updatedAt: wire.updated_at,\n };\n}\n","import {\n type CmsPostContent,\n type CmsPostContentPage,\n type FetchPostOptions,\n type FetchPostsOptions,\n DEFAULT_BASE_URL,\n assertServer,\n buildGetInit,\n toApiError,\n} from \"./server-base\";\nimport { type CmsPostWire, postFromWire } from \"./server-post-wire\";\n\n/**\n * 외부 사이트가 cms-renderer 에 주입할 RootTale 디자인 토큰.\n * admin /design (admin.roottale.com) 에서 저장한 값과 동일한 shape.\n * 미설정 필드는 응답에서 누락 → renderer 의 CSS fallback 이 사용된다.\n */\nexport interface RootTaleTheme {\n colors?: {\n primary?: string;\n primaryForeground?: string;\n foreground?: string;\n background?: string;\n secondary?: string;\n secondaryForeground?: string;\n accent?: string;\n accentForeground?: string;\n muted?: string;\n mutedForeground?: string;\n destructive?: string;\n destructiveForeground?: string;\n border?: string;\n input?: string;\n ring?: string;\n };\n fonts?: {\n body?: string;\n display?: string;\n };\n radius?: {\n sm?: string;\n md?: string;\n lg?: string;\n xl?: string;\n };\n /**\n * W6-3 — `sites.name`(DB SSoT, `rt init --name` 이 저장) 미러. 외부 사이트가\n * `<title>`/OG/헤더·푸터 브랜드명을 site.config.ts 정적 리터럴 대신 런타임\n * 반영하는 데 쓴다. 미설정이면 소비자가 site.config.ts 값으로 fallback.\n */\n siteName?: string;\n announcement?: AnnouncementSettings;\n actionDock?: ActionDockSettings;\n inquiryBoard?: InquiryBoardSettings;\n siteNav?: SiteNavSettings;\n /**\n * ROOT-ADMIN 연동 설정에 입력한 검색엔진 소유 확인 코드. `siteVerificationMetadata`\n * 로 Next metadata `verification` 모양으로 바꿔 루트 layout 에 출력한다.\n */\n siteVerification?: SiteVerificationSettings;\n}\n\n/** 엔진별 소유 확인 코드(고객·RootTale 계정이 각각 확인할 수 있어 여러 개). */\nexport interface SiteVerificationSettings {\n google?: string[];\n naver?: string[];\n}\n\nexport type AnnouncementVariant = \"single\" | \"rotating\" | \"cta\";\nexport type AnnouncementTone = \"default\" | \"primary\";\n\nexport interface AnnouncementItem {\n text: string;\n href?: string;\n ctaLabel?: string;\n}\n\nexport interface AnnouncementSettings {\n enabled: boolean;\n variant: AnnouncementVariant;\n tone: AnnouncementTone;\n items: AnnouncementItem[];\n startsAt?: string;\n endsAt?: string;\n}\n\nexport type ActionDockKind = \"tel\" | \"link\" | \"map\" | \"channel\";\nexport type ActionDockPosition = \"bottom\" | \"side\";\n\nexport interface ActionDockAction {\n label: string;\n href: string;\n kind: ActionDockKind;\n}\n\nexport interface ActionDockSettings {\n enabled: boolean;\n actions: ActionDockAction[];\n position: ActionDockPosition;\n collapseAt?: number;\n}\n\nexport interface InquiryBoardSettings {\n enabled: boolean;\n}\n\nexport interface SiteNavLink {\n label: string;\n href: string;\n description?: string;\n}\n\nexport interface SiteNavColumn {\n title?: string;\n links: SiteNavLink[];\n}\n\nexport interface SiteNavGroup {\n label: string;\n href?: string;\n columns?: SiteNavColumn[];\n}\n\nexport interface SiteNavCta {\n label: string;\n href: string;\n}\n\nexport interface SiteNavSettings {\n navGroups: SiteNavGroup[];\n cta?: SiteNavCta;\n utilityLinks?: SiteNavLink[];\n}\n\n/**\n * 테마 fetch 캐시 tag 의 정본 이름 (W0-4). starter layout 의 `fetchTheme` 호출에\n * `tags: [THEME_CACHE_TAG]` 로 부착하고, revalidate route 가 `theme.updated`\n * 수신 시 `revalidateTag(THEME_CACHE_TAG)` 로 무효화한다. 양쪽이 이 상수를 공유해\n * drift 를 방지한다.\n */\nexport const THEME_CACHE_TAG = \"rt-theme\";\n\nexport interface FetchThemeOptions {\n apiKey: string;\n baseUrl?: string;\n siteId?: string;\n signal?: AbortSignal;\n /** ISR revalidate 초(opt-in, W0-9). 미지정 시 현행 동작 유지. */\n revalidate?: number;\n /**\n * on-demand 무효화용 Next fetch cache tags(opt-in, W0-4). 보통\n * `[THEME_CACHE_TAG]`. 미지정 시 현행 동작 유지.\n */\n tags?: string[];\n}\n\n/**\n * tenant 의 RootTale 디자인 토큰을 가져온다. admin /design 의 마지막 저장값\n * (없으면 빈 객체) — renderer 가 이 값을 CSS 변수로 주입한다.\n *\n * 자동 fetch (renderer 가 theme prop 없을 때) 와 명시 호출 모두 지원.\n * 네트워크/서버 에러는 throw — 호출자가 fallback 처리.\n */\nexport async function fetchTheme(options: FetchThemeOptions): Promise<RootTaleTheme> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams();\n if (options.siteId) params.set(\"site_id\", options.siteId);\n const qs = params.toString();\n const url = `${baseUrl}/v1/cms/public/theme${qs ? `?${qs}` : \"\"}`;\n\n const response = await fetch(url, buildGetInit(options.apiKey, options));\n\n if (!response.ok) {\n throw await toApiError(response);\n }\n\n const json = (await response.json()) as {\n colors?: RootTaleTheme[\"colors\"];\n fonts?: RootTaleTheme[\"fonts\"];\n radius?: RootTaleTheme[\"radius\"];\n site_name?: string;\n announcement?: AnnouncementSettings;\n actionDock?: ActionDockSettings;\n inquiryBoard?: InquiryBoardSettings;\n siteNav?: SiteNavSettings;\n siteVerification?: SiteVerificationSettings;\n };\n const theme: RootTaleTheme = {};\n if (json.colors) theme.colors = json.colors;\n if (json.fonts) theme.fonts = json.fonts;\n if (json.radius) theme.radius = json.radius;\n if (json.site_name) theme.siteName = json.site_name;\n if (json.announcement) theme.announcement = json.announcement;\n if (json.actionDock) theme.actionDock = json.actionDock;\n if (json.inquiryBoard) theme.inquiryBoard = json.inquiryBoard;\n if (json.siteNav) theme.siteNav = json.siteNav;\n if (json.siteVerification) theme.siteVerification = json.siteVerification;\n return theme;\n}\n\nexport type AnalyticsTagProvider = \"ga4\" | \"clarity\" | \"meta_pixel\" | \"naver\";\n\n/** analytics-runtime `TagConfig` 와 구조 호환 — `buildManifest` 입력. */\nexport interface AnalyticsTagConfig {\n provider: AnalyticsTagProvider;\n id: string;\n enabled: boolean;\n}\n\nexport interface SiteAnalyticsTags {\n siteId: string;\n tags: AnalyticsTagConfig[];\n}\n\nexport interface FetchAnalyticsOptions {\n apiKey: string;\n baseUrl?: string;\n siteId?: string;\n signal?: AbortSignal;\n /** ISR revalidate 초(opt-in, W0-9). 미지정 시 현행 동작 유지. */\n revalidate?: number;\n}\n\n/**\n * ADR-0038 plane ① — 외부 사이트가 `<head>` 에 주입할 런타임 태그(GA4 measurement\n * id / Clarity project id 등, 비밀 아님). RootTale 관리용 default + 테넌트 고객용\n * 설정을 머지한 결과를 반환한다. analytics-runtime `buildManifest` 입력으로 쓰고,\n * 동의는 항상 fail-closed(런타임이 처리)이므로 키 노출 위험 없음(public id).\n */\nexport async function fetchAnalyticsConfig(\n options: FetchAnalyticsOptions,\n): Promise<SiteAnalyticsTags> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams();\n if (options.siteId) params.set(\"site_id\", options.siteId);\n const qs = params.toString();\n const url = `${baseUrl}/v1/cms/public/analytics${qs ? `?${qs}` : \"\"}`;\n\n const response = await fetch(url, buildGetInit(options.apiKey, options));\n\n if (!response.ok) {\n throw await toApiError(response);\n }\n\n const json = (await response.json()) as {\n site_id: string;\n tags?: AnalyticsTagConfig[];\n };\n return { siteId: json.site_id, tags: json.tags ?? [] };\n}\n\n\nexport async function fetchPosts(options: FetchPostsOptions): Promise<CmsPostContentPage> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams();\n if (options.limit !== undefined) params.set(\"limit\", String(options.limit));\n if (options.cursor) params.set(\"cursor\", options.cursor);\n if (options.type) params.set(\"type\", options.type);\n if (options.author) params.set(\"author\", options.author);\n if (options.siteId) params.set(\"site_id\", options.siteId);\n if (options.collectionKey) params.set(\"collection_key\", options.collectionKey);\n if (options.modelKey) params.set(\"model_key\", options.modelKey);\n if (options.excludeCollections) params.set(\"exclude_collections\", \"true\");\n if (options.locale) params.set(\"locale\", options.locale);\n const qs = params.toString();\n const url = `${baseUrl}/v1/cms/public/posts${qs ? `?${qs}` : \"\"}`;\n\n const response = await fetch(url, buildGetInit(options.apiKey, options));\n\n if (!response.ok) {\n throw await toApiError(response);\n }\n\n const json = (await response.json()) as {\n items: CmsPostWire[];\n has_more: boolean;\n next_cursor: string | null;\n };\n return {\n items: json.items.map(postFromWire),\n hasMore: json.has_more,\n nextCursor: json.next_cursor,\n };\n}\n\nexport async function fetchPost(options: FetchPostOptions): Promise<CmsPostContent | null> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams();\n if (options.siteId) params.set(\"site_id\", options.siteId);\n if (options.locale) params.set(\"locale\", options.locale);\n const qs = params.toString();\n const url = `${baseUrl}/v1/cms/public/posts/${encodeURIComponent(options.slugOrId)}${qs ? `?${qs}` : \"\"}`;\n\n const response = await fetch(url, buildGetInit(options.apiKey, options));\n\n if (response.status === 404) return null;\n if (!response.ok) {\n throw await toApiError(response);\n }\n\n const json = (await response.json()) as CmsPostWire;\n return postFromWire(json);\n}\n\nexport interface FetchPageBySlugOptions {\n readonly apiKey: string;\n readonly baseUrl?: string;\n readonly slug: string;\n readonly siteId?: string;\n /** ADR-0052 A안 (W4-6 PR C1) — `FetchPostsOptions.locale` 과 동일 시맨틱. */\n readonly locale?: string;\n readonly signal?: AbortSignal;\n readonly revalidate?: number;\n}\n\nexport async function fetchPageBySlug(\n options: FetchPageBySlugOptions,\n): Promise<CmsPostContent | null> {\n let cursor: string | undefined;\n do {\n const page = await fetchPosts({\n apiKey: options.apiKey,\n baseUrl: options.baseUrl,\n limit: 100,\n ...(cursor ? { cursor } : {}),\n type: \"page\",\n siteId: options.siteId,\n ...(options.locale ? { locale: options.locale } : {}),\n signal: options.signal,\n revalidate: options.revalidate,\n });\n const match = page.items.find((item) => item.slug === options.slug);\n if (match) return match;\n cursor = page.nextCursor ?? undefined;\n } while (cursor);\n return null;\n}\n\n","import {\n DEFAULT_BASE_URL,\n assertServer,\n buildGetInit,\n toApiError,\n type CmsPostContent,\n} from \"./server-base\";\n\n/**\n * 블로그 표시 설정 — admin /design 의 \"블로그 표시\" 에서 저장한 사이트 기본값.\n * renderer 가 prop 미지정 시 이 값을 기본으로 적용 (글별 override 는 post 의\n * metaJson.display 가 우선).\n */\n/** @deprecated 전환 흐름은 사이트 공통 블록이나 프로젝트 코드가 소유한다. */\nexport interface RootTaleBlogPostCta {\n title: string | null;\n description: string | null;\n buttonLabel: string;\n buttonHref: string;\n}\n\n/**\n * 사이트 레벨 SEO 기본값 — 외부 사이트 `<head>`(설명·로고·파비콘)와 글별 OG\n * 미설정 시 공유 썸네일 폴백(defaultOgImageUrl)에 쓴다. 모두 nullable.\n */\nexport interface RootTaleSiteProfile {\n siteDescription: string | null;\n /** 메인 홈(`/`) 검색 제목. null 이면 사이트 이름(theme `siteName`)을 쓴다. */\n homeTitle: string | null;\n /** 메인 홈(`/`) 검색 설명. null 이면 siteDescription 을 쓴다. */\n homeDescription: string | null;\n logoUrl: string | null;\n faviconUrl: string | null;\n /** 글별 og:image 미설정 시 폴백(1200×630 권장). */\n defaultOgImageUrl: string | null;\n}\n\n/** @deprecated 사이트맵 구조는 사이트 프로젝트 코드가 소유한다. 응답 호환 전용. */\nexport interface RootTaleSitemapSettings {\n /** 블로그 글 entry 에 대표 이미지(featuredImageUrl)를 `<image:image>` 로 포함. */\n blogImages: boolean;\n /** 다국어 hreflang locales (BCP-47). 빈 배열 = 단일 로케일(alternates 미emit). */\n locales: string[];\n /** authors 하위 사이트맵 포함 (작가 아카이브 라우트 필요). */\n authors: boolean;\n /**\n * 색인 위생(seo-geo-contract v3.1 §1) — \"검색에 이 분류 페이지 노출\" 을 켠\n * 카테고리 slug 목록. 아카이브는 기본 noindex 라서 여기 없는 카테고리의 모음\n * 페이지는 sitemap 에서 빠진다.\n *\n * **`undefined` = 승격 정보를 모름** (구버전 API 응답이거나 admin 설정 fetch 를\n * 생략한 경로). 이때 `createSitemapIndex` 는 정책을 적용하지 않고 기존 동작을\n * 유지한다 — 모른다는 이유로 이미 색인된 URL 을 지우지 않기 위해서다.\n *\n * 이 구분을 소비처가 직접 해석하지 말고 cms-renderer-next 의\n * `resolveCategoryPromotion(slugs, slug)` 에 그대로 넘길 것 — 사이트맵과\n * 아카이브 페이지가 같은 3-state(`promoted`/`not-promoted`/`unknown`)를 써야\n * 두 소스의 결론이 갈리지 않는다.\n */\n promotedCategorySlugs?: string[];\n}\n\nexport interface RootTaleBlogSettings {\n showTableOfContents: boolean;\n showAuthor: boolean;\n showDate: boolean;\n showAuthorCard: boolean;\n /** 목차 상단 제목. null = renderer 기본(\"목차\"). */\n tocTitle: string | null;\n /**\n * 목차 배치. \"inline\"=본문 위 접이식(기본), \"sidebar\"=넓은 화면에서 본문\n * 오른쪽 sticky 사이드바(좁은 화면은 자동 인라인 폴백).\n */\n tocPosition: \"inline\" | \"sidebar\";\n /** 작성자 카드 상단 라벨. null = renderer 기본(\"작성자\"). */\n authorCardEyebrow: string | null;\n /** @deprecated 작성자 소개는 글의 authorBio를 사용한다. 항상 null. */\n authorCardDescription: string | null;\n /** @deprecated 작성자 이름은 글의 authorName을 사용한다. 항상 null. */\n authorProfileName: string | null;\n /** @deprecated 작성자 소개는 글의 authorBio를 사용한다. 항상 null. */\n authorProfileBio: string | null;\n /** @deprecated 작성자 사진은 글의 authorImageUrl을 사용한다. 항상 null. */\n authorProfileImageUrl: string | null;\n /** @deprecated 렌더러가 자동 적용하지 않는다. 사이트 시멘틱 토큰/CSS에서 관리한다. */\n authorProfileImageRadius: \"circle\" | \"rounded\" | \"square\";\n /** @deprecated 렌더러가 자동 적용하지 않는다. 사이트 콘텐츠 데이터에서 관리한다. */\n authorProfileImagePositionX: number;\n /** @deprecated 렌더러가 자동 적용하지 않는다. 사이트 콘텐츠 데이터에서 관리한다. */\n authorProfileImagePositionY: number;\n /** 사이트 레벨 SEO 기본값(설명·로고·파비콘·기본 OG). */\n siteProfile: RootTaleSiteProfile;\n /** @deprecated 렌더러가 자동 적용하지 않는다. 응답 호환 전용. */\n postCta: RootTaleBlogPostCta | null;\n /**\n * @deprecated 사이트맵 정책은 사이트 코드에서 명시한다. optional — 구버전\n * 응답/직접 구성한 객체와의 하위호환을 위해 보존한다.\n */\n sitemap?: RootTaleSitemapSettings;\n}\n\n/**\n * 블로그 표시 설정 fetch 캐시 tag 의 정본 이름 (ADR-0096 Phase C).\n * `fetchBlogSettings` 호출에 `tags: [BLOG_SETTINGS_CACHE_TAG]` 로 부착하고,\n * revalidate route 가 `theme.updated` 수신 시\n * `revalidateTag(BLOG_SETTINGS_CACHE_TAG)` 로 무효화한다. 양쪽이 이 상수를\n * 공유해 drift 를 방지한다.\n */\nexport const BLOG_SETTINGS_CACHE_TAG = \"rt-blog-settings\";\n\nexport interface FetchBlogSettingsOptions {\n apiKey: string;\n baseUrl?: string;\n siteId?: string;\n signal?: AbortSignal;\n /** ISR revalidate 초(opt-in, W0-9). 미지정 시 현행 동작 유지. */\n revalidate?: number;\n /**\n * on-demand 무효화용 Next fetch cache tags(opt-in, ADR-0096 Phase C). 보통\n * `[BLOG_SETTINGS_CACHE_TAG]`. 미지정 시 현행 동작 유지.\n */\n tags?: string[];\n}\n\n/**\n * tenant 의 블로그 표시 기본값을 가져온다. 네트워크/서버 에러는 throw —\n * 호출자(renderer)가 안전 기본값으로 fallback.\n */\nexport async function fetchBlogSettings(\n options: FetchBlogSettingsOptions,\n): Promise<RootTaleBlogSettings> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams();\n if (options.siteId) params.set(\"site_id\", options.siteId);\n const qs = params.toString();\n const url = `${baseUrl}/v1/cms/public/blog-settings${qs ? `?${qs}` : \"\"}`;\n\n const response = await fetch(url, buildGetInit(options.apiKey, options));\n\n if (!response.ok) {\n throw await toApiError(response);\n }\n\n const json = (await response.json()) as {\n show_table_of_contents?: boolean;\n show_author?: boolean;\n show_date?: boolean;\n show_author_card?: boolean;\n toc_title?: string | null;\n toc_position?: string | null;\n author_card_eyebrow?: string | null;\n author_card_description?: string | null;\n author_profile_name?: string | null;\n author_profile_bio?: string | null;\n author_profile_image_url?: string | null;\n author_profile_image_radius?: string | null;\n author_profile_image_position_x?: number;\n author_profile_image_position_y?: number;\n site_profile?: {\n site_description?: string | null;\n home_title?: string | null;\n home_description?: string | null;\n logo_url?: string | null;\n favicon_url?: string | null;\n default_og_image_url?: string | null;\n } | null;\n post_cta?: {\n title?: string | null;\n description?: string | null;\n button_label?: string;\n button_href?: string;\n } | null;\n sitemap?: {\n blog_images?: boolean;\n locales?: string[];\n authors?: boolean;\n promoted_category_slugs?: string[];\n } | null;\n };\n const radius =\n json.author_profile_image_radius === \"rounded\" ||\n json.author_profile_image_radius === \"square\"\n ? json.author_profile_image_radius\n : \"circle\";\n const percent = (x: unknown): number =>\n typeof x === \"number\" && Number.isFinite(x)\n ? Math.max(0, Math.min(100, Math.round(x)))\n : 50;\n const nonEmpty = (x: unknown): string | null =>\n typeof x === \"string\" && x.length > 0 ? x : null;\n return {\n showTableOfContents: json.show_table_of_contents === true,\n showAuthor: json.show_author !== false,\n showDate: json.show_date !== false,\n showAuthorCard: json.show_author_card !== false,\n tocTitle:\n typeof json.toc_title === \"string\" && json.toc_title.length > 0\n ? json.toc_title\n : null,\n tocPosition: json.toc_position === \"sidebar\" ? \"sidebar\" : \"inline\",\n authorCardEyebrow:\n typeof json.author_card_eyebrow === \"string\" &&\n json.author_card_eyebrow.length > 0\n ? json.author_card_eyebrow\n : null,\n authorCardDescription:\n typeof json.author_card_description === \"string\" &&\n json.author_card_description.length > 0\n ? json.author_card_description\n : null,\n authorProfileName:\n typeof json.author_profile_name === \"string\" &&\n json.author_profile_name.length > 0\n ? json.author_profile_name\n : null,\n authorProfileBio:\n typeof json.author_profile_bio === \"string\" &&\n json.author_profile_bio.length > 0\n ? json.author_profile_bio\n : null,\n authorProfileImageUrl:\n typeof json.author_profile_image_url === \"string\" &&\n json.author_profile_image_url.length > 0\n ? json.author_profile_image_url\n : null,\n authorProfileImageRadius: radius,\n authorProfileImagePositionX: percent(json.author_profile_image_position_x),\n authorProfileImagePositionY: percent(json.author_profile_image_position_y),\n siteProfile: {\n siteDescription: nonEmpty(json.site_profile?.site_description),\n homeTitle: nonEmpty(json.site_profile?.home_title),\n homeDescription: nonEmpty(json.site_profile?.home_description),\n logoUrl: nonEmpty(json.site_profile?.logo_url),\n faviconUrl: nonEmpty(json.site_profile?.favicon_url),\n defaultOgImageUrl: nonEmpty(json.site_profile?.default_og_image_url),\n },\n postCta:\n json.post_cta &&\n typeof json.post_cta.button_label === \"string\" &&\n json.post_cta.button_label.length > 0 &&\n typeof json.post_cta.button_href === \"string\" &&\n json.post_cta.button_href.length > 0\n ? {\n title:\n typeof json.post_cta.title === \"string\" &&\n json.post_cta.title.length > 0\n ? json.post_cta.title\n : null,\n description:\n typeof json.post_cta.description === \"string\" &&\n json.post_cta.description.length > 0\n ? json.post_cta.description\n : null,\n buttonLabel: json.post_cta.button_label,\n buttonHref: json.post_cta.button_href,\n }\n : null,\n sitemap: {\n blogImages: json.sitemap?.blog_images !== false,\n locales: Array.isArray(json.sitemap?.locales)\n ? json.sitemap.locales.filter(\n (s): s is string => typeof s === \"string\" && s.length > 0,\n )\n : [],\n authors: json.sitemap?.authors === true,\n // 배열이 아니면(구버전 API 응답) 필드 자체를 생략 = \"승격 정보 모름\".\n // 빈 배열([])은 \"승격된 카테고리가 하나도 없음\" 이라는 확정 정보라 의미가\n // 다르다 — 이 구분이 사이트맵 필터의 fail-soft 근거다.\n ...(Array.isArray(json.sitemap?.promoted_category_slugs)\n ? {\n promotedCategorySlugs: json.sitemap.promoted_category_slugs.filter(\n (s): s is string => typeof s === \"string\" && s.length > 0,\n ),\n }\n : {}),\n },\n };\n}\n\n// ── 콘텐츠 Collections (ADR-0060) ────────────────────────────────────────────\n\n/**\n * 콘텐츠 스트림 1개 (camelCase). cms-core `RouteCollection` 과 구조 호환 —\n * `createSitemap`/`createFeedRoute` 의 `collections` 에 그대로 넘길 수 있다.\n */\nexport interface CmsRouteCollection {\n key: string;\n label: string;\n /** \"\" = 주소 없는 유형(라우트 없음) — 페이지 안에서 불러와 쓰는 콘텐츠. */\n basePath: string;\n layout: string;\n categories: string[];\n feed: boolean;\n archives: boolean;\n og: boolean;\n requireSubcategory: boolean;\n pinning: boolean;\n icon: string | null;\n /** 글별 상세 라우트 존재 여부 (기본 true — 하위 호환). false = 목록 전용 유형. */\n detail: boolean;\n detailPath: \"flat\" | \"category\";\n categoryPath: \"namespaced\" | \"direct\";\n categoryCardinality: \"multiple\" | \"exactly-one\";\n}\n\n/**\n * 콘텐츠 유형(collections) fetch 캐시 tag 의 정본 이름 (ADR-0096 Phase C).\n * `fetchCollections` 호출에 `tags: [COLLECTIONS_CACHE_TAG]` 로 부착하고,\n * revalidate route 가 `theme.updated` 수신 시\n * `revalidateTag(COLLECTIONS_CACHE_TAG)` 로 무효화한다. 양쪽이 이 상수를 공유해\n * drift 를 방지한다.\n */\nexport const COLLECTIONS_CACHE_TAG = \"rt-collections\";\n\nexport interface FetchCollectionsOptions {\n apiKey: string;\n baseUrl?: string;\n siteId?: string;\n signal?: AbortSignal;\n /** ISR revalidate 초(opt-in, W0-9). 미지정 시 현행 동작 유지. */\n revalidate?: number;\n /**\n * on-demand 무효화용 Next fetch cache tags(opt-in, ADR-0096 Phase C). 보통\n * `[COLLECTIONS_CACHE_TAG]`. 미지정 시 현행 동작 유지.\n */\n tags?: string[];\n}\n\n/**\n * 사이트의 콘텐츠 Collections (공지/블로그 등 스트림 분리)를 가져온다. 미설정이면\n * 빈 배열(레거시 단일 /blog). 네트워크/서버 에러는 throw — 호출자가 빈 배열로\n * fail-soft 하도록 한다(렌더가 collections 때문에 죽지 않는 것이 계약).\n */\nexport async function fetchCollections(\n options: FetchCollectionsOptions,\n): Promise<CmsRouteCollection[]> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams();\n if (options.siteId) params.set(\"site_id\", options.siteId);\n const qs = params.toString();\n const url = `${baseUrl}/v1/cms/public/collections${qs ? `?${qs}` : \"\"}`;\n\n const response = await fetch(url, buildGetInit(options.apiKey, options));\n if (!response.ok) {\n throw await toApiError(response);\n }\n\n const json = (await response.json()) as {\n collections?: Array<Record<string, unknown>>;\n };\n const items = Array.isArray(json.collections) ? json.collections : [];\n return items.flatMap((o) => {\n const key = typeof o.key === \"string\" ? o.key : \"\";\n // \"\" = 주소 없는 유형(라우트/사이트맵/피드/아카이브 파생 없음, ADR-0060) — key\n // 만 있으면 유지한다. 소속 판정(`postsNotInAnyCollection` 방어선)의 시맨틱을\n // 서버(declaredKeys=key 기준)와 맞추기 위해 basePath 없다고 드롭하지 않는다.\n const basePath = typeof o.base_path === \"string\" ? o.base_path : \"\";\n if (!key) return [];\n return [\n {\n key,\n basePath,\n label: typeof o.label === \"string\" ? o.label : key,\n layout: typeof o.layout === \"string\" ? o.layout : \"grid\",\n categories: Array.isArray(o.categories)\n ? o.categories.filter((s): s is string => typeof s === \"string\")\n : [],\n feed: o.feed === true,\n archives: o.archives === true,\n og: o.og === true,\n requireSubcategory: o.require_subcategory === true,\n pinning: o.pinning === true,\n icon: typeof o.icon === \"string\" ? o.icon : null,\n // 기본 true(하위 호환) — 명시적으로 false 인 경우만 목록 전용.\n detail: o.detail !== false,\n detailPath: o.detail_path === \"category\" ? \"category\" : \"flat\",\n categoryPath: o.category_path === \"direct\" ? \"direct\" : \"namespaced\",\n categoryCardinality:\n o.category_cardinality === \"exactly-one\" ? \"exactly-one\" : \"multiple\",\n },\n ];\n });\n}\n\n/**\n * fetch 실패/미설정 시 안전 기본값 — 빈 배열(레거시 단일 /blog). 사이트가\n * `fetchCollections().catch(() => fetchCollectionsSafeDefault())` 로 쓴다.\n */\nexport const fetchCollectionsSafeDefault: CmsRouteCollection[] = [];\n\n// ── 콘텐츠 모델 ──────────────────────────────────────────────────────────────\n\nexport interface CmsContentModel {\n key: string;\n label: string;\n cardinality: \"singleton\" | \"collection\";\n preset: \"page\" | \"article\" | \"entity\" | null;\n source: \"code\" | \"site\";\n presentation:\n | { readonly kind: \"data_only\"; readonly listPaths?: readonly string[] }\n | {\n readonly kind: \"fixed_page\";\n readonly path: string;\n readonly templateKey?: string;\n readonly allowedSections: readonly string[];\n }\n | {\n readonly kind: \"detail\";\n readonly detailPath: string;\n readonly templateKey?: string;\n /** 글을 목록으로 보여 주는 고정 화면(홈·회사소개 등). 글 변경 웹훅에 함께 실린다. */\n readonly listPaths?: readonly string[];\n }\n | {\n readonly kind: \"category_tree\";\n readonly basePath: string;\n readonly categoryDepth: number;\n /** `leaf`면 하위 분류가 없는 분류에 단계와 무관하게 글을 붙인다. 생략은 `deepest`. */\n readonly entryCategory?: \"deepest\" | \"leaf\";\n readonly categorySelection?: \"first\";\n readonly categoryFallbackSlug?: string;\n readonly reservedCategorySlugs?: readonly string[];\n readonly templateKey?: string;\n readonly listPaths?: readonly string[];\n };\n}\n\nexport interface FetchContentModelsOptions {\n apiKey: string;\n baseUrl?: string;\n siteId?: string;\n signal?: AbortSignal;\n revalidate?: number;\n tags?: string[];\n}\n\n/** 활성 페이지·글·정보 모델의 안전한 공개 projection을 가져온다. */\nexport async function fetchContentModels(\n options: FetchContentModelsOptions,\n): Promise<CmsContentModel[]> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams();\n if (options.siteId) params.set(\"site_id\", options.siteId);\n const qs = params.toString();\n const response = await fetch(\n `${baseUrl}/v1/cms/public/content-models${qs ? `?${qs}` : \"\"}`,\n buildGetInit(options.apiKey, options),\n );\n if (!response.ok) throw await toApiError(response);\n\n const json = (await response.json()) as {\n models?: Array<Record<string, unknown>>;\n };\n return (Array.isArray(json.models) ? json.models : []).flatMap((model) => {\n if (typeof model.key !== \"string\" || typeof model.label !== \"string\") return [];\n const cardinality =\n model.cardinality === \"singleton\" ? \"singleton\" : \"collection\";\n const preset =\n model.preset === \"page\" || model.preset === \"article\" || model.preset === \"entity\"\n ? model.preset\n : null;\n const rawPresentation =\n model.presentation && typeof model.presentation === \"object\" &&\n !Array.isArray(model.presentation)\n ? model.presentation as Record<string, unknown>\n : {};\n const listPaths = Array.isArray(rawPresentation.listPaths)\n ? rawPresentation.listPaths.filter(\n (item): item is string => typeof item === \"string\" && item.startsWith(\"/\"),\n )\n : [];\n const withListPaths = listPaths.length > 0 ? { listPaths } : {};\n const presentation: CmsContentModel[\"presentation\"] =\n rawPresentation.kind === \"fixed_page\" && typeof rawPresentation.path === \"string\"\n ? {\n kind: \"fixed_page\",\n path: rawPresentation.path,\n allowedSections: Array.isArray(rawPresentation.allowedSections)\n ? rawPresentation.allowedSections.filter(\n (item): item is string => typeof item === \"string\",\n )\n : [],\n ...(typeof rawPresentation.templateKey === \"string\"\n ? { templateKey: rawPresentation.templateKey }\n : {}),\n }\n : rawPresentation.kind === \"detail\" && typeof rawPresentation.detailPath === \"string\"\n ? {\n kind: \"detail\",\n detailPath: rawPresentation.detailPath,\n ...(typeof rawPresentation.templateKey === \"string\"\n ? { templateKey: rawPresentation.templateKey }\n : {}),\n ...withListPaths,\n }\n : rawPresentation.kind === \"category_tree\" &&\n typeof rawPresentation.basePath === \"string\" &&\n typeof rawPresentation.categoryDepth === \"number\"\n ? {\n kind: \"category_tree\",\n basePath: rawPresentation.basePath,\n categoryDepth: rawPresentation.categoryDepth,\n ...(rawPresentation.categorySelection === \"first\" ? { categorySelection: \"first\" as const } : {}),\n ...(typeof rawPresentation.categoryFallbackSlug === \"string\" ? { categoryFallbackSlug: rawPresentation.categoryFallbackSlug } : {}),\n ...(Array.isArray(rawPresentation.reservedCategorySlugs)\n ? { reservedCategorySlugs: rawPresentation.reservedCategorySlugs.filter((slug): slug is string => typeof slug === \"string\") }\n : {}),\n ...(rawPresentation.entryCategory === \"leaf\" ||\n rawPresentation.entryCategory === \"deepest\"\n ? { entryCategory: rawPresentation.entryCategory }\n : {}),\n ...(typeof rawPresentation.templateKey === \"string\"\n ? { templateKey: rawPresentation.templateKey }\n : {}),\n ...withListPaths,\n }\n : { kind: \"data_only\", ...withListPaths };\n return [{\n key: model.key,\n label: model.label,\n cardinality,\n preset,\n source: model.source === \"site\" ? \"site\" : \"code\",\n presentation,\n }];\n });\n}\n\n// ── 작가 아카이브 (blanche식 /blog/author/{slug}) ────────────────────────────\n\n/** 작가 1명 (camelCase) — author_slug 가 있고 발행 글이 1건 이상인 작가만. */\nexport interface RootTaleAuthor {\n /** 아카이브 URL slug (/blog/author/{slug}). */\n slug: string;\n /** 표시 이름 (author_display_name > user.name 폴백). */\n name: string;\n imageUrl: string | null;\n imagePositionX?: number | null;\n imagePositionY?: number | null;\n bio: string | null;\n /** 발행 글(type=post) 수 (≥1). */\n postCount: number;\n /** 작가의 최근 글 수정일 (사이트맵 lastmod). */\n lastUpdatedAt: string | null;\n}\n\nexport interface FetchAuthorsOptions {\n apiKey: string;\n baseUrl?: string;\n siteId?: string;\n signal?: AbortSignal;\n /** ISR revalidate 초(opt-in, W0-9). 미지정 시 현행 동작 유지. */\n revalidate?: number;\n}\n\n/**\n * 사이트의 작가 목록(작가 아카이브·사이트맵 파생용)을 가져온다. author_slug 가 있고\n * 발행 글이 1건 이상인 작가만. 네트워크/서버 에러는 throw — 호출자(라우트/사이트맵\n * 팩토리)가 빈 배열로 fail-soft 한다.\n */\nexport async function fetchAuthors(\n options: FetchAuthorsOptions,\n): Promise<RootTaleAuthor[]> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams();\n if (options.siteId) params.set(\"site_id\", options.siteId);\n const qs = params.toString();\n const url = `${baseUrl}/v1/cms/public/authors${qs ? `?${qs}` : \"\"}`;\n\n const response = await fetch(url, buildGetInit(options.apiKey, options));\n if (!response.ok) {\n throw await toApiError(response);\n }\n\n const json = (await response.json()) as {\n authors?: Array<Record<string, unknown>>;\n };\n const items = Array.isArray(json.authors) ? json.authors : [];\n return items.flatMap((o) => {\n const slug = typeof o.slug === \"string\" ? o.slug : \"\";\n const name = typeof o.name === \"string\" ? o.name : \"\";\n if (!slug || !name) return [];\n return [\n {\n slug,\n name,\n imageUrl: typeof o.image_url === \"string\" ? o.image_url : null,\n imagePositionX:\n typeof o.image_position_x === \"number\" ? o.image_position_x : null,\n imagePositionY:\n typeof o.image_position_y === \"number\" ? o.image_position_y : null,\n bio: typeof o.bio === \"string\" ? o.bio : null,\n postCount: typeof o.post_count === \"number\" ? o.post_count : 0,\n lastUpdatedAt:\n typeof o.last_updated_at === \"string\" ? o.last_updated_at : null,\n },\n ];\n });\n}\n\n/** fetch 실패/미설정 시 사용할 안전 기본값 (기존 렌더러 동작). */\nexport const DEFAULT_BLOG_SETTINGS: RootTaleBlogSettings = {\n showTableOfContents: false,\n showAuthor: true,\n showDate: true,\n showAuthorCard: true,\n tocTitle: null,\n tocPosition: \"inline\",\n authorCardEyebrow: null,\n authorCardDescription: null,\n authorProfileName: null,\n authorProfileBio: null,\n authorProfileImageUrl: null,\n authorProfileImageRadius: \"circle\",\n authorProfileImagePositionX: 50,\n authorProfileImagePositionY: 50,\n siteProfile: {\n siteDescription: null,\n homeTitle: null,\n homeDescription: null,\n logoUrl: null,\n faviconUrl: null,\n defaultOgImageUrl: null,\n },\n postCta: null,\n sitemap: {\n blogImages: true,\n locales: [],\n authors: false,\n // 이 기본값은 **블로그 렌더링 폴백 전용**이다 (cms-renderer-next 의\n // `fetchBlogSettingsFailSoft` = `fetchBlogSettings(...).catch(() => DEFAULT_BLOG_SETTINGS)`,\n // 소비처는 `server/blog-list.tsx`·`server/blog-post.tsx` 뿐 — 둘 다 색인\n // 정책과 무관한 본문 표시 설정만 읽는다).\n //\n // **색인 정책 경로는 이 상수를 쓰지 않는다** — 사이트맵은\n // `resolveSitemapSettings`(cms-renderer-next/routes-sitemap-entries.ts)가\n // 자체 try/catch 로 `remote = {}` 를 쓰고, 아카이브 페이지는 조회 실패 시\n // 설정 객체 자체를 `undefined` 로 넘긴다. 그래서 두 경로 모두 fetch 실패가\n // `promotedCategorySlugs === undefined`(= 승격 정보 모름) 로 전달돼\n // `resolveCategoryPromotion` 이 `unknown` 을 만들고 기존 URL 이 보존된다.\n // 여기 `[]` 는 \"승격 0건\"(확정) 이지만 그 의미가 색인 정책까지 전달되는\n // 경로는 없다 — 새 소비처를 만들 때 이 상수를 승격 판정에 넘기지 말 것.\n promotedCategorySlugs: [],\n },\n};\n\n\nexport interface ResolvedPostDisplay {\n showTableOfContents: boolean;\n showAuthor: boolean;\n showDate: boolean;\n showAuthorCard: boolean;\n tocTitle: string;\n tocPosition: \"inline\" | \"sidebar\";\n authorCardEyebrow: string;\n authorCardDescription: string | null;\n authorProfileName: string | null;\n authorProfileBio: string | null;\n authorProfileImageUrl: string | null;\n authorProfileImageRadius: \"circle\" | \"rounded\" | \"square\";\n authorProfileImagePositionX: number;\n authorProfileImagePositionY: number;\n}\n\n/**\n * 글 1건의 실제 표시 여부 결정. 우선순위:\n * 명시 prop > 글별 override(post.metaJson.display) > 사이트 기본값(settings).\n * tocTitle: 명시 prop > settings.tocTitle > \"목차\".\n */\nexport function resolvePostDisplay(\n post: { metaJson?: Record<string, unknown> | null },\n settings: RootTaleBlogSettings,\n explicit?: { showTableOfContents?: boolean; tableOfContentsTitle?: string },\n): ResolvedPostDisplay {\n const display =\n post.metaJson && typeof post.metaJson === \"object\"\n ? ((post.metaJson as Record<string, unknown>).display as\n | Record<string, unknown>\n | undefined)\n : undefined;\n const ov = (key: string): boolean | undefined =>\n display && typeof display[key] === \"boolean\"\n ? (display[key] as boolean)\n : undefined;\n return {\n showTableOfContents:\n explicit?.showTableOfContents ?? ov(\"toc\") ?? settings.showTableOfContents,\n showAuthor: ov(\"author\") ?? settings.showAuthor,\n showDate: ov(\"date\") ?? settings.showDate,\n showAuthorCard: settings.showAuthorCard,\n tocTitle: explicit?.tableOfContentsTitle ?? settings.tocTitle ?? \"목차\",\n tocPosition: settings.tocPosition ?? \"inline\",\n authorCardEyebrow: settings.authorCardEyebrow ?? \"작성자\",\n authorCardDescription: settings.authorCardDescription,\n authorProfileName: settings.authorProfileName,\n authorProfileBio: settings.authorProfileBio,\n authorProfileImageUrl: settings.authorProfileImageUrl,\n authorProfileImageRadius: settings.authorProfileImageRadius,\n authorProfileImagePositionX: settings.authorProfileImagePositionX,\n authorProfileImagePositionY: settings.authorProfileImagePositionY,\n };\n}\n\n/**\n * 관련 글 선별 — 현재 글과 **같은 카테고리**를 1개 이상 공유하는 후보를\n * 발행일 내림차순으로 골라 최대 `limit` 개 반환 (자기 자신 제외).\n *\n * 순수 함수: 후보(`candidates`)는 호출부가 `fetchPosts` 로 미리 받아 전달한다\n * (서버에 카테고리 필터 파라미터가 없어 최근 글 배치에서 선별). 현재 글에\n * 카테고리가 없으면 빈 배열.\n */\nexport function selectRelatedPosts(\n current: CmsPostContent,\n candidates: CmsPostContent[],\n limit = 3,\n): CmsPostContent[] {\n if (limit <= 0) return [];\n const currentCats = new Set(\n current.terms\n .filter((t) => t.taxonomy === \"category\")\n .map((t) => t.id),\n );\n if (currentCats.size === 0) return [];\n return candidates\n .filter(\n (p) =>\n p.id !== current.id &&\n p.terms.some(\n (t) => t.taxonomy === \"category\" && currentCats.has(t.id),\n ),\n )\n .sort((a, b) => b.publishedAt.localeCompare(a.publishedAt))\n .slice(0, limit);\n}\n","/**\n * 분류(category)별 발행 글 수 — **서버측 집계** 조회.\n *\n * 분류 아카이브의 \"0건 = 404\" 판정과 사이트맵 포함 여부(색인 위생)를 정확히\n * 계산하기 위한 값이다. 이 API 가 없던 시절 렌더러는 최근 글 배치를 받아 세었고,\n * 글이 많은 사이트에서는 배치 한도 밖에만 글이 있는 분류가 페이지·사이트맵에서\n * 함께 사라졌다. 소비처(`@roottale/cms-renderer-next`)는 이 함수를 먼저 쓰고,\n * 실패하면 예전 배치 파생으로 fail-soft 한다.\n */\n\nimport {\n CmsApiError,\n DEFAULT_BASE_URL,\n assertServer,\n buildGetInit,\n toApiError,\n} from \"./server-base\";\n\n/** 분류 1건 + 요청 스코프 안의 발행 글 수. */\nexport interface CmsCategoryCount {\n /** 안정 term id. 계층형 모델에서 post.terms의 id와 연결한다. */\n id?: string | null;\n slug: string;\n name: string;\n /** 부모 category term id. 루트면 null. */\n parentId?: string | null;\n termOrder?: number;\n /** 요청 스코프(미소속/특정 스트림) 안에서 이 분류에 걸린 발행 글 수. 0 가능. */\n publishedPostCount: number;\n /** 이 분류가 귀속된 콘텐츠 유형 key (ADR-0060). 미귀속·구버전 응답이면 null. */\n collectionKey: string | null;\n /**\n * 어드민 \"검색에 이 분류 페이지 노출\" 토글. **승격 판정 입력으로 쓰지 말 것** —\n * 렌더러의 3-state 승격은 `fetchBlogSettings().sitemap.promotedCategorySlugs`\n * 하나에서만 조립한다(두 소스를 섞으면 사이트맵과 페이지가 서로 다른 시점의\n * 값을 볼 수 있다). 진단·어드민 표시용 참고값이다.\n */\n hubPromoted: boolean;\n description: string | null;\n seoTitle: string | null;\n seoDescription: string | null;\n /** 공유 전용 설정. 본문 분류 이미지와 별도로 관리한다. */\n seoOgImage?: string | null;\n seoOgImageAlt?: string | null;\n seoNofollow?: boolean | null;\n imageMediaId?: string | null;\n imageUrl?: string | null;\n imageAlt?: string | null;\n path: string | null;\n}\n\nexport interface CmsCategoryCounts {\n categories: CmsCategoryCount[];\n /** Apply hubPromoted metadata policy only after explicit site activation. */\n categoryIndexActivated?: boolean;\n /** 분류 수가 서버 상한을 넘어 일부가 빠졌으면 true — 소비처는 경고를 남길 것. */\n truncated: boolean;\n}\n\nexport interface FetchCategoryCountsOptions {\n /** `rtlk_cust_*` API key. 블로그 조회와 같은 키. */\n apiKey: string;\n baseUrl?: string;\n siteId?: string;\n /**\n * 그 섹션(collection) 소속 글만 센다 — 스트림 아카이브용. `excludeCollections`\n * 와 동시 지정 불가(서버가 400).\n */\n collectionKey?: string;\n /** 어느 스트림에도 안 속한 글만 센다 — 레거시 `/blog` 계열 아카이브의 모집단. */\n excludeCollections?: boolean;\n /** BCP-47. 미지정이면 사이트 기본 로케일(포스트 목록과 같은 기본 동작). */\n locale?: string;\n signal?: AbortSignal;\n /** ISR revalidate 초(opt-in). 미지정 시 현행 동작 유지. */\n revalidate?: number;\n /** Additional Next.js data-cache tags (for example METADATA_CACHE_TAG). */\n tags?: string[];\n}\n\nfunction toNonNegativeInt(value: unknown): number {\n return typeof value === \"number\" && Number.isInteger(value) && value >= 0\n ? value\n : 0;\n}\n\n/**\n * 사이트의 모든 분류와 발행 글 수를 가져온다. 네트워크/서버 에러는 **throw** —\n * 호출자가 예전 파생 방식으로 fail-soft 할지, 화면을 죽일지 정한다.\n *\n * 이 엔드포인트가 없는 구버전 API 는 404 를 주고 그것도 `CmsApiError` 로 throw\n * 되므로, 호출자는 \"구버전 API\" 와 \"일시 장애\" 를 같은 catch 로 다루면 된다.\n * 응답 shape 이 예상과 다른 경우(`categories` 가 배열이 아님)도 throw 로 취급한다\n * — 빈 목록으로 오해하면 \"분류 0건\" 이라는 **확정 정보**가 되어 아카이브가 전부\n * 404 가 되기 때문이다.\n */\nexport async function fetchCategoryCounts(\n options: FetchCategoryCountsOptions,\n): Promise<CmsCategoryCounts> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams();\n if (options.siteId) params.set(\"site_id\", options.siteId);\n if (options.collectionKey) params.set(\"collection_key\", options.collectionKey);\n if (options.excludeCollections) params.set(\"exclude_collections\", \"true\");\n if (options.locale) params.set(\"locale\", options.locale);\n const qs = params.toString();\n const url = `${baseUrl}/v1/cms/public/categories${qs ? `?${qs}` : \"\"}`;\n\n const response = await fetch(url, buildGetInit(options.apiKey, options));\n if (!response.ok) {\n throw await toApiError(response);\n }\n\n const json = (await response.json()) as {\n categories?: Array<Record<string, unknown>>;\n truncated?: boolean;\n category_index_activated?: boolean;\n };\n if (!Array.isArray(json.categories)) {\n throw new CmsApiError(\n \"RootTale CMS API returned no `categories` array for /v1/cms/public/categories\",\n response.status,\n \"unexpected_response\",\n );\n }\n return {\n categories: json.categories.flatMap((o) => {\n const slug = typeof o.slug === \"string\" ? o.slug : \"\";\n const name = typeof o.name === \"string\" ? o.name : \"\";\n if (!slug || !name) return [];\n return [\n {\n id: typeof o.id === \"string\" ? o.id : null,\n slug,\n name,\n parentId: typeof o.parent_id === \"string\" ? o.parent_id : null,\n ...(typeof o.term_order === \"number\" && Number.isInteger(o.term_order) ? { termOrder: o.term_order } : {}),\n publishedPostCount: toNonNegativeInt(o.published_post_count),\n collectionKey:\n typeof o.collection_key === \"string\" ? o.collection_key : null,\n hubPromoted: o.hub_promoted === true,\n description: typeof o.description === \"string\" ? o.description : null,\n seoTitle: typeof o.seo_title === \"string\" ? o.seo_title : null,\n seoDescription:\n typeof o.seo_description === \"string\" ? o.seo_description : null,\n seoOgImage: typeof o.seo_og_image === \"string\" ? o.seo_og_image : null,\n seoOgImageAlt: typeof o.seo_og_image_alt === \"string\" ? o.seo_og_image_alt : null,\n seoNofollow: typeof o.seo_nofollow === \"boolean\" ? o.seo_nofollow : null,\n imageMediaId:\n typeof o.image_media_id === \"string\" ? o.image_media_id : null,\n imageUrl: typeof o.image_url === \"string\" ? o.image_url : null,\n imageAlt: typeof o.image_alt === \"string\" ? o.image_alt : null,\n path: typeof o.path === \"string\" ? o.path : null,\n },\n ];\n }),\n truncated: json.truncated === true,\n ...(typeof json.category_index_activated === \"boolean\" ? { categoryIndexActivated: json.category_index_activated } : {}),\n };\n}\n","import { DEFAULT_BASE_URL, assertServer, buildGetInit, toApiError } from \"./server-base\";\n\n// ── 비즈니스 프로필 (로컬 SEO) ───────────────────────────────────────────────\n\n/** Schema.org LocalBusiness 하위 타입 (admin \"운영 > 비즈니스 프로필\" 의 업종). */\nexport type BusinessType =\n | \"LocalBusiness\"\n | \"ProfessionalService\"\n | \"AccountingService\"\n | \"LegalService\"\n | \"MedicalClinic\"\n | \"Dentist\"\n | \"RealEstateAgent\"\n | \"Restaurant\"\n | \"BeautySalon\";\n\n/** Schema.org openingHours 요일 약어. */\nexport type BusinessDayCode = \"Mo\" | \"Tu\" | \"We\" | \"Th\" | \"Fr\" | \"Sa\" | \"Su\";\n\nexport interface BusinessOpeningHours {\n days: BusinessDayCode[];\n /** HH:MM (24시간). */\n opens: string;\n /** HH:MM (24시간). */\n closes: string;\n}\n\n/**\n * 비즈니스 프로필 — admin \"운영 > 비즈니스 프로필\" 에서 저장한 사업장 정보.\n * `localBusinessSchema(profile, { url })` 로 LocalBusiness JSON-LD 를 만들어\n * layout 에 1회 렌더한다 (로컬 SEO).\n */\nexport interface RootTaleBusinessProfile {\n /** 사업장 이름 (간판명). */\n name: string;\n /** 사업자등록 상호 — 간판명과 다를 때만 설정. */\n legalName: string | null;\n /** 간판명의 표기 변형(띄어쓰기 등) — JSON-LD alternateName. */\n alternateName: string | null;\n businessType: BusinessType;\n telephone: string | null;\n /** 팩스번호 — JSON-LD faxNumber. */\n faxNumber: string | null;\n email: string | null;\n address: {\n streetAddress: string;\n addressLocality: string;\n addressRegion: string | null;\n postalCode: string | null;\n } | null;\n geo: { latitude: number; longitude: number } | null;\n openingHours: BusinessOpeningHours[];\n /** 가격대 표기 (예: \"₩₩\"). */\n priceRange: string | null;\n /** 서비스 지역 (예: \"서울 강남구\"). */\n areaServed: string[];\n /** 취급 업무 (예: \"기장·신고대리\") — JSON-LD hasOfferCatalog. */\n services: string[];\n /** 외부 프로필 URL — JSON-LD sameAs 로 들어가 동일 사업장임을 연결. */\n profiles: {\n naverPlace: string | null;\n googleBusiness: string | null;\n kakaoChannel: string | null;\n /** 지도 앱 딥링크 — naverPlace 가 없을 때 hasMap 대체값. */\n kakaoMap: string | null;\n instagram: string | null;\n naverBlog: string | null;\n };\n}\n\n/**\n * 사업장 정보 fetch 캐시 tag 의 정본 이름 (ADR-0096 Phase C). 사이트의\n * `fetchBusinessProfile` 호출에 `tags: [BUSINESS_CACHE_TAG]` 로 부착하고,\n * revalidate route 가 `theme.updated` 수신 시 `revalidateTag(BUSINESS_CACHE_TAG)`\n * 로 무효화한다. 양쪽이 이 상수를 공유해 drift 를 방지한다.\n *\n * tag 를 붙이지 않으면 `revalidatePath` 로는 이 fetch 의 Data Cache 엔트리를\n * 지울 수 없어 `revalidate` 초만큼 낡은 주소·전화가 남는다.\n */\nexport const BUSINESS_CACHE_TAG = \"rt-business\";\n\nexport interface FetchBusinessProfileOptions {\n apiKey: string;\n baseUrl?: string;\n siteId?: string;\n signal?: AbortSignal;\n /** ISR revalidate 초(opt-in, W0-9). 미지정 시 현행 동작 유지. */\n revalidate?: number;\n /**\n * on-demand 무효화용 Next fetch cache tags(opt-in, ADR-0096 Phase C). 보통\n * `[BUSINESS_CACHE_TAG]`. 미지정 시 현행 동작 유지.\n */\n tags?: string[];\n}\n\ntype BusinessProfileWire = {\n configured?: boolean;\n name?: string | null;\n legal_name?: string | null;\n alternate_name?: string | null;\n business_type?: string | null;\n telephone?: string | null;\n fax_number?: string | null;\n email?: string | null;\n address?: {\n street_address?: string;\n address_locality?: string;\n address_region?: string | null;\n postal_code?: string | null;\n } | null;\n geo?: { latitude?: number; longitude?: number } | null;\n opening_hours?: {\n days?: BusinessDayCode[];\n opens?: string;\n closes?: string;\n }[];\n price_range?: string | null;\n area_served?: string[];\n services?: string[];\n profiles?: {\n naver_place?: string | null;\n google_business?: string | null;\n kakao_channel?: string | null;\n kakao_map?: string | null;\n instagram?: string | null;\n naver_blog?: string | null;\n } | null;\n};\n\n/** wire(snake_case) → RootTaleBusinessProfile. 미설정이면 null (export for tests). */\nexport function businessProfileFromWire(\n json: BusinessProfileWire,\n): RootTaleBusinessProfile | null {\n // configured=false 또는 이름 없음 = 미설정 — 호출자는 JSON-LD 렌더를 건너뛴다.\n if (json.configured === false) return null;\n if (typeof json.name !== \"string\" || json.name.length === 0) return null;\n const address =\n json.address &&\n typeof json.address.street_address === \"string\" &&\n typeof json.address.address_locality === \"string\"\n ? {\n streetAddress: json.address.street_address,\n addressLocality: json.address.address_locality,\n addressRegion: json.address.address_region ?? null,\n postalCode: json.address.postal_code ?? null,\n }\n : null;\n const geo =\n json.geo &&\n typeof json.geo.latitude === \"number\" &&\n typeof json.geo.longitude === \"number\"\n ? { latitude: json.geo.latitude, longitude: json.geo.longitude }\n : null;\n const openingHours: BusinessOpeningHours[] = [];\n for (const row of json.opening_hours ?? []) {\n if (\n Array.isArray(row.days) &&\n row.days.length > 0 &&\n typeof row.opens === \"string\" &&\n typeof row.closes === \"string\"\n ) {\n openingHours.push({ days: row.days, opens: row.opens, closes: row.closes });\n }\n }\n return {\n name: json.name,\n legalName: json.legal_name ?? null,\n alternateName: json.alternate_name ?? null,\n businessType:\n typeof json.business_type === \"string\" && json.business_type.length > 0\n ? (json.business_type as BusinessType)\n : \"LocalBusiness\",\n telephone: json.telephone ?? null,\n faxNumber: json.fax_number ?? null,\n email: json.email ?? null,\n address,\n geo,\n openingHours,\n priceRange: json.price_range ?? null,\n areaServed: json.area_served ?? [],\n services: json.services ?? [],\n profiles: {\n naverPlace: json.profiles?.naver_place ?? null,\n googleBusiness: json.profiles?.google_business ?? null,\n kakaoChannel: json.profiles?.kakao_channel ?? null,\n kakaoMap: json.profiles?.kakao_map ?? null,\n instagram: json.profiles?.instagram ?? null,\n naverBlog: json.profiles?.naver_blog ?? null,\n },\n };\n}\n\n/**\n * tenant 의 비즈니스 프로필을 가져온다. 미설정이면 null — 호출자는 null 일 때\n * JSON-LD 렌더를 건너뛰면 된다. 네트워크/서버 에러는 throw — 호출자가\n * `.catch(() => null)` 등으로 fallback.\n */\nexport async function fetchBusinessProfile(\n options: FetchBusinessProfileOptions,\n): Promise<RootTaleBusinessProfile | null> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams();\n if (options.siteId) params.set(\"site_id\", options.siteId);\n const qs = params.toString();\n const url = `${baseUrl}/v1/cms/public/business-profile${qs ? `?${qs}` : \"\"}`;\n\n const response = await fetch(url, buildGetInit(options.apiKey, options));\n\n // 구 서버(라우트 미배포)는 404 — 미설정과 동일하게 null.\n if (response.status === 404) return null;\n if (!response.ok) {\n throw await toApiError(response);\n }\n\n const json = (await response.json()) as BusinessProfileWire;\n return businessProfileFromWire(json);\n}\n\n","import {\n type CmsPostType,\n DEFAULT_BASE_URL,\n assertServer,\n buildGetInit,\n toApiError,\n} from \"./server-base\";\nimport type { CmsRouteCollection } from \"./server-blog\";\n\n// ── 공개 검색 (WP `?s=` 패리티) ────────────────────────────────────────────\n\nexport interface SearchPostsOptions {\n apiKey: string;\n baseUrl?: string;\n /** 검색 키워드 (title·excerpt·본문 텍스트 매칭). */\n query: string;\n /** 결과 수 1-50, default 10. */\n limit?: number;\n /** 기본 = `post` 만. page 포함 검색은 명시. */\n type?: CmsPostType | \"all\";\n /** BCP-47 locale. 미지정 시 사이트 기본 언어. */\n locale?: string;\n siteId?: string;\n signal?: AbortSignal;\n /** ISR revalidate 초(opt-in, W0-9). 미지정 시 현행 동작 유지. */\n revalidate?: number;\n}\n\n/** 검색 결과 카드용 슬림 hit — body 미포함. */\nexport interface CmsSearchHit {\n id: string;\n type: CmsPostType;\n collectionKey: string | null;\n modelKey: string | null;\n locale: string;\n title: string;\n slug: string;\n excerpt: string | null;\n featuredImageUrl: string | null;\n /** ISO 8601. */\n publishedAt: string;\n}\n\n/**\n * published 글 키워드 검색 (사이트 내 검색). 관련도순, body 미포함 슬림\n * hit — 상세는 slug 로 `fetchPost`. 구 서버(라우트 미배포)는 404 — 빈 배열.\n */\nexport async function searchPosts(\n options: SearchPostsOptions,\n): Promise<CmsSearchHit[]> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams();\n params.set(\"q\", options.query);\n if (options.limit !== undefined) params.set(\"limit\", String(options.limit));\n if (options.type) params.set(\"type\", options.type);\n if (options.locale) params.set(\"locale\", options.locale);\n if (options.siteId) params.set(\"site_id\", options.siteId);\n const url = `${baseUrl}/v1/cms/public/search?${params.toString()}`;\n\n const response = await fetch(url, buildGetInit(options.apiKey, options));\n\n if (response.status === 404) return [];\n if (!response.ok) {\n throw await toApiError(response);\n }\n\n const json = (await response.json()) as {\n items: {\n id: string;\n type: CmsPostType;\n collection_key: string | null;\n model_key?: string | null;\n locale: string;\n title: string;\n slug: string;\n excerpt: string | null;\n featured_media_url: string | null;\n published_at: string;\n }[];\n };\n return json.items.map((w) => ({\n id: w.id,\n type: w.type,\n collectionKey: w.collection_key ?? null,\n modelKey: w.model_key ?? null,\n locale: w.locale,\n title: w.title,\n slug: w.slug,\n excerpt: w.excerpt,\n featuredImageUrl: w.featured_media_url ?? null,\n publishedAt: w.published_at,\n }));\n}\n\ntype RoutableSearchHit = Pick<\n CmsSearchHit,\n \"type\" | \"slug\" | \"collectionKey\"\n>;\n\n/** 검색 hit을 사이트의 실제 공개 상세 주소로 바꾼다. 주소 없는 유형은 null. */\nexport function resolveSearchHitPath(\n hit: RoutableSearchHit,\n collections: readonly Pick<\n CmsRouteCollection,\n \"key\" | \"basePath\" | \"detail\"\n >[],\n locale?: string,\n): string | null {\n let path: string | null;\n if (hit.type === \"page\") {\n path = `/${hit.slug}`;\n } else {\n const collection = hit.collectionKey\n ? collections.find((candidate) => candidate.key === hit.collectionKey)\n : undefined;\n if (!collection) {\n path = `/blog/${hit.slug}`;\n } else if (!collection.detail || collection.basePath.length === 0) {\n path = null;\n } else {\n const basePath = `/${collection.basePath}`.replace(/\\/{2,}/g, \"/\").replace(/\\/$/, \"\");\n path = `${basePath}/${hit.slug}`;\n }\n }\n\n if (!path || !locale) return path;\n const localePrefix = `/${locale.replace(/^\\/+|\\/+$/g, \"\")}`;\n return `${localePrefix}${path}`;\n}\n\n// ── 네비게이션 메뉴 (WP 외모>메뉴 패리티) ──────────────────────────────────\n\n/** 메뉴 항목 트리 노드 (깊이 ≤ 2). */\nexport interface RootTaleMenuItem {\n id: string;\n label: string;\n /** 상대 경로(`/about`) 또는 절대 http(s) URL — 서버가 sanitize. */\n url: string;\n /** true 면 `target=\"_blank\" rel=\"noopener\"` 로 렌더할 것. */\n newTab?: boolean;\n children?: RootTaleMenuItem[];\n}\n\nexport interface RootTaleMenu {\n id: string;\n /** 사람용 이름 — \"헤더 메뉴\". */\n name: string;\n /** 위치 핸들 — \"primary\" | \"footer\" 등. */\n slug: string;\n items: RootTaleMenuItem[];\n updatedAt: string | null;\n}\n\n/**\n * 메뉴(네비게이션) fetch 캐시 tag 의 정본 이름 (ADR-0096 Phase C).\n * `fetchMenu`·`fetchMenus` 호출에 `tags: [MENUS_CACHE_TAG]` 로 부착하고,\n * revalidate route 가 `theme.updated` 수신 시 `revalidateTag(MENUS_CACHE_TAG)`\n * 로 무효화한다. 양쪽이 이 상수를 공유해 drift 를 방지한다.\n *\n * 메뉴 1개(`fetchMenu`)와 전체(`fetchMenus`)가 같은 tag 를 쓴다 — 어드민에서\n * 메뉴를 저장하면 어느 쪽 캐시든 같이 낡기 때문이다.\n */\nexport const MENUS_CACHE_TAG = \"rt-menus\";\n\nexport interface FetchMenuOptions {\n apiKey: string;\n baseUrl?: string;\n /** 위치 핸들 — \"primary\" | \"footer\" 등. */\n slug: string;\n siteId?: string;\n signal?: AbortSignal;\n /** ISR revalidate 초(opt-in, W0-9). 미지정 시 현행 동작 유지. */\n revalidate?: number;\n /**\n * on-demand 무효화용 Next fetch cache tags(opt-in, ADR-0096 Phase C). 보통\n * `[MENUS_CACHE_TAG]`. 미지정 시 현행 동작 유지.\n */\n tags?: string[];\n}\n\nexport interface FetchMenusOptions {\n apiKey: string;\n baseUrl?: string;\n siteId?: string;\n signal?: AbortSignal;\n /** ISR revalidate 초(opt-in, W0-9). 미지정 시 현행 동작 유지. */\n revalidate?: number;\n /**\n * on-demand 무효화용 Next fetch cache tags(opt-in, ADR-0096 Phase C). 보통\n * `[MENUS_CACHE_TAG]`. 미지정 시 현행 동작 유지.\n */\n tags?: string[];\n}\n\ntype CmsMenuItemWire = {\n id: string;\n label: string;\n url: string;\n new_tab?: boolean;\n children?: CmsMenuItemWire[];\n};\n\ntype CmsMenuWire = {\n id: string;\n name: string;\n slug: string;\n items: CmsMenuItemWire[];\n updated_at: string | null;\n};\n\nfunction menuItemFromWire(w: CmsMenuItemWire): RootTaleMenuItem {\n const item: RootTaleMenuItem = { id: w.id, label: w.label, url: w.url };\n if (w.new_tab) item.newTab = true;\n if (w.children && w.children.length > 0) {\n item.children = w.children.map(menuItemFromWire);\n }\n return item;\n}\n\nfunction menuFromWire(w: CmsMenuWire): RootTaleMenu {\n return {\n id: w.id,\n name: w.name,\n slug: w.slug,\n items: (w.items ?? []).map(menuItemFromWire),\n updatedAt: w.updated_at ?? null,\n };\n}\n\n/**\n * 위치 핸들 slug 로 네비게이션 메뉴 1개를 가져온다. admin \"디자인 > 메뉴\" 의\n * 저장값. 미설정/미존재(404)는 null — 사이트는 자체 fallback 네비를 렌더한다.\n * 구 서버(라우트 미배포)도 404 → null 로 동일하게 fail-soft.\n */\nexport async function fetchMenu(\n options: FetchMenuOptions,\n): Promise<RootTaleMenu | null> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams();\n if (options.siteId) params.set(\"site_id\", options.siteId);\n const qs = params.toString();\n const url = `${baseUrl}/v1/cms/public/menus/${encodeURIComponent(options.slug)}${qs ? `?${qs}` : \"\"}`;\n\n const response = await fetch(url, buildGetInit(options.apiKey, options));\n\n if (response.status === 404) return null;\n if (!response.ok) {\n throw await toApiError(response);\n }\n\n const json = (await response.json()) as CmsMenuWire;\n return menuFromWire(json);\n}\n\n/** 사이트의 네비게이션 메뉴 전체. 구 서버(라우트 미배포)는 404 — 빈 배열. */\nexport async function fetchMenus(\n options: FetchMenusOptions,\n): Promise<RootTaleMenu[]> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams();\n if (options.siteId) params.set(\"site_id\", options.siteId);\n const qs = params.toString();\n const url = `${baseUrl}/v1/cms/public/menus${qs ? `?${qs}` : \"\"}`;\n\n const response = await fetch(url, buildGetInit(options.apiKey, options));\n\n if (response.status === 404) return [];\n if (!response.ok) {\n throw await toApiError(response);\n }\n\n const json = (await response.json()) as { items: CmsMenuWire[] };\n return (json.items ?? []).map(menuFromWire);\n}\n\n// ── 커스텀 리다이렉트 (WP RankMath Redirections 패리티) ─────────────────────\n\nexport interface RootTaleRedirect {\n id: string;\n /** 정규화된 출발 경로 (앞 슬래시, 쿼리 제외). 예: `/old-event`. */\n fromPath: string;\n /** 도착지 — 사이트 내부 경로(`/promo`) 또는 절대 URL. */\n toTarget: string;\n /** 301 영구 / 302 임시. */\n statusCode: 301 | 302;\n}\n\nexport interface FetchRedirectsOptions {\n apiKey: string;\n baseUrl?: string;\n siteId?: string;\n signal?: AbortSignal;\n /** ISR revalidate 초(opt-in, W0-9). 미지정 시 현행 동작 유지. */\n revalidate?: number;\n}\n\ntype CmsRedirectWire = {\n id: string;\n from_path: string;\n to_target: string;\n status_code: number;\n};\n\n/**\n * 사이트의 활성 커스텀 리다이렉트 규칙 전체. 고객 사이트 middleware\n * (`createRedirectMiddleware`)가 요청 경로 매칭에 사용한다. 구 서버(라우트\n * 미배포)는 404 — 빈 배열로 fail-soft (트래픽을 막지 않음).\n */\nexport async function fetchRedirects(\n options: FetchRedirectsOptions,\n): Promise<RootTaleRedirect[]> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams();\n if (options.siteId) params.set(\"site_id\", options.siteId);\n const qs = params.toString();\n const url = `${baseUrl}/v1/cms/public/redirects${qs ? `?${qs}` : \"\"}`;\n\n const response = await fetch(url, buildGetInit(options.apiKey, options));\n\n if (response.status === 404) return [];\n if (!response.ok) {\n throw await toApiError(response);\n }\n\n const json = (await response.json()) as { items: CmsRedirectWire[] };\n return (json.items ?? []).map((w) => ({\n id: w.id,\n fromPath: w.from_path,\n toTarget: w.to_target,\n statusCode: (w.status_code === 302 ? 302 : 301) as 301 | 302,\n }));\n}\n","import { DEFAULT_BASE_URL, assertServer, buildGetInit, toApiError } from \"./server-base\";\n\nexport const EXPOSURE_CACHE_TAG = \"rt-exposures\";\n\nexport interface CmsExposureCampaign {\n id: string;\n slotKey: string;\n kind: \"banner\" | \"popup\";\n content: {\n variant: string;\n eyebrow?: string;\n title: string;\n description?: string;\n assetUrl?: string;\n assetAlt?: string;\n linkLabel?: string;\n linkUrl?: string;\n mobileAssetUrl?: string;\n mobileAssetAlt?: string;\n linkTarget?: \"self\" | \"blank\";\n };\n repeat: \"always\" | \"session\" | \"day\" | \"never\";\n endsAt?: string | null;\n delivery?: {\n version: 2;\n timeZone: string;\n delaySeconds: number;\n scrollPercent: 25 | 50 | 75 | null;\n dismissible: boolean;\n hideForToday: boolean;\n size: \"small\" | \"medium\" | \"large\";\n dayBoundary: \"calendar\" | \"rolling\";\n };\n}\n\nexport interface FetchExposureOptions {\n apiKey: string;\n baseUrl?: string;\n siteId?: string;\n slotKey: string;\n path: string;\n signal?: AbortSignal;\n revalidate?: number;\n tags?: string[];\n}\n\n/** 개발자가 선언한 슬롯과 현재 경로에 맞는 활성 캠페인 하나만 가져온다. */\nexport async function fetchExposure(\n options: FetchExposureOptions,\n): Promise<CmsExposureCampaign | null> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams({ slot_key: options.slotKey, path: options.path });\n if (options.siteId) params.set(\"site_id\", options.siteId);\n const response = await fetch(\n `${baseUrl}/v1/cms/public/exposures?${params.toString()}`,\n buildGetInit(options.apiKey, {\n ...options,\n // 레거시 RSC도 발행 웹훅을 실제로 받으며 긴 ISR에 묶이지 않는다.\n revalidate: Math.min(options.revalidate ?? 60, 60),\n tags: [...new Set([EXPOSURE_CACHE_TAG, ...(options.tags ?? [])])],\n }),\n );\n if (!response.ok) throw await toApiError(response);\n const json = (await response.json()) as {\n campaign?: {\n id: string;\n slot_key: string;\n kind: \"banner\" | \"popup\";\n content: {\n variant: string;\n eyebrow?: string;\n title: string;\n description?: string;\n asset_url?: string;\n asset_alt?: string;\n link_label?: string;\n link_url?: string;\n };\n repeat: CmsExposureCampaign[\"repeat\"];\n } | null;\n };\n const campaign = json.campaign;\n if (!campaign) return null;\n return {\n id: campaign.id,\n slotKey: campaign.slot_key,\n kind: campaign.kind,\n content: {\n variant: campaign.content.variant,\n ...(campaign.content.eyebrow ? { eyebrow: campaign.content.eyebrow } : {}),\n title: campaign.content.title,\n ...(campaign.content.description ? { description: campaign.content.description } : {}),\n ...(campaign.content.asset_url ? { assetUrl: campaign.content.asset_url } : {}),\n ...(campaign.content.asset_alt ? { assetAlt: campaign.content.asset_alt } : {}),\n ...(campaign.content.link_label ? { linkLabel: campaign.content.link_label } : {}),\n ...(campaign.content.link_url ? { linkUrl: campaign.content.link_url } : {}),\n },\n repeat: campaign.repeat,\n };\n}\n","import { DEFAULT_BASE_URL, assertServer, authHeaders, requestSignal, toApiError } from \"./server-base\";\nimport type { CmsExposureCampaign } from \"./server-exposure\";\n\nexport interface CmsExposureDecision {\n campaigns: CmsExposureCampaign[];\n evaluatedAt: string;\n nextChangeAt: string | null;\n validUntil: string;\n contractVersion: number | null;\n supportedFeatures: string[];\n}\n\nexport interface FetchExposureDecisionOptions {\n apiKey: string;\n baseUrl?: string;\n siteId?: string;\n slotKeys: readonly string[];\n path: string;\n device: \"desktop\" | \"mobile\";\n signal?: AbortSignal;\n}\n\nfunction object(value: unknown): Record<string, unknown> {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) throw new Error(\"Invalid exposure response\");\n return value as Record<string, unknown>;\n}\n\nfunction requiredString(value: unknown): string {\n if (typeof value !== \"string\" || value.length === 0) throw new Error(\"Invalid exposure response\");\n return value;\n}\n\nfunction timestamp(value: unknown): string {\n const result = requiredString(value);\n if (!Number.isFinite(Date.parse(result))) throw new Error(\"Invalid exposure timestamp\");\n return result;\n}\n\nfunction safeUrl(value: unknown, internal = true): string | undefined {\n if (value === undefined || value === null) return undefined;\n const text = requiredString(value);\n if (/[\\u0000-\\u0020\\u007f\\\\]/.test(text)) throw new Error(\"Invalid exposure URL\");\n if (internal && text.startsWith(\"/\") && !text.startsWith(\"//\")) return text;\n const url = new URL(text);\n if (url.protocol !== \"https:\" || url.username || url.password) throw new Error(\"Invalid exposure URL\");\n return text;\n}\n\nfunction campaignFromWire(value: unknown): CmsExposureCampaign {\n const row = object(value);\n const content = object(row.content);\n if (row.kind !== \"banner\" && row.kind !== \"popup\") throw new Error(\"Invalid exposure kind\");\n if (![\"always\", \"session\", \"day\", \"never\"].includes(String(row.repeat))) throw new Error(\"Invalid exposure repeat\");\n const result: CmsExposureCampaign = {\n id: requiredString(row.id), slotKey: requiredString(row.slot_key), kind: row.kind,\n repeat: row.repeat as CmsExposureCampaign[\"repeat\"],\n endsAt: row.ends_at == null ? null : timestamp(row.ends_at),\n content: { variant: requiredString(content.variant), title: requiredString(content.title),\n ...(typeof content.eyebrow === \"string\" ? { eyebrow: content.eyebrow } : {}),\n ...(typeof content.description === \"string\" ? { description: content.description } : {}),\n ...(typeof content.asset_alt === \"string\" ? { assetAlt: content.asset_alt } : {}),\n ...(typeof content.mobile_asset_alt === \"string\" ? { mobileAssetAlt: content.mobile_asset_alt } : {}),\n ...(typeof content.link_label === \"string\" ? { linkLabel: content.link_label } : {}),\n assetUrl: safeUrl(content.asset_url, false), mobileAssetUrl: safeUrl(content.mobile_asset_url, false),\n linkUrl: safeUrl(content.link_url), linkTarget: content.link_target === \"blank\" ? \"blank\" : \"self\",\n },\n };\n if (row.delivery != null) {\n const delivery = object(row.delivery);\n if (delivery.version !== 2 || typeof delivery.delay_seconds !== \"number\" ||\n delivery.delay_seconds < 0 || delivery.delay_seconds > 30 ||\n ![null, 25, 50, 75].includes(delivery.scroll_percent as number | null) ||\n typeof delivery.dismissible !== \"boolean\" || typeof delivery.hide_for_today !== \"boolean\" ||\n ![\"small\", \"medium\", \"large\"].includes(String(delivery.size)) ||\n ![\"calendar\", \"rolling\"].includes(String(delivery.day_boundary))) throw new Error(\"Invalid exposure delivery\");\n const timeZone = requiredString(delivery.time_zone);\n new Intl.DateTimeFormat(\"en\", { timeZone });\n result.delivery = { version: 2, timeZone, delaySeconds: delivery.delay_seconds,\n scrollPercent: delivery.scroll_percent as 25 | 50 | 75 | null,\n dismissible: delivery.dismissible, hideForToday: delivery.hide_for_today,\n size: delivery.size as \"small\" | \"medium\" | \"large\", dayBoundary: delivery.day_boundary as \"calendar\" | \"rolling\" };\n }\n return result;\n}\n\n/** 현재 시각·경로·기기의 최종 선택은 ISR/SWR 캐시 없이 매번 판정한다. */\nexport async function fetchExposureDecision(options: FetchExposureDecisionOptions): Promise<CmsExposureDecision> {\n assertServer();\n const params = new URLSearchParams({ slot_keys: options.slotKeys.join(\",\"), path: options.path, device: options.device });\n if (options.siteId) params.set(\"site_id\", options.siteId);\n const base = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const response = await fetch(`${base}/v1/cms/public/exposure-decisions?${params}`, {\n headers: authHeaders(options.apiKey), cache: \"no-store\", signal: requestSignal(options.signal),\n });\n if (!response.ok) throw await toApiError(response);\n const payload = object(await response.json());\n if (!Array.isArray(payload.campaigns) || payload.campaigns.length > 200) throw new Error(\"Invalid exposure campaigns\");\n const evaluatedAt = timestamp(payload.evaluated_at);\n const validUntil = timestamp(payload.valid_until);\n if (Date.parse(validUntil) <= Date.parse(evaluatedAt)) throw new Error(\"Expired exposure response\");\n return { campaigns: payload.campaigns.map(campaignFromWire), evaluatedAt, validUntil,\n nextChangeAt: payload.next_change_at == null ? null : timestamp(payload.next_change_at),\n contractVersion: typeof payload.contract_version === \"number\" ? payload.contract_version : null,\n supportedFeatures: Array.isArray(payload.supported_features) ? payload.supported_features.filter((item): item is string => typeof item === \"string\") : [],\n };\n}\n","import {\n DEFAULT_BASE_URL,\n assertServer,\n authHeaders,\n buildGetInit,\n toApiError,\n} from \"./server-base\";\n\nexport type SiteInquiryStatus = \"pending\" | \"answered\";\n\nexport interface SiteInquiryListItem {\n id: string;\n title: string;\n category: string | null;\n authorMasked: string | null;\n status: SiteInquiryStatus | null;\n isSecret: boolean;\n locked: boolean;\n createdAt: string | null;\n}\n\nexport interface SiteInquiryListPage {\n items: SiteInquiryListItem[];\n hasMore: boolean;\n nextCursor: string | null;\n}\n\nexport interface SiteInquiryDetail {\n id: string;\n title: string;\n category: string | null;\n authorMasked: string;\n status: SiteInquiryStatus;\n isSecret: boolean;\n body: string;\n answerBody: string | null;\n answeredAt: string | null;\n createdAt: string;\n}\n\nexport interface FetchSiteInquiriesOptions {\n apiKey: string;\n baseUrl?: string;\n siteId?: string;\n limit?: number;\n cursor?: string;\n signal?: AbortSignal;\n revalidate?: number;\n}\n\nexport interface FetchSiteInquiryOptions {\n apiKey: string;\n baseUrl?: string;\n siteId?: string;\n id: string;\n password?: string;\n signal?: AbortSignal;\n revalidate?: number;\n}\n\nexport interface SubmitSiteInquiryFields {\n siteId?: string;\n category?: string;\n title: string;\n body: string;\n authorName: string;\n contact?: string;\n isSecret: boolean;\n secretPassword?: string;\n privacyConsent: boolean;\n}\n\nexport interface SubmitSiteInquiryOptions {\n apiKey: string;\n baseUrl?: string;\n fields: SubmitSiteInquiryFields;\n signal?: AbortSignal;\n}\n\nexport interface SubmitSiteInquiryResult {\n id: string;\n status: SiteInquiryStatus;\n}\n\ntype SiteInquiryListItemWire = {\n id: string;\n title: string;\n category: string | null;\n author_masked: string | null;\n status: SiteInquiryStatus | null;\n is_secret: boolean;\n locked: boolean;\n created_at: string | null;\n};\n\ntype SiteInquiryDetailWire = {\n id: string;\n title: string;\n category: string | null;\n author_masked: string;\n status: SiteInquiryStatus;\n is_secret: boolean;\n body: string;\n answer_body: string | null;\n answered_at: string | null;\n created_at: string;\n};\n\nexport async function fetchSiteInquiries(\n options: FetchSiteInquiriesOptions,\n): Promise<SiteInquiryListPage> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams();\n if (options.limit !== undefined) params.set(\"limit\", String(options.limit));\n if (options.cursor) params.set(\"cursor\", options.cursor);\n if (options.siteId) params.set(\"site_id\", options.siteId);\n const qs = params.toString();\n const url = `${baseUrl}/v1/cms/public/inquiries${qs ? `?${qs}` : \"\"}`;\n const response = await fetch(url, buildGetInit(options.apiKey, options));\n if (!response.ok) throw await toApiError(response);\n const json = (await response.json()) as {\n items: SiteInquiryListItemWire[];\n has_more: boolean;\n next_cursor: string | null;\n };\n return {\n items: json.items.map(fromListWire),\n hasMore: json.has_more,\n nextCursor: json.next_cursor,\n };\n}\n\nexport async function fetchSiteInquiry(\n options: FetchSiteInquiryOptions,\n): Promise<SiteInquiryDetail | null> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams();\n if (options.siteId) params.set(\"site_id\", options.siteId);\n if (options.password) params.set(\"password\", options.password);\n const qs = params.toString();\n const url = `${baseUrl}/v1/cms/public/inquiries/${encodeURIComponent(options.id)}${qs ? `?${qs}` : \"\"}`;\n const response = await fetch(url, buildGetInit(options.apiKey, options));\n if (response.status === 404) return null;\n if (!response.ok) throw await toApiError(response);\n return fromDetailWire((await response.json()) as SiteInquiryDetailWire);\n}\n\nexport async function submitSiteInquiry(\n options: SubmitSiteInquiryOptions,\n): Promise<SubmitSiteInquiryResult> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const fields = options.fields;\n const response = await fetch(`${baseUrl}/v1/cms/public/inquiries`, {\n method: \"POST\",\n headers: {\n ...authHeaders(options.apiKey),\n \"content-type\": \"application/json\",\n },\n body: JSON.stringify({\n site_id: fields.siteId,\n category: fields.category,\n title: fields.title,\n body: fields.body,\n author_name: fields.authorName,\n contact: fields.contact,\n is_secret: fields.isSecret,\n secret_password: fields.secretPassword,\n privacy_consent: fields.privacyConsent,\n }),\n signal: options.signal,\n });\n if (!response.ok) throw await toApiError(response);\n return (await response.json()) as SubmitSiteInquiryResult;\n}\n\nfunction fromListWire(item: SiteInquiryListItemWire): SiteInquiryListItem {\n return {\n id: item.id,\n title: item.title,\n category: item.category,\n authorMasked: item.author_masked,\n status: item.status,\n isSecret: item.is_secret,\n locked: item.locked,\n createdAt: item.created_at,\n };\n}\n\nfunction fromDetailWire(item: SiteInquiryDetailWire): SiteInquiryDetail {\n return {\n id: item.id,\n title: item.title,\n category: item.category,\n authorMasked: item.author_masked,\n status: item.status,\n isSecret: item.is_secret,\n body: item.body,\n answerBody: item.answer_body,\n answeredAt: item.answered_at,\n createdAt: item.created_at,\n };\n}\n","import { inquiryFormDefinitionSchema, type InquiryAnswer, type InquiryFormDefinition } from \"@roottale/inquiry-forms\";\n\nimport type { InquiryAttribution } from \"./attribution\";\nimport { CmsApiError, DEFAULT_BASE_URL, assertServer, authHeaders, requestSignal, toApiError } from \"./server-base\";\n\nexport type { InquiryKind, InquiryFormDefinition, InquiryFormField, InquiryAnswer } from \"@roottale/inquiry-forms\";\n\n/** A published form belongs to exactly one site. Versions are immutable. */\nexport interface PublishedInquiryForm {\n formId: string;\n siteId: string;\n formKey: string;\n version: number;\n definition: InquiryFormDefinition;\n}\n\nexport interface FetchInquiryFormOptions {\n apiKey: string;\n baseUrl?: string;\n /** Required for a tenant-wide key; a site-bound key already identifies its site. */\n siteId?: string;\n formId: string;\n signal?: AbortSignal;\n}\n\nexport interface SubmitFormInquiryFields {\n siteId?: string;\n /** Version that the visitor saw, including its consent notice. */\n version: number;\n answers: Record<string, InquiryAnswer>;\n privacyConsent: boolean;\n /** Caller-generated key. Reuse the same key and payload after an uncertain response. */\n idempotencyKey: string;\n placement?: string;\n turnstileToken?: string;\n attribution?: InquiryAttribution | null;\n /** readJourney() array; the API validates entries and enforces the site's privacy mode. */\n journey?: readonly unknown[];\n}\n\nexport interface SubmitFormInquiryOptions {\n apiKey: string;\n baseUrl?: string;\n formId: string;\n fields: SubmitFormInquiryFields;\n signal?: AbortSignal;\n}\n\nexport interface FormInquiryReceipt {\n id: string;\n inquiryNo: number;\n receivedAt: string;\n /** True when this is the receipt of an already saved submission. */\n replayed: boolean;\n}\n\nexport type SubmitFormInquiryResult =\n | { ok: true; receipt: FormInquiryReceipt }\n | {\n ok: false;\n /** Transport/invalid_response can mean the server saved the inquiry before the response was lost. */\n kind: \"api\" | \"transport\" | \"invalid_response\";\n code: string;\n status: number | null;\n message: string;\n fieldErrors: Record<string, string[]>;\n formErrors: string[];\n };\n\n/** Server-only. Always reads the current published form; 404 returns null. */\nexport async function fetchInquiryForm(options: FetchInquiryFormOptions): Promise<PublishedInquiryForm | null> {\n assertServer();\n const params = new URLSearchParams();\n if (options.siteId) params.set(\"site_id\", options.siteId);\n const query = params.toString();\n const response = await fetch(`${formUrl(options)}${query ? `?${query}` : \"\"}`, {\n headers: authHeaders(options.apiKey),\n cache: \"no-store\",\n redirect: \"manual\",\n signal: requestSignal(options.signal),\n });\n if (response.status === 404) return null;\n if (!response.ok) throw await toApiError(response);\n\n const body = await readJson(response);\n if (!isRecord(body) || body.form_id !== options.formId || !isId(body.site_id)\n || (options.siteId !== undefined && body.site_id !== options.siteId)\n || !isNonEmptyString(body.form_key) || !isPositiveInteger(body.version)) {\n throw invalidFormResponse(response.status);\n }\n const definition = inquiryFormDefinitionSchema.safeParse(body.definition);\n if (!definition.success) throw invalidFormResponse(response.status);\n return {\n formId: options.formId,\n siteId: body.site_id,\n formKey: body.form_key,\n version: body.version,\n definition: definition.data,\n };\n}\n\n/**\n * Server-only JSON intake. A successful HTTP status is insufficient without a valid receipt.\n * Does not retry or generate keys; the caller keeps its key until the receipt is confirmed.\n */\nexport async function submitFormInquiry(options: SubmitFormInquiryOptions): Promise<SubmitFormInquiryResult> {\n assertServer();\n const fields = options.fields;\n let response: Response;\n try {\n response = await fetch(`${formUrl(options)}/submissions`, {\n method: \"POST\",\n headers: { ...authHeaders(options.apiKey), \"content-type\": \"application/json\" },\n body: JSON.stringify({\n site_id: fields.siteId,\n version: fields.version,\n answers: fields.answers,\n privacy_consent: fields.privacyConsent,\n idempotency_key: fields.idempotencyKey,\n placement: fields.placement,\n turnstile_token: fields.turnstileToken,\n attribution: fields.attribution ?? undefined,\n journey: fields.journey,\n }),\n cache: \"no-store\",\n redirect: \"manual\",\n signal: options.signal,\n });\n } catch {\n return failure(\"transport\", \"network_error\", null,\n \"접수 결과를 확인하지 못했습니다. 같은 내용으로 다시 시도해주세요.\");\n }\n\n const body = await readJson(response);\n if (!response.ok) {\n const code = isRecord(body) && isNonEmptyString(body.code) ? body.code : codeForStatus(response.status);\n const message = isRecord(body) && isNonEmptyString(body.message)\n ? body.message : \"문의 전송에 실패했습니다. 입력 내용과 연결 상태를 확인해주세요.\";\n const details = isRecord(body) && isRecord(body.details) ? body.details : undefined;\n return {\n ...failure(\"api\", code, response.status, message),\n fieldErrors: parseFieldErrors(details?.field_errors),\n formErrors: stringArray(details?.form_errors),\n };\n }\n\n if (!isRecord(body) || !isId(body.id) || !isPositiveInteger(body.inquiry_no)\n || !isTimestamp(body.received_at) || typeof body.replayed !== \"boolean\") {\n return failure(\"invalid_response\", \"invalid_response\", response.status,\n \"접수증을 확인하지 못했습니다. 같은 내용으로 다시 시도해주세요.\");\n }\n return {\n ok: true,\n receipt: { id: body.id, inquiryNo: body.inquiry_no, receivedAt: body.received_at, replayed: body.replayed },\n };\n}\n\nfunction formUrl(options: { baseUrl?: string; formId: string }): string {\n const base = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n return `${base}/v1/public/inquiry-forms/${encodeURIComponent(options.formId)}`;\n}\n\nfunction failure(\n kind: \"api\" | \"transport\" | \"invalid_response\", code: string, status: number | null, message: string,\n): Extract<SubmitFormInquiryResult, { ok: false }> {\n return { ok: false, kind, code, status, message, fieldErrors: {}, formErrors: [] };\n}\n\nfunction invalidFormResponse(status: number): CmsApiError {\n return new CmsApiError(\"RootTale inquiry form response is invalid\", status, \"invalid_response\");\n}\n\nasync function readJson(response: Response): Promise<unknown> {\n try { return await response.json(); } catch { return null; }\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction isNonEmptyString(value: unknown): value is string {\n return typeof value === \"string\" && value.trim().length > 0;\n}\n\nfunction isId(value: unknown): value is string {\n return typeof value === \"string\" && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);\n}\n\nfunction isPositiveInteger(value: unknown): value is number {\n return typeof value === \"number\" && Number.isSafeInteger(value) && value > 0;\n}\n\nfunction isTimestamp(value: unknown): value is string {\n return typeof value === \"string\"\n && /^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?(?:Z|[+-]\\d{2}:\\d{2})$/.test(value)\n && Number.isFinite(Date.parse(value));\n}\n\nfunction stringArray(value: unknown): string[] {\n return Array.isArray(value) ? value.filter((item): item is string => typeof item === \"string\") : [];\n}\n\nfunction parseFieldErrors(value: unknown): Record<string, string[]> {\n if (!isRecord(value)) return {};\n return Object.fromEntries(Object.entries(value).map(([key, errors]) => [key, stringArray(errors)]));\n}\n\nfunction codeForStatus(status: number): string {\n if (status === 401) return \"invalid_api_key\";\n if (status === 403) return \"insufficient_scope\";\n if (status === 404) return \"not_found\";\n if (status === 429) return \"rate_limited\";\n return \"request_failed\";\n}\n","import {\n type CmsPostContent,\n CmsApiError,\n DEFAULT_BASE_URL,\n assertServer,\n buildGetInit,\n toApiError,\n} from \"./server-base\";\nimport { type CmsPostWire, postFromWire } from \"./server-post-wire\";\n\n/**\n * 글 미리보기 — 어드민 편집기가 발급한 토큰으로 초안을 **발행 글과 같은 모양**으로\n * 받아 온다. 고객 사이트는 글 상세 템플릿을 그대로 재사용해 미리보기를 그리면\n * 된다(어드민이 대신 그리던 미리보기는 사이트 폰트·너비·목차와 달랐다).\n *\n * - 응답은 서버가 `cache-control: no-store` 로 보내고, 이 함수도 `cache: \"no-store\"`\n * 로 요청한다. 미리보기 페이지는 `export const dynamic = \"force-dynamic\"` 으로 두라.\n * - 토큰은 글 1건·1시간짜리. 만료되면 `CmsApiError`(status 410, code\n * `preview_expired`) 를 던진다 — `isPreviewExpiredError` 로 구분해 안내 문구를 보인다.\n */\nexport interface FetchPostPreviewOptions {\n /** `rtlk_cust_*` API key. Required. */\n readonly apiKey: string;\n /** Override the API base URL. Defaults to `https://api.roottale.com`. */\n readonly baseUrl?: string;\n /** 어드민 '미리보기'·'공유 링크' 가 URL `?token=` 으로 넘긴 토큰. */\n readonly token: string;\n readonly siteId?: string;\n readonly signal?: AbortSignal;\n}\n\nexport interface CmsPostPreviewInfo {\n /** 토큰 만료 시각(ISO). */\n readonly expiresAt: string;\n /** 글의 실제 상태. 본문 `status` 는 템플릿 재사용을 위해 항상 `published`. */\n readonly sourceStatus:\n | \"draft\"\n | \"pending\"\n | \"scheduled\"\n | \"published\"\n | \"private\"\n | \"trash\";\n}\n\nexport interface CmsPostPreviewContent extends CmsPostContent {\n readonly preview: CmsPostPreviewInfo;\n}\n\ntype CmsPostPreviewWire = CmsPostWire & {\n preview: { expires_at: string; source_status: CmsPostPreviewInfo[\"sourceStatus\"] };\n};\n\n/** 만료된 미리보기 링크(410 `preview_expired`)인지 구분한다. */\nexport function isPreviewExpiredError(error: unknown): boolean {\n return (\n error instanceof CmsApiError &&\n (error.code === \"preview_expired\" || error.status === 410)\n );\n}\n\n/**\n * 토큰이 없거나 다른 사이트의 것이면 `null`(404). 만료는 throw(410) — 두 경우를\n * 다르게 안내할 수 있게 구분한다.\n */\nexport async function fetchPostPreview(\n options: FetchPostPreviewOptions,\n): Promise<CmsPostPreviewContent | null> {\n assertServer();\n const token = options.token.trim();\n if (!token) return null;\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams({ token });\n if (options.siteId) params.set(\"site_id\", options.siteId);\n const url = `${baseUrl}/v1/cms/public/posts/preview?${params.toString()}`;\n\n const response = await fetch(url, {\n ...buildGetInit(options.apiKey, { signal: options.signal }),\n cache: \"no-store\",\n });\n\n if (response.status === 404) return null;\n if (!response.ok) {\n throw await toApiError(response);\n }\n\n const json = (await response.json()) as CmsPostPreviewWire;\n return {\n ...postFromWire(json),\n preview: {\n expiresAt: json.preview.expires_at,\n sourceStatus: json.preview.source_status,\n },\n };\n}\n","import { DEFAULT_BASE_URL, assertServer, buildGetInit, toApiError } from \"./server-base\";\n\n/** 블록 표시 형태 — 어드민이 정한 모양. 사이트는 값을 받아 그리기만 한다. */\nexport interface CmsSitePatternPresentation {\n /** `plain` = 본문처럼, `card` = 배경색 있는 카드. */\n layout: \"plain\" | \"card\";\n /** 카드 배경색(hex). */\n background: string;\n /** 마지막 문단의 링크를 버튼으로. */\n linkStyle: \"text\" | \"buttons\";\n /** 버튼 배경색 순서대로(글자는 흰색). 링크가 더 많으면 마지막 색 반복. */\n buttonColors: string[];\n}\n\nexport const DEFAULT_SITE_PATTERN_PRESENTATION: CmsSitePatternPresentation = {\n layout: \"plain\",\n background: \"#f8f9fa\",\n linkStyle: \"text\",\n buttonColors: [\"#03c75a\", \"#1a1a1a\", \"#666666\"],\n};\n\n/** 공통 블록 목록 fetch 캐시 tag — `theme.updated` 웹훅이 설정류와 함께 지운다. */\nexport const SITE_PATTERNS_CACHE_TAG = \"rt-site-patterns\";\n\n/** 글 하단 자리 key — 글 응답 `patternSlots` 와 어드민 배치 규칙이 같은 이름을 쓴다. */\nexport const POST_FOOTER_PATTERN_SLOT = \"post_footer\";\n\n/**\n * 공통 블록(Site Pattern) — 어드민 콘텐츠 도구 > 공통 블록에서 발행한 재사용 본문.\n * 어느 글의 어느 자리에 놓을지는 글 응답 `patternSlots` 가 이미 말해 주므로 사이트는\n * `patternSlots[slot]` 로 이 목록에서 key 를 찾아 글 본문과 같은 렌더러로 그리면 된다.\n */\nexport interface CmsSitePattern {\n id: string;\n key: string;\n name: string;\n description: string | null;\n /** Tiptap 문서 JSON. 글 본문(`bodyJson`)과 같은 렌더·정화 경로로 그린다. */\n bodyJson: Record<string, unknown>;\n /** 표시 형태(구 서버는 기본값). */\n presentation: CmsSitePatternPresentation;\n /** ISO 8601. */\n updatedAt: string;\n}\n\nexport interface FetchSitePatternsOptions {\n apiKey: string;\n baseUrl?: string;\n siteId?: string;\n signal?: AbortSignal;\n /** ISR revalidate 초(opt-in). 미지정 시 현행 동작 유지. */\n revalidate?: number;\n /** on-demand 무효화용 Next fetch cache tags(opt-in). 보통 `[SITE_PATTERNS_CACHE_TAG]`. */\n tags?: string[];\n}\n\n/**\n * 발행된 공통 블록 전체를 가져온다. 네트워크/서버 에러는 throw — 호출자가 빈 배열로\n * fail-soft 한다(블록이 없다고 글이 안 열리면 안 된다).\n */\nexport async function fetchSitePatterns(\n options: FetchSitePatternsOptions,\n): Promise<CmsSitePattern[]> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams();\n if (options.siteId) params.set(\"site_id\", options.siteId);\n const qs = params.toString();\n const url = `${baseUrl}/v1/cms/public/patterns${qs ? `?${qs}` : \"\"}`;\n\n const response = await fetch(url, buildGetInit(options.apiKey, options));\n if (!response.ok) throw await toApiError(response);\n\n const json = (await response.json()) as { patterns?: unknown };\n const items = Array.isArray(json.patterns) ? json.patterns : [];\n return items.flatMap((item) => {\n const pattern = sitePatternFromWire(item);\n return pattern ? [pattern] : [];\n });\n}\n\nfunction sitePatternFromWire(raw: unknown): CmsSitePattern | null {\n if (!raw || typeof raw !== \"object\" || Array.isArray(raw)) return null;\n const o = raw as Record<string, unknown>;\n if (typeof o.id !== \"string\" || typeof o.key !== \"string\" || o.key.length === 0) return null;\n if (typeof o.name !== \"string\") return null;\n const bodyJson = o.body_json;\n if (!bodyJson || typeof bodyJson !== \"object\" || Array.isArray(bodyJson)) return null;\n return {\n id: o.id,\n key: o.key,\n name: o.name,\n description: typeof o.description === \"string\" ? o.description : null,\n bodyJson: bodyJson as Record<string, unknown>,\n presentation: presentationFromWire(o.presentation),\n updatedAt: typeof o.updated_at === \"string\" ? o.updated_at : \"\",\n };\n}\n\nconst HEX_RE = /^#[0-9a-f]{6}$/;\n\nfunction presentationFromWire(raw: unknown): CmsSitePatternPresentation {\n const base = DEFAULT_SITE_PATTERN_PRESENTATION;\n if (!raw || typeof raw !== \"object\") return { ...base, buttonColors: [...base.buttonColors] };\n const o = raw as Record<string, unknown>;\n const colors = Array.isArray(o.button_colors)\n ? o.button_colors.filter((c): c is string => typeof c === \"string\" && HEX_RE.test(c))\n : [];\n return {\n layout: o.layout === \"card\" ? \"card\" : \"plain\",\n background: typeof o.background === \"string\" && HEX_RE.test(o.background) ? o.background : base.background,\n linkStyle: o.link_style === \"buttons\" ? \"buttons\" : \"text\",\n buttonColors: colors.length > 0 ? colors : [...base.buttonColors],\n };\n}\n\n/**\n * 표시 형태 → 래퍼 요소에 얹을 data 속성과 CSS 변수. 사이트 CSS 는 이 변수만 읽는다:\n * `[data-pattern-layout=\"card\"]`, `[data-pattern-link-style=\"buttons\"]`, `--rt-pattern-bg`,\n * `--rt-pattern-btn-1..n`(버튼 n번째 색).\n */\nexport function sitePatternPresentationAttributes(presentation: CmsSitePatternPresentation): {\n dataset: Record<string, string>;\n style: Record<string, string>;\n} {\n const style: Record<string, string> = { \"--rt-pattern-bg\": presentation.background };\n presentation.buttonColors.forEach((color, index) => {\n style[`--rt-pattern-btn-${index + 1}`] = color;\n });\n return {\n dataset: {\n \"data-pattern-layout\": presentation.layout,\n \"data-pattern-link-style\": presentation.linkStyle,\n },\n style,\n };\n}\n\n/**\n * 글의 `patternSlots` 와 블록 목록으로 그 자리에 놓을 블록을 고른다. 자리가 비었거나\n * (null·구버전 응답) key 가 목록에 없으면(미발행·삭제) null.\n */\nexport function selectSitePatternForSlot(\n patternSlots: Readonly<Record<string, string | null>> | null | undefined,\n patterns: readonly CmsSitePattern[],\n slot: string = POST_FOOTER_PATTERN_SLOT,\n): CmsSitePattern | null {\n const key = patternSlots?.[slot] ?? null;\n if (!key) return null;\n return patterns.find((pattern) => pattern.key === key) ?? null;\n}\n","import { MetadataRobotsPolicySchema, MetadataValuesSchema, type PublicRouteMetadata } from \"@roottale/cms-core\";\nimport { CmsApiError, DEFAULT_BASE_URL, assertServer, buildGetInit, toApiError } from \"./server-base\";\n\nexport { readMetadataValues, resolveMetadata, formatMetadataDefaults, metadataBodyParagraphs, metadataContentImageUrl, MetadataDefaultRulesSchema } from \"@roottale/cms-core\";\nexport type { MetadataField, MetadataValues, MetadataPatch, PublicRouteMetadata, MetadataResolutionInput, MetadataRobotsPolicy, MetadataDefaultsDeclaration, MetadataDefaultsInput, MetadataDefaultRules, MetadataTextRule } from \"@roottale/cms-core\";\n\nexport const METADATA_CACHE_TAG = \"rt-metadata\";\nexport const SITE_METADATA_DEFAULTS_CACHE_TAG = \"rt-metadata-defaults\";\n/** Add to public post/term fetches whose fields determine a declared content policy. */\nexport const CONTENT_POLICY_CACHE_TAG = \"rt-content-policy\";\n\n/** locale를 생략한 요청은 사이트 기본 locale을 조회한다. */\nexport function routeMetadataCacheTag(routeKey: string, locale?: string): string {\n if (!/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(routeKey) || routeKey.length > 160) throw new Error(\"Invalid metadata route key\");\n const language = locale?.trim().toLowerCase() || \"default\";\n if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(language) || language.length > 40) {\n throw new Error(\"Invalid metadata locale\");\n }\n return `${METADATA_CACHE_TAG}:${routeKey}:${language}`;\n}\n\nexport interface FetchRouteMetadataOptions {\n apiKey: string;\n baseUrl?: string;\n siteId?: string;\n routeKey: string;\n locale?: string;\n signal?: AbortSignal;\n /** 기본 300초. 웹훅 유실 시 재조회하며, 0이면 항상 최신값을 읽는다. */\n revalidate?: number;\n}\n\n/**\n * 고유 route key 직접 조회. 등록되지 않은 경로만 null이며, 연결된 빈 설정은\n * overrides:{}인 객체다. HTTP/네트워크/응답 오류는 throw하여 빈 설정으로 캐시하지 않는다.\n * 공개 읽기는 구독 등급을 검사하지 않아 요금제 하향 후에도 기존 설정을 유지한다.\n */\nexport async function fetchRouteMetadata(options: FetchRouteMetadataOptions): Promise<PublicRouteMetadata | null> {\n assertServer();\n const routeTag = routeMetadataCacheTag(options.routeKey, options.locale);\n const params = new URLSearchParams({ route_key: options.routeKey });\n if (options.locale) params.set(\"locale\", options.locale);\n if (options.siteId) params.set(\"site_id\", options.siteId);\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const response = await fetch(`${baseUrl}/v1/cms/public/metadata?${params}`, buildGetInit(options.apiKey, {\n signal: options.signal,\n revalidate: options.revalidate ?? 300,\n tags: [METADATA_CACHE_TAG, SITE_METADATA_DEFAULTS_CACHE_TAG, routeTag],\n }));\n if (!response.ok) throw await toApiError(response);\n const json: unknown = await response.json();\n if (!json || typeof json !== \"object\" || !(\"metadata\" in json)) throw unexpectedResponse(response.status);\n if (json.metadata === null) return null;\n if (!isRouteMetadata(json.metadata) || json.metadata.routeKey !== options.routeKey) throw unexpectedResponse(response.status);\n return json.metadata;\n}\n\nfunction isValues(value: unknown): value is Record<string, unknown> {\n return MetadataValuesSchema.safeParse(value).success;\n}\n\nfunction isRouteMetadata(value: unknown): value is PublicRouteMetadata {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) return false;\n const row = value as Record<string, unknown>;\n return typeof row.routeKey === \"string\" && typeof row.locale === \"string\" &&\n typeof row.path === \"string\" && row.path.startsWith(\"/\") && !row.path.startsWith(\"//\") &&\n typeof row.version === \"string\" && isValues(row.overrides) && isValues(row.defaults) &&\n isValues(row.effective) && (row.fallback === undefined || isValues(row.fallback)) &&\n (row.robots === undefined || MetadataRobotsPolicySchema.safeParse(row.robots).success) &&\n Array.isArray(row.supportedFields) &&\n row.supportedFields.every((field) => [\"title\", \"description\", \"ogImage\", \"ogImageAlt\", \"noindex\", \"nofollow\"].includes(field));\n}\n\nfunction unexpectedResponse(status: number): CmsApiError {\n return new CmsApiError(\"RootTale CMS API returned invalid route metadata\", status, \"unexpected_response\");\n}\n","function canonicalize(value: unknown): unknown {\n if (Array.isArray(value)) return value.map(canonicalize);\n if (!value || typeof value !== \"object\") return value;\n return Object.fromEntries(Object.entries(value)\n .sort(([left], [right]) => left.localeCompare(right))\n .map(([key, child]) => [key, canonicalize(child)]));\n}\n\n/**\n * 배포 도구가 정규화해 등록한 manifest의 SHA-256(64자리 hex).\n * 이 함수는 스키마 검사·기본값 채우기를 하지 않는다. 원본 JSON을 그대로 해시하면\n * 등록 API가 정규화한 계약과 달라질 수 있으므로 배포 시 정규화한 manifest를 사용한다.\n */\nexport async function metadataContractChecksum(normalizedContract: unknown): Promise<string> {\n const json = JSON.stringify(canonicalize(normalizedContract));\n if (json === undefined) throw new Error(\"Metadata contract must be a normalized JSON value\");\n const digest = await globalThis.crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(json));\n return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, \"0\")).join(\"\");\n}\n","/**\n * 설정류 fetch 캐시 tag 한 벌 (ADR-0096 Phase C).\n *\n * 개별 tag 상수는 각자의 fetch 함수 옆에 산다 — `THEME_CACHE_TAG`\n * (server-posts-theme) · `BUSINESS_CACHE_TAG`(server-business) ·\n * `MENUS_CACHE_TAG`(server-discovery) · `BLOG_SETTINGS_CACHE_TAG` ·\n * `COLLECTIONS_CACHE_TAG`(server-blog). 본 모듈은 그것들을 **묶기만** 한다:\n * 어느 도메인 모듈에 배열을 두어도 나머지 셋을 import 해야 해서 모듈 방향이\n * 꼬이기 때문이다(여기서 한 방향으로만 import 한다).\n */\n\nimport { BLOG_SETTINGS_CACHE_TAG, COLLECTIONS_CACHE_TAG } from \"./server-blog\";\nimport { BUSINESS_CACHE_TAG } from \"./server-business\";\nimport { MENUS_CACHE_TAG } from \"./server-discovery\";\nimport { THEME_CACHE_TAG } from \"./server-posts-theme\";\nimport { EXPOSURE_CACHE_TAG } from \"./server-exposure\";\nimport { SITE_PATTERNS_CACHE_TAG } from \"./server-patterns\";\nimport { METADATA_CACHE_TAG, SITE_METADATA_DEFAULTS_CACHE_TAG } from \"./server-metadata\";\n\n/**\n * 어드민 \"설정\" 저장으로 낡는 fetch 캐시 tag 전부 — 테마·사업장 정보·메뉴·\n * 블로그 표시 설정·콘텐츠 유형·노출 캠페인·공통 블록.\n *\n * 수신측(`createRevalidateRoute`)이 `theme.updated` 한 번에 이 목록을 통째로\n * 무효화한다. 사이트가 이 중 일부 fetch 만 쓰더라도 안 쓰는 tag 의\n * `revalidateTag` 는 no-op 이므로 해가 없다 — 그래서 \"설정을 저장했다\" 신호\n * 하나로 설정류 전체를 지우는 것이 안전한 기본값이다.\n */\nexport const SETTINGS_CACHE_TAGS: readonly string[] = [\n THEME_CACHE_TAG,\n BUSINESS_CACHE_TAG,\n MENUS_CACHE_TAG,\n BLOG_SETTINGS_CACHE_TAG,\n COLLECTIONS_CACHE_TAG,\n EXPOSURE_CACHE_TAG,\n SITE_PATTERNS_CACHE_TAG,\n METADATA_CACHE_TAG,\n SITE_METADATA_DEFAULTS_CACHE_TAG,\n];\n","// 프레임워크 무관 SEO 헬퍼 — RSS 2.0 피드 + Schema.org JSON-LD 빌더.\n// 외부 사이트(Next route handler / Astro endpoint / RSC)가 fetchPosts 결과로\n// /feed.xml 응답이나 <head> JSON-LD 를 만들 때 사용. 순수 함수(네트워크 없음).\n\nimport type {\n CmsPostContent,\n RootTaleBusinessProfile,\n SiteVerificationSettings,\n} from \"./server\";\n\n// ── RSS 2.0 ────────────────────────────────────────────────────────────────\n\nexport interface RssItemInput {\n title: string;\n /** 글 절대 URL. */\n link: string;\n /** 고유 식별자. 미지정 시 link 사용. */\n guid?: string;\n description?: string | null;\n /** 발행 시각. */\n pubDate?: string | Date | null;\n}\n\nexport interface RssFeedInput {\n /** 사이트/피드 제목. */\n title: string;\n /** 사이트 홈 URL. */\n link: string;\n /** 이 피드 자체의 URL (atom:self). */\n feedUrl: string;\n /** WebSub hub 주소. 지정하면 atom:hub discovery link를 낸다. */\n hubUrl?: string;\n description?: string;\n /** 기본 \"ko-KR\". */\n language?: string;\n items: RssItemInput[];\n}\n\n/** RSS 2.0 XML 문자열 생성. */\nexport function generateRssXml(input: RssFeedInput): string {\n const lang = input.language ?? \"ko-KR\";\n const lastBuild =\n input.items.length > 0 ? toRfc822(input.items[0]!.pubDate) : \"\";\n const lines: string[] = [];\n lines.push('<?xml version=\"1.0\" encoding=\"UTF-8\"?>');\n lines.push(\n '<rss version=\"2.0\" xmlns:atom=\"http://www.w3.org/2005/Atom\" xmlns:content=\"http://purl.org/rss/1.0/modules/content/\">',\n );\n lines.push(\"<channel>\");\n lines.push(`<title>${escapeXml(input.title)}</title>`);\n lines.push(`<link>${escapeXml(input.link)}</link>`);\n lines.push(\n `<atom:link href=\"${escapeXml(input.feedUrl)}\" rel=\"self\" type=\"application/rss+xml\" />`,\n );\n if (input.hubUrl) {\n lines.push(\n `<atom:link href=\"${escapeXml(input.hubUrl)}\" rel=\"hub\" />`,\n );\n }\n lines.push(\n `<description>${escapeXml(input.description ?? input.title)}</description>`,\n );\n lines.push(`<language>${escapeXml(lang)}</language>`);\n if (lastBuild) lines.push(`<lastBuildDate>${lastBuild}</lastBuildDate>`);\n for (const item of input.items) {\n const guid = item.guid ?? item.link;\n lines.push(\"<item>\");\n lines.push(`<title>${escapeXml(item.title)}</title>`);\n lines.push(`<link>${escapeXml(item.link)}</link>`);\n lines.push(`<guid isPermaLink=\"false\">${escapeXml(guid)}</guid>`);\n if (item.description) {\n lines.push(`<description>${cdata(item.description)}</description>`);\n }\n const pub = toRfc822(item.pubDate);\n if (pub) lines.push(`<pubDate>${pub}</pubDate>`);\n lines.push(\"</item>\");\n }\n lines.push(\"</channel>\");\n lines.push(\"</rss>\");\n return lines.join(\"\\n\");\n}\n\n/** CmsPostContent[] → RSS item[]. postUrl 로 글 절대 URL 생성. */\nexport function rssItemsFromPosts(\n posts: CmsPostContent[],\n opts: { postUrl: (post: CmsPostContent) => string },\n): RssItemInput[] {\n return posts.map((p) => ({\n title: p.title,\n link: opts.postUrl(p),\n guid: opts.postUrl(p),\n description: p.excerpt,\n pubDate: p.publishedAt,\n }));\n}\n\nfunction toRfc822(value: string | Date | null | undefined): string {\n if (!value) return \"\";\n const d = value instanceof Date ? value : new Date(value);\n return Number.isNaN(d.getTime()) ? \"\" : d.toUTCString();\n}\n\n// ── Schema.org JSON-LD 빌더 ─────────────────────────────────────────────────\n\ntype JsonLd = Record<string, unknown>;\n\nexport interface ArticleSchemaInput {\n headline: string;\n url: string;\n description?: string | null;\n image?: string | null;\n datePublished?: string | Date | null;\n dateModified?: string | Date | null;\n authorName?: string | null;\n publisherName?: string;\n publisherLogo?: string;\n /** Schema.org 타입. 기본 \"Article\" (미지정 시 기존 출력 그대로). */\n type?: \"Article\" | \"BlogPosting\";\n /** 지정 시 `@id` emit (관례: `<url>#article`). */\n id?: string;\n /** 둘 다 있고 image 도 있으면 image 를 ImageObject 로 승격. */\n imageWidth?: number;\n imageHeight?: number;\n /** 저자 페이지 URL — 있으면 author 에 url + `@id`(`<authorUrl>#person`) 추가. */\n authorUrl?: string;\n /** 있으면 publisher 에 `@id` 추가 (name/logo 는 유지). */\n publisherId?: string;\n /** → articleSection. */\n section?: string;\n /** → inLanguage (기본값 없음, 지정 시만). */\n inLanguage?: string;\n}\n\nexport function articleSchema(input: ArticleSchemaInput): JsonLd {\n const schema: JsonLd = {\n \"@context\": \"https://schema.org\",\n \"@type\": input.type ?? \"Article\",\n headline: input.headline,\n mainEntityOfPage: { \"@type\": \"WebPage\", \"@id\": input.url },\n };\n if (input.id) schema[\"@id\"] = input.id;\n if (input.description) schema.description = input.description;\n if (input.image) {\n schema.image =\n input.imageWidth && input.imageHeight\n ? {\n \"@type\": \"ImageObject\",\n url: input.image,\n width: input.imageWidth,\n height: input.imageHeight,\n }\n : input.image;\n }\n const pub = toIso(input.datePublished);\n if (pub) schema.datePublished = pub;\n const mod = toIso(input.dateModified);\n if (mod) schema.dateModified = mod;\n if (input.authorName) {\n schema.author = {\n \"@type\": \"Person\",\n name: input.authorName,\n ...(input.authorUrl\n ? { \"@id\": `${input.authorUrl}#person`, url: input.authorUrl }\n : {}),\n };\n }\n if (input.publisherName) {\n schema.publisher = {\n \"@type\": \"Organization\",\n ...(input.publisherId ? { \"@id\": input.publisherId } : {}),\n name: input.publisherName,\n ...(input.publisherLogo\n ? { logo: { \"@type\": \"ImageObject\", url: input.publisherLogo } }\n : {}),\n };\n }\n if (input.section) schema.articleSection = input.section;\n if (input.inLanguage) schema.inLanguage = input.inLanguage;\n return schema;\n}\n\n/**\n * 저자 프로필 페이지용 ProfilePage JSON-LD.\n * `/blog/author/<slug>` 같은 저자 페이지 `<head>` 에서 사용.\n */\nexport function profilePageSchema(input: {\n url: string;\n name: string;\n image?: string | null;\n description?: string | null;\n}): JsonLd {\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"ProfilePage\",\n mainEntity: {\n \"@type\": \"Person\",\n \"@id\": `${input.url}#person`,\n name: input.name,\n url: input.url,\n ...(input.image ? { image: input.image } : {}),\n ...(input.description ? { description: input.description } : {}),\n },\n };\n}\n\n/**\n * 글 목록·카테고리 허브 페이지용 CollectionPage JSON-LD.\n * `/blog`, `/blog/category/<slug>` 등에서 사용.\n */\nexport function collectionPageSchema(input: {\n url: string;\n name: string;\n description?: string | null;\n siteUrl?: string;\n}): JsonLd {\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"CollectionPage\",\n \"@id\": `${input.url}#collection`,\n url: input.url,\n name: input.name,\n ...(input.description ? { description: input.description } : {}),\n ...(input.siteUrl\n ? { isPartOf: { \"@type\": \"WebSite\", url: input.siteUrl } }\n : {}),\n };\n}\n\nexport function breadcrumbSchema(\n items: { name: string; url: string }[],\n): JsonLd {\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"BreadcrumbList\",\n itemListElement: items.map((it, i) => ({\n \"@type\": \"ListItem\",\n position: i + 1,\n name: it.name,\n item: it.url,\n })),\n };\n}\n\nexport function organizationSchema(input: {\n name: string;\n url: string;\n logo?: string;\n sameAs?: string[];\n}): JsonLd {\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"Organization\",\n name: input.name,\n url: input.url,\n ...(input.logo ? { logo: input.logo } : {}),\n ...(input.sameAs && input.sameAs.length > 0 ? { sameAs: input.sameAs } : {}),\n };\n}\n\n// ── LocalBusiness (로컬 SEO) ────────────────────────────────────────────────\n\n/** Mo~Su 약어 → Schema.org dayOfWeek 전체 이름. */\nconst DAY_OF_WEEK: Record<string, string> = {\n Mo: \"Monday\",\n Tu: \"Tuesday\",\n We: \"Wednesday\",\n Th: \"Thursday\",\n Fr: \"Friday\",\n Sa: \"Saturday\",\n Su: \"Sunday\",\n};\n\n/**\n * `localBusinessSchema` 입력 — `fetchBusinessProfile` 의 반환값\n * (RootTaleBusinessProfile) 을 그대로 받도록 느슨하게 정의 (null/undefined 모두\n * 허용). 수동 구성도 가능.\n */\nexport interface LocalBusinessProfileInput {\n name: string;\n legalName?: string | null;\n /** 간판명의 표기 변형(띄어쓰기 등) — alternateName. */\n alternateName?: string | null;\n /** Schema.org LocalBusiness 하위 타입. 미지정 시 \"LocalBusiness\". */\n businessType?: string | null;\n telephone?: string | null;\n faxNumber?: string | null;\n email?: string | null;\n address?: {\n streetAddress: string;\n addressLocality: string;\n addressRegion?: string | null;\n postalCode?: string | null;\n } | null;\n geo?: { latitude: number; longitude: number } | null;\n openingHours?:\n | { days: string[]; opens: string; closes: string }[]\n | null;\n priceRange?: string | null;\n areaServed?: string[] | null;\n /** 취급 업무 — hasOfferCatalog 의 Offer 목록으로 들어간다. */\n services?: string[] | null;\n /** 외부 프로필 URL — 값들이 sameAs 로 들어간다 (네이버플레이스·구글 등). */\n profiles?: Record<string, string | null | undefined> | null;\n}\n\n/**\n * LocalBusiness JSON-LD — admin \"운영 > 비즈니스 프로필\" 의\n * `fetchBusinessProfile` 결과로 layout 에 1회 렌더한다 (로컬 SEO).\n *\n * - `@type` 은 `[businessType, \"LocalBusiness\"]` 중복 제거 배열.\n * - `profiles` 의 URL 들은 `sameAs` 로 — 검색엔진이 네이버플레이스/구글\n * 비즈니스 프로필과 동일 사업장임을 연결한다.\n */\n/** 취급 업무 상한 — api-contracts / api-core sanitize 와 같은 값이어야 한다. */\nexport const LOCAL_BUSINESS_MAX_SERVICES = 12;\nexport const LOCAL_BUSINESS_SERVICE_MAX_LENGTH = 80;\n\n/** hasMap 으로 빠지고 sameAs 에는 넣지 않는 지도 딥링크 키. */\nconst MAP_PROFILE_KEYS = new Set([\"kakaoMap\", \"kakao_map\"]);\n\nexport function localBusinessSchema(\n profile: LocalBusinessProfileInput | RootTaleBusinessProfile,\n opts: { url: string; image?: string | null },\n): JsonLd {\n const input = profile as LocalBusinessProfileInput;\n const types = Array.from(\n new Set([input.businessType || \"LocalBusiness\", \"LocalBusiness\"]),\n );\n const schema: JsonLd = {\n \"@context\": \"https://schema.org\",\n \"@type\": types,\n name: input.name,\n url: opts.url,\n };\n if (input.legalName) schema.legalName = input.legalName;\n if (input.alternateName) schema.alternateName = input.alternateName;\n if (opts.image) schema.image = opts.image;\n if (input.telephone) schema.telephone = input.telephone;\n if (input.faxNumber) schema.faxNumber = input.faxNumber;\n if (input.email) schema.email = input.email;\n if (input.address) {\n schema.address = {\n \"@type\": \"PostalAddress\",\n streetAddress: input.address.streetAddress,\n addressLocality: input.address.addressLocality,\n ...(input.address.addressRegion\n ? { addressRegion: input.address.addressRegion }\n : {}),\n ...(input.address.postalCode\n ? { postalCode: input.address.postalCode }\n : {}),\n addressCountry: \"KR\",\n };\n }\n if (input.geo) {\n schema.geo = {\n \"@type\": \"GeoCoordinates\",\n latitude: input.geo.latitude,\n longitude: input.geo.longitude,\n };\n }\n const openingHours = (input.openingHours ?? []).filter(\n (row) => row.days.length > 0 && row.opens && row.closes,\n );\n if (openingHours.length > 0) {\n schema.openingHoursSpecification = openingHours.map((row) => ({\n \"@type\": \"OpeningHoursSpecification\",\n dayOfWeek: row.days.map((d) => DAY_OF_WEEK[d] ?? d),\n opens: row.opens,\n closes: row.closes,\n }));\n }\n if (input.priceRange) schema.priceRange = input.priceRange;\n if (input.areaServed && input.areaServed.length > 0) {\n schema.areaServed = input.areaServed;\n }\n // 취급 업무 → OfferCatalog. \"무엇을 하는 곳인가\" 를 검색엔진에 명시하는 신호.\n //\n // 이 함수는 공개 API 라 admin/sanitize 를 거치지 않은 수동 입력도 들어온다.\n // 두 경로의 결과가 갈리지 않도록 여기서도 같은 정규화(공백 정리 → 길이 제한\n // → 중복 제거 → 개수 제한)를 적용한다.\n //\n // Service 단위 areaServed 는 넣지 않는다 — 서비스별 지역 데이터가 없어서\n // 첫 지역만 쓰면 \"이 서비스는 서울에서만 제공\" 이라는 없는 사실을 주장하게 된다.\n // 지역은 최상위 areaServed 가 이미 담고 있다.\n const services: string[] = [];\n for (const raw of input.services ?? []) {\n if (services.length >= LOCAL_BUSINESS_MAX_SERVICES) break;\n if (typeof raw !== \"string\") continue;\n const name = raw.trim().slice(0, LOCAL_BUSINESS_SERVICE_MAX_LENGTH);\n if (name && !services.includes(name)) services.push(name);\n }\n if (services.length > 0) {\n schema.hasOfferCatalog = {\n \"@type\": \"OfferCatalog\",\n name: `${input.name} 서비스`,\n itemListElement: services.map((name) => ({\n \"@type\": \"Offer\",\n itemOffered: { \"@type\": \"Service\", name },\n })),\n };\n }\n const profiles = input.profiles ?? {};\n // 지도 링크는 sameAs 가 아니라 hasMap 이 정확한 자리다. 네이버 플레이스 우선,\n // 없으면 지도 딥링크로 대체한다. `profiles` 는 수동 구성도 허용하는 느슨한\n // Record 라, 지도 키는 camelCase·snake_case 를 모두 인정한다.\n const pickUrl = (key: string): string | null => {\n const value = profiles[key];\n return typeof value === \"string\" && value.length > 0 ? value : null;\n };\n const mapUrl = pickUrl(\"naverPlace\") ?? pickUrl(\"kakaoMap\") ?? pickUrl(\"kakao_map\");\n if (mapUrl) schema.hasMap = mapUrl;\n // sameAs 는 \"같은 사업체를 가리키는 프로필 페이지\" 만 — 지도 딥링크는 프로필이\n // 아니라 위치 링크라 hasMap 으로만 내보내고 여기서는 뺀다.\n const sameAs = Object.entries(profiles)\n .filter(([key]) => !MAP_PROFILE_KEYS.has(key))\n .map(([, value]) => value)\n .filter((u): u is string => typeof u === \"string\" && u.length > 0);\n if (sameAs.length > 0) schema.sameAs = sameAs;\n return schema;\n}\n\nexport function websiteSchema(input: {\n name: string;\n url: string;\n searchUrl?: string;\n}): JsonLd {\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"WebSite\",\n name: input.name,\n url: input.url,\n ...(input.searchUrl\n ? {\n potentialAction: {\n \"@type\": \"SearchAction\",\n target: `${input.searchUrl}{search_term_string}`,\n \"query-input\": \"required name=search_term_string\",\n },\n }\n : {}),\n };\n}\n\nexport function faqSchema(\n qa: { question: string; answer: string }[],\n): JsonLd {\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"FAQPage\",\n mainEntity: qa.map((x) => ({\n \"@type\": \"Question\",\n name: x.question,\n acceptedAnswer: { \"@type\": \"Answer\", text: x.answer },\n })),\n };\n}\n\n/**\n * ADR-0065 — 글 조회수 식별자 meta 태그 HTML.\n *\n * 글 상세 페이지 `<head>` 에 렌더하면, RootTale 비콘이 이 값을 읽어 pageview 에\n * `post_id` 를 동봉한다 → URL/컬렉션 basePath 변경에 무관하게 조회수가 해당 글에\n * 정확히 집계된다. `post.id`(uuidv7) 를 그대로 넘기면 된다.\n *\n * Next App Router 는 `generateMetadata` 의 `other: { \"rt:content-id\": post.id }`\n * 로도 동일 태그를 낼 수 있다 — 그 경우 본 헬퍼 대신 metadata 를 쓴다.\n *\n * @example\n * // 프레임워크 무관(Astro/RSC set:html 등):\n * <head>{contentIdMeta(post.id)}</head>\n */\nexport function contentIdMeta(postId: string): string {\n return `<meta name=\"rt:content-id\" content=\"${escapeXml(postId)}\" />`;\n}\n\n/**\n * JSON-LD 객체 → XSS-safe JSON *문자열* (script 태그 없음). React 소비자가\n * `<script type=\"application/ld+json\" dangerouslySetInnerHTML={{ __html: ... }} />`\n * 로 직접 방출할 때 사용 — `</script>` 주입 방지 escape 의 단일 소스.\n */\nexport function jsonLdJson(schema: JsonLd | JsonLd[]): string {\n // `</script>` 주입 방지를 위해 `<` 이스케이프.\n return JSON.stringify(schema).replace(/</g, \"\\\\u003c\");\n}\n\n/** JSON-LD 객체 → `<script type=\"application/ld+json\">` HTML 문자열 (XSS-safe). */\nexport function jsonLdScript(schema: JsonLd | JsonLd[]): string {\n return `<script type=\"application/ld+json\">${jsonLdJson(schema)}</script>`;\n}\n\nfunction toIso(value: string | Date | null | undefined): string | null {\n if (!value) return null;\n const d = value instanceof Date ? value : new Date(value);\n return Number.isNaN(d.getTime()) ? null : d.toISOString();\n}\n\nfunction escapeXml(value: string): string {\n return value\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\");\n}\n\nfunction cdata(value: string): string {\n return `<![CDATA[${value.replace(/]]>/g, \"]]]]><![CDATA[>\")}]]>`;\n}\n\n// ── 검색엔진 소유 확인 ──────────────────────────────────────────────────────\n\n/** Next.js `Metadata[\"verification\"]` 와 구조 호환. */\nexport interface SiteVerificationMetadata {\n google?: string[];\n other?: Record<string, string[]>;\n}\n\n/**\n * `theme.siteVerification` → 루트 layout metadata 의 `verification`.\n * 코드가 없으면 undefined 를 돌려줘 spread 로 생략할 수 있다.\n */\nexport function siteVerificationMetadata(\n settings: SiteVerificationSettings | undefined,\n): SiteVerificationMetadata | undefined {\n const google = settings?.google?.filter(Boolean) ?? [];\n const naver = settings?.naver?.filter(Boolean) ?? [];\n if (google.length === 0 && naver.length === 0) return undefined;\n return {\n ...(google.length > 0 ? { google } : {}),\n ...(naver.length > 0 ? { other: { \"naver-site-verification\": naver } } : {}),\n };\n}\n","/**\n * `@roottale/cms-client` — 리드(문의) 접수 server-side 헬퍼.\n *\n * 외부 고객사 사이트가 *블로그 조회(`fetchPosts`)와 동일한 API key 하나* 로 리드도\n * 접수하기 위한 SSR-only 클라이언트. 브라우저가 아니라 사이트 서버(Server Action /\n * Route Handler)에서 호출해야 한다 — API key 는 절대 브라우저로 노출 금지\n * (ADR-0023 §5.1 #15). 키가 tenant 를 권위 식별하므로 `tenant_slug` 불필요.\n *\n * 사용 예 (Next.js server action):\n * ```ts\n * \"use server\";\n * import { submitInquiry } from \"@roottale/cms-client/server\";\n *\n * const result = await submitInquiry({\n * apiKey: process.env.ROOTTALE_API_KEY!, // fetchPosts 와 같은 키\n * fields: {\n * vertical: \"tax\",\n * contactName: name,\n * businessName: company,\n * email,\n * phone,\n * message,\n * privacyConsent: true,\n * },\n * });\n * if (result.ok) showSuccess(); // 그 키의 tenant /crm 에 도착\n * else showError(result.message);\n * ```\n *\n * intake API 는 302 `?ok=1` / `?err=*` 로 응답 — 본 헬퍼가 파싱해 구조화 결과 반환.\n */\n\nimport type { InquiryAttribution } from \"./attribution\";\n\nconst DEFAULT_BASE_URL = \"https://api.roottale.com\";\n\nexport type InquiryVertical = \"consulting\" | \"medical\" | \"tax\" | \"legal\";\n\n/** intake API 의 `err` 코드 → 한국어 사용자 메시지. */\nexport const INQUIRY_ERROR_MESSAGES: Record<string, string> = {\n consent_privacy: \"개인정보 수집·이용 동의가 필요합니다.\",\n consent_overseas: \"의료 분야는 국외이전 동의가 필요합니다.\",\n invalid_vertical: \"업종 분류가 유효하지 않습니다.\",\n missing_fields: \"필수 항목이 비어 있습니다.\",\n invalid_email: \"이메일 형식이 올바르지 않습니다.\",\n invalid_api_key: \"연동 인증에 실패했습니다. API 키를 확인해주세요.\",\n rate_limited: \"요청이 너무 많습니다. 잠시 후 다시 시도해주세요.\",\n turnstile_failed: \"보안 확인에 실패했습니다. 새로고침 후 다시 시도해주세요.\",\n internal: \"전송에 실패했습니다. 잠시 후 다시 시도해주세요.\",\n};\n\nexport interface SubmitInquiryFields {\n vertical: InquiryVertical;\n contactName: string;\n businessName: string;\n email: string;\n phone: string;\n /** 자유 문의 내용 (선택). */\n message?: string;\n /** 상담 분야 라벨 등 부가 메타 (선택). */\n consultationField?: string;\n /** 현재 사이트 URL (선택). */\n currentSiteUrl?: string;\n /** 개인정보 수집·이용 동의 (필수 true). */\n privacyConsent: boolean;\n /** 의료 vertical 의 국외이전 동의 (medical 일 때 필수). */\n overseasTransferConsent?: boolean;\n /**\n * \"patient\"(고객사 방문자 문의, 기본) | \"sales\"(RootTale 자체 B2B 영업 리드).\n * sales 는 medical 국외이전 동의 게이트 면제.\n */\n leadKind?: \"patient\" | \"sales\";\n turnstileToken?: string;\n /**\n * 표준 필드 외에 폼이 받은 임의 항목(key→value)을 있는 그대로 전달. intake 가\n * 암호화 보관하고 CRM 상세에서 노출한다. 예약 키(표준 필드)·`_` 접두 키는 무시.\n * 주의: 개인정보가 담길 수 있으므로 폼의 동의 고지에 수집 항목을 반영할 것.\n */\n extras?: Record<string, string>;\n /**\n * 유입 first-touch 어트리뷰션 (ADR-0048 §2, 선택). 브라우저에서\n * `readAttribution()`(`@roottale/cms-client/attribution`)으로 읽어 Server\n * Action 인자로 넘긴 값을 그대로 전달하면 된다. CRM 에 \"이 문의가 어느\n * 글/링크/QR 에서 왔는지\"가 표시된다. null/undefined 면 미동반 제출.\n */\n attribution?: InquiryAttribution | null;\n /**\n * 방문 여정(journey) JSON 원문(설계 v2 §3, 선택) — `@roottale/analytics-runtime`\n * `readJourney()` 로 읽은 sessionStorage `_rt_journey` 링버퍼를 그대로\n * `JSON.stringify` 한 문자열. 파싱·검증은 서버(zod, medical/legal 마일스톤\n * 게이트 재강제)가 한다 — 여기서는 원문 그대로 `attr_journey` FormData 로\n * 전달만. null/undefined/빈 문자열이면 미동반 제출.\n */\n journey?: string;\n}\n\n/** submitInquiry 가 전용으로 매핑하는 키 — extras 에서 충돌 시 무시. */\nconst RESERVED_INQUIRY_KEYS = new Set([\n \"vertical\",\n \"contact_name\",\n \"business_name\",\n \"email\",\n \"phone\",\n \"message\",\n \"consultation_field\",\n \"current_site_url\",\n \"privacy_consent\",\n \"overseas_transfer_consent\",\n \"lead_kind\",\n \"cf-turnstile-response\",\n]);\n\nexport interface SubmitInquiryOptions {\n /** `rtlk_cust_*` API key — `fetchPosts` 와 동일한 키. Required. */\n apiKey: string;\n /** API base URL 오버라이드. 기본 `https://api.roottale.com`. */\n baseUrl?: string;\n fields: SubmitInquiryFields;\n signal?: AbortSignal;\n}\n\nexport type SubmitInquiryResult =\n | { ok: true }\n | { ok: false; code: string | null; message: string };\n\n/** `err` 코드를 사용자 메시지로 (미지의 코드는 generic). */\nexport function inquiryErrorMessage(code: string | null | undefined): string {\n return (code && INQUIRY_ERROR_MESSAGES[code]) || INQUIRY_ERROR_MESSAGES.internal;\n}\n\n/**\n * intake API 의 302 Location (`?ok=1` / `?err=*`) 을 구조화 결과로 파싱.\n * 외부 사이트가 ERROR_MESSAGES·파싱을 각자 재구현하지 않도록 공유.\n */\nexport function parseInquiryRedirect(\n location: string | null,\n base: string = DEFAULT_BASE_URL,\n): SubmitInquiryResult {\n if (!location) {\n return { ok: false, code: null, message: inquiryErrorMessage(null) };\n }\n let url: URL;\n try {\n url = new URL(location, base);\n } catch {\n return { ok: false, code: null, message: inquiryErrorMessage(null) };\n }\n if (url.searchParams.get(\"ok\") === \"1\") return { ok: true };\n const code = url.searchParams.get(\"err\");\n return { ok: false, code, message: inquiryErrorMessage(code) };\n}\n\nexport function formatKoreanPhoneNumber(input: string): string {\n let digits = input.replace(/\\D/g, \"\");\n if (digits.startsWith(\"82\") && digits.length >= 11) {\n digits = `0${digits.slice(2)}`;\n }\n if (digits.length === 11) {\n return `${digits.slice(0, 3)}-${digits.slice(3, 7)}-${digits.slice(7)}`;\n }\n if (digits.length === 10) {\n if (digits.startsWith(\"02\")) {\n return `${digits.slice(0, 2)}-${digits.slice(2, 6)}-${digits.slice(6)}`;\n }\n return `${digits.slice(0, 3)}-${digits.slice(3, 6)}-${digits.slice(6)}`;\n }\n if (digits.length === 8) {\n return `${digits.slice(0, 4)}-${digits.slice(4)}`;\n }\n return input.trim();\n}\n\n/**\n * 리드를 RootTale intake API 로 제출 (server-to-server, Bearer 인증).\n * 키의 tenant 로 라우팅되어 admin(admin.roottale.com) /crm 에서 관리된다.\n */\nexport async function submitInquiry(\n options: SubmitInquiryOptions,\n): Promise<SubmitInquiryResult> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const f = options.fields;\n\n const body = new FormData();\n body.set(\"vertical\", f.vertical);\n body.set(\"contact_name\", f.contactName);\n body.set(\"business_name\", f.businessName);\n body.set(\"email\", f.email);\n body.set(\"phone\", formatKoreanPhoneNumber(f.phone));\n if (f.message) body.set(\"message\", f.message);\n if (f.consultationField) body.set(\"consultation_field\", f.consultationField);\n if (f.currentSiteUrl) body.set(\"current_site_url\", f.currentSiteUrl);\n // 사용자가 명시 동의한 경우에만 전달 (자동 부여 금지).\n if (f.privacyConsent) body.set(\"privacy_consent\", \"on\");\n if (f.overseasTransferConsent) body.set(\"overseas_transfer_consent\", \"on\");\n if (f.leadKind) body.set(\"lead_kind\", f.leadKind);\n if (f.turnstileToken) body.set(\"cf-turnstile-response\", f.turnstileToken);\n // 유입 어트리뷰션 → intake `attr_*` 필드 (허용 키만, 빈 값 생략).\n if (f.attribution) {\n for (const [key, value] of Object.entries(f.attribution)) {\n // ad_click(설계 v2 §2) 은 boolean \"존재 플래그\" — true 일 때만 \"1\" 로\n // 전송(false/미설정은 신호 없음과 동일하므로 생략).\n if (typeof value === \"boolean\") {\n if (value) body.set(`attr_${key}`, \"1\");\n continue;\n }\n if (typeof value !== \"string\" || !value.trim()) continue;\n body.set(`attr_${key}`, value.trim());\n }\n }\n // 방문 여정(설계 v2 §3) — attribution 과 별도 채널(`attr_journey`). 파싱은\n // 서버(zod)가 전담 — 여기서는 원문 그대로 전달.\n if (f.journey && f.journey.trim()) {\n body.set(\"attr_journey\", f.journey.trim());\n }\n // 표준 외 임의 항목 — 예약 키/`_`접두/빈 값/중복은 건너뛰고 원본 그대로 전달.\n if (f.extras) {\n for (const [rawKey, rawValue] of Object.entries(f.extras)) {\n const key = rawKey.trim();\n const value = String(rawValue ?? \"\").trim();\n if (!key || !value) continue;\n if (key.startsWith(\"_\") || RESERVED_INQUIRY_KEYS.has(key)) continue;\n if (key.startsWith(\"attr_\")) continue; // 어트리뷰션 채널과 충돌 방지\n\n if (body.has(key)) continue;\n body.set(key, value);\n }\n }\n\n let response: Response;\n try {\n response = await fetch(`${baseUrl}/v1/public/inquiries`, {\n method: \"POST\",\n headers: { authorization: `Bearer ${options.apiKey}` },\n body,\n // intake 는 302 로 결과 신호 — 자동 추적 금지하고 Location 직접 읽음.\n redirect: \"manual\",\n signal: options.signal,\n });\n } catch {\n return { ok: false, code: \"internal\", message: inquiryErrorMessage(\"internal\") };\n }\n\n // 잘못된/만료된 키 → 라우트가 401.\n if (response.status === 401) {\n return {\n ok: false,\n code: \"invalid_api_key\",\n message: inquiryErrorMessage(\"invalid_api_key\"),\n };\n }\n if (response.status === 429) {\n return {\n ok: false,\n code: \"rate_limited\",\n message: inquiryErrorMessage(\"rate_limited\"),\n };\n }\n\n const location = response.headers.get(\"location\");\n if (location) return parseInquiryRedirect(location, baseUrl);\n\n // redirect 없이 성공 응답이면 ok 로 간주, 그 외엔 generic 실패.\n if (response.ok) return { ok: true };\n return { ok: false, code: \"internal\", message: inquiryErrorMessage(\"internal\") };\n}\n\nfunction assertServer(): void {\n if (typeof window !== \"undefined\") {\n throw new Error(\n \"@roottale/cms-client submitInquiry must not run in the browser. \" +\n \"Call it from a Server Action / Route Handler. \" +\n \"Shipping the API key to the browser breaks the security model (ADR-0023 §5.1 #15).\",\n );\n }\n}\n"],"mappings":"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","names":["DEFAULT_BASE_URL","assertServer"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@roottale/cms-client",
3
- "version": "0.69.0",
3
+ "version": "0.70.1",
4
4
  "type": "module",
5
5
  "description": "RootTale CMS Public API server-side fetch client (Bearer rtlk_cust_* auth). SSR-only — refuses to run in the browser to prevent key leak (ADR-0023 §5.1 #15). Pairs with @roottale/cms-renderer-next.",
6
6
  "main": "./dist/server.js",
@@ -30,7 +30,7 @@
30
30
  ],
31
31
  "dependencies": {
32
32
  "jose": "^5.10.0",
33
- "@roottale/cms-core": "^0.69.0",
33
+ "@roottale/cms-core": "^0.70.1",
34
34
  "@roottale/inquiry-forms": "^0.2.0"
35
35
  },
36
36
  "devDependencies": {