diff --git a/apps/site/gate-report.md b/apps/site/gate-report.md index bd4f2f9..decc570 100644 --- a/apps/site/gate-report.md +++ b/apps/site/gate-report.md @@ -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 ``` diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index c8ecc06..0531e05 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -1,5 +1,7 @@ # @designpaca/core +## 0.8.0 + ## 0.7.3 ## 0.7.2 diff --git a/packages/core/package.json b/packages/core/package.json index 5733c85..11f08df 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@designpaca/core", - "version": "0.7.3", + "version": "0.8.0", "private": true, "description": "designpaca 설치 엔진 — 타깃 어댑터, 매니페스트, 드리프트 감지", "type": "module", diff --git a/packages/skill/SKILL.md b/packages/skill/SKILL.md index a683681..f394174 100644 --- a/packages/skill/SKILL.md +++ b/packages/skill/SKILL.md @@ -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은 살린다. diff --git a/packages/skill/references/audit-gate.md b/packages/skill/references/audit-gate.md index 5a58182..9f75103 100644 --- a/packages/skill/references/audit-gate.md +++ b/packages/skill/references/audit-gate.md @@ -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 ` 로 대상을 한정한다. +15. **시각 기준 갱신은 의도 변경 리뷰를 동반한다** — "달라졌으니 갱신"으로 결함이 기준에 박힌다(실측: 왼쪽 인셋 결함 화면이 기준 갱신으로 '정상'이 되어 재발을 숨겼다). 갱신 전에 무엇이 의도적으로 바뀌었는지 한 줄씩 나열하고, 사용자 보고가 있던 화면은 수리 후 재촬영한다. +16. **사용자 보고 스크린샷은 비전 요약이 아니라 픽셀 판정으로 해부한다** — 비전 모델의 픽셀 추정("~0px", "~16px")은 부정확하다. pngjs 등으로 화면을 가로 밴드로 나눠 각 밴드의 텍스트 시작 x를 직접 재면(보임 행만, 왼쪽 테두리 아티팩트 제외) "무엇이 어디서 어긋났는지"가 수치로 나온다. 실측: '제목이 가장자리에 붙는다'는 보고가 앱바 24px·제목 16px의 리듬 불일치로 확정됐다. +17. **새 검사는 수정 전 코드로 RED 재현 후 GREEN** — 검사 자체의 측정 하자(오탐)도 이렇게 잡는다. 실측: 왼쪽 인셋 검사 초안이 ①`display:none` 요소의 rect=0, ②전폭 컨테이너의 보더박스(텍스트가 아님)로 38개 오탐 — RED 단계에서 발견해 교정했다. ## 리포트 양식 diff --git a/packages/skill/references/preflight.md b/packages/skill/references/preflight.md index 0e43763..521d9fa 100644 --- a/packages/skill/references/preflight.md +++ b/packages/skill/references/preflight.md @@ -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. 접근성 — 타협 없음 diff --git a/packages/skill/tools/design-gate.mjs b/packages/skill/tools/design-gate.mjs index 5f290ce..8f8ec0f 100644 --- a/packages/skill/tools/design-gate.mjs +++ b/packages/skill/tools/design-gate.mjs @@ -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 설치 시) ──