feat: 자산 현황 레이아웃 개선 및 위치 정보 가독성 최적화

- 박스형 디자인 배제 및 선 기반의 미니멀 레이아웃 적용
- 목록 클릭 시 우측 패널에 배치도 및 위치 마커 즉시 표시
- 사진 비율 유지 및 좌표 정밀 보정 (onload/resize 로직 도입)
- 총 보유 자산 통계에 외부(운영)/내부(테스트) 대수 세부 표시 추가
- 빌드 오류 수정을 위한 구문 정리
This commit is contained in:
2026-06-08 09:44:15 +09:00
parent 06f3baaa58
commit 2ec9261c03

View File

@@ -3,9 +3,10 @@ import { dynamicSort, renderPageHeader, calculateAssetAge, formatInline } from '
import { setupTableSorting, SortState } from '../../core/tableHandler'; import { setupTableSorting, SortState } from '../../core/tableHandler';
import { renderFilterBar, applyCommonFilters } from '../../core/filterHandler'; import { renderFilterBar, applyCommonFilters } from '../../core/filterHandler';
import { state } from '../../core/state'; import { state } from '../../core/state';
import { IMAGE_LOCATIONS } from '../../components/Modal/SharedData';
import { import {
createIcons, RefreshCcw, Plus, Edit2, Trash2, Users, Cloud, createIcons, RefreshCcw, Plus, Edit2, Trash2, Users, Cloud,
CreditCard, DollarSign, Paperclip CreditCard, DollarSign, Paperclip, X
} from 'lucide'; } from 'lucide';
export interface ColumnDef { export interface ColumnDef {
@@ -71,7 +72,16 @@ export function createListView(container: HTMLElement, config: ListViewConfig) {
// --- 내부 상태 --- // --- 내부 상태 ---
let selectedLocation: string | null = '기술개발센터'; let selectedLocation: string | null = '기술개발센터';
let selectedDetailLocation: string | null = 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 renderSystemStatus = () => {
@@ -118,6 +128,220 @@ export function createListView(container: HTMLElement, config: ListViewConfig) {
const chartData = isPcView ? pcData : locLabels.map(l => locationCounts[l]); const chartData = isPcView ? pcData : locLabels.map(l => locationCounts[l]);
const chartColors = ['#1E5149', '#4255bd', '#92400E', '#B91C1C', '#6D28D9', '#BE185D', '#0369A1', '#15803D', '#4B5563']; 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 = () => { const updateTableOnly = () => {
let filtered = selectedLocation let filtered = selectedLocation
? fullList.filter(a => (a[ASSET_SCHEMA.LOCATION.key] || '미지정') === selectedLocation) ? fullList.filter(a => (a[ASSET_SCHEMA.LOCATION.key] || '미지정') === selectedLocation)
@@ -157,7 +381,7 @@ export function createListView(container: HTMLElement, config: ListViewConfig) {
row.addEventListener('click', () => { row.addEventListener('click', () => {
const id = (row as HTMLElement).getAttribute('data-id'); const id = (row as HTMLElement).getAttribute('data-id');
const asset = fullList.find(a => a.id === id); const asset = fullList.find(a => a.id === id);
if (asset && config.onRowClick) config.onRowClick(asset); if (asset) updateDetailPanel(asset);
}); });
row.addEventListener('mouseenter', () => { (row as HTMLElement).style.backgroundColor = '#F8FAFA'; }); row.addEventListener('mouseenter', () => { (row as HTMLElement).style.backgroundColor = '#F8FAFA'; });
row.addEventListener('mouseleave', () => { (row as HTMLElement).style.backgroundColor = 'transparent'; }); row.addEventListener('mouseleave', () => { (row as HTMLElement).style.backgroundColor = 'transparent'; });
@@ -165,128 +389,6 @@ export function createListView(container: HTMLElement, config: ListViewConfig) {
} }
}; };
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: 1.5rem; 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>
${isPcView ? `
<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>
<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>
<span style="font-size: 22px; font-weight: 800; color: #35635C; line-height: 1;">${externalCount}</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>
<span style="font-size: 22px; font-weight: 800; color: #94A3B8; line-height: 1;">${internalCount}</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: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; flex: 1; min-height: 0;">
<!-- 좌측: 자산 현황 표 (5:5 비율) -->
<div class="list-section" style="display: flex; flex-direction: column; min-height: 0; background: white; border-radius: 8px; border: 1px solid var(--border-color); overflow: hidden;">
<div style="display: flex; justify-content: space-between; align-items: center; padding: 0.75rem 1.25rem; background: #f9fafb; border-bottom: 1px solid var(--border-color); 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: #fefefe; 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: 1px solid var(--border-color); width: 60px; text-align:center;">분류</th>
<th style="padding: 10px 0; font-weight: 700; border-bottom: 1px solid var(--border-color); width: 130px;">용도/자산명</th>
<th style="padding: 10px 0; font-weight: 700; border-bottom: 1px solid var(--border-color); text-align:center; width: 90px;">관리자(정)</th>
<th style="padding: 10px 0; font-weight: 700; border-bottom: 1px solid var(--border-color); text-align:center; width: 90px;">관리자(부)</th>
<th style="padding: 10px 0; text-align: center; font-weight: 700; border-bottom: 1px solid var(--border-color); width: 100px;">상세위치</th>
</tr>
</thead>
<tbody id="system-status-tbody" style="font-size: 12px;"></tbody>
</table>
</div>
</div>
<!-- 우측: 상세 정보 패널 -->
<div id="system-detail-panel" style="display: flex; flex-direction: column; min-height: 0; background: #fcfcfc; border-radius: 8px; border: 1px solid var(--border-color); padding: 1.5rem; overflow: hidden; box-shadow: 0 10px 25px rgba(0,0,0,0.05);">
<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.15;">🖼️</div>
<p style="font-size: 1.125rem; font-weight: 500;">목록에서 자산을 선택하면<br><strong>상세 정보와 배치도</strong>가 이곳에 표시됩니다.</p>
</div>
<div id="detail-content" style="display: none; height: 100%; flex-direction: column;">
<!-- 상단 요약 정보 -->
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.25rem; padding-bottom: 1rem; border-bottom: 1px solid var(--border-color); flex-shrink: 0;">
<div style="display: flex; gap: 2rem; align-items: center;">
<div>
<label style="display: block; font-size: 10px; font-weight: 700; color: var(--text-muted); text-transform: uppercase; margin-bottom: 2px;">자산번호</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: 2px;">메모 요약</label>
<div id="detail-memo" style="font-size: 13px; color: var(--text-main); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 300px;"></div>
</div>
</div>
<button id="btn-system-view-detail" class="btn-primary" style="padding: 0.5rem 1.25rem; font-size: 12px; font-weight: 600;">상세수정</button>
</div>
<!-- 메인 배치도 영역 (잘림 방지 보정) -->
<div style="flex: 1; display: flex; flex-direction: column; min-height: 0; margin-top: 0.5rem; overflow: hidden;">
<div style="margin-bottom: 0.5rem; flex-shrink: 0;">
<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; background: #1a1a1a; border-radius: 6px; overflow: hidden; display: flex; align-items: center; justify-content: center; border: 1px solid #000; box-shadow: inset 0 0 50px rgba(0,0,0,0.6); position: relative;">
<div class="layout-map-container readonly" style="position: relative; display: inline-block; line-height: 0; max-width: 100%; max-height: 100%;">
<img id="detail-photo" src="" style="display: block; max-width: 100%; max-height: 100%; object-fit: contain;" />
<div id="detail-marker" class="layout-marker pulse-marker" style="display: none; position: absolute;"></div>
<div id="detail-overlay-layer" class="digital-overlay-layer" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none;"></div>
</div>
<div id="detail-no-photo" style="display: none; flex-direction: column; align-items: center; gap: 1rem;">
<div style="font-size: 4rem; opacity: 0.15;">🖼️</div>
<span style="color: #555; font-size: 13px; font-weight: 500;">등록된 배치도 또는 사진이 없습니다.</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;
(window as any).dispatchLocFilter = (loc: string) => { (window as any).dispatchLocFilter = (loc: string) => {
if (isPcView) return; if (isPcView) return;
selectedLocation = loc; selectedLocation = loc;