/* blueyetutor — /library * Tabs: 파일별 / 단원별 * - 파일별: subject filter → expandable PDF list (rename/delete, MCQ grid + FRQ list, unit editor per problem) * - 단원별: subject + MCQ/FRQ + unit → pooled grid across PDFs (also unit-editable) * * Assumed endpoints: * GET /api/pdfs → PdfIndexRow[] * GET /api/pdfs/{pdf_id}/problems → Problem[] * PATCH /api/pdfs/{pdf_id} { display_name } * DELETE /api/pdfs/{pdf_id} * PATCH /api/problems/{pdf_id}/{qtype}/{qnum}/units { units } * GET /api/subjects → [name] * GET /api/units?subject=&qtype= → [{unit, count}] * GET /api/problems/browse?subject=&qtype=&units= */ const { useState: libUseState, useMemo: libUseMemo, useEffect: libUseEffect } = React; function LibraryPage() { const store = useStore(); const [view, setView] = libUseState("file"); return (
Library

처리한 모든 파일을 한 자리에

{store.pdfs.length === 0 ? ( location.hash = "#/upload"}>업로드로 이동} /> ) : ( <>
{store.pdfs.length} files · {store.problems.length} problems
{view === "file" ? : } )}
); } // ---------- 파일별 ---------- function LibraryByFile() { const store = useStore(); const subjects = ["전체", ...new Set(store.pdfs.map(d => d.subject || "(미분류)"))]; const [sel, setSel] = libUseState("전체"); const [q, setQ] = libUseState(""); const view = sel === "전체" ? store.pdfs : store.pdfs.filter(d => (d.subject || "(미분류)") === sel); const needle = q.trim().toLowerCase(); const nameOf = d => (d.display_name || d.pdf_id || "").toLowerCase(); const shown = needle ? view.filter(d => nameOf(d).includes(needle)) : view; return (
{/* subject filter */}
{subjects.map(s => ( ))}
{/* aggregate metrics */}
s + d.mcq, 0)} /> s + d.frq, 0)} /> s + d.labeled, 0)} highlight />
{/* filename search */}
setQ(e.target.value)} /> {needle && {shown.length} / {view.length}}
{/* flat list */}
{shown.length === 0 ? (
{needle ? `"${q}" 와 일치하는 파일이 없어요.` : "표시할 파일이 없어요."}
) : shown.map(d => )}
); } function LibraryPdfRow({ pdf }) { const store = useStore(); const [open, setOpen] = libUseState(false); const [editMode, setEditMode] = libUseState(false); // ← 핵심: row-level 수정 모드 const [confirmDel, setConfirmDel] = libUseState(false); const [name, setName] = libUseState(pdf.display_name || pdf.pdf_id); const probs = libUseMemo(() => store.problems.filter(p => p.pdf_id === pdf.pdf_id), [store.problems, pdf.pdf_id]); const mcqs = probs.filter(p => p.qtype === "MCQ"); const frqs = probs.filter(p => p.qtype === "FRQ"); const onEditAnswer = p => async v => window.storeActions.setProblemAnswer(p.id, v); const onEditSubject = p => async s => window.storeActions.setProblemSubject(p.id, s); return (
setOpen(!open)}> {open ? : } {pdf.display_name || pdf.pdf_id} {/* 과목 Badge — 수정 모드면 클릭 가능한 trigger 로 바뀐다. */} {editMode ? : {pdf.subject || "(미분류)"}} MCQ {pdf.mcq} · FRQ {pdf.frq} · 라벨 {pdf.labeled}
{pdf.mcq + pdf.frq === 0 && 미지원 / 0문제} {open && ( )}
{open && (
{/* rename + delete — 단순 운영 / 항상 노출 */}
setName(e.target.value)} /> {confirmDel ? ( ) : ( )}
{pdf.pdf_id}
{editMode && ( 수정 모드 · 과목 / 답 / 단원을 모두 직접 바꿀 수 있어요. 답 / 단원은 카드 안에서 직접 클릭. 마치면 우상단 "수정 종료". )} {mcqs.length > 0 && (

MCQ ({mcqs.length})

{mcqs.map(p => ( window.storeActions.setProblemUnits(p.pdf_id, p.qtype, p.qnum, u)} /> ))}
)} {frqs.length > 0 && (

FRQ ({frqs.length})

{frqs.map(p => ( window.storeActions.setProblemUnits(p.pdf_id, p.qtype, p.qnum, u)} /> ))}
)}
)}
); } // 과목 Badge 가 곧 trigger — 클릭 시 Modal 열려 6 과목 중 선택. // editMode 일 때만 LibraryPdfRow 헤더가 이 컴포넌트를 그린다. function PdfSubjectBadgeEditor({ pdf }) { const [open, setOpen] = libUseState(false); const [next, setNext] = libUseState(pdf.subject || ""); const [busy, setBusy] = libUseState(false); const all = Object.keys(TAXONOMY); return ( <> setOpen(false)} footer={<> }>
현재: {pdf.subject || "(미분류)"}
이 PDF 의 모든 문제 ({(pdf.mcq || 0) + (pdf.frq || 0)}개) 의 과목 라벨을 한 번에 바꿉니다. 단원 라벨은 그대로 — 다음 라벨링 때 새 과목 기준으로 채워집니다.
); } // ---------- 단원별 ---------- function LibraryByUnit() { const store = useStore(); const subjects = Object.keys(TAXONOMY).filter(s => store.pdfs.some(d => d.subject === s)); const [subject, setSubject] = libUseState(subjects[0] || ""); const [qtype, setQtype] = libUseState("MCQ"); const counts = libUseMemo(() => window.unitsFor(subject, qtype), [subject, qtype, store.problems]); const ordered = (TAXONOMY[subject] || []).filter(u => counts[u]); const [unit, setUnit] = libUseState(ordered[0] || ""); React.useEffect(() => { setUnit(ordered[0] || ""); }, [subject, qtype]); const problems = libUseMemo(() => window.browseProblems(subject, qtype, unit ? [unit] : []), [subject, qtype, unit, store.problems]); const names = Object.fromEntries(store.pdfs.map(p => [p.pdf_id, p.display_name || p.pdf_id])); if (subjects.length === 0) { return ; } return (
{ordered.length === 0 ? ( 이 과목·{qtype} 에 라벨된 단원이 없습니다. ) : ( <>
{unit} — {qtype} · {problems.length} 문제 (전 파일 횡단)
{/* 단원별 grid 는 표시만 — 단원/답 수정은 "파일별" 탭의 수정 모드에서 (박현우 명시). */} {problems.map(p => ( ))}
)}
); } Object.assign(window, { LibraryPage });