import { useMemo } from 'react'; import type { ComponentType, ReactNode } from 'react'; import { useParams, Link } from 'react-router'; import { motion } from 'motion/react'; import { AEO_GEO_RUBRIC } from '../data/aeoGeoRubric'; import { DISCOVERY_RESULTS } from '../data/discovery_viewclinic'; import { scoreDiscovery, levelToSeverity } from '../lib/discoveryScore'; import type { ActionPriority, CategoryId, CategoryScore, ControlLevel, EvidenceGrade, EvidenceMethod, RubricCriterion, } from '../types/discovery'; import { SectionWrapper } from '../components/report/ui/SectionWrapper'; import { ScoreRing } from '../components/report/ui/ScoreRing'; import { SeverityBadge } from '../components/report/ui/SeverityBadge'; import { ServerFilled, DatabaseFilled, FileTextFilled, ShareFilled, EyeFilled, ShieldFilled, PrismFilled, CheckFilled, CrossFilled, WarningFilled, } from '../components/icons/FilledIcons'; /* ────────────────────────── 라벨 맵 ────────────────────────── */ const CATEGORY_ICON: Record> = { access: ServerFilled, entity: DatabaseFilled, content: FileTextFilled, surface: ShareFilled, measure: EyeFilled, hygiene: ShieldFilled, }; const EVIDENCE_LABEL: Record = { measured: '실측', academic: '학술', vendor: '벤더', estimate: '추정', }; const CONTROL_LABEL: Record = { direct: '직접 통제', influence: '영향 가능', none: '통제 불가', }; const METHOD_LABEL: Record = { auto: '자동', semi: '반자동', manual: '수동', }; const PRIORITY_STYLE: Record = { P0: 'bg-[#FFF0F0] text-[#7C3A4B] border-[#F5D5DC]', P1: 'bg-[#FFF6ED] text-[#7C5C3A] border-[#F5E0C5]', P2: 'bg-[#F3F0FF] text-[#4A3A7C] border-[#D5CDF5]', }; const scoreColor = (pct: number) => pct <= 40 ? '#C084CF' : pct <= 60 ? '#8B9CF7' : pct <= 80 ? '#7C6DD8' : '#6C5CE7'; /* ────────────────────────── 페이지 ────────────────────────── */ export default function DiscoveryReportPage() { const { id } = useParams<{ id: string }>(); const result = id ? DISCOVERY_RESULTS[id] : undefined; const overall = useMemo( () => (result ? scoreDiscovery(AEO_GEO_RUBRIC, result) : null), [result], ); if (!result || !overall) { return (

진단 결과를 찾을 수 없습니다.

홈으로
); } const resultById = new Map(result.results.map((r) => [r.criterionId, r])); return (
{/* ── 1. 헤더 (dark) ── */}
INFINITH AI Discovery · AEO / GEO Audit v{result.rubricVersion}

AI Answer Readiness

{result.clinicName}

{result.industry}

{result.url}

실측일 {result.auditedAt} · 검증 항목 {overall.categories.reduce((s, c) => s + c.verifiedCount, 0)} / {' '}{AEO_GEO_RUBRIC.criteria.length} · 미검증 항목은 분모 제외

{overall.grade} {overall.earned} / {overall.possible}점
{/* 카테고리 바 */}
{overall.categories.map((cs, i) => ( ))}
{/* ── 2. 핵심 발견 (light) ── */}
{result.keyFindings.map((f, i) => (

{f.title}

{f.detail}

))}
{/* ── 3. 카테고리별 상세 (dark) ── */}
{overall.categories.map((cs) => { const Icon = CATEGORY_ICON[cs.category.id]; const criteria = AEO_GEO_RUBRIC.criteria.filter((c) => c.category === cs.category.id); return (

{cs.category.code} · {cs.category.nameEn}

{cs.category.name}

배점 {cs.weight} · 검증 {cs.verifiedCount}/{cs.totalCount}

{cs.earned} / {cs.possible}점

{cs.category.description}

{criteria.map((c) => { const r = resultById.get(c.id); const level = r?.level ?? 'unverified'; const levelLabel = level === 'unverified' ? '미검증' : c.levels[level].label; return (
{c.id}

{c.name}

{c.weight}점

{r?.evidence}

{r?.source && (

{r.source}

)}
{EVIDENCE_LABEL[c.evidenceGrade]} {CONTROL_LABEL[c.control]} {METHOD_LABEL[c.method]}
{level !== 'unverified' && ( {Math.round((c.weight * level) / 3 * 10) / 10} / {c.weight} )}
); })}
); })}
{/* ── 4. 실행 계획 (light) ── */}
{(['P0', 'P1', 'P2'] as ActionPriority[]).map((p) => (
{p} {p === 'P0' ? '즉시: 크롤러 접근·엔티티·측정' : p === 'P1' ? '4주 내: 콘텐츠 구조' : '지속: 신선도·시딩'}
{result.actions.filter((a) => a.priority === p).map((a, i) => (

{a.title}

{a.effort}

{a.detail}

{a.criterionIds.map((cid) => ( {cid} ))}
))}
))}
{/* ── 5. 하지 않을 것 + 측정 조건 (dark) ── */}

하지 않을 것

    {result.notDoing.map((n) => (
  • {n.item}

    {n.reason}

  • ))}

측정 조건

    {result.conditions.map((c) => (
  • {c}

  • ))}

점수는 검증된 항목만으로 정규화했습니다. 미검증 항목(강남언니·GBP·GA4·로그)을 확인하면 점수가 오를 수도, 내릴 수도 있습니다. 이 리포트는 답변엔진 노출을 보장하지 않으며, 크롤러 접근·엔티티·콘텐츠 구조라는 전제조건의 충족 여부를 진단합니다.

{/* ── 6. 채점 기준표 (light) ── */}
{AEO_GEO_RUBRIC.categories.map((cat) => ( c.category === cat.id)} /> ))}
ID 항목 배점 근거 통제 채점 3점 기준
); } /* ────────────────────────── 서브 컴포넌트 ────────────────────────── */ function CategoryBar({ cs, index }: { key?: string; cs: CategoryScore; index: number }) { const Icon = CATEGORY_ICON[cs.category.id]; return (
{cs.category.code}. {cs.category.name}
{cs.weight}점
{cs.pct}% {cs.verifiedCount < cs.totalCount ? `미검증 ${cs.totalCount - cs.verifiedCount}` : '전 항목 검증'}
); } function FindingIcon({ severity }: { severity: 'critical' | 'warning' | 'good' }) { if (severity === 'good') return ; if (severity === 'warning') return ; return ; } function Tag({ children }: { key?: string; children: ReactNode }) { return ( {children} ); } function RubricRows({ categoryName, criteria }: { key?: string; categoryName: string; criteria: RubricCriterion[] }) { return ( <> {categoryName} {criteria.map((c) => ( {c.id}

{c.name}

{c.rationale}

{c.howToCheck}

{c.weight} {EVIDENCE_LABEL[c.evidenceGrade]} {CONTROL_LABEL[c.control]} {METHOD_LABEL[c.method]} {c.levels[3].label} ))} ); }