Files
ai_platform/scripts/capture-menu-screenshots.mjs
dsyoon 073a8343dd feat: xavis ai_platform 기능 이전 및 ncue 환경 전환
xavis 소스·DB 스키마·활용사례/F-Scan/프롬프트 라이브러리 등 기능 반영.
@xavis.co.kr → @ncue.net, 관리자 토큰 ncue-admin, 런타임 data/ Git 추적 제외.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-26 22:27:48 +09:00

294 lines
10 KiB
JavaScript

/**
* 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);
});