@roottale/analytics-runtime 0.5.0 → 0.6.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.
@@ -0,0 +1,99 @@
1
+ // src/entry-channels.ts
2
+ var ENTRY_CHANNEL_IDS = [
3
+ "search",
4
+ "direct",
5
+ "referral",
6
+ "social",
7
+ "paid",
8
+ "ai",
9
+ "shortlink",
10
+ "other"
11
+ ];
12
+ function isEntryChannelId(value) {
13
+ return ENTRY_CHANNEL_IDS.includes(value);
14
+ }
15
+ var ENTRY_CHANNEL_LABEL = {
16
+ search: "\uAC80\uC0C9",
17
+ direct: "\uC8FC\uC18C \uC785\uB825\xB7\uC990\uACA8\uCC3E\uAE30",
18
+ referral: "\uB2E4\uB978 \uC0AC\uC774\uD2B8 \uB9C1\uD06C",
19
+ social: "\uC18C\uC15C \uBBF8\uB514\uC5B4",
20
+ paid: "\uAD11\uACE0",
21
+ ai: "AI \uCC57\uBD07",
22
+ shortlink: "\uB2E8\uCD95\uB9C1\uD06C\xB7QR",
23
+ other: "\uAE30\uD0C0 \uCEA0\uD398\uC778"
24
+ };
25
+ var SEARCH_HOSTS = [
26
+ { host: "google.com" },
27
+ { host: "google.co.kr" },
28
+ { host: "search.naver.com" },
29
+ { host: "naver.com", exact: true },
30
+ { host: "m.naver.com", exact: true },
31
+ { host: "search.daum.net" },
32
+ { host: "daum.net", exact: true },
33
+ { host: "m.daum.net", exact: true },
34
+ { host: "bing.com" },
35
+ { host: "duckduckgo.com" },
36
+ { host: "search.yahoo.com" },
37
+ { host: "yandex.com" },
38
+ { host: "baidu.com" },
39
+ { host: "search.zum.com" },
40
+ { host: "zum.com", exact: true },
41
+ { host: "search.nate.com" },
42
+ { host: "nate.com", exact: true }
43
+ ];
44
+ var SOCIAL_HOSTS = [
45
+ { host: "facebook.com" },
46
+ { host: "instagram.com" },
47
+ { host: "threads.net" },
48
+ { host: "twitter.com" },
49
+ { host: "x.com" },
50
+ { host: "t.co", exact: true },
51
+ { host: "linkedin.com" },
52
+ { host: "youtube.com" },
53
+ { host: "youtu.be", exact: true },
54
+ { host: "tiktok.com" },
55
+ { host: "pinterest.com" },
56
+ { host: "band.us" },
57
+ { host: "story.kakao.com" },
58
+ { host: "pf.kakao.com" },
59
+ { host: "cafe.naver.com" },
60
+ { host: "cafe.daum.net" }
61
+ ];
62
+ function matchesRules(host, rules) {
63
+ for (const rule of rules) {
64
+ if (host === rule.host) return true;
65
+ if (!rule.exact && host.endsWith(`.${rule.host}`)) return true;
66
+ }
67
+ return false;
68
+ }
69
+ var PAID_MEDIUM_RE = /^(cpc|ppc|cpm|cpv|cpa|display|banner|paid[\w-]*|retargeting|remarketing)$/i;
70
+ var SOCIAL_MEDIUM_RE = /^(social|sns|social[-_]media)$/i;
71
+ function classifyEntryChannel(signals) {
72
+ const host = signals.referrerHost?.trim().toLowerCase() || null;
73
+ const medium = signals.utmMedium?.trim() || null;
74
+ const hasUtm = Boolean(
75
+ signals.utmSource?.trim() || medium || signals.utmCampaign?.trim()
76
+ );
77
+ if (signals.aiReferralId) return "ai";
78
+ if (signals.rtSrc?.trim()) return "shortlink";
79
+ if (signals.hasAdClick || medium && PAID_MEDIUM_RE.test(medium)) {
80
+ return "paid";
81
+ }
82
+ if (medium && SOCIAL_MEDIUM_RE.test(medium)) return "social";
83
+ if (host) {
84
+ if (matchesRules(host, SEARCH_HOSTS)) return "search";
85
+ if (matchesRules(host, SOCIAL_HOSTS)) return "social";
86
+ }
87
+ if (hasUtm) return "other";
88
+ if (host) return "referral";
89
+ if (signals.hasReferrer) return null;
90
+ return "direct";
91
+ }
92
+
93
+ export {
94
+ ENTRY_CHANNEL_IDS,
95
+ isEntryChannelId,
96
+ ENTRY_CHANNEL_LABEL,
97
+ classifyEntryChannel
98
+ };
99
+ //# sourceMappingURL=chunk-KIOFZXJX.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/entry-channels.ts"],"sourcesContent":["/**\n * 유입 경로(entry channel) 분류 — ROOT-ANALYTICS 자체 수집의 \"어디서 왔나\".\n *\n * 비콘은 신호(외부 referrer **호스트명**·utm·광고클릭 유무·rt_src·AI 라벨)만\n * 보내고, 판정은 서버(`api-core /v1/collect`)가 이 모듈로 한다 — 판정 목록을\n * 고치면 워커 배포만으로 전 사이트에 반영된다(고객 사이트 재배포 불필요).\n *\n * Privacy(ADR-0038 §2 동의 면제 유지): 입력이 이미 비식별이다 — referrer 는\n * 경로·쿼리 없는 호스트명뿐, gclid/fbclid 는 존재 여부 불리언뿐(원값 미수신),\n * utm 은 고객이 자기 캠페인 링크에 직접 붙인 라벨이다. 출력은 고정 사전의\n * id 하나라 임의 문자열이 차원에 들어갈 수 없다.\n *\n * GA4 화면의 6묶음(search/direct/referral/social/paid/other)과 말을 맞추되,\n * 자체 수집만 아는 두 가지를 더 가른다 — `ai`(챗봇 유입, ai-referrals.ts 와\n * 동일 사전) · `shortlink`(roottale.link 단축링크/QR 의 rt_src).\n *\n * 이 파일은 브라우저 비콘 생성기·서버 양쪽에서 참조하므로 런타임 의존이\n * 없어야 한다(ai-referrals.ts 와 동일 제약).\n */\n\nexport const ENTRY_CHANNEL_IDS = [\n \"search\",\n \"direct\",\n \"referral\",\n \"social\",\n \"paid\",\n \"ai\",\n \"shortlink\",\n \"other\",\n] as const;\n\nexport type EntryChannelId = (typeof ENTRY_CHANNEL_IDS)[number];\n\n/** 수집 라우트·어드민 조회의 입력 검증 — 사전 밖 문자열 차단. */\nexport function isEntryChannelId(value: string): value is EntryChannelId {\n return (ENTRY_CHANNEL_IDS as readonly string[]).includes(value);\n}\n\n/** 고객 화면 라벨 — GA4 6묶음과 같은 말을 쓴다(두 화면이 다르게 부르지 않게). */\nexport const ENTRY_CHANNEL_LABEL: Record<EntryChannelId, string> = {\n search: \"검색\",\n direct: \"주소 입력·즐겨찾기\",\n referral: \"다른 사이트 링크\",\n social: \"소셜 미디어\",\n paid: \"광고\",\n ai: \"AI 챗봇\",\n shortlink: \"단축링크·QR\",\n other: \"기타 캠페인\",\n};\n\n/**\n * 호스트 규칙 — 기본은 정확 일치 또는 서브도메인(`.` 접미) 일치.\n * `exact` 는 정확 일치만: `naver.com` 을 접미 매칭하면 `blog.naver.com`(블로그\n * = 다른 사이트 링크)까지 검색으로 잘못 묶인다.\n */\ntype HostRule = { readonly host: string; readonly exact?: true };\n\nconst SEARCH_HOSTS: readonly HostRule[] = [\n { host: \"google.com\" },\n { host: \"google.co.kr\" },\n { host: \"search.naver.com\" },\n { host: \"naver.com\", exact: true },\n { host: \"m.naver.com\", exact: true },\n { host: \"search.daum.net\" },\n { host: \"daum.net\", exact: true },\n { host: \"m.daum.net\", exact: true },\n { host: \"bing.com\" },\n { host: \"duckduckgo.com\" },\n { host: \"search.yahoo.com\" },\n { host: \"yandex.com\" },\n { host: \"baidu.com\" },\n { host: \"search.zum.com\" },\n { host: \"zum.com\", exact: true },\n { host: \"search.nate.com\" },\n { host: \"nate.com\", exact: true },\n];\n\nconst SOCIAL_HOSTS: readonly HostRule[] = [\n { host: \"facebook.com\" },\n { host: \"instagram.com\" },\n { host: \"threads.net\" },\n { host: \"twitter.com\" },\n { host: \"x.com\" },\n { host: \"t.co\", exact: true },\n { host: \"linkedin.com\" },\n { host: \"youtube.com\" },\n { host: \"youtu.be\", exact: true },\n { host: \"tiktok.com\" },\n { host: \"pinterest.com\" },\n { host: \"band.us\" },\n { host: \"story.kakao.com\" },\n { host: \"pf.kakao.com\" },\n { host: \"cafe.naver.com\" },\n { host: \"cafe.daum.net\" },\n];\n\n/** 부분 문자열 매칭 금지 — `naver.com.evil.com` 같은 위장 도메인 차단. */\nfunction matchesRules(host: string, rules: readonly HostRule[]): boolean {\n for (const rule of rules) {\n if (host === rule.host) return true;\n if (!rule.exact && host.endsWith(`.${rule.host}`)) return true;\n }\n return false;\n}\n\n/** utm_medium 유료 표기 — GA4 의 paid 판정 표기와 겹치는 관용 값들. */\nconst PAID_MEDIUM_RE =\n /^(cpc|ppc|cpm|cpv|cpa|display|banner|paid[\\w-]*|retargeting|remarketing)$/i;\n\nconst SOCIAL_MEDIUM_RE = /^(social|sns|social[-_]media)$/i;\n\nexport interface EntryChannelSignals {\n /** 외부 referrer 호스트명(소문자, www 제거) — 같은 호스트면 비콘이 생략. */\n readonly referrerHost?: string | null;\n readonly utmSource?: string | null;\n readonly utmMedium?: string | null;\n readonly utmCampaign?: string | null;\n /** gclid/fbclid 존재 여부 — 원값은 어디에도 없다. */\n readonly hasAdClick?: boolean;\n /** roottale.link 단축링크/QR 토큰(rt_src). */\n readonly rtSrc?: string | null;\n /** 비콘이 판정한 AI 챗봇 라벨(ai-referrals 사전 통과분). */\n readonly aiReferralId?: string | null;\n /** referrer 존재 여부(내부 이동 포함) — 비콘 `ref` 플래그. */\n readonly hasReferrer?: boolean;\n}\n\n/**\n * 한 pageview 의 유입 경로 판정. **`null` = 유입이 아님**(내부 이동 — 같은\n * 사이트 안에서 다음 페이지로 넘어온 조회). 집계에서 내부 이동을 빼야\n * \"채널별 유입\"이 실제 들어온 횟수를 뜻한다.\n *\n * 우선순위: ai > shortlink > paid > social(utm) > search(host) >\n * social(host) > other(utm만) > referral(그 외 외부 host) > 내부/직접.\n * utm_medium 명시가 host 추정보다 먼저다 — 고객이 직접 붙인 라벨이 더 정확.\n */\nexport function classifyEntryChannel(\n signals: EntryChannelSignals,\n): EntryChannelId | null {\n const host = signals.referrerHost?.trim().toLowerCase() || null;\n const medium = signals.utmMedium?.trim() || null;\n const hasUtm = Boolean(\n signals.utmSource?.trim() || medium || signals.utmCampaign?.trim(),\n );\n\n if (signals.aiReferralId) return \"ai\";\n if (signals.rtSrc?.trim()) return \"shortlink\";\n if (signals.hasAdClick || (medium && PAID_MEDIUM_RE.test(medium))) {\n return \"paid\";\n }\n if (medium && SOCIAL_MEDIUM_RE.test(medium)) return \"social\";\n if (host) {\n if (matchesRules(host, SEARCH_HOSTS)) return \"search\";\n if (matchesRules(host, SOCIAL_HOSTS)) return \"social\";\n }\n if (hasUtm) return \"other\"; // email·뉴스레터 등 — 라벨은 utm 원문으로 확인\n if (host) return \"referral\";\n // referrer 는 있는데 외부 호스트가 아니다 = 내부 이동 → 유입 아님.\n if (signals.hasReferrer) return null;\n return \"direct\";\n}\n"],"mappings":";AAoBO,IAAM,oBAAoB;AAAA,EAC/B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAKO,SAAS,iBAAiB,OAAwC;AACvE,SAAQ,kBAAwC,SAAS,KAAK;AAChE;AAGO,IAAM,sBAAsD;AAAA,EACjE,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,UAAU;AAAA,EACV,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,IAAI;AAAA,EACJ,WAAW;AAAA,EACX,OAAO;AACT;AASA,IAAM,eAAoC;AAAA,EACxC,EAAE,MAAM,aAAa;AAAA,EACrB,EAAE,MAAM,eAAe;AAAA,EACvB,EAAE,MAAM,mBAAmB;AAAA,EAC3B,EAAE,MAAM,aAAa,OAAO,KAAK;AAAA,EACjC,EAAE,MAAM,eAAe,OAAO,KAAK;AAAA,EACnC,EAAE,MAAM,kBAAkB;AAAA,EAC1B,EAAE,MAAM,YAAY,OAAO,KAAK;AAAA,EAChC,EAAE,MAAM,cAAc,OAAO,KAAK;AAAA,EAClC,EAAE,MAAM,WAAW;AAAA,EACnB,EAAE,MAAM,iBAAiB;AAAA,EACzB,EAAE,MAAM,mBAAmB;AAAA,EAC3B,EAAE,MAAM,aAAa;AAAA,EACrB,EAAE,MAAM,YAAY;AAAA,EACpB,EAAE,MAAM,iBAAiB;AAAA,EACzB,EAAE,MAAM,WAAW,OAAO,KAAK;AAAA,EAC/B,EAAE,MAAM,kBAAkB;AAAA,EAC1B,EAAE,MAAM,YAAY,OAAO,KAAK;AAClC;AAEA,IAAM,eAAoC;AAAA,EACxC,EAAE,MAAM,eAAe;AAAA,EACvB,EAAE,MAAM,gBAAgB;AAAA,EACxB,EAAE,MAAM,cAAc;AAAA,EACtB,EAAE,MAAM,cAAc;AAAA,EACtB,EAAE,MAAM,QAAQ;AAAA,EAChB,EAAE,MAAM,QAAQ,OAAO,KAAK;AAAA,EAC5B,EAAE,MAAM,eAAe;AAAA,EACvB,EAAE,MAAM,cAAc;AAAA,EACtB,EAAE,MAAM,YAAY,OAAO,KAAK;AAAA,EAChC,EAAE,MAAM,aAAa;AAAA,EACrB,EAAE,MAAM,gBAAgB;AAAA,EACxB,EAAE,MAAM,UAAU;AAAA,EAClB,EAAE,MAAM,kBAAkB;AAAA,EAC1B,EAAE,MAAM,eAAe;AAAA,EACvB,EAAE,MAAM,iBAAiB;AAAA,EACzB,EAAE,MAAM,gBAAgB;AAC1B;AAGA,SAAS,aAAa,MAAc,OAAqC;AACvE,aAAW,QAAQ,OAAO;AACxB,QAAI,SAAS,KAAK,KAAM,QAAO;AAC/B,QAAI,CAAC,KAAK,SAAS,KAAK,SAAS,IAAI,KAAK,IAAI,EAAE,EAAG,QAAO;AAAA,EAC5D;AACA,SAAO;AACT;AAGA,IAAM,iBACJ;AAEF,IAAM,mBAAmB;AA2BlB,SAAS,qBACd,SACuB;AACvB,QAAM,OAAO,QAAQ,cAAc,KAAK,EAAE,YAAY,KAAK;AAC3D,QAAM,SAAS,QAAQ,WAAW,KAAK,KAAK;AAC5C,QAAM,SAAS;AAAA,IACb,QAAQ,WAAW,KAAK,KAAK,UAAU,QAAQ,aAAa,KAAK;AAAA,EACnE;AAEA,MAAI,QAAQ,aAAc,QAAO;AACjC,MAAI,QAAQ,OAAO,KAAK,EAAG,QAAO;AAClC,MAAI,QAAQ,cAAe,UAAU,eAAe,KAAK,MAAM,GAAI;AACjE,WAAO;AAAA,EACT;AACA,MAAI,UAAU,iBAAiB,KAAK,MAAM,EAAG,QAAO;AACpD,MAAI,MAAM;AACR,QAAI,aAAa,MAAM,YAAY,EAAG,QAAO;AAC7C,QAAI,aAAa,MAAM,YAAY,EAAG,QAAO;AAAA,EAC/C;AACA,MAAI,OAAQ,QAAO;AACnB,MAAI,KAAM,QAAO;AAEjB,MAAI,QAAQ,YAAa,QAAO;AAChC,SAAO;AACT;","names":[]}
@@ -0,0 +1,52 @@
1
+ /**
2
+ * 유입 경로(entry channel) 분류 — ROOT-ANALYTICS 자체 수집의 "어디서 왔나".
3
+ *
4
+ * 비콘은 신호(외부 referrer **호스트명**·utm·광고클릭 유무·rt_src·AI 라벨)만
5
+ * 보내고, 판정은 서버(`api-core /v1/collect`)가 이 모듈로 한다 — 판정 목록을
6
+ * 고치면 워커 배포만으로 전 사이트에 반영된다(고객 사이트 재배포 불필요).
7
+ *
8
+ * Privacy(ADR-0038 §2 동의 면제 유지): 입력이 이미 비식별이다 — referrer 는
9
+ * 경로·쿼리 없는 호스트명뿐, gclid/fbclid 는 존재 여부 불리언뿐(원값 미수신),
10
+ * utm 은 고객이 자기 캠페인 링크에 직접 붙인 라벨이다. 출력은 고정 사전의
11
+ * id 하나라 임의 문자열이 차원에 들어갈 수 없다.
12
+ *
13
+ * GA4 화면의 6묶음(search/direct/referral/social/paid/other)과 말을 맞추되,
14
+ * 자체 수집만 아는 두 가지를 더 가른다 — `ai`(챗봇 유입, ai-referrals.ts 와
15
+ * 동일 사전) · `shortlink`(roottale.link 단축링크/QR 의 rt_src).
16
+ *
17
+ * 이 파일은 브라우저 비콘 생성기·서버 양쪽에서 참조하므로 런타임 의존이
18
+ * 없어야 한다(ai-referrals.ts 와 동일 제약).
19
+ */
20
+ declare const ENTRY_CHANNEL_IDS: readonly ["search", "direct", "referral", "social", "paid", "ai", "shortlink", "other"];
21
+ type EntryChannelId = (typeof ENTRY_CHANNEL_IDS)[number];
22
+ /** 수집 라우트·어드민 조회의 입력 검증 — 사전 밖 문자열 차단. */
23
+ declare function isEntryChannelId(value: string): value is EntryChannelId;
24
+ /** 고객 화면 라벨 — GA4 6묶음과 같은 말을 쓴다(두 화면이 다르게 부르지 않게). */
25
+ declare const ENTRY_CHANNEL_LABEL: Record<EntryChannelId, string>;
26
+ interface EntryChannelSignals {
27
+ /** 외부 referrer 호스트명(소문자, www 제거) — 같은 호스트면 비콘이 생략. */
28
+ readonly referrerHost?: string | null;
29
+ readonly utmSource?: string | null;
30
+ readonly utmMedium?: string | null;
31
+ readonly utmCampaign?: string | null;
32
+ /** gclid/fbclid 존재 여부 — 원값은 어디에도 없다. */
33
+ readonly hasAdClick?: boolean;
34
+ /** roottale.link 단축링크/QR 토큰(rt_src). */
35
+ readonly rtSrc?: string | null;
36
+ /** 비콘이 판정한 AI 챗봇 라벨(ai-referrals 사전 통과분). */
37
+ readonly aiReferralId?: string | null;
38
+ /** referrer 존재 여부(내부 이동 포함) — 비콘 `ref` 플래그. */
39
+ readonly hasReferrer?: boolean;
40
+ }
41
+ /**
42
+ * 한 pageview 의 유입 경로 판정. **`null` = 유입이 아님**(내부 이동 — 같은
43
+ * 사이트 안에서 다음 페이지로 넘어온 조회). 집계에서 내부 이동을 빼야
44
+ * "채널별 유입"이 실제 들어온 횟수를 뜻한다.
45
+ *
46
+ * 우선순위: ai > shortlink > paid > social(utm) > search(host) >
47
+ * social(host) > other(utm만) > referral(그 외 외부 host) > 내부/직접.
48
+ * utm_medium 명시가 host 추정보다 먼저다 — 고객이 직접 붙인 라벨이 더 정확.
49
+ */
50
+ declare function classifyEntryChannel(signals: EntryChannelSignals): EntryChannelId | null;
51
+
52
+ export { ENTRY_CHANNEL_IDS, ENTRY_CHANNEL_LABEL, type EntryChannelId, type EntryChannelSignals, classifyEntryChannel, isEntryChannelId };
@@ -0,0 +1,13 @@
1
+ import {
2
+ ENTRY_CHANNEL_IDS,
3
+ ENTRY_CHANNEL_LABEL,
4
+ classifyEntryChannel,
5
+ isEntryChannelId
6
+ } from "./chunk-KIOFZXJX.js";
7
+ export {
8
+ ENTRY_CHANNEL_IDS,
9
+ ENTRY_CHANNEL_LABEL,
10
+ classifyEntryChannel,
11
+ isEntryChannelId
12
+ };
13
+ //# sourceMappingURL=entry-channels.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
package/dist/index.d.ts CHANGED
@@ -1,11 +1,20 @@
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';
1
+ import { C as ConsentCategory, a as ConsentState, T as TagProvider, A as AnalyticsSiteConfig, b as AnalyticsManifest } from './types-VA3xp0bt.js';
2
+ export { c as ConsentValue, R as ResolvedTag, d as TagConfig } from './types-VA3xp0bt.js';
3
3
  export { AI_REFERRAL_IDS, AI_REFERRAL_LABEL, AiReferralId, AiReferralMetric, aiReferralRulesLiteral, classifyAiReferral, isAiReferralId, summarizeAiReferrals } from './ai-referrals.js';
4
+ export { ENTRY_CHANNEL_IDS, ENTRY_CHANNEL_LABEL, EntryChannelId, EntryChannelSignals, classifyEntryChannel, isEntryChannelId } from './entry-channels.js';
4
5
  export { GA4_MIRROR, TRACK_EVENTS, TrackEventDef, TrackEventName, isTrackEvent } from './events.js';
5
6
 
6
7
  declare const ALL_CONSENT_CATEGORIES: readonly ConsentCategory[];
7
- /** fail-closed 기본값 — 전부 denied. */
8
+ /**
9
+ * 방문자의 명시적 동의 선택 저장 키(localStorage) — 과거 배너가 저장한 값을
10
+ * 로더가 계속 읽는다. **과거에 "거부"를 누른 방문자의 선택은 기본값 변경
11
+ * 후에도 존중된다**(기본값은 선택 기록이 없는 방문자에게만 적용).
12
+ */
13
+ declare const CONSENT_STORAGE_KEY = "rt_consent_v1";
14
+ /** 전부 denied — 광고 도구 게이트·테스트 기준값으로 유지. */
8
15
  declare const DENIED_CONSENT: ConsentState;
16
+ /** 기본 동의(Amend 4) — 통계 granted · 광고 denied. */
17
+ declare const DEFAULT_CONSENT: ConsentState;
9
18
  /**
10
19
  * provider 가 발화하기 전 granted 여야 하는 동의 카테고리.
11
20
  * - analytics 도구(GA4·Clarity·Naver) → `analytics_storage`
@@ -21,22 +30,35 @@ declare function isTagAllowed(required: readonly ConsentCategory[], consent: Con
21
30
  * - `enabled=false` 또는 빈 id 는 제외.
22
31
  * - provider 당 1개 (중복은 첫 항목만).
23
32
  * - 각 태그에 필요한 동의 카테고리(`PROVIDER_CONSENT`)를 부착.
24
- * - 동의 기본값은 항상 전부 denied (fail-closed).
33
+ * - 동의 기본값 = 통계 granted · 광고 denied (ADR-0038 Amend 4 — 광고 도구는
34
+ * 동의 UI 없이는 발화하지 않는다).
25
35
  */
26
36
  declare function buildManifest(config: AnalyticsSiteConfig): AnalyticsManifest;
27
37
 
28
38
  /**
29
- * Consent Mode v2 default 부트스트랩. 전 카테고리 denied + `wait_for_update`.
30
- * 반드시 어떤 분석/광고 태그보다 먼저 실행돼야 한다.
39
+ * Consent Mode v2 default 부트스트랩 — 통계 granted · 광고 denied (Amend 4).
40
+ * 반드시 어떤 분석/광고 태그보다 먼저 실행돼야 한다. `wait_for_update` 는
41
+ * 로더가 저장된 과거 선택을 적용할 짧은 창.
31
42
  */
32
43
  declare function renderConsentBootstrap(): string;
33
44
  /**
34
- * manifest 를 embed 한 로더 스크립트. fail-closed — 기본 동의가 denied 라
35
- * `applyAll()` 은 아무 태그도 주입하지 않고, 동의 배너의 `updateConsent` 후에만
36
- * 허용된 태그가 붙는다.
45
+ * manifest 를 embed 한 로더 스크립트. 초기 `applyAll()` 이 동의 충족 태그
46
+ * (기본값에선 분석 도구)를 즉시 주입한다. 광고 도구는 `requiredConsent` 의
47
+ * ad_* 카테고리가 기본 denied 라 `updateConsent` 없이는 절대 붙지 않는다.
37
48
  */
38
49
  declare function renderLoaderScript(manifest: AnalyticsManifest): string;
39
50
 
51
+ interface ConsentBannerOptions {
52
+ /** 개인정보처리방침 링크 (테넌트별). 없으면 링크 생략. */
53
+ privacyUrl?: string;
54
+ /** 본문 카피. 기본 한국어. */
55
+ message?: string;
56
+ }
57
+ /** 배너 마크업(HTML). 스크립트 미포함 — 컨테이너로 주입. */
58
+ declare function renderConsentBannerMarkup(options?: ConsentBannerOptions): string;
59
+ /** 배너 동작(JS 본문). <script is:inline> 로 감싸 주입. */
60
+ declare function renderConsentBannerScript(): string;
61
+
40
62
  interface BeaconOptions {
41
63
  /** first-party 수집 엔드포인트 (예: https://api.roottale.com/v1/collect). */
42
64
  collectUrl: string;
@@ -113,6 +135,15 @@ interface LastTouchAttribution {
113
135
  * 비콘 dispatcher JS 본문. <script is:inline> 로 감싸 주입.
114
136
  * `ref` 는 referrer 존재 여부(0/1)만 — raw referrer 미전송(privacy).
115
137
  *
138
+ * 유입 경로 신호(2026-08-19) — pageview 에만 동봉, 전부 있을 때만 실린다:
139
+ * `rh` 외부 referrer **호스트명**(경로·쿼리 없음, www 제거) · `us`/`um`/`uc`
140
+ * utm source/medium/campaign · `rs` rt_src · `gc:1` gclid/fbclid **존재
141
+ * 여부**(원값 미전송) · `lg` 브라우저 언어 1차 서브태그(ko·en···, full
142
+ * locale 미전송). 판정은 서버가 한다 —
143
+ * `entry-channels.ts` classifyEntryChannel() 이 `/v1/collect` 에서 채널
144
+ * 묶음으로 접어 AE blob10~ 에 적재(값 계약은 collect.ts 와 공유, 동시 갱신).
145
+ * raw referrer URL 은 여전히 어디에도 전송·저장되지 않는다(ADR-0026 §5).
146
+ *
116
147
  * ADR-0065 — 글 조회수 식별자: pageview 시 `<meta name="rt:content-id">` 의
117
148
  * content 를 읽어 `pid`(=post_id) 로 동봉한다. 사이트 레벨 단일 비콘이 페이지별
118
149
  * 글 식별자를 갖게 해 URL/컬렉션 basePath 변경에 무관한 안정 매칭을 만든다.
@@ -201,4 +232,4 @@ declare global {
201
232
  }
202
233
  }
203
234
 
204
- export { ALL_CONSENT_CATEGORIES, ATTRIBUTION_STORAGE_KEY, ATTRIBUTION_TTL_MS, AnalyticsManifest, AnalyticsSiteConfig, type BeaconOptions, ConsentCategory, ConsentState, DENIED_CONSENT, type FirstTouchAttribution, JOURNEY_MAX_ITEMS, JOURNEY_STORAGE_KEY, type JourneyEntry, type LastTouchAttribution, type LastTouchChannel, PROVIDER_CONSENT, TagProvider, buildManifest, initObservers, isTagAllowed, readJourney, renderBeaconScript, renderConsentBootstrap, renderGa4BridgeScript, renderJourneyBridgeScript, renderLoaderScript, teardownObservers, trackCustomEvent };
235
+ export { ALL_CONSENT_CATEGORIES, ATTRIBUTION_STORAGE_KEY, ATTRIBUTION_TTL_MS, AnalyticsManifest, AnalyticsSiteConfig, type BeaconOptions, CONSENT_STORAGE_KEY, type ConsentBannerOptions, ConsentCategory, ConsentState, DEFAULT_CONSENT, DENIED_CONSENT, type FirstTouchAttribution, JOURNEY_MAX_ITEMS, JOURNEY_STORAGE_KEY, type JourneyEntry, type LastTouchAttribution, type LastTouchChannel, PROVIDER_CONSENT, TagProvider, buildManifest, initObservers, isTagAllowed, readJourney, renderBeaconScript, renderConsentBannerMarkup, renderConsentBannerScript, renderConsentBootstrap, renderGa4BridgeScript, renderJourneyBridgeScript, renderLoaderScript, teardownObservers, trackCustomEvent };
package/dist/index.js CHANGED
@@ -1,24 +1,28 @@
1
1
  import {
2
+ ALL_CONSENT_CATEGORIES,
3
+ CONSENT_STORAGE_KEY,
4
+ DEFAULT_CONSENT,
5
+ DENIED_CONSENT,
2
6
  JOURNEY_MAX_ITEMS,
3
7
  JOURNEY_STORAGE_KEY,
8
+ PROVIDER_CONSENT,
9
+ isTagAllowed,
4
10
  readJourney,
5
- renderConsentBannerMarkup,
6
- renderConsentBannerScript,
7
11
  renderConsentBootstrap,
8
12
  renderGa4BridgeScript,
9
13
  renderJourneyBridgeScript,
10
14
  renderLoaderScript
11
- } from "./chunk-G7OLBHT5.js";
15
+ } from "./chunk-EHXYDR7C.js";
12
16
  import {
13
17
  initObservers,
14
18
  teardownObservers
15
- } from "./chunk-ABGGPWIX.js";
19
+ } from "./chunk-GV4CWEWY.js";
16
20
  import {
17
21
  ATTRIBUTION_STORAGE_KEY,
18
22
  ATTRIBUTION_TTL_MS,
19
23
  renderBeaconScript,
20
24
  trackCustomEvent
21
- } from "./chunk-ANB42OLW.js";
25
+ } from "./chunk-K3NVHH7K.js";
22
26
  import {
23
27
  GA4_MIRROR,
24
28
  TRACK_EVENTS,
@@ -31,30 +35,13 @@ import {
31
35
  classifyAiReferral,
32
36
  isAiReferralId,
33
37
  summarizeAiReferrals
34
- } from "./chunk-2WQ7PVRU.js";
35
-
36
- // src/consent.ts
37
- var ALL_CONSENT_CATEGORIES = [
38
- "analytics_storage",
39
- "ad_storage",
40
- "ad_user_data",
41
- "ad_personalization"
42
- ];
43
- var DENIED_CONSENT = {
44
- analytics_storage: "denied",
45
- ad_storage: "denied",
46
- ad_user_data: "denied",
47
- ad_personalization: "denied"
48
- };
49
- var PROVIDER_CONSENT = {
50
- ga4: ["analytics_storage"],
51
- clarity: ["analytics_storage"],
52
- naver: ["analytics_storage"],
53
- meta_pixel: ["ad_storage", "ad_user_data", "ad_personalization"]
54
- };
55
- function isTagAllowed(required, consent) {
56
- return required.every((c) => consent[c] === "granted");
57
- }
38
+ } from "./chunk-6NWJK7V7.js";
39
+ import {
40
+ ENTRY_CHANNEL_IDS,
41
+ ENTRY_CHANNEL_LABEL,
42
+ classifyEntryChannel,
43
+ isEntryChannelId
44
+ } from "./chunk-KIOFZXJX.js";
58
45
 
59
46
  // src/manifest.ts
60
47
  function buildManifest(config) {
@@ -73,16 +60,73 @@ function buildManifest(config) {
73
60
  return {
74
61
  siteId: config.siteId,
75
62
  tags,
76
- consentDefault: { ...DENIED_CONSENT }
63
+ consentDefault: { ...DEFAULT_CONSENT }
77
64
  };
78
65
  }
66
+
67
+ // src/consent-banner.ts
68
+ var BANNER_ID = "rt-consent";
69
+ var STORAGE_KEY = CONSENT_STORAGE_KEY;
70
+ function renderConsentBannerMarkup(options = {}) {
71
+ const message = options.message ?? "\uC774 \uC0AC\uC774\uD2B8\uB294 ROOT-ANALYTICS\uC640 \uC678\uBD80 \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.";
72
+ const privacy = options.privacyUrl ? ` <a href="${escapeAttr(options.privacyUrl)}" target="_blank" rel="noopener">\uAC1C\uC778\uC815\uBCF4\uCC98\uB9AC\uBC29\uCE68</a>` : "";
73
+ return [
74
+ `<div id="${BANNER_ID}" role="dialog" aria-live="polite" aria-label="\uB3D9\uC758 \uC548\uB0B4" hidden`,
75
+ ` style="position:fixed;left:0;right:0;bottom:0;z-index:2147483647;`,
76
+ `background:#fff;border-top:1px solid #e5e7eb;padding:16px;`,
77
+ `display:flex;flex-wrap:wrap;gap:8px;align-items:center;justify-content:center;`,
78
+ `font-size:14px;line-height:1.5;color:#111;box-shadow:0 -2px 12px rgba(0,0,0,.06)">`,
79
+ `<span style="flex:1 1 280px;min-width:240px">${escapeHtml(message)}${privacy}</span>`,
80
+ `<button id="rt-consent-all" type="button" style="${BTN_PRIMARY}">\uBAA8\uB450 \uD5C8\uC6A9</button>`,
81
+ `<button id="rt-consent-analytics" type="button" style="${BTN_SECONDARY}">\uBD84\uC11D\uB9CC \uD5C8\uC6A9</button>`,
82
+ `<button id="rt-consent-deny" type="button" style="${BTN_GHOST}">\uAC70\uBD80</button>`,
83
+ `</div>`
84
+ ].join("");
85
+ }
86
+ function renderConsentBannerScript() {
87
+ return `(function(){
88
+ var KEY=${JSON.stringify(STORAGE_KEY)};
89
+ var el=document.getElementById(${JSON.stringify(BANNER_ID)});
90
+ function apply(state){
91
+ if(window.RootTaleAnalytics&&typeof window.RootTaleAnalytics.updateConsent==='function'){
92
+ window.RootTaleAnalytics.updateConsent(state);
93
+ }}
94
+ function persist(state){try{localStorage.setItem(KEY,JSON.stringify(state));}catch(e){}}
95
+ function read(){try{return JSON.parse(localStorage.getItem(KEY)||'null');}catch(e){return null;}}
96
+ var ALL={analytics_storage:'granted',ad_storage:'granted',ad_user_data:'granted',ad_personalization:'granted'};
97
+ var ANALYTICS={analytics_storage:'granted',ad_storage:'denied',ad_user_data:'denied',ad_personalization:'denied'};
98
+ var DENY={analytics_storage:'denied',ad_storage:'denied',ad_user_data:'denied',ad_personalization:'denied'};
99
+ function choose(state){apply(state);persist(state);if(el){el.hidden=true;}}
100
+ var prior=read();
101
+ if(prior){apply(prior);if(el){el.hidden=true;}}
102
+ else if(el){el.hidden=false;}
103
+ function bind(id,state){var b=document.getElementById(id);if(b){b.addEventListener('click',function(){choose(state);});}}
104
+ bind('rt-consent-all',ALL);
105
+ bind('rt-consent-analytics',ANALYTICS);
106
+ bind('rt-consent-deny',DENY);
107
+ })();`;
108
+ }
109
+ var BTN_BASE = "padding:8px 14px;border-radius:8px;font-size:14px;cursor:pointer;border:1px solid #d1d5db;";
110
+ var BTN_PRIMARY = BTN_BASE + "background:#111;color:#fff;border-color:#111;";
111
+ var BTN_SECONDARY = BTN_BASE + "background:#f3f4f6;color:#111;";
112
+ var BTN_GHOST = BTN_BASE + "background:transparent;color:#6b7280;";
113
+ function escapeHtml(s) {
114
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
115
+ }
116
+ function escapeAttr(s) {
117
+ return escapeHtml(s).replace(/"/g, "&quot;");
118
+ }
79
119
  export {
80
120
  AI_REFERRAL_IDS,
81
121
  AI_REFERRAL_LABEL,
82
122
  ALL_CONSENT_CATEGORIES,
83
123
  ATTRIBUTION_STORAGE_KEY,
84
124
  ATTRIBUTION_TTL_MS,
125
+ CONSENT_STORAGE_KEY,
126
+ DEFAULT_CONSENT,
85
127
  DENIED_CONSENT,
128
+ ENTRY_CHANNEL_IDS,
129
+ ENTRY_CHANNEL_LABEL,
86
130
  GA4_MIRROR,
87
131
  JOURNEY_MAX_ITEMS,
88
132
  JOURNEY_STORAGE_KEY,
@@ -91,8 +135,10 @@ export {
91
135
  aiReferralRulesLiteral,
92
136
  buildManifest,
93
137
  classifyAiReferral,
138
+ classifyEntryChannel,
94
139
  initObservers,
95
140
  isAiReferralId,
141
+ isEntryChannelId,
96
142
  isTagAllowed,
97
143
  isTrackEvent,
98
144
  readJourney,
package/dist/index.js.map CHANGED
@@ -1 +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":[]}
1
+ {"version":3,"sources":["../src/manifest.ts","../src/consent-banner.ts"],"sourcesContent":["// ADR-0038 §1 — site 태그 구성 → manifest (순수 함수).\n\nimport { DEFAULT_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 * - 동의 기본값 = 통계 granted · 광고 denied (ADR-0038 Amend 4 — 광고 도구는\n * 동의 UI 없이는 발화하지 않는다).\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: { ...DEFAULT_CONSENT },\n };\n}\n","// ADR-0038 §2 — 동의 배너. **Amend 4(2026-08-19)부터 기본 미렌더** —\n// 통계는 기본 허용이라 배너가 필요 없고, 광고 도구는 기본 denied 로 잠겨\n// 있다. 광고 연동을 도입하는 날 이 모듈을 다시 렌더러에 물린다(그때까지\n// 광고 태그는 발화 불가). 과거 배너가 저장한 선택은 loader.ts 가 계속 읽어\n// 존중한다(CONSENT_STORAGE_KEY 공유).\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\nimport { CONSENT_STORAGE_KEY } from \"./consent\";\n\nexport interface ConsentBannerOptions {\n /** 개인정보처리방침 링크 (테넌트별). 없으면 링크 생략. */\n privacyUrl?: string;\n /** 본문 카피. 기본 한국어. */\n message?: string;\n}\n\nconst BANNER_ID = \"rt-consent\";\nconst STORAGE_KEY = CONSENT_STORAGE_KEY;\n\n/** 배너 마크업(HTML). 스크립트 미포함 — 컨테이너로 주입. */\nexport function renderConsentBannerMarkup(\n options: ConsentBannerOptions = {},\n): string {\n const message =\n options.message ??\n \"이 사이트는 ROOT-ANALYTICS와 외부 분석·광고 도구를 사용할 수 있습니다. 동의 여부를 선택해 주세요.\";\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"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAaO,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,gBAAgB;AAAA,EACvC;AACF;;;ACbA,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;","names":[]}
package/dist/next.js CHANGED
@@ -2,11 +2,11 @@
2
2
  import {
3
3
  initObservers,
4
4
  teardownObservers
5
- } from "./chunk-ABGGPWIX.js";
5
+ } from "./chunk-GV4CWEWY.js";
6
6
  import {
7
7
  trackCustomEvent
8
- } from "./chunk-ANB42OLW.js";
9
- import "./chunk-2WQ7PVRU.js";
8
+ } from "./chunk-K3NVHH7K.js";
9
+ import "./chunk-6NWJK7V7.js";
10
10
 
11
11
  // src/next.tsx
12
12
  import { useEffect, useRef } from "react";
package/dist/react.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { b as AnalyticsManifest, c as ConsentBannerOptions } from './consent-banner-WVGsxcY2.js';
2
+ import { b as AnalyticsManifest } from './types-VA3xp0bt.js';
3
3
 
4
4
  interface RootTaleAnalyticsProps {
5
5
  /** buildManifest() 결과. plane ① 태그 + fail-closed 동의 기본값. */
@@ -8,12 +8,8 @@ interface RootTaleAnalyticsProps {
8
8
  collectUrl?: string;
9
9
  /** 비콘용 site 식별자. collectUrl 과 함께 제공. */
10
10
  siteId?: string;
11
- /** 동의 배너 개인정보처리방침 링크. */
12
- privacyUrl?: string;
13
11
  /** 비콘 pageview 자동 전송 (기본 true). */
14
12
  trackPageview?: boolean;
15
- /** 동의 배너 카피 override. */
16
- bannerOptions?: ConsentBannerOptions;
17
13
  /**
18
14
  * GA4 미러 브리지 스크립트 주입 여부(설계 §0, 기본 true). beacon 이 없으면
19
15
  * (collectUrl/siteId 미설정) 어차피 미주입 — 이벤트를 흘려보낼 send() 자체가
@@ -34,6 +30,6 @@ interface RootTaleAnalyticsProps {
34
30
  * 단일 드롭인 컴포넌트. RSC 호환(hook/상태 없음) — Server Component layout 에
35
31
  * 바로 배치 가능.
36
32
  */
37
- declare function RootTaleAnalytics({ manifest, collectUrl, siteId, privacyUrl, trackPageview, bannerOptions, enableGa4Bridge, enableObservers, }: RootTaleAnalyticsProps): react_jsx_runtime.JSX.Element;
33
+ declare function RootTaleAnalytics({ manifest, collectUrl, siteId, trackPageview, enableGa4Bridge, enableObservers, }: RootTaleAnalyticsProps): react_jsx_runtime.JSX.Element;
38
34
 
39
35
  export { RootTaleAnalytics, type RootTaleAnalyticsProps };
package/dist/react.js CHANGED
@@ -1,16 +1,14 @@
1
1
  import {
2
- renderConsentBannerMarkup,
3
- renderConsentBannerScript,
4
2
  renderConsentBootstrap,
5
3
  renderGa4BridgeScript,
6
4
  renderJourneyBridgeScript,
7
5
  renderLoaderScript
8
- } from "./chunk-G7OLBHT5.js";
6
+ } from "./chunk-EHXYDR7C.js";
9
7
  import {
10
8
  renderBeaconScript
11
- } from "./chunk-ANB42OLW.js";
9
+ } from "./chunk-K3NVHH7K.js";
12
10
  import "./chunk-GBIGQHVN.js";
13
- import "./chunk-2WQ7PVRU.js";
11
+ import "./chunk-6NWJK7V7.js";
14
12
 
15
13
  // src/react.tsx
16
14
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
@@ -18,17 +16,11 @@ function RootTaleAnalytics({
18
16
  manifest,
19
17
  collectUrl,
20
18
  siteId,
21
- privacyUrl,
22
19
  trackPageview,
23
- bannerOptions,
24
20
  enableGa4Bridge = true,
25
21
  enableObservers = true
26
22
  }) {
27
23
  const beacon = collectUrl && siteId ? renderBeaconScript({ collectUrl, siteId, trackPageview }) : null;
28
- const bannerOpts = {
29
- privacyUrl,
30
- ...bannerOptions
31
- };
32
24
  return /* @__PURE__ */ jsxs(Fragment, { children: [
33
25
  /* @__PURE__ */ jsx(
34
26
  "script",
@@ -42,21 +34,6 @@ function RootTaleAnalytics({
42
34
  dangerouslySetInnerHTML: { __html: renderLoaderScript(manifest) }
43
35
  }
44
36
  ),
45
- /* @__PURE__ */ jsx(
46
- "div",
47
- {
48
- suppressHydrationWarning: true,
49
- dangerouslySetInnerHTML: {
50
- __html: renderConsentBannerMarkup(bannerOpts)
51
- }
52
- }
53
- ),
54
- /* @__PURE__ */ jsx(
55
- "script",
56
- {
57
- dangerouslySetInnerHTML: { __html: renderConsentBannerScript() }
58
- }
59
- ),
60
37
  beacon ? /* @__PURE__ */ jsx("script", { dangerouslySetInnerHTML: { __html: beacon } }) : null,
61
38
  beacon ? /* @__PURE__ */ jsx(
62
39
  "script",
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\";\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":[]}
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// (통계 granted·광고 denied, ADR-0038 Amend 4) → loader → 비콘.\n//\n// 동의 배너는 렌더하지 않는다(Amend 4) — 통계는 한국법상 옵트인 대상이\n// 아니고, 광고 도구는 기본 denied 라 동의 UI 없이는 발화 자체가 불가하다.\n// 광고 연동을 도입하는 날이 오면 그때 consent-banner.ts 를 다시 물린다.\n\nimport type { AnalyticsManifest } from \"./types\";\nimport { renderConsentBootstrap, renderLoaderScript } from \"./loader\";\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 /** 비콘 pageview 자동 전송 (기본 true). */\n trackPageview?: boolean;\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 trackPageview,\n enableGa4Bridge = true,\n enableObservers = true,\n}: RootTaleAnalyticsProps) {\n const beacon =\n collectUrl && siteId\n ? renderBeaconScript({ collectUrl, siteId, trackPageview })\n : null;\n\n return (\n <>\n <script\n dangerouslySetInnerHTML={{ __html: renderConsentBootstrap() }}\n />\n <script\n dangerouslySetInnerHTML={{ __html: renderLoaderScript(manifest) }}\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":";;;;;;;;;;;;;AAuEI,mBACE,KADF;AAdG,SAAS,kBAAkB;AAAA,EAChC;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;AAEN,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,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":[]}
@@ -39,15 +39,4 @@ interface AnalyticsManifest {
39
39
  consentDefault: ConsentState;
40
40
  }
41
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 };
42
+ export type { AnalyticsSiteConfig as A, ConsentCategory as C, ResolvedTag as R, TagProvider as T, ConsentState as a, AnalyticsManifest as b, ConsentValue as c, TagConfig as d };