425 lines
19 KiB
JavaScript
425 lines
19 KiB
JavaScript
|
|
#!/usr/bin/env node
|
|||
|
|
/**
|
|||
|
|
* 다운로드 버튼 상세 디버깅 스크립트
|
|||
|
|
*
|
|||
|
|
* 각 페이지의 다운로드 버튼 클릭 시 실제로 무엇이 일어나는지 추적
|
|||
|
|
* - 모든 네트워크 요청 로깅
|
|||
|
|
* - Blob URL 생성 감지
|
|||
|
|
* - 새 탭/팝업 감지
|
|||
|
|
* - Console 메시지 캡처
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
const fs = require('fs');
|
|||
|
|
const path = require('path');
|
|||
|
|
|
|||
|
|
const SAM_ROOT = path.resolve(__dirname, '..', '..');
|
|||
|
|
const PW_PATH = path.join(SAM_ROOT, 'react', 'node_modules', 'playwright');
|
|||
|
|
const { chromium } = require(PW_PATH);
|
|||
|
|
|
|||
|
|
const BASE_URL = 'https://dev.codebridge-x.com';
|
|||
|
|
const AUTH = { username: 'TestUser5', password: 'password123!' };
|
|||
|
|
const RESULTS_DIR = path.join(SAM_ROOT, 'e2e', 'results', 'hotfix');
|
|||
|
|
const DOWNLOAD_DIR = path.join(RESULTS_DIR, 'downloads');
|
|||
|
|
|
|||
|
|
const C = {
|
|||
|
|
green: t => `\x1b[32m${t}\x1b[0m`,
|
|||
|
|
red: t => `\x1b[31m${t}\x1b[0m`,
|
|||
|
|
yellow: t => `\x1b[33m${t}\x1b[0m`,
|
|||
|
|
cyan: t => `\x1b[36m${t}\x1b[0m`,
|
|||
|
|
dim: t => `\x1b[2m${t}\x1b[0m`,
|
|||
|
|
bold: t => `\x1b[1m${t}\x1b[0m`,
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
|
|||
|
|
|
|||
|
|
function getTimestamp() {
|
|||
|
|
const n = new Date();
|
|||
|
|
const pad = v => v.toString().padStart(2, '0');
|
|||
|
|
return `${n.getFullYear()}-${pad(n.getMonth() + 1)}-${pad(n.getDate())}_${pad(n.getHours())}-${pad(n.getMinutes())}-${pad(n.getSeconds())}`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 실패한 페이지만 재검증 (버튼은 있는데 다운로드 실패한 것)
|
|||
|
|
const DEBUG_TARGETS = [
|
|||
|
|
{ id: 'acc-daily-report', level1: '회계관리', level2: '일일 일보' },
|
|||
|
|
{ id: 'acc-bank-tx', level1: '회계관리', level2: '계좌입출금내역' },
|
|||
|
|
{ id: 'acc-card-history', level1: '회계관리', level2: '카드사용내역' },
|
|||
|
|
{ id: 'acc-tax', level1: '회계관리', level2: '세금계산서관리' },
|
|||
|
|
{ id: 'acc-receivable', level1: '회계관리', level2: '미수금현황' },
|
|||
|
|
{ id: 'acc-vendor-ledger', level1: '회계관리', level2: '거래처원장' },
|
|||
|
|
{ id: 'material-stock', level1: '자재관리', level2: '재고현황' },
|
|||
|
|
// NO_BUTTON 페이지도 재확인 - 실제 어떤 버튼들이 있는지
|
|||
|
|
{ id: 'acc-purchase', level1: '회계관리', level2: '매입관리', scanOnly: true },
|
|||
|
|
{ id: 'acc-expense', level1: '회계관리', level2: '지출예상내역서', scanOnly: true },
|
|||
|
|
{ id: 'acc-payment', level1: '회계관리', level2: '결제내역', scanOnly: true },
|
|||
|
|
{ id: 'acc-sales', level1: '회계관리', level2: '매출관리', scanOnly: true },
|
|||
|
|
{ id: 'acc-withdrawal', level1: '회계관리', level2: '출금관리', scanOnly: true },
|
|||
|
|
{ id: 'hr-attendance', level1: '인사관리', level2: '근태현황', scanOnly: true },
|
|||
|
|
{ id: 'purchase-status', level1: '구매관리', level2: '구매현황', scanOnly: true },
|
|||
|
|
{ id: 'sales-pricing', level1: '판매관리', level2: '단가관리', scanOnly: true },
|
|||
|
|
{ id: 'sales-client', level1: '판매관리', level2: '거래처관리', scanOnly: true },
|
|||
|
|
{ id: 'sales-quotation', level1: '판매관리', level2: '견적관리', scanOnly: true },
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
async function main() {
|
|||
|
|
[DOWNLOAD_DIR].forEach(d => {
|
|||
|
|
if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true });
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
console.log(C.bold('\n╔══════════════════════════════════════════════════╗'));
|
|||
|
|
console.log(C.bold('║ 🔍 다운로드 버튼 상세 디버깅 ║'));
|
|||
|
|
console.log(C.bold('╚══════════════════════════════════════════════════╝\n'));
|
|||
|
|
|
|||
|
|
const browser = await chromium.launch({
|
|||
|
|
headless: false,
|
|||
|
|
args: ['--window-position=1920,0', '--window-size=1920,1080'],
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const context = await browser.newContext({
|
|||
|
|
viewport: { width: 1920, height: 1080 },
|
|||
|
|
locale: 'ko-KR',
|
|||
|
|
acceptDownloads: true,
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const page = await context.newPage();
|
|||
|
|
|
|||
|
|
// 로그인
|
|||
|
|
console.log(C.cyan('🔐 로그인...'));
|
|||
|
|
await page.goto(`${BASE_URL}/login`, { waitUntil: 'networkidle', timeout: 30000 });
|
|||
|
|
await page.fill('input[type="text"], input[name="username"], #username', AUTH.username);
|
|||
|
|
await page.fill('input[type="password"], input[name="password"], #password', AUTH.password);
|
|||
|
|
await page.click('button[type="submit"]');
|
|||
|
|
await page.waitForURL('**/dashboard**', { timeout: 15000 });
|
|||
|
|
console.log(C.green('✅ 로그인 성공\n'));
|
|||
|
|
|
|||
|
|
const allResults = [];
|
|||
|
|
|
|||
|
|
for (let i = 0; i < DEBUG_TARGETS.length; i++) {
|
|||
|
|
const target = DEBUG_TARGETS[i];
|
|||
|
|
console.log(C.bold(`\n[${ i + 1}/${DEBUG_TARGETS.length}] ${target.level1} > ${target.level2} ${target.scanOnly ? '(버튼 스캔만)' : '(다운로드 디버깅)'}`));
|
|||
|
|
console.log(C.dim('─'.repeat(60)));
|
|||
|
|
|
|||
|
|
// 대시보드로 이동
|
|||
|
|
await page.goto(`${BASE_URL}/dashboard`, { waitUntil: 'domcontentloaded', timeout: 15000 });
|
|||
|
|
await sleep(1000);
|
|||
|
|
|
|||
|
|
// 메뉴 탐색
|
|||
|
|
const navOk = await navigateViaMenu(page, target.level1, target.level2);
|
|||
|
|
if (!navOk) {
|
|||
|
|
console.log(C.red(' ❌ 메뉴 탐색 실패'));
|
|||
|
|
allResults.push({ ...target, status: 'NAV_FAIL' });
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
await sleep(2500);
|
|||
|
|
|
|||
|
|
const currentUrl = page.url();
|
|||
|
|
console.log(C.dim(` 📍 ${currentUrl}`));
|
|||
|
|
|
|||
|
|
if (target.scanOnly) {
|
|||
|
|
// 버튼 스캔만
|
|||
|
|
const buttons = await page.evaluate(() => {
|
|||
|
|
const allBtns = Array.from(document.querySelectorAll('button, a[role="button"], [role="button"]'));
|
|||
|
|
return allBtns
|
|||
|
|
.filter(b => {
|
|||
|
|
const rect = b.getBoundingClientRect();
|
|||
|
|
return rect.width > 0 && rect.height > 0;
|
|||
|
|
})
|
|||
|
|
.map(b => ({
|
|||
|
|
text: (b.innerText?.trim() || '').substring(0, 50),
|
|||
|
|
ariaLabel: b.getAttribute('aria-label') || '',
|
|||
|
|
className: (b.className || '').substring(0, 60),
|
|||
|
|
tagName: b.tagName,
|
|||
|
|
}))
|
|||
|
|
.filter(b => b.text || b.ariaLabel);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 다운로드 관련 버튼 필터
|
|||
|
|
const dlKeywords = ['엑셀', 'Excel', 'excel', '다운로드', 'download', 'Download', 'PDF', 'pdf', 'Export', 'export', '내보내기', '출력', '인쇄', 'CSV', 'csv'];
|
|||
|
|
const dlButtons = buttons.filter(b => {
|
|||
|
|
const combined = `${b.text} ${b.ariaLabel}`;
|
|||
|
|
return dlKeywords.some(kw => combined.includes(kw));
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
if (dlButtons.length > 0) {
|
|||
|
|
console.log(C.yellow(` 📋 다운로드 관련 버튼 발견 ${dlButtons.length}개:`));
|
|||
|
|
dlButtons.forEach(b => console.log(C.yellow(` "${b.text}" (aria: ${b.ariaLabel || 'none'})`)));
|
|||
|
|
} else {
|
|||
|
|
console.log(C.dim(` ℹ️ 다운로드 관련 버튼 없음`));
|
|||
|
|
// 전체 버튼 목록 출력 (디버깅용)
|
|||
|
|
console.log(C.dim(` 전체 버튼 ${buttons.length}개: ${buttons.slice(0, 10).map(b => `"${b.text}"`).join(', ')}${buttons.length > 10 ? '...' : ''}`));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
allResults.push({ ...target, status: dlButtons.length > 0 ? 'HAS_BUTTON' : 'NO_BUTTON', buttons: dlButtons, allButtons: buttons.slice(0, 15) });
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ───── 다운로드 디버깅 (FAIL 페이지) ─────
|
|||
|
|
// 1. 다운로드 버튼 찾기
|
|||
|
|
const dlButtons = await page.evaluate(() => {
|
|||
|
|
const kw = ['엑셀', 'Excel', 'excel', '다운로드', 'download', 'PDF', 'pdf', 'Export', 'export', '내보내기', '출력', 'CSV'];
|
|||
|
|
const allBtns = Array.from(document.querySelectorAll('button, a[role="button"], [role="button"], a'));
|
|||
|
|
const found = [];
|
|||
|
|
for (const btn of allBtns) {
|
|||
|
|
const text = btn.innerText?.trim() || '';
|
|||
|
|
const ariaLabel = btn.getAttribute('aria-label') || '';
|
|||
|
|
const href = btn.getAttribute('href') || '';
|
|||
|
|
const combined = `${text} ${ariaLabel} ${href}`;
|
|||
|
|
const rect = btn.getBoundingClientRect();
|
|||
|
|
if (rect.width <= 0 || rect.height <= 0) continue;
|
|||
|
|
if (kw.some(k => combined.includes(k))) {
|
|||
|
|
found.push({
|
|||
|
|
text: text.substring(0, 50),
|
|||
|
|
ariaLabel,
|
|||
|
|
href,
|
|||
|
|
tagName: btn.tagName,
|
|||
|
|
disabled: btn.disabled || btn.getAttribute('aria-disabled') === 'true',
|
|||
|
|
index: allBtns.indexOf(btn),
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return found;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
if (dlButtons.length === 0) {
|
|||
|
|
console.log(C.yellow(' ⚠️ 다운로드 버튼 없음'));
|
|||
|
|
allResults.push({ ...target, status: 'NO_BUTTON' });
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for (const btn of dlButtons) {
|
|||
|
|
console.log(C.cyan(` 🔘 버튼: "${btn.text}" (tag: ${btn.tagName}, disabled: ${btn.disabled})`));
|
|||
|
|
|
|||
|
|
if (btn.disabled) {
|
|||
|
|
console.log(C.yellow(` ⚠️ 버튼 비활성화 상태`));
|
|||
|
|
allResults.push({ ...target, status: 'DISABLED', button: btn.text });
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 2. 네트워크 요청 감시 시작
|
|||
|
|
const networkLogs = [];
|
|||
|
|
const reqHandler = req => {
|
|||
|
|
const url = req.url();
|
|||
|
|
if (!url.includes('_next/static') && !url.includes('favicon') && !url.includes('chunk')) {
|
|||
|
|
networkLogs.push({ type: 'request', method: req.method(), url: url.substring(0, 120) });
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
const resHandler = res => {
|
|||
|
|
const url = res.url();
|
|||
|
|
if (!url.includes('_next/static') && !url.includes('favicon') && !url.includes('chunk')) {
|
|||
|
|
networkLogs.push({
|
|||
|
|
type: 'response', status: res.status(), url: url.substring(0, 120),
|
|||
|
|
contentType: res.headers()['content-type']?.substring(0, 50) || '',
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
page.on('request', reqHandler);
|
|||
|
|
page.on('response', resHandler);
|
|||
|
|
|
|||
|
|
// Console 로그 감시
|
|||
|
|
const consoleLogs = [];
|
|||
|
|
const consoleHandler = msg => consoleLogs.push(`[${msg.type()}] ${msg.text().substring(0, 100)}`);
|
|||
|
|
page.on('console', consoleHandler);
|
|||
|
|
|
|||
|
|
// 다운로드 이벤트 감시
|
|||
|
|
let downloadEvent = null;
|
|||
|
|
const downloadHandler = dl => { downloadEvent = dl; };
|
|||
|
|
page.on('download', downloadHandler);
|
|||
|
|
|
|||
|
|
// 팝업 감시
|
|||
|
|
let popupPage = null;
|
|||
|
|
const popupHandler = p => { popupPage = p; };
|
|||
|
|
context.on('page', popupHandler);
|
|||
|
|
|
|||
|
|
// 3. 버튼 클릭
|
|||
|
|
await page.evaluate((idx) => {
|
|||
|
|
const allBtns = Array.from(document.querySelectorAll('button, a[role="button"], [role="button"], a'));
|
|||
|
|
if (allBtns[idx]) allBtns[idx].click();
|
|||
|
|
}, btn.index);
|
|||
|
|
|
|||
|
|
// 4. 8초 대기
|
|||
|
|
await sleep(8000);
|
|||
|
|
|
|||
|
|
// 5. 다운로드 처리
|
|||
|
|
if (downloadEvent) {
|
|||
|
|
const fname = downloadEvent.suggestedFilename();
|
|||
|
|
const savePath = path.join(DOWNLOAD_DIR, `${target.id}_${fname}`);
|
|||
|
|
await downloadEvent.saveAs(savePath);
|
|||
|
|
const stat = fs.statSync(savePath);
|
|||
|
|
console.log(C.green(` ✅ 다운로드 성공: ${fname} (${(stat.size / 1024).toFixed(1)}KB)`));
|
|||
|
|
allResults.push({ ...target, status: 'PASS', button: btn.text, file: fname, size: stat.size });
|
|||
|
|
} else if (popupPage) {
|
|||
|
|
console.log(C.yellow(` 📋 새 탭/팝업 열림: ${popupPage.url().substring(0, 80)}`));
|
|||
|
|
try { await popupPage.close(); } catch {}
|
|||
|
|
allResults.push({ ...target, status: 'POPUP', button: btn.text, popupUrl: popupPage.url() });
|
|||
|
|
} else {
|
|||
|
|
// 네트워크 로그 분석
|
|||
|
|
const apiCalls = networkLogs.filter(l => l.type === 'response' && !l.url.includes('_next'));
|
|||
|
|
console.log(C.dim(` 네트워크 응답 ${apiCalls.length}건:`));
|
|||
|
|
apiCalls.forEach(l => {
|
|||
|
|
const icon = l.status >= 200 && l.status < 300 ? '🟢' : l.status >= 400 ? '🔴' : '🟡';
|
|||
|
|
console.log(C.dim(` ${icon} ${l.status} ${l.url} [${l.contentType}]`));
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Blob URL 또는 토스트 확인
|
|||
|
|
const blobCheck = await page.evaluate(() => {
|
|||
|
|
const toast = document.querySelector('[class*="toast"], [class*="Toastify"], [role="alert"]');
|
|||
|
|
const anchors = Array.from(document.querySelectorAll('a')).filter(a => a.href?.startsWith('blob:'));
|
|||
|
|
return {
|
|||
|
|
toast: toast?.innerText?.trim() || null,
|
|||
|
|
blobUrls: anchors.map(a => a.href),
|
|||
|
|
};
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
if (blobCheck.toast) console.log(C.yellow(` 📌 토스트: "${blobCheck.toast}"`));
|
|||
|
|
if (blobCheck.blobUrls.length > 0) console.log(C.cyan(` 📎 Blob URL 발견: ${blobCheck.blobUrls.length}개`));
|
|||
|
|
|
|||
|
|
if (consoleLogs.length > 0) {
|
|||
|
|
const interesting = consoleLogs.filter(l => !l.includes('[info]') || l.includes('download') || l.includes('export') || l.includes('error'));
|
|||
|
|
if (interesting.length > 0) {
|
|||
|
|
console.log(C.dim(` Console: ${interesting.slice(0, 3).join('; ')}`));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 서버 액션 POST 확인
|
|||
|
|
const serverActions = networkLogs.filter(l => l.type === 'request' && l.method === 'POST');
|
|||
|
|
if (serverActions.length > 0) {
|
|||
|
|
console.log(C.yellow(` 📤 POST 요청 ${serverActions.length}건:`));
|
|||
|
|
serverActions.forEach(l => console.log(C.dim(` ${l.url}`)));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
allResults.push({ ...target, status: 'NO_DOWNLOAD', button: btn.text, network: apiCalls, serverActions });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 리스너 해제
|
|||
|
|
page.off('request', reqHandler);
|
|||
|
|
page.off('response', resHandler);
|
|||
|
|
page.off('console', consoleHandler);
|
|||
|
|
page.off('download', downloadHandler);
|
|||
|
|
context.off('page', popupHandler);
|
|||
|
|
|
|||
|
|
// 모달 닫기
|
|||
|
|
await page.evaluate(async () => {
|
|||
|
|
for (let i = 0; i < 3; i++) {
|
|||
|
|
const modal = document.querySelector("[role='dialog'], [aria-modal='true'], [class*='modal']:not([class*='tooltip'])");
|
|||
|
|
if (!modal || modal.getBoundingClientRect().width === 0) break;
|
|||
|
|
const closeBtn = modal.querySelector("button[class*='close'], [aria-label='닫기']")
|
|||
|
|
|| Array.from(modal.querySelectorAll('button')).find(b => ['닫기', 'Close', '취소', '확인'].some(t => b.innerText?.includes(t)));
|
|||
|
|
if (closeBtn) closeBtn.click();
|
|||
|
|
else document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', keyCode: 27, bubbles: true }));
|
|||
|
|
await new Promise(r => setTimeout(r, 500));
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
await sleep(500);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─── 결과 요약 ─────────────────────────────────────────────
|
|||
|
|
console.log(C.bold('\n══════════════════════════════════════════════════'));
|
|||
|
|
console.log(C.bold('📊 다운로드 디버깅 결과 요약'));
|
|||
|
|
console.log(C.bold('══════════════════════════════════════════════════\n'));
|
|||
|
|
|
|||
|
|
const pass = allResults.filter(r => r.status === 'PASS');
|
|||
|
|
const noBtn = allResults.filter(r => r.status === 'NO_BUTTON');
|
|||
|
|
const hasBtn = allResults.filter(r => r.status === 'HAS_BUTTON');
|
|||
|
|
const noDl = allResults.filter(r => r.status === 'NO_DOWNLOAD');
|
|||
|
|
const disabled = allResults.filter(r => r.status === 'DISABLED');
|
|||
|
|
|
|||
|
|
console.log(` ${C.green(`✅ 다운로드 성공: ${pass.length}개`)} ${pass.map(r => r.id).join(', ')}`);
|
|||
|
|
console.log(` ${C.red(`❌ 버튼 있으나 다운로드 안됨: ${noDl.length}개`)} ${noDl.map(r => r.id).join(', ')}`);
|
|||
|
|
console.log(` ${C.yellow(`⚠️ 버튼 비활성화: ${disabled.length}개`)}`);
|
|||
|
|
console.log(` ${C.yellow(`🔘 다운로드 버튼 발견(스캔): ${hasBtn.length}개`)} ${hasBtn.map(r => `${r.id}(${r.buttons.map(b=>b.text).join(',')})`).join(', ')}`);
|
|||
|
|
console.log(` ${C.dim(`⏭️ 버튼 없음: ${noBtn.length}개`)} ${noBtn.map(r => r.id).join(', ')}`);
|
|||
|
|
|
|||
|
|
// 리포트 저장
|
|||
|
|
const ts = getTimestamp();
|
|||
|
|
const reportLines = ['# 🔍 다운로드 디버깅 상세 리포트\n', `**실행 시간**: ${ts}\n`];
|
|||
|
|
|
|||
|
|
reportLines.push('## 카테고리별 결과\n');
|
|||
|
|
|
|||
|
|
if (pass.length > 0) {
|
|||
|
|
reportLines.push('### ✅ 다운로드 성공\n');
|
|||
|
|
pass.forEach(r => reportLines.push(`- **${r.level1} > ${r.level2}**: ${r.button} → ${r.file} (${(r.size/1024).toFixed(1)}KB)\n`));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (noDl.length > 0) {
|
|||
|
|
reportLines.push('\n### ❌ 버튼 있으나 다운로드 미동작\n');
|
|||
|
|
noDl.forEach(r => {
|
|||
|
|
reportLines.push(`- **${r.level1} > ${r.level2}**: "${r.button}"\n`);
|
|||
|
|
if (r.network?.length > 0) {
|
|||
|
|
reportLines.push(` - 네트워크 응답: ${r.network.map(n => `${n.status} ${n.url}`).join('; ')}\n`);
|
|||
|
|
}
|
|||
|
|
if (r.serverActions?.length > 0) {
|
|||
|
|
reportLines.push(` - Server Actions: ${r.serverActions.map(s => s.url).join('; ')}\n`);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (hasBtn.length > 0) {
|
|||
|
|
reportLines.push('\n### 🔘 버튼 발견 (스캔 결과)\n');
|
|||
|
|
hasBtn.forEach(r => {
|
|||
|
|
reportLines.push(`- **${r.level1} > ${r.level2}**: ${r.buttons.map(b => `"${b.text}"`).join(', ')}\n`);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (noBtn.length > 0) {
|
|||
|
|
reportLines.push('\n### ⏭️ 다운로드 버튼 없음\n');
|
|||
|
|
noBtn.forEach(r => {
|
|||
|
|
const sample = r.allButtons?.slice(0, 8).map(b => `"${b.text}"`).join(', ') || 'N/A';
|
|||
|
|
reportLines.push(`- **${r.level1} > ${r.level2}**: 주요 버튼: ${sample}\n`);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const reportPath = path.join(RESULTS_DIR, `Download-Debug_${ts}.md`);
|
|||
|
|
fs.writeFileSync(reportPath, reportLines.join(''), 'utf-8');
|
|||
|
|
console.log(C.cyan(`\n📄 리포트: ${reportPath}`));
|
|||
|
|
|
|||
|
|
await browser.close();
|
|||
|
|
console.log(C.dim('🔒 브라우저 닫힘\n'));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─── 사이드바 메뉴 탐색 ─────────────────────────────────────
|
|||
|
|
async function navigateViaMenu(page, level1, level2) {
|
|||
|
|
try {
|
|||
|
|
await page.evaluate(() => {
|
|||
|
|
const sidebar = document.querySelector('.sidebar-scroll, [class*="sidebar"], nav');
|
|||
|
|
if (sidebar) sidebar.scrollTo({ top: 0, behavior: 'instant' });
|
|||
|
|
});
|
|||
|
|
await sleep(300);
|
|||
|
|
|
|||
|
|
for (let scroll = 0; scroll < 15; scroll++) {
|
|||
|
|
const found = await page.evaluate((l1) => {
|
|||
|
|
const btns = Array.from(document.querySelectorAll('button, [role="button"], a'));
|
|||
|
|
const btn = btns.find(b => b.innerText?.trim().startsWith(l1));
|
|||
|
|
if (btn) { btn.click(); return true; }
|
|||
|
|
return false;
|
|||
|
|
}, level1);
|
|||
|
|
|
|||
|
|
if (found) {
|
|||
|
|
await sleep(500);
|
|||
|
|
const nav2 = await page.evaluate((l2) => {
|
|||
|
|
const items = Array.from(document.querySelectorAll('a, button'));
|
|||
|
|
const item = items.find(el => el.innerText?.trim() === l2);
|
|||
|
|
if (item) { item.click(); return true; }
|
|||
|
|
return false;
|
|||
|
|
}, level2);
|
|||
|
|
if (nav2) {
|
|||
|
|
await sleep(2000);
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
await page.evaluate(() => {
|
|||
|
|
const sidebar = document.querySelector('.sidebar-scroll, [class*="sidebar"], nav');
|
|||
|
|
if (sidebar) sidebar.scrollBy({ top: 150, behavior: 'instant' });
|
|||
|
|
});
|
|||
|
|
await sleep(100);
|
|||
|
|
}
|
|||
|
|
return false;
|
|||
|
|
} catch { return false; }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
main().catch(err => {
|
|||
|
|
console.error(C.red(`💥 오류: ${err.message}`));
|
|||
|
|
process.exit(1);
|
|||
|
|
});
|