From 2ec9261c031b13c81e1bb68540be82813a49ae5e Mon Sep 17 00:00:00 2001 From: Taehoon Date: Mon, 8 Jun 2026 09:44:15 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=9E=90=EC=82=B0=20=ED=98=84=ED=99=A9?= =?UTF-8?q?=20=EB=A0=88=EC=9D=B4=EC=95=84=EC=9B=83=20=EA=B0=9C=EC=84=A0=20?= =?UTF-8?q?=EB=B0=8F=20=EC=9C=84=EC=B9=98=20=EC=A0=95=EB=B3=B4=20=EA=B0=80?= =?UTF-8?q?=EB=8F=85=EC=84=B1=20=EC=B5=9C=EC=A0=81=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 박스형 디자인 배제 및 선 기반의 미니멀 레이아웃 적용 - 목록 클릭 시 우측 패널에 배치도 및 위치 마커 즉시 표시 - 사진 비율 유지 및 좌표 정밀 보정 (onload/resize 로직 도입) - 총 보유 자산 통계에 외부(운영)/내부(테스트) 대수 세부 표시 추가 - 빌드 오류 수정을 위한 구문 정리 --- src/views/List/ListFactory.ts | 350 ++++++++++++++++++++++------------ 1 file changed, 226 insertions(+), 124 deletions(-) diff --git a/src/views/List/ListFactory.ts b/src/views/List/ListFactory.ts index a132b46..0c3ff4a 100644 --- a/src/views/List/ListFactory.ts +++ b/src/views/List/ListFactory.ts @@ -3,9 +3,10 @@ import { dynamicSort, renderPageHeader, calculateAssetAge, formatInline } from ' 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 + CreditCard, DollarSign, Paperclip, X } from 'lucide'; export interface ColumnDef { @@ -71,7 +72,16 @@ export function createListView(container: HTMLElement, config: ListViewConfig) { // --- 내부 상태 --- 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 = () => { @@ -118,6 +128,220 @@ export function createListView(container: HTMLElement, config: ListViewConfig) { 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) @@ -157,7 +381,7 @@ export function createListView(container: HTMLElement, config: ListViewConfig) { row.addEventListener('click', () => { const id = (row as HTMLElement).getAttribute('data-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('mouseleave', () => { (row as HTMLElement).style.backgroundColor = 'transparent'; }); @@ -165,128 +389,6 @@ export function createListView(container: HTMLElement, config: ListViewConfig) { } }; - contentWrapper.innerHTML = ` -
- - -
-
-
총 보유 자산
-
${fullList.length}
- ${isPcView ? ` -
- 공용: ${pcTypeCounts.public} - 서버: ${pcTypeCounts.server} - 개인: ${pcTypeCounts.personal} -
- ` : ''} -
- -
- ${isPcView ? '' : ` -
- 외부 (운영) - ${externalCount} -
-
- 기술개발센터: ${extSubCounts.tech} - IDC: ${extSubCounts.idc} - 한맥빌딩: ${extSubCounts.hm} -
- `} -
- -
- ${isPcView ? '' : ` -
- 내부 (테스트) - ${internalCount} -
-
- 기술개발센터: ${intSubCounts.tech} - IDC: ${intSubCounts.idc} - 한맥빌딩: ${intSubCounts.hm} -
- `} -
-
- -
- -
-
-

자산 현황 목록

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

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

-
- -
-
-
- `; - (window as any).dispatchLocFilter = (loc: string) => { if (isPcView) return; selectedLocation = loc;