@roottale/cms-renderer-next 0.46.0 → 0.47.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # @roottale/cms-renderer-next
2
2
 
3
+ ## 0.47.0
4
+
5
+ ### Minor Changes
6
+
7
+ - bfe0d14: 측정 계측 체계 v2(ADR-0038/ADR-0048 amend) 서버 수용 배선 — 유입 어트리뷰션에
8
+ utm_term/utm_content/광고 클릭 플래그/문의 직전 유입(last-touch) 필드를
9
+ 추가하고, 방문 여정(journey) JSON을 문의 제출에 함께 전달할 수 있게 했습니다.
10
+ - `@roottale/cms-client`: `InquiryAttribution`에 `utm_term`·`utm_content`·
11
+ `ad_click`·`last_touch_*` 필드 추가, `readAttribution()`/
12
+ `parseAttributionJson()`가 새 필드를 읽습니다. `SubmitInquiryFields`에
13
+ `journey?: string`를 추가해 방문 여정 원문을 `attr_journey` 폼 필드로 함께
14
+ 전송합니다.
15
+ - `@roottale/analytics-runtime`: 이벤트 사전(`TRACK_EVENTS`)에 `wireDims`
16
+ 필드를 추가해 `/collect` 비콘 payload의 실제 키 이름을 명시했습니다. 브라우저
17
+ 코드 없이 순수 이벤트 사전만 담은 `./events` 서브패스를 신설했습니다(서버
18
+ 런타임에서 안전하게 import).
19
+ - `@roottale/cms-renderer-next`: 위 cms-client 타입 변경에 따른 patch(연동
20
+ linked 그룹).
21
+
22
+ ### Patch Changes
23
+
24
+ - Updated dependencies [bfe0d14]
25
+ - @roottale/cms-client@0.47.0
26
+
3
27
  ## 0.46.0
4
28
 
5
29
  ### Minor Changes
@@ -0,0 +1,55 @@
1
+ "use client";
2
+
3
+ // src/lead-form-meta.client.tsx
4
+ import { useEffect, useRef } from "react";
5
+ import { readAttribution } from "@roottale/cms-client/attribution";
6
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
7
+ var JOURNEY_STORAGE_KEY = "_rt_journey";
8
+ function LeadFormMeta({ formId }) {
9
+ const attributionRef = useRef(null);
10
+ const journeyRef = useRef(null);
11
+ const startedRef = useRef(false);
12
+ useEffect(() => {
13
+ try {
14
+ const attribution = readAttribution();
15
+ if (attribution && attributionRef.current) {
16
+ attributionRef.current.value = JSON.stringify(attribution);
17
+ }
18
+ } catch {
19
+ }
20
+ const form = attributionRef.current?.form ?? journeyRef.current?.form;
21
+ if (!form) return;
22
+ const handleFocusIn = () => {
23
+ if (startedRef.current) return;
24
+ startedRef.current = true;
25
+ window.__rtTrack?.("form_start", { form_id: formId });
26
+ };
27
+ const handleFormData = (event) => {
28
+ try {
29
+ const raw = window.sessionStorage.getItem(JOURNEY_STORAGE_KEY);
30
+ event.formData.set("journey", raw ?? "");
31
+ } catch {
32
+ }
33
+ };
34
+ const handleSubmit = () => {
35
+ window.__rtTrack?.("form_submit_attempt", { form_id: formId });
36
+ };
37
+ form.addEventListener("focusin", handleFocusIn);
38
+ form.addEventListener("formdata", handleFormData);
39
+ form.addEventListener("submit", handleSubmit);
40
+ return () => {
41
+ form.removeEventListener("focusin", handleFocusIn);
42
+ form.removeEventListener("formdata", handleFormData);
43
+ form.removeEventListener("submit", handleSubmit);
44
+ };
45
+ }, [formId]);
46
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
47
+ /* @__PURE__ */ jsx("input", { type: "hidden", name: "attribution", ref: attributionRef, defaultValue: "" }),
48
+ /* @__PURE__ */ jsx("input", { type: "hidden", name: "journey", ref: journeyRef, defaultValue: "" })
49
+ ] });
50
+ }
51
+
52
+ export {
53
+ LeadFormMeta
54
+ };
55
+ //# sourceMappingURL=chunk-VG6UUF4D.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lead-form-meta.client.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * `LeadFormMeta` — `RootTaleLeadForm`(RSC, `LeadForm.tsx`) 내부에 심는\n * 어트리뷰션·여정 hidden input 배선 + 폼 계측 발화 island (측정 설계 v2 §2/§3/§5\n * — `measurement-design-draft.md`, 워커 브리프 B). \"LeadForm 어트리뷰션 배선\n * 복구\" — starter `/api/inquiry` 가 지금까지 `attribution`/`journey` 를 전혀\n * 동반하지 않던 끊긴 고리를 여기서 다시 잇는다.\n *\n * RSC 경계(설계 §5): `LeadForm.tsx` 는 계속 서버 컴포넌트로 남고, 이 파일만\n * \"use client\" — form 태그 **내부** 자식으로 삽입한다. tsup 독립 entry\n * (`lead-form-meta-client`, tsup.config.ts) + `esbuild-plugin-preserve-directives`\n * 가 이 청크만 \"use client\" 지시어를 보존한다 — `hero-carousel-client.tsx` 가\n * `templates.js` 에서 격리되는 것과 동일한 관행을 `server.js`(LeadForm 이\n * 속한 entry)에 적용한다.\n *\n * 배선:\n * - `attribution` hidden input — `@roottale/cms-client/attribution` 의\n * `readAttribution()` 으로 localStorage `_rt_attr` first-touch 를 마운트\n * 시 1회 판독해 채운다. first-touch 봉투는 제출 시점까지 안정적이므로\n * 마운트 스냅샷으로 충분하다(설계 §2). `readAttribution()` 을 쓰는 이유 —\n * 비콘이 저장하는 압축 키(`lp/rs/us/um/uc/rf`)를 서버 `parseAttributionJson()`\n * 이 기대하는 확장 키(`landing_path/rt_src/...`)로 변환하는 매핑이 이미\n * 이 함수에 있다(파일 헤더 JSDoc 사용례와 동일 패턴) — 압축 원문을 그대로\n * 넘기면 서버가 못 읽는다.\n * - `journey` hidden input — sessionStorage `_rt_journey` 를 **마운트가\n * 아니라 `formdata` 이벤트 시점에 재직렬화**한다(설계 §3 — 방문 중 계속\n * 쌓이는 값이라 마운트 스냅샷은 진부해진다). hidden input 의 DOM `value`\n * 는 항상 빈 문자열로 SSR/초기 클라 렌더가 일치(hydration-safe) — 실제\n * 전송값은 `formdata` 이벤트에서 `event.formData.set()` 으로 직접 주입해\n * DOM 갱신·리렌더가 필요 없다. `_rt_journey` 키 자체는\n * analytics-runtime(별도 워커가 동시 작업 중이라 이 패키지를 import 하지\n * 않는다)과의 공유 계약 — 값 자체가 계약(문자열 중복은 beacon.ts\n * `ATTRIBUTION_STORAGE_KEY` 를 cms-client `attribution.ts` 가 이미 중복\n * 선언하는 기존 관행과 동일).\n * - `form_start` — form 내 첫 focusin 1회.\n * - `form_submit_attempt` — form `submit` 이벤트(제출 버튼 클릭·Enter 제출\n * 공통 — 브라우저 스펙상 `submit` 이 `formdata` 보다 먼저 fire 한다).\n *\n * `window.__rtTrack` 은 `@roottale/analytics-runtime` 의 비콘 런타임(beacon.ts)\n * 이 노출하는 전역 — 이 패키지가 그 패키지를 직접 import 하지 않고 전역\n * 계약만 소비한다(빌드 의존 최소화 + 비콘 미설정 사이트에서 자동 no-op,\n * `not-found.tsx` `trackCustomEvent` 관행과 동일 계약, 다만 여기선 전역을\n * 직접 optional-chain 호출한다).\n */\nimport { useEffect, useRef } from \"react\";\nimport type { ReactElement } from \"react\";\nimport { readAttribution } from \"@roottale/cms-client/attribution\";\n\n/** sessionStorage 여정 로그 키 — analytics-runtime 관측자와의 공유 계약(설계 §3). */\nconst JOURNEY_STORAGE_KEY = \"_rt_journey\";\n\ndeclare global {\n interface Window {\n /** beacon.ts RUNTIME 이 노출하는 `send()` — 비콘 미설정 사이트에선 undefined. */\n __rtTrack?: (ev: string, extra?: Record<string, unknown>) => void;\n }\n}\n\nexport interface LeadFormMetaProps {\n /** `form_start`/`form_submit_attempt` 차원(form_id). 예: `\"contact\"`. */\n formId: string;\n}\n\n/**\n * `RootTaleLeadForm` 의 `<form>` 내부에 렌더 — hidden input 2개(어트리뷰션·\n * 여정) + 폼 계측 리스너. 시각 출력 없음(레이아웃 영향 0).\n */\nexport function LeadFormMeta({ formId }: LeadFormMetaProps): ReactElement {\n const attributionRef = useRef<HTMLInputElement>(null);\n const journeyRef = useRef<HTMLInputElement>(null);\n const startedRef = useRef(false);\n\n useEffect(() => {\n try {\n const attribution = readAttribution();\n if (attribution && attributionRef.current) {\n attributionRef.current.value = JSON.stringify(attribution);\n }\n } catch {\n /* storage 접근 불가(프라이빗 모드 등) — 무동반 제출, 폼 자체는 정상 동작 */\n }\n\n const form = attributionRef.current?.form ?? journeyRef.current?.form;\n if (!form) return;\n\n const handleFocusIn = (): void => {\n if (startedRef.current) return;\n startedRef.current = true;\n window.__rtTrack?.(\"form_start\", { form_id: formId });\n };\n\n const handleFormData = (event: FormDataEvent): void => {\n // 설계 §3 — 마운트 스냅샷이 아니라 제출 시점 재직렬화. FormData 엔트리를\n // 직접 덮어써 hidden input 의 실제 DOM value 갱신 없이 최신값을 싣는다.\n try {\n const raw = window.sessionStorage.getItem(JOURNEY_STORAGE_KEY);\n event.formData.set(\"journey\", raw ?? \"\");\n } catch {\n /* noop — journey 없이 제출(문의 저장 자체는 서버에서 계속 진행) */\n }\n };\n\n const handleSubmit = (): void => {\n window.__rtTrack?.(\"form_submit_attempt\", { form_id: formId });\n };\n\n form.addEventListener(\"focusin\", handleFocusIn);\n form.addEventListener(\"formdata\", handleFormData as EventListener);\n form.addEventListener(\"submit\", handleSubmit);\n\n return () => {\n form.removeEventListener(\"focusin\", handleFocusIn);\n form.removeEventListener(\"formdata\", handleFormData as EventListener);\n form.removeEventListener(\"submit\", handleSubmit);\n };\n }, [formId]);\n\n return (\n <>\n <input type=\"hidden\" name=\"attribution\" ref={attributionRef} defaultValue=\"\" />\n <input type=\"hidden\" name=\"journey\" ref={journeyRef} defaultValue=\"\" />\n </>\n );\n}\n"],"mappings":";AA6CA,SAAS,WAAW,cAAc;AAElC,SAAS,uBAAuB;AAwE5B,mBACE,KADF;AArEJ,IAAM,sBAAsB;AAkBrB,SAAS,aAAa,EAAE,OAAO,GAAoC;AACxE,QAAM,iBAAiB,OAAyB,IAAI;AACpD,QAAM,aAAa,OAAyB,IAAI;AAChD,QAAM,aAAa,OAAO,KAAK;AAE/B,YAAU,MAAM;AACd,QAAI;AACF,YAAM,cAAc,gBAAgB;AACpC,UAAI,eAAe,eAAe,SAAS;AACzC,uBAAe,QAAQ,QAAQ,KAAK,UAAU,WAAW;AAAA,MAC3D;AAAA,IACF,QAAQ;AAAA,IAER;AAEA,UAAM,OAAO,eAAe,SAAS,QAAQ,WAAW,SAAS;AACjE,QAAI,CAAC,KAAM;AAEX,UAAM,gBAAgB,MAAY;AAChC,UAAI,WAAW,QAAS;AACxB,iBAAW,UAAU;AACrB,aAAO,YAAY,cAAc,EAAE,SAAS,OAAO,CAAC;AAAA,IACtD;AAEA,UAAM,iBAAiB,CAAC,UAA+B;AAGrD,UAAI;AACF,cAAM,MAAM,OAAO,eAAe,QAAQ,mBAAmB;AAC7D,cAAM,SAAS,IAAI,WAAW,OAAO,EAAE;AAAA,MACzC,QAAQ;AAAA,MAER;AAAA,IACF;AAEA,UAAM,eAAe,MAAY;AAC/B,aAAO,YAAY,uBAAuB,EAAE,SAAS,OAAO,CAAC;AAAA,IAC/D;AAEA,SAAK,iBAAiB,WAAW,aAAa;AAC9C,SAAK,iBAAiB,YAAY,cAA+B;AACjE,SAAK,iBAAiB,UAAU,YAAY;AAE5C,WAAO,MAAM;AACX,WAAK,oBAAoB,WAAW,aAAa;AACjD,WAAK,oBAAoB,YAAY,cAA+B;AACpE,WAAK,oBAAoB,UAAU,YAAY;AAAA,IACjD;AAAA,EACF,GAAG,CAAC,MAAM,CAAC;AAEX,SACE,iCACE;AAAA,wBAAC,WAAM,MAAK,UAAS,MAAK,eAAc,KAAK,gBAAgB,cAAa,IAAG;AAAA,IAC7E,oBAAC,WAAM,MAAK,UAAS,MAAK,WAAU,KAAK,YAAY,cAAa,IAAG;AAAA,KACvE;AAEJ;","names":[]}
@@ -23,6 +23,9 @@ import {
23
23
  StaggerGroup,
24
24
  StaggerItem
25
25
  } from "./chunk-ZL2NA3QT.js";
26
+ import {
27
+ LeadFormMeta
28
+ } from "./chunk-VG6UUF4D.js";
26
29
  import {
27
30
  collectCategories,
28
31
  collectCategoriesWithCounts,
@@ -126,6 +129,7 @@ function RootTaleLeadForm(props) {
126
129
  "data-roottale-cms": "lead-form",
127
130
  className: formClass,
128
131
  children: [
132
+ /* @__PURE__ */ jsx(LeadFormMeta, { formId: "contact" }),
129
133
  heading ? /* @__PURE__ */ jsx("h2", { className: "rt-cms-lead-heading", children: heading }) : null,
130
134
  description ? /* @__PURE__ */ jsx("p", { className: "rt-cms-lead-description", children: description }) : null,
131
135
  redirectUrl ? /* @__PURE__ */ jsx("input", { type: "hidden", name: "_redirect_url", value: redirectUrl }) : null,
@@ -270,7 +274,7 @@ function RootTaleLeadForm(props) {
270
274
  /* @__PURE__ */ jsx("strong", { children: "(\uD544\uC218)" }),
271
275
  " \uAC1C\uC778\uC815\uBCF4 \uC218\uC9D1\xB7\uC774\uC6A9\uC5D0 \uB3D9\uC758\uD569\uB2C8\uB2E4. (\uC218\uC9D1 \uD56D\uBAA9:",
272
276
  " ",
273
- isMedical ? "\uC774\uB984\xB7\uC5F0\uB77D\uCC98\xB7\uC774\uBA54\uC77C\xB7\uC0C1\uB2F4 \uB0B4\uC6A9(\uC791\uC131 \uC2DC)" : "\uC774\uB984\xB7\uC5F0\uB77D\uCC98\xB7\uC774\uBA54\uC77C\xB7\uC0AC\uC5C5\uCCB4\uBA85",
277
+ isMedical ? "\uC774\uB984\xB7\uC5F0\uB77D\uCC98\xB7\uC774\uBA54\uC77C\xB7\uC0C1\uB2F4 \uB0B4\uC6A9(\uC791\uC131 \uC2DC)\xB7\uC0AC\uC774\uD2B8 \uC774\uC6A9 \uAE30\uB85D(\uBC29\uBB38 \uD398\uC774\uC9C0\xB7\uC0C1\uD638\uC791\uC6A9)" : "\uC774\uB984\xB7\uC5F0\uB77D\uCC98\xB7\uC774\uBA54\uC77C\xB7\uC0AC\uC5C5\uCCB4\uBA85\xB7\uC0AC\uC774\uD2B8 \uC774\uC6A9 \uAE30\uB85D(\uBC29\uBB38 \uD398\uC774\uC9C0\xB7\uC0C1\uD638\uC791\uC6A9)",
274
278
  ". \uBCF4\uAD00 \uAE30\uAC04: \uBB38\uC758 \uC885\uB8CC \uD6C4 3\uB144)"
275
279
  ] })
276
280
  ] }),
@@ -364,6 +368,11 @@ function CtaLink({
364
368
  function ctaTrackForHref(href) {
365
369
  return href.startsWith("tel:") ? "cta_banner" : "cta_hero";
366
370
  }
371
+ function contactHrefTrack(href) {
372
+ if (href.startsWith("tel:")) return "phone_click";
373
+ if (href.startsWith("mailto:")) return "email_click";
374
+ return void 0;
375
+ }
367
376
  function TemplateHeroProof({
368
377
  proofMetrics
369
378
  }) {
@@ -813,8 +822,24 @@ function FooterContact({
813
822
  }
814
823
  return /* @__PURE__ */ jsxs2("address", { className: "rt-tpl-footer-contact", children: [
815
824
  contactInfo.address ? /* @__PURE__ */ jsx2("span", { children: contactInfo.address }) : null,
816
- contactInfo.phone ? /* @__PURE__ */ jsx2("a", { href: telHref(contactInfo.phone), children: contactInfo.phone }) : null,
817
- contactInfo.email ? /* @__PURE__ */ jsx2("a", { href: `mailto:${contactInfo.email}`, children: contactInfo.email }) : null
825
+ contactInfo.phone ? /* @__PURE__ */ jsx2(
826
+ "a",
827
+ {
828
+ href: telHref(contactInfo.phone),
829
+ "data-track": "phone_click",
830
+ "data-track-placement": "footer",
831
+ children: contactInfo.phone
832
+ }
833
+ ) : null,
834
+ contactInfo.email ? /* @__PURE__ */ jsx2(
835
+ "a",
836
+ {
837
+ href: `mailto:${contactInfo.email}`,
838
+ "data-track": "email_click",
839
+ "data-track-placement": "footer",
840
+ children: contactInfo.email
841
+ }
842
+ ) : null
818
843
  ] });
819
844
  }
820
845
  function FooterLegal(section) {
@@ -1631,7 +1656,18 @@ function ContactActions({
1631
1656
  actions
1632
1657
  }) {
1633
1658
  if (!actions || actions.length === 0) return null;
1634
- return /* @__PURE__ */ jsx2("ul", { className: "rt-tpl-contact-actions", children: actions.map((action) => /* @__PURE__ */ jsx2("li", { "data-kind": action.kind, children: /* @__PURE__ */ jsx2("a", { href: action.href, children: action.label }) }, `${action.label}-${action.href}`)) });
1659
+ return /* @__PURE__ */ jsx2("ul", { className: "rt-tpl-contact-actions", children: actions.map((action) => {
1660
+ const track = contactHrefTrack(action.href);
1661
+ return /* @__PURE__ */ jsx2("li", { "data-kind": action.kind, children: /* @__PURE__ */ jsx2(
1662
+ "a",
1663
+ {
1664
+ href: action.href,
1665
+ "data-track": track,
1666
+ "data-track-placement": track ? "contact_actions" : void 0,
1667
+ children: action.label
1668
+ }
1669
+ ) }, `${action.label}-${action.href}`);
1670
+ }) });
1635
1671
  }
1636
1672
  function TemplateContactChannelCards(section) {
1637
1673
  const { heading, channels, map, mapEmbedUrl, actions } = section.props;
@@ -1673,7 +1709,8 @@ function TemplateContactChannelCards(section) {
1673
1709
  {
1674
1710
  className: "rt-tpl-contact-channel-card",
1675
1711
  href: channel.href,
1676
- "data-track": channel.href.startsWith("tel:") ? "cta_call" : "cta_banner",
1712
+ "data-track": channel.href.startsWith("tel:") ? "cta_call" : channel.href.startsWith("mailto:") ? "email_click" : "cta_banner",
1713
+ "data-track-placement": channel.href.startsWith("mailto:") ? "contact_channels" : void 0,
1677
1714
  children: content
1678
1715
  }
1679
1716
  ) : /* @__PURE__ */ jsx2("div", { className: "rt-tpl-contact-channel-card", children: content })
@@ -1731,7 +1768,16 @@ function TemplateContactVisitBand(section) {
1731
1768
  /* @__PURE__ */ jsx2("h3", { className: "rt-tpl-contact-visit-title", children: "\uC624\uC2DC\uB294 \uAE38" }),
1732
1769
  /* @__PURE__ */ jsx2(ContactMapBlock, { map, mapEmbedUrl }),
1733
1770
  address ? /* @__PURE__ */ jsx2("address", { className: "rt-tpl-contact-visit-address", children: address }) : null,
1734
- mapAction ? /* @__PURE__ */ jsx2("a", { className: "rt-tpl-contact-visit-link", href: mapAction.href, children: mapAction.label }) : null
1771
+ mapAction ? /* @__PURE__ */ jsx2(
1772
+ "a",
1773
+ {
1774
+ className: "rt-tpl-contact-visit-link",
1775
+ href: mapAction.href,
1776
+ "data-track": contactHrefTrack(mapAction.href),
1777
+ "data-track-placement": contactHrefTrack(mapAction.href) ? "visit" : void 0,
1778
+ children: mapAction.label
1779
+ }
1780
+ ) : null
1735
1781
  ] }),
1736
1782
  /* @__PURE__ */ jsxs2("section", { className: "rt-tpl-contact-visit-panel", "aria-label": "\uC804\uD654", children: [
1737
1783
  /* @__PURE__ */ jsx2("h3", { className: "rt-tpl-contact-visit-title", children: "\uC804\uD654" }),
@@ -1745,7 +1791,16 @@ function TemplateContactVisitBand(section) {
1745
1791
  }
1746
1792
  ) : null,
1747
1793
  hours ? /* @__PURE__ */ jsx2(ContactHoursSummary, { hours: hours.slice(0, 2) }) : null,
1748
- ctaAction ? /* @__PURE__ */ jsx2("a", { className: "rt-tpl-btn rt-tpl-btn--primary", href: ctaAction.href, children: ctaAction.label }) : null
1794
+ ctaAction ? /* @__PURE__ */ jsx2(
1795
+ "a",
1796
+ {
1797
+ className: "rt-tpl-btn rt-tpl-btn--primary",
1798
+ href: ctaAction.href,
1799
+ "data-track": contactHrefTrack(ctaAction.href),
1800
+ "data-track-placement": contactHrefTrack(ctaAction.href) ? "visit" : void 0,
1801
+ children: ctaAction.label
1802
+ }
1803
+ ) : null
1749
1804
  ] })
1750
1805
  ] })
1751
1806
  ] })
@@ -3526,4 +3581,4 @@ export {
3526
3581
  RootTalePage,
3527
3582
  RenderTiptap
3528
3583
  };
3529
- //# sourceMappingURL=chunk-JXRHZGRJ.js.map
3584
+ //# sourceMappingURL=chunk-ZHXYCABN.js.map