증거(진단)와 처방(할 일)을 한 페이지에 두면 스크롤이 너무 길어져
둘 다 읽기 힘들다는 지적에 따라 단계를 나눴다.
- /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>
153 lines
7.5 KiB
TypeScript
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>
|
|
)}
|
|
</>
|
|
);
|
|
}
|