feat: 세이버메트릭스 기반 프로젝트 자산 가치 분석 시스템 고도화 (AVI/VCI 도입)
- analysis_service.py: AVI 및 VCI(자산 기여도) 산출 로직 구현 - prediction_service.py: 정체 프로젝트 AI 예보 최적화 - js/analysis.js: 전략 매트릭스 및 VCI 등급 시스템 시각화 - templates/analysis.html: UI 용어 최신화 및 스타일 통합 - ANALYSIS_REPORT.md: 분석 지표 공식 및 가이드라인 상세 기술
This commit is contained in:
310
js/analysis.js
310
js/analysis.js
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Project Master Analysis JS
|
||||
* P-WAR (Project Performance Above Replacement) 분석 엔진
|
||||
* AVI (Activity Vitality Index) & VCI (Value Contribution Index) 분석 엔진
|
||||
*/
|
||||
|
||||
// Chart.js 플러그인 전역 등록
|
||||
@@ -9,40 +9,49 @@ if (typeof ChartDataLabels !== 'undefined') {
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
console.log("Analysis engine initialized...");
|
||||
loadPWarData();
|
||||
console.log("Business Analysis Engine initialized...");
|
||||
loadProjectAnalysisData();
|
||||
});
|
||||
|
||||
async function loadPWarData() {
|
||||
async function loadProjectAnalysisData() {
|
||||
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);
|
||||
renderVitalityLeaderboard(data);
|
||||
renderValueCharts(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%에 가까울수록 방치 심각)`;
|
||||
if (infoEl) infoEl.textContent = `* 시스템 종합 자산 건전도: ${avg.avg_risk}% (운영 표준 70.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 getStatusInfo(avi, isAutoDelete) {
|
||||
if (isAutoDelete || avi < 10) return { label: '중단/방치', class: 'badge-system', key: 'dead' };
|
||||
if (avi < 30) return { label: '위험 노출', class: 'badge-danger', key: 'danger' };
|
||||
if (avi < 70) return { label: '관리 주의', class: 'badge-warning', key: 'warning' };
|
||||
return { label: '정상 운영', class: 'badge-active', key: 'active' };
|
||||
}
|
||||
|
||||
function renderSOICharts(data) {
|
||||
// VCI 등급 판정 로직 (Sabermetrics WAR 등급 기준 응용)
|
||||
function getVciGrade(vci) {
|
||||
if (vci >= 10) return { label: 'Masterpiece', class: 'grade-mvp', desc: '시스템 가치를 견인하는 핵심 자산 (MVP급)' };
|
||||
if (vci >= 2) return { label: 'Blue Chip', class: 'grade-allstar', desc: '꾸준한 활력의 우량 자산 (주전급)' };
|
||||
if (vci >= -2) return { label: 'Steady', class: 'grade-starter', desc: '표준 수준의 현상 유지 (보결급)' };
|
||||
if (vci >= -10) return { label: 'Underperform', class: 'grade-bench', desc: '운영 미비로 인한 가치 하락 (마이너급)' };
|
||||
return { label: 'Liability', class: 'grade-out', desc: '가치를 훼손 중인 방치 자산 (방출급)' };
|
||||
}
|
||||
|
||||
function renderValueCharts(data) {
|
||||
if (!data || data.length === 0) return;
|
||||
|
||||
// 1. 상태 분포 (Doughnut)
|
||||
// 1. 운영 활력 분포 (Doughnut)
|
||||
try {
|
||||
const stats = { active: [], warning: [], danger: [], dead: [] };
|
||||
data.forEach(p => {
|
||||
@@ -56,7 +65,7 @@ function renderSOICharts(data) {
|
||||
window.myStatusChart = new Chart(statusCtx, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: ['정상', '주의', '위험', '사망'],
|
||||
labels: ['정상 운영', '관리 주의', '위험 노출', '중단/방치'],
|
||||
datasets: [{
|
||||
data: [stats.active.length, stats.warning.length, stats.danger.length, stats.dead.length],
|
||||
backgroundColor: ['#1E5149', '#22c55e', '#f59e0b', '#ef4444'],
|
||||
@@ -75,53 +84,40 @@ function renderSOICharts(data) {
|
||||
onClick: (e, elements) => {
|
||||
if (elements.length > 0) {
|
||||
const idx = elements[0].index;
|
||||
openProjectListModal(['정상', '주의', '위험', '사망'][idx], stats[['active', 'warning', 'danger', 'dead'][idx]]);
|
||||
openProjectListModal(['정상 운영', '관리 주의', '위험 노출', '중단/방치'][idx], stats[['active', 'warning', 'danger', 'dead'][idx]]);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (err) { console.error("도넛 차트 에러:", err); }
|
||||
|
||||
// 2. 프로젝트 SWOT 매트릭스 (Scatter)
|
||||
// 2. 전략적 자산 매트릭스 (Scatter)
|
||||
try {
|
||||
const scatterData = data.map(p => ({
|
||||
x: Math.min(500, p.file_count),
|
||||
y: p.p_war,
|
||||
label: p.project_nm
|
||||
}));
|
||||
const sortedByAVI = [...data].sort((a, b) => b.p_war - a.p_war);
|
||||
const top5Ids = sortedByAVI.slice(0, 5).map(p => p.project_nm);
|
||||
const bottom5Ids = sortedByAVI.slice(-5).map(p => p.project_nm);
|
||||
const largeProjects = data.filter(p => p.file_count > 450).map(p => p.project_nm);
|
||||
|
||||
const vipProjectNames = new Set([...top5Ids, ...bottom5Ids, ...largeProjects]);
|
||||
|
||||
const scatterData = data.map(p => {
|
||||
const vci = p.risk_count || 0;
|
||||
const absVci = Math.abs(vci);
|
||||
return {
|
||||
x: Math.min(500, p.file_count),
|
||||
y: p.p_war,
|
||||
label: p.project_nm,
|
||||
isVip: vipProjectNames.has(p.project_nm),
|
||||
vci: vci,
|
||||
radius: Math.max(5, Math.min(25, 5 + (absVci / 10)))
|
||||
};
|
||||
});
|
||||
|
||||
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,
|
||||
@@ -133,45 +129,73 @@ function renderSOICharts(data) {
|
||||
if (p.x < 250 && p.y < 50) return '#94a3b8';
|
||||
return '#ef4444';
|
||||
},
|
||||
pointRadius: 6,
|
||||
hoverRadius: 10
|
||||
pointRadius: (ctx) => ctx.raw ? ctx.raw.radius : 5,
|
||||
hoverRadius: (ctx) => (ctx.raw ? ctx.raw.radius : 5) + 3
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
layout: { padding: { top: 30, right: 40, left: 10, bottom: 10 } },
|
||||
layout: { padding: { top: 30, right: 45, left: 10, bottom: 10 } },
|
||||
scales: {
|
||||
x: {
|
||||
type: 'linear', min: 0, max: 500,
|
||||
title: { display: true, text: '자산 규모 (파일 수)', font: { size: 11, weight: '700' } },
|
||||
title: { display: true, text: '파일 수 (Files)', 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' } },
|
||||
title: { display: true, text: '운영 활력 (AVI %)', 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,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.8)',
|
||||
borderRadius: 4, padding: 4,
|
||||
align: (ctx) => (ctx.raw && ctx.raw.y > 80 ? 'bottom' : 'top'),
|
||||
offset: (ctx) => (ctx.raw ? ctx.raw.radius : 5) + 2,
|
||||
font: { size: 10, weight: '800' },
|
||||
color: '#1e293b',
|
||||
formatter: (v) => v ? v.label : '',
|
||||
display: (ctx) => ctx.raw && ctx.raw.isVip,
|
||||
clip: false
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: { label: (ctx) => ` [${ctx.raw.label}] SOI: ${ctx.raw.y.toFixed(1)}% | 파일: ${ctx.raw.x >= 500 ? '500+' : ctx.raw.x}개` }
|
||||
callbacks: {
|
||||
label: (ctx) => ` [${ctx.raw.label}] 활력(AVI): ${ctx.raw.y.toFixed(1)}% | 가치 기여(VCI): ${ctx.raw.vci.toFixed(1)}`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: [{
|
||||
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();
|
||||
}
|
||||
}]
|
||||
});
|
||||
} catch (err) { console.error("SWOT 차트 에러:", err); }
|
||||
} catch (err) { console.error("전략 매트릭스 에러:", err); }
|
||||
}
|
||||
|
||||
function renderPWarLeaderboard(data) {
|
||||
function renderVitalityLeaderboard(data) {
|
||||
const container = document.getElementById('p-war-table-container');
|
||||
if (!container) return;
|
||||
const sortedData = [...data].sort((a, b) => a.p_war - b.p_war);
|
||||
@@ -181,29 +205,34 @@ function renderPWarLeaderboard(data) {
|
||||
<table class="data-table p-war-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 280px;">프로젝트명</th>
|
||||
<th style="width: 250px;">프로젝트명</th>
|
||||
<th>파일 수</th>
|
||||
<th>방치일</th>
|
||||
<th>정체 일수</th>
|
||||
<th>상태 판정</th>
|
||||
<th>현재 SOI <button class="btn-help" onclick="event.stopPropagation(); openAnalysisModal('soi')">?</button></th>
|
||||
<th style="text-align:center;">실무 투입</th>
|
||||
<th>AI 예보 (14d) <button class="btn-help" onclick="event.stopPropagation(); openAnalysisModal('ai')">?</button></th>
|
||||
<th style="text-align:right;">가치 기여 (VCI) <button class="btn-help" onclick="event.stopPropagation(); openAnalysisModal('vci')">?</button></th>
|
||||
<th>활력 지수 (AVI) <button class="btn-help" onclick="event.stopPropagation(); openAnalysisModal('avi')">?</button></th>
|
||||
<th style="text-align:center;">업무 집중도 <button class="btn-help" onclick="event.stopPropagation(); openAnalysisModal('focus')">?</button></th>
|
||||
<th>상태 예보 (14d) <button class="btn-help" onclick="event.stopPropagation(); openAnalysisModal('ai')">?</button></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${sortedData.map((p, idx) => {
|
||||
const status = getStatusInfo(p.p_war, p.is_auto_delete);
|
||||
const rowId = `project-${idx}`;
|
||||
const ecvText = p.file_count === 0 ? "5% (유령)" : p.file_count < 10 ? "40% (껍데기)" : "100% (신뢰)";
|
||||
const ecvClass = (p.file_count < 10) ? "highlight-penalty" : "highlight-val";
|
||||
const vci = p.risk_count || 0;
|
||||
const avi = p.p_war || 0;
|
||||
const grade = getVciGrade(vci);
|
||||
|
||||
return `
|
||||
<tr class="project-row ${status.key === 'danger' ? 'row-danger' : status.key === 'warning' ? 'row-warning' : ''}" onclick="toggleProjectDetail('${rowId}')">
|
||||
<td class="font-bold">${p.project_nm}</td>
|
||||
<td>${p.file_count.toLocaleString()}개</td>
|
||||
<td>${p.days_stagnant}일</td>
|
||||
<td><span class="${status.class}">${status.label}</span></td>
|
||||
<td class="p-war-value ${p.p_war >= 70 ? 'text-plus' : 'text-minus'}">${p.p_war.toFixed(1)}%</td>
|
||||
<td><span class="${status.class}">${status.label === '사망' ? '중단' : status.label}</span></td>
|
||||
<td style="text-align:right; font-weight:800; color:${vci >= 0 ? '#059669' : '#dc2626'};">
|
||||
${vci > 0 ? '+' : ''}${vci.toFixed(1)}
|
||||
</td>
|
||||
<td class="p-war-value ${avi >= 70 ? 'text-plus' : 'text-minus'}">${avi.toFixed(1)}%</td>
|
||||
<td style="text-align:center;">
|
||||
<div style="display:flex; flex-direction:column; align-items:center; gap:4px;">
|
||||
<span style="font-weight:800; font-size:12px; color:${p.work_effort >= 70 ? '#059669' : p.work_effort <= 30 ? '#dc2626' : '#6366f1'};">${p.work_effort}%</span>
|
||||
@@ -215,52 +244,60 @@ function renderPWarLeaderboard(data) {
|
||||
<td style="text-align:center; font-weight:700; color:#6366f1;">${p.predicted_soi !== null ? p.predicted_soi.toFixed(1) + '%' : '-'}</td>
|
||||
</tr>
|
||||
<tr id="detail-${rowId}" class="detail-row">
|
||||
<td colspan="7">
|
||||
<td colspan="8">
|
||||
<div class="detail-container">
|
||||
<div class="formula-explanation-card">
|
||||
<div class="formula-header">⚙️ AI 위험 적응형 모델(AAS) 산출 시뮬레이션</div>
|
||||
<div class="work-effort-section">
|
||||
<div class="work-effort-header">
|
||||
<span style="font-size: 13px; font-weight: 800; color: #1e5149;">📊 실질 업무 활성화 분석 (Work Vitality)</span>
|
||||
<span style="font-size: 14px; font-weight: 800; color: ${p.work_effort >= 70 ? '#059669' : p.work_effort <= 30 ? '#dc2626' : '#6366f1'};">투입률 ${p.work_effort}%</span>
|
||||
<div class="formula-header">⚙️ AI 자산 건전성 분석 시뮬레이션 (AAS Metrics)</div>
|
||||
|
||||
<div style="display: flex; gap: 20px; margin-bottom: 20px;">
|
||||
<div class="work-effort-section" style="flex: 1; margin-bottom: 0;">
|
||||
<div class="work-effort-header">
|
||||
<span style="font-size: 13px; font-weight: 800; color: #1e5149;">📊 실질 업무 집중도 Analysis</span>
|
||||
<span style="font-size: 14px; font-weight: 800; color: ${p.work_effort >= 70 ? '#059669' : p.work_effort <= 30 ? '#dc2626' : '#6366f1'};">${p.work_effort}%</span>
|
||||
</div>
|
||||
<div class="work-effort-bar-bg"><div style="width: ${p.work_effort}%; height: 100%; background: ${p.work_effort >= 70 ? '#059669' : p.work_effort <= 30 ? '#dc2626' : '#6366f1'};"></div></div>
|
||||
<div style="font-size: 11px; color: #64748b; line-height: 1.5;">최근 수집 로그 중 실질적 <b>자산 증분</b>이 포착된 밀도입니다.</div>
|
||||
</div>
|
||||
<div style="flex: 1; background: #f8fafc; border-radius: 8px; padding: 16px; border: 1px solid #e2e8f0; display: flex; align-items: center; gap: 15px;">
|
||||
<div style="text-align: center;">
|
||||
<div style="font-size: 10px; color: #64748b; font-weight: 700; margin-bottom: 2px;">VCI GRADE</div>
|
||||
<div class="grade-badge ${grade.class}" style="padding: 4px 12px; border-radius: 6px; font-weight: 900; font-size: 14px; display: inline-block;">${grade.label}</div>
|
||||
</div>
|
||||
<div style="font-size: 12px; color: #475569; line-height: 1.4; font-weight: 600;">${grade.desc}</div>
|
||||
</div>
|
||||
<div class="work-effort-bar-bg"><div style="width: ${p.work_effort}%; height: 100%; background: ${p.work_effort >= 70 ? '#059669' : p.work_effort <= 30 ? '#dc2626' : '#6366f1'}; transition: width 0.5s;"></div></div>
|
||||
<div style="font-size: 11.5px; color: #64748b; line-height: 1.5;">최근 30회 중 실제 파일 변동이 포착된 날의 비율입니다. 현재 <b>${p.work_effort >= 70 ? '매우 활발' : p.work_effort <= 30 ? '정체' : '간헐적'}</b> 상태입니다.</div>
|
||||
</div>
|
||||
|
||||
<div class="formula-steps-grid">
|
||||
<div class="formula-step">
|
||||
<div class="step-num">1</div>
|
||||
<div class="step-content">
|
||||
<div class="step-title">동적 위험 계수(λ)</div>
|
||||
<div class="step-title">동적 감쇄 계수(λ) 산출</div>
|
||||
<div class="step-desc" style="font-size:11px; color:#64748b; margin-bottom:5px;">자산 규모 및 조직 위험을 합산하여 개별 활력 곡선을 생성합니다.</div>
|
||||
<div class="math-logic">λ = <span class="highlight-var">${p.ai_lambda.toFixed(4)}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="formula-step">
|
||||
<div class="step-num">2</div>
|
||||
<div class="step-content">
|
||||
<div class="step-title">활동 품질 (Quality)</div>
|
||||
<div class="math-logic">Factor = <span class="${p.log_quality < 0.7 ? 'highlight-penalty' : 'highlight-val'}">${(p.log_quality * 100).toFixed(0)}%</span></div>
|
||||
<div class="step-title">활동 진정성 검증</div>
|
||||
<div class="math-logic">Factor = <span class="highlight-val">${(p.log_quality * 100).toFixed(0)}%</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="formula-step">
|
||||
<div class="step-num">3</div>
|
||||
<div class="step-content">
|
||||
<div class="step-title">방치 시간 감쇄</div>
|
||||
<div class="math-logic">Result = <span class="highlight-val">${((p.p_war / (p.file_count === 0 ? 0.05 : p.file_count < 10 ? 0.4 : 1) / p.log_quality) || 0).toFixed(1)}%</span></div>
|
||||
<div class="step-title">가동 보존율 (AVI)</div>
|
||||
<div class="math-logic">Result = <span class="highlight-val">${avi.toFixed(1)}%</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="formula-step">
|
||||
<div class="step-num">4</div>
|
||||
<div class="step-content">
|
||||
<div class="step-title">존재 진정성 (ECV)</div>
|
||||
<div class="math-logic">Factor = <span class="${ecvClass}">${ecvText}</span></div>
|
||||
<div class="step-title">가치 기여 영향력 (VCI)</div>
|
||||
<div class="math-logic">VCI = <span class="highlight-val">${vci.toFixed(1)}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="final-result-area">
|
||||
<div style="font-size: 11px; color: #64748b;">* 공식: AAS_Score × Quality_Factor × ECV_Factor</div>
|
||||
<div><span style="font-size: 13px; color: #64748b; font-weight: 700;">최종 P-SOI: </span><span style="color: #1e5149; font-size: 22px; font-weight: 900;">${p.p_war.toFixed(1)}%</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
@@ -275,12 +312,19 @@ 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) {
|
||||
if (!detailRow.classList.contains('active')) {
|
||||
document.querySelectorAll('.detail-row').forEach(row => row.classList.remove('active'));
|
||||
detailRow.classList.add('active');
|
||||
setTimeout(() => { container.scrollTo({ top: mainRow.offsetTop - (container.querySelector('thead').offsetHeight || 40), behavior: 'smooth' }); }, 50);
|
||||
} else detailRow.classList.remove('active');
|
||||
setTimeout(() => {
|
||||
const headerH = container.querySelector('thead').offsetHeight || 45;
|
||||
const targetScrollTop = mainRow.offsetTop - headerH;
|
||||
container.scrollTo({ top: targetScrollTop, behavior: 'smooth' });
|
||||
}, 100);
|
||||
} else {
|
||||
detailRow.classList.remove('active');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,14 +332,15 @@ function openProjectListModal(label, projects) {
|
||||
const modal = document.getElementById('analysisModal');
|
||||
const title = document.getElementById('modalTitle');
|
||||
const body = document.getElementById('modalBody');
|
||||
title.innerText = `[${label}] 프로젝트 목록 (${projects.length}건)`;
|
||||
body.innerHTML = projects.length === 0 ? '<p style="text-align:center; padding: 40px; color: #888;">데이터 없음</p>' : `
|
||||
title.innerText = `[${label}] 프로젝트 리스트 (${projects.length}건)`;
|
||||
body.innerHTML = projects.length === 0 ? '<p style="text-align:center; padding: 40px; color: #888;">대상 프로젝트 없음</p>' : `
|
||||
<div class="table-scroll-wrapper" style="max-height: 400px;">
|
||||
<table class="data-table">
|
||||
<thead><tr><th>프로젝트명</th><th>관리자</th><th>방치일</th><th>현재 SOI</th></tr></thead>
|
||||
<tbody>${projects.map(p => `<tr><td class="font-bold">${p.project_nm}</td><td>${p.master || '-'}</td><td>${p.days_stagnant}일</td><td style="font-weight:700; color:#1e5149;">${p.p_war.toFixed(1)}%</td></tr>`).join('')}</tbody>
|
||||
<thead><tr><th>프로젝트명</th><th>부서</th><th>관리자</th><th>정체일</th><th>활력(AVI)</th></tr></thead>
|
||||
<tbody>${projects.map(p => `<tr><td class="font-bold">${p.project_nm}</td><td>${p.dept || '-'}</td><td>${p.master || '-'}</td><td>${p.days_stagnant}일</td><td style="font-weight:700; color:#1e5149;">${p.p_war.toFixed(1)}%</td></tr>`).join('')}</tbody>
|
||||
</table>
|
||||
</div>`;
|
||||
</div>
|
||||
<div class="modal-footer"><button class="btn btn-primary" onclick="closeAnalysisModal()">닫기</button></div>`;
|
||||
modal.style.display = 'flex';
|
||||
}
|
||||
|
||||
@@ -303,12 +348,75 @@ 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 = '<div class="formula-section"><div class="formula-box">SOI = exp(-λ × days) × 100</div></div><p>방치일수에 따른 가치 하락 모델입니다.</p>';
|
||||
|
||||
if (type === 'avi') {
|
||||
title.innerText = '운영 활력 지수 (AVI) 등급 가이드';
|
||||
body.innerHTML = `
|
||||
<div class="formula-box" style="margin-bottom:15px;">AVI = exp(-λ × days) × Quality × 100</div>
|
||||
<p style="font-size:13px; color:#64748b; margin-bottom:15px;">자산의 가동 상태와 생존율을 나타내는 지표입니다.</p>
|
||||
<table class="data-table" style="font-size:12px;">
|
||||
<thead><tr style="background:#f8fafc;"><th>지수 (AVI)</th><th>등급</th><th>운영 상태</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>90%↑</td><td style="font-weight:900; color:#059669;">Live</td><td>실시간 성과물이 도출되는 최상급 가동</td></tr>
|
||||
<tr><td>70~90%</td><td style="font-weight:900; color:#1e5149;">Stable</td><td>주기적 업데이트가 이뤄지는 표준 안정</td></tr>
|
||||
<tr><td>30~70%</td><td style="font-weight:900; color:#f59e0b;">Idle</td><td>관리가 필요한 유휴/정체 상태</td></tr>
|
||||
<tr><td>10~30%</td><td style="font-weight:900; color:#dc2626;">Risk</td><td>자산 가치 소멸 직전의 위험 상태</td></tr>
|
||||
<tr><td>10%↓</td><td style="font-weight:900; color:#64748b;">Frozen</td><td>운영이 중단된 동결/방치 상태</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="modal-footer"><button class="btn btn-primary" onclick="closeAnalysisModal()">닫기</button></div>`;
|
||||
} else if (type === 'vci') {
|
||||
title.innerText = '자산 가치 기여도 (VCI) 등급 가이드';
|
||||
body.innerHTML = `
|
||||
<p style="font-size:13px; color:#64748b; margin-bottom:15px;">운영 표준(AVI 70%) 대비 자산 가치 기여도에 따른 프로젝트 위상 분류입니다.</p>
|
||||
<table class="data-table" style="font-size:12px;">
|
||||
<thead><tr style="background:#f8fafc;"><th>점수 (VCI)</th><th>등급</th><th>운영 의미</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>+10.0↑</td><td style="font-weight:900; color:#6366f1;">Masterpiece</td><td>시스템 가치를 견인하는 핵심 자산 (MVP급)</td></tr>
|
||||
<tr><td>+2.0 ~ +10.0</td><td style="font-weight:900; color:#059669;">Blue Chip</td><td>꾸준한 활력의 우량 자산 (주전급)</td></tr>
|
||||
<tr><td>-2.0 ~ +2.0</td><td style="font-weight:900; color:#475569;">Steady</td><td>표준 수준의 현상 유지 (보결급)</td></tr>
|
||||
<tr><td>-10.0 ~ -2.0</td><td style="font-weight:900; color:#f59e0b;">Underperform</td><td>운영 미비로 인한 가치 하락 (마이너급)</td></tr>
|
||||
<tr><td>-10.0↓</td><td style="font-weight:900; color:#dc2626;">Liability</td><td>가치를 훼손 중인 방치 자산 (방출급)</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="modal-footer"><button class="btn btn-primary" onclick="closeAnalysisModal()">닫기</button></div>`;
|
||||
} else if (type === 'focus') {
|
||||
title.innerText = '업무 집중도 (Job Focus) 등급 가이드';
|
||||
body.innerHTML = `
|
||||
<p style="font-size:13px; color:#64748b; margin-bottom:15px;">단순 관리 로그를 제외한 실질적인 산출물 변화의 밀도입니다.</p>
|
||||
<table class="data-table" style="font-size:12px;">
|
||||
<thead><tr style="background:#f8fafc;"><th>비율 (%)</th><th>등급</th><th>활동 성격</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>80%↑</td><td style="font-weight:900; color:#6366f1;">Intensive</td><td>성과물 위주의 고밀도 집중 작업</td></tr>
|
||||
<tr><td>50~80%</td><td style="font-weight:900; color:#059669;">Active</td><td>성과와 관리가 균형 잡힌 원활한 실행</td></tr>
|
||||
<tr><td>20~50%</td><td style="font-weight:900; color:#f59e0b;">Maintenance</td><td>설정/행정 등 단순 관리 중심의 작업</td></tr>
|
||||
<tr><td>20%↓</td><td style="font-weight:900; color:#dc2626;">Surface</td><td>실체적 변화가 적은 형식적 로그 중심</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="modal-footer"><button class="btn btn-primary" onclick="closeAnalysisModal()">닫기</button></div>`;
|
||||
} else {
|
||||
title.innerText = 'AI 시계열 예측 상세';
|
||||
body.innerHTML = '<p>활동 가속도 및 밀도를 분석하여 14일 뒤의 상태를 예보합니다.</p>';
|
||||
title.innerText = '상태 예보 (AI Forecast) 분석 가이드';
|
||||
body.innerHTML = `
|
||||
<div style="background:#eef2ff; padding:15px; border-radius:8px; border-left:4px solid #6366f1; margin-bottom:15px;">
|
||||
<strong style="color:#3730a3; display:block; margin-bottom:5px;">"2주 뒤의 프로젝트 건강 상태를 예측합니다"</strong>
|
||||
<p style="font-size:12.5px; color:#3730a3; margin:0;">단순한 현재 점수 나열이 아닌, 최근 활동의 <b>가속도(Acceleration)</b>와 <b>변화 패턴</b>을 AI가 분석하여 미래의 활력 지수(AVI)를 예보합니다.</p>
|
||||
</div>
|
||||
|
||||
<table class="data-table" style="font-size:12px;">
|
||||
<thead><tr style="background:#f8fafc;"><th>분석 결과</th><th>상태 등급</th><th>관리 가이드라인</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td style="color:#059669;">AVI 상승↑</td><td style="font-weight:900; color:#059669;">성장 가속</td><td>활동 모멘텀이 상승 중인 우수 자산</td></tr>
|
||||
<tr><td style="color:#475569;">AVI 유지</td><td style="font-weight:900; color:#475569;">안정 유지</td><td>현재의 리듬을 유지하는 표준 운영 상태</td></tr>
|
||||
<tr><td style="color:#f59e0b;">AVI 하락↓</td><td style="font-weight:900; color:#f59e0b;">활력 저하</td><td>정체 징후 포착, 관리 리소스 투입 검토</td></tr>
|
||||
<tr><td style="color:#dc2626;">AVI 10%↓</td><td style="font-weight:900; color:#dc2626;">중단 위기</td><td>단기 내 완전 방치 및 가치 소멸 위험</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div style="margin-top:15px; font-size:11.5px; color:#64748b; line-height:1.6;">
|
||||
<strong>※ 분석 알고리즘 안내:</strong><br>
|
||||
파일 수의 실질적 증가가 없는 프로젝트는 '성장 가속' 예보를 받을 수 없도록 설계되어 있으며, 정체가 길어질수록 감쇄 가중치가 자동으로 강화됩니다.
|
||||
</div>
|
||||
<div class="modal-footer"><button class="btn btn-primary" onclick="closeAnalysisModal()">닫기</button></div>`;
|
||||
}
|
||||
modal.style.display = 'flex';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user