feat(dashboard): update charts and styling on HW_Dashboard

This commit is contained in:
2026-06-10 09:12:29 +09:00
parent 34d99dc4b6
commit 9a2c35e652
2 changed files with 656 additions and 45 deletions

View File

@@ -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;
}

View File

@@ -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% 이상 초과 &nbsp;▸ 클릭하여 상세보기</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 } }
}
}
});
}
}