// 한남자리·철판클럽 전용 캡처. // // 이 스크립트가 덮어쓰는 파일은 아래 여섯 개뿐이다. // src/assets/shots/work-hannam-jari.png // src/assets/shots/work-hannam-jari-2.png // src/assets/shots/work-griddle-club.png // src/assets/shots/work-griddle-club-2.png // public/work/hannam-jari/og.png // public/work/griddle-club/og.png // // 공개 자산과 같은 HTTP 경로로 상대 링크를 검증하고, 폰트·이미지가 준비되지 않으면 // 불완전한 화면을 저장하지 않고 실패한다. import fs from "node:fs"; import path from "node:path"; import url from "node:url"; import puppeteer from "puppeteer-core"; import { collectPageFailures, prepareStillPage, resolveChrome, startPublicServer, waitForStableVisual, } from "./capture-stable.mjs"; const here = path.dirname(url.fileURLToPath(import.meta.url)); const root = path.resolve(here, ".."); const publicRoot = path.join(root, "public"); const shotsRoot = path.join(root, "src", "assets", "shots"); const jobs = [ { label: "한남자리 공개", route: "/work/hannam-jari/", output: path.join(shotsRoot, "work-hannam-jari.png"), }, { label: "한남자리 관리자", route: "/work/hannam-jari/admin/", output: path.join(shotsRoot, "work-hannam-jari-2.png"), }, { label: "철판클럽 공개", route: "/work/griddle-club/", output: path.join(shotsRoot, "work-griddle-club.png"), }, { label: "철판클럽 가맹", route: "/work/griddle-club/franchise/", output: path.join(shotsRoot, "work-griddle-club-2.png"), }, ]; const ogJobs = [ { label: "한남자리 OG", route: "/work/hannam-jari/", output: path.join(publicRoot, "work", "hannam-jari", "og.png"), }, { label: "철판클럽 OG", route: "/work/griddle-club/", output: path.join(publicRoot, "work", "griddle-club", "og.png"), }, ]; await fs.promises.mkdir(shotsRoot, { recursive: true }); const server = await startPublicServer(publicRoot); let browser; async function capture(job, viewport) { const page = await browser.newPage(); const failures = collectPageFailures(page, server.origin); try { await prepareStillPage(page); await page.setViewport(viewport); const response = await page.goto(server.origin + job.route, { waitUntil: "domcontentloaded", timeout: 30000 }); if (!response?.ok()) throw new Error(`${job.route} returned ${response?.status() ?? "no response"}`); await waitForStableVisual(page); if (failures.length) throw new Error(failures.join(" · ")); await fs.promises.mkdir(path.dirname(job.output), { recursive: true }); await page.screenshot({ path: job.output, type: "png", fullPage: false }); const size = Math.round((await fs.promises.stat(job.output)).size / 1024); console.log(`CAPTURE ${job.label} → ${path.relative(root, job.output)} (${viewport.width}×${viewport.height}, ${size}KB)`); } finally { await page.close(); } } try { console.log("OVERWRITE SCOPE"); for (const job of [...jobs, ...ogJobs]) console.log(`- ${path.relative(root, job.output)}`); browser = await puppeteer.launch({ executablePath: resolveChrome(), headless: "new", args: [ "--no-sandbox", "--use-angle=swiftshader", "--enable-unsafe-swiftshader", "--force-device-scale-factor=1", ], }); for (const job of jobs) await capture(job, { width: 1440, height: 900, deviceScaleFactor: 1 }); for (const job of ogJobs) await capture(job, { width: 1200, height: 630, deviceScaleFactor: 1 }); } finally { if (browser) await browser.close(); await server.close(); } console.log("쇼케이스 듀오 카드 4장과 OG 2장 캡처 완료");