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