diff --git a/src/dummyDataGenerator.ts b/src/dummyDataGenerator.ts index 5b48c9a..cfbf212 100644 --- a/src/dummyDataGenerator.ts +++ b/src/dummyDataGenerator.ts @@ -26,7 +26,7 @@ export function generateDummyData(): MasterAssetData { // 1. 개인PC 50개 for (let i = 1; i <= 50; i++) { - const purchaseYear = Math.floor(Math.random() * 8) + 2017; // 2017~2024 + const purchaseYear = Math.floor(Math.random() * 10) + 2017; // 2017~2026 hw.push({ id: Math.random().toString(36).substring(2, 9), type: '개인PC', @@ -43,7 +43,7 @@ export function generateDummyData(): MasterAssetData { HDD1: rand(['-', '1TB', '2TB']), HDD2: '', 구매일: randDate(purchaseYear, purchaseYear), - 금액: String(Math.floor(Math.random()*100 + 50) * 10000).replace(/\\B(?=(\\d{3})+(?!\\d))/g, ','), + 금액: String(Math.floor(Math.random()*100 + 50) * 10000).replace(/\B(?=(\d{3})+(?!\d))/g, ','), 납품업체: rand(['다나와', '컴퓨존', '오피스디포']), 품의서명: '', 관리자: '', IP주소: '', MACaddress: '', OS: '', HW사양: '' @@ -52,7 +52,7 @@ export function generateDummyData(): MasterAssetData { // 2. 서버 20개 for (let i = 1; i <= 20; i++) { - const purchaseYear = Math.floor(Math.random() * 8) + 2017; // 2017~2024 + const purchaseYear = Math.floor(Math.random() * 10) + 2017; // 2017~2026 hw.push({ id: Math.random().toString(36).substring(2, 9), type: '서버', @@ -74,7 +74,7 @@ export function generateDummyData(): MasterAssetData { // 3. 스토리지 20개 for (let i = 1; i <= 20; i++) { - const purchaseYear = Math.floor(Math.random() * 8) + 2017; // 2017~2024 + const purchaseYear = Math.floor(Math.random() * 10) + 2017; // 2017~2026 hw.push({ id: Math.random().toString(36).substring(2, 9), type: '스토리지', @@ -105,7 +105,7 @@ export function generateDummyData(): MasterAssetData { ]; equips.forEach((eq) => { for (let i = 1; i <= 5; i++) { - const purchaseYear = Math.floor(Math.random() * 6) + 2019; // 2019~2024 + const purchaseYear = Math.floor(Math.random() * 8) + 2019; // 2019~2026 hw.push({ id: Math.random().toString(36).substring(2, 9), type: '전산비품', @@ -127,6 +127,7 @@ export function generateDummyData(): MasterAssetData { // 5. 구독형 S/W 40개 for (let i = 1; i <= 40; i++) { const swId = Math.random().toString(36).substring(2, 9); + const purchaseYear = Math.random() < 0.3 ? 2026 : 2024; let isExpiring = Math.random() < 0.25; let endDt = new Date(); @@ -144,14 +145,15 @@ export function generateDummyData(): MasterAssetData { 법인: rand(corps), 부서: rand(depts), 제품명: rand(['Adobe CC All Apps', 'Microsoft 365', 'Slack Pro', 'Notion Team']), - 구매일: '2024-01-01', - 구독일: `2024.01.01 ~ ${endStr}`, - 금액: '600,000', + 구매일: `${purchaseYear}-01-01`, + 구독일: `${purchaseYear}.01.01 ~ ${endStr}`, + 금액: String(Math.floor(Math.random() * 100 + 10) * 10000).replace(/\B(?=(\d{3})+(?!\d))/g, ','), 수량: Math.floor(Math.random() * 5) + 3, // 3~7 계정명: `user${i}@hm.com`, 납품업체: '총판', 비고: '연간구독' }); + // ... rest unchanged const assignCount = Math.floor(Math.random() * 2) + 1; for (let j=0; jNo.분야법인부서제품명구매일${isSub ? '구독일' : ''}수량사용가능관리`; + table.innerHTML = `No.분야법인부서제품명구매일${isSub ? '구독일' : ''}금액수량사용가능관리`; container.appendChild(table); mainContent.appendChild(container); const tbody = document.getElementById('dynamic-tbody')!; - if (list.length === 0) { tbody.innerHTML = `정보가 없습니다.`; return; } + if (list.length === 0) { tbody.innerHTML = `정보가 없습니다.`; return; } list.forEach((asset, idx) => { const assigned = state.masterData.swUsers.filter(u => u.swId === asset.id).length; const avail = (typeof asset.수량 === 'number' ? asset.수량 : parseInt(asset.수량||'0', 10)) - assigned; const tr = document.createElement('tr'); tr.style.cursor = 'pointer'; - tr.innerHTML = `${idx+1}${asset.분야||''}${asset.법인}${asset.부서||''}${asset.제품명}${asset.구매일||''}${isSub ? `${asset.구독일||''}` : ''}${asset.수량}${avail}`; + tr.innerHTML = `${idx+1}${asset.분야||''}${asset.법인}${asset.부서||''}${asset.제품명}${asset.구매일||''}${isSub ? `${asset.구독일||''}` : ''}${asset.금액||'0'}${asset.수량}${avail}`; tr.addEventListener('click', (e) => { if (!(e.target as HTMLElement).closest('button')) openSwModal(asset); }); tr.querySelector('.btn-edit')?.addEventListener('click', () => openSwModal(asset)); tr.querySelector('.btn-users')?.addEventListener('click', () => openSwUserModal(asset)); diff --git a/src/views/DashboardView.ts b/src/views/DashboardView.ts index ff72541..82c1759 100644 --- a/src/views/DashboardView.ts +++ b/src/views/DashboardView.ts @@ -1,6 +1,8 @@ import { state } from '../state'; import { HardwareAsset, SoftwareAsset } from '../excelHandler'; +declare var Chart: any; + /** * 대시보드 렌더링 메인 함수 */ @@ -8,7 +10,9 @@ export function renderDashboard(mainContent: HTMLElement) { mainContent.innerHTML = ''; // 기존 차트 리소스 해제 - state.activeCharts.forEach(c => c.destroy()); + state.activeCharts.forEach(c => { + if (c && typeof c.destroy === 'function') c.destroy(); + }); state.activeCharts = []; if (state.activeCategory === 'hw') { @@ -120,9 +124,20 @@ 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++; @@ -130,6 +145,12 @@ function renderSwDashboard(container: HTMLElement) { 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; @@ -160,7 +181,8 @@ function renderSwDashboard(container: HTMLElement) { -
+ +
@@ -196,8 +218,90 @@ function renderSwDashboard(container: HTMLElement) {
+ +

${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')); @@ -305,3 +409,5 @@ function openSwUsageDetail(title: string, list: SoftwareAsset[]) { }); modal.classList.remove('hidden'); } + +