Files
sam-scenarios/hr-salary-long-term-care.json
김보곤 35739c396f feat: 달력 CRUD, 수주 일괄삭제, 급여 장기요양보험 시나리오 추가
- settings-calendar-crud: 달력관리 일정 CRUD 전체 흐름 (v3.1.0)
  - Radix UI PointerEvent 탭 전환, position:fixed Sheet 다이얼로그 대응
  - Server Actions POST 패턴 API 검증
- sales-order-bulk-delete: 수주관리 일괄삭제 기능 검증 (20 steps)
- hr-salary-long-term-care: 급여관리 장기요양보험 필드/자동계산 검증 (16 steps)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 20:48:19 +09:00

138 lines
10 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{
"id": "hr-salary-long-term-care",
"name": "급여 장기요양보험 필드 검증 테스트",
"version": "1.0.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));await w(2000);const dlg=document.querySelector('[role=\"dialog\"]');const isVis=dlg&&dlg.getBoundingClientRect().width>0;return JSON.stringify({ok:true,info:isVis?'pass: dialog open (position:fixed)':'warn: dialog not visible',dialogFound:!!dlg,visible:isVis});})()",
"timeout": 5000
},
{
"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 modal=document.querySelector('[role=\"dialog\"],[aria-modal=\"true\"],[class*=\"Dialog\"]');if(modal&&modal.offsetParent!==null){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 'pass: dialog closed';})()"
},
{
"id": 16,
"name": "[SUMMARY] API 호출 통계",
"action": "evaluate",
"script": "(()=>{const logs=window.__API_LOGS__||[];const salaryApi=logs.filter(l=>l.url.includes('salary')||l.url.includes('payroll'));return '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": "조회 위주 테스트, 등록 다이얼로그는 취소로 닫음. 데이터 변경 없음."
}
}