o2o-infinith-demo/src/pages/ActionPlanPage.tsx
Haewon Kam 890da0ecdf 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>
2026-09-21 10:27:12 +09:00

81 lines
3.7 KiB
TypeScript

/**
* /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>
);
}