import { state } from '../core/state'; import { HardwareAsset, SoftwareAsset } from '../core/excelHandler'; import { openDashboardDetail, openSwDashboardDetail, openSwUsageDetail } from '../components/Modal/DashboardDetailModal'; declare var Chart: any; /** * 대시보드 렌더링 메인 함수 */ export function renderDashboard(mainContent: HTMLElement) { if (!mainContent) return; mainContent.innerHTML = ''; // 기존 차트 리소스 해제 if (state.activeCharts) { state.activeCharts.forEach(c => { if (c && typeof c.destroy === 'function') c.destroy(); }); } state.activeCharts = []; if (state.activeCategory === 'hw') { renderHwDashboard(mainContent); } else if (state.activeCategory === 'sw') { renderSwDashboard(mainContent); } else { mainContent.innerHTML = `
운영 서비스 대시보드는 준비 중입니다.
`; } } // --- 하드웨어 대시보드 --- 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]} 사용 중
${per}%
`; }); container.innerHTML = `

자산 사용현황 요약

${usageCards}

하드웨어 보유 통계

자산 유형별 보유 현황

법인별 자산 분포

`; setTimeout(() => { if (typeof Chart === 'undefined') return; const ctxType = (document.getElementById('chart-hw-types') as HTMLCanvasElement)?.getContext('2d'); const ctxCorp = (document.getElementById('chart-hw-corps') as HTMLCanvasElement)?.getContext('2d'); if (ctxType) { const chart = new Chart(ctxType, { type: 'doughnut', data: { labels: types, datasets: [{ data: types.map(t => state.masterData.hw.filter(a => a.type === t).length), backgroundColor: ['#1E5149', '#3b82f6', '#10b981', '#f59e0b'] }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'right' } } } }); state.activeCharts.push(chart); } if (ctxCorp) { const corps = ['한맥', '삼안', '바론']; const chart = new Chart(ctxCorp, { type: 'bar', data: { labels: corps, datasets: [{ label: '보유 수량', data: corps.map(c => state.masterData.hw.filter(a => a.법인 === c).length), backgroundColor: 'rgba(30, 81, 73, 0.7)', borderRadius: 4 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } } } }); state.activeCharts.push(chart); } }, 100); container.querySelectorAll('[data-action="idle"]').forEach(card => { card.addEventListener('click', () => { const t = card.getAttribute('data-type')!; openDashboardDetail(`[${t}] 유휴 자산 목록`, groups[t].idle); }); }); } // --- 소프트웨어 대시보드 --- 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.금액 ? String(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++; } 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일 이내)
${subExp}개 제품
${subExpPer}%
유지보수 만료 예정 (30일 이내)
${permExp}개 제품
${permExpPer}%

${currentYear}년 도입 비용 분석

법인별 도입 금액 (원)

분야별 도입 금액 (원)

`; setTimeout(() => { if (typeof Chart === 'undefined') return; const ctxCorp = (document.getElementById('chart-sw-corp') as HTMLCanvasElement)?.getContext('2d'); const ctxCat = (document.getElementById('chart-sw-cat') as HTMLCanvasElement)?.getContext('2d'); if (ctxCorp) { const chart = new Chart(ctxCorp, { type: 'bar', data: { labels: corps, datasets: [{ data: corps.map(c => costByCorp[c]), backgroundColor: 'rgba(30, 81, 73, 0.8)', borderRadius: 4 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } } } }); state.activeCharts.push(chart); } if (ctxCat) { const chart = new Chart(ctxCat, { type: 'bar', data: { labels: categories, datasets: [{ data: categories.map(c => costByCat[c]), backgroundColor: 'rgba(59, 130, 246, 0.8)', borderRadius: 4 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } } } }); state.activeCharts.push(chart); } }, 100); 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; try { const buyDate = new Date(a.구매일.replace(/\./g, '-')); if (isNaN(buyDate.getTime())) return 0; return (Date.now() - buyDate.getTime()) / (1000 * 60 * 60 * 24 * 365.25); } catch { return 0; } } function isSWExpiring(sw: SoftwareAsset) { if (sw.type === '구독SW' && sw.구독일) { const parts = sw.구독일.split('~'); if (parts.length > 1) { const endMs = new Date(parts[1].trim().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('유지보수: ~')) { try { const endMs = new Date(sw.비고.split('~')[1].trim().replace(/\./g, '-')).getTime(); const diffDays = (endMs - Date.now()) / (1000 * 60 * 60 * 24); return diffDays >= 0 && diffDays <= 30; } catch { return false; } } return false; }