feat(ga4): AI 유입 트래픽 연동 뼈대 + 리포트 "측정 가능 범위" 절

2026-09-08 작업분. GA4 에서 AI 답변엔진 유입을 채널로 갈라 보는 뼈대를 세웠다.

- data/ai_channels.json: AI 답변엔진 리퍼러 목록(채널 그룹 정의의 근거)
- scripts/ga4_ai_traffic.py: GA4 Data API 로 AI 유입을 뽑는 러너. --mock 으로 합성
  데이터 검증까지 마쳤다. 실제 속성 연결은 GCP 계정이 정해진 뒤다
- supporters/src/layouts/Base.astro + 템플릿 site.json 2곳: ga4MeasurementId 태그.
  쿠키 고지 배너 결정 전까지 미리보기에는 ID 를 넣지 않는다
- scripts/build_discovery_report.ts: Integrity 절에 "측정 가능 범위" 를 넣어
  무엇을 재고 무엇을 못 재는지 리포트가 스스로 밝히게 했다
- 원진 진단 리포트 v2.1(HTML·PDF) 재생성분

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Haewon Kam 2026-09-09 10:53:54 +09:00
parent 4e73798cce
commit 8776a15183
9 changed files with 416 additions and 2 deletions

34
data/ai_channels.json Normal file
View File

@ -0,0 +1,34 @@
{
"_comment": "AI 검색 유입 분류 기준(단일 원본). GA4 세션 출처(sessionSource)·리퍼러·utm_source 를 이 표로 AI 채널에 묶는다. measurable 은 리포트의 '측정 가능 범위' 표에 그대로 나간다. 수정하면 scripts/ga4_ai_traffic.py 와 리포트 빌더가 같이 바뀐다.",
"version": "2026-09-08",
"channels": [
{ "id": "chatgpt", "label": "ChatGPT", "sources": ["chatgpt.com", "chat.openai.com", "openai.com"], "utm_sources": ["chatgpt.com", "chatgpt", "openai"],
"measurable": "yes", "note": "링크에 utm_source=chatgpt.com 을 붙여 보내므로 리퍼러가 비어도 잡힌다." },
{ "id": "perplexity", "label": "Perplexity", "sources": ["perplexity.ai", "www.perplexity.ai"], "utm_sources": ["perplexity", "perplexity.ai"],
"measurable": "partial", "note": "리퍼러를 자주 비운다. 잡히는 건수는 실제 유입의 일부다. 실측 언급률과 함께 읽는다." },
{ "id": "gemini", "label": "Gemini", "sources": ["gemini.google.com", "bard.google.com"], "utm_sources": ["gemini"],
"measurable": "yes", "note": "gemini.google.com 리퍼러로 분리된다." },
{ "id": "copilot", "label": "Copilot", "sources": ["copilot.microsoft.com", "bing.com/chat", "edgeservices.bing.com"], "utm_sources": ["copilot"],
"measurable": "partial", "note": "Bing 검색과 리퍼러가 섞이는 경우가 있다." },
{ "id": "claude", "label": "Claude", "sources": ["claude.ai"], "utm_sources": ["claude"],
"measurable": "yes", "note": "claude.ai 리퍼러." },
{ "id": "google_aio", "label": "Google AI Overview · AI Mode", "sources": [], "utm_sources": [],
"measurable": "no", "note": "구글 자연검색(google / organic)과 같은 리퍼러라 GA4로는 구분되지 않는다. 타사 도구의 'AI Overview 유입'도 추정치다. 가시성은 답변엔진 실측으로만 잰다." },
{ "id": "naver_ai", "label": "네이버 AI 브리핑", "sources": [], "utm_sources": [],
"measurable": "no", "note": "네이버 검색 리퍼러(search.naver.com)와 구분되지 않는다. 네이버 서치어드바이저 유입 키워드로 간접 확인만 가능." },
{ "id": "other_ai", "label": "기타 AI 검색", "sources": ["you.com", "search.brave.com", "phind.com", "deepseek.com", "chat.deepseek.com", "grok.com", "x.ai", "meta.ai", "kagi.com", "liner.com", "getliner.com", "wrtn.ai"], "utm_sources": ["deepseek", "grok", "you.com", "wrtn"],
"measurable": "partial", "note": "리퍼러가 있을 때만 잡힌다." }
],
"not_measurable_by_ga4": [
"AI 답변에 병원이 언급됐지만 사용자가 링크를 누르지 않은 경우(노출만 된 경우). 이건 답변엔진 실측(질문 뱅크)으로 잰다.",
"AI 크롤러(GPTBot·PerplexityBot·ClaudeBot·Google-Extended)의 방문. 사람 방문이 아니라 GA4에 잡히지 않는다. 서버 로그로 잰다.",
"리퍼러를 비운 채 넘어온 AI 유입. GA4에는 direct 로 들어간다.",
"전화 통화·오프라인 방문으로 이어진 전환. 사이트의 전화 탭·예약 클릭까지만 잰다."
],
"conversion_events": [
{ "name": "click_tel", "label": "전화 탭", "match": "a[href^='tel:']" },
{ "name": "click_reservation", "label": "상담 예약 클릭", "match": "a.cta, a[data-ga='reservation']" },
{ "name": "click_clinic_site", "label": "병원 홈페이지 이동", "match": "a[data-ga='clinic-site']" },
{ "name": "click_map", "label": "지도·길찾기 클릭", "match": "a[data-ga='map']" }
]
}

View File

@ -0,0 +1,57 @@
# GA4 AI 검색 유입 연동 안내 v0.1
작성 2026-09-08 · 스크립트 `scripts/ga4_ai_traffic.py` · 분류 기준 `data/ai_channels.json` · 태그 `supporters/src/layouts/Base.astro`
## 1. 구조
| 대상 | 누가 계측하는가 | 필요한 것 |
|---|---|---|
| 서포터즈 사이트 | INFINITH가 직접. `site.json``ga4MeasurementId`(G-XXXX)를 넣으면 빌드 시 태그가 들어간다 | INFINITH GA4 속성 1개(병원별 데이터 스트림) |
| 병원 공식 사이트 | 병원이 이미 쓰는 GA4 속성에 INFINITH 서비스 계정을 뷰어로 추가 | 병원 GA4 관리자 권한자 1명, 5분 |
코드 설치는 없다. 병원 사이트에 GA4가 없으면 태그 설치가 먼저이고 그건 병원 웹 담당자 일이다.
## 2. 연동 절차 (병원 사이트)
1. INFINITH: Google Cloud 프로젝트에서 Google Analytics Data API 사용 설정, 서비스 계정 생성, 키(JSON) 발급. `.env``GA4_SERVICE_ACCOUNT_JSON=<경로>`. 계정은 haewon이 지정한다(이전 GCP 재인증 이슈 있음).
2. 병원: GA4 관리 → 속성 액세스 관리 → 사용자 추가 → 서비스 계정 이메일, 역할 **뷰어**.
3. 병원: 속성 ID(숫자 9자리) 전달.
4. INFINITH: `python3 scripts/ga4_ai_traffic.py --property <ID> --days 90 --clinic <id> --out docs/reports/<id>/05_ai_traffic`.
5. 월간 실행 → 리포트 카드와 병원 확인 패널에 붙인다(다음 단계).
API 없이 받을 때: 병원이 GA4 탐색에서 날짜·세션 소스·세션 매체·방문 페이지·세션수·주요 이벤트를 CSV로 내보내면 `--from-csv`로 같은 리포트를 만든다.
## 3. AI 채널 분류 규칙
`data/ai_channels.json` 한 곳에서 관리한다. 세션 출처 호스트(경계 일치)와 `utm_source`로 묶는다.
| 채널 | 측정 | 이유 |
|---|---|---|
| ChatGPT | 가능 | 링크에 utm_source=chatgpt.com |
| Gemini, Claude | 가능 | 고유 리퍼러 |
| Perplexity, Copilot, 기타 AI | 일부만 | 리퍼러를 자주 비움 → direct로 섞임 |
| Google AI Overview · AI Mode | 불가 | google / organic과 구분되지 않음 |
| 네이버 AI 브리핑 | 불가 | search.naver.com과 구분되지 않음 |
리포트마다 "측정 가능 범위" 절이 자동으로 들어간다. 불가 채널에는 숫자를 만들지 않는다.
## 4. 키 이벤트
서포터즈 사이트 태그가 보내는 이벤트. GA4에서 **주요 이벤트로 표시**해야 전환으로 집계된다.
| 이벤트 | 조건 |
|---|---|
| click_tel | `tel:` 링크 |
| click_reservation | 헤더 상담 예약 버튼(`a.cta`) 또는 `data-ga="reservation"` |
| click_clinic_site | `data-ga="clinic-site"` (병원 홈페이지 이동) |
| click_map | `data-ga="map"` (지도·길찾기) |
## 5. 개인정보
GA4 태그는 쿠키를 쓴다. `anonymize_ip`는 켜 두었다. 쿠키 고지 배너를 넣을지, 쿠키 없는 대안(서버 로그, Vercel Web Analytics)으로 갈지는 haewon 결정 대기. 결정 전까지 서포터즈 미리보기(noindex)에는 측정 ID를 넣지 않는다.
## 6. 남은 일
- 리포트 카드(`build_discovery_report.ts`)에 AI 유입 요약 카드와 실측 언급률 동일 시간축 그래프.
- 병원 확인 패널(`/supporters/:id`)에 "GA4 뷰어 추가" 안내와 속성 ID 입력란.
- 월간 실행을 워커 또는 cron에.

View File

@ -0,0 +1,155 @@
<!doctype html>
<html lang="ko"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>원진성형외과 AI Answer Readiness Report v2.0</title>
<link rel="preconnect" href="https://fonts.googleapis.com"><link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.min.css">
<style>
:root{--ink:#0A1128;--navy:#021341;--violet:#4F1DA1;--accent:#6C5CE7;--light:#F7F8FC;--slate:#4A5268;--muted:#7A8399;--line:#E3E7F2}
*{box-sizing:border-box}body{margin:0;font-family:Pretendard,-apple-system,sans-serif;color:var(--ink);background:#fff;word-break:keep-all;line-height:1.62;font-size:14px}
.serif{font-family:'Playfair Display',serif}
.page{padding:48px 56px;max-width:1080px;margin:0 auto}
.dark{background:var(--ink);color:#fff}.dark .sub,.dark .note{color:#B9C0D4}.light{background:var(--light)}
.eyebrow{font-size:12px;font-weight:700;letter-spacing:.24em;color:var(--accent);text-transform:uppercase}.dark .eyebrow{color:#C4B5FD}
h1{font-size:44px;line-height:1.1;margin:10px 0 6px;letter-spacing:-.02em}h2{font-size:30px;margin:8px 0 12px;line-height:1.2}h3{font-size:17px;margin:0 0 6px}
.accent{background:linear-gradient(90deg,#C4B5FD,#93C5FD);-webkit-background-clip:text;background-clip:text;color:transparent}
.lede{font-size:16px;color:var(--slate);max-width:760px}.dark .lede{color:#D6DBEA}
.sub{font-size:12.5px;color:var(--muted)}.note{font-size:12.5px;color:var(--muted);margin-top:8px}
.badge{display:inline-block;border:1px solid;border-radius:999px;padding:3px 12px;font-size:13px;font-weight:700}
.pill{display:inline-block;border-radius:999px;padding:2px 9px;font-size:11.5px;font-weight:700}.pill.gray{background:#EEF0F6;color:#6B7280}
.grid{display:grid;gap:16px}.g2{grid-template-columns:1fr 1fr}.g3{grid-template-columns:repeat(3,1fr)}
.card{background:#fff;border:1px solid var(--line);border-radius:18px;padding:20px 22px;box-shadow:3px 4px 12px rgba(0,0,0,.05)}.dark .card{color:var(--ink)}
.rings{display:flex;gap:28px;align-items:flex-start;flex-wrap:wrap}.ring{display:flex;flex-direction:column;align-items:center;gap:8px}
.tbl{width:100%;border-collapse:collapse;font-size:13px;background:#fff;border-radius:12px;overflow:hidden;border:1px solid var(--line)}.tbl th{background:#F1F3FA;text-align:left;padding:9px 11px;font-size:12px;color:var(--slate);border-bottom:1px solid var(--line)}.tbl td{padding:9px 11px;border-bottom:1px solid #EEF0F6;vertical-align:top}.tbl tr:last-child td{border-bottom:0}.mono{font-family:ui-monospace,Menlo,monospace;font-size:12px;color:var(--accent);font-weight:700}
.lv{display:inline-block;min-width:26px;text-align:center;border-radius:6px;padding:1px 6px;font-weight:700;font-size:12px}.lv3{background:#E9F7EF;color:#2E7D4F}.lv2{background:#EEF3FF;color:#2F4FBF}.lv1{background:#FFF4E5;color:#B26A00}.lv0{background:#FDECEF;color:#B84C5F}.lvu{background:#EEF0F6;color:#6B7280}
.find{border-left:5px solid;padding-left:16px}.find .tag{font-size:11.5px;font-weight:700;letter-spacing:.08em}
.act .pri{font-family:'Playfair Display',serif;font-size:22px;font-weight:700;color:var(--accent)}.act .eff{float:right}
.kpi{display:grid;grid-template-columns:repeat(4,1fr);gap:14px}.kpi .k{background:rgba(255,255,255,.08);border:1px solid rgba(255,255,255,.14);border-radius:14px;padding:14px 16px}.kpi b{font-family:'Playfair Display',serif;font-size:30px;color:#AF90FF;display:block;line-height:1}.kpi span{font-size:12.5px;color:#C7CDE0}
a{color:var(--accent)}ul{margin:6px 0 0 18px;padding:0}li{margin:3px 0}
footer{padding:28px 56px;font-size:12px;color:var(--muted);border-top:1px solid var(--line)}
@media print{@page{size:A4;margin:12mm}body{font-size:12.5px}.page{padding:22px 10px;max-width:none}.pb{page-break-before:always}h1{font-size:34px}h2{font-size:24px}.dark{-webkit-print-color-adjust:exact;print-color-adjust:exact}.card,.tbl,.lv,.badge,.pill,.kpi .k{-webkit-print-color-adjust:exact;print-color-adjust:exact}tr,.card{page-break-inside:avoid}}
</style></head><body>
<section class="dark"><div class="page" style="padding-top:64px;padding-bottom:56px">
<div class="eyebrow">INFINITH AI Discovery · AEO / GEO Audit v2.0</div>
<h1 class="serif"><span class="accent">AI Answer Readiness</span></h1>
<div style="font-size:26px;font-weight:700;margin:4px 0 2px">원진성형외과</div>
<div class="sub" style="color:#C4B5FD">성형외과 (강남역) · <a href="https://www.k-wonjin.co.kr" style="color:#C4B5FD">https://www.k-wonjin.co.kr</a></div>
<p class="lede" style="margin-top:18px">AI 답변엔진(ChatGPT·Perplexity·Gemini·네이버 AI 브리핑)이 이 병원을 얼마나 읽고, 믿고, 인용할 수 있는지를 실측 신호로 채점한 리포트입니다. 답변 준비도(AEO)와 출처 준비도(GEO)를 각각 100점으로 보고, 확인하지 못한 항목은 점수에서 뺍니다.</p>
<div style="display:flex;gap:32px;align-items:center;flex-wrap:wrap;margin-top:26px">
<div class="card" style="display:flex;gap:26px;padding:22px 30px"><div class="ring"><svg viewBox="0 0 120 120" width="132" height="132"><circle cx="60" cy="60" r="52" fill="none" stroke="#E8ECF7" stroke-width="10"/><circle cx="60" cy="60" r="52" fill="none" stroke="#6C5CE7" stroke-width="10" stroke-linecap="round" stroke-dasharray="326.7" stroke-dashoffset="277.7" transform="rotate(-90 60 60)"/><text x="60" y="66" text-anchor="middle" font-family="Playfair Display, serif" font-weight="700" font-size="30" fill="#0A1128">15</text><text x="60" y="84" text-anchor="middle" font-size="10" fill="#7A8399">/ 100</text></svg><span class="badge" style="background:#FDECEF;color:#B84C5F;border-color:#F7C3CD">AEO 등급 D</span></div><div class="ring"><svg viewBox="0 0 120 120" width="132" height="132"><circle cx="60" cy="60" r="52" fill="none" stroke="#E8ECF7" stroke-width="10"/><circle cx="60" cy="60" r="52" fill="none" stroke="#6C5CE7" stroke-width="10" stroke-linecap="round" stroke-dasharray="326.7" stroke-dashoffset="179.7" transform="rotate(-90 60 60)"/><text x="60" y="66" text-anchor="middle" font-family="Playfair Display, serif" font-weight="700" font-size="30" fill="#0A1128">45</text><text x="60" y="84" text-anchor="middle" font-size="10" fill="#7A8399">/ 100</text></svg><span class="badge" style="background:#FFF4E5;color:#B26A00;border-color:#FFD9A8">GEO 등급 C</span></div></div>
<div class="kpi" style="flex:1;min-width:420px">
<div class="k"><b>25</b><span>실측 항목 / 36</span></div>
<div class="k"><b>49</b><span>v1.0 종합 (C)</span></div>
<div class="k"><b>5</b><span>핵심 발견</span></div>
<div class="k"><b>6</b><span>실행 항목</span></div>
</div>
</div>
<p class="note" style="margin-top:22px">실측일 2026-09-08 · 리포트 작성 2026-09-08 · 미검증 11개 항목은 분모에서 제외했습니다. 조건은 마지막 장에 적었습니다.</p>
</div></section>
<section class="light pb"><div class="page">
<div class="eyebrow">Findings</div><h2>AI가 지금 원진성형외과를 어떻게 읽는지 정리했습니다</h2>
<div class="grid g2" style="margin-top:14px">
<div class="card find" style="border-left-color:#9B8AD4"><div class="tag" style="color:#9B8AD4">강점</div><h3>크롤러 접근은 열려 있습니다. 서버 렌더링이라 AI가 본문을 읽습니다</h3><p style="margin:0;color:var(--slate)">robots.txt가 AI 검색봇 12종을 모두 허용하고, IIS가 본문을 서버에서 렌더링해 JS 없이도 14,079자를 읽을 수 있습니다. TTFB 0.09초, 초기 HTML 153KB로 가볍습니다. 다만 www 유무에 따라 최종 URL이 갈라지고(정본 미수렴), sitemap이 robots.txt에 선언돼 있지 않습니다.</p></div><div class="card find" style="border-left-color:#D4889A"><div class="tag" style="color:#D4889A">핵심 과제</div><h3>연결은 됐지만 병원 자체가 스키마로 정의돼 있지 않습니다</h3><p style="margin:0;color:var(--slate)">Organization.sameAs 10개로 SNS·지도·버티컬을 잘 묶었고 주소 표기도 한 가지입니다. 그러나 MedicalClinic·Physician 스키마가 없어 주소·전화·진료시간·의료진 17명이 구조화돼 있지 않고, 전화번호는 6종이 섞여 있습니다. 영문 사이트(wonjinbeauty.com)는 hreflang으로 연결되지 않았습니다.</p></div><div class="card find" style="border-left-color:#D4889A"><div class="tag" style="color:#D4889A">핵심 과제</div><h3>인용할 문장이 부족합니다. H1도 표도 정의문도 없습니다</h3><p style="margin:0;color:var(--slate)">메인·시술 페이지에 H1이 없고 HTML 표가 없으며 병원 정의문도 없습니다. 눈매교정(CFS)·트임 페이지에는 수술 시간·마취·실밥·일상 복귀 수치가 텍스트로 있어 인용 구조의 출발점이 될 수 있지만, 쌍꺼풀·눈매교정 나머지 페이지는 수치가 없습니다. 최신 갱신 신호가 2025-02-18(sitemap)로 18개월 전입니다.</p></div><div class="card find" style="border-left-color:#D4A872"><div class="tag" style="color:#D4A872">보완</div><h3>유튜브는 강합니다. 736편·누적 3,113만 회</h3><p style="margin:0;color:var(--slate)">설명형 롱폼(눈성형 관리법·중년 눈성형 비교·지방흡입 QnA 등)이 있어 글의 근거 영상으로 쓸 수 있습니다. 다만 상위 조회 영상의 다수가 브랜드 필름·연예인·전후 형식이라 인용용 정보형 영상은 10편 안팎입니다. 이 채널이 원진의 가장 큰 오픈웹 자산입니다.</p></div><div class="card find" style="border-left-color:#D4A872"><div class="tag" style="color:#D4A872">보완</div><h3>이미지 alt 62% 누락, 의료진 경력은 텍스트지만 구조화는 안 됨</h3><p style="margin:0;color:var(--slate)">메인 이미지 45개 중 28개가 alt 없이 실려 AI가 이미지 내용을 읽지 못합니다. 의료진 17명의 경력은 텍스트로 있어(원장당 12~23줄) Physician 스키마만 붙이면 바로 엔티티가 됩니다. 강남언니·GBP·네이버 플레이스는 이번 자동 채점 범위 밖이라 수동 실측이 남아 있습니다.</p></div>
</div>
</div></section>
<section class="pb"><div class="page">
<div class="eyebrow">AEO · Answer Readiness</div><h2>답변 준비도 8개 항목</h2>
<p class="lede">질문을 고르고, 답을 먼저 쓰고, 한 단락으로 완결하고, 결정에 필요한 정보와 다음 행동까지 닿게 하는가.</p>
<table class="tbl"><thead><tr><th style="width:52px">항목</th><th>무엇을 보는가</th><th style="width:70px">점수</th><th style="width:72px">근거</th><th>파생된 실측 항목</th></tr></thead><tbody>
<tr><td class="mono">AEO1</td><td><b>질문 수요 충족</b><div class="sub">사람들이 실제로 묻는 질문에 답하는 페이지가 있는가</div></td><td><b>0</b><span class="sub"> / 100</span></td><td>실측 파생</td><td class="sub">C2 L0</td></tr><tr><td class="mono">AEO2</td><td><b>답변의 직접성</b><div class="sub">페이지 첫 단락에 답이 먼저 나오는가</div></td><td><b>0</b><span class="sub"> / 100</span></td><td>실측 파생</td><td class="sub">C1 L0</td></tr><tr><td class="mono">AEO3</td><td><b>답변 단위의 자기완결성</b><div class="sub">한 단락만 떼어내도 누가·어디서·무엇을이 들어 있는가</div></td><td><span class="pill gray">미검증</span></td><td>미검증</td><td class="sub">C8 미검증</td></tr><tr><td class="mono">AEO4</td><td><b>의사결정에 필요한 정보</b><div class="sub">가격대·시간·회복·조건처럼 결정에 쓰는 정보가 텍스트로 있는가</div></td><td><b>40</b><span class="sub"> / 100</span></td><td>실측 파생</td><td class="sub">C4 L2 · C5 L0</td></tr><tr><td class="mono">AEO5</td><td><b>정보 구조와 가독성</b><div class="sub">제목이 주제를 서술하고 계층이 논리적인가</div></td><td><b>0</b><span class="sub"> / 100</span></td><td>실측 파생</td><td class="sub">C3 L0</td></tr><tr><td class="mono">AEO6</td><td><b>다음 행동의 명확성</b><div class="sub">답을 읽은 사람이 무엇을 하면 되는지(예약·전화·방문) 바로 보이는가</div></td><td><span class="pill gray">미검증</span></td><td>미검증</td><td class="sub"></td></tr><tr><td class="mono">AEO7</td><td><b>핵심 답변의 텍스트 추출</b><div class="sub">답이 이미지가 아니라 텍스트로 존재하고 서버가 렌더하는가</div></td><td><b>60</b><span class="sub"> / 100</span></td><td>실측 파생</td><td class="sub">A3 L3 · F1 L0</td></tr><tr><td class="mono">AEO8</td><td><b>사이트 내 답변 발견</b><div class="sub">크롤러가 답이 있는 페이지를 찾아갈 수 있는가</div></td><td><b>47</b><span class="sub"> / 100</span></td><td>실측 파생</td><td class="sub">A5 L1 · B6 L2</td></tr>
</tbody></table>
<div class="eyebrow" style="margin-top:34px">GEO · Source Readiness</div><h2>출처 준비도 8개 항목</h2>
<p class="lede">주장에 근거가 붙어 있고, 누가 책임지는지 보이고, 병원 사실이 표면마다 같고, 갱신·정정 기록이 있으며, 기계가 인용 식별자를 읽을 수 있는가.</p>
<table class="tbl"><thead><tr><th style="width:52px">항목</th><th>무엇을 보는가</th><th style="width:70px">점수</th><th style="width:72px">근거</th><th>파생된 실측 항목</th></tr></thead><tbody>
<tr><td class="mono">GEO1</td><td><b>주장과 근거의 일치</b><div class="sub">수치·수상·효과 주장마다 근거(원문·출처·조건)가 붙어 있는가</div></td><td><span class="pill gray">미검증</span></td><td>미검증</td><td class="sub"></td></tr><tr><td class="mono">GEO2</td><td><b>출처의 적합성과 추적성</b><div class="sub">인용된 URL이 정본으로 수렴하고 시간이 지나도 같은 곳을 가리키는가</div></td><td><b>33</b><span class="sub"> / 100</span></td><td>실측 파생</td><td class="sub">A4 L1</td></tr><tr><td class="mono">GEO3</td><td><b>전문성과 책임 주체</b><div class="sub">누가 썼고 누가 의학적으로 책임지는지 텍스트와 스키마로 보이는가</div></td><td><b>40</b><span class="sub"> / 100</span></td><td>실측 파생</td><td class="sub">C7 L2 · B3 L0</td></tr><tr><td class="mono">GEO4</td><td><b>병원 엔티티와 사실의 정합성</b><div class="sub">이 병원이 누구인지 사이트 안과 밖(강남언니·GBP·플레이스)에서 같은 사실로 확정되는가</div></td><td><b>63</b><span class="sub"> / 100</span></td><td>실측 파생</td><td class="sub">B2 L1 · B4 L3 · B5 L3 · D1 미검증 · D2 미검증 · D4 미검증 · D5 미검증 · D6 L0</td></tr><tr><td class="mono">GEO5</td><td><b>사실의 최신성과 정정</b><div class="sub">언제 확인한 사실인지 적혀 있고, 틀린 것을 고친 기록이 있는가</div></td><td><b>33</b><span class="sub"> / 100</span></td><td>실측 파생</td><td class="sub">C6 L1</td></tr><tr><td class="mono">GEO6</td><td><b>원천 대비 추가 가치</b><div class="sub">다른 곳에 있는 문장을 옮긴 것이 아니라 여기서만 얻는 정보(체크리스트·조건 표·실측)가 있는가</div></td><td><span class="pill gray">미검증</span></td><td>미검증</td><td class="sub"></td></tr><tr><td class="mono">GEO7</td><td><b>발행 관계와 투명성</b><div class="sub">누가 운영하고 누구의 지원을 받는지, 광고와 정보가 구분되는지 밝히는가</div></td><td><span class="pill gray">미검증</span></td><td>미검증</td><td class="sub"></td></tr><tr><td class="mono">GEO8</td><td><b>인용 식별자와 메타데이터 정합성</b><div class="sub">기계가 읽는 식별자(JSON-LD·메타)가 유효하고 화면 내용과 같은가</div></td><td><b>67</b><span class="sub"> / 100</span></td><td>실측 파생</td><td class="sub">B1 L2</td></tr>
</tbody></table>
</div></section>
<section class="pb dark"><div class="page">
<div class="eyebrow">Category Detail</div><h2>카테고리별 실측 36개 항목</h2>
<div class="grid g3" style="margin-top:12px">
<div class="card"><div class="sub">A · Access &amp; Crawlability</div><h3 style="margin-top:2px">AI 크롤러 접근성</h3><div><span class="serif" style="font-size:28px;font-weight:700;color:var(--accent)">80</span><span class="sub"> / 100 · 실측 6/7</span></div></div><div class="card"><div class="sub">B · Entity &amp; Structured Data</div><h3 style="margin-top:2px">엔티티 식별·구조화</h3><div><span class="serif" style="font-size:28px;font-weight:700;color:var(--accent)">50</span><span class="sub"> / 100 · 실측 7/7</span></div></div><div class="card"><div class="sub">C · Answer-Ready Content</div><h3 style="margin-top:2px">답변 인용 가능 콘텐츠</h3><div><span class="serif" style="font-size:28px;font-weight:700;color:var(--accent)">25</span><span class="sub"> / 100 · 실측 7/8</span></div></div><div class="card"><div class="sub">D · Off-site Surfaces</div><h3 style="margin-top:2px">AI 인용 표면 확보</h3><div><span class="serif" style="font-size:28px;font-weight:700;color:var(--accent)">53</span><span class="sub"> / 100 · 실측 2/6</span></div></div><div class="card"><div class="sub">E · Measurement</div><h3 style="margin-top:2px">AI 가시성 측정 인프라</h3><div><span class="serif" style="font-size:28px;font-weight:700;color:var(--accent)">0</span><span class="sub"> / 100 · 실측 0/4</span></div></div><div class="card"><div class="sub">F · Technical Hygiene</div><h3 style="margin-top:2px">기술 위생</h3><div><span class="serif" style="font-size:28px;font-weight:700;color:var(--accent)">42</span><span class="sub"> / 100 · 실측 3/4</span></div></div>
</div>
</div></section>
<section><div class="page">
<h3 style="margin-top:22px;font-size:18px">A · AI 크롤러 접근성 <span class="sub">AI 검색엔진이 사이트를 읽을 수 있는가. 크롤러 접근이 막히면 나머지 항목은 전부 무효다.</span></h3>
<table class="tbl"><thead><tr><th style="width:44px">ID</th><th style="width:22%">항목</th><th style="width:88px">판정</th><th>실측 근거</th></tr></thead><tbody>
<tr><td class="mono">A1</td><td><b>검색용 AI 봇 허용</b><div class="sub">배점 5</div></td><td><span class="lv lv3">L3</span><div class="sub">검색봇 전부 허용 + 200 응답</div></td><td class="sub" style="color:var(--slate)">12개 AI 봇 전부 허용. OAI-SearchBot·PerplexityBot·GPTBot UA로 GET → 전부 200</td></tr><tr><td class="mono">A2</td><td><b>CDN/WAF AI 차단 없음</b><div class="sub">배점 3</div></td><td><span class="lv lv3">L3</span><div class="sub">CDN 차단 없음 확인</div></td><td class="sub" style="color:var(--slate)">Server: Microsoft-IIS/10.0 직접 응답, Cloudflare 헤더 없음, AI UA 챌린지 없음</td></tr><tr><td class="mono">A3</td><td><b>서버 렌더링 본문</b><div class="sub">배점 3</div></td><td><span class="lv lv3">L3</span><div class="sub">80% 이상 (SSR/정적)</div></td><td class="sub" style="color:var(--slate)">서버 렌더링. JS 미실행 HTML에서 본문 텍스트 14,079자 추출</td></tr><tr><td class="mono">A4</td><td><b>HTTPS·리다이렉트 정합</b><div class="sub">배점 2</div></td><td><span class="lv lv1">L1</span><div class="sub">변형 간 200 중복</div></td><td class="sub" style="color:var(--slate)">변형 간 최종 URL 불일치(중복 콘텐츠 위험): ['https://k-wonjin.co.kr', 'https://www.k-wonjin.co.kr']</td></tr><tr><td class="mono">A5</td><td><b>sitemap.xml 제공</b><div class="sub">배점 3</div></td><td><span class="lv lv1">L1</span><div class="sub">있으나 robots 미선언</div></td><td class="sub" style="color:var(--slate)">/sitemap.xml 200이나 robots.txt에 Sitemap 미선언. URL 132개</td></tr><tr><td class="mono">A6</td><td><b>Bing 색인 (ChatGPT 검색 경로)</b><div class="sub">배점 3</div></td><td><span class="lv lvu">미검증</span><div class="sub">미검증</div></td><td class="sub" style="color:var(--slate)">[manual] Bing Webmaster Tools 등록 여부는 오너 계정 접근 필요 (또는 site: 검색 수동 확인)</td></tr><tr><td class="mono">A7</td><td><b>응답 속도·크롤 예산</b><div class="sub">배점 1</div></td><td><span class="lv lv3">L3</span><div class="sub">TTFB &lt; 0.5s, HTML &lt; 300KB</div></td><td class="sub" style="color:var(--slate)">TTFB 0.094s / 초기 HTML 153KB. 시술 페이지 145KB. 모두 기준 이내</td></tr>
</tbody></table><h3 style="margin-top:22px;font-size:18px">B · 엔티티 식별·구조화 <span class="sub">AI가 &quot;이 병원이 누구인지&quot;를 기계적으로 확정할 수 있는가. NAP 일관성과 스키마 위생.</span></h3>
<table class="tbl"><thead><tr><th style="width:44px">ID</th><th style="width:22%">항목</th><th style="width:88px">판정</th><th>실측 근거</th></tr></thead><tbody>
<tr><td class="mono">B1</td><td><b>JSON-LD 문법 유효성</b><div class="sub">배점 3</div></td><td><span class="lv lv2">L2</span><div class="sub">전부 유효</div></td><td class="sub" style="color:var(--slate)">JSON-LD 4블록 전부 유효 (@graph 미사용)</td></tr><tr><td class="mono">B2</td><td><b>업종 엔티티 스키마 (MedicalClinic / LegalService / LocalBusiness)</b><div class="sub">배점 5</div></td><td><span class="lv lv1">L1</span><div class="sub">Organization만</div></td><td class="sub" style="color:var(--slate)">ItemList + Organization만 존재. MedicalClinic/LocalBusiness 없음 → 주소·전화·진료시간이 구조화되어 있지 않음</td></tr><tr><td class="mono">B3</td><td><b>전문가 스키마 (Physician / Attorney)</b><div class="sub">배점 3</div></td><td><span class="lv lv0">L0</span><div class="sub">없음</div></td><td class="sub" style="color:var(--slate)">Physician/Attorney 스키마 없음. 전문가 정보는 HTML 텍스트·이미지로만 존재</td></tr><tr><td class="mono">B4</td><td><b>NAP 사이트 내 일관성</b><div class="sub">배점 3</div></td><td><span class="lv lv3">L3</span><div class="sub">단일 표기</div></td><td class="sub" style="color:var(--slate)">주소 단일 표기: &quot;서울시 서초구 강남대로 419 파고다타워 12&quot;. 전화 6종: 02-3477-3300, 023480-3573, 02392-0330</td></tr><tr><td class="mono">B5</td><td><b>sameAs 연결</b><div class="sub">배점 2</div></td><td><span class="lv lv3">L3</span><div class="sub">5개 이상 (SNS + 버티컬 + 지도)</div></td><td class="sub" style="color:var(--slate)">sameAs 10개 (SNS·버티컬·지도 연결)</td></tr><tr><td class="mono">B6</td><td><b>canonical·중복 제거</b><div class="sub">배점 2</div></td><td><span class="lv lv2">L2</span><div class="sub">있음</div></td><td class="sub" style="color:var(--slate)">rel=canonical self-referencing 존재</td></tr><tr><td class="mono">B7</td><td><b>다국어 hreflang</b><div class="sub">배점 2</div></td><td><span class="lv lv0">L0</span><div class="sub">다국어 페이지 있으나 미연결</div></td><td class="sub" style="color:var(--slate)">다국어 페이지 흔적(https://www.wonjinbeauty.com/en/) 있으나 hreflang 없음. 미연결</td></tr>
</tbody></table><h3 style="margin-top:22px;font-size:18px">C · 답변 인용 가능 콘텐츠 <span class="sub">패시지 단위로 잘라내도 자기완결적인 사실 덩어리가 있는가. 정의문·FAQ·수치·표·의료진 근거.</span></h3>
<table class="tbl"><thead><tr><th style="width:44px">ID</th><th style="width:22%">항목</th><th style="width:88px">판정</th><th>실측 근거</th></tr></thead><tbody>
<tr><td class="mono">C1</td><td><b>정의문형 첫 문단</b><div class="sub">배점 4</div></td><td><span class="lv lv0">L0</span><div class="sub">없음 (슬로건·이미지만)</div></td><td class="sub" style="color:var(--slate)">홈 본문 1,999자에 &quot;원진성형외과는 …에 있는 … 병원입니다&quot; 형태의 정의문 없음. 소개 문구(25년 헤리티지·강남역)는 메인 비주얼 이미지 alt 에만 있고, 소개 페이지(/hospitalinfo/about) 첫 단락도 슬로건형</td></tr><tr><td class="mono">C2</td><td><b>FAQ 섹션</b><div class="sub">배점 4</div></td><td><span class="lv lv0">L0</span><div class="sub">없음</div></td><td class="sub" style="color:var(--slate)">FAQ·자주 묻는 질문·Q&amp;A 섹션 탐지 안 됨 (메인·시술 페이지)</td></tr><tr><td class="mono">C3</td><td><b>제목 계층 의미성</b><div class="sub">배점 3</div></td><td><span class="lv lv0">L0</span><div class="sub">H1 없음 또는 5개 이상</div></td><td class="sub" style="color:var(--slate)">메인 H1 0개: 없음. 시술 페이지 H1 없음</td></tr><tr><td class="mono">C4</td><td><b>구체 수치 (가격·시간·회복)</b><div class="sub">배점 4</div></td><td><span class="lv lv2">L2</span><div class="sub">시술별 일부 수치</div></td><td class="sub" style="color:var(--slate)">눈매교정(CFS) 페이지에 수술 시간 2시간·단기 수면 또는 부분 마취·실밥 제거 7일·일상생활 3일 텍스트 존재. 트임(오픈아이) 40분·5~7일. 쌍꺼풀·눈매교정(글램아이·더블유착) 페이지는 수치 없음. 가격 표기 0회(비급여 안내 페이지는 별도)</td></tr><tr><td class="mono">C5</td><td><b>표·비교 구조</b><div class="sub">배점 2</div></td><td><span class="lv lv0">L0</span><div class="sub">없음</div></td><td class="sub" style="color:var(--slate)">HTML &lt;table&gt; 없음. 비교 정보는 이미지로 추정 (이미지 표 여부는 수동 확인)</td></tr><tr><td class="mono">C6</td><td><b>신선도 신호</b><div class="sub">배점 3</div></td><td><span class="lv lv1">L1</span><div class="sub">1년 이상</div></td><td class="sub" style="color:var(--slate)">최신 갱신 신호 2025-02-18 (sitemap lastmod 2025-02-18). 18개월 전. dateModified 마크업 없음</td></tr><tr><td class="mono">C7</td><td><b>전문가 E-E-A-T (의료진 / 변호사)</b><div class="sub">배점 3</div></td><td><span class="lv lv2">L2</span><div class="sub">자격·경력 텍스트</div></td><td class="sub" style="color:var(--slate)">의료진 상세 페이지 17명의 직함·전문의 여부·경력(원장당 12~23줄)이 HTML 텍스트로 존재. 학술 활동 페이지 3,760자. 단 Physician 스키마·저자 표기는 없어 구조화되지 않음</td></tr><tr><td class="mono">C8</td><td><b>패시지 자기완결성</b><div class="sub">배점 2</div></td><td><span class="lv lvu">미검증</span><div class="sub">미검증</div></td><td class="sub" style="color:var(--slate)">[manual] 핵심 페이지 단락 샘플 10개의 상호·시술명 포함 비율 수동 채점 필요</td></tr>
</tbody></table><h3 style="margin-top:22px;font-size:18px">D · AI 인용 표면 확보 <span class="sub">실제로 AI가 인용하는 외부 표면(강남언니·GBP·오픈웹)에 존재하는가. 네이버 자산은 글로벌 AI에 기여도 0.</span></h3>
<table class="tbl"><thead><tr><th style="width:44px">ID</th><th style="width:22%">항목</th><th style="width:88px">판정</th><th>실측 근거</th></tr></thead><tbody>
<tr><td class="mono">D1</td><td><b>업종 버티컬 프로필 (병의원: 강남언니 / 법률: 로톡)</b><div class="sub">배점 6</div></td><td><span class="lv lvu">미검증</span><div class="sub">미검증</div></td><td class="sub" style="color:var(--slate)">강남언니 프로필은 공개 페이지 수동 실측 필요 (후기 수·의료진·이벤트·갱신일). 홈페이지 sameAs 에 버티컬 링크 포함 여부는 확인됨(10개 중)</td></tr><tr><td class="mono">D2</td><td><b>Google 비즈니스 프로필</b><div class="sub">배점 4</div></td><td><span class="lv lvu">미검증</span><div class="sub">미검증</div></td><td class="sub" style="color:var(--slate)">[manual] Google Maps 검색으로 GBP 소유·리뷰·NAP 수동 실측 필요</td></tr><tr><td class="mono">D3</td><td><b>오픈웹 언급 (티스토리·유튜브·언론)</b><div class="sub">배점 4</div></td><td><span class="lv lv2">L2</span><div class="sub">유튜브 + 언론/티스토리</div></td><td class="sub" style="color:var(--slate)">자체 YouTube 채널 @wjwonjin 영상 736편·구독 1.5만·누적 조회 3,113만(2026-09-08 API 집계). 인스타그램 3계정·페이스북·네이버 블로그(popokpop) 존재. 채널 sameAs 10개로 홈페이지와 연결됨. 단 홈페이지 내 외부 링크는 JS 렌더라 HTML에서 미노출</td></tr><tr><td class="mono">D4</td><td><b>표면 간 NAP 완전 일치</b><div class="sub">배점 3</div></td><td><span class="lv lvu">미검증</span><div class="sub">미검증</div></td><td class="sub" style="color:var(--slate)">[manual] 홈페이지·버티컬·GBP·플레이스 NAP 대조표 수동 작성 필요</td></tr><tr><td class="mono">D5</td><td><b>네이버 플레이스 (AI 브리핑 전용)</b><div class="sub">배점 2</div></td><td><span class="lv lvu">미검증</span><div class="sub">미검증</div></td><td class="sub" style="color:var(--slate)">[semi·사람 판정 필요] 사이트에 네이버 플레이스 링크 없음. 플레이스 존재·홈페이지 역연결·리뷰 수 수동 확인</td></tr><tr><td class="mono">D6</td><td><b>위키데이터/위키백과 엔티티</b><div class="sub">배점 1</div></td><td><span class="lv lv0">L0</span><div class="sub">없음</div></td><td class="sub" style="color:var(--slate)">Wikidata에서 &quot;원진성형외과&quot; 검색 결과 없음</td></tr>
</tbody></table><h3 style="margin-top:22px;font-size:18px">E · AI 가시성 측정 인프라 <span class="sub">개선을 증명할 수 있는가. 소유 신호(크롤러 로그·GA4 AI 유입) 계측 여부.</span></h3>
<table class="tbl"><thead><tr><th style="width:44px">ID</th><th style="width:22%">항목</th><th style="width:88px">판정</th><th>실측 근거</th></tr></thead><tbody>
<tr><td class="mono">E1</td><td><b>AI 크롤러 로그 (IP 검증)</b><div class="sub">배점 3</div></td><td><span class="lv lvu">미검증</span><div class="sub">미검증</div></td><td class="sub" style="color:var(--slate)">[manual] 서버 로그 접근 필요 (AI 크롤러 히트 + IP 검증)</td></tr><tr><td class="mono">E2</td><td><b>GA4 AI 유입 채널</b><div class="sub">배점 3</div></td><td><span class="lv lvu">미검증</span><div class="sub">미검증</div></td><td class="sub" style="color:var(--slate)">[manual] GA4 접근 필요 (AI 유입 커스텀 채널그룹 여부)</td></tr><tr><td class="mono">E3</td><td><b>GSC AI Overviews 노출</b><div class="sub">배점 2</div></td><td><span class="lv lvu">미검증</span><div class="sub">미검증</div></td><td class="sub" style="color:var(--slate)">[manual] GSC 접근 필요 (AI Overviews 노출)</td></tr><tr><td class="mono">E4</td><td><b>기준선 스팟체크</b><div class="sub">배점 2</div></td><td><span class="lv lvu">미검증</span><div class="sub">미검증</div></td><td class="sub" style="color:var(--slate)">[manual] 기준선 스팟체크 미실시 (질의 10개 × 5회 반복은 별도 단계)</td></tr>
</tbody></table><h3 style="margin-top:22px;font-size:18px">F · 기술 위생 <span class="sub">이미지 대체텍스트·CMS 보안·페이지 무게·모바일. 보조 지표.</span></h3>
<table class="tbl"><thead><tr><th style="width:44px">ID</th><th style="width:22%">항목</th><th style="width:88px">판정</th><th>실측 근거</th></tr></thead><tbody>
<tr><td class="mono">F1</td><td><b>이미지 대체텍스트</b><div class="sub">배점 2</div></td><td><span class="lv lv0">L0</span><div class="sub">50% 이상 누락</div></td><td class="sub" style="color:var(--slate)">메인 페이지 이미지 45개 중 28개(62%) alt 누락 또는 빈 값</td></tr><tr><td class="mono">F2</td><td><b>CMS 보안·최신성</b><div class="sub">배점 1</div></td><td><span class="lv lvu">미검증</span><div class="sub">미검증</div></td><td class="sub" style="color:var(--slate)">Server: Microsoft-IIS/10.0, ASP.NET 계열 자체 제작 추정(meta generator 없음). CMS 버전·보안 패치 여부 판정 불가</td></tr><tr><td class="mono">F3</td><td><b>페이지 무게</b><div class="sub">배점 1</div></td><td><span class="lv lv3">L3</span><div class="sub">&lt; 300KB</div></td><td class="sub" style="color:var(--slate)">초기 HTML 153KB (300KB 미만)</td></tr><tr><td class="mono">F4</td><td><b>모바일 대응</b><div class="sub">배점 1</div></td><td><span class="lv lv2">L2</span><div class="sub">반응형</div></td><td class="sub" style="color:var(--slate)">viewport meta 존재, 반응형. CWV 미측정</td></tr>
</tbody></table>
</div></section>
<section class="pb light"><div class="page">
<div class="eyebrow">Action Plan</div><h2>지금 고칠 것과 4주 안에 만들 것을 나눴습니다</h2>
<h3 style="margin-top:20px">P0 · 지금 고칠 것 (1주 안)</h3><div class="grid g2"><div class="card act"><span class="pill gray eff">1일</span><div class="pri">P0</div><h3>정본 URL 수렴 + sitemap 선언</h3><p style="margin:0 0 8px;color:var(--slate)">k-wonjin.co.kr → www.k-wonjin.co.kr 로 301 통일, robots.txt 에 Sitemap: 선언, sitemap lastmod 갱신. 전화번호를 대표번호 1개로 통일.</p><div class="sub">연결 항목 <span class="mono">A4</span> <span class="mono">A5</span> <span class="mono">B4</span></div></div><div class="card act"><span class="pill gray eff">1주</span><div class="pri">P0</div><h3>MedicalClinic + Physician 스키마</h3><p style="margin:0 0 8px;color:var(--slate)">Organization 을 MedicalClinic 으로 승격(address·telephone·openingHoursSpecification·medicalSpecialty). 의료진 17명 Physician 스키마(이름·직함·전문의·프로필 URL). 영문 사이트와 hreflang 상호 연결.</p><div class="sub">연결 항목 <span class="mono">B2</span> <span class="mono">B3</span> <span class="mono">B7</span></div></div></div><h3 style="margin-top:20px">P1 · 4주 안에 만들 것</h3><div class="grid g2"><div class="card act"><span class="pill gray eff">2~4주</span><div class="pri">P1</div><h3>시술 페이지 인용 구조: H1·표·FAQ</h3><p style="margin:0 0 8px;color:var(--slate)">시술 페이지마다 H1 1개, 수술 시간·마취·실밥·일상 복귀를 HTML 표로, FAQ 5문항 + FAQPage 스키마. CFS·트임 페이지의 수치 형식을 다른 눈·코·윤곽 페이지로 확산. 메인에 병원 정의문 한 단락.</p><div class="sub">연결 항목 <span class="mono">C1</span> <span class="mono">C2</span> <span class="mono">C3</span> <span class="mono">C4</span> <span class="mono">C5</span></div></div><div class="card act"><span class="pill gray eff">1주</span><div class="pri">P1</div><h3>이미지 alt 일괄 정비 + 갱신 신호</h3><p style="margin:0 0 8px;color:var(--slate)">메인 28개 이미지 alt 작성(무엇을 찍은 사진인지 사실만). 페이지에 dateModified 마크업, sitemap lastmod 를 실제 수정일로.</p><div class="sub">연결 항목 <span class="mono">F1</span> <span class="mono">C6</span></div></div><div class="card act"><span class="pill gray eff">2~4주</span><div class="pri">P1</div><h3>서포터즈 사이트로 오픈웹 원문 공급</h3><p style="margin:0 0 8px;color:var(--slate)">자동 빌드 미리보기(supporters-wonjin.vercel.app)의 글 6편을 병원이 검토·승인하면 색인 허용 도메인으로 발행. 유튜브 설명 영상 10편을 글에 연결. 이후 질문 뱅크 기준으로 편집 캘린더 운영.</p><div class="sub">연결 항목 <span class="mono">C2</span> <span class="mono">C4</span> <span class="mono">C5</span> <span class="mono">D3</span></div></div></div><h3 style="margin-top:20px">P2 · 이어서 할 것</h3><div class="grid g2"><div class="card act"><span class="pill gray eff">지속</span><div class="pri">P2</div><h3>표면 실측과 측정 인프라</h3><p style="margin:0 0 8px;color:var(--slate)">강남언니·GBP·네이버 플레이스 NAP 대조표 작성, GA4 AI 유입 채널그룹, GSC AI Overviews 확인, 서버 로그의 AI 크롤러 히트 계측. Bing Webmaster 등록.</p><div class="sub">연결 항목 <span class="mono">D1</span> <span class="mono">D2</span> <span class="mono">D4</span> <span class="mono">D5</span> <span class="mono">E1</span> <span class="mono">E2</span> <span class="mono">E3</span> <span class="mono">A6</span></div></div></div>
</div></section>
<section class="pb dark"><div class="page">
<div class="eyebrow">Supporters Preview</div><h2>홈페이지 자료만으로 서포터즈 사이트 초안을 만들었습니다</h2>
<p class="lede">병원 URL 하나로 근거 수집, 글 생성, 발행 게이트, 미리보기 배포까지 자동으로 돌렸습니다. 모든 글은 의학 검토 대기 상태이고 색인은 막혀 있습니다. 병원이 확인 항목을 입력하고 검토·승인하면 코드 수정 없이 발행됩니다.</p>
<div class="kpi" style="margin-top:18px">
<div class="k"><b>6</b><span>글 (게이트 통과)</span></div>
<div class="k"><b>736</b><span>유튜브 영상 집계</span></div>
<div class="k"><b>66</b><span>사용 가능 이미지 (전후·사례 제외)</span></div>
<div class="k"><b>17</b><span>의료진 프로필</span></div>
</div>
<p class="note" style="margin-top:14px">미리보기 <a href="https://supporters-wonjin.vercel.app" style="color:#C4B5FD">https://supporters-wonjin.vercel.app</a> · 병원 확인 항목 입력 <a href="https://infinith-demo.vercel.app/supporters/wonjin" style="color:#C4B5FD">infinith-demo.vercel.app/supporters/wonjin</a></p>
</div></section>
<section><div class="page">
<h3 style="font-size:18px">자동 생성한 글 6편</h3>
<table class="tbl"><thead><tr><th style="width:54px">분류</th><th>제목</th><th style="width:70px">근거 영상</th><th style="width:86px">병원 확인 대기</th><th style="width:90px">상태</th></tr></thead><tbody>
<tr><td class="mono">A</td><td><b>원진성형외과는 어떤 병원인가요? 진료 분야·의료진 구성·위치</b><div class="sub"><a href="https://supporters-wonjin.vercel.app/posts/what-is-clinic">https://supporters-wonjin.vercel.app/posts/what-is-clinic</a></div></td><td>0편</td><td>5건</td><td><span class="pill gray">의학 검토 대기</span></td></tr><tr><td class="mono">B</td><td><b>원진성형외과 방문 안내: 위치·진료시간·주차·예약은 어떻게 하나요?</b><div class="sub"><a href="https://supporters-wonjin.vercel.app/posts/visit-guide">https://supporters-wonjin.vercel.app/posts/visit-guide</a></div></td><td>0편</td><td>3건</td><td><span class="pill gray">의학 검토 대기</span></td></tr><tr><td class="mono">D</td><td><b>원진성형외과 윤곽은 어떤 방법이 있고 시술 시간·마취·회복은 어떻게 되나요?</b><div class="sub"><a href="https://supporters-wonjin.vercel.app/posts/procedure-facial">https://supporters-wonjin.vercel.app/posts/procedure-facial</a></div></td><td>0편</td><td>5건</td><td><span class="pill gray">의학 검토 대기</span></td></tr><tr><td class="mono">D</td><td><b>원진성형외과 눈성형은 어떤 방법이 있고 시술 시간·마취·회복은 어떻게 되나요?</b><div class="sub"><a href="https://supporters-wonjin.vercel.app/posts/procedure-eye">https://supporters-wonjin.vercel.app/posts/procedure-eye</a></div></td><td>3편</td><td>5건</td><td><span class="pill gray">의학 검토 대기</span></td></tr><tr><td class="mono">D</td><td><b>원진성형외과 레이저안티에이징은 어떤 방법이 있고 시술 시간·마취·회복은 어떻게 되나요?</b><div class="sub"><a href="https://supporters-wonjin.vercel.app/posts/procedure-skin">https://supporters-wonjin.vercel.app/posts/procedure-skin</a></div></td><td>0편</td><td>6건</td><td><span class="pill gray">의학 검토 대기</span></td></tr><tr><td class="mono">C</td><td><b>상담에서 꼭 물어봐야 할 질문은 무엇인가요?</b><div class="sub"><a href="https://supporters-wonjin.vercel.app/posts/consult-questions">https://supporters-wonjin.vercel.app/posts/consult-questions</a></div></td><td>3편</td><td>3건</td><td><span class="pill gray">의학 검토 대기</span></td></tr>
</tbody></table>
<h3 style="font-size:18px;margin-top:26px">발견한 채널과 확인 결과</h3>
<p class="sub" style="color:var(--slate)">홈페이지 역링크, 프로필 제목·설명, 등록부, 검색 결과를 근거로 병원 것인지 판정했습니다. "확인 필요"는 병원이 맞다고 알려 주시면 연결합니다.</p>
<table class="tbl"><thead><tr><th style="width:110px">플랫폼</th><th>확인됨</th><th>확인 필요</th></tr></thead><tbody>
<tr><td><b>유튜브</b></td><td><a href="https://www.youtube.com/channel/UCNBaQBCok5xIViCnrGdisoQ">WJ원진성형외과</a> <span class="sub">(구독 28,300)</span><br><a href="https://www.youtube.com/@wjwonjin">WJ원진성형외과</a> <span class="sub">(구독 14,700)</span></td><td class="sub"><a href="https://www.youtube.com/channel/UCNmTm4UubrscPczrAfGhUvQ" style="color:var(--muted)">Wonjin Plastic Surgery Korea</a><br><a href="https://www.youtube.com/channel/UCk2UbMID_NjYZe7-WBLQXRw" style="color:var(--muted)">유세윤의 광고100 X 원진성형외과</a><br><a href="https://www.youtube.com/channel/UCEC21_7WvRx8cWlje4sLVlw" style="color:var(--muted)">WONJINPS</a><br><a href="https://www.youtube.com/channel/UCJuM0rYynWd9xAXmOf-dl9A" style="color:var(--muted)">WONJIN_OFFICIAL - KOREA No.1 PLASTIC SURGERY</a><br><a href="https://www.youtube.com/channel/UChnJVIjdmUeZkA40cRFMzFA" style="color:var(--muted)">원트성형외과</a></td></tr><tr><td><b>인스타그램</b></td><td><a href="https://www.instagram.com/wonjin_official/">Instagram</a><br><a href="https://www.instagram.com/wonjin_ps/">Instagram</a><br><a href="https://www.instagram.com/wj_cosmetic/">Instagram</a></td><td class="sub"><a href="https://www.instagram.com/wj_plasticsurgery/" style="color:var(--muted)">Instagram</a><br><a href="https://www.instagram.com/hamdust_/" style="color:var(--muted)">Instagram</a><br><a href="https://www.instagram.com/bjwnjn/" style="color:var(--muted)">Instagram</a><br><a href="https://www.instagram.com/wonjin_sunnywon/" style="color:var(--muted)">Instagram</a><br><a href="https://www.instagram.com/wonjinire/" style="color:var(--muted)">Instagram</a><br><a href="https://www.instagram.com/wxxnzin/" style="color:var(--muted)">Instagram</a><br><a href="https://www.instagram.com/wj_1j_/" style="color:var(--muted)">Instagram</a><br><a href="https://www.instagram.com/wonjin/" style="color:var(--muted)">Instagram</a><br><a href="https://www.instagram.com/wonjinps/" style="color:var(--muted)">Instagram</a></td></tr><tr><td><b>페이스북</b></td><td><a href="https://www.facebook.com/KwonjinPS">KwonjinPS</a></td><td class="sub"></td></tr><tr><td><b>네이버 블로그</b></td><td><a href="https://blog.naver.com/popokpop">popokpop</a></td><td class="sub"><a href="https://blog.naver.com/naramalssami1" style="color:var(--muted)">naramalssami1</a><br><a href="https://blog.naver.com/qeqe0420" style="color:var(--muted)">qeqe0420</a><br><a href="https://blog.naver.com/vpepwxormtfo" style="color:var(--muted)">vpepwxormtfo</a></td></tr><tr><td><b>틱톡</b></td><td><a href="https://www.tiktok.com/@wonjin_official">wonjin_official</a></td><td class="sub"></td></tr><tr><td><b>카카오톡 채널</b></td><td><a href="https://pf.kakao.com/_bxflzE">원진성형외과의원</a></td><td class="sub"></td></tr><tr><td><b>강남언니</b></td><td><a href="https://www.gangnamunni.com/hospitals/2500">원진성형외과의원 | 강남언니 - 내 선택에 확신을</a></td><td class="sub"></td></tr><tr><td><b>네이버 플레이스</b></td><td><span class="sub">찾지 못함</span></td><td class="sub"></td></tr><tr><td><b>바비톡</b></td><td><a href="https://babitalk.com/hospitals/3143">원진성형외과의원 | 후기∙가격∙이벤트 정보 - 바비톡</a></td><td class="sub"></td></tr>
</tbody></table>
<h3 style="font-size:18px;margin-top:26px">병원이 확인해 주실 것 9가지</h3>
<p class="sub" style="color:var(--slate)">홈페이지 공개 자료에 없어 병원만 답할 수 있는 값입니다. 입력 전까지 사이트에는 "확인 대기"로 표시됩니다.</p>
<div class="grid g3" style="margin-top:10px"><div class="card" style="padding:14px 16px"><span class="mono">1</span> <b>작성자 실명·직함·공개 프로필</b></div><div class="card" style="padding:14px 16px"><span class="mono">2</span> <b>글별 의학 검토 원장과 검토일</b></div><div class="card" style="padding:14px 16px"><span class="mono">3</span> <b>지원 관계 문구</b></div><div class="card" style="padding:14px 16px"><span class="mono">4</span> <b>견적 기본 포함 항목·결제 방식</b></div><div class="card" style="padding:14px 16px"><span class="mono">5</span> <b>회복 일정표의 병원 확정값</b></div><div class="card" style="padding:14px 16px"><span class="mono">6</span> <b>재수술 정책</b></div><div class="card" style="padding:14px 16px"><span class="mono">7</span> <b>페이지 간 표기 불일치의 정답</b></div><div class="card" style="padding:14px 16px"><span class="mono">8</span> <b>진료 분야별 담당 원장</b></div><div class="card" style="padding:14px 16px"><span class="mono">9</span> <b>정식 도메인·색인 허용 시점·홈페이지 링크 위치</b></div></div>
<p class="note">싣지 않는 것: 전후 사진, 환자 경험담, 다른 병원과의 비교, 효과 보장, 가격 금액. 이미지는 의료법 기준 규칙에 따라 전후·사례·모델·수술 장면·인증 마크를 자동 제외했습니다.</p>
</div></section>
<section class="pb light"><div class="page">
<div class="eyebrow">Integrity</div><h2>근거 없는 것은 약속하지 않습니다</h2>
<table class="tbl" style="margin-top:12px"><thead><tr><th style="width:30%">하지 않는 것</th><th>이유</th></tr></thead><tbody>
<tr><td><b>llms.txt 제작</b></td><td style="color:var(--slate)">Google 공식(2026-06) 효과 0, 채택률 0.13%, 서버 로그에 요청 자체가 없음. 위약이다.</td></tr><tr><td><b>&quot;스키마 넣으면 AI가 인용합니다&quot; 약속</b></td><td style="color:var(--slate)">인과 증거 없음. 스키마는 엔티티 식별을 돕는 위생 조치로만 설명한다.</td></tr><tr><td><b>단발 조회로 순위·노출 보고</b></td><td style="color:var(--slate)">LLM 답변 분산 10~34%. 반복 측정 + 신뢰구간으로만 보고한다.</td></tr><tr><td><b>답변엔진 UI 자동 조회</b></td><td style="color:var(--slate)">OpenAI·Perplexity 약관 위반. 주력 측정은 오너가 소유한 신호(로그·GA4)로 한다.</td></tr><tr><td><b>네이버 블로그 증량</b></td><td style="color:var(--slate)">blog.naver.com robots.txt가 AI 크롤링을 전면 금지. 글로벌 AI 답변 기여도 0. 네이버 AI 브리핑은 별도 트랙.</td></tr>
</tbody></table>
<h3 style="margin-top:26px;font-size:18px">측정 조건</h3>
<ul style="color:var(--slate)"><li>실측 대상: 메인 페이지 + /contents/eye/cfs 2개 URL, JS 미실행 HTML 기준</li><li>크롤러 접근성: robots.txt 파싱 + 12개 AI 봇 UA 실제 GET 요청</li><li>semi/manual 항목은 자동 수집 증거만 첨부하고 미검증 처리, 분모 제외 (사람 보정 후 확정)</li><li>답변엔진 언급률은 측정하지 않았다 (단발 조회는 무효, 반복 측정은 별도 기준선 단계에서 수행)</li></ul>
<h3 style="margin-top:26px;font-size:18px">측정 가능 범위</h3>
<p style="color:var(--slate);margin:0 0 8px">무엇을 어떤 방법으로 재고, 무엇은 잴 수 없는지 먼저 밝힙니다. 잴 수 없는 것은 숫자를 만들지 않고 비워 둡니다.</p>
<table class="tbl"><thead><tr><th style="width:26%">재는 것</th><th style="width:22%">방법</th><th style="width:80px">가능</th><th>비고</th></tr></thead><tbody>
<tr><td><b>AI 답변 준비도 · 출처 준비도</b></td><td>공개 페이지 실측(크롤러·렌더링·스키마·표면)</td><td><span class="pill gray">가능</span></td><td class="sub">오너 계정이 필요한 항목(GA4·GSC·서버 로그)은 미검증으로 표시</td></tr>
<tr><td><b>답변엔진 언급률 · 감성</b></td><td>질문 뱅크를 공식 API로 반복 조회</td><td><span class="pill gray">가능</span></td><td class="sub">ChatGPT · Perplexity 기본, 단발 조회는 쓰지 않음. 소비자 화면 자동 조회는 약관 위반이라 하지 않음</td></tr>
<tr><td><b>ChatGPT 유입</b></td><td>GA4 세션 출처 · utm</td><td><span class="pill gray">가능</span></td><td class="sub">링크에 utm_source=chatgpt.com 을 붙여 보내므로 리퍼러가 비어도 잡힌다.</td></tr><tr><td><b>Perplexity 유입</b></td><td>GA4 세션 출처 · utm</td><td><span class="pill gray">일부만</span></td><td class="sub">리퍼러를 자주 비운다. 잡히는 건수는 실제 유입의 일부다. 실측 언급률과 함께 읽는다.</td></tr><tr><td><b>Gemini 유입</b></td><td>GA4 세션 출처 · utm</td><td><span class="pill gray">가능</span></td><td class="sub">gemini.google.com 리퍼러로 분리된다.</td></tr><tr><td><b>Copilot 유입</b></td><td>GA4 세션 출처 · utm</td><td><span class="pill gray">일부만</span></td><td class="sub">Bing 검색과 리퍼러가 섞이는 경우가 있다.</td></tr><tr><td><b>Claude 유입</b></td><td>GA4 세션 출처 · utm</td><td><span class="pill gray">가능</span></td><td class="sub">claude.ai 리퍼러.</td></tr><tr><td><b>Google AI Overview · AI Mode 유입</b></td><td>GA4 세션 출처 · utm</td><td><span class="pill gray">불가</span></td><td class="sub">구글 자연검색(google / organic)과 같은 리퍼러라 GA4로는 구분되지 않는다. 타사 도구의 'AI Overview 유입'도 추정치다. 가시성은 답변엔진 실측으로만 잰다.</td></tr><tr><td><b>네이버 AI 브리핑 유입</b></td><td>GA4 세션 출처 · utm</td><td><span class="pill gray">불가</span></td><td class="sub">네이버 검색 리퍼러(search.naver.com)와 구분되지 않는다. 네이버 서치어드바이저 유입 키워드로 간접 확인만 가능.</td></tr><tr><td><b>기타 AI 검색 유입</b></td><td>GA4 세션 출처 · utm</td><td><span class="pill gray">일부만</span></td><td class="sub">리퍼러가 있을 때만 잡힌다.</td></tr>
<tr><td><b>AI 크롤러 방문</b></td><td>서버 로그(UA · IP 대역)</td><td><span class="pill gray">조건부</span></td><td class="sub">서포터즈 사이트는 로그로 계측 가능. 병원 사이트는 로그 접근 권한이 있을 때만</td></tr>
<tr><td><b>전화 · 오프라인 방문</b></td><td>-</td><td><span class="pill gray">불가</span></td><td class="sub">사이트의 전화 탭 · 예약 클릭까지만 잼</td></tr>
</tbody></table>
<h3 style="margin-top:26px;font-size:18px">채점 방법</h3>
<p style="color:var(--slate);margin:0">기준표 v1.0 36개 실측 항목(A 접근성 · B 엔티티 · C 콘텐츠 · D 표면 · E 측정 · F 위생)을 0~3 레벨로 판정하고, v2.0은 그 항목을 답변 준비도(AEO) 8개와 출처 준비도(GEO) 8개로 묶어 각 100점으로 정규화합니다. 미검증 항목은 분모에서 빼고, 치명 게이트(크롤러 차단 등)에 걸리면 상한을 둡니다. 답변엔진 언급률은 단발 조회로는 재지 않습니다.</p>
</div></section>
<footer>INFINITH · AI오투오 · 문의 o2oteam@o2o.kr · 이 리포트는 공개 페이지 실측에 근거하며, 오너 계정(GA4·GSC·서버 로그)이 필요한 항목은 미검증으로 두었습니다. 법률 자문이 아닙니다.</footer>
</body></html>

View File

@ -9,6 +9,7 @@
* 화법: 고객용. "~합니다", . . .
*/
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
import AI_CHANNELS from '../data/ai_channels.json';
import { DISCOVERY_RESULTS } from '../src/data/discoveryResults';
import { AEO_GEO_RUBRIC } from '../src/data/aeoGeoRubric';
import { AEO_GEO_RUBRIC_V2 } from '../src/data/aeoGeoRubricV2';
@ -184,6 +185,15 @@ ${status ? `<section class="pb dark"><div class="page">
</tbody></table>
<h3 style="margin-top:26px;font-size:18px"> </h3>
<ul style="color:var(--slate)">${result.conditions.map((c) => `<li>${esc(c)}</li>`).join('')}</ul>
<h3 style="margin-top:26px;font-size:18px"> </h3>
<p style="color:var(--slate);margin:0 0 8px"> , . .</p>
<table class="tbl"><thead><tr><th style="width:26%"> </th><th style="width:22%"></th><th style="width:80px"></th><th></th></tr></thead><tbody>
<tr><td><b>AI · </b></td><td> (···)</td><td><span class="pill gray"></span></td><td class="sub"> (GA4·GSC· ) </td></tr>
<tr><td><b> · </b></td><td> API로 </td><td><span class="pill gray"></span></td><td class="sub">ChatGPT · Perplexity , . </td></tr>
${(AI_CHANNELS as any).channels.map((c: any) => `<tr><td><b>${esc(c.label)} 유입</b></td><td>GA4 세션 출처 · utm</td><td><span class="pill gray">${c.measurable === 'yes' ? '가능' : c.measurable === 'partial' ? '일부만' : '불가'}</span></td><td class="sub">${esc(c.note)}</td></tr>`).join('')}
<tr><td><b>AI </b></td><td> (UA · IP )</td><td><span class="pill gray"></span></td><td class="sub"> . </td></tr>
<tr><td><b> · </b></td><td>-</td><td><span class="pill gray"></span></td><td class="sub"> · </td></tr>
</tbody></table>
<h3 style="margin-top:26px;font-size:18px"> </h3>
<p style="color:var(--slate);margin:0"> v1.0 ${total} (A · B · C · D · E · F ) 0~3 , v2.0 (AEO) ${v2.aeo.items.length} (GEO) ${v2.geo.items.length} 100 . , ( ) . .</p>
</div></section>

149
scripts/ga4_ai_traffic.py Normal file
View File

@ -0,0 +1,149 @@
#!/usr/bin/env python3
"""GA4 → AI 검색 유입 리포트. GA4 Data API(runReport)로 세션 출처·랜딩 페이지·키 이벤트를 받아 data/ai_channels.json 기준으로 AI 채널을 분류한다.
연동 방식(B2B): 병원(또는 서포터즈 사이트) GA4 속성에 우리 서비스 계정 이메일을 '뷰어' 추가하면 . 코드 설치 없음.
1) Google Cloud 프로젝트에서 Google Analytics Data API 켜기 서비스 계정 (JSON) 발급
2) GA4 관리 > 속성 액세스 관리 > 서비스 계정 이메일을 뷰어로 추가
3) .env: GA4_SERVICE_ACCOUNT_JSON=< 파일 경로> (속성 ID는 --property 인자)
사용:
python3 scripts/ga4_ai_traffic.py --property 123456789 --days 90 --clinic wonjin --out docs/reports/wonjin/05_ai_traffic
python3 scripts/ga4_ai_traffic.py --from-csv <GA4 탐색 내보내기.csv> --clinic wonjin --out ... # API 없이 (열: date, sessionSource, sessionMedium, landingPage, sessions, keyEvents)
python3 scripts/ga4_ai_traffic.py --mock --clinic demo --out /tmp/ga4demo # 합성 데이터로 파이프라인 검증
산출: <out>/ai_traffic.json (일별·채널별·랜딩별 집계) + <out>/AI_Traffic_Report.md ("측정 가능 범위" 포함)
원칙: GA4 없는 (AI Overview·네이버 AI 브리핑·리퍼러 없는 유입·노출만 경우) 숫자 없이 '측정 불가' 적는다. 추정치를 만들지 않는다.
"""
import argparse, csv, datetime as dt, json, os, random, sys, collections
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
CHANNELS = json.load(open(os.path.join(ROOT, "data", "ai_channels.json"), encoding="utf-8"))
def load_env():
p = os.path.join(ROOT, ".env")
if not os.path.exists(p): return
for line in open(p, encoding="utf-8"):
if "=" in line and not line.strip().startswith("#"):
k, v = line.split("=", 1); os.environ.setdefault(k.strip(), v.strip().strip('"').strip("'"))
# ---------- 분류 ----------
def classify(source: str, medium: str = "", utm_source: str = "") -> str | None:
"""세션 출처를 AI 채널 id 로. 아니면 None. 부분 문자열이 아니라 호스트 경계로 본다."""
s = (source or "").lower().strip(); u = (utm_source or source or "").lower().strip()
for ch in CHANNELS["channels"]:
for host in ch["sources"]:
h = host.lower()
if s == h or s.endswith("." + h) or s.startswith(h + "/") or s == h.replace("www.", ""): return ch["id"]
for us in ch["utm_sources"]:
if u == us.lower(): return ch["id"]
return None
LABEL = {c["id"]: c["label"] for c in CHANNELS["channels"]}
MEASURABLE = {c["id"]: c["measurable"] for c in CHANNELS["channels"]}
# ---------- 데이터 소스 ----------
def rows_from_api(property_id: str, days: int):
try:
from google.oauth2 import service_account
from google.auth.transport.requests import AuthorizedSession
except ImportError:
sys.exit("google-auth 가 필요합니다: pip install google-auth requests")
load_env()
key_path = os.environ.get("GA4_SERVICE_ACCOUNT_JSON")
if not key_path or not os.path.exists(key_path): sys.exit("GA4_SERVICE_ACCOUNT_JSON(서비스 계정 키 파일 경로) 필요 (.env)")
creds = service_account.Credentials.from_service_account_file(key_path, scopes=["https://www.googleapis.com/auth/analytics.readonly"])
sess = AuthorizedSession(creds)
body = {"dateRanges": [{"startDate": f"{days}daysAgo", "endDate": "yesterday"}],
"dimensions": [{"name": "date"}, {"name": "sessionSource"}, {"name": "sessionMedium"}, {"name": "landingPagePlusQueryString"}],
"metrics": [{"name": "sessions"}, {"name": "keyEvents"}, {"name": "engagedSessions"}],
"limit": 100000}
r = sess.post(f"https://analyticsdata.googleapis.com/v1beta/properties/{property_id}:runReport", json=body, timeout=60)
if r.status_code != 200: sys.exit(f"GA4 Data API {r.status_code}: {r.text[:300]}")
j = r.json()
for row in j.get("rows", []):
d = [x["value"] for x in row["dimensionValues"]]; m = [x["value"] for x in row["metricValues"]]
yield {"date": f"{d[0][:4]}-{d[0][4:6]}-{d[0][6:]}", "source": d[1], "medium": d[2], "landing": d[3].split("?")[0], "utm_source": _utm(d[3]),
"sessions": int(float(m[0])), "key_events": int(float(m[1])), "engaged": int(float(m[2]))}
def _utm(landing_with_qs: str) -> str:
if "utm_source=" not in landing_with_qs: return ""
return landing_with_qs.split("utm_source=")[1].split("&")[0]
def rows_from_csv(path: str):
with open(path, encoding="utf-8-sig") as f:
for r in csv.DictReader(f):
g = lambda *ks: next((r[k] for k in ks if k in r and r[k] not in (None, "")), "")
yield {"date": g("date", "날짜"), "source": g("sessionSource", "세션 소스", "source"), "medium": g("sessionMedium", "세션 매체", "medium"),
"landing": g("landingPage", "landingPagePlusQueryString", "방문 페이지", "landing").split("?")[0], "utm_source": _utm(g("landingPagePlusQueryString", "landing")),
"sessions": int(float(g("sessions", "세션수", "세션") or 0)), "key_events": int(float(g("keyEvents", "주요 이벤트", "conversions") or 0)), "engaged": int(float(g("engagedSessions", "참여 세션수") or 0))}
def rows_mock(days: int = 60):
random.seed(7); today = dt.date.today()
srcs = [("google", "organic", 40), ("(direct)", "(none)", 18), ("chatgpt.com", "referral", 6), ("perplexity.ai", "referral", 2), ("gemini.google.com", "referral", 1), ("search.naver.com", "organic", 25), ("instagram.com", "referral", 4)]
pages = ["/", "/posts/consult-questions", "/posts/procedure-eye", "/visit", "/videos", "/newsroom"]
for i in range(days):
d = (today - dt.timedelta(days=days - i)).isoformat()
for s, m, w in srcs:
for p in random.sample(pages, 3):
n = max(0, int(random.gauss(w / 3, w / 6)))
if n: yield {"date": d, "source": s, "medium": m, "landing": p, "utm_source": "chatgpt.com" if s == "chatgpt.com" else "", "sessions": n, "key_events": sum(1 for _ in range(n) if random.random() < 0.06), "engaged": int(n * 0.6)}
# ---------- 집계 ----------
def aggregate(rows):
daily = collections.defaultdict(lambda: collections.Counter()); by_ch = collections.Counter(); ke_ch = collections.Counter()
landing = collections.defaultdict(lambda: collections.Counter()); total = collections.Counter(); direct = 0; weeks = collections.defaultdict(lambda: collections.Counter())
for r in rows:
total["sessions"] += r["sessions"]; total["key_events"] += r["key_events"]
ch = classify(r["source"], r["medium"], r.get("utm_source", ""))
if r["source"] in ("(direct)", "direct"): direct += r["sessions"]
if not ch: continue
daily[r["date"]][ch] += r["sessions"]; by_ch[ch] += r["sessions"]; ke_ch[ch] += r["key_events"]
landing[r["landing"]][ch] += r["sessions"]; landing[r["landing"]]["_ke"] += r["key_events"]
y, w, _ = dt.date.fromisoformat(r["date"]).isocalendar(); weeks[f"{y}-W{w:02d}"][ch] += r["sessions"]
ai_total = sum(by_ch.values())
return {"total_sessions": total["sessions"], "total_key_events": total["key_events"], "direct_sessions": direct,
"ai_sessions": ai_total, "ai_share_pct": round(ai_total / total["sessions"] * 100, 2) if total["sessions"] else 0,
"by_channel": [{"id": c, "label": LABEL[c], "measurable": MEASURABLE[c], "sessions": n, "key_events": ke_ch[c], "cvr_pct": round(ke_ch[c] / n * 100, 1) if n else 0} for c, n in by_ch.most_common()],
"weekly": {w: dict(v) for w, v in sorted(weeks.items())}, "daily": {d: dict(v) for d, v in sorted(daily.items())},
"landing": sorted([{"page": p, "ai_sessions": sum(n for k, n in v.items() if k != "_ke"), "key_events": v["_ke"], "by": {k: n for k, n in v.items() if k != "_ke"}} for p, v in landing.items()], key=lambda x: -x["ai_sessions"])[:20]}
# ---------- 리포트 ----------
def scope_table_md():
lines = ["| 채널 | GA4로 측정 | 비고 |", "|---|---|---|"]
M = {"yes": "가능", "partial": "일부만", "no": "불가"}
for c in CHANNELS["channels"]: lines.append(f"| {c['label']} | **{M[c['measurable']]}** | {c['note']} |")
lines += ["", "GA4로 잴 수 없는 것:", ""] + [f"- {x}" for x in CHANNELS["not_measurable_by_ga4"]]
return "\n".join(lines)
def report_md(agg, clinic, source_desc, days):
L = [f"# AI 검색 유입 리포트 · {clinic}", "", f"기간 최근 {days}일 · 데이터 {source_desc} · 분류 기준 `data/ai_channels.json` v{CHANNELS['version']} · 작성 {dt.date.today()}", "",
"## 1. 요약", "", "| 지표 | 값 |", "|---|---|",
f"| 전체 세션 | {agg['total_sessions']:,} |", f"| AI 검색 유입 세션(측정 가능분) | {agg['ai_sessions']:,} ({agg['ai_share_pct']}%) |",
f"| AI 유입의 키 이벤트(전화 탭·예약·홈페이지 이동) | {sum(c['key_events'] for c in agg['by_channel']):,} |", f"| direct 세션(리퍼러 없음, AI 유입 일부가 여기 섞임) | {agg['direct_sessions']:,} |", "",
"## 2. 채널별", "", "| 채널 | 세션 | 키 이벤트 | 전환율 | 측정 |", "|---|---|---|---|---|"]
M = {"yes": "가능", "partial": "일부만", "no": "불가"}
for c in agg["by_channel"]: L.append(f"| {c['label']} | {c['sessions']:,} | {c['key_events']} | {c['cvr_pct']}% | {M[c['measurable']]} |")
for c in CHANNELS["channels"]:
if c["measurable"] == "no": L.append(f"| {c['label']} | 측정 불가 | | | 불가 |")
L += ["", "## 3. AI 유입 랜딩 페이지 (상위)", "", "| 페이지 | AI 세션 | 키 이벤트 | 채널 |", "|---|---|---|---|"]
for p in agg["landing"][:12]: L.append(f"| {p['page']} | {p['ai_sessions']} | {p['key_events']} | {', '.join(f'{LABEL[k]} {n}' for k, n in sorted(p['by'].items(), key=lambda x: -x[1]))} |")
L += ["", "## 4. 주별 추이", "", "| 주 | " + " | ".join(LABEL[c["id"]] for c in agg["by_channel"]) + " | 합 |", "|---|" + "---|" * (len(agg["by_channel"]) + 1)]
for w, v in agg["weekly"].items(): L.append(f"| {w} | " + " | ".join(str(v.get(c["id"], 0)) for c in agg["by_channel"]) + f" | {sum(v.values())} |")
L += ["", "## 5. 측정 가능 범위", "", "이 리포트의 숫자는 GA4가 리퍼러나 utm으로 구분할 수 있는 유입만 셉니다. 아래 표의 '일부만''불가'는 실제보다 적게 잡히거나 전혀 잡히지 않는 채널입니다. AI 답변에 병원이 얼마나 나오는지(노출)는 이 리포트가 아니라 답변엔진 실측(질문 뱅크)으로 잽니다. 두 숫자를 같은 시간축에 놓고 읽어야 합니다.", "", scope_table_md(), "",
"## 6. 연동 방식", "", "GA4 속성 액세스 관리에서 INFINITH 서비스 계정 이메일을 '뷰어'로 추가하면 매일 자동으로 갱신됩니다. 코드 설치는 없습니다. 병원 사이트에 GA4가 없으면 태그 설치가 먼저이고, 서포터즈 사이트는 INFINITH가 직접 계측합니다."]
return "\n".join(L) + "\n"
if __name__ == "__main__":
ap = argparse.ArgumentParser()
ap.add_argument("--property"); ap.add_argument("--days", type=int, default=90); ap.add_argument("--from-csv"); ap.add_argument("--mock", action="store_true")
ap.add_argument("--clinic", required=True); ap.add_argument("--out", required=True)
a = ap.parse_args()
if a.mock: rows, desc = list(rows_mock(a.days if a.days < 90 else 60)), "합성 데이터(파이프라인 검증용)"
elif a.from_csv: rows, desc = list(rows_from_csv(a.from_csv)), f"GA4 내보내기 {os.path.basename(a.from_csv)}"
elif a.property: rows, desc = list(rows_from_api(a.property, a.days)), f"GA4 Data API 속성 {a.property}"
else: sys.exit("--property 또는 --from-csv 또는 --mock 중 하나")
agg = aggregate(rows)
os.makedirs(a.out, exist_ok=True)
json.dump({"clinic": a.clinic, "source": desc, "days": a.days, "generatedAt": dt.date.today().isoformat(), "channels_version": CHANNELS["version"], **agg}, open(os.path.join(a.out, "ai_traffic.json"), "w", encoding="utf-8"), ensure_ascii=False, indent=1)
open(os.path.join(a.out, "AI_Traffic_Report.md"), "w", encoding="utf-8").write(report_md(agg, a.clinic, desc, a.days))
print(f"세션 {agg['total_sessions']:,} · AI 유입 {agg['ai_sessions']:,} ({agg['ai_share_pct']}%) · 채널 {', '.join(f'{c['label']} {c['sessions']}' for c in agg['by_channel'])}{a.out}")

View File

@ -21,5 +21,7 @@
"visitReads": [],
"newsNote": "",
"newsOutletsExample": "",
"sponsorNotice": ""
"sponsorNotice": "",
"ga4MeasurementId": "",
"_comment_ga4": "GA4 측정 ID(G-XXXX). 비우면 태그를 넣지 않는다. 키 이벤트 이름은 data/ai_channels.json conversion_events 와 같다."
}

View File

@ -16,6 +16,8 @@ const { title, description, jsonLd, ogImage = S.buildingImage?.src || '/favicon.
const site = Astro.site?.toString().replace(/\/$/, '') ?? '';
const cleanPath = Astro.url.pathname.replace(/\/index\.html$/, '/').replace(/\.html$/, '');
const canonical = (new URL(cleanPath, Astro.site).toString().replace(/\/$/, '') || site);
// GA4: site.json 의 ga4MeasurementId(G-XXXX) 가 있을 때만 태그를 넣는다. 키 이벤트는 data/ai_channels.json conversion_events 와 같은 이름(click_tel · click_reservation · click_clinic_site · click_map).
const GA4: string = /^G-[A-Z0-9]{6,}$/.test(String((S as Record<string, any>).ga4MeasurementId ?? '')) ? String((S as Record<string, any>).ga4MeasurementId) : '';
const ld = jsonLd ? JSON.stringify({ '@context': 'https://schema.org', ...(Array.isArray(jsonLd) ? { '@graph': jsonLd } : jsonLd) }) : null;
---
<!doctype html>
@ -39,6 +41,9 @@ const ld = jsonLd ? JSON.stringify({ '@context': 'https://schema.org', ...(Array
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@700;900&family=Inter:wght@400;500;600&display=swap" rel="stylesheet" />
{ld && <script type="application/ld+json" set:html={ld} />}
{GA4 && <script is:inline async src={`https://www.googletagmanager.com/gtag/js?id=${GA4}`}></script>}
{GA4 && <script is:inline set:html={`window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments)}gtag('js',new Date());gtag('config','${GA4}',{anonymize_ip:true});
document.addEventListener('click',function(e){var a=e.target.closest&&e.target.closest('a');if(!a)return;var h=a.getAttribute('href')||'';var ev=h.indexOf('tel:')===0?'click_tel':a.dataset.ga==='reservation'||a.classList.contains('cta')?'click_reservation':a.dataset.ga==='clinic-site'?'click_clinic_site':a.dataset.ga==='map'?'click_map':null;if(ev)gtag('event',ev,{link_url:h,page_path:location.pathname});},true);`} />}
</head>
<body>
{!INDEXABLE && <div class="sample-banner">샘플 사이트입니다. 정식 공개 전입니다.</div>}

View File

@ -21,5 +21,7 @@
"visitReads": [],
"newsNote": "",
"newsOutletsExample": "",
"sponsorNotice": ""
"sponsorNotice": "",
"ga4MeasurementId": "",
"_comment_ga4": "GA4 측정 ID(G-XXXX). 비우면 태그를 넣지 않는다. 키 이벤트 이름은 data/ai_channels.json conversion_events 와 같다."
}