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).
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { HardwareAsset, SoftwareAsset } from '../../core/excelHandler';
|
||||
import { state } from '../../core/state';
|
||||
import { ASSET_SCHEMA } from '../../core/schema';
|
||||
import { createIcons, X } from 'lucide';
|
||||
|
||||
const DASHBOARD_DETAIL_MODAL_HTML = `
|
||||
<div id="dashboard-detail-modal" class="modal-overlay hidden">
|
||||
@@ -37,9 +38,11 @@ export function initDashboardDetailModal() {
|
||||
closeBtn.addEventListener('click', closeModal);
|
||||
cancelBtn.addEventListener('click', closeModal);
|
||||
modal.addEventListener('click', (e) => { if (e.target === modal) closeModal(); });
|
||||
|
||||
createIcons({ icons: { X } });
|
||||
}
|
||||
|
||||
export function openDashboardDetail(title: string, list: HardwareAsset[]) {
|
||||
export function openDashboardDetail(title: string, list: any[]) {
|
||||
const modal = document.getElementById('dashboard-detail-modal');
|
||||
if (!modal) return;
|
||||
const titleEl = document.getElementById('dashboard-detail-modal-title');
|
||||
@@ -49,23 +52,23 @@ export function openDashboardDetail(title: string, list: HardwareAsset[]) {
|
||||
if (!thead) return;
|
||||
|
||||
titleEl.textContent = title;
|
||||
thead.innerHTML = `<tr><th>No</th><th>유형</th><th>자산코드</th><th>명칭/모델</th><th>위치</th><th>담당/사용자</th><th>구매연월</th><th>금액</th></tr>`;
|
||||
thead.innerHTML = `<tr><th>No</th><th>유형</th><th>명칭/모델</th><th>위치</th><th>담당/사용자</th><th>구매일자</th><th>금액</th></tr>`;
|
||||
tbody.innerHTML = '';
|
||||
if (list.length === 0) {
|
||||
tbody.innerHTML = `<tr><td colspan="8" style="text-align:center; padding: 2rem;">해당 조건의 자산이 없습니다.</td></tr>`;
|
||||
tbody.innerHTML = `<tr><td colspan="7" style="text-align:center; padding: 2rem;">해당 조건의 자산이 없습니다.</td></tr>`;
|
||||
} else {
|
||||
list.forEach((asset, idx) => {
|
||||
let manager = asset.관리자 || asset.사용자 || asset.담당자_정 || '-';
|
||||
let name = asset.명칭 || asset.모델명 || '-';
|
||||
let manager = asset[ASSET_SCHEMA.MANAGER_MAIN.key] || asset.current_user || '-';
|
||||
let name = asset[ASSET_SCHEMA.MODEL_NAME.key] || asset[ASSET_SCHEMA.ASSET_NAME.key] || '-';
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `<td>${idx+1}</td><td>${asset.type}</td><td>${asset.자산코드}</td><td>${name}</td><td>${asset.위치||'-'}</td><td>${manager}</td><td>${asset.구매일||'-'}</td><td>${asset.금액||'-'}</td>`;
|
||||
tr.innerHTML = `<td>${idx+1}</td><td>${asset.category || asset[ASSET_SCHEMA.ASSET_TYPE.key]}</td><td>${name}</td><td>${asset[ASSET_SCHEMA.LOCATION.key]||'-'}</td><td>${manager}</td><td>${asset[ASSET_SCHEMA.PURCHASE_DATE.key]||'-'}</td><td>${asset[ASSET_SCHEMA.PURCHASE_AMOUNT.key]||'-'}</td>`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
}
|
||||
modal.classList.remove('hidden');
|
||||
}
|
||||
|
||||
export function openSwDashboardDetail(title: string, list: SoftwareAsset[]) {
|
||||
export function openSwDashboardDetail(title: string, list: any[]) {
|
||||
const modal = document.getElementById('dashboard-detail-modal');
|
||||
if (!modal) return;
|
||||
const titleEl = document.getElementById('dashboard-detail-modal-title');
|
||||
@@ -79,13 +82,13 @@ export function openSwDashboardDetail(title: string, list: SoftwareAsset[]) {
|
||||
tbody.innerHTML = '';
|
||||
list.forEach((sw, idx) => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `<td>${idx+1}</td><td>${sw.type}</td><td>${sw.법인}</td><td>${sw.제품명}</td><td>${sw.수량}</td><td>${sw.금액}</td>`;
|
||||
tr.innerHTML = `<td>${idx+1}</td><td>${sw.asset_type || sw.type}</td><td>${sw[ASSET_SCHEMA.PURCHASE_CORP.key]}</td><td>${sw[ASSET_SCHEMA.PRODUCT_NAME.key]}</td><td>${sw[ASSET_SCHEMA.ASSET_COUNT.key]}</td><td>${sw[ASSET_SCHEMA.PURCHASE_AMOUNT.key]}</td>`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
modal.classList.remove('hidden');
|
||||
}
|
||||
|
||||
export function openSwUsageDetail(title: string, list: SoftwareAsset[]) {
|
||||
export function openSwUsageDetail(title: string, list: any[]) {
|
||||
const modal = document.getElementById('dashboard-detail-modal');
|
||||
if (!modal) return;
|
||||
const titleEl = document.getElementById('dashboard-detail-modal-title');
|
||||
@@ -99,14 +102,15 @@ export function openSwUsageDetail(title: string, list: SoftwareAsset[]) {
|
||||
tbody.innerHTML = '';
|
||||
list.forEach((sw, idx) => {
|
||||
const assigned = state.masterData.swUsers.filter(u => u.sw_id === sw.id).length;
|
||||
const qty = Number(sw[ASSET_SCHEMA.ASSET_COUNT.key] || 0);
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `<td>${idx+1}</td><td>${sw.법인}</td><td>${sw.제품명}</td><td>${sw.수량}</td><td>${assigned}</td><td>${Number(sw.수량) - assigned}</td>`;
|
||||
tr.innerHTML = `<td>${idx+1}</td><td>${sw[ASSET_SCHEMA.PURCHASE_CORP.key]}</td><td>${sw[ASSET_SCHEMA.PRODUCT_NAME.key]}</td><td>${qty}</td><td>${assigned}</td><td>${qty - assigned}</td>`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
modal.classList.remove('hidden');
|
||||
}
|
||||
|
||||
export function openCloudDashboardDetail(title: string, list: SoftwareAsset[]) {
|
||||
export function openCloudDashboardDetail(title: string, list: any[]) {
|
||||
const modal = document.getElementById('dashboard-detail-modal');
|
||||
if (!modal) return;
|
||||
const titleEl = document.getElementById('dashboard-detail-modal-title');
|
||||
@@ -116,15 +120,15 @@ export function openCloudDashboardDetail(title: string, list: SoftwareAsset[]) {
|
||||
if (!thead) return;
|
||||
|
||||
titleEl.textContent = title;
|
||||
thead.innerHTML = `<tr><th>No</th><th>플랫폼명</th><th>법인</th><th>제품명</th><th>결제일</th><th>당월청구액(원)</th></tr>`;
|
||||
thead.innerHTML = `<tr><th>No</th><th>플랫폼/목적</th><th>법인</th><th>제품명</th><th>결제일</th><th>당월청구액(원)</th></tr>`;
|
||||
tbody.innerHTML = '';
|
||||
if (list.length === 0) {
|
||||
tbody.innerHTML = `<tr><td colspan="6" style="text-align:center; padding: 2rem;">해당 내역이 없습니다.</td></tr>`;
|
||||
} else {
|
||||
list.forEach((sw, idx) => {
|
||||
const priceStr = sw.당월청구액 ? Number(sw.당월청구액.replace(/[^0-9]/g, '')).toLocaleString() : '0';
|
||||
const priceStr = sw[ASSET_SCHEMA.PURCHASE_AMOUNT.key] ? Number(String(sw[ASSET_SCHEMA.PURCHASE_AMOUNT.key]).replace(/[^0-9]/g, '')).toLocaleString() : '0';
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `<td>${idx+1}</td><td>${sw.플랫폼명||'-'}</td><td>${sw.법인||'-'}</td><td>${sw.제품명||'-'}</td><td>${sw.결제일 ? sw.결제일 + '일' : '-'}</td><td>₩ ${priceStr}</td>`;
|
||||
tr.innerHTML = `<td>${idx+1}</td><td>${sw[ASSET_SCHEMA.DEV_OBJ.key]||'-'}</td><td>${sw[ASSET_SCHEMA.PURCHASE_CORP.key]||'-'}</td><td>${sw[ASSET_SCHEMA.PRODUCT_NAME.key]||'-'}</td><td>${sw.pay_day ? sw.pay_day + '일' : '-'}</td><td>₩ ${priceStr}</td>`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,484 +1,388 @@
|
||||
import { state, saveHardwareAsset, deleteHardwareAsset } from '../../core/state';
|
||||
import { HardwareAsset } from '../../core/excelHandler';
|
||||
import { openModal } from './BaseModal';
|
||||
import { state, saveAsset } from '../../core/state';
|
||||
import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema';
|
||||
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,
|
||||
setEditLock,
|
||||
parseAndSetLocation,
|
||||
bindLocationEvents,
|
||||
getCombinedLocation,
|
||||
setEditLock,
|
||||
createModalFrameHTML,
|
||||
autoFillForm,
|
||||
autoExtractForm
|
||||
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 currentAsset: HardwareAsset | null = null;
|
||||
let currentHwAsset: any | null = null;
|
||||
let isEditMode = false;
|
||||
|
||||
const STATUS_LIST = ['대여중', '보관중', '수리중', '기타'];
|
||||
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>
|
||||
|
||||
/**
|
||||
* 하드웨어 필드 매핑 (통합 스키마 기반)
|
||||
*/
|
||||
const HW_FIELD_MAP: Record<string, string> = {
|
||||
'유형': ASSET_SCHEMA.TYPE.key,
|
||||
'법인': ASSET_SCHEMA.CORP.key,
|
||||
'자산코드': ASSET_SCHEMA.ASSET_CODE.key,
|
||||
'현사용조직': ASSET_SCHEMA.ORG.key,
|
||||
'이전사용조직': ASSET_SCHEMA.PREV_ORG.key,
|
||||
'상세용도': '상세용도',
|
||||
'모델명': ASSET_SCHEMA.MODEL.key,
|
||||
'메인보드': ASSET_SCHEMA.MAINBOARD.key,
|
||||
'명칭': '명칭',
|
||||
'보관위치': ASSET_SCHEMA.STORE_LOC.key,
|
||||
'현재상태': ASSET_SCHEMA.STATUS.key,
|
||||
'IP주소': ASSET_SCHEMA.IP_ADDR.key,
|
||||
'IP2': ASSET_SCHEMA.IP_ADDR2.key,
|
||||
'원격접속': '원격접속',
|
||||
'서버ID': '서버ID',
|
||||
'서버PW': '서버PW',
|
||||
'모니터링': '모니터링',
|
||||
'OS': ASSET_SCHEMA.OS.key,
|
||||
'CPU': ASSET_SCHEMA.CPU.key,
|
||||
'GPU': ASSET_SCHEMA.GPU.key,
|
||||
'RAM': ASSET_SCHEMA.RAM.key,
|
||||
'SSD1': ASSET_SCHEMA.STORAGE1.key,
|
||||
'SSD2': ASSET_SCHEMA.STORAGE2.key,
|
||||
'SSD3': ASSET_SCHEMA.STORAGE3.key,
|
||||
'HW사양': 'HW사양',
|
||||
'담당자_정': ASSET_SCHEMA.MANAGER_MAIN.key,
|
||||
'담당자_부': ASSET_SCHEMA.MANAGER_SUB.key,
|
||||
'구매일': ASSET_SCHEMA.PURCHASE_YM.key,
|
||||
'금액': ASSET_SCHEMA.PRICE.key,
|
||||
'납품업체': ASSET_SCHEMA.VENDOR.key,
|
||||
'비고': ASSET_SCHEMA.REMARKS.key,
|
||||
'사용자': ASSET_SCHEMA.USER.key
|
||||
};
|
||||
<!-- 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>
|
||||
|
||||
const HW_FORM_HTML = `
|
||||
<div class="form-section-title">기본 정보 (Identity)</div>
|
||||
<div class="form-group">
|
||||
<label for="hw-법인">${ASSET_SCHEMA.CORP.ui}</label>
|
||||
<select id="hw-법인" required>${generateOptionsHTML(CORP_LIST)}</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="hw-자산코드">${ASSET_SCHEMA.ASSET_CODE.ui}</label>
|
||||
<div class="input-with-btn">
|
||||
<input type="text" id="hw-자산코드" readonly class="is-readonly-field" placeholder="번호 생성을 클릭하세요" required />
|
||||
<button type="button" id="btn-generate-hw-code" class="btn btn-outline btn-sm">생성</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group pc-only">
|
||||
<label for="hw-사용자">${ASSET_SCHEMA.USER.ui}</label>
|
||||
<input type="text" id="hw-사용자" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="hw-현사용조직">${ASSET_SCHEMA.ORG.ui}</label>
|
||||
<select id="hw-현사용조직">${generateOptionsHTML(ORG_LIST)}</select>
|
||||
</div>
|
||||
<div class="form-group" id="hw-이전사용조직-group">
|
||||
<label for="hw-이전사용조직">${ASSET_SCHEMA.PREV_ORG.ui}</label>
|
||||
<input type="text" id="hw-이전사용조직" readonly />
|
||||
</div>
|
||||
<div class="form-group" id="hw-유형-group">
|
||||
<label for="hw-유형">유형</label>
|
||||
<select id="hw-유형">${generateOptionsHTML(HW_TYPE_LIST)}</select>
|
||||
</div>
|
||||
<div class="form-group" id="hw-상세용도-group">
|
||||
<label for="hw-상세용도">상세유형</label>
|
||||
<select id="hw-상세용도">
|
||||
<option value="">선택</option>
|
||||
<option value="서버">서버</option>
|
||||
<option value="개인PC">개인PC</option>
|
||||
</select>
|
||||
</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>
|
||||
|
||||
<div class="form-section-title op-only" id="hw-op-title">운영 및 상태 관리</div>
|
||||
<div class="form-group op-only">
|
||||
<label for="hw-보관위치">${ASSET_SCHEMA.STORE_LOC.ui}</label>
|
||||
<input type="text" id="hw-보관위치" placeholder="예: 7층 비품창고" />
|
||||
</div>
|
||||
<div class="form-group op-only">
|
||||
<label for="hw-현재상태">${ASSET_SCHEMA.STATUS.ui}</label>
|
||||
<select id="hw-현재상태">${generateOptionsHTML(STATUS_LIST)}</select>
|
||||
</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>
|
||||
|
||||
<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주소">${ASSET_SCHEMA.IP_ADDR.ui}</label><input type="text" id="hw-IP주소" /></div>
|
||||
<div class="form-group server-only" id="hw-ip2-group"><label for="hw-IP2">${ASSET_SCHEMA.IP_ADDR2.ui}</label><input type="text" id="hw-IP2" /></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="text" id="hw-서버PW" /></div>
|
||||
<div class="form-group non-server" id="hw-ip-non-server-group"><label for="hw-IP주소-non-server">${ASSET_SCHEMA.IP_ADDR.ui}</label><input type="text" id="hw-IP주소-non-server" /></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-section-title" id="hw-spec-title">시스템 사양 (Specifications)</div>
|
||||
<div class="form-group" id="hw-model-group"><label for="hw-모델명">${ASSET_SCHEMA.MODEL.ui}</label><input type="text" id="hw-모델명" /></div>
|
||||
<div class="form-group pc-only" id="hw-mainboard-group"><label for="hw-메인보드">${ASSET_SCHEMA.MAINBOARD.ui}</label><input type="text" id="hw-메인보드" /></div>
|
||||
<div class="form-group" id="hw-os-group"><label for="hw-OS">${ASSET_SCHEMA.OS.ui}</label><input type="text" id="hw-OS" /></div>
|
||||
<div class="form-group" id="hw-cpu-group"><label for="hw-CPU">${ASSET_SCHEMA.CPU.ui}</label><input type="text" id="hw-CPU" /></div>
|
||||
<div class="form-group" id="hw-gpu-group"><label for="hw-GPU">${ASSET_SCHEMA.GPU.ui}</label><input type="text" id="hw-GPU" /></div>
|
||||
<div class="form-group" id="hw-ram-group"><label for="hw-RAM">${ASSET_SCHEMA.RAM.ui}</label><input type="text" id="hw-RAM" /></div>
|
||||
<div class="form-group" id="hw-ssd1-group"><label for="hw-SSD1">${ASSET_SCHEMA.STORAGE1.ui}</label><input type="text" id="hw-SSD1" /></div>
|
||||
<div class="form-group" id="hw-ssd2-group"><label for="hw-SSD2">${ASSET_SCHEMA.STORAGE2.ui}</label><input type="text" id="hw-SSD2" /></div>
|
||||
<div class="form-group" id="hw-ssd3-group"><label for="hw-SSD3">${ASSET_SCHEMA.STORAGE3.ui}</label><input type="text" id="hw-SSD3" /></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-group full-width">
|
||||
<label>${ASSET_SCHEMA.MEMO.ui}</label>
|
||||
<textarea id="hw-memo" name="memo" rows="2"></textarea>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="form-section-title" id="hw-loc-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-담당자_정">${ASSET_SCHEMA.MANAGER_MAIN.ui}</label><input type="text" id="hw-담당자_정" /></div>
|
||||
<div class="form-group"><label for="hw-담당자_부">${ASSET_SCHEMA.MANAGER_SUB.ui}</label><input type="text" id="hw-담당자_부" /></div>
|
||||
<div class="form-group"><label for="hw-구매일">${ASSET_SCHEMA.PURCHASE_YM.ui}</label><input type="text" id="hw-구매일" placeholder="YYYYMM" maxlength="6" /></div>
|
||||
<div class="form-group"><label for="hw-금액">${ASSET_SCHEMA.PRICE.ui}</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" id="hw-vendor-group"><label for="hw-납품업체">${ASSET_SCHEMA.VENDOR.ui}</label><input type="text" id="hw-납품업체" /></div>
|
||||
<div class="form-group full-width"><label for="hw-비고">${ASSET_SCHEMA.REMARKS.ui}</label><textarea id="hw-비고" rows="2"></textarea></div>
|
||||
<div class="form-group full-width">
|
||||
<label>${ASSET_SCHEMA.DOC_NAME.ui} (파일 증빙)</label>
|
||||
<div style="display:flex; align-items:center; gap:0.5rem;">
|
||||
<input type="file" id="hw-품의서" />
|
||||
<span id="hw-품의서명" style="font-size:0.75rem; color:var(--text-light)"></span>
|
||||
<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>
|
||||
`;
|
||||
|
||||
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 = '<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.replace(/\n/g, '<br>')}</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
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'),
|
||||
model: document.getElementById('hw-model-group'),
|
||||
mainboard: document.getElementById('hw-mainboard-group'),
|
||||
os: document.getElementById('hw-os-group'),
|
||||
cpu: document.getElementById('hw-cpu-group'),
|
||||
ram: document.getElementById('hw-ram-group'),
|
||||
gpu: document.getElementById('hw-gpu-group'),
|
||||
ssd1: document.getElementById('hw-ssd1-group'),
|
||||
ssd2: document.getElementById('hw-ssd2-group'),
|
||||
ssd3: document.getElementById('hw-ssd3-group'),
|
||||
hwSpec: document.getElementById('hw-hwspec-group'),
|
||||
monitoring: document.getElementById('hw-monitoring-group'),
|
||||
vendor: document.getElementById('hw-vendor-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 = ASSET_SCHEMA.OS.ui;
|
||||
if (ramLabel) ramLabel.innerText = ASSET_SCHEMA.RAM.ui;
|
||||
if (modelLabel) modelLabel.innerText = ASSET_SCHEMA.MODEL.ui;
|
||||
|
||||
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'; }
|
||||
} 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 (groups.mainboard) groups.mainboard.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', 'gpu', 'ram', 'ssd1', 'ssd2', 'ssd3', 'hwSpec', 'vendor'].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', 'gpu', 'ram', 'ssd1', 'ssd2', 'ssd3', 'monitoring', 'vendor'].forEach(k => { if (groups[k]) groups[k]!.style.display = 'flex'; });
|
||||
} else {
|
||||
nonServer.forEach(el => (el as HTMLElement).style.display = 'flex');
|
||||
['model', 'os', 'cpu', 'gpu', 'ram', 'ssd1', 'ssd2', 'ssd3', 'hwSpec', 'vendor'].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', 'gpu', 'ram', 'ssd1', 'ssd2', 'ssd3', 'monitoring', 'vendor'].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[ASSET_SCHEMA.MODEL.key]);
|
||||
if (!asset[ASSET_SCHEMA.PURCHASE_YM.key] && asset.구매일) setFieldValue('hw-구매일', asset.구매일);
|
||||
|
||||
parseAndSetLocation(asset[ASSET_SCHEMA.LOCATION.key], '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) {
|
||||
export function initHwModal(onSave: () => void, closeModals: () => 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);
|
||||
|
||||
const logModalHTML = `
|
||||
<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>${UI_TEXT.ACTION.HISTORY_ADD}</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">${UI_TEXT.ACTION.CANCEL}</button><button id="btn-confirm-hw-log" class="btn btn-primary">추가</button></div></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.insertAdjacentHTML('beforeend', logModalHTML);
|
||||
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 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')!;
|
||||
const btnCloseHeader = document.getElementById('btn-close-hw-modal')!;
|
||||
const btnCancelFooter = document.getElementById('btn-cancel-hw-modal')!;
|
||||
|
||||
[typeSelect, detailPurposeSelect].forEach(el => {
|
||||
el?.addEventListener('change', () => applyTypeSpecificUI(typeSelect.value));
|
||||
});
|
||||
bindLocationEvents('hw-bldg-select', 'hw-floor-select', 'hw-loc-etc-group', 'hw-loc-etc');
|
||||
applyDateMask(document.getElementById('hw-purchase_date') as HTMLInputElement);
|
||||
|
||||
bindLocationEvents('hw-위치-빌딩', 'hw-위치-상세', 'hw-위치-기타-group', 'hw-위치-기타');
|
||||
const closeModalAction = () => { closeModals(); isEditMode = false; };
|
||||
btnCloseHeader.addEventListener('click', closeModalAction);
|
||||
btnCancelFooter.addEventListener('click', closeModalAction);
|
||||
|
||||
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'
|
||||
});
|
||||
setEditLock('hw-asset-form', 'view', { saveBtnId: 'btn-save-hw-asset', revertBtnId: 'btn-revert-hw-edit' });
|
||||
isEditMode = false;
|
||||
if (currentAsset) openHwModal(currentAsset, 'view');
|
||||
if (currentHwAsset) fillHwFormData(currentHwAsset);
|
||||
});
|
||||
|
||||
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://172.16.40.100:3000/api/generate-asset-code?prefix=${prefix}`);
|
||||
const data = await res.json();
|
||||
if (data.nextCode) setFieldValue('hw-자산코드', data.nextCode);
|
||||
} catch (err) { alert('자산번호 생성에 실패했습니다.'); }
|
||||
});
|
||||
|
||||
['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;
|
||||
if (id === 'hw-OS' && label?.innerText !== '출시연월') return;
|
||||
target.value = target.value.replace(/[^0-9]/g, '').substring(0, 6);
|
||||
});
|
||||
});
|
||||
|
||||
saveBtn.addEventListener('click', () => {
|
||||
if (!currentAsset) return;
|
||||
saveBtn.addEventListener('click', async () => {
|
||||
if (!currentHwAsset) 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'
|
||||
});
|
||||
setEditLock('hw-asset-form', 'edit', { saveBtnId: 'btn-save-hw-asset', revertBtnId: 'btn-revert-hw-edit' });
|
||||
isEditMode = true;
|
||||
applyTypeSpecificUI(getFieldValue('hw-유형'));
|
||||
return;
|
||||
}
|
||||
|
||||
const extracted = autoExtractForm('hw', HW_FIELD_MAP);
|
||||
if (!extracted[ASSET_SCHEMA.ASSET_CODE.key]) {
|
||||
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));
|
||||
const formData = new FormData(form);
|
||||
const updated: any = { ...currentHwAsset };
|
||||
formData.forEach((value, key) => {
|
||||
if (key !== 'id') updated[key] = value;
|
||||
});
|
||||
|
||||
if (HW_TYPE_LIST.includes(extracted.type) || extracted.type === '개인PC') {
|
||||
const diffLogs: string[] = [];
|
||||
const compareFields = [
|
||||
{ key: ASSET_SCHEMA.ORG.key, label: ASSET_SCHEMA.ORG.ui },
|
||||
{ key: ASSET_SCHEMA.LOCATION.key, label: ASSET_SCHEMA.LOCATION.ui },
|
||||
{ key: ASSET_SCHEMA.MANAGER_MAIN.key, label: '담당자' },
|
||||
{ key: ASSET_SCHEMA.STATUS.key, label: ASSET_SCHEMA.STATUS.ui },
|
||||
{ key: ASSET_SCHEMA.IP_ADDR.key, label: ASSET_SCHEMA.IP_ADDR.ui },
|
||||
{ key: '상세용도', label: '상세유형' },
|
||||
{ key: ASSET_SCHEMA.MODEL.key, label: ASSET_SCHEMA.MODEL.ui }
|
||||
];
|
||||
// Handle combined location
|
||||
updated.location = getCombinedLocation('hw-bldg-select', 'hw-floor-select', 'hw-loc-etc');
|
||||
|
||||
if (!currentAsset || !currentAsset.자산코드) {
|
||||
diffLogs.push('자산 신규 등록');
|
||||
} else {
|
||||
const asset = currentAsset!;
|
||||
const newIp = String(getFieldValue('hw-IP주소') || getFieldValue('hw-IP주소-non-server') || '').trim();
|
||||
const newLocation = String(isOpType ? extracted[ASSET_SCHEMA.STORE_LOC.key] : getCombinedLocation('hw-위치-빌딩', 'hw-위치-상세', 'hw-위치-기타') || '').trim();
|
||||
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';
|
||||
|
||||
compareFields.forEach(f => {
|
||||
let oldVal = '';
|
||||
let newVal = '';
|
||||
|
||||
if (f.key === ASSET_SCHEMA.IP_ADDR.key) {
|
||||
oldVal = String(asset[ASSET_SCHEMA.IP_ADDR.key] || '').trim();
|
||||
newVal = newIp;
|
||||
} else if (f.key === ASSET_SCHEMA.LOCATION.key) {
|
||||
oldVal = String(asset[ASSET_SCHEMA.LOCATION.key] || '').trim();
|
||||
newVal = newLocation;
|
||||
} else if (f.key === ASSET_SCHEMA.MANAGER_MAIN.key) {
|
||||
oldVal = String(asset[ASSET_SCHEMA.MANAGER_MAIN.key] || '').trim();
|
||||
newVal = String(extracted[ASSET_SCHEMA.MANAGER_MAIN.key] || '').trim();
|
||||
} else if (f.key === '상세용도') {
|
||||
oldVal = String(asset.상세용도 || '').trim();
|
||||
newVal = String((extracted.type !== 'PC' && extracted.type !== '개인PC') ? extracted.type : (extracted.상세용도 || '')).trim();
|
||||
} else {
|
||||
oldVal = String((asset as any)[f.key] || '').trim();
|
||||
newVal = String(extracted[f.key] || '').trim();
|
||||
}
|
||||
|
||||
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,
|
||||
[ASSET_SCHEMA.IP_ADDR.key]: getFieldValue('hw-IP주소') || getFieldValue('hw-IP주소-non-server'),
|
||||
위치: isOpType ? extracted[ASSET_SCHEMA.STORE_LOC.key] : getCombinedLocation('hw-위치-빌딩', 'hw-위치-상세', 'hw-위치-기타')
|
||||
};
|
||||
|
||||
if (currentAsset[ASSET_SCHEMA.ORG.key] && currentAsset[ASSET_SCHEMA.ORG.key] !== extracted[ASSET_SCHEMA.ORG.key]) {
|
||||
updated[ASSET_SCHEMA.PREV_ORG.key] = currentAsset[ASSET_SCHEMA.ORG.key];
|
||||
}
|
||||
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(UI_TEXT.MESSAGES.CONFIRM_DELETE)) {
|
||||
deleteHardwareAsset(currentAsset.id);
|
||||
const success = await saveAsset(categoryKey, updated);
|
||||
if (success) {
|
||||
alert(UI_TEXT.MESSAGES.SAVE_SUCCESS);
|
||||
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 = '';
|
||||
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'
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
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('');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user