feat: strengthen design skill and refresh all showcases
Some checks failed
ci / build (push) Has been cancelled

This commit is contained in:
Yun Chan 2026-09-12 18:16:33 +09:00
parent 69e5cc1efd
commit e09d4efb1c
257 changed files with 8196 additions and 2374 deletions

View file

@ -21,6 +21,16 @@ const jclick = (sel) => page.evaluate((s) => { const el = document.querySelector
const over = () => page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth);
const gotoFile = async (app) =>
page.goto(`file:///${path.join(root, "public/work", app, "index.html")}`, { waitUntil: "networkidle0" });
const gotoWorkFile = async (...parts) =>
page.goto(`file:///${path.join(root, "public/work", ...parts)}`, { waitUntil: "networkidle0" });
const centeredDialog = (selector) => page.$eval(selector, (dialog) => {
const rect = dialog.getBoundingClientRect();
return {
deltaX: Math.abs(rect.left + rect.width / 2 - innerWidth / 2),
deltaY: Math.abs(rect.top + rect.height / 2 - innerHeight / 2),
inViewport: rect.left >= 0 && rect.top >= 0 && rect.right <= innerWidth && rect.bottom <= innerHeight,
};
});
// ══════════ 가온 학적부 (8 뷰) ══════════
await gotoFile("gaon-lms");
@ -253,6 +263,103 @@ for (const w of [320, 390]) {
pass(`두레 ${w}px`, od <= 0, `+${od}px`);
}
// ── 사용자 보고 회귀: 팝업·모달은 좌상단에 붙거나 화면 밖으로 잘리면 안 된다 ──
await page.setViewport({ width: 390, height: 320 });
await gotoFile("gaon-lms");
await jclick("#bell");
const bellRect = await page.$eval("#bell-menu", (menu) => {
const r = menu.getBoundingClientRect();
return { left: r.left, top: r.top, right: r.right, bottom: r.bottom, clientHeight: menu.clientHeight, scrollHeight: menu.scrollHeight };
});
pass("가온 짧은 화면 알림판 뷰포트·스크롤", bellRect.left >= 0 && bellRect.top >= 0 && bellRect.right <= 390 && bellRect.bottom <= 320 && bellRect.clientHeight > 0 && bellRect.scrollHeight >= bellRect.clientHeight, JSON.stringify(bellRect));
for (const viewport of [{ width: 390, height: 844 }, { width: 1440, height: 900 }, { width: 2560, height: 900 }]) {
await page.setViewport(viewport);
await gotoFile("materia-interior");
await jclick('[onclick^="openProjectModal"]');
const projectPosition = await centeredDialog("#projectModal");
pass(`마테리아 프로젝트 모달 중앙 ${viewport.width}px`, projectPosition.inViewport && projectPosition.deltaX <= 1 && projectPosition.deltaY <= 1, JSON.stringify(projectPosition));
const projectScrollLock = await page.$eval("html", (root) => getComputedStyle(root).overflow);
pass(`마테리아 프로젝트 모달 배경 잠금 ${viewport.width}px`, projectScrollLock === "hidden", projectScrollLock);
await page.keyboard.press("Escape");
await gotoWorkFile("materia-interior", "projects", "hannam-duplex", "index.html");
await page.waitForSelector(".gallery-card-action");
await jclick(".gallery-card-action");
const lightboxPosition = await centeredDialog("#lightboxDialog");
pass(`마테리아 갤러리 모달 중앙 ${viewport.width}px`, lightboxPosition.inViewport && lightboxPosition.deltaX <= 1 && lightboxPosition.deltaY <= 1, JSON.stringify(lightboxPosition));
const lightboxScrollLock = await page.$eval("html", (root) => getComputedStyle(root).overflow);
pass(`마테리아 갤러리 모달 배경 잠금 ${viewport.width}px`, lightboxScrollLock === "hidden", lightboxScrollLock);
await page.keyboard.press("Escape");
}
await page.setViewport({ width: 390, height: 320 });
await gotoFile("hannam-jari");
await jclick(".menu-button");
const mobileNav = await page.$eval(".site-nav", (nav) => {
const r = nav.getBoundingClientRect();
return { top: r.top, bottom: r.bottom, clientHeight: nav.clientHeight, scrollHeight: nav.scrollHeight };
});
pass("한남자리 짧은 화면 메뉴 뷰포트·스크롤", mobileNav.top >= 0 && mobileNav.bottom <= 320 && mobileNav.clientHeight > 0 && mobileNav.scrollHeight >= mobileNav.clientHeight, JSON.stringify(mobileNav));
await gotoWorkFile("hannam-jari", "admin", "index.html");
await jclick("[data-select-row]");
const inspectorScroll = await page.$eval(".inspector-scroll", (panel) => ({ clientHeight: panel.clientHeight, scrollHeight: panel.scrollHeight }));
pass("한남자리 짧은 화면 조사관 내부 스크롤", inspectorScroll.clientHeight > 0 && inspectorScroll.scrollHeight >= inspectorScroll.clientHeight, JSON.stringify(inspectorScroll));
// ── 사용자 보고 회귀: 2560px에서 사진 한 장이 본문 밀도를 압도하지 않는다 ──
await page.setViewport({ width: 2560, height: 900 });
await gotoFile("griddle-club");
const griddleWide = await page.$eval(".hero", (hero) => {
const rect = (selector, scope = hero) => {
const r = scope.querySelector(selector).getBoundingClientRect();
return { left: r.left, width: r.width, right: r.right };
};
const visualLineCount = (node) => {
const range = document.createRange();
range.selectNodeContents(node);
return new Set([...range.getClientRects()].map((r) => Math.round(r.top))).size;
};
const heroRect = hero.getBoundingClientRect();
const rail = document.querySelector(".menu-grid").getBoundingClientRect();
const process = document.querySelector(".process-grid");
const processRect = process.getBoundingClientRect();
const processHeading = process.querySelector(".process-copy h2");
return {
hero: { left: heroRect.left, width: heroRect.width, right: heroRect.right },
copy: rect(".hero-copy"),
media: rect(".hero-media"),
contentRail: { left: rail.left, width: rail.width, right: rail.right },
process: {
rail: { left: processRect.left, width: processRect.width, right: processRect.right },
copy: rect(".process-copy", process),
media: rect(".process-media", process),
heading: rect(".process-copy h2", process),
headingLines: visualLineCount(processHeading),
},
};
});
const heroCentered = Math.abs(griddleWide.hero.left + griddleWide.hero.width / 2 - 1280) <= 1;
pass("철판클럽 2560px 히어로·이미지 비율", heroCentered
&& griddleWide.hero.width >= 2140
&& griddleWide.hero.width <= 2162
&& griddleWide.copy.width >= 840
&& griddleWide.copy.width <= 902
&& griddleWide.media.width >= 1050
&& griddleWide.media.width <= 1121
&& griddleWide.media.width <= griddleWide.copy.width * 1.3, JSON.stringify(griddleWide));
pass("철판클럽 2560px 후속 콘텐츠 레일", griddleWide.contentRail.width >= 1200 && griddleWide.contentRail.width <= 1482 && griddleWide.contentRail.left >= 0 && griddleWide.contentRail.right <= 2560, JSON.stringify(griddleWide.contentRail));
const processCentered = Math.abs(griddleWide.process.rail.left + griddleWide.process.rail.width / 2 - 1280) <= 1;
pass("철판클럽 2560px 과정 텍스트·이미지 분할", processCentered
&& griddleWide.process.rail.width >= 1678
&& griddleWide.process.rail.width <= 1682
&& griddleWide.process.copy.width >= 810
&& griddleWide.process.copy.width <= 835
&& griddleWide.process.media.width >= 840
&& griddleWide.process.media.width <= 870
&& griddleWide.process.heading.width >= 620
&& griddleWide.process.headingLines <= 2, JSON.stringify(griddleWide.process));
// ── CSS 중괄호 균형 (v11 교훈) ──
import fs2 from "node:fs";
for (const f of ["gaon-lms/styles/main.css", "dure-enrollment/styles/main.css", "gaon-lms/styles/tokens.css", "dure-enrollment/styles/tokens.css"]) {