feat: 대시보드 폰트 크기, 패딩 조절 및 자산목록 정렬 기준 변경(updated_at 내림차순)

This commit is contained in:
2026-06-15 11:22:07 +09:00
parent 407b9ba531
commit a4b620099c
3 changed files with 171 additions and 119 deletions

View File

@@ -18,10 +18,10 @@ export function renderHwDashboard(container: HTMLElement) {
// 2. 1페이지 매거진 리포트(제목바 제거, '| 제목' 미니멀리즘 스타일) HTML 빌드
container.innerHTML = `
<div class="view-container" style="overflow: hidden; padding: 0.75rem 1.5rem; background-color: #F8FAFC; height: calc(100vh - var(--header-height) - 48px); box-sizing: border-box; display: flex; flex-direction: column; gap: 0.9rem; font-family: 'Pretendard', sans-serif; color: #1E293B;">
<div class="view-container" style="overflow: hidden; padding: 0.4rem 1.2rem; background-color: #F8FAFC; height: calc(100vh - var(--header-height) - 48px); box-sizing: border-box; display: flex; flex-direction: column; gap: 0.5rem; font-family: 'Pretendard', sans-serif; color: #1E293B;">
<!-- 대시보드 타이틀 및 사용조직 필터 -->
<div style="display: flex; justify-content: space-between; align-items: flex-end; flex-shrink: 0; padding-bottom: 0.75rem;">
<div style="display: flex; justify-content: space-between; align-items: flex-end; flex-shrink: 0; padding-bottom: 0.4rem;">
<div style="border-left: 4px solid #1E5149; padding-left: 8px;">
<h2 style="font-size: 1.65rem; font-weight: 850; color: #1E5149; margin: 0; letter-spacing: -0.5px; display: flex; align-items: center; gap: 0.6rem;">
개인 PC 자산 대시보드
@@ -44,62 +44,62 @@ export function renderHwDashboard(container: HTMLElement) {
</div>
<!-- 메인 2단 컬럼 레이아웃 (5:5 비율) -->
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 0.9rem; flex: 1; min-height: 0; margin-bottom: 0.1rem;">
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem; flex: 1; min-height: 0; margin-bottom: 0.1rem;">
<!-- 좌측 컬럼 (Left Column) -->
<div style="display: flex; flex-direction: column; gap: 0.7rem; min-height: 0;">
<div style="display: flex; flex-direction: column; gap: 0.5rem; min-height: 0;">
<!-- 핵심 지표 카드 -->
<div class="stat-card" style="background: transparent; border-radius: 0; padding: 0.55rem 0.25rem; border: none; border-bottom: 1px solid #E2E8F0; display: flex !important; flex-direction: row !important; align-items: center; justify-content: space-between; flex-shrink: 0; gap: 0;">
<div class="stat-card" style="background: transparent; border-radius: 0; padding: 0.75rem 0.25rem; border: none; border-bottom: 1px solid #E2E8F0; display: grid !important; grid-template-columns: 1fr 1fr; gap: 0.6rem 0.9rem; flex-shrink: 0;">
<!-- 1. 보유 자산 수량 -->
<div style="flex: 1; border-right: 1px solid #EEF2F6; padding-right: 0.85rem;">
<div style="border-right: 1px solid #EEF2F6; border-bottom: 1px solid #EEF2F6; padding-bottom: 0.65rem; padding-right: 1.0rem;">
<div style="border-left: 4px solid #1E5149; padding-left: 8px; margin-bottom: 0.5rem; display: flex; align-items: center; line-height: 1;">
<span style="font-size: 1.03rem; font-weight: 800; color: #1E293B; white-space: nowrap;">보유 자산 수량</span>
<span style="font-size: 1.15rem; font-weight: 800; color: #1E293B; white-space: nowrap;">보유 자산 수량</span>
</div>
<div style="display: flex; align-items: flex-end; justify-content: space-between;">
<div>
<div id="metric-total-pcs" style="font-size: 1.96rem; font-weight: 900; color: #1E5149; line-height: 1; margin-bottom: 0.2rem;">0대</div>
<span style="font-size: 0.93rem; color: #64748B; font-weight: 700; white-space: nowrap;">전사 보유 개인용 PC</span>
<div id="metric-total-pcs" style="font-size: 2.3rem; font-weight: 900; color: #1E5149; line-height: 1; margin-bottom: 0.35rem;">0대</div>
<span style="font-size: 1.05rem; color: #64748B; font-weight: 700; white-space: nowrap;">전사 보유 개인용 PC</span>
</div>
</div>
</div>
<!-- 2. 사양 부족 검토 -->
<div id="card-under-spec" style="flex: 1; border-right: 1px solid #EEF2F6; padding-left: 0.85rem; padding-right: 0.85rem; cursor: pointer; transition: opacity 0.2s;" onmouseover="this.style.opacity='0.7'" onmouseout="this.style.opacity='1'">
<!-- 2. 사양 부족 -->
<div id="card-under-spec" style="border-bottom: 1px solid #EEF2F6; padding-bottom: 0.65rem; padding-left: 1.0rem; cursor: pointer; transition: opacity 0.2s;" onmouseover="this.style.opacity='0.7'" onmouseout="this.style.opacity='1'">
<div style="border-left: 4px solid #EF4444; padding-left: 8px; margin-bottom: 0.5rem; display: flex; align-items: center; line-height: 1;">
<span style="font-size: 1.03rem; font-weight: 800; color: #1E293B; white-space: nowrap;">사양 부족 검토</span>
<span style="font-size: 1.15rem; font-weight: 800; color: #1E293B; white-space: nowrap;">사양 부족</span>
</div>
<div style="display: flex; align-items: flex-end; justify-content: space-between;">
<div>
<div id="metric-under-spec" style="font-size: 1.96rem; font-weight: 900; color: #EF4444; line-height: 1; margin-bottom: 0.2rem;">0</div>
<span style="font-size: 0.93rem; color: #64748B; font-weight: 700; white-space: nowrap;">사양 교체 권고 자산</span>
<div id="metric-under-spec" style="font-size: 2.3rem; font-weight: 900; color: #EF4444; line-height: 1; margin-bottom: 0.35rem;">0</div>
<span style="font-size: 1.05rem; color: #64748B; font-weight: 700; white-space: nowrap;">사양 교체 권고 자산</span>
</div>
</div>
</div>
<!-- 3. 오버스펙 검토 -->
<div id="card-over-spec" style="flex: 1; border-right: 1px solid #EEF2F6; padding-left: 0.85rem; padding-right: 0.85rem; cursor: pointer; transition: opacity 0.2s;" onmouseover="this.style.opacity='0.7'" onmouseout="this.style.opacity='1'">
<!-- 3. 오버 스펙 -->
<div id="card-over-spec" style="border-right: 1px solid #EEF2F6; padding-top: 0.65rem; padding-right: 1.0rem; cursor: pointer; transition: opacity 0.2s;" onmouseover="this.style.opacity='0.7'" onmouseout="this.style.opacity='1'">
<div style="border-left: 4px solid #F59E0B; padding-left: 8px; margin-bottom: 0.5rem; display: flex; align-items: center; line-height: 1;">
<span style="font-size: 1.03rem; font-weight: 800; color: #1E293B; white-space: nowrap;">오버스펙 검토</span>
<span style="font-size: 1.15rem; font-weight: 800; color: #1E293B; white-space: nowrap;">오버 스펙</span>
</div>
<div style="display: flex; align-items: flex-end; justify-content: space-between;">
<div>
<div id="metric-over-spec" style="font-size: 1.96rem; font-weight: 900; color: #F59E0B; line-height: 1; margin-bottom: 0.2rem;">0</div>
<span style="font-size: 0.93rem; color: #64748B; font-weight: 700; white-space: nowrap;">사양 회수 권고 자산</span>
<div id="metric-over-spec" style="font-size: 2.3rem; font-weight: 900; color: #F59E0B; line-height: 1; margin-bottom: 0.35rem;">0</div>
<span style="font-size: 1.05rem; color: #64748B; font-weight: 700; white-space: nowrap;">사양 회수 권고 자산</span>
</div>
</div>
</div>
<!-- 4. 윈도우 11 불가 PC -->
<div id="card-win11-incompatible" style="flex: 1; padding-left: 0.85rem; padding-right: 0.25rem; cursor: pointer; transition: opacity 0.2s;" onmouseover="this.style.opacity='0.7'" onmouseout="this.style.opacity='1'">
<div id="card-win11-incompatible" style="padding-top: 0.65rem; padding-left: 1.0rem; cursor: pointer; transition: opacity 0.2s;" onmouseover="this.style.opacity='0.7'" onmouseout="this.style.opacity='1'">
<div style="border-left: 4px solid #3B82F6; padding-left: 8px; margin-bottom: 0.5rem; display: flex; align-items: center; line-height: 1;">
<span style="font-size: 1.03rem; font-weight: 800; color: #1E293B; white-space: nowrap;">윈도우 11 불가 PC</span>
<span style="font-size: 1.15rem; font-weight: 800; color: #1E293B; white-space: nowrap;">윈도우 11 불가 PC</span>
</div>
<div style="display: flex; align-items: flex-end; justify-content: space-between;">
<div>
<div id="metric-win11-incompatible" style="font-size: 1.96rem; font-weight: 900; color: #3B82F6; line-height: 1; margin-bottom: 0.2rem;">0대</div>
<span style="font-size: 0.93rem; color: #64748B; font-weight: 700; white-space: nowrap;">업데이트 미지원 하드웨어</span>
<div id="metric-win11-incompatible" style="font-size: 2.3rem; font-weight: 900; color: #3B82F6; line-height: 1; margin-bottom: 0.35rem;">0대</div>
<span style="font-size: 1.05rem; color: #64748B; font-weight: 700; white-space: nowrap;">업데이트 미지원 하드웨어</span>
</div>
</div>
</div>
@@ -107,27 +107,25 @@ export function renderHwDashboard(container: HTMLElement) {
</div>
<!-- PC 등급별 보유 현황 (등급별 게이지 + 우측 사양 적정성 도넛차트) -->
<!-- 등급별 자산 종합 현황 (하나로 통합) -->
<div style="background: transparent; border-radius: 0; padding: 0.75rem 0.25rem; border: none; border-bottom: 1px solid #E2E8F0; display: grid; grid-template-columns: 1.65fr 1fr; gap: 1.5rem; flex: 1.0; min-height: 0;">
<!-- 등급별 자산 종합 현황 (좌측 하단 단독 배치 및 크기 확대) -->
<div style="background: transparent; border-radius: 0; padding: 0.75rem 0.25rem; border: none; border-bottom: 1px solid #E2E8F0; display: flex; flex-direction: column; flex: 1.0; min-height: 0;">
<!-- 1열: 등급별 보유 현황 및 종합 매트릭스 테이블 영역 -->
<div style="display: flex; flex-direction: column; gap: 0.6rem; justify-content: flex-start; padding-left: 0.5rem;">
<div style="display: flex; flex-direction: column; gap: 0.9rem; justify-content: flex-start; padding-left: 0.5rem; height: 100%;">
<!-- 메인 제목 -->
<div style="border-left: 4px solid #1E5149; padding-left: 8px; margin-bottom: 0.65rem; display: flex; align-items: center; line-height: 1; height: 1.5rem;">
<span style="font-size: 1.11rem; font-weight: 800; color: #1E293B;">등급별 자산 종합 현황</span>
<div style="border-left: 4px solid #1E5149; padding-left: 8px; margin-bottom: 0.4rem; display: flex; align-items: center; line-height: 1; height: 1.7rem; flex-shrink: 0;">
<span style="font-size: 1.25rem; font-weight: 850; color: #1E293B;">등급별 자산 종합 현황</span>
</div>
<!-- 종합 매트릭스 테이블 -->
<div style="width: 100%; overflow-x: auto;">
<table style="width: 100%; border-collapse: collapse; text-align: left; font-size: 1.15rem;">
<!-- 종합 매트릭스 테이블 (폰트 크기 1.25rem 으로 확대 및 꽉 채우기) -->
<div style="width: 100%; overflow-x: auto; flex: 1; display: flex; align-items: stretch;">
<table style="width: 100%; border-collapse: collapse; text-align: left; font-size: 1.25rem; height: 100%;">
<thead>
<tr style="border-bottom: 2px solid #1E5149; color: #475569; font-weight: 800;">
<th style="padding: 14px 4px; width: 32%;">구분 (등급)</th>
<th style="padding: 14px 4px; text-align: center; width: 17%;">보유량</th>
<th style="padding: 14px 4px; text-align: center; width: 17%;">할당량</th>
<th style="padding: 14px 4px; text-align: center; width: 17%;">여분</th>
<th style="padding: 14px 4px; text-align: center; width: 17%; color: #EF4444;">부족분</th>
<tr style="border-bottom: 2px solid #1E5149; color: #475569; font-weight: 850;">
<th style="padding: 14px 10px; width: 32%; font-size: 1.25rem;">구분 (등급)</th>
<th style="padding: 14px 10px; text-align: center; width: 17%; font-size: 1.25rem;">보유량</th>
<th style="padding: 14px 10px; text-align: center; width: 17%; font-size: 1.25rem;">운영중</th>
<th style="padding: 14px 10px; text-align: center; width: 17%; font-size: 1.25rem;">재고</th>
<th style="padding: 14px 10px; text-align: center; width: 17%; color: #EF4444; font-size: 1.25rem;">부족분</th>
</tr>
</thead>
<tbody id="pc-grade-matrix-tbody">
@@ -137,20 +135,40 @@ export function renderHwDashboard(container: HTMLElement) {
</div>
</div>
<!-- 2열: 등급별 보유 비율 도넛 영역 -->
<div style="display: flex; flex-direction: column; align-items: center; justify-content: flex-start; gap: 0.8rem; padding-top: 0.5rem;">
<!-- 서브 제목 -->
<div style="margin-bottom: 0.35rem; display: flex; align-items: center; justify-content: center; line-height: 1; height: 1.5rem;">
<span style="font-size: 1.06rem; font-weight: 800; color: #475569; text-transform: uppercase;">등급별 보유 비율</span>
</div>
<!-- 도넛 그래프 (크기 확대 및 범례 추가, 제목 상단 이관) -->
<div style="display: flex; flex-direction: column; align-items: center; justify-content: center; flex-shrink: 0; padding-right: 0.25rem; width: 100%;">
<div style="width: 170px; height: 170px; position: relative;">
</div>
<!-- 우측 컬럼 (Right Column) -->
<div style="display: flex; flex-direction: column; gap: 0.5rem; min-height: 0;">
<!-- 직무별 사양 적정성 분석 차트 카드 -->
<div style="background: transparent; border-radius: 0; padding: 0.7rem 0.25rem; border: none; border-bottom: 1px solid #E2E8F0; display: flex; flex-direction: column; flex: 1.0; min-height: 0;">
<div style="border-left: 4px solid #1E5149; padding-left: 8px; margin-bottom: 0.5rem; display: flex; align-items: center; line-height: 1; flex-shrink: 0;">
<span style="font-size: 1.25rem; font-weight: 850; color: #1E293B;">직무별 사양 적정성 분석</span>
</div>
<div style="flex: 1; min-height: 0; width: 100%; position: relative;">
<canvas id="chart-job-scores" style="width: 100%; height: 100%;"></canvas>
</div>
</div>
<!-- 우측 하단: 등급별 보유 비율 도넛 & 연도별 PC 노후도 통합 배치 (너비 축소) -->
<div style="background: transparent; border-radius: 0; padding: 0.7rem 0.25rem; border: none; border-bottom: 1px solid #E2E8F0; display: grid; grid-template-columns: 1.15fr 1.25fr; gap: 0.8rem; flex: 1.0; min-height: 0;">
<!-- 1열: 등급별 보유 비율 도넛 영역 -->
<div style="display: flex; flex-direction: column; align-items: center; justify-content: flex-start; gap: 0.7rem; padding-top: 0.1rem; min-height: 0; height: 100%;">
<!-- 서브 제목 -->
<div style="width: 100%; border-left: 4px solid #1E5149; padding-left: 8px; margin-bottom: 0.5rem; display: flex; align-items: center; line-height: 1; flex-shrink: 0; height: 1.5rem;">
<span style="font-size: 1.25rem; font-weight: 850; color: #1E293B;">등급별 보유 비율</span>
</div>
<!-- 도넛 그래프 (크기 조절 및 수직 가운데 정렬) -->
<div style="display: flex; flex-direction: column; align-items: center; justify-content: center; flex: 1; width: 100%; min-height: 0;">
<div style="width: 180px; height: 180px; position: relative;">
<canvas id="chart-overall-donut"></canvas>
</div>
<!-- 커스텀 범례 -->
<div style="display: flex; gap: 0.65rem; justify-content: center; align-items: center; margin-top: 8px; font-size: 1.15rem; font-weight: 700; color: #475569;">
<!-- 커스텀 범례 (폰트 최적화) -->
<div style="display: flex; flex-wrap: wrap; gap: 0.4rem 0.6rem; justify-content: center; align-items: center; margin-top: 10px; font-size: 1.05rem; font-weight: 700; color: #475569; width: 100%;">
<div style="display: flex; align-items: center; gap: 4px;">
<span style="display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: #11302B;"></span>
<span>최상급</span>
@@ -171,35 +189,18 @@ export function renderHwDashboard(container: HTMLElement) {
</div>
</div>
<!-- 2열: 연도별 PC 노후도 및 교체 주기 예측 카드 (너비 줄임) -->
<div style="display: flex; flex-direction: column; min-height: 0;">
<div style="border-left: 4px solid #1E5149; padding-left: 8px; margin-bottom: 0.5rem; display: flex; align-items: center; line-height: 1; flex-shrink: 0; height: 1.5rem;">
<span style="font-size: 1.25rem; font-weight: 850; color: #1E293B; white-space: nowrap;">연도별 PC 노후도 및 예측</span>
</div>
</div>
<!-- 우측 컬럼 (Right Column) -->
<div style="display: flex; flex-direction: column; gap: 0.9rem; min-height: 0;">
<!-- 직무별 사양 적정성 분석 차트 카드 -->
<div style="background: transparent; border-radius: 0; padding: 0.85rem 0.25rem; border: none; border-bottom: 1px solid #E2E8F0; display: flex; flex-direction: column; flex: 1.1; min-height: 0;">
<div style="border-left: 4px solid #1E5149; padding-left: 8px; margin-bottom: 0.9rem; display: flex; align-items: center; line-height: 1; flex-shrink: 0;">
<span style="font-size: 1.11rem; font-weight: 800; color: #1E293B;">직무별 사양 적정성 분석</span>
</div>
<div style="flex: 1; min-height: 0; width: 100%; position: relative;">
<canvas id="chart-job-scores" style="width: 100%; height: 100%;"></canvas>
</div>
</div>
<!-- 연도별 PC 노후도 및 교체 주기 예측 카드 -->
<div style="background: transparent; border-radius: 0; padding: 0.85rem 0.25rem; border: none; border-bottom: 1px solid #E2E8F0; display: flex; flex-direction: column; flex: 0.9; min-height: 0;">
<div style="border-left: 4px solid #1E5149; padding-left: 8px; margin-bottom: 0.9rem; display: flex; align-items: center; line-height: 1; flex-shrink: 0;">
<span style="font-size: 1.15rem; font-weight: 800; color: #1E293B;">연도별 PC 노후도 및 교체 주기 예측</span>
</div>
<div style="flex: 1; overflow: hidden; min-height: 0; padding-left: 12px;">
<div style="flex: 1; overflow: hidden; min-height: 0; padding-right: 0.2rem;">
<table style="width: 100%; border-collapse: collapse; text-align: left; font-size: 1.15rem;">
<thead style="position: sticky; top: 0; background: white; z-index: 5;">
<tr style="border-bottom: 2px solid #1E5149; color: #475569; font-weight: 800;">
<th style="padding: 10px 4px 10px 0; width: 50%;">구분 (사용 연한)</th>
<th style="padding: 10px 4px; width: 25%; text-align: center;">보유 대수</th>
<th style="padding: 10px 4px; width: 25%; text-align: center;">권장 조치</th>
<tr style="border-bottom: 2px solid #1E5149; color: #475569; font-weight: 850;">
<th style="padding: 12px 10px; width: 45%; font-size: 1.15rem;">구분 (연한)</th>
<th style="padding: 12px 10px; text-align: center; width: 25%; font-size: 1.15rem;">보유</th>
<th style="padding: 12px 10px; text-align: center; width: 30%; font-size: 1.15rem;">권장 조치</th>
</tr>
</thead>
<tbody id="pc-aging-tbody">
@@ -208,6 +209,7 @@ export function renderHwDashboard(container: HTMLElement) {
</table>
</div>
</div>
</div>
</div>
@@ -322,20 +324,35 @@ function updateDashboardData(pcs: any[], selectedDept: string) {
const job = p[ASSET_SCHEMA.USER_POSITION.key] || '미분류';
const avg = jobScores[job]?.avg || 0;
const win11Incompatible = isWindows11Incompatible(p.cpu, p.ram);
let isUnder = false;
if (avg > 0 && job !== '재고PC') {
if (score < avg * 0.6) {
isUnder = true;
p._spec_status = '사양 부족';
} else if (score > avg * 1.5 && !win11Incompatible) {
p._spec_status = '오버스펙';
criticalList.push(p);
overSpecCount++;
} else if (win11Incompatible) {
isUnder = true;
p._spec_status = '사양 부족';
} else {
p._spec_status = '적정';
}
} else {
if (win11Incompatible) {
isUnder = true;
p._spec_status = '사양 부족';
}
}
if (isUnder) {
criticalList.push(p);
underSpecCount++;
target.under++;
target.underPcs.push(p);
} else if (score > avg * 1.5) {
p._spec_status = '오버스펙';
criticalList.push(p);
overSpecCount++;
} else {
p._spec_status = '적정';
}
}
}
@@ -347,8 +364,8 @@ function updateDashboardData(pcs: any[], selectedDept: string) {
// 5. 핵심 텍스트형 요약 지표 갱신
document.getElementById('metric-total-pcs')!.textContent = `${filtered.length}`;
document.getElementById('metric-under-spec')!.textContent = `${underSpecCount}`;
document.getElementById('metric-over-spec')!.textContent = `${overSpecCount}`;
document.getElementById('metric-under-spec')!.textContent = `${underSpecCount}`;
document.getElementById('metric-over-spec')!.textContent = `${overSpecCount}`;
document.getElementById('metric-win11-incompatible')!.textContent = `${win11IncompatibleCount}`;
// 6. 종합 매트릭스 테이블 렌더링 및 바인딩
@@ -358,16 +375,18 @@ function updateDashboardData(pcs: any[], selectedDept: string) {
const data = matrix[gradeKey];
const totalRate = filtered.length > 0 ? Math.round((data.total / filtered.length) * 100) : 0;
const cellStyle = `padding: 9px 4px; text-align: center; font-weight: 700; cursor: pointer; transition: background 0.2s;`;
const cellStyle = `padding: 14px 12px; text-align: center; font-weight: 700; cursor: pointer; transition: background 0.2s; font-size: 1.25rem;`;
const hoverEvents = `onmouseover="this.style.background='#F1F5F9'" onmouseout="this.style.background='none'"`;
const shortage = Math.max(0, data.under - data.stock);
return `
<tr style="border-bottom: 1px solid #F1F5F9;">
<td style="padding: 9px 4px; font-weight: 800; color: ${color};">${label}</td>
<td style="padding: 14px 12px; font-weight: 800; color: ${color}; font-size: 1.25rem;">${label}</td>
<td class="matrix-cell" data-grade="${gradeKey}" data-type="total" style="${cellStyle}" ${hoverEvents}>${data.total}대 <span style="font-size:1.0rem; color:#64748B; font-weight:500;">(${totalRate}%)</span></td>
<td class="matrix-cell" data-grade="${gradeKey}" data-type="active" style="${cellStyle}" ${hoverEvents}>${data.active}대</td>
<td class="matrix-cell" data-grade="${gradeKey}" data-type="stock" style="${cellStyle}" ${hoverEvents}>${data.stock}대</td>
<td class="matrix-cell" data-grade="${gradeKey}" data-type="under" style="${cellStyle} color: #EF4444;" ${hoverEvents}>${data.under}대</td>
<td class="matrix-cell" data-grade="${gradeKey}" data-type="under" style="${cellStyle} color: #EF4444;" ${hoverEvents}>${shortage}대</td>
</tr>
`;
};
@@ -375,9 +394,14 @@ function updateDashboardData(pcs: any[], selectedDept: string) {
const totalPcs = filtered.length;
const totalActive = matrix.premium.active + matrix.high.active + matrix.normal.active + matrix.entry.active;
const totalStock = matrix.premium.stock + matrix.high.stock + matrix.normal.stock + matrix.entry.stock;
const totalUnder = matrix.premium.under + matrix.high.under + matrix.normal.under + matrix.entry.under;
const cellStyleHeader = `padding: 9px 4px; text-align: center; font-weight: 800; cursor: pointer; transition: background 0.2s; background: #F8FAFC;`;
const premiumShortage = Math.max(0, matrix.premium.under - matrix.premium.stock);
const highShortage = Math.max(0, matrix.high.under - matrix.high.stock);
const normalShortage = Math.max(0, matrix.normal.under - matrix.normal.stock);
const entryShortage = Math.max(0, matrix.entry.under - matrix.entry.stock);
const totalShortage = premiumShortage + highShortage + normalShortage + entryShortage;
const cellStyleHeader = `padding: 14px 12px; text-align: center; font-weight: 800; cursor: pointer; transition: background 0.2s; background: #F8FAFC; font-size: 1.25rem;`;
const hoverEventsHeader = `onmouseover="this.style.background='#EEF2F6'" onmouseout="this.style.background='#F8FAFC'"`;
matrixTbody.innerHTML = `
@@ -386,11 +410,11 @@ function updateDashboardData(pcs: any[], selectedDept: string) {
${renderMatrixRow('normal', '중급 PC (40점 ~ 70점)', '#10B981')}
${renderMatrixRow('entry', '보급 PC (40점 미만)', '#64748B')}
<tr style="background: #F8FAFC; border-top: 2px solid #E2E8F0; font-weight: 800;">
<td style="padding: 9px 4px; color: #1E293B; font-weight: 800;">합계 (Total)</td>
<td class="matrix-cell" data-grade="all" data-type="total" style="${cellStyleHeader}" ${hoverEventsHeader}>${totalPcs}대 <span style="font-size:1.0rem; color:#64748B; font-weight:600;">(100%)</span></td>
<td style="padding: 14px 12px; color: #1E293B; font-weight: 800; font-size: 1.25rem;">합계 (Total)</td>
<td class="matrix-cell" data-grade="all" data-type="total" style="${cellStyleHeader}" ${hoverEventsHeader}>${totalPcs}대 <span style="font-size:1.125rem; color:#64748B; font-weight:600;">(100%)</span></td>
<td class="matrix-cell" data-grade="all" data-type="active" style="${cellStyleHeader}" ${hoverEventsHeader}>${totalActive}대</td>
<td class="matrix-cell" data-grade="all" data-type="stock" style="${cellStyleHeader}" ${hoverEventsHeader}>${totalStock}대</td>
<td class="matrix-cell" data-grade="all" data-type="under" style="${cellStyleHeader} color: #EF4444;" ${hoverEventsHeader}>${totalUnder}대</td>
<td class="matrix-cell" data-grade="all" data-type="under" style="${cellStyleHeader} color: #EF4444;" ${hoverEventsHeader}>${totalShortage}대</td>
</tr>
`;
@@ -413,8 +437,8 @@ function updateDashboardData(pcs: any[], selectedDept: string) {
const getTypeLabel = (t: string) => {
if (t === 'total') return '보유';
if (t === 'active') return '할당 (운영)';
if (t === 'stock') return '여분 (재고)';
if (t === 'active') return '운영';
if (t === 'stock') return '재고';
if (t === 'under') return '부족 (사양 부족)';
return '';
};
@@ -473,9 +497,9 @@ function updateDashboardData(pcs: any[], selectedDept: string) {
const renderAgingRow = (label: string, list: any[], badgeText: string, badgeStyle: string, ageGroupKey: string) => {
return `
<tr style="border-bottom:1px solid #F1F5F9; cursor:pointer; transition: background 0.2s;" class="aging-row" data-group="${ageGroupKey}" onmouseover="this.style.background='#F8FAFC'" onmouseout="this.style.background='none'">
<td style="padding:9px 4px 9px 0; font-weight:700; color:#334155;">${label}</td>
<td style="padding:9px 4px; text-align:center; font-weight:700; color:#334155;">${list.length}대</td>
<td style="padding:9px 4px; text-align:center;">
<td style="padding:10px 10px; font-weight:700; color:#334155; font-size: 1.15rem;">${label}</td>
<td style="padding:10px 10px; text-align:center; font-weight:700; color:#334155; font-size: 1.15rem;">${list.length}대</td>
<td style="padding:10px 10px; text-align:center;">
<span style="padding:2px 8px; border-radius:4px; font-size:14px; font-weight:800; ${badgeStyle}">${badgeText}</span>
</td>
</tr>
@@ -519,9 +543,9 @@ function updateDashboardData(pcs: any[], selectedDept: string) {
};
};
// 사양 부족 / 오버스펙 / 윈도우 11 불가 클릭 리스너 설정
bindCardClick('card-under-spec', '사양 부족 검토 대상', p => p._spec_status === '사양 부족');
bindCardClick('card-over-spec', '오버스펙 검토 대상', p => p._spec_status === '오버스펙');
// 사양 부족 / 오버 스펙 / 윈도우 11 불가 클릭 리스너 설정
bindCardClick('card-under-spec', '사양 부족 대상', p => p._spec_status === '사양 부족');
bindCardClick('card-over-spec', '오버 스펙 대상', p => p._spec_status === '오버스펙');
bindCardClick('card-win11-incompatible', '윈도우 11 업그레이드 불가 PC', p => isWindows11Incompatible(p.cpu, p.ram));
// 9. 직무별 사양 적정성 대수 연산 및 차트 데이터 셋 구성 (누적 막대 그래프화)
@@ -714,7 +738,7 @@ function renderChart(labels: string[], underData: number[], normalData: number[]
categoryPercentage: 0.8
},
{
label: '오버스펙',
label: '오버 스펙',
data: overData,
backgroundColor: 'rgba(217, 119, 6, 0.85)', // Amber Orange
borderColor: 'rgb(217, 119, 6)',
@@ -758,7 +782,7 @@ function renderChart(labels: string[], underData: number[], normalData: number[]
return specStatus === clickedStatus;
});
showMiniListModal(`${clickedJob} - ${clickedStatus === '적정' ? '적정 사양' : clickedStatus} 자산`, matchedPcs);
showMiniListModal(`${clickedJob} - ${clickedStatus === '적정' ? '적정 사양' : (clickedStatus === '오버스펙' ? '오버 스펙' : clickedStatus)} 자산`, matchedPcs);
}
},
plugins: {
@@ -766,10 +790,10 @@ function renderChart(labels: string[], underData: number[], normalData: number[]
position: 'top',
align: 'end',
labels: {
font: { family: 'Pretendard', size: 11, weight: '700' },
font: { family: 'Pretendard', size: 16, weight: '700' },
color: '#475569',
boxWidth: 8,
boxHeight: 8,
boxWidth: 12,
boxHeight: 12,
usePointStyle: true
}
},
@@ -792,7 +816,7 @@ function renderChart(labels: string[], underData: number[], normalData: number[]
stacked: true,
ticks: {
callback: (val: any) => `${val}`,
font: { family: 'Pretendard', size: 10, weight: '600' },
font: { family: 'Pretendard', size: 14, weight: '600' },
color: '#64748B'
},
grid: { color: '#EEF2F6' }
@@ -800,7 +824,7 @@ function renderChart(labels: string[], underData: number[], normalData: number[]
y: {
stacked: true,
ticks: {
font: { family: 'Pretendard', size: 11, weight: '700' },
font: { family: 'Pretendard', size: 16, weight: '700' },
color: '#475569'
},
grid: { display: false }
@@ -868,7 +892,7 @@ function renderDonutChart(premium: number, high: number, normal: number, entry:
top: 50%;
left: 50%;
transform: translate(-50%, -46%);
font-size: 1.75rem;
font-size: 1.65rem;
font-weight: 900;
color: #1E5149;
font-family: 'Pretendard', sans-serif;

View File

@@ -1,5 +1,5 @@
import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema';
import { dynamicSort, renderPageHeader, calculateAssetAge, formatInline } from '../../core/utils';
import { dynamicSort, renderPageHeader, calculateAssetAge, formatInline, isWindows11Incompatible } from '../../core/utils';
import { setupTableSorting, SortState } from '../../core/tableHandler';
import { renderFilterBar, applyCommonFilters } from '../../core/filterHandler';
import { state } from '../../core/state';
@@ -921,14 +921,31 @@ export function createListView(container: HTMLElement, config: ListViewConfig) {
const score = pc['_pc_score'];
const avg = jobScores[job].avg;
const cpu = pc[ASSET_SCHEMA.CPU.key] || '';
const ram = pc[ASSET_SCHEMA.RAM.key] || '';
const win11Incompatible = isWindows11Incompatible(cpu, ram);
let isUnder = false;
if (avg > 0) {
if (score < avg * 0.6) {
isUnder = true;
pc['_spec_status'] = '사양 부족';
criticalPcList.push(pc);
} else if (score > avg * 1.5) {
} else if (score > avg * 1.5 && !win11Incompatible) {
pc['_spec_status'] = '오버스펙';
criticalPcList.push(pc);
} else if (win11Incompatible) {
isUnder = true;
pc['_spec_status'] = '사양 부족';
}
} else {
if (win11Incompatible) {
isUnder = true;
pc['_spec_status'] = '사양 부족';
}
}
if (isUnder) {
criticalPcList.push(pc);
}
});
@@ -960,7 +977,7 @@ export function createListView(container: HTMLElement, config: ListViewConfig) {
<td style="padding: 10px 0; font-weight: 600; color: #334155; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;" title="${user}">${user}</td>
<td style="padding: 10px 0; color: #475569; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;" title="${dept} (${job})">${dept} (${job})</td>
<td style="padding: 10px 0; white-space: nowrap; text-align: center;">
<span style="padding: 2px 6px; border-radius: 4px; font-size: 10px; font-weight: 700; ${badgeColor}">${status}</span>
<span style="padding: 2px 6px; border-radius: 4px; font-size: 10px; font-weight: 700; ${badgeColor}">${status === '오버스펙' ? '오버 스펙' : status}</span>
</td>
<td style="padding: 10px 0; font-family: monospace; color: #64748B; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;" title="${assetCode}">${assetCode}</td>
</tr>

View File

@@ -3,16 +3,27 @@ import { openHwModal } from '../../components/Modal/HWModal';
import { sortAssets, formatInline, calculatePcScoreDeductive, getPcGrade } from '../../core/utils';
import { ASSET_SCHEMA } from '../../core/schema';
import { createListView } from './ListFactory';
import { SortState } from '../../core/tableHandler';
let persistentSortState: SortState = { key: 'updated_at', direction: 'desc' };
export function renderPcList(container: HTMLElement) {
createListView(container, {
title: 'PC',
persistentSortState,
dataSource: () => {
const list = (state.masterData.pc || []).filter((a: any) => a.asset_type !== '서버PC');
list.forEach((a: any) => {
a['_pc_score'] = calculatePcScoreDeductive(a[ASSET_SCHEMA.CPU.key], a[ASSET_SCHEMA.RAM.key], a[ASSET_SCHEMA.GPU.key], a.purchase_date);
});
return sortAssets(list);
// 변경일시(updated_at) 내림차순 정렬 (최신 변경 항목이 맨 위로)
return list.sort((a: any, b: any) => {
const dateA = a.updated_at || a.created_at || '';
const dateB = b.updated_at || b.created_at || '';
if (dateA < dateB) return 1;
if (dateA > dateB) return -1;
return 0;
});
},
searchKeys: ['CURRENT_DEPT', 'CURRENT_USER', 'MODEL_NAME', 'MAC_ADDR', 'MANAGER_MAIN', 'ASSET_TYPE'],
filterOptions: {