- HIG(탭바 5~6·시트)·M3(바텀내비 3~5·48dp)·NN/g·CSS-Tricks 조사(research/mobile-app-ux-v1.md) 후 Nielsen 10 감사로 위반 도출 - 바텀 내비 5탭 + 더보기 바텀 시트(그룹 헤더), 뷰·다이얼로그 pushState/popstate — 안드로이드 백 키가 앱 내 뒤로 작동 - safe area(env+max)·입력 16px(iOS 확대 방지)·coarse 44px 타깃·tap-highlight·overscroll 위생, 인쇄에서 모바일 크롬 제거 - PWA: manifest + sharp 생성 아이콘(가온=장부 문항, 두레=시간표 블록) - tools/e2e.mjs L6 시나리오: RED 1,742 단언 318 실패 → GREEN 1,762/0, verify 체인 7계층 통과 - 실기기(안드로이드 에뮬레이터·Chrome) 텍스트 근거 검증 — 백 키·시트·탭 전환, iOS 는 WebKit(L4) 대체 - 스킬 내재화: mobile-app-ux.md, audit-gate 7계층·하니스 12~14, design-gate 터치타깃 검사, SKILL.md 규칙 7 - 도그푸드: 게이트가 죽은 선택자 8종·thresholds 병합 버그 적발 — CSS v22(양앱) 범프 후 전체 재검증
67 lines
2.6 KiB
JavaScript
67 lines
2.6 KiB
JavaScript
/* 가온 학적부 — 순수 계산 로직. 브라우저와 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 validateScore = (raw) => {
|
|
const s = String(raw ?? "").trim();
|
|
if (!s) return { ok: false, reason: "empty" };
|
|
const v = Number(s);
|
|
if (Number.isNaN(v)) return { ok: false, reason: "nan" };
|
|
if (v < 0 || v > 100) return { ok: false, reason: "range" };
|
|
return { ok: true, value: v };
|
|
};
|
|
|
|
const api = { attSummary, dayTotal, dayPresent, counts, rosterStats, validateScore };
|
|
global.GAON = api;
|
|
if (typeof module !== "undefined") module.exports = api;
|
|
})(typeof window !== "undefined" ? window : globalThis);
|