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:
2026-03-25 17:58:58 +09:00
parent dff3305da1
commit b864d615ea
11 changed files with 275 additions and 215 deletions

View File

@@ -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>