diff --git a/docs/AI_DISCOVERY_FRONTEND_HANDOVER.md b/docs/AI_DISCOVERY_FRONTEND_HANDOVER.md new file mode 100644 index 0000000..05e36b7 --- /dev/null +++ b/docs/AI_DISCOVERY_FRONTEND_HANDOVER.md @@ -0,0 +1,94 @@ +# AI Discovery · Frontend Handover + +2026-08-31 세션 산출물을 INFINITH 프로덕트(React)에 반영하기 위한 인수인계 문서. +시안은 HTML로 확정됐고, 이 문서는 그것을 실제 라우트로 옮기는 데 필요한 전부를 담는다. + +--- + +## 0. 다음 세션 실행 프롬프트 (복붙용) + +``` +docs/AI_DISCOVERY_FRONTEND_HANDOVER.md 를 읽고 AI Discovery 랜딩 페이지를 프로덕트에 구현해줘. + +- 시안: docs/reports/ai_discovery_landing.html (픽셀 기준, 이 파일이 정답) +- 라우트: /discovery (index) 신규, 기존 /discovery/:id 리포트 페이지와 공존 +- 재사용: Navbar/Footer(BI 워드마크), ScoreRing, Solution.tsx의 루프 다이어그램 구조(AGDP → ABMR 워딩) +- 데이터: src/data/discovery_viewclinic.ts + src/lib/discoveryScore.ts 실측값만 사용, 수치 하드코딩 금지 +- 카피 규칙: 존댓말 문장형 타이틀, 엠대시 금지, break-keep, AI 답변 목업에 "구축 후 목표 상태 예시" 라벨 유지, 연락처는 o2oteam@o2o.kr만 +- 완료 기준: npm run lint 통과, /discovery가 시안과 시각적으로 일치, 모바일 1열 폴백 +끝나면 git push + vercel --prod 까지. +``` + +--- + +## 1. 이번 세션에서 만든 것 + +| 산출물 | 경로 | 상태 | +|---|---|---| +| 랜딩 시안 (단일 HTML, 픽셀 정답) | `docs/reports/ai_discovery_landing.html` | 확정 | +| 랜딩 스크린샷 (덱 삽입용) | `docs/reports/shots/land_hero.png`, `land_report.png` | 확정 | +| POC 제안서 덱 (10장) | `docs/reports/Viewclinic_POC_Proposal.pptx` / `.pdf` | 확정 | +| 덱 빌드 스크립트 | `docs/reports/build_viewclinic_poc_deck.py` | 재실행 가능 | + +시안 HTML은 외부 의존성이 Google Fonts(Playfair Display, Noto Sans KR) 하나뿐인 +단일 파일이다. 랜딩을 수정하면 스크린샷 재캡처 → 덱 리빌드 순서로 갱신한다 +(스크린샷 크롭 좌표는 배경색 감지로 잡았다: 히어로 0–1384px, 리포트 밴드는 #F4F6FB 행 범위). + +## 2. 구현 대상: 라우트와 페이지 + +- 신규 라우트: `/discovery` → `DiscoveryLandingPage.tsx` (src/main.tsx에 추가) +- 기존 라우트: `/discovery/:id` → `DiscoveryReportPage.tsx` (수정 불필요, 랜딩의 "샘플 리포트 보기"가 `/discovery/viewclinic`으로 연결) +- Navbar/Footer는 전역 레이아웃이 이미 감싼다. 랜딩 시안의 자체 네비·푸터 마크업은 옮기지 말 것. + +## 3. 섹션 → 컴포넌트 매핑 (시안 위에서 아래 순서) + +| 시안 섹션 | 컴포넌트 제안 | 재사용/참조 | +|---|---|---| +| 히어로 (72px 헤드라인 + 배지 + CTA 2개) | `discovery/DiscoveryHero.tsx` | `Hero.tsx` 구조, `.text-gradient` | +| ABMR 루프 카드 (다크) | `discovery/AbmrLoop.tsx` | **`Solution.tsx` 46–130행 다이어그램을 복제 후 워딩만 교체** | +| AI 답변 목업 카드 | `discovery/AiAnswerMock.tsx` | glass-card | +| Why Now 다크 밴드 (12종 / 0건 / 35~70%) | `discovery/WhyNow.tsx` | | +| How It Works (Audit·Build·Prove) | `discovery/HowItWorks.tsx` | glass-card + EFE6FF 아이콘 스퀘어 | +| 샘플 리포트 (48점 링 + A~F 바 + 칩 3장) | `discovery/SampleReport.tsx` | **`ScoreRing.tsx` 그대로**, 바 색상은 `getScoreColor()` 로직 재사용 | +| 8주 로드맵 타임라인 | `discovery/Roadmap.tsx` | | +| 정직성 인용문 + 3필 | `discovery/HonestyStrip.tsx` | | +| 최종 CTA (다크) | `discovery/DiscoveryCta.tsx` | | + +## 4. 디자인 실측값 (시안에 이미 적용된 프로덕션 토큰) + +- **BI 워드마크**: Playfair Display Black(900) 30px, tracking 0.05em, `linear-gradient(to right, #4F1DA1, #021341)` 텍스트 클립. Navbar/Footer 컴포넌트에 이미 있으므로 재사용. +- **DS Primary pill 버튼**: `px-10 py-4 text-lg font-medium rounded-full shadow-xl`, hero 변형은 `from-[#4F1DA1] to-[#021341]` + ArrowRight. `MultiChannelInput.tsx` 195행 참조. +- **text-gradient**: `#0A1128 → #3b5998` 클립 (index.css `.text-gradient`). +- **히어로 배경**: radial `#e0e7ff → #faf5ff → #fdf2f8` + 블롭 (Hero.tsx와 동일 계열). +- **ScoreRing**: strokeWidth 8, 트랙 `#f1f5f9`, 점수 구간색 ≤40 `#C084CF` / ≤60 `#8B9CF7` / ≤80 `#7C6DD8` / 80+ `#6C5CE7`. 48점은 `#8B9CF7`. +- **상태 칩**: index.css의 status-critical/warning/good/info 파스텔 토큰 (치명/보완/강점 칩). +- **다크 카드**: `#0F1B45` bg + `#2A3A6B` border (Why Now 통계 카드). +- 루프 회전 애니메이션: `animate-[spin_20s_linear_infinite]` (Solution.tsx와 동일). + +## 5. 데이터 연결 + +- 수치는 전부 `src/data/discovery_viewclinic.ts`(`DISCOVERY_VIEWCLINIC`)와 `src/lib/discoveryScore.ts` 계산 결과에서 가져온다. **48, 92%, 20% 등을 JSX에 하드코딩하지 말 것** (재진단 시 점수가 바뀐다). +- 카테고리 바 6개(A~F)는 discoveryScore의 카테고리별 pct를 순회 렌더. +- 시안의 고정 수치는 2026-08-28 실측 스냅샷이다. 값이 다르게 나오면 데이터 쪽이 정답. + +## 6. 카피·정직성 규칙 (이번 세션에서 확정된 것) + +1. 타이틀은 존댓말 문장형으로 통일 ("~없습니다", "~올립니다"). 반말 종결·명사구 타이틀 금지. +2. 엠대시 금지, 은유 금지, `break-keep` 적용 (기존 텍스트 스타일 규칙과 동일). +3. AI 답변 목업에는 **"AEO / GEO 구축 후 목표 상태 예시" 라벨을 반드시 유지**한다. 현재 상태로 오인되게 만들지 않는다. +4. 허위 도메인·이메일 금지. 연락처는 `o2oteam@o2o.kr` (Navbar 문의 mailto는 기존 `contact@o2o.kr` 유지). +5. 시장 통계를 새로 만들어 넣지 않는다. 시안의 3개 수치(12종 크롤러 / 네이버 기여 0건 / Direct 유실 35~70%)는 근거 있는 실측·문헌 값이며, 이 밖의 수치 추가는 근거 확인 후에만. +6. 순위·노출 보장형 문구 금지. "언급률과 신뢰구간으로 보고" 프레임 유지. + +## 7. 완료 기준·배포 + +- `npm run lint` (tsc --noEmit) 통과. +- 1440px에서 `/discovery`가 `docs/reports/ai_discovery_landing.html`과 시각적으로 일치. +- 390px에서 2열 그리드가 1열로 접히고 헤드라인이 줄바꿈 깨짐 없이 표시. +- 배포: `git push` 후 `vercel --prod` 수동 실행 (Gitea 자동 트리거 없음). + +## 8. 이후 백로그 (이번 구현 범위 아님) + +- 랜딩 CTA를 실제 진단 신청 폼/파이프라인(`discover-channels` Edge Function)에 연결. +- 태하(법무법인, 68점 B) 케이스를 두 번째 샘플로 노출할지 결정. +- 덱 스크린샷 자동 갱신 스크립트화 (랜딩 변경 → 캡처 → 크롭 → 덱 리빌드). diff --git a/docs/reports/Viewclinic AI Discovery_POC_Proposal.pdf b/docs/reports/Viewclinic AI Discovery_POC_Proposal.pdf new file mode 100644 index 0000000..7866c2d Binary files /dev/null and b/docs/reports/Viewclinic AI Discovery_POC_Proposal.pdf differ diff --git a/docs/reports/Viewclinic_AI Discovery_POC_Proposal.pptx b/docs/reports/Viewclinic_AI Discovery_POC_Proposal.pptx new file mode 100644 index 0000000..9caead0 Binary files /dev/null and b/docs/reports/Viewclinic_AI Discovery_POC_Proposal.pptx differ diff --git a/docs/reports/ai_discovery_landing.html b/docs/reports/ai_discovery_landing.html new file mode 100644 index 0000000..723e1e6 --- /dev/null +++ b/docs/reports/ai_discovery_landing.html @@ -0,0 +1,401 @@ + + + + + +INFINITH · AI Discovery AEO/GEO + + + + + +
+ + +
+
+
+ INFINITH + AI DISCOVERY +
+
+ 왜 지금인가 + 프로세스 + 샘플 리포트 +
+ 무료 진단 받기 +
+
+
+
+ + +
+
+
+
+ +
+ + AI DISCOVERY · AEO / GEO for Clinics +
+ +
AI가 추천하는 병원은
따로 있습니다.
+ +
고객은 이제 ChatGPT와 Perplexity에게 병원을 묻습니다.
답변에 인용될 근거를 36개 항목으로 실측 진단하고, 8주 안에 구축합니다.
+ +
+
+ 무료 AEO / GEO 진단 받기 + +
+
+ 샘플 리포트 보기 +
+
+ + +
+ + +
+
+ +
THE ENGINE · ABMR CYCLE
+ +
+ + + + + + + + + + + +
+ ABMR + Discovery
Loop
+
+ +
+
A
+ Audit +
+
+
B
+ Build +
+
+
M
+ Measure +
+
+
R
+ Refine +
+ + + + ← CITATION FEEDBACK + +
+ +
+
ABMR Cycle: 실측 진단(Audit) → 스키마·콘텐츠 구축(Build) → 언급률·AI 유입 측정(Measure) → 우선순위 재조정(Refine)의 무한 루프. 사이클이 돌 때마다 측정된 언급률이 다음 구축의 입력이 됩니다.
+
+
+
점수 48 → 61+
+
등급 C → B
+
언급률 상승 추적
+
+
+ + +
+
THE OUTPUT · AI 답변 인용
+
+ + 강남에서 안전하게 가슴성형 상담받을 수 있는 병원 추천해줘 +
+
모티바 공식 KOL 의료진이 있는 뷰성형외과는 강남언니 후기 19,316건과 의료진 26명 정보가 공개되어 있으며, 시술별 FAQ와 비교표를 제공합니다.
+
+
viewclinic.com
+
gangnamunni.com
+
youtube.com
+ AEO / GEO 구축 후 목표 상태 예시 +
+
+
+ + 루프 1회전(8주)마다 36개 항목을 재측정해 점수를 갱신합니다. +
+
+ + 질의 10개 × 5회 스팟체크로 인용 여부를 신뢰구간과 함께 보고합니다. +
+
+ + 측정 결과가 다음 사이클의 구축 우선순위를 자동으로 정합니다. +
+
+
+ +
+ +
+
+ + +
+
+
WHY NOW
+
검색은 끝났고, 답변이 시작됐습니다.
+
답변엔진은 광고 입찰이 아니라 크롤링 가능한 오픈웹 근거로 병원을 고릅니다. 인용되지 못하면 비교 대상에도 오르지 못합니다.
+
+
+
12종
+
AI 크롤러가 이미 읽는 중
+
GPTBot·OAI-SearchBot·PerplexityBot 등이 오늘도 병원 사이트를 수집합니다. 문제는 접근이 아니라 인용할 내용입니다.
+
+
+
0건
+
네이버 자산의 AI 기여
+
네이버 블로그·플레이스는 robots.txt로 AI 크롤러를 전면 차단합니다(2026-08 실측). 지금까지의 네이버 마케팅은 AI 답변에 닿지 않습니다.
+
+
+
35~70%
+
Direct로 숨는 AI 유입
+
AI가 보낸 방문의 상당수는 유입 경로 없이 도착합니다. 측정 인프라 없이는 이미 오는 환자도 세지 못합니다.
+
+
+
+
+ + +
+
+
+
HOW IT WORKS
+
진단하고, 구축하고, 숫자로 증명합니다.
+
+
+ +
+
+ +
+
+ STEP 01 + Audit +
+
AEO/GEO 기준표 v1.0으로 36개 항목을 실측합니다. 크롤러 접근성부터 인용 가능 콘텐츠, 측정 인프라까지 6개 카테고리 100점 척도. 항목마다 근거 등급을 명시합니다.
+
+ +
+
+ +
+
+ STEP 02 + Build +
+
MedicalClinic·Physician 스키마를 심고, 흩어진 표면의 NAP을 한 글자까지 통일합니다. 핵심 페이지에 정의문과 시술별 FAQ를 넣어 AI가 잘라 갈 단락을 만듭니다.
+
+ +
+
+ +
+
+ STEP 03 + Prove +
+
GA4 AI 채널그룹과 서버로그 크롤러 검증을 설치하고, 질의 10개 × 5회 반복 스팟체크로 변화를 측정합니다. 단발 조회가 아니라 언급률과 신뢰구간으로만 보고합니다.
+
+ +
+
+
+ + +
+
+
SAMPLE REPORT · 뷰성형외과 실측
+
읽을 수는 있지만, 인용할 근거가 없습니다.
+
실제 1차 진단 결과입니다. 어디가 강하고 어디가 비어 있는지, 점수는 카테고리 단위로 드러납니다.
+ +
+ + +
+
+ + + + +
+ 48 + / 100 +
+
+
+
등급 C
+ AI Answer Readiness +
+
36개 항목 중 31개 실측.
미검증 5개는 분모에서 제외했습니다.
+
+ + +
+ +
+
+ A · AI 크롤러 접근성 + 92% +
+
+
+ +
+
+ B · 엔티티 식별·구조화 + 30% +
+
+
+ +
+
+ C · 답변 인용 가능 콘텐츠 + 20% +
+
+
+ +
+
+ D · AI 인용 표면 확보 + 72% +
+
+
+ +
+
+ E · AI 가시성 측정 인프라 + 0% +
+
+
+ +
+
+ F · 기술 위생 + 33% +
+
+
+ +
+
+ + +
+
+
치명
+
AI가 누구인지 확정 못 함
+
MedicalClinic 스키마 없음. 시술 목록 JSON-LD는 문법 오류로 통째로 버려집니다.
+
+
+
보완
+
인용할 문장이 없음
+
정의문 0개, FAQ 0개, HTML 표 0개. 답변엔진이 잘라 갈 단락이 없습니다.
+
+
+
강점
+
표면 자산은 이미 강함
+
강남언니 후기 19,316건, GBP 4.9점. 홈페이지가 따라가면 바로 인용 근거가 됩니다.
+
+
+ +
+
+ + +
+
+
+
8-WEEK ROADMAP
+
8주 안에, C에서 B로 올립니다.
+
+
+
+
+
+
+
W1–2
+
기반 설치
+
스키마·NAP·측정 인프라를 설치하고 기준선 스팟체크를 1회 측정합니다.
+
+
+
+
W3–6
+
콘텐츠 구조
+
정의문·FAQ·H1 재설계와 HTML 비교표를 구축하고 WordPress를 업그레이드합니다.
+
+
+
+
W7–8
+
재진단·보고
+
36개 항목을 재측정하고 미검증 5개를 반영해 총점을 다시 산정합니다.
+
+
+
+
+
+ + +
+
+
"스키마를 넣으면 인용된다고 약속하지 않습니다.
인용될 수 없는 이유를 전부 제거하고, 결과를 반복 측정으로 증명합니다."
+
+
효과 없는 llms.txt는 팔지 않습니다
+
순위·노출을 보장하지 않습니다
+
단발 측정으로 보고하지 않습니다
+
+
+
+ + +
+
+
AI가 당신의 병원을 인용하게 하세요.
+
진단은 무료입니다. 36개 항목 실측 리포트를 받아보고 결정하세요.
+
+
+ 무료 AEO / GEO 진단 받기 +
+ o2oteam@o2o.kr +
+
+
+ + +
+
+ INFINITH + © 2026 INFINITH. All rights reserved.
Infinite Marketing for Premium Medical Business & Marketing Agency
+
+
+ +
+ + \ No newline at end of file diff --git a/docs/reports/build_viewclinic_poc_deck.py b/docs/reports/build_viewclinic_poc_deck.py new file mode 100644 index 0000000..651e5f9 --- /dev/null +++ b/docs/reports/build_viewclinic_poc_deck.py @@ -0,0 +1,210 @@ +""" +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) diff --git a/docs/reports/shots/land_hero.png b/docs/reports/shots/land_hero.png new file mode 100644 index 0000000..86c47c8 Binary files /dev/null and b/docs/reports/shots/land_hero.png differ diff --git a/docs/reports/shots/land_report.png b/docs/reports/shots/land_report.png new file mode 100644 index 0000000..f72cd75 Binary files /dev/null and b/docs/reports/shots/land_report.png differ diff --git a/src/components/discovery/AbmrLoop.tsx b/src/components/discovery/AbmrLoop.tsx new file mode 100644 index 0000000..9805182 --- /dev/null +++ b/src/components/discovery/AbmrLoop.tsx @@ -0,0 +1,112 @@ +/** + * ABMR 루프 카드 (다크) — 히어로 좌측. + * Solution.tsx의 원형 루프 다이어그램 구조를 복제하고 워딩만 AGDP → ABMR로 교체. + * 점수·등급 칩은 discoveryScore 계산값에서만 받는다 (하드코딩 금지). + */ +import { motion } from 'motion/react'; +import type { OverallScore } from '../../types/discovery'; + +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' }, + { letter: 'M', label: 'Measure', pos: 'bottom-0 left-1/2 -translate-x-1/2 translate-y-1/2' }, + { letter: 'R', label: 'Refine', pos: 'top-1/2 left-0 -translate-x-1/2 -translate-y-1/2' }, +]; + +interface AbmrLoopProps { + overall: OverallScore; + /** 다음 등급 목표 (A등급이면 null) */ + target: { score: number; grade: OverallScore['grade'] } | null; +} + +export default function AbmrLoop({ overall, target }: AbmrLoopProps) { + const chips = [ + ...(target + ? [`점수 ${overall.score} → ${target.score}+`, `등급 ${overall.grade} → ${target.grade}`] + : [`점수 ${overall.score} 유지 검증`, `등급 ${overall.grade} 유지 검증`]), + '언급률 상승 추적', + ]; + + return ( + + {/* Background glow */} +
+ +
+ THE ENGINE · ABMR CYCLE +
+ + {/* Circular loop diagram (Solution.tsx 구조, ABMR 워딩) */} +
+ {/* Animated glowing ring */} + + + + + + + + + + + + {/* Center orb */} +
+ + ABMR + + + Discovery
Loop +
+
+ + {/* Nodes */} + {NODES.map((node) => ( +
+
+ {node.letter} +
+ {node.label} +
+ ))} + + {/* Citation feedback curved text (M → A) */} + + + + + + + ← CITATION FEEDBACK + + + +
+ + {/* Cycle description */} +
+

+ ABMR Cycle: 실측 진단(Audit) → 스키마·콘텐츠 + 구축(Build) → 언급률·AI 유입 측정(Measure) → 우선순위 재조정(Refine)의 무한 루프. + 사이클이 돌 때마다 측정된 언급률이 다음 구축의 입력이 + 됩니다. +

+
+ + {/* Outcome chips — 계산값 파생 */} +
+ {chips.map((chip) => ( +
+ {chip} +
+ ))} +
+ + ); +} diff --git a/src/components/discovery/AiAnswerMock.tsx b/src/components/discovery/AiAnswerMock.tsx new file mode 100644 index 0000000..9be38ab --- /dev/null +++ b/src/components/discovery/AiAnswerMock.tsx @@ -0,0 +1,77 @@ +/** + * AI 답변 목업 카드 — 히어로 우측. + * ⚠️ 정직성 규약: 이 카드는 실제 AI 답변이 아니라 "구축 후 목표 상태 예시"다. + * 라벨을 절대 제거하지 않는다 (현재 상태로 오인 금지). + */ +import { motion } from 'motion/react'; + +const CITED_DOMAINS = ['viewclinic.com', 'gangnamunni.com', 'youtube.com']; + +interface AiAnswerMockProps { + clinicName: string; + /** 기준표 항목 수 (재측정 문구에 사용) */ + criteriaCount: number; +} + +export default function AiAnswerMock({ clinicName, criteriaCount }: AiAnswerMockProps) { + const protocolPoints = [ + `루프 1회전(8주)마다 ${criteriaCount}개 항목을 재측정해 점수를 갱신합니다.`, + '질의 10개 × 5회 스팟체크로 인용 여부를 신뢰구간과 함께 보고합니다.', + '측정 결과가 다음 사이클의 구축 우선순위를 자동으로 정합니다.', + ]; + + return ( + +
+ THE OUTPUT · AI 답변 인용 +
+ + {/* 질의 */} +
+ + + 강남에서 안전하게 가슴성형 상담받을 수 있는 병원 추천해줘 + +
+ + {/* 목표 상태의 답변 예시 */} +

+ 모티바 공식 KOL 의료진이 있는{' '} + + {clinicName} + + 는 강남언니 후기 19,316건과 의료진 26명 정보가 공개되어 있으며, 시술별 FAQ와 비교표를 + 제공합니다. +

+ + {/* 인용 도메인 칩 + 목표 상태 라벨 (제거 금지) */} +
+ {CITED_DOMAINS.map((domain) => ( +
+ {domain} +
+ ))} + AEO / GEO 구축 후 목표 상태 예시 +
+ + {/* 측정 프로토콜 */} +
+ {protocolPoints.map((point) => ( +
+ + {point} +
+ ))} +
+
+ ); +} diff --git a/src/components/discovery/DiscoveryCta.tsx b/src/components/discovery/DiscoveryCta.tsx new file mode 100644 index 0000000..588b83b --- /dev/null +++ b/src/components/discovery/DiscoveryCta.tsx @@ -0,0 +1,44 @@ +/** + * 최종 CTA (다크). 연락처는 o2oteam@o2o.kr 단일 채널 (허위 연락처 금지). + */ +import { motion } from 'motion/react'; +import { buildDiscoveryMailto, DISCOVERY_CONTACT_EMAIL } from './discoveryContact'; + +interface DiscoveryCtaProps { + criteriaCount: number; +} + +export default function DiscoveryCta({ criteriaCount }: DiscoveryCtaProps) { + return ( +
+ +

+ AI가 당신의 병원을 인용하게 하세요. +

+

+ 진단은 무료입니다. {criteriaCount}개 항목 실측 리포트를 받아보고 결정하세요. +

+
+ + 무료 AEO / GEO 진단 받기 + + + {DISCOVERY_CONTACT_EMAIL} + +
+
+
+ ); +} diff --git a/src/components/discovery/DiscoveryHero.tsx b/src/components/discovery/DiscoveryHero.tsx new file mode 100644 index 0000000..938a7d7 --- /dev/null +++ b/src/components/discovery/DiscoveryHero.tsx @@ -0,0 +1,107 @@ +/** + * AI Discovery 랜딩 히어로. + * Hero.tsx의 배경·배지·헤드라인 구조를 따르고, 하단에 ABMR 루프 + AI 답변 목업 2열 그리드. + * 시안: docs/reports/ai_discovery_landing.html (픽셀 기준) + */ +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 AiAnswerMock from './AiAnswerMock'; +import { buildDiscoveryMailto } from './discoveryContact'; + +interface DiscoveryHeroProps { + overall: OverallScore; + target: { score: number; grade: OverallScore['grade'] } | null; + clinicName: string; + sampleReportPath: string; + criteriaCount: number; +} + +export default function DiscoveryHero({ + overall, + target, + clinicName, + sampleReportPath, + criteriaCount, +}: DiscoveryHeroProps) { + return ( +
+ {/* Decorative blobs */} +
+
+ +
+ {/* Badge */} + + + AI DISCOVERY · AEO / GEO for Clinics + + + {/* Headline */} + + AI가 추천하는 병원은 +
+ 따로 있습니다. +
+ + {/* Sub copy */} + + 고객은 이제 ChatGPT와 Perplexity에게 병원을 묻습니다. +
답변에 인용될 근거를 {criteriaCount}개 항목으로 실측 + 진단하고, 8주 안에 구축합니다. +
+ + {/* CTAs */} + + + 무료 AEO / GEO 진단 받기 + + + + 샘플 리포트 보기 + + + + {/* ABMR loop + AI answer mock */} +
+ + +
+
+
+ ); +} diff --git a/src/components/discovery/HonestyStrip.tsx b/src/components/discovery/HonestyStrip.tsx new file mode 100644 index 0000000..bb4cf20 --- /dev/null +++ b/src/components/discovery/HonestyStrip.tsx @@ -0,0 +1,38 @@ +/** + * 정직성 인용문 + 3필 (하지 않을 것). + * 순위·노출 보장형 문구 금지, "언급률과 신뢰구간으로 보고" 프레임 유지. + */ +import { motion } from 'motion/react'; + +const PILLS = [ + '효과 없는 llms.txt는 팔지 않습니다', + '순위·노출을 보장하지 않습니다', + '단발 측정으로 보고하지 않습니다', +]; + +export default function HonestyStrip() { + return ( +
+ +
+ "스키마를 넣으면 인용된다고 약속하지 않습니다. +
인용될 수 없는 이유를 전부 제거하고, 결과를 반복 + 측정으로 증명합니다." +
+
+ {PILLS.map((pill) => ( +
+ {pill} +
+ ))} +
+
+
+ ); +} diff --git a/src/components/discovery/HowItWorks.tsx b/src/components/discovery/HowItWorks.tsx new file mode 100644 index 0000000..9076757 --- /dev/null +++ b/src/components/discovery/HowItWorks.tsx @@ -0,0 +1,103 @@ +/** + * How It Works — Audit · Build · Prove 3단계 glass-card. + * 아이콘은 시안의 커스텀 SVG를 그대로 사용 (F3F0FF 스퀘어 + accent 스트로크). + */ +import type { ReactNode } from 'react'; +import { motion } from 'motion/react'; + +interface Step { + step: string; + title: string; + detail: string; + icon: ReactNode; +} + +interface HowItWorksProps { + criteriaCount: number; + categoryCount: number; +} + +export default function HowItWorks({ criteriaCount, categoryCount }: HowItWorksProps) { + const steps: Step[] = [ + { + step: 'STEP 01', + title: 'Audit', + detail: `AEO/GEO 기준표 v1.0으로 ${criteriaCount}개 항목을 실측합니다. 크롤러 접근성부터 인용 가능 콘텐츠, 측정 인프라까지 ${categoryCount}개 카테고리 100점 척도. 항목마다 근거 등급을 명시합니다.`, + icon: ( + + ), + }, + { + step: 'STEP 02', + title: 'Build', + detail: + 'MedicalClinic·Physician 스키마를 심고, 흩어진 표면의 NAP을 한 글자까지 통일합니다. 핵심 페이지에 정의문과 시술별 FAQ를 넣어 AI가 잘라 갈 단락을 만듭니다.', + icon: ( + + ), + }, + { + step: 'STEP 03', + title: 'Prove', + detail: + 'GA4 AI 채널그룹과 서버로그 크롤러 검증을 설치하고, 질의 10개 × 5회 반복 스팟체크로 변화를 측정합니다. 단발 조회가 아니라 언급률과 신뢰구간으로만 보고합니다.', + icon: ( + + ), + }, + ]; + + return ( +
+
+ +
HOW IT WORKS
+

+ 진단하고, 구축하고, 숫자로 증명합니다. +

+
+ +
+ {steps.map((item, index) => ( + +
+ {item.icon} +
+
+ {item.step} + {item.title} +
+

{item.detail}

+
+ ))} +
+
+
+ ); +} diff --git a/src/components/discovery/Roadmap.tsx b/src/components/discovery/Roadmap.tsx new file mode 100644 index 0000000..7f0ed18 --- /dev/null +++ b/src/components/discovery/Roadmap.tsx @@ -0,0 +1,77 @@ +/** + * 8주 로드맵 타임라인. 타이틀의 등급 전환(C → B)은 계산값에서 파생. + */ +import { motion } from 'motion/react'; +import type { OverallScore } from '../../types/discovery'; + +interface RoadmapProps { + grade: OverallScore['grade']; + targetGrade: OverallScore['grade'] | null; + criteriaCount: number; + unverifiedCount: number; +} + +export default function Roadmap({ grade, targetGrade, criteriaCount, unverifiedCount }: RoadmapProps) { + const phases = [ + { + week: 'W1–2', + title: '기반 설치', + detail: '스키마·NAP·측정 인프라를 설치하고 기준선 스팟체크를 1회 측정합니다.', + }, + { + week: 'W3–6', + title: '콘텐츠 구조', + detail: '정의문·FAQ·H1 재설계와 HTML 비교표를 구축하고 WordPress를 업그레이드합니다.', + }, + { + week: 'W7–8', + title: '재진단·보고', + detail: `${criteriaCount}개 항목을 재측정하고 미검증 ${unverifiedCount}개를 반영해 총점을 다시 산정합니다.`, + }, + ]; + + return ( +
+
+ +
8-WEEK ROADMAP
+

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

+
+ +
+ {/* Connecting line (desktop only) */} +
+
+ {phases.map((phase, index) => ( + +
+
{phase.week}
+
{phase.title}
+

+ {phase.detail} +

+ + ))} +
+
+
+
+ ); +} diff --git a/src/components/discovery/SampleReport.tsx b/src/components/discovery/SampleReport.tsx new file mode 100644 index 0000000..631d531 --- /dev/null +++ b/src/components/discovery/SampleReport.tsx @@ -0,0 +1,158 @@ +/** + * 샘플 리포트 섹션 — 뷰성형외과 실측 스냅샷. + * 점수·등급·카테고리 %·항목 수는 전부 discoveryScore 계산값에서 렌더 (하드코딩 금지). + * 발견 칩 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'; + +const GRADE_BADGE_STYLE: Record = { + A: 'bg-[#EFF0FF] border-[#C5CBF5] text-[#3A3F7C]', + B: 'bg-[#F3F0FF] border-[#D5CDF5] text-[#4A3A7C]', + C: 'bg-[#FFF6ED] border-[#F5E0C5] text-[#7C5C3A]', + D: 'bg-[#FFF0F0] border-[#F5D5DC] text-[#7C3A4B]', +}; + +/** keyFindings 압축본 (시안 확정 카피). 수치는 2026-08-28 실측 스냅샷과 일치한다. */ +const FINDING_CHIPS = [ + { + label: '치명', + chipClass: 'bg-[#FFF0F0] border-[#F5D5DC] text-[#7C3A4B]', + dotColor: '#D4889A', + title: 'AI가 누구인지 확정 못 함', + detail: 'MedicalClinic 스키마 없음. 시술 목록 JSON-LD는 문법 오류로 통째로 버려집니다.', + }, + { + label: '보완', + chipClass: 'bg-[#FFF6ED] border-[#F5E0C5] text-[#7C5C3A]', + dotColor: '#D4A872', + title: '인용할 문장이 없음', + detail: '정의문 0개, FAQ 0개, HTML 표 0개. 답변엔진이 잘라 갈 단락이 없습니다.', + }, + { + label: '강점', + chipClass: 'bg-[#F3F0FF] border-[#D5CDF5] text-[#4A3A7C]', + dotColor: '#9B8AD4', + title: '표면 자산은 이미 강함', + detail: '강남언니 후기 19,316건, GBP 4.9점. 홈페이지가 따라가면 바로 인용 근거가 됩니다.', + }, +]; + +interface SampleReportProps { + overall: OverallScore; + result: DiscoveryResult; +} + +export default function SampleReport({ overall, 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; + + return ( +
+
+ +
+ SAMPLE REPORT · {result.clinicName} 실측 +
+

+ 읽을 수는 있지만, 인용할 근거가 없습니다. +

+

+ 실제 1차 진단 결과입니다. 어디가 강하고 어디가 비어 있는지, 점수는 카테고리 단위로 + 드러납니다. +

+
+ +
+ {/* Score ring card */} + + +
+
+ 등급 {overall.grade} +
+ AI Answer Readiness +
+

+ {totalCriteria}개 항목 중 {verifiedCriteria}개 실측. +
+ 미검증 {unverifiedCriteria}개는 분모에서 제외했습니다. +

+
+ + {/* Category bars */} + + {overall.categories.map((cat) => { + const color = scorePctColor(cat.pct); + return ( +
+
+ + {cat.category.code} · {cat.category.name} + + + {cat.pct}% + +
+
+ +
+
+ ); + })} +
+
+ + {/* Finding chips */} +
+ {FINDING_CHIPS.map((finding, index) => ( + +
+ + {finding.label} +
+
{finding.title}
+

{finding.detail}

+
+ ))} +
+
+
+ ); +} diff --git a/src/components/discovery/WhyNow.tsx b/src/components/discovery/WhyNow.tsx new file mode 100644 index 0000000..129298f --- /dev/null +++ b/src/components/discovery/WhyNow.tsx @@ -0,0 +1,70 @@ +/** + * Why Now 다크 밴드. + * ⚠️ 3개 수치(12종 크롤러 / 네이버 기여 0건 / Direct 유실 35~70%)는 근거 있는 실측·문헌 값. + * 새 시장 통계를 임의로 추가하지 않는다 (근거 확인 후에만). + */ +import { motion } from 'motion/react'; + +const STATS = [ + { + figure: '12종', + title: 'AI 크롤러가 이미 읽는 중', + detail: + 'GPTBot·OAI-SearchBot·PerplexityBot 등이 오늘도 병원 사이트를 수집합니다. 문제는 접근이 아니라 인용할 내용입니다.', + }, + { + figure: '0건', + title: '네이버 자산의 AI 기여', + detail: + '네이버 블로그·플레이스는 robots.txt로 AI 크롤러를 전면 차단합니다(2026-08 실측). 지금까지의 네이버 마케팅은 AI 답변에 닿지 않습니다.', + }, + { + figure: '35~70%', + title: 'Direct로 숨는 AI 유입', + detail: + 'AI가 보낸 방문의 상당수는 유입 경로 없이 도착합니다. 측정 인프라 없이는 이미 오는 환자도 세지 못합니다.', + }, +]; + +export default function WhyNow() { + return ( +
+
+ +
WHY NOW
+

+ 검색은 끝났고, 답변이 시작됐습니다. +

+

+ 답변엔진은 광고 입찰이 아니라 크롤링 가능한 오픈웹 근거로 병원을 고릅니다. 인용되지 + 못하면 비교 대상에도 오르지 못합니다. +

+
+ +
+ {STATS.map((stat, index) => ( + +
+ {stat.figure} +
+
{stat.title}
+

{stat.detail}

+
+ ))} +
+
+
+ ); +} diff --git a/src/components/discovery/discoveryContact.ts b/src/components/discovery/discoveryContact.ts new file mode 100644 index 0000000..d9cb4be --- /dev/null +++ b/src/components/discovery/discoveryContact.ts @@ -0,0 +1,8 @@ +/** + * AI Discovery 랜딩 전용 연락처. + * 랜딩 CTA는 o2oteam@o2o.kr 단일 채널 (전역 Navbar의 contact@o2o.kr과 별도 트랙). + */ +export const DISCOVERY_CONTACT_EMAIL = 'o2oteam@o2o.kr'; + +export const buildDiscoveryMailto = (subject: string) => + `mailto:${DISCOVERY_CONTACT_EMAIL}?subject=${encodeURIComponent(`[INFINITH AI Discovery] ${subject}`)}`; diff --git a/src/components/report/ui/ScoreRing.tsx b/src/components/report/ui/ScoreRing.tsx index bc392e9..9bbec5f 100644 --- a/src/components/report/ui/ScoreRing.tsx +++ b/src/components/report/ui/ScoreRing.tsx @@ -6,6 +6,10 @@ interface ScoreRingProps { size?: number; label?: string; color?: string; + /** 링 중앙 숫자 타이포 오버라이드 (기본 text-2xl) */ + valueClassName?: string; + /** 링 중앙 숫자 아래 보조 표기 (예: "/ 100") */ + subLabel?: string; } function getScoreColor(score: number, maxScore: number): string { @@ -22,6 +26,8 @@ export function ScoreRing({ size = 120, label, color, + valueClassName = 'text-2xl', + subLabel, }: ScoreRingProps) { const strokeWidth = 8; const radius = (size - strokeWidth) / 2; @@ -61,8 +67,9 @@ export function ScoreRing({ transition={{ duration: 1, ease: 'easeOut' }} /> -
- {score} +
+ {score} + {subLabel && {subLabel}}
{label && ( diff --git a/src/lib/discoveryScore.ts b/src/lib/discoveryScore.ts index b4ee99f..efd1886 100644 --- a/src/lib/discoveryScore.ts +++ b/src/lib/discoveryScore.ts @@ -30,6 +30,23 @@ export const gradeFromScore = (score: number): OverallScore['grade'] => { return 'D'; }; +/** 점수 구간별 색 (ScoreRing과 동일 기준) */ +export const scorePctColor = (pct: number) => + pct <= 40 ? '#C084CF' : pct <= 60 ? '#8B9CF7' : pct <= 80 ? '#7C6DD8' : '#6C5CE7'; + +/** + * 다음 등급까지의 목표 점수. gradeFromScore의 경계와 반드시 일치해야 한다. + * 재진단 목표 표기에 사용 (예: 48/C → { score: 61, grade: 'B' }). A등급이면 null. + */ +export const nextGradeTarget = ( + score: number, +): { score: number; grade: OverallScore['grade'] } | null => { + if (score <= 40) return { score: 41, grade: 'C' }; + if (score <= 60) return { score: 61, grade: 'B' }; + if (score <= 80) return { score: 81, grade: 'A' }; + return null; +}; + export function scoreDiscovery(rubric: Rubric, result: DiscoveryResult): OverallScore { const byId = new Map(result.results.map((r) => [r.criterionId, r])); diff --git a/src/main.tsx b/src/main.tsx index 1c46b8c..ba528d5 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -15,6 +15,7 @@ import DataValidationPage from './pages/DataValidationPage.tsx'; import ClinicProfilePage from './pages/ClinicProfilePage.tsx'; import ApiDashboardPage from './pages/ApiDashboardPage.tsx'; import LoginPage from './pages/LoginPage.tsx'; +import DiscoveryLandingPage from './pages/DiscoveryLandingPage.tsx'; import DiscoveryReportPage from './pages/DiscoveryReportPage.tsx'; import './index.css'; @@ -37,6 +38,7 @@ createRoot(document.getElementById('root')!).render( } /> } /> } /> + } /> } /> diff --git a/src/pages/DiscoveryLandingPage.tsx b/src/pages/DiscoveryLandingPage.tsx new file mode 100644 index 0000000..cf9bf89 --- /dev/null +++ b/src/pages/DiscoveryLandingPage.tsx @@ -0,0 +1,53 @@ +/** + * /discovery — AI Discovery(AEO/GEO) 서비스 랜딩. + * 시안: docs/reports/ai_discovery_landing.html (2026-08-31 확정) + * + * 데이터 원칙: 점수·등급·카테고리 %·항목 수는 전부 + * scoreDiscovery(AEO_GEO_RUBRIC, DISCOVERY_VIEWCLINIC) 계산값에서 파생한다. + * 재진단으로 데이터가 바뀌면 랜딩 수치도 함께 바뀐다 (JSX 하드코딩 금지). + */ +import { useMemo } from 'react'; +import { AEO_GEO_RUBRIC } from '../data/aeoGeoRubric'; +import { DISCOVERY_VIEWCLINIC } from '../data/discovery_viewclinic'; +import { scoreDiscovery, nextGradeTarget } from '../lib/discoveryScore'; +import DiscoveryHero from '../components/discovery/DiscoveryHero'; +import WhyNow from '../components/discovery/WhyNow'; +import HowItWorks from '../components/discovery/HowItWorks'; +import SampleReport from '../components/discovery/SampleReport'; +import Roadmap from '../components/discovery/Roadmap'; +import HonestyStrip from '../components/discovery/HonestyStrip'; +import DiscoveryCta from '../components/discovery/DiscoveryCta'; + +export default function DiscoveryLandingPage() { + const result = DISCOVERY_VIEWCLINIC; + const overall = useMemo(() => scoreDiscovery(AEO_GEO_RUBRIC, result), [result]); + const target = nextGradeTarget(overall.score); + + const criteriaCount = AEO_GEO_RUBRIC.criteria.length; + const categoryCount = AEO_GEO_RUBRIC.categories.length; + const verifiedCount = overall.categories.reduce((sum, c) => sum + c.verifiedCount, 0); + const unverifiedCount = criteriaCount - verifiedCount; + + return ( +
+ + + + + + + +
+ ); +}