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

@ -10,9 +10,11 @@
|---|---|
| 페이지를 인쇄·PDF 저장 가능하게 만든다 | §1 인쇄 |
| 인쇄 결과가 맞는지 확인한다 | §2 인쇄 검증 |
| 뉴스레터·트랜잭션 메일·초대장을 만든다 | §3 이메일 |
| 뉴스레터·트랜잭션 메일·초대장을 만든다 | §3 이메일 (스타일 두 층은 §3-1) |
| 받은편지함 표시·프리헤더·크기 한도·텍스트 대체본 | §3-2-b |
| 광고성(마케팅) 메일의 "(광고)"·수신거부 표기 | §3-2-c — 한국 수신자면 하드 게이트 |
| 이메일 클라이언트에서 실제로 확인한다 | §4 이메일 검증 |
| 출시 전 한 번에 훑는다 | §5 체크리스트 |
| 출시 전 한 번에 훑는다 | §5 자기 점검. 납품 판정은 [preflight.md](preflight.md) X11·X12 |
---
@ -157,12 +159,35 @@ addEventListener('afterprint', () => {
**프로젝트 계약**: 폭은 600px 안팎의 단일 컬럼을 기본값으로 한다. 화면이 넓어도 이메일 본문 폭을 늘리지 않는다 — 클라이언트의 미리보기 창, 모바일 메일 앱 폭이 그보다 좁은 경우가 흔하다[SKILL-IMPECCABLE].
**프로젝트 계약**: 레이아웃은 표(`<table>`) 기반으로 짜고, 스타일은 `<style>` 블록이 아니라 각 태그의 `style` 속성에 인라인으로 넣는다. `display: flex`나 `grid` 같은 최신 레이아웃 CSS, 외부 스타일시트 링크는 이메일에서 기대한 대로 렌더링되지 않을 수 있다는 것이 근거다[SKILL-IMPECCABLE].
**프로젝트 계약**: 레이아웃은 표(`<table role="presentation">`) 기반으로 짠다. `display: flex`·`grid` 같은 최신 레이아웃 CSS와 외부 스타일시트 링크는 이메일에서 기대한 대로 렌더링되지 않을 수 있다[SKILL-IMPECCABLE].
**프로젝트 계약 — 스타일은 두 층으로 둔다.**
- **핵심 스타일은 인라인**: 폭·여백·글꼴·색·버튼처럼 빠지면 메일이 깨지는 값은 각 태그의 `style` 속성에 둔다. `<style>`을 지우거나 무시하는 클라이언트·전달(forward) 경로가 있기 때문이다.
- **반응형·다크모드는 `<head>`의 `<style>`**: 모바일 폭 조정 미디어쿼리(`@media (max-width: 600px)`), 다크모드 대응(`@media (prefers-color-scheme: dark)`)은 인라인으로 표현할 수 없으므로 head의 `<style>` 블록에 둔다. Gmail도 head의 `<style>`과 폭 기준 미디어쿼리를 지원한다(Google 개발자 문서 "CSS support"). 다만 `<body>` 안의 `<style>`은 무시하는 클라이언트가 있으니 head에만 둔다.
- 손으로 두 벌을 맞추지 말고 인라이너(빌드 단계에서 `<style>` 규칙을 `style` 속성으로 병합하는 도구)로 핵심 스타일을 인라인화하고, 미디어쿼리는 head에 남긴다.
- 클라이언트별 지원 범위는 바뀐다. 쓰려는 속성은 채택 시점에 지원표(caniemail 등)로 확인하고 §4에서 실제로 연다.
```html
<table role="presentation" width="600" cellpadding="0" cellspacing="0" style="margin:0 auto; max-width:600px;">
<!doctype html>
<html lang="ko" dir="ltr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light dark">
<meta name="supported-color-schemes" content="light dark">
<title>주문이 접수되었습니다</title>
<style>
@media (max-width: 600px) { .container { width: 100% !important; } .px { padding-left: 16px !important; padding-right: 16px !important; } }
@media (prefers-color-scheme: dark) { .surface { background-color: #1c1c1e !important; } .ink { color: #f2f2f2 !important; } }
</style>
</head>
```
```html
<table role="presentation" class="container surface" width="600" cellpadding="0" cellspacing="0" style="margin:0 auto; max-width:600px; background-color:#ffffff;">
<tr>
<td style="padding:24px; font-family:Arial, sans-serif; font-size:16px; line-height:1.5; color:#1a1a1a;">
<td class="px ink" style="padding:24px; font-family:Arial, sans-serif; font-size:16px; line-height:1.5; color:#1a1a1a;">
본문 내용
</td>
</tr>
@ -185,6 +210,24 @@ addEventListener('afterprint', () => {
**프로젝트 계약**: 폼 제출, 다단계 흐름, 필터처럼 복잡한 상호작용은 이메일 안에서 구현하지 않는다. 버튼으로 웹의 해당 화면에 딥링크한다[SKILL-IMPECCABLE].
### 3-2-b. 받은편지함에서 보이는 것 — 제목·프리헤더·크기·텍스트 대체본
- **프리헤더**: 받은편지함 목록에서 제목 옆에 보이는 미리보기 문장을 본문 첫머리에 숨긴 텍스트로 직접 쓴다. 쓰지 않으면 "이메일이 보이지 않으면 여기를 누르세요" 같은 첫 줄이 미리보기로 나간다.
- **크기**: Gmail은 HTML이 약 102KB를 넘으면 본문을 잘라 "메시지 잘림" 링크로 숨긴다. 법정 표기·수신거부 링크가 보통 맨 아래에 있으므로 잘리면 그것부터 사라진다. 인라인화한 뒤의 최종 HTML 크기를 잰다.
- **plain-text 대체본**: `multipart/alternative`로 텍스트 버전을 함께 보낸다. 텍스트만 보는 클라이언트와 일부 스팸 필터가 이것을 읽는다.
- `<html lang="ko">`(외국어 메일이면 그 언어)와 `dir`을 둔다. 스크린리더 발음과 줄바꿈 규칙이 lang을 따른다.
### 3-2-c. 광고성 메일의 법정 표기 — 한국 수신자 대상
**하드 게이트(영리 목적의 광고성 메일에 한함).** 정보통신망법 제50조와 시행령은 영리 목적의 광고성 정보를 전자우편으로 보낼 때 표기 의무를 둔다. 디자인이 이 자리를 처음부터 확보하지 않으면 나중에 끼워 넣다 레이아웃이 깨진다.
- **제목이 시작되는 부분에 `(광고)`** 를 붙인다. 프리헤더나 본문에만 두면 안 된다.
- **본문에 전송자의 명칭과 연락처**(전자우편 주소·전화번호·주소 등)를 둔다.
- **수신거부 방법**을 본문에 분명히 둔다. 클릭 한 번으로 처리되는 수신거부 링크를 기본으로 하고, 링크 라벨은 한글로 쓰고 필요하면 `Unsubscribe`를 병기한다. 회색 6px 글씨처럼 찾기 어렵게 만들지 않는다 — 본문 대비 기준([color.md](color.md))을 그대로 적용한다.
- 가입 확인·영수증·비밀번호 재설정 같은 **거래 메일에는 광고를 섞지 않는다.** 섞으면 그 메일이 광고성 정보가 된다.
- 수신 동의 획득, 야간 전송 제한, 정기적인 수신 동의 확인 같은 발송 운영 의무는 디자인 범위 밖이지만, 브리프에 마케팅 메일이 있으면 운영 측에 확인할 항목으로 `design.md`에 적는다.
- 세부 문구·예외는 법령과 KISA 안내서로 확인하고, 확정 문구는 사용자(법무)에게 받는다. 다른 나라 수신자가 섞이면 그 나라 규정(예: 미국 CAN-SPAM, EU 개인정보 규정)도 브리프에서 확인한다.
### 3-3. 이미지가 막혀도, 어두운 테마에서도 읽힌다
**프로젝트 계약**: 많은 이메일 클라이언트가 기본적으로 이미지를 차단한 채 미리보기를 띄운다. 로고·CTA·핵심 문구를 이미지에만 담으면 이미지가 막힌 순간 그 내용이 통째로 사라진다. 핵심 텍스트(제목, 본문, 버튼 라벨)는 실제 HTML 텍스트로 쓰고, 이미지에는 내용을 설명하는 `alt`를 붙이며, 이미지가 있어야 할 자리에는 배경색을 지정해 이미지가 빠져도 빈 흰 칸이 아니라 브랜드 색 영역으로 보이게 한다.
@ -203,31 +246,36 @@ addEventListener('afterprint', () => {
브라우저 렌더링과 실제 이메일 클라이언트 렌더링은 별개다. 로컬 브라우저에서 HTML 파일을 열어 봤다고 이메일에서도 같다고 보고하지 않는다.
1. **실제 클라이언트로 확인한 경우만 "검증됨"으로 보고한다.** 예를 들어 실제 계정으로 지메일·아웃룩·애플 메일 등에 발송해 열어 보고, 폭·표 레이아웃·버튼·이미지 차단 상태·어두운 테마에서 각각 확인한다.
2. **확인하지 못했으면 "미검증"이라고 명시한다.** 코드가 규칙(§3)을 따랐다는 것과 실제 클라이언트에서 그대로 보인다는 것은 다른 주장이다. "표 레이아웃과 인라인 CSS를 썼으니 호환될 것이다"는 추정이지 검증이 아니다.
2. **확인하지 못했으면 "미검증"이라고 명시한다.** 코드가 규칙(§3)을 따랐다는 것과 실제 클라이언트에서 그대로 보인다는 것은 다른 주장이다. "표 레이아웃과 인라인 CSS를 썼으니 호환될 것이다"는 추정이지 검증이 아니다. 모바일 폭(미디어쿼리 적용 여부)과 다크모드는 따로 확인한다.
3. **클라이언트별 지원 수치·점유율을 지어내지 않는다.** 이 스킬은 "아웃룩이 몇 퍼센트를 지원한다" 같은 구체 수치를 확인하지 않았다. 필요하면 사용자에게 발송 대상 클라이언트 범위를 물어 §3의 어떤 항목을 더 엄격하게 지킬지 좁힌다.
---
## 5. 체크리스트
## 5. 체크리스트 (구현 중 자기 점검 — 납품 판정은 preflight.md X11·X12)
납품 판정은 [preflight.md](preflight.md) X11(인쇄)·X12(이메일)가 한다.
**인쇄**
- [ ] `@media print`로 앱 크롬(내비·툴바·플로팅 버튼)을 숨겼다
- [ ] 모달이 열린 채 인쇄되면 그 모달만 서류가 된다
- [ ] 카드·표 행이 페이지 경계에서 잘리지 않는다(`break-inside: avoid`)
- [ ] 상태색은 `print-color-adjust: exact`로 보존하되, 색만으로 상태를 구분하지 않는다(흑백에서도 구분 가능)
- [ ] 짧은 링크 텍스트가 전체 URL로 펼쳐진다
- [ ] 접힌 섹션(`details` 등)이 인쇄에서는 펼쳐진다
- [ ] 여러 장 문서면 페이지 번호·문서 제목이 있다
- [ ] 인쇄 미리보기와 PDF 출력을 실제로 열어 확인했다(§2). 못 했으면 미검증이라고 적었다
- `@media print`로 앱 크롬(내비·툴바·플로팅 버튼)을 숨겼다
- 모달이 열린 채 인쇄되면 그 모달만 서류가 된다
- 카드·표 행이 페이지 경계에서 잘리지 않는다(`break-inside: avoid`)
- 상태색은 `print-color-adjust: exact`로 보존하되, 색만으로 상태를 구분하지 않는다(흑백에서도 구분 가능)
- 짧은 링크 텍스트가 전체 URL로 펼쳐진다
- 접힌 섹션(`details` 등)이 인쇄에서는 펼쳐진다
- 여러 장 문서면 페이지 번호·문서 제목이 있다
- 인쇄 미리보기와 PDF 출력을 실제로 열어 확인했다(§2). 못 했으면 미검증이라고 적었다
**이메일**
- [ ] 폭 600px 안팎, 단일 컬럼
- [ ] 표 기반 레이아웃, 인라인 CSS
- [ ] CTA가 버튼처럼 보이는 요소다(텍스트 링크가 아니다)
- [ ] 핵심 동작·정보가 hover에 의존하지 않는다
- [ ] 복잡한 상호작용은 웹으로 딥링크했다
- [ ] 이미지가 막혀도 핵심 텍스트가 읽히고, `alt`와 배경색을 지정했다
- [ ] 컨테이너 배경색을 명시해 다크모드 반전에 대비했다
- [ ] 실제 클라이언트에서 확인했다(§4). 못 했으면 미검증이라고 적었다
- 폭 600px 안팎, 단일 컬럼
- 표 기반 레이아웃, 핵심 스타일은 인라인, 반응형·다크모드 미디어쿼리는 head의 `<style>`(§3-1)
- `<html lang>`, `color-scheme`·`supported-color-schemes` 메타, 프리헤더, plain-text 대체본이 있다(§3-2-b)
- 최종 HTML이 약 102KB 미만이라 Gmail에서 잘리지 않는다(§3-2-b)
- **광고성 메일이면** 제목 첫머리 `(광고)`, 본문의 전송자 명칭·연락처, 찾기 쉬운 수신거부 링크가 있다. 거래 메일에 광고를 섞지 않았다(§3-2-c, 하드 게이트)
- CTA가 버튼처럼 보이는 요소다(텍스트 링크가 아니다)
- 핵심 동작·정보가 hover에 의존하지 않는다
- 복잡한 상호작용은 웹으로 딥링크했다
- 이미지가 막혀도 핵심 텍스트가 읽히고, `alt`와 배경색을 지정했다
- 컨테이너 배경색을 명시해 다크모드 반전에 대비했다
- 실제 클라이언트에서 확인했다(§4). 못 했으면 미검증이라고 적었다