{ "id": "hr-salary-long-term-care", "name": "급여 장기요양보험 필드 검증 테스트", "version": "1.1.0", "enabled": true, "screenshotPolicy": { "captureOnFail": true, "captureOnPass": false }, "description": "인사관리 > 급여관리의 장기요양보험 필드 존재 확인 + 자동계산(건강보험×12.81%) 검증", "baseUrl": "https://dev.codebridge-x.com", "menuNavigation": { "level1": "인사관리", "level2": "급여관리", "expectedUrl": "/hr/salary-management", "searchWithinParent": true, "closeOtherMenus": true }, "auth": { "username": "TestUser5", "password": "password123!" }, "steps": [ { "id": 1, "name": "메뉴 진입: 인사관리 > 급여관리", "action": "menu_navigate", "level1": "인사관리", "level2": "급여관리", "expected": { "url_contains": "/hr/salary" } }, { "id": 2, "name": "페이지 로드 대기", "action": "wait", "timeout": 3000 }, { "id": 3, "name": "URL 검증", "action": "verify_url", "expected": { "url_contains": "/hr/salary-management" } }, { "id": 4, "name": "목업 감지", "action": "verify_not_mockup", "checks": ["급여 목록 표시", "등록 버튼 존재"] }, { "id": 5, "name": "테이블 로드 대기", "action": "wait_for_table", "timeout": 15000 }, { "id": 6, "name": "[READ] 첫 번째 행 클릭 (상세 다이얼로그 열기)", "phase": "READ", "action": "click_first_row" }, { "id": 7, "name": "[READ] 상세 다이얼로그 대기", "phase": "READ", "action": "wait", "timeout": 2000 }, { "id": 8, "name": "[READ] 장기요양보험 필드 존재 확인 (상세)", "phase": "READ", "action": "evaluate", "script": "(()=>{const R={phase:'DETAIL_LTC_CHECK'};const modal=document.querySelector('[role=\"dialog\"],[aria-modal=\"true\"],[class*=\"Dialog\"]');const scope=modal||document;const bodyText=scope.innerText||'';R.hasLongTermCare=bodyText.includes('장기요양')||bodyText.includes('장기요양보험');const labels=Array.from(scope.querySelectorAll('label,dt,[class*=\"label\"],[class*=\"Label\"]'));const ltcLabel=labels.find(l=>l.innerText?.includes('장기요양'));R.labelFound=!!ltcLabel;if(ltcLabel){const container=ltcLabel.closest('[class*=\"field\"],[class*=\"Field\"],[class*=\"form-item\"],[class*=\"row\"]');if(container){const valueEl=container.querySelector('input,span,dd,[class*=\"value\"],[class*=\"Value\"]');R.value=valueEl?.value||valueEl?.innerText?.trim()||'N/A';}}R.ok=R.hasLongTermCare||R.labelFound;R.info=R.ok?'pass: 장기요양보험 필드 발견':'fail: 장기요양보험 필드 미발견';return JSON.stringify(R);})()" }, { "id": 9, "name": "[READ] 건강보험/장기요양 값 비교 (자동계산 검증)", "phase": "READ", "action": "evaluate", "script": "(()=>{const R={phase:'CALC_VERIFY'};const modal=document.querySelector('[role=\"dialog\"],[aria-modal=\"true\"],[class*=\"Dialog\"]');const scope=modal||document;const labels=Array.from(scope.querySelectorAll('label,dt,[class*=\"label\"],[class*=\"Label\"]'));const getVal=keyword=>{const lbl=labels.find(l=>l.innerText?.includes(keyword));if(!lbl)return null;const container=lbl.closest('[class*=\"field\"],[class*=\"Field\"],[class*=\"form-item\"],[class*=\"row\"]');if(!container)return null;const valEl=container.querySelector('input,span,dd,[class*=\"value\"]');const raw=valEl?.value||valEl?.innerText?.trim()||'';return parseInt(raw.replace(/[^0-9]/g,''))||null;};R.healthInsurance=getVal('건강보험');R.longTermCare=getVal('장기요양');if(R.healthInsurance&&R.longTermCare){const expected=Math.round(R.healthInsurance*0.1281);R.expectedLTC=expected;R.tolerance=Math.abs(R.longTermCare-expected);R.isCorrect=R.tolerance<=10;R.info=R.isCorrect?'pass: 장기요양='+R.longTermCare+' (건강보험 '+R.healthInsurance+'×12.81%='+expected+')':'warn: 장기요양='+R.longTermCare+' vs 예상='+expected+' (차이='+R.tolerance+')';}else{R.info='warn: 건강보험('+R.healthInsurance+') 또는 장기요양('+R.longTermCare+') 값 미확인';}R.ok=true;return JSON.stringify(R);})()" }, { "id": 10, "name": "[READ] 상세 다이얼로그 닫기", "phase": "READ", "action": "close_modal_if_open" }, { "id": 11, "name": "[CREATE] 등록 다이얼로그 열기", "phase": "CREATE", "action": "evaluate", "script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));await w(500);const R={phase:'CREATE_OPEN'};const btn=Array.from(document.querySelectorAll('button')).find(b=>/급여.*등록|등록|추가/.test(b.innerText?.trim())&&b.offsetParent!==null&&!b.disabled);if(btn){btn.click();await w(2000);R.clicked=true;}else{R.clicked=false;R.info='등록 버튼 미발견';}R.ok=true;return JSON.stringify(R);})()", "timeout": 10000 }, { "id": 12, "name": "[CREATE] 등록 다이얼로그 대기 (폴링)", "phase": "CREATE", "action": "evaluate", "script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'WAIT_DIALOG'};const end=Date.now()+8000;while(Date.now()0){R.ok=true;R.info='pass: dialog open (position:fixed safe)';R.dialogFound=true;R.visible=true;return JSON.stringify(R);}await w(300);}const dlg=document.querySelector('[role=\"dialog\"]');R.ok=true;R.info='warn: dialog not visible after 8s polling';R.dialogFound=!!dlg;R.visible=false;return JSON.stringify(R);})()", "timeout": 10000 }, { "id": 13, "name": "[CREATE] 등록 폼에서 장기요양보험 필드 확인", "phase": "CREATE", "action": "evaluate", "script": "(()=>{const R={phase:'CREATE_LTC_CHECK'};const modal=document.querySelector('[role=\"dialog\"],[aria-modal=\"true\"],[class*=\"Dialog\"]');const scope=modal||document;const bodyText=scope.innerText||'';R.hasLongTermCare=bodyText.includes('장기요양')||bodyText.includes('장기요양보험');const labels=Array.from(scope.querySelectorAll('label'));const ltcLabel=labels.find(l=>l.innerText?.includes('장기요양'));R.labelFound=!!ltcLabel;if(ltcLabel){const container=ltcLabel.closest('[class*=\"field\"],[class*=\"Field\"],[class*=\"form-item\"],[class*=\"row\"]');if(container){const input=container.querySelector('input');R.hasInput=!!input;R.inputReadOnly=input?.readOnly||false;R.inputValue=input?.value||'';}}R.ok=R.hasLongTermCare||R.labelFound;R.info=R.ok?'pass: 등록 폼에 장기요양보험 필드 존재'+(R.inputReadOnly?' (자동계산, readOnly)':' (입력 가능)'):'fail: 등록 폼에 장기요양보험 필드 미발견';return JSON.stringify(R);})()" }, { "id": 14, "name": "[CREATE] 건강보험 입력 → 장기요양 자동계산 검증", "phase": "CREATE", "action": "evaluate", "script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const sv=(el,v)=>{const ns=Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value')?.set;if(ns)ns.call(el,v);else el.value=v;el.dispatchEvent(new Event('input',{bubbles:true}));el.dispatchEvent(new Event('change',{bubbles:true}));};const R={phase:'AUTO_CALC'};const modal=document.querySelector('[role=\"dialog\"],[aria-modal=\"true\"],[class*=\"Dialog\"]');const scope=modal||document;const labels=Array.from(scope.querySelectorAll('label'));const healthLabel=labels.find(l=>l.innerText?.includes('건강보험')&&!l.innerText?.includes('장기'));if(!healthLabel){R.info='건강보험 입력 필드 미발견';R.ok=true;return JSON.stringify(R);}const healthContainer=healthLabel.closest('[class*=\"field\"],[class*=\"Field\"],[class*=\"form-item\"],[class*=\"row\"]');const healthInput=healthContainer?.querySelector('input');if(!healthInput||healthInput.readOnly||healthInput.disabled){R.info='건강보험 입력 불가 (readOnly)';R.ok=true;return JSON.stringify(R);}sv(healthInput,'100000');await w(800);const ltcLabel=labels.find(l=>l.innerText?.includes('장기요양'));if(ltcLabel){const ltcContainer=ltcLabel.closest('[class*=\"field\"],[class*=\"Field\"],[class*=\"form-item\"],[class*=\"row\"]');const ltcEl=ltcContainer?.querySelector('input,span,[class*=\"value\"]');const ltcVal=ltcEl?.value||ltcEl?.innerText?.trim()||'';const numVal=parseInt(ltcVal.replace(/[^0-9]/g,''))||0;R.healthInput=100000;R.longTermCareOutput=numVal;R.expected=Math.round(100000*0.1281);R.isAutoCalculated=Math.abs(numVal-R.expected)<=10;R.info=R.isAutoCalculated?'pass: 건강보험 100,000 → 장기요양 '+numVal+' (예상 '+R.expected+')':'warn: 장기요양='+numVal+' vs 예상='+R.expected;}else{R.info='장기요양 필드 미발견';}R.ok=true;return JSON.stringify(R);})()", "timeout": 10000 }, { "id": 15, "name": "[CREATE] 등록 다이얼로그 닫기 (데이터 저장 안함)", "phase": "CREATE", "action": "evaluate", "script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const isVis=el=>!!el&&el.getBoundingClientRect().width>0;const modal=document.querySelector('[role=\"dialog\"],[aria-modal=\"true\"],[class*=\"Dialog\"],[data-state=\"open\"][class*=\"Sheet\"]');if(isVis(modal)){const cancelBtn=Array.from(modal.querySelectorAll('button')).find(b=>/취소|닫기|Close/.test(b.innerText?.trim()));if(cancelBtn){cancelBtn.click();await w(500);}else{document.dispatchEvent(new KeyboardEvent('keydown',{key:'Escape',bubbles:true}));await w(500);}}return JSON.stringify({ok:true,info:'pass: dialog closed'});})()" }, { "id": 16, "name": "[SUMMARY] API 호출 통계", "action": "evaluate", "script": "(()=>{const logs=(window.__E2E__?window.__E2E__.getApiLogs().logs:[]);const salaryApi=logs.filter(l=>l.url.includes('salary')||l.url.includes('payroll'));return JSON.stringify({ok:true,info:'pass: API summary - total='+logs.length+' salary_api='+salaryApi.length+' success='+logs.filter(l=>l.status>=200&&l.status<300).length+' failed='+logs.filter(l=>l.status>=400).length});})()" } ], "expectedAPIs": [ { "method": "GET", "endpoint": "/api/v1/payrolls", "description": "급여 목록 조회" }, { "method": "GET", "endpoint": "/api/v1/payrolls/{id}", "description": "급여 상세 조회" } ], "rollbackPlan": { "note": "조회 위주 테스트, 등록 다이얼로그는 취소로 닫음. 데이터 변경 없음." } }