feat(dashboard): update charts and styling on HW_Dashboard
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
'</div>' +
|
||||
'<div class="slider-controls">' +
|
||||
'<button id="slider-prev" class="slider-nav-btn" disabled><i data-lucide="chevron-left"></i></button>' +
|
||||
'<span id="slider-indicator" class="slider-indicator">1 / 2</span>' +
|
||||
'<span id="slider-indicator" class="slider-indicator">1 / 4</span>' +
|
||||
'<button id="slider-next" class="slider-nav-btn"><i data-lucide="chevron-right"></i></button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
@@ -668,7 +832,52 @@ export function renderHwDashboard(container: HTMLElement) {
|
||||
'<div class="dashboard-slider-viewport">' +
|
||||
'<div class="dashboard-slider-track" id="dashboard-slider-track">' +
|
||||
|
||||
// ── SLIDE 1: PC DASHBOARD ──
|
||||
// ── SLIDE 1: TOTAL EXECUTIVE DASHBOARD ──
|
||||
'<div class="dashboard-slide">' +
|
||||
'<h3 class="dashboard-section-title" style="color:#0F172A; border-bottom:2px solid #E2E8F0; display:inline-block; margin-bottom:1.5rem;">📊 전사 IT 자산 및 자원 최적화 요약</h3>' +
|
||||
|
||||
// KPI Row
|
||||
'<div class="dashboard-grid" style="grid-template-columns: repeat(4, 1fr);">' +
|
||||
'<div class="stat-card">' +
|
||||
'<div class="stat-icon icon-red"><i data-lucide="monitor"></i></div>' +
|
||||
'<span class="stat-label">PC 사양 부족 장비</span>' +
|
||||
'<div class="stat-value stat-value-danger" style="font-size:1.8rem;">' + underSpecCount + '<span style="font-size:1rem; font-weight:600; color:#64748B;">대</span></div>' +
|
||||
'<div style="font-size: 0.8125rem; color:var(--text-muted); margin-top: 0.5rem;">개인용 PC 대비 ' + (pcs.length > 0 ? Math.round((underSpecCount / pcs.length) * 100) : 0) + '% 비율</div>' +
|
||||
'</div>' +
|
||||
'<div class="stat-card">' +
|
||||
'<div class="stat-icon icon-yellow"><i data-lucide="monitor"></i></div>' +
|
||||
'<span class="stat-label">PC 오버스펙 장비</span>' +
|
||||
'<div class="stat-value" style="color:#F59E0B; font-size:1.8rem;">' + overSpecCount + '<span style="font-size:1rem; font-weight:600; color:#64748B;">대</span></div>' +
|
||||
'<div style="font-size: 0.8125rem; color:var(--text-muted); margin-top: 0.5rem;">개인용 PC 대비 ' + (pcs.length > 0 ? Math.round((overSpecCount / pcs.length) * 100) : 0) + '% 비율</div>' +
|
||||
'</div>' +
|
||||
'<div class="stat-card">' +
|
||||
'<div class="stat-icon icon-red"><i data-lucide="activity"></i></div>' +
|
||||
'<span class="stat-label">서버 자원 부족 장비</span>' +
|
||||
'<div class="stat-value stat-value-danger" style="font-size:1.8rem;">' + serverStatusGroups.underSpec + '<span style="font-size:1rem; font-weight:600; color:#64748B;">대</span></div>' +
|
||||
'<div style="font-size: 0.8125rem; color:var(--text-muted); margin-top: 0.5rem;">서버/NAS 대비 ' + (servers.length > 0 ? Math.round((serverStatusGroups.underSpec / servers.length) * 100) : 0) + '% 비율</div>' +
|
||||
'</div>' +
|
||||
'<div class="stat-card">' +
|
||||
'<div class="stat-icon icon-yellow"><i data-lucide="activity"></i></div>' +
|
||||
'<span class="stat-label">서버 자원 과잉 장비</span>' +
|
||||
'<div class="stat-value" style="color:#D97706; font-size:1.8rem;">' + serverStatusGroups.overSpec + '<span style="font-size:1rem; font-weight:600; color:#64748B;">대</span></div>' +
|
||||
'<div style="font-size: 0.8125rem; color:var(--text-muted); margin-top: 0.5rem;">서버/NAS 대비 ' + (servers.length > 0 ? Math.round((serverStatusGroups.overSpec / servers.length) * 100) : 0) + '% 비율</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
|
||||
// Charts
|
||||
'<div class="dashboard-layout-2col" style="margin-bottom: 2rem;">' +
|
||||
'<div class="dashboard-card">' +
|
||||
'<h4 class="dashboard-section-title" style="color:#EF4444;">가족사별 PC 사양 과부족 현황</h4>' +
|
||||
'<div style="height:280px;display:flex;align-items:center;justify-content:center;"><canvas id="chart-total-pc-mismatch-by-corp"></canvas></div>' +
|
||||
'</div>' +
|
||||
'<div class="dashboard-card">' +
|
||||
'<h4 class="dashboard-section-title">용도별 서버 자원 과부족 현황</h4>' +
|
||||
'<div style="height:280px;display:flex;align-items:center;justify-content:center;"><canvas id="chart-total-server-mismatch-by-purpose"></canvas></div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
|
||||
// ── SLIDE 2: PC DASHBOARD ──
|
||||
'<div class="dashboard-slide">' +
|
||||
'<h3 class="dashboard-section-title" style="color:#0F172A; border-bottom:2px solid #E2E8F0; display:inline-block; margin-bottom:1.5rem;">💻 PC 사양 적정성 분석</h3>' +
|
||||
|
||||
@@ -692,9 +901,9 @@ export function renderHwDashboard(container: HTMLElement) {
|
||||
'<div class="stat-value" style="color:#F59E0B;">' + overSpecCount + '<span style="font-size:1rem; font-weight:600; color:#64748B;">명</span></div>' +
|
||||
'<div style="font-size: 0.8125rem; color:var(--text-muted); margin-top: 0.5rem;">직무 평균 대비 30% 이상 초과 ▸ 클릭하여 상세보기</div>' +
|
||||
'</div>' +
|
||||
'<div class="stat-card">' +
|
||||
'<div class="stat-card" style="border: 1px solid rgba(239,68,68,0.3); background: rgba(254,226,226,0.15);">' +
|
||||
'<div class="stat-icon" style="background:rgba(239,68,68,0.1);color:#EF4444;"><i data-lucide="alert-triangle"></i></div>' +
|
||||
'<span class="stat-label">교체/회수 대상 비율</span>' +
|
||||
'<span class="stat-label" style="color:#EF4444;">교체/회수 대상 비율</span>' +
|
||||
'<div class="stat-value stat-value-danger" style="font-size:1.8rem;">' + (pcs.length > 0 ? Math.round(((underSpecCount + overSpecCount) / pcs.length) * 100) : 0) + '<span style="font-size:1rem; font-weight:600; color:#64748B;">%</span></div>' +
|
||||
'<div style="width:100%;height:4px;background:#E2E8F0;border-radius:2px;overflow:hidden;margin-top:1rem;">' +
|
||||
'<div style="width:' + (pcs.length > 0 ? Math.round(((underSpecCount + overSpecCount) / pcs.length) * 100) : 0) + '%;height:100%;background:linear-gradient(90deg,#F59E0B,#E11D48);"></div>' +
|
||||
@@ -763,51 +972,117 @@ export function renderHwDashboard(container: HTMLElement) {
|
||||
'<div><canvas id="chart-server-service"></canvas></div>' +
|
||||
'</div>' +
|
||||
'<div class="dashboard-card">' +
|
||||
'<h4 class="dashboard-section-title">서버/공용PC 적정성 분석</h4>' +
|
||||
'<div><canvas id="chart-server-status"></canvas></div>' +
|
||||
'<h4 class="dashboard-section-title">용도별 서버 자원 과부족 현황</h4>' +
|
||||
'<div><canvas id="chart-total-server-mismatch-by-purpose"></canvas></div>' +
|
||||
'</div>' +
|
||||
'<div class="dashboard-card">' +
|
||||
'<h4 class="dashboard-section-title">서버/스토리지 노후도 분포</h4>' +
|
||||
'<div><canvas id="chart-server-aging"></canvas></div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
|
||||
// 테이블 영역 (2열 레이아웃)
|
||||
'<div style="display:grid; grid-template-columns:1fr 1fr; gap:1.5rem; margin-bottom:2rem;">' +
|
||||
'<div class="dashboard-card" style="padding:1.25rem 1.5rem;">' +
|
||||
'<h4 class="dashboard-section-title" style="display:flex; align-items:center; gap:0.5rem; color:#D97706;">' +
|
||||
'⚠️ 자원 과잉 장비 (TOP 5)' +
|
||||
'</h4>' +
|
||||
'<div class="table-premium">' +
|
||||
'<table>' +
|
||||
'<thead><tr><th>순위</th><th>장비명</th><th>서비스</th><th>사양 요약</th><th>사용 리소스 (CPU/RAM)</th><th>일일 전송량</th><th>점수</th><th>상태</th></tr></thead>' +
|
||||
'<tbody>' + buildServerStatusTableRows(overSpecList) + '</tbody>' +
|
||||
'</table>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="dashboard-card" style="padding:1.25rem 1.5rem;">' +
|
||||
'<h4 class="dashboard-section-title" style="display:flex; align-items:center; gap:0.5rem; color:#EF4444;">' +
|
||||
'🔻 자원 부족 장비 (TOP 5)' +
|
||||
'</h4>' +
|
||||
'<div class="table-premium">' +
|
||||
'<table>' +
|
||||
'<thead><tr><th>순위</th><th>장비명</th><th>서비스</th><th>사양 요약</th><th>사용 리소스 (CPU/RAM)</th><th>일일 전송량</th><th>점수</th><th>상태</th></tr></thead>' +
|
||||
'<tbody>' + buildServerStatusTableRows(underSpecList) + '</tbody>' +
|
||||
'</table>' +
|
||||
'<h4 class="dashboard-section-title">서버 적정성 분석</h4>' +
|
||||
'<div><canvas id="chart-server-status"></canvas></div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
|
||||
// 방치 장비 목록 (Full-width)
|
||||
'<div class="dashboard-card" style="padding:1.25rem 1.5rem; margin-bottom:2rem;">' +
|
||||
'<h4 class="dashboard-section-title" style="display:flex; align-items:center; gap:0.5rem; color:#475569;">' +
|
||||
'🔍 미사용 방치 의심 장비 (회수/철수 권장)' +
|
||||
// ── SLIDE 4: ALL DETAILS EXECUTIVE CARDS ──
|
||||
'<div class="dashboard-slide">' +
|
||||
'<h3 class="dashboard-section-title" style="color:#0F172A; border-bottom:2px solid #E2E8F0; display:inline-block; margin-bottom:0.5rem; font-size:1.3rem; padding-bottom:0.25rem;">📋 전사 PC 및 서버 상세 현황 (종합 상황판)</h3>' +
|
||||
|
||||
// 세로로 분할된 2열 구조 컨테이너 (140% 내용 확대)
|
||||
'<div style="display: grid; grid-template-columns: 1fr 1.2fr; gap: 0.75rem; flex: 1; min-height: 0; zoom: 1.4;">' +
|
||||
|
||||
// 왼쪽 열: PC 현황 (height: 100% 적용)
|
||||
'<div style="display: flex; flex-direction: column; gap: 0.5rem; background: rgba(255,255,255,0.45); border-radius: 12px; padding: 0.5rem; border: 1px solid rgba(99,102,241,0.12); box-sizing: border-box; min-height: 0; height: 100%;">' +
|
||||
'<h4 style="font-size: 0.9rem; font-weight: 800; color: #1E293B; margin: 0 0 2px 0; display: flex; align-items: center; gap: 0.25rem;">' +
|
||||
'<i data-lucide="monitor" style="width:15px; height:15px; color:#3B82F6;"></i> PC 현황 요약' +
|
||||
'</h4>' +
|
||||
'<div class="table-premium">' +
|
||||
'<table>' +
|
||||
'<thead><tr><th>순위</th><th>장비명</th><th>서비스</th><th>사양 요약</th><th>사용 리소스 (CPU/RAM)</th><th>일일 전송량</th><th>점수</th><th>상태</th></tr></thead>' +
|
||||
'<tbody>' + buildServerStatusTableRows(inactiveList) + '</tbody>' +
|
||||
'</table>' +
|
||||
|
||||
// 1행: PC KPI 카드 3개 가로 배치
|
||||
'<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.4rem; margin-bottom: 0.2rem;">' +
|
||||
'<div class="stat-card" style="padding: 0.55rem 0.65rem; min-height: unset; border-radius: 8px; box-shadow: 0 1px 4px rgba(0,0,0,0.02); background: rgba(255, 255, 255, 0.85); display: flex; align-items: center; gap: 0.45rem; border: 1px solid rgba(99,102,241,0.05);">' +
|
||||
'<div style="background: rgba(59,130,246,0.1); color: #3B82F6; padding: 0.35rem; border-radius: 6px; display: flex; align-items: center; justify-content: center; flex-shrink: 0;"><i data-lucide="monitor" style="width: 15px; height: 15px;"></i></div>' +
|
||||
'<div style="display: flex; flex-direction: column; min-width: 0;">' +
|
||||
'<span class="stat-label" style="font-size: 0.68rem; color: #64748B; font-weight: 600; margin-bottom: 1px; letter-spacing: 0;">평균 PC 점수</span>' +
|
||||
'<div class="stat-value" style="font-size: 1.15rem; font-weight: 800; color: #1E293B; margin-top: 0; line-height: 1.2; display: flex; align-items: baseline; background: none; -webkit-text-fill-color: initial;">' + overallPcAvg + '<span style="font-size:0.75rem; font-weight:600; color:#64748B; margin-left: 2px;">점</span></div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="stat-card" id="kpi-under-spec-4p" style="cursor:pointer; padding: 0.55rem 0.65rem; min-height: unset; border-radius: 8px; box-shadow: 0 1px 4px rgba(0,0,0,0.02); background: rgba(255, 255, 255, 0.85); display: flex; align-items: center; gap: 0.45rem; border: 1px solid rgba(239,68,68,0.1);">' +
|
||||
'<div style="background: rgba(239,68,68,0.1); color: #EF4444; padding: 0.35rem; border-radius: 6px; display: flex; align-items: center; justify-content: center; flex-shrink: 0;"><i data-lucide="trending-down" style="width: 15px; height: 15px;"></i></div>' +
|
||||
'<div style="display: flex; flex-direction: column; min-width: 0;">' +
|
||||
'<span class="stat-label" style="font-size: 0.68rem; color: #EF4444; font-weight: 600; margin-bottom: 1px; letter-spacing: 0;">사양 부족(교체)</span>' +
|
||||
'<div class="stat-value" style="font-size: 1.15rem; font-weight: 800; color: #EF4444; margin-top: 0; line-height: 1.2; display: flex; align-items: baseline; background: none; -webkit-text-fill-color: initial;">' + underSpecCount + '<span style="font-size:0.75rem; font-weight:600; color:#EF4444; margin-left: 2px;">명</span></div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="stat-card" id="kpi-over-spec-4p" style="cursor:pointer; padding: 0.55rem 0.65rem; min-height: unset; border-radius: 8px; box-shadow: 0 1px 4px rgba(0,0,0,0.02); background: rgba(255, 255, 255, 0.85); display: flex; align-items: center; gap: 0.45rem; border: 1px solid rgba(245,158,11,0.1);">' +
|
||||
'<div style="background: rgba(245,158,11,0.1); color: #F59E0B; padding: 0.35rem; border-radius: 6px; display: flex; align-items: center; justify-content: center; flex-shrink: 0;"><i data-lucide="trending-up" style="width: 15px; height: 15px;"></i></div>' +
|
||||
'<div style="display: flex; flex-direction: column; min-width: 0;">' +
|
||||
'<span class="stat-label" style="font-size: 0.68rem; color: #F59E0B; font-weight: 600; margin-bottom: 1px; letter-spacing: 0;">오버스펙(회수)</span>' +
|
||||
'<div class="stat-value" style="font-size: 1.15rem; font-weight: 800; color: #F59E0B; margin-top: 0; line-height: 1.2; display: flex; align-items: baseline; background: none; -webkit-text-fill-color: initial;">' + overSpecCount + '<span style="font-size:0.75rem; font-weight:600; color:#64748B; margin-left: 2px;">명</span></div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
|
||||
// 2행: PC 그래프 2개 가로 배치
|
||||
'<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 0.4rem; flex: 1; min-height: 0;">' +
|
||||
'<div class="dashboard-card" style="min-height: unset; height: 100%; padding: 0.4rem 0.6rem; border-radius: 8px; display:flex; flex-direction:column; background: rgba(255, 255, 255, 0.85); margin-bottom: 0;">' +
|
||||
'<h5 style="font-size: 0.75rem; font-weight: 800; margin: 0 0 2px 0; color: #475569;">직무별 평균 PC 사양 점수</h5>' +
|
||||
'<div style="flex: 1; min-height: 0; position: relative;"><canvas id="chart-job-scores-4p" style="position: absolute; top:0; left:0; width:100%; height:100%;"></canvas></div>' +
|
||||
'</div>' +
|
||||
'<div class="dashboard-card" style="min-height: unset; height: 100%; padding: 0.4rem 0.6rem; border-radius: 8px; display:flex; flex-direction:column; background: rgba(255, 255, 255, 0.85); margin-bottom: 0;">' +
|
||||
'<h5 style="font-size: 0.75rem; font-weight: 800; margin: 0 0 2px 0; color: #475569;">가족사별 PC 사양 현황</h5>' +
|
||||
'<div style="flex: 1; min-height: 0; position: relative;"><canvas id="chart-corp-scores-4p" style="position: absolute; top:0; left:0; width:100%; height:100%;"></canvas></div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
|
||||
// 오른쪽 열: 서버 현황 (height: 100% 적용)
|
||||
'<div style="display: flex; flex-direction: column; gap: 0.5rem; background: rgba(255,255,255,0.45); border-radius: 12px; padding: 0.5rem; border: 1px solid rgba(16,185,129,0.12); box-sizing: border-box; min-height: 0; height: 100%;">' +
|
||||
'<h4 style="font-size: 0.9rem; font-weight: 800; color: #1E293B; margin: 0 0 2px 0; display: flex; align-items: center; gap: 0.25rem;">' +
|
||||
'<i data-lucide="activity" style="width:15px; height:15px; color:#10B981;"></i> 서버 및 인프라 현황 요약' +
|
||||
'</h4>' +
|
||||
|
||||
// 1행: 서버 KPI 카드 4개 가로 배치
|
||||
'<div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 0.4rem; margin-bottom: 0.2rem;">' +
|
||||
'<div class="stat-card" id="kpi-server-total-4p" style="cursor:pointer; padding: 0.55rem 0.6rem; min-height: unset; border-radius: 8px; box-shadow: 0 1px 4px rgba(0,0,0,0.02); background: rgba(255, 255, 255, 0.85); display: flex; align-items: center; gap: 0.4rem; border: 1px solid rgba(16,185,129,0.05);">' +
|
||||
'<div style="background: rgba(59,130,246,0.1); color: #3B82F6; padding: 0.3rem; border-radius: 6px; display: flex; align-items: center; justify-content: center; flex-shrink: 0;"><i data-lucide="monitor" style="width: 14px; height: 14px;"></i></div>' +
|
||||
'<div style="display: flex; flex-direction: column; min-width: 0;">' +
|
||||
'<span class="stat-label" style="font-size: 0.65rem; color: #64748B; font-weight: 600; margin-bottom: 1px; letter-spacing: 0;">총 서버 수량</span>' +
|
||||
'<div class="stat-value" style="font-size: 1.1rem; font-weight: 800; color: #1E293B; margin-top: 0; line-height: 1.2; display: flex; align-items: baseline; background: none; -webkit-text-fill-color: initial;">' + servers.length + '<span style="font-size:0.7rem; font-weight:600; color:#64748B; margin-left: 2px;">대</span></div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="stat-card" id="kpi-server-external-4p" style="cursor:pointer; padding: 0.55rem 0.6rem; min-height: unset; border-radius: 8px; box-shadow: 0 1px 4px rgba(0,0,0,0.02); background: rgba(255, 255, 255, 0.85); display: flex; align-items: center; gap: 0.4rem; border: 1px solid rgba(16,185,129,0.05);">' +
|
||||
'<div style="background: rgba(16,185,129,0.1); color: #10B981; padding: 0.3rem; border-radius: 6px; display: flex; align-items: center; justify-content: center; flex-shrink: 0;"><i data-lucide="activity" style="width: 14px; height: 14px;"></i></div>' +
|
||||
'<div style="display: flex; flex-direction: column; min-width: 0;">' +
|
||||
'<span class="stat-label" style="font-size: 0.65rem; color: #10B981; font-weight: 600; margin-bottom: 1px; letter-spacing: 0;">외부 서비스</span>' +
|
||||
'<div class="stat-value" style="font-size: 1.1rem; font-weight: 800; color: #10B981; margin-top: 0; line-height: 1.2; display: flex; align-items: baseline; background: none; -webkit-text-fill-color: initial;">' + Math.round((serverServiceGroups.external / servers.length) * 100) + '<span style="font-size:0.7rem; font-weight:600; color:#64748B; margin-left: 2px;">%</span></div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="stat-card" id="kpi-server-overspec-4p" style="cursor:pointer; padding: 0.55rem 0.6rem; min-height: unset; border-radius: 8px; box-shadow: 0 1px 4px rgba(0,0,0,0.02); background: rgba(255, 255, 255, 0.85); display: flex; align-items: center; gap: 0.4rem; border: 1px solid rgba(245,158,11,0.1);">' +
|
||||
'<div style="background: rgba(245,158,11,0.1); color: #F59E0B; padding: 0.35rem; border-radius: 6px; display: flex; align-items: center; justify-content: center; flex-shrink: 0;"><i data-lucide="trending-up" style="width: 14px; height: 14px;"></i></div>' +
|
||||
'<div style="display: flex; flex-direction: column; min-width: 0;">' +
|
||||
'<span class="stat-label" style="font-size: 0.65rem; color: #F59E0B; font-weight: 600; margin-bottom: 1px; letter-spacing: 0;">자원 과잉</span>' +
|
||||
'<div class="stat-value" style="font-size: 1.1rem; font-weight: 800; color: #F59E0B; margin-top: 0; line-height: 1.2; display: flex; align-items: baseline; background: none; -webkit-text-fill-color: initial;">' + serverStatusGroups.overSpec + '<span style="font-size:0.7rem; font-weight:600; color:#64748B; margin-left: 2px;">대</span></div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="stat-card" id="kpi-server-critical-4p" style="cursor:pointer; padding: 0.55rem 0.6rem; min-height: unset; border-radius: 8px; box-shadow: 0 1px 4px rgba(0,0,0,0.02); background: rgba(255, 255, 255, 0.85); display: flex; align-items: center; gap: 0.4rem; border: 1px solid rgba(239,68,68,0.1);">' +
|
||||
'<div style="background: rgba(239,68,68,0.1); color: #EF4444; padding: 0.35rem; border-radius: 6px; display: flex; align-items: center; justify-content: center; flex-shrink: 0;"><i data-lucide="trending-down" style="width: 14px; height: 14px;"></i></div>' +
|
||||
'<div style="display: flex; flex-direction: column; min-width: 0;">' +
|
||||
'<span class="stat-label" style="font-size: 0.65rem; color: #EF4444; font-weight: 600; margin-bottom: 1px; letter-spacing: 0;">자원 부족</span>' +
|
||||
'<div class="stat-value" style="font-size: 1.1rem; font-weight: 800; color: #EF4444; margin-top: 0; line-height: 1.2; display: flex; align-items: baseline; background: none; -webkit-text-fill-color: initial;">' + (serverStatusGroups.underSpec + serverStatusGroups.inactive) + '<span style="font-size:0.7rem; font-weight:600; color:#EF4444; margin-left: 2px;">대</span></div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
|
||||
// 2행: 서버 그래프 2개 가로 배치
|
||||
'<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 0.4rem; flex: 1; min-height: 0;">' +
|
||||
'<div class="dashboard-card" style="min-height: unset; height: 100%; padding: 0.4rem 0.6rem; border-radius: 8px; display:flex; flex-direction:column; background: rgba(255, 255, 255, 0.85); margin-bottom: 0;">' +
|
||||
'<h5 style="font-size: 0.75rem; font-weight: 800; margin: 0 0 2px 0; color: #475569;">용도별 서버 자원 과부족 현황</h5>' +
|
||||
'<div style="flex: 1; min-height: 0; position: relative;"><canvas id="chart-total-server-mismatch-by-purpose-4p" style="position: absolute; top:0; left:0; width:100%; height:100%;"></canvas></div>' +
|
||||
'</div>' +
|
||||
'<div class="dashboard-card" style="min-height: unset; height: 100%; padding: 0.4rem 0.6rem; border-radius: 8px; display:flex; flex-direction:column; background: rgba(255, 255, 255, 0.85); margin-bottom: 0;">' +
|
||||
'<h5 style="font-size: 0.75rem; font-weight: 800; margin: 0 0 2px 0; color: #475569;">서버 적정성 분석</h5>' +
|
||||
'<div style="flex: 1; min-height: 0; position: relative;"><canvas id="chart-server-status-4p" style="position: absolute; top:0; left:0; width:100%; height:100%;"></canvas></div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
|
||||
@@ -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 } }
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user