- 핵심 규칙 6: 완료의 정의는 게이트 통과. 사용자를 QA로 쓰지 않는다 - 5단계 기계 검사 4번째: design-gate 실행 의무화 - references/audit-gate.md: 사고-검사 매핑, SEO/meta 체크리스트, OS/브라우저 특성, 하니스 규칙 - tools/design-gate.mjs: 범용 게이트(메타/SEO·대비·수축·리듬·트랙·스케일×폭 + 옵션 L0/L3/L4, checks 깊은 병합) - 리듬·트랙 불변식: 숫자 라벨 등폭·빈 셀 트랙 균일·등간격 — 시간표 자동배치 결함 재현 픽스처 검출, 앱 15뷰 통과(오탐 0) feat(site): 관리 앱 2종 프로덕션 콘솔(v6→v18) - 가온 학적부 9뷰·두레 수강신청 6뷰: shadcn 문법, Pretendard/Noto Serif/IBM Plex 폰트 전략, 볼드 금지(400/500/600), WCAG AA 대비 전면 교정, 한글 keep-all 조판 - LMS 필수 요소(알림 센터·공지·진도·평가 유형·출결 사유·학점 경고), 12명 기준 데이터 정합, SEO 구조(h1 유일·OG/twitter·og.png) - 시간표 자동배치 결함 수리(명시적 격자 좌표) - QA 게이트: L0 stylelint/html-validate · L1 단위 30 · L2 감사 61+불변식 · L3 시각회귀 30화면 · L4 WebKit · L5 키보드 탐색 — npm run verify 실패 시 배포 금지 체인
This commit is contained in:
parent
78ddc8b61a
commit
72337b7ee0
226 changed files with 18212 additions and 380 deletions
90
apps/site/tools/unit/calc.test.mjs
Normal file
90
apps/site/tools/unit/calc.test.mjs
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
// 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);
|
||||
Loading…
Add table
Add a link
Reference in a new issue