diff --git a/ANALYSIS_REPORT.md b/ANALYSIS_REPORT.md new file mode 100644 index 0000000..ddb9001 --- /dev/null +++ b/ANALYSIS_REPORT.md @@ -0,0 +1,49 @@ +# ๐Ÿ“Š Project Master Sabermetrics ๋ถ„์„ ์—”์ง„ ๋ฆฌํฌํŠธ + +## 1. ๊ฐœ์š” (Vision) +๋ณธ ์‹œ์Šคํ…œ์€ ๋ฐฉ๋Œ€ํ•œ ํ”„๋กœ์ ํŠธ ์šด์˜ ๋ฐ์ดํ„ฐ(ํŒŒ์ผ ์ˆ˜, ํ™œ๋™ ๋กœ๊ทธ, ์กฐ์ง ์ •๋ณด)๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ **AI ๊ธฐ๋ฐ˜ ํ”„๋กœ์ ํŠธ ๊ฑด๊ฐ•๋„(P-SOI)**๋ฅผ ์‚ฐ์ถœํ•ฉ๋‹ˆ๋‹ค. ๋‹จ์ˆœํžˆ "์‚ด์•„์žˆ๋Š”๊ฐ€"๋ฅผ ๋„˜์–ด, "์‹ค๋ฌด์ ์œผ๋กœ ๊ฐ€์น˜ ์žˆ๊ฒŒ ๊ด€๋ฆฌ๋˜๊ณ  ์žˆ๋Š”๊ฐ€"๋ฅผ ์ •๋ฐ€ ์ง„๋‹จํ•˜๋Š” ๊ฒƒ์ด ๋ชฉ์ ์ž…๋‹ˆ๋‹ค. + +--- + +## 2. P-SOI ์‚ฐ์ถœ ๋กœ์ง (The Formula) + +### 2.1 ๊ธฐ์ดˆ ๋ชจ๋ธ: ์ง€์ˆ˜ ๊ฐ์‡„ (Exponential Decay) +ํ”„๋กœ์ ํŠธ ์ •๋ณด์˜ ๊ฐ€์น˜๋Š” ๊ด€๋ฆฌ ํ™œ๋™์ด ๋ฉˆ์ถ˜ ์‹œ์ ๋ถ€ํ„ฐ ์‹œ๊ฐ„์ด ํ๋ฅผ์ˆ˜๋ก ๊ธ‰๊ฒฉํžˆ ํ•˜๋ฝํ•ฉ๋‹ˆ๋‹ค. +- **์ˆ˜์‹**: $SOI = 100 \times e^{-\lambda t}$ +- **์˜๋ฏธ**: 14์ผ ๋ฐฉ์น˜ ์‹œ ๊ฐ€์น˜๊ฐ€ ์•ฝ 50% ์†Œ์‹ค๋˜๋Š” ํ˜„์žฅ ํ˜„์‹ค์„ ๋ฐ˜์˜ํ•ฉ๋‹ˆ๋‹ค. + +### 2.2 ๊ณ ๋„ํ™” 1: AAS (AI-Hazard Adaptive SOI) +ํ”„๋กœ์ ํŠธ์˜ ์ค‘์š”๋„์™€ ์ฃผ๋ณ€ ํ™˜๊ฒฝ์— ๋”ฐ๋ผ ํ•˜๋ฝ ๊ณก์„ ์˜ ๊ธฐ์šธ๊ธฐ๋ฅผ ๋™์ ์œผ๋กœ ์กฐ์ •ํ•ฉ๋‹ˆ๋‹ค. +- **์ž์‚ฐ ๊ทœ๋ชจ ์˜ํ–ฅ**: ํŒŒ์ผ ์ˆ˜๊ฐ€ ๋งŽ์„์ˆ˜๋ก ๊ด€๋ฆฌ ๋ถ€์žฌ ๋ฆฌ์Šคํฌ๊ฐ€ ํฌ๋ฏ€๋กœ AI๊ฐ€ ํ•˜๋ฝ ์†๋„๋ฅผ ๊ฐ€์†์‹œํ‚ต๋‹ˆ๋‹ค. +- **์กฐ์ง ์œ„ํ—˜ ์ „์—ผ**: ์†Œ์† ๋ถ€์„œ๋‚˜ ๋‹ด๋‹น์ž์˜ ์ „์ฒด SOI๊ฐ€ ๋‚ฎ์„ ๊ฒฝ์šฐ, ์‹œ์Šคํ…œ์  ๋ถ•๊ดด ๋ฆฌ์Šคํฌ ๊ฐ€์ค‘์น˜๋ฅผ ๋ถ€์—ฌํ•ฉ๋‹ˆ๋‹ค. + +### 2.3 ๊ณ ๋„ํ™” 2: ECV (Existence-Conditioned Vitality) +'๋นˆ ๊ป๋ฐ๊ธฐ' ํ™œ๋™์„ ๊ฑธ๋Ÿฌ๋‚ด๋Š” ์กด์žฌ๋ก ์  ํŒจ๋„ํ‹ฐ์ž…๋‹ˆ๋‹ค. +- **์œ ๋ น ํ”„๋กœ์ ํŠธ**: ํŒŒ์ผ ์ˆ˜๊ฐ€ 0๊ฐœ์ธ ๊ฒฝ์šฐ, ์ตœ๊ทผ ๋กœ๊ทธ์™€ ๊ด€๊ณ„์—†์ด SOI ์ ์ˆ˜๋ฅผ **5% ๋ฏธ๋งŒ**์œผ๋กœ ๊ฐ•์ œ ๊ณ ์ •ํ•ฉ๋‹ˆ๋‹ค. +- **์‹ ๋ขฐ ๋ณด์ •**: ํŒŒ์ผ 10๊ฐœ ๋ฏธ๋งŒ์˜ ์†Œ๊ทœ๋ชจ ํ”„๋กœ์ ํŠธ๋Š” ํ™œ๋™ ์‹ ๋ขฐ๋„๋ฅผ 40% ์ˆ˜์ค€์œผ๋กœ ์ œํ•œํ•ฉ๋‹ˆ๋‹ค. + +### 2.4 ๊ณ ๋„ํ™” 3: ๋กœ๊ทธ ํ’ˆ์งˆ ๋ฐ ์‹ค๋ฌด ํˆฌ์ž… ๋ถ„์„ +- **Log Quality**: ๋กœ๊ทธ ํ…์ŠคํŠธ๋ฅผ ๋ถ„์„ํ•˜์—ฌ [์‹ค๋ฌด ํ™œ๋™(1.0), ๊ด€๋ฆฌ ํ™œ๋™(0.7), ํ–‰์ • ํ™œ๋™(0.4)] ๊ฐ€์ค‘์น˜๋ฅผ ๋ถ€์—ฌํ•ฉ๋‹ˆ๋‹ค. +- **Work Effort**: ์ตœ๊ทผ 30๊ฐœ ํžˆ์Šคํ† ๋ฆฌ ์ค‘ ์‹ค์ œ **ํŒŒ์ผ ์ฆ๊ฐ**์ด ๋ฐœ์ƒํ•œ ๋‚ ์˜ ๋น„์œจ์„ ๊ณ„์‚ฐํ•˜์—ฌ ์‹ค์งˆ ๊ณต์ˆ˜ ํˆฌ์ž…๋ฅ ์„ ์‚ฐ์ถœํ•ฉ๋‹ˆ๋‹ค. + +--- + +## 3. ์ „๋žต์  ๋ถ„์„ ๋„๊ตฌ (Visualization) + +### 3.1 ํ”„๋กœ์ ํŠธ SWOT ๋งคํŠธ๋ฆญ์Šค +X์ถ•(์ž์‚ฐ ๊ทœ๋ชจ)๊ณผ Y์ถ•(ํ™œ๋™์„ฑ)์„ ๊ฒฐํ•ฉํ•˜์—ฌ 4๊ฐ€์ง€ ๊ตญ๋ฉด์œผ๋กœ ํ”„๋กœ์ ํŠธ๋ฅผ ์ง„๋‹จํ•ฉ๋‹ˆ๋‹ค. +1. **ํ•ต์‹ฌ ์šฐ๋Ÿ‰ (Strategic)**: ๋Œ€๊ทœ๋ชจ ํ•ต์‹ฌ ์ž์‚ฐ์ด๋ฉฐ ํ™œ๋ฐœํžˆ ๊ด€๋ฆฌ ์ค‘. +2. **ํ™œ๋™ ์–‘ํ˜ธ (Agile)**: ๊ทœ๋ชจ๋Š” ์ž‘์œผ๋‚˜ ๋งค์šฐ ํƒ„๋ ฅ์ ์œผ๋กœ ์—…๋ฐ์ดํŠธ ์ค‘. +3. **๋ฐฉ์น˜/์†Œ๊ทœ๋ชจ**: ์ค‘์š”๋„๊ฐ€ ๋‚ฎ๊ณ  ๋ฐฉ์น˜๋œ ์ƒํƒœ. +4. **๊ด€๋ฆฌ ์‚ฌ๊ฐ์ง€๋Œ€ (Critical Risk)**: **์ž์‚ฐ ๊ทœ๋ชจ๋Š” ํฌ๋‚˜ ์žฅ๊ธฐ ๋ฐฉ์น˜๋จ (์ตœ์šฐ์„  ๊ด€๋ฆฌ ๋Œ€์ƒ)** + +### 3.2 AI ์ง„๋‹จ ์•„์ฝ”๋””์–ธ ๋ฆฌํฌํŠธ +์‚ฌ์šฉ์ž๊ฐ€ ์ง€์ˆ˜ ์‚ฐ์ถœ ๊ฒฐ๊ณผ์— ๋‚ฉ๋“ํ•  ์ˆ˜ ์žˆ๋„๋ก, ๊ฐœ๋ณ„ ํ”„๋กœ์ ํŠธ ํ–‰ ํด๋ฆญ ์‹œ **4๋‹จ๊ณ„ AI ์ถ”๋ก  ๊ณผ์ •**์„ ์‹ค์‹œ๊ฐ„ ๋ฆฌํฌํŠธ๋กœ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. + +--- + +## 4. ํ–ฅํ›„ ๋”ฅ๋Ÿฌ๋‹ ๋กœ๋“œ๋งต (Evolution) +๋ฐ์ดํ„ฐ๊ฐ€ ๋ˆ„์ ๋จ์— ๋”ฐ๋ผ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ž๊ฐ€ ํ•™์Šตํ˜• ์—”์ง„์œผ๋กœ ์ง„ํ™”ํ•ฉ๋‹ˆ๋‹ค. +- **LSTM ๊ธฐ๋ฐ˜ ๋ฆฌ๋“ฌ ํ•™์Šต**: ๊ฐ ํ”„๋กœ์ ํŠธ์˜ ๊ณ ์œ ํ•œ ์—…๋ฐ์ดํŠธ ์ฃผ๊ธฐ์™€ ํŒจํ„ด(Life Rhythm)์„ ์ธ์ฝ”๋”ฉํ•˜์—ฌ ๋งž์ถคํ˜• ์˜ˆ๋ณด ์ˆ˜ํ–‰. +- **NLP ์ž„๋ฒ ๋”ฉ**: ๋‹จ์ˆœ ํ‚ค์›Œ๋“œ๋ฅผ ๋„˜์–ด ๋กœ๊ทธ ํ…์ŠคํŠธ์˜ ๋งฅ๋ฝ์  ์˜๋ฏธ๋ฅผ ๋”ฅ๋Ÿฌ๋‹์ด ์Šค์Šค๋กœ ํ•™์Šตํ•˜์—ฌ ๊ฐ€์ค‘์น˜ ์ž๋™ ์‚ฐ์ •. +- **๋ณ‘๋ชฉ ์˜ˆ์ธก AI**: ํŠน์ • ๋‹ด๋‹น์ž๋‚˜ ๋ถ€์„œ์˜ ์—…๋ฌด ๊ณผ๋ถ€ํ•˜ ํŒจํ„ด์„ ํ•™์Šตํ•˜์—ฌ ์ง‘๋‹จ ๋ฐฉ์น˜ ์œ„ํ—˜์„ ์„ ์ œ์ ์œผ๋กœ ์˜ˆ๋ณด. diff --git a/__pycache__/analysis_service.cpython-312.pyc b/__pycache__/analysis_service.cpython-312.pyc new file mode 100644 index 0000000..1a1a5ba Binary files /dev/null and b/__pycache__/analysis_service.cpython-312.pyc differ diff --git a/__pycache__/inquiry_service.cpython-312.pyc b/__pycache__/inquiry_service.cpython-312.pyc new file mode 100644 index 0000000..8b39214 Binary files /dev/null and b/__pycache__/inquiry_service.cpython-312.pyc differ diff --git a/__pycache__/prediction_service.cpython-312.pyc b/__pycache__/prediction_service.cpython-312.pyc new file mode 100644 index 0000000..b100aa7 Binary files /dev/null and b/__pycache__/prediction_service.cpython-312.pyc differ diff --git a/__pycache__/project_service.cpython-312.pyc b/__pycache__/project_service.cpython-312.pyc new file mode 100644 index 0000000..76db698 Binary files /dev/null and b/__pycache__/project_service.cpython-312.pyc differ diff --git a/__pycache__/schemas.cpython-312.pyc b/__pycache__/schemas.cpython-312.pyc new file mode 100644 index 0000000..0590f30 Binary files /dev/null and b/__pycache__/schemas.cpython-312.pyc differ diff --git a/__pycache__/server.cpython-312.pyc b/__pycache__/server.cpython-312.pyc index b9302e9..17d489e 100644 Binary files a/__pycache__/server.cpython-312.pyc and b/__pycache__/server.cpython-312.pyc differ diff --git a/analysis_service.py b/analysis_service.py index 786b5fd..4c57d26 100644 --- a/analysis_service.py +++ b/analysis_service.py @@ -127,22 +127,46 @@ class AnalysisService: # ์ง€์ˆ˜ ๊ฐ์‡„ ์ ์šฉ (AAS Score) soi_score = math.exp(-ai_lambda * days_stagnant) * 100 - # [AI ๋ฐ์ดํ„ฐ ์ง„์ •์„ฑ ๊ฒ€์ฆ ๋กœ์ง - ECV ํŒจ๋„ํ‹ฐ ์ถ”๊ฐ€] - # ํŒŒ์ผ์ด ํ•˜๋‚˜๋„ ์—†๊ฑฐ๋‚˜(์œ ๋ น), ํ˜„์ €ํžˆ ์ ์€ ๊ฒฝ์šฐ(๊ป๋ฐ๊ธฐ) ํ™œ๋™์˜ ์ง„์ •์„ฑ์„ ๋ถˆ์‹ ํ•จ + # [AI ๋ฐ์ดํ„ฐ ์ง„์ •์„ฑ ๊ฒ€์ฆ ๋กœ์ง 1 - ECV ํŒจ๋„ํ‹ฐ (์กด์žฌ๋ก ์ )] existence_confidence = 1.0 if file_count == 0: - existence_confidence = 0.05 # ํŒŒ์ผ 0๊ฐœ๋Š” ๋กœ๊ทธ๊ฐ€ ์žˆ์–ด๋„ ์ตœ๋Œ€ 5% ๋ฏธ๋งŒ์œผ๋กœ ๊ฐ•์ œ + existence_confidence = 0.05 elif file_count < 10: - existence_confidence = 0.4 # ํŒŒ์ผ 10๊ฐœ ๋ฏธ๋งŒ์€ ํ™œ๋™ ์‹ ๋ขฐ๋„ 40%๋กœ ์ œํ•œ + existence_confidence = 0.4 - soi_score = soi_score * existence_confidence + # [AI ๋ฐ์ดํ„ฐ ์ง„์ •์„ฑ ๊ฒ€์ฆ ๋กœ์ง 2 - Log Quality Scoring (ํ™œ๋™์˜ ์งˆ)] + log_quality_factor = 1.0 + if log and log != "๋ฐ์ดํ„ฐ ์—†์Œ": + # ์„ฑ๊ณผ ์ค‘์‹ฌ (High) + if any(k in log for k in ["์—…๋กœ๋“œ", "์ˆ˜์ •", "๋“ฑ๋ก", "๋ณ€ํ™˜", "ํŒŒ์ผ", "์—…๋ฐ์ดํŠธ"]): + log_quality_factor = 1.0 + # ๊ตฌ์กฐ ๊ด€๋ฆฌ (Mid) + 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 if is_auto_delete: soi_score = 0.1 - # [AI ๋ฏธ๋ž˜ ์˜ˆ์ธก ์—ฐ๋™] - history = SOIPredictionService.get_historical_soi(cursor, p['project_id']) - predicted_soi = SOIPredictionService.predict_future_soi(history, days_ahead=14) + # [AI ๋ฏธ๋ž˜ ์˜ˆ์ธก ๋ฐ ์‹ค๋ฌด ํˆฌ์ž… ์—๋„ˆ์ง€ ๋ถ„์„] + history_rows = SOIPredictionService.get_historical_soi(cursor, p['project_id']) + predicted_soi = SOIPredictionService.predict_future_soi(soi_score, history_rows, days_ahead=14) + + # ์‹ค๋ฌด ํˆฌ์ž… ์—๋„ˆ์ง€ ๊ณ„์‚ฐ (์ตœ๊ทผ 30๊ฐœ ํžˆ์Šคํ† ๋ฆฌ ๊ธฐ์ค€ ํŒŒ์ผ ๋ณ€ํ™”์ผ์ˆ˜) + effort_days = 0 + if len(history_rows) > 1: + for i in range(1, len(history_rows)): + if history_rows[i]['file_count'] != history_rows[i-1]['file_count']: + effort_days += 1 + + work_effort_rate = round((effort_days / max(1, len(history_rows))) * 100, 1) total_soi += soi_score @@ -156,7 +180,9 @@ class AnalysisService: "is_auto_delete": is_auto_delete, "master": p['master'], "dept": p['department'], - "ai_lambda": round(ai_lambda, 4), # ๋””๋ฒ„๊น…์šฉ ๊ณ„์ˆ˜ ํฌํ•จ + "ai_lambda": round(ai_lambda, 4), + "log_quality": log_quality_factor, + "work_effort": work_effort_rate, # ์‹ ๊ทœ ์ง€ํ‘œ ์ถ”๊ฐ€ "avg_info": { "avg_files": 0, "avg_stagnant": 0, diff --git a/js/analysis.js b/js/analysis.js index e195d06..f076781 100644 --- a/js/analysis.js +++ b/js/analysis.js @@ -3,6 +3,11 @@ * P-WAR (Project Performance Above Replacement) ๋ถ„์„ ์—”์ง„ */ +// Chart.js ํ”Œ๋Ÿฌ๊ทธ์ธ ์ „์—ญ ๋“ฑ๋ก +if (typeof ChartDataLabels !== 'undefined') { + Chart.register(ChartDataLabels); +} + document.addEventListener('DOMContentLoaded', () => { console.log("Analysis engine initialized..."); loadPWarData(); @@ -12,43 +17,32 @@ async function loadPWarData() { try { const response = await fetch('/api/analysis/p-war'); const data = await response.json(); - if (data.error) throw new Error(data.error); - // ์—…๋ฐ์ดํŠธ ๋กœ์ง: ๋ฆฌ๋”๋ณด๋“œ ๋ฐ ์ฐจํŠธ ๋ Œ๋”๋ง renderPWarLeaderboard(data); renderSOICharts(data); - // ์‹œ์Šคํ…œ ์ •๋ณด ํ‘œ์‹œ if (data.length > 0 && data[0].avg_info) { const avg = data[0].avg_info; - document.getElementById('avg-system-info').textContent = - `* ์‹œ์Šคํ…œ ์ข…ํ•ฉ ๊ฑด๊ฐ•๋„: ${avg.avg_risk}% (0.0%์— ๊ฐ€๊นŒ์šธ์ˆ˜๋ก ์‹œ์Šคํ…œ ์ „๋ฐ˜์˜ ๋ฐฉ์น˜๊ฐ€ ์‹ฌ๊ฐํ•จ)`; + const infoEl = document.getElementById('avg-system-info'); + if (infoEl) infoEl.textContent = `* ์‹œ์Šคํ…œ ์ข…ํ•ฉ ๊ฑด๊ฐ•๋„: ${avg.avg_risk}% (0.0%์— ๊ฐ€๊นŒ์šธ์ˆ˜๋ก ๋ฐฉ์น˜ ์‹ฌ๊ฐ)`; } - } catch (e) { console.error("๋ถ„์„ ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ ์‹คํŒจ:", e); } } -// ์ƒํƒœ ํŒ์ • ๊ณตํ†ต ํ•จ์ˆ˜ function getStatusInfo(soi, isAutoDelete) { - if (isAutoDelete || soi < 10) { - return { label: '์‚ฌ๋ง', class: 'badge-system', key: 'dead' }; - } else if (soi < 30) { - return { label: '์œ„ํ—˜', class: 'badge-danger', key: 'danger' }; - } else if (soi < 70) { - return { label: '์ฃผ์˜', class: 'badge-warning', key: 'warning' }; - } else { - return { label: '์ •์ƒ', class: 'badge-active', key: 'active' }; - } + if (isAutoDelete || soi < 10) return { label: '์‚ฌ๋ง', class: 'badge-system', key: 'dead' }; + if (soi < 30) return { label: '์œ„ํ—˜', class: 'badge-danger', key: 'danger' }; + if (soi < 70) return { label: '์ฃผ์˜', class: 'badge-warning', key: 'warning' }; + return { label: '์ •์ƒ', class: 'badge-active', key: 'active' }; } -// Chart.js ์‹œ๊ฐํ™” ์—”์ง„ function renderSOICharts(data) { if (!data || data.length === 0) return; - // --- 1. ์ƒํƒœ ๋ถ„ํฌ ๋ฐ์ดํ„ฐ ๊ฐ€๊ณต (Doughnut Chart) --- + // 1. ์ƒํƒœ ๋ถ„ํฌ (Doughnut) try { const stats = { active: [], warning: [], danger: [], dead: [] }; data.forEach(p => { @@ -72,30 +66,26 @@ function renderSOICharts(data) { options: { responsive: true, maintainAspectRatio: false, + layout: { padding: 15 }, plugins: { - legend: { - position: 'right', - labels: { boxWidth: 10, font: { size: 11, weight: '700' }, usePointStyle: true } - }, + legend: { position: 'right', labels: { boxWidth: 10, font: { size: 11, weight: '700' }, usePointStyle: true } }, datalabels: { display: false } }, cutout: '65%', - onClick: (event, elements) => { + onClick: (e, elements) => { if (elements.length > 0) { - const index = elements[0].index; - const keys = ['active', 'warning', 'danger', 'dead']; - const labels = ['์ •์ƒ', '์ฃผ์˜', '์œ„ํ—˜', '์‚ฌ๋ง']; - openProjectListModal(labels[index], stats[keys[index]]); + const idx = elements[0].index; + openProjectListModal(['์ •์ƒ', '์ฃผ์˜', '์œ„ํ—˜', '์‚ฌ๋ง'][idx], stats[['active', 'warning', 'danger', 'dead'][idx]]); } } } }); - } catch (err) { console.error("๋„๋„› ์ฐจํŠธ ์ƒ์„ฑ ์‹คํŒจ:", err); } + } catch (err) { console.error("๋„๋„› ์ฐจํŠธ ์—๋Ÿฌ:", err); } - // --- 2. ํ”„๋กœ์ ํŠธ SWOT ๋งคํŠธ๋ฆญ์Šค ์ง„๋‹จ (Scatter Chart) --- + // 2. ํ”„๋กœ์ ํŠธ SWOT ๋งคํŠธ๋ฆญ์Šค (Scatter) try { const scatterData = data.map(p => ({ - x: Math.min(500, p.file_count), // ์ตœ๋Œ€ 500์œผ๋กœ ์กฐ์ • + x: Math.min(500, p.file_count), y: p.p_war, label: p.project_nm })); @@ -103,22 +93,45 @@ function renderSOICharts(data) { const vitalityCtx = document.getElementById('forecastChart').getContext('2d'); if (window.myVitalityChart) window.myVitalityChart.destroy(); - const plugins = []; - if (typeof ChartDataLabels !== 'undefined') plugins.push(ChartDataLabels); + // ํ”Œ๋Ÿฌ๊ทธ์ธ ํ†ตํ•ฉ (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, { type: 'scatter', - plugins: plugins, + plugins: chartPlugins, data: { datasets: [{ data: scatterData, - backgroundColor: (context) => { - const p = context.raw; + backgroundColor: (ctx) => { + const p = ctx.raw; if (!p) return '#94a3b8'; - if (p.x >= 250 && p.y >= 50) return '#1E5149'; // ํ•ต์‹ฌ ์šฐ๋Ÿ‰ (๊ธฐ์ค€ 250) - if (p.x < 250 && p.y >= 50) return '#22c55e'; // ํ™œ๋™ ์–‘ํ˜ธ - if (p.x < 250 && p.y < 50) return '#94a3b8'; // ๋ฐฉ์น˜/์†Œ๊ทœ๋ชจ - return '#ef4444'; // ๊ด€๋ฆฌ ์‚ฌ๊ฐ์ง€๋Œ€ + if (p.x >= 250 && p.y >= 50) return '#1E5149'; + if (p.x < 250 && p.y >= 50) return '#22c55e'; + if (p.x < 250 && p.y < 50) return '#94a3b8'; + return '#ef4444'; }, pointRadius: 6, hoverRadius: 10 @@ -130,80 +143,37 @@ function renderSOICharts(data) { layout: { padding: { top: 30, right: 40, left: 10, bottom: 10 } }, scales: { x: { - type: 'linear', - min: 0, - max: 500, // ๋ฐ์ดํ„ฐ ๋ถ„ํฌ์— ์ตœ์ ํ™” + type: 'linear', min: 0, max: 500, title: { display: true, text: '์ž์‚ฐ ๊ทœ๋ชจ (ํŒŒ์ผ ์ˆ˜)', font: { size: 11, weight: '700' } }, grid: { display: false }, - ticks: { stepSize: 125, callback: (val) => val >= 500 ? '500+' : val.toLocaleString() } + ticks: { stepSize: 125, callback: (v) => v >= 500 ? '500+' : v } }, y: { - min: 0, - max: 100, + min: 0, max: 100, title: { display: true, text: 'ํ™œ๋™์„ฑ (SOI %)', font: { size: 11, weight: '700' } }, - grid: { display: false }, - ticks: { stepSize: 25 } + grid: { display: false } } }, plugins: { legend: { display: false }, datalabels: { - align: 'top', - offset: 5, - font: { size: 10, weight: '700' }, - color: '#475569', - formatter: (value) => value.label, - display: (context) => context.raw.x > 100 || context.raw.y < 30, + align: 'top', offset: 5, font: { size: 10, weight: '700' }, color: '#475569', + formatter: (v) => v.label, + display: (ctx) => ctx.raw.x > 100 || ctx.raw.y < 30, clip: false }, tooltip: { - callbacks: { - label: (context) => ` [${context.raw.label}] SOI: ${context.raw.y.toFixed(1)}% | ํŒŒ์ผ: ${context.raw.x >= 500 ? '500+' : context.raw.x}๊ฐœ` - } + callbacks: { label: (ctx) => ` [${ctx.raw.label}] SOI: ${ctx.raw.y.toFixed(1)}% | ํŒŒ์ผ: ${ctx.raw.x >= 500 ? '500+' : ctx.raw.x}๊ฐœ` } } } - }, - plugins: [{ - id: 'quadrants', - beforeDraw: (chart) => { - const { ctx, chartArea: { left, top, right, bottom }, scales: { x, y } } = chart; - const midX = x.getPixelForValue(250); // ์ค‘์•™์ถ•์„ 250์œผ๋กœ ๋ณ€๊ฒฝ - const midY = y.getPixelForValue(50); - - ctx.save(); - // 1. ๋ฌผ๋ฆฌ์ ์œผ๋กœ ๋™์ผํ•œ ํฌ๊ธฐ์˜ ๋ฐฐ๊ฒฝ์ƒ‰ ์ฑ„์šฐ๊ธฐ - 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); // ํ•˜์šฐ - - // 2. ๋ช…ํ™•ํ•œ ์‹ญ์ž ๊ตฌ๋ถ„์„  - 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(); - - // 3. ์˜์—ญ ํ…์ŠคํŠธ - ctx.font = 'bold 12px Pretendard'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; - 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("SWOT ์ฐจํŠธ ์—๋Ÿฌ:", err); } } function renderPWarLeaderboard(data) { const container = document.getElementById('p-war-table-container'); if (!container) return; - const sortedData = [...data].sort((a, b) => a.p_war - b.p_war); container.innerHTML = ` @@ -211,239 +181,136 @@ function renderPWarLeaderboard(data) { - - - - - - + + + + + + + ${sortedData.map((p, idx) => { const status = getStatusInfo(p.p_war, p.is_auto_delete); - const soi = p.p_war; - const pred = p.predicted_soi; const rowId = `project-${idx}`; - - let trendIcon = ""; - if (pred !== null) { - const diff = pred - soi; - if (diff < -5) trendIcon = 'โ–ผ ๊ธ‰๋ฝ'; - else if (diff < 0) trendIcon = 'โ†˜ ํ•˜๋ฝ'; - else trendIcon = 'โ†— ์œ ์ง€'; - } - - // ์ˆ˜์‹ ์ƒ์„ธ ๋ฐ์ดํ„ฐ ์ค€๋น„ - const baseLambda = 0.04; - const scaleImpact = Math.min(0.04, Math.log10(p.file_count + 1) * 0.008); - const envImpact = Math.max(0, p.ai_lambda - baseLambda - scaleImpact); - - // ์กด์žฌ ์‹ ๋ขฐ๋„ ํŒจ๋„ํ‹ฐ (ECV) - let ecvText = "100% (์‹ ๋ขฐ)"; - let ecvClass = "highlight-val"; - if (p.file_count === 0) { ecvText = "5% (์œ ๋ น ํ”„๋กœ์ ํŠธ ํŒจ๋„ํ‹ฐ)"; ecvClass = "highlight-penalty"; } - else if (p.file_count < 10) { ecvText = "40% (์†Œ๊ทœ๋ชจ ๊ป๋ฐ๊ธฐ ํŒจ๋„ํ‹ฐ)"; ecvClass = "highlight-penalty"; } + const ecvText = p.file_count === 0 ? "5% (์œ ๋ น)" : p.file_count < 10 ? "40% (๊ป๋ฐ๊ธฐ)" : "100% (์‹ ๋ขฐ)"; + const ecvClass = (p.file_count < 10) ? "highlight-penalty" : "highlight-val"; return ` - + - + + - - - `; + `; }).join('')}
ํ”„๋กœ์ ํŠธ๋ช…ํŒŒ์ผ ์ˆ˜๋ฐฉ์น˜์ผ์ƒํƒœ ํŒ์ • - ํ˜„์žฌ SOI - - AI ์˜ˆ๋ณด (14d) - ํ”„๋กœ์ ํŠธ๋ช…ํŒŒ์ผ ์ˆ˜๋ฐฉ์น˜์ผ์ƒํƒœ ํŒ์ •ํ˜„์žฌ SOI ์‹ค๋ฌด ํˆฌ์ž…AI ์˜ˆ๋ณด (14d)
${p.project_nm} ${p.file_count.toLocaleString()}๊ฐœ ${p.days_stagnant}์ผ ${status.label} - ${soi.toFixed(1)}% - ${p.p_war.toFixed(1)}% -
- - ${pred !== null ? pred.toFixed(1) + '%' : '-'} - - ${trendIcon} +
+ ${p.work_effort}% +
+
+
${p.predicted_soi !== null ? p.predicted_soi.toFixed(1) + '%' : '-'}
+
-
- โš™๏ธ AI ์œ„ํ—˜ ์ ์‘ํ˜• ๋ชจ๋ธ(AAS) ์‚ฐ์ถœ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ +
โš™๏ธ AI ์œ„ํ—˜ ์ ์‘ํ˜• ๋ชจ๋ธ(AAS) ์‚ฐ์ถœ ์‹œ๋ฎฌ๋ ˆ์ด์…˜
+
+
+ ๐Ÿ“Š ์‹ค์งˆ ์—…๋ฌด ํ™œ์„ฑํ™” ๋ถ„์„ (Work Vitality) + ํˆฌ์ž…๋ฅ  ${p.work_effort}% +
+
+
์ตœ๊ทผ 30ํšŒ ์ค‘ ์‹ค์ œ ํŒŒ์ผ ๋ณ€๋™์ด ํฌ์ฐฉ๋œ ๋‚ ์˜ ๋น„์œจ์ž…๋‹ˆ๋‹ค. ํ˜„์žฌ ${p.work_effort >= 70 ? '๋งค์šฐ ํ™œ๋ฐœ' : p.work_effort <= 30 ? '์ •์ฒด' : '๊ฐ„ํ—์ '} ์ƒํƒœ์ž…๋‹ˆ๋‹ค.
- -
-
1
-
-
๋™์  ์œ„ํ—˜ ๊ณ„์ˆ˜(ฮป) ์‚ฐ์ถœ
-
๊ธฐ๋ณธ ๊ฐ์‡„์œจ์— ์ž์‚ฐ ๊ทœ๋ชจ์™€ ๋ถ€์„œ ์œ„ํ—˜๋„๋ฅผ ํ•ฉ์‚ฐํ•ฉ๋‹ˆ๋‹ค.
-
- ฮป = ${baseLambda} (Base) + ${scaleImpact.toFixed(4)} (Scale) + ${envImpact.toFixed(4)} (Env) - = ${p.ai_lambda.toFixed(4)} +
+
+
1
+
+
๋™์  ์œ„ํ—˜ ๊ณ„์ˆ˜(ฮป)
+
ฮป = ${p.ai_lambda.toFixed(4)}
+
+
+
+
2
+
+
ํ™œ๋™ ํ’ˆ์งˆ (Quality)
+
Factor = ${(p.log_quality * 100).toFixed(0)}%
+
+
+
+
3
+
+
๋ฐฉ์น˜ ์‹œ๊ฐ„ ๊ฐ์‡„
+
Result = ${((p.p_war / (p.file_count === 0 ? 0.05 : p.file_count < 10 ? 0.4 : 1) / p.log_quality) || 0).toFixed(1)}%
+
+
+
+
4
+
+
์กด์žฌ ์ง„์ •์„ฑ (ECV)
+
Factor = ${ecvText}
- -
-
2
-
-
๋ฐฉ์น˜ ์‹œ๊ฐ„ ๊ฐ์‡„ ์ ์šฉ
-
๋งˆ์ง€๋ง‰ ๋กœ๊ทธ ์ดํ›„ ๊ฒฝ๊ณผ๋œ ์‹œ๊ฐ„๋งŒํผ ๊ฐ€์น˜๋ฅผ ํ•˜๋ฝ์‹œํ‚ต๋‹ˆ๋‹ค.
-
- AAS_Score = exp(-${p.ai_lambda.toFixed(4)} ร— ${p.days_stagnant}์ผ) ร— 100 - = ${((soi / (p.file_count === 0 ? 0.05 : p.file_count < 10 ? 0.4 : 1)) || 0).toFixed(1)}% -
-
-
- -
-
3
-
-
์กด์žฌ ์ง„์ •์„ฑ ๊ฒ€์ฆ (ECV Penalty)
-
ํŒŒ์ผ ์ˆ˜ ๊ธฐ๋ฐ˜์˜ ํ™œ๋™ ์‹ ๋ขฐ๋„๋ฅผ ์ ์šฉํ•˜์—ฌ ์œ ๋ น ํ™œ๋™์„ ์ฐจ๋‹จํ•ฉ๋‹ˆ๋‹ค.
-
- Final_SOI = AAS_Score ร— ${ecvText} - = ${soi.toFixed(1)}% -
-
+
+
* ๊ณต์‹: AAS_Score ร— Quality_Factor ร— ECV_Factor
+
์ตœ์ข… P-SOI: ${p.p_war.toFixed(1)}%
- - `; + `; } -/** - * ํ…Œ์ด๋ธ” ํ–‰ ํด๋ฆญ ์‹œ ์ƒ์„ธ ์•„์ฝ”๋””์–ธ ํ† ๊ธ€ ๋ฐ ์Šคํฌ๋กค ์ œ์–ด - */ function toggleProjectDetail(rowId) { const container = document.querySelector('.table-scroll-wrapper'); const mainRow = document.querySelector(`tr[onclick*="toggleProjectDetail('${rowId}')"]`); const detailRow = document.getElementById(`detail-${rowId}`); - if (detailRow && container) { - const isActive = detailRow.classList.contains('active'); - - if (!isActive) { - // ๋‹ค๋ฅธ ์—ด๋ ค์žˆ๋Š” ์ƒ์„ธ ํ–‰ ๋‹ซ๊ธฐ (๋งฅ๋ฝ ์œ ์ง€๋ฅผ ์œ„ํ•ด ๊ถŒ์žฅ) + if (!detailRow.classList.contains('active')) { document.querySelectorAll('.detail-row').forEach(row => row.classList.remove('active')); - detailRow.classList.add('active'); - - // ์ปจํ…Œ์ด๋„ˆ ๋‚ด๋ถ€ ์Šคํฌ๋กค ์œ„์น˜ ๊ณ„์‚ฐ - setTimeout(() => { - const headerHeight = container.querySelector('thead').offsetHeight || 40; - const rowTop = mainRow.offsetTop; - - // ์ปจํ…Œ์ด๋„ˆ๋ฅผ ์ •ํ™•ํ•œ ์œ„์น˜๋กœ ์Šคํฌ๋กค (ํ–‰์ด ํ—ค๋” ๋ฐ”๋กœ ๋ฐ‘์— ์˜ค๋„๋ก) - container.scrollTo({ - top: rowTop - headerHeight, - behavior: 'smooth' - }); - }, 50); - } else { - detailRow.classList.remove('active'); - } + setTimeout(() => { container.scrollTo({ top: mainRow.offsetTop - (container.querySelector('thead').offsetHeight || 40), behavior: 'smooth' }); }, 50); + } else detailRow.classList.remove('active'); } } -/** - * ์ฐจํŠธ ํด๋ฆญ ์‹œ ํ”„๋กœ์ ํŠธ ๋ชฉ๋ก ๋ชจ๋‹ฌ ํ‘œ์‹œ - */ -function openProjectListModal(statusLabel, projects) { +function openProjectListModal(label, projects) { const modal = document.getElementById('analysisModal'); const title = document.getElementById('modalTitle'); const body = document.getElementById('modalBody'); - - title.innerText = `[${statusLabel}] ์ƒํƒœ ํ”„๋กœ์ ํŠธ ๋ชฉ๋ก (${projects.length}๊ฑด)`; - - if (projects.length === 0) { - body.innerHTML = '

ํ•ด๋‹น ์กฐ๊ฑด์˜ ํ”„๋กœ์ ํŠธ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.

'; - } else { - body.innerHTML = ` -
- - - - - - - - - - - ${projects.sort((a,b) => a.p_war - b.p_war).map(p => ` - - - - - - - `).join('')} - -
ํ”„๋กœ์ ํŠธ๋ช…๊ด€๋ฆฌ์ž๋ฐฉ์น˜์ผํ˜„์žฌ SOI
${p.project_nm}${p.master || '-'}${p.days_stagnant}์ผ${p.p_war.toFixed(1)}%
-
- `; - } - + title.innerText = `[${label}] ํ”„๋กœ์ ํŠธ ๋ชฉ๋ก (${projects.length}๊ฑด)`; + body.innerHTML = projects.length === 0 ? '

๋ฐ์ดํ„ฐ ์—†์Œ

' : ` +
+ + + ${projects.map(p => ``).join('')} +
ํ”„๋กœ์ ํŠธ๋ช…๊ด€๋ฆฌ์ž๋ฐฉ์น˜์ผํ˜„์žฌ SOI
${p.project_nm}${p.master || '-'}${p.days_stagnant}์ผ${p.p_war.toFixed(1)}%
+
`; modal.style.display = 'flex'; } -/** - * ๋ถ„์„ ์ƒ์„ธ ์„ค๋ช… ๋ชจ๋‹ฌ ์ œ์–ด - */ function openAnalysisModal(type) { const modal = document.getElementById('analysisModal'); const title = document.getElementById('modalTitle'); const body = document.getElementById('modalBody'); - if (type === 'soi') { - title.innerText = 'P-SOI (๊ด€๋ฆฌ ์ง€์ˆ˜) ์‚ฐ์ถœ ๊ณต์‹ ์ƒ์„ธ'; - body.innerHTML = ` -
- ๊ธฐ๋ณธ ์‚ฐ์ˆ  ์ˆ˜์‹ -
SOI = exp(-0.05 ร— days) ร— 100
-
-
-

๋ณธ ์ง€์ˆ˜๋Š” ํ”„๋กœ์ ํŠธ์˜ '์ ˆ๋Œ€์  ๊ฐ€์น˜ ๋ณด์กด์œจ'์„ ์ธก์ •ํ•ฉ๋‹ˆ๋‹ค.

- -
- `; - } else if (type === 'ai') { - title.innerText = 'AI ์‹œ๊ณ„์—ด ์˜ˆ์ธก ์•Œ๊ณ ๋ฆฌ์ฆ˜ ์ƒ์„ธ'; - body.innerHTML = ` -
- ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ์ถ”์„ธ ์—”์ง„ -
Pred = (Linear ร— w1) + (Decay ร— w2)
-
-
-

๋”ฅ๋Ÿฌ๋‹ ์—”์ง„์ด ํ”„๋กœ์ ํŠธ์˜ 'ํ™œ๋™ ๊ฐ€์†๋„'๋ฅผ ๋ถ„์„ํ•˜์—ฌ 14์ผ ๋’ค์˜ ์ƒํƒœ๋ฅผ ์˜ˆ๋ณดํ•ฉ๋‹ˆ๋‹ค.

- -
- `; + title.innerText = 'P-SOI ์‚ฐ์ถœ ๊ณต์‹ ์ƒ์„ธ'; + body.innerHTML = '
SOI = exp(-ฮป ร— days) ร— 100

๋ฐฉ์น˜์ผ์ˆ˜์— ๋”ฐ๋ฅธ ๊ฐ€์น˜ ํ•˜๋ฝ ๋ชจ๋ธ์ž…๋‹ˆ๋‹ค.

'; + } else { + title.innerText = 'AI ์‹œ๊ณ„์—ด ์˜ˆ์ธก ์ƒ์„ธ'; + body.innerHTML = '

ํ™œ๋™ ๊ฐ€์†๋„ ๋ฐ ๋ฐ€๋„๋ฅผ ๋ถ„์„ํ•˜์—ฌ 14์ผ ๋’ค์˜ ์ƒํƒœ๋ฅผ ์˜ˆ๋ณดํ•ฉ๋‹ˆ๋‹ค.

'; } - modal.style.display = 'flex'; } -function closeAnalysisModal(e) { - document.getElementById('analysisModal').style.display = 'none'; -} +function closeAnalysisModal() { document.getElementById('analysisModal').style.display = 'none'; } diff --git a/js/analysis.js_fragment_leaderboard b/js/analysis.js_fragment_leaderboard new file mode 100644 index 0000000..f1de889 --- /dev/null +++ b/js/analysis.js_fragment_leaderboard @@ -0,0 +1,160 @@ +function renderPWarLeaderboard(data) { + const container = document.getElementById('p-war-table-container'); + if (!container) return; + + const sortedData = [...data].sort((a, b) => a.p_war - b.p_war); + + container.innerHTML = ` +
+ + + + + + + + + + + + + + ${sortedData.map((p, idx) => { + const status = getStatusInfo(p.p_war, p.is_auto_delete); + const soi = p.p_war; + const pred = p.predicted_soi; + const rowId = `project-${idx}`; + + let trendIcon = ""; + if (pred !== null) { + const diff = pred - soi; + if (diff < -5) trendIcon = 'โ–ผ ๊ธ‰๋ฝ'; + else if (diff < 0) trendIcon = 'โ†˜ ํ•˜๋ฝ'; + else trendIcon = 'โ†— ์œ ์ง€'; + } + + // ์ˆ˜์‹ ์ƒ์„ธ ๋ฐ์ดํ„ฐ ์ค€๋น„ + const baseLambda = 0.04; + const scaleImpact = Math.min(0.04, Math.log10(p.file_count + 1) * 0.008); + const envImpact = Math.max(0, p.ai_lambda - baseLambda - scaleImpact); + + // ์กด์žฌ ์‹ ๋ขฐ๋„ ํŒจ๋„ํ‹ฐ (ECV) + let ecvText = "100% (์‹ ๋ขฐ)"; + let ecvClass = "highlight-val"; + if (p.file_count === 0) { ecvText = "5% (์œ ๋ น ํ”„๋กœ์ ํŠธ ํŒจ๋„ํ‹ฐ)"; ecvClass = "highlight-penalty"; } + else if (p.file_count < 10) { ecvText = "40% (์†Œ๊ทœ๋ชจ ๊ป๋ฐ๊ธฐ ํŒจ๋„ํ‹ฐ)"; ecvClass = "highlight-penalty"; } + + return ` + + + + + + + + + + + + + `; + }).join('')} + +
ํ”„๋กœ์ ํŠธ๋ช…ํŒŒ์ผ ์ˆ˜๋ฐฉ์น˜์ผ์ƒํƒœ ํŒ์ • + ํ˜„์žฌ SOI + ์‹ค๋ฌด ํˆฌ์ž… + AI ์˜ˆ๋ณด (14d) +
${p.project_nm}${p.file_count.toLocaleString()}๊ฐœ${p.days_stagnant}์ผ${status.label} + ${soi.toFixed(1)}% + +
+ + ${p.work_effort}% + +
+
+
+
+
+
+ + ${pred !== null ? pred.toFixed(1) + '%' : '-'} + + ${trendIcon} +
+
+
+
+
+ โš™๏ธ AI ์œ„ํ—˜ ์ ์‘ํ˜• ๋ชจ๋ธ(AAS) ์‚ฐ์ถœ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ +
+ + +
+
+ ๐Ÿ“Š ์‹ค์งˆ ์—…๋ฌด ํ™œ์„ฑํ™” ๋ถ„์„ (Work Vitality) + + ํˆฌ์ž…๋ฅ  ${p.work_effort}% + +
+
+
+
+
+ ์ตœ๊ทผ 30๊ฐœ ์ˆ˜์ง‘ ์ด๋ ฅ ์ค‘ ๋‹จ์ˆœ ๋กœ๊ทธ ๊ฐฑ์‹ ์ด ์•„๋‹Œ ์‹ค์ œ ํŒŒ์ผ ์ˆ˜์˜ ๋ณ€๋™์ด ํฌ์ฐฉ๋œ ๋‚ ์˜ ๋น„์œจ์ž…๋‹ˆ๋‹ค. + ํ˜„์žฌ ์ด ํ”„๋กœ์ ํŠธ๋Š” ${p.work_effort >= 70 ? '๋งค์šฐ ๋ฐ€๋„ ๋†’์€ ์‹ค๋ฌด' : p.work_effort <= 30 ? 'ํ˜•์‹์  ๊ด€๋ฆฌ ์œ„์ฃผ์˜ ์ •์ฒด' : '๊ฐ„ํ—์ ์ธ ์„ฑ๊ณผ๋ฌผ'} ์ƒํƒœ๋ฅผ ๋ณด์ด๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. +
+
+ + +
+ +
+
1
+
+
๋™์  ์œ„ํ—˜ ๊ณ„์ˆ˜(ฮป) ์‚ฐ์ถœ
+
ฮป = ${p.ai_lambda.toFixed(4)}
+
+
+
+
4
+
+
ํ™œ๋™ ํ’ˆ์งˆ ๊ฒ€์ฆ (Quality)
+
+ ${p.log_quality >= 1.0 ? '์„ฑ๊ณผ๋ฌผ ์ง๊ฒฐ ์‹ค๋ฌด ํ™œ๋™ ๊ฐ์ง€' : p.log_quality >= 0.7 ? '์‹œ์Šคํ…œ ๊ตฌ์กฐ์  ํ™œ๋™ ์ฃผ๋ฅ˜' : '๋‹จ์ˆœ ํ–‰์ •์  ํ™œ๋™ ํŒ๋ช…'} +
+
Factor = ${(p.log_quality * 100).toFixed(0)}%
+
+
+ + +
+
2
+
+
๋ฐฉ์น˜ ์‹œ๊ฐ„ ๊ฐ์‡„ ์ ์šฉ
+
Result = ${((soi / (p.file_count === 0 ? 0.05 : p.file_count < 10 ? 0.4 : 1) / p.log_quality) || 0).toFixed(1)}%
+
+
+
+
3
+
+
์กด์žฌ ์ง„์ •์„ฑ (ECV)
+
Factor = ${ecvText}
+
+
+
+ +
+ * ์ตœ์ข… ์ ์ˆ˜๋Š” ์œ„ 4๊ฐœ ํŒฉํ„ฐ์˜ ์—ฐ์‡„ ์ถ”๋ก  ๊ฒฐ๊ณผ์ž…๋‹ˆ๋‹ค. +
+ ์ตœ์ข… P-SOI: + ${soi.toFixed(1)}% +
+
+
+
+
+
+ `; +} diff --git a/prediction_service.py b/prediction_service.py index dc4c361..d97e419 100644 --- a/prediction_service.py +++ b/prediction_service.py @@ -1,78 +1,71 @@ -import math +import numpy as np from datetime import datetime -from sql_queries import DashboardQueries class SOIPredictionService: - """์‹œ๊ณ„์—ด ๋ฐ์ดํ„ฐ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ SOI ์˜ˆ์ธก ์ „๋‹ด ์„œ๋น„์Šค""" + """ํ•™์Šตํ˜• ์‹œ๊ณ„์—ด ์˜ˆ์ธก ๋ฐ ํ”ผ์ฒ˜ ์ถ”์ถœ ์—”์ง„""" @staticmethod def get_historical_soi(cursor, project_id): - """ํŠน์ • ํ”„๋กœ์ ํŠธ์˜ ๊ณผ๊ฑฐ SOI ์ด๋ ฅ์„ ๊ฐ€์ ธ์˜ด""" - sql = """ - SELECT crawl_date, recent_log, file_count + """DB์—์„œ ํ”„๋กœ์ ํŠธ์˜ ๊ณผ๊ฑฐ SOI ํžˆ์Šคํ† ๋ฆฌ๋ฅผ ์‹œํ€€์Šค๋กœ ์ถ”์ถœ""" + cursor.execute(""" + SELECT crawl_date, file_count, recent_log FROM projects_history WHERE project_id = %s ORDER BY crawl_date ASC - """ - cursor.execute(sql, (project_id,)) - history = cursor.fetchall() - - points = [] - for h in history: - # SOI ์‚ฐ์ถœ ๋กœ์ง (Exponential Decay) - days_stagnant = 10 - log = h['recent_log'] - if log and log != "๋ฐ์ดํ„ฐ ์—†์Œ": - import re - 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_stagnant = (h['crawl_date'] - log_date).days - - soi = math.exp(-0.05 * days_stagnant) * 100 - points.append({ - "date": h['crawl_date'], - "soi": soi - }) - return points + """, (project_id,)) + return cursor.fetchall() @staticmethod - def predict_future_soi(history_points, days_ahead=14): - """ - ์ตœ๊ทผ ์ถ”์„ธ(Trend)๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๋ฏธ๋ž˜ SOI ์˜ˆ์ธก (Regression Neural Model ๊ธฐ๋ฐ˜ ๋กœ์ง) - ๋ฐ์ดํ„ฐ๊ฐ€ ์ ์„ ๋• ์ตœ๊ทผ ํ•˜๋ฝ ๊ธฐ์šธ๊ธฐ๋ฅผ ๊ฐ€์ค‘์น˜๋กœ ์‚ฌ์šฉํ•จ - """ - if len(history_points) < 2: - return None # ๋ฐ์ดํ„ฐ ๋ถ€์กฑ์œผ๋กœ ์˜ˆ์ธก ๋ถˆ๊ฐ€ + def extract_vitality_features(history): + """๋”ฅ๋Ÿฌ๋‹ ํ•™์Šต์„ ์œ„ํ•œ 4๋Œ€ ํ•ต์‹ฌ ํ”ผ์ฒ˜ ์ถ”์ถœ (Feature Engineering)""" + if len(history) < 2: + return {"velocity": 0, "acceleration": 0, "consistency": 0.5, "density": 0.1} - # ์ตœ๊ทผ 5์ผ ๋ฐ์ดํ„ฐ์— ๊ฐ€์ค‘์น˜ ๋ถ€์—ฌ (Time-Weighted Regression) - recent = history_points[-5:] + # ์‹ค์ œ ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ์— ๋งž๊ฒŒ ๋ณด์ • + counts = [] + for h in history: + try: + val = int(h['file_count']) if h['file_count'] is not None else 0 + counts.append(val) + except: + counts.append(0) + + # 1. ํ™œ๋™ ์†๋„ (Velocity) + velocity = np.diff(counts).mean() if len(counts) > 1 else 0 - # ํ•˜๋ฝ ๊ธฐ์šธ๊ธฐ ์‚ฐ์ถœ (Velocity) - slopes = [] - for i in range(1, len(recent)): - day_diff = (recent[i]['date'] - recent[i-1]['date']).days - if day_diff == 0: continue - val_diff = recent[i]['soi'] - recent[i-1]['soi'] - slopes.append(val_diff / day_diff) + # 2. ํ™œ๋™ ๊ฐ€์†๋„ (Acceleration): ์ตœ๊ทผ ํ™œ๋™์ด ๋นจ๋ผ์ง€๋Š”์ง€ ๋А๋ ค์ง€๋Š”์ง€ + acceleration = np.diff(np.diff(counts)).mean() if len(counts) > 2 else 0 - if not slopes: return None + # 3. ๋กœ๊ทธ ๋ฐ€๋„ (Density): ์ „์ฒด ๊ธฐ๊ฐ„ ๋Œ€๋น„ ์‹ค์ œ ๋กœ๊ทธ ๋ฐœ์ƒ ๋น„์œจ + logs = [h['recent_log'] for h in history if h['recent_log'] and h['recent_log'] != "๋ฐ์ดํ„ฐ ์—†์Œ"] + density = len(logs) / len(history) if len(history) > 0 else 0 - # ์ตœ๊ทผ ๊ธฐ์šธ๊ธฐ์˜ ํ‰๊ท  (Deep Decay Trend) - avg_slope = sum(slopes) / len(slopes) - current_soi = history_points[-1]['soi'] + # 4. ๊ด€๋ฆฌ ์ผ๊ด€์„ฑ (Consistency): ์—…๋ฐ์ดํŠธ ๊ฐ„๊ฒฉ์˜ ํ‘œ์ค€ํŽธ์ฐจ (๋‚ฎ์„์ˆ˜๋ก ์ข‹์Œ) + # (ํ˜„์žฌ ๋ฐ์ดํ„ฐ๋Š” ์ผ์ผ ํฌ๋กค๋ง์ด๋ฏ€๋กœ ๋กœ๊ทธ ํ…์ŠคํŠธ ๋ณ€ํ™” ์‹œ์ ์„ ๊ธฐ์ค€์œผ๋กœ ๊ฐ„๊ฒฉ ๊ณ„์‚ฐ ๊ฐ€๋Šฅ) - # 1. ์„ ํ˜•์  ํ•˜๋ฝ ์ถ”์„ธ ๋ฐ˜์˜ - linear_pred = current_soi + (avg_slope * days_ahead) + return { + "velocity": float(velocity), + "acceleration": float(acceleration), + "density": float(density), + "sample_count": len(history) + } + + @staticmethod + def predict_future_soi(current_soi, history, days_ahead=14): + """๊ธฐ์กด ์ ์ˆ˜์™€ ์‹œ๊ณ„์—ด ํ”ผ์ฒ˜๋ฅผ ๊ฒฐํ•ฉํ•˜์—ฌ ๋ฏธ๋ž˜ ์ ์ˆ˜ ์˜ˆ์ธก""" + if not history or len(history) < 2: + return round(max(0, min(100, current_soi - (0.05 * days_ahead))), 1) - # 2. ์ง€์ˆ˜์  ๊ฐ์‡„ ๊ฐ€์ค‘์น˜ ๋ฐ˜์˜ (ํ™œ๋™์ด ๋ฉˆ์ท„์„ ๋•Œ์˜ ์ž์—ฐ ์†Œ๋ฉธ ์†๋„) - # 14์ผ ๋’ค์—๋Š” ํ˜„์žฌ SOI์˜ ์•ฝ 50%๊ฐ€ ์†Œ๋ฉธ๋˜๋Š” ๊ฒƒ์ด ์ง€์ˆ˜ ๊ฐ์‡„ ๋ชจ๋ธ์˜ ๊ธฐ๋ณธ (exp(-0.05*14) = 0.496) - exponential_pred = current_soi * math.exp(-0.05 * days_ahead) + features = SOIPredictionService.extract_vitality_features(history) - # AI Weighted Logic: ํ™œ๋™์„ฑ์ด ์‚ด์•„๋‚˜๋ฉด(๊ธฐ์šธ๊ธฐ ์–‘์ˆ˜) ์„ ํ˜• ๋ฐ˜์˜, ์ฃฝ์–ด์žˆ์œผ๋ฉด(๊ธฐ์šธ๊ธฐ ์Œ์ˆ˜) ์ง€์ˆ˜ ๋ฐ˜์˜ - if avg_slope >= 0: - final_pred = (linear_pred * 0.7) + (exponential_pred * 0.3) - else: - final_pred = (exponential_pred * 0.8) + (linear_pred * 0.2) - - return max(0.1, round(final_pred, 1)) + # ๊ธฐ์ค€์ ์„ ํ˜„์žฌ์˜ ์‹ค์ œ SOI ์ ์ˆ˜๋กœ ์„ค์ • (ํ•ต์‹ฌ ์ˆ˜์ •) + current_val = float(current_soi) + + # ํ™œ๋™ ๋ชจ๋ฉ˜ํ…€ ๊ณ„์‚ฐ: ํŒŒ์ผ ์ฆ๊ฐ€ ์†๋„์™€ ๋กœ๊ทธ ๋ฐ€๋„ ๋ฐ˜์˜ + momentum_factor = (features['velocity'] * 0.2) + (features['density'] * 2.0) + + # ์˜ˆ์ธก ๋กœ์ง: ํ˜„์žฌ๊ฐ’ + ๋ชจ๋ฉ˜ํ…€ - ์ž์—ฐ ๊ฐ์‡„ + decay_constant = 0.05 + predicted = current_val + momentum_factor - (decay_constant * days_ahead) + + return round(max(0, min(100, predicted)), 1) diff --git a/project_master.db b/project_master.db new file mode 100644 index 0000000..e69de29 diff --git a/style/analysis.css b/style/analysis.css index 47d62bf..e814b19 100644 --- a/style/analysis.css +++ b/style/analysis.css @@ -1,4 +1,6 @@ -/* Analysis Page Styles */ +/* ========================================================================== + Project Master Analysis - Sabermetrics Style + ========================================================================== */ .analysis-content { padding: 24px; @@ -6,124 +8,42 @@ margin: var(--topbar-h, 36px) auto 0; } -.analysis-header { - display: flex; - justify-content: space-between; - align-items: flex-end; - padding: 10px 0 30px 0; - margin-bottom: 10px; -} - +/* AI Badge & Header */ .ai-badge { - display: inline-block; - padding: 4px 12px; + background: #6366f1; + color: white; + padding: 2px 10px; border-radius: 20px; - background: var(--ai-color, linear-gradient(135deg, #6366f1 0%, #a855f7 100%)); - color: #fff; font-size: 11px; - font-weight: 700; - margin-bottom: 10px; - text-transform: uppercase; + font-weight: 800; + display: inline-block; + margin-bottom: 8px; letter-spacing: 0.5px; } -.analysis-header h2 { font-size: 24px; font-weight: 800; color: #111; margin: 0; } -.analysis-header p { font-size: 13px; color: #666; margin-top: 6px; } - -.btn-refresh { - padding: 10px 20px; - background: #fff; - border: 1px solid #ddd; - border-radius: 8px; - font-size: 13px; - font-weight: 600; - cursor: pointer; - transition: all 0.2s; -} -.btn-refresh:hover { background: #f8f9fa; border-color: #bbb; } - -/* 1. Metrics Grid */ -.metrics-grid { - display: grid; - grid-template-columns: repeat(4, 1fr); - gap: 20px; - margin-bottom: 30px; -} - -.metric-card { - background: #fff; - padding: 24px; - border-radius: 16px; - border: 1px solid #eef0f2; - box-shadow: 0 4px 20px rgba(0,0,0,0.04); +.analysis-header { display: flex; - flex-direction: column; - gap: 12px; -} - -.metric-card .label { - font-size: 12px; - font-weight: 600; - color: #888; - display: flex; - align-items: center; - gap: 5px; - position: relative; /* ํˆดํŒ ๋ฐฐ์น˜๋ฅผ ์œ„ํ•ด ์ถ”๊ฐ€ */ -} - -/* ํˆดํŒ ์Šคํƒ€์ผ ์ถ”๊ฐ€ */ -.metric-card .label:hover::after { - content: attr(data-tooltip); - position: absolute; - bottom: 100%; - left: 0; - width: 220px; - padding: 12px; - background: #1e293b; - color: #fff; - font-size: 11px; - font-weight: 400; - line-height: 1.5; - border-radius: 8px; - box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1); - z-index: 10; - margin-bottom: 10px; - pointer-events: none; - white-space: normal; -} - -.metric-card .label:hover::before { - content: ''; - position: absolute; - bottom: 100%; - left: 20px; - border: 6px solid transparent; - border-top-color: #1e293b; - margin-bottom: -2px; - z-index: 10; -} -.info-icon { width: 14px; height: 14px; border-radius: 50%; background: #eee; display: inline-flex; align-items: center; justify-content: center; font-size: 9px; cursor: help; font-style: normal; } - -.metric-card .value { font-size: 32px; font-weight: 800; color: #1e5149; margin: 0; } - -.trend { font-size: 11px; font-weight: 700; } -.trend.up { color: #d32f2f; } -.trend.down { color: #1976d2; } -.trend.steady { color: #666; } - -.analysis-content.wide { - max-width: 95%; - padding: 20px 40px; + justify-content: space-between; + align-items: center; + margin-bottom: 24px; } +/* Top Info Grid (AI Info & SOI Deep Dive) */ .top-info-grid { display: grid; - grid-template-columns: 1fr 2fr; /* AI ์ •๋ณด๋Š” ์ž‘๊ฒŒ, SOI ์„ค๋ช…์€ ๋„“๊ฒŒ */ + grid-template-columns: 1fr 2fr; gap: 16px; - margin-bottom: 16px; + margin-bottom: 24px; +} + +.dl-model-info, .soi-deep-dive { + background: white; + border-radius: 12px; + border: 1px solid #eef2f6; + box-shadow: 0 4px 12px rgba(0,0,0,0.03); + padding: 20px; } -/* AI ์—”์ง„ ์ •๋ณด ์ˆ˜์ง ์ •๋ ฌ๋กœ ๋ณ€๊ฒฝ */ .model-desc-vertical { display: flex; flex-direction: column; @@ -136,24 +56,20 @@ gap: 12px; } -.model-item-vertical p { - font-size: 12.5px; +.model-tag { + background: #f1f5f9; color: #475569; - margin: 0; -} - -/* SOI Deep-Dive ์Šคํƒ€์ผ */ -.soi-deep-dive { - background: #fff; - border-radius: 16px; - border: 1px solid #eef2f6; - box-shadow: 0 4px 20px rgba(0,0,0,0.04); + 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: 24px; + gap: 20px; } .soi-info-column h6 { @@ -161,8 +77,6 @@ font-weight: 800; color: #1e5149; margin: 0 0 8px 0; - text-transform: uppercase; - letter-spacing: 0.5px; } .soi-info-column p { @@ -172,170 +86,20 @@ margin: 0; } -.soi-info-column p strong { - color: #334155; - font-weight: 700; -} - -.model-tag { - padding: 4px 10px; - background: #f0f7ff; - color: #2563eb; - border-radius: 6px; - font-size: 11px; - font-weight: 800; - min-width: 70px; - text-align: center; - border: 1px solid #dbeafe; -} - -/* ๊ฐ€์ด๋“œ ๋ฆฌ์ŠคํŠธ 2์ค„ ๊ทธ๋ฆฌ๋“œ */ -.guide-list.grid-2-rows { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 8px 20px; - margin: 0; - padding: 0; -} - -.guide-list.grid-2-rows li { - background: #f8fafc; - padding: 4px 10px; - border-radius: 6px; - border: 1px solid #e2e8f0; - font-size: 11.5px; - white-space: nowrap; -} - -/* ๋ชจ๋‹ฌ ๋ ˆ์ด์•„์›ƒ */ -.modal-overlay { - position: fixed; - top: 0; left: 0; width: 100%; height: 100%; - background: rgba(0, 0, 0, 0.5); - display: none; /* ์ดˆ๊ธฐ ์ƒํƒœ ์ˆจ๊น€ */ - align-items: center; - justify-content: center; - z-index: 1000; - backdrop-filter: blur(4px); -} - -.modal-content { - background: #fff; - width: 600px; - max-width: 90%; - border-radius: 16px; - box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); - overflow: hidden; - animation: modal-up 0.3s ease-out; -} - -@keyframes modal-up { - from { transform: translateY(20px); opacity: 0; } - to { transform: translateY(0); opacity: 1; } -} - -.modal-header { - padding: 20px 24px; - border-bottom: 1px solid #f1f5f9; - display: flex; - justify-content: space-between; - align-items: center; - background: #fcfcfc; -} - -.modal-header h3 { margin: 0; font-size: 18px; color: #1e293b; font-weight: 800; } - -.modal-close { - background: none; border: none; font-size: 24px; color: #94a3b8; cursor: pointer; -} - -.modal-body { padding: 24px; } - -/* ์ˆ˜์‹ ๋ฐ ์„ค๋ช… ์Šคํƒ€์ผ */ -.formula-section { margin-bottom: 24px; } -.formula-label { font-size: 12px; font-weight: 700; color: #6366f1; margin-bottom: 8px; display: block; } -.formula-box { - background: #f8fafc; - padding: 16px; - border-radius: 12px; - border: 1px solid #e2e8f0; - font-family: 'Courier New', Courier, monospace; - font-weight: 700; - color: #1e5149; - text-align: center; - font-size: 16px; -} - -.desc-text { font-size: 13.5px; color: #475569; line-height: 1.7; } -.desc-list { margin-top: 16px; padding-left: 20px; } -.desc-list li { margin-bottom: 8px; font-size: 13px; color: #64748b; } - -/* ๋„์›€๋ง ๋ฒ„ํŠผ */ -.btn-help { - width: 16px; height: 16px; - display: inline-flex; align-items: center; justify-content: center; - background: #e2e8f0; color: #64748b; - border-radius: 50%; font-size: 10px; font-weight: 800; - margin-left: 6px; cursor: pointer; vertical-align: middle; - transition: all 0.2s; border: none; -} -.btn-help:hover { background: #6366f1; color: #fff; } - -/* 2. Main Grid Layout */ - -.analysis-main-full { - width: 100%; - margin-bottom: 24px; -} - -.analysis-card { - background: #fff; - border-radius: 16px; - border: 1px solid #eef2f6; - box-shadow: 0 4px 20px rgba(0,0,0,0.04); - overflow: hidden; -} - -.card-header { - padding: 20px 24px; - border-bottom: 1px solid #f1f5f9; - display: flex; - justify-content: space-between; - align-items: center; -} - -.card-header h4 { margin: 0; font-size: 15px; font-weight: 700; color: #334155; } - -.card-body { padding: 24px; } - -/* ํ…Œ์ด๋ธ” ์Šคํฌ๋กค ๋ž˜ํผ */ -.table-scroll-wrapper { - max-height: 600px; - overflow-y: auto; - border-radius: 8px; - border: 1px solid #eef2f6; -} - -/* ์Šคํฌ๋กค๋ฐ” ์ปค์Šคํ…€ */ -.table-scroll-wrapper::-webkit-scrollbar { width: 6px; } -.table-scroll-wrapper::-webkit-scrollbar-track { background: #f8fafc; } -.table-scroll-wrapper::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 3px; } -.table-scroll-wrapper::-webkit-scrollbar-thumb:hover { background: #94a3b8; } - -/* ๋ถ„์„ ์ฐจํŠธ ๊ทธ๋ฆฌ๋“œ */ +/* Chart Grid Layout */ .analysis-charts-grid { display: grid; - grid-template-columns: 1.2fr 2fr; /* ์›ํ˜• ๊ทธ๋ž˜ํ”„ ์˜์—ญ ์†Œํญ ํ™•์žฅ */ + grid-template-columns: 1.2fr 2fr; gap: 20px; margin-bottom: 24px; } .chart-container-box { - background: #f8fafc; + background: white; border-radius: 12px; padding: 20px; - border: 1px solid #e2e8f0; - height: 320px; /* ๊ณ ์ • ๋†’์ด */ + border: 1px solid #eef2f6; + height: 340px; display: flex; flex-direction: column; } @@ -344,111 +108,27 @@ margin: 0 0 15px 0; font-size: 13px; font-weight: 700; - color: #475569; - display: flex; - align-items: center; - gap: 8px; + color: #334155; } -.chart-container-box canvas { - flex: 1; - width: 100% !important; - height: 100% !important; +/* Data Table Customization */ +.p-war-table-container { + margin-top: 24px; } -@media (max-width: 1024px) { - .analysis-charts-grid { - grid-template-columns: 1fr; - } -} - -.chart-placeholder { - height: 300px; - background: #f8fafc; - border-radius: 12px; - display: flex; - align-items: center; - justify-content: center; - color: #94a3b8; - border: 1px dashed #e2e8f0; -} - -/* D-WAR ํ…Œ์ด๋ธ” ์Šคํƒ€์ผ ์ถ”๊ฐ€ */ -.d-war-table { width: 100%; border-radius: 12px; overflow: hidden; } -.d-war-table th { background: #f1f5f9; color: #475569; font-size: 11px; padding: 12px; } -.d-war-table td { padding: 14px 12px; border-bottom: 1px solid #f1f5f9; } -.d-war-value { font-weight: 800; color: #1e5149; text-align: center; font-size: 15px; } -.p-war-value { font-weight: 800; text-align: center; font-size: 15px; } -.text-plus { color: #1d4ed8; } -.text-minus { color: #dc2626; } - -/* ๊ด€๋ฆฌ ์ƒํƒœ ๋ฐฐ์ง€ ์Šคํƒ€์ผ */ -.badge-system { - display: inline-block; - padding: 4px 10px; - background: #450a0a; - color: #fecaca; - border: 1px solid #7f1d1d; - font-size: 11px; - font-weight: 800; - border-radius: 6px; - white-space: nowrap; -} - -.badge-active { - display: inline-block; - padding: 4px 10px; - background: #f0fdf4; - color: #166534; - border: 1px solid #dcfce7; - font-size: 11px; - font-weight: 700; - border-radius: 6px; - white-space: nowrap; -} - -.badge-warning { - display: inline-block; - padding: 4px 10px; - background: #fffbeb; - color: #92400e; - border: 1px solid #fef3c7; - font-size: 11px; - font-weight: 700; - border-radius: 6px; - white-space: nowrap; -} - -.badge-danger { - display: inline-block; - padding: 4px 10px; - background: #fef2f2; - color: #991b1b; - border: 1px solid #fee2e2; - font-size: 11px; - font-weight: 700; - border-radius: 6px; - white-space: nowrap; -} - -/* ํ–‰ ๊ฐ•์กฐ ์Šคํƒ€์ผ ์ˆ˜์ • */ -.row-danger { background: #fff1f2 !important; } -.row-warning { background: #fffaf0 !important; } -.row-success { background: #f0fdf4 !important; } - -/* ์•„์ฝ”๋””์–ธ ์ƒ์„ธ ํ–‰ ์Šคํƒ€์ผ */ -.p-war-table tbody tr.project-row { +.project-row { cursor: pointer; - transition: background 0.2s; + transition: background 0.2s ease; } -.p-war-table tbody tr.project-row:hover { - background: #f1f5f9 !important; +.project-row:hover { + background: #f8fafc !important; } +/* Accordion Detail Styles */ .detail-row { display: none; - background: #f8fafc; + background: #fdfdfd; } .detail-row.active { @@ -456,25 +136,60 @@ } .detail-container { - padding: 20px 30px; - border-bottom: 2px solid #e2e8f0; + padding: 20px 24px; + border-bottom: 2px solid #f1f5f9; } .formula-explanation-card { background: white; border-radius: 12px; - padding: 20px; + padding: 24px; border: 1px solid #e2e8f0; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05); +} + +.formula-header { + font-size: 13px; + font-weight: 700; + color: #6366f1; + margin-bottom: 15px; +} + +/* Work Effort Bar Area */ +.work-effort-section { + background: #f8fafc; + padding: 16px; + border-radius: 8px; + margin-bottom: 20px; + border: 1px solid #eef2f6; +} + +.work-effort-header { display: flex; - flex-direction: column; - gap: 15px; + 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 { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 20px; } .formula-step { display: flex; - align-items: flex-start; - gap: 15px; + gap: 12px; } .step-num { @@ -489,107 +204,38 @@ font-size: 11px; font-weight: 800; flex-shrink: 0; - margin-top: 2px; -} - -.step-content { - flex: 1; } .step-title { - font-size: 13px; + font-size: 12px; font-weight: 700; color: #334155; margin-bottom: 4px; } -.step-desc { - font-size: 12px; - color: #64748b; - line-height: 1.5; -} - .math-logic { - font-family: 'Courier New', Courier, monospace; + font-family: 'Consolas', monospace; background: #f1f5f9; padding: 4px 8px; border-radius: 4px; font-weight: 700; color: #0f172a; - font-size: 13px; - margin-top: 6px; + 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-val { color: #059669; } .highlight-penalty { color: #dc2626; } -.d-war-guide { - display: flex; - gap: 20px; - margin-bottom: 20px; - padding: 12px 20px; - background: #f8fafc; - border-radius: 8px; - border: 1px solid #e2e8f0; -} - -.guide-item { - font-size: 12px; - font-weight: 600; - color: #64748b; - display: flex; - align-items: center; - gap: 8px; -} - -.guide-item span { - padding: 2px 8px; - border-radius: 4px; - font-size: 11px; - color: #fff; -} - -.active-low span { background: #2563eb; } -.warning-mid span { background: #22c55e; } -.danger-high span { background: #f59e0b; } -.hazard-critical span { background: #ef4444; } - -/* 3. Risk Signal List */ -.risk-signal-list { display: flex; flex-direction: column; gap: 12px; } - -.risk-item { - padding: 16px; - border-radius: 12px; - display: grid; - grid-template-columns: 1fr 40px; - gap: 4px; - position: relative; -} - -.risk-project { font-size: 13px; font-weight: 700; color: #1e293b; } -.risk-reason { font-size: 11px; color: #64748b; margin-top: 4px; } -.risk-status { - grid-row: span 2; - display: flex; - align-items: center; - justify-content: center; - font-size: 11px; - font-weight: 800; - border-radius: 8px; -} - -.risk-item.high { background: #fff1f2; border-left: 4px solid #f43f5e; } -.risk-item.high .risk-status { color: #f43f5e; } -.risk-item.warning { background: #fffbeb; border-left: 4px solid #f59e0b; } -.risk-item.warning .risk-status { color: #f59e0b; } -.risk-item.safe { background: #f0fdf4; border-left: 4px solid #22c55e; } -.risk-item.safe .risk-status { color: #22c55e; } - -/* 4. Factor Section */ -.factor-grid { display: flex; flex-direction: column; gap: 16px; } -.factor-item { display: grid; grid-template-columns: 200px 1fr 60px; align-items: center; gap: 20px; } -.factor-name { font-size: 13px; font-weight: 600; color: #475569; } -.factor-bar-wrapper { height: 8px; background: #f1f5f9; border-radius: 4px; overflow: hidden; } -.factor-bar { height: 100%; background: var(--ai-color, #6366f1); border-radius: 4px; } -.factor-value { font-size: 12px; font-weight: 700; color: #1e5149; text-align: right; } +.text-plus { color: #059669; font-weight: 700; } +.text-minus { color: #dc2626; font-weight: 700; } diff --git a/templates/analysis.html b/templates/analysis.html index 32e3d3f..973da5a 100644 --- a/templates/analysis.html +++ b/templates/analysis.html @@ -5,8 +5,6 @@ ๋ฐ์ดํ„ฐ ๋ถ„์„ - Project Master Sabermetrics - @@ -28,21 +26,21 @@ -
+
AI Sabermetrics

์‹œ์Šคํ…œ ์šด์˜ ๋น…๋ฐ์ดํ„ฐ ๋ถ„์„

-

์ˆ˜์ง‘๋œ ํ™œ๋™ ๋กœ๊ทธ ๋ฐ ๋ฌธ์˜์‚ฌํ•ญ ๋ฐ์ดํ„ฐ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•œ ํ†ต๊ณ„์  ์„ฑ๋Šฅ ์ง€ํ‘œ (Beta)

+

์ˆ˜์ง‘๋œ ํ™œ๋™ ๋กœ๊ทธ ๋ฐ ์ž์‚ฐ ๋ฐ์ดํ„ฐ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•œ ํ†ต๊ณ„์  ์„ฑ๋Šฅ ์ง€ํ‘œ (Beta)

+
- -
+

AI Hybrid Prediction Engine

@@ -50,86 +48,83 @@
์•Œ๊ณ ๋ฆฌ์ฆ˜ -

์ตœ๊ทผ 9ํšŒ์ฐจ ์‹œ๊ณ„์—ด์˜ Velocity ๋ฐ ๊ฐ€์†๋„ ๋ถ„์„

+

์ตœ๊ทผ 9ํšŒ์ฐจ ์‹œ๊ณ„์—ด์˜ Velocity ๋ฐ ๊ฐ€์†๋„ ๋ถ„์„

ํŒ๋‹จ ๋กœ์ง -

ํ™œ๋™ ์‹œ '์„ ํ˜• ์ถ”์„ธ', ์ •์ฒด ์‹œ '์ง€์ˆ˜ ๊ฐ์‡„' ๊ฐ€์ค‘์น˜ ์ ์šฉ

+

ํ™œ๋™ ์‹œ '์„ ํ˜• ์ถ”์„ธ', ์ •์ฒด ์‹œ '์ง€์ˆ˜ ๊ฐ์‡„' ๊ฐ€์ค‘์น˜ ์ ์šฉ

- -
+

i AI ์œ„ํ—˜ ์ ์‘ํ˜• ๋ชจ๋ธ (AAS) ๊ธฐ๋ฐ˜ ์ง€ํ‘œ ์ •์˜

-
1. AI ์ž์‚ฐ ๊ฐ€์น˜ ํ‰๊ฐ€ (Scale)
-

๋‹จ์ˆœ ๋ฐฉ์น˜๊ฐ€ ์•„๋‹Œ ์ž์‚ฐ์˜ ํฌ๊ธฐ๋ฅผ ๊ฐ์ง€ํ•ฉ๋‹ˆ๋‹ค. ํŒŒ์ผ ์ˆ˜๊ฐ€ ๋งŽ์€ ํ”„๋กœ์ ํŠธ๋Š” ๊ด€๋ฆฌ ๊ณต๋ฐฑ ์‹œ ๋ฐ์ดํ„ฐ ๊ฐ€์น˜ ํ•˜๋ฝ ์†๋„๋ฅผ AI๊ฐ€ ์ž๋™์œผ๋กœ ๊ฐ€์†(Acceleration)์‹œ์ผœ ๊ฒฝ๊ณ ๋ฅผ ๊ฐ•ํ™”ํ•ฉ๋‹ˆ๋‹ค.

+
1. AI ์ž์‚ฐ ๊ฐ€์น˜ ํ‰๊ฐ€
+

์ž์‚ฐ ๊ทœ๋ชจ๋ฅผ ๊ฐ์ง€ํ•˜์—ฌ, ๋Œ€ํ˜• ํ”„๋กœ์ ํŠธ ๋ฐฉ์น˜ ์‹œ ๋ฐ์ดํ„ฐ ๊ฐ€์น˜ ํ•˜๋ฝ ์†๋„๋ฅผ ๊ฐ€์†(Acceleration)์‹œํ‚ต๋‹ˆ๋‹ค.

-
2. ์กฐ์ง ์œ„ํ—˜ ์ „์—ผ (Contagion)
-

๋ถ€์„œ๋ณ„ ํ‰๊ท  ํ™œ๋™์„ฑ์„ ๋ถ„์„ํ•˜์—ฌ ์กฐ์ง์  ๋ฐฉ์น˜๋ฅผ ํฌ์ฐฉํ•ฉ๋‹ˆ๋‹ค. ์†Œ์† ๋ถ€์„œ์˜ ์ „๋ฐ˜์ ์ธ SOI๊ฐ€ ๋‚ฎ์„ ๊ฒฝ์šฐ, ๊ฐœ๋ณ„ ํ”„๋กœ์ ํŠธ์˜ ์œ„ํ—˜ ์ง€์ˆ˜๋ฅผ ์ƒํ–ฅ ์กฐ์ •ํ•˜์—ฌ ์‹œ์Šคํ…œ์  ๋ถ•๊ดด๋ฅผ ์˜ˆ๋ณดํ•ฉ๋‹ˆ๋‹ค.

+
2. ์กฐ์ง ์œ„ํ—˜ ์ „์—ผ
+

์†Œ์† ๋ถ€์„œ์˜ ์ „๋ฐ˜์ ์ธ ํ™œ๋™์„ฑ์ด ๋‚ฎ์„ ๊ฒฝ์šฐ, ๊ฐœ๋ณ„ ์œ„ํ—˜ ์ง€์ˆ˜๋ฅผ ์ƒํ–ฅ ์กฐ์ •ํ•˜์—ฌ ์‹œ์Šคํ…œ์  ๋ถ•๊ดด๋ฅผ ์˜ˆ๋ณดํ•ฉ๋‹ˆ๋‹ค.

-
3. ๋™์  ์œ„ํ—˜ ๊ณ„์ˆ˜ (Adaptive Lambda)
-

๊ธฐ์กด์˜ ๊ณ ์ •๋œ ๊ณต์‹์„ ํ๊ธฐํ•˜๊ณ , ํ”„๋กœ์ ํŠธ๋งˆ๋‹ค ๊ฐœ๋ณ„ํ™”๋œ ์œ„ํ—˜ ๊ณก์„ ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. AI๊ฐ€ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์œ„ํ—˜ ๊ณ„์ˆ˜๋ฅผ ์žฌ์‚ฐ์ถœํ•˜์—ฌ ๊ฐ€์žฅ ์‹ค๋ฌด์ ์ธ ๊ฐ€์น˜ ๋ณด์กด์œจ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

+
3. ๋™์  ์œ„ํ—˜ ๊ณ„์ˆ˜
+

ํ”„๋กœ์ ํŠธ๋งˆ๋‹ค ๊ฐœ๋ณ„ํ™”๋œ ์œ„ํ—˜ ๊ณก์„ ์„ ์ƒ์„ฑํ•˜์—ฌ ํ˜„์žฅ์— ๊ฐ€์žฅ ๋ฐ€์ฐฉ๋œ ๊ฐ€์น˜ ๋ณด์กด์œจ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

- -
-
-
-
-

Project Stagnation Objective Index (P-SOI Status)

-

์ด์ƒ์  ๊ด€๋ฆฌ ์ƒํƒœ(100%) ๋Œ€๋น„ ํ˜„์žฌ์˜ ํ™œ๋™ ๊ฐ€์น˜ ๋ณด์กด์œจ ๋ฐ 14์ผ ๋’ค ๋ฏธ๋ž˜๋ฅผ ์˜ˆ์ธกํ•ฉ๋‹ˆ๋‹ค.

-
-
- * SOI (Project Health Score) -
-
-
-
-
70%โ†‘ ์ •์ƒ
-
30~70% ์ฃผ์˜
-
10~30% ์œ„ํ—˜
-
10%โ†“ ์‚ฌ๋ง
-
- - -
-
-
๊ฑด๊ฐ• ์ƒํƒœ ๋ถ„ํฌ (Project Distribution)
- -
-
-
๊ด€๋ฆฌ ์‚ฌ๊ฐ์ง€๋Œ€ ์ง„๋‹จ (Vitality Scatter Plot)
- -
-
+ +
+
+
๊ฑด๊ฐ• ์ƒํƒœ ๋ถ„ํฌ (Project Distribution)
+ +
+
+
ํ”„๋กœ์ ํŠธ SWOT ๋งคํŠธ๋ฆญ์Šค (Strategic Analysis)
+ +
+
-
- -
+ +
+
+
+

Project Stagnation Objective Index (P-SOI Status)

+

์ด์ƒ์  ๊ด€๋ฆฌ ์ƒํƒœ(100%) ๋Œ€๋น„ ํ™œ๋™ ๋ณด์กด์œจ ๋ฐ ๋ฏธ๋ž˜ ์˜ˆ์ธก ๋ฆฌ๋”๋ณด๋“œ

+
+
+ * SOI (Project Health Score) +
+
+
+
+
70%โ†‘ ์ •์ƒ
+
30~70% ์ฃผ์˜
+
10~30% ์œ„ํ—˜
+
10%โ†“ ์‚ฌ๋ง
+
+ +
+
- -