From 3e69e74bc96791c4dabbbd26f9a1c088c9cd91cc Mon Sep 17 00:00:00 2001 From: JooWangi Date: Wed, 17 Jun 2026 09:22:31 +0900 Subject: [PATCH] =?UTF-8?q?Feat:=20=ED=86=B5=ED=95=A9=20=EC=82=AC=EC=96=91?= =?UTF-8?q?=20=EC=A0=81=EC=A0=95=EC=84=B1=20=EC=9D=B8=EB=9D=BC=EC=9D=B8=20?= =?UTF-8?q?=EB=B0=94=20=EA=B7=B8=EB=9E=98=ED=94=84=20=EB=B0=8F=20=EB=8C=80?= =?UTF-8?q?=EC=8B=9C=EB=B3=B4=EB=93=9C=20=EB=A0=88=EC=9D=B4=EC=95=84?= =?UTF-8?q?=EC=9B=83=20=EA=B0=9C=ED=8E=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/Dashboard/HwDashboard.ts | 576 ++++++++++++----------------- 1 file changed, 238 insertions(+), 338 deletions(-) diff --git a/src/views/Dashboard/HwDashboard.ts b/src/views/Dashboard/HwDashboard.ts index 2c91be7..72c2317 100644 --- a/src/views/Dashboard/HwDashboard.ts +++ b/src/views/Dashboard/HwDashboard.ts @@ -6,7 +6,6 @@ import { createIcons, Laptop, Cpu, Shield, Zap, Monitor, AlertTriangle, ChevronR declare var Chart: any; -let jobChartInstance: any = null; let donutChartInstance: any = null; export function renderHwDashboard(container: HTMLElement) { @@ -43,181 +42,149 @@ export function renderHwDashboard(container: HTMLElement) { - -
+ +
- -
+ +
- -
- - -
-
- 보유 자산 수량 -
-
-
-
0대
- 전사 보유 개인용 PC -
-
+ +
+
+ 보유 자산 수량
- - -
-
- 사양 부족 -
-
-
-
0대
- 사양 교체 권고 자산 -
-
+
+
0대
- - -
-
- 오버 스펙 -
-
-
-
0대
- 사양 회수 권고 자산 -
-
-
- - -
-
- 윈도우 11 불가 PC -
-
-
-
0대
- 업데이트 미지원 하드웨어 -
-
-
-
- - -
- -
- -
- 등급별 자산 종합 현황 -
- - -
- - - - - - - - - - - - - -
구분 (등급)보유량운영중재고구매 필요
-
+ +
+
+ 사양 부족 +
+
+
0대
-
- + + +
+
+ 오버 스펙 +
+
+
0대
+
+
+ + +
+
+ 윈도우 11 불가 PC +
+
+
0대
+
+
+
- -
+ +
- -
-
- 직무별 사양 적정성 분석 + +
+ +
+ 등급별 보유 비율
-
- + + +
+
+ +
+ +
+
+ + 최상급 +
+
+ + 상급 +
+
+ + 중급 +
+
+ + 보급 +
+
+ + 교체 대상 +
+
- - -
- - -
- -
- 등급별 보유 비율 -
- - -
-
- -
- -
-
- - 최상급 -
-
- - 상급 -
-
- - 중급 -
-
- - 보급 -
-
- - 교체 대상 -
-
-
-
- -
-
- 연도별 PC 노후도 및 예측 -
-
- - - - - - - - - - - -
구분 (연한)보유권장 조치
-
+ +
+
+ 연도별 PC 노후도 및 예측 +
+
+ + + + + + + + + + + +
구분 (연한)보유권장 조치
+ + +
+
+ +
+ 등급별 자산 종합 현황 및 사양 적정성 분석 +
+ + +
+ + + + + + + + + + + + + + +
구분 (등급)보유량운영중재고구매 필요사양 적정성 분석 (직무 기준)
+
+
+
+
`; @@ -402,21 +369,64 @@ function updateDashboardData(pcs: any[], selectedDept: string) { // 6. 종합 매트릭스 테이블 렌더링 및 바인딩 const matrixTbody = document.getElementById('pc-grade-matrix-tbody')!; + + const getSpecStatusCounts = (activePcsList: any[]) => { + let under = 0; + let normal = 0; + let over = 0; + activePcsList.forEach(p => { + if (p._spec_status === '사양 부족') under++; + else if (p._spec_status === '오버스펙') over++; + else normal++; + }); + return { under, normal, over }; + }; const renderMatrixRow = (gradeKey: keyof typeof matrix, label: string, color: string, shortage: number) => { const data = matrix[gradeKey]; const totalRate = filtered.length > 0 ? Math.round((data.total / filtered.length) * 100) : 0; - const cellStyle = `padding: 14px 12px; text-align: center; font-weight: 700; cursor: pointer; transition: background 0.2s; font-size: 1.25rem;`; + const cellStyle = `padding: 10px 8px; text-align: center; font-weight: 700; cursor: pointer; transition: background 0.2s; font-size: 1.1rem;`; const hoverEvents = `onmouseover="this.style.background='#F1F5F9'" onmouseout="this.style.background='none'"`; + // 사양 적정성 분석 데이터 계산 (운영중인 자산만) + const { under, normal, over } = getSpecStatusCounts(data.activePcs); + const activeCount = data.active; + + const underPct = activeCount > 0 ? (under / activeCount) * 100 : 0; + const normalPct = activeCount > 0 ? (normal / activeCount) * 100 : 0; + const overPct = activeCount > 0 ? (over / activeCount) * 100 : 0; + + let barGraphHtml = ''; + if (activeCount > 0) { + barGraphHtml = ` +
+
+ ${under > 0 ? `
` : ''} + ${normal > 0 ? `
` : ''} + ${over > 0 ? `
` : ''} +
+
+ ${under > 0 ? `부족 ${under}` : ''} + ${normal > 0 ? `적정 ${normal}` : ''} + ${over > 0 ? `오버 ${over}` : ''} +
+
+ `; + } else { + barGraphHtml = `운영중 자산 없음`; + } + return ` - ${label} - ${data.total}대 (${totalRate}%) + ${label} + ${data.total}대 (${totalRate}%) ${data.active}대 ${data.stock}대 ${shortage}대 + + ${barGraphHtml} + `; }; @@ -437,7 +447,33 @@ function updateDashboardData(pcs: any[], selectedDept: string) { const totalShortage = premiumShortage + highShortage + normalShortage + entryShortage + replaceShortage; - const cellStyleHeader = `padding: 14px 12px; text-align: center; font-weight: 800; cursor: pointer; transition: background 0.2s; background: #F8FAFC; font-size: 1.25rem;`; + const totalActivePcs = filtered.filter(p => !isStock(p)); + const { under: totUnder, normal: totNormal, over: totOver } = getSpecStatusCounts(totalActivePcs); + const totUnderPct = totalActive > 0 ? (totUnder / totalActive) * 100 : 0; + const totNormalPct = totalActive > 0 ? (totNormal / totalActive) * 100 : 0; + const totOverPct = totalActive > 0 ? (totOver / totalActive) * 100 : 0; + + let totBarGraphHtml = ''; + if (totalActive > 0) { + totBarGraphHtml = ` +
+
+ ${totUnder > 0 ? `
` : ''} + ${totNormal > 0 ? `
` : ''} + ${totOver > 0 ? `
` : ''} +
+
+ ${totUnder > 0 ? `부족 ${totUnder}` : ''} + ${totNormal > 0 ? `적정 ${totNormal}` : ''} + ${totOver > 0 ? `오버 ${totOver}` : ''} +
+
+ `; + } else { + totBarGraphHtml = `운영중 자산 없음`; + } + + const cellStyleHeader = `padding: 10px 8px; text-align: center; font-weight: 800; cursor: pointer; transition: background 0.2s; background: #F8FAFC; font-size: 1.1rem;`; const hoverEventsHeader = `onmouseover="this.style.background='#EEF2F6'" onmouseout="this.style.background='#F8FAFC'"`; matrixTbody.innerHTML = ` @@ -447,11 +483,14 @@ function updateDashboardData(pcs: any[], selectedDept: string) { ${renderMatrixRow('entry', '보급 PC (20점 ~ 40점)', '#F59E0B', entryShortage)} ${renderMatrixRow('replace', '교체 대상 PC (20점 미만 또는 Win11 불가)', '#EF4444', replaceShortage)} - 합계 (Total) - ${totalPcs}대 (100%) + 합계 (Total) + ${totalPcs}대 (100%) ${totalActive}대 ${totalStock}대 ${totalShortage}대 + + ${totBarGraphHtml} + `; @@ -509,6 +548,38 @@ function updateDashboardData(pcs: any[], selectedDept: string) { }); }); + // 바그래프 세그먼트 또는 텍스트 클릭 리스너 설정 + const handleSpecClick = (e: Event) => { + e.stopPropagation(); + const target = e.currentTarget as HTMLElement; + const grade = target.getAttribute('data-grade')!; + const status = target.getAttribute('data-spec-status')!; + + let targetPcs: any[] = []; + if (grade === 'all') { + targetPcs = filtered.filter(p => !isStock(p) && p._spec_status === status); + } else { + const data = matrix[grade as keyof typeof matrix]; + targetPcs = data.activePcs.filter(p => p._spec_status === status); + } + + const getGradeLabel = (g: string) => { + if (g === 'premium') return '최상급 PC'; + if (g === 'high') return '상급 PC'; + if (g === 'normal') return '중급 PC'; + if (g === 'entry') return '보급 PC'; + if (g === 'replace') return '교체 대상 PC'; + return '전체 PC'; + }; + + const title = `${getGradeLabel(grade)} - ${status} 자산 목록`; + showMiniListModal(title, targetPcs); + }; + + matrixTbody.querySelectorAll('.spec-segment-btn, .spec-text-btn').forEach(btn => { + btn.addEventListener('click', handleSpecClick); + }); + // 7. 연도별 PC 노후도 집계 및 렌더링 const agingCounts = { immediate: [] as any[], // 7년 이상 @@ -535,10 +606,10 @@ function updateDashboardData(pcs: any[], selectedDept: string) { const renderAgingRow = (label: string, list: any[], badgeText: string, badgeStyle: string, ageGroupKey: string) => { return ` - ${label} - ${list.length}대 - - ${badgeText} + ${label} + ${list.length}대 + + ${badgeText} `; @@ -586,50 +657,11 @@ function updateDashboardData(pcs: any[], selectedDept: string) { bindCardClick('card-over-spec', '오버 스펙 대상', p => p._spec_status === '오버스펙'); bindCardClick('card-win11-incompatible', '윈도우 11 업그레이드 불가 PC', p => isWindows11Incompatible(p.cpu, p.ram)); - // 9. 직무별 사양 적정성 대수 연산 및 차트 데이터 셋 구성 (누적 막대 그래프화) - const activeJobs = Array.from( - new Set(filtered.map((p: any) => p[ASSET_SCHEMA.USER_POSITION.key] || '미분류').filter(j => j !== '재고PC')) - ).sort(); - - const underData: number[] = []; - const normalData: number[] = []; - const overData: number[] = []; - - activeJobs.forEach(job => { - const jobPcs = filtered.filter((p: any) => (p[ASSET_SCHEMA.USER_POSITION.key] || '미분류') === job); - const totalCount = jobPcs.length; - if (totalCount === 0) { - underData.push(0); - normalData.push(0); - overData.push(0); - return; - } - let under = 0; - let normal = 0; - let over = 0; - - jobPcs.forEach(p => { - const stockYn = isStock(p); - if (!stockYn) { - if (p._spec_status === '사양 부족') { under++; } - else if (p._spec_status === '오버스펙') { over++; } - else { normal++; } - } else { - normal++; // 예외 폴백 - } - }); - - underData.push(under); - normalData.push(normal); - overData.push(over); - }); - - // 10. 차트들 렌더링 호출 - renderChart(activeJobs, underData, normalData, overData, filtered); + // 10. 도넛 차트 렌더링 호출 renderDonutChart(matrix.premium.total, matrix.high.total, matrix.normal.total, matrix.entry.total, matrix.replace.total); // 전역 상태 등록 - state.activeCharts = [jobChartInstance, donutChartInstance]; + state.activeCharts = [donutChartInstance]; } /** @@ -746,139 +778,7 @@ function showMiniListModal(title: string, list: any[]) { }); } -/** - * Chart.js 가로형 100% 스택 막대 차트 (라이트 테마 튜닝) - */ -function renderChart(labels: string[], underData: number[], normalData: number[], overData: number[], currentFiltered: any[]) { - const ctx = document.getElementById('chart-job-scores') as HTMLCanvasElement; - if (!ctx || typeof Chart === 'undefined') return; - if (jobChartInstance) { - jobChartInstance.destroy(); - jobChartInstance = null; - } - - jobChartInstance = new Chart(ctx, { - type: 'bar', - data: { - labels: labels, - datasets: [ - { - label: '사양 부족', - data: underData, - backgroundColor: 'rgba(239, 68, 68, 0.85)', // Rose Red - borderColor: 'rgb(239, 68, 68)', - borderWidth: 1, - borderRadius: 4, - barPercentage: 0.45, - categoryPercentage: 0.8 - }, - { - label: '적정 사양', - data: normalData, - backgroundColor: 'rgba(30, 81, 73, 0.85)', // Hanmac Green - borderColor: 'rgb(30, 81, 73)', - borderWidth: 1, - borderRadius: 4, - barPercentage: 0.45, - categoryPercentage: 0.8 - }, - { - label: '오버 스펙', - data: overData, - backgroundColor: 'rgba(217, 119, 6, 0.85)', // Amber Orange - borderColor: 'rgb(217, 119, 6)', - borderWidth: 1, - borderRadius: 4, - barPercentage: 0.45, - categoryPercentage: 0.8 - } - ] - }, - options: { - indexAxis: 'y', - responsive: true, - maintainAspectRatio: false, - onHover: (event: any, activeElements: any[]) => { - event.chart.canvas.style.cursor = activeElements.length ? 'pointer' : 'default'; - }, - onClick: (event: any, activeElements: any[]) => { - if (activeElements && activeElements.length > 0) { - const activeElement = activeElements[0]; - const datasetIndex = activeElement.datasetIndex; // 0: 사양 부족, 1: 적정 사양, 2: 오버스펙 - const index = activeElement.index; // 직무군 인덱스 - - const clickedJob = labels[index]; - const statusLabels = ['사양 부족', '적정', '오버스펙']; - const clickedStatus = statusLabels[datasetIndex] || '적정'; - - // 해당 직무군과 사양 상태가 매칭되는 자산 목록 필터링 - const matchedPcs = currentFiltered.filter((p: any) => { - const job = p[ASSET_SCHEMA.USER_POSITION.key] || '미분류'; - if (job !== clickedJob) return false; - - const stockYn = p.hw_status === '재고' || - p.hw_status === '대기' || - !(p.user_current || '').trim(); - - let specStatus = '적정'; - if (!stockYn) { - specStatus = p._spec_status || '적정'; - } - return specStatus === clickedStatus; - }); - - showMiniListModal(`${clickedJob} - ${clickedStatus === '적정' ? '적정 사양' : (clickedStatus === '오버스펙' ? '오버 스펙' : clickedStatus)} 자산`, matchedPcs); - } - }, - plugins: { - legend: { - position: 'top', - align: 'end', - labels: { - font: { family: 'Pretendard', size: 16, weight: '700' }, - color: '#475569', - boxWidth: 12, - boxHeight: 12, - usePointStyle: true - } - }, - tooltip: { - titleFont: { family: 'Pretendard', size: 12, weight: '700' }, - bodyFont: { family: 'Pretendard', size: 12 }, - callbacks: { - label: function (context: any) { - const datasetLabel = context.dataset.label; - const value = context.raw; // 실제 대수 - const total = context.chart.data.datasets.reduce((sum: number, dataset: any) => sum + dataset.data[context.dataIndex], 0); - const percentage = total > 0 ? Math.round((value / total) * 100) : 0; - return `${datasetLabel}: ${value}대 (${percentage}%)`; - } - } - } - }, - scales: { - x: { - stacked: true, - ticks: { - callback: (val: any) => `${val}대`, - font: { family: 'Pretendard', size: 14, weight: '600' }, - color: '#64748B' - }, - grid: { color: '#EEF2F6' } - }, - y: { - stacked: true, - ticks: { - font: { family: 'Pretendard', size: 16, weight: '700' }, - color: '#475569' - }, - grid: { display: false } - } - } - } - }); -} /** * 실시간 사양 적정률 원형 도넛 그래프 (Active Spec Rate)