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) {
- | ํ๋ก์ ํธ๋ช
|
- ํ์ผ ์ |
- ๋ฐฉ์น์ผ |
- ์ํ ํ์ |
-
- ํ์ฌ SOI
- |
-
- AI ์๋ณด (14d)
- |
+ ํ๋ก์ ํธ๋ช
|
+ ํ์ผ ์ |
+ ๋ฐฉ์น์ผ |
+ ์ํ ํ์ |
+ ํ์ฌ SOI |
+ ์ค๋ฌด ํฌ์
|
+ AI ์๋ณด (14d) |
${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 `
-
+
| ${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.predicted_soi !== null ? p.predicted_soi.toFixed(1) + '%' : '-'} |
- |
+ |
|
-
- `;
+ `;
}).join('')}
-
- `;
+ `;
}
-/**
- * ํ
์ด๋ธ ํ ํด๋ฆญ ์ ์์ธ ์์ฝ๋์ธ ํ ๊ธ ๋ฐ ์คํฌ๋กค ์ ์ด
- */
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 = `
-
- `;
- }
-
+ title.innerText = `[${label}] ํ๋ก์ ํธ ๋ชฉ๋ก (${projects.length}๊ฑด)`;
+ body.innerHTML = projects.length === 0 ? '๋ฐ์ดํฐ ์์
' : `
+ `;
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 = `
-
-
-
๋ณธ ์ง์๋ ํ๋ก์ ํธ์ '์ ๋์ ๊ฐ์น ๋ณด์กด์จ'์ ์ธก์ ํฉ๋๋ค.
-
- - ์ด์์ ์ํ (100%): ์ต๊ทผ 24์๊ฐ ์ด๋ด ํ๋ ๋ก๊ทธ๊ฐ ๋ฐ์ํ ๊ฒฝ์ฐ์
๋๋ค.
- - ์ง์ ๊ฐ์ ๋ชจ๋ธ: ๋ฐฉ์น์ผ์๊ฐ ๋์ด๋ ์๋ก ๊ฐ์น๊ฐ ๊ธฐํ๊ธ์์ ์ผ๋ก ํ๋ฝํ๋๋ก ์ค๊ณ๋์์ต๋๋ค. (14์ผ ๋ฐฉ์น ์ ์ฝ 50% ์์ค)
- - ์์คํ
์ฌ๋ง: ์ง์๊ฐ 10% ๋ฏธ๋ง์ผ ๊ฒฝ์ฐ, ํ๋ ์ฌ๊ฐ ๊ฐ๋ฅ์ฑ์ด ํฌ๋ฐํ ์ข๋น ๋ฐ์ดํฐ๋ก ๊ฐ์ฃผํฉ๋๋ค.
-
-
- `;
- } else if (type === 'ai') {
- title.innerText = 'AI ์๊ณ์ด ์์ธก ์๊ณ ๋ฆฌ์ฆ ์์ธ';
- body.innerHTML = `
-
-
-
๋ฅ๋ฌ๋ ์์ง์ด ํ๋ก์ ํธ์ 'ํ๋ ๊ฐ์๋'๋ฅผ ๋ถ์ํ์ฌ 14์ผ ๋ค์ ์ํ๋ฅผ ์๋ณดํฉ๋๋ค.
-
- - ์ถ์ธ ๋ถ์ (Linear): ์ต๊ทผ ํ๋ ๋ก๊ทธ์ ๋น๋๊ฐ ์ฆ๊ฐ ์ถ์ธ์ผ ๊ฒฝ์ฐ, ํฅํ ๊ด๋ฆฌ ์ฌ๊ฐ ๊ฐ๋ฅ์ฑ์ ๋๊ฒ ํ๊ฐํ์ฌ ๊ฐ์ ์ ๋ถ์ฌํฉ๋๋ค.
- - ์์ฐ ์๋ฉธ (Decay): ์ฅ๊ธฐ ์ ์ฒด ์ค์ธ ํ๋ก์ ํธ๋ ์ง์ ๊ฐ์ ๋ชจ๋ธ์ 80% ์ด์ ๋ฐ์ํ์ฌ ๊ธ๊ฒฉํ ํ๋ฝ์ ๊ฒฝ๊ณ ํฉ๋๋ค.
- - ์ ๋ฐ๋: ํ์ฌ Regression ๊ธฐ๋ฐ ๋ชจ๋ธ์ด๋ฉฐ, ๋ฐ์ดํฐ๊ฐ 30ํ ์ด์ ์ถ์ ๋๋ฉด LSTM ์ ๊ฒฝ๋ง์ผ๋ก ์๋ ์ ํ๋ฉ๋๋ค.
-
-
- `;
+ title.innerText = 'P-SOI ์ฐ์ถ ๊ณต์ ์์ธ';
+ body.innerHTML = '๋ฐฉ์น์ผ์์ ๋ฐ๋ฅธ ๊ฐ์น ํ๋ฝ ๋ชจ๋ธ์
๋๋ค.
';
+ } 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 = `
+
+ `;
+}
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 @@
-
+
+
-
-
+
@@ -50,86 +48,83 @@
์๊ณ ๋ฆฌ์ฆ
-
์ต๊ทผ 9ํ์ฐจ ์๊ณ์ด์ Velocity ๋ฐ ๊ฐ์๋ ๋ถ์
+
์ต๊ทผ 9ํ์ฐจ ์๊ณ์ด์ Velocity ๋ฐ ๊ฐ์๋ ๋ถ์
ํ๋จ ๋ก์ง
-
ํ๋ ์ '์ ํ ์ถ์ธ', ์ ์ฒด ์ '์ง์ ๊ฐ์' ๊ฐ์ค์น ์ ์ฉ
+
ํ๋ ์ '์ ํ ์ถ์ธ', ์ ์ฒด ์ '์ง์ ๊ฐ์' ๊ฐ์ค์น ์ ์ฉ
-
-
+
-
1. AI ์์ฐ ๊ฐ์น ํ๊ฐ (Scale)
-
๋จ์ ๋ฐฉ์น๊ฐ ์๋ ์์ฐ์ ํฌ๊ธฐ๋ฅผ ๊ฐ์งํฉ๋๋ค. ํ์ผ ์๊ฐ ๋ง์ ํ๋ก์ ํธ๋ ๊ด๋ฆฌ ๊ณต๋ฐฑ ์ ๋ฐ์ดํฐ ๊ฐ์น ํ๋ฝ ์๋๋ฅผ AI๊ฐ ์๋์ผ๋ก ๊ฐ์(Acceleration)์์ผ ๊ฒฝ๊ณ ๋ฅผ ๊ฐํํฉ๋๋ค.
+
1. AI ์์ฐ ๊ฐ์น ํ๊ฐ
+
์์ฐ ๊ท๋ชจ๋ฅผ ๊ฐ์งํ์ฌ, ๋ํ ํ๋ก์ ํธ ๋ฐฉ์น ์ ๋ฐ์ดํฐ ๊ฐ์น ํ๋ฝ ์๋๋ฅผ ๊ฐ์(Acceleration)์ํต๋๋ค.
-
2. ์กฐ์ง ์ํ ์ ์ผ (Contagion)
-
๋ถ์๋ณ ํ๊ท ํ๋์ฑ์ ๋ถ์ํ์ฌ ์กฐ์ง์ ๋ฐฉ์น๋ฅผ ํฌ์ฐฉํฉ๋๋ค. ์์ ๋ถ์์ ์ ๋ฐ์ ์ธ SOI๊ฐ ๋ฎ์ ๊ฒฝ์ฐ, ๊ฐ๋ณ ํ๋ก์ ํธ์ ์ํ ์ง์๋ฅผ ์ํฅ ์กฐ์ ํ์ฌ ์์คํ
์ ๋ถ๊ดด๋ฅผ ์๋ณดํฉ๋๋ค.
+
2. ์กฐ์ง ์ํ ์ ์ผ
+
์์ ๋ถ์์ ์ ๋ฐ์ ์ธ ํ๋์ฑ์ด ๋ฎ์ ๊ฒฝ์ฐ, ๊ฐ๋ณ ์ํ ์ง์๋ฅผ ์ํฅ ์กฐ์ ํ์ฌ ์์คํ
์ ๋ถ๊ดด๋ฅผ ์๋ณดํฉ๋๋ค.
-
3. ๋์ ์ํ ๊ณ์ (Adaptive Lambda)
-
๊ธฐ์กด์ ๊ณ ์ ๋ ๊ณต์์ ํ๊ธฐํ๊ณ , ํ๋ก์ ํธ๋ง๋ค ๊ฐ๋ณํ๋ ์ํ ๊ณก์ ์ ์์ฑํฉ๋๋ค. AI๊ฐ ์ค์๊ฐ์ผ๋ก ์ํ ๊ณ์๋ฅผ ์ฌ์ฐ์ถํ์ฌ ๊ฐ์ฅ ์ค๋ฌด์ ์ธ ๊ฐ์น ๋ณด์กด์จ์ ์ ๊ณตํฉ๋๋ค.
+
3. ๋์ ์ํ ๊ณ์
+
ํ๋ก์ ํธ๋ง๋ค ๊ฐ๋ณํ๋ ์ํ ๊ณก์ ์ ์์ฑํ์ฌ ํ์ฅ์ ๊ฐ์ฅ ๋ฐ์ฐฉ๋ ๊ฐ์น ๋ณด์กด์จ์ ์ ๊ณตํฉ๋๋ค.
-
-
-
-
-
-
-
70%โ ์ ์
-
30~70% ์ฃผ์
-
10~30% ์ํ
-
10%โ ์ฌ๋ง
-
-
-
-
-
-
๊ฑด๊ฐ ์ํ ๋ถํฌ (Project Distribution)
-
-
-
-
๊ด๋ฆฌ ์ฌ๊ฐ์ง๋ ์ง๋จ (Vitality Scatter Plot)
-
-
-
+
+
+
+
๊ฑด๊ฐ ์ํ ๋ถํฌ (Project Distribution)
+
+
+
+
ํ๋ก์ ํธ SWOT ๋งคํธ๋ฆญ์ค (Strategic Analysis)
+
+
+
-
-
-
+
+
+
+
+
+
70%โ ์ ์
+
30~70% ์ฃผ์
+
10~30% ์ํ
+
10%โ ์ฌ๋ง
+
+
+
+
-
-