o2o-infinith-demo/docs/reports/build_viewclinic_poc_deck.py
Haewon Kam 1dd1384fbf feat: POC 덱 v3 — haewon 10페이지 폰트 기준을 전 슬라이드 적용 (본문 20 / 리드·인용 24 / 카드 제목 28)
haewon 수정 원본(Viewclinic_POC_Proposal.pptx)은 보존, 10페이지는 원본 유지.
나머지 14장에 런 164개 in-place 매핑 후 v3 + v3_표준캔버스(13.33in)로 저장.
빌드 스크립트에 재생성 금지 경고와 새 폰트 상수 역반영.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155BJMHJyhqqAPGYjGKYhPS
2026-09-01 11:52:43 +09:00

321 lines
20 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Viewclinic AI Discovery POC Proposal 덱 빌드 (INFINITH Deck 규격, 15페이지)
경고: 정본은 haewon이 직접 수정한 Viewclinic_POC_Proposal.pptx와 그 파생 v3 계열이다.
이 스크립트를 다시 실행해 해당 파일들을 덮어쓰지 않는다 (feedback-never-overwrite-haewon-edits).
폰트 기준은 2026-09-01 haewon 확정: 본문·그리드 20 / 리드·인용 24 / 카드 제목 28.
리드문(ifd 내부 20pt)과 카드 제목(24pt)은 ifd 기본값이라, 재생성 시 후처리로 24/28 승격이 필요하다.
제안 4장 + 진단 리포트 요약 5장 + 부록 5장(두 점수 읽는 법 + User Question Bank 120문항 실측) + 클로징 1장.
부록 실측 데이터: docs/reports/Viewclinic_QB_Results_Combined.md (2026-09-01, OpenAI·Perplexity API)
본문 텍스트는 기본값 대비 +2pt (rows/bullets 18, grid 16.25, quote 19.25).
python3 docs/reports/build_viewclinic_poc_deck.py docs/reports/Viewclinic_POC_Proposal.pptx
"""
import os
import sys
sys.path.insert(0, "/Users/haewonkam/.claude/skills/infinith-deck/scripts")
import ifd
from ifd import Deck
BODY = 20.0 # rows 본문·불릿 (haewon 확정 2026-09-01)
GRID = 20.0 # 스코어카드 격자 셀 (haewon 확정 2026-09-01)
QUOTE = 24.0 # 인용 본문 = 리드문과 동일 (haewon 확정 2026-09-01)
def quote_up(s, text, who, x=1.25, y=6.63, w=17.5, h=1.83):
"""ifd.Slide.quote 와 동일 레이아웃, 본문만 +2pt."""
ifd._rect(s._raw, x, y, w, h, fill=ifd.LIGHT_BG if not s.dark else ifd.DARK_CARD, line=None, radius=0)
ifd._rect(s._raw, x, y, 0.04, h, fill=ifd.VIOLET, line=None, radius=0)
ifd._check(text, w - 0.5, QUOTE, 2, "quote")
ifd._write(ifd._tb(s._raw, x + 0.28, y + 0.25, w - 0.5, h - 0.9), text, QUOTE, ifd.SANS,
color=ifd.WHITE if s.dark else ifd.INK, line_spacing=1.2)
ifd._write(ifd._tb(s._raw, x + 0.28, y + h - 0.6, w - 0.5, 0.44), who, 18.0, ifd.SANS,
color=ifd.MUTED_DK if s.dark else ifd.MUTED)
OUT = sys.argv[1] if len(sys.argv) > 1 else "Viewclinic_POC_Proposal.pptx"
d = Deck()
# 01 Cover
d.cover(
eyebrow="PROOF OF CONCEPT · POC PROPOSAL",
title_lines=["AI Discovery,", "AEO / GEO."],
subtitle="뷰성형외과 × INFINITH · AI 답변엔진 노출 구축 검증 제안 · 2026. 09. 01",
meta_right="AI Discovery Proposal · 2026",
)
# 02 Why now (dark)
s = d.slide(eyebrow="Why Now · AEO / GEO",
title="고객은 이미 AI에게 ", title_accent="병원을 묻습니다",
lede=["검색은 링크 목록에서 하나의 답변으로 바뀌었습니다.",
"답변에 인용되지 못한 병원은 비교 대상에도 오르지 못합니다."],
dark=True, section="Why Now")
L, R = s.cards2(y=s.band_y)
L.chip("시장의 변화", "info").rows([
("검색이 답변으로 바뀌었다", "ChatGPT·Perplexity·네이버 AI 브리핑이 병원을 직접 추천. 클릭 없이 답변만 소비되는 비중이 커지는 중."),
("AI는 인용으로만 노출한다", "답변엔진은 크롤링 가능한 오픈웹 근거를 골라 인용. 광고 입찰이 없어 근거를 먼저 갖춘 병원이 선점."),
("네이버 자산은 기여 0", "네이버 블로그·플레이스는 robots.txt로 AI 크롤러를 전면 차단(2026-08 실측). 기존 네이버 마케팅이 AI 답변에 닿지 않음."),
], b_size=BODY)
R.chip("뷰성형외과 실측 · 50 / 100 · C", "critical").rows([
("읽을 수는 있다", "robots.txt가 AI 검색봇 12종을 전부 허용. 크롤러 접근성 98%로 기술적 전제는 이미 확보."),
("인용할 근거가 없다", "MedicalClinic 스키마 없음, FAQ 0개, 정의문 0개. 답변 인용 가능 콘텐츠 점수 28%."),
("성과를 잴 수도 없다", "AI 유입 측정 인프라 0%. AI가 보내는 환자를 세는 장치 자체가 아직 없음."),
], b_size=BODY)
# 03 Offering — 랜딩 히어로 스크린샷 + 3스텝
from pptx.util import Inches
SHOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "shots")
s = d.slide(eyebrow="Our Offering · AI Discovery",
title="진단하고, 구축하고, ", title_accent="숫자로 증명합니다",
lede="AI Discovery는 36개 항목 실측 진단, 우선순위 구축, 반복 측정 보고까지 한 파이프라인으로 제공합니다.",
section="Offering")
img_h = 9.9 - s.band_y
img_w = img_h * (1440 / 1402)
ifd._rect(s._raw, 1.25 - 0.05, s.band_y - 0.05, img_w + 0.10, img_h + 0.10,
fill="FFFFFF", line=ifd.CARD_LINE, radius=0.06)
s._raw.shapes.add_picture(f"{SHOT}/land_hero.png", Inches(1.25), Inches(s.band_y), height=Inches(img_h))
rx = 1.25 + img_w + 0.40
R = ifd.Card(s, rx, s.band_y, 18.75 - rx, img_h, s.dark)
R.rows([
("STEP 1 · Audit", "AEO/GEO 기준표 v1.0으로 36개 항목 실측. 뷰성형외과 1차 진단 완료, 50점 등급 C."),
("STEP 2 · Build", "스키마 삽입, NAP 통일, 정의문·FAQ 구축. AI가 인용할 근거를 홈페이지에 만듭니다."),
("STEP 3 · Prove", "GA4·서버로그·반복 스팟체크 설치. 언급률과 신뢰구간으로만 보고합니다."),
], b_size=BODY)
# 04 What you get (KPI + quote)
s = d.slide(eyebrow="What You Get · 8 Weeks",
title="8주 후, 이렇게 ", title_accent="달라집니다",
lede="목표는 두 가지입니다. 인용될 기술적 전제를 완성하고, AI가 보내는 환자를 처음으로 계측합니다.",
section="Outcome")
k1, k2, k3, k4 = s.cards4(y=s.band_y, h=2.55)
k1.kpi("50 → 61+", "AI ANSWER READINESS", "등급 C에서 B로")
k2.kpi("100%", "AI 유입 측정 인프라", "현재 0%. GA4·서버로그 설치")
k3.kpi("36 / 36", "검증 항목 커버리지", "미검증 4개를 계정 연동으로 해소")
k4.kpi("19,316", "강남언니 후기", "기존 자산을 AI 인용 표면으로 연결")
quote_up(s, "스키마를 넣으면 인용된다고 약속하지 않습니다. 인용될 수 없는 이유를 전부 제거하고, 결과를 반복 측정으로 증명합니다.",
"INFINITH 정직성 규약")
# ── 진단 리포트 요약 5장 ──────────────────────────────────────────────────────
# 05 Report 1 · Executive summary
s = d.slide(eyebrow="Report 1 / 5 · Executive Summary",
title="읽을 수는 있지만, ", title_accent="인용할 근거가 없습니다",
lede=["36개 항목 중 32개를 실측했고, 오너 계정이 필요한 4개는 점수 분모에서 제외했습니다.",
"접근성(A) 98%와 표면(D) 68%는 양호. 엔티티(B) 25%와 콘텐츠(C) 28%가 총점을 끌어내립니다."],
section="Report · Summary")
img_h = 9.9 - s.band_y
img_w = img_h * (1440 / 950)
ix = 1.25 + (17.5 - img_w) / 2
ifd._rect(s._raw, ix - 0.05, s.band_y - 0.05, img_w + 0.10, img_h + 0.10,
fill="FFFFFF", line=ifd.CARD_LINE, radius=0.06)
s._raw.shapes.add_picture(f"{SHOT}/land_report.png", Inches(ix), Inches(s.band_y), height=Inches(img_h))
# 06 Report 2 · What AI sees (dark)
s = d.slide(eyebrow="Report 2 / 5 · What AI Sees",
title="문제는 차단이 아니라 ", title_accent="내용입니다", dark=True, section="Report · 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 채널이 영상을 꾸준히 발행."),
], b_size=BODY)
R.chip("치명", "critical").rows([
("누구인지 확정 불가", "MedicalClinic 스키마 없음, sameAs 비어 있음. 시술 목록 JSON-LD는 문법 오류로 무효."),
("인용할 문장 없음", "H1 5개가 전부 영문 라벨. FAQ 0개, HTML 표 0개, 정의문 0개."),
("표면 간 NAP 불일치", "GBP에 전화번호 없음. 웹사이트가 viewplasticsurgery.com. 주소 표기 3종."),
], b_size=BODY)
# 07 Report 3 · Scorecard
s = d.slide(eyebrow="Report 3 / 5 · Scorecard · 6 Categories",
title="카테고리별 점수와 ", title_accent="검증 범위를 정리했습니다",
lede="점수는 검증된 항목만으로 정규화했습니다. 미검증 항목을 확인하면 총점이 바뀔 수 있습니다.",
section="Report · 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, size=GRID,
)
Rail.title("읽는 법").bullets([
"획득 분모는 검증 항목의 배점 합. E는 미검증 3개라 분모가 2점뿐.",
"총점 44.1 / 89 = 50점, 등급 C.",
"미검증 4개는 계정 접근으로 1일 안에 채웁니다.",
], size=BODY)
# 08 Report 4 · Action plan
s = d.slide(eyebrow="Report 4 / 5 · Action Plan",
title="즉시 고칠 것과 ", title_accent="4주 안에 만들 것을 나눴습니다", section="Report · 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 등록.",
], size=BODY)
b.chip("P1 · 4주 내", "warning").title("콘텐츠 구조").bullets([
"핵심 페이지 첫 단락에 정의문. 시술별 FAQ 5~8개.",
"페이지당 H1 1개 서술형. 비교표를 HTML table로.",
"WordPress 업그레이드, alt 누락 111개 보완, HTML 무게 절감.",
], size=BODY)
c.chip("P2 · 지속", "good").title("신선도·시딩").bullets([
"dateModified 마크업 활성화, 월 1회 사이트 갱신.",
"유튜브 영상을 사이트 글로 전사해 신선도를 전달.",
"티스토리·언론 기고로 naver.com 외 언급 도메인 확보.",
"분기 1회 반복 측정. 언급률과 신뢰구간으로만 보고.",
], size=BODY)
# 09 Report 5 · Roadmap (dark)
s = d.slide(eyebrow="Report 5 / 5 · Roadmap · 8 Weeks",
title="8주 안에 ", title_accent="C에서 B로 올립니다", dark=True, section="Report · Roadmap")
s.stats3([
("W12", "스키마·NAP·측정 인프라 설치.\n기준선 스팟체크 1회."),
("W36", "정의문·FAQ·H1 재설계·HTML 표.\nWordPress 업그레이드."),
("W78", "재진단. 미검증 4개 반영해\n총점 재산정."),
], y=s.band_y + 0.6)
# ── 부록 5장 · User Question Bank 120문항 실측 ───────────────────────────────
# 10 Appendix 1 · 두 점수 읽는 법
s = d.slide(eyebrow="Appendix 1 / 5 · How To Read Two Scores",
title="진단 50점과 랭킹 2위는 ", title_accent="같은 사실입니다",
lede=["보고서 점수는 인용될 준비(원인)를, TOP50 랭킹 점수는 실제 등장(결과)을 잽니다.",
"뷰성형외과는 만점 840점 기준 23점, 168번의 답변 기회 중 6번 등장했습니다."],
section="Appendix · Two Scores")
W, Rail = s.cards2(y=s.band_y, split=0.66)
W.grid(
headers=["", "진단 점수 (보고서)", "랭킹 점수 (TOP50)"],
col_w=[2.2, 4.3, 4.4],
rows=[
["재는 것", "인용될 준비가 됐는가 (원인)", "실제 답변에 등장했는가 (결과)"],
["계산", "36개 항목 체크리스트", "84문항 × 2엔진, 등장 순위 가중 합산"],
["만점", "100점", "840점 (전 답변 1위 등장 시)"],
["뷰성형외과", "50점 · 등급 C", "23점 · 2위 (등장률 3.6%, 만점 대비 2.7%)"],
],
row_h=0.62, size=GRID,
)
Rail.title("같이 읽는 법").bullets([
"2위는 빈 판의 상대 순위. 1위 디에이성형외과도 28/840.",
"진단에서 낮았던 B·C는 시술·가격 질문 등장 0회로, 높았던 D 표면은 23점 전부로 실증.",
"현재 순위의 근거는 남의 표면과 멈춘 블로그. 콘텐츠를 갖추면 1위 선점이 가능한 시점.",
], size=BODY)
# 11 Appendix 2 · Question Bank 설계
s = d.slide(eyebrow="Appendix 2 / 5 · User Question Bank",
title="환자의 질문 120개를 ", title_accent="먼저 만들었습니다",
lede=["잠재 환자가 AI에게 실제로 묻는 문장을 퍼널 4단계(인지·탐색·비교·결정)로 설계했습니다.",
"질문마다 진단 실측 기준의 예상 판정을 붙여, 구축 전후 비교의 기준선으로 씁니다."],
section="Appendix · Question Bank")
W, Rail = s.cards2(y=s.band_y, split=0.66)
W.grid(
headers=["카테고리", "문항", "대표 질문"],
col_w=[3.2, 1.0, 6.7],
rows=[
["브랜드 직접", "12", "뷰성형외과 어때? 믿을만한 병원이야?"],
["위치·운영", "10", "뷰성형외과 진료시간 어떻게 돼?"],
["지역 추천·비교", "15", "강남에서 가슴성형 잘하는 병원 어디야?"],
["시술 정보 7분야", "58", "가슴 보형물 종류 뭐가 있어?"],
["신뢰·안전", "9", "성형외과 고를 때 뭘 확인해야 해?"],
["가격·이벤트", "8", "뷰성형외과 지금 하는 이벤트 있어?"],
["후기·평판", "8", "뷰성형외과 실제 후기 어때?"],
],
row_h=0.56, size=GRID,
)
Rail.title("설계 원칙").bullets([
"검색 키워드가 아니라 채팅형 AI에 입력하는 구어체 문장.",
"브랜드 질문 36개, 논브랜드 질문 84개. 승부처는 병원 이름을 모르는 환자의 84개.",
"사전판정: 인용 기대 5, 불안정 35, 인용 불가 80.",
], size=BODY)
# 12 Appendix 3 · 실측 방법과 요약 (dark)
s = d.slide(eyebrow="Appendix 3 / 5 · Measurement",
title="두 답변엔진에 120문항을 ", title_accent="전부 실측했습니다",
lede="OpenAI와 Perplexity 정식 API로 서울 위치를 고정하고, 질문 원문 그대로 1회씩 질의했습니다.",
dark=True, section="Appendix · Measurement")
k1, k2, k3, k4 = s.cards4(y=s.band_y, h=2.55)
k1.kpi("97%", "CHATGPT · 브랜드 질문", "36문항 중 35문항 언급")
k2.kpi("11%", "CHATGPT · 논브랜드", "84문항 중 10문항 언급")
k3.kpi("100%", "PERPLEXITY · 브랜드 질문", "36문항 중 36문항 언급")
k4.kpi("3%", "PERPLEXITY · 논브랜드", "84문항 중 3문항 언급")
quote_up(s, "소비자 UI 자동 조회는 약관 위반이라 쓰지 않았습니다. 정식 API 측정과 반복 스팟체크로만 보고합니다.",
"INFINITH 정직성 규약")
# 13 Appendix 4 · 브랜드 vs 논브랜드
s = d.slide(eyebrow="Appendix 4 / 5 · Findings",
title="이름을 모르는 환자에게는 ", title_accent="존재하지 않습니다",
lede=["병원명을 아는 환자의 질문에는 두 엔진 모두 답합니다.",
"병원을 찾는 질문에서는 경쟁 병원이 추천 자리를 가져갑니다."],
section="Appendix · Findings")
L, R = s.cards2(y=s.band_y)
L.chip("브랜드 질문 · 언급 97~100%", "good").rows([
("이름을 대면 답한다", "뷰성형외과를 지목한 36문항은 위치·평점·안전 정보까지 서술. 인지도 문제는 아님."),
("근거는 남의 표면", "인용 출처는 강남언니·GetCareKorea·뉴스. 자사 홈페이지가 근거로 쓰인 답변은 예외적."),
("오답 위험 실증", "주차 질문에 Perplexity는 35대 무료로 정답, ChatGPT는 시설 없음으로 추측. 구조화 부재의 비용."),
], b_size=BODY)
R.chip("논브랜드 질문 · 언급 3~11%", "critical").rows([
("추천 목록에 없다", "추천·비교·시술 정보 84문항에서 Perplexity 3문항, ChatGPT 10문항만 뷰를 언급."),
("경쟁 병원이 선점", "Perplexity 추천은 디에이 4회, 우리 4회, 바노바기·JK 각 3회. 아이디병원은 1순위 2회."),
("시술 정보 질문 전멸", "가격·회복기간·보형물 비교 질문에서 인용 0. FAQ 0개, HTML 표 0개의 직접 결과."),
], b_size=BODY)
# 14 Appendix 5 · 핵심 증거
s = d.slide(eyebrow="Appendix 5 / 5 · Key Evidence",
title="멈춘 블로그가 아직 ", title_accent="인용되고 있습니다",
lede="ChatGPT가 논브랜드 질문에서 뷰성형외과를 인용한 드문 사례의 출처는 2024년 5월에 멈춘 자사 블로그였습니다.",
section="Appendix · Evidence")
a, b, c = s.cards3(y=s.band_y)
a.chip("발견", "info").title("실측 사례 C-02").bullets([
"강남에서 가슴성형 잘하는 병원을 묻자 뷰성형외과가 2순위로 등장.",
"인용 출처는 blog.viewclinic.com. 2024-05 이후 갱신이 멈춘 블로그.",
], size=BODY)
b.chip("해석", "violet").title("텍스트 자산은 인용된다").bullets([
"유튜브 영상 1,064개보다 오래된 블로그 글이 인용에 기여.",
"답변엔진은 크롤링 가능한 텍스트만 근거로 쓴다는 직접 증거.",
], size=BODY)
c.chip("함의", "good").title("P1 콘텐츠의 근거").bullets([
"정의문·FAQ·비교표가 생기면 논브랜드 언급률이 움직입니다.",
"8주 재측정에서 같은 120문항으로 변화를 확인합니다.",
], size=BODY)
# 15 Closing — 제안서 포맷 (웹 CTA 버튼 없음, 다음 단계 3줄)
raw = d._new(False)
ifd._write(ifd._tb(raw, ifd.MARGIN, 2.12, ifd.CONTENT_W, 0.47), "NEXT STEP · POC PROPOSAL",
ifd.T_EYEBROW, ifd.SANS, bold=True, color=ifd.VIOLET, spc=300)
_title = ["진단은 끝났습니다.", "8주로 증명하겠습니다."]
_th = 96 / 72 * 1.2 * 2
ifd._write(ifd._tb(raw, ifd.MARGIN, 2.97, ifd.CONTENT_W, _th), _title, 96.0, ifd.SANS,
bold=True, color=ifd.INK, line_spacing=0.98)
_y = 2.97 + _th + 0.35
ifd._write(ifd._tb(raw, ifd.MARGIN, _y, ifd.CONTENT_W, 0.62),
"8주 구축과 재진단으로 C를 B로 올리고, 전 과정을 숫자로 보고합니다.",
27.75, ifd.SANS, color=ifd.BODY)
_y += 1.10
for num, label, desc in [
("01", "검토", "본 제안서와 36개 항목 진단 리포트 전문을 검토합니다."),
("02", "계정 연동", "GA4·서버로그·GSC·Bing 권한 공유 시 미검증 4개 항목을 1일 안에 확정합니다."),
("03", "킥오프", "W1 스키마·NAP·측정 인프라 설치부터 착수합니다."),
]:
ifd._write(ifd._tb(raw, ifd.MARGIN, _y, 0.75, 0.45), num, 19.5, ifd.SANS, bold=True, color=ifd.VIOLET)
ifd._write(ifd._tb(raw, ifd.MARGIN + 0.85, _y, 2.1, 0.45), label, 19.5, ifd.SANS, bold=True, color=ifd.INK)
ifd._write(ifd._tb(raw, ifd.MARGIN + 3.05, _y, 14.4, 0.45), desc, 19.5, ifd.SANS, color=ifd.BODY)
_y += 0.60
ifd._write(ifd._tb(raw, ifd.MARGIN, ifd.Y_FOOTER - 0.1, 6.0, 0.6), "INFINITH", 30.0, ifd.SERIF,
bold=True, color=ifd.INK, spc=300)
ifd._write(ifd._tb(raw, 11.0, ifd.Y_FOOTER, 7.75, 0.49), "o2oteam@o2o.kr", 20.25, ifd.SANS,
color=ifd.MUTED, align=ifd.PP_ALIGN.RIGHT)
d.save(OUT)
print("saved", OUT)