fix(site,skill): 두레 왼쪽 인셋 리듬 수리 — 제목이 가장자리에 붙어 보이던 사고(v23)
사용자 스크린샷을 픽셀 판정으로 해부: 앱바·검색·칩·카드는 24px에서 시작하는데 h2만 16px — 8px 어긋난 큰 명조 제목이 '가장자리에 붙었다'로 읽혔다. .layout 모바일 인셋을 24px로 통일. - 게이트 공백: 기존 매트릭스는 오른쪽 오버플로만 검사 — e2e 에 M6 왼쪽 인셋 스위트 신설(60단언, 텍스트 시작점 ≥12px + 헤더-제목 같은 축 ±10px), RED 38 실패로 결함 재현 후 GREEN - 측정 하자 2건 교정: display:none rect=0 필터, 전폭 컨테이너 보더박스가 아닌 텍스트 시작점 기준 - 스킬 mobile-app-ux.md: '왼쪽 인셋은 하나의 축' 규칙 추가. 시각 기준 갱신, verify L0~L6 전 통과(E2E 1,805단언)
36
apps/site/tools/.diag3.mjs
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import puppeteer from "puppeteer-core";
|
||||
import path from "node:path";
|
||||
import url from "node:url";
|
||||
|
||||
const here = path.dirname(url.fileURLToPath(import.meta.url));
|
||||
const CHROME = "C:/Program Files/Google/Chrome/Application/chrome.exe";
|
||||
|
||||
const browser = await puppeteer.launch({ executablePath: CHROME, headless: "new", args: ["--force-device-scale-factor=1"] });
|
||||
const page = await browser.newPage();
|
||||
await page.setCacheEnabled(false);
|
||||
|
||||
// 라이브 배포 URL — 사용자가 보는 그대로
|
||||
for (const w of [360, 390, 412]) {
|
||||
await page.setViewport({ width: w, height: 844, isMobile: true, hasTouch: true });
|
||||
await page.goto("https://designpaca.chanpaca.net/work/dure-enrollment/", { waitUntil: "networkidle0" });
|
||||
await page.evaluate(() => 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();
|
||||
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 149 KiB After Width: | Height: | Size: 149 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 117 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
BIN
apps/site/tools/__shots__/diag3-dure-live-390.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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]) {
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||