ssboost commited on
Commit
ea56233
·
verified ·
1 Parent(s): ea16384

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +274 -277
app.py CHANGED
@@ -10,115 +10,105 @@ import uuid
10
  import shutil
11
  import glob
12
  from datetime import datetime
13
- import requests
14
- import json
15
- from dotenv import load_dotenv
16
 
17
- # 환경변수 로드
18
- load_dotenv()
 
 
 
 
 
 
 
19
 
20
- # 로깅 설정 (API 정보 완전 차단)
21
- logging.basicConfig(level=logging.WARNING)
22
  logger = logging.getLogger(__name__)
23
 
24
- # 세션별 임시 파일 관리를 위한 딕셔너리
25
- session_temp_files = {}
26
- session_data = {}
27
-
28
  def get_api_client():
29
- """환경변수에서 API 엔드포인트를 가져와 요청 함수 생성"""
30
  endpoint = os.getenv('API_ENDPOINT')
31
  if not endpoint:
32
- raise ValueError("API_ENDPOINT 환경변수가 필요합니다.")
33
-
34
- def make_request(api_name, **kwargs):
35
- try:
36
- # gradio_client와 동일한 방식으로 API 호출
37
- if not endpoint.startswith('http'):
38
- base_url = f"https://{endpoint}.hf.space"
39
- else:
40
- base_url = endpoint
41
-
42
- # Gradio API 엔드포인트 형식 맞추기
43
- url = f"{base_url}/call{api_name}"
44
-
45
- # 매개변수를 순서대로 배열로 변환
46
- if api_name == "/process_search_results":
47
- data = [kwargs.get('keyword', ''), kwargs.get('korean_only', True),
48
- kwargs.get('apply_main_keyword', '메인키워드 적용'), kwargs.get('exclude_zero_volume', False)]
49
- elif api_name == "/search_with_loading":
50
- data = [kwargs.get('keyword', ''), kwargs.get('korean_only', True),
51
- kwargs.get('apply_main_keyword', '메인키워드 적용'), kwargs.get('exclude_zero_volume', False)]
52
- elif api_name == "/filter_and_sort_table":
53
- data = [kwargs.get('selected_cat', '전체 보기'), kwargs.get('keyword_sort', '정렬 없음'),
54
- kwargs.get('total_volume_sort', '정렬 없음'), kwargs.get('usage_count_sort', '정렬 없음'),
55
- kwargs.get('selected_volume_range', '전체'), kwargs.get('exclude_zero_volume', False)]
56
- elif api_name == "/update_category_selection":
57
- data = [kwargs.get('selected_cat', '전체 보기')]
58
- elif api_name == "/process_analyze_results":
59
- data = [kwargs.get('analysis_keywords', ''), kwargs.get('selected_category', '전체 보기')]
60
- elif api_name == "/analyze_with_loading":
61
- data = [kwargs.get('analysis_keywords', ''), kwargs.get('selected_category', '전체 보기')]
62
- elif api_name == "/reset_interface":
63
- data = []
64
- elif api_name == "/get_session_id":
65
- data = []
66
- else:
67
- data = []
68
-
69
- response = requests.post(url, json={"data": data}, timeout=60)
70
-
71
- if response.status_code == 200:
72
- result = response.json()
73
- return result.get('data', [])
74
- else:
75
- raise Exception(f"API 호출 실패: {response.status_code}")
76
-
77
- except Exception as e:
78
- raise Exception(f"API 연결 오류: {str(e)}")
79
-
80
- return type('APIClient', (), {'predict': lambda self, **kwargs: make_request(kwargs.pop('api_name'), **kwargs)})()
81
 
82
  def cleanup_huggingface_temp_folders():
83
  """허깅페이스 임시 폴더 초기 정리"""
84
  try:
85
- temp_dirs = [tempfile.gettempdir(), "/tmp", "/var/tmp"]
 
 
 
 
 
 
 
 
 
 
86
  cleanup_count = 0
87
 
88
  for temp_dir in temp_dirs:
89
  if os.path.exists(temp_dir):
90
  try:
 
91
  session_files = glob.glob(os.path.join(temp_dir, "session_*.xlsx"))
92
  session_files.extend(glob.glob(os.path.join(temp_dir, "session_*.csv")))
 
 
 
 
93
 
94
  for file_path in session_files:
95
  try:
 
96
  if os.path.getmtime(file_path) < time.time() - 3600:
97
  os.remove(file_path)
98
  cleanup_count += 1
99
- except Exception:
100
- pass
101
- except Exception:
102
- pass
 
 
 
 
103
 
104
- logger.info(f"✅ 임시 폴더 정리 완료 - {cleanup_count}개 파일 삭제")
 
 
 
 
 
 
 
 
105
  except Exception as e:
106
- logger.error(f"임시 폴더 정리 중 오류: {e}")
107
 
108
  def setup_clean_temp_environment():
109
  """깨끗한 임시 환경 설정"""
110
  try:
 
111
  cleanup_huggingface_temp_folders()
112
 
 
113
  app_temp_dir = os.path.join(tempfile.gettempdir(), "control_tower_app")
114
  if os.path.exists(app_temp_dir):
115
  shutil.rmtree(app_temp_dir, ignore_errors=True)
116
  os.makedirs(app_temp_dir, exist_ok=True)
117
 
 
118
  os.environ['CONTROL_TOWER_TEMP'] = app_temp_dir
119
 
120
  logger.info(f"✅ 애플리케이션 전용 임시 디렉토리 설정: {app_temp_dir}")
 
121
  return app_temp_dir
 
122
  except Exception as e:
123
  logger.error(f"임시 환경 설정 실패: {e}")
124
  return tempfile.gettempdir()
@@ -129,12 +119,7 @@ def get_app_temp_dir():
129
 
130
  def get_session_id():
131
  """세션 ID 생성"""
132
- try:
133
- client = get_api_client()
134
- result = client.predict(api_name="/get_session_id")
135
- return result[0] if result else str(uuid.uuid4())
136
- except Exception:
137
- return str(uuid.uuid4())
138
 
139
  def cleanup_session_files(session_id, delay=300):
140
  """세션별 임시 파일 정리 함수"""
@@ -166,10 +151,11 @@ def cleanup_old_sessions():
166
  sessions_to_remove = []
167
 
168
  for session_id, data in session_data.items():
169
- if current_time - data.get('last_activity', 0) > 3600:
170
  sessions_to_remove.append(session_id)
171
 
172
  for session_id in sessions_to_remove:
 
173
  if session_id in session_temp_files:
174
  for file_path in session_temp_files[session_id]:
175
  try:
@@ -180,6 +166,7 @@ def cleanup_old_sessions():
180
  logger.error(f"오래된 세션 파일 삭제 오류: {e}")
181
  del session_temp_files[session_id]
182
 
 
183
  if session_id in session_data:
184
  del session_data[session_id]
185
  logger.info(f"오래된 세션 데이터 삭제: {session_id[:8]}...")
@@ -191,79 +178,101 @@ def update_session_activity(session_id):
191
  session_data[session_id]['last_activity'] = time.time()
192
 
193
  def create_session_temp_file(session_id, suffix='.xlsx'):
194
- """세션별 임시 파일 생성"""
195
  timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
196
  random_suffix = str(random.randint(1000, 9999))
197
 
 
198
  temp_dir = get_app_temp_dir()
199
  filename = f"session_{session_id[:8]}_{timestamp}_{random_suffix}{suffix}"
200
  temp_file_path = os.path.join(temp_dir, filename)
201
 
 
202
  with open(temp_file_path, 'w') as f:
203
  pass
204
 
205
  register_session_file(session_id, temp_file_path)
206
  return temp_file_path
207
 
208
- def search_with_loading(keyword, korean_only, apply_main_keyword, exclude_zero_volume):
209
- """원본 API: /search_with_loading"""
 
 
210
  try:
211
  client = get_api_client()
212
  result = client.predict(
213
  keyword=keyword,
214
  korean_only=korean_only,
215
- apply_main_keyword=apply_main_keyword,
216
  exclude_zero_volume=exclude_zero_volume,
217
- api_name="/search_with_loading"
218
  )
219
- return result[0] if result else ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
220
  except Exception as e:
221
- logger.error(f"search_with_loading API 호출 오류: {e}")
222
- return ""
 
 
 
 
 
 
 
 
 
 
223
 
224
- def process_search_results(keyword, korean_only, apply_main_keyword, exclude_zero_volume):
225
- """원본 API: /process_search_results"""
 
 
226
  try:
227
  client = get_api_client()
228
  result = client.predict(
229
- keyword=keyword,
230
- korean_only=korean_only,
231
- apply_main_keyword=apply_main_keyword,
232
- exclude_zero_volume=exclude_zero_volume,
233
- api_name="/process_search_results"
234
  )
235
 
236
- # 결과 안전하게 처리
237
- if len(result) >= 5:
238
- table_html, cat_choices, vol_choices, selected_cat, download_file = result[:5]
239
-
240
- # 다운로드 파일이 있는 경우 로컬로 복사
241
- local_download_file = None
242
- if download_file:
243
- session_id = get_session_id()
244
- local_download_file = create_session_temp_file(session_id, '.xlsx')
245
- try:
246
- shutil.copy2(download_file, local_download_file)
247
- except Exception as e:
248
- logger.error(f"파일 복사 오류: {e}")
249
- local_download_file = None
250
-
251
- return table_html, cat_choices, vol_choices, selected_cat, local_download_file
252
- else:
253
- return (
254
- "<p>검색 결과가 없습니다.</p>",
255
- ["전체 보기"], ["전체"], "전체 보기", None
256
- )
257
 
258
  except Exception as e:
259
- logger.error(f"process_search_results API 호출 오류: {e}")
260
- return (
261
- "<p>서비스 연결에 문제가 발생했습니다. 잠시 후 다시 시도해주세요.</p>",
262
- ["전체 보기"], ["전체"], "전체 보기", None
263
- )
264
 
265
- def filter_and_sort_table(selected_cat, keyword_sort, total_volume_sort, usage_count_sort, selected_volume_range, exclude_zero_volume):
266
- """원본 API: /filter_and_sort_table"""
 
 
267
  try:
268
  client = get_api_client()
269
  result = client.predict(
@@ -275,147 +284,148 @@ def filter_and_sort_table(selected_cat, keyword_sort, total_volume_sort, usage_c
275
  exclude_zero_volume=exclude_zero_volume,
276
  api_name="/filter_and_sort_table"
277
  )
278
- return result[0] if result else ""
 
 
279
  except Exception as e:
280
- logger.error(f"filter_and_sort_table API 호출 오류: {e}")
281
- return "<p>필터링 서비스 연결에 문제가 발생했습니다.</p>"
282
 
283
- def update_category_selection(selected_cat):
284
- """원본 API: /update_category_selection"""
 
 
285
  try:
286
  client = get_api_client()
287
  result = client.predict(
288
  selected_cat=selected_cat,
289
  api_name="/update_category_selection"
290
  )
291
- return gr.update(value=result[0] if result else selected_cat)
 
 
292
  except Exception as e:
293
- logger.error(f"update_category_selection API 호출 오류: {e}")
294
  return gr.update(value=selected_cat)
295
 
296
- def analyze_with_loading(analysis_keywords, selected_category):
297
- """원본 API: /analyze_with_loading"""
 
 
 
 
 
 
 
 
 
 
 
 
 
298
  try:
299
  client = get_api_client()
300
  result = client.predict(
301
- analysis_keywords=analysis_keywords,
302
- selected_category=selected_category,
303
- api_name="/analyze_with_loading"
304
  )
305
- return result[0] if result else ""
 
 
 
306
  except Exception as e:
307
- logger.error(f"analyze_with_loading API 호출 오류: {e}")
308
- return ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
309
 
310
- def process_analyze_results(analysis_keywords, selected_category):
311
- """원본 API: /process_analyze_results"""
 
 
312
  try:
313
  client = get_api_client()
314
  result = client.predict(
315
- analysis_keywords=analysis_keywords,
316
- selected_category=selected_category,
317
- api_name="/process_analyze_results"
 
 
318
  )
319
 
320
- if len(result) >= 2:
321
- analysis_result, download_file = result[:2]
322
-
323
- # 다운로드 파일이 있는 경우 로컬로 복사
324
- local_download_file = None
325
- if download_file:
326
- session_id = get_session_id()
327
- local_download_file = create_session_temp_file(session_id, '.xlsx')
328
- try:
329
- shutil.copy2(download_file, local_download_file)
330
- except Exception as e:
331
- logger.error(f"분��� 결과 파일 복사 오류: {e}")
332
- local_download_file = None
333
-
334
- return analysis_result, local_download_file
335
- else:
336
- return "분석 결과가 없습니다.", None
337
 
338
  except Exception as e:
339
- logger.error(f"process_analyze_results API 호출 오류: {e}")
340
- return "분석 서비스 연결에 문제가 발생했습니다. 잠시 후 다시 시도해주세요.", None
341
-
342
- def reset_interface():
343
- """원본 API: /reset_interface"""
344
- try:
345
- client = get_api_client()
346
- result = client.predict(api_name="/reset_interface")
347
- return result if result else get_default_reset_values()
348
- except Exception as e:
349
- logger.error(f"reset_interface API 호출 오류: {e}")
350
- return get_default_reset_values()
351
-
352
- def get_default_reset_values():
353
- """기본 리셋 값 반환"""
354
- return (
355
- "", True, False, "메인키워드 적용", "", ["전체 보기"], "전체 보기",
356
- ["전체"], "전체", "정렬 없음", "정렬 없음", ["전체 보기"], "전체 보기",
357
- "", "", None
358
- )
359
-
360
- # UI 처리 래퍼 함수들
361
- def wrapper_search_with_loading(keyword, korean_only, apply_main_keyword, exclude_zero_volume):
362
- """검색 로딩 UI 처리"""
363
- search_with_loading(keyword, korean_only, apply_main_keyword, exclude_zero_volume)
364
- return (
365
- gr.update(visible=True), # progress_section
366
- gr.update(visible=False) # empty_table_html
367
- )
368
 
369
- def wrapper_process_search_results(keyword, korean_only, apply_main_keyword, exclude_zero_volume):
370
- """검색 결과 처리 UI"""
371
- result = process_search_results(keyword, korean_only, apply_main_keyword, exclude_zero_volume)
372
 
373
- table_html, cat_choices, vol_choices, selected_cat, download_file = result
374
 
375
- # UI 표시 여부 결정
376
- if table_html and "검색 결과가 없습니다" not in table_html and "문제가 발생했습니다" not in table_html:
377
- keyword_section_visibility = True
378
- category_section_visibility = True
379
  empty_placeholder_vis = False
 
380
  execution_section_visibility = True
381
  else:
382
- keyword_section_visibility = False
383
- category_section_visibility = False
384
  empty_placeholder_vis = True
 
385
  execution_section_visibility = False
386
 
387
- # 가상의 state_df
388
- state_df = pd.DataFrame()
389
-
390
  return (
391
- table_html, # table_output
392
- cat_choices, # category_filter choices
393
- vol_choices, # search_volume_filter choices
394
- state_df, # state_df
395
- selected_cat, # selected_category value
396
- download_file, # download_output
397
- gr.update(visible=keyword_section_visibility), # keyword_analysis_section
398
- gr.update(visible=category_section_visibility), # category_analysis_section
399
- gr.update(visible=False), # progress_section
400
- gr.update(visible=empty_placeholder_vis), # empty_table_html
401
- gr.update(visible=execution_section_visibility), # execution_section
402
- keyword # keyword_state
403
  )
404
 
405
- def wrapper_analyze_with_loading(analysis_keywords, selected_category, state_df):
406
- """분석 로딩 UI 처리"""
407
- analyze_with_loading(analysis_keywords, selected_category)
408
- return gr.update(visible=True) # progress_section
 
 
 
 
 
 
 
 
 
 
 
 
409
 
410
- def wrapper_process_analyze_results(analysis_keywords, selected_category, state_df):
411
- """분석 결과 처리 UI"""
412
- analysis_result, download_file = process_analyze_results(analysis_keywords, selected_category)
413
- return (
414
- analysis_result, # analysis_result
415
- download_file, # download_output
416
- gr.update(visible=True), # analysis_output_section
417
- gr.update(visible=False) # progress_section
418
- )
419
 
420
  # 세션 정리 스케줄러
421
  def start_session_cleanup_scheduler():
@@ -424,6 +434,7 @@ def start_session_cleanup_scheduler():
424
  while True:
425
  time.sleep(600) # 10분마다 실행
426
  cleanup_old_sessions()
 
427
  cleanup_huggingface_temp_folders()
428
 
429
  threading.Thread(target=cleanup_scheduler, daemon=True).start()
@@ -432,14 +443,19 @@ def cleanup_on_startup():
432
  """애플리케이션 시작 시 전체 정리"""
433
  logger.info("🧹 컨트롤 타워 애플리케이션 시작 - 초기 정리 작업 시작...")
434
 
 
435
  cleanup_huggingface_temp_folders()
 
 
436
  app_temp_dir = setup_clean_temp_environment()
437
 
 
438
  global session_temp_files, session_data
439
  session_temp_files.clear()
440
  session_data.clear()
441
 
442
  logger.info(f"✅ 초기 정리 작업 완료 - 앱 전용 디렉토리: {app_temp_dir}")
 
443
  return app_temp_dir
444
 
445
  # Gradio 인터페이스 생성
@@ -455,21 +471,7 @@ def create_app():
455
  with open('style.css', 'r', encoding='utf-8') as f:
456
  custom_css = f.read()
457
  except:
458
- custom_css = """
459
- :root {
460
- --primary-color: #FB7F0D;
461
- --secondary-color: #ff9a8b;
462
- }
463
- .custom-button {
464
- background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)) !important;
465
- color: white !important;
466
- border-radius: 30px !important;
467
- height: 45px !important;
468
- font-size: 16px !important;
469
- font-weight: bold !important;
470
- width: 100% !important;
471
- }
472
- """
473
 
474
  with gr.Blocks(css=custom_css, theme=gr.themes.Default(
475
  primary_hue="orange",
@@ -478,6 +480,9 @@ def create_app():
478
  )) as demo:
479
  gr.HTML(fontawesome_html)
480
 
 
 
 
481
  # 키워드 상태 관리
482
  keyword_state = gr.State("")
483
 
@@ -650,14 +655,14 @@ def create_app():
650
  # 상태 저장용 변수
651
  state_df = gr.State()
652
 
653
- # 이벤트 연결
654
  search_btn.click(
655
- fn=wrapper_search_with_loading,
656
- inputs=[keyword, korean_only, apply_main_keyword, exclude_zero_volume],
657
  outputs=[progress_section, empty_table_html]
658
  ).then(
659
- fn=wrapper_process_search_results,
660
- inputs=[keyword, korean_only, apply_main_keyword, exclude_zero_volume],
661
  outputs=[
662
  table_output, category_filter, search_volume_filter,
663
  state_df, selected_category, download_output,
@@ -667,29 +672,29 @@ def create_app():
667
  ]
668
  )
669
 
670
- # 필터 및 정렬 변경 이벤트 연결
671
  category_filter.change(
672
  fn=filter_and_sort_table,
673
  inputs=[
674
- category_filter, gr.Textbox(value="정렬 없음", visible=False),
675
  total_volume_sort, usage_count_sort,
676
- search_volume_filter, exclude_zero_volume
677
  ],
678
  outputs=[table_output]
679
  )
680
 
681
  category_filter.change(
682
  fn=update_category_selection,
683
- inputs=[category_filter],
684
  outputs=[selected_category]
685
  )
686
 
687
  total_volume_sort.change(
688
  fn=filter_and_sort_table,
689
  inputs=[
690
- category_filter, gr.Textbox(value="정렬 없음", visible=False),
691
  total_volume_sort, usage_count_sort,
692
- search_volume_filter, exclude_zero_volume
693
  ],
694
  outputs=[table_output]
695
  )
@@ -697,9 +702,9 @@ def create_app():
697
  usage_count_sort.change(
698
  fn=filter_and_sort_table,
699
  inputs=[
700
- category_filter, gr.Textbox(value="정렬 없음", visible=False),
701
  total_volume_sort, usage_count_sort,
702
- search_volume_filter, exclude_zero_volume
703
  ],
704
  outputs=[table_output]
705
  )
@@ -707,9 +712,9 @@ def create_app():
707
  search_volume_filter.change(
708
  fn=filter_and_sort_table,
709
  inputs=[
710
- category_filter, gr.Textbox(value="정렬 없음", visible=False),
711
  total_volume_sort, usage_count_sort,
712
- search_volume_filter, exclude_zero_volume
713
  ],
714
  outputs=[table_output]
715
  )
@@ -717,28 +722,28 @@ def create_app():
717
  exclude_zero_volume.change(
718
  fn=filter_and_sort_table,
719
  inputs=[
720
- category_filter, gr.Textbox(value="정렬 없음", visible=False),
721
  total_volume_sort, usage_count_sort,
722
- search_volume_filter, exclude_zero_volume
723
  ],
724
  outputs=[table_output]
725
  )
726
 
727
- # 카테고리 분석 버튼 이벤트
728
  analyze_btn.click(
729
- fn=wrapper_analyze_with_loading,
730
- inputs=[analysis_keywords, selected_category, state_df],
731
  outputs=[progress_section]
732
  ).then(
733
- fn=wrapper_process_analyze_results,
734
- inputs=[analysis_keywords, selected_category, state_df],
735
  outputs=[analysis_result, download_output, analysis_output_section, progress_section]
736
  )
737
 
738
- # 리셋 버튼 이벤트 연결
739
  reset_btn.click(
740
  fn=reset_interface,
741
- inputs=[],
742
  outputs=[
743
  keyword, korean_only, exclude_zero_volume, apply_main_keyword,
744
  table_output, category_filter, category_filter,
@@ -753,7 +758,6 @@ def create_app():
753
 
754
  if __name__ == "__main__":
755
  # ========== 시작 시 전체 초기화 ==========
756
- print("===== Application Startup at %s =====" % time.strftime("%Y-%m-%d %H:%M:%S"))
757
  logger.info("🚀 컨트롤 타워 애플리케이션 시작...")
758
 
759
  # 1. 첫 번째: 허깅페이스 임시 폴더 정리 및 환경 설정
@@ -762,24 +766,20 @@ if __name__ == "__main__":
762
  # 2. 세션 정리 스케줄러 시작
763
  start_session_cleanup_scheduler()
764
 
765
- # 3. API 연결 테스트
766
  try:
767
- test_client = get_simple_api_client()
768
- logger.info("✅ API 연결 테스트 성공")
769
  except Exception as e:
770
- logger.error("❌ API 연결 실패 - 환경변수 API_ENDPOINT를 확인하세요")
771
- print("❌ API_ENDPOINT 환경변수가 설정되지 않았습니다.")
772
- print("💡 .env 파일에 다음과 같이 설정하세요:")
773
- print("API_ENDPOINT=your-endpoint-here")
774
- raise SystemExit(1)
775
 
776
- logger.info("===== 컨트롤 타워 애플리케이션 시작 완료 at %s =====", time.strftime("%Y-%m-%d %H:%M:%S"))
777
  logger.info(f"📁 임시 파일 저장 위치: {app_temp_dir}")
778
 
779
  # ========== 앱 실행 ==========
780
  try:
781
  app = create_app()
782
- print("🚀 Gradio 애플리케이션이 시작됩니다...")
783
  app.launch(
784
  share=False, # 보안을 위해 share 비활성화
785
  server_name="0.0.0.0", # 모든 IP에서 접근 허용
@@ -789,14 +789,11 @@ if __name__ == "__main__":
789
  show_error=True, # 에러 표시
790
  quiet=False, # 로그 표시
791
  favicon_path=None, # 파비콘 설정
792
- ssl_verify=False, # SSL 검증 비활성화 (개발용)
793
- inbrowser=False, # 자동 브라우저 열기 비활성화
794
- prevent_thread_lock=False # 스레드 잠금 방지 비활성화
795
  )
796
  except Exception as e:
797
  logger.error(f"애플리케이션 실행 실패: {e}")
798
- print(f"❌ 애플리케이션 실행 실패: {e}")
799
- raise SystemExit(1)
800
  finally:
801
  # 애플리케이션 종료 시 정리
802
  logger.info("🧹 컨트롤 타워 애플리케이션 종료 - 최종 정리 작업...")
 
10
  import shutil
11
  import glob
12
  from datetime import datetime
13
+ from gradio_client import Client
 
 
14
 
15
+ # 로깅 설정
16
+ logging.basicConfig(
17
+ level=logging.INFO,
18
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
19
+ handlers=[
20
+ logging.StreamHandler(),
21
+ logging.FileHandler('control_tower_app.log', mode='a')
22
+ ]
23
+ )
24
 
 
 
25
  logger = logging.getLogger(__name__)
26
 
27
+ # API 클라이언트 설정
 
 
 
28
  def get_api_client():
29
+ """환경변수에서 API 엔드포인트를 가져와 클라이언트 생성"""
30
  endpoint = os.getenv('API_ENDPOINT')
31
  if not endpoint:
32
+ raise ValueError("API_ENDPOINT 환경변수가 설정되지 않았습니다.")
33
+ return Client(endpoint)
34
+
35
+ # 세션별 임시 파일 관리를 위한 딕셔너리
36
+ session_temp_files = {}
37
+ session_data = {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
 
39
  def cleanup_huggingface_temp_folders():
40
  """허깅페이스 임시 폴더 초기 정리"""
41
  try:
42
+ # 일반적인 임시 디렉토리들
43
+ temp_dirs = [
44
+ tempfile.gettempdir(),
45
+ "/tmp",
46
+ "/var/tmp",
47
+ os.path.join(os.getcwd(), "temp"),
48
+ os.path.join(os.getcwd(), "tmp"),
49
+ "/gradio_cached_examples",
50
+ "/flagged"
51
+ ]
52
+
53
  cleanup_count = 0
54
 
55
  for temp_dir in temp_dirs:
56
  if os.path.exists(temp_dir):
57
  try:
58
+ # 기존 세션 파일들 정리
59
  session_files = glob.glob(os.path.join(temp_dir, "session_*.xlsx"))
60
  session_files.extend(glob.glob(os.path.join(temp_dir, "session_*.csv")))
61
+ session_files.extend(glob.glob(os.path.join(temp_dir, "*keyword*.xlsx")))
62
+ session_files.extend(glob.glob(os.path.join(temp_dir, "*keyword*.csv")))
63
+ session_files.extend(glob.glob(os.path.join(temp_dir, "tmp*.xlsx")))
64
+ session_files.extend(glob.glob(os.path.join(temp_dir, "tmp*.csv")))
65
 
66
  for file_path in session_files:
67
  try:
68
+ # 파일이 1시간 이상 오래된 경우만 삭제
69
  if os.path.getmtime(file_path) < time.time() - 3600:
70
  os.remove(file_path)
71
  cleanup_count += 1
72
+ logger.info(f"초기 정리: 오래된 임시 파일 삭제 - {file_path}")
73
+ except Exception as e:
74
+ logger.warning(f"파일 삭제 실패 (무시됨): {file_path} - {e}")
75
+
76
+ except Exception as e:
77
+ logger.warning(f"임시 디렉토리 정리 실패 (무시됨): {temp_dir} - {e}")
78
+
79
+ logger.info(f"✅ 허깅페이스 임시 폴더 초기 정리 완료 - {cleanup_count}개 파일 삭제")
80
 
81
+ # Gradio 캐시 폴더도 정리
82
+ try:
83
+ gradio_temp_dir = os.path.join(os.getcwd(), "gradio_cached_examples")
84
+ if os.path.exists(gradio_temp_dir):
85
+ shutil.rmtree(gradio_temp_dir, ignore_errors=True)
86
+ logger.info("Gradio 캐시 폴더 정리 완료")
87
+ except Exception as e:
88
+ logger.warning(f"Gradio 캐시 폴더 정리 실패 (무시됨): {e}")
89
+
90
  except Exception as e:
91
+ logger.error(f"초기 임시 폴더 정리 중 오류 (계속 진행): {e}")
92
 
93
  def setup_clean_temp_environment():
94
  """깨끗한 임시 환경 설정"""
95
  try:
96
+ # 1. 기존 임시 파일들 정리
97
  cleanup_huggingface_temp_folders()
98
 
99
+ # 2. 애플리케이션 전용 임시 디렉토리 생성
100
  app_temp_dir = os.path.join(tempfile.gettempdir(), "control_tower_app")
101
  if os.path.exists(app_temp_dir):
102
  shutil.rmtree(app_temp_dir, ignore_errors=True)
103
  os.makedirs(app_temp_dir, exist_ok=True)
104
 
105
+ # 3. 환경 변수 설정 (임시 디렉토리 지정)
106
  os.environ['CONTROL_TOWER_TEMP'] = app_temp_dir
107
 
108
  logger.info(f"✅ 애플리케이션 전용 임시 디렉토리 설정: {app_temp_dir}")
109
+
110
  return app_temp_dir
111
+
112
  except Exception as e:
113
  logger.error(f"임시 환경 설정 실패: {e}")
114
  return tempfile.gettempdir()
 
119
 
120
  def get_session_id():
121
  """세션 ID 생성"""
122
+ return str(uuid.uuid4())
 
 
 
 
 
123
 
124
  def cleanup_session_files(session_id, delay=300):
125
  """세션별 임시 파일 정리 함수"""
 
151
  sessions_to_remove = []
152
 
153
  for session_id, data in session_data.items():
154
+ if current_time - data.get('last_activity', 0) > 3600: # 1시간 초과
155
  sessions_to_remove.append(session_id)
156
 
157
  for session_id in sessions_to_remove:
158
+ # 파일 정리
159
  if session_id in session_temp_files:
160
  for file_path in session_temp_files[session_id]:
161
  try:
 
166
  logger.error(f"오래된 세션 파일 삭제 오류: {e}")
167
  del session_temp_files[session_id]
168
 
169
+ # 세션 데이터 정리
170
  if session_id in session_data:
171
  del session_data[session_id]
172
  logger.info(f"오래된 세션 데이터 삭제: {session_id[:8]}...")
 
178
  session_data[session_id]['last_activity'] = time.time()
179
 
180
  def create_session_temp_file(session_id, suffix='.xlsx'):
181
+ """세션별 임시 파일 생성 (전용 디렉토리 사용)"""
182
  timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
183
  random_suffix = str(random.randint(1000, 9999))
184
 
185
+ # 애플리케이션 전용 임시 디렉토리 사용
186
  temp_dir = get_app_temp_dir()
187
  filename = f"session_{session_id[:8]}_{timestamp}_{random_suffix}{suffix}"
188
  temp_file_path = os.path.join(temp_dir, filename)
189
 
190
+ # 빈 파일 생성
191
  with open(temp_file_path, 'w') as f:
192
  pass
193
 
194
  register_session_file(session_id, temp_file_path)
195
  return temp_file_path
196
 
197
+ def wrapper_modified(keyword, korean_only, apply_main_keyword_option, exclude_zero_volume, session_id):
198
+ """키워드 검색 및 처리 래퍼 함수 (API 클라이언트 사용)"""
199
+ update_session_activity(session_id)
200
+
201
  try:
202
  client = get_api_client()
203
  result = client.predict(
204
  keyword=keyword,
205
  korean_only=korean_only,
206
+ apply_main_keyword=apply_main_keyword_option,
207
  exclude_zero_volume=exclude_zero_volume,
208
+ api_name="/process_search_results"
209
  )
210
+
211
+ # API 결과 처리
212
+ table_html, cat_choices, vol_choices, selected_cat, download_file = result
213
+
214
+ # 로컬 파일로 다운로드 파일 복사
215
+ local_file = None
216
+ if download_file:
217
+ local_file = create_session_temp_file(session_id, '.xlsx')
218
+ shutil.copy(download_file, local_file)
219
+
220
+ return (
221
+ gr.update(value=table_html),
222
+ gr.update(choices=cat_choices),
223
+ gr.update(choices=vol_choices),
224
+ None, # DataFrame은 클라이언트에서 관리하지 않음
225
+ gr.update(choices=cat_choices, value=selected_cat),
226
+ local_file,
227
+ gr.update(visible=True),
228
+ gr.update(visible=True),
229
+ keyword
230
+ )
231
+
232
  except Exception as e:
233
+ logger.error(f"API 호출 오류: {e}")
234
+ return (
235
+ gr.update(value="<p>검색 중 오류가 발생했습니다. 다시 시도해주세요.</p>"),
236
+ gr.update(choices=["전체 보기"]),
237
+ gr.update(choices=["전체"]),
238
+ None,
239
+ gr.update(choices=["전체 보기"], value="전체 보기"),
240
+ None,
241
+ gr.update(visible=False),
242
+ gr.update(visible=False),
243
+ keyword
244
+ )
245
 
246
+ def analyze_with_auto_download(analysis_keywords, selected_category, state_df, session_id):
247
+ """카테고리 일치 분석 실행 및 자동 다운로드 (API 클라이언트 사용)"""
248
+ update_session_activity(session_id)
249
+
250
  try:
251
  client = get_api_client()
252
  result = client.predict(
253
+ analysis_keywords=analysis_keywords,
254
+ selected_category=selected_category,
255
+ api_name="/process_analyze_results"
 
 
256
  )
257
 
258
+ analysis_result, download_file = result
259
+
260
+ # 로컬 파일로 다운로드 파일 복사
261
+ local_file = None
262
+ if download_file:
263
+ local_file = create_session_temp_file(session_id, '.xlsx')
264
+ shutil.copy(download_file, local_file)
265
+
266
+ return analysis_result, local_file, gr.update(visible=True)
 
 
 
 
 
 
 
 
 
 
 
 
267
 
268
  except Exception as e:
269
+ logger.error(f"분석 API 호출 오류: {e}")
270
+ return "분석 중 오류가 발생했습니다. 다시 시도해주세요.", None, gr.update(visible=False)
 
 
 
271
 
272
+ def filter_and_sort_table(df, selected_cat, keyword_sort, total_volume_sort, usage_count_sort, selected_volume_range, exclude_zero_volume, session_id):
273
+ """테이블 필터링 및 정렬 함수 (API 클라이언트 사용)"""
274
+ update_session_activity(session_id)
275
+
276
  try:
277
  client = get_api_client()
278
  result = client.predict(
 
284
  exclude_zero_volume=exclude_zero_volume,
285
  api_name="/filter_and_sort_table"
286
  )
287
+
288
+ return result
289
+
290
  except Exception as e:
291
+ logger.error(f"필터링 API 호출 오류: {e}")
292
+ return ""
293
 
294
+ def update_category_selection(selected_cat, session_id):
295
+ """카테고리 필터 선택 시 분석할 카테고리도 같은 값으로 업데이트"""
296
+ update_session_activity(session_id)
297
+
298
  try:
299
  client = get_api_client()
300
  result = client.predict(
301
  selected_cat=selected_cat,
302
  api_name="/update_category_selection"
303
  )
304
+
305
+ return gr.update(value=result)
306
+
307
  except Exception as e:
308
+ logger.error(f"카테고리 선택 API 호출 오류: {e}")
309
  return gr.update(value=selected_cat)
310
 
311
+ def reset_interface(session_id):
312
+ """인터페이스 리셋 함수 - 세션별 데이터 초기화"""
313
+ update_session_activity(session_id)
314
+
315
+ # 세션별 임시 파일 정리
316
+ if session_id in session_temp_files:
317
+ for file_path in session_temp_files[session_id]:
318
+ try:
319
+ if os.path.exists(file_path):
320
+ os.remove(file_path)
321
+ logger.info(f"세션 {session_id[:8]}... 리셋 시 파일 삭제: {file_path}")
322
+ except Exception as e:
323
+ logger.error(f"세션 {session_id[:8]}... 리셋 시 파일 삭제 오류: {e}")
324
+ session_temp_files[session_id] = []
325
+
326
  try:
327
  client = get_api_client()
328
  result = client.predict(
329
+ api_name="/reset_interface"
 
 
330
  )
331
+
332
+ # API 결과를 그대로 반환
333
+ return result
334
+
335
  except Exception as e:
336
+ logger.error(f"리셋 API 호출 오류: {e}")
337
+ return (
338
+ "", # 검색 키워드
339
+ True, # 한글만 추출
340
+ False, # 검색량 0 키워드 제외
341
+ "메인키워드 적용", # 조합 방식
342
+ "", # HTML 테이블
343
+ ["전체 보기"], # 카테고리 필터
344
+ "전체 보기", # 카테고리 필터 선택
345
+ ["전체"], # 검색량 구간 필터
346
+ "전체", # 검색량 구간 선택
347
+ "정렬 없음", # 총검색량 정렬
348
+ "정렬 없음", # 키워드 사용횟수 정렬
349
+ ["전체 보기"], # 분석할 카테고리
350
+ "전체 보기", # 분석할 카테고리 선택
351
+ "", # 키워드 입력
352
+ "", # 분석 결과
353
+ None # 다운로드 파일
354
+ )
355
 
356
+ # 래퍼 함수들
357
+ def search_with_loading(keyword, korean_only, apply_main_keyword, exclude_zero_volume, session_id):
358
+ update_session_activity(session_id)
359
+
360
  try:
361
  client = get_api_client()
362
  result = client.predict(
363
+ keyword=keyword,
364
+ korean_only=korean_only,
365
+ apply_main_keyword=apply_main_keyword,
366
+ exclude_zero_volume=exclude_zero_volume,
367
+ api_name="/search_with_loading"
368
  )
369
 
370
+ return (
371
+ gr.update(visible=True),
372
+ gr.update(visible=False)
373
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
374
 
375
  except Exception as e:
376
+ logger.error(f"검색 로딩 API 호출 오류: {e}")
377
+ return (
378
+ gr.update(visible=False),
379
+ gr.update(visible=True)
380
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
381
 
382
+ def process_search_results(keyword, korean_only, apply_main_keyword, exclude_zero_volume, session_id):
383
+ update_session_activity(session_id)
 
384
 
385
+ result = wrapper_modified(keyword, korean_only, apply_main_keyword, exclude_zero_volume, session_id)
386
 
387
+ table_html, cat_choices, vol_choices, df, selected_cat, excel, keyword_section_vis, cat_section_vis, new_keyword_state = result
388
+
389
+ if excel is not None:
 
390
  empty_placeholder_vis = False
391
+ keyword_section_visibility = True
392
  execution_section_visibility = True
393
  else:
 
 
394
  empty_placeholder_vis = True
395
+ keyword_section_visibility = False
396
  execution_section_visibility = False
397
 
 
 
 
398
  return (
399
+ table_html, cat_choices, vol_choices, df, selected_cat, excel,
400
+ gr.update(visible=keyword_section_visibility),
401
+ gr.update(visible=cat_section_vis),
402
+ gr.update(visible=False),
403
+ gr.update(visible=empty_placeholder_vis),
404
+ gr.update(visible=execution_section_visibility),
405
+ new_keyword_state
 
 
 
 
 
406
  )
407
 
408
+ def analyze_with_loading(analysis_keywords, selected_category, state_df, session_id):
409
+ update_session_activity(session_id)
410
+
411
+ try:
412
+ client = get_api_client()
413
+ result = client.predict(
414
+ analysis_keywords=analysis_keywords,
415
+ selected_category=selected_category,
416
+ api_name="/analyze_with_loading"
417
+ )
418
+
419
+ return gr.update(visible=True)
420
+
421
+ except Exception as e:
422
+ logger.error(f"분석 로딩 API 호출 오류: {e}")
423
+ return gr.update(visible=False)
424
 
425
+ def process_analyze_results(analysis_keywords, selected_category, state_df, session_id):
426
+ update_session_activity(session_id)
427
+ results = analyze_with_auto_download(analysis_keywords, selected_category, state_df, session_id)
428
+ return results + (gr.update(visible=False),)
 
 
 
 
 
429
 
430
  # 세션 정리 스케줄러
431
  def start_session_cleanup_scheduler():
 
434
  while True:
435
  time.sleep(600) # 10분마다 실행
436
  cleanup_old_sessions()
437
+ # 추가로 허깅페이스 임시 폴더도 주기적 정리
438
  cleanup_huggingface_temp_folders()
439
 
440
  threading.Thread(target=cleanup_scheduler, daemon=True).start()
 
443
  """애플리케이션 시작 시 전체 정리"""
444
  logger.info("🧹 컨트롤 타워 애플리케이션 시작 - 초기 정리 작업 시작...")
445
 
446
+ # 1. 허깅페이스 임시 폴더 정리
447
  cleanup_huggingface_temp_folders()
448
+
449
+ # 2. 깨끗한 임시 환경 설정
450
  app_temp_dir = setup_clean_temp_environment()
451
 
452
+ # 3. 전역 변수 초기화
453
  global session_temp_files, session_data
454
  session_temp_files.clear()
455
  session_data.clear()
456
 
457
  logger.info(f"✅ 초기 정리 작업 완료 - 앱 전용 디렉토리: {app_temp_dir}")
458
+
459
  return app_temp_dir
460
 
461
  # Gradio 인터페이스 생성
 
471
  with open('style.css', 'r', encoding='utf-8') as f:
472
  custom_css = f.read()
473
  except:
474
+ custom_css = ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
475
 
476
  with gr.Blocks(css=custom_css, theme=gr.themes.Default(
477
  primary_hue="orange",
 
480
  )) as demo:
481
  gr.HTML(fontawesome_html)
482
 
483
+ # 세션 ID 상태 (각 사용자별로 고유)
484
+ session_id = gr.State(get_session_id)
485
+
486
  # 키워드 상태 관리
487
  keyword_state = gr.State("")
488
 
 
655
  # 상태 저장용 변수
656
  state_df = gr.State()
657
 
658
+ # 이벤트 연결 - 모든 함수에 session_id 추가
659
  search_btn.click(
660
+ fn=search_with_loading,
661
+ inputs=[keyword, korean_only, apply_main_keyword, exclude_zero_volume, session_id],
662
  outputs=[progress_section, empty_table_html]
663
  ).then(
664
+ fn=process_search_results,
665
+ inputs=[keyword, korean_only, apply_main_keyword, exclude_zero_volume, session_id],
666
  outputs=[
667
  table_output, category_filter, search_volume_filter,
668
  state_df, selected_category, download_output,
 
672
  ]
673
  )
674
 
675
+ # 필터 및 정렬 변경 이벤트 연결 - session_id 추가
676
  category_filter.change(
677
  fn=filter_and_sort_table,
678
  inputs=[
679
+ state_df, category_filter, gr.Textbox(value="정렬 없음", visible=False),
680
  total_volume_sort, usage_count_sort,
681
+ search_volume_filter, exclude_zero_volume, session_id
682
  ],
683
  outputs=[table_output]
684
  )
685
 
686
  category_filter.change(
687
  fn=update_category_selection,
688
+ inputs=[category_filter, session_id],
689
  outputs=[selected_category]
690
  )
691
 
692
  total_volume_sort.change(
693
  fn=filter_and_sort_table,
694
  inputs=[
695
+ state_df, category_filter, gr.Textbox(value="정렬 없음", visible=False),
696
  total_volume_sort, usage_count_sort,
697
+ search_volume_filter, exclude_zero_volume, session_id
698
  ],
699
  outputs=[table_output]
700
  )
 
702
  usage_count_sort.change(
703
  fn=filter_and_sort_table,
704
  inputs=[
705
+ state_df, category_filter, gr.Textbox(value="정렬 없음", visible=False),
706
  total_volume_sort, usage_count_sort,
707
+ search_volume_filter, exclude_zero_volume, session_id
708
  ],
709
  outputs=[table_output]
710
  )
 
712
  search_volume_filter.change(
713
  fn=filter_and_sort_table,
714
  inputs=[
715
+ state_df, category_filter, gr.Textbox(value="정렬 없음", visible=False),
716
  total_volume_sort, usage_count_sort,
717
+ search_volume_filter, exclude_zero_volume, session_id
718
  ],
719
  outputs=[table_output]
720
  )
 
722
  exclude_zero_volume.change(
723
  fn=filter_and_sort_table,
724
  inputs=[
725
+ state_df, category_filter, gr.Textbox(value="정렬 없음", visible=False),
726
  total_volume_sort, usage_count_sort,
727
+ search_volume_filter, exclude_zero_volume, session_id
728
  ],
729
  outputs=[table_output]
730
  )
731
 
732
+ # 카테고리 분석 버튼 이벤트 - session_id 추가
733
  analyze_btn.click(
734
+ fn=analyze_with_loading,
735
+ inputs=[analysis_keywords, selected_category, state_df, session_id],
736
  outputs=[progress_section]
737
  ).then(
738
+ fn=process_analyze_results,
739
+ inputs=[analysis_keywords, selected_category, state_df, session_id],
740
  outputs=[analysis_result, download_output, analysis_output_section, progress_section]
741
  )
742
 
743
+ # 리셋 버튼 이벤트 연결 - session_id 추가
744
  reset_btn.click(
745
  fn=reset_interface,
746
+ inputs=[session_id],
747
  outputs=[
748
  keyword, korean_only, exclude_zero_volume, apply_main_keyword,
749
  table_output, category_filter, category_filter,
 
758
 
759
  if __name__ == "__main__":
760
  # ========== 시작 시 전체 초기화 ==========
 
761
  logger.info("🚀 컨트롤 타워 애플리케이션 시작...")
762
 
763
  # 1. 첫 번째: 허깅페이스 임시 폴더 정리 및 환경 설정
 
766
  # 2. 세션 정리 스케줄러 시작
767
  start_session_cleanup_scheduler()
768
 
769
+ # 3. API 엔드포인트 확인
770
  try:
771
+ client = get_api_client()
772
+ logger.info("✅ API 클라이언트 연결 확인 완료")
773
  except Exception as e:
774
+ logger.error(f"❌ API 클라이언트 연결 실패: {e}")
775
+ raise
 
 
 
776
 
777
+ logger.info("===== 컨트롤 타워 Application Startup at %s =====", time.strftime("%Y-%m-%d %H:%M:%S"))
778
  logger.info(f"📁 임시 파일 저장 위치: {app_temp_dir}")
779
 
780
  # ========== 앱 실행 ==========
781
  try:
782
  app = create_app()
 
783
  app.launch(
784
  share=False, # 보안을 위해 share 비활성화
785
  server_name="0.0.0.0", # 모든 IP에서 접근 허용
 
789
  show_error=True, # 에러 표시
790
  quiet=False, # 로그 표시
791
  favicon_path=None, # 파비콘 설정
792
+ ssl_verify=False # SSL 검증 비활성화 (개발용)
 
 
793
  )
794
  except Exception as e:
795
  logger.error(f"애플리케이션 실행 실패: {e}")
796
+ raise
 
797
  finally:
798
  # 애플리케이션 종료 시 정리
799
  logger.info("🧹 컨트롤 타워 애플리케이션 종료 - 최종 정리 작업...")