125 lines
5.3 KiB
TypeScript
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>
|
|
);
|
|
}
|