merge: 공동작업자 HW_Dashboard 브랜치 병합 (대시보드 UI 및 가독성 개선 사항 병합)

This commit is contained in:
2026-06-19 13:39:29 +09:00
2 changed files with 488 additions and 413 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -153,6 +153,7 @@ export interface ListViewConfig {
showField?: boolean;
showType?: boolean;
showStatus?: boolean;
showPosition?: boolean;
};
columns: ColumnDef[];
onRowClick?: (asset: any) => void;
@@ -450,35 +451,63 @@ export function createListView(container: HTMLElement, config: ListViewConfig) {
});
// DB 기준 사양 데이터 맵핑 (state.masterData.jobSpecs 이용)
const jobSpecsMap: Record<string, number> = {};
const jobSpecsMap: Record<string, string> = {};
if (state.masterData.jobSpecs) {
state.masterData.jobSpecs.forEach((s: any) => {
jobSpecsMap[s.job_name] = s.min_score;
jobSpecsMap[s.job_name] = s.required_grade || '중급';
});
}
// 사용자 이름 → 세부 직무 맵 생성 (system_users.position 기준)
const userPositionMap: Record<string, string> = {};
if (state.masterData.users) {
state.masterData.users.forEach((u: any) => {
if (u.user_name && u.position) {
userPositionMap[u.user_name.trim()] = u.position.trim();
}
});
}
const GRADE_RANK: Record<string, number> = {
'premium': 4, '최상급': 4,
'high': 3, '상급': 3,
'normal': 2, '중급': 2,
'entry': 1, '보급': 1,
'replace': 0, '교체 대상': 0
};
// 기준 대비 사양 부족/오버스펙 분류
const criticalPcList: any[] = [];
pcs.forEach((pc: any) => {
const job = pc[ASSET_SCHEMA.USER_POSITION.key] || '미분류';
const userName = (pc[ASSET_SCHEMA.CURRENT_USER.key] || '').trim();
const job = userPositionMap[userName] || pc[ASSET_SCHEMA.USER_POSITION.key] || '미분류';
const score = pc['_pc_score'];
const standardScore = jobSpecsMap[job] !== undefined ? jobSpecsMap[job] : (jobScores[job]?.avg || 0);
const requiredGrade = jobSpecsMap[job] || jobSpecsMap[pc[ASSET_SCHEMA.USER_POSITION.key]] || '중급';
const cpu = pc[ASSET_SCHEMA.CPU.key] || '';
const ram = pc[ASSET_SCHEMA.RAM.key] || '';
const win11Incompatible = isWindows11Incompatible(cpu, ram);
let actualGrade = 'replace';
if (score >= 85) actualGrade = 'premium';
else if (score >= 70) actualGrade = 'high';
else if (score >= 40) actualGrade = 'normal';
else if (score >= 20) actualGrade = 'entry';
const reqRank = GRADE_RANK[requiredGrade] !== undefined ? GRADE_RANK[requiredGrade] : 2;
const actRank = GRADE_RANK[actualGrade] !== undefined ? GRADE_RANK[actualGrade] : 0;
let isUnder = false;
if (standardScore > 0) {
if (score < standardScore * 0.6) {
if (job !== '재고PC') {
if (win11Incompatible) {
isUnder = true;
pc['_spec_status'] = '사양 부족';
} else if (score > standardScore * 1.5 && !win11Incompatible) {
} else if (actRank < reqRank) {
isUnder = true;
pc['_spec_status'] = '사양 부족';
} else if (actRank > reqRank) {
pc['_spec_status'] = '오버스펙';
criticalPcList.push(pc);
} else if (win11Incompatible) {
isUnder = true;
pc['_spec_status'] = '사양 부족';
} else {
pc['_spec_status'] = '적정';
}
@@ -496,16 +525,38 @@ export function createListView(container: HTMLElement, config: ListViewConfig) {
}
});
// 정렬: 기준 점수 대비 사양 부족이 심한 순(비율이 낮은 순)으로 정렬
// 정렬: 요구 등급 대비 실제 성능이 많이 부족한 순(등급 편차가 큰 순)으로 정렬
criticalPcList.sort((a: any, b: any) => {
const jobA = a[ASSET_SCHEMA.USER_POSITION.key] || '미분류';
const jobB = b[ASSET_SCHEMA.USER_POSITION.key] || '미분류';
const stdA = jobSpecsMap[jobA] !== undefined ? jobSpecsMap[jobA] : (jobScores[jobA]?.avg || 0);
const stdB = jobSpecsMap[jobB] !== undefined ? jobSpecsMap[jobB] : (jobScores[jobB]?.avg || 0);
const userNameA = (a[ASSET_SCHEMA.CURRENT_USER.key] || '').trim();
const userNameB = (b[ASSET_SCHEMA.CURRENT_USER.key] || '').trim();
const jobA = userPositionMap[userNameA] || a[ASSET_SCHEMA.USER_POSITION.key] || '미분류';
const jobB = userPositionMap[userNameB] || b[ASSET_SCHEMA.USER_POSITION.key] || '미분류';
const ratioA = stdA > 0 ? a['_pc_score'] / stdA : 1;
const ratioB = stdB > 0 ? b['_pc_score'] / stdB : 1;
return ratioA - ratioB;
const reqA = jobSpecsMap[jobA] || jobSpecsMap[a[ASSET_SCHEMA.USER_POSITION.key]] || '중급';
const reqB = jobSpecsMap[jobB] || jobSpecsMap[b[ASSET_SCHEMA.USER_POSITION.key]] || '중급';
const scoreA = a['_pc_score'];
const scoreB = b['_pc_score'];
let actA = 'replace';
if (scoreA >= 85) actA = 'premium';
else if (scoreA >= 70) actA = 'high';
else if (scoreA >= 40) actA = 'normal';
else if (scoreA >= 20) actA = 'entry';
let actB = 'replace';
if (scoreB >= 85) actB = 'premium';
else if (scoreB >= 70) actB = 'high';
else if (scoreB >= 40) actB = 'normal';
else if (scoreB >= 20) actB = 'entry';
const devA = (GRADE_RANK[reqA] || 2) - (GRADE_RANK[actA] || 0);
const devB = (GRADE_RANK[reqB] || 2) - (GRADE_RANK[actB] || 0);
if (devA !== devB) {
return devB - devA; // 편차가 큰 것(더 많이 부족한 것)이 먼저 정렬됨
}
return scoreA - scoreB; // 편차가 같으면 성능 점수가 낮은 순
});
if (criticalPcList.length === 0) {
@@ -709,7 +760,6 @@ export function createListView(container: HTMLElement, config: ListViewConfig) {
<td class="text-center">${asset[ASSET_SCHEMA.LOC_DETAIL.key] || '-'}</td>
</tr>`;
}).join('');
tbody.querySelectorAll('.mini-row').forEach(row => {
row.addEventListener('click', () => {
tbody.querySelectorAll('.mini-row').forEach(r => r.classList.remove('active'));
@@ -806,7 +856,7 @@ export function createListView(container: HTMLElement, config: ListViewConfig) {
</button>
` : ''}
<button id="btn-add-asset" class="btn btn-primary">
<i data-lucide="plus" class="icon-sm"></i> 자산 추가
<i data-lucide="plus" class="icon-sm"></i> ${config.title === '직무별 기준 사양' ? '기준 사양 추가' : (config.title === '부품 마스터' ? '표준 부품 추가' : '자산 추가')}
</button>
`;