diff --git a/docs/analytics/gtm_webforai_import.json b/docs/analytics/gtm_webforai_import.json new file mode 100644 index 0000000..72b285d --- /dev/null +++ b/docs/analytics/gtm_webforai_import.json @@ -0,0 +1,268 @@ +{ + "exportFormatVersion": 2, + "exportTime": "2026-10-07 12:00:00", + "containerVersion": { + "accountId": "6381018301", + "containerId": "266335040", + "containerVersionId": "0", + "container": { + "accountId": "6381018301", + "containerId": "266335040", + "name": "webforai.kr", + "publicId": "GTM-WQNTZVZH", + "usageContext": [ + "WEB" + ] + }, + "tag": [ + { + "accountId": "6381018301", + "containerId": "266335040", + "tagId": "1", + "name": "GA4 - 기본 태그", + "type": "googtag", + "parameter": [ + { + "type": "TEMPLATE", + "key": "tagId", + "value": "G-013Z3BFY35" + } + ], + "firingTriggerId": [ + "2147479553" + ], + "tagFiringOption": "ONCE_PER_EVENT" + }, + { + "accountId": "6381018301", + "containerId": "266335040", + "tagId": "2", + "name": "GA4 이벤트 - generate_lead", + "type": "gaawe", + "parameter": [ + { + "type": "TEMPLATE", + "key": "eventName", + "value": "generate_lead" + }, + { + "type": "TEMPLATE", + "key": "measurementIdOverride", + "value": "G-013Z3BFY35" + }, + { + "type": "LIST", + "key": "eventSettingsTable", + "list": [ + { + "type": "MAP", + "map": [ + { + "type": "TEMPLATE", + "key": "parameter", + "value": "lead_source" + }, + { + "type": "TEMPLATE", + "key": "parameterValue", + "value": "{{DLV - lead_source}}" + } + ] + } + ] + } + ], + "firingTriggerId": [ + "10" + ], + "tagFiringOption": "ONCE_PER_EVENT" + }, + { + "accountId": "6381018301", + "containerId": "266335040", + "tagId": "3", + "name": "GA4 이벤트 - click_tel", + "type": "gaawe", + "parameter": [ + { + "type": "TEMPLATE", + "key": "eventName", + "value": "click_tel" + }, + { + "type": "TEMPLATE", + "key": "measurementIdOverride", + "value": "G-013Z3BFY35" + } + ], + "firingTriggerId": [ + "11" + ], + "tagFiringOption": "ONCE_PER_EVENT" + }, + { + "accountId": "6381018301", + "containerId": "266335040", + "tagId": "4", + "name": "GA4 이벤트 - click_email", + "type": "gaawe", + "parameter": [ + { + "type": "TEMPLATE", + "key": "eventName", + "value": "click_email" + }, + { + "type": "TEMPLATE", + "key": "measurementIdOverride", + "value": "G-013Z3BFY35" + } + ], + "firingTriggerId": [ + "12" + ], + "tagFiringOption": "ONCE_PER_EVENT" + } + ], + "trigger": [ + { + "accountId": "6381018301", + "containerId": "266335040", + "triggerId": "10", + "name": "CE - generate_lead", + "type": "CUSTOM_EVENT", + "customEventFilter": [ + { + "type": "EQUALS", + "parameter": [ + { + "type": "TEMPLATE", + "key": "arg0", + "value": "{{_event}}" + }, + { + "type": "TEMPLATE", + "key": "arg1", + "value": "generate_lead" + } + ] + } + ] + }, + { + "accountId": "6381018301", + "containerId": "266335040", + "triggerId": "11", + "name": "링크 클릭 - tel", + "type": "LINK_CLICK", + "filter": [ + { + "type": "STARTS_WITH", + "parameter": [ + { + "type": "TEMPLATE", + "key": "arg0", + "value": "{{Click URL}}" + }, + { + "type": "TEMPLATE", + "key": "arg1", + "value": "tel:" + } + ] + } + ], + "waitForTags": { + "type": "BOOLEAN", + "key": "waitForTags", + "value": "false" + }, + "checkValidation": { + "type": "BOOLEAN", + "key": "checkValidation", + "value": "false" + } + }, + { + "accountId": "6381018301", + "containerId": "266335040", + "triggerId": "12", + "name": "링크 클릭 - mailto", + "type": "LINK_CLICK", + "filter": [ + { + "type": "STARTS_WITH", + "parameter": [ + { + "type": "TEMPLATE", + "key": "arg0", + "value": "{{Click URL}}" + }, + { + "type": "TEMPLATE", + "key": "arg1", + "value": "mailto:" + } + ] + } + ], + "waitForTags": { + "type": "BOOLEAN", + "key": "waitForTags", + "value": "false" + }, + "checkValidation": { + "type": "BOOLEAN", + "key": "checkValidation", + "value": "false" + } + } + ], + "variable": [ + { + "accountId": "6381018301", + "containerId": "266335040", + "variableId": "20", + "name": "DLV - lead_source", + "type": "v", + "parameter": [ + { + "type": "INTEGER", + "key": "dataLayerVersion", + "value": "2" + }, + { + "type": "BOOLEAN", + "key": "setDefaultValue", + "value": "false" + }, + { + "type": "TEMPLATE", + "key": "name", + "value": "lead_source" + } + ] + } + ], + "builtInVariable": [ + { + "accountId": "6381018301", + "containerId": "266335040", + "type": "CLICK_URL", + "name": "Click URL" + }, + { + "accountId": "6381018301", + "containerId": "266335040", + "type": "PAGE_PATH", + "name": "Page Path" + }, + { + "accountId": "6381018301", + "containerId": "266335040", + "type": "EVENT", + "name": "Event" + } + ] + } +} \ No newline at end of file diff --git a/src/components/ContactModal.tsx b/src/components/ContactModal.tsx index 22e1634..a356449 100644 --- a/src/components/ContactModal.tsx +++ b/src/components/ContactModal.tsx @@ -12,6 +12,7 @@ import { createPortal } from 'react-dom'; import { AnimatePresence, motion } from 'motion/react'; import { ArrowRight } from 'lucide-react'; import { submitLead } from '../lib/submitLead'; +import { trackEvent } from '../lib/analytics'; import { CONTACT_EMAIL } from '../lib/contact'; interface ContactModalProps { @@ -86,6 +87,8 @@ export default function ContactModal({ open, onClose, source }: ContactModalProp source, website, }); + // 접수에 성공한 경우만 리드로 센다. 어느 버튼에서 열었는지(source)만 보낸다. + if (result.ok) trackEvent('generate_lead', { lead_source: source }); setState(result.ok ? 'done' : 'error'); }; diff --git a/src/lib/analytics.ts b/src/lib/analytics.ts new file mode 100644 index 0000000..e45d571 --- /dev/null +++ b/src/lib/analytics.ts @@ -0,0 +1,11 @@ +/** + * 방문 측정 이벤트. GTM 이 있는 배포(webforai.kr)에서만 dataLayer 로 넘기고, 없으면 아무것도 하지 않는다. + * 이벤트에는 개인정보(이름·연락처·문의 내용)를 넣지 않는다. 어느 버튼에서 왔는지 같은 화면 정보만 보낸다. + */ +type DataLayerWindow = Window & { dataLayer?: Record[] }; + +export function trackEvent(event: string, params: Record = {}): void { + const w = window as DataLayerWindow; + if (!Array.isArray(w.dataLayer)) return; + w.dataLayer.push({ event, ...params }); +} diff --git a/src/pages/PrivacyPage.tsx b/src/pages/PrivacyPage.tsx index dcd8b37..7f59648 100644 --- a/src/pages/PrivacyPage.tsx +++ b/src/pages/PrivacyPage.tsx @@ -1,7 +1,8 @@ /** * 개인정보처리방침 (/privacy). * 이 사이트가 실제로 처리하는 것만 적는다: 문의·진단 요청 서식(이름·연락처·회사·문의 내용) → /api/lead(Vercel) → - * Google Apps Script → 구글 시트 저장 + 알림 메일. 방문 분석 쿠키는 아직 쓰지 않는다. GA4 등을 붙이면 제8조를 고친다. + * Google Apps Script → 구글 시트 저장 + 알림 메일. 방문 분석은 GTM(GTM-WQNTZVZH) 안의 GA4(G-013Z3BFY35)로 한다. + * 광고 픽셀(Meta·네이버 등)을 GTM 에 추가하면 제1·5·8조를 함께 고친다. * 구조는 의료 분야 SaaS 방침(메디팔·애프터닥)의 조항 구성을 참고했고, 문장은 이 서비스에 맞춰 새로 썼다. */ import LegalLayout, { type LegalSection } from '../components/LegalLayout'; @@ -17,6 +18,7 @@ const SECTIONS: LegalSection[] = [ '- 필수: 성함, 연락처(전화번호 또는 이메일 중 하나 이상)', '- 선택: 회사(병원)명, 문의 내용', '서비스 이용 과정에서 접속 IP 주소, 브라우저 정보, 접속 일시가 웹 호스팅 서버의 로그로 자동 생성될 수 있습니다.', + '방문 분석을 위해 쿠키를 통해 방문 페이지, 체류 시간, 유입 경로, 기기·브라우저 종류, 대략적인 접속 지역(도시 단위)이 수집됩니다. 이 정보로 개인을 직접 식별하지 않습니다.', '회사는 만 14세 미만 아동의 개인정보를 수집하지 않으며, 환자의 건강 정보 등 민감정보를 서식으로 받지 않습니다. 문의 내용에 환자 개인정보를 적지 말아 주십시오.', ], }, @@ -49,6 +51,7 @@ const SECTIONS: LegalSection[] = [ body: [ '회사는 서비스 운영을 위해 아래와 같이 개인정보 처리업무를 위탁하며, 수탁자의 서버가 국외에 있어 개인정보가 국외로 이전됩니다.', '- Google LLC (미국): 문의 내용 저장(Google 스프레드시트) 및 접수 알림 메일 발송. 이전 항목은 제1조의 서식 입력 항목이며, 서식을 제출하는 시점에 네트워크로 전송됩니다. 보유 기간은 제3조와 같습니다.', + '- Google LLC (미국): 웹사이트 방문 분석(Google 애널리틱스, Google 태그 관리자). 이전 항목은 제1조의 방문 분석 정보이며, 웹사이트를 이용하는 시점에 네트워크로 전송됩니다. 보유 기간은 수집일부터 최대 14개월입니다.', '- Vercel Inc. (미국): 웹사이트 호스팅 및 서식 전송 처리. 이전 항목은 서식 입력 항목과 접속 기록이며, 웹사이트 이용 및 서식 제출 시 네트워크로 전송됩니다. 보유 기간은 위탁 계약 종료 시까지입니다.', '국외 이전을 원하지 않으시면 서식 대신 이메일 또는 전화로 문의하실 수 있습니다. 다만 이 경우에도 회신 메일은 회사의 메일 서비스를 통해 처리됩니다.', ], @@ -72,8 +75,12 @@ const SECTIONS: LegalSection[] = [ { title: '쿠키 등 자동 수집 장치', body: [ - '회사는 현재 이 웹사이트에서 광고 또는 방문 분석을 위한 쿠키를 사용하지 않습니다.', - '방문 분석 도구를 도입하는 경우 이 방침을 개정해 수집 항목, 목적, 거부 방법을 미리 알려 드립니다.', + '회사는 웹사이트 이용 현황을 파악하고 서비스를 개선하기 위해 Google 애널리틱스의 분석 쿠키를 사용합니다. 광고 목적의 쿠키는 사용하지 않습니다.', + '- 수집 항목: 제1조의 방문 분석 정보', + '- 이용 목적: 방문자 수, 많이 본 페이지, 유입 경로, 문의 전환 등 이용 현황 분석', + '- 거부 방법: 브라우저의 개인정보 보호 또는 쿠키 및 사이트 데이터 설정에서 이 사이트(webforai.kr)의 쿠키를 차단하거나 삭제할 수 있습니다. Google 애널리틱스 차단 브라우저 부가기능(tools.google.com/dlpage/gaoptout)으로도 수집을 거부할 수 있습니다.', + '쿠키 저장을 거부해도 웹사이트 이용과 문의에는 제한이 없습니다.', + '광고 목적의 쿠키나 추적 도구를 도입하는 경우 이 방침을 개정해 미리 알려 드립니다.', ], }, { diff --git a/vite.config.ts b/vite.config.ts index 592feba..7bc6126 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -41,6 +41,15 @@ export default defineConfig(({mode}) => { '', ] : []; + // 방문 측정: Google 태그 관리자(GTM) 컨테이너. GA4(G-013Z3BFY35)와 키 이벤트는 컨테이너 안에서 설정한다. + // 공개 배포(webforai.kr)에만 넣는다. 컨테이너 ID 는 공개 값이다. 개인정보처리방침 제5·8조와 함께 관리한다. + const GTM_ID = 'GTM-WQNTZVZH'; + const gtmHead = isDiscovery + ? [``] + : []; + const gtmBody = isDiscovery + ? `` + : ''; const headHtml = [ `${head.title}`, ``, @@ -50,6 +59,7 @@ export default defineConfig(({mode}) => { ``, ...share, ...verify, + ...gtmHead, ].join('\n '); return { @@ -78,7 +88,9 @@ export default defineConfig(({mode}) => { { name: 'infinith-head', transformIndexHtml(html: string) { - return html.replace(/[\s\S]*?<\/title>/, headHtml); + return html + .replace(/<title>[\s\S]*?<\/title>/, headHtml) + .replace('<body>', gtmBody ? `<body>\n ${gtmBody}` : '<body>'); }, }, tailwindcss(),