feat: 운영 일관성(OCI) 지표 도입 및 분석 UI/UX 정밀 복구
- analysis_service.py: 운영 일관성(OCI) 산출 로직 구현 및 장기 정체 패널티(100일 기준) 적용 - js/analysis.js: OCI 통합, 아코디언 심층 분석 텍스트 보강, SWOT 사분면 및 스크롤 로직 정밀 복구 - style/*.css: 유색 border-left/top 스타일 제거 및 흑백/그레이 계열로 디자인 정제 - templates/analysis.html: 분석 모델 명칭 원복 및 지표 정의 UI 업데이트 - ANALYSIS_REPORT.md: OCI 지표 정의 추가 및 가치 기여도(VCI) 등급 설명 정제 (야구 용어 삭제)
This commit is contained in:
199
js/analysis.js
199
js/analysis.js
@@ -1,6 +1,7 @@
|
||||
/**
|
||||
* Project Master Analysis JS
|
||||
* AVI (Activity Vitality Index) & VCI (Value Contribution Index) 분석 엔진
|
||||
* OCI (Operational Consistency Index) 통합 버전
|
||||
*/
|
||||
|
||||
// Chart.js 플러그인 전역 등록
|
||||
@@ -33,19 +34,18 @@ async function loadProjectAnalysisData() {
|
||||
}
|
||||
|
||||
function getStatusInfo(avi, isAutoDelete) {
|
||||
if (isAutoDelete || avi < 10) return { label: '중단/방치', class: 'badge-system', key: 'dead' };
|
||||
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' };
|
||||
}
|
||||
|
||||
// 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: '가치를 훼손 중인 방치 자산 (방출급)' };
|
||||
if (vci >= 10) return { label: 'Masterpiece', class: 'grade-mvp', desc: '시스템 가치를 견인하는 최우량 핵심 자산' };
|
||||
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) {
|
||||
@@ -65,7 +65,7 @@ function renderValueCharts(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'],
|
||||
@@ -84,20 +84,19 @@ function renderValueCharts(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. 전략적 자산 매트릭스 (Scatter)
|
||||
// 2. 전략적 자산 매트릭스 (Scatter) - 정밀 복구
|
||||
try {
|
||||
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 => {
|
||||
@@ -140,9 +139,8 @@ function renderValueCharts(data) {
|
||||
scales: {
|
||||
x: {
|
||||
type: 'linear', min: 0, max: 500,
|
||||
title: { display: true, text: '파일 수 (Files)', font: { size: 11, weight: '700' } },
|
||||
grid: { display: false },
|
||||
ticks: { stepSize: 125, callback: (v) => v >= 500 ? '500+' : v }
|
||||
title: { display: true, text: '자산 규모 (파일 수)', font: { size: 11, weight: '700' } },
|
||||
grid: { display: false }
|
||||
},
|
||||
y: {
|
||||
min: 0, max: 100,
|
||||
@@ -155,17 +153,14 @@ function renderValueCharts(data) {
|
||||
datalabels: {
|
||||
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}] 활력(AVI): ${ctx.raw.y.toFixed(1)}% | 가치 기여(VCI): ${ctx.raw.vci.toFixed(1)}`
|
||||
label: (ctx) => ` [${ctx.raw.label}] AVI: ${ctx.raw.y.toFixed(1)}% | VCI: ${ctx.raw.vci.toFixed(1)}`
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -210,25 +205,47 @@ function renderVitalityLeaderboard(data) {
|
||||
<th>정체 일수</th>
|
||||
<th>상태 판정</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>운영 활력 (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>
|
||||
<th>운영 일관성 (OCI) <button class="btn-help" onclick="event.stopPropagation(); openAnalysisModal('oci')">?</button></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${sortedData.map((p, idx) => {
|
||||
const status = getStatusInfo(p.p_war, p.is_auto_delete);
|
||||
const avi = p.p_war;
|
||||
const vci = p.risk_count;
|
||||
const oci = p.oci_score || 0;
|
||||
const rowId = `project-${idx}`;
|
||||
const vci = p.risk_count || 0;
|
||||
const avi = p.p_war || 0;
|
||||
const grade = getVciGrade(vci);
|
||||
|
||||
let rhythmLabel = oci >= 80 ? "정기적" : oci >= 50 ? "안정적" : oci >= 20 ? "간헐적" : "불규칙";
|
||||
let rhythmColor = oci >= 80 ? "#059669" : oci >= 50 ? "#1e5149" : oci >= 20 ? "#f59e0b" : "#dc2626";
|
||||
|
||||
// 존재 신뢰도 패널티 (ECV) 상세 설명 복구
|
||||
let ecvText = "100% (데이터 실체 검증)";
|
||||
let ecvClass = "highlight-val";
|
||||
let ecvDesc = `현재 ${p.file_count}개의 유효 성과물이 확인됩니다. 시스템적으로 실체가 완벽히 존재하는 상태입니다.`;
|
||||
if (p.file_count === 0) {
|
||||
ecvText = "5% (유령 프로젝트 판명)";
|
||||
ecvClass = "highlight-penalty";
|
||||
ecvDesc = "데이터가 전무하여 프로젝트의 디지털 실체가 없습니다. 모든 분석에서 최하위 패널티가 적용됩니다.";
|
||||
} else if (p.file_count < 10) {
|
||||
ecvText = "40% (형식적 껍데기 판명)";
|
||||
ecvClass = "highlight-penalty";
|
||||
ecvDesc = "최소 수준의 문서만 존재하며, 실질적인 운영 가치를 인정하기 어려운 소규모 상태입니다.";
|
||||
}
|
||||
|
||||
// 활동 품질 텍스트 복구
|
||||
const qualityLabel = p.log_quality >= 1.0 ? '성과물 중심의 <b>실무 활동</b>' : p.log_quality >= 0.7 ? '구조 관리를 위한 <b>시스템 활동</b>' : '단순 행정 기반의 <b>형식 활동</b>';
|
||||
const qualityDetail = p.log_quality >= 1.0 ? '최근 로그에서 파일 업로드/수정 등 가치 증분 활동이 명확히 포착되었습니다.' : p.log_quality >= 0.7 ? '폴더 생성/이동 등 구조적 관리는 이뤄지고 있으나, 직접적 결과물 생산은 부족합니다.' : '메일 확인, 권한 변경 등 시스템 유지성 활동 위주로 파악되어 품질 가중치가 낮게 적용되었습니다.';
|
||||
|
||||
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 === '사망' ? '중단' : status.label}</span></td>
|
||||
<td><span class="${status.class}">${status.label}</span></td>
|
||||
<td style="text-align:right; font-weight:800; color:${vci >= 0 ? '#059669' : '#dc2626'};">
|
||||
${vci > 0 ? '+' : ''}${vci.toFixed(1)}
|
||||
</td>
|
||||
@@ -241,22 +258,27 @@ function renderVitalityLeaderboard(data) {
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td style="text-align:center; font-weight:700; color:#6366f1;">${p.predicted_soi !== null ? p.predicted_soi.toFixed(1) + '%' : '-'}</td>
|
||||
<td style="text-align:center;">
|
||||
<div style="display:flex; align-items:center; justify-content:center; gap:8px;">
|
||||
<span style="font-weight:800; font-size:13px; color:${rhythmColor};">${oci}%</span>
|
||||
<span style="font-size:10px; padding:2px 6px; border-radius:10px; background:${rhythmColor}15; color:${rhythmColor}; border:1px solid ${rhythmColor}30;">${rhythmLabel}</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr id="detail-${rowId}" class="detail-row">
|
||||
<td colspan="8">
|
||||
<div class="detail-container">
|
||||
<div class="formula-explanation-card">
|
||||
<div class="formula-header">⚙️ AI 자산 건전성 분석 시뮬레이션 (AAS Metrics)</div>
|
||||
<div class="formula-header">⚙️ AI 위험 적응형 모델(AAS) 기반 인과관계 분석</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: 13px; font-weight: 800; color: #1e5149;">📊 실질 업무 집중도 (Job Focus)</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 style="font-size: 11px; color: #64748b; line-height: 1.5;">최근 30회 수집 이력 중 단순 로그 갱신이 아닌 <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;">
|
||||
@@ -271,32 +293,52 @@ function renderVitalityLeaderboard(data) {
|
||||
<div class="formula-step">
|
||||
<div class="step-num">1</div>
|
||||
<div class="step-content">
|
||||
<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">활동 진정성 검증</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">가동 보존율 (AVI)</div>
|
||||
<div class="math-logic">Result = <span class="highlight-val">${avi.toFixed(1)}%</span></div>
|
||||
<div class="step-title">동적 위험 계수(λ) 산출</div>
|
||||
<div style="font-size:11px; color:#64748b; margin-bottom:5px;">프로젝트 규모가 클수록 정보 망실 시의 충격을 반영하여 데이터의 하락 속도가 가속됩니다. 현재 <b>λ=${p.ai_lambda.toFixed(4)}</b>는 귀하의 자산 규모가 정밀하게 투영된 결과입니다.</div>
|
||||
<div class="math-logic">Dynamic λ = <span class="highlight-var">${p.ai_lambda.toFixed(4)}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="formula-step">
|
||||
<div class="step-num">4</div>
|
||||
<div class="step-content">
|
||||
<div class="step-title">가치 기여 영향력 (VCI)</div>
|
||||
<div class="math-logic">VCI = <span class="highlight-val">${vci.toFixed(1)}</span></div>
|
||||
<div class="step-title">활동 품질 검증 (Quality)</div>
|
||||
<div class="step-desc" style="font-size:11px; margin-bottom:5px;">최근 로그 분석 결과 <b>${qualityLabel}</b>으로 판명되었습니다. ${qualityDetail}</div>
|
||||
<div class="math-logic">Quality Factor = <span class="${p.log_quality < 0.7 ? 'highlight-penalty' : 'highlight-val'}">${(p.log_quality * 100).toFixed(0)}%</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="formula-step">
|
||||
<div class="step-num">2</div>
|
||||
<div class="step-content">
|
||||
<div class="step-title">방치 시간 감쇄 적용</div>
|
||||
<div style="font-size:11px; color:#64748b; margin-bottom:5px;">마지막 유효 활동 이후 <b>${p.days_stagnant}일</b>간의 누적 정체 시간은 지수 감쇄 곡선을 따라 데이터의 최신성과 가치를 상쇄시켰습니다.</div>
|
||||
<div class="math-logic">Decay Result = <span class="highlight-val">${((avi / (p.file_count === 0 ? 0.05 : p.file_count < 10 ? 0.4 : 1) / p.log_quality) || 0).toFixed(1)}%</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="formula-step">
|
||||
<div class="step-num">3</div>
|
||||
<div class="step-content">
|
||||
<div class="step-title">존재 진정성 (ECV)</div>
|
||||
<div style="font-size:11px; color:#64748b; margin-bottom:5px;">${ecvDesc} 파일 수 자체가 분석의 데이터 진정성을 보정하는 핵심 팩터로 작용합니다.</div>
|
||||
<div class="math-logic">Entity Factor = <span class="${ecvClass}">${ecvText}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 20px; padding-top: 15px; border-top: 2px solid #1e5149; text-align: right; display: flex; justify-content: space-between; align-items: center;">
|
||||
<div style="text-align: left; max-width: 70%;">
|
||||
<div style="font-size: 13px; font-weight: 800; color: ${vci >= 0 ? '#059669' : '#dc2626'}; margin-bottom: 4px;">
|
||||
가치 기여도 (VCI) 진단: ${vci >= 0 ? '+' : ''}${vci.toFixed(2)}
|
||||
</div>
|
||||
<div style="font-size: 11px; color: #64748b; line-height: 1.5;">
|
||||
현재 프로젝트는 운영 표준(AVI 70%) 대비 <b>${Math.abs(avi - 70).toFixed(1)}%p ${avi >= 70 ? '상회' : '하회'}</b>하고 있으며,
|
||||
<b>${p.file_count}개</b>의 자산 규모에 따른 <b>${((p.file_count / 200) + 0.5).toFixed(2)}배</b>의 가중치가 적용되었습니다.
|
||||
이는 시스템 전체 관점에서 <b>${vci >= 0 ? '순자산 가치를 증대' : '잠재적 기회비용을 손실'}</b>시키고 있는 상태로 분석됩니다.
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span style="font-size: 13px; color: #64748b; font-weight: 700;">최종 AVI: </span>
|
||||
<span style="color: #1e5149; font-size: 22px; font-weight: 900;">${avi.toFixed(1)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -317,6 +359,8 @@ function toggleProjectDetail(rowId) {
|
||||
if (!detailRow.classList.contains('active')) {
|
||||
document.querySelectorAll('.detail-row').forEach(row => row.classList.remove('active'));
|
||||
detailRow.classList.add('active');
|
||||
|
||||
// 정밀 스크롤 이동 로직 복구
|
||||
setTimeout(() => {
|
||||
const headerH = container.querySelector('thead').offsetHeight || 45;
|
||||
const targetScrollTop = mainRow.offsetTop - headerH;
|
||||
@@ -333,11 +377,11 @@ function openProjectListModal(label, projects) {
|
||||
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>' : `
|
||||
body.innerHTML = `
|
||||
<div class="table-scroll-wrapper" style="max-height: 400px;">
|
||||
<table class="data-table">
|
||||
<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>
|
||||
<thead><tr><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.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 class="modal-footer"><button class="btn btn-primary" onclick="closeAnalysisModal()">닫기</button></div>`;
|
||||
@@ -361,29 +405,47 @@ function openAnalysisModal(type) {
|
||||
<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>
|
||||
<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 = `
|
||||
<div class="formula-box" style="margin-bottom:15px;">VCI = (AVI - 70.0) × (Files / 200 + 0.5)</div>
|
||||
<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>
|
||||
<tr><td>+10.0↑</td><td style="font-weight:900; color:#6366f1;">Masterpiece</td><td>시스템 가치를 견인하는 최우량 핵심 자산</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') {
|
||||
} else if (type === 'oci') {
|
||||
title.innerText = '운영 일관성 지수 (OCI) 분석 가이드';
|
||||
body.innerHTML = `
|
||||
<div style="background:#f0fdf4; padding:15px; border-radius:8px; margin-bottom:15px;">
|
||||
<strong style="color:#166534; display:block; margin-bottom:5px;">"얼마나 꾸준하게 관리되고 있는가?"</strong>
|
||||
<p style="font-size:12.5px; color:#166534; margin:0;">미래 예측이 아닌, 최근 30일간의 <b>활동 리듬</b>과 <b>관리의 규칙성</b>을 분석하여 성실도를 점수화합니다.</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;">80%↑</td><td style="font-weight:900; color:#059669;">매우 우수</td><td>주 단위의 정기적 관리가 완벽히 이뤄짐</td></tr>
|
||||
<tr><td style="color:#1e5149;">50~80%</td><td style="font-weight:900; color:#1e5149;">양호</td><td>간헐적 정체는 있으나 꾸준히 관리됨</td></tr>
|
||||
<tr><td style="color:#f59e0b;">20~50%</td><td style="font-weight:900; color:#f59e0b;">주의</td><td>돌발적 활동 위주, 관리의 리듬이 깨짐</td></tr>
|
||||
<tr><td style="color:#dc2626;">20%↓</td><td style="font-weight:900; color:#dc2626;">매우 불량</td><td>장기 정체 중이거나 관리 의지 확인 불가</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="modal-footer"><button class="btn btn-primary" onclick="closeAnalysisModal()">닫기</button></div>`;
|
||||
} else {
|
||||
title.innerText = '업무 집중도 (Job Focus) 등급 가이드';
|
||||
body.innerHTML = `
|
||||
<p style="font-size:13px; color:#64748b; margin-bottom:15px;">단순 관리 로그를 제외한 실질적인 산출물 변화의 밀도입니다.</p>
|
||||
<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>
|
||||
@@ -394,29 +456,6 @@ function openAnalysisModal(type) {
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="modal-footer"><button class="btn btn-primary" onclick="closeAnalysisModal()">닫기</button></div>`;
|
||||
} else {
|
||||
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';
|
||||
}
|
||||
|
||||
@@ -14,39 +14,46 @@ function renderPWarLeaderboard(data) {
|
||||
<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;">
|
||||
현재 SOI <button class="btn-help" onclick="event.stopPropagation(); openAnalysisModal('soi')">?</button>
|
||||
활력 지수 (AVI) <button class="btn-help" onclick="event.stopPropagation(); openAnalysisModal('avi')">?</button>
|
||||
</th>
|
||||
<th style="position: sticky; top: 0; z-index: 10; text-align:center;">실무 투입</th>
|
||||
<th style="position: sticky; top: 0; z-index: 10; text-align:right;">가치 기여 (VCI)</th>
|
||||
<th style="position: sticky; top: 0; z-index: 10; text-align:center;">업무 집중도</th>
|
||||
<th style="position: sticky; top: 0; z-index: 10;">
|
||||
AI 예보 (14d) <button class="btn-help" onclick="event.stopPropagation(); openAnalysisModal('ai')">?</button>
|
||||
운영 일관성 (OCI) <button class="btn-help" onclick="event.stopPropagation(); openAnalysisModal('oci')">?</button>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${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 avi = p.p_war;
|
||||
const vci = p.risk_count;
|
||||
const oci = p.oci_score || 0;
|
||||
const rowId = `project-${idx}`;
|
||||
|
||||
let trendIcon = "";
|
||||
if (pred !== null) {
|
||||
const diff = pred - soi;
|
||||
if (diff < -5) trendIcon = '<span style="color:#ef4444; font-size:10px;">▼ 급락</span>';
|
||||
else if (diff < 0) trendIcon = '<span style="color:#f59e0b; font-size:10px;">↘ 하락</span>';
|
||||
else trendIcon = '<span style="color:#22c55e; font-size:10px;">↗ 유지</span>';
|
||||
let rhythmLabel = "";
|
||||
let rhythmColor = "";
|
||||
if (oci >= 80) { rhythmLabel = "정기적"; rhythmColor = "#059669"; }
|
||||
else if (oci >= 50) { rhythmLabel = "안정적"; rhythmColor = "#1e5149"; }
|
||||
else if (oci >= 20) { rhythmLabel = "간헐적"; rhythmColor = "#f59e0b"; }
|
||||
else { rhythmLabel = "불규칙"; rhythmColor = "#dc2626"; }
|
||||
|
||||
// 존재 신뢰도 패널티 (ECV) 텍스트 준비
|
||||
let ecvText = "100% (데이터 신뢰)";
|
||||
let ecvClass = "highlight-val";
|
||||
let ecvDesc = "충분한 성과물이 존재합니다.";
|
||||
if (p.file_count === 0) {
|
||||
ecvText = "5% (유령 프로젝트)";
|
||||
ecvClass = "highlight-penalty";
|
||||
ecvDesc = "성과물이 전무하여 시스템 가치가 소멸되었습니다.";
|
||||
} else if (p.file_count < 10) {
|
||||
ecvText = "40% (소규모 껍데기)";
|
||||
ecvClass = "highlight-penalty";
|
||||
ecvDesc = "최소 수준의 데이터만 존재하여 가치가 낮게 평가됩니다.";
|
||||
}
|
||||
|
||||
// 수식 상세 데이터 준비
|
||||
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"; }
|
||||
// 활동 품질 텍스트 준비
|
||||
const qualityLabel = p.log_quality >= 1.0 ? '성과물 직결 <b>실무 활동</b>' : p.log_quality >= 0.7 ? '시스템 <b>구조적 활동</b>' : '단순 <b>행정적 활동</b>';
|
||||
|
||||
return `
|
||||
<tr class="project-row ${status.key === 'danger' ? 'row-danger' : status.key === 'warning' ? 'row-warning' : ''}"
|
||||
@@ -55,8 +62,11 @@ function renderPWarLeaderboard(data) {
|
||||
<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 ${soi >= 70 ? 'text-plus' : 'text-minus'}">
|
||||
${soi.toFixed(1)}%
|
||||
<td class="p-war-value ${avi >= 70 ? 'text-plus' : 'text-minus'}">
|
||||
${avi.toFixed(1)}%
|
||||
</td>
|
||||
<td style="text-align:right; font-weight:700; color:${vci >= 0 ? '#059669' : '#dc2626'};">
|
||||
${vci >= 0 ? '+' : ''}${vci.toFixed(2)}
|
||||
</td>
|
||||
<td style="text-align:center;">
|
||||
<div style="display:flex; flex-direction:column; align-items:center; gap:4px;">
|
||||
@@ -70,27 +80,29 @@ function renderPWarLeaderboard(data) {
|
||||
</td>
|
||||
<td style="text-align:center;">
|
||||
<div style="display:flex; align-items:center; justify-content:center; gap:8px;">
|
||||
<span style="font-weight:700; font-size:14px; color:#6366f1;">
|
||||
${pred !== null ? pred.toFixed(1) + '%' : '-'}
|
||||
<span style="font-weight:800; font-size:13px; color:${rhythmColor};">
|
||||
${oci}%
|
||||
</span>
|
||||
<span style="font-size:10px; padding:2px 6px; border-radius:10px; background:${rhythmColor}15; color:${rhythmColor}; border:1px solid ${rhythmColor}30;">
|
||||
${rhythmLabel}
|
||||
</span>
|
||||
${trendIcon}
|
||||
</div>
|
||||
</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 style="font-size: 13px; font-weight: 700; color: #6366f1; margin-bottom: 15px;">
|
||||
⚙️ AI 위험 적응형 모델(AAS) 산출 시뮬레이션
|
||||
</div>
|
||||
|
||||
<!-- 실무 투입 분석 (상단 배치) -->
|
||||
<!-- 업무 집중도 분석 (상단 배치) -->
|
||||
<div style="background: #f8fafc; padding: 15px; border-radius: 8px; margin-bottom: 20px; border: 1px solid #eef2f6;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
|
||||
<span style="font-size: 13px; font-weight: 800; color: #1e5149;">📊 실질 업무 활성화 분석 (Work Vitality)</span>
|
||||
<span style="font-size: 13px; font-weight: 800; color: #1e5149;">📊 실질 업무 집중도 분석 (Job Focus)</span>
|
||||
<span style="font-size: 14px; font-weight: 800; color: ${p.work_effort >= 70 ? '#059669' : p.work_effort <= 30 ? '#dc2626' : '#6366f1'};">
|
||||
투입률 ${p.work_effort}%
|
||||
집중도 ${p.work_effort}%
|
||||
</span>
|
||||
</div>
|
||||
<div style="width: 100%; height: 6px; background: #e2e8f0; border-radius: 3px; overflow: hidden; margin-bottom: 10px;">
|
||||
@@ -102,13 +114,13 @@ function renderPWarLeaderboard(data) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 수식 단계 2x2 그리드 (1-4, 2-3 순서) -->
|
||||
<!-- 수식 단계 2x2 그리드 -->
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px;">
|
||||
<!-- Row 1: Step 1 & Step 4 -->
|
||||
<div class="formula-step">
|
||||
<div class="step-num">1</div>
|
||||
<div class="step-content">
|
||||
<div class="step-title">동적 위험 계수(λ) 산출</div>
|
||||
<div style="font-size:11px; color:#64748b; margin-bottom:5px;">자산 규모(${p.file_count}개) 및 부서 위험도를 합산한 하락 속도입니다.</div>
|
||||
<div class="math-logic">λ = <span class="highlight-var">${p.ai_lambda.toFixed(4)}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -117,34 +129,40 @@ function renderPWarLeaderboard(data) {
|
||||
<div class="step-content">
|
||||
<div class="step-title">활동 품질 검증 (Quality)</div>
|
||||
<div class="step-desc" style="font-size:11px; margin-bottom:5px;">
|
||||
${p.log_quality >= 1.0 ? '성과물 직결 <b>실무 활동</b> 감지' : p.log_quality >= 0.7 ? '시스템 <b>구조적 활동</b> 주류' : '단순 <b>행정적 활동</b> 판명'}
|
||||
최근 로그 분석 결과 ${qualityLabel}으로 판명되었습니다.
|
||||
</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>
|
||||
</div>
|
||||
|
||||
<!-- Row 2: Step 2 & Step 3 -->
|
||||
<div class="formula-step">
|
||||
<div class="step-num">2</div>
|
||||
<div class="step-content">
|
||||
<div class="step-title">방치 시간 감쇄 적용</div>
|
||||
<div class="math-logic">Result = <span class="highlight-val">${((soi / (p.file_count === 0 ? 0.05 : p.file_count < 10 ? 0.4 : 1) / p.log_quality) || 0).toFixed(1)}%</span></div>
|
||||
<div style="font-size:11px; color:#64748b; margin-bottom:5px;">${p.days_stagnant}일간의 정체로 인한 가치 보존율입니다.</div>
|
||||
<div class="math-logic">Result = <span class="highlight-val">${((avi / (p.file_count === 0 ? 0.05 : p.file_count < 10 ? 0.4 : 1) / p.log_quality) || 0).toFixed(1)}%</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="formula-step">
|
||||
<div class="step-num">3</div>
|
||||
<div class="step-content">
|
||||
<div class="step-title">존재 진정성 (ECV)</div>
|
||||
<div style="font-size:11px; color:#64748b; margin-bottom:5px;">${ecvDesc}</div>
|
||||
<div class="math-logic">Factor = <span class="${ecvClass}">${ecvText}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 20px; padding-top: 15px; border-top: 2px solid #1e5149; text-align: right; display: flex; justify-content: space-between; align-items: center;">
|
||||
<span style="font-size: 11px; color: #94a3b8;">* 최종 점수는 위 4개 팩터의 연쇄 추론 결과입니다.</span>
|
||||
<div style="text-align: left;">
|
||||
<div style="font-size: 12px; font-weight: 700; color: ${vci >= 0 ? '#059669' : '#dc2626'};">
|
||||
가치 기여도 (VCI): ${vci >= 0 ? '+' : ''}${vci.toFixed(2)}
|
||||
</div>
|
||||
<div style="font-size: 10px; color: #94a3b8;">* AVI 70% 대비 프로젝트의 실질적 자산 하중 반영</div>
|
||||
</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;">${soi.toFixed(1)}%</span>
|
||||
<span style="font-size: 13px; color: #64748b; font-weight: 700;">최종 AVI: </span>
|
||||
<span style="color: #1e5149; font-size: 22px; font-weight: 900;">${avi.toFixed(1)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user