- 서버 탭 복귀 시 최근 선택한 뷰 모드(목록/위치) 상태 유지 및 currentViewMode 상태 일원화 - 개인PC 대시보드 및 맵 에디터의 인라인 CSS 스타일을 공통 CSS 및 변수 클래스로 분리 및 가독성 개선 - Vite 멀티페이지 빌드 설정(vite.config.ts) 추가
252 lines
10 KiB
TypeScript
252 lines
10 KiB
TypeScript
import { state } from '../core/state';
|
|
import { openHwModal } from '../components/Modal/HWModal';
|
|
import { ASSET_SCHEMA } from '../core/schema';
|
|
import { LOCATION_DATA, IMAGE_LOCATIONS } from '../components/Modal/SharedData';
|
|
|
|
/**
|
|
* 위치 중심 자산 현황 뷰 (Vercel Integrated)
|
|
*/
|
|
export async function renderLocationView(container: HTMLElement) {
|
|
if (!container) return;
|
|
|
|
let currentLoc = '기술개발센터';
|
|
let currentDetail = '서버실';
|
|
let currentPage = 0;
|
|
let mapConfig: any = {};
|
|
|
|
try {
|
|
const res = await fetch('/api/maps');
|
|
mapConfig = await res.json();
|
|
} catch (err) { console.error('Failed to load map config', err); }
|
|
|
|
const render = () => {
|
|
const locImages = (IMAGE_LOCATIONS[currentLoc] && IMAGE_LOCATIONS[currentLoc][currentDetail])
|
|
? IMAGE_LOCATIONS[currentLoc][currentDetail]
|
|
: [];
|
|
const mapPath = locImages[currentPage] || '';
|
|
|
|
// 조회 모드: 설정 파일에 정의된 asset_id를 기준으로 자산 데이터 매핑
|
|
const allBoxes = mapConfig[mapPath] || [];
|
|
const boxes = allBoxes.filter((box: any) => box.asset_id != null);
|
|
|
|
// 모든 하드웨어 카테고리에서 자산 검색
|
|
const allHwAssets = [
|
|
...state.masterData.pc,
|
|
...state.masterData.server,
|
|
...state.masterData.storage,
|
|
...state.masterData.network,
|
|
...state.masterData.equipment,
|
|
...state.masterData.survey,
|
|
...state.masterData.officeSupplies,
|
|
...state.masterData.pcParts
|
|
];
|
|
|
|
container.innerHTML = `
|
|
<div class="location-view-wrapper">
|
|
<!-- 상단 통합 바 (Unified Search Bar) -->
|
|
<div class="location-filter-bar search-bar">
|
|
<div class="search-item">
|
|
<label class="list-view-toggle-label">
|
|
<input type="checkbox" id="chk-list-view-loc" />
|
|
목록보기
|
|
</label>
|
|
</div>
|
|
<div class="search-item">
|
|
<label>건물/위치</label>
|
|
<select id="sel-loc-main">
|
|
${Object.keys(LOCATION_DATA).map(loc => `<option value="${loc}" ${loc === currentLoc ? 'selected' : ''}>${loc}</option>`).join('')}
|
|
</select>
|
|
</div>
|
|
<div class="search-item">
|
|
<label>상세 위치</label>
|
|
<div class="flex items-center gap-2">
|
|
<select id="sel-loc-detail">
|
|
${(LOCATION_DATA[currentLoc] || []).map(det => `<option value="${det}" ${det === currentDetail ? 'selected' : ''}>${det}</option>`).join('')}
|
|
</select>
|
|
|
|
<!-- 페이지네이션 -->
|
|
${locImages.length > 1 ? `
|
|
<div class="map-pagination-group">
|
|
<div class="page-btns flex gap-1">
|
|
<button id="btn-prev-page" class="btn btn-outline btn-sm" ${currentPage === 0 ? 'disabled' : ''}>이전</button>
|
|
<button id="btn-next-page" class="btn btn-outline btn-sm" ${currentPage === locImages.length - 1 ? 'disabled' : ''}>다음</button>
|
|
</div>
|
|
<span class="page-info">(${currentPage + 1} / ${locImages.length})</span>
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="location-main-content">
|
|
<!-- 지도 섹션 -->
|
|
<div class="map-container-section">
|
|
<div class="map-frame-wrapper">
|
|
${mapPath ? `
|
|
<img src="${mapPath}" id="main-map-img" class="map-image">
|
|
<div id="box-overlay" class="map-overlay">
|
|
${boxes.map((box: any, idx: number) => {
|
|
const asset = allHwAssets.find(a => a.id === box.asset_id);
|
|
const name = asset ? ((asset as any).asset_purpose || asset.asset_code) : (box.name || `#${idx+1}`);
|
|
// w, h가 없거나 너무 작으면 최소 크기(3%) 보장하여 영역으로 표시
|
|
const width = Math.max(parseFloat(box.w || '3'), 3);
|
|
const height = Math.max(parseFloat(box.h || '3'), 3);
|
|
return `
|
|
<div class="location-box-area"
|
|
data-asset-id="${box.asset_id}"
|
|
data-name="${name}"
|
|
style="left:${box.x}%; top:${box.y}%; width:${width}%; height:${height}%;
|
|
border: 2px solid var(--primary-color); background: rgba(30, 81, 73, 0.1); cursor:pointer; pointer-events: auto; position: absolute;">
|
|
</div>
|
|
`}).join('')}
|
|
</div>
|
|
` : '<div class="no-map-message">해당 위치의 도면이 등록되지 않았습니다.</div>'}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 상세 정보 섹션 -->
|
|
<div class="asset-list-section">
|
|
<div class="section-header">
|
|
<h4 id="loc-list-title" class="sidebar-title">구역을 선택하세요</h4>
|
|
</div>
|
|
<div id="loc-asset-table-container" class="mini-table-wrapper">
|
|
<div class="empty-state">지도에서 자산 위치를 클릭하세요.</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
const syncOverlaySize = () => {
|
|
const img = container.querySelector('#main-map-img') as HTMLImageElement;
|
|
const overlay = container.querySelector('#box-overlay') as HTMLElement;
|
|
|
|
if (img && overlay && img.complete) {
|
|
overlay.style.width = img.clientWidth + 'px';
|
|
overlay.style.height = img.clientHeight + 'px';
|
|
overlay.style.left = img.offsetLeft + 'px';
|
|
overlay.style.top = img.offsetTop + 'px';
|
|
}
|
|
};
|
|
|
|
const img = container.querySelector('#main-map-img') as HTMLImageElement;
|
|
if (img) {
|
|
if (img.complete) {
|
|
syncOverlaySize();
|
|
setTimeout(syncOverlaySize, 50);
|
|
} else {
|
|
img.onload = syncOverlaySize;
|
|
}
|
|
}
|
|
|
|
window.removeEventListener('resize', syncOverlaySize);
|
|
window.addEventListener('resize', syncOverlaySize);
|
|
|
|
const selMain = container.querySelector('#sel-loc-main') as HTMLSelectElement;
|
|
selMain?.addEventListener('change', () => {
|
|
currentLoc = selMain.value;
|
|
currentDetail = LOCATION_DATA[currentLoc][0];
|
|
currentPage = 0;
|
|
render();
|
|
});
|
|
|
|
const selDetail = container.querySelector('#sel-loc-detail') as HTMLSelectElement;
|
|
selDetail?.addEventListener('change', () => {
|
|
currentDetail = selDetail.value;
|
|
currentPage = 0;
|
|
render();
|
|
});
|
|
|
|
container.querySelector('#btn-prev-page')?.addEventListener('click', () => { currentPage--; render(); });
|
|
container.querySelector('#btn-next-page')?.addEventListener('click', () => { currentPage++; render(); });
|
|
|
|
const chkBox = container.querySelector('#chk-list-view-loc') as HTMLInputElement;
|
|
|
|
if (chkBox) {
|
|
chkBox.checked = state.viewMode === 'list';
|
|
const handleToggle = () => {
|
|
const isListMode = chkBox.checked;
|
|
if (isListMode) {
|
|
state.viewMode = 'list';
|
|
} else {
|
|
state.viewMode = 'location';
|
|
}
|
|
window.dispatchEvent(new Event('refresh-view'));
|
|
};
|
|
chkBox.addEventListener('change', handleToggle);
|
|
}
|
|
|
|
container.querySelectorAll('.location-box-area').forEach(box => {
|
|
box.addEventListener('click', () => {
|
|
const assetId = box.getAttribute('data-asset-id');
|
|
if (!assetId) return;
|
|
|
|
const targetAsset = allHwAssets.find(a => a.id === assetId);
|
|
|
|
if (targetAsset) renderAssetDetail(targetAsset);
|
|
container.querySelectorAll('.location-box-area').forEach(b => (b as HTMLElement).style.background = 'rgba(30, 81, 73, 0.1)');
|
|
(box as HTMLElement).style.background = 'rgba(30, 81, 73, 0.4)';
|
|
});
|
|
});
|
|
};
|
|
|
|
const renderAssetDetail = (asset: any) => {
|
|
const title = container.querySelector('#loc-list-title')!;
|
|
const tableContainer = container.querySelector('#loc-asset-table-container')!;
|
|
|
|
title.innerHTML = `
|
|
<div class="detail-header-actions">
|
|
<div class="header-identity">
|
|
<span class="asset-code-title">${asset.asset_code || '미부여'}</span>
|
|
<span class="service-type-badge">${asset.service_type || '운영'}</span>
|
|
<span class="asset-type-label">${asset.asset_type || 'PC'}</span>
|
|
</div>
|
|
<button id="btn-view-from-loc" class="btn btn-primary btn-sm">조회</button>
|
|
</div>
|
|
`;
|
|
|
|
const fields = [
|
|
{ label: ASSET_SCHEMA.CURRENT_DEPT.ui, value: asset.current_dept },
|
|
{ label: ASSET_SCHEMA.HW_STATUS.ui, value: asset.hw_status },
|
|
{ label: ASSET_SCHEMA.MANAGER_MAIN.ui, value: asset.manager_primary },
|
|
{ label: ASSET_SCHEMA.MANAGER_SUB.ui, value: asset.manager_secondary },
|
|
{ label: ASSET_SCHEMA.ASSET_PURPOSE.ui, value: asset.asset_purpose, fullWidth: true },
|
|
{ label: ASSET_SCHEMA.MODEL_NAME.ui, value: asset.model_name },
|
|
{ label: ASSET_SCHEMA.OS.ui, value: asset.os },
|
|
{ label: ASSET_SCHEMA.CPU.ui, value: asset.cpu },
|
|
{ label: ASSET_SCHEMA.RAM.ui, value: asset.ram },
|
|
{ label: ASSET_SCHEMA.GPU.ui, value: asset.gpu, fullWidth: true },
|
|
{ label: ASSET_SCHEMA.IP_ADDR.ui, value: asset.ip_address },
|
|
{ label: ASSET_SCHEMA.MAC_ADDR.ui, value: asset.mac_address },
|
|
{ label: ASSET_SCHEMA.REMOTE_TOOL.ui, value: asset.remote_tool },
|
|
{ label: ASSET_SCHEMA.MONITORING.ui, value: asset.monitoring },
|
|
{ label: ASSET_SCHEMA.MEMO.ui, value: asset.memo, fullWidth: true }
|
|
];
|
|
|
|
const sectionsHTML = `
|
|
<div class="detail-section" style="margin-bottom: 0;">
|
|
<div class="detail-grid-2col" style="gap: 0.75rem 1rem;">
|
|
${fields.map(f => `
|
|
<div class="detail-item ${f.fullWidth ? 'full-width' : ''}">
|
|
<div class="detail-label-sm">${f.label}</div>
|
|
<div class="detail-value-lg">${f.value || '-'}</div>
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
tableContainer.innerHTML = `
|
|
<div class="asset-detail-sidebar">
|
|
${sectionsHTML}
|
|
</div>
|
|
`;
|
|
|
|
container.querySelector('#btn-view-from-loc')?.addEventListener('click', () => {
|
|
openHwModal(asset, 'view');
|
|
});
|
|
};
|
|
|
|
render();
|
|
}
|