feat: PC 등급 뱃지 색상 CSS 추가 및 Win11 불가 PC 교체 대상 등급 강제 적용

This commit is contained in:
2026-06-29 10:03:07 +09:00
parent f656f0a439
commit d1378d127a
10 changed files with 1263 additions and 944 deletions

View File

@@ -1,5 +1,4 @@
import { ASSET_SCHEMA, UI_TEXT } from './schema';
import { getActionButtonsHTML } from './utils';
import { generateOptionsHTML } from '../components/Modal/ModalUtils';
import { CORP_LIST } from '../components/Modal/SharedData';
@@ -16,9 +15,18 @@ export interface FilterOptions {
showField?: boolean;
showType?: boolean;
showStatus?: boolean;
showPosition?: boolean;
extraHTML?: string;
onFilterChange: (filters: any) => void;
initialFilters?: any;
fullList?: any[]; // For populating dynamic filters
}
/**
* 전역 액션 버튼 그룹 생성 (자산 추가 등)
*/
export function getActionButtonsHTML(): string {
return `<div id="filter-bar-actions" class="header-action-group"></div>`;
}
export function renderFilterBar(container: HTMLElement, options: FilterOptions) {
@@ -30,11 +38,31 @@ export function renderFilterBar(container: HTMLElement, options: FilterOptions)
showField = false,
showType = false,
showStatus = false,
showPosition = false,
extraHTML = '',
onFilterChange,
initialFilters = { keyword: '', corp: '', dept: '', loc: '', field: '', type: '', status: '' }
initialFilters = { keyword: '', corp: '', dept: '', loc: '', field: '', type: '', status: '', position: '' },
fullList = []
} = options;
container.classList.add('search-bar'); // Restored class
// Helper to get unique sorted values
const getUnique = (key: keyof typeof ASSET_SCHEMA | string) => {
const schemaItem = (ASSET_SCHEMA as any)[key];
const fieldKey = schemaItem ? schemaItem.key : key;
const dbKey = schemaItem ? schemaItem.db : null;
return Array.from(new Set(fullList.map(item => {
const val = item[fieldKey];
if (val !== undefined && val !== null) return val;
if (dbKey) return item[dbKey];
return null;
}).filter(Boolean))).sort() as string[];
};
const hasDeptName = fullList.some(item => 'dept_name' in item);
const deptUniqueKey = hasDeptName ? 'dept_name' : 'CURRENT_DEPT';
container.innerHTML = `
<div class="search-item flex-1">
<label>${keywordLabel}</label>
@@ -45,6 +73,7 @@ export function renderFilterBar(container: HTMLElement, options: FilterOptions)
<label>${ASSET_SCHEMA.ASSET_TYPE.ui}</label>
<select id="filter-type">
<option value="">전체 유형</option>
${getUnique('ASSET_TYPE').map(v => `<option value="${v}" ${initialFilters.type === v ? 'selected' : ''}>${v}</option>`).join('')}
</select>
</div>` : ''}
${showStatus ? `
@@ -52,6 +81,7 @@ export function renderFilterBar(container: HTMLElement, options: FilterOptions)
<label>${ASSET_SCHEMA.HW_STATUS.ui}</label>
<select id="filter-status">
<option value="">전체 상태</option>
${getUnique('HW_STATUS').map(v => `<option value="${v}" ${initialFilters.status === v ? 'selected' : ''}>${v}</option>`).join('')}
</select>
</div>` : ''}
${showField ? `
@@ -73,16 +103,30 @@ export function renderFilterBar(container: HTMLElement, options: FilterOptions)
${showLoc ? `
<div class="search-item">
<label>${ASSET_SCHEMA.LOCATION.ui}</label>
<select id="filter-loc"><option value="">전체 위치</option></select>
<select id="filter-loc">
<option value="">전체 위치</option>
${getUnique('LOCATION').map(v => `<option value="${v}" ${initialFilters.loc === v ? 'selected' : ''}>${v}</option>`).join('')}
</select>
</div>` : ''}
${showDept ? `
<div class="search-item">
<label>${ASSET_SCHEMA.CURRENT_DEPT.ui}</label>
<select id="filter-dept"><option value="">전체 조직</option></select>
<label>조직</label>
<select id="filter-dept">
<option value="">전체 조직</option>
${getUnique(deptUniqueKey).map(v => `<option value="${v}" ${initialFilters.dept === v ? 'selected' : ''}>${v}</option>`).join('')}
</select>
</div>` : ''}
${showPosition ? `
<div class="search-item">
<label>직무</label>
<select id="filter-position">
<option value="">전체 직무</option>
${getUnique('position').map(v => `<option value="${v}" ${initialFilters.position === v ? 'selected' : ''}>${v}</option>`).join('')}
</select>
</div>` : ''}
${extraHTML}
<button id="btn-reset-filters" class="btn btn-outline btn-reset">
<i data-lucide="refresh-ccw"></i> ${UI_TEXT.ACTION.RESET_FILTER}
<i data-lucide="refresh-ccw" class="icon-sm"></i> ${UI_TEXT.ACTION.RESET_FILTER}
</button>
${getActionButtonsHTML()}
`;
@@ -96,7 +140,8 @@ export function renderFilterBar(container: HTMLElement, options: FilterOptions)
loc: (container.querySelector('#filter-loc') as HTMLSelectElement)?.value || '',
field: (container.querySelector('#filter-field') as HTMLSelectElement)?.value || '',
type: (container.querySelector('#filter-type') as HTMLSelectElement)?.value || '',
status: (container.querySelector('#filter-status') as HTMLSelectElement)?.value || ''
status: (container.querySelector('#filter-status') as HTMLSelectElement)?.value || '',
position: (container.querySelector('#filter-position') as HTMLSelectElement)?.value || ''
};
onFilterChange(filters);
};
@@ -108,9 +153,10 @@ export function renderFilterBar(container: HTMLElement, options: FilterOptions)
container.querySelector('#filter-field')?.addEventListener('change', triggerChange);
container.querySelector('#filter-type')?.addEventListener('change', triggerChange);
container.querySelector('#filter-status')?.addEventListener('change', triggerChange);
container.querySelector('#filter-position')?.addEventListener('change', triggerChange);
container.querySelector('#btn-reset-filters')?.addEventListener('click', () => {
['filter-keyword', 'filter-corp', 'filter-dept', 'filter-loc', 'filter-field', 'filter-type', 'filter-status'].forEach(id => {
['filter-keyword', 'filter-corp', 'filter-dept', 'filter-loc', 'filter-field', 'filter-type', 'filter-status', 'filter-position'].forEach(id => {
const el = container.querySelector(`#${id}`);
if (el) (el as any).value = '';
});
@@ -121,18 +167,37 @@ export function renderFilterBar(container: HTMLElement, options: FilterOptions)
/**
* 공통 필터링 로직
*/
export function applyCommonFilters(list: any[], filters: any, searchKeys: (keyof typeof ASSET_SCHEMA)[]) {
export function applyCommonFilters(list: any[], filters: any, searchKeys: any[]) {
return list.filter(item => {
const matchKeyword = !filters.keyword || searchKeys.some(key =>
String(item[ASSET_SCHEMA[key].key] || item[ASSET_SCHEMA[key].db] || '').toLowerCase().includes(filters.keyword)
);
// 1. 키워드 검색
const matchKeyword = !filters.keyword || searchKeys.some(key => {
const schemaItem = (ASSET_SCHEMA as any)[key];
if (schemaItem) {
return String(item[schemaItem.key] || item[schemaItem.db] || '').toLowerCase().includes(filters.keyword);
}
return String(item[key] || '').toLowerCase().includes(filters.keyword);
});
// 2. 부서 필터링 (사용자 페이지 dept_name, 자산 페이지 current_dept)
let matchDept = true;
if (filters.dept) {
const itemDept = item.dept_name || item[ASSET_SCHEMA.CURRENT_DEPT.key] || item[ASSET_SCHEMA.CURRENT_DEPT.db];
matchDept = itemDept === filters.dept;
}
// 3. 직무 필터링
let matchPosition = true;
if (filters.position) {
matchPosition = item.position === filters.position;
}
// 4. 나머지 필터링
const matchCorp = !filters.corp || (item[ASSET_SCHEMA.PURCHASE_CORP.key] || item[ASSET_SCHEMA.PURCHASE_CORP.db]) === filters.corp;
const matchDept = !filters.dept || (item[ASSET_SCHEMA.CURRENT_DEPT.key] || item[ASSET_SCHEMA.CURRENT_DEPT.db]) === filters.dept;
const matchLoc = !filters.loc || (item[ASSET_SCHEMA.LOCATION.key] || item[ASSET_SCHEMA.LOCATION.db]) === filters.loc;
const matchField = !filters.field || (item[ASSET_SCHEMA.SW_FIELD.key] || item[ASSET_SCHEMA.SW_FIELD.db]) === filters.field;
const matchType = !filters.type || (item[ASSET_SCHEMA.ASSET_TYPE.key] || item[ASSET_SCHEMA.ASSET_TYPE.db]) === filters.type;
const matchStatus = !filters.status || (item[ASSET_SCHEMA.HW_STATUS.key] || item[ASSET_SCHEMA.HW_STATUS.db]) === filters.status;
return matchKeyword && matchCorp && matchDept && matchLoc && matchField && matchType && matchStatus;
return matchKeyword && matchCorp && matchDept && matchLoc && matchField && matchType && matchStatus && matchPosition;
});
}

View File

@@ -242,7 +242,8 @@ export function calculatePcScoreDeductive(cpu: string, ram: string, gpu: string,
gpuDeduction = 0;
} else if (
gpuUpper.includes('RTX 3070') || gpuUpper.includes('RTX 3060') || gpuUpper.includes('RTX 2060') ||
gpuUpper.includes('RTX A2000') || gpuUpper.includes('RTX A3000') || gpuUpper.includes('QUADRO')
gpuUpper.includes('RTX A2000') || gpuUpper.includes('RTX A3000') || gpuUpper.includes('QUADRO') ||
gpuUpper.includes('RTX 4060') || gpuUpper.includes('RTX 4050')
) {
gpuDeduction = 5;
} else if (
@@ -289,10 +290,12 @@ export function calculatePcScoreDeductive(cpu: string, ram: string, gpu: string,
* 성능 점수 기준 등급 뱃지 메타 정보 가져오기
*/
export function getPcGrade(score: number, isWin11Incompatible?: boolean): { name: string; class: string; color: string } {
// Windows 11 업그레이드 불가 PC는 성능 점수와 무관하게 교체 대상으로 분류
if (isWin11Incompatible) return { name: '교체 대상', class: 'badge-danger', color: '#EF4444' };
if (score >= 85) return { name: '최상급', class: 'b-purple', color: '#7C3AED' };
if (score >= 70) return { name: '상급', class: 'b-primary', color: '#4F46E5' };
if (score >= 40) return { name: '중급', class: 'b-green', color: '#10B981' };
if (score >= 20 && !isWin11Incompatible) return { name: '보급', class: 'b-yellow', color: '#F59E0B' };
if (score >= 20) return { name: '보급', class: 'b-yellow', color: '#F59E0B' };
return { name: '교체 대상', class: 'badge-danger', color: '#EF4444' };
}