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

@@ -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,20 +40,19 @@ 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>
`;
tableWrapper.appendChild(table);
container.appendChild(tableWrapper);
const tbody = table.querySelector('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');
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>
</tr>
</thead>
<tbody id="dynamic-tbody"></tbody>
`;
if (isInternal) {
table.innerHTML = `
<thead>
<tr>
<th style="text-align:center; width: 50px;">No.</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>`;
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 {
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>`;
}
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) : ''}` : '-';
const tr = document.createElement('tr');
tr.style.cursor = 'pointer';
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>
`;
const userBtn = tr.querySelector('.btn-user-mgmt');
userBtn?.addEventListener('click', (e) => {
e.stopPropagation();
openSwUserModal(asset);
});
tr.addEventListener('click', (e) => {
openSwModal(asset, 'view');
});
tbody.appendChild(tr);
tr.innerHTML = `
<td style="text-align:center;">${idx+1}</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>
`;
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>`;
}
}