/* 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 (
{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 });