Files
sam-scenarios/api-health-prod-misc.json
김보곤 158fc30eae feat: Phase 3 정교한 테스트 시나리오 (detail-roundtrip, cross-module, api-health)
- detail-roundtrip: 목록→행클릭→상세페이지→데이터검증→목록복귀→무결성확인 (3그룹, 8페이지)
- cross-module: 판매↔회계 거래처, 판매↔생산 품목 교차 데이터 일관성 검증
- api-health: 28개 주요 페이지 API 상태코드/응답시간/에러율 전수 감사 (3그룹)
- 전체 7/7 PASS 확인
2026-02-12 13:28:38 +09:00

280 lines
39 KiB
JSON

{
"id": "api-health-prod-misc",
"name": "API 건강성 감사: 생산/기타",
"version": "1.0.0",
"auth": {
"role": "admin"
},
"menuNavigation": {
"level1": "생산관리",
"level2": "작업지시 관리"
},
"screenshotPolicy": {
"captureOnFail": true,
"captureOnPass": false
},
"steps": [
{
"id": 1,
"name": "[생산관리 > 작업지시 관리] API 인터셉터 설치",
"action": "evaluate",
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'INSTALL_INTERCEPTOR'};window.__API_HEALTH__=[];window.__API_HEALTH_START__=Date.now();if(!window.__HEALTH_FETCH_PATCHED__){ 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').toUpperCase(); const t0=Date.now(); try{ const resp=await origFetch.apply(this,args); const dur=Date.now()-t0; if(url.includes('/api/')||url.includes('/v1/')){ window.__API_HEALTH__.push({url:url.substring(0,120),method,status:resp.status,duration:dur,ok:resp.ok,ts:Date.now()}); } return resp; }catch(e){ const dur=Date.now()-t0; if(url.includes('/api/')||url.includes('/v1/')){ window.__API_HEALTH__.push({url:url.substring(0,120),method,status:0,duration:dur,ok:false,error:e.message,ts:Date.now()}); } throw e; } }; window.__HEALTH_FETCH_PATCHED__=true;}if(!window.__HEALTH_XHR_PATCHED__){ const origOpen=XMLHttpRequest.prototype.open; const origSend=XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open=function(method,url,...rest){ this.__h_method=method;this.__h_url=url;this.__h_t0=Date.now(); return origOpen.apply(this,[method,url,...rest]); }; XMLHttpRequest.prototype.send=function(...args){ this.addEventListener('loadend',function(){ const url=this.__h_url||''; if(url.includes('/api/')||url.includes('/v1/')){ window.__API_HEALTH__.push({url:url.substring(0,120),method:(this.__h_method||'GET').toUpperCase(),status:this.status,duration:Date.now()-(this.__h_t0||Date.now()),ok:this.status>=200&&this.status<400,ts:Date.now()}); } }); return origSend.apply(this,args); }; window.__HEALTH_XHR_PATCHED__=true;}R.interceptorInstalled=true;R.ok=true;return JSON.stringify(R);})()",
"timeout": 5000,
"phase": "INSTALL"
},
{
"id": 2,
"name": "[생산관리 > 작업지시 관리] API 호출 대기",
"action": "wait",
"timeout": 3000
},
{
"id": 3,
"name": "[생산관리 > 작업지시 관리] API 건강성 감사",
"action": "evaluate",
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'API_AUDIT'};const logs=window.__API_HEALTH__||[];R.totalCalls=logs.length;if(logs.length===0){R.warn='API 호출 없음 (인터셉터 미동작 또는 API 없는 페이지)';R.grade='WARN';R.ok=true;return JSON.stringify(R);}const errors4xx=logs.filter(l=>l.status>=400&&l.status<500);const errors5xx=logs.filter(l=>l.status>=500);const networkErrors=logs.filter(l=>l.status===0);const slowCalls=logs.filter(l=>l.duration>2000);const successCalls=logs.filter(l=>l.ok);R.errors4xx=errors4xx.length;R.errors5xx=errors5xx.length;R.networkErrors=networkErrors.length;R.slowCalls=slowCalls.length;R.successCount=successCalls.length;const durations=logs.map(l=>l.duration).filter(d=>d>0);R.avgResponseTime=durations.length>0?Math.round(durations.reduce((a,b)=>a+b,0)/durations.length):0;R.maxResponseTime=durations.length>0?Math.max(...durations):0;R.minResponseTime=durations.length>0?Math.min(...durations):0;const errorRate=logs.length>0?((errors4xx.length+errors5xx.length+networkErrors.length)/logs.length*100):0;R.errorRate=Math.round(errorRate*10)/10;R.failedUrls=[...errors5xx,...errors4xx,...networkErrors].slice(0,5).map(l=>({url:l.url,status:l.status,method:l.method,duration:l.duration}));R.slowUrls=slowCalls.slice(0,3).map(l=>({url:l.url,duration:l.duration,method:l.method}));if(errors5xx.length>0||errorRate>10){R.grade='FAIL';}else if(errors4xx.length>0||slowCalls.length>3||R.avgResponseTime>1000){R.grade='WARN';}else{R.grade='PASS';}R.summary=R.totalCalls+'개 API 호출 | '+R.successCount+'성공 | '+(errors4xx.length+errors5xx.length)+'에러 | '+slowCalls.length+'느림 | 평균 '+R.avgResponseTime+'ms | 등급: '+R.grade;window.__API_HEALTH__=[];R.ok=true;return JSON.stringify(R);})()",
"timeout": 10000,
"phase": "API_AUDIT"
},
{
"id": 4,
"name": "[생산관리 > 작업실적] 메뉴 이동",
"action": "menu_navigate",
"level1": "생산관리",
"level2": "작업실적",
"timeout": 10000
},
{
"id": 5,
"name": "[생산관리 > 작업실적] API 인터셉터 설치",
"action": "evaluate",
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'INSTALL_INTERCEPTOR'};window.__API_HEALTH__=[];window.__API_HEALTH_START__=Date.now();if(!window.__HEALTH_FETCH_PATCHED__){ 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').toUpperCase(); const t0=Date.now(); try{ const resp=await origFetch.apply(this,args); const dur=Date.now()-t0; if(url.includes('/api/')||url.includes('/v1/')){ window.__API_HEALTH__.push({url:url.substring(0,120),method,status:resp.status,duration:dur,ok:resp.ok,ts:Date.now()}); } return resp; }catch(e){ const dur=Date.now()-t0; if(url.includes('/api/')||url.includes('/v1/')){ window.__API_HEALTH__.push({url:url.substring(0,120),method,status:0,duration:dur,ok:false,error:e.message,ts:Date.now()}); } throw e; } }; window.__HEALTH_FETCH_PATCHED__=true;}if(!window.__HEALTH_XHR_PATCHED__){ const origOpen=XMLHttpRequest.prototype.open; const origSend=XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open=function(method,url,...rest){ this.__h_method=method;this.__h_url=url;this.__h_t0=Date.now(); return origOpen.apply(this,[method,url,...rest]); }; XMLHttpRequest.prototype.send=function(...args){ this.addEventListener('loadend',function(){ const url=this.__h_url||''; if(url.includes('/api/')||url.includes('/v1/')){ window.__API_HEALTH__.push({url:url.substring(0,120),method:(this.__h_method||'GET').toUpperCase(),status:this.status,duration:Date.now()-(this.__h_t0||Date.now()),ok:this.status>=200&&this.status<400,ts:Date.now()}); } }); return origSend.apply(this,args); }; window.__HEALTH_XHR_PATCHED__=true;}R.interceptorInstalled=true;R.ok=true;return JSON.stringify(R);})()",
"timeout": 5000,
"phase": "INSTALL"
},
{
"id": 6,
"name": "[생산관리 > 작업실적] API 호출 대기",
"action": "wait",
"timeout": 3000
},
{
"id": 7,
"name": "[생산관리 > 작업실적] API 건강성 감사",
"action": "evaluate",
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'API_AUDIT'};const logs=window.__API_HEALTH__||[];R.totalCalls=logs.length;if(logs.length===0){R.warn='API 호출 없음 (인터셉터 미동작 또는 API 없는 페이지)';R.grade='WARN';R.ok=true;return JSON.stringify(R);}const errors4xx=logs.filter(l=>l.status>=400&&l.status<500);const errors5xx=logs.filter(l=>l.status>=500);const networkErrors=logs.filter(l=>l.status===0);const slowCalls=logs.filter(l=>l.duration>2000);const successCalls=logs.filter(l=>l.ok);R.errors4xx=errors4xx.length;R.errors5xx=errors5xx.length;R.networkErrors=networkErrors.length;R.slowCalls=slowCalls.length;R.successCount=successCalls.length;const durations=logs.map(l=>l.duration).filter(d=>d>0);R.avgResponseTime=durations.length>0?Math.round(durations.reduce((a,b)=>a+b,0)/durations.length):0;R.maxResponseTime=durations.length>0?Math.max(...durations):0;R.minResponseTime=durations.length>0?Math.min(...durations):0;const errorRate=logs.length>0?((errors4xx.length+errors5xx.length+networkErrors.length)/logs.length*100):0;R.errorRate=Math.round(errorRate*10)/10;R.failedUrls=[...errors5xx,...errors4xx,...networkErrors].slice(0,5).map(l=>({url:l.url,status:l.status,method:l.method,duration:l.duration}));R.slowUrls=slowCalls.slice(0,3).map(l=>({url:l.url,duration:l.duration,method:l.method}));if(errors5xx.length>0||errorRate>10){R.grade='FAIL';}else if(errors4xx.length>0||slowCalls.length>3||R.avgResponseTime>1000){R.grade='WARN';}else{R.grade='PASS';}R.summary=R.totalCalls+'개 API 호출 | '+R.successCount+'성공 | '+(errors4xx.length+errors5xx.length)+'에러 | '+slowCalls.length+'느림 | 평균 '+R.avgResponseTime+'ms | 등급: '+R.grade;window.__API_HEALTH__=[];R.ok=true;return JSON.stringify(R);})()",
"timeout": 10000,
"phase": "API_AUDIT"
},
{
"id": 8,
"name": "[생산관리 > 품목관리] 메뉴 이동",
"action": "menu_navigate",
"level1": "생산관리",
"level2": "품목관리",
"timeout": 10000
},
{
"id": 9,
"name": "[생산관리 > 품목관리] API 인터셉터 설치",
"action": "evaluate",
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'INSTALL_INTERCEPTOR'};window.__API_HEALTH__=[];window.__API_HEALTH_START__=Date.now();if(!window.__HEALTH_FETCH_PATCHED__){ 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').toUpperCase(); const t0=Date.now(); try{ const resp=await origFetch.apply(this,args); const dur=Date.now()-t0; if(url.includes('/api/')||url.includes('/v1/')){ window.__API_HEALTH__.push({url:url.substring(0,120),method,status:resp.status,duration:dur,ok:resp.ok,ts:Date.now()}); } return resp; }catch(e){ const dur=Date.now()-t0; if(url.includes('/api/')||url.includes('/v1/')){ window.__API_HEALTH__.push({url:url.substring(0,120),method,status:0,duration:dur,ok:false,error:e.message,ts:Date.now()}); } throw e; } }; window.__HEALTH_FETCH_PATCHED__=true;}if(!window.__HEALTH_XHR_PATCHED__){ const origOpen=XMLHttpRequest.prototype.open; const origSend=XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open=function(method,url,...rest){ this.__h_method=method;this.__h_url=url;this.__h_t0=Date.now(); return origOpen.apply(this,[method,url,...rest]); }; XMLHttpRequest.prototype.send=function(...args){ this.addEventListener('loadend',function(){ const url=this.__h_url||''; if(url.includes('/api/')||url.includes('/v1/')){ window.__API_HEALTH__.push({url:url.substring(0,120),method:(this.__h_method||'GET').toUpperCase(),status:this.status,duration:Date.now()-(this.__h_t0||Date.now()),ok:this.status>=200&&this.status<400,ts:Date.now()}); } }); return origSend.apply(this,args); }; window.__HEALTH_XHR_PATCHED__=true;}R.interceptorInstalled=true;R.ok=true;return JSON.stringify(R);})()",
"timeout": 5000,
"phase": "INSTALL"
},
{
"id": 10,
"name": "[생산관리 > 품목관리] API 호출 대기",
"action": "wait",
"timeout": 3000
},
{
"id": 11,
"name": "[생산관리 > 품목관리] API 건강성 감사",
"action": "evaluate",
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'API_AUDIT'};const logs=window.__API_HEALTH__||[];R.totalCalls=logs.length;if(logs.length===0){R.warn='API 호출 없음 (인터셉터 미동작 또는 API 없는 페이지)';R.grade='WARN';R.ok=true;return JSON.stringify(R);}const errors4xx=logs.filter(l=>l.status>=400&&l.status<500);const errors5xx=logs.filter(l=>l.status>=500);const networkErrors=logs.filter(l=>l.status===0);const slowCalls=logs.filter(l=>l.duration>2000);const successCalls=logs.filter(l=>l.ok);R.errors4xx=errors4xx.length;R.errors5xx=errors5xx.length;R.networkErrors=networkErrors.length;R.slowCalls=slowCalls.length;R.successCount=successCalls.length;const durations=logs.map(l=>l.duration).filter(d=>d>0);R.avgResponseTime=durations.length>0?Math.round(durations.reduce((a,b)=>a+b,0)/durations.length):0;R.maxResponseTime=durations.length>0?Math.max(...durations):0;R.minResponseTime=durations.length>0?Math.min(...durations):0;const errorRate=logs.length>0?((errors4xx.length+errors5xx.length+networkErrors.length)/logs.length*100):0;R.errorRate=Math.round(errorRate*10)/10;R.failedUrls=[...errors5xx,...errors4xx,...networkErrors].slice(0,5).map(l=>({url:l.url,status:l.status,method:l.method,duration:l.duration}));R.slowUrls=slowCalls.slice(0,3).map(l=>({url:l.url,duration:l.duration,method:l.method}));if(errors5xx.length>0||errorRate>10){R.grade='FAIL';}else if(errors4xx.length>0||slowCalls.length>3||R.avgResponseTime>1000){R.grade='WARN';}else{R.grade='PASS';}R.summary=R.totalCalls+'개 API 호출 | '+R.successCount+'성공 | '+(errors4xx.length+errors5xx.length)+'에러 | '+slowCalls.length+'느림 | 평균 '+R.avgResponseTime+'ms | 등급: '+R.grade;window.__API_HEALTH__=[];R.ok=true;return JSON.stringify(R);})()",
"timeout": 10000,
"phase": "API_AUDIT"
},
{
"id": 12,
"name": "[생산관리 > 작업자 화면] 메뉴 이동",
"action": "menu_navigate",
"level1": "생산관리",
"level2": "작업자 화면",
"timeout": 10000
},
{
"id": 13,
"name": "[생산관리 > 작업자 화면] API 인터셉터 설치",
"action": "evaluate",
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'INSTALL_INTERCEPTOR'};window.__API_HEALTH__=[];window.__API_HEALTH_START__=Date.now();if(!window.__HEALTH_FETCH_PATCHED__){ 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').toUpperCase(); const t0=Date.now(); try{ const resp=await origFetch.apply(this,args); const dur=Date.now()-t0; if(url.includes('/api/')||url.includes('/v1/')){ window.__API_HEALTH__.push({url:url.substring(0,120),method,status:resp.status,duration:dur,ok:resp.ok,ts:Date.now()}); } return resp; }catch(e){ const dur=Date.now()-t0; if(url.includes('/api/')||url.includes('/v1/')){ window.__API_HEALTH__.push({url:url.substring(0,120),method,status:0,duration:dur,ok:false,error:e.message,ts:Date.now()}); } throw e; } }; window.__HEALTH_FETCH_PATCHED__=true;}if(!window.__HEALTH_XHR_PATCHED__){ const origOpen=XMLHttpRequest.prototype.open; const origSend=XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open=function(method,url,...rest){ this.__h_method=method;this.__h_url=url;this.__h_t0=Date.now(); return origOpen.apply(this,[method,url,...rest]); }; XMLHttpRequest.prototype.send=function(...args){ this.addEventListener('loadend',function(){ const url=this.__h_url||''; if(url.includes('/api/')||url.includes('/v1/')){ window.__API_HEALTH__.push({url:url.substring(0,120),method:(this.__h_method||'GET').toUpperCase(),status:this.status,duration:Date.now()-(this.__h_t0||Date.now()),ok:this.status>=200&&this.status<400,ts:Date.now()}); } }); return origSend.apply(this,args); }; window.__HEALTH_XHR_PATCHED__=true;}R.interceptorInstalled=true;R.ok=true;return JSON.stringify(R);})()",
"timeout": 5000,
"phase": "INSTALL"
},
{
"id": 14,
"name": "[생산관리 > 작업자 화면] API 호출 대기",
"action": "wait",
"timeout": 3000
},
{
"id": 15,
"name": "[생산관리 > 작업자 화면] API 건강성 감사",
"action": "evaluate",
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'API_AUDIT'};const logs=window.__API_HEALTH__||[];R.totalCalls=logs.length;if(logs.length===0){R.warn='API 호출 없음 (인터셉터 미동작 또는 API 없는 페이지)';R.grade='WARN';R.ok=true;return JSON.stringify(R);}const errors4xx=logs.filter(l=>l.status>=400&&l.status<500);const errors5xx=logs.filter(l=>l.status>=500);const networkErrors=logs.filter(l=>l.status===0);const slowCalls=logs.filter(l=>l.duration>2000);const successCalls=logs.filter(l=>l.ok);R.errors4xx=errors4xx.length;R.errors5xx=errors5xx.length;R.networkErrors=networkErrors.length;R.slowCalls=slowCalls.length;R.successCount=successCalls.length;const durations=logs.map(l=>l.duration).filter(d=>d>0);R.avgResponseTime=durations.length>0?Math.round(durations.reduce((a,b)=>a+b,0)/durations.length):0;R.maxResponseTime=durations.length>0?Math.max(...durations):0;R.minResponseTime=durations.length>0?Math.min(...durations):0;const errorRate=logs.length>0?((errors4xx.length+errors5xx.length+networkErrors.length)/logs.length*100):0;R.errorRate=Math.round(errorRate*10)/10;R.failedUrls=[...errors5xx,...errors4xx,...networkErrors].slice(0,5).map(l=>({url:l.url,status:l.status,method:l.method,duration:l.duration}));R.slowUrls=slowCalls.slice(0,3).map(l=>({url:l.url,duration:l.duration,method:l.method}));if(errors5xx.length>0||errorRate>10){R.grade='FAIL';}else if(errors4xx.length>0||slowCalls.length>3||R.avgResponseTime>1000){R.grade='WARN';}else{R.grade='PASS';}R.summary=R.totalCalls+'개 API 호출 | '+R.successCount+'성공 | '+(errors4xx.length+errors5xx.length)+'에러 | '+slowCalls.length+'느림 | 평균 '+R.avgResponseTime+'ms | 등급: '+R.grade;window.__API_HEALTH__=[];R.ok=true;return JSON.stringify(R);})()",
"timeout": 10000,
"phase": "API_AUDIT"
},
{
"id": 16,
"name": "[품질관리 > 제품검사관리] 메뉴 이동",
"action": "menu_navigate",
"level1": "품질관리",
"level2": "제품검사관리",
"timeout": 10000
},
{
"id": 17,
"name": "[품질관리 > 제품검사관리] API 인터셉터 설치",
"action": "evaluate",
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'INSTALL_INTERCEPTOR'};window.__API_HEALTH__=[];window.__API_HEALTH_START__=Date.now();if(!window.__HEALTH_FETCH_PATCHED__){ 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').toUpperCase(); const t0=Date.now(); try{ const resp=await origFetch.apply(this,args); const dur=Date.now()-t0; if(url.includes('/api/')||url.includes('/v1/')){ window.__API_HEALTH__.push({url:url.substring(0,120),method,status:resp.status,duration:dur,ok:resp.ok,ts:Date.now()}); } return resp; }catch(e){ const dur=Date.now()-t0; if(url.includes('/api/')||url.includes('/v1/')){ window.__API_HEALTH__.push({url:url.substring(0,120),method,status:0,duration:dur,ok:false,error:e.message,ts:Date.now()}); } throw e; } }; window.__HEALTH_FETCH_PATCHED__=true;}if(!window.__HEALTH_XHR_PATCHED__){ const origOpen=XMLHttpRequest.prototype.open; const origSend=XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open=function(method,url,...rest){ this.__h_method=method;this.__h_url=url;this.__h_t0=Date.now(); return origOpen.apply(this,[method,url,...rest]); }; XMLHttpRequest.prototype.send=function(...args){ this.addEventListener('loadend',function(){ const url=this.__h_url||''; if(url.includes('/api/')||url.includes('/v1/')){ window.__API_HEALTH__.push({url:url.substring(0,120),method:(this.__h_method||'GET').toUpperCase(),status:this.status,duration:Date.now()-(this.__h_t0||Date.now()),ok:this.status>=200&&this.status<400,ts:Date.now()}); } }); return origSend.apply(this,args); }; window.__HEALTH_XHR_PATCHED__=true;}R.interceptorInstalled=true;R.ok=true;return JSON.stringify(R);})()",
"timeout": 5000,
"phase": "INSTALL"
},
{
"id": 18,
"name": "[품질관리 > 제품검사관리] API 호출 대기",
"action": "wait",
"timeout": 3000
},
{
"id": 19,
"name": "[품질관리 > 제품검사관리] API 건강성 감사",
"action": "evaluate",
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'API_AUDIT'};const logs=window.__API_HEALTH__||[];R.totalCalls=logs.length;if(logs.length===0){R.warn='API 호출 없음 (인터셉터 미동작 또는 API 없는 페이지)';R.grade='WARN';R.ok=true;return JSON.stringify(R);}const errors4xx=logs.filter(l=>l.status>=400&&l.status<500);const errors5xx=logs.filter(l=>l.status>=500);const networkErrors=logs.filter(l=>l.status===0);const slowCalls=logs.filter(l=>l.duration>2000);const successCalls=logs.filter(l=>l.ok);R.errors4xx=errors4xx.length;R.errors5xx=errors5xx.length;R.networkErrors=networkErrors.length;R.slowCalls=slowCalls.length;R.successCount=successCalls.length;const durations=logs.map(l=>l.duration).filter(d=>d>0);R.avgResponseTime=durations.length>0?Math.round(durations.reduce((a,b)=>a+b,0)/durations.length):0;R.maxResponseTime=durations.length>0?Math.max(...durations):0;R.minResponseTime=durations.length>0?Math.min(...durations):0;const errorRate=logs.length>0?((errors4xx.length+errors5xx.length+networkErrors.length)/logs.length*100):0;R.errorRate=Math.round(errorRate*10)/10;R.failedUrls=[...errors5xx,...errors4xx,...networkErrors].slice(0,5).map(l=>({url:l.url,status:l.status,method:l.method,duration:l.duration}));R.slowUrls=slowCalls.slice(0,3).map(l=>({url:l.url,duration:l.duration,method:l.method}));if(errors5xx.length>0||errorRate>10){R.grade='FAIL';}else if(errors4xx.length>0||slowCalls.length>3||R.avgResponseTime>1000){R.grade='WARN';}else{R.grade='PASS';}R.summary=R.totalCalls+'개 API 호출 | '+R.successCount+'성공 | '+(errors4xx.length+errors5xx.length)+'에러 | '+slowCalls.length+'느림 | 평균 '+R.avgResponseTime+'ms | 등급: '+R.grade;window.__API_HEALTH__=[];R.ok=true;return JSON.stringify(R);})()",
"timeout": 10000,
"phase": "API_AUDIT"
},
{
"id": 20,
"name": "[자재관리 > 입고관리] 메뉴 이동",
"action": "menu_navigate",
"level1": "자재관리",
"level2": "입고관리",
"timeout": 10000
},
{
"id": 21,
"name": "[자재관리 > 입고관리] API 인터셉터 설치",
"action": "evaluate",
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'INSTALL_INTERCEPTOR'};window.__API_HEALTH__=[];window.__API_HEALTH_START__=Date.now();if(!window.__HEALTH_FETCH_PATCHED__){ 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').toUpperCase(); const t0=Date.now(); try{ const resp=await origFetch.apply(this,args); const dur=Date.now()-t0; if(url.includes('/api/')||url.includes('/v1/')){ window.__API_HEALTH__.push({url:url.substring(0,120),method,status:resp.status,duration:dur,ok:resp.ok,ts:Date.now()}); } return resp; }catch(e){ const dur=Date.now()-t0; if(url.includes('/api/')||url.includes('/v1/')){ window.__API_HEALTH__.push({url:url.substring(0,120),method,status:0,duration:dur,ok:false,error:e.message,ts:Date.now()}); } throw e; } }; window.__HEALTH_FETCH_PATCHED__=true;}if(!window.__HEALTH_XHR_PATCHED__){ const origOpen=XMLHttpRequest.prototype.open; const origSend=XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open=function(method,url,...rest){ this.__h_method=method;this.__h_url=url;this.__h_t0=Date.now(); return origOpen.apply(this,[method,url,...rest]); }; XMLHttpRequest.prototype.send=function(...args){ this.addEventListener('loadend',function(){ const url=this.__h_url||''; if(url.includes('/api/')||url.includes('/v1/')){ window.__API_HEALTH__.push({url:url.substring(0,120),method:(this.__h_method||'GET').toUpperCase(),status:this.status,duration:Date.now()-(this.__h_t0||Date.now()),ok:this.status>=200&&this.status<400,ts:Date.now()}); } }); return origSend.apply(this,args); }; window.__HEALTH_XHR_PATCHED__=true;}R.interceptorInstalled=true;R.ok=true;return JSON.stringify(R);})()",
"timeout": 5000,
"phase": "INSTALL"
},
{
"id": 22,
"name": "[자재관리 > 입고관리] API 호출 대기",
"action": "wait",
"timeout": 3000
},
{
"id": 23,
"name": "[자재관리 > 입고관리] API 건강성 감사",
"action": "evaluate",
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'API_AUDIT'};const logs=window.__API_HEALTH__||[];R.totalCalls=logs.length;if(logs.length===0){R.warn='API 호출 없음 (인터셉터 미동작 또는 API 없는 페이지)';R.grade='WARN';R.ok=true;return JSON.stringify(R);}const errors4xx=logs.filter(l=>l.status>=400&&l.status<500);const errors5xx=logs.filter(l=>l.status>=500);const networkErrors=logs.filter(l=>l.status===0);const slowCalls=logs.filter(l=>l.duration>2000);const successCalls=logs.filter(l=>l.ok);R.errors4xx=errors4xx.length;R.errors5xx=errors5xx.length;R.networkErrors=networkErrors.length;R.slowCalls=slowCalls.length;R.successCount=successCalls.length;const durations=logs.map(l=>l.duration).filter(d=>d>0);R.avgResponseTime=durations.length>0?Math.round(durations.reduce((a,b)=>a+b,0)/durations.length):0;R.maxResponseTime=durations.length>0?Math.max(...durations):0;R.minResponseTime=durations.length>0?Math.min(...durations):0;const errorRate=logs.length>0?((errors4xx.length+errors5xx.length+networkErrors.length)/logs.length*100):0;R.errorRate=Math.round(errorRate*10)/10;R.failedUrls=[...errors5xx,...errors4xx,...networkErrors].slice(0,5).map(l=>({url:l.url,status:l.status,method:l.method,duration:l.duration}));R.slowUrls=slowCalls.slice(0,3).map(l=>({url:l.url,duration:l.duration,method:l.method}));if(errors5xx.length>0||errorRate>10){R.grade='FAIL';}else if(errors4xx.length>0||slowCalls.length>3||R.avgResponseTime>1000){R.grade='WARN';}else{R.grade='PASS';}R.summary=R.totalCalls+'개 API 호출 | '+R.successCount+'성공 | '+(errors4xx.length+errors5xx.length)+'에러 | '+slowCalls.length+'느림 | 평균 '+R.avgResponseTime+'ms | 등급: '+R.grade;window.__API_HEALTH__=[];R.ok=true;return JSON.stringify(R);})()",
"timeout": 10000,
"phase": "API_AUDIT"
},
{
"id": 24,
"name": "[자재관리 > 재고현황] 메뉴 이동",
"action": "menu_navigate",
"level1": "자재관리",
"level2": "재고현황",
"timeout": 10000
},
{
"id": 25,
"name": "[자재관리 > 재고현황] API 인터셉터 설치",
"action": "evaluate",
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'INSTALL_INTERCEPTOR'};window.__API_HEALTH__=[];window.__API_HEALTH_START__=Date.now();if(!window.__HEALTH_FETCH_PATCHED__){ 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').toUpperCase(); const t0=Date.now(); try{ const resp=await origFetch.apply(this,args); const dur=Date.now()-t0; if(url.includes('/api/')||url.includes('/v1/')){ window.__API_HEALTH__.push({url:url.substring(0,120),method,status:resp.status,duration:dur,ok:resp.ok,ts:Date.now()}); } return resp; }catch(e){ const dur=Date.now()-t0; if(url.includes('/api/')||url.includes('/v1/')){ window.__API_HEALTH__.push({url:url.substring(0,120),method,status:0,duration:dur,ok:false,error:e.message,ts:Date.now()}); } throw e; } }; window.__HEALTH_FETCH_PATCHED__=true;}if(!window.__HEALTH_XHR_PATCHED__){ const origOpen=XMLHttpRequest.prototype.open; const origSend=XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open=function(method,url,...rest){ this.__h_method=method;this.__h_url=url;this.__h_t0=Date.now(); return origOpen.apply(this,[method,url,...rest]); }; XMLHttpRequest.prototype.send=function(...args){ this.addEventListener('loadend',function(){ const url=this.__h_url||''; if(url.includes('/api/')||url.includes('/v1/')){ window.__API_HEALTH__.push({url:url.substring(0,120),method:(this.__h_method||'GET').toUpperCase(),status:this.status,duration:Date.now()-(this.__h_t0||Date.now()),ok:this.status>=200&&this.status<400,ts:Date.now()}); } }); return origSend.apply(this,args); }; window.__HEALTH_XHR_PATCHED__=true;}R.interceptorInstalled=true;R.ok=true;return JSON.stringify(R);})()",
"timeout": 5000,
"phase": "INSTALL"
},
{
"id": 26,
"name": "[자재관리 > 재고현황] API 호출 대기",
"action": "wait",
"timeout": 3000
},
{
"id": 27,
"name": "[자재관리 > 재고현황] API 건강성 감사",
"action": "evaluate",
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'API_AUDIT'};const logs=window.__API_HEALTH__||[];R.totalCalls=logs.length;if(logs.length===0){R.warn='API 호출 없음 (인터셉터 미동작 또는 API 없는 페이지)';R.grade='WARN';R.ok=true;return JSON.stringify(R);}const errors4xx=logs.filter(l=>l.status>=400&&l.status<500);const errors5xx=logs.filter(l=>l.status>=500);const networkErrors=logs.filter(l=>l.status===0);const slowCalls=logs.filter(l=>l.duration>2000);const successCalls=logs.filter(l=>l.ok);R.errors4xx=errors4xx.length;R.errors5xx=errors5xx.length;R.networkErrors=networkErrors.length;R.slowCalls=slowCalls.length;R.successCount=successCalls.length;const durations=logs.map(l=>l.duration).filter(d=>d>0);R.avgResponseTime=durations.length>0?Math.round(durations.reduce((a,b)=>a+b,0)/durations.length):0;R.maxResponseTime=durations.length>0?Math.max(...durations):0;R.minResponseTime=durations.length>0?Math.min(...durations):0;const errorRate=logs.length>0?((errors4xx.length+errors5xx.length+networkErrors.length)/logs.length*100):0;R.errorRate=Math.round(errorRate*10)/10;R.failedUrls=[...errors5xx,...errors4xx,...networkErrors].slice(0,5).map(l=>({url:l.url,status:l.status,method:l.method,duration:l.duration}));R.slowUrls=slowCalls.slice(0,3).map(l=>({url:l.url,duration:l.duration,method:l.method}));if(errors5xx.length>0||errorRate>10){R.grade='FAIL';}else if(errors4xx.length>0||slowCalls.length>3||R.avgResponseTime>1000){R.grade='WARN';}else{R.grade='PASS';}R.summary=R.totalCalls+'개 API 호출 | '+R.successCount+'성공 | '+(errors4xx.length+errors5xx.length)+'에러 | '+slowCalls.length+'느림 | 평균 '+R.avgResponseTime+'ms | 등급: '+R.grade;window.__API_HEALTH__=[];R.ok=true;return JSON.stringify(R);})()",
"timeout": 10000,
"phase": "API_AUDIT"
},
{
"id": 28,
"name": "[게시판 > 자유게시판] 메뉴 이동",
"action": "menu_navigate",
"level1": "게시판",
"level2": "자유게시판",
"timeout": 10000
},
{
"id": 29,
"name": "[게시판 > 자유게시판] API 인터셉터 설치",
"action": "evaluate",
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'INSTALL_INTERCEPTOR'};window.__API_HEALTH__=[];window.__API_HEALTH_START__=Date.now();if(!window.__HEALTH_FETCH_PATCHED__){ 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').toUpperCase(); const t0=Date.now(); try{ const resp=await origFetch.apply(this,args); const dur=Date.now()-t0; if(url.includes('/api/')||url.includes('/v1/')){ window.__API_HEALTH__.push({url:url.substring(0,120),method,status:resp.status,duration:dur,ok:resp.ok,ts:Date.now()}); } return resp; }catch(e){ const dur=Date.now()-t0; if(url.includes('/api/')||url.includes('/v1/')){ window.__API_HEALTH__.push({url:url.substring(0,120),method,status:0,duration:dur,ok:false,error:e.message,ts:Date.now()}); } throw e; } }; window.__HEALTH_FETCH_PATCHED__=true;}if(!window.__HEALTH_XHR_PATCHED__){ const origOpen=XMLHttpRequest.prototype.open; const origSend=XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open=function(method,url,...rest){ this.__h_method=method;this.__h_url=url;this.__h_t0=Date.now(); return origOpen.apply(this,[method,url,...rest]); }; XMLHttpRequest.prototype.send=function(...args){ this.addEventListener('loadend',function(){ const url=this.__h_url||''; if(url.includes('/api/')||url.includes('/v1/')){ window.__API_HEALTH__.push({url:url.substring(0,120),method:(this.__h_method||'GET').toUpperCase(),status:this.status,duration:Date.now()-(this.__h_t0||Date.now()),ok:this.status>=200&&this.status<400,ts:Date.now()}); } }); return origSend.apply(this,args); }; window.__HEALTH_XHR_PATCHED__=true;}R.interceptorInstalled=true;R.ok=true;return JSON.stringify(R);})()",
"timeout": 5000,
"phase": "INSTALL"
},
{
"id": 30,
"name": "[게시판 > 자유게시판] API 호출 대기",
"action": "wait",
"timeout": 3000
},
{
"id": 31,
"name": "[게시판 > 자유게시판] API 건강성 감사",
"action": "evaluate",
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'API_AUDIT'};const logs=window.__API_HEALTH__||[];R.totalCalls=logs.length;if(logs.length===0){R.warn='API 호출 없음 (인터셉터 미동작 또는 API 없는 페이지)';R.grade='WARN';R.ok=true;return JSON.stringify(R);}const errors4xx=logs.filter(l=>l.status>=400&&l.status<500);const errors5xx=logs.filter(l=>l.status>=500);const networkErrors=logs.filter(l=>l.status===0);const slowCalls=logs.filter(l=>l.duration>2000);const successCalls=logs.filter(l=>l.ok);R.errors4xx=errors4xx.length;R.errors5xx=errors5xx.length;R.networkErrors=networkErrors.length;R.slowCalls=slowCalls.length;R.successCount=successCalls.length;const durations=logs.map(l=>l.duration).filter(d=>d>0);R.avgResponseTime=durations.length>0?Math.round(durations.reduce((a,b)=>a+b,0)/durations.length):0;R.maxResponseTime=durations.length>0?Math.max(...durations):0;R.minResponseTime=durations.length>0?Math.min(...durations):0;const errorRate=logs.length>0?((errors4xx.length+errors5xx.length+networkErrors.length)/logs.length*100):0;R.errorRate=Math.round(errorRate*10)/10;R.failedUrls=[...errors5xx,...errors4xx,...networkErrors].slice(0,5).map(l=>({url:l.url,status:l.status,method:l.method,duration:l.duration}));R.slowUrls=slowCalls.slice(0,3).map(l=>({url:l.url,duration:l.duration,method:l.method}));if(errors5xx.length>0||errorRate>10){R.grade='FAIL';}else if(errors4xx.length>0||slowCalls.length>3||R.avgResponseTime>1000){R.grade='WARN';}else{R.grade='PASS';}R.summary=R.totalCalls+'개 API 호출 | '+R.successCount+'성공 | '+(errors4xx.length+errors5xx.length)+'에러 | '+slowCalls.length+'느림 | 평균 '+R.avgResponseTime+'ms | 등급: '+R.grade;window.__API_HEALTH__=[];R.ok=true;return JSON.stringify(R);})()",
"timeout": 10000,
"phase": "API_AUDIT"
},
{
"id": 32,
"name": "[게시판 > 공지사항] 메뉴 이동",
"action": "menu_navigate",
"level1": "게시판",
"level2": "공지사항",
"timeout": 10000
},
{
"id": 33,
"name": "[게시판 > 공지사항] API 인터셉터 설치",
"action": "evaluate",
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'INSTALL_INTERCEPTOR'};window.__API_HEALTH__=[];window.__API_HEALTH_START__=Date.now();if(!window.__HEALTH_FETCH_PATCHED__){ 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').toUpperCase(); const t0=Date.now(); try{ const resp=await origFetch.apply(this,args); const dur=Date.now()-t0; if(url.includes('/api/')||url.includes('/v1/')){ window.__API_HEALTH__.push({url:url.substring(0,120),method,status:resp.status,duration:dur,ok:resp.ok,ts:Date.now()}); } return resp; }catch(e){ const dur=Date.now()-t0; if(url.includes('/api/')||url.includes('/v1/')){ window.__API_HEALTH__.push({url:url.substring(0,120),method,status:0,duration:dur,ok:false,error:e.message,ts:Date.now()}); } throw e; } }; window.__HEALTH_FETCH_PATCHED__=true;}if(!window.__HEALTH_XHR_PATCHED__){ const origOpen=XMLHttpRequest.prototype.open; const origSend=XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open=function(method,url,...rest){ this.__h_method=method;this.__h_url=url;this.__h_t0=Date.now(); return origOpen.apply(this,[method,url,...rest]); }; XMLHttpRequest.prototype.send=function(...args){ this.addEventListener('loadend',function(){ const url=this.__h_url||''; if(url.includes('/api/')||url.includes('/v1/')){ window.__API_HEALTH__.push({url:url.substring(0,120),method:(this.__h_method||'GET').toUpperCase(),status:this.status,duration:Date.now()-(this.__h_t0||Date.now()),ok:this.status>=200&&this.status<400,ts:Date.now()}); } }); return origSend.apply(this,args); }; window.__HEALTH_XHR_PATCHED__=true;}R.interceptorInstalled=true;R.ok=true;return JSON.stringify(R);})()",
"timeout": 5000,
"phase": "INSTALL"
},
{
"id": 34,
"name": "[게시판 > 공지사항] API 호출 대기",
"action": "wait",
"timeout": 3000
},
{
"id": 35,
"name": "[게시판 > 공지사항] API 건강성 감사",
"action": "evaluate",
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'API_AUDIT'};const logs=window.__API_HEALTH__||[];R.totalCalls=logs.length;if(logs.length===0){R.warn='API 호출 없음 (인터셉터 미동작 또는 API 없는 페이지)';R.grade='WARN';R.ok=true;return JSON.stringify(R);}const errors4xx=logs.filter(l=>l.status>=400&&l.status<500);const errors5xx=logs.filter(l=>l.status>=500);const networkErrors=logs.filter(l=>l.status===0);const slowCalls=logs.filter(l=>l.duration>2000);const successCalls=logs.filter(l=>l.ok);R.errors4xx=errors4xx.length;R.errors5xx=errors5xx.length;R.networkErrors=networkErrors.length;R.slowCalls=slowCalls.length;R.successCount=successCalls.length;const durations=logs.map(l=>l.duration).filter(d=>d>0);R.avgResponseTime=durations.length>0?Math.round(durations.reduce((a,b)=>a+b,0)/durations.length):0;R.maxResponseTime=durations.length>0?Math.max(...durations):0;R.minResponseTime=durations.length>0?Math.min(...durations):0;const errorRate=logs.length>0?((errors4xx.length+errors5xx.length+networkErrors.length)/logs.length*100):0;R.errorRate=Math.round(errorRate*10)/10;R.failedUrls=[...errors5xx,...errors4xx,...networkErrors].slice(0,5).map(l=>({url:l.url,status:l.status,method:l.method,duration:l.duration}));R.slowUrls=slowCalls.slice(0,3).map(l=>({url:l.url,duration:l.duration,method:l.method}));if(errors5xx.length>0||errorRate>10){R.grade='FAIL';}else if(errors4xx.length>0||slowCalls.length>3||R.avgResponseTime>1000){R.grade='WARN';}else{R.grade='PASS';}R.summary=R.totalCalls+'개 API 호출 | '+R.successCount+'성공 | '+(errors4xx.length+errors5xx.length)+'에러 | '+slowCalls.length+'느림 | 평균 '+R.avgResponseTime+'ms | 등급: '+R.grade;window.__API_HEALTH__=[];R.ok=true;return JSON.stringify(R);})()",
"timeout": 10000,
"phase": "API_AUDIT"
}
]
}