import { state, saveHardwareAsset, deleteHardwareAsset } from '../../core/state'; import { HardwareAsset, HardwareLog } from '../../core/excelHandler'; import { closeModals } from './BaseModal'; import { createIcons, History, Plus, X, Save, Edit2, RotateCcw, Paperclip } from 'lucide'; import { CORP_LIST, ORG_LIST, HW_TYPE_LIST, LOCATION_DATA, TYPE_PREFIX_MAP } from './SharedData'; import { generateOptionsHTML, setFieldValue, getFieldValue, parseAndSetLocation, bindLocationEvents, getCombinedLocation, setEditLock, createModalFrameHTML, autoFillForm, autoExtractForm } from './ModalUtils'; let currentAsset: HardwareAsset | null = null; let isEditMode = false; const STATUS_LIST = ['대여중', '보관중', '수리중', '기타']; // 필드 ID ↔ 데이터 Key 매핑 (유지보수 시 이 부분만 수정) const HW_FIELD_MAP: Record = { '유형': 'type', '법인': '법인', '자산코드': '자산코드', '현사용조직': '현사용조직', '이전사용조직': '이전사용조직', '상세용도': '상세용도', '모델명': '모델명', '명칭': '명칭', '보관위치': '보관위치', '현재상태': '현재상태', 'IP주소': 'IP주소', 'IP2': 'IP2', '원격접속': '원격접속', '서버ID': '서버ID', '서버PW': '서버PW', '모니터링': '모니터링', 'OS': 'OS', 'CPU': 'CPU', 'RAM': 'RAM', 'SSD1': 'SSD1', 'SSD2': 'SSD2', 'HW사양': 'HW사양', '담당자_정': '담당자_정', '담당자_부': '담당자_부', '구매일': '구매연월', '금액': '금액', '비고': '비고', '사용자': '사용자' }; const HW_FORM_HTML = `
기본 정보 (Identity)
운영 및 상태 관리
네트워크 정보 (Connectivity)
시스템 사양 (Specifications)
설치 위치 및 관리
`; function renderHwHistory(assetId: string) { const container = document.getElementById('hw-history-list'); if (!container) return; const logs = (state.masterData.logs || []).filter(l => l.assetId === assetId).sort((a,b) => new Date(b.date).getTime() - new Date(a.date).getTime()); if (logs.length === 0) { container.innerHTML = '
기록된 이력이 없습니다.
'; return; } container.innerHTML = logs.map(l => `
${l.date}
${l.user}
${l.details.replace(/\n/g, '
')}
`).join(''); } function applyTypeSpecificUI(type: string) { const detailPurpose = getFieldValue('hw-상세용도'); const upperType = (type || '').toUpperCase(); const groups: Record = { detailPurpose: document.getElementById('hw-상세용도-group'), networkTitle: document.getElementById('hw-network-title'), specTitle: document.getElementById('hw-spec-title'), opTitle: document.getElementById('hw-op-title'), model: document.getElementById('hw-model-group'), os: document.getElementById('hw-os-group'), cpu: document.getElementById('hw-cpu-group'), ram: document.getElementById('hw-ram-group'), ssd1: document.getElementById('hw-ssd1-group'), ssd2: document.getElementById('hw-ssd2-group'), hwSpec: document.getElementById('hw-hwspec-group'), monitoring: document.getElementById('hw-monitoring-group'), user: document.querySelector('.pc-only') as HTMLElement }; const serverOnly = document.querySelectorAll('.server-only'); const nonServer = document.querySelectorAll('.non-server'); const opOnly = document.querySelectorAll('.op-only'); const standardLoc = document.querySelectorAll('.loc-standard'); // 초기화 serverOnly.forEach(el => (el as HTMLElement).style.display = 'none'); nonServer.forEach(el => (el as HTMLElement).style.display = 'none'); opOnly.forEach(el => (el as HTMLElement).style.display = 'none'); standardLoc.forEach(el => (el as HTMLElement).style.display = 'flex'); Object.values(groups).forEach(g => { if (g) g.style.display = 'none'; }); const osLabel = document.querySelector('label[for="hw-OS"]') as HTMLElement; const ramLabel = document.querySelector('label[for="hw-RAM"]') as HTMLElement; const modelLabel = document.querySelector('label[for="hw-모델명"]') as HTMLElement; if (osLabel) osLabel.innerText = '운영체제 (OS)'; if (ramLabel) ramLabel.innerText = 'RAM 용량'; if (modelLabel) modelLabel.innerText = '모델명'; const isMobileGroup = ['모바일', '태블릿', '휴대폰'].some(t => upperType.includes(t)); const isEquipGroup = ['CPU', 'RAM', 'HDD', 'GPU'].some(t => upperType.includes(t)) || upperType.includes('비품'); const isOpType = isMobileGroup || isEquipGroup; const isPcType = upperType === 'PC' || upperType === '개인PC' || upperType === '노트북'; if (groups.opTitle) groups.opTitle.style.display = isOpType ? 'flex' : 'none'; if (isOpType) { opOnly.forEach(el => (el as HTMLElement).style.display = 'flex'); standardLoc.forEach(el => (el as HTMLElement).style.display = 'none'); if (groups.specTitle) groups.specTitle.style.display = 'flex'; if (groups.model) groups.model.style.display = 'flex'; if (['CPU', 'GPU'].some(t => upperType.includes(t))) { if (groups.os && osLabel) { osLabel.innerText = '출시연월'; groups.os.style.display = 'flex'; } } else if (['RAM', 'HDD'].some(t => upperType.includes(t))) { if (groups.ram && ramLabel) { ramLabel.innerText = '용량'; groups.ram.style.display = 'flex'; } if (upperType.includes('HDD') && modelLabel) modelLabel.innerText = 'S/N'; } else { if (groups.hwSpec) groups.hwSpec.style.display = 'flex'; } } else if (isPcType) { if (groups.user) groups.user.style.display = 'flex'; if (groups.specTitle) groups.specTitle.style.display = 'flex'; // 노트북은 상세유형 선택창 숨김 if (upperType === '노트북') { if (groups.detailPurpose) groups.detailPurpose.style.display = 'none'; nonServer.forEach(el => (el as HTMLElement).style.display = 'flex'); ['model', 'os', 'cpu', 'ram', 'ssd1', 'ssd2', 'hwSpec'].forEach(k => { if (groups[k]) groups[k]!.style.display = 'flex'; }); } else { if (groups.detailPurpose) groups.detailPurpose.style.display = 'flex'; if (detailPurpose === '서버') { serverOnly.forEach(el => (el as HTMLElement).style.display = 'flex'); if (groups.networkTitle) groups.networkTitle.style.display = 'flex'; ['model', 'os', 'cpu', 'ram', 'ssd1', 'ssd2', 'monitoring'].forEach(k => { if (groups[k]) groups[k]!.style.display = 'flex'; }); } else { nonServer.forEach(el => (el as HTMLElement).style.display = 'flex'); ['model', 'os', 'cpu', 'ram', 'ssd1', 'ssd2', 'hwSpec'].forEach(k => { if (groups[k]) groups[k]!.style.display = 'flex'; }); } } } else { serverOnly.forEach(el => (el as HTMLElement).style.display = 'flex'); if (groups.networkTitle) groups.networkTitle.style.display = 'flex'; if (groups.specTitle) groups.specTitle.style.display = 'flex'; ['model', 'os', 'cpu', 'ram', 'ssd1', 'ssd2', 'monitoring'].forEach(k => { if (groups[k]) groups[k]!.style.display = 'flex'; }); } } export function openHwModal(asset: HardwareAsset, mode: 'view' | 'add' = 'view') { currentAsset = asset; const modal = document.getElementById('hw-asset-modal')!; setEditLock('hw-asset-form', mode, { saveBtnId: 'btn-save-hw-asset', revertBtnId: 'btn-revert-hw-edit', generateBtnId: 'btn-generate-hw-code', addLogBtnId: 'btn-add-hw-log' }); isEditMode = (mode === 'add'); // 데이터 채우기 (자동 매핑) autoFillForm('hw', asset, HW_FIELD_MAP); setFieldValue('hw-명칭', asset.명칭 || asset.모델명); if (!asset.구매연월 && asset.구매일) setFieldValue('hw-구매일', asset.구매일); parseAndSetLocation(asset.위치, 'hw-위치-빌딩', 'hw-위치-상세', 'hw-위치-기타-group', 'hw-위치-기타'); applyTypeSpecificUI(asset.type); renderHwHistory(asset.id); modal.classList.remove('hidden'); createIcons({ icons: { X, Save, Edit2, RotateCcw, History, Plus, Paperclip } }); } export function initHwModal(onSave: () => void, closeModalsCb: () => void) { if (!document.getElementById('hw-asset-modal')) { const html = createModalFrameHTML('hw', '자산 상세 정보', HW_FORM_HTML, { historyTitle: '분출 및 변경 이력', addLogBtnId: 'btn-add-hw-log' }); document.body.insertAdjacentHTML('beforeend', html); // 이력 추가 모달 HTML도 함께 추가 const logModalHTML = ` `; document.body.insertAdjacentHTML('beforeend', logModalHTML); } const form = document.getElementById('hw-asset-form') as HTMLFormElement; 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 typeSelect = document.getElementById('hw-유형') as HTMLSelectElement; const detailPurposeSelect = document.getElementById('hw-상세용도') as HTMLSelectElement; const logAddBtn = document.getElementById('btn-add-hw-log')!; const logModal = document.getElementById('hw-log-modal')!; [typeSelect, detailPurposeSelect].forEach(el => { el?.addEventListener('change', () => applyTypeSpecificUI(typeSelect.value)); }); bindLocationEvents('hw-위치-빌딩', 'hw-위치-상세', 'hw-위치-기타-group', 'hw-위치-기타'); const closeModalAction = () => { closeModalsCb(); isEditMode = false; }; document.getElementById('btn-close-hw-modal')?.addEventListener('click', closeModalAction); document.getElementById('btn-cancel-hw-modal')?.addEventListener('click', closeModalAction); revertBtn.addEventListener('click', () => { setEditLock('hw-asset-form', 'view', { saveBtnId: 'btn-save-hw-asset', revertBtnId: 'btn-revert-hw-edit', generateBtnId: 'btn-generate-hw-code', addLogBtnId: 'btn-add-hw-log' }); isEditMode = false; if (currentAsset) openHwModal(currentAsset, 'view'); }); document.getElementById('btn-generate-hw-code')?.addEventListener('click', async () => { const typeValue = typeSelect.value; const purchaseDate = getFieldValue('hw-구매일'); const typeCode = TYPE_PREFIX_MAP[typeValue] || 'ETC'; const dateStr = purchaseDate.replace(/[^0-9]/g, ''); if (dateStr.length < 6) { alert('올바른 구매연월(YYYYMM)을 입력해주세요.'); return; } const prefix = `${typeCode}-${dateStr.substring(0, 6)}-`; try { const res = await fetch(`http://localhost:3000/api/generate-asset-code?prefix=${prefix}`); const data = await res.json(); if (data.nextCode) setFieldValue('hw-자산코드', data.nextCode); } catch (err) { alert('자산번호 생성에 실패했습니다.'); } }); // YYYYMM 입력 제한 로직 (숫자 6자리) ['hw-구매일', 'hw-OS'].forEach(id => { const el = document.getElementById(id) as HTMLInputElement; el?.addEventListener('input', (e) => { const target = e.target as HTMLInputElement; const label = document.querySelector(`label[for="${id}"]`) as HTMLElement; // OS 필드의 경우 라벨이 '출시연월'일 때만 숫자 제한 적용 if (id === 'hw-OS' && label?.innerText !== '출시연월') return; target.value = target.value.replace(/[^0-9]/g, '').substring(0, 6); }); }); saveBtn.addEventListener('click', () => { if (!currentAsset) return; if (!isEditMode) { setEditLock('hw-asset-form', 'edit', { saveBtnId: 'btn-save-hw-asset', revertBtnId: 'btn-revert-hw-edit', generateBtnId: 'btn-generate-hw-code', addLogBtnId: 'btn-add-hw-log' }); isEditMode = true; applyTypeSpecificUI(getFieldValue('hw-유형')); return; } // 데이터 추출 (자동 매핑) const extracted = autoExtractForm('hw', HW_FIELD_MAP); if (!extracted.자산코드) { alert('자산번호가 없습니다. [생성] 버튼을 눌러 자산번호를 먼저 부여해주세요.'); return; } const upperType = (extracted.type || '').toUpperCase(); const isOpType = ['CPU', 'RAM', 'HDD', 'GPU'].some(t => upperType.includes(t)) || upperType.includes('비품') || ['모바일', '태블릿', '휴대폰'].some(t => upperType.includes(t)); // --- 자동 변경 이력 생성 로직 --- // 모든 하드웨어 유형에 대해 자동 로깅 적용 if (HW_TYPE_LIST.includes(extracted.type) || extracted.type === '개인PC') { const diffLogs: string[] = []; const compareFields = [ { key: '현사용조직', label: '현사용조직' }, { key: '위치', label: '설치위치' }, { key: '관리자', label: '담당자' }, { key: '현재상태', label: '상태' }, { key: 'IP주소', label: 'IP' }, { key: '상세용도', label: '상세유형' }, { key: '모델명', label: '모델명' } ]; const currentIp = currentAsset.IP주소 || ''; const newIp = getFieldValue('hw-IP주소') || getFieldValue('hw-IP주소-non-server'); const currentLocation = currentAsset.위치 || ''; const newLocation = isOpType ? extracted.보관위치 : getCombinedLocation('hw-위치-빌딩', 'hw-위치-상세', 'hw-위치-기타'); compareFields.forEach(f => { let oldVal = ''; let newVal = ''; if (f.key === 'IP주소') { oldVal = currentIp; newVal = newIp; } else if (f.key === '위치') { oldVal = currentLocation; newVal = newLocation; } else if (f.key === '관리자') { oldVal = currentAsset.담당자_정 || ''; newVal = extracted.담당자_정 || ''; } else if (f.key === '상세용도') { oldVal = currentAsset.상세용도 || ''; // 비 PC 자산은 유형을 상세유형으로 간주 newVal = (extracted.type !== 'PC' && extracted.type !== '개인PC') ? extracted.type : (extracted.상세용도 || ''); } else { oldVal = (currentAsset as any)[f.key] || ''; newVal = extracted[f.key] || ''; } if (oldVal !== newVal) { diffLogs.push(`${f.label}: ${oldVal || '(없음)'} → ${newVal || '(없음)'}`); } }); if (diffLogs.length > 0) { state.masterData.logs = state.masterData.logs || []; state.masterData.logs.push({ id: Math.random().toString(36).substring(2, 9), assetId: currentAsset.id, date: new Date().toISOString().split('T')[0], user: '관리자', details: diffLogs.join('\n') }); } } // ---------------------------- const updated: any = { ...currentAsset, ...extracted, IP주소: getFieldValue('hw-IP주소') || getFieldValue('hw-IP주소-non-server'), 관리자: extracted.담당자_정, 위치: isOpType ? extracted.보관위치 : getCombinedLocation('hw-위치-빌딩', 'hw-위치-상세', 'hw-위치-기타') }; // 현 사용조직 변경 시 이전 사용조직 자동 업데이트 if (currentAsset.현사용조직 && currentAsset.현사용조직 !== extracted.현사용조직) { updated.이전사용조직 = currentAsset.현사용조직; } // 비 PC 자산에 대해 상세유형(상세용도)을 유형과 동기화 if (updated.type !== 'PC') { updated.상세용도 = updated.type; } saveHardwareAsset(updated); onSave(); setEditLock('hw-asset-form', 'view', { saveBtnId: 'btn-save-hw-asset', revertBtnId: 'btn-revert-hw-edit', generateBtnId: 'btn-generate-hw-code', addLogBtnId: 'btn-add-hw-log' }); isEditMode = false; }); deleteBtn.addEventListener('click', () => { if (currentAsset && confirm('정말로 삭제하시겠습니까?')) { deleteHardwareAsset(currentAsset.id); onSave(); closeModalAction(); } }); logAddBtn.addEventListener('click', () => { logModal.classList.remove('hidden'); (document.getElementById('new-hw-log-date') as HTMLInputElement).value = new Date().toISOString().split('T')[0]; (document.getElementById('new-hw-log-details') as HTMLTextAreaElement).value = ''; }); document.getElementById('btn-close-hw-log')?.addEventListener('click', () => logModal.classList.add('hidden')); document.getElementById('btn-cancel-hw-log')?.addEventListener('click', () => logModal.classList.add('hidden')); document.getElementById('btn-confirm-hw-log')?.addEventListener('click', () => { if (!currentAsset) return; const date = (document.getElementById('new-hw-log-date') as HTMLInputElement).value; const details = (document.getElementById('new-hw-log-details') as HTMLTextAreaElement).value; if (!date || !details) return; state.masterData.logs = state.masterData.logs || []; state.masterData.logs.push({ id: Math.random().toString(36).substring(2, 9), assetId: currentAsset.id, date, user: '관리자', details }); logModal.classList.add('hidden'); renderHwHistory(currentAsset.id); }); }