import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema'; import { dynamicSort, renderPageHeader, calculateAssetAge, formatInline } from '../../core/utils'; import { setupTableSorting, SortState } from '../../core/tableHandler'; import { renderFilterBar, applyCommonFilters } from '../../core/filterHandler'; import { state } from '../../core/state'; import { IMAGE_LOCATIONS } from '../../components/Modal/SharedData'; import { createIcons, RefreshCcw, Plus, Edit2, Trash2, Users, Cloud, CreditCard, DollarSign, Paperclip, X } 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; showType?: boolean; }; columns: ColumnDef[]; onRowClick?: (asset: any) => void; emptyMessage?: string; persistentSortState?: SortState; } export function createListView(container: HTMLElement, config: ListViewConfig) { // 1. 컨테이너 초기화 및 헤더 렌더링 container.innerHTML = ''; renderPageHeader(container, config.title); const fullList = config.dataSource(); let sortState: SortState = config.persistentSortState || { key: '', direction: 'asc' }; let currentFilters: any = { keyword: '', corp: '', dept: '', loc: '', field: '', type: '' }; // 강제로 기본 뷰 모드를 'system' (자산 현황)으로 설정 state.currentViewMode = 'system'; // 2. 뷰 전환 토글 버튼 생성 (명칭 변경) const toggleWrapper = document.createElement('div'); toggleWrapper.className = 'view-toggle-container'; toggleWrapper.innerHTML = `
`; container.appendChild(toggleWrapper); // 3. 필터 바 생성 (자산 목록에서만 사용) const filterBar = document.createElement('div'); filterBar.className = 'search-bar'; container.appendChild(filterBar); // 4. 컨텐츠 영역 생성 const contentWrapper = document.createElement('div'); contentWrapper.className = 'view-content-wrapper'; container.appendChild(contentWrapper); // --- 내부 상태 --- let selectedLocation: string | null = '기술개발센터'; let selectedDetailLocation: string | null = null; let dynamicMapConfig: Record = {}; // 맵 설정 미리 로드 const fetchMapConfig = async () => { try { const res = await fetch(`http://${location.hostname}:3000/api/maps`); dynamicMapConfig = await res.json(); } catch (err) { console.error('Failed to fetch map config:', err); } }; fetchMapConfig(); // [자산 현황] 대시보드 렌더러 const renderSystemStatus = () => { const isPcView = config.title === 'PC'; const locationCounts: Record = {}; const pcTypeCounts = { public: 0, server: 0, personal: 0 }; const extSubCounts = { tech: 0, idc: 0, hm: 0 }; const intSubCounts = { tech: 0, idc: 0, hm: 0 }; let internalCount = 0; let externalCount = 0; fullList.forEach(asset => { const loc = asset[ASSET_SCHEMA.LOCATION.key] || '미지정'; const serviceTypeKey = ASSET_SCHEMA.SERVICE_TYPE?.key || 'service_type'; const serviceType = asset[serviceTypeKey] || '외부'; const type = asset[ASSET_SCHEMA.ASSET_TYPE.key] || ''; locationCounts[loc] = (locationCounts[loc] || 0) + 1; if (isPcView) { if (type.includes('공용')) pcTypeCounts.public++; else if (type.includes('서버')) pcTypeCounts.server++; else pcTypeCounts.personal++; } if (serviceType === '내부') { internalCount++; if (loc === '기술개발센터') intSubCounts.tech++; else if (loc === 'IDC') intSubCounts.idc++; else if (loc === '한맥빌딩') intSubCounts.hm++; } else { externalCount++; if (loc === '기술개발센터') extSubCounts.tech++; else if (loc === 'IDC') extSubCounts.idc++; else if (loc === '한맥빌딩') extSubCounts.hm++; } }); const locLabels = Object.keys(locationCounts).sort((a, b) => locationCounts[b] - locationCounts[a]); const pcLabels = ['공용PC', '서버PC', '개인PC']; const pcData = [pcTypeCounts.public, pcTypeCounts.server, pcTypeCounts.personal]; const chartLabels = isPcView ? pcLabels : locLabels; const chartData = isPcView ? pcData : locLabels.map(l => locationCounts[l]); const chartColors = ['#1E5149', '#4255bd', '#92400E', '#B91C1C', '#6D28D9', '#BE185D', '#0369A1', '#15803D', '#4B5563']; contentWrapper.innerHTML = `
총 보유 자산
${fullList.length}
외부: ${externalCount} 내부: ${internalCount}
${isPcView ? `
PC 유형별 현황
공용: ${pcTypeCounts.public} 서버: ${pcTypeCounts.server} 개인: ${pcTypeCounts.personal}
` : `
외부 (운영) 상세
기술개발센터: ${extSubCounts.tech} IDC: ${extSubCounts.idc} 한맥빌딩: ${extSubCounts.hm}
`}
${isPcView ? '' : `
내부 (테스트) 상세
기술개발센터: ${intSubCounts.tech} IDC: ${intSubCounts.idc} 한맥빌딩: ${intSubCounts.hm}
`}

자산 현황 목록

위치: 상세:
분류 용도/자산명 관리자(정) 관리자(부) 상세위치
🖼️

목록에서 자산을 선택하면
상세 정보와 배치도가 표시됩니다.

`; // 상세 정보 패널 업데이트 함수 const updateDetailPanel = (asset: any) => { const emptyState = document.getElementById('detail-empty-state'); const content = document.getElementById('detail-content'); if (!emptyState || !content) return; emptyState.style.display = 'none'; content.style.display = 'flex'; // 텍스트 정보 업데이트 const codeEl = document.getElementById('detail-asset-code'); const memoEl = document.getElementById('detail-memo'); if (codeEl) codeEl.textContent = asset.asset_code || '미지정'; if (memoEl) memoEl.textContent = asset.memo || '-'; // 위치 및 사진 정보 업데이트 const photo = document.getElementById('detail-photo') as HTMLImageElement; const marker = document.getElementById('detail-marker'); const overlayLayer = document.getElementById('detail-overlay-layer'); const noPhoto = document.getElementById('detail-no-photo'); const photoWrapper = document.getElementById('detail-photo-wrapper'); const bldg = asset.location || ''; const detail = asset.location_detail || ''; const x = asset.loc_x; const y = asset.loc_y; const savedImg = asset.location_photo || asset.loc_img; const locImgs = IMAGE_LOCATIONS[bldg.trim()]?.[detail.trim()] || null; const imgPath = (savedImg && locImgs?.includes(savedImg)) ? savedImg : (locImgs ? locImgs[0] : null); if (imgPath) { photo.src = imgPath; photo.style.display = 'block'; if (noPhoto) noPhoto.style.display = 'none'; // 마커 및 오버레이 초기화는 이미지가 로드된 후 정확한 크기를 기반으로 수행 photo.onload = () => { const updateMarkerPos = () => { const imgW = photo.clientWidth; const imgH = photo.clientHeight; if (marker && x && y && x !== 'null' && y !== 'null') { // object-fit: contain 상황에서의 실제 이미지 렌더링 영역 내 좌표 계산 marker.style.left = `calc(50% - ${imgW/2}px + ${ (parseFloat(x) * imgW) / 100 }px)`; marker.style.top = `calc(50% - ${imgH/2}px + ${ (parseFloat(y) * imgH) / 100 }px)`; marker.style.display = 'block'; } if (overlayLayer) { overlayLayer.style.width = `${imgW}px`; overlayLayer.style.height = `${imgH}px`; overlayLayer.style.left = `calc(50% - ${imgW/2}px)`; overlayLayer.style.top = `calc(50% - ${imgH/2}px)`; const boxes = dynamicMapConfig[imgPath] || []; if (boxes.length > 0) { overlayLayer.innerHTML = ` ${boxes.map((b, i) => { const isSelected = b.x === x && b.y === y; const fill = isSelected ? 'rgba(255, 61, 0, 0.4)' : 'rgba(30, 81, 73, 0.02)'; const stroke = isSelected ? '#FF3D00' : 'rgba(30, 81, 73, 0.15)'; const strokeWidth = isSelected ? '0.8' : '0.2'; if (isSelected && marker) { marker.style.left = `calc(50% - ${imgW/2}px + ${ (parseFloat(b.x) + parseFloat(b.w)/2) * imgW / 100 }px)`; marker.style.top = `calc(50% - ${imgH/2}px + ${ (parseFloat(b.y) + parseFloat(b.h)/2) * imgH / 100 }px)`; } return ``; }).join('')} `; } else { overlayLayer.innerHTML = ''; } } }; updateMarkerPos(); window.addEventListener('resize', updateMarkerPos); }; } else { photo.style.display = 'none'; if (marker) marker.style.display = 'none'; if (overlayLayer) overlayLayer.innerHTML = ''; if (noPhoto) noPhoto.style.display = 'flex'; } }; const updateTableOnly = () => { let filtered = selectedLocation ? fullList.filter(a => (a[ASSET_SCHEMA.LOCATION.key] || '미지정') === selectedLocation) : fullList; const currentDetailLocs = Array.from(new Set(filtered.map(a => a[ASSET_SCHEMA.LOC_DETAIL.key] || '미지정'))).sort(); if (selectedDetailLocation) filtered = filtered.filter(a => (a[ASSET_SCHEMA.LOC_DETAIL.key] || '미지정') === selectedDetailLocation); const finalDisplayList = (!selectedLocation && !selectedDetailLocation) ? filtered.slice(0, 10) : filtered; const titleEl = document.getElementById('list-section-title'); if (titleEl) titleEl.textContent = selectedLocation ? `${selectedLocation} 자산 현황 (${finalDisplayList.length}대)` : '위치별 자산등록현황 (최근 등록)'; const selectEl = document.getElementById('select-detail-loc') as HTMLSelectElement; if (selectEl && !selectedDetailLocation) { selectEl.innerHTML = `` + currentDetailLocs.map(dl => ``).join(''); } const tbody = document.getElementById('system-status-tbody'); if (tbody) { tbody.innerHTML = finalDisplayList.length === 0 ? `조회된 자산이 없습니다.` : finalDisplayList.map(asset => { const purpose = asset[ASSET_SCHEMA.ASSET_PURPOSE.key] || ''; const serviceTypeKey = ASSET_SCHEMA.SERVICE_TYPE?.key || 'service_type'; const serviceType = asset[serviceTypeKey] || '외부'; const labelColor = serviceType === '내부' ? '#94A3B8' : '#35635C'; const managerMain = asset[ASSET_SCHEMA.MANAGER_MAIN.key] || '-'; const managerSub = asset[ASSET_SCHEMA.MANAGER_SUB.key] || '-'; return ` ${serviceType} ${purpose || '-'} ${managerMain} ${managerSub} ${asset[ASSET_SCHEMA.LOC_DETAIL.key] || '-'} `; }).join(''); tbody.querySelectorAll('.mini-row').forEach(row => { row.addEventListener('click', () => { const id = (row as HTMLElement).getAttribute('data-id'); const asset = fullList.find(a => a.id === id); if (asset) updateDetailPanel(asset); }); row.addEventListener('mouseenter', () => { (row as HTMLElement).style.backgroundColor = '#F8FAFA'; }); row.addEventListener('mouseleave', () => { (row as HTMLElement).style.backgroundColor = 'transparent'; }); }); } }; (window as any).dispatchLocFilter = (loc: string) => { if (isPcView) return; selectedLocation = loc; selectedDetailLocation = null; renderSystemStatus(); }; setTimeout(() => { const selectLoc = document.getElementById('select-loc') as HTMLSelectElement; const selectDetailLoc = document.getElementById('select-detail-loc') as HTMLSelectElement; selectLoc?.addEventListener('change', (e) => { selectedLocation = (e.target as HTMLSelectElement).value || null; selectedDetailLocation = null; updateTableOnly(); }); selectDetailLoc?.addEventListener('change', (e) => { selectedDetailLocation = (e.target as HTMLSelectElement).value || null; updateTableOnly(); }); updateTableOnly(); }, 50); }; // [자산 목록] 테이블 렌더러 const tableWrapper = document.createElement('div'); tableWrapper.className = 'table-container'; const table = document.createElement('table'); const thead = document.createElement('thead'); const tbody = document.createElement('tbody'); tbody.id = 'dynamic-tbody'; table.appendChild(thead); table.appendChild(tbody); tableWrapper.appendChild(table); const updateTable = () => { if (state.currentViewMode !== 'asset') return; let filtered = applyCommonFilters(fullList, currentFilters, config.searchKeys as any[]); if (sortState.key) filtered = dynamicSort(filtered, sortState.key, sortState.direction); thead.innerHTML = `${config.columns.map(col => ` ${col.header}`).join('')}`; tbody.innerHTML = filtered.length === 0 ? `${config.emptyMessage || UI_TEXT.MESSAGES.NO_DATA}` : filtered.map(asset => ` ${config.columns.map(col => `${col.render(asset)}`).join('')} `).join(''); tbody.querySelectorAll('.asset-row').forEach((tr, idx) => { tr.addEventListener('click', () => config.onRowClick && config.onRowClick(filtered[idx])); }); setupTableSorting(table, sortState, (key, dir) => { sortState = { key, direction: dir }; if (config.persistentSortState) { config.persistentSortState.key = key; config.persistentSortState.direction = dir; } updateTable(); }); createIcons({ icons: { RefreshCcw, Plus, Edit2, Trash2, Users, Cloud, CreditCard, DollarSign, Paperclip } }); }; // --- 뷰 전환 로직 --- const switchView = () => { contentWrapper.innerHTML = ''; if (state.currentViewMode === 'asset') { filterBar.style.display = 'flex'; contentWrapper.style.overflowY = 'auto'; contentWrapper.appendChild(tableWrapper); updateTable(); } else { filterBar.style.display = 'none'; contentWrapper.style.overflowY = 'hidden'; renderSystemStatus(); } }; // 토글 버튼 이벤트 toggleWrapper.addEventListener('click', (e) => { const btn = (e.target as HTMLElement).closest('.toggle-btn') as HTMLButtonElement; if (!btn) return; toggleWrapper.querySelectorAll('.toggle-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); state.currentViewMode = btn.getAttribute('data-mode') as 'asset' | 'system'; switchView(); }); // 필터 바 초기화 renderFilterBar(filterBar, { ...config.filterOptions, onFilterChange: (filters) => { currentFilters = { ...currentFilters, ...filters }; updateTable(); } }); // 셀렉트 박스 채우기 const populateSelect = (selector: string, dataKey: string) => { const select = container.querySelector(selector) as HTMLSelectElement; if (select) { const getVal = (a: any) => dataKey === ASSET_SCHEMA.CURRENT_DEPT.key ? (a[dataKey] || a['현사용부서'] || a['현사용조직']) : 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); if (config.filterOptions.showType) populateSelect('#filter-type', ASSET_SCHEMA.ASSET_TYPE.key); // 초기 실행 switchView(); }