feat(skill): absorb external design skills, restore interview gate, add review route

- Restore the step-0 interview as a mechanical gate the skill explicitly
  depends on; add harness.md (per-harness question tools, limits,
  fallbacks) and brief-interview.md (slots, question cards, rounds).
- Add 10 reference docs absorbed from external design skills
  (accessibility, interaction-feel, elevation, color, icons, product-copy,
  component-systems, critique, change-review, print-email) and extend
  existing references.
- Add a review-only route and two hard-gate clauses (truncated content
  reachability, three-flashes limit).
- design-gate: split tap targets into WCAG 2.5.8 and 44px contract layers,
  run axe-core when available, and fix false positives found on a real
  site (decorative alt="", stacked wordmark line count, url-only pages).
- lint-skill: fail if the interview gate section or its links disappear.
- Ship agents/openai.yaml and THIRD_PARTY_NOTICES.md.
This commit is contained in:
Yun Chan 2026-09-24 13:26:03 +09:00
parent 79e79c120b
commit 6805fb2be7
37 changed files with 5688 additions and 128 deletions

View file

@ -2,6 +2,8 @@
4-1 단계에서 읽는다. **이 단계가 끝나면 아무 이펙트 없이도 완성된 페이지**여야 한다. 그것이 이후 모든 폴백의 기반이다.
접근성 규범(포커스·키보드·라이브 리전·히트 영역)은 [accessibility.md](accessibility.md), 입력 촉감·제스처·스프링은 [interaction-feel.md](interaction-feel.md), 아이콘 정렬·상태·RTL 뒤집기는 [icons.md](icons.md), 인쇄·이메일 적응은 [print-email.md](print-email.md)에서 각각 다룬다. 이 문서는 그리드·시선 흐름·타이포그래피 적용·반응형 구조를 다룬다.
---
## 1. 그리드를 먼저 정한다
@ -43,6 +45,10 @@
- 그리드에서 한 칸을 의도적으로 비우기
- 카드 하나만 규칙을 깨고 밖으로 나가기
### 선택자 특정성은 조용히 상쇄된다
**관찰 후보 — 엔지니어링 관행.** 상위 컨테이너 클래스(`.section`)와 하위 컴포넌트 클래스(`.cta`)는 특정성이 같아(0,1,0) 로드 순서가 이긴다. `section.hero`(0,1,1)처럼 특정성이 다르면 로드 순서와 무관하게 한쪽이 이긴다. 4-e의 초광폭 분할 히어로 사례(상위 container selector와 wide stage의 `padding-inline` 중 한쪽만 남아 텍스트 폭이 붕괴한 것)는 이 원칙의 구체형이다. 4단계에서 새 컴포넌트 선택자를 쓸 때는 그 선택자가 덮으려는 상위 선택자의 특정성과, 두 선택자가 같은 속성을 정의하는지를 함께 확인한다. [SKILL-FRONTEND-DESIGN]
---
## 2. 시선 흐름
@ -54,6 +60,20 @@
3. 강조하거나 역할을 전환해야 할 때는 리듬·크기·여백·색의 차이로 시선을 머물게 할 수 있다. 모든 섹션의 리듬을 일부러 깨야 하는 것은 아니다
4. 스크롤 뒤에도 새 정보나 과업 진전이 있는지 확인한다. 반복 카드가 이탈을 만든다는 보편 증거는 없으므로, 실제 콘텐츠와 사용자 행동으로 판단한다
### 그루핑 도구에는 선호 순서가 있다
**관찰 후보 — 시작값.** 관련 요소를 묶을 때는 이 순서로 검토한다.
1. **네거티브 스페이스** — 기본값. 관련 항목은 가깝게, 무관한 항목은 멀게 둔다.
2. **배경 도형** — 카드·채워진 컨테이너. 그룹이 반드시 하나의 단위로 읽혀야 할 때(선택 가능한 행, 드래그 가능한 카드 등)만 쓴다.
3. **구분선** — 최후 수단. 공간이 너무 비싼 밀집 데이터(표, 긴 설정 목록)에서만 쓴다. 헤어라인 두께, 낮은 대비로 두고, 이미 공간이 구조를 만들었다면 큰 gap과 중복해서 쓰지 않는다.
그룹 내 간격과 그룹 간 간격의 비율은 **1:2를 시작값**으로 둔다 — 그룹 내 `--space-2`(8px)면 그룹 간은 `--space-3`(16px) 이상. 비율이 이보다 좁으면 눈이 그룹 경계를 못 잡고 노이즈로 읽는다. 프로젝트 spacing 스케일에 이미 값이 있으면 그것을 그대로 쓰고, 이 비율로 다시 맞추지 않는다. [SKILL-BETTER-LAYOUT]
### 뷰당 주요 액션은 하나다
**관찰 후보.** 첫 화면은 목차이지 책 전체가 아니다. 뷰당 주요 액션을 하나로 정하고(색으로 강제하는 방법은 [color.md](color.md) 참고), 보조 액션이 2~3개를 넘으면 메뉴 뒤로 묶는다. 레벨 1에서 모든 것을 보여주는 긴 뷰보다, 더 깊이 링크하는 짧은 뷰를 우선한다. [SKILL-BETTER-LAYOUT]
### 의미가 다르면 섹션 토폴로지도 달라야 한다
일관성은 같은 거시 그리드를 복제하는 것이 아니다. **연속한 섹션의 역할이 다른데 모두 `왼쪽 큰 제목 + 오른쪽 목록`이면 구조 슬롭**이다. 색·배경·카드 radius를 바꿔도 읽는 동작은 같다.
@ -91,6 +111,57 @@
닫힘 상태에서 목록 오른쪽 끝과 작업대 오른쪽 끝의 차이를 2px 이하로 재고, 행 선택 뒤 2열이 복원되는 것도 함께 단언한다.
### 컨트롤은 정적 텍스트와 다르게 보여야 한다
**관찰 후보 — 양방향.** 모든 상호작용 요소는 배경·테두리·일관된 배치 구역 중 하나로 정적 텍스트와 구별되어야 한다. 주변 텍스트와 똑같이 스타일된 컨트롤은 컨트롤로 읽히지 않는다. 역방향 함정도 같은 비중으로 본다 — 옆의 진짜 버튼과 똑같이 생긴 클릭 불가 배지·라벨은 헛클릭을 모은다. [SKILL-BETTER-LAYOUT]
### 광학 정렬
아이콘이 있는 버튼, 재생 삼각형, 비대칭 아이콘처럼 기하학적 중심은 맞아도 눈에는 어긋나 보이는 보정은 [icons.md](icons.md) §6을 본다.
### 점진적 공개에는 눈에 보이는 단서가 필요하다
**관찰 후보 — 시작값.** 숨긴 콘텐츠에 단서가 없으면 없는 것과 같다. 프로젝트에 기존 disclosure 패턴이 있으면 그것을 쓰고, 없을 때만 아래 레시피를 시작값으로 검토한다.
- **피킹 아이템**: 가로 스크롤러·캐러셀에서 다음 카드가 컨테이너 엣지 너머로 `16px`~`32px` 삐져나오게 한다.
- **디스클로저 컨트롤**: 접힌 섹션에는 쉐브론이나 "더 보기"류 컨트롤을 두되, 숨겨진 개수를 라벨에 명시한다 — 예: "결과 12개 더 보기". 개수 없는 "더 보기"만으로는 약하다.
- **잘림 단서**: 클램프된 텍스트는 말줄임표와 함께 전체 값에 도달할 수단(펼치기·링크·툴팁)을 같이 둔다.
```css
.scroller {
--peek: 24px; /* 다음 카드가 비치는 폭 — 시작값 16~32px, 실측 조정 */
display: flex;
gap: var(--space-2);
overflow-x: auto;
padding-inline: var(--space-4);
scroll-padding-inline: var(--space-4);
scroll-snap-type: x mandatory;
}
.scroller > * {
/* 100% 는 패딩을 뺀 콘텐츠 폭이다. 오른쪽 패딩 영역에도 다음 카드가 비치므로 그만큼 덜 뺀다 */
flex: 0 0 calc(100% - var(--space-2) - max(0px, var(--peek) - var(--space-4)));
scroll-snap-align: start;
}
```
실제 렌더에서 피크 폭이 16~32px인지 확인한다.
[SKILL-BETTER-LAYOUT]
### 표의 숫자는 끝에, 텍스트는 시작에 정렬한다
**관찰 후보.** 정렬 기준선의 작은 집합을 고르고 고수한다. 표 안에서 숫자는 트레일링 엣지에, 텍스트는 리딩 엣지에 정렬한다 — 숫자 자체의 자릿수 정렬(`tabular-nums`)은 별개 축이며 [typography.md](typography.md)를 따른다. 물리적 left/right 대신 리딩/트레일링으로 사고하는 이유는 4-j "방향 독립 레이아웃" 절을 본다. [SKILL-BETTER-LAYOUT]
### 구체적인 내비 라벨이 우산 용어보다 낫다
**관찰 후보.** 내비게이션이나 섹션 라벨을 지을 때 "홈"·"메뉴" 같은 우산 용어보다, 그 화면이 실제로 하는 일을 가리키는 구체적 이름("진행 상황"·"보관함")이 더 잘 읽힌다. 구체적 라벨은 클릭 전에 무엇을 보게 될지 예측하게 한다. [SKILL-APPLE-DESIGN]
### 모달 액션 행은 스크롤 영역과 분리한다
**프로젝트 계약.** 리사이즈 가능한 패널 바닥, 고정 높이 모달의 폴드 아래, 확장하는 키보드 뒤처럼 잘릴 수 있는 자리에 확인·취소 같은 핵심 액션을 두지 않는다. 모달 콘텐츠가 스크롤되면 액션 행은 함께 스크롤되지 않고 안정된 자리(스티키 footer 또는 뷰 상단)에 남는다. native `<dialog>` 포지셔닝·내부 scroll owner 검증은 [preflight.md](preflight.md)의 모달 체크리스트와 상호 참조한다. [SKILL-BETTER-LAYOUT]
액션이 잘려 도달할 수 없으면 조작 불능으로 하드 게이트다.
---
## 3. 타이포그래피
@ -132,11 +203,70 @@
`auto-fit` + `minmax`는 유동 반복 grid에 유용하다. 미디어쿼리와 container query는 구조·밀도·행동이 바뀌는 실제 실패 지점에서 선택한다.
### 입력 방식은 화면 크기와 별개 축이다
**관찰 후보 — 구현 패턴.** 터치스크린 노트북, 키보드가 달린 태블릿처럼 화면 크기만으로는 입력 방식을 알 수 없다. `pointer`·`hover` 미디어쿼리로 화면 크기 브레이크포인트와 별개 축으로 다룬다. 호버로만 접근되는 기능을 만들지 않는다는 원칙(아래 "모바일에서 먼저 확인해라") 자체는 이미 하드 게이트이고, 아래는 그 구현 패턴이다.
```css
/* 정밀 포인터(마우스·트랙패드) */
@media (pointer: fine) {
.button { padding: 8px 16px; }
}
/* 거친 포인터(터치·스타일러스) — 데스크톱 폭에서도 나타날 수 있다 */
@media (pointer: coarse) {
.button { padding: 12px 20px; }
}
/* 호버를 지원하는 디바이스 */
@media (hover: hover) {
.card:hover { transform: translateY(-2px); }
}
/* 호버를 지원하지 않는 디바이스(터치) — active로 대체 */
@media (hover: none) {
.card { /* hover 상태 없음 */ }
}
```
**프로젝트 계약 — 데스크톱을 고성능·비터치로 가정하지 않는다.** 넓은 화면이 강력한 디바이스·마우스 전용을 뜻하지 않는다. 저사양 노트북, 터치스크린 노트북, 접근성 보조기기가 모두 데스크톱 폭에 있을 수 있다. `pointer: coarse` 대응과 성능 예산을 데스크톱 폭에서도 함께 시험한다. 성능 예산 자체는 뷰포트가 아니라 [tokens.md](tokens.md) §5를 그대로 따른다. [SKILL-IMPECCABLE]
### 컨테이너 쿼리 — 크기부터, 스타일은 조건부
**관찰 후보 — 구현 기법.** 컴포넌트 단위 적응에는 뷰포트 기준 media query보다 컨테이너 쿼리를 우선 검토한다. 카드는 뷰포트가 아니라 자신이 속한 컬럼에 반응해야 좁은 사이드바 안에서도 깨지지 않는다.
```css
/* 크기 쿼리 — 2023년 2월부터 널리 지원 */
.card-list { container-type: inline-size; }
@container (max-width: 400px) {
.card { grid-template-columns: 1fr; }
}
/* 스타일 쿼리 — 2026년 5월부터 갓 지원(아직 널리는 아님), 폴백 경로를 함께 둔다 */
.theme-scope { container-type: inline-size; container-name: theme; }
@container theme style(--variant: compact) {
.card { padding: var(--space-2); }
}
```
크기 쿼리(`@container` 크기 조건)는 2023-02부터 널리(Widely) 지원되지만, 스타일 쿼리(`style()`)는 2026-05-19부터 갓(Newly) 지원되어 아직 모든 브라우저에 널리 퍼지지 않았다 — 스타일 쿼리에 의존하는 레이아웃은 지원 확인 후 폴백을 함께 둔다. [WEB-BASELINE]
### em·rem 브레이크포인트가 확대를 따라가는 이유
**관찰 후보.** `px` 단위 미디어쿼리는 사용자가 브라우저의 텍스트 확대(브라우저 줌이 아니라 base font-size 배율 조정)를 써도 반응하지 않는다. `em`·`rem` 단위 쿼리는 확대된 base font-size를 따라 브레이크포인트가 함께 움직인다. `preflight.md`가 이미 경고하는 "px 고정 폰트는 OS 확대를 게이트가 보장하지 못한다"는 문제의 해법 중 하나가 이것이다.
| 항목 | 단위 | 이유 |
|---|---|---|
| `font-size`, `max-width`, 브레이크포인트, 스케일링 spacing | `rem` | 확대된 base font-size에 반응해야 한다 |
| border, focus outline, box-shadow, 고정 장식 | `px` | 확대와 무관하게 일정한 두께를 유지해야 한다 |
이 표는 시작값이다. 프로젝트에 이미 다른 단위 관례가 있으면 그것을 따른다. [SKILL-BETTER-A11Y]
### 모바일에서 먼저 확인해라
데스크톱에서 아름다운 것이 모바일에서 무너지는 것이 기본이고, 그 반대는 드물다. 특히:
- 큰 타이포는 모바일에서 줄 수·가려짐·다음 행동의 가시성을 보고 크기·폭·문구·구도를 함께 조정한다. 단순 축소가 유일한 해법은 아니다
- 가로 스크롤이 생기는 요소를 찾아라 (`overflow-x: hidden` 으로 덮지 말고 원인을 고쳐라)
- 터치 타깃은 과업 빈도·오입력 위험과 WCAG 2.2 AA의 24×24 CSS px 최소 기준(예외 포함)을 함께 검토한다
- 터치 타깃은 과업 빈도·오입력 위험과 WCAG 2.2 AA의 24×24 CSS px 최소 기준(예외 포함)을 함께 검토한다. 시각 크기보다 히트 영역을 넓히는 구체 기법(의사요소 확장, 확장 영역 겹침 방지, 장식 레이어의 `pointer-events`)은 [accessibility.md](accessibility.md) §8을 본다
- 호버로만 접근되는 기능을 만들지 마라
반응형 장면은 폭만 기록하지 않는다. 높이·종횡비·zoom·스크롤·상태가 짧은 화면, 긴 화면, 확대에서 결과를 바꾸는지 먼저 보고 대표 viewport를 고른다. 프로젝트의 특정 수치를 공통 규칙으로 만들지 않으며, 실제 측정 artifact와 실패 처리는 [audit-gate.md](audit-gate.md)의 렌더 측정 계약을 따른다.
@ -217,7 +347,7 @@ new Set([...document.querySelectorAll('.nav a')]
1440px에서 멀쩡한 페이지가 1920·2560px에서 깨지는 방식은 반대다. 모바일처럼 넘치지는 않지만, 본문은 끝없이 늘어나고 고정 폭 헤드라인은 빈 여백에 밀려나며 표는 너무 좁아진다. **컨테이너 하나를 무조건 풀거나 조이지 마라.** 읽는 표면과 비교·명세 표면의 요구가 다르다.
```css
:root { --measure: 42rem; --container-wide: 74rem; }
:root { --measure: 42rem; /* 실제 폰트 기준 60~75자(한글 25~40자)로 재검증 */ --container-wide: 74rem; }
.prose { max-width: var(--measure); }
.specification { width: min(100% - 2 * var(--pad-inline), var(--container-wide)); }
@ -254,6 +384,114 @@ Apple도 다양한 화면 크기에서 적응형 레이아웃과 읽기 좋은
---
## 4-f. 데스크톱 어포던스 — 조건부
언제: 워크벤치·콘솔·전문가용 도구처럼 마우스·키보드 중심의 넓은 화면을 1차 대상으로 하는 프로젝트일 때만 연다. 일반 랜딩·마케팅 페이지에는 적용하지 않는다.
**관찰 후보.** 이런 프로젝트는 좁은 화면 패턴을 그대로 확대하지 않고, 넓은 입력 표면이 실제로 주는 것을 쓴다.
- 부가 정보용 hover 상태(단, 키보드·터치 대체 경로를 반드시 같이 둔다)
- 키보드 단축키(잦은 동작에)
- 우클릭 컨텍스트 메뉴
- 도움이 되는 곳의 드래그 앤 드롭
- Shift·Cmd로 다중 선택
- 여러 정보 패널을 동시에 노출(점진적 공개를 줄임)
이 목록은 워크벤치·콘솔의 시작값이지 일반 웹의 규범이 아니다 — 브리프가 다른 성격이면 열지 않는다. [SKILL-IMPECCABLE]
## 4-g. 모바일에서 표를 카드로 바꾼다
**관찰 후보 — 구현 패턴.** 좁은 화면에서 다열 표를 그대로 축소하면 가로 스크롤이나 텍스트 붕괴가 난다. `display: block`과 `data-label` 속성으로 각 셀을 헤더-값 쌍으로 보여주는 카드로 바꾸는 방법을 시작값으로 검토한다.
```css
@media (max-width: 40rem) {
table, thead, tbody, th, td, tr { display: block; }
thead { display: none; }
tr { margin-block-end: var(--space-3); }
td {
display: flex;
justify-content: space-between;
padding-inline-start: 50%;
position: relative;
}
td::before {
content: attr(data-label);
position: absolute;
inset-inline-start: 0;
font-weight: 600;
}
}
```
```html
<td data-label="가격">₩12,000</td>
```
변환 후에는 표의 의미(헤더-값 관계)가 스크린리더에서도 유지되는지 확인한다 — 시각적으로 카드가 되어도 마크업은 여전히 `<table>`이어야 관계가 보존된다. 다만 일부 브라우저는 표 요소의 `display`를 바꾸면 표 의미 자체를 접근성 트리에서 버린다. 변환할 때는 `role="table"`·`rowgroup`·`row`·`columnheader`·`cell`을 명시해 두고, 접근성 트리 스냅샷이나 스크린리더로 실제로 확인한다([accessibility.md](accessibility.md) §13). 레이아웃 검증(카드로 보이는지)과 의미 검증(관계가 읽히는지)은 별개이며 둘 다 확인한다. [SKILL-IMPECCABLE]
## 4-h. 전문가 도구는 밀도를 보존한다
언제: 프로젝트가 이미 확립된 밀도(콘솔, 관리자 도구, 데이터 그리드)를 갖고 있을 때.
**관찰 후보.** 컴팩트한 전문가용 도구는 히트 영역이 겹치지 않고 컨트롤이 구별되는 한, 위 "그루핑 도구" 절의 간격 시작값보다 적게 써도 된다. 확립되고 실제로 쓰이고 있는 밀도를, 이 문서의 시작값에 맞추려고 컨트롤을 키우거나 간격을 벌리지 않는다 — 이는 사용자·기존 토큰을 기본값보다 우선하는 원칙과 같은 방향이다. [SKILL-BETTER-LAYOUT]
## 4-i. 브레드크럼 — 조건부
언제: 3단계 이상의 깊은 계층 구조를 가진 콘솔·문서형·대시보드류 프로젝트일 때. 얕은 계층의 랜딩·포트폴리오에는 가치가 낮다.
**관찰 후보.** 깊은 계층에서 컨텍스트를 유지하는 수단으로 브레드크럼을 후보로 검토한다. 좁은 화면에서는 마지막 1~2단계만 보이거나 상위 단계를 축약 기호로 접는다. [SKILL-IMPECCABLE]
## 4-j. 방향 독립 레이아웃
언제: 프로젝트가 RTL 로케일(아랍어·히브리어 등)을 지원하기로 했을 때만 연다. RTL을 지원하지 않는 프로젝트에는 이 절이 적용되지 않는다 — [icons.md](icons.md) §7의 아이콘 뒤집기 표가 이 절을 참조한다.
**프로젝트 계약(조건부).** 방향 의존 수평 위치는 물리 속성 대신 논리 속성으로 쓴다. `dir="rtl"`에서 레이아웃이 자동으로 미러된다.
| Physical | Logical |
|---|---|
| `margin-left` | `margin-inline-start` |
| `padding-right` | `padding-inline-end` |
| `left: 0` | `inset-inline-start: 0` |
| `text-align: left` | `text-align: start` |
| `border-right` | `border-inline-end` |
```css
/* 좋음 — dir="rtl"에서 자동 미러 */
.section { padding-inline: var(--space-4); }
.section .child { margin-inline-start: var(--space-3); }
/* 나쁨 — RTL에서 깨짐 */
.section { padding-left: 24px; padding-right: 24px; }
.section .child { margin-left: 16px; }
```
노치 대응 같은 **진짜 물리적 기하**나 제스처 방향처럼 언어와 무관한 것에는 물리 속성을 그대로 남긴다.
**진행 시퀀스는 미러되지만 손으로 배치한 요소는 아니다.** 별점, 스텝 인디케이터, 진행 바처럼 배치가 진행을 인코딩하는 요소는 RTL에서 시퀀스가 미러되어 트레일링부터 채워진다. 논리 속성을 쓴 flexbox·grid는 자동으로 미러되지만, `position: absolute` 같은 수동 배치 요소는 미러되지 않는다는 함정이 있다 — 별도로 확인한다. 숫자 내부의 자리 순서는 방향과 무관하게 절대 뒤집히지 않는다.
읽기 순서는 좌우가 아니라 **리딩→트레일링**으로 사고한다. 위 §2 "표의 숫자는 끝에, 텍스트는 시작에 정렬한다" 절의 끝·시작도 이 어휘를 쓴다. [SKILL-BETTER-LAYOUT]
## 4-k. 가짜 현지화 검사
언제: 프로젝트가 다국어(번역)를 지원할 때.
**프로젝트 계약(조건부).** 번역된 문자열은 늘어나고, **짧은 문자열일수록 비율적으로 더 늘어난다** — 그래서 한 단어짜리 버튼 라벨이 화면에서 가장 위험하다. 문자열 확장 폭은 언어와 원문 길이별로 다르므로 하나의 퍼센트 예산에 의존하지 않는다. 배포 전 가짜 현지화(pseudo-localization, 문자를 늘리고 특수문자로 감싸 실제 번역 없이 레이아웃 반응을 시험하는 기법)나 대표 로케일 1개로 실제 테스트한다.
- 텍스트 컨테이너에 고정 폭·고정 높이를 두지 않는다(줄바꿈을 허용하거나 `min-height`를 쓴다).
- 버튼은 라벨 길이에서 스스로 크기를 얻는다(`padding-inline`, 하드코딩된 폭 금지).
```css
/* 좋음 */
.button { padding-inline: var(--space-3); white-space: nowrap; }
/* 나쁨 — 독일어 등 긴 번역에서 잘린다 */
.button { width: 96px; overflow: hidden; }
```
[SKILL-BETTER-LAYOUT]
---
## 5. 이 단계의 통과 조건
- [ ] CSS/HTML만으로 페이지가 완성됐다. JS를 꺼도 읽힌다