style: unify UI styling & restore dashboard logic

- Restored HW/SW Dashboard full features (Chart.js, filters, tables) from main
- Unified Search Bar & Filter Bar across all views (List, Location)
- Integrated asset identity info into all Modal Headers
- Standardized 'Remove Row' buttons as high-visibility circular circles
- Centralized hardcoded inline styles into dedicated CSS files
- Fixed various ReferenceErrors and layout regressions in HWModal
This commit is contained in:
2026-06-17 12:29:26 +09:00
parent b37981506e
commit 89d3ac2e89
16 changed files with 1440 additions and 596 deletions

View File

@@ -16,7 +16,10 @@ class DomainAssetModal extends BaseModal {
<div id="domain-asset-modal" class="modal-overlay hidden"> <div id="domain-asset-modal" class="modal-overlay hidden">
<div class="modal-content wide"> <div class="modal-content wide">
<div class="modal-header"> <div class="modal-header">
<h2 id="domain-modal-title" class="modal-title">${this.title}</h2> <div class="header-left">
<h2 id="domain-modal-title" class="modal-title">${this.title}</h2>
<div id="domain-header-identity" class="header-identity"></div>
</div>
<button id="btn-close-domain-modal" class="btn-icon" aria-label="닫기">&times;</button> <button id="btn-close-domain-modal" class="btn-icon" aria-label="닫기">&times;</button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
@@ -158,6 +161,7 @@ class DomainAssetModal extends BaseModal {
setFieldValue('domain-remarks', asset.remarks || ''); setFieldValue('domain-remarks', asset.remarks || '');
this.renderHistory(asset.id); this.renderHistory(asset.id);
this.updateHeaderIdentity(asset);
} }
protected onAfterOpen(asset: any, mode: string): void { protected onAfterOpen(asset: any, mode: string): void {
@@ -166,6 +170,28 @@ class DomainAssetModal extends BaseModal {
const deleteBtn = document.getElementById('btn-delete-domain-asset'); const deleteBtn = document.getElementById('btn-delete-domain-asset');
if (deleteBtn) deleteBtn.style.display = (mode === 'add') ? 'none' : 'block'; if (deleteBtn) deleteBtn.style.display = (mode === 'add') ? 'none' : 'block';
this.updateHeaderIdentity(asset);
}
private updateHeaderIdentity(asset: any) {
const container = document.getElementById('domain-header-identity');
if (!container) return;
if (this.currentMode === 'add') {
container.innerHTML = '<span class="badge badge-primary">신규 등록</span>';
return;
}
const type = getFieldValue('domain-type') || asset.type || '';
const serviceName = getFieldValue('domain-service-name') || asset.service_name || '';
const domainName = getFieldValue('domain-name') || asset.domain_name || '';
container.innerHTML = `
<span class="asset-code-title">${serviceName}</span>
<span class="service-type-badge">${type}</span>
<span class="asset-type-label">${domainName}</span>
`;
} }
private renderHistory(assetId: string) { private renderHistory(assetId: string) {

View File

@@ -30,9 +30,9 @@ class HwAssetModal extends BaseModal {
<div id="hw-asset-modal" class="modal-overlay hidden"> <div id="hw-asset-modal" class="modal-overlay hidden">
<div class="modal-content wide"> <div class="modal-content wide">
<div class="modal-header"> <div class="modal-header">
<div class="header-left" style="display: flex; align-items: center; gap: 0.75rem;"> <div class="header-left">
<h2 id="hw-modal-title" class="modal-title">${this.title}</h2> <h2 id="hw-modal-title" class="modal-title">${this.title}</h2>
<div class="category-badge-wrapper" id="hw-category-badge-container"></div> <div id="hw-header-identity" class="header-identity"></div>
</div> </div>
<button id="btn-close-hw-modal" class="btn-icon" aria-label="닫기">&times;</button> <button id="btn-close-hw-modal" class="btn-icon" aria-label="닫기">&times;</button>
</div> </div>
@@ -134,17 +134,17 @@ class HwAssetModal extends BaseModal {
<label>${ASSET_SCHEMA.OS.ui}</label> <label>${ASSET_SCHEMA.OS.ui}</label>
<input type="text" id="hw-os" name="os" /> <input type="text" id="hw-os" name="os" />
</div> </div>
<div class="form-group spec-only" style="position: relative;"> <div class="form-group spec-only relative">
<label>${ASSET_SCHEMA.CPU.ui}</label> <label>${ASSET_SCHEMA.CPU.ui}</label>
<input type="text" id="hw-cpu" name="cpu" autocomplete="off" /> <input type="text" id="hw-cpu" name="cpu" autocomplete="off" />
<div id="hw-cpu-list" class="autocomplete-list hidden"></div> <div id="hw-cpu-list" class="autocomplete-list hidden"></div>
</div> </div>
<div class="form-group spec-only" style="position: relative;"> <div class="form-group spec-only relative">
<label>${ASSET_SCHEMA.RAM.ui}</label> <label>${ASSET_SCHEMA.RAM.ui}</label>
<input type="text" id="hw-ram" name="ram" autocomplete="off" /> <input type="text" id="hw-ram" name="ram" autocomplete="off" />
<div id="hw-ram-list" class="autocomplete-list hidden"></div> <div id="hw-ram-list" class="autocomplete-list hidden"></div>
</div> </div>
<div class="form-group spec-only" style="position: relative;"> <div class="form-group spec-only relative">
<label>${ASSET_SCHEMA.GPU.ui}</label> <label>${ASSET_SCHEMA.GPU.ui}</label>
<input type="text" id="hw-gpu" name="gpu" autocomplete="off" /> <input type="text" id="hw-gpu" name="gpu" autocomplete="off" />
<div id="hw-gpu-list" class="autocomplete-list hidden"></div> <div id="hw-gpu-list" class="autocomplete-list hidden"></div>
@@ -165,16 +165,16 @@ class HwAssetModal extends BaseModal {
<!-- 동적 볼륨 정보 --> <!-- 동적 볼륨 정보 -->
<div class="form-group full-width spec-only"> <div class="form-group full-width spec-only">
<label>디스크 구성 (Volume)</label> <label>디스크 구성 (Volume)</label>
<div id="hw-volume-container" style="display: flex; flex-direction: column; gap: 8px;"></div> <div id="hw-volume-container" class="dynamic-row-container"></div>
<button type="button" id="btn-add-volume" class="btn btn-outline btn-sm" style="margin-top: 8px;">+ 볼륨 추가</button> <button type="button" id="btn-add-volume" class="btn btn-outline btn-sm">+ 볼륨 추가</button>
</div> </div>
<!-- [SECTION 4] 네트워크 및 원격 정보 --> <!-- [SECTION 4] 네트워크 및 원격 정보 -->
<div class="form-section-title net-only">네트워크 및 원격 관리</div> <div class="form-section-title net-only">네트워크 및 원격 관리</div>
<div class="form-group full-width net-only"> <div class="form-group full-width net-only">
<label>접속 정보 (IP / MAC / Remote)</label> <label>접속 정보 (IP / MAC / Remote)</label>
<div id="hw-remote-info-container" style="display: flex; flex-direction: column; gap: 12px;"></div> <div id="hw-remote-info-container" class="dynamic-row-container"></div>
<button type="button" id="btn-add-remote-info" class="btn btn-outline btn-sm" style="margin-top: 8px;">+ 접속 정보 추가</button> <button type="button" id="btn-add-remote-info" class="btn btn-outline btn-sm">+ 접속 정보 추가</button>
</div> </div>
<!-- [SECTION 5] 위치 정보 --> <!-- [SECTION 5] 위치 정보 -->
@@ -186,7 +186,7 @@ class HwAssetModal extends BaseModal {
<div class="form-group location-field"> <div class="form-group location-field">
<label>상세 위치</label> <label>상세 위치</label>
<div class="input-with-btn"> <div class="input-with-btn">
<select id="hw-location_detail" name="location_detail" style="flex: 1;"> <select id="hw-location_detail" name="location_detail">
<option value="">층을 먼저 선택하세요</option> <option value="">층을 먼저 선택하세요</option>
</select> </select>
<button type="button" id="btn-reg-loc-map" class="btn btn-outline hidden">위치 등록</button> <button type="button" id="btn-reg-loc-map" class="btn btn-outline hidden">위치 등록</button>
@@ -216,9 +216,9 @@ class HwAssetModal extends BaseModal {
<label>${ASSET_SCHEMA.APPROVAL_DOC.ui}</label> <label>${ASSET_SCHEMA.APPROVAL_DOC.ui}</label>
<div class="input-with-btn"> <div class="input-with-btn">
<input type="hidden" id="hw-approval_document" name="approval_document" /> <input type="hidden" id="hw-approval_document" name="approval_document" />
<div id="hw-file-name-display" class="is-readonly-field" style="flex: 1; border: 1px solid var(--hairline); border-radius: 6px; padding: 0 12px; height: clamp(34px, 4.5vmin, 44px); display: flex; align-items: center; font-size: var(--fs-sm); color: var(--mute);">파일 선택...</div> <div id="hw-file-name-display" class="file-upload-display">파일 선택...</div>
<div id="hw-file-link-container"></div> <div id="hw-file-link-container"></div>
<input type="file" id="hw-approval_document_file" style="display: none;" /> <input type="file" id="hw-approval_document_file" class="hidden" />
<button type="button" id="btn-file-select" class="btn btn-outline">파일 찾기</button> <button type="button" id="btn-file-select" class="btn btn-outline">파일 찾기</button>
</div> </div>
</div> </div>
@@ -247,34 +247,6 @@ class HwAssetModal extends BaseModal {
</div> </div>
</div> </div>
<style> <style>
.autocomplete-list {
position: absolute;
top: 100%;
left: 0;
right: 0;
max-height: 150px;
overflow-y: auto;
background-color: white;
border: 1px solid var(--border-color, #E2E8F0);
border-top: none;
border-radius: 0 0 4px 4px;
z-index: 1000;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
.autocomplete-item {
padding: 8px 12px;
font-size: 13px;
color: #334155;
cursor: pointer;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.autocomplete-item:hover {
background-color: #F1F5F9;
color: #1E5149;
font-weight: 600;
}
.hidden { .hidden {
display: none !important; display: none !important;
} }
@@ -303,13 +275,16 @@ class HwAssetModal extends BaseModal {
typeSelect.innerHTML = types.length > 0 ? generateOptionsHTML(types, '', true) : '<option value="">구분을 먼저 선택하세요</option>'; typeSelect.innerHTML = types.length > 0 ? generateOptionsHTML(types, '', true) : '<option value="">구분을 먼저 선택하세요</option>';
this.applyRoleVisibility(); this.applyRoleVisibility();
const badgeContainer = document.getElementById('hw-category-badge-container'); const identityContainer = document.getElementById('hw-header-identity');
if (badgeContainer) { if (identityContainer) {
badgeContainer.innerHTML = cat ? `<span class="badge badge-primary">${cat}</span>` : ''; identityContainer.innerHTML = cat ? `<span class="service-type-badge">${cat}</span>` : '';
} }
}); });
typeSelect.addEventListener('change', () => this.applyRoleVisibility()); typeSelect.addEventListener('change', () => {
this.applyRoleVisibility();
this.updateHeaderIdentity(this.currentAsset);
});
bindLocationEvents('hw-bldg-select', 'hw-location_detail', '', ''); bindLocationEvents('hw-bldg-select', 'hw-location_detail', '', '');
bldgSelect.addEventListener('change', () => setTimeout(() => this.updateMapButtonVisibility(), 100)); bldgSelect.addEventListener('change', () => setTimeout(() => this.updateMapButtonVisibility(), 100));
@@ -439,22 +414,21 @@ class HwAssetModal extends BaseModal {
const container = document.getElementById('hw-volume-container'); const container = document.getElementById('hw-volume-container');
if (!container) return; if (!container) return;
const row = document.createElement('div'); const row = document.createElement('div');
row.className = 'volume-row flex items-center gap-2'; row.className = 'volume-row items-center';
const inputStyle = 'height: clamp(34px, 4.5vmin, 44px) !important; box-sizing: border-box !important; font-size: var(--fs-sm); margin: 0; padding: 0 8px;';
row.innerHTML = ` row.innerHTML = `
<select class="vol-type" style="${inputStyle} width: 80px;" ${!this.isEditMode ? 'disabled' : ''}> <select class="vol-type" style="width: 80px; flex-shrink: 0;" ${!this.isEditMode ? 'disabled' : ''}>
<option value="SSD" ${vol.type === 'SSD' ? 'selected' : ''}>SSD</option> <option value="SSD" ${vol.type === 'SSD' ? 'selected' : ''}>SSD</option>
<option value="HDD" ${vol.type === 'HDD' ? 'selected' : ''}>HDD</option> <option value="HDD" ${vol.type === 'HDD' ? 'selected' : ''}>HDD</option>
<option value="NVMe" ${vol.type === 'NVMe' ? 'selected' : ''}>NVMe</option> <option value="NVMe" ${vol.type === 'NVMe' ? 'selected' : ''}>NVMe</option>
</select> </select>
<input type="number" class="vol-cap" value="${vol.capacity || ''}" placeholder="용량" style="${inputStyle} flex: 1;" ${!this.isEditMode ? 'readonly' : ''} /> <input type="number" class="vol-cap" value="${vol.capacity || ''}" placeholder="용량" style="flex: 1; min-width: 0;" ${!this.isEditMode ? 'readonly' : ''} />
<select class="vol-unit" style="${inputStyle} width: 70px;" ${!this.isEditMode ? 'disabled' : ''}> <select class="vol-unit" style="width: 70px; flex-shrink: 0;" ${!this.isEditMode ? 'disabled' : ''}>
<option value="GB" ${vol.unit === 'GB' ? 'selected' : ''}>GB</option> <option value="GB" ${vol.unit === 'GB' ? 'selected' : ''}>GB</option>
<option value="TB" ${vol.unit === 'TB' ? 'selected' : ''}>TB</option> <option value="TB" ${vol.unit === 'TB' ? 'selected' : ''}>TB</option>
</select> </select>
<button type="button" class="btn btn-outline btn-remove-row edit-only-btn" style="height: clamp(34px, 4.5vmin, 44px) !important; padding: 0 12px; color: var(--danger); border-color: var(--danger); display: ${this.isEditMode ? 'inline-flex' : 'none'};">&times;</button> <button type="button" class="btn-circle-remove edit-only-btn" style="display: ${this.isEditMode ? 'inline-flex' : 'none'};">&times;</button>
`; `;
row.querySelector('.btn-remove-row')?.addEventListener('click', () => row.remove()); row.querySelector('.btn-circle-remove')?.addEventListener('click', () => row.remove());
container.appendChild(row); container.appendChild(row);
} }
@@ -475,34 +449,32 @@ class HwAssetModal extends BaseModal {
} }
const row = document.createElement('div'); const row = document.createElement('div');
row.className = 'remote-info-row flex-col gap-1 w-full'; row.className = 'remote-info-row w-full';
const baseStyle = 'height: clamp(34px, 4.5vmin, 44px) !important; box-sizing: border-box !important; margin: 0;';
const compactStyle = `${baseStyle} font-size: var(--fs-xs); padding: 0 6px;`;
const line1 = document.createElement('div'); const line1 = document.createElement('div');
line1.className = 'ri-line flex items-center gap-1.5'; line1.className = 'ri-line items-center';
line1.innerHTML = ` line1.innerHTML = `
<select class="ri-type" ${!this.isEditMode ? 'disabled' : ''} style="${compactStyle} width: 75px; flex-shrink: 0;"> <select class="ri-type" ${!this.isEditMode ? 'disabled' : ''} style="width: 75px; flex-shrink: 0; font-size: var(--fs-xs); padding: 0 6px;">
<option value="IP" ${info.type === 'IP' ? 'selected' : ''}>IP 주소</option> <option value="IP" ${info.type === 'IP' ? 'selected' : ''}>IP 주소</option>
<option value="MAC" ${info.type === 'MAC' ? 'selected' : ''}>MAC 주소</option> <option value="MAC" ${info.type === 'MAC' ? 'selected' : ''}>MAC 주소</option>
</select> </select>
<input type="text" class="ri-val1" value="${info.val1 || ''}" placeholder="주소 입력" ${!this.isEditMode ? 'readonly' : ''} style="${compactStyle} flex: 1; min-width: 0;" /> <input type="text" class="ri-val1" value="${info.val1 || ''}" placeholder="주소 입력" ${!this.isEditMode ? 'readonly' : ''} style="flex: 1; min-width: 0; font-size: var(--fs-xs); padding: 0 6px;" />
<button type="button" class="btn-outline btn-remove-row ri-remove-btn edit-only-btn" style="height: clamp(34px, 4.5vmin, 44px) !important; padding: 0 10px; color: var(--danger); border-color: var(--danger); flex-shrink: 0; display: ${this.isEditMode ? 'inline-flex' : 'none'};">&times;</button> <button type="button" class="btn-circle-remove edit-only-btn" style="display: ${this.isEditMode ? 'inline-flex' : 'none'};">&times;</button>
`; `;
const line2 = document.createElement('div'); const line2 = document.createElement('div');
line2.className = 'ri-line ri-cred-line flex items-center gap-1.5'; line2.className = 'ri-line ri-cred-line items-center';
if (info.type !== 'IP') line2.classList.add('hidden'); if (info.type !== 'IP') line2.classList.add('hidden');
line2.innerHTML = ` line2.innerHTML = `
<div class="ri-connector" style="width: 16px; border-left: 1px solid var(--hairline); border-bottom: 1px solid var(--hairline); height: 16px; margin-left: 10px; margin-top: -12px; flex-shrink: 0;"></div> <div class="ri-connector" style="width: 16px; height: 16px; margin-top: -12px;"></div>
<select class="ri-tool" ${!this.isEditMode ? 'disabled' : ''} style="${compactStyle} width: 85px; flex-shrink: 0;"> <select class="ri-tool" ${!this.isEditMode ? 'disabled' : ''} style="width: 85px; flex-shrink: 0; font-size: var(--fs-xs); padding: 0 6px;">
<option value="원격접속" ${info.name === '원격접속' ? 'selected' : ''}>원격접속</option> <option value="원격접속" ${info.name === '원격접속' ? 'selected' : ''}>원격접속</option>
<option value="리눅스" ${info.name === '리눅스' ? 'selected' : ''}>리눅스</option> <option value="리눅스" ${info.name === '리눅스' ? 'selected' : ''}>리눅스</option>
<option value="기타" ${info.name === '기타' ? 'selected' : ''}>기타</option> <option value="기타" ${info.name === '기타' ? 'selected' : ''}>기타</option>
</select> </select>
<input type="text" class="ri-id" value="${parsedId}" placeholder="원격 ID" ${!this.isEditMode ? 'readonly' : ''} style="${compactStyle} flex: 1; min-width: 0;" /> <input type="text" class="ri-id" value="${parsedId}" placeholder="원격 ID" ${!this.isEditMode ? 'readonly' : ''} style="flex: 1; min-width: 0; font-size: var(--fs-xs); padding: 0 6px;" />
<input type="text" class="ri-pw" value="${parsedPw}" placeholder="원격 PW" ${!this.isEditMode ? 'readonly' : ''} style="${compactStyle} flex: 1; min-width: 0;" /> <input type="text" class="ri-pw" value="${parsedPw}" placeholder="원격 PW" ${!this.isEditMode ? 'readonly' : ''} style="flex: 1; min-width: 0; font-size: var(--fs-xs); padding: 0 6px;" />
`; `;
row.appendChild(line1); row.appendChild(line1);
@@ -518,7 +490,7 @@ class HwAssetModal extends BaseModal {
} }
}); });
row.querySelector('.btn-remove-row')?.addEventListener('click', () => row.remove()); row.querySelector('.btn-circle-remove')?.addEventListener('click', () => row.remove());
container.appendChild(row); container.appendChild(row);
} }
@@ -633,11 +605,7 @@ class HwAssetModal extends BaseModal {
this.renderHistory(asset.id); this.renderHistory(asset.id);
this.applyRoleVisibility(); this.applyRoleVisibility();
this.updatePcGradeBadge(); this.updatePcGradeBadge();
this.updateHeaderIdentity(asset);
const badgeContainer = document.getElementById('hw-category-badge-container');
if (badgeContainer) {
badgeContainer.innerHTML = asset.category ? `<span class="badge badge-primary">${asset.category}</span>` : '';
}
} }
protected onAfterOpen(asset: any, mode: string): void { protected onAfterOpen(asset: any, mode: string): void {
@@ -647,6 +615,29 @@ class HwAssetModal extends BaseModal {
this.toggleEditOnlyBtns(mode !== 'view'); this.toggleEditOnlyBtns(mode !== 'view');
this.updateMapButtonVisibility(); this.updateMapButtonVisibility();
this.applyRoleVisibility(); this.applyRoleVisibility();
this.updateHeaderIdentity(asset);
}
private updateHeaderIdentity(asset: any) {
const container = document.getElementById('hw-header-identity');
if (!container) return;
if (this.currentMode === 'add') {
container.innerHTML = '<span class="badge badge-primary">신규 등록</span>';
return;
}
const category = (document.getElementById('hw-category') as HTMLSelectElement)?.value || asset.category || '';
const type = (document.getElementById('hw-asset_type') as HTMLSelectElement)?.value || asset.asset_type || '';
const code = (document.getElementById('hw-asset_code') as HTMLInputElement)?.value || asset.asset_code || '미부여';
const serviceType = (document.getElementById('hw-service_type') as HTMLSelectElement)?.value || asset.service_type || '외부';
container.innerHTML = `
<span class="asset-code-title">${code}</span>
<span class="service-type-badge">${serviceType}</span>
<span class="service-type-badge" style="background: var(--canvas-soft-2); color: var(--primary); border: 1px solid var(--hairline);">${category}</span>
<span class="asset-type-label">${type}</span>
`;
} }
private toggleFileUploadUI(showUpload: boolean) { private toggleFileUploadUI(showUpload: boolean) {
@@ -723,40 +714,115 @@ class HwAssetModal extends BaseModal {
overlay.className = 'image-picker-overlay'; overlay.className = 'image-picker-overlay';
const renderContent = () => { const renderContent = () => {
const imgPath = imagePaths[currentIdx]; const imgPath = imagePaths[currentIdx];
const digitalMap = this.generateDynamicSVG(imgPath); const isMulti = imagePaths.length > 1;
const isHtmlMap = imgPath.toLowerCase().endsWith('.html');
const digitalMap = isHtmlMap ? '' : this.generateDynamicSVG(imgPath);
overlay.innerHTML = ` overlay.innerHTML = `
<div class="image-picker-window"> <div class="image-picker-window">
<div class="image-picker-header"><h3>${title}</h3><button class="btn-icon btn-close-picker">&times;</button></div> <div class="image-picker-header">
<h3>${title} ${isMulti ? `(${currentIdx + 1}/${imagePaths.length})` : ''}</h3>
<button class="btn-icon btn-close-picker" aria-label="닫기">&times;</button>
</div>
<div class="image-picker-content"> <div class="image-picker-content">
${isMulti ? `
<div class="picker-nav prev ${currentIdx === 0 ? 'disabled' : ''}" style="position: absolute; left: 10px; top: 50%; transform: translateY(-50%); z-index: 100; cursor: pointer; background: rgba(0,0,0,0.5); color: white; padding: 20px 10px; border-radius: 5px; font-size: 24px; user-select: none;">&#10094;</div>
<div class="picker-nav next ${currentIdx === imagePaths.length - 1 ? 'disabled' : ''}" style="position: absolute; right: 10px; top: 50%; transform: translateY(-50%); z-index: 100; cursor: pointer; background: rgba(0,0,0,0.5); color: white; padding: 20px 10px; border-radius: 5px; font-size: 24px; user-select: none;">&#10095;</div>
` : ''}
<div class="layout-map-container" id="picker-container"> <div class="layout-map-container" id="picker-container">
<div class="image-marker-wrapper" style="position: relative; display: inline-block;"> ${isHtmlMap
<img src="${imgPath}" class="layout-map-img" style="display: block; max-width: 100%; max-height: 70vh;" /> ? `<iframe src="${imgPath}" style="width:100%; height:100%; border:none; display:block;"></iframe>`
<div id="picker-marker" class="layout-marker hidden"></div> : `<div class="image-marker-wrapper">
<div class="digital-overlay-layer">${digitalMap}</div> <img src="${imgPath}" class="layout-map-img" />
</div> <div id="picker-marker" class="layout-marker hidden"></div>
<div class="digital-overlay-layer">${digitalMap}</div>
</div>`
}
</div> </div>
</div> </div>
<div class="image-picker-footer"><button id="btn-picker-cancel" class="btn btn-outline">취소</button><button id="btn-picker-save" class="btn btn-primary">위치 확정</button></div> <div class="image-picker-footer"><button id="btn-picker-cancel" class="btn btn-outline">취소</button><button id="btn-picker-save" class="btn btn-primary">위치 확정</button></div>
</div>`; </div>`;
let selectedX = ''; let selectedY = ''; let selectedX = ''; let selectedY = '';
const container = overlay.querySelector('#picker-container') as HTMLElement;
const marker = overlay.querySelector('#picker-marker') as HTMLElement; if (isMulti) {
container.addEventListener('click', (e) => { overlay.querySelector('.picker-nav.prev')?.addEventListener('click', (e) => { e.stopPropagation(); if (currentIdx > 0) { currentIdx--; renderContent(); } });
const rect = container.getBoundingClientRect(); overlay.querySelector('.picker-nav.next')?.addEventListener('click', (e) => { e.stopPropagation(); if (currentIdx < imagePaths.length - 1) { currentIdx++; renderContent(); } });
const x = ((e.clientX - rect.left) / rect.width) * 100; }
const y = ((e.clientY - rect.top) / rect.height) * 100;
selectedX = x.toFixed(2); selectedY = y.toFixed(2); if (isHtmlMap) {
marker.style.left = `${selectedX}%`; marker.style.top = `${selectedY}%`; const handleMessage = (e: MessageEvent) => {
marker.classList.remove('hidden'); if (e.data.type === 'PICK_LOCATION') {
}); selectedX = e.data.x;
overlay.querySelector('.btn-close-picker')?.addEventListener('click', () => overlay.remove()); selectedY = e.data.y;
overlay.querySelector('#btn-picker-cancel')?.addEventListener('click', () => overlay.remove()); }
overlay.querySelector('#btn-picker-save')?.addEventListener('click', () => { };
if (!selectedX || !selectedY) { alert('위치를 선택해주세요.'); return; } window.addEventListener('message', handleMessage);
setFieldValue('hw-loc_x', selectedX); setFieldValue('hw-loc_y', selectedY); overlay.querySelector('.btn-close-picker')?.addEventListener('click', () => { window.removeEventListener('message', handleMessage); overlay.remove(); });
setFieldValue('hw-location_photo', imagePaths[currentIdx]); overlay.querySelector('#btn-picker-cancel')?.addEventListener('click', () => { window.removeEventListener('message', handleMessage); overlay.remove(); });
this.updateMapButtonVisibility(); overlay.remove(); overlay.querySelector('#btn-picker-save')?.addEventListener('click', () => {
}); if (!selectedX || !selectedY) { alert('위치를 선택해주세요.'); return; }
setFieldValue('hw-loc_x', selectedX); setFieldValue('hw-loc_y', selectedY);
setFieldValue('hw-location_photo', imagePaths[currentIdx]);
this.updateMapButtonVisibility();
window.removeEventListener('message', handleMessage);
overlay.remove();
});
} else {
const container = overlay.querySelector('#picker-container') as HTMLElement;
const marker = overlay.querySelector('#picker-marker') as HTMLElement;
container.addEventListener('click', (e) => {
const rectBound = container.getBoundingClientRect();
const clickX = ((e.clientX - rectBound.left) / rectBound.width) * 100;
const clickY = ((e.clientY - rectBound.top) / rectBound.height) * 100;
let snapped = false;
overlay.querySelectorAll('rect').forEach(rect => {
const rx = parseFloat(rect.getAttribute('x') || '0');
const ry = parseFloat(rect.getAttribute('y') || '0');
const rw = parseFloat(rect.getAttribute('width') || '0');
const rh = parseFloat(rect.getAttribute('height') || '0');
if (clickX >= rx && clickX <= rx + rw && clickY >= ry && clickY <= ry + rh) {
overlay.querySelectorAll('rect').forEach(r => {
r.style.fill = 'rgba(30,81,73,0.05)';
r.style.stroke = 'rgba(30,81,73,0.2)';
r.style.strokeWidth = '0.2';
});
rect.style.fill = 'rgba(255, 61, 0, 0.4)';
rect.style.stroke = '#FF3D00';
rect.style.strokeWidth = '0.8';
selectedX = rx.toFixed(2);
selectedY = ry.toFixed(2);
marker.style.left = `${rx + rw/2}%`;
marker.style.top = `${ry + rh/2}%`;
marker.classList.remove('hidden');
snapped = true;
}
});
if (!snapped) {
selectedX = '';
selectedY = '';
marker.classList.add('hidden');
overlay.querySelectorAll('rect').forEach(r => {
r.style.fill = 'rgba(30,81,73,0.05)';
r.style.stroke = 'rgba(30,81,73,0.2)';
r.style.strokeWidth = '0.2';
});
}
});
overlay.querySelector('.btn-close-picker')?.addEventListener('click', () => overlay.remove());
overlay.querySelector('#btn-picker-cancel')?.addEventListener('click', () => overlay.remove());
overlay.querySelector('#btn-picker-save')?.addEventListener('click', () => {
if (!selectedX || !selectedY) { alert('위치를 선택해주세요.'); return; }
setFieldValue('hw-loc_x', selectedX); setFieldValue('hw-loc_y', selectedY);
setFieldValue('hw-location_photo', imagePaths[currentIdx]);
this.updateMapButtonVisibility(); overlay.remove();
});
}
}; };
renderContent(); document.body.appendChild(overlay); renderContent(); document.body.appendChild(overlay);
} }
@@ -770,27 +836,34 @@ class HwAssetModal extends BaseModal {
overlay.innerHTML = ` overlay.innerHTML = `
<div class="image-picker-window"> <div class="image-picker-window">
<div class="image-picker-header"><h3>${title}</h3><button class="btn-icon btn-close-picker">&times;</button></div> <div class="image-picker-header"><h3>${title}</h3><button class="btn-icon btn-close-picker" aria-label="닫기">&times;</button></div>
<div class="image-picker-content"> <div class="image-picker-content">
<div class="layout-map-container readonly"> <div class="layout-map-container readonly">
<div class="image-marker-wrapper" style="position: relative; display: inline-block;"> ${isHtmlMap
<img src="${imagePath}" class="layout-map-img" style="display: block; max-width: 100%; max-height: 70vh;" /> ? `<iframe src="${finalPath}" style="width:100%; height:100%; border:none; display:block;"></iframe>`
<div class="digital-overlay-layer">${digitalMap}</div> : `<div class="image-marker-wrapper">
</div> <img src="${imagePath}" class="layout-map-img" />
<div id="preview-marker" class="layout-marker pulse-marker" style="left:${x}%; top:${y}%;"></div>
<div class="digital-overlay-layer">${digitalMap}</div>
</div>`
}
</div> </div>
</div> </div>
<div class="image-picker-footer"><button id="btn-preview-close" class="btn btn-primary">확인</button></div> <div class="image-picker-footer"><button id="btn-preview-close" class="btn btn-primary">확인</button></div>
</div>`; </div>`;
document.body.appendChild(overlay); document.body.appendChild(overlay);
if (!isHtmlMap && digitalMap) { if (!isHtmlMap && digitalMap) {
const curX = parseFloat(x || '0'); const curY = parseFloat(y || '0'); const curX = parseFloat(x || '0'); const curY = parseFloat(y || '0');
overlay.querySelectorAll('rect').forEach(rect => { overlay.querySelectorAll('rect').forEach(rect => {
const sx = parseFloat(rect.getAttribute('x') || '0'); const sx = parseFloat(rect.getAttribute('x') || '0');
const sy = parseFloat(rect.getAttribute('y') || '0'); const sy = parseFloat(rect.getAttribute('y') || '0');
if (Math.abs(sx - curX) < 0.1 && Math.abs(sy - curY) < 0.1) { if (Math.abs(sx - curX) < 0.01 && Math.abs(sy - curY) < 0.01) {
rect.style.fill = 'rgba(255, 61, 0, 0.5)'; rect.style.fill = 'rgba(255, 61, 0, 0.4)'; rect.style.stroke = '#FF3D00'; rect.style.strokeWidth = '0.8';
rect.style.stroke = '#FF3D00'; rect.style.strokeWidth = '1.2'; const w = parseFloat(rect.getAttribute('width') || '0');
rect.style.filter = 'drop-shadow(0 0 6px rgba(255, 61, 0, 0.8))'; const h = parseFloat(rect.getAttribute('height') || '0');
const marker = overlay.querySelector('#preview-marker') as HTMLElement;
if (marker) { marker.style.left = `${sx + w/2}%`; marker.style.top = `${sy + h/2}%`; }
} }
}); });
} }

View File

@@ -184,9 +184,9 @@ export function createModalFrameHTML(
</div> </div>
<div class="modal-history-area"> <div class="modal-history-area">
<div class="history-header"> <div class="history-header">
<h3><i data-lucide="history" style="width:16px; height:16px;"></i> ${options.historyTitle}</h3> <h3><i data-lucide="history" class="icon-sm"></i> ${options.historyTitle}</h3>
<button type="button" id="${options.addLogBtnId}" class="btn btn-outline btn-sm"> <button type="button" id="btn-add-${idPrefix}-log" class="btn btn-outline btn-sm">
내역 추가 <i data-lucide="plus" style="width:14px; height:14px;"></i> 내역 추가 <i data-lucide="plus" class="icon-sm"></i>
</button> </button>
</div> </div>
<div id="${idPrefix}-history-list" class="history-timeline"></div> <div id="${idPrefix}-history-list" class="history-timeline"></div>

View File

@@ -508,7 +508,7 @@ export class PCFlowModal {
</div> </div>
<!-- 3. 새 인수자 검색 (이동 시 노출) --> <!-- 3. 새 인수자 검색 (이동 시 노출) -->
<div id="target-user-search-container" class="form-group hidden" style="position: relative;"> <div id="target-user-search-container" class="form-group hidden relative">
<label>새 인수 사원 검색</label> <label>새 인수 사원 검색</label>
<div class="input-with-icon"> <div class="input-with-icon">
<input type="text" id="pc-flow-target-user-search" placeholder="사원명, 부서, 사번 검색..." /> <input type="text" id="pc-flow-target-user-search" placeholder="사원명, 부서, 사번 검색..." />
@@ -518,7 +518,7 @@ export class PCFlowModal {
</div> </div>
<!-- 4. 재고 PC 검색 (불출 시 노출) --> <!-- 4. 재고 PC 검색 (불출 시 노출) -->
<div id="stock-pc-search-container" class="form-group" style="position: relative;"> <div id="stock-pc-search-container" class="form-group relative">
<label>3. 불출할 재고 PC 선택</label> <label>3. 불출할 재고 PC 선택</label>
<div class="input-with-icon"> <div class="input-with-icon">
<input type="text" id="pc-flow-stock-search" placeholder="자산코드 또는 모델명 검색..." /> <input type="text" id="pc-flow-stock-search" placeholder="자산코드 또는 모델명 검색..." />
@@ -547,7 +547,7 @@ export class PCFlowModal {
<h3>선택 내역 요약</h3> <h3>선택 내역 요약</h3>
</div> </div>
<div style="display: flex; flex-direction: column; gap: 1rem;"> <div class="dynamic-row-container">
<!-- 사원 요약 카드 --> <!-- 사원 요약 카드 -->
<div id="summary-user-card" class="summary-info-card"> <div id="summary-user-card" class="summary-info-card">
<div class="detail-label-sm">대상 사원</div> <div class="detail-label-sm">대상 사원</div>
@@ -556,7 +556,7 @@ export class PCFlowModal {
</div> </div>
<!-- 인수 사원 요약 카드 (이동 전용) --> <!-- 인수 사원 요약 카드 (이동 전용) -->
<div id="summary-target-user-card" class="summary-info-card hidden" style="background: var(--primary-light);"> <div id="summary-target-user-card" class="summary-info-card hidden bg-primary-light">
<div class="detail-label-sm">새 인수 사원</div> <div class="detail-label-sm">새 인수 사원</div>
<div id="summary-target-user-name" class="detail-value-lg">선택된 사원 없음</div> <div id="summary-target-user-name" class="detail-value-lg">선택된 사원 없음</div>
<div id="summary-target-user-dept" class="detail-label-sm">-</div> <div id="summary-target-user-dept" class="detail-label-sm">-</div>
@@ -565,7 +565,7 @@ export class PCFlowModal {
<!-- 대상 PC 자산 요약 카드 --> <!-- 대상 PC 자산 요약 카드 -->
<div id="summary-pc-card" class="summary-info-card"> <div id="summary-pc-card" class="summary-info-card">
<div class="detail-label-sm">대상 PC 자산</div> <div class="detail-label-sm">대상 PC 자산</div>
<div id="summary-pc-code" class="detail-value-lg" style="color: var(--success);">선택된 PC 없음</div> <div id="summary-pc-code" class="detail-value-lg text-success">선택된 PC 없음</div>
<div id="summary-pc-model" class="detail-label-sm">-</div> <div id="summary-pc-model" class="detail-label-sm">-</div>
</div> </div>

View File

@@ -12,13 +12,16 @@ class PartsMasterModal extends BaseModal {
protected renderFrameHTML(): string { protected renderFrameHTML(): string {
return ` return `
<div id="parts-master-asset-modal" class="modal-overlay hidden"> <div id="parts-master-asset-modal" class="modal-overlay hidden">
<div class="modal-content" style="max-width: 500px;"> <div class="modal-content narrow">
<div class="modal-header"> <div class="modal-header">
<h2 id="parts-master-modal-title" class="modal-title">${this.title}</h2> <div class="header-left">
<h2 id="parts-master-modal-title" class="modal-title">${this.title}</h2>
<div id="parts-master-header-identity" class="header-identity"></div>
</div>
<button id="btn-close-parts-master-modal" class="btn-icon" aria-label="닫기">&times;</button> <button id="btn-close-parts-master-modal" class="btn-icon" aria-label="닫기">&times;</button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<form id="parts-master-asset-form" class="grid-form" style="grid-template-columns: 1fr;"> <form id="parts-master-asset-form" class="grid-form vertical-form">
<input type="hidden" id="parts-master-id" name="id" /> <input type="hidden" id="parts-master-id" name="id" />
<div class="form-group"> <div class="form-group">
@@ -120,6 +123,7 @@ class PartsMasterModal extends BaseModal {
setFieldValue('parts-master-component-name', asset.component_name || ''); setFieldValue('parts-master-component-name', asset.component_name || '');
setFieldValue('parts-master-score-tier', asset.score_tier || ''); setFieldValue('parts-master-score-tier', asset.score_tier || '');
setFieldValue('parts-master-deduction', asset.deduction !== undefined ? asset.deduction.toString() : '0'); setFieldValue('parts-master-deduction', asset.deduction !== undefined ? asset.deduction.toString() : '0');
this.updateHeaderIdentity(asset);
} }
protected onAfterOpen(asset: any, mode: string): void { protected onAfterOpen(asset: any, mode: string): void {
@@ -144,6 +148,25 @@ class PartsMasterModal extends BaseModal {
saveBtn.textContent = '수정'; saveBtn.textContent = '수정';
saveBtn.style.display = 'block'; saveBtn.style.display = 'block';
} }
this.updateHeaderIdentity(asset);
}
private updateHeaderIdentity(asset: any) {
const container = document.getElementById('parts-master-header-identity');
if (!container) return;
if (this.currentMode === 'add') {
container.innerHTML = '<span class="badge badge-primary">신규 등록</span>';
return;
}
const cat = asset.category || '';
const name = asset.component_name || '';
container.innerHTML = `
<span class="asset-code-title">${name}</span>
<span class="service-type-badge">${cat}</span>
`;
} }
} }

View File

@@ -22,7 +22,10 @@ class SwAssetModal extends BaseModal {
<div id="sw-asset-modal" class="modal-overlay hidden"> <div id="sw-asset-modal" class="modal-overlay hidden">
<div class="modal-content wide"> <div class="modal-content wide">
<div class="modal-header"> <div class="modal-header">
<h2 id="sw-modal-title" class="modal-title">${this.title}</h2> <div class="header-left">
<h2 id="sw-modal-title" class="modal-title">${this.title}</h2>
<div id="sw-header-identity" class="header-identity"></div>
</div>
<button id="btn-close-sw-modal" class="btn-icon" aria-label="닫기">&times;</button> <button id="btn-close-sw-modal" class="btn-icon" aria-label="닫기">&times;</button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
@@ -170,14 +173,14 @@ class SwAssetModal extends BaseModal {
</div> </div>
<!-- 계약 업데이트 서브 모달 --> <!-- 계약 업데이트 서브 모달 -->
<div id="sw-update-modal" class="modal-overlay hidden" style="z-index: 1100;"> <div id="sw-update-modal" class="modal-overlay hidden sub-modal">
<div class="modal-content" style="max-width: 500px;"> <div class="modal-content narrow">
<div class="modal-header"> <div class="modal-header">
<h2 class="modal-title">계약 업데이트 반영</h2> <h2 class="modal-title">계약 업데이트 반영</h2>
<button id="btn-close-sw-update" class="btn-icon">&times;</button> <button id="btn-close-sw-update" class="btn-icon">&times;</button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="grid-form" style="grid-template-columns: 1fr;"> <div class="grid-form vertical-form">
<div class="form-group"> <div class="form-group">
<label>업데이트 일자</label> <label>업데이트 일자</label>
<input type="date" id="sw-update-date" /> <input type="date" id="sw-update-date" />
@@ -266,7 +269,7 @@ class SwAssetModal extends BaseModal {
const log = { assetId: this.currentAsset.id, date, details: `[계약갱신] ${note} (${start} ~ ${end}, 비용: ${cost})`, user: '관리자' }; const log = { assetId: this.currentAsset.id, date, details: `[계약갱신] ${note} (${start} ~ ${end}, 비용: ${cost})`, user: '관리자' };
await fetch(`${API_BASE_URL}/api/asset/history/batch`, { await fetch(`${API_BASE_URL}/api/asset/history/batch`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: 'application/json',
body: JSON.stringify([...state.masterData.logs, log]) body: JSON.stringify([...state.masterData.logs, log])
}); });
@@ -330,10 +333,32 @@ class SwAssetModal extends BaseModal {
} }
this.renderHistory(asset.id); this.renderHistory(asset.id);
this.updateHeaderIdentity(asset);
} }
protected onAfterOpen(asset: any, mode: string): void { protected onAfterOpen(asset: any, mode: string): void {
this.applySwTypeUI(asset.asset_type || asset.type); this.applySwTypeUI(asset.asset_type || asset.type);
this.updateHeaderIdentity(asset);
}
private updateHeaderIdentity(asset: any) {
const container = document.getElementById('sw-header-identity');
if (!container) return;
if (this.currentMode === 'add') {
container.innerHTML = '<span class="badge badge-primary">신규 등록</span>';
return;
}
const type = getFieldValue('sw-asset-type') || asset.asset_type || asset.type || '';
const name = getFieldValue('sw-제품명') || asset.product_name || '';
const corp = getFieldValue('sw-법인') || asset.purchase_corp || '';
container.innerHTML = `
<span class="asset-code-title">${name}</span>
<span class="service-type-badge">${corp}</span>
<span class="asset-type-label">${type}</span>
`;
} }
private applySwTypeUI(type: string) { private applySwTypeUI(type: string) {

View File

@@ -22,9 +22,9 @@ class SwUserModal extends BaseModal {
<div class="modal-body"> <div class="modal-body">
<div class="sw-info-summary" id="sw-user-sw-info"></div> <div class="sw-info-summary" id="sw-user-sw-info"></div>
<div class="user-list-toolbar" style="display:flex; justify-content:space-between; margin-bottom:1rem; align-items:center;"> <div class="flex justify-between items-center mb-4">
<h3 class="detail-section-title">할당된 사용자 목록</h3> <h3 class="detail-section-title mb-0">할당된 사용자 목록</h3>
<button type="button" id="btn-open-add-user" class="btn btn-primary btn-sm"><i data-lucide="plus"></i> 사용자 추가</button> <button type="button" id="btn-open-add-user" class="btn btn-primary btn-sm"><i data-lucide="plus" class="icon-sm"></i> 사용자 추가</button>
</div> </div>
<div class="table-container"> <div class="table-container">
@@ -35,9 +35,9 @@ class SwUserModal extends BaseModal {
<th>부서</th> <th>부서</th>
<th>직위</th> <th>직위</th>
<th>이름</th> <th>이름</th>
<th>사용기간</th> <th class="text-center">사용기간</th>
<th>신청서</th> <th class="text-center">신청서</th>
<th>관리</th> <th class="text-center">관리</th>
</tr> </tr>
</thead> </thead>
<tbody id="sw-user-table-body"></tbody> <tbody id="sw-user-table-body"></tbody>
@@ -54,14 +54,14 @@ class SwUserModal extends BaseModal {
</div> </div>
<!-- 사용자 추가/수정 서브 모달 --> <!-- 사용자 추가/수정 서브 모달 -->
<div id="sw-user-edit-modal" class="modal-overlay hidden" style="z-index: 1100;"> <div id="sw-user-edit-modal" class="modal-overlay hidden sub-modal">
<div class="modal-content" style="max-width: 400px;"> <div class="modal-content narrow">
<div class="modal-header"> <div class="modal-header">
<h3 id="sw-user-edit-title" class="modal-title">사용자 정보</h3> <h3 id="sw-user-edit-title" class="modal-title">사용자 정보</h3>
<button id="btn-close-user-edit" class="btn-icon">&times;</button> <button id="btn-close-user-edit" class="btn-icon">&times;</button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<form id="sw-user-edit-form" class="grid-form" style="grid-template-columns: 1fr;"> <form id="sw-user-edit-form" class="grid-form vertical-form">
<input type="hidden" id="edit-user-index" value="-1" /> <input type="hidden" id="edit-user-index" value="-1" />
<div class="form-group"> <div class="form-group">
<label>조직</label> <label>조직</label>
@@ -84,7 +84,7 @@ class SwUserModal extends BaseModal {
<div class="input-with-btn"> <div class="input-with-btn">
<input type="text" id="new-user-시작일" /> <input type="text" id="new-user-시작일" />
<button type="button" class="btn-icon" onclick="const p = document.getElementById('new-user-시작일-picker'); p.value = document.getElementById('new-user-시작일').value; p.showPicker();"> <button type="button" class="btn-icon" onclick="const p = document.getElementById('new-user-시작일-picker'); p.value = document.getElementById('new-user-시작일').value; p.showPicker();">
<i data-lucide="calendar"></i> <i data-lucide="calendar" class="icon-sm"></i>
</button> </button>
<input type="date" id="new-user-시작일-picker" class="hidden-picker" onchange="document.getElementById('new-user-시작일').value = this.value" tabindex="-1" /> <input type="date" id="new-user-시작일-picker" class="hidden-picker" onchange="document.getElementById('new-user-시작일').value = this.value" tabindex="-1" />
</div> </div>
@@ -94,7 +94,7 @@ class SwUserModal extends BaseModal {
<div class="input-with-btn"> <div class="input-with-btn">
<input type="text" id="new-user-종료일" /> <input type="text" id="new-user-종료일" />
<button type="button" class="btn-icon" onclick="const p = document.getElementById('new-user-종료일-picker'); p.value = document.getElementById('new-user-종료일').value; p.showPicker();"> <button type="button" class="btn-icon" onclick="const p = document.getElementById('new-user-종료일-picker'); p.value = document.getElementById('new-user-종료일').value; p.showPicker();">
<i data-lucide="calendar"></i> <i data-lucide="calendar" class="icon-sm"></i>
</button> </button>
<input type="date" id="new-user-종료일-picker" class="hidden-picker" onchange="document.getElementById('new-user-종료일').value = this.value" tabindex="-1" /> <input type="date" id="new-user-종료일-picker" class="hidden-picker" onchange="document.getElementById('new-user-종료일').value = this.value" tabindex="-1" />
</div> </div>
@@ -163,7 +163,7 @@ class SwUserModal extends BaseModal {
protected fillFormData(asset: any): void { protected fillFormData(asset: any): void {
const swInfo = document.getElementById('sw-user-sw-info')!; const swInfo = document.getElementById('sw-user-sw-info')!;
swInfo.innerHTML = ` swInfo.innerHTML = `
<div class="sw-info-header" style="margin-bottom: 1.5rem; border-bottom: 1px solid var(--hairline); padding-bottom: 1rem;"> <div class="sw-info-header border-b border-hairline pb-4 mb-6">
<div class="detail-label-sm">${asset.purchase_corp || asset. || ''}</div> <div class="detail-label-sm">${asset.purchase_corp || asset. || ''}</div>
<div class="asset-code-title">${asset.product_name || asset. || ''}</div> <div class="asset-code-title">${asset.product_name || asset. || ''}</div>
</div> </div>
@@ -184,7 +184,7 @@ class SwUserModal extends BaseModal {
if (!tbody) return; if (!tbody) return;
tbody.innerHTML = ''; tbody.innerHTML = '';
if (this.tempSwUsers.length === 0) { if (this.tempSwUsers.length === 0) {
tbody.innerHTML = '<tr><td colspan="7" style="text-align:center; padding:2rem; color:var(--mute);">할당된 사용자가 없습니다.</td></tr>'; tbody.innerHTML = '<tr><td colspan="7" class="empty-cell text-center p-8">할당된 사용자가 없습니다.</td></tr>';
return; return;
} }
@@ -195,12 +195,12 @@ class SwUserModal extends BaseModal {
<td>${user. || ''}</td> <td>${user. || ''}</td>
<td>${user. || ''}</td> <td>${user. || ''}</td>
<td>${user. || ''}</td> <td>${user. || ''}</td>
<td>${user. || ''}</td> <td class="text-center">${user. || ''}</td>
<td style="text-align:center;">${user. ? '<i data-lucide="paperclip" class="text-primary"></i>' : '-'}</td> <td class="text-center">${user. ? '<i data-lucide="paperclip" class="text-primary icon-sm"></i>' : '-'}</td>
<td> <td class="text-center">
<div style="display:flex; gap:0.5rem;"> <div class="flex gap-2 justify-center items-center">
<button class="btn btn-outline btn-sm btn-edit-user" data-idx="${idx}">수정</button> <button class="btn btn-outline btn-sm btn-edit-user" data-idx="${idx}">수정</button>
<button class="btn btn-outline btn-sm btn-danger btn-del-user" data-idx="${idx}">삭제</button> <button class="btn-circle-remove btn-del-user" data-idx="${idx}">&times;</button>
</div> </div>
</td> </td>
`; `;

View File

@@ -12,13 +12,16 @@ class UserModal extends BaseModal {
protected renderFrameHTML(): string { protected renderFrameHTML(): string {
return ` return `
<div id="user-asset-modal" class="modal-overlay hidden"> <div id="user-asset-modal" class="modal-overlay hidden">
<div class="modal-content" style="max-width: 500px;"> <div class="modal-content narrow">
<div class="modal-header"> <div class="modal-header">
<h2 id="user-modal-title" class="modal-title">${this.title}</h2> <div class="header-left">
<h2 id="user-modal-title" class="modal-title">${this.title}</h2>
<div id="user-header-identity" class="header-identity"></div>
</div>
<button id="btn-close-user-modal" class="btn-icon" aria-label="닫기">&times;</button> <button id="btn-close-user-modal" class="btn-icon" aria-label="닫기">&times;</button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<form id="user-asset-form" class="grid-form" style="grid-template-columns: 1fr;"> <form id="user-asset-form" class="grid-form vertical-form">
<input type="hidden" id="user-id" name="id" /> <input type="hidden" id="user-id" name="id" />
<div class="form-group"> <div class="form-group">
@@ -127,6 +130,7 @@ class UserModal extends BaseModal {
setFieldValue('user-dept', asset.dept_name || ''); setFieldValue('user-dept', asset.dept_name || '');
setFieldValue('user-position-input', asset.position || ''); setFieldValue('user-position-input', asset.position || '');
setFieldValue('user-status', asset.status || '재직'); setFieldValue('user-status', asset.status || '재직');
this.updateHeaderIdentity(asset);
} }
protected onAfterOpen(asset: any, mode: string): void { protected onAfterOpen(asset: any, mode: string): void {
@@ -151,6 +155,27 @@ class UserModal extends BaseModal {
saveBtn.textContent = '수정'; saveBtn.textContent = '수정';
saveBtn.style.display = 'block'; saveBtn.style.display = 'block';
} }
this.updateHeaderIdentity(asset);
}
private updateHeaderIdentity(asset: any) {
const container = document.getElementById('user-header-identity');
if (!container) return;
if (this.currentMode === 'add') {
container.innerHTML = '<span class="badge badge-primary">신규 등록</span>';
return;
}
const empNo = asset.emp_no || '';
const userName = asset.user_name || '';
const dept = asset.dept_name || '';
container.innerHTML = `
<span class="asset-code-title">${userName}</span>
<span class="service-type-badge">${empNo}</span>
<span class="asset-type-label">${dept}</span>
`;
} }
} }

View File

@@ -24,7 +24,7 @@ export interface FilterOptions {
* 전역 액션 버튼 그룹 생성 (자산 추가 등) * 전역 액션 버튼 그룹 생성 (자산 추가 등)
*/ */
export function getActionButtonsHTML(): string { export function getActionButtonsHTML(): string {
return `<div id="filter-bar-actions" class="header-action-group" style="display: flex; gap: 8px; margin-left: auto; align-self: flex-end;"></div>`; return `<div id="filter-bar-actions" class="header-action-group"></div>`;
} }
export function renderFilterBar(container: HTMLElement, options: FilterOptions) { export function renderFilterBar(container: HTMLElement, options: FilterOptions) {
@@ -41,6 +41,8 @@ export function renderFilterBar(container: HTMLElement, options: FilterOptions)
initialFilters = { keyword: '', corp: '', dept: '', loc: '', field: '', type: '', status: '' } initialFilters = { keyword: '', corp: '', dept: '', loc: '', field: '', type: '', status: '' }
} = options; } = options;
container.classList.add('search-bar'); // Restored class
container.innerHTML = ` container.innerHTML = `
<div class="search-item flex-1"> <div class="search-item flex-1">
<label>${keywordLabel}</label> <label>${keywordLabel}</label>
@@ -88,7 +90,7 @@ export function renderFilterBar(container: HTMLElement, options: FilterOptions)
</div>` : ''} </div>` : ''}
${extraHTML} ${extraHTML}
<button id="btn-reset-filters" class="btn btn-outline btn-reset"> <button id="btn-reset-filters" class="btn btn-outline btn-reset">
<i data-lucide="refresh-ccw"></i> ${UI_TEXT.ACTION.RESET_FILTER} <i data-lucide="refresh-ccw" class="icon-sm"></i> ${UI_TEXT.ACTION.RESET_FILTER}
</button> </button>
${getActionButtonsHTML()} ${getActionButtonsHTML()}
`; `;

View File

@@ -31,18 +31,18 @@
--success: #0070f3; --success: #0070f3;
--header-height: 64px; --header-height: 64px;
/* --- Global Typography Scale (Enhanced Fluid Base) --- */ /* --- Global Typography Scale (Tighter Clamps) --- */
--fs-xs: clamp(10px, 1.2vmin + 0.2vw, 15px); --fs-xs: clamp(10px, 1vmin + 0.1vw, 13px);
--fs-sm: clamp(12px, 1.4vmin + 0.3vw, 18px); --fs-sm: clamp(12px, 1.2vmin + 0.2vw, 15px);
--fs-base: clamp(14px, 1.6vmin + 0.4vw, 22px); --fs-base: clamp(13px, 1.4vmin + 0.2vw, 16px);
--fs-md: clamp(18px, 2.5vmin + 0.5vw, 30px); --fs-md: clamp(16px, 2vmin + 0.3vw, 24px);
--fs-lg: clamp(24px, 4vmin + 0.6vw, 48px); --fs-lg: clamp(20px, 3vmin + 0.4vw, 32px);
--fs-xl: clamp(32px, 6vmin + 0.8vw, 72px); --fs-xl: clamp(28px, 5vmin + 0.6vw, 48px);
/* --- Fluid Layout Units (Aggressive) --- */ /* --- Layout Units --- */
--header-height: clamp(50px, 8vmin, 90px); --header-height: 64px;
--spacing-base: clamp(0.75rem, 3vmin, 3rem); --spacing-base: 1.5rem;
--radius-base: clamp(6px, 1.5vmin, 16px); --radius-base: 8px;
} }
* { * {
@@ -433,7 +433,30 @@ input:checked + .role-slider:before {
.justify-center { justify-content: center; } .justify-center { justify-content: center; }
.gap-1 { gap: 0.25rem; } .gap-1 { gap: 0.25rem; }
.gap-2 { gap: 0.5rem; } .gap-2 { gap: 0.5rem; }
.gap-3 { gap: 0.75rem; }
.gap-4 { gap: 1rem; } .gap-4 { gap: 1rem; }
.gap-6 { gap: 1.5rem; }
.gap-y-3 { row-gap: 0.75rem; }
.gap-x-4 { column-gap: 1rem; }
.mb-0 { margin-bottom: 0 !important; }
.mb-4 { margin-bottom: 1rem !important; }
.mb-6 { margin-bottom: 1.5rem !important; }
.pb-4 { padding-bottom: 1rem !important; }
.p-4 { padding: 1rem !important; }
.p-2 { padding: 0.5rem !important; }
.p-8 { padding: 2rem !important; }
.ml-auto { margin-left: auto !important; }
.self-end { align-self: flex-end !important; }
.font-medium { font-weight: 500; }
.text-muted { color: var(--mute) !important; }
.mt-12 { margin-top: 3rem !important; }
.icon-sm { width: 16px; height: 16px; }
.h-90vh { height: 90vh !important; }
.pt-0 { padding-top: 0 !important; }
.font-semibold { font-weight: 600; }
.w-full { width: 100%; } .w-full { width: 100%; }
.h-full { height: 100%; } .h-full { height: 100%; }
@@ -442,9 +465,154 @@ input:checked + .role-slider:before {
.text-right { text-align: right !important; } .text-right { text-align: right !important; }
.text-left { text-align: left !important; } .text-left { text-align: left !important; }
.font-bold { font-weight: 700; } .font-bold { font-weight: 700; }
.font-semibold { font-weight: 600; } .bg-primary-light { background-color: var(--primary-light) !important; }
.text-success { color: var(--success) !important; }
.text-danger { color: var(--danger) !important; }
.text-blue { color: var(--color-blue) !important; }
.text-orange { color: var(--color-orange) !important; }
/* --- Unified Search & Filter Bar --- */
.search-bar {
display: flex;
flex-wrap: wrap;
gap: var(--spacing-base);
padding: 1.25rem var(--spacing-base);
border-bottom: 1px solid var(--hairline);
align-items: flex-end;
background: var(--canvas);
box-sizing: border-box;
}
.search-item {
display: flex;
flex-direction: column;
gap: 0.5rem;
justify-content: flex-end;
}
.search-item.flex-1 {
flex: 1;
min-width: 300px;
}
.search-item label {
font-size: var(--fs-xs);
font-weight: 600;
color: var(--mute);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.search-item input,
.search-item select {
height: clamp(34px, 4.5vmin, 44px);
padding: 0 0.75rem;
border: 1px solid var(--hairline);
border-radius: 6px;
font-size: var(--fs-sm);
outline: none;
background-color: var(--canvas);
color: var(--primary);
transition: border-color 0.2s;
box-sizing: border-box;
}
.search-item select {
cursor: pointer;
min-width: 120px;
}
.search-item input:focus,
.search-item select:focus {
border-color: var(--primary);
}
.header-action-group {
margin-left: auto;
align-self: flex-end;
display: flex;
align-items: center;
gap: 8px;
}
.list-view-toggle-label {
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
font-weight: 600;
color: var(--primary);
height: clamp(34px, 4.5vmin, 44px);
padding: 0 0.5rem;
font-size: var(--fs-sm);
user-select: none;
}
.list-view-toggle-label input[type="checkbox"] {
width: 16px;
height: 16px;
cursor: pointer;
}
.map-pagination-group {
display: flex;
align-items: center;
gap: 0.75rem;
margin-left: 0.5rem;
padding-left: 1rem;
border-left: 1px solid var(--hairline);
height: clamp(34px, 4.5vmin, 44px);
}
.page-info {
font-size: var(--fs-xs);
color: var(--mute);
font-weight: 500;
white-space: nowrap;
}
/* --- Modal & View Header Layouts --- */
.header-left {
display: flex;
align-items: center;
gap: 1rem;
}
/* --- Asset Identity & Header Styling (Global) --- */
.header-identity {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
flex-wrap: wrap;
}
.asset-code-title {
font-size: var(--fs-md);
font-weight: 600;
color: var(--primary);
letter-spacing: -0.05em;
line-height: 1;
}
.service-type-badge {
font-size: var(--fs-xs);
font-weight: 600;
color: var(--on-primary);
background: var(--primary);
padding: 4px 8px;
border-radius: 9999px;
text-transform: uppercase;
line-height: 1;
}
.asset-type-label {
font-size: var(--fs-sm);
font-weight: 500;
color: var(--mute);
line-height: 1;
}
/* --- Footer --- */
.main-footer { .main-footer {
border-top: 1px solid var(--border-color); border-top: 1px solid var(--border-color);
background-color: var(--canvas); background-color: var(--canvas);

View File

@@ -140,57 +140,6 @@
overflow: hidden; overflow: hidden;
} }
.location-filter-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 1.5rem;
border-bottom: 1px solid var(--hairline);
background: var(--canvas);
flex-shrink: 0;
}
.filter-actions-group {
display: flex;
align-items: center;
gap: 1.5rem;
}
.filter-group {
display: flex;
align-items: center;
gap: 0.75rem;
}
.filter-group label {
font-size: var(--fs-xs);
font-weight: 600;
color: var(--mute);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.filter-row {
display: flex;
align-items: center;
gap: 0.5rem;
}
.map-pagination-group {
margin-left: 0;
padding-left: 0.5rem;
border-left: 1px solid var(--hairline);
display: flex;
align-items: center;
gap: 0.5rem;
}
.page-info {
font-size: var(--fs-xs);
color: var(--mute);
font-weight: 500;
}
.location-main-content { .location-main-content {
flex: 1; flex: 1;
display: grid; display: grid;
@@ -361,18 +310,161 @@
margin-bottom: 4px; margin-bottom: 4px;
} }
.detail-value-lg { .dashboard-layout-2col {
font-size: var(--fs-base); display: grid;
color: var(--primary); grid-template-columns: 1fr 1fr;
font-weight: 500; gap: 2rem;
line-height: 1.4; padding: 0 2rem 2rem;
} }
.text-danger { .dashboard-card {
color: var(--danger) !important; background: var(--canvas);
font-weight: 600; border: 1px solid var(--hairline);
border-radius: 12px;
padding: 2rem;
display: flex;
flex-direction: column;
gap: 1rem;
transition: all 0.2s;
} }
.dashboard-card.clickable:hover {
border-color: var(--primary);
box-shadow: 0 12px 30px rgba(0,0,0,0.08);
transform: translateY(-2px);
}
.stat-progress-bar {
height: 8px;
background: var(--canvas-soft-2);
border-radius: 9999px;
overflow: hidden;
margin-top: 0.5rem;
}
.progress-fill {
height: 100%;
background: var(--primary);
border-radius: 9999px;
}
.dashboard-card .stat-label {
font-size: var(--fs-xs);
font-weight: 600;
color: var(--mute);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.dashboard-card .stat-value {
font-size: var(--fs-xl);
font-weight: 700;
color: var(--primary);
}
.dashboard-card .stat-sub {
font-size: var(--fs-sm);
color: var(--body);
}
.bg-soft {
background-color: var(--canvas-soft) !important;
}
.chart-placeholder {
width: 140px;
height: 140px;
display: flex;
align-items: center;
justify-content: center;
}
.circular-progress {
width: 100px;
height: 100px;
border-radius: 50%;
background: conic-gradient(var(--primary) calc(var(--val) * 1%), var(--hairline) 0);
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.circular-progress::before {
content: "";
position: absolute;
width: 70px;
height: 70px;
background: var(--canvas);
border-radius: 50%;
}
.circular-progress::after {
content: attr(style); /* This is a hack to get the value, but we'll use innerHTML in TS if needed */
position: absolute;
font-size: var(--fs-sm);
font-weight: 700;
}
.system-dashboard {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.warning-badge-orange { background-color: var(--color-orange); color: var(--white); padding: 2px 8px; border-radius: 9999px; font-size: var(--fs-xs); font-weight: 600; }
.warning-badge { background-color: var(--danger); color: var(--white); padding: 2px 8px; border-radius: 9999px; font-size: var(--fs-xs); font-weight: 600; }
.list-section {
flex: 1.3;
display: flex;
flex-direction: column;
min-height: 0;
padding: 1rem 1.5rem 0 0;
border-right: 1px solid var(--hairline);
}
.detail-panel {
flex: 0.7;
display: flex;
flex-direction: column;
min-height: 0;
padding: 1rem 0 0 1.5rem;
overflow: hidden;
}
.detail-empty-state {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: var(--mute);
}
.detail-photo-wrapper {
width: 100%;
flex: 1;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
position: relative;
border: 1px solid var(--hairline);
background: #f0f0f0;
border-radius: 8px;
}
.no-photo-state {
padding: 3rem;
text-align: center;
color: var(--mute);
}
/* Responsive Overrides */ /* Responsive Overrides */
@media (max-width: 1440px) { @media (max-width: 1440px) {
.location-main-content { .location-main-content {

View File

@@ -30,7 +30,10 @@
border: 1px solid var(--hairline); border: 1px solid var(--hairline);
} }
.modal-overlay:not(.hidden) .modal-content { transform: translateY(0); } .modal-overlay.sub-modal {
z-index: 1100;
}
.modal-header { .modal-header {
background-color: var(--canvas); background-color: var(--canvas);
@@ -59,10 +62,71 @@
transition: color 0.2s; transition: color 0.2s;
} }
.modal-header .btn-icon:hover { .header-left {
color: var(--primary) !important; display: flex;
align-items: center;
gap: 0.75rem;
} }
.form-group.relative {
position: relative;
}
/* 동적 리스트 컨테이너 */
.dynamic-row-container {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.volume-row, .remote-info-row {
display: flex;
gap: 0.5rem;
}
.remote-info-row {
flex-direction: column;
}
/* 파일 업로드 디스플레이 */
.file-upload-display {
flex: 1;
border: 1px solid var(--hairline);
border-radius: 6px;
padding: 0 12px;
height: clamp(34px, 4.5vmin, 44px);
display: flex;
align-items: center;
font-size: var(--fs-sm);
color: var(--mute);
background-color: var(--canvas-soft);
}
.btn-circle-remove {
width: clamp(34px, 4.5vmin, 44px);
height: clamp(34px, 4.5vmin, 44px);
border-radius: 50% !important;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0 !important;
color: var(--danger) !important;
border: 1px solid var(--danger) !important;
background: transparent;
font-size: 1.5rem !important; /* Larger X icon */
line-height: 1;
flex-shrink: 0;
transition: all 0.2s;
cursor: pointer;
}
.btn-circle-remove:hover {
background-color: var(--danger);
color: var(--white) !important;
}
.modal-body { .modal-body {
padding: var(--spacing-base); padding: var(--spacing-base);
overflow-y: auto; overflow-y: auto;
@@ -174,11 +238,20 @@
gap: 0.75rem; gap: 0.75rem;
} }
/* Wide Modal for History/Detail */ /* Modal Size Variants */
.modal-content.wide { .modal-content.wide {
max-width: 1000px; max-width: 1000px;
} }
.modal-content.narrow {
max-width: 500px;
}
.vertical-form {
grid-template-columns: 1fr !important;
}
.modal-body-split { .modal-body-split {
display: flex; display: flex;
gap: 2rem; gap: 2rem;
@@ -459,25 +532,31 @@
.layout-map-container { .layout-map-container {
position: relative; position: relative;
width: 100%; display: inline-block;
height: 100%; cursor: crosshair;
display: flex; background-color: var(--canvas-soft-2);
align-items: center; border-radius: 4px;
justify-content: center;
overflow: hidden; overflow: hidden;
} }
.layout-map-img { .layout-map-container.readonly {
max-width: 100%; cursor: default;
max-height: 100%;
object-fit: contain;
display: block;
} }
.layout-map-container:not(.readonly) .layout-map-img { .image-marker-wrapper {
cursor: crosshair; position: relative;
display: inline-block;
} }
.layout-map-img {
display: block;
max-width: 100%;
max-height: 75vh;
user-select: none;
-webkit-user-drag: none;
}
.layout-marker { .layout-marker {
position: absolute; position: absolute;
width: 16px; width: 16px;

View File

@@ -27,65 +27,7 @@
line-height: 1.5; line-height: 1.5;
} }
/* --- Table View & Filter Styles --- */ /* --- Table View Styles --- */
.search-bar {
display: flex;
flex-wrap: wrap;
gap: var(--spacing-base);
padding: 1.25rem var(--spacing-base);
border-bottom: 1px solid var(--hairline);
align-items: flex-end; /* This aligns inputs and buttons at the bottom */
background: var(--canvas);
}
.search-item {
display: flex;
flex-direction: column;
gap: 0.5rem;
justify-content: flex-end;
}
.search-item.flex-1 {
flex: 1;
min-width: 300px;
}
.search-item label {
font-size: var(--fs-xs);
font-weight: 600;
color: var(--mute);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.search-item input,
.search-item select {
height: clamp(34px, 4.5vmin, 44px);
padding: 0 0.75rem;
border: 1px solid var(--hairline);
border-radius: 6px;
font-size: var(--fs-sm);
outline: none;
background-color: var(--canvas);
color: var(--primary);
transition: border-color 0.2s;
}
.search-item select {
padding-right: 2.5rem !important;
cursor: pointer;
}
.search-item input:focus,
.search-item select:focus {
border-color: var(--primary);
}
.btn-reset {
height: 38px !important;
color: var(--mute) !important;
}
.table-container { .table-container {
flex: 1; flex: 1;
background-color: var(--canvas); background-color: var(--canvas);
@@ -186,15 +128,19 @@ th.sortable.desc::after { content: '▼'; opacity: 1; color: var(--primary); }
font-weight: 600; font-weight: 600;
color: var(--mute); color: var(--mute);
border-bottom: 1px solid var(--hairline); border-bottom: 1px solid var(--hairline);
background: var(--canvas); background: var(--canvas-soft);
text-transform: uppercase;
letter-spacing: 0.05em;
} }
.compact-table td { .compact-table td {
padding: 0.75rem 0.5rem; padding: 0.75rem 0.5rem;
font-size: var(--fs-base); font-size: var(--fs-sm);
border-bottom: 1px solid var(--hairline-soft, #f5f5f5); border-bottom: 1px solid var(--hairline-soft, #f5f5f5);
color: var(--primary);
} }
.compact-table tr.clickable-row:hover { .compact-table tr.clickable-row:hover {
background: var(--canvas-soft); background: var(--canvas-soft);
cursor: pointer; cursor: pointer;

File diff suppressed because it is too large Load Diff

View File

@@ -1,15 +1,16 @@
import { state } from '../../core/state'; import { state } from '../../core/state';
import { openSwUsageDetail } from '../../components/Modal/DashboardDetailModal'; import { openSwDashboardDetail, openSwUsageDetail } from '../../components/Modal/DashboardDetailModal';
import { normalizeDate } from '../../core/utils'; import { normalizeDate } from '../../core/utils';
import { ASSET_SCHEMA } from '../../core/schema'; import { ASSET_SCHEMA } from '../../core/schema';
export function renderSwDashboard(container: HTMLElement) { export function renderSwDashboard(container: HTMLElement) {
let extQty = 0, extUsed = 0, extTotal = 0; let extQty = 0, extUsed = 0, extExp = 0, extTotal = 0;
let intQty = 0, intUsed = 0, intTotal = 0; let intQty = 0, intUsed = 0, intExp = 0, intTotal = 0;
let extCost2026 = 0; let extCost2026 = 0;
let intCost2026 = 0; let intCost2026 = 0;
// 통합 SW 데이터
const allSw = [...state.masterData.swExternal, ...state.masterData.swInternal]; const allSw = [...state.masterData.swExternal, ...state.masterData.swInternal];
allSw.forEach(sw => { allSw.forEach(sw => {
@@ -20,6 +21,7 @@ export function renderSwDashboard(container: HTMLElement) {
if (sw.asset_type === '외부SW' || sw.type === '외부SW') { if (sw.asset_type === '외부SW' || sw.type === '외부SW') {
extQty += qty; extUsed += assigned; extTotal++; extQty += qty; extUsed += assigned; extTotal++;
if (isSWExpiring(sw)) extExp++;
if (sw[ASSET_SCHEMA.PURCHASE_DATE.key]?.startsWith('2026')) extCost2026 += price; if (sw[ASSET_SCHEMA.PURCHASE_DATE.key]?.startsWith('2026')) extCost2026 += price;
} else { } else {
intQty += qty; intUsed += assigned; intTotal++; intQty += qty; intUsed += assigned; intTotal++;
@@ -31,14 +33,14 @@ export function renderSwDashboard(container: HTMLElement) {
const intPer = intQty > 0 ? Math.round((intUsed/intQty)*100) : 0; const intPer = intQty > 0 ? Math.round((intUsed/intQty)*100) : 0;
container.innerHTML = ` container.innerHTML = `
<div class="view-container"> <div class="view-container bg-soft">
<h3 class="dashboard-section-title">소프트웨어 라이선스 현황</h3> <h3 class="dashboard-section-title">소프트웨어 라이선스 현황</h3>
<div class="dashboard-layout-2col"> <div class="dashboard-layout-2col mb-6">
<div class="dashboard-card clickable" data-action="ext-usage"> <div class="dashboard-card clickable" data-action="ext-usage">
<div class="stat-label">외부 소프트웨어 사용율</div> <div class="stat-label">외부 소프트웨어 사용율</div>
<div class="stat-sub">${extQty}카피 중 ${extUsed}개 할당</div> <div class="stat-sub">${extQty}카피 중 ${extUsed}개 할당</div>
<div class="stat-value"><span>${extPer}</span><span>%</span></div> <div class="stat-value text-primary">${extPer}%</div>
<div class="stat-progress-bar"> <div class="stat-progress-bar">
<div class="progress-fill" style="width: ${extPer}%;"></div> <div class="progress-fill" style="width: ${extPer}%;"></div>
</div> </div>
@@ -46,23 +48,23 @@ export function renderSwDashboard(container: HTMLElement) {
<div class="dashboard-card clickable" data-action="int-usage"> <div class="dashboard-card clickable" data-action="int-usage">
<div class="stat-label">내부 소프트웨어 현황</div> <div class="stat-label">내부 소프트웨어 현황</div>
<div class="stat-sub">등록된 내부 솔루션: ${intTotal}개</div> <div class="stat-sub">등록된 내부 솔루션: ${intTotal}개</div>
<div class="stat-value"><span>${intPer}</span><span>%</span></div> <div class="stat-value text-primary">${intPer}%</div>
<div class="stat-progress-bar"> <div class="stat-progress-bar">
<div class="progress-fill" style="width: ${intPer}%;"></div> <div class="progress-fill" style="width: ${intPer}%;"></div>
</div> </div>
</div> </div>
</div> </div>
<h3 class="dashboard-section-title">2026년 누적 도입 비용 분석</h3> <h3 class="dashboard-section-title">2026년 누적 도입 비용 분석</h3>
<div class="dashboard-layout-2col"> <div class="dashboard-layout-2col">
<div class="dashboard-card"> <div class="dashboard-card">
<div class="stat-label">외부 SW 누적 비용 (2026)</div> <div class="stat-label">외부 SW 누적 비용 (2026)</div>
<div class="stat-value"><span>₩ ${extCost2026.toLocaleString()}</span></div> <div class="stat-value text-primary">₩ ${extCost2026.toLocaleString()}</div>
</div> </div>
<div class="dashboard-card"> <div class="dashboard-card">
<div class="stat-label">내부 SW 누적 비용 (2026)</div> <div class="stat-label">내부 SW 누적 비용 (2026)</div>
<div class="stat-value" style="color: var(--color-blue);"><span>₩ ${intCost2026.toLocaleString()}</span></div> <div class="stat-value text-blue">₩ ${intCost2026.toLocaleString()}</div>
</div> </div>
</div> </div>
</div> </div>
@@ -71,3 +73,11 @@ export function renderSwDashboard(container: HTMLElement) {
container.querySelector('[data-action="ext-usage"]')?.addEventListener('click', () => openSwUsageDetail('외부 소프트웨어 사용 목록', state.masterData.swExternal)); container.querySelector('[data-action="ext-usage"]')?.addEventListener('click', () => openSwUsageDetail('외부 소프트웨어 사용 목록', state.masterData.swExternal));
container.querySelector('[data-action="int-usage"]')?.addEventListener('click', () => openSwUsageDetail('내부 소프트웨어 사용 목록', state.masterData.swInternal)); container.querySelector('[data-action="int-usage"]')?.addEventListener('click', () => openSwUsageDetail('내부 소프트웨어 사용 목록', state.masterData.swInternal));
} }
function isSWExpiring(sw: any) {
const expiry = sw[ASSET_SCHEMA.EXPIRED_DATE.key];
if (!expiry) return false;
const endMs = new Date(normalizeDate(expiry)).getTime();
const diffDays = (endMs - Date.now()) / (1000 * 60 * 60 * 24);
return diffDays >= 0 && diffDays <= 30;
}

View File

@@ -189,25 +189,10 @@ export function createListView(container: HTMLElement, config: ListViewConfig) {
// 2. 필터 바 생성 (자산 목록에서만 사용) // 2. 필터 바 생성 (자산 목록에서만 사용)
const filterBar = document.createElement('div'); const filterBar = document.createElement('div');
filterBar.className = 'search-bar'; filterBar.className = 'filter-bar';
// 자산 추가 버튼 및 목록 보기 체크박스 추가 로직 // 자산 추가 버튼 및 목록 보기 체크박스 추가 로직
const showPcFlowBtn = config.title === 'PC'; const showPcFlowBtn = config.title === 'PC';
const extraActionHTML = `
<div class="header-action-group flex items-center gap-2" style="margin-left: auto; align-self: flex-end;">
${showPcFlowBtn ? `
<button id="btn-goto-parts-master" class="btn btn-outline btn-sm">
<i data-lucide="settings" class="icon-sm"></i> 부품 마스터
</button>
<button id="btn-pc-flow" class="btn btn-outline btn-sm">
PC 이동/반납
</button>
` : ''}
<button id="btn-add-asset" class="btn btn-primary">
<i data-lucide="plus" class="icon-sm"></i> 자산 추가
</button>
</div>
`;
container.appendChild(filterBar); container.appendChild(filterBar);
container.appendChild(contentWrapper); container.appendChild(contentWrapper);
@@ -242,15 +227,12 @@ export function createListView(container: HTMLElement, config: ListViewConfig) {
selectedLocation = validLocations[0] || ''; selectedLocation = validLocations[0] || '';
} }
const locationCounts: Record<string, number> = {}; // 동적 통계 수집 객체
const pcTypeCounts = { public: 0, server: 0, personal: 0 };
// 동적 통계 수집 객체 (Hardcoding 제거)
const extStats = { const extStats = {
total: 0, total: 0,
locCounts: {} as Record<string, number>, locCounts: {} as Record<string, number>,
typeCounts: {} as Record<string, number>, typeCounts: {} as Record<string, number>,
typeLocMap: {} as Record<string, Record<string, number>>, // 유형별 위치 분포 typeLocMap: {} as Record<string, Record<string, number>>,
locWarning: 0, locWarning: 0,
typeWarning: 0 typeWarning: 0
}; };
@@ -326,10 +308,10 @@ export function createListView(container: HTMLElement, config: ListViewConfig) {
<div class="stat-group-item bordered">${generateDetailStatHTML('내부 (테스트) 상세', intStats)}</div> <div class="stat-group-item bordered">${generateDetailStatHTML('내부 (테스트) 상세', intStats)}</div>
</div> </div>
<div style="display: flex; flex: 1; min-height: 0; border-top: 1px solid var(--border-color);"> <div class="flex flex-1 min-h-0 border-t border-hairline">
<!-- 좌측: 자산 현황 목록 (Border-based Separation) --> <!-- 좌측: 자산 현황 목록 -->
<div class="list-section" style="flex: 1.3; display: flex; flex-direction: column; min-height: 0; padding: 1rem 1.5rem 0 0; border-right: 1px solid var(--hairline);"> <div class="list-section">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; flex-shrink: 0;"> <div class="flex justify-between items-center mb-4 flex-shrink-0">
<h4 id="list-section-title" class="sidebar-title"> <h4 id="list-section-title" class="sidebar-title">
${isPcView ? `🔄 PC 유동 이력 (${new Date().getMonth() + 1}월)` : '자산 현황 목록'} ${isPcView ? `🔄 PC 유동 이력 (${new Date().getMonth() + 1}월)` : '자산 현황 목록'}
</h4> </h4>
@@ -344,7 +326,7 @@ export function createListView(container: HTMLElement, config: ListViewConfig) {
</div> </div>
` : ''} ` : ''}
</div> </div>
<div style="flex: 1; overflow-y: auto;"> <div class="flex-1 overflow-y-auto">
<table class="compact-table"> <table class="compact-table">
<thead> <thead>
${isPcView ? ` ${isPcView ? `
@@ -372,17 +354,17 @@ export function createListView(container: HTMLElement, config: ListViewConfig) {
</div> </div>
</div> </div>
<!-- 우측: 상세 정보 패널 (Box-less, Line-based) --> <!-- 우측: 상세 정보 패널 -->
<div id="system-detail-panel" style="flex: 0.7; display: flex; flex-direction: column; min-height: 0; padding: 1rem 0 0 1.5rem; overflow: hidden;"> <div id="system-detail-panel" class="detail-panel">
<div id="detail-empty-state" class="detail-empty-state" style="justify-content: ${isPcView ? 'flex-start' : 'center'}; align-items: ${isPcView ? 'stretch' : 'center'};"> <div id="detail-empty-state" class="detail-empty-state">
${isPcView ? ` ${isPcView ? `
<div style="display: flex; flex-direction: column; min-height: 0; height: 100%; text-align: left;"> <div class="flex-col h-full text-left">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; flex-shrink: 0;"> <div class="flex justify-between items-center mb-4 flex-shrink-0">
<h4 class="sidebar-title text-danger"> <h4 class="sidebar-title text-danger">
⚠️ 사양 주의 장비 현황 (부족/오버스펙) ⚠️ 사양 주의 장비 현황 (부족/오버스펙)
</h4> </h4>
</div> </div>
<div style="flex: 1; overflow-y: auto;"> <div class="flex-1 overflow-y-auto">
<table class="compact-table"> <table class="compact-table">
<thead> <thead>
<tr> <tr>
@@ -402,8 +384,8 @@ export function createListView(container: HTMLElement, config: ListViewConfig) {
<p class="empty-list-message">목록에서 자산을 선택하면<br>상세 정보와 배치도가 표시됩니다.</p> <p class="empty-list-message">목록에서 자산을 선택하면<br>상세 정보와 배치도가 표시됩니다.</p>
`} `}
</div> </div>
<div id="detail-content" class="detail-content hidden" style="flex: 1; display: flex; flex-direction: column; overflow: hidden;"> <div id="detail-content" class="detail-content hidden flex-col flex-1 overflow-hidden">
<div class="detail-header-actions" style="padding: 1.25rem 1.5rem; border-bottom: 1px solid var(--hairline); background: white;"> <div class="detail-header-actions bg-canvas p-4 border-b border-hairline">
<div class="header-identity"> <div class="header-identity">
<span class="asset-code-title" id="detail-asset-code"></span> <span class="asset-code-title" id="detail-asset-code"></span>
<span class="asset-type-label" id="detail-asset-type"></span> <span class="asset-type-label" id="detail-asset-type"></span>
@@ -412,15 +394,15 @@ export function createListView(container: HTMLElement, config: ListViewConfig) {
</div> </div>
<!-- 메인 배치도 영역 --> <!-- 메인 배치도 영역 -->
<div style="flex: 1; display: flex; flex-direction: column; min-height: 0; overflow: hidden; padding: 1rem;"> <div class="flex-col flex-1 overflow-hidden p-4">
<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(--hairline); background: #f0f0f0; border-radius: 8px;"> <div id="detail-photo-wrapper" class="detail-photo-wrapper">
<div class="layout-map-container readonly" style="position: relative; display: flex; align-items: center; justify-content: center; width: 100%; height: 100%;"> <div class="layout-map-container readonly w-full h-full justify-center">
<img id="detail-photo" src="" style="display: block; max-width: 100%; max-height: 100%; width: auto; height: auto; object-fit: contain; pointer-events: none;" /> <img id="detail-photo" src="" class="layout-map-img pointer-events-none" />
<iframe id="detail-html-map" src="" style="display: none; width: 100%; height: 100%; border: none;"></iframe> <iframe id="detail-html-map" src="" class="hidden w-full h-full border-none"></iframe>
<div id="detail-marker" class="layout-marker pulse-marker" style="display: none; position: absolute; z-index: 20;"></div> <div id="detail-marker" class="layout-marker pulse-marker hidden absolute z-20"></div>
<div id="detail-overlay-layer" style="position: absolute; pointer-events: none;"></div> <div id="detail-overlay-layer" class="absolute pointer-events-none"></div>
</div> </div>
<div id="detail-no-photo" class="no-photo-state hidden" style="padding: 3rem; text-align: center; color: var(--mute);"> <div id="detail-no-photo" class="no-photo-state hidden">
<span>등록된 배치도가 없습니다.</span> <span>등록된 배치도가 없습니다.</span>
</div> </div>
</div> </div>
@@ -438,16 +420,13 @@ export function createListView(container: HTMLElement, config: ListViewConfig) {
emptyState.classList.add('hidden'); emptyState.classList.add('hidden');
content.classList.remove('hidden'); content.classList.remove('hidden');
content.style.display = 'flex';
const codeEl = document.getElementById('detail-asset-code'); const codeEl = document.getElementById('detail-asset-code');
const typeEl = document.getElementById('detail-asset-type'); const typeEl = document.getElementById('detail-asset-type');
const memoEl = document.getElementById('detail-memo');
const viewBtn = document.getElementById('btn-view-full-detail') as HTMLButtonElement; const viewBtn = document.getElementById('btn-view-full-detail') as HTMLButtonElement;
if (codeEl) codeEl.textContent = asset.asset_code || '미지정'; if (codeEl) codeEl.textContent = asset.asset_code || '미지정';
if (typeEl) typeEl.textContent = asset.asset_type || '-'; 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 photo = document.getElementById('detail-photo') as HTMLImageElement;
@@ -474,11 +453,13 @@ export function createListView(container: HTMLElement, config: ListViewConfig) {
if (overlayLayer) overlayLayer.innerHTML = ''; if (overlayLayer) overlayLayer.innerHTML = '';
if (htmlMap) { if (htmlMap) {
htmlMap.src = `${imgPath}?markerX=${x}&markerY=${y}`; htmlMap.src = `${imgPath}?markerX=${x}&markerY=${y}`;
htmlMap.classList.remove('hidden');
htmlMap.style.display = 'block'; htmlMap.style.display = 'block';
} }
} else { } else {
if (htmlMap) { if (htmlMap) {
htmlMap.src = ''; htmlMap.src = '';
htmlMap.classList.add('hidden');
htmlMap.style.display = 'none'; htmlMap.style.display = 'none';
} }
photo.src = imgPath; photo.src = imgPath;
@@ -545,27 +526,6 @@ export function createListView(container: HTMLElement, config: ListViewConfig) {
if (overlayLayer) overlayLayer.innerHTML = ''; if (overlayLayer) overlayLayer.innerHTML = '';
if (noPhoto) { noPhoto.classList.remove('hidden'); noPhoto.style.display = 'flex'; } if (noPhoto) { noPhoto.classList.remove('hidden'); noPhoto.style.display = 'flex'; }
} }
const flowLogsBtn = document.getElementById('btn-view-flow-logs');
if (flowLogsBtn) {
flowLogsBtn.onclick = () => {
const emptyState = document.getElementById('detail-empty-state');
const content = document.getElementById('detail-content');
if (emptyState && content) {
content.style.display = 'none';
emptyState.style.display = 'flex';
}
const tbody = document.getElementById('system-status-tbody');
if (tbody) {
tbody.querySelectorAll('.mini-row').forEach(r => {
r.classList.remove('active');
});
}
if (isPcView) {
updateTableOnly();
}
};
}
}; };
// [자산 현황] 테이블 렌더러 // [자산 현황] 테이블 렌더러
@@ -588,19 +548,19 @@ export function createListView(container: HTMLElement, config: ListViewConfig) {
const tbody = document.getElementById('system-status-tbody'); const tbody = document.getElementById('system-status-tbody');
if (tbody) { if (tbody) {
tbody.innerHTML = finalDisplayList.length === 0 tbody.innerHTML = finalDisplayList.length === 0
? `<tr><td colspan="5" class="empty-cell">조회된 자산이 없습니다.</td></tr>` ? `<tr><td colspan="5" class="empty-cell text-center">조회된 자산이 없습니다.</td></tr>`
: finalDisplayList.map(asset => { : finalDisplayList.map(asset => {
const purpose = asset[ASSET_SCHEMA.ASSET_PURPOSE.key] || ''; const purpose = asset[ASSET_SCHEMA.ASSET_PURPOSE.key] || '';
const serviceType = asset.service_type || '외부'; const serviceType = asset.service_type || '외부';
const type = asset[ASSET_SCHEMA.ASSET_TYPE.key] || ''; const type = asset[ASSET_SCHEMA.ASSET_TYPE.key] || '';
const loc = asset[ASSET_SCHEMA.LOCATION.key] || ''; const loc = asset[ASSET_SCHEMA.LOCATION.key] || '';
const isWarning = serviceType === '외부SW' && (loc !== 'IDC' || type.toLowerCase().includes('서버pc')); const isWarning = serviceType === '외부' && (loc !== 'IDC' || type.toLowerCase().includes('서버pc'));
const managerMain = asset[ASSET_SCHEMA.MANAGER_MAIN.key] || '-'; const managerMain = asset[ASSET_SCHEMA.MANAGER_MAIN.key] || '-';
const managerSub = asset[ASSET_SCHEMA.MANAGER_SUB.key] || '-'; const managerSub = asset[ASSET_SCHEMA.MANAGER_SUB.key] || '-';
return ` return `
<tr class="mini-row ${isWarning ? 'warning' : ''}" data-id="${asset.id}"> <tr class="mini-row clickable-row ${isWarning ? 'warning' : ''}" data-id="${asset.id}">
<td class="text-center"> <td class="text-center">
<span class="badge ${isWarning ? 'badge-danger' : 'badge-primary'}">${serviceType}</span> <span class="badge ${isWarning ? 'badge-danger' : 'badge-primary'}">${serviceType}</span>
</td> </td>
@@ -644,12 +604,10 @@ export function createListView(container: HTMLElement, config: ListViewConfig) {
let filtered = applyCommonFilters(fullList, currentFilters, config.searchKeys as any[]); let filtered = applyCommonFilters(fullList, currentFilters, config.searchKeys as any[]);
if (sortState.key) filtered = dynamicSort(filtered, sortState.key, sortState.direction); if (sortState.key) filtered = dynamicSort(filtered, sortState.key, sortState.direction);
// Headers are naturally centered via CSS now. Only apply specific widths or sorting.
thead.innerHTML = `<tr>${config.columns.map(col => `<th ${col.sortKey ? `data-sort="${col.sortKey}"` : ''} style="${col.width ? `width:${col.width};` : ''}">${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};` : ''}">${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>` 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 => { : filtered.map(asset => `<tr class="asset-row clickable" data-id="${asset.id}">${config.columns.map(col => {
// Date columns should remain centered. Everything else defaults to left (via CSS).
const isDateCol = col.header.includes('일') || col.header.includes('날짜') || col.header.includes('연월'); const isDateCol = col.header.includes('일') || col.header.includes('날짜') || col.header.includes('연월');
return `<td class="${isDateCol ? 'text-center' : ''}">${col.render(asset)}</td>`; return `<td class="${isDateCol ? 'text-center' : ''}">${col.render(asset)}</td>`;
}).join('')}</tr>`).join(''); }).join('')}</tr>`).join('');
@@ -674,9 +632,9 @@ export function createListView(container: HTMLElement, config: ListViewConfig) {
...config.filterOptions, ...config.filterOptions,
initialFilters: currentFilters, initialFilters: currentFilters,
extraHTML: isServer ? ` extraHTML: isServer ? `
<div class="search-item"> <div class="filter-group">
<label class="flex items-center gap-2 cursor-pointer font-semibold" style="color: var(--primary); height: clamp(34px, 4.5vmin, 44px); padding: 0 0.5rem;"> <label class="list-view-toggle-label">
<input type="checkbox" id="chk-list-view" ${(state as any).currentViewMode === 'asset' ? 'checked' : ''} style="width: 16px; height: 16px; cursor: pointer;" /> <input type="checkbox" id="chk-list-view" ${(state as any).currentViewMode === 'asset' ? 'checked' : ''} />
목록보기 목록보기
</label> </label>
</div> </div>
@@ -684,24 +642,24 @@ export function createListView(container: HTMLElement, config: ListViewConfig) {
onFilterChange: (filters) => { Object.assign(currentFilters, filters); updateTable(); } onFilterChange: (filters) => { Object.assign(currentFilters, filters); updateTable(); }
}); });
// 3. 필터 바 내 액션 버튼 배치 (자산 추가, 부품 마스터 등) // 3. 필터 바 내 액션 버튼 배치
const actionContainer = filterBar.querySelector('#filter-bar-actions'); const actionContainer = filterBar.querySelector('#filter-bar-actions');
if (actionContainer) { if (actionContainer) {
actionContainer.className = "header-action-group flex items-center gap-2 ml-auto self-end";
actionContainer.innerHTML = ` actionContainer.innerHTML = `
${showPcFlowBtn ? ` ${showPcFlowBtn ? `
<button id="btn-goto-parts-master" class="btn btn-outline"> <button id="btn-goto-parts-master" class="btn btn-outline">
<i data-lucide="settings" style="width: 18px; height: 18px;"></i> 부품 마스터 <i data-lucide="settings" class="icon-sm"></i> 부품 마스터
</button> </button>
<button id="btn-pc-flow" class="btn btn-outline"> <button id="btn-pc-flow" class="btn btn-outline">
PC 이동/반납 PC 이동/반납
</button> </button>
` : ''} ` : ''}
<button id="btn-add-asset" class="btn btn-primary"> <button id="btn-add-asset" class="btn btn-primary">
<i data-lucide="plus" style="width: 18px; height: 18px;"></i> 자산 추가 <i data-lucide="plus" class="icon-sm"></i> 자산 추가
</button> </button>
`; `;
// 버튼 이벤트 바인딩
actionContainer.querySelector('#btn-add-asset')?.addEventListener('click', () => { actionContainer.querySelector('#btn-add-asset')?.addEventListener('click', () => {
const dummyAsset = { id: '', category: config.title }; const dummyAsset = { id: '', category: config.title };
config.onRowClick && config.onRowClick(dummyAsset); config.onRowClick && config.onRowClick(dummyAsset);
@@ -717,7 +675,6 @@ export function createListView(container: HTMLElement, config: ListViewConfig) {
// 서버 탭 전용 목록보기 체크박스 이벤트 // 서버 탭 전용 목록보기 체크박스 이벤트
if (isServer) { if (isServer) {
const toggleBtn = filterBar.querySelector('#btn-toggle-list-view');
const chkBox = filterBar.querySelector('#chk-list-view') as HTMLInputElement; const chkBox = filterBar.querySelector('#chk-list-view') as HTMLInputElement;
const handleToggle = () => { const handleToggle = () => {
@@ -731,10 +688,6 @@ export function createListView(container: HTMLElement, config: ListViewConfig) {
} }
window.dispatchEvent(new Event('refresh-view')); window.dispatchEvent(new Event('refresh-view'));
}; };
toggleBtn?.addEventListener('click', (e) => {
if (e.target !== chkBox) handleToggle();
});
chkBox?.addEventListener('change', handleToggle); chkBox?.addEventListener('change', handleToggle);
} }