/** * 검색 버튼 유무 전체 감사 * 검색 입력란이 있는 모든 페이지에서 검색 버튼 존재 여부를 확인한다. */ 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 = 'https://dev.codebridge-x.com'; // 검색 입력란이 있는 38개 페이지 (이전 감사 결과 기준) const PAGES = [ { name: '거래처관리', l1: '회계관리', l2: '거래처관리' }, { name: '입금관리', l1: '회계관리', l2: '입금관리' }, { name: '출금관리', l1: '회계관리', l2: '출금관리' }, { name: '어음관리', l1: '회계관리', l2: '어음관리' }, { name: '악성채권추심관리', l1: '회계관리', l2: '악성채권추심관리' }, { name: '입출금계좌조회', l1: '회계관리', l2: '입출금계좌조회' }, { name: '카드내역조회', l1: '회계관리', l2: '카드내역조회' }, { name: '매입관리', l1: '회계관리', l2: '매입관리' }, { name: '매출관리', l1: '회계관리', l2: '매출관리' }, { name: '미수금현황', l1: '회계관리', l2: '미수금현황' }, { name: '지출예상내역서', l1: '회계관리', l2: '지출예상내역서' }, { name: '거래처원장', l1: '회계관리', l2: '거래처원장' }, { name: '사원관리', l1: '인사관리', l2: '사원관리' }, { name: '부서관리', l1: '인사관리', l2: '부서관리' }, { name: '급여관리', l1: '인사관리', l2: '급여관리' }, { name: '근태관리', l1: '인사관리', l2: '근태관리' }, { name: '카드관리', l1: '인사관리', l2: '카드관리' }, { name: '휴가관리', l1: '인사관리', l2: '휴가관리' }, { name: '작업지시 관리', l1: '생산관리', l2: '작업지시 관리' }, { name: '작업실적', l1: '생산관리', l2: '작업실적' }, { name: '재고현황', l1: '자재관리', l2: '재고현황' }, { name: '입고관리', l1: '자재관리', l2: '입고관리' }, { name: '제품검사관리', l1: '품질관리', l2: '제품검사관리' }, { name: '판매거래처관리', l1: '판매관리', l2: '거래처관리' }, { name: '수주관리', l1: '판매관리', l2: '수주관리' }, { name: '단가관리', l1: '판매관리', l2: '단가관리' }, { name: '견적관리', l1: '판매관리', l2: '견적관리' }, { name: '결재함', l1: '결재관리', l2: '결재함' }, { name: '기안함', l1: '결재관리', l2: '기안함' }, { name: '참조함', l1: '결재관리', l2: '참조함' }, { name: '자유게시판', l1: '게시판', l2: '자유게시판' }, { name: '게시판 관리', l1: '게시판', l2: '게시판 관리' }, { name: '공지사항', l1: '고객센터', l2: '공지사항' }, { name: 'FAQ', l1: '고객센터', l2: 'FAQ' }, { name: '이벤트 게시판', l1: '고객센터', l2: '이벤트 게시판' }, { name: '계좌관리', l1: '설정', l2: '계좌관리' }, { name: '권한관리', l1: '설정', l2: '권한관리' }, { name: '팝업관리', l1: '설정', l2: '팝업관리' }, ]; const AUDIT_SCRIPT = `(async () => { const R = { url: location.pathname }; // 검색 입력란 탐색 const si = ['input[type="search"]','input[placeholder*="검색"]','input[placeholder*="Search"]','input[role="searchbox"]','[class*="search"] input','[class*="Search"] input']; let searchEl = null; for (const s of si) { searchEl = document.querySelector(s); if (searchEl) break; } R.hasSearchInput = !!searchEl; R.placeholder = searchEl?.placeholder || ''; // 검색 버튼 탐색 (여러 패턴) const allBtns = Array.from(document.querySelectorAll('button, a[role="button"], [role="button"]')); // 1) 텍스트 기반 검색 버튼 const textSearchBtn = allBtns.find(b => { const t = b.innerText?.trim(); return t === '검색' || t === '조회' || t === 'Search'; }); // 2) 아이콘 기반 검색 버튼 (돋보기 SVG) const iconSearchBtn = document.querySelector( 'button svg[class*="search"], button svg[class*="Search"], button svg[class*="magnif"], button [class*="lucide-search"], button [data-icon="search"]' )?.closest('button'); // 3) aria-label 기반 const ariaSearchBtn = allBtns.find(b => { const label = b.getAttribute('aria-label') || ''; return label.includes('검색') || label.includes('search') || label.includes('Search'); }); // 4) 검색 입력란 옆의 버튼 (sibling/parent 기준) let adjacentBtn = null; if (searchEl) { const parent = searchEl.closest('div, form, header, [class*="toolbar"], [class*="search"]'); if (parent) { const btns = parent.querySelectorAll('button'); adjacentBtn = Array.from(btns).find(b => { const t = b.innerText?.trim(); return !t || t === '검색' || t === '조회' || b.querySelector('svg'); }); } } R.textSearchBtn = textSearchBtn ? textSearchBtn.innerText?.trim().substring(0, 20) : null; R.iconSearchBtn = iconSearchBtn ? (iconSearchBtn.innerText?.trim().substring(0, 20) || '[icon]') : null; R.ariaSearchBtn = ariaSearchBtn ? (ariaSearchBtn.getAttribute('aria-label') || ariaSearchBtn.innerText?.trim()).substring(0, 20) : null; R.adjacentBtn = adjacentBtn ? (adjacentBtn.innerText?.trim().substring(0, 20) || '[icon-btn]') : null; R.hasSearchButton = !!(textSearchBtn || iconSearchBtn || ariaSearchBtn); R.hasAdjacentBtn = !!adjacentBtn; // 초기화 버튼 탐색 const resetBtn = allBtns.find(b => { const t = b.innerText?.trim(); return t === '초기화' || t === '리셋' || t === 'Reset' || t === 'Clear'; }); R.hasResetBtn = !!resetBtn; R.resetBtnText = resetBtn?.innerText?.trim().substring(0, 20) || null; // 테이블 행 수 R.tableRows = document.querySelectorAll('table tbody tr').length; // 검색 트리거 방식 추정 (input에 keydown 핸들러 유무) if (searchEl) { const rk = Object.keys(searchEl).find(k => k.startsWith('__reactProps$')); R.reactProps = {}; if (rk && searchEl[rk]) { R.reactProps.onChange = !!searchEl[rk].onChange; R.reactProps.onKeyDown = !!searchEl[rk].onKeyDown; R.reactProps.onKeyUp = !!searchEl[rk].onKeyUp; R.reactProps.onKeyPress = !!searchEl[rk].onKeyPress; R.reactProps.onSubmit = !!searchEl[rk].onSubmit; } const form = searchEl.closest('form'); R.insideForm = !!form; if (form) { const formRk = Object.keys(form).find(k => k.startsWith('__reactProps$')); R.formOnSubmit = !!(formRk && form[formRk]?.onSubmit); } } return R; })()`; async function navigateViaMenu(page, l1, l2) { return page.evaluate(async ({ l1, l2 }) => { const sidebar = document.querySelector('.sidebar-scroll, nav, [class*="sidebar"], [class*="Sidebar"]'); if (sidebar) sidebar.scrollTo({ top: 0, behavior: 'instant' }); await new Promise(r => setTimeout(r, 300)); for (let i = 0; i < 20; i++) { const btns = Array.from(document.querySelectorAll('button, [role="button"], a')); const l1btn = btns.find(b => b.innerText?.trim().startsWith(l1)); if (l1btn) { l1btn.click(); await new Promise(r => setTimeout(r, 600)); const links = Array.from(document.querySelectorAll('a, button')); const l2link = links.find(el => el.innerText?.trim() === l2); if (l2link) { l2link.click(); await new Promise(r => setTimeout(r, 2000)); return true; } } if (sidebar) sidebar.scrollBy({ top: 150, behavior: 'instant' }); await new Promise(r => setTimeout(r, 150)); } return false; }, { l1, l2 }); } (async () => { const browser = await chromium.launch({ headless: false, args: ['--window-position=1920,0', '--window-size=1920,1080'] }); const ctx = await browser.newContext({ viewport: { width: 1920, height: 1080 }, locale: 'ko-KR' }); const page = await ctx.newPage(); await page.goto(`${BASE}/ko/login`, { waitUntil: 'networkidle', timeout: 30000 }); await page.fill('input[type="text"], input[name="username"], #username', 'TestUser5'); await page.fill('input[type="password"]', 'password123!'); await page.click('button[type="submit"]'); await page.waitForTimeout(3000); console.log('Login OK\n'); const results = []; for (let i = 0; i < PAGES.length; i++) { const pg = PAGES[i]; try { await page.goto(`${BASE}/ko/dashboard`, { waitUntil: 'networkidle', timeout: 10000 }); await page.waitForTimeout(500); await navigateViaMenu(page, pg.l1, pg.l2); await page.waitForTimeout(2000); const r = await page.evaluate(AUDIT_SCRIPT); r.name = pg.name; r.menu = `${pg.l1} > ${pg.l2}`; results.push(r); const btnIcon = r.hasSearchButton ? '✅' : (r.hasAdjacentBtn ? '⚠️' : '❌'); const btnDetail = r.textSearchBtn || r.iconSearchBtn || r.ariaSearchBtn || (r.hasAdjacentBtn ? r.adjacentBtn : '없음'); console.log(`(${i+1}/${PAGES.length}) ${btnIcon} ${pg.name.padEnd(12)} | 검색버튼: ${btnDetail.padEnd(12)} | 초기화: ${r.hasResetBtn ? r.resetBtnText : '없음'} | 행: ${r.tableRows} | onKeyDown: ${r.reactProps?.onKeyDown || false}`); } catch (err) { console.log(`(${i+1}/${PAGES.length}) ⚠️ ${pg.name}: ERROR - ${err.message.substring(0, 60)}`); results.push({ name: pg.name, menu: `${pg.l1} > ${pg.l2}`, error: err.message }); } } // Report const now = new Date(); const pad = n => n.toString().padStart(2, '0'); const ts = `${now.getFullYear()}-${pad(now.getMonth()+1)}-${pad(now.getDate())}_${pad(now.getHours())}-${pad(now.getMinutes())}-${pad(now.getSeconds())}`; const withBtn = results.filter(r => r.hasSearchButton); const withAdjOnly = results.filter(r => !r.hasSearchButton && r.hasAdjacentBtn); const noBtn = results.filter(r => !r.hasSearchButton && !r.hasAdjacentBtn && r.hasSearchInput); const noSearch = results.filter(r => !r.hasSearchInput && !r.error); let md = `# 검색 버튼 유무 전체 감사 리포트\n\n`; md += `**실행**: ${ts} | **대상**: 검색 입력란 보유 ${PAGES.length}개 페이지\n\n`; md += `## 요약\n\n`; md += `| 분류 | 수 | 비율 |\n|------|-----|------|\n`; md += `| 검색 버튼 있음 | ${withBtn.length} | ${Math.round(withBtn.length/PAGES.length*100)}% |\n`; md += `| 인접 버튼만 있음 | ${withAdjOnly.length} | ${Math.round(withAdjOnly.length/PAGES.length*100)}% |\n`; md += `| 검색 버튼 없음 (입력란만) | ${noBtn.length} | ${Math.round(noBtn.length/PAGES.length*100)}% |\n`; md += `| 검색 입력란 자체 없음 | ${noSearch.length} | ${Math.round(noSearch.length/PAGES.length*100)}% |\n\n`; md += `## 전체 페이지별 상세\n\n`; md += `| # | 메뉴 | URL | 검색버튼 | 초기화 | 행수 | onKeyDown | 비고 |\n`; md += `|---|------|-----|---------|--------|------|-----------|------|\n`; results.forEach((r, i) => { if (r.error) { md += `| ${i+1} | ${r.menu} | - | ⚠️ 에러 | - | - | - | ${r.error.substring(0, 30)} |\n`; return; } const btnIcon = r.hasSearchButton ? '✅' : (r.hasAdjacentBtn ? '⚠️' : '❌'); const btnText = r.textSearchBtn || r.iconSearchBtn || r.ariaSearchBtn || (r.hasAdjacentBtn ? `인접: ${r.adjacentBtn}` : '없음'); const kd = r.reactProps?.onKeyDown ? '✅' : '❌'; md += `| ${i+1} | ${r.menu} | ${r.url} | ${btnIcon} ${btnText} | ${r.hasResetBtn ? '✅ '+r.resetBtnText : '❌'} | ${r.tableRows} | ${kd} | ${r.placeholder?.substring(0,25)||''} |\n`; }); if (noBtn.length > 0) { md += `\n## ❌ 검색 버튼 없는 페이지 (검색 입력란만 존재)\n\n`; noBtn.forEach(r => { md += `### ${r.menu}\n`; md += `- URL: ${r.url}\n`; md += `- placeholder: ${r.placeholder}\n`; md += `- 초기화 버튼: ${r.hasResetBtn ? '있음 ('+r.resetBtnText+')' : '없음'}\n`; md += `- React onKeyDown: ${r.reactProps?.onKeyDown || false}\n`; md += `- form 내부: ${r.insideForm || false}\n`; md += `- 테이블 행: ${r.tableRows}\n\n`; }); } const reportPath = path.join('C:/Users/codeb/sam/e2e/results/hotfix', `Search-Button-Audit_${ts}.md`); fs.writeFileSync(reportPath, md, 'utf8'); console.log(`\n${'='.repeat(60)}`); console.log(`검색 버튼 있음: ${withBtn.length}개`); console.log(`인접 버튼만: ${withAdjOnly.length}개`); console.log(`검색 버튼 없음: ${noBtn.length}개`); console.log(`검색 입력 없음: ${noSearch.length}개`); console.log(`Report: ${reportPath}`); await browser.close(); })();