{ "id": "search-bug-draft", "name": "기안함 검색 버그 상세 검증", "description": "결재관리 > 기안함 검색 기능 미동작 버그 상세 분석. 검색어 입력 → 행 수 변화 → API 호출 → 필터 연동 등 전방위 검증", "baseUrl": "https://dev.codebridge-x.com", "menuNavigation": { "level1": "결재관리", "level2": "기안함", "expectedUrl": "/approval/draft", "searchWithinParent": true, "closeOtherMenus": true }, "auth": { "username": "TestUser5", "password": "password123!" }, "steps": [ { "id": 1, "name": "페이지 진입 대기", "action": "wait", "duration": 2000 }, { "id": 2, "name": "[사전조사] 검색 UI 구조 분석", "action": "evaluate", "script": "(async()=>{const R={page:'기안함',url:location.href};const si=['input[type=\"search\"]','input[placeholder*=\"검색\"]','input[placeholder*=\"Search\"]','input[role=\"searchbox\"]','[class*=\"search\"] input','[class*=\"Search\"] input'];R.searchInputs=[];for(const s of si){const els=document.querySelectorAll(s);els.forEach(e=>{R.searchInputs.push({sel:s,ph:e.placeholder||'',type:e.type,name:e.name||'',id:e.id||'',cls:e.className?.substring(0,60)||''})});}const rows=document.querySelectorAll('table tbody tr');R.initialRowCount=rows.length;R.firstRowTexts=rows.length>0?Array.from(rows[0].querySelectorAll('td')).map(td=>td.innerText?.trim().substring(0,25)):[];R.sampleDocTitles=Array.from(rows).slice(0,5).map(r=>{const tds=r.querySelectorAll('td');return tds.length>=3?tds[2].innerText?.trim().substring(0,30):(tds[0]?.innerText?.trim().substring(0,30)||'')}).filter(Boolean);const btns=Array.from(document.querySelectorAll('button')).filter(b=>['검색','조회','Search','초기화','리셋'].some(t=>b.innerText?.includes(t)));R.searchButtons=btns.map(b=>({text:b.innerText?.trim().substring(0,20),cls:b.className?.substring(0,40)||''}));const filters=document.querySelectorAll('select,[role=\"combobox\"]');R.filterCount=filters.length;const tabs=document.querySelectorAll('[role=\"tab\"],button[class*=\"tab\"],a[class*=\"tab\"]');R.tabCount=tabs.length;R.tabTexts=Array.from(tabs).map(t=>t.innerText?.trim().substring(0,15));const statCards=document.querySelectorAll('[class*=\"card\"],[class*=\"Card\"],[class*=\"stat\"]');R.statCards=statCards.length;return JSON.stringify(R)})()" }, { "id": 3, "name": "[테스트1] 존재하지 않는 검색어 입력 (input 이벤트)", "action": "evaluate", "script": "(async()=>{const R={test:'nonsense_search_via_input'};const si=['input[type=\"search\"]','input[placeholder*=\"검색\"]','input[role=\"searchbox\"]','[class*=\"search\"] input','[class*=\"Search\"] input'];let el=null;for(const s of si){el=document.querySelector(s);if(el)break;}if(!el)return JSON.stringify({error:'검색 입력란 없음'});const rowsBefore=document.querySelectorAll('table tbody tr').length;R.rowsBefore=rowsBefore;R.searchPlaceholder=el.placeholder;const nativeSetter=Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value')?.set;el.focus();if(nativeSetter)nativeSetter.call(el,'zzz_no_match_bug_test');else el.value='zzz_no_match_bug_test';el.dispatchEvent(new Event('input',{bubbles:true}));el.dispatchEvent(new Event('change',{bubbles:true}));el.dispatchEvent(new KeyboardEvent('keyup',{bubbles:true}));await new Promise(r=>setTimeout(r,2000));const rowsAfter=document.querySelectorAll('table tbody tr').length;R.rowsAfter=rowsAfter;R.filtered=rowsBefore!==rowsAfter;R.verdict=R.filtered?'PASS: 검색 필터링 동작':'FAIL: 행 수 변화 없음 ('+rowsBefore+'→'+rowsAfter+')';return JSON.stringify(R)})()" }, { "id": 4, "name": "[테스트2] Enter 키로 검색 실행", "action": "evaluate", "script": "(async()=>{const R={test:'search_via_enter_key'};const si=['input[type=\"search\"]','input[placeholder*=\"검색\"]','input[role=\"searchbox\"]','[class*=\"search\"] input'];let el=null;for(const s of si){el=document.querySelector(s);if(el)break;}if(!el)return JSON.stringify({error:'검색 입력란 없음'});const rowsBefore=document.querySelectorAll('table tbody tr').length;R.rowsBefore=rowsBefore;el.focus();el.dispatchEvent(new KeyboardEvent('keydown',{key:'Enter',code:'Enter',keyCode:13,bubbles:true}));el.dispatchEvent(new KeyboardEvent('keypress',{key:'Enter',code:'Enter',keyCode:13,bubbles:true}));el.dispatchEvent(new KeyboardEvent('keyup',{key:'Enter',code:'Enter',keyCode:13,bubbles:true}));const form=el.closest('form');if(form){form.dispatchEvent(new Event('submit',{bubbles:true,cancelable:true}));}await new Promise(r=>setTimeout(r,2000));const rowsAfter=document.querySelectorAll('table tbody tr').length;R.rowsAfter=rowsAfter;R.filtered=rowsBefore!==rowsAfter;R.currentValue=el.value;R.verdict=R.filtered?'PASS: Enter키 검색 동작':'FAIL: Enter키 후에도 행 수 불변 ('+rowsBefore+'→'+rowsAfter+')';return JSON.stringify(R)})()" }, { "id": 5, "name": "[테스트3] 검색 버튼 클릭으로 검색", "action": "evaluate", "script": "(async()=>{const R={test:'search_via_button'};const btns=Array.from(document.querySelectorAll('button,a[role=\"button\"]'));const searchBtn=btns.find(b=>['검색','조회','Search'].some(t=>b.innerText?.trim()===t||b.getAttribute('aria-label')?.includes(t)));const iconBtn=!searchBtn?document.querySelector('button svg[class*=\"search\"],button svg[class*=\"Search\"],button [class*=\"magnif\"]')?.closest('button'):null;const btn=searchBtn||iconBtn;R.buttonFound=!!btn;R.buttonText=btn?.innerText?.trim().substring(0,20)||btn?.getAttribute('aria-label')||'none';if(btn){const rowsBefore=document.querySelectorAll('table tbody tr').length;R.rowsBefore=rowsBefore;btn.click();await new Promise(r=>setTimeout(r,2000));const rowsAfter=document.querySelectorAll('table tbody tr').length;R.rowsAfter=rowsAfter;R.filtered=rowsBefore!==rowsAfter;R.verdict=R.filtered?'PASS: 버튼 검색 동작':'FAIL: 버튼 클릭 후에도 행 수 불변 ('+rowsBefore+'→'+rowsAfter+')';}else{R.verdict='SKIP: 검색 버튼 없음';}return JSON.stringify(R)})()" }, { "id": 6, "name": "[테스트4] React onChange 직접 트리거", "action": "evaluate", "script": "(async()=>{const R={test:'react_onChange_trigger'};const si=['input[type=\"search\"]','input[placeholder*=\"검색\"]','input[role=\"searchbox\"]','[class*=\"search\"] input'];let el=null;for(const s of si){el=document.querySelector(s);if(el)break;}if(!el)return JSON.stringify({error:'검색 입력란 없음'});const nativeSetter=Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value')?.set;if(nativeSetter)nativeSetter.call(el,'');el.dispatchEvent(new Event('input',{bubbles:true}));await new Promise(r=>setTimeout(r,1000));const rowsReset=document.querySelectorAll('table tbody tr').length;R.rowsAfterClear=rowsReset;if(nativeSetter)nativeSetter.call(el,'zzz_no_match_react');const reactKey=Object.keys(el).find(k=>k.startsWith('__reactProps$'));if(reactKey&&el[reactKey]?.onChange){el[reactKey].onChange({target:el,currentTarget:el});R.reactOnChange=true;}else{el.dispatchEvent(new Event('input',{bubbles:true}));R.reactOnChange=false;}await new Promise(r=>setTimeout(r,2000));const rowsAfter=document.querySelectorAll('table tbody tr').length;R.rowsAfter=rowsAfter;R.filtered=rowsReset!==rowsAfter;R.verdict=R.filtered?'PASS: React onChange 검색 동작':'FAIL: React onChange 후에도 행 수 불변 ('+rowsReset+'→'+rowsAfter+')';return JSON.stringify(R)})()" }, { "id": 7, "name": "[테스트5] API 호출 모니터링 + 검색", "action": "evaluate", "script": "(async()=>{const R={test:'api_monitoring'};const captured=[];const origFetch=window.fetch;window.fetch=async function(...args){const url=typeof args[0]==='string'?args[0]:args[0]?.url||'';const method=args[1]?.method||'GET';captured.push({url:url.substring(0,120),method,time:Date.now()});return origFetch.apply(this,args);};const si=['input[type=\"search\"]','input[placeholder*=\"검색\"]','input[role=\"searchbox\"]','[class*=\"search\"] input'];let el=null;for(const s of si){el=document.querySelector(s);if(el)break;}if(!el){window.fetch=origFetch;return JSON.stringify({error:'검색 입력란 없음'});}const nativeSetter=Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value')?.set;if(nativeSetter)nativeSetter.call(el,'');el.dispatchEvent(new Event('input',{bubbles:true}));await new Promise(r=>setTimeout(r,500));captured.length=0;if(nativeSetter)nativeSetter.call(el,'api_test_query');el.dispatchEvent(new Event('input',{bubbles:true}));el.dispatchEvent(new Event('change',{bubbles:true}));el.dispatchEvent(new KeyboardEvent('keydown',{key:'Enter',code:'Enter',keyCode:13,bubbles:true}));await new Promise(r=>setTimeout(r,3000));window.fetch=origFetch;R.apiCallsAfterSearch=captured.filter(c=>!c.url.includes('hot-update')&&!c.url.includes('sockjs'));R.apiCallCount=R.apiCallsAfterSearch.length;R.searchRelatedApis=R.apiCallsAfterSearch.filter(c=>c.url.includes('approval')||c.url.includes('draft')||c.url.includes('search')||c.url.includes('query')||c.url.includes('keyword'));R.verdict=R.searchRelatedApis.length>0?'API 호출 감지: '+R.searchRelatedApis.map(a=>a.url).join(', '):'FAIL: 검색 관련 API 호출 없음';return JSON.stringify(R)})()" }, { "id": 8, "name": "[검색 초기화] 입력란 비우기", "action": "evaluate", "script": "(async()=>{const si=['input[type=\"search\"]','input[placeholder*=\"검색\"]','input[role=\"searchbox\"]','[class*=\"search\"] input'];let el=null;for(const s of si){el=document.querySelector(s);if(el)break;}if(!el)return 'no search input';const nativeSetter=Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value')?.set;if(nativeSetter)nativeSetter.call(el,'');else el.value='';const reactKey=Object.keys(el).find(k=>k.startsWith('__reactProps$'));if(reactKey&&el[reactKey]?.onChange){el[reactKey].onChange({target:el,currentTarget:el});}el.dispatchEvent(new Event('input',{bubbles:true}));el.dispatchEvent(new Event('change',{bubbles:true}));await new Promise(r=>setTimeout(r,1500));return 'cleared, rows: '+document.querySelectorAll('table tbody tr').length})()" }, { "id": 9, "name": "[테스트6] 실존 데이터로 검색 테스트", "action": "evaluate", "script": "(async()=>{const R={test:'real_data_search'};const rows=document.querySelectorAll('table tbody tr');R.totalRows=rows.length;if(rows.length===0)return JSON.stringify({error:'테이블 행 없음'});const firstRowCells=Array.from(rows[0].querySelectorAll('td'));R.firstRowData=firstRowCells.map(c=>c.innerText?.trim().substring(0,30));let searchTerm='';for(const cell of firstRowCells){const t=cell.innerText?.trim();if(t&&t.length>=2&&!/^\\d+$/.test(t)&&!t.includes('원')&&!t.includes(',')&&!t.includes('/')&&!t.includes('-')&&t.length<20){searchTerm=t.substring(0,4);break;}}R.searchTerm=searchTerm;if(!searchTerm)return JSON.stringify({...R,error:'검색어 추출 실패'});const si=['input[type=\"search\"]','input[placeholder*=\"검색\"]','input[role=\"searchbox\"]','[class*=\"search\"] input'];let el=null;for(const s of si){el=document.querySelector(s);if(el)break;}if(!el)return JSON.stringify({error:'검색 입력란 없음'});const nativeSetter=Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value')?.set;if(nativeSetter)nativeSetter.call(el,searchTerm);const reactKey=Object.keys(el).find(k=>k.startsWith('__reactProps$'));if(reactKey&&el[reactKey]?.onChange){el[reactKey].onChange({target:el,currentTarget:el});}el.dispatchEvent(new Event('input',{bubbles:true}));el.dispatchEvent(new Event('change',{bubbles:true}));el.dispatchEvent(new KeyboardEvent('keydown',{key:'Enter',code:'Enter',keyCode:13,bubbles:true}));await new Promise(r=>setTimeout(r,2000));const rowsAfter=document.querySelectorAll('table tbody tr').length;R.rowsAfter=rowsAfter;R.verdict=R.totalRows!==rowsAfter?'PASS: 실존 데이터 검색 동작 ('+R.totalRows+'→'+rowsAfter+')':'FAIL: 실존 데이터 검색에도 행 수 불변 ('+R.totalRows+'→'+rowsAfter+')';return JSON.stringify(R)})()" }, { "id": 10, "name": "[테스트7] 상태 필터(탭/드롭다운) 동작 검증", "action": "evaluate", "script": "(async()=>{const R={test:'filter_interaction'};const rowsBefore=document.querySelectorAll('table tbody tr').length;R.rowsBefore=rowsBefore;const tabs=Array.from(document.querySelectorAll('[role=\"tab\"],button[class*=\"tab\"]'));R.tabs=tabs.map(t=>t.innerText?.trim().substring(0,15));const statusFilter=tabs.find(t=>['반려','임시저장','완료','진행중','진행'].some(s=>t.innerText?.includes(s)));if(statusFilter){R.clickedFilter=statusFilter.innerText?.trim();statusFilter.click();await new Promise(r=>setTimeout(r,2000));const rowsAfter=document.querySelectorAll('table tbody tr').length;R.rowsAfter=rowsAfter;R.filtered=rowsBefore!==rowsAfter;R.verdict=R.filtered?'PASS: 상태 필터 동작 ('+rowsBefore+'→'+rowsAfter+')':'상태 필터 클릭 후 행 수 불변 ('+rowsBefore+'→'+rowsAfter+')';}else{const selects=document.querySelectorAll('select,[role=\"combobox\"]');R.selectCount=selects.length;R.verdict=selects.length>0?'필터 드롭다운 존재('+selects.length+'개), 탭 필터 없음':'SKIP: 필터 UI 없음';}return JSON.stringify(R)})()" }, { "id": 11, "name": "[결론] 검색 기능 최종 판정", "action": "evaluate", "script": "(async()=>{const si=['input[type=\"search\"]','input[placeholder*=\"검색\"]','input[role=\"searchbox\"]','[class*=\"search\"] input'];let el=null;for(const s of si){el=document.querySelector(s);if(el)break;}const R={page:'기안함',url:location.href,searchInputExists:!!el,searchPlaceholder:el?.placeholder||'none',totalRows:document.querySelectorAll('table tbody tr').length,headers:Array.from(document.querySelectorAll('table thead th')).map(h=>h.innerText?.trim().substring(0,15)),filters:document.querySelectorAll('select,[role=\"combobox\"]').length,tabs:Array.from(document.querySelectorAll('[role=\"tab\"]')).map(t=>t.innerText?.trim().substring(0,15))};R.conclusion='기안함 페이지에 검색 입력란(placeholder: '+R.searchPlaceholder+')이 존재하지만, input/change/Enter/button/React onChange 어떤 방식으로도 테이블 행 필터링이 발생하지 않음. 검색 관련 API 호출도 미감지. 검색 기능 미구현 또는 이벤트 바인딩 누락 버그로 판정.';return JSON.stringify(R)})()" } ] }