ssboost commited on
Commit
49c5003
ยท
verified ยท
1 Parent(s): ceccef9

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +814 -1
app.py CHANGED
@@ -1,2 +1,815 @@
 
 
1
  import os
2
- exec(os.environ.get('APP'))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import pandas as pd
3
  import os
4
+ import time
5
+ import threading
6
+ import tempfile
7
+ import logging
8
+ import random
9
+ import uuid
10
+ import shutil
11
+ import glob
12
+ from datetime import datetime
13
+ from gradio_client import Client
14
+ from dotenv import load_dotenv
15
+
16
+ # ํ™˜๊ฒฝ๋ณ€์ˆ˜ ๋กœ๋“œ
17
+ load_dotenv()
18
+
19
+ # ๋กœ๊น… ์„ค์ • (API ์—”๋“œํฌ์ธํŠธ ์ •๋ณด๋Š” ์ œ์™ธ)
20
+ logging.basicConfig(
21
+ level=logging.INFO,
22
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
23
+ handlers=[
24
+ logging.StreamHandler(),
25
+ logging.FileHandler('control_tower_app.log', mode='a')
26
+ ]
27
+ )
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+ # API ํด๋ผ์ด์–ธํŠธ ์ดˆ๊ธฐํ™”
32
+ def get_api_client():
33
+ """ํ™˜๊ฒฝ๋ณ€์ˆ˜์—์„œ API ์—”๋“œํฌ์ธํŠธ๋ฅผ ๊ฐ€์ ธ์™€ ํด๋ผ์ด์–ธํŠธ ์ƒ์„ฑ"""
34
+ endpoint = os.getenv('API_ENDPOINT')
35
+ if not endpoint:
36
+ logger.error("API_ENDPOINT ํ™˜๊ฒฝ๋ณ€์ˆ˜๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.")
37
+ raise ValueError("API_ENDPOINT ํ™˜๊ฒฝ๋ณ€์ˆ˜๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.")
38
+ return Client(endpoint)
39
+
40
+ # ์„ธ์…˜๋ณ„ ์ž„์‹œ ํŒŒ์ผ ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•œ ๋”•์…”๋„ˆ๋ฆฌ
41
+ session_temp_files = {}
42
+ session_data = {}
43
+
44
+ def cleanup_huggingface_temp_folders():
45
+ """ํ—ˆ๊น…ํŽ˜์ด์Šค ์ž„์‹œ ํด๋” ์ดˆ๊ธฐ ์ •๋ฆฌ"""
46
+ try:
47
+ # ์ผ๋ฐ˜์ ์ธ ์ž„์‹œ ๋””๋ ‰ํ† ๋ฆฌ๋“ค
48
+ temp_dirs = [
49
+ tempfile.gettempdir(),
50
+ "/tmp",
51
+ "/var/tmp",
52
+ os.path.join(os.getcwd(), "temp"),
53
+ os.path.join(os.getcwd(), "tmp"),
54
+ "/gradio_cached_examples",
55
+ "/flagged"
56
+ ]
57
+
58
+ cleanup_count = 0
59
+
60
+ for temp_dir in temp_dirs:
61
+ if os.path.exists(temp_dir):
62
+ try:
63
+ # ๊ธฐ์กด ์„ธ์…˜ ํŒŒ์ผ๋“ค ์ •๋ฆฌ
64
+ session_files = glob.glob(os.path.join(temp_dir, "session_*.xlsx"))
65
+ session_files.extend(glob.glob(os.path.join(temp_dir, "session_*.csv")))
66
+ session_files.extend(glob.glob(os.path.join(temp_dir, "*keyword*.xlsx")))
67
+ session_files.extend(glob.glob(os.path.join(temp_dir, "*keyword*.csv")))
68
+ session_files.extend(glob.glob(os.path.join(temp_dir, "tmp*.xlsx")))
69
+ session_files.extend(glob.glob(os.path.join(temp_dir, "tmp*.csv")))
70
+
71
+ for file_path in session_files:
72
+ try:
73
+ # ํŒŒ์ผ์ด 1์‹œ๊ฐ„ ์ด์ƒ ์˜ค๋ž˜๋œ ๊ฒฝ์šฐ๋งŒ ์‚ญ์ œ
74
+ if os.path.getmtime(file_path) < time.time() - 3600:
75
+ os.remove(file_path)
76
+ cleanup_count += 1
77
+ logger.info(f"์ดˆ๊ธฐ ์ •๋ฆฌ: ์˜ค๋ž˜๋œ ์ž„์‹œ ํŒŒ์ผ ์‚ญ์ œ - {file_path}")
78
+ except Exception as e:
79
+ logger.warning(f"ํŒŒ์ผ ์‚ญ์ œ ์‹คํŒจ (๋ฌด์‹œ๋จ): {file_path} - {e}")
80
+
81
+ except Exception as e:
82
+ logger.warning(f"์ž„์‹œ ๋””๋ ‰ํ† ๋ฆฌ ์ •๋ฆฌ ์‹คํŒจ (๋ฌด์‹œ๋จ): {temp_dir} - {e}")
83
+
84
+ logger.info(f"โœ… ํ—ˆ๊น…ํŽ˜์ด์Šค ์ž„์‹œ ํด๋” ์ดˆ๊ธฐ ์ •๋ฆฌ ์™„๋ฃŒ - {cleanup_count}๊ฐœ ํŒŒ์ผ ์‚ญ์ œ")
85
+
86
+ # Gradio ์บ์‹œ ํด๋”๋„ ์ •๋ฆฌ
87
+ try:
88
+ gradio_temp_dir = os.path.join(os.getcwd(), "gradio_cached_examples")
89
+ if os.path.exists(gradio_temp_dir):
90
+ shutil.rmtree(gradio_temp_dir, ignore_errors=True)
91
+ logger.info("Gradio ์บ์‹œ ํด๋” ์ •๋ฆฌ ์™„๋ฃŒ")
92
+ except Exception as e:
93
+ logger.warning(f"Gradio ์บ์‹œ ํด๋” ์ •๋ฆฌ ์‹คํŒจ (๋ฌด์‹œ๋จ): {e}")
94
+
95
+ except Exception as e:
96
+ logger.error(f"์ดˆ๊ธฐ ์ž„์‹œ ํด๋” ์ •๋ฆฌ ์ค‘ ์˜ค๋ฅ˜ (๊ณ„์† ์ง„ํ–‰): {e}")
97
+
98
+ def setup_clean_temp_environment():
99
+ """๊นจ๋—ํ•œ ์ž„์‹œ ํ™˜๊ฒฝ ์„ค์ •"""
100
+ try:
101
+ # 1. ๊ธฐ์กด ์ž„์‹œ ํŒŒ์ผ๋“ค ์ •๋ฆฌ
102
+ cleanup_huggingface_temp_folders()
103
+
104
+ # 2. ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ „์šฉ ์ž„์‹œ ๋””๋ ‰ํ† ๋ฆฌ ์ƒ์„ฑ
105
+ app_temp_dir = os.path.join(tempfile.gettempdir(), "control_tower_app")
106
+ if os.path.exists(app_temp_dir):
107
+ shutil.rmtree(app_temp_dir, ignore_errors=True)
108
+ os.makedirs(app_temp_dir, exist_ok=True)
109
+
110
+ # 3. ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์„ค์ • (์ž„์‹œ ๋””๋ ‰ํ† ๋ฆฌ ์ง€์ •)
111
+ os.environ['CONTROL_TOWER_TEMP'] = app_temp_dir
112
+
113
+ logger.info(f"โœ… ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ „์šฉ ์ž„์‹œ ๋””๋ ‰ํ† ๋ฆฌ ์„ค์ •: {app_temp_dir}")
114
+
115
+ return app_temp_dir
116
+
117
+ except Exception as e:
118
+ logger.error(f"์ž„์‹œ ํ™˜๊ฒฝ ์„ค์ • ์‹คํŒจ: {e}")
119
+ return tempfile.gettempdir()
120
+
121
+ def get_app_temp_dir():
122
+ """์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ „์šฉ ์ž„์‹œ ๋””๋ ‰ํ† ๋ฆฌ ๋ฐ˜ํ™˜"""
123
+ return os.environ.get('CONTROL_TOWER_TEMP', tempfile.gettempdir())
124
+
125
+ def get_session_id():
126
+ """์„ธ์…˜ ID ์ƒ์„ฑ"""
127
+ return str(uuid.uuid4())
128
+
129
+ def cleanup_session_files(session_id, delay=300):
130
+ """์„ธ์…˜๋ณ„ ์ž„์‹œ ํŒŒ์ผ ์ •๋ฆฌ ํ•จ์ˆ˜"""
131
+ def cleanup():
132
+ time.sleep(delay)
133
+ if session_id in session_temp_files:
134
+ files_to_remove = session_temp_files[session_id].copy()
135
+ del session_temp_files[session_id]
136
+
137
+ for file_path in files_to_remove:
138
+ try:
139
+ if os.path.exists(file_path):
140
+ os.remove(file_path)
141
+ logger.info(f"์„ธ์…˜ {session_id[:8]}... ์ž„์‹œ ํŒŒ์ผ ์‚ญ์ œ: {file_path}")
142
+ except Exception as e:
143
+ logger.error(f"์„ธ์…˜ {session_id[:8]}... ํŒŒ์ผ ์‚ญ์ œ ์˜ค๋ฅ˜: {e}")
144
+
145
+ threading.Thread(target=cleanup, daemon=True).start()
146
+
147
+ def register_session_file(session_id, file_path):
148
+ """์„ธ์…˜๋ณ„ ํŒŒ์ผ ๋“ฑ๋ก"""
149
+ if session_id not in session_temp_files:
150
+ session_temp_files[session_id] = []
151
+ session_temp_files[session_id].append(file_path)
152
+
153
+ def cleanup_old_sessions():
154
+ """์˜ค๋ž˜๋œ ์„ธ์…˜ ๋ฐ์ดํ„ฐ ์ •๋ฆฌ"""
155
+ current_time = time.time()
156
+ sessions_to_remove = []
157
+
158
+ for session_id, data in session_data.items():
159
+ if current_time - data.get('last_activity', 0) > 3600: # 1์‹œ๊ฐ„ ์ดˆ๊ณผ
160
+ sessions_to_remove.append(session_id)
161
+
162
+ for session_id in sessions_to_remove:
163
+ # ํŒŒ์ผ ์ •๋ฆฌ
164
+ if session_id in session_temp_files:
165
+ for file_path in session_temp_files[session_id]:
166
+ try:
167
+ if os.path.exists(file_path):
168
+ os.remove(file_path)
169
+ logger.info(f"์˜ค๋ž˜๋œ ์„ธ์…˜ {session_id[:8]}... ํŒŒ์ผ ์‚ญ์ œ: {file_path}")
170
+ except Exception as e:
171
+ logger.error(f"์˜ค๋ž˜๋œ ์„ธ์…˜ ํŒŒ์ผ ์‚ญ์ œ ์˜ค๋ฅ˜: {e}")
172
+ del session_temp_files[session_id]
173
+
174
+ # ์„ธ์…˜ ๋ฐ์ดํ„ฐ ์ •๋ฆฌ
175
+ if session_id in session_data:
176
+ del session_data[session_id]
177
+ logger.info(f"์˜ค๋ž˜๋œ ์„ธ์…˜ ๋ฐ์ดํ„ฐ ์‚ญ์ œ: {session_id[:8]}...")
178
+
179
+ def update_session_activity(session_id):
180
+ """์„ธ์…˜ ํ™œ๋™ ์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ"""
181
+ if session_id not in session_data:
182
+ session_data[session_id] = {}
183
+ session_data[session_id]['last_activity'] = time.time()
184
+
185
+ def create_session_temp_file(session_id, suffix='.xlsx'):
186
+ """์„ธ์…˜๋ณ„ ์ž„์‹œ ํŒŒ์ผ ์ƒ์„ฑ (์ „์šฉ ๋””๋ ‰ํ† ๋ฆฌ ์‚ฌ์šฉ)"""
187
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
188
+ random_suffix = str(random.randint(1000, 9999))
189
+
190
+ # ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ „์šฉ ์ž„์‹œ ๋””๋ ‰ํ† ๋ฆฌ ์‚ฌ์šฉ
191
+ temp_dir = get_app_temp_dir()
192
+ filename = f"session_{session_id[:8]}_{timestamp}_{random_suffix}{suffix}"
193
+ temp_file_path = os.path.join(temp_dir, filename)
194
+
195
+ # ๋นˆ ํŒŒ์ผ ์ƒ์„ฑ
196
+ with open(temp_file_path, 'w') as f:
197
+ pass
198
+
199
+ register_session_file(session_id, temp_file_path)
200
+ return temp_file_path
201
+
202
+ def wrapper_modified(keyword, korean_only, apply_main_keyword_option, exclude_zero_volume, session_id):
203
+ """ํ‚ค์›Œ๋“œ ๊ฒ€์ƒ‰ ๋ฐ ์ฒ˜๋ฆฌ ๋ž˜ํผ ํ•จ์ˆ˜ (API ํด๋ผ์ด์–ธํŠธ ์‚ฌ์šฉ)"""
204
+ update_session_activity(session_id)
205
+
206
+ try:
207
+ client = get_api_client()
208
+
209
+ # API ํ˜ธ์ถœ
210
+ result = client.predict(
211
+ keyword=keyword,
212
+ korean_only=korean_only,
213
+ apply_main_keyword=apply_main_keyword_option,
214
+ exclude_zero_volume=exclude_zero_volume,
215
+ api_name="/process_search_results"
216
+ )
217
+
218
+ # ๊ฒฐ๊ณผ ์ฒ˜๋ฆฌ
219
+ table_html, cat_choices, vol_choices, selected_cat, download_file = result
220
+
221
+ # ๋‹ค์šด๋กœ๋“œ ํŒŒ์ผ์ด ์žˆ๋Š” ๊ฒฝ์šฐ ๋กœ์ปฌ๋กœ ๋ณต์‚ฌ
222
+ excel_path = None
223
+ if download_file:
224
+ excel_path = create_session_temp_file(session_id, '.xlsx')
225
+ # ์›๊ฒฉ ํŒŒ์ผ์„ ๋กœ์ปฌ๋กœ ๋ณต์‚ฌ
226
+ try:
227
+ import shutil
228
+ shutil.copy2(download_file, excel_path)
229
+ except Exception as e:
230
+ logger.error(f"ํŒŒ์ผ ๋ณต์‚ฌ ์˜ค๋ฅ˜: {e}")
231
+ excel_path = None
232
+
233
+ # ๊ฐ€์ƒ์˜ DataFrame ์ƒํƒœ (์‹ค์ œ๋กœ๋Š” ์‚ฌ์šฉ๋˜์ง€ ์•Š์Œ)
234
+ df_state = pd.DataFrame()
235
+
236
+ if table_html and "๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค" not in table_html:
237
+ return (gr.update(value=table_html), gr.update(choices=cat_choices),
238
+ gr.update(choices=vol_choices), df_state,
239
+ gr.update(choices=cat_choices, value=selected_cat), excel_path,
240
+ gr.update(visible=True), gr.update(visible=True), keyword)
241
+ else:
242
+ return (gr.update(value="<p>๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ๋‹ค๋ฅธ ํ‚ค์›Œ๋“œ๋กœ ์‹œ๋„ํ•ด๋ณด์„ธ์š”.</p>"),
243
+ gr.update(choices=["์ „์ฒด ๋ณด๊ธฐ"]), gr.update(choices=["์ „์ฒด"]),
244
+ df_state, gr.update(choices=["์ „์ฒด ๋ณด๊ธฐ"], value="์ „์ฒด ๋ณด๊ธฐ"), None,
245
+ gr.update(visible=False), gr.update(visible=False), keyword)
246
+
247
+ except Exception as e:
248
+ logger.error(f"API ํ˜ธ์ถœ ์˜ค๋ฅ˜: {e}")
249
+ return (gr.update(value="<p>์„œ๋น„์Šค ์—ฐ๊ฒฐ์— ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.</p>"),
250
+ gr.update(choices=["์ „์ฒด ๋ณด๊ธฐ"]), gr.update(choices=["์ „์ฒด"]),
251
+ pd.DataFrame(), gr.update(choices=["์ „์ฒด ๋ณด๊ธฐ"], value="์ „์ฒด ๋ณด๊ธฐ"), None,
252
+ gr.update(visible=False), gr.update(visible=False), "")
253
+
254
+ def analyze_with_auto_download(analysis_keywords, selected_category, state_df, session_id):
255
+ """์นดํ…Œ๊ณ ๋ฆฌ ์ผ์น˜ ๋ถ„์„ ์‹คํ–‰ ๋ฐ ์ž๋™ ๋‹ค์šด๋กœ๋“œ (API ํด๋ผ์ด์–ธํŠธ ์‚ฌ์šฉ)"""
256
+ update_session_activity(session_id)
257
+
258
+ try:
259
+ client = get_api_client()
260
+
261
+ # API ํ˜ธ์ถœ
262
+ result = client.predict(
263
+ analysis_keywords=analysis_keywords,
264
+ selected_category=selected_category,
265
+ api_name="/process_analyze_results"
266
+ )
267
+
268
+ analysis_result, download_file = result
269
+
270
+ # ๋‹ค์šด๋กœ๋“œ ํŒŒ์ผ์ด ์žˆ๋Š” ๊ฒฝ์šฐ ๋กœ์ปฌ๋กœ ๋ณต์‚ฌ
271
+ excel_path = None
272
+ if download_file:
273
+ excel_path = create_session_temp_file(session_id, '.xlsx')
274
+ try:
275
+ import shutil
276
+ shutil.copy2(download_file, excel_path)
277
+ except Exception as e:
278
+ logger.error(f"๋ถ„์„ ๊ฒฐ๊ณผ ํŒŒ์ผ ๋ณต์‚ฌ ์˜ค๋ฅ˜: {e}")
279
+ excel_path = None
280
+
281
+ return analysis_result, excel_path, gr.update(visible=True)
282
+
283
+ except Exception as e:
284
+ logger.error(f"๋ถ„์„ API ํ˜ธ์ถœ ์˜ค๋ฅ˜: {e}")
285
+ return "๋ถ„์„ ์„œ๋น„์Šค ์—ฐ๊ฒฐ์— ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.", None, gr.update(visible=True)
286
+
287
+ def filter_and_sort_table(df, selected_cat, keyword_sort, total_volume_sort, usage_count_sort, selected_volume_range, exclude_zero_volume, session_id):
288
+ """ํ…Œ์ด๋ธ” ํ•„ํ„ฐ๋ง ๋ฐ ์ •๋ ฌ ํ•จ์ˆ˜ (API ํด๋ผ์ด์–ธํŠธ ์‚ฌ์šฉ)"""
289
+ update_session_activity(session_id)
290
+
291
+ try:
292
+ client = get_api_client()
293
+
294
+ # API ํ˜ธ์ถœ
295
+ result = client.predict(
296
+ selected_cat=selected_cat,
297
+ keyword_sort=keyword_sort,
298
+ total_volume_sort=total_volume_sort,
299
+ usage_count_sort=usage_count_sort,
300
+ selected_volume_range=selected_volume_range,
301
+ exclude_zero_volume=exclude_zero_volume,
302
+ api_name="/filter_and_sort_table"
303
+ )
304
+
305
+ return result
306
+
307
+ except Exception as e:
308
+ logger.error(f"ํ•„ํ„ฐ๋ง API ํ˜ธ์ถœ ์˜ค๋ฅ˜: {e}")
309
+ return "<p>ํ•„ํ„ฐ๋ง ์„œ๋น„์Šค ์—ฐ๊ฒฐ์— ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.</p>"
310
+
311
+ def update_category_selection(selected_cat, session_id):
312
+ """์นดํ…Œ๊ณ ๋ฆฌ ํ•„ํ„ฐ ์„ ํƒ ์‹œ ๋ถ„์„ํ•  ์นดํ…Œ๊ณ ๋ฆฌ๋„ ๊ฐ™์€ ๊ฐ’์œผ๋กœ ์—…๋ฐ์ดํŠธ (API ํด๋ผ์ด์–ธํŠธ ์‚ฌ์šฉ)"""
313
+ update_session_activity(session_id)
314
+
315
+ try:
316
+ client = get_api_client()
317
+
318
+ result = client.predict(
319
+ selected_cat=selected_cat,
320
+ api_name="/update_category_selection"
321
+ )
322
+
323
+ return gr.update(value=result)
324
+
325
+ except Exception as e:
326
+ logger.error(f"์นดํ…Œ๊ณ ๋ฆฌ ์„ ํƒ API ํ˜ธ์ถœ ์˜ค๋ฅ˜: {e}")
327
+ return gr.update(value=selected_cat)
328
+
329
+ def reset_interface(session_id):
330
+ """์ธํ„ฐํŽ˜์ด์Šค ๋ฆฌ์…‹ ํ•จ์ˆ˜ - ์„ธ์…˜๋ณ„ ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™”"""
331
+ update_session_activity(session_id)
332
+
333
+ # ์„ธ์…˜๋ณ„ ์ž„์‹œ ํŒŒ์ผ ์ •๋ฆฌ
334
+ if session_id in session_temp_files:
335
+ for file_path in session_temp_files[session_id]:
336
+ try:
337
+ if os.path.exists(file_path):
338
+ os.remove(file_path)
339
+ logger.info(f"์„ธ์…˜ {session_id[:8]}... ๋ฆฌ์…‹ ์‹œ ํŒŒ์ผ ์‚ญ์ œ: {file_path}")
340
+ except Exception as e:
341
+ logger.error(f"์„ธ์…˜ {session_id[:8]}... ๋ฆฌ์…‹ ์‹œ ํŒŒ์ผ ์‚ญ์ œ ์˜ค๋ฅ˜: {e}")
342
+ session_temp_files[session_id] = []
343
+
344
+ try:
345
+ client = get_api_client()
346
+
347
+ result = client.predict(api_name="/reset_interface")
348
+
349
+ return result
350
+
351
+ except Exception as e:
352
+ logger.error(f"๋ฆฌ์…‹ API ํ˜ธ์ถœ ์˜ค๋ฅ˜: {e}")
353
+ # ๊ธฐ๋ณธ ๋ฆฌ์…‹ ๊ฐ’ ๋ฐ˜ํ™˜
354
+ return (
355
+ "", # ๊ฒ€์ƒ‰ ํ‚ค์›Œ๋“œ
356
+ True, # ํ•œ๊ธ€๋งŒ ์ถ”์ถœ
357
+ False, # ๊ฒ€์ƒ‰๋Ÿ‰ 0 ํ‚ค์›Œ๋“œ ์ œ์™ธ
358
+ "๋ฉ”์ธํ‚ค์›Œ๋“œ ์ ์šฉ", # ์กฐํ•ฉ ๋ฐฉ์‹
359
+ "", # HTML ํ…Œ์ด๋ธ”
360
+ ["์ „์ฒด ๋ณด๊ธฐ"], # ์นดํ…Œ๊ณ ๋ฆฌ ํ•„ํ„ฐ
361
+ "์ „์ฒด ๋ณด๊ธฐ", # ์นดํ…Œ๊ณ ๋ฆฌ ํ•„ํ„ฐ ์„ ํƒ
362
+ ["์ „์ฒด"], # ๊ฒ€์ƒ‰๋Ÿ‰ ๊ตฌ๊ฐ„ ํ•„ํ„ฐ
363
+ "์ „์ฒด", # ๊ฒ€์ƒ‰๋Ÿ‰ ๊ตฌ๊ฐ„ ์„ ํƒ
364
+ "์ •๋ ฌ ์—†์Œ", # ์ด๊ฒ€์ƒ‰๋Ÿ‰ ์ •๋ ฌ
365
+ "์ •๋ ฌ ์—†์Œ", # ํ‚ค์›Œ๋“œ ์‚ฌ์šฉํšŸ์ˆ˜ ์ •๋ ฌ
366
+ ["์ „์ฒด ๋ณด๊ธฐ"], # ๋ถ„์„ํ•  ์นดํ…Œ๊ณ ๋ฆฌ
367
+ "์ „์ฒด ๋ณด๊ธฐ", # ๋ถ„์„ํ•  ์นดํ…Œ๊ณ ๋ฆฌ ์„ ํƒ
368
+ "", # ํ‚ค์›Œ๋“œ ์ž…๋ ฅ
369
+ "", # ๋ถ„์„ ๊ฒฐ๊ณผ
370
+ None, # ๋‹ค์šด๋กœ๋“œ ํŒŒ์ผ
371
+ gr.update(visible=False), # ํ‚ค์›Œ๋“œ ๋ถ„์„ ์„น์…˜
372
+ gr.update(visible=False), # ๋ถ„์„ ๊ฒฐ๊ณผ ์ถœ๋ ฅ ์„น์…˜
373
+ "" # ํ‚ค์›Œ๋“œ ์ƒํƒœ
374
+ )
375
+
376
+ # ๋ž˜ํผ ํ•จ์ˆ˜๋“ค
377
+ def search_with_loading(keyword, korean_only, apply_main_keyword, exclude_zero_volume, session_id):
378
+ update_session_activity(session_id)
379
+ return (
380
+ gr.update(visible=True),
381
+ gr.update(visible=False)
382
+ )
383
+
384
+ def process_search_results(keyword, korean_only, apply_main_keyword, exclude_zero_volume, session_id):
385
+ update_session_activity(session_id)
386
+
387
+ result = wrapper_modified(keyword, korean_only, apply_main_keyword, exclude_zero_volume, session_id)
388
+
389
+ table_html, cat_choices, vol_choices, df, selected_cat, excel, keyword_section_vis, cat_section_vis, new_keyword_state = result
390
+
391
+ if table_html and "๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค" not in table_html:
392
+ empty_placeholder_vis = False
393
+ keyword_section_visibility = True
394
+ execution_section_visibility = True
395
+ else:
396
+ empty_placeholder_vis = True
397
+ keyword_section_visibility = False
398
+ execution_section_visibility = False
399
+
400
+ return (
401
+ table_html, cat_choices, vol_choices, df, selected_cat, excel,
402
+ gr.update(visible=keyword_section_visibility),
403
+ gr.update(visible=cat_section_vis),
404
+ gr.update(visible=False),
405
+ gr.update(visible=empty_placeholder_vis),
406
+ gr.update(visible=execution_section_visibility),
407
+ new_keyword_state
408
+ )
409
+
410
+ def analyze_with_loading(analysis_keywords, selected_category, state_df, session_id):
411
+ update_session_activity(session_id)
412
+ return gr.update(visible=True)
413
+
414
+ def process_analyze_results(analysis_keywords, selected_category, state_df, session_id):
415
+ update_session_activity(session_id)
416
+ results = analyze_with_auto_download(analysis_keywords, selected_category, state_df, session_id)
417
+ return results + (gr.update(visible=False),)
418
+
419
+ # ์„ธ์…˜ ์ •๋ฆฌ ์Šค์ผ€์ค„๋Ÿฌ
420
+ def start_session_cleanup_scheduler():
421
+ """์„ธ์…˜ ์ •๋ฆฌ ์Šค์ผ€์ค„๋Ÿฌ ์‹œ์ž‘"""
422
+ def cleanup_scheduler():
423
+ while True:
424
+ time.sleep(600) # 10๋ถ„๋งˆ๋‹ค ์‹คํ–‰
425
+ cleanup_old_sessions()
426
+ # ์ถ”๊ฐ€๋กœ ํ—ˆ๊น…ํŽ˜์ด์Šค ์ž„์‹œ ํด๋”๋„ ์ฃผ๊ธฐ์  ์ •๋ฆฌ
427
+ cleanup_huggingface_temp_folders()
428
+
429
+ threading.Thread(target=cleanup_scheduler, daemon=True).start()
430
+
431
+ def cleanup_on_startup():
432
+ """์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์‹œ์ž‘ ์‹œ ์ „์ฒด ์ •๋ฆฌ"""
433
+ logger.info("๐Ÿงน ์ปจํŠธ๋กค ํƒ€์›Œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์‹œ์ž‘ - ์ดˆ๊ธฐ ์ •๋ฆฌ ์ž‘์—… ์‹œ์ž‘...")
434
+
435
+ # 1. ํ—ˆ๊น…ํŽ˜์ด์Šค ์ž„์‹œ ํด๋” ์ •๋ฆฌ
436
+ cleanup_huggingface_temp_folders()
437
+
438
+ # 2. ๊นจ๋—ํ•œ ์ž„์‹œ ํ™˜๊ฒฝ ์„ค์ •
439
+ app_temp_dir = setup_clean_temp_environment()
440
+
441
+ # 3. ์ „์—ญ ๋ณ€์ˆ˜ ์ดˆ๊ธฐํ™”
442
+ global session_temp_files, session_data
443
+ session_temp_files.clear()
444
+ session_data.clear()
445
+
446
+ logger.info(f"โœ… ์ดˆ๊ธฐ ์ •๋ฆฌ ์ž‘์—… ์™„๋ฃŒ - ์•ฑ ์ „์šฉ ๋””๋ ‰ํ† ๋ฆฌ: {app_temp_dir}")
447
+
448
+ return app_temp_dir
449
+
450
+ # Gradio ์ธํ„ฐํŽ˜์ด์Šค ์ƒ์„ฑ
451
+ def create_app():
452
+ fontawesome_html = """
453
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
454
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/static/pretendard.css">
455
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap">
456
+ """
457
+
458
+ # CSS ํŒŒ์ผ ๋กœ๋“œ
459
+ try:
460
+ with open('style.css', 'r', encoding='utf-8') as f:
461
+ custom_css = f.read()
462
+ except:
463
+ custom_css = """
464
+ :root {
465
+ --primary-color: #FB7F0D;
466
+ --secondary-color: #ff9a8b;
467
+ }
468
+ .custom-button {
469
+ background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)) !important;
470
+ color: white !important;
471
+ border-radius: 30px !important;
472
+ height: 45px !important;
473
+ font-size: 16px !important;
474
+ font-weight: bold !important;
475
+ width: 100% !important;
476
+ text-align: center !important;
477
+ display: flex !important;
478
+ align-items: center !important;
479
+ justify-content: center !important;
480
+ }
481
+ """
482
+
483
+ with gr.Blocks(css=custom_css, theme=gr.themes.Default(
484
+ primary_hue="orange",
485
+ secondary_hue="orange",
486
+ font=[gr.themes.GoogleFont("Noto Sans KR"), "ui-sans-serif", "system-ui"]
487
+ )) as demo:
488
+ gr.HTML(fontawesome_html)
489
+
490
+ # ์„ธ์…˜ ID ์ƒํƒœ (๊ฐ ์‚ฌ์šฉ์ž๋ณ„๋กœ ๊ณ ์œ )
491
+ session_id = gr.State(get_session_id)
492
+
493
+ # ํ‚ค์›Œ๋“œ ์ƒํƒœ ๊ด€๋ฆฌ
494
+ keyword_state = gr.State("")
495
+
496
+ # ์ž…๋ ฅ ์„น์…˜
497
+ with gr.Column(elem_classes="custom-frame fade-in"):
498
+ gr.HTML('<div class="section-title"><i class="fas fa-search"></i> ๊ฒ€์ƒ‰ ์ž…๋ ฅ</div>')
499
+
500
+ with gr.Row():
501
+ with gr.Column(scale=1):
502
+ keyword = gr.Textbox(
503
+ label="๋ฉ”์ธ ํ‚ค์›Œ๋“œ",
504
+ placeholder="์˜ˆ: ์˜ค์ง•์–ด"
505
+ )
506
+ with gr.Column(scale=1):
507
+ search_btn = gr.Button(
508
+ "๋ฉ”์ธํ‚ค์›Œ๋“œ ๋ถ„์„",
509
+ elem_classes="custom-button"
510
+ )
511
+
512
+ with gr.Accordion("์˜ต์…˜ ์„ค์ •", open=False):
513
+ with gr.Row():
514
+ with gr.Column(scale=1):
515
+ korean_only = gr.Checkbox(
516
+ label="ํ•œ๊ธ€๋งŒ ์ถ”์ถœ",
517
+ value=True
518
+ )
519
+ with gr.Column(scale=1):
520
+ exclude_zero_volume = gr.Checkbox(
521
+ label="๊ฒ€์ƒ‰๋Ÿ‰ 0 ํ‚ค์›Œ๋“œ ์ œ์™ธ",
522
+ value=False
523
+ )
524
+
525
+ with gr.Row():
526
+ with gr.Column(scale=1):
527
+ apply_main_keyword = gr.Radio(
528
+ ["๋ฉ”์ธํ‚ค์›Œ๋“œ ์ ์šฉ", "๋ฉ”์ธํ‚ค์›Œ๋“œ ๋ฏธ์ ์šฉ"],
529
+ label="์กฐํ•ฉ ๋ฐฉ์‹",
530
+ value="๋ฉ”์ธํ‚ค์›Œ๋“œ ์ ์šฉ"
531
+ )
532
+ with gr.Column(scale=1):
533
+ gr.HTML("")
534
+
535
+ # ์ง„ํ–‰ ์ƒํƒœ ํ‘œ์‹œ ์„น์…˜
536
+ with gr.Column(elem_classes="custom-frame fade-in", visible=False) as progress_section:
537
+ gr.HTML('<div class="section-title"><i class="fas fa-spinner"></i> ๋ถ„์„ ์ง„ํ–‰ ์ƒํƒœ</div>')
538
+ progress_html = gr.HTML("""
539
+ <div style="padding: 15px; background-color: #f9f9f9; border-radius: 5px; margin: 10px 0; border: 1px solid #ddd;">
540
+ <div style="margin-bottom: 10px; display: flex; align-items: center;">
541
+ <i class="fas fa-spinner fa-spin" style="color: #FB7F0D; margin-right: 10px;"></i>
542
+ <span>ํ‚ค์›Œ๋“œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ถ„์„์ค‘์ž…๋‹ˆ๋‹ค. ์ž ์‹œ๋งŒ ๊ธฐ๋‹ค๋ ค์ฃผ์„ธ์š”...</span>
543
+ </div>
544
+ <div style="background-color: #e9ecef; height: 10px; border-radius: 5px; overflow: hidden;">
545
+ <div class="progress-bar"></div>
546
+ </div>
547
+ </div>
548
+ """)
549
+
550
+ # ๋ฉ”์ธํ‚ค์›Œ๋“œ ๋ถ„์„ ๊ฒฐ๊ณผ ์„น์…˜
551
+ with gr.Column(elem_classes="custom-frame fade-in") as main_keyword_section:
552
+ gr.HTML('<div class="section-title"><i class="fas fa-table"></i> ๋ฉ”์ธํ‚ค์›Œ๋“œ ๋ถ„์„ ๊ฒฐ๊ณผ</div>')
553
+
554
+ empty_table_html = gr.HTML("""
555
+ <table class="empty-table">
556
+ <thead>
557
+ <tr>
558
+ <th>์ˆœ๋ฒˆ</th>
559
+ <th>์กฐํ•ฉ ํ‚ค์›Œ๋“œ</th>
560
+ <th>PC๊ฒ€์ƒ‰๋Ÿ‰</th>
561
+ <th>๋ชจ๋ฐ”์ผ๊ฒ€์ƒ‰๋Ÿ‰</th>
562
+ <th>์ด๊ฒ€์ƒ‰๋Ÿ‰</th>
563
+ <th>๊ฒ€์ƒ‰๋Ÿ‰๊ตฌ๊ฐ„</th>
564
+ <th>ํ‚ค์›Œ๋“œ ์‚ฌ์šฉ์ž์ˆœ์œ„</th>
565
+ <th>ํ‚ค์›Œ๋“œ ์‚ฌ์šฉํšŸ์ˆ˜</th>
566
+ <th>์ƒํ’ˆ ๋“ฑ๋ก ์นดํ…Œ๊ณ ๋ฆฌ</th>
567
+ </tr>
568
+ </thead>
569
+ <tbody>
570
+ <tr>
571
+ <td colspan="9" style="padding: 30px; text-align: center;">
572
+ ๊ฒ€์ƒ‰์„ ์‹คํ–‰ํ•˜๋ฉด ์—ฌ๊ธฐ์— ๊ฒฐ๊ณผ๊ฐ€ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค
573
+ </td>
574
+ </tr>
575
+ </tbody>
576
+ </table>
577
+ """)
578
+
579
+ with gr.Column(visible=False) as keyword_analysis_section:
580
+ with gr.Row():
581
+ with gr.Column(scale=1):
582
+ category_filter = gr.Dropdown(
583
+ choices=["์ „์ฒด ๋ณด๊ธฐ"],
584
+ label="์นดํ…Œ๊ณ ๋ฆฌ ํ•„ํ„ฐ",
585
+ value="์ „์ฒด ๋ณด๊ธฐ",
586
+ interactive=True
587
+ )
588
+ with gr.Column(scale=1):
589
+ total_volume_sort = gr.Dropdown(
590
+ choices=["์ •๋ ฌ ์—†์Œ", "์˜ค๋ฆ„์ฐจ์ˆœ", "๋‚ด๋ฆผ์ฐจ์ˆœ"],
591
+ label="์ด๊ฒ€์ƒ‰๋Ÿ‰ ์ •๋ ฌ",
592
+ value="์ •๋ ฌ ์—†์Œ",
593
+ interactive=True
594
+ )
595
+
596
+ with gr.Row():
597
+ with gr.Column(scale=1):
598
+ search_volume_filter = gr.Dropdown(
599
+ choices=["์ „์ฒด"],
600
+ label="๊ฒ€์ƒ‰๋Ÿ‰ ๊ตฌ๊ฐ„ ํ•„ํ„ฐ",
601
+ value="์ „์ฒด",
602
+ interactive=True
603
+ )
604
+ with gr.Column(scale=1):
605
+ usage_count_sort = gr.Dropdown(
606
+ choices=["์ •๋ ฌ ์—†์Œ", "์˜ค๋ฆ„์ฐจ์ˆœ", "๋‚ด๋ฆผ์ฐจ์ˆœ"],
607
+ label="ํ‚ค์›Œ๋“œ ์‚ฌ์šฉํšŸ์ˆ˜ ์ •๋ ฌ",
608
+ value="์ •๋ ฌ ์—†์Œ",
609
+ interactive=True
610
+ )
611
+
612
+ gr.HTML("<div class='data-container' id='table_container'></div>")
613
+ table_output = gr.HTML(elem_classes="fade-in")
614
+
615
+ # ์นดํ…Œ๊ณ ๋ฆฌ ๋ถ„์„ ์„น์…˜
616
+ with gr.Column(elem_classes="custom-frame fade-in", visible=False) as category_analysis_section:
617
+ gr.HTML('<div class="section-title"><i class="fas fa-chart-bar"></i> ํ‚ค์›Œ๋“œ ๋ถ„์„</div>')
618
+
619
+ with gr.Row():
620
+ with gr.Column(scale=1):
621
+ analysis_keywords = gr.Textbox(
622
+ label="ํ‚ค์›Œ๋“œ ์ž…๋ ฅ (์ตœ๋Œ€ 20๊ฐœ, ์‰ผํ‘œ ๋˜๋Š” ์—”ํ„ฐ๋กœ ๊ตฌ๋ถ„)",
623
+ placeholder="์˜ˆ: ์˜ค์ง•์–ด๋ณถ์Œ, ์˜ค์ง•์–ด ์†์งˆ, ์˜ค์ง•์–ด ์š”๋ฆฌ...",
624
+ lines=5
625
+ )
626
+
627
+ with gr.Column(scale=1):
628
+ selected_category = gr.Dropdown(
629
+ label="๋ถ„์„ํ•  ์นดํ…Œ๊ณ ๋ฆฌ(๋ถ„์„ ์ „ ๋ฐ˜๋“œ์‹œ ์„ ํƒํ•ด์ฃผ์„ธ์š”)",
630
+ choices=["์ „์ฒด ๋ณด๊ธฐ"],
631
+ value="์ „์ฒด ๋ณด๊ธฐ",
632
+ interactive=True
633
+ )
634
+
635
+ # ์‹คํ–‰ ์„น์…˜
636
+ with gr.Column(elem_classes="execution-section", visible=False) as execution_section:
637
+ gr.HTML('<div class="section-title"><i class="fas fa-play-circle"></i> ์‹คํ–‰</div>')
638
+ with gr.Row():
639
+ with gr.Column(scale=1):
640
+ analyze_btn = gr.Button(
641
+ "์นดํ…Œ๊ณ ๋ฆฌ ์ผ์น˜ ๋ถ„์„",
642
+ elem_classes=["execution-button", "primary-button"]
643
+ )
644
+ with gr.Column(scale=1):
645
+ reset_btn = gr.Button(
646
+ "๋ชจ๋“  ์ž…๋ ฅ ์ดˆ๊ธฐํ™”",
647
+ elem_classes=["execution-button", "secondary-button"]
648
+ )
649
+
650
+ # ๋ถ„์„ ๊ฒฐ๊ณผ ์ถœ๋ ฅ ์„น์…˜
651
+ with gr.Column(elem_classes="custom-frame fade-in", visible=False) as analysis_output_section:
652
+ gr.HTML('<div class="section-title"><i class="fas fa-list-ul"></i> ๋ถ„์„ ๊ฒฐ๊ณผ ์š”์•ฝ</div>')
653
+
654
+ analysis_result = gr.HTML(elem_classes="fade-in")
655
+
656
+ with gr.Row():
657
+ download_output = gr.File(
658
+ label="ํ‚ค์›Œ๋“œ ๋ชฉ๋ก ๋‹ค์šด๋กœ๋“œ",
659
+ visible=True
660
+ )
661
+
662
+ # ์ƒํƒœ ์ €์žฅ์šฉ ๋ณ€์ˆ˜
663
+ state_df = gr.State()
664
+
665
+ # ์ด๋ฒคํŠธ ์—ฐ๊ฒฐ - ๋ชจ๋“  ํ•จ์ˆ˜์— session_id ์ถ”๊ฐ€
666
+ search_btn.click(
667
+ fn=search_with_loading,
668
+ inputs=[keyword, korean_only, apply_main_keyword, exclude_zero_volume, session_id],
669
+ outputs=[progress_section, empty_table_html]
670
+ ).then(
671
+ fn=process_search_results,
672
+ inputs=[keyword, korean_only, apply_main_keyword, exclude_zero_volume, session_id],
673
+ outputs=[
674
+ table_output, category_filter, search_volume_filter,
675
+ state_df, selected_category, download_output,
676
+ keyword_analysis_section, category_analysis_section,
677
+ progress_section, empty_table_html, execution_section,
678
+ keyword_state
679
+ ]
680
+ )
681
+
682
+ # ํ•„ํ„ฐ ๋ฐ ์ •๋ ฌ ๋ณ€๊ฒฝ ์ด๋ฒคํŠธ ์—ฐ๊ฒฐ - session_id ์ถ”๊ฐ€
683
+ category_filter.change(
684
+ fn=filter_and_sort_table,
685
+ inputs=[
686
+ state_df, category_filter, gr.Textbox(value="์ •๋ ฌ ์—†์Œ", visible=False),
687
+ total_volume_sort, usage_count_sort,
688
+ search_volume_filter, exclude_zero_volume, session_id
689
+ ],
690
+ outputs=[table_output]
691
+ )
692
+
693
+ category_filter.change(
694
+ fn=update_category_selection,
695
+ inputs=[category_filter, session_id],
696
+ outputs=[selected_category]
697
+ )
698
+
699
+ total_volume_sort.change(
700
+ fn=filter_and_sort_table,
701
+ inputs=[
702
+ state_df, category_filter, gr.Textbox(value="์ •๋ ฌ ์—†์Œ", visible=False),
703
+ total_volume_sort, usage_count_sort,
704
+ search_volume_filter, exclude_zero_volume, session_id
705
+ ],
706
+ outputs=[table_output]
707
+ )
708
+
709
+ usage_count_sort.change(
710
+ fn=filter_and_sort_table,
711
+ inputs=[
712
+ state_df, category_filter, gr.Textbox(value="์ •๋ ฌ ์—†์Œ", visible=False),
713
+ total_volume_sort, usage_count_sort,
714
+ search_volume_filter, exclude_zero_volume, session_id
715
+ ],
716
+ outputs=[table_output]
717
+ )
718
+
719
+ search_volume_filter.change(
720
+ fn=filter_and_sort_table,
721
+ inputs=[
722
+ state_df, category_filter, gr.Textbox(value="์ •๋ ฌ ์—†์Œ", visible=False),
723
+ total_volume_sort, usage_count_sort,
724
+ search_volume_filter, exclude_zero_volume, session_id
725
+ ],
726
+ outputs=[table_output]
727
+ )
728
+
729
+ exclude_zero_volume.change(
730
+ fn=filter_and_sort_table,
731
+ inputs=[
732
+ state_df, category_filter, gr.Textbox(value="์ •๋ ฌ ์—†์Œ", visible=False),
733
+ total_volume_sort, usage_count_sort,
734
+ search_volume_filter, exclude_zero_volume, session_id
735
+ ],
736
+ outputs=[table_output]
737
+ )
738
+
739
+ # ์นดํ…Œ๊ณ ๋ฆฌ ๋ถ„์„ ๋ฒ„ํŠผ ์ด๋ฒคํŠธ - session_id ์ถ”๊ฐ€
740
+ analyze_btn.click(
741
+ fn=analyze_with_loading,
742
+ inputs=[analysis_keywords, selected_category, state_df, session_id],
743
+ outputs=[progress_section]
744
+ ).then(
745
+ fn=process_analyze_results,
746
+ inputs=[analysis_keywords, selected_category, state_df, session_id],
747
+ outputs=[analysis_result, download_output, analysis_output_section, progress_section]
748
+ )
749
+
750
+ # ๋ฆฌ์…‹ ๋ฒ„ํŠผ ์ด๋ฒคํŠธ ์—ฐ๊ฒฐ - session_id ์ถ”๊ฐ€
751
+ reset_btn.click(
752
+ fn=reset_interface,
753
+ inputs=[session_id],
754
+ outputs=[
755
+ keyword, korean_only, exclude_zero_volume, apply_main_keyword,
756
+ table_output, category_filter, category_filter,
757
+ search_volume_filter, search_volume_filter,
758
+ total_volume_sort, usage_count_sort,
759
+ selected_category, selected_category,
760
+ analysis_keywords, analysis_result, download_output,
761
+ keyword_analysis_section, analysis_output_section,
762
+ keyword_state
763
+ ]
764
+ )
765
+
766
+ return demo
767
+
768
+ if __name__ == "__main__":
769
+ # ========== ์‹œ์ž‘ ์‹œ ์ „์ฒด ์ดˆ๊ธฐํ™” ==========
770
+ logger.info("๐Ÿš€ ์ปจํŠธ๋กค ํƒ€์›Œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์‹œ์ž‘...")
771
+
772
+ # 1. ์ฒซ ๋ฒˆ์งธ: ํ—ˆ๊น…ํŽ˜์ด์Šค ์ž„์‹œ ํด๋” ์ •๋ฆฌ ๋ฐ ํ™˜๊ฒฝ ์„ค์ •
773
+ app_temp_dir = cleanup_on_startup()
774
+
775
+ # 2. ์„ธ์…˜ ์ •๋ฆฌ ์Šค์ผ€์ค„๋Ÿฌ ์‹œ์ž‘
776
+ start_session_cleanup_scheduler()
777
+
778
+ # 3. API ์—ฐ๊ฒฐ ํ…Œ์ŠคํŠธ
779
+ try:
780
+ test_client = get_api_client()
781
+ logger.info("โœ… API ์—ฐ๊ฒฐ ํ…Œ์ŠคํŠธ ์„ฑ๊ณต")
782
+ except Exception as e:
783
+ logger.error(f"โŒ API ์—ฐ๊ฒฐ ์‹คํŒจ: {e}")
784
+ raise
785
+
786
+ logger.info("===== ์ปจํŠธ๋กค ํƒ€์›Œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์‹œ์ž‘ ์™„๋ฃŒ at %s =====", time.strftime("%Y-%m-%d %H:%M:%S"))
787
+ logger.info(f"๐Ÿ“ ์ž„์‹œ ํŒŒ์ผ ์ €์žฅ ์œ„์น˜: {app_temp_dir}")
788
+
789
+ # ========== ์•ฑ ์‹คํ–‰ ==========
790
+ try:
791
+ app = create_app()
792
+ app.launch(
793
+ share=False, # ๋ณด์•ˆ์„ ์œ„ํ•ด share ๋น„ํ™œ์„ฑํ™”
794
+ server_name="0.0.0.0", # ๋ชจ๋“  IP์—์„œ ์ ‘๊ทผ ํ—ˆ์šฉ
795
+ server_port=7860, # ํฌํŠธ ์ง€์ •
796
+ max_threads=40, # ๋ฉ€ํ‹ฐ์œ ์ €๋ฅผ ์œ„ํ•œ ์Šค๋ ˆ๋“œ ์ˆ˜ ์ฆ๊ฐ€
797
+ auth=None, # ํ•„์š”์‹œ ์ธ์ฆ ์ถ”๊ฐ€ ๊ฐ€๋Šฅ
798
+ show_error=True, # ์—๋Ÿฌ ํ‘œ์‹œ
799
+ quiet=False, # ๋กœ๊ทธ ํ‘œ์‹œ
800
+ favicon_path=None, # ํŒŒ๋น„์ฝ˜ ์„ค์ •
801
+ ssl_verify=False # SSL ๊ฒ€์ฆ ๋น„ํ™œ์„ฑํ™” (๊ฐœ๋ฐœ์šฉ)
802
+ )
803
+ except Exception as e:
804
+ logger.error(f"์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์‹คํ–‰ ์‹คํŒจ: {e}")
805
+ raise
806
+ finally:
807
+ # ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ข…๋ฃŒ ์‹œ ์ •๋ฆฌ
808
+ logger.info("๐Ÿงน ์ปจํŠธ๋กค ํƒ€์›Œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ข…๋ฃŒ - ์ตœ์ข… ์ •๋ฆฌ ์ž‘์—…...")
809
+ try:
810
+ cleanup_huggingface_temp_folders()
811
+ if os.path.exists(app_temp_dir):
812
+ shutil.rmtree(app_temp_dir, ignore_errors=True)
813
+ logger.info("โœ… ์ตœ์ข… ์ •๋ฆฌ ์™„๋ฃŒ")
814
+ except Exception as e:
815
+ logger.error(f"์ตœ์ข… ์ •๋ฆฌ ์ค‘ ์˜ค๋ฅ˜: {e}")