feat(site,skill): 모바일 앱 수준 UX — 바텀내비·백 키 히스토리·바텀 시트·safe area·PWA 와 L6 시나리오 E2E

- 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(양앱) 범프 후 전체 재검증
This commit is contained in:
Yun Chan 2026-08-23 14:33:15 +09:00
parent 72337b7ee0
commit 14cb138e1a
133 changed files with 2139 additions and 208 deletions

View file

@ -86,5 +86,37 @@ t("seatsState: 여석 6 → 임박 아님 (경계)", () => assert.ok(!DURE.seats
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);