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
parent
60cd055042
commit
2d6e95c414
|
|
@ -1,12 +1,12 @@
|
||||||
import React from 'react';
|
import { Link } from 'react-router';
|
||||||
|
|
||||||
export default function Footer() {
|
export default function Footer() {
|
||||||
return (
|
return (
|
||||||
<footer className="bg-white border-t border-slate-100 py-12 px-6">
|
<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="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>
|
<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">
|
<div className="text-sm text-slate-500 text-center md:text-left">
|
||||||
© {new Date().getFullYear()} INFINITH. All rights reserved. <br className="md:hidden" />
|
© {new Date().getFullYear()} INFINITH. All rights reserved. <br className="md:hidden" />
|
||||||
Infinite Marketing for Premium Medical Business & Marketing Agency
|
Infinite Marketing for Premium Medical Business & Marketing Agency
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
import React from 'react';
|
import { Link } from 'react-router';
|
||||||
import { motion } from 'motion/react';
|
import { motion } from 'motion/react';
|
||||||
|
|
||||||
export default function Navbar() {
|
export default function Navbar() {
|
||||||
return (
|
return (
|
||||||
<nav className="fixed top-0 left-0 right-0 z-50 bg-white/70 backdrop-blur-lg border-b border-white/20">
|
<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="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>
|
<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">
|
<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="#solution" className="hover:text-primary-900 transition-colors">Solution</a>
|
||||||
<a href="#modules" className="hover:text-primary-900 transition-colors">Modules</a>
|
<a href="#modules" className="hover:text-primary-900 transition-colors">Modules</a>
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,45 @@
|
||||||
import { useLocation, useNavigate } from 'react-router';
|
import { useLocation, useNavigate } from 'react-router';
|
||||||
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
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 = [
|
const PAGE_FLOW = [
|
||||||
{ path: '/', label: '랜딩' },
|
{ prefix: '/', label: '랜딩', exact: true },
|
||||||
{ path: '/report/view-clinic', label: '마케팅 분석' },
|
{ prefix: '/report/', label: '마케팅 분석' },
|
||||||
{ path: '/plan/view-clinic', label: '콘텐츠 기획' },
|
{ prefix: '/plan/', label: '콘텐츠 기획' },
|
||||||
{ path: '/studio/view-clinic', label: '콘텐츠 제작' },
|
{ prefix: '/studio/', label: '콘텐츠 제작' },
|
||||||
{ path: '/channels', label: '채널 연결' },
|
{ prefix: '/channels', label: '채널 연결', exact: true },
|
||||||
{ path: '/distribute', label: '콘텐츠 배포' },
|
{ prefix: '/distribute', label: '콘텐츠 배포', exact: true },
|
||||||
{ path: '/performance', label: '성과 관리' },
|
{ 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() {
|
export default function PageNavigator() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const currentIndex = PAGE_FLOW.findIndex((p) => p.path === location.pathname);
|
const currentIndex = findCurrentIndex(location.pathname);
|
||||||
if (currentIndex === -1) return null;
|
if (currentIndex === -1) return null;
|
||||||
|
|
||||||
const prev = currentIndex > 0 ? PAGE_FLOW[currentIndex - 1] : null;
|
const prev = currentIndex > 0 ? PAGE_FLOW[currentIndex - 1] : null;
|
||||||
|
|
@ -28,7 +52,7 @@ export default function PageNavigator() {
|
||||||
>
|
>
|
||||||
{/* Back */}
|
{/* Back */}
|
||||||
<button
|
<button
|
||||||
onClick={() => prev && navigate(prev.path)}
|
onClick={() => prev && navigate(getNavigatePath(prev.prefix, location.pathname))}
|
||||||
disabled={!prev}
|
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"
|
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">
|
<div className="flex items-center gap-2 px-2">
|
||||||
{PAGE_FLOW.map((page, i) => (
|
{PAGE_FLOW.map((page, i) => (
|
||||||
<button
|
<button
|
||||||
key={page.path}
|
key={page.prefix}
|
||||||
onClick={() => navigate(page.path)}
|
onClick={() => navigate(getNavigatePath(page.prefix, location.pathname))}
|
||||||
title={page.label}
|
title={page.label}
|
||||||
className={`rounded-full transition-all ${
|
className={`rounded-full transition-all ${
|
||||||
i === currentIndex
|
i === currentIndex
|
||||||
|
|
@ -54,7 +78,7 @@ export default function PageNavigator() {
|
||||||
|
|
||||||
{/* Next */}
|
{/* Next */}
|
||||||
<button
|
<button
|
||||||
onClick={() => next && navigate(next.path)}
|
onClick={() => next && navigate(getNavigatePath(next.prefix, location.pathname))}
|
||||||
disabled={!next}
|
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"
|
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"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
import { motion } from 'motion/react';
|
import { motion } from 'motion/react';
|
||||||
import { useNavigate } from 'react-router';
|
import { useNavigate, useParams } from 'react-router';
|
||||||
import { Rocket, Download, Loader2 } from 'lucide-react';
|
import { Rocket, Download, Loader2 } from 'lucide-react';
|
||||||
import { useExportPDF } from '../../hooks/useExportPDF';
|
import { useExportPDF } from '../../hooks/useExportPDF';
|
||||||
|
|
||||||
export default function PlanCTA() {
|
export default function PlanCTA() {
|
||||||
const { exportPDF, isExporting } = useExportPDF();
|
const { exportPDF, isExporting } = useExportPDF();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { id } = useParams<{ id: string }>();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.section
|
<motion.section
|
||||||
|
|
@ -37,7 +38,7 @@ export default function PlanCTA() {
|
||||||
<div className="flex flex-col sm:flex-row gap-3 justify-center">
|
<div className="flex flex-col sm:flex-row gap-3 justify-center">
|
||||||
<button
|
<button
|
||||||
type="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"
|
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"
|
||||||
>
|
>
|
||||||
콘텐츠 제작 시작
|
콘텐츠 제작 시작
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { motion } from 'motion/react';
|
import { motion } from 'motion/react';
|
||||||
|
import { useParams } from 'react-router';
|
||||||
import { TrendingUp, ArrowUpRight, Download, Loader2 } from 'lucide-react';
|
import { TrendingUp, ArrowUpRight, Download, Loader2 } from 'lucide-react';
|
||||||
import { SectionWrapper } from './ui/SectionWrapper';
|
import { SectionWrapper } from './ui/SectionWrapper';
|
||||||
import { useExportPDF } from '../../hooks/useExportPDF';
|
import { useExportPDF } from '../../hooks/useExportPDF';
|
||||||
|
|
@ -6,6 +7,7 @@ import type { KPIMetric } from '../../types/report';
|
||||||
|
|
||||||
interface KPIDashboardProps {
|
interface KPIDashboardProps {
|
||||||
metrics: KPIMetric[];
|
metrics: KPIMetric[];
|
||||||
|
clinicName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isNegativeValue(value: string): boolean {
|
function isNegativeValue(value: string): boolean {
|
||||||
|
|
@ -13,7 +15,8 @@ function isNegativeValue(value: string): boolean {
|
||||||
return lower === '0' || lower.includes('없음') || lower.includes('불가') || lower === 'n/a';
|
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();
|
const { exportPDF, isExporting } = useExportPDF();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -75,14 +78,14 @@ export default function KPIDashboard({ metrics }: KPIDashboardProps) {
|
||||||
</p>
|
</p>
|
||||||
<div className="flex flex-col sm:flex-row items-center justify-center gap-4">
|
<div className="flex flex-col sm:flex-row items-center justify-center gap-4">
|
||||||
<a
|
<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"
|
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} />
|
<ArrowUpRight size={18} />
|
||||||
</a>
|
</a>
|
||||||
<button
|
<button
|
||||||
onClick={() => exportPDF('INFINITH_Marketing_Report_뷰성형외과')}
|
onClick={() => exportPDF(`INFINITH_Marketing_Report_${clinicName || 'Report'}`)}
|
||||||
disabled={isExporting}
|
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"
|
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"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ export function useReport(id: string | undefined): UseReportResult {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Source 2: Fetch from Supabase by report ID (bookmarked/shared link)
|
// Source 2: Fetch from Supabase by report ID (bookmarked/shared link)
|
||||||
if (id && id !== 'view-clinic') {
|
if (id) {
|
||||||
fetchReportById(id)
|
fetchReportById(id)
|
||||||
.then((row) => {
|
.then((row) => {
|
||||||
const transformed = transformApiReport(
|
const transformed = transformApiReport(
|
||||||
|
|
|
||||||
|
|
@ -137,7 +137,7 @@ export default function ReportPage() {
|
||||||
|
|
||||||
<RoadmapTimeline months={data.roadmap} />
|
<RoadmapTimeline months={data.roadmap} />
|
||||||
|
|
||||||
<KPIDashboard metrics={data.kpiDashboard} />
|
<KPIDashboard metrics={data.kpiDashboard} clinicName={data.clinicSnapshot.name} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ScreenshotProvider>
|
</ScreenshotProvider>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue