diff --git a/src/App.tsx b/src/App.tsx index 83cc273..a0df455 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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 (
{!isLoadingPage && } diff --git a/src/components/ClinicNav.tsx b/src/components/ClinicNav.tsx index 8cfe02a..4965e34 100644 --- a/src/components/ClinicNav.tsx +++ b/src/components/ClinicNav.tsx @@ -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 }) { {/* 이전 단계로. 상단 절차 메뉴는 좁은 화면에서 가로 스크롤 안으로 밀려 눈에 안 들어온다. - 되돌아가는 길은 늘 같은 자리에 있어야 한다. 첫 단계에서는 돌아갈 곳이 없어 띄우지 않는다. */} + 되돌아가는 길은 늘 같은 자리에 있어야 한다. 첫 단계에서는 돌아갈 곳이 없어 띄우지 않는다. + + 다음 단계로 가는 길도 같은 이유로 여기에 둔다. 화면마다 따로 심으면 빠지는 곳이 생긴다. + 실제로 사이트 빌드 화면은 빌드가 끝났을 때만 다음 단계 카드를 그려서, 빌드 중이거나 + 멈췄을 때는 상단 메뉴 말고는 넘어갈 길이 없었다. 마지막 단계에서는 갈 곳이 없어 띄우지 않는다. */} {prev && ( )} + + {next && ( + + + + 다음 단계 + {next.label} + + + + + + + )} ); } diff --git a/src/components/discovery/ActionPlanSection.tsx b/src/components/discovery/ActionPlanSection.tsx new file mode 100644 index 0000000..9033afa --- /dev/null +++ b/src/components/discovery/ActionPlanSection.tsx @@ -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 = { + 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 = { + P0: '즉시: 크롤러 접근·엔티티·측정', + P1: '4주 내: 콘텐츠 구조', + P2: '지속: 신선도·시딩', +}; + +export function ActionPlanSection({ result }: { result: DiscoveryResult }) { + return ( + +
+ {(['P0', 'P1', 'P2'] as ActionPriority[]).map((p) => ( +
+
+ + {p} + + {PRIORITY_HEADING[p]} +
+
+ {result.actions.filter((a) => a.priority === p).map((a, i) => ( + +
+

{a.title}

+ {a.effort} +
+

{a.detail}

+
+ {a.criterionIds.map((cid) => ( + {cid} + ))} +
+
+ ))} +
+
+ ))} +
+
+ ); +} diff --git a/src/components/discovery/ClinicInputsPanel.tsx b/src/components/discovery/ClinicInputsPanel.tsx index cc7ad26..233b3ab 100644 --- a/src/components/discovery/ClinicInputsPanel.tsx +++ b/src/components/discovery/ClinicInputsPanel.tsx @@ -117,7 +117,7 @@ export function ClinicInputsPanel({ clinicId }: { clinicId: string }) {

갱신 {build.updated_at.slice(0, 16).replace('T', ' ')} · 글 {posts.length}편{typeof build.report?.totalCostUsd === 'number' ? ` · 생성 비용 $${build.report.totalCostUsd}` : ''}

{build.preview_url && ( - 미리보기 열기 (noindex) + {build.status === 'published' || build.status === 'approved' ? '공개본 사이트 열기' : '초안 사이트 열기'} )} )} diff --git a/src/components/discovery/IntegritySection.tsx b/src/components/discovery/IntegritySection.tsx new file mode 100644 index 0000000..85e82c7 --- /dev/null +++ b/src/components/discovery/IntegritySection.tsx @@ -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 ( + +
+
+

하지 않을 것

+
    + {result.notDoing.map((n) => ( +
  • + +
    +

    {n.item}

    +

    {n.reason}

    +
    +
  • + ))} +
+
+
+

측정 조건

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

    {c}

    +
  • + ))} +
+
+

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

+
+
+
+
+ ); +} diff --git a/src/components/discovery/Tag.tsx b/src/components/discovery/Tag.tsx new file mode 100644 index 0000000..1b581ad --- /dev/null +++ b/src/components/discovery/Tag.tsx @@ -0,0 +1,13 @@ +/** + * 작은 라벨 배지. 진단 리포트(Scorecard의 근거/통제/채점 태그)와 개선 제안(항목 ID 태그) + * 양쪽에서 쓰여 공용 파일로 뺐다. + */ +import type { ReactNode } from 'react'; + +export function Tag({ children }: { key?: string; children: ReactNode }) { + return ( + + {children} + + ); +} diff --git a/src/main.tsx b/src/main.tsx index 4593dc4..3d92fc9 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -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( } /> } /> } /> + } /> } /> } /> } /> diff --git a/src/pages/ActionPlanPage.tsx b/src/pages/ActionPlanPage.tsx new file mode 100644 index 0000000..a4320c5 --- /dev/null +++ b/src/pages/ActionPlanPage.tsx @@ -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 ; + } + + return ( +
+
+
+
+
+ + + INFINITH AI Discovery · Action Plan + +
+

+ + 개선 제안 + +

+

{result.clinicName}

+

+ 진단(v{result.rubricVersion} · {overall.score}/{overall.grade})에서 확인된 항목을 근거로, + 무엇을 언제까지 할지와 저희가 하지 않을 것을 정리했습니다. +

+
+
+ + + + n + c.verifiedCount, 0), + total: AEO_GEO_RUBRIC.criteria.length, + }} + /> +
+ ); +} diff --git a/src/pages/DiscoveryReportPage.tsx b/src/pages/DiscoveryReportPage.tsx index b3a8d93..57fa24f 100644 --- a/src/pages/DiscoveryReportPage.tsx +++ b/src/pages/DiscoveryReportPage.tsx @@ -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 = { 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'; @@ -265,113 +257,11 @@ export default function DiscoveryReportPage() { - {/* ── 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} - ))} -
-
- ))} -
-
- ))} -
-
+ {/* 개선 제안(Action Plan)·하지 않을 것·빌드 제안·확인 항목 패널은 /actions/:id 로 옮겼다. + 증거(진단)와 처방(할 일)은 읽는 목적이 달라 여기 한 페이지에 두면 스크롤만 길어진다. + 확인 항목 패널은 /supporters/:id 라는 전용 단계가 이미 있어 여기서 다시 보여줄 필요가 없다. */} - {/* ── 4b. 진단 요약 → 개선 계획 → 빌드 (dark) ── */} - n + c.verifiedCount, 0), - total: AEO_GEO_RUBRIC.criteria.length, - }} - /> - - {/* ── 5. 하지 않을 것 + 측정 조건 (dark) ── */} - -
-
-

하지 않을 것

-
    - {result.notDoing.map((n) => ( -
  • - -
    -

    {n.item}

    -

    {n.reason}

    -
    -
  • - ))} -
-
-
-

측정 조건

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

    {c}

    -
  • - ))} -
-
-

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

-
-
-
-
- - {/* ── 5b. 서포터즈 자동 빌드 · 병원 확인 항목 (light, v2 §7-5) ── */} - - - {/* ── 6. 채점 기준표 (light) ── */} + {/* ── 4. 채점 기준표 (light) ── */} ; } -function Tag({ children }: { key?: string; children: ReactNode }) { - return ( - - {children} - - ); -} - function RubricRows({ categoryName, criteria }: { key?: string; categoryName: string; criteria: RubricCriterion[] }) { return ( <> diff --git a/src/pages/SiteBuildPage.tsx b/src/pages/SiteBuildPage.tsx index 04091e8..1be46a8 100644 --- a/src/pages/SiteBuildPage.tsx +++ b/src/pages/SiteBuildPage.tsx @@ -205,7 +205,7 @@ export default function SiteBuildPage() { rel="noreferrer" className="btn-on-dark justify-center" > - 사이트 열어 보기 + {state === 'published' || state === 'approved' ? '공개본 사이트 열기' : '초안 사이트 열기'} )}