designpaca/packages/skill/references/audit-gate.md
Yun Chan 46b47884c9 feat(skill): 왼쪽 인셋 리듬·픽셀 판정을 스킬에 내재화
이번 라운드(스크롤 잔류·빈약한 헤더·인셋 불일치 — 사용자 보고 3건)의 교훈을 스킬로 승격:
- design-gate leftInset 검사 내장(텍스트 노드 Range 기준 ≥12px, 모바일 390px 전 뷰) — 오버플로 검사는 오른쪽만 보는 계측 공백을 메움. 도그푸드: 양앱 통과(가온 16·두레 24 각자 일관 축)
- 브랜드 검사 오탐 수정(동일 셀렉터 다중 요소 시 보이는 것 기준 — 도그푸드가 잡음)
- preflight §0-E 왼쪽 인셋 리듬, 하니스 규칙 15~17(기준 갱신 의도 리뷰·스크린샷 픽셀 판정·RED 재현), 사고표 4행
- SKILL.md 규칙 6 보강: 계측의 공백 — 보고가 오면 픽셀 판정으로 수치화하고 게이트에 추가
2026-08-23 15:40:58 +09:00

100 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# audit-gate — 자율 검증 폐쇄 루프
5단계(감사)를 **완료 선언의 게이트**로 만드는 문서다. 핵심 원칙은 하나다:
> **완료의 정의는 게이트 통과다. 사용자를 QA 로 쓰지 않는다.**
"고쳤다"는 말의 근거는 검증 리포트의 수치다. 게이트가 실패 항목을 내면 4단계로 돌아가 고치고 **다시 전체를 돌린다** — 고친 것만 재검하지 않는다. 이 루프(구현 → 게이트 → 실패 → 수정 → 재게이트)가 designpaca의 기본 동작이며, 게이트 없이 6단계로 넘어가는 것은 경로 무관 금지다.
## 왜 필요한가 — 실제 사고 목록
이 게이트의 각 검사는 사용자 보고로야 발견됐던 결함에서 왔다. 어느 검사가 어느 사고를 잡는지가 설계 근거다.
| 사고 (실제) | 검사 |
|---|---|
| CSS 닫는 중괄호 누락 — 이후 규칙 절반이 죽은 채 렌더 | stylelint(구문) + 중괄호 균형 |
| 브랜드가 모바일에서 1글자 폭으로 수축, 세로로 쌓임 | 수축 탐지(짧은 라벨 n줄 이상) + 브랜드 1줄 |
| 기기 글꼴 확대(안드로이드 큰 글꼴 1.3×)에서 제목 부서짐 | 스케일×폭 h1 행렬 |
| 본문 보조색이 배경 대비 4.4:1 (AA 미달) | 전 요소 대비 스캔(블렌드 계산) |
| 조사만 하고 안 고친 영역(관심 과목 등) | 전 뷰 순회 — 검사는 '내가 고친 것'이 아니라 전체 표면 |
| 대시보드 26명 ↔ 명단 12명 숫자 모순 | 콘텐츠 정합(요약 = 재계산 비교) |
| aria-label 을 div/ul 에 오용(보조기술 무시) | html-validate + axe |
| dl 안 `<p>`, role="grid" 자식 구문 위반 | html-validate + axe |
| 계산 로직 오류(행 배열을 값으로 셈 → 0/3) | 단위 테스트(브라우저 밖 경계값) |
| 같은 데이터 속성이 여러 뷰에 존재해 선택자 오작동 | 하니스 규칙: 선택자는 컨테이너 스코프 |
| 혼합 span 그리드의 자동 배치가 시간 라벨을 흩어놓음(같은 열 폭 40/78 혼재, Δ 불규칙) | **리듬·트랙 불변식** — 같은 부모·같은 클래스 형제 중 (a) 숫자/시간 라벨(등폭 의도)과 (b) 빈 격자 셀(트랙 균일 의도)은 폭이 균일해야 하고, 한 축 정렬·크기 균일 형제는 등간격이어야 한다. 한국어 라벨은 글자수가 같아도 내용 폭차가 자연스러우므로 폭 검사 대상에서 뺀다(오탐 방지). 근본 수칙: 혼합 span 그리드는 자동 배치를 믿지 않고 좌표를 명시하라 |
| 프리셋 전환(v7)에서 브랜드 표면이 누락 — theme-color·파비콘이 종이 시대 값으로 11버전 생존 | **theme-color 정합**(메타 값 ↔ 실제 배경색 비교) + 파비콘·og:image 존재. 토큰 마이그레이션은 브랜드 표면 3종을 갱신하기 전까지 끝나지 않는다 |
| 컴포넌트 제거 시 CSS/JS 잔존 — 미사용 선택자 12종·이중 계산 블록(첫 블록이 즉시 덮임) | **죽은 선택자 검사**(CSS 클래스 ↔ HTML·JS 텍스트 사용률 대조). 제거는 HTML·CSS·JS 삼위일체 |
| 크롬에 컨트롤 1개 추가 → 그 줄의 최소폭 합 초과(320px +18px) | 오버플로 전 폭 검사(기존) + 규칙: 크롤에 컨트롤을 더하면 최소폭 합을 가장 좁은 폭에서 다시 잰다 |
| 같은 명시도의 숨김 규칙이 기본 규칙보다 앞에 있어 짐 — 모바일에서 숨기려던 kbd 안내가 렌더됨(6% 시각 diff) | 시각 회귀 + 규칙: **숨김 오버라이드는 기본 규칙보다 파일 뒤쪽에** 두거나 컴포넌트 규칙 근처의 미디어쿼리로 |
| 탐색 차터가 결과를 단언하지 않아 "9288점" 입력이 조용히 통과(값을 덧붙인 타이핑) | 하니스 규칙 8(검사는 단언)·9(number 입력 조작) |
| 모바일 라운드 — 중앙 모달이 썸존 밖, 백 키가 앱을 통째로 종료, safe area 무시, 입력 확대 | L6 시나리오(백·시트·터치타깃 스윕) + `references/mobile-app-ux.md` 표준 — **OS 관습은 조사 없이 바꾸지 않는다** |
| 탭 전환 스크롤 잔류 — 짧은 뷰에서 클램프되어 헤더 반쯤 잘림, 뷰마다 크롬 위치 제각각 | 뷰 전환 `scrollTo(0,0)` 단언(M5) + mobile-app-ux.md |
| 크럼을 헤더로 내보내 빈약한 헤더 — 계측 전부 통과했지만 사용자에게 '깨져 보인다' | 헤더 sticky·브랜드+액션 구조 단언 + **시각 무게 평가(눈)** — mobile-app-ux.md |
| 왼쪽 인셋 리듬 불일치(제목 16 vs 앱바·카드 24) — 오버플로 검사는 오른쪽만 보므로 통과 | M6 왼쪽 인셋 스위트(텍스트 시작점 ≥12px·같은 축 ±10px) + preflight §0-E |
## 7계층
`tools/design-gate.mjs` 가 한 파일로 돌리는 것과 프로젝트가 보강하는 것이 있다.
| 계층 | 도구 | 비고 |
|---|---|---|
| L0 정적 | stylelint + html-validate | 문법·구문 위반을 브라우저 켜기 전에 |
| L1 단위 | 순수 계산 로직 추출(calc.js) + 경계값 테스트 | 프로젝트별 작성 — 0명/만석/초과/빈배열. **입력 검증·신청 가능 같은 '판정'(가능/불가+사유)도 여기 둔다** — UI 는 판정을 문장으로 번역만 |
| L2 불변식 | `design-gate.mjs` — 오버플로·h1·수축·대비·SEO/meta·죽은 선택자·토큰 위생·theme-color 정합 | 전 뷰 × 전 폭 |
| L3 시각 회귀 | 기준 화면 pixelmatch diff (≥0.1% 실패) | 갱신은 검증된 배포 후 `--update-baseline` |
| L4 WebKit | Playwright webkit | 사파리 엔진 렌더 차이 |
| L5 탐색 | 키보드 완전 통과 + 사용자 여정 차터 | 프로젝트별 작성 — 마우스 금지 |
| L6 시나리오 E2E | 실사용자 여정 재현 — 페르소나의 하루·장기 과업 포함 | 프로젝트별 작성. **기대값 단언 필수**(하니스 규칙 8), 백 키·시트·양방향 흐름은 `mobile-app-ux.md` 패턴 기준. 실측: RED 1,742 단언 중 318 실패가 신규 기능에 정확히 집중 — 시나리오를 먼저 쓰면 미구현이 수치로 드러난다 |
## 설치 (프로젝트에 게이트 심기)
```
node <스킬>/tools/design-gate.mjs --init # gate.config.json 초안
# pages[].views 를 실제 뷰 목록으로, viewAttribute 를 data-view 등으로
npm i -D puppeteer-core # CHROME_PATH 또는 자동 탐색
package.json: "verify": "node <스킬>/tools/design-gate.mjs"
배포는 항상: npm run verify && build && deploy # verify 실패(exit 1)면 배포 불가
```
L1·L5 는 프로젝트 안에 `tools/unit/*.test.mjs`, `tools/exploratory.mjs` 로 보강하고 verify 체인에 묶는다. 근거와 완성 예는 designpaca 저장소 `apps/site/tools/` 가 정본 샘플이다.
## SEO / meta 감사 (design-gate 내장)
조사 기준(2026): title 10~65자·유일, description 60~165자, viewport, `html lang`, charset, canonical(프로덕션), og:title/description/image/url, twitter:card, favicon, h1 유일, img alt 전부, JSON-LD 파싱. 데모·로컬 페이지는 canonical/robots 없음을 허용한다 — **검사는 맥락을 존중하되 나열 근거는 리포트에 남긴다.**
## OS · 브라우저 특성 — 검사에 반영할 것
- **Windows**: 스크롤바가 오버레이가 아니다 — `overflow-x:auto` 짝에는 `overflow-y:hidden` + `scrollbar-width` 검토. 스크롤바 자체가 레이아웃을 민다.
- **iOS/Safari(WebKit)**: `100vh` 주소창 문제(`100dvh`), 텍스트 인플레이션(`-webkit-text-size-adjust:100%`), WebKit 만의 flex/행높이 차이 → L4 로 재검.
- **안드로이드**: 시스템 글꼴 확대(1.15~1.3× 흔함) — rem 전면 확대로 제목 칸 수축 → 스케일×폭 행렬으로 재현·검증.
- **폰트 스왑**: `font-display: swap` 의 CLS — 폴백 메트릭 보정(typography.md §4) 없이 검사 통과를 선언하지 않는다.
## 하니스 규칙 (테스트를 만드는 테스트)
1. **선택자는 스코프필수** — 같은 data-* 가 다른 뷰에 있으면 숨은 요소를 잡는다(`#as-table [data-assign]` 처럼).
2. **page.evaluate 클로저 금지** — Node 스코프 함수를 페이지 안에서 부르지 않는다. 측정 코드는 문자열로 주입.
3. **줄 수 측정의 오탐 두 종류** — 폰트 메트릭 top 차이(±5px 허용오차), 인라인 아이콘과 텍스트의 top 차이(텍스트 노드만 분리 측정).
4. **CSS 수정 직후 검증은 캐시 차단**(`setCacheEnabled(false)`) + 실제 로드된 `?v=` 확인.
5. **비전(모델) 검수는 '여부'가 아니라 '지점 평가'에만** — 여부는 계측으로.
6. **모달 닫힘 직후 포커스 강탈** — 뷰 전환+타깃 포커스는 한 evaluate 로, settle 후 유실 재포커스.
7. **게이트를 통과해도 리포트에 수치를 남긴다** — "ALL PASS"가 아니라 "무엇을 몇으로 확인했나".
8. **검사는 단언한다** — `pass("차터", st)` 처럼 상태를 상세로만 찍는 건 검사가 아니다. 실측 사고: 점수 입력에 "88"을 `type()` 하면 기존 값에 **덧붙어** "9288"이 되었고, 차터는 상태 텍스트만 인쇄해 통과했다. 기대값과 비교해 pass/fail 을 내라.
9. **number 입력은 선택-덮어쓰기가 안 된다** — 트리플 클릭 선택이 무시된다. 값을 비우고 타이핑하거나 `el.value = x; dispatchEvent(new Event("input"))` 로 설정한다.
10. **등장 모션 지속시간 ≤ 감사 도구 대기시간** — 시각 회귀가 뷰 전환 100ms 뒤에 스크린샷을 찍는다면 등장 애니메이션은 그보다 짧게(≤70ms). 아니면 매번 다른 프레임이 찍혀 회귀가 흔들린다.
11. **같은 diff 수치가 반복되면 패치가 적용되지 않은 것** — 수정하고도 시각 diff %가 소수점까지 동일하면 수정이 렌더에 닿지 않았다(캐시·캐스케이드 순서·잘못된 파일). 고치기를 반복하지 말고 적용 자체를 의심해라.
12. **페이지→노드 직렬화는 NaN 방어** — `getComputedStyle(el).fontSize` 등을 문자열 보간하면 "16px"이 NaN이 되고, 하니스는 NaN을 `null`로 받아 `"////px"` 같은 고장난 상세를 리포트한다. 수치는 항상 `parseFloat(x) || 0` 로 감싼다.
13. **`position: fixed` 요소의 보임 판정은 offsetParent 로 하지 마라** — fixed 는 offsetParent 가 `null`이다. display 계산값·높이로 판정한다.
14. **실기기(에뮬레이터) 검증은 텍스트 근거로** — `uiautomator dump` 의 `text="…"`에서 화면 제목·활성 탭을 읽어 단언한다. 스크린샷+비전은 "백 직후 전환 중 프레임"을 잡는 타이밍 오탐이 있다(실측 사고). 물리 폰이 붙어 있으면 `adb -s <emulator>` 로 대상을 한정한다.
15. **시각 기준 갱신은 의도 변경 리뷰를 동반한다** — "달라졌으니 갱신"으로 결함이 기준에 박힌다(실측: 왼쪽 인셋 결함 화면이 기준 갱신으로 '정상'이 되어 재발을 숨겼다). 갱신 전에 무엇이 의도적으로 바뀌었는지 한 줄씩 나열하고, 사용자 보고가 있던 화면은 수리 후 재촬영한다.
16. **사용자 보고 스크린샷은 비전 요약이 아니라 픽셀 판정으로 해부한다** — 비전 모델의 픽셀 추정("~0px", "~16px")은 부정확하다. pngjs 등으로 화면을 가로 밴드로 나눠 각 밴드의 텍스트 시작 x를 직접 재면(보임 행만, 왼쪽 테두리 아티팩트 제외) "무엇이 어디서 어긋났는지"가 수치로 나온다. 실측: '제목이 가장자리에 붙는다'는 보고가 앱바 24px·제목 16px의 리듬 불일치로 확정됐다.
17. **새 검사는 수정 전 코드로 RED 재현 후 GREEN** — 검사 자체의 측정 하자(오탐)도 이렇게 잡는다. 실측: 왼쪽 인셋 검사 초안이 ①`display:none` 요소의 rect=0, ②전폭 컨테이너의 보더박스(텍스트가 아님)로 38개 오탐 — RED 단계에서 발견해 교정했다.
## 리포트 양식
```
게이트: 전 계층 통과 — 배포 가능 · 총 Ns
| 계층 | 결과 | 항목 | 소요 |
```
실패 상세는 원문 테일 포함. 리포트 파일(gate-report.md / verify-report.md)은 커밋 대상이다.