@roottale/cms-mcp 0.66.0 → 0.67.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -28,6 +28,19 @@ MCP tool 또는 공개 CLI를 사용합니다.
28
28
 
29
29
  ## 관리 API 빠른 흐름
30
30
 
31
+ 공통 메타데이터는 `GET /v1/cms/metadata`로 검색·cursor 조회하고,
32
+ `GET /v1/cms/metadata/{kind}/{key}`로 현재 버전과 지원 필드를 확인합니다.
33
+ `kind`는 `route | post | term`입니다. `PATCH`는
34
+ `{ "expected_version": "조회한 version", "patch": { "title": null, "noindex": false } }`
35
+ 형식이며 수정은 Pro/Premium(또는 루트테일의 사이트별 허용)과 대상별 쓰기 scope가 모두 필요합니다.
36
+ 편집이 닫혀 있으면 403 `upgrade_required`(요금제)·`site_denied`(사이트별 제한)·`billing_locked`(구독 중단)를 반환합니다.
37
+ `site_id`·`locale`은 선택입니다. 본문만 수정하면 저장된 SEO를 보존합니다.
38
+ 고정 경로 공개 조회는 `GET /v1/cms/public/metadata?route_key=home&locale=ko`이며
39
+ `{ "site_id": "…", "metadata": null }` 또는 camelCase `PublicRouteMetadata`를 반환합니다.
40
+ 공개 응답의 `metadata`에는 `routeKey`, `locale`, `path`, `version`, `overrides`,
41
+ `defaults`, `effective`, `supportedFields`, 선택적 `fallback`과 `robots` 강제 정책이 있습니다.
42
+ 공개 읽기는 등급과 관계없이 기존 설정을 사용합니다. 자세한 연동은 [공통 메타데이터](seo.md#공통-메타데이터)를 참고하세요.
43
+
31
44
  관리 API는 초안 작성 → 미디어 연결 → 발행을 분리합니다. 자동화가 실수로
32
45
  미완성 글을 공개하지 않도록 글 생성의 기본 상태는 `draft`입니다.
33
46
 
@@ -167,6 +180,9 @@ tenant/site 경로, 크기, 형식을 검증한 뒤 미디어를 등록합니다
167
180
  완료 응답을 받지 못했거나 `503`을 받으면 같은 `r2_key`와 요청 본문으로 완료
168
181
  요청을 재시도하세요. 새 등록은 `201`, 이미 완료된 업로드의 재시도는 같은
169
182
  미디어 ID와 `200`을 반환합니다. 삭제된 업로드는 다시 완료할 수 없습니다.
183
+ 서버의 보존 조회는 일시적인 timeout·429·5xx를 제한 재시도하지만, 원본 대조에
184
+ 성공하기 전에는 완료로 응답하지 않습니다. 자동 조회 재시도 후에도 `503`이면
185
+ 위와 같이 같은 업로드의 완료 요청을 재시도합니다.
170
186
  응답의 `id`는 글의 `featured_media_id`에, `url`은 Tiptap image 노드의
171
187
  `attrs.src`에 사용합니다.
172
188
 
@@ -491,9 +507,12 @@ fallback 네비를 렌더하세요 (`menus.md` 참고).
491
507
  `generateMetadata` 에서 `post.seo?.ogImage ?? post.featured_media_url ??
492
508
  settings.siteProfile.defaultOgImageUrl` 순으로 우선합니다. `logo_url`·
493
509
  `favicon_url`·`site_description` 은 사이트 `<head>` 에 적용합니다.
494
- `home_title`·`home_description` 은 **메인 홈(`/`) 전용** 검색 제목·설명(어드민
495
- "검색·공유 표시 > 메인 홈 검색 노출")으로, `null` 이면 사이트 이름(theme
496
- `site_name`)·`site_description` 으로 폴백하세요(`seo.md` "메인 홈 메타데이터").
510
+ `home_title`·`home_description`은 **메인 홈(`/`) 전용** 호환 필드입니다.
511
+ 계약 등록만으로 기존 홈 설정을 바꾸지 않습니다. 초기값 보존 또는 실제 설치 확인에서
512
+ 홈 전환을 완료한 뒤 해당 경로의 `effective.title`·`effective.description`을 투영합니다.
513
+ 전환 뒤에는 초기화·연결 해제·계약 제거로 예전 홈 설정을 되살리지 않습니다.
514
+ 연결된 CMS 홈이 초안이거나 소유 경로가 제거되면 `null`을 반환합니다.
515
+ 새 연동은 `fetchRouteMetadata`와 공통 빌더를 사용하세요(`seo.md` 참고).
497
516
 
498
517
  ## GET /v1/cms/public/patterns
499
518
 
@@ -564,6 +583,11 @@ settings.siteProfile.defaultOgImageUrl` 순으로 우선합니다. `logo_url`·
564
583
  판정에는 쓰지 마세요**. 그 판정의 단일 소스는 `/blog-settings` 의
565
584
  `sitemap.promoted_category_slugs` 입니다(두 소스를 섞으면 사이트맵과 페이지가
566
585
  서로 다른 시점의 값을 볼 수 있습니다). 여기 값은 진단·표시용입니다.
586
+ - `category_index_activated`는 사이트의 분류 색인 정책 전환이 끝났는지 알려 줍니다.
587
+ 사이트 계약에 `terms.categoryIndexActivation: "required"`를 선언한 사이트만 사용합니다.
588
+ 이 값이 `true`일 때만 `hub_promoted: false`인 분류를 `noindex`와 사이트맵 제외로
589
+ 처리하고, `false`이거나 값이 없으면 기존처럼 모든 공개 분류를 색인하세요. 선언하지
590
+ 않은 사이트는 이 값을 읽지 않습니다.
567
591
  - 분류가 서버 상한(500개)을 넘으면 응답에 `"truncated": true` 가 붙습니다.
568
592
 
569
593
  `@roottale/cms-client` 로 직접 부를 때:
@@ -9,6 +9,11 @@ description: 어드민 "설정 > 주소 이동"에서 정의한 임의 경로
9
9
  경로 이동 규칙(`/old-event → /promo`)을 사이트 Proxy로 적용합니다. 코드
10
10
  수정 없이 고객이 직접 규칙을 추가·수정·삭제할 수 있습니다.
11
11
 
12
+ 수동 등록·수정·삭제·CSV 적용·되돌리기는 **Pro/Premium** 또는 루트테일이 해당 사이트에 주소 이동을 허용한 경우에 사용할 수 있습니다.
13
+ 기본 요금제로 내려가도 기존 규칙은 계속 적용되고 읽기·내보내기를 유지합니다.
14
+ 일반 글 주소 변경에 따른 자동 301은 기본 기능입니다. 공개 조회와 Proxy에
15
+ 요금제 검사를 추가하면 기존 주소가 끊어지므로 읽기에는 등급 제한을 두지 마세요.
16
+
12
17
  두 종류의 주소 이동이 있습니다.
13
18
 
14
19
  - **글 주소 변경 자동 301** — 블로그 글의 슬러그를 바꾸면 자동으로 옛 주소가
package/docs/inquiries.md CHANGED
@@ -28,7 +28,7 @@ API 키는 블로그 조회와 같은 `rtlk_cust_*` 키를 사이트 서버에
28
28
 
29
29
  ## 같은 사이트에 견적·AS 폼 연결하기
30
30
 
31
- 1. ROOT-ADMIN에서 사이트의 **문의 폼**을 열어 견적과 AS 폼을 각각 만듭니다.
31
+ 1. RootTale 내부 개발자가 ROOT-ADMIN에서 해당 사이트를 선택한 뒤 **루트테일 관리자 → 문의 폼**을 열어 견적과 AS 폼을 각각 만듭니다. 고객 계정은 폼 설정을 편집하지 않고 **받은 문의**에서 접수 내용을 관리합니다.
32
32
  2. 견적에는 `sales`, AS에는 `service` 업무 유형을 지정합니다. 항목 키·타입·필수
33
33
  조건·개인정보 동의 문구를 저장하고 두 폼의 ID를 서버 설정에 넣습니다.
34
34
  3. 각 폼을 `fetchInquiryForm`으로 읽어 해당 `version`과 동의 문구를 보여줍니다.
@@ -110,6 +110,7 @@ const menu = await fetchMenu({ apiKey, slug: "primary", tags: [MENUS_CACHE_TAG]
110
110
  | `fetchBlogSettings` | `BLOG_SETTINGS_CACHE_TAG` | 설정 > 블로그 |
111
111
  | `fetchCollections` | `COLLECTIONS_CACHE_TAG` | 설정 > 콘텐츠 유형 |
112
112
  | `fetchSitePatterns` | `SITE_PATTERNS_CACHE_TAG` | 콘텐츠 도구 > 공통 블록 |
113
+ | `fetchRouteMetadata` | `METADATA_CACHE_TAG` · `SITE_METADATA_DEFAULTS_CACHE_TAG` · 경로별 태그(자동 부착) | 설정 > 검색·공유 표시 |
113
114
 
114
115
  이들을 한 벌로 묶은 `SETTINGS_CACHE_TAGS` 배열도 있습니다. 수신 라우트는
115
116
  설정 저장 신호(`theme.updated`) 한 번에 **이 목록 전체**를 지웁니다 — 무엇이
package/docs/seo.md CHANGED
@@ -8,6 +8,8 @@ description: RSS 피드, 사이트맵, JSON-LD 스키마, fleet 프로브 라우
8
8
  `@roottale/cms-renderer-next/routes`의 팩토리로 RSS·사이트맵을 한 줄에
9
9
  구성하고, `@roottale/cms-client/server`의 헬퍼로 JSON-LD를 생성합니다.
10
10
 
11
+ 고정 페이지의 검색·공유 메타데이터는 아래 [공통 메타데이터](#공통-메타데이터)를 참고하세요.
12
+
11
13
  ## RSS 피드
12
14
 
13
15
  ```ts
@@ -27,6 +29,56 @@ export const GET = createFeedRoute({
27
29
 
28
30
  발행된 글이 자동 포함된 RSS 2.0 XML을 반환합니다.
29
31
 
32
+ ## 공통 메타데이터
33
+
34
+ ROOT-ADMIN **설정 → 검색·공유 표시**에서 홈·코드 페이지·목록 첫 화면·글·분류를 관리합니다. 제목·설명·공유 이미지·이미지 대체 텍스트·`noindex`·`nofollow`를 사이트가 지원하는 범위에서 편집합니다. 등록·수정·초기화는 **Pro/Premium** 또는 루트테일이 해당 사이트에 이 기능을 허용한 경우에 가능합니다. 루트테일이 사이트별로 제한하면 요금제와 관계없이 편집이 닫힙니다. 기본 요금제로 내려가거나 허용이 풀려도 기존 설정의 공개 적용과 읽기·내보내기는 유지됩니다. 일반 본문 편집과 대표 이미지 변경은 이 제한과 별개입니다.
35
+
36
+ 본문이 없는 페이지는 콘텐츠 계약 `metadata.routes`에 안정적인 `routeKey`를 등록합니다. 코드 화면은 `source: { kind: "code", path: "/about" }`, 기존 본문 페이지는 `{ kind: "page", pageKey: "about" }`, 모델 목록은 `{ kind: "model-list", modelKey: "column" }`으로 선언합니다. CMS 페이지를 메타데이터 저장 용도로 새로 만들 필요가 없습니다. 주소를 바꿔도 같은 페이지의 `routeKey`는 유지합니다. SDK 설치만으로 편집을 열지는 않으며 실제 배포 계약·연동 증거가 필요합니다.
37
+
38
+ ```ts
39
+ import { fetchRouteMetadata } from "@roottale/cms-client/server";
40
+ import { buildPageMetadata } from "@roottale/cms-renderer-next/routes";
41
+
42
+ export async function generateMetadata() {
43
+ const route = await fetchRouteMetadata({
44
+ apiKey: process.env.ROOTTALE_API_KEY!, routeKey: "about", locale: "ko",
45
+ });
46
+ return buildPageMetadata({
47
+ ...route,
48
+ fallback: { title: "소개 | 예시 사이트", description: "사이트 소개" },
49
+ }, {
50
+ canonical: "https://example.com/about",
51
+ verification: route ? {
52
+ routeKey: route.routeKey, locale: route.locale, version: route.version,
53
+ // 사이트가 배포한 콘텐츠 계약 체크섬도 함께 전달합니다.
54
+ contractChecksum: process.env.ROOTTALE_CONTENT_CONTRACT_CHECKSUM,
55
+ } : undefined,
56
+ });
57
+ }
58
+ ```
59
+
60
+ `fetchRouteMetadata`는 공개 API를 고유 키로 한 번 조회합니다. `null`은 등록·연결된 경로가 없다는 정상 응답이고, 연결됐지만 직접 설정이 없으면 `overrides: {}`인 객체입니다. HTTP·네트워크·잘못된 응답은 예외입니다. 예외를 `null`로 바꿔 장기 캐시하지 마세요. 오류를 기록하고 코드 기본값으로 화면을 유지하는 예시는 `lib/route-metadata.ts`에 있습니다.
61
+
62
+ 해석 순서는 필드별 **직접 설정 → 콘텐츠/코드 기본값 → 사이트 기본값 → 코드 fallback**입니다. `false`는 명시값이며 강제 미리보기·비공개 제한이 우선합니다. `buildPageMetadata`는 최종 제목을 `title.absolute`로 내보내고 OG·X에 같은 문구와 이미지 대체 텍스트를 적용합니다. 기존 Googlebot 제한은 `googleBot` 옵션으로 전달합니다. canonical은 사이트 코드가 `canonical` 또는 `siteUrl`+`path`로 정하며 `canonical: null`이면 생략합니다. 기존 `buildPostMetadata`의 `seo.canonical`은 기본 유지하고, 코드 소유 사이트는 `allowCanonicalOverride: false`로 고정합니다.
63
+
64
+ 글은 공개 응답의 `metaJson.seo`, 분류는 `seoTitle/seoDescription/seoOgImage/seoOgImageAlt/seoNofollow`를 같은 빌더의 `overrides`에 연결합니다. 분류 색인 여부는 기존 `hubPromoted` 정책을 유지합니다. 글마다 경로 메타데이터 API를 추가 호출하지 않습니다. 본문이 있는 CMS 페이지는 `lib/page-metadata.ts` 예시처럼 기존 글 원본을 사용합니다.
65
+
66
+ 조회에는 `METADATA_CACHE_TAG`, `SITE_METADATA_DEFAULTS_CACHE_TAG`, `routeMetadataCacheTag(routeKey, locale)`가 자동 부착됩니다. 기본 재조회 주기는 300초이며 `revalidate`로 조정할 수 있습니다. 기존 서명 `theme.updated`와 `createRevalidateRoute`가 공통 메타데이터 태그·루트 layout·영향 경로를 무효화합니다. 수신부에 `(tag) => revalidateTag(tag, { expire: 0 })`를 전달해야 합니다. 공개 읽기·SSR·Proxy에는 요금제 조건을 넣지 마세요.
67
+
68
+ 사이트맵도 같은 최종 `noindex`·대표 경로 정책을 사용하고 활성 리다이렉트 출발 주소를 제외해야 합니다. 목록의 페이지 번호·필터별 정책은 사이트 라우트에서 계산합니다. 메타데이터 저장 성공·웹훅 수신 성공·실제 HTML 반영 확인은 서로 다른 상태입니다.
69
+
70
+ ### 관리 API·MCP
71
+
72
+ CLI도 같은 관리 API를 사용합니다. `rt cms metadata list --site-id <site-id> --kind route --state custom`, `rt cms metadata get route home --site-id <site-id>`로 조회합니다. 수정은 `rt cms metadata update route home --site-id <site-id> --input-file patch.json`이며 파일에는 `{ "expected_version": "직전 조회값", "patch": { "title": null } }`처럼 실제 변경만 넣습니다. 인증키를 명령 인자에 넣지 마세요.
73
+
74
+ 코드에 있는 최후 기본 공유 이미지는 경로 계약의 `fallback.ogImage`로 선언합니다. 페이지 자체 이미지에 해당하는 `defaults.ogImage`와 구별하면 사이트 공통 공유 이미지가 먼저 적용되고, 공통 이미지까지 없을 때 코드 fallback을 사용합니다. 공개 경로 응답의 `fallback`도 빌더에 그대로 전달합니다.
75
+
76
+ 설치 확인 표식의 체크섬은 `metadataContractChecksum(normalizedContract)`로 계산할 수 있습니다(`@roottale/cms-client/server`). 이 함수는 배포 도구가 정규화해 등록한 manifest를 입력받아 재귀 키 정렬과 SHA-256만 수행합니다. 스키마 검사나 생략된 기본값 채우기는 하지 않습니다. 등록 API와 사이트가 같은 정규화된 계약을 사용해야 하며 입력 JSON이 다르면 설치 확인이 실패합니다. 첫 설치에서 경로 설정이 아직 없으면 `verification.version`은 생략하고 `routeKey`·`locale`·`contractChecksum`을 전달합니다.
77
+
78
+ `filterMetadataSitemapEntries(entries, policyFor, { redirects })`는 이미 읽은 메타데이터와 `fetchRedirects`의 활성 규칙을 사용해 사이트맵을 거릅니다. `policyFor(entry)`는 `{ metadata: SSR에_사용한_입력, canonical: 최종_대표주소, published: true }`를 반환합니다. 미확인 설정은 기존 포함을 유지하며, 최종 `noindex`·비공개·다른 대표 주소·활성 이동 출발 주소를 제외합니다. 함수가 네트워크를 대신 조회하지는 않습니다.
79
+
80
+ `listCmsMetadata` → `getCmsMetadata` → `updateCmsMetadata` 순서로 지원 필드와 현재 `version`을 확인하고 변경합니다. PATCH는 `expected_version`과 `patch`를 받습니다. 필드 미전송은 유지, `null`은 자동값 복귀, 불리언 `false`는 그대로 저장됩니다. 오래된 버전은 409로 거절합니다. 임의 canonical·HTML 입력은 제공하지 않습니다. 역할·API scope·Pro·연결 검사는 서버가 수행하며 다른 API 경로로 우회할 수 없습니다.
81
+
30
82
  ## 사이트맵
31
83
 
32
84
  `createSitemapIndex`는 **사이트맵 인덱스**(`/sitemap.xml`)와 섹션별 하위 사이트맵
@@ -422,10 +474,16 @@ const crumbs = breadcrumbSchema([
422
474
 
423
475
  ## 메인 홈 메타데이터 (홈 제목·홈 설명)
424
476
 
425
- 어드민 **설정 > 검색·공유 표시 > 메인 홈 검색 노출**에서 저장한 홈 제목·홈
426
- 설명은 `fetchBlogSettings().siteProfile.homeTitle` / `homeDescription` 으로
427
- 내려옵니다(공개 API `site_profile.home_title` / `home_description`). 홈(`/`)
428
- 라우트의 `generateMetadata` 에서 이 값을 우선 쓰고, 비어 있으면(null) 사이트
477
+ 새 연동은 홈도 위의 `fetchRouteMetadata`와 `buildPageMetadata`를 사용합니다.
478
+ 기존 연동의 `fetchBlogSettings().siteProfile.homeTitle` / `homeDescription`
479
+ (공개 API `site_profile.home_title` / `home_description`)은 호환을 위해 유지합니다.
480
+ 계약 등록만으로 전환하지 않고, 초기값 보존 또는 실제 설치 확인에서 홈 전환을
481
+ 완료할 때까지 기존 홈 설정을 반환합니다. 전환 뒤에는 현재 소유 경로의 `effective`
482
+ 제목·설명을 반환하며 초기화·연결 해제·계약 제거로 예전 홈 설정이 다시 나타나지 않습니다.
483
+ CMS 홈이 초안이거나 소유 경로가 제거되면 `null`을 반환합니다.
484
+
485
+ 아래는 기존 연동용 예시입니다. 홈(`/`) 라우트의 `generateMetadata`에서
486
+ 이 값을 우선 쓰고, 비어 있으면(null) 사이트
429
487
  이름(`fetchTheme().siteName`)·사이트 설명(`siteProfile.siteDescription`)으로
430
488
  폴백하세요. `title` 은 root layout 의 `%s | 사이트명` 템플릿을 타지 않도록
431
489
  `{ absolute }` 로 넘깁니다. `openGraph` 는 page 값이 layout 값을 통째로
@@ -200,6 +200,15 @@ const tocPosition = "inline";
200
200
  블록 key | null)로 계산해 내려주므로 사이트는 규칙을 몰라도 됩니다.** 현재
201
201
  자리는 글 하단 `post_footer` 하나입니다.
202
202
 
203
+ 사이트 코드에서 특정 key의 사진을 직접 조회하는 연동은 제작 계약의 `fixedPatterns`로
204
+ 표시합니다. 이 경우 공통 블록 화면은 연결된 사진 미리보기·사용 위치·사진 편집·숨김만
205
+ 제공하고 배치표·새 블록 만들기·삭제를 숨깁니다. 기존 블록이나 배치 데이터를 삭제하지
206
+ 않습니다. 선언이 없는 사이트는 기존 배치 화면을 그대로 사용합니다.
207
+ `examples/nextjs/cms/fixed-patterns.example.json`은 기존 계약에 병합할 필드 예시입니다.
208
+ 관리자와 API Worker 배포 후 현재 계약의 모델·노출 슬롯을 보존하고 version을 올려
209
+ 연결해야 합니다. 선언 자체가 사이트 출력을 만들지는 않으며, 소비 코드는
210
+ `fetchSitePatterns`에서 선언된 key를 찾고 저장 웹훅으로 캐시를 갱신해야 합니다.
211
+
203
212
  - `RootTaleBlogPost`(cms-renderer-next 0.57+)는 본문 바로 아래에 자동으로 그립니다
204
213
  — 추가 코드가 필요 없습니다.
205
214
  - 자체 글 화면을 만든 사이트는 `RootTalePostPattern` 서버 컴포넌트를 본문 아래에
@@ -0,0 +1,9 @@
1
+ {
2
+ "fixedPatterns": [
3
+ {
4
+ "key": "article-footer-image",
5
+ "label": "글 하단 이미지",
6
+ "location": "모든 블로그 글의 목록 링크 바로 위에 표시됩니다."
7
+ }
8
+ ]
9
+ }
@@ -0,0 +1,18 @@
1
+ import type { Metadata } from "next";
2
+ import { readMetadataValues, type CmsPostContent } from "@roottale/cms-client/server";
3
+ import { buildPageMetadata } from "@roottale/cms-renderer-next/routes";
4
+
5
+ /** 본문이 있는 CMS 페이지는 기존 글 SEO를 사용한다. 코드 경로는 route-metadata.ts 참고. */
6
+ export function pageMetadata(
7
+ page: CmsPostContent | null,
8
+ defaults: { title: string; description: string; canonical: string; imageUrl: string },
9
+ ): Metadata {
10
+ return buildPageMetadata({
11
+ overrides: readMetadataValues(page?.metaJson.seo),
12
+ defaults: {
13
+ title: page?.title || defaults.title,
14
+ description: page?.excerpt || defaults.description,
15
+ ogImage: page?.featuredImageUrl || defaults.imageUrl,
16
+ },
17
+ }, { canonical: defaults.canonical });
18
+ }
@@ -0,0 +1,31 @@
1
+ import type { Metadata } from "next";
2
+ import { fetchRouteMetadata, type MetadataValues, type PublicRouteMetadata } from "@roottale/cms-client/server";
3
+ import { buildPageMetadata } from "@roottale/cms-renderer-next/routes";
4
+
5
+ /** fallback은 계약 manifest와 같은 사이트 정의에서 가져온다. API key는 서버 전용이다. */
6
+ export async function routeMetadata(
7
+ routeKey: string,
8
+ options: { canonical: string; fallback: MetadataValues; contractChecksum: string; forceNoindex?: boolean },
9
+ ): Promise<Metadata> {
10
+ let route: PublicRouteMetadata | null = null;
11
+ try {
12
+ route = await fetchRouteMetadata({
13
+ apiKey: process.env.ROOTTALE_API_KEY!, baseUrl: process.env.ROOTTALE_API_BASE,
14
+ routeKey, locale: "ko", revalidate: 300,
15
+ });
16
+ } catch (error) {
17
+ // 오류를 "설정 없음"으로 저장하는 별도 캐시를 만들지 않는다.
18
+ console.error("메타데이터 조회 실패", { routeKey, error: error instanceof Error ? error.name : "unknown" });
19
+ }
20
+ return buildPageMetadata({
21
+ ...route,
22
+ fallback: options.fallback,
23
+ robots: { ...route?.robots, forceNoindex: options.forceNoindex || route?.robots?.forceNoindex },
24
+ }, {
25
+ canonical: options.canonical,
26
+ verification: {
27
+ routeKey, locale: route?.locale ?? "ko", version: route?.version,
28
+ contractChecksum: options.contractChecksum,
29
+ },
30
+ });
31
+ }
@@ -1,4 +1,6 @@
1
1
  // 서버 전용. 하나의 사이트에 등록한 두 폼은 서로 다른 ID와 발행 버전을 갖는다.
2
+ // 폼 ID는 RootTale 내부 개발자가 해당 사이트의 루트테일 관리자 → 문의 폼에서 등록한다.
3
+ // 고객은 받은 문의에서 두 폼의 접수 내용을 함께 관리한다.
2
4
  import { fetchInquiryForm } from "@roottale/cms-client/server";
3
5
 
4
6
  export type SiteFormKey = "quote" | "service";
@@ -5,6 +5,9 @@
5
5
  공개 페이지에는 완료 응답의 `url`을 사용합니다. 보존이 활성화된 환경에서는
6
6
  임시 업로드 경로와 최종 `r2_key`가 다릅니다.
7
7
 
8
+ 보존 조회의 일시적 오류는 서버가 제한 재시도합니다. 아래 `503` 처리는 그 재시도
9
+ 후에도 보존을 확인하지 못한 경우에 적용하며, 성공 응답 전에는 등록 완료로 표시하지 않습니다.
10
+
8
11
  ```ts
9
12
  // 인증된 서버 측 작업에서 실행합니다. API 키를 클라이언트 번들에 넣지 않습니다.
10
13
  // uploadRequest는 1단계 응답, fileInfo는 1단계에 보낸 파일 정보입니다.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@roottale/cms-mcp",
3
- "version": "0.66.0",
3
+ "version": "0.67.1",
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": {