designpaca/packages/skill/references/print-email.md
Yun Chan 88c11682b1 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
2026-10-06 16:55:38 +09:00

281 lines
20 KiB
Markdown

# print-email — 인쇄와 이메일
브리프가 인쇄 가능한 문서(출력·PDF 저장·서명용 서류)나 이메일(뉴스레터·트랜잭션 메일·초대장)을 명시할 때만 연다. 둘 중 하나만 필요하면 해당 절만 읽는다.
이 문서는 `SKILL.md` 4-5(서류로서의 완성)의 인쇄 규칙을 일반화한다. 4-5는 프리셋이 서류·원장·콘솔 계열(장부, 시간표, 관리 화면)일 때만 도는 좁은 규칙이고, 여기서는 **그 밖의 어떤 화면이든 브리프가 인쇄를 요구하면** 적용할 수 있게 넓힌다. 4-5의 규칙(모달이 열려 있으면 그 서류만, `print-color-adjust: exact`)과 모순되지 않으며, 겹치는 부분은 새로 쓰지 않고 그대로 가져와 쓴다.
## 이 문서를 읽는 법
| 상황 | 읽을 곳 |
|---|---|
| 페이지를 인쇄·PDF 저장 가능하게 만든다 | §1 인쇄 |
| 인쇄 결과가 맞는지 확인한다 | §2 인쇄 검증 |
| 뉴스레터·트랜잭션 메일·초대장을 만든다 | §3 이메일 (스타일 두 층은 §3-1) |
| 받은편지함 표시·프리헤더·크기 한도·텍스트 대체본 | §3-2-b |
| 광고성(마케팅) 메일의 "(광고)"·수신거부 표기 | §3-2-c — 한국 수신자면 하드 게이트 |
| 이메일 클라이언트에서 실제로 확인한다 | §4 이메일 검증 |
| 출시 전 한 번에 훑는다 | §5 자기 점검. 납품 판정은 [preflight.md](preflight.md) X11·X12 |
---
## 0. 언제 여는가
- 브리프에 "인쇄", "PDF로 저장", "출력해서 제출", "서명 후 인쇄" 같은 말이 있으면 §1·§2.
- 브리프에 "뉴스레터", "이메일로 보낸다", "가입 확인 메일", "영수증 메일", "초대 메일" 같은 말이 있으면 §3·§4.
- 둘 다 아니면 이 문서는 읽지 않는다. 모든 웹 페이지를 기본적으로 인쇄·이메일 대응시키지 않는다 — 그것은 이 문서가 조건부로 존재하는 이유 자체를 없앤다.
---
## 1. 인쇄
언제: 브리프가 인쇄 가능한 문서를 요구할 때. 프리셋이 서류·원장·콘솔이면 `SKILL.md` 4-5를 먼저 따르고, 이 절은 그 규칙이 없는 일반 페이지(랜딩, 포트폴리오, 안내문 등)에 인쇄 대응을 추가할 때 쓴다.
### 1-1. `@media print` 뼈대
**프로젝트 계약**: 인쇄가 요구되면 `@media print` 블록으로 화면과 서류를 분리한다. 화면에서 필요한 앱 크롬(내비, 툴바, 플로팅 버튼, 사이드바)은 인쇄에서 의미가 없다 — 사용자는 종이 위에서 그것을 누를 수 없다.
```css
@media print {
header.app-nav,
[role="toolbar"],
.floating-action,
.cookie-banner,
button:not(.print-keep) {
display: none !important;
}
}
```
**프로젝트 계약**: 모달이 열린 채로 인쇄가 시작되면 배경의 앱 화면이 아니라 **그 모달 내용만** 서류가 되어야 한다. `SKILL.md` 4-5의 기존 규칙을 그대로 쓴다.
```css
@media print {
body:has(dialog[open]) > *:not(:has(dialog[open])) {
display: none !important;
}
}
```
**관찰 후보(시작값)**: `@page`의 여백은 브라우저 기본값에서 시작한다. 제본이나 클립이 필요한 서류만 여백을 늘린다. 정확히 몇 cm가 맞는지는 프로젝트마다 다르므로 숫자를 단정하지 않고, 실제 출력물로 확인한다(§2).
```css
@page {
margin: 2cm 1.5cm; /* 제본 쪽에 더 필요하면 margin-left 만 늘린다 — 실측 조정 */
}
```
### 1-2. 콘텐츠를 서류에 맞게 바꾼다
**프로젝트 계약**: 카드·표 행·영수증 구획처럼 한 덩어리로 읽혀야 하는 블록은 페이지 경계에서 잘리지 않게 한다.
```css
@media print {
.card,
tr,
.receipt-line,
figure {
break-inside: avoid;
}
h2, h3 {
break-after: avoid; /* 제목만 페이지 끝에 남고 본문이 다음 장으로 넘어가는 것을 막는다 */
}
}
```
**프로젝트 계약**: 상태색처럼 의미를 나르는 색(경고·성공·실패 배지 등)만 `print-color-adjust: exact`로 보존한다. 나머지는 브라우저의 기본 인쇄 설정(그레이스케일·절약 모드 포함)에 맡긴다. 전체를 강제 컬러로 찍으면 사용자의 잉크 절약 설정을 무시하게 된다.
```css
@media print {
.status-badge,
.status-dot {
print-color-adjust: exact;
-webkit-print-color-adjust: exact;
}
}
```
**하드 게이트**: 화면에서 잘라 보여주던 것(짧은 링크 텍스트, 접힌 섹션)은 종이 위에서 클릭이나 hover로 나머지에 닿을 수 없다. [typography.md](typography.md) §13의 "잘린 텍스트 — 전체 값에 도달하는 수단" 원칙과 같은 이유로, 인쇄에서는 그 값 자체를 펼쳐 보여줘야 정보 손실이 없다.
```css
@media print {
a[href^="http"]:not([href*="#"])::after {
content: " (" attr(href) ")";
word-break: break-all;
}
}
```
닫힌 `<details>`의 내용은 브라우저가 내부 슬롯으로 숨기므로, 자식 요소에 `display`를 주는 CSS로는 확실히 펼쳐지지 않는다. 인쇄 직전에 `open`을 켜고 끝나면 되돌린다.
```js
let reopened = [];
addEventListener('beforeprint', () => {
reopened = [...document.querySelectorAll('details:not([open])')];
reopened.forEach((d) => { d.open = true; });
});
addEventListener('afterprint', () => {
reopened.forEach((d) => { d.open = false; });
reopened = [];
});
```
### 1-3. 서류의 신원 — 머리말·바닥말·출력일
**관찰 후보**: 여러 장으로 인쇄될 문서(보고서, 명세서)는 페이지 번호와 문서 제목을 각 장에 남긴다. 독자가 순서를 잃어버리지 않게 하는 목적이므로, 한 장짜리 인쇄물(영수증, 초대장)에는 강제하지 않는다.
```css
@media print {
@page {
@bottom-right { content: counter(page) " / " counter(pages); }
}
.print-footer::after {
content: "출력일 " attr(data-print-date);
}
}
```
`@page` 안의 여백 상자(`@bottom-right` 등)는 브라우저 지원이 고르지 않으므로 점진적 향상으로 둔다 — 지원하지 않는 브라우저에서는 페이지 번호가 빠질 뿐이다. 페이지 번호가 계약상 반드시 필요하면 인쇄 미리보기에서 대상 브라우저로 확인하고, 안 되면 PDF 생성 도구 쪽에서 넣는다. `attr(data-print-date)`는 CSS만으로 오늘 날짜를 넣을 수 없으므로, 인쇄 전에 JS로 `document.querySelector('.print-footer').dataset.printDate = new Date().toLocaleDateString('ko-KR')` 같은 식으로 채워 둔다.
---
## 2. 인쇄 검증
인쇄는 화면 렌더링과 다른 엔진 경로를 타기 때문에, 화면에서 괜찮아 보인다고 인쇄도 괜찮다고 결론 내리지 않는다.
1. **인쇄 미리보기를 캡처한다.** 브라우저의 인쇄 미리보기(Ctrl/Cmd+P)를 열어 각 페이지를 실제로 넘겨 본다. 브라우저 자동화 도구가 있으면 인쇄 미디어를 에뮬레이트해 스크린샷으로 남긴다 — 쓸 수 있는 도구는 [harness.md](harness.md)를 따른다.
2. **PDF로 저장해 연다.** "PDF로 저장"으로 실제 파일을 만들고 별도 뷰어로 열어, 브라우저 미리보기와 최종 PDF가 다르게 나오는 경우(특히 폰트·배경색)가 없는지 확인한다.
3. **경계에서 확인할 것**: 카드·표·영수증 구획이 페이지 경계에서 잘리지 않는지, 앱 크롬이 실제로 사라졌는지, 모달만 인쇄될 상황에서 배경 화면이 섞여 나오지 않는지, 링크 URL과 펼친 `details`가 실제로 종이에 보이는지, 상태색이 흑백 인쇄에서도 배지 모양(테두리·아이콘)만으로 구분되는지(색만으로 구분되면 흑백에서 정보가 사라진다).
4. **확인하지 못했으면 "미검증"이라고 적는다.** 소스만 보고 통과로 추정하지 않는다 — `audit-gate.md`의 미검증 관례를 그대로 따른다.
---
## 3. 이메일
언제: 브리프가 뉴스레터, 트랜잭션 메일(가입 확인·영수증·비밀번호 재설정), 초대장처럼 **이메일 클라이언트 안에서 열리는 HTML**을 명시할 때. 웹사이트에 "문의하기 → 메일 발송" 버튼이 있는 정도는 해당하지 않는다.
이메일 클라이언트는 웹 브라우저가 아니다. **관찰 후보 — 이 문서에서 실측하지 않았다.** 일부 클라이언트는 데스크톱 문서 편집기의 렌더링 경로를 그대로 쓰거나, 외부 스타일시트·최신 레이아웃 CSS를 지원하지 않거나 부분적으로만 지원한다는 관찰이 있다. 정확히 어떤 클라이언트가 무엇을 지원하는지는 이 문서에서 확인하지 않았으므로 클라이언트별 지원 수치를 단정하지 않는다. 아래는 그런 제약을 가정했을 때 안전한 기본값이며, 실제 확인은 §4를 따른다.
### 3-1. 레이아웃
**프로젝트 계약**: 폭은 600px 안팎의 단일 컬럼을 기본값으로 한다. 화면이 넓어도 이메일 본문 폭을 늘리지 않는다 — 클라이언트의 미리보기 창, 모바일 메일 앱 폭이 그보다 좁은 경우가 흔하다[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
<!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 class="px ink" style="padding:24px; font-family:Arial, sans-serif; font-size:16px; line-height:1.5; color:#1a1a1a;">
본문 내용
</td>
</tr>
</table>
```
### 3-2. 인터랙션
**프로젝트 계약**: 행동 유도는 텍스트 링크가 아니라 버튼처럼 보이는 요소로 만든다. 배경색을 채운 `<a>`를 표 셀 안에 넣는 방식이 흔한 구현이다[SKILL-IMPECCABLE](`<button>` 태그 자체는 이메일 클라이언트에서 일관되게 렌더링되지 않는 경우가 있어 피한다 — **관찰 후보, 이 문서에서 실측하지 않았다**).
```html
<a href="https://example.com/confirm"
style="display:inline-block; padding:12px 24px; background:#1a56db; color:#ffffff;
text-decoration:none; border-radius:6px; font-weight:600;">
가입 확인하기
</a>
```
**하드 게이트**: 대부분의 이메일 클라이언트는 hover 상태 자체가 없다(모바일 메일 앱, 미리보기 창). 핵심 동작(다음 단계로 가는 CTA, 정보 확인)이 hover에만 반응하도록 만들면 애초에 작동하지 않는다. 추가 정보나 보조 동작이 hover에 의존해서도 안 된다 — 항상 보이는 상태로 둔다[SKILL-IMPECCABLE].
**프로젝트 계약**: 폼 제출, 다단계 흐름, 필터처럼 복잡한 상호작용은 이메일 안에서 구현하지 않는다. 버튼으로 웹의 해당 화면에 딥링크한다[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`를 붙이며, 이미지가 있어야 할 자리에는 배경색을 지정해 이미지가 빠져도 빈 흰 칸이 아니라 브랜드 색 영역으로 보이게 한다.
```html
<img src="logo.png" alt="회사 로고" width="120"
style="display:block; background-color:#f3f4f6;">
```
**관찰 후보**: 일부 클라이언트는 사용자가 어두운 테마를 켰을 때 메일 배경·글자색을 자동으로 반전하거나 재조정한다. 이 동작은 클라이언트마다 다르고 이 문서에서 실측하지 않았으므로 어떤 클라이언트가 어떻게 반전하는지는 단정하지 않는다. 실무적으로 안전한 방향은 배경색을 투명에 맡기지 않고 컨테이너마다 명시적으로 지정하는 것, 로고처럼 투명 배경을 가정한 자산을 흰 배경 전용으로 만들지 않는 것이다. 실제 확인은 §4를 따른다.
---
## 4. 이메일 검증
브라우저 렌더링과 실제 이메일 클라이언트 렌더링은 별개다. 로컬 브라우저에서 HTML 파일을 열어 봤다고 이메일에서도 같다고 보고하지 않는다.
1. **실제 클라이언트로 확인한 경우만 "검증됨"으로 보고한다.** 예를 들어 실제 계정으로 지메일·아웃룩·애플 메일 등에 발송해 열어 보고, 폭·표 레이아웃·버튼·이미지 차단 상태·어두운 테마에서 각각 확인한다.
2. **확인하지 못했으면 "미검증"이라고 명시한다.** 코드가 규칙(§3)을 따랐다는 것과 실제 클라이언트에서 그대로 보인다는 것은 다른 주장이다. "표 레이아웃과 인라인 CSS를 썼으니 호환될 것이다"는 추정이지 검증이 아니다. 모바일 폭(미디어쿼리 적용 여부)과 다크모드는 따로 확인한다.
3. **클라이언트별 지원 수치·점유율을 지어내지 않는다.** 이 스킬은 "아웃룩이 몇 퍼센트를 지원한다" 같은 구체 수치를 확인하지 않았다. 필요하면 사용자에게 발송 대상 클라이언트 범위를 물어 §3의 어떤 항목을 더 엄격하게 지킬지 좁힌다.
---
## 5. 체크리스트 (구현 중 자기 점검 — 납품 판정은 preflight.md X11·X12)
납품 판정은 [preflight.md](preflight.md) X11(인쇄)·X12(이메일)가 한다.
**인쇄**
- `@media print`로 앱 크롬(내비·툴바·플로팅 버튼)을 숨겼다
- 모달이 열린 채 인쇄되면 그 모달만 서류가 된다
- 카드·표 행이 페이지 경계에서 잘리지 않는다(`break-inside: avoid`)
- 상태색은 `print-color-adjust: exact`로 보존하되, 색만으로 상태를 구분하지 않는다(흑백에서도 구분 가능)
- 짧은 링크 텍스트가 전체 URL로 펼쳐진다
- 접힌 섹션(`details` 등)이 인쇄에서는 펼쳐진다
- 여러 장 문서면 페이지 번호·문서 제목이 있다
- 인쇄 미리보기와 PDF 출력을 실제로 열어 확인했다(§2). 못 했으면 미검증이라고 적었다
**이메일**
- 폭 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). 못 했으면 미검증이라고 적었다