// cert.jsx — C-01 인증 패키지 빌더
const { Icon, Pill, Tag, Avatar, Button, Progress } = window.RAlly;
const CertPackage = ({ go }) => {
const [tab, setTab] = React.useState('required');
const [openGroups, setOpenGroups] = React.useState({ tech: true, risk: true, sop: false, capa: false });
const [data, setData] = React.useState(null);
const [trackId, setTrackId] = React.useState(null);
const [sub, setSub] = React.useState(null);
React.useEffect(() => {
window.RAlly.resolveTrackId().then((tid) => {
setTrackId(tid);
if (tid) {
fetch('/api/track/' + tid + '/cert').then((r) => r.json()).then(setData).catch(() => {});
fetch('/api/track/' + tid + '/submission').then((r) => r.json()).then(setSub).catch(() => {});
}
});
}, []);
const toggle = (k) => setOpenGroups((p) => ({ ...p, [k]: !p[k] }));
const cd = data || { groups: [], total: 0, done: 0, pct: 0, missing: 0 };
const trackName = (cd.track && cd.track.name) || '트랙';
return (
{/* Header */}
STAGE 04 · 결승선
2등급 의료기기 + SaMD
MFDS 품목허가
결승선까지 같이 달려요
{trackName} · MFDS 품목허가 패키지
{/* Coral banner with stat + checklist preview */}
필수 자료 {cd.done}개 완료, {cd.missing}개 미비
완주하지 않은 문서를 채우면 제출 패키지가 자동으로 생성됩니다.
아직 {cd.missing}개가 남아 있어요.
NECESSARY DOCS
{cd.done}/{cd.total}
{cd.pct}% — {cd.pct >= 90 ? '결승선 직전' : '주행 중'}
{/* 제출 준비 패키지 — 심사구분×pillar 체크리스트 + 게이트 + 경로 (실데이터) */}
{/* Track-style milestone strip (데모 전용) */}
{window.RAlly.isDemo() && (
)}
{/* Tabs */}
{/* Groups (백엔드 집계) */}
{tab === 'required' && (
{cd.groups.map((g) => (
toggle(g.id)}
rows={g.rows}
/>
))}
)}
{tab === 'optional' && (
선택 자료 6/8 — 이 탭의 모킹은 생략되었어요.
)}
{tab === 'form' && (
MFDS 신청서 양식 — 이 탭의 모킹은 생략되었어요.
)}
{tab === 'history' && (
제출 이력 — 첫 제출 전이라 비어 있어요.
)}
{/* Bottom action banner — 실제 게이트 기준 */}
{sub && sub.gate && sub.gate.submittable
? '문서 세트가 준비됐어요 — 패키지를 내려받아 제출하세요'
: ((sub && sub.gate && sub.gate.summary) || '미완 문서를 채우면 제출 패키지가 자동 생성돼요')}
ZIP · 카테고리별 문서 + 요건 매핑 표. 실측·서명 등 [입력 필요] 값은 실무자가 채웁니다.
);
};
// 제출 준비 패키지 — 실데이터(/api/track/{id}/submission)
const SubmissionPanel = ({ sub, go }) => {
if (!sub || !sub.ok) return null;
const gate = sub.gate || {};
const ok = gate.submittable;
return (
{/* 게이트 요약 */}
{ok ? '제출 준비 완료 — 최종 RA 검토 후 제출' : '제출 전 남은 항목이 있습니다'}
{gate.summary || '문서·입력 상태를 점검하세요.'}
{(sub.pillars || []).map((p) =>
{p})}
심사구분 · {sub.review_type}
{/* 제출 경로 안내 */}
제출 경로 안내
{(sub.routes || []).map((r, i) => (
))}
{/* pillar별 제출문서 체크리스트 */}
{(sub.groups || []).map((g) => (
{g.pillar} 기둥
{g.ready}/{g.total} 준비완료
{g.docs.map((d) => (
{d.done ? : }
{d.label}
{d.inputs > 0 &&
입력 {d.inputs}}
{d.state}
))}
))}
{sub.note}
);
};
const Milestones = () => {
const items = [
{ label: '내부 검토 완료', date: '2026.06.15', state: 'done' },
{ label: '자문 RA 승인', date: '2026.07.10', state: 'done' },
{ label: 'GMP 적합성 평가', date: '2026.08.01', state: 'current' },
{ label: 'MFDS 사전 상담', date: '2026.08.20', state: 'todo' },
{ label: '품목허가 신청', date: '2026.09.30', state: 'todo', finish: true },
];
return (
{items.map((m, i) => (
{m.state === 'done' && }
{m.finish && m.state === 'todo' && }
{m.label}
{m.date}
))}
);
};
const CertGroup = ({ title, count, tone, open, onToggle, rows }) => (
{title}
{count}
{rows.length}개 항목
{open && (
{rows.map((r, i) => (
))}
)}
);
const CertRow = ({ state, title, meta, cta }) => {
const isMissing = state === 'missing';
return (
{isMissing ? : }
{cta ? (
) : (
PDF
)}
);
};
window.RAlly.CertPackage = CertPackage;