feat(skill): refine brand-claim copy diagnosis and impact-based render checks
Limit competitor substitution to brand-differentiation claims, add impact-scoped render measurement guidance to audit-gate, and correct motion, Range, and font-check wording found during review.
This commit is contained in:
parent
2a7017717b
commit
79e79c120b
8 changed files with 114 additions and 26 deletions
|
|
@ -54,6 +54,43 @@
|
|||
| L5 탐색 | 키보드 완전 통과 + 사용자 여정 차터 | 프로젝트별 작성 — 마우스 금지 |
|
||||
| L6 시나리오 E2E | 실사용자 여정 재현 — 페르소나의 하루·장기 과업 포함 | 프로젝트별 작성. **기대값 단언 필수**(하니스 규칙 8), 백 키·시트·양방향 흐름은 `mobile-app-ux.md` 패턴 기준. 실측: RED 1,742 단언 중 318 실패가 신규 기능에 정확히 집중 — 시나리오를 먼저 쓰면 미구현이 수치로 드러난다 |
|
||||
|
||||
## 렌더 측정 계약
|
||||
|
||||
자동 측정은 실제 장면을 재현할 때만 의미가 있다. 새 범용 JS 엔진을 만들라는 뜻이 아니다. 프로젝트의 기존 E2E·시각 회귀 도구에 아래 계약을 적고, 페이지와 기능의 위험에 맞는 단언만 구현한다.
|
||||
|
||||
### 1. 장면과 레이아웃
|
||||
|
||||
각 artifact에는 빌드·revision 식별자, URL, viewport의 너비·높이·종횡비·zoom, 스크롤 위치, UI 상태, 로드한 폰트·이미지·영상 자산을 함께 남긴다. 폭만 적어서는 같은 폭에서 높이가 달라진 크롭, 짧은 화면의 고정 크롬, 확대에 따른 줄바꿈을 재현할 수 없다. 짧은 가로·긴 세로·확대가 결과에 영향을 주면 대표 장면에 포함한다. 배경 위치, 분기 종횡비, 헤더 높이, viewport 단위 선택은 프로젝트 계약으로 정한다.
|
||||
|
||||
화면 맞춤 hero는 헤더가 overlay인지, 문서 흐름에서 높이를 차지하는지 먼저 구분한다. 그 역할을 포함해 첫 화면의 남은 높이를 재고, 내용이 고정 높이보다 커지면 자연스럽게 확장되어야 한다. 모든 페이지 섹션에 `100vh`를 강요하지 않는다. 컨테이너의 `overflow=0`만으로 피사체·카피·행동의 안전 영역이 보존됐다고 판정하지 말고, 실제 크롭과 인접 콘텐츠의 겹침을 화면에서 확인한다.
|
||||
|
||||
### 2. 모션과 상태
|
||||
|
||||
reveal·전환은 실제 UI 스크롤·클릭·키보드 이벤트로 장면을 먼저 활성화한다. 그 장면에 필요한 폰트·이미지가 준비된 뒤 대상과 전환에 관여하는 조상 요소의 유한 전환 완료를 관측하고, 프로젝트가 의도한 최종 가시성·색·대비·위치에 도달했는지 확인한다. 최종 가시성은 `opacity: 1`을 일괄 요구하는 값이 아니다. 의도한 반투명 텍스트도 조상 opacity 합성을 포함한 실제 대비와 상태 계약으로 판정한다. 정한 상한 시간 안에 끝나지 않으면 실패 또는 미검증으로 기록하고 원인을 남긴다.
|
||||
|
||||
계속 움직이는 배경·카운터·캔버스는 별도 상태 계약으로 관측한다. 모든 페이지 애니메이션이 끝날 때까지 기다리는 검사는 만들지 않는다. 정지 상태의 대비, 일반 설정에서 trigger와 완료가 실제로 작동하는지, `prefers-reduced-motion` 경로를 각각 보고한다. 안정한 정상 상태의 대비가 영구 애니메이션의 저대비나 발화 실패까지 통과시키지 않는다.
|
||||
|
||||
고정 sleep, 검사 중 `opacity: 1` 강제, `animation: none` 주입으로 제품 모션 검사를 GREEN으로 만들지 않는다. 썸네일·OG용 정지 프레임은 별도 산출물이다. 그 경우에는 완성된 프레임을 고정하고 축소 모션을 적용할 수 있지만, 실제 동작 검수의 대체가 아니다.
|
||||
|
||||
### 3. 텍스트·클리핑·폰트
|
||||
|
||||
`Range.getClientRects()`는 선택 범위의 요소 box와 텍스트 font metric에 따른 사각형이며 글리프 잉크 경계가 아니다. heading·line box 밖 rect만으로 실제 잘림을 확정하지 않는다. viewport 밖 rect는 가시성 후보로 계속 검사하되, viewport와 실제 `overflow` clipping 조상을 함께 추적한다. `clip-path`·mask·transform처럼 rect만으로 설명하기 어려운 경계는 실제 렌더를 열어 수동 판정한다. 텍스트 후보가 보이면 인접 콘텐츠와 겹치거나 읽기 영역을 침범하는지도 함께 확인한다. [CSSOM View의 `Range.getClientRects()` 편집 초안](https://drafts.csswg.org/cssom-view/#dom-range-getclientrects)을 2026-09-20에 확인했다.
|
||||
|
||||
`document.fonts.ready`와 FontFaceSet 상태는 로드 준비의 근거일 뿐, 모든 화면 문자가 의도한 폰트로 그려졌다는 증명은 아니다. 필요한 문자·언어·숫자·기호와 웨이트를 실제 카피로 렌더하고, 브라우저가 제공하는 rendered-font 확인 또는 필요한 글리프 검증과 화면 관찰을 결합한다. 폰트 선택·subset·fallback의 세부 절차는 [typography.md](typography.md)를 따른다.
|
||||
|
||||
### 4. 실패와 증거
|
||||
|
||||
실패는 제품 결함, 측정기 오류, 계약 변경으로 분류한다. 실패 artifact와 원문 결과는 보존한다. 의도 변경 근거와 전후 렌더 증거 없이 기대값을 고치거나 assertion을 완화해 통과시키지 않는다. 측정기가 틀렸다면 UI를 먼저 고치지 말고 RED 재현을 남겨 측정기를 고친 뒤 같은 사례를 GREEN으로 확인한다.
|
||||
|
||||
artifact 한 건에는 빌드·revision, viewport·zoom·스크롤·상태, 기대값과 관측값, 자동 결과, 실제 화면의 시각 판정을 같이 남긴다. 수치와 스크린샷이 서로 다른 장면이면 증거가 아니다.
|
||||
|
||||
| 작은 재현 예제 | 재현 조건 | 정상 기대 | 오판 / 금지되는 처리 |
|
||||
|---|---|---|---|
|
||||
| reveal 진행 중 / 완료 뒤 저대비 | 실제 스크롤로 reveal을 시작한 직후와 유한 전환 종료 뒤를 각각 캡처한다 | 진행 중 프레임은 완료 판정에서 제외하고, 종료 뒤에는 의도한 색·합성 대비와 가시성을 확인한다 | sleep 한 번 뒤 진행 프레임을 PASS로 쓰거나, 종료했지만 실제 색 대비가 부족한 텍스트를 통과시킨다 |
|
||||
| line box 넘침 / 실제 hidden 조상 clip | 같은 긴 문장의 실제 잉크가 heading·line box 밖으로 나가게 두고, 한쪽은 `overflow: visible`, 다른 쪽은 잉크가 경계를 넘는 `overflow: hidden` 조상 안에 둔다 | 전자는 실제 잉크와 인접 영역을 보고 보존 여부를 판정하며, 후자는 실제 clip을 화면에서 확인한다 | Range rect 하나로 두 사례를 모두 잘림 또는 모두 정상으로 확정한다 |
|
||||
| 같은 폭, 다른 높이의 피사체 크롭 | 같은 폭의 이미지 장면을 짧은 viewport와 긴 viewport에서 header 역할·스크롤 위치를 같게 맞춘다 | 피사체와 행동의 안전 영역이 두 높이에서 의도대로 남는지 비교한다 | 폭만 기록해 짧은 화면에서 잘린 얼굴·문구를 놓친다 |
|
||||
| 의도 변경 없는 낡은 expected 값 | 렌더가 달라졌지만 디자인 의도 변경 기록이 없는 시각 회귀를 만든다 | 기존 기대값을 유지하고 제품 결함 또는 측정기 오류를 분류한다 | diff를 없애려고 expected·baseline만 무단 갱신한다 |
|
||||
|
||||
## 설치 (프로젝트에 게이트 심기)
|
||||
|
||||
```
|
||||
|
|
@ -81,19 +118,19 @@ L1·L5 는 프로젝트 안에 `tools/unit/*.test.mjs`, `tools/exploratory.mjs`
|
|||
|
||||
1. **선택자는 스코프필수** — 같은 data-* 가 다른 뷰에 있으면 숨은 요소를 잡는다(`#as-table [data-assign]` 처럼).
|
||||
2. **page.evaluate 클로저 금지** — Node 스코프 함수를 페이지 안에서 부르지 않는다. 측정 코드는 문자열로 주입.
|
||||
3. **줄 수 측정의 오탐 두 종류** — 폰트 메트릭 top 차이(±5px 허용오차), 인라인 아이콘과 텍스트의 top 차이(텍스트 노드만 분리 측정).
|
||||
3. **줄 수 측정의 오탐 두 종류** — 폰트 메트릭 top 차이(±5px 허용오차), 인라인 아이콘과 텍스트의 top 차이(텍스트 노드만 분리 측정). line box와 실제 글리프 잘림의 구분은 [렌더 측정 계약](#렌더-측정-계약)을 따른다.
|
||||
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). 아니면 매번 다른 프레임이 찍혀 회귀가 흔들린다.
|
||||
10. **등장 모션과 캡처 상태를 맞춘다** — 모션 지속시간은 사용자 경험의 목적·입력 반응·감소 모션 계약으로 정한다. 시각 회귀는 그 값을 줄이라고 요구하지 않고, 실제 완료를 관측한 뒤 캡처한다. 임의 sleep으로 진행 프레임을 통과시키지 않으며 실제 모션 검수는 [렌더 측정 계약](#렌더-측정-계약)을 따른다.
|
||||
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. **시각 기준 갱신은 의도 변경 리뷰를 동반한다** — "달라졌으니 갱신"으로 결함이 기준에 박힌다(실측: 왼쪽 인셋 결함 화면이 기준 갱신으로 '정상'이 되어 재발을 숨겼다). 갱신 전에 무엇이 의도적으로 바뀌었는지 한 줄씩 나열하고, 사용자 보고가 있던 화면은 수리 후 재촬영한다.
|
||||
15. **시각 기준 갱신은 의도 변경 리뷰를 동반한다** — "달라졌으니 갱신"으로 결함이 기준에 박힌다(실측: 왼쪽 인셋 결함 화면이 기준 갱신으로 '정상'이 되어 재발을 숨겼다). 갱신 전에 무엇이 의도적으로 바뀌었는지 한 줄씩 나열하고, 사용자 보고가 있던 화면은 수리 후 재촬영한다. 증거 보존과 실패 분류는 [렌더 측정 계약](#렌더-측정-계약)을 따른다.
|
||||
16. **사용자 보고 스크린샷은 비전 요약이 아니라 픽셀 판정으로 해부한다** — 비전 모델의 픽셀 추정("~0px", "~16px")은 부정확하다. pngjs 등으로 화면을 가로 밴드로 나눠 각 밴드의 텍스트 시작 x를 직접 재면(보임 행만, 왼쪽 테두리 아티팩트 제외) "무엇이 어디서 어긋났는지"가 수치로 나온다. 실측: '제목이 가장자리에 붙는다'는 보고가 앱바 24px·제목 16px의 리듬 불일치로 확정됐다.
|
||||
17. **새 검사는 수정 전 코드로 RED 재현 후 GREEN** — 검사 자체의 측정 하자(오탐)도 이렇게 잡는다. 실측: 왼쪽 인셋 검사 초안이 ①`display:none` 요소의 rect=0, ②전폭 컨테이너의 보더박스(텍스트가 아님)로 38개 오탐 — RED 단계에서 발견해 교정했다.
|
||||
18. **시각 E2E는 기본 화면만 찍지 않는다** — URL별 390·1440 기본 화면에 더해 결과가 바뀐 입력, 오류/검토, 모달/차단 상태를 찍는다. 하니스는 파일 개수·가로세로·0바이트·해시 중복을 단언하고, 에이전트는 파일을 실제로 열어 시장 적합성·위계·크롭·반복을 판정한다. 스크린샷을 저장했다는 사실만으로 PASS하지 않는다.
|
||||
|
|
@ -111,7 +148,7 @@ L1·L5 는 프로젝트 안에 `tools/unit/*.test.mjs`, `tools/exploratory.mjs`
|
|||
30. **native dialog의 위치를 UA 스타일에 맡기지 마라** — 전역 reset은 dialog의 기본 margin을 지울 수 있다. open 상태에서 `(left + width/2, top + height/2)`와 viewport 중심의 오차, 화면 안 rect, `html` overflow, Escape 뒤 복귀를 390·1440·2560에서 측정한다. 내용이 길면 dialog 또는 내부 본문 하나만 scroll owner여야 한다.
|
||||
31. **초광폭 히어로는 비율 계약을 잰다** — 2560 같은 실제 wide viewport에서 hero 폭/정렬, copy 최소 폭, media 최대 폭, 다음 정보 레일 폭을 함께 단언한다. media만 넓고 카피가 가늘어지는 상태를 `overflow=0`으로 통과시키지 않는다. 절대 수치는 페이지별 토큰이 소유하며, 풀블리드 편집 사진 예외는 이유를 기록한다.
|
||||
32. **캐시 무효화도 사용자 화면에서 단언한다** — static CSS/JS를 덮어쓴 뒤 HTML만 새 URL이면 브라우저는 이전 stylesheet를 계속 쓴다. stylesheet와 `@import` 의 버전을 같이 바꾸고, 공개 URL에서 실제 href·새 토큰/핵심 computed value를 검사한다. HTTP 200과 새 HTML만으로 배포 성공을 선언하지 않는다.
|
||||
33. **썸네일은 안정한 프레임만 저장한다** — 캡처 전 local HTTP 경로에서 `document.fonts.ready`·모든 이미지 `decode()`·bounded settle을 기다리고, `prefers-reduced-motion: reduce`를 적용한다. 그 뒤 video/audio·CSS/WAAPI·rAF canvas를 멈추고, local 4xx·pageerror·깨진 이미지가 하나라도 있으면 실패시킨다. 원리는 `animation:none`으로 초기 상태를 재현하는 것이 아니라 **완성된 한 프레임을 고정**하는 것이다. 카드·OG·핵심 상태가 같은 계약을 공유하는지 파일 치수·0바이트·시각 검수까지 단언한다.
|
||||
33. **썸네일은 안정한 프레임만 저장한다** — 캡처 전 local HTTP 경로에서 `document.fonts.ready`·모든 이미지 `decode()`·bounded settle을 기다리고, `prefers-reduced-motion: reduce`를 적용한다. 그 뒤 video/audio·CSS/WAAPI·rAF canvas를 멈추고, local 4xx·pageerror·깨진 이미지가 하나라도 있으면 실패시킨다. 원리는 `animation:none`으로 초기 상태를 재현하는 것이 아니라 **완성된 한 프레임을 고정**하는 것이다. 이는 실제 모션 검수와 별도다. 카드·OG·핵심 상태가 같은 계약을 공유하는지 파일 치수·0바이트·시각 검수까지 단언한다.
|
||||
34. **초광폭 text-media split은 가족별로 잰다** — hero가 통과했다고 과정·추천·가맹 분할이 통과한 것이 아니다. 서로 다른 DOM/여백 규칙을 가진 각 split root에서 1920·2560의 rail, copy/media rect, heading의 실제 내용 폭과 줄 수를 hard fail로 기록한다. wide override는 양쪽 logical padding을 명시하고, 상위 container의 `max-width`가 specificity 때문에 named stage를 다시 줄이지 않는지 단언한다. grid 바깥 rect가 정상이어도 text content가 한 글자 열로 수축하면 실패다.
|
||||
|
||||
## 리포트 양식
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue