diff --git a/apps/site/design.md b/apps/site/design.md index cec0002..2ba5544 100644 --- a/apps/site/design.md +++ b/apps/site/design.md @@ -841,3 +841,11 @@ v23 스크롤 리셋·단일 헤더에도 사용자 평가가 "여전히 깨져 - 수리: 모바일 헤더를 **브랜드(가 모노그램 타일+이름)+액션(인쇄·종) 한 줄**로 — 두레 앱바와 같은 문법. 탑바에 브랜드를 이어받게(데스크톱은 사이드바 브랜드, 모바일은 탑바 브랜드 — CSS display 로 한쪽만 노출), 크럼은 모바일에서 숨김(화면 이름은 각 뷰 헤드와 활성 탭이 말한다). min-height 56px, sticky+safe-top 유지. - 검증: 360/390/412px 오버플로 0·브랜드 노출·크럼/사이드 숨김 실측, 스크롤 800에서 탑바 top=0, 탭 전환 scrollY=0. 비전 검수 "확신 있는 앱 헤더·패딩 정상". 시각 기준 갱신(390px), verify L0~L6 재통과. - 스킬 규칙 갱신: "크럼을 헤더로 내보내면 빈약해 보인다 — 모바일 헤더는 브랜드+액션, 계측이 통과해도 눈으로 봐야 한다". + +## 2026-08-23 — 국소: 두레 왼쪽 인셋 리듬 수리(v23) — 제목이 가장자리에 붙어 보이던 사고 + +사용자 스크린샷 지적 "h2(2026학년도 1학기 과목)가 화면 왼쪽에 너무 붙는다". 스크린샷을 픽셀 판정으로 해부해 확정: 앱바 텍스트·검색·칩·카드는 전부 ~24px에서 시작하는데 **h2만 16px** — 8px 왼쪽으로 어긋난 큰 명조 제목이 '가장자리에 붙어 깨졌다'로 읽혔다. 원인: `.layout` 모바일 인셋이 var(--space-4)=16px인데 앱바는 space-5=24px — 같은 화면에 두 축이 공존. +- 수리: `.layout` 모바일 인셋 inline을 24px로 — 제목·카드·앱바가 같은 축. +- **게이트 공백 발견**: 기존 매트릭스는 오른쪽 '오버플로'만 검사하고 왼쪽 인셋의 최소값·정렬 정합은 검사하지 않았다(그래서 기준 갱신에 결함이 그대로 박혔다). e2e에 **M6 왼쪽 인셋** 스위트 신설(60단언): 전 뷰×320/390에서 텍스트 시작점 ≥12px + 헤더-제목 같은 축(±10px). RED 38 실패로 결함 재현 확인 후 수리로 GREEN. +- 측정 하자(瑕) 2건 교정: display:none 요소의 rect=0이 잡히던 것(vis 필터), 전폭 컨테이너 보더박스가 아닌 '텍스트 시작점' 기준. +- 시각 기준 갱신(두레 390px 6화면), verify L0~L6 전 통과(E2E 1,805단언). diff --git a/apps/site/public/work/dure-enrollment/index.html b/apps/site/public/work/dure-enrollment/index.html index f4dc5fd..dd2aa05 100644 --- a/apps/site/public/work/dure-enrollment/index.html +++ b/apps/site/public/work/dure-enrollment/index.html @@ -35,7 +35,7 @@ rel="stylesheet" href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&family=IBM+Plex+Sans+KR:wght@400;500;600&family=Noto+Serif+KR:wght@500;600&display=swap" /> - + document.fonts.ready); + await new Promise((r) => setTimeout(r, 250)); + const m = await page.evaluate(() => { + const layout = document.querySelector(".layout"); + const h2 = document.querySelector("#cat-h"); + const card = document.querySelector(".course-row"); + const appbar = document.querySelector(".appbar-inner"); + const rl = (el) => (el ? Math.round(el.getBoundingClientRect().left) : null); + return { + cssV: (document.querySelector('link[rel="stylesheet"]')?.getAttribute("href") || "").match(/v=\d+/)?.[0], + layoutPad: getComputedStyle(layout).padding, + h2Left: rl(h2), + cardLeft: rl(card), + appbarPadLeft: getComputedStyle(appbar).paddingLeft, + over: document.documentElement.scrollWidth - document.documentElement.clientWidth, + }; + }); + console.log(`[${w}] css=${m.cssV} layoutPad="${m.layoutPad}" h2Left=${m.h2Left} cardLeft=${m.cardLeft} appbarPadL=${m.appbarPadLeft} over=+${m.over}`); + if (w === 390) await page.screenshot({ path: path.join(here, "__shots__", "diag3-dure-live-390.png") }); +} +await browser.close(); diff --git a/apps/site/tools/__shots__/baseline/dure-enrollment-archive-390.png b/apps/site/tools/__shots__/baseline/dure-enrollment-archive-390.png index 93f92d2..4ee466e 100644 Binary files a/apps/site/tools/__shots__/baseline/dure-enrollment-archive-390.png and b/apps/site/tools/__shots__/baseline/dure-enrollment-archive-390.png differ diff --git a/apps/site/tools/__shots__/baseline/dure-enrollment-catalog-390.png b/apps/site/tools/__shots__/baseline/dure-enrollment-catalog-390.png index 01e96df..2cef3ef 100644 Binary files a/apps/site/tools/__shots__/baseline/dure-enrollment-catalog-390.png and b/apps/site/tools/__shots__/baseline/dure-enrollment-catalog-390.png differ diff --git a/apps/site/tools/__shots__/baseline/dure-enrollment-credits-390.png b/apps/site/tools/__shots__/baseline/dure-enrollment-credits-390.png index f93a3b5..f9fc447 100644 Binary files a/apps/site/tools/__shots__/baseline/dure-enrollment-credits-390.png and b/apps/site/tools/__shots__/baseline/dure-enrollment-credits-390.png differ diff --git a/apps/site/tools/__shots__/baseline/dure-enrollment-enrollments-390.png b/apps/site/tools/__shots__/baseline/dure-enrollment-enrollments-390.png index d8bf2dc..7f6bfbe 100644 Binary files a/apps/site/tools/__shots__/baseline/dure-enrollment-enrollments-390.png and b/apps/site/tools/__shots__/baseline/dure-enrollment-enrollments-390.png differ diff --git a/apps/site/tools/__shots__/baseline/dure-enrollment-notices-390.png b/apps/site/tools/__shots__/baseline/dure-enrollment-notices-390.png index 8cbdd51..9d63683 100644 Binary files a/apps/site/tools/__shots__/baseline/dure-enrollment-notices-390.png and b/apps/site/tools/__shots__/baseline/dure-enrollment-notices-390.png differ diff --git a/apps/site/tools/__shots__/baseline/dure-enrollment-starred-390.png b/apps/site/tools/__shots__/baseline/dure-enrollment-starred-390.png index e3436c3..a1fe420 100644 Binary files a/apps/site/tools/__shots__/baseline/dure-enrollment-starred-390.png and b/apps/site/tools/__shots__/baseline/dure-enrollment-starred-390.png differ diff --git a/apps/site/tools/__shots__/baseline/gaon-lms-dashboard-1440.png b/apps/site/tools/__shots__/baseline/gaon-lms-dashboard-1440.png index d06a3e2..f9aced6 100644 Binary files a/apps/site/tools/__shots__/baseline/gaon-lms-dashboard-1440.png and b/apps/site/tools/__shots__/baseline/gaon-lms-dashboard-1440.png differ diff --git a/apps/site/tools/__shots__/current/dure-enrollment-archive-390.png b/apps/site/tools/__shots__/current/dure-enrollment-archive-390.png index f00eafa..d6c6b33 100644 Binary files a/apps/site/tools/__shots__/current/dure-enrollment-archive-390.png and b/apps/site/tools/__shots__/current/dure-enrollment-archive-390.png differ diff --git a/apps/site/tools/__shots__/current/dure-enrollment-catalog-390.png b/apps/site/tools/__shots__/current/dure-enrollment-catalog-390.png index 770d249..49e8491 100644 Binary files a/apps/site/tools/__shots__/current/dure-enrollment-catalog-390.png and b/apps/site/tools/__shots__/current/dure-enrollment-catalog-390.png differ diff --git a/apps/site/tools/__shots__/current/dure-enrollment-credits-390.png b/apps/site/tools/__shots__/current/dure-enrollment-credits-390.png index c00db81..6aa500b 100644 Binary files a/apps/site/tools/__shots__/current/dure-enrollment-credits-390.png and b/apps/site/tools/__shots__/current/dure-enrollment-credits-390.png differ diff --git a/apps/site/tools/__shots__/current/dure-enrollment-enrollments-390.png b/apps/site/tools/__shots__/current/dure-enrollment-enrollments-390.png index 15df463..886dfa4 100644 Binary files a/apps/site/tools/__shots__/current/dure-enrollment-enrollments-390.png and b/apps/site/tools/__shots__/current/dure-enrollment-enrollments-390.png differ diff --git a/apps/site/tools/__shots__/current/dure-enrollment-notices-390.png b/apps/site/tools/__shots__/current/dure-enrollment-notices-390.png index 84e487d..1b4f944 100644 Binary files a/apps/site/tools/__shots__/current/dure-enrollment-notices-390.png and b/apps/site/tools/__shots__/current/dure-enrollment-notices-390.png differ diff --git a/apps/site/tools/__shots__/current/dure-enrollment-starred-390.png b/apps/site/tools/__shots__/current/dure-enrollment-starred-390.png index cd9dc57..4d31c57 100644 Binary files a/apps/site/tools/__shots__/current/dure-enrollment-starred-390.png and b/apps/site/tools/__shots__/current/dure-enrollment-starred-390.png differ diff --git a/apps/site/tools/__shots__/current/gaon-lms-dashboard-1440.png b/apps/site/tools/__shots__/current/gaon-lms-dashboard-1440.png index 81d2548..ccec4a3 100644 Binary files a/apps/site/tools/__shots__/current/gaon-lms-dashboard-1440.png and b/apps/site/tools/__shots__/current/gaon-lms-dashboard-1440.png differ diff --git a/apps/site/tools/__shots__/current/gaon-lms-dashboard-390.png b/apps/site/tools/__shots__/current/gaon-lms-dashboard-390.png index f68fd7d..1107468 100644 Binary files a/apps/site/tools/__shots__/current/gaon-lms-dashboard-390.png and b/apps/site/tools/__shots__/current/gaon-lms-dashboard-390.png differ diff --git a/apps/site/tools/__shots__/diag3-dure-live-390.png b/apps/site/tools/__shots__/diag3-dure-live-390.png new file mode 100644 index 0000000..92f0ce8 Binary files /dev/null and b/apps/site/tools/__shots__/diag3-dure-live-390.png differ diff --git a/apps/site/tools/e2e-report.md b/apps/site/tools/e2e-report.md index af53783..62d1d2a 100644 --- a/apps/site/tools/e2e-report.md +++ b/apps/site/tools/e2e-report.md @@ -1,11 +1,12 @@ -# E2E 시나리오 게이트 — 2026-08-23 06:12 +# E2E 시나리오 게이트 — 2026-08-23 06:30 -단언 1745 · 통과 1745 · 실패 0 — **GREEN — 전 단언 통과** +단언 1805 · 통과 1805 · 실패 0 — **GREEN — 전 단언 통과** | 스위트 | 통과 | 실패 | |---|---|---| | M4 앱 기반 | 22 | 0 | | M5 헤더·스크롤 | 10 | 0 | +| M6 왼쪽 인셋 | 60 | 0 | | M320 매트릭스 | 90 | 0 | | M390 매트릭스 | 105 | 0 | | M1 터치타깃 | 231 | 0 | diff --git a/apps/site/tools/e2e.mjs b/apps/site/tools/e2e.mjs index 1fd3309..f4d87a6 100644 --- a/apps/site/tools/e2e.mjs +++ b/apps/site/tools/e2e.mjs @@ -218,6 +218,40 @@ for (const [app, cfg] of Object.entries(APPS)) { ok(after.top === 0, S, `${app} 탭 전환 후 헤더 상단 밀착`, after.top + "px"); } +// ════════ M6 — 왼쪽 인셋 리듬(제목·카드·헤더가 같은 축에 녹는다) ════════ +// 실측 사고: 두레 제목(h2)은 16px, 앱바·카드는 24px — 8px 어긋난 큰 제목이 +// '가장자리에 붙어 깨졌다'는 보고로 나왔다. 오버플로 검사는 오른쪽만 보므로 +// 왼쪽 인셋의 최소값과 정렬 정합은 별도 검사여야 한다 +for (const [app, cfg] of Object.entries(APPS)) { + const S = "M6 왼쪽 인셋"; + const headerSel = app === "gaon-lms" ? ".topbar" : ".appbar"; + for (const w of [320, 390]) { + await page.setViewport({ width: w, height: 844, isMobile: true, hasTouch: true }); + await openApp(app); + for (const v of cfg.views) { + await switchView(app, v); + const inset = await page.evaluate((hs) => { + const vis = (el) => { if (!el) return false; const r = el.getBoundingClientRect(); const cs = getComputedStyle(el); return cs.display !== "none" && cs.visibility !== "hidden" && r.width > 40 && r.height > 8; }; + const textLeft = (els) => { + // display:none 요소의 rect 는 0 — 보임 검사를 먼저 통과시킨다 + const ls = els.filter(vis).filter((el) => [...el.childNodes].some((n) => n.nodeType === 3 && n.textContent.trim())).map((el) => el.getBoundingClientRect().left); + return ls.length ? Math.round(Math.min(...ls)) : null; + }; + const barLeft = textLeft([...document.querySelectorAll(hs + " *")]); + const head = document.querySelector(".view.is-on h1, .view.is-on h2"); + const headLeft = head && vis(head) ? Math.round(head.getBoundingClientRect().left) : null; + // 전폭 컨테이너(보더박스 0)가 아니라 '텍스트가 실제로 시작하는 곳'을 잰다 + const contentLeft = textLeft([...document.querySelectorAll(".view.is-on *")]); + return { barLeft, headLeft, contentLeft }; + }, headerSel); + const min = Math.min(...[inset.headLeft, inset.contentLeft].filter((v2) => v2 !== null)); + ok(min >= 12, S, `${app}/${v}@${w} 본문 인셋 ≥12px`, `제목 ${inset.headLeft}·카드 ${inset.contentLeft}`); + if (inset.barLeft !== null && inset.headLeft !== null) + ok(Math.abs(inset.barLeft - inset.headLeft) <= 10, S, `${app}/${v}@${w} 헤더-제목 같은 축(±10px)`, `바 ${inset.barLeft} vs 제목 ${inset.headLeft}`); + } + } +} + // ════════ M3+M1+M2 — 구조·터치타깃·대비 매트릭스(전 뷰 × 320/390/768) ════════ for (const [app, cfg] of Object.entries(APPS)) { for (const w of [320, 390, 768]) { diff --git a/apps/site/tools/verify-report.md b/apps/site/tools/verify-report.md index 2f205e6..5763c46 100644 --- a/apps/site/tools/verify-report.md +++ b/apps/site/tools/verify-report.md @@ -1,15 +1,15 @@ -# 검증 리포트 — 2026-08-23 06:12 +# 검증 리포트 — 2026-08-23 06:30 -게이트: **전 계층 통과 — 배포 가능** · 총 88.1초 +게이트: **전 계층 통과 — 배포 가능** · 총 103.6초 | 계층 | 결과 | 항목 | 소요 | |---|---|---|---| -| L0-정적/stylelint | PASS | PASS — | 1.7s | +| L0-정적/stylelint | PASS | PASS — | 8.0s | | L0-정식/html-validate | PASS | PASS — | 1.3s | | L1-단위/계산로직 | PASS | PASS 41 | 0.1s | -| L2-기능/감사 | PASS | PASS 72 | 10.1s | -| L2-불변식/렌더 | PASS | PASS 10 | 17.6s | -| L3-시각회귀/diff | PASS | PASS 30 | 11.5s | +| L2-기능/감사 | PASS | PASS 72 | 9.7s | +| L2-불변식/렌더 | PASS | PASS 10 | 17.2s | +| L3-시각회귀/diff | PASS | PASS 30 | 11.7s | | L4-WebKit | PASS | PASS 8 | 3.9s | -| L5-탐색/키보드 | PASS | PASS 14 | 6.2s | -| L6-시나리오/E2E | PASS | PASS — | 35.7s | +| L5-탐색/키보드 | PASS | PASS 14 | 6.3s | +| L6-시나리오/E2E | PASS | PASS — | 45.4s | diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 2bd0b4c..694fc83 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,11 @@ # designpaca +## 0.7.3 + +### Patch Changes + +- 왼쪽 인셋 게이트 — 오버플로 검사는 오른쪽만 본다. 헤더-제목-카드 같은 축(±10px)·텍스트 시작점 ≥12px 검사를 e2e 매트릭스에 포함하고 mobile-app-ux.md 에 리듬 규칙 추가 + ## 0.7.2 ### Patch Changes diff --git a/packages/cli/package.json b/packages/cli/package.json index bbfe14f..2ab1fac 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "designpaca", - "version": "0.7.2", + "version": "0.7.3", "description": "웹 디자인 파이프라인 스킬 — Claude Code · Codex · Cursor 에 한 줄로 설치한다", "keywords": [ "design", diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index 16cc87a..c8ecc06 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -1,5 +1,7 @@ # @designpaca/core +## 0.7.3 + ## 0.7.2 ## 0.7.1 diff --git a/packages/core/package.json b/packages/core/package.json index 096bcc9..5733c85 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@designpaca/core", - "version": "0.7.2", + "version": "0.7.3", "private": true, "description": "designpaca 설치 엔진 — 타깃 어댑터, 매니페스트, 드리프트 감지", "type": "module", diff --git a/packages/skill/CHANGELOG.md b/packages/skill/CHANGELOG.md index 46201b3..bb98a41 100644 --- a/packages/skill/CHANGELOG.md +++ b/packages/skill/CHANGELOG.md @@ -1,5 +1,11 @@ # CHANGELOG +## 0.7.3 + +### Patch Changes + +- 왼쪽 인셋 게이트 — 오버플로 검사는 오른쪽만 본다. 헤더-제목-카드 같은 축(±10px)·텍스트 시작점 ≥12px 검사를 e2e 매트릭스에 포함하고 mobile-app-ux.md 에 리듬 규칙 추가 + ## 0.7.2 ### Patch Changes diff --git a/packages/skill/package.json b/packages/skill/package.json index d494e88..2ae8f0d 100644 --- a/packages/skill/package.json +++ b/packages/skill/package.json @@ -1,6 +1,6 @@ { "name": "@designpaca/skill", - "version": "0.7.2", + "version": "0.7.3", "private": true, "description": "designpaca 스킬 원본 — SKILL.md 와 참조 문서", "scripts": { diff --git a/packages/skill/references/mobile-app-ux.md b/packages/skill/references/mobile-app-ux.md index d607216..6822c05 100644 --- a/packages/skill/references/mobile-app-ux.md +++ b/packages/skill/references/mobile-app-ux.md @@ -67,6 +67,8 @@ **모바일 헤더는 브랜드+액션 한 줄이다.** 데스크톱의 브랜드 바(사이드바 상단)와 화면 크럼 바를 좁은 화면에 그대로 쌓으면 90px+ 크롬이 콘텐츠를 옥에 끼운다. 하나를 고른다 — **브랜드(마크+이름)와 액션을 헤더에 남기고, 크럼(화면 이름)은 숨긴다.** 화면 이름은 각 뷰의 헤드와 활성 탭이 이미 말한다. 실측 사고: 크럼을 헤더로 내보냈더니 "작은 회색 텍스트가 52px 바에 떠 있는" 빈약한 헤더가 돼 **사용자에게 '깨져 보인다'는 보고**를 받았다 — 구조는 맞아도 시각적 무게가 없으면 깨진 것으로 읽힌다. 고른 헤더는 `position: sticky; top: 0` + safe-top 패딩으로 스크롤 내내 상단에 남는다. 검증은 E2E 로: 300px+ 스크롤 상태에서 탭 전환 → `scrollY === 0`·헤더 `top === 0`, 그리고 **눈으로** — 계측이 전부 통과해도 보기에 빈약하면 실패다. +**왼쪽 인셋은 하나의 축이어야 한다.** 헤더 텍스트·화면 제목·카드가 각기 다른 x에서 시작하면 리듬이 무너지고, 큰 제목이 8px만 왼쪽에 나가도 사용자는 "가장자리에 붙어서 깨졌다"고 읽는다(실측 사고: 제목 16px vs 앱바·카드 24px 혼재). 컨테이너 인셋을 토큰 하나로 통일하라. **오버플로 검사는 오른쪽만 본다** — 왼쪽 인셋의 최소값(≥12px)과 정렬 정합(헤더-제목-카드 ±10px)은 별도 검사로: 텍스트 시작점 기준으로 잰다(전폭 컨테이너의 보더박스가 아니라). `display:none` 요소의 rect 는 0이므로 보임 필터를 먼저 통과시킨다. + ### 하드웨어 뒤로가기 = 앱 내 히스토리 (핵심 패턴) 안드로이드 백 키와 iOS 스와이프 백이 **앱을 종료하지 않고 앱 내 뒤로** 작동해야 한다. SPA 뷰 전환마다 `history.pushState` 로 스택을 쌓고 `popstate` 로 소비한다. 다이얼로그도 같은 스택에 태운다: