""" Viewclinic AI Answer Readiness Report 덱 빌드 (INFINITH Deck 규격) python3 docs/reports/build_viewclinic_deck.py docs/reports/Viewclinic_AI_Answer_Readiness_Report.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.pptx" d = Deck() # 01 Cover d.cover( eyebrow="AI DISCOVERY · AEO / GEO AUDIT v1.0", title_lines=["AI Answer", "Readiness Report."], subtitle="뷰성형외과 · viewclinic.com · 36개 항목 실측 2026. 08. 28 · 재채점·실측 검증 09. 01", meta_right="AI Discovery Report · 2026", ) # 02 Executive summary s = d.slide(eyebrow="Executive Summary", title="읽을 수는 있지만, ", title_accent="인용할 근거가 없다", lede=["36개 항목 중 32개를 실측했고, 오너 계정이 필요한 4개는 점수 분모에서 제외했습니다.", "접근성(A) 98%와 표면(D) 68%는 양호. 엔티티(B) 25%와 콘텐츠(C) 28%가 총점을 끌어내립니다."], section="Summary") k1, k2, k3, k4 = s.cards4(y=s.band_y, h=3.4) k1.kpi("50 / 100", "AI ANSWER READINESS", "등급 C · 44.1 / 89점") k2.kpi("98%", "A · 크롤러 접근성", "AI 검색봇 12종 전부 허용") k3.kpi("28%", "C · 인용 가능 콘텐츠", "정의문 0 · FAQ 0 · HTML 표 0") 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가 OAI-SearchBot·PerplexityBot 등 12종을 허용. Cloudflare 차단 없음."), ("서버 렌더링 본문", "WordPress가 본문을 서버에서 렌더. JS 없이 10,757자 추출."), ("강남언니·YouTube 활발", "강남언니 후기 19,316건, 의료진 26명. YouTube 채널이 최신 영상을 꾸준히 발행."), ]) R.chip("치명", "critical").rows([ ("누구인지 확정 불가", "MedicalClinic 스키마 없음, sameAs 비어 있음. 시술 목록 JSON-LD는 문법 오류로 무효."), ("인용할 문장 없음", "H1 5개가 전부 영문 라벨. FAQ 0개, HTML 표 0개, 정의문 0개."), ("표면 간 NAP 불일치", "GBP에 전화번호 없음. 웹사이트가 viewplasticsurgery.com. 주소 표기 3종."), ]) # 04 Scorecard s = d.slide(eyebrow="Scorecard · 6 Categories", title="카테고리별 점수와 ", title_accent="검증 범위", lede="점수는 검증된 항목만으로 정규화했습니다. 미검증 항목을 확인하면 총점이 바뀔 수 있습니다.", section="Scorecard") W, Rail = s.cards2(y=s.band_y, split=0.66) W.grid( headers=["카테고리", "배점", "획득", "검증", "판정"], col_w=[4.6, 1.3, 2.0, 1.4, 1.6], rows=[ ["A · AI 크롤러 접근성", "20", "16.7 / 17", "6 / 7", "우수"], ["B · 엔티티 식별·구조화", "20", "5.0 / 20", "7 / 7", "미흡"], ["C · 답변 인용 가능 콘텐츠", "25", "7.0 / 25", "8 / 8", "치명"], ["D · AI 인용 표면 확보", "20", "13.7 / 20", "6 / 6", "양호"], ["E · AI 가시성 측정 인프라", "10", "0 / 2", "1 / 4", "미설치"], ["F · 기술 위생", "5", "1.7 / 5", "4 / 4", "미흡"], ], tones=[[None, None, None, None, "info"], [None, None, None, None, "warning"], [None, None, None, None, "critical"], [None, None, None, None, "good"], [None, None, None, None, "critical"], [None, None, None, None, "warning"]], row_h=0.62, ) Rail.title("읽는 법").bullets([ "획득 분모는 검증 항목의 배점 합. E는 미검증 3개라 분모가 2점뿐.", "총점 44.1 / 89 = 50점, 등급 C. 2026-08-31 자동 채점기 재채점 반영.", "미검증 4개는 GA4·서버로그·GSC·Bing 계정 접근으로 1일 안에 채운다.", ]) # 05 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 봇 허용", "robots.txt Disallow는 /wp-admin/ 뿐. 12종 UA로 실제 GET, 전부 200."), ("A2 CDN 차단 없음", "nginx 직접 응답. Cloudflare 헤더와 AI UA 챌린지 없음."), ("A4 HTTPS 정합", "http://viewclinic.com → 301 → https://www.viewclinic.com/ 정본 수렴."), ]) R.chip("보완·미검증", "warning").rows([ ("A5 sitemap 재채점 우수", "page·portfolio sitemap lastmod 2026-08-31로 최신. 블로그 post만 2024-05-31 정지."), ("A7 응답 속도·무게", "TTFB 0.135s는 양호. 초기 HTML 838KB로 기준 300KB의 2.8배."), ("A6 Bing 색인 (미검증)", "ChatGPT 검색은 Bing 인덱스에 의존. Webmaster 등록 여부는 계정 확인 필요."), ]) # 06 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 블록 쉼표 누락(line 11). 시술 목록 구조화 데이터 전체가 파서에서 버려짐."), ("B2 MedicalClinic 없음", "Organization·WebSite만 존재. 주소·전화·진료시간·전문분야가 구조화되지 않음."), ("B3 Physician 없음", "의료진 정보는 텍스트와 이미지로만 존재. 누가 수술하는지 기계가 읽지 못함."), ]) R.chip("미흡", "warning").rows([ ("B5 sameAs 비어 있음", "푸터에 YouTube·Instagram·Facebook 링크가 있으나 스키마에 연결되지 않음."), ("B4 NAP 표기 3종 혼재", "주소 표기가 메인 2종에 시술 페이지 1종까지 3종. 전화도 2회선과 휴대폰 혼재."), ("B7 영문 사이트 미연결", "viewplasticsurgery.com이 별도로 있으나 hreflang 없음. 두 도메인이 서로 모름."), ]) # 07 C 콘텐츠 s = d.slide(eyebrow="C · Answer-Ready Content · 28%", title="답변엔진이 잘라 갈 ", title_accent="단락이 없다", section="C · Content") L, R = s.cards2(y=s.band_y) L.chip("0점 항목", "critical").rows([ ("C1 정의문 없음", "메인 첫 화면은 슬라이더와 \"VIEW SELFIE\" 라벨. 시술 페이지 첫 텍스트는 상담 폼."), ("C2 FAQ 없음", "메인·가슴성형 페이지에서 FAQ·Q&A 섹션이 탐지되지 않음."), ("C3 H1 5개, 전부 라벨", "VIEW SELFIE / EVENT / SHORTS / FILM / SPECIALITY. 시술 페이지는 H1 없음."), ]) R.chip("부분 점수", "warning").rows([ ("C7 의료진 권위 신호 있음", "모티바 KOL·복지부 표창·갈더마 방문 등 17회 언급. 단 구조화되지 않음."), ("C4 수치 표기 미흡", "\"보형물 사용량 1위\" 같은 주장형. 가격대·수술시간·회복기간 표 없음."), ("C6 신선도는 살아 있음", "시술 페이지 dateModified 2026-04-17, sitemap 2026-08-31. 블로그만 2024-05 정지."), ]) # 08 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,316건, 의료진 26명, 이벤트 52건. 병의원 AI 인용의 핵심 표면 확보."), ("D2 GBP 4.9점 (901)", "소유자 게시물 활성. 단 전화번호 미등록, 웹사이트가 viewplasticsurgery.com."), ("D4 NAP 표기 3종", "홈페이지·강남언니·GBP 주소 표기가 전부 다름. 한 병원으로 묶을 근거 약함."), ]) R.chip("E · 측정 0%", "critical").rows([ ("E1 크롤러 로그", "nginx 직접 호스팅. AI 봇 히트 추출과 IP 검증 파이프라인 없음."), ("E2 GA4 AI 유입 채널", "chatgpt.com·perplexity.ai 커스텀 채널그룹 없음. 계정 접근 필요."), ("E4 기준선 스팟체크", "미실시. 질의 10개 × 5회 반복 측정이 개선 증명의 전제."), ]) # 09 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주", "critical").title("엔티티·표면·측정").bullets([ "깨진 JSON-LD 수정, MedicalClinic 스키마 삽입, sameAs 연결.", "GBP 전화번호 등록, 웹사이트를 viewclinic.com으로 교정.", "4개 표면 NAP를 한 글자까지 통일. 두 도메인 hreflang 연결.", "GA4 AI 채널그룹, 서버로그 IP 검증, GSC·Bing 등록.", ]) b.chip("P1 · 4주 내", "warning").title("콘텐츠 구조").bullets([ "핵심 페이지 첫 단락에 정의문. 시술별 FAQ 5~8개와 FAQPage 스키마.", "페이지당 H1 1개 서술형. 비교표를 HTML table로. Physician 스키마.", "WordPress 5.5.3 업그레이드, alt 누락 111개 보완, HTML 무게 절감.", ]) c.chip("P2 · 지속", "good").title("신선도·시딩").bullets([ "dateModified 마크업 활성화, 월 1회 사이트 갱신.", "유튜브 영상을 사이트 글로 전사해 신선도를 전달.", "티스토리·언론 기고로 naver.com 외 언급 도메인 확보.", "분기 1회 반복 측정. 언급률과 신뢰구간으로만 보고.", ]) # 10 Roadmap (stats style, dark) s = d.slide(eyebrow="Roadmap · 8 Weeks", title="8주 안에 ", title_accent="C에서 B로", dark=True, section="Roadmap") s.stats3([ ("W1–2", "스키마·NAP·측정 인프라 설치.\n기준선 스팟체크 1회."), ("W3–6", "정의문·FAQ·H1 재설계·HTML 표.\nWordPress 업그레이드."), ("W7–8", "재진단. 미검증 4개 반영해\n총점 재산정."), ], y=s.band_y + 0.6) # 10b Validation · Question Bank 실측 (2026-09-01) s = d.slide(eyebrow="Validation · 120-Question Live Test", title="진단을 120문항 실측으로 ", title_accent="검증했다", lede="잠재 환자 질문 120개를 OpenAI·Perplexity 정식 API로 질의해 진단의 예측력을 채점했습니다 (2026-09-01).", section="Validation") L, R = s.cards2(y=s.band_y) L.chip("예측 적중", "good").rows([ ("논브랜드 질문에서 부재", "추천·시술 정보 84문항 중 Perplexity 3문항, ChatGPT 10문항만 언급. 시술 정보 52문항은 두 엔진 모두 0건."), ("원인은 콘텐츠(C) 28%", "FAQ 0개, HTML 표 0개, 정의문 0개가 논브랜드 전멸로 그대로 이어짐."), ("표면(D)이 브랜드 방어", "병원명을 아는 질문 36문항은 97~100% 답변. 근거는 강남언니·GBP·뉴스."), ]) R.chip("진단 밖 발견 · v1.1 반영", "info").rows([ ("제3자 표면이 더 넓다", "뉴스 기사·채용공고·바비톡·네이버 예약이 개원연도·주차·휴진을 정확히 답함. D3 실측 범위 확대."), ("멈춘 블로그가 인용됨", "ChatGPT 추천 답변의 근거가 blog.viewclinic.com. 텍스트 자산은 인용된다는 직접 증거."), ("엔진별 오답 편차", "주차 질문에 Perplexity는 35대 무료로 정답, ChatGPT는 시설 없음으로 추측. 구조화 부재의 비용."), ]) # 11 What we don't sell 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("만들지 않는 것").bullets([ "llms.txt. Google 공식 효과 0, 채택률 0.13%, 서버 로그에 요청 자체가 없음.", "네이버 블로그 증량. robots.txt가 AI 크롤링을 전면 금지, 글로벌 AI 기여도 0.", ]) b.icon_square().title("약속하지 않는 것").bullets([ "\"스키마 넣으면 인용됩니다\". 인과 증거 없음, 위생 조치로만 설명.", "순위·노출 보장. LLM 답변은 동일 질문에도 10~34% 흔들림.", ]) c.icon_square().title("측정 규율").bullets([ "단발 조회로 성과 보고 금지. 반복 측정과 신뢰구간만.", "답변엔진 UI 자동 조회 금지. OpenAI·Perplexity 약관 위반.", "AI 유입은 항상 하한값. 35~70%가 Direct로 유실.", ]) # 12 Method s = d.slide(eyebrow="Method · AEO / GEO Rubric v1.0", title="36개 항목, 100점, ", title_accent="근거 등급을 명시", section="Method") a, b, c = s.cards3(y=s.band_y) a.icon_square().title("채점 방식").bullets([ "항목별 0~3 레벨 × 배점. 미검증은 0점이 아니라 분모 제외.", "A 20 · B 20 · C 25 · D 20 · E 10 · F 5.", "등급 A 81+ · B 61~80 · C 41~60 · D 40 이하.", ]) b.icon_square().title("배점 근거").bullets([ "차단되면 절대 인용될 수 없다는 사실은 논쟁이 없다. A·B에 무게.", "GEO 콘텐츠 전술의 효과는 학술적으로 논쟁 중. C는 좋은 콘텐츠 원칙으로만.", "병의원은 강남언니가 인용을 장악. D1 단일 항목 최고 배점 6.", ]) c.icon_square().title("항목마다 붙는 태그").bullets([ "근거 등급: 실측 / 학술 / 벤더 / 추정.", "통제 가능성: 직접 / 영향 / 불가.", "채점 방식: 자동 / 반자동 / 수동. 자동 항목은 Edge Function으로 이식.", ]) # 13 Closing d.closing(eyebrow="Next Step", title_lines=["표면은 확보됐습니다.", "홈페이지가 따라가면 됩니다."], lede="P0 세 가지: 스키마, NAP 통일, 측정 인프라. 미검증 4개는 계정 접근 후 1일 안에 채웁니다.", cta_primary="P0 착수하기 →", cta_secondary="리포트 페이지 보기", contact="o2oteam@o2o.kr") d.save(OUT) print("saved", OUT)