jeongkee commited on
Commit
e4fcc0b
ยท
verified ยท
1 Parent(s): 37f70d5

Upload 5 files

Browse files
Files changed (5) hide show
  1. .gitignore +50 -0
  2. .python-version +1 -0
  3. README.md +89 -7
  4. app.py +1935 -0
  5. requirements.txt +10 -0
.gitignore ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ build/
8
+ develop-eggs/
9
+ dist/
10
+ downloads/
11
+ eggs/
12
+ .eggs/
13
+ lib/
14
+ lib64/
15
+ parts/
16
+ sdist/
17
+ var/
18
+ wheels/
19
+ *.egg-info/
20
+ .installed.cfg
21
+ *.egg
22
+
23
+ # Virtual Environment
24
+ venv/
25
+ ENV/
26
+ env/
27
+ .venv
28
+
29
+ # IDE
30
+ .vscode/
31
+ .idea/
32
+ *.swp
33
+ *.swo
34
+ *~
35
+
36
+ # OS
37
+ .DS_Store
38
+ Thumbs.db
39
+
40
+ # Secrets
41
+ .env
42
+ *.pem
43
+ *.key
44
+
45
+ # Gradio
46
+ gradio_cached_examples/
47
+ flagged/
48
+
49
+ # Logs
50
+ *.log
.python-version ADDED
@@ -0,0 +1 @@
 
 
1
+ 3.10
README.md CHANGED
@@ -1,14 +1,96 @@
1
  ---
2
- title: CompositeAI
3
- emoji: ๐Ÿ†
4
- colorFrom: indigo
5
- colorTo: green
6
  sdk: gradio
7
- sdk_version: 6.2.0
8
  app_file: app.py
9
  pinned: false
10
  license: mit
11
- short_description: compositeAI
12
  ---
13
 
14
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: POSCO DX MRO Composite AI
3
+ emoji: ๐Ÿญ
4
+ colorFrom: blue
5
+ colorTo: purple
6
  sdk: gradio
7
+ sdk_version: 4.44.1
8
  app_file: app.py
9
  pinned: false
10
  license: mit
 
11
  ---
12
 
13
+ # ๐Ÿญ POSCO DX - MRO Composite AI
14
+
15
+ ## ์—…๋ฌด ํ”„๋กœ์„ธ์Šค ์ž๋™ํ™” + AI ์˜์‚ฌ๊ฒฐ์ • ์ง€์› ์‹œ์Šคํ…œ
16
+
17
+ ์ด ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์€ POSCO์˜ MRO(Maintenance, Repair, Operations) ํ”„๋กœ์„ธ์Šค๋ฅผ ์ž๋™ํ™”ํ•˜๊ณ  ์ตœ์ ํ™”ํ•˜๋Š” 3-Agent Collaboration ์‹œ์Šคํ…œ์ž…๋‹ˆ๋‹ค.
18
+
19
+ ### ๐ŸŽฏ ์ฃผ์š” ๊ธฐ๋Šฅ
20
+
21
+ 1. **MRO ์šด์˜ ์ž๋™ํ™”**
22
+ - ์„ค๋น„-๋ถ€ํ’ˆ ์ž๋™ ๋งค์นญ
23
+ - ์ „์‚ฌ ์žฌ๊ณ  ์‹ค์‹œ๊ฐ„ ์กฐํšŒ
24
+ - ๋ฐœ์ฃผ ํ•„์š”์„ฑ ์ž๋™ ํŒ๋‹จ
25
+
26
+ 2. **๊ตฌ๋งค/์กฐ๋‹ฌ ์ตœ์ ํ™”**
27
+ - ๊ณต๊ธ‰์—…์ฒด ์ž๋™ ๋น„๊ต
28
+ - Neuro-Symbolic AI ๊ทœ์ • ๊ฒ€์ฆ
29
+ - Linear Programming ์ตœ์ ํ™”
30
+
31
+ 3. **๊ฒฝ์˜์ง„ ์˜์‚ฌ๊ฒฐ์ • ์ง€์›**
32
+ - ์‹ค์‹œ๊ฐ„ KPI ๋Œ€์‹œ๋ณด๋“œ
33
+ - Action Items ์ž๋™ ์ƒ์„ฑ
34
+ - ๊ฐ์‚ฌ ์ถ”์  (Audit Trail)
35
+
36
+ ### ๐Ÿš€ ์‚ฌ์šฉ ๋ฐฉ๋ฒ•
37
+
38
+ #### 1. ๊ธฐ๋ณธ ์‚ฌ์šฉ (Demo Mode)
39
+
40
+ API ํ‚ค ์—†์ด๋„ ๋ฐ๋ชจ ๋ฐ์ดํ„ฐ๋กœ ์‹œ์Šคํ…œ์˜ ๋ชจ๋“  ๊ธฐ๋Šฅ์„ ์ฒดํ—˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:
41
+
42
+ 1. ์‹œ๋‚˜๋ฆฌ์˜ค ์„ ํƒ (๊ธด๊ธ‰ ๊ณ ์žฅ ๋Œ€์‘ / ์ •๊ธฐ ๋ฐœ์ฃผ ๊ณ„ํš / ๊ทœ์ • ์ค€์ˆ˜ ๊ฒ€์ฆ)
43
+ 2. ํŒŒ๋ผ๋ฏธํ„ฐ ํ™•์ธ (์„ค๋น„ ID, ํ’ˆ๋ชฉ ID, ์ˆ˜๋Ÿ‰)
44
+ 3. "๐Ÿš€ Composite AI ๋ถ„์„ ์‹คํ–‰" ๋ฒ„ํŠผ ํด๋ฆญ
45
+ 4. ๊ฐ ํƒญ์—์„œ ๊ฒฐ๊ณผ ํ™•์ธ
46
+
47
+ #### 2. OpenAI ๊ธฐ๋Šฅ ํ™œ์„ฑํ™”
48
+
49
+ LLM ๊ธฐ๋ฐ˜ AI ๋ถ„์„์„ ์‚ฌ์šฉํ•˜๋ ค๋ฉด:
50
+
51
+ 1. **Space Settings**๋กœ ์ด๋™
52
+ 2. **Secrets** ์„น์…˜์—์„œ ์ƒˆ Secret ์ถ”๊ฐ€:
53
+ - Name: `OPENAI_API_KEY`
54
+ - Value: `sk-...` (๊ท€ํ•˜์˜ OpenAI API ํ‚ค)
55
+ 3. Space๋ฅผ ์žฌ์‹œ์ž‘ํ•˜๋ฉด ์ž๋™์œผ๋กœ API ํ‚ค๊ฐ€ ๋กœ๋“œ๋ฉ๋‹ˆ๋‹ค
56
+
57
+ ### ๐Ÿ“Š ๊ธฐ๋Œ€ ํšจ๊ณผ
58
+
59
+ - โฑ๏ธ **์ฒ˜๋ฆฌ ์‹œ๊ฐ„**: ๊ธฐ์กด 3-5์ผ โ†’ **1์‹œ๊ฐ„ ์ด๋‚ด**
60
+ - ๐Ÿ’ฐ **๋น„์šฉ ์ ˆ๊ฐ**: ํ‰๊ท  **15-25%** ๊ตฌ๋งค ๋น„์šฉ ์ ˆ๊ฐ
61
+ - โš–๏ธ **์ปดํ”Œ๋ผ์ด์–ธ์Šค**: **100%** ๊ทœ์ • ์ค€์ˆ˜
62
+ - ๐Ÿ“ˆ **ํšจ์œจ์„ฑ**: ๋‹ด๋‹น์ž ์—…๋ฌด ์‹œ๊ฐ„ **60%** ๋‹จ์ถ•
63
+
64
+ ### ๐Ÿ› ๏ธ ๊ธฐ์ˆ  ์Šคํƒ
65
+
66
+ - **Frontend**: Gradio 4.0+
67
+ - **Data Processing**: Pandas, NumPy
68
+ - **Visualization**: Plotly
69
+ - **Optimization**: PuLP (Linear Programming)
70
+ - **AI/LLM**: OpenAI GPT-4o-mini (optional)
71
+ - **Workflow**: LangGraph
72
+
73
+ ### ๐Ÿ“ฆ ์„ค์น˜ ๋ฐฉ๋ฒ• (๋กœ์ปฌ)
74
+
75
+ ```bash
76
+ pip install -r requirements.txt
77
+ python app.py
78
+ ```
79
+
80
+ ### ๐Ÿ” ๋ณด์•ˆ ์ฐธ๊ณ ์‚ฌํ•ญ
81
+
82
+ - API ํ‚ค๋Š” ์ ˆ๋Œ€ ์ฝ”๋“œ์— ํ•˜๋“œ์ฝ”๋”ฉํ•˜์ง€ ๋งˆ์„ธ์š”
83
+ - Hugging Face Spaces์˜ Secrets ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•˜์„ธ์š”
84
+ - ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ๋Š” ์ ์ ˆํ•œ ์ ‘๊ทผ ์ œ์–ด๋ฅผ ๊ตฌํ˜„ํ•˜์„ธ์š”
85
+
86
+ ### ๐Ÿ“„ ๋ผ์ด์„ ์Šค
87
+
88
+ ์ด ํ”„๋กœ์ ํŠธ๋Š” ๋ฐ๋ชจ ๋ชฉ์ ์œผ๋กœ ์ œ๊ณต๋ฉ๋‹ˆ๋‹ค.
89
+
90
+ ### ๐Ÿ‘ค ๋ฌธ์˜
91
+
92
+ ํ”„๋กœ์ ํŠธ ๊ด€๋ จ ๋ฌธ์˜์‚ฌํ•ญ์€ ์ด์Šˆ๋ฅผ ํ†ตํ•ด ๋‚จ๊ฒจ์ฃผ์„ธ์š”.
93
+
94
+ ---
95
+
96
+ **Created by G-Mission AI Team**
app.py ADDED
@@ -0,0 +1,1935 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # =========================================================
2
+ # POSCO DX - MRO Composite AI - PROCESS GUIDE ENHANCED
3
+ # ์—…๋ฌด ํ”„๋กœ์„ธ์Šค ๊ฐ€์ด๋“œ ํ†ตํ•ฉ ๋ฒ„์ „ - Hugging Face Spaces ๋ฐฐํฌ์šฉ
4
+ # =========================================================
5
+
6
+ import os
7
+ import json
8
+ import time
9
+ import random
10
+ import traceback
11
+ from dataclasses import dataclass
12
+ from typing import Dict, Any, List, Optional, Tuple, TypedDict
13
+ from datetime import datetime, timedelta
14
+
15
+ import numpy as np
16
+ import pandas as pd
17
+ import networkx as nx
18
+
19
+ # โœ… Plotly imports
20
+ import plotly
21
+ import plotly.graph_objects as go
22
+ import plotly.express as px
23
+ from plotly.subplots import make_subplots
24
+
25
+ print(f"โœ… NumPy: {np.__version__}")
26
+ print(f"โœ… Pandas: {pd.__version__}")
27
+ print(f"โœ… Plotly: {plotly.__version__}")
28
+
29
+ try:
30
+ from pulp import LpProblem, LpMinimize, LpVariable, lpSum, LpStatus
31
+ PULP_AVAILABLE = True
32
+ print("โœ… PuLP available")
33
+ except ImportError:
34
+ print("โš ๏ธ PuLP not available")
35
+ PULP_AVAILABLE = False
36
+
37
+ import gradio as gr
38
+ print(f"โœ… Gradio: {gr.__version__}")
39
+
40
+ try:
41
+ from langgraph.graph import StateGraph, END
42
+ LANGGRAPH_AVAILABLE = True
43
+ print("โœ… LangGraph available")
44
+ except ImportError:
45
+ print("โš ๏ธ LangGraph not available")
46
+ LANGGRAPH_AVAILABLE = False
47
+
48
+ try:
49
+ from openai import OpenAI
50
+ OPENAI_AVAILABLE = True
51
+ print("โœ… OpenAI available")
52
+ except ImportError:
53
+ print("โš ๏ธ OpenAI not available")
54
+ OPENAI_AVAILABLE = False
55
+
56
+ # =========================================================
57
+ # API Key Configuration for Hugging Face Spaces
58
+ # =========================================================
59
+ # Hugging Face Spaces์—์„œ ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋กœ API ํ‚ค ๋กœ๋“œ
60
+ OPENAI_API_KEY = os.environ.get('OPENAI_API_KEY', '').strip()
61
+
62
+ if OPENAI_API_KEY:
63
+ os.environ['OPENAI_API_KEY'] = OPENAI_API_KEY
64
+ print("โœ… OpenAI API Key loaded from environment")
65
+ else:
66
+ print("โš ๏ธ DEMO MODE - No API Key found")
67
+ print("๐Ÿ’ก To use OpenAI features, add OPENAI_API_KEY to your Hugging Face Space Secrets")
68
+
69
+ print("\n" + "=" * 60)
70
+ print("โœ… ํ”„๋กœ์„ธ์Šค ๊ฐ€์ด๋“œ ํ†ตํ•ฉ ๋ฒ„์ „ ์ดˆ๊ธฐํ™” ์™„๋ฃŒ!")
71
+ print("=" * 60 + "\n")
72
+
73
+ # =========================================================
74
+ # Process Guide Configuration
75
+ # =========================================================
76
+ PROCESS_WORKFLOWS = {
77
+ "mro": {
78
+ "title": "๐Ÿ”ง MRO ์šด์˜ ํ”„๋กœ์„ธ์Šค",
79
+ "steps": [
80
+ {
81
+ "id": "1",
82
+ "name": "๊ณ ์žฅ/์ •๋น„ ์š”์ฒญ ์ ‘์ˆ˜",
83
+ "description": "์„ค๋น„ ๊ณ ์žฅ ๋˜๋Š” ์˜ˆ๋ฐฉ์ •๋น„ ์š”์ฒญ์„ ์ ‘์ˆ˜ํ•ฉ๋‹ˆ๋‹ค",
84
+ "input": "์„ค๋น„ ID, ๊ณ ์žฅ ์œ ํ˜•, ์šฐ์„ ์ˆœ์œ„",
85
+ "output": "์š”์ฒญ ๋ฒˆํ˜ธ, ์„ค๋น„ ์ƒ์„ธ์ •๋ณด",
86
+ "owner": "ํ˜„์žฅ ๋‹ด๋‹น์ž โ†’ MROํŒ€",
87
+ "duration": "5๋ถ„"
88
+ },
89
+ {
90
+ "id": "2",
91
+ "name": "์„ค๋น„ ์ •๋ณด ์กฐํšŒ",
92
+ "description": "Knowledge Graph์—์„œ ์„ค๋น„ ์ƒ์„ธ ์ •๋ณด๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค",
93
+ "input": "์„ค๋น„ ID",
94
+ "output": "์„ค๋น„๋ช…, ์œ„์น˜, ์ค‘์š”๋„, ์ •๋น„์ด๋ ฅ",
95
+ "owner": "MROํŒ€ (AI ์ž๋™)",
96
+ "duration": "1๋ถ„"
97
+ },
98
+ {
99
+ "id": "3",
100
+ "name": "ํ˜ธํ™˜ ๋ถ€ํ’ˆ ์ž๋™ ๋งค์นญ",
101
+ "description": "์„ค๋น„์™€ ํ˜ธํ™˜๋˜๋Š” ๋ชจ๋“  ๋ถ€ํ’ˆ์„ ์ž๋™์œผ๋กœ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค",
102
+ "input": "์„ค๋น„ ID, ์„ค๋น„ ํƒ€์ž…",
103
+ "output": "ํ˜ธํ™˜ ๋ถ€ํ’ˆ ๋ฆฌ์ŠคํŠธ, ํ•„์ˆ˜/์„ ํƒ ๊ตฌ๋ถ„",
104
+ "owner": "MROํŒ€ (AI ์ž๋™)",
105
+ "duration": "2๋ถ„"
106
+ },
107
+ {
108
+ "id": "4",
109
+ "name": "์ „์‚ฌ ์žฌ๊ณ  ํ˜„ํ™ฉ ํ™•์ธ",
110
+ "description": "๋ณธ์‚ฌ ๋ฐ ๊ฐ ์ œ์ฒ ์†Œ์˜ ์žฌ๊ณ  ํ˜„ํ™ฉ์„ ์‹ค์‹œ๊ฐ„ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค",
111
+ "input": "ํ’ˆ๋ชฉ ID",
112
+ "output": "์ฐฝ๊ณ ๋ณ„ ์žฌ๊ณ ๋Ÿ‰, ์•ˆ์ „์žฌ๊ณ , ์˜ˆ์•ฝ์ˆ˜๋Ÿ‰",
113
+ "owner": "MROํŒ€ (AI ์ž๋™)",
114
+ "duration": "1๋ถ„"
115
+ },
116
+ {
117
+ "id": "5",
118
+ "name": "๋ฐœ์ฃผ ํ•„์š”์„ฑ ํŒ๋‹จ",
119
+ "description": "์žฌ๊ณ  ๋ถ€์กฑ ์‹œ ๋ฐœ์ฃผ ์š”์ฒญ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค",
120
+ "input": "ํ˜„์žฌ๊ณ , ์•ˆ์ „์žฌ๊ณ , ์ˆ˜์š”๋Ÿ‰",
121
+ "output": "๋ฐœ์ฃผ ํ•„์š” ์—ฌ๋ถ€, ๋ฐœ์ฃผ ์ˆ˜๋Ÿ‰",
122
+ "owner": "MROํŒ€",
123
+ "duration": "3๋ถ„"
124
+ },
125
+ {
126
+ "id": "6",
127
+ "name": "๊ตฌ๋งคํŒ€ ๋ฐœ์ฃผ ์š”์ฒญ",
128
+ "description": "๊ตฌ๋งคํŒ€์— ๋ฐœ์ฃผ ์š”์ฒญ์„œ๋ฅผ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค",
129
+ "input": "ํ’ˆ๋ชฉ ์ •๋ณด, ์ˆ˜๋Ÿ‰, ๋‚ฉ๊ธฐ ์š”๊ตฌ์‚ฌํ•ญ",
130
+ "output": "๋ฐœ์ฃผ ์š”์ฒญ ๋ฒˆํ˜ธ",
131
+ "owner": "MROํŒ€ โ†’ ๊ตฌ๋งคํŒ€",
132
+ "duration": "2๋ถ„"
133
+ }
134
+ ],
135
+ "total_duration": "์•ฝ 15๋ถ„",
136
+ "success_criteria": [
137
+ "โœ“ ์„ค๋น„ ์ •๋ณด ์ •ํ™•๏ฟฝ๏ฟฝ ์‹๋ณ„",
138
+ "โœ“ ํ˜ธํ™˜ ๋ถ€ํ’ˆ 100% ๋งค์นญ",
139
+ "โœ“ ์žฌ๊ณ  ํ˜„ํ™ฉ ์‹ค์‹œ๊ฐ„ ๋ฐ˜์˜",
140
+ "โœ“ ๋ฐœ์ฃผ ์ˆ˜๋Ÿ‰ ์ตœ์ ํ™”"
141
+ ]
142
+ },
143
+ "procurement": {
144
+ "title": "๐Ÿ’ฐ ๊ตฌ๋งค/์กฐ๋‹ฌ ํ”„๋กœ์„ธ์Šค",
145
+ "steps": [
146
+ {
147
+ "id": "1",
148
+ "name": "๋ฐœ์ฃผ ์š”์ฒญ ์ ‘์ˆ˜",
149
+ "description": "MROํŒ€์œผ๋กœ๋ถ€ํ„ฐ ๋ฐœ์ฃผ ์š”์ฒญ์„ ์ ‘์ˆ˜ํ•ฉ๋‹ˆ๋‹ค",
150
+ "input": "๋ฐœ์ฃผ ์š”์ฒญ์„œ, ํ’ˆ๋ชฉ, ์ˆ˜๋Ÿ‰, ๋‚ฉ๊ธฐ",
151
+ "output": "๊ตฌ๋งค ์ž‘์—… ๋ฒˆํ˜ธ",
152
+ "owner": "๊ตฌ๋งคํŒ€",
153
+ "duration": "3๋ถ„"
154
+ },
155
+ {
156
+ "id": "2",
157
+ "name": "๊ณต๊ธ‰์—…์ฒด ์ •๋ณด ์กฐํšŒ",
158
+ "description": "ํ’ˆ๋ชฉ๋ณ„ ๋“ฑ๋ก๋œ ๋ชจ๋“  ๊ณต๊ธ‰์—…์ฒด๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค",
159
+ "input": "ํ’ˆ๋ชฉ ID",
160
+ "output": "๊ณต๊ธ‰์—…์ฒด ๋ฆฌ์ŠคํŠธ, ๋‹จ๊ฐ€, ๋‚ฉ๊ธฐ, ESG๋“ฑ๊ธ‰",
161
+ "owner": "๊ตฌ๋งคํŒ€ (AI ์ž๋™)",
162
+ "duration": "2๋ถ„"
163
+ },
164
+ {
165
+ "id": "3",
166
+ "name": "๊ทœ์ • ์ค€์ˆ˜ ๊ฒ€์ฆ",
167
+ "description": "Neuro-Symbolic AI๋กœ ๊ตฌ๋งค ๊ทœ์ •์„ ์ž๋™ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค",
168
+ "input": "ํ’ˆ๋ชฉ ์†์„ฑ, ๊ณต๊ธ‰์—…์ฒด ์ •๋ณด",
169
+ "output": "๊ทœ์ • ์œ„๋ฐ˜ ์—ฌ๋ถ€, ์ฐจ๋‹จ/๊ฒฝ๊ณ  ๋ฆฌ์ŠคํŠธ",
170
+ "owner": "๊ตฌ๋งคํŒ€ (AI ์ž๋™)",
171
+ "duration": "1๋ถ„"
172
+ },
173
+ {
174
+ "id": "4",
175
+ "name": "์ตœ์  ๋ฐฐ๋ถ„ ๊ณ„์‚ฐ",
176
+ "description": "Linear Programming์œผ๋กœ ์ตœ์  ๋ฐœ์ฃผ ๊ณ„ํš์„ ์ˆ˜๋ฆฝํ•ฉ๋‹ˆ๋‹ค",
177
+ "input": "๊ณต๊ธ‰์—…์ฒด ์˜คํผ, ์ˆ˜์š”๋Ÿ‰, ์ œ์•ฝ์กฐ๊ฑด",
178
+ "output": "์—…์ฒด๋ณ„ ๋ฐœ์ฃผ๋Ÿ‰, ์ด ๋น„์šฉ, ์˜ˆ์ƒ ๋‚ฉ๊ธฐ",
179
+ "owner": "๊ตฌ๋งคํŒ€ (AI ์ž๋™)",
180
+ "duration": "2๋ถ„"
181
+ },
182
+ {
183
+ "id": "5",
184
+ "name": "๋ฐœ์ฃผ ์ „๋žต ์ˆ˜๋ฆฝ",
185
+ "description": "LLM์ด ์ตœ์ ํ™” ๊ฒฐ๊ณผ๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ๊ตฌ๋งค ์ „๋žต์„ ์ œ์•ˆํ•ฉ๋‹ˆ๋‹ค",
186
+ "input": "์ตœ์ ํ™” ๊ฒฐ๊ณผ, ์‹œ์žฅ ์ƒํ™ฉ",
187
+ "output": "๋ฐœ์ฃผ ์ „๋žต, ๋ฆฌ์Šคํฌ ๋ถ„์„, ๋Œ€์•ˆ",
188
+ "owner": "๊ตฌ๋งคํŒ€ (AI ์ง€์›)",
189
+ "duration": "5๋ถ„"
190
+ },
191
+ {
192
+ "id": "6",
193
+ "name": "๊ฒฝ์˜์ง„ ์Šน์ธ ์š”์ฒญ",
194
+ "description": "๋ฐœ์ฃผ ๊ณ„ํš์„ ๊ฒฝ์˜์ง„์—๊ฒŒ ์Šน์ธ ์š”์ฒญํ•ฉ๋‹ˆ๋‹ค",
195
+ "input": "๋ฐœ์ฃผ ๊ณ„ํš์„œ, ๋น„์šฉ ๋ถ„์„",
196
+ "output": "์Šน์ธ ์š”์ฒญ ๋ฒˆํ˜ธ",
197
+ "owner": "๊ตฌ๋งคํŒ€ โ†’ ๊ฒฝ์˜์ง„",
198
+ "duration": "3๋ถ„"
199
+ },
200
+ {
201
+ "id": "7",
202
+ "name": "PO ๋ฐœํ–‰ (์Šน์ธ ํ›„)",
203
+ "description": "์Šน์ธ ํ›„ ๊ณต๊ธ‰์—…์ฒด์— ์ •์‹ ๋ฐœ์ฃผ์„œ๋ฅผ ๋ฐœํ–‰ํ•ฉ๋‹ˆ๋‹ค",
204
+ "input": "์Šน์ธ๋œ ๋ฐœ์ฃผ ๊ณ„ํš",
205
+ "output": "PO ๋ฒˆํ˜ธ, ๊ณ„์•ฝ์„œ",
206
+ "owner": "๊ตฌ๋งคํŒ€",
207
+ "duration": "10๋ถ„"
208
+ }
209
+ ],
210
+ "total_duration": "์•ฝ 25๋ถ„ (์Šน์ธ ๋Œ€๊ธฐ ์ œ์™ธ)",
211
+ "success_criteria": [
212
+ "โœ“ ๊ทœ์ • 100% ์ค€์ˆ˜",
213
+ "โœ“ ๋น„์šฉ ์ตœ์ ํ™” ๋‹ฌ์„ฑ",
214
+ "โœ“ ๋‚ฉ๊ธฐ ์š”๊ตฌ์‚ฌํ•ญ ์ถฉ์กฑ",
215
+ "โœ“ ESG ๋“ฑ๊ธ‰ ๊ธฐ์ค€ ๋งŒ์กฑ"
216
+ ]
217
+ },
218
+ "executive": {
219
+ "title": "๐Ÿ‘” ๊ฒฝ์˜์ง„ ์˜์‚ฌ๊ฒฐ์ • ํ”„๋กœ์„ธ์Šค",
220
+ "steps": [
221
+ {
222
+ "id": "1",
223
+ "name": "์Šน์ธ ์š”์ฒญ ์•Œ๋ฆผ",
224
+ "description": "๋ฐœ์ฃผ ์Šน์ธ ์š”์ฒญ ์•Œ๋ฆผ์„ ์ˆ˜์‹ ํ•ฉ๋‹ˆ๋‹ค",
225
+ "input": "์Šน์ธ ์š”์ฒญ ๋ฒˆํ˜ธ, ์š”์•ฝ ์ •๋ณด",
226
+ "output": "์•Œ๋ฆผ ํ™•์ธ",
227
+ "owner": "์‹œ์Šคํ…œ โ†’ ๊ฒฝ์˜์ง„",
228
+ "duration": "์ฆ‰์‹œ"
229
+ },
230
+ {
231
+ "id": "2",
232
+ "name": "KPI ๋Œ€์‹œ๋ณด๋“œ ํ™•์ธ",
233
+ "description": "์‹ค์‹œ๊ฐ„ KPI ๋Œ€์‹œ๋ณด๋“œ๋ฅผ ํ†ตํ•ด ์ „๋ฐ˜์  ํ˜„ํ™ฉ์„ ํŒŒ์•…ํ•ฉ๋‹ˆ๋‹ค",
234
+ "input": "์—†์Œ",
235
+ "output": "๋น„์šฉ์ ˆ๊ฐ๋ฅ , ์ปดํ”Œ๋ผ์ด์–ธ์Šค, ESG์ ์ˆ˜ ๋“ฑ",
236
+ "owner": "๊ฒฝ์˜์ง„",
237
+ "duration": "2๋ถ„"
238
+ },
239
+ {
240
+ "id": "3",
241
+ "name": "Action Items ๊ฒ€ํ† ",
242
+ "description": "์šฐ์„ ์ˆœ์œ„๋ณ„ ์กฐ์น˜ ํ•ญ๋ชฉ์„ ๊ฒ€ํ† ํ•ฉ๋‹ˆ๋‹ค",
243
+ "input": "Action Items ๋ฆฌ์ŠคํŠธ",
244
+ "output": "๊ฒ€ํ†  ์˜๊ฒฌ",
245
+ "owner": "๊ฒฝ์˜์ง„",
246
+ "duration": "5๋ถ„"
247
+ },
248
+ {
249
+ "id": "4",
250
+ "name": "๋ฐœ์ฃผ ์ƒ์„ธ ๋ถ„์„",
251
+ "description": "๋ฐœ์ฃผ ๊ณ„ํš์˜ ํƒ€๋‹น์„ฑ์„ ๋ฉด๋ฐ€ํžˆ ๊ฒ€ํ† ํ•ฉ๋‹ˆ๋‹ค",
252
+ "input": "๋ฐœ์ฃผ ๊ณ„ํš์„œ, ์ตœ์ ํ™” ๊ฒฐ๊ณผ, ๊ทœ์ • ๊ฒ€์ฆ",
253
+ "output": "๋ถ„์„ ์˜๊ฒฌ",
254
+ "owner": "๊ฒฝ์˜์ง„",
255
+ "duration": "10๋ถ„"
256
+ },
257
+ {
258
+ "id": "5",
259
+ "name": "์˜์‚ฌ๊ฒฐ์ •",
260
+ "description": "์Šน์ธ/๋ฐ˜๋ ค/์กฐ๊ฑด๋ถ€์Šน์ธ์„ ๊ฒฐ์ •ํ•ฉ๋‹ˆ๋‹ค",
261
+ "input": "๊ฒ€ํ†  ๊ฒฐ๊ณผ",
262
+ "output": "์Šน์ธ ๊ฒฐ์ •, ํ”ผ๋“œ๋ฐฑ",
263
+ "owner": "๊ฒฝ์˜์ง„",
264
+ "duration": "3๋ถ„"
265
+ },
266
+ {
267
+ "id": "6",
268
+ "name": "ํ”ผ๋“œ๋ฐฑ ์ œ๊ณต",
269
+ "description": "๊ฐœ์„  ์ œ์•ˆ ๋˜๋Š” ์ง€์‹œ์‚ฌํ•ญ์„ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค",
270
+ "input": "์˜์‚ฌ๊ฒฐ์ • ๊ทผ๊ฑฐ",
271
+ "output": "ํ”ผ๋“œ๋ฐฑ ๋ฉ”์‹œ์ง€, ๊ฐœ์„  ๋ฐฉํ–ฅ",
272
+ "owner": "๊ฒฝ์˜์ง„ โ†’ ๊ตฌ๋งคํŒ€",
273
+ "duration": "5๋ถ„"
274
+ }
275
+ ],
276
+ "total_duration": "์•ฝ 25๋ถ„",
277
+ "success_criteria": [
278
+ "โœ“ ์ „๋žต์  ํƒ€๋‹น์„ฑ ๊ฒ€์ฆ",
279
+ "โœ“ ๋ฆฌ์Šคํฌ ์ˆ˜์šฉ ๊ฐ€๋Šฅ ์ˆ˜์ค€",
280
+ "โœ“ ์˜ˆ์‚ฐ ๋ฒ”์œ„ ๋‚ด ์ง‘ํ–‰",
281
+ "โœ“ ์žฅ๊ธฐ ๋ชฉํ‘œ ๋ถ€ํ•ฉ"
282
+ ]
283
+ }
284
+ }
285
+
286
+ # =========================================================
287
+ # Enhanced Configuration with Real Part Names
288
+ # =========================================================
289
+ SCENARIO_PRESETS = {
290
+ "๊ธด๊ธ‰ ๊ณ ์žฅ ๋Œ€์‘": {
291
+ "description": "๐Ÿšจ ํฌํ•ญ์ œ์ฒ ์†Œ ์ปจ๋ฒ ์ด์–ด ๋ฒ ์–ด๋ง ๊ธด๊ธ‰ ๊ณ ์žฅ",
292
+ "equipment_id": "CONV-PH-007",
293
+ "item_id": "",
294
+ "demand_qty": 10,
295
+ "context": "์ปจ๋ฒ ์ด์–ด ๋ฒ ์–ด๋ง ๊ณ ์žฅ์œผ๋กœ ์ƒ์‚ฐ๋ผ์ธ ์ค‘๋‹จ. ์ฆ‰์‹œ ๊ต์ฒด ํ•„์š”.",
296
+ "priority": "๊ธด๊ธ‰",
297
+ "guide": "๋ฆฌ๋“œํƒ€์ž„ ์ตœ์†Œํ™” ์šฐ์„ . ๊ตญ๋‚ด ๊ณต๊ธ‰์—…์ฒด ์šฐ์„  ๊ณ ๋ ค."
298
+ },
299
+ "์ •๊ธฐ ๋ฐœ์ฃผ ๊ณ„ํš": {
300
+ "description": "๐Ÿ“‹ ์›”๊ฐ„ ์ •๊ธฐ ๋ฐœ์ฃผ - ์œ ์••ํŽŒํ”„ ์˜ˆ๋ฐฉ์ •๋น„",
301
+ "equipment_id": "PUMP-GY-003",
302
+ "item_id": "SEAL-A45",
303
+ "demand_qty": 50,
304
+ "context": "์›”๊ฐ„ ์˜ˆ๋ฐฉ์ •๋น„ ๊ณ„ํš. ์ตœ์  ๊ฐ€๊ฒฉ ๋ฐ ์žฌ๊ณ  ๊ท ํ˜• ํ•„์š”.",
305
+ "priority": "์ •์ƒ",
306
+ "guide": "๋น„์šฉ ์ตœ์ ํ™” ์šฐ์„ . ESG ๋“ฑ๊ธ‰ ๊ณ ๋ ค."
307
+ },
308
+ "๊ทœ์ • ์ค€์ˆ˜ ๊ฒ€์ฆ": {
309
+ "description": "โš–๏ธ ๊ทœ์ œํ’ˆ๋ชฉ(ํŠน์ˆ˜ํ™”ํ•™๋ฌผ์งˆ) ๊ตฌ๋งค ๊ฒ€์ฆ",
310
+ "equipment_id": "VALVE-PH-005",
311
+ "item_id": "",
312
+ "demand_qty": 20,
313
+ "context": "ํŠน์ˆ˜ ์‹ค๋ง์žฌ ๊ตฌ๋งค. ํ•ด์™ธ๊ตฌ๋งค ์ฐจ๋‹จ ๊ทœ์ • ์ค€์ˆ˜ ํ•„์ˆ˜.",
314
+ "priority": "๊ทœ์ •์ค€์ˆ˜",
315
+ "guide": "์ปดํ”Œ๋ผ์ด์–ธ์Šค 100% ์ค€์ˆ˜. ๊ตญ๋‚ด์—…์ฒด๋งŒ ํ—ˆ์šฉ."
316
+ }
317
+ }
318
+
319
+ # Real part names and categories
320
+ REAL_PART_NAMES = {
321
+ "๋ฒ ์–ด๋ง": ["SKF 6205 ๋ณผ๋ฒ ์–ด๋ง", "NSK ์›ํ†ต๋ฒ ์–ด๋ง", "NTN ํ…Œ์ดํผ๋ฒ ์–ด๋ง"],
322
+ "์œคํ™œ์œ ": ["์‰˜ ์˜ค๋งˆ๋ผ 220", "๋ชจ๋นŒ DTE 25", "์ง€์—์Šค์นผํ…์Šค ํ„ฐ๋นˆ์œ "],
323
+ "ํ•„ํ„ฐ": ["ํ•˜์ด๋“œ๋กœ๋ฝ ์œ ์••ํ•„ํ„ฐ", "ํŒŒ์ปค ์—์–ดํ•„ํ„ฐ", "๋„๋‚œ๋“œ์Šจ ์ •๋ฐ€ํ•„ํ„ฐ"],
324
+ "๋ฒจํŠธ": ["๊ฒŒ์ด์ธ  ํŒŒ์›Œ๊ทธ๋ฆฝ ๋ฒจํŠธ", "๋ฐ˜๋„ V๋ฒจํŠธ", "์˜ตํ‹ฐ๋ฒจํŠธ ํƒ€์ด๋ฐ๋ฒจํŠธ"],
325
+ "์„ผ์„œ": ["์ง€๋ฉ˜์Šค ๊ทผ์ ‘์„ผ์„œ", "์˜ค๋ฏ€๋ก  ๊ด‘์ „์„ผ์„œ", "ํ•˜๋‹ˆ์›ฐ ์••๋ ฅ์„ผ์„œ"],
326
+ "ํŒจํ‚น": ["NOK ์˜ค๋ง", "ํŒŒ์ปค ์œ ์••์”ฐ", "๋ฐœ์นด ๊ทธ๋žœ๋“œํŒจํ‚น"],
327
+ "ํ“จ์ฆˆ": ["LS์‚ฐ์ „ MCCB", "์Šˆ๋‚˜์ด๋” ์ฐจ๋‹จ๊ธฐ", "ABB ํ“จ์ฆˆ"],
328
+ "ํ˜ธ์Šค": ["ํŒŒ์ปค ์œ ์••ํ˜ธ์Šค", "๋งŒ๋ฆฌ ๊ณ ์••ํ˜ธ์Šค", "๋ธŒ๋ฆฌ์ง€์Šคํ†ค ์‚ฐ์—…ํ˜ธ์Šค"],
329
+ "๋ณผํŠธ": ["SUS304 ์œก๊ฐ๋ณผํŠธ", "๊ณ ์žฅ๋ ฅ๋ณผํŠธ F10T", "์•ต์ปค๋ณผํŠธ M16"],
330
+ "์‹ค๋ง์žฌ": ["๋กํƒ€์ดํŠธ ์‹ค๋ž€ํŠธ", "์“ฐ๋ฆฌ๋ณธ๋“œ ์•ก์ƒํŒจํ‚น", "ํ—จ์ผˆ ๋ฐ€๋ด‰์žฌ"]
331
+ }
332
+
333
+ # Enhanced supplier info
334
+ REAL_SUPPLIERS = [
335
+ {"name": "ํฌ์Šค์ฝ”์ผ€๋ฏธ์นผ", "type": "๊ตญ๋‚ด", "esg": "A", "specialty": "ํ™”ํ•™/์œคํ™œ์œ "},
336
+ {"name": "ํšจ์„ฑ์ค‘๊ณต์—…", "type": "๊ตญ๋‚ด", "esg": "A", "specialty": "๋ฒ ์–ด๋ง/๊ธฐ๊ณ„"},
337
+ {"name": "LS์‚ฐ์ „", "type": "๊ตญ๋‚ด", "esg": "B", "specialty": "์ „๊ธฐ/์„ผ์„œ"},
338
+ {"name": "์‚ผํ™”์ฝ˜๋ด์„œ", "type": "๊ตญ๋‚ด", "esg": "B", "specialty": "์ „๊ธฐ๋ถ€ํ’ˆ"},
339
+ {"name": "ํƒœ๊ด‘์‚ฐ์—…", "type": "๊ตญ๋‚ด", "esg": "C", "specialty": "ํ˜ธ์Šค/ํŒจํ‚น"},
340
+ {"name": "ํ•œ๊ตญํŒŒ์ปค", "type": "๊ตญ๋‚ด", "esg": "A", "specialty": "์œ ์••๋ถ€ํ’ˆ"},
341
+ {"name": "๊ทธ๋ผ์ฝ”(Graco)", "type": "ํ•ด์™ธ", "esg": "B", "specialty": "์œ ์••์žฅ๋น„"},
342
+ {"name": "์—๋จธ์Šจ(Emerson)", "type": "ํ•ด์™ธ", "esg": "C", "specialty": "๋ฐธ๋ธŒ/์„ผ์„œ"}
343
+ ]
344
+
345
+ # =========================================================
346
+ # Utility Functions
347
+ # =========================================================
348
+ def now_ts() -> str:
349
+ return time.strftime("%Y-%m-%d %H:%M:%S")
350
+
351
+ def safe_json(obj: Any) -> str:
352
+ try:
353
+ return json.dumps(obj, ensure_ascii=False, indent=2)
354
+ except Exception:
355
+ return str(obj)
356
+
357
+ def format_status(status_dict: Dict[str, Any]) -> str:
358
+ lines = [
359
+ "=" * 60,
360
+ "๐Ÿ“Š ์‹œ์Šคํ…œ ์‹คํ–‰ ์ƒํƒœ",
361
+ "=" * 60,
362
+ "",
363
+ f"๐Ÿ”Œ ์—ฐ๊ฒฐ: {status_dict.get('mode', 'Unknown')}",
364
+ f"๐ŸŽฏ ์‹œ๋‚˜๋ฆฌ์˜ค: {status_dict.get('scenario', 'N/A')}",
365
+ f"โš™๏ธ ์„ค๋น„: {status_dict.get('equipment', 'N/A')}",
366
+ f"๐Ÿ“ฆ ํ’ˆ๋ชฉ: {status_dict.get('item_name', 'N/A')}",
367
+ f"๐Ÿ“Š ์ˆ˜์š”: {status_dict.get('demand', 'N/A')}๊ฐœ",
368
+ f"๐Ÿšจ ์šฐ์„ ์ˆœ์œ„: {status_dict.get('priority', 'N/A')}",
369
+ f"\nโœ… ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ: {'ํ†ต๊ณผ' if status_dict.get('tables_ok') else '์‹คํŒจ'}",
370
+ f"โฑ๏ธ ์ง„ํ–‰: {status_dict.get('progress', 'N/A')}",
371
+ "\n" + "=" * 60
372
+ ]
373
+ return "\n".join(lines)
374
+
375
+ def create_process_guide_html(process_key: str) -> str:
376
+ """์—…๋ฌด ํ”„๋กœ์„ธ์Šค ๊ฐ€์ด๋“œ๋ฅผ HTML๋กœ ์ƒ์„ฑ"""
377
+ workflow = PROCESS_WORKFLOWS.get(process_key, {})
378
+
379
+ if not workflow:
380
+ return "<p>ํ”„๋กœ์„ธ์Šค ์ •๋ณด๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.</p>"
381
+
382
+ html = f"""
383
+ <div style="font-family: 'Malgun Gothic', Arial, sans-serif; padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 10px; color: white;">
384
+ <h2 style="margin-top: 0;">{workflow['title']}</h2>
385
+ <p style="font-size: 14px; opacity: 0.9;">์ด ์†Œ์š”์‹œ๊ฐ„: <strong>{workflow['total_duration']}</strong></p>
386
+ </div>
387
+
388
+ <div style="margin-top: 20px;">
389
+ """
390
+
391
+ for step in workflow['steps']:
392
+ html += f"""
393
+ <div style="margin-bottom: 20px; padding: 15px; border-left: 4px solid #667eea; background: #f8f9fa; border-radius: 5px;">
394
+ <div style="display: flex; align-items: center; margin-bottom: 10px;">
395
+ <div style="background: #667eea; color: white; width: 30px; height: 30px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: bold; margin-right: 10px;">
396
+ {step['id']}
397
+ </div>
398
+ <h3 style="margin: 0; color: #2c3e50;">{step['name']}</h3>
399
+ <span style="margin-left: auto; background: #e3f2fd; padding: 3px 10px; border-radius: 10px; font-size: 12px; color: #1976d2;">
400
+ โฑ๏ธ {step['duration']}
401
+ </span>
402
+ </div>
403
+
404
+ <p style="margin: 10px 0; color: #555;">{step['description']}</p>
405
+
406
+ <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 10px;">
407
+ <div style="background: white; padding: 10px; border-radius: 5px; border: 1px solid #e0e0e0;">
408
+ <strong style="color: #1976d2;">๐Ÿ“ฅ ์ž…๋ ฅ:</strong><br>
409
+ <span style="font-size: 13px; color: #666;">{step['input']}</span>
410
+ </div>
411
+ <div style="background: white; padding: 10px; border-radius: 5px; border: 1px solid #e0e0e0;">
412
+ <strong style="color: #388e3c;">๐Ÿ“ค ์ถœ๋ ฅ:</strong><br>
413
+ <span style="font-size: 13px; color: #666;">{step['output']}</span>
414
+ </div>
415
+ </div>
416
+
417
+ <div style="margin-top: 10px; padding: 8px; background: white; border-radius: 5px; border: 1px solid #e0e0e0;">
418
+ <strong style="color: #f57c00;">๐Ÿ‘ค ๋‹ด๋‹น:</strong>
419
+ <span style="font-size: 13px; color: #666;">{step['owner']}</span>
420
+ </div>
421
+ </div>
422
+ """
423
+
424
+ html += """
425
+ </div>
426
+
427
+ <div style="margin-top: 30px; padding: 20px; background: #e8f5e9; border-radius: 10px; border-left: 4px solid #4caf50;">
428
+ <h3 style="margin-top: 0; color: #2e7d32;">โœ… ์„ฑ๊ณต ๊ธฐ์ค€</h3>
429
+ <ul style="margin: 0; padding-left: 20px;">
430
+ """
431
+
432
+ for criterion in workflow['success_criteria']:
433
+ html += f"<li style='margin: 5px 0; color: #1b5e20;'>{criterion}</li>"
434
+
435
+ html += """
436
+ </ul>
437
+ </div>
438
+ """
439
+
440
+ return html
441
+
442
+ # =========================================================
443
+ # Enhanced Data Generator with Real Names
444
+ # =========================================================
445
+ def generate_demo_tables(seed: int = 7) -> Dict[str, pd.DataFrame]:
446
+ """Generate realistic demo data"""
447
+ random.seed(seed)
448
+ np.random.seed(seed)
449
+
450
+ plants = pd.DataFrame([
451
+ {"plant_id": "PH", "plant_name": "ํฌํ•ญ์ œ์ฒ ์†Œ", "region": "๊ฒฝ๋ถ", "capacity": 1000},
452
+ {"plant_id": "GY", "plant_name": "๊ด‘์–‘์ œ์ฒ ์†Œ", "region": "์ „๋‚จ", "capacity": 1200},
453
+ {"plant_id": "HQ", "plant_name": "๋ณธ์‚ฌ", "region": "์„œ์šธ", "capacity": 0},
454
+ ])
455
+
456
+ # Equipment with real names
457
+ equipment = []
458
+ eq_configs = [
459
+ ("PUMP", "์œ ์••ํŽŒํ”„", ["PH", "GY"], 6),
460
+ ("CONV", "์ปจ๋ฒ ์ด์–ด", ["PH", "GY"], 4),
461
+ ("VALVE", "์ œ์–ด๋ฐธ๋ธŒ", ["PH", "GY"], 3),
462
+ ("MOTOR", "๊ตฌ๋™๋ชจํ„ฐ", ["PH", "GY"], 5),
463
+ ]
464
+
465
+ eq_id = 1
466
+ for eq_type, eq_name_kr, plants_list, count in eq_configs:
467
+ for plant in plants_list:
468
+ for i in range(1, count + 1):
469
+ equipment.append({
470
+ "equipment_id": f"{eq_type}-{plant}-{eq_id:03d}",
471
+ "equipment_name": f"{eq_name_kr}-{plant}-{i}ํ˜ธ๊ธฐ",
472
+ "plant_id": plant,
473
+ "equipment_type": eq_name_kr,
474
+ "criticality": random.choice(["๊ธด๊ธ‰", "๊ธด๊ธ‰", "์ค‘์š”", "๋ณดํ†ต"]),
475
+ "status": "๊ฐ€๋™์ค‘",
476
+ "last_maintenance": (datetime.now() - timedelta(days=random.randint(30, 180))).strftime("%Y-%m-%d"),
477
+ })
478
+ eq_id += 1
479
+ equipment = pd.DataFrame(equipment)
480
+
481
+ # Items with real part names
482
+ items = []
483
+ item_id = 1
484
+ for category, part_list in REAL_PART_NAMES.items():
485
+ for part_name in part_list:
486
+ items.append({
487
+ "item_id": f"{category[:3].upper()}-{chr(65 + (item_id % 3))}{item_id:02d}",
488
+ "item_name": part_name,
489
+ "category": category,
490
+ "uom": "EA",
491
+ "risk_class": "๊ทœ์ œ" if "ํ™”ํ•™" in part_name or "ํŠน์ˆ˜" in category else "์ผ๋ฐ˜",
492
+ "unit_weight": round(0.5 + random.random() * 5, 1),
493
+ "shelf_life_days": random.choice([365, 730, 1095, None]),
494
+ })
495
+ item_id += 1
496
+ items = pd.DataFrame(items)
497
+
498
+ # Compatibility
499
+ compat = []
500
+ for eq_idx, eq_row in equipment.iterrows():
501
+ eq_id = eq_row["equipment_id"]
502
+ eq_type = eq_row["equipment_type"]
503
+
504
+ # Match parts to equipment type
505
+ if "ํŽŒํ”„" in eq_type:
506
+ relevant_cats = ["๋ฒ ์–ด๋ง", "์œคํ™œ์œ ", "ํŒจํ‚น"]
507
+ elif "์ปจ๋ฒ ์ด์–ด" in eq_type:
508
+ relevant_cats = ["๋ฒ ์–ด๋ง", "๋ฒจํŠธ", "์„ผ์„œ"]
509
+ elif "๋ฐธ๋ธŒ" in eq_type:
510
+ relevant_cats = ["์‹ค๋ง์žฌ", "ํŒจํ‚น", "์œคํ™œ์œ "]
511
+ else:
512
+ relevant_cats = ["๋ฒ ์–ด๋ง", "์„ผ์„œ", "ํ•„ํ„ฐ"]
513
+
514
+ for cat in relevant_cats:
515
+ cat_items = items[items["category"] == cat]
516
+ if len(cat_items) > 0:
517
+ selected = cat_items.sample(min(2, len(cat_items)))
518
+ for _, item in selected.iterrows():
519
+ compat.append({
520
+ "equipment_id": eq_id,
521
+ "item_id": item["item_id"],
522
+ "is_mandatory": (cat == relevant_cats[0]),
523
+ "annual_consumption_est": random.randint(20, 200),
524
+ "failure_rate": round(random.random() * 0.05, 3),
525
+ })
526
+ compat = pd.DataFrame(compat).drop_duplicates(["equipment_id", "item_id"])
527
+
528
+ # Storages
529
+ storages = pd.DataFrame([
530
+ {"storage_id": "WH-HQ", "plant_id": "HQ", "storage_name": "๋ณธ์‚ฌ ์ค‘์•™์ฐฝ๊ณ ", "capacity": 10000},
531
+ {"storage_id": "WH-PH", "plant_id": "PH", "storage_name": "ํฌํ•ญ MRO์ฐฝ๊ณ ", "capacity": 5000},
532
+ {"storage_id": "WH-GY", "plant_id": "GY", "storage_name": "๊ด‘์–‘ MRO์ฐฝ๊ณ ", "capacity": 5000},
533
+ ])
534
+
535
+ # Inventory with realistic levels
536
+ inventory = []
537
+ for st_idx, st_row in storages.iterrows():
538
+ sampled_items = items.sample(min(25, len(items)))
539
+ for _, item in sampled_items.iterrows():
540
+ stock_level = random.randint(10, 100)
541
+ safety_stock = int(stock_level * 0.2)
542
+ inventory.append({
543
+ "storage_id": st_row["storage_id"],
544
+ "item_id": item["item_id"],
545
+ "on_hand": stock_level,
546
+ "safety_stock": safety_stock,
547
+ "reserved": random.randint(0, min(5, stock_level)),
548
+ "last_updated": (datetime.now() - timedelta(days=random.randint(1, 30))).strftime("%Y-%m-%d"),
549
+ })
550
+ inventory = pd.DataFrame(inventory)
551
+
552
+ # Suppliers with real names
553
+ suppliers = pd.DataFrame([
554
+ {
555
+ "supplier_id": f"SUP-{i:03d}",
556
+ "supplier_name": sup["name"],
557
+ "supplier_type": sup["type"],
558
+ "rating": round(3.5 + random.random() * 1.5, 1),
559
+ "esg_level": sup["esg"],
560
+ "specialty": sup["specialty"],
561
+ "region": sup["type"],
562
+ "payment_terms": random.choice(["NET30", "NET45", "NET60"]),
563
+ "established_year": random.randint(1990, 2020),
564
+ }
565
+ for i, sup in enumerate(REAL_SUPPLIERS, 1)
566
+ ])
567
+
568
+ # Supplier offers with realistic pricing
569
+ offers = []
570
+ for _, item in items.iterrows():
571
+ num_suppliers = random.randint(3, 4)
572
+ selected_sups = suppliers.sample(min(num_suppliers, len(suppliers)))
573
+
574
+ base_price = 10000 + random.randint(0, 90000)
575
+
576
+ for rank, (_, sup) in enumerate(selected_sups.iterrows()):
577
+ price_multiplier = 1.0 + (rank * 0.05) + random.uniform(-0.1, 0.1)
578
+
579
+ offers.append({
580
+ "item_id": item["item_id"],
581
+ "supplier_id": sup["supplier_id"],
582
+ "unit_price": int(base_price * price_multiplier),
583
+ "lead_time_days": 3 + rank * 2 + random.randint(0, 5),
584
+ "moq": [10, 20, 50, 100][rank % 4],
585
+ "contract_type": random.choice(["๋‹จ๊ฐ€๊ณ„์•ฝ", "์žฅ๊ธฐ๊ณ„์•ฝ", "์ŠคํŒŸ"]),
586
+ "discount_rate": round(random.random() * 0.1, 2) if rank == 0 else 0,
587
+ "quality_grade": random.choice(["A", "A", "B", "C"]),
588
+ })
589
+ supplier_offers = pd.DataFrame(offers)
590
+
591
+ # Policies
592
+ policies = pd.DataFrame([
593
+ {
594
+ "policy_id": "R-001",
595
+ "rule_name": "๊ทœ์ œํ’ˆ๋ชฉ ํ•ด์™ธ๊ตฌ๋งค ์ œํ•œ",
596
+ "rule_logic": "IF item.risk_class == '๊ทœ์ œ' AND supplier.region == 'ํ•ด์™ธ' THEN block",
597
+ "severity": "์ฐจ๋‹จ",
598
+ "department": "๋ฒ•๋ฌดํŒ€"
599
+ },
600
+ {
601
+ "policy_id": "R-002",
602
+ "rule_name": "์•ˆ์ „์žฌ๊ณ  ๋ฏธ๋งŒ ๊ธด๊ธ‰๋ฐœ์ฃผ",
603
+ "rule_logic": "IF (on_hand - reserved) < safety_stock THEN expedite",
604
+ "severity": "๊ฒฝ๊ณ ",
605
+ "department": "MROํŒ€"
606
+ },
607
+ {
608
+ "policy_id": "R-003",
609
+ "rule_name": "๊ธด๊ธ‰์„ค๋น„ ์šฐ์„ ๋ฐฐ๋ถ„",
610
+ "rule_logic": "IF equipment.criticality == '๊ธด๊ธ‰' THEN priority",
611
+ "severity": "์šฐ์„ ์ˆœ์œ„",
612
+ "department": "์ƒ์‚ฐํŒ€"
613
+ },
614
+ {
615
+ "policy_id": "R-004",
616
+ "rule_name": "ESG C๋“ฑ๊ธ‰ ์ œํ•œ",
617
+ "rule_logic": "IF supplier.esg_level == 'C' THEN penalize",
618
+ "severity": "ํŒจ๋„ํ‹ฐ",
619
+ "department": "๊ตฌ๋งคํŒ€"
620
+ },
621
+ ])
622
+
623
+ # Purchase history
624
+ purchase_history = []
625
+ for i in range(200):
626
+ item = items.sample(1).iloc[0]
627
+ supplier = suppliers.sample(1).iloc[0]
628
+ qty = random.randint(10, 100)
629
+ price = random.randint(10000, 100000)
630
+
631
+ purchase_history.append({
632
+ "po_id": f"PO-2024-{10000 + i}",
633
+ "date": (datetime.now() - timedelta(days=random.randint(1, 365))).strftime("%Y-%m-%d"),
634
+ "item_id": item["item_id"],
635
+ "supplier_id": supplier["supplier_id"],
636
+ "qty": qty,
637
+ "unit_price": price,
638
+ "total_amount": qty * price,
639
+ "delivery_status": random.choice(["์™„๋ฃŒ", "์™„๋ฃŒ", "์™„๋ฃŒ", "์ง€์—ฐ", "์ง„ํ–‰์ค‘"]),
640
+ })
641
+ purchase_history = pd.DataFrame(purchase_history)
642
+
643
+ return {
644
+ "plants": plants,
645
+ "equipment": equipment,
646
+ "items": items,
647
+ "compat": compat,
648
+ "storages": storages,
649
+ "inventory": inventory,
650
+ "suppliers": suppliers,
651
+ "supplier_offers": supplier_offers,
652
+ "policies": policies,
653
+ "purchase_history": purchase_history
654
+ }
655
+
656
+ def validate_tables(tables: Dict[str, pd.DataFrame]) -> Tuple[bool, List[str]]:
657
+ """Validate tables"""
658
+ required = ["plants", "equipment", "items", "compat", "storages", "inventory",
659
+ "suppliers", "supplier_offers", "policies", "purchase_history"]
660
+
661
+ issues = []
662
+ for k in required:
663
+ if k not in tables:
664
+ issues.append(f"Missing: {k}")
665
+ elif not isinstance(tables[k], pd.DataFrame):
666
+ issues.append(f"Invalid type: {k}")
667
+ elif len(tables[k]) == 0:
668
+ issues.append(f"Empty: {k}")
669
+
670
+ return len(issues) == 0, issues
671
+
672
+ # =========================================================
673
+ # Plotly Dashboard Functions
674
+ # =========================================================
675
+ def create_mro_inventory_dashboard(inv_df: pd.DataFrame, item_name: str) -> go.Figure:
676
+ """MRO ์žฌ๊ณ  ํ˜„ํ™ฉ ๋Œ€์‹œ๋ณด๋“œ"""
677
+ if len(inv_df) == 0:
678
+ fig = go.Figure()
679
+ fig.add_annotation(text="์žฌ๊ณ  ๋ฐ์ดํ„ฐ ์—†์Œ", showarrow=False, font_size=20)
680
+ fig.update_layout(height=700, title_text="์žฌ๊ณ  ์ •๋ณด ์—†์Œ")
681
+ return fig
682
+
683
+ # ์„œ๋ธŒํ”Œ๋กฏ ์ƒ์„ฑ
684
+ fig = make_subplots(
685
+ rows=2, cols=2,
686
+ subplot_titles=('์ฐฝ๊ณ ๋ณ„ ์žฌ๊ณ  ํ˜„ํ™ฉ', '์•ˆ์ „์žฌ๊ณ  ๋Œ€๋น„ ํ˜„์žฌ๊ณ ', '์žฌ๊ณ  ์ƒํƒœ', '์ฐฝ๊ณ ๋ณ„ ์ ์œ ์œจ'),
687
+ specs=[[{"type": "bar"}, {"type": "indicator"}],
688
+ [{"type": "pie"}, {"type": "table"}]]
689
+ )
690
+
691
+ # 1. ์ฐฝ๊ณ ๋ณ„ ์žฌ๊ณ  ๋ฐ” ์ฐจํŠธ
692
+ fig.add_trace(
693
+ go.Bar(
694
+ x=inv_df['storage_name'],
695
+ y=inv_df['on_hand'],
696
+ name='ํ˜„์žฌ๊ณ ',
697
+ marker_color='lightblue',
698
+ text=inv_df['on_hand'],
699
+ textposition='auto',
700
+ ),
701
+ row=1, col=1
702
+ )
703
+
704
+ fig.add_trace(
705
+ go.Bar(
706
+ x=inv_df['storage_name'],
707
+ y=inv_df['safety_stock'],
708
+ name='์•ˆ์ „์žฌ๊ณ ',
709
+ marker_color='orange',
710
+ text=inv_df['safety_stock'],
711
+ textposition='auto',
712
+ ),
713
+ row=1, col=1
714
+ )
715
+
716
+ # 2. ์ด ์žฌ๊ณ  ๊ฒŒ์ด์ง€
717
+ total_stock = inv_df['on_hand'].sum()
718
+ total_safety = inv_df['safety_stock'].sum()
719
+
720
+ fig.add_trace(
721
+ go.Indicator(
722
+ mode="gauge+number+delta",
723
+ value=total_stock,
724
+ delta={'reference': total_safety, 'increasing': {'color': "green"}},
725
+ title={'text': f"์ด ์žฌ๊ณ ๋Ÿ‰<br><sub>{item_name}</sub>"},
726
+ gauge={
727
+ 'axis': {'range': [0, total_safety * 2]},
728
+ 'bar': {'color': "darkblue"},
729
+ 'steps': [
730
+ {'range': [0, total_safety], 'color': "lightgray"},
731
+ {'range': [total_safety, total_safety * 1.5], 'color': "lightgreen"}
732
+ ],
733
+ 'threshold': {
734
+ 'line': {'color': "red", 'width': 4},
735
+ 'thickness': 0.75,
736
+ 'value': total_safety
737
+ }
738
+ }
739
+ ),
740
+ row=1, col=2
741
+ )
742
+
743
+ # 3. ์žฌ๊ณ  ์ƒํƒœ ํŒŒ์ด ์ฐจํŠธ
744
+ inv_df['available'] = inv_df['on_hand'] - inv_df['reserved']
745
+
746
+ fig.add_trace(
747
+ go.Pie(
748
+ labels=['๊ฐ€์šฉ์žฌ๊ณ ', '์˜ˆ์•ฝ๋จ', '์•ˆ์ „์žฌ๊ณ '],
749
+ values=[
750
+ inv_df['available'].sum(),
751
+ inv_df['reserved'].sum(),
752
+ max(0, total_safety - inv_df['available'].sum())
753
+ ],
754
+ marker_colors=['green', 'orange', 'red'],
755
+ hole=0.3,
756
+ ),
757
+ row=2, col=1
758
+ )
759
+
760
+ # 4. ์ƒ์„ธ ํ…Œ์ด๋ธ”
761
+ fig.add_trace(
762
+ go.Table(
763
+ header=dict(
764
+ values=['์ฐฝ๊ณ ', 'ํ˜„์žฌ๊ณ ', '์•ˆ์ „์žฌ๊ณ ', '์˜ˆ์•ฝ', '๊ฐ€์šฉ'],
765
+ fill_color='paleturquoise',
766
+ align='left'
767
+ ),
768
+ cells=dict(
769
+ values=[
770
+ inv_df['storage_name'],
771
+ inv_df['on_hand'],
772
+ inv_df['safety_stock'],
773
+ inv_df['reserved'],
774
+ inv_df['available']
775
+ ],
776
+ fill_color='lavender',
777
+ align='left'
778
+ )
779
+ ),
780
+ row=2, col=2
781
+ )
782
+
783
+ fig.update_layout(
784
+ height=700,
785
+ showlegend=True,
786
+ title_text=f"๐Ÿ“ฆ MRO ์žฌ๊ณ  ๋ถ„์„ ๋Œ€์‹œ๋ณด๋“œ - {item_name}",
787
+ title_font_size=20
788
+ )
789
+
790
+ return fig
791
+
792
+ def create_mro_workflow_status(equipment_info: Dict, compat_items: pd.DataFrame) -> go.Figure:
793
+ """MRO ์›Œํฌํ”Œ๋กœ์šฐ ์ƒํƒœ ์‹œ๊ฐํ™”"""
794
+ fig = go.Figure()
795
+
796
+ # ์›Œํฌํ”Œ๋กœ์šฐ ๋‹จ๊ณ„
797
+ steps = [
798
+ "์„ค๋น„ ํ™•์ธ",
799
+ "ํ˜ธํ™˜๋ถ€ํ’ˆ ์กฐํšŒ",
800
+ "์žฌ๊ณ  ํ™•์ธ",
801
+ "์ˆ˜์š” ๊ฒ€์ฆ",
802
+ "๋ฐœ์ฃผ ์š”์ฒญ"
803
+ ]
804
+
805
+ statuses = ["์™„๋ฃŒ", "์™„๋ฃŒ", "์ง„ํ–‰์ค‘", "๋Œ€๊ธฐ", "๋Œ€๊ธฐ"]
806
+ colors = ["green", "green", "orange", "gray", "gray"]
807
+
808
+ # Funnel ์ฐจํŠธ๋กœ ์›Œํฌํ”Œ๋กœ์šฐ ํ‘œํ˜„
809
+ fig.add_trace(go.Funnel(
810
+ y=steps,
811
+ x=[100, 80, 60, 40, 20],
812
+ textposition="inside",
813
+ textinfo="label+percent initial",
814
+ marker={"color": colors},
815
+ connector={"line": {"color": "royalblue", "width": 3}}
816
+ ))
817
+
818
+ equipment_name = equipment_info.get('equipment_name', 'N/A') if equipment_info else 'N/A'
819
+
820
+ fig.update_layout(
821
+ title_text=f"๐Ÿ”„ MRO ์›Œํฌํ”Œ๋กœ์šฐ ์ง„ํ–‰ ์ƒํƒœ<br><sub>์„ค๋น„: {equipment_name}</sub>",
822
+ height=400,
823
+ showlegend=False
824
+ )
825
+
826
+ return fig
827
+
828
+ def create_procurement_comparison_dashboard(offers_df: pd.DataFrame, rules_eval: Dict) -> go.Figure:
829
+ """๊ตฌ๋งค ๋‹ด๋‹น์ž - ๊ณต๊ธ‰์—…์ฒด ๋น„๊ต ๋Œ€์‹œ๋ณด๋“œ"""
830
+ if len(offers_df) == 0:
831
+ fig = go.Figure()
832
+ fig.add_annotation(text="๊ณต๊ธ‰์—…์ฒด ๋ฐ์ดํ„ฐ ์—†์Œ", showarrow=False, font_size=20)
833
+ fig.update_layout(height=700, title_text="๊ณต๊ธ‰์—…์ฒด ์ •๋ณด ์—†์Œ")
834
+ return fig
835
+
836
+ # ์„œ๋ธŒํ”Œ๋กฏ
837
+ fig = make_subplots(
838
+ rows=2, cols=2,
839
+ subplot_titles=(
840
+ '๐Ÿ’ฐ ๊ฐ€๊ฒฉ ๋น„๊ต',
841
+ 'โฑ๏ธ ๋‚ฉ๊ธฐ ๋น„๊ต',
842
+ '๐Ÿ“Š ESG ๋“ฑ๊ธ‰ ๋ถ„ํฌ',
843
+ '๐ŸŽฏ ์ข…ํ•ฉ ํ‰๊ฐ€'
844
+ ),
845
+ specs=[
846
+ [{"type": "bar"}, {"type": "scatter"}],
847
+ [{"type": "pie"}, {"type": "table"}]
848
+ ]
849
+ )
850
+
851
+ # ๊ทœ์น™ ํ‰๊ฐ€ ๊ฒฐ๊ณผ ์ถ”๊ฐ€
852
+ offers_df['blocked'] = offers_df['supplier_id'].apply(
853
+ lambda x: rules_eval.get(x, {}).get('block', False)
854
+ )
855
+ offers_df['color'] = offers_df['blocked'].apply(lambda x: 'red' if x else 'green')
856
+
857
+ # 1. ๊ฐ€๊ฒฉ ๋น„๊ต ๋ฐ” ์ฐจํŠธ
858
+ fig.add_trace(
859
+ go.Bar(
860
+ x=offers_df['supplier_name'],
861
+ y=offers_df['unit_price'],
862
+ marker_color=offers_df['color'],
863
+ text=[f"{p:,}์›" for p in offers_df['unit_price']],
864
+ textposition='auto',
865
+ name='๋‹จ๊ฐ€',
866
+ ),
867
+ row=1, col=1
868
+ )
869
+
870
+ # 2. ๊ฐ€๊ฒฉ-๋‚ฉ๊ธฐ ์Šค์บํ„ฐ
871
+ fig.add_trace(
872
+ go.Scatter(
873
+ x=offers_df['lead_time_days'],
874
+ y=offers_df['unit_price'],
875
+ mode='markers+text',
876
+ marker=dict(
877
+ size=15,
878
+ color=offers_df['color'],
879
+ line=dict(width=2, color='white')
880
+ ),
881
+ text=offers_df['supplier_name'],
882
+ textposition="top center",
883
+ name='๊ณต๊ธ‰์—…์ฒด',
884
+ ),
885
+ row=1, col=2
886
+ )
887
+
888
+ # 3. ESG ๋“ฑ๊ธ‰ ํŒŒ์ด
889
+ esg_counts = offers_df['esg_level'].value_counts()
890
+ fig.add_trace(
891
+ go.Pie(
892
+ labels=esg_counts.index,
893
+ values=esg_counts.values,
894
+ marker_colors=['lightgreen', 'lightyellow', 'lightcoral'],
895
+ hole=0.3,
896
+ ),
897
+ row=2, col=1
898
+ )
899
+
900
+ # 4. ์ข…ํ•ฉ ํ‰๊ฐ€ ํ…Œ์ด๋ธ”
901
+ evaluation = offers_df.copy()
902
+ evaluation['์ข…ํ•ฉ์ ์ˆ˜'] = (
903
+ (100 - (evaluation['unit_price'] / evaluation['unit_price'].max() * 50)) +
904
+ (100 - (evaluation['lead_time_days'] / evaluation['lead_time_days'].max() * 30)) +
905
+ evaluation['esg_level'].map({'A': 20, 'B': 10, 'C': 0})
906
+ ).round(1)
907
+ evaluation['์ˆœ์œ„'] = evaluation['์ข…ํ•ฉ์ ์ˆ˜'].rank(ascending=False).astype(int)
908
+
909
+ fig.add_trace(
910
+ go.Table(
911
+ header=dict(
912
+ values=['์ˆœ์œ„', '๊ณต๊ธ‰์—…์ฒด', '๋‹จ๊ฐ€', '๋‚ฉ๊ธฐ', 'ESG', '์ ์ˆ˜'],
913
+ fill_color='paleturquoise',
914
+ align='center'
915
+ ),
916
+ cells=dict(
917
+ values=[
918
+ evaluation['์ˆœ์œ„'],
919
+ evaluation['supplier_name'],
920
+ [f"{p:,}" for p in evaluation['unit_price']],
921
+ [f"{d}์ผ" for d in evaluation['lead_time_days']],
922
+ evaluation['esg_level'],
923
+ evaluation['์ข…ํ•ฉ์ ์ˆ˜']
924
+ ],
925
+ fill_color=[['white' if not b else 'lightcoral' for b in evaluation['blocked']]],
926
+ align='center'
927
+ )
928
+ ),
929
+ row=2, col=2
930
+ )
931
+
932
+ fig.update_layout(
933
+ height=700,
934
+ showlegend=False,
935
+ title_text="๐Ÿ“Š ๊ณต๊ธ‰์—…์ฒด ์ข…ํ•ฉ ๋น„๊ต ๋Œ€์‹œ๋ณด๋“œ",
936
+ title_font_size=20
937
+ )
938
+
939
+ fig.update_xaxes(title_text="๋‚ฉ๊ธฐ (์ผ)", row=1, col=2)
940
+ fig.update_yaxes(title_text="๋‹จ๊ฐ€ (์›)", row=1, col=2)
941
+
942
+ return fig
943
+
944
+ def create_procurement_workflow(opt_result: Dict) -> go.Figure:
945
+ """๊ตฌ๋งค ์›Œํฌํ”Œ๋กœ์šฐ ์ง„ํ–‰ ์ƒํƒœ"""
946
+ fig = go.Figure()
947
+
948
+ # ์›Œํฌํ”Œ๋กœ์šฐ ๋‹จ๊ณ„์™€ ์ƒํƒœ
949
+ workflow_steps = [
950
+ {"step": "1. ์ˆ˜์š” ์ ‘์ˆ˜", "status": "์™„๋ฃŒ", "time": "10๋ถ„"},
951
+ {"step": "2. ๊ณต๊ธ‰์—…์ฒด ์กฐํšŒ", "status": "์™„๋ฃŒ", "time": "5๋ถ„"},
952
+ {"step": "3. ๊ทœ์ • ๊ฒ€์ฆ", "status": "์™„๋ฃŒ", "time": "2๋ถ„"},
953
+ {"step": "4. ์ตœ์ ํ™” ๋ถ„์„", "status": "์™„๋ฃŒ", "time": "3๋ถ„"},
954
+ {"step": "5. ๋ฐœ์ฃผ ์Šน์ธ", "status": "๋Œ€๊ธฐ์ค‘", "time": "-"},
955
+ {"step": "6. PO ๋ฐœํ–‰", "status": "๋Œ€๊ธฐ์ค‘", "time": "-"},
956
+ ]
957
+
958
+ # Progress Bar ์Šคํƒ€์ผ
959
+ y_pos = list(range(len(workflow_steps)))
960
+ colors = []
961
+ for step_info in workflow_steps:
962
+ if step_info["status"] == "์™„๋ฃŒ":
963
+ colors.append("lightgreen")
964
+ elif step_info["status"] == "์ง„ํ–‰์ค‘":
965
+ colors.append("lightyellow")
966
+ else:
967
+ colors.append("lightgray")
968
+
969
+ fig.add_trace(go.Bar(
970
+ y=[s["step"] for s in workflow_steps],
971
+ x=[100 if s["status"] == "์™„๋ฃŒ" else 50 if s["status"] == "์ง„ํ–‰์ค‘" else 0
972
+ for s in workflow_steps],
973
+ orientation='h',
974
+ marker=dict(color=colors),
975
+ text=[f"{s['status']} ({s['time']})" for s in workflow_steps],
976
+ textposition='auto',
977
+ ))
978
+
979
+ fig.update_layout(
980
+ title_text="๐Ÿ”„ ๊ตฌ๋งค ์›Œํฌํ”Œ๋กœ์šฐ ์ง„ํ–‰ ํ˜„ํ™ฉ",
981
+ xaxis_title="์ง„ํ–‰๋ฅ  (%)",
982
+ height=400,
983
+ showlegend=False
984
+ )
985
+
986
+ return fig
987
+
988
+ def create_executive_kpi_dashboard(
989
+ opt_result: Dict,
990
+ offers_df: pd.DataFrame,
991
+ purchase_history: pd.DataFrame
992
+ ) -> go.Figure:
993
+ """๊ฒฝ์˜์ง„ KPI ๋Œ€์‹œ๋ณด๋“œ"""
994
+
995
+ fig = make_subplots(
996
+ rows=2, cols=3,
997
+ subplot_titles=(
998
+ '๐Ÿ’ฐ ๋น„์šฉ ์ ˆ๊ฐ',
999
+ 'โš–๏ธ ์ปดํ”Œ๋ผ์ด์–ธ์Šค',
1000
+ '๐Ÿ“ˆ ESG ์ ์ˆ˜',
1001
+ 'โฑ๏ธ ์ฒ˜๋ฆฌ ์‹œ๊ฐ„',
1002
+ '๐ŸŽฏ ๋ชฉํ‘œ ๋‹ฌ์„ฑ๋ฅ ',
1003
+ '๐Ÿ“Š ์›”๊ฐ„ ํŠธ๋ Œ๋“œ'
1004
+ ),
1005
+ specs=[
1006
+ [{"type": "indicator"}, {"type": "indicator"}, {"type": "indicator"}],
1007
+ [{"type": "indicator"}, {"type": "indicator"}, {"type": "scatter"}]
1008
+ ]
1009
+ )
1010
+
1011
+ # 1. ๋น„์šฉ ์ ˆ๊ฐ
1012
+ if len(offers_df) > 0:
1013
+ min_price = offers_df['unit_price'].min()
1014
+ max_price = offers_df['unit_price'].max()
1015
+ savings = ((max_price - min_price) / max_price * 100) if max_price > 0 else 0
1016
+ else:
1017
+ savings = 0
1018
+
1019
+ fig.add_trace(
1020
+ go.Indicator(
1021
+ mode="gauge+number+delta",
1022
+ value=savings,
1023
+ title={'text': "๋น„์šฉ ์ ˆ๊ฐ๋ฅ  (%)"},
1024
+ delta={'reference': 10},
1025
+ gauge={
1026
+ 'axis': {'range': [0, 50]},
1027
+ 'bar': {'color': "darkblue"},
1028
+ 'steps': [
1029
+ {'range': [0, 10], 'color': "lightgray"},
1030
+ {'range': [10, 25], 'color': "lightgreen"},
1031
+ {'range': [25, 50], 'color': "green"}
1032
+ ],
1033
+ 'threshold': {
1034
+ 'line': {'color': "red", 'width': 4},
1035
+ 'thickness': 0.75,
1036
+ 'value': 15
1037
+ }
1038
+ }
1039
+ ),
1040
+ row=1, col=1
1041
+ )
1042
+
1043
+ # 2. ์ปดํ”Œ๋ผ์ด์–ธ์Šค ์ค€์ˆ˜์œจ
1044
+ fig.add_trace(
1045
+ go.Indicator(
1046
+ mode="gauge+number",
1047
+ value=100,
1048
+ title={'text': "๊ทœ์ • ์ค€์ˆ˜์œจ (%)"},
1049
+ gauge={
1050
+ 'axis': {'range': [0, 100]},
1051
+ 'bar': {'color': "green"},
1052
+ 'steps': [
1053
+ {'range': [0, 80], 'color': "lightcoral"},
1054
+ {'range': [80, 95], 'color': "lightyellow"},
1055
+ {'range': [95, 100], 'color': "lightgreen"}
1056
+ ]
1057
+ }
1058
+ ),
1059
+ row=1, col=2
1060
+ )
1061
+
1062
+ # 3. ESG ํ‰๊ท  ์ ์ˆ˜
1063
+ if len(offers_df) > 0:
1064
+ esg_score = offers_df['esg_level'].map({'A': 100, 'B': 70, 'C': 40}).mean()
1065
+ else:
1066
+ esg_score = 0
1067
+
1068
+ fig.add_trace(
1069
+ go.Indicator(
1070
+ mode="gauge+number",
1071
+ value=esg_score,
1072
+ title={'text': "ESG ํ‰๊ท  ์ ์ˆ˜"},
1073
+ gauge={
1074
+ 'axis': {'range': [0, 100]},
1075
+ 'bar': {'color': "darkgreen"},
1076
+ 'steps': [
1077
+ {'range': [0, 50], 'color': "lightcoral"},
1078
+ {'range': [50, 80], 'color': "lightyellow"},
1079
+ {'range': [80, 100], 'color': "lightgreen"}
1080
+ ]
1081
+ }
1082
+ ),
1083
+ row=1, col=3
1084
+ )
1085
+
1086
+ # 4. ํ‰๊ท  ์ฒ˜๋ฆฌ ์‹œ๊ฐ„
1087
+ fig.add_trace(
1088
+ go.Indicator(
1089
+ mode="number+delta",
1090
+ value=20,
1091
+ title={'text': "์ฒ˜๋ฆฌ ์‹œ๊ฐ„ (๋ถ„)"},
1092
+ delta={'reference': 30, 'increasing': {'color': "red"}, 'decreasing': {'color': "green"}},
1093
+ number={'suffix': "๋ถ„"}
1094
+ ),
1095
+ row=2, col=1
1096
+ )
1097
+
1098
+ # 5. ๋ชฉํ‘œ ๋‹ฌ์„ฑ๋ฅ 
1099
+ fig.add_trace(
1100
+ go.Indicator(
1101
+ mode="gauge+number",
1102
+ value=85,
1103
+ title={'text': "์›”๊ฐ„ ๋ชฉํ‘œ ๋‹ฌ์„ฑ๋ฅ  (%)"},
1104
+ gauge={
1105
+ 'axis': {'range': [0, 100]},
1106
+ 'bar': {'color': "royalblue"},
1107
+ 'threshold': {
1108
+ 'line': {'color': "red", 'width': 4},
1109
+ 'thickness': 0.75,
1110
+ 'value': 80
1111
+ }
1112
+ }
1113
+ ),
1114
+ row=2, col=2
1115
+ )
1116
+
1117
+ # 6. ์›”๊ฐ„ ํŠธ๋ Œ๋“œ
1118
+ months = ['1์›”', '2์›”', '3์›”', '4์›”', '5์›”', '6์›”']
1119
+ values = [75, 78, 82, 85, 88, 90]
1120
+
1121
+ fig.add_trace(
1122
+ go.Scatter(
1123
+ x=months,
1124
+ y=values,
1125
+ mode='lines+markers',
1126
+ name='๋ฐœ์ฃผ ํšจ์œจ',
1127
+ line=dict(color='royalblue', width=3),
1128
+ marker=dict(size=10)
1129
+ ),
1130
+ row=2, col=3
1131
+ )
1132
+
1133
+ fig.update_layout(
1134
+ height=700,
1135
+ showlegend=False,
1136
+ title_text="๐Ÿ“Š ๊ฒฝ์˜์ง„ KPI ๋Œ€์‹œ๋ณด๋“œ",
1137
+ title_font_size=22
1138
+ )
1139
+
1140
+ return fig
1141
+
1142
+ def create_action_items_table(opt_result: Dict, offers_df: pd.DataFrame) -> pd.DataFrame:
1143
+ """๊ฒฝ์˜์ง„ Action Items ์ƒ์„ฑ"""
1144
+
1145
+ action_items = []
1146
+
1147
+ # 1. ์ฆ‰์‹œ ๋ฐœ์ฃผ ์Šน์ธ ํ•ญ๋ชฉ
1148
+ alloc = opt_result.get('allocation', {})
1149
+ if alloc:
1150
+ for supplier_id, details in alloc.items():
1151
+ if isinstance(details, dict):
1152
+ action_items.append({
1153
+ "์šฐ์„ ์ˆœ์œ„": "๐Ÿ”ด ๊ธด๊ธ‰",
1154
+ "Action Item": f"{details.get('supplier_name')} ๋ฐœ์ฃผ ์Šน์ธ",
1155
+ "์ˆ˜๋Ÿ‰": f"{details.get('qty')}๊ฐœ",
1156
+ "์˜ˆ์ƒ ๋น„์šฉ": f"{details.get('qty', 0) * details.get('unit_price', 0):,}์›",
1157
+ "๋‹ด๋‹น": "๊ตฌ๋งคํŒ€",
1158
+ "๊ธฐํ•œ": "์ฆ‰์‹œ",
1159
+ "์ƒํƒœ": "์Šน์ธ ๋Œ€๊ธฐ"
1160
+ })
1161
+
1162
+ # 2. ์žฌ๊ณ  ๋ณด์ถฉ ๊ถŒ๊ณ 
1163
+ action_items.append({
1164
+ "์šฐ์„ ์ˆœ์œ„": "๐ŸŸก ์ค‘์š”",
1165
+ "Action Item": "์•ˆ์ „์žฌ๊ณ  ๋ฏธ๋‹ฌ ํ’ˆ๋ชฉ ๋ณด์ถฉ",
1166
+ "์ˆ˜๋Ÿ‰": "3๊ฐœ ํ’ˆ๋ชฉ",
1167
+ "์˜ˆ์ƒ ๋น„์šฉ": "๊ฒ€ํ†  ํ•„์š”",
1168
+ "๋‹ด๋‹น": "MROํŒ€",
1169
+ "๊ธฐํ•œ": "1์ฃผ์ผ ๋‚ด",
1170
+ "์ƒํƒœ": "๊ฒ€ํ†  ์ค‘"
1171
+ })
1172
+
1173
+ # 3. ESG ๊ฐœ์„ 
1174
+ if len(offers_df) > 0:
1175
+ c_grade_count = len(offers_df[offers_df['esg_level'] == 'C'])
1176
+ if c_grade_count > 0:
1177
+ action_items.append({
1178
+ "์šฐ์„ ์ˆœ์œ„": "๐ŸŸข ๋ณดํ†ต",
1179
+ "Action Item": "ESG C๋“ฑ๊ธ‰ ๊ณต๊ธ‰์—…์ฒด ๋Œ€์ฒด ๊ฒ€ํ† ",
1180
+ "์ˆ˜๋Ÿ‰": f"{c_grade_count}๊ฐœ์‚ฌ",
1181
+ "์˜ˆ์ƒ ๋น„์šฉ": "์˜ํ–ฅ๋„ ๋ถ„์„ ํ•„์š”",
1182
+ "๋‹ด๋‹น": "๊ตฌ๋งคํŒ€",
1183
+ "๊ธฐํ•œ": "1๊ฐœ์›” ๋‚ด",
1184
+ "์ƒํƒœ": "๊ณ„ํš ๋‹จ๊ณ„"
1185
+ })
1186
+
1187
+ # 4. ์žฅ๊ธฐ ๊ณ„์•ฝ ํ˜‘์ƒ
1188
+ action_items.append({
1189
+ "์šฐ์„ ์ˆœ์œ„": "๐ŸŸข ๋ณดํ†ต",
1190
+ "Action Item": "์ฃผ์š” ๊ณต๊ธ‰์—…์ฒด ์žฅ๊ธฐ๊ณ„์•ฝ ํ˜‘์ƒ",
1191
+ "์ˆ˜๋Ÿ‰": "2-3๊ฐœ์‚ฌ",
1192
+ "์˜ˆ์ƒ ๋น„์šฉ": "5-10% ์ ˆ๊ฐ ์˜ˆ์ƒ",
1193
+ "๋‹ด๋‹น": "๊ตฌ๋งคํŒ€",
1194
+ "๊ธฐํ•œ": "๋ถ„๊ธฐ ๋‚ด",
1195
+ "์ƒํƒœ": "๊ณ„ํš ๋‹จ๊ณ„"
1196
+ })
1197
+
1198
+ return pd.DataFrame(action_items)
1199
+
1200
+ # =========================================================
1201
+ # Core Components
1202
+ # =========================================================
1203
+ @dataclass
1204
+ class ToolCallLog:
1205
+ ts: str
1206
+ actor: str
1207
+ tool: str
1208
+ input: Dict[str, Any]
1209
+ output_preview: str
1210
+
1211
+ class MCPToolRegistry:
1212
+ def __init__(self, tables: Dict[str, pd.DataFrame]):
1213
+ self.tables = tables
1214
+ self.logs: List[ToolCallLog] = []
1215
+
1216
+ def _log(self, actor: str, tool: str, inp: Dict[str, Any], out: Any):
1217
+ self.logs.append(ToolCallLog(
1218
+ ts=now_ts(),
1219
+ actor=actor,
1220
+ tool=tool,
1221
+ input=inp,
1222
+ output_preview=str(out)[:500]
1223
+ ))
1224
+
1225
+ def query_inventory(self, actor: str, item_id: str) -> pd.DataFrame:
1226
+ inv = self.tables["inventory"]
1227
+ stor = self.tables["storages"]
1228
+ df = inv[inv["item_id"] == item_id].copy()
1229
+ if len(df) > 0:
1230
+ df = df.merge(stor, on="storage_id", how="left")
1231
+ self._log(actor, "query_inventory", {"item_id": item_id}, f"{len(df)} rows")
1232
+ return df
1233
+
1234
+ def query_offers(self, actor: str, item_id: str) -> pd.DataFrame:
1235
+ offers = self.tables["supplier_offers"]
1236
+ suppliers = self.tables["suppliers"]
1237
+ df = offers[offers["item_id"] == item_id].copy()
1238
+ if len(df) > 0:
1239
+ df = df.merge(suppliers, on="supplier_id", how="left")
1240
+ self._log(actor, "query_offers", {"item_id": item_id}, f"{len(df)} rows")
1241
+ return df
1242
+
1243
+ def query_compat_items(self, actor: str, equipment_id: str) -> pd.DataFrame:
1244
+ compat = self.tables["compat"]
1245
+ items = self.tables["items"]
1246
+ df = compat[compat["equipment_id"] == equipment_id].copy()
1247
+ if len(df) > 0:
1248
+ df = df.merge(items, on="item_id", how="left")
1249
+ self._log(actor, "query_compat_items", {"equipment_id": equipment_id}, f"{len(df)} rows")
1250
+ return df
1251
+
1252
+ def get_equipment_info(self, actor: str, equipment_id: str) -> Dict[str, Any]:
1253
+ eq = self.tables["equipment"]
1254
+ match = eq[eq["equipment_id"] == equipment_id]
1255
+ if len(match) == 0:
1256
+ return {}
1257
+ info = match.iloc[0].to_dict()
1258
+ self._log(actor, "get_equipment_info", {"equipment_id": equipment_id}, safe_json(info))
1259
+ return info
1260
+
1261
+ def audit_log_df(self) -> pd.DataFrame:
1262
+ if not self.logs:
1263
+ return pd.DataFrame({"๋ฉ”์‹œ์ง€": ["๋กœ๊ทธ ์—†์Œ"]})
1264
+ return pd.DataFrame([{
1265
+ "์‹œ๊ฐ„": l.ts[:19],
1266
+ "์—์ด์ „ํŠธ": l.actor,
1267
+ "๋„๊ตฌ": l.tool,
1268
+ "์ž…๋ ฅ": str(l.input)[:50],
1269
+ } for l in self.logs])
1270
+
1271
+ def apply_rules(tables: Dict[str, pd.DataFrame], item_id: str,
1272
+ supplier_row: Dict[str, Any]) -> Dict[str, Any]:
1273
+ """Apply rules"""
1274
+ items = tables["items"]
1275
+ item_match = items[items["item_id"] == item_id]
1276
+
1277
+ if len(item_match) == 0:
1278
+ return {"block": False, "alerts": [], "explanations": [], "rules_fired": []}
1279
+
1280
+ item = item_match.iloc[0].to_dict()
1281
+
1282
+ decision = {
1283
+ "block": False,
1284
+ "alerts": [],
1285
+ "explanations": [],
1286
+ "rules_fired": []
1287
+ }
1288
+
1289
+ if item.get("risk_class") == "๊ทœ์ œ" and supplier_row.get("region") == "ํ•ด์™ธ":
1290
+ decision["block"] = True
1291
+ decision["rules_fired"].append("R-001")
1292
+ decision["explanations"].append(
1293
+ f"๐Ÿšซ R-001: ๊ทœ์ œํ’ˆ๋ชฉ({item.get('item_name')}) ํ•ด์™ธ์—…์ฒด({supplier_row.get('supplier_name')}) ๊ตฌ๋งค ์ฐจ๋‹จ"
1294
+ )
1295
+
1296
+ if supplier_row.get("esg_level") == "C":
1297
+ decision["rules_fired"].append("R-004")
1298
+ decision["explanations"].append(
1299
+ f"๐Ÿ“Š R-004: ESG C๋“ฑ๊ธ‰({supplier_row.get('supplier_name')}) ํŒจ๋„ํ‹ฐ"
1300
+ )
1301
+
1302
+ return decision
1303
+
1304
+ def optimize_order_allocation(demand_qty: int, offers_df: pd.DataFrame,
1305
+ rules_eval: Dict[str, Dict[str, Any]]) -> Dict[str, Any]:
1306
+ """Optimize allocation"""
1307
+ if not PULP_AVAILABLE:
1308
+ return {
1309
+ "status": "UNAVAILABLE",
1310
+ "reason": "PuLP not installed",
1311
+ "allocation": {},
1312
+ "demand": demand_qty
1313
+ }
1314
+
1315
+ feasible = []
1316
+ blocked = []
1317
+
1318
+ for _, r in offers_df.iterrows():
1319
+ sid = r["supplier_id"]
1320
+ if rules_eval.get(sid, {}).get("block"):
1321
+ blocked.append({
1322
+ "supplier_id": sid,
1323
+ "supplier_name": r.get("supplier_name", sid),
1324
+ "reason": "๊ทœ์น™ ์œ„๋ฐ˜"
1325
+ })
1326
+ else:
1327
+ feasible.append(r)
1328
+
1329
+ if len(feasible) == 0:
1330
+ return {
1331
+ "status": "INFEASIBLE",
1332
+ "reason": "๋ชจ๋“  ๊ณต๊ธ‰์—…์ฒด ์ฐจ๋‹จ",
1333
+ "allocation": {},
1334
+ "blocked_suppliers": blocked,
1335
+ "demand": demand_qty
1336
+ }
1337
+
1338
+ fdf = pd.DataFrame(feasible)
1339
+ prob = LpProblem("MRO_Allocation", LpMinimize)
1340
+
1341
+ x = {}
1342
+ for _, r in fdf.iterrows():
1343
+ sid = r["supplier_id"]
1344
+ x[sid] = LpVariable(f"x_{sid}", lowBound=0, cat="Integer")
1345
+
1346
+ prob += lpSum(list(x.values())) >= demand_qty, "DemandConstraint"
1347
+
1348
+ obj_terms = []
1349
+ for _, r in fdf.iterrows():
1350
+ sid = r["supplier_id"]
1351
+ price = float(r["unit_price"])
1352
+ obj_terms.append(x[sid] * price)
1353
+
1354
+ prob += lpSum(obj_terms), "TotalCost"
1355
+ prob.solve()
1356
+
1357
+ alloc = {}
1358
+ total_cost = 0.0
1359
+ for _, r in fdf.iterrows():
1360
+ sid = r["supplier_id"]
1361
+ val = x[sid].value()
1362
+ if val is not None and val > 0:
1363
+ qty = int(val)
1364
+ alloc[sid] = {
1365
+ "qty": qty,
1366
+ "unit_price": float(r["unit_price"]),
1367
+ "supplier_name": r.get("supplier_name", sid),
1368
+ "lead_time": int(r.get("lead_time_days", 0))
1369
+ }
1370
+ total_cost += qty * float(r["unit_price"])
1371
+
1372
+ return {
1373
+ "status": LpStatus.get(prob.status, "Unknown"),
1374
+ "allocation": alloc,
1375
+ "demand": demand_qty,
1376
+ "blocked_suppliers": blocked,
1377
+ "total_cost": round(total_cost, 2)
1378
+ }
1379
+
1380
+ class LLMOrchestrator:
1381
+ def __init__(self):
1382
+ self.api_key = os.environ.get("OPENAI_API_KEY", "").strip()
1383
+ self.demo_mode = (not self.api_key or not OPENAI_AVAILABLE)
1384
+
1385
+ if not self.demo_mode:
1386
+ try:
1387
+ self.client = OpenAI(api_key=self.api_key)
1388
+ test_resp = self.client.chat.completions.create(
1389
+ model="gpt-4o-mini",
1390
+ messages=[{"role": "user", "content": "test"}],
1391
+ max_tokens=5
1392
+ )
1393
+ print("โœ… OpenAI API ์—ฐ๊ฒฐ ์„ฑ๊ณต!")
1394
+ except Exception:
1395
+ self.demo_mode = True
1396
+ self.client = None
1397
+ else:
1398
+ self.client = None
1399
+
1400
+ def chat(self, role: str, system: str, user: str) -> str:
1401
+ if self.demo_mode:
1402
+ return self._demo_response(role)
1403
+ try:
1404
+ resp = self.client.chat.completions.create(
1405
+ model="gpt-4o-mini",
1406
+ temperature=0.2,
1407
+ messages=[
1408
+ {"role": "system", "content": system},
1409
+ {"role": "user", "content": user}
1410
+ ]
1411
+ )
1412
+ return resp.choices[0].message.content
1413
+ except Exception as e:
1414
+ return f"[ERROR: {e}]\n" + self._demo_response(role)
1415
+
1416
+ def _demo_response(self, role: str) -> str:
1417
+ return f"[DEMO MODE - {role}] AI ๋ถ„์„ ์™„๋ฃŒ"
1418
+
1419
+ # =========================================================
1420
+ # LangGraph Workflow
1421
+ # =========================================================
1422
+ class DemoState(TypedDict, total=False):
1423
+ tables: Dict[str, pd.DataFrame]
1424
+ mcp: MCPToolRegistry
1425
+ llm: LLMOrchestrator
1426
+ scenario: str
1427
+ equipment_id: str
1428
+ item_id: str
1429
+ demand_qty: int
1430
+ priority: str
1431
+ tables_ok: bool
1432
+ validation_issues: List[str]
1433
+ progress: str
1434
+ inventory_view: pd.DataFrame
1435
+ offers_view: pd.DataFrame
1436
+ rules_eval: Dict[str, Any]
1437
+ optimization: Dict[str, Any]
1438
+ narrative: Dict[str, str]
1439
+ audit_log: pd.DataFrame
1440
+ selected_item_name: str
1441
+ equipment_info: Dict[str, Any]
1442
+ compat_items: pd.DataFrame
1443
+
1444
+ def node_validate(state: DemoState) -> DemoState:
1445
+ ok, issues = validate_tables(state["tables"])
1446
+ state["tables_ok"] = ok
1447
+ state["validation_issues"] = issues
1448
+ state["progress"] = "1/4 ๊ฒ€์ฆ ์™„๋ฃŒ"
1449
+ return state
1450
+
1451
+ def node_mro_agent(state: DemoState) -> DemoState:
1452
+ mcp: MCPToolRegistry = state["mcp"]
1453
+ equipment_id = state.get("equipment_id", "")
1454
+ item_id = state.get("item_id", "")
1455
+
1456
+ # Get equipment info
1457
+ equipment_info = mcp.get_equipment_info("MRO_AGENT", equipment_id)
1458
+ state["equipment_info"] = equipment_info
1459
+
1460
+ # Get compatible items
1461
+ compat_df = pd.DataFrame()
1462
+ if equipment_id:
1463
+ compat_df = mcp.query_compat_items("MRO_AGENT", equipment_id)
1464
+ state["compat_items"] = compat_df
1465
+
1466
+ if not item_id and len(compat_df) > 0:
1467
+ mandatory = compat_df[compat_df["is_mandatory"] == True]
1468
+ if len(mandatory) > 0:
1469
+ selected = mandatory.iloc[0]
1470
+ else:
1471
+ selected = compat_df.iloc[0]
1472
+ item_id = selected["item_id"]
1473
+ state["item_id"] = item_id
1474
+ state["selected_item_name"] = selected.get("item_name", item_id)
1475
+
1476
+ inv_df = pd.DataFrame()
1477
+ if item_id:
1478
+ inv_df = mcp.query_inventory("MRO_AGENT", item_id)
1479
+ state["inventory_view"] = inv_df
1480
+
1481
+ llm: LLMOrchestrator = state["llm"]
1482
+ if "narrative" not in state:
1483
+ state["narrative"] = {}
1484
+ state["narrative"]["mro"] = llm.chat("MRO", "MRO ๋ถ„์„", "์„ค๋น„/์žฌ๊ณ ")
1485
+ state["progress"] = "2/4 MRO ์™„๋ฃŒ"
1486
+ return state
1487
+
1488
+ def node_procurement_agent(state: DemoState) -> DemoState:
1489
+ mcp: MCPToolRegistry = state["mcp"]
1490
+ item_id = state.get("item_id", "")
1491
+ demand_qty = int(state.get("demand_qty", 10))
1492
+
1493
+ offers_df = pd.DataFrame()
1494
+ if item_id:
1495
+ offers_df = mcp.query_offers("PROC_AGENT", item_id)
1496
+ state["offers_view"] = offers_df
1497
+
1498
+ rules_eval = {}
1499
+ if len(offers_df) > 0:
1500
+ for _, r in offers_df.iterrows():
1501
+ sid = r["supplier_id"]
1502
+ supplier_row = {
1503
+ "supplier_id": sid,
1504
+ "supplier_name": r.get("supplier_name", sid),
1505
+ "region": r.get("region", ""),
1506
+ "esg_level": r.get("esg_level", ""),
1507
+ }
1508
+ rules_eval[sid] = apply_rules(state["tables"], item_id, supplier_row)
1509
+ state["rules_eval"] = rules_eval
1510
+
1511
+ opt_result = {}
1512
+ if len(offers_df) > 0:
1513
+ opt_result = optimize_order_allocation(demand_qty, offers_df, rules_eval)
1514
+ else:
1515
+ opt_result = {
1516
+ "status": "NO_DATA",
1517
+ "reason": "๊ณต๊ธ‰์—…์ฒด ์ •๋ณด ์—†์Œ",
1518
+ "allocation": {},
1519
+ "demand": demand_qty
1520
+ }
1521
+ state["optimization"] = opt_result
1522
+
1523
+ llm: LLMOrchestrator = state["llm"]
1524
+ state["narrative"]["proc"] = llm.chat("PROC", "๊ตฌ๋งค ์ „๋žต", "์ตœ์ ํ™”")
1525
+ state["narrative"]["exec"] = llm.chat("EXEC", "์ž„์› ์š”์•ฝ", "์ข…ํ•ฉ")
1526
+ state["progress"] = "3/4 ๊ตฌ๋งค ์™„๋ฃŒ"
1527
+ return state
1528
+
1529
+ def node_collect_audit(state: DemoState) -> DemoState:
1530
+ mcp: MCPToolRegistry = state["mcp"]
1531
+ state["audit_log"] = mcp.audit_log_df()
1532
+ state["progress"] = "4/4 ์™„๋ฃŒ โœ“"
1533
+ return state
1534
+
1535
+ def build_workflow():
1536
+ if not LANGGRAPH_AVAILABLE:
1537
+ return None
1538
+ try:
1539
+ graph = StateGraph(DemoState)
1540
+ graph.add_node("validate", node_validate)
1541
+ graph.add_node("mro_agent", node_mro_agent)
1542
+ graph.add_node("procurement_agent", node_procurement_agent)
1543
+ graph.add_node("collect_audit", node_collect_audit)
1544
+ graph.set_entry_point("validate")
1545
+ graph.add_edge("validate", "mro_agent")
1546
+ graph.add_edge("mro_agent", "procurement_agent")
1547
+ graph.add_edge("procurement_agent", "collect_audit")
1548
+ graph.add_edge("collect_audit", END)
1549
+ return graph.compile()
1550
+ except Exception as e:
1551
+ print(f"โš ๏ธ LangGraph failed: {e}")
1552
+ return None
1553
+
1554
+ APP = build_workflow()
1555
+
1556
+ # =========================================================
1557
+ # Main Execution - Enhanced with Dashboards
1558
+ # =========================================================
1559
+ def run_demo(scenario: str, seed: int, equipment_id: str, item_id: str,
1560
+ demand_qty: int) -> Tuple:
1561
+ """Main execution - returns 12 outputs (enhanced)"""
1562
+ try:
1563
+ seed_int = int(seed)
1564
+ demand_int = int(demand_qty)
1565
+
1566
+ tables = generate_demo_tables(seed=seed_int)
1567
+ mcp = MCPToolRegistry(tables)
1568
+ llm = LLMOrchestrator()
1569
+
1570
+ preset = SCENARIO_PRESETS.get(scenario, SCENARIO_PRESETS["๊ธด๊ธ‰ ๊ณ ์žฅ ๋Œ€์‘"])
1571
+
1572
+ state: DemoState = {
1573
+ "tables": tables,
1574
+ "mcp": mcp,
1575
+ "llm": llm,
1576
+ "scenario": scenario,
1577
+ "equipment_id": equipment_id.strip(),
1578
+ "item_id": item_id.strip(),
1579
+ "demand_qty": demand_int,
1580
+ "priority": preset.get("priority", "์ •์ƒ"),
1581
+ }
1582
+
1583
+ if APP is not None:
1584
+ out = APP.invoke(state)
1585
+ else:
1586
+ out = node_validate(state)
1587
+ out = node_mro_agent(out)
1588
+ out = node_procurement_agent(out)
1589
+ out = node_collect_audit(out)
1590
+
1591
+ status = {
1592
+ "mode": "โš ๏ธ DEMO" if llm.demo_mode else "โœ… LLM",
1593
+ "scenario": scenario,
1594
+ "tables_ok": out.get("tables_ok", False),
1595
+ "equipment": out.get("equipment_id", ""),
1596
+ "item_name": out.get("selected_item_name", ""),
1597
+ "demand": out.get("demand_qty", 0),
1598
+ "priority": out.get("priority", "์ •์ƒ"),
1599
+ "progress": out.get("progress", "์™„๋ฃŒ"),
1600
+ }
1601
+
1602
+ status_text = format_status(status)
1603
+
1604
+ # Data extraction
1605
+ inv_df = out.get("inventory_view", pd.DataFrame())
1606
+ offers_df = out.get("offers_view", pd.DataFrame())
1607
+ audit_df = out.get("audit_log", pd.DataFrame())
1608
+ equipment_info = out.get("equipment_info", {})
1609
+ compat_items = out.get("compat_items", pd.DataFrame())
1610
+ rules_eval = out.get("rules_eval", {})
1611
+ opt_result = out.get("optimization", {})
1612
+ purchase_history = tables.get("purchase_history", pd.DataFrame())
1613
+
1614
+ item_name = out.get("selected_item_name", "๋ถ€ํ’ˆ")
1615
+
1616
+ # Create dashboards
1617
+ mro_dashboard = create_mro_inventory_dashboard(inv_df, item_name)
1618
+ mro_workflow = create_mro_workflow_status(equipment_info, compat_items)
1619
+ proc_dashboard = create_procurement_comparison_dashboard(offers_df, rules_eval)
1620
+ proc_workflow = create_procurement_workflow(opt_result)
1621
+ exec_dashboard = create_executive_kpi_dashboard(opt_result, offers_df, purchase_history)
1622
+ action_items = create_action_items_table(opt_result, offers_df)
1623
+
1624
+ # Fallback for empty dataframes
1625
+ if len(audit_df) == 0:
1626
+ audit_df = pd.DataFrame({"๋ฉ”์‹œ์ง€": ["๊ฐ์‚ฌ๋กœ๊ทธ ์—†์Œ"]})
1627
+
1628
+ print("โœ… ๋Œ€์‹œ๋ณด๋“œ ์ƒ์„ฑ ์™„๋ฃŒ\n")
1629
+
1630
+ # Return 12 outputs
1631
+ return (
1632
+ status_text, # 1
1633
+ mro_dashboard, # 2 - MRO ์žฌ๊ณ  ๋Œ€์‹œ๋ณด๋“œ
1634
+ mro_workflow, # 3 - MRO ์›Œํฌํ”Œ๋กœ์šฐ
1635
+ proc_dashboard, # 4 - ๊ตฌ๋งค ๋น„๊ต ๋Œ€์‹œ๋ณด๋“œ
1636
+ proc_workflow, # 5 - ๊ตฌ๋งค ์›Œํฌํ”Œ๋กœ์šฐ
1637
+ exec_dashboard, # 6 - ๊ฒฝ์˜์ง„ KPI
1638
+ action_items, # 7 - Action Items
1639
+ offers_df, # 8 - ๊ณต๊ธ‰์—…์ฒด ์›๋ณธ ๋ฐ์ดํ„ฐ
1640
+ inv_df, # 9 - ์žฌ๊ณ  ์›๋ณธ ๋ฐ์ดํ„ฐ
1641
+ opt_result, # 10 - ์ตœ์ ํ™” ๊ฒฐ๊ณผ (dict๋ฅผ text๋กœ)
1642
+ audit_df, # 11 - ๊ฐ์‚ฌ ๋กœ๊ทธ
1643
+ out.get("selected_item_name", "N/A") # 12 - ํ’ˆ๋ชฉ๋ช…
1644
+ )
1645
+
1646
+ except Exception as e:
1647
+ print(f"โŒ ์˜ค๋ฅ˜: {e}\n{traceback.format_exc()}")
1648
+ error_msg = f"โŒ ์˜ค๋ฅ˜\n\n{str(e)}"
1649
+ empty_fig = go.Figure()
1650
+ empty_fig.add_annotation(text="์˜ค๋ฅ˜ ๋ฐœ์ƒ", showarrow=False)
1651
+ empty_df = pd.DataFrame({"์˜ค๋ฅ˜": [str(e)[:100]]})
1652
+
1653
+ return (
1654
+ error_msg, empty_fig, empty_fig, empty_fig, empty_fig,
1655
+ empty_fig, empty_df, empty_df, empty_df, {}, empty_df, "N/A"
1656
+ )
1657
+
1658
+ def update_scenario(scenario: str) -> Tuple[str, str, int, str]:
1659
+ preset = SCENARIO_PRESETS.get(scenario, SCENARIO_PRESETS["๊ธด๊ธ‰ ๊ณ ์žฅ ๋Œ€์‘"])
1660
+ guide_text = f"""**๐Ÿ“Œ {preset['description']}**
1661
+
1662
+ **๋ฐฐ๊ฒฝ**: {preset['context']}
1663
+ **์šฐ์„ ์ˆœ์œ„**: {preset.get('priority', '์ •์ƒ')}
1664
+ **๊ฐ€์ด๋“œ**: {preset.get('guide', '')}
1665
+ """
1666
+
1667
+ return (
1668
+ preset["equipment_id"],
1669
+ preset["item_id"],
1670
+ preset["demand_qty"],
1671
+ guide_text
1672
+ )
1673
+
1674
+ # =========================================================
1675
+ # Enhanced Gradio UI with Process Guides
1676
+ # =========================================================
1677
+ print("๐ŸŽจ ํ”„๋กœ์„ธ์Šค ๊ฐ€์ด๋“œ ํ†ตํ•ฉ UI ๊ตฌ์„ฑ ์ค‘...\n")
1678
+
1679
+ with gr.Blocks(title="POSCO DX MRO Composite AI", theme=gr.themes.Soft()) as demo:
1680
+
1681
+ gr.Markdown("""
1682
+ # ๐Ÿญ POSCO DX - MRO Composite AI ํ”„๋กœ์„ธ์Šค ๊ฐ€์ด๋“œ ํ†ตํ•ฉ ๋ฒ„์ „
1683
+
1684
+ ## ๐ŸŽฏ ์—…๋ฌด ํ”„๋กœ์„ธ์Šค ์ž๋™ํ™” + AI ์˜์‚ฌ๊ฒฐ์ • ์ง€์› ์‹œ์Šคํ…œ
1685
+
1686
+ **3-Agent Collaboration**: MRO ์šด์˜ โ†’ ๊ตฌ๋งค/์กฐ๋‹ฌ โ†’ ๊ฒฝ์˜์ง„ ์Šน์ธ
1687
+ """)
1688
+
1689
+ # Process Overview Section
1690
+ with gr.Accordion("๐Ÿ“– ์ „์ฒด ์—…๋ฌด ํ”„๋กœ์„ธ์Šค ๊ฐœ์š”", open=False):
1691
+ gr.Markdown("""
1692
+ ### ๐Ÿ”„ End-to-End ์›Œํฌํ”Œ๋กœ์šฐ
1693
+
1694
+ ```
1695
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
1696
+ โ”‚ 1๏ธโƒฃ MRO ์šด์˜ โ”‚ โ”€โ”€โ”€> โ”‚ 2๏ธโƒฃ ๊ตฌ๋งค/์กฐ๋‹ฌ โ”‚ โ”€โ”€โ”€> โ”‚ 3๏ธโƒฃ ๊ฒฝ์˜์ง„ ์Šน์ธ โ”‚
1697
+ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚
1698
+ โ”‚ โ€ข ๊ณ ์žฅ ์ ‘์ˆ˜ โ”‚ โ”‚ โ€ข ๊ณต๊ธ‰์—…์ฒด ์กฐํšŒ โ”‚ โ”‚ โ€ข KPI ํ™•์ธ โ”‚
1699
+ โ”‚ โ€ข ๋ถ€ํ’ˆ ํ™•์ธ โ”‚ โ”‚ โ€ข ๊ทœ์ • ๊ฒ€์ฆ โ”‚ โ”‚ โ€ข ์˜์‚ฌ๊ฒฐ์ • โ”‚
1700
+ โ”‚ โ€ข ์žฌ๊ณ  ํ™•์ธ โ”‚ โ”‚ โ€ข ์ตœ์ ํ™” ๋ถ„์„ โ”‚ โ”‚ โ€ข ํ”ผ๋“œ๋ฐฑ โ”‚
1701
+ โ”‚ โ€ข ๋ฐœ์ฃผ ์š”์ฒญ โ”‚ โ”‚ โ€ข ์Šน์ธ ์š”์ฒญ โ”‚ โ”‚ โ”‚
1702
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
1703
+ โฑ๏ธ 15๋ถ„ โฑ๏ธ 25๋ถ„ โฑ๏ธ 25๋ถ„
1704
+ ```
1705
+
1706
+ ### ๐Ÿ’ก ํ•ต์‹ฌ ๊ฐ€์น˜
1707
+
1708
+ 1. **์ž๋™ํ™”**: ์„ค๋น„-๋ถ€ํ’ˆ ๋งค์นญ, ์žฌ๊ณ  ์กฐํšŒ, ๊ทœ์ • ๊ฒ€์ฆ ๋“ฑ ๋ฐ˜๋ณต ์—…๋ฌด ์ž๋™ํ™”
1709
+ 2. **์ตœ์ ํ™”**: AI ๊ธฐ๋ฐ˜ ๋น„์šฉ ์ตœ์ ํ™” ๋ฐ ๊ณต๊ธ‰์—…์ฒด ์„ ์ •
1710
+ 3. **๊ฒ€์ฆ**: Neuro-Symbolic AI๋กœ 100% ๊ทœ์ • ์ค€์ˆ˜ ๋ณด์žฅ
1711
+ 4. **๊ฐ€์‹œ์„ฑ**: ์‹ค์‹œ๊ฐ„ ๋Œ€์‹œ๋ณด๋“œ๋กœ ์ „ ๊ณผ์ • ๋ชจ๋‹ˆํ„ฐ๋ง
1712
+ 5. **์ถ”์ ์„ฑ**: ๋ชจ๋“  ์˜์‚ฌ๊ฒฐ์ • ๊ณผ์ • ์ž๋™ ๊ธฐ๋ก
1713
+
1714
+ ### ๐Ÿ“Š ๊ธฐ๋Œ€ ํšจ๊ณผ
1715
+
1716
+ - โฑ๏ธ **์ฒ˜๋ฆฌ ์‹œ๊ฐ„**: ๊ธฐ์กด 3-5์ผ โ†’ **1์‹œ๊ฐ„ ์ด๋‚ด**
1717
+ - ๐Ÿ’ฐ **๋น„์šฉ ์ ˆ๊ฐ**: ํ‰๊ท  **15-25%** ๊ตฌ๋งค ๋น„์šฉ ์ ˆ๊ฐ
1718
+ - โš–๏ธ **์ปดํ”Œ๋ผ์ด์–ธ์Šค**: **100%** ๊ทœ์ • ์ค€์ˆ˜
1719
+ - ๐Ÿ“ˆ **ํšจ์œจ์„ฑ**: ๋‹ด๋‹น์ž ์—…๋ฌด ์‹œ๊ฐ„ **60%** ๋‹จ์ถ•
1720
+ """)
1721
+
1722
+ with gr.Row():
1723
+ with gr.Column():
1724
+ gr.Markdown("### ๐ŸŽฏ ์‹œ๋‚˜๋ฆฌ์˜ค")
1725
+ scenario_radio = gr.Radio(
1726
+ choices=list(SCENARIO_PRESETS.keys()),
1727
+ value="๊ธด๊ธ‰ ๊ณ ์žฅ ๋Œ€์‘",
1728
+ label="๋ถ„์„ ์‹œ๋‚˜๋ฆฌ์˜ค"
1729
+ )
1730
+ scenario_info = gr.Markdown(
1731
+ value=f"""**๐Ÿ“Œ {SCENARIO_PRESETS['๊ธด๊ธ‰ ๊ณ ์žฅ ๋Œ€์‘']['description']}**
1732
+
1733
+ **๋ฐฐ๊ฒฝ**: {SCENARIO_PRESETS['๊ธด๊ธ‰ ๊ณ ์žฅ ๋Œ€์‘']['context']}
1734
+ **๊ฐ€์ด๋“œ**: {SCENARIO_PRESETS['๊ธด๊ธ‰ ๊ณ ์žฅ ๋Œ€์‘']['guide']}
1735
+ """
1736
+ )
1737
+
1738
+ with gr.Column():
1739
+ gr.Markdown("### โš™๏ธ ํŒŒ๋ผ๋ฏธํ„ฐ")
1740
+ seed_number = gr.Number(value=7, label="๏ฟฝ๏ฟฝ์ดํ„ฐ ์‹œ๋“œ", precision=0)
1741
+ equipment_text = gr.Textbox(value="CONV-PH-007", label="์„ค๋น„ ID")
1742
+ item_text = gr.Textbox(value="", label="ํ’ˆ๋ชฉ ID (์„ ํƒ)")
1743
+ demand_number = gr.Number(value=10, label="์ˆ˜๋Ÿ‰", precision=0)
1744
+
1745
+ run_button = gr.Button("๐Ÿš€ Composite AI ๋ถ„์„ ์‹คํ–‰", variant="primary", size="lg")
1746
+
1747
+ gr.Markdown("---")
1748
+
1749
+ status_output = gr.Textbox(label="๐Ÿ“Š ์‹คํ–‰ ์ƒํƒœ", lines=10)
1750
+ selected_item_display = gr.Textbox(label="๐Ÿ“ฆ ์„ ํƒ๋œ ํ’ˆ๋ชฉ", interactive=False)
1751
+
1752
+ with gr.Tabs():
1753
+ with gr.Tab("๐Ÿ”ง MRO ๋‹ด๋‹น์ž"):
1754
+ # Process Guide for MRO
1755
+ with gr.Accordion("๐Ÿ“‹ MRO ์šด์˜ ํ”„๋กœ์„ธ์Šค ๊ฐ€์ด๋“œ", open=True):
1756
+ mro_process_html = gr.HTML(create_process_guide_html("mro"))
1757
+
1758
+ gr.Markdown("---")
1759
+ gr.Markdown("### ๐Ÿ“Š ๋Œ€์‹œ๋ณด๋“œ ๋ฐ ๋ถ„์„ ๊ฒฐ๊ณผ")
1760
+
1761
+ mro_inventory_plot = gr.Plot(label="๐Ÿ“ฆ ์žฌ๊ณ  ๋ถ„์„ ๋Œ€์‹œ๋ณด๋“œ")
1762
+ mro_workflow_plot = gr.Plot(label="๐Ÿ”„ MRO ์›Œํฌํ”Œ๋กœ์šฐ ์ง„ํ–‰")
1763
+ mro_inventory_table = gr.Dataframe(label="๐Ÿ“‹ ์ƒ์„ธ ์žฌ๊ณ  ๋ฐ์ดํ„ฐ")
1764
+
1765
+ with gr.Tab("๐Ÿ’ฐ ๊ตฌ๋งค ๋‹ด๋‹น์ž"):
1766
+ # Process Guide for Procurement
1767
+ with gr.Accordion("๐Ÿ“‹ ๊ตฌ๋งค/์กฐ๋‹ฌ ํ”„๋กœ์„ธ์Šค ๊ฐ€์ด๋“œ", open=True):
1768
+ proc_process_html = gr.HTML(create_process_guide_html("procurement"))
1769
+
1770
+ gr.Markdown("---")
1771
+ gr.Markdown("### ๐Ÿ“Š ๋Œ€์‹œ๋ณด๋“œ ๋ฐ ๋ถ„์„ ๊ฒฐ๊ณผ")
1772
+
1773
+ proc_comparison_plot = gr.Plot(label="๐Ÿ“Š ๊ณต๊ธ‰์—…์ฒด ๋น„๊ต ๋Œ€์‹œ๋ณด๋“œ")
1774
+ proc_workflow_plot = gr.Plot(label="๐Ÿ”„ ๊ตฌ๋งค ์›Œํฌํ”Œ๋กœ์šฐ")
1775
+ proc_offers_table = gr.Dataframe(label="๐Ÿ“‹ ๊ณต๊ธ‰์—…์ฒด ์ƒ์„ธ ์ •๋ณด")
1776
+
1777
+ with gr.Tab("๐Ÿ‘” ๊ฒฝ์˜์ง„"):
1778
+ # Process Guide for Executive
1779
+ with gr.Accordion("๐Ÿ“‹ ๊ฒฝ์˜์ง„ ์˜์‚ฌ๊ฒฐ์ • ํ”„๋กœ์„ธ์Šค ๊ฐ€์ด๋“œ", open=True):
1780
+ exec_process_html = gr.HTML(create_process_guide_html("executive"))
1781
+
1782
+ gr.Markdown("---")
1783
+ gr.Markdown("### ๐Ÿ“Š ๋Œ€์‹œ๋ณด๋“œ ๋ฐ ๋ถ„์„ ๊ฒฐ๊ณผ")
1784
+
1785
+ exec_kpi_plot = gr.Plot(label="๐Ÿ“Š ๊ฒฝ์˜์ง„ KPI ๋Œ€์‹œ๋ณด๋“œ")
1786
+ exec_action_table = gr.Dataframe(label="๐Ÿ“‹ Action Items")
1787
+
1788
+ gr.Markdown("### ๐Ÿ’ฌ ๊ฒฝ์˜์ง„ ํ”ผ๋“œ๋ฐฑ")
1789
+ with gr.Row():
1790
+ feedback_text = gr.Textbox(
1791
+ label="๊ฐœ์„  ์ œ์•ˆ / ํ”ผ๋“œ๋ฐฑ",
1792
+ placeholder="์˜ˆ: ESG C๋“ฑ๊ธ‰ ์—…์ฒด ๋น„์ค‘์„ 20% ์ดํ•˜๋กœ ์ œํ•œํ•˜์‹œ๊ธฐ ๋ฐ”๋ž๋‹ˆ๋‹ค.",
1793
+ lines=3
1794
+ )
1795
+ with gr.Row():
1796
+ approve_btn = gr.Button("โœ… ์Šน์ธ", variant="primary")
1797
+ reject_btn = gr.Button("โŒ ๋ฐ˜๋ ค", variant="stop")
1798
+ suggest_btn = gr.Button("๐Ÿ’ก ๊ฐœ์„  ์ œ์•ˆ", variant="secondary")
1799
+
1800
+ feedback_output = gr.Textbox(label="ํ”ผ๋“œ๋ฐฑ ์ฒ˜๋ฆฌ ๊ฒฐ๊ณผ", lines=2)
1801
+
1802
+ with gr.Tab("๐Ÿ“ ๊ฐ์‚ฌ ๋กœ๊ทธ"):
1803
+ gr.Markdown("""
1804
+ ### ๊ฐ์‚ฌ ์ถ”์  (Audit Trail)
1805
+
1806
+ **๋ชฉ์ **: ๋ชจ๋“  ์˜์‚ฌ๊ฒฐ์ • ๊ณผ์ • ์ถ”์  ๋ฐ ์ปดํ”Œ๋ผ์ด์–ธ์Šค ํ™•๋ณด
1807
+
1808
+ **๊ธฐ๋ก ํ•ญ๋ชฉ**:
1809
+ - ๐Ÿ• ์‹œ๊ฐ„: ์ž‘์—… ์ˆ˜ํ–‰ ์‹œ๊ฐ
1810
+ - ๐Ÿ‘ค ์—์ด์ „ํŠธ: MRO/๊ตฌ๋งค/๊ฒฝ์˜์ง„
1811
+ - ๐Ÿ”ง ๋„๊ตฌ: ์‚ฌ์šฉํ•œ ๊ธฐ๋Šฅ
1812
+ - ๐Ÿ“ฅ ์ž…๋ ฅ: ํŒŒ๋ผ๋ฏธํ„ฐ
1813
+ - ๐Ÿ“ค ์ถœ๋ ฅ: ๊ฒฐ๊ณผ ์š”์•ฝ
1814
+
1815
+ **ํ™œ์šฉ**:
1816
+ - ๊ทœ์ • ์ค€์ˆ˜ ๊ฐ์‚ฌ
1817
+ - ํ”„๋กœ์„ธ์Šค ๊ฐœ์„ 
1818
+ - ์ฑ…์ž„ ์ถ”์ ์„ฑ
1819
+ """)
1820
+ audit_table = gr.Dataframe(label="๐Ÿ“ ์ „์ฒด ๊ฐ์‚ฌ ๋กœ๊ทธ")
1821
+
1822
+ # Hidden outputs for optimization result
1823
+ opt_result_json = gr.JSON(label="์ตœ์ ํ™” ์ƒ์„ธ ๊ฒฐ๊ณผ", visible=False)
1824
+
1825
+ gr.Markdown("""
1826
+ ---
1827
+ ## ๐Ÿ’ก ์‹œ์Šคํ…œ ์‚ฌ์šฉ ๊ฐ€์ด๋“œ
1828
+
1829
+ ### ๐Ÿ“‹ ๋‹จ๊ณ„๋ณ„ ์‚ฌ์šฉ๋ฒ•
1830
+
1831
+ #### 1๏ธโƒฃ ์‹œ๋‚˜๋ฆฌ์˜ค ์„ ํƒ
1832
+ - **๊ธด๊ธ‰ ๊ณ ์žฅ ๋Œ€์‘**: ์„ค๋น„ ๊ณ ์žฅ์œผ๋กœ ์ฆ‰์‹œ ๊ต์ฒด๊ฐ€ ํ•„์š”ํ•œ ๊ฒฝ์šฐ
1833
+ - **์ •๊ธฐ ๋ฐœ์ฃผ ๊ณ„ํš**: ์›”๊ฐ„/๋ถ„๊ธฐ ์ •๊ธฐ ๋ฐœ์ฃผ ์ตœ์ ํ™”
1834
+ - **๊ทœ์ • ์ค€์ˆ˜ ๊ฒ€์ฆ**: ํŠน์ˆ˜ ํ’ˆ๋ชฉ ๊ตฌ๋งค ์‹œ ์ปดํ”Œ๋ผ์ด์–ธ์Šค ํ™•์ธ
1835
+
1836
+ #### 2๏ธโƒฃ ํŒŒ๋ผ๋ฏธํ„ฐ ์ž…๋ ฅ
1837
+ - **์„ค๋น„ ID**: ๊ณ ์žฅ/์ •๋น„ ๋Œ€์ƒ ์„ค๋น„ (์ž๋™ ์ž…๋ ฅ)
1838
+ - **ํ’ˆ๋ชฉ ID**: ํŠน์ • ํ’ˆ๋ชฉ ์ง€์ • (์„ ํƒ์‚ฌํ•ญ, ๋น„์šฐ๋ฉด ์ž๋™ ์„ ํƒ)
1839
+ - **์ˆ˜๋Ÿ‰**: ๋ฐœ์ฃผ ํ•„์š” ์ˆ˜๋Ÿ‰
1840
+
1841
+ #### 3๏ธโƒฃ ๋ถ„์„ ์‹คํ–‰
1842
+ - "๐Ÿš€ Composite AI ๋ถ„์„ ์‹คํ–‰" ๋ฒ„ํŠผ ํด๋ฆญ
1843
+ - ์•ฝ 5-10์ดˆ ๋‚ด ๊ฒฐ๊ณผ ํ™•์ธ
1844
+
1845
+ #### 4๏ธโƒฃ ๊ฒฐ๊ณผ ๊ฒ€ํ† 
1846
+ - **MRO ํƒญ**: ์žฌ๊ณ  ํ˜„ํ™ฉ ๋ฐ ๋ฐœ์ฃผ ํ•„์š”์„ฑ ํ™•์ธ
1847
+ - **๊ตฌ๋งค ํƒญ**: ๊ณต๊ธ‰์—…์ฒด ๋น„๊ต ๋ฐ ์ตœ์  ์„ ํƒ
1848
+ - **๊ฒฝ์˜์ง„ ํƒญ**: KPI ํ™•์ธ ๋ฐ ์˜์‚ฌ๊ฒฐ์ •
1849
+
1850
+ #### 5๏ธโƒฃ ์Šน์ธ/ํ”ผ๋“œ๋ฐฑ
1851
+ - Action Items ๊ฒ€ํ†  ํ›„ ์Šน์ธ/๋ฐ˜๋ ค ๊ฒฐ์ •
1852
+ - ๊ฐœ์„  ์ œ์•ˆ ์ž…๋ ฅ ์‹œ ์ž๋™์œผ๋กœ ๋‹ด๋‹น ๋ถ€์„œ์— ์ „๋‹ฌ
1853
+
1854
+ ### ๐ŸŽ“ ํ”„๋กœ์„ธ์Šค ์ดํ•ด
1855
+
1856
+ ๊ฐ ํƒญ์˜ "๐Ÿ“‹ ํ”„๋กœ์„ธ์Šค ๊ฐ€์ด๋“œ"๋ฅผ ํŽผ์น˜๋ฉด:
1857
+ - ๋‹จ๊ณ„๋ณ„ ์ƒ์„ธ ์ ˆ์ฐจ
1858
+ - ์ž…๋ ฅ/์ถœ๋ ฅ ๋ช…์„ธ
1859
+ - ๋‹ด๋‹น์ž ๋ฐ ์†Œ์š” ์‹œ๊ฐ„
1860
+ - ์„ฑ๊ณต ๊ธฐ์ค€
1861
+
1862
+ ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
1863
+
1864
+ ### ๐Ÿ” ์ฃผ์š” ๊ธฐ๋Šฅ
1865
+
1866
+ 1. **์ž๋™ ๋ถ€ํ’ˆ ๋งค์นญ**: ์„ค๋น„ ID๋งŒ์œผ๋กœ ํ˜ธํ™˜ ๋ถ€ํ’ˆ ์ž๋™ ๊ฒ€์ƒ‰
1867
+ 2. **์ „์‚ฌ ์žฌ๊ณ  ํ†ตํ•ฉ**: ๋ณธ์‚ฌ, ํฌํ•ญ, ๊ด‘์–‘ ์ „์ฒด ์ฐฝ๊ณ  ์‹ค์‹œ๊ฐ„ ์กฐํšŒ
1868
+ 3. **AI ๊ทœ์ • ๊ฒ€์ฆ**: ๊ทœ์ œํ’ˆ๋ชฉ, ESG ๋“ฑ๊ธ‰ ๋“ฑ ์ž๋™ ๊ฒ€์ฆ
1869
+ 4. **์ตœ์ ํ™” ์—”์ง„**: Linear Programming์œผ๋กœ ๋น„์šฉ ์ตœ์†Œํ™”
1870
+ 5. **์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ ๋Œ€์‹œ๋ณด๋“œ**: Plotly ์ฐจํŠธ๋กœ ๋“œ๋ฆด๋‹ค์šด ๋ถ„์„ ๊ฐ€๋Šฅ
1871
+
1872
+ ### ๐Ÿ”‘ API Key ์„ค์ • (Hugging Face Spaces)
1873
+
1874
+ OpenAI ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•˜๋ ค๋ฉด:
1875
+ 1. Space Settings โ†’ Secrets์œผ๋กœ ์ด๋™
1876
+ 2. ์ƒˆ Secret ์ถ”๊ฐ€:
1877
+ - Name: `OPENAI_API_KEY`
1878
+ - Value: `your-openai-api-key`
1879
+ 3. Space ์žฌ์‹œ์ž‘
1880
+
1881
+ API ํ‚ค ์—†์ด๋„ ๋ฐ๋ชจ ๋ชจ๋“œ๋กœ ๊ธฐ๋ณธ ๊ธฐ๋Šฅ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.
1882
+ """)
1883
+
1884
+ # Event Handlers
1885
+ scenario_radio.change(
1886
+ fn=update_scenario,
1887
+ inputs=[scenario_radio],
1888
+ outputs=[equipment_text, item_text, demand_number, scenario_info]
1889
+ )
1890
+
1891
+ run_button.click(
1892
+ fn=run_demo,
1893
+ inputs=[scenario_radio, seed_number, equipment_text, item_text, demand_number],
1894
+ outputs=[
1895
+ status_output, # 1
1896
+ mro_inventory_plot, # 2
1897
+ mro_workflow_plot, # 3
1898
+ proc_comparison_plot, # 4
1899
+ proc_workflow_plot, # 5
1900
+ exec_kpi_plot, # 6
1901
+ exec_action_table, # 7
1902
+ proc_offers_table, # 8
1903
+ mro_inventory_table, # 9
1904
+ opt_result_json, # 10
1905
+ audit_table, # 11
1906
+ selected_item_display # 12
1907
+ ]
1908
+ )
1909
+
1910
+ # Feedback handlers
1911
+ def handle_approve(feedback):
1912
+ return f"โœ… ์Šน์ธ ์™„๋ฃŒ: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\nํ”ผ๋“œ๋ฐฑ: {feedback}"
1913
+
1914
+ def handle_reject(feedback):
1915
+ return f"โŒ ๋ฐ˜๋ ค๋จ: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n์‚ฌ์œ : {feedback}"
1916
+
1917
+ def handle_suggest(feedback):
1918
+ return f"๐Ÿ’ก ๊ฐœ์„  ์ œ์•ˆ ์ ‘์ˆ˜: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n๋‚ด์šฉ: {feedback}"
1919
+
1920
+ approve_btn.click(fn=handle_approve, inputs=[feedback_text], outputs=[feedback_output])
1921
+ reject_btn.click(fn=handle_reject, inputs=[feedback_text], outputs=[feedback_output])
1922
+ suggest_btn.click(fn=handle_suggest, inputs=[feedback_text], outputs=[feedback_output])
1923
+
1924
+ print("=" * 60)
1925
+ print("โœ… ํ”„๋กœ์„ธ์Šค ๊ฐ€์ด๋“œ ํ†ตํ•ฉ UI ์™„๋ฃŒ!")
1926
+ print("=" * 60)
1927
+
1928
+ if __name__ == "__main__":
1929
+ demo.launch(
1930
+ server_name="0.0.0.0",
1931
+ server_port=7860,
1932
+ show_error=True
1933
+ )
1934
+
1935
+ print("\n๐ŸŽ‰ ํ”„๋กœ์„ธ์Šค ๊ฐ€์ด๋“œ ํ†ตํ•ฉ ๋ฒ„์ „ ์‹คํ–‰ ์ค‘!\n")
requirements.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ numpy==1.24.3
2
+ pandas==2.0.3
3
+ networkx==3.1
4
+ plotly==5.18.0
5
+ pulp==2.7.0
6
+ gradio==4.44.1
7
+ langgraph==0.0.40
8
+ openai==1.40.0
9
+ langchain-core==0.1.52
10
+