/* blueyetutor — /history * Workbook history. Card list, expand to view answer key (manifest items) * + re-download. Answer key is recoverable from manifest even if the * printed PDF was generated with `answers_included=false`. * * Assumed endpoints: * GET /api/workbooks → WbHistory[] * GET /api/workbooks/{id}/pdf → file download */ const { useState: histUseState } = React; function HistoryPage() { const store = useStore(); const rows = store.history; return (
History

지금까지 만든 문제집

생성한 모든 문제집·Mock Exam·오답노트. 답지는 PDF 미포함이어도 여기서 항상 확인 가능합니다.

{rows.length === 0 ? ( location.hash = "#/workbook"}>문제집 만들러 가기} /> ) : (
{rows.map(w => )}
)}
); } function HistoryRow({ w }) { const [open, setOpen] = histUseState(false); const [keyOpen, setKeyOpen] = histUseState(!w.answers_included); const [assignOpen, setAssignOpen] = histUseState(false); const [sEmail, setSEmail] = histUseState(""); const [sTitle, setSTitle] = histUseState(w.file_name || "과제"); const [aBusy, setABusy] = histUseState(false); const [aMsg, setAMsg] = histUseState(null); // {ok, text} const doAssign = async () => { const email = sEmail.trim().toLowerCase(); if (!/\S+@\S+\.\S+/.test(email)) { setAMsg({ ok: false, text: "이메일 형식이 올바르지 않습니다." }); return; } setABusy(true); setAMsg(null); const r = await window.storeActions.assignWorkbook(w.id, email, sTitle.trim() || w.file_name); setABusy(false); if (r && r.ok) setAMsg({ ok: true, text: `${email} 에게 배정했습니다 (MCQ ${r.n_mcq} · FRQ ${r.n_frq}). 학생은 이 이메일로 가입하면 됩니다.` }); else setAMsg({ ok: false, text: (r && r.message) || "배정에 실패했습니다." }); }; return (
setOpen(!open)}> {open ? : } {w.created_at} {w.subject || "?"} {w.qtype || "?"} {w.n || "?"}문제 {w.answers_included ? 답지 포함 : 답지 없음}
setAssignOpen(false)} footer={<> }>
이 문제집을 학생 이메일로 보냅니다. 학생은 그 이메일로 가입하면 '내 과제'에서 풀고 MCQ가 자동 채점됩니다.
setSEmail(e.target.value)} placeholder="student@example.com" /> setSTitle(e.target.value)} maxLength={80} /> {aMsg && {aMsg.text}}
{open && (
단원: {w.units || "-"}  ·  파일: {w.file_name}
{keyOpen && }
)}
); } function HistoryAnswers({ manifest }) { const items = (manifest && manifest.items) || []; const mcq = items.filter(it => it.qtype === "MCQ"); const frq = items.filter(it => it.qtype === "FRQ"); if (items.length === 0) return
(manifest 없음 — 이 기록은 답 재현 불가)
; return (
{mcq.length > 0 && (

MCQ 정답

{mcq.map(it => ( ))}
문항정답출처
{it.n} {it.answer || "?"} {it.pdf_id} Q{it.qnum}
)} {frq.length > 0 && (

FRQ scoring guide

{frq.map(it => (
FRQ {it.n} — {it.pdf_id} Q{it.qnum}
))}
)}
); } // Lazily fetch a finished workbook's FRQ scoring-guide crops for "답 확인". function ScoringImgs({ pdfId, qnum }) { const [urls, setUrls] = histUseState(null); React.useEffect(() => { let live = true; fetch(`/api/scoring/${encodeURIComponent(pdfId)}/${qnum}`, { credentials: "include" }) .then(r => r.ok ? r.json() : { urls: [] }) .then(d => { if (live) setUrls(d.urls || []); }) .catch(() => { if (live) setUrls([]); }); return () => { live = false; }; }, [pdfId, qnum]); if (urls === null) return
scoring 불러오는 중…
; if (urls.length === 0) return
(scoring 크롭 없음)
; return (
{urls.map((u, i) => )}
); } Object.assign(window, { HistoryPage, HistoryRow, HistoryAnswers });