// L1 단위 테스트 — 계산 로직 경계값. 브라우저 없이 Node 에서 직접 실행. import assert from "node:assert/strict"; import fs from "node:fs"; import path from "node:path"; import url from "node:url"; const here = path.dirname(url.fileURLToPath(import.meta.url)); const load = (rel) => { const m = {}; new Function("module", fs.readFileSync(path.join(here, rel), "utf8"))(m); return m.exports; }; const GAON = load("../../public/work/gaon-lms/calc.js"); const DURE = load("../../public/work/dure-enrollment/calc.js"); let passed = 0, failed = 0; const t = (name, fn) => { try { fn(); passed++; console.log("PASS " + name); } catch (e) { failed++; console.log("FAIL " + name + " — " + e.message); } }; console.log("── 가온 ──"); t("attSummary: 전원 출석 → 100%", () => assert.equal(GAON.attSummary([2, 2, 2, 2, 2]), "100%")); t("attSummary: 혼합 4/5 → 80%", () => assert.equal(GAON.attSummary([2, 2, 2, 2, 1]), "80%")); t("attSummary: 빈 행 → 휴학", () => assert.equal(GAON.attSummary([]), "휴학")); t("attSummary: null/undefined 무시", () => assert.equal(GAON.attSummary([2, null, 2, undefined, 0]), "67%")); t("attSummary: 1/3 반올림 → 33%", () => assert.equal(GAON.attSummary([2, 0, 0]), "33%")); t("dayTotal/dayPresent: 행 배열 목록 (0/3 사고 회귀)", () => { const marks = [[2, 2, 2, 2, 2], [2, 1, 2, 2, 2], null]; assert.equal(GAON.dayTotal(marks, 0), 2); assert.equal(GAON.dayPresent(marks, 0), 2); assert.equal(GAON.dayPresent(marks, 1), 1); // 지각 1명은 출석 아님 }); t("dayPresent: 전원 결석일 → 0", () => assert.equal(GAON.dayPresent([[0, 0], [0, 0]], 0), 0)); t("dayTotal: 전부 null 행 → 0 (나눗셈 가드)", () => assert.equal(GAON.dayTotal([null, null], 0), 0)); t("counts: 집계 정합", () => { const c = GAON.counts([[2, 1, 0], [2, 2, 2]]); assert.deepEqual([c.att, c.late, c.abs, c.total, c.rate], [4, 1, 1, 6, 67]); }); t("counts: 빈 입력 → total 0, rate 0 (NaN 아님)", () => { const c = GAON.counts([]); assert.equal(c.rate, 0); assert.equal(c.total, 0); assert.ok(!Number.isNaN(c.rate)); }); t("rosterStats: 0명 명단 → 0/NaN 없음", () => { const s = GAON.rosterStats([]); assert.deepEqual([s.total, s.active, s.avgAtt, s.hwRate], [0, 0, 0, 0]); }); t("rosterStats: 재학만 통계에 포함", () => { const s = GAON.rosterStats([ { status: "재학", att: 90, hw: "8/8" }, { status: "재학", att: 70, hw: "6/8" }, { status: "휴학", att: 0, hw: "0/8" }, ]); assert.equal(s.active, 2); assert.equal(s.avgAtt, 80); assert.deepEqual([s.hwDone, s.hwTotal, s.hwRate], [14, 16, 88]); }); t("rosterStats: 홀수 반올림", () => assert.equal(GAON.rosterStats([{ status: "재학", att: 91, hw: "1/3" }]).avgAtt, 91)); console.log("── 두레 ──"); t("isOverlap: 같은 요일 겹침 → true", () => assert.equal(DURE.isOverlap({ days: [1, 3], start: 10, end: 11.5 }, { days: [3], start: 11, end: 12 }), true)); t("isOverlap: 닿는 경계(끝==시작) → false", () => assert.equal(DURE.isOverlap({ days: [1], start: 10, end: 12 }, { days: [1], start: 12, end: 14 }), false)); t("isOverlap: 다른 요일 같은 시간 → false", () => assert.equal(DURE.isOverlap({ days: [1], start: 10, end: 12 }, { days: [2], start: 10, end: 12 }), false)); t("isOverlap: 동일 과목 → true", () => assert.equal(DURE.isOverlap({ days: [2, 4], start: 13, end: 14.5 }, { days: [4, 2], start: 13, end: 14.5 }), true)); t("isOverlap: 부분 겹침 → true", () => assert.equal(DURE.isOverlap({ days: [5], start: 9, end: 12 }, { days: [5], start: 11, end: 13 }), true)); t("credits: 합계", () => assert.equal(DURE.credits([{ credits: 3 }, { credits: 2 }]), 5)); t("credits: 빈 목록 → 0", () => assert.equal(DURE.credits([]), 0)); t("credits: credits 없는 항목 → 0 취급", () => assert.equal(DURE.credits([{}, { credits: 3 }]), 3)); t("wouldExceed: 15+3 ≤ 18 → false", () => assert.equal(DURE.wouldExceed(15, 3, 18), false)); t("wouldExceed: 16+3 > 18 → true (경계)", () => assert.equal(DURE.wouldExceed(16, 3, 18), true)); t("wouldExceed: 18+0 → false (경계)", () => assert.equal(DURE.wouldExceed(18, 0, 18), false)); t("seatsState: 만석 → 정원 마감", () => assert.deepEqual(DURE.seatsState({ seats: 0 }), { label: "정원 마감", closed: true, near: false })); t("seatsState: 여석 5 → 임박", () => assert.ok(DURE.seatsState({ seats: 5 }).near)); t("seatsState: 여석 6 → 임박 아님 (경계)", () => assert.ok(!DURE.seatsState({ seats: 6 }).near)); t("remaining: 정상 잔여", () => assert.equal(DURE.remaining(130, 94, 3), 33)); t("remaining: 초과 → 음수", () => assert.equal(DURE.remaining(24, 21, 6), -3)); console.log(`\n${failed ? `${failed} FAIL` : "UNIT ALL PASS"} (${passed} passed)`); process.exit(failed ? 1 : 0);