- 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(양앱) 범프 후 전체 재검증
41 lines
1.8 KiB
JavaScript
41 lines
1.8 KiB
JavaScript
/* 두레 수강신청 — 순수 계산 로직. 브라우저와 Node 테스트가 같은 함수를 공유한다. */
|
|
(function (global) {
|
|
"use strict";
|
|
|
|
// 두 과목의 시간 겹침 — 닿는 경계(end == start)는 겹침이 아니다.
|
|
const isOverlap = (a, b) => {
|
|
const daysA = a.days || [], daysB = b.days || [];
|
|
const shared = daysA.some((d) => daysB.includes(d));
|
|
if (!shared) return false;
|
|
return a.start < b.end && b.start < a.end;
|
|
};
|
|
|
|
const credits = (list) => (list || []).reduce((acc, c) => acc + (c.credits || 0), 0);
|
|
|
|
// 학점 초과 예측 — 현재 신청 학점에 이 과목을 더했을 때 기준 초과 여부
|
|
const wouldExceed = (currentCredits, courseCredits, max) =>
|
|
currentCredits + courseCredits > max;
|
|
|
|
const seatsState = (c) => {
|
|
if (!c.seats) return { label: "정원 마감", closed: true, near: false };
|
|
return { label: `여석 ${c.seats}`, closed: false, near: c.seats <= 5 };
|
|
};
|
|
|
|
// 이수 잔여 — 음수면 '초과'
|
|
const remaining = (need, done, now) => need - done - now;
|
|
|
|
// 신청 가능 판정 — 정원·겹침·학점을 한 번에. 모달의 경고 문장과 실행취소 재심사가 같은 판정을 공유한다.
|
|
const canEnroll = (course, enrolledList, currentCredits, max) => {
|
|
if (!course || !course.seats) return { ok: false, reason: "full" };
|
|
const names = (enrolledList || [])
|
|
.filter((o) => isOverlap(course, o))
|
|
.map((o) => o.name);
|
|
if (names.length) return { ok: false, reason: "overlap", names };
|
|
if (wouldExceed(currentCredits, course.credits, max)) return { ok: false, reason: "credits" };
|
|
return { ok: true };
|
|
};
|
|
|
|
const api = { isOverlap, credits, wouldExceed, seatsState, remaining, canEnroll };
|
|
global.DURE = api;
|
|
if (typeof module !== "undefined") module.exports = api;
|
|
})(typeof window !== "undefined" ? window : globalThis);
|