// 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("── 가온 v19: 채점 검증 ──"); t("validateScore: 정상 92 → ok·값 반환", () => assert.deepEqual(GAON.validateScore("92"), { ok: true, value: 92 })); t("validateScore: 경계 0·100 → ok", () => { assert.equal(GAON.validateScore("0").ok, true); assert.equal(GAON.validateScore("100").ok, true); }); t("validateScore: 경계 밖 101·-1 → range", () => { assert.equal(GAON.validateScore("101").reason, "range"); assert.equal(GAON.validateScore("-1").reason, "range"); }); t("validateScore: 빈 칸·공백 → empty", () => { assert.equal(GAON.validateScore("").reason, "empty"); assert.equal(GAON.validateScore(" ").reason, "empty"); }); t("validateScore: 숫자 아님 → nan", () => assert.equal(GAON.validateScore("9점").reason, "nan")); t("validateScore: 소수 88.5 → ok", () => assert.equal(GAON.validateScore("88.5").value, 88.5)); console.log("── 두레 v19: 신청 판정 ──"); t("canEnroll: 정상 → ok", () => assert.deepEqual(DURE.canEnroll({ name: "논리와 토론", seats: 5, days: [2], start: 16, end: 18, credits: 2 }, [], 15, 18), { ok: true })); t("canEnroll: 정원 마감 → full 이 우선(겹침보다 먼저)", () => assert.equal(DURE.canEnroll({ name: "드로잉", seats: 0, days: [3], start: 14, end: 17, credits: 3 }, [{ name: "생명과학", days: [3], start: 15, end: 16 }], 0, 18).reason, "full")); t("canEnroll: 겹침 → 겹친 과목명 반환", () => { const r = DURE.canEnroll({ name: "생명과학 산책", seats: 3, days: [1], start: 14, end: 16, credits: 2 }, [{ name: "미시경제학", days: [1], start: 13, end: 14.5 }], 0, 18); assert.equal(r.reason, "overlap"); assert.deepEqual(r.names, ["미시경제학"]); }); t("canEnroll: 학점 초과 경계 16+3 → credits", () => assert.equal(DURE.canEnroll({ name: "문학과 사회", seats: 9, days: [1, 3], start: 10, end: 11.5, credits: 3 }, [], 16, 18).reason, "credits")); t("canEnroll: 15+3 → 통과", () => assert.equal(DURE.canEnroll({ name: "문학과 사회", seats: 9, days: [1, 3], start: 10, end: 11.5, credits: 3 }, [], 15, 18).ok, true)); console.log(`\n${failed ? `${failed} FAIL` : "UNIT ALL PASS"} (${passed} passed)`); process.exit(failed ? 1 : 0);