@roottale/analytics-runtime 0.3.1 → 0.5.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/dist/ai-referrals.d.ts +40 -0
- package/dist/ai-referrals.js +17 -0
- package/dist/ai-referrals.js.map +1 -0
- package/dist/chunk-2WQ7PVRU.js +59 -0
- package/dist/chunk-2WQ7PVRU.js.map +1 -0
- package/dist/chunk-ABGGPWIX.js +200 -0
- package/dist/chunk-ABGGPWIX.js.map +1 -0
- package/dist/chunk-ANB42OLW.js +111 -0
- package/dist/chunk-ANB42OLW.js.map +1 -0
- package/dist/{chunk-ZFY3KBJJ.js → chunk-G7OLBHT5.js} +61 -57
- package/dist/chunk-G7OLBHT5.js.map +1 -0
- package/dist/chunk-GBIGQHVN.js +123 -0
- package/dist/chunk-GBIGQHVN.js.map +1 -0
- package/dist/events.d.ts +147 -0
- package/dist/events.js +11 -0
- package/dist/events.js.map +1 -0
- package/dist/index.d.ts +104 -3
- package/dist/index.js +45 -5
- package/dist/index.js.map +1 -1
- package/dist/next.d.ts +7 -0
- package/dist/next.js +34 -0
- package/dist/next.js.map +1 -0
- package/dist/react.d.ts +16 -1
- package/dist/react.js +32 -4
- package/dist/react.js.map +1 -1
- package/package.json +21 -1
- package/dist/chunk-ZFY3KBJJ.js.map +0 -1
package/dist/react.js
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import {
|
|
2
|
-
renderBeaconScript,
|
|
3
2
|
renderConsentBannerMarkup,
|
|
4
3
|
renderConsentBannerScript,
|
|
5
4
|
renderConsentBootstrap,
|
|
5
|
+
renderGa4BridgeScript,
|
|
6
|
+
renderJourneyBridgeScript,
|
|
6
7
|
renderLoaderScript
|
|
7
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-G7OLBHT5.js";
|
|
9
|
+
import {
|
|
10
|
+
renderBeaconScript
|
|
11
|
+
} from "./chunk-ANB42OLW.js";
|
|
12
|
+
import "./chunk-GBIGQHVN.js";
|
|
13
|
+
import "./chunk-2WQ7PVRU.js";
|
|
8
14
|
|
|
9
15
|
// src/react.tsx
|
|
10
16
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -14,7 +20,9 @@ function RootTaleAnalytics({
|
|
|
14
20
|
siteId,
|
|
15
21
|
privacyUrl,
|
|
16
22
|
trackPageview,
|
|
17
|
-
bannerOptions
|
|
23
|
+
bannerOptions,
|
|
24
|
+
enableGa4Bridge = true,
|
|
25
|
+
enableObservers = true
|
|
18
26
|
}) {
|
|
19
27
|
const beacon = collectUrl && siteId ? renderBeaconScript({ collectUrl, siteId, trackPageview }) : null;
|
|
20
28
|
const bannerOpts = {
|
|
@@ -49,7 +57,27 @@ function RootTaleAnalytics({
|
|
|
49
57
|
dangerouslySetInnerHTML: { __html: renderConsentBannerScript() }
|
|
50
58
|
}
|
|
51
59
|
),
|
|
52
|
-
beacon ? /* @__PURE__ */ jsx("script", { dangerouslySetInnerHTML: { __html: beacon } }) : null
|
|
60
|
+
beacon ? /* @__PURE__ */ jsx("script", { dangerouslySetInnerHTML: { __html: beacon } }) : null,
|
|
61
|
+
beacon ? /* @__PURE__ */ jsx(
|
|
62
|
+
"script",
|
|
63
|
+
{
|
|
64
|
+
dangerouslySetInnerHTML: { __html: renderJourneyBridgeScript() }
|
|
65
|
+
}
|
|
66
|
+
) : null,
|
|
67
|
+
beacon && enableGa4Bridge ? /* @__PURE__ */ jsx(
|
|
68
|
+
"script",
|
|
69
|
+
{
|
|
70
|
+
dangerouslySetInnerHTML: { __html: renderGa4BridgeScript() }
|
|
71
|
+
}
|
|
72
|
+
) : null,
|
|
73
|
+
enableObservers === false ? /* @__PURE__ */ jsx(
|
|
74
|
+
"script",
|
|
75
|
+
{
|
|
76
|
+
dangerouslySetInnerHTML: {
|
|
77
|
+
__html: "window.__rtObserversEnabled=false;"
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
) : null
|
|
53
81
|
] });
|
|
54
82
|
}
|
|
55
83
|
export {
|
package/dist/react.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/react.tsx"],"sourcesContent":["// ADR-0038 — Next.js/React 드롭인 어댑터.\n//\n// `@roottale/analytics-runtime/react` 로 import. core 는 React-free 유지하고\n// 본 어댑터만 react(peerDependency)에 의존한다.\n//\n// 사용 (Next App Router root layout):\n// import { RootTaleAnalytics } from \"@roottale/analytics-runtime/react\";\n// ...\n// <body>\n// {children}\n// <RootTaleAnalytics manifest={manifest} collectUrl={url} siteId={id} />\n// </body>\n//\n// 모든 <script> 는 dangerouslySetInnerHTML 인라인 — Next 가 실제 실행되는\n// <script> 로 렌더(set:html innerHTML 함정 회피). 순서: consent bootstrap(전부\n// denied) → loader(fail-closed) → 배너 → 비콘.\n\nimport type { AnalyticsManifest } from \"./types\";\nimport { renderConsentBootstrap, renderLoaderScript } from \"./loader\";\nimport {\n renderConsentBannerMarkup,\n renderConsentBannerScript,\n type ConsentBannerOptions,\n} from \"./consent-banner\";\nimport { renderBeaconScript } from \"./beacon\";\n\nexport interface RootTaleAnalyticsProps {\n /** buildManifest() 결과. plane ① 태그 + fail-closed 동의 기본값. */\n manifest: AnalyticsManifest;\n /** plane ② 비콘 수집 엔드포인트. 없으면 비콘 미주입. */\n collectUrl?: string;\n /** 비콘용 site 식별자. collectUrl 과 함께 제공. */\n siteId?: string;\n /** 동의 배너 개인정보처리방침 링크. */\n privacyUrl?: string;\n /** 비콘 pageview 자동 전송 (기본 true). */\n trackPageview?: boolean;\n /** 동의 배너 카피 override. */\n bannerOptions?: ConsentBannerOptions;\n}\n\n/**\n * 단일 드롭인 컴포넌트. RSC 호환(hook/상태 없음) — Server Component layout 에\n * 바로 배치 가능.\n */\nexport function RootTaleAnalytics({\n manifest,\n collectUrl,\n siteId,\n privacyUrl,\n trackPageview,\n bannerOptions,\n}: RootTaleAnalyticsProps) {\n const beacon =\n collectUrl && siteId\n ? renderBeaconScript({ collectUrl, siteId, trackPageview })\n : null;\n const bannerOpts: ConsentBannerOptions = {\n privacyUrl,\n ...bannerOptions,\n };\n\n return (\n <>\n <script\n dangerouslySetInnerHTML={{ __html: renderConsentBootstrap() }}\n />\n <script\n dangerouslySetInnerHTML={{ __html: renderLoaderScript(manifest) }}\n />\n {/*\n suppressHydrationWarning: `renderConsentBannerScript()` 는 일반\n `<script>` 로 삽입되어 브라우저가 이 div 뒤에서 그 스크립트를 만나는\n 즉시(React 하이드레이션이 시작되기 전, 최초 HTML 파싱 도중) 동의 기록이\n 없으면 `el.hidden = false` 를 실행해 `hidden` 속성을 지운다 — 배너를\n 깜빡임 없이 보여주기 위해 의도된 동작이다. 그 결과 React 가 하이드레이션\n 시점에 비교하는 실제 DOM 은 이 컴포넌트가 SSR 로 낸 `__html`(hidden 포함)\n 과 달라져 \"tree hydrated but attributes didn't match\" 경고가 난다 —\n React 문서가 안내하는 FOUC-방지 스크립트의 표준 처방(next-themes 류가\n `<html>` 에 쓰는 것과 동일 패턴)대로 이 노드에서만 경고를 억제한다.\n */}\n <div\n suppressHydrationWarning\n dangerouslySetInnerHTML={{\n __html: renderConsentBannerMarkup(bannerOpts),\n }}\n />\n <script\n dangerouslySetInnerHTML={{ __html: renderConsentBannerScript() }}\n />\n {beacon ? (\n <script dangerouslySetInnerHTML={{ __html: beacon }} />\n ) : null}\n </>\n );\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../src/react.tsx"],"sourcesContent":["// ADR-0038 — Next.js/React 드롭인 어댑터.\n//\n// `@roottale/analytics-runtime/react` 로 import. core 는 React-free 유지하고\n// 본 어댑터만 react(peerDependency)에 의존한다.\n//\n// 사용 (Next App Router root layout):\n// import { RootTaleAnalytics } from \"@roottale/analytics-runtime/react\";\n// ...\n// <body>\n// {children}\n// <RootTaleAnalytics manifest={manifest} collectUrl={url} siteId={id} />\n// </body>\n//\n// 모든 <script> 는 dangerouslySetInnerHTML 인라인 — Next 가 실제 실행되는\n// <script> 로 렌더(set:html innerHTML 함정 회피). 순서: consent bootstrap(전부\n// denied) → loader(fail-closed) → 배너 → 비콘.\n\nimport type { AnalyticsManifest } from \"./types\";\nimport { renderConsentBootstrap, renderLoaderScript } from \"./loader\";\nimport {\n renderConsentBannerMarkup,\n renderConsentBannerScript,\n type ConsentBannerOptions,\n} from \"./consent-banner\";\nimport { renderBeaconScript } from \"./beacon\";\nimport { renderJourneyBridgeScript } from \"./journey\";\nimport { renderGa4BridgeScript } from \"./ga4-bridge\";\n\nexport interface RootTaleAnalyticsProps {\n /** buildManifest() 결과. plane ① 태그 + fail-closed 동의 기본값. */\n manifest: AnalyticsManifest;\n /** plane ② 비콘 수집 엔드포인트. 없으면 비콘 미주입. */\n collectUrl?: string;\n /** 비콘용 site 식별자. collectUrl 과 함께 제공. */\n siteId?: string;\n /** 동의 배너 개인정보처리방침 링크. */\n privacyUrl?: string;\n /** 비콘 pageview 자동 전송 (기본 true). */\n trackPageview?: boolean;\n /** 동의 배너 카피 override. */\n bannerOptions?: ConsentBannerOptions;\n /**\n * GA4 미러 브리지 스크립트 주입 여부(설계 §0, 기본 true). beacon 이 없으면\n * (collectUrl/siteId 미설정) 어차피 미주입 — 이벤트를 흘려보낼 send() 자체가\n * 없기 때문.\n */\n enableGa4Bridge?: boolean;\n /**\n * 방문 여정(observers.ts) 감지기 활성 여부(설계 §1 2단계, 기본 true). 본\n * 컴포넌트는 RSC 호환(hook 없음)이라 IntersectionObserver 등을 직접\n * 구동하지 못한다 — `false` 로 지정하면 `window.__rtObserversEnabled=false`\n * 를 심어 `RootTaleAnalyticsNextAdapter`(next.tsx)가 initObservers() 호출\n * 전 이 값을 읽고 건너뛰게 한다. `true`(기본)는 아무 것도 심지 않음(next.tsx\n * 의 기본값이 이미 활성이므로 무동작이 곧 활성).\n */\n enableObservers?: boolean;\n}\n\n/**\n * 단일 드롭인 컴포넌트. RSC 호환(hook/상태 없음) — Server Component layout 에\n * 바로 배치 가능.\n */\nexport function RootTaleAnalytics({\n manifest,\n collectUrl,\n siteId,\n privacyUrl,\n trackPageview,\n bannerOptions,\n enableGa4Bridge = true,\n enableObservers = true,\n}: RootTaleAnalyticsProps) {\n const beacon =\n collectUrl && siteId\n ? renderBeaconScript({ collectUrl, siteId, trackPageview })\n : null;\n const bannerOpts: ConsentBannerOptions = {\n privacyUrl,\n ...bannerOptions,\n };\n\n return (\n <>\n <script\n dangerouslySetInnerHTML={{ __html: renderConsentBootstrap() }}\n />\n <script\n dangerouslySetInnerHTML={{ __html: renderLoaderScript(manifest) }}\n />\n {/*\n suppressHydrationWarning: `renderConsentBannerScript()` 는 일반\n `<script>` 로 삽입되어 브라우저가 이 div 뒤에서 그 스크립트를 만나는\n 즉시(React 하이드레이션이 시작되기 전, 최초 HTML 파싱 도중) 동의 기록이\n 없으면 `el.hidden = false` 를 실행해 `hidden` 속성을 지운다 — 배너를\n 깜빡임 없이 보여주기 위해 의도된 동작이다. 그 결과 React 가 하이드레이션\n 시점에 비교하는 실제 DOM 은 이 컴포넌트가 SSR 로 낸 `__html`(hidden 포함)\n 과 달라져 \"tree hydrated but attributes didn't match\" 경고가 난다 —\n React 문서가 안내하는 FOUC-방지 스크립트의 표준 처방(next-themes 류가\n `<html>` 에 쓰는 것과 동일 패턴)대로 이 노드에서만 경고를 억제한다.\n */}\n <div\n suppressHydrationWarning\n dangerouslySetInnerHTML={{\n __html: renderConsentBannerMarkup(bannerOpts),\n }}\n />\n <script\n dangerouslySetInnerHTML={{ __html: renderConsentBannerScript() }}\n />\n {beacon ? (\n <script dangerouslySetInnerHTML={{ __html: beacon }} />\n ) : null}\n {/*\n journey 브리지는 beacon 이 있을 때만 의미가 있다(훅 대상인\n window.__rtTrack/send() 자체가 beacon 스크립트가 심는다) — opt-out\n 프롭 없음(1차 캡처 인프라, 설계 §3).\n */}\n {beacon ? (\n <script\n dangerouslySetInnerHTML={{ __html: renderJourneyBridgeScript() }}\n />\n ) : null}\n {beacon && enableGa4Bridge ? (\n <script\n dangerouslySetInnerHTML={{ __html: renderGa4BridgeScript() }}\n />\n ) : null}\n {enableObservers === false ? (\n <script\n dangerouslySetInnerHTML={{\n __html: \"window.__rtObserversEnabled=false;\",\n }}\n />\n ) : null}\n </>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;AAkFI,mBACE,KADF;AApBG,SAAS,kBAAkB;AAAA,EAChC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,kBAAkB;AAAA,EAClB,kBAAkB;AACpB,GAA2B;AACzB,QAAM,SACJ,cAAc,SACV,mBAAmB,EAAE,YAAY,QAAQ,cAAc,CAAC,IACxD;AACN,QAAM,aAAmC;AAAA,IACvC;AAAA,IACA,GAAG;AAAA,EACL;AAEA,SACE,iCACE;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,yBAAyB,EAAE,QAAQ,uBAAuB,EAAE;AAAA;AAAA,IAC9D;AAAA,IACA;AAAA,MAAC;AAAA;AAAA,QACC,yBAAyB,EAAE,QAAQ,mBAAmB,QAAQ,EAAE;AAAA;AAAA,IAClE;AAAA,IAYA;AAAA,MAAC;AAAA;AAAA,QACC,0BAAwB;AAAA,QACxB,yBAAyB;AAAA,UACvB,QAAQ,0BAA0B,UAAU;AAAA,QAC9C;AAAA;AAAA,IACF;AAAA,IACA;AAAA,MAAC;AAAA;AAAA,QACC,yBAAyB,EAAE,QAAQ,0BAA0B,EAAE;AAAA;AAAA,IACjE;AAAA,IACC,SACC,oBAAC,YAAO,yBAAyB,EAAE,QAAQ,OAAO,GAAG,IACnD;AAAA,IAMH,SACC;AAAA,MAAC;AAAA;AAAA,QACC,yBAAyB,EAAE,QAAQ,0BAA0B,EAAE;AAAA;AAAA,IACjE,IACE;AAAA,IACH,UAAU,kBACT;AAAA,MAAC;AAAA;AAAA,QACC,yBAAyB,EAAE,QAAQ,sBAAsB,EAAE;AAAA;AAAA,IAC7D,IACE;AAAA,IACH,oBAAoB,QACnB;AAAA,MAAC;AAAA;AAAA,QACC,yBAAyB;AAAA,UACvB,QAAQ;AAAA,QACV;AAAA;AAAA,IACF,IACE;AAAA,KACN;AAEJ;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@roottale/analytics-runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "RootTale analytics runtime (ADR-0038) — Consent Mode v2 fail-closed tag loader, consent banner, and first-party data-track beacon. Framework-agnostic strings + optional React drop-in (<RootTaleAnalytics>).",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -16,6 +16,21 @@
|
|
|
16
16
|
"import": "./dist/react.js",
|
|
17
17
|
"default": "./dist/react.js"
|
|
18
18
|
},
|
|
19
|
+
"./next": {
|
|
20
|
+
"types": "./dist/next.d.ts",
|
|
21
|
+
"import": "./dist/next.js",
|
|
22
|
+
"default": "./dist/next.js"
|
|
23
|
+
},
|
|
24
|
+
"./events": {
|
|
25
|
+
"types": "./dist/events.d.ts",
|
|
26
|
+
"import": "./dist/events.js",
|
|
27
|
+
"default": "./dist/events.js"
|
|
28
|
+
},
|
|
29
|
+
"./ai-referrals": {
|
|
30
|
+
"types": "./dist/ai-referrals.d.ts",
|
|
31
|
+
"import": "./dist/ai-referrals.js",
|
|
32
|
+
"default": "./dist/ai-referrals.js"
|
|
33
|
+
},
|
|
19
34
|
"./package.json": "./package.json"
|
|
20
35
|
},
|
|
21
36
|
"files": [
|
|
@@ -23,15 +38,20 @@
|
|
|
23
38
|
"README.md"
|
|
24
39
|
],
|
|
25
40
|
"peerDependencies": {
|
|
41
|
+
"next": "^16.0.0",
|
|
26
42
|
"react": "^19.0.0"
|
|
27
43
|
},
|
|
28
44
|
"peerDependenciesMeta": {
|
|
45
|
+
"next": {
|
|
46
|
+
"optional": true
|
|
47
|
+
},
|
|
29
48
|
"react": {
|
|
30
49
|
"optional": true
|
|
31
50
|
}
|
|
32
51
|
},
|
|
33
52
|
"devDependencies": {
|
|
34
53
|
"@types/react": "^19.0.0",
|
|
54
|
+
"next": "^16.0.0",
|
|
35
55
|
"react": "^19.0.0",
|
|
36
56
|
"tsup": "^8.0.0",
|
|
37
57
|
"typescript": "^5.7.0",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/loader.ts","../src/consent-banner.ts","../src/beacon.ts"],"sourcesContent":["// ADR-0038 §2·§4 — 공유 주입 산출물. Astro·Next 렌더러가 동일하게 mount.\n//\n// 두 조각으로 head 에 inline:\n// 1. renderConsentBootstrap() — *가장 먼저*. Consent Mode v2 default = 전부\n// denied (fail-closed). 어떤 태그보다 앞서야 함.\n// 2. renderLoaderScript(manifest) — manifest 를 embed 한 런타임. 태그는 동의\n// update 전까지 발화하지 않음. 동의 배너가 `RootTaleAnalytics.updateConsent`\n// 를 호출하면 그때 허용된 태그만 주입.\n\nimport type { AnalyticsManifest } from \"./types\";\n\n/**\n * Consent Mode v2 default 부트스트랩. 전 카테고리 denied + `wait_for_update`.\n * 반드시 어떤 분석/광고 태그보다 먼저 실행돼야 한다.\n */\nexport function renderConsentBootstrap(): string {\n return [\n \"window.dataLayer=window.dataLayer||[];\",\n \"function gtag(){dataLayer.push(arguments);}\",\n \"gtag('consent','default',{\",\n \"'ad_storage':'denied','ad_user_data':'denied',\",\n \"'ad_personalization':'denied','analytics_storage':'denied',\",\n \"'wait_for_update':500});\",\n ].join(\"\");\n}\n\n// 브라우저 런타임 — manifest 를 읽고 동의가 충족된 태그만 주입한다.\n// `__RT_MANIFEST__` 는 renderLoaderScript 가 JSON 으로 치환.\nconst RUNTIME = `(function(){\nvar M=__RT_MANIFEST__;\nvar consent=Object.assign({},M.consentDefault);\nvar loaded={};\nfunction allowed(req){for(var i=0;i<req.length;i++){if(consent[req[i]]!=='granted')return false;}return true;}\nfunction inject(tag){\nif(loaded[tag.provider]||!allowed(tag.requiredConsent))return;\nloaded[tag.provider]=true;\nif(tag.provider==='ga4'){\nvar s=document.createElement('script');s.async=true;\ns.src='https://www.googletagmanager.com/gtag/js?id='+encodeURIComponent(tag.id);\ndocument.head.appendChild(s);gtag('js',new Date());gtag('config',tag.id);\n}else if(tag.provider==='clarity'){\n(function(c,l,a,r,i){c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};\nvar t=l.createElement(r);t.async=1;t.src='https://www.clarity.ms/tag/'+i;\nvar y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);})(window,document,'clarity','script',tag.id);\n}else if(tag.provider==='meta_pixel'){\n(function(f,b,e,v){if(f.fbq)return;var n=f.fbq=function(){n.callMethod?n.callMethod.apply(n,arguments):n.queue.push(arguments)};\nif(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';n.queue=[];var t=b.createElement(e);t.async=!0;\nt.src=v;var s=b.getElementsByTagName(e)[0];s.parentNode.insertBefore(t,s)})(window,document,'script','https://connect.facebook.net/en_US/fbevents.js');\nfbq('init',tag.id);fbq('track','PageView');\n}else if(tag.provider==='naver'){\nvar w=document.createElement('script');w.async=true;w.src='//wcs.naver.net/wcslog.js';\nw.onload=function(){if(window.wcs){window.wcs_add={wa:tag.id};if(window.wcs.inflow)window.wcs.inflow();if(window.wcs_do)window.wcs_do();}};\ndocument.head.appendChild(w);\n}}\nfunction applyAll(){for(var i=0;i<M.tags.length;i++){inject(M.tags[i]);}}\nwindow.RootTaleAnalytics={\nmanifest:M,\ngetConsent:function(){return Object.assign({},consent);},\nupdateConsent:function(partial){\nfor(var k in partial){if(Object.prototype.hasOwnProperty.call(consent,k))consent[k]=partial[k];}\nif(typeof gtag==='function'){gtag('consent','update',partial);}\napplyAll();\n}};\napplyAll();\n})();`;\n\n/**\n * manifest 를 embed 한 로더 스크립트. fail-closed — 기본 동의가 denied 라\n * `applyAll()` 은 아무 태그도 주입하지 않고, 동의 배너의 `updateConsent` 후에만\n * 허용된 태그가 붙는다.\n */\nexport function renderLoaderScript(manifest: AnalyticsManifest): string {\n return \"var __RT_MANIFEST__=\" + JSON.stringify(manifest) + \";\" + RUNTIME;\n}\n","// ADR-0038 §2 — 동의 배너. plane ① 외부 트래커는 동의 전까지 미발화(fail-closed)\n// 이므로, 배너가 방문자 선택을 받아 RootTaleAnalytics.updateConsent 를 호출한다.\n//\n// 주의: `set:html`/`dangerouslySetInnerHTML` 로 주입된 <script> 는 실행되지\n// 않는다. 본 모듈은 *마크업* 과 *JS 본문* 을 분리 반환하고, 렌더러가 JS 는\n// <script is:inline> 로, 마크업은 컨테이너로 주입해야 한다.\n//\n// 마크업/스크립트가 공유하는 DOM id (계약):\n// #rt-consent · #rt-consent-all · #rt-consent-analytics · #rt-consent-deny\n\nexport interface ConsentBannerOptions {\n /** 개인정보처리방침 링크 (테넌트별). 없으면 링크 생략. */\n privacyUrl?: string;\n /** 본문 카피. 기본 한국어. */\n message?: string;\n}\n\nconst BANNER_ID = \"rt-consent\";\nconst STORAGE_KEY = \"rt_consent_v1\";\n\n/** 배너 마크업(HTML). 스크립트 미포함 — 컨테이너로 주입. */\nexport function renderConsentBannerMarkup(\n options: ConsentBannerOptions = {},\n): string {\n const message =\n options.message ??\n \"이 사이트는 방문 분석·광고 도구를 사용할 수 있습니다. 동의 여부를 선택해 주세요.\";\n const privacy = options.privacyUrl\n ? ` <a href=\"${escapeAttr(options.privacyUrl)}\" target=\"_blank\" rel=\"noopener\">개인정보처리방침</a>`\n : \"\";\n\n return [\n `<div id=\"${BANNER_ID}\" role=\"dialog\" aria-live=\"polite\" aria-label=\"동의 안내\" hidden`,\n ` style=\"position:fixed;left:0;right:0;bottom:0;z-index:2147483647;`,\n `background:#fff;border-top:1px solid #e5e7eb;padding:16px;`,\n `display:flex;flex-wrap:wrap;gap:8px;align-items:center;justify-content:center;`,\n `font-size:14px;line-height:1.5;color:#111;box-shadow:0 -2px 12px rgba(0,0,0,.06)\">`,\n `<span style=\"flex:1 1 280px;min-width:240px\">${escapeHtml(message)}${privacy}</span>`,\n `<button id=\"rt-consent-all\" type=\"button\" style=\"${BTN_PRIMARY}\">모두 허용</button>`,\n `<button id=\"rt-consent-analytics\" type=\"button\" style=\"${BTN_SECONDARY}\">분석만 허용</button>`,\n `<button id=\"rt-consent-deny\" type=\"button\" style=\"${BTN_GHOST}\">거부</button>`,\n `</div>`,\n ].join(\"\");\n}\n\n/** 배너 동작(JS 본문). <script is:inline> 로 감싸 주입. */\nexport function renderConsentBannerScript(): string {\n return `(function(){\nvar KEY=${JSON.stringify(STORAGE_KEY)};\nvar el=document.getElementById(${JSON.stringify(BANNER_ID)});\nfunction apply(state){\nif(window.RootTaleAnalytics&&typeof window.RootTaleAnalytics.updateConsent==='function'){\nwindow.RootTaleAnalytics.updateConsent(state);\n}}\nfunction persist(state){try{localStorage.setItem(KEY,JSON.stringify(state));}catch(e){}}\nfunction read(){try{return JSON.parse(localStorage.getItem(KEY)||'null');}catch(e){return null;}}\nvar ALL={analytics_storage:'granted',ad_storage:'granted',ad_user_data:'granted',ad_personalization:'granted'};\nvar ANALYTICS={analytics_storage:'granted',ad_storage:'denied',ad_user_data:'denied',ad_personalization:'denied'};\nvar DENY={analytics_storage:'denied',ad_storage:'denied',ad_user_data:'denied',ad_personalization:'denied'};\nfunction choose(state){apply(state);persist(state);if(el){el.hidden=true;}}\nvar prior=read();\nif(prior){apply(prior);if(el){el.hidden=true;}}\nelse if(el){el.hidden=false;}\nfunction bind(id,state){var b=document.getElementById(id);if(b){b.addEventListener('click',function(){choose(state);});}}\nbind('rt-consent-all',ALL);\nbind('rt-consent-analytics',ANALYTICS);\nbind('rt-consent-deny',DENY);\n})();`;\n}\n\nconst BTN_BASE =\n \"padding:8px 14px;border-radius:8px;font-size:14px;cursor:pointer;border:1px solid #d1d5db;\";\nconst BTN_PRIMARY = BTN_BASE + \"background:#111;color:#fff;border-color:#111;\";\nconst BTN_SECONDARY = BTN_BASE + \"background:#f3f4f6;color:#111;\";\nconst BTN_GHOST = BTN_BASE + \"background:transparent;color:#6b7280;\";\n\nfunction escapeHtml(s: string): string {\n return s\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\");\n}\n\nfunction escapeAttr(s: string): string {\n return escapeHtml(s).replace(/\"/g, \""\");\n}\n","// ADR-0038 plane ② — first-party 비콘 dispatcher.\n//\n// `data-track` 컨벤션(className 아님): 루트 1개 delegated listener 가\n// `[data-track]` 클릭을 잡아 first-party `/collect` 로 전송. cms-blocks 가\n// 채워지면 컴포넌트가 `data-track` 을 emit → 모든 테넌트 사이트가 일관 이벤트.\n//\n// `window.__rtTrack(ev, extra?)` — 클릭이 아닌 시점(마운트 등)에 커스텀 이벤트를\n// 보내야 하는 극소수 케이스(예: starter `not-found.tsx` 의 404 비콘, W4-5 PR 2)\n// 를 위한 탈출구. `send()` 를 그대로 노출하므로 payload 형태·anonymize 정책은\n// data-track 경로와 동일(단일소스). 이 스크립트가 렌더되지 않은 사이트(비콘\n// 미설정 — collectUrl/siteId 없음)에서는 전역 자체가 정의되지 않으므로 호출부는\n// 항상 optional chaining(`window.__rtTrack?.(...)`) 으로 감싸 게이트를 존중한다.\n//\n// 동의 면제 조건(ADR-0038 §2): cookieless · cross-site 영속 ID 없음 ·\n// raw IP/UA 는 서버(/collect)에서 hash/truncate · 제3자 미공유. 따라서 본\n// dispatcher 는 동의 게이트 없이 동작(운영 목적 first-party 분석).\n//\n// dual-emit 금지: 본 비콘은 plane ② 전용. GA4(plane ①) 로는 보내지 않는다.\n//\n// ADR-0048 §1 — first-touch 어트리뷰션 캡처: 페이지 로드 시 landing path ·\n// rt_src(단축링크/QR 토큰) · utm 3종 · 외부 referrer hostname 을\n// localStorage[\"_rt_attr\"] 에 저장. 이미 있으면 유지(first-touch 우선, TTL 30일).\n// 식별자를 저장하지 않으므로 위 동의 면제 논리가 그대로 성립한다. 문의 폼이\n// `@roottale/cms-client` readAttribution() 으로 읽어 inquiry 에 동반시킨다 —\n// 키/값 형태는 cms-client 와의 공유 계약(변경 시 양쪽 동시 갱신).\n\nexport interface BeaconOptions {\n /** first-party 수집 엔드포인트 (예: https://api.roottale.com/v1/collect). */\n collectUrl: string;\n /** site 식별자 (테넌트 cross-site 아님 — site 단위 집계용). */\n siteId: string;\n /** pageview 자동 전송 (기본 true). */\n trackPageview?: boolean;\n /** first-touch 어트리뷰션 캡처 (ADR-0048 §1, 기본 true). */\n captureAttribution?: boolean;\n}\n\n/**\n * first-touch 저장 localStorage 키 — `@roottale/cms-client` readAttribution()\n * 과의 공유 계약. 값 형태는 {@link FirstTouchAttribution}.\n */\nexport const ATTRIBUTION_STORAGE_KEY = \"_rt_attr\";\n\n/** first-touch 보존 기간 (30일, ms). 경과 시 다음 방문이 새 first-touch. */\nexport const ATTRIBUTION_TTL_MS = 30 * 24 * 60 * 60 * 1000;\n\n/**\n * localStorage[\"_rt_attr\"] 에 저장되는 first-touch 형태 (식별자 없음).\n * 키를 짧게 유지 — 비콘 인라인 런타임과 폼 hidden field 양쪽에서 쓰인다.\n */\nexport interface FirstTouchAttribution {\n /** 포맷 버전. */\n v: 1;\n /** landing pathname (query 제외). */\n lp: string;\n /** 캡처 시각 (epoch ms). */\n ts: number;\n /** rt_src — 단축링크/QR 토큰 (있을 때만). */\n rs?: string;\n /** utm_source (있을 때만). */\n us?: string;\n /** utm_medium (있을 때만). */\n um?: string;\n /** utm_campaign (있을 때만). */\n uc?: string;\n /** 외부 referrer hostname — raw URL 미저장, 동일 host 면 생략. */\n rf?: string;\n}\n\nconst RUNTIME = `(function(){\nvar C=__RT_BEACON__;\nfunction metaPid(){try{var m=document.querySelector('meta[name=\"rt:content-id\"]');var v=m&&m.getAttribute('content');return v?String(v).slice(0,64):null;}catch(e){return null;}}\nfunction send(ev,extra){\ntry{\nvar p={ev:ev,sid:C.sid,path:location.pathname,ref:document.referrer?1:0,ts:Date.now()};\nif(extra){for(var k in extra){p[k]=extra[k];}}\nvar body=JSON.stringify(p);\nif(navigator.sendBeacon){navigator.sendBeacon(C.ep,new Blob([body],{type:'application/json'}));}\nelse{fetch(C.ep,{method:'POST',body:body,headers:{'Content-Type':'application/json'},keepalive:true}).catch(function(){});}\n}catch(e){}\n}\nif(typeof window!=='undefined'){window.__rtTrack=send;}\ndocument.addEventListener('click',function(e){\nvar t=e.target;\nvar el=t&&t.closest?t.closest('[data-track]'):null;\nif(!el)return;\nvar extra={};\nvar ds=el.dataset||{};\nfor(var k in ds){if(k.indexOf('track')===0&&k!=='track'){extra[k]=ds[k];}}\nsend(el.getAttribute('data-track'),extra);\n},{capture:true});\nif(C.attr){\ntry{\nvar AK='_rt_attr';\nvar cur=null;\ntry{cur=JSON.parse(localStorage.getItem(AK)||'null');}catch(e){}\nif(!cur||typeof cur.ts!=='number'||Date.now()-cur.ts>2592e6){\nvar sp=new URLSearchParams(location.search);\nvar a={v:1,lp:location.pathname,ts:Date.now()};\nvar rs=sp.get('rt_src');if(rs)a.rs=rs.slice(0,32);\nvar us=sp.get('utm_source');if(us)a.us=us.slice(0,100);\nvar um=sp.get('utm_medium');if(um)a.um=um.slice(0,100);\nvar uc=sp.get('utm_campaign');if(uc)a.uc=uc.slice(0,100);\ntry{if(document.referrer){var rh=new URL(document.referrer).hostname;if(rh&&rh!==location.hostname)a.rf=rh.slice(0,253);}}catch(e){}\nlocalStorage.setItem(AK,JSON.stringify(a));\n}\n}catch(e){}\n}\nif(C.pv){var _pid=metaPid();send('pageview',_pid?{pid:_pid}:null);}\n})();`;\n\n/**\n * 비콘 dispatcher JS 본문. <script is:inline> 로 감싸 주입.\n * `ref` 는 referrer 존재 여부(0/1)만 — raw referrer 미전송(privacy).\n *\n * ADR-0065 — 글 조회수 식별자: pageview 시 `<meta name=\"rt:content-id\">` 의\n * content 를 읽어 `pid`(=post_id) 로 동봉한다. 사이트 레벨 단일 비콘이 페이지별\n * 글 식별자를 갖게 해 URL/컬렉션 basePath 변경에 무관한 안정 매칭을 만든다.\n * meta 미존재 시 `pid` 생략(graceful) — `/collect` 가 post_id 없는 pageview 로 처리.\n * 값 형태는 `/v1/collect` 와의 공유 계약(변경 시 양쪽 동시 갱신).\n */\nexport function renderBeaconScript(options: BeaconOptions): string {\n const cfg = JSON.stringify({\n ep: options.collectUrl,\n sid: options.siteId,\n pv: options.trackPageview !== false,\n attr: options.captureAttribution !== false,\n });\n return \"var __RT_BEACON__=\" + cfg + \";\" + RUNTIME;\n}\n\ndeclare global {\n interface Window {\n /** RUNTIME 이 노출하는 send() — data-track 클릭 delegation 과 동일 함수. */\n __rtTrack?: (ev: string, extra?: Record<string, unknown>) => void;\n }\n}\n\n/**\n * `window.__rtTrack` 안전 호출 래퍼(W4-5 PR 2) — 클릭이 아닌 시점(마운트 등)\n * 에 커스텀 이벤트를 보내야 하는 클라이언트 컴포넌트(예: starter\n * `app/not-found.tsx` 의 404 비콘)의 표준 진입점. `RootTaleAnalytics` 가 비콘\n * 없이 렌더된 사이트(collectUrl/siteId 미설정)에서는 전역 자체가 없으므로\n * 자동으로 no-op — 별도 게이트 없이 안전.\n */\nexport function trackCustomEvent(ev: string, extra?: Record<string, unknown>): void {\n if (typeof window === \"undefined\") return;\n window.__rtTrack?.(ev, extra);\n}\n"],"mappings":";AAeO,SAAS,yBAAiC;AAC/C,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,EAAE,KAAK,EAAE;AACX;AAIA,IAAM,UAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA2CT,SAAS,mBAAmB,UAAqC;AACtE,SAAO,yBAAyB,KAAK,UAAU,QAAQ,IAAI,MAAM;AACnE;;;ACxDA,IAAM,YAAY;AAClB,IAAM,cAAc;AAGb,SAAS,0BACd,UAAgC,CAAC,GACzB;AACR,QAAM,UACJ,QAAQ,WACR;AACF,QAAM,UAAU,QAAQ,aACpB,aAAa,WAAW,QAAQ,UAAU,CAAC,0FAC3C;AAEJ,SAAO;AAAA,IACL,YAAY,SAAS;AAAA,IACrB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,gDAAgD,WAAW,OAAO,CAAC,GAAG,OAAO;AAAA,IAC7E,oDAAoD,WAAW;AAAA,IAC/D,0DAA0D,aAAa;AAAA,IACvE,qDAAqD,SAAS;AAAA,IAC9D;AAAA,EACF,EAAE,KAAK,EAAE;AACX;AAGO,SAAS,4BAAoC;AAClD,SAAO;AAAA,UACC,KAAK,UAAU,WAAW,CAAC;AAAA,iCACJ,KAAK,UAAU,SAAS,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAmB1D;AAEA,IAAM,WACJ;AACF,IAAM,cAAc,WAAW;AAC/B,IAAM,gBAAgB,WAAW;AACjC,IAAM,YAAY,WAAW;AAE7B,SAAS,WAAW,GAAmB;AACrC,SAAO,EACJ,QAAQ,MAAM,OAAO,EACrB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,MAAM;AACzB;AAEA,SAAS,WAAW,GAAmB;AACrC,SAAO,WAAW,CAAC,EAAE,QAAQ,MAAM,QAAQ;AAC7C;;;AC5CO,IAAM,0BAA0B;AAGhC,IAAM,qBAAqB,KAAK,KAAK,KAAK,KAAK;AAyBtD,IAAMA,WAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAoDT,SAAS,mBAAmB,SAAgC;AACjE,QAAM,MAAM,KAAK,UAAU;AAAA,IACzB,IAAI,QAAQ;AAAA,IACZ,KAAK,QAAQ;AAAA,IACb,IAAI,QAAQ,kBAAkB;AAAA,IAC9B,MAAM,QAAQ,uBAAuB;AAAA,EACvC,CAAC;AACD,SAAO,uBAAuB,MAAM,MAAMA;AAC5C;AAgBO,SAAS,iBAAiB,IAAY,OAAuC;AAClF,MAAI,OAAO,WAAW,YAAa;AACnC,SAAO,YAAY,IAAI,KAAK;AAC9B;","names":["RUNTIME"]}
|