refactor(ui): unify 14 list views into a single ListFactory component

- ListFactory.ts를 생성하여 중복되는 테이블 생성, 정렬, 필터 로직을 공통 컴포넌트화

- 14개의 ListView.ts 파일들을 ListFactory를 호출하는 설정 객체 형태로 리팩토링

- TypeScript 컴파일 에러(타입 불일치 및 누락된 속성) 수정 완료
This commit is contained in:
2026-05-26 19:47:01 +09:00
parent 2c67037fc4
commit bf7fb0ffe6
19 changed files with 617 additions and 1589 deletions

View File

@@ -43,7 +43,7 @@ export function renderNavigation(onTabChange: (tab: string) => void) {
trigger.addEventListener('click', () => {
if (state.activeCategory !== catKey) {
state.activeCategory = catKey;
state.activeCategory = catKey as any;
const firstTab = config.tabs[0];
state.activeSubTab = firstTab;
render();
@@ -55,14 +55,14 @@ export function renderNavigation(onTabChange: (tab: string) => void) {
const shelf = document.createElement('div');
shelf.className = 'lnb-shelf';
config.tabs.forEach(tab => {
config.tabs.forEach((tab: string) => {
const item = document.createElement('div');
item.className = `lnb-item ${isActive && state.activeSubTab === tab ? 'active' : ''}`;
item.textContent = tab;
item.addEventListener('click', (e) => {
e.stopPropagation();
state.activeCategory = catKey;
state.activeCategory = catKey as any;
state.activeSubTab = tab;
render();
onTabChange(tab);

View File

@@ -18,6 +18,8 @@ export interface MasterAssetData {
domain: any[];
cost: any[];
vip: any[];
mobile?: any[]; // Legacy mobile support
equip?: any[]; // Backward compat
// Backward compatibility
subSw: any[];

View File

@@ -75,7 +75,7 @@ export function renderSwDashboard(container: HTMLElement) {
}
function isSWExpiring(sw: any) {
const expiry = sw[ASSET_SCHEMA.EXPIRY_DATE.key];
const expiry = sw[ASSET_SCHEMA.EXPIRED_DATE.key];
if (!expiry) return false;
const endMs = new Date(normalizeDate(expiry)).getTime();
const diffDays = (endMs - Date.now()) / (1000 * 60 * 60 * 24);

View File

@@ -1,104 +1,31 @@
import { state } from '../../core/state';
import { openSwModal } from '../../components/Modal/SWModal';
import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema';
import { dynamicSort, formatInline, getActionButtonsHTML, renderPageHeader } from '../../core/utils';
import { setupTableSorting, SortState } from '../../core/tableHandler';
import { renderFilterBar, applyCommonFilters } from '../../core/filterHandler';
import { createIcons, Cloud, CreditCard, DollarSign, RefreshCcw, Plus } from 'lucide';
import { formatInline } from '../../core/utils';
import { ASSET_SCHEMA } from '../../core/schema';
import { createListView } from './ListFactory';
/**
* 클라우드(운영 서비스) 자산 목록 뷰
*/
export function renderCloudList(container: HTMLElement) {
renderPageHeader(container, '클라우드');
const fullList = state.masterData.cloud || [];
let sortState: SortState = { key: '', direction: 'asc' };
let currentFilters = { keyword: '', corp: '', dept: '', field: '' };
const filterBar = document.createElement('div');
filterBar.className = 'search-bar';
container.appendChild(filterBar);
const tableWrapper = document.createElement('div');
tableWrapper.className = 'table-container';
const table = document.createElement('table');
table.innerHTML = `
<thead>
<tr>
<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 class="col-memo" data-sort="${ASSET_SCHEMA.MEMO.key}">${ASSET_SCHEMA.MEMO.ui}</th>
</tr>
</thead>
<tbody id="cloud-tbody"></tbody>
`;
tableWrapper.appendChild(table);
container.appendChild(tableWrapper);
const tbody = table.querySelector('tbody')!;
const updateTable = () => {
let filtered = applyCommonFilters(fullList, currentFilters, ['PRODUCT_NAME', 'ASSET_PURPOSE', 'PURCHASE_VENDOR']);
if (sortState.key) {
filtered = dynamicSort(filtered, sortState.key, sortState.direction);
}
tbody.innerHTML = '';
if (filtered.length === 0) {
tbody.innerHTML = `<tr><td colspan="5" 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>${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 class="col-memo">${formatInline(asset[ASSET_SCHEMA.MEMO.key]||'')}</td>
`;
tr.addEventListener('click', () => openSwModal(asset, 'view'));
tbody.appendChild(tr);
});
setupTableSorting(table, sortState, (key, dir) => {
sortState = { key, direction: dir };
updateTable();
});
createIcons({ icons: { Cloud, CreditCard, DollarSign, RefreshCcw, Plus } });
};
renderFilterBar(filterBar, {
keywordLabel: `통합 검색 (${ASSET_SCHEMA.PRODUCT_NAME.ui}/${ASSET_SCHEMA.PURCHASE_VENDOR.ui})`,
showCorp: true,
showDept: true,
onFilterChange: (filters) => {
currentFilters = filters;
updateTable();
}
createListView(container, {
title: '클라우드',
dataSource: () => state.masterData.cloud || [],
searchKeys: ['PRODUCT_NAME', 'ASSET_PURPOSE', 'PURCHASE_VENDOR'],
filterOptions: {
keywordLabel: `통합 검색 (${ASSET_SCHEMA.PRODUCT_NAME.ui}/${ASSET_SCHEMA.PURCHASE_VENDOR.ui})`,
showCorp: true,
showDept: true
},
onRowClick: (asset) => openSwModal(asset, 'view'),
columns: [
{ header: ASSET_SCHEMA.PRODUCT_NAME.ui, sortKey: ASSET_SCHEMA.PRODUCT_NAME.key, render: a => a[ASSET_SCHEMA.PRODUCT_NAME.key] || '' },
{ header: ASSET_SCHEMA.ASSET_PURPOSE.ui, sortKey: ASSET_SCHEMA.ASSET_PURPOSE.key, render: a => a[ASSET_SCHEMA.ASSET_PURPOSE.key] || '' },
{ header: ASSET_SCHEMA.PURCHASE_VENDOR.ui, sortKey: ASSET_SCHEMA.PURCHASE_VENDOR.key, render: a => a[ASSET_SCHEMA.PURCHASE_VENDOR.key] || '' },
{
header: ASSET_SCHEMA.PURCHASE_AMOUNT.ui,
sortKey: ASSET_SCHEMA.PURCHASE_AMOUNT.key,
align: 'right',
render: a => `<span style="font-weight:600;">₩ ${a[ASSET_SCHEMA.PURCHASE_AMOUNT.key] ? Number(String(a[ASSET_SCHEMA.PURCHASE_AMOUNT.key]).replace(/,/g, '')).toLocaleString() : '0'}</span>`
},
{ header: ASSET_SCHEMA.MEMO.ui, sortKey: ASSET_SCHEMA.MEMO.key, className: 'col-memo', render: a => formatInline(a[ASSET_SCHEMA.MEMO.key] || '') }
]
});
// Populate Dept Options
const deptSelect = container.querySelector('#filter-dept') as HTMLSelectElement;
if (deptSelect) {
const orgUnits = Array.from(new Set(fullList.map(a => a[ASSET_SCHEMA.CURRENT_DEPT.key]))).filter(Boolean).sort();
orgUnits.forEach(dept => {
const opt = document.createElement('option');
opt.value = String(dept);
opt.textContent = String(dept);
deptSelect.appendChild(opt);
});
}
updateTable();
}

View File

@@ -1,108 +1,35 @@
import { state } from '../../core/state';
import { formatInline, sortAssets, dynamicSort, renderPageHeader } from '../../core/utils';
import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema';
import { setupTableSorting, SortState } from '../../core/tableHandler';
import { renderFilterBar, applyCommonFilters } from '../../core/filterHandler';
import { createIcons, RefreshCcw, Plus } from 'lucide';
import { sortAssets, formatInline } from '../../core/utils';
import { ASSET_SCHEMA } from '../../core/schema';
import { createListView } from './ListFactory';
/**
* 비용관리 자산 목록 뷰
*/
export function renderCostList(container: HTMLElement) {
renderPageHeader(container, '비용관리');
const fullList = sortAssets(state.masterData.cloud?.filter((a: any) => a.category === '비용관리') || []);
let sortState: SortState = { key: '', direction: 'asc' };
let currentFilters = { keyword: '', corp: '', dept: '', field: '' };
const filterBar = document.createElement('div');
filterBar.className = 'search-bar';
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" 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 data-sort="${ASSET_SCHEMA.EMAIL_ACCOUNT.key}">${ASSET_SCHEMA.EMAIL_ACCOUNT.ui}</th>
<th class="col-memo" 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 = () => {
let filtered = applyCommonFilters(fullList, currentFilters, ['PRODUCT_NAME', 'MANAGER_MAIN', 'EMAIL_ACCOUNT']);
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';
const loc = asset[ASSET_SCHEMA.LOCATION.key] || '';
const detail = asset[ASSET_SCHEMA.LOC_DETAIL.key] || '';
const displayLoc = detail ? `${loc}(${detail})` : (loc || '-');
tr.innerHTML = `
<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">${displayLoc}</td>
<td>${asset[ASSET_SCHEMA.EMAIL_ACCOUNT.key] || '-'}</td>
<td class="col-memo">${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, Plus } });
};
renderFilterBar(filterBar, {
keywordLabel: `통합 검색 (${ASSET_SCHEMA.PRODUCT_NAME.ui}/${ASSET_SCHEMA.MANAGER_MAIN.ui})`,
showCorp: true,
showDept: true,
onFilterChange: (filters) => {
currentFilters = filters;
updateTable();
}
createListView(container, {
title: '비용관리',
dataSource: () => sortAssets(state.masterData.cloud?.filter((a: any) => a.category === '비용관리') || []),
searchKeys: ['PRODUCT_NAME', 'MANAGER_MAIN', 'EMAIL_ACCOUNT'],
filterOptions: {
keywordLabel: `통합 검색 (${ASSET_SCHEMA.PRODUCT_NAME.ui}/${ASSET_SCHEMA.MANAGER_MAIN.ui})`,
showCorp: true,
showDept: true
},
onRowClick: () => alert('상세 정보 준비 중입니다.'),
columns: [
{ header: ASSET_SCHEMA.ASSET_TYPE.ui, sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', render: a => a[ASSET_SCHEMA.ASSET_TYPE.key] || '' },
{ header: ASSET_SCHEMA.ASSET_PURPOSE.ui, sortKey: ASSET_SCHEMA.ASSET_PURPOSE.key, render: a => formatInline(a[ASSET_SCHEMA.ASSET_PURPOSE.key] || '-') },
{ header: '현 사용자', sortKey: ASSET_SCHEMA.MANAGER_MAIN.key, align: 'center', render: a => a[ASSET_SCHEMA.MANAGER_MAIN.key] || '-' },
{
header: ASSET_SCHEMA.LOCATION.ui,
sortKey: ASSET_SCHEMA.LOCATION.key,
align: 'center',
render: a => {
const loc = a[ASSET_SCHEMA.LOCATION.key] || '';
const detail = a[ASSET_SCHEMA.LOC_DETAIL.key] || '';
return detail ? `${loc}(${detail})` : (loc || '-');
}
},
{ header: ASSET_SCHEMA.EMAIL_ACCOUNT.ui, sortKey: ASSET_SCHEMA.EMAIL_ACCOUNT.key, render: a => a[ASSET_SCHEMA.EMAIL_ACCOUNT.key] || '-' },
{ header: ASSET_SCHEMA.MEMO.ui, sortKey: ASSET_SCHEMA.MEMO.key, className: 'col-memo', render: a => formatInline(a[ASSET_SCHEMA.MEMO.key] || '-') }
]
});
// Populate Dept Options
const deptSelect = container.querySelector('#filter-dept') as HTMLSelectElement;
if (deptSelect) {
const orgUnits = Array.from(new Set(fullList.map(a => a[ASSET_SCHEMA.CURRENT_DEPT.key]))).filter(Boolean).sort();
orgUnits.forEach(dept => {
const opt = document.createElement('option');
opt.value = String(dept);
opt.textContent = String(dept);
deptSelect.appendChild(opt);
});
}
updateTable();
}

View File

@@ -1,108 +1,38 @@
import { state } from '../../core/state';
import { dynamicSort, formatInline, getActionButtonsHTML, renderPageHeader } from '../../core/utils';
import { createIcons, Plus, Edit2, Trash2, RefreshCcw } from 'lucide';
import { openDomainModal } from '../../components/Modal/DomainModal';
import { setupTableSorting, SortState } from '../../core/tableHandler';
import { formatInline } from '../../core/utils';
import { ASSET_SCHEMA } from '../../core/schema';
import { renderFilterBar, applyCommonFilters } from '../../core/filterHandler';
import { SortState } from '../../core/tableHandler';
import { createListView } from './ListFactory';
// 정렬 상태를 모듈 수준에서 관리하여 화면 갱신 시에도 유지되도록 함
let persistentSortState: SortState = { key: '', direction: 'asc' };
export function renderDomainList(container: HTMLElement) {
renderPageHeader(container, '도메인');
const fullList = state.masterData.domain;
let currentFilters = { keyword: '', corp: '', dept: '', field: '' };
// 검색바 추가
const filterBar = document.createElement('div');
filterBar.className = 'search-bar';
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: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.EXPIRED_DATE.key}">${ASSET_SCHEMA.EXPIRED_DATE.ui}</th>
<th class="col-memo" 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 = () => {
let filtered = applyCommonFilters(fullList, currentFilters, ['DOMAIN_ADDR', 'ASSET_PURPOSE', 'PRODUCT_NAME']);
if (persistentSortState.key) {
filtered = dynamicSort(filtered, persistentSortState.key, persistentSortState.direction);
}
tbody.innerHTML = '';
if (filtered.length === 0) {
tbody.innerHTML = `<tr><td colspan="6" style="text-align:center; padding: 3rem; color: var(--text-muted);">등록된 도메인 정보가 없습니다.</td></tr>`;
return;
}
filtered.forEach((item, idx) => {
const tr = document.createElement('tr');
tr.className = 'domain-row';
tr.style.cursor = 'pointer';
tr.innerHTML = `
<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.EXPIRED_DATE.key] || ''}</td>
<td class="col-memo">${formatInline(item[ASSET_SCHEMA.MEMO.key]||'-')}</td>
`;
tr.addEventListener('click', (e) => {
openDomainModal(item);
});
tbody.appendChild(tr);
});
setupTableSorting(table, persistentSortState, (key, dir) => {
persistentSortState = { key, direction: dir };
updateTable();
});
createIcons({ icons: { Plus, Edit2, Trash2, RefreshCcw } });
};
renderFilterBar(filterBar, {
keywordLabel: `통합 검색 (${ASSET_SCHEMA.DOMAIN_ADDR.ui}/${ASSET_SCHEMA.PRODUCT_NAME.ui})`,
showCorp: true,
showDept: true,
onFilterChange: (filters) => {
currentFilters = filters;
updateTable();
}
createListView(container, {
title: '도메인',
dataSource: () => state.masterData.domain || [],
searchKeys: ['DOMAIN_ADDR', 'ASSET_PURPOSE', 'PRODUCT_NAME'],
persistentSortState,
emptyMessage: '등록된 도메인 정보가 없습니다.',
filterOptions: {
keywordLabel: `통합 검색 (${ASSET_SCHEMA.DOMAIN_ADDR.ui}/${ASSET_SCHEMA.PRODUCT_NAME.ui})`,
showCorp: true,
showDept: true
},
onRowClick: (item) => openDomainModal(item),
columns: [
{ header: ASSET_SCHEMA.DOMAIN_ADDR.ui, sortKey: ASSET_SCHEMA.DOMAIN_ADDR.key, align: 'left', render: a => a[ASSET_SCHEMA.DOMAIN_ADDR.key] || '' },
{ header: ASSET_SCHEMA.ASSET_PURPOSE.ui, sortKey: ASSET_SCHEMA.ASSET_PURPOSE.key, align: 'left', render: a => a[ASSET_SCHEMA.ASSET_PURPOSE.key] || '' },
{
header: ASSET_SCHEMA.ASSET_TYPE.ui,
sortKey: ASSET_SCHEMA.ASSET_TYPE.key,
align: 'center',
render: a => `<span class="badge badge-${a[ASSET_SCHEMA.ASSET_TYPE.key] === '관리중' ? 'primary' : 'muted'}">${a[ASSET_SCHEMA.ASSET_TYPE.key] || '-'}</span>`
},
{ header: ASSET_SCHEMA.PURCHASE_CORP.ui, sortKey: ASSET_SCHEMA.PURCHASE_CORP.key, align: 'center', render: a => a[ASSET_SCHEMA.PURCHASE_CORP.key] || '' },
{ header: ASSET_SCHEMA.EXPIRED_DATE.ui, sortKey: ASSET_SCHEMA.EXPIRED_DATE.key, align: 'center', render: a => a[ASSET_SCHEMA.EXPIRED_DATE.key] || '' },
{ header: ASSET_SCHEMA.MEMO.ui, sortKey: ASSET_SCHEMA.MEMO.key, className: 'col-memo', render: a => formatInline(a[ASSET_SCHEMA.MEMO.key] || '-') }
]
});
// Populate Dept Options
const deptSelect = container.querySelector('#filter-dept') as HTMLSelectElement;
if (deptSelect) {
const orgUnits = Array.from(new Set(fullList.map(a => a[ASSET_SCHEMA.CURRENT_DEPT.key]))).filter(Boolean).sort();
orgUnits.forEach(dept => {
const opt = document.createElement('option');
opt.value = String(dept);
opt.textContent = String(dept);
deptSelect.appendChild(opt);
});
}
updateTable();
}

View File

@@ -1,125 +1,43 @@
import { state } from '../../core/state';
import { openHwModal } from '../../components/Modal/HWModal';
import { formatInline, sortAssets, dynamicSort, renderPageHeader } from '../../core/utils';
import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema';
import { setupTableSorting, SortState } from '../../core/tableHandler';
import { renderFilterBar, applyCommonFilters } from '../../core/filterHandler';
import { createIcons, RefreshCcw, Plus } from 'lucide';
import { sortAssets, formatInline } from '../../core/utils';
import { ASSET_SCHEMA } from '../../core/schema';
import { createListView } from './ListFactory';
/**
* 전산비품 자산 목록 뷰
*/
export function renderEquipmentList(container: HTMLElement) {
renderPageHeader(container, '업무지원장비');
const fullList = sortAssets(state.masterData.equipment);
let sortState: SortState = { key: '', direction: 'asc' };
let currentFilters = { keyword: '', loc: '', dept: '', field: '' };
const filterBar = document.createElement('div');
filterBar.className = 'search-bar';
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" data-sort="${ASSET_SCHEMA.HW_STATUS.key}">${ASSET_SCHEMA.HW_STATUS.ui}</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.CURRENT_USER.key}">${ASSET_SCHEMA.CURRENT_USER.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.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="col-memo" 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 = () => {
let filtered = applyCommonFilters(fullList, currentFilters, ['MODEL_NAME', 'CURRENT_USER', 'ASSET_MFR']);
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';
const loc = asset[ASSET_SCHEMA.LOCATION.key] || '';
const detail = asset[ASSET_SCHEMA.LOC_DETAIL.key] || '';
const displayLoc = detail ? `${loc}(${detail})` : (loc || '-');
tr.innerHTML = `
<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.CURRENT_USER.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">${displayLoc}</td>
<td class="col-memo">${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, Plus } });
};
renderFilterBar(filterBar, {
keywordLabel: `통합 검색 (${ASSET_SCHEMA.MODEL_NAME.ui}/${ASSET_SCHEMA.ASSET_MFR.ui})`,
showLoc: true,
showDept: true,
onFilterChange: (filters) => {
currentFilters = filters;
updateTable();
}
createListView(container, {
title: '업무지원장비',
dataSource: () => sortAssets(state.masterData.equipment || []),
searchKeys: ['MODEL_NAME', 'CURRENT_USER', 'ASSET_MFR'],
filterOptions: {
keywordLabel: `통합 검색 (${ASSET_SCHEMA.MODEL_NAME.ui}/${ASSET_SCHEMA.ASSET_MFR.ui})`,
showLoc: true,
showDept: true
},
onRowClick: (asset) => openHwModal(asset, 'view'),
columns: [
{
header: ASSET_SCHEMA.HW_STATUS.ui,
sortKey: ASSET_SCHEMA.HW_STATUS.key,
align: 'center',
render: a => `<span class="badge badge-${a[ASSET_SCHEMA.HW_STATUS.key] === '대여중' ? 'primary' : 'success'}">${a[ASSET_SCHEMA.HW_STATUS.key] || '보관중'}</span>`
},
{ header: ASSET_SCHEMA.CURRENT_USER.ui, sortKey: ASSET_SCHEMA.CURRENT_USER.key, align: 'center', render: a => a[ASSET_SCHEMA.CURRENT_USER.key] || '-' },
{ header: ASSET_SCHEMA.ASSET_TYPE.ui, sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', render: a => a[ASSET_SCHEMA.ASSET_TYPE.key] || '' },
{ header: ASSET_SCHEMA.ASSET_MFR.ui, sortKey: ASSET_SCHEMA.ASSET_MFR.key, align: 'center', render: a => a[ASSET_SCHEMA.ASSET_MFR.key] || '' },
{ header: ASSET_SCHEMA.MODEL_NAME.ui, sortKey: ASSET_SCHEMA.MODEL_NAME.key, render: a => formatInline(a[ASSET_SCHEMA.MODEL_NAME.key] || a. || '-') },
{ header: ASSET_SCHEMA.ASSET_COUNT.ui, sortKey: ASSET_SCHEMA.ASSET_COUNT.key, align: 'center', render: a => a[ASSET_SCHEMA.ASSET_COUNT.key] || '1' },
{
header: ASSET_SCHEMA.LOCATION.ui,
sortKey: ASSET_SCHEMA.LOCATION.key,
align: 'center',
render: a => {
const loc = a[ASSET_SCHEMA.LOCATION.key] || '';
const detail = a[ASSET_SCHEMA.LOC_DETAIL.key] || '';
return detail ? `${loc}(${detail})` : (loc || '-');
}
},
{ header: ASSET_SCHEMA.MEMO.ui, sortKey: ASSET_SCHEMA.MEMO.key, className: 'col-memo', render: a => formatInline(a[ASSET_SCHEMA.MEMO.key] || '-') }
]
});
// Populate Location Options
const locSelect = container.querySelector('#filter-loc') as HTMLSelectElement;
if (locSelect) {
const locations = Array.from(new Set(fullList.map(a => a[ASSET_SCHEMA.LOCATION.key]))).filter(Boolean).sort();
locations.forEach(loc => {
const opt = document.createElement('option');
opt.value = String(loc);
opt.textContent = String(loc);
locSelect.appendChild(opt);
});
}
// Populate Dept Options
const deptSelect = container.querySelector('#filter-dept') as HTMLSelectElement;
if (deptSelect) {
const orgUnits = Array.from(new Set(fullList.map(a => a[ASSET_SCHEMA.CURRENT_DEPT.key]))).filter(Boolean).sort();
orgUnits.forEach(dept => {
const opt = document.createElement('option');
opt.value = String(dept);
opt.textContent = String(dept);
deptSelect.appendChild(opt);
});
}
updateTable();
}

View File

@@ -1,123 +1,42 @@
import { state } from '../../core/state';
import { openHwModal } from '../../components/Modal/HWModal';
import { formatInline, sortAssets, dynamicSort, renderPageHeader } from '../../core/utils';
import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema';
import { setupTableSorting, SortState } from '../../core/tableHandler';
import { renderFilterBar, applyCommonFilters } from '../../core/filterHandler';
import { createIcons, RefreshCcw, Plus } from 'lucide';
import { sortAssets, formatInline } from '../../core/utils';
import { ASSET_SCHEMA } from '../../core/schema';
import { createListView } from './ListFactory';
/**
* 시설자산 자산 목록 뷰
*/
export function renderFacilityList(container: HTMLElement) {
renderPageHeader(container, '사무가구');
const fullList = sortAssets(state.masterData.equipment?.filter((a: any) => a.category === '시설자산') || []);
let sortState: SortState = { key: '', direction: 'asc' };
let currentFilters = { keyword: '', corp: '', dept: '', field: '' };
const filterBar = document.createElement('div');
filterBar.className = 'search-bar';
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" 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.ASSET_COUNT.key}">${ASSET_SCHEMA.ASSET_COUNT.ui}</th>
<th class="col-memo" 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 = () => {
let filtered = applyCommonFilters(fullList, currentFilters, ['MODEL_NAME', 'ASSET_MFR']);
if (sortState.key) {
filtered = dynamicSort(filtered, sortState.key, sortState.direction);
}
tbody.innerHTML = '';
if (filtered.length === 0) {
tbody.innerHTML = `<tr><td colspan="7" 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';
const loc = asset[ASSET_SCHEMA.LOCATION.key] || '';
const detail = asset[ASSET_SCHEMA.LOC_DETAIL.key] || '';
const displayLoc = detail ? `${loc}(${detail})` : (loc || '-');
tr.innerHTML = `
<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">${displayLoc}</td>
<td class="text-center">${asset[ASSET_SCHEMA.ASSET_COUNT.key] || '1'}</td>
<td class="col-memo">${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, Plus } });
};
renderFilterBar(filterBar, {
keywordLabel: `통합 검색 (${ASSET_SCHEMA.MODEL_NAME.ui})`,
showLoc: true,
showDept: true,
onFilterChange: (filters) => {
currentFilters = filters;
updateTable();
}
createListView(container, {
title: '사무가구',
dataSource: () => sortAssets(state.masterData.equipment?.filter((a: any) => a.category === '시설자산') || []),
searchKeys: ['MODEL_NAME', 'ASSET_MFR'],
filterOptions: {
keywordLabel: `통합 검색 (${ASSET_SCHEMA.MODEL_NAME.ui})`,
showLoc: true,
showDept: true
},
onRowClick: (asset) => openHwModal(asset, 'view'),
columns: [
{
header: ASSET_SCHEMA.HW_STATUS.ui,
sortKey: ASSET_SCHEMA.HW_STATUS.key,
align: 'center',
render: a => `<span class="badge badge-success">${a[ASSET_SCHEMA.HW_STATUS.key] || '보관중'}</span>`
},
{ header: ASSET_SCHEMA.ASSET_TYPE.ui, sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', render: a => a[ASSET_SCHEMA.ASSET_TYPE.key] || '' },
{ header: ASSET_SCHEMA.ASSET_MFR.ui, sortKey: ASSET_SCHEMA.ASSET_MFR.key, align: 'center', render: a => a[ASSET_SCHEMA.ASSET_MFR.key] || '' },
{ header: ASSET_SCHEMA.MODEL_NAME.ui, sortKey: ASSET_SCHEMA.MODEL_NAME.key, render: a => formatInline(a[ASSET_SCHEMA.MODEL_NAME.key] || '-') },
{
header: ASSET_SCHEMA.LOCATION.ui,
sortKey: ASSET_SCHEMA.LOCATION.key,
align: 'center',
render: a => {
const loc = a[ASSET_SCHEMA.LOCATION.key] || '';
const detail = a[ASSET_SCHEMA.LOC_DETAIL.key] || '';
return detail ? `${loc}(${detail})` : (loc || '-');
}
},
{ header: ASSET_SCHEMA.ASSET_COUNT.ui, sortKey: ASSET_SCHEMA.ASSET_COUNT.key, align: 'center', render: a => a[ASSET_SCHEMA.ASSET_COUNT.key] || '1' },
{ header: ASSET_SCHEMA.MEMO.ui, sortKey: ASSET_SCHEMA.MEMO.key, className: 'col-memo', render: a => formatInline(a[ASSET_SCHEMA.MEMO.key] || '-') }
]
});
// Populate Loc Options
const locSelect = container.querySelector('#filter-loc') as HTMLSelectElement;
if (locSelect) {
const locations = Array.from(new Set(fullList.map(a => a[ASSET_SCHEMA.LOCATION.key]))).filter(Boolean).sort();
locations.forEach(loc => {
const opt = document.createElement('option');
opt.value = String(loc);
opt.textContent = String(loc);
locSelect.appendChild(opt);
});
}
// Populate Dept Options
const deptSelect = container.querySelector('#filter-dept') as HTMLSelectElement;
if (deptSelect) {
const orgUnits = Array.from(new Set(fullList.map(a => a[ASSET_SCHEMA.CURRENT_DEPT.key]))).filter(Boolean).sort();
orgUnits.forEach(dept => {
const opt = document.createElement('option');
opt.value = String(dept);
opt.textContent = String(dept);
deptSelect.appendChild(opt);
});
}
updateTable();
}

View File

@@ -1,101 +1,25 @@
import { state } from '../../core/state';
import { formatInline, sortAssets, dynamicSort, renderPageHeader } from '../../core/utils';
import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema';
import { setupTableSorting, SortState } from '../../core/tableHandler';
import { renderFilterBar, applyCommonFilters } from '../../core/filterHandler';
import { createIcons, RefreshCcw, Plus } from 'lucide';
import { sortAssets, formatInline } from '../../core/utils';
import { ASSET_SCHEMA } from '../../core/schema';
import { createListView } from './ListFactory';
/**
* 선물(내빈/외빈) 자산 목록 뷰
*/
export function renderGiftList(container: HTMLElement) {
renderPageHeader(container, '선물');
const fullList = sortAssets(state.masterData.equipment?.filter((a: any) => a.category === '선물') || []);
let sortState: SortState = { key: '', direction: 'asc' };
let currentFilters = { keyword: '', corp: '', dept: '', field: '' };
const filterBar = document.createElement('div');
filterBar.className = 'search-bar';
container.appendChild(filterBar);
const tableWrapper = document.createElement('div');
tableWrapper.className = 'table-container';
const table = document.createElement('table');
table.innerHTML = `
<thead>
<tr>
<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.EXPIRED_DATE.key}">${ASSET_SCHEMA.EXPIRED_DATE.ui}</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.ASSET_COUNT.key}">${ASSET_SCHEMA.ASSET_COUNT.ui}</th>
<th class="col-memo" 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 = () => {
let filtered = applyCommonFilters(fullList, currentFilters, ['PRODUCT_NAME', 'MODEL_NAME']);
if (sortState.key) {
filtered = dynamicSort(filtered, sortState.key, sortState.direction);
}
tbody.innerHTML = '';
if (filtered.length === 0) {
tbody.innerHTML = `<tr><td colspan="5" 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>${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.EXPIRED_DATE.key] || ''}</td>
<td class="text-center">${asset[ASSET_SCHEMA.ASSET_COUNT.key] || '1'}</td>
<td class="col-memo">${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, Plus } });
};
renderFilterBar(filterBar, {
keywordLabel: `통합 검색 (${ASSET_SCHEMA.PRODUCT_NAME.ui})`,
showCorp: true,
showDept: true,
onFilterChange: (filters) => {
currentFilters = filters;
updateTable();
}
createListView(container, {
title: '선물',
dataSource: () => sortAssets(state.masterData.equipment?.filter((a: any) => a.category === '선물') || []),
searchKeys: ['PRODUCT_NAME', 'MODEL_NAME'],
filterOptions: {
keywordLabel: `통합 검색 (${ASSET_SCHEMA.PRODUCT_NAME.ui})`,
showCorp: true,
showDept: true
},
onRowClick: () => alert('상세 정보 준비 중입니다.'),
columns: [
{ header: '자산명', sortKey: ASSET_SCHEMA.PRODUCT_NAME.key, render: a => formatInline(a[ASSET_SCHEMA.PRODUCT_NAME.key] || a[ASSET_SCHEMA.MODEL_NAME.key] || '-') },
{ header: '구매연월', sortKey: ASSET_SCHEMA.PURCHASE_DATE.key, align: 'center', render: a => a[ASSET_SCHEMA.PURCHASE_DATE.key] || '' },
{ header: ASSET_SCHEMA.EXPIRED_DATE.ui, sortKey: ASSET_SCHEMA.EXPIRED_DATE.key, align: 'center', render: a => a[ASSET_SCHEMA.EXPIRED_DATE.key] || '' },
{ header: ASSET_SCHEMA.ASSET_COUNT.ui, sortKey: ASSET_SCHEMA.ASSET_COUNT.key, align: 'center', render: a => a[ASSET_SCHEMA.ASSET_COUNT.key] || '1' },
{ header: ASSET_SCHEMA.MEMO.ui, sortKey: ASSET_SCHEMA.MEMO.key, className: 'col-memo', render: a => formatInline(a[ASSET_SCHEMA.MEMO.key] || '-') }
]
});
// Populate Dept Options
const deptSelect = container.querySelector('#filter-dept') as HTMLSelectElement;
if (deptSelect) {
const orgUnits = Array.from(new Set(fullList.map(a => a[ASSET_SCHEMA.CURRENT_DEPT.key]))).filter(Boolean).sort();
orgUnits.forEach(dept => {
const opt = document.createElement('option');
opt.value = String(dept);
opt.textContent = String(dept);
deptSelect.appendChild(opt);
});
}
updateTable();
}

View File

@@ -0,0 +1,157 @@
import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema';
import { dynamicSort, renderPageHeader } from '../../core/utils';
import { setupTableSorting, SortState } from '../../core/tableHandler';
import { renderFilterBar, applyCommonFilters } from '../../core/filterHandler';
import { createIcons, RefreshCcw, Plus, Edit2, Trash2, Users, Cloud, CreditCard, DollarSign, Paperclip } from 'lucide';
export interface ColumnDef {
header: string;
sortKey?: string;
width?: string;
align?: 'left' | 'center' | 'right';
className?: string;
render: (asset: any) => string;
}
export interface ListViewConfig {
title: string;
dataSource: () => any[];
searchKeys: string[];
filterOptions: {
keywordLabel: string;
showCorp?: boolean;
showDept?: boolean;
showLoc?: boolean;
showField?: boolean;
};
columns: ColumnDef[];
onRowClick?: (asset: any) => void;
emptyMessage?: string;
persistentSortState?: SortState; // Allow passing external sort state (like DomainListView)
}
export function createListView(container: HTMLElement, config: ListViewConfig) {
renderPageHeader(container, config.title);
const fullList = config.dataSource();
let sortState: SortState = config.persistentSortState || { key: '', direction: 'asc' };
// Initialize currentFilters with all possible keys to avoid undefined issues
let currentFilters: any = { keyword: '', corp: '', dept: '', loc: '', field: '' };
const filterBar = document.createElement('div');
filterBar.className = 'search-bar';
container.appendChild(filterBar);
const tableWrapper = document.createElement('div');
tableWrapper.className = 'table-container';
const table = document.createElement('table');
// 1. 헤더 생성
const thead = document.createElement('thead');
const trHead = document.createElement('tr');
config.columns.forEach(col => {
const th = document.createElement('th');
th.innerHTML = col.header;
if (col.sortKey) th.setAttribute('data-sort', col.sortKey);
if (col.width) th.style.width = col.width;
if (col.align) th.style.textAlign = col.align;
if (col.className) th.className = col.className;
trHead.appendChild(th);
});
thead.appendChild(trHead);
table.appendChild(thead);
// 2. 본문 생성
const tbody = document.createElement('tbody');
tbody.id = 'dynamic-tbody';
table.appendChild(tbody);
tableWrapper.appendChild(table);
container.appendChild(tableWrapper);
// 3. 테이블 업데이트 로직
const updateTable = () => {
let filtered = applyCommonFilters(fullList, currentFilters, config.searchKeys as any[]);
if (sortState.key) {
filtered = dynamicSort(filtered, sortState.key, sortState.direction);
}
tbody.innerHTML = '';
if (filtered.length === 0) {
const emptyMsg = config.emptyMessage || UI_TEXT.MESSAGES.NO_DATA;
tbody.innerHTML = `<tr><td colspan="${config.columns.length}" class="text-center" style="padding: 3rem; color: var(--text-muted);">${emptyMsg}</td></tr>`;
return;
}
filtered.forEach((asset) => {
const tr = document.createElement('tr');
if (config.onRowClick) {
tr.style.cursor = 'pointer';
tr.addEventListener('click', () => config.onRowClick!(asset));
}
config.columns.forEach(col => {
const td = document.createElement('td');
if (col.align) td.style.textAlign = col.align;
if (col.className) td.className = col.className;
td.innerHTML = col.render(asset);
tr.appendChild(td);
});
tbody.appendChild(tr);
});
setupTableSorting(table, sortState, (key, dir) => {
sortState = { key, direction: dir };
// If external state was provided, sync it back
if (config.persistentSortState) {
config.persistentSortState.key = key;
config.persistentSortState.direction = dir;
}
updateTable();
});
// 모든 가능한 아이콘 로드 (안전하게)
createIcons({ icons: { RefreshCcw, Plus, Edit2, Trash2, Users, Cloud, CreditCard, DollarSign, Paperclip } });
};
// 4. 필터 바 렌더링
renderFilterBar(filterBar, {
...config.filterOptions,
onFilterChange: (filters) => {
currentFilters = { ...currentFilters, ...filters };
updateTable();
}
});
// 5. 동적 Select 박스 데이터 채우기
const populateSelect = (selector: string, dataKey: string) => {
const select = container.querySelector(selector) as HTMLSelectElement;
if (select) {
// Handle multiple possible keys for department names due to legacy data
const getVal = (a: any) => {
if (dataKey === ASSET_SCHEMA.CURRENT_DEPT.key) {
return a[dataKey] || a['현사용부서'] || a['현사용조직'];
}
return a[dataKey];
}
const uniqueValues = Array.from(new Set(fullList.map(getVal))).filter(Boolean).sort();
uniqueValues.forEach(val => {
const opt = document.createElement('option');
opt.value = String(val);
opt.textContent = String(val);
select.appendChild(opt);
});
}
};
if (config.filterOptions.showLoc) populateSelect('#filter-loc', ASSET_SCHEMA.LOCATION.key);
if (config.filterOptions.showDept) populateSelect('#filter-dept', ASSET_SCHEMA.CURRENT_DEPT.key);
if (config.filterOptions.showCorp) populateSelect('#filter-corp', ASSET_SCHEMA.PURCHASE_CORP.key);
// 6. 초기 렌더링
updateTable();
}

View File

@@ -1,111 +1,38 @@
import { state } from '../../core/state';
import { openHwModal } from '../../components/Modal/HWModal';
import { formatInline, sortAssets, dynamicSort, renderPageHeader } from '../../core/utils';
import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema';
import { setupTableSorting, SortState } from '../../core/tableHandler';
import { renderFilterBar, applyCommonFilters } from '../../core/filterHandler';
import { createIcons, Paperclip, RefreshCcw, Plus } from 'lucide';
import { sortAssets, formatInline } from '../../core/utils';
import { ASSET_SCHEMA } from '../../core/schema';
import { createListView } from './ListFactory';
/**
* 모바일 자산 목록 뷰 (레거시 지원용)
*/
export function renderMobileList(container: HTMLElement) {
renderPageHeader(container, 'PC');
const fullList = sortAssets(state.masterData.mobile || []);
let sortState: SortState = { key: '', direction: 'asc' };
let currentFilters = { keyword: '', corp: '', dept: '', field: '' };
const filterBar = document.createElement('div');
filterBar.className = 'search-bar';
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;" 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>
<th class="col-memo" 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 = () => {
let filtered = applyCommonFilters(fullList, currentFilters, ['MODEL_NAME']);
if (sortState.key) {
filtered = dynamicSort(filtered, sortState.key, sortState.direction);
}
tbody.innerHTML = '';
if (filtered.length === 0) {
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: any, idx: number) => {
const tr = document.createElement('tr');
tr.style.cursor = 'pointer';
const mainManager = asset[ASSET_SCHEMA.MANAGER_MAIN.key] || '';
tr.innerHTML = `
<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] || '') + (asset[ASSET_SCHEMA.LOC_DETAIL.key] ? `(${asset[ASSET_SCHEMA.LOC_DETAIL.key]})` : (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>
<td class="col-memo">${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: { Paperclip, RefreshCcw, Plus } });
};
renderFilterBar(filterBar, {
keywordLabel: `통합 검색 (${ASSET_SCHEMA.MODEL_NAME.ui})`,
showCorp: true,
showDept: true,
onFilterChange: (filters) => {
currentFilters = filters;
updateTable();
}
createListView(container, {
title: 'PC', // Legacy support
dataSource: () => sortAssets(state.masterData.mobile || []),
searchKeys: ['MODEL_NAME'],
filterOptions: {
keywordLabel: `통합 검색 (${ASSET_SCHEMA.MODEL_NAME.ui})`,
showCorp: true,
showDept: true
},
onRowClick: (asset) => openHwModal(asset, 'view'),
columns: [
{ header: ASSET_SCHEMA.HW_STATUS.ui, sortKey: ASSET_SCHEMA.HW_STATUS.key, align: 'center', render: a => a[ASSET_SCHEMA.HW_STATUS.key] || '운영중' },
{ header: ASSET_SCHEMA.PURCHASE_CORP.ui, sortKey: ASSET_SCHEMA.PURCHASE_CORP.key, align: 'center', render: a => a[ASSET_SCHEMA.PURCHASE_CORP.key] || '' },
{ header: ASSET_SCHEMA.MODEL_NAME.ui, sortKey: ASSET_SCHEMA.MODEL_NAME.key, render: a => a[ASSET_SCHEMA.MODEL_NAME.key] || '' },
{
header: ASSET_SCHEMA.LOCATION.ui,
sortKey: ASSET_SCHEMA.LOCATION.key,
align: 'center',
render: a => {
const loc = a[ASSET_SCHEMA.LOCATION.key] || '';
const detail = a[ASSET_SCHEMA.LOC_DETAIL.key] || '';
return detail ? `${loc}(${detail})` : (loc || '-');
}
},
{ header: ASSET_SCHEMA.PURCHASE_DATE.ui, sortKey: ASSET_SCHEMA.PURCHASE_DATE.key, align: 'center', render: a => a[ASSET_SCHEMA.PURCHASE_DATE.key] || '' },
{ header: ASSET_SCHEMA.PURCHASE_AMOUNT.ui, sortKey: ASSET_SCHEMA.PURCHASE_AMOUNT.key, align: 'right', render: a => Number(a[ASSET_SCHEMA.PURCHASE_AMOUNT.key]||0).toLocaleString() },
{ header: '담당자', align: 'center', render: a => a[ASSET_SCHEMA.MANAGER_MAIN.key] || '' },
{ header: ASSET_SCHEMA.MEMO.ui, sortKey: ASSET_SCHEMA.MEMO.key, className: 'col-memo', render: a => formatInline(a[ASSET_SCHEMA.MEMO.key] || '-') }
]
});
// Populate Dept Options
const deptSelect = container.querySelector('#filter-dept') as HTMLSelectElement;
if (deptSelect) {
const orgUnits = Array.from(new Set(fullList.map(a => a[ASSET_SCHEMA.CURRENT_DEPT.key]))).filter(Boolean).sort();
orgUnits.forEach(dept => {
const opt = document.createElement('option');
opt.value = String(dept);
opt.textContent = String(dept);
deptSelect.appendChild(opt);
});
}
updateTable();
}

View File

@@ -1,125 +1,43 @@
import { state } from '../../core/state';
import { openHwModal } from '../../components/Modal/HWModal';
import { formatInline, sortAssets, dynamicSort, renderPageHeader } from '../../core/utils';
import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema';
import { setupTableSorting, SortState } from '../../core/tableHandler';
import { renderFilterBar, applyCommonFilters } from '../../core/filterHandler';
import { createIcons, RefreshCcw, Plus } from 'lucide';
import { sortAssets, formatInline } from '../../core/utils';
import { ASSET_SCHEMA } from '../../core/schema';
import { createListView } from './ListFactory';
/**
* 네트워크 자산 목록 뷰
*/
export function renderNetworkList(container: HTMLElement) {
renderPageHeader(container, '네트워크');
const fullList = sortAssets(state.masterData.network || []);
let sortState: SortState = { key: '', direction: 'asc' };
let currentFilters = { keyword: '', loc: '', dept: '', field: '' };
const filterBar = document.createElement('div');
filterBar.className = 'search-bar';
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" data-sort="${ASSET_SCHEMA.HW_STATUS.key}">${ASSET_SCHEMA.HW_STATUS.ui}</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.CURRENT_USER.key}">${ASSET_SCHEMA.CURRENT_USER.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.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="col-memo" 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 = () => {
let filtered = applyCommonFilters(fullList, currentFilters, ['MODEL_NAME', 'CURRENT_USER', 'ASSET_MFR']);
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';
const loc = asset[ASSET_SCHEMA.LOCATION.key] || '';
const detail = asset[ASSET_SCHEMA.LOC_DETAIL.key] || '';
const displayLoc = detail ? `${loc}(${detail})` : (loc || '-');
tr.innerHTML = `
<td class="text-center"><span class="badge badge-success">${asset[ASSET_SCHEMA.HW_STATUS.key] || '운영중'}</span></td>
<td class="text-center">${asset[ASSET_SCHEMA.CURRENT_USER.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">${displayLoc}</td>
<td class="col-memo">${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, Plus } });
};
renderFilterBar(filterBar, {
keywordLabel: `통합 검색 (${ASSET_SCHEMA.MODEL_NAME.ui}/${ASSET_SCHEMA.ASSET_MFR.ui})`,
showLoc: true,
showDept: true,
onFilterChange: (filters) => {
currentFilters = filters;
updateTable();
}
createListView(container, {
title: '네트워크',
dataSource: () => sortAssets(state.masterData.network || []),
searchKeys: ['MODEL_NAME', 'CURRENT_USER', 'ASSET_MFR'],
filterOptions: {
keywordLabel: `통합 검색 (${ASSET_SCHEMA.MODEL_NAME.ui}/${ASSET_SCHEMA.ASSET_MFR.ui})`,
showLoc: true,
showDept: true
},
onRowClick: (asset) => openHwModal(asset, 'view'),
columns: [
{
header: ASSET_SCHEMA.HW_STATUS.ui,
sortKey: ASSET_SCHEMA.HW_STATUS.key,
align: 'center',
render: a => `<span class="badge badge-success">${a[ASSET_SCHEMA.HW_STATUS.key] || '운영중'}</span>`
},
{ header: ASSET_SCHEMA.CURRENT_USER.ui, sortKey: ASSET_SCHEMA.CURRENT_USER.key, align: 'center', render: a => a[ASSET_SCHEMA.CURRENT_USER.key] || '-' },
{ header: ASSET_SCHEMA.ASSET_TYPE.ui, sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', render: a => a[ASSET_SCHEMA.ASSET_TYPE.key] || '' },
{ header: ASSET_SCHEMA.ASSET_MFR.ui, sortKey: ASSET_SCHEMA.ASSET_MFR.key, align: 'center', render: a => a[ASSET_SCHEMA.ASSET_MFR.key] || '' },
{ header: ASSET_SCHEMA.MODEL_NAME.ui, sortKey: ASSET_SCHEMA.MODEL_NAME.key, render: a => formatInline(a[ASSET_SCHEMA.MODEL_NAME.key] || '-') },
{ header: ASSET_SCHEMA.ASSET_COUNT.ui, sortKey: ASSET_SCHEMA.ASSET_COUNT.key, align: 'center', render: a => a[ASSET_SCHEMA.ASSET_COUNT.key] || '1' },
{
header: ASSET_SCHEMA.LOCATION.ui,
sortKey: ASSET_SCHEMA.LOCATION.key,
align: 'center',
render: a => {
const loc = a[ASSET_SCHEMA.LOCATION.key] || '';
const detail = a[ASSET_SCHEMA.LOC_DETAIL.key] || '';
return detail ? `${loc}(${detail})` : (loc || '-');
}
},
{ header: ASSET_SCHEMA.MEMO.ui, sortKey: ASSET_SCHEMA.MEMO.key, className: 'col-memo', render: a => formatInline(a[ASSET_SCHEMA.MEMO.key] || '-') }
]
});
// Populate Location Options
const locSelect = container.querySelector('#filter-loc') as HTMLSelectElement;
if (locSelect) {
const locations = Array.from(new Set(fullList.map(a => a[ASSET_SCHEMA.LOCATION.key]))).filter(Boolean).sort();
locations.forEach(loc => {
const opt = document.createElement('option');
opt.value = String(loc);
opt.textContent = String(loc);
locSelect.appendChild(opt);
});
}
// Populate Dept Options
const deptSelect = container.querySelector('#filter-dept') as HTMLSelectElement;
if (deptSelect) {
const orgUnits = Array.from(new Set(fullList.map(a => a[ASSET_SCHEMA.CURRENT_DEPT.key]))).filter(Boolean).sort();
orgUnits.forEach(dept => {
const opt = document.createElement('option');
opt.value = String(dept);
opt.textContent = String(dept);
deptSelect.appendChild(opt);
});
}
updateTable();
}

View File

@@ -1,127 +1,39 @@
import { state } from '../../core/state';
import { openHwModal } from '../../components/Modal/HWModal';
import { formatInline, sortAssets, dynamicSort, renderPageHeader } from '../../core/utils';
import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema';
import { setupTableSorting, SortState } from '../../core/tableHandler';
import { renderFilterBar, applyCommonFilters } from '../../core/filterHandler';
import { createIcons, Paperclip, RefreshCcw, Plus } from 'lucide';
import { sortAssets, formatInline } from '../../core/utils';
import { ASSET_SCHEMA } from '../../core/schema';
import { createListView } from './ListFactory';
export function renderPcList(container: HTMLElement) {
renderPageHeader(container, 'PC');
// asset_pc 데이터 중 '서버PC' 유형은 제외하고 렌더링 (서버 리스트에서 보여줌)
const fullList = sortAssets((state.masterData.pc || []).filter((a: any) => a.asset_type !== '서버PC'));
let sortState: SortState = { key: '', direction: 'asc' };
let currentFilters = { keyword: '', corp: '', dept: '', field: '' };
const filterBar = document.createElement('div');
filterBar.className = 'search-bar';
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;" data-sort="${ASSET_SCHEMA.CURRENT_USER.key}">${ASSET_SCHEMA.CURRENT_USER.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.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.HDD3.key}">HDD3</th>
<th style="text-align:center;" data-sort="${ASSET_SCHEMA.HDD4.key}">HDD4</th>
<th style="text-align:center;" data-sort="${ASSET_SCHEMA.MAC_ADDR.key}">${ASSET_SCHEMA.MAC_ADDR.ui}</th>
<th class="col-memo" 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 = () => {
let filtered = applyCommonFilters(fullList, currentFilters, ['CURRENT_DEPT', 'CURRENT_USER', 'MODEL_NAME', 'MAC_ADDR', 'MANAGER_MAIN']);
if (sortState.key) {
filtered = dynamicSort(filtered, sortState.key, sortState.direction);
}
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>`;
return;
}
filtered.forEach((asset, idx) => {
const tr = document.createElement('tr');
tr.style.cursor = 'pointer';
tr.innerHTML = `
<td style="text-align:center;">${asset[ASSET_SCHEMA.CURRENT_USER.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;">${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;">${asset[ASSET_SCHEMA.HDD3.key]||'-'}</td>
<td style="text-align:center;">${asset[ASSET_SCHEMA.HDD4.key]||'-'}</td>
<td style="text-align:center; font-family:monospace; font-size:11px;">${asset[ASSET_SCHEMA.MAC_ADDR.key]||'-'}</td>
<td class="col-memo">${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: { Paperclip, RefreshCcw, Plus } });
};
renderFilterBar(filterBar, {
keywordLabel: `통합 검색 (${ASSET_SCHEMA.MODEL_NAME.ui}/${ASSET_SCHEMA.MANAGER_MAIN.ui}/${ASSET_SCHEMA.CURRENT_USER.ui})`,
showLoc: true,
showDept: true,
onFilterChange: (filters) => {
currentFilters = filters;
updateTable();
}
createListView(container, {
title: 'PC',
dataSource: () => sortAssets((state.masterData.pc || []).filter((a: any) => a.asset_type !== '서버PC')),
searchKeys: ['CURRENT_DEPT', 'CURRENT_USER', 'MODEL_NAME', 'MAC_ADDR', 'MANAGER_MAIN'],
filterOptions: {
keywordLabel: `통합 검색 (${ASSET_SCHEMA.MODEL_NAME.ui}/${ASSET_SCHEMA.MANAGER_MAIN.ui}/${ASSET_SCHEMA.CURRENT_USER.ui})`,
showLoc: true,
showDept: true
},
onRowClick: (asset) => openHwModal(asset, 'view'),
columns: [
{ header: ASSET_SCHEMA.CURRENT_USER.ui, sortKey: ASSET_SCHEMA.CURRENT_USER.key, align: 'center', render: a => a[ASSET_SCHEMA.CURRENT_USER.key] || '-' },
{ header: ASSET_SCHEMA.CPU.ui, sortKey: ASSET_SCHEMA.CPU.key, align: 'center', render: a => a[ASSET_SCHEMA.CPU.key] || '' },
{ header: ASSET_SCHEMA.MAINBOARD.ui, sortKey: ASSET_SCHEMA.MAINBOARD.key, align: 'center', render: a => a[ASSET_SCHEMA.MAINBOARD.key] || '-' },
{ header: ASSET_SCHEMA.RAM.ui, sortKey: ASSET_SCHEMA.RAM.key, align: 'center', render: a => a[ASSET_SCHEMA.RAM.key] || '' },
{ header: ASSET_SCHEMA.GPU.ui, sortKey: ASSET_SCHEMA.GPU.key, align: 'center', render: a => a[ASSET_SCHEMA.GPU.key] || '-' },
{ header: 'SSD1', sortKey: ASSET_SCHEMA.SSD1.key, align: 'center', render: a => a[ASSET_SCHEMA.SSD1.key] || '-' },
{ header: 'SSD2', sortKey: ASSET_SCHEMA.SSD2.key, align: 'center', render: a => a[ASSET_SCHEMA.SSD2.key] || '-' },
{ header: 'HDD1', sortKey: ASSET_SCHEMA.HDD1.key, align: 'center', render: a => a[ASSET_SCHEMA.HDD1.key] || '-' },
{ header: 'HDD2', sortKey: ASSET_SCHEMA.HDD2.key, align: 'center', render: a => a[ASSET_SCHEMA.HDD2.key] || '-' },
{ header: 'HDD3', sortKey: ASSET_SCHEMA.HDD3.key, align: 'center', render: a => a[ASSET_SCHEMA.HDD3.key] || '-' },
{ header: 'HDD4', sortKey: ASSET_SCHEMA.HDD4.key, align: 'center', render: a => a[ASSET_SCHEMA.HDD4.key] || '-' },
{
header: ASSET_SCHEMA.MAC_ADDR.ui,
sortKey: ASSET_SCHEMA.MAC_ADDR.key,
align: 'center',
render: a => `<span style="font-family:monospace; font-size:11px;">${a[ASSET_SCHEMA.MAC_ADDR.key] || '-'}</span>`
},
{ header: ASSET_SCHEMA.MEMO.ui, sortKey: ASSET_SCHEMA.MEMO.key, className: 'col-memo', render: a => formatInline(a[ASSET_SCHEMA.MEMO.key] || '-') }
]
});
// Populate Loc Options
const locSelect = container.querySelector('#filter-loc') as HTMLSelectElement;
if (locSelect) {
const locations = Array.from(new Set(fullList.map(a => a[ASSET_SCHEMA.LOCATION.key]))).filter(Boolean).sort();
locations.forEach(loc => {
const opt = document.createElement('option');
opt.value = String(loc);
opt.textContent = String(loc);
locSelect.appendChild(opt);
});
}
// Populate Dept Options
const deptSelect = container.querySelector('#filter-dept') as HTMLSelectElement;
if (deptSelect) {
const orgUnits = Array.from(new Set(fullList.map(a => a[ASSET_SCHEMA.CURRENT_DEPT.key] || a['현사용부서'] || a['현사용조직']))).filter(Boolean).sort();
orgUnits.forEach(dept => {
const opt = document.createElement('option');
opt.value = String(dept);
opt.textContent = String(dept);
deptSelect.appendChild(opt);
});
}
updateTable();
}

View File

@@ -1,127 +1,44 @@
import { state } from '../../core/state';
import { openHwModal } from '../../components/Modal/HWModal';
import { formatInline, sortAssets, dynamicSort, renderPageHeader } from '../../core/utils';
import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema';
import { setupTableSorting, SortState } from '../../core/tableHandler';
import { renderFilterBar, applyCommonFilters } from '../../core/filterHandler';
import { createIcons, RefreshCcw, Plus } from 'lucide';
import { sortAssets, formatInline } from '../../core/utils';
import { ASSET_SCHEMA } from '../../core/schema';
import { createListView } from './ListFactory';
/**
* PC부품 자산 목록 뷰
*/
export function renderPcPartList(container: HTMLElement) {
renderPageHeader(container, 'PC부품');
const fullList = sortAssets(state.masterData.equipment?.filter((a: any) => a.category === 'PC부품') || []);
let sortState: SortState = { key: '', direction: 'asc' };
let currentFilters = { keyword: '', loc: '', dept: '', field: '' };
const filterBar = document.createElement('div');
filterBar.className = 'search-bar';
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" 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="col-memo" 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 = () => {
let filtered = applyCommonFilters(fullList, currentFilters, ['MODEL_NAME', 'ASSET_TYPE']);
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';
const loc = asset[ASSET_SCHEMA.LOCATION.key] || '';
const detail = asset[ASSET_SCHEMA.LOC_DETAIL.key] || '';
const displayLoc = detail ? `${loc}(${detail})` : (loc || '-');
tr.innerHTML = `
<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">${displayLoc}</td>
<td class="col-memo">${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, Plus } });
};
renderFilterBar(filterBar, {
keywordLabel: `통합 검색 (${ASSET_SCHEMA.MODEL_NAME.ui})`,
showLoc: true,
showDept: true,
onFilterChange: (filters) => {
currentFilters = filters;
updateTable();
}
createListView(container, {
title: 'PC부품',
dataSource: () => sortAssets(state.masterData.equipment?.filter((a: any) => a.category === 'PC부품') || []),
searchKeys: ['MODEL_NAME', 'ASSET_TYPE'],
filterOptions: {
keywordLabel: `통합 검색 (${ASSET_SCHEMA.MODEL_NAME.ui})`,
showLoc: true,
showDept: true
},
onRowClick: (asset) => openHwModal(asset, 'view'),
columns: [
{
header: ASSET_SCHEMA.HW_STATUS.ui,
sortKey: ASSET_SCHEMA.HW_STATUS.key,
align: 'center',
render: a => `<span class="badge badge-success">${a[ASSET_SCHEMA.HW_STATUS.key] || '보관중'}</span>`
},
{ header: ASSET_SCHEMA.ASSET_TYPE.ui, sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', render: a => a[ASSET_SCHEMA.ASSET_TYPE.key] || '' },
{ header: ASSET_SCHEMA.ASSET_MFR.ui, sortKey: ASSET_SCHEMA.ASSET_MFR.key, align: 'center', render: a => a[ASSET_SCHEMA.ASSET_MFR.key] || '' },
{ header: ASSET_SCHEMA.MODEL_NAME.ui, sortKey: ASSET_SCHEMA.MODEL_NAME.key, render: a => formatInline(a[ASSET_SCHEMA.MODEL_NAME.key] || '-') },
{ header: ASSET_SCHEMA.VOLUME.ui, sortKey: ASSET_SCHEMA.VOLUME.key, align: 'center', render: a => a[ASSET_SCHEMA.VOLUME.key] || '-' },
{ header: ASSET_SCHEMA.MONITOR_INCH.ui, sortKey: ASSET_SCHEMA.MONITOR_INCH.key, align: 'center', render: a => a[ASSET_SCHEMA.MONITOR_INCH.key] || '-' },
{ header: ASSET_SCHEMA.ASSET_COUNT.ui, sortKey: ASSET_SCHEMA.ASSET_COUNT.key, align: 'center', render: a => a[ASSET_SCHEMA.ASSET_COUNT.key] || '1' },
{
header: ASSET_SCHEMA.LOCATION.ui,
sortKey: ASSET_SCHEMA.LOCATION.key,
align: 'center',
render: a => {
const loc = a[ASSET_SCHEMA.LOCATION.key] || '';
const detail = a[ASSET_SCHEMA.LOC_DETAIL.key] || '';
return detail ? `${loc}(${detail})` : (loc || '-');
}
},
{ header: ASSET_SCHEMA.MEMO.ui, sortKey: ASSET_SCHEMA.MEMO.key, className: 'col-memo', render: a => formatInline(a[ASSET_SCHEMA.MEMO.key] || '-') }
]
});
// Populate Location Options
const locSelect = container.querySelector('#filter-loc') as HTMLSelectElement;
if (locSelect) {
const locations = Array.from(new Set(fullList.map(a => a[ASSET_SCHEMA.LOCATION.key]))).filter(Boolean).sort();
locations.forEach(loc => {
const opt = document.createElement('option');
opt.value = String(loc);
opt.textContent = String(loc);
locSelect.appendChild(opt);
});
}
// Populate Dept Options
const deptSelect = container.querySelector('#filter-dept') as HTMLSelectElement;
if (deptSelect) {
const orgUnits = Array.from(new Set(fullList.map(a => a[ASSET_SCHEMA.CURRENT_DEPT.key]))).filter(Boolean).sort();
orgUnits.forEach(dept => {
const opt = document.createElement('option');
opt.value = String(dept);
opt.textContent = String(dept);
deptSelect.appendChild(opt);
});
}
updateTable();
}

View File

@@ -1,125 +1,44 @@
import { state } from '../../core/state';
import { openHwModal } from '../../components/Modal/HWModal';
import { formatInline, sortAssets, dynamicSort, renderPageHeader } from '../../core/utils';
import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema';
import { setupTableSorting, SortState } from '../../core/tableHandler';
import { renderFilterBar, applyCommonFilters } from '../../core/filterHandler';
import { createIcons, RefreshCcw, Plus } from 'lucide';
import { sortAssets, formatInline } from '../../core/utils';
import { ASSET_SCHEMA } from '../../core/schema';
import { createListView } from './ListFactory';
export function renderServerList(container: HTMLElement) {
renderPageHeader(container, '서버');
// asset_server 데이터와 asset_pc 데이터 중 '서버PC' 유형만 추출하여 병합
const serverList = state.masterData.server || [];
const serverPcList = (state.masterData.pc || []).filter((a: any) => a.asset_type === '서버PC');
const fullList = sortAssets([...serverList, ...serverPcList]);
let sortState: SortState = { key: '', direction: 'asc' };
let currentFilters = { keyword: '', loc: '', dept: '', field: '' };
const filterBar = document.createElement('div');
filterBar.className = 'search-bar';
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" data-sort="${ASSET_SCHEMA.CURRENT_DEPT.key}">${ASSET_SCHEMA.CURRENT_DEPT.ui}</th>
<th style="width: 15%;" data-sort="${ASSET_SCHEMA.ASSET_PURPOSE.key}">${ASSET_SCHEMA.ASSET_PURPOSE.ui}</th>
<th class="text-center" style="width: 10%;" data-sort="${ASSET_SCHEMA.ASSET_TYPE.key}">${ASSET_SCHEMA.ASSET_TYPE.ui}</th>
<th style="width: 15%;">모델/메인보드</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.LOCATION.key}">${ASSET_SCHEMA.LOCATION.ui}</th>
<th class="col-memo" style="width: 35%;" 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 = () => {
let filtered = applyCommonFilters(fullList, currentFilters, ['CURRENT_DEPT', 'MODEL_NAME', 'ASSET_PURPOSE']);
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';
const loc = asset[ASSET_SCHEMA.LOCATION.key] || '';
const detail = asset[ASSET_SCHEMA.LOC_DETAIL.key] || '';
const displayLoc = detail ? `${loc}(${detail})` : (loc || '-');
const modelOrMainboard = asset[ASSET_SCHEMA.MODEL_NAME.key]
|| asset[ASSET_SCHEMA.ASSET_NAME.key]
|| asset[ASSET_SCHEMA.MAINBOARD.key]
|| '-';
tr.innerHTML = `
<td class="text-center">${asset[ASSET_SCHEMA.CURRENT_DEPT.key]||'-'}</td>
<td>${formatInline(asset[ASSET_SCHEMA.ASSET_PURPOSE.key]||'-')}</td>
<td class="text-center">${asset[ASSET_SCHEMA.ASSET_TYPE.key]||'-'}</td>
<td>${formatInline(modelOrMainboard)}</td>
<td class="text-center">${displayLoc}</td>
<td class="col-memo">${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, Plus } });
};
renderFilterBar(filterBar, {
keywordLabel: `통합 검색 (${ASSET_SCHEMA.CURRENT_DEPT.ui}/${ASSET_SCHEMA.MODEL_NAME.ui})`,
showLoc: true,
showDept: true,
onFilterChange: (filters) => {
currentFilters = filters;
updateTable();
}
createListView(container, {
title: '서버',
dataSource: () => {
const serverList = state.masterData.server || [];
const serverPcList = (state.masterData.pc || []).filter((a: any) => a.asset_type === '서버PC');
return sortAssets([...serverList, ...serverPcList]);
},
searchKeys: ['CURRENT_DEPT', 'MODEL_NAME', 'ASSET_PURPOSE'],
filterOptions: {
keywordLabel: `통합 검색 (${ASSET_SCHEMA.CURRENT_DEPT.ui}/${ASSET_SCHEMA.MODEL_NAME.ui})`,
showLoc: true,
showDept: true
},
onRowClick: (asset) => openHwModal(asset, 'view'),
columns: [
{ header: ASSET_SCHEMA.CURRENT_DEPT.ui, sortKey: ASSET_SCHEMA.CURRENT_DEPT.key, align: 'center', render: a => a[ASSET_SCHEMA.CURRENT_DEPT.key] || '-' },
{ header: ASSET_SCHEMA.ASSET_PURPOSE.ui, sortKey: ASSET_SCHEMA.ASSET_PURPOSE.key, width: '15%', render: a => formatInline(a[ASSET_SCHEMA.ASSET_PURPOSE.key] || '-') },
{ header: ASSET_SCHEMA.ASSET_TYPE.ui, sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', width: '10%', render: a => a[ASSET_SCHEMA.ASSET_TYPE.key] || '-' },
{
header: '모델/메인보드',
width: '15%',
render: a => formatInline(a[ASSET_SCHEMA.MODEL_NAME.key] || a[ASSET_SCHEMA.ASSET_NAME.key] || a[ASSET_SCHEMA.MAINBOARD.key] || '-')
},
{
header: ASSET_SCHEMA.LOCATION.ui,
sortKey: ASSET_SCHEMA.LOCATION.key,
align: 'center',
render: a => {
const loc = a[ASSET_SCHEMA.LOCATION.key] || '';
const detail = a[ASSET_SCHEMA.LOC_DETAIL.key] || '';
return detail ? `${loc}(${detail})` : (loc || '-');
}
},
{ header: ASSET_SCHEMA.MEMO.ui, sortKey: ASSET_SCHEMA.MEMO.key, width: '35%', className: 'col-memo', render: a => formatInline(a[ASSET_SCHEMA.MEMO.key] || '-') }
]
});
// Populate Location Options
const locSelect = container.querySelector('#filter-loc') as HTMLSelectElement;
if (locSelect) {
const locations = Array.from(new Set(fullList.map(a => a[ASSET_SCHEMA.LOCATION.key]))).filter(Boolean).sort();
locations.forEach(loc => {
const opt = document.createElement('option');
opt.value = String(loc);
opt.textContent = String(loc);
locSelect.appendChild(opt);
});
}
// Populate Dept Options
const deptSelect = container.querySelector('#filter-dept') as HTMLSelectElement;
if (deptSelect) {
const orgUnits = Array.from(new Set(fullList.map(a => a[ASSET_SCHEMA.CURRENT_DEPT.key]))).filter(Boolean).sort();
orgUnits.forEach(dept => {
const opt = document.createElement('option');
opt.value = dept;
opt.textContent = dept;
deptSelect.appendChild(opt);
});
}
updateTable();
}

View File

@@ -1,121 +1,41 @@
import { state } from '../../core/state';
import { openHwModal } from '../../components/Modal/HWModal';
import { formatInline, sortAssets, dynamicSort, renderPageHeader } from '../../core/utils';
import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema';
import { setupTableSorting, SortState } from '../../core/tableHandler';
import { renderFilterBar, applyCommonFilters } from '../../core/filterHandler';
import { createIcons, RefreshCcw, Plus } from 'lucide';
import { sortAssets, formatInline } from '../../core/utils';
import { ASSET_SCHEMA } from '../../core/schema';
import { createListView } from './ListFactory';
/**
* 공간정보장비 자산 목록 뷰
*/
export function renderSpaceInfoList(container: HTMLElement) {
renderPageHeader(container, '공간정보장비');
const fullList = sortAssets(state.masterData.equipment?.filter((a: any) => a.category === '공간정보장비') || []);
let sortState: SortState = { key: '', direction: 'asc' };
let currentFilters = { keyword: '', loc: '', dept: '', field: '' };
const filterBar = document.createElement('div');
filterBar.className = 'search-bar';
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" data-sort="${ASSET_SCHEMA.HW_STATUS.key}">${ASSET_SCHEMA.HW_STATUS.ui}</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.CURRENT_USER.key}">${ASSET_SCHEMA.CURRENT_USER.ui}</th>
<th data-sort="${ASSET_SCHEMA.ASSET_NAME.key}">${ASSET_SCHEMA.ASSET_NAME.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.LOCATION.key}">${ASSET_SCHEMA.LOCATION.ui}</th>
<th class="col-memo" 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 = () => {
let filtered = applyCommonFilters(fullList, currentFilters, ['MODEL_NAME', 'PRODUCT_NAME', 'CURRENT_USER']);
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';
const loc = asset[ASSET_SCHEMA.LOCATION.key] || '';
const detail = asset[ASSET_SCHEMA.LOC_DETAIL.key] || '';
const displayLoc = detail ? `${loc}(${detail})` : (loc || '-');
tr.innerHTML = `
<td class="text-center"><span class="badge badge-success">${asset[ASSET_SCHEMA.HW_STATUS.key] || '운영중'}</span></td>
<td class="text-center">${asset[ASSET_SCHEMA.CURRENT_USER.key] || '-'}</td>
<td>${formatInline(asset[ASSET_SCHEMA.PRODUCT_NAME.key] || asset[ASSET_SCHEMA.MODEL_NAME.key] || asset[ASSET_SCHEMA.ASSET_NAME.key] || '-')}</td>
<td class="text-center">${asset[ASSET_SCHEMA.ASSET_TYPE.key] || ''}</td>
<td class="text-center">${displayLoc}</td>
<td class="col-memo">${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, Plus } });
};
renderFilterBar(filterBar, {
keywordLabel: `통합 검색 (${ASSET_SCHEMA.MODEL_NAME.ui}/${ASSET_SCHEMA.CURRENT_USER.ui})`,
showLoc: true,
showDept: true,
onFilterChange: (filters) => {
currentFilters = filters;
updateTable();
}
createListView(container, {
title: '공간정보장비',
dataSource: () => sortAssets(state.masterData.equipment?.filter((a: any) => a.category === '공간정보장비') || []),
searchKeys: ['MODEL_NAME', 'PRODUCT_NAME', 'CURRENT_USER'],
filterOptions: {
keywordLabel: `통합 검색 (${ASSET_SCHEMA.MODEL_NAME.ui}/${ASSET_SCHEMA.CURRENT_USER.ui})`,
showLoc: true,
showDept: true
},
onRowClick: (asset) => openHwModal(asset, 'view'),
columns: [
{
header: ASSET_SCHEMA.HW_STATUS.ui,
sortKey: ASSET_SCHEMA.HW_STATUS.key,
align: 'center',
render: a => `<span class="badge badge-success">${a[ASSET_SCHEMA.HW_STATUS.key] || '운영중'}</span>`
},
{ header: ASSET_SCHEMA.CURRENT_USER.ui, sortKey: ASSET_SCHEMA.CURRENT_USER.key, align: 'center', render: a => a[ASSET_SCHEMA.CURRENT_USER.key] || '-' },
{ header: ASSET_SCHEMA.ASSET_NAME.ui, sortKey: ASSET_SCHEMA.ASSET_NAME.key, render: a => formatInline(a[ASSET_SCHEMA.PRODUCT_NAME.key] || a[ASSET_SCHEMA.MODEL_NAME.key] || a[ASSET_SCHEMA.ASSET_NAME.key] || '-') },
{ header: ASSET_SCHEMA.ASSET_TYPE.ui, sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', render: a => a[ASSET_SCHEMA.ASSET_TYPE.key] || '' },
{
header: ASSET_SCHEMA.LOCATION.ui,
sortKey: ASSET_SCHEMA.LOCATION.key,
align: 'center',
render: a => {
const loc = a[ASSET_SCHEMA.LOCATION.key] || '';
const detail = a[ASSET_SCHEMA.LOC_DETAIL.key] || '';
return detail ? `${loc}(${detail})` : (loc || '-');
}
},
{ header: ASSET_SCHEMA.MEMO.ui, sortKey: ASSET_SCHEMA.MEMO.key, className: 'col-memo', render: a => formatInline(a[ASSET_SCHEMA.MEMO.key] || '-') }
]
});
// Populate Location Options
const locSelect = container.querySelector('#filter-loc') as HTMLSelectElement;
if (locSelect) {
const locations = Array.from(new Set(fullList.map(a => a[ASSET_SCHEMA.LOCATION.key]))).filter(Boolean).sort();
locations.forEach(loc => {
const opt = document.createElement('option');
opt.value = String(loc);
opt.textContent = String(loc);
locSelect.appendChild(opt);
});
}
// Populate Dept Options
const deptSelect = container.querySelector('#filter-dept') as HTMLSelectElement;
if (deptSelect) {
const orgUnits = Array.from(new Set(fullList.map(a => a[ASSET_SCHEMA.CURRENT_DEPT.key]))).filter(Boolean).sort();
orgUnits.forEach(dept => {
const opt = document.createElement('option');
opt.value = String(dept);
opt.textContent = String(dept);
deptSelect.appendChild(opt);
});
}
updateTable();
}

View File

@@ -1,123 +1,38 @@
import { state } from '../../core/state';
import { openHwModal } from '../../components/Modal/HWModal';
import { formatInline, sortAssets, dynamicSort, renderPageHeader } from '../../core/utils';
import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema';
import { setupTableSorting, SortState } from '../../core/tableHandler';
import { renderFilterBar, applyCommonFilters } from '../../core/filterHandler';
import { createIcons, RefreshCcw, Plus } from 'lucide';
import { sortAssets, formatInline } from '../../core/utils';
import { ASSET_SCHEMA } from '../../core/schema';
import { createListView } from './ListFactory';
/**
* 스토리지 자산 목록 뷰
*/
export function renderStorageList(container: HTMLElement) {
renderPageHeader(container, '스토리지');
const fullList = sortAssets(state.masterData.storage);
let sortState: SortState = { key: '', direction: 'asc' };
let currentFilters = { keyword: '', loc: '', dept: '', field: '' };
const filterBar = document.createElement('div');
filterBar.className = 'search-bar';
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" data-sort="${ASSET_SCHEMA.HW_STATUS.key}">${ASSET_SCHEMA.HW_STATUS.ui}</th>
<th class="text-center" data-sort="${ASSET_SCHEMA.CURRENT_USER.key}">${ASSET_SCHEMA.CURRENT_USER.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.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="col-memo" 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 = () => {
let filtered = applyCommonFilters(fullList, currentFilters, ['MODEL_NAME', 'CURRENT_USER', 'SERIAL_NUM']);
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';
const loc = asset[ASSET_SCHEMA.LOCATION.key] || '';
const detail = asset[ASSET_SCHEMA.LOC_DETAIL.key] || '';
const displayLoc = detail ? `${loc}(${detail})` : (loc || '-');
tr.innerHTML = `
<td class="text-center">${asset[ASSET_SCHEMA.HW_STATUS.key]||'-'}</td>
<td class="text-center">${asset[ASSET_SCHEMA.CURRENT_USER.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">${displayLoc}</td>
<td class="col-memo">${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, Plus } });
};
renderFilterBar(filterBar, {
keywordLabel: `통합 검색 (${ASSET_SCHEMA.MODEL_NAME.ui}/${ASSET_SCHEMA.CURRENT_USER.ui})`,
showLoc: true,
showDept: true,
onFilterChange: (filters) => {
currentFilters = filters;
updateTable();
}
createListView(container, {
title: '스토리지',
dataSource: () => sortAssets(state.masterData.storage || []),
searchKeys: ['MODEL_NAME', 'CURRENT_USER', 'SERIAL_NUM'],
filterOptions: {
keywordLabel: `통합 검색 (${ASSET_SCHEMA.MODEL_NAME.ui}/${ASSET_SCHEMA.CURRENT_USER.ui})`,
showLoc: true,
showDept: true
},
onRowClick: (asset) => openHwModal(asset, 'view'),
columns: [
{ header: ASSET_SCHEMA.HW_STATUS.ui, sortKey: ASSET_SCHEMA.HW_STATUS.key, align: 'center', render: a => a[ASSET_SCHEMA.HW_STATUS.key] || '-' },
{ header: ASSET_SCHEMA.CURRENT_USER.ui, sortKey: ASSET_SCHEMA.CURRENT_USER.key, align: 'center', render: a => a[ASSET_SCHEMA.CURRENT_USER.key] || '-' },
{ header: ASSET_SCHEMA.ASSET_TYPE.ui, sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', render: a => a[ASSET_SCHEMA.ASSET_TYPE.key] || '-' },
{ header: ASSET_SCHEMA.VOLUME.ui, sortKey: ASSET_SCHEMA.VOLUME.key, align: 'center', render: a => a[ASSET_SCHEMA.VOLUME.key] || '-' },
{ header: ASSET_SCHEMA.MODEL_NAME.ui, sortKey: ASSET_SCHEMA.MODEL_NAME.key, render: a => formatInline(a[ASSET_SCHEMA.MODEL_NAME.key] || a[ASSET_SCHEMA.ASSET_NAME.key] || '-') },
{ header: ASSET_SCHEMA.SERIAL_NUM.ui, sortKey: ASSET_SCHEMA.SERIAL_NUM.key, align: 'center', render: a => a[ASSET_SCHEMA.SERIAL_NUM.key] || '-' },
{
header: ASSET_SCHEMA.LOCATION.ui,
sortKey: ASSET_SCHEMA.LOCATION.key,
align: 'center',
render: a => {
const loc = a[ASSET_SCHEMA.LOCATION.key] || '';
const detail = a[ASSET_SCHEMA.LOC_DETAIL.key] || '';
return detail ? `${loc}(${detail})` : (loc || '-');
}
},
{ header: ASSET_SCHEMA.MEMO.ui, sortKey: ASSET_SCHEMA.MEMO.key, className: 'col-memo', render: a => formatInline(a[ASSET_SCHEMA.MEMO.key] || '-') }
]
});
// Populate Location Options
const locSelect = container.querySelector('#filter-loc') as HTMLSelectElement;
if (locSelect) {
const locations = Array.from(new Set(fullList.map(a => a[ASSET_SCHEMA.LOCATION.key]))).filter(Boolean).sort();
locations.forEach(loc => {
const opt = document.createElement('option');
opt.value = String(loc);
opt.textContent = String(loc);
locSelect.appendChild(opt);
});
}
// Populate Dept Options
const deptSelect = container.querySelector('#filter-dept') as HTMLSelectElement;
if (deptSelect) {
const orgUnits = Array.from(new Set(fullList.map(a => a[ASSET_SCHEMA.CURRENT_DEPT.key]))).filter(Boolean).sort();
orgUnits.forEach(dept => {
const opt = document.createElement('option');
opt.value = String(dept);
opt.textContent = String(dept);
deptSelect.appendChild(opt);
});
}
updateTable();
}

View File

@@ -1,145 +1,42 @@
import { state } from '../../core/state';
import { openSwModal } from '../../components/Modal/SWModal';
import { sortAssets, dynamicSort, formatInline, getActionButtonsHTML, renderPageHeader } from '../../core/utils';
import { setupTableSorting, SortState } from '../../core/tableHandler';
import { sortAssets, formatInline } from '../../core/utils';
import { ASSET_SCHEMA } from '../../core/schema';
import { renderFilterBar, applyCommonFilters } from '../../core/filterHandler';
import { createIcons, Edit2, Users, RefreshCcw, Plus } from 'lucide';
import { createListView } from './ListFactory';
export function renderSwList(container: HTMLElement) {
const isInternal = state.activeSubTab === '내부';
renderPageHeader(container, isInternal ? '내부' : '외부');
const fullList = sortAssets(isInternal ? state.masterData.swInternal : state.masterData.swExternal);
let sortState: SortState = { key: '', direction: 'asc' };
let currentFilters = { keyword: '', corp: '', dept: '', field: '' };
const filterBar = document.createElement('div');
filterBar.className = 'search-bar';
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;" 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 class="col-memo" 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;" 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;">${ASSET_SCHEMA.CURRENT_USER.ui}</th>
<th style="text-align:center;">${ASSET_SCHEMA.PREV_USER.ui}</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.EXPIRED_DATE.key}">만료일</th>
<th class="col-memo" 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 = () => {
let filtered = applyCommonFilters(fullList, currentFilters, ['PRODUCT_NAME', 'CURRENT_USER', 'CURRENT_DEPT']);
if (sortState.key) {
filtered = dynamicSort(filtered, sortState.key, sortState.direction);
}
tbody.innerHTML = '';
if (filtered.length === 0) {
tbody.innerHTML = `<tr><td colspan="${isInternal ? 5 : 11}" style="text-align:center; padding: 3rem; color: var(--text-muted);">검색 결과가 없습니다.</td></tr>`;
return;
}
filtered.forEach((asset, idx) => {
if (isInternal) {
const tr = document.createElement('tr');
tr.style.cursor = 'pointer';
tr.innerHTML = `
<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 class="col-memo">${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';
tr.innerHTML = `
<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;">${asset[ASSET_SCHEMA.CURRENT_USER.key]||'-'}</td>
<td style="text-align:center;">${asset[ASSET_SCHEMA.PREV_USER.key]||'-'}</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.EXPIRED_DATE.key]||''}</td>
<td class="col-memo">${formatInline(asset[ASSET_SCHEMA.MEMO.key]||'-')}</td>
`;
tr.addEventListener('click', () => openSwModal(asset, 'view'));
tbody.appendChild(tr);
}
});
setupTableSorting(table, sortState, (key, dir) => {
sortState = { key, direction: dir };
updateTable();
});
createIcons({ icons: { Edit2, Users, RefreshCcw, Plus } });
};
renderFilterBar(filterBar, {
keywordLabel: `통합 검색 (${ASSET_SCHEMA.PRODUCT_NAME.ui}/${ASSET_SCHEMA.CURRENT_DEPT.ui})`,
showField: true,
showCorp: true,
showDept: true,
onFilterChange: (filters) => {
currentFilters = filters;
updateTable();
}
createListView(container, {
title: isInternal ? '내부' : '외부',
dataSource: () => sortAssets(isInternal ? state.masterData.swInternal : state.masterData.swExternal),
searchKeys: ['PRODUCT_NAME', 'CURRENT_USER', 'CURRENT_DEPT'],
emptyMessage: '검색 결과가 없습니다.',
filterOptions: {
keywordLabel: `통합 검색 (${ASSET_SCHEMA.PRODUCT_NAME.ui}/${ASSET_SCHEMA.CURRENT_DEPT.ui})`,
showField: true,
showCorp: true,
showDept: true
},
onRowClick: (asset) => openSwModal(asset, 'view'),
columns: isInternal ? [
{ header: ASSET_SCHEMA.SW_FIELD.ui, sortKey: ASSET_SCHEMA.SW_FIELD.key, align: 'center', render: a => a[ASSET_SCHEMA.SW_FIELD.key] || '' },
{ header: ASSET_SCHEMA.DEV_OBJ.ui, sortKey: ASSET_SCHEMA.DEV_OBJ.key, align: 'center', render: a => a[ASSET_SCHEMA.DEV_OBJ.key] || '' },
{ header: ASSET_SCHEMA.SW_STATUS.ui, sortKey: ASSET_SCHEMA.SW_STATUS.key, align: 'center', render: a => a[ASSET_SCHEMA.SW_STATUS.key] || '보유중' },
{ header: ASSET_SCHEMA.SW_TYPE.ui, sortKey: ASSET_SCHEMA.SW_TYPE.key, align: 'center', render: a => a[ASSET_SCHEMA.SW_TYPE.key] || '내부' },
{ header: ASSET_SCHEMA.MEMO.ui, sortKey: ASSET_SCHEMA.MEMO.key, className: 'col-memo', render: a => formatInline(a[ASSET_SCHEMA.MEMO.key] || '-') }
] : [
{ header: '자산명', sortKey: ASSET_SCHEMA.PRODUCT_NAME.key, render: a => a[ASSET_SCHEMA.PRODUCT_NAME.key] || '' },
{ header: '유형', sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', render: a => a[ASSET_SCHEMA.ASSET_TYPE.key] || '외부' },
{ header: ASSET_SCHEMA.SW_STATUS.ui, sortKey: ASSET_SCHEMA.SW_STATUS.key, align: 'center', render: a => a[ASSET_SCHEMA.SW_STATUS.key] || '사용중' },
{ header: ASSET_SCHEMA.SW_FIELD.ui, sortKey: ASSET_SCHEMA.SW_FIELD.key, align: 'center', render: a => a[ASSET_SCHEMA.SW_FIELD.key] || '' },
{ header: ASSET_SCHEMA.CURRENT_DEPT.ui, sortKey: ASSET_SCHEMA.CURRENT_DEPT.key, align: 'center', render: a => a[ASSET_SCHEMA.CURRENT_DEPT.key] || '' },
{ header: ASSET_SCHEMA.CURRENT_USER.ui, align: 'center', render: a => a[ASSET_SCHEMA.CURRENT_USER.key] || '-' },
{ header: ASSET_SCHEMA.PREV_USER.ui, align: 'center', render: a => a[ASSET_SCHEMA.PREV_USER.key] || '-' },
{ header: '구매연월', sortKey: ASSET_SCHEMA.PURCHASE_DATE.key, align: 'center', render: a => a[ASSET_SCHEMA.PURCHASE_DATE.key] || '' },
{ header: '시작일', align: 'center', render: a => a[ASSET_SCHEMA.PURCHASE_DATE.key] || '' },
{ header: '만료일', sortKey: ASSET_SCHEMA.EXPIRED_DATE.key, align: 'center', render: a => a[ASSET_SCHEMA.EXPIRED_DATE.key] || '' },
{ header: ASSET_SCHEMA.MEMO.ui, sortKey: ASSET_SCHEMA.MEMO.key, className: 'col-memo', render: a => formatInline(a[ASSET_SCHEMA.MEMO.key] || '-') }
]
});
// Populate Dept Options
const deptSelect = container.querySelector('#filter-dept') as HTMLSelectElement;
if (deptSelect) {
const orgUnits = Array.from(new Set(fullList.map(a => a[ASSET_SCHEMA.CURRENT_DEPT.key]))).filter(Boolean).sort();
orgUnits.forEach(dept => {
const opt = document.createElement('option');
opt.value = String(dept);
opt.textContent = String(dept);
deptSelect.appendChild(opt);
});
}
updateTable();
}

View File

@@ -13,7 +13,6 @@ import { renderSpaceInfoList } from './List/SpaceInfoListView';
import { renderGiftList } from './List/GiftListView';
import { renderFacilityList } from './List/FacilityListView';
import { renderCostList } from './List/CostListView';
import { renderUserList } from './List/UserListView';
import { createIcons, Plus, X, LayoutDashboard, Monitor, Server, Database, Laptop, CalendarClock, Key, Cpu, Layers, Users, Paperclip, Edit2, RefreshCcw } from 'lucide';
/**