Files
test-mcp/js/analysis.js

123 lines
5.4 KiB
JavaScript

/**
* 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);
updateSummaryMetrics(data);
renderPWarLeaderboard(data);
renderRiskSignals(data);
// 시스템 평균 정보 표시
if (data.length > 0 && data[0].avg_info) {
const avg = data[0].avg_info;
document.getElementById('avg-system-info').textContent =
`* 0.0 = 시스템 평균 (파일 ${avg.avg_files.toLocaleString()}개 / 방치 ${avg.avg_stagnant}일 / 리스크 ${avg.avg_risk}건)`;
}
} catch (e) {
console.error("분석 데이터 로딩 실패:", e);
}
}
function updateSummaryMetrics(data) {
// 1. 평균 P-WAR 산출
const avgPWar = data.reduce((acc, cur) => acc + cur.p_war, 0) / data.length;
document.querySelector('.metric-card.sra .value').textContent = avgPWar.toFixed(2);
// 2. 고위험 좀비 프로젝트 비율 (P-WAR < -1.0 기준)
const zombieCount = data.filter(p => p.p_war < -1.0).length;
const zombieRate = (zombieCount / data.length) * 100;
document.querySelector('.metric-card.stability .value').textContent = `${zombieRate.toFixed(1)}%`;
// 3. 총 활성 리소스 규모
const totalActiveFiles = data.filter(p => p.p_war > 0).reduce((acc, cur) => acc + cur.file_count, 0);
document.querySelector('.metric-card.piso .value').textContent = (totalActiveFiles / 1000).toFixed(1) + "k";
// 4. 방치 리스크 총합
const totalRisks = data.reduce((acc, cur) => acc + cur.risk_count, 0);
document.querySelector('.metric-card.iwar .value').textContent = totalRisks;
}
function renderPWarLeaderboard(data) {
const container = document.querySelector('.timeline-analysis .card-body');
const sortedData = [...data].sort((a, b) => b.p_war - a.p_war);
container.innerHTML = `
<div class="table-scroll-wrapper">
<table class="data-table p-war-table">
<thead>
<tr>
<th style="position: sticky; top: 0; z-index: 10; width: 250px;">프로젝트명</th>
<th style="position: sticky; top: 0; z-index: 10; width: 140px;">관리 상태</th>
<th style="position: sticky; top: 0; z-index: 10;">파일 수</th>
<th style="position: sticky; top: 0; z-index: 10;">방치일</th>
<th style="position: sticky; top: 0; z-index: 10;">미결리스크</th>
<th style="position: sticky; top: 0; z-index: 10;">P-WAR (기여도)</th>
</tr>
</thead>
<tbody>
${sortedData.map(p => {
let statusBadge = "";
if (p.is_auto_delete) {
statusBadge = '<span class="badge-system">잠김예정 프로젝트</span>';
} else if (p.p_war > 0) {
statusBadge = '<span class="badge-active">운영 중</span>';
} else if (p.p_war <= -0.3) {
statusBadge = '<span class="badge-danger">방치-삭제대상</span>';
} else {
statusBadge = '<span class="badge-warning">위험군</span>';
}
return `
<tr class="${p.is_auto_delete || p.p_war <= -0.3 ? 'row-danger' : p.p_war < 0 ? 'row-warning' : ''}">
<td class="font-bold">${p.project_nm}</td>
<td>${statusBadge}</td>
<td>${p.file_count.toLocaleString()}</td>
<td>${p.days_stagnant}</td>
<td>${p.risk_count}</td>
<td class="p-war-value ${p.p_war >= 0 ? 'text-plus' : 'text-minus'}">
${p.p_war > 0 ? '+' : ''}${p.p_war}
</td>
</tr>
`;
}).join('')}
</tbody>
</table>
</div>
`;
}
function renderRiskSignals(data) {
const container = document.querySelector('.risk-signal-list');
// 1. 시스템 삭제(잠김예정) 프로젝트 우선 추출
const autoDeleted = data.filter(p => p.is_auto_delete).slice(0, 3);
// 2. 그 외 P-WAR가 낮은 순(음수)으로 추출
const highRiskProjects = data.filter(p => p.p_war < -1.0 && !p.is_auto_delete).slice(0, 5 - autoDeleted.length);
const combined = [...autoDeleted, ...highRiskProjects];
container.innerHTML = combined.map(p => `
<div class="risk-item high">
<div class="risk-project">${p.project_nm} (${p.master})</div>
<div class="risk-reason">
${p.is_auto_delete ? '[잠김예정] 활동 부재로 인한 시스템 자동 삭제 발생' : `P-WAR ${p.p_war} (대체 수준 이하 정체)`}
</div>
<div class="risk-status">위험</div>
</div>
`).join('');
}