import { state, saveAsset, deleteAsset } from '../../core/state'; import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema'; import { generateOptionsHTML, setFieldValue, getFieldValue, parseAndSetLocation, bindLocationEvents, applyDateMask } from './ModalUtils'; import { CORP_LIST, LOCATION_DATA, CATEGORY_TYPE_MAP, HW_STATUS_LIST, ORG_LIST, IMAGE_LOCATIONS, TYPE_PREFIX_MAP } from './SharedData'; import { BaseModal } from './BaseModal'; class HwAssetModal extends BaseModal { private dynamicMapConfig: Record = {}; constructor() { super('hw', '자산 상세 정보'); } protected renderFrameHTML(): string { const sharedStyle = 'height: 38px !important; box-sizing: border-box !important; font-size: 13px; margin: 0;'; const inputStyle = sharedStyle; const btnStyle = `padding: 0 16px; display: inline-flex; align-items: center; justify-content: center; font-weight: 600; white-space: nowrap; cursor: pointer; ${sharedStyle}`; return ` `; } 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); 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 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); } }); 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); } }); deleteBtn.addEventListener('click', async () => { if (!this.currentAsset || !confirm(UI_TEXT.MESSAGES.CONFIRM_DELETE)) return; if (await deleteAsset(this.getCategoryKey(this.currentAsset), this.currentAsset.id)) { alert('성공적으로 삭제되었습니다.'); onSave(); this.close(); closeModals(); } }); revertBtn.addEventListener('click', () => { this.setEditLockMode('view'); if (this.currentAsset) this.fillFormData(this.currentAsset); this.updateMapButtonVisibility(); this.toggleEditOnlyBtns(false); }); // 동적 볼륨 추가 기능 연결 const btnAddVolume = document.getElementById('btn-add-volume')!; btnAddVolume.addEventListener('click', () => this.addVolumeRow()); 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 () => { if (!this.currentAsset) return; if (!this.isEditMode) { this.setEditLockMode('edit'); this.isEditMode = true; this.updateMapButtonVisibility(); this.toggleFileUploadUI(true); this.toggleEditOnlyBtns(true); 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 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'); if (await saveAsset(this.getCategoryKey(updated), updated)) { 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.style.display = 'flex'; row.style.gap = '8px'; row.style.alignItems = 'center'; row.className = 'volume-row'; const inputStyle = 'height: 38px !important; box-sizing: border-box !important; font-size: 13px; margin: 0; padding: 0 8px;'; row.innerHTML = ` `; row.querySelector('.btn-remove-vol')?.addEventListener('click', () => row.remove()); container.appendChild(row); } private toggleEditOnlyBtns(isEdit: boolean) { const addBtn = document.getElementById('btn-add-volume'); if (addBtn) addBtn.style.display = isEdit ? 'inline-flex' : 'none'; document.querySelectorAll('.edit-only-btn').forEach(btn => { (btn as HTMLElement).style.display = isEdit ? 'inline-flex' : 'none'; }); } protected fillFormData(asset: any): void { setFieldValue('hw-id', asset.id); setFieldValue('hw-asset_code', asset.asset_code || ''); setFieldValue('hw-purchase_corp', asset.purchase_corp || ''); setFieldValue('hw-category', asset.category || ''); 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 || ''); setFieldValue('hw-hw_status', asset.hw_status || '운영'); 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-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-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)); } setFieldValue('hw-ip_address', asset.ip_address || ''); setFieldValue('hw-ip_address_2', asset.ip_address_2 || ''); setFieldValue('hw-mac_address', asset.mac_address || ''); 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 || ''); 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 = `[파일 보기]`; } 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'); this.renderHistory(asset.id); this.applyRoleVisibility(); } 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(); } private toggleFileUploadUI(showUpload: boolean) { const fileBtn = document.getElementById('btn-file-select') as HTMLElement; 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 || ''; // 인프라 장비 (서버, 저장매체, 네트워크, 보안장비, 공간정보장비, 서버PC) const infraCategories = ['서버', '저장매체', '네트워크', '보안장비', '공간정보장비']; const isInfra = infraCategories.includes(category) || type.includes('서버') || type.includes('저장시스템'); // 개인 장비 (PC, 노트북, 모바일, 태블릿) - '서버PC'는 제외 const personalCategories = ['PC', '노트북', '모바일', '태블릿']; const isPersonal = (personalCategories.includes(category) || type.includes('개인PC') || type.includes('노트북')) && !type.includes('서버PC'); // 시스템 사양 (PC, 서버 등) const specCategories = ['PC', '서버', '노트북', '스토리지', '워크스테이션']; const hasSpec = specCategories.includes(category) || type.includes('서버PC'); // 네트워크 정보 (IP/MAC) 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('서버'); // JS에서 display: block 강제 대신 빈 문자열 할당하여 네이티브 CSS flex 활용 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 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'; } else { viewLocBtn.style.display = 'none'; } } private getImagesForLocation(bldg: string, detail: string): string[] | null { if (!bldg || !detail) return null; return IMAGE_LOCATIONS[bldg.trim()]?.[detail.trim()] || null; } private async fetchMapConfig() { try { const res = await fetch(`http://${location.hostname}:3000/api/maps`); this.dynamicMapConfig = await res.json(); } catch (err) { console.error('Failed to fetch map config:', err); } } private generateDynamicSVG(imagePath: string): string { const boxes = this.dynamicMapConfig[imagePath] || []; if (boxes.length === 0) return ''; return `${boxes.map((b) => ``).join('')}`; } private openImagePicker(imagePaths: string[], title: string) { let currentIdx = 0; const overlay = document.createElement('div'); overlay.className = 'image-picker-overlay'; const renderContent = () => { const imgPath = imagePaths[currentIdx]; const digitalMap = this.generateDynamicSVG(imgPath); overlay.innerHTML = `

${title}

${digitalMap}
`; let selectedX = ''; let selectedY = ''; 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); } private openImagePreview(imagePath: string, title: string, x: string, y: string) { const overlay = document.createElement('div'); overlay.className = 'image-picker-overlay'; const digitalMap = this.generateDynamicSVG(imagePath); overlay.innerHTML = `

${title}

${digitalMap}
`; document.body.appendChild(overlay); if (digitalMap) { const curX = parseFloat(x || '0'); const curY = parseFloat(y || '0'); 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}%`; } } }); } overlay.querySelector('.btn-close-picker')?.addEventListener('click', () => overlay.remove()); overlay.querySelector('#btn-preview-close')?.addEventListener('click', () => overlay.remove()); } private renderHistory(assetId: string) { const container = document.getElementById('hw-history-list'); if (!container) return; const logs = (state.masterData.logs || []).filter(l => l.assetId === assetId); if (logs.length === 0) { container.innerHTML = '
이력이 없습니다.
'; return; } container.innerHTML = logs.map(l => `
${l.date}
${l.user}
${l.details}
`).join(''); } private getCategoryKey(asset: any): string { const cat = asset.category; const code = asset.asset_code || ''; if (asset.asset_type === '서버PC') return 'pc'; if (cat === '서버' || code.startsWith('SVR')) return 'server'; if (cat === '스토리지' || code.startsWith('STO')) return 'storage'; if (cat === '네트워크' || code.startsWith('NET')) return 'network'; if (cat === '업무지원장비' || code.startsWith('EQP')) return 'equipment'; if (cat === '공간정보장비') return 'survey'; if (cat === 'PC부품') return 'pcParts'; return (cat === 'PC' || code.startsWith('PC')) ? 'pc' : 'officeSupplies'; } } export const hwModal = new HwAssetModal(); export function initHwModal(onSave: () => void, closeModals: () => void) { hwModal.init(onSave, closeModals); } export function openHwModal(asset: any, mode: 'view' | 'edit' | 'add' = 'view') { hwModal.open(asset, mode); }