o2o-infinith-demo/src/components/ClinicNav.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

153 lines
7.5 KiB
TypeScript

/**
* 병원 작업 공간 상단 메뉴.
*
* 마케팅 메뉴(Product·Pricing·Use Cases)는 진단을 받은 병원에게 쓸모가 없다.
* 이 화면들은 진단 → 빌드 → 사진 확인 → 확인 항목이라는 하나의 절차이므로,
* 상단을 그 절차로 바꾼다. 지금 어디에 있고 다음이 무엇인지가 한 줄에 보여야 한다.
*
* 단계의 완료 여부는 각 화면이 판단한다. 여기서는 위치만 표시한다.
*/
import { Link, useLocation } from 'react-router';
import { motion } from 'motion/react';
import { PrismFilled } from './icons/FilledIcons';
type Step = { key: string; label: string; to: (id: string) => string; match: RegExp };
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\/[^/]+/ },
];
/** 작업 공간 경로면 병원 id 를 돌려준다. 아니면 null. */
export function clinicIdFromPath(pathname: string): string | null {
for (const s of STEPS) {
if (s.match.test(pathname)) return pathname.split('/')[2] ?? null;
}
return null;
}
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 (
<>
<nav className="fixed top-0 left-0 right-0 z-50 bg-white/95 border-b border-slate-100 backdrop-blur-md" data-no-print>
<div className="max-w-7xl mx-auto px-6 h-20 flex items-center gap-6">
{/* 로고 + 병원 */}
<Link to="/" className="flex items-center gap-3 shrink-0" aria-label="INFINITH 홈">
<PrismFilled size={22} className="text-[#6C5CE7]" />
<span className="hidden sm:block font-serif text-xl font-black tracking-[0.05em] bg-gradient-to-r from-[#4F1DA1] to-[#021341] bg-clip-text text-transparent">
INFINITH
</span>
</Link>
<span className="hidden md:block w-px h-7 bg-slate-200 shrink-0" />
<span className="hidden md:block text-sm font-semibold text-slate-500 shrink-0 max-w-[160px] truncate" title={clinicId}>
{clinicId}
</span>
{/* 단계 */}
<ol className="flex items-center gap-1 overflow-x-auto ml-auto [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
{STEPS.map((s, i) => {
const isCurrent = i === currentIndex;
const isPast = currentIndex > -1 && i < currentIndex;
return (
<li key={s.key} className="flex items-center shrink-0">
{i > 0 && <span className="w-4 h-px bg-slate-200 mx-0.5" aria-hidden="true" />}
<Link
to={s.to(clinicId)}
aria-current={isCurrent ? 'page' : undefined}
className={`group inline-flex items-center gap-2 rounded-full pl-2 pr-3.5 py-2 text-sm font-semibold transition-colors
${isCurrent
? 'bg-[#F3F0FF] text-[#4A3A7C]'
: 'text-slate-500 hover:bg-slate-100 hover:text-[#1D0024]'}`}
>
<span
className={`w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold shrink-0 transition-colors
${isCurrent
? 'bg-gradient-to-r from-[#4F1DA1] to-[#021341] text-white'
: isPast
? 'bg-[#D5CDF5] text-[#4A3A7C]'
: 'bg-slate-100 text-slate-400 group-hover:bg-slate-200'}`}
>
{i + 1}
</span>
<span className="whitespace-nowrap">{s.label}</span>
</Link>
</li>
);
})}
</ol>
</div>
</nav>
{/* 이전 단계로. 상단 절차 메뉴는 좁은 화면에서 가로 스크롤 안으로 밀려 눈에 안 들어온다.
되돌아가는 길은 늘 같은 자리에 있어야 한다. 첫 단계에서는 돌아갈 곳이 없어 띄우지 않는다.
다음 단계로 가는 길도 같은 이유로 여기에 둔다. 화면마다 따로 심으면 빠지는 곳이 생긴다.
실제로 사이트 빌드 화면은 빌드가 끝났을 때만 다음 단계 카드를 그려서, 빌드 중이거나
멈췄을 때는 상단 메뉴 말고는 넘어갈 길이 없었다. 마지막 단계에서는 갈 곳이 없어 띄우지 않는다. */}
{prev && (
<motion.div
initial={{ opacity: 0, x: -12 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.3, duration: 0.3 }}
className="fixed left-4 bottom-4 md:left-6 md:bottom-6 z-40"
data-no-print
>
<Link
to={prev.to(clinicId)}
className="group inline-flex items-center gap-2.5 rounded-full bg-white/95 backdrop-blur-md border border-slate-200
shadow-[0_4px_20px_rgba(29,0,36,0.14)] pl-3 pr-5 py-3 transition-shadow hover:shadow-[0_6px_26px_rgba(29,0,36,0.2)]"
>
<span className="w-7 h-7 rounded-full bg-slate-100 text-[#1D0024] flex items-center justify-center
transition-colors group-hover:bg-[#F3F0FF]">
<svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true">
<path d="M8.9 1.4 3.3 7l5.6 5.6a1.1 1.1 0 0 0 1.6-1.6L6.5 7l4-4A1.1 1.1 0 0 0 8.9 1.4Z" fill="currentColor" />
</svg>
</span>
<span className="text-left leading-tight">
<span className="block text-[11px] font-semibold text-slate-400"> </span>
<span className="block text-sm font-bold text-[#1D0024] whitespace-nowrap">{prev.label}</span>
</span>
</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>
)}
</>
);
}