o2o-infinith-frontend/src/features/clinics/components/tabs/AnalysisTab.tsx

101 lines
3.4 KiB
TypeScript

/**
* AnalysisTab — 워크스페이스 분석 탭 (리포트+플랜 통합).
* 각 분석 run = 1:1 plan. 카드 본문 클릭은 리포트, 옆 버튼은 플랜.
*
* 리스트는 무한스크롤 — 서버가 전체 배열을 던지면 클라이언트에서 10개씩 잘라 노출.
*/
import { useMemo } from 'react';
import { Plus } from 'lucide-react';
import { Link } from 'react-router';
import { SectionWrapper } from '@/features/report/components/ui/SectionWrapper';
import { EmptyState } from '@/shared/ui/empty-state';
import { useInfiniteList } from '@/shared/hooks/useInfiniteList';
import { AnalysisCard } from '../cards/AnalysisCard';
import type { WorkspacePlan, WorkspaceRun } from '../../types/workspace';
interface AnalysisTabProps {
clinicId: string;
runs: WorkspaceRun[];
plans: WorkspacePlan[];
}
const PAGE_SIZE = 10;
export function AnalysisTab({ clinicId, runs, plans }: AnalysisTabProps) {
const planByRun = useMemo(() => {
const map: Record<string, WorkspacePlan> = {};
const priority = { active: 3, draft: 2, archived: 1 } as const;
for (const plan of plans) {
const prev = map[plan.baseRunId];
if (!prev || priority[plan.status] > priority[prev.status]) {
map[plan.baseRunId] = plan;
}
}
return map;
}, [plans]);
// plan 이 없는 run 도 노출 — 백엔드 plans 엔드포인트가 채워지면 자연스럽게 매칭됨.
const rows = useMemo(
() =>
[...runs]
.sort((a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime())
.map((run) => ({ run, plan: planByRun[run.runId] })),
[runs, planByRun],
);
const { visible, hasMore, sentinelRef } = useInfiniteList(rows, {
initialSize: PAGE_SIZE,
pageSize: PAGE_SIZE,
});
return (
<SectionWrapper
id="analysis"
title="분석 리포트"
subtitle={`${rows.length}건 · 카드 본문 클릭은 리포트 보기, 옆 버튼으로 해당 분석의 플랜으로 이동합니다.`}
>
<div className="flex justify-end mb-5">
<Link
to="/report/loading"
className="inline-flex items-center gap-1.5 px-5 py-2.5 rounded-full text-xs font-semibold text-white bg-gradient-to-r from-[#4F1DA1] to-[#021341] shadow-sm hover:opacity-90 transition-all"
>
<Plus size={14} />
</Link>
</div>
{rows.length === 0 ? (
<EmptyState
size="lg"
hint="설정 탭에서 분석 대상을 등록하고, 상단의 '새 분석 시작' 을 눌러 첫 리포트를 받아보세요."
/>
) : (
<>
<div className="space-y-3">
{visible.map(({ run, plan }, i) => (
<AnalysisCard
key={run.runId}
clinicId={clinicId}
run={run}
plan={plan}
highlighted={i === 0}
/>
))}
</div>
{hasMore && (
<div
ref={sentinelRef}
className="flex items-center justify-center py-8 text-xs text-slate-400"
>
<div className="flex items-center gap-2">
<div className="w-3 h-3 border-2 border-slate-300 border-t-brand-purple rounded-full animate-spin" />
...
</div>
</div>
)}
</>
)}
</SectionWrapper>
);
}