@roottale/cms-renderer-next 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.
- package/CHANGELOG.md +37 -0
- package/README.md +3 -0
- package/dist/chunk-L3I4W5NP.js +16 -0
- package/dist/chunk-L3I4W5NP.js.map +1 -0
- package/dist/chunk-SEPUIKVM.js +922 -0
- package/dist/chunk-SEPUIKVM.js.map +1 -0
- package/dist/chunk-UIBT23RJ.js +3 -0
- package/dist/chunk-UIBT23RJ.js.map +1 -0
- package/dist/cms-public.css +111 -13
- package/dist/exposure-client.d.ts +4 -9
- package/dist/exposure-client.js +5 -3
- package/dist/exposures.d.ts +51 -0
- package/dist/exposures.js +17 -0
- package/dist/exposures.js.map +1 -0
- package/dist/routes.d.ts +20 -1
- package/dist/routes.js +41 -2
- package/dist/routes.js.map +1 -1
- package/dist/server.d.ts +11 -4
- package/dist/server.js +16 -11
- package/dist/server.js.map +1 -1
- package/package.json +11 -4
- package/dist/chunk-GVKENK6H.js +0 -75
- package/dist/chunk-GVKENK6H.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/exposures.tsx","../src/exposure-decision-client.ts","../src/exposure-runtime-data.ts","../src/exposure-group.tsx","../src/exposure-content.tsx","../src/exposure-dialog.tsx","../src/exposure-carousel.tsx","../src/exposure-scroll.ts","../src/exposure-storage.ts"],"sourcesContent":["\"use client\";\n\nimport { createContext, useCallback, useContext, useEffect, useRef, useState, type CSSProperties, type ReactNode } from \"react\";\nimport type { CmsExposureCampaign } from \"@roottale/cms-client/server\";\nimport { useExposureDecision } from \"./exposure-decision-client\";\nimport type { ExposurePlacement } from \"./exposure-content\";\nimport { ExposureCampaignGroup } from \"./exposure-group\";\nimport { exposureHomePaths, isExposureHome } from \"./exposure-home\";\nimport { exposureServerNow, liveCampaigns } from \"./exposure-runtime-data\";\nimport type { ExposureRuntime } from \"./exposure-runtime-context\";\n\nexport { RootTaleExposurePreview } from \"./exposure-content\";\nexport { RootTaleExposureCarouselPreview } from \"./exposure-group\";\nexport type { ExposurePlacement } from \"./exposure-content\";\n\nexport interface RootTaleExposureProviderProps {\n endpoint: string;\n /** 사이트의 usePathname() 값. API 키는 서버에만 둔다. */\n pathname: string;\n slots: readonly string[];\n breakpoint?: number;\n /** 메인 홈만 허용하며 locale 홈은 실제로 만든 exact 경로를 명시한다. */\n homePaths?: readonly string[];\n slideDurationMs?: number;\n hideMobileBannersOnScroll?: boolean;\n legacyDismissalKeyPrefixes?: readonly string[];\n children: ReactNode;\n}\n\nconst Context = createContext<ExposureRuntime | null>(null);\nconst emptyPrefixes: readonly string[] = [];\n\nexport function RootTaleExposureProvider({ endpoint, pathname, slots, breakpoint = 768, homePaths, slideDurationMs = 5000,\n hideMobileBannersOnScroll = true, legacyDismissalKeyPrefixes = emptyPrefixes, children }: RootTaleExposureProviderProps) {\n const [themeElement, setThemeElement] = useState<HTMLDivElement | null>(null);\n const [device, setDevice] = useState<\"desktop\" | \"mobile\">(\"desktop\");\n const [heights, setHeights] = useState<Record<string, { placement: \"top\" | \"bottom\"; height: number }>>({});\n useEffect(() => {\n const media = window.matchMedia(`(max-width: ${breakpoint - 1}px)`);\n const update = () => setDevice(media.matches ? \"mobile\" : \"desktop\");\n update(); media.addEventListener(\"change\", update);\n return () => media.removeEventListener(\"change\", update);\n }, [breakpoint]);\n const path = pathname.split(/[?#]/, 1)[0] || \"/\";\n const home = isExposureHome(path, exposureHomePaths(homePaths));\n const routeKey = `${path}:${device}`;\n const snapshot = useExposureDecision(endpoint, path, device, slots, home);\n const popupVisit = useRef<{ key: string; slot?: string }>({ key: routeKey });\n useEffect(() => { popupVisit.current = { key: routeKey }; }, [routeKey]);\n const now = useCallback(() => snapshot ? exposureServerNow(snapshot) : Date.now(), [snapshot]);\n const claimPopup = useCallback((slot: string) => {\n const claimed = popupVisit.current.slot;\n if (claimed && claimed !== slot) return false;\n popupVisit.current.slot = slot;\n return true;\n }, []);\n const setHeight = useCallback((key: string, placement: \"top\" | \"bottom\", height: number) => {\n setHeights((previous) => previous[key]?.height === height && previous[key]?.placement === placement ? previous : { ...previous, [key]: { placement, height } });\n }, []);\n const value: ExposureRuntime = { campaigns: liveCampaigns(snapshot), device, routeKey, home,\n slideDurationMs: Number.isFinite(slideDurationMs) ? slideDurationMs : 5000, hideMobileBannersOnScroll,\n now, claimPopup, setHeight, legacyPrefixes: legacyDismissalKeyPrefixes, themeElement };\n const top = Math.max(0, ...Object.values(heights).filter((item) => item.placement === \"top\").map((item) => item.height));\n const bottom = Math.max(0, ...Object.values(heights).filter((item) => item.placement === \"bottom\").map((item) => item.height));\n const style = { display: \"contents\", \"--rt-exposure-top-height\": `${top}px`, \"--rt-exposure-bottom-height\": `${bottom}px` } as CSSProperties;\n return <Context.Provider value={value}><div ref={setThemeElement} data-roottale-exposure-root=\"\" style={style}>{children}</div></Context.Provider>;\n}\n\nexport interface RootTaleExposureSlotProps {\n slotKey: string;\n placement: ExposurePlacement;\n allowedVariants: readonly string[];\n /** 배너를 닫아도 같은 방문에서 이전 공지를 이어서 표시하지 않는다. */\n fallback?: ReactNode;\n className?: string;\n}\n\nexport function RootTaleExposureSlot({ slotKey, placement, allowedVariants, fallback = null, className = \"\" }: RootTaleExposureSlotProps) {\n const runtime = useContext(Context);\n if (runtime && !runtime.home) return fallback;\n const campaigns = runtime?.campaigns.filter((item) => item.slotKey === slotKey && item.kind === (placement === \"popup\" ? \"popup\" : \"banner\") && allowedVariants.includes(item.content.variant)).slice(0, 20);\n if (!runtime || !campaigns?.length) return fallback;\n return <ExposureCampaignGroup key={`${runtime.routeKey}:${slotKey}`} campaigns={campaigns} runtime={runtime} placement={placement} className={className} />;\n}\n\n/** 기존 서버 슬롯의 client island. 신규 설치는 Provider/Slot을 사용한다. */\nexport function RootTaleLegacyExposure({ campaign, allowedVariants }: { campaign: CmsExposureCampaign; allowedVariants: readonly string[] }) {\n const [device, setDevice] = useState<\"desktop\" | \"mobile\">(\"desktop\");\n useEffect(() => { const media = window.matchMedia(\"(max-width: 767px)\"); const update = () => setDevice(media.matches ? \"mobile\" : \"desktop\"); update(); media.addEventListener(\"change\", update); return () => media.removeEventListener(\"change\", update); }, []);\n if (!allowedVariants.includes(campaign.content.variant)) return null;\n const runtime: ExposureRuntime = { campaigns: [campaign], device, routeKey: \"legacy\", home: true, slideDurationMs: 5000, hideMobileBannersOnScroll: true,\n now: Date.now, claimPopup: () => true, setHeight: () => undefined, legacyPrefixes: emptyPrefixes, themeElement: null };\n return <ExposureCampaignGroup key={campaign.id} campaigns={[campaign]} runtime={runtime} placement={campaign.kind === \"popup\" ? \"popup\" : \"inline\"} className=\"\" />;\n}\n","\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { readExposureDecision, type TimedExposureDecision } from \"./exposure-runtime-data\";\n\nexport function useExposureDecision(endpoint: string, pathname: string, device: \"mobile\" | \"desktop\", slots: readonly string[], enabled = true) {\n const slotsKey = slots.join(\",\");\n const requestKey = JSON.stringify([endpoint, pathname, device, slotsKey, enabled]);\n const [result, setResult] = useState<{ key: string; snapshot: TimedExposureDecision } | null>(null);\n\n useEffect(() => {\n if (!enabled) return;\n let disposed = false;\n let sequence = 0;\n let controller: AbortController | undefined;\n let pollTimer: ReturnType<typeof setTimeout> | undefined;\n let expiryTimer: ReturnType<typeof setTimeout> | undefined;\n let requestTimer: ReturnType<typeof setTimeout> | undefined;\n let current: TimedExposureDecision | null = null;\n const clearPoll = () => { clearTimeout(pollTimer); clearTimeout(requestTimer); controller?.abort(); sequence++; };\n\n function schedule(delay: number) {\n clearTimeout(pollTimer);\n if (!disposed && document.visibilityState !== \"hidden\") pollTimer = setTimeout(refresh, Math.max(100, Math.min(delay, 60_000)));\n }\n\n function expireKnownCampaigns() {\n if (!current) return;\n // Timer callbacks rerender at known expiry even while a replacement request is offline.\n setResult(performance.now() >= current.expiresAt ? null : { key: requestKey, snapshot: current });\n armExpiry();\n }\n\n function armExpiry() {\n clearTimeout(expiryTimer);\n if (!current || current.expiresAt <= performance.now()) return;\n const ends = current.decision.campaigns.flatMap((campaign) => campaign.endsAt ?\n [current!.receivedAt + Date.parse(campaign.endsAt) - current!.serverAtReceipt] : []);\n const future = [current.expiresAt, ...ends].filter((time) => time > performance.now());\n expiryTimer = setTimeout(expireKnownCampaigns, Math.max(1, Math.min(...future) - performance.now()));\n }\n\n async function refresh() {\n if (disposed || document.visibilityState === \"hidden\") return;\n clearPoll();\n if (current && performance.now() >= current.expiresAt) setResult(null);\n const seq = sequence;\n controller = new AbortController();\n const startedAt = performance.now();\n let refreshed = false;\n requestTimer = setTimeout(() => controller?.abort(), 10_000);\n try {\n const url = new URL(endpoint, window.location.origin);\n if (url.origin !== window.location.origin || url.username || url.password) throw new Error(\"Exposure endpoint must be same origin\");\n url.search = new URLSearchParams({ path: pathname, device }).toString();\n const response = await fetch(url, { cache: \"no-store\", credentials: \"same-origin\", signal: controller.signal });\n if (!response.ok) throw new Error(\"Exposure unavailable\");\n const payload: unknown = await response.json();\n if (disposed || seq !== sequence) return;\n const receivedAt = performance.now();\n current = readExposureDecision(payload, slotsKey.split(\",\"), receivedAt, receivedAt - startedAt);\n refreshed = current !== null;\n setResult(current ? { key: requestKey, snapshot: current } : null);\n armExpiry();\n } catch {\n if (disposed || seq !== sequence) return;\n if (!current || performance.now() >= current.expiresAt) { current = null; setResult(null); }\n } finally {\n if (!disposed && seq === sequence) {\n clearTimeout(requestTimer);\n const now = performance.now();\n const next = current?.nextAt;\n const boundaryDelay = next != null && next > now ? next - now : 60_000;\n // 오래 걸리는 요청에도 현재 노출이 만료 전에 갱신될 여유를 둔다.\n const refreshDelay = refreshed && current ? Math.max(1000, current.expiresAt - now - 10_000) : 60_000;\n schedule(Math.min(boundaryDelay, refreshDelay));\n }\n }\n }\n\n function onVisibility() {\n if (document.visibilityState === \"hidden\") clearPoll();\n else void refresh();\n }\n void refresh();\n document.addEventListener(\"visibilitychange\", onVisibility);\n window.addEventListener(\"focus\", refresh);\n return () => {\n disposed = true; clearPoll(); clearTimeout(expiryTimer);\n document.removeEventListener(\"visibilitychange\", onVisibility);\n window.removeEventListener(\"focus\", refresh);\n };\n }, [endpoint, pathname, device, slotsKey, requestKey, enabled]);\n\n // Layout survives client navigation: never paint the preceding route while effects catch up.\n return enabled && result?.key === requestKey ? result.snapshot : null;\n}\n","import type { CmsExposureCampaign, CmsExposureDecision } from \"@roottale/cms-client/server\";\n\nexport interface TimedExposureDecision {\n decision: CmsExposureDecision;\n receivedAt: number;\n serverAtReceipt: number;\n expiresAt: number;\n nextAt: number | null;\n}\n\nfunction validLink(value: unknown, internal = true): boolean {\n if (value == null) return true;\n if (typeof value !== \"string\" || /[\\u0000-\\u0020\\u007f\\\\]/.test(value)) return false;\n if (internal && value.startsWith(\"/\") && !value.startsWith(\"//\")) return true;\n try { const url = new URL(value); return url.protocol === \"https:\" && !url.username && !url.password; } catch { return false; }\n}\n\n/** 판정 응답이 손상돼도 페이지 본문은 유지하는 브라우저 입력 경계. */\nexport function readExposureDecision(value: unknown, slots: readonly string[], receivedAt: number, elapsed: number): TimedExposureDecision | null {\n if (!value || typeof value !== \"object\") return null;\n const data = value as CmsExposureDecision;\n const evaluated = Date.parse(data.evaluatedAt);\n const validUntil = Date.parse(data.validUntil);\n if (!Number.isFinite(evaluated) || !Number.isFinite(validUntil) || validUntil <= evaluated ||\n !Array.isArray(data.campaigns) || data.campaigns.length > 200) return null;\n // 편도 시간을 모르므로 왕복 전체를 차감한다. 늦은 응답으로 유효기간을 늘리지 않는다.\n const serverAtReceipt = evaluated + Math.max(0, elapsed);\n const expiresAt = receivedAt + Math.min(validUntil - serverAtReceipt, 120_000);\n if (expiresAt <= receivedAt) return null;\n const campaigns: CmsExposureCampaign[] = [];\n for (const item of data.campaigns) {\n if (!item || typeof item.id !== \"string\" || !slots.includes(item.slotKey) ||\n (item.kind !== \"banner\" && item.kind !== \"popup\") || !item.content ||\n typeof item.content.title !== \"string\" || typeof item.content.variant !== \"string\" ||\n ![\"always\", \"session\", \"day\", \"never\"].includes(item.repeat) ||\n !validLink(item.content.linkUrl) || !validLink(item.content.assetUrl, false) || !validLink(item.content.mobileAssetUrl, false)) return null;\n if (item.endsAt != null && !Number.isFinite(Date.parse(item.endsAt))) return null;\n if (item.delivery) {\n const delivery = item.delivery;\n if (delivery.version !== 2 || !Number.isFinite(delivery.delaySeconds) || delivery.delaySeconds < 0 || delivery.delaySeconds > 30 ||\n ![null, 25, 50, 75].includes(delivery.scrollPercent) || ![\"small\", \"medium\", \"large\"].includes(delivery.size) ||\n ![\"calendar\", \"rolling\"].includes(delivery.dayBoundary) || typeof delivery.dismissible !== \"boolean\" || typeof delivery.hideForToday !== \"boolean\") return null;\n try { new Intl.DateTimeFormat(\"en\", { timeZone: delivery.timeZone }); } catch { return null; }\n }\n const popupSlot = campaigns.find((existing) => existing.kind === \"popup\")?.slotKey;\n if (!campaigns.some((existing) => existing.id === item.id) &&\n campaigns.filter((existing) => existing.slotKey === item.slotKey).length < 20 &&\n (item.kind !== \"popup\" || popupSlot === undefined || popupSlot === item.slotKey)) campaigns.push(item);\n }\n const boundary = data.nextChangeAt == null ? null : Date.parse(data.nextChangeAt);\n if (boundary !== null && !Number.isFinite(boundary)) return null;\n return { decision: { ...data, campaigns }, receivedAt, serverAtReceipt, expiresAt,\n nextAt: boundary === null ? null : receivedAt + boundary - serverAtReceipt };\n}\n\nexport function exposureServerNow(snapshot: TimedExposureDecision): number {\n return snapshot.serverAtReceipt + performance.now() - snapshot.receivedAt;\n}\n\nexport function liveCampaigns(snapshot: TimedExposureDecision | null): CmsExposureCampaign[] {\n if (!snapshot || performance.now() >= snapshot.expiresAt) return [];\n const now = exposureServerNow(snapshot);\n return snapshot.decision.campaigns.filter((campaign) => !campaign.endsAt || Date.parse(campaign.endsAt) > now);\n}\n","\"use client\";\n\nimport { useEffect, useLayoutEffect, useRef, useState } from \"react\";\nimport type { CmsExposureCampaign } from \"@roottale/cms-client/server\";\nimport { ExposureContent, contentVariant, type ExposurePlacement } from \"./exposure-content\";\nimport { ExposureDialog, exposureCanOpenPopup, releaseExposurePopup, reserveExposurePopup } from \"./exposure-dialog\";\nimport { ExposureCarouselControls, useExposureCarousel } from \"./exposure-carousel\";\nimport type { ExposureRuntime } from \"./exposure-runtime-context\";\nimport { useMobileExposureScroll } from \"./exposure-scroll\";\nimport { exposureWasHidden, rememberExposureGroupDismissal, rememberExposureView } from \"./exposure-storage\";\n\nfunction useSlideScrollReset(campaignId: string | undefined) {\n const ref = useRef<HTMLDivElement>(null);\n useLayoutEffect(() => {\n // 모달·포커스는 유지하고 새 안내의 내용만 처음부터 읽을 수 있게 한다.\n if (ref.current) ref.current.scrollTop = 0;\n }, [campaignId]);\n return ref;\n}\n\nfunction exposureGroupSize(campaigns: readonly CmsExposureCampaign[]): \"small\" | \"medium\" | \"large\" {\n if (campaigns.some((campaign) => campaign.delivery?.size === \"large\")) return \"large\";\n if (campaigns.some((campaign) => (campaign.delivery?.size ?? \"medium\") === \"medium\")) return \"medium\";\n return \"small\";\n}\n\nexport function ExposureCampaignGroup({ campaigns, runtime, placement, className }: {\n campaigns: readonly CmsExposureCampaign[]; runtime: ExposureRuntime; placement: ExposurePlacement; className: string;\n}) {\n const [visible, setVisible] = useState(false);\n const [dismissed, setDismissed] = useState(false);\n const [height, setMeasuredHeight] = useState(0);\n const [inView, setInView] = useState(() => placement !== \"inline\" || typeof IntersectionObserver === \"undefined\");\n const measuredRef = useRef<HTMLDivElement>(null);\n const campaignIds = campaigns.map((campaign) => campaign.id).join(\",\");\n const [admission, setAdmission] = useState(() => ({ ids: campaignIds,\n allowed: new Map(campaigns.map((campaign) => [campaign.id, !exposureWasHidden(campaign, runtime.now(), runtime.legacyPrefixes)])) }));\n if (admission.ids !== campaignIds) {\n const allowed = new Map(admission.allowed);\n for (const campaign of campaigns) if (!allowed.has(campaign.id)) allowed.set(campaign.id, !exposureWasHidden(campaign, runtime.now(), runtime.legacyPrefixes));\n setAdmission({ ids: campaignIds, allowed });\n }\n const viewed = useRef(new Set<string>());\n const owner = useRef(Symbol(\"exposure-group\"));\n const latest = useRef({ campaigns, runtime });\n useEffect(() => { latest.current = { campaigns, runtime }; }, [campaigns, runtime]);\n const eligible = campaigns.filter((campaign) => admission.allowed.get(campaign.id));\n const first = eligible[0];\n const firstId = first?.id;\n const slotKey = campaigns[0]?.slotKey ?? \"\";\n const hiddenOnScroll = useMobileExposureScroll(runtime.device === \"mobile\" && runtime.hideMobileBannersOnScroll && (placement === \"top\" || placement === \"bottom\"));\n const active = visible && !dismissed && eligible.length > 0;\n const carousel = useExposureCarousel(eligible, active && !hiddenOnScroll && inView, runtime.slideDurationMs, placement === \"popup\");\n const campaign = carousel.campaign;\n const activeId = campaign?.id;\n const setHeight = runtime.setHeight;\n const device = runtime.device;\n const slideRef = useSlideScrollReset(activeId);\n\n useEffect(() => {\n const first = latest.current.campaigns.find((campaign) => campaign.id === firstId);\n if (visible || dismissed || !first) return;\n let opened = false;\n let delayPassed = false;\n const scrollPercent = first.delivery?.scrollPercent;\n const tryOpen = () => {\n if (opened || !delayPassed || document.visibilityState === \"hidden\") return;\n if (scrollPercent) {\n const scrollable = document.documentElement.scrollHeight - window.innerHeight;\n if (scrollable <= 0 || window.scrollY / scrollable * 100 < scrollPercent) return;\n }\n if (placement === \"popup\") {\n if (!exposureCanOpenPopup() || !reserveExposurePopup(owner.current)) return;\n if (!latest.current.runtime.claimPopup(first.slotKey)) { releaseExposurePopup(owner.current); return; }\n }\n opened = true; setVisible(true); clearInterval(retry);\n };\n const timer = setTimeout(() => { delayPassed = true; tryOpen(); }, placement === \"popup\" ? (first.delivery?.delaySeconds ?? 0) * 1000 : 0);\n const retry = setInterval(tryOpen, 500);\n window.addEventListener(\"scroll\", tryOpen, { passive: true });\n window.addEventListener(\"resize\", tryOpen); document.addEventListener(\"visibilitychange\", tryOpen);\n return () => { clearTimeout(timer); clearInterval(retry); window.removeEventListener(\"scroll\", tryOpen); window.removeEventListener(\"resize\", tryOpen); document.removeEventListener(\"visibilitychange\", tryOpen); };\n }, [firstId, visible, dismissed, placement]);\n\n useEffect(() => { const token = owner.current; return () => releaseExposurePopup(token); }, []);\n useEffect(() => {\n if (placement !== \"inline\" || !active) return;\n if (typeof IntersectionObserver === \"undefined\") return;\n const element = measuredRef.current;\n const observer = new IntersectionObserver(([entry]) => setInView(Boolean(entry?.isIntersecting)), { threshold: 0.1 });\n if (element) observer.observe(element);\n return () => observer.disconnect();\n }, [placement, active]);\n useEffect(() => {\n const campaign = latest.current.campaigns.find((item) => item.id === activeId);\n if (!active || hiddenOnScroll || !inView || !campaign || viewed.current.has(campaign.id)) return;\n // 현재 묶음의 방문 중 한 번 기록은 다음 슬라이드의 표시를 방해하지 않는다.\n viewed.current.add(campaign.id); rememberExposureView(campaign, latest.current.runtime.now());\n }, [active, hiddenOnScroll, inView, activeId]);\n\n useEffect(() => {\n if (placement !== \"top\" && placement !== \"bottom\") return;\n const element = measuredRef.current;\n const measure = () => {\n const rect = element?.getBoundingClientRect();\n const fullHeight = active && rect ? Math.ceil(rect.height) : 0;\n setMeasuredHeight((previous) => active ? Math.max(previous, fullHeight) : 0);\n setHeight(slotKey, placement, hiddenOnScroll ? 0 : placement === \"top\" && device === \"desktop\" && rect ? Math.max(0, Math.min(fullHeight, Math.ceil(rect.bottom))) : fullHeight);\n };\n measure();\n const observer = typeof ResizeObserver === \"undefined\" ? null : new ResizeObserver(measure);\n if (element) observer?.observe(element);\n window.addEventListener(\"resize\", measure); window.addEventListener(\"scroll\", measure, { passive: true });\n return () => { observer?.disconnect(); window.removeEventListener(\"resize\", measure); window.removeEventListener(\"scroll\", measure); setHeight(slotKey, placement, 0); };\n }, [active, placement, slotKey, setHeight, hiddenOnScroll, device]);\n\n function close(forToday = false) {\n releaseExposurePopup(owner.current);\n rememberExposureGroupDismissal(latest.current.campaigns, latest.current.runtime.now(), forToday);\n setDismissed(true);\n }\n if (!active || !campaign) return null;\n const closeControls = <>\n {placement === \"popup\" || placement === \"bottom\" || (campaign.delivery?.dismissible ?? true) ? <button type=\"button\" data-exposure-close=\"\" className=\"rt-exposure__close\" aria-label=\"안내 닫기\" onClick={() => close()}>×</button> : null}\n {placement === \"popup\" && campaigns.some((item) => item.delivery?.hideForToday) ? <button type=\"button\" className=\"rt-exposure__hide-today\" onClick={() => close(true)}>오늘 그만 보기</button> : null}\n </>;\n const body = <div className=\"rt-exposure-carousel\" aria-roledescription={eligible.length > 1 ? \"캐러셀\" : undefined} {...carousel.interactions}>\n <div ref={slideRef} className=\"rt-exposure-slide\" role=\"group\" aria-roledescription={eligible.length > 1 ? \"슬라이드\" : undefined} aria-label={`${carousel.index + 1} / ${eligible.length}`}>\n <ExposureContent key={campaign.id} campaign={campaign} device={runtime.device} />\n </div>\n {closeControls}\n <ExposureCarouselControls carousel={carousel} campaigns={eligible} />\n </div>;\n const classes = `rt-exposure rt-exposure--${campaign.kind} rt-exposure--${contentVariant(campaign)}`;\n if (placement === \"popup\") return <ExposureDialog title=\"홈페이지 안내\" className={`${classes} ${className}`} onClose={() => close()}\n device={runtime.device} size={exposureGroupSize(eligible)} themeElement={runtime.themeElement}>{body}</ExposureDialog>;\n return <div className={className} data-roottale-exposure-slot={slotKey} data-roottale-exposure-placement={placement}\n style={placement === \"bottom\" || runtime.device === \"mobile\" && placement === \"top\" ? { height } : { minHeight: height }}>\n <div ref={measuredRef} className={`rt-exposure-position rt-exposure-position--${placement}`} data-device={runtime.device}\n data-scroll-hidden={hiddenOnScroll ? \"true\" : \"false\"} aria-hidden={hiddenOnScroll || undefined} inert={hiddenOnScroll || undefined}>\n <aside className={classes} aria-label=\"홈페이지 안내\">{body}</aside>\n </div>\n </div>;\n}\n\nexport function RootTaleExposureCarouselPreview({ campaigns, placement = \"popup\", device = \"desktop\", slideDurationMs = 5000 }: {\n campaigns: readonly CmsExposureCampaign[]; placement?: ExposurePlacement; device?: \"desktop\" | \"mobile\"; slideDurationMs?: number;\n}) {\n const carousel = useExposureCarousel(campaigns, true, slideDurationMs, true);\n const campaign = carousel.campaign;\n const slideRef = useSlideScrollReset(campaign?.id);\n if (!campaign) return null;\n return <div className={`rt-exposure rt-exposure--${campaign.kind} rt-exposure--preview`} data-placement={placement} data-device={device} data-size={exposureGroupSize(campaigns)}>\n <div className=\"rt-exposure-carousel\" {...carousel.interactions}>\n <div ref={slideRef} className=\"rt-exposure-slide\">\n <ExposureContent key={campaign.id} campaign={campaign} device={device} />\n </div>\n <ExposureCarouselControls carousel={carousel} campaigns={campaigns} />\n </div>\n </div>;\n}\n","\"use client\";\n\nimport { useState, type ReactNode } from \"react\";\nimport type { CmsExposureCampaign } from \"@roottale/cms-client/server\";\n\nexport type ExposurePlacement = \"top\" | \"inline\" | \"bottom\" | \"popup\";\n\nexport function ExposureContent({ campaign, device, controls }: {\n campaign: CmsExposureCampaign;\n device: \"mobile\" | \"desktop\";\n controls?: ReactNode;\n}) {\n const { content } = campaign;\n const [failedUrls, setFailedUrls] = useState<readonly string[]>([]);\n const preferred = device === \"mobile\" && content.mobileAssetUrl ? content.mobileAssetUrl : content.assetUrl;\n const textOnly = campaign.delivery?.version === 2 && [\"notice\", \"card\"].includes(content.variant);\n const assetUrl = textOnly ? undefined : preferred && !failedUrls.includes(preferred) ? preferred : content.assetUrl && !failedUrls.includes(content.assetUrl) ? content.assetUrl : undefined;\n const assetAlt = device === \"mobile\" && assetUrl === content.mobileAssetUrl ? content.mobileAssetAlt ?? content.assetAlt : content.assetAlt;\n const imageOnly = content.variant === \"image\" && Boolean(assetUrl);\n const linkProps = { href: content.linkUrl, ...(content.linkTarget === \"blank\" ? { target: \"_blank\", rel: \"noopener noreferrer\" } : {}) };\n const asset = assetUrl ? <img className=\"rt-exposure__asset\" src={assetUrl} alt={assetAlt ?? content.title}\n onError={() => setFailedUrls((previous) => [...previous, assetUrl])} /> : null;\n return <div className={`rt-exposure__body${imageOnly ? \" rt-exposure__body--image\" : \"\"}`}>\n {imageOnly && content.linkUrl ? <a {...linkProps} aria-label={content.linkLabel ?? content.title}>{asset}</a> : asset}\n <div className={`rt-exposure__copy${imageOnly ? \" rt-exposure__sr-only\" : \"\"}`}>\n {content.eyebrow ? <p className=\"rt-exposure__eyebrow\">{content.eyebrow}</p> : null}\n <h2 className=\"rt-exposure__title\">{content.title}</h2>\n {content.description ? <p className=\"rt-exposure__description\">{content.description}</p> : null}\n {!imageOnly && content.linkUrl && content.linkLabel ? <a className=\"rt-exposure__link\" {...linkProps}>{content.linkLabel}</a> : null}\n </div>\n {controls}\n </div>;\n}\n\n/** 실제 콘텐츠 출력과 같지만 방문 기록·조회·포커스 이동이 없는 편집기 미리보기. */\nexport function RootTaleExposurePreview({ campaign, placement = campaign.kind === \"popup\" ? \"popup\" : \"inline\", device = \"desktop\", className = \"\" }: {\n campaign: CmsExposureCampaign;\n placement?: ExposurePlacement;\n device?: \"mobile\" | \"desktop\";\n className?: string;\n}) {\n return <div className={`rt-exposure rt-exposure--${campaign.kind} rt-exposure--${contentVariant(campaign)} rt-exposure--preview ${className}`}\n data-placement={placement} data-device={device} data-size={campaign.delivery?.size ?? \"medium\"}>\n <ExposureContent campaign={campaign} device={device} />\n </div>;\n}\n\nexport function contentVariant(campaign: CmsExposureCampaign): string {\n // 공개 문자열을 임의 CSS 클래스 목록으로 확장하지 않는다.\n return campaign.content.variant.replace(/[^a-zA-Z0-9_-]/g, \"\");\n}\n","\"use client\";\n\nimport { useEffect, useRef, type ReactNode } from \"react\";\nimport { createPortal } from \"react-dom\";\n\nlet activeDialog: HTMLElement | null = null;\nlet openingOwner: symbol | null = null;\n\nexport function reserveExposurePopup(owner: symbol): boolean {\n if (openingOwner !== null && openingOwner !== owner) return false;\n openingOwner = owner;\n return true;\n}\n\nexport function releaseExposurePopup(owner: symbol) {\n if (openingOwner === owner) openingOwner = null;\n}\n\nexport function exposureCanOpenPopup(): boolean {\n const focused = document.activeElement;\n if (focused instanceof HTMLElement && (focused.matches(\"input, textarea, select\") || focused.isContentEditable)) return false;\n if (activeDialog?.isConnected) return false;\n return !Array.from(document.querySelectorAll<HTMLElement>('dialog[open], [role=\"dialog\"][aria-modal=\"true\"]'))\n .some((element) => {\n // keepMounted Drawer처럼 DOM에 남은 닫힌 모달을 활성 모달로 세지 않는다.\n for (let ancestor: HTMLElement | null = element; ancestor; ancestor = ancestor.parentElement) {\n if (ancestor.hidden || ancestor.inert || ancestor.getAttribute(\"aria-hidden\") === \"true\") return false;\n const style = getComputedStyle(ancestor);\n if (style.display === \"none\" || style.visibility === \"hidden\" || style.visibility === \"collapse\") return false;\n }\n return true;\n });\n}\n\nexport function ExposureDialog({ title, className, children, onClose, device, size, themeElement }: {\n title: string; className: string; children: ReactNode; onClose: () => void;\n device: \"desktop\" | \"mobile\"; size: \"small\" | \"medium\" | \"large\";\n themeElement: HTMLElement | null;\n}) {\n const rootRef = useRef<HTMLDivElement>(null);\n const dialogRef = useRef<HTMLElement>(null);\n const closeRef = useRef(onClose);\n useEffect(() => { closeRef.current = onClose; }, [onClose]);\n useEffect(() => {\n const target = rootRef.current;\n if (!themeElement || !target) return;\n const computed = getComputedStyle(themeElement);\n target.style.fontFamily = computed.fontFamily;\n target.style.color = computed.color;\n for (let index = 0; index < computed.length; index++) {\n const key = computed.item(index);\n if (key.startsWith(\"--\")) target.style.setProperty(key, computed.getPropertyValue(key));\n }\n }, [themeElement]);\n useEffect(() => {\n const root = rootRef.current;\n const dialog = dialogRef.current;\n if (!root || !dialog) return;\n activeDialog = dialog;\n const previous = document.activeElement instanceof HTMLElement ? document.activeElement : null;\n const previousOverflow = document.body.style.overflow;\n const previousPadding = document.body.style.paddingRight;\n const gutter = Math.max(0, window.innerWidth - document.documentElement.clientWidth);\n const padding = Number.parseFloat(getComputedStyle(document.body).paddingRight) || 0;\n document.body.style.overflow = \"hidden\";\n if (gutter > 0) document.body.style.paddingRight = `${padding + gutter}px`;\n const inertBefore = new Map<HTMLElement, boolean>();\n function inertBackground() {\n for (const child of document.body.children) {\n if (!(child instanceof HTMLElement) || child === root || child.contains(root) || inertBefore.has(child)) continue;\n inertBefore.set(child, child.inert); child.inert = true;\n }\n }\n inertBackground();\n const observer = new MutationObserver(inertBackground);\n observer.observe(document.body, { childList: true });\n const focusable = () => Array.from(dialog.querySelectorAll<HTMLElement>('a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex=\"0\"]'))\n .filter((element) => !element.hidden && !element.closest('[hidden], [aria-hidden=\"true\"]'));\n (dialog.querySelector<HTMLElement>('[data-exposure-close]') ?? dialog).focus({ preventScroll: true });\n const handleKey = (event: KeyboardEvent) => {\n if (event.key === \"Escape\") { event.preventDefault(); event.stopPropagation(); closeRef.current(); }\n if (event.key !== \"Tab\") return;\n const items = focusable();\n const first = items[0]; const last = items[items.length - 1];\n if (!first || !last) { event.preventDefault(); dialog.focus(); }\n else if (event.shiftKey && (document.activeElement === first || document.activeElement === dialog)) { event.preventDefault(); last.focus(); }\n else if (!event.shiftKey && (document.activeElement === last || document.activeElement === dialog)) { event.preventDefault(); first.focus(); }\n };\n const handleFocus = (event: FocusEvent) => {\n if (event.target instanceof Node && !dialog.contains(event.target)) (focusable()[0] ?? dialog).focus({ preventScroll: true });\n };\n document.addEventListener(\"keydown\", handleKey, true);\n document.addEventListener(\"focusin\", handleFocus);\n return () => {\n observer.disconnect();\n document.removeEventListener(\"keydown\", handleKey, true);\n document.removeEventListener(\"focusin\", handleFocus);\n for (const [element, wasInert] of inertBefore) element.inert = wasInert;\n if (activeDialog === dialog) activeDialog = null;\n document.body.style.overflow = previousOverflow;\n document.body.style.paddingRight = previousPadding;\n if (previous?.isConnected && !previous.inert) previous.focus({ preventScroll: true });\n else {\n const fallback = document.querySelector<HTMLElement>('main, [role=\"main\"]') ?? document.body;\n const hadTabIndex = fallback.hasAttribute(\"tabindex\");\n if (!hadTabIndex) fallback.tabIndex = -1;\n fallback.focus({ preventScroll: true });\n if (!hadTabIndex) fallback.removeAttribute(\"tabindex\");\n }\n };\n }, []);\n\n return createPortal(<div ref={rootRef} className=\"rt-exposure-backdrop\" data-device={device}>\n <section ref={dialogRef} className={className} role=\"dialog\" aria-modal=\"true\" aria-label={title} tabIndex={-1} data-size={size}>\n {children}\n </section>\n </div>, document.body);\n}\n","\"use client\";\n\nimport { useCallback, useEffect, useRef, useState, type FocusEvent, type TouchEvent } from \"react\";\nimport type { CmsExposureCampaign } from \"@roottale/cms-client/server\";\nimport { exposureCanOpenPopup } from \"./exposure-dialog\";\n\nexport function useExposureCarousel(campaigns: readonly CmsExposureCampaign[], enabled: boolean, durationMs: number, popup: boolean) {\n const [selection, setSelection] = useState({ id: campaigns[0]?.id, cycle: 0 });\n const [elapsedState, setElapsed] = useState({ id: campaigns[0]?.id, value: 0 });\n const [userPaused, setUserPaused] = useState(false);\n const [motionAllowed, setMotionAllowed] = useState(false);\n const [reducedMotion, setReducedMotion] = useState(false);\n const [hovered, setHovered] = useState(false);\n const [focused, setFocused] = useState(false);\n const [touching, setTouching] = useState(false);\n const [environmentPaused, setEnvironmentPaused] = useState(false);\n const elapsedRef = useRef(0);\n const touchOrigin = useRef<{ x: number; y: number } | null>(null);\n const index = Math.max(0, campaigns.findIndex((campaign) => campaign.id === selection.id));\n const campaign = campaigns[index];\n const elapsed = elapsedState.id === campaign?.id ? elapsedState.value : 0;\n const latest = useRef({ campaigns, index });\n useEffect(() => { latest.current = { campaigns, index }; }, [campaigns, index]);\n const duration = Math.max(3000, Math.min(30_000, durationMs));\n const paused = userPaused || hovered || focused || touching || environmentPaused || (reducedMotion && !motionAllowed);\n\n const select = useCallback((nextIndex: number) => {\n const items = latest.current.campaigns;\n if (!items.length) return;\n const normalized = (nextIndex + items.length) % items.length;\n latest.current.index = normalized;\n elapsedRef.current = 0; setElapsed({ id: items[normalized]!.id, value: 0 });\n setSelection((previous) => ({ id: items[normalized]!.id, cycle: previous.cycle + 1 }));\n }, []);\n const next = useCallback(() => select(latest.current.index + 1), [select]);\n const previous = useCallback(() => select(latest.current.index - 1), [select]);\n\n useEffect(() => {\n const media = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n const changed = () => setReducedMotion(media.matches);\n changed(); media.addEventListener(\"change\", changed);\n return () => media.removeEventListener(\"change\", changed);\n }, []);\n\n useEffect(() => {\n const changed = () => setEnvironmentPaused(document.visibilityState === \"hidden\" || (!popup && !exposureCanOpenPopup()));\n changed();\n const check = setInterval(changed, 250);\n document.addEventListener(\"visibilitychange\", changed);\n return () => { clearInterval(check); document.removeEventListener(\"visibilitychange\", changed); };\n }, [popup]);\n\n useEffect(() => { elapsedRef.current = 0; }, [campaign?.id, selection.cycle]);\n useEffect(() => {\n if (!enabled || paused || campaigns.length < 2) return;\n let previousTick = performance.now();\n const timer = setInterval(() => {\n const now = performance.now();\n elapsedRef.current += now - previousTick; previousTick = now;\n if (elapsedRef.current >= duration) next();\n else setElapsed({ id: campaign?.id, value: elapsedRef.current });\n }, 100);\n return () => clearInterval(timer);\n }, [campaign?.id, campaigns.length, enabled, paused, duration, next, selection.cycle]);\n\n function onTouchStart(event: TouchEvent<HTMLElement>) {\n const touch = event.touches[0];\n if (touch) { touchOrigin.current = { x: touch.clientX, y: touch.clientY }; setTouching(true); }\n }\n function onTouchEnd(event: TouchEvent<HTMLElement>) {\n const origin = touchOrigin.current; const touch = event.changedTouches[0];\n touchOrigin.current = null; setTouching(false);\n if (!origin || !touch || campaigns.length < 2) return;\n const dx = touch.clientX - origin.x; const dy = touch.clientY - origin.y;\n if (Math.abs(dx) >= 48 && Math.abs(dx) > Math.abs(dy) * 1.4) { if (dx < 0) next(); else previous(); }\n }\n function onFocus(event: FocusEvent<HTMLElement>) {\n // 모달을 열며 옮긴 닫기 버튼의 초기 포커스는 자동 전환을 막지 않는다.\n if (!(event.target instanceof HTMLElement)) return;\n if (event.target.closest(\"[data-exposure-close], .rt-exposure__hide-today\")) { setFocused(false); return; }\n setFocused(true);\n }\n const interactions = {\n onPointerEnter: (event: { pointerType: string }) => { if (event.pointerType === \"mouse\") setHovered(true); },\n onPointerLeave: () => setHovered(false),\n onFocusCapture: onFocus,\n onBlurCapture: (event: FocusEvent<HTMLElement>) => { if (!(event.relatedTarget instanceof Node) || !event.currentTarget.contains(event.relatedTarget)) setFocused(false); },\n onTouchStart, onTouchEnd,\n onTouchCancel: () => { touchOrigin.current = null; setTouching(false); },\n };\n const toggle = () => {\n // 재생을 명시적으로 누른 경우 그 버튼의 초점은 재생 요청을 다시 막지 않는다.\n setFocused(false);\n setHovered(false);\n if (reducedMotion && !motionAllowed) { setMotionAllowed(true); setUserPaused(false); }\n else setUserPaused((value) => !value);\n };\n return { campaign, index, paused, userPaused: userPaused || (reducedMotion && !motionAllowed), progress: Math.min(1, elapsed / duration),\n seconds: Math.max(1, Math.ceil((duration - elapsed) / 1000)), next, previous, select, toggle, interactions };\n}\n\nexport function ExposureCarouselControls({ carousel, campaigns }: { carousel: ReturnType<typeof useExposureCarousel>; campaigns: readonly CmsExposureCampaign[] }) {\n if (campaigns.length < 2) return null;\n const firstDot = Math.max(0, Math.min(campaigns.length - 5, carousel.index - 2));\n return <div className=\"rt-exposure-carousel-controls\">\n <div className=\"rt-exposure-carousel-meta\">\n <span className=\"rt-exposure-carousel-count\" aria-live={carousel.paused ? \"polite\" : \"off\"}><strong>{String(carousel.index + 1).padStart(2, \"0\")}</strong><span> / {String(campaigns.length).padStart(2, \"0\")}</span></span>\n <span className=\"rt-exposure-carousel-time\">{carousel.paused ? \"전환 일시정지\" : `${carousel.seconds}초 후 다음 안내`}</span>\n <div className=\"rt-exposure-carousel-actions\">\n <button type=\"button\" aria-label=\"이전 안내\" onClick={carousel.previous}><svg viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path d=\"m14 6-6 6 6 6\" /></svg></button>\n <button type=\"button\" aria-label={carousel.userPaused ? \"자동 전환 재생\" : \"자동 전환 일시정지\"} onClick={carousel.toggle} aria-pressed={carousel.userPaused}>\n <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\">{carousel.userPaused ? <path d=\"m9 5 10 7-10 7Z\" /> : <path d=\"M9 5v14M15 5v14\" />}</svg>\n </button>\n <button type=\"button\" aria-label=\"다음 안내\" onClick={carousel.next}><svg viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path d=\"m10 6 6 6-6 6\" /></svg></button>\n </div>\n </div>\n <div className=\"rt-exposure-carousel-progress\" role=\"progressbar\" aria-label=\"다음 안내까지 진행\" aria-valuemin={0} aria-valuemax={100} aria-valuenow={Math.round(carousel.progress * 100)}>\n <span style={{ transform: `scaleX(${carousel.progress})` }} />\n </div>\n <div className=\"rt-exposure-carousel-dots\" aria-label=\"안내 선택\">\n {campaigns.slice(firstDot, firstDot + 5).map((campaign, offset) => { const index = firstDot + offset; return <button key={campaign.id} type=\"button\" aria-label={`${index + 1}번째 안내: ${campaign.content.title}`} aria-current={index === carousel.index ? \"true\" : undefined} onClick={() => carousel.select(index)}><span /></button>; })}\n </div>\n </div>;\n}\n","\"use client\";\nimport { useEffect, useState } from \"react\";\n\nexport function useMobileExposureScroll(enabled: boolean): boolean {\n const [hidden, setHidden] = useState(false);\n useEffect(() => {\n if (!enabled) { setHidden(false); return; }\n let previous = Math.max(0, window.scrollY);\n let distance = 0;\n const onScroll = () => {\n const current = Math.max(0, window.scrollY);\n const delta = current - previous; previous = current;\n if (current < 32) { distance = 0; setHidden(false); return; }\n if (Math.sign(delta) !== Math.sign(distance)) distance = 0;\n distance += delta;\n if (distance >= 12) { setHidden(true); distance = 0; }\n else if (distance <= -8) { setHidden(false); distance = 0; }\n };\n window.addEventListener(\"scroll\", onScroll, { passive: true });\n return () => window.removeEventListener(\"scroll\", onScroll);\n }, [enabled]);\n return enabled && hidden;\n}\n","import type { CmsExposureCampaign } from \"@roottale/cms-client/server\";\n\nfunction read(kind: \"session\" | \"local\", key: string): string | null {\n try {\n const store = kind === \"session\" ? sessionStorage : localStorage;\n return store.getItem(key);\n } catch { return null; }\n}\n\nconst cookieFallback = new Map<string, string>();\nconst sessionCookie = \"rt_exposure_session\";\nconst preferencesCookie = \"rt_exposure_preferences\";\n\nfunction compactKey(value: string, scope: \"c\" | \"g\"): string {\n // 비밀이나 권한이 아닌 UI 기록 식별자. 64bit로 줄여 다수 슬라이드도 쿠키 한도 안에 둔다.\n let left = 2166136261; let right = 2246822519;\n for (let index = 0; index < value.length; index++) {\n left = Math.imul(left ^ value.charCodeAt(index), 16777619);\n right = Math.imul(right ^ value.charCodeAt(index), 3266489917);\n }\n return `${scope}${(left >>> 0).toString(16).padStart(8, \"0\")}${(right >>> 0).toString(16).padStart(8, \"0\")}`;\n}\n\nfunction rawCookie(name: string): string | null {\n const entry = document.cookie.split(\";\").map((part) => part.trim()).find((part) => part.startsWith(`${name}=`));\n return entry ? entry.slice(name.length + 1) : null;\n}\n\nfunction readPreferences(name: string): Map<string, string> {\n let raw: string | null;\n try {\n raw = cookieFallback.get(name) ?? rawCookie(name) ?? null;\n } catch { raw = cookieFallback.get(name) ?? null; }\n const result = new Map<string, string>();\n for (const entry of raw?.split(\".\") ?? []) {\n const match = /^([cg][a-f0-9]{16})_([svun])([0-9a-z]+)$/.exec(entry);\n if (match) result.set(match[1]!, `${match[2]}:${match[2] === \"u\" ? Number.parseInt(match[3]!, 36) * 1000 : 1}`);\n }\n return result;\n}\n\nfunction writePreferenceKey(key: string, value: string, persistent: boolean, now: number) {\n const name = persistent ? preferencesCookie : sessionCookie;\n const values = readPreferences(name);\n for (const [id, preference] of values) if (preference.startsWith(\"u:\") && Number(preference.slice(2)) <= now) values.delete(id);\n values.delete(key); values.set(key, value);\n const packed = [...values].slice(-100).map(([id, preference]) => `${id}_${preference[0]}${preference.startsWith(\"u:\") ? Math.ceil(Number(preference.slice(2)) / 1000).toString(36) : \"1\"}`).join(\".\");\n try {\n const secure = location.protocol === \"https:\" ? \"; Secure\" : \"\";\n document.cookie = `${name}=${packed}; Path=/; SameSite=Lax${secure}${persistent ? \"; Max-Age=34560000\" : \"\"}`;\n if (rawCookie(name) !== packed) cookieFallback.set(name, packed);\n else cookieFallback.delete(name);\n } catch { cookieFallback.set(name, packed); }\n}\n\nfunction dateInZone(epoch: number, timeZone: string): string {\n return new Intl.DateTimeFormat(\"en-CA\", { timeZone, year: \"numeric\", month: \"2-digit\", day: \"2-digit\" }).format(epoch);\n}\n\n/** DST로 하루가 23/25시간인 곳도 날짜가 바뀌는 첫 시각을 찾는다. */\nexport function nextExposureMidnight(now: number, timeZone: string): number {\n const today = dateInZone(now, timeZone);\n let low = now;\n let high = now + 48 * 3_600_000;\n while (high - low > 1) {\n const mid = Math.floor((low + high) / 2);\n if (dateInZone(mid, timeZone) === today) low = mid;\n else high = mid;\n }\n return high;\n}\n\nexport function exposureWasHidden(campaign: CmsExposureCampaign, now: number, legacyPrefixes: readonly string[] = []): boolean {\n const persistent = readPreferences(preferencesCookie);\n const preferences = [persistent.get(compactKey(campaign.slotKey, \"g\")), persistent.get(compactKey(campaign.id, \"c\")), readPreferences(sessionCookie).get(compactKey(campaign.id, \"c\"))];\n if (preferences.some((preference) => preference?.startsWith(\"s:\") || preference?.startsWith(\"v:\") || preference === \"n:1\" || (preference?.startsWith(\"u:\") && Number(preference.slice(2)) > now))) return true;\n // 종전 local/sessionStorage 기록은 읽어서 호환한다. 새 기록은 쿠키만 쓴다.\n const until = Number(read(\"local\", `rt-exposure-until:${campaign.id}`));\n if (Number.isFinite(until) && until > now) return true;\n if (campaign.repeat === \"always\") return false;\n if (campaign.repeat === \"session\" && read(\"session\", `rt-exposure-viewed:${campaign.id}`)) return true;\n const kind = campaign.repeat === \"session\" ? \"session\" : \"local\";\n for (const prefix of [\"rt-exposure-dismissed:\", ...legacyPrefixes]) {\n const value = read(kind, `${prefix}${campaign.id}`);\n if (value === null) continue;\n if (campaign.repeat !== \"day\") return true;\n const dismissedAt = Number(value);\n // 레거시 접두어·delivery 없는 기존 항목은 종전의 24시간 의미를 보존한다.\n const calendar = prefix === \"rt-exposure-dismissed:\" && campaign.delivery?.dayBoundary === \"calendar\";\n if (Number.isFinite(dismissedAt) && (calendar ? dateInZone(dismissedAt, campaign.delivery!.timeZone) === dateInZone(now, campaign.delivery!.timeZone) : now - dismissedAt < 86_400_000)) return true;\n }\n return false;\n}\n\nexport function rememberExposureView(campaign: CmsExposureCampaign, now: number) {\n if (campaign.repeat === \"session\") writePreferenceKey(compactKey(campaign.id, \"c\"), \"v:1\", false, now);\n // 새 표시 규칙의 일/영구 빈도는 방문자가 닫지 않고 이동해도 실제 노출을 센다.\n if (campaign.delivery && (campaign.repeat === \"day\" || campaign.repeat === \"never\")) rememberExposureDismissal(campaign, now);\n}\n\nexport function rememberExposureDismissal(campaign: CmsExposureCampaign, now: number, forToday = false) {\n if (forToday || campaign.repeat === \"day\") {\n const until = forToday || campaign.delivery?.dayBoundary === \"calendar\" ? nextExposureMidnight(now, campaign.delivery?.timeZone ?? \"Asia/Seoul\") : now + 86_400_000;\n writePreferenceKey(compactKey(campaign.id, \"c\"), `u:${until}`, true, now);\n } else if (campaign.repeat === \"never\") writePreferenceKey(compactKey(campaign.id, \"c\"), \"n:1\", true, now);\n else writePreferenceKey(compactKey(campaign.id, \"c\"), \"s:1\", false, now);\n}\n\n/** 현재 보이는 묶음 전체를 닫는다. 새 캠페인 ID의 다음 방문까지 숨기지는 않는다. */\nexport function rememberExposureGroupDismissal(campaigns: readonly CmsExposureCampaign[], now: number, forToday = false) {\n if (forToday && campaigns[0]) {\n writePreferenceKey(compactKey(campaigns[0].slotKey, \"g\"), `u:${nextExposureMidnight(now, campaigns[0].delivery?.timeZone ?? \"Asia/Seoul\")}`, true, now);\n return;\n }\n for (const campaign of campaigns) rememberExposureDismissal(campaign, now, forToday);\n}\n"],"mappings":"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","names":["useCallback","useEffect","useRef","useState","useEffect","useRef","useState","useState","useEffect","jsx","useEffect","useRef","useState","jsx","jsxs","useState","useRef","campaign","useEffect","previous","useEffect","useState","jsx","jsxs","useRef","useState","campaign","useEffect","first","jsx","useState","useEffect","useRef","useCallback"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/dist/cms-public.css
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* @roottale/cms-renderer-next/styles — scoped CSS for public CMS components.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* CMS 문서 스타일은 `[data-roottale-cms]` 아래에 둔다. 팝업 포털과 배너는
|
|
5
|
+
* 독립적인 `.rt-exposure*` 이름을 쓰며 공유 보조 클래스는 `.rt-*`로 구분한다.
|
|
6
|
+
* 문서 선택자는 `:where()`로 명시도를 낮춘다(ADR-0023 §5.1 #10).
|
|
7
|
+
* 노출 UI는 테마 변수와 해당 클래스 범위에서 고객 사이트가 조정할 수 있다.
|
|
8
|
+
* `!important`는 사용자의 동작 줄이기 설정을 보장하는 규칙에만 사용한다.
|
|
9
9
|
*
|
|
10
10
|
* Token contract (zero-dependency):
|
|
11
11
|
* Every `--rt-*` variable referenced below has a static fallback. The
|
|
@@ -26,17 +26,115 @@
|
|
|
26
26
|
.rt-sr-only { position: absolute; inline-size: 1px; block-size: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
|
|
27
27
|
.rt-exposure { color: var(--rt-color-foreground, #111827); background: var(--rt-color-background, #fff); border: 1px solid var(--rt-color-border, #e5e7eb); }
|
|
28
28
|
.rt-exposure__body { position: relative; display: grid; gap: 1rem; padding: 1rem; }
|
|
29
|
-
.rt-exposure__asset { max-inline-size: 100%; block-size: auto; border-radius: .5rem; }
|
|
30
|
-
.rt-exposure__copy { display: grid; gap: .5rem; }
|
|
29
|
+
.rt-exposure__asset { display: block; max-inline-size: 100%; inline-size: auto; block-size: auto; max-block-size: var(--rt-exposure-image-max-height, 60dvh); object-fit: contain; border-radius: .5rem; margin-inline: auto; }
|
|
30
|
+
.rt-exposure__copy { --rt-exposure-copy-gap: .5rem; display: grid; gap: var(--rt-exposure-copy-gap, .5rem); min-inline-size: 0; padding-inline-end: 2.25rem; overflow-wrap: anywhere; }
|
|
31
31
|
.rt-exposure__eyebrow, .rt-exposure__title, .rt-exposure__description { margin: 0; }
|
|
32
|
-
.rt-exposure__eyebrow { font-size: .75rem; font-weight:
|
|
33
|
-
.rt-exposure__title { font-size: 1.25rem; line-height: 1.
|
|
34
|
-
.rt-
|
|
32
|
+
.rt-exposure__eyebrow { font-size: .75rem; font-weight: 400; }
|
|
33
|
+
.rt-exposure__title { font-size: 1.25rem; line-height: 1.4; font-weight: 400; letter-spacing: -.02em; }
|
|
34
|
+
.rt-exposure__description { font-size: .9375rem; line-height: 1.65; white-space: pre-line; }
|
|
35
|
+
.rt-exposure__link { inline-size: fit-content; font-weight: 400; color: var(--rt-color-primary, currentColor); text-underline-offset: .2em; }
|
|
36
|
+
.rt-exposure__description + .rt-exposure__link { margin-block-start: var(--rt-exposure-copy-gap, .5rem); }
|
|
35
37
|
.rt-exposure__close { position: absolute; inset-block-start: .5rem; inset-inline-end: .5rem; inline-size: 2.75rem; block-size: 2.75rem; border: 0; border-radius: 999px; background: color-mix(in srgb, var(--rt-color-background, #fff) 85%, transparent); color: inherit; cursor: pointer; font-size: 1.5rem; }
|
|
38
|
+
.rt-exposure__close:focus-visible, .rt-exposure__link:focus-visible, .rt-exposure__hide-today:focus-visible { outline: 2px solid currentColor; outline-offset: 3px; }
|
|
39
|
+
.rt-exposure__hide-today { display: block; margin-inline-start: auto; min-block-size: 2.75rem; padding: .5rem 1rem; border: 0; background: transparent; color: inherit; font: inherit; font-size: .8125rem; text-decoration: underline; text-underline-offset: .2em; cursor: pointer; }
|
|
40
|
+
.rt-exposure__body--image { padding: 0; }
|
|
41
|
+
.rt-exposure__body--image .rt-exposure__hide-today { margin: 0 .5rem .25rem; }
|
|
42
|
+
.rt-exposure__sr-only { position: absolute; inline-size: 1px; block-size: 1px; padding: 0; margin: -1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }
|
|
36
43
|
.rt-exposure-backdrop { position: fixed; inset: 0; z-index: 9999; display: grid; place-items: center; padding: 1rem; background: rgb(0 0 0 / .55); }
|
|
37
|
-
.rt-exposure--popup { inline-size: min(100%, 34rem); max-block-size: calc(100dvh - 2rem); overflow:
|
|
38
|
-
|
|
39
|
-
|
|
44
|
+
.rt-exposure--popup { display: flex; flex-direction: column; inline-size: min(100%, var(--rt-exposure-popup-width, 34rem)); max-block-size: calc(100dvh - 2rem); overflow: hidden; overscroll-behavior: contain; border-radius: 1rem; box-shadow: 0 1.5rem 4rem rgb(0 0 0 / .25); }
|
|
45
|
+
.rt-exposure--popup > .rt-exposure-carousel { display: flex; flex-direction: column; min-block-size: 0; }
|
|
46
|
+
.rt-exposure--popup .rt-exposure-slide { min-block-size: 0; overflow: auto; overscroll-behavior: contain; }
|
|
47
|
+
.rt-exposure--popup .rt-exposure-carousel-controls, .rt-exposure--popup .rt-exposure__hide-today { flex-shrink: 0; }
|
|
48
|
+
.rt-exposure--popup.rt-exposure--preview > .rt-exposure__body { min-block-size: 0; overflow: auto; }
|
|
49
|
+
.rt-exposure--popup[data-size="small"] { --rt-exposure-popup-width: 26rem; }
|
|
50
|
+
.rt-exposure--popup[data-size="large"] { --rt-exposure-popup-width: 46rem; }
|
|
51
|
+
.rt-exposure-backdrop[data-device="mobile"] { align-items: end; padding: max(1rem, env(safe-area-inset-top)) 0 0; }
|
|
52
|
+
.rt-exposure-backdrop[data-device="mobile"] .rt-exposure--popup { inline-size: 100%; max-block-size: calc(90dvh - env(safe-area-inset-top)); border-radius: 1rem 1rem 0 0; padding-block-end: env(safe-area-inset-bottom); }
|
|
53
|
+
.rt-exposure-position--top { position: relative; z-index: var(--rt-exposure-top-z-index, 50); }
|
|
54
|
+
.rt-exposure-position--bottom { position: fixed; inset-inline: 0; inset-block-end: 0; z-index: var(--rt-exposure-bottom-z-index, 60); }
|
|
55
|
+
.rt-exposure-position--bottom > .rt-exposure { padding-block-end: env(safe-area-inset-bottom); }
|
|
56
|
+
.rt-exposure-position--top, .rt-exposure-position--bottom { --rt-exposure-image-max-height: var(--rt-exposure-banner-max-height, 12rem); }
|
|
57
|
+
.rt-exposure-position--inline { inline-size: 100%; }
|
|
58
|
+
.rt-exposure--preview { position: relative; max-inline-size: 100%; }
|
|
59
|
+
.rt-exposure-carousel { position: relative; font: inherit; touch-action: pan-y pinch-zoom; }
|
|
60
|
+
.rt-exposure-slide { min-inline-size: 0; }
|
|
61
|
+
.rt-exposure-slide > .rt-exposure__body { animation: rt-exposure-reveal 180ms ease-out; }
|
|
62
|
+
.rt-exposure-carousel-controls { padding: .375rem 1rem .5rem; border-block-start: 1px solid var(--rt-color-border, rgb(0 0 0 / .1)); }
|
|
63
|
+
.rt-exposure-carousel-meta { display: flex; align-items: center; gap: .75rem; min-block-size: 2.75rem; }
|
|
64
|
+
.rt-exposure-carousel-count { white-space: nowrap; font-size: .75rem; font-variant-numeric: tabular-nums; }
|
|
65
|
+
.rt-exposure-carousel-count strong { font-weight: 400; }
|
|
66
|
+
.rt-exposure-carousel-time { margin-inline-end: auto; font-size: .6875rem; font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
67
|
+
.rt-exposure-carousel-actions { display: flex; flex-shrink: 0; gap: 0; }
|
|
68
|
+
.rt-exposure-carousel-actions button { display: grid; place-items: center; inline-size: 2.75rem; block-size: 2.75rem; padding: .75rem; border: 0; background: transparent; color: inherit; border-radius: .25rem; cursor: pointer; }
|
|
69
|
+
.rt-exposure-carousel-actions svg { inline-size: 1rem; block-size: 1rem; fill: none; stroke: currentColor; stroke-width: 1.4; stroke-linecap: round; stroke-linejoin: round; }
|
|
70
|
+
.rt-exposure-carousel-progress { block-size: 2px; overflow: hidden; background: var(--rt-color-border, rgb(0 0 0 / .1)); }
|
|
71
|
+
.rt-exposure-carousel-progress > span { display: block; block-size: 100%; background: var(--rt-color-primary, currentColor); transform-origin: left; transition: transform 100ms linear; }
|
|
72
|
+
.rt-exposure-carousel-dots { display: flex; justify-content: center; align-items: center; }
|
|
73
|
+
.rt-exposure-carousel-dots button { display: grid; place-items: center; inline-size: 2.25rem; block-size: 2.75rem; padding: .75rem; border: 0; background: transparent; color: inherit; cursor: pointer; }
|
|
74
|
+
.rt-exposure-carousel-dots button > span { box-sizing: border-box; inline-size: .25rem; block-size: .25rem; border: 1px solid currentColor; border-radius: 50%; background: transparent; }
|
|
75
|
+
.rt-exposure-carousel-dots button[aria-current="true"] > span { background: currentColor; }
|
|
76
|
+
.rt-exposure-carousel button:focus-visible { outline: 1px solid currentColor; outline-offset: 2px; }
|
|
77
|
+
.rt-exposure-position { transition: transform 180ms ease, opacity 180ms ease; }
|
|
78
|
+
.rt-exposure-position--top[data-device="mobile"] { position: fixed; inset-inline: 0; inset-block-start: var(--rt-exposure-top-offset, 0px); }
|
|
79
|
+
.rt-exposure-position[data-scroll-hidden="true"] { opacity: 0; pointer-events: none; visibility: hidden; }
|
|
80
|
+
.rt-exposure-position--top[data-scroll-hidden="true"] { transform: translateY(-100%); }
|
|
81
|
+
.rt-exposure-position--bottom[data-scroll-hidden="true"] { transform: translateY(100%); }
|
|
82
|
+
/* 모바일 고정 안내 하나의 높이 예산은 화면의 24%다.
|
|
83
|
+
짧은 안내는 자연 높이, 긴 안내는 본문만 스크롤한다. 조작 행은 2.75rem을 유지한다. */
|
|
84
|
+
:is(.rt-exposure-position--top, .rt-exposure-position--bottom)[data-device="mobile"] > .rt-exposure,
|
|
85
|
+
.rt-exposure--preview[data-device="mobile"]:is([data-placement="top"], [data-placement="bottom"]) {
|
|
86
|
+
--rt-exposure-fixed-controls-height: calc(3rem + 3px);
|
|
87
|
+
--rt-exposure-image-max-height: min(var(--rt-exposure-banner-max-height, 12rem), 12dvh);
|
|
88
|
+
box-sizing: border-box;
|
|
89
|
+
display: flex;
|
|
90
|
+
flex-direction: column;
|
|
91
|
+
max-block-size: max(24dvh, calc(2.75rem + var(--rt-exposure-fixed-controls-height) + 2px + var(--rt-exposure-fixed-safe-area, 0px)));
|
|
92
|
+
overflow: hidden;
|
|
93
|
+
}
|
|
94
|
+
.rt-exposure-position--bottom[data-device="mobile"] > .rt-exposure { --rt-exposure-fixed-safe-area: env(safe-area-inset-bottom); }
|
|
95
|
+
:is(.rt-exposure-position--top, .rt-exposure-position--bottom, .rt-exposure--preview:is([data-placement="top"], [data-placement="bottom"]))[data-device="mobile"] .rt-exposure-carousel {
|
|
96
|
+
display: flex;
|
|
97
|
+
flex-direction: column;
|
|
98
|
+
min-block-size: 0;
|
|
99
|
+
}
|
|
100
|
+
:is(.rt-exposure-position--top, .rt-exposure-position--bottom, .rt-exposure--preview:is([data-placement="top"], [data-placement="bottom"]))[data-device="mobile"] .rt-exposure-slide,
|
|
101
|
+
.rt-exposure--preview[data-device="mobile"]:is([data-placement="top"], [data-placement="bottom"]) > .rt-exposure__body {
|
|
102
|
+
min-block-size: 0;
|
|
103
|
+
overflow: auto;
|
|
104
|
+
overscroll-behavior: auto;
|
|
105
|
+
}
|
|
106
|
+
:is(.rt-exposure-position--top, .rt-exposure-position--bottom, .rt-exposure--preview:is([data-placement="top"], [data-placement="bottom"]))[data-device="mobile"] .rt-exposure__body {
|
|
107
|
+
gap: .5rem;
|
|
108
|
+
padding: .5rem .75rem;
|
|
109
|
+
}
|
|
110
|
+
:is(.rt-exposure-position--top, .rt-exposure-position--bottom, .rt-exposure--preview:is([data-placement="top"], [data-placement="bottom"]))[data-device="mobile"] .rt-exposure__body--image { padding: 0; }
|
|
111
|
+
:is(.rt-exposure-position--top, .rt-exposure-position--bottom, .rt-exposure--preview:is([data-placement="top"], [data-placement="bottom"]))[data-device="mobile"] .rt-exposure__copy {
|
|
112
|
+
display: block;
|
|
113
|
+
font-size: .8125rem;
|
|
114
|
+
line-height: 1.5;
|
|
115
|
+
}
|
|
116
|
+
:is(.rt-exposure-position--top, .rt-exposure-position--bottom, .rt-exposure--preview:is([data-placement="top"], [data-placement="bottom"]))[data-device="mobile"] :is(.rt-exposure__eyebrow, .rt-exposure__title) { margin-block-end: .25rem; }
|
|
117
|
+
:is(.rt-exposure-position--top, .rt-exposure-position--bottom, .rt-exposure--preview:is([data-placement="top"], [data-placement="bottom"]))[data-device="mobile"] .rt-exposure__title { font-size: 1rem; }
|
|
118
|
+
:is(.rt-exposure-position--top, .rt-exposure-position--bottom, .rt-exposure--preview:is([data-placement="top"], [data-placement="bottom"]))[data-device="mobile"] .rt-exposure__description {
|
|
119
|
+
display: inline;
|
|
120
|
+
font-size: inherit;
|
|
121
|
+
line-height: inherit;
|
|
122
|
+
}
|
|
123
|
+
:is(.rt-exposure-position--top, .rt-exposure-position--bottom, .rt-exposure--preview:is([data-placement="top"], [data-placement="bottom"]))[data-device="mobile"] .rt-exposure__link {
|
|
124
|
+
display: inline;
|
|
125
|
+
margin-block-start: 0;
|
|
126
|
+
}
|
|
127
|
+
:is(.rt-exposure-position--top, .rt-exposure-position--bottom, .rt-exposure--preview:is([data-placement="top"], [data-placement="bottom"]))[data-device="mobile"] .rt-exposure__description + .rt-exposure__link { margin-inline-start: .5rem; }
|
|
128
|
+
:is(.rt-exposure-position--top, .rt-exposure-position--bottom, .rt-exposure--preview:is([data-placement="top"], [data-placement="bottom"]))[data-device="mobile"] .rt-exposure__close { inset-block-start: .25rem; inset-inline-end: .25rem; }
|
|
129
|
+
:is(.rt-exposure-position--top, .rt-exposure-position--bottom, .rt-exposure--preview:is([data-placement="top"], [data-placement="bottom"]))[data-device="mobile"] .rt-exposure-carousel-controls {
|
|
130
|
+
flex-shrink: 0;
|
|
131
|
+
padding: 0 .75rem .25rem;
|
|
132
|
+
}
|
|
133
|
+
:is(.rt-exposure-position--top, .rt-exposure-position--bottom, .rt-exposure--preview:is([data-placement="top"], [data-placement="bottom"]))[data-device="mobile"] .rt-exposure-carousel-meta { gap: .5rem; }
|
|
134
|
+
:is(.rt-exposure-position--top, .rt-exposure-position--bottom, .rt-exposure--preview:is([data-placement="top"], [data-placement="bottom"]))[data-device="mobile"] .rt-exposure-carousel-dots { display: none; }
|
|
135
|
+
@keyframes rt-exposure-reveal { from { opacity: .35; } to { opacity: 1; } }
|
|
136
|
+
@media (min-width: 48rem) { .rt-exposure--banner .rt-exposure__body:not(.rt-exposure__body--image):has(> .rt-exposure__asset) { grid-template-columns: minmax(0, 12rem) 1fr; align-items: center; } }
|
|
137
|
+
@media (prefers-reduced-motion: reduce) { .rt-exposure, .rt-exposure-backdrop, .rt-exposure-position, .rt-exposure-slide > .rt-exposure__body, .rt-exposure-carousel-progress > span { scroll-behavior: auto; animation: none !important; transition: none !important; } }
|
|
40
138
|
|
|
41
139
|
/* Category filter — chips are <a> links (/blog/categories/slug), SSR-only, no JS. */
|
|
42
140
|
[data-roottale-cms] :where(.rt-cms-filter) {
|
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
readonly campaign: CmsExposureCampaign;
|
|
6
|
-
readonly allowedVariants: readonly string[];
|
|
7
|
-
}): react_jsx_runtime.JSX.Element | null;
|
|
8
|
-
|
|
9
|
-
export { RootTaleExposure };
|
|
1
|
+
export { RootTaleLegacyExposure as RootTaleExposure } from './exposures.js';
|
|
2
|
+
import 'react';
|
|
3
|
+
import 'react/jsx-runtime';
|
|
4
|
+
import '@roottale/cms-client/server';
|
package/dist/exposure-client.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
"use client";
|
|
2
|
+
import "./chunk-UIBT23RJ.js";
|
|
2
3
|
import {
|
|
3
|
-
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
RootTaleLegacyExposure
|
|
5
|
+
} from "./chunk-SEPUIKVM.js";
|
|
6
|
+
import "./chunk-L3I4W5NP.js";
|
|
5
7
|
export {
|
|
6
|
-
RootTaleExposure
|
|
8
|
+
RootTaleLegacyExposure as RootTaleExposure
|
|
7
9
|
};
|
|
8
10
|
//# sourceMappingURL=exposure-client.js.map
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
4
|
+
import { CmsExposureCampaign } from '@roottale/cms-client/server';
|
|
5
|
+
|
|
6
|
+
type ExposurePlacement = "top" | "inline" | "bottom" | "popup";
|
|
7
|
+
/** 실제 콘텐츠 출력과 같지만 방문 기록·조회·포커스 이동이 없는 편집기 미리보기. */
|
|
8
|
+
declare function RootTaleExposurePreview({ campaign, placement, device, className }: {
|
|
9
|
+
campaign: CmsExposureCampaign;
|
|
10
|
+
placement?: ExposurePlacement;
|
|
11
|
+
device?: "mobile" | "desktop";
|
|
12
|
+
className?: string;
|
|
13
|
+
}): react_jsx_runtime.JSX.Element;
|
|
14
|
+
|
|
15
|
+
declare function RootTaleExposureCarouselPreview({ campaigns, placement, device, slideDurationMs }: {
|
|
16
|
+
campaigns: readonly CmsExposureCampaign[];
|
|
17
|
+
placement?: ExposurePlacement;
|
|
18
|
+
device?: "desktop" | "mobile";
|
|
19
|
+
slideDurationMs?: number;
|
|
20
|
+
}): react_jsx_runtime.JSX.Element | null;
|
|
21
|
+
|
|
22
|
+
interface RootTaleExposureProviderProps {
|
|
23
|
+
endpoint: string;
|
|
24
|
+
/** 사이트의 usePathname() 값. API 키는 서버에만 둔다. */
|
|
25
|
+
pathname: string;
|
|
26
|
+
slots: readonly string[];
|
|
27
|
+
breakpoint?: number;
|
|
28
|
+
/** 메인 홈만 허용하며 locale 홈은 실제로 만든 exact 경로를 명시한다. */
|
|
29
|
+
homePaths?: readonly string[];
|
|
30
|
+
slideDurationMs?: number;
|
|
31
|
+
hideMobileBannersOnScroll?: boolean;
|
|
32
|
+
legacyDismissalKeyPrefixes?: readonly string[];
|
|
33
|
+
children: ReactNode;
|
|
34
|
+
}
|
|
35
|
+
declare function RootTaleExposureProvider({ endpoint, pathname, slots, breakpoint, homePaths, slideDurationMs, hideMobileBannersOnScroll, legacyDismissalKeyPrefixes, children }: RootTaleExposureProviderProps): react_jsx_runtime.JSX.Element;
|
|
36
|
+
interface RootTaleExposureSlotProps {
|
|
37
|
+
slotKey: string;
|
|
38
|
+
placement: ExposurePlacement;
|
|
39
|
+
allowedVariants: readonly string[];
|
|
40
|
+
/** 배너를 닫아도 같은 방문에서 이전 공지를 이어서 표시하지 않는다. */
|
|
41
|
+
fallback?: ReactNode;
|
|
42
|
+
className?: string;
|
|
43
|
+
}
|
|
44
|
+
declare function RootTaleExposureSlot({ slotKey, placement, allowedVariants, fallback, className }: RootTaleExposureSlotProps): string | number | bigint | boolean | react_jsx_runtime.JSX.Element | Iterable<ReactNode> | Promise<string | number | bigint | boolean | react.ReactPortal | react.ReactElement<unknown, string | react.JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | null;
|
|
45
|
+
/** 기존 서버 슬롯의 client island. 신규 설치는 Provider/Slot을 사용한다. */
|
|
46
|
+
declare function RootTaleLegacyExposure({ campaign, allowedVariants }: {
|
|
47
|
+
campaign: CmsExposureCampaign;
|
|
48
|
+
allowedVariants: readonly string[];
|
|
49
|
+
}): react_jsx_runtime.JSX.Element | null;
|
|
50
|
+
|
|
51
|
+
export { type ExposurePlacement, RootTaleExposureCarouselPreview, RootTaleExposurePreview, RootTaleExposureProvider, type RootTaleExposureProviderProps, RootTaleExposureSlot, type RootTaleExposureSlotProps, RootTaleLegacyExposure };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import {
|
|
3
|
+
RootTaleExposureCarouselPreview,
|
|
4
|
+
RootTaleExposurePreview,
|
|
5
|
+
RootTaleExposureProvider,
|
|
6
|
+
RootTaleExposureSlot,
|
|
7
|
+
RootTaleLegacyExposure
|
|
8
|
+
} from "./chunk-SEPUIKVM.js";
|
|
9
|
+
import "./chunk-L3I4W5NP.js";
|
|
10
|
+
export {
|
|
11
|
+
RootTaleExposureCarouselPreview,
|
|
12
|
+
RootTaleExposurePreview,
|
|
13
|
+
RootTaleExposureProvider,
|
|
14
|
+
RootTaleExposureSlot,
|
|
15
|
+
RootTaleLegacyExposure
|
|
16
|
+
};
|
|
17
|
+
//# sourceMappingURL=exposures.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/dist/routes.d.ts
CHANGED
|
@@ -268,6 +268,19 @@ interface RevalidateRouteConfig {
|
|
|
268
268
|
*/
|
|
269
269
|
declare function createRevalidateRoute(config: RevalidateRouteConfig): (req: Request) => Promise<Response>;
|
|
270
270
|
|
|
271
|
+
interface ExposureRouteConfig {
|
|
272
|
+
/** 서버 전용 고객 CMS 키. 브라우저의 요청으로 바꾸지 않는다. */
|
|
273
|
+
apiKey: string;
|
|
274
|
+
siteId?: string;
|
|
275
|
+
baseUrl?: string;
|
|
276
|
+
/** 실제 화면에 설치하고 계약에 등록한 슬롯만 선언한다. */
|
|
277
|
+
slots: readonly string[];
|
|
278
|
+
/** 명시한 메인 홈에서만 노출한다. locale 홈은 제작 코드가 추가한다. */
|
|
279
|
+
homePaths?: readonly string[];
|
|
280
|
+
}
|
|
281
|
+
/** 브라우저는 path/device만 지정한다. 키·상류 URL·사이트·슬롯은 배포 코드가 소유한다. */
|
|
282
|
+
declare function createExposureRoute(config: ExposureRouteConfig): (request: Request) => Promise<Response>;
|
|
283
|
+
|
|
271
284
|
/** `routes` — 동적 OG 이미지 (글별 1200×630, 한글 폰트 subset). */
|
|
272
285
|
|
|
273
286
|
/** Next `opengraph-image` export 컨벤션과 호환되는 크기 상수. */
|
|
@@ -1006,6 +1019,12 @@ declare function postRedirectPath(post: {
|
|
|
1006
1019
|
interface FleetInfoRouteConfig {
|
|
1007
1020
|
/** 사이트 식별 라벨 (선택 — 기본 Vercel project URL 또는 null). */
|
|
1008
1021
|
site?: string;
|
|
1022
|
+
/** 실제 런타임과 슬롯을 설치한 사이트만 선언한다. */
|
|
1023
|
+
exposures?: {
|
|
1024
|
+
runtimeVersion: 2;
|
|
1025
|
+
endpoint: string;
|
|
1026
|
+
slots: readonly string[];
|
|
1027
|
+
};
|
|
1009
1028
|
}
|
|
1010
1029
|
/**
|
|
1011
1030
|
* `GET /.well-known/roottale.json` — fleet 가시성 컨벤션 라우트.
|
|
@@ -1023,4 +1042,4 @@ interface FleetInfoRouteConfig {
|
|
|
1023
1042
|
*/
|
|
1024
1043
|
declare function createFleetInfoRoute(config?: FleetInfoRouteConfig): () => Promise<Response>;
|
|
1025
1044
|
|
|
1026
|
-
export { ARCHIVE_POST_COUNT_LIMIT, type AlwaysNoindexArchiveKind, type ArchiveCategoriesConfig, type ArchiveCategoriesOptions, type ArchiveIndexDecision, type ArchiveIndexInput, type ArchiveKind, type ArchiveMetadata, type ArchiveMetadataFields, type ArchiveMetadataInput, type ArchivePromotion, type BuildArchiveMetadataOptions, type BuildPostMetadataOptions, type CollectionsInput, type FleetInfoRouteConfig, type ImageResponseLike, type LlmsTxtLink, type LlmsTxtRouteConfig, type LlmsTxtSection, OG_IMAGE_CONTENT_TYPE, OG_IMAGE_SIZE, type OgImageRouteConfig, type PostLocaleOptions, type PostMetadata, type PostMetadataInput, type PostSeoOverrides, type PreviewMetadata, type PromotableArchiveKind, type PublicUrlPost, type RedirectMiddlewareConfig, type RevalidateRouteConfig, type RobotsMeta, type RootTaleSiteConfig, type SitemapEntry, type SitemapIndexOptions, type SitemapSectionId, buildArchiveMetadata, buildHreflangLanguages, buildPostMetadata, buildPreviewMetadata, createFeedRoute, createFleetInfoRoute, createLlmsTxtRoute, createPostOgImage, createRedirectMiddleware, createRevalidateRoute, createSitemap, createSitemapIndex, decideArchiveIndex, fetchBlogArchiveCategories, fetchBlogArchivePosts, matchRedirect, normalizeRedirectPath, postRedirectPath, resolveAuthorPromotion, resolveCategoryPromotion, resolveCollections, resolveCollectionsResult, resolveRobotsMeta, safeFetchPosts, storedPostPath, trimSlash };
|
|
1045
|
+
export { ARCHIVE_POST_COUNT_LIMIT, type AlwaysNoindexArchiveKind, type ArchiveCategoriesConfig, type ArchiveCategoriesOptions, type ArchiveIndexDecision, type ArchiveIndexInput, type ArchiveKind, type ArchiveMetadata, type ArchiveMetadataFields, type ArchiveMetadataInput, type ArchivePromotion, type BuildArchiveMetadataOptions, type BuildPostMetadataOptions, type CollectionsInput, type ExposureRouteConfig, type FleetInfoRouteConfig, type ImageResponseLike, type LlmsTxtLink, type LlmsTxtRouteConfig, type LlmsTxtSection, OG_IMAGE_CONTENT_TYPE, OG_IMAGE_SIZE, type OgImageRouteConfig, type PostLocaleOptions, type PostMetadata, type PostMetadataInput, type PostSeoOverrides, type PreviewMetadata, type PromotableArchiveKind, type PublicUrlPost, type RedirectMiddlewareConfig, type RevalidateRouteConfig, type RobotsMeta, type RootTaleSiteConfig, type SitemapEntry, type SitemapIndexOptions, type SitemapSectionId, buildArchiveMetadata, buildHreflangLanguages, buildPostMetadata, buildPreviewMetadata, createExposureRoute, createFeedRoute, createFleetInfoRoute, createLlmsTxtRoute, createPostOgImage, createRedirectMiddleware, createRevalidateRoute, createSitemap, createSitemapIndex, decideArchiveIndex, fetchBlogArchiveCategories, fetchBlogArchivePosts, matchRedirect, normalizeRedirectPath, postRedirectPath, resolveAuthorPromotion, resolveCategoryPromotion, resolveCollections, resolveCollectionsResult, resolveRobotsMeta, safeFetchPosts, storedPostPath, trimSlash };
|
package/dist/routes.js
CHANGED
|
@@ -11,6 +11,10 @@ import {
|
|
|
11
11
|
storedPostPath,
|
|
12
12
|
trimSlash
|
|
13
13
|
} from "./chunk-GDP2VGJR.js";
|
|
14
|
+
import {
|
|
15
|
+
exposureHomePaths,
|
|
16
|
+
isExposureHome
|
|
17
|
+
} from "./chunk-L3I4W5NP.js";
|
|
14
18
|
|
|
15
19
|
// src/routes.ts
|
|
16
20
|
import { CMS_CLIENT_VERSION } from "@roottale/cms-client/server";
|
|
@@ -696,6 +700,39 @@ function createRevalidateRoute(config) {
|
|
|
696
700
|
};
|
|
697
701
|
}
|
|
698
702
|
|
|
703
|
+
// src/routes-exposures.ts
|
|
704
|
+
import { fetchExposureDecision } from "@roottale/cms-client/server";
|
|
705
|
+
var headers = { "cache-control": "private, no-store, max-age=0", "x-content-type-options": "nosniff" };
|
|
706
|
+
function createExposureRoute(config) {
|
|
707
|
+
const homes = exposureHomePaths(config.homePaths);
|
|
708
|
+
return async (request) => {
|
|
709
|
+
const params = new URL(request.url).searchParams;
|
|
710
|
+
const path = params.get("path") ?? "/";
|
|
711
|
+
const device = params.get("device") ?? "desktop";
|
|
712
|
+
if ([...params.keys()].some((key) => key !== "path" && key !== "device") || params.getAll("path").length > 1 || params.getAll("device").length > 1 || !path.startsWith("/") || path.startsWith("//") || path.length > 2048 || /[\u0000-\u0020\u007f\\?#]/.test(path) || device !== "desktop" && device !== "mobile") {
|
|
713
|
+
return Response.json({ error: "Invalid exposure request" }, { status: 400, headers });
|
|
714
|
+
}
|
|
715
|
+
if (!config.apiKey || config.slots.length === 0) return Response.json({ error: "Exposure connection unavailable" }, { status: 503, headers });
|
|
716
|
+
if (!isExposureHome(path, homes)) {
|
|
717
|
+
const now = Date.now();
|
|
718
|
+
return Response.json({
|
|
719
|
+
campaigns: [],
|
|
720
|
+
evaluatedAt: new Date(now).toISOString(),
|
|
721
|
+
nextChangeAt: null,
|
|
722
|
+
validUntil: new Date(now + 9e4).toISOString(),
|
|
723
|
+
contractVersion: null,
|
|
724
|
+
supportedFeatures: ["exposure-decisions-v2", "exposure-home-carousel"]
|
|
725
|
+
}, { headers });
|
|
726
|
+
}
|
|
727
|
+
try {
|
|
728
|
+
const decision = await fetchExposureDecision({ ...config, slotKeys: config.slots, path, device, signal: request.signal });
|
|
729
|
+
return Response.json(decision, { headers });
|
|
730
|
+
} catch {
|
|
731
|
+
return Response.json({ error: "Exposure temporarily unavailable" }, { status: 503, headers });
|
|
732
|
+
}
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
|
|
699
736
|
// src/routes-og.ts
|
|
700
737
|
import { fetchPost } from "@roottale/cms-client/server";
|
|
701
738
|
var OG_IMAGE_SIZE = { width: 1200, height: 630 };
|
|
@@ -927,8 +964,9 @@ function createFleetInfoRoute(config = {}) {
|
|
|
927
964
|
site: config.site ?? env.VERCEL_PROJECT_PRODUCTION_URL ?? null,
|
|
928
965
|
framework: "next",
|
|
929
966
|
cms_client: CMS_CLIENT_VERSION,
|
|
930
|
-
cms_renderer: true ? "0.
|
|
931
|
-
build_sha: env.VERCEL_GIT_COMMIT_SHA?.slice(0, 7) ?? null
|
|
967
|
+
cms_renderer: true ? "0.63.0" : "dev",
|
|
968
|
+
build_sha: env.VERCEL_GIT_COMMIT_SHA?.slice(0, 7) ?? null,
|
|
969
|
+
...config.exposures ? { exposures: config.exposures } : {}
|
|
932
970
|
},
|
|
933
971
|
{ headers: { "cache-control": "public, max-age=300" } }
|
|
934
972
|
);
|
|
@@ -942,6 +980,7 @@ export {
|
|
|
942
980
|
buildHreflangLanguages,
|
|
943
981
|
buildPostMetadata,
|
|
944
982
|
buildPreviewMetadata,
|
|
983
|
+
createExposureRoute,
|
|
945
984
|
createFeedRoute,
|
|
946
985
|
createFleetInfoRoute,
|
|
947
986
|
createLlmsTxtRoute,
|