diff --git a/.gitignore b/.gitignore index c8a273e..b86406d 100644 Binary files a/.gitignore and b/.gitignore differ diff --git a/ANALYSIS_REPORT.md b/ANALYSIS_REPORT.md index ddb9001..51b9c74 100644 --- a/ANALYSIS_REPORT.md +++ b/ANALYSIS_REPORT.md @@ -1,49 +1,76 @@ -# ๐Ÿ“Š Project Master Sabermetrics ๋ถ„์„ ์—”์ง„ ๋ฆฌํฌํŠธ +# ๐Ÿ“Š ์‹œ์Šคํ…œ ์šด์˜ ์ž์‚ฐ ๊ฐ€์น˜ ๋ถ„์„ ๋ณด๊ณ ์„œ (Sabermetrics Edition) -## 1. ๊ฐœ์š” (Vision) -๋ณธ ์‹œ์Šคํ…œ์€ ๋ฐฉ๋Œ€ํ•œ ํ”„๋กœ์ ํŠธ ์šด์˜ ๋ฐ์ดํ„ฐ(ํŒŒ์ผ ์ˆ˜, ํ™œ๋™ ๋กœ๊ทธ, ์กฐ์ง ์ •๋ณด)๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ **AI ๊ธฐ๋ฐ˜ ํ”„๋กœ์ ํŠธ ๊ฑด๊ฐ•๋„(P-SOI)**๋ฅผ ์‚ฐ์ถœํ•ฉ๋‹ˆ๋‹ค. ๋‹จ์ˆœํžˆ "์‚ด์•„์žˆ๋Š”๊ฐ€"๋ฅผ ๋„˜์–ด, "์‹ค๋ฌด์ ์œผ๋กœ ๊ฐ€์น˜ ์žˆ๊ฒŒ ๊ด€๋ฆฌ๋˜๊ณ  ์žˆ๋Š”๊ฐ€"๋ฅผ ์ •๋ฐ€ ์ง„๋‹จํ•˜๋Š” ๊ฒƒ์ด ๋ชฉ์ ์ž…๋‹ˆ๋‹ค. +๋ณธ ๋ณด๊ณ ์„œ๋Š” ์•ผ๊ตฌ์˜ ํ†ต๊ณ„ ๋ถ„์„ ๊ธฐ๋ฒ•์ธ **์„ธ์ด๋ฒ„๋ฉ”ํŠธ๋ฆญ์Šค(Sabermetrics)**๋ฅผ ํ”„๋กœ์ ํŠธ ๊ด€๋ฆฌ ์‹œ์Šคํ…œ์— ์ด์‹ํ•˜์—ฌ, ๋‹จ์ˆœ ํ™œ๋™๋Ÿ‰ ์ธก์ •์„ ๋„˜์–ด **'์‹ค์งˆ์  ์ž์‚ฐ ๊ฐ€์น˜'**์™€ **'๋ฏธ๋ž˜ ์šด์˜ ์œ„ํ—˜'**์„ ์ •๋ฐ€ ๋ถ„์„ํ•œ ๊ฒฐ๊ณผ์ž…๋‹ˆ๋‹ค. --- -## 2. P-SOI ์‚ฐ์ถœ ๋กœ์ง (The Formula) +## 1. ํ•ต์‹ฌ ๋ถ„์„ ์ง€ํ‘œ ์ •์˜ (Core Metrics) -### 2.1 ๊ธฐ์ดˆ ๋ชจ๋ธ: ์ง€์ˆ˜ ๊ฐ์‡„ (Exponential Decay) -ํ”„๋กœ์ ํŠธ ์ •๋ณด์˜ ๊ฐ€์น˜๋Š” ๊ด€๋ฆฌ ํ™œ๋™์ด ๋ฉˆ์ถ˜ ์‹œ์ ๋ถ€ํ„ฐ ์‹œ๊ฐ„์ด ํ๋ฅผ์ˆ˜๋ก ๊ธ‰๊ฒฉํžˆ ํ•˜๋ฝํ•ฉ๋‹ˆ๋‹ค. -- **์ˆ˜์‹**: $SOI = 100 \times e^{-\lambda t}$ -- **์˜๋ฏธ**: 14์ผ ๋ฐฉ์น˜ ์‹œ ๊ฐ€์น˜๊ฐ€ ์•ฝ 50% ์†Œ์‹ค๋˜๋Š” ํ˜„์žฅ ํ˜„์‹ค์„ ๋ฐ˜์˜ํ•ฉ๋‹ˆ๋‹ค. +### 1.1 ์šด์˜ ํ™œ๋ ฅ ์ง€์ˆ˜ (AVI, Activity Vitality Index) +ํ”„๋กœ์ ํŠธ๊ฐ€ ํ˜„์žฌ ์–ผ๋งˆ๋‚˜ '์‚ด์•„์„œ ์ˆจ ์‰ฌ๊ณ  ์žˆ๋Š”๊ฐ€'๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ์ƒ์กด ์ง€์ˆ˜์ž…๋‹ˆ๋‹ค. -### 2.2 ๊ณ ๋„ํ™” 1: AAS (AI-Hazard Adaptive SOI) -ํ”„๋กœ์ ํŠธ์˜ ์ค‘์š”๋„์™€ ์ฃผ๋ณ€ ํ™˜๊ฒฝ์— ๋”ฐ๋ผ ํ•˜๋ฝ ๊ณก์„ ์˜ ๊ธฐ์šธ๊ธฐ๋ฅผ ๋™์ ์œผ๋กœ ์กฐ์ •ํ•ฉ๋‹ˆ๋‹ค. -- **์ž์‚ฐ ๊ทœ๋ชจ ์˜ํ–ฅ**: ํŒŒ์ผ ์ˆ˜๊ฐ€ ๋งŽ์„์ˆ˜๋ก ๊ด€๋ฆฌ ๋ถ€์žฌ ๋ฆฌ์Šคํฌ๊ฐ€ ํฌ๋ฏ€๋กœ AI๊ฐ€ ํ•˜๋ฝ ์†๋„๋ฅผ ๊ฐ€์†์‹œํ‚ต๋‹ˆ๋‹ค. -- **์กฐ์ง ์œ„ํ—˜ ์ „์—ผ**: ์†Œ์† ๋ถ€์„œ๋‚˜ ๋‹ด๋‹น์ž์˜ ์ „์ฒด SOI๊ฐ€ ๋‚ฎ์„ ๊ฒฝ์šฐ, ์‹œ์Šคํ…œ์  ๋ถ•๊ดด ๋ฆฌ์Šคํฌ ๊ฐ€์ค‘์น˜๋ฅผ ๋ถ€์—ฌํ•ฉ๋‹ˆ๋‹ค. +* **์‚ฐ์ถœ ๊ณต์‹**: $AVI = exp(-\lambda \times days) \times Quality \times 100$ +* **ํ•ต์‹ฌ ๋ฐ์ดํ„ฐ**: + * **์ •์ฒด ์ผ์ˆ˜(days)**: ๋งˆ์ง€๋ง‰ ์œ ์˜๋ฏธํ•œ ํŒŒ์ผ ์—…๋ฐ์ดํŠธ ์ดํ›„ ๊ฒฝ๊ณผ ์‹œ๊ฐ„. + * **๊ฐ์‡„ ๊ณ„์ˆ˜($\lambda$)**: ์ž์‚ฐ ๊ทœ๋ชจ(ํŒŒ์ผ ์ˆ˜)๊ฐ€ ํด์ˆ˜๋ก, ์†Œ์† ๋ถ€์„œ์˜ ๋ฐฉ์น˜์œจ์ด ๋†’์„์ˆ˜๋ก ์ปค์ง€๋ฉฐ ์ ์ˆ˜๋ฅผ ๋” ๋น ๋ฅด๊ฒŒ ํ•˜๋ฝ์‹œํ‚ต๋‹ˆ๋‹ค. + * **ํ™œ๋™ ํ’ˆ์งˆ(Quality)**: ๋‹จ์ˆœ ์‹œ์Šคํ…œ ๋กœ๊ทธ(๋‹จ์ˆœ ์ ‘์†, ์„ค์ • ๋ณ€๊ฒฝ)๋Š” ๋‚ฎ๊ฒŒ ํ‰๊ฐ€ํ•˜๊ณ , ์‹ค์งˆ์ ์ธ ํŒŒ์ผ ์ฆ๋ถ„ ํ™œ๋™์— ๊ฐ€์ ์„ ๋ถ€์—ฌํ•ฉ๋‹ˆ๋‹ค. +* **์˜๋ฏธ**: 100%์— ๊ฐ€๊นŒ์šธ์ˆ˜๋ก ์‹ค์‹œ๊ฐ„ ๊ฐ€๋™ ์ƒํƒœ์ด๋ฉฐ, 0%์— ๊ฐ€๊นŒ์šธ์ˆ˜๋ก ๋ฐ์ดํ„ฐ ๋…ธํ›„ํ™”๊ฐ€ ์™„๋ฃŒ๋œ '์‚ฌ๋ง' ์ƒํƒœ๋ฅผ ๋œปํ•ฉ๋‹ˆ๋‹ค. -### 2.3 ๊ณ ๋„ํ™” 2: ECV (Existence-Conditioned Vitality) -'๋นˆ ๊ป๋ฐ๊ธฐ' ํ™œ๋™์„ ๊ฑธ๋Ÿฌ๋‚ด๋Š” ์กด์žฌ๋ก ์  ํŒจ๋„ํ‹ฐ์ž…๋‹ˆ๋‹ค. -- **์œ ๋ น ํ”„๋กœ์ ํŠธ**: ํŒŒ์ผ ์ˆ˜๊ฐ€ 0๊ฐœ์ธ ๊ฒฝ์šฐ, ์ตœ๊ทผ ๋กœ๊ทธ์™€ ๊ด€๊ณ„์—†์ด SOI ์ ์ˆ˜๋ฅผ **5% ๋ฏธ๋งŒ**์œผ๋กœ ๊ฐ•์ œ ๊ณ ์ •ํ•ฉ๋‹ˆ๋‹ค. -- **์‹ ๋ขฐ ๋ณด์ •**: ํŒŒ์ผ 10๊ฐœ ๋ฏธ๋งŒ์˜ ์†Œ๊ทœ๋ชจ ํ”„๋กœ์ ํŠธ๋Š” ํ™œ๋™ ์‹ ๋ขฐ๋„๋ฅผ 40% ์ˆ˜์ค€์œผ๋กœ ์ œํ•œํ•ฉ๋‹ˆ๋‹ค. +### 1.2 ์ž์‚ฐ ๊ฐ€์น˜ ๊ธฐ์—ฌ๋„ (VCI, Value Contribution Index) +์‹œ์Šคํ…œ ์ „์ฒด์˜ ์šด์˜ ํ‘œ์ค€ ๋Œ€๋น„, ํ•ด๋‹น ํ”„๋กœ์ ํŠธ๊ฐ€ ๊ธฐ์—ฌํ•˜๊ณ  ์žˆ๋Š” ๊ฐ€์น˜์˜ ์ƒ๋Œ€์  ํ•˜์ค‘์„ ์ธก์ •ํ•ฉ๋‹ˆ๋‹ค. -### 2.4 ๊ณ ๋„ํ™” 3: ๋กœ๊ทธ ํ’ˆ์งˆ ๋ฐ ์‹ค๋ฌด ํˆฌ์ž… ๋ถ„์„ -- **Log Quality**: ๋กœ๊ทธ ํ…์ŠคํŠธ๋ฅผ ๋ถ„์„ํ•˜์—ฌ [์‹ค๋ฌด ํ™œ๋™(1.0), ๊ด€๋ฆฌ ํ™œ๋™(0.7), ํ–‰์ • ํ™œ๋™(0.4)] ๊ฐ€์ค‘์น˜๋ฅผ ๋ถ€์—ฌํ•ฉ๋‹ˆ๋‹ค. -- **Work Effort**: ์ตœ๊ทผ 30๊ฐœ ํžˆ์Šคํ† ๋ฆฌ ์ค‘ ์‹ค์ œ **ํŒŒ์ผ ์ฆ๊ฐ**์ด ๋ฐœ์ƒํ•œ ๋‚ ์˜ ๋น„์œจ์„ ๊ณ„์‚ฐํ•˜์—ฌ ์‹ค์งˆ ๊ณต์ˆ˜ ํˆฌ์ž…๋ฅ ์„ ์‚ฐ์ถœํ•ฉ๋‹ˆ๋‹ค. +* **์‚ฐ์ถœ ๊ณต์‹**: $VCI = (AVI - 70.0) \times (\frac{Files}{200} + 0.5)$ +* **ํ•ต์‹ฌ ๋กœ์ง**: + * **๊ฑด๊ฐ• ๊ธฐ์ค€์„ (70.0%)**: ์‹œ์Šคํ…œ ์ž์‚ฐ ๊ฐ€์น˜๋ฅผ ์œ ์ง€ํ•˜๊ธฐ ์œ„ํ•œ ์ตœ์†Œ ๋งˆ์ง€๋…ธ์„ (Replacement Level)์ž…๋‹ˆ๋‹ค. + * **๊ทœ๋ชจ ๊ฐ€์ค‘์น˜**: ํŒŒ์ผ ์ˆ˜๊ฐ€ ๋งŽ์€ ๋Œ€ํ˜• ํ”„๋กœ์ ํŠธ์ผ์ˆ˜๋ก ๋™์ผํ•œ ๋ฐฉ์น˜ ์ƒํ™ฉ์—์„œ ์‹œ์Šคํ…œ์— ์ฃผ๋Š” ์ถฉ๊ฒฉ(์Œ์ˆ˜๊ฐ’)์ด ๊ธฐํ•˜๊ธ‰์ˆ˜์ ์œผ๋กœ ์ปค์ง‘๋‹ˆ๋‹ค. +* **์˜๋ฏธ**: ์–‘์ˆ˜(+)๋Š” ๊ฐ€์น˜ ์ฐฝ์ถœ, ์Œ์ˆ˜(-)๋Š” ์‹œ์Šคํ…œ ๊ธฐํšŒ๋น„์šฉ์„ ๊ฐ‰์•„๋จน๋Š” '๊ฐ€์น˜ ํŒŒ๊ดด' ์ƒํƒœ์ž„์„ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค. + +### 1.3 ์—…๋ฌด ์ง‘์ค‘๋„ (Job Focus) +๋‹จ์ˆœ ๊ด€๋ฆฌ ํ–‰์œ„๋ฅผ ์ œ์™ธํ•˜๊ณ , ์‹ค์ œ ์„ฑ๊ณผ๋ฌผ(ํŒŒ์ผ)์„ ์ƒ์‚ฐํ•˜๋Š” ๋ฐ ์–ผ๋งˆ๋‚˜ ๋ชฐ์ž…ํ–ˆ๋Š”์ง€๋ฅผ ํŒ๋ณ„ํ•ฉ๋‹ˆ๋‹ค. + +* **์‚ฐ์ถœ ๊ณต์‹**: $Job Focus = \frac{\text{์ตœ๊ทผ 30ํšŒ ์ค‘ ์‹ค์งˆ ํŒŒ์ผ ๋ณ€๋™ ๋ฐœ์ƒ ํšŸ์ˆ˜}}{\text{์ „์ฒด ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ํšŸ์ˆ˜}} \times 100$ +* **์˜๋ฏธ**: ๋กœ๊ทธ๋งŒ ๋‚จ๊ธฐ๋Š” '๋ณด์—ฌ์ฃผ๊ธฐ์‹ ํ™œ๋™'์„ ํ•„ํ„ฐ๋งํ•˜์—ฌ ์šด์˜์˜ ์ง„์ •์„ฑ์„ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. --- -## 3. ์ „๋žต์  ๋ถ„์„ ๋„๊ตฌ (Visualization) +## 2. ๋“ฑ๊ธ‰ ์ฒด๊ณ„ ๋ฐ ๊ด€๋ฆฌ ๊ฐ€์ด๋“œ (Grade System) -### 3.1 ํ”„๋กœ์ ํŠธ SWOT ๋งคํŠธ๋ฆญ์Šค -X์ถ•(์ž์‚ฐ ๊ทœ๋ชจ)๊ณผ Y์ถ•(ํ™œ๋™์„ฑ)์„ ๊ฒฐํ•ฉํ•˜์—ฌ 4๊ฐ€์ง€ ๊ตญ๋ฉด์œผ๋กœ ํ”„๋กœ์ ํŠธ๋ฅผ ์ง„๋‹จํ•ฉ๋‹ˆ๋‹ค. -1. **ํ•ต์‹ฌ ์šฐ๋Ÿ‰ (Strategic)**: ๋Œ€๊ทœ๋ชจ ํ•ต์‹ฌ ์ž์‚ฐ์ด๋ฉฐ ํ™œ๋ฐœํžˆ ๊ด€๋ฆฌ ์ค‘. -2. **ํ™œ๋™ ์–‘ํ˜ธ (Agile)**: ๊ทœ๋ชจ๋Š” ์ž‘์œผ๋‚˜ ๋งค์šฐ ํƒ„๋ ฅ์ ์œผ๋กœ ์—…๋ฐ์ดํŠธ ์ค‘. -3. **๋ฐฉ์น˜/์†Œ๊ทœ๋ชจ**: ์ค‘์š”๋„๊ฐ€ ๋‚ฎ๊ณ  ๋ฐฉ์น˜๋œ ์ƒํƒœ. -4. **๊ด€๋ฆฌ ์‚ฌ๊ฐ์ง€๋Œ€ (Critical Risk)**: **์ž์‚ฐ ๊ทœ๋ชจ๋Š” ํฌ๋‚˜ ์žฅ๊ธฐ ๋ฐฉ์น˜๋จ (์ตœ์šฐ์„  ๊ด€๋ฆฌ ๋Œ€์ƒ)** +### 2.1 VCI ๋“ฑ๊ธ‰ (ํ”„๋กœ์ ํŠธ ์œ„์ƒ) +| ๋“ฑ๊ธ‰ (Grade) | ์ ์ˆ˜ ๊ธฐ์ค€ | ์šด์˜ ์˜๋ฏธ ๋ฐ ๊ด€๋ฆฌ ์ „๋žต | +| :--- | :--- | :--- | +| **Masterpiece** | +10.0 ์ด์ƒ | **ํ•ต์‹ฌ ์ž์‚ฐ (MVP)**: ์‹œ์Šคํ…œ ๊ฐ€์น˜๋ฅผ ๊ฒฌ์ธํ•˜๋Š” ์ตœ์šฐ๋Ÿ‰ ํ”„๋กœ์ ํŠธ | +| **Blue Chip** | +2.0 ~ +10.0 | **์šฐ๋Ÿ‰ ์ž์‚ฐ (์ฃผ์ „)**: ๊พธ์ค€ํ•œ ํ™œ๋ ฅ์œผ๋กœ ๊ฐ€์น˜๋ฅผ ์ฐฝ์ถœํ•˜๋Š” ํ•ต์‹ฌ๊ตฐ | +| **Steady** | -2.0 ~ +2.0 | **ํ˜„์ƒ ์œ ์ง€ (๋ณด๊ฒฐ)**: ํ‘œ์ค€ ์ˆ˜์ค€์˜ ์šด์˜์„ ์œ ์ง€ ์ค‘์ธ ์•ˆ์ •๊ตฐ | +| **Underperform** | -10.0 ~ -2.0 | **์ €์„ฑ๊ณผ (๋งˆ์ด๋„ˆ)**: ๊ทœ๋ชจ ๋Œ€๋น„ ํ™œ๋ ฅ์ด ๋ถ€์กฑํ•˜์—ฌ ๋ฆฌ์†Œ์Šค ํˆฌ์ž… ํ•„์š” | +| **Liability** | -10.0 ์ดํ•˜ | **๊ฐ€์น˜ ํŒŒ๊ดด (๋ฐฉ์ถœ)**: ์‹œ์Šคํ…œ ๊ฐ€์น˜๋ฅผ ํ›ผ์† ์ค‘์ธ ์ข€๋น„ ํ”„๋กœ์ ํŠธ. ํƒ€์ ˆ ๊ฒ€ํ†  ์‹œ๊ธ‰ | -### 3.2 AI ์ง„๋‹จ ์•„์ฝ”๋””์–ธ ๋ฆฌํฌํŠธ -์‚ฌ์šฉ์ž๊ฐ€ ์ง€์ˆ˜ ์‚ฐ์ถœ ๊ฒฐ๊ณผ์— ๋‚ฉ๋“ํ•  ์ˆ˜ ์žˆ๋„๋ก, ๊ฐœ๋ณ„ ํ”„๋กœ์ ํŠธ ํ–‰ ํด๋ฆญ ์‹œ **4๋‹จ๊ณ„ AI ์ถ”๋ก  ๊ณผ์ •**์„ ์‹ค์‹œ๊ฐ„ ๋ฆฌํฌํŠธ๋กœ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. +### 2.2 ์ƒํƒœ ์˜ˆ๋ณด (AI Forecast) +์ตœ๊ทผ ํ™œ๋™์˜ **๊ฐ€์†๋„(Acceleration)**์™€ **๊ด€์„ฑ(Momentum)**์„ AI๊ฐ€ ๋ถ„์„ํ•œ 14์ผ ๋’ค ์ „๋ง์ž…๋‹ˆ๋‹ค. + +* **์„ฑ์žฅ ๊ฐ€์† (Bullish)**: ํ™œ๋™ ์—๋„ˆ์ง€๊ฐ€ ์ฆ๊ฐ€ ์ถ”์„ธ์ด๋ฉฐ ๊ฐ€์น˜๊ฐ€ ์˜ค๋ฅผ ์ „๋ง. +* **์•ˆ์ • ์œ ์ง€ (Neutral)**: ํ˜„์žฌ์˜ ์•ˆ์ •์ ์ธ ์šด์˜ ๋ฆฌ๋“ฌ์„ ์ง€์†ํ•  ์ „๋ง. +* **ํ™œ๋ ฅ ์ €ํ•˜ (Bearish)**: ์ •์ฒด ์ง•ํ›„ ํฌ์ฐฉ. ๋‹จ๊ธฐ ๋‚ด ๊ฐ€๋™๋ฅ  ํ•˜๋ฝ ์˜ˆ์ƒ. +* **์ค‘๋‹จ ์œ„๊ธฐ (Warning)**: ๊ธ‰๊ฒฉํ•œ ํ™œ๋™ ์ €ํ•˜๋กœ ์ธํ•œ ์ž์‚ฐ ์†Œ๋ฉธ ์œ„ํ—˜ ๋…ธ์ถœ. --- -## 4. ํ–ฅํ›„ ๋”ฅ๋Ÿฌ๋‹ ๋กœ๋“œ๋งต (Evolution) -๋ฐ์ดํ„ฐ๊ฐ€ ๋ˆ„์ ๋จ์— ๋”ฐ๋ผ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ž๊ฐ€ ํ•™์Šตํ˜• ์—”์ง„์œผ๋กœ ์ง„ํ™”ํ•ฉ๋‹ˆ๋‹ค. -- **LSTM ๊ธฐ๋ฐ˜ ๋ฆฌ๋“ฌ ํ•™์Šต**: ๊ฐ ํ”„๋กœ์ ํŠธ์˜ ๊ณ ์œ ํ•œ ์—…๋ฐ์ดํŠธ ์ฃผ๊ธฐ์™€ ํŒจํ„ด(Life Rhythm)์„ ์ธ์ฝ”๋”ฉํ•˜์—ฌ ๋งž์ถคํ˜• ์˜ˆ๋ณด ์ˆ˜ํ–‰. -- **NLP ์ž„๋ฒ ๋”ฉ**: ๋‹จ์ˆœ ํ‚ค์›Œ๋“œ๋ฅผ ๋„˜์–ด ๋กœ๊ทธ ํ…์ŠคํŠธ์˜ ๋งฅ๋ฝ์  ์˜๋ฏธ๋ฅผ ๋”ฅ๋Ÿฌ๋‹์ด ์Šค์Šค๋กœ ํ•™์Šตํ•˜์—ฌ ๊ฐ€์ค‘์น˜ ์ž๋™ ์‚ฐ์ •. -- **๋ณ‘๋ชฉ ์˜ˆ์ธก AI**: ํŠน์ • ๋‹ด๋‹น์ž๋‚˜ ๋ถ€์„œ์˜ ์—…๋ฌด ๊ณผ๋ถ€ํ•˜ ํŒจํ„ด์„ ํ•™์Šตํ•˜์—ฌ ์ง‘๋‹จ ๋ฐฉ์น˜ ์œ„ํ—˜์„ ์„ ์ œ์ ์œผ๋กœ ์˜ˆ๋ณด. +## 3. ๋ฐ์ดํ„ฐ ๋ถ„์„ ํ”„๋กœ์„ธ์Šค (Analysis Process) + +1. **๋ฐ์ดํ„ฐ ์ˆ˜์ง‘**: `projects_history` ํ…Œ์ด๋ธ”๋กœ๋ถ€ํ„ฐ ์ผ๋ณ„ ํŒŒ์ผ ์ˆ˜ ๋ฐ ๋กœ๊ทธ ํ…์ŠคํŠธ๋ฅผ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค. +2. **ํ”ผ์ฒ˜ ์ถ”์ถœ**: + * **Velocity**: ํŒŒ์ผ ์ˆ˜์˜ ๋ณ€ํ™” ์†๋„ ๊ณ„์‚ฐ. + * **Acceleration**: ํ™œ๋™์˜ ๊ฐ€์†/๊ฐ์† ์—ฌ๋ถ€ ํŒ๋ณ„. + * **Stagnation**: ๋งˆ์ง€๋ง‰ ํ™œ๋™ ์ดํ›„์˜ ๊ณต๋ฐฑ ๊ธฐ๊ฐ„ ์ธก์ •. +3. **AI ์‹œ๋ฎฌ๋ ˆ์ด์…˜**: ์ถ”์ถœ๋œ ํ”ผ์ฒ˜๋ฅผ AAS(AI ์œ„ํ—˜ ์ ์‘ํ˜• ๋ชจ๋ธ)์— ์ž…๋ ฅํ•˜์—ฌ ๊ฐœ๋ณ„ ํ”„๋กœ์ ํŠธ๋งŒ์˜ **'์œ„ํ—˜ ๊ณก์„ '**์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. +4. **์ตœ์ข… ํŒ์ •**: AVI์™€ VCI๋ฅผ ๊ฒฐํ•ฉํ•˜์—ฌ ๋ฆฌ๋”๋ณด๋“œ์— ๋“ฑ๊ธ‰๊ณผ ๊ด€๋ฆฌ ๊ฐ€์ด๋“œ๋ผ์ธ์„ ์†ก์ถœํ•ฉ๋‹ˆ๋‹ค. + +--- + +## 4. ๊ด€๋ฆฌ์ž ์ œ์–ธ (Action Plan) + +* **VCI ์Œ์ˆ˜ ํ”„๋กœ์ ํŠธ ์ง‘์ค‘ ๊ด€๋ฆฌ**: ๋‹จ์ˆœ ํ™œ๋™๋Ÿ‰์ด ์•„๋‹Œ VCI๊ฐ€ ๋‚ฎ์€ ๋Œ€ํ˜• ํ”„๋กœ์ ํŠธ๋ถ€ํ„ฐ ์šฐ์„ ์ ์œผ๋กœ ์ธ๋ ฅ์„ ๋ฐฐ์น˜ํ•˜๊ฑฐ๋‚˜ ์šด์˜ ์ •์ฑ…์„ ์žฌ์ ๊ฒ€ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. +* **AI Forecast ํ™œ์šฉ**: 'ํ™œ๋ ฅ ์ €ํ•˜' ์˜ˆ๋ณด๊ฐ€ ๋œฌ ํ”„๋กœ์ ํŠธ๋Š” ์‹ค์ œ AVI๊ฐ€ ๊ธ‰๋ฝํ•˜๊ธฐ ์ „ ์„ ์ œ์ ์ธ ์กฐ์น˜(์—…๋ฌด ๋…๋ ค, ํŒŒ์ผ ํ˜„ํ–‰ํ™”)๋ฅผ ์ทจํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. +* **ํŒŒ์ผ ์ˆ˜์™€ ํ™œ๋ ฅ์˜ ๊ท ํ˜•**: ํŒŒ์ผ ์ˆ˜๊ฐ€ ๋งŽ์€๋ฐ ํ™œ๋ ฅ(AVI)์ด ๋‚ฎ์€ ๊ฒฝ์šฐ, ์‹œ์Šคํ…œ ์ „์ฒด์˜ ๋ฐ์ดํ„ฐ ๋ฌด๊ฒฐ์„ฑ์„ ํ•ด์น  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ๋ฐ์ดํ„ฐ ํด๋ Œ์ง•์ด๋‚˜ ์•„์นด์ด๋น™์„ ๊ถŒ๊ณ ํ•ฉ๋‹ˆ๋‹ค. + +--- +*๋ณธ ๋ถ„์„ ์—”์ง„์€ Project Master Sabermetrics ์•Œ๊ณ ๋ฆฌ์ฆ˜์— ์˜ํ•ด ์ž๋™ ์ƒ์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.* diff --git a/__pycache__/analysis_service.cpython-312.pyc b/__pycache__/analysis_service.cpython-312.pyc index 1a1a5ba..df75f8a 100644 Binary files a/__pycache__/analysis_service.cpython-312.pyc and b/__pycache__/analysis_service.cpython-312.pyc differ diff --git a/__pycache__/prediction_service.cpython-312.pyc b/__pycache__/prediction_service.cpython-312.pyc index b100aa7..d80a302 100644 Binary files a/__pycache__/prediction_service.cpython-312.pyc and b/__pycache__/prediction_service.cpython-312.pyc differ diff --git a/__pycache__/sql_queries.cpython-312.pyc b/__pycache__/sql_queries.cpython-312.pyc index 75c1741..b7d27ec 100644 Binary files a/__pycache__/sql_queries.cpython-312.pyc and b/__pycache__/sql_queries.cpython-312.pyc differ diff --git a/analysis_service.py b/analysis_service.py index 4c57d26..30ba1ef 100644 --- a/analysis_service.py +++ b/analysis_service.py @@ -170,11 +170,17 @@ class AnalysisService: total_soi += soi_score + # [์ตœ์ข… ์„ธ์ด๋ฒ„๋ฉ”ํŠธ๋ฆญ์Šค ๋ณด์ •: P-WAR+ (Adjusted Score)] + # ์ ˆ๋Œ€ ๊ธฐ์ค€์„ (Replacement Level): 70.0% (์ด ์ดํ•˜๋Š” ์ž์‚ฐ ๊ฐ€์น˜ ํŒŒ๊ดด๋กœ ๊ฐ„์ฃผ) + REPLACEMENT_LEVEL = 70.0 + asset_weight = (file_count / 200.0) + 0.5 # ํŒŒ์ผ 100๊ฐœ๋‹น 0.5๋ฐฐ ๊ฐ€์ค‘ (์ตœ์†Œ 0.5๋ฐฐ) + p_war_score = (soi_score - REPLACEMENT_LEVEL) * asset_weight + results.append({ "project_nm": p['short_nm'] or p['project_nm'], "file_count": file_count, "days_stagnant": days_stagnant, - "risk_count": 0, + "risk_count": round(p_war_score, 2), # P-WAR+ ์ ˆ๋Œ€ ๊ธฐ์—ฌ๋„ ์ ์ˆ˜ (ํ‰๊ท ์˜ ํ•จ์ • ๊ทน๋ณต์šฉ) "p_war": round(soi_score, 1), "predicted_soi": predicted_soi, "is_auto_delete": is_auto_delete, diff --git a/js/analysis.js b/js/analysis.js index f076781..f5ad012 100644 --- a/js/analysis.js +++ b/js/analysis.js @@ -1,6 +1,6 @@ /** * Project Master Analysis JS - * P-WAR (Project Performance Above Replacement) ๋ถ„์„ ์—”์ง„ + * AVI (Activity Vitality Index) & VCI (Value Contribution Index) ๋ถ„์„ ์—”์ง„ */ // Chart.js ํ”Œ๋Ÿฌ๊ทธ์ธ ์ „์—ญ ๋“ฑ๋ก @@ -9,40 +9,49 @@ if (typeof ChartDataLabels !== 'undefined') { } document.addEventListener('DOMContentLoaded', () => { - console.log("Analysis engine initialized..."); - loadPWarData(); + console.log("Business Analysis Engine initialized..."); + loadProjectAnalysisData(); }); -async function loadPWarData() { +async function loadProjectAnalysisData() { 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); + renderVitalityLeaderboard(data); + renderValueCharts(data); if (data.length > 0 && data[0].avg_info) { const avg = data[0].avg_info; const infoEl = document.getElementById('avg-system-info'); - if (infoEl) infoEl.textContent = `* ์‹œ์Šคํ…œ ์ข…ํ•ฉ ๊ฑด๊ฐ•๋„: ${avg.avg_risk}% (0.0%์— ๊ฐ€๊นŒ์šธ์ˆ˜๋ก ๋ฐฉ์น˜ ์‹ฌ๊ฐ)`; + if (infoEl) infoEl.textContent = `* ์‹œ์Šคํ…œ ์ข…ํ•ฉ ์ž์‚ฐ ๊ฑด์ „๋„: ${avg.avg_risk}% (์šด์˜ ํ‘œ์ค€ 70.0% ๋Œ€๋น„)`; } } catch (e) { console.error("๋ถ„์„ ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ ์‹คํŒจ:", e); } } -function getStatusInfo(soi, isAutoDelete) { - 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' }; +function getStatusInfo(avi, isAutoDelete) { + if (isAutoDelete || avi < 10) return { label: '์ค‘๋‹จ/๋ฐฉ์น˜', class: 'badge-system', key: 'dead' }; + if (avi < 30) return { label: '์œ„ํ—˜ ๋…ธ์ถœ', class: 'badge-danger', key: 'danger' }; + if (avi < 70) return { label: '๊ด€๋ฆฌ ์ฃผ์˜', class: 'badge-warning', key: 'warning' }; + return { label: '์ •์ƒ ์šด์˜', class: 'badge-active', key: 'active' }; } -function renderSOICharts(data) { +// VCI ๋“ฑ๊ธ‰ ํŒ์ • ๋กœ์ง (Sabermetrics WAR ๋“ฑ๊ธ‰ ๊ธฐ์ค€ ์‘์šฉ) +function getVciGrade(vci) { + if (vci >= 10) return { label: 'Masterpiece', class: 'grade-mvp', desc: '์‹œ์Šคํ…œ ๊ฐ€์น˜๋ฅผ ๊ฒฌ์ธํ•˜๋Š” ํ•ต์‹ฌ ์ž์‚ฐ (MVP๊ธ‰)' }; + if (vci >= 2) return { label: 'Blue Chip', class: 'grade-allstar', desc: '๊พธ์ค€ํ•œ ํ™œ๋ ฅ์˜ ์šฐ๋Ÿ‰ ์ž์‚ฐ (์ฃผ์ „๊ธ‰)' }; + if (vci >= -2) return { label: 'Steady', class: 'grade-starter', desc: 'ํ‘œ์ค€ ์ˆ˜์ค€์˜ ํ˜„์ƒ ์œ ์ง€ (๋ณด๊ฒฐ๊ธ‰)' }; + if (vci >= -10) return { label: 'Underperform', class: 'grade-bench', desc: '์šด์˜ ๋ฏธ๋น„๋กœ ์ธํ•œ ๊ฐ€์น˜ ํ•˜๋ฝ (๋งˆ์ด๋„ˆ๊ธ‰)' }; + return { label: 'Liability', class: 'grade-out', desc: '๊ฐ€์น˜๋ฅผ ํ›ผ์† ์ค‘์ธ ๋ฐฉ์น˜ ์ž์‚ฐ (๋ฐฉ์ถœ๊ธ‰)' }; +} + +function renderValueCharts(data) { if (!data || data.length === 0) return; - // 1. ์ƒํƒœ ๋ถ„ํฌ (Doughnut) + // 1. ์šด์˜ ํ™œ๋ ฅ ๋ถ„ํฌ (Doughnut) try { const stats = { active: [], warning: [], danger: [], dead: [] }; data.forEach(p => { @@ -56,7 +65,7 @@ function renderSOICharts(data) { window.myStatusChart = new Chart(statusCtx, { type: 'doughnut', data: { - labels: ['์ •์ƒ', '์ฃผ์˜', '์œ„ํ—˜', '์‚ฌ๋ง'], + labels: ['์ •์ƒ ์šด์˜', '๊ด€๋ฆฌ ์ฃผ์˜', '์œ„ํ—˜ ๋…ธ์ถœ', '์ค‘๋‹จ/๋ฐฉ์น˜'], datasets: [{ data: [stats.active.length, stats.warning.length, stats.danger.length, stats.dead.length], backgroundColor: ['#1E5149', '#22c55e', '#f59e0b', '#ef4444'], @@ -75,53 +84,40 @@ function renderSOICharts(data) { onClick: (e, elements) => { if (elements.length > 0) { const idx = elements[0].index; - openProjectListModal(['์ •์ƒ', '์ฃผ์˜', '์œ„ํ—˜', '์‚ฌ๋ง'][idx], stats[['active', 'warning', 'danger', 'dead'][idx]]); + openProjectListModal(['์ •์ƒ ์šด์˜', '๊ด€๋ฆฌ ์ฃผ์˜', '์œ„ํ—˜ ๋…ธ์ถœ', '์ค‘๋‹จ/๋ฐฉ์น˜'][idx], stats[['active', 'warning', 'danger', 'dead'][idx]]); } } } }); } catch (err) { console.error("๋„๋„› ์ฐจํŠธ ์—๋Ÿฌ:", err); } - // 2. ํ”„๋กœ์ ํŠธ SWOT ๋งคํŠธ๋ฆญ์Šค (Scatter) + // 2. ์ „๋žต์  ์ž์‚ฐ ๋งคํŠธ๋ฆญ์Šค (Scatter) try { - const scatterData = data.map(p => ({ - x: Math.min(500, p.file_count), - y: p.p_war, - label: p.project_nm - })); + const sortedByAVI = [...data].sort((a, b) => b.p_war - a.p_war); + const top5Ids = sortedByAVI.slice(0, 5).map(p => p.project_nm); + const bottom5Ids = sortedByAVI.slice(-5).map(p => p.project_nm); + const largeProjects = data.filter(p => p.file_count > 450).map(p => p.project_nm); + + const vipProjectNames = new Set([...top5Ids, ...bottom5Ids, ...largeProjects]); + + const scatterData = data.map(p => { + const vci = p.risk_count || 0; + const absVci = Math.abs(vci); + return { + x: Math.min(500, p.file_count), + y: p.p_war, + label: p.project_nm, + isVip: vipProjectNames.has(p.project_nm), + vci: vci, + radius: Math.max(5, Math.min(25, 5 + (absVci / 10))) + }; + }); const vitalityCtx = document.getElementById('forecastChart').getContext('2d'); if (window.myVitalityChart) window.myVitalityChart.destroy(); - // ํ”Œ๋Ÿฌ๊ทธ์ธ ํ†ตํ•ฉ (Duplicate Key ๋ฐฉ์ง€) - const chartPlugins = []; - if (typeof ChartDataLabels !== 'undefined') chartPlugins.push(ChartDataLabels); - - chartPlugins.push({ - id: 'quadrants', - beforeDraw: (chart) => { - const { ctx, chartArea: { left, top, right, bottom }, scales: { x, y } } = chart; - const midX = x.getPixelForValue(250); - const midY = y.getPixelForValue(50); - ctx.save(); - ctx.fillStyle = 'rgba(34, 197, 94, 0.03)'; ctx.fillRect(left, top, midX - left, midY - top); - ctx.fillStyle = 'rgba(30, 81, 73, 0.03)'; ctx.fillRect(midX, top, right - midX, midY - top); - ctx.fillStyle = 'rgba(148, 163, 184, 0.03)'; ctx.fillRect(left, midY, midX - left, bottom - midY); - ctx.fillStyle = 'rgba(239, 68, 68, 0.05)'; ctx.fillRect(midX, midY, right - midX, bottom - midY); - ctx.lineWidth = 2; ctx.strokeStyle = 'rgba(0,0,0,0.1)'; ctx.beginPath(); - ctx.moveTo(midX, top); ctx.lineTo(midX, bottom); ctx.moveTo(left, midY); ctx.lineTo(right, midY); ctx.stroke(); - ctx.font = 'bold 12px Pretendard'; ctx.textAlign = 'center'; ctx.fillStyle = 'rgba(0,0,0,0.2)'; - ctx.fillText('ํ™œ๋™ ์–‘ํ˜ธ', (left + midX) / 2, (top + midY) / 2); - ctx.fillText('ํ•ต์‹ฌ ์šฐ๋Ÿ‰', (midX + right) / 2, (top + midY) / 2); - ctx.fillText('๋ฐฉ์น˜/์†Œ๊ทœ๋ชจ', (left + midX) / 2, (midY + bottom) / 2); - ctx.fillStyle = 'rgba(239, 68, 68, 0.4)'; ctx.fillText('๊ด€๋ฆฌ ์‚ฌ๊ฐ์ง€๋Œ€', (midX + right) / 2, (midY + bottom) / 2); - ctx.restore(); - } - }); - window.myVitalityChart = new Chart(vitalityCtx, { type: 'scatter', - plugins: chartPlugins, data: { datasets: [{ data: scatterData, @@ -133,45 +129,73 @@ function renderSOICharts(data) { if (p.x < 250 && p.y < 50) return '#94a3b8'; return '#ef4444'; }, - pointRadius: 6, - hoverRadius: 10 + pointRadius: (ctx) => ctx.raw ? ctx.raw.radius : 5, + hoverRadius: (ctx) => (ctx.raw ? ctx.raw.radius : 5) + 3 }] }, options: { responsive: true, maintainAspectRatio: false, - layout: { padding: { top: 30, right: 40, left: 10, bottom: 10 } }, + layout: { padding: { top: 30, right: 45, left: 10, bottom: 10 } }, scales: { x: { type: 'linear', min: 0, max: 500, - title: { display: true, text: '์ž์‚ฐ ๊ทœ๋ชจ (ํŒŒ์ผ ์ˆ˜)', font: { size: 11, weight: '700' } }, + title: { display: true, text: 'ํŒŒ์ผ ์ˆ˜ (Files)', font: { size: 11, weight: '700' } }, grid: { display: false }, ticks: { stepSize: 125, callback: (v) => v >= 500 ? '500+' : v } }, y: { min: 0, max: 100, - title: { display: true, text: 'ํ™œ๋™์„ฑ (SOI %)', font: { size: 11, weight: '700' } }, + title: { display: true, text: '์šด์˜ ํ™œ๋ ฅ (AVI %)', font: { size: 11, weight: '700' } }, grid: { display: false } } }, plugins: { legend: { display: false }, datalabels: { - 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, + backgroundColor: 'rgba(255, 255, 255, 0.8)', + borderRadius: 4, padding: 4, + align: (ctx) => (ctx.raw && ctx.raw.y > 80 ? 'bottom' : 'top'), + offset: (ctx) => (ctx.raw ? ctx.raw.radius : 5) + 2, + font: { size: 10, weight: '800' }, + color: '#1e293b', + formatter: (v) => v ? v.label : '', + display: (ctx) => ctx.raw && ctx.raw.isVip, clip: false }, tooltip: { - callbacks: { label: (ctx) => ` [${ctx.raw.label}] SOI: ${ctx.raw.y.toFixed(1)}% | ํŒŒ์ผ: ${ctx.raw.x >= 500 ? '500+' : ctx.raw.x}๊ฐœ` } + callbacks: { + label: (ctx) => ` [${ctx.raw.label}] ํ™œ๋ ฅ(AVI): ${ctx.raw.y.toFixed(1)}% | ๊ฐ€์น˜ ๊ธฐ์—ฌ(VCI): ${ctx.raw.vci.toFixed(1)}` + } } } - } + }, + plugins: [{ + id: 'quadrants', + beforeDraw: (chart) => { + const { ctx, chartArea: { left, top, right, bottom }, scales: { x, y } } = chart; + const midX = x.getPixelForValue(250); + const midY = y.getPixelForValue(50); + ctx.save(); + ctx.fillStyle = 'rgba(34, 197, 94, 0.03)'; ctx.fillRect(left, top, midX - left, midY - top); + ctx.fillStyle = 'rgba(30, 81, 73, 0.03)'; ctx.fillRect(midX, top, right - midX, midY - top); + ctx.fillStyle = 'rgba(148, 163, 184, 0.03)'; ctx.fillRect(left, midY, midX - left, bottom - midY); + ctx.fillStyle = 'rgba(239, 68, 68, 0.05)'; ctx.fillRect(midX, midY, right - midX, bottom - midY); + ctx.lineWidth = 2; ctx.strokeStyle = 'rgba(0,0,0,0.1)'; ctx.beginPath(); + ctx.moveTo(midX, top); ctx.lineTo(midX, bottom); ctx.moveTo(left, midY); ctx.lineTo(right, midY); ctx.stroke(); + ctx.font = 'bold 12px Pretendard'; ctx.textAlign = 'center'; ctx.fillStyle = 'rgba(0,0,0,0.2)'; + ctx.fillText('ํ™œ๋ ฅ ์–‘ํ˜ธ', (left + midX) / 2, (top + midY) / 2); + ctx.fillText('ํ•ต์‹ฌ ๊ฐ€์น˜', (midX + right) / 2, (top + midY) / 2); + ctx.fillText('์ •์ฒด/์†Œ๊ทœ๋ชจ', (left + midX) / 2, (midY + bottom) / 2); + ctx.fillStyle = 'rgba(239, 68, 68, 0.4)'; ctx.fillText('์ž์‚ฐ ์†์‹ค ์œ„ํ—˜', (midX + right) / 2, (midY + bottom) / 2); + ctx.restore(); + } + }] }); - } catch (err) { console.error("SWOT ์ฐจํŠธ ์—๋Ÿฌ:", err); } + } catch (err) { console.error("์ „๋žต ๋งคํŠธ๋ฆญ์Šค ์—๋Ÿฌ:", err); } } -function renderPWarLeaderboard(data) { +function renderVitalityLeaderboard(data) { const container = document.getElementById('p-war-table-container'); if (!container) return; const sortedData = [...data].sort((a, b) => a.p_war - b.p_war); @@ -181,29 +205,34 @@ function renderPWarLeaderboard(data) { - + - + - - - + + + + ${sortedData.map((p, idx) => { const status = getStatusInfo(p.p_war, p.is_auto_delete); const rowId = `project-${idx}`; - const ecvText = p.file_count === 0 ? "5% (์œ ๋ น)" : p.file_count < 10 ? "40% (๊ป๋ฐ๊ธฐ)" : "100% (์‹ ๋ขฐ)"; - const ecvClass = (p.file_count < 10) ? "highlight-penalty" : "highlight-val"; + const vci = p.risk_count || 0; + const avi = p.p_war || 0; + const grade = getVciGrade(vci); return ` - - + + + - @@ -275,12 +312,19 @@ 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) { if (!detailRow.classList.contains('active')) { document.querySelectorAll('.detail-row').forEach(row => row.classList.remove('active')); detailRow.classList.add('active'); - setTimeout(() => { container.scrollTo({ top: mainRow.offsetTop - (container.querySelector('thead').offsetHeight || 40), behavior: 'smooth' }); }, 50); - } else detailRow.classList.remove('active'); + setTimeout(() => { + const headerH = container.querySelector('thead').offsetHeight || 45; + const targetScrollTop = mainRow.offsetTop - headerH; + container.scrollTo({ top: targetScrollTop, behavior: 'smooth' }); + }, 100); + } else { + detailRow.classList.remove('active'); + } } } @@ -288,14 +332,15 @@ function openProjectListModal(label, projects) { const modal = document.getElementById('analysisModal'); const title = document.getElementById('modalTitle'); const body = document.getElementById('modalBody'); - title.innerText = `[${label}] ํ”„๋กœ์ ํŠธ ๋ชฉ๋ก (${projects.length}๊ฑด)`; - body.innerHTML = projects.length === 0 ? '

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

' : ` + title.innerText = `[${label}] ํ”„๋กœ์ ํŠธ ๋ฆฌ์ŠคํŠธ (${projects.length}๊ฑด)`; + body.innerHTML = projects.length === 0 ? '

๋Œ€์ƒ ํ”„๋กœ์ ํŠธ ์—†์Œ

' : `
ํ”„๋กœ์ ํŠธ๋ช…ํ”„๋กœ์ ํŠธ๋ช… ํŒŒ์ผ ์ˆ˜๋ฐฉ์น˜์ผ์ •์ฒด ์ผ์ˆ˜ ์ƒํƒœ ํŒ์ •ํ˜„์žฌ SOI ์‹ค๋ฌด ํˆฌ์ž…AI ์˜ˆ๋ณด (14d) ๊ฐ€์น˜ ๊ธฐ์—ฌ (VCI) ํ™œ๋ ฅ ์ง€์ˆ˜ (AVI) ์—…๋ฌด ์ง‘์ค‘๋„ ์ƒํƒœ ์˜ˆ๋ณด (14d)
${p.project_nm} ${p.file_count.toLocaleString()}๊ฐœ ${p.days_stagnant}์ผ${status.label}${p.p_war.toFixed(1)}%${status.label === '์‚ฌ๋ง' ? '์ค‘๋‹จ' : status.label} + ${vci > 0 ? '+' : ''}${vci.toFixed(1)} + ${avi.toFixed(1)}%
${p.work_effort}% @@ -215,52 +244,60 @@ function renderPWarLeaderboard(data) {
${p.predicted_soi !== null ? p.predicted_soi.toFixed(1) + '%' : '-'}
+
-
โš™๏ธ AI ์œ„ํ—˜ ์ ์‘ํ˜• ๋ชจ๋ธ(AAS) ์‚ฐ์ถœ ์‹œ๋ฎฌ๋ ˆ์ด์…˜
-
-
- ๐Ÿ“Š ์‹ค์งˆ ์—…๋ฌด ํ™œ์„ฑํ™” ๋ถ„์„ (Work Vitality) - ํˆฌ์ž…๋ฅ  ${p.work_effort}% +
โš™๏ธ AI ์ž์‚ฐ ๊ฑด์ „์„ฑ ๋ถ„์„ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ (AAS Metrics)
+ +
+
+
+ ๐Ÿ“Š ์‹ค์งˆ ์—…๋ฌด ์ง‘์ค‘๋„ Analysis + ${p.work_effort}% +
+
+
์ตœ๊ทผ ์ˆ˜์ง‘ ๋กœ๊ทธ ์ค‘ ์‹ค์งˆ์  ์ž์‚ฐ ์ฆ๋ถ„์ด ํฌ์ฐฉ๋œ ๋ฐ€๋„์ž…๋‹ˆ๋‹ค.
+
+
+
+
VCI GRADE
+
${grade.label}
+
+
${grade.desc}
-
-
์ตœ๊ทผ 30ํšŒ ์ค‘ ์‹ค์ œ ํŒŒ์ผ ๋ณ€๋™์ด ํฌ์ฐฉ๋œ ๋‚ ์˜ ๋น„์œจ์ž…๋‹ˆ๋‹ค. ํ˜„์žฌ ${p.work_effort >= 70 ? '๋งค์šฐ ํ™œ๋ฐœ' : p.work_effort <= 30 ? '์ •์ฒด' : '๊ฐ„ํ—์ '} ์ƒํƒœ์ž…๋‹ˆ๋‹ค.
+
1
-
๋™์  ์œ„ํ—˜ ๊ณ„์ˆ˜(ฮป)
+
๋™์  ๊ฐ์‡„ ๊ณ„์ˆ˜(ฮป) ์‚ฐ์ถœ
+
์ž์‚ฐ ๊ทœ๋ชจ ๋ฐ ์กฐ์ง ์œ„ํ—˜์„ ํ•ฉ์‚ฐํ•˜์—ฌ ๊ฐœ๋ณ„ ํ™œ๋ ฅ ๊ณก์„ ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
ฮป = ${p.ai_lambda.toFixed(4)}
2
-
ํ™œ๋™ ํ’ˆ์งˆ (Quality)
-
Factor = ${(p.log_quality * 100).toFixed(0)}%
+
ํ™œ๋™ ์ง„์ •์„ฑ ๊ฒ€์ฆ
+
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)}%
+
๊ฐ€๋™ ๋ณด์กด์œจ (AVI)
+
Result = ${avi.toFixed(1)}%
4
-
์กด์žฌ ์ง„์ •์„ฑ (ECV)
-
Factor = ${ecvText}
+
๊ฐ€์น˜ ๊ธฐ์—ฌ ์˜ํ–ฅ๋ ฅ (VCI)
+
VCI = ${vci.toFixed(1)}
-
-
* ๊ณต์‹: AAS_Score ร— Quality_Factor ร— ECV_Factor
-
์ตœ์ข… P-SOI: ${p.p_war.toFixed(1)}%
-
- - ${projects.map(p => ``).join('')} + + ${projects.map(p => ``).join('')}
ํ”„๋กœ์ ํŠธ๋ช…๊ด€๋ฆฌ์ž๋ฐฉ์น˜์ผํ˜„์žฌ SOI
${p.project_nm}${p.master || '-'}${p.days_stagnant}์ผ${p.p_war.toFixed(1)}%
ํ”„๋กœ์ ํŠธ๋ช…๋ถ€์„œ๊ด€๋ฆฌ์ž์ •์ฒด์ผํ™œ๋ ฅ(AVI)
${p.project_nm}${p.dept || '-'}${p.master || '-'}${p.days_stagnant}์ผ${p.p_war.toFixed(1)}%
- `; + + `; modal.style.display = 'flex'; } @@ -303,12 +348,75 @@ 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(-ฮป ร— days) ร— 100

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

'; + + if (type === 'avi') { + title.innerText = '์šด์˜ ํ™œ๋ ฅ ์ง€์ˆ˜ (AVI) ๋“ฑ๊ธ‰ ๊ฐ€์ด๋“œ'; + body.innerHTML = ` +
AVI = exp(-ฮป ร— days) ร— Quality ร— 100
+

์ž์‚ฐ์˜ ๊ฐ€๋™ ์ƒํƒœ์™€ ์ƒ์กด์œจ์„ ๋‚˜ํƒ€๋‚ด๋Š” ์ง€ํ‘œ์ž…๋‹ˆ๋‹ค.

+ + + + + + + + + +
์ง€์ˆ˜ (AVI)๋“ฑ๊ธ‰์šด์˜ ์ƒํƒœ
90%โ†‘Live์‹ค์‹œ๊ฐ„ ์„ฑ๊ณผ๋ฌผ์ด ๋„์ถœ๋˜๋Š” ์ตœ์ƒ๊ธ‰ ๊ฐ€๋™
70~90%Stable์ฃผ๊ธฐ์  ์—…๋ฐ์ดํŠธ๊ฐ€ ์ด๋ค„์ง€๋Š” ํ‘œ์ค€ ์•ˆ์ •
30~70%Idle๊ด€๋ฆฌ๊ฐ€ ํ•„์š”ํ•œ ์œ ํœด/์ •์ฒด ์ƒํƒœ
10~30%Risk์ž์‚ฐ ๊ฐ€์น˜ ์†Œ๋ฉธ ์ง์ „์˜ ์œ„ํ—˜ ์ƒํƒœ
10%โ†“Frozen์šด์˜์ด ์ค‘๋‹จ๋œ ๋™๊ฒฐ/๋ฐฉ์น˜ ์ƒํƒœ
+ `; + } else if (type === 'vci') { + title.innerText = '์ž์‚ฐ ๊ฐ€์น˜ ๊ธฐ์—ฌ๋„ (VCI) ๋“ฑ๊ธ‰ ๊ฐ€์ด๋“œ'; + body.innerHTML = ` +

์šด์˜ ํ‘œ์ค€(AVI 70%) ๋Œ€๋น„ ์ž์‚ฐ ๊ฐ€์น˜ ๊ธฐ์—ฌ๋„์— ๋”ฐ๋ฅธ ํ”„๋กœ์ ํŠธ ์œ„์ƒ ๋ถ„๋ฅ˜์ž…๋‹ˆ๋‹ค.

+ + + + + + + + + +
์ ์ˆ˜ (VCI)๋“ฑ๊ธ‰์šด์˜ ์˜๋ฏธ
+10.0โ†‘Masterpiece์‹œ์Šคํ…œ ๊ฐ€์น˜๋ฅผ ๊ฒฌ์ธํ•˜๋Š” ํ•ต์‹ฌ ์ž์‚ฐ (MVP๊ธ‰)
+2.0 ~ +10.0Blue Chip๊พธ์ค€ํ•œ ํ™œ๋ ฅ์˜ ์šฐ๋Ÿ‰ ์ž์‚ฐ (์ฃผ์ „๊ธ‰)
-2.0 ~ +2.0Steadyํ‘œ์ค€ ์ˆ˜์ค€์˜ ํ˜„์ƒ ์œ ์ง€ (๋ณด๊ฒฐ๊ธ‰)
-10.0 ~ -2.0Underperform์šด์˜ ๋ฏธ๋น„๋กœ ์ธํ•œ ๊ฐ€์น˜ ํ•˜๋ฝ (๋งˆ์ด๋„ˆ๊ธ‰)
-10.0โ†“Liability๊ฐ€์น˜๋ฅผ ํ›ผ์† ์ค‘์ธ ๋ฐฉ์น˜ ์ž์‚ฐ (๋ฐฉ์ถœ๊ธ‰)
+ `; + } else if (type === 'focus') { + title.innerText = '์—…๋ฌด ์ง‘์ค‘๋„ (Job Focus) ๋“ฑ๊ธ‰ ๊ฐ€์ด๋“œ'; + body.innerHTML = ` +

๋‹จ์ˆœ ๊ด€๋ฆฌ ๋กœ๊ทธ๋ฅผ ์ œ์™ธํ•œ ์‹ค์งˆ์ ์ธ ์‚ฐ์ถœ๋ฌผ ๋ณ€ํ™”์˜ ๋ฐ€๋„์ž…๋‹ˆ๋‹ค.

+ + + + + + + + +
๋น„์œจ (%)๋“ฑ๊ธ‰ํ™œ๋™ ์„ฑ๊ฒฉ
80%โ†‘Intensive์„ฑ๊ณผ๋ฌผ ์œ„์ฃผ์˜ ๊ณ ๋ฐ€๋„ ์ง‘์ค‘ ์ž‘์—…
50~80%Active์„ฑ๊ณผ์™€ ๊ด€๋ฆฌ๊ฐ€ ๊ท ํ˜• ์žกํžŒ ์›ํ™œํ•œ ์‹คํ–‰
20~50%Maintenance์„ค์ •/ํ–‰์ • ๋“ฑ ๋‹จ์ˆœ ๊ด€๋ฆฌ ์ค‘์‹ฌ์˜ ์ž‘์—…
20%โ†“Surface์‹ค์ฒด์  ๋ณ€ํ™”๊ฐ€ ์ ์€ ํ˜•์‹์  ๋กœ๊ทธ ์ค‘์‹ฌ
+ `; } else { - title.innerText = 'AI ์‹œ๊ณ„์—ด ์˜ˆ์ธก ์ƒ์„ธ'; - body.innerHTML = '

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

'; + title.innerText = '์ƒํƒœ ์˜ˆ๋ณด (AI Forecast) ๋ถ„์„ ๊ฐ€์ด๋“œ'; + body.innerHTML = ` +
+ "2์ฃผ ๋’ค์˜ ํ”„๋กœ์ ํŠธ ๊ฑด๊ฐ• ์ƒํƒœ๋ฅผ ์˜ˆ์ธกํ•ฉ๋‹ˆ๋‹ค" +

๋‹จ์ˆœํ•œ ํ˜„์žฌ ์ ์ˆ˜ ๋‚˜์—ด์ด ์•„๋‹Œ, ์ตœ๊ทผ ํ™œ๋™์˜ ๊ฐ€์†๋„(Acceleration)์™€ ๋ณ€ํ™” ํŒจํ„ด์„ AI๊ฐ€ ๋ถ„์„ํ•˜์—ฌ ๋ฏธ๋ž˜์˜ ํ™œ๋ ฅ ์ง€์ˆ˜(AVI)๋ฅผ ์˜ˆ๋ณดํ•ฉ๋‹ˆ๋‹ค.

+
+ + + + + + + + + +
๋ถ„์„ ๊ฒฐ๊ณผ์ƒํƒœ ๋“ฑ๊ธ‰๊ด€๋ฆฌ ๊ฐ€์ด๋“œ๋ผ์ธ
AVI ์ƒ์Šนโ†‘์„ฑ์žฅ ๊ฐ€์†ํ™œ๋™ ๋ชจ๋ฉ˜ํ…€์ด ์ƒ์Šน ์ค‘์ธ ์šฐ์ˆ˜ ์ž์‚ฐ
AVI ์œ ์ง€์•ˆ์ • ์œ ์ง€ํ˜„์žฌ์˜ ๋ฆฌ๋“ฌ์„ ์œ ์ง€ํ•˜๋Š” ํ‘œ์ค€ ์šด์˜ ์ƒํƒœ
AVI ํ•˜๋ฝโ†“ํ™œ๋ ฅ ์ €ํ•˜์ •์ฒด ์ง•ํ›„ ํฌ์ฐฉ, ๊ด€๋ฆฌ ๋ฆฌ์†Œ์Šค ํˆฌ์ž… ๊ฒ€ํ† 
AVI 10%โ†“์ค‘๋‹จ ์œ„๊ธฐ๋‹จ๊ธฐ ๋‚ด ์™„์ „ ๋ฐฉ์น˜ ๋ฐ ๊ฐ€์น˜ ์†Œ๋ฉธ ์œ„ํ—˜
+ +
+ โ€ป ๋ถ„์„ ์•Œ๊ณ ๋ฆฌ์ฆ˜ ์•ˆ๋‚ด:
+ ํŒŒ์ผ ์ˆ˜์˜ ์‹ค์งˆ์  ์ฆ๊ฐ€๊ฐ€ ์—†๋Š” ํ”„๋กœ์ ํŠธ๋Š” '์„ฑ์žฅ ๊ฐ€์†' ์˜ˆ๋ณด๋ฅผ ๋ฐ›์„ ์ˆ˜ ์—†๋„๋ก ์„ค๊ณ„๋˜์–ด ์žˆ์œผ๋ฉฐ, ์ •์ฒด๊ฐ€ ๊ธธ์–ด์งˆ์ˆ˜๋ก ๊ฐ์‡„ ๊ฐ€์ค‘์น˜๊ฐ€ ์ž๋™์œผ๋กœ ๊ฐ•ํ™”๋ฉ๋‹ˆ๋‹ค. +
+ `; } modal.style.display = 'flex'; } diff --git a/prediction_service.py b/prediction_service.py index d97e419..7212d0b 100644 --- a/prediction_service.py +++ b/prediction_service.py @@ -53,19 +53,45 @@ class SOIPredictionService: @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) + # ๋ฐ์ดํ„ฐ๊ฐ€ ๋„ˆ๋ฌด ์ ์œผ๋ฉด ๋ฌด์กฐ๊ฑด ๋ณด์ˆ˜์  ๊ฐ์‡„ (14์ผ ๊ธฐ์ค€ ์•ฝ -2.1์ ) + if not history or len(history) < 3: + return round(max(0, min(100, current_soi - (0.15 * days_ahead))), 1) features = SOIPredictionService.extract_vitality_features(history) - - # ๊ธฐ์ค€์ ์„ ํ˜„์žฌ์˜ ์‹ค์ œ SOI ์ ์ˆ˜๋กœ ์„ค์ • (ํ•ต์‹ฌ ์ˆ˜์ •) current_val = float(current_soi) - # ํ™œ๋™ ๋ชจ๋ฉ˜ํ…€ ๊ณ„์‚ฐ: ํŒŒ์ผ ์ฆ๊ฐ€ ์†๋„์™€ ๋กœ๊ทธ ๋ฐ€๋„ ๋ฐ˜์˜ - momentum_factor = (features['velocity'] * 0.2) + (features['density'] * 2.0) + # [์ •๋ฐ€ ์ •์ฒด ๋ถ„์„] + # 1. ํŒŒ์ผ ์ˆ˜ ๋ณ€ํ™” ํ™•์ธ (์ตœ๊ทผ 5๊ฐœ ์ƒ˜ํ”Œ) + recent_counts = [int(h['file_count'] or 0) for h in history[-5:]] + is_hard_stagnant = len(set(recent_counts)) <= 1 # ํŒŒ์ผ ์ˆ˜ ๋ณ€๋™์ด ์ „ํ˜€ ์—†์Œ - # ์˜ˆ์ธก ๋กœ์ง: ํ˜„์žฌ๊ฐ’ + ๋ชจ๋ฉ˜ํ…€ - ์ž์—ฐ ๊ฐ์‡„ - decay_constant = 0.05 + # 2. ์ตœ๊ทผ ๋กœ๊ทธ ์ƒํƒœ ํ™•์ธ + last_log = history[-1]['recent_log'] + is_no_activity = last_log is None or last_log == "๋ฐ์ดํ„ฐ ์—†์Œ" or "ํด๋”์ž๋™์‚ญ์ œ" in last_log + + # [๋ชจ๋ฉ˜ํ…€ ์‚ฐ์ถœ ๋กœ์ง ๊ฐœํŽธ] + if is_hard_stagnant: + # ํŒŒ์ผ ๋ณ€ํ™”๊ฐ€ ์—†๋‹ค๋ฉด ์•„๋ฌด๋ฆฌ ๋กœ๊ทธ๊ฐ€ ์žˆ์–ด๋„ '์œ ์ง€ ๊ด€๋ฆฌ'์ผ ๋ฟ '์„ฑ์žฅ'์ด ์•„๋‹˜ + # ์˜คํžˆ๋ ค ์‹œ๊ฐ„์ด ๊ฐˆ์ˆ˜๋ก ๊ธฐ์ˆ  ๋ถ€์ฑ„์™€ ๋ฐ์ดํ„ฐ ๋…ธํ›„ํ™”๊ฐ€ ์ง„ํ–‰๋œ๋‹ค๊ณ  ํŒ๋‹จ (๊ฐ•๋ ฅ ํŒจ๋„ํ‹ฐ) + momentum_factor = -2.5 if is_no_activity else -1.0 + else: + # ์‹ค์งˆ์ ์ธ ํŒŒ์ผ ์ˆ˜ ๋ณ€ํ™”(Velocity)๊ฐ€ ์žˆ์„ ๋•Œ๋งŒ ๊ธ์ •์  ๋ชจ๋ฉ˜ํ…€ ๊ฒ€ํ†  + v_gain = features['velocity'] * 0.5 + d_gain = features['density'] * 0.8 + momentum_factor = v_gain + d_gain - 0.5 # ๊ธฐ๋ณธ์ ์œผ๋กœ ํ•˜ํ–ฅ ์••๋ ฅ ๋ถ€์—ฌ + + # ์˜ˆ์ธก ๋กœ์ง: ํ˜„์žฌ๊ฐ’ + ๋ชจ๋ฉ˜ํ…€ - (์‹œ๊ฐ„์— ๋”ฐ๋ฅธ ์ž์—ฐ ๋ถ€์‹) + # ์ •์ฒด ์‹œ momentum_factor๊ฐ€ -1.0~-2.5์ด๋ฏ€๋กœ ๊ฐ์‡„๊ฐ€ ๋งค์šฐ ๋น ๋ฆ„ + decay_constant = 0.08 predicted = current_val + momentum_factor - (decay_constant * days_ahead) + # [์ตœ์ข… ๋ฐฉ์–ด ๋กœ์ง] + # ์‹ค์งˆ์  ํŒŒ์ผ ์ฆ๊ฐ€(velocity > 0)๊ฐ€ ํฌ์ฐฉ๋˜์ง€ ์•Š์•˜๋‹ค๋ฉด ์˜ˆ๋ณด๋Š” ํ˜„์žฌ๊ฐ’๋ณด๋‹ค ํด ์ˆ˜ ์—†์Œ + if features['velocity'] <= 0 and predicted > current_val: + predicted = current_val - 1.5 # ๊ฐ•์ œ ํ•˜๋ฝ + + # ์‚ฌ๋ง ์„ ๊ณ  (AVI๊ฐ€ ์ด๋ฏธ ๋‚ฎ๊ณ  ์ •์ฒด ์ค‘์ด๋ฉด 0์— ์ˆ˜๋ ดํ•˜๋„๋ก ๊ฐ€์†) + if current_val < 20 and is_hard_stagnant: + predicted = max(0, predicted - 5.0) + return round(max(0, min(100, predicted)), 1) diff --git a/sql_queries.py b/sql_queries.py index 99ac959..fd61b1f 100644 --- a/sql_queries.py +++ b/sql_queries.py @@ -40,7 +40,7 @@ class DashboardQueries: # ํ™œ์„ฑ๋„ ๋ถ„์„์„ ์œ„ํ•œ ํ”„๋กœ์ ํŠธ ๋ชฉ๋ก ์กฐํšŒ GET_PROJECT_LIST_FOR_ANALYSIS = """ - SELECT m.project_id, m.project_nm, m.short_nm, h.recent_log, h.file_count + SELECT m.project_id, m.project_nm, m.short_nm, m.department, h.recent_log, h.file_count FROM projects_master m LEFT JOIN projects_history h ON m.project_id = h.project_id AND h.crawl_date = %s """ diff --git a/style/analysis.css b/style/analysis.css index e814b19..208ced2 100644 --- a/style/analysis.css +++ b/style/analysis.css @@ -1,5 +1,6 @@ /* ========================================================================== - Project Master Analysis - Sabermetrics Style + Project Master Analysis - Specific Styles + (Inherits base styles from common.css) ========================================================================== */ .analysis-content { @@ -18,7 +19,6 @@ font-weight: 800; display: inline-block; margin-bottom: 8px; - letter-spacing: 0.5px; } .analysis-header { @@ -26,216 +26,208 @@ justify-content: space-between; align-items: center; margin-bottom: 24px; + padding: 10px 0; + border-bottom: 1px solid var(--border-color); } -/* Top Info Grid (AI Info & SOI Deep Dive) */ +.analysis-header h2 { font-size: 22px; font-weight: 800; color: var(--text-main); margin-bottom: 4px; } +.analysis-header p { font-size: 13px; color: var(--text-sub); } + +/* Top Info Grid */ .top-info-grid { display: grid; - grid-template-columns: 1fr 2fr; + grid-template-columns: 1fr 2.2fr; gap: 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); + border-radius: var(--radius-xl); + border: 1px solid var(--border-color); padding: 20px; + box-shadow: var(--box-shadow); } -.model-desc-vertical { - display: flex; - flex-direction: column; - gap: 12px; -} +.card-header { margin-bottom: 15px; display: flex; align-items: center; justify-content: space-between; } +.card-header h4 { font-size: 14px; font-weight: 800; color: var(--primary-color); margin: 0; } -.model-item-vertical { - display: flex; - align-items: center; - gap: 12px; -} +.model-desc-vertical { display: flex; flex-direction: column; gap: 12px; } +.model-item-vertical { display: flex; align-items: center; gap: 12px; } +.model-tag { background: var(--bg-muted); color: var(--text-sub); padding: 2px 8px; border-radius: 4px; font-size: 10px; font-weight: 700; } -.model-tag { - background: #f1f5f9; - color: #475569; - padding: 2px 8px; - border-radius: 4px; - font-size: 10px; - font-weight: 700; - white-space: nowrap; -} - -.soi-info-columns { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 20px; -} - -.soi-info-column h6 { - font-size: 12px; - font-weight: 800; - color: #1e5149; - margin: 0 0 8px 0; -} - -.soi-info-column p { - font-size: 11.5px; - color: #64748b; - line-height: 1.6; - margin: 0; -} +.soi-info-columns { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; } +.soi-info-column h6 { font-size: 12px; font-weight: 800; color: var(--primary-color); margin: 0 0 8px 0; } +.soi-info-column p { font-size: 11.5px; color: var(--text-sub); line-height: 1.6; margin: 0; } /* Chart Grid Layout */ .analysis-charts-grid { display: grid; - grid-template-columns: 1.2fr 2fr; + grid-template-columns: 1fr 1.8fr; gap: 20px; margin-bottom: 24px; } .chart-container-box { background: white; - border-radius: 12px; + border-radius: var(--radius-xl); padding: 20px; - border: 1px solid #eef2f6; - height: 340px; + border: 1px solid var(--border-color); + height: 360px; display: flex; flex-direction: column; + box-shadow: var(--box-shadow); } -.chart-container-box h5 { - margin: 0 0 15px 0; - font-size: 13px; - font-weight: 700; - color: #334155; +.chart-container-box h5 { margin: 0 0 15px 0; font-size: 13px; font-weight: 700; color: var(--text-main); } + +/* Timeline Analysis Card */ +.analysis-card { + background: white; + border-radius: var(--radius-xl); + border: 1px solid var(--border-color); + box-shadow: var(--box-shadow); + margin-bottom: 24px; + overflow: hidden; } +.analysis-card .card-header { + padding: 16px 24px; + background: #fff; + border-bottom: 1px solid var(--border-color); +} + +.analysis-card .card-body { padding: 24px; } + +/* SOI Guide Styles */ +.d-war-guide { + display: flex; + gap: 10px; + margin-bottom: 20px; + padding: 12px; + background: var(--bg-muted); + border-radius: var(--radius-lg); +} + +.guide-item { + font-size: 11px; + font-weight: 700; + padding: 4px 10px; + border-radius: 4px; + display: flex; + align-items: center; + gap: 6px; +} + +.guide-item.active-low { background: #dcfce7; color: #166534; } +.guide-item.warning-mid { background: #fef9c3; color: #854d0e; } +.guide-item.danger-high { background: #ffedd5; color: #9a3412; } +.guide-item.hazard-critical { background: #fee2e2; color: #991b1b; } + /* Data Table Customization */ -.p-war-table-container { - margin-top: 24px; +.table-scroll-wrapper { + overflow-x: auto; + overflow-y: auto; + max-height: 600px; + border-radius: var(--radius-lg); + border: 1px solid var(--border-color); + background: white; } -.project-row { - cursor: pointer; - transition: background 0.2s ease; +.p-war-table { + width: 100%; + border-collapse: separate; + border-spacing: 0; + table-layout: fixed; /* ์ปฌ๋Ÿผ ๋„ˆ๋น„ ๊ณ ์ • */ } -.project-row:hover { - background: #f8fafc !important; +.p-war-table th { + position: sticky; + top: 0; + z-index: 20; + background: #f8fafc; + padding: 16px 15px; + font-size: 12px; + font-weight: 800; + color: #475569; + border-bottom: 2px solid #e2e8f0; + white-space: nowrap; } +.p-war-table td { + padding: 14px 15px; + font-size: 13px; + border-bottom: 1px solid #f1f5f9; + vertical-align: middle; +} + +/* ์ปฌ๋Ÿผ๋ณ„ ๋„ˆ๋น„ ์ •์˜ */ +.p-war-table th:nth-child(1), .p-war-table td:nth-child(1) { width: 28%; text-align: left; } /* ํ”„๋กœ์ ํŠธ๋ช… */ +.p-war-table th:nth-child(2), .p-war-table td:nth-child(2) { width: 10%; text-align: right; } /* ํŒŒ์ผ ์ˆ˜ */ +.p-war-table th:nth-child(3), .p-war-table td:nth-child(3) { width: 10%; text-align: right; } /* ๋ฐฉ์น˜์ผ */ +.p-war-table th:nth-child(4), .p-war-table td:nth-child(4) { width: 10%; text-align: center; } /* ์ƒํƒœ ํŒ์ • */ +.p-war-table th:nth-child(5), .p-war-table td:nth-child(5) { width: 14%; text-align: right; } /* P-WAR+ */ +.p-war-table th:nth-child(6), .p-war-table td:nth-child(6) { width: 12%; text-align: right; } /* ํ˜„์žฌ SOI */ +.p-war-table th:nth-child(7), .p-war-table td:nth-child(7) { width: 12%; text-align: center; } /* ์‹ค๋ฌด ํˆฌ์ž… */ +.p-war-table th:nth-child(8), .p-war-table td:nth-child(8) { width: 14%; text-align: center; } /* AI ์˜ˆ๋ณด */ + +.project-row { cursor: pointer; transition: background 0.2s; } +.project-row:hover { background: var(--hover-bg) !important; } + +/* SOI Value Styling */ +.p-war-value { font-weight: 800; font-family: 'Consolas', monospace; } + /* Accordion Detail Styles */ -.detail-row { - display: none; - background: #fdfdfd; -} - -.detail-row.active { - display: table-row; -} - -.detail-container { - padding: 20px 24px; - border-bottom: 2px solid #f1f5f9; -} +.detail-row { display: none; background: #fafafa; } +.detail-row.active { display: table-row; } +.detail-container { padding: 20px 24px; } .formula-explanation-card { background: white; - border-radius: 12px; + border-radius: var(--radius-lg); padding: 24px; - border: 1px solid #e2e8f0; - box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05); + border: 1px solid var(--border-color); + box-shadow: var(--box-shadow); } -.formula-header { - font-size: 13px; - font-weight: 700; - color: #6366f1; - margin-bottom: 15px; -} +.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; - 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; -} +/* Work Effort Section */ +.work-effort-section { background: var(--bg-muted); padding: 16px; border-radius: var(--radius-lg); margin-bottom: 20px; } +.work-effort-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; } +.work-effort-bar-bg { width: 100%; height: 6px; background: #e2e8f0; border-radius: 3px; overflow: hidden; margin-bottom: 10px; } /* Formula Steps Grid */ -.formula-steps-grid { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 20px; -} +.formula-steps-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; } +.formula-step { display: flex; gap: 12px; } +.step-num { background: var(--primary-color); color: white; width: 20px; height: 20px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 11px; font-weight: 800; flex-shrink: 0; } +.step-title { font-size: 12px; font-weight: 700; color: var(--text-main); margin-bottom: 4px; } +.math-logic { font-family: 'Consolas', monospace; background: var(--bg-muted); padding: 4px 8px; border-radius: 4px; font-weight: 700; color: var(--text-main); font-size: 12px; display: inline-block; } -.formula-step { +.final-result-area { margin-top: 20px; padding-top: 15px; border-top: 2px solid var(--primary-color); display: flex; justify-content: space-between; align-items: center; } + +/* Modal Analysis Specific */ +.modal-footer { + padding: 16px 24px; + background: #fff; + border-top: 1px solid var(--border-color); + text-align: right; display: flex; - gap: 12px; + justify-content: flex-end; } -.step-num { - background: #1e5149; - color: white; - width: 20px; - height: 20px; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - font-size: 11px; - font-weight: 800; - flex-shrink: 0; -} +/* Formula & Badges */ +.formula-section { margin-bottom: 20px; } +.formula-box { background: var(--primary-lv-0); color: var(--primary-color); padding: 15px; border-radius: var(--radius-lg); font-weight: 800; text-align: center; font-family: monospace; font-size: 16px; } -.step-title { - font-size: 12px; - font-weight: 700; - color: #334155; - margin-bottom: 4px; -} +.badge-active { background: #dcfce7; color: #166534; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 700; } +.badge-warning { background: #fef9c3; color: #854d0e; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 700; } +.badge-danger { background: #ffedd5; color: #9a3412; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 700; } +.badge-system { background: #fee2e2; color: #991b1b; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 700; } -.math-logic { - font-family: 'Consolas', monospace; - background: #f1f5f9; - padding: 4px 8px; - border-radius: 4px; - font-weight: 700; - color: #0f172a; - font-size: 12px; - display: inline-block; -} - -.final-result-area { - margin-top: 20px; - padding-top: 15px; - border-top: 2px solid #1e5149; - display: flex; - justify-content: space-between; - align-items: center; -} - -/* Utility Classes */ .highlight-var { color: #2563eb; } .highlight-val { color: #059669; } .highlight-penalty { color: #dc2626; } .text-plus { color: #059669; font-weight: 700; } .text-minus { color: #dc2626; font-weight: 700; } +.font-bold { font-weight: 700; } diff --git a/templates/analysis.html b/templates/analysis.html index 973da5a..630c45d 100644 --- a/templates/analysis.html +++ b/templates/analysis.html @@ -30,11 +30,11 @@
AI Sabermetrics
-

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

-

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

+

์‹œ์Šคํ…œ ์šด์˜ ์ž์‚ฐ ๊ฐ€์น˜ ๋ถ„์„

+

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

- +
@@ -47,12 +47,12 @@
- ์•Œ๊ณ ๋ฆฌ์ฆ˜ -

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

+ ๋ถ„์„ ๋ชจ๋ธ +

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

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

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

+ ๊ฐ€์ค‘์น˜ ๋กœ์ง +

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

@@ -65,16 +65,16 @@
-
1. AI ์ž์‚ฐ ๊ฐ€์น˜ ํ‰๊ฐ€
-

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

+
1. ์ž์‚ฐ ๊ฐ€์น˜ ๋ณ€๋™ ์ถ”์ 
+

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

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

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

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

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

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

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

+
3. ๋™์  ๊ฐ€์น˜ ๊ณ„์ˆ˜
+

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

@@ -84,11 +84,11 @@
-
๊ฑด๊ฐ• ์ƒํƒœ ๋ถ„ํฌ (Project Distribution)
+
์šด์˜ ํ™œ๋ ฅ ๋ถ„ํฌ (Activity Distribution)
-
ํ”„๋กœ์ ํŠธ SWOT ๋งคํŠธ๋ฆญ์Šค (Strategic Analysis)
+
์ž์‚ฐ ๊ฐ€์น˜ ์ „๋žต ๋งคํŠธ๋ฆญ์Šค (Strategic Analysis)
@@ -97,19 +97,19 @@
-

Project Stagnation Objective Index (P-SOI Status)

-

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

+

Project Activity Vitality Leaderboard (AVI Status)

+

์šด์˜ ํ‘œ์ค€(AVI 70%) ๋Œ€๋น„ ํŒŒ์ผ ๋ณด์กด์œจ ๋ฐ ๋ฏธ๋ž˜ ๊ฐ€์น˜ ๊ธฐ์—ฌ ๋ฆฌ๋”๋ณด๋“œ

- * SOI (Project Health Score) + * AVI (Activity Vitality Index)
-
70%โ†‘ ์ •์ƒ
-
30~70% ์ฃผ์˜
-
10~30% ์œ„ํ—˜
-
10%โ†“ ์‚ฌ๋ง
+
70%โ†‘ ์ •์ƒ ์šด์˜
+
30~70% ๊ด€๋ฆฌ ์ฃผ์˜
+
10~30% ์œ„ํ—˜ ๋…ธ์ถœ
+
10%โ†“ ์ค‘๋‹จ/๋ฐฉ์น˜
@@ -124,7 +124,7 @@