designpaca/apps/site/tools/capture-new-shots.mjs
Yun Chan 4d813420dc
Some checks failed
ci / build (push) Failing after 1m52s
feat(showcase): 신규 쇼케이스 3종 — RFS · 트웬티온스 · 새벽청과
각 워크를 서로 다른 클라이언트로 보고 0~6단계를 따로 돌았다(토큰 체계 신규 정의 =
전체 경로). 셋 모두 라이트/다크 반전을 .band-light / .band-dark 가 --band-* 를
재정의하는 규칙으로 정의해 하드 게이트 4를 통과시켰다 — 반전이 섹션마다의 기분이
아니라 두 클래스가 만드는 규칙이다.

RFS — 실시간 유체 시뮬레이터 (rfs-cfd)
  프리셋 darkroom-spec · 컨셉 "암실과 사양서"
  R1 luminary.ai · R2 teenage.engineering EP-133 · 보조 simscale.com (전부 실측)
  히어로에서 브라우저가 실제로 Navier–Stokes 를 푼다. 순수 WebGL2, three.js 미사용.
  이류 → 온도 소스 → 부력·설비 힘 → 발산 → 압력 Jacobi → 투영 → 파티클 전진.
  격자 224×126 / 파티클 26,000 / Jacobi 22회, 좁은 화면은 절반으로 줄인다.
  prefers-reduced-motion 이면 시작하지 않고 그 자리에 RFS 실물 파티클 렌더가 남는다.
  단위계로 세 번 실패했다: dt 를 프레임 단위로 넘겨 파티클이 0.2초 만에 소멸했고,
  전부 급기구에서만 재생성해 실 한 가닥이 됐으며, 랙이 위로 뿜어 핫/콜드 아일이
  갈라지지 않았다. dt 를 초로 못박고 속도를 초당 격자 셀로 통일해 해결했다.
  수치는 전부 RFS 저장소 README 출처. 정확도 지표는 싣지 않았다 — 사내 검증 자료이고
  현재 통과 상태가 아니다.

트웬티온스 (twentyoz-corp)
  프리셋 field-record · 컨셉 "네 개의 현장"
  R1 samsara.com · R2 lars-mueller-publishers.com · R3 = 현행 twentyoz.kr 감사
  현행 사이트 실측: 대비 실패 172개 요소(9~11px 가 3.61:1), 한글 폰트 미지정으로
  한글이 시스템 기본으로 떨어짐, h1 자간 −0.06em(한글 상한의 세 배), 배경 #020202.
  'Digital Genetics' 은유가 산업용 IoT 사업을 가리고 있었다.
  유지한 자산은 radius 0 과 무채색·모노 기조.
  첫 화면에 회사 이름 대신 현장 선택기를 뒀다 — 라디오 그룹이라 JS 0바이트이고
  좌우 화살표 키가 그냥 먹는다. 파트너 19곳은 원본에 도메인 표기가 없어 나누지 않았다.

새벽청과 (saebyeok-cheonggwa)
  프리셋 market-board · 컨셉 "오늘 새벽 두 시의 값"
  R1 natoora.com · R2 phillips.com · R3 제철 12개월 띠
  커머스가 감추는 시세 변동을 히어로의 주인공으로 삼았다. 상품 사진은 아래로 내렸다.
  경매 시각·경락가격·등급·당도 표시 규정을 조사해 카피의 근거로 썼다.
  무화과 당도를 "—" 로 두고 값이 없는 게 아니라 표시가 허용되지 않는 품목이라고 적었다.
  등락은 색이 아니라 ▲▼보합 기호가 먼저다.
  표의 숫자가 예시임을 판 아래와 푸터 두 곳에 명시하고, 주소·전화는 비웠다(게이트 11).
  사진 4장은 codex 내장 image_gen 생성물이며 합계 341KB.

검증: Astro 빌드 통과, verify 체인 L0~L6 10계층 전부 통과,
showcase-audit 신규 3종 12/12 통과(320/390/768/1440).
성능 실측(RFS): JS 24KB · 총 704KB. 폰트만 530KB 로 예산(200KB) 초과 —
한글 웹폰트 자체가 원인이고 기존 cheongon-care 는 650KB 로 더 크다.
저장소 공통 baseline 이라 이번 판은 기록만 남기고 넘어간다.

기존 16종에서 발견된 70건(대비 미달·히어로 줄 수 초과)은 이번 범위를 넘어
design.md 에 목록만 남겼다.
2026-08-24 12:51:48 +09:00

85 lines
2.7 KiB
JavaScript

// 신규 쇼케이스 3종의 카드 컷 촬영 — 본컷(히어로)과 -2컷(그 페이지의 두 번째 주장).
// WebGL 이 있는 페이지가 있으므로 --disable-gpu 를 쓰지 않고 swiftshader 로 소프트 렌더한다.
//
// 사용: node tools/capture-new-shots.mjs
import puppeteer from "puppeteer-core";
import fs from "node:fs";
import path from "node:path";
import url from "node:url";
const here = path.dirname(url.fileURLToPath(import.meta.url));
const root = path.resolve(here, "..");
const shots = path.join(root, "src", "assets", "shots");
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const CHROME = [
"C:/Program Files/Google/Chrome/Application/chrome.exe",
"C:/Program Files (x86)/Google/Chrome/Application/chrome.exe",
process.env.CHROME_PATH,
].filter(Boolean).find((p) => fs.existsSync(p));
if (!CHROME) {
console.error("크롬을 찾지 못했다.");
process.exit(2);
}
const JOBS = [
{
slug: "rfs-cfd",
// 유체가 정상류에 들 때까지 기다린다. 갓 켠 화면을 찍으면 빈 판이 나온다
settle: 5200,
second: { scrollTo: "#solver", wait: 700 },
},
{
slug: "twentyoz-corp",
settle: 700,
// 두 번째 컷은 '한 일' 원장 — 이 사이트의 본체다
second: { scrollTo: "#ledger", wait: 500 },
},
{
slug: "saebyeok-cheonggwa",
settle: 900,
second: { scrollTo: "#season", wait: 500 },
},
];
const browser = await puppeteer.launch({
executablePath: CHROME,
headless: "new",
args: [
"--no-sandbox",
"--use-angle=swiftshader",
"--enable-unsafe-swiftshader",
"--force-device-scale-factor=1",
],
defaultViewport: { width: 1440, height: 900 },
});
for (const job of JOBS) {
const page = await browser.newPage();
await page.setCacheEnabled(false);
const file = path.join(root, "public/work", job.slug, "index.html");
await page.goto(url.pathToFileURL(file).href, { waitUntil: "networkidle0" });
await page.evaluate(() => document.fonts.ready);
await sleep(job.settle);
const main = path.join(shots, `work-${job.slug}.png`);
await page.screenshot({ path: main });
await page.screenshot({ path: path.join(root, "public/work", job.slug, "og.png") });
await page.evaluate((sel) => {
document.querySelector(sel)?.scrollIntoView({ behavior: "instant", block: "start" });
}, job.second.scrollTo);
await sleep(job.second.wait);
const alt = path.join(shots, `work-${job.slug}-2.png`);
await page.screenshot({ path: alt });
await page.close();
const kb = (p) => Math.round(fs.statSync(p).size / 1024);
console.log(`${job.slug}: 본컷 ${kb(main)}KB · 2컷 ${kb(alt)}KB`);
}
await browser.close();
console.log("촬영 완료");