/** * Project Master Analysis JS * P-WAR (Project Performance Above Replacement) 분석 엔진 */ 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; document.getElementById('avg-system-info').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' }; } else if (soi < 30) { return { label: '위험', class: 'badge-danger', key: 'danger' }; } else if (soi < 70) { return { label: '주의', class: 'badge-warning', key: 'warning' }; } else { return { label: '정상', class: 'badge-active', key: 'active' }; } } // Chart.js 시각화 엔진 function renderSOICharts(data) { if (!data || data.length === 0) return; // --- 1. 상태 분포 데이터 가공 (Doughnut Chart) --- 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, plugins: { legend: { position: 'right', labels: { boxWidth: 10, font: { size: 11, weight: '700' }, usePointStyle: true } }, datalabels: { display: false } }, cutout: '65%', onClick: (event, elements) => { if (elements.length > 0) { const index = elements[0].index; const keys = ['active', 'warning', 'danger', 'dead']; const labels = ['정상', '주의', '위험', '사망']; openProjectListModal(labels[index], stats[keys[index]]); } } } }); } catch (err) { console.error("도넛 차트 생성 실패:", err); } // --- 2. 프로젝트 SWOT 매트릭스 진단 (Scatter Chart) --- try { const scatterData = data.map(p => ({ x: Math.min(500, p.file_count), // 최대 500으로 조정 y: p.p_war, label: p.project_nm })); const vitalityCtx = document.getElementById('forecastChart').getContext('2d'); if (window.myVitalityChart) window.myVitalityChart.destroy(); const plugins = []; if (typeof ChartDataLabels !== 'undefined') plugins.push(ChartDataLabels); window.myVitalityChart = new Chart(vitalityCtx, { type: 'scatter', plugins: plugins, data: { datasets: [{ data: scatterData, backgroundColor: (context) => { const p = context.raw; if (!p) return '#94a3b8'; if (p.x >= 250 && p.y >= 50) return '#1E5149'; // 핵심 우량 (기준 250) 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: (val) => val >= 500 ? '500+' : val.toLocaleString() } }, y: { min: 0, max: 100, title: { display: true, text: '활동성 (SOI %)', font: { size: 11, weight: '700' } }, grid: { display: false }, ticks: { stepSize: 25 } } }, plugins: { legend: { display: false }, datalabels: { align: 'top', offset: 5, font: { size: 10, weight: '700' }, color: '#475569', formatter: (value) => value.label, display: (context) => context.raw.x > 100 || context.raw.y < 30, clip: false }, tooltip: { callbacks: { label: (context) => ` [${context.raw.label}] SOI: ${context.raw.y.toFixed(1)}% | 파일: ${context.raw.x >= 500 ? '500+' : context.raw.x}개` } } } }, plugins: [{ id: 'quadrants', beforeDraw: (chart) => { const { ctx, chartArea: { left, top, right, bottom }, scales: { x, y } } = chart; const midX = x.getPixelForValue(250); // 중앙축을 250으로 변경 const midY = y.getPixelForValue(50); ctx.save(); // 1. 물리적으로 동일한 크기의 배경색 채우기 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); // 하우 // 2. 명확한 십자 구분선 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(); // 3. 영역 텍스트 ctx.font = 'bold 12px Pretendard'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; 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(); } }] }); } 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 = `
${sortedData.map((p, idx) => { const status = getStatusInfo(p.p_war, p.is_auto_delete); const soi = p.p_war; const pred = p.predicted_soi; const rowId = `project-${idx}`; let trendIcon = ""; if (pred !== null) { const diff = pred - soi; if (diff < -5) trendIcon = '▼ 급락'; else if (diff < 0) trendIcon = '↘ 하락'; else trendIcon = '↗ 유지'; } // 수식 상세 데이터 준비 const baseLambda = 0.04; const scaleImpact = Math.min(0.04, Math.log10(p.file_count + 1) * 0.008); const envImpact = Math.max(0, p.ai_lambda - baseLambda - scaleImpact); // 존재 신뢰도 패널티 (ECV) let ecvText = "100% (신뢰)"; let ecvClass = "highlight-val"; if (p.file_count === 0) { ecvText = "5% (유령 프로젝트 패널티)"; ecvClass = "highlight-penalty"; } else if (p.file_count < 10) { ecvText = "40% (소규모 껍데기 패널티)"; ecvClass = "highlight-penalty"; } return ` `; }).join('')}
프로젝트명 파일 수 방치일 상태 판정 현재 SOI AI 예보 (14d)
${p.project_nm} ${p.file_count.toLocaleString()}개 ${p.days_stagnant}일 ${status.label} ${soi.toFixed(1)}%
${pred !== null ? pred.toFixed(1) + '%' : '-'} ${trendIcon}
⚙️ AI 위험 적응형 모델(AAS) 산출 시뮬레이션
1
동적 위험 계수(λ) 산출
기본 감쇄율에 자산 규모와 부서 위험도를 합산합니다.
λ = ${baseLambda} (Base) + ${scaleImpact.toFixed(4)} (Scale) + ${envImpact.toFixed(4)} (Env) = ${p.ai_lambda.toFixed(4)}
2
방치 시간 감쇄 적용
마지막 로그 이후 경과된 시간만큼 가치를 하락시킵니다.
AAS_Score = exp(-${p.ai_lambda.toFixed(4)} × ${p.days_stagnant}일) × 100 = ${((soi / (p.file_count === 0 ? 0.05 : p.file_count < 10 ? 0.4 : 1)) || 0).toFixed(1)}%
3
존재 진정성 검증 (ECV Penalty)
파일 수 기반의 활동 신뢰도를 적용하여 유령 활동을 차단합니다.
Final_SOI = AAS_Score × ${ecvText} = ${soi.toFixed(1)}%
`; } /** * 테이블 행 클릭 시 상세 아코디언 토글 및 스크롤 제어 */ function toggleProjectDetail(rowId) { const container = document.querySelector('.table-scroll-wrapper'); const mainRow = document.querySelector(`tr[onclick*="toggleProjectDetail('${rowId}')"]`); const detailRow = document.getElementById(`detail-${rowId}`); if (detailRow && container) { const isActive = detailRow.classList.contains('active'); if (!isActive) { // 다른 열려있는 상세 행 닫기 (맥락 유지를 위해 권장) document.querySelectorAll('.detail-row').forEach(row => row.classList.remove('active')); detailRow.classList.add('active'); // 컨테이너 내부 스크롤 위치 계산 setTimeout(() => { const headerHeight = container.querySelector('thead').offsetHeight || 40; const rowTop = mainRow.offsetTop; // 컨테이너를 정확한 위치로 스크롤 (행이 헤더 바로 밑에 오도록) container.scrollTo({ top: rowTop - headerHeight, behavior: 'smooth' }); }, 50); } else { detailRow.classList.remove('active'); } } } /** * 차트 클릭 시 프로젝트 목록 모달 표시 */ function openProjectListModal(statusLabel, projects) { const modal = document.getElementById('analysisModal'); const title = document.getElementById('modalTitle'); const body = document.getElementById('modalBody'); title.innerText = `[${statusLabel}] 상태 프로젝트 목록 (${projects.length}건)`; if (projects.length === 0) { body.innerHTML = '

해당 조건의 프로젝트가 없습니다.

'; } else { body.innerHTML = `
${projects.sort((a,b) => a.p_war - b.p_war).map(p => ` `).join('')}
프로젝트명 관리자 방치일 현재 SOI
${p.project_nm} ${p.master || '-'} ${p.days_stagnant}일 ${p.p_war.toFixed(1)}%
`; } modal.style.display = 'flex'; } /** * 분석 상세 설명 모달 제어 */ function openAnalysisModal(type) { const modal = document.getElementById('analysisModal'); const title = document.getElementById('modalTitle'); const body = document.getElementById('modalBody'); if (type === 'soi') { title.innerText = 'P-SOI (관리 지수) 산출 공식 상세'; body.innerHTML = `
기본 산술 수식
SOI = exp(-0.05 × days) × 100

본 지수는 프로젝트의 '절대적 가치 보존율'을 측정합니다.

`; } else if (type === 'ai') { title.innerText = 'AI 시계열 예측 알고리즘 상세'; body.innerHTML = `
하이브리드 추세 엔진
Pred = (Linear × w1) + (Decay × w2)

딥러닝 엔진이 프로젝트의 '활동 가속도'를 분석하여 14일 뒤의 상태를 예보합니다.

`; } modal.style.display = 'flex'; } function closeAnalysisModal(e) { document.getElementById('analysisModal').style.display = 'none'; }