/** * AI Platform 메뉴 화면 캡처 (Playwright) * - 로그인: 운영 URL (PROD 전용 화면) * - 나머지: 로컬 SUPER 모드 서버 (인증 없이 캡처) * - 대시보드: OPS 세션 쿠키(허용 이메일) 주입 후 캡처 */ import { chromium } from "playwright"; import crypto from "crypto"; import fs from "fs/promises"; import fsSync from "fs"; import path from "path"; import { fileURLToPath } from "url"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const OUT_DIR = process.env.CAPTURE_OUT_DIR || path.join(__dirname, "..", "docs", "ppt-screenshots"); const LOCAL_BASE = process.env.CAPTURE_BASE_URL || "http://127.0.0.1:8030"; const PROD_BASE = (process.env.CAPTURE_PROD_BASE || "https://ai.xavis.co.kr").replace(/\/$/, ""); const PROD_LOGIN = `${PROD_BASE}/login`; /** 목록(학습·사례)은 운영 데이터·전체 로드 후 캡처 */ const CAPTURE_LISTS_FROM_PROD = process.env.CAPTURE_LISTS_FROM_PROD !== "0"; /** 일반 사용자 OPS 세션 (관리자·특수 메뉴 쿠키 없음) */ const CAPTURE_USER_EMAIL = ( process.env.CAPTURE_USER_EMAIL || "employee@ncue.net" ) .trim() .toLowerCase(); /** general: 가이드봇·WM·대시보드·체크리스트 제외 캡처 */ const CAPTURE_PROFILE = (process.env.CAPTURE_PROFILE || "general").trim().toLowerCase(); const LOCAL_PAGES_ALL = [ { id: "ai-explore", path: "/ai-explore", waitMs: 800 }, { id: "meeting-minutes", path: "/ai-explore/meeting-minutes", waitMs: 1200 }, { id: "task-checklist", path: "/ai-explore/task-checklist", waitMs: 1200 }, { id: "chat", path: "/ai-explore/chat", waitMs: 800 }, { id: "fscan", path: "/ai-explore/fscan", waitMs: 2000 }, { id: "prompts", path: "/ai-explore/prompts", waitMs: 1500 }, { id: "ax-apply", path: "/ax-apply", waitMs: 1000 }, ]; /** 목록 패널 전체(element) 캡처 — viewport 잘림 방지 */ const LIST_PANEL_PAGES = [ { id: "learning", path: "/learning", selector: "section.panel:has(#lecture-results-root)", useProd: true, preload: "learning", }, { id: "ai-cases", path: "/ai-cases", selector: "section.panel:has(.success-story-grid)", useProd: true, preload: "none", }, { id: "ai-cases-compose", path: "/ai-cases/compose", selector: "main.use-case-compose", useProd: true, preload: "none", }, ]; const LOCAL_PAGES = CAPTURE_PROFILE === "general" ? LOCAL_PAGES_ALL.filter( (p) => !["task-checklist", "dashboard", "dashboard-business-performance"].includes(p.id) ) : LOCAL_PAGES_ALL; /** 일반 사용자 좌측 메뉴(가이드봇·WM·대시보드 없음) */ const MENU_OVERVIEW = { id: "menu-overview", path: "/ai-explore", waitMs: 1000 }; function readEnvValue(key) { const envPath = path.join(__dirname, "..", ".env"); try { const raw = fsSync.readFileSync(envPath, "utf8"); const re = new RegExp(`^${key}=(.+)$`, "m"); const m = raw.match(re); if (!m) return ""; return m[1].split("#")[0].trim().replace(/^["']|["']$/g, ""); } catch { return ""; } } /** ops-auth.js 와 동일한 세션 쿠키 서명 */ function buildOpsSessionCookie(email) { const secret = readEnvValue("AUTH_SECRET") || readEnvValue("ADMIN_TOKEN") || "ncue-admin"; const exp = Number.MAX_SAFE_INTEGER; const iat = Date.now(); const payload = `${email}|${exp}|${iat}`; const sig = crypto.createHmac("sha256", secret).update(payload).digest("hex"); return Buffer.from(JSON.stringify({ email, exp, iat, sig })).toString("base64url"); } function captureUserEmail() { return CAPTURE_USER_EMAIL; } async function capturePage(page, url, outPath, waitMs = 1000, fullPage = false) { await page.goto(url, { waitUntil: "networkidle", timeout: 90000 }); await page.waitForTimeout(waitMs); await page.screenshot({ path: outPath, fullPage }); console.log(" saved:", outPath); } /** 학습센터 무한 스크롤 — 모든 페이지 로드 */ async function loadAllLearningPages(page) { await page.waitForSelector("#lecture-grid", { timeout: 60000 }); let guard = 0; while (guard < 80) { guard += 1; const state = await page.evaluate(() => { const sentinel = document.getElementById("infinite-scroll-sentinel"); const btn = document.getElementById("lecture-load-more-btn"); const countEl = document.getElementById("lecture-total-count"); const cards = document.querySelectorAll("#lecture-grid .lecture-card, #lecture-grid a.lecture-card"); return { hasNext: !!(sentinel && sentinel.getAttribute("data-has-next") === "true"), hasBtn: !!btn, total: countEl ? countEl.textContent.trim() : "", loaded: cards.length, }; }); if (!state.hasNext) break; if (state.hasBtn) { await page.locator("#lecture-load-more-btn").click({ timeout: 5000 }).catch(() => {}); } else { await page.evaluate(() => { const s = document.getElementById("infinite-scroll-sentinel"); if (s) s.scrollIntoView({ block: "end", behavior: "instant" }); window.scrollTo(0, document.body.scrollHeight); }); } await page.waitForTimeout(900); await page .waitForFunction( () => { const el = document.getElementById("infinite-scroll-loading"); return !el || el.style.display === "none" || el.style.display === ""; }, { timeout: 45000 } ) .catch(() => {}); } const finalCount = await page.evaluate(() => { const countEl = document.getElementById("lecture-total-count"); const cards = document.querySelectorAll("#lecture-grid .lecture-card, #lecture-grid a.lecture-card"); return { total: countEl ? countEl.textContent.trim() : "?", loaded: cards.length }; }); console.log(" learning loaded:", finalCount.loaded, "/ total", finalCount.total); } /** 목록 섹션(element) 캡처 — 카드 전체가 포함되도록 */ async function captureListPanel(page, baseUrl, item, outPath) { const url = `${baseUrl}${item.path}`; await page.goto(url, { waitUntil: "networkidle", timeout: 90000 }); if (item.preload === "learning") { await loadAllLearningPages(page); } await page.waitForSelector(item.selector, { timeout: 60000 }); await page.waitForTimeout(600); const panel = page.locator(item.selector).first(); await panel.scrollIntoViewIfNeeded(); await panel.screenshot({ path: outPath }); const meta = await page.evaluate((sel) => { const cards = sel.includes("lecture") ? document.querySelectorAll("#lecture-grid .lecture-card, #lecture-grid a.lecture-card").length : document.querySelectorAll(".success-story-grid .success-story-card, .success-story-grid a").length; const chip = document.querySelector(".count-chip"); return { cards, chip: chip ? chip.textContent.trim() : "" }; }, item.selector); console.log(" saved:", outPath, meta.chip || "", "visible cards:", meta.cards); } async function main() { await fs.mkdir(OUT_DIR, { recursive: true }); const browser = await chromium.launch({ headless: true }); const context = await browser.newContext({ viewport: { width: 1440, height: 900 }, locale: "ko-KR", }); const page = await context.newPage(); const userEmail = captureUserEmail(); const opsCookie = buildOpsSessionCookie(userEmail); const cookieJar = [ { name: "ops_user_session", value: opsCookie, url: LOCAL_BASE, httpOnly: true, sameSite: "Lax", }, ]; if (CAPTURE_LISTS_FROM_PROD) { cookieJar.push({ name: "ops_user_session", value: opsCookie, domain: new URL(PROD_BASE).hostname, path: "/", httpOnly: true, secure: true, sameSite: "Lax", }); } await context.addCookies(cookieJar); console.log("Capture persona:", userEmail, "(no admin cookie)", "profile:", CAPTURE_PROFILE); console.log("Output dir:", OUT_DIR); console.log("[1/4] Login (production)..."); try { await capturePage( page, PROD_LOGIN, path.join(OUT_DIR, "login.png"), 1500 ); } catch (err) { console.warn(" login capture failed:", err.message); } console.log("[2/4] Local menus (PROD + OPS cookie, no admin)..."); for (const item of LOCAL_PAGES) { const outPath = path.join(OUT_DIR, `${item.id}.png`); try { await capturePage(page, `${LOCAL_BASE}${item.path}`, outPath, item.waitMs); } catch (err) { console.warn(` ${item.id} failed:`, err.message); } } console.log("[2b/4] List panels (full load + element capture)..."); for (const item of LIST_PANEL_PAGES) { const outPath = path.join(OUT_DIR, `${item.id}.png`); const base = item.useProd && CAPTURE_LISTS_FROM_PROD ? PROD_BASE : LOCAL_BASE; try { await captureListPanel(page, base, item, outPath); } catch (err) { console.warn(` ${item.id} list panel failed (${base}):`, err.message); if (base !== LOCAL_BASE) { try { await captureListPanel(page, LOCAL_BASE, item, outPath); } catch (err2) { console.warn(` ${item.id} local fallback failed:`, err2.message); } } } } console.log("[3/4] Menu overview..."); try { await capturePage( page, `${LOCAL_BASE}${MENU_OVERVIEW.path}`, path.join(OUT_DIR, `${MENU_OVERVIEW.id}.png`), MENU_OVERVIEW.waitMs ); } catch (err) { console.warn(" menu-overview failed:", err.message); } if (CAPTURE_PROFILE !== "general") { console.log("[4/4] Dashboard (허용 계정:", userEmail, ")..."); for (const item of [ { id: "dashboard", path: "/dashboard", waitMs: 1000 }, { id: "dashboard-business-performance", path: "/dashboard/business-performance", waitMs: 2500 }, ]) { const outPath = path.join(OUT_DIR, `${item.id}.png`); try { await capturePage(page, `${LOCAL_BASE}${item.path}`, outPath, item.waitMs); } catch (err) { console.warn(` ${item.id} failed:`, err.message); } } } else { console.log("[4/4] Dashboard skip (general profile)"); } await browser.close(); console.log("Done. Output:", OUT_DIR); } main().catch((err) => { console.error(err); process.exit(1); });