ssboost commited on
Commit
f00f5cb
·
verified ·
1 Parent(s): 7567d30

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +178 -468
app.py CHANGED
@@ -5,7 +5,6 @@ import time
5
  import threading
6
  import tempfile
7
  import logging
8
- import random
9
  import uuid
10
  import shutil
11
  import glob
@@ -19,7 +18,7 @@ logging.basicConfig(
19
  format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
20
  handlers=[
21
  logging.StreamHandler(),
22
- logging.FileHandler('main_keyword_app.log', mode='a')
23
  ]
24
  )
25
 
@@ -152,8 +151,10 @@ def cleanup_huggingface_temp_folders():
152
  # 기존 세션 파일들 정리
153
  session_files = glob.glob(os.path.join(temp_dir, "session_*.xlsx"))
154
  session_files.extend(glob.glob(os.path.join(temp_dir, "session_*.csv")))
155
- session_files.extend(glob.glob(os.path.join(temp_dir, "*keyword*.xlsx")))
156
- session_files.extend(glob.glob(os.path.join(temp_dir, "*keyword*.csv")))
 
 
157
  session_files.extend(glob.glob(os.path.join(temp_dir, "tmp*.xlsx")))
158
  session_files.extend(glob.glob(os.path.join(temp_dir, "tmp*.csv")))
159
 
@@ -191,13 +192,13 @@ def setup_clean_temp_environment():
191
  cleanup_huggingface_temp_folders()
192
 
193
  # 2. 애플리케이션 전용 임시 디렉토리 생성
194
- app_temp_dir = os.path.join(tempfile.gettempdir(), "keyword_app")
195
  if os.path.exists(app_temp_dir):
196
  shutil.rmtree(app_temp_dir, ignore_errors=True)
197
  os.makedirs(app_temp_dir, exist_ok=True)
198
 
199
  # 3. 환경 변수 설정 (임시 디렉토리 지정)
200
- os.environ['KEYWORD_APP_TEMP'] = app_temp_dir
201
 
202
  logger.info(f"✅ 애플리케이션 전용 임시 디렉토리 설정: {app_temp_dir}")
203
 
@@ -209,7 +210,7 @@ def setup_clean_temp_environment():
209
 
210
  def get_app_temp_dir():
211
  """애플리케이션 전용 임시 디렉토리 반환"""
212
- return os.environ.get('KEYWORD_APP_TEMP', tempfile.gettempdir())
213
 
214
  def get_session_id():
215
  """세션 ID 생성"""
@@ -274,7 +275,7 @@ def update_session_activity(session_id):
274
  def create_session_temp_file(session_id, suffix='.xlsx'):
275
  """세션별 임시 파일 생성 (전용 디렉토리 사용)"""
276
  timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
277
- random_suffix = str(random.randint(1000, 9999))
278
 
279
  # 애플리케이션 전용 임시 디렉토리 사용
280
  temp_dir = get_app_temp_dir()
@@ -288,152 +289,119 @@ def create_session_temp_file(session_id, suffix='.xlsx'):
288
  register_session_file(session_id, temp_file_path)
289
  return temp_file_path
290
 
291
- def wrapper_modified(keyword, korean_only, apply_main_keyword_option, exclude_zero_volume, session_id):
292
- """키워드 검색 및 처리 래퍼 함수 (세션 ID 추가)"""
293
  update_session_activity(session_id)
294
 
295
- # 현재 키워드 사용 (세션별로 관리)
296
- current_keyword = keyword
297
 
298
- # 키워드가 비어있는 경우 처리
299
- if not keyword:
300
- return (gr.update(value=""), gr.update(choices=["전체 보기"]), gr.update(choices=["전체"]),
301
- None, gr.update(choices=["전체 보기"], value="전체 보기"), None,
302
- gr.update(visible=False), gr.update(visible=False), current_keyword)
303
 
304
- # 네이버 쇼핑 API 검색 수행
305
- search_results = product_search.fetch_naver_shopping_data(keyword, korean_only, apply_main_keyword_option == "메인키워드 적용")
 
306
 
307
- # 검색 결과가 없는 경우
308
- if not search_results.get("product_list"):
309
- return (gr.update(value="<p>검색 결과가 없습니다. 다른 키워드로 시도해보세요.</p>"),
310
- gr.update(choices=["전체 보기"]), gr.update(choices=["전체"]),
311
- None, gr.update(choices=["전체 보기"], value="전체 보기"), None,
312
- gr.update(visible=False), gr.update(visible=False), current_keyword)
313
 
314
- # 검색 결과 처리 - 키워드 전달 및 검색량 0 키워드 제외 옵션 전달
315
- result = keyword_processor.process_search_results(search_results, current_keyword, exclude_zero_volume)
316
 
317
- df_products = result["products_df"]
318
- df_keywords = result["keywords_df"]
319
- category_list = result["categories"]
320
-
321
- if df_keywords.empty:
322
- return (gr.update(value="<p>추출된 키워드가 없습니다. 다른 옵션으로 시도해보세요.</p>"),
323
- gr.update(choices=["전체 보기"]), gr.update(choices=["전체"]),
324
- df_keywords, gr.update(choices=["전체 보기"], value="전체 보기"), None,
325
- gr.update(visible=False), gr.update(visible=False), current_keyword)
326
-
327
- # HTML 테이블 생성
328
- html = export_utils.create_table_without_checkboxes(df_keywords)
329
-
330
- # 필터링을 위한 고유 값 리스트 생성
331
- volume_range_choices = ["전체"] + sorted(df_keywords["검색량구간"].unique().tolist())
332
-
333
- # 분석할 카테고리 드롭다운도 같은 선택지로 업데이트
334
- first_category = category_list[0] if category_list else "전체 보기"
335
 
336
- # 세션별 엑셀 파일 생성
337
- excel_path = create_session_excel_file(df_keywords, session_id)
 
338
 
339
- # 분석 섹션 표시
340
- return (gr.update(value=html), gr.update(choices=category_list), gr.update(choices=volume_range_choices),
341
- df_keywords, gr.update(choices=category_list, value=first_category), excel_path,
342
- gr.update(visible=True), gr.update(visible=True), current_keyword)
343
-
344
- def create_session_excel_file(df, session_id):
345
- """세션별 엑셀 파일 생성"""
346
  try:
347
- excel_path = create_session_temp_file(session_id, '.xlsx')
348
- df.to_excel(excel_path, index=False, engine='openpyxl')
349
- logger.info(f"세션 {session_id[:8]}... 엑셀 파일 생성: {excel_path}")
350
- return excel_path
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
351
  except Exception as e:
352
- logger.error(f"세션별 엑셀 파일 생성 오류: {e}")
 
 
353
  return None
354
 
355
- def analyze_with_auto_download(analysis_keywords, selected_category, state_df, session_id):
356
- """카테고리 일치 분석 실행 및 자동 다운로드 (세션 ID 추가)"""
357
- update_session_activity(session_id)
358
-
359
- # 분석할 키워드나 카테고리가 없는 경우
360
- if not analysis_keywords or not selected_category:
361
- return "키워드와 카테고리를 모두 선택해주세요.", None, gr.update(visible=False)
362
-
363
- # 분석 실행 - 동적 로딩된 category_analysis 모듈 사용
364
- analysis_result = category_analysis.analyze_keywords_by_category(analysis_keywords, selected_category, state_df)
365
-
366
- # 세션별 엑셀 파일 생성
367
- excel_path = create_session_excel_file(state_df, session_id)
368
-
369
- # 분석 결과 출력 섹션 표시
370
- return analysis_result, excel_path, gr.update(visible=True)
371
-
372
- def filter_and_sort_table(df, selected_cat, keyword_sort, total_volume_sort, usage_count_sort, selected_volume_range, exclude_zero_volume, session_id):
373
- """테이블 필터링 및 정렬 함수 (세션 ID 추가)"""
374
- update_session_activity(session_id)
375
-
376
- if df is None or df.empty:
377
- return ""
378
-
379
- # 필터링 적용
380
- filtered_df = df.copy()
381
-
382
- # 카테고리 필터 적용
383
- if selected_cat and selected_cat != "전체 보기":
384
- cat_name_to_filter = selected_cat.rsplit(" (", 1)[0]
385
- filtered_df = filtered_df[filtered_df["관련 카테고리"].astype(str).str.contains(cat_name_to_filter, case=False, na=False)]
386
-
387
- def get_filtered_category_display(current_categories_str):
388
- if pd.isna(current_categories_str):
389
- return ""
390
-
391
- categories = str(current_categories_str).split('\n')
392
- matched_categories = [cat for cat in categories if cat_name_to_filter.lower() in cat.lower()]
393
- if matched_categories:
394
- return "\n".join(matched_categories)
395
-
396
- return current_categories_str
397
-
398
- filtered_df['관련 카테고리'] = filtered_df['관련 카테고리'].apply(get_filtered_category_display)
399
-
400
- # 검색량 구간 필터 적용
401
- if selected_volume_range and selected_volume_range != "전체":
402
- filtered_df = filtered_df[filtered_df["검색량구간"] == selected_volume_range]
403
-
404
- # 검색량 0 제외 필터 적용
405
- if exclude_zero_volume:
406
- filtered_df = filtered_df[filtered_df["총검색량"] > 0]
407
- logger.info(f"세션 {session_id[:8]}... 검색량 0 제외 필터 적용 - 남은 키워드 수: {len(filtered_df)}")
408
-
409
- # 정렬 적용
410
- if keyword_sort != "정렬 없음":
411
- is_ascending = keyword_sort == "오름차순"
412
- filtered_df = filtered_df.sort_values(by="조합 키워드", ascending=is_ascending)
413
-
414
- if total_volume_sort != "정렬 없음":
415
- is_ascending = total_volume_sort == "오름차순"
416
- filtered_df = filtered_df.sort_values(by="총검색량", ascending=is_ascending)
417
-
418
- # 키워드 사용횟수 정렬 적용
419
- if usage_count_sort != "정렬 없음":
420
- is_ascending = usage_count_sort == "오름차순"
421
- filtered_df = filtered_df.sort_values(by="키워드 사용횟수", ascending=is_ascending)
422
-
423
- # 순번을 1부터 순차적으로 유지하기 위해 행 인덱스 재설정
424
- filtered_df = filtered_df.reset_index(drop=True)
425
-
426
- # 순번을 포함한 HTML 테이블 생성
427
- html = export_utils.create_table_without_checkboxes(filtered_df)
428
-
429
- return html
430
-
431
- def update_category_selection(selected_cat, session_id):
432
- """카테고리 필터 선택 시 분석할 카테고리도 같은 값으로 업��이트"""
433
- update_session_activity(session_id)
434
- logger.debug(f"세션 {session_id[:8]}... 카테고리 선택 변경: {selected_cat}")
435
- return gr.update(value=selected_cat)
436
-
437
  def reset_interface(session_id):
438
  """인터페이스 리셋 함수 - 세션별 데이터 초기화"""
439
  update_session_activity(session_id)
@@ -450,69 +418,24 @@ def reset_interface(session_id):
450
  session_temp_files[session_id] = []
451
 
452
  return (
453
- "", # 검색 키워드
454
- True, # 한글만 추출
455
- False, # 검색량 0 키워드 제외
456
- "메인키워드 적용", # 조합 방식
457
- "", # HTML 테이블
458
- ["전체 보기"], # 카테고리 필터
459
- "전체 보기", # 카테고리 필터 선택
460
- ["전체"], # 검색량 구간 필터
461
- "전체", # 검색량 구간 선택
462
- "정렬 없음", # 총검색량 정렬
463
- "정렬 없음", # 키워드 사용횟수 정렬
464
- None, # 상태 DataFrame
465
- ["전체 보기"], # 분석할 카테고리
466
- "전체 보기", # 분석할 카테고리 선택
467
- "", # 키워드 입력
468
- "", # 분석 결과
469
- None, # 다운로드 파일
470
- gr.update(visible=False), # 키워드 분석 섹션
471
- gr.update(visible=False), # 분석 결과 출력 섹션
472
- "" # 키워드 상태
473
- )
474
-
475
- # 래퍼 함수들도 세션 ID 추가
476
- def search_with_loading(keyword, korean_only, apply_main_keyword, exclude_zero_volume, session_id):
477
- update_session_activity(session_id)
478
- return (
479
- gr.update(visible=True),
480
- gr.update(visible=False)
481
- )
482
-
483
- def process_search_results(keyword, korean_only, apply_main_keyword, exclude_zero_volume, session_id):
484
- update_session_activity(session_id)
485
-
486
- result = wrapper_modified(keyword, korean_only, apply_main_keyword, exclude_zero_volume, session_id)
487
-
488
- table_html, cat_choices, vol_choices, df, selected_cat, excel, keyword_section_vis, cat_section_vis, new_keyword_state = result
489
-
490
- if not isinstance(df, type(None)) and not df.empty:
491
- empty_placeholder_vis = False
492
- keyword_section_visibility = True
493
- execution_section_visibility = True
494
- else:
495
- empty_placeholder_vis = True
496
- keyword_section_visibility = False
497
- execution_section_visibility = False
498
-
499
- return (
500
- table_html, cat_choices, vol_choices, df, selected_cat, excel,
501
- gr.update(visible=keyword_section_visibility),
502
- gr.update(visible=cat_section_vis),
503
- gr.update(visible=False),
504
- gr.update(visible=empty_placeholder_vis),
505
- gr.update(visible=execution_section_visibility),
506
- new_keyword_state
507
  )
508
 
509
- def analyze_with_loading(analysis_keywords, selected_category, state_df, session_id):
 
510
  update_session_activity(session_id)
511
  return gr.update(visible=True)
512
 
513
- def process_analyze_results(analysis_keywords, selected_category, state_df, session_id):
 
514
  update_session_activity(session_id)
515
- results = analyze_with_auto_download(analysis_keywords, selected_category, state_df, session_id)
 
516
  return results + (gr.update(visible=False),)
517
 
518
  # 세션 정리 스케줄러
@@ -529,7 +452,7 @@ def start_session_cleanup_scheduler():
529
 
530
  def cleanup_on_startup():
531
  """애플리케이션 시작 시 전체 정리"""
532
- logger.info("🧹 애플리케이션 시작 - 초기 정리 작업 시작...")
533
 
534
  # 1. 허깅페이스 임시 폴더 정리
535
  cleanup_huggingface_temp_folders()
@@ -548,6 +471,7 @@ def cleanup_on_startup():
548
 
549
  # Gradio 인터페이스 생성
550
  def create_app():
 
551
  fontawesome_html = """
552
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
553
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/static/pretendard.css">
@@ -559,6 +483,7 @@ def create_app():
559
  with open('style.css', 'r', encoding='utf-8') as f:
560
  custom_css = f.read()
561
  except:
 
562
  custom_css = """
563
  :root {
564
  --primary-color: #FB7F0D;
@@ -631,30 +556,6 @@ def create_app():
631
  50% { width: 70%; }
632
  100% { width: 10%; }
633
  }
634
- .empty-table {
635
- width: 100%;
636
- border-collapse: collapse;
637
- font-size: 14px;
638
- margin-top: 20px;
639
- }
640
- .empty-table th {
641
- background-color: #FB7F0D;
642
- color: white;
643
- text-align: left;
644
- padding: 12px;
645
- border: 1px solid #ddd;
646
- }
647
- .empty-table td {
648
- padding: 10px;
649
- border: 1px solid #ddd;
650
- text-align: center;
651
- color: #999;
652
- }
653
- .button-container {
654
- margin-top: 20px;
655
- display: flex;
656
- gap: 15px;
657
- }
658
  .execution-section {
659
  margin-top: 20px;
660
  background-color: #f9f9f9;
@@ -683,56 +584,47 @@ def create_app():
683
  # 세션 ID 상태 (각 사용자별로 고유)
684
  session_id = gr.State(get_session_id)
685
 
686
- # 키워드 상태 관리
687
- keyword_state = gr.State("")
688
-
689
  # 입력 섹션
690
  with gr.Column(elem_classes="custom-frame fade-in"):
691
- gr.HTML('<div class="section-title"><i class="fas fa-search"></i> 검색 입력</div>')
692
 
 
693
  with gr.Row():
694
  with gr.Column(scale=1):
695
- keyword = gr.Textbox(
696
  label="메인 키워드",
697
  placeholder="예: 오징어"
698
  )
699
  with gr.Column(scale=1):
700
- search_btn = gr.Button(
701
- "메인키워드 분석",
702
- elem_classes="custom-button"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
703
  )
704
-
705
- with gr.Accordion("옵션 설정", open=False):
706
- with gr.Row():
707
- with gr.Column(scale=1):
708
- korean_only = gr.Checkbox(
709
- label="한글만 추출",
710
- value=True
711
- )
712
- with gr.Column(scale=1):
713
- exclude_zero_volume = gr.Checkbox(
714
- label="검색량 0 키워드 제외",
715
- value=False
716
- )
717
-
718
- with gr.Row():
719
- with gr.Column(scale=1):
720
- apply_main_keyword = gr.Radio(
721
- ["메인키워드 적용", "메인키워드 미적용"],
722
- label="조합 방식",
723
- value="메인키워드 적용"
724
- )
725
- with gr.Column(scale=1):
726
- gr.HTML("")
727
 
728
- # 진행 상태 표시 섹션
729
  with gr.Column(elem_classes="custom-frame fade-in", visible=False) as progress_section:
730
  gr.HTML('<div class="section-title"><i class="fas fa-spinner"></i> 분석 진행 상태</div>')
 
731
  progress_html = gr.HTML("""
732
  <div style="padding: 15px; background-color: #f9f9f9; border-radius: 5px; margin: 10px 0; border: 1px solid #ddd;">
733
  <div style="margin-bottom: 10px; display: flex; align-items: center;">
734
  <i class="fas fa-spinner fa-spin" style="color: #FB7F0D; margin-right: 10px;"></i>
735
- <span>키워드 데이터를 분석중입니다. 잠시만 기다려주세요...</span>
736
  </div>
737
  <div style="background-color: #e9ecef; height: 10px; border-radius: 5px; overflow: hidden;">
738
  <div class="progress-bar"></div>
@@ -740,219 +632,43 @@ def create_app():
740
  </div>
741
  """)
742
 
743
- # 메인키워드 분석 결과 섹션
744
- with gr.Column(elem_classes="custom-frame fade-in") as main_keyword_section:
745
- gr.HTML('<div class="section-title"><i class="fas fa-table"></i> 메인키워드 분석 결과</div>')
746
-
747
- empty_table_html = gr.HTML("""
748
- <table class="empty-table">
749
- <thead>
750
- <tr>
751
- <th>순번</th>
752
- <th>조합 키워드</th>
753
- <th>PC검색량</th>
754
- <th>모바일검색량</th>
755
- <th>총검색량</th>
756
- <th>검색량구간</th>
757
- <th>키워드 사용자순위</th>
758
- <th>키워드 사용횟수</th>
759
- <th>상품 등록 카테고리</th>
760
- </tr>
761
- </thead>
762
- <tbody>
763
- <tr>
764
- <td colspan="9" style="padding: 30px; text-align: center;">
765
- 검색을 실행하면 여기에 결과가 표시됩니다
766
- </td>
767
- </tr>
768
- </tbody>
769
- </table>
770
- """)
771
-
772
- with gr.Column(visible=False) as keyword_analysis_section:
773
- with gr.Row():
774
- with gr.Column(scale=1):
775
- category_filter = gr.Dropdown(
776
- choices=["전체 보기"],
777
- label="카테고리 필터",
778
- value="전체 보기",
779
- interactive=True
780
- )
781
- with gr.Column(scale=1):
782
- total_volume_sort = gr.Dropdown(
783
- choices=["정렬 없음", "오름차순", "내림차순"],
784
- label="총검색량 정렬",
785
- value="정렬 없음",
786
- interactive=True
787
- )
788
-
789
- with gr.Row():
790
- with gr.Column(scale=1):
791
- search_volume_filter = gr.Dropdown(
792
- choices=["전체"],
793
- label="검색량 구간 필터",
794
- value="전체",
795
- interactive=True
796
- )
797
- with gr.Column(scale=1):
798
- usage_count_sort = gr.Dropdown(
799
- choices=["정렬 없음", "오름차순", "내림차순"],
800
- label="키워드 사용횟수 정렬",
801
- value="정렬 없음",
802
- interactive=True
803
- )
804
-
805
- gr.HTML("<div class='data-container' id='table_container'></div>")
806
- table_output = gr.HTML(elem_classes="fade-in")
807
-
808
- # 카테고리 분석 섹션
809
- with gr.Column(elem_classes="custom-frame fade-in", visible=False) as category_analysis_section:
810
- gr.HTML('<div class="section-title"><i class="fas fa-chart-bar"></i> 키워드 분석</div>')
811
-
812
- with gr.Row():
813
- with gr.Column(scale=1):
814
- analysis_keywords = gr.Textbox(
815
- label="키워드 입력 (최대 20개, 쉼표 또는 엔터로 구분)",
816
- placeholder="예: 오징어볶음, 오징어 손질, 오징어 요리...",
817
- lines=5
818
- )
819
-
820
- with gr.Column(scale=1):
821
- selected_category = gr.Dropdown(
822
- label="분석할 카테고리(분석 전 반드시 선택해주세요)",
823
- choices=["전체 보기"],
824
- value="전체 보기",
825
- interactive=True
826
- )
827
-
828
- # 실행 섹션
829
- with gr.Column(elem_classes="execution-section", visible=False) as execution_section:
830
- gr.HTML('<div class="section-title"><i class="fas fa-play-circle"></i> 실행</div>')
831
- with gr.Row():
832
- with gr.Column(scale=1):
833
- analyze_btn = gr.Button(
834
- "카테고리 일치 분석",
835
- elem_classes=["execution-button", "primary-button"]
836
- )
837
- with gr.Column(scale=1):
838
- reset_btn = gr.Button(
839
- "모든 입력 초기화",
840
- elem_classes=["execution-button", "secondary-button"]
841
- )
842
-
843
- # 분석 결과 출력 섹션
844
- with gr.Column(elem_classes="custom-frame fade-in", visible=False) as analysis_output_section:
845
- gr.HTML('<div class="section-title"><i class="fas fa-list-ul"></i> 분석 결과 요약</div>')
846
 
847
- analysis_result = gr.HTML(elem_classes="fade-in")
 
848
 
849
- with gr.Row():
850
- download_output = gr.File(
851
- label="키워드 목록 다운로드",
852
- visible=True
853
- )
854
-
855
- # 상태 저장용 변수
856
- state_df = gr.State()
857
-
858
- # 이벤트 연결 - 모든 함수에 session_id 추가
859
- search_btn.click(
860
- fn=search_with_loading,
861
- inputs=[keyword, korean_only, apply_main_keyword, exclude_zero_volume, session_id],
862
- outputs=[progress_section, empty_table_html]
863
  ).then(
864
- fn=process_search_results,
865
- inputs=[keyword, korean_only, apply_main_keyword, exclude_zero_volume, session_id],
866
  outputs=[
867
- table_output, category_filter, search_volume_filter,
868
- state_df, selected_category, download_output,
869
- keyword_analysis_section, category_analysis_section,
870
- progress_section, empty_table_html, execution_section,
871
- keyword_state
872
  ]
873
  )
874
 
875
- # 필터 정렬 변경 이벤트 연결 - session_id 추가
876
- category_filter.change(
877
- fn=filter_and_sort_table,
878
- inputs=[
879
- state_df, category_filter, gr.Textbox(value="정렬 없음", visible=False),
880
- total_volume_sort, usage_count_sort,
881
- search_volume_filter, exclude_zero_volume, session_id
882
- ],
883
- outputs=[table_output]
884
- )
885
-
886
- category_filter.change(
887
- fn=update_category_selection,
888
- inputs=[category_filter, session_id],
889
- outputs=[selected_category]
890
- )
891
-
892
- total_volume_sort.change(
893
- fn=filter_and_sort_table,
894
- inputs=[
895
- state_df, category_filter, gr.Textbox(value="정렬 없음", visible=False),
896
- total_volume_sort, usage_count_sort,
897
- search_volume_filter, exclude_zero_volume, session_id
898
- ],
899
- outputs=[table_output]
900
- )
901
-
902
- usage_count_sort.change(
903
- fn=filter_and_sort_table,
904
- inputs=[
905
- state_df, category_filter, gr.Textbox(value="정렬 없음", visible=False),
906
- total_volume_sort, usage_count_sort,
907
- search_volume_filter, exclude_zero_volume, session_id
908
- ],
909
- outputs=[table_output]
910
- )
911
-
912
- search_volume_filter.change(
913
- fn=filter_and_sort_table,
914
- inputs=[
915
- state_df, category_filter, gr.Textbox(value="정렬 없음", visible=False),
916
- total_volume_sort, usage_count_sort,
917
- search_volume_filter, exclude_zero_volume, session_id
918
- ],
919
- outputs=[table_output]
920
- )
921
-
922
- exclude_zero_volume.change(
923
- fn=filter_and_sort_table,
924
- inputs=[
925
- state_df, category_filter, gr.Textbox(value="정렬 없음", visible=False),
926
- total_volume_sort, usage_count_sort,
927
- search_volume_filter, exclude_zero_volume, session_id
928
- ],
929
- outputs=[table_output]
930
- )
931
-
932
- # 카테고리 분석 버튼 이벤트 - session_id 추가
933
- analyze_btn.click(
934
- fn=analyze_with_loading,
935
- inputs=[analysis_keywords, selected_category, state_df, session_id],
936
- outputs=[progress_section]
937
- ).then(
938
- fn=process_analyze_results,
939
- inputs=[analysis_keywords, selected_category, state_df, session_id],
940
- outputs=[analysis_result, download_output, analysis_output_section, progress_section]
941
- )
942
-
943
- # 리셋 버튼 이벤트 연결 - session_id 추가
944
  reset_btn.click(
945
  fn=reset_interface,
946
  inputs=[session_id],
947
  outputs=[
948
- keyword, korean_only, exclude_zero_volume, apply_main_keyword,
949
- table_output, category_filter, category_filter,
950
- search_volume_filter, search_volume_filter,
951
- total_volume_sort, usage_count_sort,
952
- state_df, selected_category, selected_category,
953
- analysis_keywords, analysis_result, download_output,
954
- keyword_analysis_section, analysis_output_section,
955
- keyword_state
956
  ]
957
  )
958
 
@@ -960,7 +676,7 @@ def create_app():
960
 
961
  if __name__ == "__main__":
962
  # ========== 시작 시 전체 초기화 ==========
963
- logger.info("🚀 메인키워드 분석 애플리케이션 시작...")
964
 
965
  # 1. 첫 번째: 허깅페이스 임시 폴더 정리 및 환경 설정
966
  app_temp_dir = cleanup_on_startup()
@@ -969,18 +685,12 @@ if __name__ == "__main__":
969
  start_session_cleanup_scheduler()
970
 
971
  # 3. API 설정 초기화
972
- try:
973
- api_utils.initialize_api_configs()
974
- except Exception as e:
975
- logger.warning(f"API 설정 초기화 중 오류 (계속 진행): {e}")
976
 
977
  # 4. Gemini 모델 초기화
978
- try:
979
- gemini_model = text_utils.get_gemini_model()
980
- except Exception as e:
981
- logger.warning(f"Gemini 모델 초기화 중 오류 (계속 진행): {e}")
982
 
983
- logger.info("===== 멀티유저 메인키워드 분석 Application Startup at %s =====", time.strftime("%Y-%m-%d %H:%M:%S"))
984
  logger.info(f"📁 임시 파일 저장 위치: {app_temp_dir}")
985
 
986
  # ========== 앱 실행 ==========
 
5
  import threading
6
  import tempfile
7
  import logging
 
8
  import uuid
9
  import shutil
10
  import glob
 
18
  format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
19
  handlers=[
20
  logging.StreamHandler(),
21
+ logging.FileHandler('category_analysis_app.log', mode='a')
22
  ]
23
  )
24
 
 
151
  # 기존 세션 파일들 정리
152
  session_files = glob.glob(os.path.join(temp_dir, "session_*.xlsx"))
153
  session_files.extend(glob.glob(os.path.join(temp_dir, "session_*.csv")))
154
+ session_files.extend(glob.glob(os.path.join(temp_dir, "*category*.xlsx")))
155
+ session_files.extend(glob.glob(os.path.join(temp_dir, "*category*.csv")))
156
+ session_files.extend(glob.glob(os.path.join(temp_dir, "*analysis*.xlsx")))
157
+ session_files.extend(glob.glob(os.path.join(temp_dir, "*analysis*.csv")))
158
  session_files.extend(glob.glob(os.path.join(temp_dir, "tmp*.xlsx")))
159
  session_files.extend(glob.glob(os.path.join(temp_dir, "tmp*.csv")))
160
 
 
192
  cleanup_huggingface_temp_folders()
193
 
194
  # 2. 애플리케이션 전용 임시 디렉토리 생성
195
+ app_temp_dir = os.path.join(tempfile.gettempdir(), "category_analysis_app")
196
  if os.path.exists(app_temp_dir):
197
  shutil.rmtree(app_temp_dir, ignore_errors=True)
198
  os.makedirs(app_temp_dir, exist_ok=True)
199
 
200
  # 3. 환경 변수 설정 (임시 디렉토리 지정)
201
+ os.environ['CATEGORY_APP_TEMP'] = app_temp_dir
202
 
203
  logger.info(f"✅ 애플리케이션 전용 임시 디렉토리 설정: {app_temp_dir}")
204
 
 
210
 
211
  def get_app_temp_dir():
212
  """애플리케이션 전용 임시 디렉토리 반환"""
213
+ return os.environ.get('CATEGORY_APP_TEMP', tempfile.gettempdir())
214
 
215
  def get_session_id():
216
  """세션 ID 생성"""
 
275
  def create_session_temp_file(session_id, suffix='.xlsx'):
276
  """세션별 임시 파일 생성 (전용 디렉토리 사용)"""
277
  timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
278
+ random_suffix = str(time.time_ns())[-4:]
279
 
280
  # 애플리케이션 전용 임시 디렉토리 사용
281
  temp_dir = get_app_temp_dir()
 
289
  register_session_file(session_id, temp_file_path)
290
  return temp_file_path
291
 
292
+ def analyze_product_terms_wrapper(product_name, main_keyword, current_state, session_id):
293
+ """상품명 키워드 분석 래퍼 함수 - 세션 ID 추가"""
294
  update_session_activity(session_id)
295
 
296
+ if not product_name:
297
+ return "상품명을 입력해주세요.", current_state, None, gr.update(visible=False)
298
 
299
+ # 분석 수행 - HTML 결과와 키워드 분석 결과 함께 받기
300
+ result_html, keyword_results = category_analysis.analyze_product_terms(product_name, main_keyword)
 
 
 
301
 
302
+ # 새로운 상태 생성
303
+ if current_state is None or not isinstance(current_state, dict):
304
+ current_state = {}
305
 
306
+ # 분석 결과를 상태에 추가
307
+ current_state["keyword_analysis_results"] = keyword_results
308
+ current_state["product_name"] = product_name
309
+ current_state["main_keyword"] = main_keyword
 
 
310
 
311
+ # 세션별 엑셀 파일 다운로드 - 자동 다운로드
312
+ excel_path = download_analysis(current_state, session_id)
313
 
314
+ # 출력 섹션 표시
315
+ return result_html, current_state, excel_path, gr.update(visible=True)
316
+
317
+ def download_analysis(result, session_id):
318
+ """카테고리 분석 결과 다운로드 (세션별)"""
319
+ update_session_activity(session_id)
 
 
 
 
 
 
 
 
 
 
 
 
320
 
321
+ if not result or not isinstance(result, dict):
322
+ logger.warning(f"세션 {session_id[:8]}... 분석 결과가 없습니다.")
323
+ return None
324
 
 
 
 
 
 
 
 
325
  try:
326
+ # 상품명 분석 결과가 있는지 확인
327
+ if "keyword_analysis_results" in result:
328
+ logger.info(f"세션 {session_id[:8]}... 키워드 분석 결과 포함하여 다운로드: {len(result['keyword_analysis_results'])}개 키워드")
329
+
330
+ # 세션별 임시 파일 생성
331
+ temp_filename = create_session_temp_file(session_id, '.xlsx')
332
+
333
+ # 데이터프레임 생성
334
+ keywords = []
335
+ pc_volumes = []
336
+ mobile_volumes = []
337
+ total_volumes = []
338
+ ranges = []
339
+ category_items = []
340
+
341
+ for kw_result in result["keyword_analysis_results"]:
342
+ keywords.append(kw_result.get("키워드", ""))
343
+ pc_volumes.append(kw_result.get("PC검색량", 0))
344
+ mobile_volumes.append(kw_result.get("모바일검색량", 0))
345
+ total_volumes.append(kw_result.get("총검색량", 0))
346
+ ranges.append(kw_result.get("검색량구간", ""))
347
+ category_items.append(kw_result.get("카테고리항목", ""))
348
+
349
+ # 데이터프레임으로 변환
350
+ df = pd.DataFrame({
351
+ "키워드": keywords,
352
+ "PC검색량": pc_volumes,
353
+ "모바일검색량": mobile_volumes,
354
+ "총검색량": total_volumes,
355
+ "검색량구간": ranges,
356
+ "카테고리항목": category_items
357
+ })
358
+
359
+ with pd.ExcelWriter(temp_filename, engine="xlsxwriter") as writer:
360
+ df.to_excel(writer, sheet_name="상품명 검증 결과", index=False)
361
+
362
+ ws = writer.sheets["상품명 검증 결과"]
363
+
364
+ # 줄바꿈 + 위쪽 정렬 서식
365
+ wrap_fmt = writer.book.add_format({
366
+ "text_wrap": True,
367
+ "valign": "top"
368
+ })
369
+
370
+ # F열('카테고리항목') 전체에 서식 적용 + 열 너비
371
+ ws.set_column("F:F", 40, wrap_fmt)
372
+
373
+ # 열 너비 설정
374
+ worksheet = writer.sheets['상품명 검증 결과']
375
+ worksheet.set_column('A:A', 20) # 키워드
376
+ worksheet.set_column('B:B', 12) # PC검색량
377
+ worksheet.set_column('C:C', 12) # 모바일검색량
378
+ worksheet.set_column('D:D', 12) # 총검색량
379
+ worksheet.set_column('E:E', 12) # 검색량구간
380
+ worksheet.set_column('F:F', 40) # 카테고리항목
381
+
382
+ # 헤더 서식 지정
383
+ header_format = writer.book.add_format({
384
+ 'bold': True,
385
+ 'bg_color': '#FB7F0D',
386
+ 'color': 'white',
387
+ 'border': 1
388
+ })
389
+
390
+ # 헤더에 서식 적용
391
+ for col_num, value in enumerate(df.columns.values):
392
+ worksheet.write(0, col_num, value, header_format)
393
+
394
+ logger.info(f"세션 {session_id[:8]}... 엑셀 파일 저장 완료: {temp_filename}")
395
+ return temp_filename
396
+ else:
397
+ logger.warning(f"세션 {session_id[:8]}... 키워드 분석 결과가 없습니다.")
398
+ return None
399
  except Exception as e:
400
+ logger.error(f"세션 {session_id[:8]}... 다운로드 오류 발생: {e}")
401
+ import traceback
402
+ logger.error(traceback.format_exc())
403
  return None
404
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
405
  def reset_interface(session_id):
406
  """인터페이스 리셋 함수 - 세션별 데이터 초기화"""
407
  update_session_activity(session_id)
 
418
  session_temp_files[session_id] = []
419
 
420
  return (
421
+ "", # 메인 키워드 입력
422
+ "", # 상품명 입력
423
+ "", # 분석 결과 출력
424
+ None, # 다운로드 파일
425
+ None, # 상태 변수
426
+ gr.update(visible=False) # 분석 결과 섹션
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
427
  )
428
 
429
+ def product_analyze_with_loading(product_name, main_keyword, current_state, session_id):
430
+ """로딩 표시 함수"""
431
  update_session_activity(session_id)
432
  return gr.update(visible=True)
433
 
434
+ def process_product_analyze(product_name, main_keyword, current_state, session_id):
435
+ """실제 분석 수행"""
436
  update_session_activity(session_id)
437
+ results = analyze_product_terms_wrapper(product_name, main_keyword, current_state, session_id)
438
+ # 로딩 인디케이터 숨기기
439
  return results + (gr.update(visible=False),)
440
 
441
  # 세션 정리 스케줄러
 
452
 
453
  def cleanup_on_startup():
454
  """애플리케이션 시작 시 전체 정리"""
455
+ logger.info("🧹 카테고리 분석 애플리케이션 시작 - 초기 정리 작업 시작...")
456
 
457
  # 1. 허깅페이스 임시 폴더 정리
458
  cleanup_huggingface_temp_folders()
 
471
 
472
  # Gradio 인터페이스 생성
473
  def create_app():
474
+ # FontAwesome 아이콘 포함
475
  fontawesome_html = """
476
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
477
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/static/pretendard.css">
 
483
  with open('style.css', 'r', encoding='utf-8') as f:
484
  custom_css = f.read()
485
  except:
486
+ # CSS 파일이 없는 경우 기본 스타일 사용
487
  custom_css = """
488
  :root {
489
  --primary-color: #FB7F0D;
 
556
  50% { width: 70%; }
557
  100% { width: 10%; }
558
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
559
  .execution-section {
560
  margin-top: 20px;
561
  background-color: #f9f9f9;
 
584
  # 세션 ID 상태 (각 사용자별로 고유)
585
  session_id = gr.State(get_session_id)
586
 
 
 
 
587
  # 입력 섹션
588
  with gr.Column(elem_classes="custom-frame fade-in"):
589
+ gr.HTML('<div class="section-title"><i class="fas fa-tag"></i> 상품명 분석 입력</div>')
590
 
591
+ # 메인 키워드와 상품명 입력을 한 줄에 배치
592
  with gr.Row():
593
  with gr.Column(scale=1):
594
+ main_keyword = gr.Textbox(
595
  label="메인 키워드",
596
  placeholder="예: 오징어"
597
  )
598
  with gr.Column(scale=1):
599
+ product_name = gr.Textbox(
600
+ label="상품명",
601
+ placeholder="예: 손질 오징어 촉촉한 진미채"
602
+ )
603
+
604
+ # 실행 섹션 - 버튼 통합
605
+ with gr.Column(elem_classes="execution-section"):
606
+ gr.HTML('<div class="section-title"><i class="fas fa-play-circle"></i> 실행</div>')
607
+ with gr.Row():
608
+ with gr.Column(scale=1):
609
+ analyze_product_btn = gr.Button(
610
+ "상품명 분석",
611
+ elem_classes=["execution-button", "primary-button"]
612
+ )
613
+ with gr.Column(scale=1):
614
+ reset_btn = gr.Button(
615
+ "모든 입력 초기화",
616
+ elem_classes=["execution-button", "secondary-button"]
617
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
618
 
619
+ # 진행 상태 표시 섹션 (초기에는 숨김)
620
  with gr.Column(elem_classes="custom-frame fade-in", visible=False) as progress_section:
621
  gr.HTML('<div class="section-title"><i class="fas fa-spinner"></i> 분석 진행 상태</div>')
622
+ # 사용자 친화적인 진행 상태 표시
623
  progress_html = gr.HTML("""
624
  <div style="padding: 15px; background-color: #f9f9f9; border-radius: 5px; margin: 10px 0; border: 1px solid #ddd;">
625
  <div style="margin-bottom: 10px; display: flex; align-items: center;">
626
  <i class="fas fa-spinner fa-spin" style="color: #FB7F0D; margin-right: 10px;"></i>
627
+ <span>상품명 분석중입니다. 잠시만 기다려주세요...</span>
628
  </div>
629
  <div style="background-color: #e9ecef; height: 10px; border-radius: 5px; overflow: hidden;">
630
  <div class="progress-bar"></div>
 
632
  </div>
633
  """)
634
 
635
+ # 상품명 키워드 분석 결과 섹션 (초기에는 숨김)
636
+ with gr.Column(elem_classes="custom-frame fade-in", visible=False) as product_analysis_section:
637
+ gr.HTML('<div class="section-title"><i class="fas fa-table"></i> 상품명 키워드 분석 결과</div>')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
638
 
639
+ # 상품명 분석 결과
640
+ product_analysis_result = gr.HTML(elem_classes="fade-in")
641
 
642
+ # 엑셀 다운로드 파일
643
+ download_file = gr.File(
644
+ label="분석 결과 다운로드",
645
+ visible=True
646
+ )
647
+
648
+ # 상태 저장용 변수 - 분석 결과 저장
649
+ analysis_result_state = gr.State()
650
+
651
+ # 상품명 분석 버튼 연결 - 로딩 표시 자동 다운로드 (세션 ID 추가)
652
+ analyze_product_btn.click(
653
+ fn=product_analyze_with_loading,
654
+ inputs=[product_name, main_keyword, analysis_result_state, session_id],
655
+ outputs=[progress_section]
656
  ).then(
657
+ fn=process_product_analyze,
658
+ inputs=[product_name, main_keyword, analysis_result_state, session_id],
659
  outputs=[
660
+ product_analysis_result, analysis_result_state,
661
+ download_file, product_analysis_section, progress_section
 
 
 
662
  ]
663
  )
664
 
665
+ # 리셋 버튼 이벤트 연결 (세션 ID 추가)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
666
  reset_btn.click(
667
  fn=reset_interface,
668
  inputs=[session_id],
669
  outputs=[
670
+ main_keyword, product_name, product_analysis_result,
671
+ download_file, analysis_result_state, product_analysis_section
 
 
 
 
 
 
672
  ]
673
  )
674
 
 
676
 
677
  if __name__ == "__main__":
678
  # ========== 시작 시 전체 초기화 ==========
679
+ logger.info("🚀 카테고리 분석 애플리케이션 시작...")
680
 
681
  # 1. 첫 번째: 허깅페이스 임시 폴더 정리 및 환경 설정
682
  app_temp_dir = cleanup_on_startup()
 
685
  start_session_cleanup_scheduler()
686
 
687
  # 3. API 설정 초기화
688
+ api_utils.initialize_api_configs()
 
 
 
689
 
690
  # 4. Gemini 모델 초기화
691
+ gemini_model = text_utils.get_gemini_model()
 
 
 
692
 
693
+ logger.info("===== 멀티유저 카테고리 분석 Application Startup at %s =====", time.strftime("%Y-%m-%d %H:%M:%S"))
694
  logger.info(f"📁 임시 파일 저장 위치: {app_temp_dir}")
695
 
696
  # ========== 앱 실행 ==========