#!/usr/bin/env node /** * Search Audit Data Collector * Visits all pages and collects search/filter UI detection results * Outputs a structured JSON report */ 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 PAGES = [ { l1: '회계관리', l2: '거래처관리' }, { l1: '회계관리', l2: '입금관리' }, { l1: '회계관리', l2: '출금관리' }, { l1: '회계관리', l2: '어음관리' }, { l1: '회계관리', l2: '악성채권추심관리' }, { l1: '회계관리', l2: '입출금계좌조회' }, { l1: '회계관리', l2: '카드내역조회' }, { l1: '회계관리', l2: '매입관리' }, { l1: '회계관리', l2: '매출관리' }, { l1: '회계관리', l2: '미수금현황' }, { l1: '회계관리', l2: '지출예상내역서' }, { l1: '회계관리', l2: '결제내역' }, { l1: '회계관리', l2: '거래처원장' }, { l1: '인사관리', l2: '사원관리' }, { l1: '인사관리', l2: '부서관리' }, { l1: '인사관리', l2: '급여관리' }, { l1: '인사관리', l2: '근태관리' }, { l1: '인사관리', l2: '근태현황' }, { l1: '인사관리', l2: '카드관리' }, { l1: '인사관리', l2: '휴가관리' }, { l1: '생산관리', l2: '품목관리' }, { l1: '생산관리', l2: '생산 현황판' }, { l1: '생산관리', l2: '작업자 화면' }, { l1: '생산관리', l2: '작업지시 관리' }, { l1: '생산관리', l2: '작업실적' }, { l1: '품목관리', l2: '품목기준관리' }, { l1: '품질관리', l2: '품질인정심사 시스템' }, { l1: '품질관리', l2: '제품검사관리' }, { l1: '자재관리', l2: '재고현황' }, { l1: '자재관리', l2: '입고관리' }, { l1: '판매관리', l2: '거래처관리' }, { l1: '판매관리', l2: '수주관리' }, { l1: '판매관리', l2: '단가관리' }, { l1: '판매관리', l2: '견적관리' }, { l1: '출고관리', l2: '출고관리' }, { l1: '결재관리', l2: '결재함' }, { l1: '결재관리', l2: '기안함' }, { l1: '결재관리', l2: '참조함' }, { l1: '게시판', l2: '자유게시판' }, { l1: '게시판', l2: '게시판 관리' }, { l1: '고객센터', l2: '공지사항' }, { l1: '고객센터', l2: 'FAQ' }, { l1: '고객센터', l2: '이벤트 게시판' }, { l1: '설정', l2: '회사정보' }, { l1: '설정', l2: '계정정보' }, { l1: '설정', l2: '근태설정' }, { l1: '설정', l2: '계좌관리' }, { l1: '설정', l2: '알림설정' }, { l1: '설정', l2: '권한관리' }, { l1: '설정', l2: '팝업관리' }, { l1: '설정', l2: '직책관리' }, { l1: '설정', l2: '직급관리' }, { l1: '설정', l2: '구독관리' }, { l1: '설정', l2: '휴가정책' }, { l1: '설정', l2: '근무일정' }, ]; const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); const AUDIT_SCRIPT = `(async()=>{ const R={p:location.pathname,url:location.href}; const ss=['input[type="search"]','input[placeholder*="검색"]','input[placeholder*="Search"]','input[role="searchbox"]','[class*="search"] input']; let si=null; for(const s of ss){si=document.querySelector(s);if(si)break;} if(!si) si=Array.from(document.querySelectorAll('input[type="text"],input:not([type])')).find(i=>/검색|search|조회/i.test(i.placeholder||'')); R.hasSearch=!!si; R.searchPlaceholder=si?(si.placeholder||'').substring(0,60):''; R.filters=document.querySelectorAll('button[role="combobox"],select').length; R.filterTexts=Array.from(document.querySelectorAll('button[role="combobox"]')).map(f=>(f.innerText||'').trim().substring(0,20)).filter(Boolean); R.tabs=document.querySelectorAll('[role="tab"],[role="tablist"] button').length; const rc=()=>document.querySelectorAll('table tbody tr,[role="row"]').length; R.rows=rc(); R.hasTable=R.rows>0||!!document.querySelector('table,[role="grid"]'); if(si&&R.rows>0){ const ns=Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value')?.set; if(ns)ns.call(si,'zzz_no_match_e2e');else si.value='zzz_no_match_e2e'; si.dispatchEvent(new Event('input',{bubbles:true})); si.dispatchEvent(new Event('change',{bubbles:true})); si.dispatchEvent(new KeyboardEvent('keydown',{key:'Enter',keyCode:13,bubbles:true})); const sb=document.querySelector('button[class*="search"],button[aria-label*="검색"]'); if(sb)sb.click(); await new Promise(w=>setTimeout(w,1500)); R.afterRows=rc(); R.searchWorked=R.afterRowssetTimeout(w,800)); } return R; })()`; async function navigateViaMenu(page, level1, level2) { // Collapse all await page.evaluate(() => { const btn = Array.from(document.querySelectorAll('button')).find(b => b.innerText?.trim() === '모두 접기'); if (btn) btn.click(); }); await sleep(300); // Scroll top await page.evaluate(() => { const sidebar = document.querySelector('.sidebar-scroll, [data-sidebar="content"], nav'); if (sidebar) sidebar.scrollTo({ top: 0, behavior: 'instant' }); }); await sleep(300); // Click L1 const l1Found = await page.evaluate(async ({ l1Text }) => { const sidebar = document.querySelector('.sidebar-scroll, [data-sidebar="content"], nav'); for (let i = 0; i < 20; i++) { const items = Array.from(document.querySelectorAll('a, button, [role="button"], [role="menuitem"], [role="treeitem"]')); const match = items.find(el => { const text = (el.textContent || el.innerText || '').trim(); return text && (text === l1Text || text.startsWith(l1Text)); }); if (match) { match.scrollIntoView({ behavior: 'instant', block: 'center' }); await new Promise(r => setTimeout(r, 100)); match.click(); return true; } if (sidebar) { sidebar.scrollBy({ top: 150, behavior: 'instant' }); await new Promise(r => setTimeout(r, 100)); } } return false; }, { l1Text: level1 }); if (!l1Found) return false; await sleep(500); if (level2) { const l2Found = await page.evaluate(async ({ l2Text }) => { const sidebar = document.querySelector('.sidebar-scroll, [data-sidebar="content"], nav'); for (let i = 0; i < 15; i++) { const items = Array.from(document.querySelectorAll('a, button, [role="button"], [role="menuitem"], [role="treeitem"]')); let match = items.find(el => (el.textContent || el.innerText || '').trim() === l2Text); if (!match) match = items.find(el => (el.textContent || el.innerText || '').trim().includes(l2Text)); if (match) { match.scrollIntoView({ behavior: 'instant', block: 'center' }); await new Promise(r => setTimeout(r, 100)); match.click(); return true; } if (sidebar) { sidebar.scrollBy({ top: 100, behavior: 'instant' }); await new Promise(r => setTimeout(r, 100)); } } return false; }, { l2Text: level2 }); if (!l2Found) return false; await sleep(2000); } return true; } (async () => { 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' }); const page = await context.newPage(); // Login await page.goto(`${BASE_URL}/ko/login`, { waitUntil: 'domcontentloaded', timeout: 20000 }); await page.fill('#userId', AUTH.username); await page.fill('#password', AUTH.password); await page.click("button[type='submit']"); await sleep(3000); console.log('Logged in'); const results = []; let failed = 0; for (let i = 0; i < PAGES.length; i++) { const pg = PAGES[i]; const label = `${pg.l1} > ${pg.l2}`; process.stdout.write(`(${i + 1}/${PAGES.length}) ${label} ... `); try { // Go to dashboard await page.goto(`${BASE_URL}/dashboard`, { waitUntil: 'load', timeout: 10000 }); await sleep(1500); // Navigate const navOk = await navigateViaMenu(page, pg.l1, pg.l2); if (!navOk) { console.log('NAV FAIL'); results.push({ menu: label, error: 'navigation_failed' }); failed++; continue; } // Run audit const data = await page.evaluate(AUDIT_SCRIPT); data.menu = label; results.push(data); const status = []; if (data.hasSearch) status.push(`검색:${data.searchWorked === true ? '✅' : data.searchWorked === false ? '❌' : '⬜'}`); else status.push('검색:없음'); if (data.filters > 0) status.push(`필터:${data.filters}`); if (data.rows > 0) status.push(`행:${data.rows}`); if (data.tabs > 0) status.push(`탭:${data.tabs}`); console.log(status.join(' | ')); } catch (err) { console.log(`ERROR: ${err.message.substring(0, 60)}`); results.push({ menu: label, error: err.message }); failed++; } } await browser.close(); // Generate report const ts = (() => { const n = new Date(); const p = v => v.toString().padStart(2, '0'); return `${n.getFullYear()}-${p(n.getMonth() + 1)}-${p(n.getDate())}_${p(n.getHours())}-${p(n.getMinutes())}-${p(n.getSeconds())}`; })(); // Summary const withSearch = results.filter(r => r.hasSearch); const searchWorked = results.filter(r => r.searchWorked === true); const searchFailed = results.filter(r => r.searchWorked === false); const noSearch = results.filter(r => !r.hasSearch && !r.error); const withFilters = results.filter(r => (r.filters || 0) > 0); const withTable = results.filter(r => r.hasTable); const errors = results.filter(r => r.error); let md = `# 검색 기능 전체 탐색 감사 리포트 **실행**: ${ts} | **총 페이지**: ${PAGES.length} | **에러**: ${errors.length} ## 요약 | 항목 | 수 | 비율 | |------|-----|------| | 검색 입력 있음 | ${withSearch.length} | ${Math.round(withSearch.length / PAGES.length * 100)}% | | 검색 동작 확인 | ${searchWorked.length} | ${withSearch.length > 0 ? Math.round(searchWorked.length / withSearch.length * 100) : 0}% | | 검색 미동작 | ${searchFailed.length} | ${withSearch.length > 0 ? Math.round(searchFailed.length / withSearch.length * 100) : 0}% | | 검색 없음 | ${noSearch.length} | ${Math.round(noSearch.length / PAGES.length * 100)}% | | 필터 있음 | ${withFilters.length} | ${Math.round(withFilters.length / PAGES.length * 100)}% | | 테이블 있음 | ${withTable.length} | ${Math.round(withTable.length / PAGES.length * 100)}% | | 탐색 에러 | ${errors.length} | ${Math.round(errors.length / PAGES.length * 100)}% | ## 전체 페이지별 상세 | # | 메뉴 | URL | 검색 | 필터 | 탭 | 테이블행 | 검색동작 | 비고 | |---|------|-----|------|------|-----|---------|---------|------| `; results.forEach((r, idx) => { if (r.error) { md += `| ${idx + 1} | ${r.menu} | - | - | - | - | - | - | ❌ ${r.error.substring(0, 40)} |\n`; } else { const searchIcon = r.hasSearch ? '✅' : '❌'; const workedIcon = r.searchWorked === true ? '✅ 동작' : r.searchWorked === false ? '⚠️ 미동작' : r.hasSearch ? '⬜ 테이블없음' : '-'; const rowInfo = r.hasTable ? `${r.rows}${r.afterRows !== undefined ? `→${r.afterRows}` : ''}` : '없음'; const note = r.searchPlaceholder || ''; md += `| ${idx + 1} | ${r.menu} | ${(r.p || '').substring(0, 35)} | ${searchIcon} | ${r.filters || 0} | ${r.tabs || 0} | ${rowInfo} | ${workedIcon} | ${note.substring(0, 30)} |\n`; } }); if (searchFailed.length > 0) { md += `\n## ⚠️ 검색 미동작 페이지 (상세)\n\n`; searchFailed.forEach(r => { md += `### ${r.menu}\n`; md += `- URL: ${r.url || r.p}\n`; md += `- 검색 placeholder: ${r.searchPlaceholder}\n`; md += `- 검색 전 행: ${r.rows} → 검색 후 행: ${r.afterRows}\n`; md += `- 필터: ${r.filters}개, 탭: ${r.tabs}개\n\n`; }); } if (noSearch.length > 0) { md += `\n## 검색 입력 없는 페이지\n\n`; noSearch.forEach(r => { md += `- ${r.menu} (${r.p}) - 필터: ${r.filters || 0}, 탭: ${r.tabs || 0}, 테이블: ${r.hasTable ? r.rows + '행' : '없음'}\n`; }); } const reportPath = path.join(SAM_ROOT, 'e2e', 'results', 'hotfix', `Search-Audit-Report_${ts}.md`); fs.writeFileSync(reportPath, md, 'utf-8'); console.log(`\n=== 감사 완료 ===`); console.log(`총: ${PAGES.length} | 검색있음: ${withSearch.length} | 동작: ${searchWorked.length} | 미동작: ${searchFailed.length} | 에러: ${errors.length}`); console.log(`리포트: ${reportPath}`); })();