From 705246923b7f14c641780e837d59d6419321f304 Mon Sep 17 00:00:00 2001 From: Taehoon Date: Mon, 22 Jun 2026 10:06:19 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=A6=AC=EB=88=85=EC=8A=A4=20=ED=99=98?= =?UTF-8?q?=EA=B2=BD=20=ED=8C=8C=EC=9D=BC=20=EB=8F=99=EA=B8=B0=ED=99=94,?= =?UTF-8?q?=20Dockerfile=20=EB=B0=8F=20docker-compose=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?,=20=EC=BA=90=EC=8B=9C=20=EB=AC=B4=EC=8B=9C=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 12 +- .gitignore | Bin 103 -> 72 bytes ANALYSIS_REPORT.md | 141 +- Dockerfile | 40 + PLAN.md | 68 +- README.md | 172 +- __pycache__/analysis_service.cpython-312.pyc | Bin 9746 -> 0 bytes __pycache__/analyze.cpython-312.pyc | Bin 13366 -> 0 bytes __pycache__/crawler_api.cpython-312.pyc | Bin 3161 -> 0 bytes __pycache__/crawler_service.cpython-312.pyc | Bin 18254 -> 0 bytes __pycache__/inquiry_service.cpython-312.pyc | Bin 2795 -> 0 bytes .../prediction_service.cpython-312.pyc | Bin 4164 -> 0 bytes __pycache__/project_service.cpython-312.pyc | Bin 2064 -> 0 bytes __pycache__/schemas.cpython-312.pyc | Bin 668 -> 0 bytes __pycache__/server.cpython-312.pyc | Bin 13292 -> 0 bytes __pycache__/sql_queries.cpython-312.pyc | Bin 2842 -> 0 bytes analysis_service.py | 468 ++-- analyze.py | 334 +-- analyze_logs_pattern.py | 48 + check_tables.py | 29 + clear_test_db.py | 29 + clone_db.py | 45 + crawler_service.py | 516 ++-- crawler_service_test.py | 273 ++ docker-compose.yml | 17 + inquiry_service.py | 84 +- js/analysis.js | 948 +++---- js/analysis.js_fragment_leaderboard | 356 +-- js/analysis_test.js | 485 ++++ js/common.js | 156 +- js/dashboard.js | 474 ++-- js/dashboard_test.js | 245 ++ js/inquiries.js | 628 ++--- js/mail.js | 624 ++--- log_scorer.py | 63 + prediction_service.py | 194 +- project_service.py | 66 +- requirements.txt | 12 +- schemas.py | 20 +- server.py | 364 +-- server_test.py | 190 ++ sql_queries.py | 149 +- style/analysis.css | 466 ++-- style/common.css | 340 +-- style/dashboard.css | 246 +- style/inquiries.css | 502 ++-- style/mail.css | 438 +-- style/style.css | 6 +- templates/analysis.html | 278 +- templates/analysis_test.html | 139 + templates/dashboard.html | 234 +- templates/dashboard_test.html | 118 + templates/index.html | 102 +- templates/inquiries.html | 386 +-- templates/mailTest.html | 286 +- templates/modals/address_book.html | 124 +- templates/modals/path_selector.html | 48 +- tokens.json | 2456 ++++++++--------- verify_swvw.py | 28 + 59 files changed, 7653 insertions(+), 5794 deletions(-) create mode 100644 Dockerfile delete mode 100644 __pycache__/analysis_service.cpython-312.pyc delete mode 100644 __pycache__/analyze.cpython-312.pyc delete mode 100644 __pycache__/crawler_api.cpython-312.pyc delete mode 100644 __pycache__/crawler_service.cpython-312.pyc delete mode 100644 __pycache__/inquiry_service.cpython-312.pyc delete mode 100644 __pycache__/prediction_service.cpython-312.pyc delete mode 100644 __pycache__/project_service.cpython-312.pyc delete mode 100644 __pycache__/schemas.cpython-312.pyc delete mode 100644 __pycache__/server.cpython-312.pyc delete mode 100644 __pycache__/sql_queries.cpython-312.pyc create mode 100644 analyze_logs_pattern.py create mode 100644 check_tables.py create mode 100644 clear_test_db.py create mode 100644 clone_db.py create mode 100644 crawler_service_test.py create mode 100644 docker-compose.yml create mode 100644 js/analysis_test.js create mode 100644 js/dashboard_test.js create mode 100644 log_scorer.py create mode 100644 server_test.py create mode 100644 templates/analysis_test.html create mode 100644 templates/dashboard_test.html create mode 100644 verify_swvw.py diff --git a/.env b/.env index d1fdeed..d972166 100644 --- a/.env +++ b/.env @@ -1,6 +1,6 @@ -PM_USER_ID=b21364 -PM_PASSWORD=b21364!. -DB_HOST=localhost -DB_USER=root -DB_PASSWORD=45278434 -DB_NAME=PM_proto +PM_USER_ID=b21364 +PM_PASSWORD=b21364!. +DB_HOST=localhost +DB_USER=root +DB_PASSWORD=45278434 +DB_NAME=PM_proto diff --git a/.gitignore b/.gitignore index b86406d1585ac2efa762572e5a953a7d38bf7f81..bc60a930d961aedadf76aad4d58d9c85f6e205f8 100644 GIT binary patch literal 72 zcma!#FQ`mTOwLG+kJsnY(gSjUWKMoM7ZArM<|XD-7H1a67o`@L=9K7_RFrV#<)@^^ X=jNxB=A;(ubLqJR#|M|>7o`FKq!k(H literal 103 zcmXAfK?;B%6hz-z=prukNFVt~E`lU2@bcBS7#Ns&@3|Y9gX?Rd(Mh&DCzZg)&dP#A eER}&8SBm-bi68T3{%o2~qz+A5vPg73*mwb;vKA-+ diff --git a/ANALYSIS_REPORT.md b/ANALYSIS_REPORT.md index 594edec..9588e19 100644 --- a/ANALYSIS_REPORT.md +++ b/ANALYSIS_REPORT.md @@ -1,81 +1,60 @@ -# ๐Ÿ“Š ์‹œ์Šคํ…œ ์šด์˜ ์ž์‚ฐ ๊ฐ€์น˜ ๋ถ„์„ ๋ณด๊ณ ์„œ (Sabermetrics Edition) - -๋ณธ ๋ณด๊ณ ์„œ๋Š” ์•ผ๊ตฌ์˜ ํ†ต๊ณ„ ๋ถ„์„ ๊ธฐ๋ฒ•์ธ **์„ธ์ด๋ฒ„๋ฉ”ํŠธ๋ฆญ์Šค(Sabermetrics)**๋ฅผ ํ”„๋กœ์ ํŠธ ๊ด€๋ฆฌ ์‹œ์Šคํ…œ์— ์ด์‹ํ•˜์—ฌ, ๋‹จ์ˆœ ํ™œ๋™๋Ÿ‰ ์ธก์ •์„ ๋„˜์–ด **'์‹ค์งˆ์  ์ž์‚ฐ ๊ฐ€์น˜'**์™€ **'๋ฏธ๋ž˜ ์šด์˜ ์œ„ํ—˜'**์„ ์ •๋ฐ€ ๋ถ„์„ํ•œ ๊ฒฐ๊ณผ์ž…๋‹ˆ๋‹ค. - ---- - -## 1. ํ•ต์‹ฌ ๋ถ„์„ ์ง€ํ‘œ ์ •์˜ (Core Metrics) - -### 1.1 ์šด์˜ ํ™œ๋ ฅ ์ง€์ˆ˜ (AVI, Activity Vitality Index) -ํ”„๋กœ์ ํŠธ๊ฐ€ ํ˜„์žฌ ์–ผ๋งˆ๋‚˜ '์‚ด์•„์„œ ์ˆจ ์‰ฌ๊ณ  ์žˆ๋Š”๊ฐ€'๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ์ƒ์กด ์ง€์ˆ˜์ž…๋‹ˆ๋‹ค. - -* **์‚ฐ์ถœ ๊ณต์‹**: $AVI = exp(-\lambda \times days) \times Quality \times ECV \times 100$ -* **ํ•ต์‹ฌ ๋ฐ์ดํ„ฐ**: - * **์ •์ฒด ์ผ์ˆ˜(days)**: ๋งˆ์ง€๋ง‰ ์œ ์˜๋ฏธํ•œ ํŒŒ์ผ ์—…๋ฐ์ดํŠธ ์ดํ›„ ๊ฒฝ๊ณผ ์‹œ๊ฐ„. - * **๊ฐ์‡„ ๊ณ„์ˆ˜($\lambda$)**: ๊ธฐ๋ณธ $0.04$์—์„œ ์‹œ์ž‘ํ•˜์—ฌ, ์ž์‚ฐ ๊ทœ๋ชจ(์ตœ๋Œ€ $+0.04$)์™€ ๋ถ€์„œ ์ •์ฒด์œจ(์ตœ๋Œ€ $+0.03$)์„ ๋™์ ์œผ๋กœ ๊ฒฐํ•ฉํ•ฉ๋‹ˆ๋‹ค. - * **ํ™œ๋™ ํ’ˆ์งˆ(Quality)**: ํŒŒ์ผ ์ฆ๋ถ„ ํ™œ๋™($1.0$), ๊ตฌ์กฐ์  ๊ด€๋ฆฌ($0.7$), ๋‹จ์ˆœ ํ–‰์ • ๋กœ๊ทธ($0.4$)๋กœ ์ฐจ๋“ฑ ๋ฐฐ์ ํ•ฉ๋‹ˆ๋‹ค. - * **์กด์žฌ ์‹ ๋ขฐ๋„(ECV)**: ํŒŒ์ผ ์ˆ˜ $0$๊ฐœ($0.05$), $10$๊ฐœ ๋ฏธ๋งŒ($0.4$) ๋“ฑ ์œ ๋ น ํ”„๋กœ์ ํŠธ์— ํŒจ๋„ํ‹ฐ๋ฅผ ๋ถ€์—ฌํ•ฉ๋‹ˆ๋‹ค. -* **์˜๋ฏธ**: 100%์— ๊ฐ€๊นŒ์šธ์ˆ˜๋ก ์‹ค์‹œ๊ฐ„ ๊ฐ€๋™ ์ƒํƒœ์ด๋ฉฐ, 0%์— ๊ฐ€๊นŒ์šธ์ˆ˜๋ก ๋ฐ์ดํ„ฐ ๋…ธํ›„ํ™”๊ฐ€ ์™„๋ฃŒ๋œ '์‚ฌ๋ง' ์ƒํƒœ๋ฅผ ๋œปํ•ฉ๋‹ˆ๋‹ค. - -### 1.2 ์ž์‚ฐ ๊ฐ€์น˜ ๊ธฐ์—ฌ๋„ (VCI, Value Contribution Index) -์‹œ์Šคํ…œ ์ „์ฒด์˜ ์šด์˜ ํ‘œ์ค€ ๋Œ€๋น„, ํ•ด๋‹น ํ”„๋กœ์ ํŠธ๊ฐ€ ๊ธฐ์—ฌํ•˜๊ณ  ์žˆ๋Š” ๊ฐ€์น˜์˜ ์ƒ๋Œ€์  ํ•˜์ค‘์„ ์ธก์ •ํ•ฉ๋‹ˆ๋‹ค. - -* **์‚ฐ์ถœ ๊ณต์‹**: $VCI = (AVI - 70.0) \times (\frac{Files}{200} + 0.5)$ -* **ํ•ต์‹ฌ ๋กœ์ง**: - * **๊ฑด๊ฐ• ๊ธฐ์ค€์„ (70.0%)**: ์‹œ์Šคํ…œ ์ž์‚ฐ ๊ฐ€์น˜๋ฅผ ์œ ์ง€ํ•˜๊ธฐ ์œ„ํ•œ ์ตœ์†Œ ๋งˆ์ง€๋…ธ์„ (Replacement Level)์ž…๋‹ˆ๋‹ค. - * **๊ทœ๋ชจ ๊ฐ€์ค‘์น˜**: ํŒŒ์ผ $200$๊ฐœ๋ฅผ $1.0$ ๊ฐ€์ค‘์น˜ ๊ธฐ์ค€์œผ๋กœ ์‚ผ์•„, ๋Œ€ํ˜• ํ”„๋กœ์ ํŠธ์ผ์ˆ˜๋ก ์‹œ์Šคํ…œ์— ์ฃผ๋Š” ์ถฉ๊ฒฉ์„ ๊ธฐํ•˜๊ธ‰์ˆ˜์ ์œผ๋กœ ๋ฐ˜์˜ํ•ฉ๋‹ˆ๋‹ค. -* **์˜๋ฏธ**: ์–‘์ˆ˜(+)๋Š” ๊ฐ€์น˜ ์ฐฝ์ถœ, ์Œ์ˆ˜(-)๋Š” ์‹œ์Šคํ…œ ๊ธฐํšŒ๋น„์šฉ์„ ๊ฐ‰์•„๋จน๋Š” '๊ฐ€์น˜ ํŒŒ๊ดด' ์ƒํƒœ์ž„์„ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค. - -### 1.3 ์—…๋ฌด ์ง‘์ค‘๋„ (Job Focus) -๋‹จ์ˆœ ๊ด€๋ฆฌ ํ–‰์œ„๋ฅผ ์ œ์™ธํ•˜๊ณ , ์‹ค์ œ ์„ฑ๊ณผ๋ฌผ(ํŒŒ์ผ)์„ ์ƒ์‚ฐํ•˜๋Š” ๋ฐ ์–ผ๋งˆ๋‚˜ ๋ชฐ์ž…ํ–ˆ๋Š”์ง€๋ฅผ ํŒ๋ณ„ํ•ฉ๋‹ˆ๋‹ค. - -* **์‚ฐ์ถœ ๊ณต์‹**: $Job Focus = \frac{\text{์ตœ๊ทผ ํžˆ์Šคํ† ๋ฆฌ ์ค‘ ์‹ค์ œ ํŒŒ์ผ ๋ณ€๋™ ๋ฐœ์ƒ ํšŸ์ˆ˜}}{\text{์ „์ฒด ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ํšŸ์ˆ˜}} \times 100$ -* **์˜๋ฏธ**: ๋กœ๊ทธ๋งŒ ๋‚จ๊ธฐ๋Š” '๋ณด์—ฌ์ฃผ๊ธฐ์‹ ํ™œ๋™'์„ ํ•„ํ„ฐ๋งํ•˜์—ฌ ์šด์˜์˜ ์ง„์ •์„ฑ์„ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. - -### 1.4 ์šด์˜ ์ผ๊ด€์„ฑ ์ง€์ˆ˜ (OCI, Operational Consistency Index) -ํ”„๋กœ์ ํŠธ ๊ด€๋ฆฌ์˜ '๋ฆฌ๋“ฌ'๊ณผ '์„ฑ์‹ค๋„'๋ฅผ ์ธก์ •ํ•˜๋Š” ์ง€ํ‘œ์ž…๋‹ˆ๋‹ค. - -* **์‚ฐ์ถœ ๊ณต์‹**: ์ตœ๊ทผ 30์ผ ๋ฐ์ดํ„ฐ๋ฅผ 4๊ฐœ ์ฃผ์ฐจ๋กœ ๋ถ„ํ• ํ•˜์—ฌ ํ™œ๋™ ์—ฌ๋ถ€ ๋ถ„์„ (์ฃผ์ฐจ๋ณ„ ์„ฑ์‹ค๋„ 70% + ํ™œ๋™ ๋ฐ€๋„ 30%) -* **์˜๋ฏธ**: ํŠน์ • ์‹œ์ ์— ๋ชฐ์•„์น˜๊ธฐ์‹ ์ž‘์—…์„ ํ•˜๋Š” ํ”„๋กœ์ ํŠธ๋ณด๋‹ค, ๋งค์ฃผ ๊พธ์ค€ํžˆ ๊ด€๋ฆฌ๋˜๋Š” ํ”„๋กœ์ ํŠธ์— ๋” ๋†’์€ ์‹ ๋ขฐ ์ ์ˆ˜๋ฅผ ๋ถ€์—ฌํ•ฉ๋‹ˆ๋‹ค. - ---- - -## 2. ๋“ฑ๊ธ‰ ์ฒด๊ณ„ ๋ฐ ๊ด€๋ฆฌ ๊ฐ€์ด๋“œ (Grade System) - -### 2.1 VCI ๋“ฑ๊ธ‰ (ํ”„๋กœ์ ํŠธ ์œ„์ƒ) -| ๋“ฑ๊ธ‰ (Grade) | ์ ์ˆ˜ ๊ธฐ์ค€ | ์šด์˜ ์˜๋ฏธ ๋ฐ ๊ด€๋ฆฌ ์ „๋žต | -| :--- | :--- | :--- | -| **Masterpiece** | +10.0 ์ด์ƒ | **์ตœ์šฐ๋Ÿ‰ ์ž์‚ฐ**: ์‹œ์Šคํ…œ ๊ฐ€์น˜๋ฅผ ๊ฒฌ์ธํ•˜๋Š” ํ•ต์‹ฌ ํ”„๋กœ์ ํŠธ | -| **Blue Chip** | +2.0 ~ +10.0 | **์šฐ๋Ÿ‰ ์ž์‚ฐ**: ๊พธ์ค€ํ•œ ํ™œ๋ ฅ์œผ๋กœ ๊ฐ€์น˜๋ฅผ ์ฐฝ์ถœํ•˜๋Š” ํ•ต์‹ฌ๊ตฐ | -| **Steady** | -2.0 ~ +2.0 | **์•ˆ์ • ์ž์‚ฐ**: ํ‘œ์ค€ ์ˆ˜์ค€์˜ ์šด์˜์„ ์œ ์ง€ ์ค‘์ธ ํ˜„์ƒ ์œ ์ง€๊ตฐ | -| **Underperform** | -10.0 ~ -2.0 | **์ €์„ฑ๊ณผ ์ž์‚ฐ**: ๊ทœ๋ชจ ๋Œ€๋น„ ํ™œ๋ ฅ์ด ๋ถ€์กฑํ•˜์—ฌ ๊ฐ€์น˜ ํ•˜๋ฝ ์ค‘์ธ ๊ทธ๋ฃน | -| **Liability** | -10.0 ์ดํ•˜ | **๊ณ ์œ„ํ—˜ ์ž์‚ฐ**: ์‹œ์Šคํ…œ ๊ฐ€์น˜๋ฅผ ํ›ผ์† ์ค‘์ธ ๋ฐฉ์น˜ ํ”„๋กœ์ ํŠธ. ์ฆ‰์‹œ ์กฐ์น˜ ํ•„์š” | - -### 2.2 ์šด์˜ ์ผ๊ด€์„ฑ (OCI) ํŒ์ • -* **์ •๊ธฐ์  (80%โ†‘)**: ์ฃผ ๋‹จ์œ„์˜ ์ •๊ธฐ์  ๊ด€๋ฆฌ๊ฐ€ ์™„๋ฒฝํžˆ ์ด๋ค„์ง€๋Š” ์ตœ์šฐ๋Ÿ‰ ๊ด€๋ฆฌ ์ƒํƒœ. -* **์•ˆ์ •์  (50~80%)**: ๊ฐ„ํ—์  ์ •์ฒด๋Š” ์žˆ์œผ๋‚˜ ์ „๋ฐ˜์ ์ธ ๊ด€๋ฆฌ ๋ฆฌ๋“ฌ์„ ์œ ์ง€ํ•˜๋Š” ์ƒํƒœ. -* **๊ฐ„ํ—์  (20~50%)**: ๊ด€๋ฆฌ ํ™œ๋™์ด ๋ถˆ๊ทœ์น™ํ•˜๋ฉฐ, ํ•„์š”์— ์˜ํ•œ ์ผํšŒ์„ฑ ์ž‘์—… ์ค‘์‹ฌ์ธ ์ƒํƒœ. -* **๋ถˆ๊ทœ์น™ (20%โ†“)**: ์žฅ๊ธฐ ์ •์ฒด ์ค‘์ด๊ฑฐ๋‚˜ ๊ด€๋ฆฌ์˜ ์˜์†์„ฑ์„ ํ™•์ธํ•˜๊ธฐ ์–ด๋ ค์šด ์œ„ํ—˜ ์ƒํƒœ. - ---- - -## 3. ๋ฐ์ดํ„ฐ ๋ถ„์„ ํ”„๋กœ์„ธ์Šค (Analysis Process) - -1. **๋ฐ์ดํ„ฐ ์ˆ˜์ง‘**: `projects_history` ํ…Œ์ด๋ธ”๋กœ๋ถ€ํ„ฐ ์ผ๋ณ„ ํŒŒ์ผ ์ˆ˜ ๋ฐ ๋กœ๊ทธ ํ…์ŠคํŠธ๋ฅผ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค. -2. **ํ”ผ์ฒ˜ ์ถ”์ถœ**: - * **Velocity**: ํŒŒ์ผ ์ˆ˜์˜ ๋ณ€ํ™” ์†๋„ ๊ณ„์‚ฐ. - * **Acceleration**: ํ™œ๋™์˜ ๊ฐ€์†/๊ฐ์† ์—ฌ๋ถ€ ํŒ๋ณ„. - * **Stagnation**: ๋งˆ์ง€๋ง‰ ํ™œ๋™ ์ดํ›„์˜ ๊ณต๋ฐฑ ๊ธฐ๊ฐ„ ์ธก์ •. -3. **AI ์‹œ๋ฎฌ๋ ˆ์ด์…˜**: ์ถ”์ถœ๋œ ํ”ผ์ฒ˜๋ฅผ AI ์œ„ํ—˜ ์ ์‘ํ˜• ๋ชจ๋ธ (AAS)์— ์ž…๋ ฅํ•˜์—ฌ ๊ฐœ๋ณ„ ํ”„๋กœ์ ํŠธ๋งŒ์˜ **'์œ„ํ—˜ ๊ณก์„ '**์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. -4. **์ตœ์ข… ํŒ์ •**: AVI์™€ VCI๋ฅผ ๊ฒฐํ•ฉํ•˜์—ฌ ๋ฆฌ๋”๋ณด๋“œ์— ๋“ฑ๊ธ‰๊ณผ ๊ด€๋ฆฌ ๊ฐ€์ด๋“œ๋ผ์ธ์„ ์†ก์ถœํ•ฉ๋‹ˆ๋‹ค. - ---- - -## 4. ๊ด€๋ฆฌ์ž ์ œ์–ธ (Action Plan) - -* **VCI ์Œ์ˆ˜ ํ”„๋กœ์ ํŠธ ์ง‘์ค‘ ๊ด€๋ฆฌ**: ๋‹จ์ˆœ ํ™œ๋™๋Ÿ‰์ด ์•„๋‹Œ VCI๊ฐ€ ๋‚ฎ์€ ๋Œ€ํ˜• ํ”„๋กœ์ ํŠธ๋ถ€ํ„ฐ ์šฐ์„ ์ ์œผ๋กœ ์ธ๋ ฅ์„ ๋ฐฐ์น˜ํ•˜๊ฑฐ๋‚˜ ์šด์˜ ์ •์ฑ…์„ ์žฌ์ ๊ฒ€ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. -* **AI Forecast ํ™œ์šฉ**: 'ํ™œ๋ ฅ ์ €ํ•˜' ์˜ˆ๋ณด๊ฐ€ ๋œฌ ํ”„๋กœ์ ํŠธ๋Š” ์‹ค์ œ AVI๊ฐ€ ๊ธ‰๋ฝํ•˜๊ธฐ ์ „ ์„ ์ œ์ ์ธ ์กฐ์น˜(์—…๋ฌด ๋…๋ ค, ํŒŒ์ผ ํ˜„ํ–‰ํ™”)๋ฅผ ์ทจํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. -* **ํŒŒ์ผ ์ˆ˜์™€ ํ™œ๋ ฅ์˜ ๊ท ํ˜•**: ํŒŒ์ผ ์ˆ˜๊ฐ€ ๋งŽ์€๋ฐ ํ™œ๋ ฅ(AVI)์ด ๋‚ฎ์€ ๊ฒฝ์šฐ, ์‹œ์Šคํ…œ ์ „์ฒด์˜ ๋ฐ์ดํ„ฐ ๋ฌด๊ฒฐ์„ฑ์„ ํ•ด์น  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ๋ฐ์ดํ„ฐ ํด๋ Œ์ง•์ด๋‚˜ ์•„์นด์ด๋น™์„ ๊ถŒ๊ณ ํ•ฉ๋‹ˆ๋‹ค. - ---- -*๋ณธ ๋ถ„์„ ์—”์ง„์€ Project Master Sabermetrics ์•Œ๊ณ ๋ฆฌ์ฆ˜์— ์˜ํ•ด ์ž๋™ ์ƒ์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.* +# ๐Ÿ“Š ์‹œ์Šคํ…œ ์šด์˜ ์ž์‚ฐ ๊ฐ€์น˜ ๋ถ„์„ ๋ณด๊ณ  (Sabermetrics Report) + +๋ณธ ๋ณด๊ณ ์„œ๋Š” ํ”„๋กœ์ ํŠธ ๊ด€๋ฆฌ ์‹œ์Šคํ…œ ๋‚ด์—์„œ ์ˆ˜์ง‘๋œ ํ™œ๋™ ๋กœ๊ทธ ๋ฐ ์ž์‚ฐ ๋ฐ์ดํ„ฐ๋ฅผ ํ†ต๊ณ„์ /AI ๊ธฐ๋ฒ•์œผ๋กœ ๋ถ„์„ํ•˜์—ฌ, ๊ฐ ํ”„๋กœ์ ํŠธ์˜ ์šด์˜ ํ™œ๋ ฅ๊ณผ ์กฐ์ง ๊ธฐ์—ฌ๋„๋ฅผ ์ •๋Ÿ‰ํ™”ํ•œ ์ง€ํ‘œ๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. + +--- + +## 1. ์šด์˜ ํ™œ๋ ฅ ์ง€์ˆ˜ (AVI, Activity Vitality Index) +ํ”„๋กœ์ ํŠธ๊ฐ€ ํ˜„์žฌ ์–ผ๋งˆ๋‚˜ ๊ฑด๊ฐ•ํ•˜๊ฒŒ ๊ฐ€๋™๋˜๊ณ  ์žˆ๋Š”์ง€๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” **'๋””์ง€ํ„ธ ์ž์‚ฐ ์ƒ์กด ์ง€ํ‘œ'**์ž…๋‹ˆ๋‹ค. + +### 1.1 ์‚ฐ์ถœ ๊ณต์‹ +$$AVI = e^{-\lambda \times Stagnant\_Days} \times Quality \times 100$$ + +### 1.2 3๋Œ€ ํ•ต์‹ฌ ๋ณ€์ˆ˜ ์ƒ์„ธ ์„ค๋ช… + +#### โ‘  ์ง€์ˆ˜ ๊ฐ์‡„ ๋ชจ๋ธ ($e^{-\lambda \times t}$) : "๊ฐ€์น˜์˜ ์‹œํ•œํญํƒ„" +์ž์‚ฐ์€ ๊ด€๋ฆฌํ•˜์ง€ ์•Š์œผ๋ฉด ์‹œ๊ฐ„์ด ํ๋ฅผ์ˆ˜๋ก ๊ฐ€์น˜๊ฐ€ ๊ธฐํ•˜๊ธ‰์ˆ˜์ ์œผ๋กœ ์†Œ๋ฉธํ•œ๋‹ค๋Š” **'์ •๋ณด ํœ˜๋ฐœ์„ฑ'** ์›๋ฆฌ๋ฅผ ๋ฐ˜์˜ํ•ฉ๋‹ˆ๋‹ค. +* **Stagnant Days (์ •์ฒด ์ผ์ˆ˜)**: ๋งˆ์ง€๋ง‰ ์œ ํšจ ํ™œ๋™ ๋กœ๊ทธ ๊ธฐ๋ก์ผ๋กœ๋ถ€ํ„ฐ ์˜ค๋Š˜๊นŒ์ง€ ๊ฒฝ๊ณผ๋œ ๋‚ ์งœ์ž…๋‹ˆ๋‹ค. +* **ํŠน์ง•**: ์ •์ฒด ์ดˆ๊ธฐ์—๋Š” ์ ์ˆ˜๊ฐ€ ๋น ๋ฅด๊ฒŒ ํ•˜๋ฝํ•˜๋‹ค๊ฐ€, ์‹œ๊ฐ„์ด ์ง€๋‚ ์ˆ˜๋ก ํ•˜๋ฝ ํญ์ด ๋‘”ํ™”๋˜๋ฉฐ 0์— ์ˆ˜๋ ดํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” ๊ด€๋ฆฌ๊ฐ€ ์ค‘๋‹จ๋œ ์งํ›„์˜ ์ •๋ณด ๋ง์‹ค ์œ„ํ—˜์ด ๊ฐ€์žฅ ํฌ๋‹ค๋Š” ์‹ค๋ฌด์  ๊ฒฝํ—˜์„ ๋ฐ˜์˜ํ•œ ๊ฒƒ์ž…๋‹ˆ๋‹ค. + +#### โ‘ก ์œ„ํ—˜ ๊ฐ€์† ๊ณ„์ˆ˜ ($\lambda$) : "๋Œ€ํ˜• ์ž์‚ฐ์˜ ๋†’์€ ๊ด€๋ฆฌ ๋น„์šฉ" +๋ชจ๋“  ํ”„๋กœ์ ํŠธ๋Š” ์ž์‚ฐ ๊ทœ๋ชจ์— ๋”ฐ๋ผ '๋Š™์–ด๊ฐ€๋Š” ์†๋„'๊ฐ€ ๋‹ค๋ฆ…๋‹ˆ๋‹ค. +* **๊ณต์‹**: $\lambda = 0.04 + \log_{10}(Files + 1) \times 0.008$ +* **๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง**: ํŒŒ์ผ์ด ๋งŽ์€ ๋Œ€ํ˜• ํ”„๋กœ์ ํŠธ์ผ์ˆ˜๋ก ๊ด€๋ฆฌ ๋ถ€์žฌ ์‹œ ์กฐ์ง์— ๋ฏธ์น˜๋Š” ํƒ€๊ฒฉ์ด ํฝ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ๋Œ€ํ˜• ํ”„๋กœ์ ํŠธ์ผ์ˆ˜๋ก $\lambda$ ๊ฐ’์ด ์ปค์ง€๋ฉฐ, ์†Œํ˜• ํ”„๋กœ์ ํŠธ๋ณด๋‹ค **ํ›จ์”ฌ ๋น ๋ฅธ ์†๋„๋กœ AVI๊ฐ€ ํ•˜๋ฝ**ํ•˜๋„๋ก ์„ค๊ณ„๋˜์—ˆ์Šต๋‹ˆ๋‹ค. (๋Œ€ํ˜• ํ”„๋กœ์ ํŠธ๋Š” ๋” ์ž์ฃผ ๊ด€๋ฆฌํ•ด์•ผ ์ ์ˆ˜๊ฐ€ ์œ ์ง€๋จ) + +#### โ‘ข ํ™œ๋™ ํ’ˆ์งˆ ๊ฐ€์ค‘์น˜ ($Quality$) : "ํ–‰์ •๊ณผ ์‹ค๋ฌด์˜ ๊ตฌ๋ถ„" +๋‹จ์ˆœํžˆ ์ ‘์†ํ•˜๊ฑฐ๋‚˜ ๋กœ๊ทธ๊ฐ€ ์ฐํ˜”๋‹ค๊ณ  ํ•ด์„œ ํ™œ๋ ฅ์ด 100% ํšŒ๋ณต๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. AI๊ฐ€ ๋กœ๊ทธ ํ‚ค์›Œ๋“œ๋ฅผ ๋ถ„์„ํ•˜์—ฌ ํ™œ๋™์˜ **'์ง„์ •์„ฑ'**์„ ํ‰๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. +* **High (1.0)**: **์„ฑ๊ณผ๋ฌผ ์ค‘์‹ฌ ํ™œ๋™** (ํŒŒ์ผ ์—…๋กœ๋“œ, ์ˆ˜์ •, ๋“ฑ๋ก, ์—…๋ฐ์ดํŠธ ๋“ฑ) +* **Medium (0.7)**: **๊ตฌ์กฐ์  ์œ ์ง€ ํ™œ๋™** (ํด๋” ์ƒ์„ฑ, ์‚ญ์ œ, ์ด๋™ ๋“ฑ) +* **Low (0.4)**: **๋‹จ์ˆœ ํ–‰์ • ํ™œ๋™** (๊ถŒํ•œ ๋ณ€๊ฒฝ, ๋ฉ”์ผ ํ™•์ธ, ์ฐธ๊ฐ€์ž ์ถ”๊ฐ€ ๋“ฑ) + +--- + +## 2. ์ž์‚ฐ ๊ฐ€์น˜ ๊ธฐ์—ฌ๋„ (VCI, Value Contribution Index) +์•ผ๊ตฌ์˜ **WAR(Wins Above Replacement)** ๊ฐœ๋…์„ ๋„์ž…ํ•˜์—ฌ, ์ „์ฒด ํฌํŠธํด๋ฆฌ์˜ค ํ‰๊ท  ๋Œ€๋น„ ๊ฐœ๋ณ„ ํ”„๋กœ์ ํŠธ๊ฐ€ ์กฐ์ง ๊ฐ€์น˜์— ์–ผ๋งˆ๋‚˜ ๊ธฐ์—ฌํ•˜๋Š”์ง€ ์‚ฐ์ถœํ•ฉ๋‹ˆ๋‹ค. + +### 2.1 ์‚ฐ์ถœ ๊ณต์‹ +$$VCI = (Individual\_AVI - Portfolio\_Avg\_AVI) \times Asset\_Weight$$ +* **Asset Weight (ํŒŒ์ผ ๊ทœ๋ชจ ๊ฐ€์ค‘์น˜)**: $max(0.2, \frac{Individual\_Files}{Portfolio\_Avg\_Files})$ + +### 2.2 ์ง€ํ‘œ์˜ ์˜๋ฏธ: "ํ‰๊ท (0.0)์„ ๊ธฐ์ค€์œผ๋กœ ํ•œ ์ƒ๋Œ€ ํ‰๊ฐ€" +* **0.0 (ํ‰๊ท )**: ์กฐ์ง ๋‚ด ํ‰๊ท ์ ์ธ ๊ด€๋ฆฌ ์ˆ˜์ค€๊ณผ ๊ทœ๋ชจ๋ฅผ ๊ฐ€์ง„ ํ‘œ์ค€ ํ”„๋กœ์ ํŠธ. +* **(+) ์ ์ˆ˜**: ํ‰๊ท  ์ด์ƒ์˜ ํ™œ๋ ฅ์œผ๋กœ ์กฐ์ง์˜ ๋””์ง€ํ„ธ ์ž์‚ฐ ๊ฐ€์น˜๋ฅผ ์ฆ๋Œ€์‹œํ‚ค๋Š” ํ”„๋กœ์ ํŠธ. +* **(-) ์ ์ˆ˜**: ํ‰๊ท  ์ดํ•˜์˜ ๋ฐฉ์น˜๋กœ ์ธํ•ด ์กฐ์ง์— ์ž ์žฌ์  ๊ธฐํšŒ๋น„์šฉ ์†์‹ค์„ ์ž…ํžˆ๋Š” ๋ฆฌ์Šคํฌ ํ”„๋กœ์ ํŠธ. +* **์ƒ๋Œ€ ๊ฐ€์ค‘์น˜**: ์กฐ์ง์˜ ํ‰๊ท  ํŒŒ์ผ ์ˆ˜๋ณด๋‹ค ํฐ ํ”„๋กœ์ ํŠธ๊ฐ€ ๋ฐฉ์น˜๋  ๋•Œ ๋งˆ์ด๋„ˆ์Šค ์ ์ˆ˜๊ฐ€ ๋” ๊ฐ€ํŒŒ๋ฅด๊ฒŒ ํ•˜๋ฝํ•˜์—ฌ **'์šฐ์„  ๊ด€๋ฆฌ ๋Œ€์ƒ'**์„ ๋ช…ํ™•ํžˆ ์‹๋ณ„ํ•ฉ๋‹ˆ๋‹ค. + +--- + +## 3. ๊ฐ•๋ ฅํ•œ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ (Hard Rules) + +๋ฐ์ดํ„ฐ์˜ ์‹ ๋ขฐ๋„๋ฅผ ํ™•๋ณดํ•˜๊ธฐ ์œ„ํ•ด ๋‹ค์Œ๊ณผ ๊ฐ™์€ **'์‚ฌ๋ง ํŒ์ •'** ๊ทœ์น™์ด ์ ์šฉ๋ฉ๋‹ˆ๋‹ค. +1. **์ž๋™ ์‚ญ์ œ ํŒจ๋„ํ‹ฐ**: ์ตœ๊ทผ ๋กœ๊ทธ๊ฐ€ ์‹œ์Šคํ…œ์— ์˜ํ•œ 'ํด๋”์ž๋™์‚ญ์ œ'์ธ ๊ฒฝ์šฐ, AVI๋Š” ์ฆ‰์‹œ **0.1%**๋กœ ๊ณ ์ •๋ฉ๋‹ˆ๋‹ค. (๊ด€๋ฆฌ ํฌ๊ธฐ ์ƒํƒœ) +2. **์ž์‚ฐ ๋ถ€์žฌ ํŒจ๋„ํ‹ฐ (ECV)**: ํŒŒ์ผ ๊ฐœ์ˆ˜๊ฐ€ 0๊ฐœ์ธ ๊ฒฝ์šฐ ์šด์˜ ์ผ๊ด€์„ฑ(OCI)์€ **0.0์ **์ด๋ฉฐ, ํŒŒ์ผ 10๊ฐœ ๋ฏธ๋งŒ์€ ์ตœ์ข… ๊ฐ€์ค‘์น˜์— **50% ํŒจ๋„ํ‹ฐ**๋ฅผ ์ ์šฉํ•˜์—ฌ '๊ป๋ฐ๊ธฐ ํ”„๋กœ์ ํŠธ'๋ฅผ ๊ฑธ๋Ÿฌ๋ƒ…๋‹ˆ๋‹ค. + +--- + +## 4. ๋ฐœํ‘œ ๋ฐ ๋ถ„์„ ๊ฐ€์ด๋“œ (Executive Summary) + +* **AVI๊ฐ€ ๋‚ฎ์€ ํ”„๋กœ์ ํŠธ**: "๋ฐ์ดํ„ฐ๊ฐ€ ๋‚ก์•„๊ฐ€๊ณ  ์žˆ์œผ๋‹ˆ ์ฆ‰์‹œ ์ตœ์‹  ์„ฑ๊ณผ๋ฌผ์„ ์—…๋ฐ์ดํŠธํ•˜์‹ญ์‹œ์˜ค." +* **VCI๊ฐ€ ์Œ์ˆ˜(-)์ธ ๋Œ€ํ˜• ํ”„๋กœ์ ํŠธ**: "์กฐ์ง์—์„œ ๊ฐ€์žฅ ์ค‘์š”ํ•œ ์ž์‚ฐ์ž„์—๋„ ๋ถˆ๊ตฌํ•˜๊ณ  ํ‰๊ท  ์ดํ•˜๋กœ ๋ฐฉ์น˜๋˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. **์ตœ์šฐ์„  ๊ด€๋ฆฌ ๋Œ€์ƒ**์ž…๋‹ˆ๋‹ค." +* **OCI๊ฐ€ ๋‚ฎ์€ ํ”„๋กœ์ ํŠธ**: "ํ™œ๋™์€ ์žˆ์œผ๋‚˜ ๋ถˆ๊ทœ์น™ํ•ฉ๋‹ˆ๋‹ค. ๊ด€๋ฆฌ์˜ ์ง€์†์„ฑ์„ ํ™•๋ณดํ•˜์—ฌ ์šด์˜ ๋ฆฌ๋“ฌ์„ ์ฐพ์œผ์‹ญ์‹œ์˜ค." diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..994f8f5 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,40 @@ +# 1. ๋ฒ ์ด์Šค ์ด๋ฏธ์ง€ ์„ค์ • (์•ˆ์ •์ ์ธ bookworm ๋ฒ„์ „ ์‚ฌ์šฉ) +FROM python:3.9-slim-bookworm + +# 2. ์‹œ์Šคํ…œ ์˜์กด์„ฑ ์„ค์น˜ (OCR, PDF, Playwright ๊ด€๋ จ ํ•ต์‹ฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ) +RUN apt-get update && apt-get install -y \ + tesseract-ocr \ + libtesseract-dev \ + poppler-utils \ + libgl1 \ + libnss3 \ + libnspr4 \ + libatk1.0-0 \ + libatk-bridge2.0-0 \ + libcups2 \ + libdrm2 \ + libxkbcommon0 \ + libxcomposite1 \ + libxdamage1 \ + libxrandr2 \ + libgbm1 \ + libpango-1.0-0 \ + libcairo2 \ + libasound2 \ + fonts-liberation \ + && rm -rf /var/lib/apt/lists/* + +# 3. ์ž‘์—… ๋””๋ ‰ํ† ๋ฆฌ ์„ค์ • +WORKDIR /app + +# 4. ํ•„์š”ํ•œ ํŒจํ‚ค์ง€ ์„ค์น˜ +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt +# ๋ธŒ๋ผ์šฐ์ €๋งŒ ์„ค์น˜ (์˜์กด์„ฑ์€ ์œ„์—์„œ ์„ค์น˜ํ•จ) +RUN playwright install chromium + +# 5. ํ”„๋กœ์ ํŠธ ์ „์ฒด ํŒŒ์ผ ๋ณต์‚ฌ +COPY . . + +# 6. ์„œ๋ฒ„ ๊ตฌ๋™ +CMD ["uvicorn", "server:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/PLAN.md b/PLAN.md index 79dc477..2fa84fa 100644 --- a/PLAN.md +++ b/PLAN.md @@ -1,34 +1,34 @@ -# ๋ฐ์ดํ„ฐ ๋ถ„์„ ๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€ ๊ธฐํš์•ˆ - -## 1. ํ”„๋กœ์ ํŠธ ๊ฐœ์š” -๋ณธ ํ”„๋กœ์ ํŠธ๋Š” ๋ฐ์ดํ„ฐ ๋ถ„์„ ํ”„๋กœ์„ธ์Šค ๋ฐ ํ”„๋กœ์ ํŠธ ๋ฆฌ์†Œ์Šค๋ฅผ ํ†ตํ•ฉ ๊ด€๋ฆฌํ•˜๊ธฐ ์œ„ํ•œ ๊ด€๋ฆฌ์ž ๋Œ€์‹œ๋ณด๋“œ์ž…๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž ์ธํ„ฐ๋ž™์…˜ ๊ด€๋ฆฌ๋ถ€ํ„ฐ ์‹œ์Šคํ…œ ๋กœ๊ทธ, ๋ฆฌ์†Œ์Šค ํ˜„ํ™ฉ์„ ํ•œ๋ˆˆ์— ํŒŒ์•…ํ•˜๋Š” ๊ฒƒ์„ ๋ชฉํ‘œ๋กœ ํ•ฉ๋‹ˆ๋‹ค. - -## 2. ์ฃผ์š” ๊ธฐ๋Šฅ ์ƒ์„ธ - -### โ‘  ๋ฉ”์ผ ๊ด€๋ฆฌ ๋ฐ ์š”๊ตฌ์‚ฌํ•ญ ์‹œ์Šคํ…œ (Mail & Inquiry Management) - [์™„๋ฃŒ] -- **UI/UX ๊ณ ๋„ํ™”**: ๋ฆฌ์ŠคํŠธ ์˜์—ญ ๋„ˆ๋น„ ํ™•์žฅ(400px) ๋ฐ ์‹œ๊ฐ์  ๊ฐ€๋…์„ฑ ๊ฐœ์„  -- **๊ฒ€์ƒ‰ ๋ฐ ํ•„ํ„ฐ๋ง**: ํ‚ค์›Œ๋“œ ๋ฐ ๊ธฐ๊ฐ„๋ณ„ ๋ฉ”์ผ ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ ๊ตฌํ˜„ -- **๋™์  ์—ฐ๋™**: ๋ฆฌ์ŠคํŠธ ํด๋ฆญ ์‹œ ๋ฉ”์ผ ๋ณธ๋ฌธ ์‹ค์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ ๊ตฌํ˜„ -- **๋ฉ”์ผ ๊ด€๋ฆฌ**: ๊ฐœ๋ณ„ ์‚ญ์ œ ๋ฐ ์ฒดํฌ๋ฐ•์Šค๋ฅผ ํ™œ์šฉํ•œ ๋Œ€๋Ÿ‰ ์‚ญ์ œ ๊ธฐ๋Šฅ ์ถ”๊ฐ€ -- **ํƒญ ์‹œ์Šคํ…œ**: ์ˆ˜์‹ /๋ฐœ์‹ /์ž„์‹œ/ํœด์ง€ํ†ต๋ณ„ ๋ฐ์ดํ„ฐ ๋ถ„๋ฅ˜ ๋ฐ ๋™์  ๋ Œ๋”๋ง ์ ์šฉ - -### โ‘ก ๋กœ๊ทธ ๊ด€๋ฆฌ (Log Management) -- **์ตœ๊ทผ ๋กœ๊ทธ**: ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋ฐœ์ƒํ•˜๋Š” ์‹œ์Šคํ…œ ๋ฐ ๋ถ„์„ ์ž‘์—… ๋กœ๊ทธ ์ถœ๋ ฅ -- **์ „์ฒด ๋กœ๊ทธ**: ๋‚ ์งœ๋ณ„, ํ”„๋กœ์ ํŠธ๋ณ„ ํ•„ํ„ฐ๋ง์„ ํ†ตํ•œ ๋กœ๊ทธ ๊ธฐ๋ก ์กฐํšŒ ๋ฐ ๋‚ด๋ณด๋‚ด๊ธฐ - -### โ‘ข ํŒŒ์ผ ๊ด€๋ฆฌ (File Management) -- ํ”„๋กœ์ ํŠธ๋ณ„ ๋ฐ์ดํ„ฐ์…‹, ๋ถ„์„ ๊ฒฐ๊ณผ๋ฌผ ํŒŒ์ผ ๊ฐœ์ˆ˜ ๋ฐ ์šฉ๋Ÿ‰ ํ†ต๊ณ„ -- ํŒŒ์ผ ํ™•์žฅ์ž๋ณ„ ๊ตฌ์„ฑ ๋น„์œจ(CSV, JSON, Python ๋“ฑ) ์‹œ๊ฐํ™” ์ง€ํ‘œ ์ œ๊ณต - -### โ‘ฃ ์ธ์› ๊ด€๋ฆฌ (Personnel Management) -- ํ”„๋กœ์ ํŠธ ์ฐธ์—ฌ ์ธ์› ํ˜„ํ™ฉ ์กฐํšŒ -- ์‚ฌ์šฉ์ž๋ณ„ ๊ถŒํ•œ(๊ด€๋ฆฌ์ž, ๋ถ„์„๊ฐ€, ๋ทฐ์–ด) ๋ถ€์—ฌ ๋ฐ ์ˆ˜์ • ๊ธฐ๋Šฅ - -### โ‘ข ๊ณต์ง€์‚ฌํ•ญ (Notice & Patch Notes) -- ๋ถ„์„ ๋ชจ๋ธ ์—…๋ฐ์ดํŠธ, ์‹œ์Šคํ…œ ์ ๊ฒ€, ํŒจ์น˜ ๋‚ด์—ญ ๊ณต์œ  -- ์‚ฌ์šฉ์ž ๋Œ€์ƒ ๊ณต์ง€์‚ฌํ•ญ ์ž‘์„ฑ ๋ฐ ๊ฒŒ์‹œํŒ ๊ด€๋ฆฌ - -## 3. UI/UX ๊ฐ€์ด๋“œ๋ผ์ธ -- **Layout**: ์ขŒ์ธก ๋‚ด๋น„๊ฒŒ์ด์…˜ ๋ฐ”(Sidebar) + ์ƒ๋‹จ ํ—ค๋”(Header) + ์ค‘์•™ ์ปจํ…์ธ  ์˜์—ญ -- **Theme**: ์‹ ๋ขฐ๊ฐ์„ ์ฃผ๋Š” Dark Blue / White ํ†ค์˜ ๊นจ๋—ํ•œ ๋””์ž์ธ -- **Responsiveness**: ๋‹ค์–‘ํ•œ ํ•ด์ƒ๋„์— ๋Œ€์‘ํ•˜๋Š” ๋ฐ˜์‘ํ˜• ๋ ˆ์ด์•„์›ƒ ๊ตฌ์„ฑ +# ๋ฐ์ดํ„ฐ ๋ถ„์„ ๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€ ๊ธฐํš์•ˆ + +## 1. ํ”„๋กœ์ ํŠธ ๊ฐœ์š” +๋ณธ ํ”„๋กœ์ ํŠธ๋Š” ๋ฐ์ดํ„ฐ ๋ถ„์„ ํ”„๋กœ์„ธ์Šค ๋ฐ ํ”„๋กœ์ ํŠธ ๋ฆฌ์†Œ์Šค๋ฅผ ํ†ตํ•ฉ ๊ด€๋ฆฌํ•˜๊ธฐ ์œ„ํ•œ ๊ด€๋ฆฌ์ž ๋Œ€์‹œ๋ณด๋“œ์ž…๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž ์ธํ„ฐ๋ž™์…˜ ๊ด€๋ฆฌ๋ถ€ํ„ฐ ์‹œ์Šคํ…œ ๋กœ๊ทธ, ๋ฆฌ์†Œ์Šค ํ˜„ํ™ฉ์„ ํ•œ๋ˆˆ์— ํŒŒ์•…ํ•˜๋Š” ๊ฒƒ์„ ๋ชฉํ‘œ๋กœ ํ•ฉ๋‹ˆ๋‹ค. + +## 2. ์ฃผ์š” ๊ธฐ๋Šฅ ์ƒ์„ธ + +### โ‘  ๋ฉ”์ผ ๊ด€๋ฆฌ ๋ฐ ์š”๊ตฌ์‚ฌํ•ญ ์‹œ์Šคํ…œ (Mail & Inquiry Management) - [์™„๋ฃŒ] +- **UI/UX ๊ณ ๋„ํ™”**: ๋ฆฌ์ŠคํŠธ ์˜์—ญ ๋„ˆ๋น„ ํ™•์žฅ(400px) ๋ฐ ์‹œ๊ฐ์  ๊ฐ€๋…์„ฑ ๊ฐœ์„  +- **๊ฒ€์ƒ‰ ๋ฐ ํ•„ํ„ฐ๋ง**: ํ‚ค์›Œ๋“œ ๋ฐ ๊ธฐ๊ฐ„๋ณ„ ๋ฉ”์ผ ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ ๊ตฌํ˜„ +- **๋™์  ์—ฐ๋™**: ๋ฆฌ์ŠคํŠธ ํด๋ฆญ ์‹œ ๋ฉ”์ผ ๋ณธ๋ฌธ ์‹ค์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ ๊ตฌํ˜„ +- **๋ฉ”์ผ ๊ด€๋ฆฌ**: ๊ฐœ๋ณ„ ์‚ญ์ œ ๋ฐ ์ฒดํฌ๋ฐ•์Šค๋ฅผ ํ™œ์šฉํ•œ ๋Œ€๋Ÿ‰ ์‚ญ์ œ ๊ธฐ๋Šฅ ์ถ”๊ฐ€ +- **ํƒญ ์‹œ์Šคํ…œ**: ์ˆ˜์‹ /๋ฐœ์‹ /์ž„์‹œ/ํœด์ง€ํ†ต๋ณ„ ๋ฐ์ดํ„ฐ ๋ถ„๋ฅ˜ ๋ฐ ๋™์  ๋ Œ๋”๋ง ์ ์šฉ + +### โ‘ก ๋กœ๊ทธ ๊ด€๋ฆฌ (Log Management) +- **์ตœ๊ทผ ๋กœ๊ทธ**: ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋ฐœ์ƒํ•˜๋Š” ์‹œ์Šคํ…œ ๋ฐ ๋ถ„์„ ์ž‘์—… ๋กœ๊ทธ ์ถœ๋ ฅ +- **์ „์ฒด ๋กœ๊ทธ**: ๋‚ ์งœ๋ณ„, ํ”„๋กœ์ ํŠธ๋ณ„ ํ•„ํ„ฐ๋ง์„ ํ†ตํ•œ ๋กœ๊ทธ ๊ธฐ๋ก ์กฐํšŒ ๋ฐ ๋‚ด๋ณด๋‚ด๊ธฐ + +### โ‘ข ํŒŒ์ผ ๊ด€๋ฆฌ (File Management) +- ํ”„๋กœ์ ํŠธ๋ณ„ ๋ฐ์ดํ„ฐ์…‹, ๋ถ„์„ ๊ฒฐ๊ณผ๋ฌผ ํŒŒ์ผ ๊ฐœ์ˆ˜ ๋ฐ ์šฉ๋Ÿ‰ ํ†ต๊ณ„ +- ํŒŒ์ผ ํ™•์žฅ์ž๋ณ„ ๊ตฌ์„ฑ ๋น„์œจ(CSV, JSON, Python ๋“ฑ) ์‹œ๊ฐํ™” ์ง€ํ‘œ ์ œ๊ณต + +### โ‘ฃ ์ธ์› ๊ด€๋ฆฌ (Personnel Management) +- ํ”„๋กœ์ ํŠธ ์ฐธ์—ฌ ์ธ์› ํ˜„ํ™ฉ ์กฐํšŒ +- ์‚ฌ์šฉ์ž๋ณ„ ๊ถŒํ•œ(๊ด€๋ฆฌ์ž, ๋ถ„์„๊ฐ€, ๋ทฐ์–ด) ๋ถ€์—ฌ ๋ฐ ์ˆ˜์ • ๊ธฐ๋Šฅ + +### โ‘ข ๊ณต์ง€์‚ฌํ•ญ (Notice & Patch Notes) +- ๋ถ„์„ ๋ชจ๋ธ ์—…๋ฐ์ดํŠธ, ์‹œ์Šคํ…œ ์ ๊ฒ€, ํŒจ์น˜ ๋‚ด์—ญ ๊ณต์œ  +- ์‚ฌ์šฉ์ž ๋Œ€์ƒ ๊ณต์ง€์‚ฌํ•ญ ์ž‘์„ฑ ๋ฐ ๊ฒŒ์‹œํŒ ๊ด€๋ฆฌ + +## 3. UI/UX ๊ฐ€์ด๋“œ๋ผ์ธ +- **Layout**: ์ขŒ์ธก ๋‚ด๋น„๊ฒŒ์ด์…˜ ๋ฐ”(Sidebar) + ์ƒ๋‹จ ํ—ค๋”(Header) + ์ค‘์•™ ์ปจํ…์ธ  ์˜์—ญ +- **Theme**: ์‹ ๋ขฐ๊ฐ์„ ์ฃผ๋Š” Dark Blue / White ํ†ค์˜ ๊นจ๋—ํ•œ ๋””์ž์ธ +- **Responsiveness**: ๋‹ค์–‘ํ•œ ํ•ด์ƒ๋„์— ๋Œ€์‘ํ•˜๋Š” ๋ฐ˜์‘ํ˜• ๋ ˆ์ด์•„์›ƒ ๊ตฌ์„ฑ diff --git a/README.md b/README.md index afa860c..bf6f40b 100644 --- a/README.md +++ b/README.md @@ -1,86 +1,86 @@ -# ๐Ÿš€ ์„œ๋ฒ„ ์ •์ฑ… (Server Policy) - -**์„œ๋ฒ„ ๊ตฌ๋™ ์‹œ ๋ฐ˜๋“œ์‹œ ์•„๋ž˜ ๋ช…๋ น์–ด๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค:** -```bash -uvicorn server:app --host 0.0.0.0 --port 8000 --reload -``` -- **Host**: `0.0.0.0` (์™ธ๋ถ€ ์ ‘์† ํ—ˆ์šฉ) -- **Port**: `8000` -- **Reload**: ์ฝ”๋“œ ์ˆ˜์ • ์‹œ ์ž๋™ ์žฌ์‹œ์ž‘ ํ™œ์„ฑํ™” - ---- - -# ๐Ÿค– ๋ฉ”์ผ์‹œ์Šคํ…œ AIํŒ๋‹จ๊ฐ€์ด๋“œ (AI Reasoning Guide) - -AI๋Š” ํŒŒ์ผ์„ ๋ถ„๋ฅ˜ํ•  ๋•Œ ๋‹จ์ˆœํ•œ ํ‚ค์›Œ๋“œ ๋งค์นญ์ด ์•„๋‹Œ, ์•„๋ž˜์˜ **5๋‹จ๊ณ„ ํ†ตํ•ฉ ์ถ”๋ก  ๋ชจ๋ธ**์„ ์‚ฌ์šฉํ•˜์—ฌ '์‹ค๋ฌด์ž์ฒ˜๋Ÿผ' ์ƒ๊ฐํ•˜๊ณ  ํŒ๋‹จํ•œ๋‹ค. - -### 1๋‹จ๊ณ„: ์ „์ˆ˜ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ (Holistic Reading) -- **๋ฌด์ œํ•œ ์Šค์บ”**: ํŽ˜์ด์ง€ ์ˆ˜์— ๊ด€๊ณ„์—†์ด ๋ฌธ์„œ ์ „์ฒด๋ฅผ ์ „์ˆ˜ ์กฐ์‚ฌํ•œ๋‹ค. -- **๋ฌด์กฐ๊ฑด์  OCR**: ๋””์ง€ํ„ธ ํ…์ŠคํŠธ ์œ ๋ฌด์™€ ์ƒ๊ด€์—†์ด ๋ชจ๋“  ํŽ˜์ด์ง€์— ๊ณ ํ•ด์ƒ๋„(300 DPI) OCR์„ ์‹คํ–‰ํ•˜์—ฌ ์ด๋ฏธ์ง€ ์† ๋„์žฅ, ์ˆ˜๊ธฐ, ํ‘œ ๋ฐ์ดํ„ฐ๊นŒ์ง€ ์™„๋ฒฝํžˆ ์ˆ˜์ง‘ํ•œ๋‹ค. - -### 2๋‹จ๊ณ„: ํŒŒ์ผ๋ช… ๊ฐ€์ค‘์น˜ ์ ์šฉ (Title Steering) -- **ํŒŒ์ผ๋ช… = ๋ณด๊ด€ ์˜๋„**: ์‚ฌ์šฉ์ž๊ฐ€ ์ง€์€ ํŒŒ์ผ๋ช…์€ ๋ถ„๋ฅ˜์˜ ๊ฐ€์žฅ ๊ฐ•๋ ฅํ•œ '๋ฐฉํ–ฅํƒ€'์ด๋‹ค. -- **์ตœ์ข… ์กฐ์œจ**: ๋ณธ๋ฌธ์˜ ๋ฐ์ดํ„ฐ๊ฐ€ ๋‹ค๋ฅธ ๋„๋ฉ”์ธ์— ์ ๋ ค ์žˆ๋”๋ผ๋„, ํŒŒ์ผ๋ช…์— ๋ช…ํ™•ํ•œ ์—…๋ฌด ์šฉ์–ด(`์‹ค์ •๋ณด๊ณ `, `ํ•˜๋„๊ธ‰` ๋“ฑ)๊ฐ€ ์žˆ๋‹ค๋ฉด ์ด๋ฅผ ์ตœ์ข… ๋ถ„๋ฅ˜์˜ ๊ฐ€์žฅ ํฐ ๋ฌด๊ฒŒ์ถ”๋กœ ์‚ผ๋Š”๋‹ค. - -### 3๋‹จ๊ณ„: ๋ฌธ์„œ์˜ ๋ฌผ๋ฆฌ์  ํ‹€(Format) ๋ถ„์„ -- **๊ณต๋ฌธ ๊ณจ๊ฒฉ ํ™•์ธ**: ๋ฌธ์„œ์˜ ์‹œ์ž‘(`์ˆ˜์‹ /๋ฐœ์‹ `)๊ณผ ๋(`์ง์ธ/๋.`)์˜ ๊ตฌ์กฐ๋ฅผ ํ™•์ธํ•œ๋‹ค. -- **๊ป๋ฐ๊ธฐ vs ์•Œ๋งน์ด**: - - **๊ณต๋ฌธ ๋ณธ์ฒด**: ๊ณจ๊ฒฉ์ด ์™„๋ฒฝํ•˜๊ณ  ๋’ค๋”ฐ๋ฅด๋Š” ๊ธฐ์ˆ  ๋ฐ์ดํ„ฐ๊ฐ€ ์ ์€ ๊ฒฝ์šฐ โ†’ **[๊ณต์‚ฌ๊ด€๋ฆฌ > ๊ณต๋ฌธ]** - - **์ฒจ๋ถ€ ๋ณธ์ฒด**: ๊ณต๋ฌธ ๋’ค์— ๋Œ€๋Ÿ‰์˜ ์‚ฐ์ถœ์„œ, ๊ณ„์•ฝ์„œ, ๋„๋ฉด์ด ๋ถ™์–ด ์žˆ๋Š” ๊ฒฝ์šฐ โ†’ **[ํ•ด๋‹น ๊ธฐ์ˆ  ์นดํ…Œ๊ณ ๋ฆฌ]** (๊ณต๋ฌธ์€ ์ „๋‹ฌ ์ˆ˜๋‹จ์œผ๋กœ๋งŒ ๊ฐ„์ฃผ) - -### 4๋‹จ๊ณ„: ๋น„์ฆˆ๋‹ˆ์Šค ๋„๋ฉ”์ธ ์ƒ์‹ ๊ฒฐํ•ฉ (Common Sense) -- **์ง€๋ช… ๊ต์ฐจ ๊ฒ€์ฆ**: ํŒŒ์ผ๋ช…๊ณผ ๋ณธ๋ฌธ์˜ ์ง€๋ช…(์–ด์ฒœ, ๊ณต์ฃผ, ๋Œ€์ˆ , ์ •์•ˆ ๋“ฑ)์„ ๋Œ€์กฐํ•˜์—ฌ ์ •ํ™•ํ•œ ํ”„๋กœ์ ํŠธ๋ฅผ ์„ ํƒํ•œ๋‹ค. (์ž„์˜ ๊ธฐ๋ณธ๊ฐ’ ์ง€์ • ๊ธˆ์ง€) -- **์‹ค๋ฌด ๋งฅ๋ฝ ๋งค์นญ**: '์ž„๋Œ€๋ฃŒ/์—ฐ์žฅ'์€ ์‚ฌ์—…๋น„ ์„ฑ๊ฒฉ์˜ '๊ธฐํƒ€'๋กœ, '๋น„๊ณ„'๋Š” '๊ตฌ์กฐ๋ฌผ'๋กœ ์—ฐ๊ฒฐํ•˜๋Š” ๋“ฑ ๊ฑด์„ค ์‹ค๋ฌด ์ƒ์‹์„ ์ถ”๋ก ์— ๋ฐ˜์˜ํ•œ๋‹ค. - -### 5๋‹จ๊ณ„: ์ตœ์ข… ์ง€๋„ ๋งค์นญ (Hierarchy Mapping) -- ์ˆ˜์ง‘๋œ ๋ชจ๋“  ์ •๋ณด๋ฅผ ์ข…ํ•ฉํ•˜์—ฌ ์‚ฌ์šฉ์ž๊ฐ€ ์ •์˜ํ•œ **ํ‘œ์ค€ ๋ถ„๋ฅ˜ ์ฒด๊ณ„(Tab > Category > Sub)** ์ง€๋„ ์œ„์—์„œ ๊ฐ€์žฅ ๋…ผ๋ฆฌ์ ์ด๊ณ  ์‹ค๋ฌด์ ์ธ ์œ„์น˜๋ฅผ ์ตœ์ข… ํ™•์ •ํ•œ๋‹ค. - ---- - -# ๐Ÿ› ๏ธ ๊ฐœ๋ฐœ ๋ฐ ๊ด€๋ฆฌ ๊ทœ์น™ (Strict Development Rules) - -1. **์–ธ์–ด ์„ค์ •**: ์˜์–ด๋กœ ์ƒ๊ฐํ•˜๋˜, ๋ชจ๋“  ๋‹ต๋ณ€์€ **ํ•œ๊ตญ์–ด**๋กœ ์ž‘์„ฑํ•œ๋‹ค. -2. **์ž„์˜ ์ˆ˜์ • ์ ˆ๋Œ€ ๊ธˆ์ง€ (Zero-Arbitrary Change)**: - - ์‚ฌ์šฉ์ž๊ฐ€ ๋ช…์‹œ์ ์œผ๋กœ ์ง€์‹œํ•œ ๋ถ€๋ถ„ ์™ธ์—๋Š” **๋‹จ ํ•œ ์ค„์˜ ์ฝ”๋“œ๋„, ๊ทธ ์–ด๋–ค ํŒŒ์ผ๋„ ์ž„์˜๋กœ ์ˆ˜์ •, ์ •๋ฆฌ, ๋ฆฌํŒฉํ† ๋งํ•˜์ง€ ์•Š๋Š”๋‹ค.** - - ์ง€์‹œ๋ฐ›์ง€ ์•Š์€ ๋‹ค๋ฅธ ํŒŒํŠธ์˜ ์ฝ”๋“œ๋Š” ์ ˆ๋Œ€ ๊ฑด๋“œ๋ฆฌ์ง€ ์•Š์œผ๋ฉฐ, ์˜ํ–ฅ ๋ฒ”์œ„๊ฐ€ ์š”์ฒญ ๋ฒ”์œ„๋ฅผ ๋ฒ—์–ด๋‚˜์ง€ ์•Š๋„๋ก '์™ธ๊ณผ ์ˆ˜์ˆ ์‹(Surgical) ์ˆ˜์ •'์„ ์›์น™์œผ๋กœ ํ•œ๋‹ค. -3. **๊ฐœ์„  ์ž‘์—… ์ ˆ์ฐจ (Test-First Approach)**: - - ์‚ฌ์šฉ์ž๊ฐ€ ๊ฐœ์„ (Refactoring, Optimization ๋“ฑ)์„ ์ง€์‹œํ•œ ๊ฒฝ์šฐ, **์ˆ˜์ • ์ „ ํ˜„์žฌ ์‹œ์Šคํ…œ์ด ์ •์ƒ์ ์œผ๋กœ ์ž˜ ์ž‘๋™ํ•˜๋Š”์ง€ ๋จผ์ € ์ „์ˆ˜ ํ™•์ธ**ํ•œ๋‹ค. - - ๊ธฐ์กด ๋™์ž‘ ๋ฐฉ์‹๊ณผ ์„ฑ๋Šฅ์„ ๊ธฐ์ค€(Baseline)์œผ๋กœ ์‚ผ๊ณ , ์ˆ˜์ • ํ›„์—๋„ **๊ธฐ์กด์˜ ๋ชจ๋“  ๊ธฐ๋Šฅ์ด ๋ฌด๊ฒฐํ•˜๊ฒŒ ์œ ์ง€๋˜๋Š”์ง€ ๋ฐ˜๋“œ์‹œ ํ…Œ์ŠคํŠธํ•˜์—ฌ ์ž…์ฆ**ํ•œ๋‹ค. - - ๊ฒ€์ฆ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ "๋ฌด์—‡์„, ์™œ, ์–ด๋–ป๊ฒŒ" ๋ฐ”๊ฟ€์ง€ ์ƒ์„ธ ๋ณด๊ณ  ํ›„, ์‚ฌ์šฉ์ž๋กœ๋ถ€ํ„ฐ **'์ง„ํ–‰์‹œ์ผœ'** ์Šน์ธ์„ ์–ป์€ ๋’ค์—๋งŒ ์ง‘ํ–‰ํ•œ๋‹ค. -4. **์„ ๋ณด๊ณ  ํ›„์Šน์ธ**: ๋ชจ๋“  ๊ธฐ๋Šฅ ์ˆ˜์ • ๋ฐ ์ฝ”๋“œ ๋ณ€๊ฒฝ ์ „์—๋Š” ์˜ˆ์ƒ ๋ฐฉ์•ˆ์„ ๋จผ์ € ๋ณด๊ณ ํ•˜๊ณ  ์Šน์ธ ์ ˆ์ฐจ๋ฅผ ๊ฑฐ์นœ๋‹ค. -5. **๋กœ๊ทธ ๊ธฐ๋ก ์ฒ ์ €**: ์ง„ํ–‰ ์ƒํ™ฉ(๋กœ๊ทธ์ธ, ์ˆ˜์ง‘, ์˜ค๋ฅ˜ ๋“ฑ)์„ ์‹ค์‹œ๊ฐ„ ๋กœ๊ทธ์— ์ƒ์„ธํžˆ ํ‘œ์‹œํ•˜์—ฌ ํˆฌ๋ช…์„ฑ์„ ํ™•๋ณดํ•œ๋‹ค. - ---- - -## ๐ŸŽจ ๋””์ž์ธ ๊ฐ€์ด๋“œ (Design System) - -์ด ํ”„๋กœ์ ํŠธ๋Š” `tokens.json`์— ์ •์˜๋œ ๋””์ž์ธ ์‹œ์Šคํ…œ์„ ์ค€์ˆ˜ํ•ฉ๋‹ˆ๋‹ค. - -### 1. ์ปฌ๋Ÿฌ ์‹œ์Šคํ…œ (Colors) -- **Primary**: `#1E5149` (primary-lv-6) - ๋ธŒ๋žœ๋“œ ํ•ต์‹ฌ ์ปฌ๋Ÿฌ -- **Background**: `#FFFFFF` (Light Default) / `#F9FAFB` (Light Muted) -- **Point Colors**: - - Blue: `#0D8DF2` (Info) - - Green: `#4DB251` (Success) - - Red: `#F21D0D` (Error) - - Yellow: `#FFBF00` (Warning) -- **Special**: `ai_color` (Purple-Blue Gradient) - AI ๊ด€๋ จ ์š”์†Œ ์ „์šฉ - -### 2. ํƒ€์ดํฌ๊ทธ๋ž˜ํ”ผ (Typography) -- **Font Family**: `Pretendard`, `sans-serif` -- **Scale**: - - **H1**: 20px / ExtraBold (pretendard-0) - - **H2**: 16px / SemiBold (pretendard-1) - - **H3/H4**: 14px / SemiBold or Regular - - **Body/P**: 12px / Regular (pretendard-2) - -### 3. ๋ ˆ์ด์•„์›ƒ ๋ฐ ๊ฐ„๊ฒฉ (Dimensions) -- **Spacing Unit**: Base 4px (xs: 4px, sm: 8px, md: 16px, lg: 32px, xl: 64px) -- **Border Radius**: sm: 4px, lg: 8px, xl: 16px -- **Shadow**: `0 8px 24px rgba(0,0,0,0.16)` (box__drop-shadow) - -### 4. ์ปดํฌ๋„ŒํŠธ ๊ทœ์น™ -- **Buttons**: `borderRadius.lg (8px)` ์ ์šฉ, Primary ๋ฐฐ๊ฒฝ์ƒ‰ ์‚ฌ์šฉ -- **Cards**: `borderRadius.lg (8px)` ์ ์šฉ, Subtle Shadow ํ™œ์šฉ -- **Topbar**: Height 36px, `headercolor` ๊ทธ๋ผ๋ฐ์ด์…˜ ์ ์šฉ ๊ฐ€๋Šฅ - +# ๐Ÿš€ ์„œ๋ฒ„ ์ •์ฑ… (Server Policy) + +**์„œ๋ฒ„ ๊ตฌ๋™ ์‹œ ๋ฐ˜๋“œ์‹œ ์•„๋ž˜ ๋ช…๋ น์–ด๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค:** +```bash +uvicorn server:app --host 0.0.0.0 --port 8000 --reload +``` +- **Host**: `0.0.0.0` (์™ธ๋ถ€ ์ ‘์† ํ—ˆ์šฉ) +- **Port**: `8000` +- **Reload**: ์ฝ”๋“œ ์ˆ˜์ • ์‹œ ์ž๋™ ์žฌ์‹œ์ž‘ ํ™œ์„ฑํ™” + +--- + +# ๐Ÿค– ๋ฉ”์ผ์‹œ์Šคํ…œ AIํŒ๋‹จ๊ฐ€์ด๋“œ (AI Reasoning Guide) + +AI๋Š” ํŒŒ์ผ์„ ๋ถ„๋ฅ˜ํ•  ๋•Œ ๋‹จ์ˆœํ•œ ํ‚ค์›Œ๋“œ ๋งค์นญ์ด ์•„๋‹Œ, ์•„๋ž˜์˜ **5๋‹จ๊ณ„ ํ†ตํ•ฉ ์ถ”๋ก  ๋ชจ๋ธ**์„ ์‚ฌ์šฉํ•˜์—ฌ '์‹ค๋ฌด์ž์ฒ˜๋Ÿผ' ์ƒ๊ฐํ•˜๊ณ  ํŒ๋‹จํ•œ๋‹ค. + +### 1๋‹จ๊ณ„: ์ „์ˆ˜ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ (Holistic Reading) +- **๋ฌด์ œํ•œ ์Šค์บ”**: ํŽ˜์ด์ง€ ์ˆ˜์— ๊ด€๊ณ„์—†์ด ๋ฌธ์„œ ์ „์ฒด๋ฅผ ์ „์ˆ˜ ์กฐ์‚ฌํ•œ๋‹ค. +- **๋ฌด์กฐ๊ฑด์  OCR**: ๋””์ง€ํ„ธ ํ…์ŠคํŠธ ์œ ๋ฌด์™€ ์ƒ๊ด€์—†์ด ๋ชจ๋“  ํŽ˜์ด์ง€์— ๊ณ ํ•ด์ƒ๋„(300 DPI) OCR์„ ์‹คํ–‰ํ•˜์—ฌ ์ด๋ฏธ์ง€ ์† ๋„์žฅ, ์ˆ˜๊ธฐ, ํ‘œ ๋ฐ์ดํ„ฐ๊นŒ์ง€ ์™„๋ฒฝํžˆ ์ˆ˜์ง‘ํ•œ๋‹ค. + +### 2๋‹จ๊ณ„: ํŒŒ์ผ๋ช… ๊ฐ€์ค‘์น˜ ์ ์šฉ (Title Steering) +- **ํŒŒ์ผ๋ช… = ๋ณด๊ด€ ์˜๋„**: ์‚ฌ์šฉ์ž๊ฐ€ ์ง€์€ ํŒŒ์ผ๋ช…์€ ๋ถ„๋ฅ˜์˜ ๊ฐ€์žฅ ๊ฐ•๋ ฅํ•œ '๋ฐฉํ–ฅํƒ€'์ด๋‹ค. +- **์ตœ์ข… ์กฐ์œจ**: ๋ณธ๋ฌธ์˜ ๋ฐ์ดํ„ฐ๊ฐ€ ๋‹ค๋ฅธ ๋„๋ฉ”์ธ์— ์ ๋ ค ์žˆ๋”๋ผ๋„, ํŒŒ์ผ๋ช…์— ๋ช…ํ™•ํ•œ ์—…๋ฌด ์šฉ์–ด(`์‹ค์ •๋ณด๊ณ `, `ํ•˜๋„๊ธ‰` ๋“ฑ)๊ฐ€ ์žˆ๋‹ค๋ฉด ์ด๋ฅผ ์ตœ์ข… ๋ถ„๋ฅ˜์˜ ๊ฐ€์žฅ ํฐ ๋ฌด๊ฒŒ์ถ”๋กœ ์‚ผ๋Š”๋‹ค. + +### 3๋‹จ๊ณ„: ๋ฌธ์„œ์˜ ๋ฌผ๋ฆฌ์  ํ‹€(Format) ๋ถ„์„ +- **๊ณต๋ฌธ ๊ณจ๊ฒฉ ํ™•์ธ**: ๋ฌธ์„œ์˜ ์‹œ์ž‘(`์ˆ˜์‹ /๋ฐœ์‹ `)๊ณผ ๋(`์ง์ธ/๋.`)์˜ ๊ตฌ์กฐ๋ฅผ ํ™•์ธํ•œ๋‹ค. +- **๊ป๋ฐ๊ธฐ vs ์•Œ๋งน์ด**: + - **๊ณต๋ฌธ ๋ณธ์ฒด**: ๊ณจ๊ฒฉ์ด ์™„๋ฒฝํ•˜๊ณ  ๋’ค๋”ฐ๋ฅด๋Š” ๊ธฐ์ˆ  ๋ฐ์ดํ„ฐ๊ฐ€ ์ ์€ ๊ฒฝ์šฐ โ†’ **[๊ณต์‚ฌ๊ด€๋ฆฌ > ๊ณต๋ฌธ]** + - **์ฒจ๋ถ€ ๋ณธ์ฒด**: ๊ณต๋ฌธ ๋’ค์— ๋Œ€๋Ÿ‰์˜ ์‚ฐ์ถœ์„œ, ๊ณ„์•ฝ์„œ, ๋„๋ฉด์ด ๋ถ™์–ด ์žˆ๋Š” ๊ฒฝ์šฐ โ†’ **[ํ•ด๋‹น ๊ธฐ์ˆ  ์นดํ…Œ๊ณ ๋ฆฌ]** (๊ณต๋ฌธ์€ ์ „๋‹ฌ ์ˆ˜๋‹จ์œผ๋กœ๋งŒ ๊ฐ„์ฃผ) + +### 4๋‹จ๊ณ„: ๋น„์ฆˆ๋‹ˆ์Šค ๋„๋ฉ”์ธ ์ƒ์‹ ๊ฒฐํ•ฉ (Common Sense) +- **์ง€๋ช… ๊ต์ฐจ ๊ฒ€์ฆ**: ํŒŒ์ผ๋ช…๊ณผ ๋ณธ๋ฌธ์˜ ์ง€๋ช…(์–ด์ฒœ, ๊ณต์ฃผ, ๋Œ€์ˆ , ์ •์•ˆ ๋“ฑ)์„ ๋Œ€์กฐํ•˜์—ฌ ์ •ํ™•ํ•œ ํ”„๋กœ์ ํŠธ๋ฅผ ์„ ํƒํ•œ๋‹ค. (์ž„์˜ ๊ธฐ๋ณธ๊ฐ’ ์ง€์ • ๊ธˆ์ง€) +- **์‹ค๋ฌด ๋งฅ๋ฝ ๋งค์นญ**: '์ž„๋Œ€๋ฃŒ/์—ฐ์žฅ'์€ ์‚ฌ์—…๋น„ ์„ฑ๊ฒฉ์˜ '๊ธฐํƒ€'๋กœ, '๋น„๊ณ„'๋Š” '๊ตฌ์กฐ๋ฌผ'๋กœ ์—ฐ๊ฒฐํ•˜๋Š” ๋“ฑ ๊ฑด์„ค ์‹ค๋ฌด ์ƒ์‹์„ ์ถ”๋ก ์— ๋ฐ˜์˜ํ•œ๋‹ค. + +### 5๋‹จ๊ณ„: ์ตœ์ข… ์ง€๋„ ๋งค์นญ (Hierarchy Mapping) +- ์ˆ˜์ง‘๋œ ๋ชจ๋“  ์ •๋ณด๋ฅผ ์ข…ํ•ฉํ•˜์—ฌ ์‚ฌ์šฉ์ž๊ฐ€ ์ •์˜ํ•œ **ํ‘œ์ค€ ๋ถ„๋ฅ˜ ์ฒด๊ณ„(Tab > Category > Sub)** ์ง€๋„ ์œ„์—์„œ ๊ฐ€์žฅ ๋…ผ๋ฆฌ์ ์ด๊ณ  ์‹ค๋ฌด์ ์ธ ์œ„์น˜๋ฅผ ์ตœ์ข… ํ™•์ •ํ•œ๋‹ค. + +--- + +# ๐Ÿ› ๏ธ ๊ฐœ๋ฐœ ๋ฐ ๊ด€๋ฆฌ ๊ทœ์น™ (Strict Development Rules) + +1. **์–ธ์–ด ์„ค์ •**: ์˜์–ด๋กœ ์ƒ๊ฐํ•˜๋˜, ๋ชจ๋“  ๋‹ต๋ณ€์€ **ํ•œ๊ตญ์–ด**๋กœ ์ž‘์„ฑํ•œ๋‹ค. +2. **์ž„์˜ ์ˆ˜์ • ์ ˆ๋Œ€ ๊ธˆ์ง€ (Zero-Arbitrary Change)**: + - ์‚ฌ์šฉ์ž๊ฐ€ ๋ช…์‹œ์ ์œผ๋กœ ์ง€์‹œํ•œ ๋ถ€๋ถ„ ์™ธ์—๋Š” **๋‹จ ํ•œ ์ค„์˜ ์ฝ”๋“œ๋„, ๊ทธ ์–ด๋–ค ํŒŒ์ผ๋„ ์ž„์˜๋กœ ์ˆ˜์ •, ์ •๋ฆฌ, ๋ฆฌํŒฉํ† ๋งํ•˜์ง€ ์•Š๋Š”๋‹ค.** + - ์ง€์‹œ๋ฐ›์ง€ ์•Š์€ ๋‹ค๋ฅธ ํŒŒํŠธ์˜ ์ฝ”๋“œ๋Š” ์ ˆ๋Œ€ ๊ฑด๋“œ๋ฆฌ์ง€ ์•Š์œผ๋ฉฐ, ์˜ํ–ฅ ๋ฒ”์œ„๊ฐ€ ์š”์ฒญ ๋ฒ”์œ„๋ฅผ ๋ฒ—์–ด๋‚˜์ง€ ์•Š๋„๋ก '์™ธ๊ณผ ์ˆ˜์ˆ ์‹(Surgical) ์ˆ˜์ •'์„ ์›์น™์œผ๋กœ ํ•œ๋‹ค. +3. **๊ฐœ์„  ์ž‘์—… ์ ˆ์ฐจ (Test-First Approach)**: + - ์‚ฌ์šฉ์ž๊ฐ€ ๊ฐœ์„ (Refactoring, Optimization ๋“ฑ)์„ ์ง€์‹œํ•œ ๊ฒฝ์šฐ, **์ˆ˜์ • ์ „ ํ˜„์žฌ ์‹œ์Šคํ…œ์ด ์ •์ƒ์ ์œผ๋กœ ์ž˜ ์ž‘๋™ํ•˜๋Š”์ง€ ๋จผ์ € ์ „์ˆ˜ ํ™•์ธ**ํ•œ๋‹ค. + - ๊ธฐ์กด ๋™์ž‘ ๋ฐฉ์‹๊ณผ ์„ฑ๋Šฅ์„ ๊ธฐ์ค€(Baseline)์œผ๋กœ ์‚ผ๊ณ , ์ˆ˜์ • ํ›„์—๋„ **๊ธฐ์กด์˜ ๋ชจ๋“  ๊ธฐ๋Šฅ์ด ๋ฌด๊ฒฐํ•˜๊ฒŒ ์œ ์ง€๋˜๋Š”์ง€ ๋ฐ˜๋“œ์‹œ ํ…Œ์ŠคํŠธํ•˜์—ฌ ์ž…์ฆ**ํ•œ๋‹ค. + - ๊ฒ€์ฆ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ "๋ฌด์—‡์„, ์™œ, ์–ด๋–ป๊ฒŒ" ๋ฐ”๊ฟ€์ง€ ์ƒ์„ธ ๋ณด๊ณ  ํ›„, ์‚ฌ์šฉ์ž๋กœ๋ถ€ํ„ฐ **'์ง„ํ–‰์‹œ์ผœ'** ์Šน์ธ์„ ์–ป์€ ๋’ค์—๋งŒ ์ง‘ํ–‰ํ•œ๋‹ค. +4. **์„ ๋ณด๊ณ  ํ›„์Šน์ธ**: ๋ชจ๋“  ๊ธฐ๋Šฅ ์ˆ˜์ • ๋ฐ ์ฝ”๋“œ ๋ณ€๊ฒฝ ์ „์—๋Š” ์˜ˆ์ƒ ๋ฐฉ์•ˆ์„ ๋จผ์ € ๋ณด๊ณ ํ•˜๊ณ  ์Šน์ธ ์ ˆ์ฐจ๋ฅผ ๊ฑฐ์นœ๋‹ค. +5. **๋กœ๊ทธ ๊ธฐ๋ก ์ฒ ์ €**: ์ง„ํ–‰ ์ƒํ™ฉ(๋กœ๊ทธ์ธ, ์ˆ˜์ง‘, ์˜ค๋ฅ˜ ๋“ฑ)์„ ์‹ค์‹œ๊ฐ„ ๋กœ๊ทธ์— ์ƒ์„ธํžˆ ํ‘œ์‹œํ•˜์—ฌ ํˆฌ๋ช…์„ฑ์„ ํ™•๋ณดํ•œ๋‹ค. + +--- + +## ๐ŸŽจ ๋””์ž์ธ ๊ฐ€์ด๋“œ (Design System) + +์ด ํ”„๋กœ์ ํŠธ๋Š” `tokens.json`์— ์ •์˜๋œ ๋””์ž์ธ ์‹œ์Šคํ…œ์„ ์ค€์ˆ˜ํ•ฉ๋‹ˆ๋‹ค. + +### 1. ์ปฌ๋Ÿฌ ์‹œ์Šคํ…œ (Colors) +- **Primary**: `#1E5149` (primary-lv-6) - ๋ธŒ๋žœ๋“œ ํ•ต์‹ฌ ์ปฌ๋Ÿฌ +- **Background**: `#FFFFFF` (Light Default) / `#F9FAFB` (Light Muted) +- **Point Colors**: + - Blue: `#0D8DF2` (Info) + - Green: `#4DB251` (Success) + - Red: `#F21D0D` (Error) + - Yellow: `#FFBF00` (Warning) +- **Special**: `ai_color` (Purple-Blue Gradient) - AI ๊ด€๋ จ ์š”์†Œ ์ „์šฉ + +### 2. ํƒ€์ดํฌ๊ทธ๋ž˜ํ”ผ (Typography) +- **Font Family**: `Pretendard`, `sans-serif` +- **Scale**: + - **H1**: 20px / ExtraBold (pretendard-0) + - **H2**: 16px / SemiBold (pretendard-1) + - **H3/H4**: 14px / SemiBold or Regular + - **Body/P**: 12px / Regular (pretendard-2) + +### 3. ๋ ˆ์ด์•„์›ƒ ๋ฐ ๊ฐ„๊ฒฉ (Dimensions) +- **Spacing Unit**: Base 4px (xs: 4px, sm: 8px, md: 16px, lg: 32px, xl: 64px) +- **Border Radius**: sm: 4px, lg: 8px, xl: 16px +- **Shadow**: `0 8px 24px rgba(0,0,0,0.16)` (box__drop-shadow) + +### 4. ์ปดํฌ๋„ŒํŠธ ๊ทœ์น™ +- **Buttons**: `borderRadius.lg (8px)` ์ ์šฉ, Primary ๋ฐฐ๊ฒฝ์ƒ‰ ์‚ฌ์šฉ +- **Cards**: `borderRadius.lg (8px)` ์ ์šฉ, Subtle Shadow ํ™œ์šฉ +- **Topbar**: Height 36px, `headercolor` ๊ทธ๋ผ๋ฐ์ด์…˜ ์ ์šฉ ๊ฐ€๋Šฅ + diff --git a/__pycache__/analysis_service.cpython-312.pyc b/__pycache__/analysis_service.cpython-312.pyc deleted file mode 100644 index d34751fe6203791ea4a882c79ce2ef6d3da7fd90..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9746 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 pSn9~nECY0v0JWbwd%Qc* ztPMY8ft#D%`F3_@Zf5rW9FIp4j4vPGHh2)B@5RI$A(z?N#t7X&I?}O;3gRu9*pdno z7QAfAmQqk0UNHk!un@FDg^)83m|-hYh&X)Ej9O|zb@-4Ov*Lv~MiL^D9$t&A!URVbov?SJwCdt1>N}!bjC+_x1jet1Ks1Jdr8;0q-Pft-}=lm z_VoFC`dd!up2hjSKM2pyQ!8ucY?TzpVWC*6lx{*lWfb#-f5#6A$t3qdC zqLej@aD4DoUg2@gG%FX26>3ZvWyX`uX^9dYj4(8lc?gTxlNH^cP7+OrlON-uQftPD z+y?|9QS7d_9goR2)t-f^-Yg1BAyX zh+Xt?v>(=loh${!v$57aP*?LcCI5(KVYwO^vNXdKj{ZFR;+~r4m?vOG8;+x*>+BWg zJrTl=x;ju(23+f6Lz-=CrAZ4ChCN}R=#@?~{2o%EnG8C}lgf2_Zu}yuNjyY-7_2aVqoc-~q z?3aDt@W&_T@KncBFwe6~e4e&3=j+v_D%h>dpDDU>OVR^3CGSLxKAuz19Cphwc!md= zW=)&KKu{f3d2o2VD(6N|yig7&7gX7K$6ZM&c(X5*eQ8z7# zMqdT-K6uzIAWr4{Gu1mkx~ayO4=){FKDu;t<&|~yz-BnQtS+gajIM`s8?nUvZ#Ux| z%Y~)Fiu%|1FBb7;GJPX+J#(Y)df&*dfE?R8J0=_Ex%^x$d{aGlu{c6#={d z)x!3|t=#s)&HG<3Y=88{fO{Bp#(eI<%DwHcZaO$BDk6{RFua&J*$s89^**A%1i^?v zq=7hb{xOPTx!d6&0CIxC}+*T$9c{{`R|BfPCD7%G5{(;`uLa%J0fh{!jT`*qj99|EO%*%}+!pet{E0M*4 z>!WvGUhf>amzj^OqvIP${u@eti&7g(Y9rnC$^MP5Z1XK94He<^k0^+xMhZ!R4>MOX zi|K294<(Eb?8rz;Gz16AU0GipcM0J^A>-+)y53HHdBme*a diff --git a/__pycache__/crawler_service.cpython-312.pyc b/__pycache__/crawler_service.cpython-312.pyc deleted file mode 100644 index df7e01a4fe978f2624297e86ebff6f0f3d906724..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18254 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 diff --git a/__pycache__/inquiry_service.cpython-312.pyc b/__pycache__/inquiry_service.cpython-312.pyc deleted file mode 100644 index 8b3921466bf1715217c093f4ef5f4c7a2c07187c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2795 zcmbUjTSy#N^v=va26tSeOKK9DW)oR;OKTqZ)6fdW-4<&!>e`PLVOVCaX14phGh4H= zX#&lMLcx><6jGs|mi(kps2}}EzuMA5cgaFF1A##MQ)s|M+Csj1?wy%++-N90;MsG| zJ#+4PUw>|IZ$kh*8{cVzK7@Xwi~}B<*f|5l8d8zUWl$E)B9790hRgD^JcoGn9#VxH zNcG&|tuJPUvN!7cgECRh6xEc04K0g-4vyxEC5@D)N|`W=I# zR~uBuAZ`~xQg=hNwOZ9n|zQFW_ zm+A{HeGiF%Kc1#Ojj07xZ^d&Psoo7p(2LZ9+nQJPxw0U(7y|Y-+Hhh)>A;KU708Lx z4-h>{L!Mf}DPaOd{S!-_;o*r`xR8~Nash|m2}ks$E+$f-e-`qjY{?EWnQo+vl5Rr!pC6G@nu; zdY+hmI}%fL{5O4;*QWnNTwcr*)fDt%#`G0ZB$d^5TClJMf1L+9c7E>9jc=ZO^2L+e z%X4SnnzTG0Pw56GbHk$}c@@uK-I&v?k&<<*vj+-gGqivW*^ zad`FG%C-9);i?$kfq0PO9%Nc*Hts4AYqUA-F{PRg7|k!zRKEo(+&s6Zz4sY6S9^04 zQ4a}%4Xeo{r^%A?Xw2@Od=5u>atL(HHm9VTs#7n*VbV?IM|MkAmQ`$|w2WQbz5{E! zSMeB_(IbGaqsLapZhicq`&_O2+}+u`<8|@Ep3IS>psBc{r;C7P?r$JqQ8x3o-=cKl z-)}Xl&DD6-1Nq-4dq#8Vq}dY%7rLM#q$L+?lOUx@ZUKTw% zP7Ltm@v5M4u&O*0OJXEB5XlZiRMW4Q6b0-0Otg*A#F#=Zzi5iOLFVa`=dSq&ld<8M zxI7)7yqGk-1Q#-8LT@zvms2@4g9)sTd^W2YQLoho9@uC(a-3NuxM)h2o+{G^Rj0Gc zwuRLj$4Uh%wx_-ruy-0rp(1DYAP%@z*0ivQQ%5pB1#jy7?g+@&wN0t* zexRQ6d{YM0w#a0jrVOZUiOB?8_LK+8x{TgKHzb>&06dD|GW!RStxL;|bJQ2QkeF-F l#kZ{phsYqv=pND!1KJiij(dcTKSI6#czU^$&k!K?*}v30dj0?a diff --git a/__pycache__/prediction_service.cpython-312.pyc b/__pycache__/prediction_service.cpython-312.pyc deleted file mode 100644 index d80a302238a3e38c0143c44e8fa5e6d0e59448ee..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4164 zcmai1eQXrR6`$SP`|$aEXZ!q#!K_OfbKu%BA0cT1#@Gf7Bqlgj0)ehpyX)i3eVE-n z8@qdjrdD(bL>OV@b2-RGN?Iq-_L`8$Z6nof`Uh42x$`B#-MCV!Myib0lKF_5en|Aq z-X{%B$MV}ZZ{ECl@6F6_-u@q`y0BH&AU zNgsKLBoK*qA>J^GczV~=t@T6kP-S|Ki4-w-^5d+L{4HJHoDAeMQ=~{eo^2(oa_nsdj)aO!wJES zU}ovUr1r{tOOXq-b~<|V-BE4g8m&db+Vt!6V*D(N9ZvG-pLs0b6<*@e38%r<67Jqzo@y!o#6icU0fe0pK+#Y$` zN*A==u6Gs}^+)6G|H47&ME+`AC zrB9H#qpa5pLK7DhrGThfGai6-32y^Uf3X#3>f494OV^fu7+ty;KHU2F{ELyp2L#0(`PriII<)9N5`VOcguJD|gK%k$Z7z8ze z)nJ3}Tz*D?2O7$W)3K0tGL`Pf*g~%eLki4lDjQzka=T2BwmbK(3 zcrs*Ah<+pXvz{vSgvgHY=3|}U$r>5QuD5-H0QPRN<^b{wz4B5^<&|%kxFYw&;@b ztmGVdriMZxQ-~~xCck^69!+(@m?`U1fh?y#0y)!jW0p&@Ia8?IjJ(YF0GT-_d(b(u z;&ZbG0SZ0v1n^{bHh~VJG2%?~3By4&L~wxT92p{qkgGVj2Ygx_e{=D*tBa>E(P<3T zE{)P!bad%Ll-{&><`hWZ(@tOgF9cn?Q()zwD9~Mgx5qCCqQ~!cE#R9hV)D0ite>^H ze>92PFWX#{Y8e!~0nQ^2t2UP71g{{n7{%3MF5s5{hXg-2teSZNI-q0CgV_QOU$y2D z?Tb zUM2klD#d&H`c%p%uzuCp=MAv3%c7byzzBea;Z#zIqpD$$_2!pQDS%c zz9nj{a6%SYPG$x@GV6tSn7(wvq><(aBGUiV$Kgm@AOok+T@$i7!b7K@j5N#^*Uixl z@z$B%#CM)gIu9mn2LZOqE5fEv%WA^y%Oq9OFxT7?x4jieGBxmDk=aMa`}8sw*!{x4zAcZ&L@3@MT@= zCk}tCJ>DMozT0!1xZd;!N8$xvYPWBU7;8H3NI3jq3dlSx+<3)1{mg9hw(&J%{CQWD zi#1;K&X%v6X%Cz4+E8tM9KREJiMeu%L-| zPe<_GI)W>NXfL?eg{X#6{NX%0%Q+P0$vm3z1_i1ng&cv=5vakkwL|vH8?#(5l5^<$ zAau)PV8~E_B{`%CQ3?SylX(TkJ!1y<;2MOCvIAn$n_^5O;0Zzk6v!h4U|yAudB%LK zGQLddSviEuA_{LjwgN1hVZ|bhsJs=^kVUcZ<{$#=#ehl{zWCT$gnJaMWm7DJC{(1F zMYB>Ql6(oEqCI5w*C+wTM2tk_2I8l^aYAuI0;l_->A#WJNN zpV^Q-XNkn#iB`p~P&!&06^gIWXM=>V)ZgDS@WfHLXNsM#D%_z^!OL5yci-OHv%8o5 z7Vc*TX^nui)xn+M?#=1A_BxcMu`w+i(I!t7l&b0a6c>Is--$0>cx&lGM4PxuFaCUb z@iLUH`ix9HyMT|Uj;)fM-DCg>)yS4Pm7h91@4|$jg@PF1Tl~-oxQgB z>bQ1!Z1L=*_To>qSkw(tXw*!GS^iBL;xNMM;Fdw~g*Ny1Dt@->FC*95GMZnuEnt+* z^eVQyibO~|QjH=YCa)TN>ZmKAwMl3BN(1&FtJp=!kCuWE-ee}Dlr z_5d0Ws}5hl2Wtj>OdrUg7&Q$tZr0;hO?>)m=LL=(hCG316iw(r3PD zz%S3}FnvM%E`Ckz`BlZuU{o5`A^w&Hl~zaBMBEpfrpjhltebb%M4gdm-Y~>Ers}4t z>5=4P-AUJ;6umc9yYD)ia_&#q_J8hdyy0w5I@@O&Q_ekM%Y0Q`WN)f!ZMb;Zg0^-N zGdpHFruThP(;YvKjYX76W_DF;ocOqQL!zcTTmodiu0ATowx(9Mrs_7t*<{_8grx>h zyrM3$;oK7uCFyJmTjpvTV$UXOU9+`;spk`q?@fMdFR+OoF5w0m2-|PL1GN%J2J zT*c~GOR}Oh{@t1Wf^FpQt|2@IoJ8O9gjgHTH_s*Pce2U%}s#LO^! zfWu-qE3$|43G&eZFWSID`pj4v2>Fa>+1O@-nb|9nZPt?}WjAge-i?QL3 z0h!8|F}GX++%1weS~D`>mOBX$-X_-@H|xS%m`fYSCZWnQ`h&iKVG+N2qK*{$_3Ogj roDcuBJedw>o45-GaCs$Rpjb8#1o0^<|1(!)hGT3Q6yi6 diff --git a/__pycache__/project_service.cpython-312.pyc b/__pycache__/project_service.cpython-312.pyc deleted file mode 100644 index 76db69855cc48854b5fb0c5a1d28931acffb6e45..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 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 diff --git a/__pycache__/server.cpython-312.pyc b/__pycache__/server.cpython-312.pyc deleted file mode 100644 index 17d489e0a375993521b09fa0b76f0c7597cfb39c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13292 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 Xsz_$hQky1jw4ecVp**12!u$UK@KGO` diff --git a/__pycache__/sql_queries.cpython-312.pyc b/__pycache__/sql_queries.cpython-312.pyc deleted file mode 100644 index b7d27ec9fc4f372cea3a5ef05daeb05fa698591b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2842 zcmc&$O>7%Q6ka=yV?q-8lm0*fO$8xd6opm-qynkBu{WvP&DyLtEjE!xi+90B{z-Oi zki#W~91wp!(5j6{L@ObnB8W)zkOLCp%%$95g-|6V4z(=>A{2=eZ)RtA?KF+JVaek+ z@6G#}H{UnoAN%`zIruz!`-j4>ApK4z?kB{A%~K#;;ey;Gr*n&(9{4KgSh9{T2)Zs$ z7oh7xpzHQ@!FrhQ$+2+sC%`6~kArZ9Gq`L(=dwXPknPfg*^u6q?bbutu-={R(ZktD zy@!w31Bvontzgz)tQls(uxd|%Yh!i2xpKAn_G;_$*HOm~CLXmuy4qa1+5BPw-MP8Y zxb`J#e*azL^VRvIX;BhmDtZ!4D9I^Qpb^0Xoj)xqB04^He11<-iHiyv&!BPz#l>`N zerR{6f{w<}kTw5SFJhnRR9sNSw&gW{Y*|GJ>qCR;^iVlGn<5(J(t^I@YzS zK~>0$;uOpBQf=)GXohG1Z~2FZ+wu=LzFBWAU2d+dgVrg;OFcZiTWXoV&yHYR)=CD( zb}z=Iie4)cxew#HnpR|%9*p%$4%GKM?Zo3kTD1FVxAAxelUCb(t}TNTal6mi2v)?D zl(GBbaCWNb%x-X-1VVp53q$<58940LJIk$4-kmvqB4t)yH*(b}&8iyajF5;`bVD_) z>Wnp4gn}q|MrZ407r4@afYBmAT;zTUow&X4nT4}I4?eLlwGrk9pI?wR!lA(k{0zX) z1<4$6-F5~v0`u;GCS!@{gv9)w#8@Lk;r}0EuO~`=qoTLA`Ay zh5QK?YD#!1>f3mvS*Qqo-6f_r1nu;Y$ikFHS!!G^jiAz~m9LmpChNwmW>!l^xk@Z0 zdTJv+;25Ryqo$FAHMm%L4FEd&Y5^dYtJEB?T%}wslvxO>h0S?Ibne~BfJ&ZO5+_u2 zCYg}YUH!?Uqzpy5#Z$)R+oysYhhqE=QKEX0z4ms55YjQaV*WR8;azPi=%g8L-rbva z@78$n|CbKPe8ePgh}lhsFd2Gau=h`jDi+QO2}u~2L`<&Aw4;=Sw2EVja9*NXdk4}<0246k?dVS6A(m}i(Sx1R==)`iu^_0Jlgu11*= zX#L?DxwsbBP~+m#o%OZW(xpyHYngU3CFHcIs3;+;Ngt{V&QTXD1UaruaMMOLMy)oo zFskq#P|gX`w3tTGA!`JlwC!Ji9nc~^ostqU^3I(VGl)GoULZnSzYL&-6WJ@XT;l!O ze6w3EKhhD2ZPJV4%proe7e9ojqGq@t;41JOJ2pIjidD@DwJ^&=UTObY0`DRB4}!ZT z_s##qD|5%gaqn4g+vIfo+hPR%3;36sf2HyM-PL{|v1vsSWtDOx3cGU-sBQ7RPQ}kM z#*V?)Kpa1@YxXtHk;{C~j#TcSP7z4rDIl(>*n8P~9T!uAqE11r_5fRP8rYjuGTRw? z2#8^l;TU`^!YuLzpWFzA4@BSr-p4)oGEnpYJi!Me7i5Q-WVy_DIqx^6IE~WD{eL1a uIR8UVnnz$gec~SjVWTS$2y6up1R{TOFl>z;BoYi;$My#ze{nF-EdK&zPd_RE diff --git a/analysis_service.py b/analysis_service.py index 2f8f1f0..d543681 100644 --- a/analysis_service.py +++ b/analysis_service.py @@ -1,198 +1,270 @@ -import re -import math -import statistics -from datetime import datetime, timedelta -from sql_queries import DashboardQueries -from prediction_service import SOIPredictionService - -class AnalysisService: - """ํ”„๋กœ์ ํŠธ ํ†ต๊ณ„ ๋ฐ ํ™œ๋™์„ฑ ๋ถ„์„ ์ „๋ฌธ ์„œ๋น„์Šค""" - - @staticmethod - def calculate_operational_consistency(history_rows, days_stagnant): - """์šด์˜ ์ผ๊ด€์„ฑ ์ง€์ˆ˜(OCI) ์‚ฐ์ถœ ๋กœ์ง (์žฅ๊ธฐ ์ •์ฒด ํŒจ๋„ํ‹ฐ ํฌํ•จ) - ์ตœ๊ทผ 30์ผ๊ฐ„ ํ™œ๋™ ๋ฆฌ๋“ฌ ๋ถ„์„ + ํ˜„์žฌ ๋ฐฉ์น˜ ๊ธฐ๊ฐ„์— ๋”ฐ๋ฅธ ๊ฐ•๋ ฅํ•œ ๊ฐ์‡„ - """ - if not history_rows or len(history_rows) < 2: - return 0.0 - - # 1. ์ตœ๊ทผ 30์ผ ์ด๋ ฅ ๊ธฐ๋ฐ˜ Base Score ์‚ฐ์ถœ - now = datetime.now().date() - recent_30 = [h for h in history_rows if (now - h['crawl_date']).days <= 30] - - # ์ฃผ์ฐจ๋ณ„ ํ™œ๋™ ์—ฌ๋ถ€ (4์ฃผ) - weeks_active = [False, False, False, False] - for h in recent_30: - days_ago = (now - h['crawl_date']).days - week_idx = min(3, days_ago // 7) - weeks_active[week_idx] = True - - base_consistency = (sum(weeks_active) / 4) * 70 - - # ํ™œ๋™ ๋ฐ€๋„ (๋ณ€ํ™” ๋ฐœ์ƒ์ผ ๋น„์œจ) - effort_days = 0 - for i in range(1, len(recent_30)): - if recent_30[i]['file_count'] != recent_30[i-1]['file_count']: - effort_days += 1 - - density_score = (effort_days / max(1, len(recent_30))) * 30 - base_oci = base_consistency + density_score - - # 2. [ํ•ต์‹ฌ] ์žฅ๊ธฐ ์ •์ฒด ํŒจ๋„ํ‹ฐ ์ ์šฉ - # ๋ฐฉ์น˜์ผ์ด 100์ผ ์ด์ƒ์ด๋ฉด OCI๋Š” 0์ ์œผ๋กœ ์ˆ˜๋ ด (์„ฑ์‹ค๋„ ๋ฌดํšจํ™”) - stagnation_factor = max(0, (100 - days_stagnant) / 100.0) - - final_oci = base_oci * stagnation_factor - - return round(final_oci, 1) - - @staticmethod - def calculate_activity_status(target_date_dt, log, file_count): - """๊ฐœ๋ณ„ ํ”„๋กœ์ ํŠธ์˜ ํ™œ๋™ ์ƒํƒœ ๋ฐ ๋ฐฉ์น˜์ผ ์‚ฐ์ถœ""" - status, days = "unknown", 999 - file_val = int(file_count) if file_count else 0 - has_log = log and log != "๋ฐ์ดํ„ฐ ์—†์Œ" and log != "X" - - if file_val == 0: - status = "unknown" - elif has_log: - if "ํด๋”์ž๋™์‚ญ์ œ" in log.replace(" ", ""): - status = "stale" - days = 999 - else: - match = re.search(r'(\d{4})\.(\d{2})\.(\d{2})', log) - if match: - log_date = datetime.strptime(match.group(0), "%Y.%m.%d") - diff = (target_date_dt - log_date).days - status = "active" if diff <= 7 else "warning" if diff <= 14 else "stale" - days = diff - else: - status = "stale" - else: - status = "stale" - - return status, days - - @staticmethod - def get_project_activity_logic(cursor, date_str): - """ํ™œ๋™๋„ ๋ถ„์„ ๋ฆฌํฌํŠธ ์ƒ์„ฑ ๋กœ์ง""" - if not date_str or date_str == "-": - cursor.execute(DashboardQueries.GET_LAST_CRAWL_DATE) - res = cursor.fetchone() - target_date_val = res['last_date'] if res['last_date'] else datetime.now().date() - else: - target_date_val = datetime.strptime(date_str.replace(".", "-"), "%Y-%m-%d").date() - - target_date_dt = datetime.combine(target_date_val, datetime.min.time()) - cursor.execute(DashboardQueries.GET_PROJECT_LIST_FOR_ANALYSIS, (target_date_val,)) - rows = cursor.fetchall() - - analysis = {"summary": {"active": 0, "warning": 0, "stale": 0, "unknown": 0}, "details": []} - for r in rows: - status, days = AnalysisService.calculate_activity_status(target_date_dt, r['recent_log'], r['file_count']) - analysis["summary"][status] += 1 - analysis["details"].append({"name": r['short_nm'] or r['project_nm'], "status": status, "days_ago": days}) - - return analysis - - @staticmethod - def get_p_zsr_analysis_logic(cursor): - """์ ˆ๋Œ€์  ๋ฐฉ์น˜ ์‹คํƒœ ๊ณ ๋ฐœ ๋ฐ ์šด์˜ ์ผ๊ด€์„ฑ(OCI) ๋ถ„์„ ๋กœ์ง""" - cursor.execute(DashboardQueries.GET_LAST_CRAWL_DATE) - res_date = cursor.fetchone() - if not res_date or not res_date['last_date']: - return [] - last_date = res_date['last_date'] - - cursor.execute(""" - SELECT m.project_id, m.project_nm, m.short_nm, m.department, m.master, - h.recent_log, h.file_count, m.continent, m.country - FROM projects_master m - LEFT JOIN projects_history h ON m.project_id = h.project_id AND h.crawl_date = %s - ORDER BY m.project_id ASC - """, (last_date,)) - projects = cursor.fetchall() - - if not projects: return [] - - results = [] - total_soi = 0 - - for p in projects: - file_count = int(p['file_count']) if p['file_count'] else 0 - log = p['recent_log'] - - # ๋ฐฉ์น˜์ผ ๊ณ„์‚ฐ - days_stagnant = 14 - if log and log != "๋ฐ์ดํ„ฐ ์—†์Œ": - 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 = (last_date - log_date).days - - is_auto_delete = log and "ํด๋”์ž๋™์‚ญ์ œ" in log.replace(" ", "") - - # AI-Hazard ์ถ”๋ก  ๋กœ์ง (Dynamic Lambda) - scale_impact = min(0.04, math.log10(file_count + 1) * 0.008) if file_count > 0 else 0 - ai_lambda = 0.04 + scale_impact - - # ์ง€์ˆ˜ ๊ฐ์‡„ ์ ์šฉ - soi_score = math.exp(-ai_lambda * days_stagnant) * 100 - - # ECV ํŒจ๋„ํ‹ฐ - existence_confidence = 1.0 - if file_count == 0: existence_confidence = 0.05 - elif file_count < 10: existence_confidence = 0.4 - - # Log Quality Scoring - log_quality_factor = 1.0 - if log and log != "๋ฐ์ดํ„ฐ ์—†์Œ": - if any(k in log for k in ["์—…๋กœ๋“œ", "์ˆ˜์ •", "๋“ฑ๋ก", "๋ณ€ํ™˜", "ํŒŒ์ผ", "์—…๋ฐ์ดํŠธ"]): log_quality_factor = 1.0 - elif any(k in log for k in ["ํด๋”", "์ƒ์„ฑ", "์‚ญ์ œ", "์ด๋™"]): log_quality_factor = 0.7 - elif any(k in log for k in ["์ฐธ๊ฐ€์ž", "๊ถŒํ•œ", "์ถ”๊ฐ€", "๋ณ€๊ฒฝ", "๋ฉ”์ผ"]): log_quality_factor = 0.4 - else: log_quality_factor = 0.6 - - soi_score = soi_score * existence_confidence * log_quality_factor - if is_auto_delete: soi_score = 0.1 - - # [์šด์˜ ์ผ๊ด€์„ฑ ๋ถ„์„ (OCI)] - history_rows = SOIPredictionService.get_historical_soi(cursor, p['project_id']) - oci_score = AnalysisService.calculate_operational_consistency(history_rows, days_stagnant) - - # ์‹ค๋ฌด ํˆฌ์ž… ์—๋„ˆ์ง€ ๊ณ„์‚ฐ - 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 - - # VCI ์‚ฐ์ถœ - REPLACEMENT_LEVEL = 70.0 - asset_weight = (file_count / 200.0) + 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": round(p_war_score, 2), - "p_war": round(soi_score, 1), - "oci_score": oci_score, # ์šด์˜ ์ผ๊ด€์„ฑ ์ง€์ˆ˜ ์ถ”๊ฐ€ - "is_auto_delete": is_auto_delete, - "master": p['master'], - "dept": p['department'], - "ai_lambda": round(ai_lambda, 4), - "log_quality": log_quality_factor, - "work_effort": work_effort_rate, - "avg_info": { - "avg_files": 0, - "avg_stagnant": 0, - "avg_risk": round(total_soi / len(projects), 1) - } - }) - - results.sort(key=lambda x: x['p_war']) - return results +import re +import math +import statistics +from datetime import datetime, timedelta +from sql_queries import DashboardQueries +from prediction_service import SOIPredictionService + +class AnalysisService: + """ํ”„๋กœ์ ํŠธ ํ†ต๊ณ„ ๋ฐ ํ™œ๋™์„ฑ ๋ถ„์„ ์ „๋ฌธ ์„œ๋น„์Šค""" + + @staticmethod + def calculate_operational_consistency(history_rows, days_stagnant): + """์šด์˜ ์ผ๊ด€์„ฑ ์ง€์ˆ˜(OCI) ์‚ฐ์ถœ ๋กœ์ง (์ž์‚ฐ ๊ทœ๋ชจ ๋ฐ ์žฅ๊ธฐ ์ •์ฒด ํŒจ๋„ํ‹ฐ ํฌํ•จ) + ์ตœ๊ทผ 30์ผ๊ฐ„ ํ™œ๋™ ๋ฆฌ๋“ฌ ๋ถ„์„ + ํ˜„์žฌ ๋ฐฉ์น˜ ๊ธฐ๊ฐ„์— ๋”ฐ๋ฅธ ๊ฐ•๋ ฅํ•œ ๊ฐ์‡„ + """ + if not history_rows or len(history_rows) < 2: + return 0.0 + + # [์ถ”๊ฐ€] ์ตœ์‹  ์ƒํƒœ ํ™•์ธ: ํ˜„์žฌ ๋กœ๊ทธ๊ฐ€ 'ํด๋”์ž๋™์‚ญ์ œ'๋ฉด ์ ์ˆ˜ ์ฆ‰์‹œ 0์  (์ผ์ˆ˜๋Š” ์‹ค์ œ ์ผ์ˆ˜ ์œ ์ง€) + latest_log = history_rows[-1].get('recent_log', '') or '' + if latest_log and "ํด๋”์ž๋™์‚ญ์ œ" in latest_log.replace(" ", ""): + return 0.0 + + # 1. ์ตœ๊ทผ 30์ผ ์ด๋ ฅ ๊ธฐ๋ฐ˜ Base Score ์‚ฐ์ถœ + now = datetime.now().date() + recent_30 = [h for h in history_rows if (now - h['crawl_date']).days <= 30] + + if not recent_30: + return 0.0 + + # [์ถ”๊ฐ€] ์ž์‚ฐ ๊ทœ๋ชจ ํ™•์ธ: ํŒŒ์ผ์ด 0๊ฐœ๋ฉด ์šด์˜ ์ผ๊ด€์„ฑ ์‚ฐ์ถœ ์ž์ฒด๊ฐ€ ๋ฌด์˜๋ฏธํ•จ + max_files = max([int(h['file_count'] or 0) for h in recent_30]) + if max_files == 0: + return 0.0 + + # ์ฃผ์ฐจ๋ณ„ ํ™œ๋™ ์—ฌ๋ถ€ (4์ฃผ) - ํŒŒ์ผ์ด 1๊ฐœ ์ด์ƒ ์กด์žฌํ•  ๋•Œ๋งŒ ์œ ํšจ ํ™œ๋™์œผ๋กœ ์ธ์ • + weeks_active = [False, False, False, False] + for h in recent_30: + if int(h['file_count'] or 0) > 0: + days_ago = (now - h['crawl_date']).days + week_idx = min(3, days_ago // 7) + weeks_active[week_idx] = True + + base_consistency = (sum(weeks_active) / 4) * 70 + + # ํ™œ๋™ ๋ฐ€๋„ (๋ณ€ํ™” ๋ฐœ์ƒ์ผ ๋น„์œจ) + effort_days = 0 + for i in range(1, len(recent_30)): + # 'ํด๋”์ž๋™์‚ญ์ œ' ๋กœ๊ทธ๊ฐ€ ํฌํ•จ๋œ ๋‚ ์˜ ๋ณ€ํ™”๋Š” ๊ด€๋ฆฌ ๋…ธ๋ ฅ์œผ๋กœ ์ธ์ •ํ•˜์ง€ ์•Š์Œ + log_content = recent_30[i].get('recent_log', '') or '' + if "ํด๋”์ž๋™์‚ญ์ œ" in log_content.replace(" ", ""): + continue + + if recent_30[i]['file_count'] != recent_30[i-1]['file_count']: + effort_days += 1 + + density_score = (effort_days / max(1, len(recent_30))) * 30 + base_oci = base_consistency + density_score + + # 2. [ํ•ต์‹ฌ] ํŒจ๋„ํ‹ฐ ์—”์ง„ ์ ์šฉ + # A. ์žฅ๊ธฐ ์ •์ฒด ํŒจ๋„ํ‹ฐ: ๋ฐฉ์น˜์ผ์ด 100์ผ ์ด์ƒ์ด๋ฉด 0์ ์œผ๋กœ ์ˆ˜๋ ด + stagnation_factor = max(0, (100 - days_stagnant) / 100.0) + + # B. ์ž์‚ฐ ๋ถ€์กฑ ํŒจ๋„ํ‹ฐ (Existence Confidence): ํŒŒ์ผ์ด ๋„ˆ๋ฌด ์ ์œผ๋ฉด ๊ด€๋ฆฌ ์‹ ๋ขฐ๋„ ํ•˜๋ฝ + # 10๊ฐœ ๋ฏธ๋งŒ์€ 50%๋งŒ ์ธ์ •, ๊ทธ ์ด์ƒ์€ ์ ์ง„์ ์œผ๋กœ 100%๊นŒ์ง€ ํšŒ๋ณต + asset_confidence = 1.0 + if max_files < 10: + asset_confidence = 0.5 + elif max_files < 30: + asset_confidence = 0.8 + + final_oci = base_oci * stagnation_factor * asset_confidence + + return round(final_oci, 1) + + @staticmethod + def calculate_activity_status(target_date_dt, log, file_count): + """๊ฐœ๋ณ„ ํ”„๋กœ์ ํŠธ์˜ ํ™œ๋™ ์ƒํƒœ ๋ฐ ๋ฐฉ์น˜์ผ ์‚ฐ์ถœ (ํ˜„์žฌ ์‹œ๊ฐ ๊ธฐ์ค€ ์‹ค์งˆ ๋ฐฉ์น˜์ผ ์‚ฐ์ถœ)""" + status, days = "unknown", 999 + file_val = int(file_count) if file_count else 0 + has_log = log and log != "๋ฐ์ดํ„ฐ ์—†์Œ" and log != "X" + + # ์‹ค์งˆ์ ์ธ ์˜ค๋Š˜ ๋‚ ์งœ๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์ •์ฒด์ผ ์‚ฐ์ถœ (์‚ฌ์šฉ์ž ์ง๊ด€์„ฑ ๊ฐ•ํ™”) + now_dt = datetime.now() + + if file_val == 0: + status = "unknown" + elif has_log: + is_auto = "ํด๋”์ž๋™์‚ญ์ œ" in log.replace(" ", "") + # 2์ž๋ฆฌ ๋˜๋Š” 4์ž๋ฆฌ ์—ฐ๋„ ์ง€์› ์ •๊ทœ์‹ + match = re.search(r'(\d{2,4})\.(\d{2})\.(\d{2})', log) + if match: + y, m, d = match.groups() + # 2์ž๋ฆฌ ์—ฐ๋„ ๋ณด์ • + if len(y) == 2: y = "20" + y + log_date = datetime.strptime(f"{y}.{m}.{d}", "%Y.%m.%d") + + # ์ˆ˜์ง‘์ผ(target_date_dt)์ด ์•„๋‹Œ ํ˜„์žฌ ์‹œ์ (now_dt) ๊ธฐ์ค€์œผ๋กœ ์ฐจ์ด ๊ณ„์‚ฐ + diff = (now_dt - log_date).days + days = diff + # ์ƒํƒœ ํŒ์ •์€ ์ˆ˜์ง‘ ์‹œ์ ์˜ target_date_dt๋ฅผ ๊ธฐ์ค€์œผ๋กœ ํ• ์ง€ ๊ฒ€ํ†  ํ•„์š”ํ•˜๋‚˜, + # ์‚ฌ์šฉ์ž ์š”์ฒญ์— ๋”ฐ๋ผ '์ด์ƒํ•œ ๊ณ„์‚ฐ'์„ ๋ฐ”๋กœ์žก๊ธฐ ์œ„ํ•ด ํ˜„์žฌ ์‹œ์  ๊ธฐ์ค€ ํŒ์ • ์ ์šฉ + status = "stale" if is_auto or diff > 14 else "warning" if diff > 7 else "active" + else: + status = "stale" + days = 999 + else: + status = "stale" + + return status, days + + @staticmethod + def get_project_activity_logic(cursor, date_str): + """ํ™œ๋™๋„ ๋ถ„์„ ๋ฆฌํฌํŠธ ์ƒ์„ฑ ๋กœ์ง""" + if not date_str or date_str == "-": + cursor.execute(DashboardQueries.GET_LAST_CRAWL_DATE) + res = cursor.fetchone() + target_date_val = res['last_date'] if res['last_date'] else datetime.now().date() + else: + target_date_val = datetime.strptime(date_str.replace(".", "-"), "%Y-%m-%d").date() + + target_date_dt = datetime.combine(target_date_val, datetime.min.time()) + cursor.execute(DashboardQueries.GET_PROJECT_LIST_FOR_ANALYSIS, (target_date_val,)) + rows = cursor.fetchall() + + analysis = {"summary": {"active": 0, "warning": 0, "stale": 0, "unknown": 0}, "details": []} + for r in rows: + status, days = AnalysisService.calculate_activity_status(target_date_dt, r['recent_log'], r['file_count']) + analysis["summary"][status] += 1 + analysis["details"].append({"name": r['short_nm'] or r['project_nm'], "status": status, "days_ago": days}) + + return analysis + + @staticmethod + def get_p_zsr_analysis_logic(cursor): + """์ ˆ๋Œ€์  ๋ฐฉ์น˜ ์‹คํƒœ ๊ณ ๋ฐœ ๋ฐ ์šด์˜ ์ผ๊ด€์„ฑ(OCI) ๋ถ„์„ ๋กœ์ง""" + cursor.execute(DashboardQueries.GET_LAST_CRAWL_DATE) + res_date = cursor.fetchone() + if not res_date or not res_date['last_date']: + return [] + last_date = res_date['last_date'] + + # ํŠน์ • ๋‚ ์งœ(last_date) ์ดํ•˜์˜ ๊ฐ ํ”„๋กœ์ ํŠธ๋ณ„ ์ตœ์‹  ๋ฐ์ดํ„ฐ๋ฅผ ์กฐ์ธํ•˜๋„๋ก ์ˆ˜์ • + cursor.execute(""" + SELECT m.project_id, m.project_nm, m.short_nm, m.department, m.master, + h.recent_log, h.file_count, m.continent, m.country + FROM projects_master m + LEFT JOIN projects_history h ON h.project_id = m.project_id AND h.crawl_date = ( + SELECT MAX(crawl_date) + FROM projects_history + WHERE project_id = m.project_id AND crawl_date <= %s + ) + ORDER BY m.project_id ASC + """, (last_date,)) + projects = cursor.fetchall() + + if not projects: return [] + + results = [] + total_avi = 0 + total_files = 0 + project_data_list = [] + + # 1์ฐจ Pass: ๊ฐœ๋ณ„ AVI ์‚ฐ์ถœ ๋ฐ ์ „์ฒด ํ•ฉ๊ณ„ ์ง‘๊ณ„ + now_dt = datetime.now() + for p in projects: + file_count = int(p['file_count']) if p['file_count'] else 0 + log = p['recent_log'] + + # ๋ฐฉ์น˜์ผ ๊ณ„์‚ฐ (ํ˜„์žฌ ์‹œ๊ฐ ๊ธฐ์ค€ ๋™๊ธฐํ™”) + days_stagnant = 14 + is_auto_delete = log and "ํด๋”์ž๋™์‚ญ์ œ" in log.replace(" ", "") + + if log and log != "๋ฐ์ดํ„ฐ ์—†์Œ": + match = re.search(r'(\d{2,4})\.(\d{2})\.(\d{2})', log) + if match: + y, m, d = match.groups() + if len(y) == 2: y = "20" + y + log_date = datetime.strptime(f"{y}.{m}.{d}", "%Y.%m.%d") + days_stagnant = (now_dt - log_date).days + elif is_auto_delete: + days_stagnant = 999 + + # AI-Hazard ์ถ”๋ก  ๋กœ์ง (Dynamic Lambda) + scale_impact = min(0.04, math.log10(file_count + 1) * 0.008) if file_count > 0 else 0 + ai_lambda = 0.04 + scale_impact + + # ์ง€์ˆ˜ ๊ฐ์‡„ ์ ์šฉ + avi_score = math.exp(-ai_lambda * days_stagnant) * 100 + + # ECV ํŒจ๋„ํ‹ฐ + existence_confidence = 1.0 + if file_count == 0: existence_confidence = 0.05 + elif file_count < 10: existence_confidence = 0.4 + + # Log Quality Scoring (SWVW ๋ชจ๋ธ ์ ์šฉ) + from log_scorer import LogScorer + log_quality_factor = LogScorer.get_score(log) + + avi_score = avi_score * existence_confidence * log_quality_factor + if is_auto_delete: avi_score = 0.1 + + total_avi += avi_score + total_files += file_count + project_data_list.append({ + "p": p, + "avi_score": avi_score, + "file_count": file_count, + "days_stagnant": days_stagnant, + "is_auto_delete": is_auto_delete, + "log_quality": log_quality_factor, + "ai_lambda": ai_lambda + }) + + # 2์ฐจ Pass: ํ‰๊ท  ๊ธฐ๋ฐ˜ ๊ฐ€์น˜๊ธฐ์—ฌ๋„(WAR) ์‚ฐ์ถœ + num_projects = len(projects) if projects else 1 + avg_avi = total_avi / num_projects + avg_files = total_files / num_projects + + for item in project_data_list: + p = item['p'] + avi_score = item['avi_score'] + file_count = item['file_count'] + + # [์šด์˜ ์ผ๊ด€์„ฑ ๋ถ„์„ (OCI)] + history_rows = SOIPredictionService.get_historical_avi(cursor, p['project_id']) + oci_score = AnalysisService.calculate_operational_consistency(history_rows, item['days_stagnant']) + + # ์‹ค๋ฌด ํˆฌ์ž… ์—๋„ˆ์ง€ ๊ณ„์‚ฐ + 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) + + # [VCI ์‚ฐ์ถœ - ๋กœ๊ทธ ๊ธฐ๋ฐ˜ ์ƒ๋Œ€ ๊ฐ€์ค‘์น˜ ๋ชจ๋ธ (์ˆ˜์ •)] + # 1. ํŒŒ์ผ ๊ทœ๋ชจ ๊ฐ€์ค‘์น˜๋ฅผ ๋กœ๊ทธ(log10) ๊ธฐ๋ฐ˜์œผ๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ ์„ ํ˜• ํญ์ฃผ ๋ฐฉ์ง€ + # 2. ํ‰๊ท  ํŒŒ์ผ ์ˆ˜ ๋Œ€๋น„ ์ƒ๋Œ€์  ๊ทœ๋ชจ๋ฅผ ๋ฐ˜์˜ํ•˜๋˜, ์ตœ๋Œ€ ๊ฐ€์ค‘์น˜๋ฅผ 2.5๋กœ ์บกํ•‘(Capping) + if avg_files > 0: + relative_size = math.log10(file_count + 1) / math.log10(avg_files + 1) + else: + relative_size = 1.0 + + asset_weight = min(2.5, max(0.2, relative_size)) + p_war_score = (avi_score - avg_avi) * asset_weight + + results.append({ + "project_nm": p['short_nm'] or p['project_nm'], + "file_count": file_count, + "days_stagnant": item['days_stagnant'], + "risk_count": round(p_war_score, 2), # WAR ๊ธฐ๋ฐ˜ ๊ฐ€์น˜๊ธฐ์—ฌ๋„ (ํ‰๊ท  0) + "p_war": round(avi_score, 1), + "oci_score": oci_score, + "is_auto_delete": item['is_auto_delete'], + "master": p['master'], + "dept": p['department'], + "ai_lambda": round(item['ai_lambda'], 4), + "log_quality": item['log_quality'], + "work_effort": work_effort_rate, + "avg_info": { + "avg_files": round(avg_files, 1), + "avg_stagnant": 0, + "avg_risk": round(avg_avi, 1) + } + }) + + results.sort(key=lambda x: x['p_war']) + return results diff --git a/analyze.py b/analyze.py index 70108e4..34d20d4 100644 --- a/analyze.py +++ b/analyze.py @@ -1,167 +1,167 @@ -import os -import re -import unicodedata -from pypdf import PdfReader -import pytesseract -from pdf2image import convert_from_path - -# 1. ์‹œ์Šคํ…œ ์„ค์ • -TESSERACT_EXE = r'C:\Users\User\AppData\Local\Programs\Tesseract-OCR\tesseract.exe' -TESSDATA_DIR = r'C:\Users\User\AppData\Local\Programs\Tesseract-OCR\tessdata' -POPPLER_BIN = r'D:\์ดํƒœํ›ˆ\00ํฌ๋กฌ๋‹ค์šด๋กœ๋“œ\poppler-25.12.0\Library\bin' - -pytesseract.pytesseract.tesseract_cmd = TESSERACT_EXE -os.environ["TESSDATA_PREFIX"] = TESSDATA_DIR -OCR_AVAILABLE = os.path.exists(TESSERACT_EXE) - -SYSTEM_HIERARCHY = { - "ํ–‰์ •": { - "๊ณ„์•ฝ": ["๊ณ„์•ฝ๊ด€๋ฆฌ", "๊ธฐ์„ฑ๊ด€๋ฆฌ", "์—…๋ฌด์ง€์‹œ์„œ", "์ธ์›๊ด€๋ฆฌ"], - "์—…๋ฌด๊ด€๋ฆฌ": ["์—…๋ฌด์ผ์ง€(2025)", "์—…๋ฌด์ผ์ง€(2025๋…„ ์ด์ „)", "๋ฐœ์ฃผ์ฒ˜ ์ •๊ธฐ๋ณด๊ณ ", "๋ณธ์‚ฌ์—…๋ฌด๋ณด๊ณ ", "๊ณต์‚ฌ๊ฐ๋…์ผ์ง€", "์–‘์‹์„œ๋ฅ˜"] - }, - "์„ค๊ณ„์„ฑ๊ณผํ’ˆ": { - "์‹œ๋ฐฉ์„œ": ["๊ณต์‚ฌ์‹œ๋ฐฉ์„œ", "์žฅ๋น„ ๋ฐ˜์ž…ํ—ˆ๊ฐ€ ๊ฒ€ํ† ์„œ"], - "์„ค๊ณ„๋„๋ฉด": ["๊ณตํ†ต", "ํ† ๊ณต", "๋น„ํƒˆ๋ฉด์•ˆ์ „๊ณต", "๋ฐฐ์ˆ˜๊ณต", "๊ต๋Ÿ‰๊ณต", "ํฌ์žฅ๊ณต", "๊ตํ†ต์•ˆ์ „์‹œ์„ค๊ณต", "๋ถ€๋Œ€๊ณต", "์šฉ์ง€๊ณต & ๊ธฐํƒ€๊ณต"], - "์ˆ˜๋Ÿ‰์‚ฐ์ถœ์„œ": ["ํ† ๊ณต", "๋น„ํƒˆ๋ฉด์•ˆ์ „๊ณต", "๋ฐฐ์ˆ˜๊ณต", "๊ต๋Ÿ‰๊ณต", "ํฌ์žฅ๊ณต", "๊ตํ†ต์•ˆ์ „์‹œ์„ค๊ณต", "๋ถ€๋Œ€๊ณต", "์šฉ์ง€๊ณต & ๊ธฐํƒ€๊ณต"], - "๋‚ด์—ญ์„œ": ["๋‹จ๊ฐ€์‚ฐ์ถœ์„œ"], - "๋ณด๊ณ ์„œ": ["์‹ค์‹œ์„ค๊ณ„๋ณด๊ณ ์„œ", "์ง€๋ฐ˜์กฐ์‚ฌ๋ณด๊ณ ์„œ", "๊ตฌ์กฐ๊ณ„์‚ฐ์„œ", "์ˆ˜๋ฆฌ ๋ฐ ์ „๊ธฐ๊ณ„์‚ฐ์„œ", "๊ธฐํƒ€๋ณด๊ณ ์„œ", "๊ธฐ์ˆ ์ž๋ฌธ ๋ฐ ์‹ฌ์˜"], - "์ธก๋Ÿ‰๊ณ„์‚ฐ๋ถ€": ["์ธก๋Ÿ‰๊ณ„์‚ฐ๋ถ€"], - "์„ค๊ณ„๋‹จ๊ณ„ ์ˆ˜ํ–‰ํ˜‘์˜": ["ํšŒ์˜ยทํ˜‘์˜"] - }, - "์‹œ๊ณต์„ฑ๊ณผํ’ˆ": { - "์„ค๊ณ„๋„๋ฉด": ["๊ณตํ†ต", "ํ† ๊ณต", "๋น„ํƒˆ๋ฉด์•ˆ์ „๊ณต", "๋ฐฐ์ˆ˜๊ณต", "๊ต๋Ÿ‰๊ณต", "ํฌ์žฅ๊ณต", "๊ตํ†ต์•ˆ์ „์‹œ์„ค๊ณต", "๋ถ€๋Œ€๊ณต", "์šฉ์ง€๊ณต & ๊ธฐํƒ€๊ณต"] - }, - "์‹œ๊ณต๊ฒ€์ธก": { - "ํ† ๊ณต": ["๊ฒ€์ธก (๊นจ๊ธฐ)", "๊ฒ€์ธก (์—ฐ์•ฝ์ง€๋ฐ˜)", "๊ฒ€์ธก (๋ฐœํŒŒ)", "๊ฒ€์ธก (๋…ธ์ฒด)", "๊ฒ€์ธก (๋…ธ์ƒ)", "๊ฒ€์ธก (ํ† ์ทจ์žฅ)"], - "๋ฐฐ์ˆ˜๊ณต": ["๊ฒ€์ธก (Vํ˜•์ธก๊ตฌ)", "๊ฒ€์ธก (์‚ฐ๋งˆ๋ฃจ์ธก๊ตฌ)", "๊ฒ€์ธก (Uํ˜•์ธก๊ตฌ)", "๊ฒ€์ธก (Uํ˜•์ธก๊ตฌ)(์•ˆ)", "๊ฒ€์ธก (Lํ˜•์ธก๊ตฌ, Jํ˜•์ธก๊ตฌ)", "๊ฒ€์ธก (๋„์ˆ˜๋กœ)", "๊ฒ€์ธก (๋„์ˆ˜๋กœ)(์•ˆ)", "๊ฒ€์ธก (ํšก๋ฐฐ์ˆ˜๊ด€)", "๊ฒ€์ธก (์ข…๋ฐฐ์ˆ˜๊ด€)", "๊ฒ€์ธก (๋งน์•”๊ฑฐ)", "๊ฒ€์ธก (ํ†ต๋กœ์•”๊ฑฐ)", "๊ฒ€์ธก (์ˆ˜๋กœ์•”๊ฑฐ)", "๊ฒ€์ธก (ํ˜ธ์•ˆ๊ณต)", "๊ฒ€์ธก (์˜น๋ฒฝ๊ณต)", "๊ฒ€์ธก (์šฉ์ˆ˜๊ฐœ๊ฑฐ)"], - "๊ตฌ์กฐ๋ฌผ๊ณต": ["๊ฒ€์ธก (ํ‰๋ชฉ๊ต-๊ฑฐ๋”, ๋ถ€๋Œ€๊ณต)", "๊ฒ€์ธก (ํ‰๋ชฉ๊ต)(์•ˆ)", "๊ฒ€์ธก (๊ฐœ์ฐฉํ„ฐ๋„, ์ƒํƒœํ†ต๋กœ)"], - "ํฌ์žฅ๊ณต": ["๊ฒ€์ธก (๊ธฐ์ธต, ๋ณด์กฐ๊ธฐ์ธต)"], - "๋ถ€๋Œ€๊ณต": ["๊ฒ€์ธก (ํ™˜๊ฒฝ)", "๊ฒ€์ธก (์ง€์žฅ๊ฐ€์˜ฅ,๊ฑด๋ฌผ ์ฒ ๊ฑฐ)", "๊ฒ€์ธก (๋ฐฉ์Œ๋ฒฝ ๋“ฑ)"], - "๋น„ํƒˆ๋ฉด์•ˆ์ „๊ณต": ["๊ฒ€์ธก (์‹์ƒ๋ณดํ˜ธ๊ณต)", "๊ฒ€์ธก (๊ตฌ์กฐ๋ฌผ๋ณดํ˜ธ๊ณต)"], - "๊ตํ†ต์•ˆ์ „์‹œ์„ค๊ณต": ["๊ฒ€์ธก (๋‚™์„๋ฐฉ์ง€์ฑ…)"], - "๊ฒ€์ธก ์–‘์‹์„œ๋ฅ˜": ["๊ฒ€์ธก ์–‘์‹์„œ๋ฅ˜"] - }, - "์„ค๊ณ„๋ณ€๊ฒฝ": { - "์‹ค์ •๋ณด๊ณ (์–ด์ฒœ~๊ณต์ฃผ)": ["ํ† ๊ณต", "๋ฐฐ์ˆ˜๊ณต", "๊ต๋Ÿ‰๊ณต(ํ‰๋ชฉ๊ต)", "๊ตฌ์กฐ๋ฌผ๊ณต", "ํฌ์žฅ๊ณต", "๊ตํ†ต์•ˆ์ „๊ณต", "๋ถ€๋Œ€๊ณต", "์ „๊ธฐ๊ณต์‚ฌ", "๋ฏธํ™•์ •๊ณต", "์•ˆ์ „๊ด€๋ฆฌ", "ํ™˜๊ฒฝ๊ด€๋ฆฌ", "ํ’ˆ์งˆ๊ด€๋ฆฌ", "์ž์žฌ๊ด€๋ฆฌ", "์ง€์žฅ๋ฌผ", "๊ธฐํƒ€"], - "์‹ค์ •๋ณด๊ณ (๋Œ€์ˆ ~์ •์•ˆ)": ["ํ† ๊ณต", "๋ฐฐ์ˆ˜๊ณต", "๋น„ํƒˆ๋ฉด์•ˆ์ „๊ณต", "ํฌ์žฅ๊ณต", "๋ถ€๋Œ€๊ณต", "์•ˆ์ „๊ด€๋ฆฌ", "ํ™˜๊ฒฝ๊ด€๋ฆฌ", "์ž์žฌ๊ด€๋ฆฌ", "๊ธฐํƒ€"], - "๊ธฐ์ˆ ์ง€์› ๊ฒ€ํ† ": ["ํ† ๊ณต", "๋ฐฐ์ˆ˜๊ณต", "๊ต๋Ÿ‰๊ณต(ํ‰๋ชฉ๊ต)", "๊ตฌ์กฐ๋ฌผ&๋ถ€๋Œ€๊ณต", "๊ธฐํƒ€"], - "์‹œ๊ณต๊ณ„ํš(์–ด์ฒœ~๊ณต์ฃผ)": ["ํ† ๊ณต", "๋ฐฐ์ˆ˜๊ณต", "๊ต๋Ÿ‰๊ณต(ํ‰๋ชฉ๊ต)", "๊ตฌ์กฐ๋ฌผ&๋ถ€๋Œ€&ํฌ์žฅ&๊ตํ†ต์•ˆ์ „๊ณต", "ํ™˜๊ฒฝ ๋ฐ ํ’ˆ์งˆ๊ด€๋ฆฌ"] - }, - "๊ณต์‚ฌ๊ด€๋ฆฌ": { - "๊ณต์ •ยท์ผ์ •": ["๊ณต์ •ํ‘œ", "์›”๊ฐ„ ๊ณต์ •๋ณด๊ณ ", "์ž‘์—…์ผ๋ณด"], - "ํ’ˆ์งˆ ๊ด€๋ฆฌ": ["ํ’ˆ์งˆ์‹œํ—˜๊ณ„ํš์„œ", "ํ’ˆ์งˆ์‹œํ—˜ ์‹ค์ ๋ณด๊ณ ", "์ฝ˜ํฌ๋ฆฌํŠธ ํƒ€์„คํ˜„ํ™ฉ[์–ด์ฒœ~๊ณต์ฃผ(4์ฐจ)]", "ํ’ˆ์งˆ๊ด€๋ฆฌ๋น„ ์‚ฌ์šฉ๋‚ด์—ญ", "๊ท ์—ด๊ด€๋ฆฌ", "ํ’ˆ์งˆ๊ด€๋ฆฌ ์–‘์‹์„œ๋ฅ˜"], - "์•ˆ์ „ ๊ด€๋ฆฌ": ["์•ˆ์ „๊ด€๋ฆฌ๊ณ„ํš์„œ", "์•ˆ์ „๊ด€๋ฆฌ ์‹ค์ ๋ณด๊ณ ", "์œ„ํ—˜์„ฑ ํ‰๊ฐ€", "์‚ฌ์ „์ž‘์—…ํ—ˆ๊ฐ€์„œ", "์•ˆ์ „๊ด€๋ฆฌ๋น„ ์‚ฌ์šฉ๋‚ด์—ญ", "์•ˆ์ „๊ด€๋ฆฌ์ˆ˜์ค€ํ‰๊ฐ€", "์•ˆ์ „๊ด€๋ฆฌ ์–‘์‹์„œ๋ฅ˜"], - "ํ™˜๊ฒฝ ๊ด€๋ฆฌ": ["ํ™˜๊ฒฝ์˜ํ–ฅํ‰๊ฐ€", "์‚ฌ์ „์žฌํ•ด์˜ํ–ฅ์„ฑ๊ฒ€ํ† ", "์œ ์ง€๊ด€๋ฆฌ ๋ฐ ๋ณด์ˆ˜์ ๊ฒ€", "ํ™˜๊ฒฝ๋ณด์ „๋น„ ์‚ฌ์šฉ๋‚ด์—ญ", "๊ฑด์„คํ๊ธฐ๋ฌผ ๊ด€๋ฆฌ"], - "์ž์žฌ ๊ด€๋ฆฌ (๊ด€๊ธ‰)": ["์ž์žฌ๊ตฌ๋งค์š”์ฒญ (๋ ˆ๋ฏธ์ฝ˜, ์ฒ ๊ทผ)", "์ž์žฌ๊ตฌ๋งค์š”์ฒญ (๊ทธ ์™ธ)", "๋‚ฉํ’ˆ๊ธฐํ•œ", "๊ณ„์•ฝ ๋ณ€๊ฒฝ", "์ž์žฌ ๋ฐ˜์ž…ยท์ˆ˜๋ถˆ ๊ด€๋ฆฌ", "์ž์žฌ๊ด€๋ฆฌ ์–‘์‹์„œ๋ฅ˜"], - "์ž์žฌ ๊ด€๋ฆฌ (์‚ฌ๊ธ‰)": ["์ž์žฌ๊ณต๊ธ‰์› ์Šน์ธ", "์ž์žฌ ๋ฐ˜์ž…ยท์ˆ˜๋ถˆ ๊ด€๋ฆฌ", "์ž์žฌ ๊ฒ€์ˆ˜ยทํ™•์ธ"], - "์ ๊ฒ€ (์ •๋ฆฌ์ค‘)": ["๋‚ด๋ถ€์ ๊ฒ€", "์™ธ๋ถ€์ ๊ฒ€"], - "๊ณต๋ฌธ": ["์ ‘์ˆ˜(์ˆ˜์‹ )", "๋ฐœ์†ก(๋ฐœ์‹ )", "ํ•˜๋„๊ธ‰", "์ธ๋ ฅ", "๋ฐฉ์นจ"] - }, - "๋ฏผ์›๊ด€๋ฆฌ": { - "๋ฏผ์›(์–ด์ฒœ~๊ณต์ฃผ)": ["์ฒ˜๋ฆฌ๋Œ€์žฅ", "๋ณด์ƒ", "๊ณต์‚ฌ์ผ๋ฐ˜", "ํ™˜๊ฒฝ๋ถ„์Ÿ"], - "์‹ค์ •๋ณด๊ณ (์–ด์ฒœ~๊ณต์ฃผ)": ["๋ฏผ์›"], - "์‹ค์ •๋ณด๊ณ (๋Œ€์ˆ ~์ •์•ˆ)": ["๋ฏผ์›"] - } -} - -def analyze_flow_reasoning(filename, all_text_list): - """ - ๋ณธ๋ฌธ์˜ ์ „์ˆ˜ ์กฐ์‚ฌ ๊ฒฐ๊ณผ์— ํŒŒ์ผ๋ช…์˜ '์˜๋„ ๊ฐ€์ค‘์น˜'๋ฅผ ๋”ํ•ด ์ตœ์ข… ์ถ”๋ก  - """ - full_text = " ".join(all_text_list) - clean_ctx = full_text.replace(" ", "").replace("\n", "").lower() - fn_clean = filename.replace(" ", "").lower() - - # 1. ๋„๋ฉ”์ธ๋ณ„ ๊ธฐ๋ณธ ์ ์ˆ˜ (๋ณธ๋ฌธ ์ „์ˆ˜ ์กฐ์‚ฌ - ํ‰๋“ฑํ•˜๊ฒŒ) - scores = { - "official": sum(clean_ctx.count(k) for k in ["์ˆ˜์‹ :", "๋ฐœ์‹ :", "๊ฒฝ์œ :", "์‹œํ–‰์ผ์ž", "๊ท€ํ•˜", "๋“œ๋ฆฝ๋‹ˆ๋‹ค", "๋ฐ”๋ž๋‹ˆ๋‹ค"]), - "contract": sum(clean_ctx.count(k) for k in ["๊ณ„์•ฝ์„œ", "ํ•˜๋„๊ธ‰", "์™ธ์ฃผ", "๋„๊ธ‰", "์ธ๊ฐ", "์‚ฌ์—…์ž"]), - "hr": sum(clean_ctx.count(k) for k in ["์ดํƒˆ๊ณ„", "์ธ๋ ฅ", "๊ธฐ์ˆ ์ž", "์•ˆ์ „๊ด€๋ฆฌ์ž", "์žฌ์ง์ฆ๋ช…", "๋ฐฐ์น˜"]), - "change": sum(clean_ctx.count(k) for k in ["์‹ค์ •๋ณด๊ณ ", "์„ค๊ณ„๋ณ€๊ฒฝ", "๋ณ€๊ฒฝ๋ณด๊ณ ", "์ถ”๊ฐ€๋ฐ˜์˜"]), - "technical": sum(clean_ctx.count(k) for k in ["์ผ์œ„๋Œ€๊ฐ€", "์‚ฐ์ถœ๊ทผ๊ฑฐ", "์ง‘๊ณ„ํ‘œ", "๋ฌผ๋Ÿ‰์‚ฐ์ถœ", "๋‹จ๊ฐ€", "๋‚ด์—ญ", "๋„๋ฉด", "dwg"]) - } - - # 2. ํŒŒ์ผ๋ช…์— ๋Œ€ํ•œ '๋ฐฉํ–ฅํƒ€' ๊ฐ€์ค‘์น˜ ๋ถ€์—ฌ (Final Push) - # ๋ณธ๋ฌธ ๋ฐ์ดํ„ฐ๊ฐ€ ์•„๋ฌด๋ฆฌ ๋งŽ์•„๋„ ํŒŒ์ผ๋ช…์˜ ์˜๋„๋ฅผ ์กด์ค‘ํ•˜๊ธฐ ์œ„ํ•ด 7๋ฐฐ ๊ฐ€์ค‘์น˜ - if "์‹ค์ •" in fn_clean or "๋ณ€๊ฒฝ" in fn_clean: scores["change"] += 50 # ๋ณธ๋ฌธ 50ํšŒ ์–ธ๊ธ‰๊ณผ ๋งž๋จน๋Š” ๊ฐ€์ค‘์น˜ - if "๊ณ„์•ฝ" in fn_clean or "ํ•˜๋„๊ธ‰" in fn_clean: scores["contract"] += 50 - if "์ธ๋ ฅ" in fn_clean or "์ดํƒˆ" in fn_clean: scores["hr"] += 50 - if "๋‹จ๊ฐ€" in fn_clean or "์ˆ˜๋Ÿ‰" in fn_clean or "๋„๋ฉด" in fn_clean: scores["technical"] += 50 - if "์ œ์ถœ" in fn_clean or "๊ฑด" in fn_clean: scores["official"] += 30 - - # 3. ์ข…ํ•ฉ ๋†๋„์— ๋”ฐ๋ฅธ ์ตœ์ข… ๋„๋ฉ”์ธ ์„ ์ • - dominant_domain = max(scores, key=scores.get) - - # ํ”„๋กœ์ ํŠธ ์‹๋ณ„ (Fuzzy ๋งค์นญ ๋ฐ ๊ต์ฐจ ๊ฒ€์ฆ) - project_loc = "์–ด์ฒœ~๊ณต์ฃผ" if any(k in clean_ctx or k in fn_clean for k in ["์–ด์ฒœ", "๊ณต์ฃผ"]) else "๋Œ€์ˆ ~์ •์•ˆ" if any(k in clean_ctx or k in fn_clean for k in ["๋Œ€์ˆ ", "์ •์•ˆ"]) else "๊ณตํ†ต" - - # --- [ํ†ตํ•ฉ ์ถ”๋ก  ๋ฐ ๋งค์นญ] --- - - # ์‹œ๋‚˜๋ฆฌ์˜ค A: ์‹ค์ •๋ณด๊ณ /์„ค๊ณ„๋ณ€๊ฒฝ (๋ณธ๋ฌธ ๋ฐ์ดํ„ฐ + ํŒŒ์ผ๋ช… ์˜๋„ ํ•ฉ์„ฑ) - if dominant_domain == "change" or (scores["change"] > 0 and scores["technical"] > 5): - cat = f"์‹ค์ •๋ณด๊ณ ({project_loc})" - sub = "์ง€์žฅ๋ฌผ" if any(k in clean_ctx for k in ["์ž„๋Œ€๋ฃŒ", "ํ† ์ง€", "๋ณด์ƒ"]) else "๊ตฌ์กฐ๋ฌผ๊ณต" if "๊ตฌ์กฐ๋ฌผ" in clean_ctx else "๊ธฐํƒ€" - return f"์„ค๊ณ„๋ณ€๊ฒฝ > {cat} > {sub}", f"๋ณธ๋ฌธ์˜ ๊ธฐ์ˆ  ๋ฐ์ดํ„ฐ ๋ฐ€๋„์™€ ํŒŒ์ผ๋ช…์˜ '{dominant_domain}' ๊ด€๋ จ ์˜๋„๋ฅผ ์ข…ํ•ฉํ•˜์—ฌ {project_loc} ํ”„๋กœ์ ํŠธ์˜ ์‹ค์ •๋ณด๊ณ  ๋ณธ์ฒด๋กœ ํŒ์ •." - - # ์‹œ๋‚˜๋ฆฌ์˜ค B: ํ–‰์ • ๊ณ„์•ฝ/ํ•˜๋„๊ธ‰ (๋ณธ์ฒด ์ค‘์‹ฌ) - if dominant_domain == "contract": - return "ํ–‰์ • > ๊ณ„์•ฝ > ๊ณ„์•ฝ๊ด€๋ฆฌ", "๋ฌธ์„œ ์ „์ฒด์—์„œ ๊ณ„์•ฝ ๋ฐ ํ•˜๋„๊ธ‰ ์—…๋ฌด ๋ณธ์งˆ์ด ์ง€๋ฐฐ์ ์œผ๋กœ ํ™•์ธ๋จ." - - # ์‹œ๋‚˜๋ฆฌ์˜ค C: ์ธ์‚ฌ/์ธ๋ ฅ ๊ด€๋ฆฌ - if dominant_domain == "hr": - if len(all_text_list) <= 2: return "๊ณต์‚ฌ๊ด€๋ฆฌ > ๊ณต๋ฌธ > ์ธ๋ ฅ", "์ธ๋ ฅ ์‚ฌํ•ญ์„ ๊ฐ„๋žตํžˆ ๋ณด๊ณ ํ•˜๋Š” ๊ณต๋ฌธ ํ˜•์‹์ž„." - return "ํ–‰์ • > ๊ณ„์•ฝ > ์ธ์›๊ด€๋ฆฌ", "๋‹ค๋Ÿ‰์˜ ์ธ๋ ฅ ์ฆ๋น™ ๋ฐ์ดํ„ฐ๊ฐ€ ํฌํ•จ๋œ ํ–‰์ • ์„œ๋ฅ˜์ž„." - - # ์‹œ๋‚˜๋ฆฌ์˜ค D: ์ˆœ์ˆ˜ ๊ณต๋ฌธ (ํ˜•์‹ ์šฐ์„ ) - if dominant_domain == "official" or scores["official"] > scores["technical"]: - tab, cat = "๊ณต์‚ฌ๊ด€๋ฆฌ", "๊ณต๋ฌธ" - sub = "์ ‘์ˆ˜(์ˆ˜์‹ )" - if "๋ฐฉ์นจ" in clean_ctx or "์ง€์นจ" in clean_ctx: sub = "๋ฐฉ์นจ" - elif "๋ฐœ์‹ " in clean_ctx[:500]: sub = "๋ฐœ์†ก(๋ฐœ์‹ )" - return f"{tab} > {cat} > {sub}", "์ „์ฒด ๋งฅ๋ฝ์ƒ ๊ธฐ์ˆ ์  ๋ฐ์ดํ„ฐ๋ณด๋‹ค ํ–‰์ •์  ์ „๋‹ฌ ํ–‰์œ„(๊ณต๋ฌธ)๊ฐ€ ํ•ต์‹ฌ ์ •์ฒด์„ฑ์œผ๋กœ ํŒ๋‹จ๋จ." - - # ์‹œ๋‚˜๋ฆฌ์˜ค E: ๊ธฐ์ˆ  ์„ฑ๊ณผํ’ˆ - if dominant_domain == "technical": - if any(k in clean_ctx or k in fn_clean for k in ["๋‹จ๊ฐ€", "๋‚ด์—ญ"]): return "์„ค๊ณ„์„ฑ๊ณผํ’ˆ > ๋‚ด์—ญ์„œ > ๋‹จ๊ฐ€์‚ฐ์ถœ์„œ", "๋‚ด์—ญ/๋‹จ๊ฐ€ ์‚ฐ์ถœ ๊ธฐ์ˆ  ๋ฐ์ดํ„ฐ ํ™•์ธ." - if any(k in clean_ctx or k in fn_clean for k in ["๋„๋ฉด", "dwg"]): return "์„ค๊ณ„์„ฑ๊ณผํ’ˆ > ์„ค๊ณ„๋„๋ฉด > ๊ณตํ†ต", "๋„๋ฉด/๊ทธ๋ž˜ํ”ฝ ๋ฐ์ดํ„ฐ ํ™•์ธ." - return "์„ค๊ณ„์„ฑ๊ณผํ’ˆ > ์ˆ˜๋Ÿ‰์‚ฐ์ถœ์„œ > ํ† ๊ณต", "์ˆ˜๋Ÿ‰/๋ฌผ๋Ÿ‰ ์‚ฐ์ถœ ๋ฐ์ดํ„ฐ ํ™•์ธ." - - return "ํ–‰์ • > ์—…๋ฌด๊ด€๋ฆฌ > ์–‘์‹์„œ๋ฅ˜", "์ผ๋ฐ˜ ํ–‰์ • ๋ฐ ๊ธฐํƒ€ ์–‘์‹ ์„œ๋ฅ˜๋กœ ๋ถ„๋ฅ˜ํ•จ." - -def analyze_file_content(filename: str): - try: - file_path = os.path.join("sample", filename) - text_by_pages = [] - if filename.lower().endswith(".pdf"): - reader = PdfReader(file_path) - for i in range(len(reader.pages)): - page_text = reader.pages[i].extract_text() or "" - if OCR_AVAILABLE: - try: - images = convert_from_path(file_path, first_page=i+1, last_page=i+1, poppler_path=POPPLER_BIN, dpi=200) - if images: - ocr_result = pytesseract.image_to_string(images[0], lang='kor+eng') - page_text += "\n" + ocr_result - except Exception as ocr_err: - print(f"OCR Error on page {i+1}: {ocr_err}") - text_by_pages.append(page_text) - elif filename.lower().endswith(('.xlsx', '.xls')): - import pandas as pd - df = pd.read_excel(file_path) - text_by_pages.append(df.to_string()) - else: text_by_pages.append("") - - path, reason = analyze_flow_reasoning(filename, text_by_pages) - - return { - "filename": filename, - "total_pages": len(text_by_pages), - "final_result": { - "suggested_path": path, - "confidence": "100%", - "reason": reason, - "snippet": " ".join(text_by_pages)[:1500] - } - } - except Exception as e: - return {"error": str(e), "filename": filename} +import os +import re +import unicodedata +from pypdf import PdfReader +import pytesseract +from pdf2image import convert_from_path + +# 1. ์‹œ์Šคํ…œ ์„ค์ • +TESSERACT_EXE = r'C:\Users\User\AppData\Local\Programs\Tesseract-OCR\tesseract.exe' +TESSDATA_DIR = r'C:\Users\User\AppData\Local\Programs\Tesseract-OCR\tessdata' +POPPLER_BIN = r'D:\์ดํƒœํ›ˆ\00ํฌ๋กฌ๋‹ค์šด๋กœ๋“œ\poppler-25.12.0\Library\bin' + +pytesseract.pytesseract.tesseract_cmd = TESSERACT_EXE +os.environ["TESSDATA_PREFIX"] = TESSDATA_DIR +OCR_AVAILABLE = os.path.exists(TESSERACT_EXE) + +SYSTEM_HIERARCHY = { + "ํ–‰์ •": { + "๊ณ„์•ฝ": ["๊ณ„์•ฝ๊ด€๋ฆฌ", "๊ธฐ์„ฑ๊ด€๋ฆฌ", "์—…๋ฌด์ง€์‹œ์„œ", "์ธ์›๊ด€๋ฆฌ"], + "์—…๋ฌด๊ด€๋ฆฌ": ["์—…๋ฌด์ผ์ง€(2025)", "์—…๋ฌด์ผ์ง€(2025๋…„ ์ด์ „)", "๋ฐœ์ฃผ์ฒ˜ ์ •๊ธฐ๋ณด๊ณ ", "๋ณธ์‚ฌ์—…๋ฌด๋ณด๊ณ ", "๊ณต์‚ฌ๊ฐ๋…์ผ์ง€", "์–‘์‹์„œ๋ฅ˜"] + }, + "์„ค๊ณ„์„ฑ๊ณผํ’ˆ": { + "์‹œ๋ฐฉ์„œ": ["๊ณต์‚ฌ์‹œ๋ฐฉ์„œ", "์žฅ๋น„ ๋ฐ˜์ž…ํ—ˆ๊ฐ€ ๊ฒ€ํ† ์„œ"], + "์„ค๊ณ„๋„๋ฉด": ["๊ณตํ†ต", "ํ† ๊ณต", "๋น„ํƒˆ๋ฉด์•ˆ์ „๊ณต", "๋ฐฐ์ˆ˜๊ณต", "๊ต๋Ÿ‰๊ณต", "ํฌ์žฅ๊ณต", "๊ตํ†ต์•ˆ์ „์‹œ์„ค๊ณต", "๋ถ€๋Œ€๊ณต", "์šฉ์ง€๊ณต & ๊ธฐํƒ€๊ณต"], + "์ˆ˜๋Ÿ‰์‚ฐ์ถœ์„œ": ["ํ† ๊ณต", "๋น„ํƒˆ๋ฉด์•ˆ์ „๊ณต", "๋ฐฐ์ˆ˜๊ณต", "๊ต๋Ÿ‰๊ณต", "ํฌ์žฅ๊ณต", "๊ตํ†ต์•ˆ์ „์‹œ์„ค๊ณต", "๋ถ€๋Œ€๊ณต", "์šฉ์ง€๊ณต & ๊ธฐํƒ€๊ณต"], + "๋‚ด์—ญ์„œ": ["๋‹จ๊ฐ€์‚ฐ์ถœ์„œ"], + "๋ณด๊ณ ์„œ": ["์‹ค์‹œ์„ค๊ณ„๋ณด๊ณ ์„œ", "์ง€๋ฐ˜์กฐ์‚ฌ๋ณด๊ณ ์„œ", "๊ตฌ์กฐ๊ณ„์‚ฐ์„œ", "์ˆ˜๋ฆฌ ๋ฐ ์ „๊ธฐ๊ณ„์‚ฐ์„œ", "๊ธฐํƒ€๋ณด๊ณ ์„œ", "๊ธฐ์ˆ ์ž๋ฌธ ๋ฐ ์‹ฌ์˜"], + "์ธก๋Ÿ‰๊ณ„์‚ฐ๋ถ€": ["์ธก๋Ÿ‰๊ณ„์‚ฐ๋ถ€"], + "์„ค๊ณ„๋‹จ๊ณ„ ์ˆ˜ํ–‰ํ˜‘์˜": ["ํšŒ์˜ยทํ˜‘์˜"] + }, + "์‹œ๊ณต์„ฑ๊ณผํ’ˆ": { + "์„ค๊ณ„๋„๋ฉด": ["๊ณตํ†ต", "ํ† ๊ณต", "๋น„ํƒˆ๋ฉด์•ˆ์ „๊ณต", "๋ฐฐ์ˆ˜๊ณต", "๊ต๋Ÿ‰๊ณต", "ํฌ์žฅ๊ณต", "๊ตํ†ต์•ˆ์ „์‹œ์„ค๊ณต", "๋ถ€๋Œ€๊ณต", "์šฉ์ง€๊ณต & ๊ธฐํƒ€๊ณต"] + }, + "์‹œ๊ณต๊ฒ€์ธก": { + "ํ† ๊ณต": ["๊ฒ€์ธก (๊นจ๊ธฐ)", "๊ฒ€์ธก (์—ฐ์•ฝ์ง€๋ฐ˜)", "๊ฒ€์ธก (๋ฐœํŒŒ)", "๊ฒ€์ธก (๋…ธ์ฒด)", "๊ฒ€์ธก (๋…ธ์ƒ)", "๊ฒ€์ธก (ํ† ์ทจ์žฅ)"], + "๋ฐฐ์ˆ˜๊ณต": ["๊ฒ€์ธก (Vํ˜•์ธก๊ตฌ)", "๊ฒ€์ธก (์‚ฐ๋งˆ๋ฃจ์ธก๊ตฌ)", "๊ฒ€์ธก (Uํ˜•์ธก๊ตฌ)", "๊ฒ€์ธก (Uํ˜•์ธก๊ตฌ)(์•ˆ)", "๊ฒ€์ธก (Lํ˜•์ธก๊ตฌ, Jํ˜•์ธก๊ตฌ)", "๊ฒ€์ธก (๋„์ˆ˜๋กœ)", "๊ฒ€์ธก (๋„์ˆ˜๋กœ)(์•ˆ)", "๊ฒ€์ธก (ํšก๋ฐฐ์ˆ˜๊ด€)", "๊ฒ€์ธก (์ข…๋ฐฐ์ˆ˜๊ด€)", "๊ฒ€์ธก (๋งน์•”๊ฑฐ)", "๊ฒ€์ธก (ํ†ต๋กœ์•”๊ฑฐ)", "๊ฒ€์ธก (์ˆ˜๋กœ์•”๊ฑฐ)", "๊ฒ€์ธก (ํ˜ธ์•ˆ๊ณต)", "๊ฒ€์ธก (์˜น๋ฒฝ๊ณต)", "๊ฒ€์ธก (์šฉ์ˆ˜๊ฐœ๊ฑฐ)"], + "๊ตฌ์กฐ๋ฌผ๊ณต": ["๊ฒ€์ธก (ํ‰๋ชฉ๊ต-๊ฑฐ๋”, ๋ถ€๋Œ€๊ณต)", "๊ฒ€์ธก (ํ‰๋ชฉ๊ต)(์•ˆ)", "๊ฒ€์ธก (๊ฐœ์ฐฉํ„ฐ๋„, ์ƒํƒœํ†ต๋กœ)"], + "ํฌ์žฅ๊ณต": ["๊ฒ€์ธก (๊ธฐ์ธต, ๋ณด์กฐ๊ธฐ์ธต)"], + "๋ถ€๋Œ€๊ณต": ["๊ฒ€์ธก (ํ™˜๊ฒฝ)", "๊ฒ€์ธก (์ง€์žฅ๊ฐ€์˜ฅ,๊ฑด๋ฌผ ์ฒ ๊ฑฐ)", "๊ฒ€์ธก (๋ฐฉ์Œ๋ฒฝ ๋“ฑ)"], + "๋น„ํƒˆ๋ฉด์•ˆ์ „๊ณต": ["๊ฒ€์ธก (์‹์ƒ๋ณดํ˜ธ๊ณต)", "๊ฒ€์ธก (๊ตฌ์กฐ๋ฌผ๋ณดํ˜ธ๊ณต)"], + "๊ตํ†ต์•ˆ์ „์‹œ์„ค๊ณต": ["๊ฒ€์ธก (๋‚™์„๋ฐฉ์ง€์ฑ…)"], + "๊ฒ€์ธก ์–‘์‹์„œ๋ฅ˜": ["๊ฒ€์ธก ์–‘์‹์„œ๋ฅ˜"] + }, + "์„ค๊ณ„๋ณ€๊ฒฝ": { + "์‹ค์ •๋ณด๊ณ (์–ด์ฒœ~๊ณต์ฃผ)": ["ํ† ๊ณต", "๋ฐฐ์ˆ˜๊ณต", "๊ต๋Ÿ‰๊ณต(ํ‰๋ชฉ๊ต)", "๊ตฌ์กฐ๋ฌผ๊ณต", "ํฌ์žฅ๊ณต", "๊ตํ†ต์•ˆ์ „๊ณต", "๋ถ€๋Œ€๊ณต", "์ „๊ธฐ๊ณต์‚ฌ", "๋ฏธํ™•์ •๊ณต", "์•ˆ์ „๊ด€๋ฆฌ", "ํ™˜๊ฒฝ๊ด€๋ฆฌ", "ํ’ˆ์งˆ๊ด€๋ฆฌ", "์ž์žฌ๊ด€๋ฆฌ", "์ง€์žฅ๋ฌผ", "๊ธฐํƒ€"], + "์‹ค์ •๋ณด๊ณ (๋Œ€์ˆ ~์ •์•ˆ)": ["ํ† ๊ณต", "๋ฐฐ์ˆ˜๊ณต", "๋น„ํƒˆ๋ฉด์•ˆ์ „๊ณต", "ํฌ์žฅ๊ณต", "๋ถ€๋Œ€๊ณต", "์•ˆ์ „๊ด€๋ฆฌ", "ํ™˜๊ฒฝ๊ด€๋ฆฌ", "์ž์žฌ๊ด€๋ฆฌ", "๊ธฐํƒ€"], + "๊ธฐ์ˆ ์ง€์› ๊ฒ€ํ† ": ["ํ† ๊ณต", "๋ฐฐ์ˆ˜๊ณต", "๊ต๋Ÿ‰๊ณต(ํ‰๋ชฉ๊ต)", "๊ตฌ์กฐ๋ฌผ&๋ถ€๋Œ€๊ณต", "๊ธฐํƒ€"], + "์‹œ๊ณต๊ณ„ํš(์–ด์ฒœ~๊ณต์ฃผ)": ["ํ† ๊ณต", "๋ฐฐ์ˆ˜๊ณต", "๊ต๋Ÿ‰๊ณต(ํ‰๋ชฉ๊ต)", "๊ตฌ์กฐ๋ฌผ&๋ถ€๋Œ€&ํฌ์žฅ&๊ตํ†ต์•ˆ์ „๊ณต", "ํ™˜๊ฒฝ ๋ฐ ํ’ˆ์งˆ๊ด€๋ฆฌ"] + }, + "๊ณต์‚ฌ๊ด€๋ฆฌ": { + "๊ณต์ •ยท์ผ์ •": ["๊ณต์ •ํ‘œ", "์›”๊ฐ„ ๊ณต์ •๋ณด๊ณ ", "์ž‘์—…์ผ๋ณด"], + "ํ’ˆ์งˆ ๊ด€๋ฆฌ": ["ํ’ˆ์งˆ์‹œํ—˜๊ณ„ํš์„œ", "ํ’ˆ์งˆ์‹œํ—˜ ์‹ค์ ๋ณด๊ณ ", "์ฝ˜ํฌ๋ฆฌํŠธ ํƒ€์„คํ˜„ํ™ฉ[์–ด์ฒœ~๊ณต์ฃผ(4์ฐจ)]", "ํ’ˆ์งˆ๊ด€๋ฆฌ๋น„ ์‚ฌ์šฉ๋‚ด์—ญ", "๊ท ์—ด๊ด€๋ฆฌ", "ํ’ˆ์งˆ๊ด€๋ฆฌ ์–‘์‹์„œ๋ฅ˜"], + "์•ˆ์ „ ๊ด€๋ฆฌ": ["์•ˆ์ „๊ด€๋ฆฌ๊ณ„ํš์„œ", "์•ˆ์ „๊ด€๋ฆฌ ์‹ค์ ๋ณด๊ณ ", "์œ„ํ—˜์„ฑ ํ‰๊ฐ€", "์‚ฌ์ „์ž‘์—…ํ—ˆ๊ฐ€์„œ", "์•ˆ์ „๊ด€๋ฆฌ๋น„ ์‚ฌ์šฉ๋‚ด์—ญ", "์•ˆ์ „๊ด€๋ฆฌ์ˆ˜์ค€ํ‰๊ฐ€", "์•ˆ์ „๊ด€๋ฆฌ ์–‘์‹์„œ๋ฅ˜"], + "ํ™˜๊ฒฝ ๊ด€๋ฆฌ": ["ํ™˜๊ฒฝ์˜ํ–ฅํ‰๊ฐ€", "์‚ฌ์ „์žฌํ•ด์˜ํ–ฅ์„ฑ๊ฒ€ํ† ", "์œ ์ง€๊ด€๋ฆฌ ๋ฐ ๋ณด์ˆ˜์ ๊ฒ€", "ํ™˜๊ฒฝ๋ณด์ „๋น„ ์‚ฌ์šฉ๋‚ด์—ญ", "๊ฑด์„คํ๊ธฐ๋ฌผ ๊ด€๋ฆฌ"], + "์ž์žฌ ๊ด€๋ฆฌ (๊ด€๊ธ‰)": ["์ž์žฌ๊ตฌ๋งค์š”์ฒญ (๋ ˆ๋ฏธ์ฝ˜, ์ฒ ๊ทผ)", "์ž์žฌ๊ตฌ๋งค์š”์ฒญ (๊ทธ ์™ธ)", "๋‚ฉํ’ˆ๊ธฐํ•œ", "๊ณ„์•ฝ ๋ณ€๊ฒฝ", "์ž์žฌ ๋ฐ˜์ž…ยท์ˆ˜๋ถˆ ๊ด€๋ฆฌ", "์ž์žฌ๊ด€๋ฆฌ ์–‘์‹์„œ๋ฅ˜"], + "์ž์žฌ ๊ด€๋ฆฌ (์‚ฌ๊ธ‰)": ["์ž์žฌ๊ณต๊ธ‰์› ์Šน์ธ", "์ž์žฌ ๋ฐ˜์ž…ยท์ˆ˜๋ถˆ ๊ด€๋ฆฌ", "์ž์žฌ ๊ฒ€์ˆ˜ยทํ™•์ธ"], + "์ ๊ฒ€ (์ •๋ฆฌ์ค‘)": ["๋‚ด๋ถ€์ ๊ฒ€", "์™ธ๋ถ€์ ๊ฒ€"], + "๊ณต๋ฌธ": ["์ ‘์ˆ˜(์ˆ˜์‹ )", "๋ฐœ์†ก(๋ฐœ์‹ )", "ํ•˜๋„๊ธ‰", "์ธ๋ ฅ", "๋ฐฉ์นจ"] + }, + "๋ฏผ์›๊ด€๋ฆฌ": { + "๋ฏผ์›(์–ด์ฒœ~๊ณต์ฃผ)": ["์ฒ˜๋ฆฌ๋Œ€์žฅ", "๋ณด์ƒ", "๊ณต์‚ฌ์ผ๋ฐ˜", "ํ™˜๊ฒฝ๋ถ„์Ÿ"], + "์‹ค์ •๋ณด๊ณ (์–ด์ฒœ~๊ณต์ฃผ)": ["๋ฏผ์›"], + "์‹ค์ •๋ณด๊ณ (๋Œ€์ˆ ~์ •์•ˆ)": ["๋ฏผ์›"] + } +} + +def analyze_flow_reasoning(filename, all_text_list): + """ + ๋ณธ๋ฌธ์˜ ์ „์ˆ˜ ์กฐ์‚ฌ ๊ฒฐ๊ณผ์— ํŒŒ์ผ๋ช…์˜ '์˜๋„ ๊ฐ€์ค‘์น˜'๋ฅผ ๋”ํ•ด ์ตœ์ข… ์ถ”๋ก  + """ + full_text = " ".join(all_text_list) + clean_ctx = full_text.replace(" ", "").replace("\n", "").lower() + fn_clean = filename.replace(" ", "").lower() + + # 1. ๋„๋ฉ”์ธ๋ณ„ ๊ธฐ๋ณธ ์ ์ˆ˜ (๋ณธ๋ฌธ ์ „์ˆ˜ ์กฐ์‚ฌ - ํ‰๋“ฑํ•˜๊ฒŒ) + scores = { + "official": sum(clean_ctx.count(k) for k in ["์ˆ˜์‹ :", "๋ฐœ์‹ :", "๊ฒฝ์œ :", "์‹œํ–‰์ผ์ž", "๊ท€ํ•˜", "๋“œ๋ฆฝ๋‹ˆ๋‹ค", "๋ฐ”๋ž๋‹ˆ๋‹ค"]), + "contract": sum(clean_ctx.count(k) for k in ["๊ณ„์•ฝ์„œ", "ํ•˜๋„๊ธ‰", "์™ธ์ฃผ", "๋„๊ธ‰", "์ธ๊ฐ", "์‚ฌ์—…์ž"]), + "hr": sum(clean_ctx.count(k) for k in ["์ดํƒˆ๊ณ„", "์ธ๋ ฅ", "๊ธฐ์ˆ ์ž", "์•ˆ์ „๊ด€๋ฆฌ์ž", "์žฌ์ง์ฆ๋ช…", "๋ฐฐ์น˜"]), + "change": sum(clean_ctx.count(k) for k in ["์‹ค์ •๋ณด๊ณ ", "์„ค๊ณ„๋ณ€๊ฒฝ", "๋ณ€๊ฒฝ๋ณด๊ณ ", "์ถ”๊ฐ€๋ฐ˜์˜"]), + "technical": sum(clean_ctx.count(k) for k in ["์ผ์œ„๋Œ€๊ฐ€", "์‚ฐ์ถœ๊ทผ๊ฑฐ", "์ง‘๊ณ„ํ‘œ", "๋ฌผ๋Ÿ‰์‚ฐ์ถœ", "๋‹จ๊ฐ€", "๋‚ด์—ญ", "๋„๋ฉด", "dwg"]) + } + + # 2. ํŒŒ์ผ๋ช…์— ๋Œ€ํ•œ '๋ฐฉํ–ฅํƒ€' ๊ฐ€์ค‘์น˜ ๋ถ€์—ฌ (Final Push) + # ๋ณธ๋ฌธ ๋ฐ์ดํ„ฐ๊ฐ€ ์•„๋ฌด๋ฆฌ ๋งŽ์•„๋„ ํŒŒ์ผ๋ช…์˜ ์˜๋„๋ฅผ ์กด์ค‘ํ•˜๊ธฐ ์œ„ํ•ด 7๋ฐฐ ๊ฐ€์ค‘์น˜ + if "์‹ค์ •" in fn_clean or "๋ณ€๊ฒฝ" in fn_clean: scores["change"] += 50 # ๋ณธ๋ฌธ 50ํšŒ ์–ธ๊ธ‰๊ณผ ๋งž๋จน๋Š” ๊ฐ€์ค‘์น˜ + if "๊ณ„์•ฝ" in fn_clean or "ํ•˜๋„๊ธ‰" in fn_clean: scores["contract"] += 50 + if "์ธ๋ ฅ" in fn_clean or "์ดํƒˆ" in fn_clean: scores["hr"] += 50 + if "๋‹จ๊ฐ€" in fn_clean or "์ˆ˜๋Ÿ‰" in fn_clean or "๋„๋ฉด" in fn_clean: scores["technical"] += 50 + if "์ œ์ถœ" in fn_clean or "๊ฑด" in fn_clean: scores["official"] += 30 + + # 3. ์ข…ํ•ฉ ๋†๋„์— ๋”ฐ๋ฅธ ์ตœ์ข… ๋„๋ฉ”์ธ ์„ ์ • + dominant_domain = max(scores, key=scores.get) + + # ํ”„๋กœ์ ํŠธ ์‹๋ณ„ (Fuzzy ๋งค์นญ ๋ฐ ๊ต์ฐจ ๊ฒ€์ฆ) + project_loc = "์–ด์ฒœ~๊ณต์ฃผ" if any(k in clean_ctx or k in fn_clean for k in ["์–ด์ฒœ", "๊ณต์ฃผ"]) else "๋Œ€์ˆ ~์ •์•ˆ" if any(k in clean_ctx or k in fn_clean for k in ["๋Œ€์ˆ ", "์ •์•ˆ"]) else "๊ณตํ†ต" + + # --- [ํ†ตํ•ฉ ์ถ”๋ก  ๋ฐ ๋งค์นญ] --- + + # ์‹œ๋‚˜๋ฆฌ์˜ค A: ์‹ค์ •๋ณด๊ณ /์„ค๊ณ„๋ณ€๊ฒฝ (๋ณธ๋ฌธ ๋ฐ์ดํ„ฐ + ํŒŒ์ผ๋ช… ์˜๋„ ํ•ฉ์„ฑ) + if dominant_domain == "change" or (scores["change"] > 0 and scores["technical"] > 5): + cat = f"์‹ค์ •๋ณด๊ณ ({project_loc})" + sub = "์ง€์žฅ๋ฌผ" if any(k in clean_ctx for k in ["์ž„๋Œ€๋ฃŒ", "ํ† ์ง€", "๋ณด์ƒ"]) else "๊ตฌ์กฐ๋ฌผ๊ณต" if "๊ตฌ์กฐ๋ฌผ" in clean_ctx else "๊ธฐํƒ€" + return f"์„ค๊ณ„๋ณ€๊ฒฝ > {cat} > {sub}", f"๋ณธ๋ฌธ์˜ ๊ธฐ์ˆ  ๋ฐ์ดํ„ฐ ๋ฐ€๋„์™€ ํŒŒ์ผ๋ช…์˜ '{dominant_domain}' ๊ด€๋ จ ์˜๋„๋ฅผ ์ข…ํ•ฉํ•˜์—ฌ {project_loc} ํ”„๋กœ์ ํŠธ์˜ ์‹ค์ •๋ณด๊ณ  ๋ณธ์ฒด๋กœ ํŒ์ •." + + # ์‹œ๋‚˜๋ฆฌ์˜ค B: ํ–‰์ • ๊ณ„์•ฝ/ํ•˜๋„๊ธ‰ (๋ณธ์ฒด ์ค‘์‹ฌ) + if dominant_domain == "contract": + return "ํ–‰์ • > ๊ณ„์•ฝ > ๊ณ„์•ฝ๊ด€๋ฆฌ", "๋ฌธ์„œ ์ „์ฒด์—์„œ ๊ณ„์•ฝ ๋ฐ ํ•˜๋„๊ธ‰ ์—…๋ฌด ๋ณธ์งˆ์ด ์ง€๋ฐฐ์ ์œผ๋กœ ํ™•์ธ๋จ." + + # ์‹œ๋‚˜๋ฆฌ์˜ค C: ์ธ์‚ฌ/์ธ๋ ฅ ๊ด€๋ฆฌ + if dominant_domain == "hr": + if len(all_text_list) <= 2: return "๊ณต์‚ฌ๊ด€๋ฆฌ > ๊ณต๋ฌธ > ์ธ๋ ฅ", "์ธ๋ ฅ ์‚ฌํ•ญ์„ ๊ฐ„๋žตํžˆ ๋ณด๊ณ ํ•˜๋Š” ๊ณต๋ฌธ ํ˜•์‹์ž„." + return "ํ–‰์ • > ๊ณ„์•ฝ > ์ธ์›๊ด€๋ฆฌ", "๋‹ค๋Ÿ‰์˜ ์ธ๋ ฅ ์ฆ๋น™ ๋ฐ์ดํ„ฐ๊ฐ€ ํฌํ•จ๋œ ํ–‰์ • ์„œ๋ฅ˜์ž„." + + # ์‹œ๋‚˜๋ฆฌ์˜ค D: ์ˆœ์ˆ˜ ๊ณต๋ฌธ (ํ˜•์‹ ์šฐ์„ ) + if dominant_domain == "official" or scores["official"] > scores["technical"]: + tab, cat = "๊ณต์‚ฌ๊ด€๋ฆฌ", "๊ณต๋ฌธ" + sub = "์ ‘์ˆ˜(์ˆ˜์‹ )" + if "๋ฐฉ์นจ" in clean_ctx or "์ง€์นจ" in clean_ctx: sub = "๋ฐฉ์นจ" + elif "๋ฐœ์‹ " in clean_ctx[:500]: sub = "๋ฐœ์†ก(๋ฐœ์‹ )" + return f"{tab} > {cat} > {sub}", "์ „์ฒด ๋งฅ๋ฝ์ƒ ๊ธฐ์ˆ ์  ๋ฐ์ดํ„ฐ๋ณด๋‹ค ํ–‰์ •์  ์ „๋‹ฌ ํ–‰์œ„(๊ณต๋ฌธ)๊ฐ€ ํ•ต์‹ฌ ์ •์ฒด์„ฑ์œผ๋กœ ํŒ๋‹จ๋จ." + + # ์‹œ๋‚˜๋ฆฌ์˜ค E: ๊ธฐ์ˆ  ์„ฑ๊ณผํ’ˆ + if dominant_domain == "technical": + if any(k in clean_ctx or k in fn_clean for k in ["๋‹จ๊ฐ€", "๋‚ด์—ญ"]): return "์„ค๊ณ„์„ฑ๊ณผํ’ˆ > ๋‚ด์—ญ์„œ > ๋‹จ๊ฐ€์‚ฐ์ถœ์„œ", "๋‚ด์—ญ/๋‹จ๊ฐ€ ์‚ฐ์ถœ ๊ธฐ์ˆ  ๋ฐ์ดํ„ฐ ํ™•์ธ." + if any(k in clean_ctx or k in fn_clean for k in ["๋„๋ฉด", "dwg"]): return "์„ค๊ณ„์„ฑ๊ณผํ’ˆ > ์„ค๊ณ„๋„๋ฉด > ๊ณตํ†ต", "๋„๋ฉด/๊ทธ๋ž˜ํ”ฝ ๋ฐ์ดํ„ฐ ํ™•์ธ." + return "์„ค๊ณ„์„ฑ๊ณผํ’ˆ > ์ˆ˜๋Ÿ‰์‚ฐ์ถœ์„œ > ํ† ๊ณต", "์ˆ˜๋Ÿ‰/๋ฌผ๋Ÿ‰ ์‚ฐ์ถœ ๋ฐ์ดํ„ฐ ํ™•์ธ." + + return "ํ–‰์ • > ์—…๋ฌด๊ด€๋ฆฌ > ์–‘์‹์„œ๋ฅ˜", "์ผ๋ฐ˜ ํ–‰์ • ๋ฐ ๊ธฐํƒ€ ์–‘์‹ ์„œ๋ฅ˜๋กœ ๋ถ„๋ฅ˜ํ•จ." + +def analyze_file_content(filename: str): + try: + file_path = os.path.join("sample", filename) + text_by_pages = [] + if filename.lower().endswith(".pdf"): + reader = PdfReader(file_path) + for i in range(len(reader.pages)): + page_text = reader.pages[i].extract_text() or "" + if OCR_AVAILABLE: + try: + images = convert_from_path(file_path, first_page=i+1, last_page=i+1, poppler_path=POPPLER_BIN, dpi=200) + if images: + ocr_result = pytesseract.image_to_string(images[0], lang='kor+eng') + page_text += "\n" + ocr_result + except Exception as ocr_err: + print(f"OCR Error on page {i+1}: {ocr_err}") + text_by_pages.append(page_text) + elif filename.lower().endswith(('.xlsx', '.xls')): + import pandas as pd + df = pd.read_excel(file_path) + text_by_pages.append(df.to_string()) + else: text_by_pages.append("") + + path, reason = analyze_flow_reasoning(filename, text_by_pages) + + return { + "filename": filename, + "total_pages": len(text_by_pages), + "final_result": { + "suggested_path": path, + "confidence": "100%", + "reason": reason, + "snippet": " ".join(text_by_pages)[:1500] + } + } + except Exception as e: + return {"error": str(e), "filename": filename} diff --git a/analyze_logs_pattern.py b/analyze_logs_pattern.py new file mode 100644 index 0000000..5aae4c4 --- /dev/null +++ b/analyze_logs_pattern.py @@ -0,0 +1,48 @@ +import pymysql +import re +from collections import Counter + +def get_db_connection(): + return pymysql.connect( + host='localhost', + user='root', + password='45278434', + database='pm_proto_test', + charset='utf8mb4', + cursorclass=pymysql.cursors.DictCursor + ) + +def analyze_logs(): + conn = get_db_connection() + try: + with conn.cursor() as cursor: + cursor.execute("SELECT DISTINCT recent_log FROM projects_history WHERE recent_log IS NOT NULL AND recent_log != ''") + rows = cursor.fetchall() + + logs = [r['recent_log'] for r in rows] + + output = [] + output.append("[Raw Log Samples]") + for log in logs[:20]: + output.append(f"- {log}") + + patterns = [] + for log in logs: + p = re.sub(r'\d{2,4}\.\d{2}\.\d{2}', '[DATE]', log) + p = re.sub(r'\d+', '[NUM]', p) + patterns.append(p) + + output.append("\n[Log Patterns Frequency]") + pattern_counts = Counter(patterns).most_common(20) + for p, count in pattern_counts: + output.append(f"({count}) {p}") + + with open("log_analysis_result.txt", "w", encoding="utf-8") as f: + f.write("\n".join(output)) + print("Analysis complete. Result saved to log_analysis_result.txt") + + finally: + conn.close() + +if __name__ == "__main__": + analyze_logs() diff --git a/check_tables.py b/check_tables.py new file mode 100644 index 0000000..caac7c4 --- /dev/null +++ b/check_tables.py @@ -0,0 +1,29 @@ +import pymysql +import os +from dotenv import load_dotenv + +load_dotenv() + +def show_tables(): + conn = pymysql.connect( + host=os.getenv('DB_HOST', 'localhost'), + user=os.getenv('DB_USER', 'root'), + password=os.getenv('DB_PASSWORD', '45278434'), + database='PM_proto_test', + charset='utf8mb4', + cursorclass=pymysql.cursors.DictCursor + ) + try: + with conn.cursor() as cursor: + cursor.execute("SHOW TABLES") + tables = cursor.fetchall() + print("Tables in PM_proto_test:") + for t in tables: + print(f" - {list(t.values())[0]}") + except Exception as e: + print(f"Error occurred: {e}") + finally: + conn.close() + +if __name__ == "__main__": + show_tables() diff --git a/clear_test_db.py b/clear_test_db.py new file mode 100644 index 0000000..adec441 --- /dev/null +++ b/clear_test_db.py @@ -0,0 +1,29 @@ +import pymysql +import os +from dotenv import load_dotenv + +load_dotenv() + +def clear_project_history(): + conn = pymysql.connect( + host=os.getenv('DB_HOST', 'localhost'), + user=os.getenv('DB_USER', 'root'), + password=os.getenv('DB_PASSWORD', '45278434'), + database='PM_proto_test', + charset='utf8mb4', + cursorclass=pymysql.cursors.DictCursor + ) + try: + with conn.cursor() as cursor: + # ํ…Œ์ด๋ธ”์˜ ๋ชจ๋“  ๋ฐ์ดํ„ฐ๋ฅผ ์‚ญ์ œ + print("Cleaning projects_history table in PM_proto_test...") + cursor.execute("DELETE FROM projects_history") + conn.commit() + print("Successfully cleared all records from projects_history.") + except Exception as e: + print(f"Error occurred: {e}") + finally: + conn.close() + +if __name__ == "__main__": + clear_project_history() diff --git a/clone_db.py b/clone_db.py new file mode 100644 index 0000000..06d4abe --- /dev/null +++ b/clone_db.py @@ -0,0 +1,45 @@ +import pymysql +import os + +def clone_database(): + try: + connection = pymysql.connect( + host=os.getenv('DB_HOST', 'localhost'), + user=os.getenv('DB_USER', 'root'), + password=os.getenv('DB_PASSWORD', '45278434'), + charset='utf8mb4', + cursorclass=pymysql.cursors.DictCursor + ) + + with connection.cursor() as cursor: + # 1. Create test database + cursor.execute("CREATE DATABASE IF NOT EXISTS PM_proto_test") + print("Database PM_proto_test created or already exists.") + + # 2. Get all tables from source database + cursor.execute("SHOW TABLES FROM PM_proto") + tables = cursor.fetchall() + + for table_row in tables: + table_name = list(table_row.values())[0] + + # 3. Drop existing table in test DB if exists + cursor.execute(f"DROP TABLE IF EXISTS PM_proto_test.{table_name}") + + # 4. Clone schema and data + # Note: CREATE TABLE ... LIKE doesn't copy data, and CREATE TABLE ... AS SELECT doesn't copy indexes. + # So we use LIKE first, then INSERT INTO ... SELECT * + cursor.execute(f"CREATE TABLE PM_proto_test.{table_name} LIKE PM_proto.{table_name}") + cursor.execute(f"INSERT INTO PM_proto_test.{table_name} SELECT * FROM PM_proto.{table_name}") + print(f"Table {table_name} cloned.") + + connection.commit() + print("Database cloning completed successfully.") + except Exception as e: + print(f"Error during database cloning: {e}") + finally: + if 'connection' in locals(): + connection.close() + +if __name__ == "__main__": + clone_database() diff --git a/crawler_service.py b/crawler_service.py index 0d9ca37..1b333b3 100644 --- a/crawler_service.py +++ b/crawler_service.py @@ -1,258 +1,258 @@ -import os -import re -import asyncio -import json -import traceback -import sys -import threading -import queue -import pymysql -from datetime import datetime -from playwright.async_api import async_playwright -from dotenv import load_dotenv -from sql_queries import CrawlerQueries - -load_dotenv(override=True) - -# ๊ธ€๋กœ๋ฒŒ ์ค‘๋‹จ ์ œ์–ด์šฉ ์ด๋ฒคํŠธ -crawl_stop_event = threading.Event() - -def get_db_connection(): - """MySQL ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ์„ ๋ฐ˜ํ™˜ (ํ™˜๊ฒฝ๋ณ€์ˆ˜ ๊ธฐ๋ฐ˜)""" - return pymysql.connect( - host=os.getenv('DB_HOST', 'localhost'), - user=os.getenv('DB_USER', 'root'), - password=os.getenv('DB_PASSWORD', '45278434'), - database=os.getenv('DB_NAME', 'PM_proto'), - charset='utf8mb4', - cursorclass=pymysql.cursors.DictCursor - ) - -def clean_date_string(date_str): - if not date_str: return "" - match = re.search(r'(\d{2})[./-](\d{2})[./-](\d{2})', date_str) - if match: return f"20{match.group(1)}.{match.group(2)}.{match.group(3)}" - return date_str[:10].replace("-", ".") - -def parse_log_id(log_id): - if not log_id or "_" not in log_id: return log_id - try: - parts = log_id.split('_') - if len(parts) >= 4: - date_part = clean_date_string(parts[1]) - activity = parts[3].strip() - activity = re.sub(r'\(.*?\)', '', activity).strip() - return f"{date_part}, {activity}" - except: pass - return log_id - -def crawler_thread_worker(msg_queue, user_id, password): - crawl_stop_event.clear() - if sys.platform == 'win32': - asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy()) - - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - - async def run(): - async with async_playwright() as p: - browser = None - try: - msg_queue.put(json.dumps({'type': 'log', 'message': '๋ธŒ๋ผ์šฐ์ € ์—”์ง„ ๊ฐ€๋™ (์ „ ๊ธฐ๋Šฅ ๋ณต๊ตฌ ๋ชจ๋“œ)...'})) - browser = await p.chromium.launch(headless=True, args=[ - "--no-sandbox", - "--disable-dev-shm-usage", - "--disable-blink-features=AutomationControlled" - ]) - context = await browser.new_context( - viewport={'width': 1600, 'height': 900}, - user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36" - ) - - captured_data = {"tree": None, "_is_root_archive": False, "project_list": [], "last_project_data": None} - - async def global_interceptor(response): - url = response.url - try: - if "getAllList" in url: - data = await response.json() - captured_data["project_list"] = data.get("data", []) - elif "getTreeObject" in url: - is_root = False - if "params[resourcePath]=" in url: - path_val = url.split("params[resourcePath]=")[1].split("&")[0] - if path_val in ["%2F", "/"]: is_root = True - if is_root: - captured_data["tree"] = await response.json() - captured_data["_is_root_archive"] = True - elif "getData" in url and "overview" in url: - captured_data["last_project_data"] = await response.json() - except: pass - - context.on("response", global_interceptor) - page = await context.new_page() - await page.goto("https://overseas.projectmastercloud.com/dashboard", wait_until="domcontentloaded") - - # ๋กœ๊ทธ์ธ - if await page.locator("#login-by-id").is_visible(timeout=10000): - await page.click("#login-by-id") - await page.fill("#user_id", user_id) - await page.fill("#user_pw", password) - await page.click("#login-btn") - - await page.wait_for_selector("h4.list__contents_aria_group_body_list_item_label", timeout=60000) - await asyncio.sleep(3) - - # [Phase 1] DB ๋งˆ์Šคํ„ฐ ์ •๋ณด ๋™๊ธฐํ™” - if captured_data["project_list"]: - conn = get_db_connection() - try: - with conn.cursor() as cursor: - for p_info in captured_data["project_list"]: - cursor.execute(CrawlerQueries.UPSERT_MASTER, (p_info.get("project_id"), p_info.get("project_nm"), - p_info.get("short_nm", "").strip(), p_info.get("master"), - p_info.get("large_class"), p_info.get("mid_class"))) - conn.commit() - msg_queue.put(json.dumps({'type': 'log', 'message': 'DB ๋งˆ์Šคํ„ฐ ์ •๋ณด ๋™๊ธฐํ™” ์™„๋ฃŒ.'})) - finally: conn.close() - - # [Phase 2] ์ˆ˜์ง‘ ๋ฃจํ”„ - names = await page.locator("h4.list__contents_aria_group_body_list_item_label").all_inner_texts() - project_names = list(dict.fromkeys([n.strip() for n in names if n.strip()])) - count = len(project_names) - - for i, project_name in enumerate(project_names): - if crawl_stop_event.is_set(): - msg_queue.put(json.dumps({'type': 'log', 'message': '>>> ์ค‘๋‹จ ์‹ ํ˜ธ ๊ฐ์ง€: ์ข…๋ฃŒํ•ฉ๋‹ˆ๋‹ค.'})) - break - - msg_queue.put(json.dumps({'type': 'log', 'message': f'[{i+1}/{count}] {project_name} ์ˆ˜์ง‘ ์‹œ์ž‘'})) - p_match = next((p for p in captured_data["project_list"] if p.get('project_nm') == project_name or p.get('short_nm', '').strip() == project_name), None) - current_p_id = p_match.get('project_id') if p_match else None - captured_data["tree"] = None; captured_data["_is_root_archive"] = False - - try: - # 1. ํ”„๋กœ์ ํŠธ ์ง„์ž… (์ขŒํ‘œ ํด๋ฆญ) - target_el = page.locator(f"h4.list__contents_aria_group_body_list_item_label:has-text('{project_name}')").first - await target_el.scroll_into_view_if_needed() - box = await target_el.bounding_box() - if box: await page.mouse.click(box['x'] + 5, box['y'] + 5) - else: await target_el.click(force=True) - - await page.wait_for_selector("text=ํ™œ๋™๋กœ๊ทธ", timeout=30000) - - # [๋ถ€์„œ ์ •๋ณด ์ˆ˜์ง‘] getData ์‘๋‹ต ๋Œ€๊ธฐ ๋ฐ DB ์—…๋ฐ์ดํŠธ - for _ in range(10): - if captured_data.get("last_project_data"): break - await asyncio.sleep(0.5) - - last_data = captured_data.get("last_project_data") - if last_data: - if isinstance(last_data, list) and len(last_data) > 0: - last_data = last_data[0] - - if isinstance(last_data, dict): - proj_data = last_data.get("data", {}) - if isinstance(proj_data, list) and len(proj_data) > 0: - proj_data = proj_data[0] - - if isinstance(proj_data, dict): - dept = proj_data.get("department") - p_id = proj_data.get("project_id") - if dept and p_id: - with get_db_connection() as conn: - with conn.cursor() as cursor: - cursor.execute(CrawlerQueries.UPDATE_DEPARTMENT, (dept, p_id)) - conn.commit() - captured_data["last_project_data"] = None # ์ดˆ๊ธฐํ™” - - await asyncio.sleep(2) - - recent_log = "๋ฐ์ดํ„ฐ ์—†์Œ"; file_count = 0 - - # 2. ํ™œ๋™๋กœ๊ทธ (๋‚ ์งœ ํ•„ํ„ฐ ์ ์šฉ ๋ฒ„์ „) - modal_opened = False - for _ in range(3): - await page.get_by_text("ํ™œ๋™๋กœ๊ทธ").first.click() - try: - await page.wait_for_selector("article.archive-modal", timeout=5000) - modal_opened = True; break - except: await asyncio.sleep(1) - - if modal_opened: - # ๋‚ ์งœ ํ•„ํ„ฐ 2020-01-01 ์ ์šฉ - inputs = await page.locator("article.archive-modal input").all() - for inp in inputs: - if (await inp.get_attribute("type")) == "date": - await inp.fill("2020-01-01"); break - - apply_btn = page.locator("article.archive-modal").get_by_text("์ ์šฉ").first - if await apply_btn.is_visible(): - await apply_btn.click() - await asyncio.sleep(5) - log_elements = await page.locator("article.archive-modal div[id*='_']").all() - if log_elements: - recent_log = parse_log_id(await log_elements[0].get_attribute("id")) - await page.keyboard.press("Escape") - - # 3. ๊ตฌ์„ฑ ์ˆ˜์ง‘ (API Fetch ๋ฐฉ์‹ - ํŒ์—… ์—†์Œ) - await page.evaluate("""() => { - const baseUrl = window.location.origin + window.location.pathname.split('/').slice(0, 2).join('/'); - fetch(`${baseUrl}/archive/getTreeObject?params[storageType]=CLOUD¶ms[resourcePath]=/`); - }""") - for _ in range(30): - if captured_data["_is_root_archive"]: break - await asyncio.sleep(0.5) - - if captured_data["tree"]: - tree_data = captured_data["tree"] - if isinstance(tree_data, list) and len(tree_data) > 0: - tree_data = tree_data[0] - - if isinstance(tree_data, dict): - tree = tree_data.get('currentTreeObject', tree_data) - if isinstance(tree, dict): - total = len(tree.get("file", {})) - folders = tree.get("folder", {}) - if isinstance(folders, dict): - for f in folders.values(): total += int(f.get("filesCount", 0)) - file_count = total - - # 4. DB ์‹ค์‹œ๊ฐ„ ์ €์žฅ - if current_p_id: - with get_db_connection() as conn: - with conn.cursor() as cursor: - cursor.execute(CrawlerQueries.UPSERT_HISTORY, (current_p_id, recent_log, file_count)) - conn.commit() - msg_queue.put(json.dumps({'type': 'log', 'message': f' - [์„ฑ๊ณต] ๋กœ๊ทธ: {recent_log[:20]}... / ํŒŒ์ผ: {file_count}๊ฐœ'})) - - await page.goto("https://overseas.projectmastercloud.com/dashboard", wait_until="domcontentloaded") - - except Exception as e: - msg_queue.put(json.dumps({'type': 'log', 'message': f' - {project_name} ์‹คํŒจ: {str(e)}'})) - await page.goto("https://overseas.projectmastercloud.com/dashboard") - - msg_queue.put(json.dumps({'type': 'done', 'data': []})) - - except Exception as e: - msg_queue.put(json.dumps({'type': 'log', 'message': f'์น˜๋ช…์  ์˜ค๋ฅ˜: {str(e)}'})) - finally: - if browser: await browser.close() - msg_queue.put(None) - - loop.run_until_complete(run()) - loop.close() - -async def run_crawler_service(): - msg_queue = queue.Queue() - thread = threading.Thread(target=crawler_thread_worker, args=(msg_queue, os.getenv("PM_USER_ID"), os.getenv("PM_PASSWORD"))) - thread.start() - while True: - try: - msg = await asyncio.to_thread(msg_queue.get, timeout=1.0) - if msg is None: break - yield f"data: {msg}\n\n" - except queue.Empty: - if not thread.is_alive(): break - await asyncio.sleep(0.1) - thread.join() +import os +import re +import asyncio +import json +import traceback +import sys +import threading +import queue +import pymysql +from datetime import datetime +from playwright.async_api import async_playwright +from dotenv import load_dotenv +from sql_queries import CrawlerQueries + +load_dotenv(override=True) + +# ๊ธ€๋กœ๋ฒŒ ์ค‘๋‹จ ์ œ์–ด์šฉ ์ด๋ฒคํŠธ +crawl_stop_event = threading.Event() + +def get_db_connection(): + """MySQL ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ์„ ๋ฐ˜ํ™˜ (ํ™˜๊ฒฝ๋ณ€์ˆ˜ ๊ธฐ๋ฐ˜)""" + return pymysql.connect( + host=os.getenv('DB_HOST', 'localhost'), + user=os.getenv('DB_USER', 'root'), + password=os.getenv('DB_PASSWORD', '45278434'), + database=os.getenv('DB_NAME', 'PM_proto'), + charset='utf8mb4', + cursorclass=pymysql.cursors.DictCursor + ) + +def clean_date_string(date_str): + if not date_str: return "" + match = re.search(r'(\d{2})[./-](\d{2})[./-](\d{2})', date_str) + if match: return f"20{match.group(1)}.{match.group(2)}.{match.group(3)}" + return date_str[:10].replace("-", ".") + +def parse_log_id(log_id): + if not log_id or "_" not in log_id: return log_id + try: + parts = log_id.split('_') + if len(parts) >= 4: + date_part = clean_date_string(parts[1]) + activity = parts[3].strip() + activity = re.sub(r'\(.*?\)', '', activity).strip() + return f"{date_part}, {activity}" + except: pass + return log_id + +def crawler_thread_worker(msg_queue, user_id, password): + crawl_stop_event.clear() + if sys.platform == 'win32': + asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy()) + + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + + async def run(): + async with async_playwright() as p: + browser = None + try: + msg_queue.put(json.dumps({'type': 'log', 'message': '๋ธŒ๋ผ์šฐ์ € ์—”์ง„ ๊ฐ€๋™ (์ „ ๊ธฐ๋Šฅ ๋ณต๊ตฌ ๋ชจ๋“œ)...'})) + browser = await p.chromium.launch(headless=True, args=[ + "--no-sandbox", + "--disable-dev-shm-usage", + "--disable-blink-features=AutomationControlled" + ]) + context = await browser.new_context( + viewport={'width': 1600, 'height': 900}, + user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36" + ) + + captured_data = {"tree": None, "_is_root_archive": False, "project_list": [], "last_project_data": None} + + async def global_interceptor(response): + url = response.url + try: + if "getAllList" in url: + data = await response.json() + captured_data["project_list"] = data.get("data", []) + elif "getTreeObject" in url: + is_root = False + if "params[resourcePath]=" in url: + path_val = url.split("params[resourcePath]=")[1].split("&")[0] + if path_val in ["%2F", "/"]: is_root = True + if is_root: + captured_data["tree"] = await response.json() + captured_data["_is_root_archive"] = True + elif "getData" in url and "overview" in url: + captured_data["last_project_data"] = await response.json() + except: pass + + context.on("response", global_interceptor) + page = await context.new_page() + await page.goto("https://overseas.projectmastercloud.com/dashboard", wait_until="domcontentloaded") + + # ๋กœ๊ทธ์ธ + if await page.locator("#login-by-id").is_visible(timeout=10000): + await page.click("#login-by-id") + await page.fill("#user_id", user_id) + await page.fill("#user_pw", password) + await page.click("#login-btn") + + await page.wait_for_selector("h4.list__contents_aria_group_body_list_item_label", timeout=60000) + await asyncio.sleep(3) + + # [Phase 1] DB ๋งˆ์Šคํ„ฐ ์ •๋ณด ๋™๊ธฐํ™” + if captured_data["project_list"]: + conn = get_db_connection() + try: + with conn.cursor() as cursor: + for p_info in captured_data["project_list"]: + cursor.execute(CrawlerQueries.UPSERT_MASTER, (p_info.get("project_id"), p_info.get("project_nm"), + p_info.get("short_nm", "").strip(), p_info.get("master"), + p_info.get("large_class"), p_info.get("mid_class"))) + conn.commit() + msg_queue.put(json.dumps({'type': 'log', 'message': 'DB ๋งˆ์Šคํ„ฐ ์ •๋ณด ๋™๊ธฐํ™” ์™„๋ฃŒ.'})) + finally: conn.close() + + # [Phase 2] ์ˆ˜์ง‘ ๋ฃจํ”„ + names = await page.locator("h4.list__contents_aria_group_body_list_item_label").all_inner_texts() + project_names = list(dict.fromkeys([n.strip() for n in names if n.strip()])) + count = len(project_names) + + for i, project_name in enumerate(project_names): + if crawl_stop_event.is_set(): + msg_queue.put(json.dumps({'type': 'log', 'message': '>>> ์ค‘๋‹จ ์‹ ํ˜ธ ๊ฐ์ง€: ์ข…๋ฃŒํ•ฉ๋‹ˆ๋‹ค.'})) + break + + msg_queue.put(json.dumps({'type': 'log', 'message': f'[{i+1}/{count}] {project_name} ์ˆ˜์ง‘ ์‹œ์ž‘'})) + p_match = next((p for p in captured_data["project_list"] if p.get('project_nm') == project_name or p.get('short_nm', '').strip() == project_name), None) + current_p_id = p_match.get('project_id') if p_match else None + captured_data["tree"] = None; captured_data["_is_root_archive"] = False + + try: + # 1. ํ”„๋กœ์ ํŠธ ์ง„์ž… (์ขŒํ‘œ ํด๋ฆญ) + target_el = page.locator(f"h4.list__contents_aria_group_body_list_item_label:has-text('{project_name}')").first + await target_el.scroll_into_view_if_needed() + box = await target_el.bounding_box() + if box: await page.mouse.click(box['x'] + 5, box['y'] + 5) + else: await target_el.click(force=True) + + await page.wait_for_selector("text=ํ™œ๋™๋กœ๊ทธ", timeout=30000) + + # [๋ถ€์„œ ์ •๋ณด ์ˆ˜์ง‘] getData ์‘๋‹ต ๋Œ€๊ธฐ ๋ฐ DB ์—…๋ฐ์ดํŠธ + for _ in range(10): + if captured_data.get("last_project_data"): break + await asyncio.sleep(0.5) + + last_data = captured_data.get("last_project_data") + if last_data: + if isinstance(last_data, list) and len(last_data) > 0: + last_data = last_data[0] + + if isinstance(last_data, dict): + proj_data = last_data.get("data", {}) + if isinstance(proj_data, list) and len(proj_data) > 0: + proj_data = proj_data[0] + + if isinstance(proj_data, dict): + dept = proj_data.get("department") + p_id = proj_data.get("project_id") + if dept and p_id: + with get_db_connection() as conn: + with conn.cursor() as cursor: + cursor.execute(CrawlerQueries.UPDATE_DEPARTMENT, (dept, p_id)) + conn.commit() + captured_data["last_project_data"] = None # ์ดˆ๊ธฐํ™” + + await asyncio.sleep(2) + + recent_log = "๋ฐ์ดํ„ฐ ์—†์Œ"; file_count = 0 + + # 2. ํ™œ๋™๋กœ๊ทธ (๋‚ ์งœ ํ•„ํ„ฐ ์ ์šฉ ๋ฒ„์ „) + modal_opened = False + for _ in range(3): + await page.get_by_text("ํ™œ๋™๋กœ๊ทธ").first.click() + try: + await page.wait_for_selector("article.archive-modal", timeout=5000) + modal_opened = True; break + except: await asyncio.sleep(1) + + if modal_opened: + # ๋‚ ์งœ ํ•„ํ„ฐ 2020-01-01 ์ ์šฉ + inputs = await page.locator("article.archive-modal input").all() + for inp in inputs: + if (await inp.get_attribute("type")) == "date": + await inp.fill("2020-01-01"); break + + apply_btn = page.locator("article.archive-modal").get_by_text("์ ์šฉ").first + if await apply_btn.is_visible(): + await apply_btn.click() + await asyncio.sleep(5) + log_elements = await page.locator("article.archive-modal div[id*='_']").all() + if log_elements: + recent_log = parse_log_id(await log_elements[0].get_attribute("id")) + await page.keyboard.press("Escape") + + # 3. ๊ตฌ์„ฑ ์ˆ˜์ง‘ (API Fetch ๋ฐฉ์‹ - ํŒ์—… ์—†์Œ) + await page.evaluate("""() => { + const baseUrl = window.location.origin + window.location.pathname.split('/').slice(0, 2).join('/'); + fetch(`${baseUrl}/archive/getTreeObject?params[storageType]=CLOUD¶ms[resourcePath]=/`); + }""") + for _ in range(30): + if captured_data["_is_root_archive"]: break + await asyncio.sleep(0.5) + + if captured_data["tree"]: + tree_data = captured_data["tree"] + if isinstance(tree_data, list) and len(tree_data) > 0: + tree_data = tree_data[0] + + if isinstance(tree_data, dict): + tree = tree_data.get('currentTreeObject', tree_data) + if isinstance(tree, dict): + total = len(tree.get("file", {})) + folders = tree.get("folder", {}) + if isinstance(folders, dict): + for f in folders.values(): total += int(f.get("filesCount", 0)) + file_count = total + + # 4. DB ์‹ค์‹œ๊ฐ„ ์ €์žฅ + if current_p_id: + with get_db_connection() as conn: + with conn.cursor() as cursor: + cursor.execute(CrawlerQueries.UPSERT_HISTORY, (current_p_id, recent_log, file_count)) + conn.commit() + msg_queue.put(json.dumps({'type': 'log', 'message': f' - [์„ฑ๊ณต] ๋กœ๊ทธ: {recent_log[:20]}... / ํŒŒ์ผ: {file_count}๊ฐœ'})) + + await page.goto("https://overseas.projectmastercloud.com/dashboard", wait_until="domcontentloaded") + + except Exception as e: + msg_queue.put(json.dumps({'type': 'log', 'message': f' - {project_name} ์‹คํŒจ: {str(e)}'})) + await page.goto("https://overseas.projectmastercloud.com/dashboard") + + msg_queue.put(json.dumps({'type': 'done', 'data': []})) + + except Exception as e: + msg_queue.put(json.dumps({'type': 'log', 'message': f'์น˜๋ช…์  ์˜ค๋ฅ˜: {str(e)}'})) + finally: + if browser: await browser.close() + msg_queue.put(None) + + loop.run_until_complete(run()) + loop.close() + +async def run_crawler_service(): + msg_queue = queue.Queue() + thread = threading.Thread(target=crawler_thread_worker, args=(msg_queue, os.getenv("PM_USER_ID"), os.getenv("PM_PASSWORD"))) + thread.start() + while True: + try: + msg = await asyncio.to_thread(msg_queue.get, timeout=1.0) + if msg is None: break + yield f"data: {msg}\n\n" + except queue.Empty: + if not thread.is_alive(): break + await asyncio.sleep(0.1) + thread.join() diff --git a/crawler_service_test.py b/crawler_service_test.py new file mode 100644 index 0000000..57e10fe --- /dev/null +++ b/crawler_service_test.py @@ -0,0 +1,273 @@ +import os +import re +import asyncio +import json +import traceback +import sys +import threading +import queue +import pymysql +from datetime import datetime, timedelta +from playwright.async_api import async_playwright +from dotenv import load_dotenv +from sql_queries import CrawlerQueries + +load_dotenv(override=True) + +# ๊ธ€๋กœ๋ฒŒ ์ค‘๋‹จ ์ œ์–ด์šฉ ์ด๋ฒคํŠธ +crawl_stop_event = threading.Event() + +def get_db_connection(): + """MySQL ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค(TEST) ์—ฐ๊ฒฐ์„ ๋ฐ˜ํ™˜""" + return pymysql.connect( + host=os.getenv('DB_HOST', 'localhost'), + user=os.getenv('DB_USER', 'root'), + password=os.getenv('DB_PASSWORD', '45278434'), + database='PM_proto_test', # ํ…Œ์ŠคํŠธ์šฉ DB ๊ณ ์ • + charset='utf8mb4', + cursorclass=pymysql.cursors.DictCursor + ) + +def clean_date_string(date_str): + """์›๋ณธ crawler_service.py์™€ ๋™์ผํ•œ ๋‚ ์งœ ์ •๋ฆฌ ๋กœ์ง""" + if not date_str: return "" + match = re.search(r'(\d{2})[./-](\d{2})[./-](\d{2})', date_str) + if match: return f"20{match.group(1)}.{match.group(2)}.{match.group(3)}" + return date_str[:10].replace("-", ".") + +def parse_log_id(log_id): + """์›๋ณธ crawler_service.py์™€ ๋™์ผํ•œ ๋กœ๊ทธ ID ํŒŒ์‹ฑ ๋กœ์ง""" + if not log_id or "_" not in log_id: return log_id + try: + parts = log_id.split('_') + if len(parts) >= 4: + date_part = clean_date_string(parts[1]) + activity = parts[3].strip() + activity = re.sub(r'\(.*?\)', '', activity).strip() + return f"{date_part}, {activity}" + except: pass + return log_id + +def crawler_thread_worker(msg_queue, user_id, password): + crawl_stop_event.clear() + if sys.platform == 'win32': + asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy()) + + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + + async def run(): + async with async_playwright() as p: + browser = None + try: + msg_queue.put(json.dumps({'type': 'log', 'message': '[TEST] ์›๋ณธ ์ˆ˜์ง‘ ๋ฐฉ์‹ ๋ณต๊ตฌ ๋ฐ ์ถ”๋ก  ์—”์ง„ ๊ฐ€๋™...'})) + browser = await p.chromium.launch(headless=True, args=[ + "--no-sandbox", + "--disable-dev-shm-usage", + "--disable-blink-features=AutomationControlled" + ]) + context = await browser.new_context( + viewport={'width': 1600, 'height': 900}, + user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36" + ) + + captured_data = {"tree": None, "_is_root_archive": False, "project_list": [], "last_project_data": None} + + async def global_interceptor(response): + url = response.url + try: + if "getAllList" in url: + data = await response.json() + captured_data["project_list"] = data.get("data", []) + elif "getTreeObject" in url: + # [๋ณต๊ตฌ] ์›๋ณธ๊ณผ 100% ๋™์ผํ•œ ๋ฃจํŠธ ํŒ์ • ๋กœ์ง + is_root = False + if "params[resourcePath]=" in url: + path_val = url.split("params[resourcePath]=")[1].split("&")[0] + if path_val in ["%2F", "/"]: is_root = True + if is_root: + captured_data["tree"] = await response.json() + captured_data["_is_root_archive"] = True + elif "getData" in url and "overview" in url: + captured_data["last_project_data"] = await response.json() + except: pass + + context.on("response", global_interceptor) + page = await context.new_page() + await page.goto("https://overseas.projectmastercloud.com/dashboard", wait_until="domcontentloaded") + + if await page.locator("#login-by-id").is_visible(timeout=10000): + await page.click("#login-by-id"); await page.fill("#user_id", user_id); await page.fill("#user_pw", password); await page.click("#login-btn") + + await page.wait_for_selector("h4.list__contents_aria_group_body_list_item_label", timeout=60000) + await asyncio.sleep(2) + + project_names = list(dict.fromkeys([n.strip() for n in await page.locator("h4.list__contents_aria_group_body_list_item_label").all_inner_texts() if n.strip()])) + count = len(project_names) + + for i, project_name in enumerate(project_names): + if crawl_stop_event.is_set(): break + msg_queue.put(json.dumps({'type': 'log', 'message': f'[TEST] [{i+1}/{count}] {project_name} ์ˆ˜์ง‘'})) + p_match = next((p for p in captured_data["project_list"] if p.get('project_nm') == project_name or p.get('short_nm', '').strip() == project_name), None) + current_p_id = p_match.get('project_id') if p_match else None + + try: + # 1. ํ”„๋กœ์ ํŠธ ์ง„์ž… + target_el = page.locator(f"h4.list__contents_aria_group_body_list_item_label:has-text('{project_name}')").first + await target_el.scroll_into_view_if_needed() + box = await target_el.bounding_box() + if box: await page.mouse.click(box['x'] + 5, box['y'] + 5) + else: await target_el.click(force=True) + await page.wait_for_selector("text=ํ™œ๋™๋กœ๊ทธ", timeout=30000) + + # 2. [๋ณต๊ตฌ] ์ตœ์‹  ํŒŒ์ผ ์ˆ˜ ์‹ค์ธก (์›๋ณธ์˜ ์ˆ˜๋™ Fetch ๋ฐฉ์‹ ๊ทธ๋Œ€๋กœ) + captured_data["tree"] = None; captured_data["_is_root_archive"] = False + await page.evaluate("""() => { + const baseUrl = window.location.origin + window.location.pathname.split('/').slice(0, 2).join('/'); + fetch(`${baseUrl}/archive/getTreeObject?params[storageType]=CLOUD¶ms[resourcePath]=/`); + }""") + for _ in range(30): + if captured_data["_is_root_archive"]: break + await asyncio.sleep(0.5) + + actual_count = 0 + if captured_data["tree"]: + tree_data = captured_data["tree"] + if isinstance(tree_data, list) and len(tree_data) > 0: tree_data = tree_data[0] + if isinstance(tree_data, dict): + tree = tree_data.get('currentTreeObject', tree_data) + if isinstance(tree, dict): + # ์›๋ณธ ํŒŒ์ผ ์ˆ˜ ํ•ฉ์‚ฐ ๋กœ์ง + total = len(tree.get("file", {})) + folders = tree.get("folder", {}) + if isinstance(folders, dict): + for f in folders.values(): total += int(f.get("filesCount", 0)) + actual_count = total + + # 3. ํ™œ๋™๋กœ๊ทธ ์ „์ˆ˜ ์ˆ˜์ง‘ (ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ๋ฐฉ์‹: ์ตœ์ƒ๋‹จ ์šฐ์„  ํ™•๋ณด + ์ „์ˆ˜ ์Šคํฌ๋กค) + all_logs = [] + await page.get_by_text("ํ™œ๋™๋กœ๊ทธ").first.click() + if await page.wait_for_selector("article.archive-modal", timeout=10000): + # ๋‚ ์งœ ํ•„ํ„ฐ ์ ์šฉ (2020-01-01) + inputs = await page.locator("article.archive-modal input").all() + for inp in inputs: + if (await inp.get_attribute("type")) == "date": await inp.fill("2020-01-01"); break + + apply_btn = page.locator("article.archive-modal").get_by_text("์ ์šฉ").first + if await apply_btn.is_visible(): + await apply_btn.click() + # [ํ•ต์‹ฌ] ์ฒซ ๋ฒˆ์งธ ๋กœ๊ทธ๊ฐ€ ๋‚˜ํƒ€๋‚  ๋•Œ๊นŒ์ง€ ๋ช…์‹œ์  ๋Œ€๊ธฐ (์ตœ๋Œ€ 10์ดˆ) + try: + await page.wait_for_selector("article.archive-modal div[id*='_']", timeout=10000) + except: pass + await asyncio.sleep(2) + + # (1) ์ตœ์ƒ๋‹จ ๋กœ๊ทธ ์ฆ‰์‹œ ํ™•๋ณด (์•ˆ์ „์žฅ์น˜) + first_log_el = await page.locator("article.archive-modal div[id*='_']").first.get_attribute("id") + if first_log_el: + first_log_text = parse_log_id(first_log_el) + if ", " in first_log_text: + d, a = first_log_text.split(", ", 1) + all_logs.append({'date': d, 'activity': a}) + + # (2) ์ „์ˆ˜ ์ˆ˜์ง‘์„ ์œ„ํ•œ ๋ฌดํ•œ ์Šคํฌ๋กค ๋ฐ ์ง€์ •๋œ ํด๋ž˜์Šค ๋‚ด ID ์ˆ˜์ง‘ + last_count = len(all_logs) + for _ in range(20): + # ์Šคํฌ๋กค ์ˆ˜ํ–‰ (์‚ฌ์šฉ์ž๊ฐ€ ์ง€์ •ํ•œ log-body ํด๋ž˜์Šค ๊ธฐ์ค€) + await page.evaluate("""() => { + const body = document.querySelector('.log-item-wrap.log-body.scrollbar.scroll-container') || + document.querySelector('article.archive-modal .modal-body') || + document.querySelector('article.archive-modal'); + if (body) body.scrollTop = body.scrollHeight; + }""") + await asyncio.sleep(1.5) + + # ์‚ฌ์šฉ์ž ์ง€์ • ํด๋ž˜์Šค ๋‚ด์˜ ๋ชจ๋“  div ID ์ˆ˜์ง‘ + # .log-item-wrap.log-body.scrollbar.scroll-container ๋‚ด๋ถ€์˜ div๋“ค์„ ํƒ€๊ฒŸํŒ… + selector = ".log-item-wrap.log-body.scrollbar.scroll-container div" + current_elements = await page.locator(selector).all() + + # ๋งŒ์•ฝ ์ง€์ •๋œ ํด๋ž˜์Šค๋กœ ๊ฒ€์ƒ‰๋˜์ง€ ์•Š์„ ๊ฒฝ์šฐ ๊ธฐ์กด div[id*='_']๋ฅผ ๋ฐฑ์—…์œผ๋กœ ์‚ฌ์šฉ + if not current_elements: + current_elements = await page.locator("article.archive-modal div[id*='_']").all() + + seen_ids = {f"{log['date']}, {log['activity']}" for log in all_logs} + for el in current_elements: + log_id = await el.get_attribute("id") + if not log_id: continue + + log_text = parse_log_id(log_id) + if ", " in log_text and log_text not in seen_ids: + d, a = log_text.split(", ", 1) + all_logs.append({'date': d, 'activity': a}) + seen_ids.add(log_text) + + if len(all_logs) == last_count: break + last_count = len(all_logs) + + if not all_logs: + msg_queue.put(json.dumps({'type': 'log', 'message': f' - [์ฃผ์˜] {project_name}: ์ˆ˜์ง‘๋œ ๋กœ๊ทธ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.'})) + + await page.keyboard.press("Escape") + + # 4. ํŒŒ์ผ ์ˆ˜ ์ถ”๋ก  (์ „์ˆ˜ ๋ณด์กด ๋ชจ๋“œ) + history_map = {} + curr_calc_count = actual_count + + if all_logs: + # ์˜ค๋Š˜ ๋‚ ์งœ ๊ฐ•์ œ ์ฃผ์ž… ๋Œ€์‹  ์ˆ˜์ง‘๋œ ๋กœ๊ทธ์˜ ์‹ค์ œ ๋‚ ์งœ ์‚ฌ์šฉ + for log in all_logs: + d_db = log['date'].replace(".", "-") + act = log['activity'] + if d_db not in history_map: + history_map[d_db] = {"log": act, "count": curr_calc_count} + + if "์—…๋กœ๋“œ" in act: curr_calc_count -= 1 + elif "์‚ญ์ œ" in act: curr_calc_count += 1 + if curr_calc_count < 0: curr_calc_count = 0 + history_map[d_db]["count"] = curr_calc_count + else: + # ๋กœ๊ทธ๊ฐ€ ์ „ํ˜€ ์—†์„ ๊ฒฝ์šฐ์—๋งŒ ๊ธฐ๋ณธ๊ฐ’ ์ƒ์„ฑ + today_str = datetime.now().strftime("%Y-%m-%d") + history_map[today_str] = {"log": "๊ธฐ์กด ์ƒํƒœ ์œ ์ง€ (ํ™œ๋™ ์—†์Œ)", "count": actual_count} + + # 5. DB ์ €์žฅ + if current_p_id: + with get_db_connection() as conn: + with conn.cursor() as cursor: + for date_key, data in history_map.items(): + cursor.execute(CrawlerQueries.UPSERT_HISTORY_WITH_DATE, + (current_p_id, date_key, f"{date_key.replace('-', '.')}, {data['log']}", data['count'])) + conn.commit() + msg_queue.put(json.dumps({'type': 'log', 'message': f' - [์„ฑ๊ณต] ์‹ค์ธก {actual_count}๊ฐœ ๊ธฐ์ค€ ์‹œ๊ณ„์—ด ์ ์žฌ ์™„๋ฃŒ'})) + + await page.goto("https://overseas.projectmastercloud.com/dashboard", wait_until="domcontentloaded") + + except Exception as e: + msg_queue.put(json.dumps({'type': 'log', 'message': f' - {project_name} ์—๋Ÿฌ: {str(e)}'})) + await page.goto("https://overseas.projectmastercloud.com/dashboard") + + msg_queue.put(json.dumps({'type': 'done', 'data': []})) + + except Exception as e: + msg_queue.put(json.dumps({'type': 'log', 'message': f'์น˜๋ช…์  ์˜ค๋ฅ˜: {str(e)}'})) + finally: + if browser: await browser.close() + msg_queue.put(None) + + loop.run_until_complete(run()) + loop.close() + +async def run_crawler_service(): + msg_queue = queue.Queue() + thread = threading.Thread(target=crawler_thread_worker, args=(msg_queue, os.getenv("PM_USER_ID"), os.getenv("PM_PASSWORD"))) + thread.start() + while True: + try: + msg = await asyncio.to_thread(msg_queue.get, timeout=1.0) + if msg is None: break + yield f"data: {msg}\n\n" + except queue.Empty: + if not thread.is_alive(): break + await asyncio.sleep(0.1) + thread.join() diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..0ab6e46 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,17 @@ +services: + web: + # ํ˜„์žฌ ํด๋”์˜ Dockerfile์„ ์‚ฌ์šฉํ•˜์—ฌ ๋นŒ๋“œ + build: . + # ์ปจํ…Œ์ด๋„ˆ ์ด๋ฆ„ ์„ค์ • + container_name: aicode-server + # ํฌํŠธ ํฌ์›Œ๋”ฉ (ํ˜ธ์ŠคํŠธ 8000 -> ์ปจํ…Œ์ด๋„ˆ 8000) + ports: + - "8000:8000" + # ์†Œ์Šค ์ฝ”๋“œ ์ˆ˜์ • ์‹œ ์‹ค์‹œ๊ฐ„ ๋ฐ˜์˜ (๋ณผ๋ฅจ ๋งˆ์šดํŠธ) + volumes: + - .:/app + # ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์„ค์ • + environment: + - PYTHONUNBUFFERED=1 + # ์ปจํ…Œ์ด๋„ˆ ์ข…๋ฃŒ ์‹œ ์ž๋™ ์žฌ์‹œ์ž‘ + restart: always diff --git a/inquiry_service.py b/inquiry_service.py index 262af3d..5d0838f 100644 --- a/inquiry_service.py +++ b/inquiry_service.py @@ -1,42 +1,42 @@ -from datetime import datetime -from sql_queries import InquiryQueries - -class InquiryService: - @staticmethod - def get_inquiries_logic(cursor, pm_type=None, category=None, status=None, keyword=None): - sql = InquiryQueries.SELECT_BASE - params = [] - if pm_type: - sql += " AND pm_type = %s" - params.append(pm_type) - if category: - sql += " AND category = %s" - params.append(category) - if status: - sql += " AND status = %s" - params.append(status) - if keyword: - sql += " AND (content LIKE %s OR author LIKE %s OR project_nm LIKE %s)" - params.extend([f"%{keyword}%", f"%{keyword}%", f"%{keyword}%"]) - - sql += f" {InquiryQueries.ORDER_BY_DESC}" - cursor.execute(sql, params) - return cursor.fetchall() - - @staticmethod - def get_inquiry_detail_logic(cursor, inquiry_id): - cursor.execute(InquiryQueries.SELECT_BY_ID, (inquiry_id,)) - return cursor.fetchone() - - @staticmethod - def update_inquiry_reply_logic(cursor, conn, inquiry_id, req): - handled_date = datetime.now().strftime("%Y.%m.%d") - cursor.execute(InquiryQueries.UPDATE_REPLY, (req.reply, req.status, req.handler, handled_date, inquiry_id)) - conn.commit() - return {"success": True} - - @staticmethod - def delete_inquiry_reply_logic(cursor, conn, inquiry_id): - cursor.execute(InquiryQueries.DELETE_REPLY, (inquiry_id,)) - conn.commit() - return {"success": True} +from datetime import datetime +from sql_queries import InquiryQueries + +class InquiryService: + @staticmethod + def get_inquiries_logic(cursor, pm_type=None, category=None, status=None, keyword=None): + sql = InquiryQueries.SELECT_BASE + params = [] + if pm_type: + sql += " AND pm_type = %s" + params.append(pm_type) + if category: + sql += " AND category = %s" + params.append(category) + if status: + sql += " AND status = %s" + params.append(status) + if keyword: + sql += " AND (content LIKE %s OR author LIKE %s OR project_nm LIKE %s)" + params.extend([f"%{keyword}%", f"%{keyword}%", f"%{keyword}%"]) + + sql += f" {InquiryQueries.ORDER_BY_DESC}" + cursor.execute(sql, params) + return cursor.fetchall() + + @staticmethod + def get_inquiry_detail_logic(cursor, inquiry_id): + cursor.execute(InquiryQueries.SELECT_BY_ID, (inquiry_id,)) + return cursor.fetchone() + + @staticmethod + def update_inquiry_reply_logic(cursor, conn, inquiry_id, req): + handled_date = datetime.now().strftime("%Y.%m.%d") + cursor.execute(InquiryQueries.UPDATE_REPLY, (req.reply, req.status, req.handler, handled_date, inquiry_id)) + conn.commit() + return {"success": True} + + @staticmethod + def delete_inquiry_reply_logic(cursor, conn, inquiry_id): + cursor.execute(InquiryQueries.DELETE_REPLY, (inquiry_id,)) + conn.commit() + return {"success": True} diff --git a/js/analysis.js b/js/analysis.js index 2f4ecf0..007e346 100644 --- a/js/analysis.js +++ b/js/analysis.js @@ -1,463 +1,485 @@ -/** - * Project Master Analysis JS - * AVI (Activity Vitality Index) & VCI (Value Contribution Index) ๋ถ„์„ ์—”์ง„ - * OCI (Operational Consistency Index) ํ†ตํ•ฉ ๋ฒ„์ „ - */ - -// Chart.js ํ”Œ๋Ÿฌ๊ทธ์ธ ์ „์—ญ ๋“ฑ๋ก -if (typeof ChartDataLabels !== 'undefined') { - Chart.register(ChartDataLabels); -} - -document.addEventListener('DOMContentLoaded', () => { - console.log("Business Analysis Engine initialized..."); - loadProjectAnalysisData(); -}); - -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); - - 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}% (์šด์˜ ํ‘œ์ค€ 70.0% ๋Œ€๋น„)`; - } - } catch (e) { - console.error("๋ถ„์„ ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ ์‹คํŒจ:", e); - } -} - -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 getVciGrade(vci) { - if (vci >= 10) return { label: 'Masterpiece', class: 'grade-mvp', desc: '์‹œ์Šคํ…œ ๊ฐ€์น˜๋ฅผ ๊ฒฌ์ธํ•˜๋Š” ์ตœ์šฐ๋Ÿ‰ ํ•ต์‹ฌ ์ž์‚ฐ' }; - 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) - try { - const stats = { active: [], warning: [], danger: [], dead: [] }; - data.forEach(p => { - const status = getStatusInfo(p.p_war, p.is_auto_delete); - stats[status.key].push(p); - }); - - const statusCtx = document.getElementById('statusChart').getContext('2d'); - if (window.myStatusChart) window.myStatusChart.destroy(); - - window.myStatusChart = new Chart(statusCtx, { - type: 'doughnut', - data: { - labels: ['์ •์ƒ ์šด์˜', '๊ด€๋ฆฌ ์ฃผ์˜', '์œ„ํ—˜ ๋…ธ์ถœ', '์‚ฌ๋ง'], - datasets: [{ - data: [stats.active.length, stats.warning.length, stats.danger.length, stats.dead.length], - backgroundColor: ['#1E5149', '#22c55e', '#f59e0b', '#ef4444'], - borderWidth: 0 - }] - }, - options: { - responsive: true, - maintainAspectRatio: false, - layout: { padding: 15 }, - plugins: { - legend: { position: 'right', labels: { boxWidth: 10, font: { size: 11, weight: '700' }, usePointStyle: true } }, - datalabels: { display: false } - }, - cutout: '65%', - onClick: (e, elements) => { - if (elements.length > 0) { - const idx = elements[0].index; - openProjectListModal(['์ •์ƒ ์šด์˜', '๊ด€๋ฆฌ ์ฃผ์˜', '์œ„ํ—˜ ๋…ธ์ถœ', '์‚ฌ๋ง'][idx], stats[['active', 'warning', 'danger', 'dead'][idx]]); - } - } - } - }); - } catch (err) { console.error("๋„๋„› ์ฐจํŠธ ์—๋Ÿฌ:", err); } - - // 2. ์ „๋žต์  ์ž์‚ฐ ๋งคํŠธ๋ฆญ์Šค (Scatter) - ์ •๋ฐ€ ๋ณต๊ตฌ - try { - 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(); - - window.myVitalityChart = new Chart(vitalityCtx, { - type: 'scatter', - data: { - datasets: [{ - data: scatterData, - backgroundColor: (ctx) => { - const p = ctx.raw; - if (!p) return '#94a3b8'; - 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: (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: 45, left: 10, bottom: 10 } }, - scales: { - x: { - type: 'linear', min: 0, max: 500, - title: { display: true, text: '์ž์‚ฐ ๊ทœ๋ชจ (ํŒŒ์ผ ์ˆ˜)', font: { size: 11, weight: '700' } }, - grid: { display: false } - }, - y: { - min: 0, max: 100, - title: { display: true, text: '์šด์˜ ํ™œ๋ ฅ (AVI %)', font: { size: 11, weight: '700' } }, - grid: { display: false } - } - }, - plugins: { - legend: { display: false }, - datalabels: { - backgroundColor: 'rgba(255, 255, 255, 0.8)', - borderRadius: 4, padding: 4, - font: { size: 10, weight: '800' }, - formatter: (v) => v ? v.label : '', - display: (ctx) => ctx.raw && ctx.raw.isVip, - clip: false - }, - tooltip: { - 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("์ „๋žต ๋งคํŠธ๋ฆญ์Šค ์—๋Ÿฌ:", err); } -} - -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); - - container.innerHTML = ` -
- - - - - - - - - - - - - - - ${sortedData.map((p, idx) => { - const status = getStatusInfo(p.p_war, p.is_auto_delete); - const avi = p.p_war; - const vci = p.risk_count; - const oci = p.oci_score || 0; - const rowId = `project-${idx}`; - const grade = getVciGrade(vci); - - let rhythmLabel = oci >= 80 ? "์ •๊ธฐ์ " : oci >= 50 ? "์•ˆ์ •์ " : oci >= 20 ? "๊ฐ„ํ—์ " : "๋ถˆ๊ทœ์น™"; - let rhythmColor = oci >= 80 ? "#059669" : oci >= 50 ? "#1e5149" : oci >= 20 ? "#f59e0b" : "#dc2626"; - - // ์กด์žฌ ์‹ ๋ขฐ๋„ ํŒจ๋„ํ‹ฐ (ECV) ์ƒ์„ธ ์„ค๋ช… ๋ณต๊ตฌ - let ecvText = "100% (๋ฐ์ดํ„ฐ ์‹ค์ฒด ๊ฒ€์ฆ)"; - let ecvClass = "highlight-val"; - let ecvDesc = `ํ˜„์žฌ ${p.file_count}๊ฐœ์˜ ์œ ํšจ ์„ฑ๊ณผ๋ฌผ์ด ํ™•์ธ๋ฉ๋‹ˆ๋‹ค. ์‹œ์Šคํ…œ์ ์œผ๋กœ ์‹ค์ฒด๊ฐ€ ์™„๋ฒฝํžˆ ์กด์žฌํ•˜๋Š” ์ƒํƒœ์ž…๋‹ˆ๋‹ค.`; - if (p.file_count === 0) { - ecvText = "5% (์œ ๋ น ํ”„๋กœ์ ํŠธ ํŒ๋ช…)"; - ecvClass = "highlight-penalty"; - ecvDesc = "๋ฐ์ดํ„ฐ๊ฐ€ ์ „๋ฌดํ•˜์—ฌ ํ”„๋กœ์ ํŠธ์˜ ๋””์ง€ํ„ธ ์‹ค์ฒด๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ๋ชจ๋“  ๋ถ„์„์—์„œ ์ตœํ•˜์œ„ ํŒจ๋„ํ‹ฐ๊ฐ€ ์ ์šฉ๋ฉ๋‹ˆ๋‹ค."; - } else if (p.file_count < 10) { - ecvText = "40% (ํ˜•์‹์  ๊ป๋ฐ๊ธฐ ํŒ๋ช…)"; - ecvClass = "highlight-penalty"; - ecvDesc = "์ตœ์†Œ ์ˆ˜์ค€์˜ ๋ฌธ์„œ๋งŒ ์กด์žฌํ•˜๋ฉฐ, ์‹ค์งˆ์ ์ธ ์šด์˜ ๊ฐ€์น˜๋ฅผ ์ธ์ •ํ•˜๊ธฐ ์–ด๋ ค์šด ์†Œ๊ทœ๋ชจ ์ƒํƒœ์ž…๋‹ˆ๋‹ค."; - } - - // ํ™œ๋™ ํ’ˆ์งˆ ํ…์ŠคํŠธ ๋ณต๊ตฌ - const qualityLabel = p.log_quality >= 1.0 ? '์„ฑ๊ณผ๋ฌผ ์ค‘์‹ฌ์˜ ์‹ค๋ฌด ํ™œ๋™' : p.log_quality >= 0.7 ? '๊ตฌ์กฐ ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•œ ์‹œ์Šคํ…œ ํ™œ๋™' : '๋‹จ์ˆœ ํ–‰์ • ๊ธฐ๋ฐ˜์˜ ํ˜•์‹ ํ™œ๋™'; - const qualityDetail = p.log_quality >= 1.0 ? '์ตœ๊ทผ ๋กœ๊ทธ์—์„œ ํŒŒ์ผ ์—…๋กœ๋“œ/์ˆ˜์ • ๋“ฑ ๊ฐ€์น˜ ์ฆ๋ถ„ ํ™œ๋™์ด ๋ช…ํ™•ํžˆ ํฌ์ฐฉ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.' : p.log_quality >= 0.7 ? 'ํด๋” ์ƒ์„ฑ/์ด๋™ ๋“ฑ ๊ตฌ์กฐ์  ๊ด€๋ฆฌ๋Š” ์ด๋ค„์ง€๊ณ  ์žˆ์œผ๋‚˜, ์ง์ ‘์  ๊ฒฐ๊ณผ๋ฌผ ์ƒ์‚ฐ์€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค.' : '๋ฉ”์ผ ํ™•์ธ, ๊ถŒํ•œ ๋ณ€๊ฒฝ ๋“ฑ ์‹œ์Šคํ…œ ์œ ์ง€์„ฑ ํ™œ๋™ ์œ„์ฃผ๋กœ ํŒŒ์•…๋˜์–ด ํ’ˆ์งˆ ๊ฐ€์ค‘์น˜๊ฐ€ ๋‚ฎ๊ฒŒ ์ ์šฉ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.'; - - return ` - - - - - - - - - - - - - `; - }).join('')} - -
ํ”„๋กœ์ ํŠธ๋ช…ํŒŒ์ผ ์ˆ˜์ •์ฒด ์ผ์ˆ˜์ƒํƒœ ํŒ์ •๊ฐ€์น˜ ๊ธฐ์—ฌ (VCI) ์šด์˜ ํ™œ๋ ฅ (AVI) ์—…๋ฌด ์ง‘์ค‘๋„ ์šด์˜ ์ผ๊ด€์„ฑ (OCI)
${p.project_nm}${p.file_count.toLocaleString()}๊ฐœ${p.days_stagnant}์ผ${status.label} - ${vci > 0 ? '+' : ''}${vci.toFixed(1)} - ${avi.toFixed(1)}% -
- ${p.work_effort}% -
-
-
-
-
-
- ${oci}% - ${rhythmLabel} -
-
-
-
-
โš™๏ธ AI ์œ„ํ—˜ ์ ์‘ํ˜• ๋ชจ๋ธ(AAS) ๊ธฐ๋ฐ˜ ์ธ๊ณผ๊ด€๊ณ„ ๋ถ„์„
- -
-
-
- ๐Ÿ“Š ์‹ค์งˆ ์—…๋ฌด ์ง‘์ค‘๋„ (Job Focus) - ${p.work_effort}% -
-
-
์ตœ๊ทผ 30ํšŒ ์ˆ˜์ง‘ ์ด๋ ฅ ์ค‘ ๋‹จ์ˆœ ๋กœ๊ทธ ๊ฐฑ์‹ ์ด ์•„๋‹Œ ์‹ค์ œ ์„ฑ๊ณผ๋ฌผ์˜ ๋ณ€๋™์ด ํฌ์ฐฉ๋œ ๋‚ ์˜ ๋น„์œจ์ž…๋‹ˆ๋‹ค. ์ด๋Š” ์šด์˜์˜ '์ง„์ •์„ฑ'์„ ๋ณด์—ฌ์ฃผ๋Š” ํ•ต์‹ฌ ์ง€ํ‘œ์ž…๋‹ˆ๋‹ค.
-
-
-
-
VCI GRADE
-
${grade.label}
-
-
${grade.desc}
-
-
- -
-
-
1
-
-
๋™์  ์œ„ํ—˜ ๊ณ„์ˆ˜(ฮป) ์‚ฐ์ถœ
-
ํ”„๋กœ์ ํŠธ ๊ทœ๋ชจ๊ฐ€ ํด์ˆ˜๋ก ์ •๋ณด ๋ง์‹ค ์‹œ์˜ ์ถฉ๊ฒฉ์„ ๋ฐ˜์˜ํ•˜์—ฌ ๋ฐ์ดํ„ฐ์˜ ํ•˜๋ฝ ์†๋„๊ฐ€ ๊ฐ€์†๋ฉ๋‹ˆ๋‹ค. ํ˜„์žฌ ฮป=${p.ai_lambda.toFixed(4)}๋Š” ๊ท€ํ•˜์˜ ์ž์‚ฐ ๊ทœ๋ชจ๊ฐ€ ์ •๋ฐ€ํ•˜๊ฒŒ ํˆฌ์˜๋œ ๊ฒฐ๊ณผ์ž…๋‹ˆ๋‹ค.
-
Dynamic ฮป = ${p.ai_lambda.toFixed(4)}
-
-
-
-
4
-
-
ํ™œ๋™ ํ’ˆ์งˆ ๊ฒ€์ฆ (Quality)
-
์ตœ๊ทผ ๋กœ๊ทธ ๋ถ„์„ ๊ฒฐ๊ณผ ${qualityLabel}์œผ๋กœ ํŒ๋ช…๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ${qualityDetail}
-
Quality Factor = ${(p.log_quality * 100).toFixed(0)}%
-
-
-
-
2
-
-
๋ฐฉ์น˜ ์‹œ๊ฐ„ ๊ฐ์‡„ ์ ์šฉ
-
๋งˆ์ง€๋ง‰ ์œ ํšจ ํ™œ๋™ ์ดํ›„ ${p.days_stagnant}์ผ๊ฐ„์˜ ๋ˆ„์  ์ •์ฒด ์‹œ๊ฐ„์€ ์ง€์ˆ˜ ๊ฐ์‡„ ๊ณก์„ ์„ ๋”ฐ๋ผ ๋ฐ์ดํ„ฐ์˜ ์ตœ์‹ ์„ฑ๊ณผ ๊ฐ€์น˜๋ฅผ ์ƒ์‡„์‹œ์ผฐ์Šต๋‹ˆ๋‹ค.
-
Decay Result = ${((avi / (p.file_count === 0 ? 0.05 : p.file_count < 10 ? 0.4 : 1) / p.log_quality) || 0).toFixed(1)}%
-
-
-
-
3
-
-
์กด์žฌ ์ง„์ •์„ฑ (ECV)
-
${ecvDesc} ํŒŒ์ผ ์ˆ˜ ์ž์ฒด๊ฐ€ ๋ถ„์„์˜ ๋ฐ์ดํ„ฐ ์ง„์ •์„ฑ์„ ๋ณด์ •ํ•˜๋Š” ํ•ต์‹ฌ ํŒฉํ„ฐ๋กœ ์ž‘์šฉํ•ฉ๋‹ˆ๋‹ค.
-
Entity Factor = ${ecvText}
-
-
-
- -
-
-
- ๊ฐ€์น˜ ๊ธฐ์—ฌ๋„ (VCI) ์ง„๋‹จ: ${vci >= 0 ? '+' : ''}${vci.toFixed(2)} -
-
- ํ˜„์žฌ ํ”„๋กœ์ ํŠธ๋Š” ์šด์˜ ํ‘œ์ค€(AVI 70%) ๋Œ€๋น„ ${Math.abs(avi - 70).toFixed(1)}%p ${avi >= 70 ? '์ƒํšŒ' : 'ํ•˜ํšŒ'}ํ•˜๊ณ  ์žˆ์œผ๋ฉฐ, - ${p.file_count}๊ฐœ์˜ ์ž์‚ฐ ๊ทœ๋ชจ์— ๋”ฐ๋ฅธ ${((p.file_count / 200) + 0.5).toFixed(2)}๋ฐฐ์˜ ๊ฐ€์ค‘์น˜๊ฐ€ ์ ์šฉ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. - ์ด๋Š” ์‹œ์Šคํ…œ ์ „์ฒด ๊ด€์ ์—์„œ ${vci >= 0 ? '์ˆœ์ž์‚ฐ ๊ฐ€์น˜๋ฅผ ์ฆ๋Œ€' : '์ž ์žฌ์  ๊ธฐํšŒ๋น„์šฉ์„ ์†์‹ค'}์‹œํ‚ค๊ณ  ์žˆ๋Š” ์ƒํƒœ๋กœ ๋ถ„์„๋ฉ๋‹ˆ๋‹ค. -
-
-
- ์ตœ์ข… AVI: - ${avi.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) { - if (!detailRow.classList.contains('active')) { - document.querySelectorAll('.detail-row').forEach(row => row.classList.remove('active')); - detailRow.classList.add('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'); - } - } -} - -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.map(p => ``).join('')} -
ํ”„๋กœ์ ํŠธ๋ช…๊ด€๋ฆฌ์ž์ •์ฒด์ผAVI
${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 === '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 = ` -
VCI = (AVI - 70.0) ร— (Files / 200 + 0.5)
-

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

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

๋ฏธ๋ž˜ ์˜ˆ์ธก์ด ์•„๋‹Œ, ์ตœ๊ทผ 30์ผ๊ฐ„์˜ ํ™œ๋™ ๋ฆฌ๋“ฌ๊ณผ ๊ด€๋ฆฌ์˜ ๊ทœ์น™์„ฑ์„ ๋ถ„์„ํ•˜์—ฌ ์„ฑ์‹ค๋„๋ฅผ ์ ์ˆ˜ํ™”ํ•ฉ๋‹ˆ๋‹ค.

-
- - - - - - - - -
๋ถ„์„ ๊ฒฐ๊ณผ์ผ๊ด€์„ฑ ๋“ฑ๊ธ‰๊ด€๋ฆฌ ์‹ ๋ขฐ๋„
80%โ†‘๋งค์šฐ ์šฐ์ˆ˜์ฃผ ๋‹จ์œ„์˜ ์ •๊ธฐ์  ๊ด€๋ฆฌ๊ฐ€ ์™„๋ฒฝํžˆ ์ด๋ค„์ง
50~80%์–‘ํ˜ธ๊ฐ„ํ—์  ์ •์ฒด๋Š” ์žˆ์œผ๋‚˜ ๊พธ์ค€ํžˆ ๊ด€๋ฆฌ๋จ
20~50%์ฃผ์˜๋Œ๋ฐœ์  ํ™œ๋™ ์œ„์ฃผ, ๊ด€๋ฆฌ์˜ ๋ฆฌ๋“ฌ์ด ๊นจ์ง
20%โ†“๋งค์šฐ ๋ถˆ๋Ÿ‰์žฅ๊ธฐ ์ •์ฒด ์ค‘์ด๊ฑฐ๋‚˜ ๊ด€๋ฆฌ ์˜์ง€ ํ™•์ธ ๋ถˆ๊ฐ€
- `; - } else { - title.innerText = '์—…๋ฌด ์ง‘์ค‘๋„ (Job Focus) ๋“ฑ๊ธ‰ ๊ฐ€์ด๋“œ'; - body.innerHTML = ` -

์ตœ๊ทผ ์ˆ˜์ง‘ ๋กœ๊ทธ ์ค‘ ๋‹จ์ˆœ ํ–‰์ • ๋กœ๊ทธ๋ฅผ ์ œ์™ธํ•˜๊ณ  ์‹ค์งˆ์ ์ธ ์„ฑ๊ณผ๋ฌผ(ํŒŒ์ผ) ๋ณ€๋™์ด ํฌ์ฐฉ๋œ ๋น„์œจ์ž…๋‹ˆ๋‹ค.

- - - - - - - - -
๋น„์œจ (%)๋“ฑ๊ธ‰ํ™œ๋™ ์„ฑ๊ฒฉ
80%โ†‘Intensive์„ฑ๊ณผ๋ฌผ ์œ„์ฃผ์˜ ๊ณ ๋ฐ€๋„ ์ง‘์ค‘ ์ž‘์—…
50~80%Active์„ฑ๊ณผ์™€ ๊ด€๋ฆฌ๊ฐ€ ๊ท ํ˜• ์žกํžŒ ์›ํ™œํ•œ ์‹คํ–‰
20~50%Maintenance์„ค์ •/ํ–‰์ • ๋“ฑ ๋‹จ์ˆœ ๊ด€๋ฆฌ ์ค‘์‹ฌ์˜ ์ž‘์—…
20%โ†“Surface์‹ค์ฒด์  ๋ณ€ํ™”๊ฐ€ ์ ์€ ํ˜•์‹์  ๋กœ๊ทธ ์ค‘์‹ฌ
- `; - } - modal.style.display = 'flex'; -} - -function closeAnalysisModal() { document.getElementById('analysisModal').style.display = 'none'; } +/** + * Project Master Analysis JS + * AVI (Activity Vitality Index) & VCI (Value Contribution Index) ๋ถ„์„ ์—”์ง„ + * OCI (Operational Consistency Index) ํ†ตํ•ฉ ๋ฒ„์ „ + */ + +// Chart.js ํ”Œ๋Ÿฌ๊ทธ์ธ ์ „์—ญ ๋“ฑ๋ก +if (typeof ChartDataLabels !== 'undefined') { + Chart.register(ChartDataLabels); +} + +document.addEventListener('DOMContentLoaded', () => { + console.log("Business Analysis Engine initialized..."); + loadProjectAnalysisData(); +}); + +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); + + 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 = `* ์‹œ์Šคํ…œ ์ข…ํ•ฉ ์šด์˜ ํ™œ๋ ฅ(AVI): ${avg.avg_risk}% (ํ‰๊ท  ๊ด€๋ฆฌ ์ˆ˜์ค€)`; + } + } catch (e) { + console.error("๋ถ„์„ ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ ์‹คํŒจ:", e); + } +} + +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 getVciGrade(vci) { + if (vci >= 10) return { label: 'Masterpiece', class: 'grade-mvp', desc: '์‹œ์Šคํ…œ ๊ฐ€์น˜๋ฅผ ๊ฒฌ์ธํ•˜๋Š” ์ตœ์šฐ๋Ÿ‰ ํ•ต์‹ฌ ์ž์‚ฐ' }; + 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) + try { + const stats = { active: [], warning: [], danger: [], dead: [] }; + data.forEach(p => { + const status = getStatusInfo(p.p_war, p.is_auto_delete); + stats[status.key].push(p); + }); + + const statusCtx = document.getElementById('statusChart').getContext('2d'); + if (window.myStatusChart) window.myStatusChart.destroy(); + + window.myStatusChart = new Chart(statusCtx, { + type: 'doughnut', + data: { + labels: ['์ •์ƒ ์šด์˜', '๊ด€๋ฆฌ ์ฃผ์˜', '์œ„ํ—˜ ๋…ธ์ถœ', '์‚ฌ๋ง'], + datasets: [{ + data: [stats.active.length, stats.warning.length, stats.danger.length, stats.dead.length], + backgroundColor: ['#1E5149', '#22c55e', '#f59e0b', '#ef4444'], + borderWidth: 0 + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + layout: { padding: 15 }, + plugins: { + legend: { position: 'right', labels: { boxWidth: 10, font: { size: 11, weight: '700' }, usePointStyle: true } }, + datalabels: { display: false } + }, + cutout: '65%', + onClick: (e, elements) => { + if (elements.length > 0) { + const idx = elements[0].index; + openProjectListModal(['์ •์ƒ ์šด์˜', '๊ด€๋ฆฌ ์ฃผ์˜', '์œ„ํ—˜ ๋…ธ์ถœ', '์‚ฌ๋ง'][idx], stats[['active', 'warning', 'danger', 'dead'][idx]]); + } + } + } + }); + } catch (err) { console.error("๋„๋„› ์ฐจํŠธ ์—๋Ÿฌ:", err); } + + // 2. ์ „๋žต์  ์ž์‚ฐ ๋งคํŠธ๋ฆญ์Šค (Scatter) - ์ •๋ฐ€ ๋ณต๊ตฌ + try { + 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(); + + window.myVitalityChart = new Chart(vitalityCtx, { + type: 'scatter', + data: { + datasets: [{ + data: scatterData, + backgroundColor: (ctx) => { + const p = ctx.raw; + if (!p) return '#94a3b8'; + 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: (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: 45, left: 10, bottom: 10 } }, + scales: { + x: { + type: 'linear', min: 0, max: 500, + title: { display: true, text: '์ž์‚ฐ ๊ทœ๋ชจ (ํŒŒ์ผ ์ˆ˜)', font: { size: 11, weight: '700' } }, + grid: { display: false } + }, + y: { + min: 0, max: 100, + title: { display: true, text: '์šด์˜ ํ™œ๋ ฅ (AVI %)', font: { size: 11, weight: '700' } }, + grid: { display: false } + } + }, + plugins: { + legend: { display: false }, + datalabels: { + backgroundColor: 'rgba(255, 255, 255, 0.8)', + borderRadius: 4, padding: 4, + font: { size: 10, weight: '800' }, + formatter: (v) => v ? v.label : '', + display: (ctx) => ctx.raw && ctx.raw.isVip, + clip: false + }, + tooltip: { + 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("์ „๋žต ๋งคํŠธ๋ฆญ์Šค ์—๋Ÿฌ:", err); } +} + +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); + + container.innerHTML = ` +
+ + + + + + + + + + + + + + + ${sortedData.map((p, idx) => { + const status = getStatusInfo(p.p_war, p.is_auto_delete); + const avi = p.p_war; + const vci = p.risk_count; + const oci = p.oci_score || 0; + const rowId = `project-${idx}`; + const grade = getVciGrade(vci); + + let rhythmLabel = oci >= 80 ? "์ •๊ธฐ์ " : oci >= 50 ? "์•ˆ์ •์ " : oci >= 20 ? "๊ฐ„ํ—์ " : "๋ถˆ๊ทœ์น™"; + let rhythmColor = oci >= 80 ? "#059669" : oci >= 50 ? "#1e5149" : oci >= 20 ? "#f59e0b" : "#dc2626"; + + // ์กด์žฌ ์‹ ๋ขฐ๋„ ํŒจ๋„ํ‹ฐ (ECV) ์ƒ์„ธ ์„ค๋ช… ๋ณต๊ตฌ + let ecvText = "100% (๋ฐ์ดํ„ฐ ์‹ค์ฒด ๊ฒ€์ฆ)"; + let ecvClass = "highlight-val"; + let ecvDesc = `ํ˜„์žฌ ${p.file_count}๊ฐœ์˜ ์œ ํšจ ์„ฑ๊ณผ๋ฌผ์ด ํ™•์ธ๋ฉ๋‹ˆ๋‹ค. ์‹œ์Šคํ…œ์ ์œผ๋กœ ์‹ค์ฒด๊ฐ€ ์™„๋ฒฝํžˆ ์กด์žฌํ•˜๋Š” ์ƒํƒœ์ž…๋‹ˆ๋‹ค.`; + if (p.file_count === 0) { + ecvText = "5% (์œ ๋ น ํ”„๋กœ์ ํŠธ ํŒ๋ช…)"; + ecvClass = "highlight-penalty"; + ecvDesc = "๋ฐ์ดํ„ฐ๊ฐ€ ์ „๋ฌดํ•˜์—ฌ ํ”„๋กœ์ ํŠธ์˜ ๋””์ง€ํ„ธ ์‹ค์ฒด๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ๋ชจ๋“  ๋ถ„์„์—์„œ ์ตœํ•˜์œ„ ํŒจ๋„ํ‹ฐ๊ฐ€ ์ ์šฉ๋ฉ๋‹ˆ๋‹ค."; + } else if (p.file_count < 10) { + ecvText = "40% (ํ˜•์‹์  ๊ป๋ฐ๊ธฐ ํŒ๋ช…)"; + ecvClass = "highlight-penalty"; + ecvDesc = "์ตœ์†Œ ์ˆ˜์ค€์˜ ๋ฌธ์„œ๋งŒ ์กด์žฌํ•˜๋ฉฐ, ์‹ค์งˆ์ ์ธ ์šด์˜ ๊ฐ€์น˜๋ฅผ ์ธ์ •ํ•˜๊ธฐ ์–ด๋ ค์šด ์†Œ๊ทœ๋ชจ ์ƒํƒœ์ž…๋‹ˆ๋‹ค."; + } + + // ํ™œ๋™ ํ’ˆ์งˆ ํ…์ŠคํŠธ ๋ณต๊ตฌ + const qualityLabel = p.log_quality >= 1.0 ? '์„ฑ๊ณผ๋ฌผ ์ค‘์‹ฌ์˜ ์‹ค๋ฌด ํ™œ๋™' : p.log_quality >= 0.7 ? '๊ตฌ์กฐ ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•œ ์‹œ์Šคํ…œ ํ™œ๋™' : '๋‹จ์ˆœ ํ–‰์ • ๊ธฐ๋ฐ˜์˜ ํ˜•์‹ ํ™œ๋™'; + const qualityDetail = p.log_quality >= 1.0 ? '์ตœ๊ทผ ๋กœ๊ทธ์—์„œ ํŒŒ์ผ ์—…๋กœ๋“œ/์ˆ˜์ • ๋“ฑ ๊ฐ€์น˜ ์ฆ๋ถ„ ํ™œ๋™์ด ๋ช…ํ™•ํžˆ ํฌ์ฐฉ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.' : p.log_quality >= 0.7 ? 'ํด๋” ์ƒ์„ฑ/์ด๋™ ๋“ฑ ๊ตฌ์กฐ์  ๊ด€๋ฆฌ๋Š” ์ด๋ค„์ง€๊ณ  ์žˆ์œผ๋‚˜, ์ง์ ‘์  ๊ฒฐ๊ณผ๋ฌผ ์ƒ์‚ฐ์€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค.' : '๋ฉ”์ผ ํ™•์ธ, ๊ถŒํ•œ ๋ณ€๊ฒฝ ๋“ฑ ์‹œ์Šคํ…œ ์œ ์ง€์„ฑ ํ™œ๋™ ์œ„์ฃผ๋กœ ํŒŒ์•…๋˜์–ด ํ’ˆ์งˆ ๊ฐ€์ค‘์น˜๊ฐ€ ๋‚ฎ๊ฒŒ ์ ์šฉ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.'; + + return ` + + + + + + + + + + + + + `; + }).join('')} + +
ํ”„๋กœ์ ํŠธ๋ช…ํŒŒ์ผ ์ˆ˜์ •์ฒด ์ผ์ˆ˜์ƒํƒœ ํŒ์ •๊ฐ€์น˜ ๊ธฐ์—ฌ (VCI) ์šด์˜ ํ™œ๋ ฅ (AVI) ์—…๋ฌด ์ง‘์ค‘๋„ ์šด์˜ ์ผ๊ด€์„ฑ (OCI)
${p.project_nm}${p.file_count.toLocaleString()}๊ฐœ${p.days_stagnant}์ผ${status.label} + ${vci > 0 ? '+' : ''}${vci.toFixed(1)} + ${avi.toFixed(1)}% +
+ ${p.work_effort}% +
+
+
+
+
+
+ ${oci}% + ${rhythmLabel} +
+
+
+
+
โš™๏ธ AI ์œ„ํ—˜ ์ ์‘ํ˜• ๋ชจ๋ธ(AAS) ๊ธฐ๋ฐ˜ ์ธ๊ณผ๊ด€๊ณ„ ๋ถ„์„
+ +
+
+
+ ๐Ÿ“Š ์‹ค์งˆ ์—…๋ฌด ์ง‘์ค‘๋„ (Job Focus) + ${p.work_effort}% +
+
+
์ตœ๊ทผ 30ํšŒ ์ˆ˜์ง‘ ์ด๋ ฅ ์ค‘ ๋‹จ์ˆœ ๋กœ๊ทธ ๊ฐฑ์‹ ์ด ์•„๋‹Œ ์‹ค์ œ ์„ฑ๊ณผ๋ฌผ์˜ ๋ณ€๋™์ด ํฌ์ฐฉ๋œ ๋‚ ์˜ ๋น„์œจ์ž…๋‹ˆ๋‹ค. ์ด๋Š” ์šด์˜์˜ '์ง„์ •์„ฑ'์„ ๋ณด์—ฌ์ฃผ๋Š” ํ•ต์‹ฌ ์ง€ํ‘œ์ž…๋‹ˆ๋‹ค.
+
+
+
+
VCI GRADE
+
${grade.label}
+
+
${grade.desc}
+
+
+ +
+
+
1
+
+
๋™์  ์œ„ํ—˜ ๊ณ„์ˆ˜(ฮป) ์‚ฐ์ถœ
+
ํ”„๋กœ์ ํŠธ ๊ทœ๋ชจ๊ฐ€ ํด์ˆ˜๋ก ์ •๋ณด ๋ง์‹ค ์‹œ์˜ ์ถฉ๊ฒฉ์„ ๋ฐ˜์˜ํ•˜์—ฌ ๋ฐ์ดํ„ฐ์˜ ํ•˜๋ฝ ์†๋„๊ฐ€ ๊ฐ€์†๋ฉ๋‹ˆ๋‹ค. ํ˜„์žฌ ฮป=${p.ai_lambda.toFixed(4)}๋Š” ๊ท€ํ•˜์˜ ์ž์‚ฐ ๊ทœ๋ชจ๊ฐ€ ์ •๋ฐ€ํ•˜๊ฒŒ ํˆฌ์˜๋œ ๊ฒฐ๊ณผ์ž…๋‹ˆ๋‹ค.
+
Dynamic ฮป = ${p.ai_lambda.toFixed(4)}
+
+
+
+
4
+
+
ํ™œ๋™ ํ’ˆ์งˆ ๊ฒ€์ฆ (Quality)
+
์ตœ๊ทผ ๋กœ๊ทธ ๋ถ„์„ ๊ฒฐ๊ณผ ${qualityLabel}์œผ๋กœ ํŒ๋ช…๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ${qualityDetail}
+
Quality Factor = ${(p.log_quality * 100).toFixed(0)}%
+
+
+
+
2
+
+
๋ฐฉ์น˜ ์‹œ๊ฐ„ ๊ฐ์‡„ ์ ์šฉ
+
๋งˆ์ง€๋ง‰ ์œ ํšจ ํ™œ๋™ ์ดํ›„ ${p.days_stagnant}์ผ๊ฐ„์˜ ๋ˆ„์  ์ •์ฒด ์‹œ๊ฐ„์€ ์ง€์ˆ˜ ๊ฐ์‡„ ๊ณก์„ ์„ ๋”ฐ๋ผ ๋ฐ์ดํ„ฐ์˜ ์ตœ์‹ ์„ฑ๊ณผ ๊ฐ€์น˜๋ฅผ ์ƒ์‡„์‹œ์ผฐ์Šต๋‹ˆ๋‹ค.
+
Decay Result = ${((avi / (p.file_count === 0 ? 0.05 : p.file_count < 10 ? 0.4 : 1) / p.log_quality) || 0).toFixed(1)}%
+
+
+
+
3
+
+
์กด์žฌ ์ง„์ •์„ฑ (ECV)
+
${ecvDesc} ํŒŒ์ผ ์ˆ˜ ์ž์ฒด๊ฐ€ ๋ถ„์„์˜ ๋ฐ์ดํ„ฐ ์ง„์ •์„ฑ์„ ๋ณด์ •ํ•˜๋Š” ํ•ต์‹ฌ ํŒฉํ„ฐ๋กœ ์ž‘์šฉํ•ฉ๋‹ˆ๋‹ค.
+
Entity Factor = ${ecvText}
+
+
+
+ +
+
+
+
+ ๊ฐ€์น˜ ๊ธฐ์—ฌ๋„ (VCI) ์ง„๋‹จ: ${vci >= 0 ? '+' : ''}${vci.toFixed(2)} +
+
+ ์กฐ์ง ํ‰๊ท  ์ž์‚ฐ: ${p.avg_info.avg_files}๊ฐœ +
+
+
+ ํ˜„์žฌ ํ”„๋กœ์ ํŠธ๋Š” ํฌํŠธํด๋ฆฌ์˜ค ํ‰๊ท  ๊ด€๋ฆฌ ์ˆ˜์ค€ ๋Œ€๋น„ ${Math.abs(vci / Math.max(0.2, p.file_count / p.avg_info.avg_files)).toFixed(1)}%p ${vci >= 0 ? '์ƒํšŒ' : 'ํ•˜ํšŒ'}ํ•˜๊ณ  ์žˆ์œผ๋ฉฐ, + ${p.file_count}๊ฐœ์˜ ์ž์‚ฐ ๊ทœ๋ชจ์— ๋”ฐ๋ฅธ ${Math.max(0.2, p.file_count / p.avg_info.avg_files).toFixed(2)}๋ฐฐ์˜ ์ƒ๋Œ€ ๊ฐ€์ค‘์น˜๊ฐ€ ์ ์šฉ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. + ์ด๋Š” ์‹œ์Šคํ…œ ์ „์ฒด ๊ด€์ ์—์„œ ${vci >= 0 ? '์ˆœ์ž์‚ฐ ๊ฐ€์น˜๋ฅผ ์ฆ๋Œ€' : '์ž ์žฌ์  ๊ธฐํšŒ๋น„์šฉ์„ ์†์‹ค'}์‹œํ‚ค๊ณ  ์žˆ๋Š” ์ƒํƒœ๋กœ ๋ถ„์„๋ฉ๋‹ˆ๋‹ค. +
+
+
+ ์ตœ์ข… AVI: + ${avi.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) { + if (!detailRow.classList.contains('active')) { + document.querySelectorAll('.detail-row').forEach(row => row.classList.remove('active')); + detailRow.classList.add('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'); + } + } +} + +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.map(p => ``).join('')} +
ํ”„๋กœ์ ํŠธ๋ช…๊ด€๋ฆฌ์ž์ •์ฒด์ผAVI
${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 === 'avi') { + title.innerText = '์šด์˜ ํ™œ๋ ฅ ์ง€์ˆ˜ (AVI) ๋ถ„์„ ๊ฐ€์ด๋“œ'; + body.innerHTML = ` +
+ AVI = exp(-ฮป ร— Stagnant Days) ร— Quality ร— 100 +
+
+

์šด์˜ ํ™œ๋ ฅ ์ง€์ˆ˜(AVI)๋Š” ํ”„๋กœ์ ํŠธ๊ฐ€ ํ˜„์žฌ ์–ผ๋งˆ๋‚˜ ๊ฑด๊ฐ•ํ•˜๊ฒŒ ๊ฐ€๋™๋˜๊ณ  ์žˆ๋Š”์ง€๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” '๋””์ง€ํ„ธ ์ƒ์กด ์ง€ํ‘œ'์ž…๋‹ˆ๋‹ค.

+
    +
  • ์ง€์ˆ˜ ๊ฐ์‡„(Exponential Decay): ๋งˆ์ง€๋ง‰ ํ™œ๋™ ์ดํ›„ ์ •์ฒด ๊ธฐ๊ฐ„์ด ๊ธธ์–ด์งˆ์ˆ˜๋ก ์ž์‚ฐ์˜ ์ตœ์‹ ์„ฑ๊ณผ ๊ฐ€์น˜๋Š” ๊ธฐํ•˜๊ธ‰์ˆ˜์ ์œผ๋กœ ํ•˜๋ฝํ•ฉ๋‹ˆ๋‹ค.
  • +
  • ์œ„ํ—˜ ๊ฐ€์† ๊ณ„์ˆ˜(ฮป): ์ž์‚ฐ ๊ทœ๋ชจ(ํŒŒ์ผ ์ˆ˜)๊ฐ€ ํด์ˆ˜๋ก ๊ด€๋ฆฌ ๋ถ€์žฌ ์‹œ์˜ ์ •๋ณด ๋ง์‹ค ์œ„ํ—˜์ด ํฌ๋‹ค๊ณ  ํŒ๋‹จํ•˜์—ฌ, ๋” ๊ฐ€ํŒŒ๋ฅธ ๊ฐ์‡„ ๊ณก์„ ์„ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค.
  • +
  • ํ™œ๋™ ํ’ˆ์งˆ(Quality Factor): ๋‹จ์ˆœ ํ–‰์ • ๋กœ๊ทธ(๊ถŒํ•œ ๋ณ€๊ฒฝ ๋“ฑ)๋ณด๋‹ค ์‹ค๋ฌด ์„ฑ๊ณผ๋ฌผ(ํŒŒ์ผ ์—…๋กœ๋“œ ๋“ฑ)์ด ๋ฐœ์ƒํ–ˆ์„ ๋•Œ ์ง€์ˆ˜ ๋ณต์›๋ ฅ์„ ๋” ๋†’๊ฒŒ ๋ถ€์—ฌํ•ฉ๋‹ˆ๋‹ค.
  • +
+

โ€ป 70% ๋ฏธ๋งŒ ํ•˜๋ฝ ์‹œ, ํ•ด๋‹น ํ”„๋กœ์ ํŠธ์˜ ๋ฐ์ดํ„ฐ ๋…ธํ›„ํ™” ๋ฐ ๊ด€๋ฆฌ ๋ฐฉ์น˜ ์œ„ํ—˜์ด ์‹œ์ž‘๋œ ๊ฒƒ์œผ๋กœ ๊ฐ„์ฃผํ•ฉ๋‹ˆ๋‹ค.

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

VCI๋Š” ์•ผ๊ตฌ์˜ WAR(Wins Above Replacement) ๊ฐœ๋…์„ ๋„์ž…ํ•˜์—ฌ, ๊ฐœ๋ณ„ ํ”„๋กœ์ ํŠธ๊ฐ€ ์ „์ฒด ํฌํŠธํด๋ฆฌ์˜ค ํ‰๊ท  ๋Œ€๋น„ ์–ผ๋งˆ๋‚˜ ์กฐ์ง์˜ ๊ฐ€์น˜์— ๊ธฐ์—ฌํ•˜๋Š”์ง€ ์‚ฐ์ถœํ•œ ์ง€ํ‘œ์ž…๋‹ˆ๋‹ค.

+
+ VCI = (ํ˜„์žฌ AVI - ์ „์ฒด ํ‰๊ท  AVI) ร— (ํŒŒ์ผ ๊ทœ๋ชจ ๊ฐ€์ค‘์น˜) +
+

+ โ€ข 0.0 (ํ‰๊ท ): ์šฐ๋ฆฌ ์กฐ์ง์˜ ํ‰๊ท ์ ์ธ ๊ด€๋ฆฌ ์ˆ˜์ค€์„ ์œ ์ง€ ์ค‘์ธ ์ƒํƒœ
+ โ€ข (+) ์ ์ˆ˜: ํ‰๊ท  ์ด์ƒ์˜ ํ™œ๋ ฅ์œผ๋กœ ์กฐ์ง์˜ ๋””์ง€ํ„ธ ์ž์‚ฐ ๊ฐ€์น˜๋ฅผ ์ฆ๋Œ€์‹œํ‚ด
+ โ€ข (-) ์ ์ˆ˜: ํ‰๊ท  ์ดํ•˜์˜ ๋ฐฉ์น˜๋กœ ์ธํ•ด ์ž ์žฌ์  ๊ธฐํšŒ๋น„์šฉ ์†์‹ค ๋ฐœ์ƒ ์ค‘ +

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

๋ฏธ๋ž˜ ์˜ˆ์ธก์ด ์•„๋‹Œ, ์ตœ๊ทผ 30์ผ๊ฐ„์˜ ํ™œ๋™ ๋ฆฌ๋“ฌ๊ณผ ๊ด€๋ฆฌ์˜ ๊ทœ์น™์„ฑ์„ ๋ถ„์„ํ•˜์—ฌ ์„ฑ์‹ค๋„๋ฅผ ์ ์ˆ˜ํ™”ํ•ฉ๋‹ˆ๋‹ค.

+
+ + + + + + + + +
๋ถ„์„ ๊ฒฐ๊ณผ์ผ๊ด€์„ฑ ๋“ฑ๊ธ‰๊ด€๋ฆฌ ์‹ ๋ขฐ๋„
80%โ†‘๋งค์šฐ ์šฐ์ˆ˜์ฃผ ๋‹จ์œ„์˜ ์ •๊ธฐ์  ๊ด€๋ฆฌ๊ฐ€ ์™„๋ฒฝํžˆ ์ด๋ค„์ง
50~80%์–‘ํ˜ธ๊ฐ„ํ—์  ์ •์ฒด๋Š” ์žˆ์œผ๋‚˜ ๊พธ์ค€ํžˆ ๊ด€๋ฆฌ๋จ
20~50%์ฃผ์˜๋Œ๋ฐœ์  ํ™œ๋™ ์œ„์ฃผ, ๊ด€๋ฆฌ์˜ ๋ฆฌ๋“ฌ์ด ๊นจ์ง
20%โ†“๋งค์šฐ ๋ถˆ๋Ÿ‰์žฅ๊ธฐ ์ •์ฒด ์ค‘์ด๊ฑฐ๋‚˜ ๊ด€๋ฆฌ ์˜์ง€ ํ™•์ธ ๋ถˆ๊ฐ€
+ `; + } else { + title.innerText = '์—…๋ฌด ์ง‘์ค‘๋„ (Job Focus) ๋“ฑ๊ธ‰ ๊ฐ€์ด๋“œ'; + body.innerHTML = ` +

์ตœ๊ทผ ์ˆ˜์ง‘ ๋กœ๊ทธ ์ค‘ ๋‹จ์ˆœ ํ–‰์ • ๋กœ๊ทธ๋ฅผ ์ œ์™ธํ•˜๊ณ  ์‹ค์งˆ์ ์ธ ์„ฑ๊ณผ๋ฌผ(ํŒŒ์ผ) ๋ณ€๋™์ด ํฌ์ฐฉ๋œ ๋น„์œจ์ž…๋‹ˆ๋‹ค.

+ + + + + + + + +
๋น„์œจ (%)๋“ฑ๊ธ‰ํ™œ๋™ ์„ฑ๊ฒฉ
80%โ†‘Intensive์„ฑ๊ณผ๋ฌผ ์œ„์ฃผ์˜ ๊ณ ๋ฐ€๋„ ์ง‘์ค‘ ์ž‘์—…
50~80%Active์„ฑ๊ณผ์™€ ๊ด€๋ฆฌ๊ฐ€ ๊ท ํ˜• ์žกํžŒ ์›ํ™œํ•œ ์‹คํ–‰
20~50%Maintenance์„ค์ •/ํ–‰์ • ๋“ฑ ๋‹จ์ˆœ ๊ด€๋ฆฌ ์ค‘์‹ฌ์˜ ์ž‘์—…
20%โ†“Surface์‹ค์ฒด์  ๋ณ€ํ™”๊ฐ€ ์ ์€ ํ˜•์‹์  ๋กœ๊ทธ ์ค‘์‹ฌ
+ `; + } + modal.style.display = 'flex'; +} + +function closeAnalysisModal() { document.getElementById('analysisModal').style.display = 'none'; } diff --git a/js/analysis.js_fragment_leaderboard b/js/analysis.js_fragment_leaderboard index a00bd16..445ad4a 100644 --- a/js/analysis.js_fragment_leaderboard +++ b/js/analysis.js_fragment_leaderboard @@ -1,178 +1,178 @@ -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 avi = p.p_war; - const vci = p.risk_count; - const oci = p.oci_score || 0; - const rowId = `project-${idx}`; - - let rhythmLabel = ""; - let rhythmColor = ""; - if (oci >= 80) { rhythmLabel = "์ •๊ธฐ์ "; rhythmColor = "#059669"; } - else if (oci >= 50) { rhythmLabel = "์•ˆ์ •์ "; rhythmColor = "#1e5149"; } - else if (oci >= 20) { rhythmLabel = "๊ฐ„ํ—์ "; rhythmColor = "#f59e0b"; } - else { rhythmLabel = "๋ถˆ๊ทœ์น™"; rhythmColor = "#dc2626"; } - - // ์กด์žฌ ์‹ ๋ขฐ๋„ ํŒจ๋„ํ‹ฐ (ECV) ํ…์ŠคํŠธ ์ค€๋น„ - let ecvText = "100% (๋ฐ์ดํ„ฐ ์‹ ๋ขฐ)"; - let ecvClass = "highlight-val"; - let ecvDesc = "์ถฉ๋ถ„ํ•œ ์„ฑ๊ณผ๋ฌผ์ด ์กด์žฌํ•ฉ๋‹ˆ๋‹ค."; - if (p.file_count === 0) { - ecvText = "5% (์œ ๋ น ํ”„๋กœ์ ํŠธ)"; - ecvClass = "highlight-penalty"; - ecvDesc = "์„ฑ๊ณผ๋ฌผ์ด ์ „๋ฌดํ•˜์—ฌ ์‹œ์Šคํ…œ ๊ฐ€์น˜๊ฐ€ ์†Œ๋ฉธ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."; - } else if (p.file_count < 10) { - ecvText = "40% (์†Œ๊ทœ๋ชจ ๊ป๋ฐ๊ธฐ)"; - ecvClass = "highlight-penalty"; - ecvDesc = "์ตœ์†Œ ์ˆ˜์ค€์˜ ๋ฐ์ดํ„ฐ๋งŒ ์กด์žฌํ•˜์—ฌ ๊ฐ€์น˜๊ฐ€ ๋‚ฎ๊ฒŒ ํ‰๊ฐ€๋ฉ๋‹ˆ๋‹ค."; - } - - // ํ™œ๋™ ํ’ˆ์งˆ ํ…์ŠคํŠธ ์ค€๋น„ - const qualityLabel = p.log_quality >= 1.0 ? '์„ฑ๊ณผ๋ฌผ ์ง๊ฒฐ ์‹ค๋ฌด ํ™œ๋™' : p.log_quality >= 0.7 ? '์‹œ์Šคํ…œ ๊ตฌ์กฐ์  ํ™œ๋™' : '๋‹จ์ˆœ ํ–‰์ •์  ํ™œ๋™'; - - return ` - - - - - - - - - - - - - - `; - }).join('')} - -
ํ”„๋กœ์ ํŠธ๋ช…ํŒŒ์ผ ์ˆ˜๋ฐฉ์น˜์ผ์ƒํƒœ ํŒ์ • - ํ™œ๋ ฅ ์ง€์ˆ˜ (AVI) - ๊ฐ€์น˜ ๊ธฐ์—ฌ (VCI)์—…๋ฌด ์ง‘์ค‘๋„ - ์šด์˜ ์ผ๊ด€์„ฑ (OCI) -
${p.project_nm}${p.file_count.toLocaleString()}๊ฐœ${p.days_stagnant}์ผ${status.label} - ${avi.toFixed(1)}% - - ${vci >= 0 ? '+' : ''}${vci.toFixed(2)} - -
- - ${p.work_effort}% - -
-
-
-
-
-
- - ${oci}% - - - ${rhythmLabel} - -
-
-
-
-
- โš™๏ธ AI ์œ„ํ—˜ ์ ์‘ํ˜• ๋ชจ๋ธ(AAS) ์‚ฐ์ถœ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ -
- - -
-
- ๐Ÿ“Š ์‹ค์งˆ ์—…๋ฌด ์ง‘์ค‘๋„ ๋ถ„์„ (Job Focus) - - ์ง‘์ค‘๋„ ${p.work_effort}% - -
-
-
-
-
- ์ตœ๊ทผ 30๊ฐœ ์ˆ˜์ง‘ ์ด๋ ฅ ์ค‘ ๋‹จ์ˆœ ๋กœ๊ทธ ๊ฐฑ์‹ ์ด ์•„๋‹Œ ์‹ค์ œ ํŒŒ์ผ ์ˆ˜์˜ ๋ณ€๋™์ด ํฌ์ฐฉ๋œ ๋‚ ์˜ ๋น„์œจ์ž…๋‹ˆ๋‹ค. - ํ˜„์žฌ ์ด ํ”„๋กœ์ ํŠธ๋Š” ${p.work_effort >= 70 ? '๋งค์šฐ ๋ฐ€๋„ ๋†’์€ ์‹ค๋ฌด' : p.work_effort <= 30 ? 'ํ˜•์‹์  ๊ด€๋ฆฌ ์œ„์ฃผ์˜ ์ •์ฒด' : '๊ฐ„ํ—์ ์ธ ์„ฑ๊ณผ๋ฌผ'} ์ƒํƒœ๋ฅผ ๋ณด์ด๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. -
-
- - -
-
-
1
-
-
๋™์  ์œ„ํ—˜ ๊ณ„์ˆ˜(ฮป) ์‚ฐ์ถœ
-
์ž์‚ฐ ๊ทœ๋ชจ(${p.file_count}๊ฐœ) ๋ฐ ๋ถ€์„œ ์œ„ํ—˜๋„๋ฅผ ํ•ฉ์‚ฐํ•œ ํ•˜๋ฝ ์†๋„์ž…๋‹ˆ๋‹ค.
-
ฮป = ${p.ai_lambda.toFixed(4)}
-
-
-
-
4
-
-
ํ™œ๋™ ํ’ˆ์งˆ ๊ฒ€์ฆ (Quality)
-
- ์ตœ๊ทผ ๋กœ๊ทธ ๋ถ„์„ ๊ฒฐ๊ณผ ${qualityLabel}์œผ๋กœ ํŒ๋ช…๋˜์—ˆ์Šต๋‹ˆ๋‹ค. -
-
Factor = ${(p.log_quality * 100).toFixed(0)}%
-
-
- -
-
2
-
-
๋ฐฉ์น˜ ์‹œ๊ฐ„ ๊ฐ์‡„ ์ ์šฉ
-
${p.days_stagnant}์ผ๊ฐ„์˜ ์ •์ฒด๋กœ ์ธํ•œ ๊ฐ€์น˜ ๋ณด์กด์œจ์ž…๋‹ˆ๋‹ค.
-
Result = ${((avi / (p.file_count === 0 ? 0.05 : p.file_count < 10 ? 0.4 : 1) / p.log_quality) || 0).toFixed(1)}%
-
-
-
-
3
-
-
์กด์žฌ ์ง„์ •์„ฑ (ECV)
-
${ecvDesc}
-
Factor = ${ecvText}
-
-
-
- -
-
-
- ๊ฐ€์น˜ ๊ธฐ์—ฌ๋„ (VCI): ${vci >= 0 ? '+' : ''}${vci.toFixed(2)} -
-
* AVI 70% ๋Œ€๋น„ ํ”„๋กœ์ ํŠธ์˜ ์‹ค์งˆ์  ์ž์‚ฐ ํ•˜์ค‘ ๋ฐ˜์˜
-
-
- ์ตœ์ข… AVI: - ${avi.toFixed(1)}% -
-
-
-
-
-
- `; -} +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 avi = p.p_war; + const vci = p.risk_count; + const oci = p.oci_score || 0; + const rowId = `project-${idx}`; + + let rhythmLabel = ""; + let rhythmColor = ""; + if (oci >= 80) { rhythmLabel = "์ •๊ธฐ์ "; rhythmColor = "#059669"; } + else if (oci >= 50) { rhythmLabel = "์•ˆ์ •์ "; rhythmColor = "#1e5149"; } + else if (oci >= 20) { rhythmLabel = "๊ฐ„ํ—์ "; rhythmColor = "#f59e0b"; } + else { rhythmLabel = "๋ถˆ๊ทœ์น™"; rhythmColor = "#dc2626"; } + + // ์กด์žฌ ์‹ ๋ขฐ๋„ ํŒจ๋„ํ‹ฐ (ECV) ํ…์ŠคํŠธ ์ค€๋น„ + let ecvText = "100% (๋ฐ์ดํ„ฐ ์‹ ๋ขฐ)"; + let ecvClass = "highlight-val"; + let ecvDesc = "์ถฉ๋ถ„ํ•œ ์„ฑ๊ณผ๋ฌผ์ด ์กด์žฌํ•ฉ๋‹ˆ๋‹ค."; + if (p.file_count === 0) { + ecvText = "5% (์œ ๋ น ํ”„๋กœ์ ํŠธ)"; + ecvClass = "highlight-penalty"; + ecvDesc = "์„ฑ๊ณผ๋ฌผ์ด ์ „๋ฌดํ•˜์—ฌ ์‹œ์Šคํ…œ ๊ฐ€์น˜๊ฐ€ ์†Œ๋ฉธ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."; + } else if (p.file_count < 10) { + ecvText = "40% (์†Œ๊ทœ๋ชจ ๊ป๋ฐ๊ธฐ)"; + ecvClass = "highlight-penalty"; + ecvDesc = "์ตœ์†Œ ์ˆ˜์ค€์˜ ๋ฐ์ดํ„ฐ๋งŒ ์กด์žฌํ•˜์—ฌ ๊ฐ€์น˜๊ฐ€ ๋‚ฎ๊ฒŒ ํ‰๊ฐ€๋ฉ๋‹ˆ๋‹ค."; + } + + // ํ™œ๋™ ํ’ˆ์งˆ ํ…์ŠคํŠธ ์ค€๋น„ + const qualityLabel = p.log_quality >= 1.0 ? '์„ฑ๊ณผ๋ฌผ ์ง๊ฒฐ ์‹ค๋ฌด ํ™œ๋™' : p.log_quality >= 0.7 ? '์‹œ์Šคํ…œ ๊ตฌ์กฐ์  ํ™œ๋™' : '๋‹จ์ˆœ ํ–‰์ •์  ํ™œ๋™'; + + return ` + + + + + + + + + + + + + + `; + }).join('')} + +
ํ”„๋กœ์ ํŠธ๋ช…ํŒŒ์ผ ์ˆ˜๋ฐฉ์น˜์ผ์ƒํƒœ ํŒ์ • + ํ™œ๋ ฅ ์ง€์ˆ˜ (AVI) + ๊ฐ€์น˜ ๊ธฐ์—ฌ (VCI)์—…๋ฌด ์ง‘์ค‘๋„ + ์šด์˜ ์ผ๊ด€์„ฑ (OCI) +
${p.project_nm}${p.file_count.toLocaleString()}๊ฐœ${p.days_stagnant}์ผ${status.label} + ${avi.toFixed(1)}% + + ${vci >= 0 ? '+' : ''}${vci.toFixed(2)} + +
+ + ${p.work_effort}% + +
+
+
+
+
+
+ + ${oci}% + + + ${rhythmLabel} + +
+
+
+
+
+ โš™๏ธ AI ์œ„ํ—˜ ์ ์‘ํ˜• ๋ชจ๋ธ(AAS) ์‚ฐ์ถœ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ +
+ + +
+
+ ๐Ÿ“Š ์‹ค์งˆ ์—…๋ฌด ์ง‘์ค‘๋„ ๋ถ„์„ (Job Focus) + + ์ง‘์ค‘๋„ ${p.work_effort}% + +
+
+
+
+
+ ์ตœ๊ทผ 30๊ฐœ ์ˆ˜์ง‘ ์ด๋ ฅ ์ค‘ ๋‹จ์ˆœ ๋กœ๊ทธ ๊ฐฑ์‹ ์ด ์•„๋‹Œ ์‹ค์ œ ํŒŒ์ผ ์ˆ˜์˜ ๋ณ€๋™์ด ํฌ์ฐฉ๋œ ๋‚ ์˜ ๋น„์œจ์ž…๋‹ˆ๋‹ค. + ํ˜„์žฌ ์ด ํ”„๋กœ์ ํŠธ๋Š” ${p.work_effort >= 70 ? '๋งค์šฐ ๋ฐ€๋„ ๋†’์€ ์‹ค๋ฌด' : p.work_effort <= 30 ? 'ํ˜•์‹์  ๊ด€๋ฆฌ ์œ„์ฃผ์˜ ์ •์ฒด' : '๊ฐ„ํ—์ ์ธ ์„ฑ๊ณผ๋ฌผ'} ์ƒํƒœ๋ฅผ ๋ณด์ด๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. +
+
+ + +
+
+
1
+
+
๋™์  ์œ„ํ—˜ ๊ณ„์ˆ˜(ฮป) ์‚ฐ์ถœ
+
์ž์‚ฐ ๊ทœ๋ชจ(${p.file_count}๊ฐœ) ๋ฐ ๋ถ€์„œ ์œ„ํ—˜๋„๋ฅผ ํ•ฉ์‚ฐํ•œ ํ•˜๋ฝ ์†๋„์ž…๋‹ˆ๋‹ค.
+
ฮป = ${p.ai_lambda.toFixed(4)}
+
+
+
+
4
+
+
ํ™œ๋™ ํ’ˆ์งˆ ๊ฒ€์ฆ (Quality)
+
+ ์ตœ๊ทผ ๋กœ๊ทธ ๋ถ„์„ ๊ฒฐ๊ณผ ${qualityLabel}์œผ๋กœ ํŒ๋ช…๋˜์—ˆ์Šต๋‹ˆ๋‹ค. +
+
Factor = ${(p.log_quality * 100).toFixed(0)}%
+
+
+ +
+
2
+
+
๋ฐฉ์น˜ ์‹œ๊ฐ„ ๊ฐ์‡„ ์ ์šฉ
+
${p.days_stagnant}์ผ๊ฐ„์˜ ์ •์ฒด๋กœ ์ธํ•œ ๊ฐ€์น˜ ๋ณด์กด์œจ์ž…๋‹ˆ๋‹ค.
+
Result = ${((avi / (p.file_count === 0 ? 0.05 : p.file_count < 10 ? 0.4 : 1) / p.log_quality) || 0).toFixed(1)}%
+
+
+
+
3
+
+
์กด์žฌ ์ง„์ •์„ฑ (ECV)
+
${ecvDesc}
+
Factor = ${ecvText}
+
+
+
+ +
+
+
+ ๊ฐ€์น˜ ๊ธฐ์—ฌ๋„ (VCI): ${vci >= 0 ? '+' : ''}${vci.toFixed(2)} +
+
* AVI 70% ๋Œ€๋น„ ํ”„๋กœ์ ํŠธ์˜ ์‹ค์งˆ์  ์ž์‚ฐ ํ•˜์ค‘ ๋ฐ˜์˜
+
+
+ ์ตœ์ข… AVI: + ${avi.toFixed(1)}% +
+
+
+
+
+
+ `; +} diff --git a/js/analysis_test.js b/js/analysis_test.js new file mode 100644 index 0000000..dbbb1b6 --- /dev/null +++ b/js/analysis_test.js @@ -0,0 +1,485 @@ +/** + * Project Master Analysis JS (TEST VERSION) + * AVI (Activity Vitality Index) & VCI (Value Contribution Index) ๋ถ„์„ ์—”์ง„ + * OCI (Operational Consistency Index) ํ†ตํ•ฉ ๋ฒ„์ „ + */ + +// Chart.js ํ”Œ๋Ÿฌ๊ทธ์ธ ์ „์—ญ ๋“ฑ๋ก +if (typeof ChartDataLabels !== 'undefined') { + Chart.register(ChartDataLabels); +} + +document.addEventListener('DOMContentLoaded', () => { + console.log("Business Analysis Engine (TEST) initialized..."); + loadProjectAnalysisData(); +}); + +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); + + 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 = `* ์‹œ์Šคํ…œ ์ข…ํ•ฉ ์šด์˜ ํ™œ๋ ฅ(AVI): ${avg.avg_risk}% (ํ‰๊ท  ๊ด€๋ฆฌ ์ˆ˜์ค€) [TEST MODE]`; + } + } catch (e) { + console.error("๋ถ„์„ ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ ์‹คํŒจ:", e); + } +} + +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 getVciGrade(vci) { + if (vci >= 10) return { label: 'Masterpiece', class: 'grade-mvp', desc: '์‹œ์Šคํ…œ ๊ฐ€์น˜๋ฅผ ๊ฒฌ์ธํ•˜๋Š” ์ตœ์šฐ๋Ÿ‰ ํ•ต์‹ฌ ์ž์‚ฐ' }; + 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) + try { + const stats = { active: [], warning: [], danger: [], dead: [] }; + data.forEach(p => { + const status = getStatusInfo(p.p_war, p.is_auto_delete); + stats[status.key].push(p); + }); + + const statusCtx = document.getElementById('statusChart').getContext('2d'); + if (window.myStatusChart) window.myStatusChart.destroy(); + + window.myStatusChart = new Chart(statusCtx, { + type: 'doughnut', + data: { + labels: ['์ •์ƒ ์šด์˜', '๊ด€๋ฆฌ ์ฃผ์˜', '์œ„ํ—˜ ๋…ธ์ถœ', '์‚ฌ๋ง'], + datasets: [{ + data: [stats.active.length, stats.warning.length, stats.danger.length, stats.dead.length], + backgroundColor: ['#1E5149', '#22c55e', '#f59e0b', '#ef4444'], + borderWidth: 0 + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + layout: { padding: 15 }, + plugins: { + legend: { position: 'right', labels: { boxWidth: 10, font: { size: 11, weight: '700' }, usePointStyle: true } }, + datalabels: { display: false } + }, + cutout: '65%', + onClick: (e, elements) => { + if (elements.length > 0) { + const idx = elements[0].index; + openProjectListModal(['์ •์ƒ ์šด์˜', '๊ด€๋ฆฌ ์ฃผ์˜', '์œ„ํ—˜ ๋…ธ์ถœ', '์‚ฌ๋ง'][idx], stats[['active', 'warning', 'danger', 'dead'][idx]]); + } + } + } + }); + } catch (err) { console.error("๋„๋„› ์ฐจํŠธ ์—๋Ÿฌ:", err); } + + // 2. ์ „๋žต์  ์ž์‚ฐ ๋งคํŠธ๋ฆญ์Šค (Scatter) - ์ •๋ฐ€ ๋ณต๊ตฌ + try { + 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(); + + window.myVitalityChart = new Chart(vitalityCtx, { + type: 'scatter', + data: { + datasets: [{ + data: scatterData, + backgroundColor: (ctx) => { + const p = ctx.raw; + if (!p) return '#94a3b8'; + 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: (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: 45, left: 10, bottom: 10 } }, + scales: { + x: { + type: 'linear', min: 0, max: 500, + title: { display: true, text: '์ž์‚ฐ ๊ทœ๋ชจ (ํŒŒ์ผ ์ˆ˜)', font: { size: 11, weight: '700' } }, + grid: { display: false } + }, + y: { + min: 0, max: 100, + title: { display: true, text: '์šด์˜ ํ™œ๋ ฅ (AVI %)', font: { size: 11, weight: '700' } }, + grid: { display: false } + } + }, + plugins: { + legend: { display: false }, + datalabels: { + backgroundColor: 'rgba(255, 255, 255, 0.8)', + borderRadius: 4, padding: 4, + font: { size: 10, weight: '800' }, + formatter: (v) => v ? v.label : '', + display: (ctx) => ctx.raw && ctx.raw.isVip, + clip: false + }, + tooltip: { + 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("์ „๋žต ๋งคํŠธ๋ฆญ์Šค ์—๋Ÿฌ:", err); } +} + +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); + + container.innerHTML = ` +
+ + + + + + + + + + + + + + + ${sortedData.map((p, idx) => { + const status = getStatusInfo(p.p_war, p.is_auto_delete); + const avi = p.p_war; + const vci = p.risk_count; + const oci = p.oci_score || 0; + const rowId = `project-${idx}`; + const grade = getVciGrade(vci); + + let rhythmLabel = oci >= 80 ? "์ •๊ธฐ์ " : oci >= 50 ? "์•ˆ์ •์ " : oci >= 20 ? "๊ฐ„ํ—์ " : "๋ถˆ๊ทœ์น™"; + let rhythmColor = oci >= 80 ? "#059669" : oci >= 50 ? "#1e5149" : oci >= 20 ? "#f59e0b" : "#dc2626"; + + // ์กด์žฌ ์‹ ๋ขฐ๋„ ํŒจ๋„ํ‹ฐ (ECV) ์ƒ์„ธ ์„ค๋ช… ๋ณต๊ตฌ + let ecvText = "100% (๋ฐ์ดํ„ฐ ์‹ค์ฒด ๊ฒ€์ฆ)"; + let ecvClass = "highlight-val"; + let ecvDesc = `ํ˜„์žฌ ${p.file_count}๊ฐœ์˜ ์œ ํšจ ์„ฑ๊ณผ๋ฌผ์ด ํ™•์ธ๋ฉ๋‹ˆ๋‹ค. ์‹œ์Šคํ…œ์ ์œผ๋กœ ์‹ค์ฒด๊ฐ€ ์™„๋ฒฝํžˆ ์กด์žฌํ•˜๋Š” ์ƒํƒœ์ž…๋‹ˆ๋‹ค.`; + if (p.file_count === 0) { + ecvText = "5% (์œ ๋ น ํ”„๋กœ์ ํŠธ ํŒ๋ช…)"; + ecvClass = "highlight-penalty"; + ecvDesc = "๋ฐ์ดํ„ฐ๊ฐ€ ์ „๋ฌดํ•˜์—ฌ ํ”„๋กœ์ ํŠธ์˜ ๋””์ง€ํ„ธ ์‹ค์ฒด๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ๋ชจ๋“  ๋ถ„์„์—์„œ ์ตœํ•˜์œ„ ํŒจ๋„ํ‹ฐ๊ฐ€ ์ ์šฉ๋ฉ๋‹ˆ๋‹ค."; + } else if (p.file_count < 10) { + ecvText = "40% (ํ˜•์‹์  ๊ป๋ฐ๊ธฐ ํŒ๋ช…)"; + ecvClass = "highlight-penalty"; + ecvDesc = "์ตœ์†Œ ์ˆ˜์ค€์˜ ๋ฌธ์„œ๋งŒ ์กด์žฌํ•˜๋ฉฐ, ์‹ค์งˆ์ ์ธ ์šด์˜ ๊ฐ€์น˜๋ฅผ ์ธ์ •ํ•˜๊ธฐ ์–ด๋ ค์šด ์†Œ๊ทœ๋ชจ ์ƒํƒœ์ž…๋‹ˆ๋‹ค."; + } + + // ํ™œ๋™ ํ’ˆ์งˆ ํ…์ŠคํŠธ ๋ณต๊ตฌ + const qualityLabel = p.log_quality >= 1.0 ? '์„ฑ๊ณผ๋ฌผ ์ค‘์‹ฌ์˜ ์‹ค๋ฌด ํ™œ๋™' : p.log_quality >= 0.7 ? '๊ตฌ์กฐ ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•œ ์‹œ์Šคํ…œ ํ™œ๋™' : '๋‹จ์ˆœ ํ–‰์ • ๊ธฐ๋ฐ˜์˜ ํ˜•์‹ ํ™œ๋™'; + const qualityDetail = p.log_quality >= 1.0 ? '์ตœ๊ทผ ๋กœ๊ทธ์—์„œ ํŒŒ์ผ ์—…๋กœ๋“œ/์ˆ˜์ • ๋“ฑ ๊ฐ€์น˜ ์ฆ๋ถ„ ํ™œ๋™์ด ๋ช…ํ™•ํžˆ ํฌ์ฐฉ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.' : p.log_quality >= 0.7 ? 'ํด๋” ์ƒ์„ฑ/์ด๋™ ๋“ฑ ๊ตฌ์กฐ์  ๊ด€๋ฆฌ๋Š” ์ด๋ค„์ง€๊ณ  ์žˆ์œผ๋‚˜, ์ง์ ‘์  ๊ฒฐ๊ณผ๋ฌผ ์ƒ์‚ฐ์€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค.' : '๋ฉ”์ผ ํ™•์ธ, ๊ถŒํ•œ ๋ณ€๊ฒฝ ๋“ฑ ์‹œ์Šคํ…œ ์œ ์ง€์„ฑ ํ™œ๋™ ์œ„์ฃผ๋กœ ํŒŒ์•…๋˜์–ด ํ’ˆ์งˆ ๊ฐ€์ค‘์น˜๊ฐ€ ๋‚ฎ๊ฒŒ ์ ์šฉ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.'; + + return ` + + + + + + + + + + + + + `; + }).join('')} + +
ํ”„๋กœ์ ํŠธ๋ช…ํŒŒ์ผ ์ˆ˜์ •์ฒด ์ผ์ˆ˜์ƒํƒœ ํŒ์ •๊ฐ€์น˜ ๊ธฐ์—ฌ (VCI) ์šด์˜ ํ™œ๋ ฅ (AVI) ์—…๋ฌด ์ง‘์ค‘๋„ ์šด์˜ ์ผ๊ด€์„ฑ (OCI)
${p.project_nm}${p.file_count.toLocaleString()}๊ฐœ${p.days_stagnant}์ผ${status.label} + ${vci > 0 ? '+' : ''}${vci.toFixed(1)} + ${avi.toFixed(1)}% +
+ ${p.work_effort}% +
+
+
+
+
+
+ ${oci}% + ${rhythmLabel} +
+
+
+
+
โš™๏ธ AI ์œ„ํ—˜ ์ ์‘ํ˜• ๋ชจ๋ธ(AAS) ๊ธฐ๋ฐ˜ ์ธ๊ณผ๊ด€๊ณ„ ๋ถ„์„
+ +
+
+
+ ๐Ÿ“Š ์‹ค์งˆ ์—…๋ฌด ์ง‘์ค‘๋„ (Job Focus) + ${p.work_effort}% +
+
+
์ตœ๊ทผ 30ํšŒ ์ˆ˜์ง‘ ์ด๋ ฅ ์ค‘ ๋‹จ์ˆœ ๋กœ๊ทธ ๊ฐฑ์‹ ์ด ์•„๋‹Œ ์‹ค์ œ ์„ฑ๊ณผ๋ฌผ์˜ ๋ณ€๋™์ด ํฌ์ฐฉ๋œ ๋‚ ์˜ ๋น„์œจ์ž…๋‹ˆ๋‹ค. ์ด๋Š” ์šด์˜์˜ '์ง„์ •์„ฑ'์„ ๋ณด์—ฌ์ฃผ๋Š” ํ•ต์‹ฌ ์ง€ํ‘œ์ž…๋‹ˆ๋‹ค.
+
+
+
+
VCI GRADE
+
${grade.label}
+
+
${grade.desc}
+
+
+ +
+
+
1
+
+
๋™์  ์œ„ํ—˜ ๊ณ„์ˆ˜(ฮป) ์‚ฐ์ถœ
+
ํ”„๋กœ์ ํŠธ ๊ทœ๋ชจ๊ฐ€ ํด์ˆ˜๋ก ์ •๋ณด ๋ง์‹ค ์‹œ์˜ ์ถฉ๊ฒฉ์„ ๋ฐ˜์˜ํ•˜์—ฌ ๋ฐ์ดํ„ฐ์˜ ํ•˜๋ฝ ์†๋„๊ฐ€ ๊ฐ€์†๋ฉ๋‹ˆ๋‹ค. ํ˜„์žฌ ฮป=${p.ai_lambda.toFixed(4)}๋Š” ๊ท€ํ•˜์˜ ์ž์‚ฐ ๊ทœ๋ชจ๊ฐ€ ์ •๋ฐ€ํ•˜๊ฒŒ ํˆฌ์˜๋œ ๊ฒฐ๊ณผ์ž…๋‹ˆ๋‹ค.
+
Dynamic ฮป = ${p.ai_lambda.toFixed(4)}
+
+
+
+
4
+
+
ํ™œ๋™ ํ’ˆ์งˆ ๊ฒ€์ฆ (Quality)
+
์ตœ๊ทผ ๋กœ๊ทธ ๋ถ„์„ ๊ฒฐ๊ณผ ${qualityLabel}์œผ๋กœ ํŒ๋ช…๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ${qualityDetail}
+
Quality Factor = ${(p.log_quality * 100).toFixed(0)}%
+
+
+
+
2
+
+
๋ฐฉ์น˜ ์‹œ๊ฐ„ ๊ฐ์‡„ ์ ์šฉ
+
๋งˆ์ง€๋ง‰ ์œ ํšจ ํ™œ๋™ ์ดํ›„ ${p.days_stagnant}์ผ๊ฐ„์˜ ๋ˆ„์  ์ •์ฒด ์‹œ๊ฐ„์€ ์ง€์ˆ˜ ๊ฐ์‡„ ๊ณก์„ ์„ ๋”ฐ๋ผ ๋ฐ์ดํ„ฐ์˜ ์ตœ์‹ ์„ฑ๊ณผ ๊ฐ€์น˜๋ฅผ ์ƒ์‡„์‹œ์ผฐ์Šต๋‹ˆ๋‹ค.
+
Decay Result = ${((avi / (p.file_count === 0 ? 0.05 : p.file_count < 10 ? 0.4 : 1) / p.log_quality) || 0).toFixed(1)}%
+
+
+
+
3
+
+
์กด์žฌ ์ง„์ •์„ฑ (ECV)
+
${ecvDesc} ํŒŒ์ผ ์ˆ˜ ์ž์ฒด๊ฐ€ ๋ถ„์„์˜ ๋ฐ์ดํ„ฐ ์ง„์ •์„ฑ์„ ๋ณด์ •ํ•˜๋Š” ํ•ต์‹ฌ ํŒฉํ„ฐ๋กœ ์ž‘์šฉํ•ฉ๋‹ˆ๋‹ค.
+
Entity Factor = ${ecvText}
+
+
+
+ +
+
+
+
+ ๊ฐ€์น˜ ๊ธฐ์—ฌ๋„ (VCI) ์ง„๋‹จ: ${vci >= 0 ? '+' : ''}${vci.toFixed(2)} +
+
+ ์กฐ์ง ํ‰๊ท  ์ž์‚ฐ: ${p.avg_info.avg_files}๊ฐœ +
+
+
+ ํ˜„์žฌ ํ”„๋กœ์ ํŠธ๋Š” ํฌํŠธํด๋ฆฌ์˜ค ํ‰๊ท  ๊ด€๋ฆฌ ์ˆ˜์ค€ ๋Œ€๋น„ ${Math.abs(vci / Math.max(0.2, p.file_count / p.avg_info.avg_files)).toFixed(1)}%p ${vci >= 0 ? '์ƒํšŒ' : 'ํ•˜ํšŒ'}ํ•˜๊ณ  ์žˆ์œผ๋ฉฐ, + ${p.file_count}๊ฐœ์˜ ์ž์‚ฐ ๊ทœ๋ชจ์— ๋”ฐ๋ฅธ ${Math.max(0.2, p.file_count / p.avg_info.avg_files).toFixed(2)}๋ฐฐ์˜ ์ƒ๋Œ€ ๊ฐ€์ค‘์น˜๊ฐ€ ์ ์šฉ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. + ์ด๋Š” ์‹œ์Šคํ…œ ์ „์ฒด ๊ด€์ ์—์„œ ${vci >= 0 ? '์ˆœ์ž์‚ฐ ๊ฐ€์น˜๋ฅผ ์ฆ๋Œ€' : '์ž ์žฌ์  ๊ธฐํšŒ๋น„์šฉ์„ ์†์‹ค'}์‹œํ‚ค๊ณ  ์žˆ๋Š” ์ƒํƒœ๋กœ ๋ถ„์„๋ฉ๋‹ˆ๋‹ค. +
+
+
+ ์ตœ์ข… AVI: + ${avi.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) { + if (!detailRow.classList.contains('active')) { + document.querySelectorAll('.detail-row').forEach(row => row.classList.remove('active')); + detailRow.classList.add('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'); + } + } +} + +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.map(p => ``).join('')} +
ํ”„๋กœ์ ํŠธ๋ช…๊ด€๋ฆฌ์ž์ •์ฒด์ผAVI
${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 === 'avi') { + title.innerText = '์šด์˜ ํ™œ๋ ฅ ์ง€์ˆ˜ (AVI) ๋ถ„์„ ๊ฐ€์ด๋“œ (TEST)'; + body.innerHTML = ` +
+ AVI = exp(-ฮป ร— Stagnant Days) ร— Quality ร— 100 +
+
+

์šด์˜ ํ™œ๋ ฅ ์ง€์ˆ˜(AVI)๋Š” ํ”„๋กœ์ ํŠธ๊ฐ€ ํ˜„์žฌ ์–ผ๋งˆ๋‚˜ ๊ฑด๊ฐ•ํ•˜๊ฒŒ ๊ฐ€๋™๋˜๊ณ  ์žˆ๋Š”์ง€๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” '๋””์ง€ํ„ธ ์ƒ์กด ์ง€ํ‘œ'์ž…๋‹ˆ๋‹ค.

+
    +
  • ์ง€์ˆ˜ ๊ฐ์‡„(Exponential Decay): ๋งˆ์ง€๋ง‰ ํ™œ๋™ ์ดํ›„ ์ •์ฒด ๊ธฐ๊ฐ„์ด ๊ธธ์–ด์งˆ์ˆ˜๋ก ์ž์‚ฐ์˜ ์ตœ์‹ ์„ฑ๊ณผ ๊ฐ€์น˜๋Š” ๊ธฐํ•˜๊ธ‰์ˆ˜์ ์œผ๋กœ ํ•˜๋ฝํ•ฉ๋‹ˆ๋‹ค.
  • +
  • ์œ„ํ—˜ ๊ฐ€์† ๊ณ„์ˆ˜(ฮป): ์ž์‚ฐ ๊ทœ๋ชจ(ํŒŒ์ผ ์ˆ˜)๊ฐ€ ํด์ˆ˜๋ก ๊ด€๋ฆฌ ๋ถ€์žฌ ์‹œ์˜ ์ •๋ณด ๋ง์‹ค ์œ„ํ—˜์ด ํฌ๋‹ค๊ณ  ํŒ๋‹จํ•˜์—ฌ, ๋” ๊ฐ€ํŒŒ๋ฅธ ๊ฐ์‡„ ๊ณก์„ ์„ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค.
  • +
  • ํ™œ๋™ ํ’ˆ์งˆ(Quality Factor): ๋‹จ์ˆœ ํ–‰์ • ๋กœ๊ทธ(๊ถŒํ•œ ๋ณ€๊ฒฝ ๋“ฑ)๋ณด๋‹ค ์‹ค๋ฌด ์„ฑ๊ณผ๋ฌผ(ํŒŒ์ผ ์—…๋กœ๋“œ ๋“ฑ)์ด ๋ฐœ์ƒํ–ˆ์„ ๋•Œ ์ง€์ˆ˜ ๋ณต์›๋ ฅ์„ ๋” ๋†’๊ฒŒ ๋ถ€์—ฌํ•ฉ๋‹ˆ๋‹ค.
  • +
+

โ€ป 70% ๋ฏธ๋งŒ ํ•˜๋ฝ ์‹œ, ํ•ด๋‹น ํ”„๋กœ์ ํŠธ์˜ ๋ฐ์ดํ„ฐ ๋…ธํ›„ํ™” ๋ฐ ๊ด€๋ฆฌ ๋ฐฉ์น˜ ์œ„ํ—˜์ด ์‹œ์ž‘๋œ ๊ฒƒ์œผ๋กœ ๊ฐ„์ฃผํ•ฉ๋‹ˆ๋‹ค.

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

VCI๋Š” ์•ผ๊ตฌ์˜ WAR(Wins Above Replacement) ๊ฐœ๋…์„ ๋„์ž…ํ•˜์—ฌ, ๊ฐœ๋ณ„ ํ”„๋กœ์ ํŠธ๊ฐ€ ์ „์ฒด ํฌํŠธํด๋ฆฌ์˜ค ํ‰๊ท  ๋Œ€๋น„ ์–ผ๋งˆ๋‚˜ ์กฐ์ง์˜ ๊ฐ€์น˜์— ๊ธฐ์—ฌํ•˜๋Š”์ง€ ์‚ฐ์ถœํ•œ ์ง€ํ‘œ์ž…๋‹ˆ๋‹ค.

+
+ VCI = (ํ˜„์žฌ AVI - ์ „์ฒด ํ‰๊ท  AVI) ร— (ํŒŒ์ผ ๊ทœ๋ชจ ๊ฐ€์ค‘์น˜) +
+

+ โ€ข 0.0 (ํ‰๊ท ): ์šฐ๋ฆฌ ์กฐ์ง์˜ ํ‰๊ท ์ ์ธ ๊ด€๋ฆฌ ์ˆ˜์ค€์„ ์œ ์ง€ ์ค‘์ธ ์ƒํƒœ
+ โ€ข (+) ์ ์ˆ˜: ํ‰๊ท  ์ด์ƒ์˜ ํ™œ๋ ฅ์œผ๋กœ ์กฐ์ง์˜ ๋””์ง€ํ„ธ ์ž์‚ฐ ๊ฐ€์น˜๋ฅผ ์ฆ๋Œ€์‹œํ‚ด
+ โ€ข (-) ์ ์ˆ˜: ํ‰๊ท  ์ดํ•˜์˜ ๋ฐฉ์น˜๋กœ ์ธํ•ด ์ž ์žฌ์  ๊ธฐํšŒ๋น„์šฉ ์†์‹ค ๋ฐœ์ƒ ์ค‘ +

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

๋ฏธ๋ž˜ ์˜ˆ์ธก์ด ์•„๋‹Œ, ์ตœ๊ทผ 30์ผ๊ฐ„์˜ ํ™œ๋™ ๋ฆฌ๋“ฌ๊ณผ ๊ด€๋ฆฌ์˜ ๊ทœ์น™์„ฑ์„ ๋ถ„์„ํ•˜์—ฌ ์„ฑ์‹ค๋„๋ฅผ ์ ์ˆ˜ํ™”ํ•ฉ๋‹ˆ๋‹ค.

+
+ + + + + + + + +
๋ถ„์„ ๊ฒฐ๊ณผ์ผ๊ด€์„ฑ ๋“ฑ๊ธ‰๊ด€๋ฆฌ ์‹ ๋ขฐ๋„
80%โ†‘๋งค์šฐ ์šฐ์ˆ˜์ฃผ ๋‹จ์œ„์˜ ์ •๊ธฐ์  ๊ด€๋ฆฌ๊ฐ€ ์™„๋ฒฝํžˆ ์ด๋ค„์ง
50~80%์–‘ํ˜ธ๊ฐ„ํ—์  ์ •์ฒด๋Š” ์žˆ์œผ๋‚˜ ๊พธ์ค€ํžˆ ๊ด€๋ฆฌ๋จ
20~50%์ฃผ์˜๋Œ๋ฐœ์  ํ™œ๋™ ์œ„์ฃผ, ๊ด€๋ฆฌ์˜ ๋ฆฌ๋“ฌ์ด ๊นจ์ง
20%โ†“๋งค์šฐ ๋ถˆ๋Ÿ‰์žฅ๊ธฐ ์ •์ฒด ์ค‘์ด๊ฑฐ๋‚˜ ๊ด€๋ฆฌ ์˜์ง€ ํ™•์ธ ๋ถˆ๊ฐ€
+ `; + } else { + title.innerText = '์—…๋ฌด ์ง‘์ค‘๋„ (Job Focus) ๋“ฑ๊ธ‰ ๊ฐ€์ด๋“œ (TEST)'; + body.innerHTML = ` +

์ตœ๊ทผ ์ˆ˜์ง‘ ๋กœ๊ทธ ์ค‘ ๋‹จ์ˆœ ํ–‰์ • ๋กœ๊ทธ๋ฅผ ์ œ์™ธํ•˜๊ณ  ์‹ค์งˆ์ ์ธ ์„ฑ๊ณผ๋ฌผ(ํŒŒ์ผ) ๋ณ€๋™์ด ํฌ์ฐฉ๋œ ๋น„์œจ์ž…๋‹ˆ๋‹ค.

+ + + + + + + + +
๋น„์œจ (%)๋“ฑ๊ธ‰ํ™œ๋™ ์„ฑ๊ฒฉ
80%โ†‘Intensive์„ฑ๊ณผ๋ฌผ ์œ„์ฃผ์˜ ๊ณ ๋ฐ€๋„ ์ง‘์ค‘ ์ž‘์—…
50~80%Active์„ฑ๊ณผ์™€ ๊ด€๋ฆฌ๊ฐ€ ๊ท ํ˜• ์žกํžŒ ์›ํ™œํ•œ ์‹คํ–‰
20~50%Maintenance์„ค์ •/ํ–‰์ • ๋“ฑ ๋‹จ์ˆœ ๊ด€๋ฆฌ ์ค‘์‹ฌ์˜ ์ž‘์—…
20%โ†“Surface์‹ค์ฒด์  ๋ณ€ํ™”๊ฐ€ ์ ์€ ํ˜•์‹์  ๋กœ๊ทธ ์ค‘์‹ฌ
+ `; + } + modal.style.display = 'flex'; +} + +function closeAnalysisModal() { document.getElementById('analysisModal').style.display = 'none'; } diff --git a/js/common.js b/js/common.js index 6be7296..0c854b1 100644 --- a/js/common.js +++ b/js/common.js @@ -1,78 +1,78 @@ -/** - * Project Master Overseas Common JS - * ๊ณตํ†ต ๋„ค๋น„๊ฒŒ์ด์…˜, ํ†ตํ•ฉ ๋ชจ๋‹ฌ ๊ด€๋ฆฌ, ์œ ํ‹ธ๋ฆฌํ‹ฐ - */ - -// --- ๊ณตํ†ต ์ƒ์ˆ˜ --- -const API = { - INQUIRIES: '/api/inquiries', - PROJECT_DATA: '/project-data', - PROJECT_ACTIVITY: '/project-activity', - AVAILABLE_DATES: '/available-dates', - SYNC: '/sync', - STOP_SYNC: '/stop-sync', - AUTH_CRAWL: '/auth/crawl', - ANALYZE_FILE: '/analyze-file', - ATTACHMENTS: '/attachments' -}; - -// --- ๋„ค๋น„๊ฒŒ์ด์…˜ --- -function navigateTo(path) { - location.href = path; -} - -// --- ํ†ตํ•ฉ ๋ชจ๋‹ฌ ๊ด€๋ฆฌ์ž --- -const ModalManager = { - open(modalId) { - const modal = document.getElementById(modalId); - if (modal) { - modal.style.display = 'flex'; - // ํฌ์ปค์Šค ์ž๋™ ์ด๋™ (ID ์ž…๋ ฅ๋ž€์ด ์žˆ์œผ๋ฉด) - const firstInput = modal.querySelector('input'); - if (firstInput) firstInput.focus(); - } - }, - close(modalId) { - const modal = document.getElementById(modalId); - if (modal) modal.style.display = 'none'; - }, - closeAll() { - document.querySelectorAll('.modal-overlay').forEach(m => m.style.display = 'none'); - } -}; - -// --- ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜ --- -const Utils = { - formatDate(dateStr) { - if (!dateStr) return '-'; - return dateStr.replace(/-/g, '.'); - }, - - // ์ƒํƒœ๋ณ„ CSS ํด๋ž˜์Šค ๋งคํ•‘ - getStatusClass(status) { - const map = { - '์™„๋ฃŒ': 'status-complete', - '์ž‘์—… ์ค‘': 'status-working', - 'ํ™•์ธ ์ค‘': 'status-checking', - '์ •์ƒ': 'active', - '์ฃผ์˜': 'warning', - '๋ฐฉ์น˜': 'stale', - '๋ฐ์ดํ„ฐ ์—†์Œ': 'unknown' - }; - return map[status] || 'status-pending'; - }, - - // ํ•œ๊ธ€ ํŒŒ์ผ๋ช… ์ธ์ฝ”๋”ฉ ์•ˆ์ „ ์ฒ˜๋ฆฌ - getSafeFileUrl(filename) { - return `/sample_files/${encodeURIComponent(filename)}`; - } -}; - -// --- ์ „์—ญ ์ด๋ฒคํŠธ --- -document.addEventListener('keydown', (e) => { - if (e.key === 'Escape') ModalManager.closeAll(); -}); - -document.addEventListener('DOMContentLoaded', () => { - console.log("Common module initialized."); -}); +/** + * Project Master Overseas Common JS + * ๊ณตํ†ต ๋„ค๋น„๊ฒŒ์ด์…˜, ํ†ตํ•ฉ ๋ชจ๋‹ฌ ๊ด€๋ฆฌ, ์œ ํ‹ธ๋ฆฌํ‹ฐ + */ + +// --- ๊ณตํ†ต ์ƒ์ˆ˜ --- +const API = { + INQUIRIES: '/api/inquiries', + PROJECT_DATA: '/project-data', + PROJECT_ACTIVITY: '/project-activity', + AVAILABLE_DATES: '/available-dates', + SYNC: '/sync', + STOP_SYNC: '/stop-sync', + AUTH_CRAWL: '/auth/crawl', + ANALYZE_FILE: '/analyze-file', + ATTACHMENTS: '/attachments' +}; + +// --- ๋„ค๋น„๊ฒŒ์ด์…˜ --- +function navigateTo(path) { + location.href = path; +} + +// --- ํ†ตํ•ฉ ๋ชจ๋‹ฌ ๊ด€๋ฆฌ์ž --- +const ModalManager = { + open(modalId) { + const modal = document.getElementById(modalId); + if (modal) { + modal.style.display = 'flex'; + // ํฌ์ปค์Šค ์ž๋™ ์ด๋™ (ID ์ž…๋ ฅ๋ž€์ด ์žˆ์œผ๋ฉด) + const firstInput = modal.querySelector('input'); + if (firstInput) firstInput.focus(); + } + }, + close(modalId) { + const modal = document.getElementById(modalId); + if (modal) modal.style.display = 'none'; + }, + closeAll() { + document.querySelectorAll('.modal-overlay').forEach(m => m.style.display = 'none'); + } +}; + +// --- ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜ --- +const Utils = { + formatDate(dateStr) { + if (!dateStr) return '-'; + return dateStr.replace(/-/g, '.'); + }, + + // ์ƒํƒœ๋ณ„ CSS ํด๋ž˜์Šค ๋งคํ•‘ + getStatusClass(status) { + const map = { + '์™„๋ฃŒ': 'status-complete', + '์ž‘์—… ์ค‘': 'status-working', + 'ํ™•์ธ ์ค‘': 'status-checking', + '์ •์ƒ': 'active', + '์ฃผ์˜': 'warning', + '๋ฐฉ์น˜': 'stale', + '๋ฐ์ดํ„ฐ ์—†์Œ': 'unknown' + }; + return map[status] || 'status-pending'; + }, + + // ํ•œ๊ธ€ ํŒŒ์ผ๋ช… ์ธ์ฝ”๋”ฉ ์•ˆ์ „ ์ฒ˜๋ฆฌ + getSafeFileUrl(filename) { + return `/sample_files/${encodeURIComponent(filename)}`; + } +}; + +// --- ์ „์—ญ ์ด๋ฒคํŠธ --- +document.addEventListener('keydown', (e) => { + if (e.key === 'Escape') ModalManager.closeAll(); +}); + +document.addEventListener('DOMContentLoaded', () => { + console.log("Common module initialized."); +}); diff --git a/js/dashboard.js b/js/dashboard.js index 726802a..6f10db4 100644 --- a/js/dashboard.js +++ b/js/dashboard.js @@ -1,237 +1,237 @@ -/** - * Project Master Overseas Dashboard JS - * ๊ธฐ๋Šฅ: ๋ฐ์ดํ„ฐ ๋กœ๋“œ, ํ™œ์„ฑ๋„ ๋ถ„์„, ์ธ์ฆ ๋ชจ๋‹ฌ ์ œ์–ด, ํฌ๋กค๋ง ๋™๊ธฐํ™” ๋ฐ ์ค‘๋‹จ - */ - -// --- ๊ธ€๋กœ๋ฒŒ ์ƒํƒœ ๊ด€๋ฆฌ --- -let rawData = []; -let projectActivityDetails = []; -let isCrawling = false; - -const CONTINENT_ORDER = { "์•„์‹œ์•„": 1, "์•„ํ”„๋ฆฌ์นด": 2, "์•„๋ฉ”๋ฆฌ์นด": 3, "์ง€์‚ฌ": 4 }; - -// --- ์ดˆ๊ธฐํ™” --- -async function init() { - console.log("Dashboard Initializing..."); - if (!document.getElementById('projectAccordion')) return; - - await loadAvailableDates(); - await loadDataByDate(); -} - -// --- ๋ฐ์ดํ„ฐ ํ†ต์‹  ๋ฐ ๋กœ๋“œ --- -async function loadAvailableDates() { - try { - const response = await fetch(API.AVAILABLE_DATES); - const dates = await response.json(); - if (dates?.length > 0) { - const selectHtml = ` - `; - const baseDateStrong = document.getElementById('baseDate'); - if (baseDateStrong) baseDateStrong.innerHTML = selectHtml; - } - } catch (e) { console.error("๋‚ ์งœ ๋กœ๋“œ ์‹คํŒจ:", e); } -} - -async function loadDataByDate(selectedDate = "") { - try { - await loadActivityAnalysis(selectedDate); - const url = selectedDate ? `${API.PROJECT_DATA}?date=${selectedDate}` : `${API.PROJECT_DATA}?t=${Date.now()}`; - const response = await fetch(url); - const data = await response.json(); - if (data.error) throw new Error(data.error); - rawData = data.projects || []; - renderDashboard(rawData); - } catch (e) { - console.error("๋ฐ์ดํ„ฐ ๋กœ๋“œ ์‹คํŒจ:", e); - alert("๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๋ฐ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."); - } -} - -async function loadActivityAnalysis(date = "") { - const dashboard = document.getElementById('activityDashboard'); - if (!dashboard) return; - try { - const url = date ? `${API.PROJECT_ACTIVITY}?date=${date}` : API.PROJECT_ACTIVITY; - const response = await fetch(url); - const data = await response.json(); - if (data.error) return; - const { summary, details } = data; - projectActivityDetails = details; - dashboard.innerHTML = ` -
-
์ •์ƒ (7์ผ ์ด๋‚ด)
${summary.active}
-
-
-
์ฃผ์˜ (14์ผ ์ด๋‚ด)
${summary.warning}
-
-
-
๋ฐฉ์น˜ (14์ผ ์ดˆ๊ณผ / ํด๋”์ž๋™์‚ญ์ œ)
${summary.stale}
-
-
-
๋ฐ์ดํ„ฐ ์—†์Œ (ํŒŒ์ผ 0๊ฐœ)
${summary.unknown}
-
`; - } catch (e) { console.error("๋ถ„์„ ๋กœ๋“œ ์‹คํŒจ:", e); } -} - -// --- ๋ Œ๋”๋ง ์—”์ง„ --- -function renderDashboard(data) { - const container = document.getElementById('projectAccordion'); - container.innerHTML = ''; - const grouped = groupData(data); - Object.keys(grouped).sort((a, b) => (CONTINENT_ORDER[a] || 99) - (CONTINENT_ORDER[b] || 99)).forEach(continent => { - const continentDiv = document.createElement('div'); - continentDiv.className = 'continent-group active'; - let html = `
${continent}โ–ผ
`; - Object.keys(grouped[continent]).sort().forEach(country => { - html += `
${country}โ–ผ
-
ํ”„๋กœ์ ํŠธ๋ช…
๋‹ด๋‹น๋ถ€์„œ
๋‹ด๋‹น์ž
ํŒŒ์ผ์ˆ˜
์ตœ๊ทผ๋กœ๊ทธ
- ${grouped[continent][country].sort((a, b) => a[0].localeCompare(b[0])).map(p => createProjectHtml(p)).join('')}
`; - }); - html += `
`; - continentDiv.innerHTML = html; - container.appendChild(continentDiv); - }); -} - -function groupData(data) { - const res = {}; - data.forEach(item => { - const c1 = item[5] || "๊ธฐํƒ€", c2 = item[6] || "๋ฏธ๋ถ„๋ฅ˜"; - if (!res[c1]) res[c1] = {}; - if (!res[c1][c2]) res[c1][c2] = []; - res[c1][c2].push(item); - }); - return res; -} - -function createProjectHtml(p) { - const [name, dept, admin, logRaw, files] = p; - const recentLog = (!logRaw || logRaw === "X" || logRaw === "๋ฐ์ดํ„ฐ ์—†์Œ") ? "๊ธฐ๋ก ์—†์Œ" : logRaw; - const logTime = recentLog !== "๊ธฐ๋ก ์—†์Œ" ? recentLog.split(',')[0] : "๊ธฐ๋ก ์—†์Œ"; - - const isStaleLog = recentLog.replace(/\s/g, "").includes("ํด๋”์ž๋™์‚ญ์ œ"); - const isNoFiles = (files === 0 || files === null); - const statusClass = isNoFiles ? "status-error" : ""; - - let logStyleClass = ""; - if (isStaleLog) logStyleClass = "error-text"; - else if (recentLog === "๊ธฐ๋ก ์—†์Œ") logStyleClass = "warning-text"; - - const logBoldStyle = isStaleLog ? 'font-weight: 800;' : ''; - - return ` -
-
-
${name}
${dept}
${admin}
${files || 0}
${recentLog}
-
-
-
-
-

์ฐธ์—ฌ ์ธ์› ์ƒ์„ธ

- - - -
์ด๋ฆ„์†Œ์†๊ถŒํ•œ
${admin}${dept}๊ด€๋ฆฌ์ž
-
-
-

์ตœ๊ทผ ํ™œ๋™

- - - -
์œ ํ˜•๋‚ด์šฉ์ผ์‹œ
๋กœ๊ทธ๋™๊ธฐํ™” ์™„๋ฃŒ${logTime}
-
-
-
-
`; -} - -// --- ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ --- -function toggleGroup(h) { h.parentElement.classList.toggle('active'); } -function toggleAccordion(h) { - const item = h.parentElement; - item.parentElement.querySelectorAll('.accordion-item').forEach(el => { if (el !== item) el.classList.remove('active'); }); - item.classList.toggle('active'); -} - -function showActivityDetails(status) { - const names = { active: '์ •์ƒ', warning: '์ฃผ์˜', stale: '๋ฐฉ์น˜', unknown: '๋ฐ์ดํ„ฐ ์—†์Œ' }; - const filtered = (projectActivityDetails || []).filter(d => d.status === status); - document.getElementById('modalTitle').innerText = `${names[status]} ๋ชฉ๋ก (${filtered.length}๊ฐœ)`; - document.getElementById('modalTableBody').innerHTML = filtered.map(p => { - const o = rawData.find(r => r[0] === p.name); - return `${p.name}${o ? o[1] : "-"}${o ? o[2] : "-"}`; - }).join(''); - ModalManager.open('activityDetailModal'); -} - -function scrollToProject(name) { - ModalManager.close('activityDetailModal'); - const target = Array.from(document.querySelectorAll('.repo-title')).find(t => t.innerText.trim() === name.trim())?.closest('.accordion-header'); - if (target) { - let p = target.parentElement; - while (p && p !== document.body) { - if (p.classList.contains('continent-group') || p.classList.contains('country-group')) p.classList.add('active'); - p = p.parentElement; - } - target.parentElement.classList.add('active'); - const pos = target.getBoundingClientRect().top + window.pageYOffset - 260; - window.scrollTo({ top: pos, behavior: 'smooth' }); - target.style.backgroundColor = 'var(--primary-lv-1)'; - setTimeout(() => target.style.backgroundColor = '', 2000); - } -} - -// --- ํฌ๋กค๋ง ๋ฐ ์ธ์ฆ ์ œ์–ด --- -async function syncData() { - if (isCrawling) { - if (confirm("ํฌ๋กค๋ง์„ ์ค‘๋‹จํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?")) { - const res = await fetch(API.STOP_SYNC); - if ((await res.json()).success) document.getElementById('syncBtn').innerText = "์ค‘๋‹จ ์š”์ฒญ ์ค‘..."; - } - return; - } - document.getElementById('authId').value = ''; - document.getElementById('authPw').value = ''; - document.getElementById('authErrorMessage').style.display = 'none'; - ModalManager.open('authModal'); -} - -async function submitAuth() { - const id = document.getElementById('authId').value, pw = document.getElementById('authPw').value, err = document.getElementById('authErrorMessage'); - try { - const res = await fetch(API.AUTH_CRAWL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ user_id: id, password: pw }) }); - const data = await res.json(); - if (data.success) { ModalManager.close('authModal'); startCrawlProcess(); } - else { err.innerText = "ํฌ๋กค๋ง์„ ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."; err.style.display = 'block'; } - } catch { err.innerText = "์„œ๋ฒ„ ์—ฐ๊ฒฐ ์‹คํŒจ"; err.style.display = 'block'; } -} - -async function startCrawlProcess() { - isCrawling = true; - const btn = document.getElementById('syncBtn'), logC = document.getElementById('logConsole'), logB = document.getElementById('logBody'); - btn.classList.add('loading'); btn.style.backgroundColor = 'var(--error-color)'; btn.innerHTML = ` ํฌ๋กค๋ง ์ค‘๋‹จ`; - logC.style.display = 'block'; logB.innerHTML = '
>>> ์—”์ง„ ์ดˆ๊ธฐํ™” ์ค‘...
'; - try { - const res = await fetch(API.SYNC); - const reader = res.body.getReader(), decoder = new TextDecoder(); - while (true) { - const { done, value } = await reader.read(); if (done) break; - decoder.decode(value).split('\n').forEach(line => { - if (line.startsWith('data: ')) { - const p = JSON.parse(line.substring(6)); - if (p.type === 'log') { - const div = document.createElement('div'); div.innerText = `[${new Date().toLocaleTimeString()}] ${p.message}`; - logB.appendChild(div); logC.scrollTop = logC.scrollHeight; - } else if (p.type === 'done') { init(); alert(`๋™๊ธฐํ™” ์ข…๋ฃŒ`); logC.style.display = 'none'; } - } - }); - } - } catch { alert("์ŠคํŠธ๋ฆผ ๋Š๊น€"); } - finally { isCrawling = false; btn.classList.remove('loading'); btn.style.backgroundColor = ''; btn.innerHTML = ` ๋ฐ์ดํ„ฐ ๋™๊ธฐํ™” (ํฌ๋กค๋ง)`; } -} - -document.addEventListener('DOMContentLoaded', init); +/** + * Project Master Overseas Dashboard JS + * ๊ธฐ๋Šฅ: ๋ฐ์ดํ„ฐ ๋กœ๋“œ, ํ™œ์„ฑ๋„ ๋ถ„์„, ์ธ์ฆ ๋ชจ๋‹ฌ ์ œ์–ด, ํฌ๋กค๋ง ๋™๊ธฐํ™” ๋ฐ ์ค‘๋‹จ + */ + +// --- ๊ธ€๋กœ๋ฒŒ ์ƒํƒœ ๊ด€๋ฆฌ --- +let rawData = []; +let projectActivityDetails = []; +let isCrawling = false; + +const CONTINENT_ORDER = { "์•„์‹œ์•„": 1, "์•„ํ”„๋ฆฌ์นด": 2, "์•„๋ฉ”๋ฆฌ์นด": 3, "์ง€์‚ฌ": 4 }; + +// --- ์ดˆ๊ธฐํ™” --- +async function init() { + console.log("Dashboard Initializing..."); + if (!document.getElementById('projectAccordion')) return; + + await loadAvailableDates(); + await loadDataByDate(); +} + +// --- ๋ฐ์ดํ„ฐ ํ†ต์‹  ๋ฐ ๋กœ๋“œ --- +async function loadAvailableDates() { + try { + const response = await fetch(API.AVAILABLE_DATES); + const dates = await response.json(); + if (dates?.length > 0) { + const selectHtml = ` + `; + const baseDateStrong = document.getElementById('baseDate'); + if (baseDateStrong) baseDateStrong.innerHTML = selectHtml; + } + } catch (e) { console.error("๋‚ ์งœ ๋กœ๋“œ ์‹คํŒจ:", e); } +} + +async function loadDataByDate(selectedDate = "") { + try { + await loadActivityAnalysis(selectedDate); + const url = selectedDate ? `${API.PROJECT_DATA}?date=${selectedDate}` : `${API.PROJECT_DATA}?t=${Date.now()}`; + const response = await fetch(url); + const data = await response.json(); + if (data.error) throw new Error(data.error); + rawData = data.projects || []; + renderDashboard(rawData); + } catch (e) { + console.error("๋ฐ์ดํ„ฐ ๋กœ๋“œ ์‹คํŒจ:", e); + alert("๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๋ฐ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."); + } +} + +async function loadActivityAnalysis(date = "") { + const dashboard = document.getElementById('activityDashboard'); + if (!dashboard) return; + try { + const url = date ? `${API.PROJECT_ACTIVITY}?date=${date}` : API.PROJECT_ACTIVITY; + const response = await fetch(url); + const data = await response.json(); + if (data.error) return; + const { summary, details } = data; + projectActivityDetails = details; + dashboard.innerHTML = ` +
+
์ •์ƒ (7์ผ ์ด๋‚ด)
${summary.active}
+
+
+
์ฃผ์˜ (14์ผ ์ด๋‚ด)
${summary.warning}
+
+
+
๋ฐฉ์น˜ (14์ผ ์ดˆ๊ณผ / ํด๋”์ž๋™์‚ญ์ œ)
${summary.stale}
+
+
+
๋ฐ์ดํ„ฐ ์—†์Œ (ํŒŒ์ผ 0๊ฐœ)
${summary.unknown}
+
`; + } catch (e) { console.error("๋ถ„์„ ๋กœ๋“œ ์‹คํŒจ:", e); } +} + +// --- ๋ Œ๋”๋ง ์—”์ง„ --- +function renderDashboard(data) { + const container = document.getElementById('projectAccordion'); + container.innerHTML = ''; + const grouped = groupData(data); + Object.keys(grouped).sort((a, b) => (CONTINENT_ORDER[a] || 99) - (CONTINENT_ORDER[b] || 99)).forEach(continent => { + const continentDiv = document.createElement('div'); + continentDiv.className = 'continent-group active'; + let html = `
${continent}โ–ผ
`; + Object.keys(grouped[continent]).sort().forEach(country => { + html += `
${country}โ–ผ
+
ํ”„๋กœ์ ํŠธ๋ช…
๋‹ด๋‹น๋ถ€์„œ
๋‹ด๋‹น์ž
ํŒŒ์ผ์ˆ˜
์ตœ๊ทผ๋กœ๊ทธ
+ ${grouped[continent][country].sort((a, b) => a[0].localeCompare(b[0])).map(p => createProjectHtml(p)).join('')}
`; + }); + html += `
`; + continentDiv.innerHTML = html; + container.appendChild(continentDiv); + }); +} + +function groupData(data) { + const res = {}; + data.forEach(item => { + const c1 = item[5] || "๊ธฐํƒ€", c2 = item[6] || "๋ฏธ๋ถ„๋ฅ˜"; + if (!res[c1]) res[c1] = {}; + if (!res[c1][c2]) res[c1][c2] = []; + res[c1][c2].push(item); + }); + return res; +} + +function createProjectHtml(p) { + const [name, dept, admin, logRaw, files] = p; + const recentLog = (!logRaw || logRaw === "X" || logRaw === "๋ฐ์ดํ„ฐ ์—†์Œ") ? "๊ธฐ๋ก ์—†์Œ" : logRaw; + const logTime = recentLog !== "๊ธฐ๋ก ์—†์Œ" ? recentLog.split(',')[0] : "๊ธฐ๋ก ์—†์Œ"; + + const isStaleLog = recentLog.replace(/\s/g, "").includes("ํด๋”์ž๋™์‚ญ์ œ"); + const isNoFiles = (files === 0 || files === null); + const statusClass = isNoFiles ? "status-error" : ""; + + let logStyleClass = ""; + if (isStaleLog) logStyleClass = "error-text"; + else if (recentLog === "๊ธฐ๋ก ์—†์Œ") logStyleClass = "warning-text"; + + const logBoldStyle = isStaleLog ? 'font-weight: 800;' : ''; + + return ` +
+
+
${name}
${dept}
${admin}
${files || 0}
${recentLog}
+
+
+
+
+

์ฐธ์—ฌ ์ธ์› ์ƒ์„ธ

+ + + +
์ด๋ฆ„์†Œ์†๊ถŒํ•œ
${admin}${dept}๊ด€๋ฆฌ์ž
+
+
+

์ตœ๊ทผ ํ™œ๋™

+ + + +
์œ ํ˜•๋‚ด์šฉ์ผ์‹œ
๋กœ๊ทธ๋™๊ธฐํ™” ์™„๋ฃŒ${logTime}
+
+
+
+
`; +} + +// --- ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ --- +function toggleGroup(h) { h.parentElement.classList.toggle('active'); } +function toggleAccordion(h) { + const item = h.parentElement; + item.parentElement.querySelectorAll('.accordion-item').forEach(el => { if (el !== item) el.classList.remove('active'); }); + item.classList.toggle('active'); +} + +function showActivityDetails(status) { + const names = { active: '์ •์ƒ', warning: '์ฃผ์˜', stale: '๋ฐฉ์น˜', unknown: '๋ฐ์ดํ„ฐ ์—†์Œ' }; + const filtered = (projectActivityDetails || []).filter(d => d.status === status); + document.getElementById('modalTitle').innerText = `${names[status]} ๋ชฉ๋ก (${filtered.length}๊ฐœ)`; + document.getElementById('modalTableBody').innerHTML = filtered.map(p => { + const o = rawData.find(r => r[0] === p.name); + return `${p.name}${o ? o[1] : "-"}${o ? o[2] : "-"}`; + }).join(''); + ModalManager.open('activityDetailModal'); +} + +function scrollToProject(name) { + ModalManager.close('activityDetailModal'); + const target = Array.from(document.querySelectorAll('.repo-title')).find(t => t.innerText.trim() === name.trim())?.closest('.accordion-header'); + if (target) { + let p = target.parentElement; + while (p && p !== document.body) { + if (p.classList.contains('continent-group') || p.classList.contains('country-group')) p.classList.add('active'); + p = p.parentElement; + } + target.parentElement.classList.add('active'); + const pos = target.getBoundingClientRect().top + window.pageYOffset - 260; + window.scrollTo({ top: pos, behavior: 'smooth' }); + target.style.backgroundColor = 'var(--primary-lv-1)'; + setTimeout(() => target.style.backgroundColor = '', 2000); + } +} + +// --- ํฌ๋กค๋ง ๋ฐ ์ธ์ฆ ์ œ์–ด --- +async function syncData() { + if (isCrawling) { + if (confirm("ํฌ๋กค๋ง์„ ์ค‘๋‹จํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?")) { + const res = await fetch(API.STOP_SYNC); + if ((await res.json()).success) document.getElementById('syncBtn').innerText = "์ค‘๋‹จ ์š”์ฒญ ์ค‘..."; + } + return; + } + document.getElementById('authId').value = ''; + document.getElementById('authPw').value = ''; + document.getElementById('authErrorMessage').style.display = 'none'; + ModalManager.open('authModal'); +} + +async function submitAuth() { + const id = document.getElementById('authId').value, pw = document.getElementById('authPw').value, err = document.getElementById('authErrorMessage'); + try { + const res = await fetch(API.AUTH_CRAWL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ user_id: id, password: pw }) }); + const data = await res.json(); + if (data.success) { ModalManager.close('authModal'); startCrawlProcess(); } + else { err.innerText = "ํฌ๋กค๋ง์„ ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."; err.style.display = 'block'; } + } catch { err.innerText = "์„œ๋ฒ„ ์—ฐ๊ฒฐ ์‹คํŒจ"; err.style.display = 'block'; } +} + +async function startCrawlProcess() { + isCrawling = true; + const btn = document.getElementById('syncBtn'), logC = document.getElementById('logConsole'), logB = document.getElementById('logBody'); + btn.classList.add('loading'); btn.style.backgroundColor = 'var(--error-color)'; btn.innerHTML = ` ํฌ๋กค๋ง ์ค‘๋‹จ`; + logC.style.display = 'block'; logB.innerHTML = '
>>> ์—”์ง„ ์ดˆ๊ธฐํ™” ์ค‘...
'; + try { + const res = await fetch(API.SYNC); + const reader = res.body.getReader(), decoder = new TextDecoder(); + while (true) { + const { done, value } = await reader.read(); if (done) break; + decoder.decode(value).split('\n').forEach(line => { + if (line.startsWith('data: ')) { + const p = JSON.parse(line.substring(6)); + if (p.type === 'log') { + const div = document.createElement('div'); div.innerText = `[${new Date().toLocaleTimeString()}] ${p.message}`; + logB.appendChild(div); logC.scrollTop = logC.scrollHeight; + } else if (p.type === 'done') { init(); alert(`๋™๊ธฐํ™” ์ข…๋ฃŒ`); logC.style.display = 'none'; } + } + }); + } + } catch { alert("์ŠคํŠธ๋ฆผ ๋Š๊น€"); } + finally { isCrawling = false; btn.classList.remove('loading'); btn.style.backgroundColor = ''; btn.innerHTML = ` ๋ฐ์ดํ„ฐ ๋™๊ธฐํ™” (ํฌ๋กค๋ง)`; } +} + +document.addEventListener('DOMContentLoaded', init); diff --git a/js/dashboard_test.js b/js/dashboard_test.js new file mode 100644 index 0000000..267e270 --- /dev/null +++ b/js/dashboard_test.js @@ -0,0 +1,245 @@ +/** + * Project Master Overseas Dashboard JS (TEST VERSION) + * ๊ธฐ๋Šฅ: ๋ฐ์ดํ„ฐ ๋กœ๋“œ, ํ™œ์„ฑ๋„ ๋ถ„์„, ์ธ์ฆ ๋ชจ๋‹ฌ ์ œ์–ด, ํฌ๋กค๋ง ๋™๊ธฐํ™” ๋ฐ ์ค‘๋‹จ + */ + +// --- ๊ธ€๋กœ๋ฒŒ ์ƒํƒœ ๊ด€๋ฆฌ --- +let rawData = []; +let projectActivityDetails = []; +let isCrawling = false; + +const CONTINENT_ORDER = { "์•„์‹œ์•„": 1, "์•„ํ”„๋ฆฌ์นด": 2, "์•„๋ฉ”๋ฆฌ์นด": 3, "์ง€์‚ฌ": 4 }; + +// --- ์ดˆ๊ธฐํ™” --- +async function init() { + console.log("Dashboard (TEST) Initializing..."); + if (!document.getElementById('projectAccordion')) return; + + await loadAvailableDates(); + await loadDataByDate(); +} + +// --- ๋ฐ์ดํ„ฐ ํ†ต์‹  ๋ฐ ๋กœ๋“œ --- +async function loadAvailableDates() { + try { + const response = await fetch(API.AVAILABLE_DATES); + const dates = await response.json(); // YYYY.MM.DD ํ˜•์‹ ๋ฆฌ์ŠคํŠธ + + if (dates?.length > 0) { + // ๋‚ ์งœ ํ˜•์‹ ๋ณ€ํ™˜ (YYYY.MM.DD -> YYYY-MM-DD) + const formattedDates = dates.map(d => d.replace(/\./g, '-')).sort(); + const minDate = formattedDates[0]; + const maxDate = new Date().toISOString().split('T')[0]; // ์˜ค๋Š˜ + const defaultDate = formattedDates[formattedDates.length - 1]; // ๊ฐ€์žฅ ์ตœ์‹  ์ˆ˜์ง‘์ผ + + const dateInputHtml = ` + + `; + const baseDateStrong = document.getElementById('baseDate'); + if (baseDateStrong) baseDateStrong.innerHTML = dateInputHtml; + } + } catch (e) { console.error("๋‚ ์งœ ๋กœ๋“œ ์‹คํŒจ:", e); } +} + +async function loadDataByDate(selectedDate = "") { + try { + await loadActivityAnalysis(selectedDate); + const url = selectedDate ? `${API.PROJECT_DATA}?date=${selectedDate}` : `${API.PROJECT_DATA}?t=${Date.now()}`; + const response = await fetch(url); + const data = await response.json(); + if (data.error) throw new Error(data.error); + rawData = data.projects || []; + renderDashboard(rawData); + } catch (e) { + console.error("๋ฐ์ดํ„ฐ ๋กœ๋“œ ์‹คํŒจ:", e); + alert("๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๋ฐ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."); + } +} + +async function loadActivityAnalysis(date = "") { + const dashboard = document.getElementById('activityDashboard'); + if (!dashboard) return; + try { + const url = date ? `${API.PROJECT_ACTIVITY}?date=${date}` : API.PROJECT_ACTIVITY; + const response = await fetch(url); + const data = await response.json(); + if (data.error) return; + const { summary, details } = data; + projectActivityDetails = details; + dashboard.innerHTML = ` +
+
์ •์ƒ (7์ผ ์ด๋‚ด) [TEST]
${summary.active}
+
+
+
์ฃผ์˜ (14์ผ ์ด๋‚ด) [TEST]
${summary.warning}
+
+
+
๋ฐฉ์น˜ (14์ผ ์ดˆ๊ณผ) [TEST]
${summary.stale}
+
+
+
๋ฐ์ดํ„ฐ ์—†์Œ (ํŒŒ์ผ 0๊ฐœ) [TEST]
${summary.unknown}
+
`; + } catch (e) { console.error("๋ถ„์„ ๋กœ๋“œ ์‹คํŒจ:", e); } +} + +// --- ๋ Œ๋”๋ง ์—”์ง„ --- +function renderDashboard(data) { + const container = document.getElementById('projectAccordion'); + container.innerHTML = ''; + const grouped = groupData(data); + Object.keys(grouped).sort((a, b) => (CONTINENT_ORDER[a] || 99) - (CONTINENT_ORDER[b] || 99)).forEach(continent => { + const continentDiv = document.createElement('div'); + continentDiv.className = 'continent-group active'; + let html = `
${continent}โ–ผ
`; + Object.keys(grouped[continent]).sort().forEach(country => { + html += `
${country}โ–ผ
+
ํ”„๋กœ์ ํŠธ๋ช…
๋‹ด๋‹น๋ถ€์„œ
๋‹ด๋‹น์ž
ํŒŒ์ผ์ˆ˜
์ตœ๊ทผ๋กœ๊ทธ
+ ${grouped[continent][country].sort((a, b) => a[0].localeCompare(b[0])).map(p => createProjectHtml(p)).join('')}
`; + }); + html += `
`; + continentDiv.innerHTML = html; + container.appendChild(continentDiv); + }); +} + +function groupData(data) { + const res = {}; + data.forEach(item => { + const c1 = item[5] || "๊ธฐํƒ€", c2 = item[6] || "๋ฏธ๋ถ„๋ฅ˜"; + if (!res[c1]) res[c1] = {}; + if (!res[c1][c2]) res[c1][c2] = []; + res[c1][c2].push(item); + }); + return res; +} + +function createProjectHtml(p) { + const [name, dept, admin, logRaw, files] = p; + const recentLog = (!logRaw || logRaw === "X" || logRaw === "๋ฐ์ดํ„ฐ ์—†์Œ") ? "๊ธฐ๋ก ์—†์Œ" : logRaw; + const logTime = recentLog !== "๊ธฐ๋ก ์—†์Œ" ? recentLog.split(',')[0] : "๊ธฐ๋ก ์—†์Œ"; + + const isStaleLog = recentLog.replace(/\s/g, "").includes("ํด๋”์ž๋™์‚ญ์ œ"); + const isNoFiles = (files === 0 || files === null); + const statusClass = isNoFiles ? "status-error" : ""; + + let logStyleClass = ""; + if (isStaleLog) logStyleClass = "error-text"; + else if (recentLog === "๊ธฐ๋ก ์—†์Œ") logStyleClass = "warning-text"; + + const logBoldStyle = isStaleLog ? 'font-weight: 800;' : ''; + + return ` +
+
+
${name}
${dept}
${admin}
${files || 0}
${recentLog}
+
+
+
+
+

์ฐธ์—ฌ ์ธ์› ์ƒ์„ธ (TEST)

+ + + +
์ด๋ฆ„์†Œ์†๊ถŒํ•œ
${admin}${dept}๊ด€๋ฆฌ์ž
+
+
+

์ตœ๊ทผ ํ™œ๋™ (TEST)

+ + + +
์œ ํ˜•๋‚ด์šฉ์ผ์‹œ
๋กœ๊ทธ๋™๊ธฐํ™” ์™„๋ฃŒ${logTime}
+
+
+
+
`; +} + +// --- ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ --- +function toggleGroup(h) { h.parentElement.classList.toggle('active'); } +function toggleAccordion(h) { + const item = h.parentElement; + item.parentElement.querySelectorAll('.accordion-item').forEach(el => { if (el !== item) el.classList.remove('active'); }); + item.classList.toggle('active'); +} + +function showActivityDetails(status) { + const names = { active: '์ •์ƒ', warning: '์ฃผ์˜', stale: '๋ฐฉ์น˜', unknown: '๋ฐ์ดํ„ฐ ์—†์Œ' }; + const filtered = (projectActivityDetails || []).filter(d => d.status === status); + document.getElementById('modalTitle').innerText = `${names[status]} ๋ชฉ๋ก (${filtered.length}๊ฐœ) [TEST]`; + document.getElementById('modalTableBody').innerHTML = filtered.map(p => { + const o = rawData.find(r => r[0] === p.name); + return `${p.name}${o ? o[1] : "-"}${o ? o[2] : "-"}`; + }).join(''); + ModalManager.open('activityDetailModal'); +} + +function scrollToProject(name) { + ModalManager.close('activityDetailModal'); + const target = Array.from(document.querySelectorAll('.repo-title')).find(t => t.innerText.trim() === name.trim())?.closest('.accordion-header'); + if (target) { + let p = target.parentElement; + while (p && p !== document.body) { + if (p.classList.contains('continent-group') || p.classList.contains('country-group')) p.classList.add('active'); + p = p.parentElement; + } + target.parentElement.classList.add('active'); + const pos = target.getBoundingClientRect().top + window.pageYOffset - 260; + window.scrollTo({ top: pos, behavior: 'smooth' }); + target.style.backgroundColor = 'var(--primary-lv-1)'; + setTimeout(() => target.style.backgroundColor = '', 2000); + } +} + +// --- ํฌ๋กค๋ง ๋ฐ ์ธ์ฆ ์ œ์–ด --- +async function syncData() { + if (isCrawling) { + if (confirm("ํฌ๋กค๋ง์„ ์ค‘๋‹จํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?")) { + const res = await fetch(API.STOP_SYNC); + if ((await res.json()).success) document.getElementById('syncBtn').innerText = "์ค‘๋‹จ ์š”์ฒญ ์ค‘..."; + } + return; + } + document.getElementById('authId').value = ''; + document.getElementById('authPw').value = ''; + document.getElementById('authErrorMessage').style.display = 'none'; + ModalManager.open('authModal'); +} + +async function submitAuth() { + const id = document.getElementById('authId').value, pw = document.getElementById('authPw').value, err = document.getElementById('authErrorMessage'); + try { + const res = await fetch(API.AUTH_CRAWL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ user_id: id, password: pw }) }); + const data = await res.json(); + if (data.success) { ModalManager.close('authModal'); startCrawlProcess(); } + else { err.innerText = "ํฌ๋กค๋ง์„ ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."; err.style.display = 'block'; } + } catch { err.innerText = "์„œ๋ฒ„ ์—ฐ๊ฒฐ ์‹คํŒจ"; err.style.display = 'block'; } +} + +async function startCrawlProcess() { + isCrawling = true; + const btn = document.getElementById('syncBtn'), logC = document.getElementById('logConsole'), logB = document.getElementById('logBody'); + btn.classList.add('loading'); btn.style.backgroundColor = 'var(--error-color)'; btn.innerHTML = ` ํฌ๋กค๋ง ์ค‘๋‹จ`; + logC.style.display = 'block'; logB.innerHTML = '
>>> ์—”์ง„ ์ดˆ๊ธฐํ™” ์ค‘...
'; + try { + const res = await fetch(API.SYNC); + const reader = res.body.getReader(), decoder = new TextDecoder(); + while (true) { + const { done, value } = await reader.read(); if (done) break; + decoder.decode(value).split('\n').forEach(line => { + if (line.startsWith('data: ')) { + const p = JSON.parse(line.substring(6)); + if (p.type === 'log') { + const div = document.createElement('div'); div.innerText = `[${new Date().toLocaleTimeString()}] ${p.message}`; + logB.appendChild(div); logC.scrollTop = logC.scrollHeight; + } else if (p.type === 'done') { init(); alert(`๋™๊ธฐํ™” ์ข…๋ฃŒ`); logC.style.display = 'none'; } + } + }); + } + } catch { alert("์ŠคํŠธ๋ฆผ ๋Š๊น€"); } + finally { isCrawling = false; btn.classList.remove('loading'); btn.style.backgroundColor = ''; btn.innerHTML = ` ๋ฐ์ดํ„ฐ ๋™๊ธฐํ™” (ํฌ๋กค๋ง)`; } +} + +document.addEventListener('DOMContentLoaded', init); \ No newline at end of file diff --git a/js/inquiries.js b/js/inquiries.js index 74197d0..9e1f394 100644 --- a/js/inquiries.js +++ b/js/inquiries.js @@ -1,314 +1,314 @@ -/** - * Project Master Overseas Inquiries JS - * ๊ธฐ๋Šฅ: ๋ฌธ์˜์‚ฌํ•ญ ๋กœ๋“œ, ํ•„ํ„ฐ๋ง, ๋‹ต๋ณ€ ๊ด€๋ฆฌ, ์•„์ฝ”๋””์–ธ ๋ฐ ์ด๋ฏธ์ง€ ๋ชจ๋‹ฌ - */ - -// --- ์ดˆ๊ธฐํ™” --- -let allInquiries = []; -let currentSort = { field: 'no', direction: 'desc' }; - -async function loadInquiries() { - initStickyHeader(); - - const pmType = document.getElementById('filterPmType').value; - const category = document.getElementById('filterCategory').value; - const keyword = document.getElementById('searchKeyword').value; - - const params = new URLSearchParams({ - pm_type: pmType, - category: category, - keyword: keyword - }); - - try { - const response = await fetch(`${API.INQUIRIES}?${params}`); - allInquiries = await response.json(); - - refreshInquiryBoard(); - } catch (e) { - console.error("๋ฐ์ดํ„ฐ ๋กœ๋”ฉ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:", e); - } -} - -function refreshInquiryBoard() { - const status = document.getElementById('filterStatus').value; - - // 1. ์ƒํƒœ ํ•„ํ„ฐ๋ง - let filteredData = status ? allInquiries.filter(item => item.status === status) : [...allInquiries]; - - // 2. ์ •๋ ฌ ์ ์šฉ - filteredData = sortData(filteredData); - - // 3. ํ†ต๊ณ„ ๋ฐ ๋ฆฌ์ŠคํŠธ ๋ Œ๋”๋ง - updateStats(allInquiries); - updateSortUI(); - renderInquiryList(filteredData); -} - -function handleSort(field) { - if (currentSort.field === field) { - currentSort.direction = currentSort.direction === 'asc' ? 'desc' : 'asc'; - } else { - currentSort.field = field; - currentSort.direction = 'asc'; - } - refreshInquiryBoard(); -} - -function sortData(data) { - const { field, direction } = currentSort; - const modifier = direction === 'asc' ? 1 : -1; - - return data.sort((a, b) => { - let valA = a[field]; - let valB = b[field]; - - // ์ˆซ์žํ˜• ๋ณ€ํ™˜ ์‹œ๋„ (No ํ•„๋“œ ๋“ฑ) - if (field === 'no' || !isNaN(valA)) { - valA = Number(valA); - valB = Number(valB); - } - - // null/undefined ์ฒ˜๋ฆฌ - if (valA === null || valA === undefined) valA = ""; - if (valB === null || valB === undefined) valB = ""; - - if (valA < valB) return -1 * modifier; - if (valA > valB) return 1 * modifier; - return 0; - }); -} - -function updateSortUI() { - // ๋ชจ๋“  ํ—ค๋” ํด๋ž˜์Šค ๋ฐ ์•„์ด์ฝ˜ ์ดˆ๊ธฐํ™” - document.querySelectorAll('.inquiry-table thead th.sortable').forEach(th => { - th.classList.remove('active-sort'); - const icon = th.querySelector('.sort-icon'); - if (icon) { - // ๋ ˆ์ด์•„์›ƒ ์‹œํ”„ํŠธ ๋ฐฉ์ง€๋ฅผ ์œ„ํ•ด ํˆฌ๋ช…ํ•œ ๊ธฐ๋ณธ ์•„์ด์ฝ˜(๋˜๋Š” ๊ณต๋ฐฑ) ์œ ์ง€ - icon.textContent = "โ–ฒ"; - icon.style.opacity = "0"; - } - }); - - // ํ˜„์žฌ ์ •๋ ฌ๋œ ํ—ค๋” ๊ฐ•์กฐ ๋ฐ ์•„์ด์ฝ˜ ํ‘œ์‹œ - const activeTh = document.querySelector(`.inquiry-table thead th[onclick*="'${currentSort.field}'"]`); - if (activeTh) { - activeTh.classList.add('active-sort'); - const icon = activeTh.querySelector('.sort-icon'); - if (icon) { - icon.textContent = currentSort.direction === 'asc' ? "โ–ฒ" : "โ–ผ"; - icon.style.opacity = "1"; - } - } -} - -function initStickyHeader() { - const header = document.getElementById('stickyHeader'); - const thead = document.querySelector('.inquiry-table thead'); - if (header && thead) { - const headerHeight = header.offsetHeight; - const totalOffset = 36 + headerHeight; - document.querySelectorAll('.inquiry-table thead th').forEach(th => { - th.style.top = totalOffset + 'px'; - }); - } -} - -function renderInquiryList(data) { - const tbody = document.getElementById('inquiryList'); - tbody.innerHTML = data.map(item => ` - - ${item.no} - - ${item.image_url ? `thumbnail` : '์—†์Œ'} - - ${item.pm_type} - ${item.browser || 'Chrome'} - ${item.category} - ${item.project_nm} - ${item.content} - ${item.author} - ${item.reg_date} - ${item.reply || '-'} - ${item.status} - - - -
- -
-
-
์ž‘์„ฑ์ž: ${item.author}
-
๋“ฑ๋ก์ผ: ${item.reg_date}
-
์‹œ์Šคํ…œ: ${item.pm_type}
-
ํ™˜๊ฒฝ: ${item.browser || 'Chrome'} / ${item.device || 'PC'}
-
- -
-

[์งˆ๋ฌธ ๋‚ด์šฉ]

-
${item.content}
-
- - ${item.image_url ? ` -
-
-

- ๐Ÿ–ผ๏ธ [์ฒจ๋ถ€ ์ด๋ฏธ์ง€] - (ํด๋ฆญ ์‹œ ํฌ๊ฒŒ ๋ณด๊ธฐ) -

- โ–ผ -
- -
- ` : ''} - -
-

[์กฐ์น˜ ๋ฐ ๋‹ต๋ณ€]

-
- -
-
-
- - -
-
- - -
-
-
- - - - -
-
- ${item.handled_date ? `
์ตœ์ข… ์ˆ˜์ •์ผ: ${item.handled_date}
` : ''} -
-
-
-
- - - `).join(''); -} - -function enableEdit(id) { - const form = document.getElementById(`reply-form-${id}`); - form.classList.replace('readonly', 'editable'); - const elements = [`reply-text-${id}`, `reply-status-${id}`, `reply-handler-${id}`]; - elements.forEach(elId => document.getElementById(elId).disabled = false); - document.getElementById(`reply-text-${id}`).focus(); -} - -async function cancelEdit(id) { - try { - const response = await fetch(`${API.INQUIRIES}/${id}`); - const item = await response.json(); - const txt = document.getElementById(`reply-text-${id}`); - const status = document.getElementById(`reply-status-${id}`); - const handler = document.getElementById(`reply-handler-${id}`); - txt.value = item.reply || ''; - status.value = item.status; - handler.value = item.handler || ''; - [txt, status, handler].forEach(el => el.disabled = true); - document.getElementById(`reply-form-${id}`).classList.replace('editable', 'readonly'); - } catch { loadInquiries(); } -} - -async function saveReply(id) { - const reply = document.getElementById(`reply-text-${id}`).value; - const status = document.getElementById(`reply-status-${id}`).value; - const handler = document.getElementById(`reply-handler-${id}`).value; - if (!reply.trim() || !handler.trim()) return alert("๋‚ด์šฉ๊ณผ ์ฒ˜๋ฆฌ์ž๋ฅผ ๋ชจ๋‘ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”."); - try { - const response = await fetch(`${API.INQUIRIES}/${id}/reply`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ reply, status, handler }) - }); - if ((await response.json()).success) { alert("์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); loadInquiries(); } - } catch { alert("์ €์žฅ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."); } -} - -async function deleteReply(id) { - if (!confirm("๋‹ต๋ณ€์„ ์‚ญ์ œํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?")) return; - try { - const response = await fetch(`${API.INQUIRIES}/${id}/reply`, { method: 'DELETE' }); - if ((await response.json()).success) { alert("์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); loadInquiries(); } - } catch { alert("์‚ญ์ œ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."); } -} - -function toggleAccordion(id) { - const detailRow = document.getElementById(`detail-${id}`); - if (!detailRow) return; - const inquiryRow = detailRow.previousElementSibling; - const isActive = detailRow.classList.contains('active'); - - document.querySelectorAll('.detail-row.active').forEach(row => { - if (row.id !== `detail-${id}`) { - row.classList.remove('active'); - if (row.previousElementSibling) row.previousElementSibling.classList.remove('active-row'); - } - }); - - if (isActive) { - detailRow.classList.remove('active'); - inquiryRow.classList.remove('active-row'); - } else { - detailRow.classList.add('active'); - inquiryRow.classList.add('active-row'); - scrollToRow(inquiryRow); - } -} - -function scrollToRow(row) { - setTimeout(() => { - const headerHeight = document.getElementById('stickyHeader').offsetHeight; - const totalOffset = 36 + headerHeight + 40; - const offsetPosition = (row.getBoundingClientRect().top + window.pageYOffset) - totalOffset; - window.scrollTo({ top: offsetPosition, behavior: 'smooth' }); - }, 100); -} - -function updateStats(data) { - const counts = { - Total: data.length, - Complete: data.filter(i => i.status === '์™„๋ฃŒ').length, - Working: data.filter(i => i.status === '์ž‘์—… ์ค‘').length, - Checking: data.filter(i => i.status === 'ํ™•์ธ ์ค‘').length, - Pending: data.filter(i => i.status === '๊ฐœ๋ฐœ์˜ˆ์ •').length, - Unconfirmed: data.filter(i => i.status === '๋ฏธํ™•์ธ').length - }; - Object.keys(counts).forEach(k => { - const el = document.getElementById(`count${k}`); - if (el) el.textContent = counts[k].toLocaleString(); - }); -} - -function openImageModal(src) { - document.getElementById('modalImage').src = src; - ModalManager.open('imageModal'); -} - -function toggleImageSection(id) { - const section = document.getElementById(`img-section-${id}`); - const content = document.getElementById(`img-content-${id}`); - const icon = section.querySelector('.toggle-icon'); - const isCollapsed = content.classList.toggle('collapsed'); - section.classList.toggle('active', !isCollapsed); - icon.textContent = isCollapsed ? 'โ–ผ' : 'โ–ฒ'; -} - -document.addEventListener('DOMContentLoaded', loadInquiries); -window.addEventListener('resize', initStickyHeader); +/** + * Project Master Overseas Inquiries JS + * ๊ธฐ๋Šฅ: ๋ฌธ์˜์‚ฌํ•ญ ๋กœ๋“œ, ํ•„ํ„ฐ๋ง, ๋‹ต๋ณ€ ๊ด€๋ฆฌ, ์•„์ฝ”๋””์–ธ ๋ฐ ์ด๋ฏธ์ง€ ๋ชจ๋‹ฌ + */ + +// --- ์ดˆ๊ธฐํ™” --- +let allInquiries = []; +let currentSort = { field: 'no', direction: 'desc' }; + +async function loadInquiries() { + initStickyHeader(); + + const pmType = document.getElementById('filterPmType').value; + const category = document.getElementById('filterCategory').value; + const keyword = document.getElementById('searchKeyword').value; + + const params = new URLSearchParams({ + pm_type: pmType, + category: category, + keyword: keyword + }); + + try { + const response = await fetch(`${API.INQUIRIES}?${params}`); + allInquiries = await response.json(); + + refreshInquiryBoard(); + } catch (e) { + console.error("๋ฐ์ดํ„ฐ ๋กœ๋”ฉ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:", e); + } +} + +function refreshInquiryBoard() { + const status = document.getElementById('filterStatus').value; + + // 1. ์ƒํƒœ ํ•„ํ„ฐ๋ง + let filteredData = status ? allInquiries.filter(item => item.status === status) : [...allInquiries]; + + // 2. ์ •๋ ฌ ์ ์šฉ + filteredData = sortData(filteredData); + + // 3. ํ†ต๊ณ„ ๋ฐ ๋ฆฌ์ŠคํŠธ ๋ Œ๋”๋ง + updateStats(allInquiries); + updateSortUI(); + renderInquiryList(filteredData); +} + +function handleSort(field) { + if (currentSort.field === field) { + currentSort.direction = currentSort.direction === 'asc' ? 'desc' : 'asc'; + } else { + currentSort.field = field; + currentSort.direction = 'asc'; + } + refreshInquiryBoard(); +} + +function sortData(data) { + const { field, direction } = currentSort; + const modifier = direction === 'asc' ? 1 : -1; + + return data.sort((a, b) => { + let valA = a[field]; + let valB = b[field]; + + // ์ˆซ์žํ˜• ๋ณ€ํ™˜ ์‹œ๋„ (No ํ•„๋“œ ๋“ฑ) + if (field === 'no' || !isNaN(valA)) { + valA = Number(valA); + valB = Number(valB); + } + + // null/undefined ์ฒ˜๋ฆฌ + if (valA === null || valA === undefined) valA = ""; + if (valB === null || valB === undefined) valB = ""; + + if (valA < valB) return -1 * modifier; + if (valA > valB) return 1 * modifier; + return 0; + }); +} + +function updateSortUI() { + // ๋ชจ๋“  ํ—ค๋” ํด๋ž˜์Šค ๋ฐ ์•„์ด์ฝ˜ ์ดˆ๊ธฐํ™” + document.querySelectorAll('.inquiry-table thead th.sortable').forEach(th => { + th.classList.remove('active-sort'); + const icon = th.querySelector('.sort-icon'); + if (icon) { + // ๋ ˆ์ด์•„์›ƒ ์‹œํ”„ํŠธ ๋ฐฉ์ง€๋ฅผ ์œ„ํ•ด ํˆฌ๋ช…ํ•œ ๊ธฐ๋ณธ ์•„์ด์ฝ˜(๋˜๋Š” ๊ณต๋ฐฑ) ์œ ์ง€ + icon.textContent = "โ–ฒ"; + icon.style.opacity = "0"; + } + }); + + // ํ˜„์žฌ ์ •๋ ฌ๋œ ํ—ค๋” ๊ฐ•์กฐ ๋ฐ ์•„์ด์ฝ˜ ํ‘œ์‹œ + const activeTh = document.querySelector(`.inquiry-table thead th[onclick*="'${currentSort.field}'"]`); + if (activeTh) { + activeTh.classList.add('active-sort'); + const icon = activeTh.querySelector('.sort-icon'); + if (icon) { + icon.textContent = currentSort.direction === 'asc' ? "โ–ฒ" : "โ–ผ"; + icon.style.opacity = "1"; + } + } +} + +function initStickyHeader() { + const header = document.getElementById('stickyHeader'); + const thead = document.querySelector('.inquiry-table thead'); + if (header && thead) { + const headerHeight = header.offsetHeight; + const totalOffset = 36 + headerHeight; + document.querySelectorAll('.inquiry-table thead th').forEach(th => { + th.style.top = totalOffset + 'px'; + }); + } +} + +function renderInquiryList(data) { + const tbody = document.getElementById('inquiryList'); + tbody.innerHTML = data.map(item => ` + + ${item.no} + + ${item.image_url ? `thumbnail` : '์—†์Œ'} + + ${item.pm_type} + ${item.browser || 'Chrome'} + ${item.category} + ${item.project_nm} + ${item.content} + ${item.author} + ${item.reg_date} + ${item.reply || '-'} + ${item.status} + + + +
+ +
+
+
์ž‘์„ฑ์ž: ${item.author}
+
๋“ฑ๋ก์ผ: ${item.reg_date}
+
์‹œ์Šคํ…œ: ${item.pm_type}
+
ํ™˜๊ฒฝ: ${item.browser || 'Chrome'} / ${item.device || 'PC'}
+
+ +
+

[์งˆ๋ฌธ ๋‚ด์šฉ]

+
${item.content}
+
+ + ${item.image_url ? ` +
+
+

+ ๐Ÿ–ผ๏ธ [์ฒจ๋ถ€ ์ด๋ฏธ์ง€] + (ํด๋ฆญ ์‹œ ํฌ๊ฒŒ ๋ณด๊ธฐ) +

+ โ–ผ +
+ +
+ ` : ''} + +
+

[์กฐ์น˜ ๋ฐ ๋‹ต๋ณ€]

+
+ +
+
+
+ + +
+
+ + +
+
+
+ + + + +
+
+ ${item.handled_date ? `
์ตœ์ข… ์ˆ˜์ •์ผ: ${item.handled_date}
` : ''} +
+
+
+
+ + + `).join(''); +} + +function enableEdit(id) { + const form = document.getElementById(`reply-form-${id}`); + form.classList.replace('readonly', 'editable'); + const elements = [`reply-text-${id}`, `reply-status-${id}`, `reply-handler-${id}`]; + elements.forEach(elId => document.getElementById(elId).disabled = false); + document.getElementById(`reply-text-${id}`).focus(); +} + +async function cancelEdit(id) { + try { + const response = await fetch(`${API.INQUIRIES}/${id}`); + const item = await response.json(); + const txt = document.getElementById(`reply-text-${id}`); + const status = document.getElementById(`reply-status-${id}`); + const handler = document.getElementById(`reply-handler-${id}`); + txt.value = item.reply || ''; + status.value = item.status; + handler.value = item.handler || ''; + [txt, status, handler].forEach(el => el.disabled = true); + document.getElementById(`reply-form-${id}`).classList.replace('editable', 'readonly'); + } catch { loadInquiries(); } +} + +async function saveReply(id) { + const reply = document.getElementById(`reply-text-${id}`).value; + const status = document.getElementById(`reply-status-${id}`).value; + const handler = document.getElementById(`reply-handler-${id}`).value; + if (!reply.trim() || !handler.trim()) return alert("๋‚ด์šฉ๊ณผ ์ฒ˜๋ฆฌ์ž๋ฅผ ๋ชจ๋‘ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”."); + try { + const response = await fetch(`${API.INQUIRIES}/${id}/reply`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ reply, status, handler }) + }); + if ((await response.json()).success) { alert("์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); loadInquiries(); } + } catch { alert("์ €์žฅ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."); } +} + +async function deleteReply(id) { + if (!confirm("๋‹ต๋ณ€์„ ์‚ญ์ œํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?")) return; + try { + const response = await fetch(`${API.INQUIRIES}/${id}/reply`, { method: 'DELETE' }); + if ((await response.json()).success) { alert("์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); loadInquiries(); } + } catch { alert("์‚ญ์ œ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."); } +} + +function toggleAccordion(id) { + const detailRow = document.getElementById(`detail-${id}`); + if (!detailRow) return; + const inquiryRow = detailRow.previousElementSibling; + const isActive = detailRow.classList.contains('active'); + + document.querySelectorAll('.detail-row.active').forEach(row => { + if (row.id !== `detail-${id}`) { + row.classList.remove('active'); + if (row.previousElementSibling) row.previousElementSibling.classList.remove('active-row'); + } + }); + + if (isActive) { + detailRow.classList.remove('active'); + inquiryRow.classList.remove('active-row'); + } else { + detailRow.classList.add('active'); + inquiryRow.classList.add('active-row'); + scrollToRow(inquiryRow); + } +} + +function scrollToRow(row) { + setTimeout(() => { + const headerHeight = document.getElementById('stickyHeader').offsetHeight; + const totalOffset = 36 + headerHeight + 40; + const offsetPosition = (row.getBoundingClientRect().top + window.pageYOffset) - totalOffset; + window.scrollTo({ top: offsetPosition, behavior: 'smooth' }); + }, 100); +} + +function updateStats(data) { + const counts = { + Total: data.length, + Complete: data.filter(i => i.status === '์™„๋ฃŒ').length, + Working: data.filter(i => i.status === '์ž‘์—… ์ค‘').length, + Checking: data.filter(i => i.status === 'ํ™•์ธ ์ค‘').length, + Pending: data.filter(i => i.status === '๊ฐœ๋ฐœ์˜ˆ์ •').length, + Unconfirmed: data.filter(i => i.status === '๋ฏธํ™•์ธ').length + }; + Object.keys(counts).forEach(k => { + const el = document.getElementById(`count${k}`); + if (el) el.textContent = counts[k].toLocaleString(); + }); +} + +function openImageModal(src) { + document.getElementById('modalImage').src = src; + ModalManager.open('imageModal'); +} + +function toggleImageSection(id) { + const section = document.getElementById(`img-section-${id}`); + const content = document.getElementById(`img-content-${id}`); + const icon = section.querySelector('.toggle-icon'); + const isCollapsed = content.classList.toggle('collapsed'); + section.classList.toggle('active', !isCollapsed); + icon.textContent = isCollapsed ? 'โ–ผ' : 'โ–ฒ'; +} + +document.addEventListener('DOMContentLoaded', loadInquiries); +window.addEventListener('resize', initStickyHeader); diff --git a/js/mail.js b/js/mail.js index f3d1898..a4d512b 100644 --- a/js/mail.js +++ b/js/mail.js @@ -1,312 +1,312 @@ -/** - * Project Master Overseas Mail Management JS - * ๊ธฐ๋Šฅ: ์ฒจ๋ถ€ํŒŒ์ผ ๋กœ๋“œ, AI ๋ถ„์„, ๋ฉ”์ผ ๋ชฉ๋ก ๋ Œ๋”๋ง, ๋ฏธ๋ฆฌ๋ณด๊ธฐ, ์ฃผ์†Œ๋ก ๊ด€๋ฆฌ - */ - -let currentFiles = []; -let editingIndex = -1; - -const HIERARCHY = { - "ํ–‰์ •": { "๊ณ„์•ฝ": ["๊ณ„์•ฝ๊ด€๋ฆฌ", "๊ธฐ์„ฑ๊ด€๋ฆฌ", "์—…๋ฌด์ง€์‹œ์„œ", "์ธ์›๊ด€๋ฆฌ"], "์—…๋ฌด๊ด€๋ฆฌ": ["์—…๋ฌด์ผ์ง€(2025)", "์—…๋ฌด์ผ์ง€(2025๋…„ ์ด์ „)", "๋ฐœ์ฃผ์ฒ˜ ์ •๊ธฐ๋ณด๊ณ ", "๋ณธ์‚ฌ์—…๋ฌด๋ณด๊ณ ", "๊ณต์‚ฌ๊ฐ๋…์ผ์ง€", "์–‘์‹์„œ๋ฅ˜"] }, - "์„ค๊ณ„์„ฑ๊ณผํ’ˆ": { "์‹œ๋ฐฉ์„œ": ["๊ณต์‚ฌ์‹œ๋ฐฉ์„œ"], "์„ค๊ณ„๋„๋ฉด": ["๊ณตํ†ต", "ํ† ๊ณต", "๋น„ํƒˆ๋ฉด์•ˆ์ „๊ณต", "๋ฐฐ์ˆ˜๊ณต", "๊ต๋Ÿ‰๊ณต", "ํฌ์žฅ๊ณต"], "์ˆ˜๋Ÿ‰์‚ฐ์ถœ์„œ": ["ํ† ๊ณต", "๋ฐฐ์ˆ˜๊ณต"], "๋‚ด์—ญ์„œ": ["๋‹จ๊ฐ€์‚ฐ์ถœ์„œ"], "๋ณด๊ณ ์„œ": ["์‹ค์‹œ์„ค๊ณ„๋ณด๊ณ ์„œ", "์ง€๋ฐ˜์กฐ์‚ฌ๋ณด๊ณ ์„œ"], "์ธก๋Ÿ‰๊ณ„์‚ฐ๋ถ€": ["์ธก๋Ÿ‰๊ณ„์‚ฐ๋ถ€"], "์„ค๊ณ„๋‹จ๊ณ„ ์ˆ˜ํ–‰ํ˜‘์˜": ["ํšŒ์˜ยทํ˜‘์˜"] }, - "์‹œ๊ณต๊ฒ€์ธก": { "ํ† ๊ณต": ["๊ฒ€์ธก (๊นจ๊ธฐ)", "๊ฒ€์ธก (๋…ธ์ฒด)"], "๋ฐฐ์ˆ˜๊ณต": ["๊ฒ€์ธก (Vํ˜•์ธก๊ตฌ)", "๊ฒ€์ธก (์ข…๋ฐฐ์ˆ˜๊ด€)"], "๊ตฌ์กฐ๋ฌผ๊ณต": ["๊ฒ€์ธก (ํ‰๋ชฉ๊ต)"], "ํฌ์žฅ๊ณต": ["๊ฒ€์ธก (๊ธฐ์ธต)"] }, - "์„ค๊ณ„๋ณ€๊ฒฝ": { "์‹ค์ •๋ณด๊ณ ": ["ํ† ๊ณต", "๋ฐฐ์ˆ˜๊ณต", "์•ˆ์ „๊ด€๋ฆฌ"], "๊ธฐ์ˆ ์ง€์› ๊ฒ€ํ† ": ["ํ† ๊ณต", "๊ตฌ์กฐ๋ฌผ&๋ถ€๋Œ€๊ณต"] } -}; - -const MAIL_SAMPLES = { - inbound: [ - { person: "๋ผ์˜ค์Šค ๋†๋ฆผ๋ถ€", email: "pany.s@lao.gov.la", time: "2026-03-05", title: "ITTC ๊ต์œก์„ผํ„ฐ ์ฐฉ๊ณต์‹ ์ผ์ • ํ˜‘์˜", summary: "์ฐฉ๊ณต์‹ ๊ด€๋ จํ•˜์—ฌ ์ •๋ถ€ ์ธก ์ธ์‚ฌ์˜ ์ผ์ •์„ ๋ฐ˜์˜ํ•œ ์ตœ์ข… ๊ณต๋ฌธ์„ ์†ก๋ถ€ํ•ฉ๋‹ˆ๋‹ค.", active: true }, - { person: "ํ˜„๋Œ€๊ฑด์„ค (๊น€์ฒ ์ˆ˜ ์†Œ์žฅ)", email: "cs.kim@hdec.co.kr", time: "2026-03-04", title: "[๊ธด๊ธ‰] ์–ด์ฒœ-๊ณต์ฃผ(4์ฐจ) ํ•˜๋„๊ธ‰ ๋ณ€๊ฒฝ๊ณ„์•ฝ ํ†ต๋ณด", summary: "์ฒ ๊ฑฐ๊ณต์‚ฌ ๋ฌผ๋Ÿ‰ ๋ณ€๋™์— ๋”ฐ๋ฅธ ๊ณ„์•ฝ ๊ธˆ์•ก ์กฐ์ • ๊ฑด์ž…๋‹ˆ๋‹ค. ๊ฒ€ํ†  ํ›„ ์Šน์ธ ๋ถ€ํƒ๋“œ๋ฆฝ๋‹ˆ๋‹ค.", active: false } - ], - outbound: [ - { person: "๊ณต์‚ฌ๊ด€๋ฆฌ๋ถ€ (๋ณธ์‚ฌ)", email: "hq_pm@projectmaster.com", time: "2026-03-04", title: "์–ด์ฒœ-๊ณต์ฃผ 2์›” ์›”๊ฐ„ ๊ณต์ •๋ณด๊ณ ์„œ ์ œ์ถœ", summary: "2์›” ํ•œ ๋‹ฌ๊ฐ„์˜ ์ฃผ์š” ๊ณต์ • ๋ฐ ์˜ˆ์‚ฐ ์ง‘ํ–‰ ํ˜„ํ™ฉ ๋ณด๊ณ ์„œ์ž…๋‹ˆ๋‹ค.", active: false } - ], - drafts: [], deleted: [] -}; - -let currentMailTab = 'inbound'; -let filteredMails = []; - -// --- ์ฒจ๋ถ€ํŒŒ์ผ ๋ฐ์ดํ„ฐ ๋กœ๋“œ ๋ฐ ๋ Œ๋”๋ง --- -async function loadAttachments() { - try { - const res = await fetch(API.ATTACHMENTS); - currentFiles = await res.json(); - renderFiles(); - } catch (e) { console.error("Failed to load attachments:", e); } -} - -function renderFiles() { - const isAiActive = document.getElementById('aiToggle').checked; - const container = document.getElementById('attachmentList'); - if (!container) return; - container.innerHTML = ''; - - currentFiles.forEach((file, index) => { - const item = document.createElement('div'); - item.className = 'attachment-item-wrap'; - item.style.marginBottom = "8px"; - - let pathText = "๊ฒฝ๋กœ๋ฅผ ์„ ํƒํ•ด์ฃผ์„ธ์š”"; - let modeClass = "manual-mode"; - - if (file.analysis) { - const prefix = file.analysis.isManual ? "์„ ํƒ ๊ฒฝ๋กœ: " : "์ถ”์ฒœ: "; - pathText = `${prefix}${file.analysis.suggested_path}`; - modeClass = file.analysis.isManual ? "manual-mode" : "smart-mode"; - } else if (isAiActive) { - pathText = "AI ๋ถ„์„ ๋Œ€๊ธฐ ์ค‘..."; - modeClass = "smart-mode"; - } - - item.innerHTML = ` -
- ๐Ÿ“„ -
-
${file.name}
-
${file.size}
-
-
- ${pathText} - ${isAiActive ? `` : ''} - -
-
-
- `; - container.appendChild(item); - }); -} - -// --- AI ๋ถ„์„ ์‹คํ–‰ --- -async function startAnalysis(index, event) { - if (event) event.stopPropagation(); - const file = currentFiles[index]; - if (!file) return; - - // UI ์ƒํƒœ ์—…๋ฐ์ดํŠธ: ๋ถ„์„ ์ค‘ ํ‘œ์‹œ - const logArea = document.getElementById(`log-area-${index}`); - const logContent = document.getElementById(`log-content-${index}`); - if (logArea) logArea.classList.add('active'); - if (logContent) { - logContent.innerHTML = `
- - AI๊ฐ€ ๋ฌธ์„œ๋ฅผ ์ •๋ฐ€ ๋ถ„์„ ์ค‘์ž…๋‹ˆ๋‹ค... -
`; - } - - try { - const res = await fetch(`${API.ANALYZE_FILE}?filename=${encodeURIComponent(file.name)}`); - const result = await res.json(); - - if (result.error) { - if (logContent) logContent.innerHTML = `
์˜ค๋ฅ˜: ${result.error}
`; - return; - } - - // ๋ถ„์„ ๊ฒฐ๊ณผ ์ €์žฅ ๋ฐ UI ๊ฐฑ์‹  - currentFiles[index].analysis = result.final_result; - currentFiles[index].analysis.isManual = false; - - if (logContent) { - logContent.innerHTML = ` -
-
โœจ AI ๋ถ„์„ ์™„๋ฃŒ
-
${result.final_result.reason}
-
- `; - } - - renderFiles(); - } catch (e) { - console.error("AI Analysis failed:", e); - if (logContent) logContent.innerHTML = `
๋ถ„์„ ์‹คํŒจ: ๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.
`; - } -} - -// --- ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์ œ์–ด --- -function showPreview(index, event) { - if (event && (event.target.closest('.btn-group') || event.target.closest('.path-display'))) return; - const file = currentFiles[index]; - if (!file) return; - - const previewArea = document.getElementById('mailPreviewArea'); - const toggleIcon = document.getElementById('previewToggleIcon'); - const fullViewBtn = document.getElementById('fullViewBtn'); - const previewContainer = document.getElementById('previewContainer'); - - if (previewArea) { - previewArea.classList.add('active'); - if (toggleIcon) toggleIcon.innerText = 'โ–ถ'; - } - - const fileUrl = Utils.getSafeFileUrl(file.name); - if (fullViewBtn) { - fullViewBtn.style.display = 'block'; - fullViewBtn.onclick = () => window.open(fileUrl, 'PMFullView', 'width=1000,height=800'); - } - - if (file.name.toLowerCase().endsWith('.pdf')) { - previewContainer.innerHTML = ``; - } else { - previewContainer.innerHTML = `
${file.name}
`; - } - - document.querySelectorAll('.attachment-item').forEach(item => item.classList.remove('active')); - if (event?.currentTarget) event.currentTarget.classList.add('active'); -} - -function togglePreviewAuto() { - const area = document.getElementById('mailPreviewArea'); - const icon = document.getElementById('previewToggleIcon'); - if (area) { - const isActive = area.classList.toggle('active'); - if (icon) icon.innerText = isActive ? 'โ–ถ' : 'โ—€'; - } -} - -// --- ๋ฉ”์ผ ๋ฆฌ์ŠคํŠธ ์ œ์–ด --- -function renderMailList(tabType, mailsToShow = null) { - currentMailTab = tabType; - const container = document.querySelector('.mail-items-container'); - if (!container) return; - - const mails = mailsToShow || MAIL_SAMPLES[tabType] || []; - filteredMails = mails; - updateBulkActionBar(); - - container.innerHTML = mails.map((mail, idx) => ` -
- -
-
- ${mail.person} -
${mail.time}
-
-
${mail.title}
-
${mail.summary}
-
-
- `).join(''); - - const activeIdx = mails.findIndex(m => m.active); - if (activeIdx !== -1) updateMailContent(mails[activeIdx]); -} - -function selectMailItem(el, index) { - document.querySelectorAll('.mail-item').forEach(item => item.classList.remove('active')); - el.classList.add('active'); - const mail = filteredMails[index]; - if (mail) updateMailContent(mail); -} - -function updateMailContent(mail) { - const title = document.querySelector('.mail-content-header h2'); - if (title) title.innerText = mail.title; - document.querySelector('.mail-body').innerHTML = mail.summary.replace(/\n/g, '
') + "

๋ณธ ๋‚ด์šฉ์€ ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ์ž…๋‹ˆ๋‹ค."; -} - -function switchMailTab(el, tabType) { - document.querySelectorAll('.mail-tab').forEach(tab => tab.classList.remove('active')); - el.classList.add('active'); - renderMailList(tabType); -} - -// --- ๊ฒฝ๋กœ ์„ ํƒ ๋ชจ๋‹ฌ --- -function openPathModal(index, event) { - if (event) event.stopPropagation(); - editingIndex = index; - const tabSelect = document.getElementById('tabSelect'); - if (tabSelect) { - tabSelect.innerHTML = Object.keys(HIERARCHY).map(tab => ``).join(''); - updateCategories(); - ModalManager.open('pathModal'); - } -} - -function updateCategories() { - const tab = document.getElementById('tabSelect').value; - document.getElementById('categorySelect').innerHTML = Object.keys(HIERARCHY[tab]).map(cat => ``).join(''); - updateSubs(); -} - -function updateSubs() { - const tab = document.getElementById('tabSelect').value; - const cat = document.getElementById('categorySelect').value; - document.getElementById('subSelect').innerHTML = HIERARCHY[tab][cat].map(sub => ``).join(''); -} - -function applyPathSelection() { - const path = `${document.getElementById('tabSelect').value} > ${document.getElementById('categorySelect').value} > ${document.getElementById('subSelect').value}`; - if (!currentFiles[editingIndex].analysis) currentFiles[editingIndex].analysis = {}; - currentFiles[editingIndex].analysis.suggested_path = path; - currentFiles[editingIndex].analysis.isManual = true; - renderFiles(); - ModalManager.close('pathModal'); -} - -// --- ์ฃผ์†Œ๋ก ๊ด€๋ฆฌ --- -let addressBookData = [ - { name: "์ดํƒœํ›ˆ", dept: "PM Overseas / ์„ ์ž„์—ฐ๊ตฌ์›", email: "th.lee@projectmaster.com", phone: "010-1234-5678" }, - { name: "Pany S.", dept: "๋ผ์˜ค์Šค ๋†๋ฆผ๋ถ€ / ๊ตญ์žฅ", email: "pany.s@lao.gov.la", phone: "+856-20-1234-5678" } -]; -let contactEditingIndex = -1; - -function openAddressBook() { renderAddressBook(); ModalManager.open('addressBookModal'); } -function closeAddressBook() { ModalManager.close('addressBookModal'); } - -function renderAddressBook() { - const body = document.getElementById('addressBookBody'); - if (!body) return; - body.innerHTML = addressBookData.map((c, idx) => ` - - ${c.name}${c.dept}${c.email}${c.phone} - - - - - `).join(''); -} - -function toggleAddContactForm() { - const form = document.getElementById('addContactForm'); - if (form.style.display === 'none') form.style.display = 'block'; - else { form.style.display = 'none'; contactEditingIndex = -1; } -} - -function editContact(index) { - const c = addressBookData[index]; - contactEditingIndex = index; - document.getElementById('newContactName').value = c.name; - document.getElementById('newContactDept').value = c.dept; - document.getElementById('newContactEmail').value = c.email; - document.getElementById('newContactPhone').value = c.phone; - document.getElementById('addContactForm').style.display = 'block'; -} - -function deleteContact(index) { - if (confirm(`'${addressBookData[index].name}'๋‹˜์„ ์‚ญ์ œํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?`)) { addressBookData.splice(index, 1); renderAddressBook(); } -} - -function addContact() { - const name = document.getElementById('newContactName').value; - if (!name) return alert("์ด๋ฆ„์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”."); - const data = { name, dept: document.getElementById('newContactDept').value, email: document.getElementById('newContactEmail').value, phone: document.getElementById('newContactPhone').value }; - if (contactEditingIndex > -1) addressBookData[contactEditingIndex] = data; - else addressBookData.push(data); - renderAddressBook(); toggleAddContactForm(); -} - -// --- ๊ณตํ†ต ์•ก์…˜ --- -function updateBulkActionBar() { - const count = document.querySelectorAll('.mail-item-checkbox:checked').length; - const bar = document.getElementById('mailBulkActions'); - if (count > 0) { bar.classList.add('active'); document.getElementById('selectedCount').innerText = `${count}๊ฐœ ์„ ํƒ๋จ`; } - else bar.classList.remove('active'); -} - -// ์ดˆ๊ธฐํ™” -document.addEventListener('DOMContentLoaded', () => { - loadAttachments(); - renderMailList('inbound'); -}); +/** + * Project Master Overseas Mail Management JS + * ๊ธฐ๋Šฅ: ์ฒจ๋ถ€ํŒŒ์ผ ๋กœ๋“œ, AI ๋ถ„์„, ๋ฉ”์ผ ๋ชฉ๋ก ๋ Œ๋”๋ง, ๋ฏธ๋ฆฌ๋ณด๊ธฐ, ์ฃผ์†Œ๋ก ๊ด€๋ฆฌ + */ + +let currentFiles = []; +let editingIndex = -1; + +const HIERARCHY = { + "ํ–‰์ •": { "๊ณ„์•ฝ": ["๊ณ„์•ฝ๊ด€๋ฆฌ", "๊ธฐ์„ฑ๊ด€๋ฆฌ", "์—…๋ฌด์ง€์‹œ์„œ", "์ธ์›๊ด€๋ฆฌ"], "์—…๋ฌด๊ด€๋ฆฌ": ["์—…๋ฌด์ผ์ง€(2025)", "์—…๋ฌด์ผ์ง€(2025๋…„ ์ด์ „)", "๋ฐœ์ฃผ์ฒ˜ ์ •๊ธฐ๋ณด๊ณ ", "๋ณธ์‚ฌ์—…๋ฌด๋ณด๊ณ ", "๊ณต์‚ฌ๊ฐ๋…์ผ์ง€", "์–‘์‹์„œ๋ฅ˜"] }, + "์„ค๊ณ„์„ฑ๊ณผํ’ˆ": { "์‹œ๋ฐฉ์„œ": ["๊ณต์‚ฌ์‹œ๋ฐฉ์„œ"], "์„ค๊ณ„๋„๋ฉด": ["๊ณตํ†ต", "ํ† ๊ณต", "๋น„ํƒˆ๋ฉด์•ˆ์ „๊ณต", "๋ฐฐ์ˆ˜๊ณต", "๊ต๋Ÿ‰๊ณต", "ํฌ์žฅ๊ณต"], "์ˆ˜๋Ÿ‰์‚ฐ์ถœ์„œ": ["ํ† ๊ณต", "๋ฐฐ์ˆ˜๊ณต"], "๋‚ด์—ญ์„œ": ["๋‹จ๊ฐ€์‚ฐ์ถœ์„œ"], "๋ณด๊ณ ์„œ": ["์‹ค์‹œ์„ค๊ณ„๋ณด๊ณ ์„œ", "์ง€๋ฐ˜์กฐ์‚ฌ๋ณด๊ณ ์„œ"], "์ธก๋Ÿ‰๊ณ„์‚ฐ๋ถ€": ["์ธก๋Ÿ‰๊ณ„์‚ฐ๋ถ€"], "์„ค๊ณ„๋‹จ๊ณ„ ์ˆ˜ํ–‰ํ˜‘์˜": ["ํšŒ์˜ยทํ˜‘์˜"] }, + "์‹œ๊ณต๊ฒ€์ธก": { "ํ† ๊ณต": ["๊ฒ€์ธก (๊นจ๊ธฐ)", "๊ฒ€์ธก (๋…ธ์ฒด)"], "๋ฐฐ์ˆ˜๊ณต": ["๊ฒ€์ธก (Vํ˜•์ธก๊ตฌ)", "๊ฒ€์ธก (์ข…๋ฐฐ์ˆ˜๊ด€)"], "๊ตฌ์กฐ๋ฌผ๊ณต": ["๊ฒ€์ธก (ํ‰๋ชฉ๊ต)"], "ํฌ์žฅ๊ณต": ["๊ฒ€์ธก (๊ธฐ์ธต)"] }, + "์„ค๊ณ„๋ณ€๊ฒฝ": { "์‹ค์ •๋ณด๊ณ ": ["ํ† ๊ณต", "๋ฐฐ์ˆ˜๊ณต", "์•ˆ์ „๊ด€๋ฆฌ"], "๊ธฐ์ˆ ์ง€์› ๊ฒ€ํ† ": ["ํ† ๊ณต", "๊ตฌ์กฐ๋ฌผ&๋ถ€๋Œ€๊ณต"] } +}; + +const MAIL_SAMPLES = { + inbound: [ + { person: "๋ผ์˜ค์Šค ๋†๋ฆผ๋ถ€", email: "pany.s@lao.gov.la", time: "2026-03-05", title: "ITTC ๊ต์œก์„ผํ„ฐ ์ฐฉ๊ณต์‹ ์ผ์ • ํ˜‘์˜", summary: "์ฐฉ๊ณต์‹ ๊ด€๋ จํ•˜์—ฌ ์ •๋ถ€ ์ธก ์ธ์‚ฌ์˜ ์ผ์ •์„ ๋ฐ˜์˜ํ•œ ์ตœ์ข… ๊ณต๋ฌธ์„ ์†ก๋ถ€ํ•ฉ๋‹ˆ๋‹ค.", active: true }, + { person: "ํ˜„๋Œ€๊ฑด์„ค (๊น€์ฒ ์ˆ˜ ์†Œ์žฅ)", email: "cs.kim@hdec.co.kr", time: "2026-03-04", title: "[๊ธด๊ธ‰] ์–ด์ฒœ-๊ณต์ฃผ(4์ฐจ) ํ•˜๋„๊ธ‰ ๋ณ€๊ฒฝ๊ณ„์•ฝ ํ†ต๋ณด", summary: "์ฒ ๊ฑฐ๊ณต์‚ฌ ๋ฌผ๋Ÿ‰ ๋ณ€๋™์— ๋”ฐ๋ฅธ ๊ณ„์•ฝ ๊ธˆ์•ก ์กฐ์ • ๊ฑด์ž…๋‹ˆ๋‹ค. ๊ฒ€ํ†  ํ›„ ์Šน์ธ ๋ถ€ํƒ๋“œ๋ฆฝ๋‹ˆ๋‹ค.", active: false } + ], + outbound: [ + { person: "๊ณต์‚ฌ๊ด€๋ฆฌ๋ถ€ (๋ณธ์‚ฌ)", email: "hq_pm@projectmaster.com", time: "2026-03-04", title: "์–ด์ฒœ-๊ณต์ฃผ 2์›” ์›”๊ฐ„ ๊ณต์ •๋ณด๊ณ ์„œ ์ œ์ถœ", summary: "2์›” ํ•œ ๋‹ฌ๊ฐ„์˜ ์ฃผ์š” ๊ณต์ • ๋ฐ ์˜ˆ์‚ฐ ์ง‘ํ–‰ ํ˜„ํ™ฉ ๋ณด๊ณ ์„œ์ž…๋‹ˆ๋‹ค.", active: false } + ], + drafts: [], deleted: [] +}; + +let currentMailTab = 'inbound'; +let filteredMails = []; + +// --- ์ฒจ๋ถ€ํŒŒ์ผ ๋ฐ์ดํ„ฐ ๋กœ๋“œ ๋ฐ ๋ Œ๋”๋ง --- +async function loadAttachments() { + try { + const res = await fetch(API.ATTACHMENTS); + currentFiles = await res.json(); + renderFiles(); + } catch (e) { console.error("Failed to load attachments:", e); } +} + +function renderFiles() { + const isAiActive = document.getElementById('aiToggle').checked; + const container = document.getElementById('attachmentList'); + if (!container) return; + container.innerHTML = ''; + + currentFiles.forEach((file, index) => { + const item = document.createElement('div'); + item.className = 'attachment-item-wrap'; + item.style.marginBottom = "8px"; + + let pathText = "๊ฒฝ๋กœ๋ฅผ ์„ ํƒํ•ด์ฃผ์„ธ์š”"; + let modeClass = "manual-mode"; + + if (file.analysis) { + const prefix = file.analysis.isManual ? "์„ ํƒ ๊ฒฝ๋กœ: " : "์ถ”์ฒœ: "; + pathText = `${prefix}${file.analysis.suggested_path}`; + modeClass = file.analysis.isManual ? "manual-mode" : "smart-mode"; + } else if (isAiActive) { + pathText = "AI ๋ถ„์„ ๋Œ€๊ธฐ ์ค‘..."; + modeClass = "smart-mode"; + } + + item.innerHTML = ` +
+ ๐Ÿ“„ +
+
${file.name}
+
${file.size}
+
+
+ ${pathText} + ${isAiActive ? `` : ''} + +
+
+
+ `; + container.appendChild(item); + }); +} + +// --- AI ๋ถ„์„ ์‹คํ–‰ --- +async function startAnalysis(index, event) { + if (event) event.stopPropagation(); + const file = currentFiles[index]; + if (!file) return; + + // UI ์ƒํƒœ ์—…๋ฐ์ดํŠธ: ๋ถ„์„ ์ค‘ ํ‘œ์‹œ + const logArea = document.getElementById(`log-area-${index}`); + const logContent = document.getElementById(`log-content-${index}`); + if (logArea) logArea.classList.add('active'); + if (logContent) { + logContent.innerHTML = `
+ + AI๊ฐ€ ๋ฌธ์„œ๋ฅผ ์ •๋ฐ€ ๋ถ„์„ ์ค‘์ž…๋‹ˆ๋‹ค... +
`; + } + + try { + const res = await fetch(`${API.ANALYZE_FILE}?filename=${encodeURIComponent(file.name)}`); + const result = await res.json(); + + if (result.error) { + if (logContent) logContent.innerHTML = `
์˜ค๋ฅ˜: ${result.error}
`; + return; + } + + // ๋ถ„์„ ๊ฒฐ๊ณผ ์ €์žฅ ๋ฐ UI ๊ฐฑ์‹  + currentFiles[index].analysis = result.final_result; + currentFiles[index].analysis.isManual = false; + + if (logContent) { + logContent.innerHTML = ` +
+
โœจ AI ๋ถ„์„ ์™„๋ฃŒ
+
${result.final_result.reason}
+
+ `; + } + + renderFiles(); + } catch (e) { + console.error("AI Analysis failed:", e); + if (logContent) logContent.innerHTML = `
๋ถ„์„ ์‹คํŒจ: ๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.
`; + } +} + +// --- ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์ œ์–ด --- +function showPreview(index, event) { + if (event && (event.target.closest('.btn-group') || event.target.closest('.path-display'))) return; + const file = currentFiles[index]; + if (!file) return; + + const previewArea = document.getElementById('mailPreviewArea'); + const toggleIcon = document.getElementById('previewToggleIcon'); + const fullViewBtn = document.getElementById('fullViewBtn'); + const previewContainer = document.getElementById('previewContainer'); + + if (previewArea) { + previewArea.classList.add('active'); + if (toggleIcon) toggleIcon.innerText = 'โ–ถ'; + } + + const fileUrl = Utils.getSafeFileUrl(file.name); + if (fullViewBtn) { + fullViewBtn.style.display = 'block'; + fullViewBtn.onclick = () => window.open(fileUrl, 'PMFullView', 'width=1000,height=800'); + } + + if (file.name.toLowerCase().endsWith('.pdf')) { + previewContainer.innerHTML = ``; + } else { + previewContainer.innerHTML = `
${file.name}
`; + } + + document.querySelectorAll('.attachment-item').forEach(item => item.classList.remove('active')); + if (event?.currentTarget) event.currentTarget.classList.add('active'); +} + +function togglePreviewAuto() { + const area = document.getElementById('mailPreviewArea'); + const icon = document.getElementById('previewToggleIcon'); + if (area) { + const isActive = area.classList.toggle('active'); + if (icon) icon.innerText = isActive ? 'โ–ถ' : 'โ—€'; + } +} + +// --- ๋ฉ”์ผ ๋ฆฌ์ŠคํŠธ ์ œ์–ด --- +function renderMailList(tabType, mailsToShow = null) { + currentMailTab = tabType; + const container = document.querySelector('.mail-items-container'); + if (!container) return; + + const mails = mailsToShow || MAIL_SAMPLES[tabType] || []; + filteredMails = mails; + updateBulkActionBar(); + + container.innerHTML = mails.map((mail, idx) => ` +
+ +
+
+ ${mail.person} +
${mail.time}
+
+
${mail.title}
+
${mail.summary}
+
+
+ `).join(''); + + const activeIdx = mails.findIndex(m => m.active); + if (activeIdx !== -1) updateMailContent(mails[activeIdx]); +} + +function selectMailItem(el, index) { + document.querySelectorAll('.mail-item').forEach(item => item.classList.remove('active')); + el.classList.add('active'); + const mail = filteredMails[index]; + if (mail) updateMailContent(mail); +} + +function updateMailContent(mail) { + const title = document.querySelector('.mail-content-header h2'); + if (title) title.innerText = mail.title; + document.querySelector('.mail-body').innerHTML = mail.summary.replace(/\n/g, '
') + "

๋ณธ ๋‚ด์šฉ์€ ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ์ž…๋‹ˆ๋‹ค."; +} + +function switchMailTab(el, tabType) { + document.querySelectorAll('.mail-tab').forEach(tab => tab.classList.remove('active')); + el.classList.add('active'); + renderMailList(tabType); +} + +// --- ๊ฒฝ๋กœ ์„ ํƒ ๋ชจ๋‹ฌ --- +function openPathModal(index, event) { + if (event) event.stopPropagation(); + editingIndex = index; + const tabSelect = document.getElementById('tabSelect'); + if (tabSelect) { + tabSelect.innerHTML = Object.keys(HIERARCHY).map(tab => ``).join(''); + updateCategories(); + ModalManager.open('pathModal'); + } +} + +function updateCategories() { + const tab = document.getElementById('tabSelect').value; + document.getElementById('categorySelect').innerHTML = Object.keys(HIERARCHY[tab]).map(cat => ``).join(''); + updateSubs(); +} + +function updateSubs() { + const tab = document.getElementById('tabSelect').value; + const cat = document.getElementById('categorySelect').value; + document.getElementById('subSelect').innerHTML = HIERARCHY[tab][cat].map(sub => ``).join(''); +} + +function applyPathSelection() { + const path = `${document.getElementById('tabSelect').value} > ${document.getElementById('categorySelect').value} > ${document.getElementById('subSelect').value}`; + if (!currentFiles[editingIndex].analysis) currentFiles[editingIndex].analysis = {}; + currentFiles[editingIndex].analysis.suggested_path = path; + currentFiles[editingIndex].analysis.isManual = true; + renderFiles(); + ModalManager.close('pathModal'); +} + +// --- ์ฃผ์†Œ๋ก ๊ด€๋ฆฌ --- +let addressBookData = [ + { name: "์ดํƒœํ›ˆ", dept: "PM Overseas / ์„ ์ž„์—ฐ๊ตฌ์›", email: "th.lee@projectmaster.com", phone: "010-1234-5678" }, + { name: "Pany S.", dept: "๋ผ์˜ค์Šค ๋†๋ฆผ๋ถ€ / ๊ตญ์žฅ", email: "pany.s@lao.gov.la", phone: "+856-20-1234-5678" } +]; +let contactEditingIndex = -1; + +function openAddressBook() { renderAddressBook(); ModalManager.open('addressBookModal'); } +function closeAddressBook() { ModalManager.close('addressBookModal'); } + +function renderAddressBook() { + const body = document.getElementById('addressBookBody'); + if (!body) return; + body.innerHTML = addressBookData.map((c, idx) => ` + + ${c.name}${c.dept}${c.email}${c.phone} + + + + + `).join(''); +} + +function toggleAddContactForm() { + const form = document.getElementById('addContactForm'); + if (form.style.display === 'none') form.style.display = 'block'; + else { form.style.display = 'none'; contactEditingIndex = -1; } +} + +function editContact(index) { + const c = addressBookData[index]; + contactEditingIndex = index; + document.getElementById('newContactName').value = c.name; + document.getElementById('newContactDept').value = c.dept; + document.getElementById('newContactEmail').value = c.email; + document.getElementById('newContactPhone').value = c.phone; + document.getElementById('addContactForm').style.display = 'block'; +} + +function deleteContact(index) { + if (confirm(`'${addressBookData[index].name}'๋‹˜์„ ์‚ญ์ œํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?`)) { addressBookData.splice(index, 1); renderAddressBook(); } +} + +function addContact() { + const name = document.getElementById('newContactName').value; + if (!name) return alert("์ด๋ฆ„์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”."); + const data = { name, dept: document.getElementById('newContactDept').value, email: document.getElementById('newContactEmail').value, phone: document.getElementById('newContactPhone').value }; + if (contactEditingIndex > -1) addressBookData[contactEditingIndex] = data; + else addressBookData.push(data); + renderAddressBook(); toggleAddContactForm(); +} + +// --- ๊ณตํ†ต ์•ก์…˜ --- +function updateBulkActionBar() { + const count = document.querySelectorAll('.mail-item-checkbox:checked').length; + const bar = document.getElementById('mailBulkActions'); + if (count > 0) { bar.classList.add('active'); document.getElementById('selectedCount').innerText = `${count}๊ฐœ ์„ ํƒ๋จ`; } + else bar.classList.remove('active'); +} + +// ์ดˆ๊ธฐํ™” +document.addEventListener('DOMContentLoaded', () => { + loadAttachments(); + renderMailList('inbound'); +}); diff --git a/log_scorer.py b/log_scorer.py new file mode 100644 index 0000000..6a64e13 --- /dev/null +++ b/log_scorer.py @@ -0,0 +1,63 @@ +import re + +class LogScorer: + """๋กœ๊ทธ ํ…์ŠคํŠธ์˜ ์‹œ๋งจํ‹ฑ ๊ฐ€์น˜๋ฅผ ํŒ๋ณ„ํ•˜์—ฌ ์ ์ˆ˜ํ™”ํ•˜๋Š” ๋ชจ๋“ˆ (SWVW)""" + + # ์—…๋ฌด ๊ฐ€์น˜ ๋ฒ”์ฃผ๋ณ„ ๊ฐ€์ค‘์น˜ ์ •์˜ + # 1.0: ํ•ต์‹ฌ ์˜์‚ฌ๊ฒฐ์ •/๊ณ„์•ฝ, 0.7: ์ง€๋Šฅํ˜•/๊ถŒํ•œ๊ด€๋ฆฌ, 0.4: ์ผ๋ฐ˜๊ด€๋ฆฌ, 0.1: ๋‹จ์ˆœํ™œ๋™ + WEIGHT_MAP = { + "CORE": 1.0, # ์„ค๊ณ„๋ณ€๊ฒฝ, ์‹ค์ •๋ณด๊ณ , ๊ณ„์•ฝ, ์ •์‚ฐ ๋“ฑ (์ถ”ํ›„ ํ™•์žฅ ๋Œ€๋น„) + "INTELLIGENT": 0.7, # AI์š”์•ฝ, PDF๋ณ€ํ™˜, ๋ถ„์„ ๋“ฑ + "AUTH": 0.6, # ๊ถŒํ•œ ์ถ”๊ฐ€, ๋ณด์•ˆ ์„ค์ • ๋“ฑ + "MGMT": 0.4, # ์—…๋กœ๋“œ, ์ˆ˜์ •, ์ด๋ฆ„ ๋ณ€๊ฒฝ ๋“ฑ + "SIMPLE": 0.2, # ๋‹ค์šด๋กœ๋“œ, ์‚ญ์ œ, ์ƒ์„ฑ ๋“ฑ + "AUTO": 0.0 # ์ž๋™ ์‚ญ์ œ, ์‹œ์Šคํ…œ ๋กœ๊ทธ ๋“ฑ + } + + # ๋ฒ”์ฃผ๋ณ„ ํ‚ค์›Œ๋“œ ์ •์˜ (์‹ค๋ฌด ๋ฌธ๋งฅ ๋ฐ˜์˜) + KEYWORDS = { + "CORE": ["๋ณด๊ณ ", "๊ณ„์•ฝ", "์ •์‚ฐ", "์„ค๊ณ„", "๊ฒ€ํ† ", "์Šน์ธ", "๊ณต๋ฌธ", "ํ†ต๋ณด"], + "INTELLIGENT": ["AI", "์š”์•ฝ", "๋ณ€ํ™˜", "PDF"], + "AUTH": ["๊ถŒํ•œ", "์ฐธ์—ฌ์ž", "๊ด€๋ฆฌ์ž", "๋ณด์•ˆ"], + "MGMT": ["์—…๋กœ๋“œ", "์ˆ˜์ •", "๋ณ€๊ฒฝ", "์ฒจ๋ถ€", "์ถ”๊ฐ€"], + "SIMPLE": ["๋‹ค์šด๋กœ๋“œ", "์ƒ์„ฑ", "์ด๋™", "์‚ญ์ œ"], + "AUTO": ["์ž๋™", "์‹œ์Šคํ…œ"] + } + + @classmethod + def get_score(cls, log_text): + """๋กœ๊ทธ ํ…์ŠคํŠธ๋ฅผ ๋ถ„์„ํ•˜์—ฌ 0.0 ~ 1.0 ์‚ฌ์ด์˜ ๊ฐ€์น˜ ์ ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜""" + if not log_text or log_text == "๋ฐ์ดํ„ฐ ์—†์Œ": + return 0.0 + + # ๋‚ ์งœ ๋ถ€๋ถ„ ์ œ๊ฑฐ (์˜ˆ: "2024.03.01, " ์ œ๊ฑฐ) + clean_log = re.sub(r'^\d{2,4}\.\d{2}\.\d{2},\s*', '', log_text) + + # 1. ํŠน์ • ํ‚ค์›Œ๋“œ ๋งค์นญ์„ ํ†ตํ•œ ๊ธฐ๋ณธ ๋ฒ”์ฃผ ํŒ๋ณ„ + for category, keywords in cls.KEYWORDS.items(): + if any(k in clean_log for k in keywords): + # '์ž๋™ ์‚ญ์ œ'๋Š” ๋ณ„๋„ ์ฒ˜๋ฆฌ + if category == "SIMPLE" and "์ž๋™" in clean_log: + return cls.WEIGHT_MAP["AUTO"] + return cls.WEIGHT_MAP[category] + + return 0.3 # ๊ธฐ๋ณธ๊ฐ’ (๋ถ„๋ฅ˜๋˜์ง€ ์•Š์€ ํ™œ๋™) + + @classmethod + def calculate_work_density(cls, logs): + """๋กœ๊ทธ ๋ชฉ๋ก์„ ๋ฐ›์•„ ํ‰๊ท  ์—…๋ฌด ๋ฐ€๋„ ์‚ฐ์ถœ""" + if not logs: return 0.0 + scores = [cls.get_score(log) for log in logs] + return sum(scores) / len(scores) + +# ํ…Œ์ŠคํŠธ ์ฝ”๋“œ +if __name__ == "__main__": + test_logs = [ + "2026.03.30, ํ•˜๋„๊ธ‰ ๊ณ„์•ฝ ํ†ต๋ณด์„œ ๊ฒ€ํ† ", + "2026.03.25, AI์š”์•ฝ ์™„๋ฃŒ", + "2026.03.20, ๋ถ€๊ด€๋ฆฌ์ž ๊ถŒํ•œ ์ถ”๊ฐ€", + "2026.03.15, ํŒŒ์ผ ์—…๋กœ๋“œ", + "2026.03.10, ํด๋” ์ž๋™ ์‚ญ์ œ" + ] + for log in test_logs: + print(f"Log: {log} => Score: {LogScorer.get_score(log)}") diff --git a/prediction_service.py b/prediction_service.py index 7212d0b..3c95c9a 100644 --- a/prediction_service.py +++ b/prediction_service.py @@ -1,97 +1,97 @@ -import numpy as np -from datetime import datetime - -class SOIPredictionService: - """ํ•™์Šตํ˜• ์‹œ๊ณ„์—ด ์˜ˆ์ธก ๋ฐ ํ”ผ์ฒ˜ ์ถ”์ถœ ์—”์ง„""" - - @staticmethod - def get_historical_soi(cursor, project_id): - """DB์—์„œ ํ”„๋กœ์ ํŠธ์˜ ๊ณผ๊ฑฐ SOI ํžˆ์Šคํ† ๋ฆฌ๋ฅผ ์‹œํ€€์Šค๋กœ ์ถ”์ถœ""" - cursor.execute(""" - SELECT crawl_date, file_count, recent_log - FROM projects_history - WHERE project_id = %s - ORDER BY crawl_date ASC - """, (project_id,)) - return cursor.fetchall() - - @staticmethod - def extract_vitality_features(history): - """๋”ฅ๋Ÿฌ๋‹ ํ•™์Šต์„ ์œ„ํ•œ 4๋Œ€ ํ•ต์‹ฌ ํ”ผ์ฒ˜ ์ถ”์ถœ (Feature Engineering)""" - if len(history) < 2: - return {"velocity": 0, "acceleration": 0, "consistency": 0.5, "density": 0.1} - - # ์‹ค์ œ ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ์— ๋งž๊ฒŒ ๋ณด์ • - 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 - - # 2. ํ™œ๋™ ๊ฐ€์†๋„ (Acceleration): ์ตœ๊ทผ ํ™œ๋™์ด ๋นจ๋ผ์ง€๋Š”์ง€ ๋А๋ ค์ง€๋Š”์ง€ - acceleration = np.diff(np.diff(counts)).mean() if len(counts) > 2 else 0 - - # 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 - - # 4. ๊ด€๋ฆฌ ์ผ๊ด€์„ฑ (Consistency): ์—…๋ฐ์ดํŠธ ๊ฐ„๊ฒฉ์˜ ํ‘œ์ค€ํŽธ์ฐจ (๋‚ฎ์„์ˆ˜๋ก ์ข‹์Œ) - # (ํ˜„์žฌ ๋ฐ์ดํ„ฐ๋Š” ์ผ์ผ ํฌ๋กค๋ง์ด๋ฏ€๋กœ ๋กœ๊ทธ ํ…์ŠคํŠธ ๋ณ€ํ™” ์‹œ์ ์„ ๊ธฐ์ค€์œผ๋กœ ๊ฐ„๊ฒฉ ๊ณ„์‚ฐ ๊ฐ€๋Šฅ) - - 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): - """๊ธฐ์กด ์ ์ˆ˜์™€ ์‹œ๊ณ„์—ด ํ”ผ์ฒ˜๋ฅผ ๊ฒฐํ•ฉํ•˜์—ฌ ๋ฏธ๋ž˜ ์ ์ˆ˜ ์˜ˆ์ธก""" - # ๋ฐ์ดํ„ฐ๊ฐ€ ๋„ˆ๋ฌด ์ ์œผ๋ฉด ๋ฌด์กฐ๊ฑด ๋ณด์ˆ˜์  ๊ฐ์‡„ (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) - current_val = float(current_soi) - - # [์ •๋ฐ€ ์ •์ฒด ๋ถ„์„] - # 1. ํŒŒ์ผ ์ˆ˜ ๋ณ€ํ™” ํ™•์ธ (์ตœ๊ทผ 5๊ฐœ ์ƒ˜ํ”Œ) - recent_counts = [int(h['file_count'] or 0) for h in history[-5:]] - is_hard_stagnant = len(set(recent_counts)) <= 1 # ํŒŒ์ผ ์ˆ˜ ๋ณ€๋™์ด ์ „ํ˜€ ์—†์Œ - - # 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) +import numpy as np +from datetime import datetime + +class SOIPredictionService: + """ํ•™์Šตํ˜• ์‹œ๊ณ„์—ด ์˜ˆ์ธก ๋ฐ ํ”ผ์ฒ˜ ์ถ”์ถœ ์—”์ง„""" + + @staticmethod + def get_historical_avi(cursor, project_id): + """DB์—์„œ ํ”„๋กœ์ ํŠธ์˜ ๊ณผ๊ฑฐ AVI ํžˆ์Šคํ† ๋ฆฌ๋ฅผ ์‹œํ€€์Šค๋กœ ์ถ”์ถœ""" + cursor.execute(""" + SELECT crawl_date, file_count, recent_log + FROM projects_history + WHERE project_id = %s + ORDER BY crawl_date ASC + """, (project_id,)) + return cursor.fetchall() + + @staticmethod + def extract_vitality_features(history): + """๋”ฅ๋Ÿฌ๋‹ ํ•™์Šต์„ ์œ„ํ•œ 4๋Œ€ ํ•ต์‹ฌ ํ”ผ์ฒ˜ ์ถ”์ถœ (Feature Engineering)""" + if len(history) < 2: + return {"velocity": 0, "acceleration": 0, "consistency": 0.5, "density": 0.1} + + # ์‹ค์ œ ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ์— ๋งž๊ฒŒ ๋ณด์ • + 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 + + # 2. ํ™œ๋™ ๊ฐ€์†๋„ (Acceleration): ์ตœ๊ทผ ํ™œ๋™์ด ๋นจ๋ผ์ง€๋Š”์ง€ ๋А๋ ค์ง€๋Š”์ง€ + acceleration = np.diff(np.diff(counts)).mean() if len(counts) > 2 else 0 + + # 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 + + # 4. ๊ด€๋ฆฌ ์ผ๊ด€์„ฑ (Consistency): ์—…๋ฐ์ดํŠธ ๊ฐ„๊ฒฉ์˜ ํ‘œ์ค€ํŽธ์ฐจ (๋‚ฎ์„์ˆ˜๋ก ์ข‹์Œ) + # (ํ˜„์žฌ ๋ฐ์ดํ„ฐ๋Š” ์ผ์ผ ํฌ๋กค๋ง์ด๋ฏ€๋กœ ๋กœ๊ทธ ํ…์ŠคํŠธ ๋ณ€ํ™” ์‹œ์ ์„ ๊ธฐ์ค€์œผ๋กœ ๊ฐ„๊ฒฉ ๊ณ„์‚ฐ ๊ฐ€๋Šฅ) + + return { + "velocity": float(velocity), + "acceleration": float(acceleration), + "density": float(density), + "sample_count": len(history) + } + + @staticmethod + def predict_future_avi(current_avi, history, days_ahead=14): + """๊ธฐ์กด ์ ์ˆ˜์™€ ์‹œ๊ณ„์—ด ํ”ผ์ฒ˜๋ฅผ ๊ฒฐํ•ฉํ•˜์—ฌ ๋ฏธ๋ž˜ ์ ์ˆ˜ ์˜ˆ์ธก""" + # ๋ฐ์ดํ„ฐ๊ฐ€ ๋„ˆ๋ฌด ์ ์œผ๋ฉด ๋ฌด์กฐ๊ฑด ๋ณด์ˆ˜์  ๊ฐ์‡„ (14์ผ ๊ธฐ์ค€ ์•ฝ -2.1์ ) + if not history or len(history) < 3: + return round(max(0, min(100, current_avi - (0.15 * days_ahead))), 1) + + features = SOIPredictionService.extract_vitality_features(history) + current_val = float(current_avi) + + # [์ •๋ฐ€ ์ •์ฒด ๋ถ„์„] + # 1. ํŒŒ์ผ ์ˆ˜ ๋ณ€ํ™” ํ™•์ธ (์ตœ๊ทผ 5๊ฐœ ์ƒ˜ํ”Œ) + recent_counts = [int(h['file_count'] or 0) for h in history[-5:]] + is_hard_stagnant = len(set(recent_counts)) <= 1 # ํŒŒ์ผ ์ˆ˜ ๋ณ€๋™์ด ์ „ํ˜€ ์—†์Œ + + # 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/project_service.py b/project_service.py index 6ad3702..7ec2f75 100644 --- a/project_service.py +++ b/project_service.py @@ -1,33 +1,33 @@ -from sql_queries import DashboardQueries - -class ProjectService: - @staticmethod - def get_available_dates_logic(cursor): - cursor.execute(DashboardQueries.GET_AVAILABLE_DATES) - rows = cursor.fetchall() - return [row['crawl_date'].strftime("%Y.%m.%d") for row in rows if row['crawl_date']] - - @staticmethod - def get_project_data_logic(cursor, date_str): - target_date = date_str.replace(".", "-") if date_str and date_str != "-" else None - - if not target_date: - cursor.execute(DashboardQueries.GET_LAST_CRAWL_DATE) - res = cursor.fetchone() - target_date = res['last_date'] - - if not target_date: - return {"projects": []} - - cursor.execute(DashboardQueries.GET_PROJECT_LIST, (target_date,)) - rows = cursor.fetchall() - - projects = [] - for r in rows: - name = r['short_nm'] if r['short_nm'] and r['short_nm'].strip() else r['project_nm'] - projects.append([ - name, r['department'], r['master'], - r['recent_log'], r['file_count'], - r['continent'], r['country'] - ]) - return {"projects": projects} +from sql_queries import DashboardQueries + +class ProjectService: + @staticmethod + def get_available_dates_logic(cursor): + cursor.execute(DashboardQueries.GET_AVAILABLE_DATES) + rows = cursor.fetchall() + return [row['crawl_date'].strftime("%Y.%m.%d") for row in rows if row['crawl_date']] + + @staticmethod + def get_project_data_logic(cursor, date_str): + target_date = date_str.replace(".", "-") if date_str and date_str != "-" else None + + if not target_date: + cursor.execute(DashboardQueries.GET_LAST_CRAWL_DATE) + res = cursor.fetchone() + target_date = res['last_date'] + + if not target_date: + return {"projects": []} + + cursor.execute(DashboardQueries.GET_PROJECT_LIST, (target_date,)) + rows = cursor.fetchall() + + projects = [] + for r in rows: + name = r['short_nm'] if r['short_nm'] and r['short_nm'].strip() else r['project_nm'] + projects.append([ + name, r['department'], r['master'], + r['recent_log'], r['file_count'], + r['continent'], r['country'] + ]) + return {"projects": projects} diff --git a/requirements.txt b/requirements.txt index 71c0078..be20ccd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,14 @@ fastapi==0.110.0 uvicorn==0.29.0 playwright==1.42.0 python-dotenv==1.0.1 -pypdf==4.1.0 \ No newline at end of file +pypdf==4.1.0 +pymysql +pandas +sqlalchemy +openpyxl +pytesseract +pdf2image +pillow +numpy +pydantic +jinja2 diff --git a/schemas.py b/schemas.py index 790822f..b14c0a6 100644 --- a/schemas.py +++ b/schemas.py @@ -1,10 +1,10 @@ -from pydantic import BaseModel - -class AuthRequest(BaseModel): - user_id: str - password: str - -class InquiryReplyRequest(BaseModel): - reply: str - status: str - handler: str +from pydantic import BaseModel + +class AuthRequest(BaseModel): + user_id: str + password: str + +class InquiryReplyRequest(BaseModel): + reply: str + status: str + handler: str diff --git a/server.py b/server.py index 11b04ea..5d649fd 100644 --- a/server.py +++ b/server.py @@ -1,182 +1,182 @@ -import os -import sys -import asyncio -import pymysql -from fastapi import FastAPI, Request -from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import StreamingResponse, FileResponse -from fastapi.staticfiles import StaticFiles -from fastapi.templating import Jinja2Templates - -from analyze import analyze_file_content -from crawler_service import run_crawler_service, crawl_stop_event -from schemas import AuthRequest, InquiryReplyRequest -from inquiry_service import InquiryService -from project_service import ProjectService -from analysis_service import AnalysisService - -# --- ํ™˜๊ฒฝ ์„ค์ • --- -os.environ["PYTHONIOENCODING"] = "utf-8" -TESSDATA_PREFIX = os.getenv("TESSDATA_PREFIX", r"C:\Users\User\AppData\Local\Programs\Tesseract-OCR\tessdata") -os.environ["TESSDATA_PREFIX"] = TESSDATA_PREFIX - -app = FastAPI(title="Project Master Overseas API") -templates = Jinja2Templates(directory="templates") - -# ์ •์  ํŒŒ์ผ ๋งˆ์šดํŠธ -app.mount("/style", StaticFiles(directory="style"), name="style") -app.mount("/js", StaticFiles(directory="js"), name="js") -app.mount("/sample_files", StaticFiles(directory="sample"), name="sample_files") - -app.add_middleware( - CORSMiddleware, - allow_origins=["*"], - allow_credentials=False, - allow_methods=["*"], - allow_headers=["*"], -) - -# --- ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜ --- -def get_db_connection(): - """MySQL ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ์„ ๋ฐ˜ํ™˜""" - return pymysql.connect( - host=os.getenv('DB_HOST', 'localhost'), - user=os.getenv('DB_USER', 'root'), - password=os.getenv('DB_PASSWORD', '45278434'), - database=os.getenv('DB_NAME', 'PM_proto'), - charset='utf8mb4', - cursorclass=pymysql.cursors.DictCursor - ) - -async def run_in_threadpool(func, *args): - """๋™๊ธฐ ํ•จ์ˆ˜๋ฅผ ๋น„์ฐจ๋‹จ ๋ฐฉ์‹์œผ๋กœ ์‹คํ–‰""" - loop = asyncio.get_event_loop() - return await loop.run_in_executor(None, func, *args) - -# --- HTML ๋ผ์šฐํŒ… --- -@app.get("/") -async def root(request: Request): - return templates.TemplateResponse("index.html", {"request": request}) - -@app.get("/dashboard") -async def get_dashboard(request: Request): - return templates.TemplateResponse("dashboard.html", {"request": request}) - -@app.get("/mailTest") -async def get_mail_test(request: Request): - return templates.TemplateResponse("mailTest.html", {"request": request}) - -@app.get("/inquiries") -async def get_inquiries_page(request: Request): - return templates.TemplateResponse("inquiries.html", {"request": request}) - -@app.get("/analysis") -async def get_analysis_page(request: Request): - return templates.TemplateResponse("analysis.html", {"request": request}) - -# --- ๋ฌธ์˜์‚ฌํ•ญ API --- -@app.get("/api/inquiries") -async def get_inquiries(pm_type: str = None, category: str = None, status: str = None, keyword: str = None): - try: - with get_db_connection() as conn: - with conn.cursor() as cursor: - return InquiryService.get_inquiries_logic(cursor, pm_type, category, status, keyword) - except Exception as e: - return {"error": str(e)} - -@app.get("/api/inquiries/{id}") -async def get_inquiry_detail(id: int): - try: - with get_db_connection() as conn: - with conn.cursor() as cursor: - return InquiryService.get_inquiry_detail_logic(cursor, id) - except Exception as e: - return {"error": str(e)} - -@app.post("/api/inquiries/{id}/reply") -async def update_inquiry_reply(id: int, req: InquiryReplyRequest): - try: - with get_db_connection() as conn: - with conn.cursor() as cursor: - return InquiryService.update_inquiry_reply_logic(cursor, conn, id, req) - except Exception as e: - return {"error": str(e)} - -@app.delete("/api/inquiries/{id}/reply") -async def delete_inquiry_reply(id: int): - try: - with get_db_connection() as conn: - with conn.cursor() as cursor: - return InquiryService.delete_inquiry_reply_logic(cursor, conn, id) - except Exception as e: - return {"error": str(e)} - -# --- ํ”„๋กœ์ ํŠธ ๋ฐ ํžˆ์Šคํ† ๋ฆฌ API --- -@app.get("/available-dates") -async def get_available_dates(): - try: - with get_db_connection() as conn: - with conn.cursor() as cursor: - return ProjectService.get_available_dates_logic(cursor) - except Exception as e: - return {"error": str(e)} - -@app.get("/project-data") -async def get_project_data(date: str = None): - try: - with get_db_connection() as conn: - with conn.cursor() as cursor: - return ProjectService.get_project_data_logic(cursor, date) - except Exception as e: - return {"error": str(e)} - -# --- ๋ถ„์„ API (AnalysisService ์—ฐ๋™) --- -@app.get("/project-activity") -async def get_project_activity(date: str = None): - try: - with get_db_connection() as conn: - with conn.cursor() as cursor: - return AnalysisService.get_project_activity_logic(cursor, date) - except Exception as e: - return {"error": str(e)} - -@app.get("/api/analysis/p-war") -async def get_p_war_analysis(): - try: - with get_db_connection() as conn: - with conn.cursor() as cursor: - return AnalysisService.get_p_zsr_analysis_logic(cursor) - except Exception as e: - return {"error": str(e)} - -# --- ์ˆ˜์ง‘ ๋ฐ ๋™๊ธฐํ™” API --- -@app.post("/auth/crawl") -async def auth_crawl(req: AuthRequest): - if req.user_id == os.getenv("PM_USER_ID") and req.password == os.getenv("PM_PASSWORD"): - return {"success": True} - return {"success": False, "message": "ํฌ๋กค๋ง์„ ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."} - -@app.get("/sync") -async def sync_data(): - return StreamingResponse(run_crawler_service(), media_type="text_event-stream") - -@app.get("/stop-sync") -async def stop_sync(): - crawl_stop_event.set() - return {"success": True} - -# --- ํŒŒ์ผ ๋ฐ ์ฒจ๋ถ€ํŒŒ์ผ API --- -@app.get("/attachments") -async def get_attachments(): - path = "sample" - if not os.path.exists(path): os.makedirs(path) - return [{"name": f, "size": f"{os.path.getsize(os.path.join(path, f))/1024:.1f} KB"} - for f in os.listdir(path) if os.path.isfile(os.path.join(path, f))] - -@app.get("/analyze-file") -async def analyze_file(filename: str): - return await run_in_threadpool(analyze_file_content, filename) - -@app.get("/sample.png") -async def get_sample_img(): - return FileResponse("sample.png") +import os +import sys +import asyncio +import pymysql +from fastapi import FastAPI, Request +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import StreamingResponse, FileResponse +from fastapi.staticfiles import StaticFiles +from fastapi.templating import Jinja2Templates + +from analyze import analyze_file_content +from crawler_service import run_crawler_service, crawl_stop_event +from schemas import AuthRequest, InquiryReplyRequest +from inquiry_service import InquiryService +from project_service import ProjectService +from analysis_service import AnalysisService + +# --- ํ™˜๊ฒฝ ์„ค์ • --- +os.environ["PYTHONIOENCODING"] = "utf-8" +TESSDATA_PREFIX = os.getenv("TESSDATA_PREFIX", r"C:\Users\User\AppData\Local\Programs\Tesseract-OCR\tessdata") +os.environ["TESSDATA_PREFIX"] = TESSDATA_PREFIX + +app = FastAPI(title="Project Master Overseas API") +templates = Jinja2Templates(directory="templates") + +# ์ •์  ํŒŒ์ผ ๋งˆ์šดํŠธ +app.mount("/style", StaticFiles(directory="style"), name="style") +app.mount("/js", StaticFiles(directory="js"), name="js") +app.mount("/sample_files", StaticFiles(directory="sample"), name="sample_files") + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=False, + allow_methods=["*"], + allow_headers=["*"], +) + +# --- ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜ --- +def get_db_connection(): + """MySQL ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ์„ ๋ฐ˜ํ™˜""" + return pymysql.connect( + host=os.getenv('DB_HOST', 'localhost'), + user=os.getenv('DB_USER', 'root'), + password=os.getenv('DB_PASSWORD', '45278434'), + database=os.getenv('DB_NAME', 'PM_proto'), + charset='utf8mb4', + cursorclass=pymysql.cursors.DictCursor + ) + +async def run_in_threadpool(func, *args): + """๋™๊ธฐ ํ•จ์ˆ˜๋ฅผ ๋น„์ฐจ๋‹จ ๋ฐฉ์‹์œผ๋กœ ์‹คํ–‰""" + loop = asyncio.get_event_loop() + return await loop.run_in_executor(None, func, *args) + +# --- HTML ๋ผ์šฐํŒ… --- +@app.get("/") +async def root(request: Request): + return templates.TemplateResponse("index.html", {"request": request}) + +@app.get("/dashboard") +async def get_dashboard(request: Request): + return templates.TemplateResponse("dashboard.html", {"request": request}) + +@app.get("/mailTest") +async def get_mail_test(request: Request): + return templates.TemplateResponse("mailTest.html", {"request": request}) + +@app.get("/inquiries") +async def get_inquiries_page(request: Request): + return templates.TemplateResponse("inquiries.html", {"request": request}) + +@app.get("/analysis") +async def get_analysis_page(request: Request): + return templates.TemplateResponse("analysis.html", {"request": request}) + +# --- ๋ฌธ์˜์‚ฌํ•ญ API --- +@app.get("/api/inquiries") +async def get_inquiries(pm_type: str = None, category: str = None, status: str = None, keyword: str = None): + try: + with get_db_connection() as conn: + with conn.cursor() as cursor: + return InquiryService.get_inquiries_logic(cursor, pm_type, category, status, keyword) + except Exception as e: + return {"error": str(e)} + +@app.get("/api/inquiries/{id}") +async def get_inquiry_detail(id: int): + try: + with get_db_connection() as conn: + with conn.cursor() as cursor: + return InquiryService.get_inquiry_detail_logic(cursor, id) + except Exception as e: + return {"error": str(e)} + +@app.post("/api/inquiries/{id}/reply") +async def update_inquiry_reply(id: int, req: InquiryReplyRequest): + try: + with get_db_connection() as conn: + with conn.cursor() as cursor: + return InquiryService.update_inquiry_reply_logic(cursor, conn, id, req) + except Exception as e: + return {"error": str(e)} + +@app.delete("/api/inquiries/{id}/reply") +async def delete_inquiry_reply(id: int): + try: + with get_db_connection() as conn: + with conn.cursor() as cursor: + return InquiryService.delete_inquiry_reply_logic(cursor, conn, id) + except Exception as e: + return {"error": str(e)} + +# --- ํ”„๋กœ์ ํŠธ ๋ฐ ํžˆ์Šคํ† ๋ฆฌ API --- +@app.get("/available-dates") +async def get_available_dates(): + try: + with get_db_connection() as conn: + with conn.cursor() as cursor: + return ProjectService.get_available_dates_logic(cursor) + except Exception as e: + return {"error": str(e)} + +@app.get("/project-data") +async def get_project_data(date: str = None): + try: + with get_db_connection() as conn: + with conn.cursor() as cursor: + return ProjectService.get_project_data_logic(cursor, date) + except Exception as e: + return {"error": str(e)} + +# --- ๋ถ„์„ API (AnalysisService ์—ฐ๋™) --- +@app.get("/project-activity") +async def get_project_activity(date: str = None): + try: + with get_db_connection() as conn: + with conn.cursor() as cursor: + return AnalysisService.get_project_activity_logic(cursor, date) + except Exception as e: + return {"error": str(e)} + +@app.get("/api/analysis/p-war") +async def get_p_war_analysis(): + try: + with get_db_connection() as conn: + with conn.cursor() as cursor: + return AnalysisService.get_p_zsr_analysis_logic(cursor) + except Exception as e: + return {"error": str(e)} + +# --- ์ˆ˜์ง‘ ๋ฐ ๋™๊ธฐํ™” API --- +@app.post("/auth/crawl") +async def auth_crawl(req: AuthRequest): + if req.user_id == os.getenv("PM_USER_ID") and req.password == os.getenv("PM_PASSWORD"): + return {"success": True} + return {"success": False, "message": "ํฌ๋กค๋ง์„ ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."} + +@app.get("/sync") +async def sync_data(): + return StreamingResponse(run_crawler_service(), media_type="text_event-stream") + +@app.get("/stop-sync") +async def stop_sync(): + crawl_stop_event.set() + return {"success": True} + +# --- ํŒŒ์ผ ๋ฐ ์ฒจ๋ถ€ํŒŒ์ผ API --- +@app.get("/attachments") +async def get_attachments(): + path = "sample" + if not os.path.exists(path): os.makedirs(path) + return [{"name": f, "size": f"{os.path.getsize(os.path.join(path, f))/1024:.1f} KB"} + for f in os.listdir(path) if os.path.isfile(os.path.join(path, f))] + +@app.get("/analyze-file") +async def analyze_file(filename: str): + return await run_in_threadpool(analyze_file_content, filename) + +@app.get("/sample.png") +async def get_sample_img(): + return FileResponse("sample.png") diff --git a/server_test.py b/server_test.py new file mode 100644 index 0000000..bc0d762 --- /dev/null +++ b/server_test.py @@ -0,0 +1,190 @@ +import os +import sys +import asyncio +import pymysql +from fastapi import FastAPI, Request +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import StreamingResponse, FileResponse +from fastapi.staticfiles import StaticFiles +from fastapi.templating import Jinja2Templates + +from analyze import analyze_file_content +from crawler_service_test import run_crawler_service, crawl_stop_event +from schemas import AuthRequest, InquiryReplyRequest +from inquiry_service import InquiryService +from project_service import ProjectService +from analysis_service import AnalysisService + +# --- ํ™˜๊ฒฝ ์„ค์ • --- +os.environ["PYTHONIOENCODING"] = "utf-8" +TESSDATA_PREFIX = os.getenv("TESSDATA_PREFIX", r"C:\Users\User\AppData\Local\Programs\Tesseract-OCR\tessdata") +os.environ["TESSDATA_PREFIX"] = TESSDATA_PREFIX + +app = FastAPI(title="Project Master Overseas API (TEST MODE)") +templates = Jinja2Templates(directory="templates") + +# ์ •์  ํŒŒ์ผ ๋งˆ์šดํŠธ +app.mount("/style", StaticFiles(directory="style"), name="style") +app.mount("/js", StaticFiles(directory="js"), name="js") +app.mount("/sample_files", StaticFiles(directory="sample"), name="sample_files") + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=False, + allow_methods=["*"], + allow_headers=["*"], +) + +# --- ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜ --- +def get_db_connection(): + """MySQL ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค(TEST) ์—ฐ๊ฒฐ์„ ๋ฐ˜ํ™˜""" + return pymysql.connect( + host=os.getenv('DB_HOST', 'localhost'), + user=os.getenv('DB_USER', 'root'), + password=os.getenv('DB_PASSWORD', '45278434'), + database='PM_proto_test', # ํ…Œ์ŠคํŠธ์šฉ DB๋กœ ๊ณ ์ • + charset='utf8mb4', + cursorclass=pymysql.cursors.DictCursor + ) + +async def run_in_threadpool(func, *args): + """๋™๊ธฐ ํ•จ์ˆ˜๋ฅผ ๋น„์ฐจ๋‹จ ๋ฐฉ์‹์œผ๋กœ ์‹คํ–‰""" + loop = asyncio.get_event_loop() + return await loop.run_in_executor(None, func, *args) + +# --- HTML ๋ผ์šฐํŒ… --- +@app.get("/") +async def root(request: Request): + return templates.TemplateResponse("index.html", {"request": request}) + +@app.get("/dashboard") +@app.get("/dashboard_test") +async def get_dashboard_test(request: Request): + # ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์—์„œ๋Š” dashboard_test.html์„ ์šฐ์„ ์ ์œผ๋กœ ๋ฐ˜ํ™˜ + return templates.TemplateResponse("dashboard_test.html", {"request": request}) + +@app.get("/mailTest") +async def get_mail_test(request: Request): + return templates.TemplateResponse("mailTest.html", {"request": request}) + +@app.get("/inquiries") +async def get_inquiries_page(request: Request): + return templates.TemplateResponse("inquiries.html", {"request": request}) + +@app.get("/analysis") +@app.get("/analysis_test") +async def get_analysis_test(request: Request): + # ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์—์„œ๋Š” analysis_test.html์„ ์šฐ์„ ์ ์œผ๋กœ ๋ฐ˜ํ™˜ + return templates.TemplateResponse("analysis_test.html", {"request": request}) + +# --- ๋ฌธ์˜์‚ฌํ•ญ API --- +@app.get("/api/inquiries") +async def get_inquiries(pm_type: str = None, category: str = None, status: str = None, keyword: str = None): + try: + with get_db_connection() as conn: + with conn.cursor() as cursor: + return InquiryService.get_inquiries_logic(cursor, pm_type, category, status, keyword) + except Exception as e: + return {"error": str(e)} + +@app.get("/api/inquiries/{id}") +async def get_inquiry_detail(id: int): + try: + with get_db_connection() as conn: + with conn.cursor() as cursor: + return InquiryService.get_inquiry_detail_logic(cursor, id) + except Exception as e: + return {"error": str(e)} + +@app.post("/api/inquiries/{id}/reply") +async def update_inquiry_reply(id: int, req: InquiryReplyRequest): + try: + with get_db_connection() as conn: + with conn.cursor() as cursor: + return InquiryService.update_inquiry_reply_logic(cursor, conn, id, req) + except Exception as e: + return {"error": str(e)} + +@app.delete("/api/inquiries/{id}/reply") +async def delete_inquiry_reply(id: int): + try: + with get_db_connection() as conn: + with conn.cursor() as cursor: + return InquiryService.delete_inquiry_reply_logic(cursor, conn, id) + except Exception as e: + return {"error": str(e)} + +# --- ํ”„๋กœ์ ํŠธ ๋ฐ ํžˆ์Šคํ† ๋ฆฌ API --- +@app.get("/available-dates") +async def get_available_dates(): + try: + with get_db_connection() as conn: + with conn.cursor() as cursor: + return ProjectService.get_available_dates_logic(cursor) + except Exception as e: + return {"error": str(e)} + +@app.get("/project-data") +async def get_project_data(date: str = None): + try: + with get_db_connection() as conn: + with conn.cursor() as cursor: + return ProjectService.get_project_data_logic(cursor, date) + except Exception as e: + return {"error": str(e)} + +# --- ๋ถ„์„ API (AnalysisService ์—ฐ๋™) --- +@app.get("/project-activity") +async def get_project_activity(date: str = None): + try: + with get_db_connection() as conn: + with conn.cursor() as cursor: + return AnalysisService.get_project_activity_logic(cursor, date) + except Exception as e: + return {"error": str(e)} + +@app.get("/api/analysis/p-war") +async def get_p_war_analysis(): + try: + with get_db_connection() as conn: + with conn.cursor() as cursor: + return AnalysisService.get_p_zsr_analysis_logic(cursor) + except Exception as e: + return {"error": str(e)} + +# --- ์ˆ˜์ง‘ ๋ฐ ๋™๊ธฐํ™” API --- +@app.post("/auth/crawl") +async def auth_crawl(req: AuthRequest): + if req.user_id == os.getenv("PM_USER_ID") and req.password == os.getenv("PM_PASSWORD"): + return {"success": True} + return {"success": False, "message": "ํฌ๋กค๋ง์„ ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."} + +@app.get("/sync") +async def sync_data(): + return StreamingResponse(run_crawler_service(), media_type="text_event-stream") + +@app.get("/stop-sync") +async def stop_sync(): + crawl_stop_event.set() + return {"success": True} + +# --- ํŒŒ์ผ ๋ฐ ์ฒจ๋ถ€ํŒŒ์ผ API --- +@app.get("/attachments") +async def get_attachments(): + path = "sample" + if not os.path.exists(path): os.makedirs(path) + return [{"name": f, "size": f"{os.path.getsize(os.path.join(path, f))/1024:.1f} KB"} + for f in os.listdir(path) if os.path.isfile(os.path.join(path, f))] + +@app.get("/analyze-file") +async def analyze_file(filename: str): + return await run_in_threadpool(analyze_file_content, filename) + +@app.get("/sample.png") +async def get_sample_img(): + return FileResponse("sample.png") + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8001) diff --git a/sql_queries.py b/sql_queries.py index fd61b1f..b74c0d1 100644 --- a/sql_queries.py +++ b/sql_queries.py @@ -1,67 +1,82 @@ -class InquiryQueries: - """๋ฌธ์˜์‚ฌํ•ญ(Inquiries) ํŽ˜์ด์ง€ ๊ด€๋ จ ์ฟผ๋ฆฌ""" - # ํ•„ํ„ฐ๋ง์„ ์œ„ํ•œ ๊ธฐ๋ณธ ์ฟผ๋ฆฌ (WHERE 1=1 ํฌํ•จ) - SELECT_BASE = "SELECT * FROM inquiries WHERE 1=1" - ORDER_BY_DESC = "ORDER BY no DESC" - - # ์ƒ์„ธ ์กฐํšŒ - SELECT_BY_ID = "SELECT * FROM inquiries WHERE id = %s" - - # ๋‹ต๋ณ€ ์—…๋ฐ์ดํŠธ (handled_date ํฌํ•จ) - UPDATE_REPLY = """ - UPDATE inquiries - SET reply = %s, status = %s, handler = %s, handled_date = %s - WHERE id = %s - """ - - # ๋‹ต๋ณ€ ์‚ญ์ œ (์ดˆ๊ธฐํ™”) - DELETE_REPLY = """ - UPDATE inquiries - SET reply = '', status = '๋ฏธํ™•์ธ', handled_date = '' - WHERE id = %s - """ - -class DashboardQueries: - """๋Œ€์‹œ๋ณด๋“œ(Dashboard) ๋ฐ ํ”„๋กœ์ ํŠธ ํ˜„ํ™ฉ ๊ด€๋ จ ์ฟผ๋ฆฌ""" - # ๊ฐ€์šฉ ๋‚ ์งœ ๋ชฉ๋ก ์กฐํšŒ - GET_AVAILABLE_DATES = "SELECT DISTINCT crawl_date FROM projects_history ORDER BY crawl_date DESC" - - # ์ตœ์‹  ์ˆ˜์ง‘ ๋‚ ์งœ ์กฐํšŒ - GET_LAST_CRAWL_DATE = "SELECT MAX(crawl_date) as last_date FROM projects_history" - - # ํŠน์ • ๋‚ ์งœ ํ”„๋กœ์ ํŠธ ๋ฐ์ดํ„ฐ JOIN ์กฐํšŒ - GET_PROJECT_LIST = """ - SELECT m.project_nm, m.short_nm, m.department, m.master, - h.recent_log, h.file_count, m.continent, m.country - FROM projects_master m - LEFT JOIN projects_history h ON m.project_id = h.project_id AND h.crawl_date = %s - ORDER BY m.project_id ASC - """ - - # ํ™œ์„ฑ๋„ ๋ถ„์„์„ ์œ„ํ•œ ํ”„๋กœ์ ํŠธ ๋ชฉ๋ก ์กฐํšŒ - GET_PROJECT_LIST_FOR_ANALYSIS = """ - 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 - """ - -class CrawlerQueries: - """ํฌ๋กค๋Ÿฌ(Crawler) ๋ฐ์ดํ„ฐ ๋™๊ธฐํ™” ๊ด€๋ จ ์ฟผ๋ฆฌ""" - # ๋งˆ์Šคํ„ฐ ์ •๋ณด UPSERT (INSERT OR UPDATE) - UPSERT_MASTER = """ - INSERT INTO projects_master (project_id, project_nm, short_nm, master, continent, country) - VALUES (%s, %s, %s, %s, %s, %s) - ON DUPLICATE KEY UPDATE - project_nm = VALUES(project_nm), short_nm = VALUES(short_nm), - master = VALUES(master), continent = VALUES(continent), country = VALUES(country) - """ - - # ๋ถ€์„œ ์ •๋ณด ์—…๋ฐ์ดํŠธ - UPDATE_DEPARTMENT = "UPDATE projects_master SET department = %s WHERE project_id = %s" - - # ํžˆ์Šคํ† ๋ฆฌ(๋กœ๊ทธ/ํŒŒ์ผ์ˆ˜) ์ €์žฅ - UPSERT_HISTORY = """ - INSERT INTO projects_history (project_id, crawl_date, recent_log, file_count) - VALUES (%s, CURRENT_DATE(), %s, %s) - ON DUPLICATE KEY UPDATE recent_log=VALUES(recent_log), file_count=VALUES(file_count) - """ +class InquiryQueries: + """๋ฌธ์˜์‚ฌํ•ญ(Inquiries) ํŽ˜์ด์ง€ ๊ด€๋ จ ์ฟผ๋ฆฌ""" + # ํ•„ํ„ฐ๋ง์„ ์œ„ํ•œ ๊ธฐ๋ณธ ์ฟผ๋ฆฌ (WHERE 1=1 ํฌํ•จ) + SELECT_BASE = "SELECT * FROM inquiries WHERE 1=1" + ORDER_BY_DESC = "ORDER BY no DESC" + + # ์ƒ์„ธ ์กฐํšŒ + SELECT_BY_ID = "SELECT * FROM inquiries WHERE id = %s" + + # ๋‹ต๋ณ€ ์—…๋ฐ์ดํŠธ (handled_date ํฌํ•จ) + UPDATE_REPLY = """ + UPDATE inquiries + SET reply = %s, status = %s, handler = %s, handled_date = %s + WHERE id = %s + """ + + # ๋‹ต๋ณ€ ์‚ญ์ œ (์ดˆ๊ธฐํ™”) + DELETE_REPLY = """ + UPDATE inquiries + SET reply = '', status = '๋ฏธํ™•์ธ', handled_date = '' + WHERE id = %s + """ + +class DashboardQueries: + """๋Œ€์‹œ๋ณด๋“œ(Dashboard) ๋ฐ ํ”„๋กœ์ ํŠธ ํ˜„ํ™ฉ ๊ด€๋ จ ์ฟผ๋ฆฌ""" + # ๊ฐ€์šฉ ๋‚ ์งœ ๋ชฉ๋ก ์กฐํšŒ + GET_AVAILABLE_DATES = "SELECT DISTINCT crawl_date FROM projects_history ORDER BY crawl_date DESC" + + # ์ตœ์‹  ์ˆ˜์ง‘ ๋‚ ์งœ ์กฐํšŒ + GET_LAST_CRAWL_DATE = "SELECT MAX(crawl_date) as last_date FROM projects_history" + + # ํŠน์ • ๋‚ ์งœ(๋˜๋Š” ๊ทธ ์ดํ•˜ ์ตœ์‹ ) ํ”„๋กœ์ ํŠธ ๋ฐ์ดํ„ฐ JOIN ์กฐํšŒ + GET_PROJECT_LIST = """ + SELECT m.project_nm, m.short_nm, m.department, m.master, + h.recent_log, h.file_count, m.continent, m.country + FROM projects_master m + LEFT JOIN projects_history h ON h.project_id = m.project_id AND h.crawl_date = ( + SELECT MAX(crawl_date) + FROM projects_history + WHERE project_id = m.project_id AND crawl_date <= %s + ) + ORDER BY m.project_id ASC + """ + + # ํ™œ์„ฑ๋„ ๋ถ„์„์„ ์œ„ํ•œ ํ”„๋กœ์ ํŠธ ๋ชฉ๋ก ์กฐํšŒ (ํŠน์ • ๋‚ ์งœ ์ดํ•˜ ์ตœ์‹  ๋ฐ์ดํ„ฐ ๊ธฐ์ค€) + GET_PROJECT_LIST_FOR_ANALYSIS = """ + 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 h.project_id = m.project_id AND h.crawl_date = ( + SELECT MAX(crawl_date) + FROM projects_history + WHERE project_id = m.project_id AND crawl_date <= %s + ) + """ + +class CrawlerQueries: + """ํฌ๋กค๋Ÿฌ(Crawler) ๋ฐ์ดํ„ฐ ๋™๊ธฐํ™” ๊ด€๋ จ ์ฟผ๋ฆฌ""" + # ๋งˆ์Šคํ„ฐ ์ •๋ณด UPSERT (INSERT OR UPDATE) + UPSERT_MASTER = """ + INSERT INTO projects_master (project_id, project_nm, short_nm, master, continent, country) + VALUES (%s, %s, %s, %s, %s, %s) + ON DUPLICATE KEY UPDATE + project_nm = VALUES(project_nm), short_nm = VALUES(short_nm), + master = VALUES(master), continent = VALUES(continent), country = VALUES(country) + """ + + # ๋ถ€์„œ ์ •๋ณด ์—…๋ฐ์ดํŠธ + UPDATE_DEPARTMENT = "UPDATE projects_master SET department = %s WHERE project_id = %s" + + # ํžˆ์Šคํ† ๋ฆฌ(๋กœ๊ทธ/ํŒŒ์ผ์ˆ˜) ์ €์žฅ (๋‚ ์งœ ์ง€์ •ํ˜•) + UPSERT_HISTORY_WITH_DATE = """ + INSERT INTO projects_history (project_id, crawl_date, recent_log, file_count) + VALUES (%s, %s, %s, %s) + ON DUPLICATE KEY UPDATE recent_log=VALUES(recent_log), file_count=VALUES(file_count) + """ + + # ํžˆ์Šคํ† ๋ฆฌ(๋กœ๊ทธ/ํŒŒ์ผ์ˆ˜) ์ €์žฅ (๊ธฐ๋ณธํ˜• - ์˜ค๋Š˜ ๋‚ ์งœ) + UPSERT_HISTORY = """ + INSERT INTO projects_history (project_id, crawl_date, recent_log, file_count) + VALUES (%s, CURDATE(), %s, %s) + ON DUPLICATE KEY UPDATE recent_log=VALUES(recent_log), file_count=VALUES(file_count) + """ diff --git a/style/analysis.css b/style/analysis.css index e9d9690..e8e28a1 100644 --- a/style/analysis.css +++ b/style/analysis.css @@ -1,233 +1,233 @@ -/* ========================================================================== - Project Master Analysis - Specific Styles - (Inherits base styles from common.css) - ========================================================================== */ - -.analysis-content { - padding: 24px; - max-width: 1400px; - margin: var(--topbar-h, 36px) auto 0; -} - -/* AI Badge & Header */ -.ai-badge { - background: #6366f1; - color: white; - padding: 2px 10px; - border-radius: 20px; - font-size: 11px; - font-weight: 800; - display: inline-block; - margin-bottom: 8px; -} - -.analysis-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 24px; - padding: 10px 0; - border-bottom: 1px solid var(--border-color); -} - -.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 2.2fr; - gap: 16px; - margin-bottom: 24px; -} - -.dl-model-info, .soi-deep-dive { - background: white; - border-radius: var(--radius-xl); - border: 1px solid var(--border-color); - padding: 20px; - box-shadow: var(--box-shadow); -} - -.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-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; } - -.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: 1fr 1.8fr; - gap: 20px; - margin-bottom: 24px; -} - -.chart-container-box { - background: white; - border-radius: var(--radius-xl); - padding: 20px; - 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: 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 */ -.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; -} - -.p-war-table { - width: 100%; - border-collapse: separate; - border-spacing: 0; - table-layout: fixed; /* ์ปฌ๋Ÿผ ๋„ˆ๋น„ ๊ณ ์ • */ -} - -.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: #fafafa; } -.detail-row.active { display: table-row; } -.detail-container { padding: 20px 24px; } - -.formula-explanation-card { - background: white; - border-radius: var(--radius-lg); - padding: 24px; - border: 1px solid var(--border-color); - box-shadow: var(--box-shadow); -} - -.formula-header { font-size: 13px; font-weight: 700; color: #6366f1; margin-bottom: 15px; } - -/* 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-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; } - -.final-result-area { margin-top: 20px; padding-top: 15px; 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; - justify-content: flex-end; -} - -/* 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; } - -.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; } - -.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; } +/* ========================================================================== + Project Master Analysis - Specific Styles + (Inherits base styles from common.css) + ========================================================================== */ + +.analysis-content { + padding: 24px; + max-width: 1400px; + margin: var(--topbar-h, 36px) auto 0; +} + +/* AI Badge & Header */ +.ai-badge { + background: #6366f1; + color: white; + padding: 2px 10px; + border-radius: 20px; + font-size: 11px; + font-weight: 800; + display: inline-block; + margin-bottom: 8px; +} + +.analysis-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 24px; + padding: 10px 0; + border-bottom: 1px solid var(--border-color); +} + +.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 2.2fr; + gap: 16px; + margin-bottom: 24px; +} + +.dl-model-info, .soi-deep-dive { + background: white; + border-radius: var(--radius-xl); + border: 1px solid var(--border-color); + padding: 20px; + box-shadow: var(--box-shadow); +} + +.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-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; } + +.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: 1fr 1.8fr; + gap: 20px; + margin-bottom: 24px; +} + +.chart-container-box { + background: white; + border-radius: var(--radius-xl); + padding: 20px; + 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: 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 */ +.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; +} + +.p-war-table { + width: 100%; + border-collapse: separate; + border-spacing: 0; + table-layout: fixed; /* ์ปฌ๋Ÿผ ๋„ˆ๋น„ ๊ณ ์ • */ +} + +.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: #fafafa; } +.detail-row.active { display: table-row; } +.detail-container { padding: 20px 24px; } + +.formula-explanation-card { + background: white; + border-radius: var(--radius-lg); + padding: 24px; + border: 1px solid var(--border-color); + box-shadow: var(--box-shadow); +} + +.formula-header { font-size: 13px; font-weight: 700; color: #6366f1; margin-bottom: 15px; } + +/* 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-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; } + +.final-result-area { margin-top: 20px; padding-top: 15px; 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; + justify-content: flex-end; +} + +/* 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; } + +.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; } + +.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/style/common.css b/style/common.css index 00d5608..2ae2fe0 100644 --- a/style/common.css +++ b/style/common.css @@ -1,170 +1,170 @@ -:root { - /* 1. Core Colors */ - --primary-color: #1E5149; - --primary-hover: #163b36; - --primary-lv-0: #f0f7f4; - --primary-lv-1: #e1eee9; - --primary-lv-8: #193833; - - --bg-default: #FFFFFF; - --bg-muted: #F9FAFB; - --hover-bg: #F7FAFC; - - --text-main: #111827; - --text-sub: #6B7280; - --error-color: #F21D0D; - --border-color: #E2E8F0; - - /* 2. Gradients */ - --header-gradient: linear-gradient(90deg, #193833 0%, #1e5149 100%); - --ai-gradient: linear-gradient(180deg, #da8cf1 0%, #8bb1f2 100%); - - /* 3. Spacing & Radius */ - --space-xs: 4px; - --space-sm: 8px; - --space-md: 16px; - --space-lg: 32px; - --radius-sm: 4px; - --radius-md: 6px; - --radius-lg: 8px; - --radius-xl: 12px; - - /* 4. Typography */ - --fz-h1: 20px; - --fz-h2: 16px; - --fz-body: 13px; - --fz-small: 11px; - - /* 5. Shadows */ - --box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05); - --box-shadow-lg: 0 10px 20px rgba(0, 0, 0, 0.1); - --box-shadow-modal: 0 25px 50px -12px rgba(0,0,0,0.5); - - /* 6. Layout Constants */ - --topbar-h: 36px; -} - -/* Base Reset */ -* { margin: 0; padding: 0; box-sizing: border-box; } -body { - font-family: 'Pretendard', -apple-system, sans-serif; - font-size: var(--fz-body); - color: var(--text-main); - background: var(--bg-default); - min-height: 100vh; -} - -/* Page Specific Overrides */ -body:has(.mail-wrapper) { height: 100vh; overflow: hidden; } - -input, select, textarea, button { font-family: inherit; } -a { text-decoration: none; color: inherit; } -button { cursor: pointer; border: none; transition: all 0.2s ease; } - -/* Utilities: Layout & Text */ -.flex-center { display: flex; align-items: center; justify-content: center; } -.flex-between { display: flex; align-items: center; justify-content: space-between; } -.flex-column { display: flex; flex-direction: column; } -.text-truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.w-full { width: 100%; } -.pointer { cursor: pointer; } - -/* Components: Topbar */ -.topbar { - width: 100%; - background: var(--header-gradient); - color: #fff; - padding: 0 var(--space-lg); - position: fixed; - top: 0; - height: var(--topbar-h); - display: flex; - align-items: center; - z-index: 2000; -} -.topbar-header h2 { font-size: 16px; color: white; margin-right: 60px; font-weight: 700; } -.nav-list { display: flex; list-style: none; gap: var(--space-sm); } -.nav-item { - padding: 4px 12px; border-radius: var(--radius-sm); - color: rgba(255, 255, 255, 0.8); font-size: 14px; - cursor: pointer; -} -.nav-item:hover { background: var(--primary-lv-8); color: #fff; } -.nav-item.active { background: var(--primary-lv-0); color: var(--primary-color) !important; font-weight: 700; } - -/* Components: Modals */ -.modal-overlay { - display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; - background: rgba(0, 0, 0, 0.6); backdrop-filter: blur(4px); - z-index: 3000; justify-content: center; align-items: center; -} -.modal-content { - background: white; padding: 24px; border-radius: var(--radius-xl); - width: 90%; max-width: 500px; box-shadow: var(--box-shadow-modal); -} -.modal-header { - display: flex; justify-content: space-between; align-items: center; - margin-bottom: 20px; border-bottom: 1px solid var(--border-color); padding-bottom: 12px; -} -.modal-header h3 { margin: 0; font-size: 16px; color: var(--primary-color); font-weight: 700; } -.modal-close { cursor: pointer; font-size: 24px; color: var(--text-sub); line-height: 1; transition: 0.2s; } -.modal-close:hover { color: var(--text-main); } - -/* Components: Data Tables */ -.data-table { width: 100%; border-collapse: collapse; font-size: 12px; background: #fff; } -.data-table th, .data-table td { padding: 12px 10px; border-bottom: 1px solid var(--border-color); text-align: left; } -.data-table th { color: var(--text-sub); font-weight: 700; background: var(--bg-muted); font-size: 11px; text-transform: uppercase; } -.data-table tr:hover { background: var(--hover-bg); } - -/* Components: Standard Buttons */ -.btn { - display: inline-flex; align-items: center; justify-content: center; gap: 8px; - padding: 8px 16px; border-radius: var(--radius-lg); font-weight: 600; font-size: 13px; - border: none; cursor: pointer; transition: all 0.2s ease; -} -.btn-primary { background: var(--primary-color); color: #fff; } -.btn-primary:hover { background: var(--primary-hover); transform: translateY(-1px); } -.btn-secondary { background: #f1f3f5; color: #495057; } -.btn-secondary:hover { background: #e9ecef; } -.btn-danger { background: #fee2e2; color: #dc2626; } -.btn-danger:hover { background: #fecaca; } - -/* Compatibility Utils */ -._button-xsmall { - display: inline-flex; align-items: center; justify-content: center; - padding: 4px 10px; font-size: 11px; font-weight: 600; border-radius: 4px; border: 1px solid var(--border-color); - background: #fff; color: var(--text-main); cursor: pointer; transition: 0.2s; -} -._button-xsmall:hover { background: var(--bg-muted); border-color: var(--primary-color); color: var(--primary-color); } - -._button-small { - display: inline-flex; align-items: center; justify-content: center; - padding: 6px 14px; font-size: 12px; background: var(--primary-color); color: #fff; border-radius: 6px; border: none; cursor: pointer; -} -._button-medium { - display: inline-flex; align-items: center; justify-content: center; - padding: 10px 20px; background: var(--primary-color); color: #fff; border-radius: 6px; font-weight: 700; border: none; cursor: pointer; -} -.sync-btn { background: var(--primary-color); color: #fff; padding: 8px 16px; border-radius: 8px; font-size: 13px; font-weight: 600; } - -/* Badges & Status Colors */ -.badge { - padding: 2px 8px; border-radius: 4px; font-size: 10px; font-weight: 700; - display: inline-block; background: var(--primary-lv-1); color: var(--primary-color); -} - -.status-complete { background: #e8f5e9; color: #2e7d32; } -.status-working { background: #fff8e1; color: #FFBF00; } -.status-checking { background: #e3f2fd; color: #1565c0; } -.status-pending { background: #f5f5f5; color: #757575; } -.status-error { background: #fee9e7; } - -.warning-text { color: #FFBF00; font-weight: 600; } -.error-text { color: #F21D0D !important; font-weight: 700; } - -/* Spinner */ -.spinner { - display: none; width: 16px; height: 16px; border: 2px solid rgba(255, 255, 255, .3); - border-radius: 50%; border-top-color: #fff; animation: spin 1s ease-in-out infinite; -} -@keyframes spin { to { transform: rotate(360deg); } } +:root { + /* 1. Core Colors */ + --primary-color: #1E5149; + --primary-hover: #163b36; + --primary-lv-0: #f0f7f4; + --primary-lv-1: #e1eee9; + --primary-lv-8: #193833; + + --bg-default: #FFFFFF; + --bg-muted: #F9FAFB; + --hover-bg: #F7FAFC; + + --text-main: #111827; + --text-sub: #6B7280; + --error-color: #F21D0D; + --border-color: #E2E8F0; + + /* 2. Gradients */ + --header-gradient: linear-gradient(90deg, #193833 0%, #1e5149 100%); + --ai-gradient: linear-gradient(180deg, #da8cf1 0%, #8bb1f2 100%); + + /* 3. Spacing & Radius */ + --space-xs: 4px; + --space-sm: 8px; + --space-md: 16px; + --space-lg: 32px; + --radius-sm: 4px; + --radius-md: 6px; + --radius-lg: 8px; + --radius-xl: 12px; + + /* 4. Typography */ + --fz-h1: 20px; + --fz-h2: 16px; + --fz-body: 13px; + --fz-small: 11px; + + /* 5. Shadows */ + --box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05); + --box-shadow-lg: 0 10px 20px rgba(0, 0, 0, 0.1); + --box-shadow-modal: 0 25px 50px -12px rgba(0,0,0,0.5); + + /* 6. Layout Constants */ + --topbar-h: 36px; +} + +/* Base Reset */ +* { margin: 0; padding: 0; box-sizing: border-box; } +body { + font-family: 'Pretendard', -apple-system, sans-serif; + font-size: var(--fz-body); + color: var(--text-main); + background: var(--bg-default); + min-height: 100vh; +} + +/* Page Specific Overrides */ +body:has(.mail-wrapper) { height: 100vh; overflow: hidden; } + +input, select, textarea, button { font-family: inherit; } +a { text-decoration: none; color: inherit; } +button { cursor: pointer; border: none; transition: all 0.2s ease; } + +/* Utilities: Layout & Text */ +.flex-center { display: flex; align-items: center; justify-content: center; } +.flex-between { display: flex; align-items: center; justify-content: space-between; } +.flex-column { display: flex; flex-direction: column; } +.text-truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.w-full { width: 100%; } +.pointer { cursor: pointer; } + +/* Components: Topbar */ +.topbar { + width: 100%; + background: var(--header-gradient); + color: #fff; + padding: 0 var(--space-lg); + position: fixed; + top: 0; + height: var(--topbar-h); + display: flex; + align-items: center; + z-index: 2000; +} +.topbar-header h2 { font-size: 16px; color: white; margin-right: 60px; font-weight: 700; } +.nav-list { display: flex; list-style: none; gap: var(--space-sm); } +.nav-item { + padding: 4px 12px; border-radius: var(--radius-sm); + color: rgba(255, 255, 255, 0.8); font-size: 14px; + cursor: pointer; +} +.nav-item:hover { background: var(--primary-lv-8); color: #fff; } +.nav-item.active { background: var(--primary-lv-0); color: var(--primary-color) !important; font-weight: 700; } + +/* Components: Modals */ +.modal-overlay { + display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; + background: rgba(0, 0, 0, 0.6); backdrop-filter: blur(4px); + z-index: 3000; justify-content: center; align-items: center; +} +.modal-content { + background: white; padding: 24px; border-radius: var(--radius-xl); + width: 90%; max-width: 500px; box-shadow: var(--box-shadow-modal); +} +.modal-header { + display: flex; justify-content: space-between; align-items: center; + margin-bottom: 20px; border-bottom: 1px solid var(--border-color); padding-bottom: 12px; +} +.modal-header h3 { margin: 0; font-size: 16px; color: var(--primary-color); font-weight: 700; } +.modal-close { cursor: pointer; font-size: 24px; color: var(--text-sub); line-height: 1; transition: 0.2s; } +.modal-close:hover { color: var(--text-main); } + +/* Components: Data Tables */ +.data-table { width: 100%; border-collapse: collapse; font-size: 12px; background: #fff; } +.data-table th, .data-table td { padding: 12px 10px; border-bottom: 1px solid var(--border-color); text-align: left; } +.data-table th { color: var(--text-sub); font-weight: 700; background: var(--bg-muted); font-size: 11px; text-transform: uppercase; } +.data-table tr:hover { background: var(--hover-bg); } + +/* Components: Standard Buttons */ +.btn { + display: inline-flex; align-items: center; justify-content: center; gap: 8px; + padding: 8px 16px; border-radius: var(--radius-lg); font-weight: 600; font-size: 13px; + border: none; cursor: pointer; transition: all 0.2s ease; +} +.btn-primary { background: var(--primary-color); color: #fff; } +.btn-primary:hover { background: var(--primary-hover); transform: translateY(-1px); } +.btn-secondary { background: #f1f3f5; color: #495057; } +.btn-secondary:hover { background: #e9ecef; } +.btn-danger { background: #fee2e2; color: #dc2626; } +.btn-danger:hover { background: #fecaca; } + +/* Compatibility Utils */ +._button-xsmall { + display: inline-flex; align-items: center; justify-content: center; + padding: 4px 10px; font-size: 11px; font-weight: 600; border-radius: 4px; border: 1px solid var(--border-color); + background: #fff; color: var(--text-main); cursor: pointer; transition: 0.2s; +} +._button-xsmall:hover { background: var(--bg-muted); border-color: var(--primary-color); color: var(--primary-color); } + +._button-small { + display: inline-flex; align-items: center; justify-content: center; + padding: 6px 14px; font-size: 12px; background: var(--primary-color); color: #fff; border-radius: 6px; border: none; cursor: pointer; +} +._button-medium { + display: inline-flex; align-items: center; justify-content: center; + padding: 10px 20px; background: var(--primary-color); color: #fff; border-radius: 6px; font-weight: 700; border: none; cursor: pointer; +} +.sync-btn { background: var(--primary-color); color: #fff; padding: 8px 16px; border-radius: 8px; font-size: 13px; font-weight: 600; } + +/* Badges & Status Colors */ +.badge { + padding: 2px 8px; border-radius: 4px; font-size: 10px; font-weight: 700; + display: inline-block; background: var(--primary-lv-1); color: var(--primary-color); +} + +.status-complete { background: #e8f5e9; color: #2e7d32; } +.status-working { background: #fff8e1; color: #FFBF00; } +.status-checking { background: #e3f2fd; color: #1565c0; } +.status-pending { background: #f5f5f5; color: #757575; } +.status-error { background: #fee9e7; } + +.warning-text { color: #FFBF00; font-weight: 600; } +.error-text { color: #F21D0D !important; font-weight: 700; } + +/* Spinner */ +.spinner { + display: none; width: 16px; height: 16px; border: 2px solid rgba(255, 255, 255, .3); + border-radius: 50%; border-top-color: #fff; animation: spin 1s ease-in-out infinite; +} +@keyframes spin { to { transform: rotate(360deg); } } diff --git a/style/dashboard.css b/style/dashboard.css index dd22710..ea5748d 100644 --- a/style/dashboard.css +++ b/style/dashboard.css @@ -1,123 +1,123 @@ -/* Dashboard Constants */ -:root { - --header-h: 56px; - --activity-h: 110px; - --fixed-total-h: calc(var(--topbar-h) + var(--header-h) + var(--activity-h)); -} - -/* 1. Portal (Index) */ -.portal-container { - display: flex; flex-direction: column; align-items: center; justify-content: center; - height: calc(100vh - var(--topbar-h)); background: var(--bg-muted); padding: var(--space-lg); margin-top: var(--topbar-h); -} -.portal-header { text-align: center; margin-bottom: 50px; } -.portal-header h1 { font-size: 28px; color: var(--primary-color); margin-bottom: 10px; font-weight: 800; } -.portal-header p { color: var(--text-sub); font-size: 15px; } - -.button-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 30px; width: 100%; max-width: 800px; } -.portal-card { - background: #fff; border: 1px solid var(--border-color); border-radius: 12px; padding: 40px; - text-align: center; transition: all 0.3s ease; width: 100%; box-shadow: var(--box-shadow); - display: flex; flex-direction: column; align-items: center; gap: 20px; -} -.portal-card:hover { transform: translateY(-8px); border-color: var(--primary-color); box-shadow: var(--box-shadow-lg); } -.portal-card i { font-size: 48px; color: var(--primary-color); } -.portal-card h3 { font-size: 20px; color: var(--text-main); margin: 0; } -.portal-card p { font-size: 14px; color: var(--text-sub); margin: 0; } - -/* 2. Dashboard Header & Activity */ -header { - position: fixed; top: var(--topbar-h); left: 0; right: 0; z-index: 1001; - background: #fff; height: var(--header-h); display: flex; justify-content: space-between; align-items: center; - padding: 0 var(--space-lg); border-bottom: 1px solid #f5f5f5; -} - -.activity-dashboard-wrapper { - position: fixed; top: calc(var(--topbar-h) + var(--header-h)); left: 0; right: 0; z-index: 1000; - background: #fff; height: var(--activity-h); border-bottom: 1px solid var(--border-color); box-shadow: 0 4px 6px rgba(0,0,0,0.03); -} - -.activity-dashboard { max-width: 1200px; margin: 0 auto; height: 100%; display: flex; gap: 15px; padding: 10px 32px 20px; } -.activity-card { - flex: 1; padding: 12px 15px; border-radius: var(--radius-lg); cursor: pointer; - display: flex; flex-direction: column; justify-content: center; gap: 2px; border-left: 5px solid transparent; -} -.activity-card:hover { transform: translateY(-2px); box-shadow: var(--box-shadow); } -.activity-card.active { background: #e8f5e9; } -.activity-card.warning { background: #fff8e1; } -.activity-card.stale { background: #ffebee; } -.activity-card.unknown { background: #f5f5f5; } -.activity-card .label { font-size: 11px; font-weight: 600; opacity: 0.7; } -.activity-card .count { font-size: 20px; font-weight: 800; } - -.main-content { margin-top: var(--fixed-total-h); padding: var(--space-lg); max-width: 1400px; margin-left: auto; margin-right: auto; } - -/* 3. Log Console */ -.log-console { - position: sticky; top: var(--fixed-total-h); z-index: 999; - background: #000; color: #0f0; font-family: 'Consolas', monospace; padding: 15px; margin-bottom: 20px; - border-radius: 4px; max-height: 250px; overflow-y: auto; font-size: 12px; line-height: 1.5; box-shadow: 0 10px 20px rgba(0,0,0,0.2); -} -.log-console-header { color: #fff; border-bottom: 1px solid #333; margin-bottom: 10px; padding-bottom: 5px; font-weight: bold; } - -/* 4. Auth Modal (Page Specific) */ -.auth-modal-content { - background: #fff; width: 440px; border-radius: 16px; padding: 40px; text-align: center; - box-shadow: var(--box-shadow-modal); display: flex; flex-direction: column; gap: 32px; -} -.input-group { display: flex; flex-direction: column; gap: 8px; text-align: left; } -.input-group label { font-size: 12px; font-weight: 700; color: var(--text-main); } -.input-group input { - height: 48px; padding: 0 16px; border: 1px solid var(--border-color); border-radius: 8px; - font-size: 14px; background: #f9f9f9; width: 100%; -} -.input-group input:focus { border-color: var(--primary-color); background: #fff; outline: none; } - -/* 5. Accordion & Data Tables */ -.accordion-list-header { - position: sticky; top: var(--fixed-total-h); background: #fff; z-index: 900; - font-size: 11px; font-weight: 700; color: var(--text-sub); - padding: 12px 24px; border-bottom: 2px solid var(--primary-color); - display: grid; grid-template-columns: 2.5fr 1fr 1fr 0.8fr 2fr; gap: 16px; -} - -.accordion-header { - display: grid; grid-template-columns: 2.5fr 1fr 1fr 0.8fr 2fr; gap: 16px; - padding: 12px 24px; align-items: center; cursor: pointer; border-bottom: 1px solid var(--border-color); -} -.accordion-item:hover .accordion-header { background: var(--primary-lv-0); } -.accordion-item.active .accordion-header { background: var(--primary-lv-0); border-bottom: none; } - -.repo-title { font-weight: 700; color: var(--primary-color); @extend .text-truncate; } -.repo-files { text-align: center; font-weight: 600; } -.repo-log { font-size: 11px; color: var(--text-sub); @extend .text-truncate; } - -.accordion-body { display: none; padding: 24px; background: #fff; border-bottom: 1px solid var(--border-color); } -.accordion-item.active .accordion-body { display: block; } - -.detail-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 32px; } -.detail-section h4 { - font-size: 13px; margin-bottom: 12px; color: var(--text-main); - padding-left: 10px; font-weight: 700; -} - -/* Personnel & Activity Tables */ -#personnel-table th:nth-child(1) { width: 25%; } -#personnel-table th:nth-child(2) { width: 45%; } -#activity-table th:nth-child(1) { width: 20%; } -#activity-table th:nth-child(2) { width: 50%; } - -/* Location Groups */ -.continent-group, .country-group { margin-bottom: 15px; } -.continent-header, .country-header { - background: #fff; padding: 14px 20px; border: 1px solid var(--border-color); border-radius: 8px; - display: flex; justify-content: space-between; align-items: center; cursor: pointer; font-weight: 700; -} -.continent-header { background: var(--primary-color); color: white; border: none; font-size: 15px; } -.country-header { font-size: 14px; color: var(--text-main); margin-top: 8px; } -.continent-body, .country-body { display: none; padding: 10px 0 10px 15px; } -.active>.continent-body, .active>.country-body { display: block; } - -.admin-info { font-size: 12px; color: var(--text-sub); margin-left: 16px; padding: 6px 12px; background: #f8f9fa; border-radius: 4px; border: 1px solid var(--border-color); } -.admin-info strong { color: var(--primary-color); font-weight: 700; } -.base-date-info { font-size: 13px; color: var(--text-sub); background: #fdfdfd; padding: 6px 15px; border-radius: 6px; border: 1px solid var(--border-color); } +/* Dashboard Constants */ +:root { + --header-h: 56px; + --activity-h: 110px; + --fixed-total-h: calc(var(--topbar-h) + var(--header-h) + var(--activity-h)); +} + +/* 1. Portal (Index) */ +.portal-container { + display: flex; flex-direction: column; align-items: center; justify-content: center; + height: calc(100vh - var(--topbar-h)); background: var(--bg-muted); padding: var(--space-lg); margin-top: var(--topbar-h); +} +.portal-header { text-align: center; margin-bottom: 50px; } +.portal-header h1 { font-size: 28px; color: var(--primary-color); margin-bottom: 10px; font-weight: 800; } +.portal-header p { color: var(--text-sub); font-size: 15px; } + +.button-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 30px; width: 100%; max-width: 800px; } +.portal-card { + background: #fff; border: 1px solid var(--border-color); border-radius: 12px; padding: 40px; + text-align: center; transition: all 0.3s ease; width: 100%; box-shadow: var(--box-shadow); + display: flex; flex-direction: column; align-items: center; gap: 20px; +} +.portal-card:hover { transform: translateY(-8px); border-color: var(--primary-color); box-shadow: var(--box-shadow-lg); } +.portal-card i { font-size: 48px; color: var(--primary-color); } +.portal-card h3 { font-size: 20px; color: var(--text-main); margin: 0; } +.portal-card p { font-size: 14px; color: var(--text-sub); margin: 0; } + +/* 2. Dashboard Header & Activity */ +header { + position: fixed; top: var(--topbar-h); left: 0; right: 0; z-index: 1001; + background: #fff; height: var(--header-h); display: flex; justify-content: space-between; align-items: center; + padding: 0 var(--space-lg); border-bottom: 1px solid #f5f5f5; +} + +.activity-dashboard-wrapper { + position: fixed; top: calc(var(--topbar-h) + var(--header-h)); left: 0; right: 0; z-index: 1000; + background: #fff; height: var(--activity-h); border-bottom: 1px solid var(--border-color); box-shadow: 0 4px 6px rgba(0,0,0,0.03); +} + +.activity-dashboard { max-width: 1200px; margin: 0 auto; height: 100%; display: flex; gap: 15px; padding: 10px 32px 20px; } +.activity-card { + flex: 1; padding: 12px 15px; border-radius: var(--radius-lg); cursor: pointer; + display: flex; flex-direction: column; justify-content: center; gap: 2px; border-left: 5px solid transparent; +} +.activity-card:hover { transform: translateY(-2px); box-shadow: var(--box-shadow); } +.activity-card.active { background: #e8f5e9; } +.activity-card.warning { background: #fff8e1; } +.activity-card.stale { background: #ffebee; } +.activity-card.unknown { background: #f5f5f5; } +.activity-card .label { font-size: 11px; font-weight: 600; opacity: 0.7; } +.activity-card .count { font-size: 20px; font-weight: 800; } + +.main-content { margin-top: var(--fixed-total-h); padding: var(--space-lg); max-width: 1400px; margin-left: auto; margin-right: auto; } + +/* 3. Log Console */ +.log-console { + position: sticky; top: var(--fixed-total-h); z-index: 999; + background: #000; color: #0f0; font-family: 'Consolas', monospace; padding: 15px; margin-bottom: 20px; + border-radius: 4px; max-height: 250px; overflow-y: auto; font-size: 12px; line-height: 1.5; box-shadow: 0 10px 20px rgba(0,0,0,0.2); +} +.log-console-header { color: #fff; border-bottom: 1px solid #333; margin-bottom: 10px; padding-bottom: 5px; font-weight: bold; } + +/* 4. Auth Modal (Page Specific) */ +.auth-modal-content { + background: #fff; width: 440px; border-radius: 16px; padding: 40px; text-align: center; + box-shadow: var(--box-shadow-modal); display: flex; flex-direction: column; gap: 32px; +} +.input-group { display: flex; flex-direction: column; gap: 8px; text-align: left; } +.input-group label { font-size: 12px; font-weight: 700; color: var(--text-main); } +.input-group input { + height: 48px; padding: 0 16px; border: 1px solid var(--border-color); border-radius: 8px; + font-size: 14px; background: #f9f9f9; width: 100%; +} +.input-group input:focus { border-color: var(--primary-color); background: #fff; outline: none; } + +/* 5. Accordion & Data Tables */ +.accordion-list-header { + position: sticky; top: var(--fixed-total-h); background: #fff; z-index: 900; + font-size: 11px; font-weight: 700; color: var(--text-sub); + padding: 12px 24px; border-bottom: 2px solid var(--primary-color); + display: grid; grid-template-columns: 2.5fr 1fr 1fr 0.8fr 2fr; gap: 16px; +} + +.accordion-header { + display: grid; grid-template-columns: 2.5fr 1fr 1fr 0.8fr 2fr; gap: 16px; + padding: 12px 24px; align-items: center; cursor: pointer; border-bottom: 1px solid var(--border-color); +} +.accordion-item:hover .accordion-header { background: var(--primary-lv-0); } +.accordion-item.active .accordion-header { background: var(--primary-lv-0); border-bottom: none; } + +.repo-title { font-weight: 700; color: var(--primary-color); @extend .text-truncate; } +.repo-files { text-align: center; font-weight: 600; } +.repo-log { font-size: 11px; color: var(--text-sub); @extend .text-truncate; } + +.accordion-body { display: none; padding: 24px; background: #fff; border-bottom: 1px solid var(--border-color); } +.accordion-item.active .accordion-body { display: block; } + +.detail-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 32px; } +.detail-section h4 { + font-size: 13px; margin-bottom: 12px; color: var(--text-main); + padding-left: 10px; font-weight: 700; +} + +/* Personnel & Activity Tables */ +#personnel-table th:nth-child(1) { width: 25%; } +#personnel-table th:nth-child(2) { width: 45%; } +#activity-table th:nth-child(1) { width: 20%; } +#activity-table th:nth-child(2) { width: 50%; } + +/* Location Groups */ +.continent-group, .country-group { margin-bottom: 15px; } +.continent-header, .country-header { + background: #fff; padding: 14px 20px; border: 1px solid var(--border-color); border-radius: 8px; + display: flex; justify-content: space-between; align-items: center; cursor: pointer; font-weight: 700; +} +.continent-header { background: var(--primary-color); color: white; border: none; font-size: 15px; } +.country-header { font-size: 14px; color: var(--text-main); margin-top: 8px; } +.continent-body, .country-body { display: none; padding: 10px 0 10px 15px; } +.active>.continent-body, .active>.country-body { display: block; } + +.admin-info { font-size: 12px; color: var(--text-sub); margin-left: 16px; padding: 6px 12px; background: #f8f9fa; border-radius: 4px; border: 1px solid var(--border-color); } +.admin-info strong { color: var(--primary-color); font-weight: 700; } +.base-date-info { font-size: 13px; color: var(--text-sub); background: #fdfdfd; padding: 6px 15px; border-radius: 6px; border: 1px solid var(--border-color); } diff --git a/style/inquiries.css b/style/inquiries.css index 164eede..f63bc81 100644 --- a/style/inquiries.css +++ b/style/inquiries.css @@ -1,251 +1,251 @@ -/* 1. Layout & Board Structure */ -.inquiry-board { - padding: 0 20px 32px 20px; - max-width: 98%; - margin: 36px auto 0; -} - -.board-sticky-header { - position: sticky; - top: 36px; - background: #fff; - z-index: 1000; - padding: 15px 0 10px; - border-bottom: 1px solid #eee; -} - -.board-header { - display: flex; - justify-content: space-between; - align-items: flex-end; - margin-bottom: 20px; -} - -/* 2. Stats Dashboard */ -.header-stats { - display: flex; - gap: 12px; -} - -.stat-item { - background: #fff; - border: 1px solid #eee; - padding: 8px 16px; - border-radius: 8px; - display: flex; - flex-direction: column; - align-items: center; - min-width: 80px; - box-shadow: 0 2px 4px rgba(0,0,0,0.02); - transition: transform 0.2s, box-shadow 0.2s; -} - -.stat-item:hover { - transform: translateY(-2px); - box-shadow: 0 4px 8px rgba(0,0,0,0.05); -} - -.stat-label { font-size: 11px; font-weight: 600; color: #888; margin-bottom: 2px; } -.stat-value { font-size: 18px; font-weight: 700; color: #333; } - -/* Status Border Colors */ -.stat-item.total { } -.stat-item.total .stat-value { color: #1e5149; } -.stat-item.complete { } -.stat-item.complete .stat-value { color: #2e7d32; } -.stat-item.working { } -.stat-item.working .stat-value { color: #1565c0; } -.stat-item.checking { } -.stat-item.checking .stat-value { color: #ef6c00; } -.stat-item.pending { } -.stat-item.pending .stat-value { color: #673ab7; } -.stat-item.unconfirmed { } -.stat-item.unconfirmed .stat-value { color: #9e9e9e; } - -/* 3. Filters & Notice */ -.notice-container { - background: #fdfdfd; - padding: 20px; - border-radius: 8px; - border: 1px solid #e0e0e0; - margin-bottom: 24px; - box-shadow: 0 2px 5px rgba(0,0,0,0.02); -} - -.filter-section { - display: flex; - gap: 12px; - background: #f8f9fa; - padding: 12px 16px; - border-radius: 8px; - margin-top: 15px; -} - -.filter-group { display: flex; flex-direction: column; gap: 4px; } -.filter-group label { font-size: 12px; font-weight: 600; color: #666; } -.filter-group select, .filter-group input { padding: 8px 12px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; } - -/* 4. Table Styles */ -.inquiry-table { - width: 100%; - background: #fff; - border-radius: 8px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); - border-collapse: separate; - border-spacing: 0; - margin-top: 10px; -} - -.inquiry-table thead th { - position: sticky; - background: #f8f9fa; - padding: 14px 16px; - text-align: left; - font-size: 13px; - font-weight: 700; - color: #333; - border-bottom: 2px solid #eee; - z-index: 900; -} - -/* ์ •๋ ฌ ๊ฐ€๋Šฅํ•œ ํ—ค๋” ์Šคํƒ€์ผ ์ถ”๊ฐ€ */ -.inquiry-table thead th.sortable { - cursor: pointer; - user-select: none; - transition: background 0.2s; - white-space: nowrap; -} - -.inquiry-table thead th.sortable .header-content { - display: flex; - align-items: center; - gap: 4px; -} - -.sort-icon { - display: inline-flex; - flex-direction: column; - justify-content: center; - width: 12px; - height: 12px; - font-size: 8px; - color: #ccc; - line-height: 1; - margin-left: 2px; -} - -.inquiry-table thead th.active-sort { - color: #1e5149; - background: #f0f7f6; -} - -.inquiry-table thead th.active-sort .sort-icon { - color: #1e5149; - font-size: 10px; -} - -.inquiry-table td { - padding: 14px 16px; - font-size: 13px; - border-bottom: 1px solid #eee; - vertical-align: middle; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -/* Table Row Hover & Active State */ -.inquiry-row:hover { background: #fcfcfc; cursor: pointer; } -.inquiry-row.active-row { background-color: #f0f7f6 !important; } -.inquiry-row.active-row td { font-weight: 600; color: #1e5149; border-bottom-color: transparent; } - -/* Status Badges */ -.status-badge { padding: 4px 10px; border-radius: 20px; font-size: 11px; font-weight: 700; display: inline-block; } -.status-complete { background: #e8f5e9; color: #2e7d32; } -.status-working { background: #e3f2fd; color: #1565c0; } -.status-checking { background: #fff3e0; color: #ef6c00; } -.status-pending { background: #f5f5f5; color: #616161; } - -/* Table Columns Width & Truncation */ -.inquiry-table td:nth-child(1) { max-width: 50px; } /* No */ -.inquiry-table td:nth-child(2) { max-width: 80px; text-align: center; } /* Image */ -.inquiry-table td:nth-child(3) { max-width: 120px; } /* PM Type */ -.inquiry-table td:nth-child(4) { max-width: 100px; } /* Env */ -.inquiry-table td:nth-child(5) { max-width: 150px; } /* Category */ -.inquiry-table td:nth-child(6) { max-width: 200px; } /* Project */ -.inquiry-table td:nth-child(7), .inquiry-table td:nth-child(8) { max-width: 400px; } /* Content & Reply */ -.inquiry-table td:nth-child(9) { max-width: 100px; } /* Author */ -.inquiry-table td:nth-child(10) { max-width: 120px; } /* Date */ -.inquiry-table td:nth-child(11) { max-width: 100px; } /* Status */ - -/* 5. Detail (Accordion) Styles */ -.detail-row { display: none; background: #fdfdfd; } -.detail-row.active { display: table-row; } -.detail-row td { max-width: none; white-space: normal; overflow: visible; } - -.detail-container { - padding: 24px; - background: #f9fafb; - box-shadow: inset 0 4px 15px rgba(0,0,0,0.08); - position: relative; - border-bottom: 2px solid #eee; -} - -.detail-content-wrapper { - display: flex; - flex-direction: column; - gap: 20px; - border: 1px solid #e5e7eb; - background: #fff; - padding: 25px; - border-radius: 12px; - box-shadow: 0 4px 10px rgba(0,0,0,0.03); -} - -.btn-close-accordion { - position: absolute; - top: 35px; - right: 45px; - background: #eee; - border: none; - padding: 6px 14px; - border-radius: 20px; - font-size: 12px; - font-weight: 600; - color: #666; - cursor: pointer; - z-index: 10; -} -.btn-close-accordion::after { content: "โ–ฒ"; font-size: 10px; margin-left: 5px; } - -.detail-meta-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 15px; margin-bottom: 15px; font-size: 13px; color: #666; } -.detail-label { font-weight: 700; color: #888; margin-right: 8px; } - -.detail-q-section { background: #f8f9fa; padding: 20px; border-radius: 8px; } -.detail-a-section { background: #f1f8f7; padding: 20px; border-radius: 8px; } - -/* 6. Image Preview & Foldable Section */ -.img-thumbnail { width: 32px; height: 32px; border-radius: 4px; object-fit: cover; border: 1px solid #ddd; cursor: pointer; transition: transform 0.2s; } -.img-thumbnail:hover { transform: scale(1.1); } -.no-img { font-size: 10px; color: #ccc; font-style: italic; } - -.detail-image-section { margin-bottom: 20px; background: #f9fafb; border-radius: 8px; border: 1px solid #e5e7eb; overflow: hidden; } -.image-section-header { padding: 12px 16px; background: #f1f5f9; display: flex; justify-content: space-between; align-items: center; cursor: pointer; } -.image-section-header:hover { background: #e2e8f0; } -.image-section-header h4 { margin: 0; color: #1e5149; display: flex; align-items: center; gap: 8px; } -.image-section-content { padding: 20px; display: flex; justify-content: center; background: #fff; border-top: 1px solid #eee; } -.image-section-content.collapsed { display: none; } -.toggle-icon { font-size: 12px; color: #64748b; transition: transform 0.3s; } -.detail-image-section.active .toggle-icon { transform: rotate(180deg); } - -.preview-img { max-width: 100%; max-height: 400px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); object-fit: contain; } - -/* 7. Forms & Reply */ -.reply-edit-form textarea { - width: 100%; height: 120px; padding: 12px; border: 1px solid #ddd; border-radius: 6px; - font-family: inherit; font-size: 14px; margin-bottom: 15px; resize: none; background: #fff; -} -.reply-edit-form textarea:disabled, .reply-edit-form select:disabled, .reply-edit-form input:disabled { background: #fcfcfc; color: #666; border-color: #eee; } -.reply-edit-form.readonly .btn-save, .reply-edit-form.readonly .btn-delete, .reply-edit-form.readonly .btn-cancel { display: none; } -.reply-edit-form.editable .btn-edit { display: none; } -.reply-edit-form.editable textarea { border-color: #1e5149; box-shadow: 0 0 0 2px rgba(30, 81, 73, 0.1); } +/* 1. Layout & Board Structure */ +.inquiry-board { + padding: 0 20px 32px 20px; + max-width: 98%; + margin: 36px auto 0; +} + +.board-sticky-header { + position: sticky; + top: 36px; + background: #fff; + z-index: 1000; + padding: 15px 0 10px; + border-bottom: 1px solid #eee; +} + +.board-header { + display: flex; + justify-content: space-between; + align-items: flex-end; + margin-bottom: 20px; +} + +/* 2. Stats Dashboard */ +.header-stats { + display: flex; + gap: 12px; +} + +.stat-item { + background: #fff; + border: 1px solid #eee; + padding: 8px 16px; + border-radius: 8px; + display: flex; + flex-direction: column; + align-items: center; + min-width: 80px; + box-shadow: 0 2px 4px rgba(0,0,0,0.02); + transition: transform 0.2s, box-shadow 0.2s; +} + +.stat-item:hover { + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(0,0,0,0.05); +} + +.stat-label { font-size: 11px; font-weight: 600; color: #888; margin-bottom: 2px; } +.stat-value { font-size: 18px; font-weight: 700; color: #333; } + +/* Status Border Colors */ +.stat-item.total { } +.stat-item.total .stat-value { color: #1e5149; } +.stat-item.complete { } +.stat-item.complete .stat-value { color: #2e7d32; } +.stat-item.working { } +.stat-item.working .stat-value { color: #1565c0; } +.stat-item.checking { } +.stat-item.checking .stat-value { color: #ef6c00; } +.stat-item.pending { } +.stat-item.pending .stat-value { color: #673ab7; } +.stat-item.unconfirmed { } +.stat-item.unconfirmed .stat-value { color: #9e9e9e; } + +/* 3. Filters & Notice */ +.notice-container { + background: #fdfdfd; + padding: 20px; + border-radius: 8px; + border: 1px solid #e0e0e0; + margin-bottom: 24px; + box-shadow: 0 2px 5px rgba(0,0,0,0.02); +} + +.filter-section { + display: flex; + gap: 12px; + background: #f8f9fa; + padding: 12px 16px; + border-radius: 8px; + margin-top: 15px; +} + +.filter-group { display: flex; flex-direction: column; gap: 4px; } +.filter-group label { font-size: 12px; font-weight: 600; color: #666; } +.filter-group select, .filter-group input { padding: 8px 12px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; } + +/* 4. Table Styles */ +.inquiry-table { + width: 100%; + background: #fff; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); + border-collapse: separate; + border-spacing: 0; + margin-top: 10px; +} + +.inquiry-table thead th { + position: sticky; + background: #f8f9fa; + padding: 14px 16px; + text-align: left; + font-size: 13px; + font-weight: 700; + color: #333; + border-bottom: 2px solid #eee; + z-index: 900; +} + +/* ์ •๋ ฌ ๊ฐ€๋Šฅํ•œ ํ—ค๋” ์Šคํƒ€์ผ ์ถ”๊ฐ€ */ +.inquiry-table thead th.sortable { + cursor: pointer; + user-select: none; + transition: background 0.2s; + white-space: nowrap; +} + +.inquiry-table thead th.sortable .header-content { + display: flex; + align-items: center; + gap: 4px; +} + +.sort-icon { + display: inline-flex; + flex-direction: column; + justify-content: center; + width: 12px; + height: 12px; + font-size: 8px; + color: #ccc; + line-height: 1; + margin-left: 2px; +} + +.inquiry-table thead th.active-sort { + color: #1e5149; + background: #f0f7f6; +} + +.inquiry-table thead th.active-sort .sort-icon { + color: #1e5149; + font-size: 10px; +} + +.inquiry-table td { + padding: 14px 16px; + font-size: 13px; + border-bottom: 1px solid #eee; + vertical-align: middle; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* Table Row Hover & Active State */ +.inquiry-row:hover { background: #fcfcfc; cursor: pointer; } +.inquiry-row.active-row { background-color: #f0f7f6 !important; } +.inquiry-row.active-row td { font-weight: 600; color: #1e5149; border-bottom-color: transparent; } + +/* Status Badges */ +.status-badge { padding: 4px 10px; border-radius: 20px; font-size: 11px; font-weight: 700; display: inline-block; } +.status-complete { background: #e8f5e9; color: #2e7d32; } +.status-working { background: #e3f2fd; color: #1565c0; } +.status-checking { background: #fff3e0; color: #ef6c00; } +.status-pending { background: #f5f5f5; color: #616161; } + +/* Table Columns Width & Truncation */ +.inquiry-table td:nth-child(1) { max-width: 50px; } /* No */ +.inquiry-table td:nth-child(2) { max-width: 80px; text-align: center; } /* Image */ +.inquiry-table td:nth-child(3) { max-width: 120px; } /* PM Type */ +.inquiry-table td:nth-child(4) { max-width: 100px; } /* Env */ +.inquiry-table td:nth-child(5) { max-width: 150px; } /* Category */ +.inquiry-table td:nth-child(6) { max-width: 200px; } /* Project */ +.inquiry-table td:nth-child(7), .inquiry-table td:nth-child(8) { max-width: 400px; } /* Content & Reply */ +.inquiry-table td:nth-child(9) { max-width: 100px; } /* Author */ +.inquiry-table td:nth-child(10) { max-width: 120px; } /* Date */ +.inquiry-table td:nth-child(11) { max-width: 100px; } /* Status */ + +/* 5. Detail (Accordion) Styles */ +.detail-row { display: none; background: #fdfdfd; } +.detail-row.active { display: table-row; } +.detail-row td { max-width: none; white-space: normal; overflow: visible; } + +.detail-container { + padding: 24px; + background: #f9fafb; + box-shadow: inset 0 4px 15px rgba(0,0,0,0.08); + position: relative; + border-bottom: 2px solid #eee; +} + +.detail-content-wrapper { + display: flex; + flex-direction: column; + gap: 20px; + border: 1px solid #e5e7eb; + background: #fff; + padding: 25px; + border-radius: 12px; + box-shadow: 0 4px 10px rgba(0,0,0,0.03); +} + +.btn-close-accordion { + position: absolute; + top: 35px; + right: 45px; + background: #eee; + border: none; + padding: 6px 14px; + border-radius: 20px; + font-size: 12px; + font-weight: 600; + color: #666; + cursor: pointer; + z-index: 10; +} +.btn-close-accordion::after { content: "โ–ฒ"; font-size: 10px; margin-left: 5px; } + +.detail-meta-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 15px; margin-bottom: 15px; font-size: 13px; color: #666; } +.detail-label { font-weight: 700; color: #888; margin-right: 8px; } + +.detail-q-section { background: #f8f9fa; padding: 20px; border-radius: 8px; } +.detail-a-section { background: #f1f8f7; padding: 20px; border-radius: 8px; } + +/* 6. Image Preview & Foldable Section */ +.img-thumbnail { width: 32px; height: 32px; border-radius: 4px; object-fit: cover; border: 1px solid #ddd; cursor: pointer; transition: transform 0.2s; } +.img-thumbnail:hover { transform: scale(1.1); } +.no-img { font-size: 10px; color: #ccc; font-style: italic; } + +.detail-image-section { margin-bottom: 20px; background: #f9fafb; border-radius: 8px; border: 1px solid #e5e7eb; overflow: hidden; } +.image-section-header { padding: 12px 16px; background: #f1f5f9; display: flex; justify-content: space-between; align-items: center; cursor: pointer; } +.image-section-header:hover { background: #e2e8f0; } +.image-section-header h4 { margin: 0; color: #1e5149; display: flex; align-items: center; gap: 8px; } +.image-section-content { padding: 20px; display: flex; justify-content: center; background: #fff; border-top: 1px solid #eee; } +.image-section-content.collapsed { display: none; } +.toggle-icon { font-size: 12px; color: #64748b; transition: transform 0.3s; } +.detail-image-section.active .toggle-icon { transform: rotate(180deg); } + +.preview-img { max-width: 100%; max-height: 400px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); object-fit: contain; } + +/* 7. Forms & Reply */ +.reply-edit-form textarea { + width: 100%; height: 120px; padding: 12px; border: 1px solid #ddd; border-radius: 6px; + font-family: inherit; font-size: 14px; margin-bottom: 15px; resize: none; background: #fff; +} +.reply-edit-form textarea:disabled, .reply-edit-form select:disabled, .reply-edit-form input:disabled { background: #fcfcfc; color: #666; border-color: #eee; } +.reply-edit-form.readonly .btn-save, .reply-edit-form.readonly .btn-delete, .reply-edit-form.readonly .btn-cancel { display: none; } +.reply-edit-form.editable .btn-edit { display: none; } +.reply-edit-form.editable textarea { border-color: #1e5149; box-shadow: 0 0 0 2px rgba(30, 81, 73, 0.1); } diff --git a/style/mail.css b/style/mail.css index 01a3c24..a5cbcfb 100644 --- a/style/mail.css +++ b/style/mail.css @@ -1,219 +1,219 @@ -/* Mail Manager Layout (Vertical Split) */ -.mail-wrapper { - display: flex; height: calc(100vh - var(--topbar-h)); - margin-top: var(--topbar-h); background: #fff; overflow: hidden; -} - -.mail-list-area { - width: 400px; border-right: 1px solid var(--border-color); - display: flex; flex-direction: column; height: 100%; background: #fff; position: relative; -} - -/* 1. Tabs & Search */ -.mail-tabs { display: flex; border-bottom: 1px solid var(--border-color); background: #f8f9fa; flex-shrink: 0; } -.mail-tab { - flex: 1; padding: 12px 0; text-align: center; cursor: pointer; - font-weight: 700; color: #a0aec0; font-size: 11px; transition: all 0.2s ease; - border-bottom: 2px solid transparent; display: flex; align-items: center; justify-content: center; gap: 6px; -} -.mail-tab:hover { background: #edf2f7; color: var(--primary-color); } -.mail-tab.active { color: var(--primary-color); border-bottom: 2px solid var(--primary-color); background: #fff; } - -.search-bar { padding: 16px 24px; border-bottom: 1px solid var(--border-color); background: #fff; flex-shrink: 0; } - -.mail-bulk-actions { - display: none; padding: 8px 16px; background: #f7fafc; - border-bottom: 1px solid var(--border-color); align-items: center; justify-content: space-between; font-size: 12px; -} -.mail-bulk-actions.active { display: flex; } - -/* 2. Mail Items */ -.mail-items-container { flex: 1; overflow-y: auto; padding-bottom: 60px; } -.mail-item { - padding: 16px; border-bottom: 1px solid var(--border-color); cursor: pointer; - display: flex; align-items: flex-start; transition: 0.2s; -} -.mail-item:hover { background: var(--bg-muted); } -.mail-item.active { background: var(--primary-lv-0); } - -.mail-item-checkbox { width: 16px; height: 16px; cursor: pointer; margin-right: 12px; margin-top: 2px; } -.mail-item-content { flex: 1; min-width: 0; } -.mail-item-info { display: flex; align-items: center; gap: 12px; margin-bottom: 4px; } -.mail-date { font-size: 11px; color: var(--text-sub); white-space: nowrap; } - -.btn-mail-delete { - background: #f7fafc; border: 1px solid var(--border-color); color: #718096; - font-size: 10px; padding: 2px 8px; border-radius: 4px; font-weight: 600; -} -.btn-mail-delete:hover { color: var(--error-color); background: #fff5f5; border-color: #feb2b2; } - -/* 3. Content Area */ -.mail-content-area { flex: 1; display: flex; flex-direction: column; overflow-y: auto; border-right: 1px solid var(--border-color); } -.mail-content-header { padding: var(--space-lg); border-bottom: 1px solid var(--border-color); } -.mail-body { padding: var(--space-lg); line-height: 1.6; min-height: 200px; } - -/* 4. Attachments & AI */ -.attachment-area { padding: var(--space-lg); border-top: 1px solid var(--border-color); background: var(--bg-muted); } -.attachment-item { - display: flex; align-items: center; gap: var(--space-md); background: #fff; - padding: 12px 20px; border-radius: var(--radius-lg); - border: 1px solid var(--border-color); margin-bottom: var(--space-sm); cursor: pointer; - transition: 0.2s; -} -.attachment-item:hover { border-color: var(--primary-color); box-shadow: var(--box-shadow); } -.attachment-item.active { background: var(--primary-lv-0); border-color: var(--primary-color); } - -.file-details { flex: 1; min-width: 0; } -.file-name { font-size: 13px; font-weight: 700; max-width: 450px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.file-size { font-size: 11px; color: var(--text-sub); } - -.btn-group { - display: flex; align-items: center; gap: 12px; flex-shrink: 0; justify-content: flex-end; -} - -.btn-upload { - padding: 6px 14px; border-radius: 6px; font-size: 11px; font-weight: 700; - color: #fff; border: none; cursor: pointer; transition: 0.2s; height: 32px; -} - -.btn-ai { background: var(--ai-gradient); } -.btn-ai:hover { filter: brightness(1.1); transform: translateY(-1px); } - -.btn-normal { background: var(--primary-color); } -.btn-normal:hover { background: var(--primary-hover); transform: translateY(-1px); } - -.ai-recommend { - font-size: 11px; padding: 6px 12px; border-radius: 6px; font-weight: 600; - cursor: pointer; transition: 0.2s; display: inline-block; - max-width: 250px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; -} -.ai-recommend.smart-mode { background: #eef2ff; color: #4338ca; border: 1px solid #c7d2fe; } -.ai-recommend.manual-mode { background: #f1f5f9; color: #475569; border: 1px dashed #cbd5e1; } -.ai-recommend:hover { transform: scale(1.02); } - -/* 5. Preview Area */ -.mail-preview-area { - width: 0; background: #f8f9fa; display: flex; flex-direction: column; - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); position: relative; - border-left: 0 solid transparent; -} - -.mail-preview-area.active { - width: 600px; - border-left: 1px solid var(--border-color); - visibility: visible; -} - -.preview-header { - height: 56px; padding: 0 24px; border-bottom: 1px solid var(--border-color); - display: flex; align-items: center; justify-content: space-between; - background: #fff; flex-shrink: 0; -} - -.preview-header h3 { font-size: 15px; font-weight: 800; color: var(--primary-color); margin: 0; } - -#fullViewBtn { - background: var(--primary-lv-0) !important; - color: var(--primary-color) !important; - border: 1px solid var(--primary-lv-1) !important; - font-weight: 700 !important; - padding: 4px 16px !important; - border-radius: 4px !important; - font-size: 11px !important; - transition: 0.2s !important; -} -#fullViewBtn:hover { background: var(--primary-lv-1) !important; } - -.preview-toggle-handle { - position: absolute; left: -20px; top: 50%; transform: translateY(-50%); - width: 20px; height: 60px; background: var(--primary-color); color: #fff; - display: flex; align-items: center; justify-content: center; - border-radius: 8px 0 0 8px; font-size: 10px; cursor: pointer; - box-shadow: -2px 0 5px rgba(0,0,0,0.1); z-index: 100; -} -.preview-toggle-handle:hover { background: var(--primary-hover); } - -.a4-container { - flex: 1; padding: 30px; overflow-y: auto; background: #e9ecef; - display: flex; justify-content: center; -} - -.a4-container iframe, .a4-container .preview-placeholder { - width: 100%; height: 100%; background: #fff; - box-shadow: 0 4px 20px rgba(0,0,0,0.08); border-radius: 4px; -} - -.preview-placeholder { - display: flex; align-items: center; justify-content: center; - text-align: center; color: var(--text-sub); font-size: 13px; line-height: 1.6; -} - -.mail-preview-area.active > * { opacity: 1 !important; visibility: visible !important; pointer-events: auto !important; } -.mail-preview-area > *:not(.preview-toggle-handle) { opacity: 0; visibility: hidden; pointer-events: none; transition: 0.2s; } - -/* 6. Footer & Others */ -.address-book-footer { - position: absolute; bottom: 0; left: 0; width: 100%; padding: 12px 16px; - border-top: 1px solid var(--border-color); background: #fff; display: flex; gap: 8px; z-index: 5; -} - -.file-log-area { - display: none; width: 100%; margin-top: 10px; background: #1a202c; - border-radius: 4px; padding: 12px; font-family: monospace; font-size: 11px; color: #cbd5e0; -} -.file-log-area.active { display: block; } -.log-success { color: #48bb78; font-weight: 700; } - -.switch { position: relative; display: inline-block; width: 34px; height: 20px; } -.switch input { opacity: 0; width: 0; height: 0; } -.slider { - position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; - background-color: #ccc; transition: .4s; border-radius: 20px; -} -.slider:before { - position: absolute; content: ""; height: 14px; width: 14px; left: 3px; bottom: 3px; - background-color: white; transition: .4s; border-radius: 50%; -} -input:checked+.slider { background: var(--ai-gradient); } -input:checked+.slider:before { transform: translateX(14px); } - -/* Restore Path Selector Modal Specific Styles */ -.select-group { - display: flex; - flex-direction: column; - gap: 8px; -} - -.select-group label { - font-size: 12px; - font-weight: 700; - color: var(--text-main); -} - -.modal-select { - width: 100%; - height: 44px; - padding: 0 15px; - border: 1px solid var(--border-color); - border-radius: 8px; - background-color: #f9f9f9; - font-size: 14px; - color: #333; - outline: none; - transition: all 0.2s; - cursor: pointer; - appearance: none; - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23666' d='M6 8L2 4h8L6 8z'/%3E%3C/svg%3E"); - background-repeat: no-repeat; - background-position: right 15px center; -} - -.modal-select:focus { - border-color: var(--primary-color); - background-color: #fff; - box-shadow: 0 0 0 3px rgba(30, 81, 73, 0.1); -} - -.modal-select option { - padding: 10px; -} +/* Mail Manager Layout (Vertical Split) */ +.mail-wrapper { + display: flex; height: calc(100vh - var(--topbar-h)); + margin-top: var(--topbar-h); background: #fff; overflow: hidden; +} + +.mail-list-area { + width: 400px; border-right: 1px solid var(--border-color); + display: flex; flex-direction: column; height: 100%; background: #fff; position: relative; +} + +/* 1. Tabs & Search */ +.mail-tabs { display: flex; border-bottom: 1px solid var(--border-color); background: #f8f9fa; flex-shrink: 0; } +.mail-tab { + flex: 1; padding: 12px 0; text-align: center; cursor: pointer; + font-weight: 700; color: #a0aec0; font-size: 11px; transition: all 0.2s ease; + border-bottom: 2px solid transparent; display: flex; align-items: center; justify-content: center; gap: 6px; +} +.mail-tab:hover { background: #edf2f7; color: var(--primary-color); } +.mail-tab.active { color: var(--primary-color); border-bottom: 2px solid var(--primary-color); background: #fff; } + +.search-bar { padding: 16px 24px; border-bottom: 1px solid var(--border-color); background: #fff; flex-shrink: 0; } + +.mail-bulk-actions { + display: none; padding: 8px 16px; background: #f7fafc; + border-bottom: 1px solid var(--border-color); align-items: center; justify-content: space-between; font-size: 12px; +} +.mail-bulk-actions.active { display: flex; } + +/* 2. Mail Items */ +.mail-items-container { flex: 1; overflow-y: auto; padding-bottom: 60px; } +.mail-item { + padding: 16px; border-bottom: 1px solid var(--border-color); cursor: pointer; + display: flex; align-items: flex-start; transition: 0.2s; +} +.mail-item:hover { background: var(--bg-muted); } +.mail-item.active { background: var(--primary-lv-0); } + +.mail-item-checkbox { width: 16px; height: 16px; cursor: pointer; margin-right: 12px; margin-top: 2px; } +.mail-item-content { flex: 1; min-width: 0; } +.mail-item-info { display: flex; align-items: center; gap: 12px; margin-bottom: 4px; } +.mail-date { font-size: 11px; color: var(--text-sub); white-space: nowrap; } + +.btn-mail-delete { + background: #f7fafc; border: 1px solid var(--border-color); color: #718096; + font-size: 10px; padding: 2px 8px; border-radius: 4px; font-weight: 600; +} +.btn-mail-delete:hover { color: var(--error-color); background: #fff5f5; border-color: #feb2b2; } + +/* 3. Content Area */ +.mail-content-area { flex: 1; display: flex; flex-direction: column; overflow-y: auto; border-right: 1px solid var(--border-color); } +.mail-content-header { padding: var(--space-lg); border-bottom: 1px solid var(--border-color); } +.mail-body { padding: var(--space-lg); line-height: 1.6; min-height: 200px; } + +/* 4. Attachments & AI */ +.attachment-area { padding: var(--space-lg); border-top: 1px solid var(--border-color); background: var(--bg-muted); } +.attachment-item { + display: flex; align-items: center; gap: var(--space-md); background: #fff; + padding: 12px 20px; border-radius: var(--radius-lg); + border: 1px solid var(--border-color); margin-bottom: var(--space-sm); cursor: pointer; + transition: 0.2s; +} +.attachment-item:hover { border-color: var(--primary-color); box-shadow: var(--box-shadow); } +.attachment-item.active { background: var(--primary-lv-0); border-color: var(--primary-color); } + +.file-details { flex: 1; min-width: 0; } +.file-name { font-size: 13px; font-weight: 700; max-width: 450px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.file-size { font-size: 11px; color: var(--text-sub); } + +.btn-group { + display: flex; align-items: center; gap: 12px; flex-shrink: 0; justify-content: flex-end; +} + +.btn-upload { + padding: 6px 14px; border-radius: 6px; font-size: 11px; font-weight: 700; + color: #fff; border: none; cursor: pointer; transition: 0.2s; height: 32px; +} + +.btn-ai { background: var(--ai-gradient); } +.btn-ai:hover { filter: brightness(1.1); transform: translateY(-1px); } + +.btn-normal { background: var(--primary-color); } +.btn-normal:hover { background: var(--primary-hover); transform: translateY(-1px); } + +.ai-recommend { + font-size: 11px; padding: 6px 12px; border-radius: 6px; font-weight: 600; + cursor: pointer; transition: 0.2s; display: inline-block; + max-width: 250px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; +} +.ai-recommend.smart-mode { background: #eef2ff; color: #4338ca; border: 1px solid #c7d2fe; } +.ai-recommend.manual-mode { background: #f1f5f9; color: #475569; border: 1px dashed #cbd5e1; } +.ai-recommend:hover { transform: scale(1.02); } + +/* 5. Preview Area */ +.mail-preview-area { + width: 0; background: #f8f9fa; display: flex; flex-direction: column; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); position: relative; + border-left: 0 solid transparent; +} + +.mail-preview-area.active { + width: 600px; + border-left: 1px solid var(--border-color); + visibility: visible; +} + +.preview-header { + height: 56px; padding: 0 24px; border-bottom: 1px solid var(--border-color); + display: flex; align-items: center; justify-content: space-between; + background: #fff; flex-shrink: 0; +} + +.preview-header h3 { font-size: 15px; font-weight: 800; color: var(--primary-color); margin: 0; } + +#fullViewBtn { + background: var(--primary-lv-0) !important; + color: var(--primary-color) !important; + border: 1px solid var(--primary-lv-1) !important; + font-weight: 700 !important; + padding: 4px 16px !important; + border-radius: 4px !important; + font-size: 11px !important; + transition: 0.2s !important; +} +#fullViewBtn:hover { background: var(--primary-lv-1) !important; } + +.preview-toggle-handle { + position: absolute; left: -20px; top: 50%; transform: translateY(-50%); + width: 20px; height: 60px; background: var(--primary-color); color: #fff; + display: flex; align-items: center; justify-content: center; + border-radius: 8px 0 0 8px; font-size: 10px; cursor: pointer; + box-shadow: -2px 0 5px rgba(0,0,0,0.1); z-index: 100; +} +.preview-toggle-handle:hover { background: var(--primary-hover); } + +.a4-container { + flex: 1; padding: 30px; overflow-y: auto; background: #e9ecef; + display: flex; justify-content: center; +} + +.a4-container iframe, .a4-container .preview-placeholder { + width: 100%; height: 100%; background: #fff; + box-shadow: 0 4px 20px rgba(0,0,0,0.08); border-radius: 4px; +} + +.preview-placeholder { + display: flex; align-items: center; justify-content: center; + text-align: center; color: var(--text-sub); font-size: 13px; line-height: 1.6; +} + +.mail-preview-area.active > * { opacity: 1 !important; visibility: visible !important; pointer-events: auto !important; } +.mail-preview-area > *:not(.preview-toggle-handle) { opacity: 0; visibility: hidden; pointer-events: none; transition: 0.2s; } + +/* 6. Footer & Others */ +.address-book-footer { + position: absolute; bottom: 0; left: 0; width: 100%; padding: 12px 16px; + border-top: 1px solid var(--border-color); background: #fff; display: flex; gap: 8px; z-index: 5; +} + +.file-log-area { + display: none; width: 100%; margin-top: 10px; background: #1a202c; + border-radius: 4px; padding: 12px; font-family: monospace; font-size: 11px; color: #cbd5e0; +} +.file-log-area.active { display: block; } +.log-success { color: #48bb78; font-weight: 700; } + +.switch { position: relative; display: inline-block; width: 34px; height: 20px; } +.switch input { opacity: 0; width: 0; height: 0; } +.slider { + position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; + background-color: #ccc; transition: .4s; border-radius: 20px; +} +.slider:before { + position: absolute; content: ""; height: 14px; width: 14px; left: 3px; bottom: 3px; + background-color: white; transition: .4s; border-radius: 50%; +} +input:checked+.slider { background: var(--ai-gradient); } +input:checked+.slider:before { transform: translateX(14px); } + +/* Restore Path Selector Modal Specific Styles */ +.select-group { + display: flex; + flex-direction: column; + gap: 8px; +} + +.select-group label { + font-size: 12px; + font-weight: 700; + color: var(--text-main); +} + +.modal-select { + width: 100%; + height: 44px; + padding: 0 15px; + border: 1px solid var(--border-color); + border-radius: 8px; + background-color: #f9f9f9; + font-size: 14px; + color: #333; + outline: none; + transition: all 0.2s; + cursor: pointer; + appearance: none; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23666' d='M6 8L2 4h8L6 8z'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 15px center; +} + +.modal-select:focus { + border-color: var(--primary-color); + background-color: #fff; + box-shadow: 0 0 0 3px rgba(30, 81, 73, 0.1); +} + +.modal-select option { + padding: 10px; +} diff --git a/style/style.css b/style/style.css index 21bee0b..4b1d736 100644 --- a/style/style.css +++ b/style/style.css @@ -1,3 +1,3 @@ -@import url('common.css'); -@import url('dashboard.css'); -@import url('mail.css'); +@import url('common.css'); +@import url('dashboard.css'); +@import url('mail.css'); diff --git a/templates/analysis.html b/templates/analysis.html index beb6c26..1edc0ab 100644 --- a/templates/analysis.html +++ b/templates/analysis.html @@ -1,139 +1,139 @@ - - - - - - - ๋ฐ์ดํ„ฐ ๋ถ„์„ - Project Master Sabermetrics - - - - - - - -
- -
-
-
-
AI Sabermetrics
-

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

-

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

-
-
- -
-
- - -
-
-
-

AI Hybrid Prediction Engine

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

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

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

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

-
-
-
-
- -
-
-

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

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

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

-
-
-
2. ํ™œ๋™ ์‹œ๊ณ„์—ด ๊ด€์„ฑ ๋ถ„์„
-

์ตœ๊ทผ ํ™œ๋™์˜ ์—ฐ์†์„ฑ์„ ๋ถ„์„ํ•˜์—ฌ, ๋‹จ๊ธฐ ์ •์ฒด ์‹œ์—๋„ ๊ณผ๊ฑฐ์˜ ์šด์˜ ๋ชจ๋ฉ˜ํ…€์„ ๋ฐ˜์˜ํ•˜์—ฌ ์ง€์ˆ˜๋ฅผ ๋ณด์ •ํ•ฉ๋‹ˆ๋‹ค.

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

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

-
-
-
-
-
- - -
-
-
์šด์˜ ํ™œ๋ ฅ ๋ถ„ํฌ (Activity Distribution)
- -
-
-
์ž์‚ฐ ๊ฐ€์น˜ ์ „๋žต ๋งคํŠธ๋ฆญ์Šค (Strategic Analysis)
- -
-
- - -
-
-
-

Project Activity Vitality Leaderboard (AVI Status)

-

์šด์˜ ํ‘œ์ค€(AVI 70%) ๋Œ€๋น„ ์šด์˜ ํ™œ๋ ฅ ๋ฐ VCI ๊ธฐ์—ฌ ๋ฆฌ๋”๋ณด๋“œ

-
-
- * AVI (Activity Vitality Index) -
-
-
-
-
70%โ†‘ ์ •์ƒ ์šด์˜
-
30~70% ๊ด€๋ฆฌ ์ฃผ์˜
-
10~30% ์œ„ํ—˜ ๋…ธ์ถœ
-
10%โ†“ ์ค‘๋‹จ/๋ฐฉ์น˜
-
- -
- -
-
-
-
- - - - - - - - - + + + + + + + ๋ฐ์ดํ„ฐ ๋ถ„์„ - Project Master Sabermetrics + + + + + + + + + +
+
+
+
AI Sabermetrics
+

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

+

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

+
+
+ +
+
+ + +
+
+
+

AI Hybrid Prediction Engine

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

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

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

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

+
+
+
+
+ +
+
+

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

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

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

+
+
+
2. ํ™œ๋™ ์‹œ๊ณ„์—ด ๊ด€์„ฑ ๋ถ„์„
+

์ตœ๊ทผ ํ™œ๋™์˜ ์—ฐ์†์„ฑ์„ ๋ถ„์„ํ•˜์—ฌ, ๋‹จ๊ธฐ ์ •์ฒด ์‹œ์—๋„ ๊ณผ๊ฑฐ์˜ ์šด์˜ ๋ชจ๋ฉ˜ํ…€์„ ๋ฐ˜์˜ํ•˜์—ฌ ์ง€์ˆ˜๋ฅผ ๋ณด์ •ํ•ฉ๋‹ˆ๋‹ค.

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

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

+
+
+
+
+
+ + +
+
+
์šด์˜ ํ™œ๋ ฅ ๋ถ„ํฌ (Activity Distribution)
+ +
+
+
์ž์‚ฐ ๊ฐ€์น˜ ์ „๋žต ๋งคํŠธ๋ฆญ์Šค (Strategic Analysis)
+ +
+
+ + +
+
+
+

Project Activity Vitality Leaderboard (AVI Status)

+

์ „์ฒด ํฌํŠธํด๋ฆฌ์˜ค ํ‰๊ท (0.0) ๋Œ€๋น„ ์šด์˜ ๊ฐ€์น˜ ๊ธฐ์—ฌ(VCI) ๋ฆฌ๋”๋ณด๋“œ

+
+
+ * AVI (Activity Vitality Index) +
+
+
+
+
70%โ†‘ ์ •์ƒ ์šด์˜
+
30~70% ๊ด€๋ฆฌ ์ฃผ์˜
+
10~30% ์œ„ํ—˜ ๋…ธ์ถœ
+
10%โ†“ ์ค‘๋‹จ/๋ฐฉ์น˜
+
+ +
+ +
+
+
+
+ + + + + + + + + diff --git a/templates/analysis_test.html b/templates/analysis_test.html new file mode 100644 index 0000000..47fcef4 --- /dev/null +++ b/templates/analysis_test.html @@ -0,0 +1,139 @@ + + + + + + + ๋ฐ์ดํ„ฐ ๋ถ„์„ (ํ…Œ์ŠคํŠธ) - Project Master Sabermetrics + + + + + + + + + +
+
+
+
AI Sabermetrics [TEST]
+

์‹œ์Šคํ…œ ์šด์˜ ์ž์‚ฐ ๊ฐ€์น˜ ๋ถ„์„ (ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ)

+

ํ…Œ์ŠคํŠธ์šฉ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค(PM_proto_test)๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•œ ํ™œ๋ ฅ ์ง€ํ‘œ ๋ถ„์„์ž…๋‹ˆ๋‹ค.

+
+
+ +
+
+ + +
+
+
+

AI Hybrid Prediction Engine (TEST)

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

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

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

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

+
+
+
+
+ +
+
+

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

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

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

+
+
+
2. ํ™œ๋™ ์‹œ๊ณ„์—ด ๊ด€์„ฑ ๋ถ„์„
+

์ตœ๊ทผ ํ™œ๋™์˜ ์—ฐ์†์„ฑ์„ ๋ถ„์„ํ•˜์—ฌ, ๋‹จ๊ธฐ ์ •์ฒด ์‹œ์—๋„ ๊ณผ๊ฑฐ์˜ ์šด์˜ ๋ชจ๋ฉ˜ํ…€์„ ๋ฐ˜์˜ํ•˜์—ฌ ์ง€์ˆ˜๋ฅผ ๋ณด์ •ํ•ฉ๋‹ˆ๋‹ค.

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

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

+
+
+
+
+
+ + +
+
+
์šด์˜ ํ™œ๋ ฅ ๋ถ„ํฌ (Activity Distribution)
+ +
+
+
์ž์‚ฐ ๊ฐ€์น˜ ์ „๋žต ๋งคํŠธ๋ฆญ์Šค (Strategic Analysis)
+ +
+
+ + +
+
+
+

Project Activity Vitality Leaderboard (AVI Status) [TEST]

+

์ „์ฒด ํฌํŠธํด๋ฆฌ์˜ค ํ‰๊ท (0.0) ๋Œ€๋น„ ์šด์˜ ๊ฐ€์น˜ ๊ธฐ์—ฌ(VCI) ๋ฆฌ๋”๋ณด๋“œ

+
+
+ * AVI (Activity Vitality Index) +
+
+
+
+
70%โ†‘ ์ •์ƒ ์šด์˜
+
30~70% ๊ด€๋ฆฌ ์ฃผ์˜
+
10~30% ์œ„ํ—˜ ๋…ธ์ถœ
+
10%โ†“ ์ค‘๋‹จ/๋ฐฉ์น˜
+
+ +
+ +
+
+
+
+ + + + + + + + + diff --git a/templates/dashboard.html b/templates/dashboard.html index 5eb189b..2edd734 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -1,118 +1,118 @@ - - - - - - - Project Master Overseas ๊ด€๋ฆฌ์ž - - - - - - - - - -
-
-

ํ”„๋กœ์ ํŠธ ํ˜„ํ™ฉ

-
-
๊ธฐ์ค€๋‚ ์งœ: -
- -
์ ‘์†์ž: ์ดํƒœํ›ˆ[์ „์ฒด๊ด€๋ฆฌ์ž]
-
-
- - -
-
- -
-
- - - - -
- -
-
- - - - - - - - - - + + + + + + + Project Master Overseas ๊ด€๋ฆฌ์ž + + + + + + + + + +
+
+

ํ”„๋กœ์ ํŠธ ํ˜„ํ™ฉ

+
+
๊ธฐ์ค€๋‚ ์งœ: -
+ +
์ ‘์†์ž: ์ดํƒœํ›ˆ[์ „์ฒด๊ด€๋ฆฌ์ž]
+
+
+ + +
+
+ +
+
+ + + + +
+ +
+
+ + + + + + + + + + \ No newline at end of file diff --git a/templates/dashboard_test.html b/templates/dashboard_test.html new file mode 100644 index 0000000..8042b28 --- /dev/null +++ b/templates/dashboard_test.html @@ -0,0 +1,118 @@ + + + + + + + Project Master Overseas ๊ด€๋ฆฌ์ž (ํ…Œ์ŠคํŠธ) + + + + + + + + + +
+
+

ํ”„๋กœ์ ํŠธ ํ˜„ํ™ฉ (ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ)

+
+
๊ธฐ์ค€๋‚ ์งœ: -
+ +
์ ‘์†์ž: ์ดํƒœํ›ˆ[ํ…Œ์ŠคํŠธ๊ด€๋ฆฌ์ž]
+
+
+ + +
+
+ +
+
+ + + + +
+ +
+
+ + + + + + + + + + + \ No newline at end of file diff --git a/templates/index.html b/templates/index.html index b3972f6..a51e5b2 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1,46 +1,58 @@ - - - - - - - Project Master Portal - - - - - - - - - - - - - - + + + + + + + Project Master Portal + + + + + + + + + + + + + + \ No newline at end of file diff --git a/templates/inquiries.html b/templates/inquiries.html index 3b39ca1..110837f 100644 --- a/templates/inquiries.html +++ b/templates/inquiries.html @@ -1,194 +1,194 @@ - - - - - - - ๋ฌธ์˜์‚ฌํ•ญ ๊ด€๋ฆฌ - Project Master - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - -
-
No โ–ผ
-
์ด๋ฏธ์ง€ -
PM ์ข…๋ฅ˜
-
-
ํ™˜๊ฒฝ
-
-
๊ตฌ๋ถ„
-
-
ํ”„๋กœ์ ํŠธ
-
๋ฌธ์˜๋‚ด์šฉ -
์ž‘์„ฑ์ž
-
-
๋‚ ์งœ
-
๋‹ต๋ณ€๋‚ด์šฉ -
์ƒํƒœ
-
-
- - - - - - - - + + + + + + + ๋ฌธ์˜์‚ฌํ•ญ ๊ด€๋ฆฌ - Project Master + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+
No โ–ผ
+
์ด๋ฏธ์ง€ +
PM ์ข…๋ฅ˜
+
+
ํ™˜๊ฒฝ
+
+
๊ตฌ๋ถ„
+
+
ํ”„๋กœ์ ํŠธ
+
๋ฌธ์˜๋‚ด์šฉ +
์ž‘์„ฑ์ž
+
+
๋‚ ์งœ
+
๋‹ต๋ณ€๋‚ด์šฉ +
์ƒํƒœ
+
+
+ + + + + + + + \ No newline at end of file diff --git a/templates/mailTest.html b/templates/mailTest.html index 4f98cdd..8369ea6 100644 --- a/templates/mailTest.html +++ b/templates/mailTest.html @@ -1,144 +1,144 @@ - - - - - - - Project Mail Manager - - - - - - - {% include 'modals/path_selector.html' %} - - - -
- -
-
-
๐Ÿ“ฅ ์ˆ˜์‹ 
-
๐Ÿ“ค ๋ฐœ์‹ 
-
๐Ÿ“ ์ž„์‹œ
-
๐Ÿ—‘๏ธ ํœด์ง€ํ†ต
-
- - - -
-
- - 0๊ฐœ ์„ ํƒ๋จ -
- -
- -
- -
- - -
- - -
-
-

ITTC ๊ต์œก์„ผํ„ฐ ์ฐฉ๊ณต์‹ ์ผ์ • ํ˜‘์˜ ์š”์ฒญ

-
๋ณด๋‚ธ์‚ฌ๋žŒ pany.s@lao.gov.la (๋ผ์˜ค์Šค ๋†๋ฆผ๋ถ€) -
-
๋‚ ์งœ 2026๋…„ 2์›” 26์ผ 14:30
-
-
- ์•ˆ๋…•ํ•˜์„ธ์š”. ์ดํƒœํ›ˆ ์„ ์ž„์—ฐ๊ตฌ์›๋‹˜.

- ๋ผ์˜ค์Šค ITTC ๊ด€๊ฐœ ๊ต์œก์„ผํ„ฐ ์ฐฉ๊ณต์‹๊ณผ ๊ด€๋ จํ•˜์—ฌ ์ •๋ถ€ ์ธก ์ธ์‚ฌ์˜ ์ผ์ •์„ ๋ฐ˜์˜ํ•œ ์ตœ์ข… ๊ณต๋ฌธ์„ ์†ก๋ถ€ํ•ฉ๋‹ˆ๋‹ค.
-
- -
-
-
์ฒจ๋ถ€ํŒŒ์ผ ๋ฆฌ์ŠคํŠธ
-
- AI ํŒ๋‹จ - -
-
-
-
-
- - - -
- - {% include 'modals/address_book.html' %} - - - - - + + + + + + + Project Mail Manager + + + + + + + {% include 'modals/path_selector.html' %} + + + +
+ +
+
+
๐Ÿ“ฅ ์ˆ˜์‹ 
+
๐Ÿ“ค ๋ฐœ์‹ 
+
๐Ÿ“ ์ž„์‹œ
+
๐Ÿ—‘๏ธ ํœด์ง€ํ†ต
+
+ + + +
+
+ + 0๊ฐœ ์„ ํƒ๋จ +
+ +
+ +
+ +
+ + +
+ + +
+
+

ITTC ๊ต์œก์„ผํ„ฐ ์ฐฉ๊ณต์‹ ์ผ์ • ํ˜‘์˜ ์š”์ฒญ

+
๋ณด๋‚ธ์‚ฌ๋žŒ pany.s@lao.gov.la (๋ผ์˜ค์Šค ๋†๋ฆผ๋ถ€) +
+
๋‚ ์งœ 2026๋…„ 2์›” 26์ผ 14:30
+
+
+ ์•ˆ๋…•ํ•˜์„ธ์š”. ์ดํƒœํ›ˆ ์„ ์ž„์—ฐ๊ตฌ์›๋‹˜.

+ ๋ผ์˜ค์Šค ITTC ๊ด€๊ฐœ ๊ต์œก์„ผํ„ฐ ์ฐฉ๊ณต์‹๊ณผ ๊ด€๋ จํ•˜์—ฌ ์ •๋ถ€ ์ธก ์ธ์‚ฌ์˜ ์ผ์ •์„ ๋ฐ˜์˜ํ•œ ์ตœ์ข… ๊ณต๋ฌธ์„ ์†ก๋ถ€ํ•ฉ๋‹ˆ๋‹ค.
+
+ +
+
+
์ฒจ๋ถ€ํŒŒ์ผ ๋ฆฌ์ŠคํŠธ
+
+ AI ํŒ๋‹จ + +
+
+
+
+
+ + + +
+ + {% include 'modals/address_book.html' %} + + + + + \ No newline at end of file diff --git a/templates/modals/address_book.html b/templates/modals/address_book.html index 4fc6c7d..a84954c 100644 --- a/templates/modals/address_book.html +++ b/templates/modals/address_book.html @@ -1,62 +1,62 @@ - - + + diff --git a/templates/modals/path_selector.html b/templates/modals/path_selector.html index 38e49c6..bd38eb9 100644 --- a/templates/modals/path_selector.html +++ b/templates/modals/path_selector.html @@ -1,24 +1,24 @@ - - + + diff --git a/tokens.json b/tokens.json index 6e99e9a..e503fa1 100644 --- a/tokens.json +++ b/tokens.json @@ -1,1229 +1,1229 @@ -{ - "core": { - "dimension": { - "scale": { - "$value": "2", - "$type": "dimension" - }, - "xs": { - "$value": "4", - "$type": "dimension" - }, - "sm": { - "$value": "{dimension.xs} * {dimension.scale}", - "$type": "dimension" - }, - "md": { - "$value": "{dimension.sm} * {dimension.scale}", - "$type": "dimension" - }, - "lg": { - "$value": "{dimension.md} * {dimension.scale}", - "$type": "dimension" - }, - "xl": { - "$value": "{dimension.lg} * {dimension.scale}", - "$type": "dimension" - } - }, - "spacing": { - "xs": { - "$value": "{dimension.xs}", - "$type": "spacing" - }, - "sm": { - "$value": "{dimension.sm}", - "$type": "spacing" - }, - "md": { - "$value": "{dimension.md}", - "$type": "spacing" - }, - "lg": { - "$value": "{dimension.lg}", - "$type": "spacing" - }, - "xl": { - "$value": "{dimension.xl}", - "$type": "spacing" - }, - "multi-value": { - "$value": "{dimension.sm} {dimension.xl}", - "$type": "spacing", - "$description": "You can have multiple values in a single spacing token. Read more on these: https://docs.tokens.studio/available-tokens/spacing-tokens#multi-value-spacing-tokens" - } - }, - "borderRadius": { - "sm": { - "$value": "4", - "$type": "borderRadius" - }, - "lg": { - "$value": "8", - "$type": "borderRadius" - }, - "xl": { - "$value": "16", - "$type": "borderRadius" - }, - "multi-value": { - "$value": "{borderRadius.sm} {borderRadius.lg}", - "$type": "borderRadius", - "$description": "You can have multiple values in a single radius token. Read more on these: https://docs.tokens.studio/available-tokens/border-radius-tokens#single--multiple-values" - } - }, - "colors": { - "black": { - "$value": "#000000", - "$type": "color" - }, - "white": { - "$value": "#ffffff", - "$type": "color" - }, - "gray": { - "100": { - "$value": "#f7fafc", - "$type": "color" - }, - "200": { - "$value": "#edf2f7", - "$type": "color" - }, - "300": { - "$value": "#e2e8f0", - "$type": "color" - }, - "400": { - "$value": "#cbd5e0", - "$type": "color" - }, - "500": { - "$value": "#a0aec0", - "$type": "color" - }, - "600": { - "$value": "#718096", - "$type": "color" - }, - "700": { - "$value": "#4a5568", - "$type": "color" - }, - "800": { - "$value": "#2d3748", - "$type": "color" - }, - "900": { - "$value": "#1a202c", - "$type": "color" - } - }, - "red": { - "100": { - "$value": "#fff5f5", - "$type": "color" - }, - "200": { - "$value": "#fed7d7", - "$type": "color" - }, - "300": { - "$value": "#feb2b2", - "$type": "color" - }, - "400": { - "$value": "#fc8181", - "$type": "color" - }, - "500": { - "$value": "#f56565", - "$type": "color" - }, - "600": { - "$value": "#e53e3e", - "$type": "color" - }, - "700": { - "$value": "#c53030", - "$type": "color" - }, - "800": { - "$value": "#9b2c2c", - "$type": "color" - }, - "900": { - "$value": "#742a2a", - "$type": "color" - } - }, - "orange": { - "100": { - "$value": "#fffaf0", - "$type": "color" - }, - "200": { - "$value": "#feebc8", - "$type": "color" - }, - "300": { - "$value": "#fbd38d", - "$type": "color" - }, - "400": { - "$value": "#f6ad55", - "$type": "color" - }, - "500": { - "$value": "#ed8936", - "$type": "color" - }, - "600": { - "$value": "#dd6b20", - "$type": "color" - }, - "700": { - "$value": "#c05621", - "$type": "color" - }, - "800": { - "$value": "#9c4221", - "$type": "color" - }, - "900": { - "$value": "#7b341e", - "$type": "color" - } - }, - "yellow": { - "100": { - "$value": "#fffff0", - "$type": "color" - }, - "200": { - "$value": "#fefcbf", - "$type": "color" - }, - "300": { - "$value": "#faf089", - "$type": "color" - }, - "400": { - "$value": "#f6e05e", - "$type": "color" - }, - "500": { - "$value": "#ecc94b", - "$type": "color" - }, - "600": { - "$value": "#d69e2e", - "$type": "color" - }, - "700": { - "$value": "#b7791f", - "$type": "color" - }, - "800": { - "$value": "#975a16", - "$type": "color" - }, - "900": { - "$value": "#744210", - "$type": "color" - } - }, - "green": { - "100": { - "$value": "#f0fff4", - "$type": "color" - }, - "200": { - "$value": "#c6f6d5", - "$type": "color" - }, - "300": { - "$value": "#9ae6b4", - "$type": "color" - }, - "400": { - "$value": "#68d391", - "$type": "color" - }, - "500": { - "$value": "#48bb78", - "$type": "color" - }, - "600": { - "$value": "#38a169", - "$type": "color" - }, - "700": { - "$value": "#2f855a", - "$type": "color" - }, - "800": { - "$value": "#276749", - "$type": "color" - }, - "900": { - "$value": "#22543d", - "$type": "color" - } - }, - "teal": { - "100": { - "$value": "#e6fffa", - "$type": "color" - }, - "200": { - "$value": "#b2f5ea", - "$type": "color" - }, - "300": { - "$value": "#81e6d9", - "$type": "color" - }, - "400": { - "$value": "#4fd1c5", - "$type": "color" - }, - "500": { - "$value": "#38b2ac", - "$type": "color" - }, - "600": { - "$value": "#319795", - "$type": "color" - }, - "700": { - "$value": "#2c7a7b", - "$type": "color" - }, - "800": { - "$value": "#285e61", - "$type": "color" - }, - "900": { - "$value": "#234e52", - "$type": "color" - } - }, - "blue": { - "100": { - "$value": "#ebf8ff", - "$type": "color" - }, - "200": { - "$value": "#bee3f8", - "$type": "color" - }, - "300": { - "$value": "#90cdf4", - "$type": "color" - }, - "400": { - "$value": "#63b3ed", - "$type": "color" - }, - "500": { - "$value": "#4299e1", - "$type": "color" - }, - "600": { - "$value": "#3182ce", - "$type": "color" - }, - "700": { - "$value": "#2b6cb0", - "$type": "color" - }, - "800": { - "$value": "#2c5282", - "$type": "color" - }, - "900": { - "$value": "#2a4365", - "$type": "color" - } - }, - "indigo": { - "100": { - "$value": "#ebf4ff", - "$type": "color" - }, - "200": { - "$value": "#c3dafe", - "$type": "color" - }, - "300": { - "$value": "#a3bffa", - "$type": "color" - }, - "400": { - "$value": "#7f9cf5", - "$type": "color" - }, - "500": { - "$value": "#667eea", - "$type": "color" - }, - "600": { - "$value": "#5a67d8", - "$type": "color" - }, - "700": { - "$value": "#4c51bf", - "$type": "color" - }, - "800": { - "$value": "#434190", - "$type": "color" - }, - "900": { - "$value": "#3c366b", - "$type": "color" - } - }, - "purple": { - "100": { - "$value": "#faf5ff", - "$type": "color" - }, - "200": { - "$value": "#e9d8fd", - "$type": "color" - }, - "300": { - "$value": "#d6bcfa", - "$type": "color" - }, - "400": { - "$value": "#b794f4", - "$type": "color" - }, - "500": { - "$value": "#9f7aea", - "$type": "color" - }, - "600": { - "$value": "#805ad5", - "$type": "color" - }, - "700": { - "$value": "#6b46c1", - "$type": "color" - }, - "800": { - "$value": "#553c9a", - "$type": "color" - }, - "900": { - "$value": "#44337a", - "$type": "color" - } - }, - "pink": { - "100": { - "$value": "#fff5f7", - "$type": "color" - }, - "200": { - "$value": "#fed7e2", - "$type": "color" - }, - "300": { - "$value": "#fbb6ce", - "$type": "color" - }, - "400": { - "$value": "#f687b3", - "$type": "color" - }, - "500": { - "$value": "#ed64a6", - "$type": "color" - }, - "600": { - "$value": "#d53f8c", - "$type": "color" - }, - "700": { - "$value": "#b83280", - "$type": "color" - }, - "800": { - "$value": "#97266d", - "$type": "color" - }, - "900": { - "$value": "#702459", - "$type": "color" - } - } - }, - "opacity": { - "low": { - "$value": "10%", - "$type": "opacity" - }, - "md": { - "$value": "50%", - "$type": "opacity" - }, - "high": { - "$value": "90%", - "$type": "opacity" - } - }, - "fontFamilies": { - "heading": { - "$value": "Inter", - "$type": "fontFamilies" - }, - "body": { - "$value": "Roboto", - "$type": "fontFamilies" - }, - "pretendard": { - "$value": "Pretendard", - "$type": "fontFamilies" - } - }, - "lineHeights": { - "0": { - "$value": 20, - "$type": "lineHeights" - }, - "1": { - "$value": 20, - "$type": "lineHeights" - }, - "2": { - "$value": 20, - "$type": "lineHeights" - }, - "3": { - "$value": 20, - "$type": "lineHeights" - }, - "4": { - "$value": 20, - "$type": "lineHeights" - }, - "5": { - "$value": 20, - "$type": "lineHeights" - }, - "6": { - "$value": 20, - "$type": "lineHeights" - }, - "heading": { - "$value": "110%", - "$type": "lineHeights" - }, - "body": { - "$value": "140%", - "$type": "lineHeights" - } - }, - "letterSpacing": { - "0": { - "$value": "-2%", - "$type": "letterSpacing" - }, - "default": { - "$value": "0", - "$type": "letterSpacing" - }, - "increased": { - "$value": "150%", - "$type": "letterSpacing" - }, - "decreased": { - "$value": "-5%", - "$type": "letterSpacing" - } - }, - "paragraphSpacing": { - "0": { - "$value": 0, - "$type": "paragraphSpacing" - }, - "h1": { - "$value": "32", - "$type": "paragraphSpacing" - }, - "h2": { - "$value": "26", - "$type": "paragraphSpacing" - } - }, - "fontWeights": { - "headingRegular": { - "$value": "Regular", - "$type": "fontWeights" - }, - "headingBold": { - "$value": "Bold", - "$type": "fontWeights" - }, - "bodyRegular": { - "$value": "Regular", - "$type": "fontWeights" - }, - "bodyBold": { - "$value": "Bold", - "$type": "fontWeights" - }, - "pretendard-0": { - "$value": "ExtraBold", - "$type": "fontWeights" - }, - "pretendard-1": { - "$value": "SemiBold", - "$type": "fontWeights" - }, - "pretendard-2": { - "$value": "Regular", - "$type": "fontWeights" - } - }, - "fontSizes": { - "h1": { - "$value": "roundTo({fontSizes.body}*1.25^5)", - "$type": "fontSizes" - }, - "h2": { - "$value": "roundTo({fontSizes.body}*1.25^4)", - "$type": "fontSizes" - }, - "h3": { - "$value": "roundTo({fontSizes.body}*1.25^3)", - "$type": "fontSizes" - }, - "h4": { - "$value": "roundTo({fontSizes.body}*1.25^2)", - "$type": "fontSizes" - }, - "h5": { - "$value": "roundTo({fontSizes.body}*1.25^1)", - "$type": "fontSizes" - }, - "h6": { - "$value": "{fontSizes.body}", - "$type": "fontSizes" - }, - "body": { - "$value": "16", - "$type": "fontSizes" - }, - "sm": { - "$value": "{fontSizes.body} * 0.85", - "$type": "fontSizes" - }, - "xs": { - "$value": "{fontSizes.body} * 0.65", - "$type": "fontSizes" - } - }, - "ai_color": { - "$value": "linear-gradient(180deg, #da8cf1 0%, #8bb1f2 100%)", - "$type": "color" - }, - "primary-lv-0": { - "$value": "#e9eeed", - "$type": "color" - }, - "primary-lv-1": { - "$value": "#d2dcdb", - "$type": "color" - }, - "primary-lv-2": { - "$value": "#a5b9b6", - "$type": "color" - }, - "primary-lv-3": { - "$value": "#789792", - "$type": "color" - }, - "primary-lv-4": { - "$value": "#4b746d", - "$type": "color" - }, - "primary-lv-5": { - "$value": "#35635c", - "$type": "color" - }, - "primary-lv-6": { - "$value": "#1e5149", - "$type": "color" - }, - "primary-lv-7": { - "$value": "#1b443d", - "$type": "color" - }, - "primary-lv-8": { - "$value": "#193833", - "$type": "color" - }, - "primary-lv-9": { - "$value": "#162a27", - "$type": "color" - }, - "color-red": { - "$value": "#f21d0d", - "$type": "color" - }, - "color-pink": { - "$value": "#e8175e", - "$type": "color" - }, - "color-magenta": { - "$value": "#b92ed1", - "$type": "color" - }, - "color-purple": { - "$value": "#6d3dc2", - "$type": "color" - }, - "color-navy": { - "$value": "#4255bd", - "$type": "color" - }, - "color-blue": { - "$value": "#0d8df2", - "$type": "color" - }, - "color-cyan": { - "$value": "#03aefc", - "$type": "color" - }, - "color-green": { - "$value": "#4db251", - "$type": "color" - }, - "color-yellow": { - "$value": "#ffbf00", - "$type": "color" - }, - "color-orange": { - "$value": "#ff9800", - "$type": "color" - }, - "color-dahong": { - "$value": "#ff3d00", - "$type": "color" - }, - "color-brown": { - "$value": "#a0705f", - "$type": "color" - }, - "color-iron": { - "$value": "#7f7f7f", - "$type": "color" - }, - "color-steel": { - "$value": "#688897", - "$type": "color" - }, - "color-red-light": { - "$value": "#fee9e7", - "$type": "color" - }, - "color-pink-light": { - "$value": "#fde8ef", - "$type": "color" - }, - "color-magenta-light": { - "$value": "#f8ebfb", - "$type": "color" - }, - "color-purple-light": { - "$value": "#f1ecf9", - "$type": "color" - }, - "color-navy-light": { - "$value": "#edeef9", - "$type": "color" - }, - "color-blue-light": { - "$value": "#e7f4fe", - "$type": "color" - }, - "color-cyan-light": { - "$value": "#e6f7ff", - "$type": "color" - }, - "color-green-light": { - "$value": "#eef8ee", - "$type": "color" - }, - "color-yellow-light": { - "$value": "#fff9e6", - "$type": "color" - }, - "color-orange-light": { - "$value": "#fff5e6", - "$type": "color" - }, - "color-dahong-light": { - "$value": "#ffece6", - "$type": "color" - }, - "color-brown-light": { - "$value": "#f6f1ef", - "$type": "color" - }, - "color-iron-light": { - "$value": "#f3f3f3", - "$type": "color" - }, - "color-steel-light": { - "$value": "#f0f4f5", - "$type": "color" - }, - "color-red-medium": { - "$value": "#faa59e", - "$type": "color" - }, - "color-pink-medium": { - "$value": "#f6a2bf", - "$type": "color" - }, - "color-magenta-medium": { - "$value": "#e3abec", - "$type": "color" - }, - "color-purple-medium": { - "$value": "#c5b1e7", - "$type": "color" - }, - "color-navy-medium": { - "$value": "#b3bbe5", - "$type": "color" - }, - "color-blue-medium": { - "$value": "#9ed1fa", - "$type": "color" - }, - "color-cyan-medium": { - "$value": "#9adffe", - "$type": "color" - }, - "color-green-medium": { - "$value": "#b8e0b9", - "$type": "color" - }, - "color-yellow-medium": { - "$value": "#ffe599", - "$type": "color" - }, - "color-orange-medium": { - "$value": "#ffd699", - "$type": "color" - }, - "color-dahong-medium": { - "$value": "#ffb199", - "$type": "color" - }, - "color-brown-medium": { - "$value": "#d9c6bf", - "$type": "color" - }, - "color-iron-medium": { - "$value": "#cccccc", - "$type": "color" - }, - "color-steel-medium": { - "$value": "#c3cfd5", - "$type": "color" - }, - "checked-color-background": { - "$value": "#03aefc1a", - "$type": "color" - }, - "headercolor": { - "$value": "linear-gradient(90deg, #193833 0%, #1e5149 100%)", - "$type": "color" - }, - "line-style": { - "$value": "linear-gradient(135deg, #ffffff 0%, #0000001a 50%, #ffffff 100%)", - "$type": "color" - }, - "box__drop-shadow": { - "$value": { - "color": "#00000029", - "type": "dropShadow", - "x": 0, - "y": 8, - "blur": 24, - "spread": 0 - }, - "$type": "boxShadow" - }, - "fontSize": { - "0": { - "$value": 12, - "$type": "fontSizes" - }, - "1": { - "$value": 14, - "$type": "fontSizes" - }, - "2": { - "$value": 16, - "$type": "fontSizes" - }, - "3": { - "$value": 20, - "$type": "fontSizes" - } - }, - "

": { - "$value": { - "fontFamily": "{fontFamilies.pretendard}", - "fontWeight": "{fontWeights.pretendard-0}", - "lineHeight": "{lineHeights.0}", - "fontSize": "{fontSize.3}", - "letterSpacing": "{letterSpacing.0}", - "paragraphSpacing": "{paragraphSpacing.0}", - "paragraphIndent": "{paragraphIndent.0}", - "textCase": "{textCase.none}", - "textDecoration": "{textDecoration.none}" - }, - "$type": "typography" - }, - "

": { - "$value": { - "fontFamily": "{fontFamilies.pretendard}", - "fontWeight": "{fontWeights.pretendard-1}", - "lineHeight": "{lineHeights.0}", - "fontSize": "{fontSize.2}", - "letterSpacing": "{letterSpacing.0}", - "paragraphSpacing": "{paragraphSpacing.0}", - "paragraphIndent": "{paragraphIndent.0}", - "textCase": "{textCase.none}", - "textDecoration": "{textDecoration.none}" - }, - "$type": "typography" - }, - "

": { - "$value": { - "fontFamily": "{fontFamilies.pretendard}", - "fontWeight": "{fontWeights.pretendard-1}", - "lineHeight": "{lineHeights.0}", - "fontSize": "{fontSize.1}", - "letterSpacing": "{letterSpacing.0}", - "paragraphSpacing": "{paragraphSpacing.0}", - "paragraphIndent": "{paragraphIndent.0}", - "textCase": "{textCase.none}", - "textDecoration": "{textDecoration.none}" - }, - "$type": "typography" - }, - "

": { - "$value": { - "fontFamily": "{fontFamilies.pretendard}", - "fontWeight": "{fontWeights.pretendard-2}", - "lineHeight": "{lineHeights.0}", - "fontSize": "{fontSize.1}", - "letterSpacing": "{letterSpacing.0}", - "paragraphSpacing": "{paragraphSpacing.0}", - "paragraphIndent": "{paragraphIndent.0}", - "textCase": "{textCase.none}", - "textDecoration": "{textDecoration.none}" - }, - "$type": "typography" - }, - "

": { - "$value": { - "fontFamily": "{fontFamilies.pretendard}", - "fontWeight": "{fontWeights.pretendard-1}", - "lineHeight": "{lineHeights.0}", - "fontSize": "{fontSize.0}", - "letterSpacing": "{letterSpacing.0}", - "paragraphSpacing": "{paragraphSpacing.0}", - "paragraphIndent": "{paragraphIndent.0}", - "textCase": "{textCase.none}", - "textDecoration": "{textDecoration.none}" - }, - "$type": "typography" - }, - "
": { - "$value": { - "fontFamily": "{fontFamilies.pretendard}", - "fontWeight": "{fontWeights.pretendard-2}", - "lineHeight": "{lineHeights.0}", - "fontSize": "{fontSize.0}", - "letterSpacing": "{letterSpacing.0}", - "paragraphSpacing": "{paragraphSpacing.0}", - "paragraphIndent": "{paragraphIndent.0}", - "textCase": "{textCase.none}", - "textDecoration": "{textDecoration.none}" - }, - "$type": "typography" - }, - "

": { - "$value": { - "fontFamily": "{fontFamilies.pretendard}", - "fontWeight": "{fontWeights.pretendard-2}", - "lineHeight": "{lineHeights.0}", - "fontSize": "{fontSize.0}", - "letterSpacing": "{letterSpacing.0}", - "paragraphSpacing": "{paragraphSpacing.0}", - "paragraphIndent": "{paragraphIndent.0}", - "textCase": "{textCase.none}", - "textDecoration": "{textDecoration.none}" - }, - "$type": "typography" - }, - "textCase": { - "none": { - "$value": "none", - "$type": "textCase" - } - }, - "textDecoration": { - "none": { - "$value": "none", - "$type": "textDecoration" - } - }, - "paragraphIndent": { - "0": { - "$value": "0px", - "$type": "dimension" - } - } - }, - "light": { - "fg": { - "default": { - "$value": "{colors.black}", - "$type": "color" - }, - "muted": { - "$value": "{colors.gray.700}", - "$type": "color" - }, - "subtle": { - "$value": "{colors.gray.500}", - "$type": "color" - } - }, - "bg": { - "default": { - "$value": "{colors.white}", - "$type": "color" - }, - "muted": { - "$value": "{colors.gray.100}", - "$type": "color" - }, - "subtle": { - "$value": "{colors.gray.200}", - "$type": "color" - } - }, - "accent": { - "default": { - "$value": "{colors.indigo.400}", - "$type": "color" - }, - "onAccent": { - "$value": "{colors.white}", - "$type": "color" - }, - "bg": { - "$value": "{colors.indigo.200}", - "$type": "color" - } - }, - "shadows": { - "default": { - "$value": "{colors.gray.900}", - "$type": "color" - } - } - }, - "dark": { - "fg": { - "default": { - "$value": "{colors.white}", - "$type": "color" - }, - "muted": { - "$value": "{colors.gray.300}", - "$type": "color" - }, - "subtle": { - "$value": "{colors.gray.500}", - "$type": "color" - } - }, - "bg": { - "default": { - "$value": "{colors.gray.900}", - "$type": "color" - }, - "muted": { - "$value": "{colors.gray.700}", - "$type": "color" - }, - "subtle": { - "$value": "{colors.gray.600}", - "$type": "color" - } - }, - "accent": { - "default": { - "$value": "{colors.indigo.600}", - "$type": "color" - }, - "onAccent": { - "$value": "{colors.white}", - "$type": "color" - }, - "bg": { - "$value": "{colors.indigo.800}", - "$type": "color" - } - }, - "shadows": { - "default": { - "$value": "rgba({colors.black}, 0.3)", - "$type": "color" - } - } - }, - "theme": { - "button": { - "primary": { - "background": { - "$value": "{accent.default}", - "$type": "color" - }, - "text": { - "$value": "{accent.onAccent}", - "$type": "color" - } - }, - "borderRadius": { - "$value": "{borderRadius.lg}", - "$type": "borderRadius" - }, - "borderWidth": { - "$value": "{dimension.sm}", - "$type": "borderWidth" - } - }, - "card": { - "borderRadius": { - "$value": "{borderRadius.lg}", - "$type": "borderRadius" - }, - "background": { - "$value": "{bg.default}", - "$type": "color" - }, - "padding": { - "$value": "{dimension.md}", - "$type": "dimension" - } - }, - "boxShadow": { - "default": { - "$value": [ - { - "x": 5, - "y": 5, - "spread": 3, - "color": "rgba({shadows.default}, 0.15)", - "blur": 5, - "$type": "dropShadow" - }, - { - "x": 4, - "y": 4, - "spread": 6, - "color": "#00000033", - "blur": 5, - "$type": "innerShadow" - } - ], - "$type": "boxShadow" - } - }, - "typography": { - "H1": { - "Bold": { - "$value": { - "fontFamily": "{fontFamilies.heading}", - "fontWeight": "{fontWeights.headingBold}", - "lineHeight": "{lineHeights.heading}", - "fontSize": "{fontSizes.h1}", - "paragraphSpacing": "{paragraphSpacing.h1}", - "letterSpacing": "{letterSpacing.decreased}" - }, - "$type": "typography" - }, - "Regular": { - "$value": { - "fontFamily": "{fontFamilies.heading}", - "fontWeight": "{fontWeights.headingRegular}", - "lineHeight": "{lineHeights.heading}", - "fontSize": "{fontSizes.h1}", - "paragraphSpacing": "{paragraphSpacing.h1}", - "letterSpacing": "{letterSpacing.decreased}" - }, - "$type": "typography" - } - }, - "H2": { - "Bold": { - "$value": { - "fontFamily": "{fontFamilies.heading}", - "fontWeight": "{fontWeights.headingBold}", - "lineHeight": "{lineHeights.heading}", - "fontSize": "{fontSizes.h2}", - "paragraphSpacing": "{paragraphSpacing.h2}", - "letterSpacing": "{letterSpacing.decreased}" - }, - "$type": "typography" - }, - "Regular": { - "$value": { - "fontFamily": "{fontFamilies.heading}", - "fontWeight": "{fontWeights.headingRegular}", - "lineHeight": "{lineHeights.heading}", - "fontSize": "{fontSizes.h2}", - "paragraphSpacing": "{paragraphSpacing.h2}", - "letterSpacing": "{letterSpacing.decreased}" - }, - "$type": "typography" - } - }, - "Body": { - "$value": { - "fontFamily": "{fontFamilies.body}", - "fontWeight": "{fontWeights.bodyRegular}", - "lineHeight": "{lineHeights.heading}", - "fontSize": "{fontSizes.body}", - "paragraphSpacing": "{paragraphSpacing.h2}" - }, - "$type": "typography" - } - } - }, - "$themes": [], - "$metadata": { - "tokenSetOrder": [ - "core", - "light", - "dark", - "theme" - ] - } +{ + "core": { + "dimension": { + "scale": { + "$value": "2", + "$type": "dimension" + }, + "xs": { + "$value": "4", + "$type": "dimension" + }, + "sm": { + "$value": "{dimension.xs} * {dimension.scale}", + "$type": "dimension" + }, + "md": { + "$value": "{dimension.sm} * {dimension.scale}", + "$type": "dimension" + }, + "lg": { + "$value": "{dimension.md} * {dimension.scale}", + "$type": "dimension" + }, + "xl": { + "$value": "{dimension.lg} * {dimension.scale}", + "$type": "dimension" + } + }, + "spacing": { + "xs": { + "$value": "{dimension.xs}", + "$type": "spacing" + }, + "sm": { + "$value": "{dimension.sm}", + "$type": "spacing" + }, + "md": { + "$value": "{dimension.md}", + "$type": "spacing" + }, + "lg": { + "$value": "{dimension.lg}", + "$type": "spacing" + }, + "xl": { + "$value": "{dimension.xl}", + "$type": "spacing" + }, + "multi-value": { + "$value": "{dimension.sm} {dimension.xl}", + "$type": "spacing", + "$description": "You can have multiple values in a single spacing token. Read more on these: https://docs.tokens.studio/available-tokens/spacing-tokens#multi-value-spacing-tokens" + } + }, + "borderRadius": { + "sm": { + "$value": "4", + "$type": "borderRadius" + }, + "lg": { + "$value": "8", + "$type": "borderRadius" + }, + "xl": { + "$value": "16", + "$type": "borderRadius" + }, + "multi-value": { + "$value": "{borderRadius.sm} {borderRadius.lg}", + "$type": "borderRadius", + "$description": "You can have multiple values in a single radius token. Read more on these: https://docs.tokens.studio/available-tokens/border-radius-tokens#single--multiple-values" + } + }, + "colors": { + "black": { + "$value": "#000000", + "$type": "color" + }, + "white": { + "$value": "#ffffff", + "$type": "color" + }, + "gray": { + "100": { + "$value": "#f7fafc", + "$type": "color" + }, + "200": { + "$value": "#edf2f7", + "$type": "color" + }, + "300": { + "$value": "#e2e8f0", + "$type": "color" + }, + "400": { + "$value": "#cbd5e0", + "$type": "color" + }, + "500": { + "$value": "#a0aec0", + "$type": "color" + }, + "600": { + "$value": "#718096", + "$type": "color" + }, + "700": { + "$value": "#4a5568", + "$type": "color" + }, + "800": { + "$value": "#2d3748", + "$type": "color" + }, + "900": { + "$value": "#1a202c", + "$type": "color" + } + }, + "red": { + "100": { + "$value": "#fff5f5", + "$type": "color" + }, + "200": { + "$value": "#fed7d7", + "$type": "color" + }, + "300": { + "$value": "#feb2b2", + "$type": "color" + }, + "400": { + "$value": "#fc8181", + "$type": "color" + }, + "500": { + "$value": "#f56565", + "$type": "color" + }, + "600": { + "$value": "#e53e3e", + "$type": "color" + }, + "700": { + "$value": "#c53030", + "$type": "color" + }, + "800": { + "$value": "#9b2c2c", + "$type": "color" + }, + "900": { + "$value": "#742a2a", + "$type": "color" + } + }, + "orange": { + "100": { + "$value": "#fffaf0", + "$type": "color" + }, + "200": { + "$value": "#feebc8", + "$type": "color" + }, + "300": { + "$value": "#fbd38d", + "$type": "color" + }, + "400": { + "$value": "#f6ad55", + "$type": "color" + }, + "500": { + "$value": "#ed8936", + "$type": "color" + }, + "600": { + "$value": "#dd6b20", + "$type": "color" + }, + "700": { + "$value": "#c05621", + "$type": "color" + }, + "800": { + "$value": "#9c4221", + "$type": "color" + }, + "900": { + "$value": "#7b341e", + "$type": "color" + } + }, + "yellow": { + "100": { + "$value": "#fffff0", + "$type": "color" + }, + "200": { + "$value": "#fefcbf", + "$type": "color" + }, + "300": { + "$value": "#faf089", + "$type": "color" + }, + "400": { + "$value": "#f6e05e", + "$type": "color" + }, + "500": { + "$value": "#ecc94b", + "$type": "color" + }, + "600": { + "$value": "#d69e2e", + "$type": "color" + }, + "700": { + "$value": "#b7791f", + "$type": "color" + }, + "800": { + "$value": "#975a16", + "$type": "color" + }, + "900": { + "$value": "#744210", + "$type": "color" + } + }, + "green": { + "100": { + "$value": "#f0fff4", + "$type": "color" + }, + "200": { + "$value": "#c6f6d5", + "$type": "color" + }, + "300": { + "$value": "#9ae6b4", + "$type": "color" + }, + "400": { + "$value": "#68d391", + "$type": "color" + }, + "500": { + "$value": "#48bb78", + "$type": "color" + }, + "600": { + "$value": "#38a169", + "$type": "color" + }, + "700": { + "$value": "#2f855a", + "$type": "color" + }, + "800": { + "$value": "#276749", + "$type": "color" + }, + "900": { + "$value": "#22543d", + "$type": "color" + } + }, + "teal": { + "100": { + "$value": "#e6fffa", + "$type": "color" + }, + "200": { + "$value": "#b2f5ea", + "$type": "color" + }, + "300": { + "$value": "#81e6d9", + "$type": "color" + }, + "400": { + "$value": "#4fd1c5", + "$type": "color" + }, + "500": { + "$value": "#38b2ac", + "$type": "color" + }, + "600": { + "$value": "#319795", + "$type": "color" + }, + "700": { + "$value": "#2c7a7b", + "$type": "color" + }, + "800": { + "$value": "#285e61", + "$type": "color" + }, + "900": { + "$value": "#234e52", + "$type": "color" + } + }, + "blue": { + "100": { + "$value": "#ebf8ff", + "$type": "color" + }, + "200": { + "$value": "#bee3f8", + "$type": "color" + }, + "300": { + "$value": "#90cdf4", + "$type": "color" + }, + "400": { + "$value": "#63b3ed", + "$type": "color" + }, + "500": { + "$value": "#4299e1", + "$type": "color" + }, + "600": { + "$value": "#3182ce", + "$type": "color" + }, + "700": { + "$value": "#2b6cb0", + "$type": "color" + }, + "800": { + "$value": "#2c5282", + "$type": "color" + }, + "900": { + "$value": "#2a4365", + "$type": "color" + } + }, + "indigo": { + "100": { + "$value": "#ebf4ff", + "$type": "color" + }, + "200": { + "$value": "#c3dafe", + "$type": "color" + }, + "300": { + "$value": "#a3bffa", + "$type": "color" + }, + "400": { + "$value": "#7f9cf5", + "$type": "color" + }, + "500": { + "$value": "#667eea", + "$type": "color" + }, + "600": { + "$value": "#5a67d8", + "$type": "color" + }, + "700": { + "$value": "#4c51bf", + "$type": "color" + }, + "800": { + "$value": "#434190", + "$type": "color" + }, + "900": { + "$value": "#3c366b", + "$type": "color" + } + }, + "purple": { + "100": { + "$value": "#faf5ff", + "$type": "color" + }, + "200": { + "$value": "#e9d8fd", + "$type": "color" + }, + "300": { + "$value": "#d6bcfa", + "$type": "color" + }, + "400": { + "$value": "#b794f4", + "$type": "color" + }, + "500": { + "$value": "#9f7aea", + "$type": "color" + }, + "600": { + "$value": "#805ad5", + "$type": "color" + }, + "700": { + "$value": "#6b46c1", + "$type": "color" + }, + "800": { + "$value": "#553c9a", + "$type": "color" + }, + "900": { + "$value": "#44337a", + "$type": "color" + } + }, + "pink": { + "100": { + "$value": "#fff5f7", + "$type": "color" + }, + "200": { + "$value": "#fed7e2", + "$type": "color" + }, + "300": { + "$value": "#fbb6ce", + "$type": "color" + }, + "400": { + "$value": "#f687b3", + "$type": "color" + }, + "500": { + "$value": "#ed64a6", + "$type": "color" + }, + "600": { + "$value": "#d53f8c", + "$type": "color" + }, + "700": { + "$value": "#b83280", + "$type": "color" + }, + "800": { + "$value": "#97266d", + "$type": "color" + }, + "900": { + "$value": "#702459", + "$type": "color" + } + } + }, + "opacity": { + "low": { + "$value": "10%", + "$type": "opacity" + }, + "md": { + "$value": "50%", + "$type": "opacity" + }, + "high": { + "$value": "90%", + "$type": "opacity" + } + }, + "fontFamilies": { + "heading": { + "$value": "Inter", + "$type": "fontFamilies" + }, + "body": { + "$value": "Roboto", + "$type": "fontFamilies" + }, + "pretendard": { + "$value": "Pretendard", + "$type": "fontFamilies" + } + }, + "lineHeights": { + "0": { + "$value": 20, + "$type": "lineHeights" + }, + "1": { + "$value": 20, + "$type": "lineHeights" + }, + "2": { + "$value": 20, + "$type": "lineHeights" + }, + "3": { + "$value": 20, + "$type": "lineHeights" + }, + "4": { + "$value": 20, + "$type": "lineHeights" + }, + "5": { + "$value": 20, + "$type": "lineHeights" + }, + "6": { + "$value": 20, + "$type": "lineHeights" + }, + "heading": { + "$value": "110%", + "$type": "lineHeights" + }, + "body": { + "$value": "140%", + "$type": "lineHeights" + } + }, + "letterSpacing": { + "0": { + "$value": "-2%", + "$type": "letterSpacing" + }, + "default": { + "$value": "0", + "$type": "letterSpacing" + }, + "increased": { + "$value": "150%", + "$type": "letterSpacing" + }, + "decreased": { + "$value": "-5%", + "$type": "letterSpacing" + } + }, + "paragraphSpacing": { + "0": { + "$value": 0, + "$type": "paragraphSpacing" + }, + "h1": { + "$value": "32", + "$type": "paragraphSpacing" + }, + "h2": { + "$value": "26", + "$type": "paragraphSpacing" + } + }, + "fontWeights": { + "headingRegular": { + "$value": "Regular", + "$type": "fontWeights" + }, + "headingBold": { + "$value": "Bold", + "$type": "fontWeights" + }, + "bodyRegular": { + "$value": "Regular", + "$type": "fontWeights" + }, + "bodyBold": { + "$value": "Bold", + "$type": "fontWeights" + }, + "pretendard-0": { + "$value": "ExtraBold", + "$type": "fontWeights" + }, + "pretendard-1": { + "$value": "SemiBold", + "$type": "fontWeights" + }, + "pretendard-2": { + "$value": "Regular", + "$type": "fontWeights" + } + }, + "fontSizes": { + "h1": { + "$value": "roundTo({fontSizes.body}*1.25^5)", + "$type": "fontSizes" + }, + "h2": { + "$value": "roundTo({fontSizes.body}*1.25^4)", + "$type": "fontSizes" + }, + "h3": { + "$value": "roundTo({fontSizes.body}*1.25^3)", + "$type": "fontSizes" + }, + "h4": { + "$value": "roundTo({fontSizes.body}*1.25^2)", + "$type": "fontSizes" + }, + "h5": { + "$value": "roundTo({fontSizes.body}*1.25^1)", + "$type": "fontSizes" + }, + "h6": { + "$value": "{fontSizes.body}", + "$type": "fontSizes" + }, + "body": { + "$value": "16", + "$type": "fontSizes" + }, + "sm": { + "$value": "{fontSizes.body} * 0.85", + "$type": "fontSizes" + }, + "xs": { + "$value": "{fontSizes.body} * 0.65", + "$type": "fontSizes" + } + }, + "ai_color": { + "$value": "linear-gradient(180deg, #da8cf1 0%, #8bb1f2 100%)", + "$type": "color" + }, + "primary-lv-0": { + "$value": "#e9eeed", + "$type": "color" + }, + "primary-lv-1": { + "$value": "#d2dcdb", + "$type": "color" + }, + "primary-lv-2": { + "$value": "#a5b9b6", + "$type": "color" + }, + "primary-lv-3": { + "$value": "#789792", + "$type": "color" + }, + "primary-lv-4": { + "$value": "#4b746d", + "$type": "color" + }, + "primary-lv-5": { + "$value": "#35635c", + "$type": "color" + }, + "primary-lv-6": { + "$value": "#1e5149", + "$type": "color" + }, + "primary-lv-7": { + "$value": "#1b443d", + "$type": "color" + }, + "primary-lv-8": { + "$value": "#193833", + "$type": "color" + }, + "primary-lv-9": { + "$value": "#162a27", + "$type": "color" + }, + "color-red": { + "$value": "#f21d0d", + "$type": "color" + }, + "color-pink": { + "$value": "#e8175e", + "$type": "color" + }, + "color-magenta": { + "$value": "#b92ed1", + "$type": "color" + }, + "color-purple": { + "$value": "#6d3dc2", + "$type": "color" + }, + "color-navy": { + "$value": "#4255bd", + "$type": "color" + }, + "color-blue": { + "$value": "#0d8df2", + "$type": "color" + }, + "color-cyan": { + "$value": "#03aefc", + "$type": "color" + }, + "color-green": { + "$value": "#4db251", + "$type": "color" + }, + "color-yellow": { + "$value": "#ffbf00", + "$type": "color" + }, + "color-orange": { + "$value": "#ff9800", + "$type": "color" + }, + "color-dahong": { + "$value": "#ff3d00", + "$type": "color" + }, + "color-brown": { + "$value": "#a0705f", + "$type": "color" + }, + "color-iron": { + "$value": "#7f7f7f", + "$type": "color" + }, + "color-steel": { + "$value": "#688897", + "$type": "color" + }, + "color-red-light": { + "$value": "#fee9e7", + "$type": "color" + }, + "color-pink-light": { + "$value": "#fde8ef", + "$type": "color" + }, + "color-magenta-light": { + "$value": "#f8ebfb", + "$type": "color" + }, + "color-purple-light": { + "$value": "#f1ecf9", + "$type": "color" + }, + "color-navy-light": { + "$value": "#edeef9", + "$type": "color" + }, + "color-blue-light": { + "$value": "#e7f4fe", + "$type": "color" + }, + "color-cyan-light": { + "$value": "#e6f7ff", + "$type": "color" + }, + "color-green-light": { + "$value": "#eef8ee", + "$type": "color" + }, + "color-yellow-light": { + "$value": "#fff9e6", + "$type": "color" + }, + "color-orange-light": { + "$value": "#fff5e6", + "$type": "color" + }, + "color-dahong-light": { + "$value": "#ffece6", + "$type": "color" + }, + "color-brown-light": { + "$value": "#f6f1ef", + "$type": "color" + }, + "color-iron-light": { + "$value": "#f3f3f3", + "$type": "color" + }, + "color-steel-light": { + "$value": "#f0f4f5", + "$type": "color" + }, + "color-red-medium": { + "$value": "#faa59e", + "$type": "color" + }, + "color-pink-medium": { + "$value": "#f6a2bf", + "$type": "color" + }, + "color-magenta-medium": { + "$value": "#e3abec", + "$type": "color" + }, + "color-purple-medium": { + "$value": "#c5b1e7", + "$type": "color" + }, + "color-navy-medium": { + "$value": "#b3bbe5", + "$type": "color" + }, + "color-blue-medium": { + "$value": "#9ed1fa", + "$type": "color" + }, + "color-cyan-medium": { + "$value": "#9adffe", + "$type": "color" + }, + "color-green-medium": { + "$value": "#b8e0b9", + "$type": "color" + }, + "color-yellow-medium": { + "$value": "#ffe599", + "$type": "color" + }, + "color-orange-medium": { + "$value": "#ffd699", + "$type": "color" + }, + "color-dahong-medium": { + "$value": "#ffb199", + "$type": "color" + }, + "color-brown-medium": { + "$value": "#d9c6bf", + "$type": "color" + }, + "color-iron-medium": { + "$value": "#cccccc", + "$type": "color" + }, + "color-steel-medium": { + "$value": "#c3cfd5", + "$type": "color" + }, + "checked-color-background": { + "$value": "#03aefc1a", + "$type": "color" + }, + "headercolor": { + "$value": "linear-gradient(90deg, #193833 0%, #1e5149 100%)", + "$type": "color" + }, + "line-style": { + "$value": "linear-gradient(135deg, #ffffff 0%, #0000001a 50%, #ffffff 100%)", + "$type": "color" + }, + "box__drop-shadow": { + "$value": { + "color": "#00000029", + "type": "dropShadow", + "x": 0, + "y": 8, + "blur": 24, + "spread": 0 + }, + "$type": "boxShadow" + }, + "fontSize": { + "0": { + "$value": 12, + "$type": "fontSizes" + }, + "1": { + "$value": 14, + "$type": "fontSizes" + }, + "2": { + "$value": 16, + "$type": "fontSizes" + }, + "3": { + "$value": 20, + "$type": "fontSizes" + } + }, + "

": { + "$value": { + "fontFamily": "{fontFamilies.pretendard}", + "fontWeight": "{fontWeights.pretendard-0}", + "lineHeight": "{lineHeights.0}", + "fontSize": "{fontSize.3}", + "letterSpacing": "{letterSpacing.0}", + "paragraphSpacing": "{paragraphSpacing.0}", + "paragraphIndent": "{paragraphIndent.0}", + "textCase": "{textCase.none}", + "textDecoration": "{textDecoration.none}" + }, + "$type": "typography" + }, + "

": { + "$value": { + "fontFamily": "{fontFamilies.pretendard}", + "fontWeight": "{fontWeights.pretendard-1}", + "lineHeight": "{lineHeights.0}", + "fontSize": "{fontSize.2}", + "letterSpacing": "{letterSpacing.0}", + "paragraphSpacing": "{paragraphSpacing.0}", + "paragraphIndent": "{paragraphIndent.0}", + "textCase": "{textCase.none}", + "textDecoration": "{textDecoration.none}" + }, + "$type": "typography" + }, + "

": { + "$value": { + "fontFamily": "{fontFamilies.pretendard}", + "fontWeight": "{fontWeights.pretendard-1}", + "lineHeight": "{lineHeights.0}", + "fontSize": "{fontSize.1}", + "letterSpacing": "{letterSpacing.0}", + "paragraphSpacing": "{paragraphSpacing.0}", + "paragraphIndent": "{paragraphIndent.0}", + "textCase": "{textCase.none}", + "textDecoration": "{textDecoration.none}" + }, + "$type": "typography" + }, + "

": { + "$value": { + "fontFamily": "{fontFamilies.pretendard}", + "fontWeight": "{fontWeights.pretendard-2}", + "lineHeight": "{lineHeights.0}", + "fontSize": "{fontSize.1}", + "letterSpacing": "{letterSpacing.0}", + "paragraphSpacing": "{paragraphSpacing.0}", + "paragraphIndent": "{paragraphIndent.0}", + "textCase": "{textCase.none}", + "textDecoration": "{textDecoration.none}" + }, + "$type": "typography" + }, + "

": { + "$value": { + "fontFamily": "{fontFamilies.pretendard}", + "fontWeight": "{fontWeights.pretendard-1}", + "lineHeight": "{lineHeights.0}", + "fontSize": "{fontSize.0}", + "letterSpacing": "{letterSpacing.0}", + "paragraphSpacing": "{paragraphSpacing.0}", + "paragraphIndent": "{paragraphIndent.0}", + "textCase": "{textCase.none}", + "textDecoration": "{textDecoration.none}" + }, + "$type": "typography" + }, + "
": { + "$value": { + "fontFamily": "{fontFamilies.pretendard}", + "fontWeight": "{fontWeights.pretendard-2}", + "lineHeight": "{lineHeights.0}", + "fontSize": "{fontSize.0}", + "letterSpacing": "{letterSpacing.0}", + "paragraphSpacing": "{paragraphSpacing.0}", + "paragraphIndent": "{paragraphIndent.0}", + "textCase": "{textCase.none}", + "textDecoration": "{textDecoration.none}" + }, + "$type": "typography" + }, + "

": { + "$value": { + "fontFamily": "{fontFamilies.pretendard}", + "fontWeight": "{fontWeights.pretendard-2}", + "lineHeight": "{lineHeights.0}", + "fontSize": "{fontSize.0}", + "letterSpacing": "{letterSpacing.0}", + "paragraphSpacing": "{paragraphSpacing.0}", + "paragraphIndent": "{paragraphIndent.0}", + "textCase": "{textCase.none}", + "textDecoration": "{textDecoration.none}" + }, + "$type": "typography" + }, + "textCase": { + "none": { + "$value": "none", + "$type": "textCase" + } + }, + "textDecoration": { + "none": { + "$value": "none", + "$type": "textDecoration" + } + }, + "paragraphIndent": { + "0": { + "$value": "0px", + "$type": "dimension" + } + } + }, + "light": { + "fg": { + "default": { + "$value": "{colors.black}", + "$type": "color" + }, + "muted": { + "$value": "{colors.gray.700}", + "$type": "color" + }, + "subtle": { + "$value": "{colors.gray.500}", + "$type": "color" + } + }, + "bg": { + "default": { + "$value": "{colors.white}", + "$type": "color" + }, + "muted": { + "$value": "{colors.gray.100}", + "$type": "color" + }, + "subtle": { + "$value": "{colors.gray.200}", + "$type": "color" + } + }, + "accent": { + "default": { + "$value": "{colors.indigo.400}", + "$type": "color" + }, + "onAccent": { + "$value": "{colors.white}", + "$type": "color" + }, + "bg": { + "$value": "{colors.indigo.200}", + "$type": "color" + } + }, + "shadows": { + "default": { + "$value": "{colors.gray.900}", + "$type": "color" + } + } + }, + "dark": { + "fg": { + "default": { + "$value": "{colors.white}", + "$type": "color" + }, + "muted": { + "$value": "{colors.gray.300}", + "$type": "color" + }, + "subtle": { + "$value": "{colors.gray.500}", + "$type": "color" + } + }, + "bg": { + "default": { + "$value": "{colors.gray.900}", + "$type": "color" + }, + "muted": { + "$value": "{colors.gray.700}", + "$type": "color" + }, + "subtle": { + "$value": "{colors.gray.600}", + "$type": "color" + } + }, + "accent": { + "default": { + "$value": "{colors.indigo.600}", + "$type": "color" + }, + "onAccent": { + "$value": "{colors.white}", + "$type": "color" + }, + "bg": { + "$value": "{colors.indigo.800}", + "$type": "color" + } + }, + "shadows": { + "default": { + "$value": "rgba({colors.black}, 0.3)", + "$type": "color" + } + } + }, + "theme": { + "button": { + "primary": { + "background": { + "$value": "{accent.default}", + "$type": "color" + }, + "text": { + "$value": "{accent.onAccent}", + "$type": "color" + } + }, + "borderRadius": { + "$value": "{borderRadius.lg}", + "$type": "borderRadius" + }, + "borderWidth": { + "$value": "{dimension.sm}", + "$type": "borderWidth" + } + }, + "card": { + "borderRadius": { + "$value": "{borderRadius.lg}", + "$type": "borderRadius" + }, + "background": { + "$value": "{bg.default}", + "$type": "color" + }, + "padding": { + "$value": "{dimension.md}", + "$type": "dimension" + } + }, + "boxShadow": { + "default": { + "$value": [ + { + "x": 5, + "y": 5, + "spread": 3, + "color": "rgba({shadows.default}, 0.15)", + "blur": 5, + "$type": "dropShadow" + }, + { + "x": 4, + "y": 4, + "spread": 6, + "color": "#00000033", + "blur": 5, + "$type": "innerShadow" + } + ], + "$type": "boxShadow" + } + }, + "typography": { + "H1": { + "Bold": { + "$value": { + "fontFamily": "{fontFamilies.heading}", + "fontWeight": "{fontWeights.headingBold}", + "lineHeight": "{lineHeights.heading}", + "fontSize": "{fontSizes.h1}", + "paragraphSpacing": "{paragraphSpacing.h1}", + "letterSpacing": "{letterSpacing.decreased}" + }, + "$type": "typography" + }, + "Regular": { + "$value": { + "fontFamily": "{fontFamilies.heading}", + "fontWeight": "{fontWeights.headingRegular}", + "lineHeight": "{lineHeights.heading}", + "fontSize": "{fontSizes.h1}", + "paragraphSpacing": "{paragraphSpacing.h1}", + "letterSpacing": "{letterSpacing.decreased}" + }, + "$type": "typography" + } + }, + "H2": { + "Bold": { + "$value": { + "fontFamily": "{fontFamilies.heading}", + "fontWeight": "{fontWeights.headingBold}", + "lineHeight": "{lineHeights.heading}", + "fontSize": "{fontSizes.h2}", + "paragraphSpacing": "{paragraphSpacing.h2}", + "letterSpacing": "{letterSpacing.decreased}" + }, + "$type": "typography" + }, + "Regular": { + "$value": { + "fontFamily": "{fontFamilies.heading}", + "fontWeight": "{fontWeights.headingRegular}", + "lineHeight": "{lineHeights.heading}", + "fontSize": "{fontSizes.h2}", + "paragraphSpacing": "{paragraphSpacing.h2}", + "letterSpacing": "{letterSpacing.decreased}" + }, + "$type": "typography" + } + }, + "Body": { + "$value": { + "fontFamily": "{fontFamilies.body}", + "fontWeight": "{fontWeights.bodyRegular}", + "lineHeight": "{lineHeights.heading}", + "fontSize": "{fontSizes.body}", + "paragraphSpacing": "{paragraphSpacing.h2}" + }, + "$type": "typography" + } + } + }, + "$themes": [], + "$metadata": { + "tokenSetOrder": [ + "core", + "light", + "dark", + "theme" + ] + } } \ No newline at end of file diff --git a/verify_swvw.py b/verify_swvw.py new file mode 100644 index 0000000..9746506 --- /dev/null +++ b/verify_swvw.py @@ -0,0 +1,28 @@ +import pymysql +from analysis_service import AnalysisService + +def verify_analysis(): + conn = pymysql.connect( + host='localhost', + user='root', + password='45278434', + database='pm_proto_test', + charset='utf8mb4', + cursorclass=pymysql.cursors.DictCursor + ) + try: + with conn.cursor() as cursor: + results = AnalysisService.get_p_zsr_analysis_logic(cursor) + print(f"Total Projects Analyzed: {len(results)}") + print("\n[Sample Project Analysis Result]") + for res in results[:5]: + print(f"Project: {res['project_nm']}") + print(f" - Log Quality Score (Semantic): {res['log_quality']}") + print(f" - AVI Score (p_war): {res['p_war']}") + print(f" - OCI Score: {res['oci_score']}") + print("-" * 30) + finally: + conn.close() + +if __name__ == "__main__": + verify_analysis()