// 신규 쇼케이스 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"; import { collectPageFailures, prepareStillPage, resolveChrome, routeForFile, startPublicServer, waitForStableVisual, } from "./capture-stable.mjs"; const here = path.dirname(url.fileURLToPath(import.meta.url)); const root = path.resolve(here, ".."); const shots = path.join(root, "src", "assets", "shots"); const publicRoot = path.join(root, "public"); function requestedSlugs(argv) { const slugs = new Set(); for (let index = 2; index < argv.length; index += 1) { const argument = argv[index]; if (argument === "--slug") { const slug = argv[index + 1]; if (!slug) throw new Error("--slug requires a showcase slug"); slugs.add(slug); index += 1; } else if (argument.startsWith("--slug=")) { slugs.add(argument.slice("--slug=".length)); } else { throw new Error(`Unknown argument: ${argument}`); } } return slugs; } 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 requested = requestedSlugs(process.argv); const known = new Set(JOBS.map((job) => job.slug)); const unknown = [...requested].filter((slug) => !known.has(slug)); if (unknown.length) throw new Error(`Unknown showcase slug(s): ${unknown.join(", ")}`); const selectedJobs = requested.size ? JOBS.filter((job) => requested.has(job.slug)) : JOBS; const server = await startPublicServer(publicRoot); let browser; try { browser = await puppeteer.launch({ executablePath: resolveChrome(), headless: "new", args: [ "--no-sandbox", "--use-angle=swiftshader", "--enable-unsafe-swiftshader", "--force-device-scale-factor=1", ], defaultViewport: { width: 1440, height: 900, deviceScaleFactor: 1 }, }); for (const job of selectedJobs) { const page = await browser.newPage(); const failures = collectPageFailures(page, server.origin); try { await prepareStillPage(page); const file = path.join(publicRoot, "work", job.slug, "index.html"); const response = await page.goto(server.origin + routeForFile(publicRoot, file), { waitUntil: "domcontentloaded", timeout: 30000, }); if (!response?.ok()) throw new Error(`${job.slug} returned ${response?.status() ?? "no response"}`); await waitForStableVisual(page, { settleMs: job.settle }); if (failures.length) throw new Error(failures.join(" · ")); const main = path.join(shots, `work-${job.slug}.png`); await page.screenshot({ path: main }); await page.screenshot({ path: path.join(publicRoot, "work", job.slug, "og.png") }); await page.evaluate((selector) => { document.querySelector(selector)?.scrollIntoView({ behavior: "auto", block: "start" }); }, job.second.scrollTo); await new Promise((resolve) => setTimeout(resolve, job.second.wait)); const alt = path.join(shots, `work-${job.slug}-2.png`); await page.screenshot({ path: alt }); const kb = (filePath) => Math.round(fs.statSync(filePath).size / 1024); console.log(`${job.slug}: 본컷 ${kb(main)}KB · 2컷 ${kb(alt)}KB`); } finally { await page.close(); } } } finally { if (browser) await browser.close(); await server.close(); } console.log("정착 상태 촬영 완료");