{ "id": "edge-boundary-acc-sales", "name": "엣지 케이스: 경계값 입력 검증 (회계 > 매출관리)", "version": "1.0.0", "auth": { "role": "admin" }, "menuNavigation": { "level1": "회계관리", "level2": "매출관리" }, "screenshotPolicy": { "captureOnFail": true, "captureOnPass": false }, "steps": [ { "id": 1, "name": "[회계관리 > 매출관리] 페이지 로드 대기", "action": "wait", "timeout": 5000 }, { "id": 2, "name": "[회계관리 > 매출관리] 테이블 로드 대기", "action": "wait_for_table", "timeout": 20000 }, { "id": 3, "name": "[회계관리 > 매출관리] [EDGE] 등록 폼 열기", "action": "evaluate", "script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'OPEN_FORM'};const priorities=['매출 등록','매출등록','등록','추가','신규'];let btn=null;for(const kw of priorities){btn=Array.from(document.querySelectorAll('button')).find(b=>{const t=b.innerText?.trim()||'';return t.includes(kw)&&b.offsetParent!==null&&!b.disabled;});if(btn)break;}if(!btn){R.err='등록 버튼 없음';R.ok=true;return JSON.stringify(R);}R.btnText=btn.innerText?.trim();btn.click();await w(2500);R.url=location.pathname+location.search;R.ok=true;return JSON.stringify(R);})()", "timeout": 15000, "phase": "OPEN_FORM" }, { "id": 4, "name": "[회계관리 > 매출관리] [EDGE] 폼 렌더링 대기", "action": "wait", "timeout": 2000 }, { "id": 5, "name": "[회계관리 > 매출관리] [EDGE] 수량=0 입력 → 자동계산 반응 확인", "action": "evaluate", "script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const sv=(el,v)=>{const p=el.tagName==='TEXTAREA'?HTMLTextAreaElement.prototype:HTMLInputElement.prototype;const ns=Object.getOwnPropertyDescriptor(p,'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:'ZERO_QTY'};const inputs=Array.from(document.querySelectorAll('input[type=\"text\"],input[type=\"number\"],input:not([type])')).filter(i=>i.offsetParent!==null&&!i.readOnly&&!i.disabled);const qtyInput=inputs.find(i=>(i.placeholder||'').includes('수량')||(i.name||'').includes('quantity')||(i.name||'').includes('qty'));if(qtyInput){sv(qtyInput,'0');await w(500);R.qtySet=true;R.qtyValue=qtyInput.value;const supplyInputs=inputs.filter(i=>(i.placeholder||'').includes('공급')||(i.name||'').includes('supply')||(i.name||'').includes('amount'));R.supplyValue=supplyInputs.length>0?supplyInputs[0].value:'N/A';const hasError=document.querySelector('[class*=\"error\"],[class*=\"Error\"],[role=\"alert\"],.text-red-500,.text-destructive');R.errorShown=!!hasError;}else{R.qtyInputNotFound=true;const numInputs=inputs.filter(i=>i.type==='number'||(i.placeholder||'').match(/[0-9]/));if(numInputs.length>0){sv(numInputs[0],'0');await w(500);R.fallbackSet=true;}}R.ok=true;R.info=R.qtySet?'수량=0 입력 완료, 공급가액='+R.supplyValue:'수량 필드 미발견';return JSON.stringify(R);})()", "timeout": 10000, "phase": "BOUNDARY" }, { "id": 6, "name": "[회계관리 > 매출관리] [EDGE] 수량=-1 입력 → 거부/에러 확인", "action": "evaluate", "script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const sv=(el,v)=>{const p=el.tagName==='TEXTAREA'?HTMLTextAreaElement.prototype:HTMLInputElement.prototype;const ns=Object.getOwnPropertyDescriptor(p,'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:'NEGATIVE_QTY'};const inputs=Array.from(document.querySelectorAll('input[type=\"text\"],input[type=\"number\"],input:not([type])')).filter(i=>i.offsetParent!==null&&!i.readOnly&&!i.disabled);const qtyInput=inputs.find(i=>(i.placeholder||'').includes('수량')||(i.name||'').includes('quantity')||(i.name||'').includes('qty'));if(qtyInput){sv(qtyInput,'-1');await w(500);R.qtySet=true;R.qtyValue=qtyInput.value;R.qtyAccepted=qtyInput.value==='-1';const hasError=document.querySelector('[class*=\"error\"],[class*=\"Error\"],[role=\"alert\"],.text-red-500,.text-destructive');R.errorShown=!!hasError;if(hasError){R.errorText=hasError.innerText?.trim().substring(0,80);}R.ariaInvalid=qtyInput.getAttribute('aria-invalid')==='true';}else{R.qtyInputNotFound=true;const numInputs=inputs.filter(i=>i.type==='number');if(numInputs.length>0){sv(numInputs[0],'-1');await w(500);R.fallbackSet=true;R.fallbackAccepted=numInputs[0].value==='-1';}}R.ok=true;R.info=R.errorShown?'✅ 음수 입력 시 에러 표시':'⚠️ 음수 입력 에러 미표시';return JSON.stringify(R);})()", "timeout": 10000, "phase": "BOUNDARY" }, { "id": 7, "name": "[회계관리 > 매출관리] [EDGE] 단가=99999.99 소수점 입력 → 처리 확인", "action": "evaluate", "script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const sv=(el,v)=>{const p=el.tagName==='TEXTAREA'?HTMLTextAreaElement.prototype:HTMLInputElement.prototype;const ns=Object.getOwnPropertyDescriptor(p,'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:'DECIMAL_PRICE'};const inputs=Array.from(document.querySelectorAll('input[type=\"text\"],input[type=\"number\"],input:not([type])')).filter(i=>i.offsetParent!==null&&!i.readOnly&&!i.disabled);const priceInput=inputs.find(i=>(i.placeholder||'').includes('단가')||(i.name||'').includes('price')||(i.name||'').includes('unitPrice'));if(priceInput){sv(priceInput,'99999.99');await w(500);R.priceSet=true;R.displayValue=priceInput.value;R.decimalKept=priceInput.value.includes('.');R.rounded=!priceInput.value.includes('.')&&priceInput.value!=='';const hasError=document.querySelector('[class*=\"error\"],[class*=\"Error\"],[role=\"alert\"],.text-red-500,.text-destructive');R.errorShown=!!hasError;}else{R.priceInputNotFound=true;const numInputs=inputs.filter(i=>i.type==='number'||(i.placeholder||'').match(/단가|금액|price/i));if(numInputs.length>1){sv(numInputs[1],'99999.99');await w(500);R.fallbackSet=true;R.fallbackValue=numInputs[1].value;}}R.ok=true;R.info=R.decimalKept?'소수점 유지됨: '+R.displayValue:(R.rounded?'소수점 반올림/제거됨: '+R.displayValue:'단가 필드 미발견');return JSON.stringify(R);})()", "timeout": 10000, "phase": "BOUNDARY" }, { "id": 8, "name": "[회계관리 > 매출관리] [EDGE] 품목명 255자 초과 입력 → 잘림/에러 확인", "action": "evaluate", "script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const sv=(el,v)=>{const p=el.tagName==='TEXTAREA'?HTMLTextAreaElement.prototype:HTMLInputElement.prototype;const ns=Object.getOwnPropertyDescriptor(p,'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:'MAX_LENGTH'};const longStr='E2E_TEST_LONG_'+'A'.repeat(260);const inputs=Array.from(document.querySelectorAll('input[type=\"text\"],input:not([type]),textarea')).filter(i=>i.offsetParent!==null&&!i.readOnly&&!i.disabled);const itemInput=inputs.find(i=>(i.placeholder||'').includes('품목')||(i.name||'').includes('item')||(i.name||'').includes('product'));if(itemInput){sv(itemInput,longStr);await w(500);R.inputSet=true;R.inputLength=itemInput.value.length;R.maxLengthAttr=itemInput.maxLength||'none';R.truncated=itemInput.value.length0){sv(inputs[0],longStr);await w(500);R.fallbackSet=true;R.fallbackLength=inputs[0].value.length;R.fallbackTruncated=inputs[0].value.length 매출관리] [EDGE] 특수문자/XSS 입력 → 방어 확인", "action": "evaluate", "script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const sv=(el,v)=>{const p=el.tagName==='TEXTAREA'?HTMLTextAreaElement.prototype:HTMLInputElement.prototype;const ns=Object.getOwnPropertyDescriptor(p,'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:'XSS_CHECK'};const xssPayload='';const inputs=Array.from(document.querySelectorAll('input[type=\"text\"],input:not([type]),textarea')).filter(i=>i.offsetParent!==null&&!i.readOnly&&!i.disabled);const itemInput=inputs.find(i=>(i.placeholder||'').includes('품목')||(i.name||'').includes('item')||(i.name||'').includes('product'));const targetInput=itemInput||inputs[0];if(targetInput){sv(targetInput,xssPayload);await w(500);R.inputSet=true;R.storedValue=targetInput.value;R.xssAccepted=targetInput.value.includes('