118 lines
4.4 KiB
TypeScript
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>
|
|
);
|
|
}
|