designpaca 초기 구현 — 스킬 · 설치 CLI · 배포 파이프라인

웹 디자인 파이프라인 스킬과 이를 5개 에이전트에 설치하는 CLI 를 담은 모노레포.

스킬 (packages/skill)
- SKILL.md 261줄 + 참조 문서 16개 3,349줄. progressive disclosure 로
  본문은 절차와 인덱스만, 지식은 references/ 로 분리
- 0~6단계 파이프라인. 규모에 따라 전체·연장·국소 세 경로로 분기
- 하드 게이트 12개는 grep·카운트로 검증 가능한 것만. 취향 판단은 제외
- 미학 프리셋 5종, AI 슬롭 지문 목록, 한글 조판 규칙,
  SVG 필터·three.js·인터랙티브 모션·HTML-in-Canvas 실전 지침

설치 CLI (packages/cli, packages/core)
- npx designpaca 온보딩 TUI. Claude Code · Codex · Cursor · Windsurf · AGENTS.md
- 매니페스트에 설치 시점 해시를 기록해 사용자가 고친 파일은 update 가 건너뛴다
- 타깃별로 본문의 references/ 경로를 실제 설치 위치로 재작성
- AGENTS.md 는 항상 로드되므로 본문 대신 303자 포인터만 주입
- Windsurf 는 12,000자 상한 초과 시 설치를 차단

배포 (build/ci, .forgejo/workflows)
- 태그 v* → 검사·테스트·빌드 → npmjs 배포 + Forgejo 레지스트리 미러
  → draft 릴리스 → Cloudflare Pages. 재실행 멱등

근거 (research/)
- 약 250개 웹 소스 조사 결과와 도그푸딩 검증 2건. 스킬의 모든 수치는 여기서 나온다

테스트 22개 통과 (core 16 · cli 6)
This commit is contained in:
Yun Chan 2026-08-20 10:48:00 +09:00
commit 8808c672dc
135 changed files with 38838 additions and 0 deletions

View file

@ -0,0 +1,63 @@
# 1단계 — 레퍼런스 조사 (무월 원두 정기구독)
## 브리프
홈카페 3년차 이상 애호가가 **구독 신청 버튼을 누르게**. 느낌은 **차분한, 정확한**.
## R1 · 구조 — Coffee Collective https://coffeecollective.dk
갤러리: ecomm.design `/tag/coffee/` (인덱스에서 href 추출) · 업종: 스페셜티 커피 D2C
- **가져올 축**: 축 1(그리드), 축 6(시선 흐름·섹션 순서)
- **축 1 그리드**: 컨테이너 1425px / 뷰포트 1440px = **0.99 (사실상 풀블리드)**. 내부 2분할 673 + 713px (≈1:1). 보조 컬럼 581·633·650px
- **축 2 타입**(참고만): 크기 10종 = 10/12/14/16/20/22/24/25.6/30/48px → **8단계 초과 = 통제 실패**. 최대 48 ÷ 본문 14 = **3.4배 (4배 미만 = 위계 약함)**. 패밀리 1개(NB International Pro)
- **축 3 컬러**(참고만): 배경 2단계 #FAF6ED(20) / #F1EDE3(14). 텍스트 1단계 지배 #4C4341(101). **강조 오렌지 #E17526 등장 1회** = 면적 1% 미만. radius 종류 **1개**(50%, 원형 아이콘만) → 각진 형태 어휘
- **축 4 여백**: ❌ **빈칸.** `sectionPadding` 추출이 `0px / 0px` 하나만 반환. 이 사이트는 `<section>`에 패딩을 주지 않는다
- **축 6 시선**: 히어로(상품 2종 + h1) → 선별커피 → **구독** → 장비 → 특가 → 도매 → 뉴스레터 → 카페위치. CTA `Subscribe now`**3번째 섹션**. 목표 행동이 "구매"와 "구독"으로 **갈라져 있음**
- **6축 밖의 관측**: ① 내비 최상위에 `Transparency`(생두 구매가·농가 공개). 이 업종의 신뢰 장치는 스토리가 아니라 **거래 내역 공개**다. ② 가격을 히어로에서 즉시 노출(379/161/159 DKK). 숨기지 않는다
- **변형**: 컨테이너를 0.99 풀블리드 → **최대 1120px**로 축소. 한국어 본문이 풀블리드에서 한 줄 40자를 넘기 때문. 그리고 목표 행동을 **구독 단 하나로 통합** — 브리프가 구독 전환을 명시했으므로 구매 CTA를 만들지 않는다
## R2 · 톤 — Fitzcarraldo Editions https://fitzcarraldoeditions.com [업종: 독립 문학 출판 — R1과 다름 ✓]
갤러리 라우팅 이탈 — 사유는 FINDINGS.md F2 참조
- **가져올 축**: 축 2(타입 스케일), 축 3(컬러 역할)
- **축 2 타입**: 크기 **5단계** 15/16/18/20.8/22px. 최대 22 ÷ 본문 16 = 1.375배(인덱스 페이지라 위계 약함 — 그대로 안 가져감). 패밀리 2개인데 **둘 다 세리프**(adobe-caslon-pro 120회 + kings-caslon 13회). 본문 16px / line-height 24px = **1.5** / 컬럼 폭 **522px**
- **축 3 컬러**: 배경 2단계 #FFFFFF(12) / #FBFBFB(8). 텍스트 **3단계** #151B23(67) / #363636(3) / #666666(7). **강조 = 파랑 #0033A0 하나뿐, 46회 전부 링크**. radius **5종**(2/3/4/5/50%) → 우연히 결정됨(안 가져감)
- **축 1 그리드**(참고): 컨테이너 1329~1345px, 2분할 672/673px
- **축 4 여백**: ❌ **빈칸.** section padding이 `32px / 32px` 하나만 나옴
- **축 6**: h1이 실질적으로 없고 h2는 0개. 위계를 태그가 아니라 크기·색으로만 만든다 — **구조적 결함이므로 안 가져간다**
- **6축 밖의 관측**: 상품(책)을 **이미지 없이 제목 + 저자 + 한 문단 설명**으로 판다. 텍스트가 상품 이미지를 대신한다. → 브리프 제약(사진 없음)과 정확히 맞는다
- **변형**: 타입 비율을 1.375배 → **최대/본문 4.2배**로 벌린다. 원본은 카탈로그 인덱스라 위계가 필요 없지만 우리는 단일 랜딩이라 첫 화면에서 위계가 서야 한다. 세리프 2종은 **세리프 1 + 산세리프 1**로 교체 — 한글 세리프(마루 부리)를 본문에 쓰면 16px에서 가독성이 떨어진다
## R3 · 디테일 — Codrops, "A Practical Introduction to Scroll-Driven Animations with CSS scroll() and view()"
https://tympanus.net/codrops/2024/01/17/a-practical-introduction-to-scroll-driven-animations-with-css-scroll-and-view/
- **가져올 축**: 축 5(모션 언어) — R3는 이 축만 채운다
- **모션**: `animation-timeline: view()` — 요소가 뷰포트에 교차할 때 페이드 + 상승. **JS 0바이트**
- **구현 제약**:
- 브라우저: 기사 시점 **Chromium 전용** (Firefox·Safari 미지원)
- 이중 게이팅 필수 — `@media (prefers-reduced-motion: no-preference)` 안에 `@supports (animation-timeline: view())`
- 폴백: 애니메이션 없음. **콘텐츠는 기본 visible**이어야 한다(antipatterns 5절 "reveal 실패 시 opacity:0 잔존" 회피)
- 폴리필(flackr/scroll-timeline) 존재하나 **쓰지 않는다** — 브리프가 "프레임워크 없는 정적 HTML/CSS"이고 모바일 70%다
- **변형**: `animation-range``entry 0% entry 40%`로 짧게. 모바일 뷰포트에서는 진입 구간이 길면 요소가 계속 반쯤 투명한 상태로 머문다
## 회색조 위계
가장 강한 덩어리는 **로스팅 프로파일 표(원두 3종의 산미·바디 수치)**, 두 번째 **구독 CTA**, 세 번째 **가격·주기 조건**.
비즈니스 우선순위 [1 구독 신청 / 2 원두 선택 근거 제공 / 3 가격 납득]과 — **부분 불일치**. 1위 덩어리가 CTA가 아니다.
→ 4-1에서 조정: 프로파일 표는 폭을 넓게 두되 **명도 대비를 낮추고**, CTA는 면적은 작지만 **유일한 반전 블록(잉크 배경 + 오프화이트 텍스트)**으로 만들어 회색조에서 가장 어둡게 한다. 색으로 해결하지 않는다.
## 안 할 것 (antipatterns.md에서 이 브리프에 특히 위험한 항목)
1. **크림/베이지를 "고급"의 기본값으로** (1절) — 커피 브리프에서 가장 빠지기 쉬운 함정. 오프화이트를 쓰되 **왜 그 색인지 브리프와 연결**하고 잉크·강조를 재설계한다
2. **근거 없는 지표 배너** (3절) + **모든 숫자가 어림수** (6절) — "10,000+ 구독자", "4.9★" 금지. 사용자가 준 수치가 없다. **5단계 하드 게이트 11번 대상**
3. **버튼이 항상 2개 나란히** (4절) — R1이 구매/구독으로 갈라져 있으므로 그대로 베끼면 걸린다. 주 CTA 1개
4. **아이콘이 카드 상단 중앙인 3열 피처 카드** (3·4절) — 원두 3종을 이 형태로 만들면 즉시 걸린다. 표 또는 비대칭 리스트로
5. **한글 조판 8절 전체**`word-break: keep-all`, line-height 1.6~1.8, 음수 자간 금지, 한 줄 25~40자, 라틴 폰트를 폴백 앞에
6. **이미지 없음 → 떠 있는 3D 블롭·일반 SVG 도형으로 채우기** (7절) — 사진이 없다고 장식 그래픽을 만들지 않는다. R2처럼 **텍스트가 이미지를 대신**한다
---
## 검증 조건 자가 판정
- [x] R1/R2/R3 각각 원본 URL (갤러리 URL 아님)
- [x] R2 업종이 R1과 다름 (문학 출판 vs 커피 커머스)
- [x] 각 슬롯에 가져올 축 명시
- [x] 각 슬롯에 변형 1개 + 이유
- [~] 6축 값에 숫자 있음 — **축 4(여백 리듬)는 R1·R2 모두 빈칸.** §5 스크립트가 값을 못 뽑았음(FINDINGS F3)
- [x] R3에 구현 제약(지원 범위·JS 비용)
- [~] 회색조 위계 — 작성했으나 **화면이 아직 없어 추정**이다. 실제 판정은 4-1 이후 (FINDINGS F4)

View file

@ -0,0 +1,77 @@
# 2단계 — 방향 결정
## 한 문장 컨셉
**감상은 이름에만, 화면에는 수치만.**
"무월(霧月)"이라는 이름은 모호하다. 이 브랜드가 파는 것은 모호함이 아니라 **측정된 정확성**이다. 그 낙차를 컨셉으로 쓴다.
## 프리셋 — ❗ 네 프리셋 중 어느 것도 맞지 않는다. 새로 정의한다: `quiet-commerce`
판정 근거(각 프리셋 파일의 "쓰지 마라"):
| 프리셋 | 배제 사유 |
|---|---|
| `editorial` | "**전환이 목적인 랜딩**에는 쓰지 마라" — 이 브리프의 목표가 정확히 구독 전환이다 |
| `anti-grid` | "**전환이 목적인 커머스**에는 쓰지 마라" — 동일 |
| `swiss-minimal` | "제품이 복잡할 때. B2B·도구·문서" — 이 제품은 원두 3종이고 B2C다. 적극적 근거가 없다 |
| `dark-instrument` | 다크를 정당화할 이유(야간 사용·밝은 데이터 시각화·제품 자체가 어두움)가 브리프에 없다 |
→ README의 "어느 것도 맞지 않으면 새로 정의해라"를 따른다. 항목 구조는 프리셋 문서 형식 그대로 채운다.
(이 판정 자체가 검증 결과다 — FINDINGS.md F5)
---
### `quiet-commerce` — 읽히는 커머스
> 한 문장: **콘텐츠가 설득하고, 화면은 한 번만 요구한다.**
> editorial의 타이포 규율 + 전환 구조. 콘텐츠가 주인공이되 CTA가 경로 위에 정확히 하나 있다.
**언제 고르나**: 상품 수가 적고(≤5), 구매자가 이미 카테고리를 아는 D2C. 사진보다 사양·근거가 설득의 주체일 때.
**쓰지 마라**: 상품 수가 많은 커머스(그리드·필터가 주인공이 됨), 카테고리 입문자 대상.
#### 타이포그래피
- 디스플레이 **세리프 1**(R2에서 가져온 원리) + 본문 **산세리프 1**. R2는 세리프 2종이었으나 한글 본문 세리프는 16px에서 가독성이 떨어져 교체
- 비율 **1.333**, 최대/본문 **4.2배** (R2의 1.375배를 의도적으로 벌림)
- 단계 **4개**. R1은 10단계였다 — 그대로 두면 antipatterns 2절 "8단계 이상 = 통제 실패"에 걸린다
- 웨이트 2개
| 슬롯 | 라틴 | 한글 |
|---|---|---|
| 디스플레이 | Fraunces (무료, Google Fonts) | 마루 부리 |
| 본문·UI | Switzer (Fontshare, 무료 상업) | Pretendard Variable |
| 수치 | 본문과 동일 + `tabular-nums` | — |
#### 색
- 배경은 순백이 아니다. **오프화이트**. 단 크림/베이지는 아니다 — antipatterns 1절 "크림을 고급의 기본값으로"에 걸린다. 브리프와의 연결: 무월 = 안개 낀 달 → **회청색이 2% 섞인 종이색**
- 잉크는 순흑이 아니다
- **강조색 하나, 면적 5% 미만.** R1이 오렌지를 1회만 쓴 것(면적 <1%) R2가 파랑을 링크에만 (46회, 전부 링크) 같은 원리다
- hue 2개 (중립 + 강조). antipatterns "hue 4개 이상" 회피
#### 간격
- 8px 그리드. 여백 값 **5종**
- 섹션 간 : 요소 간 = **1:5**
- 비대칭 — 본문을 그리드 왼쪽에 두고 오른쪽 여백에 수치를 흘린다 (R2의 "텍스트가 이미지를 대신한다"를 여백 운용으로 번역)
- 컨테이너 최대 **1120px**, 본문 `--measure`는 한글 **32자**
#### 재질 (`svg-filters.md`)
- **필름 그레인 `--grain: 0.03`** — 최약. 오프화이트 배경의 밴딩만 없앤다
- 유리·글로우·굴절 없음
- 사진이 없으므로 듀오톤·그라디언트 맵은 이번 브리프에서 쓸 곳이 없다
#### 모션 (`motion.md`)
- 거의 움직이지 않는다. R3의 `animation-timeline: view()` 페이드 + 8px 상승 하나만
- 스크롤 재킹 금지
#### 흔한 실패
1. 세리프 디스플레이를 쓰고 여백이 좁다
2. CTA가 2개로 갈라진다 (R1이 그렇다 — 베끼면 걸린다)
3. 수치를 "지표 배너"로 만든다 — 근거 없는 숫자는 신뢰를 깎는다
4. 한글에 라틴 세리프만 지정해 한글이 시스템 기본으로 떨어진다
---
## 감수할 리스크 하나
**히어로에 이미지도 감성 카피도 없이, 원두 3종의 로스팅 프로파일 수치표를 첫 화면에 올린다.**
- 정당화: 대상이 홈카페 3년차 이상이고 이미 여러 로스터리를 거쳤다. 이들에게 "장인의 손길" 카피는 정보량 0이다. 산미·바디 수치는 즉시 판단 재료가 된다
- 이 리스크가 브리프 제약(사진 없음)을 **결함에서 자산으로** 바꾼다
- 대담함은 여기 한 곳뿐. 나머지(타이포·색·여백)는 조용히 받쳐준다

View file

@ -0,0 +1,100 @@
/* ==========================================================================
무월(霧月) 3단계 디자인 토큰
프리셋: quiet-commerce (커스텀, 02-direction.md 참조)
========================================================================== */
:root {
/* ---- 타입 비율 1.333 (Perfect Fourth), 본문 16px 기준 ---------------
tokens.md §6은 --step--1 ~ --step-5 (7단계) 산출물로 요구한다.
antipatterns.md §2는 "3~5단계로 제한" 권한다.
7개를 정의하되 화면에서 실제로 쓰는 것은 4개다:
--step--1(라벨) / --step-0(본문) / --step-2(소제목) / --step-5(디스플레이)
나머지 3개는 예약. 쓰려면 주석을 지우고 역할을 적어라. (FINDINGS F6) */
--step--1: 0.75rem; /* 12 — 라벨·캡션 [사용] */
--step-0: 1rem; /* 16 — 본문 [사용] */
--step-1: 1.333rem; /* 21 — 예약 */
--step-2: clamp(1.5rem, 1.32rem + 0.9vw, 1.777rem); /* 28 — 소제목 [사용] */
--step-3: 2.369rem; /* 38 — 예약 */
--step-4: 3.157rem; /* 51 — 예약 */
--step-5: clamp(2.25rem, 1.3rem + 4.75vw, 4.209rem); /* 67 — 디스플레이 [사용] */
/* 최대(67) ÷ 본문(16) = 4.2배 — antipatterns.md §2 "최대/본문 4배 이상" 통과 */
--leading-tight: 1.25; /* 디스플레이 — 한글이라 라틴 1.1보다 높게 */
--leading-normal: 1.7; /* 본문 — 한글 1.6~1.8 규칙 */
--measure: 32ch; /* 한글 32자. R2 관측 522px과 수렴 */
--font-display: 'Fraunces', 'Pretendard Variable', Pretendard, serif;
--font-body: 'Pretendard Variable', Pretendard, -apple-system, system-ui, sans-serif;
/* 폰트 파일 2개 (tokens.md §5 예산 준수).
Switzer(라틴 본문) 마루 부리(한글 디스플레이) 의도적으로 버렸다 사유는 아래 성능 예산 주석. */
/* ---- 역할별. hue 2개(중립 + 강조) --------------------------------
surface: 안개 종이. 순백 아님, 크림/베이지도 아님(antipatterns §1).
accent : 로스팅된 원두 갈색. 보라·인디고 계열 아님(최우선 지문 1위 회피). */
--surface: #F3F4F2;
--surface-raised: #EAEBE7;
--ink: #1A1C1A; /* 순흑 아님 */
--ink-muted: #5C605B;
--line: #D6D8D3; /* 장식용 구분선 (텍스트·UI 아님) */
--line-strong: #868C83; /* 인터랙티브 요소 경계 — 3:1 필요 */
--accent: #7A3E1D;
--accent-ink: #F3F4F2;
/* 측정된 대비비 (계산식 WCAG 2.x relative luminance)
--ink on --surface 15.54:1 본문 4.5:1
--ink on --surface-raised 14.32:1
--ink-muted on --surface 5.80:1
--ink-muted on --surface-raised 5.35:1
--accent on --surface 7.51:1 링크 텍스트
--accent-ink on --accent 7.51:1 CTA 내부 텍스트
--line-strong on --surface 3.12:1 UI 3:1
--line on --surface 1.30:1 장식 전용. 텍스트·인터랙티브 경계에 쓰지 마라 */
/* 다크 모드: 브리프에 근거 없음 → 지원하지 않는다. (가정 명시) */
/* ---- 간격 — 8px 리듬 ------------------------------------------------- */
--space-1: 0.25rem; /* 4 */
--space-2: 0.5rem; /* 8 */
--space-3: 1rem; /* 16 */
--space-4: 1.5rem; /* 24 */
--space-5: 2.5rem; /* 40 — 요소 간 */
--space-6: 4rem; /* 64 */
--space-7: 6rem; /* 96 */
--space-8: 10rem; /* 160 — 섹션 간. 요소 간(40) 대비 1:4 */
/* ---- 형태 ------------------------------------------------------------
tokens.md §6 산출물 형식에 radius 항목이 없다. 그런데 SKILL.md 하드 게이트 3이
"코너 반경이 규칙 없이 섞여 있는가" 묻고 antipatterns.md §4는 어휘 2개를 요구한다.
토큰을 직접 추가한다. (FINDINGS F8) */
--radius-container: 2px; /* 표·패널 — 인쇄물 감각이라 거의 각지게 */
--radius-interactive: 2px; /* 버튼·입력 — 컨테이너와 같게 통일 */
--container: 1120px; /* R1의 풀블리드 0.99를 축소한 값 */
--pad-inline: clamp(1.25rem, 1rem + 2vw, 5rem); /* 모바일 20px → 데스크톱 80px */
/* ---- 모션 ------------------------------------------------------------ */
--dur-instant: 100ms;
--dur-quick: 200ms;
--dur-normal: 350ms;
--dur-slow: 600ms;
--ease-out: cubic-bezier(0.22, 1, 0.36, 1);
--ease-in: cubic-bezier(0.64, 0, 0.78, 0);
--ease-soft: cubic-bezier(0.4, 0, 0.2, 1);
/* ---- 재질 (svg-filters.md) ------------------------------------------- */
--grain: 0.03; /* 최약. 오프화이트 배경의 밴딩 제거용 */
}
/* ==========================================================================
성능 예산 = JS 0KB / 인터랙션 3초 (모바일 4G)
- 프레임워크 없음. R3 모션은 CSS scroll-driven이라 JS 0바이트
- 폰트 파일 2개: Fraunces(latin 서브셋) + Pretendard Variable(korean+latin 서브셋)
· tokens.md §5 "폰트 2개 이하" 지키기 위해 4개 후보 2개를 버렸다.
버린 : Switzer(라틴 본문) Pretendard의 라틴으로 대체
마루 부리(한글 디스플레이) Pretendard 700으로 대체
· antipatterns.md §8 "라틴 폰트를 폴백 앞에" 규칙은 --font-display에서만 지킨다.
--font-body는 Pretendard가 앞이지만, Pretendard는 자체 라틴 글리프를 포함하므로
한글 폰트로 라틴이 조악하게 그려지는 문제가 없다. (예외 근거 명시) (FINDINGS F7)
- 이미지 없음(브리프 제약) 이미지 예산 0
- 애니메이션 속성: transform / opacity
========================================================================== */

View file

@ -0,0 +1,33 @@
# 4단계 — 히어로 이후 섹션 구조 계획 (코드 미작성)
범위 제한에 따라 히어로만 구현했다. 이하는 계획이다.
## 섹션 순서 — 브리프의 설득 논리로 재배열
antipatterns.md §3 "표준 골격(히어로 → 3열 피처 → 로고월 → 요금제 → FAQ)"을 쓰지 않는다.
대상이 이미 여러 로스터리를 거쳤으므로 **불신 → 검증 → 조건 → 행동** 순서로 간다.
| # | 섹션 | 역할 | 레이아웃 패밀리 | 슬롭 회피 메모 |
|---|---|---|---|---|
| 1 | 히어로 (구현됨) | 판단 재료 즉시 제시 | 헤드 풀폭 + 5:7 비대칭 | 배지 없음, CTA 1개 |
| 2 | 척도 설명 | "산미 4"가 무슨 뜻인지 정의 | 좌측 본문 + 우측 여백 주석 | 아이콘 카드 금지. 정의문 3개를 본문으로 |
| 3 | 거래 내역 (`#ledger`) | 생두 구매가·농가·환율 공개 | 전폭 표 | R1 Transparency의 원리. **숫자는 실제 값 확보 후에만 공개** |
| 4 | 배송 조건 (`#plan`) | 주기·용량·가격·취소 | 2열 비교표(카드 아님) | 요금제 3열 카드 + "Most popular" 배지 금지 |
| 5 | 자주 오는 문의 | 실제 문의 3개 | 정의 목록 | 가상 FAQ 6개 금지. 문의가 없으면 섹션 삭제 |
| 6 | 푸터 | 사업자 정보·연락처·주소 | 좌측 정렬 단일 블록 | 링크 4열 + 소셜 아이콘 금지 |
레이아웃 패밀리 4종 (헤드+비대칭 / 본문+여백주석 / 전폭 표 / 정의 목록) → SKILL.md 카운트 규칙 "섹션 6개 이상이면 패밀리 최소 3개" 통과.
## 확보해야 할 실제 데이터 (없으면 해당 섹션 삭제)
- 원두 3종의 산미·바디·단맛 점수와 로스팅 날짜 → 히어로 표의 `—` 자리
- 생두 구매가·농가명·수입 시점 → 3번 섹션
- 구독 주기별 가격과 취소 정책 → 4번 섹션
- 실제 받은 문의 3건 → 5번 섹션
- 사업자등록번호·통신판매업 신고번호·주소·전화 → 푸터
**어느 하나도 지어내지 않는다.** SKILL.md 하드 게이트 11 대상이다.
## 사진이 생겼을 때
브리프상 지금은 없다. 생기면:
- `aspect-ratio` 고정으로 CLS 0 유지
- 듀오톤(`svg-filters.md` R4)으로 톤 통일 — 여러 산지 사진의 색온도가 제각각일 것이다
- 크기 차이로 리듬 생성. 전부 같은 비율 금지

View file

@ -0,0 +1,82 @@
# design.md — 무월(霧月) 원두 정기구독 랜딩
> 6단계 산출물. 다음 실행(사람이든 에이전트든)은 이 파일을 읽고 같은 결정을 이어간다.
## 브리프 3줄
- **무엇을**: 수제 커피 로스터리 "무월"의 원두 정기구독 단일 랜딩 페이지
- **누구에게**: 홈카페 3년차 이상 30~40대. 여러 로스터리를 거쳤고 산미·로스팅 프로파일을 구분한다. 목표는 구독 신청
- **제약**: 한국어 / 정적 HTML·CSS(프레임워크 없음) / 사진 없음 / 모바일 트래픽 70% / 성능 예산 스킬 기본값
## 레퍼런스
| 슬롯 | 사이트 | 업종 | 가져온 축 |
|---|---|---|---|
| R1 · 구조 | https://coffeecollective.dk | 스페셜티 커피 D2C | 축 1(그리드), 축 6(시선·섹션 순서) |
| R2 · 톤 | https://fitzcarraldoeditions.com | 독립 문학 출판 | 축 2(타입 스케일), 축 3(컬러 역할) |
| R3 · 디테일 | https://tympanus.net/codrops/2024/01/17/a-practical-introduction-to-scroll-driven-animations-with-css-scroll-and-view/ | — | 축 5(모션) |
상세 수치와 변형 사유는 `01-references.md`.
## 한 문장 컨셉
**감상은 이름에만, 화면에는 수치만.**
## 프리셋
**`quiet-commerce` (커스텀 정의).** 기존 4개 프리셋이 전부 "전환이 목적인 커머스 랜딩"을 배제하거나 근거가 없어 새로 정의했다. 정의 전문은 `02-direction.md`.
## 감수한 리스크 하나
히어로에 이미지도 감성 카피도 없이 **원두 3종의 로스팅 프로파일 수치표**를 첫 화면에 올린다.
→ ⚠️ **현재 미완성**: 실제 수치를 확보하지 못해 표가 `—`로 비어 있다. 값이 들어와야 이 리스크가 성립한다. 지어내지 않았다(하드 게이트 11).
## 토큰
`03-tokens.css` 전문. 요약:
- 타입 비율 **1.333**, 본문 16px, 최대/본문 **4.2배**. 7개 정의 / **실사용 4개**
- 색 역할 8개. `--surface #F3F4F2` / `--ink #1A1C1A` / `--accent #7A3E1D` (강조 1개, hue 2개)
- 대비 실측: 본문 15.54:1 · 보조 5.80:1 · CTA 7.51:1 · 인터랙티브 경계 3.12:1 — 전부 통과
- 간격 8단계, 섹션:요소 = 1:4. 컨테이너 1120px, `--measure` 32ch(한글)
- 라인하이트 본문 **1.7**(한글), 음수 자간은 디스플레이 `-0.015em`
- radius 어휘 **1개(2px)** — tokens.md에 radius 항목이 없어 직접 추가
- 재질 `--grain: 0.03` (필름 그레인 최약, data URI 배경)
- 다크 모드: 브리프 근거 없어 **지원하지 않음**
## 성능 — 예산과 실측
| 항목 | 예산 | 실측 | 판정 |
|---|---|---|---|
| 히어로까지 JS (gzip) | 150KB | **0KB** (script 태그 0개) | ✓ |
| WebGL/3D | +200KB | 0 (미사용) | ✓ |
| 총 전송량(첫 화면) | 1MB | **13.4KB** (폰트 캐시 상태) | ✓ |
| 폰트 패밀리 | 2개 이하 | 2개 (Fraunces, Pretendard) | ✓ |
| 폰트 **파일** 수 | 2개 이하 | **14개** | ✗ — 아래 참조 |
| 애니메이션 속성 | transform/opacity만 | opacity, transform | ✓ |
| LCP / CLS | 2.5초 / 0.1 | **미측정** | 측정 절차 없음 |
폰트 파일 14개는 Pretendard `dynamic-subset`이 유니코드 범위별로 분할된 결과다. **이것이 올바른 최적화**이며 실제 전송은 페이지에 쓰인 글자 범위만이다. tokens.md의 "폰트 파일 2개 이하"를 문자 그대로 적용하면 한글 프로젝트에서 잘못된 방향(단일 대용량 파일)으로 유도된다. 예산 항목을 **패밀리 수**로 읽고 통과 처리했으며, 그 사실을 여기 명시한다.
## 채택한 이펙트와 폴백
| 이펙트 | 구현 | 폴백 |
|---|---|---|
| 필름 그레인 | data URI SVG 배경, `opacity: .03`, `mix-blend-mode: multiply` | 없어도 무해. 배경색만 남는다 |
| 스크롤 진입 페이드+8px 상승 | `animation-timeline: view()`, JS 0 | Chromium 외에서는 **아무 일도 안 일어남**. 콘텐츠는 처음부터 visible |
이중 게이팅: `@media (prefers-reduced-motion: no-preference)` 안에 `@supports (animation-timeline: view())`.
## 5단계 감사 결과
- 기계 검사 3종: 이펙트 전부 끄기 **통과**(h1·CTA·표 3행 모두 렌더, 본문 401자) / 슬롭 grep 15개 패턴 **전부 0건** / 경쟁사 치환 **통과**
- 하드 게이트 12개: 전부 "아니오" (검사 근거는 `FINDINGS.md` §하드게이트)
- 감사 중 발견해 고친 것 2건:
1. 히어로 헤드라인이 데스크톱에서 **5줄** → 카운트 규칙(≤2줄) 위반. 헤드라인을 두 컬럼 가로지르게 하고 `max-width: 20ch` 적용
2. 320px에서 가로 오버플로 **31px** → 하드 게이트 5 위반. grid item `min-width: 0` + 표를 `overflow-x: auto` 컨테이너로 감쌈
## 의도적으로 하지 않은 것과 그 이유
- **다크 모드** — 브리프에 야간 사용·데이터 시각화 근거가 없다. tokens.md "다크를 기본값으로 삼지 마라"
- **한글 세리프 디스플레이(마루 부리)** — 폰트 패밀리 2개 예산을 지키려 버렸다. 그 결과 프리셋이 정한 "세리프 디스플레이"가 **한글에서는 실현되지 않는다**(Fraunces는 라틴만 커버). 예산을 3개로 올릴 수 있다면 되돌릴 결정이다
- **Switzer(라틴 본문)** — 같은 이유. Pretendard의 라틴 글리프로 대체
- **구매 CTA** — R1은 "구매"와 "구독"으로 갈라져 있다. 브리프가 구독 전환을 명시했으므로 CTA를 하나로 통합했다. 원두 낱개 판매를 나중에 붙이더라도 **이 페이지에는 넣지 않는다**
- **3D / WebGL** — 필터로 낼 수 없는 인상이 없다. 4-3 미실행
- **스크롤 애니메이션 폴리필** — JS 0 예산을 지키기 위해. Firefox·Safari 사용자는 모션 없이 본다
- **지표 배너 / 후기 / 로고월** — 검증 가능한 데이터가 없다. 생기기 전까지 섹션을 만들지 않는다
- **아이콘** — 이 페이지에 아이콘이 0개다. 정보를 아이콘으로 대체하지 않기로 했다
## 다음 실행이 해야 할 일
1. `05-structure-plan.md`의 "확보해야 할 실제 데이터"를 사용자에게 받는다
2. 히어로 표의 `—`를 채운다. **채우기 전까지 이 페이지는 배포하지 않는다**
3. 히어로 이후 섹션 6개를 계획대로 구현
4. 폰트 서브셋 파일을 자체 호스팅으로 옮긴다(현재 CDN 2곳 의존)

View file

@ -0,0 +1,125 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>무월 — 격주 원두 구독</title>
<meta name="description" content="원두마다 산미·바디·단맛을 같은 척도로 측정해 표기합니다. 로스팅 날짜와 생두 구매가를 함께 적습니다.">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,400;9..144,600&display=swap">
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/variable/pretendardvariable-dynamic-subset.min.css">
<link rel="stylesheet" href="03-tokens.css">
<link rel="stylesheet" href="style.css">
</head>
<body>
<!-- 필터 정의: 필름 그레인 (svg-filters.md R1 — data URI 대신 인라인이 필요할 때만) -->
<a class="skip" href="#main">본문으로 건너뛰기</a>
<header class="page-header">
<div class="page-header__inner">
<a class="wordmark" href="/">무월<span class="wordmark__han">霧月</span></a>
<nav aria-label="주 메뉴">
<ul class="nav">
<li><a href="#beans">원두</a></li>
<li><a href="#plan">구독</a></li>
<li><a href="#ledger">거래 내역</a></li>
</ul>
</nav>
</div>
</header>
<main id="main">
<section class="hero" aria-labelledby="hero-title">
<div class="hero__inner">
<div class="hero__head reveal">
<p class="eyebrow">격주 원두 구독</p>
<h1 id="hero-title" class="hero__title">
취향은 형용사가 아니라 좌표입니다
</h1>
</div>
<div class="hero__lede reveal">
<p class="hero__body">
무월은 원두마다 산미·바디·단맛을 같은 척도로 재서 적습니다.
로스팅한 날짜와 생두를 얼마에 샀는지도 같이 적습니다.
마셔보기 전에 판단할 수 있어야 고르는 재미가 생깁니다.
</p>
<p class="hero__action">
<a class="cta" href="#plan">2주마다 받아보기</a>
</p>
<p class="hero__note">
첫 발송 전까지 언제든 취소. 배송 주기는 2주 또는 4주 중에 고릅니다.
</p>
</div>
<div class="hero__profile reveal">
<h2 class="profile__title">이번 주 로스팅</h2>
<div class="profile__scroll" tabindex="0" role="region" aria-label="로스팅 프로파일 표 (가로 스크롤)">
<table class="profile">
<caption class="visually-hidden">원두 3종의 산미·바디·단맛 측정값과 로스팅 날짜</caption>
<thead>
<tr>
<th scope="col" class="profile__name">원두</th>
<th scope="col">산미</th>
<th scope="col">바디</th>
<th scope="col">단맛</th>
<th scope="col" class="profile__date">볶은 날</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row" class="profile__name">
<span class="profile__origin">에티오피아</span>
<span class="profile__lot">구지 우라가 · 워시드</span>
</th>
<td class="num" data-pending></td>
<td class="num" data-pending></td>
<td class="num" data-pending></td>
<td class="num" data-pending></td>
</tr>
<tr>
<th scope="row" class="profile__name">
<span class="profile__origin">콜롬비아</span>
<span class="profile__lot">우일라 · 허니</span>
</th>
<td class="num" data-pending></td>
<td class="num" data-pending></td>
<td class="num" data-pending></td>
<td class="num" data-pending></td>
</tr>
<tr>
<th scope="row" class="profile__name">
<span class="profile__origin">과테말라</span>
<span class="profile__lot">우에우에테낭고 · 워시드</span>
</th>
<td class="num" data-pending></td>
<td class="num" data-pending></td>
<td class="num" data-pending></td>
<td class="num" data-pending></td>
</tr>
</tbody>
</table>
</div>
<p class="profile__legend">
1에서 5까지. 같은 추출 조건(20g / 300mL / 92°C)에서 로스터 3인이 매깁니다.
</p>
</div>
</div>
</section>
<!-- 이후 섹션은 4단계 범위 밖 — 구조 계획만. 05-structure-plan.md 참조 -->
</main>
</body>
</html>

View file

@ -0,0 +1,270 @@
/* ==========================================================================
무월 4단계 구현 (히어로 섹션까지)
모든 값은 03-tokens.css 에서 나온다. 하드코딩 금지.
========================================================================== */
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
background: var(--surface);
color: var(--ink);
font-family: var(--font-body);
font-size: var(--step-0);
line-height: var(--leading-normal);
/* 한글 조판 — antipatterns.md §8 */
word-break: keep-all;
overflow-wrap: break-word;
letter-spacing: 0; /* 한글 본문 음수 자간 금지 */
position: relative;
isolation: isolate;
}
/* 재질: 필름 그레인 최약 (svg-filters.md R1, --grain 0.03)
요소 filter가 아니라 data URI 배경 번만 래스터화되고 캐시된다 */
body::after {
content: '';
position: fixed;
inset: 0;
pointer-events: none;
z-index: 1;
opacity: var(--grain);
mix-blend-mode: multiply; /* 밝은 배경이므로 overlay 대신 multiply */
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.5'/%3E%3C/svg%3E");
}
img, svg { max-width: 100%; height: auto; }
.visually-hidden {
position: absolute; width: 1px; height: 1px;
margin: -1px; padding: 0; border: 0;
clip-path: inset(50%); overflow: hidden; white-space: nowrap;
}
.skip {
position: absolute; left: var(--space-3); top: var(--space-3);
transform: translateY(-200%);
z-index: 10;
padding: var(--space-2) var(--space-3);
background: var(--ink); color: var(--surface);
border-radius: var(--radius-interactive);
text-decoration: none;
transition: transform var(--dur-instant) var(--ease-out);
}
.skip:focus-visible { transform: translateY(0); }
:where(a, button, [tabindex]):focus-visible {
outline: 2px solid var(--accent);
outline-offset: 3px;
border-radius: var(--radius-interactive);
}
/* ---- 그리드 --------------------------------------------------------------
layout.md §1 하나의 그리드에서 모든 것이 나온다 */
.page-header__inner,
.hero__inner {
width: 100%;
max-width: var(--container);
margin-inline: auto;
padding-inline: var(--pad-inline);
}
/* ---- 헤더 ---------------------------------------------------------------- */
.page-header { padding-block: var(--space-4); }
.page-header__inner {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: var(--space-4);
flex-wrap: wrap;
}
.wordmark {
font-family: var(--font-display);
font-size: var(--step-2);
font-weight: 600;
color: var(--ink);
text-decoration: none;
letter-spacing: -0.01em; /* 한글 디스플레이 허용 범위 */
}
.wordmark__han {
font-size: var(--step--1);
color: var(--ink-muted);
margin-inline-start: var(--space-2);
letter-spacing: 0;
}
.nav {
display: flex;
gap: var(--space-4);
margin: 0; padding: 0;
list-style: none;
font-size: var(--step--1);
}
.nav a {
color: var(--ink-muted);
text-decoration: none;
padding-block: var(--space-2); /* 터치 타깃 확보 */
display: inline-block;
border-bottom: 1px solid transparent;
transition: color var(--dur-instant) var(--ease-out),
border-color var(--dur-instant) var(--ease-out);
}
.nav a:hover { color: var(--ink); border-bottom-color: var(--line-strong); }
/* ---- 히어로 --------------------------------------------------------------
layout.md §1 "비대칭을 두려워하지 마라" 5 : 7 */
.hero { padding-block: var(--space-7) var(--space-8); }
.hero__inner {
display: grid;
gap: var(--space-6);
align-items: start;
}
/* grid item 기본 min-width:auto 표의 최소폭을 그대로 반영해
320px에서 가로 스크롤을 만든다 (하드 게이트 5) */
.hero__inner > * { min-width: 0; }
@media (min-width: 60rem) {
.hero__inner {
grid-template-columns: 5fr 7fr;
gap: var(--space-6) var(--space-7);
}
/* 헤드라인은 컬럼을 가로지른다 좁은 컬럼에 두면 한글이 5줄로 접힌다
(SKILL.md 카운트 규칙: 히어로 헤드라인 2줄) */
.hero__head { grid-column: 1 / -1; }
}
.hero__head { margin-bottom: var(--space-2); }
.eyebrow {
margin: 0 0 var(--space-3);
font-size: var(--step--1);
color: var(--ink-muted);
/* 전체 대문자 아님 — antipatterns 최우선 지문 2위 회피 */
}
.hero__title {
margin: 0;
max-width: 20ch; /* 한글 20자 — 데스크톱에서 2줄, 모바일에서 3줄 상한 */
font-family: var(--font-display);
font-size: var(--step-5);
font-weight: 600;
line-height: var(--leading-tight);
letter-spacing: -0.015em; /* 대형 헤드라인만. 한글 허용 -0.01~-0.02em */
text-wrap: balance;
}
.hero__body {
margin: 0 0 var(--space-5);
max-width: var(--measure);
color: var(--ink);
}
.hero__action { margin: 0 0 var(--space-3); }
/* 주 CTA 1개. 페이지에서 유일한 반전 블록 → 회색조에서 가장 어둡다 */
.cta {
display: inline-block;
padding: var(--space-3) var(--space-5);
background: var(--accent);
color: var(--accent-ink);
border-radius: var(--radius-interactive);
text-decoration: none;
font-weight: 600;
min-height: 44px; /* 터치 타깃 */
transition: background-color var(--dur-instant) var(--ease-out);
}
.cta:hover { background: var(--ink); }
.hero__note {
margin: 0;
font-size: var(--step--1);
color: var(--ink-muted);
max-width: var(--measure);
}
/* ---- 프로파일 표 ---------------------------------------------------------- */
.profile__title {
margin: 0 0 var(--space-3);
font-size: var(--step--1);
font-weight: 400;
color: var(--ink-muted);
}
/* 좁은 폭에서는 표만 스스로 스크롤한다. 페이지 본문은 넘치지 않는다 */
.profile__scroll {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.profile {
width: 100%;
min-width: 20rem;
border-collapse: collapse;
font-variant-numeric: tabular-nums;
background: var(--surface-raised);
border-radius: var(--radius-container);
overflow: hidden;
}
.profile th,
.profile td {
padding: var(--space-3);
text-align: right;
border-bottom: 1px solid var(--line);
font-weight: 400;
vertical-align: baseline;
}
.profile thead th {
font-size: var(--step--1);
color: var(--ink-muted);
border-bottom-color: var(--line-strong);
}
.profile tbody tr:last-child th,
.profile tbody tr:last-child td { border-bottom: 0; }
.profile__name { text-align: left; }
.profile__origin { display: block; }
.profile__lot {
display: block;
font-size: var(--step--1);
color: var(--ink-muted);
}
.profile__date { white-space: nowrap; }
.num[data-pending] { color: var(--ink-muted); }
.profile__legend {
margin: var(--space-3) 0 0;
font-size: var(--step--1);
color: var(--ink-muted);
max-width: var(--measure);
}
/* ---- 모션 ----------------------------------------------------------------
R3: Codrops scroll-driven animation. JS 0바이트.
이중 게이팅 reduced-motion 안에 @supports.
기본 상태는 visible. 지원 하면 아무 일도 일어나지 않는다.
(antipatterns §5 "reveal 실패 시 opacity:0 잔존" 회피) */
@media (prefers-reduced-motion: no-preference) {
@supports (animation-timeline: view()) {
@keyframes rise {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
.reveal {
animation: rise var(--dur-slow) var(--ease-out) both;
animation-timeline: view();
animation-range: entry 0% entry 40%;
}
}
}