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).
389 lines
18 KiB
TypeScript
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('');
|
|
}
|