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
This commit is contained in:
Haewon Kam 2026-08-31 16:17:25 +09:00
parent 85d72e4cfb
commit 18740ca800
21 changed files with 1580 additions and 2 deletions

View File

@ -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) 하나뿐인
단일 파일이다. 랜딩을 수정하면 스크린샷 재캡처 → 덱 리빌드 순서로 갱신한다
(스크린샷 크롭 좌표는 배경색 감지로 잡았다: 히어로 01384px, 리포트 밴드는 #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` 46130행 다이어그램을 복제 후 워딩만 교체** |
| 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) 케이스를 두 번째 샘플로 노출할지 결정.
- 덱 스크린샷 자동 갱신 스크립트화 (랜딩 변경 → 캡처 → 크롭 → 덱 리빌드).

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,401 @@
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>INFINITH · AI Discovery AEO/GEO</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,600;0,700;0,800;0,900;1,600&family=Noto+Sans+KR:wght@400;500;600;700&display=swap">
<style>
body { margin: 0; background: #f8fafc; font-family: "Pretendard Variable", "Pretendard", "Noto Sans KR", "Apple SD Gothic Neo", "Malgun Gothic", sans-serif; color: #1e293b; -webkit-font-smoothing: antialiased; word-break: keep-all; }
a { color: #6C5CE7; text-decoration: none; }
a:hover { color: #4F1DA1; }
@keyframes if-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
</style>
</head>
<body>
<div style="width: 100%; background: #f8fafc;">
<!-- ── Nav ─────────────────────────────────────────────────────────── -->
<div style="display: flex; justify-content: center; background: rgba(255,255,255,0.95); backdrop-filter: blur(12px); border-bottom: 1px solid #f1f5f9;">
<div style="width: 100%; max-width: 1120px; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; height: 80px; box-sizing: border-box;">
<div style="display: flex; align-items: baseline; gap: 10px;">
<span style="font-family: 'Playfair Display', Georgia, serif; font-weight: 900; font-size: 30px; letter-spacing: 0.05em; background: linear-gradient(to right, #4F1DA1, #021341); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; color: transparent;">INFINITH</span>
<span style="font-size: 13px; font-weight: 600; letter-spacing: 0.08em; color: #6C5CE7;">AI DISCOVERY</span>
</div>
<div style="display: flex; align-items: center; gap: 28px;">
<a href="#" style="font-size: 15px; font-weight: 500; color: #475569;">왜 지금인가</a>
<a href="#" style="font-size: 15px; font-weight: 500; color: #475569;">프로세스</a>
<a href="#" style="font-size: 15px; font-weight: 500; color: #475569;">샘플 리포트</a>
<div style="display: flex; align-items: center; height: 44px; padding: 0 22px; background: #0A1128; border-radius: 9999px;">
<span style="font-size: 15px; font-weight: 600; color: #ffffff;">무료 진단 받기</span>
</div>
</div>
</div>
</div>
<!-- ── Hero ────────────────────────────────────────────────────────── -->
<div style="position: relative; overflow: hidden; background: radial-gradient(ellipse at top, #e0e7ff 0%, #faf5ff 45%, #fdf2f8 100%);">
<div style="position: absolute; top: 120px; left: 60px; width: 256px; height: 256px; background: #d8b4fe; border-radius: 9999px; filter: blur(64px); opacity: 0.3;"></div>
<div style="position: absolute; top: 180px; right: 60px; width: 256px; height: 256px; background: #f9a8d4; border-radius: 9999px; filter: blur(64px); opacity: 0.3;"></div>
<div style="position: relative; display: flex; flex-direction: column; align-items: center; text-align: center; padding: 104px 24px 88px;">
<div style="display: inline-flex; align-items: center; gap: 8px; padding: 8px 16px; border-radius: 9999px; background: rgba(255,255,255,0.6); backdrop-filter: blur(8px); border: 1px solid rgba(255,255,255,0.4); box-shadow: 0 1px 2px rgba(0,0,0,0.05); margin-bottom: 32px;">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M4.5 8c0-1.4 1-2.5 2.2-2.5 1.6 0 2.6 5 4.2 5 1.2 0 2.2-1.1 2.2-2.5s-1-2.5-2.2-2.5c-1.6 0-2.6 5-4.2 5C5.5 10.5 4.5 9.4 4.5 8Z" stroke="#6C5CE7" stroke-width="1.4"/></svg>
<span style="font-size: 14px; font-weight: 500; color: #334155;">AI DISCOVERY · AEO / GEO for Clinics</span>
</div>
<div style="font-family: 'Playfair Display', 'Noto Sans KR', Georgia, serif; font-weight: 700; font-size: 72px; line-height: 1.1; letter-spacing: -0.02em; color: #0A1128; margin-bottom: 24px;">AI가 추천하는 병원은<br><span style="background: linear-gradient(to right, #0A1128, #3b5998); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; color: transparent;">따로 있습니다.</span></div>
<div style="font-size: 20px; line-height: 1.625; color: #475569; max-width: 768px; margin-bottom: 40px;">고객은 이제 ChatGPT와 Perplexity에게 병원을 묻습니다.<br>답변에 인용될 근거를 36개 항목으로 실측 진단하고, 8주 안에 구축합니다.</div>
<div style="display: flex; align-items: center; gap: 16px; margin-bottom: 72px;">
<div style="display: inline-flex; align-items: center; justify-content: center; gap: 8px; padding: 16px 40px; background: linear-gradient(to right, #4F1DA1, #021341); border-radius: 9999px; box-shadow: 0 20px 25px -5px rgba(0,0,0,0.1), 0 8px 10px -6px rgba(0,0,0,0.1);">
<span style="font-size: 18px; font-weight: 500; color: #ffffff;">무료 AEO / GEO 진단 받기</span>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none"><path d="M5 12h14M13 6l6 6-6 6" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
</div>
<div style="display: inline-flex; align-items: center; justify-content: center; padding: 16px 40px; background: rgba(255,255,255,0.8); border: 1px solid #e2e8f0; border-radius: 9999px;">
<span style="font-size: 18px; font-weight: 500; color: #0A1128;">샘플 리포트 보기</span>
</div>
</div>
<!-- ABMR loop + AI answer output -->
<div style="width: 100%; max-width: 1120px; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 24px; text-align: left;">
<!-- Loop card (dark) -->
<div style="position: relative; overflow: hidden; background: #0A1128; border-radius: 16px; padding: 28px 32px; display: flex; flex-direction: column; align-items: center; box-sizing: border-box;">
<div style="position: absolute; top: 45%; left: 50%; transform: translate(-50%,-50%); width: 420px; height: 420px; background: rgba(108,92,231,0.2); border-radius: 9999px; filter: blur(80px);"></div>
<div style="position: relative; align-self: flex-start; font-size: 13px; font-weight: 700; letter-spacing: 0.2em; color: #AF90FF; margin-bottom: 8px;">THE ENGINE · ABMR CYCLE</div>
<div style="position: relative; width: 300px; height: 300px; margin: 44px 0 60px;">
<svg viewBox="0 0 100 100" style="position: absolute; inset: 0; width: 100%; height: 100%; animation: if-spin 20s linear infinite;">
<defs>
<linearGradient id="loopGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#a78bfa" stop-opacity="0.15"></stop>
<stop offset="50%" stop-color="#c084fc" stop-opacity="0.6"></stop>
<stop offset="100%" stop-color="#e879f9" stop-opacity="0.15"></stop>
</linearGradient>
</defs>
<circle cx="50" cy="50" r="30" fill="none" stroke="url(#loopGrad)" stroke-width="0.5" stroke-dasharray="2 2"></circle>
</svg>
<div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); width: 148px; height: 148px; border-radius: 9999px; background: rgba(10,17,40,0.9); border: 1px solid rgba(255,255,255,0.05); box-shadow: 0 0 60px rgba(167,139,250,0.12); display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px; z-index: 2;">
<span style="font-size: 30px; font-weight: 700; background: linear-gradient(to right, #fff3eb, #e4cfff, #f5f9ff); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; color: transparent;">ABMR</span>
<span style="font-family: 'Playfair Display', Georgia, serif; font-weight: 700; font-size: 16px; color: #ffffff; text-align: center; line-height: 1.25;">Discovery<br>Loop</span>
</div>
<div style="position: absolute; top: 0; left: 50%; transform: translate(-50%,-50%); z-index: 3; display: flex; flex-direction: column; align-items: center; gap: 7px;">
<div style="width: 58px; height: 58px; border-radius: 9999px; border: 1px solid rgba(168,85,247,0.3); background: rgba(10,17,40,0.8); box-shadow: 0 0 20px rgba(168,85,247,0.15); display: flex; align-items: center; justify-content: center;"><span style="font-size: 25px; font-weight: 700; color: #d8b4fe;">A</span></div>
<span style="font-size: 14px; font-weight: 500; color: #e9d5ff;">Audit</span>
</div>
<div style="position: absolute; top: 50%; right: 0; transform: translate(50%,-50%); z-index: 3; display: flex; flex-direction: column; align-items: center; gap: 7px;">
<div style="width: 58px; height: 58px; border-radius: 9999px; border: 1px solid rgba(168,85,247,0.3); background: rgba(10,17,40,0.8); box-shadow: 0 0 20px rgba(168,85,247,0.15); display: flex; align-items: center; justify-content: center;"><span style="font-size: 25px; font-weight: 700; color: #d8b4fe;">B</span></div>
<span style="font-size: 14px; font-weight: 500; color: #e9d5ff;">Build</span>
</div>
<div style="position: absolute; bottom: 0; left: 50%; transform: translate(-50%,50%); z-index: 3; display: flex; flex-direction: column; align-items: center; gap: 7px;">
<div style="width: 58px; height: 58px; border-radius: 9999px; border: 1px solid rgba(168,85,247,0.3); background: rgba(10,17,40,0.8); box-shadow: 0 0 20px rgba(168,85,247,0.15); display: flex; align-items: center; justify-content: center;"><span style="font-size: 25px; font-weight: 700; color: #d8b4fe;">M</span></div>
<span style="font-size: 14px; font-weight: 500; color: #e9d5ff;">Measure</span>
</div>
<div style="position: absolute; top: 50%; left: 0; transform: translate(-50%,-50%); z-index: 3; display: flex; flex-direction: column; align-items: center; gap: 7px;">
<div style="width: 58px; height: 58px; border-radius: 9999px; border: 1px solid rgba(168,85,247,0.3); background: rgba(10,17,40,0.8); box-shadow: 0 0 20px rgba(168,85,247,0.15); display: flex; align-items: center; justify-content: center;"><span style="font-size: 25px; font-weight: 700; color: #d8b4fe;">R</span></div>
<span style="font-size: 14px; font-weight: 500; color: #e9d5ff;">Refine</span>
</div>
<svg viewBox="0 0 100 100" style="position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; z-index: 2;">
<defs><path id="citePath" d="M 10.6 56.9 A 40 40 0 0 0 43.1 89.4" fill="none"></path></defs>
<text font-size="3.5" font-weight="500" letter-spacing="0.6" fill="#d8b4fe" opacity="0.8"><textPath href="#citePath" startOffset="50%" text-anchor="middle">← CITATION FEEDBACK</textPath></text>
</svg>
</div>
<div style="position: relative; width: 100%; background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1); border-radius: 12px; padding: 14px 18px; box-sizing: border-box;">
<div style="font-size: 13.5px; line-height: 1.75; color: #cbd5e1; text-align: center;"><span style="font-weight: 700; color: #d8b4fe;">ABMR Cycle:</span> 실측 진단(Audit) → 스키마·콘텐츠 구축(Build) → 언급률·AI 유입 측정(Measure) → 우선순위 재조정(Refine)의 무한 루프. 사이클이 돌 때마다 <span style="color: #ffffff; font-weight: 500;">측정된 언급률이 다음 구축의 입력</span>이 됩니다.</div>
</div>
<div style="position: relative; display: flex; justify-content: center; gap: 10px; margin-top: 12px; width: 100%;">
<div style="padding: 6px 14px; background: rgba(175,144,255,0.12); border: 1px solid rgba(175,144,255,0.3); border-radius: 9999px;"><span style="font-size: 13px; font-weight: 600; color: #d8b4fe;">점수 48 → 61+</span></div>
<div style="padding: 6px 14px; background: rgba(175,144,255,0.12); border: 1px solid rgba(175,144,255,0.3); border-radius: 9999px;"><span style="font-size: 13px; font-weight: 600; color: #d8b4fe;">등급 C → B</span></div>
<div style="padding: 6px 14px; background: rgba(175,144,255,0.12); border: 1px solid rgba(175,144,255,0.3); border-radius: 9999px;"><span style="font-size: 13px; font-weight: 600; color: #d8b4fe;">언급률 상승 추적</span></div>
</div>
</div>
<!-- AI answer mock (loop output) -->
<div style="background: rgba(255,255,255,0.7); backdrop-filter: blur(24px); border: 1px solid rgba(255,255,255,0.4); box-shadow: 0 8px 32px rgba(31,38,135,0.07); border-radius: 16px; padding: 28px 32px; box-sizing: border-box; display: flex; flex-direction: column; justify-content: center;">
<div style="font-size: 13px; font-weight: 700; letter-spacing: 0.2em; color: #6C5CE7; margin-bottom: 20px;">THE OUTPUT · AI 답변 인용</div>
<div style="display: flex; align-items: center; gap: 10px; padding-bottom: 16px; border-bottom: 1px solid #eef0f7; margin-bottom: 16px;">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="9" cy="9" r="5.5" stroke="#64748b" stroke-width="1.5"></circle><path d="M13.5 13.5 17 17" stroke="#64748b" stroke-width="1.5" stroke-linecap="round"></path></svg>
<span style="font-size: 16px; font-weight: 500; color: #334155;">강남에서 안전하게 가슴성형 상담받을 수 있는 병원 추천해줘</span>
</div>
<div style="font-size: 15px; line-height: 1.8; color: #475569; margin-bottom: 16px;">모티바 공식 KOL 의료진이 있는 <span style="background: #F3F0FF; color: #4A3A7C; font-weight: 600; padding: 2px 6px; border-radius: 6px;">뷰성형외과</span>는 강남언니 후기 19,316건과 의료진 26명 정보가 공개되어 있으며, 시술별 FAQ와 비교표를 제공합니다.</div>
<div style="display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin-bottom: 24px;">
<div style="display: flex; align-items: center; gap: 6px; padding: 5px 12px; background: #EFF0FF; border: 1px solid #C5CBF5; border-radius: 9999px;"><span style="font-size: 12.5px; font-weight: 500; color: #3A3F7C;">viewclinic.com</span></div>
<div style="display: flex; align-items: center; gap: 6px; padding: 5px 12px; background: #EFF0FF; border: 1px solid #C5CBF5; border-radius: 9999px;"><span style="font-size: 12.5px; font-weight: 500; color: #3A3F7C;">gangnamunni.com</span></div>
<div style="display: flex; align-items: center; gap: 6px; padding: 5px 12px; background: #EFF0FF; border: 1px solid #C5CBF5; border-radius: 9999px;"><span style="font-size: 12.5px; font-weight: 500; color: #3A3F7C;">youtube.com</span></div>
<span style="font-size: 12.5px; color: #94a3b8; margin-left: 6px;">AEO / GEO 구축 후 목표 상태 예시</span>
</div>
<div style="border-top: 1px solid #eef0f7; padding-top: 18px; display: flex; flex-direction: column; gap: 10px;">
<div style="display: flex; align-items: center; gap: 10px;">
<span style="width: 7px; height: 7px; background: #9B8AD4; border-radius: 9999px;"></span>
<span style="font-size: 14px; color: #475569;">루프 1회전(8주)마다 36개 항목을 재측정해 점수를 갱신합니다.</span>
</div>
<div style="display: flex; align-items: center; gap: 10px;">
<span style="width: 7px; height: 7px; background: #9B8AD4; border-radius: 9999px;"></span>
<span style="font-size: 14px; color: #475569;">질의 10개 × 5회 스팟체크로 인용 여부를 신뢰구간과 함께 보고합니다.</span>
</div>
<div style="display: flex; align-items: center; gap: 10px;">
<span style="width: 7px; height: 7px; background: #9B8AD4; border-radius: 9999px;"></span>
<span style="font-size: 14px; color: #475569;">측정 결과가 다음 사이클의 구축 우선순위를 자동으로 정합니다.</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- ── Why now (dark) ──────────────────────────────────────────────── -->
<div style="background: #0A1128; display: flex; justify-content: center;">
<div style="width: 100%; max-width: 1120px; padding: 88px 24px; box-sizing: border-box;">
<div style="font-size: 14px; font-weight: 700; letter-spacing: 0.24em; color: #AF90FF; margin-bottom: 16px;">WHY NOW</div>
<div style="font-family: 'Playfair Display', 'Noto Sans KR', Georgia, serif; font-weight: 700; font-size: 40px; line-height: 1.25; color: #ffffff; margin-bottom: 12px;">검색은 끝났고, 답변이 시작됐습니다.</div>
<div style="font-size: 17px; line-height: 1.7; color: #94a3b8; margin-bottom: 56px;">답변엔진은 광고 입찰이 아니라 크롤링 가능한 오픈웹 근거로 병원을 고릅니다. 인용되지 못하면 비교 대상에도 오르지 못합니다.</div>
<div style="display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 32px;">
<div style="display: flex; flex-direction: column; gap: 10px; padding: 28px; background: #0F1B45; border: 1px solid #2A3A6B; border-radius: 16px;">
<div style="font-family: 'Playfair Display', Georgia, serif; font-weight: 700; font-size: 46px; color: #AF90FF;">12종</div>
<div style="font-size: 16px; font-weight: 600; color: #ffffff;">AI 크롤러가 이미 읽는 중</div>
<div style="font-size: 14.5px; line-height: 1.7; color: #94a3b8;">GPTBot·OAI-SearchBot·PerplexityBot 등이 오늘도 병원 사이트를 수집합니다. 문제는 접근이 아니라 인용할 내용입니다.</div>
</div>
<div style="display: flex; flex-direction: column; gap: 10px; padding: 28px; background: #0F1B45; border: 1px solid #2A3A6B; border-radius: 16px;">
<div style="font-family: 'Playfair Display', Georgia, serif; font-weight: 700; font-size: 46px; color: #AF90FF;">0건</div>
<div style="font-size: 16px; font-weight: 600; color: #ffffff;">네이버 자산의 AI 기여</div>
<div style="font-size: 14.5px; line-height: 1.7; color: #94a3b8;">네이버 블로그·플레이스는 robots.txt로 AI 크롤러를 전면 차단합니다(2026-08 실측). 지금까지의 네이버 마케팅은 AI 답변에 닿지 않습니다.</div>
</div>
<div style="display: flex; flex-direction: column; gap: 10px; padding: 28px; background: #0F1B45; border: 1px solid #2A3A6B; border-radius: 16px;">
<div style="font-family: 'Playfair Display', Georgia, serif; font-weight: 700; font-size: 46px; color: #AF90FF;">35~70%</div>
<div style="font-size: 16px; font-weight: 600; color: #ffffff;">Direct로 숨는 AI 유입</div>
<div style="font-size: 14.5px; line-height: 1.7; color: #94a3b8;">AI가 보낸 방문의 상당수는 유입 경로 없이 도착합니다. 측정 인프라 없이는 이미 오는 환자도 세지 못합니다.</div>
</div>
</div>
</div>
</div>
<!-- ── How it works ────────────────────────────────────────────────── -->
<div style="display: flex; justify-content: center; background: #f8fafc;">
<div style="width: 100%; max-width: 1120px; padding: 96px 24px; box-sizing: border-box;">
<div style="text-align: center; margin-bottom: 56px;">
<div style="font-size: 14px; font-weight: 700; letter-spacing: 0.24em; color: #6C5CE7; margin-bottom: 16px;">HOW IT WORKS</div>
<div style="font-family: 'Playfair Display', 'Noto Sans KR', Georgia, serif; font-weight: 700; font-size: 40px; line-height: 1.25; color: #0A1128;">진단하고, 구축하고, 숫자로 증명합니다.</div>
</div>
<div style="display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 24px;">
<div style="display: flex; flex-direction: column; gap: 14px; background: rgba(255,255,255,0.7); backdrop-filter: blur(24px); border: 1px solid rgba(255,255,255,0.4); box-shadow: 0 8px 32px rgba(31,38,135,0.07); border-radius: 16px; padding: 32px;">
<div style="display: flex; align-items: center; justify-content: center; width: 48px; height: 48px; background: #F3F0FF; border-radius: 12px;">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none"><circle cx="11" cy="11" r="6.5" stroke="#6C5CE7" stroke-width="1.6"/><path d="M16 16l4 4" stroke="#6C5CE7" stroke-width="1.6" stroke-linecap="round"/><path d="M8.5 11h5M11 8.5v5" stroke="#6C5CE7" stroke-width="1.6" stroke-linecap="round"/></svg>
</div>
<div style="display: flex; align-items: baseline; gap: 10px;">
<span style="font-size: 13px; font-weight: 700; letter-spacing: 0.12em; color: #6C5CE7;">STEP 01</span>
<span style="font-family: 'Playfair Display', Georgia, serif; font-weight: 700; font-size: 24px; color: #0A1128;">Audit</span>
</div>
<div style="font-size: 15.5px; line-height: 1.75; color: #475569;">AEO/GEO 기준표 v1.0으로 36개 항목을 실측합니다. 크롤러 접근성부터 인용 가능 콘텐츠, 측정 인프라까지 6개 카테고리 100점 척도. 항목마다 근거 등급을 명시합니다.</div>
</div>
<div style="display: flex; flex-direction: column; gap: 14px; background: rgba(255,255,255,0.7); backdrop-filter: blur(24px); border: 1px solid rgba(255,255,255,0.4); box-shadow: 0 8px 32px rgba(31,38,135,0.07); border-radius: 16px; padding: 32px;">
<div style="display: flex; align-items: center; justify-content: center; width: 48px; height: 48px; background: #F3F0FF; border-radius: 12px;">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none"><path d="M8 4H6a2 2 0 0 0-2 2v3c0 1-.6 2-1.6 2.4v1.2C3.4 13 4 14 4 15v3a2 2 0 0 0 2 2h2M16 4h2a2 2 0 0 1 2 2v3c0 1 .6 2 1.6 2.4v1.2C20.6 13 20 14 20 15v3a2 2 0 0 1-2 2h-2" stroke="#6C5CE7" stroke-width="1.6" stroke-linecap="round"/></svg>
</div>
<div style="display: flex; align-items: baseline; gap: 10px;">
<span style="font-size: 13px; font-weight: 700; letter-spacing: 0.12em; color: #6C5CE7;">STEP 02</span>
<span style="font-family: 'Playfair Display', Georgia, serif; font-weight: 700; font-size: 24px; color: #0A1128;">Build</span>
</div>
<div style="font-size: 15.5px; line-height: 1.75; color: #475569;">MedicalClinic·Physician 스키마를 심고, 흩어진 표면의 NAP을 한 글자까지 통일합니다. 핵심 페이지에 정의문과 시술별 FAQ를 넣어 AI가 잘라 갈 단락을 만듭니다.</div>
</div>
<div style="display: flex; flex-direction: column; gap: 14px; background: rgba(255,255,255,0.7); backdrop-filter: blur(24px); border: 1px solid rgba(255,255,255,0.4); box-shadow: 0 8px 32px rgba(31,38,135,0.07); border-radius: 16px; padding: 32px;">
<div style="display: flex; align-items: center; justify-content: center; width: 48px; height: 48px; background: #F3F0FF; border-radius: 12px;">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none"><path d="M4 20V10M10 20V4M16 20v-8M22 20H2" stroke="#6C5CE7" stroke-width="1.6" stroke-linecap="round"/></svg>
</div>
<div style="display: flex; align-items: baseline; gap: 10px;">
<span style="font-size: 13px; font-weight: 700; letter-spacing: 0.12em; color: #6C5CE7;">STEP 03</span>
<span style="font-family: 'Playfair Display', Georgia, serif; font-weight: 700; font-size: 24px; color: #0A1128;">Prove</span>
</div>
<div style="font-size: 15.5px; line-height: 1.75; color: #475569;">GA4 AI 채널그룹과 서버로그 크롤러 검증을 설치하고, 질의 10개 × 5회 반복 스팟체크로 변화를 측정합니다. 단발 조회가 아니라 언급률과 신뢰구간으로만 보고합니다.</div>
</div>
</div>
</div>
</div>
<!-- ── Sample report ───────────────────────────────────────────────── -->
<div style="display: flex; justify-content: center; background: #F4F6FB;">
<div style="width: 100%; max-width: 1120px; padding: 96px 24px; box-sizing: border-box;">
<div style="font-size: 14px; font-weight: 700; letter-spacing: 0.24em; color: #6C5CE7; margin-bottom: 16px;">SAMPLE REPORT · 뷰성형외과 실측</div>
<div style="font-family: 'Playfair Display', 'Noto Sans KR', Georgia, serif; font-weight: 700; font-size: 40px; line-height: 1.25; color: #0A1128; margin-bottom: 12px;">읽을 수는 있지만, 인용할 근거가 없습니다.</div>
<div style="font-size: 17px; line-height: 1.7; color: #64748b; margin-bottom: 48px;">실제 1차 진단 결과입니다. 어디가 강하고 어디가 비어 있는지, 점수는 카테고리 단위로 드러납니다.</div>
<div style="display: grid; grid-template-columns: 380px minmax(0, 1fr); gap: 24px;">
<!-- Score ring card -->
<div style="display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 16px; background: #ffffff; border: 1px solid #e2e8f0; border-radius: 16px; padding: 40px 32px; box-shadow: 0 8px 32px rgba(31,38,135,0.07);">
<div style="position: relative; width: 160px; height: 160px;">
<svg width="160" height="160" viewBox="0 0 160 160" style="transform: rotate(-90deg);">
<circle cx="80" cy="80" r="76" fill="none" stroke="#f1f5f9" stroke-width="8"></circle>
<circle cx="80" cy="80" r="76" fill="none" stroke="#8B9CF7" stroke-width="8" stroke-linecap="round" stroke-dasharray="477.5" stroke-dashoffset="248.3"></circle>
</svg>
<div style="position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px;">
<span style="font-size: 40px; font-weight: 700; color: #0A1128;">48</span>
<span style="font-size: 13px; color: #94a3b8;">/ 100</span>
</div>
</div>
<div style="display: flex; align-items: center; gap: 10px;">
<div style="display: flex; align-items: center; padding: 5px 14px; background: #FFF6ED; border: 1px solid #F5E0C5; border-radius: 9999px;"><span style="font-size: 14px; font-weight: 700; color: #7C5C3A;">등급 C</span></div>
<span style="font-size: 14px; color: #64748b;">AI Answer Readiness</span>
</div>
<div style="font-size: 13.5px; line-height: 1.6; color: #94a3b8; text-align: center;">36개 항목 중 31개 실측.<br>미검증 5개는 분모에서 제외했습니다.</div>
</div>
<!-- Category bars -->
<div style="display: flex; flex-direction: column; justify-content: center; gap: 18px; background: #ffffff; border: 1px solid #e2e8f0; border-radius: 16px; padding: 36px 40px; box-shadow: 0 8px 32px rgba(31,38,135,0.07);">
<div style="display: flex; flex-direction: column; gap: 7px;">
<div style="display: flex; justify-content: space-between; align-items: baseline;">
<span style="font-size: 15px; font-weight: 600; color: #0A1128;">A · AI 크롤러 접근성</span>
<span style="font-size: 15px; font-weight: 700; color: #6C5CE7;">92%</span>
</div>
<div style="height: 8px; background: #f1f5f9; border-radius: 9999px;"><div style="width: 92%; height: 8px; background: #6C5CE7; border-radius: 9999px;"></div></div>
</div>
<div style="display: flex; flex-direction: column; gap: 7px;">
<div style="display: flex; justify-content: space-between; align-items: baseline;">
<span style="font-size: 15px; font-weight: 600; color: #0A1128;">B · 엔티티 식별·구조화</span>
<span style="font-size: 15px; font-weight: 700; color: #C084CF;">30%</span>
</div>
<div style="height: 8px; background: #f1f5f9; border-radius: 9999px;"><div style="width: 30%; height: 8px; background: #C084CF; border-radius: 9999px;"></div></div>
</div>
<div style="display: flex; flex-direction: column; gap: 7px;">
<div style="display: flex; justify-content: space-between; align-items: baseline;">
<span style="font-size: 15px; font-weight: 600; color: #0A1128;">C · 답변 인용 가능 콘텐츠</span>
<span style="font-size: 15px; font-weight: 700; color: #C084CF;">20%</span>
</div>
<div style="height: 8px; background: #f1f5f9; border-radius: 9999px;"><div style="width: 20%; height: 8px; background: #C084CF; border-radius: 9999px;"></div></div>
</div>
<div style="display: flex; flex-direction: column; gap: 7px;">
<div style="display: flex; justify-content: space-between; align-items: baseline;">
<span style="font-size: 15px; font-weight: 600; color: #0A1128;">D · AI 인용 표면 확보</span>
<span style="font-size: 15px; font-weight: 700; color: #7C6DD8;">72%</span>
</div>
<div style="height: 8px; background: #f1f5f9; border-radius: 9999px;"><div style="width: 72%; height: 8px; background: #7C6DD8; border-radius: 9999px;"></div></div>
</div>
<div style="display: flex; flex-direction: column; gap: 7px;">
<div style="display: flex; justify-content: space-between; align-items: baseline;">
<span style="font-size: 15px; font-weight: 600; color: #0A1128;">E · AI 가시성 측정 인프라</span>
<span style="font-size: 15px; font-weight: 700; color: #C084CF;">0%</span>
</div>
<div style="height: 8px; background: #f1f5f9; border-radius: 9999px;"><div style="width: 2%; height: 8px; background: #C084CF; border-radius: 9999px;"></div></div>
</div>
<div style="display: flex; flex-direction: column; gap: 7px;">
<div style="display: flex; justify-content: space-between; align-items: baseline;">
<span style="font-size: 15px; font-weight: 600; color: #0A1128;">F · 기술 위생</span>
<span style="font-size: 15px; font-weight: 700; color: #C084CF;">33%</span>
</div>
<div style="height: 8px; background: #f1f5f9; border-radius: 9999px;"><div style="width: 33%; height: 8px; background: #C084CF; border-radius: 9999px;"></div></div>
</div>
</div>
</div>
<!-- Findings chips row -->
<div style="display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 24px; margin-top: 24px;">
<div style="display: flex; flex-direction: column; gap: 10px; background: #ffffff; border: 1px solid #e2e8f0; border-radius: 16px; padding: 24px 28px;">
<div style="display: inline-flex; align-self: flex-start; align-items: center; gap: 7px; padding: 4px 12px; background: #FFF0F0; border: 1px solid #F5D5DC; border-radius: 9999px;"><span style="width: 7px; height: 7px; background: #D4889A; border-radius: 9999px;"></span><span style="font-size: 13px; font-weight: 700; color: #7C3A4B;">치명</span></div>
<div style="font-size: 15.5px; font-weight: 600; color: #0A1128;">AI가 누구인지 확정 못 함</div>
<div style="font-size: 14px; line-height: 1.7; color: #64748b;">MedicalClinic 스키마 없음. 시술 목록 JSON-LD는 문법 오류로 통째로 버려집니다.</div>
</div>
<div style="display: flex; flex-direction: column; gap: 10px; background: #ffffff; border: 1px solid #e2e8f0; border-radius: 16px; padding: 24px 28px;">
<div style="display: inline-flex; align-self: flex-start; align-items: center; gap: 7px; padding: 4px 12px; background: #FFF6ED; border: 1px solid #F5E0C5; border-radius: 9999px;"><span style="width: 7px; height: 7px; background: #D4A872; border-radius: 9999px;"></span><span style="font-size: 13px; font-weight: 700; color: #7C5C3A;">보완</span></div>
<div style="font-size: 15.5px; font-weight: 600; color: #0A1128;">인용할 문장이 없음</div>
<div style="font-size: 14px; line-height: 1.7; color: #64748b;">정의문 0개, FAQ 0개, HTML 표 0개. 답변엔진이 잘라 갈 단락이 없습니다.</div>
</div>
<div style="display: flex; flex-direction: column; gap: 10px; background: #ffffff; border: 1px solid #e2e8f0; border-radius: 16px; padding: 24px 28px;">
<div style="display: inline-flex; align-self: flex-start; align-items: center; gap: 7px; padding: 4px 12px; background: #F3F0FF; border: 1px solid #D5CDF5; border-radius: 9999px;"><span style="width: 7px; height: 7px; background: #9B8AD4; border-radius: 9999px;"></span><span style="font-size: 13px; font-weight: 700; color: #4A3A7C;">강점</span></div>
<div style="font-size: 15.5px; font-weight: 600; color: #0A1128;">표면 자산은 이미 강함</div>
<div style="font-size: 14px; line-height: 1.7; color: #64748b;">강남언니 후기 19,316건, GBP 4.9점. 홈페이지가 따라가면 바로 인용 근거가 됩니다.</div>
</div>
</div>
</div>
</div>
<!-- ── Roadmap ─────────────────────────────────────────────────────── -->
<div style="display: flex; justify-content: center; background: #f8fafc;">
<div style="width: 100%; max-width: 1120px; padding: 96px 24px; box-sizing: border-box;">
<div style="text-align: center; margin-bottom: 64px;">
<div style="font-size: 14px; font-weight: 700; letter-spacing: 0.24em; color: #6C5CE7; margin-bottom: 16px;">8-WEEK ROADMAP</div>
<div style="font-family: 'Playfair Display', 'Noto Sans KR', Georgia, serif; font-weight: 700; font-size: 40px; line-height: 1.25; color: #0A1128;">8주 안에, C에서 B로 올립니다.</div>
</div>
<div style="position: relative;">
<div style="position: absolute; top: 11px; left: 8%; right: 8%; height: 2px; background: #D5CDF5;"></div>
<div style="display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 32px;">
<div style="display: flex; flex-direction: column; align-items: center; text-align: center; gap: 14px;">
<div style="position: relative; width: 24px; height: 24px; background: #6C5CE7; border: 5px solid #f8fafc; border-radius: 9999px; box-sizing: border-box;"></div>
<div style="font-size: 14px; font-weight: 700; letter-spacing: 0.1em; color: #6C5CE7;">W12</div>
<div style="font-size: 17px; font-weight: 700; color: #0A1128;">기반 설치</div>
<div style="font-size: 14.5px; line-height: 1.7; color: #64748b; max-width: 280px;">스키마·NAP·측정 인프라를 설치하고 기준선 스팟체크를 1회 측정합니다.</div>
</div>
<div style="display: flex; flex-direction: column; align-items: center; text-align: center; gap: 14px;">
<div style="position: relative; width: 24px; height: 24px; background: #6C5CE7; border: 5px solid #f8fafc; border-radius: 9999px; box-sizing: border-box;"></div>
<div style="font-size: 14px; font-weight: 700; letter-spacing: 0.1em; color: #6C5CE7;">W36</div>
<div style="font-size: 17px; font-weight: 700; color: #0A1128;">콘텐츠 구조</div>
<div style="font-size: 14.5px; line-height: 1.7; color: #64748b; max-width: 280px;">정의문·FAQ·H1 재설계와 HTML 비교표를 구축하고 WordPress를 업그레이드합니다.</div>
</div>
<div style="display: flex; flex-direction: column; align-items: center; text-align: center; gap: 14px;">
<div style="position: relative; width: 24px; height: 24px; background: #6C5CE7; border: 5px solid #f8fafc; border-radius: 9999px; box-sizing: border-box;"></div>
<div style="font-size: 14px; font-weight: 700; letter-spacing: 0.1em; color: #6C5CE7;">W78</div>
<div style="font-size: 17px; font-weight: 700; color: #0A1128;">재진단·보고</div>
<div style="font-size: 14.5px; line-height: 1.7; color: #64748b; max-width: 280px;">36개 항목을 재측정하고 미검증 5개를 반영해 총점을 다시 산정합니다.</div>
</div>
</div>
</div>
</div>
</div>
<!-- ── Honesty ─────────────────────────────────────────────────────── -->
<div style="display: flex; justify-content: center; background: #ffffff; border-top: 1px solid #eef0f7; border-bottom: 1px solid #eef0f7;">
<div style="width: 100%; max-width: 880px; padding: 72px 24px; box-sizing: border-box; text-align: center;">
<div style="font-family: 'Playfair Display', 'Noto Sans KR', Georgia, serif; font-style: italic; font-weight: 600; font-size: 27px; line-height: 1.5; color: #0A1128; margin-bottom: 28px;">"스키마를 넣으면 인용된다고 약속하지 않습니다.<br>인용될 수 없는 이유를 전부 제거하고, 결과를 반복 측정으로 증명합니다."</div>
<div style="display: flex; justify-content: center; gap: 12px;">
<div style="padding: 8px 18px; background: #F4F6FB; border: 1px solid #e2e8f0; border-radius: 9999px;"><span style="font-size: 14px; font-weight: 500; color: #475569;">효과 없는 llms.txt는 팔지 않습니다</span></div>
<div style="padding: 8px 18px; background: #F4F6FB; border: 1px solid #e2e8f0; border-radius: 9999px;"><span style="font-size: 14px; font-weight: 500; color: #475569;">순위·노출을 보장하지 않습니다</span></div>
<div style="padding: 8px 18px; background: #F4F6FB; border: 1px solid #e2e8f0; border-radius: 9999px;"><span style="font-size: 14px; font-weight: 500; color: #475569;">단발 측정으로 보고하지 않습니다</span></div>
</div>
</div>
</div>
<!-- ── Final CTA ───────────────────────────────────────────────────── -->
<div style="display: flex; justify-content: center; background: #0A1128;">
<div style="width: 100%; max-width: 1120px; padding: 96px 24px; box-sizing: border-box; display: flex; flex-direction: column; align-items: center; text-align: center;">
<div style="font-family: 'Playfair Display', 'Noto Sans KR', Georgia, serif; font-weight: 700; font-size: 44px; line-height: 1.25; color: #ffffff; margin-bottom: 18px;">AI가 당신의 병원을 인용하게 하세요.</div>
<div style="font-size: 18px; line-height: 1.7; color: #94a3b8; margin-bottom: 40px;">진단은 무료입니다. 36개 항목 실측 리포트를 받아보고 결정하세요.</div>
<div style="display: flex; align-items: center; gap: 14px;">
<div style="display: flex; align-items: center; height: 52px; padding: 0 30px; background: #ffffff; border-radius: 9999px;">
<span style="font-size: 16px; font-weight: 700; color: #0A1128;">무료 AEO / GEO 진단 받기</span>
</div>
<span style="font-size: 15px; color: #64748b;">o2oteam@o2o.kr</span>
</div>
</div>
</div>
<!-- ── Footer ──────────────────────────────────────────────────────── -->
<div style="display: flex; justify-content: center; background: #ffffff; border-top: 1px solid #f1f5f9;">
<div style="width: 100%; max-width: 1120px; padding: 48px 24px; box-sizing: border-box; display: flex; align-items: center; justify-content: space-between;">
<span style="font-family: 'Playfair Display', Georgia, serif; font-weight: 900; font-size: 30px; letter-spacing: 0.05em; background: linear-gradient(to right, #4F1DA1, #021341); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; color: transparent;">INFINITH</span>
<span style="font-size: 14px; line-height: 1.7; color: #64748b; text-align: right;">© 2026 INFINITH. All rights reserved.<br>Infinite Marketing for Premium Medical Business &amp; Marketing Agency</span>
</div>
</div>
</div>
</body>
</html>

View File

@ -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([
("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)

Binary file not shown.

After

Width:  |  Height:  |  Size: 701 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

View File

@ -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 (
<motion.div
initial={{ opacity: 0, y: 24 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.1 }}
className="relative overflow-hidden bg-[#0A1128] rounded-2xl px-8 py-7 flex flex-col items-center text-left"
>
{/* Background glow */}
<div className="absolute top-[45%] left-1/2 -translate-x-1/2 -translate-y-1/2 w-[420px] h-[420px] bg-accent/20 rounded-full blur-[80px] pointer-events-none" />
<div className="relative self-start text-[13px] font-bold tracking-[0.2em] text-[#AF90FF] mb-2">
THE ENGINE · ABMR CYCLE
</div>
{/* Circular loop diagram (Solution.tsx 구조, ABMR 워딩) */}
<div className="relative w-[240px] h-[240px] md:w-[300px] md:h-[300px] my-11 mb-15">
{/* Animated glowing ring */}
<svg className="absolute inset-0 w-full h-full animate-[spin_20s_linear_infinite]" viewBox="0 0 100 100">
<defs>
<linearGradient id="abmrRingGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stopColor="#a78bfa" stopOpacity="0.15" />
<stop offset="50%" stopColor="#c084fc" stopOpacity="0.6" />
<stop offset="100%" stopColor="#e879f9" stopOpacity="0.15" />
</linearGradient>
</defs>
<circle cx="50" cy="50" r="30" fill="none" stroke="url(#abmrRingGrad)" strokeWidth="0.5" strokeDasharray="2 2" />
</svg>
{/* Center orb */}
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-[2] w-[120px] h-[120px] md:w-[148px] md:h-[148px] rounded-full bg-[#0A1128]/90 border border-white/5 shadow-[0_0_60px_rgba(167,139,250,0.12)] flex flex-col items-center justify-center gap-0.5">
<span className="text-2xl md:text-3xl font-bold bg-gradient-to-r from-[#fff3eb] via-[#e4cfff] to-[#f5f9ff] bg-clip-text text-transparent">
ABMR
</span>
<span className="font-serif font-bold text-sm md:text-base text-white text-center leading-tight">
Discovery<br />Loop
</span>
</div>
{/* Nodes */}
{NODES.map((node) => (
<div key={node.letter} className={`absolute ${node.pos} z-[3] flex flex-col items-center gap-[7px]`}>
<div className="w-12 h-12 md:w-[58px] md:h-[58px] rounded-full border border-purple-500/30 bg-[#0A1128]/80 shadow-[0_0_20px_rgba(168,85,247,0.15)] flex items-center justify-center">
<span className="text-xl md:text-[25px] font-bold text-purple-300">{node.letter}</span>
</div>
<span className="text-sm font-medium text-purple-100">{node.label}</span>
</div>
))}
{/* Citation feedback curved text (M → A) */}
<svg className="absolute inset-0 w-full h-full pointer-events-none z-[2]" viewBox="0 0 100 100">
<defs>
<path id="citePath" d="M 10.6 56.9 A 40 40 0 0 0 43.1 89.4" fill="none" />
</defs>
<text fontSize="3.5" className="font-medium uppercase tracking-widest" fill="#d8b4fe" opacity="0.8">
<textPath href="#citePath" startOffset="50%" textAnchor="middle">
CITATION FEEDBACK
</textPath>
</text>
</svg>
</div>
{/* Cycle description */}
<div className="relative w-full bg-white/5 border border-white/10 rounded-xl px-[18px] py-3.5">
<p className="text-[13.5px] leading-[1.75] text-slate-300 text-center break-keep">
<span className="font-bold text-purple-300">ABMR Cycle:</span> (Audit) ·
(Build) ·AI (Measure) (Refine) .
<span className="text-white font-medium"> </span>
.
</p>
</div>
{/* Outcome chips — 계산값 파생 */}
<div className="relative flex flex-wrap justify-center gap-2.5 mt-3 w-full">
{chips.map((chip) => (
<div key={chip} className="px-3.5 py-1.5 bg-[#AF90FF]/12 border border-[#AF90FF]/30 rounded-full">
<span className="text-[13px] font-semibold text-purple-300">{chip}</span>
</div>
))}
</div>
</motion.div>
);
}

View File

@ -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 (
<motion.div
initial={{ opacity: 0, y: 24 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.2 }}
className="glass-card px-8 py-7 flex flex-col justify-center text-left"
>
<div className="text-[13px] font-bold tracking-[0.2em] text-accent mb-5">
THE OUTPUT · AI
</div>
{/* 질의 */}
<div className="flex items-center gap-2.5 pb-4 border-b border-[#eef0f7] mb-4">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" aria-hidden="true">
<circle cx="9" cy="9" r="5.5" stroke="#64748b" strokeWidth="1.5" />
<path d="M13.5 13.5 17 17" stroke="#64748b" strokeWidth="1.5" strokeLinecap="round" />
</svg>
<span className="text-base font-medium text-slate-700 break-keep">
</span>
</div>
{/* 목표 상태의 답변 예시 */}
<p className="text-[15px] leading-[1.8] text-slate-600 mb-4 break-keep">
KOL {' '}
<span className="bg-[#F3F0FF] text-[#4A3A7C] font-semibold px-1.5 py-0.5 rounded-md">
{clinicName}
</span>
19,316 26 , FAQ와
.
</p>
{/* 인용 도메인 칩 + 목표 상태 라벨 (제거 금지) */}
<div className="flex items-center flex-wrap gap-2 mb-6">
{CITED_DOMAINS.map((domain) => (
<div key={domain} className="px-3 py-[5px] bg-[#EFF0FF] border border-[#C5CBF5] rounded-full">
<span className="text-[12.5px] font-medium text-[#3A3F7C]">{domain}</span>
</div>
))}
<span className="text-[12.5px] text-slate-400 ml-1.5">AEO / GEO </span>
</div>
{/* 측정 프로토콜 */}
<div className="border-t border-[#eef0f7] pt-[18px] flex flex-col gap-2.5">
{protocolPoints.map((point) => (
<div key={point} className="flex items-center gap-2.5">
<span className="w-[7px] h-[7px] shrink-0 bg-[#9B8AD4] rounded-full" />
<span className="text-sm text-slate-600 break-keep">{point}</span>
</div>
))}
</div>
</motion.div>
);
}

View File

@ -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 (
<section className="bg-[#0A1128] px-6 py-24">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
className="max-w-7xl mx-auto flex flex-col items-center text-center"
>
<h2 className="font-serif text-3xl md:text-[44px] font-bold leading-tight text-white mb-[18px] break-keep">
AI가 .
</h2>
<p className="text-lg leading-[1.7] text-slate-400 mb-10 break-keep">
. {criteriaCount} .
</p>
<div className="flex flex-col sm:flex-row items-center gap-3.5">
<a
href={buildDiscoveryMailto('무료 진단 신청')}
className="inline-flex items-center h-[52px] px-[30px] bg-white rounded-full text-base font-bold text-primary-900 hover:bg-slate-100 transition-colors"
>
AEO / GEO
</a>
<a
href={buildDiscoveryMailto('무료 진단 신청')}
className="text-[15px] text-slate-500 hover:text-slate-300 transition-colors"
>
{DISCOVERY_CONTACT_EMAIL}
</a>
</div>
</motion.div>
</section>
);
}

View File

@ -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 (
<section className="relative overflow-hidden bg-[radial-gradient(ellipse_at_top,#e0e7ff_0%,#faf5ff_45%,#fdf2f8_100%)]">
{/* Decorative blobs */}
<div className="absolute top-[120px] left-[60px] w-64 h-64 bg-purple-300 rounded-full blur-3xl opacity-30 pointer-events-none" />
<div className="absolute top-[180px] right-[60px] w-64 h-64 bg-pink-300 rounded-full blur-3xl opacity-30 pointer-events-none" />
<div className="relative flex flex-col items-center text-center px-6 pt-32 md:pt-46 pb-16 md:pb-22">
{/* Badge */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-white/60 backdrop-blur-sm border border-white/40 shadow-sm mb-8"
>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<path
d="M4.5 8c0-1.4 1-2.5 2.2-2.5 1.6 0 2.6 5 4.2 5 1.2 0 2.2-1.1 2.2-2.5s-1-2.5-2.2-2.5c-1.6 0-2.6 5-4.2 5C5.5 10.5 4.5 9.4 4.5 8Z"
stroke="#6C5CE7"
strokeWidth="1.4"
/>
</svg>
<span className="text-sm font-medium text-slate-700">AI DISCOVERY · AEO / GEO for Clinics</span>
</motion.div>
{/* Headline */}
<motion.h1
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.1 }}
className="font-serif text-5xl md:text-7xl font-bold text-primary-900 leading-[1.1] tracking-[-0.02em] mb-6 break-keep"
>
AI가
<br />
<span className="text-gradient"> .</span>
</motion.h1>
{/* Sub copy */}
<motion.p
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.2 }}
className="text-lg md:text-xl text-slate-600 leading-relaxed max-w-3xl mb-10 break-keep"
>
ChatGPT와 Perplexity에게 .
<br className="hidden md:block" /> {criteriaCount}
, 8 .
</motion.p>
{/* CTAs */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.3 }}
className="flex flex-col sm:flex-row items-center gap-4 mb-18"
>
<a
href={buildDiscoveryMailto('무료 진단 신청')}
className="inline-flex items-center justify-center gap-2 px-10 py-4 text-lg font-medium text-white rounded-full shadow-xl bg-gradient-to-r from-[#4F1DA1] to-[#021341] hover:opacity-90 transition-opacity"
>
AEO / GEO
<ArrowRight className="w-5 h-5" />
</a>
<Link
to={sampleReportPath}
className="inline-flex items-center justify-center px-10 py-4 text-lg font-medium text-primary-900 bg-white/80 border border-slate-200 rounded-full hover:bg-white transition-colors"
>
</Link>
</motion.div>
{/* ABMR loop + AI answer mock */}
<div className="w-full max-w-7xl grid md:grid-cols-2 gap-6">
<AbmrLoop overall={overall} target={target} />
<AiAnswerMock clinicName={clinicName} criteriaCount={criteriaCount} />
</div>
</div>
</section>
);
}

View File

@ -0,0 +1,38 @@
/**
* + 3 ( ).
* · , "언급률과 신뢰구간으로 보고" .
*/
import { motion } from 'motion/react';
const PILLS = [
'효과 없는 llms.txt는 팔지 않습니다',
'순위·노출을 보장하지 않습니다',
'단발 측정으로 보고하지 않습니다',
];
export default function HonestyStrip() {
return (
<section className="bg-white border-y border-[#eef0f7] px-6 py-18">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
className="max-w-[880px] mx-auto text-center"
>
<blockquote className="font-serif italic font-semibold text-xl md:text-[27px] leading-normal text-primary-900 mb-7 break-keep">
" .
<br className="hidden md:block" /> ,
."
</blockquote>
<div className="flex flex-wrap justify-center gap-3">
{PILLS.map((pill) => (
<div key={pill} className="px-[18px] py-2 bg-[#F4F6FB] border border-slate-200 rounded-full">
<span className="text-sm font-medium text-slate-600 break-keep">{pill}</span>
</div>
))}
</div>
</motion.div>
</section>
);
}

View File

@ -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: (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<circle cx="11" cy="11" r="6.5" stroke="#6C5CE7" strokeWidth="1.6" />
<path d="M16 16l4 4" stroke="#6C5CE7" strokeWidth="1.6" strokeLinecap="round" />
<path d="M8.5 11h5M11 8.5v5" stroke="#6C5CE7" strokeWidth="1.6" strokeLinecap="round" />
</svg>
),
},
{
step: 'STEP 02',
title: 'Build',
detail:
'MedicalClinic·Physician 스키마를 심고, 흩어진 표면의 NAP을 한 글자까지 통일합니다. 핵심 페이지에 정의문과 시술별 FAQ를 넣어 AI가 잘라 갈 단락을 만듭니다.',
icon: (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path
d="M8 4H6a2 2 0 0 0-2 2v3c0 1-.6 2-1.6 2.4v1.2C3.4 13 4 14 4 15v3a2 2 0 0 0 2 2h2M16 4h2a2 2 0 0 1 2 2v3c0 1 .6 2 1.6 2.4v1.2C20.6 13 20 14 20 15v3a2 2 0 0 1-2 2h-2"
stroke="#6C5CE7"
strokeWidth="1.6"
strokeLinecap="round"
/>
</svg>
),
},
{
step: 'STEP 03',
title: 'Prove',
detail:
'GA4 AI 채널그룹과 서버로그 크롤러 검증을 설치하고, 질의 10개 × 5회 반복 스팟체크로 변화를 측정합니다. 단발 조회가 아니라 언급률과 신뢰구간으로만 보고합니다.',
icon: (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M4 20V10M10 20V4M16 20v-8M22 20H2" stroke="#6C5CE7" strokeWidth="1.6" strokeLinecap="round" />
</svg>
),
},
];
return (
<section id="how-it-works" className="bg-slate-50 px-6 py-24">
<div className="max-w-7xl mx-auto">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
className="text-center mb-14"
>
<div className="text-sm font-bold tracking-[0.24em] text-accent mb-4">HOW IT WORKS</div>
<h2 className="font-serif text-3xl md:text-[40px] font-bold leading-tight text-primary-900 break-keep">
, , .
</h2>
</motion.div>
<div className="grid md:grid-cols-3 gap-6">
{steps.map((item, index) => (
<motion.div
key={item.title}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: index * 0.1 }}
className="glass-card flex flex-col gap-3.5 p-8"
>
<div className="flex items-center justify-center w-12 h-12 bg-[#F3F0FF] rounded-xl">
{item.icon}
</div>
<div className="flex items-baseline gap-2.5">
<span className="text-[13px] font-bold tracking-[0.12em] text-accent">{item.step}</span>
<span className="font-serif text-2xl font-bold text-primary-900">{item.title}</span>
</div>
<p className="text-[15.5px] leading-[1.75] text-slate-600 break-keep">{item.detail}</p>
</motion.div>
))}
</div>
</div>
</section>
);
}

View File

@ -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: 'W12',
title: '기반 설치',
detail: '스키마·NAP·측정 인프라를 설치하고 기준선 스팟체크를 1회 측정합니다.',
},
{
week: 'W36',
title: '콘텐츠 구조',
detail: '정의문·FAQ·H1 재설계와 HTML 비교표를 구축하고 WordPress를 업그레이드합니다.',
},
{
week: 'W78',
title: '재진단·보고',
detail: `${criteriaCount}개 항목을 재측정하고 미검증 ${unverifiedCount}개를 반영해 총점을 다시 산정합니다.`,
},
];
return (
<section className="bg-slate-50 px-6 py-24">
<div className="max-w-7xl mx-auto">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
className="text-center mb-16"
>
<div className="text-sm font-bold tracking-[0.24em] text-accent mb-4">8-WEEK ROADMAP</div>
<h2 className="font-serif text-3xl md:text-[40px] font-bold leading-tight text-primary-900 break-keep">
{targetGrade
? `8주 안에, ${grade}에서 ${targetGrade}로 올립니다.`
: `8주마다, ${grade} 등급을 재검증합니다.`}
</h2>
</motion.div>
<div className="relative">
{/* Connecting line (desktop only) */}
<div className="hidden md:block absolute top-[11px] left-[8%] right-[8%] h-0.5 bg-[#D5CDF5]" />
<div className="grid md:grid-cols-3 gap-10 md:gap-8">
{phases.map((phase, index) => (
<motion.div
key={phase.week}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: index * 0.1 }}
className="flex flex-col items-center text-center gap-3.5"
>
<div className="relative w-6 h-6 bg-accent border-[5px] border-slate-50 rounded-full box-border" />
<div className="text-sm font-bold tracking-[0.1em] text-accent">{phase.week}</div>
<div className="text-[17px] font-bold text-primary-900">{phase.title}</div>
<p className="text-[14.5px] leading-[1.7] text-slate-500 max-w-[280px] break-keep">
{phase.detail}
</p>
</motion.div>
))}
</div>
</div>
</div>
</section>
);
}

View File

@ -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<OverallScore['grade'], string> = {
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 (
<section id="sample-report" className="bg-[#F4F6FB] px-6 py-24">
<div className="max-w-7xl mx-auto">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
>
<div className="text-sm font-bold tracking-[0.24em] text-accent mb-4">
SAMPLE REPORT · {result.clinicName}
</div>
<h2 className="font-serif text-3xl md:text-[40px] font-bold leading-tight text-primary-900 mb-3 break-keep">
, .
</h2>
<p className="text-[17px] leading-[1.7] text-slate-500 mb-12 break-keep">
1 . ,
.
</p>
</motion.div>
<div className="grid lg:grid-cols-[380px_minmax(0,1fr)] gap-6">
{/* Score ring card */}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.1 }}
className="flex flex-col items-center justify-center gap-4 bg-white border border-slate-200 rounded-2xl px-8 py-10 shadow-[0_8px_32px_rgba(31,38,135,0.07)]"
>
<ScoreRing score={overall.score} size={160} valueClassName="text-[40px]" subLabel="/ 100" />
<div className="flex items-center gap-2.5">
<div className={`px-3.5 py-[5px] border rounded-full ${GRADE_BADGE_STYLE[overall.grade]}`}>
<span className="text-sm font-bold"> {overall.grade}</span>
</div>
<span className="text-sm text-slate-500">AI Answer Readiness</span>
</div>
<p className="text-[13.5px] leading-relaxed text-slate-400 text-center break-keep">
{totalCriteria} {verifiedCriteria} .
<br />
{unverifiedCriteria} .
</p>
</motion.div>
{/* Category bars */}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.2 }}
className="flex flex-col justify-center gap-[18px] bg-white border border-slate-200 rounded-2xl px-6 md:px-10 py-9 shadow-[0_8px_32px_rgba(31,38,135,0.07)]"
>
{overall.categories.map((cat) => {
const color = scorePctColor(cat.pct);
return (
<div key={cat.category.id} className="flex flex-col gap-[7px]">
<div className="flex justify-between items-baseline gap-3">
<span className="text-[15px] font-semibold text-primary-900 break-keep">
{cat.category.code} · {cat.category.name}
</span>
<span className="text-[15px] font-bold" style={{ color }}>
{cat.pct}%
</span>
</div>
<div className="h-2 bg-slate-100 rounded-full overflow-hidden">
<motion.div
className="h-2 rounded-full"
style={{ backgroundColor: color }}
initial={{ width: 0 }}
whileInView={{ width: `${Math.max(cat.pct, 2)}%` }}
viewport={{ once: true }}
transition={{ duration: 0.8, ease: 'easeOut' }}
/>
</div>
</div>
);
})}
</motion.div>
</div>
{/* Finding chips */}
<div className="grid md:grid-cols-3 gap-6 mt-6">
{FINDING_CHIPS.map((finding, index) => (
<motion.div
key={finding.title}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: index * 0.1 }}
className="flex flex-col gap-2.5 bg-white border border-slate-200 rounded-2xl px-7 py-6"
>
<div
className={`inline-flex self-start items-center gap-[7px] px-3 py-1 border rounded-full ${finding.chipClass}`}
>
<span className="w-[7px] h-[7px] rounded-full" style={{ backgroundColor: finding.dotColor }} />
<span className="text-[13px] font-bold">{finding.label}</span>
</div>
<div className="text-[15.5px] font-semibold text-primary-900 break-keep">{finding.title}</div>
<p className="text-sm leading-[1.7] text-slate-500 break-keep">{finding.detail}</p>
</motion.div>
))}
</div>
</div>
</section>
);
}

View File

@ -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 (
<section id="why-now" className="bg-[#0A1128] px-6 py-22">
<div className="max-w-7xl mx-auto">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
>
<div className="text-sm font-bold tracking-[0.24em] text-[#AF90FF] mb-4">WHY NOW</div>
<h2 className="font-serif text-3xl md:text-[40px] font-bold leading-tight text-white mb-3 break-keep">
, .
</h2>
<p className="text-[17px] leading-[1.7] text-slate-400 mb-14 break-keep">
.
.
</p>
</motion.div>
<div className="grid md:grid-cols-3 gap-8">
{STATS.map((stat, index) => (
<motion.div
key={stat.figure}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: index * 0.1 }}
className="flex flex-col gap-2.5 p-7 bg-[#0F1B45] border border-[#2A3A6B] rounded-2xl"
>
<div className="font-serif text-[46px] font-bold text-[#AF90FF] leading-tight">
{stat.figure}
</div>
<div className="text-base font-semibold text-white break-keep">{stat.title}</div>
<p className="text-[14.5px] leading-[1.7] text-slate-400 break-keep">{stat.detail}</p>
</motion.div>
))}
</div>
</div>
</section>
);
}

View File

@ -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}`)}`;

View File

@ -6,6 +6,10 @@ interface ScoreRingProps {
size?: number; size?: number;
label?: string; label?: string;
color?: string; color?: string;
/** 링 중앙 숫자 타이포 오버라이드 (기본 text-2xl) */
valueClassName?: string;
/** 링 중앙 숫자 아래 보조 표기 (예: "/ 100") */
subLabel?: string;
} }
function getScoreColor(score: number, maxScore: number): string { function getScoreColor(score: number, maxScore: number): string {
@ -22,6 +26,8 @@ export function ScoreRing({
size = 120, size = 120,
label, label,
color, color,
valueClassName = 'text-2xl',
subLabel,
}: ScoreRingProps) { }: ScoreRingProps) {
const strokeWidth = 8; const strokeWidth = 8;
const radius = (size - strokeWidth) / 2; const radius = (size - strokeWidth) / 2;
@ -61,8 +67,9 @@ export function ScoreRing({
transition={{ duration: 1, ease: 'easeOut' }} transition={{ duration: 1, ease: 'easeOut' }}
/> />
</svg> </svg>
<div className="absolute inset-0 flex items-center justify-center"> <div className="absolute inset-0 flex flex-col items-center justify-center">
<span className="text-2xl font-bold text-[#0A1128]">{score}</span> <span className={`${valueClassName} font-bold text-[#0A1128]`}>{score}</span>
{subLabel && <span className="text-[13px] text-slate-400">{subLabel}</span>}
</div> </div>
</div> </div>
{label && ( {label && (

View File

@ -30,6 +30,23 @@ export const gradeFromScore = (score: number): OverallScore['grade'] => {
return 'D'; 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 { export function scoreDiscovery(rubric: Rubric, result: DiscoveryResult): OverallScore {
const byId = new Map<string, CriterionResult>(result.results.map((r) => [r.criterionId, r])); const byId = new Map<string, CriterionResult>(result.results.map((r) => [r.criterionId, r]));

View File

@ -15,6 +15,7 @@ import DataValidationPage from './pages/DataValidationPage.tsx';
import ClinicProfilePage from './pages/ClinicProfilePage.tsx'; import ClinicProfilePage from './pages/ClinicProfilePage.tsx';
import ApiDashboardPage from './pages/ApiDashboardPage.tsx'; import ApiDashboardPage from './pages/ApiDashboardPage.tsx';
import LoginPage from './pages/LoginPage.tsx'; import LoginPage from './pages/LoginPage.tsx';
import DiscoveryLandingPage from './pages/DiscoveryLandingPage.tsx';
import DiscoveryReportPage from './pages/DiscoveryReportPage.tsx'; import DiscoveryReportPage from './pages/DiscoveryReportPage.tsx';
import './index.css'; import './index.css';
@ -37,6 +38,7 @@ createRoot(document.getElementById('root')!).render(
<Route path="data-validation" element={<DataValidationPage />} /> <Route path="data-validation" element={<DataValidationPage />} />
<Route path="clinic/:id" element={<ClinicProfilePage />} /> <Route path="clinic/:id" element={<ClinicProfilePage />} />
<Route path="api-dashboard" element={<ApiDashboardPage />} /> <Route path="api-dashboard" element={<ApiDashboardPage />} />
<Route path="discovery" element={<DiscoveryLandingPage />} />
<Route path="discovery/:id" element={<DiscoveryReportPage />} /> <Route path="discovery/:id" element={<DiscoveryReportPage />} />
</Route> </Route>
</Routes> </Routes>

View File

@ -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 (
<div className="[word-break:keep-all]">
<DiscoveryHero
overall={overall}
target={target}
clinicName={result.clinicName}
sampleReportPath={`/discovery/${result.id}`}
criteriaCount={criteriaCount}
/>
<WhyNow />
<HowItWorks criteriaCount={criteriaCount} categoryCount={categoryCount} />
<SampleReport overall={overall} result={result} />
<Roadmap
grade={overall.grade}
targetGrade={target?.grade ?? null}
criteriaCount={criteriaCount}
unverifiedCount={unverifiedCount}
/>
<HonestyStrip />
<DiscoveryCta criteriaCount={criteriaCount} />
</div>
);
}