@roottale/analytics-runtime 0.1.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/README.md ADDED
@@ -0,0 +1,76 @@
1
+ # @roottale/analytics-runtime
2
+
3
+ ADR-0038 plane ① (외부 트래커 주입) + 동의(Consent Mode v2 fail-closed) + plane ②
4
+ (first-party 비콘) 의 **프레임워크/DB 무관** 런타임. 문자열만 산출하므로
5
+ `cms-renderer-astro`·`cms-renderer-next` 가 동일하게 mount 한다.
6
+
7
+ ## ⚠️ 주입 규칙
8
+
9
+ `set:html` / `dangerouslySetInnerHTML` 로 주입한 `<script>` 는 **실행되지 않는다**.
10
+ 본 패키지 함수는 *JS 본문* 또는 *HTML 마크업* 만 반환하고, 렌더러가 JS 는
11
+ 실제 `<script>` 엘리먼트로 감싼다.
12
+
13
+ - Astro: `<script is:inline set:html={js} />`
14
+ - Next: `<script dangerouslySetInnerHTML={{ __html: js }} />`
15
+
16
+ ## head 주입 (순서 중요)
17
+
18
+ 1. `renderConsentBootstrap()` — **가장 먼저**. Consent Mode v2 default 전부 denied.
19
+ 2. `renderLoaderScript(manifest)` — 태그 로더. 동의 update 전 미발화(fail-closed).
20
+
21
+ ```ts
22
+ import {
23
+ buildManifest,
24
+ renderConsentBootstrap,
25
+ renderLoaderScript,
26
+ } from "@roottale/analytics-runtime";
27
+
28
+ const manifest = buildManifest({ siteId, tags }); // tags = SiteAnalyticsTagsRepo.listForSite()
29
+ const bootstrap = renderConsentBootstrap();
30
+ const loader = renderLoaderScript(manifest);
31
+ ```
32
+
33
+ ## body 주입
34
+
35
+ - `renderConsentBannerMarkup(opts)` — 배너 HTML(컨테이너로 주입). 기본 숨김.
36
+ - `renderConsentBannerScript()` — 배너 동작 JS(선택→`updateConsent`+localStorage).
37
+ - `renderBeaconScript({ collectUrl, siteId })` — plane ② `data-track` dispatcher.
38
+ cookieless·anonymized 라 동의 게이트 없이 동작(ADR-0038 §2).
39
+
40
+ ## Astro mount 예시 (`web-front/src/layouts/BaseLayout.astro`)
41
+
42
+ ```astro
43
+ ---
44
+ import {
45
+ buildManifest, renderConsentBootstrap, renderLoaderScript,
46
+ renderConsentBannerMarkup, renderConsentBannerScript, renderBeaconScript,
47
+ } from "@roottale/analytics-runtime";
48
+ const { manifest, siteId, collectUrl, privacyUrl } = Astro.props;
49
+ const bootstrap = renderConsentBootstrap();
50
+ const loader = renderLoaderScript(manifest);
51
+ const beacon = renderBeaconScript({ collectUrl, siteId });
52
+ const bannerJs = renderConsentBannerScript();
53
+ ---
54
+ <head>
55
+ <script is:inline set:html={bootstrap} />
56
+ <script is:inline set:html={loader} />
57
+ <!-- ...기존 SEO 메타... -->
58
+ </head>
59
+ <body>
60
+ <slot />
61
+ <Fragment set:html={renderConsentBannerMarkup({ privacyUrl })} />
62
+ <script is:inline set:html={bannerJs} />
63
+ <script is:inline set:html={beacon} />
64
+ </body>
65
+ ```
66
+
67
+ `[slug].astro` 가 `getBuildClient()` 로 `site.id`/`site.tenantId` 를 얻고,
68
+ `SiteAnalyticsTagsRepo(db, tenantId).listForSite(siteId)` → `buildManifest` →
69
+ `BaseLayout` props 로 전달.
70
+
71
+ ## 상태 (ADR-0038)
72
+
73
+ - ✅ 로직·산출물·동의 배너·비콘 dispatcher (본 패키지, 단위 테스트)
74
+ - ⏳ web-front mount (workspace dep 등록 = `pnpm install` 후 BaseLayout 주입)
75
+ - ⏳ plane ② `/collect` 엔드포인트 (tenant-api + CF Analytics Engine)
76
+ - ⏳ plane ③ GA4/GSC ETL + `getWeeklyVisitors()`
@@ -0,0 +1,145 @@
1
+ // src/loader.ts
2
+ function renderConsentBootstrap() {
3
+ return [
4
+ "window.dataLayer=window.dataLayer||[];",
5
+ "function gtag(){dataLayer.push(arguments);}",
6
+ "gtag('consent','default',{",
7
+ "'ad_storage':'denied','ad_user_data':'denied',",
8
+ "'ad_personalization':'denied','analytics_storage':'denied',",
9
+ "'wait_for_update':500});"
10
+ ].join("");
11
+ }
12
+ var RUNTIME = `(function(){
13
+ var M=__RT_MANIFEST__;
14
+ var consent=Object.assign({},M.consentDefault);
15
+ var loaded={};
16
+ function allowed(req){for(var i=0;i<req.length;i++){if(consent[req[i]]!=='granted')return false;}return true;}
17
+ function inject(tag){
18
+ if(loaded[tag.provider]||!allowed(tag.requiredConsent))return;
19
+ loaded[tag.provider]=true;
20
+ if(tag.provider==='ga4'){
21
+ var s=document.createElement('script');s.async=true;
22
+ s.src='https://www.googletagmanager.com/gtag/js?id='+encodeURIComponent(tag.id);
23
+ document.head.appendChild(s);gtag('js',new Date());gtag('config',tag.id);
24
+ }else if(tag.provider==='clarity'){
25
+ (function(c,l,a,r,i){c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};
26
+ var t=l.createElement(r);t.async=1;t.src='https://www.clarity.ms/tag/'+i;
27
+ var y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);})(window,document,'clarity','script',tag.id);
28
+ }else if(tag.provider==='meta_pixel'){
29
+ (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)};
30
+ if(!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;
31
+ t.src=v;var s=b.getElementsByTagName(e)[0];s.parentNode.insertBefore(t,s)})(window,document,'script','https://connect.facebook.net/en_US/fbevents.js');
32
+ fbq('init',tag.id);fbq('track','PageView');
33
+ }else if(tag.provider==='naver'){
34
+ var w=document.createElement('script');w.async=true;w.src='//wcs.naver.net/wcslog.js';
35
+ w.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();}};
36
+ document.head.appendChild(w);
37
+ }}
38
+ function applyAll(){for(var i=0;i<M.tags.length;i++){inject(M.tags[i]);}}
39
+ window.RootTaleAnalytics={
40
+ manifest:M,
41
+ getConsent:function(){return Object.assign({},consent);},
42
+ updateConsent:function(partial){
43
+ for(var k in partial){if(Object.prototype.hasOwnProperty.call(consent,k))consent[k]=partial[k];}
44
+ if(typeof gtag==='function'){gtag('consent','update',partial);}
45
+ applyAll();
46
+ }};
47
+ applyAll();
48
+ })();`;
49
+ function renderLoaderScript(manifest) {
50
+ return "var __RT_MANIFEST__=" + JSON.stringify(manifest) + ";" + RUNTIME;
51
+ }
52
+
53
+ // src/consent-banner.ts
54
+ var BANNER_ID = "rt-consent";
55
+ var STORAGE_KEY = "rt_consent_v1";
56
+ function renderConsentBannerMarkup(options = {}) {
57
+ const message = options.message ?? "\uC774 \uC0AC\uC774\uD2B8\uB294 \uBC29\uBB38 \uBD84\uC11D\xB7\uAD11\uACE0 \uB3C4\uAD6C\uB97C \uC0AC\uC6A9\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4. \uB3D9\uC758 \uC5EC\uBD80\uB97C \uC120\uD0DD\uD574 \uC8FC\uC138\uC694.";
58
+ const privacy = options.privacyUrl ? ` <a href="${escapeAttr(options.privacyUrl)}" target="_blank" rel="noopener">\uAC1C\uC778\uC815\uBCF4\uCC98\uB9AC\uBC29\uCE68</a>` : "";
59
+ return [
60
+ `<div id="${BANNER_ID}" role="dialog" aria-live="polite" aria-label="\uB3D9\uC758 \uC548\uB0B4" hidden`,
61
+ ` style="position:fixed;left:0;right:0;bottom:0;z-index:2147483647;`,
62
+ `background:#fff;border-top:1px solid #e5e7eb;padding:16px;`,
63
+ `display:flex;flex-wrap:wrap;gap:8px;align-items:center;justify-content:center;`,
64
+ `font-size:14px;line-height:1.5;color:#111;box-shadow:0 -2px 12px rgba(0,0,0,.06)">`,
65
+ `<span style="flex:1 1 280px;min-width:240px">${escapeHtml(message)}${privacy}</span>`,
66
+ `<button id="rt-consent-all" type="button" style="${BTN_PRIMARY}">\uBAA8\uB450 \uD5C8\uC6A9</button>`,
67
+ `<button id="rt-consent-analytics" type="button" style="${BTN_SECONDARY}">\uBD84\uC11D\uB9CC \uD5C8\uC6A9</button>`,
68
+ `<button id="rt-consent-deny" type="button" style="${BTN_GHOST}">\uAC70\uBD80</button>`,
69
+ `</div>`
70
+ ].join("");
71
+ }
72
+ function renderConsentBannerScript() {
73
+ return `(function(){
74
+ var KEY=${JSON.stringify(STORAGE_KEY)};
75
+ var el=document.getElementById(${JSON.stringify(BANNER_ID)});
76
+ function apply(state){
77
+ if(window.RootTaleAnalytics&&typeof window.RootTaleAnalytics.updateConsent==='function'){
78
+ window.RootTaleAnalytics.updateConsent(state);
79
+ }}
80
+ function persist(state){try{localStorage.setItem(KEY,JSON.stringify(state));}catch(e){}}
81
+ function read(){try{return JSON.parse(localStorage.getItem(KEY)||'null');}catch(e){return null;}}
82
+ var ALL={analytics_storage:'granted',ad_storage:'granted',ad_user_data:'granted',ad_personalization:'granted'};
83
+ var ANALYTICS={analytics_storage:'granted',ad_storage:'denied',ad_user_data:'denied',ad_personalization:'denied'};
84
+ var DENY={analytics_storage:'denied',ad_storage:'denied',ad_user_data:'denied',ad_personalization:'denied'};
85
+ function choose(state){apply(state);persist(state);if(el){el.hidden=true;}}
86
+ var prior=read();
87
+ if(prior){apply(prior);if(el){el.hidden=true;}}
88
+ else if(el){el.hidden=false;}
89
+ function bind(id,state){var b=document.getElementById(id);if(b){b.addEventListener('click',function(){choose(state);});}}
90
+ bind('rt-consent-all',ALL);
91
+ bind('rt-consent-analytics',ANALYTICS);
92
+ bind('rt-consent-deny',DENY);
93
+ })();`;
94
+ }
95
+ var BTN_BASE = "padding:8px 14px;border-radius:8px;font-size:14px;cursor:pointer;border:1px solid #d1d5db;";
96
+ var BTN_PRIMARY = BTN_BASE + "background:#111;color:#fff;border-color:#111;";
97
+ var BTN_SECONDARY = BTN_BASE + "background:#f3f4f6;color:#111;";
98
+ var BTN_GHOST = BTN_BASE + "background:transparent;color:#6b7280;";
99
+ function escapeHtml(s) {
100
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
101
+ }
102
+ function escapeAttr(s) {
103
+ return escapeHtml(s).replace(/"/g, "&quot;");
104
+ }
105
+
106
+ // src/beacon.ts
107
+ var RUNTIME2 = `(function(){
108
+ var C=__RT_BEACON__;
109
+ function send(ev,extra){
110
+ try{
111
+ var p={ev:ev,sid:C.sid,path:location.pathname,ref:document.referrer?1:0,ts:Date.now()};
112
+ if(extra){for(var k in extra){p[k]=extra[k];}}
113
+ var body=JSON.stringify(p);
114
+ if(navigator.sendBeacon){navigator.sendBeacon(C.ep,new Blob([body],{type:'application/json'}));}
115
+ else{fetch(C.ep,{method:'POST',body:body,headers:{'Content-Type':'application/json'},keepalive:true}).catch(function(){});}
116
+ }catch(e){}
117
+ }
118
+ document.addEventListener('click',function(e){
119
+ var t=e.target;
120
+ var el=t&&t.closest?t.closest('[data-track]'):null;
121
+ if(!el)return;
122
+ var extra={};
123
+ var ds=el.dataset||{};
124
+ for(var k in ds){if(k.indexOf('track')===0&&k!=='track'){extra[k]=ds[k];}}
125
+ send(el.getAttribute('data-track'),extra);
126
+ },{capture:true});
127
+ if(C.pv){send('pageview',null);}
128
+ })();`;
129
+ function renderBeaconScript(options) {
130
+ const cfg = JSON.stringify({
131
+ ep: options.collectUrl,
132
+ sid: options.siteId,
133
+ pv: options.trackPageview !== false
134
+ });
135
+ return "var __RT_BEACON__=" + cfg + ";" + RUNTIME2;
136
+ }
137
+
138
+ export {
139
+ renderConsentBootstrap,
140
+ renderLoaderScript,
141
+ renderConsentBannerMarkup,
142
+ renderConsentBannerScript,
143
+ renderBeaconScript
144
+ };
145
+ //# sourceMappingURL=chunk-6SFRLOFD.js.map
@@ -0,0 +1 @@
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, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\");\n}\n\nfunction escapeAttr(s: string): string {\n return escapeHtml(s).replace(/\"/g, \"&quot;\");\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// 동의 면제 조건(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\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}\n\nconst RUNTIME = `(function(){\nvar C=__RT_BEACON__;\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}\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.pv){send('pageview',null);}\n})();`;\n\n/**\n * 비콘 dispatcher JS 본문. <script is:inline> 로 감싸 주입.\n * `ref` 는 referrer 존재 여부(0/1)만 — raw referrer 미전송(privacy).\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 });\n return \"var __RT_BEACON__=\" + cfg + \";\" + RUNTIME;\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;;;AChEA,IAAMA,WAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA2BT,SAAS,mBAAmB,SAAgC;AACjE,QAAM,MAAM,KAAK,UAAU;AAAA,IACzB,IAAI,QAAQ;AAAA,IACZ,KAAK,QAAQ;AAAA,IACb,IAAI,QAAQ,kBAAkB;AAAA,EAChC,CAAC;AACD,SAAO,uBAAuB,MAAM,MAAMA;AAC5C;","names":["RUNTIME"]}
@@ -0,0 +1,53 @@
1
+ /** Google Consent Mode v2 카테고리. */
2
+ type ConsentCategory = "analytics_storage" | "ad_storage" | "ad_user_data" | "ad_personalization";
3
+ type ConsentValue = "granted" | "denied";
4
+ type ConsentState = Record<ConsentCategory, ConsentValue>;
5
+ /**
6
+ * plane ① 태그 provider (비밀 아닌 public id 로 주입).
7
+ * - `ga4`: measurement id (G-XXXXXXX)
8
+ * - `clarity`: project id
9
+ * - `meta_pixel`: pixel id
10
+ * - `naver`: Naver Analytics(wcslog) id. *Search Advisor 사이트 인증*(meta 태그)
11
+ * 은 plane ③ 으로 별도 — 본 런타임 태그 아님.
12
+ */
13
+ type TagProvider = "ga4" | "clarity" | "meta_pixel" | "naver";
14
+ /** 테넌트가 settings/integrations 에서 붙여넣는 태그 설정 (비밀 아님). */
15
+ interface TagConfig {
16
+ provider: TagProvider;
17
+ id: string;
18
+ enabled: boolean;
19
+ }
20
+ /** 한 site 의 plane ① 태그 구성 입력. */
21
+ interface AnalyticsSiteConfig {
22
+ siteId: string;
23
+ tags: TagConfig[];
24
+ }
25
+ /** manifest 로 해석된 태그 — 발화 전 필요한 동의 카테고리 포함. */
26
+ interface ResolvedTag {
27
+ provider: TagProvider;
28
+ id: string;
29
+ /** 이 태그가 발화하기 전 *모두* granted 여야 하는 동의 카테고리. */
30
+ requiredConsent: ConsentCategory[];
31
+ }
32
+ /**
33
+ * 렌더러가 주입하는 최종 manifest. 동의 기본값은 *항상 전부 denied*
34
+ * (fail-closed) — 태그는 동의 update 전까지 발화하지 않는다.
35
+ */
36
+ interface AnalyticsManifest {
37
+ siteId: string;
38
+ tags: ResolvedTag[];
39
+ consentDefault: ConsentState;
40
+ }
41
+
42
+ interface ConsentBannerOptions {
43
+ /** 개인정보처리방침 링크 (테넌트별). 없으면 링크 생략. */
44
+ privacyUrl?: string;
45
+ /** 본문 카피. 기본 한국어. */
46
+ message?: string;
47
+ }
48
+ /** 배너 마크업(HTML). 스크립트 미포함 — 컨테이너로 주입. */
49
+ declare function renderConsentBannerMarkup(options?: ConsentBannerOptions): string;
50
+ /** 배너 동작(JS 본문). <script is:inline> 로 감싸 주입. */
51
+ declare function renderConsentBannerScript(): string;
52
+
53
+ export { type AnalyticsSiteConfig as A, type ConsentCategory as C, type ResolvedTag as R, type TagProvider as T, type ConsentState as a, type AnalyticsManifest as b, type ConsentBannerOptions as c, type ConsentValue as d, type TagConfig as e, renderConsentBannerScript as f, renderConsentBannerMarkup as r };
@@ -0,0 +1,52 @@
1
+ import { C as ConsentCategory, a as ConsentState, T as TagProvider, A as AnalyticsSiteConfig, b as AnalyticsManifest } from './consent-banner-WVGsxcY2.js';
2
+ export { c as ConsentBannerOptions, d as ConsentValue, R as ResolvedTag, e as TagConfig, r as renderConsentBannerMarkup, f as renderConsentBannerScript } from './consent-banner-WVGsxcY2.js';
3
+
4
+ declare const ALL_CONSENT_CATEGORIES: readonly ConsentCategory[];
5
+ /** fail-closed 기본값 — 전부 denied. */
6
+ declare const DENIED_CONSENT: ConsentState;
7
+ /**
8
+ * provider 가 발화하기 전 granted 여야 하는 동의 카테고리.
9
+ * - analytics 도구(GA4·Clarity·Naver) → `analytics_storage`
10
+ * - 광고 도구(Meta Pixel) → `ad_storage` + `ad_user_data` + `ad_personalization`
11
+ * (국외이전·광고목적이라 동의 요건 가장 강함)
12
+ */
13
+ declare const PROVIDER_CONSENT: Record<TagProvider, ConsentCategory[]>;
14
+ /** required 카테고리가 *모두* granted 일 때만 true. */
15
+ declare function isTagAllowed(required: readonly ConsentCategory[], consent: ConsentState): boolean;
16
+
17
+ /**
18
+ * site 태그 구성을 manifest 로 해석.
19
+ * - `enabled=false` 또는 빈 id 는 제외.
20
+ * - provider 당 1개 (중복은 첫 항목만).
21
+ * - 각 태그에 필요한 동의 카테고리(`PROVIDER_CONSENT`)를 부착.
22
+ * - 동의 기본값은 항상 전부 denied (fail-closed).
23
+ */
24
+ declare function buildManifest(config: AnalyticsSiteConfig): AnalyticsManifest;
25
+
26
+ /**
27
+ * Consent Mode v2 default 부트스트랩. 전 카테고리 denied + `wait_for_update`.
28
+ * 반드시 어떤 분석/광고 태그보다 먼저 실행돼야 한다.
29
+ */
30
+ declare function renderConsentBootstrap(): string;
31
+ /**
32
+ * manifest 를 embed 한 로더 스크립트. fail-closed — 기본 동의가 denied 라
33
+ * `applyAll()` 은 아무 태그도 주입하지 않고, 동의 배너의 `updateConsent` 후에만
34
+ * 허용된 태그가 붙는다.
35
+ */
36
+ declare function renderLoaderScript(manifest: AnalyticsManifest): string;
37
+
38
+ interface BeaconOptions {
39
+ /** first-party 수집 엔드포인트 (예: https://api.roottale.com/v1/collect). */
40
+ collectUrl: string;
41
+ /** site 식별자 (테넌트 cross-site 아님 — site 단위 집계용). */
42
+ siteId: string;
43
+ /** pageview 자동 전송 (기본 true). */
44
+ trackPageview?: boolean;
45
+ }
46
+ /**
47
+ * 비콘 dispatcher JS 본문. <script is:inline> 로 감싸 주입.
48
+ * `ref` 는 referrer 존재 여부(0/1)만 — raw referrer 미전송(privacy).
49
+ */
50
+ declare function renderBeaconScript(options: BeaconOptions): string;
51
+
52
+ export { ALL_CONSENT_CATEGORIES, AnalyticsManifest, AnalyticsSiteConfig, type BeaconOptions, ConsentCategory, ConsentState, DENIED_CONSENT, PROVIDER_CONSENT, TagProvider, buildManifest, isTagAllowed, renderBeaconScript, renderConsentBootstrap, renderLoaderScript };
package/dist/index.js ADDED
@@ -0,0 +1,64 @@
1
+ import {
2
+ renderBeaconScript,
3
+ renderConsentBannerMarkup,
4
+ renderConsentBannerScript,
5
+ renderConsentBootstrap,
6
+ renderLoaderScript
7
+ } from "./chunk-6SFRLOFD.js";
8
+
9
+ // src/consent.ts
10
+ var ALL_CONSENT_CATEGORIES = [
11
+ "analytics_storage",
12
+ "ad_storage",
13
+ "ad_user_data",
14
+ "ad_personalization"
15
+ ];
16
+ var DENIED_CONSENT = {
17
+ analytics_storage: "denied",
18
+ ad_storage: "denied",
19
+ ad_user_data: "denied",
20
+ ad_personalization: "denied"
21
+ };
22
+ var PROVIDER_CONSENT = {
23
+ ga4: ["analytics_storage"],
24
+ clarity: ["analytics_storage"],
25
+ naver: ["analytics_storage"],
26
+ meta_pixel: ["ad_storage", "ad_user_data", "ad_personalization"]
27
+ };
28
+ function isTagAllowed(required, consent) {
29
+ return required.every((c) => consent[c] === "granted");
30
+ }
31
+
32
+ // src/manifest.ts
33
+ function buildManifest(config) {
34
+ const seen = /* @__PURE__ */ new Set();
35
+ const tags = config.tags.filter(
36
+ (t) => t.enabled && typeof t.id === "string" && t.id.trim() !== ""
37
+ ).filter((t) => {
38
+ if (seen.has(t.provider)) return false;
39
+ seen.add(t.provider);
40
+ return true;
41
+ }).map((t) => ({
42
+ provider: t.provider,
43
+ id: t.id.trim(),
44
+ requiredConsent: PROVIDER_CONSENT[t.provider]
45
+ }));
46
+ return {
47
+ siteId: config.siteId,
48
+ tags,
49
+ consentDefault: { ...DENIED_CONSENT }
50
+ };
51
+ }
52
+ export {
53
+ ALL_CONSENT_CATEGORIES,
54
+ DENIED_CONSENT,
55
+ PROVIDER_CONSENT,
56
+ buildManifest,
57
+ isTagAllowed,
58
+ renderBeaconScript,
59
+ renderConsentBannerMarkup,
60
+ renderConsentBannerScript,
61
+ renderConsentBootstrap,
62
+ renderLoaderScript
63
+ };
64
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/consent.ts","../src/manifest.ts"],"sourcesContent":["// ADR-0038 §2 — 동의 fail-closed + provider↔consent 매핑.\n\nimport type { ConsentCategory, ConsentState, TagProvider } from \"./types\";\n\nexport const ALL_CONSENT_CATEGORIES: readonly ConsentCategory[] = [\n \"analytics_storage\",\n \"ad_storage\",\n \"ad_user_data\",\n \"ad_personalization\",\n] as const;\n\n/** fail-closed 기본값 — 전부 denied. */\nexport const DENIED_CONSENT: ConsentState = {\n analytics_storage: \"denied\",\n ad_storage: \"denied\",\n ad_user_data: \"denied\",\n ad_personalization: \"denied\",\n};\n\n/**\n * provider 가 발화하기 전 granted 여야 하는 동의 카테고리.\n * - analytics 도구(GA4·Clarity·Naver) → `analytics_storage`\n * - 광고 도구(Meta Pixel) → `ad_storage` + `ad_user_data` + `ad_personalization`\n * (국외이전·광고목적이라 동의 요건 가장 강함)\n */\nexport const PROVIDER_CONSENT: Record<TagProvider, ConsentCategory[]> = {\n ga4: [\"analytics_storage\"],\n clarity: [\"analytics_storage\"],\n naver: [\"analytics_storage\"],\n meta_pixel: [\"ad_storage\", \"ad_user_data\", \"ad_personalization\"],\n};\n\n/** required 카테고리가 *모두* granted 일 때만 true. */\nexport function isTagAllowed(\n required: readonly ConsentCategory[],\n consent: ConsentState,\n): boolean {\n return required.every((c) => consent[c] === \"granted\");\n}\n","// ADR-0038 §1 — site 태그 구성 → manifest (순수 함수).\n\nimport { DENIED_CONSENT, PROVIDER_CONSENT } from \"./consent\";\nimport type { AnalyticsManifest, AnalyticsSiteConfig } from \"./types\";\n\n/**\n * site 태그 구성을 manifest 로 해석.\n * - `enabled=false` 또는 빈 id 는 제외.\n * - provider 당 1개 (중복은 첫 항목만).\n * - 각 태그에 필요한 동의 카테고리(`PROVIDER_CONSENT`)를 부착.\n * - 동의 기본값은 항상 전부 denied (fail-closed).\n */\nexport function buildManifest(config: AnalyticsSiteConfig): AnalyticsManifest {\n const seen = new Set<string>();\n const tags = config.tags\n .filter(\n (t) => t.enabled && typeof t.id === \"string\" && t.id.trim() !== \"\",\n )\n .filter((t) => {\n if (seen.has(t.provider)) return false;\n seen.add(t.provider);\n return true;\n })\n .map((t) => ({\n provider: t.provider,\n id: t.id.trim(),\n requiredConsent: PROVIDER_CONSENT[t.provider],\n }));\n\n return {\n siteId: config.siteId,\n tags,\n consentDefault: { ...DENIED_CONSENT },\n };\n}\n"],"mappings":";;;;;;;;;AAIO,IAAM,yBAAqD;AAAA,EAChE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAGO,IAAM,iBAA+B;AAAA,EAC1C,mBAAmB;AAAA,EACnB,YAAY;AAAA,EACZ,cAAc;AAAA,EACd,oBAAoB;AACtB;AAQO,IAAM,mBAA2D;AAAA,EACtE,KAAK,CAAC,mBAAmB;AAAA,EACzB,SAAS,CAAC,mBAAmB;AAAA,EAC7B,OAAO,CAAC,mBAAmB;AAAA,EAC3B,YAAY,CAAC,cAAc,gBAAgB,oBAAoB;AACjE;AAGO,SAAS,aACd,UACA,SACS;AACT,SAAO,SAAS,MAAM,CAAC,MAAM,QAAQ,CAAC,MAAM,SAAS;AACvD;;;AC1BO,SAAS,cAAc,QAAgD;AAC5E,QAAM,OAAO,oBAAI,IAAY;AAC7B,QAAM,OAAO,OAAO,KACjB;AAAA,IACC,CAAC,MAAM,EAAE,WAAW,OAAO,EAAE,OAAO,YAAY,EAAE,GAAG,KAAK,MAAM;AAAA,EAClE,EACC,OAAO,CAAC,MAAM;AACb,QAAI,KAAK,IAAI,EAAE,QAAQ,EAAG,QAAO;AACjC,SAAK,IAAI,EAAE,QAAQ;AACnB,WAAO;AAAA,EACT,CAAC,EACA,IAAI,CAAC,OAAO;AAAA,IACX,UAAU,EAAE;AAAA,IACZ,IAAI,EAAE,GAAG,KAAK;AAAA,IACd,iBAAiB,iBAAiB,EAAE,QAAQ;AAAA,EAC9C,EAAE;AAEJ,SAAO;AAAA,IACL,QAAQ,OAAO;AAAA,IACf;AAAA,IACA,gBAAgB,EAAE,GAAG,eAAe;AAAA,EACtC;AACF;","names":[]}
@@ -0,0 +1,24 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { b as AnalyticsManifest, c as ConsentBannerOptions } from './consent-banner-WVGsxcY2.js';
3
+
4
+ interface RootTaleAnalyticsProps {
5
+ /** buildManifest() 결과. plane ① 태그 + fail-closed 동의 기본값. */
6
+ manifest: AnalyticsManifest;
7
+ /** plane ② 비콘 수집 엔드포인트. 없으면 비콘 미주입. */
8
+ collectUrl?: string;
9
+ /** 비콘용 site 식별자. collectUrl 과 함께 제공. */
10
+ siteId?: string;
11
+ /** 동의 배너 개인정보처리방침 링크. */
12
+ privacyUrl?: string;
13
+ /** 비콘 pageview 자동 전송 (기본 true). */
14
+ trackPageview?: boolean;
15
+ /** 동의 배너 카피 override. */
16
+ bannerOptions?: ConsentBannerOptions;
17
+ }
18
+ /**
19
+ * 단일 드롭인 컴포넌트. RSC 호환(hook/상태 없음) — Server Component layout 에
20
+ * 바로 배치 가능.
21
+ */
22
+ declare function RootTaleAnalytics({ manifest, collectUrl, siteId, privacyUrl, trackPageview, bannerOptions, }: RootTaleAnalyticsProps): react_jsx_runtime.JSX.Element;
23
+
24
+ export { RootTaleAnalytics, type RootTaleAnalyticsProps };
package/dist/react.js ADDED
@@ -0,0 +1,57 @@
1
+ import {
2
+ renderBeaconScript,
3
+ renderConsentBannerMarkup,
4
+ renderConsentBannerScript,
5
+ renderConsentBootstrap,
6
+ renderLoaderScript
7
+ } from "./chunk-6SFRLOFD.js";
8
+
9
+ // src/react.tsx
10
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
11
+ function RootTaleAnalytics({
12
+ manifest,
13
+ collectUrl,
14
+ siteId,
15
+ privacyUrl,
16
+ trackPageview,
17
+ bannerOptions
18
+ }) {
19
+ const beacon = collectUrl && siteId ? renderBeaconScript({ collectUrl, siteId, trackPageview }) : null;
20
+ const bannerOpts = {
21
+ privacyUrl,
22
+ ...bannerOptions
23
+ };
24
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
25
+ /* @__PURE__ */ jsx(
26
+ "script",
27
+ {
28
+ dangerouslySetInnerHTML: { __html: renderConsentBootstrap() }
29
+ }
30
+ ),
31
+ /* @__PURE__ */ jsx(
32
+ "script",
33
+ {
34
+ dangerouslySetInnerHTML: { __html: renderLoaderScript(manifest) }
35
+ }
36
+ ),
37
+ /* @__PURE__ */ jsx(
38
+ "div",
39
+ {
40
+ dangerouslySetInnerHTML: {
41
+ __html: renderConsentBannerMarkup(bannerOpts)
42
+ }
43
+ }
44
+ ),
45
+ /* @__PURE__ */ jsx(
46
+ "script",
47
+ {
48
+ dangerouslySetInnerHTML: { __html: renderConsentBannerScript() }
49
+ }
50
+ ),
51
+ beacon ? /* @__PURE__ */ jsx("script", { dangerouslySetInnerHTML: { __html: beacon } }) : null
52
+ ] });
53
+ }
54
+ export {
55
+ RootTaleAnalytics
56
+ };
57
+ //# sourceMappingURL=react.js.map
@@ -0,0 +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 <div\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":";;;;;;;;;AA+DI,mBACE,KADF;AAlBG,SAAS,kBAAkB;AAAA,EAChC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,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,IACA;AAAA,MAAC;AAAA;AAAA,QACC,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,KACN;AAEJ;","names":[]}
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@roottale/analytics-runtime",
3
+ "version": "0.1.0",
4
+ "type": "module",
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
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js",
12
+ "default": "./dist/index.js"
13
+ },
14
+ "./react": {
15
+ "types": "./dist/react.d.ts",
16
+ "import": "./dist/react.js",
17
+ "default": "./dist/react.js"
18
+ },
19
+ "./package.json": "./package.json"
20
+ },
21
+ "files": ["dist/", "README.md"],
22
+ "scripts": {
23
+ "build": "tsup",
24
+ "type-check": "tsc --noEmit",
25
+ "test": "vitest run --passWithNoTests"
26
+ },
27
+ "peerDependencies": {
28
+ "react": "^19.0.0"
29
+ },
30
+ "peerDependenciesMeta": {
31
+ "react": {
32
+ "optional": true
33
+ }
34
+ },
35
+ "devDependencies": {
36
+ "@types/react": "^19.0.0",
37
+ "react": "^19.0.0",
38
+ "tsup": "^8.0.0",
39
+ "typescript": "^5.7.0",
40
+ "vitest": "^2.1.0"
41
+ },
42
+ "publishConfig": {
43
+ "access": "public"
44
+ },
45
+ "license": "UNLICENSED",
46
+ "keywords": ["roottale", "analytics", "consent", "consent-mode", "beacon", "ga4", "gtm"],
47
+ "homepage": "https://github.com/RootTale/roottale-platform/tree/main/packages/analytics-runtime",
48
+ "repository": {
49
+ "type": "git",
50
+ "url": "https://github.com/RootTale/roottale-platform.git",
51
+ "directory": "packages/analytics-runtime"
52
+ }
53
+ }