본문 바로가기

팝업·배너와 예약 표시

ROOT-ADMIN의 콘텐츠 > 팝업·배너에서 팝업과 배너를 관리합니다. 작성 권한이 있는 모든 고객사에 두 메뉴를 제공합니다. 홈페이지 연결은 제작자가 담당하며, 고객이 API 키나 예약 작업을 설정할 필요는 없습니다.

운영 화면

편집 화면은 내용, 표시 기간, 표시할 페이지로 구성됩니다. 표시할 페이지는 메인 홈으로 고정되어 있으며, 하위 페이지에서는 팝업과 배너가 나타나지 않습니다. 이미지 선택·업로드, 문구와 링크, PC·모바일 미리보기를 한 화면에서 사용할 수 있습니다. PC 이미지와 모바일 이미지를 따로 선택할 수 있고, 이미지의 핵심 내용을 대체 텍스트로 입력합니다.

기간은 지금 / 예약 시작과 종료 없음 / 종료일을 각각 선택합니다. 날짜만 정하면 시작일 자정부터 종료일이 끝날 때까지 표시합니다. 정확한 시간이 필요하면 분 단위로 입력합니다. 화면에 안내한 시간대가 기준이며 기본은 한국 시간(Asia/Seoul)입니다. 7일간은 시작일을 포함한 7일입니다. 9월 20일에 시작하면 9월 26일이 끝날 때 종료합니다.

임시 저장과 수정안 저장은 홈페이지에 반영하지 않습니다. 공개 버튼을 누르면 현재 내용과 일정이 반영됩니다. 이미 공개한 항목의 수정안을 저장해도 현재 안내는 계속 표시됩니다. 일정 변경은 공개본의 일정만 바꾸므로 저장해 둔 문구·이미지 수정안이 함께 발행되지 않습니다.

상태사용할 작업
초안편집, 발행·예약
예약일정 변경, 예약 취소
게시 중수정안 저장, 변경 반영, 종료일 변경, 일시 중지
일시 중지편집, 유효한 기간 내 재개
종료새 초안으로 다시 사용
보관초안으로 복원

자동 슬라이드와 모바일 동작

같은 위치에 기간·기기 조건이 맞는 항목을 여러 개 발행하면 자동 캐러셀이 됩니다. 기본 전환 간격은 5초이며, 순서는 우선순위·발행 시각·ID로 결정합니다. 한 위치에 동시에 표시 가능한 항목은 최대 20개입니다. 서로 겹치지 않는 예약은 이 제한에 합산하지 않습니다. 팝업은 한 창 안에서 순환하므로 여러 창이 겹쳐 열리지 않습니다.

진행 막대와 남은 초는 다음 슬라이드 전환까지의 시간입니다. 행사 종료일까지의 남은 시간을 뜻하지 않습니다. 이전·다음, 슬라이드 선택, 재생·일시정지를 제공하며, 가리키거나 조작하는 동안에는 진행도 멈춥니다. 움직임 줄이기 설정에서는 자동 재생을 시작하지 않고 방문자가 직접 재생할 수 있습니다.

모바일에서는 가로 스와이프로 넘길 수 있습니다. 상단·하단 배너는 아래로 스크롤할 때 숨기고 위로 스크롤하면 다시 표시하며, 본문 배너는 본문 흐름을 따릅니다. 팝업은 화면 하단에 맞추고 닫기·오늘 그만 보기·슬라이드 조작 영역을 확보합니다.

닫기와 표시 기록은 해당 사이트의 쿠키에 저장합니다. 일반 닫기는 현재 묶음을 함께 닫고 항목의 재표시 설정을 따릅니다. 매번 표시(always)의 일반 닫기는 현재 화면에만 적용되며 새로고침·재접속하면 다시 표시합니다. 매번 표시로 변경한 항목은 이전 일반 닫기·방문 기록에 막히지 않습니다. 오늘 그만 보기는 해당 위치 전체를 사이트 시간대의 다음 자정까지 숨기므로 그날 추가된 슬라이드도 다시 열리지 않습니다. 방문 중 한 번은 세션 쿠키를 사용하며, 이전 runtime의 localStorage/sessionStorage 기록도 읽어서 기존 숨김 선택을 존중합니다. 로그인 계정이나 다른 기기에 공유되지는 않습니다. 쿠키가 차단된 경우 현재 화면의 메모리 상태로 중복 표시를 막습니다.

Next.js 연결

이 API는 @roottale/cms-client와 @roottale/cms-renderer-next 0.62.0 이상을 사용합니다. 실제 위치가 배치된 사이트만 연결 완료로 봅니다. 전체 예제는 examples/nextjs에 있으며 실제 신규 사이트 생성 템플릿에도 포함됩니다.

  1. 서버 전용 /api/exposures 경로를 만듭니다.
  2. 최상위 공개 레이아웃에 Provider와 상단·하단·팝업 위치를 배치합니다.
  3. 홈 본문의 원하는 위치에 본문 배너를 배치합니다.
  4. 실제 위치 계약을 제작자의 인증된 배포·provisioning 과정에서 동기화합니다.
// app/api/exposures/route.ts — CMS 키는 서버 환경 변수에만 보관합니다.
import { createExposureRoute } from "@roottale/cms-renderer-next/routes";

export const dynamic = "force-dynamic";
export const GET = createExposureRoute({
  apiKey: process.env.ROOTTALE_API_KEY!,
  siteId: process.env.ROOTTALE_SITE_ID,
  slots: ["site-banner", "home-banner", "site-bottom-banner", "site-popup"],
  homePaths: ["/"],
});
"use client";
import { usePathname } from "next/navigation";
import { RootTaleExposureProvider, RootTaleExposureSlot } from "@roottale/cms-renderer-next/exposures";

const slots = ["site-banner", "home-banner", "site-bottom-banner", "site-popup"];

export function SiteExposures({ children }: { children: React.ReactNode }) {
  const pathname = usePathname();
  return <RootTaleExposureProvider endpoint="/api/exposures" pathname={pathname ?? "/"} slots={slots} homePaths={["/"]}>
    <RootTaleExposureSlot slotKey="site-banner" placement="top" allowedVariants={["card"]} />
    {children}
    <RootTaleExposureSlot slotKey="site-bottom-banner" placement="bottom" allowedVariants={["card"]} />
    <RootTaleExposureSlot slotKey="site-popup" placement="popup" allowedVariants={["card", "image-card"]} />
  </RootTaleExposureProvider>;
}

// 위 Provider 내부의 실제 홈 본문에 배치합니다.
export function HomeBanner() {
  return <RootTaleExposureSlot slotKey="home-banner" placement="inline" allowedVariants={["card", "image-card"]} />;
}

루트 레이아웃에서 @roottale/cms-renderer-next/styles를 한 번 불러옵니다. 기존 공지바는 상단 슬롯의 fallback으로 전달하면 새 배너와 중복되지 않습니다. 기존 공지바의 하위 페이지 표시 범위는 유지하며, 새 ROOT-ADMIN 안내만 홈 전용입니다. 고정 상담 버튼·하단 내비게이션은 Provider의 --rt-exposure-bottom-height를 오프셋에 반영합니다. 고정 헤더는 --rt-exposure-top-height를 사용할 수 있습니다. 모바일 배너를 기존 고정 헤더 아래에 놓으려면 --rt-exposure-top-offset으로 시작 위치를 지정합니다.

기본 홈 경로는 /입니다. 실제 언어별 홈이 있는 사이트는 homePaths에 /ko, /en 같은 정확한 경로를 선언하고 Provider·조회 경로·슬롯 계약에서 같은 값을 사용합니다. /en/about은 홈이 아닙니다. slideDurationMs의 기본값은 5000, hideMobileBannersOnScroll은 기본 true이며 제작자가 사이트 코드에서 조정합니다.

API 키, upstream 주소, site ID, 허용 슬롯은 서버 코드가 소유합니다. 브라우저가 조회할 수 있는 값은 현재 path와 device뿐입니다. API 키를 NEXT_PUBLIC_* 환경 변수나 Provider props로 넘기지 않습니다.

예약과 캐시

Next.js의 시간 기반 revalidate 값만으로는 예약 시각에 열린 화면이 바뀌지 않습니다. 팝업·배너의 최종 노출 결과는 별도 no-store 조회를 사용합니다. 응답에는 서버의 판단 시각, 다음 변경 시각, 유효 기한이 포함되며, 현재 표시할 항목이 없어도 다음 예약 시작 시각을 반환합니다.

Provider는 최초 진입·페이지 이동·기기 조건 변경·다음 예약 경계에서 다시 조회합니다. 열려 있는 탭은 60초마다 변경 여부를 확인하고, 숨겨진 탭은 돌아올 때 갱신합니다. 알고 있는 종료 시각이 지나면 표시를 종료합니다. 조회 장애나 유효 기한 만료는 팝업·배너만 숨기며 홈페이지 본문 렌더링을 중단하지 않습니다. 저장 변경은 이 조회 주기 안에 반영되므로 이미 열려 있는 모든 화면에 서버 푸시를 보내는 기능은 아닙니다.

기존 CMS 웹훅과 createRevalidateRoute는 그대로 유지합니다. 발행·일시 중지·일정 변경은 rt-exposures 및 관련 경로의 캐시 무효화를 통지합니다. 페이지 전체의 ISR 설정이나 다른 CMS 캐시를 끌 필요는 없습니다.

슬롯 계약과 연결 상태

슬롯은 고객 관리 API로 만들지 않습니다. 제작자가 실제 배치한 위치의 안정된 key, 종류, 지원 형태와 이미지 크기, runtimeVersion: 2, placement를 계약에 선언합니다. 기존 슬롯 key는 유지합니다. 새 메타데이터가 없는 이전 계약의 checksum은 바뀌지 않습니다.

{
  "key": "site-popup", "kind": "popup", "variants": ["card", "image-card"],
  "assetKinds": ["image"], "pathPrefixes": ["/"],
  "repeatOptions": ["session", "day", "never"],
  "runtimeVersion": 2, "placement": "popup", "label": "홈 팝업",
  "homePaths": ["/"], "pathRules": [{ "path": "/", "match": "exact" }]
}

/.well-known/roottale.json에 createFleetInfoRoute({ exposures: { runtimeVersion: 2, endpoint: "/api/exposures", slots } })를 연결하면 RootTale가 배포 상태를 대조할 수 있습니다. 소스의 실제 위치와 서버에 등록한 계약, 이 응답이 일치해야 합니다. 홈페이지 업데이트가 필요한 경우 관리자 메뉴와 기존 항목은 유지하되 발행을 막습니다.

기존 단일 슬롯 fetchExposure와 /server의 RootTaleExposureSlot은 호환용입니다. v2 표시 조건이 있는 항목은 이전 renderer에서 노출되지 않습니다. 기존 사이트는 Provider와 조회 경로를 함께 업데이트한 뒤 v2 계약을 등록합니다.

작성 중 미리보기

ROOT-ADMIN의 편집 미리보기는 고객 홈페이지를 PC 1280×900 또는 모바일 390×844 iframe으로 열고, 저장 전 내용을 해당 사이트의 실제 RootTaleExposureSlot에 전달합니다. 사이트 CSS·폰트·헤더·닫기 조작부를 그대로 사용합니다. 편집 칸 너비에 맞춰 전체 화면을 축소하므로 관리자 브라우저 너비가 사이트의 모바일 분기점을 바꾸지 않습니다.

이 연결은 사이트와 관리자 모두 실제 사이트 미리보기 기능이 포함된 renderer 릴리스를 사용해야 합니다(0.63.0에는 미포함). Provider가 연결을 담당하므로 새 서버 API나 CMS 키 전달은 필요 없습니다. 이전 버전·iframe 차단·누락 슬롯·지원하지 않는 형식에서는 관리자가 연결 대기를 표시하며, 공통 디자인을 실제 사이트 화면으로 대신 표시하지 않습니다.

기본 허용 편집기 출처는 https://admin.roottale.com입니다. 자체 관리자나 로컬 검수는 사이트 코드의 previewOrigins에 정확한 출처를 추가합니다. 와일드카드는 사용하지 않습니다.

<RootTaleExposureProvider
  endpoint="/api/exposures"
  pathname={pathname ?? "/"}
  slots={EXPOSURE_SLOTS}
  previewOrigins={["https://editor.example.com"]}
>
  {children}
</RootTaleExposureProvider>

사이트의 frame-ancestors·X-Frame-Options가 관리자 임베딩을 허용해야 합니다. 사이트가 이를 차단한다면 허용할 관리자 출처만 명시적으로 설정합니다. 미리보기용 URL의 fragment에는 연결 식별자와 편집기 출처만 들어가고 초안 내용·CMS 키는 포함하지 않습니다. 입력은 허용된 부모 창·출처·세션을 확인한 후 메모리에서만 처리합니다.

편집 미리보기는 현재 한 항목을 예약·스크롤 대기 없이 표시합니다. 기기 조건·예약 시각과 다른 발행 항목을 합친 노출 판정 검증은 아래 결정 API로 구분합니다. 기존 숨김 쿠키는 무시하며 새 표시·닫기 기록을 남기지 않습니다. 링크 이동·사이트 폼 제출을 막고, 사이트에 포함된 자체 분석 스크립트 등은 사이트의 별도 미리보기 정책을 따릅니다. 팝업이 편집 중인 입력의 초점을 가져가지 않으며 다시 보기로 닫은 안내를 다시 확인합니다.

독립 콘텐츠 렌더링

RootTaleExposurePreview는 한 항목의 실제 콘텐츠를 표시합니다. 여러 항목을 함께 확인할 때는 RootTaleExposureCarouselPreview에 공개 DTO 형태의 campaigns와 placement, device를 전달합니다. 두 미리보기 모두 API 조회·방문 기록·숨김 쿠키를 변경하지 않으며, 캐러셀 미리보기는 같은 슬라이드 조작과 진행 표시를 사용합니다. 관리자 기본 편집 미리보기는 현재 수정하는 한 항목을 표시합니다.

자동화 API·MCP·CLI

관리 API는 expected_version으로 동시 수정을 검사합니다. 수정안을 저장할 때는 POST /v1/cms/exposures/{id}/draft, 일정만 바꿀 때는 POST .../{id}/schedule을 사용합니다. POST .../{id}/pause, /resume, /cancel, /duplicate, /restore도 같은 버전 검사를 합니다. POST /v1/cms/exposures/preview는 저장 없이 조건을 확인합니다. 수정 미리보기에는 exposure_id(MCP에서는 exposureId)를 전달하면 기존 항목을 미리보기 내용으로 대체하여 다른 발행 항목과의 실제 순서를 확인합니다. 공개 조회는 GET /v1/cms/public/exposure-decisions?slot_keys=...&path=...&device=desktop입니다.

MCP에는 saveCmsExposureDraft, updateCmsExposureSchedule, pauseCmsExposure, resumeCmsExposure, cancelCmsExposureReservation, duplicateCmsExposure, restoreCmsExposure, previewCmsExposure가 추가됩니다. MCP·CLI 입력은 camelCase, HTTP API의 콘텐츠와 delivery·schedule 필드는 snake_case입니다.

npx @roottale/cms-mcp cli exposures save-draft <id> --input-file draft.json
npx @roottale/cms-mcp cli exposures schedule <id> --input-file schedule.json
npx @roottale/cms-mcp cli exposures pause <id> --expected-version 7
npx @roottale/cms-mcp cli exposures preview --input-file preview.json

exposures:write는 작성·수정안 저장에, exposures:publish는 공개본 변경에 필요합니다. 기존 PATCH /v1/cms/exposures/{id}는 공개본 수정 의미를 유지하므로 수정안 저장에는 사용하지 않습니다. 다른 고객사·사이트의 미디어는 연결할 수 없습니다.

여러 팝업의 사이트별 높이 조절에는 .rt-exposure-sizer를 사용할 수 있습니다. 기본 스타일은 숨김이며, 이 영역은 접근성과 상호작용에서 제외됩니다. 같은 너비의 콘텐츠를 겹쳐 배치하면 가장 높은 콘텐츠 기준의 공통 높이를 확보할 수 있습니다. 팝업은 이미지 로딩 후 표시합니다.