Compare commits

..

2 commits

Author SHA1 Message Date
Yun Chan
26686e363b chore: v0.8.0 으로 버전을 올린다
Some checks failed
ci / build (push) Failing after 22s
release / release (push) Successful in 44s
2026-08-23 15:40:58 +09:00
Yun Chan
46b47884c9 feat(skill): 왼쪽 인셋 리듬·픽셀 판정을 스킬에 내재화
이번 라운드(스크롤 잔류·빈약한 헤더·인셋 불일치 — 사용자 보고 3건)의 교훈을 스킬로 승격:
- design-gate leftInset 검사 내장(텍스트 노드 Range 기준 ≥12px, 모바일 390px 전 뷰) — 오버플로 검사는 오른쪽만 보는 계측 공백을 메움. 도그푸드: 양앱 통과(가온 16·두레 24 각자 일관 축)
- 브랜드 검사 오탐 수정(동일 셀렉터 다중 요소 시 보이는 것 기준 — 도그푸드가 잡음)
- preflight §0-E 왼쪽 인셋 리듬, 하니스 규칙 15~17(기준 갱신 의도 리뷰·스크린샷 픽셀 판정·RED 재현), 사고표 4행
- SKILL.md 규칙 6 보강: 계측의 공백 — 보고가 오면 픽셀 판정으로 수치화하고 게이트에 추가
2026-08-23 15:40:58 +09:00
11 changed files with 88 additions and 8 deletions

View file

@ -1,4 +1,4 @@
# 게이트 리포트 — 2026-08-23 04:48
# 게이트 리포트 — 2026-08-23 06:40
**전 항목 통과 — 배포 가능**
@ -65,6 +65,15 @@ PASS gaon-lms/calendar 터치타깃 — 컨트롤 ≥44px·인라인 ≥24px
PASS gaon-lms/counsel 터치타깃 — 컨트롤 ≥44px·인라인 ≥24px
PASS gaon-lms/report 터치타깃 — 컨트롤 ≥44px·인라인 ≥24px
PASS gaon-lms/notice 터치타깃 — 컨트롤 ≥44px·인라인 ≥24px
PASS gaon-lms/dashboard 왼쪽 인셋 — 최소 텍스트 시작 16px
PASS gaon-lms/students 왼쪽 인셋 — 최소 텍스트 시작 15px
PASS gaon-lms/attendance 왼쪽 인셋 — 최소 텍스트 시작 16px
PASS gaon-lms/grades 왼쪽 인셋 — 최소 텍스트 시작 16px
PASS gaon-lms/files 왼쪽 인셋 — 최소 텍스트 시작 16px
PASS gaon-lms/calendar 왼쪽 인셋 — 최소 텍스트 시작 16px
PASS gaon-lms/counsel 왼쪽 인셋 — 최소 텍스트 시작 16px
PASS gaon-lms/report 왼쪽 인셋 — 최소 텍스트 시작 16px
PASS gaon-lms/notice 왼쪽 인셋 — 최소 텍스트 시작 16px
PASS dure-enrollment SEO/meta — title 15자·desc 79자·OG✓·alt 0결
PASS dure-enrollment/catalog 오버플로 — 320/375/390/768/1440
PASS dure-enrollment/catalog h1 1줄
@ -104,6 +113,12 @@ PASS dure-enrollment/enrollments 터치타깃 — 컨트롤 ≥44px·인라인
PASS dure-enrollment/credits 터치타깃 — 컨트롤 ≥44px·인라인 ≥24px
PASS dure-enrollment/archive 터치타깃 — 컨트롤 ≥44px·인라인 ≥24px
PASS dure-enrollment/notices 터치타깃 — 컨트롤 ≥44px·인라인 ≥24px
PASS dure-enrollment/catalog 왼쪽 인셋 — 최소 텍스트 시작 24px
PASS dure-enrollment/starred 왼쪽 인셋 — 최소 텍스트 시작 24px
PASS dure-enrollment/enrollments 왼쪽 인셋 — 최소 텍스트 시작 24px
PASS dure-enrollment/credits 왼쪽 인셋 — 최소 텍스트 시작 24px
PASS dure-enrollment/archive 왼쪽 인셋 — 최소 텍스트 시작 24px
PASS dure-enrollment/notices 왼쪽 인셋 — 최소 텍스트 시작 24px
PASS L3 시각 회귀 — SKIP — checks.visual=false
PASS L4 WebKit — SKIP — checks.webkit=false
```

View file

@ -1,5 +1,11 @@
# designpaca
## 0.8.0
### Minor Changes
- 왼쪽 인셋 리듬·픽셀 판정을 스킬에 내재화 — design-gate leftInset 검사(텍스트 노드 Range 기준 ≥12px, 모바일 390px 전 뷰), 브랜드 검사 오탐 수정(보이는 요소 기준), preflight §0-E(왼쪽 인셋 리듬), 하니스 규칙 15~17(기준 갱신은 의도 변경 리뷰 동반 — 결함이 기준에 박히는 것 방지, 사용자 스크린샷 픽셀 판정 해부, 새 검사 RED 재현 후 GREEN), 사고표 4행(스크롤 잔류·빈약한 헤더·인셋 불일치), SKILL.md 규칙 6 보강(계측의 공백 — 보고가 오면 픽셀 판정 후 게이트에 추가)
## 0.7.3
### Patch Changes

View file

@ -1,6 +1,6 @@
{
"name": "designpaca",
"version": "0.7.3",
"version": "0.8.0",
"description": "웹 디자인 파이프라인 스킬 — Claude Code · Codex · Cursor 에 한 줄로 설치한다",
"keywords": [
"design",

View file

@ -1,5 +1,7 @@
# @designpaca/core
## 0.8.0
## 0.7.3
## 0.7.2

View file

@ -1,6 +1,6 @@
{
"name": "@designpaca/core",
"version": "0.7.3",
"version": "0.8.0",
"private": true,
"description": "designpaca 설치 엔진 — 타깃 어댑터, 매니페스트, 드리프트 감지",
"type": "module",

View file

@ -1,5 +1,11 @@
# CHANGELOG
## 0.8.0
### Minor Changes
- 왼쪽 인셋 리듬·픽셀 판정을 스킬에 내재화 — design-gate leftInset 검사(텍스트 노드 Range 기준 ≥12px, 모바일 390px 전 뷰), 브랜드 검사 오탐 수정(보이는 요소 기준), preflight §0-E(왼쪽 인셋 리듬), 하니스 규칙 15~17(기준 갱신은 의도 변경 리뷰 동반 — 결함이 기준에 박히는 것 방지, 사용자 스크린샷 픽셀 판정 해부, 새 검사 RED 재현 후 GREEN), 사고표 4행(스크롤 잔류·빈약한 헤더·인셋 불일치), SKILL.md 규칙 6 보강(계측의 공백 — 보고가 오면 픽셀 판정 후 게이트에 추가)
## 0.7.3
### Patch Changes

View file

@ -34,7 +34,7 @@ description: "웹 디자인 전 과정을 끌고 가는 파이프라인 스킬.
3. **화려함은 4순위다.** Awwwards 배점이 Design 40 / Usability 30 / Creativity 20 / Content 10이다. 3D를 얹는 것보다 타입 스케일을 정돈하는 게 결과에 두 배 유효하다.
4. **대담함은 한 곳에만.** 리스크는 하나다. 나머지는 조용히 받쳐준다. 두 곳에서 소리치면 둘 다 죽는다.
5. **성능 예산을 넘기면 그 이펙트는 채택하지 않는다.** 예산은 3단계에서 정하고 5단계에서 검증한다. 데모·포트폴리오 브리프면 예산을 올려도 되지만 **올렸다고 말해야 한다**.
6. **완료의 정의는 게이트 통과다(자율 검증 폐쇄 루프).** 5단계에서 `tools/design-gate.mjs` 를 프로젝트에 심어 돌리고, 실패 항목이 있으면 4단계로 돌아가 고치고 **전체를 재실행**한다. 게이트 없이 6단계로 넘어가지 않는다. 사용자를 QA 로 쓰지 않는다 — 검증은 기본 내장, 사용자 피드백은 보조다. 상세는 `references/audit-gate.md`.
6. **완료의 정의는 게이트 통과다(자율 검증 폐쇄 루프).** 5단계에서 `tools/design-gate.mjs` 를 프로젝트에 심어 돌리고, 실패 항목이 있으면 4단계로 돌아가 고치고 **전체를 재실행**한다. 게이트 없이 6단계로 넘어가지 않는다. 사용자를 QA 로 쓰지 않는다 — 검증은 기본 내장, 사용자 피드백은 보조다. 상세는 `references/audit-gate.md`. **게이트는 계측이고 계측엔 공백이 있다**(왼쪽 인셋·시각적 무게는 오버플로 검사를 통과한다). 사용자 보고가 오면 방어하지 말라 — 스크린샷을 픽셀 판정으로 해부해 원인을 수치화하고, 그 검사를 게이트에 추가해 재발을 막는다(하니스 규칙 15~17).
7. **OS·플랫폼 관습은 조사 없이 바꾸지 않는다.** 내비 구조·뒤로가기·시트·safe area·터치 타깃 같은 관습은 사용자가 이미 OS 전체에서 배운 것이다. "웹에서는 그랬다"를 폰에 옮기면 백 키가 앱을 종료하는 웹앱이 된다(실측 사고). 바꾸기 전에 HIG·M3·NN/g 에서 근거를 찾고, 모바일 앱 수준 브리프면 `references/mobile-app-ux.md` 를 구현 전에 읽는다.
**접근성은 예외다. 이것만은 협상하지 않는다.** 키보드·포커스·대비·`prefers-reduced-motion`은 어떤 이펙트보다, 어떤 브리프보다 우선한다. 픽셀은 왜곡해도 DOM은 살린다.

View file

@ -1,6 +1,6 @@
{
"name": "@designpaca/skill",
"version": "0.7.3",
"version": "0.8.0",
"private": true,
"description": "designpaca 스킬 원본 — SKILL.md 와 참조 문서",
"scripts": {

View file

@ -29,6 +29,9 @@
| 같은 명시도의 숨김 규칙이 기본 규칙보다 앞에 있어 짐 — 모바일에서 숨기려던 kbd 안내가 렌더됨(6% 시각 diff) | 시각 회귀 + 규칙: **숨김 오버라이드는 기본 규칙보다 파일 뒤쪽에** 두거나 컴포넌트 규칙 근처의 미디어쿼리로 |
| 탐색 차터가 결과를 단언하지 않아 "9288점" 입력이 조용히 통과(값을 덧붙인 타이핑) | 하니스 규칙 8(검사는 단언)·9(number 입력 조작) |
| 모바일 라운드 — 중앙 모달이 썸존 밖, 백 키가 앱을 통째로 종료, safe area 무시, 입력 확대 | L6 시나리오(백·시트·터치타깃 스윕) + `references/mobile-app-ux.md` 표준 — **OS 관습은 조사 없이 바꾸지 않는다** |
| 탭 전환 스크롤 잔류 — 짧은 뷰에서 클램프되어 헤더 반쯤 잘림, 뷰마다 크롬 위치 제각각 | 뷰 전환 `scrollTo(0,0)` 단언(M5) + mobile-app-ux.md |
| 크럼을 헤더로 내보내 빈약한 헤더 — 계측 전부 통과했지만 사용자에게 '깨져 보인다' | 헤더 sticky·브랜드+액션 구조 단언 + **시각 무게 평가(눈)** — mobile-app-ux.md |
| 왼쪽 인셋 리듬 불일치(제목 16 vs 앱바·카드 24) — 오버플로 검사는 오른쪽만 보므로 통과 | M6 왼쪽 인셋 스위트(텍스트 시작점 ≥12px·같은 축 ±10px) + preflight §0-E |
## 7계층
@ -83,6 +86,9 @@ L1·L5 는 프로젝트 안에 `tools/unit/*.test.mjs`, `tools/exploratory.mjs`
12. **페이지→노드 직렬화는 NaN 방어** — `getComputedStyle(el).fontSize` 등을 문자열 보간하면 "16px"이 NaN이 되고, 하니스는 NaN을 `null`로 받아 `"////px"` 같은 고장난 상세를 리포트한다. 수치는 항상 `parseFloat(x) || 0` 로 감싼다.
13. **`position: fixed` 요소의 보임 판정은 offsetParent 로 하지 마라** — fixed 는 offsetParent 가 `null`이다. display 계산값·높이로 판정한다.
14. **실기기(에뮬레이터) 검증은 텍스트 근거로** — `uiautomator dump` 의 `text="…"`에서 화면 제목·활성 탭을 읽어 단언한다. 스크린샷+비전은 "백 직후 전환 중 프레임"을 잡는 타이밍 오탐이 있다(실측 사고). 물리 폰이 붙어 있으면 `adb -s <emulator>` 로 대상을 한정한다.
15. **시각 기준 갱신은 의도 변경 리뷰를 동반한다** — "달라졌으니 갱신"으로 결함이 기준에 박힌다(실측: 왼쪽 인셋 결함 화면이 기준 갱신으로 '정상'이 되어 재발을 숨겼다). 갱신 전에 무엇이 의도적으로 바뀌었는지 한 줄씩 나열하고, 사용자 보고가 있던 화면은 수리 후 재촬영한다.
16. **사용자 보고 스크린샷은 비전 요약이 아니라 픽셀 판정으로 해부한다** — 비전 모델의 픽셀 추정("~0px", "~16px")은 부정확하다. pngjs 등으로 화면을 가로 밴드로 나눠 각 밴드의 텍스트 시작 x를 직접 재면(보임 행만, 왼쪽 테두리 아티팩트 제외) "무엇이 어디서 어긋났는지"가 수치로 나온다. 실측: '제목이 가장자리에 붙는다'는 보고가 앱바 24px·제목 16px의 리듬 불일치로 확정됐다.
17. **새 검사는 수정 전 코드로 RED 재현 후 GREEN** — 검사 자체의 측정 하자(오탐)도 이렇게 잡는다. 실측: 왼쪽 인셋 검사 초안이 ①`display:none` 요소의 rect=0, ②전폭 컨테이너의 보더박스(텍스트가 아님)로 38개 오탐 — RED 단계에서 발견해 교정했다.
## 리포트 양식

View file

@ -42,6 +42,12 @@ canvas { display: none !important; }
- 토큰 파일이 있는데 다른 CSS 파일에 색 리터럴(hex·rgb)이 남아 있으면 실패다. 반지름 리터럴도 마찬가지 — 눈 검사로는 절대 전부 못 잡는다(실측: 8건의 상수색이 눈 검사를 통과했다)
- `theme-color` 메타가 실제 배경색과 다르면 **낡은 토큰**이다. 프리셋 전환에서 브랜드 표면(theme-color·파비콘·og:image)이 누락됐다는 뜻이다
### E. 왼쪽 인셋 리듬 (모바일)
오버플로 검사는 오른쪽만 본다 — **왼쪽이 붙어 깨지는 결함은 통과한다**(실측: 제목 16px·앱바·카드 24px 혼재가 게이트를 통과해 사용자 보고로 나왔다). 모바일 폭(320·390)에서:
- 화면의 모든 보이는 텍스트 시작점 ≥ 12px
- 헤더 텍스트·화면 제목·카드 텍스트가 **같은 축**(±10px)
- 측정은 전폭 컨테이너의 보더박스가 아니라 **텍스트 시작점**, `display:none` 요소(rect=0)는 제외하고
---
## 1. 접근성 — 타협 없음

View file

@ -31,8 +31,8 @@ const DEFAULT_CFG = {
],
widths: [320, 375, 390, 768, 1024, 1440],
fontScales: [1, 1.3],
thresholds: { contrastNormal: 4.5, contrastLarge: 3, visualDiffPct: 0.1, stackLines: 3, radiusLiterals: 3, tapTargetMin: 44, inlineTargetMin: 24 },
checks: { meta: true, contrast: true, stack: true, wrap: true, rhythm: true, scaleMatrix: true, tapTargets: true, static: false, visual: false, webkit: false, deadCss: true, cssHygiene: true },
thresholds: { contrastNormal: 4.5, contrastLarge: 3, visualDiffPct: 0.1, stackLines: 3, radiusLiterals: 3, tapTargetMin: 44, inlineTargetMin: 24, leftInsetMin: 12 },
checks: { meta: true, contrast: true, stack: true, wrap: true, rhythm: true, scaleMatrix: true, tapTargets: true, leftInset: true, static: false, visual: false, webkit: false, deadCss: true, cssHygiene: true },
deadCssIgnore: ["is-"], // 동적으로 조립되는 클래스 접두사(is-${type} 등). 오탐 방지 — 프로젝트마다 추가한다
chromePath: null,
};
@ -382,7 +382,12 @@ for (const p of CFG.pages) {
await page.goto(href, { waitUntil: "networkidle0" });
await page.evaluate(UTILS);
if (p.brand) {
const bl = await page.evaluate(`__lines(${JSON.stringify(p.brand)})`);
// 동일 셀렉터가 여러 개(사이드바+탑바 등)면 보이는 것을 잰다 — display:none 의 rect 는 0
const bl = await page.evaluate((sel) => {
const els = [...document.querySelectorAll(sel)];
const el = els.find((e) => e.offsetParent !== null || getComputedStyle(e).position === "fixed") || els[0];
return window.__lines(el);
}, p.brand);
bl === 1 ? pass(`${p.name} 브랜드 1줄`) : fail(`${p.name} 브랜드 ${bl}줄(수축 의심)`);
}
}
@ -415,6 +420,40 @@ for (const p of CFG.pages) {
bad.length ? fail(`${p.name}/${v} 터치타깃`, bad.join(" · ")) : pass(`${p.name}/${v} 터치타깃`, `컨트롤 ≥${CFG.thresholds.tapTargetMin}px·인라인 ≥${CFG.thresholds.inlineTargetMin}px`);
}
} else skip("터치타깃", "checks.tapTargets=false");
// ── 왼쪽 인셋(모바일) — 오버플로 검사는 오른쪽만 본다.
// 실측 사고: 제목 16px vs 앱바·카드 24px 혼재가 모든 게이트를 통과했고
// 사용자에게 '가장자리에 붙어 깨졌다'고 보고됐다. 텍스트 노드 Range 로 글자 시작점을 잰다
// (전폭 컨테이너의 보더박스가 아니라. display:none 의 rect=0 은 보임 검사로 제외). ──
if (CFG.checks.leftInset) {
await page.setViewport({ width: 390, height: 844, isMobile: true, hasTouch: true });
await page.goto(href, { waitUntil: "networkidle0" });
for (const v of p.views || ["single"]) {
await goView(v);
const inset = await page.evaluate(() => {
let min = null;
const walk = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
let n;
while ((n = walk.nextNode())) {
if (!n.textContent.trim() || !n.parentElement) continue;
const cs = getComputedStyle(n.parentElement);
if (cs.display === "none" || cs.visibility === "hidden" || cs.position === "fixed") continue;
const range = document.createRange();
range.selectNodeContents(n);
const r = range.getBoundingClientRect();
if (r.width < 2 || r.height < 4 || r.left < 0) continue;
const l = Math.round(r.left);
if (min === null || l < min) min = l;
}
return min;
});
inset === null
? skip(`${p.name}/${v} 왼쪽 인셋`, "텍스트 없음")
: inset >= CFG.thresholds.leftInsetMin
? pass(`${p.name}/${v} 왼쪽 인셋`, `최소 텍스트 시작 ${inset}px`)
: fail(`${p.name}/${v} 왼쪽 인셋`, `텍스트가 ${inset}px에서 시작 — 기준 ≥${CFG.thresholds.leftInsetMin}px(가장자리에 붙어 보인다)`);
}
} else skip("왼쪽 인셋", "checks.leftInset=false");
}
// ── L3 시각 회귀 (옵션: pixelmatch·pngjs 설치 시) ──