- 박스형 디자인 배제 및 선 기반의 미니멀 레이아웃 적용 - 목록 클릭 시 우측 패널에 배치도 및 위치 마커 즉시 표시 - 사진 비율 유지 및 좌표 정밀 보정 (onload/resize 로직 도입) - 총 보유 자산 통계에 외부(운영)/내부(테스트) 대수 세부 표시 추가 - 빌드 오류 수정을 위한 구문 정리
517 lines
28 KiB
TypeScript
517 lines
28 KiB
TypeScript
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 = `
|
|
<div class="view-toggle">
|
|
<button class="toggle-btn ${state.currentViewMode === 'system' ? 'active' : ''}" data-mode="system">자산 현황</button>
|
|
<button class="toggle-btn ${state.currentViewMode === 'asset' ? 'active' : ''}" data-mode="asset">자산 목록</button>
|
|
</div>
|
|
`;
|
|
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<string, any[]> = {};
|
|
|
|
// 맵 설정 미리 로드
|
|
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<string, number> = {};
|
|
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 = `
|
|
<div class="system-dashboard" style="height: calc(100vh - 240px); overflow: hidden; padding: 0.5rem 0; font-family: 'Pretendard', sans-serif; letter-spacing: -0.02em; display: flex; flex-direction: column;">
|
|
|
|
<!-- [자산 통계 그룹] -->
|
|
<div style="border-bottom: 1px solid var(--border-color); padding-bottom: 1.25rem; margin-bottom: 1rem; flex-shrink: 0; display: grid; grid-template-columns: 1fr 1.5fr 1.5fr; gap: 2rem;">
|
|
<div class="stat-group-item" style="min-width: 0;">
|
|
<div style="font-size: 11px; font-weight: 600; color: var(--text-muted); margin-bottom: 0.25rem;">총 보유 자산</div>
|
|
<div style="font-size: 28px; font-weight: 800; color: var(--text-main); line-height: 1.1;">${fullList.length}<span style="font-size: 13px; font-weight: 600; margin-left: 4px; color: var(--text-muted);">대</span></div>
|
|
<div style="display: flex; gap: 0.75rem; font-size: 11px; color: var(--text-muted); margin-top: 0.5rem;">
|
|
<span>외부: <strong style="color:#35635C">${externalCount}</strong></span>
|
|
<span>내부: <strong style="color:#94A3B8">${internalCount}</strong></span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="stat-group-item" style="border-left: 1px solid var(--border-color); padding-left: 1.5rem; min-width: 0;">
|
|
${isPcView ? `
|
|
<div style="font-size: 11px; font-weight: 600; color: var(--text-muted); margin-bottom: 0.25rem;">PC 유형별 현황</div>
|
|
<div style="display: flex; gap: 0.75rem; font-size: 11px; color: var(--text-muted); margin-top: 0.5rem;">
|
|
<span>공용: <strong style="color:var(--text-main)">${pcTypeCounts.public}</strong></span>
|
|
<span>서버: <strong style="color:var(--text-main)">${pcTypeCounts.server}</strong></span>
|
|
<span>개인: <strong style="color:var(--text-main)">${pcTypeCounts.personal}</strong></span>
|
|
</div>
|
|
` : `
|
|
<div style="display: flex; align-items: flex-end; gap: 0.75rem; margin-bottom: 0.5rem;">
|
|
<span style="font-size: 11px; font-weight: 600; color: var(--text-muted);">외부 (운영) 상세</span>
|
|
</div>
|
|
<div style="display: flex; gap: 0.75rem; font-size: 11px; color: var(--text-muted); flex-wrap: wrap;">
|
|
<span>기술개발센터: <strong style="color:var(--text-main)">${extSubCounts.tech}</strong></span>
|
|
<span>IDC: <strong style="color:var(--text-main)">${extSubCounts.idc}</strong></span>
|
|
<span>한맥빌딩: <strong style="color:var(--text-main)">${extSubCounts.hm}</strong></span>
|
|
</div>
|
|
`}
|
|
</div>
|
|
|
|
<div class="stat-group-item" style="border-left: 1px solid var(--border-color); padding-left: 1.5rem; min-width: 0;">
|
|
${isPcView ? '' : `
|
|
<div style="display: flex; align-items: flex-end; gap: 0.75rem; margin-bottom: 0.5rem;">
|
|
<span style="font-size: 11px; font-weight: 600; color: var(--text-muted);">내부 (테스트) 상세</span>
|
|
</div>
|
|
<div style="display: flex; gap: 0.75rem; font-size: 11px; color: var(--text-muted); flex-wrap: wrap;">
|
|
<span>기술개발센터: <strong style="color:var(--text-main)">${intSubCounts.tech}</strong></span>
|
|
<span>IDC: <strong style="color:var(--text-main)">${intSubCounts.idc}</strong></span>
|
|
<span>한맥빌딩: <strong style="color:var(--text-main)">${intSubCounts.hm}</strong></span>
|
|
</div>
|
|
`}
|
|
</div>
|
|
</div>
|
|
|
|
<div style="display: flex; flex: 1; min-height: 0; border-top: 1px solid var(--border-color);">
|
|
<!-- 좌측: 자산 현황 목록 (Border-based Separation) -->
|
|
<div class="list-section" style="flex: 1; display: flex; flex-direction: column; min-height: 0; padding: 1rem 1.5rem 0 0; border-right: 1px solid var(--border-color);">
|
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; flex-shrink: 0;">
|
|
<h4 id="list-section-title" style="font-size: 14px; font-weight: 700; color: var(--text-main); margin:0;">자산 현황 목록</h4>
|
|
<div style="display: flex; align-items: center; gap: 8px;">
|
|
<span style="font-size: 11px; font-weight: 600; color: var(--text-muted);">위치:</span>
|
|
<select id="select-loc" style="padding: 2px 8px; font-size: 11px; border-radius: 4px; border: 1px solid var(--border-color); outline: none; background: white; cursor:pointer; font-family: 'Pretendard';">
|
|
<option value="">전체</option>
|
|
${Array.from(new Set(fullList.map(a => a[ASSET_SCHEMA.LOCATION.key] || '미지정'))).sort().map(l => `<option value="${l}" ${l === selectedLocation ? 'selected' : ''}>${l}</option>`).join('')}
|
|
</select>
|
|
<span style="font-size: 11px; font-weight: 600; color: var(--text-muted);">상세:</span>
|
|
<select id="select-detail-loc" style="padding: 2px 8px; font-size: 11px; border-radius: 4px; border: 1px solid var(--border-color); outline: none; background: white; cursor:pointer; font-family: 'Pretendard'; max-width: 120px;"></select>
|
|
</div>
|
|
</div>
|
|
<div style="flex: 1; overflow-y: auto;">
|
|
<table style="width: 100%; border-collapse: collapse; table-layout: fixed;">
|
|
<thead style="position: sticky; top: 0; background: #fff; z-index: 10;">
|
|
<tr style="text-align: left; font-size: 11px; color: var(--text-muted);">
|
|
<th style="padding: 10px 0; font-weight: 700; border-bottom: 2px solid var(--border-color); width: 60px; text-align:center; background: #fff;">분류</th>
|
|
<th style="padding: 10px 0; font-weight: 700; border-bottom: 2px solid var(--border-color); width: 130px; background: #fff;">용도/자산명</th>
|
|
<th style="padding: 10px 0; font-weight: 700; border-bottom: 2px solid var(--border-color); text-align:center; width: 90px; background: #fff;">관리자(정)</th>
|
|
<th style="padding: 10px 0; font-weight: 700; border-bottom: 2px solid var(--border-color); text-align:center; width: 90px; background: #fff;">관리자(부)</th>
|
|
<th style="padding: 10px 0; text-align: center; font-weight: 700; border-bottom: 2px solid var(--border-color); width: 100px; background: #fff;">상세위치</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="system-status-tbody" style="font-size: 12px;"></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 우측: 상세 정보 패널 (Box-less, Line-based) -->
|
|
<div id="system-detail-panel" style="flex: 1; display: flex; flex-direction: column; min-height: 0; padding: 1rem 0 0 1.5rem; overflow: hidden;">
|
|
<div id="detail-empty-state" style="height: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center; color: var(--text-muted); text-align: center;">
|
|
<div style="font-size: 4rem; margin-bottom: 1.5rem; opacity: 0.1; transform: grayscale(1);">🖼️</div>
|
|
<p style="font-size: 1.125rem; font-weight: 500; color: #94A3B8;">목록에서 자산을 선택하면<br>상세 정보와 배치도가 표시됩니다.</p>
|
|
</div>
|
|
<div id="detail-content" style="display: none; height: 100%; flex-direction: column;">
|
|
<!-- 상단 요약 정보 -->
|
|
<div style="display: flex; gap: 3rem; align-items: center; margin-bottom: 1.5rem; padding-bottom: 1rem; border-bottom: 1px solid var(--border-color); flex-shrink: 0;">
|
|
<div>
|
|
<label style="display: block; font-size: 10px; font-weight: 700; color: var(--text-muted); text-transform: uppercase; margin-bottom: 4px;">자산번호</label>
|
|
<div id="detail-asset-code" style="font-size: 1.25rem; font-weight: 800; color: var(--primary-color);"></div>
|
|
</div>
|
|
<div>
|
|
<label style="display: block; font-size: 10px; font-weight: 700; color: var(--text-muted); text-transform: uppercase; margin-bottom: 4px;">메모 요약</label>
|
|
<div id="detail-memo" style="font-size: 13px; color: var(--text-main); font-weight: 500;"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 메인 배치도 영역 -->
|
|
<div style="flex: 1; display: flex; flex-direction: column; min-height: 0; overflow: hidden;">
|
|
<div style="margin-bottom: 0.75rem; flex-shrink: 0; display: flex; justify-content: space-between; align-items: center;">
|
|
<label style="font-size: 11px; font-weight: 700; color: var(--text-main); text-transform: uppercase;">설치 위치 배치도</label>
|
|
</div>
|
|
<div id="detail-photo-wrapper" style="width: 100%; flex: 1; overflow: hidden; display: flex; align-items: center; justify-content: center; position: relative; border: 1px solid var(--border-color); background: #f0f0f0;">
|
|
<div class="layout-map-container readonly" style="position: relative; display: flex; align-items: center; justify-content: center; width: 100%; height: 100%;">
|
|
<img id="detail-photo" src="" style="display: block; max-width: 100%; max-height: 100%; width: auto; height: auto; object-fit: contain; pointer-events: none;" />
|
|
<div id="detail-marker" class="layout-marker pulse-marker" style="display: none; position: absolute; z-index: 20;"></div>
|
|
<div id="detail-overlay-layer" class="digital-overlay-layer" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; display: flex; align-items: center; justify-content: center;"></div>
|
|
</div>
|
|
<div id="detail-no-photo" style="display: none; height: 100%; flex-direction: column; align-items: center; justify-content: center; gap: 1rem;">
|
|
<div style="font-size: 3rem; opacity: 0.1;">🖼️</div>
|
|
<span style="color: #94A3B8; font-size: 13px; font-weight: 500;">등록된 배치도가 없습니다.</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// 상세 정보 패널 업데이트 함수
|
|
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 = `
|
|
<svg viewBox="0 0 100 100" preserveAspectRatio="none" style="width:100%; height:100%;">
|
|
<g class="seat-group">
|
|
${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 `<rect class="map-seat-obj" x="${b.x}" y="${b.y}" width="${b.w}" height="${b.h}" rx="0.5" style="fill:${fill}; stroke:${stroke}; stroke-width:${strokeWidth};" />`;
|
|
}).join('')}
|
|
</g>
|
|
</svg>
|
|
`;
|
|
} 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 = `<option value="">전체보기</option>` + currentDetailLocs.map(dl => `<option value="${dl}">${dl}</option>`).join('');
|
|
}
|
|
|
|
const tbody = document.getElementById('system-status-tbody');
|
|
if (tbody) {
|
|
tbody.innerHTML = finalDisplayList.length === 0
|
|
? `<tr><td colspan="4" style="padding: 3rem; text-align: center; color: var(--text-muted);">조회된 자산이 없습니다.</td></tr>`
|
|
: 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 `
|
|
<tr style="border-bottom: 1px solid var(--border-color); cursor: pointer;" class="mini-row" data-id="${asset.id}">
|
|
<td style="padding: 10px 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;"><span style="color: ${labelColor}; font-weight: 700; font-size: 12px;">${serviceType}</span></td>
|
|
<td style="padding: 10px 0; font-weight: 600; color: var(--text-main); font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;" title="${purpose}">${purpose || '-'}</td>
|
|
<td style="padding: 10px 0; text-align: center; color: var(--text-main); font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${managerMain}</td>
|
|
<td style="padding: 10px 0; text-align: center; color: var(--text-main); font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${managerSub}</td>
|
|
<td style="padding: 10px 0; text-align: center; color: var(--text-main); font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${asset[ASSET_SCHEMA.LOC_DETAIL.key] || '-'}</td>
|
|
</tr>`;
|
|
}).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 = `<tr>${config.columns.map(col => `
|
|
<th ${col.sortKey ? `data-sort="${col.sortKey}"` : ''}
|
|
style="${col.width ? `width:${col.width};` : ''}${col.align ? `text-align:${col.align};` : ''}"
|
|
class="${col.className || ''}">${col.header}</th>`).join('')}</tr>`;
|
|
|
|
tbody.innerHTML = filtered.length === 0
|
|
? `<tr><td colspan="${config.columns.length}" class="text-center" style="padding: 3rem; color: var(--text-muted);">${config.emptyMessage || UI_TEXT.MESSAGES.NO_DATA}</td></tr>`
|
|
: filtered.map(asset => `
|
|
<tr style="cursor:pointer;" class="asset-row" data-id="${asset.id}">
|
|
${config.columns.map(col => `<td style="${col.align ? `text-align:${col.align};` : ''}" class="${col.className || ''}">${col.render(asset)}</td>`).join('')}
|
|
</tr>`).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();
|
|
}
|