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 } from './SharedData'; import { BaseModal } from './BaseModal'; import { createIcons, X, History, Plus, Save, Paperclip, Calendar, Monitor, Cpu, Network, ShieldCheck } from 'lucide'; class HwAssetModal extends BaseModal { private dynamicMapConfig: Record = {}; constructor() { super('hw', '자산 상세 정보'); } protected renderFrameHTML(): string { 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) : ''; }); bldgSelect.addEventListener('change', () => setTimeout(() => this.updateMapButtonVisibility(), 100)); detailSelect.addEventListener('change', () => this.updateMapButtonVisibility()); document.getElementById('btn-reg-loc-map')?.addEventListener('click', async () => { 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 () => { await this.fetchMapConfig(); const images = this.getImagesForLocation(bldgSelect.value, detailSelect.value); const x = getFieldValue('hw-loc_x'); const y = getFieldValue('hw-loc_y'); const savedImg = getFieldValue('hw-location_photo'); if (images) { const imgPath = savedImg && images.includes(savedImg) ? savedImg : images[0]; this.openImagePreview(imgPath, `${detailSelect.value} 위치 확인`, 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(); }); saveBtn.addEventListener('click', async () => { if (!this.currentAsset) return; if (!this.isEditMode) { this.setEditLockMode('edit'); this.isEditMode = true; this.updateMapButtonVisibility(); return; } 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(); } }); createIcons({ icons: { History, Plus, Save, Paperclip, Calendar, Monitor, Cpu, Network, ShieldCheck } }); } 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-previous_dept', asset.previous_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-asset_purpose', asset.asset_purpose || ''); setFieldValue('hw-model_name', asset.model_name || ''); setFieldValue('hw-cpu', asset.cpu || ''); setFieldValue('hw-ram', asset.ram || ''); setFieldValue('hw-gpu', asset.gpu || ''); setFieldValue('hw-ssd_1', asset.ssd_1 || ''); setFieldValue('hw-ssd_2', asset.ssd_2 || ''); setFieldValue('hw-hdd_1', asset.hdd_1 || ''); setFieldValue('hw-hdd_2', asset.hdd_2 || ''); setFieldValue('hw-hdd_3', asset.hdd_3 || ''); setFieldValue('hw-hdd_4', asset.hdd_4 || ''); setFieldValue('hw-mainboard', asset.mainboard || ''); setFieldValue('hw-os', asset.os || ''); 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-purchase_date', asset.purchase_date || ''); setFieldValue('hw-purchase_vendor', asset.purchase_vendor || ''); setFieldValue('hw-purchase_amount', asset.purchase_amount || ''); const docName = document.getElementById('hw-approval_document_name'); if (docName) docName.textContent = asset.approval_document || ''; 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); } protected onAfterOpen(asset: any, mode: string): void { this.updateMapButtonVisibility(asset); const isServer = asset.category === '서버' || asset.asset_code?.startsWith('SVR') || asset.asset_type === '서버PC'; const isPc = asset.category === 'PC' || asset.asset_code?.startsWith('PC'); const isVip = asset.category === '선물' || asset.category === 'VIP'; document.querySelectorAll('.server-only').forEach(el => (el as HTMLElement).style.display = isServer ? 'flex' : 'none'); document.querySelectorAll('.non-server').forEach(el => (el as HTMLElement).style.display = !isServer ? 'flex' : 'none'); document.querySelectorAll('.pc-only').forEach(el => (el as HTMLElement).style.display = isPc ? 'flex' : 'none'); document.querySelectorAll('.user-tracking-field').forEach(el => (el as HTMLElement).style.display = (!isServer && !isVip) ? 'flex' : '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.classList.remove('hidden'); else regLocBtn.classList.add('hidden'); if (hasImage && hasCoords) viewLocBtn.classList.remove('hidden'); else viewLocBtn.classList.add('hidden'); } 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, i) => ``).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 isMulti = imagePaths.length > 1; const digitalMap = this.generateDynamicSVG(imgPath); overlay.innerHTML = `

${title} ${isMulti ? `(${currentIdx + 1}/${imagePaths.length})` : ''}

${isMulti ? `
` : ''}
${digitalMap}
`; createIcons({ icons: { X } }); let selectedX = ''; let selectedY = ''; const container = overlay.querySelector('#picker-container') as HTMLElement; const marker = overlay.querySelector('#picker-marker') as HTMLElement; overlay.querySelectorAll('.map-seat-obj').forEach(seat => { seat.addEventListener('click', (e) => { e.stopPropagation(); const target = e.currentTarget as SVGRectElement; selectedX = target.getAttribute('x') || ''; selectedY = target.getAttribute('y') || ''; const w = target.getAttribute('width') || '0'; const h = target.getAttribute('height') || '0'; marker.style.left = `${parseFloat(selectedX) + parseFloat(w)/2}%`; marker.style.top = `${parseFloat(selectedY) + parseFloat(h)/2}%`; marker.classList.remove('hidden'); }); }); if (!digitalMap) { 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()); if (isMulti) { overlay.querySelector('.picker-nav.prev')?.addEventListener('click', (e) => { if (currentIdx > 0) { currentIdx--; renderContent(); } }); overlay.querySelector('.picker-nav.next')?.addEventListener('click', (e) => { if (currentIdx < imagePaths.length - 1) { currentIdx++; renderContent(); } }); } 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); createIcons({ icons: { X } }); if (digitalMap) { overlay.querySelectorAll('.map-seat-obj').forEach(seat => { const sx = seat.getAttribute('x'); const sy = seat.getAttribute('y'); if (sx === x && sy === y) { (seat as SVGRectElement).style.fill = 'rgba(255, 61, 0, 0.4)'; (seat as SVGRectElement).style.stroke = '#FF3D00'; (seat as SVGRectElement).style.strokeWidth = '0.8'; const marker = overlay.querySelector('#preview-marker') as HTMLElement; const w = seat.getAttribute('width') || '0'; const h = seat.getAttribute('height') || '0'; marker.style.left = `${parseFloat(sx!) + parseFloat(w)/2}%`; marker.style.top = `${parseFloat(sy!) + parseFloat(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); }