From eead43837df3b451be8de0caa9436d60cb0a8865 Mon Sep 17 00:00:00 2001 From: Taehoon Date: Fri, 5 Jun 2026 10:51:29 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20PC=20=EB=A7=9E=EC=B6=A4=ED=98=95=20?= =?UTF-8?q?=EB=8C=80=EC=8B=9C=EB=B3=B4=EB=93=9C=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?=EB=B0=8F=20=EC=9E=90=EC=82=B0=20=ED=98=84=ED=99=A9=20=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=95=84=EC=9B=83=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 - PC 자산 관리 화면에 유형별(공용/서버/개인) 통계 및 차트 적용 - 자산 현황 대시보드의 위치 분류 체계 통일 (센터/IDC/한맥빌딩) - 하단 요약 표의 자산번호 컬럼을 비고(Memo)로 교체 및 말줄임표 적용 - 차트 크기 확대 및 네비게이션 메뉴 레이아웃 안정화 - ListFactory.ts 내 formatInline 미정의 오류 수정 --- src/styles/common.css | 439 +++++++--------------------------- src/views/List/ListFactory.ts | 370 +++++++++++++++++++++++----- 2 files changed, 388 insertions(+), 421 deletions(-) diff --git a/src/styles/common.css b/src/styles/common.css index d340b05..331aaaf 100644 --- a/src/styles/common.css +++ b/src/styles/common.css @@ -1,5 +1,5 @@ :root { - /* --- System Colors (Added) --- */ + /* --- System Colors --- */ --color-red: #F21D0D; --color-pink: #E8175E; --color-magenta: #B92ED1; @@ -15,37 +15,6 @@ --color-iron: #7F7F7F; --color-steel: #688897; - --color-red-light: #FEE9E7; - --color-pink-light: #FDE8EF; - --color-magenta-light: #F8EBFB; - --color-purple-light: #F1ECF9; - --color-navy-light: #EDEEF9; - --color-blue-light: #E7F4FE; - --color-cyan-light: #E6F7FF; - --color-green-light: #EEF8EE; - --color-yellow-light: #FFF9E6; - --color-orange-light: #FFF5E6; - --color-dahong-light: #FFECE6; - --color-brown-light: #F6F1EF; - --color-iron-light: #F3F3F3; - --color-steel-light: #F0F4F5; - - --color-red-medium: #FAA59E; - --color-pink-medium: #F6A2BF; - --color-magenta-medium: #E3ABEC; - --color-purple-medium: #C5B1E7; - --color-navy-medium: #B3BBE5; - --color-blue-medium: #9ED1FA; - --color-cyan-medium: #9ADFFE; - --color-green-medium: #B8E0B9; - --color-yellow-medium: #FFE599; - --color-orange-medium: #FFD699; - --color-dahong-medium: #FFB199; - --color-dahong: #FF3D00; - --color-dahong-light: #FFECE6; - --color-dahong-medium: #FFB199; - --color-dahong-dark: #cc3100; - /* --- Primary Brand Levels --- */ --primary-lv-0: #E9EEED; --primary-lv-1: #D2DCDB; @@ -63,40 +32,26 @@ --primary-hover: var(--primary-lv-5); --primary-light: var(--primary-lv-0); - --edit-mode-color: var(--color-dahong); - --edit-mode-light: var(--color-dahong-light); - --edit-mode-focus: var(--color-dahong-medium); - --edit-mode-dark: var(--color-dahong-dark); - --text-main: #111827; --text-muted: #6B7280; --border-color: #E5E7EB; --bg-color: #F9FAFB; --bg-light: #FAFAFA; - --sidebar-bg: #ffffff; --white: #FFFFFF; --danger: var(--color-red); - --info: var(--color-blue); --success: var(--color-green); - --warning: var(--color-orange); - - --dash-primary: #6cc020; - --dash-light: #f2f9ec; - --dash-danger: #cf222e; - --header-height: 52px; - } +} * { box-sizing: border-box; margin: 0; padding: 0; letter-spacing: -0.02em; - /* 모든 요소에 자간 규칙 일괄 적용 */ } body { - font-family: 'Pretendard Variable', Pretendard, -apple-system, BlinkMacSystemFont, system-ui, Roboto, 'Helvetica Neue', 'Segoe UI', 'Apple SD Gothic Neo', 'Noto Sans KR', 'Malgun Gothic', sans-serif; + font-family: 'Pretendard Variable', Pretendard, sans-serif; color: var(--text-main); background-color: var(--bg-color); line-height: 1.5; @@ -111,12 +66,13 @@ body { width: 100%; } -/* --- Main Header & GNB/LNB --- */ +/* --- Header --- */ .main-header { background-color: var(--white); border-bottom: 1px solid var(--border-color); z-index: 100; height: var(--header-height); + flex-shrink: 0; } .header-container { @@ -127,239 +83,45 @@ body { gap: 1.5rem; } -.brand { - display: flex; - align-items: center; - gap: 0.75rem; -} +.brand { display: flex; align-items: center; gap: 0.75rem; } +.main-logo { height: 34px; width: auto; } +.brand h1 { font-size: 1.1rem; font-weight: 800; color: var(--text-main); white-space: nowrap; } +.brand h1 .sub-title { font-size: 0.85rem; color: var(--primary-color); font-weight: 600; margin-left: 0.25rem; } -.main-logo { - height: 34px; - width: auto; -} +.integrated-nav { flex: 1; height: 100%; display: flex; align-items: center; gap: 0.25rem; overflow: hidden; } +.nav-group { display: flex; align-items: center; height: 100%; position: relative; flex-shrink: 0; } +.gnb-trigger { font-size: 14px; font-weight: 700; color: var(--text-muted); padding: 0 0.75rem; cursor: pointer; height: 100%; display: flex; align-items: center; white-space: nowrap; transition: color 0.2s; } +.nav-group.active .gnb-trigger, .nav-group:hover .gnb-trigger { color: var(--text-main); } +.lnb-shelf { display: none; align-items: center; gap: 0.2rem; padding: 0 0.5rem; height: 60%; border-left: 1px solid var(--border-color); margin-left: 0.2rem; } -.brand h1 { - font-size: 1.1rem; - /* 전체적으로 살짝 축소 */ - font-weight: 800; - color: var(--text-main); - white-space: nowrap; -} +/* 기본적으로 활성 탭의 서브메뉴 표시 */ +.nav-group.active.is-showing-shelf .lnb-shelf { display: flex; } -.brand h1 .sub-title { - font-size: 0.85rem; - /* 영문 제목은 더 작게 */ - color: var(--primary-color); - font-weight: 600; - margin-left: 0.25rem; -} +/* GNB 전체 영역에 마우스가 올라가면 활성 탭의 서브메뉴를 일단 숨김 (다른 메뉴 탐색 우선) */ +.integrated-nav:hover .nav-group.active.is-showing-shelf .lnb-shelf { display: none; } -.integrated-nav { - flex: 1; - height: 100%; - display: flex; - align-items: center; - gap: 0.5rem; -} +/* 마우스가 올라간 메뉴의 서브메뉴만 표시 */ +.nav-group:hover .lnb-shelf { display: flex !important; } -.nav-group { - display: flex; - align-items: center; - height: 100%; -} +.lnb-item { font-size: 13px; font-weight: 500; color: var(--text-muted); cursor: pointer; padding: 0.2rem 0.6rem; border-radius: 4px; white-space: nowrap; transition: all 0.2s; } +.lnb-item:hover { color: var(--primary-color); background-color: var(--primary-light); } +.lnb-item.active { color: var(--primary-color); background-color: var(--primary-light); font-weight: 700; } -.gnb-trigger { - font-size: 14px; - font-weight: 700; - color: var(--text-main); - padding: 0 1rem; - cursor: pointer; - height: 100%; - display: flex; - align-items: center; - white-space: nowrap; -} +.role-switcher { display: flex; align-items: center; gap: 0.75rem; padding: 0 0.75rem; border-right: 1px solid var(--border-color); height: 24px; } +.role-label { font-size: 11px; font-weight: 700; color: var(--text-muted); } +.role-label.active { color: var(--primary-color); } +.switch { position: relative; display: inline-block; width: 34px; height: 18px; } +.switch input { opacity: 0; width: 0; height: 0; } +.slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; transition: .4s; border-radius: 34px; } +.slider:before { position: absolute; content: ""; height: 12px; width: 12px; left: 3px; bottom: 3px; background-color: white; transition: .4s; border-radius: 50%; } +input:checked + .slider { background-color: var(--color-orange); } +input:checked + .slider:before { transform: translateX(16px); } -.lnb-shelf { - display: none; - align-items: center; - gap: 0.25rem; - padding: 0 0.75rem; - height: 60%; - border-left: 1px solid var(--border-color); - margin-left: 0.25rem; - animation: fadeIn 0.2s ease-out; -} - -.nav-group:hover .lnb-shelf, -.nav-group.is-showing-shelf .lnb-shelf { - display: flex; -} - -.lnb-item { - font-size: 13px; - font-weight: 500; - color: var(--text-muted); - cursor: pointer; - padding: 0.2rem 0.6rem; - border-radius: 4px; - white-space: nowrap; -} - -.lnb-item:hover { - color: var(--primary-color); - background-color: var(--bg-color); -} - -.lnb-item.active { - color: var(--primary-color); - background-color: var(--primary-light); - font-weight: 700; -} - -@keyframes fadeIn { - from { - opacity: 0; - transform: translateX(-5px); - } - - to { - opacity: 1; - transform: translateX(0); - } -} - -/* --- Role Switcher Toggle --- */ -.role-switcher { - display: flex; - align-items: center; - gap: 0.75rem; - margin-right: 0.5rem; - padding: 0 0.75rem; - border-right: 1px solid var(--border-color); - height: 24px; -} - -.role-label { - font-size: 11px; - font-weight: 700; - color: var(--text-muted); - transition: color 0.2s; -} - -.role-label.active { - color: var(--primary-color); -} - -.role-label.admin.active { - color: var(--color-orange); -} - -/* Toggle Switch Base */ -.switch { - position: relative; - display: inline-block; - width: 34px; - height: 18px; -} - -.switch input { - opacity: 0; - width: 0; - height: 0; -} - -.slider { - position: absolute; - cursor: pointer; - top: 0; left: 0; right: 0; bottom: 0; - background-color: #ccc; - transition: .4s; -} - -.slider:before { - position: absolute; - content: ""; - height: 12px; - width: 12px; - left: 3px; - bottom: 3px; - background-color: white; - transition: .4s; -} - -input:checked + .slider { - background-color: var(--color-orange); -} - -input:focus + .slider { - box-shadow: 0 0 1px var(--color-orange); -} - -input:checked + .slider:before { - transform: translateX(16px); -} - -.slider.round { - border-radius: 34px; -} - -.slider.round:before { - border-radius: 50%; -} - -/* --- Global Actions & Buttons --- */ -.header-actions { - display: flex; - gap: 0.3rem; - align-items: center; -} - -.btn { - display: inline-flex; - align-items: center; - justify-content: center; - gap: 0.35rem; - padding: 0 0.8rem; - font-size: 12px; - font-weight: 600; - border-radius: 4px; - cursor: pointer; - height: 28px; - line-height: 1; - white-space: nowrap; /* 텍스트 줄바꿈 방지 */ - flex-shrink: 0; /* 크기 찌그러짐 방지 */ -} - -.btn i, -.btn svg { - width: 12px !important; - height: 12px !important; -} - -.btn-primary { - background-color: var(--primary-color); - color: var(--white); - border: 1px solid var(--primary-color); -} - -.btn-outline { - background-color: transparent; - color: var(--text-muted); - border: 1px solid var(--border-color); -} - -.btn-danger { - color: var(--danger) !important; - border-color: var(--danger) !important; -} - -/* --- Layout Frame --- */ +/* --- Layout Content --- */ .content-area { flex: 1; - padding: 1.25rem 2rem 0; /* 상단 여백 1.25rem 추가 */ + padding: 1.25rem 2rem 0; overflow: hidden; - /* 전체 스크롤 차단 */ display: flex; flex-direction: column; } @@ -370,93 +132,58 @@ input:checked + .slider:before { display: flex; flex-direction: column; overflow: hidden; - /* 내부 스크롤을 유도하기 위해 설정 */ } +.view-content-wrapper { + flex: 1; + overflow-y: auto; + padding-bottom: 2rem; +} + +/* --- View Toggle --- */ +.view-toggle-container { margin-bottom: 1rem; display: flex; justify-content: flex-start; } +.view-toggle { display: inline-flex; background-color: var(--primary-lv-0); padding: 4px; border-radius: 8px; border: 1px solid var(--border-color); } +.toggle-btn { padding: 6px 16px; font-size: 13px; font-weight: 600; color: var(--text-muted); background: none; border: none; border-radius: 6px; cursor: pointer; } +.toggle-btn.active { background-color: var(--white); color: var(--primary-color); box-shadow: 0 2px 4px rgba(0,0,0,0.05); } + +/* --- System Status List (Docker Style) --- */ +.system-status-list { display: flex; flex-direction: column; gap: 0.5rem; } +.system-list-header { display: flex; align-items: center; padding: 0.75rem 1.25rem; background-color: var(--bg-light); border-bottom: 1px solid var(--border-color); font-size: 11px; font-weight: 700; color: var(--text-muted); text-transform: uppercase; } +.system-row { display: flex; align-items: center; padding: 1rem 1.25rem; background-color: var(--white); border: 1px solid var(--border-color); border-radius: 6px; transition: all 0.2s; } +.system-row:hover { border-color: var(--primary-lv-3); box-shadow: 0 4px 12px rgba(0,0,0,0.03); } +.col-status { width: 100px; display: flex; align-items: center; gap: 0.5rem; } +.col-info { flex: 1.5; } +.col-network { flex: 1; } +.col-remote { flex: 1; display: flex; align-items: center; gap: 0.5rem; } +.col-traffic { flex: 1.2; } +.col-actions { width: 120px; display: flex; justify-content: flex-end; } +.status-dot { width: 10px; height: 10px; border-radius: 50%; } +.status-dot.online { background-color: var(--success); box-shadow: 0 0 6px var(--success); } +.status-text { font-size: 11px; font-weight: 600; color: var(--success); } +.asset-primary { font-weight: 700; font-size: 14px; } +.asset-secondary { font-size: 12px; color: var(--text-muted); } +.ip-address { font-weight: 600; font-family: monospace; color: var(--primary-color); } +.traffic-mini-chart { display: flex; flex-direction: column; gap: 4px; } +.traffic-info { display: flex; justify-content: space-between; font-size: 11px; } +.progress-bg { height: 4px; background: var(--primary-lv-0); border-radius: 2px; overflow: hidden; } +.progress-fill { height: 100%; background: var(--primary-color); } +.icon-btn { width: 28px; height: 28px; display: flex; align-items: center; justify-content: center; border-radius: 4px; border: 1px solid var(--border-color); background: var(--white); color: var(--text-muted); cursor: pointer; } +.icon-btn:hover { background-color: var(--primary-light); border-color: var(--primary-color); color: var(--primary-color); } + /* --- Footer --- */ -.main-footer { - height: 40px; - background-color: var(--white); - border-top: 1px solid var(--border-color); - display: flex; - align-items: center; - justify-content: flex-end; - padding: 0 1.5rem; - flex-shrink: 0; -} +.main-footer { height: 40px; background-color: var(--white); border-top: 1px solid var(--border-color); display: flex; align-items: center; justify-content: flex-end; padding: 0 1.5rem; flex-shrink: 0; } +.main-footer p { font-size: 0.75rem; color: var(--text-muted); } -.main-footer p { - font-family: 'Pretendard Variable', Pretendard, sans-serif; - font-size: 0.75rem; - font-weight: 300; - line-height: 1.25rem; - letter-spacing: -0.0175rem; - color: var(--text-muted); - user-select: none; - pointer-events: all; - -webkit-user-drag: none; - margin: 0; - padding: 0; - box-sizing: border-box; -} - -.hidden { - display: none !important; -} - -.text-nowrap { - white-space: nowrap; -} - -/* --- Utility Styles --- */ -.badge { - padding: 2px 6px; - border-radius: 4px; - font-size: 11px; - font-weight: 700; - white-space: nowrap; -} - -.badge-primary { - background-color: var(--primary-color); - color: white; -} - -.badge-muted { - background-color: #9CA3AF; - color: white; -} - -.text-tag { - color: var(--text-muted); - font-size: 11px; - padding: 1px 5px; - border: 1px solid var(--border-color); - border-radius: 3px; - background-color: var(--bg-light); -} - -.font-bold { - font-weight: 700; -} - -/* --- Responsive Design (Tablet & Mobile) --- */ -@media (max-width: 1200px) { - .header-container { gap: 0.75rem; padding: 0 1rem; } - .brand h1 { font-size: 1rem; } - .brand h1 .sub-title { font-size: 0.75rem; } -} - -@media (max-width: 992px) { - .main-header { height: auto; padding: 0.5rem 0; } - .header-container { flex-direction: column; align-items: flex-start; gap: 0.5rem; } - .integrated-nav { width: 100%; justify-content: flex-start; border-top: 1px solid var(--border-color); padding-top: 0.5rem; } - .header-actions { width: 100%; justify-content: flex-end; padding-top: 0.5rem; } - .content-area { padding: 0 1rem; } -} +/* --- Utility --- */ +.btn { display: inline-flex; align-items: center; justify-content: center; gap: 0.35rem; padding: 0 0.8rem; font-size: 12px; font-weight: 600; border-radius: 4px; cursor: pointer; height: 28px; } +.btn-primary { background-color: var(--primary-color); color: var(--white); border: none; } +.btn-outline { background-color: transparent; color: var(--text-muted); border: 1px solid var(--border-color); } +.badge { padding: 2px 6px; border-radius: 4px; font-size: 11px; font-weight: 700; } +.badge-primary { background-color: var(--primary-color); color: white; } +.badge-light { background: var(--bg-color); color: var(--text-muted); border: 1px solid var(--border-color); } +.hidden { display: none !important; } @media (max-width: 768px) { - .brand h1 .sub-title { display: none; } /* 아주 좁은 화면에선 영문명 숨김 */ - .header-actions .btn span { display: none; } /* 버튼 텍스트 숨기고 아이콘만 표시 */ - .header-actions .btn { padding: 0 0.5rem; } -} \ No newline at end of file + .brand h1 .sub-title { display: none; } + .header-actions .btn span { display: none; } +} diff --git a/src/views/List/ListFactory.ts b/src/views/List/ListFactory.ts index d460e66..0dc5b69 100644 --- a/src/views/List/ListFactory.ts +++ b/src/views/List/ListFactory.ts @@ -1,8 +1,12 @@ import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema'; -import { dynamicSort, renderPageHeader } from '../../core/utils'; +import { dynamicSort, renderPageHeader, calculateAssetAge, formatInline } from '../../core/utils'; import { setupTableSorting, SortState } from '../../core/tableHandler'; import { renderFilterBar, applyCommonFilters } from '../../core/filterHandler'; -import { createIcons, RefreshCcw, Plus, Edit2, Trash2, Users, Cloud, CreditCard, DollarSign, Paperclip } from 'lucide'; +import { state } from '../../core/state'; +import { + createIcons, RefreshCcw, Plus, Edit2, Trash2, Users, Cloud, + CreditCard, DollarSign, Paperclip +} from 'lucide'; export interface ColumnDef { header: string; @@ -28,97 +32,340 @@ export interface ListViewConfig { columns: ColumnDef[]; onRowClick?: (asset: any) => void; emptyMessage?: string; - persistentSortState?: SortState; // Allow passing external sort state (like DomainListView) + 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' }; - - // Initialize currentFilters with all possible keys to avoid undefined issues 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 = ` +
+ + +
+ `; + 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; + + + // [자산 현황] 대시보드 렌더러 + const renderSystemStatus = () => { + const isPcView = config.title === 'PC'; + const locationCounts: Record = {}; + 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 serviceType = asset[ASSET_SCHEMA.SERVICE_TYPE.key] || '외부'; + 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']; + + 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 = `` + currentDetailLocs.map(dl => ``).join(''); + } + + const tbody = document.getElementById('system-status-tbody'); + if (tbody) { + tbody.innerHTML = finalDisplayList.length === 0 + ? `조회된 자산이 없습니다.` + : finalDisplayList.map(asset => { + const purpose = asset[ASSET_SCHEMA.ASSET_PURPOSE.key] || ''; + const serviceType = asset[ASSET_SCHEMA.SERVICE_TYPE.key] || '외부'; + const labelColor = serviceType === '내부' ? '#94A3B8' : '#35635C'; + const memo = asset[ASSET_SCHEMA.MEMO.key] || ''; + return ` + + ${serviceType} + ${purpose || '-'} + ${formatInline(memo) || '-'} + ${asset[ASSET_SCHEMA.LOC_DETAIL.key] || '-'} + `; + }).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 && config.onRowClick) config.onRowClick(asset); + }); + row.addEventListener('mouseenter', () => { (row as HTMLElement).style.backgroundColor = '#F8FAFA'; }); + row.addEventListener('mouseleave', () => { (row as HTMLElement).style.backgroundColor = 'transparent'; }); + }); + } + }; + + 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} +
+ `} +
+
+ +
+ +
+
+

${isPcView ? '유형별 분포' : '위치별 분포'}

+ ${!isPcView && selectedLocation ? `` : ''} +
+
+ ${chartLabels.map((l, i) => ` +
+ + ${l} + ${chartData[i]} +
+ `).join('')} +
+
+ +
+
+ +
+
+

자산등록현황

+
+ 상세위치 필터: + +
+
+
+ + + + + + + + + + +
분류용도비고상세위치
+
+
+
+
+ `; + + (window as any).dispatchLocFilter = (loc: string) => { + if (isPcView) return; // PC 뷰에서는 위치 필터링 비활성화 (유형별로 보기 때문) + selectedLocation = loc; + selectedDetailLocation = null; + renderSystemStatus(); + }; + + setTimeout(() => { + const ctx = document.getElementById('system-location-chart') as HTMLCanvasElement; + if (ctx && typeof (window as any).Chart !== 'undefined') { + new (window as any).Chart(ctx, { + type: 'doughnut', + data: { labels: chartLabels, datasets: [{ data: chartData, backgroundColor: chartColors, borderWidth: 0 }] }, + options: { + responsive: true, maintainAspectRatio: false, cutout: '70%', + onClick: (evt: any, elements: any[]) => { + if (!isPcView && elements.length > 0) { + selectedLocation = locLabels[elements[0].index]; + selectedDetailLocation = null; + renderSystemStatus(); + } + }, + plugins: { legend: { display: false } } + } + }); + } + document.getElementById('btn-reset-loc')?.addEventListener('click', () => { + selectedLocation = null; + selectedDetailLocation = null; + renderSystemStatus(); + }); + document.getElementById('select-detail-loc')?.addEventListener('change', (e) => { + selectedDetailLocation = (e.target as HTMLSelectElement).value || null; + updateTableOnly(); + }); + updateTableOnly(); + }, 100); + }; + + // [자산 목록] 테이블 렌더러 const tableWrapper = document.createElement('div'); tableWrapper.className = 'table-container'; const table = document.createElement('table'); - - // 1. 헤더 생성 const thead = document.createElement('thead'); - const trHead = document.createElement('tr'); - config.columns.forEach(col => { - const th = document.createElement('th'); - th.innerHTML = col.header; - if (col.sortKey) th.setAttribute('data-sort', col.sortKey); - if (col.width) th.style.width = col.width; - if (col.align) th.style.textAlign = col.align; - if (col.className) th.className = col.className; - trHead.appendChild(th); - }); - thead.appendChild(trHead); - table.appendChild(thead); - - // 2. 본문 생성 const tbody = document.createElement('tbody'); tbody.id = 'dynamic-tbody'; + table.appendChild(thead); table.appendChild(tbody); - tableWrapper.appendChild(table); - container.appendChild(tableWrapper); - // 3. 테이블 업데이트 로직 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); - if (sortState.key) { - filtered = dynamicSort(filtered, sortState.key, sortState.direction); - } + thead.innerHTML = `${config.columns.map(col => ` + ${col.header}`).join('')}`; - tbody.innerHTML = ''; - if (filtered.length === 0) { - const emptyMsg = config.emptyMessage || UI_TEXT.MESSAGES.NO_DATA; - tbody.innerHTML = `${emptyMsg}`; - return; - } + tbody.innerHTML = filtered.length === 0 + ? `${config.emptyMessage || UI_TEXT.MESSAGES.NO_DATA}` + : filtered.map(asset => ` + + ${config.columns.map(col => `${col.render(asset)}`).join('')} + `).join(''); - filtered.forEach((asset) => { - const tr = document.createElement('tr'); - if (config.onRowClick) { - tr.style.cursor = 'pointer'; - tr.addEventListener('click', () => config.onRowClick!(asset)); - } - - config.columns.forEach(col => { - const td = document.createElement('td'); - if (col.align) td.style.textAlign = col.align; - if (col.className) td.className = col.className; - td.innerHTML = col.render(asset); - tr.appendChild(td); - }); - - tbody.appendChild(tr); + 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 external state was provided, sync it back if (config.persistentSortState) { - config.persistentSortState.key = key; - config.persistentSortState.direction = dir; + config.persistentSortState.key = key; + config.persistentSortState.direction = dir; } updateTable(); }); - // 모든 가능한 아이콘 로드 (안전하게) createIcons({ icons: { RefreshCcw, Plus, Edit2, Trash2, Users, Cloud, CreditCard, DollarSign, Paperclip } }); }; - // 4. 필터 바 렌더링 + // --- 뷰 전환 로직 --- + 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) => { @@ -127,18 +374,11 @@ export function createListView(container: HTMLElement, config: ListViewConfig) { } }); - // 5. 동적 Select 박스 데이터 채우기 + // 셀렉트 박스 채우기 const populateSelect = (selector: string, dataKey: string) => { const select = container.querySelector(selector) as HTMLSelectElement; if (select) { - // Handle multiple possible keys for department names due to legacy data - const getVal = (a: any) => { - if (dataKey === ASSET_SCHEMA.CURRENT_DEPT.key) { - return a[dataKey] || a['현사용부서'] || a['현사용조직']; - } - return a[dataKey]; - } - + 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'); @@ -154,6 +394,6 @@ export function createListView(container: HTMLElement, config: ListViewConfig) { if (config.filterOptions.showCorp) populateSelect('#filter-corp', ASSET_SCHEMA.PURCHASE_CORP.key); if (config.filterOptions.showType) populateSelect('#filter-type', ASSET_SCHEMA.ASSET_TYPE.key); - // 6. 초기 렌더링 - updateTable(); + // 초기 실행 + switchView(); }