xavis 소스·DB 스키마·활용사례/F-Scan/프롬프트 라이브러리 등 기능 반영. @xavis.co.kr → @ncue.net, 관리자 토큰 ncue-admin, 런타임 data/ Git 추적 제외. Co-authored-by: Cursor <cursoragent@cursor.com>
294 lines
10 KiB
JavaScript
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);
|
|
});
|