-
-
+
+
-
- ${ASSET_SCHEMA.MONITOR_INCH.ui}
-
-
-
-
디스크(용량) 정보
-
-
-
-
네트워크 및 원격 접속 정보
-
-
-
-
설치 위치
-
- 건물/위치
- ${generateOptionsHTML(Object.keys(LOCATION_DATA))}
-
-
-
-
-
구매 및 증빙 정보
-
- ${ASSET_SCHEMA.PURCHASE_DATE.ui}
-
-
-
- ${ASSET_SCHEMA.PURCHASE_VENDOR.ui}
-
-
-
- ${ASSET_SCHEMA.PURCHASE_AMOUNT.ui}
-
-
-
-
@@ -233,10 +75,7 @@ class HwAssetModal extends BaseModal {
@@ -255,85 +94,73 @@ class HwAssetModal extends BaseModal {
`;
}
+ private renderDynamicForm(category: string) {
+ const dynamicContainer = document.getElementById('dynamic-form-content');
+ const commonContainer = document.getElementById('common-form-content');
+ const badgeContainer = document.getElementById('hw-category-badge-container');
+ if (!dynamicContainer || !commonContainer || !badgeContainer) return;
+
+ if (!category) {
+ dynamicContainer.innerHTML = `
자산 구분을 선택하면 해당 서식이 표시됩니다.
`;
+ commonContainer.classList.add('hidden');
+ badgeContainer.innerHTML = '';
+ return;
+ }
+
+ commonContainer.classList.remove('hidden');
+ badgeContainer.innerHTML = `
${category} `;
+
+ if (category === 'PC' || category === '노트북') {
+ dynamicContainer.innerHTML = renderPcForm();
+ } else if (category === '서버' || category === '스토리지' || category === '네트워크') {
+ dynamicContainer.innerHTML = renderServerForm();
+ } else {
+ // 기타 하드웨어 (업무지원장비 등) - 서버 폼의 기본 필드를 재활용하거나 범용 폼 사용
+ dynamicContainer.innerHTML = renderServerForm();
+ }
+
+ // 폼 변경 후 이벤트 재바인딩
+ this.rebindDynamicEvents();
+ }
+
+ private rebindDynamicEvents() {
+ const bldgSelect = document.getElementById('hw-bldg-select') as HTMLSelectElement;
+ if (bldgSelect) {
+ bindLocationEvents('hw-bldg-select', 'hw-location_detail', '', '');
+ bldgSelect.addEventListener('change', () => setTimeout(() => this.updateMapButtonVisibility(), 100));
+ document.getElementById('hw-location_detail')?.addEventListener('change', () => this.updateMapButtonVisibility());
+ }
+
+ const purchaseDate = document.getElementById('hw-purchase_date') as HTMLInputElement;
+ if (purchaseDate) applyDateMask(purchaseDate);
+
+ // 파일 업로드 이벤트 재연동
+ const fileInput = document.getElementById('hw-approval_document_file') as HTMLInputElement;
+ fileInput?.addEventListener('change', async (e) => this.handleFileUpload(e));
+
+ // 코드 생성 이벤트 재연동
+ document.getElementById('btn-gen-hw-code')?.addEventListener('click', () => this.handleGenerateCode());
+
+ // 위치 맵 관련 이벤트
+ document.getElementById('btn-reg-loc-map')?.addEventListener('click', (e) => this.handleRegLocMap(e));
+ document.getElementById('btn-view-loc-map')?.addEventListener('click', (e) => this.handleViewLocMap(e));
+ }
+
protected initChildLogic(onSave: () => void, closeModals: () => void): void {
const saveBtn = document.getElementById('btn-save-hw-asset')!;
const revertBtn = document.getElementById('btn-revert-hw-edit')!;
const deleteBtn = document.getElementById('btn-delete-hw-asset')!;
const categorySelect = document.getElementById('hw-category') as HTMLSelectElement;
const typeSelect = document.getElementById('hw-asset_type') as HTMLSelectElement;
- const bldgSelect = document.getElementById('hw-bldg-select') as HTMLSelectElement;
- const detailSelect = document.getElementById('hw-location_detail') as HTMLSelectElement;
-
- 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) : '
구분을 먼저 선택하세요 ';
- this.applyRoleVisibility();
- });
-
- typeSelect.addEventListener('change', () => {
- this.applyRoleVisibility();
- });
-
- document.getElementById('btn-gen-hw-code')?.addEventListener('click', async () => {
- const type = typeSelect.value;
const cat = categorySelect.value;
- if (!type) { alert('유형을 먼저 선택해주세요.'); return; }
-
- const purchaseDateEl = document.getElementById('hw-purchase_date') as HTMLInputElement;
- const purchaseDate = purchaseDateEl?.value || '';
-
- if (!purchaseDate) {
- alert('구매일자를 먼저 입력해야 자산번호 생성이 가능합니다.');
- purchaseDateEl?.focus();
- return;
- }
-
- // 유형 기반 매핑 우선, 없으면 구분 기반, 그래도 없으면 ETC
- const prefix = TYPE_PREFIX_MAP[type] || TYPE_PREFIX_MAP[cat] || 'ETC';
- try {
- const res = await fetch(`http://${location.hostname}:3000/api/generate-asset-code?prefix=${prefix}&purchaseDate=${purchaseDate}`);
- const data = await res.json();
- if (data.nextCode) setFieldValue('hw-asset_code', data.nextCode);
- } catch (err) { console.error('코드 생성 실패:', err); }
- });
-
- bldgSelect.addEventListener('change', () => setTimeout(() => this.updateMapButtonVisibility(), 100));
- detailSelect.addEventListener('change', () => this.updateMapButtonVisibility());
-
- document.getElementById('btn-reg-loc-map')?.addEventListener('click', async (e) => {
- e.preventDefault(); e.stopPropagation();
- await this.fetchMapConfig();
- const images = this.getImagesForLocation(bldgSelect.value, detailSelect.value);
- if (images) this.openImagePicker(images, `${detailSelect.value} 위치 등록`);
- });
-
- document.getElementById('btn-view-loc-map')?.addEventListener('click', async (e) => {
- e.preventDefault(); e.stopPropagation();
- await this.fetchMapConfig();
- const x = getFieldValue('hw-loc_x');
- const y = getFieldValue('hw-loc_y');
- const savedImg = getFieldValue('hw-location_photo');
- const bldg = getFieldValue('hw-bldg-select');
- const detail = getFieldValue('hw-location_detail');
- const images = this.getImagesForLocation(bldg, detail);
- if (images) {
- const imgPath = savedImg && images.includes(savedImg) ? savedImg : images[0];
- this.openImagePreview(imgPath, `${detail} 위치 확인`, x, y);
+ const types = CATEGORY_TYPE_MAP[cat] || [];
+ typeSelect.innerHTML = types.length > 0 ? generateOptionsHTML(types, '', true) : '
구분을 먼저 선택하세요 ';
+ this.renderDynamicForm(cat);
+ if (this.currentAsset) {
+ // 새로 생성된 폼에 기존 데이터 다시 채우기
+ this.fillFormData(this.currentAsset);
}
});
@@ -345,43 +172,8 @@ class HwAssetModal extends BaseModal {
});
revertBtn.addEventListener('click', () => {
- this.isEditMode = false;
this.setEditLockMode('view');
if (this.currentAsset) this.fillFormData(this.currentAsset);
- this.updateMapButtonVisibility();
- this.toggleEditOnlyBtns(false);
- });
-
- // 동적 기능 이벤트 연결
- document.getElementById('btn-add-volume')?.addEventListener('click', () => this.addVolumeRow());
- document.getElementById('btn-add-remote-info')?.addEventListener('click', () => this.addRemoteInfoRow());
-
- const fileInput = document.getElementById('hw-approval_document_file') as HTMLInputElement;
- const fileNameDisplay = document.getElementById('hw-file-name-display');
- const fileLinkContainer = document.getElementById('hw-file-link-container');
-
- fileInput?.addEventListener('change', async (e) => {
- const file = (e.target as HTMLInputElement).files?.[0];
- if (!file) return;
- if (fileNameDisplay) fileNameDisplay.textContent = file.name;
- const reader = new FileReader();
- reader.onload = async () => {
- try {
- const res = await fetch(`http://${location.hostname}:3000/api/upload`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ fileName: file.name, fileData: reader.result })
- });
- const data = await res.json();
- if (data.success) {
- setFieldValue('hw-approval_document', data.filePath);
- if (fileLinkContainer) {
- fileLinkContainer.innerHTML = `
[업로드 완료: 파일 보기] `;
- }
- }
- } catch (err) { console.error('파일 업로드 실패:', err); alert('파일 업로드 중 오류가 발생했습니다.'); }
- };
- reader.readAsDataURL(file);
});
saveBtn.addEventListener('click', async () => {
@@ -391,291 +183,151 @@ class HwAssetModal extends BaseModal {
this.isEditMode = true;
this.updateMapButtonVisibility();
this.toggleFileUploadUI(true);
- this.toggleEditOnlyBtns(true);
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) => {
- const type = (row.querySelector('.vol-type') as HTMLSelectElement).value;
- const cap = (row.querySelector('.vol-cap') as HTMLInputElement).value;
- const unit = (row.querySelector('.vol-unit') as HTMLSelectElement).value;
- if (cap) vols.push({ type, capacity: parseFloat(cap), unit, slot: idx + 1 });
- });
- setFieldValue('hw-volumes-data', JSON.stringify(vols));
-
- // 동적 네트워크/원격 데이터 수집
- const nets: any[] = [];
- document.querySelectorAll('#hw-remote-info-container .remote-info-row').forEach(row => {
- const type = (row.querySelector('.ri-type') as HTMLSelectElement).value;
- const val1 = (row.querySelector('.ri-val1') as HTMLInputElement).value;
-
- if (type === 'IP' && val1) {
- const tool = (row.querySelector('.ri-tool') as HTMLSelectElement)?.value || '';
- const id = (row.querySelector('.ri-id') as HTMLInputElement)?.value || '';
- const pw = (row.querySelector('.ri-pw') as HTMLInputElement)?.value || '';
- const val2Str = (id || pw) ? JSON.stringify({ id, pw }) : '';
- nets.push({ type: 'IP', name: tool, val1: val1, val2: val2Str });
- } else if (type === 'MAC' && val1) {
- nets.push({ type: 'MAC', name: 'MAC 주소', val1: val1, val2: '' });
- }
- });
- setFieldValue('hw-remotes-data', JSON.stringify(nets));
-
const formData = new FormData(this.formEl!);
const updated = { ...this.currentAsset };
formData.forEach((value, key) => { if (key !== 'id') updated[key] = value; });
- updated.location = getFieldValue('hw-bldg-select');
+ const bldgSelect = document.getElementById('hw-bldg-select') as HTMLSelectElement;
+ if (bldgSelect) updated.location = bldgSelect.value;
if (await saveAsset(this.getCategoryKey(updated), updated)) {
- alert(UI_TEXT.MESSAGES.SAVE_SUCCESS);
+ alert(UI_TEXT.MESSAGES.SAVE_SUCCESS);
onSave(); this.close(); closeModals();
}
});
}
- private addVolumeRow(vol: any = { type: 'SSD', capacity: '', unit: 'GB' }) {
- const container = document.getElementById('hw-volume-container');
- if (!container) return;
- const row = document.createElement('div');
- row.className = 'volume-row';
- row.style.display = 'flex'; row.style.gap = '8px'; row.style.alignItems = 'center';
- const inputStyle = 'height: 38px !important; box-sizing: border-box !important; font-size: 13px; margin: 0; padding: 0 8px;';
- row.innerHTML = `
-
- SSD
- HDD
- NVMe
-
-
-
- GB
- TB
-
-
×
- `;
- row.querySelector('.btn-remove-row')?.addEventListener('click', () => row.remove());
- container.appendChild(row);
+ private async handleGenerateCode() {
+ const categorySelect = document.getElementById('hw-category') as HTMLSelectElement;
+ const cat = categorySelect.value;
+ if (!cat) { alert('구분을 먼저 선택해주세요.'); return; }
+ const prefix = TYPE_PREFIX_MAP[cat] || 'ETC';
+ const purchaseDate = (document.getElementById('hw-purchase_date') as HTMLInputElement)?.value || '';
+ try {
+ const res = await fetch(`http://${location.hostname}:3000/api/generate-asset-code?prefix=${prefix}&purchaseDate=${purchaseDate}`);
+ const data = await res.json();
+ if (data.nextCode) setFieldValue('hw-asset_code', data.nextCode);
+ } catch (err) { console.error('코드 생성 실패:', err); }
}
- private addRemoteInfoRow(info: any = { type: 'IP', name: '원격접속', val1: '', val2: '' }) {
- const container = document.getElementById('hw-remote-info-container');
- if (!container) return;
-
- // Parse val2 (which contains JSON with id and pw if type is IP)
- let parsedId = '';
- let parsedPw = '';
- if (info.type === 'IP' && info.val2) {
- try {
- const parsed = typeof info.val2 === 'string' ? JSON.parse(info.val2) : info.val2;
- parsedId = parsed.id || '';
- parsedPw = parsed.pw || '';
- } catch (e) {
- // Legacy fallback if val2 was just a simple string
- parsedId = info.val2;
+ private async handleFileUpload(e: Event) {
+ const file = (e.target as HTMLInputElement).files?.[0];
+ if (!file) return;
+ const fileNameDisplay = document.getElementById('hw-file-name-display');
+ const fileLinkContainer = document.getElementById('hw-file-link-container');
+ if (fileNameDisplay) fileNameDisplay.textContent = file.name;
+ const reader = new FileReader();
+ reader.onload = async () => {
+ try {
+ const res = await fetch(`http://${location.hostname}:3000/api/upload`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ fileName: file.name, fileData: reader.result })
+ });
+ const data = await res.json();
+ if (data.success) {
+ setFieldValue('hw-approval_document', data.filePath);
+ if (fileLinkContainer) {
+ fileLinkContainer.innerHTML = `
[업로드 완료: 파일 보기] `;
+ }
}
+ } catch (err) { console.error('파일 업로드 실패:', err); alert('파일 업로드 중 오류가 발생했습니다.'); }
+ };
+ reader.readAsDataURL(file);
+ }
+
+ private async handleRegLocMap(e: MouseEvent) {
+ e.preventDefault(); e.stopPropagation();
+ const bldgSelect = document.getElementById('hw-bldg-select') as HTMLSelectElement;
+ const detailSelect = document.getElementById('hw-location_detail') as HTMLSelectElement;
+ await this.fetchMapConfig();
+ const images = this.getImagesForLocation(bldgSelect.value, detailSelect.value);
+ if (images) this.openImagePicker(images, `${detailSelect.value} 위치 등록`);
+ }
+
+ private async handleViewLocMap(e: MouseEvent) {
+ e.preventDefault(); e.stopPropagation();
+ await this.fetchMapConfig();
+ const x = getFieldValue('hw-loc_x');
+ const y = getFieldValue('hw-loc_y');
+ const savedImg = getFieldValue('hw-location_photo');
+ const bldg = getFieldValue('hw-bldg-select');
+ const detail = getFieldValue('hw-location_detail');
+ const images = this.getImagesForLocation(bldg, detail);
+ if (images) {
+ const imgPath = savedImg && images.includes(savedImg) ? savedImg : images[0];
+ this.openImagePreview(imgPath, `${detail} 위치 확인`, x, y);
}
-
- const row = document.createElement('div');
- row.className = 'remote-info-row';
-
- // First Line: Type & Address
- const line1 = document.createElement('div');
- line1.className = 'ri-line';
- line1.innerHTML = `
-
- IP 주소
- MAC 주소
-
-
-
×
- `;
-
- // Second Line: Tool & Credentials (Only for IP)
- const line2 = document.createElement('div');
- line2.className = 'ri-line ri-cred-line';
- if (info.type !== 'IP') line2.classList.add('hidden');
-
- line2.innerHTML = `
-
-
- 원격접속
- 리눅스
- 기타
-
-
-
-
- `;
-
- row.appendChild(line1);
- row.appendChild(line2);
-
- // Toggle logic
- const typeSelect = row.querySelector('.ri-type') as HTMLSelectElement;
- typeSelect.addEventListener('change', (e) => {
- const isIP = (e.target as HTMLSelectElement).value === 'IP';
- line2.classList.toggle('hidden', !isIP);
- if (!isIP) {
- (row.querySelector('.ri-id') as HTMLInputElement).value = '';
- (row.querySelector('.ri-pw') as HTMLInputElement).value = '';
- }
- });
-
- row.querySelector('.btn-remove-row')?.addEventListener('click', () => row.remove());
- container.appendChild(row);
- }
-
- private toggleEditOnlyBtns(isEdit: boolean) {
- ['btn-add-volume', 'btn-add-remote-info'].forEach(id => {
- const btn = document.getElementById(id);
- if (btn) btn.style.display = isEdit ? 'inline-flex' : 'none';
- });
- document.querySelectorAll('.edit-only-btn').forEach(btn => {
- (btn as HTMLElement).style.display = isEdit ? 'inline-flex' : 'none';
- });
-
- // 동적 생성된 필드들 (볼륨/원격정보)의 상태 일괄 토글
- const containers = ['#hw-volume-container', '#hw-remote-info-container'];
- containers.forEach(selector => {
- document.querySelectorAll(`${selector} input`).forEach(input => {
- if (isEdit) input.removeAttribute('readonly');
- else input.setAttribute('readonly', 'true');
- });
- document.querySelectorAll(`${selector} select`).forEach(select => {
- if (isEdit) select.removeAttribute('disabled');
- else select.setAttribute('disabled', 'true');
- });
- });
}
protected fillFormData(asset: any): void {
- setFieldValue('hw-id', asset.id);
- setFieldValue('hw-asset_code', asset.asset_code || '');
- setFieldValue('hw-purchase_corp', asset.purchase_corp || '');
+ // 1. 분류 먼저 설정 및 동적 폼 렌더링
setFieldValue('hw-category', asset.category || '');
+ this.renderDynamicForm(asset.category || '');
+
+ // 2. 타입 설정
const types = CATEGORY_TYPE_MAP[asset.category] || [];
const typeSelect = document.getElementById('hw-asset_type') as HTMLSelectElement;
if (typeSelect) typeSelect.innerHTML = types.length > 0 ? generateOptionsHTML(types, asset.asset_type, true) : '
구분을 먼저 선택하세요 ';
setFieldValue('hw-asset_type', asset.asset_type || '');
+
+ // 3. 나머지 데이터 채우기 (공통 및 동적 필드)
+ setFieldValue('hw-id', asset.id);
+ setFieldValue('hw-asset_code', asset.asset_code || '');
+ setFieldValue('hw-purchase_corp', asset.purchase_corp || '');
setFieldValue('hw-hw_status', asset.hw_status || '운영');
- setFieldValue('hw-service_type', asset.service_type || '외부');
- setFieldValue('hw-asset_purpose', asset.asset_purpose || '');
setFieldValue('hw-current_dept', asset.current_dept || '');
setFieldValue('hw-manager_primary', asset.manager_primary || '');
setFieldValue('hw-manager_secondary', asset.manager_secondary || '');
setFieldValue('hw-user_current', asset.user_current || '');
setFieldValue('hw-user_position', asset.user_position || '');
- setFieldValue('hw-previous_user', asset.previous_user || '');
+ setFieldValue('hw-emp_no', asset.emp_no || '');
setFieldValue('hw-model_name', asset.model_name || '');
- setFieldValue('hw-asset_mfr', asset.asset_mfr || '');
- setFieldValue('hw-os', asset.os || '');
setFieldValue('hw-cpu', asset.cpu || '');
setFieldValue('hw-ram', asset.ram || '');
+ setFieldValue('hw-os', asset.os || '');
setFieldValue('hw-gpu', asset.gpu || '');
- setFieldValue('hw-mainboard', asset.mainboard || '');
-
- // 동적 볼륨 렌더링
- const volumeContainer = document.getElementById('hw-volume-container');
- if (volumeContainer) volumeContainer.innerHTML = '';
- let vols = [];
- try { vols = asset.volumes ? (typeof asset.volumes === 'string' ? JSON.parse(asset.volumes) : asset.volumes) : []; } catch(e) {}
- vols.forEach((v: any) => this.addVolumeRow(v));
-
- // 통합 원격 접속 정보 렌더링 초기화 및 생성
- const remoteInfoContainer = document.getElementById('hw-remote-info-container');
- if (remoteInfoContainer) {
- remoteInfoContainer.innerHTML = '';
- let nets = [];
- try {
- nets = asset.remotes ? (typeof asset.remotes === 'string' ? JSON.parse(asset.remotes) : asset.remotes) : [];
- } catch(e) {}
-
- // Fallback: 서버에서 배열을 안 줬지만 기존 평탄화 데이터가 있는 경우
- if (nets.length === 0 && (asset.ip_address || asset.mac_address || asset.remote_tool || asset.remote_id)) {
- if (asset.ip_address) {
- const tool = asset.remote_tool || '원격접속';
- const creds = (asset.remote_id || asset.remote_pw) ? JSON.stringify({ id: asset.remote_id || '', pw: asset.remote_pw || '' }) : '';
- nets.push({ type: 'IP', name: tool, val1: asset.ip_address, val2: creds });
- }
- if (asset.mac_address) {
- nets.push({ type: 'MAC', name: 'MAC 주소', val1: asset.mac_address, val2: '' });
- }
- if (!asset.ip_address && (asset.remote_tool || asset.remote_id)) {
- const creds = JSON.stringify({ id: asset.remote_id || '', pw: asset.remote_pw || '' });
- nets.push({ type: 'IP', name: asset.remote_tool || '기타', val1: '', val2: creds });
- }
- }
- nets.forEach((n: any) => this.addRemoteInfoRow(n));
- }
-
+ setFieldValue('hw-mac_address', asset.mac_address || '');
+ setFieldValue('hw-ip_address', asset.ip_address || '');
+ setFieldValue('hw-ip_address_2', asset.ip_address_2 || '');
+ setFieldValue('hw-remote_tool', asset.remote_tool || '');
+ setFieldValue('hw-remote_id', asset.remote_id || '');
+ setFieldValue('hw-remote_pw', asset.remote_pw || '');
setFieldValue('hw-monitoring', asset.monitoring || '비대상');
- setFieldValue('hw-serial_num', asset.serial_num || '');
- setFieldValue('hw-monitor_inch', asset.monitor_inch || '');
- setFieldValue('hw-volume', asset.volume || '');
- setFieldValue('hw-asset_count', asset.asset_count || '');
setFieldValue('hw-purchase_date', asset.purchase_date || '');
setFieldValue('hw-purchase_vendor', asset.purchase_vendor || '');
setFieldValue('hw-purchase_amount', asset.purchase_amount || '');
setFieldValue('hw-approval_document', asset.approval_document || '');
+ setFieldValue('hw-asset_purpose', asset.asset_purpose || '');
const docName = document.getElementById('hw-file-name-display');
if (docName) docName.textContent = asset.approval_document ? asset.approval_document.split('/').pop() : '파일 선택...';
const fileLinkContainer = document.getElementById('hw-file-link-container');
if (fileLinkContainer && asset.approval_document) {
- fileLinkContainer.innerHTML = `
[파일 보기] `;
+ fileLinkContainer.innerHTML = `
[파일 보기] `;
} else if (fileLinkContainer) {
fileLinkContainer.innerHTML = '';
}
-
setFieldValue('hw-memo', asset.memo || '');
setFieldValue('hw-location_detail', asset.location_detail || '');
setFieldValue('hw-loc_x', asset.loc_x || '');
setFieldValue('hw-loc_y', asset.loc_y || '');
setFieldValue('hw-location_photo', asset.location_photo || asset.loc_img || '');
- parseAndSetLocation(asset.location || '', asset.location_detail || '', 'hw-bldg-select', 'hw-location_detail');
+
+ const bldgSelect = document.getElementById('hw-bldg-select') as HTMLSelectElement;
+ if (bldgSelect) {
+ 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 {
const genBtn = document.getElementById('btn-gen-hw-code');
if (genBtn) genBtn.style.display = (mode === 'add') ? 'inline-flex' : 'none';
this.toggleFileUploadUI(mode !== 'view');
- this.toggleEditOnlyBtns(mode !== 'view');
- this.updateMapButtonVisibility(asset);
- this.applyRoleVisibility();
+ this.updateMapButtonVisibility();
}
private toggleFileUploadUI(showUpload: boolean) {
@@ -683,50 +335,30 @@ class HwAssetModal extends BaseModal {
if (fileBtn) fileBtn.style.display = showUpload ? 'inline-flex' : 'none';
}
- private applyRoleVisibility(): void {
- const category = (document.getElementById('hw-category') as HTMLSelectElement)?.value || '';
- const type = (document.getElementById('hw-asset_type') as HTMLSelectElement)?.value || '';
+ private updateMapButtonVisibility() {
+ const bldgSelect = document.getElementById('hw-bldg-select') as HTMLSelectElement;
+ const detailSelect = document.getElementById('hw-location_detail') as HTMLSelectElement;
+ if (!bldgSelect || !detailSelect) return;
- const infraCategories = ['서버', '저장매체', '네트워크', '보안장비', '공간정보장비'];
- const isInfra = infraCategories.includes(category) || type.includes('서버') || type.includes('저장시스템');
- const personalCategories = ['PC', '노트북', '모바일', '태블릿'];
- const isPersonal = (personalCategories.includes(category) || type.includes('개인PC') || type.includes('노트북')) && !type.includes('서버PC');
- const specCategories = ['PC', '서버', '노트북', '스토리지', '워크스테이션'];
- const hasSpec = specCategories.includes(category) || type.includes('서버PC');
- const noNetCategories = ['저장매체', '네트워크', '공간정보장비', 'PC부품', '사무가구'];
- const showNet = (isInfra || isPersonal) && !noNetCategories.includes(category);
- const hasSN = !['사무가구', 'PC부품'].includes(category);
- const isParts = ['PC부품', '사무가구'].includes(category);
- const showRemote = category === '서버' || type.includes('서버');
-
- document.querySelectorAll('.remote-section, .remote-field, .monitoring-field').forEach(el => (el as HTMLElement).style.display = showRemote ? '' : 'none');
- document.querySelectorAll('.net-only').forEach(el => (el as HTMLElement).style.display = showNet ? '' : 'none');
- document.querySelectorAll('.spec-only').forEach(el => (el as HTMLElement).style.display = hasSpec ? '' : 'none');
- document.querySelectorAll('.location-section, .location-field').forEach(el => (el as HTMLElement).style.display = (isInfra || category === '공간정보장비') ? '' : 'none');
- document.querySelectorAll('.org-user-section, .org-user-field').forEach(el => (el as HTMLElement).style.display = (isPersonal || isParts || category === '업무지원장비') ? '' : 'none');
- document.querySelectorAll('.personal-only').forEach(el => (el as HTMLElement).style.display = isPersonal ? '' : 'none');
- document.querySelectorAll('.sn-only').forEach(el => (el as HTMLElement).style.display = hasSN ? '' : 'none');
- document.querySelectorAll('.monitor-only').forEach(el => (el as HTMLElement).style.display = type.includes('모니터') ? '' : 'none');
- document.querySelectorAll('.parts-only').forEach(el => (el as HTMLElement).style.display = isParts ? '' : 'none');
- }
-
- private updateMapButtonVisibility(asset?: any) {
- const bldg = asset ? (asset.location || '') : getFieldValue('hw-bldg-select');
- const detail = asset ? (asset.location_detail || '') : getFieldValue('hw-location_detail');
- const x = asset ? (asset.loc_x || '') : getFieldValue('hw-loc_x');
- const y = asset ? (asset.loc_y || '') : getFieldValue('hw-loc_y');
+ const bldg = bldgSelect.value;
+ const detail = detailSelect.value;
+ const x = getFieldValue('hw-loc_x');
+ const y = getFieldValue('hw-loc_y');
const hasCoords = (x !== '' && y !== '' && x !== 'null' && y !== 'null');
const hasImage = !!this.getImagesForLocation(bldg, detail);
const regLocBtn = document.getElementById('btn-reg-loc-map')!;
const viewLocBtn = document.getElementById('btn-view-loc-map')!;
- if (hasImage && this.isEditMode) regLocBtn.style.display = 'inline-flex'; else regLocBtn.style.display = 'none';
- if (hasImage && hasCoords) {
- viewLocBtn.style.display = 'inline-flex';
- viewLocBtn.style.pointerEvents = 'auto';
- viewLocBtn.style.opacity = '1';
+ if (hasImage && this.isEditMode) {
+ regLocBtn.classList.remove('hidden');
} else {
- viewLocBtn.style.display = 'none';
+ regLocBtn.classList.add('hidden');
+ }
+
+ if (hasImage && hasCoords) {
+ viewLocBtn.classList.remove('hidden');
+ } else {
+ viewLocBtn.classList.add('hidden');
}
}
@@ -754,110 +386,40 @@ class HwAssetModal extends BaseModal {
overlay.className = 'image-picker-overlay';
const renderContent = () => {
const imgPath = imagePaths[currentIdx];
- const isMulti = imagePaths.length > 1;
- const isHtmlMap = imgPath.toLowerCase().endsWith('.html');
- const digitalMap = isHtmlMap ? '' : this.generateDynamicSVG(imgPath);
-
+ const digitalMap = this.generateDynamicSVG(imgPath);
overlay.innerHTML = `
-
-
- ${isMulti ? `
-
◀
-
▶
- ` : ''}
-
- ${isHtmlMap
- ? `
`
- : `
${digitalMap}
`
- }
+
+
+
+
+
+
+
+
${digitalMap}
+
+
-
- `;
-
+
+
`;
let selectedX = ''; let selectedY = '';
-
- if (isMulti) {
- overlay.querySelector('.picker-nav.prev')?.addEventListener('click', (e) => { e.stopPropagation(); if (currentIdx > 0) { currentIdx--; renderContent(); } });
- overlay.querySelector('.picker-nav.next')?.addEventListener('click', (e) => { e.stopPropagation(); if (currentIdx < imagePaths.length - 1) { currentIdx++; renderContent(); } });
- }
-
- if (isHtmlMap) {
- // HTML 지도 메시지 리스너
- const handleMessage = (e: MessageEvent) => {
- if (e.data.type === 'PICK_LOCATION') {
- selectedX = e.data.x;
- selectedY = e.data.y;
- }
- };
- window.addEventListener('message', handleMessage);
- overlay.querySelector('.btn-close-picker')?.addEventListener('click', () => { window.removeEventListener('message', handleMessage); overlay.remove(); });
- overlay.querySelector('#btn-picker-cancel')?.addEventListener('click', () => { window.removeEventListener('message', handleMessage); overlay.remove(); });
- overlay.querySelector('#btn-picker-save')?.addEventListener('click', () => {
- if (!selectedX || !selectedY) { alert('위치를 선택해주세요.'); return; }
- setFieldValue('hw-loc_x', selectedX); setFieldValue('hw-loc_y', selectedY);
- setFieldValue('hw-location_photo', imagePaths[currentIdx]);
- this.updateMapButtonVisibility();
- window.removeEventListener('message', handleMessage);
- overlay.remove();
- });
- } else {
- const container = overlay.querySelector('#picker-container') as HTMLElement;
- const marker = overlay.querySelector('#picker-marker') as HTMLElement;
- container.addEventListener('click', (e) => {
- const rectBound = container.getBoundingClientRect();
- const clickX = ((e.clientX - rectBound.left) / rectBound.width) * 100;
- const clickY = ((e.clientY - rectBound.top) / rectBound.height) * 100;
-
- let snapped = false;
- overlay.querySelectorAll('rect').forEach(rect => {
- const rx = parseFloat(rect.getAttribute('x') || '0');
- const ry = parseFloat(rect.getAttribute('y') || '0');
- const rw = parseFloat(rect.getAttribute('width') || '0');
- const rh = parseFloat(rect.getAttribute('height') || '0');
-
- if (clickX >= rx && clickX <= rx + rw && clickY >= ry && clickY <= ry + rh) {
- overlay.querySelectorAll('rect').forEach(r => {
- r.style.fill = 'rgba(30,81,73,0.05)';
- r.style.stroke = 'rgba(30,81,73,0.2)';
- r.style.strokeWidth = '0.2';
- });
- rect.style.fill = 'rgba(255, 61, 0, 0.4)';
- rect.style.stroke = '#FF3D00';
- rect.style.strokeWidth = '0.8';
-
- selectedX = rx.toFixed(2);
- selectedY = ry.toFixed(2);
-
- marker.style.left = `${rx + rw/2}%`;
- marker.style.top = `${ry + rh/2}%`;
- marker.classList.remove('hidden');
- snapped = true;
- }
- });
-
- if (!snapped) {
- selectedX = '';
- selectedY = '';
- marker.classList.add('hidden');
- overlay.querySelectorAll('rect').forEach(r => {
- r.style.fill = 'rgba(30,81,73,0.05)';
- r.style.stroke = 'rgba(30,81,73,0.2)';
- r.style.strokeWidth = '0.2';
- });
- }
- });
- overlay.querySelector('.btn-close-picker')?.addEventListener('click', () => overlay.remove());
- overlay.querySelector('#btn-picker-cancel')?.addEventListener('click', () => overlay.remove());
- overlay.querySelector('#btn-picker-save')?.addEventListener('click', () => {
- if (!selectedX || !selectedY) { alert('위치를 선택해주세요.'); return; }
- setFieldValue('hw-loc_x', selectedX); setFieldValue('hw-loc_y', selectedY);
- setFieldValue('hw-location_photo', imagePaths[currentIdx]);
- this.updateMapButtonVisibility(); overlay.remove();
- });
- }
+ const container = overlay.querySelector('#picker-container') as HTMLElement;
+ const marker = overlay.querySelector('#picker-marker') as HTMLElement;
+ container.addEventListener('click', (e) => {
+ const rect = container.getBoundingClientRect();
+ const x = ((e.clientX - rect.left) / rect.width) * 100;
+ const y = ((e.clientY - rect.top) / rect.height) * 100;
+ selectedX = x.toFixed(2); selectedY = y.toFixed(2);
+ marker.style.left = `${selectedX}%`; marker.style.top = `${selectedY}%`;
+ marker.classList.remove('hidden');
+ });
+ overlay.querySelector('.btn-close-picker')?.addEventListener('click', () => overlay.remove());
+ overlay.querySelector('#btn-picker-cancel')?.addEventListener('click', () => overlay.remove());
+ overlay.querySelector('#btn-picker-save')?.addEventListener('click', () => {
+ if (!selectedX || !selectedY) { alert('위치를 선택해주세요.'); return; }
+ setFieldValue('hw-loc_x', selectedX); setFieldValue('hw-loc_y', selectedY);
+ setFieldValue('hw-location_photo', imagePaths[currentIdx]);
+ this.updateMapButtonVisibility(); overlay.remove();
+ });
};
renderContent(); document.body.appendChild(overlay);
}
@@ -867,21 +429,21 @@ class HwAssetModal extends BaseModal {
overlay.className = 'image-picker-overlay';
const isHtmlMap = imagePath.toLowerCase().endsWith('.html');
const digitalMap = isHtmlMap ? '' : this.generateDynamicSVG(imagePath);
-
- // HTML 지도인 경우 좌표를 쿼리 파라미터로 전달
const finalPath = isHtmlMap ? `${imagePath}?markerX=${x}&markerY=${y}` : imagePath;
overlay.innerHTML = `
-
-
-
- ${isHtmlMap
- ? `
`
- : `
${digitalMap}
`
- }
+
+
+
+
+
+
+
${digitalMap}
+
+
-
- `;
+
+
`;
document.body.appendChild(overlay);
if (!isHtmlMap && digitalMap) {
@@ -889,12 +451,11 @@ class HwAssetModal extends BaseModal {
overlay.querySelectorAll('rect').forEach(rect => {
const sx = parseFloat(rect.getAttribute('x') || '0');
const sy = parseFloat(rect.getAttribute('y') || '0');
- if (Math.abs(sx - curX) < 0.01 && Math.abs(sy - curY) < 0.01) {
- rect.style.fill = 'rgba(255, 61, 0, 0.4)'; rect.style.stroke = '#FF3D00'; rect.style.strokeWidth = '0.8';
- const w = parseFloat(rect.getAttribute('width') || '0');
- const h = parseFloat(rect.getAttribute('height') || '0');
- const marker = overlay.querySelector('#preview-marker') as HTMLElement;
- if (marker) { marker.style.left = `${sx + w/2}%`; marker.style.top = `${sy + h/2}%`; }
+ if (Math.abs(sx - curX) < 0.1 && Math.abs(sy - curY) < 0.1) {
+ rect.style.fill = 'rgba(255, 61, 0, 0.5)'; // 주황색 강조
+ rect.style.stroke = '#FF3D00';
+ rect.style.strokeWidth = '1.2';
+ rect.style.filter = 'drop-shadow(0 0 6px rgba(255, 61, 0, 0.8))';
}
});
}
@@ -905,51 +466,9 @@ class HwAssetModal extends BaseModal {
private renderHistory(assetId: string) {
const container = document.getElementById('hw-history-list');
if (!container) return;
-
- // state.masterData.logs에서 해당 자산의 이력 필터링 (최신순)
- const logs = (state.masterData.logs || [])
- .filter(l => l.asset_id === assetId)
- .sort((a, b) => new Date(b.created_at || b.log_date || '').getTime() - new Date(a.created_at || a.log_date || '').getTime());
-
- if (logs.length === 0) {
- container.innerHTML = '
기록된 변동 이력이 없습니다.
';
- return;
- }
-
- container.innerHTML = logs.map(l => {
- let eventTag = '기타';
- let tagClass = 'tag-default';
- let itemClass = '';
-
- switch(l.event_type) {
- case 'DEPT_CHANGE':
- eventTag = '조직'; tagClass = 'tag-dept'; itemClass = 'evt-dept';
- break;
- case 'USER_CHANGE':
- eventTag = '사용자'; tagClass = 'tag-user'; itemClass = 'evt-user';
- break;
- case 'ROLE_CHANGE':
- eventTag = '용도'; tagClass = 'tag-role'; itemClass = 'evt-role';
- break;
- case 'STATUS_CHANGE':
- eventTag = '상태'; tagClass = 'tag-status'; itemClass = 'evt-status';
- break;
- }
-
- // 화살표 기호(➔)를 사용하여 변경 사항 강조
- const formattedDetails = (l.details || '').replace(' -> ', '
➔ ');
-
- return `
-
-
-
${l.log_user || '시스템'}
-
${formattedDetails}
-
- `;
- }).join('');
+ const logs = (state.masterData.logs || []).filter(l => l.assetId === assetId || l.asset_id === assetId);
+ if (logs.length === 0) { container.innerHTML = '
이력이 없습니다.
'; return; }
+ container.innerHTML = logs.map(l => `
${l.log_date || l.date || ''}
${l.log_user || l.user || '시스템'}
${l.details}
`).join('');
}
private getCategoryKey(asset: any): string {
@@ -964,77 +483,6 @@ class HwAssetModal extends BaseModal {
if (cat === 'PC부품') return 'pcParts';
return (cat === 'PC' || code.startsWith('PC')) ? 'pc' : 'officeSupplies';
}
-
- private async fetchMasterComponents(): Promise
{
- 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 = '검색 결과 없음
';
- } else {
- list.innerHTML = filtered.map(c => `${c.component_name}
`).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();
diff --git a/src/components/Modal/PCFlowModal.ts b/src/components/Modal/PCFlowModal.ts
index fd09bca..74f0419 100644
--- a/src/components/Modal/PCFlowModal.ts
+++ b/src/components/Modal/PCFlowModal.ts
@@ -309,21 +309,17 @@ export class PCFlowModal {
private renderUserSuggestions(users: any[], container: HTMLElement, onSelect: (user: any) => void) {
container.innerHTML = '';
if (users.length === 0) {
- container.innerHTML = '일치하는 사원이 없습니다.
';
+ container.innerHTML = '일치하는 사원이 없습니다.
';
container.classList.remove('hidden');
return;
}
users.forEach(u => {
const item = document.createElement('div');
- item.style.padding = '8px 12px';
- item.style.cursor = 'pointer';
- item.style.fontSize = '13px';
- item.style.borderBottom = '1px solid #F3F4F6';
- item.className = 'suggestion-item';
+ item.className = 'autocomplete-item';
item.innerHTML = `
- ${u.user_name}
-