refactor: complete modal class-based architecture, design system integration, and map editor modularization
This commit is contained in:
@@ -8,17 +8,19 @@ export function renderCloudList(container: HTMLElement) {
|
||||
createListView(container, {
|
||||
title: '클라우드',
|
||||
dataSource: () => state.masterData.cloud || [],
|
||||
searchKeys: ['PRODUCT_NAME', 'ASSET_PURPOSE', 'PURCHASE_VENDOR'],
|
||||
searchKeys: ['PRODUCT_NAME', 'ASSET_PURPOSE', 'PURCHASE_VENDOR', 'ASSET_TYPE'],
|
||||
filterOptions: {
|
||||
keywordLabel: `통합 검색 (${ASSET_SCHEMA.PRODUCT_NAME.ui}/${ASSET_SCHEMA.PURCHASE_VENDOR.ui})`,
|
||||
showCorp: true,
|
||||
showDept: true
|
||||
showDept: true,
|
||||
showType: 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.ASSET_TYPE.ui, sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', width: '10%', render: a => a[ASSET_SCHEMA.ASSET_TYPE.key] || '-' },
|
||||
{
|
||||
header: ASSET_SCHEMA.PURCHASE_AMOUNT.ui,
|
||||
sortKey: ASSET_SCHEMA.PURCHASE_AMOUNT.key,
|
||||
|
||||
@@ -7,15 +7,16 @@ export function renderCostList(container: HTMLElement) {
|
||||
createListView(container, {
|
||||
title: '비용관리',
|
||||
dataSource: () => sortAssets(state.masterData.cloud?.filter((a: any) => a.category === '비용관리') || []),
|
||||
searchKeys: ['PRODUCT_NAME', 'MANAGER_MAIN', 'EMAIL_ACCOUNT'],
|
||||
searchKeys: ['PRODUCT_NAME', 'MANAGER_MAIN', 'EMAIL_ACCOUNT', 'ASSET_TYPE'],
|
||||
filterOptions: {
|
||||
keywordLabel: `통합 검색 (${ASSET_SCHEMA.PRODUCT_NAME.ui}/${ASSET_SCHEMA.MANAGER_MAIN.ui})`,
|
||||
showCorp: true,
|
||||
showDept: true
|
||||
showDept: true,
|
||||
showType: 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_TYPE.ui, sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', width: '10%', 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] || '-' },
|
||||
{
|
||||
|
||||
@@ -12,24 +12,20 @@ export function renderDomainList(container: HTMLElement) {
|
||||
createListView(container, {
|
||||
title: '도메인',
|
||||
dataSource: () => state.masterData.domain || [],
|
||||
searchKeys: ['DOMAIN_ADDR', 'ASSET_PURPOSE', 'PRODUCT_NAME'],
|
||||
searchKeys: ['DOMAIN_ADDR', 'ASSET_PURPOSE', 'PRODUCT_NAME', 'ASSET_TYPE'],
|
||||
persistentSortState,
|
||||
emptyMessage: '등록된 도메인 정보가 없습니다.',
|
||||
filterOptions: {
|
||||
keywordLabel: `통합 검색 (${ASSET_SCHEMA.DOMAIN_ADDR.ui}/${ASSET_SCHEMA.PRODUCT_NAME.ui})`,
|
||||
showCorp: true,
|
||||
showDept: true
|
||||
showDept: true,
|
||||
showType: 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.ASSET_TYPE.ui, sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', width: '10%', render: a => a[ASSET_SCHEMA.ASSET_TYPE.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.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] || '-') }
|
||||
|
||||
@@ -8,11 +8,12 @@ export function renderEquipmentList(container: HTMLElement) {
|
||||
createListView(container, {
|
||||
title: '업무지원장비',
|
||||
dataSource: () => sortAssets(state.masterData.equipment || []),
|
||||
searchKeys: ['MODEL_NAME', 'CURRENT_USER', 'ASSET_MFR'],
|
||||
searchKeys: ['MODEL_NAME', 'CURRENT_USER', 'ASSET_MFR', 'ASSET_TYPE'],
|
||||
filterOptions: {
|
||||
keywordLabel: `통합 검색 (${ASSET_SCHEMA.MODEL_NAME.ui}/${ASSET_SCHEMA.ASSET_MFR.ui})`,
|
||||
showLoc: true,
|
||||
showDept: true
|
||||
showDept: true,
|
||||
showType: true
|
||||
},
|
||||
onRowClick: (asset) => openHwModal(asset, 'view'),
|
||||
columns: [
|
||||
@@ -23,7 +24,7 @@ export function renderEquipmentList(container: HTMLElement) {
|
||||
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_TYPE.ui, sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', width: '10%', 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' },
|
||||
|
||||
@@ -8,11 +8,12 @@ export function renderFacilityList(container: HTMLElement) {
|
||||
createListView(container, {
|
||||
title: '사무가구',
|
||||
dataSource: () => sortAssets(state.masterData.equipment?.filter((a: any) => a.category === '시설자산') || []),
|
||||
searchKeys: ['MODEL_NAME', 'ASSET_MFR'],
|
||||
searchKeys: ['MODEL_NAME', 'ASSET_MFR', 'ASSET_TYPE'],
|
||||
filterOptions: {
|
||||
keywordLabel: `통합 검색 (${ASSET_SCHEMA.MODEL_NAME.ui})`,
|
||||
showLoc: true,
|
||||
showDept: true
|
||||
showDept: true,
|
||||
showType: true
|
||||
},
|
||||
onRowClick: (asset) => openHwModal(asset, 'view'),
|
||||
columns: [
|
||||
@@ -22,7 +23,7 @@ export function renderFacilityList(container: HTMLElement) {
|
||||
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_TYPE.ui, sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', width: '10%', 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] || '-') },
|
||||
{
|
||||
|
||||
@@ -7,15 +7,17 @@ export function renderGiftList(container: HTMLElement) {
|
||||
createListView(container, {
|
||||
title: '선물',
|
||||
dataSource: () => sortAssets(state.masterData.equipment?.filter((a: any) => a.category === '선물') || []),
|
||||
searchKeys: ['PRODUCT_NAME', 'MODEL_NAME'],
|
||||
searchKeys: ['PRODUCT_NAME', 'MODEL_NAME', 'ASSET_TYPE'],
|
||||
filterOptions: {
|
||||
keywordLabel: `통합 검색 (${ASSET_SCHEMA.PRODUCT_NAME.ui})`,
|
||||
showCorp: true,
|
||||
showDept: true
|
||||
showDept: true,
|
||||
showType: 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: ASSET_SCHEMA.ASSET_TYPE.ui, sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', width: '10%', render: a => a[ASSET_SCHEMA.ASSET_TYPE.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' },
|
||||
|
||||
@@ -23,6 +23,7 @@ export interface ListViewConfig {
|
||||
showDept?: boolean;
|
||||
showLoc?: boolean;
|
||||
showField?: boolean;
|
||||
showType?: boolean;
|
||||
};
|
||||
columns: ColumnDef[];
|
||||
onRowClick?: (asset: any) => void;
|
||||
@@ -37,7 +38,7 @@ export function createListView(container: HTMLElement, config: ListViewConfig) {
|
||||
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: '' };
|
||||
let currentFilters: any = { keyword: '', corp: '', dept: '', loc: '', field: '', type: '' };
|
||||
|
||||
const filterBar = document.createElement('div');
|
||||
filterBar.className = 'search-bar';
|
||||
@@ -151,6 +152,7 @@ export function createListView(container: HTMLElement, config: ListViewConfig) {
|
||||
if (config.filterOptions.showLoc) populateSelect('#filter-loc', ASSET_SCHEMA.LOCATION.key);
|
||||
if (config.filterOptions.showDept) populateSelect('#filter-dept', ASSET_SCHEMA.CURRENT_DEPT.key);
|
||||
if (config.filterOptions.showCorp) populateSelect('#filter-corp', ASSET_SCHEMA.PURCHASE_CORP.key);
|
||||
if (config.filterOptions.showType) populateSelect('#filter-type', ASSET_SCHEMA.ASSET_TYPE.key);
|
||||
|
||||
// 6. 초기 렌더링
|
||||
updateTable();
|
||||
|
||||
@@ -8,16 +8,18 @@ export function renderMobileList(container: HTMLElement) {
|
||||
createListView(container, {
|
||||
title: 'PC', // Legacy support
|
||||
dataSource: () => sortAssets(state.masterData.mobile || []),
|
||||
searchKeys: ['MODEL_NAME'],
|
||||
searchKeys: ['MODEL_NAME', 'ASSET_TYPE'],
|
||||
filterOptions: {
|
||||
keywordLabel: `통합 검색 (${ASSET_SCHEMA.MODEL_NAME.ui})`,
|
||||
showCorp: true,
|
||||
showDept: true
|
||||
showDept: true,
|
||||
showType: 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.ASSET_TYPE.ui, sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', width: '10%', render: a => a[ASSET_SCHEMA.ASSET_TYPE.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,
|
||||
|
||||
@@ -8,11 +8,12 @@ export function renderNetworkList(container: HTMLElement) {
|
||||
createListView(container, {
|
||||
title: '네트워크',
|
||||
dataSource: () => sortAssets(state.masterData.network || []),
|
||||
searchKeys: ['MODEL_NAME', 'CURRENT_USER', 'ASSET_MFR'],
|
||||
searchKeys: ['MODEL_NAME', 'CURRENT_USER', 'ASSET_MFR', 'ASSET_TYPE'],
|
||||
filterOptions: {
|
||||
keywordLabel: `통합 검색 (${ASSET_SCHEMA.MODEL_NAME.ui}/${ASSET_SCHEMA.ASSET_MFR.ui})`,
|
||||
showLoc: true,
|
||||
showDept: true
|
||||
showDept: true,
|
||||
showType: true
|
||||
},
|
||||
onRowClick: (asset) => openHwModal(asset, 'view'),
|
||||
columns: [
|
||||
@@ -23,7 +24,7 @@ export function renderNetworkList(container: HTMLElement) {
|
||||
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_TYPE.ui, sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', width: '10%', 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' },
|
||||
|
||||
@@ -8,32 +8,40 @@ export function renderPcList(container: HTMLElement) {
|
||||
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'],
|
||||
searchKeys: ['CURRENT_DEPT', 'CURRENT_USER', 'MODEL_NAME', 'MAC_ADDR', 'MANAGER_MAIN', 'ASSET_TYPE'],
|
||||
filterOptions: {
|
||||
keywordLabel: `통합 검색 (${ASSET_SCHEMA.MODEL_NAME.ui}/${ASSET_SCHEMA.MANAGER_MAIN.ui}/${ASSET_SCHEMA.CURRENT_USER.ui})`,
|
||||
showLoc: true,
|
||||
showDept: true
|
||||
showDept: true,
|
||||
showType: 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.ASSET_TYPE.ui, sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', width: '10%', render: a => a[ASSET_SCHEMA.ASSET_TYPE.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: 'SSD',
|
||||
align: 'center',
|
||||
width: '8%',
|
||||
render: a => [a[ASSET_SCHEMA.SSD1.key], a[ASSET_SCHEMA.SSD2.key]].filter(Boolean).join(' / ') || '-'
|
||||
},
|
||||
{
|
||||
header: 'HDD',
|
||||
align: 'center',
|
||||
width: '12%',
|
||||
render: a => [a[ASSET_SCHEMA.HDD1.key], a[ASSET_SCHEMA.HDD2.key], a[ASSET_SCHEMA.HDD3.key], a[ASSET_SCHEMA.HDD4.key]].filter(Boolean).join(' / ') || '-'
|
||||
},
|
||||
{
|
||||
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] || '-') }
|
||||
{ header: ASSET_SCHEMA.MEMO.ui, sortKey: ASSET_SCHEMA.MEMO.key, className: 'col-memo', width: '30%', render: a => formatInline(a[ASSET_SCHEMA.MEMO.key] || '-') }
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
@@ -12,7 +12,8 @@ export function renderPcPartList(container: HTMLElement) {
|
||||
filterOptions: {
|
||||
keywordLabel: `통합 검색 (${ASSET_SCHEMA.MODEL_NAME.ui})`,
|
||||
showLoc: true,
|
||||
showDept: true
|
||||
showDept: true,
|
||||
showType: true
|
||||
},
|
||||
onRowClick: (asset) => openHwModal(asset, 'view'),
|
||||
columns: [
|
||||
@@ -22,7 +23,7 @@ export function renderPcPartList(container: HTMLElement) {
|
||||
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_TYPE.ui, sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', width: '10%', 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] || '-' },
|
||||
|
||||
@@ -12,11 +12,12 @@ export function renderServerList(container: HTMLElement) {
|
||||
const serverPcList = (state.masterData.pc || []).filter((a: any) => a.asset_type === '서버PC');
|
||||
return sortAssets([...serverList, ...serverPcList]);
|
||||
},
|
||||
searchKeys: ['CURRENT_DEPT', 'MODEL_NAME', 'ASSET_PURPOSE'],
|
||||
searchKeys: ['CURRENT_DEPT', 'MODEL_NAME', 'ASSET_PURPOSE', 'ASSET_TYPE'],
|
||||
filterOptions: {
|
||||
keywordLabel: `통합 검색 (${ASSET_SCHEMA.CURRENT_DEPT.ui}/${ASSET_SCHEMA.MODEL_NAME.ui})`,
|
||||
showLoc: true,
|
||||
showDept: true
|
||||
showDept: true,
|
||||
showType: true
|
||||
},
|
||||
onRowClick: (asset) => openHwModal(asset, 'view'),
|
||||
columns: [
|
||||
|
||||
@@ -8,11 +8,12 @@ export function renderSpaceInfoList(container: HTMLElement) {
|
||||
createListView(container, {
|
||||
title: '공간정보장비',
|
||||
dataSource: () => sortAssets(state.masterData.equipment?.filter((a: any) => a.category === '공간정보장비') || []),
|
||||
searchKeys: ['MODEL_NAME', 'PRODUCT_NAME', 'CURRENT_USER'],
|
||||
searchKeys: ['MODEL_NAME', 'PRODUCT_NAME', 'CURRENT_USER', 'ASSET_TYPE'],
|
||||
filterOptions: {
|
||||
keywordLabel: `통합 검색 (${ASSET_SCHEMA.MODEL_NAME.ui}/${ASSET_SCHEMA.CURRENT_USER.ui})`,
|
||||
showLoc: true,
|
||||
showDept: true
|
||||
showDept: true,
|
||||
showType: true
|
||||
},
|
||||
onRowClick: (asset) => openHwModal(asset, 'view'),
|
||||
columns: [
|
||||
@@ -24,7 +25,7 @@ export function renderSpaceInfoList(container: HTMLElement) {
|
||||
},
|
||||
{ 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.ASSET_TYPE.ui, sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', width: '10%', render: a => a[ASSET_SCHEMA.ASSET_TYPE.key] || '-' },
|
||||
{
|
||||
header: ASSET_SCHEMA.LOCATION.ui,
|
||||
sortKey: ASSET_SCHEMA.LOCATION.key,
|
||||
|
||||
@@ -8,17 +8,18 @@ export function renderStorageList(container: HTMLElement) {
|
||||
createListView(container, {
|
||||
title: '스토리지',
|
||||
dataSource: () => sortAssets(state.masterData.storage || []),
|
||||
searchKeys: ['MODEL_NAME', 'CURRENT_USER', 'SERIAL_NUM'],
|
||||
searchKeys: ['MODEL_NAME', 'CURRENT_USER', 'SERIAL_NUM', 'ASSET_TYPE'],
|
||||
filterOptions: {
|
||||
keywordLabel: `통합 검색 (${ASSET_SCHEMA.MODEL_NAME.ui}/${ASSET_SCHEMA.CURRENT_USER.ui})`,
|
||||
showLoc: true,
|
||||
showDept: true
|
||||
showDept: true,
|
||||
showType: 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.ASSET_TYPE.ui, sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', width: '10%', 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] || '-' },
|
||||
|
||||
@@ -10,24 +10,26 @@ export function renderSwList(container: HTMLElement) {
|
||||
createListView(container, {
|
||||
title: isInternal ? '내부' : '외부',
|
||||
dataSource: () => sortAssets(isInternal ? state.masterData.swInternal : state.masterData.swExternal),
|
||||
searchKeys: ['PRODUCT_NAME', 'CURRENT_USER', 'CURRENT_DEPT'],
|
||||
searchKeys: ['PRODUCT_NAME', 'CURRENT_USER', 'CURRENT_DEPT', 'ASSET_TYPE'],
|
||||
emptyMessage: '검색 결과가 없습니다.',
|
||||
filterOptions: {
|
||||
keywordLabel: `통합 검색 (${ASSET_SCHEMA.PRODUCT_NAME.ui}/${ASSET_SCHEMA.CURRENT_DEPT.ui})`,
|
||||
showField: true,
|
||||
showCorp: true,
|
||||
showDept: true
|
||||
showDept: true,
|
||||
showType: 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.ASSET_TYPE.ui, sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', width: '10%', render: a => a[ASSET_SCHEMA.ASSET_TYPE.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.ASSET_TYPE.ui, sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', width: '10%', 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] || '' },
|
||||
|
||||
222
src/views/MapEditor.ts
Normal file
222
src/views/MapEditor.ts
Normal file
@@ -0,0 +1,222 @@
|
||||
import { IMAGE_LOCATIONS } from '../components/Modal/SharedData';
|
||||
import { createIcons, X, Save, Trash2, ChevronLeft, ChevronRight } from 'lucide';
|
||||
|
||||
export class MapEditor {
|
||||
private container: HTMLElement;
|
||||
private wrapper: HTMLElement;
|
||||
private img: HTMLImageElement;
|
||||
private boxListEl: HTMLElement;
|
||||
private pathLabel: HTMLElement;
|
||||
private statusEl: HTMLElement;
|
||||
private saveBtn: HTMLButtonElement;
|
||||
private fileSidebar: HTMLElement;
|
||||
|
||||
private allMapConfig: Record<string, any[]> = {};
|
||||
private boxes: any[] = [];
|
||||
private isDrawing: boolean = false;
|
||||
private startX: number = 0;
|
||||
private startY: number = 0;
|
||||
private currentBox: HTMLElement | null = null;
|
||||
private currentPath: string = '';
|
||||
|
||||
constructor() {
|
||||
this.container = document.getElementById('container')!;
|
||||
this.wrapper = document.getElementById('wrapper')!;
|
||||
this.img = document.getElementById('target-img') as HTMLImageElement;
|
||||
this.boxListEl = document.getElementById('box-list')!;
|
||||
this.pathLabel = document.getElementById('current-path')!;
|
||||
this.statusEl = document.getElementById('save-status')!;
|
||||
this.saveBtn = document.getElementById('btn-save-server') as HTMLButtonElement;
|
||||
this.fileSidebar = document.getElementById('file-sidebar')!;
|
||||
}
|
||||
|
||||
public async init() {
|
||||
this.renderFileSidebar();
|
||||
await this.loadConfig();
|
||||
this.bindEvents();
|
||||
this.selectFirstFile();
|
||||
createIcons({ icons: { X, Save, Trash2, ChevronLeft, ChevronRight } });
|
||||
}
|
||||
|
||||
private renderFileSidebar() {
|
||||
let html = '';
|
||||
Object.entries(IMAGE_LOCATIONS).forEach(([bldg, details]) => {
|
||||
html += `<div class="folder-item">${bldg}</div>`;
|
||||
Object.entries(details).forEach(([detail, paths]) => {
|
||||
paths.forEach(path => {
|
||||
const fileName = path.split('/').pop() || path;
|
||||
html += `<div class="file-item" data-path="${path}">${fileName}</div>`;
|
||||
});
|
||||
});
|
||||
});
|
||||
this.fileSidebar.innerHTML = html;
|
||||
|
||||
this.fileSidebar.querySelectorAll('.file-item').forEach(item => {
|
||||
item.addEventListener('click', () => {
|
||||
this.fileSidebar.querySelectorAll('.file-item').forEach(i => i.classList.remove('active'));
|
||||
item.classList.add('active');
|
||||
this.renderCurrentFile();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private selectFirstFile() {
|
||||
const firstItem = this.fileSidebar.querySelector('.file-item') as HTMLElement;
|
||||
if (firstItem) {
|
||||
firstItem.classList.add('active');
|
||||
this.renderCurrentFile();
|
||||
}
|
||||
}
|
||||
|
||||
private async loadConfig() {
|
||||
try {
|
||||
const res = await fetch(`http://${location.hostname}:3000/api/maps`);
|
||||
this.allMapConfig = await res.json();
|
||||
} catch (err) {
|
||||
console.error('Failed to load config:', err);
|
||||
}
|
||||
}
|
||||
|
||||
private renderCurrentFile() {
|
||||
const activeItem = this.fileSidebar.querySelector('.file-item.active') as HTMLElement;
|
||||
if (!activeItem) return;
|
||||
|
||||
this.currentPath = activeItem.dataset.path || '';
|
||||
this.boxes = this.allMapConfig[this.currentPath] || [];
|
||||
this.pathLabel.textContent = this.currentPath;
|
||||
this.img.src = this.currentPath;
|
||||
this.render();
|
||||
}
|
||||
|
||||
private bindEvents() {
|
||||
this.wrapper.addEventListener('mousedown', (e) => {
|
||||
if (e.button !== 0) return;
|
||||
this.isDrawing = true;
|
||||
const rect = this.wrapper.getBoundingClientRect();
|
||||
this.startX = e.clientX - rect.left;
|
||||
this.startY = e.clientY - rect.top;
|
||||
|
||||
this.currentBox = document.createElement('div');
|
||||
this.currentBox.className = 'draw-box';
|
||||
this.currentBox.style.left = this.startX + 'px';
|
||||
this.currentBox.style.top = this.startY + 'px';
|
||||
|
||||
const label = document.createElement('div');
|
||||
label.className = 'box-label';
|
||||
label.textContent = '#' + (this.boxes.length + 1);
|
||||
this.currentBox.appendChild(label);
|
||||
|
||||
this.wrapper.appendChild(this.currentBox);
|
||||
});
|
||||
|
||||
window.addEventListener('mousemove', (e) => {
|
||||
if (!this.isDrawing || !this.currentBox) return;
|
||||
const rect = this.wrapper.getBoundingClientRect();
|
||||
const currentX = Math.max(0, Math.min(e.clientX - rect.left, rect.width));
|
||||
const currentY = Math.max(0, Math.min(e.clientY - rect.top, rect.height));
|
||||
|
||||
const width = currentX - this.startX;
|
||||
const height = currentY - this.startY;
|
||||
|
||||
this.currentBox.style.width = Math.abs(width) + 'px';
|
||||
this.currentBox.style.height = Math.abs(height) + 'px';
|
||||
this.currentBox.style.left = (width > 0 ? this.startX : currentX) + 'px';
|
||||
this.currentBox.style.top = (height > 0 ? this.startY : currentY) + 'px';
|
||||
});
|
||||
|
||||
window.addEventListener('mouseup', () => {
|
||||
if (!this.isDrawing || !this.currentBox) return;
|
||||
this.isDrawing = false;
|
||||
|
||||
const width = parseFloat(this.currentBox.style.width);
|
||||
const height = parseFloat(this.currentBox.style.height);
|
||||
|
||||
if (width > 3 && height > 3) {
|
||||
const rect = this.wrapper.getBoundingClientRect();
|
||||
const boxData = {
|
||||
x: (parseFloat(this.currentBox.style.left) / rect.width * 100).toFixed(2),
|
||||
y: (parseFloat(this.currentBox.style.top) / rect.height * 100).toFixed(2),
|
||||
w: (width / rect.width * 100).toFixed(2),
|
||||
h: (height / rect.height * 100).toFixed(2)
|
||||
};
|
||||
this.boxes.push(boxData);
|
||||
this.render();
|
||||
}
|
||||
|
||||
this.currentBox.remove();
|
||||
this.currentBox = null;
|
||||
});
|
||||
|
||||
(window as any).removeBox = (index: number) => {
|
||||
this.boxes.splice(index, 1);
|
||||
this.render();
|
||||
};
|
||||
|
||||
(window as any).clearAll = () => {
|
||||
if(confirm('모든 박스를 삭제할까요?')) {
|
||||
this.boxes = [];
|
||||
this.render();
|
||||
}
|
||||
};
|
||||
|
||||
(window as any).saveToServer = () => this.saveToServer();
|
||||
}
|
||||
|
||||
private async saveToServer() {
|
||||
if (!this.currentPath) return;
|
||||
|
||||
try {
|
||||
this.saveBtn.disabled = true;
|
||||
this.saveBtn.textContent = '저장 중...';
|
||||
|
||||
const res = await fetch(`http://${location.hostname}:3000/api/maps/save`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ path: this.currentPath, boxes: this.boxes })
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
this.allMapConfig[this.currentPath] = [...this.boxes];
|
||||
this.statusEl.textContent = '✅ 서버 저장 완료 (' + new Date().toLocaleTimeString() + ')';
|
||||
setTimeout(() => this.statusEl.textContent = '', 3000);
|
||||
} else {
|
||||
alert('저장 실패!');
|
||||
}
|
||||
} catch (err) {
|
||||
alert('서버 연결 오류!');
|
||||
} finally {
|
||||
this.saveBtn.disabled = false;
|
||||
this.saveBtn.textContent = '서버에 즉시 저장';
|
||||
}
|
||||
}
|
||||
|
||||
private render() {
|
||||
this.boxListEl.innerHTML = '';
|
||||
const oldBoxes = this.wrapper.querySelectorAll('.placed-box');
|
||||
oldBoxes.forEach(b => b.remove());
|
||||
|
||||
this.boxes.forEach((box, i) => {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'placed-box';
|
||||
div.style.left = box.x + '%';
|
||||
div.style.top = box.y + '%';
|
||||
div.style.width = box.w + '%';
|
||||
div.style.height = box.h + '%';
|
||||
|
||||
const label = document.createElement('div');
|
||||
label.className = 'box-label';
|
||||
label.textContent = '#' + (i + 1);
|
||||
div.appendChild(label);
|
||||
|
||||
this.wrapper.appendChild(div);
|
||||
|
||||
const item = document.createElement('div');
|
||||
item.className = 'box-item';
|
||||
item.innerHTML = `
|
||||
<span>#${i+1}: [${box.x}, ${box.y}]</span>
|
||||
<button class="btn-del" onclick="removeBox(${i})">×</button>
|
||||
`;
|
||||
this.boxListEl.appendChild(item);
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user