From be3210463f0d81e5158cffc670a93d369eaccfee Mon Sep 17 00:00:00 2001 From: Taehoon Date: Mon, 23 Mar 2026 17:49:24 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20=EB=B6=84=EC=84=9D=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EC=BD=94=EB=93=9C=20=EC=A0=95=EB=8F=88=20?= =?UTF-8?q?=EB=B0=8F=20AI=20=EC=97=94=EC=A7=84=20=EA=B3=A0=EB=8F=84?= =?UTF-8?q?=ED=99=94=20=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ANALYSIS_REPORT.md | 49 ++ __pycache__/analysis_service.cpython-312.pyc | Bin 0 -> 8966 bytes __pycache__/inquiry_service.cpython-312.pyc | Bin 0 -> 2795 bytes .../prediction_service.cpython-312.pyc | Bin 0 -> 3473 bytes __pycache__/project_service.cpython-312.pyc | Bin 0 -> 2064 bytes __pycache__/schemas.cpython-312.pyc | Bin 0 -> 668 bytes __pycache__/server.cpython-312.pyc | Bin 21447 -> 13292 bytes analysis_service.py | 44 +- js/analysis.js | 409 +++++-------- js/analysis.js_fragment_leaderboard | 160 +++++ prediction_service.py | 113 ++-- project_master.db | 0 style/analysis.css | 564 ++++-------------- templates/analysis.html | 105 ++-- 14 files changed, 590 insertions(+), 854 deletions(-) create mode 100644 ANALYSIS_REPORT.md create mode 100644 __pycache__/analysis_service.cpython-312.pyc create mode 100644 __pycache__/inquiry_service.cpython-312.pyc create mode 100644 __pycache__/prediction_service.cpython-312.pyc create mode 100644 __pycache__/project_service.cpython-312.pyc create mode 100644 __pycache__/schemas.cpython-312.pyc create mode 100644 js/analysis.js_fragment_leaderboard create mode 100644 project_master.db 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 0000000000000000000000000000000000000000..1a1a5bad99b70dce0ee969c0f86e15af2568ac95 GIT binary patch literal 8966 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 literal 0 HcmV?d00001 diff --git a/__pycache__/prediction_service.cpython-312.pyc b/__pycache__/prediction_service.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b100aa727c2da4a3fcb0408c024632f39ba72e67 GIT binary patch literal 3473 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 literal 0 HcmV?d00001 diff --git a/__pycache__/project_service.cpython-312.pyc b/__pycache__/project_service.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..76db69855cc48854b5fb0c5a1d28931acffb6e45 GIT binary patch literal 2064 zcmaJ?O>7fK6rR~1d*e8^V~7(-38{ip9B3#iN-3nJCP`_)m>)+}b&a;m?j*6Xy=Ha; z4Rr)kb8sZ2IK3oNi)4DD;?hbSqN-A>st2TA?BX`oR+XSVRVq?wgeo|7X1(i%zmBx; zXWo1B-p>2p?vE`kE8kZlrDe}8#P=qmM)}4MbgnlA^2W@Fw-AYLcHNbA0l~5N0`2+vEk6jm>5149*u+# zMn=Tp@YqPq5XO-%PfF>uA!s@t*OM6(Vhx8pg*6o$99A!A21_YDqPx5M!9K!=_a*+i zdh_1LpWXYkkl6jYb;(C1jigG1M~74ejUi1>CeGLHY&%+PVS}6+3 zCzjoQ6J*Z-NCRYSo>f>1DKi(Ra1$U2V-%YIpVOS_$CKun@kaG#-j>JhRBPlVd1m-L zHw;ckNp45pq42Z{++j;8@AIQEZ+m4qB00?Zoq0ah`gBa*Nj#j?gK)v(O%``qyxHPz z$|s!v-QJbs98kDCPwynu5`6?;egylCrZhpV2}47$;Hr^Pm{hSYW;2G%hA@>Cl#{TY zL0R2!WK6IdE{tTNQv@5X@gzaCtWFVoldNX-WR|)*%^f@)YBHP{<fTG$uwyV`G?h%izhZCUb^E%H>d22?^LucLn+Z z5Q)iX)+J2Ghi;JZMH;3J#BJ0IlCjQ88Klu7$V8h9(-s*48e_jPiIuF$3_fErpLU~_ zX%7jx4DRk3{I26x?E9_7$(2BFCD2>at`AiMuar;43(a@En`>_Wd~_~a?cA}_d7#pH zpz40B=I>bCwXo~9zo#%%+q`AzY-Mv_;RyBJ*i~Z68+yv_p7qndx6;0MdH-^MRTy1A z@pm?~6@*_~+X~^@wqPms`Oz<>W!G1km4VU9z-V>gNO5;D{>dBV6DP|tv2x;r!f?&K zasJrcv6^pVQCe~@sHJn)JFb0H^R*UtE_h3=rNL`ksj_3iQ-Y&BIFBz`>0GCf_aBwZkkg4 z7BHk9H(B4{#P!*)w1Sxt6fnmrDWXonZnb}45oCqGh zRnY!`;?-kMUi|}Mz>_Dzf{^s$$#-@WP$+a@pZ9q`_WgLi)oN>iuX=G6entDkI?LxR z$m|fwIS3FWf&u9e0uDR_;Y>hKNj}rEuHzI@7o*-kQJSv=m7lgGAZGP3*Rque8THDa zN?de+*#q>?K|+TJ=s2%IQt1j}=NHVeET@5M){nBZe=0|r)TyatS}GO_;}5ykZxSWi zm!9z%i}^q@X4V)RBqEC}uQE2uc(lk+ohs8{jK^`3@-$3h&DbT(>{>T_2u}~Xzb?L9 zk3U_1eAnIGf2oo)*-Kkor&4u~j-TKuuW*H~?)Bw>>z!e)tZW6?t8^9NElg`05h00)-Z|(83KovvnMgvQXuxau^|$ns~~3W<}#!nKqR^k3}R^Tlsc= z-9oX+hGm;es36fz1YCK&K7Kx}HpVY-`N>IHZ428dzdy_ckJGTH?2+ZkMU(#<|M3q? n;UB4OtRwj8`v|`&A>=#Uo4XF#d>_mK&D^J?`60LgG{vyLjpv-F literal 0 HcmV?d00001 diff --git a/__pycache__/server.cpython-312.pyc b/__pycache__/server.cpython-312.pyc index b9302e90b3850d031377426d768c002a30895f0b..17d489e0a375993521b09fa0b76f0c7597cfb39c 100644 GIT binary patch literal 13292 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 Xsz_$hQky1jw4ecVp**12!u$UK@KGO` literal 21447 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 QPQfY`U@DXcF=BZ5zqTXOR{#J2 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
-
-
-

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

-
    -
  • ์ด์ƒ์  ์ƒํƒœ (100%): ์ตœ๊ทผ 24์‹œ๊ฐ„ ์ด๋‚ด ํ™œ๋™ ๋กœ๊ทธ๊ฐ€ ๋ฐœ์ƒํ•œ ๊ฒฝ์šฐ์ž…๋‹ˆ๋‹ค.
  • -
  • ์ง€์ˆ˜ ๊ฐ์‡„ ๋ชจ๋ธ: ๋ฐฉ์น˜์ผ์ˆ˜๊ฐ€ ๋Š˜์–ด๋‚ ์ˆ˜๋ก ๊ฐ€์น˜๊ฐ€ ๊ธฐํ•˜๊ธ‰์ˆ˜์ ์œผ๋กœ ํ•˜๋ฝํ•˜๋„๋ก ์„ค๊ณ„๋˜์—ˆ์Šต๋‹ˆ๋‹ค. (14์ผ ๋ฐฉ์น˜ ์‹œ ์•ฝ 50% ์†Œ์‹ค)
  • -
  • ์‹œ์Šคํ…œ ์‚ฌ๋ง: ์ง€์ˆ˜๊ฐ€ 10% ๋ฏธ๋งŒ์ผ ๊ฒฝ์šฐ, ํ™œ๋™ ์žฌ๊ฐœ ๊ฐ€๋Šฅ์„ฑ์ด ํฌ๋ฐ•ํ•œ ์ข€๋น„ ๋ฐ์ดํ„ฐ๋กœ ๊ฐ„์ฃผํ•ฉ๋‹ˆ๋‹ค.
  • -
-
- `; - } else if (type === 'ai') { - title.innerText = 'AI ์‹œ๊ณ„์—ด ์˜ˆ์ธก ์•Œ๊ณ ๋ฆฌ์ฆ˜ ์ƒ์„ธ'; - body.innerHTML = ` -
- ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ์ถ”์„ธ ์—”์ง„ -
Pred = (Linear ร— w1) + (Decay ร— w2)
-
-
-

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

-
    -
  • ์ถ”์„ธ ๋ถ„์„ (Linear): ์ตœ๊ทผ ํ™œ๋™ ๋กœ๊ทธ์˜ ๋นˆ๋„๊ฐ€ ์ฆ๊ฐ€ ์ถ”์„ธ์ผ ๊ฒฝ์šฐ, ํ–ฅํ›„ ๊ด€๋ฆฌ ์žฌ๊ฐœ ๊ฐ€๋Šฅ์„ฑ์„ ๋†’๊ฒŒ ํ‰๊ฐ€ํ•˜์—ฌ ๊ฐ€์ ์„ ๋ถ€์—ฌํ•ฉ๋‹ˆ๋‹ค.
  • -
  • ์ž์—ฐ ์†Œ๋ฉธ (Decay): ์žฅ๊ธฐ ์ •์ฒด ์ค‘์ธ ํ”„๋กœ์ ํŠธ๋Š” ์ง€์ˆ˜ ๊ฐ์‡„ ๋ชจ๋ธ์„ 80% ์ด์ƒ ๋ฐ˜์˜ํ•˜์—ฌ ๊ธ‰๊ฒฉํ•œ ํ•˜๋ฝ์„ ๊ฒฝ๊ณ ํ•ฉ๋‹ˆ๋‹ค.
  • -
  • ์ •๋ฐ€๋„: ํ˜„์žฌ Regression ๊ธฐ๋ฐ˜ ๋ชจ๋ธ์ด๋ฉฐ, ๋ฐ์ดํ„ฐ๊ฐ€ 30ํšŒ ์ด์ƒ ์ถ•์ ๋˜๋ฉด LSTM ์‹ ๊ฒฝ๋ง์œผ๋กœ ์ž๋™ ์ „ํ™˜๋ฉ๋‹ˆ๋‹ค.
  • -
-
- `; + 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%โ†“ ์‚ฌ๋ง
+
+ +
+
- -