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]}
`;
});
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}개 사용 중
영구 소프트웨어 사용정보
${permQty}개의 제품 중 ${permUsed}개 사용 중
구독 SW 만료 예정
30일 이내
전체 ${subTotal}개 제품 중 ${subExp}개 만료 예정
${subExp}개
유지보수 만료 예정
30일 이내
전체 ${permTotal}개 제품 중 ${permExp}개 만료 예정
${permExp}개
${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');
}