/* 가온 학적부 — 순수 계산 로직. 브라우저와 Node 테스트가 같은 함수를 공유한다. */ (function (global) { "use strict"; const attSummary = (row) => { const days = (row || []).filter((v) => v !== null && v !== undefined); if (!days.length) return "휴학"; const att = days.filter((v) => v === 2).length; return `${Math.round((att / days.length) * 100)}%`; }; // 일별 합계 — marks 는 '행 배열'의 목록이다. v11 사고(행 자체를 세어 0/3) 방지가 이 함수의 존재 이유. const dayTotal = (marks, d) => marks.filter((row) => row && row[d] !== null && row[d] !== undefined).length; const dayPresent = (marks, d) => marks.filter((row) => row && row[d] === 2).length; const counts = (marks) => { const flat = (marks || []).filter(Boolean).flat(); const att = flat.filter((v) => v === 2).length; const late = flat.filter((v) => v === 1).length; const abs = flat.filter((v) => v === 0).length; return { att, late, abs, total: flat.length, rate: flat.length ? Math.round((att / flat.length) * 100) : 0, }; }; // 명단 통계 — 0명 나눗셈 가드 포함 const rosterStats = (students) => { const list = students || []; const active = list.filter((s) => s.status === "재학"); const avgAtt = active.length ? Math.round(active.reduce((a, s) => a + (s.att || 0), 0) / active.length) : 0; const hw = active.reduce( (a, s) => { const [done, total] = String(s.hw || "0/0").split("/").map(Number); return { done: a.done + (done || 0), total: a.total + (total || 0) }; }, { done: 0, total: 0 } ); return { total: list.length, active: active.length, off: list.length - active.length, avgAtt, hwDone: hw.done, hwTotal: hw.total, hwRate: hw.total ? Math.round((hw.done / hw.total) * 100) : 0, }; }; const api = { attSummary, dayTotal, dayPresent, counts, rosterStats }; global.GAON = api; if (typeof module !== "undefined") module.exports = api; })(typeof window !== "undefined" ? window : globalThis);