fix: remove all hardcoded view-clinic references for dynamic report routing

- useReport: remove view-clinic guard so any reportId fetches from Supabase
- KPIDashboard: dynamic plan link + clinicName-based PDF filename
- PlanCTA: dynamic studio path via useParams
- PageNavigator: prefix-based path matching for dynamic route IDs
- Navbar/Footer: logo links to landing via React Router Link

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
claude/bold-hawking
Haewon Kam 2026-04-02 12:02:34 +09:00
parent 60cd055042
commit 2d6e95c414
7 changed files with 53 additions and 25 deletions

View File

@ -1,12 +1,12 @@
import React from 'react';
import { Link } from 'react-router';
export default function Footer() {
return (
<footer className="bg-white border-t border-slate-100 py-12 px-6">
<div className="max-w-7xl mx-auto flex flex-col md:flex-row items-center justify-between gap-6">
<div className="flex items-center gap-2">
<Link to="/" className="flex items-center gap-2">
<span className="font-serif text-3xl font-black tracking-[0.05em] bg-gradient-to-r from-[#4F1DA1] to-[#021341] bg-clip-text text-transparent">INFINITH</span>
</div>
</Link>
<div className="text-sm text-slate-500 text-center md:text-left">
&copy; {new Date().getFullYear()} INFINITH. All rights reserved. <br className="md:hidden" />
Infinite Marketing for Premium Medical Business & Marketing Agency

View File

@ -1,13 +1,13 @@
import React from 'react';
import { Link } from 'react-router';
import { motion } from 'motion/react';
export default function Navbar() {
return (
<nav className="fixed top-0 left-0 right-0 z-50 bg-white/70 backdrop-blur-lg border-b border-white/20">
<div className="max-w-7xl mx-auto px-6 h-20 flex items-center justify-between">
<div className="flex items-center gap-2">
<Link to="/" className="flex items-center gap-2">
<span className="font-serif text-3xl font-black tracking-[0.05em] bg-gradient-to-r from-[#4F1DA1] to-[#021341] bg-clip-text text-transparent">INFINITH</span>
</div>
</Link>
<div className="hidden md:flex items-center gap-8 text-sm font-medium text-slate-600">
<a href="#solution" className="hover:text-primary-900 transition-colors">Solution</a>
<a href="#modules" className="hover:text-primary-900 transition-colors">Modules</a>

View File

@ -1,21 +1,45 @@
import { useLocation, useNavigate } from 'react-router';
import { ChevronLeft, ChevronRight } from 'lucide-react';
/**
* Page flow uses path prefixes for dynamic routes.
* Matching is done via startsWith so /report/{any-id} matches '/report/'.
*/
const PAGE_FLOW = [
{ path: '/', label: '랜딩' },
{ path: '/report/view-clinic', label: '마케팅 분석' },
{ path: '/plan/view-clinic', label: '콘텐츠 기획' },
{ path: '/studio/view-clinic', label: '콘텐츠 제작' },
{ path: '/channels', label: '채널 연결' },
{ path: '/distribute', label: '콘텐츠 배포' },
{ path: '/performance', label: '성과 관리' },
{ prefix: '/', label: '랜딩', exact: true },
{ prefix: '/report/', label: '마케팅 분석' },
{ prefix: '/plan/', label: '콘텐츠 기획' },
{ prefix: '/studio/', label: '콘텐츠 제작' },
{ prefix: '/channels', label: '채널 연결', exact: true },
{ prefix: '/distribute', label: '콘텐츠 배포', exact: true },
{ prefix: '/performance', label: '성과 관리', exact: true },
];
function findCurrentIndex(pathname: string): number {
return PAGE_FLOW.findIndex((p) =>
p.exact ? pathname === p.prefix : pathname.startsWith(p.prefix)
);
}
function getNavigatePath(prefix: string, currentPath: string): string {
// For dynamic routes, try to preserve the current report/plan ID
if (prefix === '/') return '/';
// Extract ID from current path (e.g., /report/abc123 → abc123)
const idMatch = currentPath.match(/\/(report|plan|studio|clinic)\/(.+)/);
const currentId = idMatch?.[2] || 'live';
if (prefix.endsWith('/')) {
return `${prefix}${currentId}`;
}
return prefix;
}
export default function PageNavigator() {
const location = useLocation();
const navigate = useNavigate();
const currentIndex = PAGE_FLOW.findIndex((p) => p.path === location.pathname);
const currentIndex = findCurrentIndex(location.pathname);
if (currentIndex === -1) return null;
const prev = currentIndex > 0 ? PAGE_FLOW[currentIndex - 1] : null;
@ -28,7 +52,7 @@ export default function PageNavigator() {
>
{/* Back */}
<button
onClick={() => prev && navigate(prev.path)}
onClick={() => prev && navigate(getNavigatePath(prev.prefix, location.pathname))}
disabled={!prev}
className="flex items-center gap-2 px-3 py-2 rounded-full text-sm font-medium transition-all disabled:opacity-30 disabled:cursor-not-allowed hover:bg-slate-50 text-slate-600"
>
@ -40,8 +64,8 @@ export default function PageNavigator() {
<div className="flex items-center gap-2 px-2">
{PAGE_FLOW.map((page, i) => (
<button
key={page.path}
onClick={() => navigate(page.path)}
key={page.prefix}
onClick={() => navigate(getNavigatePath(page.prefix, location.pathname))}
title={page.label}
className={`rounded-full transition-all ${
i === currentIndex
@ -54,7 +78,7 @@ export default function PageNavigator() {
{/* Next */}
<button
onClick={() => next && navigate(next.path)}
onClick={() => next && navigate(getNavigatePath(next.prefix, location.pathname))}
disabled={!next}
className="flex items-center gap-2 px-3 py-2 rounded-full text-sm font-medium transition-all disabled:opacity-30 disabled:cursor-not-allowed hover:bg-slate-50 text-slate-600"
>

View File

@ -1,11 +1,12 @@
import { motion } from 'motion/react';
import { useNavigate } from 'react-router';
import { useNavigate, useParams } from 'react-router';
import { Rocket, Download, Loader2 } from 'lucide-react';
import { useExportPDF } from '../../hooks/useExportPDF';
export default function PlanCTA() {
const { exportPDF, isExporting } = useExportPDF();
const navigate = useNavigate();
const { id } = useParams<{ id: string }>();
return (
<motion.section
@ -37,7 +38,7 @@ export default function PlanCTA() {
<div className="flex flex-col sm:flex-row gap-3 justify-center">
<button
type="button"
onClick={() => navigate('/studio/view-clinic')}
onClick={() => navigate(`/studio/${id || 'live'}`)}
className="inline-flex items-center justify-center gap-2 rounded-full bg-gradient-to-r from-[#4F1DA1] to-[#021341] px-6 py-3 text-sm font-medium text-white shadow-md hover:shadow-lg transition-shadow"
>

View File

@ -1,4 +1,5 @@
import { motion } from 'motion/react';
import { useParams } from 'react-router';
import { TrendingUp, ArrowUpRight, Download, Loader2 } from 'lucide-react';
import { SectionWrapper } from './ui/SectionWrapper';
import { useExportPDF } from '../../hooks/useExportPDF';
@ -6,6 +7,7 @@ import type { KPIMetric } from '../../types/report';
interface KPIDashboardProps {
metrics: KPIMetric[];
clinicName?: string;
}
function isNegativeValue(value: string): boolean {
@ -13,7 +15,8 @@ function isNegativeValue(value: string): boolean {
return lower === '0' || lower.includes('없음') || lower.includes('불가') || lower === 'n/a';
}
export default function KPIDashboard({ metrics }: KPIDashboardProps) {
export default function KPIDashboard({ metrics, clinicName }: KPIDashboardProps) {
const { id } = useParams<{ id: string }>();
const { exportPDF, isExporting } = useExportPDF();
return (
@ -75,14 +78,14 @@ export default function KPIDashboard({ metrics }: KPIDashboardProps) {
</p>
<div className="flex flex-col sm:flex-row items-center justify-center gap-4">
<a
href="/plan/view-clinic"
href={`/plan/${id || 'live'}`}
className="inline-flex items-center gap-2 bg-gradient-to-r from-[#4F1DA1] to-[#021341] text-white font-semibold px-8 py-4 rounded-full hover:shadow-xl transition-all"
>
<ArrowUpRight size={18} />
</a>
<button
onClick={() => exportPDF('INFINITH_Marketing_Report_뷰성형외과')}
onClick={() => exportPDF(`INFINITH_Marketing_Report_${clinicName || 'Report'}`)}
disabled={isExporting}
className="inline-flex items-center gap-2 bg-white border border-slate-200 text-[#021341] font-semibold px-8 py-4 rounded-full hover:bg-slate-50 shadow-sm hover:shadow-md transition-all disabled:opacity-60 disabled:cursor-not-allowed"
>

View File

@ -51,7 +51,7 @@ export function useReport(id: string | undefined): UseReportResult {
}
// Source 2: Fetch from Supabase by report ID (bookmarked/shared link)
if (id && id !== 'view-clinic') {
if (id) {
fetchReportById(id)
.then((row) => {
const transformed = transformApiReport(

View File

@ -137,7 +137,7 @@ export default function ReportPage() {
<RoadmapTimeline months={data.roadmap} />
<KPIDashboard metrics={data.kpiDashboard} />
<KPIDashboard metrics={data.kpiDashboard} clinicName={data.clinicSnapshot.name} />
</div>
</div>
</ScreenshotProvider>