@roottale/cms-client 0.41.0 → 0.42.0

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,31 @@
1
1
  # @roottale/cms-client
2
2
 
3
+ ## 0.42.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 9699c3f: `fetchPosts()`(cms-client)에 `collectionKey?`/`excludeCollections?` 옵션 추가 — 공개 posts API(`GET /v1/cms/public/posts`)의 새 `collection_key=`/`exclude_collections=true` 파라미터를 실어 보낸다. 서버가 페이지네이션 `LIMIT` 전에 스트림 글을 걸러내므로, 아카이브 스트림(증상/질환/치료 등)이 있는 사이트의 `/blog` 목록에서 `limit` 슬롯이 스트림 글에 낭비되던 문제(블로그 글 3개만 남는 왜곡)가 해소된다.
8
+
9
+ `RootTaleBlogList`(cms-renderer-next)가 `excludeCollections`+`collections` 지정 시 이 서버 파라미터를 함께 전달하도록 갱신 — 기존 클라이언트 측 `postsNotInAnyCollection` 필터는 방어선으로 그대로 유지한다(동작 diff 0, `blog-list-exclude-collections.md` 후속).
10
+
11
+ - 68df1be: `jsonLdJson(schema)`(cms-client) — script 태그 없이 XSS-safe(`</script>` 주입 방지) JSON-LD 문자열만 주는 헬퍼. React 소비자가 `<script type="application/ld+json" dangerouslySetInnerHTML>` 로 직접 방출할 때 쓴다(escape 단일 소스). `jsonLdScript` 는 이 헬퍼를 감싼다. cms-renderer-next 의 FAQ JSON-LD 도 동일 헬퍼로 통일(동작 불변).
12
+ - 7429d10: ADR-0052 A안(콘텐츠 다국어) W4-6 PR B — `fetchPosts()`/`fetchPost()`(cms-client)에 `locale?` 옵션 추가. 지정 시 그 BCP-47 locale 로 필터, 미지정 시 서버가 사이트 기본 locale(`settingsJson.locales[0]`, 미설정이면 'ko')만 반환한다(하위호환 — 번역판이 생겨도 기존 호출부는 원어 콘텐츠만 계속 본다). 응답 `CmsPostContent`에 `locale?`/`translations?`(같은 글의 발행된 언어판 목록, 이 글 자신 포함) 필드 추가 — 둘 다 구 서버 대비 optional.
13
+
14
+ `createSitemapIndex()`(cms-renderer-next)의 `localizeEntries` 가 fail-closed 로 바뀜: admin `locales` 설정에 값이 있어도, 그 locale 로 발행된 글이 실제로 0건이면 hreflang alternates 생성 대상에서 제외한다(공개 API/공용 함수 시그니처 변경 없음 — 존재하지 않는 `/{locale}/...` URL 을 검색엔진에 알리던 버그 수정).
15
+
16
+ ### Patch Changes
17
+
18
+ - d6313d2: ADR-0052 A안(콘텐츠 다국어) W4-6 PR C1 — 렌더러 라우팅 + hreflang.
19
+ - `PostMetadata.alternates` 타입 확장: `{ canonical, languages?: Record<string,string> }`. `buildPostMetadata()` 가 새 `translations`(글 응답)·`locales`(defaultLocale/pathFor) 옵션을 받으면 hreflang `alternates.languages` 를 계산한다 — 양방향 + `x-default` = 기본 locale(자기 자신 포함). 신규 export `buildHreflangLanguages()` 를 별도로도 쓸 수 있다(OG `type:"article"` 이 안 맞는 CMS 정적 페이지 등).
20
+ - `createSitemapIndex()` 의 `blog` 섹션이 글 단위 실제 `translations[]` 로 hreflang alternates 를 계산한다(slug 1:1 path-prefix 가정 제거) — 언어판 없는 글은 alternates 생략. `static`/`categories`/`authors` 섹션은 v1 라우트 범위 밖(locale 라우트가 실제로 없음)이라 alternates 를 붙이지 않는다(기존 slug-1:1 스캐폴딩 제거 — breaking for consumers relying on that scaffolding, 그래서 minor).
21
+ - `@roottale/cms-client`: `fetchPageBySlug()` 에 `locale?` 옵션 추가(기존 `fetchPosts`/`fetchPost` 와 동일 시맨틱). `server.ts` 배럴이 `CmsPostTranslationRef` 를 재노출하지 않던 PR B 누락도 함께 수정.
22
+
23
+ ## 0.41.1
24
+
25
+ ### Patch Changes
26
+
27
+ - 3810879: Add Turnstile-aware inquiry submission fields, rate-limit error handling, and docs for protected lead intake.
28
+
3
29
  ## 0.41.0
4
30
 
5
31
  ### Minor Changes
package/dist/server.d.ts CHANGED
@@ -40,6 +40,25 @@ interface CmsArchiveRelatedGroup {
40
40
  label: string;
41
41
  items: CmsArchiveRelatedPost[];
42
42
  }
43
+ /** W5-2 — `fields` 표시 메타 1건 (서버 계약 `CmsFieldMeta` 대응). */
44
+ interface CmsFieldMetaEntry {
45
+ /** `fields` 의 key. */
46
+ name: string;
47
+ /** 필드 그룹에 정의된 사람용 라벨. */
48
+ label: string;
49
+ /** 필드 타입 (text/textarea/select/relationship/…). */
50
+ type: string;
51
+ /** select/radio 계열만 — 저장 값(value)→표시 라벨. */
52
+ choices?: Array<{
53
+ value: string;
54
+ label: string;
55
+ }>;
56
+ }
57
+ /** ADR-0052 A안 (W4-6 PR B) — 언어판 참조(locale + 그 언어판의 slug). */
58
+ interface CmsPostTranslationRef {
59
+ locale: string;
60
+ slug: string;
61
+ }
43
62
  interface CmsPostContent {
44
63
  /** UUIDv7. */
45
64
  id: string;
@@ -56,6 +75,16 @@ interface CmsPostContent {
56
75
  collectionKey: string | null;
57
76
  /** Lowercase kebab-case slug (1-120 chars). */
58
77
  slug: string;
78
+ /**
79
+ * ADR-0052 A안 — 이 글판의 BCP-47 locale. 단일 언어 사이트는 항상 'ko'.
80
+ * 구 서버(마이그레이션 전) 는 미포함 — optional (하위호환).
81
+ */
82
+ locale?: string;
83
+ /**
84
+ * ADR-0052 A안 — 같은 글의 발행된 언어판 목록(이 글 자신 포함). 번역이 없으면
85
+ * 자기 자신 1건만. 구 서버는 미포함 — optional (하위호환).
86
+ */
87
+ translations?: CmsPostTranslationRef[];
59
88
  title: string;
60
89
  excerpt: string | null;
61
90
  /** Media id (resolved by renderer / consumer to URL). */
@@ -84,6 +113,12 @@ interface CmsPostContent {
84
113
  * 해석된 그룹이 없거나 구 서버면 null.
85
114
  */
86
115
  fields: Record<string, unknown> | null;
116
+ /**
117
+ * W5-2 — `fields` 의 표시 메타(라벨·타입·choices), 필드 그룹 정의 순서.
118
+ * 서버가 필드 정의로 계산해 동봉 — 렌더러가 vertical 지식 없이 양식 값을
119
+ * 라벨과 함께 표시한다. 해석된 그룹 없거나 구 서버면 빈 배열.
120
+ */
121
+ fieldsMeta: CmsFieldMetaEntry[];
87
122
  /**
88
123
  * Attached taxonomy terms (categories + tags + custom). Server 가 category →
89
124
  * tag → custom 순서로 정렬해 보내며, 각 taxonomy 내부는 name asc. attach 가 0 개면
@@ -125,6 +160,23 @@ interface FetchPostsOptions {
125
160
  author?: string;
126
161
  /** Override the auto-resolved site id (Phase 1 = 1 site/tenant, usually omitted). */
127
162
  siteId?: string;
163
+ /**
164
+ * ADR-0060 — 특정 섹션(collection) 소속 글만(포지티브 스코프). `excludeCollections`
165
+ * 와 동시 지정 불가(서버가 400).
166
+ */
167
+ collectionKey?: string;
168
+ /**
169
+ * 네거티브 스코프 — 어느 스트림에도 안 속한("미소속") 글만
170
+ * (`blog-list-collection-scope.md` §2②). `collectionKey` 와 동시 지정 불가.
171
+ */
172
+ excludeCollections?: boolean;
173
+ /**
174
+ * ADR-0052 A안 (W4-6 PR B) — BCP-47 소문자 locale 필터(예: "en"). 미지정 시
175
+ * 서버가 사이트 기본 locale(`settingsJson.locales[0]`, 미설정이면 'ko') 만
176
+ * 반환한다 — 하위호환(번역판이 생겨도 이 옵션을 안 쓰는 기존 호출부는 원어
177
+ * 콘텐츠만 계속 받는다).
178
+ */
179
+ locale?: string;
128
180
  /** Optional AbortSignal. */
129
181
  signal?: AbortSignal;
130
182
  /** ISR revalidate 초(opt-in, W0-9). 미지정 시 현행 동작 유지. */
@@ -137,6 +189,8 @@ interface FetchPostOptions {
137
189
  slugOrId: string;
138
190
  /** Override the auto-resolved site id. */
139
191
  siteId?: string;
192
+ /** ADR-0052 A안 — `FetchPostsOptions.locale` 과 동일 시맨틱. */
193
+ locale?: string;
140
194
  signal?: AbortSignal;
141
195
  /** ISR revalidate 초(opt-in, W0-9). 미지정 시 현행 동작 유지. */
142
196
  revalidate?: number;
@@ -149,7 +203,7 @@ declare class CmsApiError extends Error {
149
203
 
150
204
  /**
151
205
  * 외부 사이트가 cms-renderer 에 주입할 RootTale 디자인 토큰.
152
- * admin /design (mysite.roottale.com) 에서 저장한 값과 동일한 shape.
206
+ * admin /design (admin.roottale.com) 에서 저장한 값과 동일한 shape.
153
207
  * 미설정 필드는 응답에서 누락 → renderer 의 CSS fallback 이 사용된다.
154
208
  */
155
209
  interface RootTaleTheme {
@@ -186,6 +240,64 @@ interface RootTaleTheme {
186
240
  * 반영하는 데 쓴다. 미설정이면 소비자가 site.config.ts 값으로 fallback.
187
241
  */
188
242
  siteName?: string;
243
+ announcement?: AnnouncementSettings;
244
+ actionDock?: ActionDockSettings;
245
+ inquiryBoard?: InquiryBoardSettings;
246
+ siteNav?: SiteNavSettings;
247
+ }
248
+ type AnnouncementVariant = "single" | "rotating" | "cta";
249
+ type AnnouncementTone = "default" | "primary";
250
+ interface AnnouncementItem {
251
+ text: string;
252
+ href?: string;
253
+ ctaLabel?: string;
254
+ }
255
+ interface AnnouncementSettings {
256
+ enabled: boolean;
257
+ variant: AnnouncementVariant;
258
+ tone: AnnouncementTone;
259
+ items: AnnouncementItem[];
260
+ startsAt?: string;
261
+ endsAt?: string;
262
+ }
263
+ type ActionDockKind = "tel" | "link" | "map" | "channel";
264
+ type ActionDockPosition = "bottom" | "side";
265
+ interface ActionDockAction {
266
+ label: string;
267
+ href: string;
268
+ kind: ActionDockKind;
269
+ }
270
+ interface ActionDockSettings {
271
+ enabled: boolean;
272
+ actions: ActionDockAction[];
273
+ position: ActionDockPosition;
274
+ collapseAt?: number;
275
+ }
276
+ interface InquiryBoardSettings {
277
+ enabled: boolean;
278
+ }
279
+ interface SiteNavLink {
280
+ label: string;
281
+ href: string;
282
+ description?: string;
283
+ }
284
+ interface SiteNavColumn {
285
+ title?: string;
286
+ links: SiteNavLink[];
287
+ }
288
+ interface SiteNavGroup {
289
+ label: string;
290
+ href?: string;
291
+ columns?: SiteNavColumn[];
292
+ }
293
+ interface SiteNavCta {
294
+ label: string;
295
+ href: string;
296
+ }
297
+ interface SiteNavSettings {
298
+ navGroups: SiteNavGroup[];
299
+ cta?: SiteNavCta;
300
+ utilityLinks?: SiteNavLink[];
189
301
  }
190
302
  /**
191
303
  * 테마 fetch 캐시 tag 의 정본 이름 (W0-4). starter layout 의 `fetchTheme` 호출에
@@ -248,6 +360,8 @@ interface FetchPageBySlugOptions {
248
360
  readonly baseUrl?: string;
249
361
  readonly slug: string;
250
362
  readonly siteId?: string;
363
+ /** ADR-0052 A안 (W4-6 PR C1) — `FetchPostsOptions.locale` 과 동일 시맨틱. */
364
+ readonly locale?: string;
251
365
  readonly signal?: AbortSignal;
252
366
  readonly revalidate?: number;
253
367
  }
@@ -642,6 +756,77 @@ interface FetchRedirectsOptions {
642
756
  */
643
757
  declare function fetchRedirects(options: FetchRedirectsOptions): Promise<RootTaleRedirect[]>;
644
758
 
759
+ type SiteInquiryStatus = "pending" | "answered";
760
+ interface SiteInquiryListItem {
761
+ id: string;
762
+ title: string;
763
+ category: string | null;
764
+ authorMasked: string | null;
765
+ status: SiteInquiryStatus | null;
766
+ isSecret: boolean;
767
+ locked: boolean;
768
+ createdAt: string | null;
769
+ }
770
+ interface SiteInquiryListPage {
771
+ items: SiteInquiryListItem[];
772
+ hasMore: boolean;
773
+ nextCursor: string | null;
774
+ }
775
+ interface SiteInquiryDetail {
776
+ id: string;
777
+ title: string;
778
+ category: string | null;
779
+ authorMasked: string;
780
+ status: SiteInquiryStatus;
781
+ isSecret: boolean;
782
+ body: string;
783
+ answerBody: string | null;
784
+ answeredAt: string | null;
785
+ createdAt: string;
786
+ }
787
+ interface FetchSiteInquiriesOptions {
788
+ apiKey: string;
789
+ baseUrl?: string;
790
+ siteId?: string;
791
+ limit?: number;
792
+ cursor?: string;
793
+ signal?: AbortSignal;
794
+ revalidate?: number;
795
+ }
796
+ interface FetchSiteInquiryOptions {
797
+ apiKey: string;
798
+ baseUrl?: string;
799
+ siteId?: string;
800
+ id: string;
801
+ password?: string;
802
+ signal?: AbortSignal;
803
+ revalidate?: number;
804
+ }
805
+ interface SubmitSiteInquiryFields {
806
+ siteId?: string;
807
+ category?: string;
808
+ title: string;
809
+ body: string;
810
+ authorName: string;
811
+ contact?: string;
812
+ isSecret: boolean;
813
+ secretPassword?: string;
814
+ privacyConsent: boolean;
815
+ }
816
+ interface SubmitSiteInquiryOptions {
817
+ apiKey: string;
818
+ baseUrl?: string;
819
+ fields: SubmitSiteInquiryFields;
820
+ signal?: AbortSignal;
821
+ }
822
+ interface SubmitSiteInquiryResult {
823
+ id: string;
824
+ status: SiteInquiryStatus;
825
+ }
826
+ declare function fetchSiteInquiries(options: FetchSiteInquiriesOptions): Promise<SiteInquiryListPage>;
827
+ declare function fetchSiteInquiry(options: FetchSiteInquiryOptions): Promise<SiteInquiryDetail | null>;
828
+ declare function submitSiteInquiry(options: SubmitSiteInquiryOptions): Promise<SubmitSiteInquiryResult>;
829
+
645
830
  interface RssItemInput {
646
831
  title: string;
647
832
  /** 글 절대 URL. */
@@ -761,6 +946,12 @@ declare function faqSchema(qa: {
761
946
  * <head>{contentIdMeta(post.id)}</head>
762
947
  */
763
948
  declare function contentIdMeta(postId: string): string;
949
+ /**
950
+ * JSON-LD 객체 → XSS-safe JSON *문자열* (script 태그 없음). React 소비자가
951
+ * `<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: ... }} />`
952
+ * 로 직접 방출할 때 사용 — `</script>` 주입 방지 escape 의 단일 소스.
953
+ */
954
+ declare function jsonLdJson(schema: JsonLd | JsonLd[]): string;
764
955
  /** JSON-LD 객체 → `<script type="application/ld+json">` HTML 문자열 (XSS-safe). */
765
956
  declare function jsonLdScript(schema: JsonLd | JsonLd[]): string;
766
957
 
@@ -820,6 +1011,7 @@ interface SubmitInquiryFields {
820
1011
  * sales 는 medical 국외이전 동의 게이트 면제.
821
1012
  */
822
1013
  leadKind?: "patient" | "sales";
1014
+ turnstileToken?: string;
823
1015
  /**
824
1016
  * 표준 필드 외에 폼이 받은 임의 항목(key→value)을 있는 그대로 전달. intake 가
825
1017
  * 암호화 보관하고 CRM 상세에서 노출한다. 예약 키(표준 필드)·`_` 접두 키는 무시.
@@ -859,8 +1051,8 @@ declare function parseInquiryRedirect(location: string | null, base?: string): S
859
1051
  declare function formatKoreanPhoneNumber(input: string): string;
860
1052
  /**
861
1053
  * 리드를 RootTale intake API 로 제출 (server-to-server, Bearer 인증).
862
- * 키의 tenant 로 라우팅되어 admin(mysite.roottale.com) /crm 에서 관리된다.
1054
+ * 키의 tenant 로 라우팅되어 admin(admin.roottale.com) /crm 에서 관리된다.
863
1055
  */
864
1056
  declare function submitInquiry(options: SubmitInquiryOptions): Promise<SubmitInquiryResult>;
865
1057
 
866
- export { type AnalyticsTagConfig, type AnalyticsTagProvider, type ArticleSchemaInput, type BusinessDayCode, type BusinessOpeningHours, type BusinessType, CMS_CLIENT_VERSION, CmsApiError, type CmsArchiveRelatedGroup, type CmsArchiveRelatedPost, type CmsPostContent, type CmsPostContentPage, type CmsPostType, type CmsRouteCollection, type CmsSearchHit, type CmsTaxonomyKind, type CmsTermRef, DEFAULT_BLOG_SETTINGS, type FetchAnalyticsOptions, type FetchAuthorsOptions, type FetchBlogSettingsOptions, type FetchBusinessProfileOptions, type FetchCollectionsOptions, type FetchMenuOptions, type FetchMenusOptions, type FetchPageBySlugOptions, type FetchPostOptions, type FetchPostsOptions, type FetchRedirectsOptions, type FetchThemeOptions, INQUIRY_ERROR_MESSAGES, InquiryAttribution, type InquiryVertical, type LocalBusinessProfileInput, type ResolvedPostDisplay, type RootTaleAuthor, type RootTaleBlogPostCta, type RootTaleBlogSettings, type RootTaleBusinessProfile, type RootTaleMenu, type RootTaleMenuItem, type RootTaleRedirect, type RootTaleSiteProfile, type RootTaleSitemapSettings, type RootTaleTheme, type RssFeedInput, type RssItemInput, type SearchPostsOptions, type SiteAnalyticsTags, type SubmitInquiryFields, type SubmitInquiryOptions, type SubmitInquiryResult, THEME_CACHE_TAG, articleSchema, breadcrumbSchema, businessProfileFromWire, contentIdMeta, faqSchema, fetchAnalyticsConfig, fetchAuthors, fetchBlogSettings, fetchBusinessProfile, fetchCollections, fetchCollectionsSafeDefault, fetchMenu, fetchMenus, fetchPageBySlug, fetchPost, fetchPosts, fetchRedirects, fetchTheme, formatKoreanPhoneNumber, generateRssXml, inquiryErrorMessage, jsonLdScript, localBusinessSchema, organizationSchema, parseInquiryRedirect, resolvePostDisplay, rssItemsFromPosts, searchPosts, selectRelatedPosts, submitInquiry, websiteSchema };
1058
+ export { type ActionDockAction, type ActionDockKind, type ActionDockPosition, type ActionDockSettings, type AnalyticsTagConfig, type AnalyticsTagProvider, type AnnouncementItem, type AnnouncementSettings, type AnnouncementTone, type AnnouncementVariant, type ArticleSchemaInput, type BusinessDayCode, type BusinessOpeningHours, type BusinessType, CMS_CLIENT_VERSION, CmsApiError, type CmsArchiveRelatedGroup, type CmsArchiveRelatedPost, type CmsFieldMetaEntry, type CmsPostContent, type CmsPostContentPage, type CmsPostTranslationRef, type CmsPostType, type CmsRouteCollection, type CmsSearchHit, type CmsTaxonomyKind, type CmsTermRef, DEFAULT_BLOG_SETTINGS, type FetchAnalyticsOptions, type FetchAuthorsOptions, type FetchBlogSettingsOptions, type FetchBusinessProfileOptions, type FetchCollectionsOptions, type FetchMenuOptions, type FetchMenusOptions, type FetchPageBySlugOptions, type FetchPostOptions, type FetchPostsOptions, type FetchRedirectsOptions, type FetchSiteInquiriesOptions, type FetchSiteInquiryOptions, type FetchThemeOptions, INQUIRY_ERROR_MESSAGES, InquiryAttribution, type InquiryBoardSettings, type InquiryVertical, type LocalBusinessProfileInput, type ResolvedPostDisplay, type RootTaleAuthor, type RootTaleBlogPostCta, type RootTaleBlogSettings, type RootTaleBusinessProfile, type RootTaleMenu, type RootTaleMenuItem, type RootTaleRedirect, type RootTaleSiteProfile, type RootTaleSitemapSettings, type RootTaleTheme, type RssFeedInput, type RssItemInput, type SearchPostsOptions, type SiteAnalyticsTags, type SiteInquiryDetail, type SiteInquiryListItem, type SiteInquiryListPage, type SiteInquiryStatus, type SiteNavColumn, type SiteNavCta, type SiteNavGroup, type SiteNavLink, type SiteNavSettings, type SubmitInquiryFields, type SubmitInquiryOptions, type SubmitInquiryResult, type SubmitSiteInquiryFields, type SubmitSiteInquiryOptions, type SubmitSiteInquiryResult, THEME_CACHE_TAG, articleSchema, breadcrumbSchema, businessProfileFromWire, contentIdMeta, faqSchema, fetchAnalyticsConfig, fetchAuthors, fetchBlogSettings, fetchBusinessProfile, fetchCollections, fetchCollectionsSafeDefault, fetchMenu, fetchMenus, fetchPageBySlug, fetchPost, fetchPosts, fetchRedirects, fetchSiteInquiries, fetchSiteInquiry, fetchTheme, formatKoreanPhoneNumber, generateRssXml, inquiryErrorMessage, jsonLdJson, jsonLdScript, localBusinessSchema, organizationSchema, parseInquiryRedirect, resolvePostDisplay, rssItemsFromPosts, searchPosts, selectRelatedPosts, submitInquiry, submitSiteInquiry, websiteSchema };
package/dist/server.js CHANGED
@@ -4,7 +4,7 @@ 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.41.0" : "dev";
7
+ var CLIENT_VERSION = true ? "0.42.0" : "dev";
8
8
  var CMS_CLIENT_VERSION = CLIENT_VERSION;
9
9
  function authHeaders(apiKey) {
10
10
  return {
@@ -74,6 +74,10 @@ async function fetchTheme(options) {
74
74
  if (json.fonts) theme.fonts = json.fonts;
75
75
  if (json.radius) theme.radius = json.radius;
76
76
  if (json.site_name) theme.siteName = json.site_name;
77
+ if (json.announcement) theme.announcement = json.announcement;
78
+ if (json.actionDock) theme.actionDock = json.actionDock;
79
+ if (json.inquiryBoard) theme.inquiryBoard = json.inquiryBoard;
80
+ if (json.siteNav) theme.siteNav = json.siteNav;
77
81
  return theme;
78
82
  }
79
83
  async function fetchAnalyticsConfig(options) {
@@ -99,6 +103,9 @@ async function fetchPosts(options) {
99
103
  if (options.type) params.set("type", options.type);
100
104
  if (options.author) params.set("author", options.author);
101
105
  if (options.siteId) params.set("site_id", options.siteId);
106
+ if (options.collectionKey) params.set("collection_key", options.collectionKey);
107
+ if (options.excludeCollections) params.set("exclude_collections", "true");
108
+ if (options.locale) params.set("locale", options.locale);
102
109
  const qs = params.toString();
103
110
  const url = `${baseUrl}/v1/cms/public/posts${qs ? `?${qs}` : ""}`;
104
111
  const response = await fetch(url, buildGetInit(options.apiKey, options));
@@ -117,6 +124,7 @@ async function fetchPost(options) {
117
124
  const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
118
125
  const params = new URLSearchParams();
119
126
  if (options.siteId) params.set("site_id", options.siteId);
127
+ if (options.locale) params.set("locale", options.locale);
120
128
  const qs = params.toString();
121
129
  const url = `${baseUrl}/v1/cms/public/posts/${encodeURIComponent(options.slugOrId)}${qs ? `?${qs}` : ""}`;
122
130
  const response = await fetch(url, buildGetInit(options.apiKey, options));
@@ -137,6 +145,7 @@ async function fetchPageBySlug(options) {
137
145
  ...cursor ? { cursor } : {},
138
146
  type: "page",
139
147
  siteId: options.siteId,
148
+ ...options.locale ? { locale: options.locale } : {},
140
149
  signal: options.signal,
141
150
  revalidate: options.revalidate
142
151
  });
@@ -154,6 +163,8 @@ function fromWire(wire) {
154
163
  type: wire.type,
155
164
  collectionKey: wire.collection_key ?? null,
156
165
  slug: wire.slug,
166
+ ...wire.locale !== void 0 ? { locale: wire.locale } : {},
167
+ ...wire.translations !== void 0 ? { translations: wire.translations } : {},
157
168
  title: wire.title,
158
169
  excerpt: wire.excerpt,
159
170
  featuredMediaId: wire.featured_media_id,
@@ -166,6 +177,17 @@ function fromWire(wire) {
166
177
  status: wire.status,
167
178
  metaJson: wire.meta_json,
168
179
  fields: wire.fields ?? null,
180
+ fieldsMeta: (wire.fields_meta ?? []).map((entry) => ({
181
+ name: entry.name,
182
+ label: entry.label,
183
+ type: entry.type,
184
+ ...entry.choices ? {
185
+ choices: entry.choices.map((c) => ({
186
+ value: c.value,
187
+ label: c.label
188
+ }))
189
+ } : {}
190
+ })),
169
191
  terms: wire.terms ?? [],
170
192
  archiveRelated: (wire.archive_related ?? []).map((group) => ({
171
193
  collectionKey: group.collection_key,
@@ -509,6 +531,91 @@ async function fetchRedirects(options) {
509
531
  }));
510
532
  }
511
533
 
534
+ // src/server-site-inquiries.ts
535
+ async function fetchSiteInquiries(options) {
536
+ assertServer();
537
+ const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
538
+ const params = new URLSearchParams();
539
+ if (options.limit !== void 0) params.set("limit", String(options.limit));
540
+ if (options.cursor) params.set("cursor", options.cursor);
541
+ if (options.siteId) params.set("site_id", options.siteId);
542
+ const qs = params.toString();
543
+ const url = `${baseUrl}/v1/cms/public/inquiries${qs ? `?${qs}` : ""}`;
544
+ const response = await fetch(url, buildGetInit(options.apiKey, options));
545
+ if (!response.ok) throw await toApiError(response);
546
+ const json = await response.json();
547
+ return {
548
+ items: json.items.map(fromListWire),
549
+ hasMore: json.has_more,
550
+ nextCursor: json.next_cursor
551
+ };
552
+ }
553
+ async function fetchSiteInquiry(options) {
554
+ assertServer();
555
+ const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
556
+ const params = new URLSearchParams();
557
+ if (options.siteId) params.set("site_id", options.siteId);
558
+ if (options.password) params.set("password", options.password);
559
+ const qs = params.toString();
560
+ const url = `${baseUrl}/v1/cms/public/inquiries/${encodeURIComponent(options.id)}${qs ? `?${qs}` : ""}`;
561
+ const response = await fetch(url, buildGetInit(options.apiKey, options));
562
+ if (response.status === 404) return null;
563
+ if (!response.ok) throw await toApiError(response);
564
+ return fromDetailWire(await response.json());
565
+ }
566
+ async function submitSiteInquiry(options) {
567
+ assertServer();
568
+ const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
569
+ const fields = options.fields;
570
+ const response = await fetch(`${baseUrl}/v1/cms/public/inquiries`, {
571
+ method: "POST",
572
+ headers: {
573
+ ...authHeaders(options.apiKey),
574
+ "content-type": "application/json"
575
+ },
576
+ body: JSON.stringify({
577
+ site_id: fields.siteId,
578
+ category: fields.category,
579
+ title: fields.title,
580
+ body: fields.body,
581
+ author_name: fields.authorName,
582
+ contact: fields.contact,
583
+ is_secret: fields.isSecret,
584
+ secret_password: fields.secretPassword,
585
+ privacy_consent: fields.privacyConsent
586
+ }),
587
+ signal: options.signal
588
+ });
589
+ if (!response.ok) throw await toApiError(response);
590
+ return await response.json();
591
+ }
592
+ function fromListWire(item) {
593
+ return {
594
+ id: item.id,
595
+ title: item.title,
596
+ category: item.category,
597
+ authorMasked: item.author_masked,
598
+ status: item.status,
599
+ isSecret: item.is_secret,
600
+ locked: item.locked,
601
+ createdAt: item.created_at
602
+ };
603
+ }
604
+ function fromDetailWire(item) {
605
+ return {
606
+ id: item.id,
607
+ title: item.title,
608
+ category: item.category,
609
+ authorMasked: item.author_masked,
610
+ status: item.status,
611
+ isSecret: item.is_secret,
612
+ body: item.body,
613
+ answerBody: item.answer_body,
614
+ answeredAt: item.answered_at,
615
+ createdAt: item.created_at
616
+ };
617
+ }
618
+
512
619
  // src/seo.ts
513
620
  function generateRssXml(input) {
514
621
  const lang = input.language ?? "ko-KR";
@@ -697,9 +804,11 @@ function faqSchema(qa) {
697
804
  function contentIdMeta(postId) {
698
805
  return `<meta name="rt:content-id" content="${escapeXml(postId)}" />`;
699
806
  }
807
+ function jsonLdJson(schema) {
808
+ return JSON.stringify(schema).replace(/</g, "\\u003c");
809
+ }
700
810
  function jsonLdScript(schema) {
701
- const json = JSON.stringify(schema).replace(/</g, "\\u003c");
702
- return `<script type="application/ld+json">${json}</script>`;
811
+ return `<script type="application/ld+json">${jsonLdJson(schema)}</script>`;
703
812
  }
704
813
  function toIso(value) {
705
814
  if (!value) return null;
@@ -722,6 +831,8 @@ var INQUIRY_ERROR_MESSAGES = {
722
831
  missing_fields: "\uD544\uC218 \uD56D\uBAA9\uC774 \uBE44\uC5B4 \uC788\uC2B5\uB2C8\uB2E4.",
723
832
  invalid_email: "\uC774\uBA54\uC77C \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.",
724
833
  invalid_api_key: "\uC5F0\uB3D9 \uC778\uC99D\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4. API \uD0A4\uB97C \uD655\uC778\uD574\uC8FC\uC138\uC694.",
834
+ rate_limited: "\uC694\uCCAD\uC774 \uB108\uBB34 \uB9CE\uC2B5\uB2C8\uB2E4. \uC7A0\uC2DC \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD574\uC8FC\uC138\uC694.",
835
+ turnstile_failed: "\uBCF4\uC548 \uD655\uC778\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4. \uC0C8\uB85C\uACE0\uCE68 \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD574\uC8FC\uC138\uC694.",
725
836
  internal: "\uC804\uC1A1\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4. \uC7A0\uC2DC \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD574\uC8FC\uC138\uC694."
726
837
  };
727
838
  var RESERVED_INQUIRY_KEYS = /* @__PURE__ */ new Set([
@@ -735,7 +846,8 @@ var RESERVED_INQUIRY_KEYS = /* @__PURE__ */ new Set([
735
846
  "current_site_url",
736
847
  "privacy_consent",
737
848
  "overseas_transfer_consent",
738
- "lead_kind"
849
+ "lead_kind",
850
+ "cf-turnstile-response"
739
851
  ]);
740
852
  function inquiryErrorMessage(code) {
741
853
  return code && INQUIRY_ERROR_MESSAGES[code] || INQUIRY_ERROR_MESSAGES.internal;
@@ -789,6 +901,7 @@ async function submitInquiry(options) {
789
901
  if (f.privacyConsent) body.set("privacy_consent", "on");
790
902
  if (f.overseasTransferConsent) body.set("overseas_transfer_consent", "on");
791
903
  if (f.leadKind) body.set("lead_kind", f.leadKind);
904
+ if (f.turnstileToken) body.set("cf-turnstile-response", f.turnstileToken);
792
905
  if (f.attribution) {
793
906
  for (const [key, value] of Object.entries(f.attribution)) {
794
907
  if (typeof value !== "string" || !value.trim()) continue;
@@ -826,6 +939,13 @@ async function submitInquiry(options) {
826
939
  message: inquiryErrorMessage("invalid_api_key")
827
940
  };
828
941
  }
942
+ if (response.status === 429) {
943
+ return {
944
+ ok: false,
945
+ code: "rate_limited",
946
+ message: inquiryErrorMessage("rate_limited")
947
+ };
948
+ }
829
949
  const location = response.headers.get("location");
830
950
  if (location) return parseInquiryRedirect(location, baseUrl);
831
951
  if (response.ok) return { ok: true };
@@ -861,10 +981,13 @@ export {
861
981
  fetchPost,
862
982
  fetchPosts,
863
983
  fetchRedirects,
984
+ fetchSiteInquiries,
985
+ fetchSiteInquiry,
864
986
  fetchTheme,
865
987
  formatKoreanPhoneNumber,
866
988
  generateRssXml,
867
989
  inquiryErrorMessage,
990
+ jsonLdJson,
868
991
  jsonLdScript,
869
992
  localBusinessSchema,
870
993
  organizationSchema,
@@ -875,6 +998,7 @@ export {
875
998
  searchPosts,
876
999
  selectRelatedPosts,
877
1000
  submitInquiry,
1001
+ submitSiteInquiry,
878
1002
  websiteSchema
879
1003
  };
880
1004
  //# sourceMappingURL=server.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/server-base.ts","../src/server-posts-theme.ts","../src/server-blog.ts","../src/server-business.ts","../src/server-discovery.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}\n\nexport interface CmsArchiveRelatedPost {\n id: string;\n title: string;\n slug: string;\n excerpt: string | null;\n collectionKey: string;\n}\n\nexport interface CmsArchiveRelatedGroup {\n collectionKey: string;\n label: string;\n items: CmsArchiveRelatedPost[];\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 /** Lowercase kebab-case slug (1-120 chars). */\n slug: string;\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 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 /** Public biography text for the post author. */\n authorBio: string | null;\n /** Tiptap / Gutenberg-compatible Block JSON. Sanitization is renderer's responsibility. */\n bodyJson: Record<string, unknown>;\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 * 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 /** 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 /** Optional AbortSignal. */\n signal?: AbortSignal;\n /** ISR revalidate 초(opt-in, W0-9). 미지정 시 현행 동작 유지. */\n revalidate?: number;\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 signal?: AbortSignal;\n /** ISR revalidate 초(opt-in, W0-9). 미지정 시 현행 동작 유지. */\n revalidate?: number;\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 {\n type CmsPostType,\n type CmsPostContent,\n type CmsPostContentPage,\n type CmsTermRef,\n type FetchPostOptions,\n type FetchPostsOptions,\n DEFAULT_BASE_URL,\n assertServer,\n buildGetInit,\n toApiError,\n} from \"./server-base\";\n\n/**\n * 외부 사이트가 cms-renderer 에 주입할 RootTale 디자인 토큰.\n * admin /design (mysite.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}\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 };\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 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 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(fromWire),\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 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 fromWire(json);\n}\n\nexport interface FetchPageBySlugOptions {\n readonly apiKey: string;\n readonly baseUrl?: string;\n readonly slug: string;\n readonly siteId?: 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 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\ntype 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 slug: string;\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_name?: string | null;\n author_image_url?: string | null;\n author_bio?: string | null;\n body_json: Record<string, unknown>;\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?: CmsTermRef[];\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 /** ADR-0065 — 누적 조회수. 사이트가 공개 조회수를 켠 경우에만 포함. */\n view_count?: number;\n published_at: string;\n created_at: string;\n updated_at: string;\n};\n\nfunction fromWire(wire: CmsPostWire): CmsPostContent {\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 slug: wire.slug,\n title: wire.title,\n excerpt: wire.excerpt,\n featuredMediaId: wire.featured_media_id,\n featuredImageUrl: wire.featured_media_url ?? null,\n authorId: wire.author_id,\n authorName: wire.author_name ?? null,\n authorImageUrl: wire.author_image_url ?? null,\n authorBio: wire.author_bio ?? null,\n bodyJson: wire.body_json,\n status: wire.status,\n metaJson: wire.meta_json,\n fields: wire.fields ?? null,\n terms: wire.terms ?? [],\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 publishedAt: wire.published_at,\n createdAt: wire.created_at,\n updatedAt: wire.updated_at,\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/**\n * 글 하단 고정 CTA — admin 블로그 설정에서 활성화 시 모든 글 본문 끝에 그려진다.\n * null(미설정/비활성)이면 렌더러는 CTA 블록을 생략. buttonLabel/buttonHref 는\n * 객체가 존재하면 항상 non-empty(서버가 둘 다 있을 때만 객체를 내려줌).\n */\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 logoUrl: string | null;\n faviconUrl: string | null;\n /** 글별 og:image 미설정 시 폴백(1200×630 권장). */\n defaultOgImageUrl: string | null;\n}\n\n/**\n * 사이트맵 생성 정책 (admin 설정) — `createSitemapIndex` 가 소비해 무엇을 내보낼지\n * 결정한다. blanche식 인덱스 분리(static/blog/categories[/authors]) 위에 이미지·\n * 다국어 토글을 얹는다.\n */\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\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 /** 작성자 카드 설명. null = 설명 없음. */\n authorCardDescription: string | null;\n /** 작성자 프로필 이름. null = 글 작성자 이름 사용. */\n authorProfileName: string | null;\n /** 작성자 프로필 bio. null = 설명 없음. */\n authorProfileBio: string | null;\n /** 작성자 프로필 이미지 URL. null = 이니셜 아바타 사용. */\n authorProfileImageUrl: string | null;\n /** 작성자 프로필 이미지 모서리. */\n authorProfileImageRadius: \"circle\" | \"rounded\" | \"square\";\n /** 작성자 프로필 이미지 object-position x (%). */\n authorProfileImagePositionX: number;\n /** 작성자 프로필 이미지 object-position y (%). */\n authorProfileImagePositionY: number;\n /** 사이트 레벨 SEO 기본값(설명·로고·파비콘·기본 OG). */\n siteProfile: RootTaleSiteProfile;\n /** 글 하단 고정 CTA. null = 미설정/비활성. */\n postCta: RootTaleBlogPostCta | null;\n /**\n * 사이트맵 생성 정책 (admin 설정). createSitemapIndex 가 소비. optional —\n * 구버전 응답/직접 구성한 객체와의 하위호환(소비 시 기본값으로 fail-soft).\n */\n sitemap?: RootTaleSitemapSettings;\n}\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\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 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 } | 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 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 },\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 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}\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\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 const basePath = typeof o.base_path === \"string\" ? o.base_path : \"\";\n if (!key || !basePath) 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 },\n ];\n });\n}\n\n/**\n * fetch 실패/미설정 시 안전 기본값 — 빈 배열(레거시 단일 /blog). 사이트가\n * `fetchCollections().catch(() => fetchCollectionsSafeDefault())` 로 쓴다.\n */\nexport const fetchCollectionsSafeDefault: CmsRouteCollection[] = [];\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 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 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 logoUrl: null,\n faviconUrl: null,\n defaultOgImageUrl: null,\n },\n postCta: null,\n sitemap: {\n blogImages: true,\n locales: [],\n authors: false,\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","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 businessType: BusinessType;\n telephone: 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 /** 외부 프로필 URL — JSON-LD sameAs 로 들어가 동일 사업장임을 연결. */\n profiles: {\n naverPlace: string | null;\n googleBusiness: string | null;\n kakaoChannel: string | null;\n instagram: string | null;\n naverBlog: string | null;\n };\n}\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\ntype BusinessProfileWire = {\n configured?: boolean;\n name?: string | null;\n legal_name?: string | null;\n business_type?: string | null;\n telephone?: 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 profiles?: {\n naver_place?: string | null;\n google_business?: string | null;\n kakao_channel?: 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 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 email: json.email ?? null,\n address,\n geo,\n openingHours,\n priceRange: json.price_range ?? null,\n areaServed: json.area_served ?? [],\n profiles: {\n naverPlace: json.profiles?.naver_place ?? null,\n googleBusiness: json.profiles?.google_business ?? null,\n kakaoChannel: json.profiles?.kakao_channel ?? 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\";\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;\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 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.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 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 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\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\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\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\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","// 프레임워크 무관 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 { CmsPostContent, RootTaleBusinessProfile } 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 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 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}\n\nexport function articleSchema(input: ArticleSchemaInput): JsonLd {\n const schema: JsonLd = {\n \"@context\": \"https://schema.org\",\n \"@type\": \"Article\",\n headline: input.headline,\n mainEntityOfPage: { \"@type\": \"WebPage\", \"@id\": input.url },\n };\n if (input.description) schema.description = input.description;\n if (input.image) schema.image = input.image;\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 = { \"@type\": \"Person\", name: input.authorName };\n if (input.publisherName) {\n schema.publisher = {\n \"@type\": \"Organization\",\n name: input.publisherName,\n ...(input.publisherLogo\n ? { logo: { \"@type\": \"ImageObject\", url: input.publisherLogo } }\n : {}),\n };\n }\n return schema;\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 /** Schema.org LocalBusiness 하위 타입. 미지정 시 \"LocalBusiness\". */\n businessType?: string | null;\n telephone?: 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 /** 외부 프로필 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 */\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 (opts.image) schema.image = opts.image;\n if (input.telephone) schema.telephone = input.telephone;\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 const sameAs = Object.values(input.profiles ?? {}).filter(\n (u): u is string => typeof u === \"string\" && u.length > 0,\n );\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/** JSON-LD 객체 → `<script type=\"application/ld+json\">` HTML 문자열 (XSS-safe). */\nexport function jsonLdScript(schema: JsonLd | JsonLd[]): string {\n // `</script>` 주입 방지를 위해 `<` 이스케이프.\n const json = JSON.stringify(schema).replace(/</g, \"\\\\u003c\");\n return `<script type=\"application/ld+json\">${json}</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 * `@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 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 /**\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\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]);\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(mysite.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 // 유입 어트리뷰션 → intake `attr_*` 필드 (허용 키만, 빈 값 생략).\n if (f.attribution) {\n for (const [key, value] of Object.entries(f.attribution)) {\n if (typeof value !== \"string\" || !value.trim()) continue;\n body.set(`attr_${key}`, value.trim());\n }\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\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-posts-theme.ts","../src/server-blog.ts","../src/server-business.ts","../src/server-discovery.ts","../src/server-site-inquiries.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}\n\nexport interface CmsArchiveRelatedPost {\n id: string;\n title: string;\n slug: string;\n excerpt: string | null;\n collectionKey: string;\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 /** 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 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 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 /** Public biography text for the post author. */\n authorBio: string | null;\n /** Tiptap / Gutenberg-compatible Block JSON. Sanitization is renderer's responsibility. */\n bodyJson: Record<string, unknown>;\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 /** 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 * 네거티브 스코프 — 어느 스트림에도 안 속한(\"미소속\") 글만\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}\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}\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 {\n type CmsPostType,\n type CmsPostContent,\n type CmsPostContentPage,\n type CmsTermRef,\n type FetchPostOptions,\n type FetchPostsOptions,\n DEFAULT_BASE_URL,\n assertServer,\n buildGetInit,\n toApiError,\n} from \"./server-base\";\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\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 };\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 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.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(fromWire),\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 fromWire(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\ntype 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 slug: string;\n /** ADR-0052 A안 — 구 서버는 미포함 — optional(하위호환). */\n locale?: string;\n /** ADR-0052 A안 — 구 서버는 미포함 — optional(하위호환). */\n translations?: Array<{ locale: string; slug: string }>;\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_name?: string | null;\n author_image_url?: string | null;\n author_bio?: string | null;\n body_json: Record<string, unknown>;\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?: CmsTermRef[];\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 published_at: string;\n created_at: string;\n updated_at: string;\n};\n\nfunction fromWire(wire: CmsPostWire): CmsPostContent {\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 slug: wire.slug,\n ...(wire.locale !== undefined ? { locale: wire.locale } : {}),\n ...(wire.translations !== undefined ? { translations: wire.translations } : {}),\n title: wire.title,\n excerpt: wire.excerpt,\n featuredMediaId: wire.featured_media_id,\n featuredImageUrl: wire.featured_media_url ?? null,\n authorId: wire.author_id,\n authorName: wire.author_name ?? null,\n authorImageUrl: wire.author_image_url ?? null,\n authorBio: wire.author_bio ?? null,\n bodyJson: wire.body_json,\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 ?? [],\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 publishedAt: wire.published_at,\n createdAt: wire.created_at,\n updatedAt: wire.updated_at,\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/**\n * 글 하단 고정 CTA — admin 블로그 설정에서 활성화 시 모든 글 본문 끝에 그려진다.\n * null(미설정/비활성)이면 렌더러는 CTA 블록을 생략. buttonLabel/buttonHref 는\n * 객체가 존재하면 항상 non-empty(서버가 둘 다 있을 때만 객체를 내려줌).\n */\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 logoUrl: string | null;\n faviconUrl: string | null;\n /** 글별 og:image 미설정 시 폴백(1200×630 권장). */\n defaultOgImageUrl: string | null;\n}\n\n/**\n * 사이트맵 생성 정책 (admin 설정) — `createSitemapIndex` 가 소비해 무엇을 내보낼지\n * 결정한다. blanche식 인덱스 분리(static/blog/categories[/authors]) 위에 이미지·\n * 다국어 토글을 얹는다.\n */\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\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 /** 작성자 카드 설명. null = 설명 없음. */\n authorCardDescription: string | null;\n /** 작성자 프로필 이름. null = 글 작성자 이름 사용. */\n authorProfileName: string | null;\n /** 작성자 프로필 bio. null = 설명 없음. */\n authorProfileBio: string | null;\n /** 작성자 프로필 이미지 URL. null = 이니셜 아바타 사용. */\n authorProfileImageUrl: string | null;\n /** 작성자 프로필 이미지 모서리. */\n authorProfileImageRadius: \"circle\" | \"rounded\" | \"square\";\n /** 작성자 프로필 이미지 object-position x (%). */\n authorProfileImagePositionX: number;\n /** 작성자 프로필 이미지 object-position y (%). */\n authorProfileImagePositionY: number;\n /** 사이트 레벨 SEO 기본값(설명·로고·파비콘·기본 OG). */\n siteProfile: RootTaleSiteProfile;\n /** 글 하단 고정 CTA. null = 미설정/비활성. */\n postCta: RootTaleBlogPostCta | null;\n /**\n * 사이트맵 생성 정책 (admin 설정). createSitemapIndex 가 소비. optional —\n * 구버전 응답/직접 구성한 객체와의 하위호환(소비 시 기본값으로 fail-soft).\n */\n sitemap?: RootTaleSitemapSettings;\n}\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\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 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 } | 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 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 },\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 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}\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\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 const basePath = typeof o.base_path === \"string\" ? o.base_path : \"\";\n if (!key || !basePath) 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 },\n ];\n });\n}\n\n/**\n * fetch 실패/미설정 시 안전 기본값 — 빈 배열(레거시 단일 /blog). 사이트가\n * `fetchCollections().catch(() => fetchCollectionsSafeDefault())` 로 쓴다.\n */\nexport const fetchCollectionsSafeDefault: CmsRouteCollection[] = [];\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 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 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 logoUrl: null,\n faviconUrl: null,\n defaultOgImageUrl: null,\n },\n postCta: null,\n sitemap: {\n blogImages: true,\n locales: [],\n authors: false,\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","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 businessType: BusinessType;\n telephone: 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 /** 외부 프로필 URL — JSON-LD sameAs 로 들어가 동일 사업장임을 연결. */\n profiles: {\n naverPlace: string | null;\n googleBusiness: string | null;\n kakaoChannel: string | null;\n instagram: string | null;\n naverBlog: string | null;\n };\n}\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\ntype BusinessProfileWire = {\n configured?: boolean;\n name?: string | null;\n legal_name?: string | null;\n business_type?: string | null;\n telephone?: 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 profiles?: {\n naver_place?: string | null;\n google_business?: string | null;\n kakao_channel?: 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 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 email: json.email ?? null,\n address,\n geo,\n openingHours,\n priceRange: json.price_range ?? null,\n areaServed: json.area_served ?? [],\n profiles: {\n naverPlace: json.profiles?.naver_place ?? null,\n googleBusiness: json.profiles?.google_business ?? null,\n kakaoChannel: json.profiles?.kakao_channel ?? 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\";\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;\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 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.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 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 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\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\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\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\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 {\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","// 프레임워크 무관 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 { CmsPostContent, RootTaleBusinessProfile } 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 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 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}\n\nexport function articleSchema(input: ArticleSchemaInput): JsonLd {\n const schema: JsonLd = {\n \"@context\": \"https://schema.org\",\n \"@type\": \"Article\",\n headline: input.headline,\n mainEntityOfPage: { \"@type\": \"WebPage\", \"@id\": input.url },\n };\n if (input.description) schema.description = input.description;\n if (input.image) schema.image = input.image;\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 = { \"@type\": \"Person\", name: input.authorName };\n if (input.publisherName) {\n schema.publisher = {\n \"@type\": \"Organization\",\n name: input.publisherName,\n ...(input.publisherLogo\n ? { logo: { \"@type\": \"ImageObject\", url: input.publisherLogo } }\n : {}),\n };\n }\n return schema;\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 /** Schema.org LocalBusiness 하위 타입. 미지정 시 \"LocalBusiness\". */\n businessType?: string | null;\n telephone?: 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 /** 외부 프로필 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 */\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 (opts.image) schema.image = opts.image;\n if (input.telephone) schema.telephone = input.telephone;\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 const sameAs = Object.values(input.profiles ?? {}).filter(\n (u): u is string => typeof u === \"string\" && u.length > 0,\n );\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 * `@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\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 if (typeof value !== \"string\" || !value.trim()) continue;\n body.set(`attr_${key}`, value.trim());\n }\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.41.0",
3
+ "version": "0.42.0",
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",