From f2e757fef718693e35c6f0c588dc0a1192989a93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Thu, 12 Feb 2026 15:57:02 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20api-health=20=EC=8B=9C=EB=82=98=EB=A6=AC?= =?UTF-8?q?=EC=98=A4=20v2.0=20(=EB=82=B4=EC=9E=A5=20ApiMonitor=20+=20Perfo?= =?UTF-8?q?rmance=20API=20=ED=95=98=EC=9D=B4=EB=B8=8C=EB=A6=AC=EB=93=9C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 --- api-health-acc.json | 172 +++++++++++++++++++------------------- api-health-prod-misc.json | 154 +++++++++++++++++----------------- api-health-sales-hr.json | 154 +++++++++++++++++----------------- 3 files changed, 240 insertions(+), 240 deletions(-) diff --git a/api-health-acc.json b/api-health-acc.json index 11e262e..16c7dcd 100644 --- a/api-health-acc.json +++ b/api-health-acc.json @@ -1,7 +1,7 @@ { "id": "api-health-acc", "name": "API 건강성 감사: 회계", - "version": "1.0.0", + "version": "2.0.0", "auth": { "role": "admin" }, @@ -16,11 +16,11 @@ "steps": [ { "id": 1, - "name": "[회계관리 > 거래처관리] API 인터셉터 설치", + "name": "[회계관리 > 거래처관리] 마커 기록", "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);})()", + "script": "(async()=>{const apiLogs=window.__E2E__?window.__E2E__.getApiLogs():{logs:[],summary:{}};window.__AH_MARK__={ ts:Date.now(), apiLogCount:apiLogs.logs.length, perfCount:performance.getEntriesByType('resource').length};return JSON.stringify({phase:'MARK_START',ok:true,info:'apiLogs:'+apiLogs.logs.length+' perf:'+window.__AH_MARK__.perfCount});})()", "timeout": 5000, - "phase": "INSTALL" + "phase": "MARK" }, { "id": 2, @@ -32,26 +32,26 @@ "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);})()", + "script": "(async()=>{const R={phase:'API_AUDIT'};const mark=window.__AH_MARK__||{ts:0,apiLogCount:0,perfCount:0};const apiData=window.__E2E__?window.__E2E__.getApiLogs():{logs:[],errors:[],summary:{}};const newApiLogs=apiData.logs.slice(mark.apiLogCount);R.monitorCalls=newApiLogs.length;const allRes=performance.getEntriesByType('resource');const newRes=allRes.slice(mark.perfCount);const apiRes=newRes.filter(e=>e.name.includes('/api/')||e.name.includes('/v1/')||e.name.includes('/graphql'));R.perfApiCalls=apiRes.length;const jsRes=newRes.filter(e=>e.initiatorType==='script'||e.name.endsWith('.js'));const cssRes=newRes.filter(e=>e.initiatorType==='link'||e.name.endsWith('.css'));const imgRes=newRes.filter(e=>e.initiatorType==='img'||e.name.match(new RegExp('\\\\.(png|jpg|svg|gif|webp)')));R.resourceBreakdown={total:newRes.length,api:apiRes.length,js:jsRes.length,css:cssRes.length,img:imgRes.length};R.totalCalls=Math.max(newApiLogs.length,apiRes.length);R.totalResources=newRes.length;if(R.totalCalls===0){ if(newRes.length>0){ const avgDur=Math.round(newRes.reduce((s,e)=>s+e.duration,0)/newRes.length); R.grade='PASS';R.ok=true; R.summary=newRes.length+'개 리소스 로드(API 0) | avg '+avgDur+'ms | PASS'; R.avgResponseTime=avgDur; }else{ R.grade='PASS';R.ok=true; R.summary='리소스/API 호출 없음 (SPA 캐시) | PASS'; } return JSON.stringify(R);}if(newApiLogs.length>0){ const errors4xx=newApiLogs.filter(l=>l.status>=400&&l.status<500); const errors5xx=newApiLogs.filter(l=>l.status>=500); const netErr=newApiLogs.filter(l=>!l.ok&&l.status===0); const slow=newApiLogs.filter(l=>l.duration>2000); const ok=newApiLogs.filter(l=>l.ok); R.errors4xx=errors4xx.length;R.errors5xx=errors5xx.length; R.slowCalls=slow.length;R.successCount=ok.length; const durs=newApiLogs.map(l=>l.duration).filter(d=>d>0); R.avgResponseTime=durs.length>0?Math.round(durs.reduce((a,b)=>a+b,0)/durs.length):0; R.maxResponseTime=durs.length>0?Math.max(...durs):0; const errRate=((errors4xx.length+errors5xx.length+netErr.length)/newApiLogs.length*100); R.errorRate=Math.round(errRate*10)/10; R.failedUrls=[...errors5xx,...errors4xx].slice(0,3).map(l=>l.url.split('?')[0].substring(0,80)); if(errors5xx.length>0||errRate>10){R.grade='FAIL';} else if(errors4xx.length>0||slow.length>3||R.avgResponseTime>1000){R.grade='WARN';} else{R.grade='PASS';} R.summary=newApiLogs.length+'개 API | '+ok.length+'OK '+(errors4xx.length+errors5xx.length)+'err '+slow.length+'slow | avg '+R.avgResponseTime+'ms | '+R.grade;}else{ const durs=apiRes.map(e=>Math.round(e.duration)); R.avgResponseTime=durs.length>0?Math.round(durs.reduce((a,b)=>a+b,0)/durs.length):0; R.slowCalls=apiRes.filter(e=>e.duration>2000).length; if(R.slowCalls>3||R.avgResponseTime>1000){R.grade='WARN';} else{R.grade='PASS';} R.summary=apiRes.length+'개 API(perf) | avg '+R.avgResponseTime+'ms | '+R.grade;}R.ok=true;return JSON.stringify(R);})()", "timeout": 10000, "phase": "API_AUDIT" }, { "id": 4, + "name": "[회계관리 > 어음관리] 마커 기록", + "action": "evaluate", + "script": "(async()=>{const apiLogs=window.__E2E__?window.__E2E__.getApiLogs():{logs:[],summary:{}};window.__AH_MARK__={ ts:Date.now(), apiLogCount:apiLogs.logs.length, perfCount:performance.getEntriesByType('resource').length};return JSON.stringify({phase:'MARK_START',ok:true,info:'apiLogs:'+apiLogs.logs.length+' perf:'+window.__AH_MARK__.perfCount});})()", + "timeout": 3000, + "phase": "MARK" + }, + { + "id": 5, "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 호출 대기", @@ -62,26 +62,26 @@ "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);})()", + "script": "(async()=>{const R={phase:'API_AUDIT'};const mark=window.__AH_MARK__||{ts:0,apiLogCount:0,perfCount:0};const apiData=window.__E2E__?window.__E2E__.getApiLogs():{logs:[],errors:[],summary:{}};const newApiLogs=apiData.logs.slice(mark.apiLogCount);R.monitorCalls=newApiLogs.length;const allRes=performance.getEntriesByType('resource');const newRes=allRes.slice(mark.perfCount);const apiRes=newRes.filter(e=>e.name.includes('/api/')||e.name.includes('/v1/')||e.name.includes('/graphql'));R.perfApiCalls=apiRes.length;const jsRes=newRes.filter(e=>e.initiatorType==='script'||e.name.endsWith('.js'));const cssRes=newRes.filter(e=>e.initiatorType==='link'||e.name.endsWith('.css'));const imgRes=newRes.filter(e=>e.initiatorType==='img'||e.name.match(new RegExp('\\\\.(png|jpg|svg|gif|webp)')));R.resourceBreakdown={total:newRes.length,api:apiRes.length,js:jsRes.length,css:cssRes.length,img:imgRes.length};R.totalCalls=Math.max(newApiLogs.length,apiRes.length);R.totalResources=newRes.length;if(R.totalCalls===0){ if(newRes.length>0){ const avgDur=Math.round(newRes.reduce((s,e)=>s+e.duration,0)/newRes.length); R.grade='PASS';R.ok=true; R.summary=newRes.length+'개 리소스 로드(API 0) | avg '+avgDur+'ms | PASS'; R.avgResponseTime=avgDur; }else{ R.grade='PASS';R.ok=true; R.summary='리소스/API 호출 없음 (SPA 캐시) | PASS'; } return JSON.stringify(R);}if(newApiLogs.length>0){ const errors4xx=newApiLogs.filter(l=>l.status>=400&&l.status<500); const errors5xx=newApiLogs.filter(l=>l.status>=500); const netErr=newApiLogs.filter(l=>!l.ok&&l.status===0); const slow=newApiLogs.filter(l=>l.duration>2000); const ok=newApiLogs.filter(l=>l.ok); R.errors4xx=errors4xx.length;R.errors5xx=errors5xx.length; R.slowCalls=slow.length;R.successCount=ok.length; const durs=newApiLogs.map(l=>l.duration).filter(d=>d>0); R.avgResponseTime=durs.length>0?Math.round(durs.reduce((a,b)=>a+b,0)/durs.length):0; R.maxResponseTime=durs.length>0?Math.max(...durs):0; const errRate=((errors4xx.length+errors5xx.length+netErr.length)/newApiLogs.length*100); R.errorRate=Math.round(errRate*10)/10; R.failedUrls=[...errors5xx,...errors4xx].slice(0,3).map(l=>l.url.split('?')[0].substring(0,80)); if(errors5xx.length>0||errRate>10){R.grade='FAIL';} else if(errors4xx.length>0||slow.length>3||R.avgResponseTime>1000){R.grade='WARN';} else{R.grade='PASS';} R.summary=newApiLogs.length+'개 API | '+ok.length+'OK '+(errors4xx.length+errors5xx.length)+'err '+slow.length+'slow | avg '+R.avgResponseTime+'ms | '+R.grade;}else{ const durs=apiRes.map(e=>Math.round(e.duration)); R.avgResponseTime=durs.length>0?Math.round(durs.reduce((a,b)=>a+b,0)/durs.length):0; R.slowCalls=apiRes.filter(e=>e.duration>2000).length; if(R.slowCalls>3||R.avgResponseTime>1000){R.grade='WARN';} else{R.grade='PASS';} R.summary=apiRes.length+'개 API(perf) | avg '+R.avgResponseTime+'ms | '+R.grade;}R.ok=true;return JSON.stringify(R);})()", "timeout": 10000, "phase": "API_AUDIT" }, { "id": 8, + "name": "[회계관리 > 입금관리] 마커 기록", + "action": "evaluate", + "script": "(async()=>{const apiLogs=window.__E2E__?window.__E2E__.getApiLogs():{logs:[],summary:{}};window.__AH_MARK__={ ts:Date.now(), apiLogCount:apiLogs.logs.length, perfCount:performance.getEntriesByType('resource').length};return JSON.stringify({phase:'MARK_START',ok:true,info:'apiLogs:'+apiLogs.logs.length+' perf:'+window.__AH_MARK__.perfCount});})()", + "timeout": 3000, + "phase": "MARK" + }, + { + "id": 9, "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 호출 대기", @@ -92,26 +92,26 @@ "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);})()", + "script": "(async()=>{const R={phase:'API_AUDIT'};const mark=window.__AH_MARK__||{ts:0,apiLogCount:0,perfCount:0};const apiData=window.__E2E__?window.__E2E__.getApiLogs():{logs:[],errors:[],summary:{}};const newApiLogs=apiData.logs.slice(mark.apiLogCount);R.monitorCalls=newApiLogs.length;const allRes=performance.getEntriesByType('resource');const newRes=allRes.slice(mark.perfCount);const apiRes=newRes.filter(e=>e.name.includes('/api/')||e.name.includes('/v1/')||e.name.includes('/graphql'));R.perfApiCalls=apiRes.length;const jsRes=newRes.filter(e=>e.initiatorType==='script'||e.name.endsWith('.js'));const cssRes=newRes.filter(e=>e.initiatorType==='link'||e.name.endsWith('.css'));const imgRes=newRes.filter(e=>e.initiatorType==='img'||e.name.match(new RegExp('\\\\.(png|jpg|svg|gif|webp)')));R.resourceBreakdown={total:newRes.length,api:apiRes.length,js:jsRes.length,css:cssRes.length,img:imgRes.length};R.totalCalls=Math.max(newApiLogs.length,apiRes.length);R.totalResources=newRes.length;if(R.totalCalls===0){ if(newRes.length>0){ const avgDur=Math.round(newRes.reduce((s,e)=>s+e.duration,0)/newRes.length); R.grade='PASS';R.ok=true; R.summary=newRes.length+'개 리소스 로드(API 0) | avg '+avgDur+'ms | PASS'; R.avgResponseTime=avgDur; }else{ R.grade='PASS';R.ok=true; R.summary='리소스/API 호출 없음 (SPA 캐시) | PASS'; } return JSON.stringify(R);}if(newApiLogs.length>0){ const errors4xx=newApiLogs.filter(l=>l.status>=400&&l.status<500); const errors5xx=newApiLogs.filter(l=>l.status>=500); const netErr=newApiLogs.filter(l=>!l.ok&&l.status===0); const slow=newApiLogs.filter(l=>l.duration>2000); const ok=newApiLogs.filter(l=>l.ok); R.errors4xx=errors4xx.length;R.errors5xx=errors5xx.length; R.slowCalls=slow.length;R.successCount=ok.length; const durs=newApiLogs.map(l=>l.duration).filter(d=>d>0); R.avgResponseTime=durs.length>0?Math.round(durs.reduce((a,b)=>a+b,0)/durs.length):0; R.maxResponseTime=durs.length>0?Math.max(...durs):0; const errRate=((errors4xx.length+errors5xx.length+netErr.length)/newApiLogs.length*100); R.errorRate=Math.round(errRate*10)/10; R.failedUrls=[...errors5xx,...errors4xx].slice(0,3).map(l=>l.url.split('?')[0].substring(0,80)); if(errors5xx.length>0||errRate>10){R.grade='FAIL';} else if(errors4xx.length>0||slow.length>3||R.avgResponseTime>1000){R.grade='WARN';} else{R.grade='PASS';} R.summary=newApiLogs.length+'개 API | '+ok.length+'OK '+(errors4xx.length+errors5xx.length)+'err '+slow.length+'slow | avg '+R.avgResponseTime+'ms | '+R.grade;}else{ const durs=apiRes.map(e=>Math.round(e.duration)); R.avgResponseTime=durs.length>0?Math.round(durs.reduce((a,b)=>a+b,0)/durs.length):0; R.slowCalls=apiRes.filter(e=>e.duration>2000).length; if(R.slowCalls>3||R.avgResponseTime>1000){R.grade='WARN';} else{R.grade='PASS';} R.summary=apiRes.length+'개 API(perf) | avg '+R.avgResponseTime+'ms | '+R.grade;}R.ok=true;return JSON.stringify(R);})()", "timeout": 10000, "phase": "API_AUDIT" }, { "id": 12, + "name": "[회계관리 > 출금관리] 마커 기록", + "action": "evaluate", + "script": "(async()=>{const apiLogs=window.__E2E__?window.__E2E__.getApiLogs():{logs:[],summary:{}};window.__AH_MARK__={ ts:Date.now(), apiLogCount:apiLogs.logs.length, perfCount:performance.getEntriesByType('resource').length};return JSON.stringify({phase:'MARK_START',ok:true,info:'apiLogs:'+apiLogs.logs.length+' perf:'+window.__AH_MARK__.perfCount});})()", + "timeout": 3000, + "phase": "MARK" + }, + { + "id": 13, "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 호출 대기", @@ -122,26 +122,26 @@ "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);})()", + "script": "(async()=>{const R={phase:'API_AUDIT'};const mark=window.__AH_MARK__||{ts:0,apiLogCount:0,perfCount:0};const apiData=window.__E2E__?window.__E2E__.getApiLogs():{logs:[],errors:[],summary:{}};const newApiLogs=apiData.logs.slice(mark.apiLogCount);R.monitorCalls=newApiLogs.length;const allRes=performance.getEntriesByType('resource');const newRes=allRes.slice(mark.perfCount);const apiRes=newRes.filter(e=>e.name.includes('/api/')||e.name.includes('/v1/')||e.name.includes('/graphql'));R.perfApiCalls=apiRes.length;const jsRes=newRes.filter(e=>e.initiatorType==='script'||e.name.endsWith('.js'));const cssRes=newRes.filter(e=>e.initiatorType==='link'||e.name.endsWith('.css'));const imgRes=newRes.filter(e=>e.initiatorType==='img'||e.name.match(new RegExp('\\\\.(png|jpg|svg|gif|webp)')));R.resourceBreakdown={total:newRes.length,api:apiRes.length,js:jsRes.length,css:cssRes.length,img:imgRes.length};R.totalCalls=Math.max(newApiLogs.length,apiRes.length);R.totalResources=newRes.length;if(R.totalCalls===0){ if(newRes.length>0){ const avgDur=Math.round(newRes.reduce((s,e)=>s+e.duration,0)/newRes.length); R.grade='PASS';R.ok=true; R.summary=newRes.length+'개 리소스 로드(API 0) | avg '+avgDur+'ms | PASS'; R.avgResponseTime=avgDur; }else{ R.grade='PASS';R.ok=true; R.summary='리소스/API 호출 없음 (SPA 캐시) | PASS'; } return JSON.stringify(R);}if(newApiLogs.length>0){ const errors4xx=newApiLogs.filter(l=>l.status>=400&&l.status<500); const errors5xx=newApiLogs.filter(l=>l.status>=500); const netErr=newApiLogs.filter(l=>!l.ok&&l.status===0); const slow=newApiLogs.filter(l=>l.duration>2000); const ok=newApiLogs.filter(l=>l.ok); R.errors4xx=errors4xx.length;R.errors5xx=errors5xx.length; R.slowCalls=slow.length;R.successCount=ok.length; const durs=newApiLogs.map(l=>l.duration).filter(d=>d>0); R.avgResponseTime=durs.length>0?Math.round(durs.reduce((a,b)=>a+b,0)/durs.length):0; R.maxResponseTime=durs.length>0?Math.max(...durs):0; const errRate=((errors4xx.length+errors5xx.length+netErr.length)/newApiLogs.length*100); R.errorRate=Math.round(errRate*10)/10; R.failedUrls=[...errors5xx,...errors4xx].slice(0,3).map(l=>l.url.split('?')[0].substring(0,80)); if(errors5xx.length>0||errRate>10){R.grade='FAIL';} else if(errors4xx.length>0||slow.length>3||R.avgResponseTime>1000){R.grade='WARN';} else{R.grade='PASS';} R.summary=newApiLogs.length+'개 API | '+ok.length+'OK '+(errors4xx.length+errors5xx.length)+'err '+slow.length+'slow | avg '+R.avgResponseTime+'ms | '+R.grade;}else{ const durs=apiRes.map(e=>Math.round(e.duration)); R.avgResponseTime=durs.length>0?Math.round(durs.reduce((a,b)=>a+b,0)/durs.length):0; R.slowCalls=apiRes.filter(e=>e.duration>2000).length; if(R.slowCalls>3||R.avgResponseTime>1000){R.grade='WARN';} else{R.grade='PASS';} R.summary=apiRes.length+'개 API(perf) | avg '+R.avgResponseTime+'ms | '+R.grade;}R.ok=true;return JSON.stringify(R);})()", "timeout": 10000, "phase": "API_AUDIT" }, { "id": 16, + "name": "[회계관리 > 매출관리] 마커 기록", + "action": "evaluate", + "script": "(async()=>{const apiLogs=window.__E2E__?window.__E2E__.getApiLogs():{logs:[],summary:{}};window.__AH_MARK__={ ts:Date.now(), apiLogCount:apiLogs.logs.length, perfCount:performance.getEntriesByType('resource').length};return JSON.stringify({phase:'MARK_START',ok:true,info:'apiLogs:'+apiLogs.logs.length+' perf:'+window.__AH_MARK__.perfCount});})()", + "timeout": 3000, + "phase": "MARK" + }, + { + "id": 17, "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 호출 대기", @@ -152,26 +152,26 @@ "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);})()", + "script": "(async()=>{const R={phase:'API_AUDIT'};const mark=window.__AH_MARK__||{ts:0,apiLogCount:0,perfCount:0};const apiData=window.__E2E__?window.__E2E__.getApiLogs():{logs:[],errors:[],summary:{}};const newApiLogs=apiData.logs.slice(mark.apiLogCount);R.monitorCalls=newApiLogs.length;const allRes=performance.getEntriesByType('resource');const newRes=allRes.slice(mark.perfCount);const apiRes=newRes.filter(e=>e.name.includes('/api/')||e.name.includes('/v1/')||e.name.includes('/graphql'));R.perfApiCalls=apiRes.length;const jsRes=newRes.filter(e=>e.initiatorType==='script'||e.name.endsWith('.js'));const cssRes=newRes.filter(e=>e.initiatorType==='link'||e.name.endsWith('.css'));const imgRes=newRes.filter(e=>e.initiatorType==='img'||e.name.match(new RegExp('\\\\.(png|jpg|svg|gif|webp)')));R.resourceBreakdown={total:newRes.length,api:apiRes.length,js:jsRes.length,css:cssRes.length,img:imgRes.length};R.totalCalls=Math.max(newApiLogs.length,apiRes.length);R.totalResources=newRes.length;if(R.totalCalls===0){ if(newRes.length>0){ const avgDur=Math.round(newRes.reduce((s,e)=>s+e.duration,0)/newRes.length); R.grade='PASS';R.ok=true; R.summary=newRes.length+'개 리소스 로드(API 0) | avg '+avgDur+'ms | PASS'; R.avgResponseTime=avgDur; }else{ R.grade='PASS';R.ok=true; R.summary='리소스/API 호출 없음 (SPA 캐시) | PASS'; } return JSON.stringify(R);}if(newApiLogs.length>0){ const errors4xx=newApiLogs.filter(l=>l.status>=400&&l.status<500); const errors5xx=newApiLogs.filter(l=>l.status>=500); const netErr=newApiLogs.filter(l=>!l.ok&&l.status===0); const slow=newApiLogs.filter(l=>l.duration>2000); const ok=newApiLogs.filter(l=>l.ok); R.errors4xx=errors4xx.length;R.errors5xx=errors5xx.length; R.slowCalls=slow.length;R.successCount=ok.length; const durs=newApiLogs.map(l=>l.duration).filter(d=>d>0); R.avgResponseTime=durs.length>0?Math.round(durs.reduce((a,b)=>a+b,0)/durs.length):0; R.maxResponseTime=durs.length>0?Math.max(...durs):0; const errRate=((errors4xx.length+errors5xx.length+netErr.length)/newApiLogs.length*100); R.errorRate=Math.round(errRate*10)/10; R.failedUrls=[...errors5xx,...errors4xx].slice(0,3).map(l=>l.url.split('?')[0].substring(0,80)); if(errors5xx.length>0||errRate>10){R.grade='FAIL';} else if(errors4xx.length>0||slow.length>3||R.avgResponseTime>1000){R.grade='WARN';} else{R.grade='PASS';} R.summary=newApiLogs.length+'개 API | '+ok.length+'OK '+(errors4xx.length+errors5xx.length)+'err '+slow.length+'slow | avg '+R.avgResponseTime+'ms | '+R.grade;}else{ const durs=apiRes.map(e=>Math.round(e.duration)); R.avgResponseTime=durs.length>0?Math.round(durs.reduce((a,b)=>a+b,0)/durs.length):0; R.slowCalls=apiRes.filter(e=>e.duration>2000).length; if(R.slowCalls>3||R.avgResponseTime>1000){R.grade='WARN';} else{R.grade='PASS';} R.summary=apiRes.length+'개 API(perf) | avg '+R.avgResponseTime+'ms | '+R.grade;}R.ok=true;return JSON.stringify(R);})()", "timeout": 10000, "phase": "API_AUDIT" }, { "id": 20, + "name": "[회계관리 > 매입관리] 마커 기록", + "action": "evaluate", + "script": "(async()=>{const apiLogs=window.__E2E__?window.__E2E__.getApiLogs():{logs:[],summary:{}};window.__AH_MARK__={ ts:Date.now(), apiLogCount:apiLogs.logs.length, perfCount:performance.getEntriesByType('resource').length};return JSON.stringify({phase:'MARK_START',ok:true,info:'apiLogs:'+apiLogs.logs.length+' perf:'+window.__AH_MARK__.perfCount});})()", + "timeout": 3000, + "phase": "MARK" + }, + { + "id": 21, "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 호출 대기", @@ -182,26 +182,26 @@ "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);})()", + "script": "(async()=>{const R={phase:'API_AUDIT'};const mark=window.__AH_MARK__||{ts:0,apiLogCount:0,perfCount:0};const apiData=window.__E2E__?window.__E2E__.getApiLogs():{logs:[],errors:[],summary:{}};const newApiLogs=apiData.logs.slice(mark.apiLogCount);R.monitorCalls=newApiLogs.length;const allRes=performance.getEntriesByType('resource');const newRes=allRes.slice(mark.perfCount);const apiRes=newRes.filter(e=>e.name.includes('/api/')||e.name.includes('/v1/')||e.name.includes('/graphql'));R.perfApiCalls=apiRes.length;const jsRes=newRes.filter(e=>e.initiatorType==='script'||e.name.endsWith('.js'));const cssRes=newRes.filter(e=>e.initiatorType==='link'||e.name.endsWith('.css'));const imgRes=newRes.filter(e=>e.initiatorType==='img'||e.name.match(new RegExp('\\\\.(png|jpg|svg|gif|webp)')));R.resourceBreakdown={total:newRes.length,api:apiRes.length,js:jsRes.length,css:cssRes.length,img:imgRes.length};R.totalCalls=Math.max(newApiLogs.length,apiRes.length);R.totalResources=newRes.length;if(R.totalCalls===0){ if(newRes.length>0){ const avgDur=Math.round(newRes.reduce((s,e)=>s+e.duration,0)/newRes.length); R.grade='PASS';R.ok=true; R.summary=newRes.length+'개 리소스 로드(API 0) | avg '+avgDur+'ms | PASS'; R.avgResponseTime=avgDur; }else{ R.grade='PASS';R.ok=true; R.summary='리소스/API 호출 없음 (SPA 캐시) | PASS'; } return JSON.stringify(R);}if(newApiLogs.length>0){ const errors4xx=newApiLogs.filter(l=>l.status>=400&&l.status<500); const errors5xx=newApiLogs.filter(l=>l.status>=500); const netErr=newApiLogs.filter(l=>!l.ok&&l.status===0); const slow=newApiLogs.filter(l=>l.duration>2000); const ok=newApiLogs.filter(l=>l.ok); R.errors4xx=errors4xx.length;R.errors5xx=errors5xx.length; R.slowCalls=slow.length;R.successCount=ok.length; const durs=newApiLogs.map(l=>l.duration).filter(d=>d>0); R.avgResponseTime=durs.length>0?Math.round(durs.reduce((a,b)=>a+b,0)/durs.length):0; R.maxResponseTime=durs.length>0?Math.max(...durs):0; const errRate=((errors4xx.length+errors5xx.length+netErr.length)/newApiLogs.length*100); R.errorRate=Math.round(errRate*10)/10; R.failedUrls=[...errors5xx,...errors4xx].slice(0,3).map(l=>l.url.split('?')[0].substring(0,80)); if(errors5xx.length>0||errRate>10){R.grade='FAIL';} else if(errors4xx.length>0||slow.length>3||R.avgResponseTime>1000){R.grade='WARN';} else{R.grade='PASS';} R.summary=newApiLogs.length+'개 API | '+ok.length+'OK '+(errors4xx.length+errors5xx.length)+'err '+slow.length+'slow | avg '+R.avgResponseTime+'ms | '+R.grade;}else{ const durs=apiRes.map(e=>Math.round(e.duration)); R.avgResponseTime=durs.length>0?Math.round(durs.reduce((a,b)=>a+b,0)/durs.length):0; R.slowCalls=apiRes.filter(e=>e.duration>2000).length; if(R.slowCalls>3||R.avgResponseTime>1000){R.grade='WARN';} else{R.grade='PASS';} R.summary=apiRes.length+'개 API(perf) | avg '+R.avgResponseTime+'ms | '+R.grade;}R.ok=true;return JSON.stringify(R);})()", "timeout": 10000, "phase": "API_AUDIT" }, { "id": 24, + "name": "[회계관리 > 악성채권관리] 마커 기록", + "action": "evaluate", + "script": "(async()=>{const apiLogs=window.__E2E__?window.__E2E__.getApiLogs():{logs:[],summary:{}};window.__AH_MARK__={ ts:Date.now(), apiLogCount:apiLogs.logs.length, perfCount:performance.getEntriesByType('resource').length};return JSON.stringify({phase:'MARK_START',ok:true,info:'apiLogs:'+apiLogs.logs.length+' perf:'+window.__AH_MARK__.perfCount});})()", + "timeout": 3000, + "phase": "MARK" + }, + { + "id": 25, "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 호출 대기", @@ -212,26 +212,26 @@ "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);})()", + "script": "(async()=>{const R={phase:'API_AUDIT'};const mark=window.__AH_MARK__||{ts:0,apiLogCount:0,perfCount:0};const apiData=window.__E2E__?window.__E2E__.getApiLogs():{logs:[],errors:[],summary:{}};const newApiLogs=apiData.logs.slice(mark.apiLogCount);R.monitorCalls=newApiLogs.length;const allRes=performance.getEntriesByType('resource');const newRes=allRes.slice(mark.perfCount);const apiRes=newRes.filter(e=>e.name.includes('/api/')||e.name.includes('/v1/')||e.name.includes('/graphql'));R.perfApiCalls=apiRes.length;const jsRes=newRes.filter(e=>e.initiatorType==='script'||e.name.endsWith('.js'));const cssRes=newRes.filter(e=>e.initiatorType==='link'||e.name.endsWith('.css'));const imgRes=newRes.filter(e=>e.initiatorType==='img'||e.name.match(new RegExp('\\\\.(png|jpg|svg|gif|webp)')));R.resourceBreakdown={total:newRes.length,api:apiRes.length,js:jsRes.length,css:cssRes.length,img:imgRes.length};R.totalCalls=Math.max(newApiLogs.length,apiRes.length);R.totalResources=newRes.length;if(R.totalCalls===0){ if(newRes.length>0){ const avgDur=Math.round(newRes.reduce((s,e)=>s+e.duration,0)/newRes.length); R.grade='PASS';R.ok=true; R.summary=newRes.length+'개 리소스 로드(API 0) | avg '+avgDur+'ms | PASS'; R.avgResponseTime=avgDur; }else{ R.grade='PASS';R.ok=true; R.summary='리소스/API 호출 없음 (SPA 캐시) | PASS'; } return JSON.stringify(R);}if(newApiLogs.length>0){ const errors4xx=newApiLogs.filter(l=>l.status>=400&&l.status<500); const errors5xx=newApiLogs.filter(l=>l.status>=500); const netErr=newApiLogs.filter(l=>!l.ok&&l.status===0); const slow=newApiLogs.filter(l=>l.duration>2000); const ok=newApiLogs.filter(l=>l.ok); R.errors4xx=errors4xx.length;R.errors5xx=errors5xx.length; R.slowCalls=slow.length;R.successCount=ok.length; const durs=newApiLogs.map(l=>l.duration).filter(d=>d>0); R.avgResponseTime=durs.length>0?Math.round(durs.reduce((a,b)=>a+b,0)/durs.length):0; R.maxResponseTime=durs.length>0?Math.max(...durs):0; const errRate=((errors4xx.length+errors5xx.length+netErr.length)/newApiLogs.length*100); R.errorRate=Math.round(errRate*10)/10; R.failedUrls=[...errors5xx,...errors4xx].slice(0,3).map(l=>l.url.split('?')[0].substring(0,80)); if(errors5xx.length>0||errRate>10){R.grade='FAIL';} else if(errors4xx.length>0||slow.length>3||R.avgResponseTime>1000){R.grade='WARN';} else{R.grade='PASS';} R.summary=newApiLogs.length+'개 API | '+ok.length+'OK '+(errors4xx.length+errors5xx.length)+'err '+slow.length+'slow | avg '+R.avgResponseTime+'ms | '+R.grade;}else{ const durs=apiRes.map(e=>Math.round(e.duration)); R.avgResponseTime=durs.length>0?Math.round(durs.reduce((a,b)=>a+b,0)/durs.length):0; R.slowCalls=apiRes.filter(e=>e.duration>2000).length; if(R.slowCalls>3||R.avgResponseTime>1000){R.grade='WARN';} else{R.grade='PASS';} R.summary=apiRes.length+'개 API(perf) | avg '+R.avgResponseTime+'ms | '+R.grade;}R.ok=true;return JSON.stringify(R);})()", "timeout": 10000, "phase": "API_AUDIT" }, { "id": 28, + "name": "[회계관리 > 예상지출관리] 마커 기록", + "action": "evaluate", + "script": "(async()=>{const apiLogs=window.__E2E__?window.__E2E__.getApiLogs():{logs:[],summary:{}};window.__AH_MARK__={ ts:Date.now(), apiLogCount:apiLogs.logs.length, perfCount:performance.getEntriesByType('resource').length};return JSON.stringify({phase:'MARK_START',ok:true,info:'apiLogs:'+apiLogs.logs.length+' perf:'+window.__AH_MARK__.perfCount});})()", + "timeout": 3000, + "phase": "MARK" + }, + { + "id": 29, "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 호출 대기", @@ -242,26 +242,26 @@ "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);})()", + "script": "(async()=>{const R={phase:'API_AUDIT'};const mark=window.__AH_MARK__||{ts:0,apiLogCount:0,perfCount:0};const apiData=window.__E2E__?window.__E2E__.getApiLogs():{logs:[],errors:[],summary:{}};const newApiLogs=apiData.logs.slice(mark.apiLogCount);R.monitorCalls=newApiLogs.length;const allRes=performance.getEntriesByType('resource');const newRes=allRes.slice(mark.perfCount);const apiRes=newRes.filter(e=>e.name.includes('/api/')||e.name.includes('/v1/')||e.name.includes('/graphql'));R.perfApiCalls=apiRes.length;const jsRes=newRes.filter(e=>e.initiatorType==='script'||e.name.endsWith('.js'));const cssRes=newRes.filter(e=>e.initiatorType==='link'||e.name.endsWith('.css'));const imgRes=newRes.filter(e=>e.initiatorType==='img'||e.name.match(new RegExp('\\\\.(png|jpg|svg|gif|webp)')));R.resourceBreakdown={total:newRes.length,api:apiRes.length,js:jsRes.length,css:cssRes.length,img:imgRes.length};R.totalCalls=Math.max(newApiLogs.length,apiRes.length);R.totalResources=newRes.length;if(R.totalCalls===0){ if(newRes.length>0){ const avgDur=Math.round(newRes.reduce((s,e)=>s+e.duration,0)/newRes.length); R.grade='PASS';R.ok=true; R.summary=newRes.length+'개 리소스 로드(API 0) | avg '+avgDur+'ms | PASS'; R.avgResponseTime=avgDur; }else{ R.grade='PASS';R.ok=true; R.summary='리소스/API 호출 없음 (SPA 캐시) | PASS'; } return JSON.stringify(R);}if(newApiLogs.length>0){ const errors4xx=newApiLogs.filter(l=>l.status>=400&&l.status<500); const errors5xx=newApiLogs.filter(l=>l.status>=500); const netErr=newApiLogs.filter(l=>!l.ok&&l.status===0); const slow=newApiLogs.filter(l=>l.duration>2000); const ok=newApiLogs.filter(l=>l.ok); R.errors4xx=errors4xx.length;R.errors5xx=errors5xx.length; R.slowCalls=slow.length;R.successCount=ok.length; const durs=newApiLogs.map(l=>l.duration).filter(d=>d>0); R.avgResponseTime=durs.length>0?Math.round(durs.reduce((a,b)=>a+b,0)/durs.length):0; R.maxResponseTime=durs.length>0?Math.max(...durs):0; const errRate=((errors4xx.length+errors5xx.length+netErr.length)/newApiLogs.length*100); R.errorRate=Math.round(errRate*10)/10; R.failedUrls=[...errors5xx,...errors4xx].slice(0,3).map(l=>l.url.split('?')[0].substring(0,80)); if(errors5xx.length>0||errRate>10){R.grade='FAIL';} else if(errors4xx.length>0||slow.length>3||R.avgResponseTime>1000){R.grade='WARN';} else{R.grade='PASS';} R.summary=newApiLogs.length+'개 API | '+ok.length+'OK '+(errors4xx.length+errors5xx.length)+'err '+slow.length+'slow | avg '+R.avgResponseTime+'ms | '+R.grade;}else{ const durs=apiRes.map(e=>Math.round(e.duration)); R.avgResponseTime=durs.length>0?Math.round(durs.reduce((a,b)=>a+b,0)/durs.length):0; R.slowCalls=apiRes.filter(e=>e.duration>2000).length; if(R.slowCalls>3||R.avgResponseTime>1000){R.grade='WARN';} else{R.grade='PASS';} R.summary=apiRes.length+'개 API(perf) | avg '+R.avgResponseTime+'ms | '+R.grade;}R.ok=true;return JSON.stringify(R);})()", "timeout": 10000, "phase": "API_AUDIT" }, { "id": 32, + "name": "[회계관리 > 카드내역관리] 마커 기록", + "action": "evaluate", + "script": "(async()=>{const apiLogs=window.__E2E__?window.__E2E__.getApiLogs():{logs:[],summary:{}};window.__AH_MARK__={ ts:Date.now(), apiLogCount:apiLogs.logs.length, perfCount:performance.getEntriesByType('resource').length};return JSON.stringify({phase:'MARK_START',ok:true,info:'apiLogs:'+apiLogs.logs.length+' perf:'+window.__AH_MARK__.perfCount});})()", + "timeout": 3000, + "phase": "MARK" + }, + { + "id": 33, "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 호출 대기", @@ -272,26 +272,26 @@ "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);})()", + "script": "(async()=>{const R={phase:'API_AUDIT'};const mark=window.__AH_MARK__||{ts:0,apiLogCount:0,perfCount:0};const apiData=window.__E2E__?window.__E2E__.getApiLogs():{logs:[],errors:[],summary:{}};const newApiLogs=apiData.logs.slice(mark.apiLogCount);R.monitorCalls=newApiLogs.length;const allRes=performance.getEntriesByType('resource');const newRes=allRes.slice(mark.perfCount);const apiRes=newRes.filter(e=>e.name.includes('/api/')||e.name.includes('/v1/')||e.name.includes('/graphql'));R.perfApiCalls=apiRes.length;const jsRes=newRes.filter(e=>e.initiatorType==='script'||e.name.endsWith('.js'));const cssRes=newRes.filter(e=>e.initiatorType==='link'||e.name.endsWith('.css'));const imgRes=newRes.filter(e=>e.initiatorType==='img'||e.name.match(new RegExp('\\\\.(png|jpg|svg|gif|webp)')));R.resourceBreakdown={total:newRes.length,api:apiRes.length,js:jsRes.length,css:cssRes.length,img:imgRes.length};R.totalCalls=Math.max(newApiLogs.length,apiRes.length);R.totalResources=newRes.length;if(R.totalCalls===0){ if(newRes.length>0){ const avgDur=Math.round(newRes.reduce((s,e)=>s+e.duration,0)/newRes.length); R.grade='PASS';R.ok=true; R.summary=newRes.length+'개 리소스 로드(API 0) | avg '+avgDur+'ms | PASS'; R.avgResponseTime=avgDur; }else{ R.grade='PASS';R.ok=true; R.summary='리소스/API 호출 없음 (SPA 캐시) | PASS'; } return JSON.stringify(R);}if(newApiLogs.length>0){ const errors4xx=newApiLogs.filter(l=>l.status>=400&&l.status<500); const errors5xx=newApiLogs.filter(l=>l.status>=500); const netErr=newApiLogs.filter(l=>!l.ok&&l.status===0); const slow=newApiLogs.filter(l=>l.duration>2000); const ok=newApiLogs.filter(l=>l.ok); R.errors4xx=errors4xx.length;R.errors5xx=errors5xx.length; R.slowCalls=slow.length;R.successCount=ok.length; const durs=newApiLogs.map(l=>l.duration).filter(d=>d>0); R.avgResponseTime=durs.length>0?Math.round(durs.reduce((a,b)=>a+b,0)/durs.length):0; R.maxResponseTime=durs.length>0?Math.max(...durs):0; const errRate=((errors4xx.length+errors5xx.length+netErr.length)/newApiLogs.length*100); R.errorRate=Math.round(errRate*10)/10; R.failedUrls=[...errors5xx,...errors4xx].slice(0,3).map(l=>l.url.split('?')[0].substring(0,80)); if(errors5xx.length>0||errRate>10){R.grade='FAIL';} else if(errors4xx.length>0||slow.length>3||R.avgResponseTime>1000){R.grade='WARN';} else{R.grade='PASS';} R.summary=newApiLogs.length+'개 API | '+ok.length+'OK '+(errors4xx.length+errors5xx.length)+'err '+slow.length+'slow | avg '+R.avgResponseTime+'ms | '+R.grade;}else{ const durs=apiRes.map(e=>Math.round(e.duration)); R.avgResponseTime=durs.length>0?Math.round(durs.reduce((a,b)=>a+b,0)/durs.length):0; R.slowCalls=apiRes.filter(e=>e.duration>2000).length; if(R.slowCalls>3||R.avgResponseTime>1000){R.grade='WARN';} else{R.grade='PASS';} R.summary=apiRes.length+'개 API(perf) | avg '+R.avgResponseTime+'ms | '+R.grade;}R.ok=true;return JSON.stringify(R);})()", "timeout": 10000, "phase": "API_AUDIT" }, { "id": 36, + "name": "[회계관리 > 결제관리] 마커 기록", + "action": "evaluate", + "script": "(async()=>{const apiLogs=window.__E2E__?window.__E2E__.getApiLogs():{logs:[],summary:{}};window.__AH_MARK__={ ts:Date.now(), apiLogCount:apiLogs.logs.length, perfCount:performance.getEntriesByType('resource').length};return JSON.stringify({phase:'MARK_START',ok:true,info:'apiLogs:'+apiLogs.logs.length+' perf:'+window.__AH_MARK__.perfCount});})()", + "timeout": 3000, + "phase": "MARK" + }, + { + "id": 37, "name": "[회계관리 > 결제관리] 메뉴 이동", "action": "menu_navigate", "level1": "회계관리", "level2": "결제관리", "timeout": 10000 }, - { - "id": 37, - "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": 38, "name": "[회계관리 > 결제관리] API 호출 대기", @@ -302,7 +302,7 @@ "id": 39, "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);})()", + "script": "(async()=>{const R={phase:'API_AUDIT'};const mark=window.__AH_MARK__||{ts:0,apiLogCount:0,perfCount:0};const apiData=window.__E2E__?window.__E2E__.getApiLogs():{logs:[],errors:[],summary:{}};const newApiLogs=apiData.logs.slice(mark.apiLogCount);R.monitorCalls=newApiLogs.length;const allRes=performance.getEntriesByType('resource');const newRes=allRes.slice(mark.perfCount);const apiRes=newRes.filter(e=>e.name.includes('/api/')||e.name.includes('/v1/')||e.name.includes('/graphql'));R.perfApiCalls=apiRes.length;const jsRes=newRes.filter(e=>e.initiatorType==='script'||e.name.endsWith('.js'));const cssRes=newRes.filter(e=>e.initiatorType==='link'||e.name.endsWith('.css'));const imgRes=newRes.filter(e=>e.initiatorType==='img'||e.name.match(new RegExp('\\\\.(png|jpg|svg|gif|webp)')));R.resourceBreakdown={total:newRes.length,api:apiRes.length,js:jsRes.length,css:cssRes.length,img:imgRes.length};R.totalCalls=Math.max(newApiLogs.length,apiRes.length);R.totalResources=newRes.length;if(R.totalCalls===0){ if(newRes.length>0){ const avgDur=Math.round(newRes.reduce((s,e)=>s+e.duration,0)/newRes.length); R.grade='PASS';R.ok=true; R.summary=newRes.length+'개 리소스 로드(API 0) | avg '+avgDur+'ms | PASS'; R.avgResponseTime=avgDur; }else{ R.grade='PASS';R.ok=true; R.summary='리소스/API 호출 없음 (SPA 캐시) | PASS'; } return JSON.stringify(R);}if(newApiLogs.length>0){ const errors4xx=newApiLogs.filter(l=>l.status>=400&&l.status<500); const errors5xx=newApiLogs.filter(l=>l.status>=500); const netErr=newApiLogs.filter(l=>!l.ok&&l.status===0); const slow=newApiLogs.filter(l=>l.duration>2000); const ok=newApiLogs.filter(l=>l.ok); R.errors4xx=errors4xx.length;R.errors5xx=errors5xx.length; R.slowCalls=slow.length;R.successCount=ok.length; const durs=newApiLogs.map(l=>l.duration).filter(d=>d>0); R.avgResponseTime=durs.length>0?Math.round(durs.reduce((a,b)=>a+b,0)/durs.length):0; R.maxResponseTime=durs.length>0?Math.max(...durs):0; const errRate=((errors4xx.length+errors5xx.length+netErr.length)/newApiLogs.length*100); R.errorRate=Math.round(errRate*10)/10; R.failedUrls=[...errors5xx,...errors4xx].slice(0,3).map(l=>l.url.split('?')[0].substring(0,80)); if(errors5xx.length>0||errRate>10){R.grade='FAIL';} else if(errors4xx.length>0||slow.length>3||R.avgResponseTime>1000){R.grade='WARN';} else{R.grade='PASS';} R.summary=newApiLogs.length+'개 API | '+ok.length+'OK '+(errors4xx.length+errors5xx.length)+'err '+slow.length+'slow | avg '+R.avgResponseTime+'ms | '+R.grade;}else{ const durs=apiRes.map(e=>Math.round(e.duration)); R.avgResponseTime=durs.length>0?Math.round(durs.reduce((a,b)=>a+b,0)/durs.length):0; R.slowCalls=apiRes.filter(e=>e.duration>2000).length; if(R.slowCalls>3||R.avgResponseTime>1000){R.grade='WARN';} else{R.grade='PASS';} R.summary=apiRes.length+'개 API(perf) | avg '+R.avgResponseTime+'ms | '+R.grade;}R.ok=true;return JSON.stringify(R);})()", "timeout": 10000, "phase": "API_AUDIT" } diff --git a/api-health-prod-misc.json b/api-health-prod-misc.json index 027d1c3..f6dc9c2 100644 --- a/api-health-prod-misc.json +++ b/api-health-prod-misc.json @@ -1,7 +1,7 @@ { "id": "api-health-prod-misc", "name": "API 건강성 감사: 생산/기타", - "version": "1.0.0", + "version": "2.0.0", "auth": { "role": "admin" }, @@ -16,11 +16,11 @@ "steps": [ { "id": 1, - "name": "[생산관리 > 작업지시 관리] API 인터셉터 설치", + "name": "[생산관리 > 작업지시 관리] 마커 기록", "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);})()", + "script": "(async()=>{const apiLogs=window.__E2E__?window.__E2E__.getApiLogs():{logs:[],summary:{}};window.__AH_MARK__={ ts:Date.now(), apiLogCount:apiLogs.logs.length, perfCount:performance.getEntriesByType('resource').length};return JSON.stringify({phase:'MARK_START',ok:true,info:'apiLogs:'+apiLogs.logs.length+' perf:'+window.__AH_MARK__.perfCount});})()", "timeout": 5000, - "phase": "INSTALL" + "phase": "MARK" }, { "id": 2, @@ -32,26 +32,26 @@ "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);})()", + "script": "(async()=>{const R={phase:'API_AUDIT'};const mark=window.__AH_MARK__||{ts:0,apiLogCount:0,perfCount:0};const apiData=window.__E2E__?window.__E2E__.getApiLogs():{logs:[],errors:[],summary:{}};const newApiLogs=apiData.logs.slice(mark.apiLogCount);R.monitorCalls=newApiLogs.length;const allRes=performance.getEntriesByType('resource');const newRes=allRes.slice(mark.perfCount);const apiRes=newRes.filter(e=>e.name.includes('/api/')||e.name.includes('/v1/')||e.name.includes('/graphql'));R.perfApiCalls=apiRes.length;const jsRes=newRes.filter(e=>e.initiatorType==='script'||e.name.endsWith('.js'));const cssRes=newRes.filter(e=>e.initiatorType==='link'||e.name.endsWith('.css'));const imgRes=newRes.filter(e=>e.initiatorType==='img'||e.name.match(new RegExp('\\\\.(png|jpg|svg|gif|webp)')));R.resourceBreakdown={total:newRes.length,api:apiRes.length,js:jsRes.length,css:cssRes.length,img:imgRes.length};R.totalCalls=Math.max(newApiLogs.length,apiRes.length);R.totalResources=newRes.length;if(R.totalCalls===0){ if(newRes.length>0){ const avgDur=Math.round(newRes.reduce((s,e)=>s+e.duration,0)/newRes.length); R.grade='PASS';R.ok=true; R.summary=newRes.length+'개 리소스 로드(API 0) | avg '+avgDur+'ms | PASS'; R.avgResponseTime=avgDur; }else{ R.grade='PASS';R.ok=true; R.summary='리소스/API 호출 없음 (SPA 캐시) | PASS'; } return JSON.stringify(R);}if(newApiLogs.length>0){ const errors4xx=newApiLogs.filter(l=>l.status>=400&&l.status<500); const errors5xx=newApiLogs.filter(l=>l.status>=500); const netErr=newApiLogs.filter(l=>!l.ok&&l.status===0); const slow=newApiLogs.filter(l=>l.duration>2000); const ok=newApiLogs.filter(l=>l.ok); R.errors4xx=errors4xx.length;R.errors5xx=errors5xx.length; R.slowCalls=slow.length;R.successCount=ok.length; const durs=newApiLogs.map(l=>l.duration).filter(d=>d>0); R.avgResponseTime=durs.length>0?Math.round(durs.reduce((a,b)=>a+b,0)/durs.length):0; R.maxResponseTime=durs.length>0?Math.max(...durs):0; const errRate=((errors4xx.length+errors5xx.length+netErr.length)/newApiLogs.length*100); R.errorRate=Math.round(errRate*10)/10; R.failedUrls=[...errors5xx,...errors4xx].slice(0,3).map(l=>l.url.split('?')[0].substring(0,80)); if(errors5xx.length>0||errRate>10){R.grade='FAIL';} else if(errors4xx.length>0||slow.length>3||R.avgResponseTime>1000){R.grade='WARN';} else{R.grade='PASS';} R.summary=newApiLogs.length+'개 API | '+ok.length+'OK '+(errors4xx.length+errors5xx.length)+'err '+slow.length+'slow | avg '+R.avgResponseTime+'ms | '+R.grade;}else{ const durs=apiRes.map(e=>Math.round(e.duration)); R.avgResponseTime=durs.length>0?Math.round(durs.reduce((a,b)=>a+b,0)/durs.length):0; R.slowCalls=apiRes.filter(e=>e.duration>2000).length; if(R.slowCalls>3||R.avgResponseTime>1000){R.grade='WARN';} else{R.grade='PASS';} R.summary=apiRes.length+'개 API(perf) | avg '+R.avgResponseTime+'ms | '+R.grade;}R.ok=true;return JSON.stringify(R);})()", "timeout": 10000, "phase": "API_AUDIT" }, { "id": 4, + "name": "[생산관리 > 작업실적] 마커 기록", + "action": "evaluate", + "script": "(async()=>{const apiLogs=window.__E2E__?window.__E2E__.getApiLogs():{logs:[],summary:{}};window.__AH_MARK__={ ts:Date.now(), apiLogCount:apiLogs.logs.length, perfCount:performance.getEntriesByType('resource').length};return JSON.stringify({phase:'MARK_START',ok:true,info:'apiLogs:'+apiLogs.logs.length+' perf:'+window.__AH_MARK__.perfCount});})()", + "timeout": 3000, + "phase": "MARK" + }, + { + "id": 5, "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 호출 대기", @@ -62,26 +62,26 @@ "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);})()", + "script": "(async()=>{const R={phase:'API_AUDIT'};const mark=window.__AH_MARK__||{ts:0,apiLogCount:0,perfCount:0};const apiData=window.__E2E__?window.__E2E__.getApiLogs():{logs:[],errors:[],summary:{}};const newApiLogs=apiData.logs.slice(mark.apiLogCount);R.monitorCalls=newApiLogs.length;const allRes=performance.getEntriesByType('resource');const newRes=allRes.slice(mark.perfCount);const apiRes=newRes.filter(e=>e.name.includes('/api/')||e.name.includes('/v1/')||e.name.includes('/graphql'));R.perfApiCalls=apiRes.length;const jsRes=newRes.filter(e=>e.initiatorType==='script'||e.name.endsWith('.js'));const cssRes=newRes.filter(e=>e.initiatorType==='link'||e.name.endsWith('.css'));const imgRes=newRes.filter(e=>e.initiatorType==='img'||e.name.match(new RegExp('\\\\.(png|jpg|svg|gif|webp)')));R.resourceBreakdown={total:newRes.length,api:apiRes.length,js:jsRes.length,css:cssRes.length,img:imgRes.length};R.totalCalls=Math.max(newApiLogs.length,apiRes.length);R.totalResources=newRes.length;if(R.totalCalls===0){ if(newRes.length>0){ const avgDur=Math.round(newRes.reduce((s,e)=>s+e.duration,0)/newRes.length); R.grade='PASS';R.ok=true; R.summary=newRes.length+'개 리소스 로드(API 0) | avg '+avgDur+'ms | PASS'; R.avgResponseTime=avgDur; }else{ R.grade='PASS';R.ok=true; R.summary='리소스/API 호출 없음 (SPA 캐시) | PASS'; } return JSON.stringify(R);}if(newApiLogs.length>0){ const errors4xx=newApiLogs.filter(l=>l.status>=400&&l.status<500); const errors5xx=newApiLogs.filter(l=>l.status>=500); const netErr=newApiLogs.filter(l=>!l.ok&&l.status===0); const slow=newApiLogs.filter(l=>l.duration>2000); const ok=newApiLogs.filter(l=>l.ok); R.errors4xx=errors4xx.length;R.errors5xx=errors5xx.length; R.slowCalls=slow.length;R.successCount=ok.length; const durs=newApiLogs.map(l=>l.duration).filter(d=>d>0); R.avgResponseTime=durs.length>0?Math.round(durs.reduce((a,b)=>a+b,0)/durs.length):0; R.maxResponseTime=durs.length>0?Math.max(...durs):0; const errRate=((errors4xx.length+errors5xx.length+netErr.length)/newApiLogs.length*100); R.errorRate=Math.round(errRate*10)/10; R.failedUrls=[...errors5xx,...errors4xx].slice(0,3).map(l=>l.url.split('?')[0].substring(0,80)); if(errors5xx.length>0||errRate>10){R.grade='FAIL';} else if(errors4xx.length>0||slow.length>3||R.avgResponseTime>1000){R.grade='WARN';} else{R.grade='PASS';} R.summary=newApiLogs.length+'개 API | '+ok.length+'OK '+(errors4xx.length+errors5xx.length)+'err '+slow.length+'slow | avg '+R.avgResponseTime+'ms | '+R.grade;}else{ const durs=apiRes.map(e=>Math.round(e.duration)); R.avgResponseTime=durs.length>0?Math.round(durs.reduce((a,b)=>a+b,0)/durs.length):0; R.slowCalls=apiRes.filter(e=>e.duration>2000).length; if(R.slowCalls>3||R.avgResponseTime>1000){R.grade='WARN';} else{R.grade='PASS';} R.summary=apiRes.length+'개 API(perf) | avg '+R.avgResponseTime+'ms | '+R.grade;}R.ok=true;return JSON.stringify(R);})()", "timeout": 10000, "phase": "API_AUDIT" }, { "id": 8, + "name": "[생산관리 > 품목관리] 마커 기록", + "action": "evaluate", + "script": "(async()=>{const apiLogs=window.__E2E__?window.__E2E__.getApiLogs():{logs:[],summary:{}};window.__AH_MARK__={ ts:Date.now(), apiLogCount:apiLogs.logs.length, perfCount:performance.getEntriesByType('resource').length};return JSON.stringify({phase:'MARK_START',ok:true,info:'apiLogs:'+apiLogs.logs.length+' perf:'+window.__AH_MARK__.perfCount});})()", + "timeout": 3000, + "phase": "MARK" + }, + { + "id": 9, "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 호출 대기", @@ -92,26 +92,26 @@ "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);})()", + "script": "(async()=>{const R={phase:'API_AUDIT'};const mark=window.__AH_MARK__||{ts:0,apiLogCount:0,perfCount:0};const apiData=window.__E2E__?window.__E2E__.getApiLogs():{logs:[],errors:[],summary:{}};const newApiLogs=apiData.logs.slice(mark.apiLogCount);R.monitorCalls=newApiLogs.length;const allRes=performance.getEntriesByType('resource');const newRes=allRes.slice(mark.perfCount);const apiRes=newRes.filter(e=>e.name.includes('/api/')||e.name.includes('/v1/')||e.name.includes('/graphql'));R.perfApiCalls=apiRes.length;const jsRes=newRes.filter(e=>e.initiatorType==='script'||e.name.endsWith('.js'));const cssRes=newRes.filter(e=>e.initiatorType==='link'||e.name.endsWith('.css'));const imgRes=newRes.filter(e=>e.initiatorType==='img'||e.name.match(new RegExp('\\\\.(png|jpg|svg|gif|webp)')));R.resourceBreakdown={total:newRes.length,api:apiRes.length,js:jsRes.length,css:cssRes.length,img:imgRes.length};R.totalCalls=Math.max(newApiLogs.length,apiRes.length);R.totalResources=newRes.length;if(R.totalCalls===0){ if(newRes.length>0){ const avgDur=Math.round(newRes.reduce((s,e)=>s+e.duration,0)/newRes.length); R.grade='PASS';R.ok=true; R.summary=newRes.length+'개 리소스 로드(API 0) | avg '+avgDur+'ms | PASS'; R.avgResponseTime=avgDur; }else{ R.grade='PASS';R.ok=true; R.summary='리소스/API 호출 없음 (SPA 캐시) | PASS'; } return JSON.stringify(R);}if(newApiLogs.length>0){ const errors4xx=newApiLogs.filter(l=>l.status>=400&&l.status<500); const errors5xx=newApiLogs.filter(l=>l.status>=500); const netErr=newApiLogs.filter(l=>!l.ok&&l.status===0); const slow=newApiLogs.filter(l=>l.duration>2000); const ok=newApiLogs.filter(l=>l.ok); R.errors4xx=errors4xx.length;R.errors5xx=errors5xx.length; R.slowCalls=slow.length;R.successCount=ok.length; const durs=newApiLogs.map(l=>l.duration).filter(d=>d>0); R.avgResponseTime=durs.length>0?Math.round(durs.reduce((a,b)=>a+b,0)/durs.length):0; R.maxResponseTime=durs.length>0?Math.max(...durs):0; const errRate=((errors4xx.length+errors5xx.length+netErr.length)/newApiLogs.length*100); R.errorRate=Math.round(errRate*10)/10; R.failedUrls=[...errors5xx,...errors4xx].slice(0,3).map(l=>l.url.split('?')[0].substring(0,80)); if(errors5xx.length>0||errRate>10){R.grade='FAIL';} else if(errors4xx.length>0||slow.length>3||R.avgResponseTime>1000){R.grade='WARN';} else{R.grade='PASS';} R.summary=newApiLogs.length+'개 API | '+ok.length+'OK '+(errors4xx.length+errors5xx.length)+'err '+slow.length+'slow | avg '+R.avgResponseTime+'ms | '+R.grade;}else{ const durs=apiRes.map(e=>Math.round(e.duration)); R.avgResponseTime=durs.length>0?Math.round(durs.reduce((a,b)=>a+b,0)/durs.length):0; R.slowCalls=apiRes.filter(e=>e.duration>2000).length; if(R.slowCalls>3||R.avgResponseTime>1000){R.grade='WARN';} else{R.grade='PASS';} R.summary=apiRes.length+'개 API(perf) | avg '+R.avgResponseTime+'ms | '+R.grade;}R.ok=true;return JSON.stringify(R);})()", "timeout": 10000, "phase": "API_AUDIT" }, { "id": 12, + "name": "[생산관리 > 작업자 화면] 마커 기록", + "action": "evaluate", + "script": "(async()=>{const apiLogs=window.__E2E__?window.__E2E__.getApiLogs():{logs:[],summary:{}};window.__AH_MARK__={ ts:Date.now(), apiLogCount:apiLogs.logs.length, perfCount:performance.getEntriesByType('resource').length};return JSON.stringify({phase:'MARK_START',ok:true,info:'apiLogs:'+apiLogs.logs.length+' perf:'+window.__AH_MARK__.perfCount});})()", + "timeout": 3000, + "phase": "MARK" + }, + { + "id": 13, "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 호출 대기", @@ -122,26 +122,26 @@ "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);})()", + "script": "(async()=>{const R={phase:'API_AUDIT'};const mark=window.__AH_MARK__||{ts:0,apiLogCount:0,perfCount:0};const apiData=window.__E2E__?window.__E2E__.getApiLogs():{logs:[],errors:[],summary:{}};const newApiLogs=apiData.logs.slice(mark.apiLogCount);R.monitorCalls=newApiLogs.length;const allRes=performance.getEntriesByType('resource');const newRes=allRes.slice(mark.perfCount);const apiRes=newRes.filter(e=>e.name.includes('/api/')||e.name.includes('/v1/')||e.name.includes('/graphql'));R.perfApiCalls=apiRes.length;const jsRes=newRes.filter(e=>e.initiatorType==='script'||e.name.endsWith('.js'));const cssRes=newRes.filter(e=>e.initiatorType==='link'||e.name.endsWith('.css'));const imgRes=newRes.filter(e=>e.initiatorType==='img'||e.name.match(new RegExp('\\\\.(png|jpg|svg|gif|webp)')));R.resourceBreakdown={total:newRes.length,api:apiRes.length,js:jsRes.length,css:cssRes.length,img:imgRes.length};R.totalCalls=Math.max(newApiLogs.length,apiRes.length);R.totalResources=newRes.length;if(R.totalCalls===0){ if(newRes.length>0){ const avgDur=Math.round(newRes.reduce((s,e)=>s+e.duration,0)/newRes.length); R.grade='PASS';R.ok=true; R.summary=newRes.length+'개 리소스 로드(API 0) | avg '+avgDur+'ms | PASS'; R.avgResponseTime=avgDur; }else{ R.grade='PASS';R.ok=true; R.summary='리소스/API 호출 없음 (SPA 캐시) | PASS'; } return JSON.stringify(R);}if(newApiLogs.length>0){ const errors4xx=newApiLogs.filter(l=>l.status>=400&&l.status<500); const errors5xx=newApiLogs.filter(l=>l.status>=500); const netErr=newApiLogs.filter(l=>!l.ok&&l.status===0); const slow=newApiLogs.filter(l=>l.duration>2000); const ok=newApiLogs.filter(l=>l.ok); R.errors4xx=errors4xx.length;R.errors5xx=errors5xx.length; R.slowCalls=slow.length;R.successCount=ok.length; const durs=newApiLogs.map(l=>l.duration).filter(d=>d>0); R.avgResponseTime=durs.length>0?Math.round(durs.reduce((a,b)=>a+b,0)/durs.length):0; R.maxResponseTime=durs.length>0?Math.max(...durs):0; const errRate=((errors4xx.length+errors5xx.length+netErr.length)/newApiLogs.length*100); R.errorRate=Math.round(errRate*10)/10; R.failedUrls=[...errors5xx,...errors4xx].slice(0,3).map(l=>l.url.split('?')[0].substring(0,80)); if(errors5xx.length>0||errRate>10){R.grade='FAIL';} else if(errors4xx.length>0||slow.length>3||R.avgResponseTime>1000){R.grade='WARN';} else{R.grade='PASS';} R.summary=newApiLogs.length+'개 API | '+ok.length+'OK '+(errors4xx.length+errors5xx.length)+'err '+slow.length+'slow | avg '+R.avgResponseTime+'ms | '+R.grade;}else{ const durs=apiRes.map(e=>Math.round(e.duration)); R.avgResponseTime=durs.length>0?Math.round(durs.reduce((a,b)=>a+b,0)/durs.length):0; R.slowCalls=apiRes.filter(e=>e.duration>2000).length; if(R.slowCalls>3||R.avgResponseTime>1000){R.grade='WARN';} else{R.grade='PASS';} R.summary=apiRes.length+'개 API(perf) | avg '+R.avgResponseTime+'ms | '+R.grade;}R.ok=true;return JSON.stringify(R);})()", "timeout": 10000, "phase": "API_AUDIT" }, { "id": 16, + "name": "[품질관리 > 제품검사관리] 마커 기록", + "action": "evaluate", + "script": "(async()=>{const apiLogs=window.__E2E__?window.__E2E__.getApiLogs():{logs:[],summary:{}};window.__AH_MARK__={ ts:Date.now(), apiLogCount:apiLogs.logs.length, perfCount:performance.getEntriesByType('resource').length};return JSON.stringify({phase:'MARK_START',ok:true,info:'apiLogs:'+apiLogs.logs.length+' perf:'+window.__AH_MARK__.perfCount});})()", + "timeout": 3000, + "phase": "MARK" + }, + { + "id": 17, "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 호출 대기", @@ -152,26 +152,26 @@ "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);})()", + "script": "(async()=>{const R={phase:'API_AUDIT'};const mark=window.__AH_MARK__||{ts:0,apiLogCount:0,perfCount:0};const apiData=window.__E2E__?window.__E2E__.getApiLogs():{logs:[],errors:[],summary:{}};const newApiLogs=apiData.logs.slice(mark.apiLogCount);R.monitorCalls=newApiLogs.length;const allRes=performance.getEntriesByType('resource');const newRes=allRes.slice(mark.perfCount);const apiRes=newRes.filter(e=>e.name.includes('/api/')||e.name.includes('/v1/')||e.name.includes('/graphql'));R.perfApiCalls=apiRes.length;const jsRes=newRes.filter(e=>e.initiatorType==='script'||e.name.endsWith('.js'));const cssRes=newRes.filter(e=>e.initiatorType==='link'||e.name.endsWith('.css'));const imgRes=newRes.filter(e=>e.initiatorType==='img'||e.name.match(new RegExp('\\\\.(png|jpg|svg|gif|webp)')));R.resourceBreakdown={total:newRes.length,api:apiRes.length,js:jsRes.length,css:cssRes.length,img:imgRes.length};R.totalCalls=Math.max(newApiLogs.length,apiRes.length);R.totalResources=newRes.length;if(R.totalCalls===0){ if(newRes.length>0){ const avgDur=Math.round(newRes.reduce((s,e)=>s+e.duration,0)/newRes.length); R.grade='PASS';R.ok=true; R.summary=newRes.length+'개 리소스 로드(API 0) | avg '+avgDur+'ms | PASS'; R.avgResponseTime=avgDur; }else{ R.grade='PASS';R.ok=true; R.summary='리소스/API 호출 없음 (SPA 캐시) | PASS'; } return JSON.stringify(R);}if(newApiLogs.length>0){ const errors4xx=newApiLogs.filter(l=>l.status>=400&&l.status<500); const errors5xx=newApiLogs.filter(l=>l.status>=500); const netErr=newApiLogs.filter(l=>!l.ok&&l.status===0); const slow=newApiLogs.filter(l=>l.duration>2000); const ok=newApiLogs.filter(l=>l.ok); R.errors4xx=errors4xx.length;R.errors5xx=errors5xx.length; R.slowCalls=slow.length;R.successCount=ok.length; const durs=newApiLogs.map(l=>l.duration).filter(d=>d>0); R.avgResponseTime=durs.length>0?Math.round(durs.reduce((a,b)=>a+b,0)/durs.length):0; R.maxResponseTime=durs.length>0?Math.max(...durs):0; const errRate=((errors4xx.length+errors5xx.length+netErr.length)/newApiLogs.length*100); R.errorRate=Math.round(errRate*10)/10; R.failedUrls=[...errors5xx,...errors4xx].slice(0,3).map(l=>l.url.split('?')[0].substring(0,80)); if(errors5xx.length>0||errRate>10){R.grade='FAIL';} else if(errors4xx.length>0||slow.length>3||R.avgResponseTime>1000){R.grade='WARN';} else{R.grade='PASS';} R.summary=newApiLogs.length+'개 API | '+ok.length+'OK '+(errors4xx.length+errors5xx.length)+'err '+slow.length+'slow | avg '+R.avgResponseTime+'ms | '+R.grade;}else{ const durs=apiRes.map(e=>Math.round(e.duration)); R.avgResponseTime=durs.length>0?Math.round(durs.reduce((a,b)=>a+b,0)/durs.length):0; R.slowCalls=apiRes.filter(e=>e.duration>2000).length; if(R.slowCalls>3||R.avgResponseTime>1000){R.grade='WARN';} else{R.grade='PASS';} R.summary=apiRes.length+'개 API(perf) | avg '+R.avgResponseTime+'ms | '+R.grade;}R.ok=true;return JSON.stringify(R);})()", "timeout": 10000, "phase": "API_AUDIT" }, { "id": 20, + "name": "[자재관리 > 입고관리] 마커 기록", + "action": "evaluate", + "script": "(async()=>{const apiLogs=window.__E2E__?window.__E2E__.getApiLogs():{logs:[],summary:{}};window.__AH_MARK__={ ts:Date.now(), apiLogCount:apiLogs.logs.length, perfCount:performance.getEntriesByType('resource').length};return JSON.stringify({phase:'MARK_START',ok:true,info:'apiLogs:'+apiLogs.logs.length+' perf:'+window.__AH_MARK__.perfCount});})()", + "timeout": 3000, + "phase": "MARK" + }, + { + "id": 21, "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 호출 대기", @@ -182,26 +182,26 @@ "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);})()", + "script": "(async()=>{const R={phase:'API_AUDIT'};const mark=window.__AH_MARK__||{ts:0,apiLogCount:0,perfCount:0};const apiData=window.__E2E__?window.__E2E__.getApiLogs():{logs:[],errors:[],summary:{}};const newApiLogs=apiData.logs.slice(mark.apiLogCount);R.monitorCalls=newApiLogs.length;const allRes=performance.getEntriesByType('resource');const newRes=allRes.slice(mark.perfCount);const apiRes=newRes.filter(e=>e.name.includes('/api/')||e.name.includes('/v1/')||e.name.includes('/graphql'));R.perfApiCalls=apiRes.length;const jsRes=newRes.filter(e=>e.initiatorType==='script'||e.name.endsWith('.js'));const cssRes=newRes.filter(e=>e.initiatorType==='link'||e.name.endsWith('.css'));const imgRes=newRes.filter(e=>e.initiatorType==='img'||e.name.match(new RegExp('\\\\.(png|jpg|svg|gif|webp)')));R.resourceBreakdown={total:newRes.length,api:apiRes.length,js:jsRes.length,css:cssRes.length,img:imgRes.length};R.totalCalls=Math.max(newApiLogs.length,apiRes.length);R.totalResources=newRes.length;if(R.totalCalls===0){ if(newRes.length>0){ const avgDur=Math.round(newRes.reduce((s,e)=>s+e.duration,0)/newRes.length); R.grade='PASS';R.ok=true; R.summary=newRes.length+'개 리소스 로드(API 0) | avg '+avgDur+'ms | PASS'; R.avgResponseTime=avgDur; }else{ R.grade='PASS';R.ok=true; R.summary='리소스/API 호출 없음 (SPA 캐시) | PASS'; } return JSON.stringify(R);}if(newApiLogs.length>0){ const errors4xx=newApiLogs.filter(l=>l.status>=400&&l.status<500); const errors5xx=newApiLogs.filter(l=>l.status>=500); const netErr=newApiLogs.filter(l=>!l.ok&&l.status===0); const slow=newApiLogs.filter(l=>l.duration>2000); const ok=newApiLogs.filter(l=>l.ok); R.errors4xx=errors4xx.length;R.errors5xx=errors5xx.length; R.slowCalls=slow.length;R.successCount=ok.length; const durs=newApiLogs.map(l=>l.duration).filter(d=>d>0); R.avgResponseTime=durs.length>0?Math.round(durs.reduce((a,b)=>a+b,0)/durs.length):0; R.maxResponseTime=durs.length>0?Math.max(...durs):0; const errRate=((errors4xx.length+errors5xx.length+netErr.length)/newApiLogs.length*100); R.errorRate=Math.round(errRate*10)/10; R.failedUrls=[...errors5xx,...errors4xx].slice(0,3).map(l=>l.url.split('?')[0].substring(0,80)); if(errors5xx.length>0||errRate>10){R.grade='FAIL';} else if(errors4xx.length>0||slow.length>3||R.avgResponseTime>1000){R.grade='WARN';} else{R.grade='PASS';} R.summary=newApiLogs.length+'개 API | '+ok.length+'OK '+(errors4xx.length+errors5xx.length)+'err '+slow.length+'slow | avg '+R.avgResponseTime+'ms | '+R.grade;}else{ const durs=apiRes.map(e=>Math.round(e.duration)); R.avgResponseTime=durs.length>0?Math.round(durs.reduce((a,b)=>a+b,0)/durs.length):0; R.slowCalls=apiRes.filter(e=>e.duration>2000).length; if(R.slowCalls>3||R.avgResponseTime>1000){R.grade='WARN';} else{R.grade='PASS';} R.summary=apiRes.length+'개 API(perf) | avg '+R.avgResponseTime+'ms | '+R.grade;}R.ok=true;return JSON.stringify(R);})()", "timeout": 10000, "phase": "API_AUDIT" }, { "id": 24, + "name": "[자재관리 > 재고현황] 마커 기록", + "action": "evaluate", + "script": "(async()=>{const apiLogs=window.__E2E__?window.__E2E__.getApiLogs():{logs:[],summary:{}};window.__AH_MARK__={ ts:Date.now(), apiLogCount:apiLogs.logs.length, perfCount:performance.getEntriesByType('resource').length};return JSON.stringify({phase:'MARK_START',ok:true,info:'apiLogs:'+apiLogs.logs.length+' perf:'+window.__AH_MARK__.perfCount});})()", + "timeout": 3000, + "phase": "MARK" + }, + { + "id": 25, "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 호출 대기", @@ -212,26 +212,26 @@ "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);})()", + "script": "(async()=>{const R={phase:'API_AUDIT'};const mark=window.__AH_MARK__||{ts:0,apiLogCount:0,perfCount:0};const apiData=window.__E2E__?window.__E2E__.getApiLogs():{logs:[],errors:[],summary:{}};const newApiLogs=apiData.logs.slice(mark.apiLogCount);R.monitorCalls=newApiLogs.length;const allRes=performance.getEntriesByType('resource');const newRes=allRes.slice(mark.perfCount);const apiRes=newRes.filter(e=>e.name.includes('/api/')||e.name.includes('/v1/')||e.name.includes('/graphql'));R.perfApiCalls=apiRes.length;const jsRes=newRes.filter(e=>e.initiatorType==='script'||e.name.endsWith('.js'));const cssRes=newRes.filter(e=>e.initiatorType==='link'||e.name.endsWith('.css'));const imgRes=newRes.filter(e=>e.initiatorType==='img'||e.name.match(new RegExp('\\\\.(png|jpg|svg|gif|webp)')));R.resourceBreakdown={total:newRes.length,api:apiRes.length,js:jsRes.length,css:cssRes.length,img:imgRes.length};R.totalCalls=Math.max(newApiLogs.length,apiRes.length);R.totalResources=newRes.length;if(R.totalCalls===0){ if(newRes.length>0){ const avgDur=Math.round(newRes.reduce((s,e)=>s+e.duration,0)/newRes.length); R.grade='PASS';R.ok=true; R.summary=newRes.length+'개 리소스 로드(API 0) | avg '+avgDur+'ms | PASS'; R.avgResponseTime=avgDur; }else{ R.grade='PASS';R.ok=true; R.summary='리소스/API 호출 없음 (SPA 캐시) | PASS'; } return JSON.stringify(R);}if(newApiLogs.length>0){ const errors4xx=newApiLogs.filter(l=>l.status>=400&&l.status<500); const errors5xx=newApiLogs.filter(l=>l.status>=500); const netErr=newApiLogs.filter(l=>!l.ok&&l.status===0); const slow=newApiLogs.filter(l=>l.duration>2000); const ok=newApiLogs.filter(l=>l.ok); R.errors4xx=errors4xx.length;R.errors5xx=errors5xx.length; R.slowCalls=slow.length;R.successCount=ok.length; const durs=newApiLogs.map(l=>l.duration).filter(d=>d>0); R.avgResponseTime=durs.length>0?Math.round(durs.reduce((a,b)=>a+b,0)/durs.length):0; R.maxResponseTime=durs.length>0?Math.max(...durs):0; const errRate=((errors4xx.length+errors5xx.length+netErr.length)/newApiLogs.length*100); R.errorRate=Math.round(errRate*10)/10; R.failedUrls=[...errors5xx,...errors4xx].slice(0,3).map(l=>l.url.split('?')[0].substring(0,80)); if(errors5xx.length>0||errRate>10){R.grade='FAIL';} else if(errors4xx.length>0||slow.length>3||R.avgResponseTime>1000){R.grade='WARN';} else{R.grade='PASS';} R.summary=newApiLogs.length+'개 API | '+ok.length+'OK '+(errors4xx.length+errors5xx.length)+'err '+slow.length+'slow | avg '+R.avgResponseTime+'ms | '+R.grade;}else{ const durs=apiRes.map(e=>Math.round(e.duration)); R.avgResponseTime=durs.length>0?Math.round(durs.reduce((a,b)=>a+b,0)/durs.length):0; R.slowCalls=apiRes.filter(e=>e.duration>2000).length; if(R.slowCalls>3||R.avgResponseTime>1000){R.grade='WARN';} else{R.grade='PASS';} R.summary=apiRes.length+'개 API(perf) | avg '+R.avgResponseTime+'ms | '+R.grade;}R.ok=true;return JSON.stringify(R);})()", "timeout": 10000, "phase": "API_AUDIT" }, { "id": 28, + "name": "[게시판 > 자유게시판] 마커 기록", + "action": "evaluate", + "script": "(async()=>{const apiLogs=window.__E2E__?window.__E2E__.getApiLogs():{logs:[],summary:{}};window.__AH_MARK__={ ts:Date.now(), apiLogCount:apiLogs.logs.length, perfCount:performance.getEntriesByType('resource').length};return JSON.stringify({phase:'MARK_START',ok:true,info:'apiLogs:'+apiLogs.logs.length+' perf:'+window.__AH_MARK__.perfCount});})()", + "timeout": 3000, + "phase": "MARK" + }, + { + "id": 29, "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 호출 대기", @@ -242,26 +242,26 @@ "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);})()", + "script": "(async()=>{const R={phase:'API_AUDIT'};const mark=window.__AH_MARK__||{ts:0,apiLogCount:0,perfCount:0};const apiData=window.__E2E__?window.__E2E__.getApiLogs():{logs:[],errors:[],summary:{}};const newApiLogs=apiData.logs.slice(mark.apiLogCount);R.monitorCalls=newApiLogs.length;const allRes=performance.getEntriesByType('resource');const newRes=allRes.slice(mark.perfCount);const apiRes=newRes.filter(e=>e.name.includes('/api/')||e.name.includes('/v1/')||e.name.includes('/graphql'));R.perfApiCalls=apiRes.length;const jsRes=newRes.filter(e=>e.initiatorType==='script'||e.name.endsWith('.js'));const cssRes=newRes.filter(e=>e.initiatorType==='link'||e.name.endsWith('.css'));const imgRes=newRes.filter(e=>e.initiatorType==='img'||e.name.match(new RegExp('\\\\.(png|jpg|svg|gif|webp)')));R.resourceBreakdown={total:newRes.length,api:apiRes.length,js:jsRes.length,css:cssRes.length,img:imgRes.length};R.totalCalls=Math.max(newApiLogs.length,apiRes.length);R.totalResources=newRes.length;if(R.totalCalls===0){ if(newRes.length>0){ const avgDur=Math.round(newRes.reduce((s,e)=>s+e.duration,0)/newRes.length); R.grade='PASS';R.ok=true; R.summary=newRes.length+'개 리소스 로드(API 0) | avg '+avgDur+'ms | PASS'; R.avgResponseTime=avgDur; }else{ R.grade='PASS';R.ok=true; R.summary='리소스/API 호출 없음 (SPA 캐시) | PASS'; } return JSON.stringify(R);}if(newApiLogs.length>0){ const errors4xx=newApiLogs.filter(l=>l.status>=400&&l.status<500); const errors5xx=newApiLogs.filter(l=>l.status>=500); const netErr=newApiLogs.filter(l=>!l.ok&&l.status===0); const slow=newApiLogs.filter(l=>l.duration>2000); const ok=newApiLogs.filter(l=>l.ok); R.errors4xx=errors4xx.length;R.errors5xx=errors5xx.length; R.slowCalls=slow.length;R.successCount=ok.length; const durs=newApiLogs.map(l=>l.duration).filter(d=>d>0); R.avgResponseTime=durs.length>0?Math.round(durs.reduce((a,b)=>a+b,0)/durs.length):0; R.maxResponseTime=durs.length>0?Math.max(...durs):0; const errRate=((errors4xx.length+errors5xx.length+netErr.length)/newApiLogs.length*100); R.errorRate=Math.round(errRate*10)/10; R.failedUrls=[...errors5xx,...errors4xx].slice(0,3).map(l=>l.url.split('?')[0].substring(0,80)); if(errors5xx.length>0||errRate>10){R.grade='FAIL';} else if(errors4xx.length>0||slow.length>3||R.avgResponseTime>1000){R.grade='WARN';} else{R.grade='PASS';} R.summary=newApiLogs.length+'개 API | '+ok.length+'OK '+(errors4xx.length+errors5xx.length)+'err '+slow.length+'slow | avg '+R.avgResponseTime+'ms | '+R.grade;}else{ const durs=apiRes.map(e=>Math.round(e.duration)); R.avgResponseTime=durs.length>0?Math.round(durs.reduce((a,b)=>a+b,0)/durs.length):0; R.slowCalls=apiRes.filter(e=>e.duration>2000).length; if(R.slowCalls>3||R.avgResponseTime>1000){R.grade='WARN';} else{R.grade='PASS';} R.summary=apiRes.length+'개 API(perf) | avg '+R.avgResponseTime+'ms | '+R.grade;}R.ok=true;return JSON.stringify(R);})()", "timeout": 10000, "phase": "API_AUDIT" }, { "id": 32, + "name": "[게시판 > 공지사항] 마커 기록", + "action": "evaluate", + "script": "(async()=>{const apiLogs=window.__E2E__?window.__E2E__.getApiLogs():{logs:[],summary:{}};window.__AH_MARK__={ ts:Date.now(), apiLogCount:apiLogs.logs.length, perfCount:performance.getEntriesByType('resource').length};return JSON.stringify({phase:'MARK_START',ok:true,info:'apiLogs:'+apiLogs.logs.length+' perf:'+window.__AH_MARK__.perfCount});})()", + "timeout": 3000, + "phase": "MARK" + }, + { + "id": 33, "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 호출 대기", @@ -272,7 +272,7 @@ "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);})()", + "script": "(async()=>{const R={phase:'API_AUDIT'};const mark=window.__AH_MARK__||{ts:0,apiLogCount:0,perfCount:0};const apiData=window.__E2E__?window.__E2E__.getApiLogs():{logs:[],errors:[],summary:{}};const newApiLogs=apiData.logs.slice(mark.apiLogCount);R.monitorCalls=newApiLogs.length;const allRes=performance.getEntriesByType('resource');const newRes=allRes.slice(mark.perfCount);const apiRes=newRes.filter(e=>e.name.includes('/api/')||e.name.includes('/v1/')||e.name.includes('/graphql'));R.perfApiCalls=apiRes.length;const jsRes=newRes.filter(e=>e.initiatorType==='script'||e.name.endsWith('.js'));const cssRes=newRes.filter(e=>e.initiatorType==='link'||e.name.endsWith('.css'));const imgRes=newRes.filter(e=>e.initiatorType==='img'||e.name.match(new RegExp('\\\\.(png|jpg|svg|gif|webp)')));R.resourceBreakdown={total:newRes.length,api:apiRes.length,js:jsRes.length,css:cssRes.length,img:imgRes.length};R.totalCalls=Math.max(newApiLogs.length,apiRes.length);R.totalResources=newRes.length;if(R.totalCalls===0){ if(newRes.length>0){ const avgDur=Math.round(newRes.reduce((s,e)=>s+e.duration,0)/newRes.length); R.grade='PASS';R.ok=true; R.summary=newRes.length+'개 리소스 로드(API 0) | avg '+avgDur+'ms | PASS'; R.avgResponseTime=avgDur; }else{ R.grade='PASS';R.ok=true; R.summary='리소스/API 호출 없음 (SPA 캐시) | PASS'; } return JSON.stringify(R);}if(newApiLogs.length>0){ const errors4xx=newApiLogs.filter(l=>l.status>=400&&l.status<500); const errors5xx=newApiLogs.filter(l=>l.status>=500); const netErr=newApiLogs.filter(l=>!l.ok&&l.status===0); const slow=newApiLogs.filter(l=>l.duration>2000); const ok=newApiLogs.filter(l=>l.ok); R.errors4xx=errors4xx.length;R.errors5xx=errors5xx.length; R.slowCalls=slow.length;R.successCount=ok.length; const durs=newApiLogs.map(l=>l.duration).filter(d=>d>0); R.avgResponseTime=durs.length>0?Math.round(durs.reduce((a,b)=>a+b,0)/durs.length):0; R.maxResponseTime=durs.length>0?Math.max(...durs):0; const errRate=((errors4xx.length+errors5xx.length+netErr.length)/newApiLogs.length*100); R.errorRate=Math.round(errRate*10)/10; R.failedUrls=[...errors5xx,...errors4xx].slice(0,3).map(l=>l.url.split('?')[0].substring(0,80)); if(errors5xx.length>0||errRate>10){R.grade='FAIL';} else if(errors4xx.length>0||slow.length>3||R.avgResponseTime>1000){R.grade='WARN';} else{R.grade='PASS';} R.summary=newApiLogs.length+'개 API | '+ok.length+'OK '+(errors4xx.length+errors5xx.length)+'err '+slow.length+'slow | avg '+R.avgResponseTime+'ms | '+R.grade;}else{ const durs=apiRes.map(e=>Math.round(e.duration)); R.avgResponseTime=durs.length>0?Math.round(durs.reduce((a,b)=>a+b,0)/durs.length):0; R.slowCalls=apiRes.filter(e=>e.duration>2000).length; if(R.slowCalls>3||R.avgResponseTime>1000){R.grade='WARN';} else{R.grade='PASS';} R.summary=apiRes.length+'개 API(perf) | avg '+R.avgResponseTime+'ms | '+R.grade;}R.ok=true;return JSON.stringify(R);})()", "timeout": 10000, "phase": "API_AUDIT" } diff --git a/api-health-sales-hr.json b/api-health-sales-hr.json index 8ce87a0..a140b10 100644 --- a/api-health-sales-hr.json +++ b/api-health-sales-hr.json @@ -1,7 +1,7 @@ { "id": "api-health-sales-hr", "name": "API 건강성 감사: 판매/인사", - "version": "1.0.0", + "version": "2.0.0", "auth": { "role": "admin" }, @@ -16,11 +16,11 @@ "steps": [ { "id": 1, - "name": "[판매관리 > 거래처관리] API 인터셉터 설치", + "name": "[판매관리 > 거래처관리] 마커 기록", "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);})()", + "script": "(async()=>{const apiLogs=window.__E2E__?window.__E2E__.getApiLogs():{logs:[],summary:{}};window.__AH_MARK__={ ts:Date.now(), apiLogCount:apiLogs.logs.length, perfCount:performance.getEntriesByType('resource').length};return JSON.stringify({phase:'MARK_START',ok:true,info:'apiLogs:'+apiLogs.logs.length+' perf:'+window.__AH_MARK__.perfCount});})()", "timeout": 5000, - "phase": "INSTALL" + "phase": "MARK" }, { "id": 2, @@ -32,26 +32,26 @@ "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);})()", + "script": "(async()=>{const R={phase:'API_AUDIT'};const mark=window.__AH_MARK__||{ts:0,apiLogCount:0,perfCount:0};const apiData=window.__E2E__?window.__E2E__.getApiLogs():{logs:[],errors:[],summary:{}};const newApiLogs=apiData.logs.slice(mark.apiLogCount);R.monitorCalls=newApiLogs.length;const allRes=performance.getEntriesByType('resource');const newRes=allRes.slice(mark.perfCount);const apiRes=newRes.filter(e=>e.name.includes('/api/')||e.name.includes('/v1/')||e.name.includes('/graphql'));R.perfApiCalls=apiRes.length;const jsRes=newRes.filter(e=>e.initiatorType==='script'||e.name.endsWith('.js'));const cssRes=newRes.filter(e=>e.initiatorType==='link'||e.name.endsWith('.css'));const imgRes=newRes.filter(e=>e.initiatorType==='img'||e.name.match(new RegExp('\\\\.(png|jpg|svg|gif|webp)')));R.resourceBreakdown={total:newRes.length,api:apiRes.length,js:jsRes.length,css:cssRes.length,img:imgRes.length};R.totalCalls=Math.max(newApiLogs.length,apiRes.length);R.totalResources=newRes.length;if(R.totalCalls===0){ if(newRes.length>0){ const avgDur=Math.round(newRes.reduce((s,e)=>s+e.duration,0)/newRes.length); R.grade='PASS';R.ok=true; R.summary=newRes.length+'개 리소스 로드(API 0) | avg '+avgDur+'ms | PASS'; R.avgResponseTime=avgDur; }else{ R.grade='PASS';R.ok=true; R.summary='리소스/API 호출 없음 (SPA 캐시) | PASS'; } return JSON.stringify(R);}if(newApiLogs.length>0){ const errors4xx=newApiLogs.filter(l=>l.status>=400&&l.status<500); const errors5xx=newApiLogs.filter(l=>l.status>=500); const netErr=newApiLogs.filter(l=>!l.ok&&l.status===0); const slow=newApiLogs.filter(l=>l.duration>2000); const ok=newApiLogs.filter(l=>l.ok); R.errors4xx=errors4xx.length;R.errors5xx=errors5xx.length; R.slowCalls=slow.length;R.successCount=ok.length; const durs=newApiLogs.map(l=>l.duration).filter(d=>d>0); R.avgResponseTime=durs.length>0?Math.round(durs.reduce((a,b)=>a+b,0)/durs.length):0; R.maxResponseTime=durs.length>0?Math.max(...durs):0; const errRate=((errors4xx.length+errors5xx.length+netErr.length)/newApiLogs.length*100); R.errorRate=Math.round(errRate*10)/10; R.failedUrls=[...errors5xx,...errors4xx].slice(0,3).map(l=>l.url.split('?')[0].substring(0,80)); if(errors5xx.length>0||errRate>10){R.grade='FAIL';} else if(errors4xx.length>0||slow.length>3||R.avgResponseTime>1000){R.grade='WARN';} else{R.grade='PASS';} R.summary=newApiLogs.length+'개 API | '+ok.length+'OK '+(errors4xx.length+errors5xx.length)+'err '+slow.length+'slow | avg '+R.avgResponseTime+'ms | '+R.grade;}else{ const durs=apiRes.map(e=>Math.round(e.duration)); R.avgResponseTime=durs.length>0?Math.round(durs.reduce((a,b)=>a+b,0)/durs.length):0; R.slowCalls=apiRes.filter(e=>e.duration>2000).length; if(R.slowCalls>3||R.avgResponseTime>1000){R.grade='WARN';} else{R.grade='PASS';} R.summary=apiRes.length+'개 API(perf) | avg '+R.avgResponseTime+'ms | '+R.grade;}R.ok=true;return JSON.stringify(R);})()", "timeout": 10000, "phase": "API_AUDIT" }, { "id": 4, + "name": "[판매관리 > 수주관리] 마커 기록", + "action": "evaluate", + "script": "(async()=>{const apiLogs=window.__E2E__?window.__E2E__.getApiLogs():{logs:[],summary:{}};window.__AH_MARK__={ ts:Date.now(), apiLogCount:apiLogs.logs.length, perfCount:performance.getEntriesByType('resource').length};return JSON.stringify({phase:'MARK_START',ok:true,info:'apiLogs:'+apiLogs.logs.length+' perf:'+window.__AH_MARK__.perfCount});})()", + "timeout": 3000, + "phase": "MARK" + }, + { + "id": 5, "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 호출 대기", @@ -62,26 +62,26 @@ "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);})()", + "script": "(async()=>{const R={phase:'API_AUDIT'};const mark=window.__AH_MARK__||{ts:0,apiLogCount:0,perfCount:0};const apiData=window.__E2E__?window.__E2E__.getApiLogs():{logs:[],errors:[],summary:{}};const newApiLogs=apiData.logs.slice(mark.apiLogCount);R.monitorCalls=newApiLogs.length;const allRes=performance.getEntriesByType('resource');const newRes=allRes.slice(mark.perfCount);const apiRes=newRes.filter(e=>e.name.includes('/api/')||e.name.includes('/v1/')||e.name.includes('/graphql'));R.perfApiCalls=apiRes.length;const jsRes=newRes.filter(e=>e.initiatorType==='script'||e.name.endsWith('.js'));const cssRes=newRes.filter(e=>e.initiatorType==='link'||e.name.endsWith('.css'));const imgRes=newRes.filter(e=>e.initiatorType==='img'||e.name.match(new RegExp('\\\\.(png|jpg|svg|gif|webp)')));R.resourceBreakdown={total:newRes.length,api:apiRes.length,js:jsRes.length,css:cssRes.length,img:imgRes.length};R.totalCalls=Math.max(newApiLogs.length,apiRes.length);R.totalResources=newRes.length;if(R.totalCalls===0){ if(newRes.length>0){ const avgDur=Math.round(newRes.reduce((s,e)=>s+e.duration,0)/newRes.length); R.grade='PASS';R.ok=true; R.summary=newRes.length+'개 리소스 로드(API 0) | avg '+avgDur+'ms | PASS'; R.avgResponseTime=avgDur; }else{ R.grade='PASS';R.ok=true; R.summary='리소스/API 호출 없음 (SPA 캐시) | PASS'; } return JSON.stringify(R);}if(newApiLogs.length>0){ const errors4xx=newApiLogs.filter(l=>l.status>=400&&l.status<500); const errors5xx=newApiLogs.filter(l=>l.status>=500); const netErr=newApiLogs.filter(l=>!l.ok&&l.status===0); const slow=newApiLogs.filter(l=>l.duration>2000); const ok=newApiLogs.filter(l=>l.ok); R.errors4xx=errors4xx.length;R.errors5xx=errors5xx.length; R.slowCalls=slow.length;R.successCount=ok.length; const durs=newApiLogs.map(l=>l.duration).filter(d=>d>0); R.avgResponseTime=durs.length>0?Math.round(durs.reduce((a,b)=>a+b,0)/durs.length):0; R.maxResponseTime=durs.length>0?Math.max(...durs):0; const errRate=((errors4xx.length+errors5xx.length+netErr.length)/newApiLogs.length*100); R.errorRate=Math.round(errRate*10)/10; R.failedUrls=[...errors5xx,...errors4xx].slice(0,3).map(l=>l.url.split('?')[0].substring(0,80)); if(errors5xx.length>0||errRate>10){R.grade='FAIL';} else if(errors4xx.length>0||slow.length>3||R.avgResponseTime>1000){R.grade='WARN';} else{R.grade='PASS';} R.summary=newApiLogs.length+'개 API | '+ok.length+'OK '+(errors4xx.length+errors5xx.length)+'err '+slow.length+'slow | avg '+R.avgResponseTime+'ms | '+R.grade;}else{ const durs=apiRes.map(e=>Math.round(e.duration)); R.avgResponseTime=durs.length>0?Math.round(durs.reduce((a,b)=>a+b,0)/durs.length):0; R.slowCalls=apiRes.filter(e=>e.duration>2000).length; if(R.slowCalls>3||R.avgResponseTime>1000){R.grade='WARN';} else{R.grade='PASS';} R.summary=apiRes.length+'개 API(perf) | avg '+R.avgResponseTime+'ms | '+R.grade;}R.ok=true;return JSON.stringify(R);})()", "timeout": 10000, "phase": "API_AUDIT" }, { "id": 8, + "name": "[판매관리 > 견적관리] 마커 기록", + "action": "evaluate", + "script": "(async()=>{const apiLogs=window.__E2E__?window.__E2E__.getApiLogs():{logs:[],summary:{}};window.__AH_MARK__={ ts:Date.now(), apiLogCount:apiLogs.logs.length, perfCount:performance.getEntriesByType('resource').length};return JSON.stringify({phase:'MARK_START',ok:true,info:'apiLogs:'+apiLogs.logs.length+' perf:'+window.__AH_MARK__.perfCount});})()", + "timeout": 3000, + "phase": "MARK" + }, + { + "id": 9, "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 호출 대기", @@ -92,26 +92,26 @@ "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);})()", + "script": "(async()=>{const R={phase:'API_AUDIT'};const mark=window.__AH_MARK__||{ts:0,apiLogCount:0,perfCount:0};const apiData=window.__E2E__?window.__E2E__.getApiLogs():{logs:[],errors:[],summary:{}};const newApiLogs=apiData.logs.slice(mark.apiLogCount);R.monitorCalls=newApiLogs.length;const allRes=performance.getEntriesByType('resource');const newRes=allRes.slice(mark.perfCount);const apiRes=newRes.filter(e=>e.name.includes('/api/')||e.name.includes('/v1/')||e.name.includes('/graphql'));R.perfApiCalls=apiRes.length;const jsRes=newRes.filter(e=>e.initiatorType==='script'||e.name.endsWith('.js'));const cssRes=newRes.filter(e=>e.initiatorType==='link'||e.name.endsWith('.css'));const imgRes=newRes.filter(e=>e.initiatorType==='img'||e.name.match(new RegExp('\\\\.(png|jpg|svg|gif|webp)')));R.resourceBreakdown={total:newRes.length,api:apiRes.length,js:jsRes.length,css:cssRes.length,img:imgRes.length};R.totalCalls=Math.max(newApiLogs.length,apiRes.length);R.totalResources=newRes.length;if(R.totalCalls===0){ if(newRes.length>0){ const avgDur=Math.round(newRes.reduce((s,e)=>s+e.duration,0)/newRes.length); R.grade='PASS';R.ok=true; R.summary=newRes.length+'개 리소스 로드(API 0) | avg '+avgDur+'ms | PASS'; R.avgResponseTime=avgDur; }else{ R.grade='PASS';R.ok=true; R.summary='리소스/API 호출 없음 (SPA 캐시) | PASS'; } return JSON.stringify(R);}if(newApiLogs.length>0){ const errors4xx=newApiLogs.filter(l=>l.status>=400&&l.status<500); const errors5xx=newApiLogs.filter(l=>l.status>=500); const netErr=newApiLogs.filter(l=>!l.ok&&l.status===0); const slow=newApiLogs.filter(l=>l.duration>2000); const ok=newApiLogs.filter(l=>l.ok); R.errors4xx=errors4xx.length;R.errors5xx=errors5xx.length; R.slowCalls=slow.length;R.successCount=ok.length; const durs=newApiLogs.map(l=>l.duration).filter(d=>d>0); R.avgResponseTime=durs.length>0?Math.round(durs.reduce((a,b)=>a+b,0)/durs.length):0; R.maxResponseTime=durs.length>0?Math.max(...durs):0; const errRate=((errors4xx.length+errors5xx.length+netErr.length)/newApiLogs.length*100); R.errorRate=Math.round(errRate*10)/10; R.failedUrls=[...errors5xx,...errors4xx].slice(0,3).map(l=>l.url.split('?')[0].substring(0,80)); if(errors5xx.length>0||errRate>10){R.grade='FAIL';} else if(errors4xx.length>0||slow.length>3||R.avgResponseTime>1000){R.grade='WARN';} else{R.grade='PASS';} R.summary=newApiLogs.length+'개 API | '+ok.length+'OK '+(errors4xx.length+errors5xx.length)+'err '+slow.length+'slow | avg '+R.avgResponseTime+'ms | '+R.grade;}else{ const durs=apiRes.map(e=>Math.round(e.duration)); R.avgResponseTime=durs.length>0?Math.round(durs.reduce((a,b)=>a+b,0)/durs.length):0; R.slowCalls=apiRes.filter(e=>e.duration>2000).length; if(R.slowCalls>3||R.avgResponseTime>1000){R.grade='WARN';} else{R.grade='PASS';} R.summary=apiRes.length+'개 API(perf) | avg '+R.avgResponseTime+'ms | '+R.grade;}R.ok=true;return JSON.stringify(R);})()", "timeout": 10000, "phase": "API_AUDIT" }, { "id": 12, + "name": "[판매관리 > 단가관리] 마커 기록", + "action": "evaluate", + "script": "(async()=>{const apiLogs=window.__E2E__?window.__E2E__.getApiLogs():{logs:[],summary:{}};window.__AH_MARK__={ ts:Date.now(), apiLogCount:apiLogs.logs.length, perfCount:performance.getEntriesByType('resource').length};return JSON.stringify({phase:'MARK_START',ok:true,info:'apiLogs:'+apiLogs.logs.length+' perf:'+window.__AH_MARK__.perfCount});})()", + "timeout": 3000, + "phase": "MARK" + }, + { + "id": 13, "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 호출 대기", @@ -122,26 +122,26 @@ "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);})()", + "script": "(async()=>{const R={phase:'API_AUDIT'};const mark=window.__AH_MARK__||{ts:0,apiLogCount:0,perfCount:0};const apiData=window.__E2E__?window.__E2E__.getApiLogs():{logs:[],errors:[],summary:{}};const newApiLogs=apiData.logs.slice(mark.apiLogCount);R.monitorCalls=newApiLogs.length;const allRes=performance.getEntriesByType('resource');const newRes=allRes.slice(mark.perfCount);const apiRes=newRes.filter(e=>e.name.includes('/api/')||e.name.includes('/v1/')||e.name.includes('/graphql'));R.perfApiCalls=apiRes.length;const jsRes=newRes.filter(e=>e.initiatorType==='script'||e.name.endsWith('.js'));const cssRes=newRes.filter(e=>e.initiatorType==='link'||e.name.endsWith('.css'));const imgRes=newRes.filter(e=>e.initiatorType==='img'||e.name.match(new RegExp('\\\\.(png|jpg|svg|gif|webp)')));R.resourceBreakdown={total:newRes.length,api:apiRes.length,js:jsRes.length,css:cssRes.length,img:imgRes.length};R.totalCalls=Math.max(newApiLogs.length,apiRes.length);R.totalResources=newRes.length;if(R.totalCalls===0){ if(newRes.length>0){ const avgDur=Math.round(newRes.reduce((s,e)=>s+e.duration,0)/newRes.length); R.grade='PASS';R.ok=true; R.summary=newRes.length+'개 리소스 로드(API 0) | avg '+avgDur+'ms | PASS'; R.avgResponseTime=avgDur; }else{ R.grade='PASS';R.ok=true; R.summary='리소스/API 호출 없음 (SPA 캐시) | PASS'; } return JSON.stringify(R);}if(newApiLogs.length>0){ const errors4xx=newApiLogs.filter(l=>l.status>=400&&l.status<500); const errors5xx=newApiLogs.filter(l=>l.status>=500); const netErr=newApiLogs.filter(l=>!l.ok&&l.status===0); const slow=newApiLogs.filter(l=>l.duration>2000); const ok=newApiLogs.filter(l=>l.ok); R.errors4xx=errors4xx.length;R.errors5xx=errors5xx.length; R.slowCalls=slow.length;R.successCount=ok.length; const durs=newApiLogs.map(l=>l.duration).filter(d=>d>0); R.avgResponseTime=durs.length>0?Math.round(durs.reduce((a,b)=>a+b,0)/durs.length):0; R.maxResponseTime=durs.length>0?Math.max(...durs):0; const errRate=((errors4xx.length+errors5xx.length+netErr.length)/newApiLogs.length*100); R.errorRate=Math.round(errRate*10)/10; R.failedUrls=[...errors5xx,...errors4xx].slice(0,3).map(l=>l.url.split('?')[0].substring(0,80)); if(errors5xx.length>0||errRate>10){R.grade='FAIL';} else if(errors4xx.length>0||slow.length>3||R.avgResponseTime>1000){R.grade='WARN';} else{R.grade='PASS';} R.summary=newApiLogs.length+'개 API | '+ok.length+'OK '+(errors4xx.length+errors5xx.length)+'err '+slow.length+'slow | avg '+R.avgResponseTime+'ms | '+R.grade;}else{ const durs=apiRes.map(e=>Math.round(e.duration)); R.avgResponseTime=durs.length>0?Math.round(durs.reduce((a,b)=>a+b,0)/durs.length):0; R.slowCalls=apiRes.filter(e=>e.duration>2000).length; if(R.slowCalls>3||R.avgResponseTime>1000){R.grade='WARN';} else{R.grade='PASS';} R.summary=apiRes.length+'개 API(perf) | avg '+R.avgResponseTime+'ms | '+R.grade;}R.ok=true;return JSON.stringify(R);})()", "timeout": 10000, "phase": "API_AUDIT" }, { "id": 16, + "name": "[인사관리 > 사원관리] 마커 기록", + "action": "evaluate", + "script": "(async()=>{const apiLogs=window.__E2E__?window.__E2E__.getApiLogs():{logs:[],summary:{}};window.__AH_MARK__={ ts:Date.now(), apiLogCount:apiLogs.logs.length, perfCount:performance.getEntriesByType('resource').length};return JSON.stringify({phase:'MARK_START',ok:true,info:'apiLogs:'+apiLogs.logs.length+' perf:'+window.__AH_MARK__.perfCount});})()", + "timeout": 3000, + "phase": "MARK" + }, + { + "id": 17, "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 호출 대기", @@ -152,26 +152,26 @@ "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);})()", + "script": "(async()=>{const R={phase:'API_AUDIT'};const mark=window.__AH_MARK__||{ts:0,apiLogCount:0,perfCount:0};const apiData=window.__E2E__?window.__E2E__.getApiLogs():{logs:[],errors:[],summary:{}};const newApiLogs=apiData.logs.slice(mark.apiLogCount);R.monitorCalls=newApiLogs.length;const allRes=performance.getEntriesByType('resource');const newRes=allRes.slice(mark.perfCount);const apiRes=newRes.filter(e=>e.name.includes('/api/')||e.name.includes('/v1/')||e.name.includes('/graphql'));R.perfApiCalls=apiRes.length;const jsRes=newRes.filter(e=>e.initiatorType==='script'||e.name.endsWith('.js'));const cssRes=newRes.filter(e=>e.initiatorType==='link'||e.name.endsWith('.css'));const imgRes=newRes.filter(e=>e.initiatorType==='img'||e.name.match(new RegExp('\\\\.(png|jpg|svg|gif|webp)')));R.resourceBreakdown={total:newRes.length,api:apiRes.length,js:jsRes.length,css:cssRes.length,img:imgRes.length};R.totalCalls=Math.max(newApiLogs.length,apiRes.length);R.totalResources=newRes.length;if(R.totalCalls===0){ if(newRes.length>0){ const avgDur=Math.round(newRes.reduce((s,e)=>s+e.duration,0)/newRes.length); R.grade='PASS';R.ok=true; R.summary=newRes.length+'개 리소스 로드(API 0) | avg '+avgDur+'ms | PASS'; R.avgResponseTime=avgDur; }else{ R.grade='PASS';R.ok=true; R.summary='리소스/API 호출 없음 (SPA 캐시) | PASS'; } return JSON.stringify(R);}if(newApiLogs.length>0){ const errors4xx=newApiLogs.filter(l=>l.status>=400&&l.status<500); const errors5xx=newApiLogs.filter(l=>l.status>=500); const netErr=newApiLogs.filter(l=>!l.ok&&l.status===0); const slow=newApiLogs.filter(l=>l.duration>2000); const ok=newApiLogs.filter(l=>l.ok); R.errors4xx=errors4xx.length;R.errors5xx=errors5xx.length; R.slowCalls=slow.length;R.successCount=ok.length; const durs=newApiLogs.map(l=>l.duration).filter(d=>d>0); R.avgResponseTime=durs.length>0?Math.round(durs.reduce((a,b)=>a+b,0)/durs.length):0; R.maxResponseTime=durs.length>0?Math.max(...durs):0; const errRate=((errors4xx.length+errors5xx.length+netErr.length)/newApiLogs.length*100); R.errorRate=Math.round(errRate*10)/10; R.failedUrls=[...errors5xx,...errors4xx].slice(0,3).map(l=>l.url.split('?')[0].substring(0,80)); if(errors5xx.length>0||errRate>10){R.grade='FAIL';} else if(errors4xx.length>0||slow.length>3||R.avgResponseTime>1000){R.grade='WARN';} else{R.grade='PASS';} R.summary=newApiLogs.length+'개 API | '+ok.length+'OK '+(errors4xx.length+errors5xx.length)+'err '+slow.length+'slow | avg '+R.avgResponseTime+'ms | '+R.grade;}else{ const durs=apiRes.map(e=>Math.round(e.duration)); R.avgResponseTime=durs.length>0?Math.round(durs.reduce((a,b)=>a+b,0)/durs.length):0; R.slowCalls=apiRes.filter(e=>e.duration>2000).length; if(R.slowCalls>3||R.avgResponseTime>1000){R.grade='WARN';} else{R.grade='PASS';} R.summary=apiRes.length+'개 API(perf) | avg '+R.avgResponseTime+'ms | '+R.grade;}R.ok=true;return JSON.stringify(R);})()", "timeout": 10000, "phase": "API_AUDIT" }, { "id": 20, + "name": "[인사관리 > 급여관리] 마커 기록", + "action": "evaluate", + "script": "(async()=>{const apiLogs=window.__E2E__?window.__E2E__.getApiLogs():{logs:[],summary:{}};window.__AH_MARK__={ ts:Date.now(), apiLogCount:apiLogs.logs.length, perfCount:performance.getEntriesByType('resource').length};return JSON.stringify({phase:'MARK_START',ok:true,info:'apiLogs:'+apiLogs.logs.length+' perf:'+window.__AH_MARK__.perfCount});})()", + "timeout": 3000, + "phase": "MARK" + }, + { + "id": 21, "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 호출 대기", @@ -182,26 +182,26 @@ "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);})()", + "script": "(async()=>{const R={phase:'API_AUDIT'};const mark=window.__AH_MARK__||{ts:0,apiLogCount:0,perfCount:0};const apiData=window.__E2E__?window.__E2E__.getApiLogs():{logs:[],errors:[],summary:{}};const newApiLogs=apiData.logs.slice(mark.apiLogCount);R.monitorCalls=newApiLogs.length;const allRes=performance.getEntriesByType('resource');const newRes=allRes.slice(mark.perfCount);const apiRes=newRes.filter(e=>e.name.includes('/api/')||e.name.includes('/v1/')||e.name.includes('/graphql'));R.perfApiCalls=apiRes.length;const jsRes=newRes.filter(e=>e.initiatorType==='script'||e.name.endsWith('.js'));const cssRes=newRes.filter(e=>e.initiatorType==='link'||e.name.endsWith('.css'));const imgRes=newRes.filter(e=>e.initiatorType==='img'||e.name.match(new RegExp('\\\\.(png|jpg|svg|gif|webp)')));R.resourceBreakdown={total:newRes.length,api:apiRes.length,js:jsRes.length,css:cssRes.length,img:imgRes.length};R.totalCalls=Math.max(newApiLogs.length,apiRes.length);R.totalResources=newRes.length;if(R.totalCalls===0){ if(newRes.length>0){ const avgDur=Math.round(newRes.reduce((s,e)=>s+e.duration,0)/newRes.length); R.grade='PASS';R.ok=true; R.summary=newRes.length+'개 리소스 로드(API 0) | avg '+avgDur+'ms | PASS'; R.avgResponseTime=avgDur; }else{ R.grade='PASS';R.ok=true; R.summary='리소스/API 호출 없음 (SPA 캐시) | PASS'; } return JSON.stringify(R);}if(newApiLogs.length>0){ const errors4xx=newApiLogs.filter(l=>l.status>=400&&l.status<500); const errors5xx=newApiLogs.filter(l=>l.status>=500); const netErr=newApiLogs.filter(l=>!l.ok&&l.status===0); const slow=newApiLogs.filter(l=>l.duration>2000); const ok=newApiLogs.filter(l=>l.ok); R.errors4xx=errors4xx.length;R.errors5xx=errors5xx.length; R.slowCalls=slow.length;R.successCount=ok.length; const durs=newApiLogs.map(l=>l.duration).filter(d=>d>0); R.avgResponseTime=durs.length>0?Math.round(durs.reduce((a,b)=>a+b,0)/durs.length):0; R.maxResponseTime=durs.length>0?Math.max(...durs):0; const errRate=((errors4xx.length+errors5xx.length+netErr.length)/newApiLogs.length*100); R.errorRate=Math.round(errRate*10)/10; R.failedUrls=[...errors5xx,...errors4xx].slice(0,3).map(l=>l.url.split('?')[0].substring(0,80)); if(errors5xx.length>0||errRate>10){R.grade='FAIL';} else if(errors4xx.length>0||slow.length>3||R.avgResponseTime>1000){R.grade='WARN';} else{R.grade='PASS';} R.summary=newApiLogs.length+'개 API | '+ok.length+'OK '+(errors4xx.length+errors5xx.length)+'err '+slow.length+'slow | avg '+R.avgResponseTime+'ms | '+R.grade;}else{ const durs=apiRes.map(e=>Math.round(e.duration)); R.avgResponseTime=durs.length>0?Math.round(durs.reduce((a,b)=>a+b,0)/durs.length):0; R.slowCalls=apiRes.filter(e=>e.duration>2000).length; if(R.slowCalls>3||R.avgResponseTime>1000){R.grade='WARN';} else{R.grade='PASS';} R.summary=apiRes.length+'개 API(perf) | avg '+R.avgResponseTime+'ms | '+R.grade;}R.ok=true;return JSON.stringify(R);})()", "timeout": 10000, "phase": "API_AUDIT" }, { "id": 24, + "name": "[인사관리 > 근태현황] 마커 기록", + "action": "evaluate", + "script": "(async()=>{const apiLogs=window.__E2E__?window.__E2E__.getApiLogs():{logs:[],summary:{}};window.__AH_MARK__={ ts:Date.now(), apiLogCount:apiLogs.logs.length, perfCount:performance.getEntriesByType('resource').length};return JSON.stringify({phase:'MARK_START',ok:true,info:'apiLogs:'+apiLogs.logs.length+' perf:'+window.__AH_MARK__.perfCount});})()", + "timeout": 3000, + "phase": "MARK" + }, + { + "id": 25, "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 호출 대기", @@ -212,26 +212,26 @@ "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);})()", + "script": "(async()=>{const R={phase:'API_AUDIT'};const mark=window.__AH_MARK__||{ts:0,apiLogCount:0,perfCount:0};const apiData=window.__E2E__?window.__E2E__.getApiLogs():{logs:[],errors:[],summary:{}};const newApiLogs=apiData.logs.slice(mark.apiLogCount);R.monitorCalls=newApiLogs.length;const allRes=performance.getEntriesByType('resource');const newRes=allRes.slice(mark.perfCount);const apiRes=newRes.filter(e=>e.name.includes('/api/')||e.name.includes('/v1/')||e.name.includes('/graphql'));R.perfApiCalls=apiRes.length;const jsRes=newRes.filter(e=>e.initiatorType==='script'||e.name.endsWith('.js'));const cssRes=newRes.filter(e=>e.initiatorType==='link'||e.name.endsWith('.css'));const imgRes=newRes.filter(e=>e.initiatorType==='img'||e.name.match(new RegExp('\\\\.(png|jpg|svg|gif|webp)')));R.resourceBreakdown={total:newRes.length,api:apiRes.length,js:jsRes.length,css:cssRes.length,img:imgRes.length};R.totalCalls=Math.max(newApiLogs.length,apiRes.length);R.totalResources=newRes.length;if(R.totalCalls===0){ if(newRes.length>0){ const avgDur=Math.round(newRes.reduce((s,e)=>s+e.duration,0)/newRes.length); R.grade='PASS';R.ok=true; R.summary=newRes.length+'개 리소스 로드(API 0) | avg '+avgDur+'ms | PASS'; R.avgResponseTime=avgDur; }else{ R.grade='PASS';R.ok=true; R.summary='리소스/API 호출 없음 (SPA 캐시) | PASS'; } return JSON.stringify(R);}if(newApiLogs.length>0){ const errors4xx=newApiLogs.filter(l=>l.status>=400&&l.status<500); const errors5xx=newApiLogs.filter(l=>l.status>=500); const netErr=newApiLogs.filter(l=>!l.ok&&l.status===0); const slow=newApiLogs.filter(l=>l.duration>2000); const ok=newApiLogs.filter(l=>l.ok); R.errors4xx=errors4xx.length;R.errors5xx=errors5xx.length; R.slowCalls=slow.length;R.successCount=ok.length; const durs=newApiLogs.map(l=>l.duration).filter(d=>d>0); R.avgResponseTime=durs.length>0?Math.round(durs.reduce((a,b)=>a+b,0)/durs.length):0; R.maxResponseTime=durs.length>0?Math.max(...durs):0; const errRate=((errors4xx.length+errors5xx.length+netErr.length)/newApiLogs.length*100); R.errorRate=Math.round(errRate*10)/10; R.failedUrls=[...errors5xx,...errors4xx].slice(0,3).map(l=>l.url.split('?')[0].substring(0,80)); if(errors5xx.length>0||errRate>10){R.grade='FAIL';} else if(errors4xx.length>0||slow.length>3||R.avgResponseTime>1000){R.grade='WARN';} else{R.grade='PASS';} R.summary=newApiLogs.length+'개 API | '+ok.length+'OK '+(errors4xx.length+errors5xx.length)+'err '+slow.length+'slow | avg '+R.avgResponseTime+'ms | '+R.grade;}else{ const durs=apiRes.map(e=>Math.round(e.duration)); R.avgResponseTime=durs.length>0?Math.round(durs.reduce((a,b)=>a+b,0)/durs.length):0; R.slowCalls=apiRes.filter(e=>e.duration>2000).length; if(R.slowCalls>3||R.avgResponseTime>1000){R.grade='WARN';} else{R.grade='PASS';} R.summary=apiRes.length+'개 API(perf) | avg '+R.avgResponseTime+'ms | '+R.grade;}R.ok=true;return JSON.stringify(R);})()", "timeout": 10000, "phase": "API_AUDIT" }, { "id": 28, + "name": "[인사관리 > 휴가관리] 마커 기록", + "action": "evaluate", + "script": "(async()=>{const apiLogs=window.__E2E__?window.__E2E__.getApiLogs():{logs:[],summary:{}};window.__AH_MARK__={ ts:Date.now(), apiLogCount:apiLogs.logs.length, perfCount:performance.getEntriesByType('resource').length};return JSON.stringify({phase:'MARK_START',ok:true,info:'apiLogs:'+apiLogs.logs.length+' perf:'+window.__AH_MARK__.perfCount});})()", + "timeout": 3000, + "phase": "MARK" + }, + { + "id": 29, "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 호출 대기", @@ -242,26 +242,26 @@ "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);})()", + "script": "(async()=>{const R={phase:'API_AUDIT'};const mark=window.__AH_MARK__||{ts:0,apiLogCount:0,perfCount:0};const apiData=window.__E2E__?window.__E2E__.getApiLogs():{logs:[],errors:[],summary:{}};const newApiLogs=apiData.logs.slice(mark.apiLogCount);R.monitorCalls=newApiLogs.length;const allRes=performance.getEntriesByType('resource');const newRes=allRes.slice(mark.perfCount);const apiRes=newRes.filter(e=>e.name.includes('/api/')||e.name.includes('/v1/')||e.name.includes('/graphql'));R.perfApiCalls=apiRes.length;const jsRes=newRes.filter(e=>e.initiatorType==='script'||e.name.endsWith('.js'));const cssRes=newRes.filter(e=>e.initiatorType==='link'||e.name.endsWith('.css'));const imgRes=newRes.filter(e=>e.initiatorType==='img'||e.name.match(new RegExp('\\\\.(png|jpg|svg|gif|webp)')));R.resourceBreakdown={total:newRes.length,api:apiRes.length,js:jsRes.length,css:cssRes.length,img:imgRes.length};R.totalCalls=Math.max(newApiLogs.length,apiRes.length);R.totalResources=newRes.length;if(R.totalCalls===0){ if(newRes.length>0){ const avgDur=Math.round(newRes.reduce((s,e)=>s+e.duration,0)/newRes.length); R.grade='PASS';R.ok=true; R.summary=newRes.length+'개 리소스 로드(API 0) | avg '+avgDur+'ms | PASS'; R.avgResponseTime=avgDur; }else{ R.grade='PASS';R.ok=true; R.summary='리소스/API 호출 없음 (SPA 캐시) | PASS'; } return JSON.stringify(R);}if(newApiLogs.length>0){ const errors4xx=newApiLogs.filter(l=>l.status>=400&&l.status<500); const errors5xx=newApiLogs.filter(l=>l.status>=500); const netErr=newApiLogs.filter(l=>!l.ok&&l.status===0); const slow=newApiLogs.filter(l=>l.duration>2000); const ok=newApiLogs.filter(l=>l.ok); R.errors4xx=errors4xx.length;R.errors5xx=errors5xx.length; R.slowCalls=slow.length;R.successCount=ok.length; const durs=newApiLogs.map(l=>l.duration).filter(d=>d>0); R.avgResponseTime=durs.length>0?Math.round(durs.reduce((a,b)=>a+b,0)/durs.length):0; R.maxResponseTime=durs.length>0?Math.max(...durs):0; const errRate=((errors4xx.length+errors5xx.length+netErr.length)/newApiLogs.length*100); R.errorRate=Math.round(errRate*10)/10; R.failedUrls=[...errors5xx,...errors4xx].slice(0,3).map(l=>l.url.split('?')[0].substring(0,80)); if(errors5xx.length>0||errRate>10){R.grade='FAIL';} else if(errors4xx.length>0||slow.length>3||R.avgResponseTime>1000){R.grade='WARN';} else{R.grade='PASS';} R.summary=newApiLogs.length+'개 API | '+ok.length+'OK '+(errors4xx.length+errors5xx.length)+'err '+slow.length+'slow | avg '+R.avgResponseTime+'ms | '+R.grade;}else{ const durs=apiRes.map(e=>Math.round(e.duration)); R.avgResponseTime=durs.length>0?Math.round(durs.reduce((a,b)=>a+b,0)/durs.length):0; R.slowCalls=apiRes.filter(e=>e.duration>2000).length; if(R.slowCalls>3||R.avgResponseTime>1000){R.grade='WARN';} else{R.grade='PASS';} R.summary=apiRes.length+'개 API(perf) | avg '+R.avgResponseTime+'ms | '+R.grade;}R.ok=true;return JSON.stringify(R);})()", "timeout": 10000, "phase": "API_AUDIT" }, { "id": 32, + "name": "[인사관리 > 카드관리] 마커 기록", + "action": "evaluate", + "script": "(async()=>{const apiLogs=window.__E2E__?window.__E2E__.getApiLogs():{logs:[],summary:{}};window.__AH_MARK__={ ts:Date.now(), apiLogCount:apiLogs.logs.length, perfCount:performance.getEntriesByType('resource').length};return JSON.stringify({phase:'MARK_START',ok:true,info:'apiLogs:'+apiLogs.logs.length+' perf:'+window.__AH_MARK__.perfCount});})()", + "timeout": 3000, + "phase": "MARK" + }, + { + "id": 33, "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 호출 대기", @@ -272,7 +272,7 @@ "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);})()", + "script": "(async()=>{const R={phase:'API_AUDIT'};const mark=window.__AH_MARK__||{ts:0,apiLogCount:0,perfCount:0};const apiData=window.__E2E__?window.__E2E__.getApiLogs():{logs:[],errors:[],summary:{}};const newApiLogs=apiData.logs.slice(mark.apiLogCount);R.monitorCalls=newApiLogs.length;const allRes=performance.getEntriesByType('resource');const newRes=allRes.slice(mark.perfCount);const apiRes=newRes.filter(e=>e.name.includes('/api/')||e.name.includes('/v1/')||e.name.includes('/graphql'));R.perfApiCalls=apiRes.length;const jsRes=newRes.filter(e=>e.initiatorType==='script'||e.name.endsWith('.js'));const cssRes=newRes.filter(e=>e.initiatorType==='link'||e.name.endsWith('.css'));const imgRes=newRes.filter(e=>e.initiatorType==='img'||e.name.match(new RegExp('\\\\.(png|jpg|svg|gif|webp)')));R.resourceBreakdown={total:newRes.length,api:apiRes.length,js:jsRes.length,css:cssRes.length,img:imgRes.length};R.totalCalls=Math.max(newApiLogs.length,apiRes.length);R.totalResources=newRes.length;if(R.totalCalls===0){ if(newRes.length>0){ const avgDur=Math.round(newRes.reduce((s,e)=>s+e.duration,0)/newRes.length); R.grade='PASS';R.ok=true; R.summary=newRes.length+'개 리소스 로드(API 0) | avg '+avgDur+'ms | PASS'; R.avgResponseTime=avgDur; }else{ R.grade='PASS';R.ok=true; R.summary='리소스/API 호출 없음 (SPA 캐시) | PASS'; } return JSON.stringify(R);}if(newApiLogs.length>0){ const errors4xx=newApiLogs.filter(l=>l.status>=400&&l.status<500); const errors5xx=newApiLogs.filter(l=>l.status>=500); const netErr=newApiLogs.filter(l=>!l.ok&&l.status===0); const slow=newApiLogs.filter(l=>l.duration>2000); const ok=newApiLogs.filter(l=>l.ok); R.errors4xx=errors4xx.length;R.errors5xx=errors5xx.length; R.slowCalls=slow.length;R.successCount=ok.length; const durs=newApiLogs.map(l=>l.duration).filter(d=>d>0); R.avgResponseTime=durs.length>0?Math.round(durs.reduce((a,b)=>a+b,0)/durs.length):0; R.maxResponseTime=durs.length>0?Math.max(...durs):0; const errRate=((errors4xx.length+errors5xx.length+netErr.length)/newApiLogs.length*100); R.errorRate=Math.round(errRate*10)/10; R.failedUrls=[...errors5xx,...errors4xx].slice(0,3).map(l=>l.url.split('?')[0].substring(0,80)); if(errors5xx.length>0||errRate>10){R.grade='FAIL';} else if(errors4xx.length>0||slow.length>3||R.avgResponseTime>1000){R.grade='WARN';} else{R.grade='PASS';} R.summary=newApiLogs.length+'개 API | '+ok.length+'OK '+(errors4xx.length+errors5xx.length)+'err '+slow.length+'slow | avg '+R.avgResponseTime+'ms | '+R.grade;}else{ const durs=apiRes.map(e=>Math.round(e.duration)); R.avgResponseTime=durs.length>0?Math.round(durs.reduce((a,b)=>a+b,0)/durs.length):0; R.slowCalls=apiRes.filter(e=>e.duration>2000).length; if(R.slowCalls>3||R.avgResponseTime>1000){R.grade='WARN';} else{R.grade='PASS';} R.summary=apiRes.length+'개 API(perf) | avg '+R.avgResponseTime+'ms | '+R.grade;}R.ok=true;return JSON.stringify(R);})()", "timeout": 10000, "phase": "API_AUDIT" }