- 핵심 규칙 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
62
apps/site/tools/verify.mjs
Normal file
62
apps/site/tools/verify.mjs
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
// QA 게이트 — L0~L5 전 계층 실행. 1건 실패라도 있으면 exit 1 (배포 금지).
|
||||
// 사용: node tools/verify.mjs / npm run verify
|
||||
// 기준 화면 갱신: node tools/visual.mjs --update-baseline 후 커밋.
|
||||
import { spawnSync } from "node:child_process";
|
||||
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 root = path.resolve(here, "..");
|
||||
const npx = process.platform === "win32" ? "npx.cmd" : "npx";
|
||||
|
||||
const steps = [
|
||||
{
|
||||
layer: "L0-정적/stylelint",
|
||||
cmd: () => spawnSync(npx, ["stylelint", "public/work/*/styles/*.css"], { cwd: root, encoding: "utf8", shell: true }),
|
||||
},
|
||||
{
|
||||
layer: "L0-정식/html-validate",
|
||||
cmd: () => spawnSync(npx, ["html-validate", "public/work/gaon-lms/index.html", "public/work/dure-enrollment/index.html"], { cwd: root, encoding: "utf8", shell: true }),
|
||||
},
|
||||
{ layer: "L1-단위/계산로직", cmd: () => spawnSync(process.execPath, ["tools/unit/calc.test.mjs"], { cwd: root, encoding: "utf8" }) },
|
||||
{ layer: "L2-기능/감사", cmd: () => spawnSync(process.execPath, ["tools/audit.mjs"], { cwd: root, encoding: "utf8" }) },
|
||||
{ layer: "L2-불변식/렌더", cmd: () => spawnSync(process.execPath, ["tools/invariants.mjs"], { cwd: root, encoding: "utf8" }) },
|
||||
{ layer: "L3-시각회귀/diff", cmd: () => spawnSync(process.execPath, ["tools/visual.mjs"], { cwd: root, encoding: "utf8" }) },
|
||||
{ layer: "L4-WebKit", cmd: () => spawnSync(process.execPath, ["tools/webkit.mjs"], { cwd: root, encoding: "utf8" }) },
|
||||
{ layer: "L5-탐색/키보드", cmd: () => spawnSync(process.execPath, ["tools/exploratory.mjs"], { cwd: root, encoding: "utf8" }) },
|
||||
];
|
||||
|
||||
const report = [];
|
||||
const t0 = Date.now();
|
||||
let failed = 0;
|
||||
for (const step of steps) {
|
||||
const s = Date.now();
|
||||
const r = step.cmd();
|
||||
const ok = r.status === 0;
|
||||
if (!ok) failed++;
|
||||
const out = (r.stdout || "") + (r.stderr || "");
|
||||
const passLine = (out.match(/PASS\s+[^\n]/g) || []).length;
|
||||
const failLines = (out.match(/FAIL\s+[^\n]/g) || []).length;
|
||||
const tail = out.trim().split("\n").slice(-3).join(" | ").slice(0, 180);
|
||||
report.push({ layer: step.layer, ok, ms: Date.now() - s, passLine, failLines, tail });
|
||||
console.log(`${ok ? "✓" : "✗"} ${step.layer} (${((Date.now() - s) / 1000).toFixed(1)}s)${passLine ? ` — PASS ${passLine}` : ""}${failLines ? ` FAIL ${failLines}` : ""}`);
|
||||
if (!ok) console.log(" " + tail);
|
||||
}
|
||||
|
||||
const totalS = ((Date.now() - t0) / 1000).toFixed(1);
|
||||
const stamp = new Date().toISOString().replace("T", " ").slice(0, 16);
|
||||
const md = [
|
||||
`# 검증 리포트 — ${stamp}`,
|
||||
``,
|
||||
`게이트: ${failed ? `**${failed}개 계층 실패 — 배포 금지**` : "**전 계층 통과 — 배포 가능**"} · 총 ${totalS}초`,
|
||||
``,
|
||||
`| 계층 | 결과 | 항목 | 소요 |`,
|
||||
`|---|---|---|---|`,
|
||||
...report.map((r) => `| ${r.layer} | ${r.ok ? "PASS" : "**FAIL**"} | ${r.failLines ? `FAIL ${r.failLines}` : `PASS ${r.passLine || "—"}`} | ${(r.ms / 1000).toFixed(1)}s |`),
|
||||
``,
|
||||
...report.filter((r) => !r.ok).map((r) => `## ${r.layer} 실패 상세\n\n\`\`\`\n${r.tail}\n\`\`\``),
|
||||
].join("\n");
|
||||
fs.writeFileSync(path.join(here, "verify-report.md"), md);
|
||||
console.log(`\n${failed ? `게이트 실패(${failed}계층)` : "게이트 통과"} — 리포트: tools/verify-report.md`);
|
||||
process.exit(failed ? 1 : 0);
|
||||
Loading…
Add table
Add a link
Reference in a new issue