import { state } from '../state'; import { HardwareAsset, SoftwareAsset } from '../excelHandler'; declare var Chart: any; /** * 대시보드 렌더링 메인 함수 */ export function renderDashboard(mainContent: HTMLElement) { mainContent.innerHTML = ''; // 기존 차트 리소스 해제 state.activeCharts.forEach(c => { if (c && typeof c.destroy === 'function') c.destroy(); }); state.activeCharts = []; if (state.activeCategory === 'hw') { renderHwDashboard(mainContent); } else { renderSwDashboard(mainContent); } } // --- 하드웨어 대시보드 --- function renderHwDashboard(container: HTMLElement) { const types = ['개인PC', '서버', '스토리지', '전산비품']; const units = ['대', '대', '대', '개']; const groups: any = {}; types.forEach(t => { groups[t] = { idle: [], active: [], aged: [], normal: [] }; }); state.masterData.hw.forEach(a => { if (!groups[a.type]) return; if (isHwIdle(a)) groups[a.type].idle.push(a); else groups[a.type].active.push(a); const ageY = getHwAgeYears(a); const isAged = a.type === '전산비품' ? ageY >= 3 : ageY >= 5; if (isAged) groups[a.type].aged.push(a); else groups[a.type].normal.push(a); }); // 사용현황 카드 생성 let usageCards = ''; types.forEach((t, i) => { const total = groups[t].idle.length + groups[t].active.length; const used = groups[t].active.length; const per = total > 0 ? Math.round((used / total) * 100) : 0; const barColor = per >= 50 ? 'var(--dash-primary)' : 'var(--dash-danger)'; usageCards += `
${t} 사용현황
${total}${units[i]} 중 ${used}${units[i]} 사용 중 · 유휴 ${groups[t].idle.length}${units[i]}
${per}%
${per >= 50 ? '잘 사용하고 있어요' : '점검이 필요합니다'}
`; }); // 노후화 카드 생성 let agedCards = ''; types.forEach((t, i) => { const total = groups[t].aged.length + groups[t].normal.length; const agedCount = groups[t].aged.length; const agedPer = total > 0 ? Math.round((agedCount / total) * 100) : 0; const threshold = t === '전산비품' ? '3년' : '5년'; agedCards += `
${t} 노후화 현황 ${threshold} 초과
전체 ${total}${units[i]} 중 ${agedCount}${units[i]} 노후 장비
${agedCount}${units[i]}
${agedPer}%
`; }); container.innerHTML = `

사용현황

${usageCards}

노후화 자산 비율

${agedCards}
`; // 클릭 이벤트 바인딩 container.querySelectorAll('[data-action="idle"]').forEach(card => { card.addEventListener('click', () => { const t = card.getAttribute('data-type')!; openDashboardDetail(`[${t}] 유휴 자산 목록`, groups[t].idle); }); }); container.querySelectorAll('[data-action="aged"]').forEach(card => { card.addEventListener('click', () => { const t = card.getAttribute('data-type')!; openDashboardDetail(`[${t}] 노후 장비 목록`, groups[t].aged); }); }); } // --- 소프트웨어 대시보드 --- function renderSwDashboard(container: HTMLElement) { let subQty = 0, subUsed = 0, subExp = 0, subTotal = 0; let permQty = 0, permUsed = 0, permExp = 0, permTotal = 0; const currentYear = new Date().getFullYear().toString(); const corps = ['한맥', '삼안', '바론']; const categories = ['업무공통', '개발S/W', '디자인', '설계S/W']; const costByCorp: Record = { '한맥': 0, '삼안': 0, '바론': 0 }; const costByCat: Record = {}; categories.forEach(c => costByCat[c] = 0); state.masterData.sw.forEach(sw => { const assigned = state.masterData.swUsers.filter(u => u.swId === sw.id).length; const qty = typeof sw.수량 === 'number' ? sw.수량 : parseInt(sw.수량||'0', 10); const priceStr = sw.금액 ? sw.금액.replace(/,/g, '') : '0'; const price = parseInt(priceStr, 10) || 0; if (sw.type === '구독SW') { subQty += qty; subUsed += assigned; subTotal++; if (isSWExpiring(sw)) subExp++; } else { permQty += qty; permUsed += assigned; permTotal++; if (isSWExpiring(sw)) permExp++; } // 오늘이 속해있는 년도(2026)의 사용 금액 합계 if (sw.구매일 && sw.구매일.startsWith(currentYear)) { if (costByCorp[sw.법인] !== undefined) costByCorp[sw.법인] += price; if (sw.분야 && costByCat[sw.분야] !== undefined) costByCat[sw.분야] += price; } }); const subPer = subQty > 0 ? Math.round((subUsed/subQty)*100) : 0; const permPer = permQty > 0 ? Math.round((permUsed/permQty)*100) : 0; const subExpPer = subTotal > 0 ? Math.round((subExp/subTotal)*100) : 0; const permExpPer = permTotal > 0 ? Math.round((permExp/permTotal)*100) : 0; container.innerHTML = `
구독 소프트웨어 사용정보
${subQty}개의 제품 중 ${subUsed}개 사용 중
${subPer}%
영구 소프트웨어 사용정보
${permQty}개의 제품 중 ${permUsed}개 사용 중
${permPer}%
구독 SW 만료 예정 30일 이내
전체 ${subTotal}개 제품 중 ${subExp}개 만료 예정
${subExp}개
${subExpPer}%
유지보수 만료 예정 30일 이내
전체 ${permTotal}개 제품 중 ${permExp}개 만료 예정
${permExp}개
${permExpPer}%

${currentYear}년 소프트웨어 도입 비용

법인별 도입 금액 (원)

분야별 도입 금액 (원)

`; // 차트 생성 setTimeout(() => { const ctxCorp = (document.getElementById('chart-cost-corp') as HTMLCanvasElement)?.getContext('2d'); const ctxCat = (document.getElementById('chart-cost-cat') as HTMLCanvasElement)?.getContext('2d'); if (ctxCorp && typeof Chart !== 'undefined') { const chartCorp = new Chart(ctxCorp, { type: 'bar', data: { labels: corps, datasets: [{ label: '도입 금액', data: corps.map(c => costByCorp[c]), backgroundColor: '#3b82f6', borderRadius: 4, barThickness: 20 // 막대 두께 줄임 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { y: { beginAtZero: true, ticks: { callback: (v: any) => v.toLocaleString() }, grid: { display: false } // 가로줄 삭제 }, x: { grid: { display: false } } } } }); state.activeCharts.push(chartCorp); } if (ctxCat && typeof Chart !== 'undefined') { const chartCat = new Chart(ctxCat, { type: 'bar', data: { labels: categories, datasets: [{ label: '도입 금액', data: categories.map(c => costByCat[c]), backgroundColor: '#10b981', borderRadius: 4, barThickness: 20 // 막대 두께 줄임 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { y: { beginAtZero: true, ticks: { callback: (v: any) => v.toLocaleString() }, grid: { display: false } // 가로줄 삭제 }, x: { grid: { display: false } } } } }); state.activeCharts.push(chartCat); } }, 0); // 클릭 이벤트 바인딩 container.querySelector('[data-action="sub-usage"]')?.addEventListener('click', () => { openSwUsageDetail('구독 소프트웨어 사용 목록', state.masterData.sw.filter(sw => sw.type === '구독SW')); }); container.querySelector('[data-action="perm-usage"]')?.addEventListener('click', () => { openSwUsageDetail('영구 소프트웨어 사용 목록', state.masterData.sw.filter(sw => sw.type === '영구SW')); }); container.querySelector('[data-action="sub-exp"]')?.addEventListener('click', () => { openSwDashboardDetail('구독 SW 만료 예정 목록', state.masterData.sw.filter(sw => sw.type === '구독SW' && isSWExpiring(sw))); }); container.querySelector('[data-action="perm-exp"]')?.addEventListener('click', () => { openSwDashboardDetail('유지보수 만료 예정 목록', state.masterData.sw.filter(sw => sw.type === '영구SW' && isSWExpiring(sw))); }); } // --- 공통 헬퍼 함수들 --- function isHwIdle(a: HardwareAsset) { if (a.type === '개인PC') return !a.사용자 || a.사용자.trim() === '' || a.사용자.trim() === '-'; if (a.type === '스토리지') return !a.담당자_정 || a.담당자_정.trim() === '' || a.담당자_정.trim() === '-'; return !a.관리자 || a.관리자.trim() === '' || a.관리자.trim() === '-'; } function getHwAgeYears(a: HardwareAsset) { if (!a.구매일) return 0; return (Date.now() - new Date(a.구매일).getTime()) / (1000 * 60 * 60 * 24 * 365.25); } function isSWExpiring(sw: SoftwareAsset) { if (sw.type === '구독SW' && sw.구독일) { const parts = sw.구독일.split('~'); if (parts.length > 1) { const endStr = parts[1].trim(); const endMs = new Date(endStr.replace(/\./g, '-')).getTime(); const diffDays = (endMs - Date.now()) / (1000 * 60 * 60 * 24); return diffDays >= 0 && diffDays <= 30; } } else if (sw.type === '영구SW' && sw.비고 && sw.비고.includes('유지보수: ~')) { const endStr = sw.비고.split('~')[1].trim(); const endMs = new Date(endStr.replace(/\./g, '-')).getTime(); const diffDays = (endMs - Date.now()) / (1000 * 60 * 60 * 24); return diffDays >= 0 && diffDays <= 30; } return false; } // --- 대시보드 상세 모달 제어 (main.ts의 함수 재사용 또는 이동 필요) --- // 일단 main.ts에 있는 함수를 전역에서 가져와 쓸 수 없으므로, 여기서 직접 정의하거나 main.ts에서 export 해야 합니다. // 구조 개선을 위해 main.ts에서 이 함수들도 DashboardView로 옮기는 것이 좋습니다. function openDashboardDetail(title: string, list: HardwareAsset[]) { const modal = document.getElementById('dashboard-detail-modal') as HTMLDivElement; const titleEl = document.getElementById('dashboard-detail-modal-title') as HTMLHeadingElement; const tbody = document.getElementById('dashboard-detail-tbody') as HTMLTableSectionElement; const thead = tbody.closest('table')!.querySelector('thead')!; titleEl.textContent = title; thead.innerHTML = `No유형자산코드명칭/모델위치담당/사용자구매일금액`; tbody.innerHTML = ''; if (list.length === 0) { tbody.innerHTML = `해당 조건의 자산이 없습니다.`; } else { list.forEach((asset, idx) => { let manager = asset.관리자 || asset.사용자 || asset.담당자_정 || '-'; let name = asset.명칭 || asset.모델명 || '-'; const tr = document.createElement('tr'); tr.innerHTML = `${idx+1}${asset.type}${asset.자산코드}${name}${asset.위치||'-'}${manager}${asset.구매일||'-'}${asset.금액||'-'}`; tbody.appendChild(tr); }); } modal.classList.remove('hidden'); } function openSwDashboardDetail(title: string, list: SoftwareAsset[]) { const modal = document.getElementById('dashboard-detail-modal') as HTMLDivElement; const titleEl = document.getElementById('dashboard-detail-modal-title') as HTMLHeadingElement; const tbody = document.getElementById('dashboard-detail-tbody') as HTMLTableSectionElement; const thead = tbody.closest('table')!.querySelector('thead')!; titleEl.textContent = title; thead.innerHTML = `No유형법인제품명수량금액`; tbody.innerHTML = ''; list.forEach((sw, idx) => { const tr = document.createElement('tr'); tr.innerHTML = `${idx+1}${sw.type}${sw.법인}${sw.제품명}${sw.수량}${sw.금액}`; tbody.appendChild(tr); }); modal.classList.remove('hidden'); } function openSwUsageDetail(title: string, list: SoftwareAsset[]) { const modal = document.getElementById('dashboard-detail-modal') as HTMLDivElement; const titleEl = document.getElementById('dashboard-detail-modal-title') as HTMLHeadingElement; const tbody = document.getElementById('dashboard-detail-tbody') as HTMLTableSectionElement; const thead = tbody.closest('table')!.querySelector('thead')!; titleEl.textContent = title; thead.innerHTML = `No법인제품명수량사용중사용가능`; tbody.innerHTML = ''; list.forEach((sw, idx) => { const assigned = state.masterData.swUsers.filter(u => u.swId === sw.id).length; const avail = (typeof sw.수량 === 'number' ? sw.수량 : parseInt(sw.수량||'0', 10)) - assigned; const tr = document.createElement('tr'); tr.innerHTML = `${idx+1}${sw.법인}${sw.제품명}${sw.수량}${assigned}${avail}`; tbody.appendChild(tr); }); modal.classList.remove('hidden'); }