merge: remote main updates into local main
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { state, saveAsset, deleteAsset } from '../../core/state';
|
||||
import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema';
|
||||
import { calculatePcScoreDeductive, getPcGrade } from '../../core/utils';
|
||||
import {
|
||||
generateOptionsHTML,
|
||||
setFieldValue,
|
||||
@@ -13,6 +14,7 @@ import { BaseModal } from './BaseModal';
|
||||
|
||||
class HwAssetModal extends BaseModal {
|
||||
private dynamicMapConfig: Record<string, any[]> = {};
|
||||
private masterComponents: any[] = [];
|
||||
|
||||
constructor() {
|
||||
super('hw', '자산 상세 정보');
|
||||
@@ -24,6 +26,39 @@ class HwAssetModal extends BaseModal {
|
||||
const btnStyle = `padding: 0 16px; display: inline-flex; align-items: center; justify-content: center; font-weight: 600; white-space: nowrap; cursor: pointer; ${sharedStyle}`;
|
||||
|
||||
return `
|
||||
<style>
|
||||
.autocomplete-list {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
max-height: 150px;
|
||||
overflow-y: auto;
|
||||
background-color: white;
|
||||
border: 1px solid var(--border-color, #E2E8F0);
|
||||
border-top: none;
|
||||
border-radius: 0 0 4px 4px;
|
||||
z-index: 1000;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.autocomplete-item {
|
||||
padding: 8px 12px;
|
||||
font-size: 13px;
|
||||
color: #334155;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.autocomplete-item:hover {
|
||||
background-color: #F1F5F9;
|
||||
color: #1E5149;
|
||||
font-weight: 600;
|
||||
}
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
</style>
|
||||
<div id="hw-asset-modal" class="modal-overlay hidden">
|
||||
<div class="modal-content wide">
|
||||
<div class="modal-header">
|
||||
@@ -131,22 +166,31 @@ class HwAssetModal extends BaseModal {
|
||||
<label>${ASSET_SCHEMA.OS.ui}</label>
|
||||
<input type="text" id="hw-os" name="os" style="${inputStyle}" />
|
||||
</div>
|
||||
<div class="form-group spec-only">
|
||||
<div class="form-group spec-only" style="position: relative;">
|
||||
<label>${ASSET_SCHEMA.CPU.ui}</label>
|
||||
<input type="text" id="hw-cpu" name="cpu" style="${inputStyle}" />
|
||||
<input type="text" id="hw-cpu" name="cpu" autocomplete="off" placeholder="CPU 부품 검색..." style="${inputStyle}" />
|
||||
<div id="hw-cpu-autocomplete" class="autocomplete-list hidden"></div>
|
||||
</div>
|
||||
<div class="form-group spec-only">
|
||||
<div class="form-group spec-only" style="position: relative;">
|
||||
<label>${ASSET_SCHEMA.RAM.ui}</label>
|
||||
<input type="text" id="hw-ram" name="ram" style="${inputStyle}" />
|
||||
<input type="text" id="hw-ram" name="ram" autocomplete="off" placeholder="RAM 부품 검색..." style="${inputStyle}" />
|
||||
<div id="hw-ram-autocomplete" class="autocomplete-list hidden"></div>
|
||||
</div>
|
||||
<div class="form-group spec-only">
|
||||
<div class="form-group spec-only" style="position: relative;">
|
||||
<label>${ASSET_SCHEMA.GPU.ui}</label>
|
||||
<input type="text" id="hw-gpu" name="gpu" style="${inputStyle}" />
|
||||
<input type="text" id="hw-gpu" name="gpu" autocomplete="off" placeholder="GPU 부품 검색..." style="${inputStyle}" />
|
||||
<div id="hw-gpu-autocomplete" class="autocomplete-list hidden"></div>
|
||||
</div>
|
||||
<div class="form-group spec-only">
|
||||
<label>${ASSET_SCHEMA.MAINBOARD.ui}</label>
|
||||
<input type="text" id="hw-mainboard" name="mainboard" style="${inputStyle}" />
|
||||
</div>
|
||||
<div class="form-group spec-only">
|
||||
<label>성능 등급</label>
|
||||
<div id="hw-pc-grade-container" style="display: flex; align-items: center; height: 38px;">
|
||||
<span class="badge b-yellow" id="hw-pc-grade-badge">-</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group monitor-only">
|
||||
<label>${ASSET_SCHEMA.MONITOR_INCH.ui}</label>
|
||||
<input type="text" id="hw-monitor_inch" name="monitor_inch" style="${inputStyle}" />
|
||||
@@ -257,6 +301,18 @@ class HwAssetModal extends BaseModal {
|
||||
bindLocationEvents('hw-bldg-select', 'hw-location_detail', '', '');
|
||||
applyDateMask(document.getElementById('hw-purchase_date') as HTMLInputElement);
|
||||
|
||||
this.fetchMasterComponents().then(() => {
|
||||
this.bindAutocomplete('hw-cpu', 'hw-cpu-autocomplete', 'CPU');
|
||||
this.bindAutocomplete('hw-ram', 'hw-ram-autocomplete', 'RAM');
|
||||
this.bindAutocomplete('hw-gpu', 'hw-gpu-autocomplete', 'GPU');
|
||||
});
|
||||
|
||||
const specInputs = ['hw-cpu', 'hw-ram', 'hw-gpu', 'hw-purchase_date'];
|
||||
specInputs.forEach(id => {
|
||||
document.getElementById(id)?.addEventListener('input', () => this.updatePcGradeBadge());
|
||||
document.getElementById(id)?.addEventListener('change', () => this.updatePcGradeBadge());
|
||||
});
|
||||
|
||||
categorySelect.addEventListener('change', () => {
|
||||
const types = CATEGORY_TYPE_MAP[categorySelect.value] || [];
|
||||
typeSelect.innerHTML = types.length > 0 ? generateOptionsHTML(types, '', true) : '<option value="">구분을 먼저 선택하세요</option>';
|
||||
@@ -373,6 +429,35 @@ class HwAssetModal extends BaseModal {
|
||||
return;
|
||||
}
|
||||
|
||||
// CPU, RAM, GPU 마스터 테이블 기반 유효성 검사 (완전 강제 방식)
|
||||
const category = categorySelect.value;
|
||||
const type = typeSelect.value;
|
||||
const specCategories = ['PC', '서버', '노트북', '스토리지', '워크스테이션'];
|
||||
const hasSpec = specCategories.includes(category) || type.includes('서버PC');
|
||||
|
||||
if (hasSpec) {
|
||||
const cpuVal = (document.getElementById('hw-cpu') as HTMLInputElement)?.value || '';
|
||||
const ramVal = (document.getElementById('hw-ram') as HTMLInputElement)?.value || '';
|
||||
const gpuVal = (document.getElementById('hw-gpu') as HTMLInputElement)?.value || '';
|
||||
|
||||
const cpuMaster = this.masterComponents.filter(c => c.category === 'CPU').map(c => c.component_name);
|
||||
const ramMaster = this.masterComponents.filter(c => c.category === 'RAM').map(c => c.component_name);
|
||||
const gpuMaster = this.masterComponents.filter(c => c.category === 'GPU').map(c => c.component_name);
|
||||
|
||||
if (cpuVal && !cpuMaster.includes(cpuVal)) {
|
||||
alert(`[입력 오류] '${cpuVal}'은(는) 마스터 테이블에 존재하지 않는 CPU 부품명입니다. 자동완성 추천 목록에서 올바른 부품명을 골라 선택해 주세요.`);
|
||||
return;
|
||||
}
|
||||
if (ramVal && !ramMaster.includes(ramVal)) {
|
||||
alert(`[입력 오류] '${ramVal}'은(는) 마스터 테이블에 존재하지 않는 RAM 부품명입니다. 자동완성 추천 목록에서 올바른 부품명을 골라 선택해 주세요.`);
|
||||
return;
|
||||
}
|
||||
if (gpuVal && !gpuMaster.includes(gpuVal)) {
|
||||
alert(`[입력 오류] '${gpuVal}'은(는) 마스터 테이블에 존재하지 않는 GPU 부품명입니다. 자동완성 추천 목록에서 올바른 부품명을 골라 선택해 주세요.`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 동적 볼륨 데이터 수집
|
||||
const vols: any[] = [];
|
||||
document.querySelectorAll('#hw-volume-container .volume-row').forEach((row, idx) => {
|
||||
@@ -614,6 +699,7 @@ class HwAssetModal extends BaseModal {
|
||||
parseAndSetLocation(asset.location || '', asset.location_detail || '', 'hw-bldg-select', 'hw-location_detail');
|
||||
this.renderHistory(asset.id);
|
||||
this.applyRoleVisibility();
|
||||
this.updatePcGradeBadge();
|
||||
}
|
||||
|
||||
protected onAfterOpen(asset: any, mode: string): void {
|
||||
@@ -911,6 +997,77 @@ class HwAssetModal extends BaseModal {
|
||||
if (cat === 'PC부품') return 'pcParts';
|
||||
return (cat === 'PC' || code.startsWith('PC')) ? 'pc' : 'officeSupplies';
|
||||
}
|
||||
|
||||
private async fetchMasterComponents(): Promise<void> {
|
||||
try {
|
||||
const res = await fetch(`http://${location.hostname}:3000/api/hardware-components`);
|
||||
this.masterComponents = await res.json();
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch master components:', err);
|
||||
}
|
||||
}
|
||||
|
||||
private bindAutocomplete(inputId: string, autocompleteId: string, category: string) {
|
||||
const input = document.getElementById(inputId) as HTMLInputElement;
|
||||
const list = document.getElementById(autocompleteId) as HTMLDivElement;
|
||||
if (!input || !list) return;
|
||||
|
||||
const showList = (filterText: string = '') => {
|
||||
if (!this.isEditMode) return;
|
||||
const items = this.masterComponents.filter(c => c.category === category);
|
||||
const filtered = filterText
|
||||
? items.filter(c => c.component_name.toLowerCase().includes(filterText.toLowerCase()))
|
||||
: items;
|
||||
|
||||
if (filtered.length === 0) {
|
||||
list.innerHTML = '<div class="autocomplete-item" style="color: #94a3b8; cursor: default;">검색 결과 없음</div>';
|
||||
} else {
|
||||
list.innerHTML = filtered.map(c => `<div class="autocomplete-item" data-val="${c.component_name}">${c.component_name}</div>`).join('');
|
||||
}
|
||||
list.classList.remove('hidden');
|
||||
};
|
||||
|
||||
input.addEventListener('focus', () => {
|
||||
showList(input.value);
|
||||
});
|
||||
|
||||
input.addEventListener('input', () => {
|
||||
showList(input.value);
|
||||
});
|
||||
|
||||
// 아이템 클릭 이벤트 위임
|
||||
list.addEventListener('mousedown', (e) => {
|
||||
const item = (e.target as HTMLElement).closest('.autocomplete-item');
|
||||
if (item && item.getAttribute('data-val')) {
|
||||
input.value = item.getAttribute('data-val') || '';
|
||||
list.classList.add('hidden');
|
||||
this.updatePcGradeBadge(); // 뱃지 즉시 업데이트
|
||||
}
|
||||
});
|
||||
|
||||
// 아웃사이드 클릭 시 닫기
|
||||
document.addEventListener('mousedown', (e) => {
|
||||
if (e.target !== input && !list.contains(e.target as Node)) {
|
||||
list.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private updatePcGradeBadge(): void {
|
||||
const cpu = (document.getElementById('hw-cpu') as HTMLInputElement)?.value || '';
|
||||
const ram = (document.getElementById('hw-ram') as HTMLInputElement)?.value || '';
|
||||
const gpu = (document.getElementById('hw-gpu') as HTMLInputElement)?.value || '';
|
||||
const date = (document.getElementById('hw-purchase_date') as HTMLInputElement)?.value || '';
|
||||
|
||||
const score = calculatePcScoreDeductive(cpu, ram, gpu, date);
|
||||
const grade = getPcGrade(score);
|
||||
|
||||
const badge = document.getElementById('hw-pc-grade-badge');
|
||||
if (badge) {
|
||||
badge.textContent = `${grade.name} (${score}점)`;
|
||||
badge.className = `badge ${grade.class}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const hwModal = new HwAssetModal();
|
||||
|
||||
Reference in New Issue
Block a user