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) { '' + '
' + '' + - '1 / 2' + + '1 / 4' + '' + '
' + '' + @@ -668,7 +832,52 @@ export function renderHwDashboard(container: HTMLElement) { '
' + '
' + - // ── SLIDE 1: PC DASHBOARD ── + // ── SLIDE 1: TOTAL EXECUTIVE DASHBOARD ── + '
' + + '

📊 전사 IT 자산 및 자원 최적화 요약

' + + + // KPI Row + '
' + + '
' + + '
' + + 'PC 사양 부족 장비' + + '
' + underSpecCount + '
' + + '
개인용 PC 대비 ' + (pcs.length > 0 ? Math.round((underSpecCount / pcs.length) * 100) : 0) + '% 비율
' + + '
' + + '
' + + '
' + + 'PC 오버스펙 장비' + + '
' + overSpecCount + '
' + + '
개인용 PC 대비 ' + (pcs.length > 0 ? Math.round((overSpecCount / pcs.length) * 100) : 0) + '% 비율
' + + '
' + + '
' + + '
' + + '서버 자원 부족 장비' + + '
' + serverStatusGroups.underSpec + '
' + + '
서버/NAS 대비 ' + (servers.length > 0 ? Math.round((serverStatusGroups.underSpec / servers.length) * 100) : 0) + '% 비율
' + + '
' + + '
' + + '
' + + '서버 자원 과잉 장비' + + '
' + serverStatusGroups.overSpec + '
' + + '
서버/NAS 대비 ' + (servers.length > 0 ? Math.round((serverStatusGroups.overSpec / servers.length) * 100) : 0) + '% 비율
' + + '
' + + '
' + + + // Charts + '
' + + '
' + + '

가족사별 PC 사양 과부족 현황

' + + '
' + + '
' + + '
' + + '

용도별 서버 자원 과부족 현황

' + + '
' + + '
' + + '
' + + '
' + + + // ── SLIDE 2: PC DASHBOARD ── '
' + '

💻 PC 사양 적정성 분석

' + @@ -692,9 +901,9 @@ export function renderHwDashboard(container: HTMLElement) { '
' + overSpecCount + '
' + '
직무 평균 대비 30% 이상 초과  ▸ 클릭하여 상세보기
' + '
' + - '
' + + '
' + '
' + - '교체/회수 대상 비율' + + '교체/회수 대상 비율' + '
' + (pcs.length > 0 ? Math.round(((underSpecCount + overSpecCount) / pcs.length) * 100) : 0) + '%
' + '
' + '
' + @@ -763,53 +972,119 @@ export function renderHwDashboard(container: HTMLElement) { '
' + '
' + '
' + - '

서버/공용PC 적정성 분석

' + - '
' + + '

용도별 서버 자원 과부족 현황

' + + '
' + '
' + '
' + - '

서버/스토리지 노후도 분포

' + - '
' + + '

서버 적정성 분석

' + + '
' + '
' + '
' + + '
' + - // 테이블 영역 (2열 레이아웃) - '
' + - '
' + - '

' + - '⚠️ 자원 과잉 장비 (TOP 5)' + + // ── SLIDE 4: ALL DETAILS EXECUTIVE CARDS ── + '
' + + '

📋 전사 PC 및 서버 상세 현황 (종합 상황판)

' + + + // 세로로 분할된 2열 구조 컨테이너 (140% 내용 확대) + '
' + + + // 왼쪽 열: PC 현황 (height: 100% 적용) + '
' + + '

' + + ' PC 현황 요약' + '

' + - '
' + - '' + - '' + - '' + buildServerStatusTableRows(overSpecList) + '' + - '
순위장비명서비스사양 요약사용 리소스 (CPU/RAM)일일 전송량점수상태
' + + + // 1행: PC KPI 카드 3개 가로 배치 + '
' + + '
' + + '
' + + '
' + + '평균 PC 점수' + + '
' + overallPcAvg + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '사양 부족(교체)' + + '
' + underSpecCount + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '오버스펙(회수)' + + '
' + overSpecCount + '
' + + '
' + + '
' + + '
' + + + // 2행: PC 그래프 2개 가로 배치 + '
' + + '
' + + '
직무별 평균 PC 사양 점수
' + + '
' + + '
' + + '
' + + '
가족사별 PC 사양 현황
' + + '
' + + '
' + '
' + '
' + - '
' + - '

' + - '🔻 자원 부족 장비 (TOP 5)' + + + // 오른쪽 열: 서버 현황 (height: 100% 적용) + '
' + + '

' + + ' 서버 및 인프라 현황 요약' + '

' + - '
' + - '' + - '' + - '' + buildServerStatusTableRows(underSpecList) + '' + - '
순위장비명서비스사양 요약사용 리소스 (CPU/RAM)일일 전송량점수상태
' + + + // 1행: 서버 KPI 카드 4개 가로 배치 + '
' + + '
' + + '
' + + '
' + + '총 서버 수량' + + '
' + servers.length + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '외부 서비스' + + '
' + Math.round((serverServiceGroups.external / servers.length) * 100) + '%
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '자원 과잉' + + '
' + serverStatusGroups.overSpec + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '자원 부족' + + '
' + (serverStatusGroups.underSpec + serverStatusGroups.inactive) + '
' + + '
' + + '
' + + '
' + + + // 2행: 서버 그래프 2개 가로 배치 + '
' + + '
' + + '
용도별 서버 자원 과부족 현황
' + + '
' + + '
' + + '
' + + '
서버 적정성 분석
' + + '
' + + '
' + '
' + '
' + '
' + - - // 방치 장비 목록 (Full-width) - '
' + - '

' + - '🔍 미사용 방치 의심 장비 (회수/철수 권장)' + - '

' + - '
' + - '' + - '' + - '' + buildServerStatusTableRows(inactiveList) + '' + - '
순위장비명서비스사양 요약사용 리소스 (CPU/RAM)일일 전송량점수상태
' + - '
' + - '
' + + '

' + '
' + @@ -825,7 +1100,16 @@ export function renderHwDashboard(container: HTMLElement) { createIcons({ icons: { DollarSign, Monitor, AlertTriangle, Activity, ChevronLeft, ChevronRight, UserCheck, TrendingUp, TrendingDown, Building2, X, FileText } }); } - initCharts(jobScores, recommendedScores, corpScores, serverAgeGroups, serverServiceGroups, serverStatusGroups); + initCharts( + jobScores, + recommendedScores, + corpScores, + serverAgeGroups, + serverServiceGroups, + serverStatusGroups, + purposeServerUnders, + purposeServerOvers + ); // 기획서 보기 버튼 클릭 이벤트 바인딩 const btnProposal = document.getElementById('btn-open-proposal'); @@ -867,10 +1151,10 @@ export function renderHwDashboard(container: HTMLElement) { const btnNext = document.getElementById('slider-next') as HTMLButtonElement; const indicator = document.getElementById('slider-indicator') as HTMLElement; let currentSlide = 0; - const totalSlides = 2; + const totalSlides = 4; const updateSlider = () => { - track.style.transform = 'translateX(-' + (currentSlide * 50) + '%)'; + track.style.transform = 'translateX(-' + (currentSlide * 25) + '%)'; btnPrev.disabled = currentSlide === 0; btnNext.disabled = currentSlide === totalSlides - 1; indicator.textContent = (currentSlide + 1) + ' / ' + totalSlides; @@ -878,6 +1162,22 @@ export function renderHwDashboard(container: HTMLElement) { if (btnPrev) btnPrev.addEventListener('click', () => { if (currentSlide > 0) { currentSlide--; updateSlider(); } }); if (btnNext) btnNext.addEventListener('click', () => { if (currentSlide < totalSlides - 1) { currentSlide++; updateSlider(); } }); + + // 4p KPI 카드 클릭 → 모달 연동 + const kpiUnder4p = document.getElementById('kpi-under-spec-4p'); + const kpiOver4p = document.getElementById('kpi-over-spec-4p'); + if (kpiUnder4p) kpiUnder4p.addEventListener('click', () => showSpecMismatchModal(criticalPcList, jobScores, allHw, '사양 부족')); + if (kpiOver4p) kpiOver4p.addEventListener('click', () => showSpecMismatchModal(criticalPcList, jobScores, allHw, '오버스펙')); + + const kpiSvrTotal4p = document.getElementById('kpi-server-total-4p'); + const kpiSvrExternal4p = document.getElementById('kpi-server-external-4p'); + const kpiSvrOverspec4p = document.getElementById('kpi-server-overspec-4p'); + const kpiSvrCritical4p = document.getElementById('kpi-server-critical-4p'); + + if (kpiSvrTotal4p) kpiSvrTotal4p.addEventListener('click', () => showServerStatusModal(servers, allHw, '전체 서버 및 공용 장비 목록')); + if (kpiSvrExternal4p) kpiSvrExternal4p.addEventListener('click', () => showServerStatusModal(servers.filter(s => s.service_type === '외부서비스'), allHw, '외부 운영 서비스 장비 목록')); + if (kpiSvrOverspec4p) kpiSvrOverspec4p.addEventListener('click', () => showServerStatusModal(servers.filter(s => s._server_status === '자원 과잉'), allHw, '자원 과잉 장비 목록')); + if (kpiSvrCritical4p) kpiSvrCritical4p.addEventListener('click', () => showServerStatusModal(servers.filter(s => s._server_status === '자원 부족' || s._server_status === '방치 의심'), allHw, '자원 부족 및 방치 의심 장비 목록')); }, 100); } @@ -889,7 +1189,9 @@ function initCharts( corpScores: any, ageGroups: any, serviceGroups: any, - statusGroups: any + statusGroups: any, + purposeServerUnders?: any, + purposeServerOvers?: any ) { // 직무별 점수 const jobCtx = document.getElementById('chart-job-scores') as HTMLCanvasElement; @@ -1048,4 +1350,308 @@ function initCharts( } }); } + + // ─── 종합 대시보드 차트 초기화 ─── + // 1. 가족사별 PC 사양 과부족 현황 (Grouped Bar Chart) + const totalPcMismatchCtx = document.getElementById('chart-total-pc-mismatch-by-corp') as HTMLCanvasElement; + if (totalPcMismatchCtx && typeof Chart !== 'undefined' && corpScores) { + if (totalPcMismatchByCorpChartInstance) { + totalPcMismatchByCorpChartInstance.destroy(); + totalPcMismatchByCorpChartInstance = null; + } + totalPcMismatchByCorpChartInstance = new Chart(totalPcMismatchCtx, { + type: 'bar', + data: { + labels: corpScores.labels, + datasets: [ + { + label: '사양 부족 (명)', + data: corpScores.unders, + backgroundColor: '#E11D48', + borderRadius: 4 + }, + { + label: '오버스펙 (명)', + data: corpScores.overs, + backgroundColor: '#F59E0B', + borderRadius: 4 + } + ] + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + position: 'bottom', + labels: { + padding: 10, + usePointStyle: true, + boxWidth: 10 + } + } + }, + scales: { + x: { + grid: { display: false }, + border: { display: false } + }, + y: { + beginAtZero: true, + ticks: { stepSize: 1 }, + grid: { color: '#F1F5F9' }, + border: { display: false }, + title: { + display: true, + text: '인원(명)', + color: '#94A3B8', + font: { size: 11 } + } + } + }, + animation: { + duration: 1200, + easing: 'easeOutQuart' + } + } + }); + } + + // 2. 용도별 서버 자원 과부족 현황 (Grouped Bar Chart) + const totalServerMismatchCtx = document.getElementById('chart-total-server-mismatch-by-purpose') as HTMLCanvasElement; + if (totalServerMismatchCtx && typeof Chart !== 'undefined' && purposeServerUnders && purposeServerOvers) { + if (totalServerMismatchByPurposeChartInstance) { + totalServerMismatchByPurposeChartInstance.destroy(); + totalServerMismatchByPurposeChartInstance = null; + } + totalServerMismatchByPurposeChartInstance = new Chart(totalServerMismatchCtx, { + type: 'bar', + data: { + labels: ['개발/테스트', '서비스/웹/WAS', 'DB/스토리지', '해석/분석/AI', '백업/관리/보안', '기타/일반'], + datasets: [ + { + label: '자원 부족 (대)', + data: purposeServerUnders, + backgroundColor: '#EF4444', + borderRadius: 4 + }, + { + label: '자원 과잉 (대)', + data: purposeServerOvers, + backgroundColor: '#F59E0B', + borderRadius: 4 + } + ] + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + position: 'bottom', + labels: { + padding: 10, + usePointStyle: true, + boxWidth: 10 + } + } + }, + scales: { + x: { + grid: { display: false }, + border: { display: false } + }, + y: { + beginAtZero: true, + ticks: { stepSize: 1 }, + grid: { color: '#F1F5F9' }, + border: { display: false }, + title: { + display: true, + text: '장비 수(대)', + color: '#94A3B8', + font: { size: 11 } + } + } + }, + animation: { + duration: 1200, + easing: 'easeOutQuart' + } + } + }); + } + + // ─── 4페이지(종합 카드판) 차트 초기화 ─── + // 1. 직무별 평균 PC 사양 점수 (4p) + const jobCtx4p = document.getElementById('chart-job-scores-4p') as HTMLCanvasElement; + if (jobCtx4p && typeof Chart !== 'undefined') { + const labels = Object.keys(jobScores).sort((a, b) => jobScores[b].avg - jobScores[a].avg); + const avgData = labels.map(l => Math.round(jobScores[l].avg)); + const recomData = labels.map(l => recommendedScores[l] || 0); + + if (jobChartInstance4p) { + jobChartInstance4p.destroy(); + jobChartInstance4p = null; + } + + jobChartInstance4p = new Chart(jobCtx4p, { + type: 'bar', + data: { + labels: labels, + datasets: [ + { + type: 'line', + label: '권장 목표', + data: recomData, + borderColor: '#EF4444', + borderWidth: 1.5, + borderDash: [3, 3], + fill: false, + pointBackgroundColor: '#EF4444', + pointRadius: 2, + order: 1 + }, + { + type: 'bar', + label: '평균 PC 점수', + data: avgData, + backgroundColor: '#6366F1', + borderRadius: 4, + order: 2 + } + ] + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + display: true, + position: 'top', + labels: { boxWidth: 8, usePointStyle: true, font: { size: 9 }, padding: 4 } + } + }, + scales: { + y: { beginAtZero: true, max: 100, ticks: { font: { size: 8.5 } }, grid: { color: '#F1F5F9' }, border: { display: false } }, + x: { ticks: { font: { size: 8.5 } }, grid: { display: false }, border: { display: false } } + }, + animation: { duration: 800, easing: 'easeOutQuart' } + } + }); + } + + // 2. 가족사별 PC 사양 현황 (4p) + const corpCtx4p = document.getElementById('chart-corp-scores-4p') as HTMLCanvasElement; + if (corpCtx4p && typeof Chart !== 'undefined') { + if (corpChartInstance4p) { + corpChartInstance4p.destroy(); + corpChartInstance4p = null; + } + corpChartInstance4p = new Chart(corpCtx4p, { + type: 'bar', + data: { + labels: corpScores.labels, + datasets: [ + { label: '부족', data: corpScores.unders, backgroundColor: '#E11D48', borderRadius: 3 }, + { label: '과잉', data: corpScores.overs, backgroundColor: '#F59E0B', borderRadius: 3 } + ] + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { position: 'bottom', labels: { padding: 4, usePointStyle: true, boxWidth: 6, font: { size: 9 } } } + }, + scales: { + y: { beginAtZero: true, ticks: { stepSize: 1, font: { size: 8.5 } }, grid: { color: '#F1F5F9' }, border: { display: false } }, + x: { ticks: { font: { size: 8.5 } }, grid: { display: false }, border: { display: false } } + }, + animation: { duration: 1000, easing: 'easeOutQuart' } + } + }); + } + + // 3. 용도별 서버 자원 과부족 현황 (4p) + const totalServerMismatchCtx4p = document.getElementById('chart-total-server-mismatch-by-purpose-4p') as HTMLCanvasElement; + if (totalServerMismatchCtx4p && typeof Chart !== 'undefined' && purposeServerUnders && purposeServerOvers) { + if (totalServerMismatchByPurposeChartInstance4p) { + totalServerMismatchByPurposeChartInstance4p.destroy(); + totalServerMismatchByPurposeChartInstance4p = null; + } + totalServerMismatchByPurposeChartInstance4p = new Chart(totalServerMismatchCtx4p, { + type: 'bar', + data: { + labels: ['개발/테스트', '웹/WAS', 'DB/스토리지', '해석/AI', '백업/보안', '기타'], + datasets: [ + { + label: '부족', + data: purposeServerUnders, + backgroundColor: '#EF4444', + borderRadius: 3 + }, + { + label: '과잉', + data: purposeServerOvers, + backgroundColor: '#F59E0B', + borderRadius: 3 + } + ] + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + position: 'bottom', + labels: { padding: 4, usePointStyle: true, boxWidth: 6, font: { size: 9 } } + } + }, + scales: { + x: { ticks: { font: { size: 8.5 } }, grid: { display: false }, border: { display: false } }, + y: { + beginAtZero: true, + ticks: { stepSize: 1, font: { size: 8.5 } }, + grid: { color: '#F1F5F9' }, + border: { display: false } + } + }, + animation: { duration: 1000, easing: 'easeOutQuart' } + } + }); + } + + + + // 5. 서버/공용PC 적정성 분석 (4p) + const statusCtx4p = document.getElementById('chart-server-status-4p') as HTMLCanvasElement; + if (statusCtx4p && typeof Chart !== 'undefined') { + if (serverStatusChartInstance4p) { + serverStatusChartInstance4p.destroy(); + serverStatusChartInstance4p = null; + } + serverStatusChartInstance4p = new Chart(statusCtx4p, { + type: 'bar', + data: { + labels: ['적정', '부족', '과잉', '방치'], + datasets: [{ + label: '수량', + data: [statusGroups.optimal, statusGroups.underSpec, statusGroups.overSpec, statusGroups.inactive], + backgroundColor: ['#10B981', '#EF4444', '#F59E0B', '#64748B'], + borderRadius: 4 + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { display: false } + }, + scales: { + y: { beginAtZero: true, ticks: { stepSize: 5, font: { size: 8.5 } }, grid: { color: '#F1F5F9' }, border: { display: false } }, + x: { ticks: { font: { size: 8.5 } }, grid: { display: false }, border: { display: false } } + } + } + }); + } }