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

@@ -9,11 +9,12 @@
### 1.1 운영 활력 지수 (AVI, Activity Vitality Index) ### 1.1 운영 활력 지수 (AVI, Activity Vitality Index)
프로젝트가 현재 얼마나 '살아서 숨 쉬고 있는가'를 나타내는 생존 지수입니다. 프로젝트가 현재 얼마나 '살아서 숨 쉬고 있는가'를 나타내는 생존 지수입니다.
* **산출 공식**: $AVI = exp(-\lambda \times days) \times Quality \times 100$ * **산출 공식**: $AVI = exp(-\lambda \times days) \times Quality \times ECV \times 100$
* **핵심 데이터**: * **핵심 데이터**:
* **정체 일수(days)**: 마지막 유의미한 파일 업데이트 이후 경과 시간. * **정체 일수(days)**: 마지막 유의미한 파일 업데이트 이후 경과 시간.
* **감쇄 계수($\lambda$)**: 자산 규모(파일 수)가 클수록, 소속 부서의 방치율이 높을수록 커지며 점수를 더 빠르게 하락시킵니다. * **감쇄 계수($\lambda$)**: 기본 $0.04$에서 시작하여, 자산 규모(최대 $+0.04$)와 부서 정체율(최대 $+0.03$)을 동적으로 결합합니다.
* **활동 품질(Quality)**: 단순 시스템 로그(단순 접속, 설정 변경)는 낮게 평가하고, 실질적인 파일 증분 활동에 가점을 부여합니다. * **활동 품질(Quality)**: 파일 증분 활동($1.0$), 구조적 관리($0.7$), 단순 행정 로그($0.4$)로 차등 배점합니다.
* **존재 신뢰도(ECV)**: 파일 수 $0$개($0.05$), $10$개 미만($0.4$) 등 유령 프로젝트에 패널티를 부여합니다.
* **의미**: 100%에 가까울수록 실시간 가동 상태이며, 0%에 가까울수록 데이터 노후화가 완료된 '사망' 상태를 뜻합니다. * **의미**: 100%에 가까울수록 실시간 가동 상태이며, 0%에 가까울수록 데이터 노후화가 완료된 '사망' 상태를 뜻합니다.
### 1.2 자산 가치 기여도 (VCI, Value Contribution Index) ### 1.2 자산 가치 기여도 (VCI, Value Contribution Index)
@@ -22,15 +23,21 @@
* **산출 공식**: $VCI = (AVI - 70.0) \times (\frac{Files}{200} + 0.5)$ * **산출 공식**: $VCI = (AVI - 70.0) \times (\frac{Files}{200} + 0.5)$
* **핵심 로직**: * **핵심 로직**:
* **건강 기준선(70.0%)**: 시스템 자산 가치를 유지하기 위한 최소 마지노선(Replacement Level)입니다. * **건강 기준선(70.0%)**: 시스템 자산 가치를 유지하기 위한 최소 마지노선(Replacement Level)입니다.
* **규모 가중치**: 파일 수가 많은 대형 프로젝트일수록 동일한 방치 상황에서 시스템에 주는 충격(음수값)이 기하급수적으로 커집니다. * **규모 가중치**: 파일 $200$개를 $1.0$ 가중치 기준으로 삼아, 대형 프로젝트일수록 시스템에 주는 충격 기하급수적으로 반영합니다.
* **의미**: 양수(+)는 가치 창출, 음수(-)는 시스템 기회비용을 갉아먹는 '가치 파괴' 상태임을 나타냅니다. * **의미**: 양수(+)는 가치 창출, 음수(-)는 시스템 기회비용을 갉아먹는 '가치 파괴' 상태임을 나타냅니다.
### 1.3 업무 집중도 (Job Focus) ### 1.3 업무 집중도 (Job Focus)
단순 관리 행위를 제외하고, 실제 성과물(파일)을 생산하는 데 얼마나 몰입했는지를 판별합니다. 단순 관리 행위를 제외하고, 실제 성과물(파일)을 생산하는 데 얼마나 몰입했는지를 판별합니다.
* **산출 공식**: $Job Focus = \frac{\text{최근 30회 중 실 파일 변동 발생 횟수}}{\text{전체 데이터 수집 횟수}} \times 100$ * **산출 공식**: $Job Focus = \frac{\text{최근 히스토리 중 실 파일 변동 발생 횟수}}{\text{전체 데이터 수집 횟수}} \times 100$
* **의미**: 로그만 남기는 '보여주기식 활동'을 필터링하여 운영의 진정성을 확인합니다. * **의미**: 로그만 남기는 '보여주기식 활동'을 필터링하여 운영의 진정성을 확인합니다.
### 1.4 운영 일관성 지수 (OCI, Operational Consistency Index)
프로젝트 관리의 '리듬'과 '성실도'를 측정하는 지표입니다.
* **산출 공식**: 최근 30일 데이터를 4개 주차로 분할하여 활동 여부 분석 (주차별 성실도 70% + 활동 밀도 30%)
* **의미**: 특정 시점에 몰아치기식 작업을 하는 프로젝트보다, 매주 꾸준히 관리되는 프로젝트에 더 높은 신뢰 점수를 부여합니다.
--- ---
## 2. 등급 체계 및 관리 가이드 (Grade System) ## 2. 등급 체계 및 관리 가이드 (Grade System)
@@ -38,19 +45,17 @@
### 2.1 VCI 등급 (프로젝트 위상) ### 2.1 VCI 등급 (프로젝트 위상)
| 등급 (Grade) | 점수 기준 | 운영 의미 및 관리 전략 | | 등급 (Grade) | 점수 기준 | 운영 의미 및 관리 전략 |
| :--- | :--- | :--- | | :--- | :--- | :--- |
| **Masterpiece** | +10.0 이상 | **핵심 자산 (MVP)**: 시스템 가치를 견인하는 최우량 프로젝트 | | **Masterpiece** | +10.0 이상 | **최우량 자산**: 시스템 가치를 견인하는 핵심 프로젝트 |
| **Blue Chip** | +2.0 ~ +10.0 | **우량 자산 (주전)**: 꾸준한 활력으로 가치를 창출하는 핵심군 | | **Blue Chip** | +2.0 ~ +10.0 | **우량 자산**: 꾸준한 활력으로 가치를 창출하는 핵심군 |
| **Steady** | -2.0 ~ +2.0 | **현상 유지 (보결)**: 표준 수준의 운영을 유지 중인 안정군 | | **Steady** | -2.0 ~ +2.0 | **안정 자산**: 표준 수준의 운영을 유지 중인 현상 유지군 |
| **Underperform** | -10.0 ~ -2.0 | **저성과 (마이너)**: 규모 대비 활력이 부족하여 리소스 투입 필요 | | **Underperform** | -10.0 ~ -2.0 | **저성과 자산**: 규모 대비 활력이 부족하여 가치 하락 중인 그룹 |
| **Liability** | -10.0 이하 | **가치 파괴 (방출)**: 시스템 가치를 훼손 중인 좀비 프로젝트. 타절 검토 시급 | | **Liability** | -10.0 이하 | **고위험 자산**: 시스템 가치를 훼손 중인 방치 프로젝트. 즉시 조치 필요 |
### 2.2 상태 예보 (AI Forecast) ### 2.2 운영 일관성 (OCI) 판정
최근 활동의 **가속도(Acceleration)**와 **관성(Momentum)**을 AI가 분석한 14일 뒤 전망입니다. * **정기적 (80%↑)**: 주 단위의 정기적 관리가 완벽히 이뤄지는 최우량 관리 상태.
* **안정적 (50~80%)**: 간헐적 정체는 있으나 전반적인 관리 리듬을 유지하는 상태.
* **성장 가속 (Bullish)**: 활동 에너지가 증가 추세이며 가치가 오를 전망. * **간헐적 (20~50%)**: 관리 활동이 불규칙하며, 필요에 의한 일회성 작업 중심인 상태.
* **안정 유지 (Neutral)**: 현재의 안정적인 운영 리듬을 지속할 전망. * **불규칙 (20%↓)**: 장기 정체 중이거나 관리의 영속성을 확인하기 어려운 위험 상태.
* **활력 저하 (Bearish)**: 정체 징후 포착. 단기 내 가동률 하락 예상.
* **중단 위기 (Warning)**: 급격한 활동 저하로 인한 자산 소멸 위험 노출.
--- ---
@@ -61,7 +66,7 @@
* **Velocity**: 파일 수의 변화 속도 계산. * **Velocity**: 파일 수의 변화 속도 계산.
* **Acceleration**: 활동의 가속/감속 여부 판별. * **Acceleration**: 활동의 가속/감속 여부 판별.
* **Stagnation**: 마지막 활동 이후의 공백 기간 측정. * **Stagnation**: 마지막 활동 이후의 공백 기간 측정.
3. **AI 시뮬레이션**: 추출된 피처를 AAS(AI 위험 적응형 모델)에 입력하여 개별 프로젝트만의 **'위험 곡선'**을 생성합니다. 3. **AI 시뮬레이션**: 추출된 피처를 AI 위험 적응형 모델 (AAS)에 입력하여 개별 프로젝트만의 **'위험 곡선'**을 생성합니다.
4. **최종 판정**: AVI와 VCI를 결합하여 리더보드에 등급과 관리 가이드라인을 송출합니다. 4. **최종 판정**: AVI와 VCI를 결합하여 리더보드에 등급과 관리 가이드라인을 송출합니다.
--- ---

View File

@@ -1,13 +1,51 @@
import re import re
import math import math
import statistics import statistics
from datetime import datetime from datetime import datetime, timedelta
from sql_queries import DashboardQueries from sql_queries import DashboardQueries
from prediction_service import SOIPredictionService from prediction_service import SOIPredictionService
class AnalysisService: class AnalysisService:
"""프로젝트 통계 및 활동성 분석 전문 서비스""" """프로젝트 통계 및 활동성 분석 전문 서비스"""
@staticmethod
def calculate_operational_consistency(history_rows, days_stagnant):
"""운영 일관성 지수(OCI) 산출 로직 (장기 정체 패널티 포함)
최근 30일간 활동 리듬 분석 + 현재 방치 기간에 따른 강력한 감쇄
"""
if not history_rows or len(history_rows) < 2:
return 0.0
# 1. 최근 30일 이력 기반 Base Score 산출
now = datetime.now().date()
recent_30 = [h for h in history_rows if (now - h['crawl_date']).days <= 30]
# 주차별 활동 여부 (4주)
weeks_active = [False, False, False, False]
for h in recent_30:
days_ago = (now - h['crawl_date']).days
week_idx = min(3, days_ago // 7)
weeks_active[week_idx] = True
base_consistency = (sum(weeks_active) / 4) * 70
# 활동 밀도 (변화 발생일 비율)
effort_days = 0
for i in range(1, len(recent_30)):
if recent_30[i]['file_count'] != recent_30[i-1]['file_count']:
effort_days += 1
density_score = (effort_days / max(1, len(recent_30))) * 30
base_oci = base_consistency + density_score
# 2. [핵심] 장기 정체 패널티 적용
# 방치일이 100일 이상이면 OCI는 0점으로 수렴 (성실도 무효화)
stagnation_factor = max(0, (100 - days_stagnant) / 100.0)
final_oci = base_oci * stagnation_factor
return round(final_oci, 1)
@staticmethod @staticmethod
def calculate_activity_status(target_date_dt, log, file_count): def calculate_activity_status(target_date_dt, log, file_count):
"""개별 프로젝트의 활동 상태 및 방치일 산출""" """개별 프로젝트의 활동 상태 및 방치일 산출"""
@@ -59,7 +97,7 @@ class AnalysisService:
@staticmethod @staticmethod
def get_p_zsr_analysis_logic(cursor): def get_p_zsr_analysis_logic(cursor):
"""절대적 방치 실태 고발 및 AI 위험 적응형(AAS) 분석 로직""" """절대적 방치 실태 고발 및 운영 일관성(OCI) 분석 로직"""
cursor.execute(DashboardQueries.GET_LAST_CRAWL_DATE) cursor.execute(DashboardQueries.GET_LAST_CRAWL_DATE)
res_date = cursor.fetchone() res_date = cursor.fetchone()
if not res_date or not res_date['last_date']: if not res_date or not res_date['last_date']:
@@ -77,31 +115,12 @@ class AnalysisService:
if not projects: return [] if not projects: return []
# [Step 1] AI 전처리: 부서별 평균 방치일 계산 (조직적 위험도 산출용)
dept_stats = {}
for p in projects:
log = p['recent_log']
days = 14 # 기본값
if log and log != "데이터 없음":
match = re.search(r'(\d{4})\.(\d{2})\.(\d{2})', log)
if match:
log_date = datetime.strptime(match.group(0), "%Y.%m.%d").date()
days = (last_date - log_date).days
dept = p['department'] or "미분류"
if dept not in dept_stats: dept_stats[dept] = []
dept_stats[dept].append(days)
dept_avg_risk = {d: statistics.mean(days_list) for d, days_list in dept_stats.items()}
# [Step 2] AI 위험 적응형 SOI 산출 (AAS 모델)
results = [] results = []
total_soi = 0 total_soi = 0
for p in projects: for p in projects:
file_count = int(p['file_count']) if p['file_count'] else 0 file_count = int(p['file_count']) if p['file_count'] else 0
log = p['recent_log'] log = p['recent_log']
dept = p['department'] or "미분류"
# 방치일 계산 # 방치일 계산
days_stagnant = 14 days_stagnant = 14
@@ -114,52 +133,33 @@ class AnalysisService:
is_auto_delete = log and "폴더자동삭제" in log.replace(" ", "") is_auto_delete = log and "폴더자동삭제" in log.replace(" ", "")
# AI-Hazard 추론 로직 (Dynamic Lambda) # AI-Hazard 추론 로직 (Dynamic Lambda)
# 1. 자산 규모 리스크 (파일이 많을수록 방치 시 가치 하락 가속)
scale_impact = min(0.04, math.log10(file_count + 1) * 0.008) if file_count > 0 else 0 scale_impact = min(0.04, math.log10(file_count + 1) * 0.008) if file_count > 0 else 0
ai_lambda = 0.04 + scale_impact
# 2. 조직적 전염 리스크 (부서 전체가 방치 중이면 패널티 부여) # 지수 감쇄 적용
dept_risk_days = dept_avg_risk.get(dept, 14)
env_impact = min(0.03, (dept_risk_days / 30) * 0.01)
# 최종 AI 위험 계수 산출 (기본 0.04에서 변동)
ai_lambda = 0.04 + scale_impact + env_impact
# 지수 감쇄 적용 (AAS Score)
soi_score = math.exp(-ai_lambda * days_stagnant) * 100 soi_score = math.exp(-ai_lambda * days_stagnant) * 100
# [AI 데이터 진정성 검증 로직 1 - ECV 패널티 (존재론적)] # ECV 패널티
existence_confidence = 1.0 existence_confidence = 1.0
if file_count == 0: if file_count == 0: existence_confidence = 0.05
existence_confidence = 0.05 elif file_count < 10: existence_confidence = 0.4
elif file_count < 10:
existence_confidence = 0.4
# [AI 데이터 진정성 검증 로직 2 - Log Quality Scoring (활동의 질)] # Log Quality Scoring
log_quality_factor = 1.0 log_quality_factor = 1.0
if log and log != "데이터 없음": if log and log != "데이터 없음":
# 성과 중심 (High) if any(k in log for k in ["업로드", "수정", "등록", "변환", "파일", "업데이트"]): log_quality_factor = 1.0
if any(k in log for k in ["업로드", "수정", "등록", "변환", "파일", "업데이트"]): elif any(k in log for k in ["폴더", "생성", "삭제", "이동"]): log_quality_factor = 0.7
log_quality_factor = 1.0 elif any(k in log for k in ["참가자", "권한", "추가", "변경", "메일"]): log_quality_factor = 0.4
# 구조 관리 (Mid) else: log_quality_factor = 0.6
elif any(k in log for k in ["폴더", "생성", "삭제", "이동"]):
log_quality_factor = 0.7
# 단순 행정/설정 (Low)
elif any(k in log for k in ["참가자", "권한", "추가", "변경", "메일"]):
log_quality_factor = 0.4
else:
log_quality_factor = 0.6 # 기타 일반 로그
# 최종 점수 산출 (AAS * ECV * LogQuality)
soi_score = soi_score * existence_confidence * log_quality_factor soi_score = soi_score * existence_confidence * log_quality_factor
if is_auto_delete: soi_score = 0.1
if is_auto_delete: # [운영 일관성 분석 (OCI)]
soi_score = 0.1
# [AI 미래 예측 및 실무 투입 에너지 분석]
history_rows = SOIPredictionService.get_historical_soi(cursor, p['project_id']) history_rows = SOIPredictionService.get_historical_soi(cursor, p['project_id'])
predicted_soi = SOIPredictionService.predict_future_soi(soi_score, history_rows, days_ahead=14) oci_score = AnalysisService.calculate_operational_consistency(history_rows, days_stagnant)
# 실무 투입 에너지 계산 (최근 30개 히스토리 기준 파일 변화일수) # 실무 투입 에너지 계산
effort_days = 0 effort_days = 0
if len(history_rows) > 1: if len(history_rows) > 1:
for i in range(1, len(history_rows)): for i in range(1, len(history_rows)):
@@ -167,28 +167,26 @@ class AnalysisService:
effort_days += 1 effort_days += 1
work_effort_rate = round((effort_days / max(1, len(history_rows))) * 100, 1) work_effort_rate = round((effort_days / max(1, len(history_rows))) * 100, 1)
total_soi += soi_score total_soi += soi_score
# [최종 세이버메트릭스 보정: P-WAR+ (Adjusted Score)] # VCI 산출
# 절대 기준선(Replacement Level): 70.0% (이 이하는 자산 가치 파괴로 간주)
REPLACEMENT_LEVEL = 70.0 REPLACEMENT_LEVEL = 70.0
asset_weight = (file_count / 200.0) + 0.5 # 파일 100개당 0.5배 가중 (최소 0.5배) asset_weight = (file_count / 200.0) + 0.5
p_war_score = (soi_score - REPLACEMENT_LEVEL) * asset_weight p_war_score = (soi_score - REPLACEMENT_LEVEL) * asset_weight
results.append({ results.append({
"project_nm": p['short_nm'] or p['project_nm'], "project_nm": p['short_nm'] or p['project_nm'],
"file_count": file_count, "file_count": file_count,
"days_stagnant": days_stagnant, "days_stagnant": days_stagnant,
"risk_count": round(p_war_score, 2), # P-WAR+ 절대 기여도 점수 (평균의 함정 극복용) "risk_count": round(p_war_score, 2),
"p_war": round(soi_score, 1), "p_war": round(soi_score, 1),
"predicted_soi": predicted_soi, "oci_score": oci_score, # 운영 일관성 지수 추가
"is_auto_delete": is_auto_delete, "is_auto_delete": is_auto_delete,
"master": p['master'], "master": p['master'],
"dept": p['department'], "dept": p['department'],
"ai_lambda": round(ai_lambda, 4), "ai_lambda": round(ai_lambda, 4),
"log_quality": log_quality_factor, "log_quality": log_quality_factor,
"work_effort": work_effort_rate, # 신규 지표 추가 "work_effort": work_effort_rate,
"avg_info": { "avg_info": {
"avg_files": 0, "avg_files": 0,
"avg_stagnant": 0, "avg_stagnant": 0,

View File

@@ -1,6 +1,7 @@
/** /**
* Project Master Analysis JS * Project Master Analysis JS
* AVI (Activity Vitality Index) & VCI (Value Contribution Index) 분석 엔진 * AVI (Activity Vitality Index) & VCI (Value Contribution Index) 분석 엔진
* OCI (Operational Consistency Index) 통합 버전
*/ */
// Chart.js 플러그인 전역 등록 // Chart.js 플러그인 전역 등록
@@ -33,19 +34,18 @@ async function loadProjectAnalysisData() {
} }
function getStatusInfo(avi, isAutoDelete) { 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 < 30) return { label: '위험 노출', class: 'badge-danger', key: 'danger' };
if (avi < 70) return { label: '관리 주의', class: 'badge-warning', key: 'warning' }; if (avi < 70) return { label: '관리 주의', class: 'badge-warning', key: 'warning' };
return { label: '정상 운영', class: 'badge-active', key: 'active' }; return { label: '정상 운영', class: 'badge-active', key: 'active' };
} }
// VCI 등급 판정 로직 (Sabermetrics WAR 등급 기준 응용)
function getVciGrade(vci) { function getVciGrade(vci) {
if (vci >= 10) return { label: 'Masterpiece', class: 'grade-mvp', desc: '시스템 가치를 견인하는 핵심 자산 (MVP급)' }; 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: 'Blue Chip', class: 'grade-allstar', desc: '꾸준한 활력으로 가치를 창출하는 우량 자산' };
if (vci >= -2) return { label: 'Steady', class: 'grade-starter', desc: '표준 수준의 현상 유지 (보결급)' }; if (vci >= -2) return { label: 'Steady', class: 'grade-starter', desc: '표준 수준의 운영을 유지 중인 안정 자산' };
if (vci >= -10) return { label: 'Underperform', class: 'grade-bench', desc: '운영 미비로 인한 가치 하락 (마이너급)' }; if (vci >= -10) return { label: 'Underperform', class: 'grade-bench', desc: '규모 대비 활력 부족으로 가치 하락 중인 자산' };
return { label: 'Liability', class: 'grade-out', desc: '가치를 훼손 중인 방치 자산 (방출급)' }; return { label: 'Liability', class: 'grade-out', desc: '가치를 훼손 중인 고위험 방치 자산' };
} }
function renderValueCharts(data) { function renderValueCharts(data) {
@@ -65,7 +65,7 @@ function renderValueCharts(data) {
window.myStatusChart = new Chart(statusCtx, { window.myStatusChart = new Chart(statusCtx, {
type: 'doughnut', type: 'doughnut',
data: { data: {
labels: ['정상 운영', '관리 주의', '위험 노출', '중단/방치'], labels: ['정상 운영', '관리 주의', '위험 노출', '사망'],
datasets: [{ datasets: [{
data: [stats.active.length, stats.warning.length, stats.danger.length, stats.dead.length], data: [stats.active.length, stats.warning.length, stats.danger.length, stats.dead.length],
backgroundColor: ['#1E5149', '#22c55e', '#f59e0b', '#ef4444'], backgroundColor: ['#1E5149', '#22c55e', '#f59e0b', '#ef4444'],
@@ -84,20 +84,19 @@ function renderValueCharts(data) {
onClick: (e, elements) => { onClick: (e, elements) => {
if (elements.length > 0) { if (elements.length > 0) {
const idx = elements[0].index; 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); } } catch (err) { console.error("도넛 차트 에러:", err); }
// 2. 전략적 자산 매트릭스 (Scatter) // 2. 전략적 자산 매트릭스 (Scatter) - 정밀 복구
try { try {
const sortedByAVI = [...data].sort((a, b) => b.p_war - a.p_war); const sortedByAVI = [...data].sort((a, b) => b.p_war - a.p_war);
const top5Ids = sortedByAVI.slice(0, 5).map(p => p.project_nm); const top5Ids = sortedByAVI.slice(0, 5).map(p => p.project_nm);
const bottom5Ids = sortedByAVI.slice(-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 largeProjects = data.filter(p => p.file_count > 450).map(p => p.project_nm);
const vipProjectNames = new Set([...top5Ids, ...bottom5Ids, ...largeProjects]); const vipProjectNames = new Set([...top5Ids, ...bottom5Ids, ...largeProjects]);
const scatterData = data.map(p => { const scatterData = data.map(p => {
@@ -140,9 +139,8 @@ function renderValueCharts(data) {
scales: { scales: {
x: { x: {
type: 'linear', min: 0, max: 500, type: 'linear', min: 0, max: 500,
title: { display: true, text: '파일 수 (Files)', font: { size: 11, weight: '700' } }, title: { display: true, text: '자산 규모 (파일 수)', font: { size: 11, weight: '700' } },
grid: { display: false }, grid: { display: false }
ticks: { stepSize: 125, callback: (v) => v >= 500 ? '500+' : v }
}, },
y: { y: {
min: 0, max: 100, min: 0, max: 100,
@@ -155,17 +153,14 @@ function renderValueCharts(data) {
datalabels: { datalabels: {
backgroundColor: 'rgba(255, 255, 255, 0.8)', backgroundColor: 'rgba(255, 255, 255, 0.8)',
borderRadius: 4, padding: 4, 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' }, font: { size: 10, weight: '800' },
color: '#1e293b',
formatter: (v) => v ? v.label : '', formatter: (v) => v ? v.label : '',
display: (ctx) => ctx.raw && ctx.raw.isVip, display: (ctx) => ctx.raw && ctx.raw.isVip,
clip: false clip: false
}, },
tooltip: { tooltip: {
callbacks: { 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>상태 판정</th> <th>상태 판정</th>
<th style="text-align:right;">가치 기여 (VCI) <button class="btn-help" onclick="event.stopPropagation(); openAnalysisModal('vci')">?</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>운영 활력 (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 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> </tr>
</thead> </thead>
<tbody> <tbody>
${sortedData.map((p, idx) => { ${sortedData.map((p, idx) => {
const status = getStatusInfo(p.p_war, p.is_auto_delete); 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 rowId = `project-${idx}`;
const vci = p.risk_count || 0;
const avi = p.p_war || 0;
const grade = getVciGrade(vci); 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 ` return `
<tr class="project-row ${status.key === 'danger' ? 'row-danger' : status.key === 'warning' ? 'row-warning' : ''}" onclick="toggleProjectDetail('${rowId}')"> <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 class="font-bold">${p.project_nm}</td>
<td>${p.file_count.toLocaleString()}</td> <td>${p.file_count.toLocaleString()}</td>
<td>${p.days_stagnant}</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'};"> <td style="text-align:right; font-weight:800; color:${vci >= 0 ? '#059669' : '#dc2626'};">
${vci > 0 ? '+' : ''}${vci.toFixed(1)} ${vci > 0 ? '+' : ''}${vci.toFixed(1)}
</td> </td>
@@ -241,22 +258,27 @@ function renderVitalityLeaderboard(data) {
</div> </div>
</div> </div>
</td> </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>
<tr id="detail-${rowId}" class="detail-row"> <tr id="detail-${rowId}" class="detail-row">
<td colspan="8"> <td colspan="8">
<div class="detail-container"> <div class="detail-container">
<div class="formula-explanation-card"> <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 style="display: flex; gap: 20px; margin-bottom: 20px;">
<div class="work-effort-section" style="flex: 1; margin-bottom: 0;"> <div class="work-effort-section" style="flex: 1; margin-bottom: 0;">
<div class="work-effort-header"> <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> <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>
<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 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>
<div style="flex: 1; background: #f8fafc; border-radius: 8px; padding: 16px; border: 1px solid #e2e8f0; display: flex; align-items: center; gap: 15px;"> <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="text-align: center;">
@@ -271,32 +293,52 @@ function renderVitalityLeaderboard(data) {
<div class="formula-step"> <div class="formula-step">
<div class="step-num">1</div> <div class="step-num">1</div>
<div class="step-content"> <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 style="font-size:11px; color:#64748b; margin-bottom:5px;">프로젝트 규모가 클수록 정보 망실 시의 충격을 반영하여 데이터의 하락 속도가 가속됩니다. 현재 <b>λ=${p.ai_lambda.toFixed(4)}</b> .</div>
<div class="math-logic">λ = <span class="highlight-var">${p.ai_lambda.toFixed(4)}</span></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">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> </div>
</div> </div>
<div class="formula-step"> <div class="formula-step">
<div class="step-num">4</div> <div class="step-num">4</div>
<div class="step-content"> <div class="step-content">
<div class="step-title">가치 기여 영향력 (VCI)</div> <div class="step-title">활동 품질 검증 (Quality)</div>
<div class="math-logic">VCI = <span class="highlight-val">${vci.toFixed(1)}</span></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> </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> </div>
</div> </div>
@@ -317,6 +359,8 @@ function toggleProjectDetail(rowId) {
if (!detailRow.classList.contains('active')) { if (!detailRow.classList.contains('active')) {
document.querySelectorAll('.detail-row').forEach(row => row.classList.remove('active')); document.querySelectorAll('.detail-row').forEach(row => row.classList.remove('active'));
detailRow.classList.add('active'); detailRow.classList.add('active');
// 정밀 스크롤 이동 로직 복구
setTimeout(() => { setTimeout(() => {
const headerH = container.querySelector('thead').offsetHeight || 45; const headerH = container.querySelector('thead').offsetHeight || 45;
const targetScrollTop = mainRow.offsetTop - headerH; const targetScrollTop = mainRow.offsetTop - headerH;
@@ -333,11 +377,11 @@ function openProjectListModal(label, projects) {
const title = document.getElementById('modalTitle'); const title = document.getElementById('modalTitle');
const body = document.getElementById('modalBody'); const body = document.getElementById('modalBody');
title.innerText = `[${label}] 프로젝트 리스트 (${projects.length}건)`; 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;"> <div class="table-scroll-wrapper" style="max-height: 400px;">
<table class="data-table"> <table class="data-table">
<thead><tr><th>프로젝트명</th><th>부서</th><th>관리자</th><th>정체일</th><th>활력(AVI)</th></tr></thead> <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.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> <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> </table>
</div> </div>
<div class="modal-footer"><button class="btn btn-primary" onclick="closeAnalysisModal()">닫기</button></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>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>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~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> </tbody>
</table> </table>
<div class="modal-footer"><button class="btn btn-primary" onclick="closeAnalysisModal()">닫기</button></div>`; <div class="modal-footer"><button class="btn btn-primary" onclick="closeAnalysisModal()">닫기</button></div>`;
} else if (type === 'vci') { } else if (type === 'vci') {
title.innerText = '자산 가치 기여도 (VCI) 등급 가이드'; title.innerText = '자산 가치 기여도 (VCI) 등급 가이드';
body.innerHTML = ` 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> <p style="font-size:13px; color:#64748b; margin-bottom:15px;">운영 표준(AVI 70%) 대비 자산 가치 기여도에 따른 프로젝트 위상 분류입니다.</p>
<table class="data-table" style="font-size:12px;"> <table class="data-table" style="font-size:12px;">
<thead><tr style="background:#f8fafc;"><th>점수 (VCI)</th><th>등급</th><th>운영 의미</th></tr></thead> <thead><tr style="background:#f8fafc;"><th>점수 (VCI)</th><th>등급</th><th>운영 의미</th></tr></thead>
<tbody> <tbody>
<tr><td>+10.0↑</td><td style="font-weight:900; color:#6366f1;">Masterpiece</td><td>시스템 가치를 견인하는 핵심 자산 (MVP급)</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 ~ +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>-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 ~ -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:#dc2626;">Liability</td><td>가치를 훼손 중인 고위험 방치 자산</td></tr>
</tbody> </tbody>
</table> </table>
<div class="modal-footer"><button class="btn btn-primary" onclick="closeAnalysisModal()">닫기</button></div>`; <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) 등급 가이드'; title.innerText = '업무 집중도 (Job Focus) 등급 가이드';
body.innerHTML = ` 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;"> <table class="data-table" style="font-size:12px;">
<thead><tr style="background:#f8fafc;"><th>비율 (%)</th><th>등급</th><th>활동 성격</th></tr></thead> <thead><tr style="background:#f8fafc;"><th>비율 (%)</th><th>등급</th><th>활동 성격</th></tr></thead>
<tbody> <tbody>
@@ -394,29 +456,6 @@ function openAnalysisModal(type) {
</tbody> </tbody>
</table> </table>
<div class="modal-footer"><button class="btn btn-primary" onclick="closeAnalysisModal()">닫기</button></div>`; <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'; modal.style.display = 'flex';
} }

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;">상태 판정</th> <th style="position: sticky; top: 0; z-index: 10;">상태 판정</th>
<th style="position: sticky; top: 0; z-index: 10;"> <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>
<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;"> <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> </th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
${sortedData.map((p, idx) => { ${sortedData.map((p, idx) => {
const status = getStatusInfo(p.p_war, p.is_auto_delete); const status = getStatusInfo(p.p_war, p.is_auto_delete);
const soi = p.p_war; const avi = p.p_war;
const pred = p.predicted_soi; const vci = p.risk_count;
const oci = p.oci_score || 0;
const rowId = `project-${idx}`; const rowId = `project-${idx}`;
let trendIcon = ""; let rhythmLabel = "";
if (pred !== null) { let rhythmColor = "";
const diff = pred - soi; if (oci >= 80) { rhythmLabel = "정기적"; rhythmColor = "#059669"; }
if (diff < -5) trendIcon = '<span style="color:#ef4444; font-size:10px;">▼ 급락</span>'; else if (oci >= 50) { rhythmLabel = "안정적"; rhythmColor = "#1e5149"; }
else if (diff < 0) trendIcon = '<span style="color:#f59e0b; font-size:10px;">↘ 하락</span>'; else if (oci >= 20) { rhythmLabel = "간헐적"; rhythmColor = "#f59e0b"; }
else trendIcon = '<span style="color:#22c55e; font-size:10px;">↗ 유지</span>'; 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 qualityLabel = p.log_quality >= 1.0 ? '성과물 직결 <b>실무 활동</b>' : p.log_quality >= 0.7 ? '시스템 <b>구조적 활동</b>' : '단순 <b>행정적 활동</b>';
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 ` return `
<tr class="project-row ${status.key === 'danger' ? 'row-danger' : status.key === 'warning' ? 'row-warning' : ''}" <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.file_count.toLocaleString()}개</td>
<td>${p.days_stagnant}일</td> <td>${p.days_stagnant}일</td>
<td><span class="${status.class}">${status.label}</span></td> <td><span class="${status.class}">${status.label}</span></td>
<td class="p-war-value ${soi >= 70 ? 'text-plus' : 'text-minus'}"> <td class="p-war-value ${avi >= 70 ? 'text-plus' : 'text-minus'}">
${soi.toFixed(1)}% ${avi.toFixed(1)}%
</td>
<td style="text-align:right; font-weight:700; color:${vci >= 0 ? '#059669' : '#dc2626'};">
${vci >= 0 ? '+' : ''}${vci.toFixed(2)}
</td> </td>
<td style="text-align:center;"> <td style="text-align:center;">
<div style="display:flex; flex-direction:column; align-items:center; gap:4px;"> <div style="display:flex; flex-direction:column; align-items:center; gap:4px;">
@@ -70,27 +80,29 @@ function renderPWarLeaderboard(data) {
</td> </td>
<td style="text-align:center;"> <td style="text-align:center;">
<div style="display:flex; align-items:center; justify-content:center; gap:8px;"> <div style="display:flex; align-items:center; justify-content:center; gap:8px;">
<span style="font-weight:700; font-size:14px; color:#6366f1;"> <span style="font-weight:800; font-size:13px; color:${rhythmColor};">
${pred !== null ? pred.toFixed(1) + '%' : '-'} ${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> </span>
${trendIcon}
</div> </div>
</td> </td>
</tr> </tr>
<tr id="detail-${rowId}" class="detail-row"> <tr id="detail-${rowId}" class="detail-row">
<td colspan="7"> <td colspan="8">
<div class="detail-container"> <div class="detail-container">
<div class="formula-explanation-card"> <div class="formula-explanation-card">
<div style="font-size: 13px; font-weight: 700; color: #6366f1; margin-bottom: 15px;"> <div style="font-size: 13px; font-weight: 700; color: #6366f1; margin-bottom: 15px;">
⚙️ AI 위험 적응형 모델(AAS) 산출 시뮬레이션 ⚙️ AI 위험 적응형 모델(AAS) 산출 시뮬레이션
</div> </div>
<!-- 투입 분석 (상단 배치) --> <!-- 집중도 분석 (상단 배치) -->
<div style="background: #f8fafc; padding: 15px; border-radius: 8px; margin-bottom: 20px; border: 1px solid #eef2f6;"> <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;"> <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'};"> <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> </span>
</div> </div>
<div style="width: 100%; height: 6px; background: #e2e8f0; border-radius: 3px; overflow: hidden; margin-bottom: 10px;"> <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>
</div> </div>
<!-- 수식 단계 2x2 그리드 (1-4, 2-3 순서) --> <!-- 수식 단계 2x2 그리드 -->
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px;"> <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px;">
<!-- Row 1: Step 1 & Step 4 -->
<div class="formula-step"> <div class="formula-step">
<div class="step-num">1</div> <div class="step-num">1</div>
<div class="step-content"> <div class="step-content">
<div class="step-title">동적 위험 계수(λ) 산출</div> <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 class="math-logic">λ = <span class="highlight-var">${p.ai_lambda.toFixed(4)}</span></div>
</div> </div>
</div> </div>
@@ -117,34 +129,40 @@ function renderPWarLeaderboard(data) {
<div class="step-content"> <div class="step-content">
<div class="step-title">활동 품질 검증 (Quality)</div> <div class="step-title">활동 품질 검증 (Quality)</div>
<div class="step-desc" style="font-size:11px; margin-bottom:5px;"> <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>
<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="math-logic">Factor = <span class="${p.log_quality < 0.7 ? 'highlight-penalty' : 'highlight-val'}">${(p.log_quality * 100).toFixed(0)}%</span></div>
</div> </div>
</div> </div>
<!-- Row 2: Step 2 & Step 3 -->
<div class="formula-step"> <div class="formula-step">
<div class="step-num">2</div> <div class="step-num">2</div>
<div class="step-content"> <div class="step-content">
<div class="step-title">방치 시간 감쇄 적용</div> <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> </div>
<div class="formula-step"> <div class="formula-step">
<div class="step-num">3</div> <div class="step-num">3</div>
<div class="step-content"> <div class="step-content">
<div class="step-title">존재 진정성 (ECV)</div> <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 class="math-logic">Factor = <span class="${ecvClass}">${ecvText}</span></div>
</div> </div>
</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="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> <div>
<span style="font-size: 13px; color: #64748b; font-weight: 700;">최종 P-SOI: </span> <span style="font-size: 13px; color: #64748b; font-weight: 700;">최종 AVI: </span>
<span style="color: #1e5149; font-size: 22px; font-weight: 900;">${soi.toFixed(1)}%</span> <span style="color: #1e5149; font-size: 22px; font-weight: 900;">${avi.toFixed(1)}%</span>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -204,7 +204,7 @@
.step-title { font-size: 12px; font-weight: 700; color: var(--text-main); margin-bottom: 4px; } .step-title { font-size: 12px; font-weight: 700; color: var(--text-main); margin-bottom: 4px; }
.math-logic { font-family: 'Consolas', monospace; background: var(--bg-muted); padding: 4px 8px; border-radius: 4px; font-weight: 700; color: var(--text-main); font-size: 12px; display: inline-block; } .math-logic { font-family: 'Consolas', monospace; background: var(--bg-muted); padding: 4px 8px; border-radius: 4px; font-weight: 700; color: var(--text-main); font-size: 12px; display: inline-block; }
.final-result-area { margin-top: 20px; padding-top: 15px; border-top: 2px solid var(--primary-color); display: flex; justify-content: space-between; align-items: center; } .final-result-area { margin-top: 20px; padding-top: 15px; display: flex; justify-content: space-between; align-items: center; }
/* Modal Analysis Specific */ /* Modal Analysis Specific */
.modal-footer { .modal-footer {

View File

@@ -87,6 +87,7 @@ button { cursor: pointer; border: none; transition: all 0.2s ease; }
.nav-item { .nav-item {
padding: 4px 12px; border-radius: var(--radius-sm); padding: 4px 12px; border-radius: var(--radius-sm);
color: rgba(255, 255, 255, 0.8); font-size: 14px; color: rgba(255, 255, 255, 0.8); font-size: 14px;
cursor: pointer;
} }
.nav-item:hover { background: var(--primary-lv-8); color: #fff; } .nav-item:hover { background: var(--primary-lv-8); color: #fff; }
.nav-item.active { background: var(--primary-lv-0); color: var(--primary-color) !important; font-weight: 700; } .nav-item.active { background: var(--primary-lv-0); color: var(--primary-color) !important; font-weight: 700; }

View File

@@ -43,10 +43,10 @@ header {
display: flex; flex-direction: column; justify-content: center; gap: 2px; border-left: 5px solid transparent; display: flex; flex-direction: column; justify-content: center; gap: 2px; border-left: 5px solid transparent;
} }
.activity-card:hover { transform: translateY(-2px); box-shadow: var(--box-shadow); } .activity-card:hover { transform: translateY(-2px); box-shadow: var(--box-shadow); }
.activity-card.active { background: #e8f5e9; border-left-color: #4DB251; } .activity-card.active { background: #e8f5e9; }
.activity-card.warning { background: #fff8e1; border-left-color: #FFBF00; } .activity-card.warning { background: #fff8e1; }
.activity-card.stale { background: #ffebee; border-left-color: var(--error-color); } .activity-card.stale { background: #ffebee; }
.activity-card.unknown { background: #f5f5f5; border-left-color: #9e9e9e; } .activity-card.unknown { background: #f5f5f5; }
.activity-card .label { font-size: 11px; font-weight: 600; opacity: 0.7; } .activity-card .label { font-size: 11px; font-weight: 600; opacity: 0.7; }
.activity-card .count { font-size: 20px; font-weight: 800; } .activity-card .count { font-size: 20px; font-weight: 800; }
@@ -98,7 +98,7 @@ header {
.detail-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 32px; } .detail-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 32px; }
.detail-section h4 { .detail-section h4 {
font-size: 13px; margin-bottom: 12px; color: var(--text-main); font-size: 13px; margin-bottom: 12px; color: var(--text-main);
border-left: 3px solid var(--primary-color); padding-left: 10px; font-weight: 700; padding-left: 10px; font-weight: 700;
} }
/* Personnel & Activity Tables */ /* Personnel & Activity Tables */

View File

@@ -49,17 +49,17 @@
.stat-value { font-size: 18px; font-weight: 700; color: #333; } .stat-value { font-size: 18px; font-weight: 700; color: #333; }
/* Status Border Colors */ /* Status Border Colors */
.stat-item.total { border-top: 3px solid #1e5149; } .stat-item.total { }
.stat-item.total .stat-value { color: #1e5149; } .stat-item.total .stat-value { color: #1e5149; }
.stat-item.complete { border-top: 3px solid #2e7d32; } .stat-item.complete { }
.stat-item.complete .stat-value { color: #2e7d32; } .stat-item.complete .stat-value { color: #2e7d32; }
.stat-item.working { border-top: 3px solid #1565c0; } .stat-item.working { }
.stat-item.working .stat-value { color: #1565c0; } .stat-item.working .stat-value { color: #1565c0; }
.stat-item.checking { border-top: 3px solid #ef6c00; } .stat-item.checking { }
.stat-item.checking .stat-value { color: #ef6c00; } .stat-item.checking .stat-value { color: #ef6c00; }
.stat-item.pending { border-top: 3px solid #673ab7; } .stat-item.pending { }
.stat-item.pending .stat-value { color: #673ab7; } .stat-item.pending .stat-value { color: #673ab7; }
.stat-item.unconfirmed { border-top: 3px solid #9e9e9e; } .stat-item.unconfirmed { }
.stat-item.unconfirmed .stat-value { color: #9e9e9e; } .stat-item.unconfirmed .stat-value { color: #9e9e9e; }
/* 3. Filters & Notice */ /* 3. Filters & Notice */
@@ -185,7 +185,6 @@
.detail-container { .detail-container {
padding: 24px; padding: 24px;
border-left: 6px solid #1e5149;
background: #f9fafb; background: #f9fafb;
box-shadow: inset 0 4px 15px rgba(0,0,0,0.08); box-shadow: inset 0 4px 15px rgba(0,0,0,0.08);
position: relative; position: relative;
@@ -223,7 +222,7 @@
.detail-label { font-weight: 700; color: #888; margin-right: 8px; } .detail-label { font-weight: 700; color: #888; margin-right: 8px; }
.detail-q-section { background: #f8f9fa; padding: 20px; border-radius: 8px; } .detail-q-section { background: #f8f9fa; padding: 20px; border-radius: 8px; }
.detail-a-section { background: #f1f8f7; padding: 20px; border-radius: 8px; border-left: 5px solid #1e5149; } .detail-a-section { background: #f1f8f7; padding: 20px; border-radius: 8px; }
/* 6. Image Preview & Foldable Section */ /* 6. Image Preview & Foldable Section */
.img-thumbnail { width: 32px; height: 32px; border-radius: 4px; object-fit: cover; border: 1px solid #ddd; cursor: pointer; transition: transform 0.2s; } .img-thumbnail { width: 32px; height: 32px; border-radius: 4px; object-fit: cover; border: 1px solid #ddd; cursor: pointer; transition: transform 0.2s; }

View File

@@ -34,7 +34,7 @@
display: flex; align-items: flex-start; transition: 0.2s; display: flex; align-items: flex-start; transition: 0.2s;
} }
.mail-item:hover { background: var(--bg-muted); } .mail-item:hover { background: var(--bg-muted); }
.mail-item.active { background: var(--primary-lv-0); border-left: 4px solid var(--primary-color); } .mail-item.active { background: var(--primary-lv-0); }
.mail-item-checkbox { width: 16px; height: 16px; cursor: pointer; margin-right: 12px; margin-top: 2px; } .mail-item-checkbox { width: 16px; height: 16px; cursor: pointer; margin-right: 12px; margin-top: 2px; }
.mail-item-content { flex: 1; min-width: 0; } .mail-item-content { flex: 1; min-width: 0; }

View File

@@ -69,8 +69,8 @@
<p>규모를 감지하여, 대형 프로젝트 정체 시 데이터 가치 하락 속도를 <strong>가속(Acceleration)</strong>시킵니다.</p> <p>규모를 감지하여, 대형 프로젝트 정체 시 데이터 가치 하락 속도를 <strong>가속(Acceleration)</strong>시킵니다.</p>
</div> </div>
<div class="soi-info-column"> <div class="soi-info-column">
<h6>2. 조직적 위험 전염</h6> <h6>2. 활동 시계열 관성 분석</h6>
<p>소속 부서의 전반적인 활력이 낮을 경우, 개별 위험 지수를 상향 조정하여 <strong>시스템적 붕괴</strong>를 예보합니다.</p> <p>최근 활동의 연속성을 분석하여, 단기 정체 시에도 과거의 <strong>운영 모멘텀</strong>을 반영하여 지수를 보정합니다.</p>
</div> </div>
<div class="soi-info-column"> <div class="soi-info-column">
<h6>3. 동적 가치 계수</h6> <h6>3. 동적 가치 계수</h6>
@@ -98,7 +98,7 @@
<div class="card-header"> <div class="card-header">
<div style="display: flex; flex-direction: column; gap: 4px;"> <div style="display: flex; flex-direction: column; gap: 4px;">
<h4>Project Activity Vitality Leaderboard (AVI Status)</h4> <h4>Project Activity Vitality Leaderboard (AVI Status)</h4>
<p style="font-size: 11px; color: #888; margin: 0;">운영 표준(AVI 70%) 대비 파일 보존율 및 미래 가치 기여 리더보드</p> <p style="font-size: 11px; color: #888; margin: 0;">운영 표준(AVI 70%) 대비 운영 활력 및 VCI 기여 리더보드</p>
</div> </div>
<div class="card-tools"> <div class="card-tools">
<span id="avg-system-info" style="font-size: 11px; color: #888;">* AVI (Activity Vitality Index)</span> <span id="avg-system-info" style="font-size: 11px; color: #888;">* AVI (Activity Vitality Index)</span>