diff --git a/src/views/Dashboard/HwDashboard.ts b/src/views/Dashboard/HwDashboard.ts index 3dcff22..be7ac87 100644 --- a/src/views/Dashboard/HwDashboard.ts +++ b/src/views/Dashboard/HwDashboard.ts @@ -9,6 +9,55 @@ declare var Chart: any; let donutChartInstance: any = null; export function renderHwDashboard(container: HTMLElement) { + // 전역 툴팁 헬퍼 함수 등록 + (window as any).showSpecTooltip = function(event: MouseEvent, element: HTMLElement, type: string, count: number) { + const container = element.closest('.spec-bar-container'); + if (!container) return; + const tooltip = container.querySelector('.spec-tooltip') as HTMLElement; + if (!tooltip) return; + const textSpan = tooltip.querySelector('.tooltip-text') as HTMLElement; + if (textSpan) { + let color = ''; + let label = ''; + if (type === 'under') { + color = '#EF4444'; + label = '부족'; + } else if (type === 'normal') { + color = '#10B981'; + label = '적정'; + } else if (type === 'over') { + color = '#F59E0B'; + label = '오버'; + } else if (type === 'win11') { + color = '#7928ca'; + label = '윈도우 11 불가'; + } + textSpan.innerHTML = `${label} ${count}대`; + } + tooltip.style.left = event.clientX + 'px'; + tooltip.style.top = event.clientY + 'px'; + tooltip.style.opacity = '1'; + }; + + (window as any).updateSpecTooltipPos = function(event: MouseEvent, element: HTMLElement) { + const container = element.closest('.spec-bar-container'); + if (!container) return; + const tooltip = container.querySelector('.spec-tooltip') as HTMLElement; + if (tooltip) { + tooltip.style.left = event.clientX + 'px'; + tooltip.style.top = event.clientY + 'px'; + } + }; + + (window as any).hideSpecTooltip = function(element: HTMLElement) { + const container = element.closest('.spec-bar-container'); + if (!container) return; + const tooltip = container.querySelector('.spec-tooltip') as HTMLElement; + if (tooltip) { + tooltip.style.opacity = '0'; + } + }; + // 1. 개인용 PC 데이터 추출 (유형이 '개인PC'이거나 상태가 '재고' 또는 '대기' 상태인 PC 집계) const pcs = (state.masterData.pc || []).filter((a: any) => a.asset_type === '개인PC' || @@ -17,19 +66,11 @@ export function renderHwDashboard(container: HTMLElement) { // 2. 1페이지 매거진 리포트(제목바 제거, '| 제목' 미니멀리즘 스타일) HTML 빌드 container.innerHTML = ` -
+
- -
-
-

- 개인 PC 자산 대시보드 -

-
- - -
- 조직 필터: + +
+
@@ -42,125 +83,108 @@ export function renderHwDashboard(container: HTMLElement) {
- -
+ +
- -
+ +
-
-
- +
+
+ 보유 자산 수량
-
- - 보유 자산 수량 -
-
0대
+
0대
-
-
- +
+
+ 사양 부족
-
- - 사양 부족 -
-
0대
+
0대
-
-
- +
+
+ 오버 스펙
-
- - 오버 스펙 -
-
0대
+
0대
-
-
- +
+
+ 윈도우 11 불가
-
- - 윈도우 11 불가 PC -
-
0대
+
0대
- -
+ +
- -
+ +
- 등급별 보유 비율 + 조직별 사용 비율
- +
-
+
- - 최상급 + + 한맥
- - 상급 + + 삼안 +
+
+ + 장헌 +
+
+ + 한라
- 중급 + 기술개발센터
- - 보급 + + 총괄기획실
- - 교체 + + 기타
- -
+ +
- 연도별 PC 노후도 및 예측 + PC 노후도
- +
- - - + + @@ -174,26 +198,25 @@ export function renderHwDashboard(container: HTMLElement) { - -
-
+ +
+
-
- 등급별 자산 종합 현황 및 사양 적정성 분석 +
+ 등급별 자산 종합현황
-
-
구분 (연한)보유권장 조치구분 (연한)보유
+
+
- - - - - - + + + + + + @@ -226,7 +249,16 @@ export function renderHwDashboard(container: HTMLElement) { }); btn.classList.add('active'); - btn.style.background = '#1E5149'; + const dept = btn.getAttribute('data-dept') || ''; + let bgColor = '#1E5149'; + if (dept === '한맥') bgColor = '#D02121'; + else if (dept === '삼안') bgColor = '#F58120'; + else if (dept === '장헌') bgColor = '#3889C7'; + else if (dept === '한라') bgColor = '#79B2D9'; + else if (dept === '기술개발센터') bgColor = '#10B981'; + else if (dept === '총괄기획실') bgColor = '#133D84'; + + btn.style.background = bgColor; btn.style.color = 'white'; const selectedDept = btn.getAttribute('data-dept') || ''; @@ -252,13 +284,31 @@ function updateDashboardData(pcs: any[], selectedDept: string) { }); // 3. DB 기준 사양 데이터 맵핑 (state.masterData.jobSpecs 이용) - const jobSpecsMap: Record = {}; + const jobSpecsMap: Record = {}; if (state.masterData.jobSpecs) { state.masterData.jobSpecs.forEach((s: any) => { - jobSpecsMap[s.job_name] = s.min_score; + jobSpecsMap[s.job_name] = s.required_grade || '중급'; }); } + // 사용자 이름 → 세부 직무 맵 생성 (system_users.position 기준, 더 정확한 직무 구분) + const userPositionMap: Record = {}; + if (state.masterData.users) { + state.masterData.users.forEach((u: any) => { + if (u.user_name && u.position) { + userPositionMap[u.user_name.trim()] = u.position.trim(); + } + }); + } + + const GRADE_RANK: Record = { + 'premium': 4, '최상급': 4, + 'high': 3, '상급': 3, + 'normal': 2, '중급': 2, + 'entry': 1, '보급': 1, + 'replace': 0, '교체 대상': 0 + }; + const jobScores: Record = {}; pcs.forEach((p: any) => { const score = calculatePcScoreDeductive(p.cpu, p.ram, p.gpu, p.purchase_date); @@ -306,7 +356,7 @@ function updateDashboardData(pcs: any[], selectedDept: string) { currentGradeKey = 'high'; } else if (score >= 40) { currentGradeKey = 'normal'; - } else if (score >= 20 && !win11Incompatible) { + } else if (score >= 20) { currentGradeKey = 'entry'; } else { currentGradeKey = 'replace'; @@ -323,23 +373,32 @@ function updateDashboardData(pcs: any[], selectedDept: string) { currentTarget.active++; currentTarget.activePcs.push(p); - // 직무 적정성 계산 (재직 중이고 실 사용자 매핑 자산만 검토 대상) - const job = p[ASSET_SCHEMA.USER_POSITION.key] || '미분류'; - const standardScore = jobSpecsMap[job] !== undefined ? jobSpecsMap[job] : (jobScores[job]?.avg || 0); + // 직무 적정성 계산: system_users.position 우선 조회 → asset_core.user_position fallback + const userName = (p[ASSET_SCHEMA.CURRENT_USER.key] || '').trim(); + const job = userPositionMap[userName] || p[ASSET_SCHEMA.USER_POSITION.key] || '미분류'; + const requiredGrade = jobSpecsMap[job] || jobSpecsMap[p[ASSET_SCHEMA.USER_POSITION.key]] || '중급'; // 세부 직무 우선, 없으면 일반 직무, 없으면 기본 중급 + + // 미니 모달 표시용으로 해석된 세부 직무명 저장 + p._resolved_position = job; + + const actualGrade = currentGradeKey; // premium, high, normal, entry, replace 중 하나 + + const reqRank = GRADE_RANK[requiredGrade] !== undefined ? GRADE_RANK[requiredGrade] : 2; // '중급' rank + const actRank = GRADE_RANK[actualGrade] !== undefined ? GRADE_RANK[actualGrade] : 0; let isUnder = false; - if (standardScore > 0 && job !== '재고PC') { - if (score < standardScore * 0.6) { + if (job !== '재고PC') { + if (win11Incompatible) { isUnder = true; p._spec_status = '사양 부족'; - } else if (score > standardScore * 1.5 && !win11Incompatible) { + } else if (actRank < reqRank) { + isUnder = true; + p._spec_status = '사양 부족'; + } else if (actRank > reqRank) { p._spec_status = '오버스펙'; criticalList.push(p); overSpecCount++; - } else if (win11Incompatible) { - isUnder = true; - p._spec_status = '사양 부족'; } else { p._spec_status = '적정'; } @@ -357,16 +416,11 @@ function updateDashboardData(pcs: any[], selectedDept: string) { underSpecCount++; // 2. 사양 부족 시 교체받아야 할 직무별 권장 목표 등급 판정 - let targetGradeKey: keyof typeof matrix; - if (standardScore >= 85) { - targetGradeKey = 'premium'; - } else if (standardScore >= 70) { - targetGradeKey = 'high'; - } else if (standardScore >= 40) { - targetGradeKey = 'normal'; - } else { - targetGradeKey = 'entry'; // 교체 대상은 최소 보급형 사양으로 교체 - } + let targetGradeKey: keyof typeof matrix = 'normal'; + if (requiredGrade === '최상급') targetGradeKey = 'premium'; + else if (requiredGrade === '상급') targetGradeKey = 'high'; + else if (requiredGrade === '중급') targetGradeKey = 'normal'; + else if (requiredGrade === '보급') targetGradeKey = 'entry'; const targetGrade = matrix[targetGradeKey]; targetGrade.under++; @@ -390,60 +444,76 @@ function updateDashboardData(pcs: any[], selectedDept: string) { const matrixTbody = document.getElementById('pc-grade-matrix-tbody')!; const getSpecStatusCounts = (activePcsList: any[]) => { + let win11 = 0; let under = 0; let normal = 0; let over = 0; activePcsList.forEach(p => { - if (p._spec_status === '사양 부족') under++; + if (isWindows11Incompatible(p.cpu, p.ram)) win11++; + else if (p._spec_status === '사양 부족') under++; else if (p._spec_status === '오버스펙') over++; else normal++; }); - return { under, normal, over }; + return { win11, under, normal, over }; }; + const maxTotal = Math.max( + matrix.premium.total, + matrix.high.total, + matrix.normal.total, + matrix.entry.total, + matrix.replace.total + ); + const renderMatrixRow = (gradeKey: keyof typeof matrix, label: string, color: string, shortage: number) => { const data = matrix[gradeKey]; const totalRate = filtered.length > 0 ? Math.round((data.total / filtered.length) * 100) : 0; - const cellStyle = `padding: 6px 8px; text-align: center; font-weight: 700; cursor: pointer; transition: background 0.2s; font-size: 0.95rem;`; + const cellStyle = `padding: 22px 8px; text-align: center; font-weight: 700; cursor: pointer; transition: background 0.2s; font-size: 1.05rem;`; const hoverEvents = `onmouseover="this.style.background='#F1F5F9'" onmouseout="this.style.background='none'"`; // 사양 적정성 분석 데이터 계산 (운영중인 자산만) - const { under, normal, over } = getSpecStatusCounts(data.activePcs); + const { win11, under, normal, over } = getSpecStatusCounts(data.activePcs); const activeCount = data.active; + const win11Pct = activeCount > 0 ? (win11 / activeCount) * 100 : 0; const underPct = activeCount > 0 ? (under / activeCount) * 100 : 0; const normalPct = activeCount > 0 ? (normal / activeCount) * 100 : 0; const overPct = activeCount > 0 ? (over / activeCount) * 100 : 0; + const rowTotal = data.total; + const barWidthPct = maxTotal > 0 ? (rowTotal / maxTotal) * 100 : 0; + let barGraphHtml = ''; if (activeCount > 0) { barGraphHtml = ` -
-
- ${under > 0 ? `
` : ''} - ${normal > 0 ? `
` : ''} - ${over > 0 ? `
` : ''} +
+ +
+ ${win11 > 0 ? `
` : ''} + ${under > 0 ? `
` : ''} + ${normal > 0 ? `
` : ''} + ${over > 0 ? `
` : ''}
-
- ${under > 0 ? `부족 ${under}` : ''} - ${normal > 0 ? `적정 ${normal}` : ''} - ${over > 0 ? `오버 ${over}` : ''} + +
+ +
`; } else { - barGraphHtml = `운영중 자산 없음`; + barGraphHtml = `운영중 자산 없음`; } return `
- - + + - @@ -467,7 +537,7 @@ function updateDashboardData(pcs: any[], selectedDept: string) { const totalShortage = premiumShortage + highShortage + normalShortage + entryShortage + replaceShortage; const totalActivePcs = filtered.filter(p => !isStock(p)); - const { under: totUnder, normal: totNormal, over: totOver } = getSpecStatusCounts(totalActivePcs); + const { win11: totWin11, under: totUnder, normal: totNormal, over: totOver } = getSpecStatusCounts(totalActivePcs); const totUnderPct = totalActive > 0 ? (totUnder / totalActive) * 100 : 0; const totNormalPct = totalActive > 0 ? (totNormal / totalActive) * 100 : 0; const totOverPct = totalActive > 0 ? (totOver / totalActive) * 100 : 0; @@ -475,24 +545,25 @@ function updateDashboardData(pcs: any[], selectedDept: string) { let totBarGraphHtml = ''; if (totalActive > 0) { totBarGraphHtml = ` -
-
- ${totUnder > 0 ? `
` : ''} - ${totNormal > 0 ? `
` : ''} - ${totOver > 0 ? `
` : ''} +
+ +
+ ${totUnder > 0 ? `
` : ''} + ${totNormal > 0 ? `
` : ''} + ${totOver > 0 ? `
` : ''}
-
- ${totUnder > 0 ? `부족 ${totUnder}` : ''} - ${totNormal > 0 ? `적정 ${totNormal}` : ''} - ${totOver > 0 ? `오버 ${totOver}` : ''} + +
+ +
`; } else { - totBarGraphHtml = `운영중 자산 없음`; + totBarGraphHtml = `운영중 자산 없음`; } - const cellStyleHeader = `padding: 6px 8px; text-align: center; font-weight: 800; cursor: pointer; transition: background 0.2s; background: #F8FAFC; font-size: 0.95rem;`; + const cellStyleHeader = `padding: 12px 10px; text-align: center; font-weight: 800; cursor: pointer; transition: background 0.2s; background: #F8FAFC; font-size: 1.05rem;`; const hoverEventsHeader = `onmouseover="this.style.background='#EEF2F6'" onmouseout="this.style.background='#F8FAFC'"`; matrixTbody.innerHTML = ` @@ -500,17 +571,7 @@ function updateDashboardData(pcs: any[], selectedDept: string) { ${renderMatrixRow('high', '상급 PC (70점 ~ 85점)', '#1E8E7C', highShortage)} ${renderMatrixRow('normal', '중급 PC (40점 ~ 70점)', '#10B981', normalShortage)} ${renderMatrixRow('entry', '보급 PC (20점 ~ 40점)', '#F59E0B', entryShortage)} - ${renderMatrixRow('replace', '교체 대상 PC (20점 미만 또는 Win11 불가)', '#EF4444', replaceShortage)} -
- - - - - - - + ${renderMatrixRow('replace', '교체 대상 PC (20점 미만)', '#EF4444', replaceShortage)} `; // 셀별 동적 클릭 리스너 바인딩 @@ -535,7 +596,7 @@ function updateDashboardData(pcs: any[], selectedDept: string) { if (t === 'total') return '보유'; if (t === 'active') return '운영중'; if (t === 'stock') return '재고'; - if (t === 'under') return '구매 필요'; + if (t === 'under') return '부족분'; return ''; }; @@ -575,11 +636,21 @@ function updateDashboardData(pcs: any[], selectedDept: string) { const status = target.getAttribute('data-spec-status')!; let targetPcs: any[] = []; + const filterFn = (p: any) => { + if (status === '윈도우 11 불가') { + return isWindows11Incompatible(p.cpu, p.ram); + } else if (status === '사양 부족') { + return !isWindows11Incompatible(p.cpu, p.ram) && p._spec_status === '사양 부족'; + } else { + return p._spec_status === status; + } + }; + if (grade === 'all') { - targetPcs = filtered.filter(p => !isStock(p) && p._spec_status === status); + targetPcs = filtered.filter(p => !isStock(p) && filterFn(p)); } else { const data = matrix[grade as keyof typeof matrix]; - targetPcs = data.activePcs.filter(p => p._spec_status === status); + targetPcs = data.activePcs.filter(filterFn); } const getGradeLabel = (g: string) => { @@ -622,23 +693,20 @@ function updateDashboardData(pcs: any[], selectedDept: string) { const agingTbody = document.getElementById('pc-aging-tbody')!; - const renderAgingRow = (label: string, list: any[], badgeText: string, badgeStyle: string, ageGroupKey: string) => { + const renderAgingRow = (label: string, list: any[], ageGroupKey: string) => { return ` - - - + + `; }; agingTbody.innerHTML = ` - ${renderAgingRow('즉시 교체 (7년 이상)', agingCounts.immediate, '즉시 교체', 'background:#FFF1F2; color:#EF4444; border:1px solid #FCA5A5;', 'immediate')} - ${renderAgingRow('교체 검토 (3년 ~ 7년)', agingCounts.review, '교체 검토', 'background:#FFF7ED; color:#D97706; border:1px solid #FCD34D;', 'review')} - ${renderAgingRow('정상 운용 (1년 ~ 3년)', agingCounts.normal, '정상 운용', 'background:#ECFDF5; color:#059669; border:1px solid #A7F3D0;', 'normal')} - ${renderAgingRow('최신 도입 (1년 미만)', agingCounts.fresh, '최신 도입', 'background:#F0FDF4; color:#16A34A; border:1px solid #BBF7D0;', 'fresh')} + ${renderAgingRow('즉시 교체 (7년 이상)', agingCounts.immediate, 'immediate')} + ${renderAgingRow('교체 검토 (3년 ~ 7년)', agingCounts.review, 'review')} + ${renderAgingRow('정상 운용 (1년 ~ 3년)', agingCounts.normal, 'normal')} + ${renderAgingRow('최신 도입 (1년 미만)', agingCounts.fresh, 'fresh')} `; agingTbody.querySelectorAll('.aging-row').forEach(row => { @@ -656,14 +724,14 @@ function updateDashboardData(pcs: any[], selectedDept: string) { }); // 8. 요약 지표 카드 클릭 리스너 설정 - const bindCardClick = (id: string, gradeTitle: string, filterFn: (p: any) => boolean) => { + const bindCardClick = (id: string, gradeTitle: string, filterFn: (p: any) => boolean, hoverBgColor: string) => { const card = document.getElementById(id)!; if (!card) return; card.style.cursor = 'pointer'; - card.style.transition = 'opacity 0.2s'; + card.style.transition = 'background-color 0.15s ease'; - card.onmouseover = () => { card.style.opacity = '0.7'; }; - card.onmouseout = () => { card.style.opacity = '1'; }; + card.onmouseover = () => { card.style.backgroundColor = hoverBgColor; }; + card.onmouseout = () => { card.style.backgroundColor = '#ffffff'; }; card.onclick = () => { const pcsInGrade = filtered.filter(filterFn); @@ -672,12 +740,48 @@ function updateDashboardData(pcs: any[], selectedDept: string) { }; // 사양 부족 / 오버 스펙 / 윈도우 11 불가 클릭 리스너 설정 - bindCardClick('card-under-spec', '사양 부족 대상', p => p._spec_status === '사양 부족'); - bindCardClick('card-over-spec', '오버 스펙 대상', p => p._spec_status === '오버스펙'); - bindCardClick('card-win11-incompatible', '윈도우 11 업그레이드 불가 PC', p => isWindows11Incompatible(p.cpu, p.ram)); + bindCardClick('card-under-spec', '사양 부족 대상', p => p._spec_status === '사양 부족', '#FEF2F2'); + bindCardClick('card-over-spec', '오버 스펙 대상', p => p._spec_status === '오버스펙', '#FFFBEB'); + bindCardClick('card-win11-incompatible', '윈도우 11 업그레이드 불가 PC', p => isWindows11Incompatible(p.cpu, p.ram), '#F5F3FF'); + + // 9. 조직별 사용 비율 집계 (전체 개인용 PC 기준) + const deptCounts: Record = { + '한맥': 0, + '삼안': 0, + '장헌': 0, + '한라': 0, + '기술개발센터': 0, + '총괄기획실': 0, + '기타': 0 + }; + + pcs.forEach((p: any) => { + const dept = String(p[ASSET_SCHEMA.CURRENT_DEPT.key] || '').trim(); + let matched = false; + for (const key of Object.keys(deptCounts)) { + if (key !== '기타' && dept.includes(key)) { + deptCounts[key]++; + matched = true; + break; + } + } + if (!matched) { + deptCounts['기타']++; + } + }); + + const deptChartData = [ + { label: '한맥', count: deptCounts['한맥'], color: '#D02121' }, + { label: '삼안', count: deptCounts['삼안'], color: '#F58120' }, + { label: '장헌', count: deptCounts['장헌'], color: '#3889C7' }, + { label: '한라', count: deptCounts['한라'], color: '#79B2D9' }, + { label: '기술개발센터', count: deptCounts['기술개발센터'], color: '#10B981' }, + { label: '총괄기획실', count: deptCounts['총괄기획실'], color: '#133D84' }, + { label: '기타', count: deptCounts['기타'], color: '#94A3B8' } + ]; // 10. 도넛 차트 렌더링 호출 - renderDonutChart(matrix.premium.total, matrix.high.total, matrix.normal.total, matrix.entry.total, matrix.replace.total); + renderDonutChart(deptChartData); // 전역 상태 등록 state.activeCharts = [donutChartInstance]; @@ -746,7 +850,7 @@ function showMiniListModal(title: string, list: any[]) { return ` - + @@ -802,7 +906,7 @@ function showMiniListModal(title: string, list: any[]) { /** * 실시간 사양 적정률 원형 도넛 그래프 (Active Spec Rate) */ -function renderDonutChart(premium: number, high: number, normal: number, entry: number, replace: number) { +function renderDonutChart(deptData: { label: string; count: number; color: string }[]) { const ctx = document.getElementById('chart-overall-donut') as HTMLCanvasElement; if (!ctx || typeof Chart === 'undefined') return; @@ -811,21 +915,15 @@ function renderDonutChart(premium: number, high: number, normal: number, entry: donutChartInstance = null; } - const total = premium + high + normal + entry + replace; + const total = deptData.reduce((sum, d) => sum + d.count, 0); donutChartInstance = new Chart(ctx, { type: 'doughnut', data: { - labels: ['최상급', '상급', '중급', '보급', '교체 대상'], + labels: deptData.map(d => d.label), datasets: [{ - data: [premium, high, normal, entry, replace], - backgroundColor: [ - '#11302B', // premium (Hanmac Dark Green) - '#1E8E7C', // high (Hanmac Teal) - '#10B981', // normal (Hanmac Mint) - '#F59E0B', // entry (Yellow-Orange) - '#EF4444' // replace (Red) - ], + data: deptData.map(d => d.count), + backgroundColor: deptData.map(d => d.color), borderColor: '#ffffff', borderWidth: 2 }] diff --git a/src/views/List/ListFactory.ts b/src/views/List/ListFactory.ts index ae7a26b..28bcd40 100644 --- a/src/views/List/ListFactory.ts +++ b/src/views/List/ListFactory.ts @@ -153,6 +153,7 @@ export interface ListViewConfig { showField?: boolean; showType?: boolean; showStatus?: boolean; + showPosition?: boolean; }; columns: ColumnDef[]; onRowClick?: (asset: any) => void; @@ -161,9 +162,8 @@ export interface ListViewConfig { } export function createListView(container: HTMLElement, config: ListViewConfig) { - // 1. 컨테이너 초기화 및 헤더 렌더링 + // 1. 컨테이너 초기화 container.innerHTML = ''; - renderPageHeader(container, config.title); const fullList = config.dataSource(); let sortState: SortState = config.persistentSortState || { key: '', direction: 'asc' }; @@ -181,46 +181,20 @@ export function createListView(container: HTMLElement, config: ListViewConfig) { const isServer = config.title === '서버'; if (!isServer) { (state as any).currentViewMode = 'asset'; - } else if (!(state as any).currentViewMode) { - (state as any).currentViewMode = 'system'; } - // 2. 뷰 전환 토글 버튼 생성 (명칭 변경) - const toggleWrapper = document.createElement('div'); - toggleWrapper.className = 'view-toggle-container'; - - const showPcFlowBtn = config.title === 'PC'; - toggleWrapper.innerHTML = ` -
-
- - -
-
- ${showPcFlowBtn ? ` - - - ` : ''} - -
-
- `; - container.appendChild(toggleWrapper); - - // 3. 필터 바 생성 (자산 목록에서만 사용) - const filterBar = document.createElement('div'); - filterBar.className = 'search-bar'; - container.appendChild(filterBar); - - // 4. 컨텐츠 영역 생성 + // 1. 컨텐츠 영역 생성 (먼저 생성하여 참조 가능하게 함) const contentWrapper = document.createElement('div'); contentWrapper.className = 'view-content-wrapper'; + + // 2. 필터 바 생성 (자산 목록에서만 사용) + const filterBar = document.createElement('div'); + filterBar.className = 'search-bar'; + + // 자산 추가 버튼 및 목록 보기 체크박스 추가 로직 + const showPcFlowBtn = config.title === 'PC'; + + container.appendChild(filterBar); container.appendChild(contentWrapper); // --- 내부 상태 --- @@ -228,7 +202,6 @@ export function createListView(container: HTMLElement, config: ListViewConfig) { let selectedDetailLocation: string | null = null; let dynamicMapConfig: Record = {}; - // 맵 설정 미리 로드 const fetchMapConfig = async () => { try { const res = await fetch(`http://${location.hostname}:3000/api/maps`); @@ -254,15 +227,12 @@ export function createListView(container: HTMLElement, config: ListViewConfig) { selectedLocation = validLocations[0] || ''; } - const locationCounts: Record = {}; - const pcTypeCounts = { public: 0, server: 0, personal: 0 }; - - // 동적 통계 수집 객체 (Hardcoding 제거) + // 동적 통계 수집 객체 const extStats = { total: 0, locCounts: {} as Record, typeCounts: {} as Record, - typeLocMap: {} as Record>, // 유형별 위치 분포 + typeLocMap: {} as Record>, locWarning: 0, typeWarning: 0 }; @@ -273,41 +243,23 @@ export function createListView(container: HTMLElement, config: ListViewConfig) { typeLocMap: {} as Record> }; - // 중앙화된 경고 감지 로직 const checkAnomaly = (serviceType: string, loc: string, type: string) => { - if (serviceType !== '외부') return { isWarning: false, isLocWarning: false, isTypeWarning: false, reason: '' }; + if (serviceType !== '외부') return { isWarning: false, isLocWarning: false, isTypeWarning: false }; const isLocWarning = loc !== 'IDC' && loc !== '미지정' && loc !== ''; const isTypeWarning = type.toLowerCase().replace(/\s/g, '').includes('서버pc'); - const isWarning = isLocWarning || isTypeWarning; - - let reason = ''; - if (isLocWarning && isTypeWarning) reason = '위치/형식 부적절'; - else if (isLocWarning) reason = '위치 부적절'; - else if (isTypeWarning) reason = '형식 부적절'; - - return { isWarning, isLocWarning, isTypeWarning, reason }; + return { isWarning: isLocWarning || isTypeWarning, isLocWarning, isTypeWarning }; }; fullList.forEach(asset => { const loc = asset[ASSET_SCHEMA.LOCATION.key] || '미지정'; - const serviceTypeKey = (ASSET_SCHEMA as any).SERVICE_TYPE?.key || 'service_type'; - const serviceType = asset[serviceTypeKey] || '외부'; + const serviceType = asset.service_type || '외부'; 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++; - } - const targetStat = serviceType === '내부' ? intStats : extStats; targetStat.total++; if (loc) targetStat.locCounts[loc] = (targetStat.locCounts[loc] || 0) + 1; if (type) { targetStat.typeCounts[type] = (targetStat.typeCounts[type] || 0) + 1; - // 유형별 위치 분포 수집 if (!targetStat.typeLocMap[type]) targetStat.typeLocMap[type] = {}; targetStat.typeLocMap[type][loc] = (targetStat.typeLocMap[type][loc] || 0) + 1; } @@ -319,180 +271,139 @@ export function createListView(container: HTMLElement, config: ListViewConfig) { } }); - // 템플릿 제너레이터 함수 (HTML 중복 제거) const generateDetailStatHTML = (title: string, stats: any) => ` -
- ${title} -
- ${stats.locWarning ? `위치부적절: ${stats.locWarning}` : ''} - ${stats.typeWarning ? `형식부적절: ${stats.typeWarning}` : ''} +
+ ${title} +
+ ${stats.locWarning ? `위치부적절: ${stats.locWarning}` : ''} + ${stats.typeWarning ? `형식부적절: ${stats.typeWarning}` : ''}
-
-
- ${Object.entries(stats.locCounts as Record).sort((a, b) => b[1] - a[1]).slice(0, 4).map(([l, c]) => `${l}: ${c}`).join('')} +
+
+ ${Object.entries(stats.locCounts as Record).sort((a, b) => b[1] - a[1]).slice(0, 4).map(([l, c]) => `${l}: ${c}`).join('')}
-
+
${Object.entries(stats.typeCounts as Record).sort((a, b) => b[1] - a[1]).slice(0, 6).map(([t, c]) => { - const isTypeWarning = title.includes('외부') && t.toLowerCase().replace(/\s/g, '').includes('서버pc'); - - // 위치별 상세 정보 생성 (툴팁용) const locDist = stats.typeLocMap[t] || {}; - const locHint = Object.entries(locDist) - .sort((a: any, b: any) => b[1] - a[1]) - .map(([l, count]) => `${l}: ${count}대`) - .join('\n'); - - return `${t}: ${c}`; + const locHint = Object.entries(locDist).sort((a: any, b: any) => b[1] - a[1]).map(([l, count]) => `${l}: ${count}대`).join('\n'); + return `${t}: ${c}`; }).join('')}
`; contentWrapper.innerHTML = ` -
- - -
-
-
총 보유 자산
-
${fullList.length}
-
- 외부: ${extStats.total} - 내부: ${intStats.total} +
+
+
+
총 보유 자산
+
${fullList.length}
+
+ 외부: ${extStats.total} + 내부: ${intStats.total}
- -
- ${isPcView ? ` -
PC 유형별 현황
-
- 공용: ${pcTypeCounts.public} - 서버: ${pcTypeCounts.server} - 개인: ${pcTypeCounts.personal} -
- ` : generateDetailStatHTML('외부 (운영) 상세', extStats)} -
- -
- ${isPcView ? '' : generateDetailStatHTML('내부 (테스트) 상세', intStats as any)} -
+
${generateDetailStatHTML('외부 (운영) 상세', extStats)}
+
${generateDetailStatHTML('내부 (테스트) 상세', intStats)}
-
- -
-
-

+
+ +
+
+ ${!isPcView ? ` -
- 위치: - ${validLocations.map(l => ``).join('')} - 상세: - +
` : ''}
-
-

구분 (등급)보유량운영중재고구매 필요사양 적정성 분석 (직무 기준)구분 (등급)보유량운영중재고부족분사양 적정성
${label}${data.total}대 (${totalRate}%)${label}${data.total}대 (${totalRate}%) ${data.active}대 ${data.stock}대 ${shortage}대 + ${barGraphHtml}
합계 (Total)${totalPcs}대 (100%)${totalActive}대${totalStock}대${totalShortage}대 - ${totBarGraphHtml} -
${label}${list.length}대 - ${badgeText} - ${label}${list.length}대
${user}${pc.current_dept || '-'} (${pc.user_position || '-'})${pc.current_dept || '-'} (${pc._resolved_position || pc.user_position || '-'}) ${spec} ${badgeHTML}${scoreHTML} ${pc.asset_code || '-'}
- +
+
+ ${isPcView ? ` - - - - - - - - + + + + + + + + ` : ` - - - - - - + + + + + + `} - +
일자담당자구분사용자인수자자산번호상세
일자담당자구분사용자인수자자산번호상세
분류용도/자산명관리자(정)관리자(부)상세위치
분류용도/자산명관리자(정)관리자(부)상세위치
- -
-
+ +
+
${isPcView ? ` -
-
-

+
+
+
-
- - - - - - - +
+
사용자부서 (직무)상태자산코드
+ + + + + + - - + +
사용자부서 (직무)상태자산코드
사양 주의 자산이 없습니다.
사양 주의 자산이 없습니다.
` : ` -

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

+

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

`}

-