Files
sam-hotfix/e2e/runner/search-button-audit.js

253 lines
12 KiB
JavaScript
Raw Permalink Normal View History

/**
* 검색 버튼 유무 전체 감사
* 검색 입력란이 있는 모든 페이지에서 검색 버튼 존재 여부를 확인한다.
*/
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();
})();