101 lines
3.4 KiB
TypeScript
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>
|
|
);
|
|
}
|