- aeoGeoRubricV2 status active/2.0, 결과 rubricVersion 2.0. 리포트 헤더는 AEO·GEO 링 2개가 주, v1.0 종합은 보조 표기 - 랜딩: Hero 카피, ABMR 칩(AEO/GEO 목표), SampleReport(두 링 + 축별 8항목 바), Roadmap 제목, HowItWorks를 v2 계산값에서 파생. 발견 칩 수치 v1.1 스냅샷으로 정정 - 비교 엑셀 v2: scripts/build_supporters_v2_compare.ts(점수 계산 TS 단일 원본) → build_supporters_audit_v2_xlsx.py. 공식 AEO 42/GEO 56 · 서포터즈 noindex 상한 40(G1) · 색인 허용 AEO 94/GEO 77 - 덱 v2.0: build_viewclinic_deck_v2_0.py (17장, 표지 배지 v2.0, KPI AEO/GEO, 스코어카드 2장, Method v2.0) + 표준캔버스 + PDF. v1.0·v1.1 파일은 그대로 - docs/AEO_GEO_RUBRIC.md에 v2.0 채택 주석, AEO_GEO_RUBRIC_v2.md 재생성, 리포트 페이지 PDF 재출력 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
172 lines
8.2 KiB
TypeScript
172 lines
8.2 KiB
TypeScript
/**
|
|
* 샘플 리포트 섹션 — 뷰성형외과 실측 스냅샷.
|
|
* 점수·등급·항목 %·항목 수는 전부 discoveryScore(v1 실측)·discoveryScoreV2(AEO/GEO) 계산값에서 렌더 (하드코딩 금지).
|
|
* 발견 칩 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';
|
|
import type { AxisSummary } from './AbmrLoop';
|
|
|
|
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-09-07 재검증(v1.1) 스냅샷과 일치한다. */
|
|
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개, HTML 표 0개, FAQ는 프리저베 한 페이지뿐. 답변엔진이 잘라 갈 단락이 없습니다.',
|
|
},
|
|
{
|
|
label: '강점',
|
|
chipClass: 'bg-[#F3F0FF] border-[#D5CDF5] text-[#4A3A7C]',
|
|
dotColor: '#9B8AD4',
|
|
title: '표면 자산은 이미 강함',
|
|
detail: '강남언니 후기 19,373건, GBP 4.9점. 홈페이지가 따라가면 바로 인용 근거가 됩니다.',
|
|
},
|
|
];
|
|
|
|
interface SampleReportProps {
|
|
overall: OverallScore;
|
|
axes: AxisSummary[];
|
|
result: DiscoveryResult;
|
|
}
|
|
|
|
export default function SampleReport({ overall, axes, 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차 진단 결과입니다. 답변 준비도(AEO)와 출처 준비도(GEO)를 각각 100점으로 보고,
|
|
어디가 강하고 어디가 비어 있는지 항목 단위로 드러냅니다.
|
|
</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)]"
|
|
>
|
|
<div className="flex items-start gap-6">
|
|
{axes.map((ax) => (
|
|
<div key={ax.code} className="flex flex-col items-center gap-2.5">
|
|
<ScoreRing score={ax.score} size={120} valueClassName="text-[32px]" subLabel="/ 100" />
|
|
<div className={`px-3.5 py-[5px] border rounded-full ${GRADE_BADGE_STYLE[ax.grade]}`}>
|
|
<span className="text-sm font-bold">{ax.code} 등급 {ax.grade}</span>
|
|
</div>
|
|
<span className="text-sm text-slate-500">{ax.name}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
<p className="text-[13.5px] leading-relaxed text-slate-400 text-center break-keep">
|
|
{totalCriteria}개 실측 항목 중 {verifiedCriteria}개 확인.
|
|
<br />
|
|
미검증 {unverifiedCriteria}개는 분모에서 제외했습니다. v1.0 종합 {overall.score}점.
|
|
</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)]"
|
|
>
|
|
<div className="grid md:grid-cols-2 gap-x-10 gap-y-[18px]">
|
|
{axes.map((ax) => (
|
|
<div key={ax.code} className="flex flex-col gap-[14px]">
|
|
<div className="flex justify-between items-baseline gap-3 border-b border-slate-100 pb-2">
|
|
<span className="text-[15px] font-bold text-primary-900 break-keep">{ax.code} · {ax.name}</span>
|
|
<span className="text-[15px] font-bold" style={{ color: scorePctColor(ax.score) }}>{ax.score}</span>
|
|
</div>
|
|
{ax.items.map((it) => {
|
|
const pct = it.pct ?? 0;
|
|
const color = it.pct === null ? '#CBD5E1' : scorePctColor(pct);
|
|
return (
|
|
<div key={it.id} className="flex flex-col gap-[6px]">
|
|
<div className="flex justify-between items-baseline gap-3">
|
|
<span className="text-[14px] font-medium text-primary-900 break-keep">{it.name}</span>
|
|
<span className="text-[14px] font-bold" style={{ color }}>{it.pct === null ? '미검증' : `${it.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(pct, 2)}%` }}
|
|
viewport={{ once: true }}
|
|
transition={{ duration: 0.8, ease: 'easeOut' }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</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>
|
|
);
|
|
}
|