From 164568843bbed933c677df70f59ff40312632d6b Mon Sep 17 00:00:00 2001 From: Taehoon Date: Thu, 11 Jun 2026 09:47:57 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20LocationView=20=EA=B3=A0=EB=8F=84?= =?UTF-8?q?=ED=99=94=20-=20=EC=A7=80=EB=8F=84=20=ED=81=B4=EB=A6=AD=20?= =?UTF-8?q?=EC=8B=9C=20=EC=82=AC=EC=9D=B4=EB=93=9C=EB=B0=94=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=20=EC=A0=95=EB=B3=B4=20=ED=91=9C=EC=8B=9C=20=EB=B0=8F?= =?UTF-8?q?=20=EA=B5=AC=EC=97=AD=20=ED=95=84=ED=84=B0=EB=A7=81=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/state.ts | 2 + src/core/utils.ts | 2 +- src/main.ts | 36 +++-- src/styles/dashboard.css | 270 ++++++++++++++++++++++++++++++++++++++ src/views/LocationView.ts | 232 ++++++++++++++++++++++++++++++++ vite.config.ts | 10 ++ 6 files changed, 543 insertions(+), 9 deletions(-) create mode 100644 src/views/LocationView.ts diff --git a/src/core/state.ts b/src/core/state.ts index d7189bb..e5ccb5b 100644 --- a/src/core/state.ts +++ b/src/core/state.ts @@ -36,6 +36,7 @@ export interface MasterAssetData { export interface AppState { activeCategory: 'dashboard' | 'hw' | 'sw' | 'ops' | 'vip' | 'fac' | 'users' | 'etc'; activeSubTab: string; + viewMode: 'location' | 'legacy' | 'list'; masterData: MasterAssetData; activeCharts: any[]; currentUserRole: 'admin' | 'user'; @@ -45,6 +46,7 @@ export interface AppState { export const state: AppState = { activeCategory: 'hw', activeSubTab: '서버', // 대시보드 제거됨에 따라 기본값 변경 + viewMode: 'location', activeCharts: [], currentUserRole: 'user', masterData: { diff --git a/src/core/utils.ts b/src/core/utils.ts index 030b2d1..469fce2 100644 --- a/src/core/utils.ts +++ b/src/core/utils.ts @@ -1,6 +1,6 @@ import { PAGE_DESCRIPTIONS } from './schema'; -export const API_BASE_URL = `http://${location.hostname}:3000`; +export const API_BASE_URL = ''; /** * ITAM 공통 유틸리티 함수 diff --git a/src/main.ts b/src/main.ts index 53f549b..a7e52c3 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,6 +2,7 @@ import { state, loadMasterDataFromDB, saveAsset } from './core/state'; import { renderNavigation } from './components/Navigation'; import { renderDashboard } from './views/DashboardView'; import { renderSWTable } from './views/SW_Table'; +import { renderLocationView } from './views/LocationView'; import { initBaseModal } from './components/Modal/BaseModal'; import { initHwModal, openHwModal } from './components/Modal/HWModal'; import { initSwModal, openSwModal } from './components/Modal/SWModal'; @@ -47,10 +48,33 @@ function refreshView() { const mainContent = document.getElementById('main-content')!; if (!mainContent) return; - if (state.activeSubTab === '대시보드') { - renderDashboard(mainContent); + mainContent.innerHTML = ` +
+
+ + + +
+
+
+ `; + + // 이벤트 바인딩 + mainContent.querySelectorAll('.mode-toggle-btn').forEach(btn => { + btn.addEventListener('click', () => { + const mode = (btn as HTMLElement).getAttribute('data-mode') as any; + state.viewMode = mode; + refreshView(); + }); + }); + + const viewBody = document.getElementById('view-body')!; + if (state.viewMode === 'location') { + renderLocationView(viewBody); + } else if (state.viewMode === 'legacy') { + renderDashboard(viewBody); // 통계/차트 } else { - renderSWTable(mainContent); + renderSWTable(viewBody); // 리스트 형식 } } @@ -74,11 +98,7 @@ function initApp() { try { renderNavigation((tab) => { - if (tab === '대시보드') { - renderDashboard(mainContent); - } else { - renderSWTable(mainContent); - } + refreshView(); }); initHwModal(() => saveAllDataToDB(), closeAllModals); diff --git a/src/styles/dashboard.css b/src/styles/dashboard.css index 44ddd6d..5c1bbaf 100644 --- a/src/styles/dashboard.css +++ b/src/styles/dashboard.css @@ -57,3 +57,273 @@ width: 100% !important; max-height: 280px; } + +/* --- Location View Styles --- */ +.location-layout { + display: grid; + grid-template-columns: 1.2fr 1fr; + gap: 2rem; + height: calc(100vh - 180px); +} + +.map-section, .asset-section { + display: flex; + flex-direction: column; +} + +.section-title { + font-size: 1.125rem; + font-weight: 700; + margin-bottom: 1rem; + color: var(--text-main); + display: flex; + align-items: center; +} + +.map-wrapper { + flex: 1; + background: #f8fafc; + box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.05); +} + +.location-box { + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); + user-select: none; +} + +.location-box:hover { + background: rgba(30, 81, 73, 0.2) !important; + transform: scale(1.02); + z-index: 10; +} + +.location-box:active { + transform: scale(0.98); +} + +.asset-section .table-container { + flex: 1; + overflow-y: auto; +} + +.status-tag { + display: inline-block; + padding: 0.25rem 0.625rem; + border-radius: 9999px; + font-size: 0.75rem; + font-weight: 600; + background: #ecfdf5; + color: #059669; + border: 1px solid #d1fae5; +} + +.view-toggle-btn:hover { + border-color: var(--primary-color) !important; + color: var(--primary-color) !important; +} + +.view-toggle-btn.active:hover { + color: white !important; +} + +/* --- View Toggle Header --- */ +.view-header { + padding: 0.5rem 1.5rem; + background: var(--white); + border-bottom: 1px solid var(--border-color); + display: flex; + align-items: center; + justify-content: flex-start; + gap: 1rem; +} + +.view-toggle-container { + display: flex; + background: #f1f5f9; + padding: 0.25rem; + border-radius: 8px; + gap: 0.25rem; +} + +.mode-toggle-btn { + padding: 0.5rem 1rem; + border: none; + background: transparent; + border-radius: 6px; + font-size: 0.8125rem; + font-weight: 600; + color: var(--text-muted); + cursor: pointer; + transition: all 0.2s ease; +} + +.mode-toggle-btn:hover { + color: var(--text-main); +} + +.mode-toggle-btn.active { + background: var(--white); + color: var(--primary-color); + box-shadow: 0 1px 3px rgba(0,0,0,0.1); +} + +/* --- Enhanced Location View --- */ +.location-view-wrapper { + display: flex; + flex-direction: column; + height: calc(100vh - 120px); +} + +.location-filter-bar { + padding: 1rem 1.5rem; + background: var(--white); + border-bottom: 1px solid var(--border-color); + display: flex; + align-items: center; + gap: 2rem; +} + +.filter-group { + display: flex; + align-items: center; + gap: 0.75rem; +} + +.filter-group label { + font-size: 0.8125rem; + font-weight: 700; + color: var(--text-main); +} + +.filter-group select { + padding: 0.4rem 0.75rem; + border: 1px solid var(--border-color); + border-radius: 6px; + font-size: 0.8125rem; + color: var(--text-main); + background: var(--white); + min-width: 140px; +} + +.map-pagination { + margin-left: auto; + display: flex; + align-items: center; + gap: 1rem; +} + +.page-info { + font-size: 0.75rem; + color: var(--text-muted); + font-weight: 600; +} + +.page-btns button { + padding: 0.3rem 0.75rem; + border: 1px solid var(--border-color); + background: var(--white); + border-radius: 4px; + font-size: 0.75rem; + cursor: pointer; +} + +.page-btns button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.location-main-content { + flex: 1; + display: grid; + grid-template-columns: 1.4fr 1fr; + gap: 1.5rem; + padding: 1.5rem; + overflow: hidden; +} + +.map-container-section { + display: flex; + flex-direction: column; + overflow: auto; +} + +.location-box-point { + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; +} + +.box-label-text { + font-size: 0.65rem; + font-weight: 800; + color: var(--primary-color); + pointer-events: none; + text-shadow: 0 0 2px white; +} + +.asset-list-section { + background: var(--white); + border-radius: 12px; + border: 1px solid var(--border-color); + display: flex; + flex-direction: column; + overflow: hidden; +} + +.asset-list-section .section-header { + padding: 1rem 1.25rem; + border-bottom: 1px solid var(--border-color); + background: #f8fafc; +} + +.asset-list-section h4 { + margin: 0; + font-size: 0.9375rem; + color: var(--text-main); +} + +.mini-table-wrapper { + flex: 1; + overflow-y: auto; +} + +.compact-table { + width: 100%; + border-collapse: collapse; +} + +.compact-table th { + position: sticky; + top: 0; + background: var(--white); + padding: 0.75rem 1rem; + text-align: left; + font-size: 0.75rem; + font-weight: 700; + color: var(--text-muted); + border-bottom: 1px solid var(--border-color); +} + +.compact-table td { + padding: 0.75rem 1rem; + font-size: 0.8125rem; + border-bottom: 1px solid #f1f5f9; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 150px; +} + +.compact-table tr.clickable-row:hover { + background: #f1f5f9; + cursor: pointer; +} + +.empty-state { + padding: 4rem 2rem; + text-align: center; + color: var(--text-muted); + font-size: 0.8125rem; +} diff --git a/src/views/LocationView.ts b/src/views/LocationView.ts new file mode 100644 index 0000000..33d919d --- /dev/null +++ b/src/views/LocationView.ts @@ -0,0 +1,232 @@ +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'; + +/** + * 위치 중심 자산 현황 뷰 (Refined) + */ +export async function renderLocationView(container: HTMLElement) { + if (!container) return; + + // 로컬 상태 (UI 제어용) + 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] || ''; + + // 자산이 등록된(좌표가 일치하는) 구역만 필터링하여 표시 + const allBoxes = mapConfig[mapPath] || []; + const boxes = allBoxes.filter((box: any) => + state.masterData.hw.some(a => + a.location === currentLoc && + a.location_detail === currentDetail && + String(a.loc_x) === String(box.x) && + String(a.loc_y) === String(box.y) + ) + ); + + container.innerHTML = ` +
+ +
+
+ + +
+
+ + +
+ + ${locImages.length > 1 ? ` +
+ 사진: ${currentPage + 1} / ${locImages.length} +
+ + +
+
+ ` : ''} +
+ +
+ +
+
+ ${mapPath ? ` + +
+ ${boxes.map((box: any, idx: number) => { + const name = box.name || `#${idx+1}`; + return ` +
+
+ `}).join('')} +
+ ` : '
해당 위치의 도면이 등록되지 않았습니다.
'} +
+

* 지도 위의 구역을 클릭하면 자산 상세 정보가 표시됩니다.

+
+ + +
+
+

📍 구역을 선택하세요

+
+
+
지도에서 자산 위치를 클릭하세요.
+
+
+
+
+ `; + + // 이미지 로드 후 오버레이 크기 재조정 (좌표 밀림 방지) + const img = container.querySelector('#main-map-img') as HTMLImageElement; + if (img) { + img.onload = () => { + const overlay = container.querySelector('#box-overlay') as HTMLElement; + if (overlay) { + overlay.style.height = img.offsetHeight + 'px'; + } + }; + } + + // 이벤트 바인딩 + 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(); }); + + container.querySelectorAll('.location-box-point').forEach(box => { + box.addEventListener('click', () => { + const x = box.getAttribute('data-x'); + const y = box.getAttribute('data-y'); + + // 좌표 및 위치 정보를 기반으로 정확한 자산 1개 찾기 + const targetAsset = state.masterData.hw.find(a => + a.location === currentLoc && + a.location_detail === currentDetail && + String(a.loc_x) === String(x) && + String(a.loc_y) === String(y) + ); + + if (targetAsset) { + renderAssetDetail(targetAsset); + } + + container.querySelectorAll('.location-box-point').forEach(b => (b as HTMLElement).style.background = 'rgba(30, 81, 81, 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 = ` +
+ + 📍 자산 상세 정보 + +
+ `; + + // 섹션별 렌더링 함수 + const renderSection = (title: string, fields: { label: string; value: any }[]) => ` +
+
${title}
+
+ ${fields.map(f => ` +
${f.label}
+
${f.value || '-'}
+ `).join('')} +
+
+ `; + + // 하드웨어 정보 구성 + const sectionsHTML = [ + renderSection('기본 관리 정보', [ + { label: ASSET_SCHEMA.ASSET_CODE.ui, value: asset.asset_code }, + { label: ASSET_SCHEMA.PURCHASE_CORP.ui, value: asset.purchase_corp }, + { label: ASSET_SCHEMA.CATEGORY.ui, value: asset.category }, + { label: ASSET_SCHEMA.ASSET_TYPE.ui, value: asset.asset_type }, + { label: ASSET_SCHEMA.HW_STATUS.ui, value: asset.hw_status } + ]), + renderSection('시스템 사양', [ + { 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 } + ]), + renderSection('네트워크 정보', [ + { 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 } + ]), + renderSection('구매 및 기타', [ + { label: ASSET_SCHEMA.PURCHASE_DATE.ui, value: asset.purchase_date }, + { label: ASSET_SCHEMA.PURCHASE_AMOUNT.ui, value: asset.purchase_amount ? `${Number(asset.purchase_amount).toLocaleString()}원` : '-' }, + { label: ASSET_SCHEMA.MEMO.ui, value: asset.memo } + ]) + ].join(''); + + tableContainer.innerHTML = ` +
+ ${sectionsHTML} +
+ `; + + // 뒤로가기 버튼: 목록 대신 초기 상태로 리셋 + container.querySelector('#btn-back-to-list')?.addEventListener('click', () => { + title.textContent = `📍 구역을 선택하세요`; + tableContainer.innerHTML = `
지도에서 자산 위치를 클릭하세요.
`; + }); + + // 수정 버튼 (기존 모달 활용) + container.querySelector('#btn-edit-from-loc')?.addEventListener('click', () => { + openHwModal(asset, 'edit'); + }); + }; + + // showAssets 함수 제거 (목록 표시 불필요) + + + render(); +} diff --git a/vite.config.ts b/vite.config.ts index 3d35e2a..56b1c1c 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -4,5 +4,15 @@ export default defineConfig({ server: { port: 8080, host: true, // Listen on all local IPs + proxy: { + '/api': { + target: 'http://localhost:3000', + changeOrigin: true, + }, + '/uploads': { + target: 'http://localhost:3000', + changeOrigin: true, + } + } }, });