o2o-infinith-frontend/src/features/clinics/components/cards/AnalysisCard.tsx

125 lines
5.3 KiB
TypeScript

/**
* AnalysisCard — 워크스페이스 단일 분석 아이템 카드.
*
* 한 카드에 리포트 + 플랜이 같이 묶입니다.
* - 카드 본문 클릭 → 리포트 보기 (기본 동작)
* - [리포트 보기] / [플랜 보기 | + 플랜 만들기] 명시적 버튼
* - 분석 대상 플랫폼 칩(URL/핸들) 표시
*/
import { Link, useNavigate } from 'react-router';
import { Calendar, ArrowUpRight } from 'lucide-react';
import { AppIcon } from '@/shared/icons/AppIcon';
import { PlatformChips } from '../PlatformChips';
import type { WorkspacePlan, WorkspaceRun } from '../../types/workspace';
function statusBadge(status: WorkspaceRun['status']) {
const map = {
completed: { label: '완료', cls: 'bg-status-good-bg text-status-good-text border-status-good-border' },
running: { label: '진행 중', cls: 'bg-status-info-bg text-status-info-text border-status-info-border' },
queued: { label: '대기', cls: 'bg-slate-50 text-slate-500 border-slate-200' },
failed: { label: '실패', cls: 'bg-status-critical-bg text-status-critical-text border-status-critical-border' },
} as const;
return map[status];
}
function formatScore(score: number | null) {
return score == null ? '—' : String(Math.round(score));
}
function formatDate(iso: string) {
const d = new Date(iso);
return `${d.getFullYear()}.${String(d.getMonth() + 1).padStart(2, '0')}.${String(d.getDate()).padStart(2, '0')}`;
}
interface AnalysisCardProps {
clinicId: string;
run: WorkspaceRun;
/** plan 이 있으면 1:1 매칭, 없으면 plan 미생성 상태 */
plan?: WorkspacePlan;
/** 최신 row 강조 */
highlighted?: boolean;
}
export function AnalysisCard({ clinicId, run, plan, highlighted = false }: AnalysisCardProps) {
const navigate = useNavigate();
const status = statusBadge(run.status);
// PlanPage 는 두 경로 모두에서 동일 — 워크스페이스 경로로 진입해야 향후 권한/액션 차별화 여지 확보.
void plan?.planId;
const reportHref = `/report/${run.runId}`;
const planHref = `/clinics/${clinicId}/plan/${run.runId}`;
const goToReport = () => navigate(reportHref);
const stop = (e: React.MouseEvent) => e.stopPropagation();
return (
<article
onClick={goToReport}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
goToReport();
}
}}
role="button"
tabIndex={0}
className="group cursor-pointer rounded-2xl border border-slate-100 bg-white transition-all hover:border-[#D5CDF5] hover:shadow-[3px_4px_18px_rgba(79,29,161,0.08)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-purple/30"
>
<div className="flex flex-col md:flex-row md:items-stretch">
{/* Score block */}
<div className="flex md:flex-col items-center justify-center gap-3 md:gap-1 px-6 py-5 md:py-6 md:w-32 flex-shrink-0 border-b md:border-b-0 md:border-r border-slate-100">
<div className="flex items-baseline gap-0.5">
<span className="font-serif text-4xl font-bold text-[#0A1128] leading-none">
{formatScore(run.overallScore)}
</span>
<span className="text-sm text-slate-400">/100</span>
</div>
</div>
{/* Center: meta + targets */}
<div className="flex-1 min-w-0 px-6 py-5 flex flex-col justify-center gap-3">
<div className="flex items-center gap-2 flex-wrap">
<span className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-full border text-[10px] font-medium ${status.cls}`}>
<span className="w-1 h-1 rounded-full bg-current opacity-60" aria-hidden />
{status.label}
</span>
<span className="inline-flex items-center gap-1 text-xs text-slate-500">
<Calendar size={11} className="text-slate-400" />
{formatDate(run.startedAt)}
</span>
{highlighted && (
<span className="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-semibold bg-brand-purple text-white">
</span>
)}
</div>
<PlatformChips targets={run.targets} />
</div>
{/* Right: actions */}
<div className="flex md:flex-col items-stretch justify-center gap-2 px-6 py-5 md:py-6 md:w-48 flex-shrink-0 md:border-l md:border-slate-100">
<Link
to={reportHref}
onClick={stop}
className="inline-flex items-center justify-center gap-1.5 flex-1 md:flex-none px-4 py-2 rounded-full text-xs font-semibold text-white bg-gradient-to-r from-[#4F1DA1] to-[#021341] shadow-sm hover:opacity-90 transition-all"
>
<AppIcon kind="report" size={12} />
<ArrowUpRight size={11} className="opacity-80" />
</Link>
<Link
to={planHref}
onClick={stop}
className="inline-flex items-center justify-center gap-1.5 flex-1 md:flex-none px-4 py-2 rounded-full text-xs font-medium text-brand-purple bg-brand-tint-purple/60 border border-brand-tint-lavender hover:bg-brand-tint-purple transition-colors"
>
<AppIcon kind="plan" size={12} />
</Link>
</div>
</div>
</article>
);
}