/** * 배포본 전수 확인. * * 이 파일은 절차 실패에서 나왔다 — 고친 화면만 확인하고 배포해서, 나머지가 깨진 것은 매번 * 사용자가 먼저 발견했다. 운영 환경이므로 배포 전에 모든 화면을 한 번씩 열어 보는 것이 맞다. * * 각 화면에서 보는 것: main 이 그려졌는지, 콘솔 오류, 4xx/5xx API 응답, 그리고 화면에 뜬 * 오류 문구. 하나라도 있으면 그 화면을 실패로 적고 끝까지 진행한다. * * SPW=<비밀번호> node scripts/smoke/production-sweep.mjs [origin] */ import process from "node:process"; import { chromium, type Page } from "@playwright/test"; const ORIGIN = process.argv[2] ?? "https://hyeonworks.com"; const PW = process.env.SPW; const ERROR_TEXT = /요청을 처리하지 못했습니다|지원 정보 확인|표시할 수 없습니다|불러오지 못했습니다|화면을 찾을 수 없습니다|Not Found/; const results: { label: string; path: string; problems: string[] }[] = []; async function visit( page: Page, label: string, path: string, { expectMain = true }: { expectMain?: boolean } = {}, ) { const problems: string[] = []; const onConsole = (m: { type(): string; text(): string }) => { if (m.type() === "error" && !/401/.test(m.text())) problems.push(`console: ${m.text().slice(0, 120)}`); }; const onResponse = (r: { url(): string; status(): number; request(): { method(): string }; }) => { const u = new URL(r.url()).pathname; if (r.status() < 400 || !u.startsWith("/api")) return; // 두 가지는 화면이 다루는 정상 상태다: 로그인 전 세션 탐침의 401, 그리고 아직 미리보기를 // 만들지 않은 문서의 404. 이것들을 실패로 세면 매번 같은 줄이 뜨고, 진짜 실패가 그 사이에 // 묻힌다 — 늑대가 왔다고 매번 외치는 점검은 아무도 읽지 않는다. if (u.includes("/studio/session")) return; if (r.status() === 404 && r.request().method() === "GET" && u.endsWith("/preview")) return; problems.push(`${r.status()} ${r.request().method()} ${u}`); }; page.on("console", onConsole); page.on("response", onResponse); try { await page.goto(ORIGIN + path, { waitUntil: "domcontentloaded", timeout: 45000 }); await page.waitForTimeout(3000); const main = await page.locator("main").count(); const body = (await page.locator("body").innerText().catch(() => "")).replace(/\s+/g, " "); if (expectMain && main === 0) problems.push("main 없음"); const shown = body.match(ERROR_TEXT); if (shown) problems.push(`화면 문구: ${shown[0]}`); } catch (error) { problems.push(`이동 실패: ${String(error).slice(0, 90)}`); } finally { page.off("console", onConsole); page.off("response", onResponse); } results.push({ label, path, problems }); console.log(`${problems.length ? "✗" : "✓"} ${label.padEnd(22)} ${path}`); for (const p of problems) console.log(` ${p}`); } const browser = await chromium.launch(); const page = await (await browser.newContext()).newPage(); console.log("=== 공개 ==="); for (const [label, path] of [ ["홈", "/"], ["탐색", "/explore"], ["Case 목록", "/explore/cases"], ["프로젝트", "/projects"], ["변경 기록", "/releases"], ["릴리즈 상세", "/releases/0.1.0"], ["검색", "/search"], ["프로필", "/profile"], ]) await visit(page, label, path); if (!PW) { console.log("\n(SPW 없음 — Studio 생략)"); await browser.close(); process.exit(0); } console.log("\n=== 로그인 ==="); await page.goto(ORIGIN + "/studio", { waitUntil: "domcontentloaded", timeout: 60000 }); await page.waitForTimeout(2500); const start = page.getByRole("button", { name: /로그인 시작/ }).or(page.getByRole("link", { name: /로그인 시작/ })); if (await start.count()) { await start.first().click(); await page.waitForTimeout(5000); } await page.fill("#username", "hyeonworks"); await page.fill("#password", PW); await page.click("#kc-login, input[type=submit], button[type=submit]"); await page.waitForTimeout(6000); console.log(" 로그인 후:", page.url().replace(ORIGIN, "") || "/"); console.log("\n=== Studio ==="); for (const [label, path] of [ ["대시보드", "/studio"], ["작업본", "/studio/documents"], ["새 문서", "/studio/documents/new"], ["게시 기록", "/studio/publications"], ["Asset", "/studio/assets"], ["주제·프로젝트", "/studio/taxonomy"], ["릴리즈", "/studio/releases"], ]) await visit(page, label, path); // 작업본 하나를 골라 편집·검증·미리보기까지 연다 await page.goto(ORIGIN + "/studio/documents", { waitUntil: "domcontentloaded" }); await page.waitForTimeout(3000); const href = await page.locator("a[href*='/studio/documents/'][href$='/edit']").first().getAttribute("href").catch(() => null); if (href) { const id = href.split("/")[3]; console.log("\n=== 문서 흐름 ===", id); for (const [label, suffix] of [["편집", "/edit"], ["검증", "/validation"], ["미리보기", "/preview"], ["게시", "/publish"]]) await visit(page, label, `/studio/documents/${id}${suffix}`); } else console.log("\n(편집 링크를 찾지 못해 문서 흐름 생략)"); await browser.close(); const failed = results.filter((r) => r.problems.length); console.log(`\n=== 결과 === ${results.length - failed.length}/${results.length} 통과`); for (const r of failed) console.log(` ✗ ${r.label} (${r.path}): ${r.problems.join(" | ").slice(0, 160)}`); process.exit(failed.length ? 1 : 0);