feat: restructure navigation, customize list columns, and move action buttons to search bar

1. Restructured navigation hierarchy (Hardware, Software, Ops Support, etc.).
2. Customized table columns for all asset categories according to new specs.
3. Moved Template/Upload/Export/Add buttons to search bar with layout optimization.
4. Hidden Asset Code and Previous User from list views (Modal only).
5. Added Current/Previous User and detailed PC spec fields (GPU, HDD3/4).
This commit is contained in:
2026-05-20 14:34:07 +09:00
parent 2af79cdad3
commit d34ebb8500
23 changed files with 1509 additions and 964 deletions

View File

@@ -37,19 +37,7 @@
<button id="btn-open-guide-header" class="btn btn-outline" title="프로세스 가이드">
<i data-lucide="book-open"></i> 가이드
</button>
<button id="btn-download-template" class="btn btn-outline" title="통합 양식 다운로드">
<i data-lucide="download"></i> 양식
</button>
<label for="excel-upload" class="btn btn-outline" title="엑셀 파일 업로드">
<i data-lucide="upload"></i> 업로드
</label>
<input type="file" id="excel-upload" accept=".xlsx, .xls" style="display: none;" />
<button id="btn-export-excel" class="btn btn-primary" title="일괄 엑셀 저장">
<i data-lucide="file-spreadsheet"></i> 엑셀저장
</button>
<button id="btn-add-asset" class="btn btn-primary hidden">
<i data-lucide="plus"></i> 자산추가
</button>
</div>
</div>
</header>

View File

@@ -1,5 +1,6 @@
import { HardwareAsset, SoftwareAsset } from '../../core/excelHandler';
import { state } from '../../core/state';
import { ASSET_SCHEMA } from '../../core/schema';
import { createIcons, X } from 'lucide';
const DASHBOARD_DETAIL_MODAL_HTML = `
<div id="dashboard-detail-modal" class="modal-overlay hidden">
@@ -37,9 +38,11 @@ export function initDashboardDetailModal() {
closeBtn.addEventListener('click', closeModal);
cancelBtn.addEventListener('click', closeModal);
modal.addEventListener('click', (e) => { if (e.target === modal) closeModal(); });
createIcons({ icons: { X } });
}
export function openDashboardDetail(title: string, list: HardwareAsset[]) {
export function openDashboardDetail(title: string, list: any[]) {
const modal = document.getElementById('dashboard-detail-modal');
if (!modal) return;
const titleEl = document.getElementById('dashboard-detail-modal-title');
@@ -49,23 +52,23 @@ export function openDashboardDetail(title: string, list: HardwareAsset[]) {
if (!thead) return;
titleEl.textContent = title;
thead.innerHTML = `<tr><th>No</th><th>유형</th><th>자산코드</th><th>명칭/모델</th><th>위치</th><th>담당/사용자</th><th>구매연월</th><th>금액</th></tr>`;
thead.innerHTML = `<tr><th>No</th><th>유형</th><th>명칭/모델</th><th>위치</th><th>담당/사용자</th><th>구매일자</th><th>금액</th></tr>`;
tbody.innerHTML = '';
if (list.length === 0) {
tbody.innerHTML = `<tr><td colspan="8" style="text-align:center; padding: 2rem;">해당 조건의 자산이 없습니다.</td></tr>`;
tbody.innerHTML = `<tr><td colspan="7" style="text-align:center; padding: 2rem;">해당 조건의 자산이 없습니다.</td></tr>`;
} else {
list.forEach((asset, idx) => {
let manager = asset. || asset. || asset._정 || '-';
let name = asset. || asset. || '-';
let manager = asset[ASSET_SCHEMA.MANAGER_MAIN.key] || asset.current_user || '-';
let name = asset[ASSET_SCHEMA.MODEL_NAME.key] || asset[ASSET_SCHEMA.ASSET_NAME.key] || '-';
const tr = document.createElement('tr');
tr.innerHTML = `<td>${idx+1}</td><td>${asset.type}</td><td>${asset.}</td><td>${name}</td><td>${asset.||'-'}</td><td>${manager}</td><td>${asset.||'-'}</td><td>${asset.||'-'}</td>`;
tr.innerHTML = `<td>${idx+1}</td><td>${asset.category || asset[ASSET_SCHEMA.ASSET_TYPE.key]}</td><td>${name}</td><td>${asset[ASSET_SCHEMA.LOCATION.key]||'-'}</td><td>${manager}</td><td>${asset[ASSET_SCHEMA.PURCHASE_DATE.key]||'-'}</td><td>${asset[ASSET_SCHEMA.PURCHASE_AMOUNT.key]||'-'}</td>`;
tbody.appendChild(tr);
});
}
modal.classList.remove('hidden');
}
export function openSwDashboardDetail(title: string, list: SoftwareAsset[]) {
export function openSwDashboardDetail(title: string, list: any[]) {
const modal = document.getElementById('dashboard-detail-modal');
if (!modal) return;
const titleEl = document.getElementById('dashboard-detail-modal-title');
@@ -79,13 +82,13 @@ export function openSwDashboardDetail(title: string, list: SoftwareAsset[]) {
tbody.innerHTML = '';
list.forEach((sw, idx) => {
const tr = document.createElement('tr');
tr.innerHTML = `<td>${idx+1}</td><td>${sw.type}</td><td>${sw.}</td><td>${sw.}</td><td>${sw.}</td><td>${sw.}</td>`;
tr.innerHTML = `<td>${idx+1}</td><td>${sw.asset_type || sw.type}</td><td>${sw[ASSET_SCHEMA.PURCHASE_CORP.key]}</td><td>${sw[ASSET_SCHEMA.PRODUCT_NAME.key]}</td><td>${sw[ASSET_SCHEMA.ASSET_COUNT.key]}</td><td>${sw[ASSET_SCHEMA.PURCHASE_AMOUNT.key]}</td>`;
tbody.appendChild(tr);
});
modal.classList.remove('hidden');
}
export function openSwUsageDetail(title: string, list: SoftwareAsset[]) {
export function openSwUsageDetail(title: string, list: any[]) {
const modal = document.getElementById('dashboard-detail-modal');
if (!modal) return;
const titleEl = document.getElementById('dashboard-detail-modal-title');
@@ -99,14 +102,15 @@ export function openSwUsageDetail(title: string, list: SoftwareAsset[]) {
tbody.innerHTML = '';
list.forEach((sw, idx) => {
const assigned = state.masterData.swUsers.filter(u => u.sw_id === sw.id).length;
const qty = Number(sw[ASSET_SCHEMA.ASSET_COUNT.key] || 0);
const tr = document.createElement('tr');
tr.innerHTML = `<td>${idx+1}</td><td>${sw.}</td><td>${sw.}</td><td>${sw.}</td><td>${assigned}</td><td>${Number(sw.) - assigned}</td>`;
tr.innerHTML = `<td>${idx+1}</td><td>${sw[ASSET_SCHEMA.PURCHASE_CORP.key]}</td><td>${sw[ASSET_SCHEMA.PRODUCT_NAME.key]}</td><td>${qty}</td><td>${assigned}</td><td>${qty - assigned}</td>`;
tbody.appendChild(tr);
});
modal.classList.remove('hidden');
}
export function openCloudDashboardDetail(title: string, list: SoftwareAsset[]) {
export function openCloudDashboardDetail(title: string, list: any[]) {
const modal = document.getElementById('dashboard-detail-modal');
if (!modal) return;
const titleEl = document.getElementById('dashboard-detail-modal-title');
@@ -116,15 +120,15 @@ export function openCloudDashboardDetail(title: string, list: SoftwareAsset[]) {
if (!thead) return;
titleEl.textContent = title;
thead.innerHTML = `<tr><th>No</th><th>플랫폼</th><th>법인</th><th>제품명</th><th>결제일</th><th>당월청구액(원)</th></tr>`;
thead.innerHTML = `<tr><th>No</th><th>플랫폼/목적</th><th>법인</th><th>제품명</th><th>결제일</th><th>당월청구액(원)</th></tr>`;
tbody.innerHTML = '';
if (list.length === 0) {
tbody.innerHTML = `<tr><td colspan="6" style="text-align:center; padding: 2rem;">해당 내역이 없습니다.</td></tr>`;
} else {
list.forEach((sw, idx) => {
const priceStr = sw. ? Number(sw..replace(/[^0-9]/g, '')).toLocaleString() : '0';
const priceStr = sw[ASSET_SCHEMA.PURCHASE_AMOUNT.key] ? Number(String(sw[ASSET_SCHEMA.PURCHASE_AMOUNT.key]).replace(/[^0-9]/g, '')).toLocaleString() : '0';
const tr = document.createElement('tr');
tr.innerHTML = `<td>${idx+1}</td><td>${sw.||'-'}</td><td>${sw.||'-'}</td><td>${sw.||'-'}</td><td>${sw. ? sw. + '일' : '-'}</td><td>₩ ${priceStr}</td>`;
tr.innerHTML = `<td>${idx+1}</td><td>${sw[ASSET_SCHEMA.DEV_OBJ.key]||'-'}</td><td>${sw[ASSET_SCHEMA.PURCHASE_CORP.key]||'-'}</td><td>${sw[ASSET_SCHEMA.PRODUCT_NAME.key]||'-'}</td><td>${sw.pay_day ? sw.pay_day + '일' : '-'}</td><td>₩ ${priceStr}</td>`;
tbody.appendChild(tr);
});
}

View File

@@ -1,484 +1,388 @@
import { state, saveHardwareAsset, deleteHardwareAsset } from '../../core/state';
import { HardwareAsset } from '../../core/excelHandler';
import { openModal } from './BaseModal';
import { state, saveAsset } from '../../core/state';
import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema';
import { createIcons, History, Plus, X, Save, Edit2, RotateCcw, Paperclip } from 'lucide';
import { CORP_LIST, ORG_LIST, HW_TYPE_LIST, LOCATION_DATA, TYPE_PREFIX_MAP } from './SharedData';
import {
generateOptionsHTML,
setFieldValue,
getFieldValue,
setEditLock,
parseAndSetLocation,
bindLocationEvents,
getCombinedLocation,
setEditLock,
createModalFrameHTML,
autoFillForm,
autoExtractForm
applyDateMask
} from './ModalUtils';
import { CORP_LIST, LOCATION_DATA, ORG_LIST } from './SharedData';
import { createIcons, X, History, Plus, Save, Paperclip, Calendar, Monitor, Cpu, Network, ShieldCheck } from 'lucide';
let currentAsset: HardwareAsset | null = null;
let currentHwAsset: any | null = null;
let isEditMode = false;
const STATUS_LIST = ['대여중', '보관중', '수리중', '기타'];
/**
* 하드웨어 필드 매핑 (통합 스키마 기반)
*/
const HW_FIELD_MAP: Record<string, string> = {
'유형': ASSET_SCHEMA.TYPE.key,
'법인': ASSET_SCHEMA.CORP.key,
'자산코드': ASSET_SCHEMA.ASSET_CODE.key,
'현사용조직': ASSET_SCHEMA.ORG.key,
'이전사용조직': ASSET_SCHEMA.PREV_ORG.key,
'상세용도': '상세용도',
'모델명': ASSET_SCHEMA.MODEL.key,
'메인보드': ASSET_SCHEMA.MAINBOARD.key,
'명칭': '명칭',
'보관위치': ASSET_SCHEMA.STORE_LOC.key,
'현재상태': ASSET_SCHEMA.STATUS.key,
'IP주소': ASSET_SCHEMA.IP_ADDR.key,
'IP2': ASSET_SCHEMA.IP_ADDR2.key,
'원격접속': '원격접속',
'서버ID': '서버ID',
'서버PW': '서버PW',
'모니터링': '모니터링',
'OS': ASSET_SCHEMA.OS.key,
'CPU': ASSET_SCHEMA.CPU.key,
'GPU': ASSET_SCHEMA.GPU.key,
'RAM': ASSET_SCHEMA.RAM.key,
'SSD1': ASSET_SCHEMA.STORAGE1.key,
'SSD2': ASSET_SCHEMA.STORAGE2.key,
'SSD3': ASSET_SCHEMA.STORAGE3.key,
'HW사양': 'HW사양',
'담당자_정': ASSET_SCHEMA.MANAGER_MAIN.key,
'담당자_부': ASSET_SCHEMA.MANAGER_SUB.key,
'구매일': ASSET_SCHEMA.PURCHASE_YM.key,
'금액': ASSET_SCHEMA.PRICE.key,
'납품업체': ASSET_SCHEMA.VENDOR.key,
'비고': ASSET_SCHEMA.REMARKS.key,
'사용자': ASSET_SCHEMA.USER.key
};
const HW_FORM_HTML = `
<div class="form-section-title">기본 정보 (Identity)</div>
<div class="form-group">
<label for="hw-법인">${ASSET_SCHEMA.CORP.ui}</label>
<select id="hw-법인" required>${generateOptionsHTML(CORP_LIST)}</select>
const HW_MODAL_HTML = `
<div id="hw-asset-modal" class="modal-overlay hidden">
<div class="modal-content wide">
<div class="modal-header">
<h2 id="hw-modal-title">자산 상세 정보</h2>
<button id="btn-close-hw-modal" class="btn-icon" aria-label="닫기"><i data-lucide="x"></i></button>
</div>
<div class="modal-body">
<div class="modal-body-split">
<div class="modal-form-area">
<form id="hw-asset-form" class="grid-form">
<input type="hidden" id="hw-id" name="id" />
<!-- Group 1: 기본 및 관리 정보 -->
<div class="form-section-title">기본 및 관리 정보</div>
<div class="form-group">
<label for="hw-자산코드">${ASSET_SCHEMA.ASSET_CODE.ui}</label>
<label>${ASSET_SCHEMA.ASSET_CODE.ui}</label>
<div class="input-with-btn">
<input type="text" id="hw-자산코드" readonly class="is-readonly-field" placeholder="번호 생성을 클릭하세요" required />
<button type="button" id="btn-generate-hw-code" class="btn btn-outline btn-sm">생성</button>
<input type="text" id="hw-asset_code" name="asset_code" placeholder="자동 생성" readonly />
<button type="button" id="btn-gen-hw-code" class="btn btn-outline btn-sm btn-helper">생성</button>
</div>
</div>
<div class="form-group">
<label>${ASSET_SCHEMA.PURCHASE_CORP.ui}</label>
<select id="hw-purchase_corp" name="purchase_corp">${generateOptionsHTML(CORP_LIST)}</select>
</div>
<div class="form-group">
<label>${ASSET_SCHEMA.CATEGORY.ui}</label>
<input type="text" id="hw-category" name="category" />
</div>
<div class="form-group">
<label>${ASSET_SCHEMA.HW_STATUS.ui}</label>
<select id="hw-hw_status" name="hw_status">
<option value="운영">운영</option>
<option value="재고">재고</option>
<option value="수리">수리</option>
<option value="폐기">폐기</option>
</select>
</div>
<div class="form-group dept-field">
<label>${ASSET_SCHEMA.CURRENT_DEPT.ui}</label>
<select id="hw-current_dept" name="current_dept">${generateOptionsHTML(ORG_LIST)}</select>
</div>
<div class="form-group dept-field">
<label>${ASSET_SCHEMA.PREV_DEPT.ui}</label>
<select id="hw-previous_dept" name="previous_dept">${generateOptionsHTML(ORG_LIST)}</select>
</div>
<div class="form-group">
<label>${ASSET_SCHEMA.MANAGER_MAIN.ui}</label>
<input type="text" id="hw-manager_primary" name="manager_primary" />
</div>
<div class="form-group">
<label>${ASSET_SCHEMA.MANAGER_SUB.ui}</label>
<input type="text" id="hw-manager_secondary" name="manager_secondary" />
</div>
<div class="form-group pc-only">
<label for="hw-사용자">${ASSET_SCHEMA.USER.ui}</label>
<input type="text" id="hw-사용자" />
<label>${ASSET_SCHEMA.CURRENT_USER.ui}</label>
<input type="text" id="hw-current_user" name="current_user" />
</div>
<div class="form-group pc-only">
<label>${ASSET_SCHEMA.PREV_USER.ui}</label>
<input type="text" id="hw-previous_user" name="previous_user" />
</div>
<div class="form-group full-width server-only">
<label>${ASSET_SCHEMA.ASSET_PURPOSE.ui}</label>
<input type="text" id="hw-asset_purpose" name="asset_purpose" placeholder="예: DB서버, 웹서버, 백업용 등" />
</div>
<!-- Group 2: 설치 위치 -->
<div class="form-section-title">설치 위치</div>
<div class="form-group">
<label>건물/위치</label>
<select id="hw-bldg-select">${generateOptionsHTML(Object.keys(LOCATION_DATA))}</select>
</div>
<div class="form-group">
<label for="hw-현사용조직">${ASSET_SCHEMA.ORG.ui}</label>
<select id="hw-현사용조직">${generateOptionsHTML(ORG_LIST)}</select>
<label>상세 위치(층/구역)</label>
<select id="hw-floor-select"><option value="">선택</option></select>
</div>
<div class="form-group" id="hw-이전사용조직-group">
<label for="hw-이전사용조직">${ASSET_SCHEMA.PREV_ORG.ui}</label>
<input type="text" id="hw-이전사용조직" readonly />
<div class="form-group full-width" id="hw-loc-etc-group" style="display:none;">
<label>기타 상세 위치</label>
<input type="text" id="hw-loc-etc" placeholder="직접 입력" />
</div>
<div class="form-group" id="hw-유형-group">
<label for="hw-유형">유형</label>
<select id="hw-유형">${generateOptionsHTML(HW_TYPE_LIST)}</select>
<!-- Group 3: 시스템 사양 -->
<div class="form-section-title">시스템 사양</div>
<div class="form-group full-width">
<label>${ASSET_SCHEMA.MODEL_NAME.ui}</label>
<input type="text" id="hw-model_name" name="model_name" />
</div>
<div class="form-group" id="hw-상세용도-group">
<label for="hw-상세용도">상세유형</label>
<select id="hw-상세용도">
<option value="">선택</option>
<option value="서버">서버</option>
<option value="개인PC">개인PC</option>
<div class="form-group">
<label>${ASSET_SCHEMA.CPU.ui}</label>
<input type="text" id="hw-cpu" name="cpu" />
</div>
<div class="form-group">
<label>${ASSET_SCHEMA.RAM.ui}</label>
<input type="text" id="hw-ram" name="ram" />
</div>
<div class="form-group">
<label>${ASSET_SCHEMA.GPU.ui}</label>
<input type="text" id="hw-gpu" name="gpu" />
</div>
<div class="form-group">
<label>SSD 1</label>
<input type="text" id="hw-ssd_1" name="ssd_1" />
</div>
<div class="form-group">
<label>SSD 2</label>
<input type="text" id="hw-ssd_2" name="ssd_2" />
</div>
<div class="form-group">
<label>HDD 1</label>
<input type="text" id="hw-hdd_1" name="hdd_1" />
</div>
<div class="form-group">
<label>HDD 2</label>
<input type="text" id="hw-hdd_2" name="hdd_2" />
</div>
<div class="form-group pc-only">
<label>HDD 3</label>
<input type="text" id="hw-hdd_3" name="hdd_3" />
</div>
<div class="form-group pc-only">
<label>HDD 4</label>
<input type="text" id="hw-hdd_4" name="hdd_4" />
</div>
<div class="form-group pc-only">
<label>${ASSET_SCHEMA.MAINBOARD.ui}</label>
<input type="text" id="hw-mainboard" name="mainboard" />
</div>
<div class="form-group pc-only">
<label>${ASSET_SCHEMA.MAC_ADDR.ui}</label>
<input type="text" id="hw-mac_address" name="mac_address" />
</div>
<!-- Group 4: 네트워크 및 접속 정보 -->
<div class="form-section-title">네트워크 및 접속 정보</div>
<div class="form-group">
<label>${ASSET_SCHEMA.IP_ADDR.ui}</label>
<input type="text" id="hw-ip_address" name="ip_address" />
</div>
<div class="form-group server-only">
<label>${ASSET_SCHEMA.IP_ADDR2.ui}</label>
<input type="text" id="hw-ip_address_2" name="ip_address_2" />
</div>
<div class="form-group server-only">
<label>${ASSET_SCHEMA.REMOTE_TOOL.ui}</label>
<input type="text" id="hw-remote_tool" name="remote_tool" placeholder="Anydesk, Chrome 등" />
</div>
<div class="form-group server-only">
<label>${ASSET_SCHEMA.REMOTE_ID.ui}</label>
<input type="text" id="hw-remote_id" name="remote_id" />
</div>
<div class="form-group server-only">
<label>${ASSET_SCHEMA.REMOTE_PW.ui}</label>
<input type="text" id="hw-remote_pw" name="remote_pw" />
</div>
<div class="form-group server-only">
<label>${ASSET_SCHEMA.MONITORING.ui}</label>
<select id="hw-monitoring" name="monitoring">
<option value="대상">대상</option>
<option value="비대상">비대상</option>
</select>
</div>
<div class="form-section-title op-only" id="hw-op-title">운영 및 상태 관리</div>
<div class="form-group op-only">
<label for="hw-보관위치">${ASSET_SCHEMA.STORE_LOC.ui}</label>
<input type="text" id="hw-보관위치" placeholder="예: 7층 비품창고" />
<!-- Group 5: 구매 정보 -->
<div class="form-section-title">구매 및 증빙</div>
<div class="form-group">
<label>${ASSET_SCHEMA.PURCHASE_DATE.ui}</label>
<div style="display:flex; gap:0.25rem; align-items:center; position:relative;">
<input type="text" id="hw-purchase_date" name="purchase_date" style="flex:1;" />
<button type="button" class="btn-icon btn-helper" onclick="const p = document.getElementById('hw-purchase_date-picker'); p.value = document.getElementById('hw-purchase_date').value; p.showPicker();" style="padding:0.25rem;">
<i data-lucide="calendar" style="width:18px; height:18px; color:var(--primary-color);"></i>
</button>
<input type="date" id="hw-purchase_date-picker" style="position:absolute; width:0; height:0; opacity:0; pointer-events:none;" onchange="document.getElementById('hw-purchase_date').value = this.value" tabindex="-1" />
</div>
<div class="form-group op-only">
<label for="hw-현재상태">${ASSET_SCHEMA.STATUS.ui}</label>
<select id="hw-현재상태">${generateOptionsHTML(STATUS_LIST)}</select>
</div>
<div class="form-section-title server-only" id="hw-network-title">네트워크 정보 (Connectivity)</div>
<div class="form-group server-only" id="hw-ip-group"><label for="hw-IP주소">${ASSET_SCHEMA.IP_ADDR.ui}</label><input type="text" id="hw-IP주소" /></div>
<div class="form-group server-only" id="hw-ip2-group"><label for="hw-IP2">${ASSET_SCHEMA.IP_ADDR2.ui}</label><input type="text" id="hw-IP2" /></div>
<div class="form-group server-only" id="hw-remote-group"><label for="hw-원격접속">원격 도구</label><input type="text" id="hw-원격접속" /></div>
<div class="form-group server-only" id="hw-server-id-group"><label for="hw-서버ID">서버 ID</label><input type="text" id="hw-서버ID" /></div>
<div class="form-group server-only" id="hw-server-pw-group"><label for="hw-서버PW">서버 PW</label><input type="text" id="hw-서버PW" /></div>
<div class="form-group non-server" id="hw-ip-non-server-group"><label for="hw-IP주소-non-server">${ASSET_SCHEMA.IP_ADDR.ui}</label><input type="text" id="hw-IP주소-non-server" /></div>
<div class="form-section-title" id="hw-spec-title">시스템 사양 (Specifications)</div>
<div class="form-group" id="hw-model-group"><label for="hw-모델명">${ASSET_SCHEMA.MODEL.ui}</label><input type="text" id="hw-모델명" /></div>
<div class="form-group pc-only" id="hw-mainboard-group"><label for="hw-메인보드">${ASSET_SCHEMA.MAINBOARD.ui}</label><input type="text" id="hw-메인보드" /></div>
<div class="form-group" id="hw-os-group"><label for="hw-OS">${ASSET_SCHEMA.OS.ui}</label><input type="text" id="hw-OS" /></div>
<div class="form-group" id="hw-cpu-group"><label for="hw-CPU">${ASSET_SCHEMA.CPU.ui}</label><input type="text" id="hw-CPU" /></div>
<div class="form-group" id="hw-gpu-group"><label for="hw-GPU">${ASSET_SCHEMA.GPU.ui}</label><input type="text" id="hw-GPU" /></div>
<div class="form-group" id="hw-ram-group"><label for="hw-RAM">${ASSET_SCHEMA.RAM.ui}</label><input type="text" id="hw-RAM" /></div>
<div class="form-group" id="hw-ssd1-group"><label for="hw-SSD1">${ASSET_SCHEMA.STORAGE1.ui}</label><input type="text" id="hw-SSD1" /></div>
<div class="form-group" id="hw-ssd2-group"><label for="hw-SSD2">${ASSET_SCHEMA.STORAGE2.ui}</label><input type="text" id="hw-SSD2" /></div>
<div class="form-group" id="hw-ssd3-group"><label for="hw-SSD3">${ASSET_SCHEMA.STORAGE3.ui}</label><input type="text" id="hw-SSD3" /></div>
<div class="form-group server-only" id="hw-monitoring-group"><label for="hw-모니터링">모니터링 여부</label><input type="text" id="hw-모니터링" /></div>
<div class="form-group full-width non-server" id="hw-hwspec-group"><label for="hw-HW사양">사양 상세</label><textarea id="hw-HW사양" rows="2"></textarea></div>
<div class="form-section-title" id="hw-loc-title">설치 위치 및 관리</div>
<div class="form-group loc-standard"><label for="hw-위치-빌딩">설치위치 (건물)</label><select id="hw-위치-빌딩">${generateOptionsHTML(Object.keys(LOCATION_DATA))}</select></div>
<div class="form-group loc-standard"><label for="hw-위치-상세">상세 위치</label><select id="hw-위치-상세"><option value="">선택</option></select></div>
<div class="form-group" id="hw-위치-기타-group" style="display:none;"><label for="hw-위치-기타">직접 입력 (기타)</label><input type="text" id="hw-위치-기타" /></div>
<div class="form-group"><label for="hw-담당자_정">${ASSET_SCHEMA.MANAGER_MAIN.ui}</label><input type="text" id="hw-담당자_정" /></div>
<div class="form-group"><label for="hw-담당자_부">${ASSET_SCHEMA.MANAGER_SUB.ui}</label><input type="text" id="hw-담당자_부" /></div>
<div class="form-group"><label for="hw-구매일">${ASSET_SCHEMA.PURCHASE_YM.ui}</label><input type="text" id="hw-구매일" placeholder="YYYYMM" maxlength="6" /></div>
<div class="form-group"><label for="hw-금액">${ASSET_SCHEMA.PRICE.ui}</label><input type="text" id="hw-금액" oninput="this.value=this.value.replace(/[^0-9]/g,'').replace(/\\\\B(?=(\\\\d{3})+(?!\\\\d))/g,',')" /></div>
<div class="form-group" id="hw-vendor-group"><label for="hw-납품업체">${ASSET_SCHEMA.VENDOR.ui}</label><input type="text" id="hw-납품업체" /></div>
<div class="form-group full-width"><label for="hw-비고">${ASSET_SCHEMA.REMARKS.ui}</label><textarea id="hw-비고" rows="2"></textarea></div>
<div class="form-group">
<label>${ASSET_SCHEMA.PURCHASE_VENDOR.ui}</label>
<input type="text" id="hw-purchase_vendor" name="purchase_vendor" />
</div>
<div class="form-group">
<label>${ASSET_SCHEMA.PURCHASE_AMOUNT.ui}</label>
<input type="text" id="hw-purchase_amount" name="purchase_amount" oninput="this.value = this.value.replace(/[^0-9]/g, '').replace(/\\B(?=(\\d{3})+(?!\\d))/g, ',')" />
</div>
<div class="form-group full-width">
<label>${ASSET_SCHEMA.DOC_NAME.ui} (파일 증빙)</label>
<label>${ASSET_SCHEMA.APPROVAL_DOC.ui}</label>
<div style="display:flex; align-items:center; gap:0.5rem;">
<input type="file" id="hw-품의서" />
<span id="hw-품의서명" style="font-size:0.75rem; color:var(--text-light)"></span>
<input type="file" id="hw-approval_document_file" style="font-size:12px;" />
<span id="hw-approval_document_name" style="font-size:12px; color:var(--text-muted);"></span>
</div>
</div>
<div class="form-group full-width">
<label>${ASSET_SCHEMA.MEMO.ui}</label>
<textarea id="hw-memo" name="memo" rows="2"></textarea>
</div>
</form>
</div>
<div class="modal-history-area">
<div class="history-header">
<h3><i data-lucide="history" style="width:16px; height:16px;"></i> 자산 변동 이력</h3>
<button type="button" id="btn-add-hw-log" class="btn btn-outline btn-sm">
이력 추가 <i data-lucide="plus" style="width:14px; height:14px;"></i>
</button>
</div>
<div id="hw-history-list" class="history-timeline"></div>
</div>
</div>
</div>
<div class="modal-footer">
<button id="btn-delete-hw-asset" class="btn btn-outline btn-danger">삭제</button>
<div class="footer-actions">
<button id="btn-revert-hw-edit" class="btn btn-outline hidden">수정 취소</button>
<button id="btn-cancel-hw-modal" class="btn btn-outline">닫기</button>
<button id="btn-save-hw-asset" class="btn btn-primary">수정</button>
</div>
</div>
</div>
</div>
`;
function renderHwHistory(assetId: string) {
const container = document.getElementById('hw-history-list');
if (!container) return;
const logs = (state.masterData.logs || []).filter(l => l.assetId === assetId).sort((a,b) => new Date(b.date).getTime() - new Date(a.date).getTime());
if (logs.length === 0) { container.innerHTML = '<div class="empty-history">기록된 이력이 없습니다.</div>'; return; }
container.innerHTML = logs.map(l => `
<div class="history-item">
<div class="history-date">${l.date}</div>
<div class="history-user">${l.user}</div>
<div class="history-details">${l.details.replace(/\n/g, '<br>')}</div>
</div>
`).join('');
}
function applyTypeSpecificUI(type: string) {
const detailPurpose = getFieldValue('hw-상세용도');
const upperType = (type || '').toUpperCase();
const groups: Record<string, HTMLElement | null> = {
detailPurpose: document.getElementById('hw-상세용도-group'),
networkTitle: document.getElementById('hw-network-title'),
specTitle: document.getElementById('hw-spec-title'),
opTitle: document.getElementById('hw-op-title'),
model: document.getElementById('hw-model-group'),
mainboard: document.getElementById('hw-mainboard-group'),
os: document.getElementById('hw-os-group'),
cpu: document.getElementById('hw-cpu-group'),
ram: document.getElementById('hw-ram-group'),
gpu: document.getElementById('hw-gpu-group'),
ssd1: document.getElementById('hw-ssd1-group'),
ssd2: document.getElementById('hw-ssd2-group'),
ssd3: document.getElementById('hw-ssd3-group'),
hwSpec: document.getElementById('hw-hwspec-group'),
monitoring: document.getElementById('hw-monitoring-group'),
vendor: document.getElementById('hw-vendor-group'),
user: document.querySelector('.pc-only') as HTMLElement
};
const serverOnly = document.querySelectorAll('.server-only');
const nonServer = document.querySelectorAll('.non-server');
const opOnly = document.querySelectorAll('.op-only');
const standardLoc = document.querySelectorAll('.loc-standard');
serverOnly.forEach(el => (el as HTMLElement).style.display = 'none');
nonServer.forEach(el => (el as HTMLElement).style.display = 'none');
opOnly.forEach(el => (el as HTMLElement).style.display = 'none');
standardLoc.forEach(el => (el as HTMLElement).style.display = 'flex');
Object.values(groups).forEach(g => { if (g) g.style.display = 'none'; });
const osLabel = document.querySelector('label[for="hw-OS"]') as HTMLElement;
const ramLabel = document.querySelector('label[for="hw-RAM"]') as HTMLElement;
const modelLabel = document.querySelector('label[for="hw-모델명"]') as HTMLElement;
if (osLabel) osLabel.innerText = ASSET_SCHEMA.OS.ui;
if (ramLabel) ramLabel.innerText = ASSET_SCHEMA.RAM.ui;
if (modelLabel) modelLabel.innerText = ASSET_SCHEMA.MODEL.ui;
const isMobileGroup = ['모바일', '태블릿', '휴대폰'].some(t => upperType.includes(t));
const isEquipGroup = ['CPU', 'RAM', 'HDD', 'GPU'].some(t => upperType.includes(t)) || upperType.includes('비품');
const isOpType = isMobileGroup || isEquipGroup;
const isPcType = upperType === 'PC' || upperType === '개인PC' || upperType === '노트북';
if (groups.opTitle) groups.opTitle.style.display = isOpType ? 'flex' : 'none';
if (isOpType) {
opOnly.forEach(el => (el as HTMLElement).style.display = 'flex');
standardLoc.forEach(el => (el as HTMLElement).style.display = 'none');
if (groups.specTitle) groups.specTitle.style.display = 'flex';
if (groups.model) groups.model.style.display = 'flex';
if (['CPU', 'GPU'].some(t => upperType.includes(t))) {
if (groups.os && osLabel) { osLabel.innerText = '출시연월'; groups.os.style.display = 'flex'; }
} else if (['RAM', 'HDD'].some(t => upperType.includes(t))) {
if (groups.ram && ramLabel) { ramLabel.innerText = '용량'; groups.ram.style.display = 'flex'; }
} else {
if (groups.hwSpec) groups.hwSpec.style.display = 'flex';
}
}
else if (isPcType) {
if (groups.user) groups.user.style.display = 'flex';
if (groups.specTitle) groups.specTitle.style.display = 'flex';
if (groups.mainboard) groups.mainboard.style.display = 'flex';
if (upperType === '노트북') {
if (groups.detailPurpose) groups.detailPurpose.style.display = 'none';
nonServer.forEach(el => (el as HTMLElement).style.display = 'flex');
['model', 'os', 'cpu', 'gpu', 'ram', 'ssd1', 'ssd2', 'ssd3', 'hwSpec', 'vendor'].forEach(k => { if (groups[k]) groups[k]!.style.display = 'flex'; });
} else {
if (groups.detailPurpose) groups.detailPurpose.style.display = 'flex';
if (detailPurpose === '서버') {
serverOnly.forEach(el => (el as HTMLElement).style.display = 'flex');
if (groups.networkTitle) groups.networkTitle.style.display = 'flex';
['model', 'os', 'cpu', 'gpu', 'ram', 'ssd1', 'ssd2', 'ssd3', 'monitoring', 'vendor'].forEach(k => { if (groups[k]) groups[k]!.style.display = 'flex'; });
} else {
nonServer.forEach(el => (el as HTMLElement).style.display = 'flex');
['model', 'os', 'cpu', 'gpu', 'ram', 'ssd1', 'ssd2', 'ssd3', 'hwSpec', 'vendor'].forEach(k => { if (groups[k]) groups[k]!.style.display = 'flex'; });
}
}
}
else {
serverOnly.forEach(el => (el as HTMLElement).style.display = 'flex');
if (groups.networkTitle) groups.networkTitle.style.display = 'flex';
if (groups.specTitle) groups.specTitle.style.display = 'flex';
['model', 'os', 'cpu', 'gpu', 'ram', 'ssd1', 'ssd2', 'ssd3', 'monitoring', 'vendor'].forEach(k => { if (groups[k]) groups[k]!.style.display = 'flex'; });
}
}
export function openHwModal(asset: HardwareAsset, mode: 'view' | 'add' = 'view') {
currentAsset = asset;
const modal = document.getElementById('hw-asset-modal')!;
setEditLock('hw-asset-form', mode, {
saveBtnId: 'btn-save-hw-asset',
revertBtnId: 'btn-revert-hw-edit',
generateBtnId: 'btn-generate-hw-code',
addLogBtnId: 'btn-add-hw-log'
});
isEditMode = (mode === 'add');
autoFillForm('hw', asset, HW_FIELD_MAP);
setFieldValue('hw-명칭', asset. || asset[ASSET_SCHEMA.MODEL.key]);
if (!asset[ASSET_SCHEMA.PURCHASE_YM.key] && asset.) setFieldValue('hw-구매일', asset.);
parseAndSetLocation(asset[ASSET_SCHEMA.LOCATION.key], 'hw-위치-빌딩', 'hw-위치-상세', 'hw-위치-기타-group', 'hw-위치-기타');
applyTypeSpecificUI(asset.type);
renderHwHistory(asset.id);
modal.classList.remove('hidden');
createIcons({ icons: { X, Save, Edit2, RotateCcw, History, Plus, Paperclip } });
}
export function initHwModal(onSave: () => void, closeModalsCb: () => void) {
export function initHwModal(onSave: () => void, closeModals: () => void) {
if (!document.getElementById('hw-asset-modal')) {
const html = createModalFrameHTML('hw', '자산 상세 정보', HW_FORM_HTML, {
historyTitle: '분출 및 변경 이력',
addLogBtnId: 'btn-add-hw-log'
});
document.body.insertAdjacentHTML('beforeend', html);
const logModalHTML = `
<div id="hw-log-modal" class="modal-overlay hidden" style="z-index: 1100;">
<div class="modal-content" style="max-width: 400px;">
<div class="modal-header"><h2>${UI_TEXT.ACTION.HISTORY_ADD}</h2><button id="btn-close-hw-log" class="btn-icon"><i data-lucide="x"></i></button></div>
<div class="modal-body">
<div class="grid-form" style="grid-template-columns: 1fr;">
<div class="form-group"><label>날짜</label><input type="date" id="new-hw-log-date" /></div>
<div class="form-group"><label>변경/분출 내용</label><textarea id="new-hw-log-details" rows="3" placeholder="예: [분출] 기술팀 홍길동, [수리] 배터리 교체 등"></textarea></div>
</div>
</div>
<div class="modal-footer"><div></div><div class="footer-actions"><button id="btn-cancel-hw-log" class="btn btn-outline">${UI_TEXT.ACTION.CANCEL}</button><button id="btn-confirm-hw-log" class="btn btn-primary">추가</button></div></div>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', logModalHTML);
document.body.insertAdjacentHTML('beforeend', HW_MODAL_HTML);
}
const form = document.getElementById('hw-asset-form') as HTMLFormElement;
const saveBtn = document.getElementById('btn-save-hw-asset')!;
const revertBtn = document.getElementById('btn-revert-hw-edit')!;
const deleteBtn = document.getElementById('btn-delete-hw-asset')!;
const typeSelect = document.getElementById('hw-유형') as HTMLSelectElement;
const detailPurposeSelect = document.getElementById('hw-상세용도') as HTMLSelectElement;
const logAddBtn = document.getElementById('btn-add-hw-log')!;
const logModal = document.getElementById('hw-log-modal')!;
const btnCloseHeader = document.getElementById('btn-close-hw-modal')!;
const btnCancelFooter = document.getElementById('btn-cancel-hw-modal')!;
[typeSelect, detailPurposeSelect].forEach(el => {
el?.addEventListener('change', () => applyTypeSpecificUI(typeSelect.value));
});
bindLocationEvents('hw-bldg-select', 'hw-floor-select', 'hw-loc-etc-group', 'hw-loc-etc');
applyDateMask(document.getElementById('hw-purchase_date') as HTMLInputElement);
bindLocationEvents('hw-위치-빌딩', 'hw-위치-상세', 'hw-위치-기타-group', 'hw-위치-기타');
const closeModalAction = () => { closeModalsCb(); isEditMode = false; };
document.getElementById('btn-close-hw-modal')?.addEventListener('click', closeModalAction);
document.getElementById('btn-cancel-hw-modal')?.addEventListener('click', closeModalAction);
const closeModalAction = () => { closeModals(); isEditMode = false; };
btnCloseHeader.addEventListener('click', closeModalAction);
btnCancelFooter.addEventListener('click', closeModalAction);
revertBtn.addEventListener('click', () => {
setEditLock('hw-asset-form', 'view', {
saveBtnId: 'btn-save-hw-asset',
revertBtnId: 'btn-revert-hw-edit',
generateBtnId: 'btn-generate-hw-code',
addLogBtnId: 'btn-add-hw-log'
});
setEditLock('hw-asset-form', 'view', { saveBtnId: 'btn-save-hw-asset', revertBtnId: 'btn-revert-hw-edit' });
isEditMode = false;
if (currentAsset) openHwModal(currentAsset, 'view');
if (currentHwAsset) fillHwFormData(currentHwAsset);
});
document.getElementById('btn-generate-hw-code')?.addEventListener('click', async () => {
const typeValue = typeSelect.value;
const purchaseDate = getFieldValue('hw-구매일');
const typeCode = TYPE_PREFIX_MAP[typeValue] || 'ETC';
const dateStr = purchaseDate.replace(/[^0-9]/g, '');
if (dateStr.length < 6) { alert('올바른 구매연월(YYYYMM)을 입력해주세요.'); return; }
const prefix = `${typeCode}-${dateStr.substring(0, 6)}-`;
try {
const res = await fetch(`http://172.16.40.100:3000/api/generate-asset-code?prefix=${prefix}`);
const data = await res.json();
if (data.nextCode) setFieldValue('hw-자산코드', data.nextCode);
} catch (err) { alert('자산번호 생성에 실패했습니다.'); }
});
['hw-구매일', 'hw-OS'].forEach(id => {
const el = document.getElementById(id) as HTMLInputElement;
el?.addEventListener('input', (e) => {
const target = e.target as HTMLInputElement;
const label = document.querySelector(`label[for="${id}"]`) as HTMLElement;
if (id === 'hw-OS' && label?.innerText !== '출시연월') return;
target.value = target.value.replace(/[^0-9]/g, '').substring(0, 6);
});
});
saveBtn.addEventListener('click', () => {
if (!currentAsset) return;
saveBtn.addEventListener('click', async () => {
if (!currentHwAsset) return;
if (!isEditMode) {
setEditLock('hw-asset-form', 'edit', {
saveBtnId: 'btn-save-hw-asset',
revertBtnId: 'btn-revert-hw-edit',
generateBtnId: 'btn-generate-hw-code',
addLogBtnId: 'btn-add-hw-log'
});
setEditLock('hw-asset-form', 'edit', { saveBtnId: 'btn-save-hw-asset', revertBtnId: 'btn-revert-hw-edit' });
isEditMode = true;
applyTypeSpecificUI(getFieldValue('hw-유형'));
return;
}
const extracted = autoExtractForm('hw', HW_FIELD_MAP);
if (!extracted[ASSET_SCHEMA.ASSET_CODE.key]) {
alert('자산번호가 없습니다. [생성] 버튼을 눌러 자산번호를 먼저 부여해주세요.');
return;
}
const upperType = (extracted.type || '').toUpperCase();
const isOpType = ['CPU', 'RAM', 'HDD', 'GPU'].some(t => upperType.includes(t)) || upperType.includes('비품') || ['모바일', '태블릿', '휴대폰'].some(t => upperType.includes(t));
if (HW_TYPE_LIST.includes(extracted.type) || extracted.type === '개인PC') {
const diffLogs: string[] = [];
const compareFields = [
{ key: ASSET_SCHEMA.ORG.key, label: ASSET_SCHEMA.ORG.ui },
{ key: ASSET_SCHEMA.LOCATION.key, label: ASSET_SCHEMA.LOCATION.ui },
{ key: ASSET_SCHEMA.MANAGER_MAIN.key, label: '담당자' },
{ key: ASSET_SCHEMA.STATUS.key, label: ASSET_SCHEMA.STATUS.ui },
{ key: ASSET_SCHEMA.IP_ADDR.key, label: ASSET_SCHEMA.IP_ADDR.ui },
{ key: '상세용도', label: '상세유형' },
{ key: ASSET_SCHEMA.MODEL.key, label: ASSET_SCHEMA.MODEL.ui }
];
if (!currentAsset || !currentAsset.) {
diffLogs.push('자산 신규 등록');
} else {
const asset = currentAsset!;
const newIp = String(getFieldValue('hw-IP주소') || getFieldValue('hw-IP주소-non-server') || '').trim();
const newLocation = String(isOpType ? extracted[ASSET_SCHEMA.STORE_LOC.key] : getCombinedLocation('hw-위치-빌딩', 'hw-위치-상세', 'hw-위치-기타') || '').trim();
compareFields.forEach(f => {
let oldVal = '';
let newVal = '';
if (f.key === ASSET_SCHEMA.IP_ADDR.key) {
oldVal = String(asset[ASSET_SCHEMA.IP_ADDR.key] || '').trim();
newVal = newIp;
} else if (f.key === ASSET_SCHEMA.LOCATION.key) {
oldVal = String(asset[ASSET_SCHEMA.LOCATION.key] || '').trim();
newVal = newLocation;
} else if (f.key === ASSET_SCHEMA.MANAGER_MAIN.key) {
oldVal = String(asset[ASSET_SCHEMA.MANAGER_MAIN.key] || '').trim();
newVal = String(extracted[ASSET_SCHEMA.MANAGER_MAIN.key] || '').trim();
} else if (f.key === '상세용도') {
oldVal = String(asset. || '').trim();
newVal = String((extracted.type !== 'PC' && extracted.type !== '개인PC') ? extracted.type : (extracted. || '')).trim();
} else {
oldVal = String((asset as any)[f.key] || '').trim();
newVal = String(extracted[f.key] || '').trim();
}
if (oldVal !== newVal) {
diffLogs.push(`${f.label}: ${oldVal || '(없음)'}${newVal || '(없음)'}`);
}
});
}
if (diffLogs.length > 0) {
state.masterData.logs = state.masterData.logs || [];
state.masterData.logs.push({
id: Math.random().toString(36).substring(2, 9),
assetId: currentAsset.id,
date: new Date().toISOString().split('T')[0],
user: '담당자',
details: diffLogs.join('\n')
});
}
}
const updated: any = {
...currentAsset,
...extracted,
[ASSET_SCHEMA.IP_ADDR.key]: getFieldValue('hw-IP주소') || getFieldValue('hw-IP주소-non-server'),
위치: isOpType ? extracted[ASSET_SCHEMA.STORE_LOC.key] : getCombinedLocation('hw-위치-빌딩', 'hw-위치-상세', 'hw-위치-기타')
};
if (currentAsset[ASSET_SCHEMA.ORG.key] && currentAsset[ASSET_SCHEMA.ORG.key] !== extracted[ASSET_SCHEMA.ORG.key]) {
updated[ASSET_SCHEMA.PREV_ORG.key] = currentAsset[ASSET_SCHEMA.ORG.key];
}
if (updated.type !== 'PC') { updated. = updated.type; }
saveHardwareAsset(updated);
onSave();
setEditLock('hw-asset-form', 'view', { saveBtnId: 'btn-save-hw-asset', revertBtnId: 'btn-revert-hw-edit', generateBtnId: 'btn-generate-hw-code', addLogBtnId: 'btn-add-hw-log' });
isEditMode = false;
const formData = new FormData(form);
const updated: any = { ...currentHwAsset };
formData.forEach((value, key) => {
if (key !== 'id') updated[key] = value;
});
deleteBtn.addEventListener('click', () => {
if (currentAsset && confirm(UI_TEXT.MESSAGES.CONFIRM_DELETE)) {
deleteHardwareAsset(currentAsset.id);
// Handle combined location
updated.location = getCombinedLocation('hw-bldg-select', 'hw-floor-select', 'hw-loc-etc');
let categoryKey = 'pc';
if (updated.asset_code?.startsWith('SVR')) categoryKey = 'server';
else if (updated.asset_code?.startsWith('STO')) categoryKey = 'storage';
else if (updated.asset_code?.startsWith('EQP')) categoryKey = 'equipment';
const success = await saveAsset(categoryKey, updated);
if (success) {
alert(UI_TEXT.MESSAGES.SAVE_SUCCESS);
onSave();
closeModalAction();
}
});
logAddBtn.addEventListener('click', () => {
logModal.classList.remove('hidden');
(document.getElementById('new-hw-log-date') as HTMLInputElement).value = new Date().toISOString().split('T')[0];
(document.getElementById('new-hw-log-details') as HTMLTextAreaElement).value = '';
createIcons({ icons: { X, History, Plus, Save, Paperclip, Calendar, Monitor, Cpu, Network, ShieldCheck } });
}
export function openHwModal(asset: any, mode: 'view' | 'edit' | 'add' = 'view') {
currentHwAsset = asset;
const modal = document.getElementById('hw-asset-modal')!;
setEditLock('hw-asset-form', mode, {
saveBtnId: 'btn-save-hw-asset',
revertBtnId: 'btn-revert-hw-edit',
addLogBtnId: 'btn-add-hw-log'
});
document.getElementById('btn-close-hw-log')?.addEventListener('click', () => logModal.classList.add('hidden'));
document.getElementById('btn-cancel-hw-log')?.addEventListener('click', () => logModal.classList.add('hidden'));
document.getElementById('btn-confirm-hw-log')?.addEventListener('click', () => {
if (!currentAsset) return;
const date = (document.getElementById('new-hw-log-date') as HTMLInputElement).value;
const details = (document.getElementById('new-hw-log-details') as HTMLTextAreaElement).value;
if (!date || !details) return;
state.masterData.logs = state.masterData.logs || [];
state.masterData.logs.push({ id: Math.random().toString(36).substring(2, 9), assetId: currentAsset.id, date, user: '담당자', details });
logModal.classList.add('hidden');
renderHwHistory(currentAsset.id);
});
isEditMode = (mode === 'add' || mode === 'edit');
fillHwFormData(asset);
// Show/Hide category specific fields
const serverOnly = document.querySelectorAll('.server-only');
const nonServer = document.querySelectorAll('.non-server');
const pcOnly = document.querySelectorAll('.pc-only');
const isServer = asset.category === '서버' || asset.asset_code?.startsWith('SVR');
const isPc = asset.category === 'PC' || asset.asset_code?.startsWith('PC');
serverOnly.forEach(el => (el as HTMLElement).style.display = isServer ? 'flex' : 'none');
nonServer.forEach(el => (el as HTMLElement).style.display = !isServer ? 'flex' : 'none');
pcOnly.forEach(el => (el as HTMLElement).style.display = isPc ? 'flex' : 'none');
modal.classList.remove('hidden');
}
function fillHwFormData(asset: any) {
setFieldValue('hw-id', asset.id);
setFieldValue('hw-asset_code', asset.asset_code || '');
setFieldValue('hw-purchase_corp', asset.purchase_corp || '');
setFieldValue('hw-category', asset.category || '');
setFieldValue('hw-hw_status', asset.hw_status || '운영');
setFieldValue('hw-current_dept', asset.current_dept || '');
setFieldValue('hw-previous_dept', asset.previous_dept || '');
setFieldValue('hw-manager_primary', asset.manager_primary || '');
setFieldValue('hw-manager_secondary', asset.manager_secondary || '');
setFieldValue('hw-current_user', asset.current_user || '');
setFieldValue('hw-previous_user', asset.previous_user || '');
setFieldValue('hw-asset_purpose', asset.asset_purpose || '');
setFieldValue('hw-model_name', asset.model_name || '');
setFieldValue('hw-cpu', asset.cpu || '');
setFieldValue('hw-ram', asset.ram || '');
setFieldValue('hw-gpu', asset.gpu || '');
setFieldValue('hw-ssd_1', asset.ssd_1 || '');
setFieldValue('hw-ssd_2', asset.ssd_2 || '');
setFieldValue('hw-hdd_1', asset.hdd_1 || '');
setFieldValue('hw-hdd_2', asset.hdd_2 || '');
setFieldValue('hw-hdd_3', asset.hdd_3 || '');
setFieldValue('hw-hdd_4', asset.hdd_4 || '');
setFieldValue('hw-mainboard', asset.mainboard || '');
setFieldValue('hw-mac_address', asset.mac_address || '');
setFieldValue('hw-ip_address', asset.ip_address || '');
setFieldValue('hw-ip_address_2', asset.ip_address_2 || '');
setFieldValue('hw-remote_tool', asset.remote_tool || '');
setFieldValue('hw-remote_id', asset.remote_id || '');
setFieldValue('hw-remote_pw', asset.remote_pw || '');
setFieldValue('hw-monitoring', asset.monitoring || '비대상');
setFieldValue('hw-purchase_date', asset.purchase_date || '');
setFieldValue('hw-purchase_vendor', asset.purchase_vendor || '');
setFieldValue('hw-purchase_amount', asset.purchase_amount || '');
(document.getElementById('hw-approval_document_name') as HTMLElement).textContent = asset.approval_document || '';
setFieldValue('hw-memo', asset.memo || '');
parseAndSetLocation(asset.location || '', 'hw-bldg-select', 'hw-floor-select', 'hw-loc-etc-group', 'hw-loc-etc');
renderHwHistory(asset.id);
}
function renderHwHistory(assetId: string) {
const container = document.getElementById('hw-history-list');
if (!container) return;
const logs = (state.masterData.logs || []).filter(l => l.assetId === assetId);
if (logs.length === 0) {
container.innerHTML = '<div class="empty-history">이력이 없습니다.</div>';
return;
}
container.innerHTML = logs.map(l => `
<div class="history-item">
<div class="history-date">${l.date}</div>
<div class="history-user">${l.user}</div>
<div class="history-details">${l.details}</div>
</div>
`).join('');
}

View File

@@ -3,21 +3,28 @@ import { state } from '../core/state';
const MENU_CONFIG = {
hw: {
label: '하드웨어',
tabs: ['대시보드', '서버', '개인PC', '모바일기기', '스토리지', '전산비품']
tabs: ['서버', 'PC', '스토리지', '공간정보장비', 'PC부품', '네트워크', '업무지원장비']
},
sw: {
label: '소프트웨어',
tabs: ['대시보드', '구독SW', '영구SW']
tabs: ['외부', '내부']
},
ops: {
label: '운영 서비스',
tabs: ['도메인', '메일', '메신저', '청구비용']
label: '운영지원',
tabs: ['클라우드', '도메인', '비용관리']
},
vip: {
label: '내빈/외빈',
tabs: ['선물']
},
fac: {
label: '시설자산',
tabs: ['사무가구']
}
};
export function renderNavigation(onTabChange: (tab: string) => void) {
const navContainer = document.getElementById('main-nav')!;
const btnAddAsset = document.getElementById('btn-add-asset') as HTMLButtonElement;
const render = () => {
navContainer.innerHTML = '';
@@ -37,10 +44,10 @@ export function renderNavigation(onTabChange: (tab: string) => void) {
trigger.addEventListener('click', () => {
if (state.activeCategory !== catKey) {
state.activeCategory = catKey;
state.activeSubTab = '대시보드';
if (btnAddAsset) btnAddAsset.classList.remove('hidden');
const firstTab = config.tabs[0];
state.activeSubTab = firstTab;
render();
onTabChange('대시보드');
onTabChange(firstTab);
}
});
group.appendChild(trigger);
@@ -57,7 +64,6 @@ export function renderNavigation(onTabChange: (tab: string) => void) {
e.stopPropagation();
state.activeCategory = catKey;
state.activeSubTab = tab;
if (btnAddAsset) btnAddAsset.classList.remove('hidden');
render();
onTabChange(tab);
});

View File

@@ -9,50 +9,65 @@
export const ASSET_SCHEMA = {
// ─── 공통 필드 (Common) ───
ID: { key: 'id', db: 'id', ui: 'ID' },
TYPE: { key: 'type', db: 'type', ui: '자산유형' },
CORP: { key: '법인', db: 'corp', ui: '구매법인' },
ASSET_CODE: { key: '자산코드', db: 'asset_code', ui: '자산번호' },
PURCHASE_YM: { key: '구매연월', db: 'purchase_date', ui: '구매연월' },
ORG: { key: '현사용조직', db: 'current_org', ui: '현 사용조직' },
PREV_ORG: { key: '이전사용조직', db: 'prev_org', ui: '이전 사용조직' },
LOCATION: { key: '위치', db: 'location', ui: '설치위치' },
MANAGER_MAIN: { key: '담당자_정', db: 'manager_main', ui: '담당자' },
MANAGER_SUB: { key: '담당자_부', db: 'manager_sub', ui: '담당자()' },
PRICE: { key: '금액', db: 'price', ui: '도입금액' },
VENDOR: { key: '납품업체', db: 'vendor', ui: '납품업체' },
DOC_NAME: { key: '품의서명', db: 'doc_name', ui: '품의서' },
REMARKS: { key: '비고', db: 'remarks', ui: '비고' },
DETAIL_PURPOSE: { key: '상세용도', db: 'detail_purpose', ui: '용도' },
ASSET_CODE: { key: 'asset_code', db: 'asset_code', ui: '자산번호' },
CATEGORY: { key: 'category', db: 'category', ui: '구' },
ASSET_TYPE: { key: 'asset_type', db: 'asset_type', ui: '유형' },
PURCHASE_CORP: { key: 'purchase_corp',db: 'purchase_corp', ui: '구매법인' },
PURCHASE_DATE: { key: 'purchase_date',db: 'purchase_date', ui: '구매일자' },
PURCHASE_AMOUNT:{ key: 'purchase_amount', db: 'purchase_amount', ui: '구매금액' },
PURCHASE_VENDOR:{ key: 'purchase_vendor', db: 'purchase_vendor', ui: '구매업체' },
APPROVAL_DOC: { key: 'approval_document', db: 'approval_document', ui: '품의서' },
MANAGER_MAIN: { key: 'manager_primary', db: 'manager_primary', ui: '담당자()' },
MANAGER_SUB: { key: 'manager_secondary', db: 'manager_secondary', ui: '담당자(부)' },
LOCATION: { key: 'location', db: 'location', ui: '설치위치' },
LOC_DETAIL: { key: 'location_detail', db: 'location_detail', ui: '상세위치' },
MEMO: { key: 'memo', db: 'memo', ui: '메모' },
// ─── 하드웨어 상세 (Hardware) ───
USER: { key: '사용자', db: 'purpose', ui: '사용자' },
MODEL: { key: '모델명', db: 'model_name', ui: '모델명' },
MAINBOARD: { key: '메인보드', db: 'mainboard', ui: '메인보드' },
OS: { key: 'OS', db: 'os', ui: '운영체제' },
CPU: { key: 'CPU', db: 'cpu', ui: 'CPU' },
RAM: { key: 'RAM', db: 'ram', ui: 'RAM' },
STORAGE1: { key: 'SSD1', db: 'storage1', ui: 'Storage 1' },
STORAGE2: { key: 'SSD2', db: 'storage2', ui: 'Storage 2' },
IP_ADDR: { key: 'IP주소', db: 'ip_address', ui: 'IP 주소 1' },
IP_ADDR2: { key: 'IP2', db: 'ip2', ui: 'IP 주소 2' },
MAC_ADDR: { key: 'MACaddress', db: 'mac_address', ui: 'MAC 주소' },
GPU: { key: 'GPU', db: 'gpu', ui: 'GPU' },
STORAGE3: { key: 'SSD3', db: 'storage3', ui: 'Storage 3' },
STATUS: { key: '현재상태', db: 'status', ui: '현재상태' },
STORE_LOC: { key: '보관위치', db: 'storage_location',ui: '보관위치' },
HW_STATUS: { key: 'hw_status', db: 'hw_status', ui: '상태' },
MODEL_NAME: { key: 'model_name', db: 'model_name', ui: '모델명' },
ASSET_NAME: { key: 'asset_name', db: 'asset_name', ui: '모델명' },
ASSET_MFR: { key: 'asset_mfr', db: 'asset_mfr', ui: '제조사' },
CURRENT_DEPT: { key: 'current_dept', db: 'current_dept', ui: '현 사용조직' },
PREV_DEPT: { key: 'previous_dept',db: 'previous_dept', ui: '직전 사용조직' },
CURRENT_USER: { key: 'current_user', db: 'current_user', ui: '현 사용자' },
PREV_USER: { key: 'previous_user',db: 'previous_user', ui: '직전 사용자' },
CPU: { key: 'cpu', db: 'cpu', ui: 'CPU' },
RAM: { key: 'ram', db: 'ram', ui: 'RAM' },
GPU: { key: 'gpu', db: 'gpu', ui: 'GPU' },
SSD1: { key: 'ssd_1', db: 'ssd_1', ui: 'Storage 1' },
SSD2: { key: 'ssd_2', db: 'ssd_2', ui: 'Storage 2' },
HDD1: { key: 'hdd_1', db: 'hdd_1', ui: 'HDD 1' },
HDD2: { key: 'hdd_2', db: 'hdd_2', ui: 'HDD 2' },
HDD3: { key: 'hdd_3', db: 'hdd_3', ui: 'HDD 3' },
HDD4: { key: 'hdd_4', db: 'hdd_4', ui: 'HDD 4' },
MAINBOARD: { key: 'mainboard', db: 'mainboard', ui: '메인보드' },
IP_ADDR: { key: 'ip_address', db: 'ip_address', ui: 'IP 주소' },
IP_ADDR2: { key: 'ip_address_2', db: 'ip_address_2', ui: 'IP 주소 2' },
MAC_ADDR: { key: 'mac_address', db: 'mac_address', ui: 'MAC 주소' },
REMOTE_TOOL: { key: 'remote_tool', db: 'remote_tool', ui: '원격도구' },
REMOTE_ID: { key: 'remote_id', db: 'remote_id', ui: '원격 ID' },
REMOTE_PW: { key: 'remote_pw', db: 'remote_pw', ui: '원격 PW' },
MONITORING: { key: 'monitoring', db: 'monitoring', ui: '모니터링' },
VOLUME: { key: 'volume', db: 'volume', ui: '용량' },
MONITOR_INCH: { key: 'monitor_inch', db: 'monitor_inch', ui: '인치' },
ASSET_COUNT: { key: 'asset_count', db: 'asset_count', ui: '수량(개수)' },
SERIAL_NUM: { key: 'serial_num', db: 'serial_num', ui: 'S/N' },
// ─── 소프트웨어/클라우드 상세 (SW/Cloud) ───
PRODUCT: { key: '제품명', db: 'product_name', ui: '제품/서비스명' },
PLATFORM: { key: '플랫폼명', db: 'platform_name', ui: '운영 플랫폼' },
LICENSE_TYPE: { key: '라이선스유형', db: 'license_type', ui: '라이선스 유형' },
LICENSE_KEY: { key: '라이선스키', db: 'license_key', ui: '라이선스 키' },
QTY: { key: '수량', db: 'quantity', ui: '보유수량' },
EXPIRY: { key: '만료일', db: 'expiry_date', ui: '만료/구독일' },
ACCOUNT: { key: '계정명', db: 'account_name', ui: '계정(이메일)' },
PAY_METHOD: { key: '결제수단', db: 'pay_method', ui: '결제수단' },
PAY_DAY: { key: '결제일', db: 'pay_day', ui: '결제일' },
CARD_NUM: { key: '연결카드번호', db: 'card_num', ui: '카드번호(뒷4자리)' },
BILLING: { key: '당월청구액', db: 'monthly_fee', ui: '당월 청구액' }
// ─── 소프트웨어/클라우드 상세 (SW/Cloud/Domain) ───
SW_STATUS: { key: 'sw_status', db: 'sw_status', ui: 'SW 상태' },
SW_FIELD: { key: 'sw_field', db: 'sw_field', ui: '분야' },
SW_TYPE: { key: 'sw_type', db: 'sw_type', ui: 'SW 유형' },
DEV_OBJ: { key: 'dev_objective',db: 'dev_objective', ui: '개발목적' },
DEV_MGR: { key: 'dev_manager', db: 'dev_manager', ui: '담당개발자' },
SALES_MGR: { key: 'sales_manager',db: 'sales_manager', ui: '영업담당자' },
PRODUCT_NAME: { key: 'product_name', db: 'product_name', ui: '제품명' },
DOMAIN_ADDR: { key: 'domain_address', db: 'domain_address',ui: '도메인주소' },
EMAIL_ACCOUNT: { key: 'email_account', db: 'email_account', ui: '관리계정' },
EMAIL_PW: { key: 'email_pw', db: 'email_pw', ui: '계정PW' },
PURCHASE_METHOD:{ key: 'purchase_method', db: 'purchase_method', ui: '결제수단' },
ASSET_PURPOSE: { key: 'asset_purpose', db: 'asset_purpose', ui: '용도' },
EXPIRY_DATE: { key: 'expiry_date', db: 'expiry_date', ui: '만료일' }
};
/**

View File

@@ -129,3 +129,25 @@ export function dynamicSort<T>(list: T[], key: string, direction: 'asc' | 'desc'
return 0;
});
}
/**
* 목록 뷰용 액션 버튼 HTML 생성 (양식, 업로드, 엑셀저장, 자산추가)
*/
export function getActionButtonsHTML(): string {
return `
<div class="search-actions">
<button id="btn-download-template" class="btn btn-outline" title="통합 양식 다운로드">
<i data-lucide="download"></i> 양식
</button>
<label for="excel-upload" class="btn btn-outline" title="엑셀 파일 업로드">
<i data-lucide="upload"></i> 업로드
</label>
<button id="btn-export-excel" class="btn btn-primary" title="일괄 엑셀 저장">
<i data-lucide="file-spreadsheet"></i> 엑셀저장
</button>
<button id="btn-add-asset" class="btn btn-primary">
<i data-lucide="plus"></i> 자산추가
</button>
</div>
`;
}

View File

@@ -1,4 +1,4 @@
import { state, loadMasterDataFromDB } from './core/state';
import { state, loadMasterDataFromDB, saveAsset } from './core/state';
import { renderNavigation } from './components/Navigation';
import { renderDashboard } from './views/DashboardView';
import { renderSWTable } from './views/SW_Table';
@@ -35,15 +35,15 @@ async function apiBatchSave(url: string, data: any[], label: string) {
const savePcToDB = () => apiBatchSave(`http://${location.hostname}:3000/api/pc/batch`, state.masterData.pc, '개인PC');
const saveServerToDB = () => apiBatchSave(`http://${location.hostname}:3000/api/server/batch`, state.masterData.server, '서버');
const saveStorageToDB = () => apiBatchSave(`http://${location.hostname}:3000/api/storage/batch`, state.masterData.storage, '스토리지');
const saveEquipToDB = () => apiBatchSave(`http://${location.hostname}:3000/api/equip/batch`, state.masterData.equip, '전산비품');
const saveMobileToDB = () => apiBatchSave(`http://${location.hostname}:3000/api/mobile/batch`, state.masterData.mobile, '모바일기기');
const saveSubSwToDB = () => apiBatchSave(`http://${location.hostname}:3000/api/asset/software/subscription/batch`, state.masterData.subSw, '구독SW');
const savePermSwToDB = () => apiBatchSave(`http://${location.hostname}:3000/api/asset/software/perpetual/batch`, state.masterData.permSw, '영구SW');
const saveCloudToDB = () => apiBatchSave(`http://${location.hostname}:3000/api/asset/cloud/batch`, state.masterData.cloud, '클라우드');
const saveNetworkToDB = () => apiBatchSave(`http://${location.hostname}:3000/api/network/batch`, state.masterData.network, '네트워크');
const saveEquipToDB = () => apiBatchSave(`http://${location.hostname}:3000/api/equipment/batch`, state.masterData.equipment, '업무지원장비');
const saveSwInternalToDB = () => apiBatchSave(`http://${location.hostname}:3000/api/sw/internal/batch`, state.masterData.swInternal, '내부SW');
const saveSwExternalToDB = () => apiBatchSave(`http://${location.hostname}:3000/api/sw/external/batch`, state.masterData.swExternal, '외부SW');
const saveCloudToDB = () => apiBatchSave(`http://${location.hostname}:3000/api/cloud/batch`, state.masterData.cloud, '클라우드');
const saveSwUsersToDB = () => apiBatchSave(`http://${location.hostname}:3000/api/asset/software/assignment/batch`, state.masterData.swUsers, 'SW사용자');
const saveLogsToDB = () => apiBatchSave(`http://${location.hostname}:3000/api/asset/history/batch`, state.masterData.logs, '자산 로그');
// 화면 갱신 통합 핸들러 (대시보드 vs 리스트)
// 화면 갱신 통합 핸들러
function refreshView() {
const mainContent = document.getElementById('main-content')!;
if (!mainContent) return;
@@ -55,34 +55,17 @@ function refreshView() {
}
}
// 모든 하드웨어 DB 동기화
async function saveAllHardwareToDB() {
// 통합 저장 및 갱신
async function saveAllDataToDB() {
await Promise.all([
savePcToDB(),
saveServerToDB(),
saveStorageToDB(),
saveEquipToDB(),
saveMobileToDB(),
saveLogsToDB()
savePcToDB(), saveServerToDB(), saveStorageToDB(), saveNetworkToDB(),
saveEquipToDB(), saveSwInternalToDB(), saveSwExternalToDB(),
saveCloudToDB(), saveSwUsersToDB(), saveLogsToDB()
]);
await loadMasterDataFromDB();
refreshView();
}
// 모든 소프트웨어 DB 동기화
async function saveAllSoftwareToDB() {
await Promise.all([
saveSubSwToDB(),
savePermSwToDB(),
saveCloudToDB(),
saveSwUsersToDB(),
saveLogsToDB()
]);
// 저장 후 최신 데이터 다시 로드 (정합성)
await loadMasterDataFromDB();
refreshView();
}
// --- App Initialization ---
function initApp() {
const mainContent = document.getElementById('main-content')!;
@@ -91,7 +74,6 @@ function initApp() {
const { closeAllModals } = initBaseModal();
try {
// 네비게이션 렌더링 및 콜백 연결
renderNavigation((tab) => {
if (tab === '대시보드') {
renderDashboard(mainContent);
@@ -100,9 +82,8 @@ function initApp() {
}
});
// 각종 모달 및 가이드 초기화
initHwModal(() => saveAllHardwareToDB(), closeAllModals);
initSwModal(() => saveAllSoftwareToDB(), closeAllModals);
initHwModal(() => saveAllDataToDB(), closeAllModals);
initSwModal(() => saveAllDataToDB(), closeAllModals);
initSwUserModal(() => {
saveSwUsersToDB().then(() => {
@@ -118,7 +99,6 @@ function initApp() {
});
initGuide();
// DB 데이터 로드 및 초기 화면 렌더링
loadMasterDataFromDB().then((success) => {
if (success) {
refreshView();
@@ -126,58 +106,62 @@ function initApp() {
});
} catch (e) { console.error('❌ Initialization failed:', e); }
console.log('🚀 ITAM App Version 2.1.0 Loaded');
console.log('🚀 ITAM App Multi-Table Optimized');
// 버튼 이벤트 바인딩
document.getElementById('btn-download-template')?.addEventListener('click', () => downloadTemplate());
document.getElementById('btn-export-excel')?.addEventListener('click', () => exportToExcel(state.masterData));
// --- 통합 이벤트 위임 (Dynamic Elements 지원) ---
document.addEventListener('click', (e) => {
const target = e.target as HTMLElement;
const uploadInput = document.getElementById('excel-upload') as HTMLInputElement;
uploadInput?.addEventListener('change', async (e) => {
const file = (e.target as HTMLInputElement).files?.[0];
if (file) {
console.log('📂 File selected:', file.name);
try {
const data = await parseExcel(file);
console.log('📊 Parsed data keys:', Object.keys(data));
openUploadPreview(data);
// Clear input so same file can be selected again
uploadInput.value = '';
} catch (err) {
alert('엑셀 파일을 읽는 중 오류가 발생했습니다.');
console.error(err);
// 양식 다운로드
if (target.closest('#btn-download-template')) {
downloadTemplate();
return;
}
}
});
document.getElementById('btn-add-asset')?.addEventListener('click', () => {
// 엑셀 내보내기
if (target.closest('#btn-export-excel')) {
exportToExcel(state.masterData);
return;
}
// 자산 추가
if (target.closest('#btn-add-asset')) {
const tab = state.activeSubTab;
const cat = state.activeCategory;
const newId = Math.random().toString(36).substring(2, 9);
if (cat === 'hw') {
let defaultType = (tab === '개인PC') ? 'PC' : (tab === '서버' ? '서버' : (tab === '스토리지' ? '스토리지' : (tab === '전산비품' ? 'CPU' : '모바일')));
openHwModal({ id: Math.random().toString(36).substring(2, 9), type: defaultType, : '한맥', : '', : '', : '', MACaddress: '', HW사양: '', OS: '', : '', : '' } as any, 'add');
openHwModal({ id: newId, asset_code: '', category: tab } as any, 'add');
} else if (cat === 'sw') {
openSwModal({ id: Math.random().toString(36).substring(2, 9), type: tab === '대시보드' ? '구독SW' : tab, : '', : '', 수량: 1, : '', : '', : '', : '한맥' } as any, 'add');
openSwModal({ id: newId, asset_type: tab === '대시보드' ? '외부SW' : tab } as any, 'add');
} else if (cat === 'ops') {
if (tab === '도메인') openDomainModal(null);
}
return;
}
});
// 시크릿 클라우드 트리거
document.getElementById('secret-cloud-trigger')?.addEventListener('click', () => {
state.activeCategory = 'sw';
state.activeSubTab = '클라우드';
const mainContent = document.getElementById('main-content')!;
renderSWTable(mainContent);
// 엑셀 업로드 (Change 이벤트 위임)
document.addEventListener('change', async (e) => {
const target = e.target as HTMLInputElement;
if (target.id === 'excel-upload') {
const file = target.files?.[0];
if (file) {
try {
const data = await parseExcel(file);
openUploadPreview(data);
target.value = '';
} catch (err) {
alert('엑셀 파일을 읽는 중 오류가 발생했습니다.');
}
}
}
});
createIcons({
icons: { Download, Upload, FileSpreadsheet, Plus, X, LayoutDashboard, Monitor, Server, Database, Laptop, CalendarClock, Key, Cpu, Layers, Users, Paperclip, Edit2, History, RefreshCcw, BookOpen, Settings }
});
window.addEventListener('refresh-view', () => {
console.log('🔄 Refreshing view due to event');
refreshView();
});
window.addEventListener('refresh-view', () => refreshView());
}
document.addEventListener('DOMContentLoaded', initApp);

View File

@@ -3,9 +3,9 @@
.search-bar {
display: flex;
flex-wrap: wrap;
gap: 1.25rem;
padding: 1.5rem 0; /* 좌우 패딩 제거, 상하 여백 유지 */
border-bottom: 1px solid var(--border-color); /* 하단 구분선만 남김 */
gap: 0.75rem; /* 간격 축소 및 통일 */
padding: 1.2rem 0;
border-bottom: 1px solid var(--border-color);
align-items: flex-end;
margin-bottom: 0.5rem;
}
@@ -17,7 +17,19 @@
}
.search-item.flex-1 {
flex: 1;
flex: 1; /* 검색창이 남은 공간을 채우도록 설정 */
min-width: 250px;
}
.search-actions {
display: flex;
gap: 0.5rem; /* 버튼들 간의 간격 */
align-items: center;
}
.search-actions .btn {
height: 38px;
padding: 0 1rem;
}
.search-item label {
@@ -50,13 +62,13 @@
/* 필터 초기화 버튼 크기 조정 (입력창 높이 38px에 맞춤) */
.btn-reset {
margin-left: auto;
height: 38px !important;
color: var(--text-muted) !important;
padding: 0 1.2rem !important;
display: inline-flex;
align-items: center;
justify-content: center;
margin-left: 0; /* 불필요한 마진 제거 */
}
.table-container {

View File

@@ -1,9 +1,9 @@
import { state } from '../../core/state';
import { openSwModal } from '../../components/Modal/SWModal';
import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema';
import { dynamicSort } from '../../core/utils';
import { dynamicSort, getActionButtonsHTML } from '../../core/utils';
import { setupTableSorting, SortState } from '../../core/tableHandler';
import { createIcons, Cloud, CreditCard, DollarSign, RefreshCcw } from 'lucide';
import { createIcons, Cloud, CreditCard, DollarSign, RefreshCcw, Download, Upload, FileSpreadsheet, Plus } from 'lucide';
/**
* 클라우드(운영 서비스) 자산 목록 뷰
@@ -17,11 +17,11 @@ export function renderCloudList(container: HTMLElement) {
filterBar.className = 'search-bar';
filterBar.innerHTML = `
<div class="search-item flex-1">
<label>통합 검색 (${ASSET_SCHEMA.PRODUCT.ui}/부서/${ASSET_SCHEMA.ACCOUNT.ui})</label>
<label>통합 검색 (${ASSET_SCHEMA.PRODUCT_NAME.ui}/${ASSET_SCHEMA.CURRENT_DEPT.ui}/${ASSET_SCHEMA.EMAIL_ACCOUNT.ui})</label>
<input type="text" id="filter-keyword" placeholder="검색어를 입력하세요..." autocomplete="off">
</div>
<div class="search-item">
<label>${ASSET_SCHEMA.PAY_METHOD.ui}</label>
<label>${ASSET_SCHEMA.PURCHASE_METHOD.ui}</label>
<select id="filter-payment">
<option value="">전체 결제수단</option>
<option value="법인카드">법인카드</option>
@@ -31,6 +31,7 @@ export function renderCloudList(container: HTMLElement) {
<button id="btn-reset-filters" class="btn btn-outline btn-reset">
<i data-lucide="refresh-ccw"></i> ${UI_TEXT.ACTION.RESET_FILTER}
</button>
${getActionButtonsHTML()}
`;
container.appendChild(filterBar);
@@ -41,15 +42,11 @@ export function renderCloudList(container: HTMLElement) {
<thead>
<tr>
<th class="text-center" style="width:50px;">No.</th>
<th data-sort="${ASSET_SCHEMA.PLATFORM.key}">${ASSET_SCHEMA.PLATFORM.ui}</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.CORP.key}">${ASSET_SCHEMA.CORP.ui}</th>
<th class="text-center" data-sort="부서">담당부서</th>
<th data-sort="${ASSET_SCHEMA.PRODUCT.key}">용도(프로젝트)</th>
<th data-sort="${ASSET_SCHEMA.ACCOUNT.key}">${ASSET_SCHEMA.ACCOUNT.ui}</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.PAY_METHOD.key}">${ASSET_SCHEMA.PAY_METHOD.ui}</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.PAY_DAY.key}">${ASSET_SCHEMA.PAY_DAY.ui}</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.BILLING.key}">${ASSET_SCHEMA.BILLING.ui}</th>
<th>${ASSET_SCHEMA.REMARKS.ui}</th>
<th data-sort="${ASSET_SCHEMA.PRODUCT_NAME.key}">${ASSET_SCHEMA.PRODUCT_NAME.ui}</th>
<th data-sort="${ASSET_SCHEMA.ASSET_PURPOSE.key}">${ASSET_SCHEMA.ASSET_PURPOSE.ui}</th>
<th data-sort="${ASSET_SCHEMA.PURCHASE_VENDOR.key}">${ASSET_SCHEMA.PURCHASE_VENDOR.ui}</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.PURCHASE_AMOUNT.key}">${ASSET_SCHEMA.PURCHASE_AMOUNT.ui}</th>
<th style="width: 30%;">${ASSET_SCHEMA.MEMO.ui}</th>
</tr>
</thead>
<tbody id="cloud-tbody"></tbody>
@@ -68,10 +65,10 @@ export function renderCloudList(container: HTMLElement) {
let filtered = getFullList().filter(asset => {
const kwMatch = !keyword ||
(asset[ASSET_SCHEMA.PRODUCT.key] || '').toLowerCase().includes(keyword) ||
(asset. || '').toLowerCase().includes(keyword) ||
(asset[ASSET_SCHEMA.ACCOUNT.key] || '').toLowerCase().includes(keyword);
const payMatch = !payment || asset[ASSET_SCHEMA.PAY_METHOD.key] === payment;
(asset[ASSET_SCHEMA.PRODUCT_NAME.key] || '').toLowerCase().includes(keyword) ||
(asset[ASSET_SCHEMA.ASSET_PURPOSE.key] || '').toLowerCase().includes(keyword) ||
(asset[ASSET_SCHEMA.PURCHASE_VENDOR.key] || '').toLowerCase().includes(keyword);
const payMatch = !payment || asset[ASSET_SCHEMA.PURCHASE_METHOD.key] === payment;
return kwMatch && payMatch;
});
@@ -81,7 +78,7 @@ export function renderCloudList(container: HTMLElement) {
tbody.innerHTML = '';
if (filtered.length === 0) {
tbody.innerHTML = `<tr><td colspan="10" class="text-center" style="padding: 3rem; color: var(--text-muted);">${UI_TEXT.MESSAGES.NO_DATA}</td></tr>`;
tbody.innerHTML = `<tr><td colspan="6" class="text-center" style="padding: 3rem; color: var(--text-muted);">${UI_TEXT.MESSAGES.NO_DATA}</td></tr>`;
return;
}
@@ -89,24 +86,13 @@ export function renderCloudList(container: HTMLElement) {
const tr = document.createElement('tr');
tr.style.cursor = 'pointer';
const payMethod = asset[ASSET_SCHEMA.PAY_METHOD.key];
const paymentBadge = payMethod === '법인카드'
? `<span style="color:#6366f1; font-weight:600;"><i data-lucide="credit-card" style="width:14px; height:14px; vertical-align:middle; margin-right:4px;"></i>법인카드</span>`
: (payMethod === '인보이스'
? '<span style="color:#10b981; font-weight:600;"><i data-lucide="dollar-sign" style="width:14px; height:14px; vertical-align:middle; margin-right:4px;"></i>인보이스</span>'
: '<span style="color:var(--text-muted)">미설정</span>');
tr.innerHTML = `
<td class="text-center">${idx+1}</td>
<td style="font-weight:600; color:var(--primary-color)"><i data-lucide="cloud" style="width:14px; height:14px; vertical-align:middle; margin-right:4px;"></i> ${asset[ASSET_SCHEMA.PLATFORM.key]||'미지정'}</td>
<td class="text-center">${asset[ASSET_SCHEMA.CORP.key]||''}</td>
<td class="text-center">${asset.||''}</td>
<td>${asset[ASSET_SCHEMA.PRODUCT.key]||''}</td>
<td>${asset[ASSET_SCHEMA.ACCOUNT.key]||''}</td>
<td class="text-center">${paymentBadge}</td>
<td class="text-center">${asset[ASSET_SCHEMA.PAY_DAY.key] ? asset[ASSET_SCHEMA.PAY_DAY.key] + '일' : ''}</td>
<td class="text-right" style="font-weight:600;">₩ ${asset[ASSET_SCHEMA.BILLING.key] ? Number(String(asset[ASSET_SCHEMA.BILLING.key]).replace(/,/g, '')).toLocaleString() : '0'}</td>
<td>${asset[ASSET_SCHEMA.REMARKS.key]||''}</td>
<td>${asset[ASSET_SCHEMA.PRODUCT_NAME.key]||''}</td>
<td>${asset[ASSET_SCHEMA.ASSET_PURPOSE.key]||''}</td>
<td>${asset[ASSET_SCHEMA.PURCHASE_VENDOR.key]||''}</td>
<td class="text-right" style="font-weight:600;">₩ ${asset[ASSET_SCHEMA.PURCHASE_AMOUNT.key] ? Number(String(asset[ASSET_SCHEMA.PURCHASE_AMOUNT.key]).replace(/,/g, '')).toLocaleString() : '0'}</td>
<td>${formatInline(asset[ASSET_SCHEMA.MEMO.key]||'')}</td>
`;
tr.addEventListener('click', () => openSwModal(asset, 'view'));
@@ -118,7 +104,7 @@ export function renderCloudList(container: HTMLElement) {
updateTable();
});
createIcons({ icons: { Cloud, CreditCard, DollarSign, RefreshCcw } });
createIcons({ icons: { Cloud, CreditCard, DollarSign, RefreshCcw, Download, Upload, FileSpreadsheet, Plus } });
};
document.getElementById('filter-keyword')?.addEventListener('input', updateTable);

View File

@@ -0,0 +1,107 @@
import { state } from '../../core/state';
import { formatInline, sortAssets, dynamicSort, getActionButtonsHTML } from '../../core/utils';
import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema';
import { setupTableSorting, SortState } from '../../core/tableHandler';
import { createIcons, RefreshCcw, Download, Upload, FileSpreadsheet, Plus } from 'lucide';
/**
* 비용관리 자산 목록 뷰
*/
export function renderCostList(container: HTMLElement) {
// 비용관리 데이터는 cloud 또는 별도 테이블에 있을 수 있음.
const fullList = sortAssets(state.masterData.cloud?.filter((a: any) => a.category === '비용관리') || []);
let sortState: SortState = { key: '', direction: 'asc' };
const filterBar = document.createElement('div');
filterBar.className = 'search-bar';
filterBar.innerHTML = `
<div class="search-item flex-1">
<label>통합 검색 (${ASSET_SCHEMA.PRODUCT_NAME.ui}/${ASSET_SCHEMA.MANAGER_MAIN.ui})</label>
<input type="text" id="filter-keyword" placeholder="검색어를 입력하세요..." autocomplete="off">
</div>
<button id="btn-reset-filters" class="btn btn-outline btn-reset">
<i data-lucide="refresh-ccw"></i> ${UI_TEXT.ACTION.RESET_FILTER}
</button>
${getActionButtonsHTML()}
`;
container.appendChild(filterBar);
const tableWrapper = document.createElement('div');
tableWrapper.className = 'table-container';
const table = document.createElement('table');
table.innerHTML = `
<thead>
<tr>
<th class="text-center" style="width:50px;">No.</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.ASSET_TYPE.key}">${ASSET_SCHEMA.ASSET_TYPE.ui}</th>
<th data-sort="${ASSET_SCHEMA.ASSET_PURPOSE.key}">${ASSET_SCHEMA.ASSET_PURPOSE.ui}</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.MANAGER_MAIN.key}">현 사용자</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.LOCATION.key}">${ASSET_SCHEMA.LOCATION.ui}(건물)</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.LOC_DETAIL.key}">${ASSET_SCHEMA.LOC_DETAIL.ui}</th>
<th data-sort="${ASSET_SCHEMA.EMAIL_ACCOUNT.key}">${ASSET_SCHEMA.EMAIL_ACCOUNT.ui}</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.MEMO.key}">${ASSET_SCHEMA.MEMO.ui}</th>
</tr>
</thead>
<tbody id="dynamic-tbody"></tbody>
`;
tableWrapper.appendChild(table);
container.appendChild(tableWrapper);
const tbody = table.querySelector('tbody')!;
const updateTable = () => {
const keywordInput = document.getElementById('filter-keyword') as HTMLInputElement;
const keyword = keywordInput ? keywordInput.value.toLowerCase().trim() : '';
let filtered = fullList.filter(asset => {
const matchKeyword = !keyword ||
String(asset[ASSET_SCHEMA.PRODUCT_NAME.key]||'').toLowerCase().includes(keyword) ||
String(asset[ASSET_SCHEMA.MANAGER_MAIN.key]||'').toLowerCase().includes(keyword) ||
String(asset[ASSET_SCHEMA.EMAIL_ACCOUNT.key]||'').toLowerCase().includes(keyword);
return matchKeyword;
});
if (sortState.key) {
filtered = dynamicSort(filtered, sortState.key, sortState.direction);
}
tbody.innerHTML = '';
if (filtered.length === 0) {
tbody.innerHTML = `<tr><td colspan="8" class="text-center" style="padding: 3rem; color: var(--text-muted);">${UI_TEXT.MESSAGES.NO_DATA}</td></tr>`;
return;
}
filtered.forEach((asset, idx) => {
const tr = document.createElement('tr');
tr.style.cursor = 'pointer';
tr.innerHTML = `
<td class="text-center">${idx + 1}</td>
<td class="text-center">${asset[ASSET_SCHEMA.ASSET_TYPE.key] || ''}</td>
<td>${formatInline(asset[ASSET_SCHEMA.ASSET_PURPOSE.key] || '-')}</td>
<td class="text-center">${asset[ASSET_SCHEMA.MANAGER_MAIN.key] || '-'}</td>
<td class="text-center">${asset[ASSET_SCHEMA.LOCATION.key] || '-'}</td>
<td class="text-center">${asset[ASSET_SCHEMA.LOC_DETAIL.key] || '-'}</td>
<td>${asset[ASSET_SCHEMA.EMAIL_ACCOUNT.key] || '-'}</td>
<td class="text-center">${formatInline(asset[ASSET_SCHEMA.MEMO.key]||'-')}</td>
`;
// 비용관리 모달이 따로 없으면 일단 SW모달 또는 알림
tr.addEventListener('click', () => alert('상세 정보 준비 중입니다.'));
tbody.appendChild(tr);
});
setupTableSorting(table, sortState, (key, dir) => {
sortState = { key, direction: dir };
updateTable();
});
createIcons({ icons: { RefreshCcw, Download, Upload, FileSpreadsheet, Plus } });
};
document.getElementById('filter-keyword')?.addEventListener('input', updateTable);
document.getElementById('btn-reset-filters')?.addEventListener('click', () => {
(document.getElementById('filter-keyword') as HTMLInputElement).value = '';
updateTable();
});
updateTable();
}

View File

@@ -1,9 +1,9 @@
import { state } from '../../core/state';
import { formatPrice, dynamicSort, createBadge } from '../../core/utils';
import { createIcons, Plus, Edit2, Trash2 } from 'lucide';
import { formatPrice, dynamicSort, createBadge, getActionButtonsHTML } from '../../core/utils';
import { createIcons, Plus, Edit2, Trash2, RefreshCcw, Download, Upload, FileSpreadsheet } from 'lucide';
import { openDomainModal } from '../../components/Modal/DomainModal';
import { setupTableSorting, SortState } from '../../core/tableHandler';
import { formatExcelDate } from '../../core/excelHandler';
import { ASSET_SCHEMA } from '../../core/schema';
// 정렬 상태를 모듈 수준에서 관리하여 화면 갱신 시에도 유지되도록 함
let persistentSortState: SortState = { key: '', direction: 'asc' };
@@ -13,14 +13,20 @@ export function renderDomainList(container: HTMLElement) {
const fullList = state.masterData.domain;
const header = document.createElement('div');
header.className = 'list-header';
header.innerHTML = `
<div class="list-title-area">
<h2 class="list-title">도메인 관리</h2>
// 검색바 및 액션 버튼 추가
const filterBar = document.createElement('div');
filterBar.className = 'search-bar';
filterBar.innerHTML = `
<div class="search-item flex-1">
<label>통합 검색 (${ASSET_SCHEMA.DOMAIN_ADDR.ui}/${ASSET_SCHEMA.PRODUCT_NAME.ui})</label>
<input type="text" id="filter-keyword" placeholder="검색어를 입력하세요..." autocomplete="off">
</div>
<button id="btn-reset-filters" class="btn btn-outline btn-reset">
<i data-lucide="refresh-ccw"></i> 필터 초기화
</button>
${getActionButtonsHTML()}
`;
container.appendChild(header);
container.appendChild(filterBar);
const tableWrapper = document.createElement('div');
tableWrapper.className = 'table-container';
@@ -29,15 +35,12 @@ export function renderDomainList(container: HTMLElement) {
<thead>
<tr>
<th style="text-align:center; width:50px;">No.</th>
<th style="text-align:center;" data-sort="type">유형</th>
<th style="text-align:center;" data-sort="corp">법인</th>
<th style="text-align:left;" data-sort="service_name">서비스명</th>
<th style="text-align:left;" data-sort="domain_name">관리도메인</th>
<th style="text-align:left;" data-sort="remarks">구매업체</th>
<th style="text-align:center;" data-sort="start_date">시작일</th>
<th style="text-align:center;" data-sort="expiry_date">만료일</th>
<th style="text-align:right;" data-sort="price">금액</th>
<th style="text-align:center;" data-sort="manager_main">담당자(정/부)</th>
<th style="text-align:left;" data-sort="${ASSET_SCHEMA.DOMAIN_ADDR.key}">${ASSET_SCHEMA.DOMAIN_ADDR.ui}</th>
<th style="text-align:left;" data-sort="${ASSET_SCHEMA.ASSET_PURPOSE.key}">${ASSET_SCHEMA.ASSET_PURPOSE.ui}</th>
<th style="text-align:center;" data-sort="${ASSET_SCHEMA.ASSET_TYPE.key}">${ASSET_SCHEMA.ASSET_TYPE.ui}</th>
<th style="text-align:center;" data-sort="${ASSET_SCHEMA.PURCHASE_CORP.key}">${ASSET_SCHEMA.PURCHASE_CORP.ui}</th>
<th style="text-align:center;" data-sort="${ASSET_SCHEMA.EXPIRY_DATE.key}">${ASSET_SCHEMA.EXPIRY_DATE.ui}</th>
<th style="text-align:center;" data-sort="${ASSET_SCHEMA.MEMO.key}">${ASSET_SCHEMA.MEMO.ui}</th>
</tr>
</thead>
<tbody id="dynamic-tbody"></tbody>
@@ -48,7 +51,16 @@ export function renderDomainList(container: HTMLElement) {
const tbody = table.querySelector('tbody')!;
const updateTable = () => {
let filtered = [...fullList];
const keywordInput = document.getElementById('filter-keyword') as HTMLInputElement;
const keyword = keywordInput ? keywordInput.value.toLowerCase().trim() : '';
let filtered = fullList.filter(item => {
const matchKeyword = !keyword ||
(item[ASSET_SCHEMA.DOMAIN_ADDR.key] || '').toLowerCase().includes(keyword) ||
(item[ASSET_SCHEMA.ASSET_PURPOSE.key] || '').toLowerCase().includes(keyword) ||
(item[ASSET_SCHEMA.PRODUCT_NAME.key] || '').toLowerCase().includes(keyword);
return matchKeyword;
});
if (persistentSortState.key) {
filtered = dynamicSort(filtered, persistentSortState.key, persistentSortState.direction);
@@ -56,7 +68,7 @@ export function renderDomainList(container: HTMLElement) {
tbody.innerHTML = '';
if (filtered.length === 0) {
tbody.innerHTML = `<tr><td colspan="10" style="text-align:center; padding: 3rem; color: var(--text-muted);">등록된 도메인 정보가 없습니다.</td></tr>`;
tbody.innerHTML = `<tr><td colspan="7" style="text-align:center; padding: 3rem; color: var(--text-muted);">등록된 도메인 정보가 없습니다.</td></tr>`;
return;
}
@@ -64,25 +76,17 @@ export function renderDomainList(container: HTMLElement) {
const tr = document.createElement('tr');
tr.className = 'domain-row';
tr.style.cursor = 'pointer';
const managerHtml = [
item.manager_main ? `${createBadge('정', 'primary')} ${item.manager_main}` : '',
item.manager_sub ? `${createBadge('부', 'muted')} ${item.manager_sub}` : ''
].filter(v => v !== '').join(' / ');
tr.innerHTML = `
<td style="text-align:center;">${idx + 1}</td>
<td style="text-align:center;"><span class="badge badge-${item.type}">${item.type}</span></td>
<td style="text-align:center;">${item.corp || ''}</td>
<td>${item.service_name || ''}</td>
<td>${item.domain_name || ''}</td>
<td>${item.remarks || ''}</td>
<td style="text-align:center;">${formatExcelDate(item.start_date)}</td>
<td style="text-align:center;">${formatExcelDate(item.expiry_date)}</td>
<td style="text-align:right;">${formatPrice(item.price)}</td>
<td style="text-align:center;">${managerHtml || '-'}</td>
<td>${item[ASSET_SCHEMA.DOMAIN_ADDR.key] || ''}</td>
<td>${item[ASSET_SCHEMA.ASSET_PURPOSE.key] || ''}</td>
<td style="text-align:center;"><span class="badge badge-${item[ASSET_SCHEMA.ASSET_TYPE.key] === '관리중' ? 'primary' : 'muted'}">${item[ASSET_SCHEMA.ASSET_TYPE.key] || '-'}</span></td>
<td style="text-align:center;">${item[ASSET_SCHEMA.PURCHASE_CORP.key] || ''}</td>
<td style="text-align:center;">${item[ASSET_SCHEMA.EXPIRY_DATE.key] || ''}</td>
<td>${formatInline(item[ASSET_SCHEMA.MEMO.key]||'-')}</td>
`;
tr.addEventListener('click', (e) => {
console.log('Row clicked:', item.domain_name);
openDomainModal(item);
});
tbody.appendChild(tr);
@@ -92,8 +96,15 @@ export function renderDomainList(container: HTMLElement) {
persistentSortState = { key, direction: dir };
updateTable();
});
createIcons({ icons: { Plus, Edit2, Trash2, RefreshCcw, Download, Upload, FileSpreadsheet } });
};
document.getElementById('filter-keyword')?.addEventListener('input', updateTable);
document.getElementById('btn-reset-filters')?.addEventListener('click', () => {
if (document.getElementById('filter-keyword')) (document.getElementById('filter-keyword') as HTMLInputElement).value = '';
updateTable();
});
updateTable();
createIcons({ icons: { Plus, Edit2, Trash2 } });
}

View File

@@ -1,34 +1,35 @@
import { state } from '../../core/state';
import { openHwModal } from '../../components/Modal/HWModal';
import { formatInline, createBadge, sortAssets, dynamicSort } from '../../core/utils';
import { formatInline, createBadge, sortAssets, dynamicSort, getActionButtonsHTML } from '../../core/utils';
import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema';
import { setupTableSorting, SortState } from '../../core/tableHandler';
import { createIcons, RefreshCcw } from 'lucide';
import { createIcons, RefreshCcw, Download, Upload, FileSpreadsheet, Plus } from 'lucide';
/**
* 전산비품 자산 목록 뷰
* 라인 정렬 보정 및 헤더 통일
*/
export function renderEquipmentList(container: HTMLElement) {
const fullList = sortAssets(state.masterData.equip);
const fullList = sortAssets(state.masterData.equipment);
let sortState: SortState = { key: '', direction: 'asc' };
const filterBar = document.createElement('div');
filterBar.className = 'search-bar';
const corps = Array.from(new Set(fullList.map(a => a[ASSET_SCHEMA.CORP.key]))).filter(Boolean).sort();
const corps = Array.from(new Set(fullList.map(a => a[ASSET_SCHEMA.PURCHASE_CORP.key]))).filter(Boolean).sort();
filterBar.innerHTML = `
<div class="search-item flex-1">
<label>통합 검색 (${ASSET_SCHEMA.ASSET_CODE.ui}/${ASSET_SCHEMA.MODEL.ui}/${ASSET_SCHEMA.MANAGER_MAIN.ui})</label>
<label>통합 검색 (${ASSET_SCHEMA.MODEL_NAME.ui}/${ASSET_SCHEMA.MANAGER_MAIN.ui})</label>
<input type="text" id="filter-keyword" placeholder="검색어를 입력하세요..." autocomplete="off">
</div>
<div class="search-item">
<label>${ASSET_SCHEMA.CORP.ui}</label>
<label>${ASSET_SCHEMA.PURCHASE_CORP.ui}</label>
<select id="filter-corp"><option value="">전체 법인</option>${corps.map(c => `<option value="${c}">${c}</option>`).join('')}</select>
</div>
<button id="btn-reset-filters" class="btn btn-outline btn-reset">
<i data-lucide="refresh-ccw"></i> ${UI_TEXT.ACTION.RESET_FILTER}
</button>
${getActionButtonsHTML()}
`;
container.appendChild(filterBar);
@@ -39,15 +40,15 @@ export function renderEquipmentList(container: HTMLElement) {
<thead>
<tr>
<th class="text-center" style="width:50px;">No.</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.STATUS.key}">${ASSET_SCHEMA.STATUS.ui}</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.CORP.key}">${ASSET_SCHEMA.CORP.ui}</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.TYPE.key}">유형</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.ASSET_CODE.key}">${ASSET_SCHEMA.ASSET_CODE.ui}</th>
<th data-sort="${ASSET_SCHEMA.MODEL.key}">${ASSET_SCHEMA.MODEL.ui}</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.STORE_LOC.key}">${ASSET_SCHEMA.STORE_LOC.ui}</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.MANAGER_MAIN.key}">담당자(정/부)</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.PURCHASE_YM.key}">${ASSET_SCHEMA.PURCHASE_YM.ui}</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.PRICE.key}">${ASSET_SCHEMA.PRICE.ui}</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.HW_STATUS.key}">${ASSET_SCHEMA.HW_STATUS.ui}</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.MANAGER_MAIN.key}">현 사용자</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.ASSET_TYPE.key}">${ASSET_SCHEMA.ASSET_TYPE.ui}</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.ASSET_MFR.key}">${ASSET_SCHEMA.ASSET_MFR.ui}</th>
<th data-sort="${ASSET_SCHEMA.MODEL_NAME.key}">${ASSET_SCHEMA.MODEL_NAME.ui}</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.ASSET_COUNT.key}">${ASSET_SCHEMA.ASSET_COUNT.ui}</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.LOCATION.key}">${ASSET_SCHEMA.LOCATION.ui}(건물)</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.LOC_DETAIL.key}">${ASSET_SCHEMA.LOC_DETAIL.ui}</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.MEMO.key}">${ASSET_SCHEMA.MEMO.ui}</th>
</tr>
</thead>
<tbody id="dynamic-tbody"></tbody>
@@ -66,10 +67,10 @@ export function renderEquipmentList(container: HTMLElement) {
let filtered = fullList.filter(asset => {
const matchKeyword = !keyword ||
String(asset[ASSET_SCHEMA.ASSET_CODE.key]||'').toLowerCase().includes(keyword) ||
String(asset[ASSET_SCHEMA.MODEL.key]||'').toLowerCase().includes(keyword) ||
String(asset[ASSET_SCHEMA.MANAGER_MAIN.key]||'').toLowerCase().includes(keyword);
const matchCorp = !corp || asset[ASSET_SCHEMA.CORP.key] === corp;
String(asset[ASSET_SCHEMA.MODEL_NAME.key]||'').toLowerCase().includes(keyword) ||
String(asset[ASSET_SCHEMA.MANAGER_MAIN.key]||'').toLowerCase().includes(keyword) ||
String(asset[ASSET_SCHEMA.ASSET_MFR.key]||'').toLowerCase().includes(keyword);
const matchCorp = !corp || asset[ASSET_SCHEMA.PURCHASE_CORP.key] === corp;
return matchKeyword && matchCorp;
});
@@ -87,29 +88,17 @@ export function renderEquipmentList(container: HTMLElement) {
const tr = document.createElement('tr');
tr.style.cursor = 'pointer';
const statusColors: Record<string, string> = { '대여중': 'primary', '보관중': 'success', '수리중': 'danger', '기타': 'muted' };
const statusValue = asset[ASSET_SCHEMA.STATUS.key] || '보관중';
const statusType = statusColors[statusValue] || 'muted';
const statusBadge = `<span class="badge badge-${statusType}">${statusValue}</span>`;
const mainManager = asset[ASSET_SCHEMA.MANAGER_MAIN.key] || '';
const subManager = asset[ASSET_SCHEMA.MANAGER_SUB.key] || '';
const managerHtml = [
mainManager ? `${createBadge('정', 'primary')} ${mainManager}` : '',
subManager ? `${createBadge('부', 'muted')} ${subManager}` : ''
].filter(v => v !== '').join(' / ');
tr.innerHTML = `
<td class="text-center">${idx + 1}</td>
<td class="text-center">${statusBadge}</td>
<td class="text-center">${asset[ASSET_SCHEMA.CORP.key]}</td>
<td class="text-center">${asset[ASSET_SCHEMA.TYPE.key]}</td>
<td class="text-center" style="font-family: monospace;">${asset[ASSET_SCHEMA.ASSET_CODE.key] || '-'}</td>
<td>${formatInline(asset[ASSET_SCHEMA.MODEL.key] || asset.)}</td>
<td class="text-center">${asset[ASSET_SCHEMA.STORE_LOC.key] || '-'}</td>
<td class="text-center">${managerHtml || '-'}</td>
<td class="text-center">${asset[ASSET_SCHEMA.PURCHASE_YM.key] || ''}</td>
<td class="text-right">${Number(asset[ASSET_SCHEMA.PRICE.key]||0).toLocaleString()}</td>
<td class="text-center"><span class="badge badge-${asset[ASSET_SCHEMA.HW_STATUS.key] === '대여중' ? 'primary' : 'success'}">${asset[ASSET_SCHEMA.HW_STATUS.key] || '보관중'}</span></td>
<td class="text-center">${asset[ASSET_SCHEMA.MANAGER_MAIN.key] || '-'}</td>
<td class="text-center">${asset[ASSET_SCHEMA.ASSET_TYPE.key] || ''}</td>
<td class="text-center">${asset[ASSET_SCHEMA.ASSET_MFR.key] || ''}</td>
<td>${formatInline(asset[ASSET_SCHEMA.MODEL_NAME.key] || asset.)}</td>
<td class="text-center">${asset[ASSET_SCHEMA.ASSET_COUNT.key] || '1'}</td>
<td class="text-center">${asset[ASSET_SCHEMA.LOCATION.key] || '-'}</td>
<td class="text-center">${asset[ASSET_SCHEMA.LOC_DETAIL.key] || '-'}</td>
<td class="text-center">${formatInline(asset[ASSET_SCHEMA.MEMO.key]||'-')}</td>
`;
tr.addEventListener('click', () => openHwModal(asset, 'view'));
tbody.appendChild(tr);
@@ -120,7 +109,7 @@ export function renderEquipmentList(container: HTMLElement) {
updateTable();
});
createIcons({ icons: { RefreshCcw } });
createIcons({ icons: { RefreshCcw, Download, Upload, FileSpreadsheet, Plus } });
};
document.getElementById('filter-keyword')?.addEventListener('input', updateTable);

View File

@@ -0,0 +1,108 @@
import { state } from '../../core/state';
import { openHwModal } from '../../components/Modal/HWModal';
import { formatInline, sortAssets, dynamicSort, getActionButtonsHTML } from '../../core/utils';
import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema';
import { setupTableSorting, SortState } from '../../core/tableHandler';
import { createIcons, RefreshCcw, Download, Upload, FileSpreadsheet, Plus } from 'lucide';
/**
* 시설자산 자산 목록 뷰
*/
export function renderFacilityList(container: HTMLElement) {
// 시설자산 데이터는 equipment 또는 별도 테이블에 있을 수 있음.
const fullList = sortAssets(state.masterData.equipment?.filter((a: any) => a.category === '시설자산') || []);
let sortState: SortState = { key: '', direction: 'asc' };
const filterBar = document.createElement('div');
filterBar.className = 'search-bar';
filterBar.innerHTML = `
<div class="search-item flex-1">
<label>통합 검색 (${ASSET_SCHEMA.MODEL_NAME.ui})</label>
<input type="text" id="filter-keyword" placeholder="검색어를 입력하세요..." autocomplete="off">
</div>
<button id="btn-reset-filters" class="btn btn-outline btn-reset">
<i data-lucide="refresh-ccw"></i> ${UI_TEXT.ACTION.RESET_FILTER}
</button>
${getActionButtonsHTML()}
`;
container.appendChild(filterBar);
const tableWrapper = document.createElement('div');
tableWrapper.className = 'table-container';
const table = document.createElement('table');
table.innerHTML = `
<thead>
<tr>
<th class="text-center" style="width:50px;">No.</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.HW_STATUS.key}">${ASSET_SCHEMA.HW_STATUS.ui}</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.ASSET_TYPE.key}">${ASSET_SCHEMA.ASSET_TYPE.ui}</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.ASSET_MFR.key}">${ASSET_SCHEMA.ASSET_MFR.ui}</th>
<th data-sort="${ASSET_SCHEMA.MODEL_NAME.key}">${ASSET_SCHEMA.MODEL_NAME.ui}</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.LOCATION.key}">${ASSET_SCHEMA.LOCATION.ui}(건물)</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.LOC_DETAIL.key}">${ASSET_SCHEMA.LOC_DETAIL.ui}</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.ASSET_COUNT.key}">${ASSET_SCHEMA.ASSET_COUNT.ui}</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.MEMO.key}">${ASSET_SCHEMA.MEMO.ui}</th>
</tr>
</thead>
<tbody id="dynamic-tbody"></tbody>
`;
tableWrapper.appendChild(table);
container.appendChild(tableWrapper);
const tbody = table.querySelector('tbody')!;
const updateTable = () => {
const keywordInput = document.getElementById('filter-keyword') as HTMLInputElement;
const keyword = keywordInput ? keywordInput.value.toLowerCase().trim() : '';
let filtered = fullList.filter(asset => {
const matchKeyword = !keyword ||
String(asset[ASSET_SCHEMA.MODEL_NAME.key]||'').toLowerCase().includes(keyword) ||
String(asset[ASSET_SCHEMA.ASSET_MFR.key]||'').toLowerCase().includes(keyword);
return matchKeyword;
});
if (sortState.key) {
filtered = dynamicSort(filtered, sortState.key, sortState.direction);
}
tbody.innerHTML = '';
if (filtered.length === 0) {
tbody.innerHTML = `<tr><td colspan="9" class="text-center" style="padding: 3rem; color: var(--text-muted);">${UI_TEXT.MESSAGES.NO_DATA}</td></tr>`;
return;
}
filtered.forEach((asset, idx) => {
const tr = document.createElement('tr');
tr.style.cursor = 'pointer';
tr.innerHTML = `
<td class="text-center">${idx + 1}</td>
<td class="text-center"><span class="badge badge-success">${asset[ASSET_SCHEMA.HW_STATUS.key] || '보관중'}</span></td>
<td class="text-center">${asset[ASSET_SCHEMA.ASSET_TYPE.key] || ''}</td>
<td class="text-center">${asset[ASSET_SCHEMA.ASSET_MFR.key] || ''}</td>
<td>${formatInline(asset[ASSET_SCHEMA.MODEL_NAME.key] || '-')}</td>
<td class="text-center">${asset[ASSET_SCHEMA.LOCATION.key] || '-'}</td>
<td class="text-center">${asset[ASSET_SCHEMA.LOC_DETAIL.key] || '-'}</td>
<td class="text-center">${asset[ASSET_SCHEMA.ASSET_COUNT.key] || '1'}</td>
<td class="text-center">${formatInline(asset[ASSET_SCHEMA.MEMO.key]||'-')}</td>
`;
tr.addEventListener('click', () => openHwModal(asset, 'view'));
tbody.appendChild(tr);
});
setupTableSorting(table, sortState, (key, dir) => {
sortState = { key, direction: dir };
updateTable();
});
createIcons({ icons: { RefreshCcw, Download, Upload, FileSpreadsheet, Plus } });
};
document.getElementById('filter-keyword')?.addEventListener('input', updateTable);
document.getElementById('btn-reset-filters')?.addEventListener('click', () => {
(document.getElementById('filter-keyword') as HTMLInputElement).value = '';
updateTable();
});
updateTable();
}

View File

@@ -0,0 +1,100 @@
import { state } from '../../core/state';
import { formatInline, sortAssets, dynamicSort, getActionButtonsHTML } from '../../core/utils';
import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema';
import { setupTableSorting, SortState } from '../../core/tableHandler';
import { createIcons, RefreshCcw, Download, Upload, FileSpreadsheet, Plus } from 'lucide';
/**
* 선물(내빈/외빈) 자산 목록 뷰
*/
export function renderGiftList(container: HTMLElement) {
// 선물 데이터는 equipment 또는 별도 테이블에 있을 수 있음.
const fullList = sortAssets(state.masterData.equipment?.filter((a: any) => a.category === '선물') || []);
let sortState: SortState = { key: '', direction: 'asc' };
const filterBar = document.createElement('div');
filterBar.className = 'search-bar';
filterBar.innerHTML = `
<div class="search-item flex-1">
<label>통합 검색 (${ASSET_SCHEMA.PRODUCT_NAME.ui})</label>
<input type="text" id="filter-keyword" placeholder="검색어를 입력하세요..." autocomplete="off">
</div>
<button id="btn-reset-filters" class="btn btn-outline btn-reset">
<i data-lucide="refresh-ccw"></i> ${UI_TEXT.ACTION.RESET_FILTER}
</button>
${getActionButtonsHTML()}
`;
container.appendChild(filterBar);
const tableWrapper = document.createElement('div');
tableWrapper.className = 'table-container';
const table = document.createElement('table');
table.innerHTML = `
<thead>
<tr>
<th class="text-center" style="width:50px;">No.</th>
<th data-sort="${ASSET_SCHEMA.PRODUCT_NAME.key}">자산명</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.PURCHASE_DATE.key}">구매연월</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.EXPIRY_DATE.key}">${ASSET_SCHEMA.EXPIRY_DATE.ui}</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.ASSET_COUNT.key}">${ASSET_SCHEMA.ASSET_COUNT.ui}</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.MEMO.key}">${ASSET_SCHEMA.MEMO.ui}</th>
</tr>
</thead>
<tbody id="dynamic-tbody"></tbody>
`;
tableWrapper.appendChild(table);
container.appendChild(tableWrapper);
const tbody = table.querySelector('tbody')!;
const updateTable = () => {
const keywordInput = document.getElementById('filter-keyword') as HTMLInputElement;
const keyword = keywordInput ? keywordInput.value.toLowerCase().trim() : '';
let filtered = fullList.filter(asset => {
const matchKeyword = !keyword ||
String(asset[ASSET_SCHEMA.PRODUCT_NAME.key]||asset[ASSET_SCHEMA.MODEL_NAME.key]||'').toLowerCase().includes(keyword);
return matchKeyword;
});
if (sortState.key) {
filtered = dynamicSort(filtered, sortState.key, sortState.direction);
}
tbody.innerHTML = '';
if (filtered.length === 0) {
tbody.innerHTML = `<tr><td colspan="6" class="text-center" style="padding: 3rem; color: var(--text-muted);">${UI_TEXT.MESSAGES.NO_DATA}</td></tr>`;
return;
}
filtered.forEach((asset, idx) => {
const tr = document.createElement('tr');
tr.style.cursor = 'pointer';
tr.innerHTML = `
<td class="text-center">${idx + 1}</td>
<td>${formatInline(asset[ASSET_SCHEMA.PRODUCT_NAME.key] || asset[ASSET_SCHEMA.MODEL_NAME.key] || '-')}</td>
<td class="text-center">${asset[ASSET_SCHEMA.PURCHASE_DATE.key] || ''}</td>
<td class="text-center">${asset[ASSET_SCHEMA.EXPIRY_DATE.key] || ''}</td>
<td class="text-center">${asset[ASSET_SCHEMA.ASSET_COUNT.key] || '1'}</td>
<td class="text-center">${formatInline(asset[ASSET_SCHEMA.MEMO.key]||'-')}</td>
`;
tr.addEventListener('click', () => alert('상세 정보 준비 중입니다.'));
tbody.appendChild(tr);
});
setupTableSorting(table, sortState, (key, dir) => {
sortState = { key, direction: dir };
updateTable();
});
createIcons({ icons: { RefreshCcw, Download, Upload, FileSpreadsheet, Plus } });
};
document.getElementById('filter-keyword')?.addEventListener('input', updateTable);
document.getElementById('btn-reset-filters')?.addEventListener('click', () => {
(document.getElementById('filter-keyword') as HTMLInputElement).value = '';
updateTable();
});
updateTable();
}

View File

@@ -1,34 +1,36 @@
import { state } from '../../core/state';
import { openHwModal } from '../../components/Modal/HWModal';
import { formatInline, createBadge, sortAssets, dynamicSort } from '../../core/utils';
import { formatInline, createBadge, sortAssets, dynamicSort, getActionButtonsHTML } from '../../core/utils';
import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema';
import { setupTableSorting, SortState } from '../../core/tableHandler';
import { createIcons, RefreshCcw } from 'lucide';
import { createIcons, Paperclip, RefreshCcw, Download, Upload, FileSpreadsheet, Plus } from 'lucide';
/**
* 모바일기기 자산 목록 뷰
* 라인 정렬 보정 및 헤더 통일
* 모바일 자산 목록 뷰 (레거시 지원용)
*/
export function renderMobileList(container: HTMLElement) {
const fullList = sortAssets(state.masterData.mobile);
// 모바일 데이터가 별도 테이블에 없으므로 일단 빈 배열 또는 장비군에서 필터링 시도
const fullList = sortAssets(state.masterData.mobile || []);
let sortState: SortState = { key: '', direction: 'asc' };
const filterBar = document.createElement('div');
filterBar.className = 'search-bar';
const corps = Array.from(new Set(fullList.map(a => a[ASSET_SCHEMA.CORP.key]))).filter(Boolean).sort();
const corps = Array.from(new Set(fullList.map((a: any) => a[ASSET_SCHEMA.PURCHASE_CORP.key]))).filter(Boolean).sort();
filterBar.innerHTML = `
<div class="search-item flex-1">
<label>통합 검색 (${ASSET_SCHEMA.ASSET_CODE.ui}/${ASSET_SCHEMA.MODEL.ui}/${ASSET_SCHEMA.MANAGER_MAIN.ui})</label>
<label>통합 검색 (${ASSET_SCHEMA.MODEL_NAME.ui})</label>
<input type="text" id="filter-keyword" placeholder="검색어를 입력하세요..." autocomplete="off">
</div>
<div class="search-item">
<label>${ASSET_SCHEMA.CORP.ui}</label>
<label>${ASSET_SCHEMA.PURCHASE_CORP.ui}</label>
<select id="filter-corp"><option value="">전체 법인</option>${corps.map(c => `<option value="${c}">${c}</option>`).join('')}</select>
</div>
<button id="btn-reset-filters" class="btn btn-outline btn-reset">
<i data-lucide="refresh-ccw"></i> ${UI_TEXT.ACTION.RESET_FILTER}
</button>
${getActionButtonsHTML()}
`;
container.appendChild(filterBar);
@@ -38,15 +40,14 @@ export function renderMobileList(container: HTMLElement) {
table.innerHTML = `
<thead>
<tr>
<th class="text-center" style="width:50px;">No.</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.STATUS.key}">${ASSET_SCHEMA.STATUS.ui}</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.CORP.key}">${ASSET_SCHEMA.CORP.ui}</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.ASSET_CODE.key}">${ASSET_SCHEMA.ASSET_CODE.ui}</th>
<th data-sort="${ASSET_SCHEMA.MODEL.key}">${ASSET_SCHEMA.MODEL.ui}</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.STORE_LOC.key}">${ASSET_SCHEMA.STORE_LOC.ui}</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.MANAGER_MAIN.key}">담당자(정/부)</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.PURCHASE_YM.key}">${ASSET_SCHEMA.PURCHASE_YM.ui}</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.PRICE.key}">${ASSET_SCHEMA.PRICE.ui}</th>
<th style="text-align:center; width:50px;">No</th>
<th style="text-align:center;" data-sort="${ASSET_SCHEMA.HW_STATUS.key}">${ASSET_SCHEMA.HW_STATUS.ui}</th>
<th style="text-align:center;" data-sort="${ASSET_SCHEMA.PURCHASE_CORP.key}">${ASSET_SCHEMA.PURCHASE_CORP.ui}</th>
<th style="text-align:center;" data-sort="${ASSET_SCHEMA.MODEL_NAME.key}">${ASSET_SCHEMA.MODEL_NAME.ui}</th>
<th style="text-align:center;" data-sort="${ASSET_SCHEMA.LOCATION.key}">${ASSET_SCHEMA.LOCATION.ui}</th>
<th style="text-align:center;" data-sort="${ASSET_SCHEMA.PURCHASE_DATE.key}">${ASSET_SCHEMA.PURCHASE_DATE.ui}</th>
<th style="text-align:center;" data-sort="${ASSET_SCHEMA.PURCHASE_AMOUNT.key}">${ASSET_SCHEMA.PURCHASE_AMOUNT.ui}</th>
<th style="text-align:center;">담당자</th>
</tr>
</thead>
<tbody id="dynamic-tbody"></tbody>
@@ -63,12 +64,10 @@ export function renderMobileList(container: HTMLElement) {
const keyword = keywordInput ? keywordInput.value.toLowerCase().trim() : '';
const corp = corpSelect ? corpSelect.value : '';
let filtered = fullList.filter(asset => {
let filtered = fullList.filter((asset: any) => {
const matchKeyword = !keyword ||
String(asset[ASSET_SCHEMA.ASSET_CODE.key]||'').toLowerCase().includes(keyword) ||
String(asset[ASSET_SCHEMA.MODEL.key]||'').toLowerCase().includes(keyword) ||
String(asset[ASSET_SCHEMA.MANAGER_MAIN.key]||'').toLowerCase().includes(keyword);
const matchCorp = !corp || asset[ASSET_SCHEMA.CORP.key] === corp;
String(asset[ASSET_SCHEMA.MODEL_NAME.key]||'').toLowerCase().includes(keyword);
const matchCorp = !corp || asset[ASSET_SCHEMA.PURCHASE_CORP.key] === corp;
return matchKeyword && matchCorp;
});
@@ -78,36 +77,25 @@ export function renderMobileList(container: HTMLElement) {
tbody.innerHTML = '';
if (filtered.length === 0) {
tbody.innerHTML = `<tr><td colspan="9" class="text-center" style="padding: 3rem; color: var(--text-muted);">${UI_TEXT.MESSAGES.NO_DATA}</td></tr>`;
tbody.innerHTML = `<tr><td colspan="8" style="text-align:center; padding: 3rem; color: var(--text-muted);">${UI_TEXT.MESSAGES.NO_DATA}</td></tr>`;
return;
}
filtered.forEach((asset, idx) => {
filtered.forEach((asset: any, idx: number) => {
const tr = document.createElement('tr');
tr.style.cursor = 'pointer';
const statusColors: Record<string, string> = { '대여중': 'primary', '보관중': 'success', '수리중': 'danger', '기타': 'muted' };
const statusValue = asset[ASSET_SCHEMA.STATUS.key] || '보관중';
const statusType = statusColors[statusValue] || 'muted';
const statusBadge = `<span class="badge badge-${statusType}">${statusValue}</span>`;
const mainManager = asset[ASSET_SCHEMA.MANAGER_MAIN.key] || '';
const subManager = asset[ASSET_SCHEMA.MANAGER_SUB.key] || '';
const managerHtml = [
mainManager ? `${createBadge('정', 'primary')} ${mainManager}` : '',
subManager ? `${createBadge('부', 'muted')} ${subManager}` : ''
].filter(v => v !== '').join(' / ');
tr.innerHTML = `
<td class="text-center">${idx + 1}</td>
<td class="text-center">${statusBadge}</td>
<td class="text-center">${asset[ASSET_SCHEMA.CORP.key]}</td>
<td class="text-center" style="font-family: monospace;">${asset[ASSET_SCHEMA.ASSET_CODE.key] || '-'}</td>
<td>${formatInline(asset[ASSET_SCHEMA.MODEL.key] || asset.)}</td>
<td class="text-center">${asset[ASSET_SCHEMA.STORE_LOC.key] || '-'}</td>
<td class="text-center">${managerHtml || '-'}</td>
<td class="text-center">${asset[ASSET_SCHEMA.PURCHASE_YM.key] || ''}</td>
<td class="text-right">${Number(asset[ASSET_SCHEMA.PRICE.key]||0).toLocaleString()}</td>
<td style="text-align:center;">${idx+1}</td>
<td style="text-align:center;">${asset[ASSET_SCHEMA.HW_STATUS.key] || '운영중'}</td>
<td style="text-align:center;">${asset[ASSET_SCHEMA.PURCHASE_CORP.key] || ''}</td>
<td>${asset[ASSET_SCHEMA.MODEL_NAME.key] || ''}</td>
<td style="text-align:center;">${asset[ASSET_SCHEMA.LOCATION.key] || '-'}</td>
<td style="text-align:center;">${asset[ASSET_SCHEMA.PURCHASE_DATE.key] || ''}</td>
<td style="text-align:right;">${Number(asset[ASSET_SCHEMA.PURCHASE_AMOUNT.key]||0).toLocaleString()}</td>
<td style="text-align:center;">${mainManager}</td>
`;
tr.addEventListener('click', () => openHwModal(asset, 'view'));
tbody.appendChild(tr);
@@ -118,7 +106,7 @@ export function renderMobileList(container: HTMLElement) {
updateTable();
});
createIcons({ icons: { RefreshCcw } });
createIcons({ icons: { Paperclip, RefreshCcw, Download, Upload, FileSpreadsheet, Plus } });
};
document.getElementById('filter-keyword')?.addEventListener('input', updateTable);

View File

@@ -0,0 +1,110 @@
import { state } from '../../core/state';
import { openHwModal } from '../../components/Modal/HWModal';
import { formatInline, createBadge, sortAssets, dynamicSort, getActionButtonsHTML } from '../../core/utils';
import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema';
import { setupTableSorting, SortState } from '../../core/tableHandler';
import { createIcons, RefreshCcw, Download, Upload, FileSpreadsheet, Plus } from 'lucide';
/**
* 네트워크 자산 목록 뷰
*/
export function renderNetworkList(container: HTMLElement) {
const fullList = sortAssets(state.masterData.network || []);
let sortState: SortState = { key: '', direction: 'asc' };
const filterBar = document.createElement('div');
filterBar.className = 'search-bar';
filterBar.innerHTML = `
<div class="search-item flex-1">
<label>통합 검색 (${ASSET_SCHEMA.MODEL_NAME.ui}/${ASSET_SCHEMA.MANAGER_MAIN.ui})</label>
<input type="text" id="filter-keyword" placeholder="검색어를 입력하세요..." autocomplete="off">
</div>
<button id="btn-reset-filters" class="btn btn-outline btn-reset">
<i data-lucide="refresh-ccw"></i> ${UI_TEXT.ACTION.RESET_FILTER}
</button>
${getActionButtonsHTML()}
`;
container.appendChild(filterBar);
const tableWrapper = document.createElement('div');
tableWrapper.className = 'table-container';
const table = document.createElement('table');
table.innerHTML = `
<thead>
<tr>
<th class="text-center" style="width:50px;">No.</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.HW_STATUS.key}">${ASSET_SCHEMA.HW_STATUS.ui}</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.MANAGER_MAIN.key}">현 사용자</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.ASSET_TYPE.key}">${ASSET_SCHEMA.ASSET_TYPE.ui}</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.ASSET_MFR.key}">${ASSET_SCHEMA.ASSET_MFR.ui}</th>
<th data-sort="${ASSET_SCHEMA.MODEL_NAME.key}">${ASSET_SCHEMA.MODEL_NAME.ui}</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.ASSET_COUNT.key}">${ASSET_SCHEMA.ASSET_COUNT.ui}</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.LOCATION.key}">${ASSET_SCHEMA.LOCATION.ui}(건물)</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.LOC_DETAIL.key}">${ASSET_SCHEMA.LOC_DETAIL.ui}</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.MEMO.key}">${ASSET_SCHEMA.MEMO.ui}</th>
</tr>
</thead>
<tbody id="dynamic-tbody"></tbody>
`;
tableWrapper.appendChild(table);
container.appendChild(tableWrapper);
const tbody = table.querySelector('tbody')!;
const updateTable = () => {
const keywordInput = document.getElementById('filter-keyword') as HTMLInputElement;
const keyword = keywordInput ? keywordInput.value.toLowerCase().trim() : '';
let filtered = fullList.filter(asset => {
const matchKeyword = !keyword ||
String(asset[ASSET_SCHEMA.MODEL_NAME.key]||'').toLowerCase().includes(keyword) ||
String(asset[ASSET_SCHEMA.MANAGER_MAIN.key]||'').toLowerCase().includes(keyword) ||
String(asset[ASSET_SCHEMA.ASSET_MFR.key]||'').toLowerCase().includes(keyword);
return matchKeyword;
});
if (sortState.key) {
filtered = dynamicSort(filtered, sortState.key, sortState.direction);
}
tbody.innerHTML = '';
if (filtered.length === 0) {
tbody.innerHTML = `<tr><td colspan="10" class="text-center" style="padding: 3rem; color: var(--text-muted);">${UI_TEXT.MESSAGES.NO_DATA}</td></tr>`;
return;
}
filtered.forEach((asset, idx) => {
const tr = document.createElement('tr');
tr.style.cursor = 'pointer';
tr.innerHTML = `
<td class="text-center">${idx + 1}</td>
<td class="text-center"><span class="badge badge-success">${asset[ASSET_SCHEMA.HW_STATUS.key] || '운영중'}</span></td>
<td class="text-center">${asset[ASSET_SCHEMA.MANAGER_MAIN.key] || '-'}</td>
<td class="text-center">${asset[ASSET_SCHEMA.ASSET_TYPE.key] || ''}</td>
<td class="text-center">${asset[ASSET_SCHEMA.ASSET_MFR.key] || ''}</td>
<td>${formatInline(asset[ASSET_SCHEMA.MODEL_NAME.key] || '-')}</td>
<td class="text-center">${asset[ASSET_SCHEMA.ASSET_COUNT.key] || '1'}</td>
<td class="text-center">${asset[ASSET_SCHEMA.LOCATION.key] || '-'}</td>
<td class="text-center">${asset[ASSET_SCHEMA.LOC_DETAIL.key] || '-'}</td>
<td class="text-center">${formatInline(asset[ASSET_SCHEMA.MEMO.key]||'-')}</td>
`;
tr.addEventListener('click', () => openHwModal(asset, 'view'));
tbody.appendChild(tr);
});
setupTableSorting(table, sortState, (key, dir) => {
sortState = { key, direction: dir };
updateTable();
});
createIcons({ icons: { RefreshCcw, Download, Upload, FileSpreadsheet, Plus } });
};
document.getElementById('filter-keyword')?.addEventListener('input', updateTable);
document.getElementById('btn-reset-filters')?.addEventListener('click', () => {
(document.getElementById('filter-keyword') as HTMLInputElement).value = '';
updateTable();
});
updateTable();
}

View File

@@ -1,9 +1,9 @@
import { state } from '../../core/state';
import { openHwModal } from '../../components/Modal/HWModal';
import { formatInline, createBadge, sortAssets, dynamicSort } from '../../core/utils';
import { formatInline, createBadge, sortAssets, dynamicSort, getActionButtonsHTML } from '../../core/utils';
import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema';
import { setupTableSorting, SortState } from '../../core/tableHandler';
import { createIcons, Paperclip, RefreshCcw } from 'lucide';
import { createIcons, Paperclip, RefreshCcw, Download, Upload, FileSpreadsheet, Plus } from 'lucide';
/**
* PC 자산 목록 뷰
@@ -16,20 +16,21 @@ export function renderPcList(container: HTMLElement) {
const filterBar = document.createElement('div');
filterBar.className = 'search-bar';
const corps = Array.from(new Set(fullList.map(a => a[ASSET_SCHEMA.CORP.key]))).filter(Boolean).sort();
const corps = Array.from(new Set(fullList.map(a => a[ASSET_SCHEMA.PURCHASE_CORP.key]))).filter(Boolean).sort();
filterBar.innerHTML = `
<div class="search-item flex-1">
<label>통합 검색 (${ASSET_SCHEMA.ASSET_CODE.ui}/${ASSET_SCHEMA.USER.ui}/${ASSET_SCHEMA.MANAGER_MAIN.ui})</label>
<label>통합 검색 (${ASSET_SCHEMA.MODEL_NAME.ui}/${ASSET_SCHEMA.MANAGER_MAIN.ui}/${ASSET_SCHEMA.CURRENT_USER.ui})</label>
<input type="text" id="filter-keyword" placeholder="검색어를 입력하세요..." autocomplete="off">
</div>
<div class="search-item">
<label>${ASSET_SCHEMA.CORP.ui}</label>
<label>${ASSET_SCHEMA.PURCHASE_CORP.ui}</label>
<select id="filter-corp"><option value="">전체 법인</option>${corps.map(c => `<option value="${c}">${c}</option>`).join('')}</select>
</div>
<button id="btn-reset-filters" class="btn btn-outline btn-reset">
<i data-lucide="refresh-ccw"></i> ${UI_TEXT.ACTION.RESET_FILTER}
</button>
${getActionButtonsHTML()}
`;
container.appendChild(filterBar);
@@ -40,18 +41,19 @@ export function renderPcList(container: HTMLElement) {
<thead>
<tr>
<th style="text-align:center; width:50px;">No</th>
<th style="text-align:center;" data-sort="${ASSET_SCHEMA.CORP.key}">${ASSET_SCHEMA.CORP.ui}</th>
<th style="text-align:center;" data-sort="${ASSET_SCHEMA.ORG.key}">${ASSET_SCHEMA.ORG.ui}</th>
<th style="text-align:center;" data-sort="${ASSET_SCHEMA.ASSET_CODE.key}">${ASSET_SCHEMA.ASSET_CODE.ui}</th>
<th style="text-align:center;" data-sort="${ASSET_SCHEMA.USER.key}">${ASSET_SCHEMA.USER.ui}</th>
<th style="text-align:center;" data-sort="${ASSET_SCHEMA.MAINBOARD.key}">${ASSET_SCHEMA.MAINBOARD.ui}</th>
<th style="text-align:center;" data-sort="${ASSET_SCHEMA.CURRENT_DEPT.key}">${ASSET_SCHEMA.CURRENT_DEPT.ui}</th>
<th style="text-align:center;" data-sort="${ASSET_SCHEMA.CURRENT_USER.key}">${ASSET_SCHEMA.CURRENT_USER.ui}</th>
<th style="text-align:center;" data-sort="${ASSET_SCHEMA.MANAGER_MAIN.key}">${ASSET_SCHEMA.MANAGER_MAIN.ui}</th>
<th style="text-align:center;" data-sort="${ASSET_SCHEMA.CPU.key}">${ASSET_SCHEMA.CPU.ui}</th>
<th style="text-align:center;" data-sort="${ASSET_SCHEMA.MAINBOARD.key}">${ASSET_SCHEMA.MAINBOARD.ui}</th>
<th style="text-align:center;" data-sort="${ASSET_SCHEMA.RAM.key}">${ASSET_SCHEMA.RAM.ui}</th>
<th style="text-align:center;" data-sort="${ASSET_SCHEMA.STORAGE1.key}">Storage</th>
<th style="text-align:center;" data-sort="${ASSET_SCHEMA.PURCHASE_YM.key}">${ASSET_SCHEMA.PURCHASE_YM.ui}</th>
<th style="text-align:center;" data-sort="${ASSET_SCHEMA.PRICE.key}">${ASSET_SCHEMA.PRICE.ui}</th>
<th style="text-align:center;">${ASSET_SCHEMA.DOC_NAME.ui}</th>
<th style="text-align:center;" data-sort="${ASSET_SCHEMA.MANAGER_MAIN.key}">담당자(정/부)</th>
<th style="text-align:center;" data-sort="${ASSET_SCHEMA.GPU.key}">${ASSET_SCHEMA.GPU.ui}</th>
<th style="text-align:center;" data-sort="${ASSET_SCHEMA.SSD1.key}">SSD1</th>
<th style="text-align:center;" data-sort="${ASSET_SCHEMA.SSD2.key}">SSD2</th>
<th style="text-align:center;" data-sort="${ASSET_SCHEMA.HDD1.key}">HDD1</th>
<th style="text-align:center;" data-sort="${ASSET_SCHEMA.HDD2.key}">HDD2</th>
<th style="text-align:center;" data-sort="${ASSET_SCHEMA.MAC_ADDR.key}">${ASSET_SCHEMA.MAC_ADDR.ui}</th>
<th style="text-align:center;" data-sort="${ASSET_SCHEMA.MEMO.key}">${ASSET_SCHEMA.MEMO.ui}</th>
</tr>
</thead>
<tbody id="dynamic-tbody"></tbody>
@@ -70,11 +72,12 @@ export function renderPcList(container: HTMLElement) {
let filtered = fullList.filter(asset => {
const matchKeyword = !keyword ||
String(asset[ASSET_SCHEMA.ASSET_CODE.key]||'').toLowerCase().includes(keyword) ||
String(asset[ASSET_SCHEMA.USER.key]||'').toLowerCase().includes(keyword) ||
String(asset[ASSET_SCHEMA.ORG.key]||'').toLowerCase().includes(keyword) ||
String(asset[ASSET_SCHEMA.MANAGER_MAIN.key]||'').toLowerCase().includes(keyword);
const matchCorp = !corp || asset[ASSET_SCHEMA.CORP.key] === corp;
String(asset[ASSET_SCHEMA.MANAGER_MAIN.key]||'').toLowerCase().includes(keyword) ||
String(asset[ASSET_SCHEMA.CURRENT_DEPT.key]||'').toLowerCase().includes(keyword) ||
String(asset[ASSET_SCHEMA.MODEL_NAME.key]||'').toLowerCase().includes(keyword) ||
String(asset[ASSET_SCHEMA.MAC_ADDR.key]||'').toLowerCase().includes(keyword) ||
String(asset[ASSET_SCHEMA.CURRENT_USER.key]||'').toLowerCase().includes(keyword);
const matchCorp = !corp || asset[ASSET_SCHEMA.PURCHASE_CORP.key] === corp;
return matchKeyword && matchCorp;
});
@@ -84,7 +87,7 @@ export function renderPcList(container: HTMLElement) {
tbody.innerHTML = '';
if (filtered.length === 0) {
tbody.innerHTML = `<tr><td colspan="13" style="text-align:center; padding: 3rem; color: var(--text-muted);">${UI_TEXT.MESSAGES.NO_DATA}</td></tr>`;
tbody.innerHTML = `<tr><td colspan="14" style="text-align:center; padding: 3rem; color: var(--text-muted);">${UI_TEXT.MESSAGES.NO_DATA}</td></tr>`;
return;
}
@@ -92,29 +95,21 @@ export function renderPcList(container: HTMLElement) {
const tr = document.createElement('tr');
tr.style.cursor = 'pointer';
const storage = [asset[ASSET_SCHEMA.STORAGE1.key], asset[ASSET_SCHEMA.STORAGE2.key]].filter(v => v).join(' / ');
const mainManager = asset[ASSET_SCHEMA.MANAGER_MAIN.key] || '';
const subManager = asset[ASSET_SCHEMA.MANAGER_SUB.key] || '';
const managerHtml = [
mainManager ? `${createBadge('정', 'primary')} ${mainManager}` : '',
subManager ? `${createBadge('부', 'muted')} ${subManager}` : ''
].filter(v => v !== '').join(' / ');
tr.innerHTML = `
<td style="text-align:center;">${idx+1}</td>
<td style="text-align:center;">${asset[ASSET_SCHEMA.CORP.key]}</td>
<td style="text-align:center;">${asset[ASSET_SCHEMA.ORG.key]||'-'}</td>
<td style="text-align:center;">${asset[ASSET_SCHEMA.ASSET_CODE.key]}</td>
<td style="text-align:center;">${asset[ASSET_SCHEMA.USER.key]||''}</td>
<td style="text-align:center;">${asset[ASSET_SCHEMA.MAINBOARD.key]||'-'}</td>
<td style="text-align:center;">${asset[ASSET_SCHEMA.CURRENT_DEPT.key]||'-'}</td>
<td style="text-align:center;">${asset[ASSET_SCHEMA.CURRENT_USER.key]||'-'}</td>
<td style="text-align:center;">${asset[ASSET_SCHEMA.MANAGER_MAIN.key]||'-'}</td>
<td style="text-align:center;">${asset[ASSET_SCHEMA.CPU.key]||''}</td>
<td style="text-align:center;">${asset[ASSET_SCHEMA.MAINBOARD.key]||'-'}</td>
<td style="text-align:center;">${asset[ASSET_SCHEMA.RAM.key]||''}</td>
<td style="text-align:center;">${formatInline(storage)}</td>
<td style="text-align:center;">${asset[ASSET_SCHEMA.PURCHASE_YM.key] || ''}</td>
<td style="text-align:right;">${Number(asset[ASSET_SCHEMA.PRICE.key]||0).toLocaleString()}</td>
<td style="text-align:center;">${asset[ASSET_SCHEMA.DOC_NAME.key] ? '<i data-lucide="paperclip" class="text-primary"></i>' : '-'}</td>
<td style="text-align:center;">${managerHtml || '-'}</td>
<td style="text-align:center;">${asset[ASSET_SCHEMA.GPU.key]||'-'}</td>
<td style="text-align:center;">${asset[ASSET_SCHEMA.SSD1.key]||'-'}</td>
<td style="text-align:center;">${asset[ASSET_SCHEMA.SSD2.key]||'-'}</td>
<td style="text-align:center;">${asset[ASSET_SCHEMA.HDD1.key]||'-'}</td>
<td style="text-align:center;">${asset[ASSET_SCHEMA.HDD2.key]||'-'}</td>
<td style="text-align:center; font-family:monospace; font-size:11px;">${asset[ASSET_SCHEMA.MAC_ADDR.key]||'-'}</td>
<td style="text-align:center;">${formatInline(asset[ASSET_SCHEMA.MEMO.key]||'-')}</td>
`;
tr.addEventListener('click', () => openHwModal(asset, 'view'));
tbody.appendChild(tr);

View File

@@ -0,0 +1,112 @@
import { state } from '../../core/state';
import { openHwModal } from '../../components/Modal/HWModal';
import { formatInline, sortAssets, dynamicSort, getActionButtonsHTML } from '../../core/utils';
import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema';
import { setupTableSorting, SortState } from '../../core/tableHandler';
import { createIcons, RefreshCcw, Download, Upload, FileSpreadsheet, Plus } from 'lucide';
/**
* PC부품 자산 목록 뷰
*/
export function renderPcPartList(container: HTMLElement) {
// PC부품 데이터는 survey 또는 별도 테이블에 있을 수 있음. 여기선 equipment에서 필터링하거나 빈 배열 지원
const fullList = sortAssets(state.masterData.equipment?.filter((a: any) => a.category === 'PC부품') || []);
let sortState: SortState = { key: '', direction: 'asc' };
const filterBar = document.createElement('div');
filterBar.className = 'search-bar';
filterBar.innerHTML = `
<div class="search-item flex-1">
<label>통합 검색 (${ASSET_SCHEMA.MODEL_NAME.ui})</label>
<input type="text" id="filter-keyword" placeholder="검색어를 입력하세요..." autocomplete="off">
</div>
<button id="btn-reset-filters" class="btn btn-outline btn-reset">
<i data-lucide="refresh-ccw"></i> ${UI_TEXT.ACTION.RESET_FILTER}
</button>
${getActionButtonsHTML()}
`;
container.appendChild(filterBar);
const tableWrapper = document.createElement('div');
tableWrapper.className = 'table-container';
const table = document.createElement('table');
table.innerHTML = `
<thead>
<tr>
<th class="text-center" style="width:50px;">No.</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.HW_STATUS.key}">${ASSET_SCHEMA.HW_STATUS.ui}</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.ASSET_TYPE.key}">${ASSET_SCHEMA.ASSET_TYPE.ui}</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.ASSET_MFR.key}">${ASSET_SCHEMA.ASSET_MFR.ui}</th>
<th data-sort="${ASSET_SCHEMA.MODEL_NAME.key}">${ASSET_SCHEMA.MODEL_NAME.ui}</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.VOLUME.key}">${ASSET_SCHEMA.VOLUME.ui}</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.MONITOR_INCH.key}">${ASSET_SCHEMA.MONITOR_INCH.ui}</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.ASSET_COUNT.key}">${ASSET_SCHEMA.ASSET_COUNT.ui}</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.LOCATION.key}">${ASSET_SCHEMA.LOCATION.ui}(건물)</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.LOC_DETAIL.key}">${ASSET_SCHEMA.LOC_DETAIL.ui}</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.MEMO.key}">${ASSET_SCHEMA.MEMO.ui}</th>
</tr>
</thead>
<tbody id="dynamic-tbody"></tbody>
`;
tableWrapper.appendChild(table);
container.appendChild(tableWrapper);
const tbody = table.querySelector('tbody')!;
const updateTable = () => {
const keywordInput = document.getElementById('filter-keyword') as HTMLInputElement;
const keyword = keywordInput ? keywordInput.value.toLowerCase().trim() : '';
let filtered = fullList.filter(asset => {
const matchKeyword = !keyword ||
String(asset[ASSET_SCHEMA.MODEL_NAME.key]||'').toLowerCase().includes(keyword) ||
String(asset[ASSET_SCHEMA.ASSET_TYPE.key]||'').toLowerCase().includes(keyword);
return matchKeyword;
});
if (sortState.key) {
filtered = dynamicSort(filtered, sortState.key, sortState.direction);
}
tbody.innerHTML = '';
if (filtered.length === 0) {
tbody.innerHTML = `<tr><td colspan="11" class="text-center" style="padding: 3rem; color: var(--text-muted);">${UI_TEXT.MESSAGES.NO_DATA}</td></tr>`;
return;
}
filtered.forEach((asset, idx) => {
const tr = document.createElement('tr');
tr.style.cursor = 'pointer';
tr.innerHTML = `
<td class="text-center">${idx + 1}</td>
<td class="text-center"><span class="badge badge-success">${asset[ASSET_SCHEMA.HW_STATUS.key] || '보관중'}</span></td>
<td class="text-center">${asset[ASSET_SCHEMA.ASSET_TYPE.key] || ''}</td>
<td class="text-center">${asset[ASSET_SCHEMA.ASSET_MFR.key] || ''}</td>
<td>${formatInline(asset[ASSET_SCHEMA.MODEL_NAME.key] || '-')}</td>
<td class="text-center">${asset[ASSET_SCHEMA.VOLUME.key] || '-'}</td>
<td class="text-center">${asset[ASSET_SCHEMA.MONITOR_INCH.key] || '-'}</td>
<td class="text-center">${asset[ASSET_SCHEMA.ASSET_COUNT.key] || '1'}</td>
<td class="text-center">${asset[ASSET_SCHEMA.LOCATION.key] || '-'}</td>
<td class="text-center">${asset[ASSET_SCHEMA.LOC_DETAIL.key] || '-'}</td>
<td class="text-center">${formatInline(asset[ASSET_SCHEMA.MEMO.key]||'-')}</td>
`;
tr.addEventListener('click', () => openHwModal(asset, 'view'));
tbody.appendChild(tr);
});
setupTableSorting(table, sortState, (key, dir) => {
sortState = { key, direction: dir };
updateTable();
});
createIcons({ icons: { RefreshCcw, Download, Upload, FileSpreadsheet, Plus } });
};
document.getElementById('filter-keyword')?.addEventListener('input', updateTable);
document.getElementById('btn-reset-filters')?.addEventListener('click', () => {
(document.getElementById('filter-keyword') as HTMLInputElement).value = '';
updateTable();
});
updateTable();
}

View File

@@ -1,9 +1,9 @@
import { state } from '../../core/state';
import { openHwModal } from '../../components/Modal/HWModal';
import { formatInline, createBadge, sortAssets, dynamicSort } from '../../core/utils';
import { formatInline, createBadge, sortAssets, dynamicSort, getActionButtonsHTML } from '../../core/utils';
import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema';
import { setupTableSorting, SortState } from '../../core/tableHandler';
import { createIcons, RefreshCcw } from 'lucide';
import { createIcons, RefreshCcw, Download, Upload, FileSpreadsheet, Plus } from 'lucide';
/**
* 서버 자산 목록 뷰
@@ -16,25 +16,26 @@ export function renderServerList(container: HTMLElement) {
const filterBar = document.createElement('div');
filterBar.className = 'search-bar';
const corps = Array.from(new Set(fullList.map(a => a[ASSET_SCHEMA.CORP.key]))).filter(Boolean).sort();
const orgUnits = Array.from(new Set(fullList.map(a => a[ASSET_SCHEMA.ORG.key]))).filter(Boolean).sort();
const corps = Array.from(new Set(fullList.map(a => a[ASSET_SCHEMA.PURCHASE_CORP.key]))).filter(Boolean).sort();
const orgUnits = Array.from(new Set(fullList.map(a => a[ASSET_SCHEMA.CURRENT_DEPT.key]))).filter(Boolean).sort();
filterBar.innerHTML = `
<div class="search-item flex-1">
<label>통합 검색 (${ASSET_SCHEMA.ASSET_CODE.ui}/${ASSET_SCHEMA.ORG.ui}/${ASSET_SCHEMA.MODEL.ui})</label>
<label>통합 검색 (${ASSET_SCHEMA.CURRENT_DEPT.ui}/${ASSET_SCHEMA.MODEL_NAME.ui})</label>
<input type="text" id="filter-keyword" placeholder="검색어를 입력하세요..." autocomplete="off">
</div>
<div class="search-item">
<label>${ASSET_SCHEMA.CORP.ui}</label>
<label>${ASSET_SCHEMA.PURCHASE_CORP.ui}</label>
<select id="filter-corp"><option value="">전체 법인</option>${corps.map(c => `<option value="${c}">${c}</option>`).join('')}</select>
</div>
<div class="search-item">
<label>${ASSET_SCHEMA.ORG.ui}</label>
<label>${ASSET_SCHEMA.CURRENT_DEPT.ui}</label>
<select id="filter-org-unit"><option value="">전체 조직</option>${orgUnits.map(o => `<option value="${o}">${o}</option>`).join('')}</select>
</div>
<button id="btn-reset-filters" class="btn btn-outline btn-reset">
<i data-lucide="refresh-ccw"></i> ${UI_TEXT.ACTION.RESET_FILTER}
</button>
${getActionButtonsHTML()}
`;
container.appendChild(filterBar);
@@ -45,13 +46,12 @@ export function renderServerList(container: HTMLElement) {
<thead>
<tr>
<th class="text-center" style="width:50px;">No</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.CORP.key}">${ASSET_SCHEMA.CORP.ui}</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.ORG.key}">${ASSET_SCHEMA.ORG.ui}</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.ASSET_CODE.key}">${ASSET_SCHEMA.ASSET_CODE.ui}</th>
<th data-sort="${ASSET_SCHEMA.DETAIL_PURPOSE.key}">${ASSET_SCHEMA.DETAIL_PURPOSE.ui}</th>
<th data-sort="상세">상세</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.LOCATION.key}">${ASSET_SCHEMA.LOCATION.ui}</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.MANAGER_MAIN.key}">담당자(정/부)</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.CURRENT_DEPT.key}">${ASSET_SCHEMA.CURRENT_DEPT.ui}</th>
<th data-sort="${ASSET_SCHEMA.ASSET_PURPOSE.key}">${ASSET_SCHEMA.ASSET_PURPOSE.ui}</th>
<th data-sort="${ASSET_SCHEMA.MODEL_NAME.key}">${ASSET_SCHEMA.MODEL_NAME.ui}</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.LOCATION.key}">${ASSET_SCHEMA.LOCATION.ui}(건물)</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.LOC_DETAIL.key}">${ASSET_SCHEMA.LOC_DETAIL.ui}</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.MEMO.key}">${ASSET_SCHEMA.MEMO.ui}</th>
</tr>
</thead>
<tbody id="dynamic-tbody"></tbody>
@@ -72,11 +72,11 @@ export function renderServerList(container: HTMLElement) {
let filtered = fullList.filter(asset => {
const matchKeyword = !keyword ||
String(asset[ASSET_SCHEMA.ASSET_CODE.key]||'').toLowerCase().includes(keyword) ||
String(asset[ASSET_SCHEMA.ORG.key]||'').toLowerCase().includes(keyword) ||
String(asset[ASSET_SCHEMA.MODEL.key]||'').toLowerCase().includes(keyword);
const matchCorp = !corp || asset[ASSET_SCHEMA.CORP.key] === corp;
const matchOrg = !orgUnit || asset[ASSET_SCHEMA.ORG.key] === orgUnit;
String(asset[ASSET_SCHEMA.CURRENT_DEPT.key]||'').toLowerCase().includes(keyword) ||
String(asset[ASSET_SCHEMA.MODEL_NAME.key]||'').toLowerCase().includes(keyword) ||
String(asset[ASSET_SCHEMA.ASSET_PURPOSE.key]||'').toLowerCase().includes(keyword);
const matchCorp = !corp || asset[ASSET_SCHEMA.PURCHASE_CORP.key] === corp;
const matchOrg = !orgUnit || asset[ASSET_SCHEMA.CURRENT_DEPT.key] === orgUnit;
return matchKeyword && matchCorp && matchOrg;
});
@@ -86,7 +86,7 @@ export function renderServerList(container: HTMLElement) {
tbody.innerHTML = '';
if (filtered.length === 0) {
tbody.innerHTML = `<tr><td colspan="8" class="text-center" style="padding: 3rem; color: var(--text-muted);">${UI_TEXT.MESSAGES.NO_DATA}</td></tr>`;
tbody.innerHTML = `<tr><td colspan="7" class="text-center" style="padding: 3rem; color: var(--text-muted);">${UI_TEXT.MESSAGES.NO_DATA}</td></tr>`;
return;
}
@@ -94,22 +94,14 @@ export function renderServerList(container: HTMLElement) {
const tr = document.createElement('tr');
tr.style.cursor = 'pointer';
const mainManager = asset[ASSET_SCHEMA.MANAGER_MAIN.key] || '';
const subManager = asset[ASSET_SCHEMA.MANAGER_SUB.key] || '';
const managerHtml = [
mainManager ? `${createBadge('정', 'primary')} ${mainManager}` : '',
subManager ? `${createBadge('부', 'muted')} ${subManager}` : ''
].filter(v => v !== '').join(' / ');
tr.innerHTML = `
<td class="text-center">${idx+1}</td>
<td class="text-center">${asset[ASSET_SCHEMA.CORP.key]}</td>
<td class="text-center">${asset[ASSET_SCHEMA.ORG.key]||'-'}</td>
<td class="text-center">${asset[ASSET_SCHEMA.ASSET_CODE.key]}</td>
<td>${formatInline(asset[ASSET_SCHEMA.DETAIL_PURPOSE.key])}</td>
<td>${formatInline(asset.)}</td>
<td class="text-center">${asset[ASSET_SCHEMA.CURRENT_DEPT.key]||'-'}</td>
<td>${formatInline(asset[ASSET_SCHEMA.ASSET_PURPOSE.key])}</td>
<td>${formatInline(asset[ASSET_SCHEMA.MODEL_NAME.key]||asset[ASSET_SCHEMA.ASSET_NAME.key]||'-')}</td>
<td class="text-center">${formatInline(asset[ASSET_SCHEMA.LOCATION.key])}</td>
<td class="text-center">${managerHtml || '-'}</td>
<td class="text-center">${formatInline(asset[ASSET_SCHEMA.LOC_DETAIL.key]||'-')}</td>
<td class="text-center">${formatInline(asset[ASSET_SCHEMA.MEMO.key]||'-')}</td>
`;
tr.addEventListener('click', () => openHwModal(asset, 'view'));
tbody.appendChild(tr);
@@ -132,5 +124,5 @@ export function renderServerList(container: HTMLElement) {
});
updateTable();
createIcons({ icons: { RefreshCcw } });
createIcons({ icons: { RefreshCcw, Download, Upload, FileSpreadsheet, Plus } });
}

View File

@@ -0,0 +1,106 @@
import { state } from '../../core/state';
import { openHwModal } from '../../components/Modal/HWModal';
import { formatInline, sortAssets, dynamicSort, getActionButtonsHTML } from '../../core/utils';
import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema';
import { setupTableSorting, SortState } from '../../core/tableHandler';
import { createIcons, RefreshCcw, Download, Upload, FileSpreadsheet, Plus } from 'lucide';
/**
* 공간정보장비 자산 목록 뷰
*/
export function renderSpaceInfoList(container: HTMLElement) {
// 공간정보장비 데이터는 survey 또는 별도 테이블에 있을 수 있음.
const fullList = sortAssets(state.masterData.equipment?.filter((a: any) => a.category === '공간정보장비') || []);
let sortState: SortState = { key: '', direction: 'asc' };
const filterBar = document.createElement('div');
filterBar.className = 'search-bar';
filterBar.innerHTML = `
<div class="search-item flex-1">
<label>통합 검색 (${ASSET_SCHEMA.MODEL_NAME.ui}/${ASSET_SCHEMA.MANAGER_MAIN.ui})</label>
<input type="text" id="filter-keyword" placeholder="검색어를 입력하세요..." autocomplete="off">
</div>
<button id="btn-reset-filters" class="btn btn-outline btn-reset">
<i data-lucide="refresh-ccw"></i> ${UI_TEXT.ACTION.RESET_FILTER}
</button>
${getActionButtonsHTML()}
`;
container.appendChild(filterBar);
const tableWrapper = document.createElement('div');
tableWrapper.className = 'table-container';
const table = document.createElement('table');
table.innerHTML = `
<thead>
<tr>
<th class="text-center" style="width:50px;">No.</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.HW_STATUS.key}">${ASSET_SCHEMA.HW_STATUS.ui}</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.MANAGER_MAIN.key}">현 사용자</th>
<th data-sort="${ASSET_SCHEMA.PRODUCT_NAME.key}">자산명</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.ASSET_TYPE.key}">${ASSET_SCHEMA.ASSET_TYPE.ui}</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.LOCATION.key}">${ASSET_SCHEMA.LOCATION.ui}(건물)</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.LOC_DETAIL.key}">${ASSET_SCHEMA.LOC_DETAIL.ui}</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.MEMO.key}">${ASSET_SCHEMA.MEMO.ui}</th>
</tr>
</thead>
<tbody id="dynamic-tbody"></tbody>
`;
tableWrapper.appendChild(table);
container.appendChild(tableWrapper);
const tbody = table.querySelector('tbody')!;
const updateTable = () => {
const keywordInput = document.getElementById('filter-keyword') as HTMLInputElement;
const keyword = keywordInput ? keywordInput.value.toLowerCase().trim() : '';
let filtered = fullList.filter(asset => {
const matchKeyword = !keyword ||
String(asset[ASSET_SCHEMA.MODEL_NAME.key]||asset[ASSET_SCHEMA.PRODUCT_NAME.key]||'').toLowerCase().includes(keyword) ||
String(asset[ASSET_SCHEMA.MANAGER_MAIN.key]||'').toLowerCase().includes(keyword);
return matchKeyword;
});
if (sortState.key) {
filtered = dynamicSort(filtered, sortState.key, sortState.direction);
}
tbody.innerHTML = '';
if (filtered.length === 0) {
tbody.innerHTML = `<tr><td colspan="8" class="text-center" style="padding: 3rem; color: var(--text-muted);">${UI_TEXT.MESSAGES.NO_DATA}</td></tr>`;
return;
}
filtered.forEach((asset, idx) => {
const tr = document.createElement('tr');
tr.style.cursor = 'pointer';
tr.innerHTML = `
<td class="text-center">${idx + 1}</td>
<td class="text-center"><span class="badge badge-success">${asset[ASSET_SCHEMA.HW_STATUS.key] || '운영중'}</span></td>
<td class="text-center">${asset[ASSET_SCHEMA.MANAGER_MAIN.key] || '-'}</td>
<td>${formatInline(asset[ASSET_SCHEMA.PRODUCT_NAME.key] || asset[ASSET_SCHEMA.MODEL_NAME.key] || '-')}</td>
<td class="text-center">${asset[ASSET_SCHEMA.ASSET_TYPE.key] || ''}</td>
<td class="text-center">${asset[ASSET_SCHEMA.LOCATION.key] || '-'}</td>
<td class="text-center">${asset[ASSET_SCHEMA.LOC_DETAIL.key] || '-'}</td>
<td class="text-center">${formatInline(asset[ASSET_SCHEMA.MEMO.key]||'-')}</td>
`;
tr.addEventListener('click', () => openHwModal(asset, 'view'));
tbody.appendChild(tr);
});
setupTableSorting(table, sortState, (key, dir) => {
sortState = { key, direction: dir };
updateTable();
});
createIcons({ icons: { RefreshCcw, Download, Upload, FileSpreadsheet, Plus } });
};
document.getElementById('filter-keyword')?.addEventListener('input', updateTable);
document.getElementById('btn-reset-filters')?.addEventListener('click', () => {
(document.getElementById('filter-keyword') as HTMLInputElement).value = '';
updateTable();
});
updateTable();
}

View File

@@ -1,9 +1,9 @@
import { state } from '../../core/state';
import { openHwModal } from '../../components/Modal/HWModal';
import { formatInline, createBadge, sortAssets, dynamicSort } from '../../core/utils';
import { formatInline, createBadge, sortAssets, dynamicSort, getActionButtonsHTML } from '../../core/utils';
import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema';
import { setupTableSorting, SortState } from '../../core/tableHandler';
import { createIcons, RefreshCcw } from 'lucide';
import { createIcons, RefreshCcw, Download, Upload, FileSpreadsheet, Plus } from 'lucide';
/**
* 스토리지 자산 목록 뷰
@@ -16,25 +16,26 @@ export function renderStorageList(container: HTMLElement) {
const filterBar = document.createElement('div');
filterBar.className = 'search-bar';
const corps = Array.from(new Set(fullList.map(a => (a as any)[ASSET_SCHEMA.CORP.key]))).filter(Boolean).sort();
const orgUnits = Array.from(new Set(fullList.map(a => (a as any)[ASSET_SCHEMA.ORG.key]))).filter(Boolean).sort();
const corps = Array.from(new Set(fullList.map(a => a[ASSET_SCHEMA.PURCHASE_CORP.key]))).filter(Boolean).sort();
const orgUnits = Array.from(new Set(fullList.map(a => a[ASSET_SCHEMA.CURRENT_DEPT.key]))).filter(Boolean).sort();
filterBar.innerHTML = `
<div class="search-item flex-1">
<label>통합 검색 (${ASSET_SCHEMA.ASSET_CODE.ui}/${ASSET_SCHEMA.ORG.ui})</label>
<label>통합 검색 (${ASSET_SCHEMA.CURRENT_DEPT.ui})</label>
<input type="text" id="filter-keyword" placeholder="검색어를 입력하세요..." autocomplete="off">
</div>
<div class="search-item">
<label>${ASSET_SCHEMA.CORP.ui}</label>
<label>${ASSET_SCHEMA.PURCHASE_CORP.ui}</label>
<select id="filter-corp"><option value="">전체 법인</option>${corps.map(c => `<option value="${c}">${c}</option>`).join('')}</select>
</div>
<div class="search-item">
<label>${ASSET_SCHEMA.ORG.ui}</label>
<label>${ASSET_SCHEMA.CURRENT_DEPT.ui}</label>
<select id="filter-org-unit"><option value="">전체 조직</option>${orgUnits.map(o => `<option value="${o}">${o}</option>`).join('')}</select>
</div>
<button id="btn-reset-filters" class="btn btn-outline btn-reset">
<i data-lucide="refresh-ccw"></i> ${UI_TEXT.ACTION.RESET_FILTER}
</button>
${getActionButtonsHTML()}
`;
container.appendChild(filterBar);
@@ -45,13 +46,15 @@ export function renderStorageList(container: HTMLElement) {
<thead>
<tr>
<th class="text-center" style="width:50px;">No</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.CORP.key}">${ASSET_SCHEMA.CORP.ui}</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.ORG.key}">${ASSET_SCHEMA.ORG.ui}</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.ASSET_CODE.key}">${ASSET_SCHEMA.ASSET_CODE.ui}</th>
<th data-sort="용도">용도</th>
<th data-sort="상세">상세</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.LOCATION.key}">${ASSET_SCHEMA.LOCATION.ui}</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.MANAGER_MAIN.key}">담당자(정/부)</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.HW_STATUS.key}">${ASSET_SCHEMA.HW_STATUS.ui}</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.MANAGER_MAIN.key}">현 사용자</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.ASSET_TYPE.key}">${ASSET_SCHEMA.ASSET_TYPE.ui}</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.VOLUME.key}">${ASSET_SCHEMA.VOLUME.ui}</th>
<th data-sort="${ASSET_SCHEMA.MODEL_NAME.key}">${ASSET_SCHEMA.MODEL_NAME.ui}</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.SERIAL_NUM.key}">${ASSET_SCHEMA.SERIAL_NUM.ui}</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.LOCATION.key}">${ASSET_SCHEMA.LOCATION.ui}(건물)</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.LOC_DETAIL.key}">${ASSET_SCHEMA.LOC_DETAIL.ui}</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.MEMO.key}">${ASSET_SCHEMA.MEMO.ui}</th>
</tr>
</thead>
<tbody id="dynamic-tbody"></tbody>
@@ -68,14 +71,15 @@ export function renderStorageList(container: HTMLElement) {
const keyword = keywordInput ? keywordInput.value.toLowerCase().trim() : '';
const corp = corpSelect ? corpSelect.value : '';
const orgUnit = orgSelect ? orgSelect.value : '';
const orgUnit = orgSelect ? orgUnit.value : '';
let filtered = fullList.filter(asset => {
const matchKeyword = !keyword ||
String((asset as any)[ASSET_SCHEMA.ASSET_CODE.key]||'').toLowerCase().includes(keyword) ||
String((asset as any)[ASSET_SCHEMA.ORG.key]||'').toLowerCase().includes(keyword);
const matchCorp = !corp || (asset as any)[ASSET_SCHEMA.CORP.key] === corp;
const matchOrg = !orgUnit || (asset as any)[ASSET_SCHEMA.ORG.key] === orgUnit;
String(asset[ASSET_SCHEMA.MODEL_NAME.key]||'').toLowerCase().includes(keyword) ||
String(asset[ASSET_SCHEMA.MANAGER_MAIN.key]||'').toLowerCase().includes(keyword) ||
String(asset[ASSET_SCHEMA.SERIAL_NUM.key]||'').toLowerCase().includes(keyword);
const matchCorp = !corp || asset[ASSET_SCHEMA.PURCHASE_CORP.key] === corp;
const matchOrg = !orgUnit || asset[ASSET_SCHEMA.CURRENT_DEPT.key] === orgUnit;
return matchKeyword && matchCorp && matchOrg;
});
@@ -85,7 +89,7 @@ export function renderStorageList(container: HTMLElement) {
tbody.innerHTML = '';
if (filtered.length === 0) {
tbody.innerHTML = `<tr><td colspan="8" class="text-center" style="padding: 3rem; color: var(--text-muted);">${UI_TEXT.MESSAGES.NO_DATA}</td></tr>`;
tbody.innerHTML = `<tr><td colspan="10" class="text-center" style="padding: 3rem; color: var(--text-muted);">${UI_TEXT.MESSAGES.NO_DATA}</td></tr>`;
return;
}
@@ -93,22 +97,17 @@ export function renderStorageList(container: HTMLElement) {
const tr = document.createElement('tr');
tr.style.cursor = 'pointer';
const mainManager = (asset as any)[ASSET_SCHEMA.MANAGER_MAIN.key] || '';
const subManager = (asset as any)[ASSET_SCHEMA.MANAGER_SUB.key] || '';
const managerHtml = [
mainManager ? `${createBadge('정', 'primary')} ${mainManager}` : '',
subManager ? `${createBadge('부', 'muted')} ${subManager}` : ''
].filter(v => v !== '').join(' / ');
tr.innerHTML = `
<td class="text-center">${idx+1}</td>
<td class="text-center">${(asset as any)[ASSET_SCHEMA.CORP.key]}</td>
<td class="text-center">${(asset as any)[ASSET_SCHEMA.ORG.key]||'-'}</td>
<td class="text-center">${(asset as any)[ASSET_SCHEMA.ASSET_CODE.key]}</td>
<td>${formatInline(asset.)}</td>
<td>${formatInline(asset.)}</td>
<td class="text-center">${formatInline((asset as any)[ASSET_SCHEMA.LOCATION.key])}</td>
<td class="text-center">${managerHtml || '-'}</td>
<td class="text-center">${asset[ASSET_SCHEMA.HW_STATUS.key]||'-'}</td>
<td class="text-center">${asset[ASSET_SCHEMA.MANAGER_MAIN.key]||'-'}</td>
<td class="text-center">${asset[ASSET_SCHEMA.ASSET_TYPE.key]||'-'}</td>
<td class="text-center">${asset[ASSET_SCHEMA.VOLUME.key]||'-'}</td>
<td>${formatInline(asset[ASSET_SCHEMA.MODEL_NAME.key]||asset[ASSET_SCHEMA.ASSET_NAME.key]||'-')}</td>
<td class="text-center">${asset[ASSET_SCHEMA.SERIAL_NUM.key]||'-'}</td>
<td class="text-center">${formatInline(asset[ASSET_SCHEMA.LOCATION.key])}</td>
<td class="text-center">${formatInline(asset[ASSET_SCHEMA.LOC_DETAIL.key]||'-')}</td>
<td class="text-center">${formatInline(asset[ASSET_SCHEMA.MEMO.key]||'-')}</td>
`;
tr.addEventListener('click', () => openHwModal(asset, 'view'));
tbody.appendChild(tr);
@@ -131,5 +130,5 @@ export function renderStorageList(container: HTMLElement) {
});
updateTable();
createIcons({ icons: { RefreshCcw } });
createIcons({ icons: { RefreshCcw, Download, Upload, FileSpreadsheet, Plus } });
}

View File

@@ -1,15 +1,16 @@
import { state } from '../../core/state';
import { openSwModal } from '../../components/Modal/SWModal';
import { openSwUserModal } from '../../components/Modal/SWUserModal';
import { sortAssets, dynamicSort, formatPrice } from '../../core/utils';
import { sortAssets, dynamicSort, formatPrice, getActionButtonsHTML } from '../../core/utils';
import { setupTableSorting, SortState } from '../../core/tableHandler';
import { ASSET_SCHEMA } from '../../core/schema';
import { CORP_LIST } from '../../components/Modal/SharedData';
import { generateOptionsHTML } from '../../components/Modal/ModalUtils';
import { createIcons, Edit2, Users, RefreshCcw } from 'lucide';
import { createIcons, Edit2, Users, RefreshCcw, Download, Upload, FileSpreadsheet, Plus } from 'lucide';
export function renderSwList(container: HTMLElement) {
const isSub = state.activeSubTab === '구독SW';
const fullList = sortAssets(isSub ? state.masterData.subSw : state.masterData.permSw);
const isInternal = state.activeSubTab === '내부';
const fullList = sortAssets(isInternal ? state.masterData.swInternal : state.masterData.swExternal);
let sortState: SortState = { key: '', direction: 'asc' };
@@ -17,11 +18,11 @@ export function renderSwList(container: HTMLElement) {
filterBar.className = 'search-bar';
filterBar.innerHTML = `
<div class="search-item flex-1">
<label>통합 검색 (제품명/부서)</label>
<label>통합 검색 (${ASSET_SCHEMA.PRODUCT_NAME.ui}/${ASSET_SCHEMA.CURRENT_DEPT.ui})</label>
<input type="text" id="filter-keyword" placeholder="검색어를 입력하세요..." autocomplete="off">
</div>
<div class="search-item">
<label>분야</label>
<label>${ASSET_SCHEMA.SW_FIELD.ui}</label>
<select id="filter-field">
<option value="">전체 분야</option>
<option value="업무공통">업무공통</option>
@@ -31,38 +32,54 @@ export function renderSwList(container: HTMLElement) {
</select>
</div>
<div class="search-item">
<label>법인</label>
<label>${ASSET_SCHEMA.PURCHASE_CORP.ui}</label>
<select id="filter-corp">${generateOptionsHTML(CORP_LIST, '', true)}</select>
</div>
<button id="btn-reset-filters" class="btn btn-outline btn-reset">
<i data-lucide="refresh-ccw"></i> 필터 초기화
</button>
${getActionButtonsHTML()}
`;
container.appendChild(filterBar);
const tableWrapper = document.createElement('div');
tableWrapper.className = 'table-container';
const table = document.createElement('table');
if (isInternal) {
table.innerHTML = `
<thead>
<tr>
<th style="text-align:center; width: 50px;">No.</th>
<th style="text-align:center;" data-sort="상태">상태</th>
<th style="text-align:center;" data-sort="분야">분야</th>
<th style="text-align:center;" data-sort="법인">법인</th>
<th style="text-align:center;" data-sort="부서">부서</th>
<th style="text-align:center;" data-sort="제품명">제품명</th>
<th style="text-align:center;" data-sort="구매일">구매일</th>
<th style="text-align:center;" data-sort="시작일">시작일</th>
<th style="text-align:center;" data-sort="만료일">만료일</th>
<th style="text-align:center;" data-sort="금액">금액</th>
<th style="text-align:center;" data-sort="수량">수량</th>
<th style="text-align:center;">사용가능</th>
<th style="text-align:center;">사용자</th>
<th style="text-align:center;" data-sort="${ASSET_SCHEMA.SW_FIELD.key}">${ASSET_SCHEMA.SW_FIELD.ui}</th>
<th style="text-align:center;" data-sort="${ASSET_SCHEMA.DEV_OBJ.key}">${ASSET_SCHEMA.DEV_OBJ.ui}</th>
<th style="text-align:center;" data-sort="${ASSET_SCHEMA.SW_STATUS.key}">${ASSET_SCHEMA.SW_STATUS.ui}</th>
<th style="text-align:center;" data-sort="${ASSET_SCHEMA.SW_TYPE.key}">${ASSET_SCHEMA.SW_TYPE.ui}</th>
<th style="text-align:center;" data-sort="${ASSET_SCHEMA.MEMO.key}">${ASSET_SCHEMA.MEMO.ui}</th>
</tr>
</thead>
<tbody id="dynamic-tbody"></tbody>
`;
} else {
table.innerHTML = `
<thead>
<tr>
<th style="text-align:center; width: 50px;">No.</th>
<th style="text-align:center;" data-sort="${ASSET_SCHEMA.PRODUCT_NAME.key}">자산명</th>
<th style="text-align:center;" data-sort="${ASSET_SCHEMA.ASSET_TYPE.key}">유형</th>
<th style="text-align:center;" data-sort="${ASSET_SCHEMA.SW_STATUS.key}">${ASSET_SCHEMA.SW_STATUS.ui}</th>
<th style="text-align:center;" data-sort="${ASSET_SCHEMA.SW_FIELD.key}">${ASSET_SCHEMA.SW_FIELD.ui}</th>
<th style="text-align:center;" data-sort="${ASSET_SCHEMA.CURRENT_DEPT.key}">${ASSET_SCHEMA.CURRENT_DEPT.ui}</th>
<th style="text-align:center;">현 사용자</th>
<th style="text-align:center;" data-sort="${ASSET_SCHEMA.PURCHASE_DATE.key}">구매연월</th>
<th style="text-align:center;">시작일</th>
<th style="text-align:center;" data-sort="${ASSET_SCHEMA.EXPIRY_DATE.key}">만료일</th>
<th style="text-align:center;" data-sort="${ASSET_SCHEMA.MEMO.key}">${ASSET_SCHEMA.MEMO.ui}</th>
</tr>
</thead>
<tbody id="dynamic-tbody"></tbody>
`;
}
tableWrapper.appendChild(table);
container.appendChild(tableWrapper);
@@ -78,9 +95,11 @@ export function renderSwList(container: HTMLElement) {
const corp = corpSelect ? corpSelect.value : '';
let filtered = fullList.filter(asset => {
const matchKeyword = !keyword || (asset. || '').toLowerCase().includes(keyword) || (asset. || '').toLowerCase().includes(keyword);
const matchField = !field || asset. === field;
const matchCorp = !corp || asset. === corp;
const matchKeyword = !keyword ||
(asset[ASSET_SCHEMA.PRODUCT_NAME.key] || '').toLowerCase().includes(keyword) ||
(asset[ASSET_SCHEMA.CURRENT_DEPT.key] || '').toLowerCase().includes(keyword);
const matchField = !field || asset[ASSET_SCHEMA.SW_FIELD.key] === field;
const matchCorp = !corp || asset[ASSET_SCHEMA.PURCHASE_CORP.key] === corp;
return matchKeyword && matchField && matchCorp;
});
@@ -90,76 +109,46 @@ export function renderSwList(container: HTMLElement) {
tbody.innerHTML = '';
if (filtered.length === 0) {
tbody.innerHTML = `<tr><td colspan="13" style="text-align:center; padding: 3rem; color: var(--text-muted);">검색 결과가 없습니다.</td></tr>`;
tbody.innerHTML = `<tr><td colspan="${isInternal ? 6 : 11}" style="text-align:center; padding: 3rem; color: var(--text-muted);">검색 결과가 없습니다.</td></tr>`;
return;
}
filtered.forEach((asset, idx) => {
const assigned = state.masterData.swUsers.filter(u => u.sw_id === asset.id).length;
const qty = typeof asset. === 'number' ? asset.수량 : parseInt(asset.||'0', 10);
const avail = qty - assigned;
let statusHtml = '';
if (isSub) {
let isExpired = false;
if (asset.) {
const endDateStr = asset..replace(/\./g, '-');
const endDate = new Date(endDateStr);
if (!isNaN(endDate.getTime())) {
endDate.setHours(23, 59, 59, 999);
if (endDate < new Date()) isExpired = true;
}
}
if (isExpired) statusHtml = `<span style="background: var(--danger, #ef4444); color: white; padding: 2px 6px; border-radius: 4px; font-size: 0.75rem; font-weight: bold; white-space: nowrap;">만료</span>`;
else statusHtml = `<span style="background: var(--primary-color, #1E5149); color: white; padding: 2px 6px; border-radius: 4px; font-size: 0.75rem; font-weight: bold; white-space: nowrap;">사용중</span>`;
} else {
let isMaintenance = false;
if (asset. && asset.) {
const startDate = new Date(asset..replace(/\./g, '-'));
const endDate = new Date(asset..replace(/\./g, '-'));
const today = new Date();
if (!isNaN(startDate.getTime()) && !isNaN(endDate.getTime())) {
endDate.setHours(23, 59, 59, 999);
if (today >= startDate && today <= endDate) isMaintenance = true;
}
}
if (isMaintenance) statusHtml = `<span style="background: #3b82f6; color: white; padding: 2px 6px; border-radius: 4px; font-size: 0.75rem; font-weight: bold; white-space: nowrap;">유지보수</span>`;
else statusHtml = `<span style="background: #6b7280; color: white; padding: 2px 6px; border-radius: 4px; font-size: 0.75rem; font-weight: bold; white-space: nowrap;">보유중</span>`;
}
if (isInternal) {
const tr = document.createElement('tr');
tr.style.cursor = 'pointer';
tr.innerHTML = `
<td style="text-align:center;">${idx+1}</td>
<td style="text-align:center;">${asset[ASSET_SCHEMA.SW_FIELD.key]||''}</td>
<td style="text-align:center;">${asset[ASSET_SCHEMA.DEV_OBJ.key]||''}</td>
<td style="text-align:center;">${asset[ASSET_SCHEMA.SW_STATUS.key]||'보유중'}</td>
<td style="text-align:center;">${asset[ASSET_SCHEMA.SW_TYPE.key]||'내부'}</td>
<td>${formatInline(asset[ASSET_SCHEMA.MEMO.key]||'-')}</td>
`;
tr.addEventListener('click', () => openSwModal(asset, 'view'));
tbody.appendChild(tr);
} else {
const tr = document.createElement('tr');
tr.style.cursor = 'pointer';
const users = state.masterData.swUsers.filter(u => u.sw_id === asset.id);
const userText = users.length > 0 ? `${users[0].user_name}${users.length > 1 ? ' 외 ' + (users.length - 1) : ''}` : '-';
tr.innerHTML = `
<td style="text-align:center;">${idx+1}</td>
<td style="text-align:center;">${statusHtml}</td>
<td>${asset.||''}</td>
<td>${asset.}</td>
<td>${asset.||''}</td>
<td>${asset.}</td>
<td style="text-align:center;">${asset.||''}</td>
<td style="text-align:center;">${asset.||''}</td>
<td style="text-align:center;">${asset.||''}</td>
<td style="text-align:right;">${formatPrice(asset.)}</td>
<td style="text-align:center;">${qty}</td>
<td style="text-align:center;"><strong style="color: ${avail > 0 ? 'var(--primary-color)' : 'var(--danger)'}">${avail}</strong></td>
<td style="text-align:center;">
<button class="btn-icon btn-user-mgmt" title="사용자 관리" style="margin: 0 auto; color: var(--primary-color);">
<i data-lucide="users" style="width:18px; height:18px;"></i>
</button>
</td>
<td>${asset[ASSET_SCHEMA.PRODUCT_NAME.key]||''}</td>
<td style="text-align:center;">${asset[ASSET_SCHEMA.ASSET_TYPE.key]||'외부'}</td>
<td style="text-align:center;">${asset[ASSET_SCHEMA.SW_STATUS.key]||'사용중'}</td>
<td style="text-align:center;">${asset[ASSET_SCHEMA.SW_FIELD.key]||''}</td>
<td style="text-align:center;">${asset[ASSET_SCHEMA.CURRENT_DEPT.key]||''}</td>
<td style="text-align:center;">${userText}</td>
<td style="text-align:center;">${asset[ASSET_SCHEMA.PURCHASE_DATE.key]||''}</td>
<td style="text-align:center;">${asset[ASSET_SCHEMA.PURCHASE_DATE.key]||''}</td>
<td style="text-align:center;">${asset[ASSET_SCHEMA.EXPIRY_DATE.key]||''}</td>
<td>${formatInline(asset[ASSET_SCHEMA.MEMO.key]||'-')}</td>
`;
const userBtn = tr.querySelector('.btn-user-mgmt');
userBtn?.addEventListener('click', (e) => {
e.stopPropagation();
openSwUserModal(asset);
});
tr.addEventListener('click', (e) => {
openSwModal(asset, 'view');
});
tr.addEventListener('click', () => openSwModal(asset, 'view'));
tbody.appendChild(tr);
}
});
setupTableSorting(table, sortState, (key, dir) => {
@@ -167,7 +156,7 @@ export function renderSwList(container: HTMLElement) {
updateTable();
});
createIcons({ icons: { Edit2, Users, RefreshCcw } });
createIcons({ icons: { Edit2, Users, RefreshCcw, Download, Upload, FileSpreadsheet, Plus } });
};
document.getElementById('filter-keyword')?.addEventListener('input', updateTable);

View File

@@ -7,6 +7,12 @@ import { renderMobileList } from './List/MobileListView';
import { renderSwList } from './List/SwListView';
import { renderCloudList } from './List/CloudListView';
import { renderDomainList } from './List/DomainListView';
import { renderNetworkList } from './List/NetworkListView';
import { renderPcPartList } from './List/PcPartListView';
import { renderSpaceInfoList } from './List/SpaceInfoListView';
import { renderGiftList } from './List/GiftListView';
import { renderFacilityList } from './List/FacilityListView';
import { renderCostList } from './List/CostListView';
import { createIcons, Download, Upload, FileSpreadsheet, Plus, X, LayoutDashboard, Monitor, Server, Database, Laptop, CalendarClock, Key, Cpu, Layers, Users, Paperclip, Edit2, RefreshCcw } from 'lucide';
/**
@@ -24,26 +30,38 @@ export function renderSWTable(mainContent: HTMLElement) {
const tab = state.activeSubTab;
if (state.activeCategory === 'hw') {
if (tab === '개인PC') renderPcList(container);
if (tab === 'PC') renderPcList(container);
else if (tab === '서버') renderServerList(container);
else if (tab === '스토리지') renderStorageList(container);
else if (tab === '전산비품') renderEquipmentList(container);
else if (tab === '모바일기기') renderMobileList(container);
else if (tab === '업무지원장비') renderEquipmentList(container);
else if (tab === '네트워크') renderNetworkList(container);
else if (tab === 'PC부품') renderPcPartList(container);
else if (tab === '공간정보장비') renderSpaceInfoList(container);
else {
container.innerHTML = `<div style="padding:2rem; color:var(--text-muted);">"${tab}" 탭에 대한 하드웨어 리스트 뷰가 정의되지 않았습니다.</div>`;
}
} else if (state.activeCategory === 'sw') {
if (tab === '구독SW' || tab === '영구SW') {
if (tab === '외부' || tab === '내부') {
renderSwList(container);
} else if (tab === '클라우드') {
renderCloudList(container);
} else {
container.innerHTML = `<div style="padding:2rem; color:var(--text-muted);">"${tab}" 탭에 대한 소프트웨어 리스트 뷰가 정의되지 않았습니다.</div>`;
}
} else if (state.activeCategory === 'ops') {
if (tab === '도메인') renderDomainList(container);
else if (tab === '클라우드') renderCloudList(container);
else if (tab === '비용관리') renderCostList(container);
else {
container.innerHTML = `<div style="padding:2rem; color:var(--text-muted); text-align:center; margin-top:3rem;">운영 서비스(${tab}) 관리 기능은 현재 준비 중입니다.</div>`;
container.innerHTML = `<div style="padding:2rem; color:var(--text-muted);">"${tab}" 탭에 대한 운영지원 리스트 뷰가 정의되지 않았습니다.</div>`;
}
} else if (state.activeCategory === 'vip') {
if (tab === '선물') renderGiftList(container);
else {
container.innerHTML = `<div style="padding:2rem; color:var(--text-muted); text-align:center; margin-top:3rem;">내빈/외빈(${tab}) 관리 기능은 현재 준비 중입니다.</div>`;
}
} else if (state.activeCategory === 'fac') {
if (tab === '사무가구') renderFacilityList(container);
else {
container.innerHTML = `<div style="padding:2rem; color:var(--text-muted); text-align:center; margin-top:3rem;">시설자산(${tab}) 관리 기능은 현재 준비 중입니다.</div>`;
}
}