feat: 대시보드 및 모달 컴포넌트 최적화, 클라우드 자산 뷰 추가
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { state } from '../../core/state';
|
||||
import { SoftwareAsset } from '../../core/excelHandler';
|
||||
import { openSwDashboardDetail, openSwUsageDetail } from '../../components/Modal/DashboardDetailModal';
|
||||
import { openSwDashboardDetail, openSwUsageDetail, openCloudDashboardDetail } from '../../components/Modal/DashboardDetailModal';
|
||||
import { normalizeDate } from '../../core/utils';
|
||||
|
||||
declare var Chart: any;
|
||||
@@ -8,8 +8,13 @@ declare var Chart: any;
|
||||
export function renderSwDashboard(container: HTMLElement) {
|
||||
let subQty = 0, subUsed = 0, subExp = 0, subTotal = 0;
|
||||
let permQty = 0, permUsed = 0, permExp = 0, permTotal = 0;
|
||||
|
||||
let thisMonthCloudCost = 0;
|
||||
let lastMonthCloudCost = 0;
|
||||
let cloudExp = 0;
|
||||
|
||||
const currentYear = new Date().getFullYear().toString();
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
const corps = ['한맥', '삼안', '바론'];
|
||||
const categories = ['업무공통', '개발S/W', '디자인', '설계S/W'];
|
||||
|
||||
@@ -17,6 +22,8 @@ export function renderSwDashboard(container: HTMLElement) {
|
||||
const costByCat: Record<string, number> = {};
|
||||
categories.forEach(c => costByCat[c] = 0);
|
||||
|
||||
const today = new Date();
|
||||
|
||||
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);
|
||||
@@ -26,12 +33,28 @@ export function renderSwDashboard(container: HTMLElement) {
|
||||
if (sw.type === '구독SW') {
|
||||
subQty += qty; subUsed += assigned; subTotal++;
|
||||
if (isSWExpiring(sw)) subExp++;
|
||||
} else {
|
||||
} else if (sw.type === '영구SW') {
|
||||
permQty += qty; permUsed += assigned; permTotal++;
|
||||
if (isSWExpiring(sw)) permExp++;
|
||||
} else if (sw.type === '클라우드') {
|
||||
if (sw.당월청구액) {
|
||||
thisMonthCloudCost += parseInt(String(sw.당월청구액).replace(/,/g, ''), 10) || 0;
|
||||
}
|
||||
if (sw.결제일) {
|
||||
const payDay = parseInt(sw.결제일, 10);
|
||||
if (!isNaN(payDay)) {
|
||||
const nextPayMs = new Date(today.getFullYear(), today.getMonth(), payDay).getTime();
|
||||
let diff = (nextPayMs - today.getTime()) / (1000 * 60 * 60 * 24);
|
||||
if (diff < 0) {
|
||||
const nextMonthMs = new Date(today.getFullYear(), today.getMonth() + 1, payDay).getTime();
|
||||
diff = (nextMonthMs - today.getTime()) / (1000 * 60 * 60 * 24);
|
||||
}
|
||||
if (diff <= 14) cloudExp++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (sw.구매일 && sw.구매일.startsWith(currentYear)) {
|
||||
if (sw.구매일 && sw.구매일.startsWith(String(currentYear))) {
|
||||
if (costByCorp[sw.법인] !== undefined) costByCorp[sw.법인] += price;
|
||||
if (sw.분야 && costByCat[sw.분야] !== undefined) costByCat[sw.분야] += price;
|
||||
}
|
||||
@@ -41,10 +64,41 @@ export function renderSwDashboard(container: HTMLElement) {
|
||||
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;
|
||||
const cloudExpTotal = state.masterData.sw.filter(s => s.type === '클라우드').length;
|
||||
const cloudExpPer = cloudExpTotal > 0 ? Math.round((cloudExp/cloudExpTotal)*100) : 0;
|
||||
|
||||
// Cloud trend & Last month cost logic
|
||||
const cloudCostTrend = [0, 0, 0, 0];
|
||||
const trendLabels: string[] = [];
|
||||
for(let i=3; i>=0; i--) {
|
||||
const d = new Date();
|
||||
d.setMonth(d.getMonth() - i);
|
||||
trendLabels.push(`${d.getMonth()+1}월`);
|
||||
}
|
||||
|
||||
if (state.masterData.logs) {
|
||||
state.masterData.logs.forEach(log => {
|
||||
const match = log.details.match(/[^\d]*([\d,]+)/);
|
||||
if (match && (log.details.includes('청구액') || log.details.includes('비용'))) {
|
||||
const cost = parseInt(match[1].replace(/,/g, ''), 10);
|
||||
const logDate = new Date(log.date);
|
||||
const monthDiff = (today.getFullYear() - logDate.getFullYear())*12 + (today.getMonth() - logDate.getMonth());
|
||||
|
||||
if (monthDiff === 1) lastMonthCloudCost += cost;
|
||||
if (monthDiff >= 0 && monthDiff < 4) cloudCostTrend[3 - monthDiff] += cost;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const costDiff = thisMonthCloudCost - lastMonthCloudCost;
|
||||
const costDiffText = lastMonthCloudCost > 0
|
||||
? `${costDiff >= 0 ? '+' : ''}${((costDiff/lastMonthCloudCost)*100).toFixed(1)}%`
|
||||
: 'New';
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="view-container">
|
||||
<h3 class="dashboard-section-title">소프트웨어 라이선스 현황</h3>
|
||||
|
||||
<div class="dashboard-layout-2col" style="margin-bottom: 1.5rem;">
|
||||
<div class="dashboard-card" data-action="sub-usage" style="cursor:pointer; min-height:auto;">
|
||||
<span style="font-size:1rem; font-weight:700; color:var(--text-main);">구독 소프트웨어 사용율</span>
|
||||
@@ -67,46 +121,83 @@ export function renderSwDashboard(container: HTMLElement) {
|
||||
<div class="dashboard-layout-2col" style="margin-bottom: 1.5rem;">
|
||||
<div class="dashboard-card" data-action="sub-exp" style="flex-direction:row; justify-content:space-between; align-items:center; cursor:pointer; min-height:auto;">
|
||||
<div style="flex:1;">
|
||||
<span style="font-size:1rem; font-weight:700; color:var(--text-main);">구독 SW 만료 예정 (30일 이내)</span>
|
||||
<span style="font-size:1rem; font-weight:700; color:var(--text-main);">구독 SW 만료 예정<br><span style="font-size:0.8rem;font-weight:400;color:var(--text-muted);">(30일 이내)</span></span>
|
||||
<div style="font-size: 1.5rem; font-weight:700; color:${subExp > 0 ? 'var(--dash-danger)' : 'var(--text-main)'}; margin-top:0.5rem;">${subExp}개 제품</div>
|
||||
</div>
|
||||
<div style="width: 60px; height: 60px; border-radius: 50%; background: conic-gradient(var(--dash-danger) ${subExpPer}%, var(--border-color) 0); display:flex; justify-content:center; align-items:center;">
|
||||
<div style="width: 48px; height: 48px; border-radius: 50%; background: var(--white); display:flex; justify-content:center; align-items:center;">
|
||||
<span style="font-size: 0.875rem; color:var(--text-muted); font-weight:600;">${subExpPer}%</span>
|
||||
<div style="width: 50px; height: 50px; border-radius: 50%; background: conic-gradient(var(--dash-danger) ${subExpPer}%, var(--border-color) 0); display:flex; justify-content:center; align-items:center;">
|
||||
<div style="width: 40px; height: 40px; border-radius: 50%; background: var(--white); display:flex; justify-content:center; align-items:center;">
|
||||
<span style="font-size: 0.75rem; color:var(--text-muted); font-weight:600;">${subExpPer}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-card" data-action="perm-exp" style="flex-direction:row; justify-content:space-between; align-items:center; cursor:pointer; min-height:auto;">
|
||||
<div style="flex:1;">
|
||||
<span style="font-size:1rem; font-weight:700; color:var(--text-main);">유지보수 만료 예정 (30일 이내)</span>
|
||||
<span style="font-size:1rem; font-weight:700; color:var(--text-main);">유지보수 만료 예정<br><span style="font-size:0.8rem;font-weight:400;color:var(--text-muted);">(30일 이내)</span></span>
|
||||
<div style="font-size: 1.5rem; font-weight:700; color:${permExp > 0 ? 'var(--dash-danger)' : 'var(--text-main)'}; margin-top:0.5rem;">${permExp}개 제품</div>
|
||||
</div>
|
||||
<div style="width: 60px; height: 60px; border-radius: 50%; background: conic-gradient(var(--dash-danger) ${permExpPer}%, var(--border-color) 0); display:flex; justify-content:center; align-items:center;">
|
||||
<div style="width: 48px; height: 48px; border-radius: 50%; background: var(--white); display:flex; justify-content:center; align-items:center;">
|
||||
<span style="font-size: 0.875rem; color:var(--text-muted); font-weight:600;">${permExpPer}%</span>
|
||||
<div style="width: 50px; height: 50px; border-radius: 50%; background: conic-gradient(var(--dash-danger) ${permExpPer}%, var(--border-color) 0); display:flex; justify-content:center; align-items:center;">
|
||||
<div style="width: 40px; height: 40px; border-radius: 50%; background: var(--white); display:flex; justify-content:center; align-items:center;">
|
||||
<span style="font-size: 0.75rem; color:var(--text-muted); font-weight:600;">${permExpPer}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
<!-- 클라우드 전월/당월 통합 및 결제 임박 (1:1 비율) -->
|
||||
<div class="dashboard-layout-2col" style="margin-bottom: 2.5rem;">
|
||||
<div class="dashboard-card" data-action="cloud-billing" style="flex-direction:row; cursor:pointer; min-height:auto; align-items:center; gap: 1.5rem;">
|
||||
<div style="flex:1;">
|
||||
<span style="font-size:1rem; font-weight:700; color:var(--text-main);">☁️ 클라우드 청구 현황 (통합)</span>
|
||||
<div style="margin-top: 0.5rem; display:flex; align-items:baseline; gap: 0.75rem;">
|
||||
<span style="font-size: 2rem; font-weight:800; color:var(--primary-color);">₩ ${thisMonthCloudCost.toLocaleString()}</span>
|
||||
<span style="font-size: 0.85rem; font-weight:600; color:${costDiff >= 0 ? 'var(--dash-danger)' : 'var(--dash-primary)'};">
|
||||
${costDiff >= 0 ? '▲' : '▼'} ${costDiffText}
|
||||
</span>
|
||||
</div>
|
||||
<div style="font-size: 0.8125rem; color:var(--text-muted); margin-top:0.25rem;">전월 실적: ₩ ${lastMonthCloudCost.toLocaleString()}</div>
|
||||
</div>
|
||||
<div style="width: 50px; height: 50px; border-radius: 50%; background: conic-gradient(var(--primary-color) 100%, var(--border-color) 0); display:flex; justify-content:center; align-items:center; opacity:0.8;">
|
||||
<div style="width: 40px; height: 40px; border-radius: 50%; background: var(--white); display:flex; justify-content:center; align-items:center;">
|
||||
<span style="font-size: 0.75rem; color:var(--text-muted); font-weight:600;">₩</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-card" data-action="cloud-exp" style="flex-direction:row; justify-content:space-between; align-items:center; cursor:pointer; min-height:auto;">
|
||||
<div style="flex:1;">
|
||||
<span style="font-size:1rem; font-weight:700; color:var(--text-main);">클라우드 결제 임박<br><span style="font-size:0.8rem;font-weight:400;color:var(--text-muted);">(14일 이내)</span></span>
|
||||
<div style="font-size: 1.5rem; font-weight:700; color:${cloudExp > 0 ? 'var(--dash-danger)' : 'var(--text-main)'}; margin-top:0.5rem;">${cloudExp}건 청구</div>
|
||||
</div>
|
||||
<div style="width: 50px; height: 50px; border-radius: 50%; background: conic-gradient(var(--dash-danger) ${cloudExpPer}%, var(--border-color) 0); display:flex; justify-content:center; align-items:center;">
|
||||
<div style="width: 40px; height: 40px; border-radius: 50%; background: var(--white); display:flex; justify-content:center; align-items:center;">
|
||||
<span style="font-size: 0.75rem; color:var(--text-muted); font-weight:600;">${cloudExpPer}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="dashboard-section-title">비용 분석 현황</h3>
|
||||
<div style="display:grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; margin-bottom: 2rem;">
|
||||
<div class="dashboard-card" style="grid-column: span 1;">
|
||||
<h4 style="margin-bottom:1rem; font-size:0.9rem; color:var(--text-muted);">${currentYear}년 법인별 도입 금액 (원)</h4>
|
||||
<canvas id="chart-sw-corp"></canvas>
|
||||
</div>
|
||||
<div class="dashboard-card">
|
||||
<h4 style="margin-bottom:1rem; font-size:0.9rem; color:var(--text-muted);">분야별 도입 금액 (원)</h4>
|
||||
<div class="dashboard-card" style="grid-column: span 1;">
|
||||
<h4 style="margin-bottom:1rem; font-size:0.9rem; color:var(--text-muted);">${currentYear}년 분야별 도입 금액 (원)</h4>
|
||||
<canvas id="chart-sw-cat"></canvas>
|
||||
</div>
|
||||
<div class="dashboard-card" style="grid-column: span 1;">
|
||||
<h4 style="margin-bottom:1rem; font-size:0.9rem; color:var(--text-muted);">직전 4개월 클라우드 결제 추이 (원)</h4>
|
||||
<canvas id="chart-cloud-trend"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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',
|
||||
@@ -115,6 +206,33 @@ export function renderSwDashboard(container: HTMLElement) {
|
||||
});
|
||||
state.activeCharts.push(chart);
|
||||
}
|
||||
|
||||
const ctxTrend = (document.getElementById('chart-cloud-trend') as HTMLCanvasElement)?.getContext('2d');
|
||||
if (ctxTrend) {
|
||||
const chart = new Chart(ctxTrend, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: trendLabels,
|
||||
datasets: [{
|
||||
data: cloudCostTrend,
|
||||
borderColor: '#6366f1',
|
||||
backgroundColor: 'rgba(99, 102, 241, 0.05)',
|
||||
borderWidth: 2,
|
||||
fill: true,
|
||||
tension: 0.4
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: { legend: { display: false } },
|
||||
scales: { y: { beginAtZero: true } }
|
||||
}
|
||||
});
|
||||
state.activeCharts.push(chart);
|
||||
}
|
||||
|
||||
const ctxCat = (document.getElementById('chart-sw-cat') as HTMLCanvasElement)?.getContext('2d');
|
||||
if (ctxCat) {
|
||||
const chart = new Chart(ctxCat, {
|
||||
type: 'bar',
|
||||
@@ -129,6 +247,23 @@ export function renderSwDashboard(container: HTMLElement) {
|
||||
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))));
|
||||
container.querySelector('[data-action="cloud-billing"]')?.addEventListener('click', () => openCloudDashboardDetail('클라우드 청구 현황 (전체)', state.masterData.sw.filter(sw => sw.type === '클라우드')));
|
||||
|
||||
container.querySelector('[data-action="cloud-exp"]')?.addEventListener('click', () => {
|
||||
const expiringClouds = state.masterData.sw.filter(sw => {
|
||||
if (sw.type !== '클라우드' || !sw.결제일) return false;
|
||||
const payDay = parseInt(sw.결제일, 10);
|
||||
if (isNaN(payDay)) return false;
|
||||
const nextPayMs = new Date(today.getFullYear(), today.getMonth(), payDay).getTime();
|
||||
let diff = (nextPayMs - today.getTime()) / (1000 * 60 * 60 * 24);
|
||||
if (diff < 0) {
|
||||
const nextMonthMs = new Date(today.getFullYear(), today.getMonth() + 1, payDay).getTime();
|
||||
diff = (nextMonthMs - today.getTime()) / (1000 * 60 * 60 * 24);
|
||||
}
|
||||
return diff <= 14;
|
||||
});
|
||||
openCloudDashboardDetail('결제 임박 클라우드 목록 (14일 이내)', expiringClouds);
|
||||
});
|
||||
}
|
||||
|
||||
function isSWExpiring(sw: SoftwareAsset) {
|
||||
|
||||
Reference in New Issue
Block a user