style: 리팩토링 및 CSS 통합 작업 완료 (하드코딩 스타일 제거)
This commit is contained in:
@@ -4,14 +4,14 @@ import { createIcons, X } from 'lucide';
|
||||
|
||||
const DASHBOARD_DETAIL_MODAL_HTML = `
|
||||
<div id="dashboard-detail-modal" class="modal-overlay hidden">
|
||||
<div class="modal-content wide" style="max-width: 1000px;">
|
||||
<div class="modal-content wide">
|
||||
<div class="modal-header">
|
||||
<h2 id="dashboard-detail-modal-title">상세 목록</h2>
|
||||
<h2 id="dashboard-detail-modal-title" class="modal-title">상세 목록</h2>
|
||||
<button id="btn-close-dashboard-detail-modal" class="btn-icon" aria-label="닫기"><i data-lucide="x"></i></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="table-container">
|
||||
<table style="width:100%;">
|
||||
<table>
|
||||
<thead></thead>
|
||||
<tbody id="dashboard-detail-tbody"></tbody>
|
||||
</table>
|
||||
|
||||
@@ -19,18 +19,14 @@ class HwAssetModal extends BaseModal {
|
||||
}
|
||||
|
||||
protected renderFrameHTML(): string {
|
||||
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" 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;">×</button>
|
||||
<h2 id="hw-modal-title" class="modal-title">${this.title}</h2>
|
||||
<button id="btn-close-hw-modal" class="btn-icon" aria-label="닫기">×</button>
|
||||
</div>
|
||||
<div class="modal-body" style="padding: 24px; overflow-y: auto;">
|
||||
<div class="modal-body">
|
||||
<div class="modal-body-split">
|
||||
<div class="modal-form-area">
|
||||
<form id="hw-asset-form" class="grid-form">
|
||||
@@ -38,195 +34,195 @@ class HwAssetModal extends BaseModal {
|
||||
<input type="hidden" id="hw-remotes-data" name="remotes" />
|
||||
|
||||
<!-- [SECTION 1] 기본 관리 정보 -->
|
||||
<div class="form-section-title" style="padding-top: 0; margin-bottom: 12px;">기본 관리 정보</div>
|
||||
<div class="form-section-title">기본 관리 정보</div>
|
||||
<div class="form-group">
|
||||
<label>${ASSET_SCHEMA.ASSET_CODE.ui}</label>
|
||||
<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 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">생성</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>${ASSET_SCHEMA.PURCHASE_CORP.ui}</label>
|
||||
<select id="hw-purchase_corp" name="purchase_corp" style="${inputStyle}">${generateOptionsHTML(CORP_LIST)}</select>
|
||||
<select id="hw-purchase_corp" name="purchase_corp">${generateOptionsHTML(CORP_LIST)}</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>${ASSET_SCHEMA.CATEGORY.ui}</label>
|
||||
<select id="hw-category" name="category" style="${inputStyle}">
|
||||
<select id="hw-category" name="category">
|
||||
<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" style="${inputStyle}">
|
||||
<select id="hw-asset_type" name="asset_type">
|
||||
<option value="">구분을 먼저 선택하세요</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>${ASSET_SCHEMA.HW_STATUS.ui}</label>
|
||||
<select id="hw-hw_status" name="hw_status" style="${inputStyle}">${generateOptionsHTML(HW_STATUS_LIST)}</select>
|
||||
<select id="hw-hw_status" name="hw_status">${generateOptionsHTML(HW_STATUS_LIST)}</select>
|
||||
</div>
|
||||
<div class="form-group infra-only monitoring-field">
|
||||
<label>${ASSET_SCHEMA.MONITORING.ui}</label>
|
||||
<select id="hw-monitoring" name="monitoring" style="${inputStyle}">
|
||||
<select id="hw-monitoring" name="monitoring">
|
||||
<option value="비대상">비대상</option>
|
||||
<option value="대상">대상</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- [SECTION 2] 조직 및 사용자 정보 -->
|
||||
<div class="form-section-title org-user-section" style="margin-top: 24px; margin-bottom: 12px;">사용자 및 조직 정보</div>
|
||||
<div class="form-section-title org-user-section">사용자 및 조직 정보</div>
|
||||
<div class="form-group org-user-field">
|
||||
<label>${ASSET_SCHEMA.CURRENT_DEPT.ui}</label>
|
||||
<select id="hw-current_dept" name="current_dept" style="${inputStyle}">${generateOptionsHTML(ORG_LIST)}</select>
|
||||
<select id="hw-current_dept" name="current_dept">${generateOptionsHTML(ORG_LIST)}</select>
|
||||
</div>
|
||||
<div class="form-group org-user-field">
|
||||
<label>${ASSET_SCHEMA.MANAGER_MAIN.ui}</label>
|
||||
<input type="text" id="hw-manager_primary" name="manager_primary" style="${inputStyle}" />
|
||||
<input type="text" id="hw-manager_primary" name="manager_primary" />
|
||||
</div>
|
||||
<div class="form-group personal-only">
|
||||
<label>${ASSET_SCHEMA.CURRENT_USER.ui}</label>
|
||||
<input type="text" id="hw-user_current" name="user_current" style="${inputStyle}" />
|
||||
<input type="text" id="hw-user_current" name="user_current" />
|
||||
</div>
|
||||
<div class="form-group personal-only">
|
||||
<label>${ASSET_SCHEMA.USER_POSITION.ui}</label>
|
||||
<input type="text" id="hw-user_position" name="user_position" style="${inputStyle}" />
|
||||
<input type="text" id="hw-user_position" name="user_position" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>${ASSET_SCHEMA.MANAGER_SUB.ui}</label>
|
||||
<input type="text" id="hw-manager_secondary" name="manager_secondary" style="${inputStyle}" />
|
||||
<input type="text" id="hw-manager_secondary" name="manager_secondary" />
|
||||
</div>
|
||||
<div class="form-group personal-only">
|
||||
<label>${ASSET_SCHEMA.PREV_USER.ui}</label>
|
||||
<input type="text" id="hw-previous_user" name="previous_user" style="${inputStyle}" />
|
||||
<input type="text" id="hw-previous_user" name="previous_user" />
|
||||
</div>
|
||||
|
||||
<!-- [SECTION 3] 하드웨어 사양 -->
|
||||
<div class="form-section-title hardware-section" style="margin-top: 24px; margin-bottom: 12px;">시스템 사양 정보</div>
|
||||
<div class="form-section-title hardware-section">시스템 사양 정보</div>
|
||||
<div class="form-group">
|
||||
<label>${ASSET_SCHEMA.MODEL_NAME.ui}</label>
|
||||
<input type="text" id="hw-model_name" name="model_name" style="${inputStyle}" />
|
||||
<input type="text" id="hw-model_name" name="model_name" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>${ASSET_SCHEMA.ASSET_MFR.ui}</label>
|
||||
<input type="text" id="hw-asset_mfr" name="asset_mfr" style="${inputStyle}" />
|
||||
<input type="text" id="hw-asset_mfr" name="asset_mfr" />
|
||||
</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}" />
|
||||
<input type="text" id="hw-serial_num" name="serial_num" />
|
||||
</div>
|
||||
<div class="form-group spec-only">
|
||||
<label>${ASSET_SCHEMA.OS.ui}</label>
|
||||
<input type="text" id="hw-os" name="os" style="${inputStyle}" />
|
||||
<input type="text" id="hw-os" name="os" />
|
||||
</div>
|
||||
<div class="form-group spec-only">
|
||||
<label>${ASSET_SCHEMA.CPU.ui}</label>
|
||||
<input type="text" id="hw-cpu" name="cpu" style="${inputStyle}" />
|
||||
<input type="text" id="hw-cpu" name="cpu" />
|
||||
</div>
|
||||
<div class="form-group spec-only">
|
||||
<label>${ASSET_SCHEMA.RAM.ui}</label>
|
||||
<input type="text" id="hw-ram" name="ram" style="${inputStyle}" />
|
||||
<input type="text" id="hw-ram" name="ram" />
|
||||
</div>
|
||||
<div class="form-group spec-only">
|
||||
<label>${ASSET_SCHEMA.GPU.ui}</label>
|
||||
<input type="text" id="hw-gpu" name="gpu" style="${inputStyle}" />
|
||||
<input type="text" id="hw-gpu" name="gpu" />
|
||||
</div>
|
||||
<div class="form-group spec-only">
|
||||
<label>${ASSET_SCHEMA.MAINBOARD.ui}</label>
|
||||
<input type="text" id="hw-mainboard" name="mainboard" style="${inputStyle}" />
|
||||
<input type="text" id="hw-mainboard" name="mainboard" />
|
||||
</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}" />
|
||||
<input type="text" id="hw-monitor_inch" name="monitor_inch" />
|
||||
</div>
|
||||
|
||||
<!-- 동적 디스크 할당 영역 -->
|
||||
<div class="form-section-title spec-only" style="margin-top: 24px; margin-bottom: 12px;">디스크(용량) 정보</div>
|
||||
<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 class="form-section-title spec-only">디스크(용량) 정보</div>
|
||||
<div class="form-group spec-only full-width">
|
||||
<div class="history-header">
|
||||
<label>연결된 드라이브 리스트</label>
|
||||
<button type="button" id="btn-add-volume" class="btn btn-outline btn-sm hidden">+ 볼륨 추가</button>
|
||||
</div>
|
||||
<div id="hw-volume-container" style="display: flex; flex-direction: column; gap: 8px;"></div>
|
||||
<div id="hw-volume-container" class="volume-container"></div>
|
||||
<input type="hidden" id="hw-volumes-data" name="volumes" />
|
||||
</div>
|
||||
|
||||
<!-- 통합 원격 접속 정보 영역 -->
|
||||
<div class="form-section-title net-only" style="margin-top: 24px; margin-bottom: 12px;">네트워크 및 원격 접속 정보</div>
|
||||
<div class="form-group net-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);">IP/MAC 및 접속 계정 정보</label>
|
||||
<button type="button" id="btn-add-remote-info" class="btn btn-outline" style="height: 26px !important; padding: 0 10px; font-size: 11px; display: none;">+ 접속 정보 추가</button>
|
||||
<div class="form-section-title net-only">네트워크 및 원격 접속 정보</div>
|
||||
<div class="form-group net-only full-width">
|
||||
<div class="history-header">
|
||||
<label>IP/MAC 및 접속 계정 정보</label>
|
||||
<button type="button" id="btn-add-remote-info" class="btn btn-outline btn-sm hidden">+ 접속 정보 추가</button>
|
||||
</div>
|
||||
<div id="hw-remote-info-container" style="display: flex; flex-direction: column; gap: 12px;"></div>
|
||||
<div id="hw-remote-info-container" class="remote-info-container"></div>
|
||||
</div>
|
||||
|
||||
<!-- [SECTION 5] 설치 위치 -->
|
||||
<div class="form-section-title location-section" style="margin-top: 24px; margin-bottom: 12px;">설치 위치</div>
|
||||
<div class="form-section-title location-section">설치 위치</div>
|
||||
<div class="form-group location-field">
|
||||
<label>건물/위치</label>
|
||||
<select id="hw-bldg-select" name="location" style="${inputStyle}">${generateOptionsHTML(Object.keys(LOCATION_DATA))}</select>
|
||||
<select id="hw-bldg-select" name="location">${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" 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 class="input-with-btn">
|
||||
<select id="hw-location_detail" name="location_detail"><option value="">선택</option></select>
|
||||
<button type="button" id="btn-reg-loc-map" class="btn btn-primary hidden">위치등록</button>
|
||||
<button type="button" id="btn-view-loc-map" class="btn btn-primary btn-loc-action btn-loc-view hidden">위치보기</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>
|
||||
|
||||
<!-- [SECTION 6] 구매 정보 -->
|
||||
<div class="form-section-title" style="margin-top: 24px; margin-bottom: 12px;">구매 및 증빙 정보</div>
|
||||
<div class="form-section-title">구매 및 증빙 정보</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" style="${inputStyle}" />
|
||||
<input type="text" id="hw-purchase_date" name="purchase_date" placeholder="YYYY-MM-DD" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>${ASSET_SCHEMA.PURCHASE_VENDOR.ui}</label>
|
||||
<input type="text" id="hw-purchase_vendor" name="purchase_vendor" style="${inputStyle}" />
|
||||
<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" placeholder="0" style="${inputStyle}" 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" 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" 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;">
|
||||
<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">
|
||||
<span id="hw-file-name-display">파일 선택...</span>
|
||||
</button>
|
||||
</div>
|
||||
<input type="hidden" id="hw-approval_document" name="approval_document" />
|
||||
<div id="hw-file-link-container" style="margin-top: 4px;"></div>
|
||||
<div id="hw-file-link-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group full-width">
|
||||
<label>${ASSET_SCHEMA.MEMO.ui}</label>
|
||||
<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>
|
||||
<textarea id="hw-memo" name="memo" rows="3"></textarea>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="modal-history-area">
|
||||
<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 class="history-header">
|
||||
<h3>자산 변동 이력</h3>
|
||||
<button type="button" id="btn-add-hw-log" class="btn btn-outline btn-sm">이력 추가</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" style="height: 42px;">삭제</button>
|
||||
<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" 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>
|
||||
<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>
|
||||
@@ -395,25 +391,20 @@ class HwAssetModal extends BaseModal {
|
||||
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' : ''}>
|
||||
<select class="vol-type" ${!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' : ''}>
|
||||
<input type="number" class="vol-cap" value="${vol.capacity || ''}" placeholder="용량" ${!this.isEditMode ? 'readonly' : ''} />
|
||||
<select class="vol-unit" ${!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'};">×</button>
|
||||
<button type="button" class="btn btn-outline btn-remove-vol edit-only-btn" style="display: ${this.isEditMode ? 'inline-flex' : 'none'};">×</button>
|
||||
`;
|
||||
|
||||
row.querySelector('.btn-remove-vol')?.addEventListener('click', () => row.remove());
|
||||
@@ -456,7 +447,8 @@ class HwAssetModal extends BaseModal {
|
||||
// Second Line: Tool & Credentials (Only for IP)
|
||||
const line2 = document.createElement('div');
|
||||
line2.className = 'ri-line ri-cred-line';
|
||||
line2.style.display = info.type === 'IP' ? 'flex' : 'none';
|
||||
if (info.type !== 'IP') line2.classList.add('hidden');
|
||||
|
||||
line2.innerHTML = `
|
||||
<div class="ri-connector"></div>
|
||||
<select class="ri-tool" ${!this.isEditMode ? 'disabled' : ''}>
|
||||
@@ -466,7 +458,7 @@ class HwAssetModal extends BaseModal {
|
||||
</select>
|
||||
<input type="text" class="ri-id" value="${parsedId}" placeholder="원격 ID" ${!this.isEditMode ? 'readonly' : ''} />
|
||||
<input type="text" class="ri-pw" value="${parsedPw}" placeholder="원격 PW" ${!this.isEditMode ? 'readonly' : ''} />
|
||||
<div class="ri-spacer"></div> <!-- Spacer for the remove button width -->
|
||||
<div class="ri-spacer"></div>
|
||||
`;
|
||||
|
||||
row.appendChild(line1);
|
||||
@@ -476,7 +468,7 @@ class HwAssetModal extends BaseModal {
|
||||
const typeSelect = row.querySelector('.ri-type') as HTMLSelectElement;
|
||||
typeSelect.addEventListener('change', (e) => {
|
||||
const isIP = (e.target as HTMLSelectElement).value === 'IP';
|
||||
line2.style.display = isIP ? 'flex' : 'none';
|
||||
line2.classList.toggle('hidden', !isIP);
|
||||
if (!isIP) {
|
||||
(row.querySelector('.ri-id') as HTMLInputElement).value = '';
|
||||
(row.querySelector('.ri-pw') as HTMLInputElement).value = '';
|
||||
@@ -741,9 +733,9 @@ class HwAssetModal extends BaseModal {
|
||||
private renderHistory(assetId: string) {
|
||||
const container = document.getElementById('hw-history-list');
|
||||
if (!container) return;
|
||||
const logs = (state.masterData.logs || []).filter(l => l.asset_id === assetId);
|
||||
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.log_date}</div><div class=\"history-user\">${l.log_user}</div><div class=\"history-details\">${l.details}</div></div>`).join('');
|
||||
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('');
|
||||
}
|
||||
|
||||
private getCategoryKey(asset: any): string {
|
||||
|
||||
@@ -100,12 +100,12 @@ class SwAssetModal extends BaseModal {
|
||||
<div class="form-section-title">관리 및 비고</div>
|
||||
<div class="form-group sw-standard-field">
|
||||
<label>${ASSET_SCHEMA.PURCHASE_DATE.ui}</label>
|
||||
<div style="display:flex; gap:0.25rem; align-items:center; position:relative;">
|
||||
<input type="text" id="sw-구매일" name="purchase_date" style="flex:1;" />
|
||||
<button type="button" class="btn-icon" onclick="const p = document.getElementById('sw-구매일-picker'); p.value = document.getElementById('sw-구매일').value; p.showPicker();" style="padding:0.25rem;">
|
||||
<i data-lucide="calendar" style="width:18px; height:18px; color:var(--primary-color);"></i>
|
||||
<div class="input-with-btn">
|
||||
<input type="text" id="sw-구매일" name="purchase_date" />
|
||||
<button type="button" class="btn-icon" onclick="const p = document.getElementById('sw-구매일-picker'); p.value = document.getElementById('sw-구매일').value; p.showPicker();">
|
||||
<i data-lucide="calendar"></i>
|
||||
</button>
|
||||
<input type="date" id="sw-구매일-picker" style="position:absolute; width:0; height:0; opacity:0; pointer-events:none;" onchange="document.getElementById('sw-구매일').value = this.value" tabindex="-1" />
|
||||
<input type="date" id="sw-구매일-picker" class="hidden-picker" onchange="document.getElementById('sw-구매일').value = this.value" tabindex="-1" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group sw-standard-field">
|
||||
@@ -126,12 +126,12 @@ class SwAssetModal extends BaseModal {
|
||||
</div>
|
||||
<div class="form-group sw-standard-field" id="sw-expiry-group">
|
||||
<label>${ASSET_SCHEMA.EXPIRED_DATE.ui}</label>
|
||||
<div style="display:flex; gap:0.25rem; align-items:center; position:relative;">
|
||||
<input type="text" id="sw-만료일" name="expiry_date" style="flex:1;" />
|
||||
<button type="button" class="btn-icon" onclick="const p = document.getElementById('sw-만료일-picker'); p.value = document.getElementById('sw-만료일').value; p.showPicker();" style="padding:0.25rem;">
|
||||
<i data-lucide="calendar" style="width:18px; height:18px; color:var(--primary-color);"></i>
|
||||
<div class="input-with-btn">
|
||||
<input type="text" id="sw-만료일" name="expiry_date" />
|
||||
<button type="button" class="btn-icon" onclick="const p = document.getElementById('sw-만료일-picker'); p.value = document.getElementById('sw-만료일').value; p.showPicker();">
|
||||
<i data-lucide="calendar"></i>
|
||||
</button>
|
||||
<input type="date" id="sw-만료일-picker" style="position:absolute; width:0; height:0; opacity:0; pointer-events:none;" onchange="document.getElementById('sw-만료일').value = this.value" tabindex="-1" />
|
||||
<input type="date" id="sw-만료일-picker" class="hidden-picker" onchange="document.getElementById('sw-만료일').value = this.value" tabindex="-1" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group full-width">
|
||||
@@ -140,18 +140,18 @@ class SwAssetModal extends BaseModal {
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div id="sw-user-section" class="user-management-section" style="margin-top: 2rem; border-top: 1px solid var(--border-color); padding-top: 1.5rem;">
|
||||
<div id="sw-user-section" class="user-management-section">
|
||||
<button type="button" id="btn-open-sw-user" class="btn btn-outline btn-sm" title="사용자 관리">
|
||||
<i data-lucide="users" style="width:16px; height:16px; margin-right:4px;"></i> 사용자 관리
|
||||
<i data-lucide="users"></i> 사용자 관리
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-history-area">
|
||||
<div class="history-header" style="display:flex; justify-content:space-between; align-items:center;">
|
||||
<h3><i data-lucide="history" style="width:16px; height:16px;"></i> 업데이트 내역</h3>
|
||||
<div class="history-header">
|
||||
<h3><i data-lucide="history"></i> 업데이트 내역</h3>
|
||||
<button type="button" id="btn-open-sw-update" class="btn btn-outline btn-sm">
|
||||
계약 업데이트 <i data-lucide="refresh-ccw" style="width:14px; height:14px;"></i>
|
||||
계약 업데이트 <i data-lucide="refresh-ccw"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div id="sw-history-list" class="history-timeline"></div>
|
||||
@@ -173,7 +173,7 @@ class SwAssetModal extends BaseModal {
|
||||
<div id="sw-update-modal" class="modal-overlay hidden" style="z-index: 1100;">
|
||||
<div class="modal-content" style="max-width: 500px;">
|
||||
<div class="modal-header">
|
||||
<h2>계약 업데이트 반영</h2>
|
||||
<h2 class="modal-title">계약 업데이트 반영</h2>
|
||||
<button id="btn-close-sw-update" class="btn-icon"><i data-lucide="x"></i></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
@@ -184,10 +184,10 @@ class SwAssetModal extends BaseModal {
|
||||
</div>
|
||||
<div class="form-group sub-sw-update">
|
||||
<label>새로운 계약 기간</label>
|
||||
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
||||
<input type="text" id="sw-update-start" placeholder="YYYY-MM-DD" style="flex: 1;" />
|
||||
<div class="input-with-btn">
|
||||
<input type="text" id="sw-update-start" placeholder="YYYY-MM-DD" />
|
||||
<span>~</span>
|
||||
<input type="text" id="sw-update-end" placeholder="YYYY-MM-DD" style="flex: 1;" />
|
||||
<input type="text" id="sw-update-end" placeholder="YYYY-MM-DD" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
|
||||
@@ -78,12 +78,8 @@ export function renderNavigation(onTabChange: (tab: string) => void) {
|
||||
adminGroup.className = 'nav-group';
|
||||
|
||||
const adminTrigger = document.createElement('div');
|
||||
adminTrigger.className = 'gnb-trigger';
|
||||
adminTrigger.className = 'gnb-trigger admin-trigger';
|
||||
adminTrigger.innerHTML = '관리자';
|
||||
adminTrigger.style.color = 'var(--text-muted)';
|
||||
adminTrigger.style.borderLeft = '1px solid var(--border-color)';
|
||||
adminTrigger.style.marginLeft = '1rem';
|
||||
adminTrigger.style.paddingLeft = '1.5rem';
|
||||
|
||||
adminTrigger.addEventListener('click', () => {
|
||||
window.open('/map_editor.html', '_blank');
|
||||
|
||||
@@ -189,7 +189,9 @@ input:checked + .slider:before { transform: translateX(16px); }
|
||||
.badge-light { background: var(--bg-color); color: var(--text-muted); border: 1px solid var(--border-color); }
|
||||
.hidden { display: none !important; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.brand h1 .sub-title { display: none; }
|
||||
.header-actions .btn span { display: none; }
|
||||
.gnb-trigger.admin-trigger {
|
||||
color: var(--text-muted);
|
||||
border-left: 1px solid var(--border-color);
|
||||
margin-left: 1rem;
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
|
||||
@@ -52,12 +52,101 @@
|
||||
min-height: 360px;
|
||||
}
|
||||
|
||||
.dashboard-card canvas {
|
||||
flex: 1;
|
||||
width: 100% !important;
|
||||
max-height: 280px;
|
||||
.dashboard-stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.dashboard-card-mini {
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.dashboard-card h4 {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-main);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.dashboard-card .description {
|
||||
font-size: 0.8125rem;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.dashboard-card .big-value {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.dashboard-card .big-value.danger {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
background-color: var(--border-color);
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.progress-fill.danger {
|
||||
background-color: var(--danger);
|
||||
}
|
||||
|
||||
.chart-wrapper {
|
||||
height: 250px;
|
||||
}
|
||||
|
||||
.dashboard-table-container {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border-color);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dashboard-table-container table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.dashboard-table-container th {
|
||||
padding: 0.75rem;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-muted);
|
||||
border-bottom: 2px solid var(--border-color);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.dashboard-table-container td {
|
||||
padding: 0.75rem;
|
||||
font-size: 0.8125rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.badge-type {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
background: var(--bg-color);
|
||||
border: 1px solid var(--border-color);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
|
||||
/* --- Location View Styles --- */
|
||||
.location-layout {
|
||||
display: grid;
|
||||
|
||||
@@ -40,10 +40,13 @@
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.modal-header h2 {
|
||||
.modal-header h2, .modal-title {
|
||||
margin: 0;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.02em;
|
||||
color: var(--white);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.modal-header .btn-icon {
|
||||
@@ -51,6 +54,8 @@
|
||||
cursor: pointer;
|
||||
background: none !important;
|
||||
border: none !important;
|
||||
font-size: 1.75rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
@@ -97,75 +102,15 @@
|
||||
font-size: 0.875rem;
|
||||
font-weight: 700;
|
||||
color: var(--primary-color);
|
||||
padding: 1.5rem 0 0.5rem 0; /* 패딩 조정 */
|
||||
padding: 1.5rem 0 0.5rem 0;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
margin-bottom: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Modal Readonly/Edit Mode Interaction */
|
||||
.grid-form.is-view-mode input,
|
||||
.grid-form.is-view-mode select,
|
||||
.grid-form.is-view-mode textarea {
|
||||
border: none !important;
|
||||
background-color: transparent !important;
|
||||
padding-left: 0 !important;
|
||||
padding-right: 0 !important;
|
||||
pointer-events: none !important;
|
||||
color: var(--text-main) !important;
|
||||
font-weight: 500 !important;
|
||||
appearance: none !important;
|
||||
-webkit-appearance: none !important;
|
||||
-moz-appearance: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.grid-form.is-view-mode input[type="file"] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.grid-form.is-view-mode .btn-helper {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.grid-form.is-view-mode button:not(.btn-loc-action) {
|
||||
pointer-events: none !important;
|
||||
background: none !important;
|
||||
border: none !important;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.grid-form.is-view-mode select::-ms-expand {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.grid-form.is-edit-mode input,
|
||||
.grid-form.is-edit-mode select,
|
||||
.grid-form.is-edit-mode textarea {
|
||||
color: var(--edit-mode-color); /* 수정 시 글자색 변경 */
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
/* 수동 수정 불가 필드 (자산번호 등) 전용 스타일 */
|
||||
.grid-form input[readonly] {
|
||||
border-color: transparent !important;
|
||||
background-color: transparent !important;
|
||||
pointer-events: none !important;
|
||||
color: var(--text-main) !important;
|
||||
font-weight: 500 !important;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.grid-form.is-edit-mode input:focus,
|
||||
.grid-form.is-edit-mode select:focus,
|
||||
.grid-form.is-edit-mode textarea:focus {
|
||||
border-color: var(--edit-mode-color);
|
||||
box-shadow: 0 0 0 2px var(--edit-mode-focus);
|
||||
}
|
||||
|
||||
.form-section-title:first-child {
|
||||
padding-top: 0.5rem;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
@@ -177,17 +122,21 @@
|
||||
.form-group input,
|
||||
.form-group select,
|
||||
.form-group textarea {
|
||||
padding: 0.625rem;
|
||||
height: 38px;
|
||||
padding: 0 0.625rem;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
font-family: inherit;
|
||||
font-size: 0.875rem;
|
||||
font-size: 0.8125rem;
|
||||
outline: none;
|
||||
transition: all 0.2s;
|
||||
background-color: var(--white);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.form-group textarea {
|
||||
height: auto;
|
||||
padding: 0.625rem;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
@@ -208,6 +157,29 @@
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.modal-footer .btn {
|
||||
height: 42px;
|
||||
padding: 0 1.25rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.edit-only-btn {
|
||||
/* Dynamic visibility handled in JS, but base style here */
|
||||
}
|
||||
|
||||
/* File Upload Display */
|
||||
.file-upload-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
#hw-file-name-display {
|
||||
font-size: 0.8125rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
|
||||
.footer-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
|
||||
@@ -230,3 +230,91 @@ th.sortable.desc::after {
|
||||
opacity: 1;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
/* --- Mini Table for System Status --- */
|
||||
.mini-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.mini-table thead {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: var(--white);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.mini-table th {
|
||||
padding: 10px 0;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: var(--text-muted);
|
||||
border-bottom: 2px solid var(--border-color);
|
||||
background: var(--white);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mini-table th:nth-child(2) {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.mini-row {
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.mini-row:hover {
|
||||
background-color: #F8FAFA;
|
||||
}
|
||||
|
||||
.mini-row.active {
|
||||
background-color: #EBF2F1 !important;
|
||||
}
|
||||
|
||||
.mini-row td {
|
||||
padding: 10px 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mini-row td:nth-child(2) {
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.mini-row.warning {
|
||||
background-color: #FFF1F2 !important;
|
||||
border-left: 3px solid #E11D48;
|
||||
}
|
||||
|
||||
.mini-row.warning td {
|
||||
color: #991B1B !important;
|
||||
}
|
||||
|
||||
.warning-badge {
|
||||
background: #FFF1F2;
|
||||
color: #E11D48;
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #FDA4AF;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.warning-badge-orange {
|
||||
background: #FFF7ED;
|
||||
color: #C2410C;
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #FFEDD5;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
||||
@@ -64,56 +64,58 @@ export function renderHwDashboard(container: HTMLElement) {
|
||||
// 2. UI 렌더링
|
||||
container.innerHTML = `
|
||||
<div class="view-container">
|
||||
<div class="dashboard-header-stats" style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 1.5rem; margin-bottom: 2rem;">
|
||||
<div class="dashboard-card" style="min-height:auto;">
|
||||
<span style="font-size:1rem; font-weight:700; color:var(--text-main);">전체 평균 사용 연수</span>
|
||||
<div style="font-size: 0.8125rem; color:var(--text-muted); margin-bottom: 1rem;">전체 자산 기준 (권장 4.5년)</div>
|
||||
<div style="font-size: 2rem; font-weight:700; color:var(--dash-primary);">${avgAge}년</div>
|
||||
<div style="width: 100%; height: 4px; background-color: var(--border-color); border-radius: 2px; overflow: hidden; margin-top: 0.5rem;">
|
||||
<div style="width: ${Math.min(100, (Number(avgAge)/4.5)*100)}%; height: 100%; background-color: var(--dash-primary);"></div>
|
||||
<div class="dashboard-stats-grid">
|
||||
<div class="dashboard-card dashboard-card-mini">
|
||||
<h4>전체 평균 사용 연수</h4>
|
||||
<div class="description">전체 자산 기준 (권장 4.5년)</div>
|
||||
<div class="big-value">${avgAge}년</div>
|
||||
<div class="progress-container">
|
||||
<div class="progress-fill" style="width: ${Math.min(100, (Number(avgAge)/4.5)*100)}%;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-card" style="min-height:auto;">
|
||||
<span style="font-size:1rem; font-weight:700; color:var(--text-main);">5년 이상 노후 자산 비율</span>
|
||||
<div style="font-size: 0.8125rem; color:var(--text-muted); margin-bottom: 1rem;">총 ${over5YearsCount}대 해당</div>
|
||||
<div style="font-size: 2rem; font-weight:700; color:${over5Rate >= 20 ? 'var(--dash-danger)' : 'var(--dash-primary)'};">${over5Rate}%</div>
|
||||
<div style="width: 100%; height: 4px; background-color: var(--border-color); border-radius: 2px; overflow: hidden; margin-top: 0.5rem;">
|
||||
<div style="width: ${over5Rate}%; height: 100%; background-color: ${over5Rate >= 20 ? 'var(--dash-danger)' : 'var(--dash-primary)'};"></div>
|
||||
<div class="dashboard-card dashboard-card-mini">
|
||||
<h4>5년 이상 노후 자산 비율</h4>
|
||||
<div class="description">총 ${over5YearsCount}대 해당</div>
|
||||
<div class="big-value ${over5Rate >= 20 ? 'danger' : ''}">${over5Rate}%</div>
|
||||
<div class="progress-container">
|
||||
<div class="progress-fill ${over5Rate >= 20 ? 'danger' : ''}" style="width: ${over5Rate}%;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-card" style="min-height:auto;">
|
||||
<span style="font-size:1rem; font-weight:700; color:var(--text-main);">최신 도입 모델 (${latestYear}년)</span>
|
||||
<div style="font-size: 0.8125rem; color:var(--text-muted); margin-bottom: 1rem;">자산번호: ${latestAsset ? latestAsset[ASSET_SCHEMA.ASSET_CODE.key] : '-'}</div>
|
||||
<div style="font-size: 1.25rem; font-weight:700; color:var(--primary-color); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; height: 3rem; display: flex; align-items: center;" title="${latestAsset ? latestAsset[ASSET_SCHEMA.MODEL_NAME.key] : '정보 없음'}">
|
||||
<div class="dashboard-card dashboard-card-mini">
|
||||
<h4>최신 도입 모델 (${latestYear}년)</h4>
|
||||
<div class="description">자산번호: ${latestAsset ? latestAsset[ASSET_SCHEMA.ASSET_CODE.key] : '-'}</div>
|
||||
<div class="big-value" style="font-size: 1.25rem; height: 3rem; display: flex; align-items: center;" title="${latestAsset ? latestAsset[ASSET_SCHEMA.MODEL_NAME.key] : '정보 없음'}">
|
||||
${latestAsset ? (latestAsset[ASSET_SCHEMA.MODEL_NAME.key] || latestAsset[ASSET_SCHEMA.ASSET_NAME.key] || '정보 없음') : '정보 없음'}
|
||||
</div>
|
||||
<div style="width: 100%; height: 4px; background-color: var(--primary-color); border-radius: 2px; margin-top: 0.5rem;"></div>
|
||||
<div class="progress-container">
|
||||
<div class="progress-fill" style="width: 100%;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-layout-2col" style="margin-bottom: 2rem;">
|
||||
<div class="dashboard-card">
|
||||
<h4 class="card-title">자산 노후도 분포</h4>
|
||||
<div style="height: 250px;"><canvas id="chart-aging-dist"></canvas></div>
|
||||
<div class="chart-wrapper"><canvas id="chart-aging-dist"></canvas></div>
|
||||
</div>
|
||||
<div class="dashboard-card">
|
||||
<h4 class="card-title">연도별 자산 도입 추이</h4>
|
||||
<div style="height: 250px;"><canvas id="chart-purchase-trend"></canvas></div>
|
||||
<div class="chart-wrapper"><canvas id="chart-purchase-trend"></canvas></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="dashboard-section-title">⚠️ 교체 검토 대상 (가장 오래된 자산 TOP 10)</h3>
|
||||
<div class="table-container" style="background: white; border-radius: 8px; border: 1px solid var(--border-color);">
|
||||
<div class="dashboard-table-container">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="text-align:center;">순위</th>
|
||||
<th style="text-align:center;">자산번호</th>
|
||||
<th style="text-align:center;">유형</th>
|
||||
<th style="text-align:center;">모델명</th>
|
||||
<th style="text-align:center;">담당자</th>
|
||||
<th style="text-align:center;">구매일자</th>
|
||||
<th style="text-align:center;">연령</th>
|
||||
<th>순위</th>
|
||||
<th>자산번호</th>
|
||||
<th>유형</th>
|
||||
<th>모델명</th>
|
||||
<th>담당자</th>
|
||||
<th>구매일자</th>
|
||||
<th>연령</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -121,14 +123,14 @@ export function renderHwDashboard(container: HTMLElement) {
|
||||
const pDate = a[ASSET_SCHEMA.PURCHASE_DATE.key];
|
||||
const age = calculateAssetAge(pDate);
|
||||
return `
|
||||
<tr class="clickable-row" data-id="${a.id}" style="cursor:pointer;">
|
||||
<td style="text-align:center; font-weight:600; color:var(--text-muted)">${i + 1}</td>
|
||||
<td style="text-align:center;">${a[ASSET_SCHEMA.ASSET_CODE.key] || '-'}</td>
|
||||
<td style="text-align:center;"><span class="badge-type">${a[ASSET_SCHEMA.ASSET_TYPE.key] || a.category || ''}</span></td>
|
||||
<td>${a[ASSET_SCHEMA.MODEL_NAME.key] || a[ASSET_SCHEMA.ASSET_NAME.key] || '-'}</td>
|
||||
<td style="text-align:center;">${a[ASSET_SCHEMA.MANAGER_MAIN.key] || '-'}</td>
|
||||
<td style="text-align:center;">${pDate || '-'}</td>
|
||||
<td style="text-align:center;"><strong style="color:${age >= 5 ? 'var(--danger)' : 'inherit'}">${age}년</strong></td>
|
||||
<tr class="clickable-row" data-id="${a.id}">
|
||||
<td style="font-weight:600; color:var(--text-muted)">${i + 1}</td>
|
||||
<td>${a[ASSET_SCHEMA.ASSET_CODE.key] || '-'}</td>
|
||||
<td><span class="badge-type">${a[ASSET_SCHEMA.ASSET_TYPE.key] || a.category || ''}</span></td>
|
||||
<td style="text-align: left; padding-left: 1rem;">${a[ASSET_SCHEMA.MODEL_NAME.key] || a[ASSET_SCHEMA.ASSET_NAME.key] || '-'}</td>
|
||||
<td>${a[ASSET_SCHEMA.MANAGER_MAIN.key] || '-'}</td>
|
||||
<td>${pDate || '-'}</td>
|
||||
<td><strong style="color:${age >= 5 ? 'var(--danger)' : 'inherit'}">${age}년</strong></td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('')}
|
||||
|
||||
@@ -37,34 +37,34 @@ export function renderSwDashboard(container: HTMLElement) {
|
||||
<h3 class="dashboard-section-title">소프트웨어 라이선스 현황</h3>
|
||||
|
||||
<div class="dashboard-layout-2col" style="margin-bottom: 1.5rem;">
|
||||
<div class="dashboard-card" data-action="ext-usage" style="cursor:pointer; min-height:auto;">
|
||||
<span style="font-size:1rem; font-weight:700; color:var(--text-main);">외부 소프트웨어 사용율</span>
|
||||
<div style="font-size: 0.8125rem; color:var(--text-muted); margin-bottom: 1rem;">${extQty}카피 중 ${extUsed}개 할당</div>
|
||||
<div style="font-size: 2rem; font-weight:700; color:var(--dash-primary);">${extPer}%</div>
|
||||
<div style="width: 100%; height: 4px; background-color: var(--border-color); border-radius: 2px; overflow: hidden; margin-top: 0.5rem;">
|
||||
<div style="width: ${extPer}%; height: 100%; background-color: var(--dash-primary);"></div>
|
||||
<div class="dashboard-card dashboard-card-mini clickable-card" data-action="ext-usage">
|
||||
<h4>외부 소프트웨어 사용율</h4>
|
||||
<div class="description">${extQty}카피 중 ${extUsed}개 할당</div>
|
||||
<div class="big-value">${extPer}%</div>
|
||||
<div class="progress-container">
|
||||
<div class="progress-fill" style="width: ${extPer}%;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-card" data-action="int-usage" style="cursor:pointer; min-height:auto;">
|
||||
<span style="font-size:1rem; font-weight:700; color:var(--text-main);">내부 소프트웨어 현황</span>
|
||||
<div style="font-size: 0.8125rem; color:var(--text-muted); margin-bottom: 1rem;">등록된 내부 솔루션: ${intTotal}개</div>
|
||||
<div style="font-size: 2rem; font-weight:700; color:var(--dash-primary);">${intPer}%</div>
|
||||
<div style="width: 100%; height: 4px; background-color: var(--border-color); border-radius: 2px; overflow: hidden; margin-top: 0.5rem;">
|
||||
<div style="width: ${intPer}%; height: 100%; background-color: var(--dash-primary);"></div>
|
||||
<div class="dashboard-card dashboard-card-mini clickable-card" data-action="int-usage">
|
||||
<h4>내부 소프트웨어 현황</h4>
|
||||
<div class="description">등록된 내부 솔루션: ${intTotal}개</div>
|
||||
<div class="big-value">${intPer}%</div>
|
||||
<div class="progress-container">
|
||||
<div class="progress-fill" style="width: ${intPer}%;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="dashboard-section-title">2026년 누적 도입 비용 분석</h3>
|
||||
|
||||
<div style="display:grid; grid-template-columns: repeat(2, 1fr); gap:1.5rem; margin-bottom:1.5rem;">
|
||||
<div class="dashboard-card" style="min-height:auto;">
|
||||
<span style="font-size:1rem; font-weight:700; color:var(--text-main);">외부 SW 누적 비용 (2026)</span>
|
||||
<div style="font-size: 2rem; font-weight:700; color:var(--dash-primary);">₩ ${extCost2026.toLocaleString()}</div>
|
||||
<div class="dashboard-layout-2col">
|
||||
<div class="dashboard-card dashboard-card-mini">
|
||||
<h4>외부 SW 누적 비용 (2026)</h4>
|
||||
<div class="big-value">₩ ${extCost2026.toLocaleString()}</div>
|
||||
</div>
|
||||
<div class="dashboard-card" style="min-height:auto;">
|
||||
<span style="font-size:1rem; font-weight:700; color:var(--text-main);">내부 SW 누적 비용 (2026)</span>
|
||||
<div style="font-size: 2rem; font-weight:700; color:#3b82f6;">₩ ${intCost2026.toLocaleString()}</div>
|
||||
<div class="dashboard-card dashboard-card-mini">
|
||||
<h4>내부 SW 누적 비용 (2026)</h4>
|
||||
<div class="big-value" style="color:#3b82f6;">₩ ${intCost2026.toLocaleString()}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema';
|
||||
import { dynamicSort, renderPageHeader, calculateAssetAge, formatInline } from '../../core/utils';
|
||||
import { dynamicSort, renderPageHeader, calculateAssetAge } from '../../core/utils';
|
||||
import { setupTableSorting, SortState } from '../../core/tableHandler';
|
||||
import { renderFilterBar, applyCommonFilters } from '../../core/filterHandler';
|
||||
import { state } from '../../core/state';
|
||||
@@ -41,7 +41,6 @@ export function createListView(container: HTMLElement, config: ListViewConfig) {
|
||||
let sortState: SortState = config.persistentSortState || { key: '', direction: 'asc' };
|
||||
let currentFilters: any = { keyword: '', corp: '', dept: '', loc: '', field: '', type: '' };
|
||||
|
||||
// 서버 탭이 아닐 경우 '자산 현황(대시보드)' 뷰 진입 방지 및 강제 'asset' 모드
|
||||
const isServer = config.title === '서버';
|
||||
if (!isServer) {
|
||||
(state as any).currentViewMode = 'asset';
|
||||
@@ -49,17 +48,17 @@ export function createListView(container: HTMLElement, config: ListViewConfig) {
|
||||
(state as any).currentViewMode = 'system';
|
||||
}
|
||||
|
||||
// 2. 뷰 전환 토글 버튼 생성 (명칭 변경)
|
||||
// 2. 뷰 전환 토글 버튼 생성
|
||||
const toggleWrapper = document.createElement('div');
|
||||
toggleWrapper.className = 'view-toggle-container';
|
||||
toggleWrapper.innerHTML = `
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; width: 100%;">
|
||||
<div class="view-toggle" style="display: ${isServer ? 'flex' : 'none'}; gap: 0;">
|
||||
<div class="view-toggle-flex-row">
|
||||
<div class="view-toggle" style="display: ${isServer ? 'flex' : 'none'};">
|
||||
<button class="toggle-btn ${(state as any).currentViewMode === 'system' ? 'active' : ''}" data-mode="system">자산 현황</button>
|
||||
<button class="toggle-btn ${(state as any).currentViewMode === 'asset' ? 'active' : ''}" data-mode="asset">자산 목록</button>
|
||||
</div>
|
||||
<button id="btn-add-asset" style="padding: 6px 14px; font-size: 12px; font-weight: 700; background: #1E5149; color: white; border: none; border-radius: 4px; cursor: pointer; display: flex; align-items: center; gap: 4px;">
|
||||
<span style="font-size: 16px; line-height: 1;">+</span> 자산 추가
|
||||
<button id="btn-add-asset" class="btn btn-primary btn-add-new">
|
||||
<span class="plus-icon">+</span> 자산 추가
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
@@ -80,7 +79,6 @@ export function createListView(container: HTMLElement, config: ListViewConfig) {
|
||||
let selectedDetailLocation: string | null = null;
|
||||
let dynamicMapConfig: Record<string, any[]> = {};
|
||||
|
||||
// 맵 설정 미리 로드
|
||||
const fetchMapConfig = async () => {
|
||||
try {
|
||||
const res = await fetch(`http://${location.hostname}:3000/api/maps`);
|
||||
@@ -93,60 +91,26 @@ export function createListView(container: HTMLElement, config: ListViewConfig) {
|
||||
const renderSystemStatus = () => {
|
||||
const isPcView = config.title === 'PC';
|
||||
|
||||
const locationCounts: Record<string, number> = {};
|
||||
const pcTypeCounts = { public: 0, server: 0, personal: 0 };
|
||||
|
||||
// 동적 통계 수집 객체 (Hardcoding 제거)
|
||||
const extStats = {
|
||||
total: 0,
|
||||
locCounts: {} as Record<string, number>,
|
||||
typeCounts: {} as Record<string, number>,
|
||||
typeLocMap: {} as Record<string, Record<string, number>>, // 유형별 위치 분포
|
||||
locWarning: 0,
|
||||
typeWarning: 0
|
||||
};
|
||||
const intStats = {
|
||||
total: 0,
|
||||
locCounts: {} as Record<string, number>,
|
||||
typeCounts: {} as Record<string, number>,
|
||||
typeLocMap: {} as Record<string, Record<string, number>>
|
||||
};
|
||||
const extStats = { total: 0, locCounts: {} as Record<string, number>, typeCounts: {} as Record<string, number>, typeLocMap: {} as Record<string, Record<string, number>>, locWarning: 0, typeWarning: 0 };
|
||||
const intStats = { total: 0, locCounts: {} as Record<string, number>, typeCounts: {} as Record<string, number>, typeLocMap: {} as Record<string, Record<string, number>> };
|
||||
|
||||
// 중앙화된 경고 감지 로직
|
||||
const checkAnomaly = (serviceType: string, loc: string, type: string) => {
|
||||
if (serviceType !== '외부') return { isWarning: false, isLocWarning: false, isTypeWarning: false, reason: '' };
|
||||
if (serviceType !== '외부') return { isWarning: false, isLocWarning: false, isTypeWarning: false };
|
||||
const isLocWarning = loc !== 'IDC' && loc !== '미지정' && loc !== '';
|
||||
const isTypeWarning = type.toLowerCase().replace(/\s/g, '').includes('서버pc');
|
||||
const isWarning = isLocWarning || isTypeWarning;
|
||||
|
||||
let reason = '';
|
||||
if (isLocWarning && isTypeWarning) reason = '위치/형식 부적절';
|
||||
else if (isLocWarning) reason = '위치 부적절';
|
||||
else if (isTypeWarning) reason = '형식 부적절';
|
||||
|
||||
return { isWarning, isLocWarning, isTypeWarning, reason };
|
||||
return { isWarning: isLocWarning || isTypeWarning, isLocWarning, isTypeWarning };
|
||||
};
|
||||
|
||||
fullList.forEach(asset => {
|
||||
const loc = asset[ASSET_SCHEMA.LOCATION.key] || '미지정';
|
||||
const serviceTypeKey = (ASSET_SCHEMA as any).SERVICE_TYPE?.key || 'service_type';
|
||||
const serviceType = asset[serviceTypeKey] || '외부';
|
||||
const serviceType = asset.service_type || '외부';
|
||||
const type = asset[ASSET_SCHEMA.ASSET_TYPE.key] || '';
|
||||
|
||||
locationCounts[loc] = (locationCounts[loc] || 0) + 1;
|
||||
|
||||
if (isPcView) {
|
||||
if (type.includes('공용')) pcTypeCounts.public++;
|
||||
else if (type.includes('서버')) pcTypeCounts.server++;
|
||||
else pcTypeCounts.personal++;
|
||||
}
|
||||
|
||||
const targetStat = serviceType === '내부' ? intStats : extStats;
|
||||
targetStat.total++;
|
||||
if (loc) targetStat.locCounts[loc] = (targetStat.locCounts[loc] || 0) + 1;
|
||||
if (type) {
|
||||
targetStat.typeCounts[type] = (targetStat.typeCounts[type] || 0) + 1;
|
||||
// 유형별 위치 분포 수집
|
||||
if (!targetStat.typeLocMap[type]) targetStat.typeLocMap[type] = {};
|
||||
targetStat.typeLocMap[type][loc] = (targetStat.typeLocMap[type][loc] || 0) + 1;
|
||||
}
|
||||
@@ -158,136 +122,94 @@ export function createListView(container: HTMLElement, config: ListViewConfig) {
|
||||
}
|
||||
});
|
||||
|
||||
// 템플릿 제너레이터 함수 (HTML 중복 제거)
|
||||
const generateDetailStatHTML = (title: string, stats: any) => `
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 0.5rem; gap: 0.5rem;">
|
||||
<span style="font-size: 14px; font-weight: 800; color: var(--text-main); white-space: nowrap;">${title}</span>
|
||||
<div style="display: flex; gap: 4px; flex-wrap: wrap; justify-content: flex-end;">
|
||||
${stats.locWarning ? `<span style="background: #FFF7ED; color: #C2410C; font-size: 10px; font-weight: 800; padding: 2px 6px; border-radius: 4px; border: 1px solid #FFEDD5; white-space: nowrap;">위치부적절: ${stats.locWarning}</span>` : ''}
|
||||
${stats.typeWarning ? `<span style="background: #FFF1F2; color: #E11D48; font-size: 10px; font-weight: 800; padding: 2px 6px; border-radius: 4px; border: 1px solid #FDA4AF; white-space: nowrap;">형식부적절: ${stats.typeWarning}</span>` : ''}
|
||||
<div class="detail-stat-header">
|
||||
<span class="stat-title">${title}</span>
|
||||
<div class="stat-badges">
|
||||
${stats.locWarning ? `<span class="warning-badge-orange">위치부적절: ${stats.locWarning}</span>` : ''}
|
||||
${stats.typeWarning ? `<span class="warning-badge">형식부적절: ${stats.typeWarning}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; flex-direction: column; gap: 0.3rem; font-size: 13px; color: var(--text-muted);">
|
||||
<div style="display: flex; gap: 0.75rem; flex-wrap: wrap;">
|
||||
${Object.entries(stats.locCounts as Record<string, number>).sort((a, b) => b[1] - a[1]).slice(0, 4).map(([l, c]) => `<span>${l}: <strong style="color:var(--text-main); font-size: 14px;">${c}</strong></span>`).join('')}
|
||||
<div class="detail-stat-body">
|
||||
<div class="loc-summary">
|
||||
${Object.entries(stats.locCounts as Record<string, number>).sort((a, b) => b[1] - a[1]).slice(0, 4).map(([l, c]) => `<span>${l}: <strong>${c}</strong></span>`).join('')}
|
||||
</div>
|
||||
<div style="display: flex; gap: 0.6rem; flex-wrap: wrap; opacity: 0.9; border-top: 1px dashed var(--border-color); padding-top: 4px; margin-top: 2px;">
|
||||
<div class="type-summary">
|
||||
${Object.entries(stats.typeCounts as Record<string, number>).sort((a, b) => b[1] - a[1]).slice(0, 6).map(([t, c]) => {
|
||||
const isTypeWarning = title.includes('외부') && t.toLowerCase().replace(/\s/g, '').includes('서버pc');
|
||||
|
||||
// 위치별 상세 정보 생성 (툴팁용)
|
||||
const locDist = stats.typeLocMap[t] || {};
|
||||
const locHint = Object.entries(locDist)
|
||||
.sort((a: any, b: any) => b[1] - a[1])
|
||||
.map(([l, count]) => `${l}: ${count}대`)
|
||||
.join('\n');
|
||||
|
||||
return `<span title="${locHint}" style="${isTypeWarning ? 'color:#E11D48; font-weight:700;' : ''}; font-size: 13px; cursor: help;">${t}: <strong style="color:var(--text-main); font-size: 14px;">${c}</strong></span>`;
|
||||
const locHint = Object.entries(locDist).sort((a: any, b: any) => b[1] - a[1]).map(([l, count]) => `${l}: ${count}대`).join('\n');
|
||||
return `<span title="${locHint}">${t}: <strong>${c}</strong></span>`;
|
||||
}).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
contentWrapper.innerHTML = `
|
||||
<div class="system-dashboard" style="height: calc(100vh - 240px); overflow: hidden; padding: 0.5rem 0; font-family: 'Pretendard', sans-serif; letter-spacing: -0.02em; display: flex; flex-direction: column;">
|
||||
|
||||
<!-- [자산 통계 그룹] -->
|
||||
<div style="border-bottom: 1px solid var(--border-color); padding-bottom: 1.25rem; margin-bottom: 1rem; flex-shrink: 0; display: grid; grid-template-columns: 1fr 1.5fr 1.5fr; gap: 2rem;">
|
||||
<div class="stat-group-item" style="min-width: 0;">
|
||||
<div style="font-size: 11px; font-weight: 600; color: var(--text-muted); margin-bottom: 0.25rem;">총 보유 자산</div>
|
||||
<div style="font-size: 28px; font-weight: 800; color: var(--text-main); line-height: 1.1;">${fullList.length}<span style="font-size: 13px; font-weight: 600; margin-left: 4px; color: var(--text-muted);">대</span></div>
|
||||
<div style="display: flex; gap: 0.75rem; font-size: 14px; color: var(--text-muted); margin-top: 0.5rem;">
|
||||
<span>외부: <strong style="color:#35635C; font-size: 18px;">${extStats.total}</strong></span>
|
||||
<span>내부: <strong style="color:#94A3B8; font-size: 18px;">${intStats.total}</strong></span>
|
||||
<div class="system-dashboard">
|
||||
<div class="dashboard-stats-row">
|
||||
<div class="stat-group-item">
|
||||
<div class="stat-label">총 보유 자산</div>
|
||||
<div class="stat-value">${fullList.length}<span>대</span></div>
|
||||
<div class="stat-sub">
|
||||
<span>외부: <strong class="text-primary">${extStats.total}</strong></span>
|
||||
<span>내부: <strong class="text-muted">${intStats.total}</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-group-item" style="border-left: 1px solid var(--border-color); padding-left: 1.5rem; min-width: 0;">
|
||||
${isPcView ? `
|
||||
<div style="font-size: 11px; font-weight: 600; color: var(--text-muted); margin-bottom: 0.25rem;">PC 유형별 현황</div>
|
||||
<div style="display: flex; gap: 1rem; font-size: 14px; color: var(--text-muted); margin-top: 0.5rem;">
|
||||
<span>공용: <strong style="color:var(--text-main); font-size: 18px;">${pcTypeCounts.public}</strong></span>
|
||||
<span>서버: <strong style="color:var(--text-main); font-size: 18px;">${pcTypeCounts.server}</strong></span>
|
||||
<span>개인: <strong style="color:var(--text-main); font-size: 18px;">${pcTypeCounts.personal}</strong></span>
|
||||
</div>
|
||||
` : generateDetailStatHTML('외부 (운영) 상세', extStats)}
|
||||
</div>
|
||||
|
||||
<div class="stat-group-item" style="border-left: 1px solid var(--border-color); padding-left: 1.5rem; min-width: 0;">
|
||||
${isPcView ? '' : generateDetailStatHTML('내부 (테스트) 상세', intStats as any)}
|
||||
</div>
|
||||
<div class="stat-group-item bordered">${generateDetailStatHTML('외부 (운영) 상세', extStats)}</div>
|
||||
<div class="stat-group-item bordered">${generateDetailStatHTML('내부 (테스트) 상세', intStats)}</div>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; flex: 1; min-height: 0; border-top: 1px solid var(--border-color);">
|
||||
<!-- 좌측: 자산 현황 목록 (Border-based Separation) -->
|
||||
<div class="list-section" style="flex: 1.1; display: flex; flex-direction: column; min-height: 0; padding: 1rem 1.5rem 0 0; border-right: 1px solid var(--border-color);">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; flex-shrink: 0;">
|
||||
<h4 id="list-section-title" style="font-size: 14px; font-weight: 700; color: var(--text-main); margin:0;">자산 현황 목록</h4>
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<span style="font-size: 11px; font-weight: 600; color: var(--text-muted);">위치:</span>
|
||||
<select id="select-loc" style="padding: 2px 8px; font-size: 11px; border-radius: 4px; border: 1px solid var(--border-color); outline: none; background: white; cursor:pointer; font-family: 'Pretendard';">
|
||||
<div class="dashboard-main-flex">
|
||||
<div class="list-section">
|
||||
<div class="section-header-flex">
|
||||
<h4 id="list-section-title">자산 현황 목록</h4>
|
||||
<div class="filter-controls">
|
||||
<span class="filter-label">위치:</span>
|
||||
<select id="select-loc" class="filter-select">
|
||||
<option value="">전체</option>
|
||||
${Array.from(new Set(fullList.map(a => a[ASSET_SCHEMA.LOCATION.key] || '미지정'))).sort().map(l => `<option value="${l}" ${l === selectedLocation ? 'selected' : ''}>${l}</option>`).join('')}
|
||||
</select>
|
||||
<span style="font-size: 11px; font-weight: 600; color: var(--text-muted);">상세:</span>
|
||||
<select id="select-detail-loc" style="padding: 2px 8px; font-size: 11px; border-radius: 4px; border: 1px solid var(--border-color); outline: none; background: white; cursor:pointer; font-family: 'Pretendard'; max-width: 120px;"></select>
|
||||
<select id="select-detail-loc" class="filter-select select-detail-loc"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div style="flex: 1; overflow-y: auto;">
|
||||
<table style="width: 100%; border-collapse: collapse; table-layout: fixed;">
|
||||
<thead style="position: sticky; top: 0; background: #fff; z-index: 10;">
|
||||
<tr style="text-align: left; font-size: 11px; color: var(--text-muted);">
|
||||
<th style="padding: 10px 0; font-weight: 700; border-bottom: 2px solid var(--border-color); width: 80px; text-align:center; background: #fff;">분류</th>
|
||||
<th style="padding: 10px 0; font-weight: 700; border-bottom: 2px solid var(--border-color); width: 130px; background: #fff;">용도/자산명</th>
|
||||
<th style="padding: 10px 0; font-weight: 700; border-bottom: 2px solid var(--border-color); text-align:center; width: 90px; background: #fff;">관리자(정)</th>
|
||||
<th style="padding: 10px 0; font-weight: 700; border-bottom: 2px solid var(--border-color); text-align:center; width: 90px; background: #fff;">관리자(부)</th>
|
||||
<th style="padding: 10px 0; text-align: center; font-weight: 700; border-bottom: 2px solid var(--border-color); width: 100px; background: #fff;">상세위치</th>
|
||||
<div class="mini-table-container">
|
||||
<table class="mini-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 80px;">분류</th>
|
||||
<th>용도/자산명</th>
|
||||
<th style="width: 90px;">관리자(정)</th>
|
||||
<th style="width: 90px;">관리자(부)</th>
|
||||
<th style="width: 100px;">상세위치</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="system-status-tbody" style="font-size: 12px;"></tbody>
|
||||
<tbody id="system-status-tbody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 우측: 상세 정보 패널 (Box-less, Line-based) -->
|
||||
<div id="system-detail-panel" style="flex: 0.9; display: flex; flex-direction: column; min-height: 0; padding: 1rem 0 0 1.5rem; overflow: hidden;">
|
||||
<div id="detail-empty-state" style="height: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center; color: var(--text-muted); text-align: center;">
|
||||
<p style="font-size: 1.125rem; font-weight: 500; color: #94A3B8;">목록에서 자산을 선택하면<br>상세 정보와 배치도가 표시됩니다.</p>
|
||||
<div id="system-detail-panel" class="detail-panel">
|
||||
<div id="detail-empty-state" class="detail-empty-state">
|
||||
<p>목록에서 자산을 선택하면 상세 정보가 표시됩니다.</p>
|
||||
</div>
|
||||
<div id="detail-content" style="display: none; height: 100%; flex-direction: column;">
|
||||
<!-- 상단 요약 정보 (Wrapping 방지 최적화) -->
|
||||
<div style="display: flex; justify-content: space-between; align-items: flex-end; margin-bottom: 1.5rem; padding-bottom: 1rem; border-bottom: 1px solid var(--border-color); flex-shrink: 0; gap: 2rem;">
|
||||
<div style="display: flex; gap: 2.5rem; align-items: flex-end; min-width: 0; flex: 1;">
|
||||
<div style="flex-shrink: 0;">
|
||||
<label style="display: block; font-size: 10px; font-weight: 700; color: var(--text-muted); text-transform: uppercase; margin-bottom: 4px;">자산번호</label>
|
||||
<div id="detail-asset-code" style="font-size: 14px; font-weight: 800; color: var(--primary-color); white-space: nowrap;"></div>
|
||||
</div>
|
||||
<div style="flex-shrink: 0;">
|
||||
<label style="display: block; font-size: 10px; font-weight: 700; color: var(--text-muted); text-transform: uppercase; margin-bottom: 4px;">유형</label>
|
||||
<div id="detail-asset-type" style="font-size: 14px; font-weight: 600; color: var(--text-main); white-space: nowrap;"></div>
|
||||
</div>
|
||||
<div style="flex: 1; min-width: 0;">
|
||||
<label style="display: block; font-size: 10px; font-weight: 700; color: var(--text-muted); text-transform: uppercase; margin-bottom: 4px;">메모 요약</label>
|
||||
<div id="detail-memo" style="font-size: 14px; color: var(--text-main); font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;"></div>
|
||||
</div>
|
||||
<div id="detail-content" class="detail-content hidden">
|
||||
<div class="detail-summary-header">
|
||||
<div class="summary-items">
|
||||
<div class="summary-item"><label>자산번호</label><div id="detail-asset-code" class="code-value"></div></div>
|
||||
<div class="summary-item"><label>유형</label><div id="detail-asset-type" class="type-value"></div></div>
|
||||
<div class="summary-item flex-1"><label>메모 요약</label><div id="detail-memo" class="memo-value"></div></div>
|
||||
</div>
|
||||
<button id="btn-view-full-detail" style="flex-shrink: 0; padding: 6px 16px; font-size: 12px; font-weight: 700; background: var(--primary-color); color: white; border: none; border-radius: 4px; cursor: pointer; transition: opacity 0.2s;">상세 보기</button>
|
||||
<button id="btn-view-full-detail" class="btn btn-primary">상세 보기</button>
|
||||
</div>
|
||||
|
||||
<!-- 메인 배치도 영역 -->
|
||||
<div style="flex: 1; display: flex; flex-direction: column; min-height: 0; overflow: hidden;">
|
||||
<div style="margin-bottom: 0.75rem; flex-shrink: 0; display: flex; justify-content: space-between; align-items: center;">
|
||||
<label style="font-size: 11px; font-weight: 700; color: var(--text-main); text-transform: uppercase;">설치 위치 배치도</label>
|
||||
</div>
|
||||
<div id="detail-photo-wrapper" style="width: 100%; flex: 1; overflow: hidden; display: flex; align-items: center; justify-content: center; position: relative; border: 1px solid var(--border-color); background: #f0f0f0;">
|
||||
<div class="layout-map-container readonly" style="position: relative; display: flex; align-items: center; justify-content: center; width: 100%; height: 100%;">
|
||||
<img id="detail-photo" src="" style="display: block; max-width: 100%; max-height: 100%; width: auto; height: auto; object-fit: contain; pointer-events: none;" />
|
||||
<div id="detail-marker" class="layout-marker pulse-marker" style="display: none; position: absolute; z-index: 20;"></div>
|
||||
<div id="detail-overlay-layer" class="digital-overlay-layer" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; display: flex; align-items: center; justify-content: center;"></div>
|
||||
</div>
|
||||
<div id="detail-no-photo" style="display: none; height: 100%; flex-direction: column; align-items: center; justify-content: center; gap: 1rem;">
|
||||
<span style="color: #94A3B8; font-size: 13px; font-weight: 500;">등록된 배치도가 없습니다.</span>
|
||||
<div class="detail-map-section">
|
||||
<div class="map-label-header"><label>설치 위치 배치도</label></div>
|
||||
<div id="detail-photo-wrapper" class="map-photo-wrapper">
|
||||
<div class="layout-map-container readonly">
|
||||
<img id="detail-photo" src="" class="map-photo" />
|
||||
<div id="detail-marker" class="layout-marker pulse-marker hidden"></div>
|
||||
<div id="detail-overlay-layer" class="digital-overlay-layer"></div>
|
||||
</div>
|
||||
<div id="detail-no-photo" class="no-photo-state hidden"><span>등록된 배치도가 없습니다.</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -296,16 +218,15 @@ export function createListView(container: HTMLElement, config: ListViewConfig) {
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 상세 정보 패널 업데이트 함수
|
||||
const updateDetailPanel = (asset: any) => {
|
||||
const emptyState = document.getElementById('detail-empty-state');
|
||||
const content = document.getElementById('detail-content');
|
||||
if (!emptyState || !content) return;
|
||||
|
||||
emptyState.style.display = 'none';
|
||||
emptyState.classList.add('hidden');
|
||||
content.classList.remove('hidden');
|
||||
content.style.display = 'flex';
|
||||
|
||||
// 텍스트 정보 업데이트
|
||||
const codeEl = document.getElementById('detail-asset-code');
|
||||
const typeEl = document.getElementById('detail-asset-type');
|
||||
const memoEl = document.getElementById('detail-memo');
|
||||
@@ -314,75 +235,44 @@ export function createListView(container: HTMLElement, config: ListViewConfig) {
|
||||
if (codeEl) codeEl.textContent = asset.asset_code || '미지정';
|
||||
if (typeEl) typeEl.textContent = asset.asset_type || '-';
|
||||
if (memoEl) memoEl.textContent = asset.memo || '-';
|
||||
if (viewBtn) {
|
||||
viewBtn.onclick = () => config.onRowClick && config.onRowClick(asset);
|
||||
}
|
||||
if (viewBtn) viewBtn.onclick = () => config.onRowClick && config.onRowClick(asset);
|
||||
|
||||
// 위치 및 사진 정보 업데이트
|
||||
const photo = document.getElementById('detail-photo') as HTMLImageElement;
|
||||
const marker = document.getElementById('detail-marker');
|
||||
const overlayLayer = document.getElementById('detail-overlay-layer');
|
||||
const noPhoto = document.getElementById('detail-no-photo');
|
||||
const photoWrapper = document.getElementById('detail-photo-wrapper');
|
||||
|
||||
const bldg = asset.location || '';
|
||||
const detail = asset.location_detail || '';
|
||||
// 숫자 0도 유효한 좌표이므로 정확한 체크 필요
|
||||
const x = asset.loc_x;
|
||||
const y = asset.loc_y;
|
||||
const hasCoords = (x !== null && x !== undefined && x !== '' && x !== 'null') &&
|
||||
(y !== null && y !== undefined && y !== '' && y !== 'null');
|
||||
const x = asset.loc_x; const y = asset.loc_y;
|
||||
const hasCoords = (x !== null && x !== undefined && x !== '' && x !== 'null');
|
||||
|
||||
const savedImg = asset.location_photo || asset.loc_img;
|
||||
const locImgs = IMAGE_LOCATIONS[bldg.trim()]?.[detail.trim()] || null;
|
||||
const imgPath = (savedImg && locImgs?.includes(savedImg)) ? savedImg : (locImgs ? locImgs[0] : null);
|
||||
|
||||
// 좌표가 없으면 사진이 있어도 '정보 없음' 상태로 유도 (사용자 요청)
|
||||
if (imgPath && hasCoords) {
|
||||
photo.src = imgPath;
|
||||
photo.style.display = 'block';
|
||||
if (noPhoto) noPhoto.style.display = 'none';
|
||||
|
||||
photo.classList.remove('hidden');
|
||||
if (noPhoto) noPhoto.classList.add('hidden');
|
||||
photo.onload = () => {
|
||||
const updateMarkerPos = () => {
|
||||
const imgW = photo.clientWidth;
|
||||
const imgH = photo.clientHeight;
|
||||
|
||||
const imgW = photo.clientWidth; const imgH = photo.clientHeight;
|
||||
if (marker) {
|
||||
marker.style.left = `calc(50% - ${imgW/2}px + ${ (parseFloat(x as string) * imgW) / 100 }px)`;
|
||||
marker.style.top = `calc(50% - ${imgH/2}px + ${ (parseFloat(y as string) * imgH) / 100 }px)`;
|
||||
marker.style.display = 'block';
|
||||
marker.classList.remove('hidden');
|
||||
}
|
||||
|
||||
if (overlayLayer) {
|
||||
overlayLayer.style.width = `${imgW}px`;
|
||||
overlayLayer.style.height = `${imgH}px`;
|
||||
overlayLayer.style.left = `calc(50% - ${imgW/2}px)`;
|
||||
overlayLayer.style.top = `calc(50% - ${imgH/2}px)`;
|
||||
|
||||
overlayLayer.style.width = `${imgW}px`; overlayLayer.style.height = `${imgH}px`;
|
||||
overlayLayer.style.left = `calc(50% - ${imgW/2}px)`; overlayLayer.style.top = `calc(50% - ${imgH/2}px)`;
|
||||
const boxes = dynamicMapConfig[imgPath] || [];
|
||||
if (boxes.length > 0) {
|
||||
overlayLayer.innerHTML = `
|
||||
<svg viewBox="0 0 100 100" preserveAspectRatio="none" style="width:100%; height:100%;">
|
||||
<g class="seat-group">
|
||||
${boxes.map((b, i) => {
|
||||
const isSelected = b.x === x && b.y === y;
|
||||
const fill = isSelected ? 'rgba(255, 61, 0, 0.4)' : 'rgba(30, 81, 73, 0.02)';
|
||||
const stroke = isSelected ? '#FF3D00' : 'rgba(30, 81, 73, 0.15)';
|
||||
const strokeWidth = isSelected ? '0.8' : '0.2';
|
||||
|
||||
if (isSelected && marker) {
|
||||
marker.style.left = `calc(50% - ${imgW/2}px + ${ (parseFloat(b.x) + parseFloat(b.w)/2) * imgW / 100 }px)`;
|
||||
marker.style.top = `calc(50% - ${imgH/2}px + ${ (parseFloat(b.y) + parseFloat(b.h)/2) * imgH / 100 }px)`;
|
||||
}
|
||||
|
||||
return `<rect class="map-seat-obj" x="${b.x}" y="${b.y}" width="${b.w}" height="${b.h}" rx="0.5" style="fill:${fill}; stroke:${stroke}; stroke-width:${strokeWidth};" />`;
|
||||
}).join('')}
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
} else {
|
||||
overlayLayer.innerHTML = '';
|
||||
overlayLayer.innerHTML = `<svg viewBox="0 0 100 100" preserveAspectRatio="none" style="width:100%; height:100%;"><g class="seat-group">
|
||||
${boxes.map(b => {
|
||||
const isSelected = b.x === x && b.y === y;
|
||||
return `<rect class="map-seat-obj" x="${b.x}" y="${b.y}" width="${b.w}" height="${b.h}" rx="0.5" style="fill:${isSelected ? 'rgba(255, 61, 0, 0.4)' : 'rgba(30, 81, 73, 0.02)'}; stroke:${isSelected ? '#FF3D00' : 'rgba(30, 81, 73, 0.15)'};" />`;
|
||||
}).join('')}</g></svg>`;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -390,220 +280,93 @@ export function createListView(container: HTMLElement, config: ListViewConfig) {
|
||||
window.addEventListener('resize', updateMarkerPos);
|
||||
};
|
||||
} else {
|
||||
photo.style.display = 'none';
|
||||
if (marker) marker.style.display = 'none';
|
||||
photo.classList.add('hidden');
|
||||
if (marker) marker.classList.add('hidden');
|
||||
if (overlayLayer) overlayLayer.innerHTML = '';
|
||||
if (noPhoto) {
|
||||
noPhoto.style.display = 'flex';
|
||||
const msg = noPhoto.querySelector('span');
|
||||
if (msg) msg.textContent = !hasCoords ? '등록된 위치 좌표 정보가 없습니다.' : '등록된 배치도가 없습니다.';
|
||||
}
|
||||
if (noPhoto) { noPhoto.classList.remove('hidden'); noPhoto.style.display = 'flex'; }
|
||||
}
|
||||
};
|
||||
|
||||
const updateTableOnly = () => {
|
||||
let filtered = selectedLocation
|
||||
? fullList.filter(a => (a[ASSET_SCHEMA.LOCATION.key] || '미지정') === selectedLocation)
|
||||
: fullList;
|
||||
const currentDetailLocs = Array.from(new Set(filtered.map(a => a[ASSET_SCHEMA.LOC_DETAIL.key] || '미지정'))).sort();
|
||||
let filtered = selectedLocation ? fullList.filter(a => (a[ASSET_SCHEMA.LOCATION.key] || '미지정') === selectedLocation) : fullList;
|
||||
if (selectedDetailLocation) filtered = filtered.filter(a => (a[ASSET_SCHEMA.LOC_DETAIL.key] || '미지정') === selectedDetailLocation);
|
||||
const finalDisplayList = (!selectedLocation && !selectedDetailLocation) ? filtered.slice(0, 10) : filtered;
|
||||
|
||||
const titleEl = document.getElementById('list-section-title');
|
||||
if (titleEl) titleEl.textContent = selectedLocation ? `${selectedLocation} 자산 현황 (${finalDisplayList.length}대)` : '위치별 자산등록현황 (최근 등록)';
|
||||
const selectEl = document.getElementById('select-detail-loc') as HTMLSelectElement;
|
||||
if (selectEl && !selectedDetailLocation) {
|
||||
selectEl.innerHTML = `<option value="">전체보기</option>` + currentDetailLocs.map(dl => `<option value="${dl}">${dl}</option>`).join('');
|
||||
}
|
||||
const displayList = (!selectedLocation && !selectedDetailLocation) ? filtered.slice(0, 10) : filtered;
|
||||
|
||||
const tbody = document.getElementById('system-status-tbody');
|
||||
if (tbody) {
|
||||
tbody.innerHTML = finalDisplayList.length === 0
|
||||
? `<tr><td colspan="4" style="padding: 3rem; text-align: center; color: var(--text-muted);">조회된 자산이 없습니다.</td></tr>`
|
||||
: finalDisplayList.map(asset => {
|
||||
const purpose = asset[ASSET_SCHEMA.ASSET_PURPOSE.key] || '';
|
||||
const serviceTypeKey = (ASSET_SCHEMA as any).SERVICE_TYPE?.key || 'service_type';
|
||||
const serviceType = asset[serviceTypeKey] || '외부';
|
||||
const type = asset[ASSET_SCHEMA.ASSET_TYPE.key] || '';
|
||||
tbody.innerHTML = displayList.length === 0
|
||||
? `<tr><td colspan="5" class="empty-cell">조회된 자산이 없습니다.</td></tr>`
|
||||
: displayList.map(asset => {
|
||||
const serviceType = asset.service_type || '외부';
|
||||
const loc = asset[ASSET_SCHEMA.LOCATION.key] || '';
|
||||
|
||||
const labelColor = serviceType === '내부' ? '#94A3B8' : '#35635C';
|
||||
const managerMain = asset[ASSET_SCHEMA.MANAGER_MAIN.key] || '-';
|
||||
const managerSub = asset[ASSET_SCHEMA.MANAGER_SUB.key] || '-';
|
||||
|
||||
// [경고 로직] 외부 운영인데 서버PC이거나 IDC가 아닌 경우
|
||||
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;' : '';
|
||||
|
||||
let warningReason = '';
|
||||
if (isLocWarning && isTypeWarning) warningReason = '위치/형식 부적절';
|
||||
else if (isLocWarning) warningReason = '위치 부적절';
|
||||
else if (isTypeWarning) warningReason = '형식 부적절';
|
||||
|
||||
const isWarning = serviceType === '외부' && loc !== 'IDC' && loc !== '미지정';
|
||||
return `
|
||||
<tr style="border-bottom: 1px solid var(--border-color); cursor: pointer; ${warningStyle}" class="mini-row" data-id="${asset.id}">
|
||||
<td style="padding: 10px 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; text-align:center;">
|
||||
<div style="display:flex; flex-direction:column; align-items:center; gap:2px;">
|
||||
<span style="color: ${isWarning ? '#E11D48' : labelColor}; font-weight: 800; font-size: 12px;">${serviceType}</span>
|
||||
${isWarning ? `<span style="color: #E11D48; font-size: 9px; font-weight: 700; white-space: nowrap;">${warningReason}</span>` : ''}
|
||||
</div>
|
||||
</td>
|
||||
<td style="padding: 10px 0; font-weight: 600; color: ${isWarning ? '#991B1B' : 'var(--text-main)'}; font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;" title="${purpose}">${purpose || '-'}</td>
|
||||
<td style="padding: 10px 0; text-align: center; color: var(--text-main); font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${managerMain}</td>
|
||||
<td style="padding: 10px 0; text-align: center; color: var(--text-main); font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${managerSub}</td>
|
||||
<td style="padding: 10px 0; text-align: center; color: var(--text-main); font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${asset[ASSET_SCHEMA.LOC_DETAIL.key] || '-'}</td>
|
||||
<tr class="mini-row ${isWarning ? 'warning' : ''}" data-id="${asset.id}">
|
||||
<td><div class="cell-service-type"><span class="fw-800">${serviceType}</span></div></td>
|
||||
<td title="${asset.asset_purpose || ''}">${asset.asset_purpose || '-'}</td>
|
||||
<td>${asset.manager_primary || '-'}</td>
|
||||
<td>${asset.manager_secondary || '-'}</td>
|
||||
<td>${asset.location_detail || '-'}</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
tbody.querySelectorAll('.mini-row').forEach(row => {
|
||||
row.addEventListener('click', () => {
|
||||
tbody.querySelectorAll('.mini-row').forEach(r => {
|
||||
const rIsWarning = (r as HTMLElement).style.borderLeftColor === 'rgb(225, 29, 72)'; // E11D48
|
||||
(r as HTMLElement).style.backgroundColor = rIsWarning ? '#FFF1F2' : 'transparent';
|
||||
});
|
||||
(row as HTMLElement).style.backgroundColor = '#EBF2F1'; // 선택 하이라이트
|
||||
const id = (row as HTMLElement).getAttribute('data-id');
|
||||
const asset = fullList.find(a => a.id === id);
|
||||
tbody.querySelectorAll('.mini-row').forEach(r => r.classList.remove('active'));
|
||||
row.classList.add('active');
|
||||
const asset = fullList.find(a => a.id === (row as HTMLElement).getAttribute('data-id'));
|
||||
if (asset) updateDetailPanel(asset);
|
||||
});
|
||||
row.addEventListener('mouseenter', () => {
|
||||
if ((row as HTMLElement).style.backgroundColor !== 'rgb(235, 242, 241)') {
|
||||
(row as HTMLElement).style.backgroundColor = '#F8FAFA';
|
||||
}
|
||||
});
|
||||
row.addEventListener('mouseleave', () => {
|
||||
const isWarning = (row as HTMLElement).style.borderLeftColor === 'rgb(225, 29, 72)';
|
||||
if ((row as HTMLElement).style.backgroundColor !== 'rgb(235, 242, 241)') {
|
||||
(row as HTMLElement).style.backgroundColor = isWarning ? '#FFF1F2' : 'transparent';
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
(window as any).dispatchLocFilter = (loc: string) => {
|
||||
if (isPcView) return;
|
||||
selectedLocation = loc;
|
||||
selectedDetailLocation = null;
|
||||
renderSystemStatus();
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
const selectLoc = document.getElementById('select-loc') as HTMLSelectElement;
|
||||
const selectDetailLoc = document.getElementById('select-detail-loc') as HTMLSelectElement;
|
||||
|
||||
selectLoc?.addEventListener('change', (e) => {
|
||||
selectedLocation = (e.target as HTMLSelectElement).value || null;
|
||||
selectedDetailLocation = null;
|
||||
updateTableOnly();
|
||||
});
|
||||
selectDetailLoc?.addEventListener('change', (e) => {
|
||||
selectedDetailLocation = (e.target as HTMLSelectElement).value || null;
|
||||
updateTableOnly();
|
||||
});
|
||||
document.getElementById('select-loc')?.addEventListener('change', (e) => { selectedLocation = (e.target as HTMLSelectElement).value || null; updateTableOnly(); });
|
||||
document.getElementById('select-detail-loc')?.addEventListener('change', (e) => { selectedDetailLocation = (e.target as HTMLSelectElement).value || null; updateTableOnly(); });
|
||||
updateTableOnly();
|
||||
}, 50);
|
||||
};
|
||||
|
||||
// [자산 목록] 테이블 렌더러
|
||||
const tableWrapper = document.createElement('div');
|
||||
tableWrapper.className = 'table-container';
|
||||
const table = document.createElement('table');
|
||||
const thead = document.createElement('thead');
|
||||
const tbody = document.createElement('tbody');
|
||||
tbody.id = 'dynamic-tbody';
|
||||
table.appendChild(thead);
|
||||
table.appendChild(tbody);
|
||||
table.appendChild(thead); table.appendChild(tbody);
|
||||
tableWrapper.appendChild(table);
|
||||
|
||||
const updateTable = () => {
|
||||
if ((state as any).currentViewMode !== 'asset') return;
|
||||
let filtered = applyCommonFilters(fullList, currentFilters, config.searchKeys as any[]);
|
||||
if (sortState.key) filtered = dynamicSort(filtered, sortState.key, sortState.direction);
|
||||
|
||||
thead.innerHTML = `<tr>${config.columns.map(col => `
|
||||
<th ${col.sortKey ? `data-sort="${col.sortKey}"` : ''}
|
||||
style="${col.width ? `width:${col.width};` : ''}${col.align ? `text-align:${col.align};` : ''}"
|
||||
class="${col.className || ''}">${col.header}</th>`).join('')}</tr>`;
|
||||
thead.innerHTML = `<tr>${config.columns.map(col => `<th ${col.sortKey ? `data-sort="${col.sortKey}"` : ''} style="${col.width ? `width:${col.width};` : ''}" class="${col.align ? `text-${col.align}` : ''}">${col.header}</th>`).join('')}</tr>`;
|
||||
tbody.innerHTML = filtered.length === 0 ? `<tr><td colspan="${config.columns.length}" class="text-center empty-cell">${UI_TEXT.MESSAGES.NO_DATA}</td></tr>`
|
||||
: filtered.map(asset => `<tr class="asset-row clickable" data-id="${asset.id}">${config.columns.map(col => `<td class="${col.align ? `text-${col.align}` : ''}">${col.render(asset)}</td>`).join('')}</tr>`).join('');
|
||||
|
||||
tbody.innerHTML = filtered.length === 0
|
||||
? `<tr><td colspan="${config.columns.length}" class="text-center" style="padding: 3rem; color: var(--text-muted);">${config.emptyMessage || UI_TEXT.MESSAGES.NO_DATA}</td></tr>`
|
||||
: filtered.map(asset => `
|
||||
<tr style="cursor:pointer;" class="asset-row" data-id="${asset.id}">
|
||||
${config.columns.map(col => `<td style="${col.align ? `text-align:${col.align};` : ''}" class="${col.className || ''}">${col.render(asset)}</td>`).join('')}
|
||||
</tr>`).join('');
|
||||
|
||||
tbody.querySelectorAll('.asset-row').forEach((tr, idx) => {
|
||||
tr.addEventListener('click', () => config.onRowClick && config.onRowClick(filtered[idx]));
|
||||
});
|
||||
|
||||
setupTableSorting(table, sortState, (key, dir) => {
|
||||
sortState = { key, direction: dir };
|
||||
if (config.persistentSortState) {
|
||||
config.persistentSortState.key = key;
|
||||
config.persistentSortState.direction = dir;
|
||||
}
|
||||
updateTable();
|
||||
});
|
||||
|
||||
// createIcons call removed as icons are no longer needed in dashboard
|
||||
tbody.querySelectorAll('.asset-row').forEach((tr, idx) => { tr.addEventListener('click', () => config.onRowClick && config.onRowClick(filtered[idx])); });
|
||||
setupTableSorting(table, sortState, (key, dir) => { sortState = { key, direction: dir }; updateTable(); });
|
||||
};
|
||||
|
||||
// --- 뷰 전환 로직 ---
|
||||
const switchView = () => {
|
||||
contentWrapper.innerHTML = '';
|
||||
if ((state as any).currentViewMode === 'asset') {
|
||||
filterBar.style.display = 'flex';
|
||||
contentWrapper.style.overflowY = 'auto';
|
||||
contentWrapper.appendChild(tableWrapper);
|
||||
updateTable();
|
||||
filterBar.style.display = 'flex'; contentWrapper.style.overflowY = 'auto';
|
||||
contentWrapper.appendChild(tableWrapper); updateTable();
|
||||
} else {
|
||||
filterBar.style.display = 'none';
|
||||
contentWrapper.style.overflowY = 'hidden';
|
||||
filterBar.style.display = 'none'; contentWrapper.style.overflowY = 'hidden';
|
||||
renderSystemStatus();
|
||||
}
|
||||
};
|
||||
|
||||
// 토글 버튼 이벤트
|
||||
toggleWrapper.addEventListener('click', (e) => {
|
||||
const btn = (e.target as HTMLElement).closest('.toggle-btn') as HTMLButtonElement;
|
||||
if (!btn) return;
|
||||
toggleWrapper.querySelectorAll('.toggle-btn').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
(state as any).currentViewMode = btn.getAttribute('data-mode') as 'asset' | 'system';
|
||||
switchView();
|
||||
});
|
||||
|
||||
// 필터 바 초기화
|
||||
renderFilterBar(filterBar, {
|
||||
...config.filterOptions,
|
||||
onFilterChange: (filters) => {
|
||||
currentFilters = { ...currentFilters, ...filters };
|
||||
updateTable();
|
||||
if (btn) {
|
||||
toggleWrapper.querySelectorAll('.toggle-btn').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active'); (state as any).currentViewMode = btn.getAttribute('data-mode');
|
||||
switchView();
|
||||
}
|
||||
});
|
||||
|
||||
// 셀렉트 박스 채우기
|
||||
const populateSelect = (selector: string, dataKey: string) => {
|
||||
const select = container.querySelector(selector) as HTMLSelectElement;
|
||||
if (select) {
|
||||
const getVal = (a: any) => dataKey === ASSET_SCHEMA.CURRENT_DEPT.key ? (a[dataKey] || a['현사용부서'] || a['현사용조직']) : a[dataKey];
|
||||
const uniqueValues = Array.from(new Set(fullList.map(getVal))).filter(Boolean).sort();
|
||||
uniqueValues.forEach(val => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = String(val);
|
||||
opt.textContent = String(val);
|
||||
select.appendChild(opt);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (config.filterOptions.showLoc) populateSelect('#filter-loc', ASSET_SCHEMA.LOCATION.key);
|
||||
if (config.filterOptions.showDept) populateSelect('#filter-dept', ASSET_SCHEMA.CURRENT_DEPT.key);
|
||||
if (config.filterOptions.showCorp) populateSelect('#filter-corp', ASSET_SCHEMA.PURCHASE_CORP.key);
|
||||
if (config.filterOptions.showType) populateSelect('#filter-type', ASSET_SCHEMA.ASSET_TYPE.key);
|
||||
|
||||
// 초기 실행
|
||||
renderFilterBar(filterBar, { ...config.filterOptions, onFilterChange: (filters) => { currentFilters = { ...currentFilters, ...filters }; updateTable(); } });
|
||||
switchView();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user