diff --git a/src/styles/dashboard.css b/src/styles/dashboard.css index ee5b84f..838964b 100644 --- a/src/styles/dashboard.css +++ b/src/styles/dashboard.css @@ -185,11 +185,16 @@ .dashboard-slider-track { display: flex; transition: transform 0.5s cubic-bezier(0.25, 0.8, 0.25, 1); - width: 200%; /* For 2 pages */ + width: 400%; /* For 4 pages */ } .dashboard-slide { - width: 50%; /* 100% / 2 pages */ + width: 25%; /* 100% / 4 pages */ flex-shrink: 0; padding: 0 2px; /* Slight padding to avoid cutting off box-shadows */ + height: calc(100vh - 150px); + min-height: 520px; + display: flex; + flex-direction: column; + box-sizing: border-box; } diff --git a/src/views/Dashboard/HwDashboard.ts b/src/views/Dashboard/HwDashboard.ts index 069b612..932203f 100644 --- a/src/views/Dashboard/HwDashboard.ts +++ b/src/views/Dashboard/HwDashboard.ts @@ -10,6 +10,89 @@ declare global { } let jobChartInstance: any = null; +let totalPcMismatchByCorpChartInstance: any = null; +let totalServerMismatchByPurposeChartInstance: any = null; + +// 4p charts +let jobChartInstance4p: any = null; +let corpChartInstance4p: any = null; +let totalServerMismatchByPurposeChartInstance4p: any = null; +let serverServiceChartInstance4p: any = null; +let serverStatusChartInstance4p: any = null; + +// ─── 서버 용도별 카테고리 분류 헬퍼 ─── +function categorizePurpose(purpose: string): string { + if (!purpose) return '기타/일반'; + const lower = purpose.toLowerCase(); + + if ( + lower.includes('해석') || + lower.includes('abaqus') || + lower.includes('ai') || + lower.includes('시뮬레이션') || + lower.includes('processing') || + lower.includes('매핑') || + lower.includes('측량') || + lower.includes('렌더링') || + lower.includes('gpu') + ) { + return '해석/분석/AI'; + } + + if ( + lower.includes('개발') || + lower.includes('test') || + lower.includes('테스트') || + lower.includes('dev') || + lower.includes('unity') || + lower.includes('유니티') + ) { + return '개발/테스트'; + } + + if ( + lower.includes('was') || + lower.includes('web') || + lower.includes('웹') || + lower.includes('배포') || + lower.includes('nginx') || + lower.includes('apache') || + lower.includes('홈페이지') || + lower.includes('서비스') + ) { + return '서비스/웹/WAS'; + } + + if ( + lower.includes('postgresql') || + lower.includes('postgres') || + lower.includes('db') || + lower.includes('데이터') || + lower.includes('스토리지') || + lower.includes('storage') || + lower.includes('mysql') || + lower.includes('sql') || + lower.includes('oracle') + ) { + return 'DB/스토리지'; + } + + if ( + lower.includes('백업') || + lower.includes('backup') || + lower.includes('ids') || + lower.includes('ips') || + lower.includes('crowdsec') || + lower.includes('opnsense') || + lower.includes('관리') || + lower.includes('인증') || + lower.includes('보안') + ) { + return '백업/관리/보안'; + } + + return '기타/일반'; +} // ─── 네트워크 트래픽 문자열을 숫자(GB)로 파싱하는 헬퍼 ─── function parseTrafficToGb(trafficStr: string): number { @@ -635,6 +718,87 @@ export function renderHwDashboard(container: HTMLElement) { .filter(a => a['_server_status'] === '방치 의심') .slice(0, 5); + // --- TOTAL EXEC DASHBOARD STATS (종합 대시보드 통계 연산) --- + let totalAssetValue = 0; + allHw.forEach(a => { + const amt = parseInt(String(a[ASSET_SCHEMA.PURCHASE_AMOUNT.key] || '0').replace(/[^0-9]/g, ''), 10) || 0; + totalAssetValue += amt; + }); + + let costSavingPotential = 0; + servers.forEach(a => { + const status = a['_server_status']; + if (status === '자원 과잉' || status === '방치 의심') { + const amt = parseInt(String(a[ASSET_SCHEMA.PURCHASE_AMOUNT.key] || '0').replace(/[^0-9]/g, ''), 10) || 0; + costSavingPotential += amt; + } + }); + pcs.forEach(pc => { + if (pc['_spec_status'] === '오버스펙') { + const amt = parseInt(String(pc[ASSET_SCHEMA.PURCHASE_AMOUNT.key] || '0').replace(/[^0-9]/g, ''), 10) || 0; + costSavingPotential += amt; + } + }); + + const totalEvaluatedDevices = pcs.length + servers.length; + let optimalDevicesCount = 0; + pcs.forEach(pc => { if (pc['_spec_status'] === '적정') optimalDevicesCount++; }); + servers.forEach(s => { if (s['_server_status'] === '적정') optimalDevicesCount++; }); + const assetOptimizationRate = totalEvaluatedDevices > 0 ? Math.round((optimalDevicesCount / totalEvaluatedDevices) * 100) : 0; + + let pcOver5YearsCount = 0; + pcs.forEach(pc => { + const pDate = pc[ASSET_SCHEMA.PURCHASE_DATE.key]; + if (pDate && calculateAssetAge(pDate) >= 5) pcOver5YearsCount++; + }); + + let totalOver5YearsCount = pcOver5YearsCount; + servers.forEach(s => { + const pDate = s[ASSET_SCHEMA.PURCHASE_DATE.key] || s.purchase_date; + if (pDate && calculateAssetAge(pDate) >= 5) totalOver5YearsCount++; + }); + + let totalResourceBottleneckCount = 0; + servers.forEach(s => { + if (s['_server_status'] === '자원 부족') totalResourceBottleneckCount++; + }); + + let totalInactiveCount = 0; + servers.forEach(s => { + if (s['_server_status'] === '방치 의심') totalInactiveCount++; + }); + + // 용도별 서버 자원 과부족 대수 집계 + const PURPOSE_CATEGORIES = ['개발/테스트', '서비스/웹/WAS', 'DB/스토리지', '해석/분석/AI', '백업/관리/보안', '기타/일반']; + const purposeServerUnders = PURPOSE_CATEGORIES.map(cat => + servers.filter(s => categorizePurpose(s[ASSET_SCHEMA.ASSET_PURPOSE.key] || s.asset_purpose) === cat && s['_server_status'] === '자원 부족').length + ); + const purposeServerOvers = PURPOSE_CATEGORIES.map(cat => + servers.filter(s => categorizePurpose(s[ASSET_SCHEMA.ASSET_PURPOSE.key] || s.asset_purpose) === cat && s['_server_status'] === '자원 과잉').length + ); + + // 차트용 데이터 + const assetTypesCount = { + pc: pcs.length, + server: servers.filter(s => s.asset_type.includes('서버') || s.asset_type.includes('VM')).length, + storage: servers.filter(s => s.asset_type.toUpperCase().includes('NAS') || s.asset_type.toUpperCase().includes('스토리지') || s.asset_type.toUpperCase().includes('STO')).length, + other: allHw.length - pcs.length - servers.length + }; + + const pcStatusSummary = { + optimal: pcs.filter(p => p._spec_status === '적정').length, + over: pcs.filter(p => p._spec_status === '오버스펙').length, + under: pcs.filter(p => p._spec_status === '사양 부족').length, + inactive: 0 + }; + + const serverStatusSummary = { + optimal: serverStatusGroups.optimal, + over: serverStatusGroups.overSpec, + under: serverStatusGroups.underSpec, + inactive: serverStatusGroups.inactive + }; + // --- PRE-BUILD HTML --- const corpScores = buildCorpScores(pcs); @@ -660,7 +824,7 @@ export function renderHwDashboard(container: HTMLElement) { '' + '
' + '' + @@ -668,7 +832,52 @@ export function renderHwDashboard(container: HTMLElement) { '