From a4b620099c5293d989523b3e60d2efed7db83529 Mon Sep 17 00:00:00 2001 From: JooWangi Date: Mon, 15 Jun 2026 11:22:07 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=8C=80=EC=8B=9C=EB=B3=B4=EB=93=9C=20?= =?UTF-8?q?=ED=8F=B0=ED=8A=B8=20=ED=81=AC=EA=B8=B0,=20=ED=8C=A8=EB=94=A9?= =?UTF-8?q?=20=EC=A1=B0=EC=A0=88=20=EB=B0=8F=20=EC=9E=90=EC=82=B0=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20=EC=A0=95=EB=A0=AC=20=EA=B8=B0=EC=A4=80=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD(updated=5Fat=20=EB=82=B4=EB=A6=BC=EC=B0=A8=EC=88=9C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/Dashboard/HwDashboard.ts | 252 ++++++++++++++++------------- src/views/List/ListFactory.ts | 25 ++- src/views/List/PcListView.ts | 13 +- 3 files changed, 171 insertions(+), 119 deletions(-) diff --git a/src/views/Dashboard/HwDashboard.ts b/src/views/Dashboard/HwDashboard.ts index 9364620..b8208a5 100644 --- a/src/views/Dashboard/HwDashboard.ts +++ b/src/views/Dashboard/HwDashboard.ts @@ -18,10 +18,10 @@ export function renderHwDashboard(container: HTMLElement) { // 2. 1페이지 매거진 리포트(제목바 제거, '| 제목' 미니멀리즘 스타일) HTML 빌드 container.innerHTML = ` -
+
-
+

개인 PC 자산 대시보드 @@ -44,62 +44,62 @@ export function renderHwDashboard(container: HTMLElement) {

-
+
-
+
-
+
-
+
- 보유 자산 수량 + 보유 자산 수량
-
0대
- 전사 보유 개인용 PC +
0대
+ 전사 보유 개인용 PC
- -
+ +
- 사양 부족 검토 + 사양 부족
-
0명
- 사양 교체 권고 자산 +
0대
+ 사양 교체 권고 자산
- -
+ +
- 오버스펙 검토 + 오버 스펙
-
0명
- 사양 회수 권고 자산 +
0대
+ 사양 회수 권고 자산
-
+
- 윈도우 11 불가 PC + 윈도우 11 불가 PC
-
0대
- 업데이트 미지원 하드웨어 +
0대
+ 업데이트 미지원 하드웨어
@@ -107,27 +107,25 @@ export function renderHwDashboard(container: HTMLElement) {
- - -
+ +
- -
+
-
- 등급별 자산 종합 현황 +
+ 등급별 자산 종합 현황
- -
- + +
+
- - - - - - + + + + + + @@ -137,20 +135,40 @@ export function renderHwDashboard(container: HTMLElement) { - -
+
+ + + + +
+ + +
+
+ 직무별 사양 적정성 분석 +
+
+ +
+
+ + +
+ + +
-
- 등급별 보유 비율 +
+ 등급별 보유 비율
- -
-
+ +
+
- -
+ +
최상급 @@ -170,42 +188,26 @@ export function renderHwDashboard(container: HTMLElement) {
- -
- -
- -
- - -
-
- 직무별 사양 적정성 분석 -
-
- -
-
- - -
-
- 연도별 PC 노후도 및 교체 주기 예측 -
-
-
구분 (등급)보유량할당량여분부족분
구분 (등급)보유량운영중재고부족분
- - - - - - - - - - -
구분 (사용 연한)보유 대수권장 조치
+ +
+
+ 연도별 PC 노후도 및 예측 +
+
+ + + + + + + + + + + +
구분 (연한)보유권장 조치
+
@@ -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 = '사양 부족'; - criticalList.push(p); - underSpecCount++; - target.under++; - target.underPcs.push(p); - } else if (score > avg * 1.5) { + } 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); } } @@ -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 ` - ${label} + ${label} ${data.total}대 (${totalRate}%) ${data.active}대 ${data.stock}대 - ${data.under}대 + ${shortage}대 `; }; @@ -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 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: 9px 4px; text-align: center; font-weight: 800; cursor: pointer; transition: background 0.2s; background: #F8FAFC;`; + 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')} - 합계 (Total) - ${totalPcs}대 (100%) + 합계 (Total) + ${totalPcs}대 (100%) ${totalActive}대 ${totalStock}대 - ${totalUnder}대 + ${totalShortage}대 `; @@ -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 ` - ${label} - ${list.length}대 - + ${label} + ${list.length}대 + ${badgeText} @@ -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; diff --git a/src/views/List/ListFactory.ts b/src/views/List/ListFactory.ts index ba46459..b5f799d 100644 --- a/src/views/List/ListFactory.ts +++ b/src/views/List/ListFactory.ts @@ -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) { ${user} ${dept} (${job}) - ${status} + ${status === '오버스펙' ? '오버 스펙' : status} ${assetCode} diff --git a/src/views/List/PcListView.ts b/src/views/List/PcListView.ts index 178d9ac..02a2e69 100644 --- a/src/views/List/PcListView.ts +++ b/src/views/List/PcListView.ts @@ -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: {