@roottale/cms-mcp 0.61.0 → 0.62.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
 
@@ -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,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
+ }
@@ -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
+ }
@@ -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.0",
3
+ "version": "0.62.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": {