feat(discovery): 기준표 v2.0 정식 채택 — 배지·헤더·랜딩 카피·비교 엑셀·덱 v2.0
- aeoGeoRubricV2 status active/2.0, 결과 rubricVersion 2.0. 리포트 헤더는 AEO·GEO 링 2개가 주, v1.0 종합은 보조 표기 - 랜딩: Hero 카피, ABMR 칩(AEO/GEO 목표), SampleReport(두 링 + 축별 8항목 바), Roadmap 제목, HowItWorks를 v2 계산값에서 파생. 발견 칩 수치 v1.1 스냅샷으로 정정 - 비교 엑셀 v2: scripts/build_supporters_v2_compare.ts(점수 계산 TS 단일 원본) → build_supporters_audit_v2_xlsx.py. 공식 AEO 42/GEO 56 · 서포터즈 noindex 상한 40(G1) · 색인 허용 AEO 94/GEO 77 - 덱 v2.0: build_viewclinic_deck_v2_0.py (17장, 표지 배지 v2.0, KPI AEO/GEO, 스코어카드 2장, Method v2.0) + 표준캔버스 + PDF. v1.0·v1.1 파일은 그대로 - docs/AEO_GEO_RUBRIC.md에 v2.0 채택 주석, AEO_GEO_RUBRIC_v2.md 재생성, 리포트 페이지 PDF 재출력 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
c02de9ea51
commit
76ec63bc1e
@ -1,5 +1,7 @@
|
||||
# INFINITH AI Discovery AEO/GEO 채점 기준표 v1.0
|
||||
|
||||
> **2026-09-07 갱신:** 제품 점수는 v2.0(AEO 답변 준비도 100 · GEO 출처 준비도 100, `docs/AEO_GEO_RUBRIC_v2.md`)으로 바뀌었다. 이 문서의 36항목은 v2.0의 **실측 신호**로 그대로 쓰이며, v1.0 종합 100점은 리포트에 보조 표기로만 남는다.
|
||||
|
||||
> 코드 원본: `src/data/aeoGeoRubric.ts` · 타입: `src/types/discovery.ts` · 집계: `src/lib/discoveryScore.ts`
|
||||
> 리포트 페이지: `/discovery/:id` (`src/pages/DiscoveryReportPage.tsx`)
|
||||
> 근거 기반: ado2-aeo-geo 스킬 (2026-08-26 한국 실측 · Princeton GEO KDD 2024 · C-SEO Bench NeurIPS 2025)
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
# INFINITH AI Discovery 채점 기준표 v2.0 (초안) · AEO / GEO 분리 지표
|
||||
# INFINITH AI Discovery 채점 기준표 v2.0 · AEO / GEO 분리 지표
|
||||
|
||||
> 코드 원본: `src/data/aeoGeoRubricV2.ts` · 집계: `src/lib/discoveryScoreV2.ts` · 타입: `src/types/discovery.ts` (v2 블록)
|
||||
> 상태: **draft** (2026-09-07). v1.0(36항목 종합 100점)은 유지하며 두 점수를 나란히 표시한다. 이 문서는 코드에서 생성한다(`npx tsx scripts/gen_rubric_v2_doc.ts`).
|
||||
> 상태: **active** (2026-09-07). v1.0(36항목 종합 100점)은 유지하며 두 점수를 나란히 표시한다. 이 문서는 코드에서 생성한다(`npx tsx scripts/gen_rubric_v2_doc.ts`).
|
||||
|
||||
## 설계
|
||||
|
||||
|
||||
BIN
docs/deck/INFINITH_WordPress_GEO_AEO_장단점.pdf
Normal file
BIN
docs/deck/INFINITH_WordPress_GEO_AEO_장단점.pdf
Normal file
Binary file not shown.
BIN
docs/deck/INFINITH_WordPress_GEO_AEO_장단점.pptx
Normal file
BIN
docs/deck/INFINITH_WordPress_GEO_AEO_장단점.pptx
Normal file
Binary file not shown.
143
docs/deck/build_wp_geo_deck.py
Normal file
143
docs/deck/build_wp_geo_deck.py
Normal file
@ -0,0 +1,143 @@
|
||||
"""GEO/AEO 최적화 웹사이트에 워드프레스를 쓰는 것의 장단점 — INFINITH 덱."""
|
||||
import sys; sys.path.insert(0, "/Users/haewonkam/.claude/skills/infinith-deck/scripts")
|
||||
from ifd import Deck
|
||||
|
||||
d = Deck()
|
||||
|
||||
# 01 표지
|
||||
d.cover(eyebrow="GEO / AEO · PLATFORM DECISION",
|
||||
title_lines=["WordPress for", "AI Search."],
|
||||
subtitle="GEO·AEO 최적화 웹사이트에 워드프레스를 쓰는 것의 장단점 · 2026. 08. 28",
|
||||
meta_right="INFINITH Strategy Memo · 2026")
|
||||
|
||||
# 02 요약
|
||||
s = d.slide(eyebrow="Executive Summary",
|
||||
title="콘텐츠를 계속 쌓는다면 ", title_accent="워드프레스",
|
||||
lede=["GEO·AEO의 핵심은 AI 크롤러가 읽는 정적 HTML에 구조화된 사실을 꾸준히 쌓는 것입니다.",
|
||||
"워드프레스는 이것을 기본으로 해주지만, 플러그인과 테마를 통제하지 못하면 장점이 전부 단점으로 뒤집힙니다."],
|
||||
section="Summary")
|
||||
a, b, c = s.cards3(y=s.band_y)
|
||||
a.chip("결론", "good").title("병원 납품 사이트").text("비개발자가 FAQ 페이지를 직접 늘려야 합니다. 워드프레스 + 경량 테마 + 단일 스키마 플러그인 + 관리형 호스팅.").divider().foot("운영 편의 우선")
|
||||
b.chip("결론", "info").title("자체 제품 랜딩").text("Astro 또는 프리렌더링된 React. 코드로 스키마와 헤더를 완전히 제어하고, 성능을 최고 수준으로 유지합니다.").divider().foot("제어·성능 우선")
|
||||
c.chip("결론", "violet").title("확장 단계").text("워드프레스를 헤드리스 CMS로 두고 Astro가 빌드 시 가져와 정적 배포. 운영 편의와 제어를 둘 다 확보합니다.").divider().foot("프로덕트화 대비")
|
||||
|
||||
# 03 실측 (다크)
|
||||
s = d.slide(eyebrow="Why It Matters · INFINITH 실측 2026. 08. 28",
|
||||
title="JS만 있는 사이트는 ", title_accent="AI에게 안 보인다",
|
||||
lede="INFINITH 랜딩을 GPTBot, ClaudeBot, PerplexityBot, Googlebot, Yeti로 직접 요청해 받은 본문 단어 수입니다.",
|
||||
dark=True, section="Evidence")
|
||||
s.stats3([("4 → 810", "프리렌더 전후 크롤러가 읽는 본문 단어 수. 전에는 title 4단어가 전부였습니다."),
|
||||
("1.5 MB", "본문을 만들기 위해 실행해야 했던 JS 번들. GPTBot, ClaudeBot은 JS를 실행하지 않습니다."),
|
||||
("0 → 3", "JSON-LD 스키마 수. Organization, SoftwareApplication, WebSite를 추가했습니다.")])
|
||||
|
||||
# 04 요구조건
|
||||
s = d.slide(eyebrow="What GEO / AEO Requires",
|
||||
title="플랫폼을 고르기 전에 ", title_accent="세 가지 조건",
|
||||
lede="어떤 도구를 쓰든 이 세 조건을 통과해야 AI 답변에 인용됩니다. 플랫폼 비교는 이 기준으로만 합니다.",
|
||||
section="Requirements")
|
||||
a, b, c = s.cards3(y=s.band_y)
|
||||
a.icon_square().title("크롤러 가독성").text("서버가 완성된 HTML을 내려줘야 합니다. 본문, 제목 구조, 링크가 JS 실행 없이 응답에 포함되어야 합니다.").divider().foot("SSR 또는 SSG")
|
||||
b.icon_square().title("구조화 데이터").text("MedicalClinic, Physician, FAQPage, Article JSON-LD가 페이지마다 정확히 하나씩. 중복과 충돌이 없어야 합니다.").divider().foot("Schema.org 단일 소스")
|
||||
c.icon_square().title("콘텐츠 생산량").text("질문과 답변 쌍을 수십에서 수백 페이지 쌓아야 합니다. 비개발자가 매주 추가할 수 있는 구조여야 합니다.").divider().foot("운영 지속성")
|
||||
|
||||
# 05 장점 1
|
||||
s = d.slide(eyebrow="Advantages · 1 / 2",
|
||||
title="크롤러 가독성은 ", title_accent="기본으로 통과",
|
||||
lede="PHP가 완성된 HTML을 내려주므로 별도 작업 없이 본문이 크롤러에 노출됩니다.",
|
||||
section="Advantages")
|
||||
a, b, c = s.cards3(y=s.band_y)
|
||||
a.chip("장점", "good").title("서버 렌더링 기본").text("GPTBot, ClaudeBot, PerplexityBot은 JS를 실행하지 않습니다. 워드프레스 응답에는 본문이 그대로 들어 있습니다. SPA는 빈 div만 보입니다.").divider().foot("추가 작업 0")
|
||||
b.chip("장점", "good").title("스키마 생태계").text("Rank Math, Yoast, Schema Pro가 MedicalClinic, FAQPage, LocalBusiness JSON-LD를 UI로 생성합니다. robots.txt와 llms.txt도 파일 편집으로 즉시 반영됩니다.").divider().foot("비개발자 운영 가능")
|
||||
c.chip("장점", "good").title("콘텐츠 생산 속도").text("코 재수술 비용, 눈매교정 회복 기간 같은 질문 페이지를 마케팅 담당자가 에디터에서 직접 추가합니다. ACF로 구조화 필드도 관리됩니다.").divider().foot("주 단위 발행")
|
||||
|
||||
# 06 장점 2
|
||||
s = d.slide(eyebrow="Advantages · 2 / 2",
|
||||
title="락인 없이 ", title_accent="오픈웹에 남는다",
|
||||
lede="지금 워드프레스를 골라도 나중에 프론트를 바꿀 수 있고, 네이버와 달리 AI 크롤러가 접근할 수 있는 소유 자산이 됩니다.",
|
||||
section="Advantages")
|
||||
L, R = s.cards2(y=s.band_y)
|
||||
L.chip("장점", "good").title("헤드리스 전환 경로").rows([
|
||||
("REST API / WPGraphQL", "콘텐츠를 그대로 꺼내 Astro, Next.js 프론트에 연결할 수 있습니다."),
|
||||
("빌드 시 정적화", "프론트를 바꾸면 성능과 스키마 제어를 코드 수준으로 가져옵니다."),
|
||||
("이전 비용 낮음", "글, 이미지, 카테고리 구조가 DB에 남아 마이그레이션이 단순합니다."),
|
||||
])
|
||||
R.chip("장점", "good").title("오픈웹 소유 자산").rows([
|
||||
("네이버 자산은 차단됨", "네이버 블로그, 플레이스, 카페는 robots.txt로 AI 크롤러를 전면 차단합니다."),
|
||||
("자체 도메인은 열림", "워드프레스 사이트는 GPTBot, ClaudeBot이 읽고 인용할 수 있는 유일한 소유 표면입니다."),
|
||||
("브랜드 신호 축적", "인용될수록 브랜드 언급이 쌓여 다음 답변에도 다시 인용됩니다."),
|
||||
])
|
||||
|
||||
# 07 단점 1
|
||||
s = d.slide(eyebrow="Disadvantages · 1 / 2",
|
||||
title="통제 못하면 ", title_accent="성능과 HTML이 무너진다",
|
||||
lede="테마, 페이지빌더, 플러그인이 쌓이면 크롤러가 읽는 HTML이 지저분해지고 로딩이 느려집니다.",
|
||||
section="Disadvantages")
|
||||
a, b, c = s.cards3(y=s.band_y)
|
||||
a.chip("단점", "critical").title("성능 저하가 기본값").text("페이지빌더와 플러그인 20개면 LCP 4초를 넘기기 쉽습니다. AI Overview는 검색 순위 기반이라 간접 영향이 큽니다.").divider().foot("대책: 경량 테마 + 캐시")
|
||||
b.chip("단점", "critical").title("HTML 노이즈").text("Elementor, Divi가 만드는 div 중첩 10단계와 인라인 스타일은 AI가 답변 단락을 뽑을 때 노이즈입니다. h2 질문, p 답변 구조가 깨집니다.").divider().foot("대책: Gutenberg만 사용")
|
||||
c.chip("단점", "critical").title("스키마 중복 충돌").text("Yoast, 테마 내장, FAQ 플러그인이 각자 JSON-LD를 뿌려 Organization이 3개 나오는 경우가 흔합니다. Rich Results 검사에서 오류가 납니다.").divider().foot("대책: 스키마 소스 1개")
|
||||
|
||||
# 08 단점 2
|
||||
s = d.slide(eyebrow="Disadvantages · 2 / 2",
|
||||
title="보안, 제어, ", title_accent="한국 시장 커버리지",
|
||||
lede="병원 사이트는 해킹 피해가 브랜드 오염으로 이어지고, 국내 AI 답변은 네이버 자산을 우선 인용합니다.",
|
||||
section="Disadvantages")
|
||||
a, b, c = s.cards3(y=s.band_y)
|
||||
a.chip("단점", "warning").title("보안·유지보수 부담").text("플러그인 취약점으로 해킹당하면 스팸 페이지가 대량 생성되고, 이것이 AI 크롤러에 잡혀 브랜드가 오염됩니다.").divider().foot("대책: 관리형 호스팅")
|
||||
b.chip("단점", "warning").title("세밀한 제어 한계").text("페이지별 robots 메타, 크롤러별 응답 분기, Edge 캐싱 정책은 커스텀 코드나 Cloudflare에서 처리해야 합니다.").divider().foot("Astro보다 손이 많이 감")
|
||||
c.chip("단점", "warning").title("한국 시장 특수성").text("네이버 서치어드바이저 등록과 사이트맵 제출을 별도로 해야 하고, 네이버 AI 브리핑은 네이버 자산을 우선 인용합니다.").divider().foot("글로벌 AI에 강함")
|
||||
|
||||
# 09 대안 비교 (다크)
|
||||
s = d.slide(eyebrow="Alternatives",
|
||||
title="네 가지 선택지, ", title_accent="같은 기준으로",
|
||||
lede="크롤러 가독성, 운영, 스키마 제어, 성능, 유지보수, 비용. INFINITH 현재 스택(Vite SPA)을 기준선으로 넣었습니다.",
|
||||
dark=True, section="Alternatives")
|
||||
w = s.card_wide(y=s.band_y)
|
||||
w.grid(["기준", "워드프레스", "Astro / Next.js SSG", "Webflow / Framer", "Vite SPA (INFINITH 기존)"],
|
||||
[["AI 크롤러 가독성", "좋음", "최고", "좋음", "나쁨"],
|
||||
["비개발자 콘텐츠 운영", "최고", "나쁨", "좋음", "불가"],
|
||||
["스키마 제어", "플러그인 의존", "코드로 완전 제어", "제한적", "코드로 완전 제어"],
|
||||
["성능", "관리 필요", "최고", "좋음", "좋음"],
|
||||
["유지보수", "부담 있음", "낮음", "낮음", "낮음"],
|
||||
["비용", "낮음", "낮음", "월 구독", "낮음"]],
|
||||
col_w=[3.5, 3.3, 3.3, 3.3, 3.3], row_h=0.66,
|
||||
tones=[[None, "good", "good", "good", "critical"],
|
||||
[None, "good", "critical", "good", "critical"],
|
||||
[None, "warning", "good", "warning", "good"],
|
||||
[None, "warning", "good", "good", "good"],
|
||||
[None, "warning", "good", "good", "good"],
|
||||
[None, "good", "good", "warning", "good"]])
|
||||
|
||||
# 10 추천 구성
|
||||
s = d.slide(eyebrow="Recommendation",
|
||||
title="용도에 따라 ", title_accent="세 가지 구성",
|
||||
lede="한 가지 정답은 없습니다. 누가 콘텐츠를 추가하는지, 얼마나 제어가 필요한지로 정합니다.",
|
||||
section="Recommendation")
|
||||
a, b, c = s.cards3(y=s.band_y)
|
||||
a.icon_square().title("병원 클라이언트 납품").text("워드프레스 + GeneratePress 또는 Kadence + Rank Math 단일 스키마 + WP Rocket 캐시 + 관리형 호스팅. 클라이언트가 FAQ를 직접 추가합니다.").divider().foot("운영 편의 우선")
|
||||
b.icon_square().title("INFINITH 자체 랜딩").text("Astro 콘텐츠 + React 아일랜드. 현재 Vite SPA에는 프리렌더링을 붙여 크롤러 본문 4단어를 810단어로 이미 끌어올렸습니다.").divider().foot("제어·성능 우선")
|
||||
c.icon_square().title("하이브리드").text("워드프레스를 헤드리스 CMS로 두고 Astro가 빌드 시 가져와 정적 배포. 클라이언트 수가 늘어나면 이 구성이 프로덕트화에 유리합니다.").divider().foot("확장 단계")
|
||||
|
||||
# 11 운영 규칙
|
||||
s = d.slide(eyebrow="If WordPress · Operating Rules",
|
||||
title="워드프레스를 쓴다면 ", title_accent="다섯 가지 규칙",
|
||||
lede="이 규칙을 지키면 장점만 남고, 어기면 단점이 전부 살아납니다.",
|
||||
section="Rules")
|
||||
w = s.card_wide(y=s.band_y)
|
||||
w.bullets([
|
||||
"플러그인은 10개 이하. 하나 추가할 때마다 LCP와 HTML 크기를 다시 측정합니다.",
|
||||
"테마는 GeneratePress, Kadence 같은 경량 테마만. Elementor, Divi 같은 페이지빌더는 쓰지 않습니다.",
|
||||
"스키마 소스는 Rank Math 하나로 통일. 테마 내장 스키마와 별도 FAQ 플러그인의 JSON-LD 출력은 끕니다.",
|
||||
"본문은 Gutenberg 블록으로 h2 질문, p 답변 구조를 유지합니다. AI는 이 단락 단위로 인용합니다.",
|
||||
"호스팅은 관리형(WP Engine, Kinsta, Cafe24 매니지드)으로. 보안 패치와 백업을 운영자가 직접 하지 않습니다.",
|
||||
], size=17.25, step=1.0)
|
||||
|
||||
# 12 클로징
|
||||
d.closing(eyebrow="Next Step",
|
||||
title_lines=["도구보다 조건이 먼저입니다."],
|
||||
lede="크롤러 가독성, 단일 스키마, 콘텐츠 생산 구조. 이 세 가지를 통과하는 쪽을 고릅니다.",
|
||||
cta_primary="병원용 워드프레스 표준 스택 문서화 →", cta_secondary="INFINITH 랜딩 FAQ 스키마 추가",
|
||||
contact="infinith-demo.vercel.app · team.backstageai@gmail.com")
|
||||
|
||||
out = "docs/deck/INFINITH_WordPress_GEO_AEO_장단점.pptx"
|
||||
d.save(out); print("saved", out)
|
||||
298
docs/prd/INFINITH_AI-Discovery_PRD_20260828_v02.md
Normal file
298
docs/prd/INFINITH_AI-Discovery_PRD_20260828_v02.md
Normal file
@ -0,0 +1,298 @@
|
||||
# INFINITH AI Discovery PRD · 분석 → 진단 → 빌드 → 측정
|
||||
|
||||
- 버전: v0.2 (2026.08.28) · 상태: Draft for Engineering Review
|
||||
- 작성: INFINITH Product · 대상: 프론트엔드 · 백엔드 · 인프라 · 콘텐츠 운영
|
||||
- 목표 릴리스: v1.0 · 10주 · 2026.11.06
|
||||
- 관련 코드: `AEO-GEO-Audit` 브랜치 (`src/data/aeoGeoRubric.ts`, `src/types/discovery.ts`, `src/lib/discoveryScore.ts`, `src/pages/DiscoveryReportPage.tsx`, `docs/AEO_GEO_RUBRIC.md`) · `Productize` 브랜치 (랜딩 프리렌더 커밋)
|
||||
- 덱 버전: `INFINITH_AI-Discovery_PRD_20260828_v02.pptx`
|
||||
|
||||
---
|
||||
|
||||
## 1. 제품 흐름
|
||||
|
||||
병원 URL 하나를 넣으면 채널 분석과 AEO · GEO 진단이 나오고, 진단 항목이 그대로 빌드 요구사항과 측정 지표가 되는 제품.
|
||||
|
||||
| 단계 | 내용 | 상태 |
|
||||
|---|---|---|
|
||||
| 1 분석 | 채널 발견 · 6채널 데이터 수집 · 마케팅 리포트 | 구현 완료 · `discover-channels` → `collect-channel-data` → `generate-report` |
|
||||
| 2 진단 | 36항목 AEO · GEO 스코어카드 · 등급 · 액션 플랜 | 프로토타입 동작 · 수동 채점 → 자동화 대상 |
|
||||
| 3 빌드 | 진단 P0 · P1을 사이트 수정 또는 표준 사이트로 실행 | 신규 · 진단 항목이 곧 요구사항 |
|
||||
| 4 측정 | 크롤러 로그 · GA4 AI 채널 · 재진단으로 점수 변화 증명 | 신규 · 루브릭 E 카테고리를 채운다 |
|
||||
|
||||
뷰성형외과는 1 · 2단계를 거쳐 P0 3건이 나왔다. 3 · 4단계가 없어 여기서 멈춘다. 그 공백이 이 PRD의 범위다.
|
||||
|
||||
## 2. 배경
|
||||
|
||||
AI 답변엔진은 JS를 실행하지 않는다. GPTBot, ClaudeBot, PerplexityBot은 서버가 내려준 HTML만 읽는다. 진단은 이 사실에서 출발한다.
|
||||
|
||||
- **실측 · 뷰성형외과: 읽히지만 라벨뿐.** AI 봇 12종 전부 200, JS 없이 본문 10,757자. 그런데 H1 5개가 전부 영문 라벨(VIEW SELFIE / VIEW EVENT …)이고 JSON-LD는 문법 오류(ItemList 블록 쉼표 누락)로 무효.
|
||||
- **시장 · 네이버 자산은 AI에 차단됨.** 블로그 · 플레이스 · 카페는 robots.txt로 AI 크롤러를 차단한다. 인용되려면 오픈웹 소유 자산이 필요하다.
|
||||
- **실측 · INFINITH 랜딩: 4단어.** 크롤러 UA 요청 결과 title 4단어 외 본문 0. 1.5 MB JS를 실행해야 본문이 나왔다. 프리렌더 후 810단어.
|
||||
|
||||
**문제 정의.** 진단은 나오는데 다음이 없다. 진단 결과를 받은 병원이 무엇을 고쳐야 하는지, 고친 뒤 점수가 올랐는지 증명할 수단이 제품 안에 없다.
|
||||
|
||||
## 3. 케이스 · 뷰성형외과 (2026.08.28)
|
||||
|
||||
카테고리 점수는 검증 항목 기준 정규화. 미검증(A6 · D6 · E1~E3)은 분모 제외.
|
||||
|
||||
| 카테고리 | 배점 | 획득 | 점수 | 핵심 발견 | → 빌드 · 측정 요구사항 |
|
||||
|---|---|---|---|---|---|
|
||||
| A 크롤러 접근성 | 20 | 15.7 / 17 | 92% | robots · SSR · HTTPS 정상. sitemap lastmod 15개월 전 | FR-B05 sitemap 자동 갱신 |
|
||||
| B 엔티티 구조화 | 20 | 6.0 / 20 | 30% | JSON-LD 깨짐 · MedicalClinic 없음 · Physician 없음 | FR-B03 스키마 단일 소스 |
|
||||
| C 인용 가능 콘텐츠 | 25 | 5.0 / 25 | 20% | 정의문 · FAQ · 서술형 H1 없음. 수치 부족 | FR-B02 질문 템플릿 · FR-B06 검사 |
|
||||
| D 인용 표면 | 20 | 13.7 / 19 | 72% | 강남언니 19,316건 강함. GBP 전화 없음 · NAP 3종 | FR-B08 NAP 통일 체크리스트 |
|
||||
| E 측정 인프라 | 10 | 0 / 2 | 0% | 로그 · GA4 · GSC 오너 접근 필요. 기준선 없음 | FR-M01 ~ M03 |
|
||||
| F 기술 위생 | 5 | 1.7 / 5 | 33% | WordPress 5.5.3 · alt 누락 · 페이지 무게 | NFR-04 · FR-B09 CMS 업그레이드 |
|
||||
|
||||
종합 48점 · C등급. P0 3건: JSON-LD 수정 + MedicalClinic 삽입, GBP 전화 등록 + NAP 통일, 측정 인프라 설치.
|
||||
|
||||
> 점수 확인 필요: 현재 `discovery_viewclinic.ts` 레벨 데이터로 재계산한 값이 48점. 리포트 페이지 표시값과 대조할 것.
|
||||
|
||||
## 4. 목표와 지표
|
||||
|
||||
출시 10주 후 기준. 모든 지표는 제품 안에서 자동 측정된다.
|
||||
|
||||
| 단계 | 지표 | 현재 | v1.0 목표 | 측정 방법 |
|
||||
|---|---|---|---|---|
|
||||
| 진단 | 자동 채점 항목 비율 | 0% (36항목 수동) | 24 / 36 자동 (67%) | `discover-aeo-geo` 실행 로그 |
|
||||
| 진단 | URL 입력 → 리포트 소요 시간 | 약 4시간 (수동) | 10분 이내 | `analysis_runs` 타임스탬프 |
|
||||
| 진단 | 재진단 재현성 | 미검증 | 동일 URL 2회 점수 편차 3점 이내 | 회귀 테스트 |
|
||||
| 빌드 | P0 액션 완료 → 재진단 점수 상승 | 측정 불가 | 뷰성형외과 48 → 65+ (B) | 재진단 비교 |
|
||||
| 빌드 | 질문 페이지 발행 (표준 사이트) | 0 | 8주 내 40개 | CMS 발행 로그 |
|
||||
| 측정 | AI 크롤러 · AI 유입 계측 병원 | 0 | 파일럿 100% | 로그 파서 · GA4 |
|
||||
|
||||
답변엔진 언급률은 지표에서 제외한다. 소비자 UI 자동 조회는 약관 위반이고 단발 조회는 무효다.
|
||||
|
||||
## 5. 범위
|
||||
|
||||
**In scope (v1.0)**
|
||||
- 진단 Edge Function `discover-aeo-geo` (자동 채점 1 · 2 · 3단계)
|
||||
- 수동 항목 입력 슬롯 (A6 · D1 · D2 · D4 · E1~E4)과 미검증 처리
|
||||
- 진단 리포트 DB 저장 · `/discovery/:id` 실데이터 연결 · PDF
|
||||
- 빌드 A: 기존 사이트 수정 패키지 (스키마 · NAP · CMS · FAQ 삽입)
|
||||
- 빌드 B: 병원용 워드프레스 표준 사이트 (템플릿 4종 · 검사)
|
||||
- 측정: 크롤러 로그 파서 · GA4 AI 채널 · 재진단 비교 뷰
|
||||
- INFINITH 랜딩 파일럿: FAQ 섹션 · FAQPage 스키마
|
||||
|
||||
**Out of scope (v1.0)**
|
||||
- 분석 파이프라인 변경 (`discover` · `collect` · `generate-report`)
|
||||
- Q&A 콘텐츠 작성 (운영팀 · 병원 담당, 별도 가이드)
|
||||
- 네이버 블로그 · 플레이스 운영 (AI 크롤러 차단 자산)
|
||||
- 답변엔진 UI 자동 조회 · 언급률 리포트 (약관 위반)
|
||||
- llms.txt 납품 (Google 효과 0 · 채택률 0.13%)
|
||||
- 헤드리스(WordPress + Astro) 전환 (v2.0 후보)
|
||||
|
||||
빌드 A와 B를 나눈 이유: 뷰성형외과처럼 사이트가 있는 병원은 수정이고, 없는 병원은 표준 사이트다.
|
||||
|
||||
## 6. 사용자
|
||||
|
||||
| 구분 | 사용자 | 성공 기준 |
|
||||
|---|---|---|
|
||||
| Primary | 병원 마케팅 담당자 | URL을 넣고 10분 안에 스코어카드와 P0를 본다. 수정 후 재진단으로 점수 변화를 확인한다. 표준 사이트면 질문 페이지를 직접 추가한다 |
|
||||
| Secondary | INFINITH 운영팀 | 수동 항목(GBP · 강남언니 · 로그)을 입력하고 여러 병원의 점수 · 크롤 수 · 오류를 한 화면에서 본다. 병원 온보딩 1일 |
|
||||
| System | AI 크롤러 · 검색엔진 | GPTBot · ClaudeBot · PerplexityBot · Googlebot · Yeti. 진단은 이들의 UA로 실제 요청해 채점한다. 빌드 결과도 같은 방법으로 검증한다 |
|
||||
|
||||
설계 원칙: 확인 못 한 것은 감점하지 않는다(미검증은 분모 제외). 근거 없는 것은 팔지 않는다. 담당자의 실수는 시스템이 발행 전에 막는다.
|
||||
|
||||
## 7. 기능 요구사항
|
||||
|
||||
### 7.1 진단 (FR-D)
|
||||
|
||||
AEO-GEO-Audit 브랜치의 루브릭 · 타입 · 집계 로직을 그대로 쓰고, 채점 데이터만 수동에서 자동으로 바꾼다.
|
||||
|
||||
| ID | 요구사항 | 상세 | 루브릭 | 우선순위 |
|
||||
|---|---|---|---|---|
|
||||
| FR-D01 | 자동 채점 1단계 | robots 파싱 · 12개 AI 봇 UA GET · HTML 파서 (`crawler_audit.py` 이식) | A1~A5 · A7 · B1 · B4 · B6 · B7 · C2 · C3 · C5 · C6 · F1~F4 | P0 |
|
||||
| FR-D02 | 자동 채점 2단계 | 본문 텍스트를 Perplexity 또는 Claude에 투입해 구조화 판정 | B2 · B3 · C1 · C4 · C7 · C8 | P0 |
|
||||
| FR-D03 | 자동 채점 3단계 | 검색 그라운딩 도메인 집계 · Wikidata API | D3 · D6 | P1 |
|
||||
| FR-D04 | 수동 입력 슬롯 | 운영팀이 근거 · 레벨 입력. 미입력은 `unverified`로 분모 제외 | A6 · D1 · D2 · D4 · E1~E4 | P0 |
|
||||
| FR-D05 | 결과 저장 · 리포트 | `DiscoveryResult`를 DB 저장. `/discovery/:id`가 mock 대신 DB를 읽음 | 전체 | P0 |
|
||||
| FR-D06 | 액션 플랜 생성 | level 0 · 1 항목에서 P0 · P1 · P2 액션 자동 생성. 빌드 FR과 연결 | 전체 | P1 |
|
||||
| FR-D07 | 재진단 비교 | 같은 URL의 이전 결과와 항목별 diff · 점수 변화 표시 | 전체 | P1 |
|
||||
|
||||
1단계만으로 36항목 중 17개가 자동이다. 2단계까지 23개. 나머지는 오너 계정 접근이 필요해 수동이 맞다.
|
||||
|
||||
### 7.2 빌드 A · 기존 사이트 수정 (FR-B, P0)
|
||||
|
||||
뷰성형외과 액션 플랜에서 역산했다. 병원 개발사나 INFINITH가 붙여 넣을 수 있는 산출물이어야 한다.
|
||||
|
||||
| ID | 요구사항 | 상세 | 루브릭 | 우선순위 |
|
||||
|---|---|---|---|---|
|
||||
| FR-B01 | 서버 렌더링 검증 · 처방 | A3 실패 시 프리렌더 · SSR 처방 (INFINITH 랜딩 방식 문서) | A3 | P0 |
|
||||
| FR-B03 | 스키마 패키지 생성 | 진단 데이터로 MedicalClinic · Physician JSON-LD 생성. 기존 스키마 차단 안내 | B1 · B2 · B3 · B5 | P0 |
|
||||
| FR-B08 | NAP 통일 체크리스트 | 홈페이지 · 강남언니 · GBP · 플레이스 표기를 나열하고 정본 지정 | B4 · D2 · D4 | P0 |
|
||||
| FR-B04 | 메타 패키지 | title · description · canonical · OG · hreflang 코드 생성 | B6 · B7 | P1 |
|
||||
| FR-B05 | robots · sitemap 처방 | AI 봇 허용 robots.txt · lastmod 갱신 규칙 | A1 · A5 | P1 |
|
||||
| FR-B07 | FAQ 삽입 패키지 | 시술별 질문 · 요약 답변 초안 + FAQPage 스키마 코드 | C1 · C2 · C4 | P1 |
|
||||
| FR-B09 | CMS 위생 처방 | WordPress 버전 · alt 누락 목록 · 페이지 무게 리포트 | F1 · F2 · F3 | P1 |
|
||||
|
||||
빌드 A의 산출물은 코드 조각과 체크리스트다. 병원 사이트를 INFINITH가 직접 호스팅하지 않는다.
|
||||
|
||||
### 7.3 빌드 B · 표준 사이트 (FR-B, P1)
|
||||
|
||||
루브릭 A · B · C · F를 처음부터 통과하는 워드프레스 사이트. 담당자가 질문 페이지를 직접 늘린다.
|
||||
|
||||
| ID | 요구사항 | 상세 | 루브릭 | 우선순위 |
|
||||
|---|---|---|---|---|
|
||||
| FR-B10 | 표준 스택 프리셋 | 경량 테마 · Rank Math · WP Rocket · ACF · 관리형 호스팅. 1회 명령 복제 | A · F 전체 | P1 |
|
||||
| FR-B02 | 질문 페이지 템플릿 | 질문(h1) · 요약 답변(p, 60자) · 상세 · 관련 시술 · 의료진. 필드 입력만으로 생성 | C1 · C2 · C3 · C8 | P1 |
|
||||
| FR-B11 | 시술 · 의료진 템플릿 | MedicalProcedure · Physician 스키마 포함. 가격대 · 시간 · 회복 필드 | B3 · C4 · C7 | P1 |
|
||||
| FR-B12 | 스키마 단일 소스 | 페이지 유형별 JSON-LD 1개 자동 출력. 테마 · 타 플러그인 출력 차단 | B1 · B2 · B3 | P1 |
|
||||
| FR-B06 | 발행 전 검사 | 스키마 중복 · h1 2개 · alt 누락 · 요약 답변 누락 시 발행 차단 | B1 · C3 · F1 | P1 |
|
||||
| FR-B13 | sitemap · robots 자동 | 발행 시 lastmod 갱신 · AI 봇 9종 허용 | A1 · A5 · C6 | P1 |
|
||||
|
||||
빌드 B는 P1이다. 뷰성형외과처럼 사이트가 있는 병원이 먼저이므로 빌드 A가 P0다.
|
||||
|
||||
### 7.4 측정 (FR-M)
|
||||
|
||||
인용 여부를 직접 볼 수 없으므로 소유한 신호를 계측한다. 재진단이 개선의 증거다.
|
||||
|
||||
| ID | 요구사항 | 상세 | 루브릭 | 우선순위 |
|
||||
|---|---|---|---|---|
|
||||
| FR-M01 | AI 크롤러 로그 파서 | 액세스 로그 또는 Cloudflare Logpush에서 GPTBot · ClaudeBot · PerplexityBot 히트를 IP 검증 후 일 집계 | E1 | P0 |
|
||||
| FR-M02 | GA4 AI 채널 | chatgpt.com · perplexity.ai · gemini.google.com 리퍼러를 커스텀 채널로 분리 · 설정 가이드 | E2 | P0 |
|
||||
| FR-M03 | 기준선 스팟체크 기록 | 분기 반복 측정 프로토콜과 입력 슬롯 (수동) | E4 | P1 |
|
||||
| FR-M04 | 재진단 스케줄 | 월 1회 자동 재진단 · 점수 변화 알림 | 전체 | P1 |
|
||||
| FR-M05 | 운영 대시보드 | 병원별 점수 · 크롤 수 · AI 세션 · 스키마 오류 · LCP 주간 표시 | E 전체 | P1 |
|
||||
|
||||
FR-M01 · M02는 뷰성형외과 P0 3번(측정 인프라 설치)이다. 빌드보다 먼저 깔아야 전후 비교가 된다.
|
||||
|
||||
## 8. 비기능 요구사항
|
||||
|
||||
| ID | 영역 | 요구사항 | 예산 · 기준 |
|
||||
|---|---|---|---|
|
||||
| NFR-01 | 진단 | 재현성 | 동일 URL 2회 실행 점수 편차 3점 이내 · LLM 판정 항목은 temperature 0 |
|
||||
| NFR-02 | 진단 | 소요 시간 · 비용 | URL당 10분 이내 · API 비용 2,000원 이내 |
|
||||
| NFR-03 | 진단 | 대상 사이트 부하 | UA 테스트 요청 30회 이내 · 1초 간격 · 429 시 중단 |
|
||||
| NFR-04 | 빌드 B | 성능 | 모바일 LCP p75 2.5초 · CLS 0.1 · 플러그인 10개 이하 · HTML 80 KB 이하 |
|
||||
| NFR-05 | 빌드 B | HTML 품질 · 보안 | h1 1개 · div 중첩 6단계 이하 · 관리형 호스팅 · 자동 업데이트 · 일 백업 |
|
||||
| NFR-06 | 측정 | 로그 보존 · 정확도 | 90일 보존 · 봇 IP 역DNS 검증 (위장 UA 제외) |
|
||||
| NFR-07 | 공통 | 정직성 규약 | 리포트에 "하지 않을 것" 섹션 고정 · 근거 등급(실측 · 학술 · 벤더 · 추정) 표기 |
|
||||
|
||||
Elementor · Divi 같은 페이지빌더는 NFR-04 · 05를 통과하지 못하므로 빌드 B에서 금지다.
|
||||
|
||||
## 9. 아키텍처
|
||||
|
||||
Supabase Edge Functions 체인. 분석 3개 함수는 그대로, 신규 4개를 추가한다.
|
||||
|
||||
| 단계 | 함수 · 컴포넌트 | 비고 |
|
||||
|---|---|---|
|
||||
| 1 분석 | `discover-channels` → `collect-channel-data` → `generate-report` | 기존 · `marketing_reports` · `clinic_registry` |
|
||||
| 2 진단 | `discover-aeo-geo`: UA 테스트 · HTML 파서 · LLM 판정 · 집계 | 신규 · 루브릭 · 집계는 AEO-GEO-Audit 코드 이식 |
|
||||
| 3 빌드 | `build-package`: 스키마 · 메타 · NAP · FAQ 코드 생성 / WP 커스텀 플러그인 | 신규 · 빌드 A는 함수, B는 플러그인 |
|
||||
| 4 측정 | `log-ingest` (Logpush → Supabase) · GA4 커넥터 · 재진단 cron | 신규 · `discovery_results` 시계열 |
|
||||
|
||||
신규 테이블 2개: `discovery_results` (결과 · 버전) · `crawler_hits` (봇 · IP · 경로 · 일자).
|
||||
|
||||
## 10. 루브릭 → 빌드 매핑 (FR-D06 규칙표)
|
||||
|
||||
| 루브릭 항목 | level 0 · 1이면 | 빌드 산출물 | FR |
|
||||
|---|---|---|---|
|
||||
| A3 서버 렌더링 본문 | 본문이 JS에만 있음 | 프리렌더 · SSR 처방 문서 + 검증 스크립트 | FR-B01 |
|
||||
| B1 · B2 · B3 스키마 | JSON-LD 깨짐 · 엔티티 없음 | MedicalClinic · Physician JSON-LD 코드 + 차단 안내 | FR-B03 · B12 |
|
||||
| B4 · D2 · D4 NAP | 표면 간 표기 불일치 | 정본 NAP 체크리스트 · GBP 수정 항목 | FR-B08 |
|
||||
| C1 · C2 · C4 콘텐츠 | 정의문 · FAQ · 수치 없음 | 시술별 질문 · 요약 답변 초안 + FAQPage 코드 | FR-B07 · B02 |
|
||||
| C3 제목 계층 | H1 없음 · 이미지 텍스트 | H1 재설계안 · 이미지 정보 텍스트화 목록 | FR-B07 |
|
||||
| C6 신선도 | dateModified 없음 | dateModified 활성화 · 갱신 주기 규칙 | FR-B13 |
|
||||
| E1 · E2 측정 | 미설치 | 로그 파서 · GA4 채널 설정 | FR-M01 · M02 |
|
||||
| F2 CMS | 구버전 | 업그레이드 절차 · 백업 체크리스트 | FR-B09 |
|
||||
|
||||
## 11. 스키마 스펙
|
||||
|
||||
빌드 A는 코드 조각으로, 빌드 B는 Rank Math 훅으로 같은 스펙을 출력한다. 테마 내장 스키마는 끈다.
|
||||
|
||||
| 페이지 | @type | 필수 속성 | 데이터 소스 |
|
||||
|---|---|---|---|
|
||||
| 홈 | MedicalClinic + WebSite | name · address · telephone · openingHours · sameAs | 분석 1단계 `clinics` · 병원 정보 ACF |
|
||||
| 시술 | MedicalProcedure | name · procedureType · bodyLocation · howPerformed · preparation | 시술 ACF |
|
||||
| 의료진 | Physician | name · medicalSpecialty · memberOf · image · alumniOf | 의료진 ACF · 강남언니 의료진 |
|
||||
| 질문 | FAQPage (+ Article) | mainEntity[Question · acceptedAnswer] · author · dateModified | 질문 템플릿 필드 |
|
||||
| 공통 | BreadcrumbList | itemListElement | 자동 |
|
||||
|
||||
수용 기준: Rich Results Test 오류 0. 응답에 ld+json 블록 정확히 1개 (Breadcrumb 포함 시 @graph 병합). 루브릭 B1 · B2 · B3 level 3.
|
||||
|
||||
## 12. 질문 페이지 템플릿
|
||||
|
||||
루브릭 C1 · C2 · C3 · C4 · C8을 동시에 통과하는 구조.
|
||||
|
||||
| 필드 | HTML 출력 | 제약 | 예시 | 루브릭 |
|
||||
|---|---|---|---|---|
|
||||
| 질문 | h1 | 물음표로 끝남 · 40자 이내 | 코 재수술은 언제 가능한가요? | C3 |
|
||||
| 요약 답변 | p (h1 직후) | 60자 이내 · 숫자 · 기간 포함 권장 | 1차 수술 후 최소 6개월, 보통 1년 뒤 가능합니다. | C1 · C8 |
|
||||
| 상세 답변 | h2 + p 반복 | h2는 하위 질문형 · 단락 3문장 이내 | 왜 6개월을 기다려야 하나요? | C3 · C8 |
|
||||
| 가격 · 시간 · 회복 | 표 | 숫자 범위 필수 | 300~500만원 · 2시간 · 회복 2주 | C4 · C5 |
|
||||
| 관련 시술 · 의료진 | 링크 + Physician | 각 1개 이상 | 코 재수술 · 김OO 원장 | C7 |
|
||||
| 갱신일 | dateModified | 자동 | 2026-08-28 | C6 |
|
||||
|
||||
요약 답변 60자는 AI 답변엔진이 한 단락으로 인용하는 길이다. 이 필드가 비면 발행이 막힌다(FR-B06).
|
||||
|
||||
## 13. 수용 테스트
|
||||
|
||||
| ID | 단계 | 테스트 | 통과 기준 |
|
||||
|---|---|---|---|
|
||||
| AT-01 | 진단 | viewclinic.com 자동 진단 실행 | 수동 채점 대비 자동 항목 일치율 90% · 소요 10분 이내 |
|
||||
| AT-02 | 진단 | infinith-demo.vercel.app 진단 | A3 level 3 · B1 level 3 · 재실행 편차 3점 이내 |
|
||||
| AT-03 | 진단 | 미검증 처리 | 수동 슬롯 비운 채 실행 시 해당 항목 분모 제외 · 점수 계산 일치 |
|
||||
| AT-04 | 빌드 A | 스키마 패키지 적용 후 Rich Results Test | 오류 0 · 블록 1개 |
|
||||
| AT-05 | 빌드 B | `curl -A GPTBot` 5종 UA 본문 단어 수 | 300단어 이상 · 5개 UA 동일 · 429 · 403 0건 |
|
||||
| AT-06 | 빌드 B | 발행 검사 | 요약 답변 비운 채 발행 시 차단 + 안내 |
|
||||
| AT-07 | 측정 | 로그 파서 위장 UA 테스트 | 역DNS 실패 히트 제외 · 일 집계 정확 |
|
||||
| AT-08 | 루프 | 빌드 후 재진단 | 변경 항목 level 상승이 diff에 표시 · 점수 상승 |
|
||||
|
||||
AT-02 · 05는 2026.08.28 INFINITH 랜딩 점검 스크립트를 그대로 쓴다.
|
||||
|
||||
## 14. 마일스톤 (10주)
|
||||
|
||||
2주 단위. 각 단계 끝에 수용 테스트를 통과해야 다음 단계로 간다.
|
||||
|
||||
| 주차 | 단계 | 내용 | 완료 기준 |
|
||||
|---|---|---|---|
|
||||
| W1-2 | 진단 1단계 | `discover-aeo-geo` · UA 테스트 · 파서 · DB · 리포트 연결 | FR-D01 · D04 · D05 · AT-01~03 |
|
||||
| W3-4 | 측정 · 진단 2단계 | 로그 파서 · GA4 · LLM 판정 · 재진단 diff | FR-M01 · M02 · D02 · D07 · AT-07 |
|
||||
| W5-6 | 빌드 A | 스키마 · NAP · FAQ · CMS 패키지 생성 | FR-B01 · B03 · B04 · B07 · B08 · AT-04 |
|
||||
| W7-8 | 빌드 B | 표준 스택 · 템플릿 3종 · 검사 · 플러그인 | FR-B02 · B06 · B10~B13 · AT-05 · 06 |
|
||||
| W9-10 | 파일럿 · 릴리스 | 뷰성형외과 빌드 A 적용 · 재진단 · 표준 사이트 1곳 | AT-08 · 48 → 65+ · v1.0 |
|
||||
|
||||
측정(W3-4)을 빌드보다 앞에 둔 이유: 전후 비교의 기준선이 빌드 전에 있어야 한다.
|
||||
|
||||
## 15. 리스크
|
||||
|
||||
| 리스크 | 영향 | 가능성 | 대응 |
|
||||
|---|---|---|---|
|
||||
| LLM 판정 항목(2단계)의 채점 불일치 | 높음 | 중간 | temperature 0 · 근거 인용 강제 · 수동 채점과 90% 일치 회귀 테스트 |
|
||||
| 대상 사이트가 진단 요청을 차단 | 중간 | 중간 | 요청 30회 · 1초 간격 · 429 시 중단 후 unverified 처리 |
|
||||
| 병원이 빌드 A 패키지를 적용하지 않음 | 높음 | 높음 | 적용 대행 옵션 · 재진단 알림 · P0만 먼저 |
|
||||
| 담당자가 질문 페이지를 추가하지 않음 | 높음 | 높음 | 분석 리포트의 질문 목록을 초안으로 제공 · 주간 발행 목표 |
|
||||
| AI 크롤러 UA · 정책 변경 | 중간 | 높음 | 봇 목록 중앙 관리 · 분기 점검 · 루브릭 버전 관리 |
|
||||
| 스키마 플러그인 업데이트로 출력 변경 | 중간 | 중간 | AT-04 매 배포 실행 · 버전 고정 |
|
||||
|
||||
## 16. 오픈 이슈 (리뷰 미팅 결정)
|
||||
|
||||
| # | 질문 | 선택지 | 기본안 · 근거 |
|
||||
|---|---|---|---|
|
||||
| 1 | 2단계 LLM 판정 모델 | Perplexity · Claude · 둘 다 | Claude. 판정 일관성 · 근거 인용. Perplexity는 D3 그라운딩에만 |
|
||||
| 2 | 로그 수집 방식 | 호스팅 로그 API · Cloudflare Logpush · WP 플러그인 | Cloudflare Logpush → Supabase. 호스팅 무관 |
|
||||
| 3 | 빌드 A 적용 주체 | 병원 개발사 · INFINITH 대행 · 둘 다 | 둘 다. 패키지는 개발사용, 대행은 유료 옵션 |
|
||||
| 4 | 빌드 B 호스팅 · 스키마 플러그인 | Kinsta · WP Engine · Cafe24 / Rank Math · Yoast | Kinsta + Rank Math. Cloudflare 통합 · 의료 타입 · 훅 |
|
||||
| 5 | 헤드리스 전환 시점 | 병원 5곳 · 10곳 · 미정 | 병원 5곳 시 v2.0 검토. 콘텐츠는 WP DB에 남아 이전 비용 낮음 |
|
||||
|
||||
결정되지 않은 항목은 기본안으로 W1을 시작하고, 리뷰에서 뒤집히면 W2에 반영한다.
|
||||
|
||||
## 부록 · 실측 (2026.08.28)
|
||||
|
||||
| 대상 | 항목 | 결과 | 방법 |
|
||||
|---|---|---|---|
|
||||
| 뷰성형외과 | 종합 · 등급 | 48점 · C (검증 31 / 36항목) | `crawler_audit.py` + curl + HTML 파서 · 수동 채점 |
|
||||
| 뷰성형외과 | 카테고리 | A 92 · B 30 · C 20 · D 72 · E 0 · F 33 | 루브릭 v1.0 · unverified 분모 제외 |
|
||||
| 뷰성형외과 | P0 | JSON-LD 수정 + MedicalClinic · GBP 전화 + NAP 통일 · 측정 인프라 | 액션 플랜 |
|
||||
| INFINITH 랜딩 | 크롤러 5종 본문 단어 | 4 → 810 | `vite build --ssr` + `renderToString` 프리렌더 |
|
||||
| INFINITH 랜딩 | JSON-LD · lang · 메타 | 0 → 3 · en → ko · description · OG 추가 | `index.html` 정적 삽입 |
|
||||
| INFINITH 랜딩 | robots · sitemap · llms.txt | HTML 반환 → text/plain · xml 정상 | `public/` 정적 파일 |
|
||||
|
||||
재현:
|
||||
|
||||
```bash
|
||||
curl -sS -A "Mozilla/5.0 (compatible; GPTBot/1.0)" https://infinith-demo.vercel.app/ \
|
||||
| sed -e 's/<script[^>]*>.*<\/script>//g' -e 's/<[^>]*>//g' | tr -s ' \n' | wc -w
|
||||
# 뷰성형외과 리포트: /discovery/viewclinic (AEO-GEO-Audit 브랜치)
|
||||
```
|
||||
BIN
docs/prd/INFINITH_AI-Discovery_PRD_20260828_v02.pdf
Normal file
BIN
docs/prd/INFINITH_AI-Discovery_PRD_20260828_v02.pdf
Normal file
Binary file not shown.
BIN
docs/prd/INFINITH_AI-Discovery_PRD_20260828_v02.pptx
Normal file
BIN
docs/prd/INFINITH_AI-Discovery_PRD_20260828_v02.pptx
Normal file
Binary file not shown.
412
docs/prd/build_geo_aeo_prd.py
Normal file
412
docs/prd/build_geo_aeo_prd.py
Normal file
@ -0,0 +1,412 @@
|
||||
"""GEO/AEO 최적화 병원 웹사이트 PRD — ADO2 AEO-GEO 경쟁분석 덱 포맷(13.333×7.5in, Pretendard, 333/666, 5D61D1/ADB0FF)."""
|
||||
from pptx import Presentation
|
||||
from pptx.util import Inches, Pt, Emu
|
||||
from pptx.dml.color import RGBColor
|
||||
from pptx.enum.shapes import MSO_SHAPE
|
||||
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
|
||||
from pptx.oxml.ns import qn
|
||||
from lxml import etree
|
||||
|
||||
INK, MUTED, DIM, WHITE, BLACK = "333333", "666666", "444444", "FFFFFF", "000000"
|
||||
VIO, VIO_LT, VIO_BG, GRAY_BG, ZEBRA, LINE = "5D61D1", "ADB0FF", "F3F3FF", "F0F0F0", "F7F8FA", "CCCCCC"
|
||||
FONT = "Pretendard"
|
||||
DATE = "2026.08.28"
|
||||
FOOT = f"INFINITH AI Discovery · 분석 → 진단 → 빌드 → 측정 PRD | {DATE}"
|
||||
M, CW = 0.44, 12.46
|
||||
|
||||
prs = Presentation(); prs.slide_width = Inches(13.333); prs.slide_height = Inches(7.5)
|
||||
BLANK = prs.slide_layouts[6]
|
||||
page = 0
|
||||
|
||||
def rgb(h): return RGBColor.from_string(h)
|
||||
def rect(s, x, y, w, h, fill=None, line=None):
|
||||
sh = s.shapes.add_shape(MSO_SHAPE.RECTANGLE, Inches(x), Inches(y), Inches(w), Inches(h))
|
||||
if fill: sh.fill.solid(); sh.fill.fore_color.rgb = rgb(fill)
|
||||
else: sh.fill.background()
|
||||
if line: sh.line.color.rgb = rgb(line); sh.line.width = Pt(0.75)
|
||||
else: sh.line.fill.background()
|
||||
sh.shadow.inherit = False
|
||||
return sh
|
||||
def tb(s, x, y, w, h, text, size, color=INK, bold=False, align=PP_ALIGN.LEFT, anchor=MSO_ANCHOR.TOP, spc=None, ls=1.1):
|
||||
t = s.shapes.add_textbox(Inches(x), Inches(y), Inches(w), Inches(h))
|
||||
tf = t.text_frame; tf.word_wrap = True; tf.vertical_anchor = anchor
|
||||
tf.margin_left = tf.margin_right = tf.margin_top = tf.margin_bottom = 0
|
||||
lines = text if isinstance(text, list) else [text]
|
||||
for i, ln in enumerate(lines):
|
||||
p = tf.paragraphs[0] if i == 0 else tf.add_paragraph()
|
||||
p.alignment = align; p.line_spacing = ls
|
||||
r = p.add_run(); r.text = ln
|
||||
r.font.name = FONT; r.font.size = Pt(size); r.font.bold = bold; r.font.color.rgb = rgb(color)
|
||||
if spc: r.font._element.set("spc", str(spc))
|
||||
return t
|
||||
|
||||
def chrome(s, eyebrow, title, lede=None):
|
||||
global page; page += 1
|
||||
tb(s, M, 0.31, 8, 0.19, eyebrow.upper(), 11.25, MUTED, True, spc=200)
|
||||
tb(s, M, 0.60, 11.85, 0.60, title, 30, INK, True)
|
||||
if lede: tb(s, M, 1.26, 11.85, 0.40, lede, 15.75, MUTED)
|
||||
rect(s, M, 1.73, CW, 0.02, BLACK)
|
||||
rect(s, M, 7.10, CW, 0.01, LINE)
|
||||
tb(s, M, 7.20, 6, 0.16, FOOT, 10.5, MUTED)
|
||||
tb(s, 12.4, 7.18, 0.5, 0.19, f"{page:02d}", 11.25, INK, True, PP_ALIGN.RIGHT)
|
||||
|
||||
def new(eyebrow, title, lede=None):
|
||||
s = prs.slides.add_slide(BLANK); chrome(s, eyebrow, title, lede); return s
|
||||
|
||||
def card(s, x, y, w, h, tag, head, body, bar=VIO_LT):
|
||||
rect(s, x, y, w, h, WHITE, LINE); rect(s, x, y, 0.07, h, bar)
|
||||
tb(s, x + 0.23, y + 0.19, w - 0.4, 0.17, tag, 11.25, bar if bar != BLACK else DIM, True)
|
||||
tb(s, x + 0.23, y + 0.45, w - 0.4, 0.42, head, 18, INK, True)
|
||||
tb(s, x + 0.23, y + 0.95, w - 0.4, h - 1.1, body, 13.5, MUTED, ls=1.2)
|
||||
|
||||
def cards3(s, items, y=2.10, h=1.88):
|
||||
w = 3.85; gap = (CW - 3 * w) / 2
|
||||
bars = [VIO_LT, DIM, VIO]
|
||||
for i, (tag, head, body) in enumerate(items):
|
||||
card(s, M + i * (w + gap), y, w, h, tag, head, body, bars[i % 3])
|
||||
|
||||
def band(s, y, h, label, big, small=None):
|
||||
rect(s, M, y, CW, h, BLACK)
|
||||
tb(s, M + 0.29, y + 0.27, 11.56, 0.23, label, 12.75, VIO_LT, True)
|
||||
tb(s, M + 0.29, y + 0.62, 11.56, 0.75, big, 18, WHITE, True, ls=1.15)
|
||||
if small: tb(s, M + 0.29, y + h - 0.42, 11.56, 0.23, small, 12.75, LINE)
|
||||
|
||||
def table(s, headers, rows, col_w, y=2.14, row_h=0.52, size=12.5, first_bold=True):
|
||||
n, m = len(rows) + 1, len(headers)
|
||||
gs = s.shapes.add_table(n, m, Inches(M), Inches(y), Inches(CW), Inches(row_h * n))
|
||||
t = gs.table
|
||||
tblPr = t._tbl.tblPr
|
||||
for a in ("bandRow", "firstRow"): tblPr.set(a, "0")
|
||||
st = tblPr.find(qn("a:tableStyleId"))
|
||||
if st is not None: tblPr.remove(st)
|
||||
for i, w in enumerate(col_w): t.columns[i].width = Inches(w)
|
||||
for r in range(n): t.rows[r].height = Inches(row_h)
|
||||
for r in range(n):
|
||||
for c in range(m):
|
||||
cell = t.cell(r, c)
|
||||
txt = headers[c] if r == 0 else rows[r - 1][c]
|
||||
cell.margin_left, cell.margin_right = Inches(0.10), Inches(0.08)
|
||||
cell.margin_top, cell.margin_bottom = Inches(0.05), Inches(0.05)
|
||||
cell.vertical_anchor = MSO_ANCHOR.MIDDLE
|
||||
cell.fill.solid()
|
||||
cell.fill.fore_color.rgb = rgb(BLACK if r == 0 else (ZEBRA if r % 2 == 0 else WHITE))
|
||||
tf = cell.text_frame; tf.word_wrap = True
|
||||
p = tf.paragraphs[0]; p.text = ""
|
||||
run = p.add_run(); run.text = txt
|
||||
run.font.name = FONT; run.font.size = Pt(size)
|
||||
run.font.bold = (r == 0) or (first_bold and c == 0)
|
||||
run.font.color.rgb = rgb(WHITE if r == 0 else INK)
|
||||
tcPr = cell._tc.get_or_add_tcPr()
|
||||
for side in ("lnL", "lnR", "lnT", "lnB"):
|
||||
ln = etree.SubElement(tcPr, qn(f"a:{side}"), w="6350")
|
||||
sf = etree.SubElement(ln, qn("a:solidFill")); etree.SubElement(sf, qn("a:srgbClr"), val=LINE)
|
||||
return t
|
||||
|
||||
def step_rows(s, items, y=2.10, h=0.81, gap=0.21):
|
||||
"""번호 밴드 리스트(레퍼런스 S3). items: (num, label, mid, right). 위에서 아래로 진해짐 반대: 첫 줄 검정."""
|
||||
fills = [BLACK, VIO_LT, VIO_LT, GRAY_BG, GRAY_BG, GRAY_BG]
|
||||
for i, (num, label, mid, right) in enumerate(items):
|
||||
f = fills[i]; fg = WHITE if f in (BLACK, VIO_LT) else INK
|
||||
yy = y + i * (h + gap)
|
||||
rect(s, M, yy, CW, h, f)
|
||||
tb(s, M + 0.18, yy + 0.20, 1.1, 0.4, num, 22, fg, True)
|
||||
tb(s, M + 1.35, yy + 0.25, 2.0, 0.3, label, 15, fg, True)
|
||||
tb(s, M + 3.4, yy + 0.25, 4.3, 0.3, mid, 13.5, fg)
|
||||
tb(s, M + 7.8, yy + 0.25, 4.5, 0.3, right, 13.5, fg, True)
|
||||
|
||||
def bullets(s, x, y, w, items, size=13.5, step=0.42, color=MUTED, dot=VIO):
|
||||
for i, t in enumerate(items):
|
||||
yy = y + i * step
|
||||
d = s.shapes.add_shape(MSO_SHAPE.OVAL, Inches(x), Inches(yy + 0.08), Inches(0.09), Inches(0.09))
|
||||
d.fill.solid(); d.fill.fore_color.rgb = rgb(dot); d.line.fill.background(); d.shadow.inherit = False
|
||||
tb(s, x + 0.22, yy, w - 0.22, step, t, size, color, ls=1.15)
|
||||
|
||||
def takeaway(s, text, y=6.30):
|
||||
tb(s, M, y, CW, 0.47, text, 16, VIO, True)
|
||||
|
||||
|
||||
# ───────────────────────────── 01 표지
|
||||
s = prs.slides.add_slide(BLANK); page += 1
|
||||
tb(s, M, 0.42, 6, 0.19, "PRODUCT REQUIREMENTS DOCUMENT · v0.2", 11.25, MUTED, True, spc=200)
|
||||
rect(s, M, 0.77, CW, 0.02, BLACK)
|
||||
tb(s, M, 1.40, 7.5, 1.56, ["AI Discovery PRD", "분석 · 진단 · 빌드 · 측정"], 40.5, INK, True, ls=1.05)
|
||||
tb(s, M, 3.46, 7.2, 0.73, ["병원 URL 하나를 넣으면 채널 분석과 AEO · GEO 진단이 나오고,", "진단 항목이 그대로 빌드 요구사항과 측정 지표가 되는 제품"], 18.75, MUTED, ls=1.2)
|
||||
for x, w, bg, num, cap, col in [
|
||||
(8.18, 2.14, VIO_BG, "36항목", "AEO · GEO 루브릭 v1.0", VIO_LT),
|
||||
(10.52, 2.14, GRAY_BG, "C · 48점", "뷰성형외과 실측 2026.08.28", VIO),
|
||||
]:
|
||||
rect(s, x, 1.51, w, 1.38, bg); tb(s, x + 0.18, 1.70, w - 0.36, 0.54, num, 29.25, col, True); tb(s, x + 0.18, 2.36, w - 0.36, 0.36, cap, 13, INK, True)
|
||||
rect(s, 8.18, 3.09, 4.48, 1.38, GRAY_BG); tb(s, 8.36, 3.28, 4.1, 0.54, "v1.0 · 10주", 29.25, DIM, True); tb(s, 8.36, 3.95, 4.1, 0.36, "목표 릴리스 · 2026.11.06", 13, INK, True)
|
||||
tb(s, M, 6.75, 6.5, 0.21, f"작성 INFINITH Product | {DATE} | 상태 Draft for Engineering Review", 12, INK, True)
|
||||
tb(s, 8.5, 6.75, 4.4, 0.21, "대상 · 프론트엔드 · 백엔드 · 인프라 · 콘텐츠 운영", 12, MUTED, align=PP_ALIGN.RIGHT)
|
||||
|
||||
# ───────────────────────────── 02 제품 흐름
|
||||
s = new("Product Flow", "URL 하나로 분석, 진단, 빌드, 측정까지 한 흐름",
|
||||
"1 · 2단계는 이미 동작한다. 이 PRD는 2단계를 자동화하고 3 · 4단계를 새로 만든다.")
|
||||
step_rows(s, [
|
||||
("4", "측정", "크롤러 로그 · GA4 AI 채널 · 재진단으로 점수 변화 증명", "신규 · 루브릭 E 카테고리를 채운다"),
|
||||
("3", "빌드", "진단 P0 · P1을 사이트 수정 또는 표준 사이트로 실행", "신규 · 진단 항목이 곧 요구사항"),
|
||||
("2", "진단", "36항목 AEO · GEO 스코어카드 · 등급 · 액션 플랜", "프로토타입 동작 · 수동 채점 → 자동화 대상"),
|
||||
("1", "분석", "채널 발견 · 6채널 데이터 수집 · 마케팅 리포트", "구현 완료 · discover → collect → generate-report"),
|
||||
], h=0.81, gap=0.21)
|
||||
takeaway(s, "뷰성형외과는 1 · 2단계를 거쳐 P0 3건이 나왔다. 3 · 4단계가 없어 여기서 멈춘다. 그 공백이 이 PRD의 범위다.", y=6.35)
|
||||
|
||||
# ───────────────────────────── 03 배경
|
||||
s = new("Background", "AI 답변엔진은 JS를 실행하지 않는다",
|
||||
"GPTBot, ClaudeBot, PerplexityBot은 서버가 내려준 HTML만 읽는다. 진단은 이 사실에서 출발한다.")
|
||||
cards3(s, [
|
||||
("실측", "01 뷰성형외과: 읽히지만 라벨뿐", "AI 봇 12종 전부 200, JS 없이 본문 10,757자. 그런데 H1 5개가 전부 영문 라벨이고 JSON-LD는 문법 오류로 무효."),
|
||||
("시장", "02 네이버 자산은 AI에 차단됨", "블로그 · 플레이스 · 카페는 robots.txt로 AI 크롤러를 차단한다. 인용되려면 오픈웹 소유 자산이 필요하다."),
|
||||
("실측", "03 INFINITH 랜딩: 4단어", "크롤러 UA 요청 결과 title 4단어 외 본문 0. 1.5 MB JS를 실행해야 본문이 나왔다. 프리렌더 후 810단어."),
|
||||
])
|
||||
band(s, 4.31, 2.03, "문제 정의",
|
||||
"진단은 나오는데 다음이 없다. 진단 결과를 받은 병원이 무엇을 고쳐야 하는지, 고친 뒤 점수가 올랐는지 증명할 수단이 제품 안에 없다.",
|
||||
"이 PRD의 범위 · 진단 자동화 · 빌드(수정 · 표준 사이트) · 측정 인프라 · 재진단 루프")
|
||||
|
||||
# ───────────────────────────── 04 뷰성형외과 케이스
|
||||
s = new("Case · 뷰성형외과 (2026.08.28)", "진단 결과가 빌드 요구사항으로 바로 이어진다",
|
||||
"카테고리 점수는 검증 항목 기준 정규화. 미검증(A6 · D6 · E1~E3)은 분모 제외.")
|
||||
table(s, ["카테고리", "배점", "획득", "점수", "핵심 발견", "→ 빌드 · 측정 요구사항"], [
|
||||
["A 크롤러 접근성", "20", "15.7 / 17", "92%", "robots · SSR · HTTPS 정상. sitemap lastmod 15개월 전", "FR-B05 sitemap 자동 갱신"],
|
||||
["B 엔티티 구조화", "20", "6.0 / 20", "30%", "JSON-LD 깨짐 · MedicalClinic 없음 · Physician 없음", "FR-B03 스키마 단일 소스"],
|
||||
["C 인용 가능 콘텐츠", "25", "5.0 / 25", "20%", "정의문 · FAQ · 서술형 H1 없음. 수치 부족", "FR-B02 질문 템플릿 · FR-B06 검사"],
|
||||
["D 인용 표면", "20", "13.7 / 19", "72%", "강남언니 19,316건 강함. GBP 전화 없음 · NAP 3종", "FR-B08 NAP 통일 체크리스트"],
|
||||
["E 측정 인프라", "10", "0 / 2", "0%", "로그 · GA4 · GSC 오너 접근 필요. 기준선 없음", "FR-M01 ~ M03"],
|
||||
["F 기술 위생", "5", "1.7 / 5", "33%", "WordPress 5.5.3 · alt 누락 · 페이지 무게", "NFR-04 · FR-B09 CMS 업그레이드"],
|
||||
], [2.1, 0.7, 1.2, 0.8, 4.2, 3.46], row_h=0.5)
|
||||
takeaway(s, "종합 48점 · C등급. P0 3건: JSON-LD 수정 + MedicalClinic 삽입, GBP 전화 등록 + NAP 통일, 측정 인프라 설치.", y=6.2)
|
||||
|
||||
# ───────────────────────────── 05 목표와 지표
|
||||
s = new("Goals & Metrics", "목표는 진단 자동화와 점수 개선의 증명이다",
|
||||
"출시 10주 후 기준. 모든 지표는 제품 안에서 자동 측정된다.")
|
||||
table(s, ["단계", "지표", "현재", "v1.0 목표", "측정 방법"], [
|
||||
["진단", "자동 채점 항목 비율", "0% (36항목 수동)", "24 / 36 자동 (67%)", "discover-aeo-geo 실행 로그"],
|
||||
["진단", "URL 입력 → 리포트 소요 시간", "약 4시간 (수동)", "10분 이내", "analysis_runs 타임스탬프"],
|
||||
["진단", "재진단 재현성", "미검증", "동일 URL 2회 점수 편차 3점 이내", "회귀 테스트"],
|
||||
["빌드", "P0 액션 완료 → 재진단 점수 상승", "측정 불가", "뷰성형외과 48 → 65+ (B)", "재진단 비교"],
|
||||
["빌드", "질문 페이지 발행 (표준 사이트)", "0", "8주 내 40개", "CMS 발행 로그"],
|
||||
["측정", "AI 크롤러 · AI 유입 계측 병원", "0", "파일럿 100%", "로그 파서 · GA4"],
|
||||
], [1.0, 3.0, 2.4, 3.0, 3.06], row_h=0.5)
|
||||
takeaway(s, "답변엔진 언급률은 지표에서 제외한다. 소비자 UI 자동 조회는 약관 위반이고 단발 조회는 무효다.")
|
||||
|
||||
# ───────────────────────────── 06 범위
|
||||
s = new("Scope", "v1.0은 진단 자동화부터 재진단 루프까지",
|
||||
"분석(1단계)은 기존 구현을 그대로 쓴다. 콘텐츠 작성과 광고 · SNS 운영은 범위 밖이다.")
|
||||
w = 6.08
|
||||
rect(s, M, 2.10, w, 3.95, WHITE, LINE); rect(s, M, 2.10, 0.07, 3.95, VIO)
|
||||
tb(s, M + 0.25, 2.28, 5.6, 0.3, "IN SCOPE", 12.75, VIO, True, spc=200)
|
||||
bullets(s, M + 0.25, 2.70, 5.6, [
|
||||
"진단 Edge Function discover-aeo-geo (자동 채점 1 · 2 · 3단계)",
|
||||
"수동 항목 입력 슬롯 (A6 · D1 · D2 · D4 · E1~E4)과 미검증 처리",
|
||||
"진단 리포트 DB 저장 · /discovery/:id 실데이터 연결 · PDF",
|
||||
"빌드 A: 기존 사이트 수정 패키지 (스키마 · NAP · CMS · FAQ 삽입)",
|
||||
"빌드 B: 병원용 워드프레스 표준 사이트 (템플릿 4종 · 검사)",
|
||||
"측정: 크롤러 로그 파서 · GA4 AI 채널 · 재진단 비교 뷰",
|
||||
"INFINITH 랜딩 파일럿: FAQ 섹션 · FAQPage 스키마",
|
||||
], step=0.44)
|
||||
x2 = M + w + 0.30
|
||||
rect(s, x2, 2.10, w, 3.95, WHITE, LINE); rect(s, x2, 2.10, 0.07, 3.95, DIM)
|
||||
tb(s, x2 + 0.25, 2.28, 5.6, 0.3, "OUT OF SCOPE (v1.0)", 12.75, DIM, True, spc=200)
|
||||
bullets(s, x2 + 0.25, 2.70, 5.6, [
|
||||
"분석 파이프라인 변경 (discover · collect · generate-report)",
|
||||
"Q&A 콘텐츠 작성 (운영팀 · 병원 담당, 별도 가이드)",
|
||||
"네이버 블로그 · 플레이스 운영 (AI 크롤러 차단 자산)",
|
||||
"답변엔진 UI 자동 조회 · 언급률 리포트 (약관 위반)",
|
||||
"llms.txt 납품 (Google 효과 0 · 채택률 0.13%)",
|
||||
"헤드리스(WordPress + Astro) 전환 (v2.0 후보)",
|
||||
], step=0.44, dot=DIM)
|
||||
takeaway(s, "빌드 A와 B를 나눈 이유: 뷰성형외과처럼 사이트가 있는 병원은 수정이고, 없는 병원은 표준 사이트다.")
|
||||
|
||||
# ───────────────────────────── 07 사용자
|
||||
s = new("Users", "세 사용자, 세 가지 다른 성공 기준",
|
||||
"설계 기준은 병원 마케팅 담당자다. 진단을 읽고, 고치고, 다시 점수를 볼 수 있어야 한다.")
|
||||
cards3(s, [
|
||||
("PRIMARY", "병원 마케팅 담당자", "URL을 넣고 10분 안에 스코어카드와 P0를 본다. 수정 후 재진단으로 점수 변화를 확인한다. 표준 사이트면 질문 페이지를 직접 추가한다."),
|
||||
("SECONDARY", "INFINITH 운영팀", "수동 항목(GBP · 강남언니 · 로그)을 입력하고 여러 병원의 점수 · 크롤 수 · 오류를 한 화면에서 본다. 성공 기준: 병원 온보딩 1일."),
|
||||
("SYSTEM", "AI 크롤러 · 검색엔진", "GPTBot · ClaudeBot · PerplexityBot · Googlebot · Yeti. 진단은 이들의 UA로 실제 요청해 채점한다. 빌드 결과도 같은 방법으로 검증한다."),
|
||||
], h=2.3)
|
||||
band(s, 4.75, 1.55, "설계 원칙", "확인 못 한 것은 감점하지 않는다(미검증은 분모 제외). 근거 없는 것은 팔지 않는다. 담당자의 실수는 시스템이 발행 전에 막는다.")
|
||||
|
||||
# ───────────────────────────── 08 FR 진단
|
||||
s = new("Functional Requirements · 진단", "FR-D01 ~ D07: 36항목을 제품 안에서 채점한다",
|
||||
"AEO-GEO-Audit 브랜치의 루브릭 · 타입 · 집계 로직을 그대로 쓰고, 채점 데이터만 수동에서 자동으로 바꾼다.")
|
||||
table(s, ["ID", "요구사항", "상세", "루브릭", "우선순위"], [
|
||||
["FR-D01", "자동 채점 1단계", "robots 파싱 · 12개 AI 봇 UA GET · HTML 파서 (crawler_audit.py 이식)", "A1~A5 · A7 · B1 · B4 · B6 · B7 · C2 · C3 · C5 · C6 · F1~F4", "P0"],
|
||||
["FR-D02", "자동 채점 2단계", "본문 텍스트를 Perplexity 또는 Claude에 투입해 구조화 판정", "B2 · B3 · C1 · C4 · C7 · C8", "P0"],
|
||||
["FR-D03", "자동 채점 3단계", "검색 그라운딩 도메인 집계 · Wikidata API", "D3 · D6", "P1"],
|
||||
["FR-D04", "수동 입력 슬롯", "운영팀이 근거 · 레벨 입력. 미입력은 unverified로 분모 제외", "A6 · D1 · D2 · D4 · E1~E4", "P0"],
|
||||
["FR-D05", "결과 저장 · 리포트", "DiscoveryResult를 DB 저장. /discovery/:id가 mock 대신 DB를 읽음", "전체", "P0"],
|
||||
["FR-D06", "액션 플랜 생성", "level 0 · 1 항목에서 P0 · P1 · P2 액션 자동 생성. 빌드 FR과 연결", "전체", "P1"],
|
||||
["FR-D07", "재진단 비교", "같은 URL의 이전 결과와 항목별 diff · 점수 변화 표시", "전체", "P1"],
|
||||
], [1.0, 1.8, 5.2, 3.5, 0.96], row_h=0.5, size=12)
|
||||
takeaway(s, "1단계만으로 36항목 중 17개가 자동이다. 2단계까지 23개. 나머지는 오너 계정 접근이 필요해 수동이 맞다.", y=6.3)
|
||||
|
||||
# ───────────────────────────── 09 FR 빌드 A
|
||||
s = new("Functional Requirements · 빌드 A (기존 사이트 수정)", "FR-B01 ~ B09: 진단 P0 · P1을 실행 가능한 패키지로",
|
||||
"뷰성형외과 액션 플랜에서 역산했다. 병원 개발사나 INFINITH가 붙여 넣을 수 있는 산출물이어야 한다.")
|
||||
table(s, ["ID", "요구사항", "상세", "루브릭", "우선순위"], [
|
||||
["FR-B01", "서버 렌더링 검증 · 처방", "A3 실패 시 프리렌더 · SSR 처방 (INFINITH 랜딩 방식 문서)", "A3", "P0"],
|
||||
["FR-B03", "스키마 패키지 생성", "진단 데이터로 MedicalClinic · Physician JSON-LD 생성. 기존 스키마 차단 안내", "B1 · B2 · B3 · B5", "P0"],
|
||||
["FR-B08", "NAP 통일 체크리스트", "홈페이지 · 강남언니 · GBP · 플레이스 표기를 나열하고 정본 지정", "B4 · D2 · D4", "P0"],
|
||||
["FR-B04", "메타 패키지", "title · description · canonical · OG · hreflang 코드 생성", "B6 · B7", "P1"],
|
||||
["FR-B05", "robots · sitemap 처방", "AI 봇 허용 robots.txt · lastmod 갱신 규칙", "A1 · A5", "P1"],
|
||||
["FR-B07", "FAQ 삽입 패키지", "시술별 질문 · 요약 답변 초안 + FAQPage 스키마 코드", "C1 · C2 · C4", "P1"],
|
||||
["FR-B09", "CMS 위생 처방", "WordPress 버전 · alt 누락 목록 · 페이지 무게 리포트", "F1 · F2 · F3", "P1"],
|
||||
], [1.0, 2.1, 5.4, 2.4, 1.56], row_h=0.5, size=12)
|
||||
takeaway(s, "빌드 A의 산출물은 코드 조각과 체크리스트다. 병원 사이트를 INFINITH가 직접 호스팅하지 않는다.", y=6.3)
|
||||
|
||||
# ───────────────────────────── 10 FR 빌드 B
|
||||
s = new("Functional Requirements · 빌드 B (표준 사이트)", "FR-B02 · B06 · B10 ~ B12: 사이트가 없는 병원용 표준 스택",
|
||||
"루브릭 A · B · C · F를 처음부터 통과하는 워드프레스 사이트. 담당자가 질문 페이지를 직접 늘린다.")
|
||||
table(s, ["ID", "요구사항", "상세", "루브릭", "우선순위"], [
|
||||
["FR-B10", "표준 스택 프리셋", "경량 테마 · Rank Math · WP Rocket · ACF · 관리형 호스팅. 1회 명령 복제", "A · F 전체", "P1"],
|
||||
["FR-B02", "질문 페이지 템플릿", "질문(h1) · 요약 답변(p, 60자) · 상세 · 관련 시술 · 의료진. 필드 입력만으로 생성", "C1 · C2 · C3 · C8", "P1"],
|
||||
["FR-B11", "시술 · 의료진 템플릿", "MedicalProcedure · Physician 스키마 포함. 가격대 · 시간 · 회복 필드", "B3 · C4 · C7", "P1"],
|
||||
["FR-B12", "스키마 단일 소스", "페이지 유형별 JSON-LD 1개 자동 출력. 테마 · 타 플러그인 출력 차단", "B1 · B2 · B3", "P1"],
|
||||
["FR-B06", "발행 전 검사", "스키마 중복 · h1 2개 · alt 누락 · 요약 답변 누락 시 발행 차단", "B1 · C3 · F1", "P1"],
|
||||
["FR-B13", "sitemap · robots 자동", "발행 시 lastmod 갱신 · AI 봇 9종 허용", "A1 · A5 · C6", "P1"],
|
||||
], [1.0, 2.1, 5.4, 2.4, 1.56], row_h=0.5, size=12)
|
||||
takeaway(s, "빌드 B는 P1이다. 뷰성형외과처럼 사이트가 있는 병원이 먼저이므로 빌드 A가 P0다.", y=6.0)
|
||||
|
||||
# ───────────────────────────── 11 FR 측정
|
||||
s = new("Functional Requirements · 측정", "FR-M01 ~ M05: 루브릭 E를 채우고 개선을 증명한다",
|
||||
"인용 여부를 직접 볼 수 없으므로 소유한 신호를 계측한다. 재진단이 개선의 증거다.")
|
||||
table(s, ["ID", "요구사항", "상세", "루브릭", "우선순위"], [
|
||||
["FR-M01", "AI 크롤러 로그 파서", "액세스 로그 또는 Cloudflare Logpush에서 GPTBot · ClaudeBot · PerplexityBot 히트를 IP 검증 후 일 집계", "E1", "P0"],
|
||||
["FR-M02", "GA4 AI 채널", "chatgpt.com · perplexity.ai · gemini.google.com 리퍼러를 커스텀 채널로 분리 · 설정 가이드", "E2", "P0"],
|
||||
["FR-M03", "기준선 스팟체크 기록", "분기 반복 측정 프로토콜과 입력 슬롯 (수동)", "E4", "P1"],
|
||||
["FR-M04", "재진단 스케줄", "월 1회 자동 재진단 · 점수 변화 알림", "전체", "P1"],
|
||||
["FR-M05", "운영 대시보드", "병원별 점수 · 크롤 수 · AI 세션 · 스키마 오류 · LCP 주간 표시", "E 전체", "P1"],
|
||||
], [1.0, 2.1, 6.2, 1.6, 1.56], row_h=0.55, size=12)
|
||||
takeaway(s, "FR-M01 · M02는 뷰성형외과 P0 3번(측정 인프라 설치)이다. 빌드보다 먼저 깔아야 전후 비교가 된다.", y=5.9)
|
||||
|
||||
# ───────────────────────────── 12 NFR
|
||||
s = new("Non-Functional Requirements", "진단 재현성, 성능, 보안 예산",
|
||||
"진단은 같은 입력에 같은 점수를 내야 한다. 빌드 B의 예산은 CI에서 강제한다.")
|
||||
table(s, ["ID", "영역", "요구사항", "예산 · 기준"], [
|
||||
["NFR-01", "진단", "재현성", "동일 URL 2회 실행 점수 편차 3점 이내 · LLM 판정 항목은 temperature 0"],
|
||||
["NFR-02", "진단", "소요 시간 · 비용", "URL당 10분 이내 · API 비용 2,000원 이내"],
|
||||
["NFR-03", "진단", "대상 사이트 부하", "UA 테스트 요청 30회 이내 · 1초 간격 · 429 시 중단"],
|
||||
["NFR-04", "빌드 B", "성능", "모바일 LCP p75 2.5초 · CLS 0.1 · 플러그인 10개 이하 · HTML 80 KB 이하"],
|
||||
["NFR-05", "빌드 B", "HTML 품질 · 보안", "h1 1개 · div 중첩 6단계 이하 · 관리형 호스팅 · 자동 업데이트 · 일 백업"],
|
||||
["NFR-06", "측정", "로그 보존 · 정확도", "90일 보존 · 봇 IP 역DNS 검증 (위장 UA 제외)"],
|
||||
["NFR-07", "공통", "정직성 규약", "리포트에 '하지 않을 것' 섹션 고정 · 근거 등급(실측 · 학술 · 벤더 · 추정) 표기"],
|
||||
], [1.1, 1.1, 2.6, 7.66], row_h=0.5, size=12.5)
|
||||
takeaway(s, "Elementor · Divi 같은 페이지빌더는 NFR-04 · 05를 통과하지 못하므로 빌드 B에서 금지다.", y=6.2)
|
||||
|
||||
# ───────────────────────────── 13 아키텍처
|
||||
s = new("Architecture", "기존 파이프라인 뒤에 진단 · 빌드 · 측정 함수를 잇는다",
|
||||
"Supabase Edge Functions 체인. 분석 3개 함수는 그대로, 신규 4개를 추가한다.")
|
||||
step_rows(s, [
|
||||
("4", "측정", "log-ingest · GA4 커넥터 · 재진단 cron", "신규 · discovery_results 시계열"),
|
||||
("3", "빌드", "build-package: 스키마 · 메타 · NAP · FAQ 코드", "신규 · 빌드 A는 함수, B는 플러그인"),
|
||||
("2", "진단", "discover-aeo-geo: UA 테스트 · 파서 · LLM 판정", "신규 · 루브릭 · 집계는 AEO-GEO-Audit 코드 이식"),
|
||||
("1", "분석", "discover → collect → generate-report", "기존 · marketing_reports · clinic_registry"),
|
||||
], h=0.81, gap=0.21)
|
||||
takeaway(s, "신규 테이블 2개: discovery_results (결과 · 버전) · crawler_hits (봇 · IP · 경로 · 일자).", y=6.35)
|
||||
|
||||
# ───────────────────────────── 14 진단 → 빌드 매핑
|
||||
s = new("Rubric → Build Mapping", "level 0 · 1 항목이 어떤 빌드 산출물로 이어지는지",
|
||||
"FR-D06 액션 플랜 생성의 규칙표. 모든 항목이 빌드 또는 측정 FR 하나에 매핑된다.")
|
||||
table(s, ["루브릭 항목", "level 0 · 1이면", "빌드 산출물", "FR"], [
|
||||
["A3 서버 렌더링 본문", "본문이 JS에만 있음", "프리렌더 · SSR 처방 문서 + 검증 스크립트", "FR-B01"],
|
||||
["B1 · B2 · B3 스키마", "JSON-LD 깨짐 · 엔티티 없음", "MedicalClinic · Physician JSON-LD 코드 + 차단 안내", "FR-B03 · B12"],
|
||||
["B4 · D2 · D4 NAP", "표면 간 표기 불일치", "정본 NAP 체크리스트 · GBP 수정 항목", "FR-B08"],
|
||||
["C1 · C2 · C4 콘텐츠", "정의문 · FAQ · 수치 없음", "시술별 질문 · 요약 답변 초안 + FAQPage 코드", "FR-B07 · B02"],
|
||||
["C3 제목 계층", "H1 없음 · 이미지 텍스트", "H1 재설계안 · 이미지 정보 텍스트화 목록", "FR-B07"],
|
||||
["C6 신선도", "dateModified 없음", "dateModified 활성화 · 갱신 주기 규칙", "FR-B13"],
|
||||
["E1 · E2 측정", "미설치", "로그 파서 · GA4 채널 설정", "FR-M01 · M02"],
|
||||
["F2 CMS", "구버전", "업그레이드 절차 · 백업 체크리스트", "FR-B09"],
|
||||
], [2.6, 2.6, 5.3, 1.96], row_h=0.46, size=12)
|
||||
|
||||
# ───────────────────────────── 15 스키마 스펙
|
||||
s = new("Schema Specification", "페이지 유형별 JSON-LD 1개, 단일 소스",
|
||||
"빌드 A는 코드 조각으로, 빌드 B는 Rank Math 훅으로 같은 스펙을 출력한다. 테마 내장 스키마는 끈다.")
|
||||
table(s, ["페이지", "@type", "필수 속성", "데이터 소스"], [
|
||||
["홈", "MedicalClinic + WebSite", "name · address · telephone · openingHours · sameAs", "분석 1단계 clinics · 병원 정보 ACF"],
|
||||
["시술", "MedicalProcedure", "name · procedureType · bodyLocation · howPerformed · preparation", "시술 ACF"],
|
||||
["의료진", "Physician", "name · medicalSpecialty · memberOf · image · alumniOf", "의료진 ACF · 강남언니 의료진"],
|
||||
["질문", "FAQPage (+ Article)", "mainEntity[Question · acceptedAnswer] · author · dateModified", "질문 템플릿 필드"],
|
||||
["공통", "BreadcrumbList", "itemListElement", "자동"],
|
||||
], [1.4, 2.6, 5.0, 3.46], row_h=0.55)
|
||||
band(s, 5.75, 1.15, "수용 기준", "Rich Results Test 오류 0. 응답에 ld+json 블록 정확히 1개 (Breadcrumb 포함 시 @graph 병합). 루브릭 B1 · B2 · B3 level 3.")
|
||||
|
||||
# ───────────────────────────── 16 질문 페이지 템플릿
|
||||
s = new("Q&A Page Template", "AI가 인용하는 단락 구조를 템플릿이 강제한다",
|
||||
"루브릭 C1 · C2 · C3 · C4 · C8을 동시에 통과하는 구조. 담당자는 필드를 채우고, 템플릿이 HTML과 스키마를 만든다.")
|
||||
table(s, ["필드", "HTML 출력", "제약", "예시", "루브릭"], [
|
||||
["질문", "h1", "물음표로 끝남 · 40자 이내", "코 재수술은 언제 가능한가요?", "C3"],
|
||||
["요약 답변", "p (h1 직후)", "60자 이내 · 숫자 · 기간 포함 권장", "1차 수술 후 최소 6개월, 보통 1년 뒤 가능합니다.", "C1 · C8"],
|
||||
["상세 답변", "h2 + p 반복", "h2는 하위 질문형 · 단락 3문장 이내", "왜 6개월을 기다려야 하나요?", "C3 · C8"],
|
||||
["가격 · 시간 · 회복", "표", "숫자 범위 필수", "300~500만원 · 2시간 · 회복 2주", "C4 · C5"],
|
||||
["관련 시술 · 의료진", "링크 + Physician", "각 1개 이상", "코 재수술 · 김OO 원장", "C7"],
|
||||
["갱신일", "dateModified", "자동", "2026-08-28", "C6"],
|
||||
], [1.6, 1.6, 3.0, 4.3, 1.96], row_h=0.52)
|
||||
takeaway(s, "요약 답변 60자는 AI 답변엔진이 한 단락으로 인용하는 길이다. 이 필드가 비면 발행이 막힌다(FR-B06).", y=6.0)
|
||||
|
||||
# ───────────────────────────── 17 수용 테스트
|
||||
s = new("Acceptance Tests", "수용 기준은 전부 스크립트로 검증한다",
|
||||
"진단은 뷰성형외과 · INFINITH 랜딩을 회귀 기준으로 삼는다. 빌드는 크롤러 UA 테스트를 CI로 옮긴다.")
|
||||
table(s, ["ID", "단계", "테스트", "통과 기준"], [
|
||||
["AT-01", "진단", "viewclinic.com 자동 진단 실행", "수동 채점 대비 자동 항목 일치율 90% · 소요 10분 이내"],
|
||||
["AT-02", "진단", "infinith-demo.vercel.app 진단", "A3 level 3 · B1 level 3 · 재실행 편차 3점 이내"],
|
||||
["AT-03", "진단", "미검증 처리", "수동 슬롯 비운 채 실행 시 해당 항목 분모 제외 · 점수 계산 일치"],
|
||||
["AT-04", "빌드 A", "스키마 패키지 적용 후 Rich Results Test", "오류 0 · 블록 1개"],
|
||||
["AT-05", "빌드 B", "curl -A GPTBot 5종 UA 본문 단어 수", "300단어 이상 · 5개 UA 동일 · 429 · 403 0건"],
|
||||
["AT-06", "빌드 B", "발행 검사", "요약 답변 비운 채 발행 시 차단 + 안내"],
|
||||
["AT-07", "측정", "로그 파서 위장 UA 테스트", "역DNS 실패 히트 제외 · 일 집계 정확"],
|
||||
["AT-08", "루프", "빌드 후 재진단", "변경 항목 level 상승이 diff에 표시 · 점수 상승"],
|
||||
], [1.0, 1.0, 4.5, 5.96], row_h=0.46)
|
||||
takeaway(s, "AT-02 · 05는 2026.08.28 INFINITH 랜딩 점검 스크립트를 그대로 쓴다.", y=6.3)
|
||||
|
||||
# ───────────────────────────── 18 마일스톤
|
||||
s = new("Milestones", "10주 · 5단계 · 뷰성형외과 파일럿 포함",
|
||||
"2주 단위. 각 단계 끝에 수용 테스트를 통과해야 다음 단계로 간다.")
|
||||
step_rows(s, [
|
||||
("W9-10", "파일럿 · 릴리스", "뷰성형외과 빌드 A 적용 · 재진단 · 표준 사이트 1곳", "AT-08 · 48 → 65+ · v1.0"),
|
||||
("W7-8", "빌드 B", "표준 스택 · 템플릿 3종 · 검사 · 플러그인", "FR-B02 · B06 · B10~B13 · AT-05 · 06"),
|
||||
("W5-6", "빌드 A", "스키마 · NAP · FAQ · CMS 패키지 생성", "FR-B01 · B03 · B04 · B07 · B08 · AT-04"),
|
||||
("W3-4", "측정 · 진단 2단계", "로그 파서 · GA4 · LLM 판정 · 재진단 diff", "FR-M01 · M02 · D02 · D07 · AT-07"),
|
||||
("W1-2", "진단 1단계", "discover-aeo-geo · UA 테스트 · 파서 · DB · 리포트 연결", "FR-D01 · D04 · D05 · AT-01~03"),
|
||||
], h=0.72, gap=0.16)
|
||||
takeaway(s, "측정(W3-4)을 빌드보다 앞에 둔 이유: 전후 비교의 기준선이 빌드 전에 있어야 한다.", y=6.45)
|
||||
|
||||
# ───────────────────────────── 19 리스크
|
||||
s = new("Risks & Mitigations", "가장 큰 리스크는 진단 신뢰도와 병원 측 실행이다",
|
||||
"진단이 틀리면 빌드가 틀리고, 병원이 안 고치면 측정할 것이 없다.")
|
||||
table(s, ["리스크", "영향", "가능성", "대응"], [
|
||||
["LLM 판정 항목(2단계)의 채점 불일치", "높음", "중간", "temperature 0 · 근거 인용 강제 · 수동 채점과 90% 일치 회귀 테스트"],
|
||||
["대상 사이트가 진단 요청을 차단", "중간", "중간", "요청 30회 · 1초 간격 · 429 시 중단 후 unverified 처리"],
|
||||
["병원이 빌드 A 패키지를 적용하지 않음", "높음", "높음", "적용 대행 옵션 · 재진단 알림 · P0만 먼저"],
|
||||
["담당자가 질문 페이지를 추가하지 않음", "높음", "높음", "분석 리포트의 질문 목록을 초안으로 제공 · 주간 발행 목표"],
|
||||
["AI 크롤러 UA · 정책 변경", "중간", "높음", "봇 목록 중앙 관리 · 분기 점검 · 루브릭 버전 관리"],
|
||||
["스키마 플러그인 업데이트로 출력 변경", "중간", "중간", "AT-04 매 배포 실행 · 버전 고정"],
|
||||
], [3.8, 0.9, 0.9, 6.86], row_h=0.55)
|
||||
|
||||
# ───────────────────────────── 20 오픈 이슈
|
||||
s = new("Open Questions", "개발 착수 전 결정이 필요한 다섯 가지",
|
||||
"리뷰 미팅에서 결정한다. 기본안을 함께 적었다.")
|
||||
table(s, ["#", "질문", "선택지", "기본안 · 근거"], [
|
||||
["1", "2단계 LLM 판정 모델", "Perplexity · Claude · 둘 다", "Claude. 판정 일관성 · 근거 인용. Perplexity는 D3 그라운딩에만"],
|
||||
["2", "로그 수집 방식", "호스팅 로그 API · Cloudflare Logpush · WP 플러그인", "Cloudflare Logpush → Supabase. 호스팅 무관"],
|
||||
["3", "빌드 A 적용 주체", "병원 개발사 · INFINITH 대행 · 둘 다", "둘 다. 패키지는 개발사용, 대행은 유료 옵션"],
|
||||
["4", "빌드 B 호스팅 · 스키마 플러그인", "Kinsta · WP Engine · Cafe24 / Rank Math · Yoast", "Kinsta + Rank Math. Cloudflare 통합 · 의료 타입 · 훅"],
|
||||
["5", "헤드리스 전환 시점", "병원 5곳 · 10곳 · 미정", "병원 5곳 시 v2.0 검토. 콘텐츠는 WP DB에 남아 이전 비용 낮음"],
|
||||
], [0.5, 2.6, 4.0, 5.36], row_h=0.62)
|
||||
takeaway(s, "결정되지 않은 항목은 기본안으로 W1을 시작하고, 리뷰에서 뒤집히면 W2에 반영한다.", y=6.1)
|
||||
|
||||
# ───────────────────────────── 21 부록
|
||||
s = new("Appendix · Evidence", "실측 두 건 (2026.08.28)",
|
||||
"이 PRD의 수용 기준은 아래 두 실측에서 나왔다.")
|
||||
table(s, ["대상", "항목", "결과", "방법"], [
|
||||
["뷰성형외과", "종합 · 등급", "48점 · C (검증 31 / 36항목)", "crawler_audit.py + curl + HTML 파서 · 수동 채점"],
|
||||
["뷰성형외과", "카테고리", "A 92 · B 30 · C 20 · D 72 · E 0 · F 33", "루브릭 v1.0 · unverified 분모 제외"],
|
||||
["뷰성형외과", "P0", "JSON-LD 수정 + MedicalClinic · GBP 전화 + NAP 통일 · 측정 인프라", "액션 플랜"],
|
||||
["INFINITH 랜딩", "크롤러 5종 본문 단어", "4 → 810", "vite build --ssr + renderToString 프리렌더"],
|
||||
["INFINITH 랜딩", "JSON-LD · lang · 메타", "0 → 3 · en → ko · description · OG 추가", "index.html 정적 삽입"],
|
||||
["INFINITH 랜딩", "robots · sitemap · llms.txt", "HTML 반환 → text/plain · xml 정상", "public/ 정적 파일"],
|
||||
], [1.6, 2.2, 4.6, 4.06], row_h=0.5)
|
||||
band(s, 5.95, 1.0, "재현", "curl -A GPTBot infinith-demo.vercel.app | sed 's/<[^>]*>//g' | wc -w · /discovery/viewclinic")
|
||||
|
||||
out = "docs/prd/INFINITH_AI-Discovery_PRD_20260828_v02.pptx"
|
||||
prs.save(out); print("saved", out, "slides", len(prs.slides))
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,317 @@
|
||||
"""
|
||||
Viewclinic AI Answer Readiness Report v2.0 덱 빌드 (INFINITH Deck 규격)
|
||||
|
||||
v1.1(2026-09-07 재검증 정정)의 실측을 그대로 두고, 점수를 기준표 v2.0(AEO 답변 준비도 100 · GEO 출처 준비도 100)으로
|
||||
바꾼 판. v1.0·v1.1 파일은 haewon이 손본 정본이라 덮어쓰지 않고 v2.0 파일로 새로 저장한다.
|
||||
|
||||
바뀐 것: 표지 배지·부제, Executive Summary KPI(AEO 42 / GEO 56), 스코어카드 2장(AEO·GEO 항목별), 로드맵 제목,
|
||||
Method(두 점수·게이트·36개 실측 신호). A~E 실측 슬라이드와 정정·요청·정직성 슬라이드는 v1.1과 같다.
|
||||
점수 원본: src/data/aeoGeoRubricV2.ts + src/lib/discoveryScoreV2.ts (docs/AEO_GEO_RUBRIC_v2.md 재채점 표).
|
||||
|
||||
폰트: haewon 확정 기준(본문 20 / 리드 24 / 카드 제목 28). ifd 분량 검사 안에서 카피를 줄였다.
|
||||
|
||||
python3 docs/reports/viewclinic/01_readiness_report/build_viewclinic_deck_v2_0.py docs/reports/viewclinic/01_readiness_report/Viewclinic_AI_Answer_Readiness_Report_v2.0.pptx
|
||||
"""
|
||||
import sys
|
||||
sys.path.insert(0, "/Users/haewonkam/.claude/skills/infinith-deck/scripts")
|
||||
from ifd import Deck
|
||||
|
||||
OUT = sys.argv[1] if len(sys.argv) > 1 else "Viewclinic_AI_Answer_Readiness_Report_v2.0.pptx"
|
||||
BODY, HEAD = 20.0, 28.0
|
||||
d = Deck()
|
||||
|
||||
# 01 Cover
|
||||
d.cover(
|
||||
eyebrow="AI DISCOVERY · AEO / GEO AUDIT v2.0",
|
||||
title_lines=["AI Answer", "Readiness Report."],
|
||||
subtitle="뷰성형외과 · viewclinic.com · 실측 2026. 08. 28 · 재검증 09. 07 · 기준표 v2.0 채점 09. 07",
|
||||
meta_right="AI Discovery Report · 2026",
|
||||
)
|
||||
|
||||
# 02 Executive summary
|
||||
s = d.slide(eyebrow="Executive Summary",
|
||||
title="답은 부족하고, ", title_accent="출처로는 절반쯤 됩니다",
|
||||
lede=["답변 준비도(AEO) 42점, 출처 준비도(GEO) 56점. 각각 100점의 독립 지표입니다.",
|
||||
"36개 실측 항목 중 32개를 확인했고, 오너 계정이 필요한 4개는 분모에서 제외했습니다."],
|
||||
section="Summary")
|
||||
k1, k2, k3, k4 = s.cards4(y=s.band_y, h=3.4)
|
||||
k1.kpi("42 / 100", "AEO · 답변 준비도", "등급 C · 정의문 0 · 라벨형 H1 · 표 0")
|
||||
k2.kpi("56 / 100", "GEO · 출처 준비도", "등급 C · 근거 없는 주장 · 후기 광고 이력")
|
||||
k3.kpi("2 / 2", "게이트 통과", "크롤러 접근 · 본문 렌더링")
|
||||
k4.kpi("4 / 36", "미검증 실측 항목", "GA4 · 서버로그 · GSC · Bing")
|
||||
|
||||
# 03 What AI sees (dark)
|
||||
s = d.slide(eyebrow="What AI Sees · 진단의 결론",
|
||||
title="문제는 차단이 아니라 ", title_accent="내용에 있습니다", dark=True, section="Findings")
|
||||
L, R = s.cards2(y=s.band_y)
|
||||
L.chip("정상", "good").rows([
|
||||
("크롤러 접근 정상", "robots.txt가 AI 검색봇 12종을 허용. Cloudflare 차단 없음."),
|
||||
("서버 렌더링 본문", "WordPress가 본문을 서버에서 렌더. JS 없이 10,757자 추출."),
|
||||
("표면이 넓습니다", "강남언니 후기 19,373건. 유튜브 1,215편. 블로그는 발행 중이고 AI 봇 허용."),
|
||||
], t_size=HEAD, b_size=BODY)
|
||||
R.chip("치명", "critical").rows([
|
||||
("누구인지 확정 불가", "MedicalClinic 스키마 없음, sameAs 비어 있음. 시술 목록 JSON-LD 무효."),
|
||||
("인용할 문장 부족", "H1 5개가 영문 라벨. HTML 표 0, 병원 정의문 0. FAQ는 프리저베뿐."),
|
||||
("표면 간 NAP 불일치", "GBP 전화번호 없음. 웹사이트가 영문 도메인. 주소 표기 3종."),
|
||||
], t_size=HEAD, b_size=BODY)
|
||||
|
||||
# 04 AEO scorecard
|
||||
s = d.slide(eyebrow="Scorecard · AEO Answer Readiness · 42",
|
||||
title="답변 준비도 ", title_accent="8개 항목",
|
||||
lede="고객의 질문에 얼마나 명확하고 충분하게 답하는가. 항목 점수는 36개 실측 항목에서 파생하고, 실측이 없는 항목은 사람이 판정했습니다.",
|
||||
section="AEO")
|
||||
W, Rail = s.cards2(y=s.band_y, split=0.66)
|
||||
W.grid(
|
||||
headers=["항목", "배점", "점수", "근거", "판정"],
|
||||
col_w=[3.5, 1.0, 1.2, 3.7, 1.5],
|
||||
rows=[
|
||||
["AEO1 질문 수요 충족", "20", "13.3", "FAQ 프리저베 1페이지뿐", "양호"],
|
||||
["AEO2 답변의 직접성", "20", "0", "병원 정의문 없음", "치명"],
|
||||
["AEO3 자기완결성", "15", "5.0", "단락에 상호·시술명 부족", "미흡"],
|
||||
["AEO4 의사결정 정보", "15", "6.0", "수치 일부 · 표 0", "미흡"],
|
||||
["AEO5 정보 구조·가독성", "10", "0", "H1 5개 영문 라벨", "치명"],
|
||||
["AEO6 다음 행동", "10", "10", "전화·예약·방문 텍스트", "우수"],
|
||||
["AEO7 텍스트 추출", "5", "3.7", "SSR 정상, alt 37% 누락", "양호"],
|
||||
["AEO8 사이트 내 발견", "5", "4.3", "sitemap · canonical", "우수"],
|
||||
],
|
||||
tones=[[None, None, None, None, "good"], [None, None, None, None, "critical"],
|
||||
[None, None, None, None, "warning"], [None, None, None, None, "warning"],
|
||||
[None, None, None, None, "critical"], [None, None, None, None, "info"],
|
||||
[None, None, None, None, "good"], [None, None, None, None, "info"]],
|
||||
row_h=0.56, size=BODY - 2,
|
||||
)
|
||||
Rail.title("읽는 법", size=HEAD).bullets([
|
||||
"8항목 합 100. 검증 8/8, 42.3점 = 42.",
|
||||
"0점 두 항목(정의문·H1)이 40점을 비웁니다.",
|
||||
"AEO6은 사람 판정. 나머지는 실측 파생.",
|
||||
], size=BODY)
|
||||
|
||||
# 05 GEO scorecard
|
||||
s = d.slide(eyebrow="Scorecard · GEO Source Readiness · 56",
|
||||
title="출처 준비도 ", title_accent="8개 항목",
|
||||
lede="그 답을 얼마나 검증하고 신뢰할 수 있는 출처로 쓸 수 있는가. 정본 URL과 갱신 신호는 만점, 근거와 투명성이 깎입니다.",
|
||||
section="GEO")
|
||||
W, Rail = s.cards2(y=s.band_y, split=0.66)
|
||||
W.grid(
|
||||
headers=["항목", "배점", "점수", "근거", "판정"],
|
||||
col_w=[3.5, 1.0, 1.2, 3.7, 1.5],
|
||||
rows=[
|
||||
["GEO1 주장·근거 일치", "20", "6.7", "수상·1위 주장 원문 없음", "미흡"],
|
||||
["GEO2 출처 추적성", "15", "15", "http → 301 정본 수렴", "우수"],
|
||||
["GEO3 전문성·책임", "15", "6.0", "약력 일부, 스키마 없음", "미흡"],
|
||||
["GEO4 엔티티 정합성", "15", "7.0", "강남언니 강함, NAP 3종", "미흡"],
|
||||
["GEO5 최신성·정정", "10", "10", "수정일 표기, sitemap 최신", "우수"],
|
||||
["GEO6 원천 대비 가치", "10", "6.7", "주의사항·애프터케어 고유", "양호"],
|
||||
["GEO7 발행 투명성", "10", "3.3", "후기 광고 시정명령 07-12", "미흡"],
|
||||
["GEO8 인용 메타데이터", "5", "1.7", "JSON-LD 1블록 무효", "미흡"],
|
||||
],
|
||||
tones=[[None, None, None, None, "warning"], [None, None, None, None, "info"],
|
||||
[None, None, None, None, "warning"], [None, None, None, None, "warning"],
|
||||
[None, None, None, None, "info"], [None, None, None, None, "good"],
|
||||
[None, None, None, None, "warning"], [None, None, None, None, "warning"]],
|
||||
row_h=0.56, size=BODY - 2,
|
||||
)
|
||||
Rail.title("읽는 법", size=HEAD).bullets([
|
||||
"8항목 합 100. 검증 8/8, 56.4점 = 56.",
|
||||
"GEO1·GEO6·GEO7은 사람 판정(09-07 초안).",
|
||||
"근거 원문 링크만 붙여도 20점이 열립니다.",
|
||||
], size=BODY)
|
||||
|
||||
# 06 A 접근성
|
||||
s = d.slide(eyebrow="A · Access & Crawlability · 실측 98%",
|
||||
title="크롤러 접근은 ", title_accent="열려 있습니다", section="A · Access")
|
||||
L, R = s.cards2(y=s.band_y)
|
||||
L.chip("우수 · 3/3", "info").rows([
|
||||
("A1 검색용 AI 봇 허용", "Disallow는 /wp-admin/ 뿐. 12종 UA로 GET, 전부 200."),
|
||||
("A2 CDN 차단 없음", "nginx 직접 응답. Cloudflare 헤더와 AI UA 챌린지 없음."),
|
||||
("A4 HTTPS 정합", "http → 301 → https://www.viewclinic.com/ 정본 수렴."),
|
||||
], t_size=HEAD, b_size=BODY)
|
||||
R.chip("보완·미검증", "warning").rows([
|
||||
("A5 sitemap 우수", "page·portfolio lastmod 2026-08-31. 블로그는 별도 서브도메인."),
|
||||
("A7 응답 속도·무게", "TTFB 0.135s 양호. 초기 HTML 838KB, 기준 300KB의 2.8배."),
|
||||
("A6 Bing 색인 (미검증)", "ChatGPT 검색은 Bing 인덱스 의존. 계정 확인 필요."),
|
||||
], t_size=HEAD, b_size=BODY)
|
||||
|
||||
# 07 B 엔티티 (dark)
|
||||
s = d.slide(eyebrow="B · Entity & Structured Data · 실측 25%",
|
||||
title="AI가 병원을 아직 ", title_accent="하나로 묶지 못합니다", dark=True, section="B · Entity")
|
||||
L, R = s.cards2(y=s.band_y)
|
||||
L.chip("치명", "critical").rows([
|
||||
("B1 JSON-LD 문법 무효", "ItemList 블록 쉼표 누락. 09-07에도 그대로. 파서가 통째로 버림."),
|
||||
("B2 MedicalClinic 없음", "Organization·WebSite·WebPage만. 주소·전화·진료시간 미구조화."),
|
||||
("B3 Physician 없음", "의료진은 텍스트와 이미지뿐. 누가 수술하는지 기계가 못 읽음."),
|
||||
], t_size=HEAD, b_size=BODY)
|
||||
R.chip("미흡", "warning").rows([
|
||||
("B5 sameAs 비어 있음", "푸터에 유튜브·인스타·블로그 링크가 있으나 스키마 미연결."),
|
||||
("B4 NAP 표기 3종 혼재", "주소 표기 3종. 전화도 2회선과 휴대폰 혼재."),
|
||||
("B7 영문 사이트 미연결", "viewplasticsurgery.com이 별도. hreflang 없음."),
|
||||
], t_size=HEAD, b_size=BODY)
|
||||
|
||||
# 08 C 콘텐츠
|
||||
s = d.slide(eyebrow="C · Answer-Ready Content · 실측 44%",
|
||||
title="프리저베 페이지가 ", title_accent="인용 구조의 기준입니다", section="C · Content")
|
||||
L, R = s.cards2(y=s.band_y)
|
||||
L.chip("0점 항목", "critical").rows([
|
||||
("C1 병원 정의문 없음", "메인은 슬라이더와 라벨. 가슴성형 페이지 첫 텍스트는 상담 폼."),
|
||||
("C3 H1 5개, 전부 라벨", "가슴·코·오시는길은 H1 없음. 프리저베만 H1 1개 서술형."),
|
||||
("C5 HTML 표 없음", "5개 페이지 모두 table 0개. 비교 정보는 이미지."),
|
||||
], t_size=HEAD, b_size=BODY)
|
||||
R.chip("부분 점수", "warning").rows([
|
||||
("C2 FAQ는 프리저베에만", "FAQ 7문항과 유효한 FAQPage 스키마. 다른 시술 페이지는 없음."),
|
||||
("C4 수치는 시술 상세에", "프리저베 20~40분·회복 1~3일, 코 1~2시간·실밥 7일. 가격대 없음."),
|
||||
("C6 신선도 정상", "시술 페이지 2026-04-17, sitemap 08-31, 블로그 09-07 게시."),
|
||||
], t_size=HEAD, b_size=BODY)
|
||||
|
||||
# 09 D 표면 / E 측정 (dark)
|
||||
s = d.slide(eyebrow="D · Surfaces / E · Measurement",
|
||||
title="강남언니는 강하고, ", title_accent="GBP 보완이 필요합니다", dark=True, section="D · Surfaces")
|
||||
L, R = s.cards2(y=s.band_y)
|
||||
L.chip("D · 표면 실측 68%", "good").rows([
|
||||
("D1 강남언니 우수", "후기 19,373건, 의료진 26명, 이벤트 52건. 핵심 표면 확보."),
|
||||
("D2 GBP 4.9점 (901)", "게시물 활성. 전화번호 미등록, 웹사이트 영문 도메인(08-28 실측)."),
|
||||
("D3 오픈웹 표면", "유튜브 1,215편, 블로그 발행 중, 언론 1,137건. NAP 불일치로 3점 보류."),
|
||||
], t_size=HEAD, b_size=BODY)
|
||||
R.chip("E · 측정 (점수 밖 부가 지표)", "critical").rows([
|
||||
("E1 크롤러 로그", "nginx 직접 호스팅. AI 봇 히트 추출 파이프라인 없음."),
|
||||
("E2 GA4 AI 유입 채널", "chatgpt.com·perplexity.ai 채널그룹 없음. 계정 접근 필요."),
|
||||
("E4 기준선 반복 측정", "120문항 API 실측 1회(09-01). 반복 측정은 미실시."),
|
||||
], t_size=HEAD, b_size=BODY)
|
||||
|
||||
# 10 Action plan
|
||||
s = d.slide(eyebrow="Action Plan",
|
||||
title="지금 고칠 것과 ", title_accent="4주 안에 만들 것", section="Action Plan")
|
||||
a, b, c = s.cards3(y=s.band_y)
|
||||
a.chip("P0 · 1일~1주 · GEO", "critical").title("엔티티·근거·측정", size=HEAD).bullets([
|
||||
"깨진 JSON-LD 수정, MedicalClinic 스키마.",
|
||||
"수상·1위 주장에 원문 링크와 시점.",
|
||||
"GBP 전화번호 등록, 4개 표면 NAP 통일.",
|
||||
"GA4 AI 채널, 서버로그, GSC·Bing.",
|
||||
], size=BODY)
|
||||
b.chip("P1 · 4주 내 · AEO", "warning").title("콘텐츠 구조", size=HEAD).bullets([
|
||||
"핵심 페이지 첫 단락에 병원 정의문.",
|
||||
"프리저베 구조를 가슴·코·눈·윤곽으로 확산.",
|
||||
"비교표를 HTML 표로. H1 재설계.",
|
||||
"Physician 스키마, alt 111개 보완.",
|
||||
], size=BODY)
|
||||
c.chip("P2 · 지속", "good").title("연결·시딩", size=HEAD).bullets([
|
||||
"블로그 글과 시술 페이지를 내부 링크로.",
|
||||
"유튜브 영상을 블로그 글로 전사.",
|
||||
"월 1회 갱신, 분기 1회 반복 측정.",
|
||||
], size=BODY)
|
||||
|
||||
# 11 Roadmap (dark)
|
||||
s = d.slide(eyebrow="Roadmap · 8 Weeks",
|
||||
title="8주 안에 ", title_accent="AEO·GEO 모두 C에서 B로", dark=True, section="Roadmap")
|
||||
s.stats3([
|
||||
("W1–2", "스키마·NAP·근거 링크·측정 인프라.\n기준선 반복 측정 1회."),
|
||||
("W3–6", "정의문·FAQ·H1 재설계·HTML 표.\nWordPress 업그레이드."),
|
||||
("W7–8", "재진단. 미검증 4개 반영해\nAEO·GEO 재산정."),
|
||||
], y=s.band_y + 0.6)
|
||||
|
||||
# 12 Validation
|
||||
s = d.slide(eyebrow="Validation · 120-Question Live Test",
|
||||
title="진단을 120문항 실측으로 ", title_accent="검증했습니다",
|
||||
lede="잠재 환자 질문 120개를 OpenAI·Perplexity 정식 API로 질의해 예측력을 채점했습니다 (09-01).",
|
||||
section="Validation")
|
||||
L, R = s.cards2(y=s.band_y)
|
||||
L.chip("예측 적중", "good").rows([
|
||||
("논브랜드 질문에서 부재", "84문항 중 Perplexity 3, ChatGPT 10문항만 언급. 시술 정보 52문항은 0건."),
|
||||
("원인은 답변 준비도(AEO)", "병원 정의문 0, HTML 표 0, FAQ는 한 페이지. 시술별 인용 단락 부재."),
|
||||
("표면이 브랜드 방어", "병원명을 아는 36문항은 97~100% 답변. 근거는 강남언니·GBP·뉴스."),
|
||||
], t_size=HEAD, b_size=BODY)
|
||||
R.chip("진단 밖 발견", "info").rows([
|
||||
("제3자 표면이 더 넓습니다", "뉴스·채용공고·바비톡·네이버 예약이 개원연도·주차·휴진을 답함."),
|
||||
("발행 중인 블로그가 인용됨", "ChatGPT 추천 근거가 blog.viewclinic.com. 정정 반영."),
|
||||
("엔진별 오답 편차", "주차 질문에 Perplexity는 정답, ChatGPT는 추측. 구조화 부재의 비용."),
|
||||
], t_size=HEAD, b_size=BODY)
|
||||
|
||||
# 13 Corrections (dark)
|
||||
s = d.slide(eyebrow="Corrections · 2026-09-07 재검증",
|
||||
title="v1.0에서 틀린 것 ", title_accent="세 가지를 바로잡습니다", dark=True,
|
||||
lede="홈·프리저베·가슴·코·오시는길 5개 URL의 원본 HTML을 다시 받아 파싱했습니다. 이 실측이 v2.0 점수의 신호입니다.",
|
||||
section="Corrections")
|
||||
L, R = s.cards2(y=s.band_y)
|
||||
L.chip("정정", "warning").rows([
|
||||
("블로그 정지 → 발행 중", "www post-sitemap만 본 오판. blog.viewclinic.com은 09-07까지 발행, AI 봇 14종 허용."),
|
||||
("FAQ 0개 → 프리저베 7문항", "2개 URL 결과를 전체로 일반화. 프리저베에 FAQ 7문항과 FAQPage 스키마. C2 0→2."),
|
||||
("수치 없음 → 시술 상세에 있음", "프리저베·코·윤곽 페이지에 수술시간·마취·회복 텍스트. 가격대는 없음. C4 1→2."),
|
||||
], t_size=HEAD, b_size=BODY)
|
||||
R.chip("유지 · 재확인", "info").rows([
|
||||
("그대로 확인된 것", "H1 5개 라벨, ItemList 파싱 실패, sameAs 빈 값, 표 0개, 진료시간 이미지, WP 5.5.3."),
|
||||
("점수 체계 변경", "v1.0 종합 54점(C)은 참고값. v2.0은 AEO 42 · GEO 56 두 점수로 봅니다."),
|
||||
("재확인 필요", "GBP 전화번호·웹사이트는 08-28 실측값. 병원이 고쳤다면 GEO4 재채점."),
|
||||
], t_size=HEAD, b_size=BODY)
|
||||
|
||||
# 14 Request to the clinic
|
||||
s = d.slide(eyebrow="Request · 뷰 서포터즈 강화를 위해",
|
||||
title="뷰성형외과에 ", title_accent="부탁드리는 것",
|
||||
lede=["서포터즈 사이트 18편(검토 완료 10편)을 병원의 공식 답으로 완성하려면 병원만 아는 정보가 필요합니다.",
|
||||
"아래 항목은 담당자 한 분이 1~2주 안에 처리하실 수 있는 범위로 추렸습니다."],
|
||||
section="Request")
|
||||
a, b, c = s.cards3(y=s.band_y)
|
||||
a.chip("정보 · 병원만 아는 사실", "info").title("알려 주시면 반영합니다", size=HEAD).bullets([
|
||||
"가슴성형 견적 기본 포함 항목과 결제 방식.",
|
||||
"가슴성형 후 출근 기준과 걷기 시작 시점.",
|
||||
"타원 수술 재수술 상담 가능 여부.",
|
||||
"코·윤곽 재수술 대기 기간, 코 실밥 제거일.",
|
||||
], size=BODY)
|
||||
b.chip("확인 · 검토와 승인", "warning").title("확인해 주시면 켜집니다", size=HEAD).bullets([
|
||||
"검토 대기 5편의 담당 원장 확정과 검토일.",
|
||||
"검토 완료 10편의 기록을 병원 기록과 대조.",
|
||||
"서포터즈 지원 관계 문구 확정.",
|
||||
"작성자 실명·편집 책임자 표기 승인.",
|
||||
], size=BODY)
|
||||
c.chip("액션 · 홈페이지 쪽", "critical").title("병원이 직접 고쳐야 합니다", size=HEAD).bullets([
|
||||
"ItemList JSON-LD 오류 수정, MedicalClinic 추가.",
|
||||
"GBP 전화번호 등록, 웹사이트 교정.",
|
||||
"프리저베 구조를 다른 시술 페이지로 확산.",
|
||||
"서포터즈 정식 도메인 확정과 링크 연결.",
|
||||
], size=BODY)
|
||||
|
||||
# 15 Integrity
|
||||
s = d.slide(eyebrow="What We Don't Sell · 정직성 규약",
|
||||
title="근거 없는 것은 ", title_accent="약속하지 않습니다", section="Integrity")
|
||||
a, b, c = s.cards3(y=s.band_y)
|
||||
a.icon_square().title("만들지 않는 것", size=HEAD).bullets([
|
||||
"llms.txt. Google 공식 효과 0, 요청 로그 없음.",
|
||||
"네이버 블로그 증량. AI 크롤링 금지 도메인.",
|
||||
], size=BODY)
|
||||
b.icon_square().title("약속하지 않는 것", size=HEAD).bullets([
|
||||
"\"스키마 넣으면 인용됩니다\". 인과 증거 없음.",
|
||||
"순위·노출 보장. 답변은 10~34% 흔들림.",
|
||||
], size=BODY)
|
||||
c.icon_square().title("측정 규율", size=HEAD).bullets([
|
||||
"단발 조회 보고 금지. 반복 측정만.",
|
||||
"답변엔진 UI 자동 조회 금지. 약관 위반.",
|
||||
"틀린 판정은 정정 슬라이드로 남깁니다.",
|
||||
], size=BODY)
|
||||
|
||||
# 16 Method
|
||||
s = d.slide(eyebrow="Method · AEO / GEO Rubric v2.0",
|
||||
title="두 점수, 각 100점, ", title_accent="36개 실측 신호", section="Method")
|
||||
a, b, c = s.cards3(y=s.band_y)
|
||||
a.icon_square().title("두 축의 질문", size=HEAD).bullets([
|
||||
"AEO: 질문에 얼마나 명확히 답하는가.",
|
||||
"GEO: 그 답을 출처로 믿을 수 있는가.",
|
||||
"같은 항목을 두 점수에 겹쳐 넣지 않습니다.",
|
||||
], size=BODY)
|
||||
b.icon_square().title("채점 방식", size=HEAD).bullets([
|
||||
"축당 8항목 × 배점. 미검증은 분모 제외.",
|
||||
"실측 36항목의 0~3 레벨을 신호로 씁니다.",
|
||||
"실측이 없는 4항목은 사람이 근거와 함께 판정.",
|
||||
], size=BODY)
|
||||
c.icon_square().title("게이트와 등급", size=HEAD).bullets([
|
||||
"크롤러 차단·본문 미렌더는 점수 아닌 상한 40.",
|
||||
"등급 A 81+ · B 61~80 · C 41~60 · D 40 이하.",
|
||||
"제품용 기준이며 인용 확률이 아닙니다.",
|
||||
], size=BODY)
|
||||
|
||||
# 17 Closing
|
||||
d.closing(eyebrow="Next Step",
|
||||
title_lines=["출처는 절반쯤 왔습니다.", "답을 먼저 쓰면 됩니다."],
|
||||
lede="P0 세 가지: 스키마, 근거 링크, NAP 통일. 서포터즈 요청은 담당자 한 분이 처리할 수 있습니다.",
|
||||
cta_primary="P0 착수하기 →", cta_secondary="서포터즈 사이트 보기",
|
||||
contact="o2oteam@o2o.kr")
|
||||
|
||||
d.save(OUT)
|
||||
print("saved", OUT)
|
||||
Binary file not shown.
@ -0,0 +1,185 @@
|
||||
{
|
||||
"source": "Viewclinic_Supporters_vs_Official_AEO_Audit.xlsx (2026-09-04 색인 허용 빌드 실측)",
|
||||
"results": [
|
||||
{
|
||||
"criterionId": "A1",
|
||||
"level": 3,
|
||||
"evidence": "12개 AI 봇 전부 허용. OAI-SearchBot·PerplexityBot·GPTBot UA로 GET → 전부 200"
|
||||
},
|
||||
{
|
||||
"criterionId": "A2",
|
||||
"level": 3,
|
||||
"evidence": "Server: 미표기 직접 응답, Cloudflare 헤더 없음, AI UA 챌린지 없음"
|
||||
},
|
||||
{
|
||||
"criterionId": "A3",
|
||||
"level": 3,
|
||||
"evidence": "서버 렌더링. JS 미실행 HTML에서 본문 텍스트 3,070자 추출"
|
||||
},
|
||||
{
|
||||
"criterionId": "A4",
|
||||
"level": 1,
|
||||
"evidence": "변형 간 최종 URL 불일치(중복 콘텐츠 위험): ['https://view-supporters-sample.vercel.app', 'https://www.view-supporters-sample.vercel.app']"
|
||||
},
|
||||
{
|
||||
"criterionId": "A5",
|
||||
"level": 3,
|
||||
"evidence": "robots.txt에 sitemap 선언, 200 응답. URL 17개, lastmod 최신 2026-09-04"
|
||||
},
|
||||
{
|
||||
"criterionId": "A6",
|
||||
"level": "unverified",
|
||||
"evidence": "[manual] Bing Webmaster Tools 등록 여부는 오너 계정 접근 필요 (또는 site: 검색 수동 확인)"
|
||||
},
|
||||
{
|
||||
"criterionId": "A7",
|
||||
"level": 2,
|
||||
"evidence": "TTFB 0.536s / 초기 HTML 20KB. 시술 페이지 31KB"
|
||||
},
|
||||
{
|
||||
"criterionId": "B1",
|
||||
"level": 3,
|
||||
"evidence": "JSON-LD 2블록 전부 유효 + @graph 연결"
|
||||
},
|
||||
{
|
||||
"criterionId": "B2",
|
||||
"level": 3,
|
||||
"evidence": "@type MedicalClinic + 주소·전화·openingHours·전문분야/geo 구조화"
|
||||
},
|
||||
{
|
||||
"criterionId": "B3",
|
||||
"level": 3,
|
||||
"evidence": "Physician/Attorney 스키마 1개 + 전문분야·소속/sameAs"
|
||||
},
|
||||
{
|
||||
"criterionId": "B4",
|
||||
"level": 3,
|
||||
"evidence": "주소 단일 표기: \"서울특별시 강남구 봉은사로 107, 뷰성형외과빌딩\". 전화 2종: 02-539-1177, 02-539-1132"
|
||||
},
|
||||
{
|
||||
"criterionId": "B5",
|
||||
"level": 3,
|
||||
"evidence": "sameAs 5개 (SNS·버티컬·지도 연결)"
|
||||
},
|
||||
{
|
||||
"criterionId": "B6",
|
||||
"level": 2,
|
||||
"evidence": "rel=canonical self-referencing 존재"
|
||||
},
|
||||
{
|
||||
"criterionId": "B7",
|
||||
"level": 1,
|
||||
"evidence": "hreflang 없음, 다국어 페이지 흔적도 탐지 안 됨 (단일 언어)"
|
||||
},
|
||||
{
|
||||
"criterionId": "C1",
|
||||
"level": 3,
|
||||
"evidence": "홈 히어로 첫 단락과 글 8편 전부 첫 단락에 '뷰성형외과는 2005년 개원한 서울 강남 신논현역 인근 성형외과' 정의문 + 세 줄 요약(직접 답변 블록). 시술 페이지 첫 텍스트가 상담폼이 아니라 답변"
|
||||
},
|
||||
{
|
||||
"criterionId": "C2",
|
||||
"level": 3,
|
||||
"evidence": "FAQ 섹션(자주 묻는 질문) + FAQPage 스키마 존재"
|
||||
},
|
||||
{
|
||||
"criterionId": "C3",
|
||||
"level": 3,
|
||||
"evidence": "H1 1개 서술형: \"뷰성형외과를 알아보는 사람들의질문에 먼저 답합니다.\". H2 7개 중복 없음"
|
||||
},
|
||||
{
|
||||
"criterionId": "C4",
|
||||
"level": 2,
|
||||
"evidence": "시술 페이지(프리저베)에 절개 3~3.5cm, 수술 시간 약 30분, 200~300cc 등 수치가 본문·표·FAQ에 텍스트로 존재. 가격대·회복 일수 표는 없음(가격 노출 범위는 경영진 결정 대기)"
|
||||
},
|
||||
{
|
||||
"criterionId": "C5",
|
||||
"level": 2,
|
||||
"evidence": "HTML 표 1개"
|
||||
},
|
||||
{
|
||||
"criterionId": "C6",
|
||||
"level": 3,
|
||||
"evidence": "최신 갱신 신호 2026-09-04 (dateModified 2026-09-04, sitemap lastmod 2026-09-04) + dateModified 표기"
|
||||
},
|
||||
{
|
||||
"criterionId": "C7",
|
||||
"level": 3,
|
||||
"evidence": "감수 원장 약력이 텍스트로 표기(서울대 의학박사, 고려대의료원 성형외과 전문의 등) + Physician 스키마 sameAs(강남언니 프로필) + 작성자 Person 스키마. 단 감수는 '예정' 상태"
|
||||
},
|
||||
{
|
||||
"criterionId": "C8",
|
||||
"level": 3,
|
||||
"evidence": "세 줄 요약·FAQ 답변 10개 샘플 중 10개가 '뷰성형외과'와 시술명(프리저베·보형물 등)을 문장 안에 포함. 단독으로 떼어도 주어가 살아 있음"
|
||||
},
|
||||
{
|
||||
"criterionId": "D1",
|
||||
"level": 3,
|
||||
"evidence": "병원 엔티티 항목. 공식 사이트와 같은 표면(강남언니 hospitals/189)이라 공식 실측값 준용: 강남언니 hospitals/189 등록. 후기 19,316건, 이벤트 52건, 의료진 26명(정재현 9.6·후기 3,236 등), 병원정보 10"
|
||||
},
|
||||
{
|
||||
"criterionId": "D2",
|
||||
"level": 2,
|
||||
"evidence": "병원 엔티티 항목. 공식 실측값 준용: GBP 존재. 평점 4.9(리뷰 901), 소유자 제공 게시물 2일 전(소유·활성). 단 전화번호 미등록(\"장소의 전화번호 추가\" 상태), 웹사이트가 국문 홈페이"
|
||||
},
|
||||
{
|
||||
"criterionId": "D3",
|
||||
"level": 2,
|
||||
"evidence": "사이트 밖 언급 표면: 유튜브 1,215편·강남언니·언론 1,137건(뉴스룸 집계). 서포터즈 도메인 자체는 신규라 외부 언급 0. 검색 그라운딩 미실시"
|
||||
},
|
||||
{
|
||||
"criterionId": "D4",
|
||||
"level": 1,
|
||||
"evidence": "병원 엔티티 항목. 공식 실측값 준용. 서포터즈는 팩트 시트에서 홈페이지 표기를 그대로 쓰므로 표면 간 불일치를 늘리지 않음"
|
||||
},
|
||||
{
|
||||
"criterionId": "D5",
|
||||
"level": 2,
|
||||
"evidence": "병원 엔티티 항목. 공식 실측값 준용(네이버 플레이스 리뷰 776건). 서포터즈 병원 정보 페이지에 플레이스 링크 추가 예정"
|
||||
},
|
||||
{
|
||||
"criterionId": "D6",
|
||||
"level": 0,
|
||||
"evidence": "Wikidata에서 \"뷰성형외과\" 검색 결과 없음"
|
||||
},
|
||||
{
|
||||
"criterionId": "E1",
|
||||
"level": "unverified",
|
||||
"evidence": "[manual] 서버 로그 접근 필요 (AI 크롤러 히트 + IP 검증)"
|
||||
},
|
||||
{
|
||||
"criterionId": "E2",
|
||||
"level": "unverified",
|
||||
"evidence": "[manual] GA4 접근 필요 (AI 유입 커스텀 채널그룹 여부)"
|
||||
},
|
||||
{
|
||||
"criterionId": "E3",
|
||||
"level": "unverified",
|
||||
"evidence": "[manual] GSC 접근 필요 (AI Overviews 노출)"
|
||||
},
|
||||
{
|
||||
"criterionId": "E4",
|
||||
"level": "unverified",
|
||||
"evidence": "[manual] 기준선 스팟체크 미실시 (질의 10개 × 5회 반복은 별도 단계)"
|
||||
},
|
||||
{
|
||||
"criterionId": "F1",
|
||||
"level": 3,
|
||||
"evidence": "메인 페이지 이미지 18개 중 0개(0%) alt 누락 또는 빈 값"
|
||||
},
|
||||
{
|
||||
"criterionId": "F2",
|
||||
"level": 3,
|
||||
"evidence": "CMS 없음. Astro 정적 빌드(HTML 파일)라 보안 패치 대상 코어·플러그인이 없음. 채점기는 generator 메타가 없어 미검증 처리했으나 실질 위험 없음"
|
||||
},
|
||||
{
|
||||
"criterionId": "F3",
|
||||
"level": 3,
|
||||
"evidence": "초기 HTML 20KB (300KB 미만)"
|
||||
},
|
||||
{
|
||||
"criterionId": "F4",
|
||||
"level": 2,
|
||||
"evidence": "viewport meta 존재, 반응형. CWV 미측정"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -0,0 +1,185 @@
|
||||
{
|
||||
"source": "Viewclinic_Supporters_vs_Official_AEO_Audit.xlsx (2026-09-04 실측)",
|
||||
"results": [
|
||||
{
|
||||
"criterionId": "A1",
|
||||
"level": 0,
|
||||
"evidence": "검색봇 6개 차단: OAI-SearchBot, Googlebot, bingbot, PerplexityBot, Claude-SearchBot, Applebot. 봇 GET: OAI-SearchBot 200·PerplexityBot 200·GPTBot 200"
|
||||
},
|
||||
{
|
||||
"criterionId": "A2",
|
||||
"level": 3,
|
||||
"evidence": "Server: Vercel 직접 응답, Cloudflare 헤더 없음, AI UA 챌린지 없음"
|
||||
},
|
||||
{
|
||||
"criterionId": "A3",
|
||||
"level": 3,
|
||||
"evidence": "서버 렌더링. JS 미실행 HTML에서 본문 텍스트 3,132자 추출"
|
||||
},
|
||||
{
|
||||
"criterionId": "A4",
|
||||
"level": 1,
|
||||
"evidence": "변형 간 최종 URL 불일치(중복 콘텐츠 위험): ['https://view-supporters-sample.vercel.app', 'https://www.view-supporters-sample.vercel.app']"
|
||||
},
|
||||
{
|
||||
"criterionId": "A5",
|
||||
"level": 1,
|
||||
"evidence": "/sitemap.xml 200이나 robots.txt에 Sitemap 미선언. URL 17개"
|
||||
},
|
||||
{
|
||||
"criterionId": "A6",
|
||||
"level": "unverified",
|
||||
"evidence": "[manual] Bing Webmaster Tools 등록 여부는 오너 계정 접근 필요 (또는 site: 검색 수동 확인)"
|
||||
},
|
||||
{
|
||||
"criterionId": "A7",
|
||||
"level": 2,
|
||||
"evidence": "TTFB 0.536s / 초기 HTML 20KB. 시술 페이지 31KB"
|
||||
},
|
||||
{
|
||||
"criterionId": "B1",
|
||||
"level": 3,
|
||||
"evidence": "JSON-LD 2블록 전부 유효 + @graph 연결"
|
||||
},
|
||||
{
|
||||
"criterionId": "B2",
|
||||
"level": 3,
|
||||
"evidence": "@type MedicalClinic + 주소·전화·openingHours·전문분야/geo 구조화"
|
||||
},
|
||||
{
|
||||
"criterionId": "B3",
|
||||
"level": 3,
|
||||
"evidence": "Physician/Attorney 스키마 1개 + 전문분야·소속/sameAs"
|
||||
},
|
||||
{
|
||||
"criterionId": "B4",
|
||||
"level": 3,
|
||||
"evidence": "주소 단일 표기: \"서울특별시 강남구 봉은사로 107, 뷰성형외과빌딩\". 전화 2종: 02-539-1177, 02-539-1132"
|
||||
},
|
||||
{
|
||||
"criterionId": "B5",
|
||||
"level": 3,
|
||||
"evidence": "sameAs 5개 (SNS·버티컬·지도 연결)"
|
||||
},
|
||||
{
|
||||
"criterionId": "B6",
|
||||
"level": 2,
|
||||
"evidence": "rel=canonical self-referencing 존재"
|
||||
},
|
||||
{
|
||||
"criterionId": "B7",
|
||||
"level": 1,
|
||||
"evidence": "hreflang 없음, 다국어 페이지 흔적도 탐지 안 됨 (단일 언어)"
|
||||
},
|
||||
{
|
||||
"criterionId": "C1",
|
||||
"level": 3,
|
||||
"evidence": "홈 히어로 첫 단락과 글 8편 전부 첫 단락에 '뷰성형외과는 2005년 개원한 서울 강남 신논현역 인근 성형외과' 정의문 + 세 줄 요약(직접 답변 블록). 시술 페이지 첫 텍스트가 상담폼이 아니라 답변"
|
||||
},
|
||||
{
|
||||
"criterionId": "C2",
|
||||
"level": 3,
|
||||
"evidence": "FAQ 섹션(자주 묻는 질문) + FAQPage 스키마 존재"
|
||||
},
|
||||
{
|
||||
"criterionId": "C3",
|
||||
"level": 3,
|
||||
"evidence": "H1 1개 서술형: \"뷰성형외과를 알아보는 사람들의질문에 먼저 답합니다.\". H2 7개 중복 없음"
|
||||
},
|
||||
{
|
||||
"criterionId": "C4",
|
||||
"level": 2,
|
||||
"evidence": "시술 페이지(프리저베)에 절개 3~3.5cm, 수술 시간 약 30분, 200~300cc 등 수치가 본문·표·FAQ에 텍스트로 존재. 가격대·회복 일수 표는 없음(가격 노출 범위는 경영진 결정 대기)"
|
||||
},
|
||||
{
|
||||
"criterionId": "C5",
|
||||
"level": 2,
|
||||
"evidence": "HTML 표 1개"
|
||||
},
|
||||
{
|
||||
"criterionId": "C6",
|
||||
"level": 3,
|
||||
"evidence": "최신 갱신 신호 2026-09-04 (dateModified 2026-09-04, sitemap lastmod 2026-09-04) + dateModified 표기"
|
||||
},
|
||||
{
|
||||
"criterionId": "C7",
|
||||
"level": 3,
|
||||
"evidence": "감수 원장 약력이 텍스트로 표기(서울대 의학박사, 고려대의료원 성형외과 전문의 등) + Physician 스키마 sameAs(강남언니 프로필) + 작성자 Person 스키마. 단 감수는 '예정' 상태"
|
||||
},
|
||||
{
|
||||
"criterionId": "C8",
|
||||
"level": 3,
|
||||
"evidence": "세 줄 요약·FAQ 답변 10개 샘플 중 10개가 '뷰성형외과'와 시술명(프리저베·보형물 등)을 문장 안에 포함. 단독으로 떼어도 주어가 살아 있음"
|
||||
},
|
||||
{
|
||||
"criterionId": "D1",
|
||||
"level": 3,
|
||||
"evidence": "병원 엔티티 항목. 공식 사이트와 같은 표면(강남언니 hospitals/189)이라 공식 실측값 준용: 강남언니 hospitals/189 등록. 후기 19,316건, 이벤트 52건, 의료진 26명(정재현 9.6·후기 3,236 등), 병원정보 10"
|
||||
},
|
||||
{
|
||||
"criterionId": "D2",
|
||||
"level": 2,
|
||||
"evidence": "병원 엔티티 항목. 공식 실측값 준용: GBP 존재. 평점 4.9(리뷰 901), 소유자 제공 게시물 2일 전(소유·활성). 단 전화번호 미등록(\"장소의 전화번호 추가\" 상태), 웹사이트가 국문 홈페이"
|
||||
},
|
||||
{
|
||||
"criterionId": "D3",
|
||||
"level": 2,
|
||||
"evidence": "사이트 밖 언급 표면: 유튜브 1,215편·강남언니·언론 1,137건(뉴스룸 집계). 서포터즈 도메인 자체는 신규라 외부 언급 0. 검색 그라운딩 미실시"
|
||||
},
|
||||
{
|
||||
"criterionId": "D4",
|
||||
"level": 1,
|
||||
"evidence": "병원 엔티티 항목. 공식 실측값 준용. 서포터즈는 팩트 시트에서 홈페이지 표기를 그대로 쓰므로 표면 간 불일치를 늘리지 않음"
|
||||
},
|
||||
{
|
||||
"criterionId": "D5",
|
||||
"level": 2,
|
||||
"evidence": "병원 엔티티 항목. 공식 실측값 준용(네이버 플레이스 리뷰 776건). 서포터즈 병원 정보 페이지에 플레이스 링크 추가 예정"
|
||||
},
|
||||
{
|
||||
"criterionId": "D6",
|
||||
"level": 0,
|
||||
"evidence": "Wikidata에서 \"뷰성형외과\" 검색 결과 없음"
|
||||
},
|
||||
{
|
||||
"criterionId": "E1",
|
||||
"level": "unverified",
|
||||
"evidence": "[manual] 서버 로그 접근 필요 (AI 크롤러 히트 + IP 검증)"
|
||||
},
|
||||
{
|
||||
"criterionId": "E2",
|
||||
"level": "unverified",
|
||||
"evidence": "[manual] GA4 접근 필요 (AI 유입 커스텀 채널그룹 여부)"
|
||||
},
|
||||
{
|
||||
"criterionId": "E3",
|
||||
"level": "unverified",
|
||||
"evidence": "[manual] GSC 접근 필요 (AI Overviews 노출)"
|
||||
},
|
||||
{
|
||||
"criterionId": "E4",
|
||||
"level": "unverified",
|
||||
"evidence": "[manual] 기준선 스팟체크 미실시 (질의 10개 × 5회 반복은 별도 단계)"
|
||||
},
|
||||
{
|
||||
"criterionId": "F1",
|
||||
"level": 3,
|
||||
"evidence": "메인 페이지 이미지 18개 중 0개(0%) alt 누락 또는 빈 값"
|
||||
},
|
||||
{
|
||||
"criterionId": "F2",
|
||||
"level": 3,
|
||||
"evidence": "CMS 없음. Astro 정적 빌드(HTML 파일)라 보안 패치 대상 코어·플러그인이 없음. 채점기는 generator 메타가 없어 미검증 처리했으나 실질 위험 없음"
|
||||
},
|
||||
{
|
||||
"criterionId": "F3",
|
||||
"level": 3,
|
||||
"evidence": "초기 HTML 20KB (300KB 미만)"
|
||||
},
|
||||
{
|
||||
"criterionId": "F4",
|
||||
"level": 2,
|
||||
"evidence": "viewport meta 존재, 반응형. CWV 미측정"
|
||||
}
|
||||
]
|
||||
}
|
||||
700
docs/reports/viewclinic/04_supporters/v2_compare.json
Normal file
700
docs/reports/viewclinic/04_supporters/v2_compare.json
Normal file
@ -0,0 +1,700 @@
|
||||
{
|
||||
"generatedAt": "2026-09-07",
|
||||
"rubricVersion": "2.0",
|
||||
"note": "점수는 INFINITH 제품용 진단 기준이며 검색엔진의 공식 점수나 인용 확률이 아니다. 서포터즈 v1.0 레벨은 2026-09-04 실측(기존 비교 엑셀), 공식은 2026-09-07 v1.1 재검증. 사람 판정 4항목은 2026-09-07 초안.",
|
||||
"axes": [
|
||||
{
|
||||
"id": "aeo",
|
||||
"code": "AEO",
|
||||
"name": "답변 준비도",
|
||||
"nameEn": "Answer Readiness",
|
||||
"question": "고객의 질문에 얼마나 명확하고 충분하게 답하는가?",
|
||||
"description": "질문을 고르고, 답을 먼저 쓰고, 한 단락으로 완결하고, 결정에 필요한 정보와 다음 행동까지 닿게 하는가."
|
||||
},
|
||||
{
|
||||
"id": "geo",
|
||||
"code": "GEO",
|
||||
"name": "출처 준비도",
|
||||
"nameEn": "Source Readiness",
|
||||
"question": "그 답을 얼마나 검증하고 신뢰할 수 있는 출처로 사용할 수 있는가?",
|
||||
"description": "주장에 근거가 붙어 있고, 누가 책임지는지 보이고, 병원 사실이 표면마다 같고, 갱신·정정 기록이 있으며, 기계가 인용 식별자를 읽을 수 있는가."
|
||||
}
|
||||
],
|
||||
"items": [
|
||||
{
|
||||
"id": "AEO1",
|
||||
"axis": "aeo",
|
||||
"name": "질문 수요 충족",
|
||||
"weight": 20,
|
||||
"method": "semi",
|
||||
"question": "사람들이 실제로 묻는 질문에 답하는 페이지가 있는가",
|
||||
"derivedFrom": "C2 FAQ 섹션(1)"
|
||||
},
|
||||
{
|
||||
"id": "AEO2",
|
||||
"axis": "aeo",
|
||||
"name": "답변의 직접성",
|
||||
"weight": 20,
|
||||
"method": "semi",
|
||||
"question": "페이지 첫 단락에 답이 먼저 나오는가",
|
||||
"derivedFrom": "C1 정의문형 첫 문단(1)"
|
||||
},
|
||||
{
|
||||
"id": "AEO3",
|
||||
"axis": "aeo",
|
||||
"name": "답변 단위의 자기완결성",
|
||||
"weight": 15,
|
||||
"method": "manual",
|
||||
"question": "한 단락만 떼어내도 누가·어디서·무엇을이 들어 있는가",
|
||||
"derivedFrom": "C8 패시지 자기완결성(1)"
|
||||
},
|
||||
{
|
||||
"id": "AEO4",
|
||||
"axis": "aeo",
|
||||
"name": "의사결정에 필요한 정보",
|
||||
"weight": 15,
|
||||
"method": "semi",
|
||||
"question": "가격대·시간·회복·조건처럼 결정에 쓰는 정보가 텍스트로 있는가",
|
||||
"derivedFrom": "C4 구체 수치 (가격·시간·회복)(0.6), C5 표·비교 구조(0.4)"
|
||||
},
|
||||
{
|
||||
"id": "AEO5",
|
||||
"axis": "aeo",
|
||||
"name": "정보 구조와 가독성",
|
||||
"weight": 10,
|
||||
"method": "auto",
|
||||
"question": "제목이 주제를 서술하고 계층이 논리적인가",
|
||||
"derivedFrom": "C3 제목 계층 의미성(1)"
|
||||
},
|
||||
{
|
||||
"id": "AEO6",
|
||||
"axis": "aeo",
|
||||
"name": "다음 행동의 명확성",
|
||||
"weight": 10,
|
||||
"method": "manual",
|
||||
"question": "답을 읽은 사람이 무엇을 하면 되는지(예약·전화·방문) 바로 보이는가",
|
||||
"derivedFrom": ""
|
||||
},
|
||||
{
|
||||
"id": "AEO7",
|
||||
"axis": "aeo",
|
||||
"name": "핵심 답변의 텍스트 추출",
|
||||
"weight": 5,
|
||||
"method": "auto",
|
||||
"question": "답이 이미지가 아니라 텍스트로 존재하고 서버가 렌더하는가",
|
||||
"derivedFrom": "A3 서버 렌더링 본문(0.6), F1 이미지 대체텍스트(0.4)"
|
||||
},
|
||||
{
|
||||
"id": "AEO8",
|
||||
"axis": "aeo",
|
||||
"name": "사이트 내 답변 발견",
|
||||
"weight": 5,
|
||||
"method": "auto",
|
||||
"question": "크롤러가 답이 있는 페이지를 찾아갈 수 있는가",
|
||||
"derivedFrom": "A5 sitemap.xml 제공(0.6), B6 canonical·중복 제거(0.4)"
|
||||
},
|
||||
{
|
||||
"id": "GEO1",
|
||||
"axis": "geo",
|
||||
"name": "주장과 근거의 일치",
|
||||
"weight": 20,
|
||||
"method": "manual",
|
||||
"question": "수치·수상·효과 주장마다 근거(원문·출처·조건)가 붙어 있는가",
|
||||
"derivedFrom": ""
|
||||
},
|
||||
{
|
||||
"id": "GEO2",
|
||||
"axis": "geo",
|
||||
"name": "출처의 적합성과 추적성",
|
||||
"weight": 15,
|
||||
"method": "auto",
|
||||
"question": "인용된 URL이 정본으로 수렴하고 시간이 지나도 같은 곳을 가리키는가",
|
||||
"derivedFrom": "A4 HTTPS·리다이렉트 정합(1)"
|
||||
},
|
||||
{
|
||||
"id": "GEO3",
|
||||
"axis": "geo",
|
||||
"name": "전문성과 책임 주체",
|
||||
"weight": 15,
|
||||
"method": "semi",
|
||||
"question": "누가 썼고 누가 의학적으로 책임지는지 텍스트와 스키마로 보이는가",
|
||||
"derivedFrom": "C7 전문가 E-E-A-T (의료진 / 변호사)(0.6), B3 전문가 스키마 (Physician / Attorney)(0.4)"
|
||||
},
|
||||
{
|
||||
"id": "GEO4",
|
||||
"axis": "geo",
|
||||
"name": "병원 엔티티와 사실의 정합성",
|
||||
"weight": 15,
|
||||
"method": "semi",
|
||||
"question": "이 병원이 누구인지 사이트 안과 밖(강남언니·GBP·플레이스)에서 같은 사실로 확정되는가",
|
||||
"derivedFrom": "B2 업종 엔티티 스키마 (MedicalClinic / LegalService / LocalBusiness)(0.2), B4 NAP 사이트 내 일관성(0.15), B5 sameAs 연결(0.1), D1 업종 버티컬 프로필 (병의원: 강남언니 / 법률: 로톡)(0.2), D2 Google 비즈니스 프로필(0.1), D4 표면 간 NAP 완전 일치(0.15), D5 네이버 플레이스 (AI 브리핑 전용)(0.05), D6 위키데이터/위키백과 엔티티(0.05)"
|
||||
},
|
||||
{
|
||||
"id": "GEO5",
|
||||
"axis": "geo",
|
||||
"name": "사실의 최신성과 정정",
|
||||
"weight": 10,
|
||||
"method": "auto",
|
||||
"question": "언제 확인한 사실인지 적혀 있고, 틀린 것을 고친 기록이 있는가",
|
||||
"derivedFrom": "C6 신선도 신호(1)"
|
||||
},
|
||||
{
|
||||
"id": "GEO6",
|
||||
"axis": "geo",
|
||||
"name": "원천 대비 추가 가치",
|
||||
"weight": 10,
|
||||
"method": "manual",
|
||||
"question": "다른 곳에 있는 문장을 옮긴 것이 아니라 여기서만 얻는 정보(체크리스트·조건 표·실측)가 있는가",
|
||||
"derivedFrom": ""
|
||||
},
|
||||
{
|
||||
"id": "GEO7",
|
||||
"axis": "geo",
|
||||
"name": "발행 관계와 투명성",
|
||||
"weight": 10,
|
||||
"method": "manual",
|
||||
"question": "누가 운영하고 누구의 지원을 받는지, 광고와 정보가 구분되는지 밝히는가",
|
||||
"derivedFrom": ""
|
||||
},
|
||||
{
|
||||
"id": "GEO8",
|
||||
"axis": "geo",
|
||||
"name": "인용 식별자와 메타데이터 정합성",
|
||||
"weight": 5,
|
||||
"method": "auto",
|
||||
"question": "기계가 읽는 식별자(JSON-LD·메타)가 유효하고 화면 내용과 같은가",
|
||||
"derivedFrom": "B1 JSON-LD 문법 유효성(1)"
|
||||
}
|
||||
],
|
||||
"gates": [
|
||||
{
|
||||
"id": "G1",
|
||||
"name": "AI 크롤러 접근",
|
||||
"criterionIds": [
|
||||
"A1",
|
||||
"A2"
|
||||
],
|
||||
"failLevel": 1,
|
||||
"cap": 40,
|
||||
"axes": [
|
||||
"aeo",
|
||||
"geo"
|
||||
],
|
||||
"reason": "검색용 AI 봇이 차단되거나 CDN이 챌린지를 걸면 답도 출처도 될 수 없다. 점수 대신 상한 40으로 표시한다."
|
||||
},
|
||||
{
|
||||
"id": "G2",
|
||||
"name": "본문 렌더링",
|
||||
"criterionIds": [
|
||||
"A3"
|
||||
],
|
||||
"failLevel": 0,
|
||||
"cap": 40,
|
||||
"axes": [
|
||||
"aeo",
|
||||
"geo"
|
||||
],
|
||||
"reason": "JS 미실행 HTML에 본문이 없으면 크롤러에게 빈 페이지다."
|
||||
}
|
||||
],
|
||||
"variants": [
|
||||
{
|
||||
"key": "official",
|
||||
"label": "공식 홈페이지 (viewclinic.com)",
|
||||
"v1": {
|
||||
"score": 54,
|
||||
"grade": "C"
|
||||
},
|
||||
"aeo": {
|
||||
"score": 42,
|
||||
"raw": 42,
|
||||
"grade": "C",
|
||||
"verified": 8,
|
||||
"capped": []
|
||||
},
|
||||
"geo": {
|
||||
"score": 56,
|
||||
"raw": 56,
|
||||
"grade": "C",
|
||||
"verified": 8,
|
||||
"capped": []
|
||||
},
|
||||
"gates": [
|
||||
{
|
||||
"id": "G1",
|
||||
"passed": true,
|
||||
"failedBy": []
|
||||
},
|
||||
{
|
||||
"id": "G2",
|
||||
"passed": true,
|
||||
"failedBy": []
|
||||
}
|
||||
],
|
||||
"items": [
|
||||
{
|
||||
"id": "AEO1",
|
||||
"pct": 67,
|
||||
"earned": 13.3,
|
||||
"basis": "derived",
|
||||
"signals": "C2=2",
|
||||
"evidence": "C2: v1.1 정정: 프리저베 페이지(/preserve/)에 FAQ 7문항과 유효한 FAQPage 스키마 존재. 모바일 분기로 2문항이 중복. 메인·가슴성형(/breast/main/)·코성형·오시는길에는 FAQ 없음 → 시술별 확산 전이라 레벨 2"
|
||||
},
|
||||
{
|
||||
"id": "AEO2",
|
||||
"pct": 0,
|
||||
"earned": 0,
|
||||
"basis": "derived",
|
||||
"signals": "C1=0",
|
||||
"evidence": "C1: 메인 첫 화면은 슬라이더·\"VIEW SELFIE\" 등 라벨. \"뷰성형외과는 …에 있는 … 병원입니다\" 형태의 정의문 없음. 시술 페이지 첫 텍스트는 오시는길·상담폼"
|
||||
},
|
||||
{
|
||||
"id": "AEO3",
|
||||
"pct": 33,
|
||||
"earned": 5,
|
||||
"basis": "derived",
|
||||
"signals": "C8=1",
|
||||
"evidence": "C8: H2 \"철저한 정밀 검진 / ONE_STOP 시스템 / 1:1 전담 마취시스템\". 단독으로 떼면 어느 병원의 무엇인지 알 수 없음"
|
||||
},
|
||||
{
|
||||
"id": "AEO4",
|
||||
"pct": 40,
|
||||
"earned": 6,
|
||||
"basis": "derived",
|
||||
"signals": "C4=2, C5=0",
|
||||
"evidence": "C4: v1.1 정정: 프리저베 페이지에 수술시간 20~40분·마취(국소 또는 수면)·회복 1~3일, 코성형 페이지에 수술시간 1~2시간·실밥 7일·일상 5~7일, 안면윤곽 페이지에 1~2시간·전신마취·당일 또는 1일 입원이 텍스트로 있음. 가격대 표기는 없 / C5: HTML <table> 없음. 비교 정보는 이미지로 추정"
|
||||
},
|
||||
{
|
||||
"id": "AEO5",
|
||||
"pct": 0,
|
||||
"earned": 0,
|
||||
"basis": "derived",
|
||||
"signals": "C3=0",
|
||||
"evidence": "C3: H1 5개: \"VIEW SELFIE / VIEW EVENT / VIEW SHORTS / VIEW FILM / VIEW SPECIALITY\". 전부 영문 라벨. H2는 같은 문구가 2회씩 중복(모바일/PC 분기). 가슴·코·오시는길 페이지 H1 없음. "
|
||||
},
|
||||
{
|
||||
"id": "AEO6",
|
||||
"pct": 100,
|
||||
"earned": 10,
|
||||
"basis": "judged",
|
||||
"signals": "",
|
||||
"evidence": "모든 페이지 하단에 전화 02-539-1177·주소·온라인 상담 폼이 텍스트로 반복되고(수집기 반복 줄), /counsel/reservation/ 예약 페이지와 카카오톡 채널 링크가 있다. 전화·예약·방문 3경로가 텍스트로 연결됨"
|
||||
},
|
||||
{
|
||||
"id": "AEO7",
|
||||
"pct": 73,
|
||||
"earned": 3.7,
|
||||
"basis": "derived",
|
||||
"signals": "A3=3, F1=1",
|
||||
"evidence": "A3: WordPress 서버 렌더링. JS 미실행 HTML에서 본문 텍스트 10,757자 추출 / F1: 이미지 302개 중 111개(37%) alt 누락 또는 빈 값"
|
||||
},
|
||||
{
|
||||
"id": "AEO8",
|
||||
"pct": 87,
|
||||
"earned": 4.3,
|
||||
"basis": "derived",
|
||||
"signals": "A5=3, B6=2",
|
||||
"evidence": "A5: robots.txt에 sitemap 선언, 200 응답, AIOSEO 3.7.1 동적 생성, URL 3,636개. page/portfolio-sitemap lastmod 최신 2026-08-31. www의 post-sitemap은 2024-05-31에 / B6: rel=canonical self-referencing 존재"
|
||||
},
|
||||
{
|
||||
"id": "GEO1",
|
||||
"pct": 33,
|
||||
"earned": 6.7,
|
||||
"basis": "judged",
|
||||
"signals": "",
|
||||
"evidence": "\"모티바 보형물 국내 사용량 1위(2023, 모티바코리아 전체 수술량 기준)\"처럼 집계 주체·시점이 붙은 주장이 일부 있으나, 모티바 그랜드 어워드·KOL·렛미인 등 대부분의 권위 신호는 이미지·슬로건 안에 있고 원문 링크가 없다. \"마취과 전문의 상주 43% vs 23.1%\" 비교는 출처·기준 시점 없음"
|
||||
},
|
||||
{
|
||||
"id": "GEO2",
|
||||
"pct": 100,
|
||||
"earned": 15,
|
||||
"basis": "derived",
|
||||
"signals": "A4=3",
|
||||
"evidence": "A4: http://viewclinic.com → 301 → https://www.viewclinic.com/ 정본 수렴"
|
||||
},
|
||||
{
|
||||
"id": "GEO3",
|
||||
"pct": 40,
|
||||
"earned": 6,
|
||||
"basis": "derived",
|
||||
"signals": "C7=2, B3=0",
|
||||
"evidence": "C7: 원장·전문의·의료진 언급 17회, 모티바 글로벌 KOL·보건복지부장관표창·갈더마 방문 등 권위 신호 존재. 단 구조화 안 됨 / B3: Physician 스키마 없음. 의료진 정보는 HTML 텍스트·이미지로만 존재"
|
||||
},
|
||||
{
|
||||
"id": "GEO4",
|
||||
"pct": 47,
|
||||
"earned": 7,
|
||||
"basis": "derived",
|
||||
"signals": "B2=1, B4=1, B5=0, D1=3, D2=2, D4=1, D5=2, D6=0",
|
||||
"evidence": "B2: Organization + WebSite + WebPage만 존재. MedicalClinic/LocalBusiness 없음 → 주소·전화·진료시간이 구조화되어 있지 않음 / B4: 주소 3가지 표기 혼재: \"서울 강남구 봉은사로 107 뷰성형외과 빌딩\" / \"서울특별시 강남구 봉은사로 107, 뷰성형외과빌딩\" / 시술 페이지 \"서울시 강남구 봉은사로 107, 뷰성형외과빌딩(논현동…\". 전화 02-539-1177·02-539-11 / B5: Organization.sameAs: [] (빈 배열). 푸터에 YouTube·Instagram·Facebook·네이버블로그 링크가 있으나 스키마에 미연결 / D1: 강남언니 hospitals/189 등록. 후기 19,373건(2026-09-04 재확인, 08-28 19,316건), 이벤트 52건, 의료진 26명(정재현 9.6·후기 3,236 등), 병원정보 10건(CCTV·마취과 상주), 진료시간 표기 / D2: GBP 존재. 평점 4.9(리뷰 901), 소유자 제공 게시물 2일 전(소유·활성). 단 전화번호 미등록(\"장소의 전화번호 추가\" 상태), 웹사이트가 국문 홈페이지가 아닌 viewplasticsurgery.com / D4: 주소 표기 3종 혼재: 홈페이지 \"서울 강남구 봉은사로 107 뷰성형외과 빌딩\" / 강남언니 \"서울 강남구 봉은사로 107 (논현동)\" / GBP \"서울특별시 강남구 봉은사로 107\". GBP 전화 누락, 웹사이트 도메인 불일치 / D5: 네이버 플레이스 존재(리뷰 776건, 2026-04 INFINITH 리포트 기준). 홈페이지 링크 연결 여부 미확인 → 보수적으로 2 / D6: Wikidata에서 \"뷰성형외과\" 검색 결과 없음 (wbsearchentities ko, 2026-08-31)"
|
||||
},
|
||||
{
|
||||
"id": "GEO5",
|
||||
"pct": 100,
|
||||
"earned": 10,
|
||||
"basis": "derived",
|
||||
"signals": "C6=3",
|
||||
"evidence": "C6: 시술 페이지에 article:modified_time·dateModified 2026-04-17 존재, page/portfolio-sitemap lastmod 최신 2026-08-31. blog.viewclinic.com은 2026-09-07 게시까지"
|
||||
},
|
||||
{
|
||||
"id": "GEO6",
|
||||
"pct": 67,
|
||||
"earned": 6.7,
|
||||
"basis": "judged",
|
||||
"signals": "",
|
||||
"evidence": "수술 전·후 주의사항(부위별 8탭, 9,585자)·애프터케어 프로그램(4,572자)·검진센터 항목·안전수술시스템 절차는 병원 고유 정보다. 다만 표·체크리스트 형태가 없고(C5=0) 회복 일정은 문단에 흩어져 있다"
|
||||
},
|
||||
{
|
||||
"id": "GEO7",
|
||||
"pct": 33,
|
||||
"earned": 3.3,
|
||||
"basis": "judged",
|
||||
"signals": "",
|
||||
"evidence": "푸터에 대표·사업자등록번호·주소가 있어 운영 주체는 분명하다. 그러나 홈페이지에 게시한 후기 광고가 수술비 할인 대가를 표시하지 않아 2026-07-12 공정위 시정명령·공표명령을 받았고, 이벤트·전후·후기 섹션이 정보 페이지와 구분 없이 섞여 있다"
|
||||
},
|
||||
{
|
||||
"id": "GEO8",
|
||||
"pct": 33,
|
||||
"earned": 1.7,
|
||||
"basis": "derived",
|
||||
"signals": "B1=1",
|
||||
"evidence": "B1: JSON-LD 2블록 중 ItemList 블록이 문법 오류(쉼표 누락, line 11)로 파싱 실패 → 시술 목록 구조화 데이터 전체 무효"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "supporters_noindex",
|
||||
"label": "서포터즈 샘플 (현재 · noindex)",
|
||||
"v1": {
|
||||
"score": 78,
|
||||
"grade": "B"
|
||||
},
|
||||
"aeo": {
|
||||
"score": 40,
|
||||
"raw": 92,
|
||||
"grade": "D",
|
||||
"verified": 8,
|
||||
"capped": [
|
||||
"G1"
|
||||
]
|
||||
},
|
||||
"geo": {
|
||||
"score": 40,
|
||||
"raw": 77,
|
||||
"grade": "D",
|
||||
"verified": 8,
|
||||
"capped": [
|
||||
"G1"
|
||||
]
|
||||
},
|
||||
"gates": [
|
||||
{
|
||||
"id": "G1",
|
||||
"passed": false,
|
||||
"failedBy": [
|
||||
"A1"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "G2",
|
||||
"passed": true,
|
||||
"failedBy": []
|
||||
}
|
||||
],
|
||||
"items": [
|
||||
{
|
||||
"id": "AEO1",
|
||||
"pct": 100,
|
||||
"earned": 20,
|
||||
"basis": "derived",
|
||||
"signals": "C2=3",
|
||||
"evidence": "C2: FAQ 섹션(자주 묻는 질문) + FAQPage 스키마 존재"
|
||||
},
|
||||
{
|
||||
"id": "AEO2",
|
||||
"pct": 100,
|
||||
"earned": 20,
|
||||
"basis": "derived",
|
||||
"signals": "C1=3",
|
||||
"evidence": "C1: 홈 히어로 첫 단락과 글 8편 전부 첫 단락에 '뷰성형외과는 2005년 개원한 서울 강남 신논현역 인근 성형외과' 정의문 + 세 줄 요약(직접 답변 블록). 시술 페이지 첫 텍스트가 상담폼이 아니라 답변"
|
||||
},
|
||||
{
|
||||
"id": "AEO3",
|
||||
"pct": 100,
|
||||
"earned": 15,
|
||||
"basis": "derived",
|
||||
"signals": "C8=3",
|
||||
"evidence": "C8: 세 줄 요약·FAQ 답변 10개 샘플 중 10개가 '뷰성형외과'와 시술명(프리저베·보형물 등)을 문장 안에 포함. 단독으로 떼어도 주어가 살아 있음"
|
||||
},
|
||||
{
|
||||
"id": "AEO4",
|
||||
"pct": 67,
|
||||
"earned": 10,
|
||||
"basis": "derived",
|
||||
"signals": "C4=2, C5=2",
|
||||
"evidence": "C4: 시술 페이지(프리저베)에 절개 3~3.5cm, 수술 시간 약 30분, 200~300cc 등 수치가 본문·표·FAQ에 텍스트로 존재. 가격대·회복 일수 표는 없음(가격 노출 범위는 경영진 결정 대기) / C5: HTML 표 1개"
|
||||
},
|
||||
{
|
||||
"id": "AEO5",
|
||||
"pct": 100,
|
||||
"earned": 10,
|
||||
"basis": "derived",
|
||||
"signals": "C3=3",
|
||||
"evidence": "C3: H1 1개 서술형: \"뷰성형외과를 알아보는 사람들의질문에 먼저 답합니다.\". H2 7개 중복 없음"
|
||||
},
|
||||
{
|
||||
"id": "AEO6",
|
||||
"pct": 100,
|
||||
"earned": 10,
|
||||
"basis": "judged",
|
||||
"signals": "",
|
||||
"evidence": "헤더 상담 예약 버튼, /visit 페이지에 전화·카카오톡·온라인 예약 3경로가 표로, FactBlock에 tel: 링크. 전부 텍스트 링크"
|
||||
},
|
||||
{
|
||||
"id": "AEO7",
|
||||
"pct": 100,
|
||||
"earned": 5,
|
||||
"basis": "derived",
|
||||
"signals": "A3=3, F1=3",
|
||||
"evidence": "A3: 서버 렌더링. JS 미실행 HTML에서 본문 텍스트 3,132자 추출 / F1: 메인 페이지 이미지 18개 중 0개(0%) alt 누락 또는 빈 값"
|
||||
},
|
||||
{
|
||||
"id": "AEO8",
|
||||
"pct": 47,
|
||||
"earned": 2.3,
|
||||
"basis": "derived",
|
||||
"signals": "A5=1, B6=2",
|
||||
"evidence": "A5: /sitemap.xml 200이나 robots.txt에 Sitemap 미선언. URL 17개 / B6: rel=canonical self-referencing 존재"
|
||||
},
|
||||
{
|
||||
"id": "GEO1",
|
||||
"pct": 67,
|
||||
"earned": 13.3,
|
||||
"basis": "judged",
|
||||
"signals": "",
|
||||
"evidence": "18편 전편의 참고 자료에 유형(병원 제공·원장 설명·제품·규제·플랫폼·2차)과 확인일이 붙고, FDA·ISO 주장은 원문 URL로 연결. 수치 문단 5곳에 개인차·사례 한정이 빠져 게이트 경고 → \"대부분 명시\""
|
||||
},
|
||||
{
|
||||
"id": "GEO2",
|
||||
"pct": 33,
|
||||
"earned": 5,
|
||||
"basis": "derived",
|
||||
"signals": "A4=1",
|
||||
"evidence": "A4: 변형 간 최종 URL 불일치(중복 콘텐츠 위험): ['https://view-supporters-sample.vercel.app', 'https://www.view-supporters-sample.vercel.app']"
|
||||
},
|
||||
{
|
||||
"id": "GEO3",
|
||||
"pct": 100,
|
||||
"earned": 15,
|
||||
"basis": "derived",
|
||||
"signals": "C7=3, B3=3",
|
||||
"evidence": "C7: 감수 원장 약력이 텍스트로 표기(서울대 의학박사, 고려대의료원 성형외과 전문의 등) + Physician 스키마 sameAs(강남언니 프로필) + 작성자 Person 스키마. 단 감수는 '예정' 상태 / B3: Physician/Attorney 스키마 1개 + 전문분야·소속/sameAs"
|
||||
},
|
||||
{
|
||||
"id": "GEO4",
|
||||
"pct": 80,
|
||||
"earned": 12,
|
||||
"basis": "derived",
|
||||
"signals": "B2=3, B4=3, B5=3, D1=3, D2=2, D4=1, D5=2, D6=0",
|
||||
"evidence": "B2: @type MedicalClinic + 주소·전화·openingHours·전문분야/geo 구조화 / B4: 주소 단일 표기: \"서울특별시 강남구 봉은사로 107, 뷰성형외과빌딩\". 전화 2종: 02-539-1177, 02-539-1132 / B5: sameAs 5개 (SNS·버티컬·지도 연결) / D1: 병원 엔티티 항목. 공식 사이트와 같은 표면(강남언니 hospitals/189)이라 공식 실측값 준용: 강남언니 hospitals/189 등록. 후기 19,316건, 이벤트 52건, 의료진 26명(정재현 9.6·후기 3,236 등), 병원정보 10 / D2: 병원 엔티티 항목. 공식 실측값 준용: GBP 존재. 평점 4.9(리뷰 901), 소유자 제공 게시물 2일 전(소유·활성). 단 전화번호 미등록(\"장소의 전화번호 추가\" 상태), 웹사이트가 국문 홈페이 / D4: 병원 엔티티 항목. 공식 실측값 준용. 서포터즈는 팩트 시트에서 홈페이지 표기를 그대로 쓰므로 표면 간 불일치를 늘리지 않음 / D5: 병원 엔티티 항목. 공식 실측값 준용(네이버 플레이스 리뷰 776건). 서포터즈 병원 정보 페이지에 플레이스 링크 추가 예정 / D6: Wikidata에서 \"뷰성형외과\" 검색 결과 없음"
|
||||
},
|
||||
{
|
||||
"id": "GEO5",
|
||||
"pct": 100,
|
||||
"earned": 10,
|
||||
"basis": "derived",
|
||||
"signals": "C6=3",
|
||||
"evidence": "C6: 최신 갱신 신호 2026-09-04 (dateModified 2026-09-04, sitemap lastmod 2026-09-04) + dateModified 표기"
|
||||
},
|
||||
{
|
||||
"id": "GEO6",
|
||||
"pct": 100,
|
||||
"earned": 10,
|
||||
"basis": "judged",
|
||||
"signals": "",
|
||||
"evidence": "원천 콘텐츠 5유형 중 4유형 존재: 상담 질문 체크리스트, 회복 일정·마취 선택·검진 주기 조건 표(18편 전편에 표), 협진·동선 설명(이비인후과 협진·방문 안내), 원장 발언 인용(쇼츠 정리 답). 실측 기록(방문 취재)은 미착수"
|
||||
},
|
||||
{
|
||||
"id": "GEO7",
|
||||
"pct": 67,
|
||||
"earned": 6.7,
|
||||
"basis": "judged",
|
||||
"signals": "",
|
||||
"evidence": "운영 주체(AI오투오·편집 책임 안성민)·정정 이메일·병원 지원 관계를 모든 글 하단 Disclosure와 /about·푸터에 명시. 후기·이벤트 없음. 다만 고지가 글 첫 부분이 아니라 하단에 있어 심사지침의 \"첫 부분\" 기준에는 못 미침"
|
||||
},
|
||||
{
|
||||
"id": "GEO8",
|
||||
"pct": 100,
|
||||
"earned": 5,
|
||||
"basis": "derived",
|
||||
"signals": "B1=3",
|
||||
"evidence": "B1: JSON-LD 2블록 전부 유효 + @graph 연결"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "supporters_indexable",
|
||||
"label": "서포터즈 색인 허용 빌드",
|
||||
"v1": {
|
||||
"score": 86,
|
||||
"grade": "A"
|
||||
},
|
||||
"aeo": {
|
||||
"score": 94,
|
||||
"raw": 94,
|
||||
"grade": "A",
|
||||
"verified": 8,
|
||||
"capped": []
|
||||
},
|
||||
"geo": {
|
||||
"score": 77,
|
||||
"raw": 77,
|
||||
"grade": "B",
|
||||
"verified": 8,
|
||||
"capped": []
|
||||
},
|
||||
"gates": [
|
||||
{
|
||||
"id": "G1",
|
||||
"passed": true,
|
||||
"failedBy": []
|
||||
},
|
||||
{
|
||||
"id": "G2",
|
||||
"passed": true,
|
||||
"failedBy": []
|
||||
}
|
||||
],
|
||||
"items": [
|
||||
{
|
||||
"id": "AEO1",
|
||||
"pct": 100,
|
||||
"earned": 20,
|
||||
"basis": "derived",
|
||||
"signals": "C2=3",
|
||||
"evidence": "C2: FAQ 섹션(자주 묻는 질문) + FAQPage 스키마 존재"
|
||||
},
|
||||
{
|
||||
"id": "AEO2",
|
||||
"pct": 100,
|
||||
"earned": 20,
|
||||
"basis": "derived",
|
||||
"signals": "C1=3",
|
||||
"evidence": "C1: 홈 히어로 첫 단락과 글 8편 전부 첫 단락에 '뷰성형외과는 2005년 개원한 서울 강남 신논현역 인근 성형외과' 정의문 + 세 줄 요약(직접 답변 블록). 시술 페이지 첫 텍스트가 상담폼이 아니라 답변"
|
||||
},
|
||||
{
|
||||
"id": "AEO3",
|
||||
"pct": 100,
|
||||
"earned": 15,
|
||||
"basis": "derived",
|
||||
"signals": "C8=3",
|
||||
"evidence": "C8: 세 줄 요약·FAQ 답변 10개 샘플 중 10개가 '뷰성형외과'와 시술명(프리저베·보형물 등)을 문장 안에 포함. 단독으로 떼어도 주어가 살아 있음"
|
||||
},
|
||||
{
|
||||
"id": "AEO4",
|
||||
"pct": 67,
|
||||
"earned": 10,
|
||||
"basis": "derived",
|
||||
"signals": "C4=2, C5=2",
|
||||
"evidence": "C4: 시술 페이지(프리저베)에 절개 3~3.5cm, 수술 시간 약 30분, 200~300cc 등 수치가 본문·표·FAQ에 텍스트로 존재. 가격대·회복 일수 표는 없음(가격 노출 범위는 경영진 결정 대기) / C5: HTML 표 1개"
|
||||
},
|
||||
{
|
||||
"id": "AEO5",
|
||||
"pct": 100,
|
||||
"earned": 10,
|
||||
"basis": "derived",
|
||||
"signals": "C3=3",
|
||||
"evidence": "C3: H1 1개 서술형: \"뷰성형외과를 알아보는 사람들의질문에 먼저 답합니다.\". H2 7개 중복 없음"
|
||||
},
|
||||
{
|
||||
"id": "AEO6",
|
||||
"pct": 100,
|
||||
"earned": 10,
|
||||
"basis": "judged",
|
||||
"signals": "",
|
||||
"evidence": "헤더 상담 예약 버튼, /visit 페이지에 전화·카카오톡·온라인 예약 3경로가 표로, FactBlock에 tel: 링크. 전부 텍스트 링크"
|
||||
},
|
||||
{
|
||||
"id": "AEO7",
|
||||
"pct": 100,
|
||||
"earned": 5,
|
||||
"basis": "derived",
|
||||
"signals": "A3=3, F1=3",
|
||||
"evidence": "A3: 서버 렌더링. JS 미실행 HTML에서 본문 텍스트 3,070자 추출 / F1: 메인 페이지 이미지 18개 중 0개(0%) alt 누락 또는 빈 값"
|
||||
},
|
||||
{
|
||||
"id": "AEO8",
|
||||
"pct": 87,
|
||||
"earned": 4.3,
|
||||
"basis": "derived",
|
||||
"signals": "A5=3, B6=2",
|
||||
"evidence": "A5: robots.txt에 sitemap 선언, 200 응답. URL 17개, lastmod 최신 2026-09-04 / B6: rel=canonical self-referencing 존재"
|
||||
},
|
||||
{
|
||||
"id": "GEO1",
|
||||
"pct": 67,
|
||||
"earned": 13.3,
|
||||
"basis": "judged",
|
||||
"signals": "",
|
||||
"evidence": "18편 전편의 참고 자료에 유형(병원 제공·원장 설명·제품·규제·플랫폼·2차)과 확인일이 붙고, FDA·ISO 주장은 원문 URL로 연결. 수치 문단 5곳에 개인차·사례 한정이 빠져 게이트 경고 → \"대부분 명시\""
|
||||
},
|
||||
{
|
||||
"id": "GEO2",
|
||||
"pct": 33,
|
||||
"earned": 5,
|
||||
"basis": "derived",
|
||||
"signals": "A4=1",
|
||||
"evidence": "A4: 변형 간 최종 URL 불일치(중복 콘텐츠 위험): ['https://view-supporters-sample.vercel.app', 'https://www.view-supporters-sample.vercel.app']"
|
||||
},
|
||||
{
|
||||
"id": "GEO3",
|
||||
"pct": 100,
|
||||
"earned": 15,
|
||||
"basis": "derived",
|
||||
"signals": "C7=3, B3=3",
|
||||
"evidence": "C7: 감수 원장 약력이 텍스트로 표기(서울대 의학박사, 고려대의료원 성형외과 전문의 등) + Physician 스키마 sameAs(강남언니 프로필) + 작성자 Person 스키마. 단 감수는 '예정' 상태 / B3: Physician/Attorney 스키마 1개 + 전문분야·소속/sameAs"
|
||||
},
|
||||
{
|
||||
"id": "GEO4",
|
||||
"pct": 80,
|
||||
"earned": 12,
|
||||
"basis": "derived",
|
||||
"signals": "B2=3, B4=3, B5=3, D1=3, D2=2, D4=1, D5=2, D6=0",
|
||||
"evidence": "B2: @type MedicalClinic + 주소·전화·openingHours·전문분야/geo 구조화 / B4: 주소 단일 표기: \"서울특별시 강남구 봉은사로 107, 뷰성형외과빌딩\". 전화 2종: 02-539-1177, 02-539-1132 / B5: sameAs 5개 (SNS·버티컬·지도 연결) / D1: 병원 엔티티 항목. 공식 사이트와 같은 표면(강남언니 hospitals/189)이라 공식 실측값 준용: 강남언니 hospitals/189 등록. 후기 19,316건, 이벤트 52건, 의료진 26명(정재현 9.6·후기 3,236 등), 병원정보 10 / D2: 병원 엔티티 항목. 공식 실측값 준용: GBP 존재. 평점 4.9(리뷰 901), 소유자 제공 게시물 2일 전(소유·활성). 단 전화번호 미등록(\"장소의 전화번호 추가\" 상태), 웹사이트가 국문 홈페이 / D4: 병원 엔티티 항목. 공식 실측값 준용. 서포터즈는 팩트 시트에서 홈페이지 표기를 그대로 쓰므로 표면 간 불일치를 늘리지 않음 / D5: 병원 엔티티 항목. 공식 실측값 준용(네이버 플레이스 리뷰 776건). 서포터즈 병원 정보 페이지에 플레이스 링크 추가 예정 / D6: Wikidata에서 \"뷰성형외과\" 검색 결과 없음"
|
||||
},
|
||||
{
|
||||
"id": "GEO5",
|
||||
"pct": 100,
|
||||
"earned": 10,
|
||||
"basis": "derived",
|
||||
"signals": "C6=3",
|
||||
"evidence": "C6: 최신 갱신 신호 2026-09-04 (dateModified 2026-09-04, sitemap lastmod 2026-09-04) + dateModified 표기"
|
||||
},
|
||||
{
|
||||
"id": "GEO6",
|
||||
"pct": 100,
|
||||
"earned": 10,
|
||||
"basis": "judged",
|
||||
"signals": "",
|
||||
"evidence": "원천 콘텐츠 5유형 중 4유형 존재: 상담 질문 체크리스트, 회복 일정·마취 선택·검진 주기 조건 표(18편 전편에 표), 협진·동선 설명(이비인후과 협진·방문 안내), 원장 발언 인용(쇼츠 정리 답). 실측 기록(방문 취재)은 미착수"
|
||||
},
|
||||
{
|
||||
"id": "GEO7",
|
||||
"pct": 67,
|
||||
"earned": 6.7,
|
||||
"basis": "judged",
|
||||
"signals": "",
|
||||
"evidence": "운영 주체(AI오투오·편집 책임 안성민)·정정 이메일·병원 지원 관계를 모든 글 하단 Disclosure와 /about·푸터에 명시. 후기·이벤트 없음. 다만 고지가 글 첫 부분이 아니라 하단에 있어 심사지침의 \"첫 부분\" 기준에는 못 미침"
|
||||
},
|
||||
{
|
||||
"id": "GEO8",
|
||||
"pct": 100,
|
||||
"earned": 5,
|
||||
"basis": "derived",
|
||||
"signals": "B1=3",
|
||||
"evidence": "B1: JSON-LD 2블록 전부 유효 + @graph 연결"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
89
scripts/build_supporters_audit_v2_xlsx.py
Normal file
89
scripts/build_supporters_audit_v2_xlsx.py
Normal file
@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env python3
|
||||
"""공식 홈페이지 vs 서포터즈 사이트 · v2.0 AEO/GEO 비교 엑셀.
|
||||
사용: python3 scripts/build_supporters_audit_v2_xlsx.py <v2_compare.json> <out.xlsx>
|
||||
입력 JSON은 scripts/build_supporters_v2_compare.ts 가 만든다 (점수 계산은 TS 한 곳에서만)."""
|
||||
import json, sys
|
||||
from openpyxl import Workbook
|
||||
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
|
||||
from openpyxl.utils import get_column_letter
|
||||
|
||||
src, out = sys.argv[1], sys.argv[2]
|
||||
D = json.load(open(src, encoding="utf-8"))
|
||||
V = D["variants"]; ITEMS = D["items"]; AX = {a["id"]: a for a in D["axes"]}
|
||||
|
||||
NAVY = "0A1128"; VIOLET = "4F1DA1"; LIGHT = "F4F6FB"; GOOD = "F3F0FF"; WARN = "FFF6ED"; CRIT = "FFF0F0"
|
||||
hf = Font(name="Pretendard", bold=True, color="FFFFFF", size=14); bf = Font(name="Pretendard", size=14); bb = Font(name="Pretendard", size=14, bold=True)
|
||||
tf = Font(name="Pretendard", size=18, bold=True, color=NAVY)
|
||||
fill = lambda h: PatternFill("solid", fgColor=h); thin = Side(style="thin", color="E2E8F0"); bd = Border(left=thin, right=thin, top=thin, bottom=thin)
|
||||
wrap = Alignment(wrap_text=True, vertical="top"); center = Alignment(horizontal="center", vertical="center", wrap_text=True)
|
||||
|
||||
def header(ws, row, cols):
|
||||
for i, c in enumerate(cols, 1):
|
||||
cell = ws.cell(row=row, column=i, value=c); cell.font = hf; cell.fill = fill(VIOLET); cell.alignment = center; cell.border = bd
|
||||
def put(ws, row, col, val, bold=False, bg=None, align=wrap):
|
||||
cell = ws.cell(row=row, column=col, value=val); cell.font = bb if bold else bf; cell.alignment = align; cell.border = bd
|
||||
if bg: cell.fill = fill(bg)
|
||||
return cell
|
||||
def pct_bg(p):
|
||||
if p is None: return LIGHT
|
||||
return CRIT if p <= 40 else WARN if p <= 60 else GOOD
|
||||
def widths(ws, ws_widths):
|
||||
for i, w in enumerate(ws_widths, 1): ws.column_dimensions[get_column_letter(i)].width = w
|
||||
|
||||
wb = Workbook()
|
||||
# ── 1. 요약
|
||||
ws = wb.active; ws.title = "요약"
|
||||
ws["A1"] = "뷰성형외과 공식 홈페이지 vs 뷰 서포터즈 사이트 · AEO / GEO v2.0"; ws["A1"].font = tf
|
||||
ws["A2"] = f"생성 {D['generatedAt']} · 기준표 v{D['rubricVersion']} · {D['note']}"; ws["A2"].font = bf; ws["A2"].alignment = wrap; ws.merge_cells("A2:E2"); ws.row_dimensions[2].height = 60
|
||||
header(ws, 4, ["구분"] + [v["label"] for v in V] + ["비고"])
|
||||
rows = [
|
||||
("AEO 답변 준비도 (100)", [f"{v['aeo']['score']} / {v['aeo']['grade']}" + (f" (상한 {','.join(v['aeo']['capped'])}, 원점수 {v['aeo']['raw']})" if v['aeo']['capped'] else "") for v in V], AX["aeo"]["question"]),
|
||||
("GEO 출처 준비도 (100)", [f"{v['geo']['score']} / {v['geo']['grade']}" + (f" (상한 {','.join(v['geo']['capped'])}, 원점수 {v['geo']['raw']})" if v['geo']['capped'] else "") for v in V], AX["geo"]["question"]),
|
||||
("검증 항목 (AEO / GEO, 각 8)", [f"{v['aeo']['verified']} / {v['geo']['verified']}" for v in V], "미검증 항목은 분모 제외"),
|
||||
("게이트", [" · ".join(f"{g['id']} {'통과' if g['passed'] else '실패(' + ','.join(g['failedBy']) + ')'}" for g in v["gates"]) for v in V], "실패 시 두 축 상한 40. 점수가 아니라 상한"),
|
||||
("v1.0 종합 (참고)", [f"{v['v1']['score']} / {v['v1']['grade']}" for v in V], "36항목 종합. v2.0과 비교하지 않는다"),
|
||||
]
|
||||
for r, (k, vals, note) in enumerate(rows, 5):
|
||||
put(ws, r, 1, k, bold=True, bg=LIGHT)
|
||||
for i, val in enumerate(vals, 2): put(ws, r, i, val, bold=(r <= 6), align=center)
|
||||
put(ws, r, len(vals) + 2, note)
|
||||
r = 5 + len(rows) + 1
|
||||
put(ws, r, 1, "게이트 정의", bold=True, bg=LIGHT); ws.merge_cells(start_row=r, start_column=1, end_row=r, end_column=5)
|
||||
for g in D["gates"]:
|
||||
r += 1; put(ws, r, 1, f"{g['id']} {g['name']}", bold=True); put(ws, r, 2, f"v1.0 {', '.join(g['criterionIds'])} 중 하나라도 level ≤ {g['failLevel']} → 상한 {g['cap']}"); ws.merge_cells(start_row=r, start_column=2, end_row=r, end_column=3); put(ws, r, 4, g["reason"]); ws.merge_cells(start_row=r, start_column=4, end_row=r, end_column=5)
|
||||
widths(ws, [30, 34, 34, 34, 50])
|
||||
|
||||
# ── 2. 항목별 비교
|
||||
ws = wb.create_sheet("항목별 비교 (16항목)")
|
||||
cols = ["ID", "축", "항목", "배점", "채점", "v1.0 파생 신호(비중)"]
|
||||
for v in V: cols += [f"{v['label']} %", "점수", "근거"]
|
||||
header(ws, 1, cols)
|
||||
for r, it in enumerate(ITEMS, 2):
|
||||
put(ws, r, 1, it["id"], bold=True); put(ws, r, 2, AX[it["axis"]]["code"] + " " + AX[it["axis"]]["name"]); put(ws, r, 3, it["name"] + "\n" + it["question"]); put(ws, r, 4, it["weight"], align=center)
|
||||
put(ws, r, 5, {"auto": "자동", "semi": "반자동", "manual": "사람"}[it["method"]], align=center); put(ws, r, 6, it["derivedFrom"] or "없음 (사람 판정)")
|
||||
c = 7
|
||||
for v in V:
|
||||
row = next(x for x in v["items"] if x["id"] == it["id"])
|
||||
put(ws, r, c, "미검증" if row["pct"] is None else row["pct"], bg=pct_bg(row["pct"]), align=center, bold=True)
|
||||
put(ws, r, c + 1, f"{row['earned']} / {it['weight']}" if row["pct"] is not None else "—", align=center)
|
||||
put(ws, r, c + 2, ("[사람 판정] " if row["basis"] == "judged" else "") + (row["evidence"] or row["signals"]))
|
||||
c += 3
|
||||
ws.row_dimensions[r].height = 120
|
||||
widths(ws, [8, 16, 34, 7, 8, 34] + [10, 10, 60] * len(V)); ws.freeze_panes = "D2"
|
||||
|
||||
# ── 3. 축별 해석
|
||||
ws = wb.create_sheet("해석")
|
||||
header(ws, 1, ["축", "공식 홈페이지가 낮은 이유", "서포터즈가 높은 이유", "서포터즈 남은 과제"])
|
||||
def low(v, axis, n=3):
|
||||
xs = [x for x in v["items"] if x["id"].startswith(axis.upper()) and x["pct"] is not None]; xs.sort(key=lambda x: x["pct"]); return xs[:n]
|
||||
def hi(v, axis, n=3):
|
||||
xs = [x for x in v["items"] if x["id"].startswith(axis.upper()) and x["pct"] is not None]; xs.sort(key=lambda x: -x["pct"]); return xs[:n]
|
||||
off, sup = V[0], V[-1]; name = {i["id"]: i["name"] for i in ITEMS}
|
||||
for r, ax in enumerate(["aeo", "geo"], 2):
|
||||
put(ws, r, 1, f"{AX[ax]['code']} {AX[ax]['name']}\n공식 {off[ax]['score']} → 서포터즈 {sup[ax]['score']}", bold=True, bg=LIGHT)
|
||||
put(ws, r, 2, "\n".join(f"{x['id']} {name[x['id']]} {x['pct']}%" for x in low(off, ax)))
|
||||
put(ws, r, 3, "\n".join(f"{x['id']} {name[x['id']]} {x['pct']}%" for x in hi(sup, ax)))
|
||||
put(ws, r, 4, "\n".join(f"{x['id']} {name[x['id']]} {x['pct']}%" for x in low(sup, ax)))
|
||||
ws.row_dimensions[r].height = 110
|
||||
widths(ws, [26, 44, 44, 44])
|
||||
wb.save(out); print("saved", out)
|
||||
67
scripts/build_supporters_v2_compare.ts
Normal file
67
scripts/build_supporters_v2_compare.ts
Normal file
@ -0,0 +1,67 @@
|
||||
/**
|
||||
* 공식 홈페이지 vs 서포터즈 사이트 · v2.0 AEO/GEO 비교 데이터.
|
||||
* npx tsx scripts/build_supporters_v2_compare.ts <out.json>
|
||||
* 입력
|
||||
* - 공식: src/data/discovery_viewclinic.ts (v1.0 실측 + v2 사람 판정)
|
||||
* - 서포터즈: docs/reports/viewclinic/04_supporters/v1_levels/*.json (2026-09-04 v1.0 실측, 기존 비교 엑셀에서 추출)
|
||||
* + 아래 SUPPORTERS_V2 사람 판정 (2026-09-07, 사이트 소스 기준)
|
||||
* 출력 JSON은 scripts/build_supporters_audit_v2_xlsx.py 가 엑셀로 만든다.
|
||||
*/
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
import { AEO_GEO_RUBRIC } from '../src/data/aeoGeoRubric';
|
||||
import { AEO_GEO_RUBRIC_V2 } from '../src/data/aeoGeoRubricV2';
|
||||
import { DISCOVERY_VIEWCLINIC } from '../src/data/discovery_viewclinic';
|
||||
import { scoreDiscovery } from '../src/lib/discoveryScore';
|
||||
import { scoreDiscoveryV2 } from '../src/lib/discoveryScoreV2';
|
||||
import type { CriterionResult, DiscoveryResult } from '../src/types/discovery';
|
||||
|
||||
const out = process.argv[2] ?? 'docs/reports/viewclinic/04_supporters/v2_compare.json';
|
||||
const base = new URL('../docs/reports/viewclinic/04_supporters/v1_levels/', import.meta.url);
|
||||
const load = (f: string) => JSON.parse(readFileSync(new URL(f, base), 'utf8')) as { source: string; results: CriterionResult[] };
|
||||
|
||||
// 서포터즈 사이트 v2 사람 판정. 근거는 supporters/ 소스와 2026-09-07 게이트 결과.
|
||||
const SUPPORTERS_V2: CriterionResult[] = [
|
||||
{ criterionId: 'AEO6', level: 3, evidence: '헤더 상담 예약 버튼, /visit 페이지에 전화·카카오톡·온라인 예약 3경로가 표로, FactBlock에 tel: 링크. 전부 텍스트 링크', source: 'supporters/src/pages/visit.astro · layouts/Base.astro' },
|
||||
{ criterionId: 'GEO1', level: 2, evidence: '18편 전편의 참고 자료에 유형(병원 제공·원장 설명·제품·규제·플랫폼·2차)과 확인일이 붙고, FDA·ISO 주장은 원문 URL로 연결. 수치 문단 5곳에 개인차·사례 한정이 빠져 게이트 경고 → "대부분 명시"', source: 'supporters/scripts/check.mjs 2026-09-07 (NUMERIC_NO_QUALIFIER 5)' },
|
||||
{ criterionId: 'GEO6', level: 3, evidence: '원천 콘텐츠 5유형 중 4유형 존재: 상담 질문 체크리스트, 회복 일정·마취 선택·검진 주기 조건 표(18편 전편에 표), 협진·동선 설명(이비인후과 협진·방문 안내), 원장 발언 인용(쇼츠 정리 답). 실측 기록(방문 취재)은 미착수', source: 'docs/reports/viewclinic/04_supporters/Viewclinic_Supporters_Content_Plan_Phase2.md' },
|
||||
{ criterionId: 'GEO7', level: 2, evidence: '운영 주체(AI오투오·편집 책임 안성민)·정정 이메일·병원 지원 관계를 모든 글 하단 Disclosure와 /about·푸터에 명시. 후기·이벤트 없음. 다만 고지가 글 첫 부분이 아니라 하단에 있어 심사지침의 "첫 부분" 기준에는 못 미침', source: 'supporters/src/components/Disclosure.astro · v2 §12-2' },
|
||||
];
|
||||
|
||||
function asResult(id: string, name: string, url: string, results: CriterionResult[], v2: CriterionResult[]): DiscoveryResult {
|
||||
return { id, clinicName: name, url, industry: '', auditedAt: '2026-09-04', rubricVersion: '2.0', conditions: [], results, keyFindings: [], actions: [], notDoing: [], v2Results: v2 };
|
||||
}
|
||||
|
||||
const official = DISCOVERY_VIEWCLINIC;
|
||||
const noindex = load('supporters_noindex.json');
|
||||
const indexable = load('supporters_indexable.json');
|
||||
const variants = [
|
||||
{ key: 'official', label: '공식 홈페이지 (viewclinic.com)', result: official },
|
||||
{ key: 'supporters_noindex', label: '서포터즈 샘플 (현재 · noindex)', result: asResult('supporters-noindex', '뷰 서포터즈 (noindex)', 'https://view-supporters-sample.vercel.app', noindex.results, SUPPORTERS_V2) },
|
||||
{ key: 'supporters_indexable', label: '서포터즈 색인 허용 빌드', result: asResult('supporters-indexable', '뷰 서포터즈 (색인 허용)', 'https://view-supporters-sample.vercel.app', indexable.results, SUPPORTERS_V2) },
|
||||
];
|
||||
|
||||
const v1names = Object.fromEntries(AEO_GEO_RUBRIC.criteria.map((c) => [c.id, c.name]));
|
||||
const data = {
|
||||
generatedAt: new Date().toISOString().slice(0, 10),
|
||||
rubricVersion: AEO_GEO_RUBRIC_V2.version,
|
||||
note: '점수는 INFINITH 제품용 진단 기준이며 검색엔진의 공식 점수나 인용 확률이 아니다. 서포터즈 v1.0 레벨은 2026-09-04 실측(기존 비교 엑셀), 공식은 2026-09-07 v1.1 재검증. 사람 판정 4항목은 2026-09-07 초안.',
|
||||
axes: AEO_GEO_RUBRIC_V2.axes,
|
||||
items: AEO_GEO_RUBRIC_V2.items.map((i) => ({ id: i.id, axis: i.axis, name: i.name, weight: i.weight, method: i.method, question: i.question, derivedFrom: i.derivedFrom.map((d) => `${d.criterionId} ${v1names[d.criterionId]}(${d.weight})`).join(', ') })),
|
||||
gates: AEO_GEO_RUBRIC_V2.gates,
|
||||
variants: variants.map((v) => {
|
||||
const s1 = scoreDiscovery(AEO_GEO_RUBRIC, v.result);
|
||||
const s2 = scoreDiscoveryV2(AEO_GEO_RUBRIC_V2, v.result);
|
||||
const judged = new Map((v.result.v2Results ?? []).map((r) => [r.criterionId, r]));
|
||||
return {
|
||||
key: v.key, label: v.label,
|
||||
v1: { score: s1.score, grade: s1.grade },
|
||||
aeo: { score: s2.aeo.score, raw: s2.aeo.rawScore, grade: s2.aeo.grade, verified: s2.aeo.verifiedCount, capped: s2.aeo.cappedBy.map((g) => g.gate.id) },
|
||||
geo: { score: s2.geo.score, raw: s2.geo.rawScore, grade: s2.geo.grade, verified: s2.geo.verifiedCount, capped: s2.geo.cappedBy.map((g) => g.gate.id) },
|
||||
gates: s2.gates.map((g) => ({ id: g.gate.id, passed: g.passed, failedBy: g.failedBy })),
|
||||
items: [...s2.aeo.items, ...s2.geo.items].map((it) => ({ id: it.item.id, pct: it.pct === null ? null : Math.round(it.pct * 100), earned: it.earned, basis: it.basis, signals: it.signals.map((s) => `${s.criterionId}=${s.level}`).join(', '), evidence: it.basis === 'judged' ? judged.get(it.item.id)?.evidence ?? '' : it.signals.map((s) => `${s.criterionId}: ${(v.result.results.find((r) => r.criterionId === s.criterionId)?.evidence ?? '').slice(0, 140)}`).join(' / ') })),
|
||||
};
|
||||
}),
|
||||
};
|
||||
writeFileSync(out, JSON.stringify(data, null, 1));
|
||||
for (const v of data.variants) console.log(`${v.label}: v1 ${v.v1.score}/${v.v1.grade} · AEO ${v.aeo.score}/${v.aeo.grade}${v.aeo.capped.length ? ' (상한 ' + v.aeo.capped.join(',') + ', 원점수 ' + v.aeo.raw + ')' : ''} · GEO ${v.geo.score}/${v.geo.grade}${v.geo.capped.length ? ' (상한, 원점수 ' + v.geo.raw + ')' : ''}`);
|
||||
console.log('written', out);
|
||||
@ -6,6 +6,18 @@
|
||||
import { motion } from 'motion/react';
|
||||
import type { OverallScore } from '../../types/discovery';
|
||||
|
||||
/** 랜딩이 쓰는 축 요약. v2.0 AEO·GEO 계산값에서만 만든다 (하드코딩 금지) */
|
||||
export interface AxisSummary {
|
||||
code: string;
|
||||
name: string;
|
||||
nameEn: string;
|
||||
score: number;
|
||||
grade: OverallScore['grade'];
|
||||
/** 다음 등급 목표. A등급이면 null */
|
||||
target: { score: number; grade: OverallScore['grade'] } | null;
|
||||
items: { id: string; name: string; pct: number | null }[];
|
||||
}
|
||||
|
||||
const NODES = [
|
||||
{ letter: 'A', label: 'Audit', pos: 'top-0 left-1/2 -translate-x-1/2 -translate-y-1/2' },
|
||||
{ letter: 'B', label: 'Build', pos: 'top-1/2 right-0 translate-x-1/2 -translate-y-1/2' },
|
||||
@ -14,16 +26,12 @@ const NODES = [
|
||||
];
|
||||
|
||||
interface AbmrLoopProps {
|
||||
overall: OverallScore;
|
||||
/** 다음 등급 목표 (A등급이면 null) */
|
||||
target: { score: number; grade: OverallScore['grade'] } | null;
|
||||
axes: AxisSummary[];
|
||||
}
|
||||
|
||||
export default function AbmrLoop({ overall, target }: AbmrLoopProps) {
|
||||
export default function AbmrLoop({ axes }: AbmrLoopProps) {
|
||||
const chips = [
|
||||
...(target
|
||||
? [`점수 ${overall.score} → ${target.score}+`, `등급 ${overall.grade} → ${target.grade}`]
|
||||
: [`점수 ${overall.score} 유지 검증`, `등급 ${overall.grade} 유지 검증`]),
|
||||
...axes.map((ax) => (ax.target ? `${ax.code} ${ax.score}/${ax.grade} → ${ax.target.score}+/${ax.target.grade}` : `${ax.code} ${ax.score}/${ax.grade} 유지 검증`)),
|
||||
'언급률 상승 추적',
|
||||
];
|
||||
|
||||
|
||||
@ -6,13 +6,12 @@
|
||||
import { Link } from 'react-router';
|
||||
import { motion } from 'motion/react';
|
||||
import { ArrowRight } from 'lucide-react';
|
||||
import type { OverallScore } from '../../types/discovery';
|
||||
import AbmrLoop from './AbmrLoop';
|
||||
import type { AxisSummary } from './AbmrLoop';
|
||||
import AiAnswerMock from './AiAnswerMock';
|
||||
|
||||
interface DiscoveryHeroProps {
|
||||
overall: OverallScore;
|
||||
target: { score: number; grade: OverallScore['grade'] } | null;
|
||||
axes: AxisSummary[];
|
||||
clinicName: string;
|
||||
sampleReportPath: string;
|
||||
criteriaCount: number;
|
||||
@ -21,8 +20,7 @@ interface DiscoveryHeroProps {
|
||||
}
|
||||
|
||||
export default function DiscoveryHero({
|
||||
overall,
|
||||
target,
|
||||
axes,
|
||||
clinicName,
|
||||
sampleReportPath,
|
||||
criteriaCount,
|
||||
@ -72,8 +70,8 @@ export default function DiscoveryHero({
|
||||
className="text-lg md:text-xl text-slate-600 leading-relaxed max-w-3xl mb-10 break-keep"
|
||||
>
|
||||
고객은 이제 ChatGPT와 Perplexity에게 병원을 묻습니다.
|
||||
<br className="hidden md:block" /> 답변에 인용될 근거를 {criteriaCount}개 항목으로 실측
|
||||
진단하고, 8주 안에 구축합니다.
|
||||
<br className="hidden md:block" /> 답변에 인용될 근거를 {criteriaCount}개 항목으로 실측해
|
||||
답변 준비도와 출처 준비도로 진단하고, 8주 안에 구축합니다.
|
||||
</motion.p>
|
||||
|
||||
{/* CTAs */}
|
||||
@ -101,7 +99,7 @@ export default function DiscoveryHero({
|
||||
|
||||
{/* ABMR loop + AI answer mock */}
|
||||
<div className="w-full max-w-7xl grid md:grid-cols-2 gap-6">
|
||||
<AbmrLoop overall={overall} target={target} />
|
||||
<AbmrLoop axes={axes} />
|
||||
<AiAnswerMock clinicName={clinicName} criteriaCount={criteriaCount} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -22,7 +22,7 @@ export default function HowItWorks({ criteriaCount, categoryCount }: HowItWorksP
|
||||
{
|
||||
step: 'STEP 01',
|
||||
title: 'Audit',
|
||||
detail: `AEO/GEO 기준표 v1.0으로 ${criteriaCount}개 항목을 실측합니다. 크롤러 접근성부터 인용 가능 콘텐츠, 측정 인프라까지 ${categoryCount}개 카테고리 100점 척도. 항목마다 근거 등급을 명시합니다.`,
|
||||
detail: `기준표 v2.0으로 ${criteriaCount}개 항목을 실측해 두 점수로 집계합니다. AEO 답변 준비도(질문에 얼마나 명확하게 답하는가)와 GEO 출처 준비도(그 답을 얼마나 믿을 수 있는 출처로 쓸 수 있는가), 각 100점. ${categoryCount}개 실측 카테고리, 항목마다 근거 등급을 명시합니다.`,
|
||||
icon: (
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
<circle cx="11" cy="11" r="6.5" stroke="#6C5CE7" strokeWidth="1.6" />
|
||||
|
||||
@ -2,16 +2,19 @@
|
||||
* 8주 로드맵 타임라인. 타이틀의 등급 전환(C → B)은 계산값에서 파생.
|
||||
*/
|
||||
import { motion } from 'motion/react';
|
||||
import type { OverallScore } from '../../types/discovery';
|
||||
import type { AxisSummary } from './AbmrLoop';
|
||||
|
||||
interface RoadmapProps {
|
||||
grade: OverallScore['grade'];
|
||||
targetGrade: OverallScore['grade'] | null;
|
||||
axes: AxisSummary[];
|
||||
criteriaCount: number;
|
||||
unverifiedCount: number;
|
||||
}
|
||||
|
||||
export default function Roadmap({ grade, targetGrade, criteriaCount, unverifiedCount }: RoadmapProps) {
|
||||
export default function Roadmap({ axes, criteriaCount, unverifiedCount }: RoadmapProps) {
|
||||
const rising = axes.filter((ax) => ax.target);
|
||||
const title = rising.length
|
||||
? `8주 안에, ${rising.map((ax) => `${ax.name} ${ax.grade}에서 ${ax.target!.grade}로`).join(', ')} 올립니다.`
|
||||
: `8주마다, ${axes.map((ax) => `${ax.name} ${ax.grade}`).join(' · ')} 등급을 재검증합니다.`;
|
||||
const phases = [
|
||||
{
|
||||
week: 'W1–2',
|
||||
@ -26,7 +29,7 @@ export default function Roadmap({ grade, targetGrade, criteriaCount, unverifiedC
|
||||
{
|
||||
week: 'W7–8',
|
||||
title: '재진단·보고',
|
||||
detail: `${criteriaCount}개 항목을 재측정하고 미검증 ${unverifiedCount}개를 반영해 총점을 다시 산정합니다.`,
|
||||
detail: `${criteriaCount}개 항목을 재측정하고 미검증 ${unverifiedCount}개를 반영해 답변 준비도와 출처 준비도를 다시 산정합니다.`,
|
||||
},
|
||||
];
|
||||
|
||||
@ -42,9 +45,7 @@ export default function Roadmap({ grade, targetGrade, criteriaCount, unverifiedC
|
||||
>
|
||||
<div className="text-sm font-bold tracking-[0.24em] text-accent mb-4">8-WEEK ROADMAP</div>
|
||||
<h2 className="font-serif text-3xl md:text-[40px] font-bold leading-tight text-primary-900 break-keep">
|
||||
{targetGrade
|
||||
? `8주 안에, ${grade}에서 ${targetGrade}로 올립니다.`
|
||||
: `8주마다, ${grade} 등급을 재검증합니다.`}
|
||||
{title}
|
||||
</h2>
|
||||
</motion.div>
|
||||
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
/**
|
||||
* 샘플 리포트 섹션 — 뷰성형외과 실측 스냅샷.
|
||||
* 점수·등급·카테고리 %·항목 수는 전부 discoveryScore 계산값에서 렌더 (하드코딩 금지).
|
||||
* 점수·등급·항목 %·항목 수는 전부 discoveryScore(v1 실측)·discoveryScoreV2(AEO/GEO) 계산값에서 렌더 (하드코딩 금지).
|
||||
* 발견 칩 3장의 카피는 result.keyFindings를 랜딩용으로 압축한 시안 확정 문안이다.
|
||||
*/
|
||||
import { motion } from 'motion/react';
|
||||
import type { DiscoveryResult, OverallScore } from '../../types/discovery';
|
||||
import { scorePctColor } from '../../lib/discoveryScore';
|
||||
import { ScoreRing } from '../report/ui/ScoreRing';
|
||||
import type { AxisSummary } from './AbmrLoop';
|
||||
|
||||
const GRADE_BADGE_STYLE: Record<OverallScore['grade'], string> = {
|
||||
A: 'bg-[#EFF0FF] border-[#C5CBF5] text-[#3A3F7C]',
|
||||
@ -15,7 +16,7 @@ const GRADE_BADGE_STYLE: Record<OverallScore['grade'], string> = {
|
||||
D: 'bg-[#FFF0F0] border-[#F5D5DC] text-[#7C3A4B]',
|
||||
};
|
||||
|
||||
/** keyFindings 압축본 (시안 확정 카피). 수치는 2026-08-28 실측 스냅샷과 일치한다. */
|
||||
/** keyFindings 압축본 (시안 확정 카피). 수치는 2026-09-07 재검증(v1.1) 스냅샷과 일치한다. */
|
||||
const FINDING_CHIPS = [
|
||||
{
|
||||
label: '치명',
|
||||
@ -29,23 +30,24 @@ const FINDING_CHIPS = [
|
||||
chipClass: 'bg-[#FFF6ED] border-[#F5E0C5] text-[#7C5C3A]',
|
||||
dotColor: '#D4A872',
|
||||
title: '인용할 문장이 없음',
|
||||
detail: '정의문 0개, FAQ 0개, HTML 표 0개. 답변엔진이 잘라 갈 단락이 없습니다.',
|
||||
detail: '정의문 0개, HTML 표 0개, FAQ는 프리저베 한 페이지뿐. 답변엔진이 잘라 갈 단락이 없습니다.',
|
||||
},
|
||||
{
|
||||
label: '강점',
|
||||
chipClass: 'bg-[#F3F0FF] border-[#D5CDF5] text-[#4A3A7C]',
|
||||
dotColor: '#9B8AD4',
|
||||
title: '표면 자산은 이미 강함',
|
||||
detail: '강남언니 후기 19,316건, GBP 4.9점. 홈페이지가 따라가면 바로 인용 근거가 됩니다.',
|
||||
detail: '강남언니 후기 19,373건, GBP 4.9점. 홈페이지가 따라가면 바로 인용 근거가 됩니다.',
|
||||
},
|
||||
];
|
||||
|
||||
interface SampleReportProps {
|
||||
overall: OverallScore;
|
||||
axes: AxisSummary[];
|
||||
result: DiscoveryResult;
|
||||
}
|
||||
|
||||
export default function SampleReport({ overall, result }: SampleReportProps) {
|
||||
export default function SampleReport({ overall, axes, result }: SampleReportProps) {
|
||||
const totalCriteria = overall.categories.reduce((sum, c) => sum + c.totalCount, 0);
|
||||
const verifiedCriteria = overall.categories.reduce((sum, c) => sum + c.verifiedCount, 0);
|
||||
const unverifiedCriteria = totalCriteria - verifiedCriteria;
|
||||
@ -66,8 +68,8 @@ export default function SampleReport({ overall, result }: SampleReportProps) {
|
||||
읽을 수는 있지만, 인용할 근거가 없습니다.
|
||||
</h2>
|
||||
<p className="text-[17px] leading-[1.7] text-slate-500 mb-12 break-keep">
|
||||
실제 1차 진단 결과입니다. 어디가 강하고 어디가 비어 있는지, 점수는 카테고리 단위로
|
||||
드러납니다.
|
||||
실제 1차 진단 결과입니다. 답변 준비도(AEO)와 출처 준비도(GEO)를 각각 100점으로 보고,
|
||||
어디가 강하고 어디가 비어 있는지 항목 단위로 드러냅니다.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
@ -80,17 +82,21 @@ export default function SampleReport({ overall, result }: SampleReportProps) {
|
||||
transition={{ duration: 0.6, delay: 0.1 }}
|
||||
className="flex flex-col items-center justify-center gap-4 bg-white border border-slate-200 rounded-2xl px-8 py-10 shadow-[0_8px_32px_rgba(31,38,135,0.07)]"
|
||||
>
|
||||
<ScoreRing score={overall.score} size={160} valueClassName="text-[40px]" subLabel="/ 100" />
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className={`px-3.5 py-[5px] border rounded-full ${GRADE_BADGE_STYLE[overall.grade]}`}>
|
||||
<span className="text-sm font-bold">등급 {overall.grade}</span>
|
||||
<div className="flex items-start gap-6">
|
||||
{axes.map((ax) => (
|
||||
<div key={ax.code} className="flex flex-col items-center gap-2.5">
|
||||
<ScoreRing score={ax.score} size={120} valueClassName="text-[32px]" subLabel="/ 100" />
|
||||
<div className={`px-3.5 py-[5px] border rounded-full ${GRADE_BADGE_STYLE[ax.grade]}`}>
|
||||
<span className="text-sm font-bold">{ax.code} 등급 {ax.grade}</span>
|
||||
</div>
|
||||
<span className="text-sm text-slate-500">AI Answer Readiness</span>
|
||||
<span className="text-sm text-slate-500">{ax.name}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-[13.5px] leading-relaxed text-slate-400 text-center break-keep">
|
||||
{totalCriteria}개 항목 중 {verifiedCriteria}개 실측.
|
||||
{totalCriteria}개 실측 항목 중 {verifiedCriteria}개 확인.
|
||||
<br />
|
||||
미검증 {unverifiedCriteria}개는 분모에서 제외했습니다.
|
||||
미검증 {unverifiedCriteria}개는 분모에서 제외했습니다. v1.0 종합 {overall.score}점.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
@ -102,24 +108,28 @@ export default function SampleReport({ overall, result }: SampleReportProps) {
|
||||
transition={{ duration: 0.6, delay: 0.2 }}
|
||||
className="flex flex-col justify-center gap-[18px] bg-white border border-slate-200 rounded-2xl px-6 md:px-10 py-9 shadow-[0_8px_32px_rgba(31,38,135,0.07)]"
|
||||
>
|
||||
{overall.categories.map((cat) => {
|
||||
const color = scorePctColor(cat.pct);
|
||||
<div className="grid md:grid-cols-2 gap-x-10 gap-y-[18px]">
|
||||
{axes.map((ax) => (
|
||||
<div key={ax.code} className="flex flex-col gap-[14px]">
|
||||
<div className="flex justify-between items-baseline gap-3 border-b border-slate-100 pb-2">
|
||||
<span className="text-[15px] font-bold text-primary-900 break-keep">{ax.code} · {ax.name}</span>
|
||||
<span className="text-[15px] font-bold" style={{ color: scorePctColor(ax.score) }}>{ax.score}</span>
|
||||
</div>
|
||||
{ax.items.map((it) => {
|
||||
const pct = it.pct ?? 0;
|
||||
const color = it.pct === null ? '#CBD5E1' : scorePctColor(pct);
|
||||
return (
|
||||
<div key={cat.category.id} className="flex flex-col gap-[7px]">
|
||||
<div key={it.id} className="flex flex-col gap-[6px]">
|
||||
<div className="flex justify-between items-baseline gap-3">
|
||||
<span className="text-[15px] font-semibold text-primary-900 break-keep">
|
||||
{cat.category.code} · {cat.category.name}
|
||||
</span>
|
||||
<span className="text-[15px] font-bold" style={{ color }}>
|
||||
{cat.pct}%
|
||||
</span>
|
||||
<span className="text-[14px] font-medium text-primary-900 break-keep">{it.name}</span>
|
||||
<span className="text-[14px] font-bold" style={{ color }}>{it.pct === null ? '미검증' : `${it.pct}%`}</span>
|
||||
</div>
|
||||
<div className="h-2 bg-slate-100 rounded-full overflow-hidden">
|
||||
<motion.div
|
||||
className="h-2 rounded-full"
|
||||
style={{ backgroundColor: color }}
|
||||
initial={{ width: 0 }}
|
||||
whileInView={{ width: `${Math.max(cat.pct, 2)}%` }}
|
||||
whileInView={{ width: `${Math.max(pct, 2)}%` }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.8, ease: 'easeOut' }}
|
||||
/>
|
||||
@ -127,6 +137,9 @@ export default function SampleReport({ overall, result }: SampleReportProps) {
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/**
|
||||
* INFINITH AI Discovery 채점 기준표 v2.0 (초안) · AEO / GEO 분리 지표
|
||||
* INFINITH AI Discovery 채점 기준표 v2.0 · AEO / GEO 분리 지표 (2026-09-07 정식 채택. v1.0 36항목은 실측 신호로 유지)
|
||||
*
|
||||
* - AEO 답변 준비도(100): 고객의 질문에 얼마나 명확하고 충분하게 답하는가
|
||||
* - GEO 출처 준비도(100): 그 답을 얼마나 검증하고 신뢰할 수 있는 출처로 사용할 수 있는가
|
||||
@ -22,9 +22,9 @@ const L = (l0: string, l1: string, l2: string, l3: string) => [
|
||||
];
|
||||
|
||||
export const AEO_GEO_RUBRIC_V2: RubricV2 = {
|
||||
version: '2.0-draft',
|
||||
version: '2.0',
|
||||
updatedAt: '2026-09-07',
|
||||
status: 'draft',
|
||||
status: 'active',
|
||||
axes: [
|
||||
{
|
||||
id: 'aeo',
|
||||
|
||||
@ -12,7 +12,7 @@ export const DISCOVERY_TAEHA: DiscoveryResult = {
|
||||
url: 'https://taehadrug.com',
|
||||
industry: '로펌 · 마약 형사 (강남 테헤란로, 6개 분사무소)',
|
||||
auditedAt: '2026-08-28',
|
||||
rubricVersion: '1.0',
|
||||
rubricVersion: '2.0',
|
||||
conditions: [
|
||||
'선정: 네이버 "마약 변호사" 검색 파워링크 1위 광고주. 광고 랜딩은 마약 전용 도메인 taehadrug.com',
|
||||
'실측 대상: taehadrug.com 메인 + /success/success_story/, www.taehalaw.com 메인 + /member/page. JS 미실행 HTML 기준',
|
||||
|
||||
@ -15,7 +15,7 @@ export const DISCOVERY_VIEWCLINIC: DiscoveryResult = {
|
||||
url: 'https://www.viewclinic.com',
|
||||
industry: '성형외과 (강남 신논현)',
|
||||
auditedAt: '2026-09-07',
|
||||
rubricVersion: '1.0',
|
||||
rubricVersion: '2.0',
|
||||
conditions: [
|
||||
'실측 대상: 메인 페이지 + /breast/main/ (가슴성형) 2개 URL, JS 미실행 HTML 기준',
|
||||
'크롤러 접근성: robots.txt 파싱 + 12개 AI 봇 UA 실제 GET 요청',
|
||||
@ -158,7 +158,7 @@ export const DISCOVERY_VIEWCLINIC: DiscoveryResult = {
|
||||
{ item: '답변엔진 UI 자동 조회', reason: 'OpenAI·Perplexity 약관 위반. 주력 측정은 오너가 소유한 신호(로그·GA4)로 한다.' },
|
||||
{ item: '네이버 블로그 증량', reason: 'blog.naver.com robots.txt가 AI 크롤링을 전면 금지. 글로벌 AI 답변 기여도 0. 네이버 AI 브리핑은 별도 트랙.' },
|
||||
],
|
||||
// v2.0(초안) 사람 판정. v1.0에 대응 항목이 없는 4개만. 근거는 2026-09-07 수집기(evidence/viewclinic) 원문. 병원 확인 전 초안이다.
|
||||
// v2.0 사람 판정. v1.0에 대응 항목이 없는 4개만. 근거는 2026-09-07 수집기(evidence/viewclinic) 원문. 병원 확인 전 초안이다.
|
||||
v2Results: [
|
||||
{ criterionId: 'AEO6', level: 3, evidence: '모든 페이지 하단에 전화 02-539-1177·주소·온라인 상담 폼이 텍스트로 반복되고(수집기 반복 줄), /counsel/reservation/ 예약 페이지와 카카오톡 채널 링크가 있다. 전화·예약·방문 3경로가 텍스트로 연결됨', source: 'evidence/viewclinic/index.json (reservation 5, direction 2)' },
|
||||
{ criterionId: 'GEO1', level: 1, evidence: '"모티바 보형물 국내 사용량 1위(2023, 모티바코리아 전체 수술량 기준)"처럼 집계 주체·시점이 붙은 주장이 일부 있으나, 모티바 그랜드 어워드·KOL·렛미인 등 대부분의 권위 신호는 이미지·슬로건 안에 있고 원문 링크가 없다. "마취과 전문의 상주 43% vs 23.1%" 비교는 출처·기준 시점 없음', source: 'https://www.viewclinic.com/breast/breast-augmentation/motiva/ · /system/safe-surgery/ (2026-09-07)' },
|
||||
|
||||
@ -2,14 +2,17 @@
|
||||
* /discovery — AI Discovery(AEO/GEO) 서비스 랜딩.
|
||||
* 시안: docs/reports/landing/ai_discovery_landing.html (2026-08-31 확정)
|
||||
*
|
||||
* 데이터 원칙: 점수·등급·카테고리 %·항목 수는 전부
|
||||
* scoreDiscovery(AEO_GEO_RUBRIC, DISCOVERY_VIEWCLINIC) 계산값에서 파생한다.
|
||||
* 데이터 원칙: 점수·등급·항목 %·항목 수는 전부
|
||||
* scoreDiscovery(v1.0 실측) / scoreDiscoveryV2(v2.0 AEO·GEO) 계산값에서 파생한다.
|
||||
* 재진단으로 데이터가 바뀌면 랜딩 수치도 함께 바뀐다 (JSX 하드코딩 금지).
|
||||
*/
|
||||
import { useMemo, useState } from 'react';
|
||||
import { AEO_GEO_RUBRIC } from '../data/aeoGeoRubric';
|
||||
import { DISCOVERY_VIEWCLINIC } from '../data/discovery_viewclinic';
|
||||
import { scoreDiscovery, nextGradeTarget } from '../lib/discoveryScore';
|
||||
import { AEO_GEO_RUBRIC_V2 } from '../data/aeoGeoRubricV2';
|
||||
import { scoreDiscoveryV2 } from '../lib/discoveryScoreV2';
|
||||
import type { AxisSummary } from '../components/discovery/AbmrLoop';
|
||||
import DiscoveryHero from '../components/discovery/DiscoveryHero';
|
||||
import WhyNow from '../components/discovery/WhyNow';
|
||||
import HowItWorks from '../components/discovery/HowItWorks';
|
||||
@ -23,7 +26,16 @@ export default function DiscoveryLandingPage() {
|
||||
const [leadOpen, setLeadOpen] = useState(false);
|
||||
const result = DISCOVERY_VIEWCLINIC;
|
||||
const overall = useMemo(() => scoreDiscovery(AEO_GEO_RUBRIC, result), [result]);
|
||||
const target = nextGradeTarget(overall.score);
|
||||
const overallV2 = useMemo(() => scoreDiscoveryV2(AEO_GEO_RUBRIC_V2, result), [result]);
|
||||
const axes: AxisSummary[] = [overallV2.aeo, overallV2.geo].map((ax) => ({
|
||||
code: ax.axis.code,
|
||||
name: ax.axis.name,
|
||||
nameEn: ax.axis.nameEn,
|
||||
score: ax.score,
|
||||
grade: ax.grade,
|
||||
target: nextGradeTarget(ax.score),
|
||||
items: ax.items.map((it) => ({ id: it.item.id, name: it.item.name, pct: it.pct === null ? null : Math.round(it.pct * 100) })),
|
||||
}));
|
||||
|
||||
const criteriaCount = AEO_GEO_RUBRIC.criteria.length;
|
||||
const categoryCount = AEO_GEO_RUBRIC.categories.length;
|
||||
@ -33,8 +45,7 @@ export default function DiscoveryLandingPage() {
|
||||
return (
|
||||
<div className="[word-break:keep-all]">
|
||||
<DiscoveryHero
|
||||
overall={overall}
|
||||
target={target}
|
||||
axes={axes}
|
||||
clinicName={result.clinicName}
|
||||
sampleReportPath={`/discovery/${result.id}`}
|
||||
criteriaCount={criteriaCount}
|
||||
@ -42,10 +53,9 @@ export default function DiscoveryLandingPage() {
|
||||
/>
|
||||
<WhyNow />
|
||||
<HowItWorks criteriaCount={criteriaCount} categoryCount={categoryCount} />
|
||||
<SampleReport overall={overall} result={result} />
|
||||
<SampleReport overall={overall} axes={axes} result={result} />
|
||||
<Roadmap
|
||||
grade={overall.grade}
|
||||
targetGrade={target?.grade ?? null}
|
||||
axes={axes}
|
||||
criteriaCount={criteriaCount}
|
||||
unverifiedCount={unverifiedCount}
|
||||
/>
|
||||
|
||||
@ -126,20 +126,27 @@ export default function DiscoveryReportPage() {
|
||||
{result.url}
|
||||
</a>
|
||||
<p className="text-white/60 text-sm mt-4">
|
||||
실측일 {result.auditedAt} · 검증 항목 {overall.categories.reduce((s, c) => s + c.verifiedCount, 0)} /
|
||||
{' '}{AEO_GEO_RUBRIC.criteria.length} · 미검증 항목은 분모 제외
|
||||
실측일 {result.auditedAt} · 실측 항목 {overall.categories.reduce((s, c) => s + c.verifiedCount, 0)} /
|
||||
{' '}{AEO_GEO_RUBRIC.criteria.length} · AEO·GEO 각 100점 독립 지표 · 미검증 항목은 분모 제외
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-2xl shadow-[3px_4px_12px_rgba(0,0,0,0.06)] p-8 flex flex-col items-center gap-3 min-w-[260px]">
|
||||
<ScoreRing score={overall.score} size={160} label="AI Answer Readiness" />
|
||||
<div className="bg-white rounded-2xl shadow-[3px_4px_12px_rgba(0,0,0,0.06)] p-8 flex flex-col items-center gap-4 min-w-[260px]">
|
||||
<div className="flex items-start gap-8">
|
||||
{overallV2 && [overallV2.aeo, overallV2.geo].map((ax) => (
|
||||
<div key={ax.axis.id} className="flex flex-col items-center gap-2">
|
||||
<ScoreRing score={ax.score} size={130} label={ax.axis.nameEn} />
|
||||
<div className="flex items-baseline gap-2">
|
||||
<span className="font-serif text-4xl font-black text-[#0A1128]">{overall.grade}</span>
|
||||
<span className="text-sm text-slate-500">
|
||||
{overall.earned} / {overall.possible}점
|
||||
</span>
|
||||
<span className="font-serif text-3xl font-black text-[#0A1128]">{ax.grade}</span>
|
||||
<span className="text-sm text-slate-500">{ax.axis.code} {ax.axis.name}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-sm text-slate-500">
|
||||
v1.0 종합 {overall.score}/{overall.grade} · 실측 {overall.earned} / {overall.possible}점
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 카테고리 바 */}
|
||||
@ -183,7 +190,7 @@ export default function DiscoveryReportPage() {
|
||||
<SectionWrapper
|
||||
id="breakdown"
|
||||
title="Scorecard by Category"
|
||||
subtitle="36개 항목 · 각 항목의 실측 근거와 근거 등급"
|
||||
subtitle="v1.0 실측 항목 36개 · AEO/GEO 점수의 파생 신호. 각 항목의 실측 근거와 근거 등급"
|
||||
dark
|
||||
>
|
||||
<div className="space-y-6">
|
||||
@ -351,7 +358,7 @@ export default function DiscoveryReportPage() {
|
||||
<SectionWrapper
|
||||
id="rubric"
|
||||
title="Scoring Rubric"
|
||||
subtitle={`AEO/GEO 채점 기준표 v${AEO_GEO_RUBRIC.version} · ${AEO_GEO_RUBRIC.criteria.length}개 항목 · 100점`}
|
||||
subtitle={`실측 기준표 v${AEO_GEO_RUBRIC.version} · ${AEO_GEO_RUBRIC.criteria.length}개 항목. v2.0 AEO/GEO 점수는 이 항목들을 파생 신호로 쓴다`}
|
||||
>
|
||||
<div className="overflow-x-auto rounded-2xl border border-slate-100 shadow-sm bg-white">
|
||||
<table className="w-full text-base min-w-[860px]">
|
||||
|
||||
Loading…
Reference in New Issue
Block a user