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:
parent
85d72e4cfb
commit
18740ca800
94
docs/AI_DISCOVERY_FRONTEND_HANDOVER.md
Normal file
94
docs/AI_DISCOVERY_FRONTEND_HANDOVER.md
Normal 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) 하나뿐인
|
||||
단일 파일이다. 랜딩을 수정하면 스크린샷 재캡처 → 덱 리빌드 순서로 갱신한다
|
||||
(스크린샷 크롭 좌표는 배경색 감지로 잡았다: 히어로 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) 케이스를 두 번째 샘플로 노출할지 결정.
|
||||
- 덱 스크린샷 자동 갱신 스크립트화 (랜딩 변경 → 캡처 → 크롭 → 덱 리빌드).
|
||||
BIN
docs/reports/Viewclinic AI Discovery_POC_Proposal.pdf
Normal file
BIN
docs/reports/Viewclinic AI Discovery_POC_Proposal.pdf
Normal file
Binary file not shown.
BIN
docs/reports/Viewclinic_AI Discovery_POC_Proposal.pptx
Normal file
BIN
docs/reports/Viewclinic_AI Discovery_POC_Proposal.pptx
Normal file
Binary file not shown.
401
docs/reports/ai_discovery_landing.html
Normal file
401
docs/reports/ai_discovery_landing.html
Normal 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;">W1–2</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;">W3–6</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;">W7–8</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 & Marketing Agency</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
210
docs/reports/build_viewclinic_poc_deck.py
Normal file
210
docs/reports/build_viewclinic_poc_deck.py
Normal 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([
|
||||
("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)
|
||||
BIN
docs/reports/shots/land_hero.png
Normal file
BIN
docs/reports/shots/land_hero.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 701 KiB |
BIN
docs/reports/shots/land_report.png
Normal file
BIN
docs/reports/shots/land_report.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 108 KiB |
112
src/components/discovery/AbmrLoop.tsx
Normal file
112
src/components/discovery/AbmrLoop.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
77
src/components/discovery/AiAnswerMock.tsx
Normal file
77
src/components/discovery/AiAnswerMock.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
44
src/components/discovery/DiscoveryCta.tsx
Normal file
44
src/components/discovery/DiscoveryCta.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
107
src/components/discovery/DiscoveryHero.tsx
Normal file
107
src/components/discovery/DiscoveryHero.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
38
src/components/discovery/HonestyStrip.tsx
Normal file
38
src/components/discovery/HonestyStrip.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
103
src/components/discovery/HowItWorks.tsx
Normal file
103
src/components/discovery/HowItWorks.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
77
src/components/discovery/Roadmap.tsx
Normal file
77
src/components/discovery/Roadmap.tsx
Normal 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: 'W1–2',
|
||||
title: '기반 설치',
|
||||
detail: '스키마·NAP·측정 인프라를 설치하고 기준선 스팟체크를 1회 측정합니다.',
|
||||
},
|
||||
{
|
||||
week: 'W3–6',
|
||||
title: '콘텐츠 구조',
|
||||
detail: '정의문·FAQ·H1 재설계와 HTML 비교표를 구축하고 WordPress를 업그레이드합니다.',
|
||||
},
|
||||
{
|
||||
week: 'W7–8',
|
||||
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>
|
||||
);
|
||||
}
|
||||
158
src/components/discovery/SampleReport.tsx
Normal file
158
src/components/discovery/SampleReport.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
70
src/components/discovery/WhyNow.tsx
Normal file
70
src/components/discovery/WhyNow.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
8
src/components/discovery/discoveryContact.ts
Normal file
8
src/components/discovery/discoveryContact.ts
Normal 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}`)}`;
|
||||
@ -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' }}
|
||||
/>
|
||||
</svg>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<span className="text-2xl font-bold text-[#0A1128]">{score}</span>
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center">
|
||||
<span className={`${valueClassName} font-bold text-[#0A1128]`}>{score}</span>
|
||||
{subLabel && <span className="text-[13px] text-slate-400">{subLabel}</span>}
|
||||
</div>
|
||||
</div>
|
||||
{label && (
|
||||
|
||||
@ -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<string, CriterionResult>(result.results.map((r) => [r.criterionId, r]));
|
||||
|
||||
|
||||
@ -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(
|
||||
<Route path="data-validation" element={<DataValidationPage />} />
|
||||
<Route path="clinic/:id" element={<ClinicProfilePage />} />
|
||||
<Route path="api-dashboard" element={<ApiDashboardPage />} />
|
||||
<Route path="discovery" element={<DiscoveryLandingPage />} />
|
||||
<Route path="discovery/:id" element={<DiscoveryReportPage />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
|
||||
53
src/pages/DiscoveryLandingPage.tsx
Normal file
53
src/pages/DiscoveryLandingPage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user