designpaca/apps/site/design.md
Yun Chan 72337b7ee0
Some checks failed
ci / build (push) Failing after 5s
feat(skill): 자율 검증 폐쇄 루프 내재화 — 0.6.0
- 핵심 규칙 6: 완료의 정의는 게이트 통과. 사용자를 QA로 쓰지 않는다
- 5단계 기계 검사 4번째: design-gate 실행 의무화
- references/audit-gate.md: 사고-검사 매핑, SEO/meta 체크리스트, OS/브라우저 특성, 하니스 규칙
- tools/design-gate.mjs: 범용 게이트(메타/SEO·대비·수축·리듬·트랙·스케일×폭 + 옵션 L0/L3/L4, checks 깊은 병합)
- 리듬·트랙 불변식: 숫자 라벨 등폭·빈 셀 트랙 균일·등간격 — 시간표 자동배치 결함 재현 픽스처 검출, 앱 15뷰 통과(오탐 0)

feat(site): 관리 앱 2종 프로덕션 콘솔(v6→v18)

- 가온 학적부 9뷰·두레 수강신청 6뷰: shadcn 문법, Pretendard/Noto Serif/IBM Plex 폰트 전략, 볼드 금지(400/500/600), WCAG AA 대비 전면 교정, 한글 keep-all 조판
- LMS 필수 요소(알림 센터·공지·진도·평가 유형·출결 사유·학점 경고), 12명 기준 데이터 정합, SEO 구조(h1 유일·OG/twitter·og.png)
- 시간표 자동배치 결함 수리(명시적 격자 좌표)
- QA 게이트: L0 stylelint/html-validate · L1 단위 30 · L2 감사 61+불변식 · L3 시각회귀 30화면 · L4 WebKit · L5 키보드 탐색 — npm run verify 실패 시 배포 금지 체인
2026-08-22 21:22:05 +09:00

792 lines
67 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# design.md — designpaca 소개 페이지
> 6단계 산출물. 다음 실행(사람이든 에이전트든)은 이 파일을 먼저 읽고 같은 결정을 이어간다.
## 개정 이력
| 날짜 | 판 | 무엇이 바뀌었나 |
|---|---|---|
| 2026-08-20 | 초판 | `swiss-minimal` · three.js 미사용 · 라이트 기반 |
| 2026-08-20 | **2판(현행)** | `dark-instrument` · three.js 채택 · 전체 경로 재주행 |
**초판이 거짓말이 됐던 경위를 남긴다.** 사용자가 "글래스모피즘·리퀴드글래스·히어로에 힘"을 요청했을 때
그것은 2단계 방향 재결정이었고, 스킬은 그럴 때 **전체 경로로 올려 1단계부터 다시 돌라**고 씌어 있다.
그러지 않고 4단계 구현으로 바로 들어갔다. 레퍼런스 없이 재질만 갈아끼운 결과가 **스위스 미니멀 뼈대에
유리를 바른 페이지**였고, `design.md` 는 초판 상태로 남아 프리셋·이펙트·형태 수치가 전부 어긋났다.
2판은 0단계부터 다시 돌았다.
---
## 브리프 3줄
- **무엇을**: designpaca 소개 페이지. 파이프라인 스킬 + npx 설치 CLI 를 소개하는 단일 페이지, 한국어·영어 두 판
- **누구에게**: Claude Code · Codex · Cursor 를 쓰는 개발자. 이 페이지를 보고 `npx designpaca` 를 터미널에 치게 만드는 것이 목표
- **제약**: Astro 정적 → Cloudflare Pages. 이 페이지 자체가 스킬의 품질 증거물이다 — 스킬이 금지하는 것을 이 페이지가 하면 안 된다
## 레퍼런스 (전부 실측. computed style 추출까지 돌렸다)
| 슬롯 | 사이트 | 업종 | 실측값 | 가져온 **원리** | 가져오지 **않은** 것 |
|---|---|---|---|---|---|
| R1 · 구조 | zed.dev | 코드 에디터 | 컨테이너 **1120px**(68회) ↔ 전체폭 1425px(50회) 교대 · 본문 칼럼 512px · **radius 2px 가 621회** · 모노 524회로 지배 | 폭의 교대, 각진 형태 어휘, 모노를 정체성으로 | 세리프 h1(우리는 한글) · 최대÷본문 3.0배(위계가 약하다) · 섹션 11개 |
| R2 · 톤 | lusion.co | 3D 프로덕션 스튜디오 | 배경 **흰색** · 텍스트 색 **2단계**(검정 135 / 흰 16) · 폰트 **1개**(Aeonik) · 강조색 rgb(0,22,236) **4회** · 캔버스는 radius 15px 액자 안 · **h1 이 36px** | **3D 는 액자 안, 텍스트는 액자 밖.** 3D 가 크면 타이포는 물러난다. 색은 캔버스 안에서만 소리친다 | 흰 배경 자체 · 파랑 |
| R3 · 디테일 | Codrops "Inside HAOQI.DESIGN" | — | 축 5 전용 | DOM 이 구조를 소유하고 WebGL 은 셰이더 uniform 만 만진다. 한 rAF 에서 스크롤을 공유하고 뷰포트 밖에서는 정지 | R3F/Lenis 스택 |
**R3 구현 제약**: 셰이더 uniform 만 갱신하므로 `transform`/`opacity` 외의 CSS 를 애니메이션하지 않는다(게이트 #8 안전).
뷰포트 밖 렌더 정지가 필수 조건이다.
**접근 실패 기록**`iglooinc.com` 은 hugedomains 파킹으로 302, `lasvit.com` 은 403.
`reference-method.md` §3-b 가 경고한 그대로였다. 왕복 세 번에서 끊고 우회했다.
### 히어로 국소 레퍼런스 (1)
R1~R3 로 구조를 잡은 뒤에도 히어로가 어색해서 **국소 브리프 표**로 두 곳을 더 실측했다.
`design.md` 는 "3D 는 액자 안"까지만 답하고 **액자가 화면의 몇 %여야 하는지는 답하지 않는다.**
| 사이트 | 3D 영역 | 텍스트 위치 | h1 |
|---|---|---|---|
| lusion.co | 1440×645 = 첫 화면의 **71%** | 캔버스 **위**, 조밀하게 | 36px |
| basement.studio | 1425×900 = **100%** | 캔버스 **아래**(y=916) | 87px, weight 600 |
| railway.com | SVG 1062×755, h1 아래 | 중앙 상단 | 54px, **1줄** |
| **우리(고치기 전)** | 1352×465 = **52%** | 위 380px | 41.5px |
셋 다 3D 와 텍스트를 세로로 완전히 분리하고, 3D 가 첫 화면의 **70% 이상**을 먹는다.
우리는 텍스트 43% / 3D 52% 로 **둘 다 어중간했다.** basement 형(첫 화면 전체 3D)은 CTA 가
첫 화면에서 밀려나므로 전환이 목적인 우리에게 맞지 않는다. lusion 비율을 택했다.
**현행: 텍스트 24% · 액자 59% · 캡션과 링크가 액자 아래 한 줄.**
링크(저장소·npm)를 텍스트 블록에서 액자 캡션 줄로 내려 상단을 비웠다.
**레퍼런스를 보기 전에 네 번 고쳤다.** 판 폭 7.4 → 15, 액자 비율 16/7 → 16/5.5 → 16/6.6 → 16/6,
sway 0.5 → 0.9 → 0.16. 전부 계산이지 근거가 아니었다. 레퍼런스 두 곳을 재자 한 번에 정리됐다.
## 한 문장 컨셉
> **검사대 위의 물체.** 액자가 게이트고, 안에 든 것이 검사 중인 디자인이고, 밖의 글자가 판정 기록이다.
## 프리셋 — `dark-instrument`
초판의 `swiss-minimal` 에서 바꿨다. 페이지가 이미 어둡고 제품이 CLI 다.
**초판에는 이 재질을 정당화하는 프리셋이 아예 없었다** — 그것이 "유리를 발랐는데 유리일 이유가 없다"의 원인이다.
프리셋은 "글래스모피즘은 내비게이션 바 정도까지"라고 반대한다. 사용자의 명시적 지시가 프리셋보다 위이므로
없애지 않고 **의미가 생기는 자리로 옮겼다**: 유리는 **액자에만** 쓴다 — 헤더 · 검사대 프레임 · 명령창.
콘텐츠 카드(브리프, 조판 데모)에서는 뺐다. 유리는 무언가를 담을 때만 의미가 있다.
## 감수한 리스크 — 하나
**히어로에서 거대 타이포를 버리고 힘을 3D 액자로 넘긴다.**
초판 히어로의 `--display` 는 최대 112px 로 화면 높이의 절반을 먹었고, 그 아래 캔버스가 깔려 있었다.
R2 의 h1 은 36px 인데 히어로가 압도적이다 — 힘이 타이포가 아니라 물체에서 나오기 때문이다.
현행 히어로 h1 은 `clamp(var(--step-2), 1.1rem + 2.4vw, var(--step-4))`, 최대 **41.5px** 다.
페이지에서 가장 큰 글자(96px)는 히어로가 아니라 수치 섹션에 있다 — R2 도 그렇다.
## 토큰
`src/styles/tokens.css` 전문. 초판에서 바뀐 것만 적는다.
- **radius 2종**: `--radius-sm: 3px`(요소) · `--radius-frame: 14px`(액자).
근거는 R1 의 2px(621회)과 R2 의 15px. `999px` pill 3곳을 없앴다 — 계기판에 알약은 없다
- **`--glow-2`: `#6E7BFF``#3D7BD6`**. 앞의 값은 R 이 높아 보라-파랑으로 읽힌다.
`antipatterns.md` 의 슬롭 지문이자 `dark-instrument` 가 "어두운 배경일수록 빠지기 쉬운 함정"으로 지목한 색이다
- **유리 색값을 전부 토큰으로**: `--glass-sheen` · `--glass-rim` · `--core-hi`.
`base.css` 에 흩어져 있던 `rgb(255 255 255 / …)` 7곳이 하드 게이트 #1 위반이었다
- **이중 베젤(`.shell`/`.core`) 폐기** → 단일 `.frame`. 껍데기와 속의 radius 가 서로 달라 그것만으로 형태 어휘가 두 종 늘어난다
- **`--step--1: 0.8125rem` 추가**. 게이트 판독기가 스케일 밖 13px 을 쓰고 있었다
- 강조색은 여전히 딥 틸 하나. 라이트/다크 쌍(`--accent` / `--accent-inverse`)은 규칙으로 정의된 반전이라 1종으로 센다
**타입 스케일 실측**: `step--1` ~ `step-4`**6단계**, `--display`(96px)는 스케일 밖, 모노는 본문의 0.9375배 파생.
최대÷본문 = **5.6배**. 초판 페이지는 "5단계"라고 실었는데 실측은 9종이었다 — 아래 8·9번 참조.
## 성능 — 예산과 실측
**예산을 올렸고, 올렸다고 말한다.** 초판 예산은 히어로까지 JS 50KB 였다.
이 페이지는 three.js 를 파는 제품의 데모이므로 그 판단을 검증할 수 있어야 한다.
**예산 숫자 자체를 한 번 고쳤다.** 처음 three.js 예산을 160KB 로 적었는데 실측이 184KB 였다.
three.js 크기는 미리 알 수 있는 값이므로 이건 예산 설정 실패다. 190KB 로 고쳤고, **사후 조정이라는 사실을 남긴다.**
실측을 보고 예산을 맞추는 것은 예산이 아니다 — 다음 실행은 여기서부터 시작해야 한다.
| 항목 | 예산 | 실측 | 판정 |
|---|---|---|---|
| 히어로까지 JS, 초기 (gzip) | 50KB | 복사 버튼 314B + 씬 부트 | 통과 |
| three.js, 지연 로드 (gzip) | 190KB | **184KB** (동적 `import()`, IntersectionObserver 로 화면에 들어올 때만) | 통과 |
| 첫 인터랙션 | 2초 | **미측정** | 배포 후 Lighthouse |
| LCP / CLS | 2.5초 / 0.1 | **미측정** | 배포 후 |
WebGL 게이트: `prefers-reduced-motion` 이면 **아예 시작하지 않는다**. DPR 1.5 클램프.
`hardwareConcurrency <= 4` 면 판을 6장에서 3장으로 줄인다. 탭이 숨거나 화면 밖이면 그리지 않는다.
## 채택한 이펙트와 폴백
| 이펙트 | 구현 | 폴백 |
|---|---|---|
| 검사대 3D | 유리판 6장이 깊이 축에 서고 카메라가 스크롤로 통과. 굴절은 렌더 타겟 1장 샘플링(`transmission` 재질보다 훨씬 싸다) | 캔버스 아래 CSS 그라디언트가 **폴백이 아니라 결과물**이다. 액자·판독기·캡션은 그대로 남는다 |
| 게이트 판독기 | 통과한 판이 바뀔 때만 DOM 을 쓴다. 프레임마다 쓰지 않는다 | JS 없으면 `게이트 01 / 06` 정적 표시 |
| 복사 버튼 | 인라인 314B | 클립보드 실패 시 텍스트를 선택 상태로 만든다 |
## 5단계에서 실제로 걸린 것
정적 검사와 실렌더를 둘 다 돌렸다. **걸린 것을 전부 적는다 — 유리한 것만 고르면 리스크가 자기 홍보가 된다.**
1. **하드 게이트 #3 위반** — radius 4종(`sm`/`md`/`shell`/`999px`). 2종으로 정리
2. **하드 게이트 #1 위반**`base.css` 에 토큰 밖 `rgb()` 7곳. 토큰으로 올림
3. **하드 게이트 #7 위반** — 헤더 "English" 버튼 **2.59:1**. `--ink-muted``--ink` 로 올려 통과
4. **카운트 규칙 위반** — 히어로 h1 이 4줄이었다(상한 2줄). 폰트 크기를 낮춰 2줄
5. **셰이더 결함** — 판이 정면으로 서 있어 `dot(n, view) ≈ 1`, **프레넬이 구조적으로 0**이었다.
유리로 읽히게 하는 유일한 신호가 죽어 있었고 그것이 "그냥 흐릿한 사각형"의 원인이다.
법선을 미세하게 굽히고 UV 경계로 테두리 광을 따로 만들어 해결
6. **셰이더 UV 오류**`gl_FragCoord` 를 렌더 타겟 크기(캔버스의 0.6배)로 나누고 있었다. UV 가 1 을 넘어 텍스처가 잘렸다.
`getDrawingBufferSize()` 로 교정
7. **폭 리듬 부재** — 9개 섹션 중 파이프라인이 **1440px 중 625px(43%)** 만 쓰면서 높이는 1637px 로 가장 길었다.
페이지의 본체가 가장 좁았다. 2열로 열어 1113px
8. **페이지가 실은 수치가 틀렸다** — "타입 단계 5단계" 라고 실었는데 실측 **9종**이었다.
이 페이지의 리스크가 "자기 수치를 공개한다" 인데 공개한 값이 틀리면 리스크가 아니라 허위다
9. **`lang` 속성이 타입 스케일을 새게 했다** — 언어 전환 링크에 `lang="en"` 이 붙어 있고(스크린리더가
"English" 를 영어로 읽어야 하므로 옳다) `[lang]` 셀렉터가 스케일을 통째로 재정의해 그 링크만 16px 이 됐다.
접근성 표시가 조용히 디자인 토큰을 바꾸고 있었다. 크기는 헤더에서 확정하고 링크는 상속만 받게 고쳤다
**측정 스크립트 자체의 오류도 한 번 있었다.** 대비 계산에서 반투명 배경을 불투명으로 취급해
"English 1.15:1" 이라는 틀린 값이 나왔다. 알파를 합성하도록 고친 뒤 실패 0건.
**검사 도구가 틀리면 통과도 실패도 의미가 없다.**
### 최종 판정 (1440px · 320px 실렌더)
- 하드 게이트 12개 **전부 "아니오"**
- 대비: 본문·링크·버튼 전 요소 AA 이상, 실패 **0건**
- 320px: 가로 스크롤 없음(문서 폭 305px), 뷰포트 초과 요소 0, 버튼 줄바꿈 0
- 카운트: 섹션 9 / eyebrow 3(상한 `ceil(9/3)=3`) / 마퀴 0 / 히어로 헤드라인 2줄 / radius 어휘 2종 / 반투명 패널 2개
- 슬롭 지문 grep: 0건
## 의도적으로 하지 않은 것과 그 이유
- **"Anti-Slop" 이라는 명명** — tasteskill 이 쓰는 말이다. 같은 단어를 쓰면 즉시 아류로 읽힌다
- **경쟁 제품의 실측치를 페이지에 싣기** — 남의 수치는 반박 가능하다. 우리 수치만 쓴다
- **GitHub 스타 등 인기 지표** — 우리는 숫자가 0이다. 인기 대신 규율을 증거로 쓴다
- **3D 위에 본문 얹기** — R2 가 준 원리의 핵심이다. 액자 위에 얹는 글자는 판독기 하나뿐이고, 그것도 바탕을 깔았다
- **GLB 모델** — 조사 결과 추상 지오메트리가 이 브리프에서 더 낫다. 모델 하나가 수 MB 이고, 우리가 파는 것은 모델이 아니라 절차다
- **다크를 "멋있어서" 고르기** — 프리셋이 다크 기본값을 슬롭 시그니처 1위로 지목한다. 제품이 터미널이라는 근거로 고른다
- **언어 자동 리다이렉트 / 국기 아이콘** — 사용자를 놀라게 하고, 언어는 국가가 아니다
- **두 판을 번역으로 만들기** — 각 언어판을 따로 썼다
## 3D 의 역할 — 주인공이 아니라 재질
**한 번 액자 안에 넣었다가 되돌렸다.** R2(lusion.co)의 "3D 는 액자 안"은 **3D 자체가 상품일 때**의
규칙이다. 그 스튜디오는 3D 를 팔기 때문에 액자가 쇼케이스 프레임으로 정당하다.
우리는 절차를 파는데 액자 안에 넣을 상품이 없어서, 보는 사람이 "저 상자는 뭐냐"고 묻게 됐다.
게다가 의미를 `게이트 01 / 06` 판독기가 **자막으로 설명하고 있었다** — 그림이 스스로 말하지
못해서 자막을 단 것이다.
이 씬의 역할은 **재질**이다. 공간을 만드는 표면이지 볼거리가 아니다. 그래서 규칙이 반대다:
| | 주인공(액자) | 재질(배경) — 현행 |
|---|---|---|
| 테두리 광 | 0.85 | **0.42** |
| 알파 | 0.72 | **0.34** |
| UV 경계 | `smoothstep(0, 0.05)` | **`smoothstep(0, 0.17)` + 제곱** |
| 카메라 이동 | 전 구간 | **60%** |
| 텍스트 | 액자 밖 | 위에 앉되 **스크림으로 자리를 눌러줌** |
`ResizeObserver` 로 캔버스를 따라가게 했다. `window.resize` 만 듣던 동안 히어로 높이 변화를
놓쳐 배경이 화면의 3분의 2만 덮고 아래가 검게 비었다.
## 패럴랙스 지도 (실측)
깊이가 다른 층을 겹쳐 시차를 만든다. **한 층은 반드시 고정이다**
전부 움직이면 기준이 없어 그냥 흔들리는 페이지가 된다.
| 자리 | 층 | 방식 | 실측 이동 |
|---|---|---|---|
| 히어로 배경 | 셰이더 흐름 필드 | `scroll(root)` | **+56px** ↓ |
| 히어로 글자 | | `scroll(root)` | **88px** ↑ (반대 방향) |
| 파이프라인 | 게이트 씬(three.js) | `sticky` + 카메라 | 6단계를 통과 |
| ㅤ└ 단계명 | | — | **0** 고정 |
| ㅤ└ 산출물 | | `view()` | ±14px |
| 재료 · 설치 · 프리플라이트 | 배경 사진 | `fixed` | **0 — 원경** |
| 사례 | 배경 사진 | `drift` 0.4 | 이미지 높이의 28.6% |
| 조판 | 종이 | `drift` 0.25 | 122px (근경) |
| 재료 타일 | | `view()` | ±26 / ±58px |
| 프리플라이트 항목 | | `view()` | 6 → 10px (뒤일수록 늦게) |
| **수치** | | **없음** | 의도적 정지 |
세 가지 깊이가 다 있다 — **원경**(fixed) · **중경**(drift) · **근경**(콘텐츠와 함께).
종이를 원경으로 두지 않은 이유는 손에 닿는 거리의 재질이기 때문이다.
모바일(390px)에서도 `fixed` 가 유지된다. `background-attachment: fixed` 를 안 쓴 덕이다.
`prefers-reduced-motion` 에서는 게이트 씬이 아예 시작하지 않고, 고정 배경은
`position: absolute` 로 흐름에 돌아가며, 모든 `view()` 애니메이션이 꺼진다.
## 모션 문법 — 섹션마다 다른 말
같은 패럴랙스를 전부에 걸면 벽지다. 각 섹션의 주장을 움직임으로 옮겼다.
| 섹션 | 주장 | 움직임 |
|---|---|---|
| 히어로 | 결정하지 않으면 기본값이 나온다 | 글자 ↑ / 배경 ↓ — **반대 방향**이라야 시차가 보인다 |
| 파이프라인 | 단계를 밟으면 결과가 나온다 | 단계명 **고정**, 산출물만 지연 |
| 사례 | 같은 스킬에서 다른 결과 | 두 판이 다른 속도 |
| 재료 | 표면은 한 겹이 아니다 | 타일이 서로 다른 깊이 |
| 프리플라이트 | 시간순 기록 | 뒤 항목일수록 늦게 |
| **수치** | 공개한 값은 검증 가능하다 | **없음 — 의도적 정지** |
마지막 줄이 결정이다. 숫자가 스크롤에 흔들리면 그 값도 흔들려 보인다.
## 배경 레이어 — 패럴랙스의 기준면
**배경이 단색이면 패럴랙스는 성립하지 않는다.** 앞의 것이 몇 px 어긋나도 비교할 대상이 없다.
히어로 말고는 기준면이 없어서 시차가 안 보였다.
배경 이미지를 사 오는 대신 **이 페이지가 이미 주장하는 것**을 그렸다.
| 레이어 | 무엇 | 어디 | 근거 |
|---|---|---|---|
기준면은 `BgPhoto` 컴포넌트가 만든다(아래 표).
**CSS 격자를 한동안 같이 깔았다가 걷어냈다.** 사진이 들어온 뒤로 배경이 두 겹이 되어
서로 경쟁했고, 격자는 배경이라기보다 **표**로 읽혔다. 배경도 한 섹션에 하나다.
한 층은 반드시 고정이다 — 파이프라인의 단계명은 움직이지 않는다.
전부 움직이면 기준이 없어 그냥 흔들리는 페이지가 된다.
### backdrop-filter 오판을 정정했다
`"backdrop-filter 에 SVG 를 얹으면 Chrome 이 선언 전체를 버린다"` 고 적어두고
페이지의 프리플라이트 로그에까지 실었다. **사실이 아니었다.**
Chrome 151 에서 재측정: computed 값이 `none` 이 아니라 `url("#id")` 로 유지되고,
대비 높은 배경 위에서 실제로 굴절된다. 진짜 원인은 **`feImage``width`/`height`
요소 크기와 맞추지 않은 것**이었다 — `backdrop-filter` 는 필터 이미지를 자동으로 조정하지 않는다.
아직 헤더에 얹지 않은 이유는 브라우저 탓이 아니라 비용이다. Chrome 전용이고,
요소가 리사이즈될 때마다 변위 맵을 다시 만들어야 한다. 헤더는 폭이 계속 변한다.
**한 번 실패한 것을 '안 되는 것'으로 적으면 그 문장이 다음 사람의 상한선이 된다.**
로그 항목을 그 내용으로 바꿔 실었다.
### 패럴랙스를 "고정"으로 바꿨다
세 번 고친 뒤에도 티가 나지 않았다. 원인은 **방식 자체**였다 —
느리게 같이 움직이는 것으로는 원경이 안 된다. 속도만 다를 뿐 방향이 같기 때문이다.
**배경을 뷰포트에 고정한다.** `background-attachment: fixed` 는 iOS Safari 가 throttle 하므로
쓰지 않고, 섹션에 `clip-path: inset(0)` 을 걸어 **`position: fixed` 의 컨테이닝 블록**으로 만든다.
그러면 배경이 화면에 멈춘 채 그 섹션 안에서만 보인다.
실측: 스크롤 1000px 구간에서 배경의 `viewportTop` 이 계속 **0**.
대가가 둘 있고 둘 다 받아들였다.
- **조상에 `transform`·`filter`·`mask` 를 못 쓴다.** 그것들도 컨테이닝 블록을 만들어
고정을 깨뜨린다. 그래서 위아래 페이드 마스크를 포기하고 경계는 clip 으로 딱 자른다
- `prefers-reduced-motion` 에서는 `position: absolute` 로 되돌린다
**전부 고정하지는 않았다.** 원경과 중경을 나눴다.
| 섹션 | 모드 | 왜 |
|---|---|---|
| 재료 · 설치 · 프리플라이트 | `fixed` | 원경. 콘텐츠만 그 위를 지나간다 |
| 사례 | `drift` 0.4 | 두 결과물이 흐르는 자리라 배경도 같이 흐른다 |
| 조판 | `drift` 0.25 | **종이는 손에 닿는 거리다.** 원경으로 두면 거짓말이 된다 |
### 유리 테두리는 conic 이 아니라 linear 로 돈다
헤더의 헤어라인이 **변마다 끊겨** 보였다. `conic-gradient` 로 만들었기 때문이다.
conic 은 중심에서 각도로 도는데, 1180×54 처럼 납작한 형태에서는 각도가 급변해서
긴 변에서 색이 거의 안 변하고 짧은 변에서 한꺼번에 바뀐다. 실제로 아래쪽만 절반 밝았다.
`linear-gradient(100deg, …)` 로 바꾸면 네 변이 같은 그라디언트를 공유해
**테두리를 따라 이어진다.** 마스크로 1px 만 남기는 구조는 그대로다.
그리고 아주 얕은 글로우를 얹었다 — `0 0 22px -8px` 에 강조색을 42% 섞었다.
유리가 차갑게만 보이지 않게 하는 정도이고, 밝은 밴드에서는 흰빛 대신
그림자로 뒤집는다(`--glass-rim`·`--glass-bloom``.band-inverse` 에서 재정의).
### 테두리 광을 회전시켰다
정지한 헤어라인 위에 **도는 빛**을 얹었다. `conic` 의 각도를 `@property` 로 돌리면
매 프레임 배경을 다시 칠하므로(하드 게이트 #8), **정사각형 원뿔 레이어를 마스크 안에서
`rotate` 시킨다.** 움직이는 것은 transform 뿐이다.
`aspect-ratio: 1` 로 정사각형을 유지해야 회전 중 밝기가 일정하고,
`width: 200%` 로 크게 잡아야 1178×52 짜리 헤더에서도 모서리까지 닿는다. 주기 11초.
### three.js 모바일 대응
**세로 화면에서 게이트가 통째로 안 보이고 있었다.** 수직 시야는 화면 비율과 무관하지만
수평 시야는 aspect 에 비례한다 — 데스크톱(2.5:1)에서 정한 판 폭 15 가
세로 화면(0.45:1)의 수평 시야 2.5 에 비하면 **여섯 배**라, 첫 판이 화면을 완전히 덮었다.
`fit()` 에서 `camera.aspect` 를 갱신할 때 판의 `scale.x` 도 같이 잡는다.
그리고 저사양 판정을 고쳤다. `hardwareConcurrency <= 4` 만 보면 **요즘 폰이 전부
고사양으로 잡힌다**(8코어가 흔하다). `innerWidth < 760` 을 함께 본다 —
좁은 화면은 대개 배터리로 돌고 열이 더 난다. DPR 도 1.5 → **1.25**,
캔버스 불투명도도 0.7 → **0.45**(세로에서는 화면 전체를 덮으므로).
### three.js 씬 셋은 서로 다른 것이어야 한다
게이트 씬을 파이프라인으로 옮기면서 히어로에도 그대로 남겨뒀더니
**같은 그림이 페이지에 두 번** 나왔다. 히어로를 다른 것으로 바꿨다.
| 자리 | 씬 | 무엇 |
|---|---|---|
| 히어로 | `shader-plane` 흐름 필드 | 노이즈를 두 번 접어 빛이 유리를 지난 결을 만든다. 공간을 만드는 **재질** |
| 파이프라인 | `gate-scene` 유리판 여섯 장 | 스크롤하면 통과하고 그 단계가 켜진다. 의미를 가진 **주인공** |
| 재료 | `shader-plane` 스테이지 | SVG 필터 타일이 앉는 무대 |
히어로에는 무거운 씬을 임포트하지 않는다 — `three.md` §0 이 권하는 대로
**셰이더 플레인 하나**다. 파일 이름도 `hero-scene.ts``gate-scene.ts` 로 고쳤다.
이름이 실제 용도와 다르면 다음 사람이 히어로 것인 줄 알고 고친다.
캔버스 배경 위 본문 대비 실측(스크린샷 픽셀): 최악 **8.46:1**(가장 밝은 중앙 여백),
h1 자리 14.83:1.
> `readPixels` 로 재려다 전부 0 이 나왔다. `preserveDrawingBuffer: false` 면 버퍼가
> 비워져 있다. **WebGL 위 대비는 스크린샷 픽셀로 재라.**
### 게이트 씬을 파이프라인으로 옮겼다
히어로에서 뺐던 유리판 여섯 장을 여기로 되살렸다. **같은 씬인데 자막이 필요 없어졌다.**
| | 히어로에 뒀을 때 | 파이프라인으로 옮긴 뒤 |
|---|---|---|
| 옆에 있는 것 | 헤드라인과 CTA | **여섯 단계의 실제 이름** |
| 의미 전달 | `게이트 01 / 06` 판독기 — 자막 | 판을 통과하면 그 단계가 켜진다 |
캔버스는 `position: sticky; height: 100dvh` 로 화면에 머물고 섹션이 지나간다.
**진행도는 섹션 높이가 아니라 (섹션 높이 뷰포트 높이)로 잰다.**
섹션 높이로 나눴더니 캔버스가 사라진 뒤에도 진행이 남아 6단계 중 **3단계에서 멈췄다.**
그리고 기준 요소는 캔버스가 아니라 섹션이다 — sticky 캔버스는 자기 rect 가 변하지 않는다.
캔버스 위 텍스트 대비 실측 **14.2:1**(캔버스 불투명도 0.7).
### 패럴랙스가 세 번 실패한 경위
**1. 이동량이 모자랐다.** 여유를 28% 잡아놓고 `±46px`(총 92px)만 움직였다.
스크롤 1800px 구간에서 이동 비율 5% — 아무도 시차를 느끼지 못했다.
공식은 **이미지를 컨테이너보다 크게 만들고 그 초과분 전체를 움직이는 것**이다.
`depth 0.45` → 이미지 145%, 이동 `translateY(-31.03%)`. 같은 구간에서 **374px**.
**2. `overflow: hidden` 이 `view()` 타임라인을 죽이고 있었다.**
`overflow: hidden` 은 스크롤 컨테이너를 만들고, 그러면 자식의 `view()` 가 문서가 아니라
그 상자를 기준으로 잡힌다. 이미지는 상자 안에서 늘 같은 자리라 진행도가 고정된다.
**콘솔 에러도 없고 computed 값도 정상이라** 두 스크롤 위치에서 `transform` 을 재보고서야
이동 0px 인 것을 알았다. 잘라내는 일은 섹션의 `overflow: clip` 에 맡겼다 —
`clip` 은 스크롤 컨테이너를 만들지 않는다.
**3. `opacity` 로 눌러 이미지를 지웠다.** 어두운 광원 사진을 `opacity: 0.5` 로 깔면
검정도 빛도 같이 죽어 **있는지조차 보이지 않는다.** `mix-blend-mode: screen` 으로 바꿨다 —
검정은 투명해지고 밝은 곳만 더해지므로 불투명도를 0.85~1.0 으로 유지해도 본문 대비가 안전하다.
밝은 종이만 `normal` 이다.
**생성 프롬프트에도 같은 함정이 있었다.** "본문 뒤에 깔리니 어둡게"를 너무 강하게 넣어서
안전한 이미지가 아니라 **없는 이미지**를 만들었다. 대비는 합성 방식으로 풀 문제였다.
### 배경 사진 넉 장 (gpt-image-2 로 생성)
CSS 그라디언트 광원만으로는 기준면이 약해서, 굴절과 그레인이 실제로 들어 있는 이미지를 넣었다.
Codex CLI 의 내장 `image_gen`(gpt-image-2)을 ChatGPT 구독 인증으로 호출해 만들었다 — 스톡 이미지가 아니다.
**섹션마다 다른 그림을 쓴다.** 같은 배경을 전부에 깔면 그건 벽지고,
모션에 적용한 규칙("섹션마다 다른 말을 해야 한다")이 배경에서만 면제될 이유가 없다.
| 이미지 | 무엇 | 어디 | 그 섹션의 주장 |
|---|---|---|---|
| `bg-refraction` | 겹친 유리판을 통과한 빛 | 재료 · 설치(flip) | 표면은 한 겹이 아니다 |
| `bg-split` | 한 빔이 둘로 갈라진다 | 사례 | 같은 파이프라인, 다른 결과 |
| `bg-align` | 정렬선 여럿 중 **하나가 어긋나 끊겨 있다** | 프리플라이트 | 자기 결과물을 남의 것처럼 본다 |
| `bg-paper` | 무광 인쇄용지, 섬유와 데클 엣지 | 조판(밝은 밴드) | 한글이 종이 위에 있어야 한다 |
**격자를 쓰는 파이프라인·수치에는 사진을 넣지 않았다.** 거기서는 격자가 그 섹션의 주장이다.
규칙·히어로도 그대로 뒀다 — 히어로에는 three.js 가 있고, 규칙 목록은 담백해야 한다.
프롬프트는 **이 페이지의 토큰 값을 그대로** 넣었다:
- 바탕 `#0B0E12` — 순흑도 네이비도 아니게
- 광원 **둘만**: 좌상단 딥 틸 `#4FD6C4`, 우하단 채도 낮은 파랑 `#3D7BD6`.
보라·마젠타·바이올렛·무지개·따뜻한 색 **금지** (슬롭 지문)
- 프로스티드 글래스 판이 거의 옆에서 보이게 겹쳐, 가장자리만 얇게 빛나고 가운데는 거의 안 보이게
- **대비를 아주 낮고 고르게** — "이 이미지는 흰 본문 텍스트 뒤에 깔린다. 활자와 경쟁할 만큼
밝은 곳이 있으면 안 된다. 중앙 띠는 조용하고 어둡게, 밝기는 모서리 쪽에"
- 미세 필름 그레인, 밴딩 없이
- 금지: 사물·로고·글자·숫자·사람·렌즈 플레어·보케·딱딱한 도형·비네트
`components/BgPhoto.astro` 로 감쌌다. CSS `background-image` 가 아니라 `<Image loading="lazy">`
절대배치한다 — background-image 로 깔면 **항상 받고 최적화도 안 된다.**
같은 이미지를 설치 섹션에서는 `flip` 으로 좌우 반전해 같은 그림으로 읽히지 않게 했다.
소스 WebP: `refraction` 37KB · `split` 14KB · `align` 11KB · `paper` 112KB
(종이만 큰 이유는 섬유 질감이 고주파라 압축이 안 붙기 때문이다)
빌드 결과 12개 파일 합계 **276KB**. 실제로는 뷰포트에 맞는 크기 하나씩만,
그것도 `loading="lazy"` 로 받는다.
**절대배치 배경이 그리드 안에서 561px 로 잘렸다.** `.section` 은 subgrid 이고
`.section > * { grid-column: main; }` 이 걸려 있는데, 절대배치 요소라도 그리드 영역이
잡히면 **그 영역이 컨테이닝 블록**이 된다. 그래서 배경이 화면 가운데 561px 띠로만 깔렸고,
`overflow: visible` 이라 위아래 14% 여유분이 이웃 섹션 위로 넘쳐 조판 섹션의 종이가
프리플라이트 하단에 밝은 띠로 나타났다.
`.bg-photo { grid-column: full }` + `.has-bg { overflow: clip }` 으로 고쳤다.
**`position: absolute` 는 그리드에서 배치를 면제해주지 않는다.**
**codex 0.147.0 메모**`codex-image` 스킬의 `extract_imagegen.py` 는 rollout JSONL 의
`image_generation_call` base64 를 찾는데, 0.147.0 은 다시 **파일**로 저장한다
(`~/.codex/generated_images/<sid>/exec-*.png`, 이벤트는 `custom_tool_call` + `image_generation_end`).
그래서 추출이 fail-loud 로 죽는다 — 이미지는 실제로 생성돼 있다.
그리고 `$imagegen` 은 **프롬프트 맨 앞**에 있어야 내장 도구가 발동한다.
## 그리드의 `wide` 트랙을 좁은 화면에서 닫는다
`--wide-pad: clamp(0rem, -3.7rem + 15.2vw, 10rem)`
고정 `10rem` 으로 두는 동안 **모바일에서도 좌우 80px 씩 살아남아** 390px 화면에서
본문이 `x=112` 부터 시작하고 폭이 278px 로 줄었다. 파이프라인 제목이 네 줄로 접혔다.
닫고 나서 모든 텍스트가 `x=32` 에서 시작하고 제목이 두 줄이 됐다.
같은 이유로 파이프라인의 제목·리드·브리프를 스파인과 한 컨테이너에 넣었다.
섹션 기본 그리드에 두면 제목 `x=371` · 리드 `x=435` · 스파인 `x=143` 으로 **정렬선이 셋**이 된다.
지금은 전부 `x=183` 이다.
## 디자인 SSOT
토큰이 없어서 깨져 있던 세 축을 채웠다.
- **웨이트 3종** — `400 / 500 / 600`. 이전에는 코드 곳곳에 `500·560·600·620·660` 이 흩어져
**6종**이었고 셋은 토큰에 없는 값이었다. R1 모바일 실측은 `400` 이 832회, 나머지 12회다
- **컨트롤 치수** — `--control-h: 44px` · `--control-h-sm: 36px` · `--control-pad-x`.
버튼처럼 생긴 것은 전부 `.btn` + `.btn-solid` / `.btn-quiet` 를 쓴다.
이전에는 두 버튼이 높이 36 vs 44, 패딩 16 vs 8, 테두리 1px vs 0, 웨이트 400 vs 500 이었다
- **`--header-h`** — 떠 있는 헤더의 높이. 간격 토큰을 `clamp()` 로 바꾸는 순간 관계가 끊겨
h1 이 헤더 뒤로 들어갔다. **값이 아니라 관계를 토큰으로 둔다**
- **반응형 간격** — `--space-5~8``--gutter``clamp()` 로. 고정이면 모바일에서
섹션 상하 112px 이 그대로 들어가 화면의 26% 를 여백이 먹는다
- **스크롤바** — `scrollbar-color` 지정. 기본값이면 어두운 페이지 옆에 OS 색 밝은 막대가 선다
## 모바일
- 좌우 여백 24px → **32px** (R1 모바일 실측값)
- 섹션 상하 112px → **56px**
- 헤더에 섹션 앵커 4개 추가. 900px 미만에서는 `<details>` 기반 메뉴로 접힌다 —
JS 0바이트이고 열림 상태·Esc·포커스 순서·`aria-expanded` 를 브라우저가 맞게 한다
- 390px 실렌더: 가로 스크롤 0, 대비 실패 0, 웨이트 3종, radius 2종
## 히어로 3D 의 형태 결정
-**15 × 6** (액자 비율 2.5:1 과 일치). 첫 판이 화면을 완전히 덮어 이미 첫 게이트 안에 들어와 있다
- 판이 화면보다 좁으면 통과하는 내내 좌우 테두리가 남아 **세로 기둥 두 개**로 보인다. 실제로 그렇게 보였다
- 판 높이가 시야보다 작으면 위아래 테두리가 액자 경계에 겹쳐 **이중선**이 된다
- 어긋남(sway)은 `0.16 + 0.05i` 로 아주 작게. 키우면 '통과하는 문'이 아니라 **쌓아둔 사각형**이 된다.
중심축이 살아야 원근이 터널로 읽힌다
- `GATE_GAP` 6.4, 카메라 시작 z 7.2
## 다음 실행이 해야 할 일
1. **배포 후 Lighthouse 로 첫 인터랙션·LCP·CLS 를 재고 "미측정" 을 채운다.** 채우기 전까지 비워둔다
2. 폰트를 CDN 의존에서 자체 호스팅으로 옮길지 판단한다 (현재 jsDelivr · Fontshare · Google Fonts 3곳)
3. OG 이미지가 없다
4. 섹션이 9개, 총 높이 11,197px 이다. 스크롤 보상이 충분한지 재검토할 여지가 있다
---
`2026-08-20` · 2판. 0~6단계 전체 경로 재주행.
`2026-08-21` · 쇼케이스 5종 확장 (총 6개 프리셋 완성).
- 무월 (muwol-coffee): quiet-commerce · 수치표 및 거래 원장 · 앰버 1색
- Pulsegate (pulsegate): dark-instrument · 28개 엣지 레이턴시 계기판 · 코발트 상태점
- 스튜디오 폼 (form-architecture): swiss-minimal · 12열 그리드 & 흑백 단면도 선화 · 무채색 98%
- 오버톤 사운드랩 (overtone-sound): anti-grid · 비대칭 FFT 파형 레이어 & 모노 타이포 · 일렉트릭 앰버 1색
- 한성활자연구소 (font-foundry): editorial-archival · 인터랙티브 조판 시험대 & 한지톤 파치먼트 · 인주 레드 1색
- 전체 쇼케이스 모자이크 그리드 6칸(1-lg, 2-md, 3-sm) 완성, 고해상도 스크린샷 10종 캡처 완료.
---
## 2026-08-21 · 쇼케이스 10종 전면 재주행 (목요일의 화원 제외)
0~6단계를 워크 단위로 다시 돌았다. **각 워크가 서로 다른 가상 클라이언트**이므로
레퍼런스·컨셉·토큰을 페이지별로 새로 정의했다(국소 아님 — 토큰 체계 재정의 = 전체 경로).
### 감사에서 발견된 것 (재주행의 이유)
- 예전 10개 페이지는 전부 "계기판 흉내"였다: 영어 대문자 eyebrow, 티커 마퀴,
발광 상태점, 세로 GNB 레일, 카드+FIG 캡션, Math.random 가짜 샌드박스.
목요일의 화원만이 실제 편집 디자인이었다.
- **dogfood 리서치 5종(aerospace·celadon·incense·optics·synapse)의 "실측값"이
날조였다** — `research/audit-references.md` 참고. spacex는 순흑+D-DIN,
museum.go.kr는 라이트 테스트 등 전부 불일치. 이번 판에서 그 수치를 토큰 근거로
쓰지 않았고 각 페이지 tokens.css 머리글에 그 사실을 남겼다.
새로 실측한 리서치: dogfood-architecture(John Pawson 724px·동일 크기·1px 선),
dogfood-overtone(carstennicolai 1152px·압축 스케일·obys 스태거),
dogfood-foundry(klim 스페시멘 서사·레드 점묘).
### 페이지별 한 줄 (전부 tokens.css 머리글에 상세 기록)
| 워크 | 프리셋 | 이번 판의 결정 |
|---|---|---|
| 무월 | quiet-commerce | 우측 레일·eyebrow 6개 폐기. 가격 첫 화면(R1), 텍스트가 상품(R2), CTA=최암반전 블록 |
| Pulsegate | dark-instrument | 코발트는 잉크로만(배경 금지). 정적 24h 프레임, CSS scroll-draw 배선도, 단일 가격 행 |
| 향연 | quiet-luxury | 한지 SVG 그레인, 세로쓰기 캡션, 사진 없는 상품 목록, 본문 17px·1.9행간 |
| 비색의 침묵 | editorial-archival | 암실 3개의 방, 빙렬 풀블리드 판, 비색 액센트 1색 |
| 스튜디오 폼 | swiss-minimal | 무채색 100%·radius 0, 텍스트 인덱스(Pawson), 스티키 평면도 scroll-draw |
| 한성활자 | editorial-archival | 22rem 글리프+인주각, Lorem 0회, 시험대 슬라이더 3종(네이티브 input+output) |
| 오버톤 | anti-grid | 절차적 유사-FFT 캔버스(오디오 디코드 회피), 스태거 2열, 압축 스케일, 마퀴 1회 |
| Lumina | refraction/WebGL | three.js 커스텀 셰이더 프리즘(IOR·분산 유니폼), 본문은 캔버스와 세로 분리 |
| 궤도 | technical-schematic | 손그린 엔진 단면도(SVG <use> 미러), 모든 수치에 측정조건, 보류 시험까지 공개 |
| Synapse | dark-instrument | 래스터는 SIM 명시, 미측정값 '—', 윤리 섹션을 콘텐츠로 |
### 프리플라이트 (기계 검증)
- 정적 grep: 슬롭 지문 0건, `100vh` 0건, scroll 리스너 0건, uppercase 0건.
토큰 밖 hex 7곳 → 전부 토큰화(각 페이지 `--*-bright`/`--accent-hover` 추가).
- puppeteer DOM 감사 **10페이지 × 3뷰포트(1440/390/320) = 30/30 통과**:
가로스크롤 0, 깨진 이미지 0, 버튼·내비 줄바꿈 0, 텍스트×사진 겹침 0, 콘솔 에러 0.
- 걸려서 고친 것: pulsegate 320px 가로스크롤(인라인 code의 nowrap 최소폭 →
`display:block`+`min-width:0`), lumina 텍스트-캔버스 겹침(절대배치 폐기,
세로 분리 — "3D 위에 본문 금지" 원칙을 스스로 위반했었다).
- **의도적 이탈 1건**: 폼·pulsegate의 `stroke-dashoffset` 스크롤 드로잉은
게이트 8(transform/opacity 외)에서 벗어난다. 선을 긋는 유일한 저비용 수단이며
레이아웃을 강제하지 않는 로컬 리페인트라 채택하고 여기에 기록한다.
### 이미지 파이프라인 (재사용 메모)
- `tools/gen-images.mjs` → codex 내장 image_gen(gpt-image-2), 프롬프트는 stdin.
**codex 배너가 stderr로 나오므로 세션 id 파싱은 e.stdout 포함 catch 필수.**
병렬 실행 시 세션 회수 경합 — `tools/collect-images.mjs`(세션 dir 생성시각순=JOBS순)으로 회수.
- 생성 22장 → webp(900~1600px, q82) 변환로 페이지당 28~220KB.
- 헤드리스 캡처에서 WebGL이 필요한 페이지는 `--disable-gpu` 금지,
`--use-angle=swiftshader` 사용(capture-shots.mjs `webgl: true`).
### 다음 실행이 해야 할 일
1. 비전 감사에서 남은 사소한 제안(차트 축 라벨 크기, 사진 해상도 아쉬움)은
기록만 남김 — 구조 결함 아님.
2. showcase 페이지 언어판(en) 설명문을 새 디자인에 맞게 다듬을 여지.
`2026-08-21` · 국소 — 모바일 메뉴 재작성 + 3건 오버플로 수정. 1 레퍼런스:
navbar.gallery → browserbase.com(항목 많은 사이트는 내비 마크업 자체를 복제해
분리한다; 우리는 5행이라 시트를 알약 전체 폭으로). `<details>` 결정 유지.
- 시트: 11rem 조각 상자 → 알약 전체 폭, 행 48px, mono 번호(0105)·repo·언어 행,
요약 아이콘 버거→X 교체, 44px 터치 대상, 등장 translateY+opacity 180ms.
- **발견한 숨은 버그**: 닫힌 `<details>` 자식에 제작자 `display`를 주면 UA 숨김을
덮어써 '보이지 않지만 레이아웃에 사는' 시트가 된다 — 320px +7px의 진짜 원인.
`.nav-mobile:not([open]) .nav-sheet { display: none }` 안전장치 추가.
- 행 클릭·바깥 클릭 닫힘 인라인 JS 몇 줄 추가(Esc·포커스는 여전히 브라우저 몫).
- ≤480px: 언어 버튼은 헤더에서 물러나고 메덥 행으로(320px 헤더 7px 초과 해소).
- Install 섹션: `@media`의 순수 `1fr` = minmax(auto,1fr) — nowrap 명령 min-content가
트랙을 밀어 390px +5px. `minmax(0,1fr)`로. `.t-path``overflow-wrap: anywhere`.
- 검증: / · /en/ · /showcase/ × 390/320 — 오버플로 0, 메뉴 열림/앵커 닫힘/바깥 닫힘
/행 48px/시트=알약 폭 전 통과.
`2026-08-21` · 국소 — 쇼케이스에서 내비 앵커가 죽는 버그. 내비 항목은 홈 섹션
앵커(#pipeline 등)인데 홈이 아닌 페이지에서는 그 페이지 안의 없는 앵커가 된다.
Base 레이아웃에서 홈 판별 후 `/#…` 로 접두사(언어판별). 홈은 그대로 `#…`.
`2026-08-21` · 국소 — 메뉴 시트 불투명화. .glass 의 잉크(white 4.5%)는 알약용
재질이지 시트용이 아니었다: 뒤 본문이 비쳐 겹쳐 보였다. 메뉴는 크롬 —
배경을 color-mix(surface 94%)로 채우고 rim/그림자만 유리 재질 유지.
`2026-08-21` · 국소 — 쇼케이스를 카드+모달 구조로 재구성(사용자 지시).
1 레퍼런스 tailwindcss.com/showcase(카드 해부: 이미지+이름+한줄종류+태그, 3열,
카드 전체 클릭). taste-skill 4종 병행 적용.
- 카드는 간단한 정보만: 스크린샷(호버 시 2번째 스크린샷 교체) + 이름 + 종류 +
프리셋 배지 + 컨셉 2줄 클램프. 리스크·지표는 카드에서 뺄샘 — 모달로.
- 그리드 비균일 유지: 첫 사례만 2열 폭(featured, 1440px에서 1100px), 나머지 3열(340px).
- 상세는 네이티브 <dialog>+showModal — Esc·포커스 트랩·inert 를 브라우저가 처리
(<details> 메뉴와 같은 결정론). 배경 탭·닫기 버튼은 인라인 JS 몇 줄.
- 모달은 불투명 판(유리 금지 — 메뉴 시트와 같은 교훈), ::backdrop 만 블러.
큰 CTA "사이트 보기" = btn-solid + 둥근 화살 칸(cmd-copy 아이콘 어휘), 새 탭.
- 카드 전체 클릭은 절대배치 <button> 한 개(카드당) — 텍스트는 비활성, 탭 순서=카드 순서.
- 검증 3뷰포트(1440/390/320) 통과: 모달 열림·포커스 내부·Esc/배경/버튼 닫힘·
CTA _blank·오버플로 0·페이지에러 0. 닫기 버튼은 밝은 스크린샷 위 불투명 44px.
`2026-08-21` · 국소 — 프로모션 카피 전면 재작성(사용자 지시: 광고다운 포장) +
레이아웃 미조정. taste-skill 4종 병행.
- **걸린 카피 결함**: "없습니다는 제약"·"못합니다는 말" 어문 붕괴 2건, 존댓말/평서체
혼용 전역, EN 프리플라이트 3번이 design.md에서 이미 정정된 옛 이야기(Chrome 이
선언을 버린다)를 그대로 싣던 것, KO 프리플라이트 6항목만 싣고 note 는 "열세 건"이라
주장하던 것, 미사용 showcase 블록이 옛 자리표 시절 노트("표의 값은 비어 있습니다")를
설명하던 것.
- 카피 방침(파일 머리 주석으로 명문화): 본문 존댓말 통일, 제목은 단형 종결,
새 "—" 금지, 재지 않은 값은 미측정로. 포장은 혜택 문장으로 하되 수치는 그대로.
- KO 프리플라이트 13항목 완전 수록(EN 기준 역번역). EN backdrop-filter 항목 정정.
- 히어로: 리드를 "무엇인지+어떻게 다른지"로 재작성, 2차 링크 "사례 보기(/showcase)" 추가.
- 레이아웃: h1 text-wrap:balance + 하한 3rem→2.6rem(h2 33px 대 41.6px 위계 유지,
320px 4줄→2줄). EN 320px만 3줄(라틴 장문의 자연 꺾임, 데스크톱 2줄 기준 충족).
- 감사: / · /en/ × 1440/390/320 — 오버플로 0, h1 줄수 기준 충족, 링크 렌더 확인,
.entry 13항목 렌더 확인.
`2026-08-22` · 국소 — 관리 앱 형태의 사례 2종 추가(사용자 지시: 메뉴가 작동하고
모달 팝업이 있는, 학습관리 대시보드·수강신청 같은 앱).
- **gaon-lms 가온 학적부**: 종이 학적부 톤(paper #f7f5ef, 흑연 사이드바 #24272b),
형광펜 밑줄 .hl(gradient 배경, 텍스트색 불변), 날인 .stamp. 뷰 3종(대시보드/수강생/
과제·성적) 사이드 전환 + aria-current 이동, 검색·상태칩 필터, 학생 상세 모달
(메모 추가 폼), 채점 모달(저장 시 표 상태 '채점 대기→채점 완료' 갱신). 모달은
네이티브 <dialog> — 쇼케이스 모달과 같은 결정론(Esc·포커스 트랩을 브라우저에).
- **dure-enrollment 두레대학 수강신청**: 남색 잉크 + 주홍 申 도장. 과목 10개,
시간표 블록 grid-row=(start9)×2+2(헤더 1행 오프셋), 겹침·정원 마감·학점 18
초과를 도장 누르기 전에 차단(openCourse 에서 warn + apply disabled), 결과는
인라인 status(aria-live)로. 취소는 블록 제거·학점 회수까지 되돌림.
- **320px 함정 재발**: 모바일 미디어쿼리의 .layout 트랙이 순수 1fr(=minmax(auto,1fr))
라 과목 행 min-content(426px)가 문서 전체를 밀었다(+122px). Install 섹션과 동일한
결함 — 축소 트랙도 minmax(0,1fr). 과목 행 안 .c-main/.course-list 의 암시적 auto
트랙도 함께 minmax(0,1fr)로(부제 '코드·시간·강의실'이 한 단어처럼 밀리지 않게).
- 시간표 .tt(최소폭 376px)는 .tt-scroll(overflow-x:auto)이 위젯 내부 가로 스크롤로
흡수 — 시간표의 표준 패턴이며 문서 오버플로가 아니다.
- -2 스크린샷을 '모달 열림 상태'로 캡처(2종 모두) — 카드 호버 교체 컷이 첫 화면과
동일한 나머지 사례와 달리, 모달이 핵심 기능인 이 두 사례는 교체 컷이 얘기해야 한다.
- 감사 25항목 통과: 뷰 전환/aria-current/검색 축소/칩 단일 선택/모달 open·닫힘(Esc·
버튼)/메모 추가(1→2)/채점 상태 갱신/겹침 차단(미시경제학↔생명과학, 월 14:0014:30)/
신청 블록 2개(월·수)/취소 회수/1440·390·320 오버플로 0.
- work.ts SSOT 등록: 항목 2개(size md, 6행 3+3). 지표의 JS 크기는 실측(7KB/10KB).
`2026-08-22` · 국소 — 관리 앱 2종에 화면 5개씩 추가(사용자 지시: 상세 페이지를 더
밀어붙이고 디자인도 개선). taste-skill 4종 병행. 연장 경로(토큰·컨셉 그대로).
- 1 국소 레퍼런스(뷰 내비 축): docs.astro.build 사이드바 실측 — 폭 300px, 행 40px,
링크 16px, 그룹 헤더로 246개 링크를 정리. 결론: 우리는 뷰 8개라 220px 폭·기존 행
밀도 유지, **그룹 헤더 3개(학적/수업/학사)** 만 도입. WebFetch 으로는 지오메트리를
얻을 수 없어 puppeteer 로 직접 실측(docs.stripe.com·docs.github.com 은 JS 렌더로
사이드바가 안 잡혀 두 번 실패 후 성공).
- **gaon-lms 3→8 화면**: 출석부(칸을 누르면 출석→지각→결석 순환, 주 전환, 반 전환,
휴학생은 ·), 학사 일정(2026-03 달력, 월요일 시작, 오늘=3/9 날인, 다가오는 일정),
상담 기록(memo 를 색인 카드로, 카드→기존 학생 모달 재사용), 자료실(배부 도장
토글 — 가짜 다운로드 링크 대신 상태만 기록), 학기 리포트(**명단에서 실제 계산**:
재원·평균 출석률·과제 제출률, 미채점은 '채점 중'으로 정직하게).
- **dure-enrollment 1→6 화면**: 앱바 아래 색인탭 내비(활성=남색 잉크 밑줄). 관심 과목
(모달 안 관심 남기기 → 여섯 화면이 같은 상태 공유), 신청 내역(여기서도 취소),
이수 현황(네 학기 기록 + 이번 학기 신청을 학점별 실시간 집계), 과목 총람(계열
소계 + 여석 임박 주홍), 공지·변경(게시판 행 문법). 빈 상태는 다음 행동 버튼과
함께(dashed 판 + '과목 찾기로 가기').
- 새 시각 언어: 장부 판(.sheet — 흰 종이에 먹줄 한 줄, dure 전 화면), 종이 그레인
(gaon body::after 고정 레이어, opacity 0.028, pointer-events:none), 표 행 hover,
:active scale 0.92-0.995 촉감 피드백.
- **320px 함정 3연타(같은 결함)**: gaon 모바일 .app 순수 1fr → 사이드 min-content
721px 가 트랙을 밀었다(+401px). 고침: .app 축소 트랙 minmax(0,1fr) + .side
min-width:0 + .side-nav max-width:100%(여덟 항목 가로 색인줄은 aside 안에서만
스크롤). 순수 1fr 은 이제 프로젝트 금지어 — 미디어쿼리 축소 트랙에도 minmax 를
붙인다.
- 감사 48항목 통과: 뷰 14개 전환·aria-current, 출석 순환·주·반 전환, 자료 필터·배부
토글, 달력 42칸·오늘·예정 7건, 상담 필터·카드→모달, 리포트 실계산(3명·91%·96%·
84.4점), 관심→목록 ★, 신청→내역·이수 +3 반영, 총람 소계, 겹침 차단 회귀, 내역에서
취소, 320·390·1440 오버플로 0.
- 스크린샷 재캡처(본 컷 + 모달 -2 컷), work.ts 지표 갱신(뷰 8/6종, 리포트 실계산,
색인탭, 빈 상태).
`2026-08-22` · 국소 — 모달 위치 결함 수정 + 표지 재질 개선(사용자 지시: 가운데도
안 있고 안 이쁘다). 두 앱의 `* { margin: 0 }` 리셋이 `<dialog>` UA 스타일의
`margin: auto`(가운데 정렬의 정체)를 지워 모달이 왼쪽 상단(left:0, top:10)에
붙어 있었다. 쇼케이스트 모달은 별도 스코프라 무사.
- 수정: .modal 에 `margin: auto` 명시(리셋 이긴 UA 값 되살리기). 앞으로 dialog 를
쓰는 페이지에서 `* { margin: 0 }` 리셋과 세트로 검사한다.
- 표지 재질: 안쪽 먹줄(inset box-shadow ring 5px+1px — 스크롤해도 테두리 유지),
잉크빛 그림자(0 32px 64px -28px), ::backdrop 2px 블러+50% 딤, 등장 모션
translateY(14px)+scale(0.985) 240ms, 닫기 버튼 hover 채움(흑연/남색).
- 검증: 프로덕션 margin 상하 261.656/261.672 대칭(완전 중앙), -2 컷 재캡처.
`2026-08-22` · 국소 — 모달 재구성(사용자 지시: 빈약하고 안 이쁘다). "작은 상자에
항목 몇 개"를 넘어 모달을 판(版)으로: 가온 학생 모달 760px 학적 카드(큰 숫자 3개,
**실제 ATT 데이터에서 뽑은 2주 출석 스트립 10칸**, 날짜열 상담 타임라인), 채점 모달
평균 실시간 계산(91.7→84.3, 형광펜 강조), 두레 강의계획 720px(큰 숫자 3개, **주간
자리 미니 격자** 918시 비례 레일, 평가 비중 **누적 막대 한 줄** — 같은 남색의 농도
3단). 모달 제목 step-3(28/30px)으로 상향. 검증: 세그먼트 63/189/378px=10/30/60%
정확, 비전 오탐(비율 안 맞는다는 지적)은 DOM 실측으로 뒤집음. 48항목 재감사 통과.
`2026-08-22` · 국소 — "변한 게 없다"는 보고의 원인은 캐시. HTML 은 엣지가 안
담아(max-age=0) 항상 최신이지만, 정적 CSS 는 Cloudflare 존의 브라우저 캐시 TTL
4시간이 헤더를 14400 으로 덮어쓴다(cf-cache-status REVALIDATED 실측). 새 HTML +
옛 CSS 조합이 그려졌던 것. 대응: (1) 모든 워크 페이지의 CSS 링크를 main.css?v=2
로 버전 걸기 — 캐시를 안 통하는 새 URL. (2) public/_headers 로 /work/* 에
max-age=0 지정(존 설정이 우선하면 효과는 제한적). **운칙: /work 페이지 CSS 를
고치면 반드시 ?v= 숫자를 올린다.** 근본 해법은 존의 Browser Cache TTL 을
'Respect Existing Headers' 로 바꾸는 것(대시보드 권한 필요).
`2026-08-22` · 국소 — "탈락" 통보에 대한 재작업: 모달을 레퍼런스 실측 없이 머릿속에서
그린 것이 원인(스킬 1단계 위반). radix-ui.com 다이얼로그를 puppeteer 로 실측해
문법을 기준 삼음: **폭 500px·패딩 25px·제목 17px·컨트롤 35px 통일·반지름 6px·
완벽 중앙·헤어라인 링+부드러운 큰 그림자·수직 리듬 15-20px**(shadcn/Stripe 데모는
리액트 이벤트가 안 잡혀 실패, Vercel 은 풀스크린이라 부적합, Framer 는 페이지
전환). 비전 조언도 같은 결론: 오스트ere한 원시 컴포넌트를 통째로 닮지 말고
구조 문법만 취하라.
- 적용(레퍼런스 문법 + 종이 물성): 폭 760→620/600px, 제목 28→22/20px, 결산 숫자
축소, 컨트롤 높이 통일(36-38px), 그림자 0 25px 50px -12px, 백드롭 딤 0.55,
워터마크 7rem→4.5rem·투명도 5%, 수직 리듬 균일화(gap 20px).
- 검증: 로컬 비전 평가 8.5-9/10(예약 사항=워터마크 가독성 → 투명도 하향으로 반영),
48항목 회귀 통과, 프로덕션 v=4 확인(중앙 offX=0, 제목 22px, 스트립 10칸).
- 교훈(design.md 규칙으로 승격): **모달·새 컴포넌트를 만들 때는 반드시 실측
레퍼런스의 수치를 먼저 적는다. 느낌으로 폭·제목 크기를 정하지 않는다.**
`2026-08-22` · 국소 — "변화량이 작다, 근본적으로 뜯어고쳐라" 통보: 스프레드(펼침
장부) 구조로 전면 재구성. 모달을 "작은 상자"가 아니라 **앱 껍데기의 축소판**으로 —
왼쪽 250px 표지판(가온=흑연 #24272b, 두레=남색 #1e3355)에 도장·이름·결산 행·물마크,
오른쪽 종이 내용판(스크롤은 여기만). Linear/Notion 피크 다이얼로그의 분할 문법.
- 채점 모달은 단순 중앙판 유지(내용이 짧다). 모바일(≤680px)에서 표지판은 가로 띠로
접히고 통계는 물러난다.
- **"안 바뀐다"의 두 번째 범인 발견**: 로컬 3000번 서버가 옛 dist 를 서빙 중이었다
(netstat 에 LAN 다른 기기의 ESTABLISHED 확인 — 사용자가 옛 빌드를 보고 있었다).
서버를 public(소스 실시간)으로 재가동. 로컬 검증은 서빙 디렉터리부터 확인한다.
- 검증: 비전 9/10 두 건("Linear/Notion 피크와 대등"), 48항목 회귀 통과, 프로덕션
v=5(mcover 7곳) 확인.
## 2026-08-22 — 관리 앱 2종: shadcn 프로덕션 콘솔 문법(v6→v7)
- **왼쪽 상단 모달 버그 3범인** 재확인: `* { margin: 0 }` 리셋이 `dialog`의 UA `margin: auto`(가운데 정렬)를 지운다. `.modal { margin: auto }` 없으면 모달은 (0,0)에 붙는다.
- **overflow-x: auto의 반대축 함정**: 한 축만 auto/scroll로 지정하면 다른 축의 visible은 계산값 auto가 된다. 여기에 `margin-bottom: -1px`+보더 트릭이 더해지면 콘텐츠가 1px 넘어 **세로 스크롤이 생긴다**(Windows 비오버레이 스크롤바는 바로 보인다). 규칙: `overflow-x: auto`를 쓰면 `overflow-y: hidden`을 명시하고, 음수 마진+보더 트릭 대신 border-box 높이로 해결한다. 탭 스트립은 `scrollbar-width: none`까지(스와이프는 유지).
- **찌꺼기 규칙 감사**: 그리드 구조를 바꿀 때 이전 구조용 선택자(`.panel:last-child { grid-column: 1/-1 }` 등)가 남아 새 배치를 엉망으로 만든다. 레이아웃 규칙을 고치면 같은 선택자 범위의 형제 규칙을 전부 읽는다.
- **빈칸은 데이터로 메운다**: 대시보드 2열 그리드에서 우측 2행이 비었던 문제 — 패널을 `grid-row: span 2`로 늘리고 차트 아래 요일별 결산 표(출석/지각/결석/출석률+합계)를 넣어 해결. 장식이 아니라 프로덕션 정보(카드 부정보, 진행 중 배지, 미응시 경고)로 밀도를 올린다.
- 캐시 운칙 준수: CSS 고칠 때마다 `main.css?v=N` 증가 — 현재 v7.
## 2026-08-22 — LMS 필수 요소 조사 반영(v9)
- 웹 검색으로 LMS 필수 기능을 조사해 가온에 없던 다섯 가지를 추가했다: **알림 센터**(탑바 종 + 드롭다운, 읽음 처리, 바깥 클릭 닫힘), **공지사항 뷰**(핀 고정 · 카테고리 칩 · 읽은 사람 수 · 아코디언 본문), **수업 진도**(과목별 차시 진행 바), **평가 유형**(과제/퀴즈/시험 칩 + 자동 채점 상태), **출결 사유·학부모 알림톡 요약**(병결/무단/보강/발송 수). 근거: 학원 LMS는 출결 알림(알림톡)·학부모 소통·보강이 일반 LMS와 구별되는 핵심 기능이라는 조사(웹헤즈, 출결톡 등).
- 폰트 라이선스 기록(typography.md §7 의무화): 두 앱 모두 본문 Pretendard Variable(SIL OFL, 웹 임베딩·서브셋 가능, 동적 서브셋 CDN) / 모노 JetBrains Mono(OFL). 폴백은 -apple-system·system-ui, 수치 위치엔 tabular-nums 적용.
- heredoc 잘림 재발: 긴 HTML 패치는 bash heredoc 대신 .mjs 패치 파일(Write 도구)로 쓴 뒤 실행한다 — 이미 두 번째 사고다.
## 2026-08-22 — 전 뷰 프로덕션 밀도(v10)
- "대시보드만 고쳤다"는 지적이 맞았다 — 뷰별 요약 스트립(statline) 컴포넌트를 만들어 아홉 뷰 전부에 같은 문법을 적용했다. **부분 개선은 완성이 아니다. 뷰 하나를 고치면 전 뷰를 같은 기준으로 훑는다.**
- 뷰별 추가: 수강생(학년·보호자 열 + 실계산 요약 + 모달 보호자·알림톡 이력), 출석부(반·주 기준 실계산 요약 + 일별 합계 tfoot — 반 전환 시 재계산), 과제·성적(채점 진행 요약 + 과목 필터), 자료실(크기·조회 수 + 요약), 학사 일정(유형색 배지 4종 + 범례 + D-day + 월 요약), 상담(요약 + 예정 상담 3건 + 알림톡 상태), 리포트(학기 요약 카드 4장 실계산), 공지(분류 필터).
- **감사 스크립트도 제품이다**: 검사 추가 시 셀렉터 다중 개수는 반드시 $$eval — $eval 은 첫 요소만 돌려줘 undefined.length 가 된다. 뷰 루프 순서가 바뀌면 '마지막 뷰' 전제 검사도 함께 바꾼다.
- 표에 열을 더하면 그 표가 .table-wrap 안에 있는지 다시 본다 — 자료실이 래퍼 밖이라 390px +104px 넘침이 재발했다.
## 2026-08-22 — 원론 조사 기반 전면 개정(v11): 타이포그래피·UX 방법론·한글 폰트
- 14개 웹검색·출처 110여 개를 정리한 research/design-principles-v11.md(105개 항목)를 기준으로 두 앱을 개정했다. 핵심 적용: **볼드 금지·수치 웨이트 원칙**(700 제거, UA strong/th 볼드를 600/500으로 재정의, font-synthesis-weight: none), **WCAG 대비**(보조 텍스트 --faint #a1a1aa#6e6e77, 2.6:1→5.05:1), **한글 자간 0 원칙**(h2·h3의 -0.02em 제거, 큰 제목만 -0.01em), **행길이 제한**(공지 본문 68ch), **터치 대상**(coarse 포인터에서 출석 칸 40px).
- **폰트 전략(각주 한 문장)**: 가온 = Noto Serif KR 600(학적부 서류의 명조 결, 제목·표지만) + Pretendard Variable 본문. 두레 = IBM Plex Sans KR 본문(대학·기술 문서의 중성 목소리) + Noto Serif KR 600(공문·신문 결, 마스트헤드·제목). 모두 SIL OFL 웹임베딩 가능. **정적 웨이트 폰트(Plex)에 650을 요청하면 700으로 올라간다** — 가변 폰트에만 500 대역 중간값을 쓴다.
- **대형 사고: CSS 닫는 중괄호 하나를 잃어 규칙 절반이 죽은 채 배포될 뻔**. 원인은 문자열 교체 패치가 닫는 } 를 삼킴. CSS 중첩 파싱 덕에 .tabs 하위 규칙은 살아 겉보기 정상이었고, .tabs 밖 규칙(표 헤더 등)만 죽어 UA th 볼드로 증상이 드러났다. **교훈: CSS를 고치면 중괄호 균형 검사를 감사에 넣는다** — 감사 도구에 brace-depth 체크 추가.
## 2026-08-22 — 모바일 눈 검수와 갸행 수습(v12/v13)
- "모바일은 눈으로 보라"는 지적이 맞았다 — 오버플로 수치만 보고 390px 실제 렌더를 본 적이 없었다. **390px 전 뷰 스크린샷 → 시각 검수 + Range.getClientRects 기반 줄바꿈 계측**을 병행했다.
- 갸행 탐지기의 함정 두 개: ① 같은 줄의 strong/본문 서로 다른 폰트 메트릭이 top 을 12px 차이내 별도 줄로 오탐 → **top 묶음에 ±5px 허용오차 + 높이 교차검증**. ② 비전 검수는 작은 글자에서 환각(안 끊긴 시간을 끊겼다고 보고, 안 늘어난 카드를 늘어났다고 보고) → **비전은 끊김 '지점' 평가에만 쓰고, 여부는 계측으로 확정**한다.
- 적은 수정: 라벨·본문 전역 `word-break: keep-all; overflow-wrap: anywhere; text-wrap: pretty`(한글은 어절에서만 끊고 외톨이 줄 방지), 마감 임박·예정 상담 행은 ≤680px 에서 세로 스택, 공지 행 ≤680px 1열(제목 전폭), tfoot 라벨 nowrap, 두레 과목행 부정보는 **세그먼트(코드·교수·시간·강의실)별 nowrap `<i>`** — 시간 값 "10:0011:30" 이 중간에 끊히는 것을 원천 차단.
- 검증 함정: 같은 세션에서 CSS 를 고치고 곧바로 찍으면 헤드리스가 옛 파일을 볼 수 있다 — **검증 직전 setCacheEnabled(false) 와 ?v= 범프를 같이** 걸고 '실제 로드된 링크 버전'을 계측값에 함께 찍는다.
- 두레에 가온과 같은 뷰 밀도 기준 적용(관심 과목: 요약 스트립+계열 필터+학점 초과 사전 경고, 신청 내역·이수 현황·총람·공지: 실계산 요약 스트립) — "고치면 전체를 같은 기준으로" 재적용.
## 2026-08-22 — 5라운드 정밀 검수(v13/v14)
- **R1 데스크톱 밀도**: 두레 관심(402px)·이수(435px) 등 절반빈 뷰 발견 → 관심 빈 상태에 여석 임박 추천 3건, 이수 표에 구분별 진행 바+총 이수율 72% 바, 신청 내역에 학점 게이지+규칙 안내, 가온 리포트에 진행 차시 열.
- **R2 극한 폭·상태**: 320px 전 뷰 오버플로 0, 모바일 모달 1열·닫기 접근·뷰포트 내, 실제 Tab 키 포커스 링 전 요소 2px solid (스크립트 focus 는 focus-visible 미적용이 올바름 — 키보드로 재검), 신청→게이지 실시간 갱신.
- **R3 플로우**: 빈 검색 안내·특수문자 무결·필터+검색 조합·출석 순환 재계산·채점 갱신·메모→상담 동기화·관심 담기→추천 숨김·17학점+2 시도 → 신청 버튼 disabled (학점 초과 차단).
- **R4 대비·타이포 전량 스캔**: 전 요소 fg/bg 블렌드 대비 계산 → 28건 미달 전부 --muted-foreground #71717a(4.4:1) 탓 → #64646e 로 교정(5.2:1+), 달력 '오늘' 숫자 배경 #059669#047857(5.4:1). 웨이트 5종(450·650·550 포함) → **400/500/600 3종**으로 정규화. tabular 누락 0.
- **R5 데이터 정합성**: 대시보드 "재원 26명"↔명단 12명, 오늘의 수업 8/8·6/9·9/9↔과목별 재학 3·3·2·3명 등 세계관 모순 발견 → 12명 기준으로 숫자 체계 전면 재정합(헤드라인·차트·결산표·마감·과제 표·알림·출결 칩·공지 읽은 사람).
- **교훈: 데모의 숫자도 하나의 세계관을 이룬다** — 한 화면의 통계를 고치면 다른 화면의 같은 사실도 함께 고친다. 대비 스캐너(배경 블렌드 계산)와 정합 스윕은 감사 도구에 남길 가치가 있다.
## 2026-08-22 — 모바일 개행 재보고: 기기 글꼴 확대 시나리오(v14/v15)
- "모바일에서 개행 문제가 여전히 보인다"는 재보고. 프로덕션 실측(390/375/360)에선 h1 이 모두 1줄 — 문제는 **기기 글꼴 확대(안드로이드 큰 글꼴 1.15~1.3×)** 재현 시에 나타났다: h1 이 2줄로 부서지며 외톨이 글자가 생기고, rem 기반 여백까지 커져 제목 칸이 좁아진다.
- **해법 — 제목은 뷰포트 단위**: `@media ≤680px { h1 { font-size: min(1.5rem, 6.4vw) } }`. vw 는 사용자 글꼴 설정을 무시하므로 제목 크기가 고정되고, 본문은 사용자 설정을 존중해 커진다. 제목은 디스플레이 요소이므로 이 예외는 정당하다. `text-wrap: balance` 를 h1(블록) 에 추가 — 예외시 균형 분할. flex 컨테이너(.n-t)의 balance 는 무의미해 제거.
- `-webkit-text-size-adjust: 100%` 방어 추가(iOS 자동 인플레이션 차단).
- **측정 교훈 3연타**: ① flex 컨테이너 안 아이콘과 텍스트의 top 차이가 '가짜 줄'로 집계된다 — 텍스트 노드만 분리 측정할 것. ② 비전은 전사 기반으로만 믿을 것(추측 보고가 2번 환각). ③ page.evaluate 클로저 참조 함정 세 번째 반복 — 측정 함수는 문자열로 페이지에 주입한다.
- 검증: 폰트 스케일 1.0/1.15/1.3/1.45× 에서 h1 1줄 유지(양 앱), 공지 제목은 확대 시 어절 경계 2줄(자연스러움), 감사 ALL PASS.
## 2026-08-22 — 모바일 브랜드 세로 쌓임 결함(v15)
- "학적부 타이틀이 세로로 나온다"는 보고 — 표준 390px 에서도 재현: 모바일 가로 스트립에서 `.brand` 가 flex-shrink 기본값(1)으로 side-nav 에 밀려 **54px·5줄**(글자 하나씩 세로)로 찌그러짐. 스트립 높이도 145px 로 부풀어 있었다.
- 수정: `@media ≤900px { .brand { flex: none; white-space: nowrap } }` — 브랜드는 고정 폭, 내비만 스크롤. 결과: 113px·1줄, 스트립 57px, 글꼴 확대 1.15/1.3× 에도 1줄 유지.
- **교훈: 텍스트가 '세로로 쌓인다'는 보고는 컨테이너가 1글자 폭으로 수축했다는 뜻** — flex/grid 아이템의 shrink 기본값을 의심한다. 방어 검사에 '모든 텍스트 요소의 최소 자식 폭 ≥ 2em' 같은 수축 탐지를 넣을 만하다.
## 2026-08-22 — QA 게이트 구축(v16) — 6계층과 기계적 배포 강제
- 업계 조사(시각회귀·품질게이트·테스트피라미드·크로스브라우저·axe 57%·QA체크리스트)를 대조해 계획 보강 후 전 계층 구축: **L0 정적**(stylelint·html-validate) → **L1 단위**(calc.js 추출, 브라우저 없이 30 경계 테스트) → **L2 불변식**(기능 감사 61 + h1·수축·대비·axe·콘텐츠 정합·스케일×폭 행렬) → **L3 시각 회귀**(기준 30화면 pixelmatch, 0.000%) → **L4 WebKit**(Playwright, 8항목) → **L5 탐색**(키보드 완전 통과 + 여정 3개).
- 게이트가 잡은 실제 결함: CSS 중복 셀렉터 2건, aria-label 오용 26건, dl 안 <p> 구문 위반, role="img" 라벨 소실(내 정리의 부작용까지), 두레 시간표 role="grid" 구문 위반, 두레 #q 포커스 링 — **전부 사용자 보고 이전에 자동으로 발견**.
- **배포는 이제 `npm run verify && build && deploy` 체인으로만** — verify.mjs 실패(exit 1)면 배포 불가. 리포트(tools/verify-report.md)는 수치와 함께 자동 생성.
- 하니스 교훈: ① 스코프 없는 선택자는 숨은 뷰의 같은 속성을 잡는다(대시보드 due-row도 data-assign 사용) — 선택자는 컨테이너 스코프 필수. ② 모달 닫힘 직후 포커스 강탈(비동기 복원) — 뷰 전환+타깃 포커스는 한 evaluate 로, 필요시 settle 후 재포커스. ③ UMD 가드는 module.exports 존재 검사가 아니라 module 존재 검사.
## 2026-08-22 — 스킬 내재화: 자율 검증 폐쇄 루프(v17)
- designpaca 스킬에 폐쇄 루프를 박았다: **핵심 규칙 6 "완료의 정의는 게이트 통과다"**, 5단계 기계 검사 4번째 "design-gate 실행 의무". references/audit-gate.md(사고-검사 매핑·SEO/meta 체크리스트·OS/브라우저 특성·하니스 규칙 7개) + tools/design-gate.mjs(범용 게이트, --init 스캐폴딩, 의존성 미비 SKIP) 동반.
- 도그푸드: 스킬 게이트가 두 앱에서 **og:image·twitter:card 부재, 뷰마다 h1(9·6개)** 를 잡음 → 탭 앱 표준 구조로 교정(브랜드=페이지 h1 sr-only, 뷰 제목=h2.v-h — 명시적 클래스, 선택자 추측 금지), OG 메타+og.png 추가, v17 배포(게이트 체인).
- **대형 사고와 자가 포착**: 패치 스크립트의 튜플 실수(`([f,t])[1]`)가 'h'→'1'·'c'→'o' 문자 치환을 일으켜 main.css 2개·invariants.mjs 가 손상 — **시각 회귀(13.9% diff)가 즉시 포착**, dist v16 빌드에서 복구. 교훈: ① 문자열 치환 패치는 사후 검증(변경 후 포함 여부 확인) 필수 — og 삽입도 조용히 실패한 뒤 재삽입했다. ② 패턴 매칭 대신 명시적 클래스(v-h)·명시적 앵커(</title>)를 쓴다. ③ 배포 아티팩트(dist)는 복구 원본이다.
- 기준 화면 갱신 1회(h1→h2 의도 변경, 1~2px 라인박스 시프트) — 방법론대로 '검증된 의도 변경'으로 기록 후 갱신.
## 2026-08-22 — 시간표 시간 라벨 깨짐 수리(v18)
- 사용자 보고 "왼쪽 타임 라벨 깨짐" — 계측으로 확정: 라벨 top 간격이 22·194·43·22 불규칙, 일부 라벨 폭 78px(요일 열에 배치). **원인: span-2 셀이 섞인 그리드 자동 배치(auto-placement)의 취약성** — 시간 라벨(1행)과 셀(2행 스팬)이 교차하며 희소 배치 커서가 열1의 빈 행을 잡아 라벨이 엉뚱한 행·열로 흩어졌다. 세션 초반 의심 목록에 있던 "--rows 미사용 문제"의 실체이기도 하다.
- 수리: buildAxis 를 **전 요소 명시적 격자 좌표**(corner 1/1, day 1/(d+1), hour (h-9)*2+2행/1열, cell 행 span2/열 d+1)로 재작성 — 자동 배치 의존 제거. 검증: 9라벨 전부 1열(40px 균일), 간격 Δ43~44px 등차(2행×21.6px 설계 정합). 시각 diff 0.38%(라벨 재배치 = 의도) 확인 후 기준 갱신, 게이트 통과, v18 배포.
- **교훈: 혼합 span 의 그리드는 자동 배치를 믿지 않는다** — 좌표를 명시하라. 격자 레이아웃 검사에는 '라벨 등간격(Δ 균일)' 불변식을 넣을 가치가 있다(스킬 하니스 규칙 후보).