- 신규 라우트 /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
211 lines
12 KiB
Python
211 lines
12 KiB
Python
"""
|
||
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([
|
||
("W1–2", "스키마·NAP·측정 인프라 설치.\n기준선 스팟체크 1회."),
|
||
("W3–6", "정의문·FAQ·H1 재설계·HTML 표.\nWordPress 업그레이드."),
|
||
("W7–8", "재진단. 미검증 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)
|