feat: 세이버메트릭스 기반 프로젝트 자산 가치 분석 시스템 고도화 (AVI/VCI 도입)
- analysis_service.py: AVI 및 VCI(자산 기여도) 산출 로직 구현 - prediction_service.py: 정체 프로젝트 AI 예보 최적화 - js/analysis.js: 전략 매트릭스 및 VCI 등급 시스템 시각화 - templates/analysis.html: UI 용어 최신화 및 스타일 통합 - ANALYSIS_REPORT.md: 분석 지표 공식 및 가이드라인 상세 기술
This commit is contained in:
BIN
.gitignore
vendored
BIN
.gitignore
vendored
Binary file not shown.
@@ -1,49 +1,76 @@
|
|||||||
# 📊 Project Master Sabermetrics 분석 엔진 리포트
|
# 📊 시스템 운영 자산 가치 분석 보고서 (Sabermetrics Edition)
|
||||||
|
|
||||||
## 1. 개요 (Vision)
|
본 보고서는 야구의 통계 분석 기법인 **세이버메트릭스(Sabermetrics)**를 프로젝트 관리 시스템에 이식하여, 단순 활동량 측정을 넘어 **'실질적 자산 가치'**와 **'미래 운영 위험'**을 정밀 분석한 결과입니다.
|
||||||
본 시스템은 방대한 프로젝트 운영 데이터(파일 수, 활동 로그, 조직 정보)를 기반으로 **AI 기반 프로젝트 건강도(P-SOI)**를 산출합니다. 단순히 "살아있는가"를 넘어, "실무적으로 가치 있게 관리되고 있는가"를 정밀 진단하는 것이 목적입니다.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. P-SOI 산출 로직 (The Formula)
|
## 1. 핵심 분석 지표 정의 (Core Metrics)
|
||||||
|
|
||||||
### 2.1 기초 모델: 지수 감쇄 (Exponential Decay)
|
### 1.1 운영 활력 지수 (AVI, Activity Vitality Index)
|
||||||
프로젝트 정보의 가치는 관리 활동이 멈춘 시점부터 시간이 흐를수록 급격히 하락합니다.
|
프로젝트가 현재 얼마나 '살아서 숨 쉬고 있는가'를 나타내는 생존 지수입니다.
|
||||||
- **수식**: $SOI = 100 \times e^{-\lambda t}$
|
|
||||||
- **의미**: 14일 방치 시 가치가 약 50% 소실되는 현장 현실을 반영합니다.
|
|
||||||
|
|
||||||
### 2.2 고도화 1: AAS (AI-Hazard Adaptive SOI)
|
* **산출 공식**: $AVI = exp(-\lambda \times days) \times Quality \times 100$
|
||||||
프로젝트의 중요도와 주변 환경에 따라 하락 곡선의 기울기를 동적으로 조정합니다.
|
* **핵심 데이터**:
|
||||||
- **자산 규모 영향**: 파일 수가 많을수록 관리 부재 리스크가 크므로 AI가 하락 속도를 가속시킵니다.
|
* **정체 일수(days)**: 마지막 유의미한 파일 업데이트 이후 경과 시간.
|
||||||
- **조직 위험 전염**: 소속 부서나 담당자의 전체 SOI가 낮을 경우, 시스템적 붕괴 리스크 가중치를 부여합니다.
|
* **감쇄 계수($\lambda$)**: 자산 규모(파일 수)가 클수록, 소속 부서의 방치율이 높을수록 커지며 점수를 더 빠르게 하락시킵니다.
|
||||||
|
* **활동 품질(Quality)**: 단순 시스템 로그(단순 접속, 설정 변경)는 낮게 평가하고, 실질적인 파일 증분 활동에 가점을 부여합니다.
|
||||||
|
* **의미**: 100%에 가까울수록 실시간 가동 상태이며, 0%에 가까울수록 데이터 노후화가 완료된 '사망' 상태를 뜻합니다.
|
||||||
|
|
||||||
### 2.3 고도화 2: ECV (Existence-Conditioned Vitality)
|
### 1.2 자산 가치 기여도 (VCI, Value Contribution Index)
|
||||||
'빈 껍데기' 활동을 걸러내는 존재론적 패널티입니다.
|
시스템 전체의 운영 표준 대비, 해당 프로젝트가 기여하고 있는 가치의 상대적 하중을 측정합니다.
|
||||||
- **유령 프로젝트**: 파일 수가 0개인 경우, 최근 로그와 관계없이 SOI 점수를 **5% 미만**으로 강제 고정합니다.
|
|
||||||
- **신뢰 보정**: 파일 10개 미만의 소규모 프로젝트는 활동 신뢰도를 40% 수준으로 제한합니다.
|
|
||||||
|
|
||||||
### 2.4 고도화 3: 로그 품질 및 실무 투입 분석
|
* **산출 공식**: $VCI = (AVI - 70.0) \times (\frac{Files}{200} + 0.5)$
|
||||||
- **Log Quality**: 로그 텍스트를 분석하여 [실무 활동(1.0), 관리 활동(0.7), 행정 활동(0.4)] 가중치를 부여합니다.
|
* **핵심 로직**:
|
||||||
- **Work Effort**: 최근 30개 히스토리 중 실제 **파일 증감**이 발생한 날의 비율을 계산하여 실질 공수 투입률을 산출합니다.
|
* **건강 기준선(70.0%)**: 시스템 자산 가치를 유지하기 위한 최소 마지노선(Replacement Level)입니다.
|
||||||
|
* **규모 가중치**: 파일 수가 많은 대형 프로젝트일수록 동일한 방치 상황에서 시스템에 주는 충격(음수값)이 기하급수적으로 커집니다.
|
||||||
|
* **의미**: 양수(+)는 가치 창출, 음수(-)는 시스템 기회비용을 갉아먹는 '가치 파괴' 상태임을 나타냅니다.
|
||||||
|
|
||||||
|
### 1.3 업무 집중도 (Job Focus)
|
||||||
|
단순 관리 행위를 제외하고, 실제 성과물(파일)을 생산하는 데 얼마나 몰입했는지를 판별합니다.
|
||||||
|
|
||||||
|
* **산출 공식**: $Job Focus = \frac{\text{최근 30회 중 실질 파일 변동 발생 횟수}}{\text{전체 데이터 수집 횟수}} \times 100$
|
||||||
|
* **의미**: 로그만 남기는 '보여주기식 활동'을 필터링하여 운영의 진정성을 확인합니다.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. 전략적 분석 도구 (Visualization)
|
## 2. 등급 체계 및 관리 가이드 (Grade System)
|
||||||
|
|
||||||
### 3.1 프로젝트 SWOT 매트릭스
|
### 2.1 VCI 등급 (프로젝트 위상)
|
||||||
X축(자산 규모)과 Y축(활동성)을 결합하여 4가지 국면으로 프로젝트를 진단합니다.
|
| 등급 (Grade) | 점수 기준 | 운영 의미 및 관리 전략 |
|
||||||
1. **핵심 우량 (Strategic)**: 대규모 핵심 자산이며 활발히 관리 중.
|
| :--- | :--- | :--- |
|
||||||
2. **활동 양호 (Agile)**: 규모는 작으나 매우 탄력적으로 업데이트 중.
|
| **Masterpiece** | +10.0 이상 | **핵심 자산 (MVP)**: 시스템 가치를 견인하는 최우량 프로젝트 |
|
||||||
3. **방치/소규모**: 중요도가 낮고 방치된 상태.
|
| **Blue Chip** | +2.0 ~ +10.0 | **우량 자산 (주전)**: 꾸준한 활력으로 가치를 창출하는 핵심군 |
|
||||||
4. **관리 사각지대 (Critical Risk)**: **자산 규모는 크나 장기 방치됨 (최우선 관리 대상)**
|
| **Steady** | -2.0 ~ +2.0 | **현상 유지 (보결)**: 표준 수준의 운영을 유지 중인 안정군 |
|
||||||
|
| **Underperform** | -10.0 ~ -2.0 | **저성과 (마이너)**: 규모 대비 활력이 부족하여 리소스 투입 필요 |
|
||||||
|
| **Liability** | -10.0 이하 | **가치 파괴 (방출)**: 시스템 가치를 훼손 중인 좀비 프로젝트. 타절 검토 시급 |
|
||||||
|
|
||||||
### 3.2 AI 진단 아코디언 리포트
|
### 2.2 상태 예보 (AI Forecast)
|
||||||
사용자가 지수 산출 결과에 납득할 수 있도록, 개별 프로젝트 행 클릭 시 **4단계 AI 추론 과정**을 실시간 리포트로 제공합니다.
|
최근 활동의 **가속도(Acceleration)**와 **관성(Momentum)**을 AI가 분석한 14일 뒤 전망입니다.
|
||||||
|
|
||||||
|
* **성장 가속 (Bullish)**: 활동 에너지가 증가 추세이며 가치가 오를 전망.
|
||||||
|
* **안정 유지 (Neutral)**: 현재의 안정적인 운영 리듬을 지속할 전망.
|
||||||
|
* **활력 저하 (Bearish)**: 정체 징후 포착. 단기 내 가동률 하락 예상.
|
||||||
|
* **중단 위기 (Warning)**: 급격한 활동 저하로 인한 자산 소멸 위험 노출.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. 향후 딥러닝 로드맵 (Evolution)
|
## 3. 데이터 분석 프로세스 (Analysis Process)
|
||||||
데이터가 누적됨에 따라 다음과 같은 자가 학습형 엔진으로 진화합니다.
|
|
||||||
- **LSTM 기반 리듬 학습**: 각 프로젝트의 고유한 업데이트 주기와 패턴(Life Rhythm)을 인코딩하여 맞춤형 예보 수행.
|
1. **데이터 수집**: `projects_history` 테이블로부터 일별 파일 수 및 로그 텍스트를 추출합니다.
|
||||||
- **NLP 임베딩**: 단순 키워드를 넘어 로그 텍스트의 맥락적 의미를 딥러닝이 스스로 학습하여 가중치 자동 산정.
|
2. **피처 추출**:
|
||||||
- **병목 예측 AI**: 특정 담당자나 부서의 업무 과부하 패턴을 학습하여 집단 방치 위험을 선제적으로 예보.
|
* **Velocity**: 파일 수의 변화 속도 계산.
|
||||||
|
* **Acceleration**: 활동의 가속/감속 여부 판별.
|
||||||
|
* **Stagnation**: 마지막 활동 이후의 공백 기간 측정.
|
||||||
|
3. **AI 시뮬레이션**: 추출된 피처를 AAS(AI 위험 적응형 모델)에 입력하여 개별 프로젝트만의 **'위험 곡선'**을 생성합니다.
|
||||||
|
4. **최종 판정**: AVI와 VCI를 결합하여 리더보드에 등급과 관리 가이드라인을 송출합니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 관리자 제언 (Action Plan)
|
||||||
|
|
||||||
|
* **VCI 음수 프로젝트 집중 관리**: 단순 활동량이 아닌 VCI가 낮은 대형 프로젝트부터 우선적으로 인력을 배치하거나 운영 정책을 재점검해야 합니다.
|
||||||
|
* **AI Forecast 활용**: '활력 저하' 예보가 뜬 프로젝트는 실제 AVI가 급락하기 전 선제적인 조치(업무 독려, 파일 현행화)를 취할 수 있습니다.
|
||||||
|
* **파일 수와 활력의 균형**: 파일 수가 많은데 활력(AVI)이 낮은 경우, 시스템 전체의 데이터 무결성을 해칠 수 있으므로 데이터 클렌징이나 아카이빙을 권고합니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
*본 분석 엔진은 Project Master Sabermetrics 알고리즘에 의해 자동 생성되었습니다.*
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -170,11 +170,17 @@ class AnalysisService:
|
|||||||
|
|
||||||
total_soi += soi_score
|
total_soi += soi_score
|
||||||
|
|
||||||
|
# [최종 세이버메트릭스 보정: P-WAR+ (Adjusted Score)]
|
||||||
|
# 절대 기준선(Replacement Level): 70.0% (이 이하는 자산 가치 파괴로 간주)
|
||||||
|
REPLACEMENT_LEVEL = 70.0
|
||||||
|
asset_weight = (file_count / 200.0) + 0.5 # 파일 100개당 0.5배 가중 (최소 0.5배)
|
||||||
|
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": 0,
|
"risk_count": round(p_war_score, 2), # P-WAR+ 절대 기여도 점수 (평균의 함정 극복용)
|
||||||
"p_war": round(soi_score, 1),
|
"p_war": round(soi_score, 1),
|
||||||
"predicted_soi": predicted_soi,
|
"predicted_soi": predicted_soi,
|
||||||
"is_auto_delete": is_auto_delete,
|
"is_auto_delete": is_auto_delete,
|
||||||
|
|||||||
310
js/analysis.js
310
js/analysis.js
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Project Master Analysis JS
|
* Project Master Analysis JS
|
||||||
* P-WAR (Project Performance Above Replacement) 분석 엔진
|
* AVI (Activity Vitality Index) & VCI (Value Contribution Index) 분석 엔진
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Chart.js 플러그인 전역 등록
|
// Chart.js 플러그인 전역 등록
|
||||||
@@ -9,40 +9,49 @@ if (typeof ChartDataLabels !== 'undefined') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
console.log("Analysis engine initialized...");
|
console.log("Business Analysis Engine initialized...");
|
||||||
loadPWarData();
|
loadProjectAnalysisData();
|
||||||
});
|
});
|
||||||
|
|
||||||
async function loadPWarData() {
|
async function loadProjectAnalysisData() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/analysis/p-war');
|
const response = await fetch('/api/analysis/p-war');
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (data.error) throw new Error(data.error);
|
if (data.error) throw new Error(data.error);
|
||||||
|
|
||||||
renderPWarLeaderboard(data);
|
renderVitalityLeaderboard(data);
|
||||||
renderSOICharts(data);
|
renderValueCharts(data);
|
||||||
|
|
||||||
if (data.length > 0 && data[0].avg_info) {
|
if (data.length > 0 && data[0].avg_info) {
|
||||||
const avg = data[0].avg_info;
|
const avg = data[0].avg_info;
|
||||||
const infoEl = document.getElementById('avg-system-info');
|
const infoEl = document.getElementById('avg-system-info');
|
||||||
if (infoEl) infoEl.textContent = `* 시스템 종합 건강도: ${avg.avg_risk}% (0.0%에 가까울수록 방치 심각)`;
|
if (infoEl) infoEl.textContent = `* 시스템 종합 자산 건전도: ${avg.avg_risk}% (운영 표준 70.0% 대비)`;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("분석 데이터 로딩 실패:", e);
|
console.error("분석 데이터 로딩 실패:", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStatusInfo(soi, isAutoDelete) {
|
function getStatusInfo(avi, isAutoDelete) {
|
||||||
if (isAutoDelete || soi < 10) return { label: '사망', class: 'badge-system', key: 'dead' };
|
if (isAutoDelete || avi < 10) return { label: '중단/방치', class: 'badge-system', key: 'dead' };
|
||||||
if (soi < 30) return { label: '위험', class: 'badge-danger', key: 'danger' };
|
if (avi < 30) return { label: '위험 노출', class: 'badge-danger', key: 'danger' };
|
||||||
if (soi < 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' };
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderSOICharts(data) {
|
// VCI 등급 판정 로직 (Sabermetrics WAR 등급 기준 응용)
|
||||||
|
function getVciGrade(vci) {
|
||||||
|
if (vci >= 10) return { label: 'Masterpiece', class: 'grade-mvp', desc: '시스템 가치를 견인하는 핵심 자산 (MVP급)' };
|
||||||
|
if (vci >= 2) return { label: 'Blue Chip', class: 'grade-allstar', desc: '꾸준한 활력의 우량 자산 (주전급)' };
|
||||||
|
if (vci >= -2) return { label: 'Steady', class: 'grade-starter', desc: '표준 수준의 현상 유지 (보결급)' };
|
||||||
|
if (vci >= -10) return { label: 'Underperform', class: 'grade-bench', desc: '운영 미비로 인한 가치 하락 (마이너급)' };
|
||||||
|
return { label: 'Liability', class: 'grade-out', desc: '가치를 훼손 중인 방치 자산 (방출급)' };
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderValueCharts(data) {
|
||||||
if (!data || data.length === 0) return;
|
if (!data || data.length === 0) return;
|
||||||
|
|
||||||
// 1. 상태 분포 (Doughnut)
|
// 1. 운영 활력 분포 (Doughnut)
|
||||||
try {
|
try {
|
||||||
const stats = { active: [], warning: [], danger: [], dead: [] };
|
const stats = { active: [], warning: [], danger: [], dead: [] };
|
||||||
data.forEach(p => {
|
data.forEach(p => {
|
||||||
@@ -56,7 +65,7 @@ function renderSOICharts(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'],
|
||||||
@@ -75,53 +84,40 @@ function renderSOICharts(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. 프로젝트 SWOT 매트릭스 (Scatter)
|
// 2. 전략적 자산 매트릭스 (Scatter)
|
||||||
try {
|
try {
|
||||||
const scatterData = data.map(p => ({
|
const sortedByAVI = [...data].sort((a, b) => b.p_war - a.p_war);
|
||||||
x: Math.min(500, p.file_count),
|
const top5Ids = sortedByAVI.slice(0, 5).map(p => p.project_nm);
|
||||||
y: p.p_war,
|
const bottom5Ids = sortedByAVI.slice(-5).map(p => p.project_nm);
|
||||||
label: p.project_nm
|
const largeProjects = data.filter(p => p.file_count > 450).map(p => p.project_nm);
|
||||||
}));
|
|
||||||
|
const vipProjectNames = new Set([...top5Ids, ...bottom5Ids, ...largeProjects]);
|
||||||
|
|
||||||
|
const scatterData = data.map(p => {
|
||||||
|
const vci = p.risk_count || 0;
|
||||||
|
const absVci = Math.abs(vci);
|
||||||
|
return {
|
||||||
|
x: Math.min(500, p.file_count),
|
||||||
|
y: p.p_war,
|
||||||
|
label: p.project_nm,
|
||||||
|
isVip: vipProjectNames.has(p.project_nm),
|
||||||
|
vci: vci,
|
||||||
|
radius: Math.max(5, Math.min(25, 5 + (absVci / 10)))
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const vitalityCtx = document.getElementById('forecastChart').getContext('2d');
|
const vitalityCtx = document.getElementById('forecastChart').getContext('2d');
|
||||||
if (window.myVitalityChart) window.myVitalityChart.destroy();
|
if (window.myVitalityChart) window.myVitalityChart.destroy();
|
||||||
|
|
||||||
// 플러그인 통합 (Duplicate Key 방지)
|
|
||||||
const chartPlugins = [];
|
|
||||||
if (typeof ChartDataLabels !== 'undefined') chartPlugins.push(ChartDataLabels);
|
|
||||||
|
|
||||||
chartPlugins.push({
|
|
||||||
id: 'quadrants',
|
|
||||||
beforeDraw: (chart) => {
|
|
||||||
const { ctx, chartArea: { left, top, right, bottom }, scales: { x, y } } = chart;
|
|
||||||
const midX = x.getPixelForValue(250);
|
|
||||||
const midY = y.getPixelForValue(50);
|
|
||||||
ctx.save();
|
|
||||||
ctx.fillStyle = 'rgba(34, 197, 94, 0.03)'; ctx.fillRect(left, top, midX - left, midY - top);
|
|
||||||
ctx.fillStyle = 'rgba(30, 81, 73, 0.03)'; ctx.fillRect(midX, top, right - midX, midY - top);
|
|
||||||
ctx.fillStyle = 'rgba(148, 163, 184, 0.03)'; ctx.fillRect(left, midY, midX - left, bottom - midY);
|
|
||||||
ctx.fillStyle = 'rgba(239, 68, 68, 0.05)'; ctx.fillRect(midX, midY, right - midX, bottom - midY);
|
|
||||||
ctx.lineWidth = 2; ctx.strokeStyle = 'rgba(0,0,0,0.1)'; ctx.beginPath();
|
|
||||||
ctx.moveTo(midX, top); ctx.lineTo(midX, bottom); ctx.moveTo(left, midY); ctx.lineTo(right, midY); ctx.stroke();
|
|
||||||
ctx.font = 'bold 12px Pretendard'; ctx.textAlign = 'center'; ctx.fillStyle = 'rgba(0,0,0,0.2)';
|
|
||||||
ctx.fillText('활동 양호', (left + midX) / 2, (top + midY) / 2);
|
|
||||||
ctx.fillText('핵심 우량', (midX + right) / 2, (top + midY) / 2);
|
|
||||||
ctx.fillText('방치/소규모', (left + midX) / 2, (midY + bottom) / 2);
|
|
||||||
ctx.fillStyle = 'rgba(239, 68, 68, 0.4)'; ctx.fillText('관리 사각지대', (midX + right) / 2, (midY + bottom) / 2);
|
|
||||||
ctx.restore();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
window.myVitalityChart = new Chart(vitalityCtx, {
|
window.myVitalityChart = new Chart(vitalityCtx, {
|
||||||
type: 'scatter',
|
type: 'scatter',
|
||||||
plugins: chartPlugins,
|
|
||||||
data: {
|
data: {
|
||||||
datasets: [{
|
datasets: [{
|
||||||
data: scatterData,
|
data: scatterData,
|
||||||
@@ -133,45 +129,73 @@ function renderSOICharts(data) {
|
|||||||
if (p.x < 250 && p.y < 50) return '#94a3b8';
|
if (p.x < 250 && p.y < 50) return '#94a3b8';
|
||||||
return '#ef4444';
|
return '#ef4444';
|
||||||
},
|
},
|
||||||
pointRadius: 6,
|
pointRadius: (ctx) => ctx.raw ? ctx.raw.radius : 5,
|
||||||
hoverRadius: 10
|
hoverRadius: (ctx) => (ctx.raw ? ctx.raw.radius : 5) + 3
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
responsive: true,
|
responsive: true,
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
layout: { padding: { top: 30, right: 40, left: 10, bottom: 10 } },
|
layout: { padding: { top: 30, right: 45, left: 10, bottom: 10 } },
|
||||||
scales: {
|
scales: {
|
||||||
x: {
|
x: {
|
||||||
type: 'linear', min: 0, max: 500,
|
type: 'linear', min: 0, max: 500,
|
||||||
title: { display: true, text: '자산 규모 (파일 수)', font: { size: 11, weight: '700' } },
|
title: { display: true, text: '파일 수 (Files)', font: { size: 11, weight: '700' } },
|
||||||
grid: { display: false },
|
grid: { display: false },
|
||||||
ticks: { stepSize: 125, callback: (v) => v >= 500 ? '500+' : v }
|
ticks: { stepSize: 125, callback: (v) => v >= 500 ? '500+' : v }
|
||||||
},
|
},
|
||||||
y: {
|
y: {
|
||||||
min: 0, max: 100,
|
min: 0, max: 100,
|
||||||
title: { display: true, text: '활동성 (SOI %)', font: { size: 11, weight: '700' } },
|
title: { display: true, text: '운영 활력 (AVI %)', font: { size: 11, weight: '700' } },
|
||||||
grid: { display: false }
|
grid: { display: false }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
legend: { display: false },
|
legend: { display: false },
|
||||||
datalabels: {
|
datalabels: {
|
||||||
align: 'top', offset: 5, font: { size: 10, weight: '700' }, color: '#475569',
|
backgroundColor: 'rgba(255, 255, 255, 0.8)',
|
||||||
formatter: (v) => v.label,
|
borderRadius: 4, padding: 4,
|
||||||
display: (ctx) => ctx.raw.x > 100 || ctx.raw.y < 30,
|
align: (ctx) => (ctx.raw && ctx.raw.y > 80 ? 'bottom' : 'top'),
|
||||||
|
offset: (ctx) => (ctx.raw ? ctx.raw.radius : 5) + 2,
|
||||||
|
font: { size: 10, weight: '800' },
|
||||||
|
color: '#1e293b',
|
||||||
|
formatter: (v) => v ? v.label : '',
|
||||||
|
display: (ctx) => ctx.raw && ctx.raw.isVip,
|
||||||
clip: false
|
clip: false
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
callbacks: { label: (ctx) => ` [${ctx.raw.label}] SOI: ${ctx.raw.y.toFixed(1)}% | 파일: ${ctx.raw.x >= 500 ? '500+' : ctx.raw.x}개` }
|
callbacks: {
|
||||||
|
label: (ctx) => ` [${ctx.raw.label}] 활력(AVI): ${ctx.raw.y.toFixed(1)}% | 가치 기여(VCI): ${ctx.raw.vci.toFixed(1)}`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
plugins: [{
|
||||||
|
id: 'quadrants',
|
||||||
|
beforeDraw: (chart) => {
|
||||||
|
const { ctx, chartArea: { left, top, right, bottom }, scales: { x, y } } = chart;
|
||||||
|
const midX = x.getPixelForValue(250);
|
||||||
|
const midY = y.getPixelForValue(50);
|
||||||
|
ctx.save();
|
||||||
|
ctx.fillStyle = 'rgba(34, 197, 94, 0.03)'; ctx.fillRect(left, top, midX - left, midY - top);
|
||||||
|
ctx.fillStyle = 'rgba(30, 81, 73, 0.03)'; ctx.fillRect(midX, top, right - midX, midY - top);
|
||||||
|
ctx.fillStyle = 'rgba(148, 163, 184, 0.03)'; ctx.fillRect(left, midY, midX - left, bottom - midY);
|
||||||
|
ctx.fillStyle = 'rgba(239, 68, 68, 0.05)'; ctx.fillRect(midX, midY, right - midX, bottom - midY);
|
||||||
|
ctx.lineWidth = 2; ctx.strokeStyle = 'rgba(0,0,0,0.1)'; ctx.beginPath();
|
||||||
|
ctx.moveTo(midX, top); ctx.lineTo(midX, bottom); ctx.moveTo(left, midY); ctx.lineTo(right, midY); ctx.stroke();
|
||||||
|
ctx.font = 'bold 12px Pretendard'; ctx.textAlign = 'center'; ctx.fillStyle = 'rgba(0,0,0,0.2)';
|
||||||
|
ctx.fillText('활력 양호', (left + midX) / 2, (top + midY) / 2);
|
||||||
|
ctx.fillText('핵심 가치', (midX + right) / 2, (top + midY) / 2);
|
||||||
|
ctx.fillText('정체/소규모', (left + midX) / 2, (midY + bottom) / 2);
|
||||||
|
ctx.fillStyle = 'rgba(239, 68, 68, 0.4)'; ctx.fillText('자산 손실 위험', (midX + right) / 2, (midY + bottom) / 2);
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
}]
|
||||||
});
|
});
|
||||||
} catch (err) { console.error("SWOT 차트 에러:", err); }
|
} catch (err) { console.error("전략 매트릭스 에러:", err); }
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderPWarLeaderboard(data) {
|
function renderVitalityLeaderboard(data) {
|
||||||
const container = document.getElementById('p-war-table-container');
|
const container = document.getElementById('p-war-table-container');
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
const sortedData = [...data].sort((a, b) => a.p_war - b.p_war);
|
const sortedData = [...data].sort((a, b) => a.p_war - b.p_war);
|
||||||
@@ -181,29 +205,34 @@ function renderPWarLeaderboard(data) {
|
|||||||
<table class="data-table p-war-table">
|
<table class="data-table p-war-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width: 280px;">프로젝트명</th>
|
<th style="width: 250px;">프로젝트명</th>
|
||||||
<th>파일 수</th>
|
<th>파일 수</th>
|
||||||
<th>방치일</th>
|
<th>정체 일수</th>
|
||||||
<th>상태 판정</th>
|
<th>상태 판정</th>
|
||||||
<th>현재 SOI <button class="btn-help" onclick="event.stopPropagation(); openAnalysisModal('soi')">?</button></th>
|
<th style="text-align:right;">가치 기여 (VCI) <button class="btn-help" onclick="event.stopPropagation(); openAnalysisModal('vci')">?</button></th>
|
||||||
<th style="text-align:center;">실무 투입</th>
|
<th>활력 지수 (AVI) <button class="btn-help" onclick="event.stopPropagation(); openAnalysisModal('avi')">?</button></th>
|
||||||
<th>AI 예보 (14d) <button class="btn-help" onclick="event.stopPropagation(); openAnalysisModal('ai')">?</button></th>
|
<th style="text-align:center;">업무 집중도 <button class="btn-help" onclick="event.stopPropagation(); openAnalysisModal('focus')">?</button></th>
|
||||||
|
<th>상태 예보 (14d) <button class="btn-help" onclick="event.stopPropagation(); openAnalysisModal('ai')">?</button></th>
|
||||||
</tr>
|
</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 rowId = `project-${idx}`;
|
const rowId = `project-${idx}`;
|
||||||
const ecvText = p.file_count === 0 ? "5% (유령)" : p.file_count < 10 ? "40% (껍데기)" : "100% (신뢰)";
|
const vci = p.risk_count || 0;
|
||||||
const ecvClass = (p.file_count < 10) ? "highlight-penalty" : "highlight-val";
|
const avi = p.p_war || 0;
|
||||||
|
const grade = getVciGrade(vci);
|
||||||
|
|
||||||
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}</span></td>
|
<td><span class="${status.class}">${status.label === '사망' ? '중단' : status.label}</span></td>
|
||||||
<td class="p-war-value ${p.p_war >= 70 ? 'text-plus' : 'text-minus'}">${p.p_war.toFixed(1)}%</td>
|
<td style="text-align:right; font-weight:800; color:${vci >= 0 ? '#059669' : '#dc2626'};">
|
||||||
|
${vci > 0 ? '+' : ''}${vci.toFixed(1)}
|
||||||
|
</td>
|
||||||
|
<td class="p-war-value ${avi >= 70 ? 'text-plus' : 'text-minus'}">${avi.toFixed(1)}%</td>
|
||||||
<td style="text-align:center;">
|
<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;">
|
||||||
<span style="font-weight:800; font-size:12px; color:${p.work_effort >= 70 ? '#059669' : p.work_effort <= 30 ? '#dc2626' : '#6366f1'};">${p.work_effort}%</span>
|
<span style="font-weight:800; font-size:12px; color:${p.work_effort >= 70 ? '#059669' : p.work_effort <= 30 ? '#dc2626' : '#6366f1'};">${p.work_effort}%</span>
|
||||||
@@ -215,52 +244,60 @@ function renderPWarLeaderboard(data) {
|
|||||||
<td style="text-align:center; font-weight:700; color:#6366f1;">${p.predicted_soi !== null ? p.predicted_soi.toFixed(1) + '%' : '-'}</td>
|
<td style="text-align:center; font-weight:700; color:#6366f1;">${p.predicted_soi !== null ? p.predicted_soi.toFixed(1) + '%' : '-'}</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 class="formula-header">⚙️ AI 위험 적응형 모델(AAS) 산출 시뮬레이션</div>
|
<div class="formula-header">⚙️ AI 자산 건전성 분석 시뮬레이션 (AAS Metrics)</div>
|
||||||
<div class="work-effort-section">
|
|
||||||
<div class="work-effort-header">
|
<div style="display: flex; gap: 20px; margin-bottom: 20px;">
|
||||||
<span style="font-size: 13px; font-weight: 800; color: #1e5149;">📊 실질 업무 활성화 분석 (Work Vitality)</span>
|
<div class="work-effort-section" style="flex: 1; margin-bottom: 0;">
|
||||||
<span style="font-size: 14px; font-weight: 800; color: ${p.work_effort >= 70 ? '#059669' : p.work_effort <= 30 ? '#dc2626' : '#6366f1'};">투입률 ${p.work_effort}%</span>
|
<div class="work-effort-header">
|
||||||
|
<span style="font-size: 13px; font-weight: 800; color: #1e5149;">📊 실질 업무 집중도 Analysis</span>
|
||||||
|
<span style="font-size: 14px; font-weight: 800; color: ${p.work_effort >= 70 ? '#059669' : p.work_effort <= 30 ? '#dc2626' : '#6366f1'};">${p.work_effort}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="work-effort-bar-bg"><div style="width: ${p.work_effort}%; height: 100%; background: ${p.work_effort >= 70 ? '#059669' : p.work_effort <= 30 ? '#dc2626' : '#6366f1'};"></div></div>
|
||||||
|
<div style="font-size: 11px; color: #64748b; line-height: 1.5;">최근 수집 로그 중 실질적 <b>자산 증분</b>이 포착된 밀도입니다.</div>
|
||||||
|
</div>
|
||||||
|
<div style="flex: 1; background: #f8fafc; border-radius: 8px; padding: 16px; border: 1px solid #e2e8f0; display: flex; align-items: center; gap: 15px;">
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<div style="font-size: 10px; color: #64748b; font-weight: 700; margin-bottom: 2px;">VCI GRADE</div>
|
||||||
|
<div class="grade-badge ${grade.class}" style="padding: 4px 12px; border-radius: 6px; font-weight: 900; font-size: 14px; display: inline-block;">${grade.label}</div>
|
||||||
|
</div>
|
||||||
|
<div style="font-size: 12px; color: #475569; line-height: 1.4; font-weight: 600;">${grade.desc}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="work-effort-bar-bg"><div style="width: ${p.work_effort}%; height: 100%; background: ${p.work_effort >= 70 ? '#059669' : p.work_effort <= 30 ? '#dc2626' : '#6366f1'}; transition: width 0.5s;"></div></div>
|
|
||||||
<div style="font-size: 11.5px; color: #64748b; line-height: 1.5;">최근 30회 중 실제 파일 변동이 포착된 날의 비율입니다. 현재 <b>${p.work_effort >= 70 ? '매우 활발' : p.work_effort <= 30 ? '정체' : '간헐적'}</b> 상태입니다.</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="formula-steps-grid">
|
<div class="formula-steps-grid">
|
||||||
<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 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>
|
||||||
<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">활동 품질 (Quality)</div>
|
<div class="step-title">활동 진정성 검증</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="highlight-val">${(p.log_quality * 100).toFixed(0)}%</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">방치 시간 감쇄</div>
|
<div class="step-title">가동 보존율 (AVI)</div>
|
||||||
<div class="math-logic">Result = <span class="highlight-val">${((p.p_war / (p.file_count === 0 ? 0.05 : p.file_count < 10 ? 0.4 : 1) / p.log_quality) || 0).toFixed(1)}%</span></div>
|
<div class="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">존재 진정성 (ECV)</div>
|
<div class="step-title">가치 기여 영향력 (VCI)</div>
|
||||||
<div class="math-logic">Factor = <span class="${ecvClass}">${ecvText}</span></div>
|
<div class="math-logic">VCI = <span class="highlight-val">${vci.toFixed(1)}</span></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="final-result-area">
|
|
||||||
<div style="font-size: 11px; color: #64748b;">* 공식: AAS_Score × Quality_Factor × ECV_Factor</div>
|
|
||||||
<div><span style="font-size: 13px; color: #64748b; font-weight: 700;">최종 P-SOI: </span><span style="color: #1e5149; font-size: 22px; font-weight: 900;">${p.p_war.toFixed(1)}%</span></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@@ -275,12 +312,19 @@ function toggleProjectDetail(rowId) {
|
|||||||
const container = document.querySelector('.table-scroll-wrapper');
|
const container = document.querySelector('.table-scroll-wrapper');
|
||||||
const mainRow = document.querySelector(`tr[onclick*="toggleProjectDetail('${rowId}')"]`);
|
const mainRow = document.querySelector(`tr[onclick*="toggleProjectDetail('${rowId}')"]`);
|
||||||
const detailRow = document.getElementById(`detail-${rowId}`);
|
const detailRow = document.getElementById(`detail-${rowId}`);
|
||||||
|
|
||||||
if (detailRow && container) {
|
if (detailRow && container) {
|
||||||
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(() => { container.scrollTo({ top: mainRow.offsetTop - (container.querySelector('thead').offsetHeight || 40), behavior: 'smooth' }); }, 50);
|
setTimeout(() => {
|
||||||
} else detailRow.classList.remove('active');
|
const headerH = container.querySelector('thead').offsetHeight || 45;
|
||||||
|
const targetScrollTop = mainRow.offsetTop - headerH;
|
||||||
|
container.scrollTo({ top: targetScrollTop, behavior: 'smooth' });
|
||||||
|
}, 100);
|
||||||
|
} else {
|
||||||
|
detailRow.classList.remove('active');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,14 +332,15 @@ function openProjectListModal(label, projects) {
|
|||||||
const modal = document.getElementById('analysisModal');
|
const modal = document.getElementById('analysisModal');
|
||||||
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 = projects.length === 0 ? '<p style="text-align:center; padding: 40px; color: #888;">대상 프로젝트 없음</p>' : `
|
||||||
<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>현재 SOI</th></tr></thead>
|
<thead><tr><th>프로젝트명</th><th>부서</th><th>관리자</th><th>정체일</th><th>활력(AVI)</th></tr></thead>
|
||||||
<tbody>${projects.map(p => `<tr><td class="font-bold">${p.project_nm}</td><td>${p.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.dept || '-'}</td><td>${p.master || '-'}</td><td>${p.days_stagnant}일</td><td style="font-weight:700; color:#1e5149;">${p.p_war.toFixed(1)}%</td></tr>`).join('')}</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>`;
|
</div>
|
||||||
|
<div class="modal-footer"><button class="btn btn-primary" onclick="closeAnalysisModal()">닫기</button></div>`;
|
||||||
modal.style.display = 'flex';
|
modal.style.display = 'flex';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -303,12 +348,75 @@ function openAnalysisModal(type) {
|
|||||||
const modal = document.getElementById('analysisModal');
|
const modal = document.getElementById('analysisModal');
|
||||||
const title = document.getElementById('modalTitle');
|
const title = document.getElementById('modalTitle');
|
||||||
const body = document.getElementById('modalBody');
|
const body = document.getElementById('modalBody');
|
||||||
if (type === 'soi') {
|
|
||||||
title.innerText = 'P-SOI 산출 공식 상세';
|
if (type === 'avi') {
|
||||||
body.innerHTML = '<div class="formula-section"><div class="formula-box">SOI = exp(-λ × days) × 100</div></div><p>방치일수에 따른 가치 하락 모델입니다.</p>';
|
title.innerText = '운영 활력 지수 (AVI) 등급 가이드';
|
||||||
|
body.innerHTML = `
|
||||||
|
<div class="formula-box" style="margin-bottom:15px;">AVI = exp(-λ × days) × Quality × 100</div>
|
||||||
|
<p style="font-size:13px; color:#64748b; margin-bottom:15px;">자산의 가동 상태와 생존율을 나타내는 지표입니다.</p>
|
||||||
|
<table class="data-table" style="font-size:12px;">
|
||||||
|
<thead><tr style="background:#f8fafc;"><th>지수 (AVI)</th><th>등급</th><th>운영 상태</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>90%↑</td><td style="font-weight:900; color:#059669;">Live</td><td>실시간 성과물이 도출되는 최상급 가동</td></tr>
|
||||||
|
<tr><td>70~90%</td><td style="font-weight:900; color:#1e5149;">Stable</td><td>주기적 업데이트가 이뤄지는 표준 안정</td></tr>
|
||||||
|
<tr><td>30~70%</td><td style="font-weight:900; color:#f59e0b;">Idle</td><td>관리가 필요한 유휴/정체 상태</td></tr>
|
||||||
|
<tr><td>10~30%</td><td style="font-weight:900; color:#dc2626;">Risk</td><td>자산 가치 소멸 직전의 위험 상태</td></tr>
|
||||||
|
<tr><td>10%↓</td><td style="font-weight:900; color:#64748b;">Frozen</td><td>운영이 중단된 동결/방치 상태</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div class="modal-footer"><button class="btn btn-primary" onclick="closeAnalysisModal()">닫기</button></div>`;
|
||||||
|
} else if (type === 'vci') {
|
||||||
|
title.innerText = '자산 가치 기여도 (VCI) 등급 가이드';
|
||||||
|
body.innerHTML = `
|
||||||
|
<p style="font-size:13px; color:#64748b; margin-bottom:15px;">운영 표준(AVI 70%) 대비 자산 가치 기여도에 따른 프로젝트 위상 분류입니다.</p>
|
||||||
|
<table class="data-table" style="font-size:12px;">
|
||||||
|
<thead><tr style="background:#f8fafc;"><th>점수 (VCI)</th><th>등급</th><th>운영 의미</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>+10.0↑</td><td style="font-weight:900; color:#6366f1;">Masterpiece</td><td>시스템 가치를 견인하는 핵심 자산 (MVP급)</td></tr>
|
||||||
|
<tr><td>+2.0 ~ +10.0</td><td style="font-weight:900; color:#059669;">Blue Chip</td><td>꾸준한 활력의 우량 자산 (주전급)</td></tr>
|
||||||
|
<tr><td>-2.0 ~ +2.0</td><td style="font-weight:900; color:#475569;">Steady</td><td>표준 수준의 현상 유지 (보결급)</td></tr>
|
||||||
|
<tr><td>-10.0 ~ -2.0</td><td style="font-weight:900; color:#f59e0b;">Underperform</td><td>운영 미비로 인한 가치 하락 (마이너급)</td></tr>
|
||||||
|
<tr><td>-10.0↓</td><td style="font-weight:900; color:#dc2626;">Liability</td><td>가치를 훼손 중인 방치 자산 (방출급)</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div class="modal-footer"><button class="btn btn-primary" onclick="closeAnalysisModal()">닫기</button></div>`;
|
||||||
|
} else if (type === 'focus') {
|
||||||
|
title.innerText = '업무 집중도 (Job Focus) 등급 가이드';
|
||||||
|
body.innerHTML = `
|
||||||
|
<p style="font-size:13px; color:#64748b; margin-bottom:15px;">단순 관리 로그를 제외한 실질적인 산출물 변화의 밀도입니다.</p>
|
||||||
|
<table class="data-table" style="font-size:12px;">
|
||||||
|
<thead><tr style="background:#f8fafc;"><th>비율 (%)</th><th>등급</th><th>활동 성격</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>80%↑</td><td style="font-weight:900; color:#6366f1;">Intensive</td><td>성과물 위주의 고밀도 집중 작업</td></tr>
|
||||||
|
<tr><td>50~80%</td><td style="font-weight:900; color:#059669;">Active</td><td>성과와 관리가 균형 잡힌 원활한 실행</td></tr>
|
||||||
|
<tr><td>20~50%</td><td style="font-weight:900; color:#f59e0b;">Maintenance</td><td>설정/행정 등 단순 관리 중심의 작업</td></tr>
|
||||||
|
<tr><td>20%↓</td><td style="font-weight:900; color:#dc2626;">Surface</td><td>실체적 변화가 적은 형식적 로그 중심</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div class="modal-footer"><button class="btn btn-primary" onclick="closeAnalysisModal()">닫기</button></div>`;
|
||||||
} else {
|
} else {
|
||||||
title.innerText = 'AI 시계열 예측 상세';
|
title.innerText = '상태 예보 (AI Forecast) 분석 가이드';
|
||||||
body.innerHTML = '<p>활동 가속도 및 밀도를 분석하여 14일 뒤의 상태를 예보합니다.</p>';
|
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';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,19 +53,45 @@ class SOIPredictionService:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def predict_future_soi(current_soi, history, days_ahead=14):
|
def predict_future_soi(current_soi, history, days_ahead=14):
|
||||||
"""기존 점수와 시계열 피처를 결합하여 미래 점수 예측"""
|
"""기존 점수와 시계열 피처를 결합하여 미래 점수 예측"""
|
||||||
if not history or len(history) < 2:
|
# 데이터가 너무 적으면 무조건 보수적 감쇄 (14일 기준 약 -2.1점)
|
||||||
return round(max(0, min(100, current_soi - (0.05 * days_ahead))), 1)
|
if not history or len(history) < 3:
|
||||||
|
return round(max(0, min(100, current_soi - (0.15 * days_ahead))), 1)
|
||||||
|
|
||||||
features = SOIPredictionService.extract_vitality_features(history)
|
features = SOIPredictionService.extract_vitality_features(history)
|
||||||
|
|
||||||
# 기준점을 현재의 실제 SOI 점수로 설정 (핵심 수정)
|
|
||||||
current_val = float(current_soi)
|
current_val = float(current_soi)
|
||||||
|
|
||||||
# 활동 모멘텀 계산: 파일 증가 속도와 로그 밀도 반영
|
# [정밀 정체 분석]
|
||||||
momentum_factor = (features['velocity'] * 0.2) + (features['density'] * 2.0)
|
# 1. 파일 수 변화 확인 (최근 5개 샘플)
|
||||||
|
recent_counts = [int(h['file_count'] or 0) for h in history[-5:]]
|
||||||
|
is_hard_stagnant = len(set(recent_counts)) <= 1 # 파일 수 변동이 전혀 없음
|
||||||
|
|
||||||
# 예측 로직: 현재값 + 모멘텀 - 자연 감쇄
|
# 2. 최근 로그 상태 확인
|
||||||
decay_constant = 0.05
|
last_log = history[-1]['recent_log']
|
||||||
|
is_no_activity = last_log is None or last_log == "데이터 없음" or "폴더자동삭제" in last_log
|
||||||
|
|
||||||
|
# [모멘텀 산출 로직 개편]
|
||||||
|
if is_hard_stagnant:
|
||||||
|
# 파일 변화가 없다면 아무리 로그가 있어도 '유지 관리'일 뿐 '성장'이 아님
|
||||||
|
# 오히려 시간이 갈수록 기술 부채와 데이터 노후화가 진행된다고 판단 (강력 패널티)
|
||||||
|
momentum_factor = -2.5 if is_no_activity else -1.0
|
||||||
|
else:
|
||||||
|
# 실질적인 파일 수 변화(Velocity)가 있을 때만 긍정적 모멘텀 검토
|
||||||
|
v_gain = features['velocity'] * 0.5
|
||||||
|
d_gain = features['density'] * 0.8
|
||||||
|
momentum_factor = v_gain + d_gain - 0.5 # 기본적으로 하향 압력 부여
|
||||||
|
|
||||||
|
# 예측 로직: 현재값 + 모멘텀 - (시간에 따른 자연 부식)
|
||||||
|
# 정체 시 momentum_factor가 -1.0~-2.5이므로 감쇄가 매우 빠름
|
||||||
|
decay_constant = 0.08
|
||||||
predicted = current_val + momentum_factor - (decay_constant * days_ahead)
|
predicted = current_val + momentum_factor - (decay_constant * days_ahead)
|
||||||
|
|
||||||
|
# [최종 방어 로직]
|
||||||
|
# 실질적 파일 증가(velocity > 0)가 포착되지 않았다면 예보는 현재값보다 클 수 없음
|
||||||
|
if features['velocity'] <= 0 and predicted > current_val:
|
||||||
|
predicted = current_val - 1.5 # 강제 하락
|
||||||
|
|
||||||
|
# 사망 선고 (AVI가 이미 낮고 정체 중이면 0에 수렴하도록 가속)
|
||||||
|
if current_val < 20 and is_hard_stagnant:
|
||||||
|
predicted = max(0, predicted - 5.0)
|
||||||
|
|
||||||
return round(max(0, min(100, predicted)), 1)
|
return round(max(0, min(100, predicted)), 1)
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class DashboardQueries:
|
|||||||
|
|
||||||
# 활성도 분석을 위한 프로젝트 목록 조회
|
# 활성도 분석을 위한 프로젝트 목록 조회
|
||||||
GET_PROJECT_LIST_FOR_ANALYSIS = """
|
GET_PROJECT_LIST_FOR_ANALYSIS = """
|
||||||
SELECT m.project_id, m.project_nm, m.short_nm, h.recent_log, h.file_count
|
SELECT m.project_id, m.project_nm, m.short_nm, m.department, h.recent_log, h.file_count
|
||||||
FROM projects_master m
|
FROM projects_master m
|
||||||
LEFT JOIN projects_history h ON m.project_id = h.project_id AND h.crawl_date = %s
|
LEFT JOIN projects_history h ON m.project_id = h.project_id AND h.crawl_date = %s
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
/* ==========================================================================
|
/* ==========================================================================
|
||||||
Project Master Analysis - Sabermetrics Style
|
Project Master Analysis - Specific Styles
|
||||||
|
(Inherits base styles from common.css)
|
||||||
========================================================================== */
|
========================================================================== */
|
||||||
|
|
||||||
.analysis-content {
|
.analysis-content {
|
||||||
@@ -18,7 +19,6 @@
|
|||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
letter-spacing: 0.5px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.analysis-header {
|
.analysis-header {
|
||||||
@@ -26,216 +26,208 @@
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
|
padding: 10px 0;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Top Info Grid (AI Info & SOI Deep Dive) */
|
.analysis-header h2 { font-size: 22px; font-weight: 800; color: var(--text-main); margin-bottom: 4px; }
|
||||||
|
.analysis-header p { font-size: 13px; color: var(--text-sub); }
|
||||||
|
|
||||||
|
/* Top Info Grid */
|
||||||
.top-info-grid {
|
.top-info-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 2fr;
|
grid-template-columns: 1fr 2.2fr;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dl-model-info, .soi-deep-dive {
|
.dl-model-info, .soi-deep-dive {
|
||||||
background: white;
|
background: white;
|
||||||
border-radius: 12px;
|
border-radius: var(--radius-xl);
|
||||||
border: 1px solid #eef2f6;
|
border: 1px solid var(--border-color);
|
||||||
box-shadow: 0 4px 12px rgba(0,0,0,0.03);
|
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
box-shadow: var(--box-shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
.model-desc-vertical {
|
.card-header { margin-bottom: 15px; display: flex; align-items: center; justify-content: space-between; }
|
||||||
display: flex;
|
.card-header h4 { font-size: 14px; font-weight: 800; color: var(--primary-color); margin: 0; }
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.model-item-vertical {
|
.model-desc-vertical { display: flex; flex-direction: column; gap: 12px; }
|
||||||
display: flex;
|
.model-item-vertical { display: flex; align-items: center; gap: 12px; }
|
||||||
align-items: center;
|
.model-tag { background: var(--bg-muted); color: var(--text-sub); padding: 2px 8px; border-radius: 4px; font-size: 10px; font-weight: 700; }
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.model-tag {
|
.soi-info-columns { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; }
|
||||||
background: #f1f5f9;
|
.soi-info-column h6 { font-size: 12px; font-weight: 800; color: var(--primary-color); margin: 0 0 8px 0; }
|
||||||
color: #475569;
|
.soi-info-column p { font-size: 11.5px; color: var(--text-sub); line-height: 1.6; margin: 0; }
|
||||||
padding: 2px 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 10px;
|
|
||||||
font-weight: 700;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.soi-info-columns {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, 1fr);
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.soi-info-column h6 {
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 800;
|
|
||||||
color: #1e5149;
|
|
||||||
margin: 0 0 8px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.soi-info-column p {
|
|
||||||
font-size: 11.5px;
|
|
||||||
color: #64748b;
|
|
||||||
line-height: 1.6;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Chart Grid Layout */
|
/* Chart Grid Layout */
|
||||||
.analysis-charts-grid {
|
.analysis-charts-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1.2fr 2fr;
|
grid-template-columns: 1fr 1.8fr;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-container-box {
|
.chart-container-box {
|
||||||
background: white;
|
background: white;
|
||||||
border-radius: 12px;
|
border-radius: var(--radius-xl);
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
border: 1px solid #eef2f6;
|
border: 1px solid var(--border-color);
|
||||||
height: 340px;
|
height: 360px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
box-shadow: var(--box-shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-container-box h5 {
|
.chart-container-box h5 { margin: 0 0 15px 0; font-size: 13px; font-weight: 700; color: var(--text-main); }
|
||||||
margin: 0 0 15px 0;
|
|
||||||
font-size: 13px;
|
/* Timeline Analysis Card */
|
||||||
font-weight: 700;
|
.analysis-card {
|
||||||
color: #334155;
|
background: white;
|
||||||
|
border-radius: var(--radius-xl);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
box-shadow: var(--box-shadow);
|
||||||
|
margin-bottom: 24px;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.analysis-card .card-header {
|
||||||
|
padding: 16px 24px;
|
||||||
|
background: #fff;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.analysis-card .card-body { padding: 24px; }
|
||||||
|
|
||||||
|
/* SOI Guide Styles */
|
||||||
|
.d-war-guide {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 12px;
|
||||||
|
background: var(--bg-muted);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-item {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-item.active-low { background: #dcfce7; color: #166534; }
|
||||||
|
.guide-item.warning-mid { background: #fef9c3; color: #854d0e; }
|
||||||
|
.guide-item.danger-high { background: #ffedd5; color: #9a3412; }
|
||||||
|
.guide-item.hazard-critical { background: #fee2e2; color: #991b1b; }
|
||||||
|
|
||||||
/* Data Table Customization */
|
/* Data Table Customization */
|
||||||
.p-war-table-container {
|
.table-scroll-wrapper {
|
||||||
margin-top: 24px;
|
overflow-x: auto;
|
||||||
|
overflow-y: auto;
|
||||||
|
max-height: 600px;
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
background: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.project-row {
|
.p-war-table {
|
||||||
cursor: pointer;
|
width: 100%;
|
||||||
transition: background 0.2s ease;
|
border-collapse: separate;
|
||||||
|
border-spacing: 0;
|
||||||
|
table-layout: fixed; /* 컬럼 너비 고정 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.project-row:hover {
|
.p-war-table th {
|
||||||
background: #f8fafc !important;
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 20;
|
||||||
|
background: #f8fafc;
|
||||||
|
padding: 16px 15px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #475569;
|
||||||
|
border-bottom: 2px solid #e2e8f0;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.p-war-table td {
|
||||||
|
padding: 14px 15px;
|
||||||
|
font-size: 13px;
|
||||||
|
border-bottom: 1px solid #f1f5f9;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 컬럼별 너비 정의 */
|
||||||
|
.p-war-table th:nth-child(1), .p-war-table td:nth-child(1) { width: 28%; text-align: left; } /* 프로젝트명 */
|
||||||
|
.p-war-table th:nth-child(2), .p-war-table td:nth-child(2) { width: 10%; text-align: right; } /* 파일 수 */
|
||||||
|
.p-war-table th:nth-child(3), .p-war-table td:nth-child(3) { width: 10%; text-align: right; } /* 방치일 */
|
||||||
|
.p-war-table th:nth-child(4), .p-war-table td:nth-child(4) { width: 10%; text-align: center; } /* 상태 판정 */
|
||||||
|
.p-war-table th:nth-child(5), .p-war-table td:nth-child(5) { width: 14%; text-align: right; } /* P-WAR+ */
|
||||||
|
.p-war-table th:nth-child(6), .p-war-table td:nth-child(6) { width: 12%; text-align: right; } /* 현재 SOI */
|
||||||
|
.p-war-table th:nth-child(7), .p-war-table td:nth-child(7) { width: 12%; text-align: center; } /* 실무 투입 */
|
||||||
|
.p-war-table th:nth-child(8), .p-war-table td:nth-child(8) { width: 14%; text-align: center; } /* AI 예보 */
|
||||||
|
|
||||||
|
.project-row { cursor: pointer; transition: background 0.2s; }
|
||||||
|
.project-row:hover { background: var(--hover-bg) !important; }
|
||||||
|
|
||||||
|
/* SOI Value Styling */
|
||||||
|
.p-war-value { font-weight: 800; font-family: 'Consolas', monospace; }
|
||||||
|
|
||||||
/* Accordion Detail Styles */
|
/* Accordion Detail Styles */
|
||||||
.detail-row {
|
.detail-row { display: none; background: #fafafa; }
|
||||||
display: none;
|
.detail-row.active { display: table-row; }
|
||||||
background: #fdfdfd;
|
.detail-container { padding: 20px 24px; }
|
||||||
}
|
|
||||||
|
|
||||||
.detail-row.active {
|
|
||||||
display: table-row;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-container {
|
|
||||||
padding: 20px 24px;
|
|
||||||
border-bottom: 2px solid #f1f5f9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.formula-explanation-card {
|
.formula-explanation-card {
|
||||||
background: white;
|
background: white;
|
||||||
border-radius: 12px;
|
border-radius: var(--radius-lg);
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
border: 1px solid #e2e8f0;
|
border: 1px solid var(--border-color);
|
||||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);
|
box-shadow: var(--box-shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
.formula-header {
|
.formula-header { font-size: 13px; font-weight: 700; color: #6366f1; margin-bottom: 15px; }
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #6366f1;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Work Effort Bar Area */
|
/* Work Effort Section */
|
||||||
.work-effort-section {
|
.work-effort-section { background: var(--bg-muted); padding: 16px; border-radius: var(--radius-lg); margin-bottom: 20px; }
|
||||||
background: #f8fafc;
|
.work-effort-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
|
||||||
padding: 16px;
|
.work-effort-bar-bg { width: 100%; height: 6px; background: #e2e8f0; border-radius: 3px; overflow: hidden; margin-bottom: 10px; }
|
||||||
border-radius: 8px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
border: 1px solid #eef2f6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.work-effort-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.work-effort-bar-bg {
|
|
||||||
width: 100%;
|
|
||||||
height: 6px;
|
|
||||||
background: #e2e8f0;
|
|
||||||
border-radius: 3px;
|
|
||||||
overflow: hidden;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Formula Steps Grid */
|
/* Formula Steps Grid */
|
||||||
.formula-steps-grid {
|
.formula-steps-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
|
||||||
display: grid;
|
.formula-step { display: flex; gap: 12px; }
|
||||||
grid-template-columns: 1fr 1fr;
|
.step-num { background: var(--primary-color); color: white; width: 20px; height: 20px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 11px; font-weight: 800; flex-shrink: 0; }
|
||||||
gap: 20px;
|
.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; }
|
||||||
|
|
||||||
.formula-step {
|
.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; }
|
||||||
|
|
||||||
|
/* Modal Analysis Specific */
|
||||||
|
.modal-footer {
|
||||||
|
padding: 16px 24px;
|
||||||
|
background: #fff;
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
text-align: right;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.step-num {
|
/* Formula & Badges */
|
||||||
background: #1e5149;
|
.formula-section { margin-bottom: 20px; }
|
||||||
color: white;
|
.formula-box { background: var(--primary-lv-0); color: var(--primary-color); padding: 15px; border-radius: var(--radius-lg); font-weight: 800; text-align: center; font-family: monospace; font-size: 16px; }
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 800;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.step-title {
|
.badge-active { background: #dcfce7; color: #166534; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 700; }
|
||||||
font-size: 12px;
|
.badge-warning { background: #fef9c3; color: #854d0e; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 700; }
|
||||||
font-weight: 700;
|
.badge-danger { background: #ffedd5; color: #9a3412; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 700; }
|
||||||
color: #334155;
|
.badge-system { background: #fee2e2; color: #991b1b; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 700; }
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.math-logic {
|
|
||||||
font-family: 'Consolas', monospace;
|
|
||||||
background: #f1f5f9;
|
|
||||||
padding: 4px 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #0f172a;
|
|
||||||
font-size: 12px;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.final-result-area {
|
|
||||||
margin-top: 20px;
|
|
||||||
padding-top: 15px;
|
|
||||||
border-top: 2px solid #1e5149;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Utility Classes */
|
|
||||||
.highlight-var { color: #2563eb; }
|
.highlight-var { color: #2563eb; }
|
||||||
.highlight-val { color: #059669; }
|
.highlight-val { color: #059669; }
|
||||||
.highlight-penalty { color: #dc2626; }
|
.highlight-penalty { color: #dc2626; }
|
||||||
.text-plus { color: #059669; font-weight: 700; }
|
.text-plus { color: #059669; font-weight: 700; }
|
||||||
.text-minus { color: #dc2626; font-weight: 700; }
|
.text-minus { color: #dc2626; font-weight: 700; }
|
||||||
|
.font-bold { font-weight: 700; }
|
||||||
|
|||||||
@@ -30,11 +30,11 @@
|
|||||||
<header class="analysis-header">
|
<header class="analysis-header">
|
||||||
<div class="title-group">
|
<div class="title-group">
|
||||||
<div class="ai-badge">AI Sabermetrics</div>
|
<div class="ai-badge">AI Sabermetrics</div>
|
||||||
<h2>시스템 운영 빅데이터 분석</h2>
|
<h2>시스템 운영 자산 가치 분석</h2>
|
||||||
<p>수집된 활동 로그 및 자산 데이터를 기반으로 한 통계적 성능 지표 (Beta)</p>
|
<p>수집된 활동 로그 및 자산 데이터를 기반으로 한 통계적 활력 지표 (Beta)</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="analysis-actions">
|
<div class="analysis-actions">
|
||||||
<button class="btn-refresh" onclick="location.reload()">데이터 갱신</button>
|
<button class="btn btn-primary" onclick="location.reload()">데이터 갱신</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -47,12 +47,12 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="model-desc-vertical">
|
<div class="model-desc-vertical">
|
||||||
<div class="model-item-vertical">
|
<div class="model-item-vertical">
|
||||||
<span class="model-tag">알고리즘</span>
|
<span class="model-tag">분석 모델</span>
|
||||||
<p>최근 9회차 시계열의 Velocity 및 가속도 분석</p>
|
<p>최근 9회차 시계열의 Velocity 및 변화율 분석</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="model-item-vertical">
|
<div class="model-item-vertical">
|
||||||
<span class="model-tag">판단 로직</span>
|
<span class="model-tag">가중치 로직</span>
|
||||||
<p>활동 시 '선형 추세', 정체 시 '지수 감쇄' 가중치 적용</p>
|
<p>활동 시 '선형 유지', 정체 시 '지수 감쇄' 동적 적용</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -65,16 +65,16 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="soi-info-columns">
|
<div class="soi-info-columns">
|
||||||
<div class="soi-info-column">
|
<div class="soi-info-column">
|
||||||
<h6>1. AI 자산 가치 평가</h6>
|
<h6>1. 자산 가치 변동 추적</h6>
|
||||||
<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>
|
||||||
<p>프로젝트마다 <strong>개별화된 위험 곡선</strong>을 생성하여 현장에 가장 밀착된 가치 보존율을 제공합니다.</p>
|
<p>프로젝트마다 <strong>개별화된 감쇄 곡선</strong>을 생성하여 현장에 가장 밀착된 보존율을 제공합니다.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -84,11 +84,11 @@
|
|||||||
<!-- 메인 분석 차트 영역 -->
|
<!-- 메인 분석 차트 영역 -->
|
||||||
<div class="analysis-charts-grid">
|
<div class="analysis-charts-grid">
|
||||||
<div class="chart-container-box">
|
<div class="chart-container-box">
|
||||||
<h5>건강 상태 분포 (Project Distribution)</h5>
|
<h5>운영 활력 분포 (Activity Distribution)</h5>
|
||||||
<canvas id="statusChart"></canvas>
|
<canvas id="statusChart"></canvas>
|
||||||
</div>
|
</div>
|
||||||
<div class="chart-container-box">
|
<div class="chart-container-box">
|
||||||
<h5>프로젝트 SWOT 매트릭스 (Strategic Analysis)</h5>
|
<h5>자산 가치 전략 매트릭스 (Strategic Analysis)</h5>
|
||||||
<canvas id="forecastChart"></canvas>
|
<canvas id="forecastChart"></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -97,19 +97,19 @@
|
|||||||
<div class="analysis-card timeline-analysis">
|
<div class="analysis-card timeline-analysis">
|
||||||
<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 Stagnation Objective Index (P-SOI Status)</h4>
|
<h4>Project Activity Vitality Leaderboard (AVI Status)</h4>
|
||||||
<p style="font-size: 11px; color: #888; margin: 0;">이상적 관리 상태(100%) 대비 활동 보존율 및 미래 예측 리더보드</p>
|
<p style="font-size: 11px; color: #888; margin: 0;">운영 표준(AVI 70%) 대비 파일 보존율 및 미래 가치 기여 리더보드</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-tools">
|
<div class="card-tools">
|
||||||
<span id="avg-system-info" style="font-size: 11px; color: #888;">* SOI (Project Health Score)</span>
|
<span id="avg-system-info" style="font-size: 11px; color: #888;">* AVI (Activity Vitality Index)</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="d-war-guide">
|
<div class="d-war-guide">
|
||||||
<div class="guide-item active-low"><span>70%↑</span> 정상</div>
|
<div class="guide-item active-low"><span>70%↑</span> 정상 운영</div>
|
||||||
<div class="guide-item warning-mid"><span>30~70%</span> 주의</div>
|
<div class="guide-item warning-mid"><span>30~70%</span> 관리 주의</div>
|
||||||
<div class="guide-item danger-high"><span>10~30%</span> 위험</div>
|
<div class="guide-item danger-high"><span>10~30%</span> 위험 노출</div>
|
||||||
<div class="guide-item hazard-critical"><span>10%↓</span> 사망</div>
|
<div class="guide-item hazard-critical"><span>10%↓</span> 중단/방치</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="p-war-table-container">
|
<div id="p-war-table-container">
|
||||||
@@ -124,7 +124,7 @@
|
|||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h3 id="modalTitle">분석 상세</h3>
|
<h3 id="modalTitle">분석 상세</h3>
|
||||||
<button class="btn-close" onclick="closeAnalysisModal()">×</button>
|
<span class="modal-close" onclick="closeAnalysisModal()">×</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body" id="modalBody">
|
<div class="modal-body" id="modalBody">
|
||||||
<!-- 내용 동적 삽입 -->
|
<!-- 내용 동적 삽입 -->
|
||||||
|
|||||||
Reference in New Issue
Block a user