@roottale/cms-mcp 0.74.0 → 0.75.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,23 @@
1
1
  # @roottale/cms-mcp
2
2
 
3
+ ## 0.75.0
4
+
5
+ ### Patch Changes
6
+
7
+ - 812fe1a: 배경 영상 file 필드(mediaKind: video)의 PC·모바일 MP4와 첫 장면 JPG 공개 값 계약, 직원 전용 업로드·삭제와 고객 선택 절차를 문서화합니다.
8
+
9
+ 영상 묶음 완료 시 MP4와 첫 장면 JPG의 실제 파일 헤더를 확인합니다.
10
+
11
+ - a4f12c4: 공개 문의의 vertical 입력값은 검증·동의 판단에 쓰고, CRM 저장 업종과 상태 구성은 사이트 업종 설정을 따르는 계약을 안내합니다.
12
+ - d3803b8: 주소 이동 규칙 조회가 대기 시간보다 오래 걸려도 끊지 않고 끝까지 받아 다음 요청부터 적용합니다. Proxy의 `event`를 두 번째 인자로 넘기고, `store`(Vercel은 `getCache()`)를 주면 새 인스턴스의 첫 요청도 규칙을 씁니다. `fetchTimeoutMs`로 조회 제한 시간을 정합니다.
13
+ - 710b5b8: 실제화면 편집 계약에 contentAreas 선택 목록을 추가합니다. fields 방식 영역의 문자열을 기존 bodyJson에 저장하며, 연결 주소와 미리보기 예제를 제공합니다.
14
+
15
+ 화면에 출력하지 않는 영역 입력칸을 hiddenContentAreaFields로 선언할 수 있습니다. 숨긴 칸의 변경은 거부하고 기존 저장값을 보존합니다.
16
+
17
+ - fed814b: 기존 사이트의 화면 편집 연결 문서에 작업본 반복행 ID 자동 부여와 첫 발행 반영, 계약 등록 단계의 필드 key 검사를 안내합니다.
18
+ - 685d215: 실제화면 편집 미리보기가 글 미리보기 방식과 무관하게 사이트 주소에서 열리는 동작을 문서화합니다.
19
+ - 7686506: 실제화면 편집 템플릿의 공개 필드에서 이미지·파일·갤러리를 일반 공개 필드와 같은 미디어 객체로 제공하는 동작을 문서화합니다.
20
+
3
21
  ## 0.74.0
4
22
 
5
23
  ### Patch Changes
package/dist/index.js CHANGED
@@ -1855,7 +1855,7 @@ function registerTools(server) {
1855
1855
  }
1856
1856
 
1857
1857
  // src/server.ts
1858
- var VERSION = true ? "0.74.0" : "dev";
1858
+ var VERSION = true ? "0.75.0" : "dev";
1859
1859
  var SERVER_INSTRUCTIONS = `
1860
1860
  roottale-cms-mcp\uB294 RootTale CMS\uB97C \uC678\uBD80 \uC0AC\uC774\uD2B8(\uC8FC\uB85C Next.js)\uC5D0 \uC5F0\uB3D9\uD558\uACE0
1861
1861
  \uAE00\xB7\uC378\uB124\uC77C\xB7\uBCF8\uBB38 \uC774\uBBF8\uC9C0\uB97C \uC790\uB3D9\uD654\uD558\uAE30 \uC704\uD55C \uBB38\uC11C\xB7\uC608\uC2DC \uCF54\uB4DC\xB7API tool\uC744 \uC81C\uACF5\uD569\uB2C8\uB2E4.
@@ -872,7 +872,7 @@ SDK `submitFormInquiry`가 접수증 검증과 구조화 오류 반환을 제공
872
872
 
873
873
  | 필드 | 필수 | 비고 |
874
874
  |---|---|---|
875
- | `vertical` | ✅ | `consulting` \| `medical` \| `tax` \| `legal` |
875
+ | `vertical` | ✅ | `consulting` \| `medical` \| `tax` \| `legal`. 입력값은 검증·동의 판단에 쓰며, CRM 저장 업종과 상태 구성은 사이트의 업종 설정을 따릅니다. |
876
876
  | `contact_name` | ✅ | |
877
877
  | `business_name` | ✅ | |
878
878
  | `email` | ✅ | `.+@.+\..+` |
@@ -41,22 +41,29 @@ description: 어드민 "설정 > 주소 이동"에서 정의한 임의 경로
41
41
  Proxy는 모든 페이지 요청 앞에서 실행되므로 API 장애가 사이트 전체를 멈추지
42
42
  않도록 다음과 같이 동작합니다.
43
43
 
44
- - 규칙 조회는 최대 `timeoutMs`(기본 1.5초)만 기다립니다.
44
+ - 요청은 규칙을 최대 `timeoutMs`(기본 1.5초)만 기다립니다. 조회는 끊지 않고
45
+ `fetchTimeoutMs`(기본 10초)까지 계속해 다음 요청부터 그 결과를 씁니다.
45
46
  - 캐시가 만료돼도 기존 규칙이 있으면 기다리지 않고 쓰고, 갱신은 뒤에서 한 번만 합니다.
46
47
  - 조회가 실패하면 30초 동안 다시 조회하지 않고, 그동안 기존 규칙(없으면 통과)을 씁니다.
48
+ - 메모리 캐시는 인스턴스마다 따로 있습니다. 방문이 드문 사이트는 `store`로 규칙을 보관해야
49
+ 새 인스턴스의 첫 요청도 규칙을 놓치지 않습니다.
47
50
 
48
51
  ```ts
49
52
  // proxy.ts
50
- import { NextResponse } from "next/server";
53
+ import { NextResponse, type NextFetchEvent } from "next/server";
54
+ import { getCache } from "@vercel/functions";
51
55
  import { createRedirectMiddleware } from "@roottale/cms-renderer-next/routes";
52
56
 
53
57
  const redirects = createRedirectMiddleware({
54
58
  apiKey: process.env.ROOTTALE_API_KEY!,
55
- // apiBase, siteId, cacheTtlMs, timeoutMs 는 선택.
59
+ // Vercel Runtime Cache. Vercel이 아니면 store를 빼거나 get·set을 가진 보관소를 넘깁니다.
60
+ store: getCache(),
61
+ // apiBase, siteId, cacheTtlMs, timeoutMs, fetchTimeoutMs 는 선택.
56
62
  });
57
63
 
58
- export async function proxy(req: Request) {
59
- return (await redirects(req)) ?? NextResponse.next();
64
+ export async function proxy(req: Request, event?: NextFetchEvent) {
65
+ // event를 넘겨야 응답 뒤에도 규칙 조회를 끝까지 합니다.
66
+ return (await redirects(req, event)) ?? NextResponse.next();
60
67
  }
61
68
 
62
69
  // Next 내부·API·알려진 정적 자산만 제외합니다. 점 포함 경로를 전부 막으면
package/docs/inquiries.md CHANGED
@@ -245,7 +245,7 @@ export async function submitContact(
245
245
 
246
246
  | 필드 | 필수 | 설명 |
247
247
  |---|---|---|
248
- | `vertical` | ✅ | `consulting` \| `medical` \| `tax` \| `legal` |
248
+ | `vertical` | ✅ | `consulting` \| `medical` \| `tax` \| `legal`. 입력값은 검증·동의 판단에 쓰며, CRM 저장 업종과 상태 구성은 사이트의 업종 설정을 따릅니다. |
249
249
  | `contactName` | ✅ | 이름 |
250
250
  | `businessName` | ✅ | 사업체명 |
251
251
  | `email` | ✅ | 이메일 (`.+@.+\..+`) |
@@ -36,7 +36,7 @@ visualEditing: {
36
36
  },
37
37
  ```
38
38
 
39
- `previewPath`는 `:id`를 정확히 한 번 포함하는 내부 경로입니다. `templateVersion`은
39
+ `previewPath`는 `:id`를 정확히 한 번 포함하는 내부 경로입니다. 관리자는 이 경로를 사이트 주소(`sitePreview.baseUrl`, 없으면 대표 도메인)에서 엽니다. 글 미리보기 방식(`sitePreview.mode`)을 바꿀 필요는 없습니다. `templateVersion`은
40
40
  실제 배포 템플릿과 함께 갱신합니다. 템플릿 key와 그 안의 섹션 key는 중복할 수
41
41
  없으며, `modelKey`는 등록된 모델 또는 기본 `page`여야 합니다. `create: true`는
42
42
  모델마다 하나만 허용하며 새 상세 페이지 생성에 사용할 템플릿을 뜻합니다.
@@ -51,6 +51,76 @@ visualEditing: {
51
51
  수정 허용 여부는 등록된 필드 정의로 별도 검증합니다. 임의 템플릿 이름을 조회에
52
52
  붙여 편집 가능한 모델로 바꾸지 않습니다.
53
53
 
54
+ 공개 글 조회와 미리보기의 `fields`는 반복행 `_id`와 코드 소유 중첩 값을 유지하면서,
55
+ 등록된 `image`·`file` 필드만 일반 공개 필드와 같은 `{ id, url, variants, alt }`
56
+ 형태로 제공합니다. `gallery`는 이 객체의 배열입니다. 그룹·반복·유연한 콘텐츠
57
+ 안의 미디어에도 같은 규칙을 적용하며, 등록되지 않은 중첩 값은 변환하지 않습니다.
58
+ 해석할 수 없는 미디어는 `image`·`file`에서 `null`, `gallery`에서 해당 항목 제외로
59
+ 처리합니다. 사이트는 이미지의 `url` 또는 `variants`를 렌더에 사용합니다.
60
+ 관리자 선택·저장 값은 계속 미디어 UUID(갤러리는 UUID 배열)이며,
61
+ `post_object`·`relationship`·`taxonomy`·`user` 값은 템플릿에서 기존 raw ID를 유지합니다.
62
+
63
+ ### 배경 영상 필드
64
+
65
+ 영상 선택칸은 `{ type: "file", mediaKind: "video" }`로 정의합니다. `mediaKind`가
66
+ 없으면 기존 입력 방식이 유지됩니다. 저장 값은 미디어 UUID이며 공개 글 조회와
67
+ 저장된 draft 미리보기는 같은 값을 제공합니다.
68
+
69
+ ```ts
70
+ {
71
+ id: "영상 미디어 UUID",
72
+ url: "https://cdn.example/pc.mp4",
73
+ alt: null,
74
+ variants: {
75
+ mobile: "https://cdn.example/mobile.mp4",
76
+ poster: "https://cdn.example/pc.jpg",
77
+ mobilePoster: "https://cdn.example/mobile.jpg",
78
+ },
79
+ }
80
+ ```
81
+
82
+ `url`은 PC용 H.264 MP4입니다. 모바일용 주소는 `variants.mobile`, 각 첫 장면
83
+ 사진은 `poster`·`mobilePoster`에 있습니다. 소리는 없습니다. 배경 재생에는
84
+ `muted`·`playsInline`·`autoPlay`와 poster를 지정하고 모바일 화면에서는
85
+ 모바일용 주소를 사용하세요. 실제화면 편집 주소는 영상 요소를 감싼 선택 가능한
86
+ 컨테이너에 붙이면 됩니다.
87
+
88
+ 영상 업로드·삭제는 ROOT-ADMIN에서 루트테일 직원만 수행합니다. 고객은 올라온
89
+ 영상을 선택하거나 필드 연결만 제거합니다. REST·CLI·MCP 업로드는 계속 이미지와
90
+ PDF만 지원하며 영상 삭제도 거절합니다. 직원이 선택한 60초 이하 MP4·MOV·M4V·WebM을
91
+ 브라우저에서 PC 최대 1920·모바일 최대 854로 줄여 무음 MP4와 JPG 두 장을 올립니다.
92
+ 압축된 영상은 파일당 50MB까지이며 원본 파일은 전송하지 않습니다.
93
+
94
+ 주소 없는 `data_only` 모델의 글(`type: "post"`)도 `models[].templateKey`와
95
+ `visualEditing.templates`를 같은 key로 연결하면 실제화면 편집기로 엽니다.
96
+ 새 항목은 해당 모델의 글 목록에서 만들며 관리용 제목과 표시 순서를 저장합니다.
97
+ PC·모바일 미리보기는 동일한 영상 공개 값 계약을 사용합니다.
98
+
99
+ ### 기존 사이트를 연결할 때
100
+
101
+ 먼저 `content-model:sync`로 사이트 필드 그룹을 등록한 뒤 `site:content-contract:sync`로
102
+ 화면 편집 계약을 등록합니다. 필드 key는 해석된 그룹 전체에서 중첩 깊이와 관계없이
103
+ 유일해야 합니다. `fieldKeys`에 없는 key를 쓰거나 여러 섹션이 같은 값을 편집하도록
104
+ 선언하면 등록 단계에서 템플릿과 문제 key를 표시하고 중단합니다. 이 검사는 dry-run과
105
+ `--apply`에 모두 적용하며 고정 페이지의 `page` 모델도 포함합니다. 관리자와 같은
106
+ 공개 상태·모델·템플릿·분류·컬렉션 조건으로 코드 그룹, DB 그룹, override를 해석합니다.
107
+ 아직 글이 없는 템플릿과 생성용 템플릿은 분류·컬렉션이 없는 초기 상태도 검사합니다.
108
+ 필드 그룹이 없으면 `content-model:sync`를 먼저 실행하라는 안내가 표시됩니다.
109
+ `visualEditing`이 없는 계약과 `--exposure-slots`만 등록하는 경로에는 이 검사를 적용하지 않습니다.
110
+
111
+ 기존 글의 반복행에 `_id`가 없어도 목록 전체를 수동으로 다시 저장할 필요는 없습니다.
112
+ 관리자가 화면 편집 작업본을 열면 정의된 `repeater`·`flexible_content`의 누락 ID를
113
+ 중첩 행까지 UUIDv7으로 채워 **작업본에만** 저장합니다. 기존 작업본과 이력 복원·
114
+ 공개 내용으로 다시 시작할 때도 같은 규칙을 적용합니다. 기존 ID와 행 순서, 값,
115
+ 코드 소유 값은 보존하고 정의에 없는 배열은 바꾸지 않습니다. 잘못되거나 중복된 ID는
116
+ 자동 교체하지 않고 오류로 알립니다.
117
+
118
+ 이 준비 작업만으로 공개 글이나 발행 상태는 바뀌지 않습니다. ID는 다음 발행 때
119
+ 공개 응답에 함께 반영됩니다. ID만 추가한 내용을 고객의 미저장 수정으로 표시하지
120
+ 않으며, 이미 모든 ID가 있으면 저장이나 작업본 버전 증가도 없습니다. 미리보기 토큰과
121
+ PC·모바일 렌더 영수증은 보정이 끝난 저장 버전을 기준으로 발급합니다. 공개 사이트에서는
122
+ 첫 발행 전에도 기존 콘텐츠를 그대로 렌더하고, 편집 주소는 인증된 작업본의 ID를 사용합니다.
123
+
54
124
  ## 2. 필드 주소 연결
55
125
 
56
126
  RSC에서도 사용할 수 있는 `visualEditAttributes`는 `data-rt-edit` 속성을 만듭니다.
@@ -215,6 +285,10 @@ parent window·origin·session·증가하는 sequence·정확한 target을 검
215
285
 
216
286
  ## 4. 저장·미리보기·발행 확인
217
287
 
288
+ 같은 화면 폭에서 미리보기와 공개 화면의 배치·글꼴·현재 메뉴 표시를 대조합니다.
289
+ 미리보기 전용 분기에서 블록 구조를 바꾸지 않습니다. 편집 표시는 인라인 `span`으로
290
+ 붙이고, 현재 메뉴처럼 주소에 따라 달라지는 표시도 공개 주소를 기준으로 맞춥니다.
291
+
218
292
  입력은 기존 모델의 필드 패널을 통해 저장합니다. 저장 후 draft 버전이 올라가면
219
293
  새 토큰 URL로 iframe을 다시 열고 RSC를 다시 실행합니다. 관리자는 현재 선택과
220
294
  스크롤 위치를 복구하며, 옛 화면을 최신 저장 결과로 표시하지 않습니다.
@@ -355,3 +429,65 @@ CMS 원문·기대값·해시를 숨은 HTML이나 속성에 복제해서는 안
355
429
  확인하며, CSS 가시성·이미지·링크 목적지·전체 사이트를 검사하는 기능은 아닙니다.
356
430
  사이트에 표식을 먼저 배포한 뒤 새 계약을 등록합니다. 계약 버전·runtime receipt도 함께 갱신하고,
357
431
  기존 고객 콘텐츠를 다시 가져와 덮어쓰지 않습니다.
432
+
433
+ ## 사이트 내용 영역 연결
434
+
435
+ H2 key와 문단 문자열로 저장하는 `contentAreas`의 `fields` 편집기도 실제화면 편집에
436
+ 연결할 수 있습니다. 사이트 계약의 `visualEditing.contentAreas`에 **영역 key**를
437
+ 명시합니다. 목록에 없는 영역은 기존 입력 폼을 계속 사용합니다.
438
+
439
+ ```ts
440
+ contentAreas: [{
441
+ key: "hospital-intro", pageSlug: "area-hospital-intro", path: "/about", label: "병원 소개",
442
+ editor: { kind: "fields", fields: [
443
+ { key: "f-intro-title", group: "소개", label: "제목", kind: "text" },
444
+ { key: "f-intro-body", group: "소개", label: "설명", kind: "multiline" },
445
+ { key: "f-intro-image", group: "사진", label: "사진 주소", kind: "image" },
446
+ { key: "f-intro-link", group: "연결", label: "링크 주소", kind: "link" },
447
+ ] },
448
+ }],
449
+ visualEditing: {
450
+ previewPath: "/preview/content/:id", templateVersion: "hospital-1",
451
+ templates: [], contentAreas: ["hospital-intro"],
452
+ },
453
+ ```
454
+
455
+ `templates`가 비어 있어도 영역을 연결할 수 있지만 둘 다 비어 있으면 거부합니다.
456
+ 영역 key는 중복 없이 등록하며 `editor.kind`는 `fields`여야 합니다. 켠 영역끼리는
457
+ 공개 `path`가 같을 수 없습니다. 같은 `/` 경로에 여러 영역이 있다면 하나만 연결합니다.
458
+ 등록 도구의 dry-run은 연결할 영역의 저장 페이지가 없는지도 알려 줍니다.
459
+ 영역에는 별도 필드 그룹 등록이 필요하지 않습니다.
460
+
461
+ 편집 대상은 `type: "page"`이며 `slug`가 해당 영역의 `pageSlug`인 글입니다.
462
+ 기존 고정 페이지·모델의 편집 템플릿 연결이 있으면 그 연결을 우선합니다.
463
+ 사이트가 출력할 주소는 key와 한 단계 path가 같습니다.
464
+
465
+ ```tsx
466
+ <h2 {...visualEditAttributes({
467
+ entityId: post.id,
468
+ fieldKey: "f-intro-title",
469
+ path: ["f-intro-title"],
470
+ }, isPreview)}>{values["f-intro-title"]}</h2>
471
+ ```
472
+
473
+ 값은 계속 `posts.bodyJson`의 H2 key + paragraph 문자열에 저장합니다. ACF로 옮기지
474
+ 않습니다. 미리보기는 토큰이 가리키는 저장 작업본의 `bodyJson`을 읽어 공개 화면과
475
+ 같은 컴포넌트로 그립니다. [영역 미리보기 예제](../examples/nextjs/components/ContentAreaVisualPreview.tsx)를 참고하세요.
476
+
477
+ 화면에 출력하지 않는 입력칸은 `visualEditing.hiddenContentAreaFields`로 숨깁니다.
478
+ 예: `hiddenContentAreaFields: { "hospital-intro": ["f-intro-image"] }`.
479
+ 영역 key는 `visualEditing.contentAreas`에 연결돼 있어야 하고, 필드 key는 해당 영역의
480
+ `editor.fields`에 중복 없이 등록돼 있어야 합니다. 전체 `editor.fields` 정의는 유지하세요.
481
+ 숨긴 칸은 패널에서 제외하고 서버에서도 변경을 거부합니다. 다른 칸을 저장해도 숨긴 값은
482
+ 보존합니다. 편집할 칸이 남지 않은 구획(표만 있는 구획 포함)은 목록에 표시하지 않습니다.
483
+
484
+ 패널의 구획은 `field.group`의 첫 등장 순서로 표시합니다. 등록됐지만 값이 없는 key는
485
+ 빈 입력칸으로 보이고, 실제로 수정하기 전에는 문서에 추가하지 않습니다. 저장 시 수정하지
486
+ 않은 값과 원래 노드 순서를 유지합니다. 영역 숨김·삭제·반복 추가와 기본 정보·검색 설정은
487
+ 제공하지 않으며 해석할 수 없는 본문은 사유와 함께 읽기 전용으로 엽니다.
488
+
489
+ 이미지와 링크는 미디어 ID가 아닌 URL 문자열입니다. `/`로 시작하는 사이트 내부 주소와
490
+ HTTPS 주소를 허용하며 링크에는 `tel:`도 사용할 수 있습니다. 실행 URL과 인증정보가 든
491
+ 주소는 거부합니다. HTTPS 이미지는 패널에서도 미리 봅니다. 상대 이미지 주소는 사이트
492
+ 미리보기에서 확인하세요. `table`은 패널에 내지 않고 저장된 값을 보존합니다. 표 변경 요청은
493
+ 거부하므로 표 편집이 필요한 화면은 실제화면 편집에 연결하지 않고 기존 폼을 사용하세요.
@@ -0,0 +1,17 @@
1
+ import { visualEditAttributes } from "@roottale/cms-renderer-next/visual-edit";
2
+
3
+ /** 인증된 미리보기에서 조회한 post의 bodyJson을 사이트의 기존 영역 reader로 해석한다. */
4
+ export function ContentAreaVisualPreview({ post, values, isPreview }: {
5
+ post: { id: string };
6
+ values: Record<string, string>;
7
+ isPreview: boolean;
8
+ }) {
9
+ return <section>
10
+ <h2 {...visualEditAttributes({ entityId: post.id, fieldKey: "f-intro-title", path: ["f-intro-title"] }, isPreview)}>
11
+ {values["f-intro-title"]}
12
+ </h2>
13
+ <p {...visualEditAttributes({ entityId: post.id, fieldKey: "f-intro-body", path: ["f-intro-body"] }, isPreview)}>
14
+ {values["f-intro-body"]}
15
+ </p>
16
+ </section>;
17
+ }
@@ -95,3 +95,14 @@ HTTPS `*.roottale.app`은 배포 수준에서 검색을 차단한다. 검색·
95
95
  검색·공유 설정 목록에서는 별도의 검색 설정 계약이 없는 `contentAreas` 저장 문서를
96
96
  제외한다. 같은 화면의 실제 고정 경로를 골라 검색 제목·설명을 편집한다. 별도 페이지나
97
97
  게시물 메타데이터 계약이 있는 문서는 계속 표시하며, 원문·기존 설정·내보내기는 보존한다.
98
+
99
+ ### 기존 반복 목록의 화면 편집 연결
100
+
101
+ `visualEditing`을 연결할 때 필드 그룹을 먼저 등록하고 계약 등록의 dry-run을 확인한다.
102
+ 필드 key는 중첩 깊이와 관계없이 유일해야 한다. 등록 단계에서 고정 `page` 템플릿을
103
+ 포함해 관리자와 같은 필드 해석·섹션 검사를 수행한다.
104
+
105
+ 기존 글에 반복행 `_id`가 없으면 관리자에서 작업본을 열 때 누락된 ID만 자동 저장한다.
106
+ 미리보기는 저장된 작업본 ID로 행 주소를 만들고 공개 사이트에는 다음 발행 때 반영된다.
107
+ 행 순번으로 ID를 대신하거나 공개 렌더에서 임시 ID를 생성하지 않는다.
108
+ 자세한 연결 규칙은 [실제화면 콘텐츠 편집](../../docs/visual-content-editing.md)을 따른다.
@@ -39,7 +39,7 @@ export async function submitContact(
39
39
  apiKey: process.env.ROOTTALE_API_KEY!,
40
40
  baseUrl: process.env.ROOTTALE_API_BASE,
41
41
  fields: {
42
- vertical: "consulting", // 사이트 업종에 맞게: consulting | medical | tax | legal
42
+ vertical: "consulting", // 폼 검증·동의 기준. CRM 저장 업종·상태는 사이트 업종 설정을 따릅니다.
43
43
  contactName: name,
44
44
  businessName,
45
45
  email,
@@ -34,3 +34,17 @@ const media = await completed.json(); // 신규201 또는 멱등 재시도200
34
34
  공개 파일 정리 완료로 안내하세요. `503`이고 메시지가 공개 파일 정리 대기를 알리면
35
35
  관리 목록에서는 이미 사라졌어도 서버가 원본·변형 이미지·CDN 캐시를 자동으로 재정리합니다.
36
36
  이 경우 같은 파일을 다시 업로드하거나 목록에서 사라졌다는 이유로 완료 안내를 보내지 않습니다.
37
+
38
+ ## 배경 영상 선택
39
+
40
+ 영상 업로드·삭제는 ROOT-ADMIN의 루트테일 직원 화면을 사용합니다. REST·CLI·MCP로는
41
+ 올리거나 삭제할 수 없습니다. 고객은 `file` 필드에 `mediaKind: "video"`를 지정한
42
+ 선택창에서 기존 영상을 연결합니다. 공개 글과 미리보기의 필드 값을 그대로 사용합니다.
43
+
44
+ ```tsx
45
+ // video는 fields의 영상 file 값. 모바일 화면에서는 variants.mobile을 고릅니다.
46
+ <video src={video.url} poster={video.variants.poster} autoPlay muted loop playsInline />
47
+ ```
48
+
49
+ PC는 `url`·`variants.poster`, 모바일은 `variants.mobile`·`variants.mobilePoster`를
50
+ 짝으로 사용합니다. 네 주소 모두 HTTPS이며 영상에는 소리가 없습니다.
@@ -4,7 +4,7 @@
4
4
  // 아닌 임의 경로(`/old-event → /promo`)는 라우팅 이전 단계인 Proxy에서만
5
5
  // 가로챌 수 있다. 규칙은 자동 캐시되고, API 실패 시 stale 캐시를
6
6
  // 우선 사용한다. 캐시가 없거나 순환 규칙이면 트래픽을 막지 않는다.
7
- import { NextResponse } from "next/server";
7
+ import { NextResponse, type NextFetchEvent } from "next/server";
8
8
  import { createRedirectMiddleware } from "@roottale/cms-renderer-next/routes";
9
9
  import { siteVisualEditorOrigins } from "./lib/visual-preview-origins";
10
10
 
@@ -12,7 +12,7 @@ const redirects = createRedirectMiddleware({
12
12
  apiKey: process.env.ROOTTALE_API_KEY!,
13
13
  });
14
14
 
15
- export async function proxy(req: Request) {
15
+ export async function proxy(req: Request, event?: NextFetchEvent) {
16
16
  // 실제 계약의 previewPath에 맞춘다. 공개 주소의 기존 정책은 유지한다.
17
17
  const pathname = new URL(req.url).pathname;
18
18
  if (pathname === "/preview" || pathname.startsWith("/preview/")) {
@@ -23,7 +23,7 @@ export async function proxy(req: Request) {
23
23
  response.headers.set("X-Robots-Tag", "noindex, nofollow");
24
24
  return response;
25
25
  }
26
- return (await redirects(req)) ?? NextResponse.next();
26
+ return (await redirects(req, event)) ?? NextResponse.next();
27
27
  }
28
28
 
29
29
  // Next 내부·API·알려진 정적 자산만 제외. 점 포함 경로를 전부 막으면
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@roottale/cms-mcp",
3
- "version": "0.74.0",
3
+ "version": "0.75.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": {