feat: Enhance SelectedItemsDetailInputComponent with sourceKeyField auto-detection and FK mapping

- Implemented automatic detection of sourceKeyField based on component configuration, improving flexibility in data handling.
- Enhanced the SelectedItemsDetailInputConfigPanel to support automatic FK detection and mapping, streamlining the configuration process.
- Updated the database connection logic to handle DATE types correctly, preventing timezone-related issues.
- Improved overall component performance by optimizing memoization and state management for better user experience.
This commit is contained in:
DDD1542
2026-02-26 16:39:06 +09:00
parent 46ea3612fd
commit 43ead0e7f2
8 changed files with 865 additions and 31 deletions

View File

@@ -0,0 +1,170 @@
/**
* 프로덕션에서 "관리자 메뉴로 전환" 버튼 가시성 테스트
* 두 계정 (topseal_admin, rsw1206)으로 로그인하여 버튼 표시 여부 확인
*
* 실행: node scripts/browser-test-admin-switch-button.js
* 브라우저 표시: HEADLESS=0 node scripts/browser-test-admin-switch-button.js
*/
const { chromium } = require("playwright");
const fs = require("fs");
const BASE_URL = "https://v1.vexplor.com";
const SCREENSHOT_DIR = "test-screenshots/admin-switch-test";
const ACCOUNTS = [
{ userId: "topseal_admin", password: "qlalfqjsgh11", name: "topseal_admin" },
{ userId: "rsw1206", password: "qlalfqjsgh11", name: "rsw1206" },
];
async function runTest() {
const results = { topseal_admin: {}, rsw1206: {} };
const browser = await chromium.launch({
headless: process.env.HEADLESS !== "0",
});
const context = await browser.newContext({
viewport: { width: 1280, height: 900 },
ignoreHTTPSErrors: true,
});
const page = await context.newPage();
try {
if (!fs.existsSync(SCREENSHOT_DIR)) {
fs.mkdirSync(SCREENSHOT_DIR, { recursive: true });
}
const screenshot = async (name) => {
const path = `${SCREENSHOT_DIR}/${name}.png`;
await page.screenshot({ path, fullPage: true });
console.log(` [스크린샷] ${path}`);
return path;
};
for (let i = 0; i < ACCOUNTS.length; i++) {
const acc = ACCOUNTS[i];
console.log(`\n========== ${acc.name} 테스트 (${i + 1}/${ACCOUNTS.length}) ==========\n`);
// 로그인 페이지로 이동
await page.goto(`${BASE_URL}/login`, {
waitUntil: "networkidle",
timeout: 20000,
});
await page.waitForTimeout(1000);
// 로그인
await page.fill("#userId", acc.userId);
await page.fill("#password", acc.password);
await page.click('button[type="submit"]');
await page.waitForTimeout(3000);
// 로그인 성공 시 대시보드 또는 메인으로 리다이렉트될 것임
const currentUrl = page.url();
if (currentUrl.includes("/login") && !currentUrl.includes("error")) {
// 아직 로그인 페이지에 있다면 조금 더 대기
await page.waitForTimeout(3000);
}
const afterLoginUrl = page.url();
const screenshotPath = await screenshot(`01_${acc.name}_after_login`);
// "관리자 메뉴로 전환" 버튼 찾기
const buttonSelectors = [
'button:has-text("관리자 메뉴로 전환")',
'[class*="button"]:has-text("관리자 메뉴로 전환")',
'button >> text=관리자 메뉴로 전환',
];
let buttonVisible = false;
for (const sel of buttonSelectors) {
try {
const btn = page.locator(sel).first();
const count = await btn.count();
if (count > 0) {
const isVisible = await btn.isVisible();
if (isVisible) {
buttonVisible = true;
break;
}
}
} catch (_) {}
}
// 추가: 페이지 내 텍스트로 버튼 존재 여부 확인
if (!buttonVisible) {
const pageText = await page.textContent("body");
buttonVisible = pageText && pageText.includes("관리자 메뉴로 전환");
}
results[acc.name] = {
buttonVisible,
screenshotPath,
afterLoginUrl,
};
console.log(` 버튼 가시성: ${buttonVisible ? "표시됨" : "표시 안 됨"}`);
console.log(` URL: ${afterLoginUrl}`);
// 로그아웃 (다음 계정 테스트 전)
if (i < ACCOUNTS.length - 1) {
console.log("\n 로그아웃 중...");
try {
// 프로필 드롭다운 클릭 (좌측 하단)
const profileBtn = page.locator(
'button:has-text("로그아웃"), [class*="dropdown"]:has-text("로그아웃"), [data-radix-collection-item]:has-text("로그아웃")'
);
const profileTrigger = page.locator(
'button[class*="flex w-full"][class*="gap-3"]'
).first();
if (await profileTrigger.count() > 0) {
await profileTrigger.click();
await page.waitForTimeout(500);
const logoutItem = page.locator('text=로그아웃').first();
if (await logoutItem.count() > 0) {
await logoutItem.click();
await page.waitForTimeout(2000);
}
}
// 또는 직접 로그아웃 URL
if (page.url().includes("/login") === false) {
await page.goto(`${BASE_URL}/api/auth/logout`, {
waitUntil: "networkidle",
timeout: 5000,
}).catch(() => {});
await page.goto(`${BASE_URL}/login`, {
waitUntil: "networkidle",
timeout: 10000,
});
}
} catch (e) {
console.log(" 로그아웃 대체: 로그인 페이지로 직접 이동");
await page.goto(`${BASE_URL}/login`, {
waitUntil: "networkidle",
timeout: 10000,
});
}
await page.waitForTimeout(1500);
}
}
console.log("\n========== 최종 결과 ==========\n");
console.log("topseal_admin: 관리자 메뉴로 전환 버튼 =", results.topseal_admin.buttonVisible ? "표시됨" : "표시 안 됨");
console.log("rsw1206: 관리자 메뉴로 전환 버튼 =", results.rsw1206.buttonVisible ? "표시됨" : "표시 안 됨");
console.log("\n스크린샷:", SCREENSHOT_DIR);
return results;
} catch (err) {
console.error("테스트 오류:", err);
throw err;
} finally {
await browser.close();
}
}
runTest()
.then((r) => {
console.log("\n테스트 완료.");
process.exit(0);
})
.catch((e) => {
console.error(e);
process.exit(1);
});

View File

@@ -0,0 +1,167 @@
/**
* 거래처관리 화면 CRUD 브라우저 테스트
* 실행: node scripts/browser-test-customer-crud.js
* 브라우저 표시: HEADLESS=0 node scripts/browser-test-customer-crud.js
*/
const { chromium } = require("playwright");
const BASE_URL = "http://localhost:9771";
const SCREENSHOT_DIR = "test-screenshots";
async function runTest() {
const results = { success: [], failed: [], screenshots: [] };
const browser = await chromium.launch({ headless: process.env.HEADLESS !== "0" });
const context = await browser.newContext({ viewport: { width: 1280, height: 900 } });
const page = await context.newPage();
try {
// 스크린샷 디렉토리
const fs = require("fs");
if (!fs.existsSync(SCREENSHOT_DIR)) fs.mkdirSync(SCREENSHOT_DIR, { recursive: true });
const screenshot = async (name) => {
const path = `${SCREENSHOT_DIR}/${name}.png`;
await page.screenshot({ path, fullPage: true });
results.screenshots.push(path);
console.log(` [스크린샷] ${path}`);
};
console.log("\n=== 1단계: 로그인 ===\n");
await page.goto(`${BASE_URL}/login`, { waitUntil: "networkidle", timeout: 15000 });
await page.fill('#userId', 'topseal_admin');
await page.fill('#password', 'qlalfqjsgh11');
await page.click('button[type="submit"]');
await page.waitForTimeout(3000);
await screenshot("01_after_login");
results.success.push("로그인 완료");
console.log("\n=== 2단계: 거래처관리 화면 이동 ===\n");
await page.goto(`${BASE_URL}/screens/227`, { waitUntil: "domcontentloaded", timeout: 20000 });
// 테이블 또는 메인 콘텐츠 로딩 대기 (API 호출 후 React 렌더링)
try {
await page.waitForSelector('table, tbody, [role="row"], .rt-tbody', { timeout: 25000 });
results.success.push("테이블 로드 감지");
} catch (e) {
console.log(" [경고] 테이블 대기 타임아웃, 계속 진행");
}
await page.waitForTimeout(3000);
await screenshot("02_screen_227");
results.success.push("화면 227 로드");
console.log("\n=== 3단계: 거래처 선택 (READ 테스트) ===\n");
// 좌측 테이블 행 선택 - 다양한 레이아웃 대응
const rowSelectors = [
'table tbody tr.cursor-pointer',
'tbody tr.hover\\:bg-accent',
'table tbody tr:has(td)',
'tbody tr',
];
let rows = [];
for (const sel of rowSelectors) {
rows = await page.$$(sel);
if (rows.length > 0) break;
}
if (rows.length > 0) {
await rows[0].click();
results.success.push("거래처 행 클릭");
} else {
results.failed.push("거래처 테이블 행을 찾을 수 없음");
// 디버그: 페이지 구조 저장
const bodyHtml = await page.evaluate(() => {
const tables = document.querySelectorAll('table, tbody, [role="grid"], [role="table"]');
return `Tables found: ${tables.length}\n` + document.body.innerHTML.slice(0, 8000);
});
require("fs").writeFileSync(`${SCREENSHOT_DIR}/debug_body.html`, bodyHtml);
console.log(" [디버그] body HTML 일부 저장: debug_body.html");
}
await page.waitForTimeout(3000);
await screenshot("03_after_customer_select");
// SelectedItemsDetailInput 영역 확인
const detailArea = await page.$('[data-component="selected-items-detail-input"], [class*="selected-items"], .selected-items-detail');
if (detailArea) {
results.success.push("SelectedItemsDetailInput 컴포넌트 렌더링 확인");
} else {
// 품목/입력 관련 영역이 있는지
const hasInputArea = await page.$('input[placeholder*="품번"], input[placeholder*="품목"], [class*="detail"]');
results.success.push(hasInputArea ? "입력 영역 확인됨" : "SelectedItemsDetailInput 영역 미확인");
}
console.log("\n=== 4단계: 품목 추가 (CREATE 테스트) ===\n");
const addBtnLoc = page.locator('button').filter({ hasText: /추가|품목/ }).first();
const addBtnExists = await addBtnLoc.count() > 0;
if (addBtnExists) {
await addBtnLoc.click();
await page.waitForTimeout(1500);
await screenshot("04_after_add_click");
// 모달/팝업에서 품목 선택
const modalItem = await page.$('[role="dialog"] tr, [role="listbox"] [role="option"], .modal tbody tr');
if (modalItem) {
await modalItem.click();
await page.waitForTimeout(1000);
}
// 필수 필드 입력
const itemCodeInput = await page.$('input[name*="품번"], input[placeholder*="품번"], input[id*="item"]');
if (itemCodeInput) {
await itemCodeInput.fill("TEST_BROWSER");
}
await screenshot("04_before_save");
const saveBtnLoc = page.locator('button').filter({ hasText: /저장/ }).first();
if (await saveBtnLoc.count() > 0) {
await saveBtnLoc.click();
await page.waitForTimeout(3000);
await screenshot("05_after_save");
results.success.push("저장 버튼 클릭");
const toast = await page.$('[data-sonner-toast], .toast, [role="alert"]');
if (toast) {
const toastText = await toast.textContent();
results.success.push(`토스트 메시지: ${toastText?.slice(0, 50)}`);
}
} else {
results.failed.push("저장 버튼을 찾을 수 없음");
}
} else {
results.failed.push("품목 추가/추가 버튼을 찾을 수 없음");
await screenshot("04_no_add_button");
}
console.log("\n=== 5단계: 최종 결과 ===\n");
await screenshot("06_final_state");
// 콘솔 에러 수집
const consoleErrors = [];
page.on("console", (msg) => {
const type = msg.type();
if (type === "error") {
consoleErrors.push(msg.text());
}
});
} catch (err) {
results.failed.push(`예외: ${err.message}`);
try {
await page.screenshot({ path: `${SCREENSHOT_DIR}/error.png`, fullPage: true });
results.screenshots.push(`${SCREENSHOT_DIR}/error.png`);
} catch (_) {}
} finally {
await browser.close();
}
// 결과 출력
console.log("\n========== 테스트 결과 ==========\n");
console.log("성공:", results.success);
console.log("실패:", results.failed);
console.log("스크린샷:", results.screenshots);
return results;
}
runTest().then((r) => {
process.exit(r.failed.length > 0 ? 1 : 0);
}).catch((e) => {
console.error(e);
process.exit(1);
});

View File

@@ -0,0 +1,157 @@
/**
* 거래처관리 메뉴 경유 브라우저 테스트
* 영업관리 > 거래처관리 메뉴 클릭 후 상세 화면 진입
* 실행: node scripts/browser-test-customer-via-menu.js
* 브라우저 표시: HEADLESS=0 node scripts/browser-test-customer-via-menu.js
*/
const { chromium } = require("playwright");
const BASE_URL = "http://localhost:9771";
const SCREENSHOT_DIR = "test-screenshots";
const CREDENTIALS = { userId: "topseal_admin", password: "qlalfqjsgh11" };
async function runTest() {
const results = { success: [], failed: [], screenshots: [] };
const browser = await chromium.launch({ headless: process.env.HEADLESS !== "0" });
const context = await browser.newContext({ viewport: { width: 1280, height: 900 } });
const page = await context.newPage();
const fs = require("fs");
if (!fs.existsSync(SCREENSHOT_DIR)) fs.mkdirSync(SCREENSHOT_DIR, { recursive: true });
const screenshot = async (name) => {
const path = `${SCREENSHOT_DIR}/${name}.png`;
await page.screenshot({ path, fullPage: true });
results.screenshots.push(path);
console.log(` [스크린샷] ${path}`);
};
try {
// 로그인 (이미 로그인된 상태면 자동 리다이렉트됨)
console.log("\n=== 로그인 확인 ===\n");
await page.goto(`${BASE_URL}/login`, { waitUntil: "networkidle", timeout: 15000 });
const currentUrl = page.url();
if (currentUrl.includes("/login") && !(await page.$('input#userId'))) {
// 로그인 폼이 있으면 로그인
await page.fill("#userId", CREDENTIALS.userId);
await page.fill("#password", CREDENTIALS.password);
await page.click('button[type="submit"]');
await page.waitForTimeout(3000);
} else if (currentUrl.includes("/login")) {
await page.fill("#userId", CREDENTIALS.userId);
await page.fill("#password", CREDENTIALS.password);
await page.click('button[type="submit"]');
await page.waitForTimeout(3000);
}
results.success.push("로그인/세션 확인");
// 단계 1: 영업관리 메뉴 클릭
console.log("\n=== 단계 1: 영업관리 메뉴 클릭 ===\n");
const salesMenu = page.locator('nav, aside').getByText('영업관리', { exact: true }).first();
if (await salesMenu.count() > 0) {
await salesMenu.click();
await page.waitForTimeout(2000);
results.success.push("영업관리 메뉴 클릭");
} else {
const salesAlt = page.getByRole('button', { name: /영업관리/ }).or(page.getByText('영업관리').first());
if (await salesAlt.count() > 0) {
await salesAlt.first().click();
await page.waitForTimeout(2000);
results.success.push("영업관리 메뉴 클릭 (대안)");
} else {
results.failed.push("영업관리 메뉴를 찾을 수 없음");
}
}
await screenshot("01_after_sales_menu");
// 단계 2: 거래처관리 서브메뉴 클릭
console.log("\n=== 단계 2: 거래처관리 서브메뉴 클릭 ===\n");
const customerMenu = page.getByText("거래처관리", { exact: true }).first();
if (await customerMenu.count() > 0) {
await customerMenu.click();
await page.waitForTimeout(5000);
results.success.push("거래처관리 메뉴 클릭");
} else {
results.failed.push("거래처관리 메뉴를 찾을 수 없음");
}
await screenshot("02_after_customer_menu");
// 단계 3: 거래처 목록 확인 및 행 클릭
console.log("\n=== 단계 3: 거래처 목록 확인 ===\n");
const rows = await page.$$('tbody tr, table tr, [role="row"]');
const clickableRows = rows.length > 0 ? rows : [];
if (clickableRows.length > 0) {
await clickableRows[0].click();
await page.waitForTimeout(5000);
results.success.push(`거래처 행 클릭 (${clickableRows.length}개 행 중 첫 번째)`);
} else {
results.failed.push("거래처 테이블 행을 찾을 수 없음");
}
await screenshot("03_after_row_click");
// 단계 4: 편집/수정 버튼 또는 더블클릭 (분할 패널이면 행 선택만으로 우측에 상세 표시될 수 있음)
console.log("\n=== 단계 4: 상세 화면 진입 시도 ===\n");
const editBtn = page.locator('button').filter({ hasText: /편집|수정|상세/ }).first();
let editEnabled = false;
try {
if (await editBtn.count() > 0) {
editEnabled = !(await editBtn.isDisabled());
}
} catch (_) {}
try {
if (editEnabled) {
await editBtn.click();
results.success.push("편집/수정 버튼 클릭");
} else {
const row = await page.$('tbody tr, table tr');
if (row) {
await row.dblclick();
results.success.push("행 더블클릭 시도");
} else if (await editBtn.count() > 0) {
results.success.push("수정 버튼 비활성화 - 분할 패널 우측 상세 확인");
} else {
results.failed.push("편집 버튼/행을 찾을 수 없음");
}
}
} catch (e) {
results.success.push("상세 진입 스킵 - 우측 패널에 상세 표시 여부 확인");
}
await page.waitForTimeout(5000);
await screenshot("04_after_detail_enter");
// 단계 5: 품목 관련 영역 확인
console.log("\n=== 단계 5: 품목 관련 영역 확인 ===\n");
const hasItemSection = await page.getByText(/품목|납품품목|거래처 품번|거래처 품명/).first().count() > 0;
const hasDetailInput = await page.$('input[placeholder*="품번"], input[name*="품번"], [class*="selected-items"]');
if (hasItemSection || hasDetailInput) {
results.success.push("품목 관련 UI 확인됨");
} else {
results.failed.push("품목 관련 영역 미확인");
}
await screenshot("05_item_section");
console.log("\n========== 테스트 결과 ==========\n");
console.log("성공:", results.success);
console.log("실패:", results.failed);
console.log("스크린샷:", results.screenshots);
} catch (err) {
results.failed.push(`예외: ${err.message}`);
try {
await page.screenshot({ path: `${SCREENSHOT_DIR}/error.png`, fullPage: true });
results.screenshots.push(`${SCREENSHOT_DIR}/error.png`);
} catch (_) {}
console.error(err);
} finally {
await browser.close();
}
return results;
}
runTest()
.then((r) => process.exit(r.failed.length > 0 ? 1 : 0))
.catch((e) => {
console.error(e);
process.exit(1);
});

View File

@@ -0,0 +1,196 @@
/**
* 구매관리 - 공급업체관리 / 구매품목정보 CRUD 브라우저 테스트
* 실행: node scripts/browser-test-purchase-supplier.js
* 브라우저 표시: HEADLESS=0 node scripts/browser-test-purchase-supplier.js
*/
const { chromium } = require("playwright");
const BASE_URL = "http://localhost:9771";
const SCREENSHOT_DIR = "test-screenshots";
const CREDENTIALS = { userId: "topseal_admin", password: "qlalfqjsgh11" };
async function runTest() {
const results = { success: [], failed: [], screenshots: [] };
const browser = await chromium.launch({ headless: process.env.HEADLESS !== "0" });
const context = await browser.newContext({ viewport: { width: 1280, height: 900 } });
const page = await context.newPage();
const fs = require("fs");
if (!fs.existsSync(SCREENSHOT_DIR)) fs.mkdirSync(SCREENSHOT_DIR, { recursive: true });
const screenshot = async (name) => {
const path = `${SCREENSHOT_DIR}/${name}.png`;
await page.screenshot({ path, fullPage: true });
results.screenshots.push(path);
console.log(` [스크린샷] ${path}`);
return path;
};
const clickMenu = async (text) => {
const loc = page.getByText(text, { exact: true }).first();
if ((await loc.count()) > 0) {
await loc.click();
return true;
}
const alt = page.getByRole("link", { name: text }).or(page.locator(`a:has-text("${text}")`)).first();
if ((await alt.count()) > 0) {
await alt.click();
return true;
}
return false;
};
const clickRow = async () => {
const rows = await page.$$('tbody tr, table tr, [role="row"]');
for (const r of rows) {
const t = await r.textContent();
if (t && !t.includes("데이터가 없습니다") && !t.includes("로딩")) {
await r.click();
return true;
}
}
if (rows.length > 0) {
await rows[0].click();
return true;
}
return false;
};
const clickButton = async (regex) => {
const btn = page.locator("button").filter({ hasText: regex }).first();
try {
if ((await btn.count()) > 0 && !(await btn.isDisabled())) {
await btn.click();
return true;
}
} catch (_) {}
return false;
};
try {
console.log("\n=== 로그인 확인 ===\n");
await page.goto(`${BASE_URL}/login`, { waitUntil: "networkidle", timeout: 15000 });
if (page.url().includes("/login")) {
await page.fill("#userId", CREDENTIALS.userId);
await page.fill("#password", CREDENTIALS.password);
await page.click('button[type="submit"]');
await page.waitForTimeout(3000);
}
results.success.push("세션 확인");
// ========== 테스트 1: 공급업체관리 ==========
console.log("\n=== 테스트 1: 공급업체관리 ===\n");
console.log("단계 1: 구매관리 메뉴 열기");
if (await clickMenu("구매관리")) {
await page.waitForTimeout(3000);
results.success.push("구매관리 메뉴 클릭");
} else {
results.failed.push("구매관리 메뉴 미발견");
}
await screenshot("p1_01_purchase_menu");
console.log("단계 2: 공급업체관리 서브메뉴 클릭");
if (await clickMenu("공급업체관리")) {
await page.waitForTimeout(8000);
results.success.push("공급업체관리 메뉴 클릭");
} else {
results.failed.push("공급업체관리 메뉴 미발견");
}
await screenshot("p1_02_supplier_screen");
console.log("단계 3: 공급업체 선택");
if (await clickRow()) {
await page.waitForTimeout(5000);
results.success.push("공급업체 행 클릭");
} else {
results.failed.push("공급업체 테이블 행 미발견");
}
await screenshot("p1_03_after_supplier_select");
console.log("단계 4: 납품품목 탭/영역 확인");
const itemTab = page.getByText(/납품품목|품목/).first();
if ((await itemTab.count()) > 0) {
await itemTab.click();
await page.waitForTimeout(3000);
results.success.push("납품품목/품목 탭 클릭");
} else {
results.failed.push("납품품목 탭 미발견");
}
await screenshot("p1_04_item_tab");
console.log("단계 5: 품목 추가 시도");
const addBtn = page.locator("button").filter({ hasText: /추가|\+ 추가/ }).first();
let addBtnEnabled = false;
try {
addBtnEnabled = (await addBtn.count()) > 0 && !(await addBtn.isDisabled());
} catch (_) {}
if (addBtnEnabled) {
await addBtn.click();
await page.waitForTimeout(2000);
const modal = await page.$('[role="dialog"], .modal, [class*="modal"]');
if (modal) {
const modalRow = await page.$('[role="dialog"] tbody tr, .modal tbody tr');
if (modalRow) {
await modalRow.click();
await page.waitForTimeout(1500);
}
}
await page.waitForTimeout(1500);
results.success.push("추가 버튼 클릭 및 품목 선택 시도");
} else {
results.failed.push("추가 버튼 미발견 또는 비활성화");
}
await screenshot("p1_05_add_item");
// ========== 테스트 2: 구매품목정보 ==========
console.log("\n=== 테스트 2: 구매품목정보 ===\n");
console.log("단계 6: 구매품목정보 메뉴 클릭");
if (await clickMenu("구매품목정보")) {
await page.waitForTimeout(8000);
results.success.push("구매품목정보 메뉴 클릭");
} else {
results.failed.push("구매품목정보 메뉴 미발견");
}
await screenshot("p2_01_item_screen");
console.log("단계 7: 품목 선택 및 공급업체 확인");
if (await clickRow()) {
await page.waitForTimeout(5000);
results.success.push("구매품목 행 클릭");
} else {
results.failed.push("구매품목 테이블 행 미발견");
}
await screenshot("p2_02_after_item_select");
// SelectedItemsDetailInput 컴포넌트 확인
const hasDetailInput = await page.$('input[placeholder*="품번"], [class*="selected-items"], input[name*="품번"]');
results.success.push(hasDetailInput ? "SelectedItemsDetailInput 렌더링 확인" : "SelectedItemsDetailInput 미확인");
await screenshot("p2_03_final");
console.log("\n========== 테스트 결과 ==========\n");
console.log("성공:", results.success);
console.log("실패:", results.failed);
console.log("스크린샷:", results.screenshots);
} catch (err) {
results.failed.push(`예외: ${err.message}`);
try {
await page.screenshot({ path: `${SCREENSHOT_DIR}/error.png`, fullPage: true });
results.screenshots.push(`${SCREENSHOT_DIR}/error.png`);
} catch (_) {}
console.error(err);
} finally {
await browser.close();
}
return results;
}
runTest()
.then((r) => process.exit(r.failed.length > 0 ? 1 : 0))
.catch((e) => {
console.error(e);
process.exit(1);
});