diff --git a/docs/AEO_GEO_RUBRIC.md b/docs/AEO_GEO_RUBRIC.md index 3995d33..5037676 100644 --- a/docs/AEO_GEO_RUBRIC.md +++ b/docs/AEO_GEO_RUBRIC.md @@ -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) diff --git a/docs/AEO_GEO_RUBRIC_v2.md b/docs/AEO_GEO_RUBRIC_v2.md index 252aa1d..9e513a1 100644 --- a/docs/AEO_GEO_RUBRIC_v2.md +++ b/docs/AEO_GEO_RUBRIC_v2.md @@ -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`). ## 설계 diff --git a/docs/deck/INFINITH_WordPress_GEO_AEO_장단점.pdf b/docs/deck/INFINITH_WordPress_GEO_AEO_장단점.pdf new file mode 100644 index 0000000..b301bb9 Binary files /dev/null and b/docs/deck/INFINITH_WordPress_GEO_AEO_장단점.pdf differ diff --git a/docs/deck/INFINITH_WordPress_GEO_AEO_장단점.pptx b/docs/deck/INFINITH_WordPress_GEO_AEO_장단점.pptx new file mode 100644 index 0000000..c3ad1b4 Binary files /dev/null and b/docs/deck/INFINITH_WordPress_GEO_AEO_장단점.pptx differ diff --git a/docs/deck/build_wp_geo_deck.py b/docs/deck/build_wp_geo_deck.py new file mode 100644 index 0000000..95e12ed --- /dev/null +++ b/docs/deck/build_wp_geo_deck.py @@ -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) diff --git a/docs/prd/INFINITH_AI-Discovery_PRD_20260828_v02.md b/docs/prd/INFINITH_AI-Discovery_PRD_20260828_v02.md new file mode 100644 index 0000000..54bbd5f --- /dev/null +++ b/docs/prd/INFINITH_AI-Discovery_PRD_20260828_v02.md @@ -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>//g' -e 's/<[^>]*>//g' | tr -s ' \n' | wc -w +# 뷰성형외과 리포트: /discovery/viewclinic (AEO-GEO-Audit 브랜치) +``` diff --git a/docs/prd/INFINITH_AI-Discovery_PRD_20260828_v02.pdf b/docs/prd/INFINITH_AI-Discovery_PRD_20260828_v02.pdf new file mode 100644 index 0000000..5fff8af Binary files /dev/null and b/docs/prd/INFINITH_AI-Discovery_PRD_20260828_v02.pdf differ diff --git a/docs/prd/INFINITH_AI-Discovery_PRD_20260828_v02.pptx b/docs/prd/INFINITH_AI-Discovery_PRD_20260828_v02.pptx new file mode 100644 index 0000000..828892a Binary files /dev/null and b/docs/prd/INFINITH_AI-Discovery_PRD_20260828_v02.pptx differ diff --git a/docs/prd/build_geo_aeo_prd.py b/docs/prd/build_geo_aeo_prd.py new file mode 100644 index 0000000..0406c17 --- /dev/null +++ b/docs/prd/build_geo_aeo_prd.py @@ -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)) diff --git a/docs/reports/viewclinic/01_readiness_report/Viewclinic_AI_Answer_Readiness_Report_v2.0.pdf b/docs/reports/viewclinic/01_readiness_report/Viewclinic_AI_Answer_Readiness_Report_v2.0.pdf new file mode 100644 index 0000000..b350fa8 Binary files /dev/null and b/docs/reports/viewclinic/01_readiness_report/Viewclinic_AI_Answer_Readiness_Report_v2.0.pdf differ diff --git a/docs/reports/viewclinic/01_readiness_report/Viewclinic_AI_Answer_Readiness_Report_v2.0.pptx b/docs/reports/viewclinic/01_readiness_report/Viewclinic_AI_Answer_Readiness_Report_v2.0.pptx new file mode 100644 index 0000000..dc65965 Binary files /dev/null and b/docs/reports/viewclinic/01_readiness_report/Viewclinic_AI_Answer_Readiness_Report_v2.0.pptx differ diff --git a/docs/reports/viewclinic/01_readiness_report/Viewclinic_AI_Answer_Readiness_Report_v2.0_표준캔버스.pdf b/docs/reports/viewclinic/01_readiness_report/Viewclinic_AI_Answer_Readiness_Report_v2.0_표준캔버스.pdf new file mode 100644 index 0000000..f41aee9 Binary files /dev/null and b/docs/reports/viewclinic/01_readiness_report/Viewclinic_AI_Answer_Readiness_Report_v2.0_표준캔버스.pdf differ diff --git a/docs/reports/viewclinic/01_readiness_report/Viewclinic_AI_Answer_Readiness_Report_v2.0_표준캔버스.pptx b/docs/reports/viewclinic/01_readiness_report/Viewclinic_AI_Answer_Readiness_Report_v2.0_표준캔버스.pptx new file mode 100644 index 0000000..01f72dc Binary files /dev/null and b/docs/reports/viewclinic/01_readiness_report/Viewclinic_AI_Answer_Readiness_Report_v2.0_표준캔버스.pptx differ diff --git a/docs/reports/viewclinic/01_readiness_report/Viewclinic_AI_Discovery_Report_one page_2026-09-07.pdf b/docs/reports/viewclinic/01_readiness_report/Viewclinic_AI_Discovery_Report_one page_2026-09-07.pdf new file mode 100644 index 0000000..d2e9782 Binary files /dev/null and b/docs/reports/viewclinic/01_readiness_report/Viewclinic_AI_Discovery_Report_one page_2026-09-07.pdf differ diff --git a/docs/reports/viewclinic/01_readiness_report/Viewclinic_AI_Discovery_Report_v2.0_2026-09-07.pdf b/docs/reports/viewclinic/01_readiness_report/Viewclinic_AI_Discovery_Report_v2.0_2026-09-07.pdf new file mode 100644 index 0000000..c62627e Binary files /dev/null and b/docs/reports/viewclinic/01_readiness_report/Viewclinic_AI_Discovery_Report_v2.0_2026-09-07.pdf differ diff --git a/docs/reports/viewclinic/01_readiness_report/build_viewclinic_deck_v2_0.py b/docs/reports/viewclinic/01_readiness_report/build_viewclinic_deck_v2_0.py new file mode 100644 index 0000000..e8e43f5 --- /dev/null +++ b/docs/reports/viewclinic/01_readiness_report/build_viewclinic_deck_v2_0.py @@ -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) diff --git a/docs/reports/viewclinic/04_supporters/Viewclinic_Supporters_vs_Official_AEO_GEO_v2.xlsx b/docs/reports/viewclinic/04_supporters/Viewclinic_Supporters_vs_Official_AEO_GEO_v2.xlsx new file mode 100644 index 0000000..f2b5f2a Binary files /dev/null and b/docs/reports/viewclinic/04_supporters/Viewclinic_Supporters_vs_Official_AEO_GEO_v2.xlsx differ diff --git a/docs/reports/viewclinic/04_supporters/v1_levels/supporters_indexable.json b/docs/reports/viewclinic/04_supporters/v1_levels/supporters_indexable.json new file mode 100644 index 0000000..5d80210 --- /dev/null +++ b/docs/reports/viewclinic/04_supporters/v1_levels/supporters_indexable.json @@ -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 미측정" + } + ] +} \ No newline at end of file diff --git a/docs/reports/viewclinic/04_supporters/v1_levels/supporters_noindex.json b/docs/reports/viewclinic/04_supporters/v1_levels/supporters_noindex.json new file mode 100644 index 0000000..b51456c --- /dev/null +++ b/docs/reports/viewclinic/04_supporters/v1_levels/supporters_noindex.json @@ -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 미측정" + } + ] +} \ No newline at end of file diff --git a/docs/reports/viewclinic/04_supporters/v2_compare.json b/docs/reports/viewclinic/04_supporters/v2_compare.json new file mode 100644 index 0000000..9677247 --- /dev/null +++ b/docs/reports/viewclinic/04_supporters/v2_compare.json @@ -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 없음. 비교 정보는 이미지로 추정" + }, + { + "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 연결" + } + ] + } + ] +} \ No newline at end of file diff --git a/scripts/build_supporters_audit_v2_xlsx.py b/scripts/build_supporters_audit_v2_xlsx.py new file mode 100644 index 0000000..775c267 --- /dev/null +++ b/scripts/build_supporters_audit_v2_xlsx.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +"""공식 홈페이지 vs 서포터즈 사이트 · v2.0 AEO/GEO 비교 엑셀. +사용: python3 scripts/build_supporters_audit_v2_xlsx.py +입력 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) diff --git a/scripts/build_supporters_v2_compare.ts b/scripts/build_supporters_v2_compare.ts new file mode 100644 index 0000000..f531818 --- /dev/null +++ b/scripts/build_supporters_v2_compare.ts @@ -0,0 +1,67 @@ +/** + * 공식 홈페이지 vs 서포터즈 사이트 · v2.0 AEO/GEO 비교 데이터. + * npx tsx scripts/build_supporters_v2_compare.ts + * 입력 + * - 공식: 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); diff --git a/src/components/discovery/AbmrLoop.tsx b/src/components/discovery/AbmrLoop.tsx index 9805182..48e7602 100644 --- a/src/components/discovery/AbmrLoop.tsx +++ b/src/components/discovery/AbmrLoop.tsx @@ -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} 유지 검증`)), '언급률 상승 추적', ]; diff --git a/src/components/discovery/DiscoveryHero.tsx b/src/components/discovery/DiscoveryHero.tsx index d2cefd4..e16fe95 100644 --- a/src/components/discovery/DiscoveryHero.tsx +++ b/src/components/discovery/DiscoveryHero.tsx @@ -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에게 병원을 묻습니다. -
답변에 인용될 근거를 {criteriaCount}개 항목으로 실측 - 진단하고, 8주 안에 구축합니다. +
답변에 인용될 근거를 {criteriaCount}개 항목으로 실측해 + 답변 준비도와 출처 준비도로 진단하고, 8주 안에 구축합니다. {/* CTAs */} @@ -101,7 +99,7 @@ export default function DiscoveryHero({ {/* ABMR loop + AI answer mock */}
- +
diff --git a/src/components/discovery/HowItWorks.tsx b/src/components/discovery/HowItWorks.tsx index 9076757..7c011a3 100644 --- a/src/components/discovery/HowItWorks.tsx +++ b/src/components/discovery/HowItWorks.tsx @@ -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: (
8-WEEK ROADMAP

- {targetGrade - ? `8주 안에, ${grade}에서 ${targetGrade}로 올립니다.` - : `8주마다, ${grade} 등급을 재검증합니다.`} + {title}

diff --git a/src/components/discovery/SampleReport.tsx b/src/components/discovery/SampleReport.tsx index 631d531..1a2e797 100644 --- a/src/components/discovery/SampleReport.tsx +++ b/src/components/discovery/SampleReport.tsx @@ -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 = { A: 'bg-[#EFF0FF] border-[#C5CBF5] text-[#3A3F7C]', @@ -15,7 +16,7 @@ const GRADE_BADGE_STYLE: Record = { 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) { 읽을 수는 있지만, 인용할 근거가 없습니다.

- 실제 1차 진단 결과입니다. 어디가 강하고 어디가 비어 있는지, 점수는 카테고리 단위로 - 드러납니다. + 실제 1차 진단 결과입니다. 답변 준비도(AEO)와 출처 준비도(GEO)를 각각 100점으로 보고, + 어디가 강하고 어디가 비어 있는지 항목 단위로 드러냅니다.

@@ -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)]" > - -
-
- 등급 {overall.grade} -
- AI Answer Readiness +
+ {axes.map((ax) => ( +
+ +
+ {ax.code} 등급 {ax.grade} +
+ {ax.name} +
+ ))}

- {totalCriteria}개 항목 중 {verifiedCriteria}개 실측. + {totalCriteria}개 실측 항목 중 {verifiedCriteria}개 확인.
- 미검증 {unverifiedCriteria}개는 분모에서 제외했습니다. + 미검증 {unverifiedCriteria}개는 분모에서 제외했습니다. v1.0 종합 {overall.score}점.

@@ -102,31 +108,38 @@ 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); - return ( -
-
- - {cat.category.code} · {cat.category.name} - - - {cat.pct}% - -
-
- +
+ {axes.map((ax) => ( +
+
+ {ax.code} · {ax.name} + {ax.score}
+ {ax.items.map((it) => { + const pct = it.pct ?? 0; + const color = it.pct === null ? '#CBD5E1' : scorePctColor(pct); + return ( +
+
+ {it.name} + {it.pct === null ? '미검증' : `${it.pct}%`} +
+
+ +
+
+ ); + })}
- ); - })} + ))} +
diff --git a/src/data/aeoGeoRubricV2.ts b/src/data/aeoGeoRubricV2.ts index 72651f4..675c0dc 100644 --- a/src/data/aeoGeoRubricV2.ts +++ b/src/data/aeoGeoRubricV2.ts @@ -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', diff --git a/src/data/discovery_taeha.ts b/src/data/discovery_taeha.ts index d2971ff..212792b 100644 --- a/src/data/discovery_taeha.ts +++ b/src/data/discovery_taeha.ts @@ -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 기준', diff --git a/src/data/discovery_viewclinic.ts b/src/data/discovery_viewclinic.ts index 064e878..14a6315 100644 --- a/src/data/discovery_viewclinic.ts +++ b/src/data/discovery_viewclinic.ts @@ -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)' }, diff --git a/src/pages/DiscoveryLandingPage.tsx b/src/pages/DiscoveryLandingPage.tsx index e8329d5..736a1c4 100644 --- a/src/pages/DiscoveryLandingPage.tsx +++ b/src/pages/DiscoveryLandingPage.tsx @@ -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 (
- + diff --git a/src/pages/DiscoveryReportPage.tsx b/src/pages/DiscoveryReportPage.tsx index f87c87c..00ea23b 100644 --- a/src/pages/DiscoveryReportPage.tsx +++ b/src/pages/DiscoveryReportPage.tsx @@ -126,19 +126,26 @@ export default function DiscoveryReportPage() { {result.url}

- 실측일 {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점 독립 지표 · 미검증 항목은 분모 제외

-
- -
- {overall.grade} - - {overall.earned} / {overall.possible}점 - +
+
+ {overallV2 && [overallV2.aeo, overallV2.geo].map((ax) => ( +
+ +
+ {ax.grade} + {ax.axis.code} {ax.axis.name} +
+
+ ))}
+

+ v1.0 종합 {overall.score}/{overall.grade} · 실측 {overall.earned} / {overall.possible}점 +

@@ -183,7 +190,7 @@ export default function DiscoveryReportPage() {
@@ -351,7 +358,7 @@ export default function DiscoveryReportPage() {