feat(discovery): 진단 리포트에서 개선 제안(Action Plan) 단계 분리

증거(진단)와 처방(할 일)을 한 페이지에 두면 스크롤이 너무 길어져
둘 다 읽기 힘들다는 지적에 따라 단계를 나눴다.

- /actions/:id 신규: Action Plan(P0~P2) + What We Don't Sell + 빌드 제안
  (ActionPlanSection·IntegritySection 신규 컴포넌트, Tag는 두 페이지가
  같이 써서 공용 컴포넌트로 뺌)
- /discovery/🆔 위 섹션 제거, 진단(점수·근거·Scorecard·Rubric)만 남김.
  페이지 맨 아래 중복으로 박혀 있던 확인 항목 패널(ClinicInputsPanel)도
  제거 — /supporters/:id 전용 단계가 이미 있어 두 번 보여줄 필요가 없었다
- 진단 데이터가 없는 병원이 /actions/:id 로 오면 /discovery/:id 로 되돌림
- ClinicNav: 5단계로 확장(진단 리포트→개선 제안→사이트 빌드→사진 확인→
  확인 항목), 다음 단계 플로팅 버튼 추가(기존엔 이전 단계만 있었다)
- App.tsx: 라우트 전환 시 스크롤을 맨 위로 되돌림. 스크롤을 내린 채
  다음 단계를 누르면 새 페이지가 같은 위치(짧으면 빈 하단)에서 시작하던
  문제 수정. 인앱 해시 앵커(#breakdown)가 있으면 건드리지 않음
- 사이트 빌드/확인 항목 화면의 미리보기 버튼을 "초안 사이트 열기"로
  통일(공개 후에는 "공개본 사이트 열기")

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Haewon Kam 2026-09-21 10:27:12 +09:00
parent 36731d3ad1
commit 890da0ecdf
10 changed files with 267 additions and 127 deletions

View File

@ -1,3 +1,4 @@
import { useEffect } from 'react';
import { Outlet, useLocation } from 'react-router';
import Navbar from './components/Navbar';
import Footer from './components/Footer';
@ -8,6 +9,15 @@ export default function App() {
const location = useLocation();
const isLoadingPage = location.pathname.startsWith('/report/loading');
// 라우트가 바뀌어도 브라우저가 스크롤 위치를 그대로 들고 온다. 진단 리포트처럼 긴 페이지를
// 내려 보다가 "다음 단계"를 누르면, 새로 연 페이지가 그 위치(짧으면 하단 빈 공간)에서 시작해
// 항상 위부터 보게 되지 않는다. 라우트 전환마다 맨 위로 되돌린다.
// 해시(#breakdown 같은 인앱 앵커)가 있으면 그 앵커로 가려는 의도이므로 건드리지 않는다.
useEffect(() => {
if (location.hash) return;
window.scrollTo(0, 0);
}, [location.pathname, location.hash]);
return (
<div className="min-h-screen bg-slate-50 selection:bg-purple-200 selection:text-primary-900">
{!isLoadingPage && <Navbar />}

View File

@ -15,6 +15,7 @@ type Step = { key: string; label: string; to: (id: string) => string; match: Reg
const STEPS: Step[] = [
{ key: 'report', label: '진단 리포트', to: (id) => `/discovery/${id}`, match: /^\/discovery\/[^/]+/ },
{ key: 'actions', label: '개선 제안', to: (id) => `/actions/${id}`, match: /^\/actions\/[^/]+/ },
{ key: 'build', label: '사이트 빌드', to: (id) => `/build/${id}`, match: /^\/build\/[^/]+/ },
{ key: 'images', label: '사진 확인', to: (id) => `/images/${id}`, match: /^\/images\/[^/]+/ },
{ key: 'inputs', label: '확인 항목', to: (id) => `/supporters/${id}`, match: /^\/supporters\/[^/]+/ },
@ -32,6 +33,7 @@ export default function ClinicNav({ clinicId }: { clinicId: string }) {
const { pathname } = useLocation();
const currentIndex = STEPS.findIndex((s) => s.match.test(pathname));
const prev = currentIndex > 0 ? STEPS[currentIndex - 1] : null;
const next = currentIndex > -1 && currentIndex < STEPS.length - 1 ? STEPS[currentIndex + 1] : null;
return (
<>
@ -87,7 +89,11 @@ export default function ClinicNav({ clinicId }: { clinicId: string }) {
</nav>
{/* 이전 단계로. 상단 절차 메뉴는 좁은 화면에서 가로 스크롤 안으로 밀려 눈에 안 들어온다.
되돌아가는 길은 늘 같은 자리에 있어야 한다. 첫 단계에서는 돌아갈 곳이 없어 띄우지 않는다. */}
되돌아가는 길은 늘 같은 자리에 있어야 한다. 첫 단계에서는 돌아갈 곳이 없어 띄우지 않는다.
다음 단계로 가는 길도 같은 이유로 여기에 둔다. 화면마다 따로 심으면 빠지는 곳이 생긴다.
실제로 사이트 빌드 화면은 빌드가 끝났을 때만 다음 단계 카드를 그려서, 빌드 중이거나
멈췄을 때는 상단 메뉴 말고는 넘어갈 길이 없었다. 마지막 단계에서는 갈 곳이 없어 띄우지 않는다. */}
{prev && (
<motion.div
initial={{ opacity: 0, x: -12 }}
@ -114,6 +120,33 @@ export default function ClinicNav({ clinicId }: { clinicId: string }) {
</Link>
</motion.div>
)}
{next && (
<motion.div
initial={{ opacity: 0, x: 12 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.3, duration: 0.3 }}
className="fixed right-4 bottom-4 md:right-6 md:bottom-6 z-40"
data-no-print
>
<Link
to={next.to(clinicId)}
className="group inline-flex items-center gap-2.5 rounded-full bg-gradient-to-r from-[#4F1DA1] to-[#021341] text-white
shadow-[0_4px_20px_rgba(29,0,36,0.24)] pl-5 pr-3 py-3 transition-shadow hover:shadow-[0_6px_26px_rgba(29,0,36,0.32)]"
>
<span className="text-right leading-tight">
<span className="block text-[11px] font-semibold text-white/60">다음 단계</span>
<span className="block text-sm font-bold whitespace-nowrap">{next.label}</span>
</span>
<span className="w-7 h-7 rounded-full bg-white/15 flex items-center justify-center
transition-colors group-hover:bg-white/25">
<svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true">
<path d="M5.1 1.4 10.7 7l-5.6 5.6a1.1 1.1 0 0 1-1.6-1.6L7.5 7l-4-4A1.1 1.1 0 0 1 5.1 1.4Z" fill="currentColor" />
</svg>
</span>
</Link>
</motion.div>
)}
</>
);
}

View File

@ -0,0 +1,67 @@
/**
* 개선 제안(/actions/:id) 1부: 우선순위별 실행 항목.
* 원래 진단 리포트 안의 "Action Plan" 섹션이었다. 진단(증거)과 처방(할 일)은 읽는 목적이 달라
* 별도 단계로 뗐다. 내용은 그대로다.
*/
import { motion } from 'motion/react';
import { SectionWrapper } from '../report/ui/SectionWrapper';
import { Tag } from './Tag';
import type { ActionPriority, DiscoveryResult } from '../../types/discovery';
const PRIORITY_STYLE: Record<ActionPriority, string> = {
P0: 'bg-[#FFF0F0] text-[#7C3A4B] border-[#F5D5DC]',
P1: 'bg-[#FFF6ED] text-[#7C5C3A] border-[#F5E0C5]',
P2: 'bg-[#F3F0FF] text-[#4A3A7C] border-[#D5CDF5]',
};
const PRIORITY_HEADING: Record<ActionPriority, string> = {
P0: '즉시: 크롤러 접근·엔티티·측정',
P1: '4주 내: 콘텐츠 구조',
P2: '지속: 신선도·시딩',
};
export function ActionPlanSection({ result }: { result: DiscoveryResult }) {
return (
<SectionWrapper
id="actions"
title="Action Plan"
subtitle="우선순위별 실행 항목. 각 항목이 어느 기준을 올리는지 연결"
>
<div className="space-y-4">
{(['P0', 'P1', 'P2'] as ActionPriority[]).map((p) => (
<div key={p}>
<div className="flex items-center gap-3 mb-3 mt-6 first:mt-0">
<span className={`inline-flex items-center rounded-full text-sm font-semibold px-3 py-1 border ${PRIORITY_STYLE[p]}`}>
{p}
</span>
<span className="text-lg font-bold text-[#0A1128]">{PRIORITY_HEADING[p]}</span>
</div>
<div className="grid md:grid-cols-2 gap-4">
{result.actions.filter((a) => a.priority === p).map((a, i) => (
<motion.div
key={a.title}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: i * 0.08 }}
className="on-light bg-white rounded-2xl border border-slate-100 shadow-sm p-5"
>
<div className="flex items-start justify-between gap-3 mb-2">
<h4 className="font-bold text-[#0A1128]">{a.title}</h4>
<span className="ui-note shrink-0">{a.effort}</span>
</div>
<p className="ui-body mb-3">{a.detail}</p>
<div className="flex flex-wrap gap-1.5">
{a.criterionIds.map((cid) => (
<Tag key={cid}>{cid}</Tag>
))}
</div>
</motion.div>
))}
</div>
</div>
))}
</div>
</SectionWrapper>
);
}

View File

@ -117,7 +117,7 @@ export function ClinicInputsPanel({ clinicId }: { clinicId: string }) {
<p className="ui-note mt-1">갱신 {build.updated_at.slice(0, 16).replace('T', ' ')} · 글 {posts.length}편{typeof build.report?.totalCostUsd === 'number' ? ` · 생성 비용 $${build.report.totalCostUsd}` : ''}</p>
</div>
{build.preview_url && (
<a href={build.preview_url} target="_blank" rel="noreferrer" className="inline-flex items-center justify-center rounded-full bg-primary-900 text-white font-semibold px-6 py-3 text-base">미리보기 열기 (noindex)</a>
<a href={build.preview_url} target="_blank" rel="noreferrer" className="inline-flex items-center justify-center rounded-full bg-primary-900 text-white font-semibold px-6 py-3 text-base">{build.status === 'published' || build.status === 'approved' ? '공개본 사이트 열기' : '초안 사이트 열기'}</a>
)}
</div>
)}

View File

@ -0,0 +1,53 @@
/**
* 개선 제안(/actions/:id) 2부: 하지 않을 것 + 측정 조건.
* "이 개선안이 무엇을 안 파는지"라 처방 쪽 내용이다. 진단 리포트에서 옮겨왔다.
*/
import { SectionWrapper } from '../report/ui/SectionWrapper';
import { CheckFilled, CrossFilled } from '../icons/FilledIcons';
import type { DiscoveryResult } from '../../types/discovery';
export function IntegritySection({ result }: { result: DiscoveryResult }) {
return (
<SectionWrapper
id="integrity"
title="What We Don't Sell"
subtitle="근거 없는 것은 팔지 않는다. 이 리포트의 정직성 규약"
dark
>
<div className="grid lg:grid-cols-2 gap-6">
<div className="on-light bg-white rounded-2xl shadow-[3px_4px_12px_rgba(0,0,0,0.06)] p-6 text-[#0A1128]">
<h3 className="text-xl font-bold mb-4 font-sans text-[#0A1128]!">하지 않을 것</h3>
<ul className="space-y-4">
{result.notDoing.map((n) => (
<li key={n.item} className="flex gap-3">
<span className="shrink-0 text-[#D4889A] mt-0.5"><CrossFilled size={18} /></span>
<div>
<p className="text-base font-semibold">{n.item}</p>
<p className="ui-note mt-0.5">{n.reason}</p>
</div>
</li>
))}
</ul>
</div>
<div className="on-light bg-white rounded-2xl shadow-[3px_4px_12px_rgba(0,0,0,0.06)] p-6 text-[#0A1128]">
<h3 className="text-xl font-bold mb-4 font-sans text-[#0A1128]!">측정 조건</h3>
<ul className="space-y-3">
{result.conditions.map((c) => (
<li key={c} className="flex gap-3">
<span className="shrink-0 text-[#9B8AD4] mt-0.5"><CheckFilled size={18} /></span>
<p className="ui-body">{c}</p>
</li>
))}
</ul>
<div className="mt-6 rounded-xl p-4 bg-[linear-gradient(to_right,#fff3eb,#e4cfff,#f5f9ff)]">
<p className="text-sm text-slate-600 leading-relaxed">
점수는 <strong>검증된 항목만</strong>으로 정규화했습니다. 미검증 항목(강남언니·GBP·GA4·로그)을
확인하면 점수가 오를 수도, 내릴 수도 있습니다. 이 리포트는 답변엔진 노출을 보장하지 않으며,
크롤러 접근·엔티티·콘텐츠 구조라는 <strong>전제조건</strong>의 충족 여부를 진단합니다.
</p>
</div>
</div>
</div>
</SectionWrapper>
);
}

View File

@ -0,0 +1,13 @@
/**
* 작은 라벨 배지. 진단 리포트(Scorecard의 근거/통제/채점 태그)와 개선 제안(항목 ID 태그)
* 양쪽에서 쓰여 공용 파일로 뺐다.
*/
import type { ReactNode } from 'react';
export function Tag({ children }: { key?: string; children: ReactNode }) {
return (
<span className="inline-flex items-center rounded-full bg-slate-100 text-slate-600 text-sm font-medium px-2 py-0.5">
{children}
</span>
);
}

View File

@ -17,6 +17,7 @@ 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 ActionPlanPage from './pages/ActionPlanPage.tsx';
import SupportersBuildPage from './pages/SupportersBuildPage.tsx';
import ImageReviewPage from './pages/ImageReviewPage.tsx';
import SiteBuildPage from './pages/SiteBuildPage.tsx';
@ -49,6 +50,7 @@ createRoot(document.getElementById('root')!).render(
<Route path="api-dashboard" element={<ApiDashboardPage />} />
<Route path="discovery" element={<DiscoveryLandingPage />} />
<Route path="discovery/:id" element={<DiscoveryReportPage />} />
<Route path="actions/:id" element={<ActionPlanPage />} />
<Route path="supporters/:id" element={<SupportersBuildPage />} />
<Route path="images/:id" element={<ImageReviewPage />} />
<Route path="build/:id" element={<SiteBuildPage />} />

View File

@ -0,0 +1,80 @@
/**
* /actions/:id — 개선 제안 (Action Plan). 진단 리포트(/discovery/:id)에서 분리된 처방 단계.
*
* 진단은 "지금 무엇이 문제인가"(증거), 여기는 "그래서 무엇을 하는가"(처방)다.
* 읽는 목적이 다르고, 둘을 한 페이지에 두면 스크롤이 길어져 둘 다 읽기 힘들어진다.
* 진단 데이터가 없으면 처방도 쓸 수 없으므로 진단 리포트로 돌려보낸다.
*/
import { useMemo } from 'react';
import { useParams, Navigate } from 'react-router';
import { AEO_GEO_RUBRIC } from '../data/aeoGeoRubric';
import { AEO_GEO_RUBRIC_V2 } from '../data/aeoGeoRubricV2';
import { DISCOVERY_RESULTS } from '../data/discoveryResults';
import { scoreDiscovery } from '../lib/discoveryScore';
import { scoreDiscoveryV2 } from '../lib/discoveryScoreV2';
import { ActionPlanSection } from '../components/discovery/ActionPlanSection';
import { IntegritySection } from '../components/discovery/IntegritySection';
import { BuildPlanSection } from '../components/discovery/BuildPlanSection';
import { PrismFilled } from '../components/icons/FilledIcons';
export default function ActionPlanPage() {
const { id } = useParams<{ id: string }>();
const result = id ? DISCOVERY_RESULTS[id] : undefined;
const overall = useMemo(
() => (result ? scoreDiscovery(AEO_GEO_RUBRIC, result) : null),
[result],
);
const overallV2 = useMemo(
() => (result ? scoreDiscoveryV2(AEO_GEO_RUBRIC_V2, result) : null),
[result],
);
if (!id) return null;
if (!result || !overall) {
// 개선 제안은 진단 항목 위에 세운다. 진단이 없으면 여기서 보여줄 근거가 없으니
// 진단 리포트로 보낸다. 그 페이지가 "아직 진단 전" 안내와 다음 링크를 갖고 있다.
return <Navigate to={`/discovery/${id}`} replace />;
}
return (
<div data-report-content className="[word-break:keep-all]">
<section className="on-dark bg-[#0A1128] text-white relative overflow-hidden pt-28 pb-16 md:pt-36 md:pb-20 px-6">
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top_right,rgba(108,92,231,0.18),transparent_60%)]" />
<div className="relative max-w-7xl mx-auto">
<div className="inline-flex items-center gap-2 rounded-full bg-white/10 border border-white/10 px-4 py-1.5 mb-6">
<PrismFilled size={16} className="text-purple-300" />
<span className="text-sm font-semibold tracking-wide text-purple-200">
INFINITH AI Discovery · Action Plan
</span>
</div>
<h1 className="font-serif text-4xl md:text-6xl font-bold tracking-[-0.02em] mb-4">
<span className="bg-gradient-to-r from-purple-300 to-blue-300 bg-clip-text text-transparent">
개선 제안
</span>
</h1>
<p className="text-3xl font-bold mb-2">{result.clinicName}</p>
<p className="ui-body text-white/70 max-w-2xl">
진단(v{result.rubricVersion} · {overall.score}/{overall.grade})에서 확인된 항목을 근거로,
무엇을 언제까지 할지와 저희가 하지 않을 것을 정리했습니다.
</p>
</div>
</section>
<ActionPlanSection result={result} />
<IntegritySection result={result} />
<BuildPlanSection
result={result}
overall={overall}
summary={{
rubricVersion: result.rubricVersion,
grade: overall.grade,
score: overall.score,
aeo: overallV2?.aeo.score,
geo: overallV2?.geo.score,
verified: overall.categories.reduce((n, c) => n + c.verifiedCount, 0),
total: AEO_GEO_RUBRIC.criteria.length,
}}
/>
</div>
);
}

View File

@ -1,17 +1,15 @@
import { useMemo } from 'react';
import type { ComponentType, ReactNode } from 'react';
import type { ComponentType } from 'react';
import { useParams, Link } from 'react-router';
import { motion } from 'motion/react';
import { AEO_GEO_RUBRIC } from '../data/aeoGeoRubric';
import { AEO_GEO_RUBRIC_V2 } from '../data/aeoGeoRubricV2';
import { scoreDiscoveryV2 } from '../lib/discoveryScoreV2';
import { AeoGeoV2Panel } from '../components/discovery/AeoGeoV2Panel';
import { ClinicInputsPanel } from '../components/discovery/ClinicInputsPanel';
import { BuildPlanSection } from '../components/discovery/BuildPlanSection';
import { Tag } from '../components/discovery/Tag';
import { DISCOVERY_RESULTS } from '../data/discoveryResults';
import { scoreDiscovery, levelToSeverity } from '../lib/discoveryScore';
import type {
ActionPriority,
CategoryId,
CategoryScore,
ControlLevel,
@ -65,12 +63,6 @@ const METHOD_LABEL: Record<EvidenceMethod, string> = {
manual: '수동',
};
const PRIORITY_STYLE: Record<ActionPriority, string> = {
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';
@ -265,113 +257,11 @@ export default function DiscoveryReportPage() {
</div>
</SectionWrapper>
{/* ── 4. 실행 계획 (light) ── */}
<SectionWrapper
id="actions"
title="Action Plan"
subtitle="우선순위별 실행 항목. 각 항목이 어느 기준을 올리는지 연결"
>
<div className="space-y-4">
{(['P0', 'P1', 'P2'] as ActionPriority[]).map((p) => (
<div key={p}>
<div className="flex items-center gap-3 mb-3 mt-6 first:mt-0">
<span className={`inline-flex items-center rounded-full text-sm font-semibold px-3 py-1 border ${PRIORITY_STYLE[p]}`}>
{p}
</span>
<span className="text-lg font-bold text-[#0A1128]">
{p === 'P0' ? '즉시: 크롤러 접근·엔티티·측정' : p === 'P1' ? '4주 내: 콘텐츠 구조' : '지속: 신선도·시딩'}
</span>
</div>
<div className="grid md:grid-cols-2 gap-4">
{result.actions.filter((a) => a.priority === p).map((a, i) => (
<motion.div
key={a.title}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: i * 0.08 }}
className="on-light bg-white rounded-2xl border border-slate-100 shadow-sm p-5"
>
<div className="flex items-start justify-between gap-3 mb-2">
<h4 className="font-bold text-[#0A1128]">{a.title}</h4>
<span className="ui-note shrink-0">{a.effort}</span>
</div>
<p className="ui-body mb-3">{a.detail}</p>
<div className="flex flex-wrap gap-1.5">
{a.criterionIds.map((cid) => (
<Tag key={cid}>{cid}</Tag>
))}
</div>
</motion.div>
))}
</div>
</div>
))}
</div>
</SectionWrapper>
{/* 개선 제안(Action Plan)·하지 않을 것·빌드 제안·확인 항목 패널은 /actions/:id 로 옮겼다.
증거(진단)와 처방(할 일)은 읽는 목적이 달라 여기 한 페이지에 두면 스크롤만 길어진다.
확인 항목 패널은 /supporters/:id 라는 전용 단계가 이미 있어 여기서 다시 보여줄 필요가 없다. */}
{/* ── 4b. 진단 요약 → 개선 계획 → 빌드 (dark) ── */}
<BuildPlanSection
result={result}
overall={overall}
summary={{
rubricVersion: result.rubricVersion,
grade: overall.grade,
score: overall.score,
aeo: overallV2?.aeo.score,
geo: overallV2?.geo.score,
verified: overall.categories.reduce((n, c) => n + c.verifiedCount, 0),
total: AEO_GEO_RUBRIC.criteria.length,
}}
/>
{/* ── 5. 하지 않을 것 + 측정 조건 (dark) ── */}
<SectionWrapper
id="integrity"
title="What We Don't Sell"
subtitle="근거 없는 것은 팔지 않는다. 이 리포트의 정직성 규약"
dark
>
<div className="grid lg:grid-cols-2 gap-6">
<div className="on-light bg-white rounded-2xl shadow-[3px_4px_12px_rgba(0,0,0,0.06)] p-6 text-[#0A1128]">
<h3 className="text-xl font-bold mb-4 font-sans text-[#0A1128]!">하지 않을 것</h3>
<ul className="space-y-4">
{result.notDoing.map((n) => (
<li key={n.item} className="flex gap-3">
<span className="shrink-0 text-[#D4889A] mt-0.5"><CrossFilled size={18} /></span>
<div>
<p className="text-base font-semibold">{n.item}</p>
<p className="ui-note mt-0.5">{n.reason}</p>
</div>
</li>
))}
</ul>
</div>
<div className="on-light bg-white rounded-2xl shadow-[3px_4px_12px_rgba(0,0,0,0.06)] p-6 text-[#0A1128]">
<h3 className="text-xl font-bold mb-4 font-sans text-[#0A1128]!">측정 조건</h3>
<ul className="space-y-3">
{result.conditions.map((c) => (
<li key={c} className="flex gap-3">
<span className="shrink-0 text-[#9B8AD4] mt-0.5"><CheckFilled size={18} /></span>
<p className="ui-body">{c}</p>
</li>
))}
</ul>
<div className="mt-6 rounded-xl p-4 bg-[linear-gradient(to_right,#fff3eb,#e4cfff,#f5f9ff)]">
<p className="text-sm text-slate-600 leading-relaxed">
점수는 <strong>검증된 항목만</strong>으로 정규화했습니다. 미검증 항목(강남언니·GBP·GA4·로그)을
확인하면 점수가 오를 수도, 내릴 수도 있습니다. 이 리포트는 답변엔진 노출을 보장하지 않으며,
크롤러 접근·엔티티·콘텐츠 구조라는 <strong>전제조건</strong>의 충족 여부를 진단합니다.
</p>
</div>
</div>
</div>
</SectionWrapper>
{/* ── 5b. 서포터즈 자동 빌드 · 병원 확인 항목 (light, v2 §7-5) ── */}
<ClinicInputsPanel clinicId={result.id} />
{/* ── 6. 채점 기준표 (light) ── */}
{/* ── 4. 채점 기준표 (light) ── */}
<SectionWrapper
id="rubric"
title="Scoring Rubric"
@ -494,14 +384,6 @@ function FindingIcon({ severity }: { severity: 'critical' | 'warning' | 'good' }
return <span className="text-[#D4889A] shrink-0"><CrossFilled size={22} /></span>;
}
function Tag({ children }: { key?: string; children: ReactNode }) {
return (
<span className="inline-flex items-center rounded-full bg-slate-100 text-slate-600 text-sm font-medium px-2 py-0.5">
{children}
</span>
);
}
function RubricRows({ categoryName, criteria }: { key?: string; categoryName: string; criteria: RubricCriterion[] }) {
return (
<>

View File

@ -205,7 +205,7 @@ export default function SiteBuildPage() {
rel="noreferrer"
className="btn-on-dark justify-center"
>
<EyeFilled size={16} /> 사이트 열어 보기
<EyeFilled size={16} /> {state === 'published' || state === 'approved' ? '공개본 사이트 열기' : '초안 사이트 열기'}
</a>
)}