/** * Project Master Analysis JS * P-WAR (Project Performance Above Replacement) 분석 엔진 */ // Chart.js 플러그인 전역 등록 if (typeof ChartDataLabels !== 'undefined') { Chart.register(ChartDataLabels); } document.addEventListener('DOMContentLoaded', () => { console.log("Analysis engine initialized..."); loadPWarData(); }); async function loadPWarData() { try { const response = await fetch('/api/analysis/p-war'); const data = await response.json(); if (data.error) throw new Error(data.error); renderPWarLeaderboard(data); renderSOICharts(data); if (data.length > 0 && data[0].avg_info) { const avg = data[0].avg_info; const infoEl = document.getElementById('avg-system-info'); if (infoEl) infoEl.textContent = `* 시스템 종합 건강도: ${avg.avg_risk}% (0.0%에 가까울수록 방치 심각)`; } } catch (e) { console.error("분석 데이터 로딩 실패:", e); } } function getStatusInfo(soi, isAutoDelete) { if (isAutoDelete || soi < 10) return { label: '사망', class: 'badge-system', key: 'dead' }; if (soi < 30) return { label: '위험', class: 'badge-danger', key: 'danger' }; if (soi < 70) return { label: '주의', class: 'badge-warning', key: 'warning' }; return { label: '정상', class: 'badge-active', key: 'active' }; } function renderSOICharts(data) { if (!data || data.length === 0) return; // 1. 상태 분포 (Doughnut) try { const stats = { active: [], warning: [], danger: [], dead: [] }; data.forEach(p => { const status = getStatusInfo(p.p_war, p.is_auto_delete); stats[status.key].push(p); }); const statusCtx = document.getElementById('statusChart').getContext('2d'); if (window.myStatusChart) window.myStatusChart.destroy(); window.myStatusChart = new Chart(statusCtx, { type: 'doughnut', data: { labels: ['정상', '주의', '위험', '사망'], datasets: [{ data: [stats.active.length, stats.warning.length, stats.danger.length, stats.dead.length], backgroundColor: ['#1E5149', '#22c55e', '#f59e0b', '#ef4444'], borderWidth: 0 }] }, options: { responsive: true, maintainAspectRatio: false, layout: { padding: 15 }, plugins: { legend: { position: 'right', labels: { boxWidth: 10, font: { size: 11, weight: '700' }, usePointStyle: true } }, datalabels: { display: false } }, cutout: '65%', onClick: (e, elements) => { if (elements.length > 0) { const idx = elements[0].index; openProjectListModal(['정상', '주의', '위험', '사망'][idx], stats[['active', 'warning', 'danger', 'dead'][idx]]); } } } }); } catch (err) { console.error("도넛 차트 에러:", err); } // 2. 프로젝트 SWOT 매트릭스 (Scatter) try { const scatterData = data.map(p => ({ x: Math.min(500, p.file_count), y: p.p_war, label: p.project_nm })); const vitalityCtx = document.getElementById('forecastChart').getContext('2d'); if (window.myVitalityChart) window.myVitalityChart.destroy(); // 플러그인 통합 (Duplicate Key 방지) const chartPlugins = []; if (typeof ChartDataLabels !== 'undefined') chartPlugins.push(ChartDataLabels); chartPlugins.push({ id: 'quadrants', beforeDraw: (chart) => { const { ctx, chartArea: { left, top, right, bottom }, scales: { x, y } } = chart; const midX = x.getPixelForValue(250); const midY = y.getPixelForValue(50); ctx.save(); ctx.fillStyle = 'rgba(34, 197, 94, 0.03)'; ctx.fillRect(left, top, midX - left, midY - top); ctx.fillStyle = 'rgba(30, 81, 73, 0.03)'; ctx.fillRect(midX, top, right - midX, midY - top); ctx.fillStyle = 'rgba(148, 163, 184, 0.03)'; ctx.fillRect(left, midY, midX - left, bottom - midY); ctx.fillStyle = 'rgba(239, 68, 68, 0.05)'; ctx.fillRect(midX, midY, right - midX, bottom - midY); ctx.lineWidth = 2; ctx.strokeStyle = 'rgba(0,0,0,0.1)'; ctx.beginPath(); ctx.moveTo(midX, top); ctx.lineTo(midX, bottom); ctx.moveTo(left, midY); ctx.lineTo(right, midY); ctx.stroke(); ctx.font = 'bold 12px Pretendard'; ctx.textAlign = 'center'; ctx.fillStyle = 'rgba(0,0,0,0.2)'; ctx.fillText('활동 양호', (left + midX) / 2, (top + midY) / 2); ctx.fillText('핵심 우량', (midX + right) / 2, (top + midY) / 2); ctx.fillText('방치/소규모', (left + midX) / 2, (midY + bottom) / 2); ctx.fillStyle = 'rgba(239, 68, 68, 0.4)'; ctx.fillText('관리 사각지대', (midX + right) / 2, (midY + bottom) / 2); ctx.restore(); } }); window.myVitalityChart = new Chart(vitalityCtx, { type: 'scatter', plugins: chartPlugins, data: { datasets: [{ data: scatterData, backgroundColor: (ctx) => { const p = ctx.raw; if (!p) return '#94a3b8'; if (p.x >= 250 && p.y >= 50) return '#1E5149'; if (p.x < 250 && p.y >= 50) return '#22c55e'; if (p.x < 250 && p.y < 50) return '#94a3b8'; return '#ef4444'; }, pointRadius: 6, hoverRadius: 10 }] }, options: { responsive: true, maintainAspectRatio: false, layout: { padding: { top: 30, right: 40, left: 10, bottom: 10 } }, scales: { x: { type: 'linear', min: 0, max: 500, title: { display: true, text: '자산 규모 (파일 수)', font: { size: 11, weight: '700' } }, grid: { display: false }, ticks: { stepSize: 125, callback: (v) => v >= 500 ? '500+' : v } }, y: { min: 0, max: 100, title: { display: true, text: '활동성 (SOI %)', font: { size: 11, weight: '700' } }, grid: { display: false } } }, plugins: { legend: { display: false }, datalabels: { align: 'top', offset: 5, font: { size: 10, weight: '700' }, color: '#475569', formatter: (v) => v.label, display: (ctx) => ctx.raw.x > 100 || ctx.raw.y < 30, clip: false }, tooltip: { callbacks: { label: (ctx) => ` [${ctx.raw.label}] SOI: ${ctx.raw.y.toFixed(1)}% | 파일: ${ctx.raw.x >= 500 ? '500+' : ctx.raw.x}개` } } } } }); } catch (err) { console.error("SWOT 차트 에러:", err); } } function renderPWarLeaderboard(data) { const container = document.getElementById('p-war-table-container'); if (!container) return; const sortedData = [...data].sort((a, b) => a.p_war - b.p_war); container.innerHTML = `
| 프로젝트명 | 파일 수 | 방치일 | 상태 판정 | 현재 SOI | 실무 투입 | AI 예보 (14d) |
|---|---|---|---|---|---|---|
| ${p.project_nm} | ${p.file_count.toLocaleString()}개 | ${p.days_stagnant}일 | ${status.label} | ${p.p_war.toFixed(1)}% |
${p.work_effort}%
|
${p.predicted_soi !== null ? p.predicted_soi.toFixed(1) + '%' : '-'} |
|
⚙️ AI 위험 적응형 모델(AAS) 산출 시뮬레이션
📊 실질 업무 활성화 분석 (Work Vitality)
투입률 ${p.work_effort}%
최근 30회 중 실제 파일 변동이 포착된 날의 비율입니다. 현재 ${p.work_effort >= 70 ? '매우 활발' : p.work_effort <= 30 ? '정체' : '간헐적'} 상태입니다.
1
동적 위험 계수(λ)
λ = ${p.ai_lambda.toFixed(4)}
2
활동 품질 (Quality)
Factor = ${(p.log_quality * 100).toFixed(0)}%
3
방치 시간 감쇄
Result = ${((p.p_war / (p.file_count === 0 ? 0.05 : p.file_count < 10 ? 0.4 : 1) / p.log_quality) || 0).toFixed(1)}%
4
존재 진정성 (ECV)
Factor = ${ecvText}
* 공식: AAS_Score × Quality_Factor × ECV_Factor
최종 P-SOI: ${p.p_war.toFixed(1)}%
|
||||||
데이터 없음
' : `| 프로젝트명 | 관리자 | 방치일 | 현재 SOI |
|---|---|---|---|
| ${p.project_nm} | ${p.master || '-'} | ${p.days_stagnant}일 | ${p.p_war.toFixed(1)}% |
방치일수에 따른 가치 하락 모델입니다.
'; } else { title.innerText = 'AI 시계열 예측 상세'; body.innerHTML = '활동 가속도 및 밀도를 분석하여 14일 뒤의 상태를 예보합니다.
'; } modal.style.display = 'flex'; } function closeAnalysisModal() { document.getElementById('analysisModal').style.display = 'none'; }