feat: 동적 디스크 확장 기능 및 하드웨어 카테고리 필터링 고도화

This commit is contained in:
2026-06-09 16:29:54 +09:00
parent 4b408b0640
commit 2b9c965c91
9 changed files with 275 additions and 125 deletions

View File

@@ -19,161 +19,209 @@ class HwAssetModal extends BaseModal {
}
protected renderFrameHTML(): string {
// CSS 명세(modal.css)의 input 패딩(0.625rem)과 일치시켜 정렬을 완벽하게 잡는 스타일
const standardBtnStyle = 'height: auto !important; padding: 0.625rem 1.25rem; font-size: 0.875rem; line-height: 1.2; display: inline-flex; align-items: center; justify-content: center;';
const sharedStyle = 'height: 38px !important; box-sizing: border-box !important; font-size: 13px; margin: 0;';
const inputStyle = sharedStyle;
const btnStyle = `padding: 0 16px; display: inline-flex; align-items: center; justify-content: center; font-weight: 600; white-space: nowrap; cursor: pointer; ${sharedStyle}`;
return `
<div id="hw-asset-modal" class="modal-overlay hidden">
<div class="modal-content wide">
<div class="modal-header">
<h2 id="hw-modal-title">${this.title}</h2>
<button id="btn-close-hw-modal" class="btn-icon" aria-label="닫기" style="font-size: 24px; color: white; background: none; border: none; cursor: pointer;">&times;</button>
<h2 id="hw-modal-title" style="margin: 0; font-size: 18px; font-weight: 800; color: white;">${this.title}</h2>
<button id="btn-close-hw-modal" class="btn-icon" aria-label="닫기" style="font-size: 28px; color: white; background: none; border: none; cursor: pointer; line-height: 1;">&times;</button>
</div>
<div class="modal-body">
<div class="modal-body" style="padding: 24px; overflow-y: auto;">
<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" />
<div class="form-section-title" style="padding-top: 0;">기본 정보</div>
<!-- [SECTION 1] 기본 관리 정보 (필수 공통) -->
<div class="form-section-title" style="padding-top: 0; margin-bottom: 12px;">기본 관리 정보</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" style="${standardBtnStyle}">생성</button>
<div class="input-with-btn" style="display: flex; gap: 8px; align-items: stretch;">
<input type="text" id="hw-asset_code" name="asset_code" placeholder="자동 생성" readonly style="flex: 1; ${inputStyle}" />
<button type="button" id="btn-gen-hw-code" class="btn btn-outline" style="${btnStyle}">생성</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>
<select id="hw-purchase_corp" name="purchase_corp" style="${inputStyle}">${generateOptionsHTML(CORP_LIST)}</select>
</div>
<div class="form-group">
<label>${ASSET_SCHEMA.CATEGORY.ui}</label>
<select id="hw-category" name="category">
<select id="hw-category" name="category" style="${inputStyle}">
<option value="">선택</option>
${generateOptionsHTML(Object.keys(CATEGORY_TYPE_MAP), '', false)}
</select>
</div>
<div class="form-group">
<label>${ASSET_SCHEMA.ASSET_TYPE.ui}</label>
<select id="hw-asset_type" name="asset_type">
<select id="hw-asset_type" name="asset_type" style="${inputStyle}">
<option value="">구분을 먼저 선택하세요</option>
</select>
</div>
<div class="form-group">
<label>${ASSET_SCHEMA.HW_STATUS.ui}</label>
<select id="hw-hw_status" name="hw_status">${generateOptionsHTML(HW_STATUS_LIST)}</select>
<select id="hw-hw_status" name="hw_status" style="${inputStyle}">${generateOptionsHTML(HW_STATUS_LIST)}</select>
</div>
<div class="form-group server-only">
<div class="form-group infra-only monitoring-field">
<label>${ASSET_SCHEMA.MONITORING.ui}</label>
<select id="hw-monitoring" name="monitoring">
<select id="hw-monitoring" name="monitoring" style="${inputStyle}">
<option value="비대상">비대상</option>
<option value="대상">대상</option>
</select>
</div>
<div class="form-section-title user-tracking-field">담당 및 조직</div>
<div class="form-group">
<!-- [SECTION 2] 조직 및 사용자 정보 -->
<div class="form-section-title org-user-section" style="margin-top: 24px; margin-bottom: 12px;">사용자 및 조직 정보</div>
<div class="form-group org-user-field">
<label>${ASSET_SCHEMA.CURRENT_DEPT.ui}</label>
<select id="hw-current_dept" name="current_dept">${generateOptionsHTML(ORG_LIST)}</select>
<select id="hw-current_dept" name="current_dept" style="${inputStyle}">${generateOptionsHTML(ORG_LIST)}</select>
</div>
<div class="form-group">
<div class="form-group org-user-field">
<label>${ASSET_SCHEMA.MANAGER_MAIN.ui}</label>
<input type="text" id="hw-manager_primary" name="manager_primary" />
<input type="text" id="hw-manager_primary" name="manager_primary" style="${inputStyle}" />
</div>
<div class="form-group user-tracking-field">
<div class="form-group personal-only">
<label>${ASSET_SCHEMA.CURRENT_USER.ui}</label>
<input type="text" id="hw-user_current" name="user_current" />
<input type="text" id="hw-user_current" name="user_current" style="${inputStyle}" />
</div>
<div class="form-group user-tracking-field">
<div class="form-group personal-only">
<label>${ASSET_SCHEMA.USER_POSITION.ui}</label>
<input type="text" id="hw-user_position" name="user_position" />
<input type="text" id="hw-user_position" name="user_position" style="${inputStyle}" />
</div>
<div class="form-group">
<label>${ASSET_SCHEMA.MANAGER_SUB.ui}</label>
<input type="text" id="hw-manager_secondary" name="manager_secondary" />
<input type="text" id="hw-manager_secondary" name="manager_secondary" style="${inputStyle}" />
</div>
<div class="form-group user-tracking-field">
<div class="form-group personal-only">
<label>${ASSET_SCHEMA.PREV_USER.ui}</label>
<input type="text" id="hw-previous_user" name="previous_user" />
<input type="text" id="hw-previous_user" name="previous_user" style="${inputStyle}" />
</div>
<div class="form-section-title">시스템 사양 및 접속 정보</div>
<!-- [SECTION 3] 하드웨어 사양 및 네트워크 -->
<div class="form-section-title hardware-section" style="margin-top: 24px; margin-bottom: 12px;">시스템 사양 및 네트워크</div>
<div class="form-group">
<label>${ASSET_SCHEMA.MODEL_NAME.ui}</label>
<input type="text" id="hw-model_name" name="model_name" />
<input type="text" id="hw-model_name" name="model_name" style="${inputStyle}" />
</div>
<div class="form-group">
<label>${ASSET_SCHEMA.ASSET_MFR.ui}</label>
<input type="text" id="hw-asset_mfr" name="asset_mfr" style="${inputStyle}" />
</div>
<div class="form-group sn-only">
<label>${ASSET_SCHEMA.SERIAL_NUM.ui}</label>
<input type="text" id="hw-serial_num" name="serial_num" style="${inputStyle}" />
</div>
<div class="form-group spec-only">
<label>${ASSET_SCHEMA.OS.ui}</label>
<input type="text" id="hw-os" name="os" />
<input type="text" id="hw-os" name="os" style="${inputStyle}" />
</div>
<div class="form-group">
<div class="form-group spec-only">
<label>${ASSET_SCHEMA.CPU.ui}</label>
<input type="text" id="hw-cpu" name="cpu" />
<input type="text" id="hw-cpu" name="cpu" style="${inputStyle}" />
</div>
<div class="form-group">
<div class="form-group spec-only">
<label>${ASSET_SCHEMA.RAM.ui}</label>
<input type="text" id="hw-ram" name="ram" />
<input type="text" id="hw-ram" name="ram" style="${inputStyle}" />
</div>
<div class="form-group">
<label>${ASSET_SCHEMA.IP_ADDR.ui}</label>
<input type="text" id="hw-ip_address" name="ip_address" />
<div class="form-group spec-only">
<label>${ASSET_SCHEMA.GPU.ui}</label>
<input type="text" id="hw-gpu" name="gpu" style="${inputStyle}" />
</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">
<label>${ASSET_SCHEMA.MAC_ADDR.ui}</label>
<input type="text" id="hw-mac_address" name="mac_address" />
</div>
<div class="form-group server-only">
<label>${ASSET_SCHEMA.REMOTE_TOOL.ui}</label>
<input type="text" id="hw-remote_tool" name="remote_tool" />
</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 class="form-group spec-only">
<label>${ASSET_SCHEMA.MAINBOARD.ui}</label>
<input type="text" id="hw-mainboard" name="mainboard" style="${inputStyle}" />
</div>
<div class="form-section-title infra-only">설치 위치</div>
<div class="form-group infra-only">
<label>건물/위치</label>
<select id="hw-bldg-select" name="location">${generateOptionsHTML(Object.keys(LOCATION_DATA))}</select>
<!-- 동적 디스크 할당 영역 (Plan B) -->
<div class="form-group spec-only full-width" style="grid-column: span 2;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
<label style="margin: 0; font-size: 11px; font-weight: 700; color: var(--text-muted);">저장장치 (디스크)</label>
<button type="button" id="btn-add-volume" class="btn btn-outline" style="height: 26px !important; padding: 0 10px; font-size: 11px; display: none;">+ 디스크 추가</button>
</div>
<div id="hw-volume-container" style="display: flex; flex-direction: column; gap: 8px;"></div>
<input type="hidden" id="hw-volumes-data" name="volumes" />
</div>
<div class="form-group infra-only">
<div class="form-group net-only">
<label>${ASSET_SCHEMA.IP_ADDR.ui}</label>
<input type="text" id="hw-ip_address" name="ip_address" style="${inputStyle}" />
</div>
<div class="form-group net-only">
<label>${ASSET_SCHEMA.MAC_ADDR.ui}</label>
<input type="text" id="hw-mac_address" name="mac_address" style="${inputStyle}" />
</div>
<div class="form-group monitor-only">
<label>${ASSET_SCHEMA.MONITOR_INCH.ui}</label>
<input type="text" id="hw-monitor_inch" name="monitor_inch" style="${inputStyle}" />
</div>
<div class="form-group parts-only">
<label>${ASSET_SCHEMA.VOLUME.ui}</label>
<input type="text" id="hw-volume" name="volume" style="${inputStyle}" />
</div>
<div class="form-group parts-only">
<label>${ASSET_SCHEMA.ASSET_COUNT.ui}</label>
<input type="text" id="hw-asset_count" name="asset_count" style="${inputStyle}" />
</div>
<!-- [SECTION 4] 원격 접속 정보 (서버 전용) -->
<div class="form-section-title remote-section" style="margin-top: 24px; margin-bottom: 12px;">원격 접속 정보</div>
<div class="form-group remote-field">
<label>${ASSET_SCHEMA.IP_ADDR2.ui}</label>
<input type="text" id="hw-ip_address_2" name="ip_address_2" style="${inputStyle}" />
</div>
<div class="form-group remote-field">
<label>${ASSET_SCHEMA.REMOTE_TOOL.ui}</label>
<input type="text" id="hw-remote_tool" name="remote_tool" style="${inputStyle}" />
</div>
<div class="form-group remote-field">
<label>${ASSET_SCHEMA.REMOTE_ID.ui}</label>
<input type="text" id="hw-remote_id" name="remote_id" style="${inputStyle}" />
</div>
<div class="form-group remote-field">
<label>${ASSET_SCHEMA.REMOTE_PW.ui}</label>
<input type="text" id="hw-remote_pw" name="remote_pw" style="${inputStyle}" />
</div>
<!-- [SECTION 5] 설치 위치 (인프라/실물 장비 전용) -->
<div class="form-section-title location-section" style="margin-top: 24px; margin-bottom: 12px;">설치 위치</div>
<div class="form-group location-field">
<label>건물/위치</label>
<select id="hw-bldg-select" name="location" style="${inputStyle}">${generateOptionsHTML(Object.keys(LOCATION_DATA))}</select>
</div>
<div class="form-group location-field">
<label>${ASSET_SCHEMA.LOC_DETAIL.ui}</label>
<div class="input-with-btn">
<select id="hw-location_detail" name="location_detail" style="flex: 1;"><option value="">선택</option></select>
<button type="button" id="btn-reg-loc-map" class="btn btn-primary" style="${standardBtnStyle} display: none;">위치등록</button>
<button type="button" id="btn-view-loc-map" class="btn btn-primary btn-loc-action" style="${standardBtnStyle} display: none; pointer-events: auto !important;">위치보기</button>
<div class="input-with-btn" style="display: flex; gap: 8px; align-items: stretch;">
<select id="hw-location_detail" name="location_detail" style="flex: 1; ${inputStyle}"><option value="">선택</option></select>
<button type="button" id="btn-reg-loc-map" class="btn btn-primary" style="${btnStyle} display: none;">위치등록</button>
<button type="button" id="btn-view-loc-map" class="btn btn-primary btn-loc-action btn-loc-view" style="${btnStyle} display: none; pointer-events: auto !important; cursor: pointer !important;">위치보기</button>
</div>
<input type="hidden" id="hw-loc_x" name="loc_x" /><input type="hidden" id="hw-loc_y" name="loc_y" /><input type="hidden" id="hw-location_photo" name="location_photo" />
</div>
<div class="form-section-title">구매 및 증빙 정보</div>
<!-- [SECTION 6] 구매 및 증빙 (공통) -->
<div class="form-section-title" style="margin-top: 24px; margin-bottom: 12px;">구매 및 증빙 정보</div>
<div class="form-group">
<label>${ASSET_SCHEMA.PURCHASE_DATE.ui}</label>
<input type="text" id="hw-purchase_date" name="purchase_date" placeholder="YYYY-MM-DD" />
<input type="text" id="hw-purchase_date" name="purchase_date" placeholder="YYYY-MM-DD" style="${inputStyle}" />
</div>
<div class="form-group">
<label>${ASSET_SCHEMA.PURCHASE_VENDOR.ui}</label>
<input type="text" id="hw-purchase_vendor" name="purchase_vendor" />
<input type="text" id="hw-purchase_vendor" name="purchase_vendor" style="${inputStyle}" />
</div>
<div class="form-group">
<label>${ASSET_SCHEMA.PURCHASE_AMOUNT.ui}</label>
<input type="text" id="hw-purchase_amount" name="purchase_amount" placeholder="0" oninput="this.value = this.value.replace(/[^0-9]/g, '').replace(/\\B(?=(\\d{3})+(?!\\d))/g, ',')" />
<input type="text" id="hw-purchase_amount" name="purchase_amount" placeholder="0" style="${inputStyle}" oninput="this.value = this.value.replace(/[^0-9]/g, '').replace(/\\B(?=(\\d{3})+(?!\\d))/g, ',')" />
</div>
<div class="form-group">
<label>${ASSET_SCHEMA.APPROVAL_DOC.ui} (첨부파일)</label>
<div class="file-upload-wrapper">
<input type="file" id="hw-approval_document_file" style="display:none;" />
<div class="input-with-btn">
<button type="button" id="btn-file-select" onclick="document.getElementById('hw-approval_document_file').click()" class="btn btn-outline btn-loc-action" style="${standardBtnStyle} flex: 1; justify-content: flex-start; pointer-events: auto !important;">
<div class="input-with-btn" style="display: flex; gap: 8px; align-items: stretch;">
<button type="button" id="btn-file-select" onclick="document.getElementById('hw-approval_document_file').click()" class="btn btn-outline btn-loc-action" style="${btnStyle} flex: 1; justify-content: flex-start; pointer-events: auto !important; cursor: pointer !important;">
<span id="hw-file-name-display">파일 선택...</span>
</button>
</div>
@@ -183,26 +231,26 @@ class HwAssetModal extends BaseModal {
</div>
<div class="form-group full-width">
<label>${ASSET_SCHEMA.MEMO.ui}</label>
<textarea id="hw-memo" name="memo" rows="3"></textarea>
<textarea id="hw-memo" name="memo" rows="3" style="width: 100%; padding: 10px; border: 1px solid var(--border-color); border-radius: 4px; font-family: inherit; font-size: 13px; resize: none !important; box-sizing: border-box;"></textarea>
</div>
</form>
</div>
<div class="modal-history-area">
<div class="history-header">
<h3>자산 변동 이력</h3>
<button type="button" id="btn-add-hw-log" class="btn btn-outline btn-sm">이력 추가</button>
<div class="history-header" style="border-bottom: 1px solid var(--border-color); padding-bottom: 12px; margin-bottom: 16px;">
<h3 style="margin: 0; font-size: 14px; font-weight: 800;">자산 변동 이력</h3>
<button type="button" id="btn-add-hw-log" class="btn btn-outline btn-sm" style="height: 30px; font-size: 11px;">이력 추가</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>
<button id="btn-delete-hw-asset" class="btn btn-outline btn-danger" style="height: 42px;">삭제</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>
<button id="btn-revert-hw-edit" class="btn btn-outline hidden" style="height: 42px;">수정 취소</button>
<button id="btn-cancel-hw-modal" class="btn btn-outline" style="height: 42px;">닫기</button>
<button id="btn-save-hw-asset" class="btn btn-primary" style="height: 42px;">저장</button>
</div>
</div>
</div>
@@ -280,8 +328,13 @@ class HwAssetModal extends BaseModal {
this.setEditLockMode('view');
if (this.currentAsset) this.fillFormData(this.currentAsset);
this.updateMapButtonVisibility();
this.toggleEditOnlyBtns(false);
});
// 동적 볼륨 추가 기능 연결
const btnAddVolume = document.getElementById('btn-add-volume')!;
btnAddVolume.addEventListener('click', () => this.addVolumeRow());
const fileInput = document.getElementById('hw-approval_document_file') as HTMLInputElement;
const fileNameDisplay = document.getElementById('hw-file-name-display');
const fileLinkContainer = document.getElementById('hw-file-link-container');
@@ -317,12 +370,25 @@ class HwAssetModal extends BaseModal {
this.isEditMode = true;
this.updateMapButtonVisibility();
this.toggleFileUploadUI(true);
this.toggleEditOnlyBtns(true);
return;
}
// 동적 볼륨 데이터 수집 및 배열 생성
const vols: any[] = [];
document.querySelectorAll('#hw-volume-container .volume-row').forEach((row, idx) => {
const type = (row.querySelector('.vol-type') as HTMLSelectElement).value;
const cap = (row.querySelector('.vol-cap') as HTMLInputElement).value;
const unit = (row.querySelector('.vol-unit') as HTMLSelectElement).value;
if (cap) vols.push({ type, capacity: parseFloat(cap), unit, slot: idx + 1 });
});
setFieldValue('hw-volumes-data', JSON.stringify(vols));
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();
@@ -330,6 +396,44 @@ class HwAssetModal extends BaseModal {
});
}
private addVolumeRow(vol: any = { type: 'SSD', capacity: '', unit: 'GB' }) {
const container = document.getElementById('hw-volume-container');
if (!container) return;
const row = document.createElement('div');
row.style.display = 'flex';
row.style.gap = '8px';
row.style.alignItems = 'center';
row.className = 'volume-row';
const inputStyle = 'height: 38px !important; box-sizing: border-box !important; font-size: 13px; margin: 0; padding: 0 8px;';
row.innerHTML = `
<select class="vol-type" style="${inputStyle} width: 80px;" ${!this.isEditMode ? 'disabled' : ''}>
<option value="SSD" ${vol.type === 'SSD' ? 'selected' : ''}>SSD</option>
<option value="HDD" ${vol.type === 'HDD' ? 'selected' : ''}>HDD</option>
<option value="NVMe" ${vol.type === 'NVMe' ? 'selected' : ''}>NVMe</option>
</select>
<input type="number" class="vol-cap" value="${vol.capacity || ''}" placeholder="용량" style="${inputStyle} flex: 1;" ${!this.isEditMode ? 'readonly' : ''} />
<select class="vol-unit" style="${inputStyle} width: 70px;" ${!this.isEditMode ? 'disabled' : ''}>
<option value="GB" ${vol.unit === 'GB' ? 'selected' : ''}>GB</option>
<option value="TB" ${vol.unit === 'TB' ? 'selected' : ''}>TB</option>
</select>
<button type="button" class="btn btn-outline btn-remove-vol edit-only-btn" style="height: 38px !important; padding: 0 12px; color: #E11D48; border-color: #E11D48; display: ${this.isEditMode ? 'inline-flex' : 'none'};">&times;</button>
`;
row.querySelector('.btn-remove-vol')?.addEventListener('click', () => row.remove());
container.appendChild(row);
}
private toggleEditOnlyBtns(isEdit: boolean) {
const addBtn = document.getElementById('btn-add-volume');
if (addBtn) addBtn.style.display = isEdit ? 'inline-flex' : 'none';
document.querySelectorAll('.edit-only-btn').forEach(btn => {
(btn as HTMLElement).style.display = isEdit ? 'inline-flex' : 'none';
});
}
protected fillFormData(asset: any): void {
setFieldValue('hw-id', asset.id);
setFieldValue('hw-asset_code', asset.asset_code || '');
@@ -347,12 +451,35 @@ class HwAssetModal extends BaseModal {
setFieldValue('hw-user_position', asset.user_position || '');
setFieldValue('hw-previous_user', asset.previous_user || '');
setFieldValue('hw-model_name', asset.model_name || '');
setFieldValue('hw-asset_mfr', asset.asset_mfr || '');
setFieldValue('hw-os', asset.os || '');
setFieldValue('hw-cpu', asset.cpu || '');
setFieldValue('hw-ram', asset.ram || '');
setFieldValue('hw-os', asset.os || '');
setFieldValue('hw-mac_address', asset.mac_address || '');
setFieldValue('hw-gpu', asset.gpu || '');
setFieldValue('hw-mainboard', asset.mainboard || '');
// 동적 볼륨 렌더링 초기화 및 생성
const volumeContainer = document.getElementById('hw-volume-container');
if (volumeContainer) {
volumeContainer.innerHTML = '';
let vols = [];
try {
vols = asset.volumes ? (typeof asset.volumes === 'string' ? JSON.parse(asset.volumes) : asset.volumes) : [];
} catch(e) {}
vols.forEach((v: any) => this.addVolumeRow(v));
}
setFieldValue('hw-ip_address', asset.ip_address || '');
setFieldValue('hw-ip_address_2', asset.ip_address_2 || '');
setFieldValue('hw-mac_address', asset.mac_address || '');
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-serial_num', asset.serial_num || '');
setFieldValue('hw-monitor_inch', asset.monitor_inch || '');
setFieldValue('hw-volume', asset.volume || '');
setFieldValue('hw-asset_count', asset.asset_count || '');
setFieldValue('hw-purchase_date', asset.purchase_date || '');
setFieldValue('hw-purchase_vendor', asset.purchase_vendor || '');
setFieldValue('hw-purchase_amount', asset.purchase_amount || '');
@@ -379,6 +506,7 @@ class HwAssetModal extends BaseModal {
const genBtn = document.getElementById('btn-gen-hw-code');
if (genBtn) genBtn.style.display = (mode === 'add') ? 'inline-flex' : 'none';
this.toggleFileUploadUI(mode !== 'view');
this.toggleEditOnlyBtns(mode !== 'view');
this.updateMapButtonVisibility(asset);
this.applyRoleVisibility();
}
@@ -392,15 +520,41 @@ class HwAssetModal extends BaseModal {
const category = (document.getElementById('hw-category') as HTMLSelectElement)?.value || '';
const type = (document.getElementById('hw-asset_type') as HTMLSelectElement)?.value || '';
// 인프라 장비 (서버, 서버PC, 저장시스템 등)
const isInfra = ['서버', '저장매체', '네트워크', '공간정보장비'].includes(category) || type.includes('서버') || type.includes('저장시스템');
// 개인 장비 (PC, 노트북) - '서버PC'는 제외
const isPersonal = (['PC', '노트북'].includes(category) || type.includes('개인PC') || type.includes('노트북')) && !type.includes('서버PC');
// 인프라 장비 (서버, 저장매체, 네트워크, 보안장비, 공간정보장비, 서버PC)
const infraCategories = ['서버', '저장매체', '네트워크', '보장비', '공간정보장비'];
const isInfra = infraCategories.includes(category) || type.includes('서버') || type.includes('저장시스템');
// JS에서 display: block을 강제하지 않고, 빈 문자열로 설정하여 CSS의 flex가 작동하게 함
document.querySelectorAll('.server-only').forEach(el => (el as HTMLElement).style.display = isInfra ? '' : 'none');
document.querySelectorAll('.infra-only').forEach(el => (el as HTMLElement).style.display = isInfra ? '' : 'none');
document.querySelectorAll('.user-tracking-field').forEach(el => (el as HTMLElement).style.display = isPersonal ? '' : 'none');
// 개인 장비 (PC, 노트북, 모바일, 태블릿) - '서버PC'는 제외
const personalCategories = ['PC', '노트북', '모바일', '태블릿'];
const isPersonal = (personalCategories.includes(category) || type.includes('개인PC') || type.includes('노트북')) && !type.includes('서버PC');
// 시스템 사양 (PC, 서버 등)
const specCategories = ['PC', '서버', '노트북', '스토리지', '워크스테이션'];
const hasSpec = specCategories.includes(category) || type.includes('서버PC');
// 네트워크 정보 (IP/MAC)
const noNetCategories = ['저장매체', '네트워크', '공간정보장비', 'PC부품', '사무가구'];
const showNet = (isInfra || isPersonal) && !noNetCategories.includes(category);
// 시리얼 번호
const hasSN = !['사무가구', 'PC부품'].includes(category);
// 수량/용량 전용 (부품)
const isParts = ['PC부품', '사무가구'].includes(category);
// 원격 접속 (서버 전용)
const showRemote = category === '서버' || type.includes('서버');
// JS에서 display: block 강제 대신 빈 문자열 할당하여 네이티브 CSS flex 활용
document.querySelectorAll('.remote-section, .remote-field, .monitoring-field').forEach(el => (el as HTMLElement).style.display = showRemote ? '' : 'none');
document.querySelectorAll('.net-only').forEach(el => (el as HTMLElement).style.display = showNet ? '' : 'none');
document.querySelectorAll('.spec-only').forEach(el => (el as HTMLElement).style.display = hasSpec ? '' : 'none');
document.querySelectorAll('.location-section, .location-field').forEach(el => (el as HTMLElement).style.display = (isInfra || category === '공간정보장비') ? '' : 'none');
document.querySelectorAll('.org-user-section, .org-user-field').forEach(el => (el as HTMLElement).style.display = (isPersonal || isParts || category === '업무지원장비') ? '' : 'none');
document.querySelectorAll('.personal-only').forEach(el => (el as HTMLElement).style.display = isPersonal ? '' : 'none');
document.querySelectorAll('.sn-only').forEach(el => (el as HTMLElement).style.display = hasSN ? '' : 'none');
document.querySelectorAll('.monitor-only').forEach(el => (el as HTMLElement).style.display = type.includes('모니터') ? '' : 'none');
document.querySelectorAll('.parts-only').forEach(el => (el as HTMLElement).style.display = isParts ? '' : 'none');
}
private updateMapButtonVisibility(asset?: any) {

View File

@@ -20,8 +20,8 @@ export const CATEGORY_TYPE_MAP: Record<string, string[]> = {
'PC부품': ['CPU', 'RAM', 'GPU', 'SSD', 'HDD', 'RAM', '모니터'],
'공간정보장비': ['드론', '측량장비', '보조기기'],
'업무지원장비': ['카메라', '스피커', 'TV', '모바일', '유선전화기', 'XR', '프린터', '전산소모품'],
'외부': ['영구', '구독'],
'내부': ['판매용', 'Solutions', 'Inhouse', 'Engine&Module'],
'외부SW': ['영구', '구독'],
'내부SW': ['판매용', 'Solutions', 'Inhouse', 'Engine&Module'],
'비용관리': ['클라우드', '도메인', '전화', '인터넷', '이메일'],
'내빈/외빈': ['선물'],
'시설자산': ['사무가구']

View File

@@ -7,7 +7,7 @@ const MENU_CONFIG: any = {
},
sw: {
label: '소프트웨어',
tabs: ['외부', '내부']
tabs: ['외부SW', '내부SW']
},
ops: {
label: '운영지원',

View File

@@ -120,12 +120,12 @@ export const PAGE_DESCRIPTIONS: Record<string, { title: string; description: str
description: '측량 및 공간 정보 수집에 사용되는 특수 정밀 장비들의 이력과 상태를 관리합니다.',
icon: 'map'
},
'내부': {
'내부SW': {
title: '사내 개발 S/W 관리',
description: '사내에서 자체 개발하거나 운영 중인 시스템 및 소프트웨어 서비스 현황을 관리합니다.',
icon: 'code'
},
'외부': {
'외부SW': {
title: '외부 상용 S/W 관리',
description: '상용 소프트웨어의 라이선스 보유 현황, 사용자 할당 및 만료 일정을 관리합니다.',
icon: 'package'

View File

@@ -121,7 +121,7 @@ function initApp() {
if (cat === 'hw') {
openHwModal({ id: newId, asset_code: '', category: tab } as any, 'add');
} else if (cat === 'sw') {
const swType = tab === '외부' ? '외부SW' : (tab === '내부' ? '내부SW' : '외부SW');
const swType = tab === '외부SW' ? '외부SW' : (tab === '내부SW' ? '내부SW' : '외부SW');
openSwModal({ id: newId, asset_type: swType } as any, 'add');
} else if (cat === 'ops') {
if (tab === '도메인') openDomainModal(null);

View File

@@ -427,8 +427,8 @@ export function createListView(container: HTMLElement, config: ListViewConfig) {
const managerSub = asset[ASSET_SCHEMA.MANAGER_SUB.key] || '-';
// [경고 로직] 외부 운영인데 서버PC이거나 IDC가 아닌 경우
const isLocWarning = serviceType === '외부' && loc !== 'IDC';
const isTypeWarning = serviceType === '외부' && type.toLowerCase().replace(/\s/g, '').includes('서버pc');
const isLocWarning = serviceType === '외부SW' && loc !== 'IDC';
const isTypeWarning = serviceType === '외부SW' && type.toLowerCase().replace(/\s/g, '').includes('서버pc');
const isWarning = isLocWarning || isTypeWarning;
const warningStyle = isWarning ? 'background-color: #FFF1F2; border-left: 3px solid #E11D48;' : '';

View File

@@ -5,10 +5,10 @@ import { ASSET_SCHEMA } from '../../core/schema';
import { createListView } from './ListFactory';
export function renderSwList(container: HTMLElement) {
const isInternal = state.activeSubTab === '내부';
const isInternal = state.activeSubTab === '내부SW';
createListView(container, {
title: isInternal ? '내부' : '외부',
title: isInternal ? '내부SW' : '외부SW',
dataSource: () => sortAssets(isInternal ? state.masterData.swInternal : state.masterData.swExternal),
searchKeys: ['PRODUCT_NAME', 'CURRENT_USER', 'CURRENT_DEPT', 'ASSET_TYPE'],
emptyMessage: '검색 결과가 없습니다.',

View File

@@ -41,7 +41,7 @@ export function renderSWTable(mainContent: HTMLElement) {
container.innerHTML = `<div style="padding:2rem; color:var(--text-muted);">"${tab}" 탭에 대한 하드웨어 리스트 뷰가 정의되지 않았습니다.</div>`;
}
} else if (state.activeCategory === 'sw') {
if (tab === '외부' || tab === '내부') {
if (tab === '외부SW' || tab === '내부SW') {
renderSwList(container);
} else {
container.innerHTML = `<div style="padding:2rem; color:var(--text-muted);">"${tab}" 탭에 대한 소프트웨어 리스트 뷰가 정의되지 않았습니다.</div>`;