@roottale/cms-mcp 0.61.1 → 0.63.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.
@@ -5,6 +5,9 @@ description: 공개 API raw 엔드포인트 — JS 외 스택이나 저수준
5
5
 
6
6
  # HTTP API 레퍼런스
7
7
 
8
+ 팝업·배너의 v2 결정 조회와 수정안·예약·일시 중지 API는
9
+ [팝업·배너와 예약 표시](popups-and-banners.md)를 참고하세요.
10
+
8
11
  베이스 URL: `https://api.roottale.com`
9
12
  인증: 모든 요청에 `Authorization: Bearer rtlk_cust_...` 헤더.
10
13
 
package/docs/blog.md CHANGED
@@ -136,6 +136,9 @@ export default async function PostPage({
136
136
  slugOrId={post.id}
137
137
  showTableOfContents
138
138
  tableOfContentsTitle="목차"
139
+ tocPosition="inline"
140
+ theme={null}
141
+ footerPatternPresentation={null}
139
142
  relatedPostsCount={3}
140
143
  breadcrumb={{ siteUrl: process.env.NEXT_PUBLIC_SITE_URL }}
141
144
  />
@@ -166,7 +169,14 @@ export default async function PostPage({
166
169
  또는 `collections` 로 라우팅됩니다. 현재 글에 카테고리가 없거나 후보가 없으면
167
170
  렌더되지 않습니다.
168
171
 
169
- 목차(ToC)·작성자 카드·발행일 표시는 어드민의 블로그 표시 설정으로도 제어됩니다
172
+ 목차 위치는 사이트 코드의 `tocPosition="inline" | "sidebar"`로 정합니다. 명시한
173
+ 값은 CMS 설정보다 우선하며, 생략한 기존 연동은 CMS의 `tocPosition`을 유지합니다.
174
+ 관리 주체를 코드로 옮길 때는 현재 화면의 배치 값을 명시하세요. 발행 페이지와
175
+ 미리보기에는 같은 값을 적용합니다. `theme={null}`은 원격 테마 조회·스타일 주입을
176
+ 끄고, `footerPatternPresentation={null}`은 공통 블록 디자인을 사이트 CSS에 맡깁니다.
177
+ 두 prop을 생략한 기존 연동은 원격 테마·공통 블록 디자인을 계속 사용합니다.
178
+
179
+ 목차(ToC) 노출·작성자 카드·발행일 표시는 어드민의 블로그 표시 설정으로도 제어됩니다
170
180
  (`theme-and-settings.md` 참고). 여러 글에 같은 CTA가 필요하면 공통 블록의
171
181
  `post_footer` 자리를 쓰세요. 공통 블록으로 아직 옮기지 않은 기존 사이트는
172
182
  `RootTaleBlogPost`가 레거시 `postCta`를 계속 렌더하며, 공통 블록이 배치되면
@@ -314,6 +314,10 @@ const internalLinks = await fetchInternalContentPathIndex({ apiKey, revalidate:
314
314
 
315
315
  ## 노출 슬롯과 캠페인
316
316
 
317
+ 전용 관리 화면과 예약 시작·종료, 수정안 저장 및 공통 Next.js runtime 연결은
318
+ [팝업·배너와 예약 표시](popups-and-banners.md)를 따릅니다. 아래 단일 슬롯 조회는
319
+ 이전 연동과의 호환용입니다.
320
+
317
321
  노출 슬롯은 FRONT의 사이트 콘텐츠 계약에서 옵니다. 관리 API로 슬롯을 만들거나
318
322
  수정할 수 없습니다.
319
323
 
@@ -325,7 +329,8 @@ const internalLinks = await fetchInternalContentPathIndex({ apiKey, revalidate:
325
329
 
326
330
  생성은 항상 초안입니다. 수정·발행·보관은 `version`을 확인합니다. 발행 응답의
327
331
  `overlap`은 같은 슬롯·경로·기간에 겹치는 캠페인 수와 ID를 알려 줍니다. 겹침은
328
- 오류가 아니며, 공개 FRONT는 우선순위와 최신순 규칙으로 한 건을 선택합니다.
332
+ 오류가 아닙니다. 새 공통 runtime은 같은 위치의 유효한 항목을 순서대로 캐러셀로
333
+ 표시합니다. 아래 호환용 단일 조회는 우선순위와 최신순으로 한 건을 선택합니다.
329
334
 
330
335
  ```json
331
336
  {
@@ -0,0 +1,198 @@
1
+ ---
2
+ title: 팝업·배너와 예약 표시
3
+ description: 모든 ROOT-ADMIN 고객 사이트의 전용 운영 메뉴, 예약·종료와 Next.js 공통 표시 코드
4
+ ---
5
+
6
+ # 팝업·배너와 예약 표시
7
+
8
+ ROOT-ADMIN의 `콘텐츠 > 팝업·배너`에서 팝업과 배너를 관리합니다. 작성 권한이 있는
9
+ 모든 고객사에 두 메뉴를 제공합니다. 홈페이지 연결은 제작자가 담당하며, 고객이
10
+ API 키나 예약 작업을 설정할 필요는 없습니다.
11
+
12
+ ## 운영 화면
13
+
14
+ 편집 화면은 `내용`, `표시 기간`, `표시할 페이지`로 구성됩니다. 표시할 페이지는
15
+ 메인 홈으로 고정되어 있으며, 하위 페이지에서는 팝업과 배너가 나타나지 않습니다. 이미지 선택·업로드,
16
+ 문구와 링크, PC·모바일 미리보기를 한 화면에서 사용할 수 있습니다. PC 이미지와
17
+ 모바일 이미지를 따로 선택할 수 있고, 이미지의 핵심 내용을 대체 텍스트로 입력합니다.
18
+
19
+ 기간은 `지금 / 예약 시작`과 `종료 없음 / 종료일`을 각각 선택합니다. 날짜만 정하면
20
+ 시작일 자정부터 종료일이 끝날 때까지 표시합니다. 정확한 시간이 필요하면 분 단위로
21
+ 입력합니다. 화면에 안내한 시간대가 기준이며 기본은 한국 시간(`Asia/Seoul`)입니다.
22
+ `7일간`은 시작일을 포함한 7일입니다. 9월 20일에 시작하면 9월 26일이 끝날 때 종료합니다.
23
+
24
+ `임시 저장`과 `수정안 저장`은 홈페이지에 반영하지 않습니다. 공개 버튼을 누르면
25
+ 현재 내용과 일정이 반영됩니다. 이미 공개한 항목의 수정안을 저장해도 현재 안내는
26
+ 계속 표시됩니다. `일정 변경`은 공개본의 일정만 바꾸므로 저장해 둔 문구·이미지
27
+ 수정안이 함께 발행되지 않습니다.
28
+
29
+ | 상태 | 사용할 작업 |
30
+ |---|---|
31
+ | 초안 | 편집, 발행·예약 |
32
+ | 예약 | 일정 변경, 예약 취소 |
33
+ | 게시 중 | 수정안 저장, 변경 반영, 종료일 변경, 일시 중지 |
34
+ | 일시 중지 | 편집, 유효한 기간 내 재개 |
35
+ | 종료 | 새 초안으로 다시 사용 |
36
+ | 보관 | 초안으로 복원 |
37
+
38
+ ## 자동 슬라이드와 모바일 동작
39
+
40
+ 같은 위치에 기간·기기 조건이 맞는 항목을 여러 개 발행하면 자동 캐러셀이 됩니다.
41
+ 기본 전환 간격은 5초이며, 순서는 우선순위·발행 시각·ID로 결정합니다. 한 위치에
42
+ 동시에 표시 가능한 항목은 최대 20개입니다. 서로 겹치지 않는 예약은 이 제한에
43
+ 합산하지 않습니다. 팝업은 한 창 안에서 순환하므로 여러 창이 겹쳐 열리지 않습니다.
44
+
45
+ 진행 막대와 남은 초는 **다음 슬라이드 전환까지의 시간**입니다. 행사 종료일까지의
46
+ 남은 시간을 뜻하지 않습니다. 이전·다음, 슬라이드 선택, 재생·일시정지를 제공하며,
47
+ 가리키거나 조작하는 동안에는 진행도 멈춥니다. 움직임 줄이기 설정에서는 자동 재생을
48
+ 시작하지 않고 방문자가 직접 재생할 수 있습니다.
49
+
50
+ 모바일에서는 가로 스와이프로 넘길 수 있습니다. 상단·하단 배너는 아래로 스크롤할 때
51
+ 숨기고 위로 스크롤하면 다시 표시하며, 본문 배너는 본문 흐름을 따릅니다. 팝업은 화면
52
+ 하단에 맞추고 닫기·오늘 그만 보기·슬라이드 조작 영역을 확보합니다.
53
+
54
+ 닫기와 표시 기록은 해당 사이트의 쿠키에 저장합니다. 일반 닫기는 현재 묶음을 함께
55
+ 닫고 항목의 재표시 설정을 따릅니다. `오늘 그만 보기`는 해당 위치 전체를 사이트
56
+ 시간대의 다음 자정까지 숨기므로 그날 추가된 슬라이드도 다시 열리지 않습니다.
57
+ 방문 중 한 번은 세션 쿠키를 사용하며, 이전 runtime의 localStorage/sessionStorage
58
+ 기록도 읽어서 기존 숨김 선택을 존중합니다. 로그인 계정이나 다른 기기에 공유되지는
59
+ 않습니다. 쿠키가 차단된 경우 현재 화면의 메모리 상태로 중복 표시를 막습니다.
60
+
61
+ ## Next.js 연결
62
+
63
+ 이 API는 `@roottale/cms-client`와 `@roottale/cms-renderer-next` **0.62.0 이상**을
64
+ 사용합니다. 실제 위치가 배치된 사이트만 연결 완료로 봅니다. 전체 예제는
65
+ `examples/nextjs`에 있으며 실제 신규 사이트 생성 템플릿에도 포함됩니다.
66
+
67
+ 1. 서버 전용 `/api/exposures` 경로를 만듭니다.
68
+ 2. 최상위 공개 레이아웃에 Provider와 상단·하단·팝업 위치를 배치합니다.
69
+ 3. 홈 본문의 원하는 위치에 본문 배너를 배치합니다.
70
+ 4. 실제 위치 계약을 제작자의 인증된 배포·provisioning 과정에서 동기화합니다.
71
+
72
+ ```ts
73
+ // app/api/exposures/route.ts — CMS 키는 서버 환경 변수에만 보관합니다.
74
+ import { createExposureRoute } from "@roottale/cms-renderer-next/routes";
75
+
76
+ export const dynamic = "force-dynamic";
77
+ export const GET = createExposureRoute({
78
+ apiKey: process.env.ROOTTALE_API_KEY!,
79
+ siteId: process.env.ROOTTALE_SITE_ID,
80
+ slots: ["site-banner", "home-banner", "site-bottom-banner", "site-popup"],
81
+ homePaths: ["/"],
82
+ });
83
+ ```
84
+
85
+ ```tsx
86
+ "use client";
87
+ import { usePathname } from "next/navigation";
88
+ import { RootTaleExposureProvider, RootTaleExposureSlot } from "@roottale/cms-renderer-next/exposures";
89
+
90
+ const slots = ["site-banner", "home-banner", "site-bottom-banner", "site-popup"];
91
+
92
+ export function SiteExposures({ children }: { children: React.ReactNode }) {
93
+ const pathname = usePathname();
94
+ return <RootTaleExposureProvider endpoint="/api/exposures" pathname={pathname ?? "/"} slots={slots} homePaths={["/"]}>
95
+ <RootTaleExposureSlot slotKey="site-banner" placement="top" allowedVariants={["card"]} />
96
+ {children}
97
+ <RootTaleExposureSlot slotKey="site-bottom-banner" placement="bottom" allowedVariants={["card"]} />
98
+ <RootTaleExposureSlot slotKey="site-popup" placement="popup" allowedVariants={["card", "image-card"]} />
99
+ </RootTaleExposureProvider>;
100
+ }
101
+
102
+ // 위 Provider 내부의 실제 홈 본문에 배치합니다.
103
+ export function HomeBanner() {
104
+ return <RootTaleExposureSlot slotKey="home-banner" placement="inline" allowedVariants={["card", "image-card"]} />;
105
+ }
106
+ ```
107
+
108
+ 루트 레이아웃에서 `@roottale/cms-renderer-next/styles`를 한 번 불러옵니다. 기존
109
+ 공지바는 상단 슬롯의 `fallback`으로 전달하면 새 배너와 중복되지 않습니다. 기존
110
+ 공지바의 하위 페이지 표시 범위는 유지하며, 새 ROOT-ADMIN 안내만 홈 전용입니다. 고정
111
+ 상담 버튼·하단 내비게이션은 Provider의 `--rt-exposure-bottom-height`를 오프셋에
112
+ 반영합니다. 고정 헤더는 `--rt-exposure-top-height`를 사용할 수 있습니다. 모바일
113
+ 배너를 기존 고정 헤더 아래에 놓으려면 `--rt-exposure-top-offset`으로 시작 위치를 지정합니다.
114
+
115
+ 기본 홈 경로는 `/`입니다. 실제 언어별 홈이 있는 사이트는 `homePaths`에 `/ko`, `/en`
116
+ 같은 정확한 경로를 선언하고 Provider·조회 경로·슬롯 계약에서 같은 값을 사용합니다.
117
+ `/en/about`은 홈이 아닙니다. `slideDurationMs`의 기본값은 `5000`,
118
+ `hideMobileBannersOnScroll`은 기본 `true`이며 제작자가 사이트 코드에서 조정합니다.
119
+
120
+ API 키, upstream 주소, site ID, 허용 슬롯은 서버 코드가 소유합니다. 브라우저가
121
+ 조회할 수 있는 값은 현재 `path`와 `device`뿐입니다. API 키를 `NEXT_PUBLIC_*`
122
+ 환경 변수나 Provider props로 넘기지 않습니다.
123
+
124
+ ## 예약과 캐시
125
+
126
+ Next.js의 시간 기반 `revalidate` 값만으로는 예약 시각에 열린 화면이 바뀌지 않습니다.
127
+ 팝업·배너의 최종 노출 결과는 별도 `no-store` 조회를 사용합니다. 응답에는 서버의
128
+ 판단 시각, 다음 변경 시각, 유효 기한이 포함되며, 현재 표시할 항목이 없어도 다음
129
+ 예약 시작 시각을 반환합니다.
130
+
131
+ Provider는 최초 진입·페이지 이동·기기 조건 변경·다음 예약 경계에서 다시 조회합니다.
132
+ 열려 있는 탭은 60초마다 변경 여부를 확인하고, 숨겨진 탭은 돌아올 때 갱신합니다.
133
+ 알고 있는 종료 시각이 지나면 표시를 종료합니다. 조회 장애나 유효 기한 만료는
134
+ 팝업·배너만 숨기며 홈페이지 본문 렌더링을 중단하지 않습니다. 저장 변경은 이 조회
135
+ 주기 안에 반영되므로 이미 열려 있는 모든 화면에 서버 푸시를 보내는 기능은 아닙니다.
136
+
137
+ 기존 CMS 웹훅과 `createRevalidateRoute`는 그대로 유지합니다. 발행·일시 중지·일정
138
+ 변경은 `rt-exposures` 및 관련 경로의 캐시 무효화를 통지합니다. 페이지 전체의 ISR
139
+ 설정이나 다른 CMS 캐시를 끌 필요는 없습니다.
140
+
141
+ ## 슬롯 계약과 연결 상태
142
+
143
+ 슬롯은 고객 관리 API로 만들지 않습니다. 제작자가 실제 배치한 위치의 안정된 key,
144
+ 종류, 지원 형태와 이미지 크기, `runtimeVersion: 2`, `placement`를 계약에 선언합니다.
145
+ 기존 슬롯 key는 유지합니다. 새 메타데이터가 없는 이전 계약의 checksum은 바뀌지 않습니다.
146
+
147
+ ```json
148
+ {
149
+ "key": "site-popup", "kind": "popup", "variants": ["card", "image-card"],
150
+ "assetKinds": ["image"], "pathPrefixes": ["/"],
151
+ "repeatOptions": ["session", "day", "never"],
152
+ "runtimeVersion": 2, "placement": "popup", "label": "홈 팝업",
153
+ "homePaths": ["/"], "pathRules": [{ "path": "/", "match": "exact" }]
154
+ }
155
+ ```
156
+
157
+ `/.well-known/roottale.json`에 `createFleetInfoRoute({ exposures: { runtimeVersion: 2,
158
+ endpoint: "/api/exposures", slots } })`를 연결하면 RootTale가 배포 상태를 대조할 수
159
+ 있습니다. 소스의 실제 위치와 서버에 등록한 계약, 이 응답이 일치해야 합니다.
160
+ 홈페이지 업데이트가 필요한 경우 관리자 메뉴와 기존 항목은 유지하되 발행을 막습니다.
161
+
162
+ 기존 단일 슬롯 `fetchExposure`와 `/server`의 `RootTaleExposureSlot`은 호환용입니다.
163
+ v2 표시 조건이 있는 항목은 이전 renderer에서 노출되지 않습니다. 기존 사이트는
164
+ Provider와 조회 경로를 함께 업데이트한 뒤 v2 계약을 등록합니다.
165
+
166
+ ## 작성 중 미리보기
167
+
168
+ `RootTaleExposurePreview`는 한 항목의 실제 콘텐츠를 표시합니다. 여러 항목을 함께
169
+ 확인할 때는 `RootTaleExposureCarouselPreview`에 공개 DTO 형태의 `campaigns`와
170
+ `placement`, `device`를 전달합니다. 두 미리보기 모두 API 조회·방문 기록·숨김 쿠키를
171
+ 변경하지 않으며, 캐러셀 미리보기는 같은 슬라이드 조작과 진행 표시를 사용합니다.
172
+ 관리자 기본 편집 미리보기는 현재 수정하는 한 항목을 표시합니다.
173
+
174
+ ## 자동화 API·MCP·CLI
175
+
176
+ 관리 API는 `expected_version`으로 동시 수정을 검사합니다. 수정안을 저장할 때는
177
+ `POST /v1/cms/exposures/{id}/draft`, 일정만 바꿀 때는 `POST .../{id}/schedule`을
178
+ 사용합니다. `POST .../{id}/pause`, `/resume`, `/cancel`, `/duplicate`, `/restore`도
179
+ 같은 버전 검사를 합니다. `POST /v1/cms/exposures/preview`는 저장 없이 조건을 확인합니다.
180
+ 수정 미리보기에는 `exposure_id`(MCP에서는 `exposureId`)를 전달하면 기존 항목을
181
+ 미리보기 내용으로 대체하여 다른 발행 항목과의 실제 순서를 확인합니다.
182
+ 공개 조회는 `GET /v1/cms/public/exposure-decisions?slot_keys=...&path=...&device=desktop`입니다.
183
+
184
+ MCP에는 `saveCmsExposureDraft`, `updateCmsExposureSchedule`, `pauseCmsExposure`,
185
+ `resumeCmsExposure`, `cancelCmsExposureReservation`, `duplicateCmsExposure`,
186
+ `restoreCmsExposure`, `previewCmsExposure`가 추가됩니다. MCP·CLI 입력은 camelCase,
187
+ HTTP API의 콘텐츠와 `delivery`·`schedule` 필드는 snake_case입니다.
188
+
189
+ ```bash
190
+ npx @roottale/cms-mcp cli exposures save-draft <id> --input-file draft.json
191
+ npx @roottale/cms-mcp cli exposures schedule <id> --input-file schedule.json
192
+ npx @roottale/cms-mcp cli exposures pause <id> --expected-version 7
193
+ npx @roottale/cms-mcp cli exposures preview --input-file preview.json
194
+ ```
195
+
196
+ `exposures:write`는 작성·수정안 저장에, `exposures:publish`는 공개본 변경에 필요합니다.
197
+ 기존 `PATCH /v1/cms/exposures/{id}`는 공개본 수정 의미를 유지하므로 수정안 저장에는
198
+ 사용하지 않습니다. 다른 고객사·사이트의 미디어는 연결할 수 없습니다.
@@ -5,6 +5,10 @@ description: 글 발행/수정 시 사이트 캐시를 near-real-time으로 갱
5
5
 
6
6
  # 발행 웹훅 — 캐시 자동 갱신
7
7
 
8
+ 팝업·배너의 예약 시작·종료는 [별도 노출 runtime](popups-and-banners.md)이
9
+ 처리합니다. 아래 웹훅은 저장 변경에 따른 캐시 무효화 계약이며 시간 경과 자체가
10
+ 웹훅을 발생시키지는 않습니다.
11
+
8
12
  어드민에서 글을 발행/수정/삭제하면 RootTale이 고객 사이트의 revalidate
9
13
  엔드포인트로 **ES256 서명된 웹훅**을 보냅니다. 사이트는 서명을 검증하고
10
14
  `revalidatePath`를 호출해 즉시 갱신합니다.
@@ -1,11 +1,12 @@
1
1
  ---
2
2
  title: 테마·블로그 표시·ROOT-ANALYTICS 설정
3
- description: 어드민에서 관리하는 디자인 토큰, 블로그 표시 옵션, ROOT-ANALYTICS 설정을 사이트에서 조회
3
+ description: 고객 운영 정보와 사이트 코드의 디자인 설정을 분리하고 기존 설정 API를 연동
4
4
  ---
5
5
 
6
6
  # 테마·블로그 표시·ROOT-ANALYTICS 설정
7
7
 
8
- 어드민에서 설정한 값을 공개 API로 조회해 사이트에 반영합니다. 모두
8
+ 고객이 직접 수정할 콘텐츠·운영 정보는 CMS에서 조회하고, 디자인·레이아웃은
9
+ 사이트 코드에서 관리합니다. 기존 사이트의 호환용 디자인 API는 유지됩니다. 모두
9
10
  `@roottale/cms-client/server`에서 제공하며 같은 API 키를 사용합니다.
10
11
 
11
12
  ## 저장 즉시 반영 — 캐시 이름표(`tags`)
@@ -28,7 +29,13 @@ description: 어드민에서 관리하는 디자인 토큰, 블로그 표시 옵
28
29
  쪽(웹훅 수신 라우트) 배선은 `revalidation-webhooks.md` §1 "설정 저장"을
29
30
  따르세요 — **양쪽을 다 해야** 즉시 반영이 됩니다.
30
31
 
31
- ## 디자인 토큰 — fetchTheme
32
+ ## 기존 디자인 토큰 연동 — fetchTheme
33
+
34
+ 신규 사이트의 토큰은 프로젝트 CSS·코드에 둡니다. `RootTaleBlogPost`·
35
+ `RootTaleBlogList`·`RootTalePage`·`RootTaleBlogCategories`에 `theme={null}`을 주면
36
+ 원격 테마 조회와 CSS 변수 주입을 생략합니다. 명시한 테마 객체는 코드 값을 쓰고,
37
+ prop을 생략하면 기존 호환 동작으로 원격 테마를 조회합니다.
38
+ 아래 API는 아직 코드로 이전하지 않은 기존 연동에 사용합니다.
32
39
 
33
40
  ```ts
34
41
  import { THEME_CACHE_TAG, fetchTheme } from "@roottale/cms-client/server";
@@ -45,7 +52,9 @@ const theme = await fetchTheme({
45
52
 
46
53
  ## 상단 메뉴 — theme.siteNav
47
54
 
48
- 같은 `fetchTheme` 응답에 어드민 **설정 > 사이트 > 상단 메뉴**에서 저장한 GNB
55
+ 상단 메뉴 구조는 제작자가 관리하는 설정입니다. 고객 콘텐츠 편집 범위에 넣지
56
+ 않으며 신규 사이트는 라우트와 함께 코드에 둡니다. 기존 연동은 같은 `fetchTheme`
57
+ 응답에 개발자용 어드민 메뉴에서 저장한 GNB
49
58
  구조가 함께 담깁니다. 별도 호출이 없고 테마와 같은 캐시 태그로 무효화됩니다.
50
59
 
51
60
  ```ts
@@ -131,11 +140,16 @@ const settings = await fetchBlogSettings({
131
140
  // 검색 제목·설명, null 이면 사이트 이름·사이트 설명으로 폴백) · logoUrl ·
132
141
  // faviconUrl · defaultOgImageUrl — 사이트 <head>/OG 폴백 (seo.md 참고)
133
142
 
134
- // 글 단위 오버라이드(metaJson)와 합성해 최종 표시값 계산
135
- const display = resolvePostDisplay(settings, post);
143
+ // 글 단위 오버라이드(metaJson)와 합성해 노출 여부 계산
144
+ const display = resolvePostDisplay(post, settings);
145
+ // 자체 화면의 배치는 코드가 정합니다. display.tocPosition은 호환용 값입니다.
146
+ const tocPosition = "inline";
136
147
  ```
137
148
 
138
- `RootTaleBlogPost` 컴포넌트를 쓰면 위 글 표시 설정이 자동 반영됩니다. 작성자
149
+ `RootTaleBlogPost`는 노출 여부 설정을 자동 반영하되, 목차 배치는 코드 prop
150
+ `tocPosition="inline" | "sidebar"`를 명시하면 CMS 배치보다 우선합니다. 생략하면
151
+ 기존 CMS 배치를 유지하므로 기존 사이트의 화면이 바뀌지 않습니다. 관리 주체를
152
+ 코드로 옮길 때 현재 배치 값을 명시하고 발행 화면과 미리보기의 prop을 일치시킵니다. 작성자
139
153
  사진의 초점은 글에 연결된 작성자 콘텐츠 값을 적용하고, 사진 모양은 사이트의
140
154
  시멘틱 토큰/CSS를 따릅니다. 레거시 `postCta`는 아래 공통 블록이 없는 기존 글에서만
141
155
  자동 반영되며, 공통 블록이 배치되면 함께 표시되지 않습니다.
@@ -177,15 +191,39 @@ const footer = selectSitePatternForSlot(post?.patternSlots, patterns);
177
191
  ```tsx
178
192
  // 자체 글 화면 + 공용 렌더러 조합
179
193
  import { RootTalePostPattern } from "@roottale/cms-renderer-next/server";
180
- <RootTalePostPattern apiKey={apiKey} post={post} />
194
+ <RootTalePostPattern apiKey={apiKey} post={post} presentation={null} />
195
+ ```
196
+
197
+ 블록의 문구·연락처·링크는 CMS에 두고 카드·버튼·색은 사이트 코드에 둡니다.
198
+ `RootTalePostPattern.presentation` 또는 `RootTaleBlogPost.footerPatternPresentation`에
199
+ 아래 값을 지정하세요.
200
+
201
+ | 값 | 동작 |
202
+ |---|---|
203
+ | `null` | 원격 디자인의 data 속성·CSS 변수 주입을 생략하고 사이트 CSS 사용 |
204
+ | `CmsSitePatternPresentation` 객체 | 원격 디자인 전체를 코드 객체로 대체(부분 병합 아님) |
205
+ | 생략 | 기존 연동 호환을 위해 원격 `pattern.presentation` 사용 |
206
+
207
+ ```tsx
208
+ <RootTaleBlogPost
209
+ apiKey={apiKey}
210
+ slugOrId={slug}
211
+ tocPosition="inline"
212
+ theme={null}
213
+ footerPatternPresentation={{
214
+ layout: "card",
215
+ background: "#f8f9fa",
216
+ linkStyle: "buttons",
217
+ buttonColors: ["#03c75a", "#1a1a1a"],
218
+ }}
219
+ />
181
220
  ```
182
221
 
183
- 블록의 **표시 형태**(`presentation`: `layout` 본문처럼/카드, `background` 카드 배경색,
184
- `linkStyle` 글자/버튼, `buttonColors` 버튼 색 순서)도 어드민이 정해 내려줍니다. 사이트
185
- CSS 에 색·모양을 고정하지 말고 `sitePatternPresentationAttributes(pattern.presentation)`
186
- 가 주는 data 속성(`data-pattern-layout`, `data-pattern-link-style`)과 CSS 변수
187
- (`--rt-pattern-bg`, `--rt-pattern-btn-1..n`)를 래퍼에 얹은 뒤 그 값만 읽으세요.
188
- `RootTaleBlogPost`·`RootTalePostPattern` 은 이미 그렇게 그립니다.
222
+ 객체를 지정하면 `data-pattern-layout`, `data-pattern-link-style`,
223
+ `--rt-pattern-bg`, `--rt-pattern-btn-1..n`으로 전달됩니다. `null`이어도 블록 본문과
224
+ `data-pattern-key`·`data-pattern-slot`은 유지되므로 사이트 CSS에서 선택할 수 있습니다.
225
+ 자체 렌더러도 `sitePatternPresentationAttributes`에 CMS 값 대신 프로젝트가 소유한
226
+ 디자인 객체를 넘기세요. 기존 `presentation` API 데이터는 삭제하지 않습니다.
189
227
 
190
228
  블록 목록은 `rt-site-patterns` 캐시 이름표를 가지며, 어드민에서 블록·배치 규칙을
191
229
  저장하면 `theme.updated` 웹훅이 다른 설정과 함께 지웁니다(`createRevalidateRoute`
@@ -1,4 +1,6 @@
1
- // RootTale fleet 프로브 — 운영 측이 배포 버전·헬스를 확인하는 well-known 라우트.
2
1
  import { createFleetInfoRoute } from "@roottale/cms-renderer-next/routes";
2
+ import { EXPOSURE_SLOTS } from "../../../lib/exposure-slots";
3
3
 
4
- export const GET = createFleetInfoRoute({ site: "example" });
4
+ export const GET = createFleetInfoRoute({
5
+ exposures: { runtimeVersion: 2, endpoint: "/api/exposures", slots: EXPOSURE_SLOTS },
6
+ });
@@ -0,0 +1,10 @@
1
+ import { createExposureRoute } from "@roottale/cms-renderer-next/routes";
2
+ import { EXPOSURE_HOME_PATHS, EXPOSURE_SLOTS } from "../../../lib/exposure-slots";
3
+
4
+ export const dynamic = "force-dynamic";
5
+
6
+ export async function GET(request: Request): Promise<Response> {
7
+ const apiKey = process.env.ROOTTALE_API_KEY;
8
+ if (!apiKey) return new Response(null, { status: 503, headers: { "Cache-Control": "no-store" } });
9
+ return createExposureRoute({ apiKey, siteId: process.env.ROOTTALE_SITE_ID, slots: EXPOSURE_SLOTS, homePaths: EXPOSURE_HOME_PATHS })(request);
10
+ }
@@ -85,6 +85,9 @@ export default async function PostPage({ params }: Props) {
85
85
  slugOrId={post.id}
86
86
  showTitle={false}
87
87
  showTableOfContents
88
+ tocPosition="inline"
89
+ theme={null}
90
+ footerPatternPresentation={null}
88
91
  relatedPostsCount={3}
89
92
  // opt-in — 시각 브레드크럼 + BreadcrumbList JSON-LD. siteUrl 없으면
90
93
  // 시각 브레드크럼만(JSON-LD 미emit).
@@ -1,5 +1,6 @@
1
1
  // root layout — 렌더러 스타일은 여기서 1회 import (getting-started.md).
2
2
  import "@roottale/cms-renderer-next/styles";
3
+ import { SiteExposures } from "../components/site-exposures";
3
4
 
4
5
  import {
5
6
  BUSINESS_CACHE_TAG,
@@ -43,7 +44,7 @@ export default async function RootLayout({
43
44
  }}
44
45
  />
45
46
  ) : null}
46
- {children}
47
+ <SiteExposures>{children}</SiteExposures>
47
48
  </body>
48
49
  </html>
49
50
  );
@@ -0,0 +1,5 @@
1
+ import { HomeBanner } from "../components/site-exposures";
2
+
3
+ export default function HomePage() {
4
+ return <main><h1>홈페이지</h1><HomeBanner /><p>사이트 소개와 소식을 확인하세요.</p></main>;
5
+ }
@@ -62,6 +62,9 @@ export default async function PostPreviewPage({ params, searchParams }: Props) {
62
62
  apiKey={process.env.ROOTTALE_API_KEY!}
63
63
  baseUrl={process.env.ROOTTALE_API_BASE}
64
64
  previewToken={token}
65
+ tocPosition="inline"
66
+ theme={null}
67
+ footerPatternPresentation={null}
65
68
  relatedPostsCount={3}
66
69
  breadcrumb={{ siteUrl: process.env.NEXT_PUBLIC_SITE_URL }}
67
70
  />
@@ -0,0 +1,127 @@
1
+ [
2
+ {
3
+ "key": "site-banner",
4
+ "kind": "banner",
5
+ "variants": [
6
+ "card"
7
+ ],
8
+ "assetKinds": [
9
+ "image"
10
+ ],
11
+ "pathPrefixes": [
12
+ "/"
13
+ ],
14
+ "repeatOptions": [
15
+ "always",
16
+ "session",
17
+ "day",
18
+ "never"
19
+ ],
20
+ "runtimeVersion": 2,
21
+ "placement": "top",
22
+ "label": "홈 상단",
23
+ "homePaths": [
24
+ "/"
25
+ ],
26
+ "pathRules": [
27
+ {
28
+ "path": "/",
29
+ "match": "exact"
30
+ }
31
+ ]
32
+ },
33
+ {
34
+ "key": "home-banner",
35
+ "kind": "banner",
36
+ "variants": [
37
+ "card",
38
+ "image-card"
39
+ ],
40
+ "assetKinds": [
41
+ "image"
42
+ ],
43
+ "pathPrefixes": [
44
+ "/"
45
+ ],
46
+ "repeatOptions": [
47
+ "always",
48
+ "session",
49
+ "day",
50
+ "never"
51
+ ],
52
+ "runtimeVersion": 2,
53
+ "placement": "inline",
54
+ "label": "홈 본문",
55
+ "pathRules": [
56
+ {
57
+ "path": "/",
58
+ "match": "exact"
59
+ }
60
+ ],
61
+ "homePaths": [
62
+ "/"
63
+ ]
64
+ },
65
+ {
66
+ "key": "site-bottom-banner",
67
+ "kind": "banner",
68
+ "variants": [
69
+ "card"
70
+ ],
71
+ "assetKinds": [
72
+ "image"
73
+ ],
74
+ "pathPrefixes": [
75
+ "/"
76
+ ],
77
+ "repeatOptions": [
78
+ "always",
79
+ "session",
80
+ "day",
81
+ "never"
82
+ ],
83
+ "runtimeVersion": 2,
84
+ "placement": "bottom",
85
+ "label": "홈 하단",
86
+ "homePaths": [
87
+ "/"
88
+ ],
89
+ "pathRules": [
90
+ {
91
+ "path": "/",
92
+ "match": "exact"
93
+ }
94
+ ]
95
+ },
96
+ {
97
+ "key": "global-popup",
98
+ "kind": "popup",
99
+ "variants": [
100
+ "card",
101
+ "image-card"
102
+ ],
103
+ "assetKinds": [
104
+ "image"
105
+ ],
106
+ "pathPrefixes": [
107
+ "/"
108
+ ],
109
+ "repeatOptions": [
110
+ "session",
111
+ "day",
112
+ "never"
113
+ ],
114
+ "runtimeVersion": 2,
115
+ "placement": "popup",
116
+ "label": "홈 팝업",
117
+ "homePaths": [
118
+ "/"
119
+ ],
120
+ "pathRules": [
121
+ {
122
+ "path": "/",
123
+ "match": "exact"
124
+ }
125
+ ]
126
+ }
127
+ ]
@@ -1,13 +1,13 @@
1
- import { RootTaleExposureSlot } from "@roottale/cms-renderer-next/server";
1
+ "use client";
2
2
 
3
- export async function GlobalPopup({ path }: { path: string }) {
3
+ import { RootTaleExposureSlot } from "@roottale/cms-renderer-next/exposures";
4
+
5
+ export function GlobalPopup() {
4
6
  return (
5
7
  <RootTaleExposureSlot
6
- apiKey={process.env.ROOTTALE_API_KEY!}
7
8
  slotKey="global-popup"
8
- path={path}
9
+ placement="popup"
9
10
  allowedVariants={["card", "image-card"]}
10
- revalidate={60}
11
11
  />
12
12
  );
13
13
  }
@@ -0,0 +1,22 @@
1
+ "use client";
2
+
3
+ import { usePathname } from "next/navigation";
4
+ import { RootTaleExposureProvider, RootTaleExposureSlot } from "@roottale/cms-renderer-next/exposures";
5
+ import { EXPOSURE_HOME_PATHS, EXPOSURE_SLOTS } from "../lib/exposure-slots";
6
+ import { GlobalPopup } from "./global-popup";
7
+
8
+ export function SiteExposures({ children }: { children: React.ReactNode }) {
9
+ const pathname = usePathname();
10
+ return (
11
+ <RootTaleExposureProvider endpoint="/api/exposures" pathname={pathname ?? "/"} slots={EXPOSURE_SLOTS} homePaths={EXPOSURE_HOME_PATHS}>
12
+ <RootTaleExposureSlot slotKey="site-banner" placement="top" allowedVariants={["card"]} />
13
+ {children}
14
+ <RootTaleExposureSlot slotKey="site-bottom-banner" placement="bottom" allowedVariants={["card"]} />
15
+ <GlobalPopup />
16
+ </RootTaleExposureProvider>
17
+ );
18
+ }
19
+
20
+ export function HomeBanner() {
21
+ return <RootTaleExposureSlot slotKey="home-banner" placement="inline" allowedVariants={["card", "image-card"]} />;
22
+ }
@@ -0,0 +1,3 @@
1
+ // 실제로 배치한 위치만 선언합니다. API 키는 이 파일에 넣지 않습니다.
2
+ export const EXPOSURE_SLOTS = ["site-banner", "home-banner", "site-bottom-banner", "global-popup"] as const;
3
+ export const EXPOSURE_HOME_PATHS = ["/"] as const;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@roottale/cms-mcp",
3
- "version": "0.61.1",
3
+ "version": "0.63.0",
4
4
  "type": "module",
5
5
  "description": "RootTale CMS MCP server and CLI for models, entries, exposures, media, integration docs, and public API access.",
6
6
  "bin": {