feat: 소프트웨어 자산 관리 기능 고도화 및 대시보드 누적 비용 분석 기능 추가
This commit is contained in:
@@ -9,6 +9,10 @@ export function renderSwDashboard(container: HTMLElement) {
|
||||
let subQty = 0, subUsed = 0, subExp = 0, subTotal = 0;
|
||||
let permQty = 0, permUsed = 0, permExp = 0, permTotal = 0;
|
||||
|
||||
let subCost2026 = 0;
|
||||
let permCost2026 = 0;
|
||||
let cloudCost2026 = 0;
|
||||
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
const corps = ['한맥', '삼안', '바론'];
|
||||
@@ -19,7 +23,7 @@ export function renderSwDashboard(container: HTMLElement) {
|
||||
categories.forEach(c => costByCat[c] = 0);
|
||||
|
||||
// 통합 SW 데이터
|
||||
const allSw = [...state.masterData.subSw, ...state.masterData.permSw];
|
||||
const allSw = [...state.masterData.subSw, ...state.masterData.permSw, ...state.masterData.cloud];
|
||||
|
||||
allSw.forEach(sw => {
|
||||
const userMapping = state.masterData.swUsers.find(u => u.sw_id === sw.id);
|
||||
@@ -36,12 +40,35 @@ export function renderSwDashboard(container: HTMLElement) {
|
||||
if (isSWExpiring(sw)) permExp++;
|
||||
}
|
||||
|
||||
if (sw.구매일 && sw.구매일.startsWith(String(currentYear))) {
|
||||
// 초기 도입 비용 (2026년 구매건)
|
||||
if (sw.구매일 && sw.구매일.startsWith('2026')) {
|
||||
if (sw.type === '구독SW') subCost2026 += price;
|
||||
else if (sw.type === '영구SW') permCost2026 += price;
|
||||
else if (sw.type === '클라우드') cloudCost2026 += price;
|
||||
|
||||
if (costByCorp[sw.법인] !== undefined) costByCorp[sw.법인] += price;
|
||||
if (sw.분야 && costByCat[sw.분야] !== undefined) costByCat[sw.분야] += price;
|
||||
}
|
||||
});
|
||||
|
||||
// 누적 추가 비용 집계 (2026년 계약 업데이트 로그 기반)
|
||||
if (state.masterData.logs) {
|
||||
state.masterData.logs.forEach(log => {
|
||||
if (log.date && log.date.startsWith('2026') && log.cost) {
|
||||
const asset = allSw.find(a => a.id === log.assetId);
|
||||
if (asset) {
|
||||
const cost = Number(log.cost) || 0;
|
||||
if (asset.type === '구독SW') subCost2026 += cost;
|
||||
else if (asset.type === '영구SW') permCost2026 += cost;
|
||||
else if (asset.type === '클라우드') cloudCost2026 += cost;
|
||||
|
||||
if (costByCorp[asset.법인] !== undefined) costByCorp[asset.법인] += cost;
|
||||
if (asset.분야 && costByCat[asset.분야] !== undefined) costByCat[asset.분야] += cost;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -95,42 +122,32 @@ export function renderSwDashboard(container: HTMLElement) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="dashboard-section-title">${currentYear}년 도입 비용 분석</h3>
|
||||
<div class="dashboard-layout-2col">
|
||||
<div class="dashboard-card">
|
||||
<h4 style="margin-bottom:1rem; font-size:0.9rem; color:var(--text-muted);">구매법인별 도입 금액 (원)</h4>
|
||||
<canvas id="chart-sw-corp"></canvas>
|
||||
<h3 class="dashboard-section-title">2026년 누적 도입 비용 분석</h3>
|
||||
|
||||
<div style="display:grid; grid-template-columns: repeat(3, 1fr); gap:1.5rem; margin-bottom:1.5rem;">
|
||||
<div class="dashboard-card" style="min-height:auto;">
|
||||
<span style="font-size:1rem; font-weight:700; color:var(--text-main);">구독 SW 누적 비용 (2026)</span>
|
||||
<div style="font-size: 0.8125rem; color:var(--text-muted); margin-bottom: 1rem;">갱신 및 추가 비용 합계</div>
|
||||
<div style="font-size: 2rem; font-weight:700; color:var(--dash-primary);">₩ ${subCost2026.toLocaleString()}</div>
|
||||
<div style="width: 100%; height: 4px; background-color: var(--primary-color); border-radius: 2px; margin-top: 0.5rem;"></div>
|
||||
</div>
|
||||
<div class="dashboard-card">
|
||||
<h4 style="margin-bottom:1rem; font-size:0.9rem; color:var(--text-muted);">분야별 도입 금액 (원)</h4>
|
||||
<canvas id="chart-sw-cat"></canvas>
|
||||
<div class="dashboard-card" style="min-height:auto;">
|
||||
<span style="font-size:1rem; font-weight:700; color:var(--text-main);">영구 SW 누적 비용 (2026)</span>
|
||||
<div style="font-size: 0.8125rem; color:var(--text-muted); margin-bottom: 1rem;">유지보수 및 신규 도입 합계</div>
|
||||
<div style="font-size: 2rem; font-weight:700; color:#3b82f6;">₩ ${permCost2026.toLocaleString()}</div>
|
||||
<div style="width: 100%; height: 4px; background-color: #3b82f6; border-radius: 2px; margin-top: 0.5rem;"></div>
|
||||
</div>
|
||||
<div class="dashboard-card" style="min-height:auto;">
|
||||
<span style="font-size:1rem; font-weight:700; color:var(--text-main);">클라우드 누적 비용 (2026)</span>
|
||||
<div style="font-size: 0.8125rem; color:var(--text-muted); margin-bottom: 1rem;">월별 청구액 누적 합계</div>
|
||||
<div style="font-size: 2rem; font-weight:700; color:#f59e0b;">₩ ${cloudCost2026.toLocaleString()}</div>
|
||||
<div style="width: 100%; height: 4px; background-color: #f59e0b; border-radius: 2px; margin-top: 0.5rem;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
`;
|
||||
|
||||
setTimeout(() => {
|
||||
if (typeof Chart === 'undefined') return;
|
||||
|
||||
const ctxCorp = (document.getElementById('chart-sw-corp') as HTMLCanvasElement)?.getContext('2d');
|
||||
if (ctxCorp) {
|
||||
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 } } }
|
||||
});
|
||||
}
|
||||
|
||||
const ctxCat = (document.getElementById('chart-sw-cat') as HTMLCanvasElement)?.getContext('2d');
|
||||
if (ctxCat) {
|
||||
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 } } }
|
||||
});
|
||||
}
|
||||
}, 100);
|
||||
|
||||
container.querySelector('[data-action="sub-usage"]')?.addEventListener('click', () => openSwUsageDetail('구독 소프트웨어 사용 목록', state.masterData.subSw));
|
||||
container.querySelector('[data-action="perm-usage"]')?.addEventListener('click', () => openSwUsageDetail('영구 소프트웨어 사용 목록', state.masterData.permSw));
|
||||
container.querySelector('[data-action="sub-exp"]')?.addEventListener('click', () => openSwDashboardDetail('구독 SW 만료 예정 목록', state.masterData.subSw.filter(sw => isSWExpiring(sw))));
|
||||
|
||||
Reference in New Issue
Block a user