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:
@@ -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를 결합하여 리더보드에 등급과 관리 가이드라인을 송출합니다.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
Binary file not shown.
@@ -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:
|
|
||||||
soi_score = 0.1
|
|
||||||
|
|
||||||
# [AI 미래 예측 및 실무 투입 에너지 분석]
|
# [운영 일관성 분석 (OCI)]
|
||||||
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,
|
||||||
|
|||||||
199
js/analysis.js
199
js/analysis.js
@@ -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';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|||||||
@@ -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 */
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user