432 lines
19 KiB
TypeScript
432 lines
19 KiB
TypeScript
import { createIcons, Paperclip, History, Plus, X } from 'lucide';
|
|
import { HardwareAsset, HardwareLog } from '../../core/state';
|
|
import { state, saveHardwareAsset, deleteHardwareAsset } from '../../core/state';
|
|
import { setEditLock } from './ModalUtils';
|
|
|
|
let currentAsset: HardwareAsset | null = null;
|
|
let isEditMode = false;
|
|
|
|
// 공통 옵션 생성 함수
|
|
const generateOptionsHTML = (options: string[]) =>
|
|
options.map(opt => `<option value="${opt}">${opt}</option>`).join('');
|
|
|
|
const ORG_LIST = ["경영지원팀", "전략기획팀", "IT운영팀", "공정기술팀", "품질관리팀"];
|
|
const HW_TYPE_LIST = ["서버", "데스크탑", "노트북", "워크스테이션", "태블릿", "기타"];
|
|
const STATUS_LIST = ["사용중", "유휴", "수리중", "폐기예정", "기타"];
|
|
const LOCATION_DATA: Record<string, string[]> = {
|
|
"본사": ["1층 전산실", "2층 사무실", "3층 회의실"],
|
|
"IDC": ["A구역 랙 01", "A구역 랙 02", "B구역 랙 05"],
|
|
"공장": ["생산라인 1", "제어실", "창고"]
|
|
};
|
|
|
|
const HW_MODAL_HTML = `
|
|
<div id="hw-asset-modal" class="modal-overlay hidden">
|
|
<div class="modal-content modal-lg">
|
|
<div class="modal-header">
|
|
<h2 id="hw-modal-title">H/W 자산 정보</h2>
|
|
<button id="btn-close-hw-modal" class="btn-icon" aria-label="닫기"><i data-lucide="x"></i></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="modal-body-split">
|
|
<div class="modal-form-area">
|
|
<form id="hw-asset-form" class="grid-form">
|
|
<input type="hidden" id="hw-asset-id" />
|
|
<input type="hidden" id="hw-asset-type" />
|
|
|
|
<!-- Group 1: 기본 정보 (Identity) -->
|
|
<div class="form-section-title">기본 정보 (Identity)</div>
|
|
<div class="form-group">
|
|
<label for="hw-자산번호">자산번호</label>
|
|
<input type="text" id="hw-자산번호" readonly />
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="hw-관리번호">관리번호/코드</label>
|
|
<div style="display:flex; gap:0.5rem;">
|
|
<input type="text" id="hw-관리번호" readonly placeholder="자동 생성 또는 직접 입력" required />
|
|
<button type="button" id="btn-generate-hw-code" class="btn btn-outline btn-sm hidden" style="white-space:nowrap;">생성</button>
|
|
</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="hw-사용자조직">실사용조직</label>
|
|
<select id="hw-사용자조직">${generateOptionsHTML(ORG_LIST)}</select>
|
|
</div>
|
|
<div class="form-group" id="hw-관리조직-group">
|
|
<label for="hw-관리조직">자산관리조직</label>
|
|
<input type="text" id="hw-관리조직" readonly />
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="hw-자산구분">자산구분</label>
|
|
<select id="hw-자산구분">${generateOptionsHTML(HW_TYPE_LIST)}</select>
|
|
</div>
|
|
<div class="form-group" id="hw-서버용도-group" style="display:none;">
|
|
<label for="hw-서버용도">서버용도</label>
|
|
<select id="hw-서버용도">
|
|
<option value="">선택하세요</option>
|
|
<option value="DB서버">DB 서버</option>
|
|
<option value="웹서버">웹 서버</option>
|
|
<option value="어플리케이션">어플리케이션</option>
|
|
<option value="가상화">가상화 호스트</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- 운영 및 상태 관리 -->
|
|
<div class="form-section-title op-only">운영 및 상태 관리</div>
|
|
<div class="form-group op-only">
|
|
<label for="hw-보관위치">보관위치</label>
|
|
<input type="text" id="hw-보관위치" placeholder="예: 7층 비상계단옆 창고" />
|
|
</div>
|
|
<div class="form-group op-only">
|
|
<label for="hw-상태">상태</label>
|
|
<select id="hw-상태">${generateOptionsHTML(STATUS_LIST)}</select>
|
|
</div>
|
|
|
|
<!-- 네트워크 및 서버 정보 -->
|
|
<div class="form-section-title server-only" id="hw-network-title">네트워크 정보 (Connectivity)</div>
|
|
<div class="form-group server-only" id="hw-ip-group">
|
|
<label for="hw-IP주소">IP 주소 1</label>
|
|
<input type="text" id="hw-IP주소" />
|
|
</div>
|
|
<div class="form-group server-only" id="hw-ip2-group">
|
|
<label for="hw-IP2">IP 주소 2</label>
|
|
<input type="text" id="hw-IP2" />
|
|
</div>
|
|
<div class="form-group server-only" id="hw-원격접속">원격 접속/관리</div>
|
|
<div class="form-group server-only" id="hw-remote-group">
|
|
<label for="hw-원격방법">원격 방법</label>
|
|
<input type="text" id="hw-원격방법" />
|
|
</div>
|
|
<div class="form-group server-only" id="hw-server-id-group">
|
|
<label for="hw-서버ID">서버 ID</label>
|
|
<input type="text" id="hw-서버ID" />
|
|
</div>
|
|
<div class="form-group server-only" id="hw-server-pw-group">
|
|
<label for="hw-서버PW">서버 PW</label>
|
|
<input type="password" id="hw-서버PW" />
|
|
</div>
|
|
<div class="form-group non-server" id="hw-ip-non-server-group">
|
|
<label for="hw-IP주소-non-server">IP 주소</label>
|
|
<input type="text" id="hw-IP주소-non-server" />
|
|
</div>
|
|
|
|
<!-- 하드웨어 사양 -->
|
|
<div class="form-section-title" id="hw-spec-title">하드웨어 사양 (Specifications)</div>
|
|
<div class="form-group" id="hw-model-group">
|
|
<label for="hw-모델명">모델명</label>
|
|
<input type="text" id="hw-모델명" />
|
|
</div>
|
|
<div class="form-group" id="hw-os-group">
|
|
<label for="hw-OS">운영체제 (OS)</label>
|
|
<input type="text" id="hw-OS" />
|
|
</div>
|
|
<div class="form-group" id="hw-cpu-group">
|
|
<label for="hw-CPU">CPU 사양</label>
|
|
<input type="text" id="hw-CPU" />
|
|
</div>
|
|
<div class="form-group" id="hw-ram-group">
|
|
<label for="hw-RAM">RAM 용량</label>
|
|
<input type="text" id="hw-RAM" />
|
|
</div>
|
|
<div class="form-group" id="hw-ssd1-group">
|
|
<label for="hw-SSD1">Storage 1 (SSD/HDD)</label>
|
|
<input type="text" id="hw-SSD1" />
|
|
</div>
|
|
<div class="form-group" id="hw-ssd2-group">
|
|
<label for="hw-SSD2">Storage 2 (SSD/HDD)</label>
|
|
<input type="text" id="hw-SSD2" />
|
|
</div>
|
|
<div class="form-group server-only" id="hw-monitoring-group">
|
|
<label for="hw-모니터링">모니터링 유무</label>
|
|
<input type="text" id="hw-모니터링" />
|
|
</div>
|
|
<div class="form-group full-width non-server" id="hw-hwspec-group">
|
|
<label for="hw-HW사양">사양 상세</label>
|
|
<textarea id="hw-HW사양" rows="2"></textarea>
|
|
</div>
|
|
|
|
<!-- 설치/위치 관리 -->
|
|
<div class="form-section-title" id="hw-op-title">설치 및 위치 관리</div>
|
|
<div class="form-group loc-standard">
|
|
<label for="hw-설치장소">설치장소(대분류)</label>
|
|
<select id="hw-설치장소">${generateOptionsHTML(Object.keys(LOCATION_DATA))}</select>
|
|
</div>
|
|
<div class="form-group loc-standard">
|
|
<label for="hw-세부위치">세부 설치 위치</label>
|
|
<select id="hw-세부위치"><option value="">선택하세요</option></select>
|
|
</div>
|
|
<div class="form-group" id="hw-위치기타-group" style="display:none;">
|
|
<label for="hw-위치기타">직접 입력 (기타)</label>
|
|
<input type="text" id="hw-위치기타" />
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="hw-도입일">도입일</label>
|
|
<input type="text" id="hw-도입일" />
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="hw-구매가">구매가</label>
|
|
<input type="text" id="hw-구매가" oninput="this.value = this.value.replace(/[^0-9]/g, '').replace(/\\B(?=(\\d{3})+(?!\\d))/g, ',')" />
|
|
</div>
|
|
<div class="form-group full-width">
|
|
<label for="hw-비고">비고</label>
|
|
<textarea id="hw-비고" rows="2"></textarea>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="modal-history-area">
|
|
<div class="history-header">
|
|
<h3><i data-lucide="history" style="width:16px; height:16px;"></i> 유지보수 및 변경 이력</h3>
|
|
<button type="button" id="btn-add-hw-log" class="btn btn-outline btn-sm">
|
|
이력 추가 <i data-lucide="plus" style="width:14px; height:14px;"></i>
|
|
</button>
|
|
</div>
|
|
<div id="hw-history-list" class="history-timeline"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button id="btn-delete-hw-asset" class="btn btn-outline btn-danger">삭제</button>
|
|
<div class="footer-actions">
|
|
<button id="btn-revert-hw-edit" class="btn btn-outline hidden">취소</button>
|
|
<button id="btn-save-hw-asset" class="btn btn-primary">수정</button>
|
|
<button id="btn-close-modal-bottom" class="btn btn-outline">닫기</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 이력 추가 모달 -->
|
|
<div id="hw-log-modal" class="modal-overlay hidden" style="z-index: 1100;">
|
|
<div class="modal-content" style="max-width: 400px;">
|
|
<div class="modal-header">
|
|
<h2>이력 추가</h2>
|
|
<button id="btn-close-hw-log" class="btn-icon"><i data-lucide="x"></i></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="grid-form" style="grid-template-columns: 1fr;">
|
|
<div class="form-group">
|
|
<label>일자</label>
|
|
<input type="date" id="new-hw-log-date" />
|
|
</div>
|
|
<div class="form-group">
|
|
<label>변경/유지보수 내용</label>
|
|
<textarea id="new-hw-log-details" rows="3" placeholder="예: [유지보수] 먼지 제거, [부품교체] 배터리 교체 완료"></textarea>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<div></div>
|
|
<div class="footer-actions">
|
|
<button id="btn-cancel-hw-log" class="btn btn-outline">취소</button>
|
|
<button id="btn-confirm-hw-log" class="btn btn-primary">추가</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
function renderHwHistory(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 = '<div class="empty-history">기록이 없습니다.</div>';
|
|
return;
|
|
}
|
|
container.innerHTML = logs.map(l => `
|
|
<div class="history-item">
|
|
<div class="history-date">${l.date}</div>
|
|
<div class="history-user">${l.user}</div>
|
|
<div class="history-details">${l.details}</div>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
function getFieldValue(id: string): string {
|
|
return (document.getElementById(id) as HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement)?.value || '';
|
|
}
|
|
|
|
function applyTypeSpecificUI(type: string) {
|
|
const detailPurpose = getFieldValue('hw-서버용도');
|
|
const upperType = (type || '').toUpperCase();
|
|
|
|
const groups: Record<string, HTMLElement | null> = {
|
|
detailPurpose: document.getElementById('hw-서버용도-group'),
|
|
networkTitle: document.getElementById('hw-network-title'),
|
|
specTitle: document.getElementById('hw-spec-title'),
|
|
opTitle: document.getElementById('hw-op-title')
|
|
};
|
|
|
|
const isServer = upperType.includes('서버') || upperType.includes('SERVER');
|
|
const serverOnly = document.querySelectorAll('.server-only');
|
|
const nonServer = document.querySelectorAll('.non-server');
|
|
|
|
serverOnly.forEach(el => (el as HTMLElement).style.display = isServer ? 'block' : 'none');
|
|
nonServer.forEach(el => (el as HTMLElement).style.display = isServer ? 'none' : 'block');
|
|
|
|
if (groups.detailPurpose) groups.detailPurpose.style.display = isServer ? 'block' : 'none';
|
|
}
|
|
|
|
export function openHwModal(asset: any) {
|
|
currentAsset = asset;
|
|
isEditMode = false;
|
|
|
|
const modal = document.getElementById('hw-asset-modal')!;
|
|
const form = document.getElementById('hw-asset-form') as HTMLFormElement;
|
|
|
|
form.reset();
|
|
fillHwFormData(asset);
|
|
renderHwHistory(asset.id);
|
|
|
|
setEditLock('hw-asset-form', 'view', { saveBtnId: 'btn-save-hw-asset', revertBtnId: 'btn-revert-hw-edit' });
|
|
|
|
modal.classList.remove('hidden');
|
|
createIcons({ icons: { Paperclip, History, Plus, X } });
|
|
}
|
|
|
|
function fillHwFormData(asset: any) {
|
|
const setVal = (id: string, val: any) => {
|
|
const el = document.getElementById(id) as HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;
|
|
if (el) el.value = val || '';
|
|
};
|
|
|
|
setVal('hw-asset-id', asset.id);
|
|
setVal('hw-asset-type', asset.type);
|
|
setVal('hw-자산번호', asset.자산번호 || '');
|
|
setVal('hw-관리번호', asset.관리번호 || asset.관리코드 || '');
|
|
setVal('hw-사용자조직', asset.실사용조직 || '');
|
|
setVal('hw-관리조직', asset.관리조직 || '전산팀');
|
|
setVal('hw-자산구분', asset.자산구분 || asset.type || '');
|
|
setVal('hw-서버용도', asset.서버용도 || '');
|
|
setVal('hw-보관위치', asset.보관위치 || '');
|
|
setVal('hw-상태', asset.상태 || '사용중');
|
|
setVal('hw-IP주소', asset.IP주소 || '');
|
|
setVal('hw-IP2', asset.IP2 || '');
|
|
setVal('hw-원격방법', asset.원격방법 || '');
|
|
setVal('hw-서버ID', asset.서버ID || '');
|
|
setVal('hw-서버PW', asset.서버PW || '');
|
|
setVal('hw-IP주소-non-server', asset.IP주소 || '');
|
|
setVal('hw-모델명', asset.모델명 || '');
|
|
setVal('hw-OS', asset.OS || '');
|
|
setVal('hw-CPU', asset.CPU || '');
|
|
setVal('hw-RAM', asset.RAM || '');
|
|
setVal('hw-SSD1', asset.SSD1 || '');
|
|
setVal('hw-SSD2', asset.SSD2 || '');
|
|
setVal('hw-모니터링', asset.모니터링 || '');
|
|
setVal('hw-HW사양', asset.HW사양 || '');
|
|
setVal('hw-설치장소', asset.설치장소 || '');
|
|
setVal('hw-도입일', asset.도입일 || '');
|
|
setVal('hw-구매가', asset.구매가 || '');
|
|
setVal('hw-비고', asset.비고 || '');
|
|
|
|
applyTypeSpecificUI(asset.type);
|
|
}
|
|
|
|
export function initHwModal(onSave: () => void) {
|
|
document.body.insertAdjacentHTML('beforeend', HW_MODAL_HTML);
|
|
|
|
const modal = document.getElementById('hw-asset-modal')!;
|
|
const closeBtn = document.getElementById('btn-close-hw-modal')!;
|
|
const closeBtnBottom = document.getElementById('btn-close-modal-bottom')!;
|
|
const saveBtn = document.getElementById('btn-save-hw-asset')!;
|
|
const deleteBtn = document.getElementById('btn-delete-hw-asset')!;
|
|
const revertBtn = document.getElementById('btn-revert-hw-edit')!;
|
|
|
|
const closeModalAction = () => {
|
|
modal.classList.add('hidden');
|
|
isEditMode = false;
|
|
};
|
|
|
|
closeBtn.addEventListener('click', closeModalAction);
|
|
closeBtnBottom.addEventListener('click', closeModalAction);
|
|
|
|
saveBtn.addEventListener('click', () => {
|
|
if (!isEditMode) {
|
|
setEditLock('hw-asset-form', 'edit', { saveBtnId: 'btn-save-hw-asset', revertBtnId: 'btn-revert-hw-edit' });
|
|
isEditMode = true;
|
|
return;
|
|
}
|
|
|
|
const updated: any = {
|
|
...currentAsset,
|
|
관리번호: getFieldValue('hw-관리번호'),
|
|
실사용조직: getFieldValue('hw-사용자조직'),
|
|
자산구분: getFieldValue('hw-자산구분'),
|
|
서버용도: getFieldValue('hw-서버용도'),
|
|
보관위치: getFieldValue('hw-보관위치'),
|
|
상태: getFieldValue('hw-상태'),
|
|
IP주소: getFieldValue('hw-IP주소') || getFieldValue('hw-IP주소-non-server'),
|
|
IP2: getFieldValue('hw-IP2'),
|
|
원격방법: getFieldValue('hw-원격방법'),
|
|
서버ID: getFieldValue('hw-서버ID'),
|
|
서버PW: getFieldValue('hw-서버PW'),
|
|
모델명: getFieldValue('hw-모델명'),
|
|
OS: getFieldValue('hw-OS'),
|
|
CPU: getFieldValue('hw-CPU'),
|
|
RAM: getFieldValue('hw-RAM'),
|
|
SSD1: getFieldValue('hw-SSD1'),
|
|
SSD2: getFieldValue('hw-SSD2'),
|
|
모니터링: getFieldValue('hw-모니터링'),
|
|
HW사양: getFieldValue('hw-HW사양'),
|
|
설치장소: getFieldValue('hw-설치장소'),
|
|
도입일: getFieldValue('hw-도입일'),
|
|
구매가: getFieldValue('hw-구매가'),
|
|
비고: getFieldValue('hw-비고')
|
|
};
|
|
|
|
saveHardwareAsset(updated);
|
|
onSave();
|
|
setEditLock('hw-asset-form', 'view', { saveBtnId: 'btn-save-hw-asset', revertBtnId: 'btn-revert-hw-edit' });
|
|
isEditMode = false;
|
|
});
|
|
|
|
deleteBtn.addEventListener('click', () => {
|
|
if (currentAsset && confirm('정말 삭제하시겠습니까?')) {
|
|
deleteHardwareAsset(currentAsset.id);
|
|
onSave();
|
|
closeModalAction();
|
|
}
|
|
});
|
|
|
|
revertBtn.addEventListener('click', () => {
|
|
if (currentAsset) {
|
|
fillHwFormData(currentAsset);
|
|
setEditLock('hw-asset-form', 'view', { saveBtnId: 'btn-save-hw-asset', revertBtnId: 'btn-revert-hw-edit' });
|
|
isEditMode = false;
|
|
}
|
|
});
|
|
|
|
// 이력 추가 관련 이벤트
|
|
const logModal = document.getElementById('hw-log-modal')!;
|
|
document.getElementById('btn-add-hw-log')?.addEventListener('click', () => {
|
|
(document.getElementById('new-hw-log-date') as HTMLInputElement).value = new Date().toISOString().split('T')[0];
|
|
(document.getElementById('new-hw-log-details') as HTMLTextAreaElement).value = '';
|
|
logModal.classList.remove('hidden');
|
|
});
|
|
|
|
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 (!details) {
|
|
alert('내용을 입력해주세요.');
|
|
return;
|
|
}
|
|
|
|
const newLog: HardwareLog = {
|
|
id: Math.random().toString(36).substring(2, 9),
|
|
assetId: currentAsset.id,
|
|
date,
|
|
details,
|
|
user: '관리자'
|
|
};
|
|
|
|
if (!state.masterData.logs) state.masterData.logs = [];
|
|
state.masterData.logs.unshift(newLog);
|
|
renderHwHistory(currentAsset.id);
|
|
logModal.classList.add('hidden');
|
|
});
|
|
}
|