o2o-infinith-demo/docs/reports/build_viewclinic_poc_deck.py
Haewon Kam 18740ca800 feat: AI Discovery 랜딩 /discovery 구현 (시안 ai_discovery_landing.html 기준)
- 신규 라우트 /discovery → DiscoveryLandingPage, 기존 /discovery/:id 리포트와 공존
- 섹션 9종: Hero(ABMR 루프 + AI 답변 목업) / Why Now / How It Works / Sample Report / 8주 로드맵 / 정직성 스트립 / 최종 CTA
- ABMR 루프는 Solution.tsx 다이어그램 구조 복제, AGDP → ABMR 워딩 교체
- 점수·등급·카테고리 %·항목 수 전부 scoreDiscovery 계산값 파생, JSX 수치 하드코딩 없음
  (nextGradeTarget·scorePctColor 헬퍼를 discoveryScore에 추가)
- ScoreRing에 valueClassName·subLabel 옵션 prop 추가 (하위 호환)
- AI 답변 목업 "AEO / GEO 구축 후 목표 상태 예시" 라벨 유지, 연락처 o2oteam@o2o.kr 단일
- 시안 HTML·핸드오버 문서·POC 덱 산출물 포함

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WgWoJZAvvzMxTpWeenZSia
2026-08-31 16:17:25 +09:00

211 lines
12 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 규격, 10페이지)
제안 4장 + 진단 리포트 요약 5장 + 클로징 1장.
본문 텍스트는 기본값 대비 +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 sys
sys.path.insert(0, "/Users/haewonkam/.claude/skills/infinith-deck/scripts")
import ifd
from ifd import Deck
BODY = 18.0 # rows 본문·불릿 (기본 16 + 2)
GRID = 16.25 # 스코어카드 격자 셀 (기본 14.25 + 2)
QUOTE = 19.25 # 인용 본문 (기본 17.25 + 2)
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. 08. 31",
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("뷰성형외과 실측 · 48 / 100 · C", "critical").rows([
("읽을 수는 있다", "robots.txt가 AI 검색봇 12종을 전부 허용. 크롤러 접근성 92%로 기술적 전제는 이미 확보."),
("인용할 근거가 없다", "MedicalClinic 스키마 없음, FAQ 0개, 정의문 0개. 답변 인용 가능 콘텐츠 점수 20%."),
("성과를 잴 수도 없다", "AI 유입 측정 인프라 0%. AI가 보내는 환자를 세는 장치 자체가 아직 없음."),
], b_size=BODY)
# 03 Offering — 랜딩 히어로 스크린샷 + 3스텝
from pptx.util import Inches
SHOT = "/Users/haewonkam/orca/workspaces/INFINITH/AEO-GEO-Audit/docs/reports/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 / 1384)
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차 진단 완료, 48점 등급 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("48 → 61+", "AI ANSWER READINESS", "등급 C에서 B로")
k2.kpi("100%", "AI 유입 측정 인프라", "현재 0%. GA4·서버로그 설치")
k3.kpi("36 / 36", "검증 항목 커버리지", "미검증 5개를 계정 연동으로 해소")
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개 항목 중 31개를 실측했고, 오너 계정이 필요한 5개는 점수 분모에서 제외했습니다.",
"접근성(A) 92%와 표면(D) 72%는 양호. 엔티티(B) 30%와 콘텐츠(C) 20%가 총점을 끌어내립니다."],
section="Report · Summary")
img_h = 9.9 - s.band_y
img_w = img_h * (1440 / 912)
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", "15.7 / 17", "6 / 7", "우수"],
["B · 엔티티 식별·구조화", "20", "6.0 / 20", "7 / 7", "미흡"],
["C · 답변 인용 가능 콘텐츠", "25", "5.0 / 25", "8 / 8", "치명"],
["D · AI 인용 표면 확보", "20", "13.7 / 19", "5 / 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점뿐.",
"총점 42.1 / 88 = 48점, 등급 C.",
"미검증 5개는 계정 접근으로 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", "재진단. 미검증 5개 반영해\n총점 재산정."),
], y=s.band_y + 0.6)
# 10 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 권한 공유 시 미검증 5개 항목을 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)