o2o-infinith-frontend/src/features/plan/pages/UserPlanPage.tsx

118 lines
4.4 KiB
TypeScript

/**
* UserPlanPage — `/clinics/:clinicId/plan/:id`
*
* 계약된 병원 유저가 워크스페이스에서 운영하는 마케팅 기획 화면.
* GuestPlanPage 의 본문 + 워크스페이스 액션바 + 인터랙티브 섹션
* (MyAssetUpload / StrategyAdjustmentSection / WorkflowTracker).
*/
import { useEffect } from 'react';
import { Link, useParams, useLocation } from 'react-router';
import { ArrowLeft, FileSearch } from 'lucide-react';
import { useMarketingPlan } from '../hooks/useMarketingPlan';
import { ReportNav } from '@/features/report/components/ReportNav';
import { PdfDownloadButton } from '@/features/report/components/PdfDownloadButton';
import { PLAN_SECTIONS } from '@/shared/constants/planSections';
import PlanBody from '../components/PlanBody';
import MyAssetUpload from '../components/MyAssetUpload';
import StrategyAdjustmentSection from '../components/StrategyAdjustmentSection';
import WorkflowTracker from '../components/WorkflowTracker';
export default function UserPlanPage() {
const { clinicId, id } = useParams<{ clinicId: string; id: string }>();
const location = useLocation();
const stateClinicId = (location.state as { clinicId?: string } | undefined)?.clinicId || null;
const { data, isLoading, error } = useMarketingPlan(id);
useEffect(() => {
if (isLoading || !location.hash) return;
const sectionId = location.hash.slice(1);
const timer = setTimeout(() => {
const el = document.getElementById(sectionId);
if (!el) return;
const STICKY_OFFSET = 128;
const y = el.getBoundingClientRect().top + window.scrollY - STICKY_OFFSET;
window.scrollTo({ top: y, behavior: 'smooth' });
}, 300);
return () => clearTimeout(timer);
}, [isLoading, location.hash]);
if (isLoading) {
return (
<div className="min-h-screen flex items-center justify-center pt-20">
<div className="flex flex-col items-center gap-4">
<div className="w-10 h-10 border-4 border-[#6C5CE7] border-t-transparent rounded-full animate-spin" />
<p className="text-slate-500 text-sm"> ...</p>
</div>
</div>
);
}
if (error || !data) {
return (
<div className="min-h-screen flex items-center justify-center pt-20">
<div className="text-center">
<p className="text-[#7C3A4B] font-medium mb-2"> </p>
<p className="text-slate-500 text-sm">{error ?? '마케팅 기획을 찾을 수 없습니다.'}</p>
</div>
</div>
);
}
// baseRunId(이 플랜의 베이스 리포트) — 데이터 모델에 정식 필드 있으면 그걸 우선
const baseRunId =
(data as unknown as { baseReportId?: string }).baseReportId ||
(data as unknown as { reportId?: string }).reportId ||
null;
// 데모 환경: baseRunId 가 없으면 같은 plan id 로 리포트도 존재 (1:1 매핑)
const reportTargetId = baseRunId ?? id;
return (
<div className="pt-20">
<ReportNav
sections={PLAN_SECTIONS}
leftSlot={
<Link
to={`/clinics/${clinicId}`}
className="inline-flex items-center gap-1.5 text-xs font-medium text-slate-500 hover:text-primary-900 transition-colors"
>
<ArrowLeft size={14} />
</Link>
}
rightSlot={
<div className="flex items-center gap-2">
<PdfDownloadButton filename={`${data.clinicName}_Marketing_Plan`} />
{reportTargetId && (
<Link
to={`/clinics/${clinicId}/report/${reportTargetId}`}
className="inline-flex items-center gap-1.5 px-3.5 py-2 rounded-full text-xs font-medium text-slate-600 bg-slate-50 border border-slate-200 hover:bg-white hover:border-slate-300 transition-all"
>
<FileSearch size={12} />
</Link>
)}
</div>
}
/>
<PlanBody data={data} />
{/* 유저 전용 인터랙티브 섹션 */}
{data.workflow && (
<div data-no-print>
<WorkflowTracker data={data.workflow} />
</div>
)}
<div data-no-print>
<MyAssetUpload />
</div>
<div data-no-print>
<StrategyAdjustmentSection clinicId={clinicId ?? stateClinicId} planId={data.id} />
</div>
</div>
);
}