feat(skill): route fixes, delivery report, composition diversity, platform refresh

- route table: extension route for products with an existing visual
  language, feedback row, derived state tokens stay local
- completion: blocking 0 plus reported unverified; hard-gate surface is
  the touched scope; delivery report starts with what and how to open
- design.md created at step 0 with a skeleton, fingerprint line and
  change log; product-surface rubric reading; direction delegation
  states the chosen card and reason
- layout: hero families, nav forms, footer forms; art-direction page
  convergence 6 axes; genre sections; image treatments; placeholder
  conventions; multi-screen order; Tailwind theme mapping
- platform: scroll-driven examples, anchor positioning, canvas OT,
  three/R3F pins, shadcn style prefixes, Safari 26 notes, subgrid
- design-gate: four result states, exit codes, cwd module resolution,
  --url/--out-dir/--strict, oklch contrast, dead selector word match
- Korean font budget split from the Latin baseline
This commit is contained in:
Yun Chan 2026-10-06 16:55:38 +09:00
parent 9bcb45cbd3
commit 88c11682b1
51 changed files with 5834 additions and 2259 deletions

View file

@ -8,12 +8,12 @@
| 상황 | 읽을 곳 |
|---|---|
| AA와 AAA, 자동 점수와 실제 준수의 관계가 헷갈린다 | §0 |
| AA와 AAA, 자동 점수와 실제 준수의 관계가 헷갈린다. 한국 KWCAG·EU EAA 같은 적용 법규 | §0 |
| `<div onClick>` 대신 뭘 써야 하는지, ARIA를 언제 쓰는지 | §1 |
| 아이콘 전용 버튼, 인라인 SVG의 이름을 어떻게 붙이는지 | §2 |
| 포커스 링 스타일·대비·가림 방지 | §3 |
| 탭·메뉴·콤보박스·리스트박스·다이얼로그의 키보드 계약, 모달, SPA 라우팅 | §4 |
| 스킵 링크·랜드마크·링크 텍스트 | §5 |
| 스킵 링크·랜드마크·도움말 위치 일관성·링크 텍스트 | §5 |
| 라벨·자동완성·입력 타입·비활성화 규칙·오류 처리 | §6 |
| 토스트·로딩 상태를 어떻게 알리는지 | §7 |
| 터치 타깃 크기와 히트 영역 확장 | §8 |
@ -31,6 +31,16 @@
**하드 게이트.** WCAG 2.2는 A·AA·AAA 세 적합성 수준을 정의하며, 셋은 서로 다른 요구다 — AAA를 만족한다고 모두에게 접근 가능해지지는 않는다[WCAG-22]. designpaca는 **AA를 하드 게이트**로 삼는다. 이 문서의 모든 절은 별도 표기가 없는 한 AA 이하 기준을 하드 게이트로 취급한다.
**적용 법규·표준을 먼저 확인한다.** WCAG는 기술 기준이고, 어느 수준이 의무인지는 대상 시장의 법이 정한다. 브리프에 시장·발주처를 적고 해당 기준을 `design.md`에 한 줄로 남긴다.
| 시장 | 확인할 기준 | 메모 |
|---|---|---|
| 한국 | 한국형 웹 콘텐츠 접근성 지침(KWCAG) 2.2 — 국가표준, 33개 검사항목. 공공기관 사이트와 장애인차별금지법 적용 대상 | 웹 접근성 인증 심사가 이 지침을 기준으로 한다. 검사항목은 WCAG 2.x AA와 대부분 겹치지만 1:1 대응은 아니므로 공공 발주면 지침 원문으로 대조한다 |
| EU | 유럽 접근성법(EAA) — 2025-06-28부터 적용. 기술 기준은 EN 301 549다. 법적으로 인용된 판은 v3.2.1(WCAG 2.1 AA)이고, 2026-09에 WCAG 2.2 AA를 채택한 v4.1.1이 공표돼 관보 인용을 기다린다 | 전자상거래·은행·전자책·교통 등 소비자 대상 서비스가 범위다 |
| 미국 | ADA(Title II 주·지방정부, Title III 민간), Section 508(연방 조달) | Title II 규칙(2024)은 WCAG 2.1 AA를 명시한다 — 준수 기한은 발주 시점에 원문으로 확인한다. Section 508은 WCAG 2.0 AA를 참조한다 |
designpaca의 하드 게이트(WCAG 2.2 AA)는 위 기준들의 공통 하한으로 잡은 것이다. 법이 요구하는 문서화(접근성 성명, 인증)는 디자인 산출물과 별도로 브리프에 적는다.
**프로젝트 계약.** AAA 기준([WCAG-FOCUS-APPEAR] 2.4.13 등)과 HIG·M3의 44/48px 같은 플랫폼 권장은 브리프가 그 수준을 선택했을 때만 적용된다. "모바일 앱 수준"이라는 말이 브리프에 있으면 project contract로 올라간다(§8).
**자동 점수는 준수가 아니다.** Lighthouse 접근성 100점이나 axe 통과는 도구가 검출할 수 있는 하위 집합만 확인한 것이다. 100점이 WCAG 준수와 같지 않고, 낮은 점수도 그 자체로 이슈 증거를 대체하지 않는다[SKILL-WEB-A11Y]. 통과 판정은 점수가 아니라 §13의 감사 루프(키보드 조작·스크린리더 확인 포함)로 내린다.
@ -129,7 +139,9 @@
## 3. 포커스
**하드 게이트.** `:focus-visible`만 스타일링하고 `:focus` 단독을 쓰지 않는다. 브라우저는 키보드·보조기술 포커스에서만 `:focus-visible`을 보여 주고 마우스 클릭에서는 억제한다 — 클릭에서는 이미 포커스가 명백하기 때문이다. 검증된 대체 없이 `outline: none`을 쓰지 않는다. 이는 눈으로 보는 키보드 사용자의 내비게이션 자체를 지운다[SKILL-BETTER-A11Y].
**하드 게이트.** 키보드로 포커스한 요소에는 **보이는 포커스 표시**가 있어야 하고, 그 표시는 인접 색과 3:1 이상이다. 검증된 대체 없이 `outline: none`을 쓰지 않는다. 이는 눈으로 보는 키보드 사용자의 내비게이션 자체를 지운다[SKILL-BETTER-A11Y].
**프로젝트 계약 — `:focus-visible`로 스타일링한다.** 브라우저는 키보드·보조기술 포커스에서 `:focus-visible`을 보여 주고 버튼·링크의 마우스 클릭에서는 억제한다. 텍스트 입력은 클릭으로 포커스해도 `:focus-visible`이 맞는다(입력 위치를 보여 줘야 하므로). `:focus` 단독 스타일은 WCAG 위반이 아니라 마우스 클릭에도 링이 남는 스타일 선택이다 — 프로젝트가 그 동작을 원하면 써도 된다.
```css
/* 가장 안전: 브라우저 기본 링을 유지하고 여백만 준다 */
@ -223,6 +235,8 @@ el.addEventListener('keydown', (e) => {
**네이티브 `<dialog>` + `showModal()`을 우선 고려한다.** 트랩·배경 `inert`·Escape 처리를 공짜로 얻는다. `<dialog>`는 Widely available 상태다(확인 2026-09-24, baseline 2022-03-14)[WEB-BASELINE]. 기하 계약(위치·중심·scroll owner)은 [audit-gate.md](audit-gate.md)와 [preflight.md](preflight.md) §4-1이 정하므로 여기서 반복하지 않는다. 이 절은 **포커스 관리**만 다룬다.
**여닫기는 선언으로 먼저 푼다.** `<button commandfor="dlg" command="show-modal">`·`command="close"`는 스크립트 없이 `<dialog>`를 여닫는다. Baseline Newly(2025-12-12, Safari 26.2가 마지막)이므로 그보다 오래된 브라우저를 지원하면 같은 버튼에 `click` 핸들러 폴백을 둔다(`if (!('commandForElement' in HTMLButtonElement.prototype))`). 바깥 클릭으로 닫기는 `closedby="any"`가 표준이지만 Safari 안정판이 아직 지원하지 않는다(확인 2026-10-06) — Safari용 backdrop 클릭 처리를 함께 두고, 파괴적 확인 다이얼로그는 기본값을 써서 실수로 닫히지 않게 한다. [WEB-BASELINE]
네이티브를 쓸 수 없는 커스텀 오버레이는 `role="dialog"` + `aria-modal="true"` + `aria-labelledby`(제목 id)를 갖추고, 아래를 구현한다[SKILL-BETTER-A11Y]:
```js
@ -247,11 +261,15 @@ triggerEl?.focus();
## 5. 구조
**하드 게이트 — 스킵 링크.** 반복되는 내비게이션을 건너뛸 수 있는 링크를 페이지 첫 포커스 가능 요소로 둔다. 포커스 전에는 화면 밖에 숨긴다.
**하드 게이트 — 반복 블록 우회 수단(2.4.1, A).** 여러 페이지에 반복되는 내비게이션을 건너뛸 수단이 있어야 한다. 랜드마크·헤딩 구조로도 충족되지만, 스크린리더를 쓰지 않는 키보드 사용자에게는 **스킵 링크가 사실상 유일한 수단**이므로 기본으로 둔다(권장). 페이지 첫 포커스 가능 요소로 두고, 포커스 전에는 보이지 않게 한다.
```css
.skip-link { position: absolute; inset-inline-start: -999px; }
.skip-link:focus { inset-inline-start: 16px; top: 16px; }
/* 화면 밖으로 밀지 않고 잘라서 숨긴다 — RTL에서 음수 inset이 가로 넘침을 만드는 문제를 피한다 */
.skip-link:not(:focus) {
position: absolute; width: 1px; height: 1px; overflow: hidden;
clip-path: inset(50%); white-space: nowrap;
}
.skip-link:focus { position: absolute; inset-block-start: 16px; inset-inline-start: 16px; }
```
```html
@ -271,6 +289,8 @@ triggerEl?.focus();
`div`로 짠 레이아웃을 시맨틱 랜드마크로 바꾸는 것 자체가 흔히 빠뜨리는 항목이다[SKILL-WEB-A11Y].
**하드 게이트 — 도움말 위치 일관성 [WCAG-HELP] (3.2.6, A, WCAG 2.2 신규).** 여러 페이지에 연락처·고객센터·채팅·FAQ 링크 같은 도움 수단을 두면, 페이지마다 같은 상대 순서(예: 늘 푸터 마지막, 늘 헤더 오른쪽 끝)에 둔다. 도움 수단을 두라는 요구가 아니라, 둔다면 위치를 흔들지 말라는 요구다.
**헤딩 순서.** h1 하나, 레벨을 건너뛰지 않는다(기준은 [preflight.md](preflight.md) §1). 헤딩은 구조이지 스타일이 아니다 — 크기 때문에 태그를 고르지 말고, 레벨은 문서 구조로 정한 뒤 시각적 크기는 CSS로 별도로 입힌다[SKILL-BETTER-A11Y].
**링크 텍스트 — [WCAG-LINK] (2.4.4).** 링크의 목적은 링크 텍스트만으로, 또는 텍스트와 프로그램적으로 확인 가능한 맥락을 합쳐서 알 수 있어야 한다. 스크린리더의 링크 목록 모드는 맥락 없이 텍스트만 읽으므로, "더보기"·"여기"·"클릭"을 단독으로 쓰지 않고 목적어를 포함한다("가격표 보기"). 실제 카피 문구·톤은 [product-copy.md](product-copy.md)를 따른다. 새 탭으로 여는 링크는 `rel="noopener"`와 함께 "새 탭에서 열림"을 알리는 숨김 텍스트(`.sr-only`)를 둔다.
@ -279,6 +299,8 @@ triggerEl?.focus();
## 6. 폼
이 절은 폼의 접근성 계약(레이블·자동완성·오류 배선)을 정한다. 배치·필수 표시·오류 위치·한국 폼 관례(이름·휴대폰·주소·본인인증)는 [component-systems.md](component-systems.md) §10이 정본이다.
### 6.1 레이블
**하드 게이트.** 모든 컨트롤은 프로그램적 레이블이 있어야 한다 — `id`를 가리키는 `<label for>` 또는 감싸는 `<label>`. **placeholder는 레이블이 아니다.** 입력하는 순간 사라지고 대개 대비 기준도 통과하지 못한다[SKILL-BETTER-A11Y].
@ -368,10 +390,11 @@ triggerEl?.focus();
- `aria-describedby`가 필드와 인라인 오류를 연결해 스크린리더가 필드와 함께 오류를 읽게 한다.
- 오류는 필드 옆에 인라인으로, 아이콘이나 텍스트와 함께 렌더한다. **빨간 테두리만으로는 안 된다** — 색상 단독 신호다.
- 제출 시 **첫 번째로 실패한 필드에 포커스를 이동한다.** 포커스 이동 자체가 announcement이므로 별도 알림이 필요 없다.
- **필드가 5개 이상이거나 오류가 여럿이면** 폼 상단에 오류 요약을 둔다. 요약은 제목(예: "입력 내용 3건을 확인해 주세요")과 각 오류에서 해당 필드로 가는 링크(`<a href="#email">`)로 만들고, 제출 시 요약 컨테이너(`tabindex="-1"`)로 포커스를 옮긴다. 요약이 없는 짧은 폼은 위 규칙대로 첫 오류 필드로 옮긴다. 인라인 오류는 요약이 있어도 그대로 둔다.
- 제출 전 값은 trim한다 — 자동완성과 텍스트 확장이 뒤 공백을 남긴다.
- 폼 레벨 오류(특정 필드에 묶이지 않는 실패)에만 `role="alert"`를 예약한다(§7).
**관찰 후보 — 검증 시점 [SKILL-APPLE-DESIGN].** 필드를 벗어날 때(blur) 또는 타이핑 중 짧은 디바운스 후 검증하고, 제출 버튼을 누른 뒤에야 오류를 보여주지 않는다. 불완전한 상태로도 제출을 허용해 검증이 드러나게 한다 — 타이핑 자체를 막거나 문자를 필터링하지 않는다.
**관찰 후보 — 검증 시점.** 형식 오류는 필드를 벗어날 때(blur) 보여 준다. 사용자가 아직 입력 중인 값에 오류를 띄우지 않는다(타이핑 중 검증은 입력이 끝나기 전에 틀렸다고 말하는 조기 오류가 된다). 이미 오류가 표시된 필드는 고치는 순간 바로 해제한다 — "늦게 벌하고 빨리 상준다". 제출 시에는 남은 오류를 모두 보여 주고 첫 오류로 포커스를 옮긴다. 불완전한 상태로도 제출 버튼을 누를 수 있게 해 검증이 드러나게 한다 — 타이핑 자체를 막거나 문자를 필터링하지 않는다 [SKILL-APPLE-DESIGN]. 비밀번호 강도처럼 입력하며 충족 여부를 보여 주는 안내는 오류가 아니라 진행 표시이므로 예외다.
### 6.9 세션 타이밍
@ -400,6 +423,8 @@ triggerEl?.focus();
<div role="status" class="sr-only" id="status-region"></div>
```
보이지 않는 live region 대신 `element.ariaNotify('저장됨')`으로 직접 알릴 수도 있다(세 엔진 출하, Safari 27이 마지막 — 확인 2026-10-06)[WEB-BASELINE]. 아직 Widely가 아니므로 `if ('ariaNotify' in Element.prototype)`으로 분기하고 폴백은 위의 빈 `role="status"` 영역이다. 화면에 보이는 토스트가 있으면 그 요소의 `role="status"`가 먼저다. 실제 스크린리더에서 한 번만 읽히는지 확인한다.
로딩 상태는 갱신되는 영역에 `aria-busy="true"`를 걸고, "불러오는 중…"을 polite로 알린 뒤 결과("12개 결과를 불러왔습니다")를 announce한다.
**토스트 특화 규칙**[SKILL-BETTER-A11Y]:
@ -480,6 +505,8 @@ triggerEl?.focus();
.tooltip:hover { visibility: visible; }
```
조상의 `overflow`에 잘리는 툴팁은 `popover` 속성으로 top layer에 올리고 위치는 anchor positioning으로 잡는다(구현은 [motion.md](motion.md) §3 코드 D). `popover="hint"`와 `interestfor`는 Safari가 지원하지 않으므로(확인 2026-10-06) 호버·포커스로 여는 동작은 아직 스크립트로 붙인다 — 위 세 조건은 구현 방식과 무관하게 그대로 판정한다. [WEB-BASELINE]
---
## 10. 드래그 대안 — [WCAG-DRAG] (2.5.7, AA, WCAG 2.2 신규)
@ -525,8 +552,8 @@ triggerEl?.focus();
| 신호 | 지원 상태 | 판정 |
|---|---|---|
| `prefers-reduced-motion` | Widely available | **하드 게이트.** 무엇을 끄고 무엇을 남기는지는 [motion.md](motion.md)가 정본이다 |
| `prefers-contrast` | Widely available(2022-05부터) | **프로젝트 계약.** `more` 값에 반응해 대비를 높이는 것은 요구되면 지킨다 |
| `forced-colors` | Widely available(2022-09부터) | **하드 게이트** — §3의 규칙을 따른다 |
| `prefers-contrast` | Widely available(Baseline 2022-05 Newly, 2024-11경 Widely) | **프로젝트 계약.** `more` 값에 반응해 대비를 높이는 것은 요구되면 지킨다 |
| `forced-colors` | Widely available(Baseline 2022-09 Newly, 2025-03경 Widely) | **하드 게이트** — §3의 규칙을 따른다 |
| `prefers-reduced-transparency` | Limited — Chrome/Edge만 구현하고 Firefox·Safari는 미지원, Apple이 표준화에 반대 의견을 낸 상태 | **점진적 향상** — 유리 재질을 쓰는 프로젝트에서는 프로젝트 계약([elevation.md](elevation.md) §8). 반투명 표면을 불투명하게 낮추는 구체 레시피는 [elevation.md](elevation.md)·[svg-filters.md](svg-filters.md)가 조건부로 다룬다 |
**색만으로 의미를 전달하지 않는다.** 이 규칙 자체는 부록 A에서 다룬다.
@ -537,9 +564,9 @@ triggerEl?.focus();
**증거 우선 1~4단계 루프**를 닫힌 상태로 돈다 — 수정 후 같은 감사를 다시 돌리지 않으면 통과라고 말할 수 없다. 5는 그 루프를 통과한 뒤 추가로 확인하는 항목이다.
1. **자동 감사.** Chrome DevTools MCP가 있으면 `lighthouse_audit`(accessibility 카테고리)를 쓴다. 없으면 `npx lighthouse <url> --only-categories=accessibility` 또는 `npx @axe-core/cli <url>`로 대체한다. 프로젝트에 `axe-core` 가 있으면 [audit-gate.md](audit-gate.md)의 게이트가 뷰마다 axe 를 함께 돌린다. 도구 목록과 선택 기준은 [harness.md](harness.md) §6을 따른다.
1. **자동 감사.** 브라우저 개발자 도구 연동이 있으면 그 도구의 Lighthouse 접근성 감사를 쓴다. 없으면 `npx lighthouse <url> --only-categories=accessibility` 또는 `npx @axe-core/cli <url>`로 대체한다. 프로젝트에 `axe-core` 가 있으면 [audit-gate.md](audit-gate.md)의 게이트가 뷰마다 axe 를 함께 돌린다. 도구 목록과 선택 기준은 [harness.md](harness.md) §6을 따른다.
2. **실패 노드 국소화.** 감사가 지목한 노드로 대상 컴포넌트를 좁힌다.
3. **수동 확인.** Chrome DevTools MCP가 있으면 `take_snapshot`으로 접근성 트리(이름·역할·상태·랜드마크·헤딩)를 검사한다. 영향받는 플로우를 실제로 키보드로 조작한다. 스크린리더를 켤 수 있으면 최소 단축키로 확인한다 — macOS VoiceOver는 `Cmd+F5`(또는 Touch ID 3회 클릭)로 켜고 끄며 `Ctrl+Option+화살표`로 항목을 이동하고 `Ctrl+Option+U`가 로터(헤딩·링크 목록)를 연다. Windows NVDA는 `Ctrl+Alt+N`으로 시작, `Insert+Q`로 종료, `H`가 다음 헤딩, `K`가 다음 링크, `Insert+F7`이 요소 목록을 연다. 스크린리더를 켤 수 없는 환경에서는 [preflight.md](preflight.md)의 대체 절차(읽기 순서 확인)를 따른다.
3. **수동 확인.** 브라우저 개발자 도구 연동이 있으면 접근성 트리 스냅숏으로 이름·역할·상태·랜드마크·헤딩을 검사한다. 영향받는 플로우를 실제로 키보드로 조작한다. 스크린리더를 켤 수 있으면 최소 단축키로 확인한다 — macOS VoiceOver는 `Cmd+F5`(또는 Touch ID 3회 클릭)로 켜고 끄며 `Ctrl+Option+화살표`로 항목을 이동하고 `Ctrl+Option+U`가 로터(헤딩·링크 목록)를 연다. Windows NVDA는 `Ctrl+Alt+N`으로 시작, `Insert+Q`로 종료, `H`가 다음 헤딩, `K`가 다음 링크, `Insert+F7`이 요소 목록을 연다. 스크린리더를 켤 수 없는 환경에서는 [preflight.md](preflight.md)의 대체 절차(읽기 순서 확인)를 따른다.
4. **수정 후 동일 감사 재실행.** 1~3단계를 그대로 다시 돌려 재발이 없는지 확인한다. 재실행하지 않은 수정은 미검증이다.
5. **고대비 모드 확인.** `forced-colors: active`(Windows 고대비)를 에뮬레이션하거나 실제로 켜서, 색만으로 전달되던 정보가 시스템 색 치환 후에도 남아 있는지 본다(§3).