Files
ITAM/src/components/Modal/HWModal.ts
Taehoon d34ebb8500 feat: restructure navigation, customize list columns, and move action buttons to search bar
1. Restructured navigation hierarchy (Hardware, Software, Ops Support, etc.).
2. Customized table columns for all asset categories according to new specs.
3. Moved Template/Upload/Export/Add buttons to search bar with layout optimization.
4. Hidden Asset Code and Previous User from list views (Modal only).
5. Added Current/Previous User and detailed PC spec fields (GPU, HDD3/4).
2026-05-20 14:34:07 +09:00

389 lines
18 KiB
TypeScript

import { state, saveAsset } from '../../core/state';
import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema';
import {
generateOptionsHTML,
setFieldValue,
getFieldValue,
setEditLock,
parseAndSetLocation,
bindLocationEvents,
getCombinedLocation,
applyDateMask
} from './ModalUtils';
import { CORP_LIST, LOCATION_DATA, ORG_LIST } from './SharedData';
import { createIcons, X, History, Plus, Save, Paperclip, Calendar, Monitor, Cpu, Network, ShieldCheck } from 'lucide';
let currentHwAsset: any | null = null;
let isEditMode = false;
const HW_MODAL_HTML = `
<div id="hw-asset-modal" class="modal-overlay hidden">
<div class="modal-content wide">
<div class="modal-header">
<h2 id="hw-modal-title">자산 상세 정보</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-id" name="id" />
<!-- Group 1: 기본 및 관리 정보 -->
<div class="form-section-title">기본 및 관리 정보</div>
<div class="form-group">
<label>${ASSET_SCHEMA.ASSET_CODE.ui}</label>
<div class="input-with-btn">
<input type="text" id="hw-asset_code" name="asset_code" placeholder="자동 생성" readonly />
<button type="button" id="btn-gen-hw-code" class="btn btn-outline btn-sm btn-helper">생성</button>
</div>
</div>
<div class="form-group">
<label>${ASSET_SCHEMA.PURCHASE_CORP.ui}</label>
<select id="hw-purchase_corp" name="purchase_corp">${generateOptionsHTML(CORP_LIST)}</select>
</div>
<div class="form-group">
<label>${ASSET_SCHEMA.CATEGORY.ui}</label>
<input type="text" id="hw-category" name="category" />
</div>
<div class="form-group">
<label>${ASSET_SCHEMA.HW_STATUS.ui}</label>
<select id="hw-hw_status" name="hw_status">
<option value="운영">운영</option>
<option value="재고">재고</option>
<option value="수리">수리</option>
<option value="폐기">폐기</option>
</select>
</div>
<div class="form-group dept-field">
<label>${ASSET_SCHEMA.CURRENT_DEPT.ui}</label>
<select id="hw-current_dept" name="current_dept">${generateOptionsHTML(ORG_LIST)}</select>
</div>
<div class="form-group dept-field">
<label>${ASSET_SCHEMA.PREV_DEPT.ui}</label>
<select id="hw-previous_dept" name="previous_dept">${generateOptionsHTML(ORG_LIST)}</select>
</div>
<div class="form-group">
<label>${ASSET_SCHEMA.MANAGER_MAIN.ui}</label>
<input type="text" id="hw-manager_primary" name="manager_primary" />
</div>
<div class="form-group">
<label>${ASSET_SCHEMA.MANAGER_SUB.ui}</label>
<input type="text" id="hw-manager_secondary" name="manager_secondary" />
</div>
<div class="form-group pc-only">
<label>${ASSET_SCHEMA.CURRENT_USER.ui}</label>
<input type="text" id="hw-current_user" name="current_user" />
</div>
<div class="form-group pc-only">
<label>${ASSET_SCHEMA.PREV_USER.ui}</label>
<input type="text" id="hw-previous_user" name="previous_user" />
</div>
<div class="form-group full-width server-only">
<label>${ASSET_SCHEMA.ASSET_PURPOSE.ui}</label>
<input type="text" id="hw-asset_purpose" name="asset_purpose" placeholder="예: DB서버, 웹서버, 백업용 등" />
</div>
<!-- Group 2: 설치 위치 -->
<div class="form-section-title">설치 위치</div>
<div class="form-group">
<label>건물/위치</label>
<select id="hw-bldg-select">${generateOptionsHTML(Object.keys(LOCATION_DATA))}</select>
</div>
<div class="form-group">
<label>상세 위치(층/구역)</label>
<select id="hw-floor-select"><option value="">선택</option></select>
</div>
<div class="form-group full-width" id="hw-loc-etc-group" style="display:none;">
<label>기타 상세 위치</label>
<input type="text" id="hw-loc-etc" placeholder="직접 입력" />
</div>
<!-- Group 3: 시스템 사양 -->
<div class="form-section-title">시스템 사양</div>
<div class="form-group full-width">
<label>${ASSET_SCHEMA.MODEL_NAME.ui}</label>
<input type="text" id="hw-model_name" name="model_name" />
</div>
<div class="form-group">
<label>${ASSET_SCHEMA.CPU.ui}</label>
<input type="text" id="hw-cpu" name="cpu" />
</div>
<div class="form-group">
<label>${ASSET_SCHEMA.RAM.ui}</label>
<input type="text" id="hw-ram" name="ram" />
</div>
<div class="form-group">
<label>${ASSET_SCHEMA.GPU.ui}</label>
<input type="text" id="hw-gpu" name="gpu" />
</div>
<div class="form-group">
<label>SSD 1</label>
<input type="text" id="hw-ssd_1" name="ssd_1" />
</div>
<div class="form-group">
<label>SSD 2</label>
<input type="text" id="hw-ssd_2" name="ssd_2" />
</div>
<div class="form-group">
<label>HDD 1</label>
<input type="text" id="hw-hdd_1" name="hdd_1" />
</div>
<div class="form-group">
<label>HDD 2</label>
<input type="text" id="hw-hdd_2" name="hdd_2" />
</div>
<div class="form-group pc-only">
<label>HDD 3</label>
<input type="text" id="hw-hdd_3" name="hdd_3" />
</div>
<div class="form-group pc-only">
<label>HDD 4</label>
<input type="text" id="hw-hdd_4" name="hdd_4" />
</div>
<div class="form-group pc-only">
<label>${ASSET_SCHEMA.MAINBOARD.ui}</label>
<input type="text" id="hw-mainboard" name="mainboard" />
</div>
<div class="form-group pc-only">
<label>${ASSET_SCHEMA.MAC_ADDR.ui}</label>
<input type="text" id="hw-mac_address" name="mac_address" />
</div>
<!-- Group 4: 네트워크 및 접속 정보 -->
<div class="form-section-title">네트워크 및 접속 정보</div>
<div class="form-group">
<label>${ASSET_SCHEMA.IP_ADDR.ui}</label>
<input type="text" id="hw-ip_address" name="ip_address" />
</div>
<div class="form-group server-only">
<label>${ASSET_SCHEMA.IP_ADDR2.ui}</label>
<input type="text" id="hw-ip_address_2" name="ip_address_2" />
</div>
<div class="form-group server-only">
<label>${ASSET_SCHEMA.REMOTE_TOOL.ui}</label>
<input type="text" id="hw-remote_tool" name="remote_tool" placeholder="Anydesk, Chrome 등" />
</div>
<div class="form-group server-only">
<label>${ASSET_SCHEMA.REMOTE_ID.ui}</label>
<input type="text" id="hw-remote_id" name="remote_id" />
</div>
<div class="form-group server-only">
<label>${ASSET_SCHEMA.REMOTE_PW.ui}</label>
<input type="text" id="hw-remote_pw" name="remote_pw" />
</div>
<div class="form-group server-only">
<label>${ASSET_SCHEMA.MONITORING.ui}</label>
<select id="hw-monitoring" name="monitoring">
<option value="대상">대상</option>
<option value="비대상">비대상</option>
</select>
</div>
<!-- Group 5: 구매 정보 -->
<div class="form-section-title">구매 및 증빙</div>
<div class="form-group">
<label>${ASSET_SCHEMA.PURCHASE_DATE.ui}</label>
<div style="display:flex; gap:0.25rem; align-items:center; position:relative;">
<input type="text" id="hw-purchase_date" name="purchase_date" style="flex:1;" />
<button type="button" class="btn-icon btn-helper" onclick="const p = document.getElementById('hw-purchase_date-picker'); p.value = document.getElementById('hw-purchase_date').value; p.showPicker();" style="padding:0.25rem;">
<i data-lucide="calendar" style="width:18px; height:18px; color:var(--primary-color);"></i>
</button>
<input type="date" id="hw-purchase_date-picker" style="position:absolute; width:0; height:0; opacity:0; pointer-events:none;" onchange="document.getElementById('hw-purchase_date').value = this.value" tabindex="-1" />
</div>
</div>
<div class="form-group">
<label>${ASSET_SCHEMA.PURCHASE_VENDOR.ui}</label>
<input type="text" id="hw-purchase_vendor" name="purchase_vendor" />
</div>
<div class="form-group">
<label>${ASSET_SCHEMA.PURCHASE_AMOUNT.ui}</label>
<input type="text" id="hw-purchase_amount" name="purchase_amount" oninput="this.value = this.value.replace(/[^0-9]/g, '').replace(/\\B(?=(\\d{3})+(?!\\d))/g, ',')" />
</div>
<div class="form-group full-width">
<label>${ASSET_SCHEMA.APPROVAL_DOC.ui}</label>
<div style="display:flex; align-items:center; gap:0.5rem;">
<input type="file" id="hw-approval_document_file" style="font-size:12px;" />
<span id="hw-approval_document_name" style="font-size:12px; color:var(--text-muted);"></span>
</div>
</div>
<div class="form-group full-width">
<label>${ASSET_SCHEMA.MEMO.ui}</label>
<textarea id="hw-memo" name="memo" 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-cancel-hw-modal" class="btn btn-outline">닫기</button>
<button id="btn-save-hw-asset" class="btn btn-primary">수정</button>
</div>
</div>
</div>
</div>
`;
export function initHwModal(onSave: () => void, closeModals: () => void) {
if (!document.getElementById('hw-asset-modal')) {
document.body.insertAdjacentHTML('beforeend', HW_MODAL_HTML);
}
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 btnCloseHeader = document.getElementById('btn-close-hw-modal')!;
const btnCancelFooter = document.getElementById('btn-cancel-hw-modal')!;
bindLocationEvents('hw-bldg-select', 'hw-floor-select', 'hw-loc-etc-group', 'hw-loc-etc');
applyDateMask(document.getElementById('hw-purchase_date') as HTMLInputElement);
const closeModalAction = () => { closeModals(); isEditMode = false; };
btnCloseHeader.addEventListener('click', closeModalAction);
btnCancelFooter.addEventListener('click', closeModalAction);
revertBtn.addEventListener('click', () => {
setEditLock('hw-asset-form', 'view', { saveBtnId: 'btn-save-hw-asset', revertBtnId: 'btn-revert-hw-edit' });
isEditMode = false;
if (currentHwAsset) fillHwFormData(currentHwAsset);
});
saveBtn.addEventListener('click', async () => {
if (!currentHwAsset) return;
if (!isEditMode) {
setEditLock('hw-asset-form', 'edit', { saveBtnId: 'btn-save-hw-asset', revertBtnId: 'btn-revert-hw-edit' });
isEditMode = true;
return;
}
const formData = new FormData(form);
const updated: any = { ...currentHwAsset };
formData.forEach((value, key) => {
if (key !== 'id') updated[key] = value;
});
// Handle combined location
updated.location = getCombinedLocation('hw-bldg-select', 'hw-floor-select', 'hw-loc-etc');
let categoryKey = 'pc';
if (updated.asset_code?.startsWith('SVR')) categoryKey = 'server';
else if (updated.asset_code?.startsWith('STO')) categoryKey = 'storage';
else if (updated.asset_code?.startsWith('EQP')) categoryKey = 'equipment';
const success = await saveAsset(categoryKey, updated);
if (success) {
alert(UI_TEXT.MESSAGES.SAVE_SUCCESS);
onSave();
closeModalAction();
}
});
createIcons({ icons: { X, History, Plus, Save, Paperclip, Calendar, Monitor, Cpu, Network, ShieldCheck } });
}
export function openHwModal(asset: any, mode: 'view' | 'edit' | 'add' = 'view') {
currentHwAsset = asset;
const modal = document.getElementById('hw-asset-modal')!;
setEditLock('hw-asset-form', mode, {
saveBtnId: 'btn-save-hw-asset',
revertBtnId: 'btn-revert-hw-edit',
addLogBtnId: 'btn-add-hw-log'
});
isEditMode = (mode === 'add' || mode === 'edit');
fillHwFormData(asset);
// Show/Hide category specific fields
const serverOnly = document.querySelectorAll('.server-only');
const nonServer = document.querySelectorAll('.non-server');
const pcOnly = document.querySelectorAll('.pc-only');
const isServer = asset.category === '서버' || asset.asset_code?.startsWith('SVR');
const isPc = asset.category === 'PC' || asset.asset_code?.startsWith('PC');
serverOnly.forEach(el => (el as HTMLElement).style.display = isServer ? 'flex' : 'none');
nonServer.forEach(el => (el as HTMLElement).style.display = !isServer ? 'flex' : 'none');
pcOnly.forEach(el => (el as HTMLElement).style.display = isPc ? 'flex' : 'none');
modal.classList.remove('hidden');
}
function fillHwFormData(asset: any) {
setFieldValue('hw-id', asset.id);
setFieldValue('hw-asset_code', asset.asset_code || '');
setFieldValue('hw-purchase_corp', asset.purchase_corp || '');
setFieldValue('hw-category', asset.category || '');
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-current_user', asset.current_user || '');
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-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 || '');
(document.getElementById('hw-approval_document_name') as HTMLElement).textContent = asset.approval_document || '';
setFieldValue('hw-memo', asset.memo || '');
parseAndSetLocation(asset.location || '', 'hw-bldg-select', 'hw-floor-select', 'hw-loc-etc-group', 'hw-loc-etc');
renderHwHistory(asset.id);
}
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('');
}