feat: migrate ServerPC data to asset_pc, enhance filters with location, and standardize page headers

- 서버PC 자산을 asset_pc 테이블로 통합 마이그레이션 및 스키마 확장 (위치, IP 정보 복구 완료)

- 하드웨어 자산 페이지의 구매법인 필터를 자산위치 필터로 교체 및 동적 데이터 바인딩 적용

- 모든 자산 리스트 페이지 상단에 설명(Description) 필드 추가 및 헤더 표준화

- 상세 모달 내 삭제 버튼 기능 구현 및 서버PC 용도 필드 노출 오류 수정

- 현 사용조직 필터 리스트가 비어있던 DOM 셀렉터 버그 수정
This commit is contained in:
2026-05-26 17:33:03 +09:00
parent d34ebb8500
commit 82bbe85e23
43 changed files with 2055 additions and 1871 deletions

View File

@@ -1,7 +1,7 @@
import { state } from '../../core/state';
import { HardwareAsset } from '../../core/excelHandler';
import { openHwModal } from '../../components/Modal/HWModal';
import { calculateAssetAge, normalizeDate } from '../../core/utils';
import { ASSET_SCHEMA } from '../../core/schema';
declare var Chart: any;
@@ -12,14 +12,14 @@ export function renderHwDashboard(container: HTMLElement) {
let totalAge = 0;
let countWithDate = 0;
let over5YearsCount = 0;
let latestAsset: HardwareAsset | null = null;
let latestAsset: any | null = null;
let latestYear = 0;
const ageGroups = { stable: 0, warning: 0, critical: 0 };
const yearlyCount: Record<string, number> = {};
allHw.forEach(a => {
const pDate = a. || (a as any).purchase_date;
const pDate = a[ASSET_SCHEMA.PURCHASE_DATE.key];
if (!pDate) return;
const age = calculateAssetAge(pDate);
@@ -53,10 +53,10 @@ export function renderHwDashboard(container: HTMLElement) {
// 교체 시급 대상 TOP 10 (오래된 순)
const criticalList = [...allHw]
.filter(a => (a. || (a as any).purchase_date))
.filter(a => a[ASSET_SCHEMA.PURCHASE_DATE.key])
.sort((a, b) => {
const dateA = new Date(normalizeDate(a. || (a as any).purchase_date)).getTime();
const dateB = new Date(normalizeDate(b. || (b as any).purchase_date)).getTime();
const dateA = new Date(normalizeDate(a[ASSET_SCHEMA.PURCHASE_DATE.key])).getTime();
const dateB = new Date(normalizeDate(b[ASSET_SCHEMA.PURCHASE_DATE.key])).getTime();
return dateA - dateB;
})
.slice(0, 10);
@@ -69,19 +69,23 @@ export function renderHwDashboard(container: HTMLElement) {
<span style="font-size:1rem; font-weight:700; color:var(--text-main);">전체 평균 사용 연수</span>
<div style="font-size: 0.8125rem; color:var(--text-muted); margin-bottom: 1rem;">전체 자산 기준 (권장 4.5년)</div>
<div style="font-size: 2rem; font-weight:700; color:var(--dash-primary);">${avgAge}년</div>
<div style="width: 100%; height: 4px; background-color: var(--dash-primary); border-radius: 2px; margin-top: 0.5rem;"></div>
<div style="width: 100%; height: 4px; background-color: var(--border-color); border-radius: 2px; overflow: hidden; margin-top: 0.5rem;">
<div style="width: ${Math.min(100, (Number(avgAge)/4.5)*100)}%; height: 100%; background-color: var(--dash-primary);"></div>
</div>
</div>
<div class="dashboard-card" style="min-height:auto;">
<span style="font-size:1rem; font-weight:700; color:var(--text-main);">5년 이상 노후 자산 비율</span>
<div style="font-size: 0.8125rem; color:var(--text-muted); margin-bottom: 1rem;">총 ${over5YearsCount}대 해당</div>
<div style="font-size: 2rem; font-weight:700; color:${over5Rate >= 20 ? 'var(--dash-danger)' : 'var(--dash-primary)'};">${over5Rate}%</div>
<div style="width: 100%; height: 4px; background-color: ${over5Rate >= 20 ? 'var(--dash-danger)' : 'var(--dash-primary)'}; border-radius: 2px; margin-top: 0.5rem;"></div>
<div style="width: 100%; height: 4px; background-color: var(--border-color); border-radius: 2px; overflow: hidden; margin-top: 0.5rem;">
<div style="width: ${over5Rate}%; height: 100%; background-color: ${over5Rate >= 20 ? 'var(--dash-danger)' : 'var(--dash-primary)'};"></div>
</div>
</div>
<div class="dashboard-card" style="min-height:auto;">
<span style="font-size:1rem; font-weight:700; color:var(--text-main);">최신 도입 모델 (${latestYear}년)</span>
<div style="font-size: 0.8125rem; color:var(--text-muted); margin-bottom: 1rem;">자산번호: ${(latestAsset as any)?. || '-'}</div>
<div style="font-size: 1.25rem; font-weight:700; color:var(--primary-color); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; height: 3rem; display: flex; align-items: center;" title="${(latestAsset as any)?. || '정보 없음'}">
${(latestAsset as any)?. || '정보 없음'}
<div style="font-size: 0.8125rem; color:var(--text-muted); margin-bottom: 1rem;">자산번호: ${latestAsset ? latestAsset[ASSET_SCHEMA.ASSET_CODE.key] : '-'}</div>
<div style="font-size: 1.25rem; font-weight:700; color:var(--primary-color); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; height: 3rem; display: flex; align-items: center;" title="${latestAsset ? latestAsset[ASSET_SCHEMA.MODEL_NAME.key] : '정보 없음'}">
${latestAsset ? (latestAsset[ASSET_SCHEMA.MODEL_NAME.key] || latestAsset[ASSET_SCHEMA.ASSET_NAME.key] || '정보 없음') : '정보 없음'}
</div>
<div style="width: 100%; height: 4px; background-color: var(--primary-color); border-radius: 2px; margin-top: 0.5rem;"></div>
</div>
@@ -103,27 +107,31 @@ export function renderHwDashboard(container: HTMLElement) {
<table>
<thead>
<tr>
<th>순위</th>
<th>자산번호</th>
<th>유형</th>
<th>모델명</th>
<th>사용자/담당자</th>
<th>구매연월</th>
<th>연령</th>
<th style="text-align:center;">순위</th>
<th style="text-align:center;">자산번호</th>
<th style="text-align:center;">유형</th>
<th style="text-align:center;">모델명</th>
<th style="text-align:center;">담당자</th>
<th style="text-align:center;">구매일자</th>
<th style="text-align:center;">연령</th>
</tr>
</thead>
<tbody>
${criticalList.map((a, i) => `
<tr class="clickable-row" data-id="${a.id}">
${criticalList.map((a, i) => {
const pDate = a[ASSET_SCHEMA.PURCHASE_DATE.key];
const age = calculateAssetAge(pDate);
return `
<tr class="clickable-row" data-id="${a.id}" style="cursor:pointer;">
<td style="text-align:center; font-weight:600; color:var(--text-muted)">${i + 1}</td>
<td>${a. || '-'}</td>
<td><span class="badge-type">${a.type}</span></td>
<td>${a. || a. || '-'}</td>
<td>${a. || a._정 || '-'}</td>
<td style="text-align:center;">${a. || (a as any).purchase_date || '-'}</td>
<td style="text-align:center;"><strong style="color:${calculateAssetAge(a. || (a as any).purchase_date) >= 5 ? 'var(--danger)' : 'inherit'}">${calculateAssetAge(a. || (a as any).purchase_date)}년</strong></td>
<td style="text-align:center;">${a[ASSET_SCHEMA.ASSET_CODE.key] || '-'}</td>
<td style="text-align:center;"><span class="badge-type">${a[ASSET_SCHEMA.ASSET_TYPE.key] || a.category || ''}</span></td>
<td>${a[ASSET_SCHEMA.MODEL_NAME.key] || a[ASSET_SCHEMA.ASSET_NAME.key] || '-'}</td>
<td style="text-align:center;">${a[ASSET_SCHEMA.MANAGER_MAIN.key] || '-'}</td>
<td style="text-align:center;">${pDate || '-'}</td>
<td style="text-align:center;"><strong style="color:${age >= 5 ? 'var(--danger)' : 'inherit'}">${age}년</strong></td>
</tr>
`).join('')}
`;
}).join('')}
</tbody>
</table>
</div>
@@ -147,7 +155,7 @@ export function renderHwDashboard(container: HTMLElement) {
function initAgingCharts(ageGroups: any, yearlyCount: Record<string, number>) {
const agingCtx = document.getElementById('chart-aging-dist') as HTMLCanvasElement;
if (agingCtx) {
if (agingCtx && typeof Chart !== 'undefined') {
new Chart(agingCtx, {
type: 'doughnut',
data: {
@@ -168,7 +176,7 @@ function initAgingCharts(ageGroups: any, yearlyCount: Record<string, number>) {
}
const trendCtx = document.getElementById('chart-purchase-trend') as HTMLCanvasElement;
if (trendCtx) {
if (trendCtx && typeof Chart !== 'undefined') {
const years = Object.keys(yearlyCount).sort();
new Chart(trendCtx, {
type: 'bar',