import gradio as gr import pandas as pd import os import time import threading import tempfile import logging import random import uuid import shutil import glob from datetime import datetime import sys import types # 로깅 설정 logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', handlers=[ logging.StreamHandler(), logging.FileHandler('main_keyword_app.log', mode='a') ] ) logger = logging.getLogger(__name__) # 환경변수에서 모듈 코드 로드 및 동적 생성 def load_module_from_env(module_name, env_var_name): """환경변수에서 모듈 코드를 로드하여 동적으로 모듈 생성""" try: module_code = os.getenv(env_var_name) if not module_code: raise ValueError(f"환경변수 {env_var_name}가 설정되지 않았습니다.") # 새 모듈 생성 module = types.ModuleType(module_name) # 모듈에 필요한 기본 임포트들 추가 module.__dict__.update({ 'os': __import__('os'), 'time': __import__('time'), 'logging': __import__('logging'), 'pandas': __import__('pandas'), 'requests': __import__('requests'), 'tempfile': __import__('tempfile'), 'threading': __import__('threading'), 're': __import__('re'), 'random': __import__('random'), 'uuid': __import__('uuid'), 'shutil': __import__('shutil'), 'glob': __import__('glob'), 'datetime': __import__('datetime'), 'types': __import__('types'), 'collections': __import__('collections'), 'Counter': __import__('collections').Counter, 'defaultdict': __import__('collections').defaultdict, 'hmac': __import__('hmac'), 'hashlib': __import__('hashlib'), 'base64': __import__('base64'), }) # 코드 실행 exec(module_code, module.__dict__) # 시스템 모듈에 등록 sys.modules[module_name] = module logger.info(f"✅ 모듈 {module_name} 로드 완료") return module except Exception as e: logger.error(f"❌ 모듈 {module_name} 로드 실패: {e}") raise # 필요한 모듈들을 환경변수에서 로드 logger.info("🔄 모듈 로드 시작...") try: # 1. api_utils 모듈 로드 api_utils = load_module_from_env('api_utils', 'API_UTILS_CODE') # 2. text_utils 모듈 로드 (다른 모듈들이 의존하므로 먼저 로드) text_utils = load_module_from_env('text_utils', 'TEXT_UTILS_CODE') # 3. keyword_search 모듈 로드 keyword_search = load_module_from_env('keyword_search', 'KEYWORD_SEARCH_CODE') # 4. product_search 모듈 로드 (text_utils, keyword_search 의존) product_search_module = load_module_from_env('product_search', 'PRODUCT_SEARCH_CODE') # product_search 모듈에 의존성 주입 product_search_module.api_utils = api_utils product_search_module.text_utils = text_utils product_search = product_search_module # 5. keyword_processor 모듈 로드 keyword_processor_module = load_module_from_env('keyword_processor', 'KEYWORD_PROCESSOR_CODE') # keyword_processor 모듈에 의존성 주입 keyword_processor_module.text_utils = text_utils keyword_processor_module.keyword_search = keyword_search keyword_processor_module.product_search = product_search keyword_processor = keyword_processor_module # 6. export_utils 모듈 로드 export_utils = load_module_from_env('export_utils', 'EXPORT_UTILS_CODE') # 7. category_analysis 모듈 로드 (모든 모듈 의존) category_analysis_module = load_module_from_env('category_analysis', 'CATEGORY_ANALYSIS_CODE') # category_analysis 모듈에 의존성 주입 category_analysis_module.text_utils = text_utils category_analysis_module.product_search = product_search category_analysis_module.keyword_search = keyword_search category_analysis = category_analysis_module logger.info("✅ 모든 모듈 로드 완료") except Exception as e: logger.error(f"❌ 모듈 로드 중 치명적 오류: {e}") logger.error("필요한 환경변수들이 설정되었는지 확인하세요:") logger.error("- API_UTILS_CODE") logger.error("- TEXT_UTILS_CODE") logger.error("- KEYWORD_SEARCH_CODE") logger.error("- PRODUCT_SEARCH_CODE") logger.error("- KEYWORD_PROCESSOR_CODE") logger.error("- EXPORT_UTILS_CODE") logger.error("- CATEGORY_ANALYSIS_CODE") raise # 세션별 임시 파일 관리를 위한 딕셔너리 session_temp_files = {} session_data = {} def cleanup_huggingface_temp_folders(): """허깅페이스 임시 폴더 초기 정리""" try: # 일반적인 임시 디렉토리들 temp_dirs = [ tempfile.gettempdir(), "/tmp", "/var/tmp", os.path.join(os.getcwd(), "temp"), os.path.join(os.getcwd(), "tmp"), "/gradio_cached_examples", "/flagged" ] cleanup_count = 0 for temp_dir in temp_dirs: if os.path.exists(temp_dir): try: # 기존 세션 파일들 정리 session_files = glob.glob(os.path.join(temp_dir, "session_*.xlsx")) session_files.extend(glob.glob(os.path.join(temp_dir, "session_*.csv"))) session_files.extend(glob.glob(os.path.join(temp_dir, "*keyword*.xlsx"))) session_files.extend(glob.glob(os.path.join(temp_dir, "*keyword*.csv"))) session_files.extend(glob.glob(os.path.join(temp_dir, "tmp*.xlsx"))) session_files.extend(glob.glob(os.path.join(temp_dir, "tmp*.csv"))) for file_path in session_files: try: # 파일이 1시간 이상 오래된 경우만 삭제 if os.path.getmtime(file_path) < time.time() - 3600: os.remove(file_path) cleanup_count += 1 logger.info(f"초기 정리: 오래된 임시 파일 삭제 - {file_path}") except Exception as e: logger.warning(f"파일 삭제 실패 (무시됨): {file_path} - {e}") except Exception as e: logger.warning(f"임시 디렉토리 정리 실패 (무시됨): {temp_dir} - {e}") logger.info(f"✅ 허깅페이스 임시 폴더 초기 정리 완료 - {cleanup_count}개 파일 삭제") # Gradio 캐시 폴더도 정리 try: gradio_temp_dir = os.path.join(os.getcwd(), "gradio_cached_examples") if os.path.exists(gradio_temp_dir): shutil.rmtree(gradio_temp_dir, ignore_errors=True) logger.info("Gradio 캐시 폴더 정리 완료") except Exception as e: logger.warning(f"Gradio 캐시 폴더 정리 실패 (무시됨): {e}") except Exception as e: logger.error(f"초기 임시 폴더 정리 중 오류 (계속 진행): {e}") def setup_clean_temp_environment(): """깨끗한 임시 환경 설정""" try: # 1. 기존 임시 파일들 정리 cleanup_huggingface_temp_folders() # 2. 애플리케이션 전용 임시 디렉토리 생성 app_temp_dir = os.path.join(tempfile.gettempdir(), "keyword_app") if os.path.exists(app_temp_dir): shutil.rmtree(app_temp_dir, ignore_errors=True) os.makedirs(app_temp_dir, exist_ok=True) # 3. 환경 변수 설정 (임시 디렉토리 지정) os.environ['KEYWORD_APP_TEMP'] = app_temp_dir logger.info(f"✅ 애플리케이션 전용 임시 디렉토리 설정: {app_temp_dir}") return app_temp_dir except Exception as e: logger.error(f"임시 환경 설정 실패: {e}") return tempfile.gettempdir() def get_app_temp_dir(): """애플리케이션 전용 임시 디렉토리 반환""" return os.environ.get('KEYWORD_APP_TEMP', tempfile.gettempdir()) def get_session_id(): """세션 ID 생성""" return str(uuid.uuid4()) def cleanup_session_files(session_id, delay=300): """세션별 임시 파일 정리 함수""" def cleanup(): time.sleep(delay) if session_id in session_temp_files: files_to_remove = session_temp_files[session_id].copy() del session_temp_files[session_id] for file_path in files_to_remove: try: if os.path.exists(file_path): os.remove(file_path) logger.info(f"세션 {session_id[:8]}... 임시 파일 삭제: {file_path}") except Exception as e: logger.error(f"세션 {session_id[:8]}... 파일 삭제 오류: {e}") threading.Thread(target=cleanup, daemon=True).start() def register_session_file(session_id, file_path): """세션별 파일 등록""" if session_id not in session_temp_files: session_temp_files[session_id] = [] session_temp_files[session_id].append(file_path) def cleanup_old_sessions(): """오래된 세션 데이터 정리""" current_time = time.time() sessions_to_remove = [] for session_id, data in session_data.items(): if current_time - data.get('last_activity', 0) > 3600: # 1시간 초과 sessions_to_remove.append(session_id) for session_id in sessions_to_remove: # 파일 정리 if session_id in session_temp_files: for file_path in session_temp_files[session_id]: try: if os.path.exists(file_path): os.remove(file_path) logger.info(f"오래된 세션 {session_id[:8]}... 파일 삭제: {file_path}") except Exception as e: logger.error(f"오래된 세션 파일 삭제 오류: {e}") del session_temp_files[session_id] # 세션 데이터 정리 if session_id in session_data: del session_data[session_id] logger.info(f"오래된 세션 데이터 삭제: {session_id[:8]}...") def update_session_activity(session_id): """세션 활동 시간 업데이트""" if session_id not in session_data: session_data[session_id] = {} session_data[session_id]['last_activity'] = time.time() def create_session_temp_file(session_id, suffix='.xlsx'): """세션별 임시 파일 생성 (전용 디렉토리 사용)""" timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") random_suffix = str(random.randint(1000, 9999)) # 애플리케이션 전용 임시 디렉토리 사용 temp_dir = get_app_temp_dir() filename = f"session_{session_id[:8]}_{timestamp}_{random_suffix}{suffix}" temp_file_path = os.path.join(temp_dir, filename) # 빈 파일 생성 with open(temp_file_path, 'w') as f: pass register_session_file(session_id, temp_file_path) return temp_file_path def wrapper_modified(keyword, korean_only, apply_main_keyword_option, exclude_zero_volume, session_id): """키워드 검색 및 처리 래퍼 함수 (세션 ID 추가)""" update_session_activity(session_id) # 현재 키워드 사용 (세션별로 관리) current_keyword = keyword # 키워드가 비어있는 경우 처리 if not keyword: return (gr.update(value=""), gr.update(choices=["전체 보기"]), gr.update(choices=["전체"]), None, gr.update(choices=["전체 보기"], value="전체 보기"), None, gr.update(visible=False), gr.update(visible=False), current_keyword) # 네이버 쇼핑 API 검색 수행 search_results = product_search.fetch_naver_shopping_data(keyword, korean_only, apply_main_keyword_option == "메인키워드 적용") # 검색 결과가 없는 경우 if not search_results.get("product_list"): return (gr.update(value="

검색 결과가 없습니다. 다른 키워드로 시도해보세요.

"), gr.update(choices=["전체 보기"]), gr.update(choices=["전체"]), None, gr.update(choices=["전체 보기"], value="전체 보기"), None, gr.update(visible=False), gr.update(visible=False), current_keyword) # 검색 결과 처리 - 키워드 전달 및 검색량 0 키워드 제외 옵션 전달 result = keyword_processor.process_search_results(search_results, current_keyword, exclude_zero_volume) df_products = result["products_df"] df_keywords = result["keywords_df"] category_list = result["categories"] if df_keywords.empty: return (gr.update(value="

추출된 키워드가 없습니다. 다른 옵션으로 시도해보세요.

"), gr.update(choices=["전체 보기"]), gr.update(choices=["전체"]), df_keywords, gr.update(choices=["전체 보기"], value="전체 보기"), None, gr.update(visible=False), gr.update(visible=False), current_keyword) # HTML 테이블 생성 html = export_utils.create_table_without_checkboxes(df_keywords) # 필터링을 위한 고유 값 리스트 생성 volume_range_choices = ["전체"] + sorted(df_keywords["검색량구간"].unique().tolist()) # 분석할 카테고리 드롭다운도 같은 선택지로 업데이트 first_category = category_list[0] if category_list else "전체 보기" # 세션별 엑셀 파일 생성 excel_path = create_session_excel_file(df_keywords, session_id) # 분석 섹션 표시 return (gr.update(value=html), gr.update(choices=category_list), gr.update(choices=volume_range_choices), df_keywords, gr.update(choices=category_list, value=first_category), excel_path, gr.update(visible=True), gr.update(visible=True), current_keyword) def create_session_excel_file(df, session_id): """세션별 엑셀 파일 생성""" try: excel_path = create_session_temp_file(session_id, '.xlsx') df.to_excel(excel_path, index=False, engine='openpyxl') logger.info(f"세션 {session_id[:8]}... 엑셀 파일 생성: {excel_path}") return excel_path except Exception as e: logger.error(f"세션별 엑셀 파일 생성 오류: {e}") return None def analyze_with_auto_download(analysis_keywords, selected_category, state_df, session_id): """카테고리 일치 분석 실행 및 자동 다운로드 (세션 ID 추가)""" update_session_activity(session_id) # 분석할 키워드나 카테고리가 없는 경우 if not analysis_keywords or not selected_category: return "키워드와 카테고리를 모두 선택해주세요.", None, gr.update(visible=False) # 분석 실행 - 동적 로딩된 category_analysis 모듈 사용 analysis_result = category_analysis.analyze_keywords_by_category(analysis_keywords, selected_category, state_df) # 세션별 엑셀 파일 생성 excel_path = create_session_excel_file(state_df, session_id) # 분석 결과 출력 섹션 표시 return analysis_result, excel_path, gr.update(visible=True) def filter_and_sort_table(df, selected_cat, keyword_sort, total_volume_sort, usage_count_sort, selected_volume_range, exclude_zero_volume, session_id): """테이블 필터링 및 정렬 함수 (세션 ID 추가)""" update_session_activity(session_id) if df is None or df.empty: return "" # 필터링 적용 filtered_df = df.copy() # 카테고리 필터 적용 if selected_cat and selected_cat != "전체 보기": cat_name_to_filter = selected_cat.rsplit(" (", 1)[0] filtered_df = filtered_df[filtered_df["관련 카테고리"].astype(str).str.contains(cat_name_to_filter, case=False, na=False)] def get_filtered_category_display(current_categories_str): if pd.isna(current_categories_str): return "" categories = str(current_categories_str).split('\n') matched_categories = [cat for cat in categories if cat_name_to_filter.lower() in cat.lower()] if matched_categories: return "\n".join(matched_categories) return current_categories_str filtered_df['관련 카테고리'] = filtered_df['관련 카테고리'].apply(get_filtered_category_display) # 검색량 구간 필터 적용 if selected_volume_range and selected_volume_range != "전체": filtered_df = filtered_df[filtered_df["검색량구간"] == selected_volume_range] # 검색량 0 제외 필터 적용 if exclude_zero_volume: filtered_df = filtered_df[filtered_df["총검색량"] > 0] logger.info(f"세션 {session_id[:8]}... 검색량 0 제외 필터 적용 - 남은 키워드 수: {len(filtered_df)}") # 정렬 적용 if keyword_sort != "정렬 없음": is_ascending = keyword_sort == "오름차순" filtered_df = filtered_df.sort_values(by="조합 키워드", ascending=is_ascending) if total_volume_sort != "정렬 없음": is_ascending = total_volume_sort == "오름차순" filtered_df = filtered_df.sort_values(by="총검색량", ascending=is_ascending) # 키워드 사용횟수 정렬 적용 if usage_count_sort != "정렬 없음": is_ascending = usage_count_sort == "오름차순" filtered_df = filtered_df.sort_values(by="키워드 사용횟수", ascending=is_ascending) # 순번을 1부터 순차적으로 유지하기 위해 행 인덱스 재설정 filtered_df = filtered_df.reset_index(drop=True) # 순번을 포함한 HTML 테이블 생성 html = export_utils.create_table_without_checkboxes(filtered_df) return html def update_category_selection(selected_cat, session_id): """카테고리 필터 선택 시 분석할 카테고리도 같은 값으로 업데이트""" update_session_activity(session_id) logger.debug(f"세션 {session_id[:8]}... 카테고리 선택 변경: {selected_cat}") return gr.update(value=selected_cat) def reset_interface(session_id): """인터페이스 리셋 함수 - 세션별 데이터 초기화""" update_session_activity(session_id) # 세션별 임시 파일 정리 if session_id in session_temp_files: for file_path in session_temp_files[session_id]: try: if os.path.exists(file_path): os.remove(file_path) logger.info(f"세션 {session_id[:8]}... 리셋 시 파일 삭제: {file_path}") except Exception as e: logger.error(f"세션 {session_id[:8]}... 리셋 시 파일 삭제 오류: {e}") session_temp_files[session_id] = [] return ( "", # 검색 키워드 True, # 한글만 추출 False, # 검색량 0 키워드 제외 "메인키워드 적용", # 조합 방식 "", # HTML 테이블 ["전체 보기"], # 카테고리 필터 "전체 보기", # 카테고리 필터 선택 ["전체"], # 검색량 구간 필터 "전체", # 검색량 구간 선택 "정렬 없음", # 총검색량 정렬 "정렬 없음", # 키워드 사용횟수 정렬 None, # 상태 DataFrame ["전체 보기"], # 분석할 카테고리 "전체 보기", # 분석할 카테고리 선택 "", # 키워드 입력 "", # 분석 결과 None, # 다운로드 파일 gr.update(visible=False), # 키워드 분석 섹션 gr.update(visible=False), # 분석 결과 출력 섹션 "" # 키워드 상태 ) # 래퍼 함수들도 세션 ID 추가 def search_with_loading(keyword, korean_only, apply_main_keyword, exclude_zero_volume, session_id): update_session_activity(session_id) return ( gr.update(visible=True), gr.update(visible=False) ) def process_search_results(keyword, korean_only, apply_main_keyword, exclude_zero_volume, session_id): update_session_activity(session_id) result = wrapper_modified(keyword, korean_only, apply_main_keyword, exclude_zero_volume, session_id) table_html, cat_choices, vol_choices, df, selected_cat, excel, keyword_section_vis, cat_section_vis, new_keyword_state = result if not isinstance(df, type(None)) and not df.empty: empty_placeholder_vis = False keyword_section_visibility = True execution_section_visibility = True else: empty_placeholder_vis = True keyword_section_visibility = False execution_section_visibility = False return ( table_html, cat_choices, vol_choices, df, selected_cat, excel, gr.update(visible=keyword_section_visibility), gr.update(visible=cat_section_vis), gr.update(visible=False), gr.update(visible=empty_placeholder_vis), gr.update(visible=execution_section_visibility), new_keyword_state ) def analyze_with_loading(analysis_keywords, selected_category, state_df, session_id): update_session_activity(session_id) return gr.update(visible=True) def process_analyze_results(analysis_keywords, selected_category, state_df, session_id): update_session_activity(session_id) results = analyze_with_auto_download(analysis_keywords, selected_category, state_df, session_id) return results + (gr.update(visible=False),) # 세션 정리 스케줄러 def start_session_cleanup_scheduler(): """세션 정리 스케줄러 시작""" def cleanup_scheduler(): while True: time.sleep(600) # 10분마다 실행 cleanup_old_sessions() # 추가로 허깅페이스 임시 폴더도 주기적 정리 cleanup_huggingface_temp_folders() threading.Thread(target=cleanup_scheduler, daemon=True).start() def cleanup_on_startup(): """애플리케이션 시작 시 전체 정리""" logger.info("🧹 애플리케이션 시작 - 초기 정리 작업 시작...") # 1. 허깅페이스 임시 폴더 정리 cleanup_huggingface_temp_folders() # 2. 깨끗한 임시 환경 설정 app_temp_dir = setup_clean_temp_environment() # 3. 전역 변수 초기화 global session_temp_files, session_data session_temp_files.clear() session_data.clear() logger.info(f"✅ 초기 정리 작업 완료 - 앱 전용 디렉토리: {app_temp_dir}") return app_temp_dir # Gradio 인터페이스 생성 def create_app(): fontawesome_html = """ """ # CSS 파일 로드 try: with open('style.css', 'r', encoding='utf-8') as f: custom_css = f.read() except: custom_css = """ :root { --primary-color: #FB7F0D; --secondary-color: #ff9a8b; } .custom-button { background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)) !important; color: white !important; border-radius: 30px !important; height: 45px !important; font-size: 16px !important; font-weight: bold !important; width: 100% !important; text-align: center !important; display: flex !important; align-items: center !important; justify-content: center !important; } .reset-button { background: linear-gradient(135deg, #6c757d, #495057) !important; color: white !important; border-radius: 30px !important; height: 45px !important; font-size: 16px !important; font-weight: bold !important; width: 100% !important; text-align: center !important; display: flex !important; align-items: center !important; justify-content: center !important; } .section-title { border-bottom: 2px solid #FB7F0D; font-weight: bold; padding-bottom: 5px; margin-bottom: 15px; } .loading-indicator { display: flex; align-items: center; justify-content: center; padding: 15px; background-color: #f8f9fa; border-radius: 5px; margin: 10px 0; border: 1px solid #ddd; } .loading-spinner { border: 4px solid rgba(0, 0, 0, 0.1); width: 24px; height: 24px; border-radius: 50%; border-left-color: #FB7F0D; animation: spin 1s linear infinite; margin-right: 10px; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .progress-bar { height: 10px; background-color: #FB7F0D; border-radius: 5px; width: 0%; animation: progressAnim 2s ease-in-out infinite; } @keyframes progressAnim { 0% { width: 10%; } 50% { width: 70%; } 100% { width: 10%; } } .empty-table { width: 100%; border-collapse: collapse; font-size: 14px; margin-top: 20px; } .empty-table th { background-color: #FB7F0D; color: white; text-align: left; padding: 12px; border: 1px solid #ddd; } .empty-table td { padding: 10px; border: 1px solid #ddd; text-align: center; color: #999; } .button-container { margin-top: 20px; display: flex; gap: 15px; } .execution-section { margin-top: 20px; background-color: #f9f9f9; border-radius: 8px; padding: 15px; border: 1px solid #e5e5e5; } .session-info { background-color: #e8f4f8; padding: 8px 12px; border-radius: 4px; font-size: 12px; color: #0c5460; margin-bottom: 10px; text-align: center; } """ with gr.Blocks(css=custom_css, theme=gr.themes.Default( primary_hue="orange", secondary_hue="orange", font=[gr.themes.GoogleFont("Noto Sans KR"), "ui-sans-serif", "system-ui"] )) as demo: gr.HTML(fontawesome_html) # 세션 ID 상태 (각 사용자별로 고유) session_id = gr.State(get_session_id) # 키워드 상태 관리 keyword_state = gr.State("") # 입력 섹션 with gr.Column(elem_classes="custom-frame fade-in"): gr.HTML('
검색 입력
') with gr.Row(): with gr.Column(scale=1): keyword = gr.Textbox( label="메인 키워드", placeholder="예: 오징어" ) with gr.Column(scale=1): search_btn = gr.Button( "메인키워드 분석", elem_classes="custom-button" ) with gr.Accordion("옵션 설정", open=False): with gr.Row(): with gr.Column(scale=1): korean_only = gr.Checkbox( label="한글만 추출", value=True ) with gr.Column(scale=1): exclude_zero_volume = gr.Checkbox( label="검색량 0 키워드 제외", value=False ) with gr.Row(): with gr.Column(scale=1): apply_main_keyword = gr.Radio( ["메인키워드 적용", "메인키워드 미적용"], label="조합 방식", value="메인키워드 적용" ) with gr.Column(scale=1): gr.HTML("") # 진행 상태 표시 섹션 with gr.Column(elem_classes="custom-frame fade-in", visible=False) as progress_section: gr.HTML('
분석 진행 상태
') progress_html = gr.HTML("""
키워드 데이터를 분석중입니다. 잠시만 기다려주세요...
""") # 메인키워드 분석 결과 섹션 with gr.Column(elem_classes="custom-frame fade-in") as main_keyword_section: gr.HTML('
메인키워드 분석 결과
') empty_table_html = gr.HTML("""
순번 조합 키워드 PC검색량 모바일검색량 총검색량 검색량구간 키워드 사용자순위 키워드 사용횟수 상품 등록 카테고리
검색을 실행하면 여기에 결과가 표시됩니다
""") with gr.Column(visible=False) as keyword_analysis_section: with gr.Row(): with gr.Column(scale=1): category_filter = gr.Dropdown( choices=["전체 보기"], label="카테고리 필터", value="전체 보기", interactive=True ) with gr.Column(scale=1): total_volume_sort = gr.Dropdown( choices=["정렬 없음", "오름차순", "내림차순"], label="총검색량 정렬", value="정렬 없음", interactive=True ) with gr.Row(): with gr.Column(scale=1): search_volume_filter = gr.Dropdown( choices=["전체"], label="검색량 구간 필터", value="전체", interactive=True ) with gr.Column(scale=1): usage_count_sort = gr.Dropdown( choices=["정렬 없음", "오름차순", "내림차순"], label="키워드 사용횟수 정렬", value="정렬 없음", interactive=True ) gr.HTML("
") table_output = gr.HTML(elem_classes="fade-in") # 카테고리 분석 섹션 with gr.Column(elem_classes="custom-frame fade-in", visible=False) as category_analysis_section: gr.HTML('
키워드 분석
') with gr.Row(): with gr.Column(scale=1): analysis_keywords = gr.Textbox( label="키워드 입력 (최대 20개, 쉼표 또는 엔터로 구분)", placeholder="예: 오징어볶음, 오징어 손질, 오징어 요리...", lines=5 ) with gr.Column(scale=1): selected_category = gr.Dropdown( label="분석할 카테고리(분석 전 반드시 선택해주세요)", choices=["전체 보기"], value="전체 보기", interactive=True ) # 실행 섹션 with gr.Column(elem_classes="execution-section", visible=False) as execution_section: gr.HTML('
실행
') with gr.Row(): with gr.Column(scale=1): analyze_btn = gr.Button( "카테고리 일치 분석", elem_classes=["execution-button", "primary-button"] ) with gr.Column(scale=1): reset_btn = gr.Button( "모든 입력 초기화", elem_classes=["execution-button", "secondary-button"] ) # 분석 결과 출력 섹션 with gr.Column(elem_classes="custom-frame fade-in", visible=False) as analysis_output_section: gr.HTML('
분석 결과 요약
') analysis_result = gr.HTML(elem_classes="fade-in") with gr.Row(): download_output = gr.File( label="키워드 목록 다운로드", visible=True ) # 상태 저장용 변수 state_df = gr.State() # 이벤트 연결 - 모든 함수에 session_id 추가 search_btn.click( fn=search_with_loading, inputs=[keyword, korean_only, apply_main_keyword, exclude_zero_volume, session_id], outputs=[progress_section, empty_table_html] ).then( fn=process_search_results, inputs=[keyword, korean_only, apply_main_keyword, exclude_zero_volume, session_id], outputs=[ table_output, category_filter, search_volume_filter, state_df, selected_category, download_output, keyword_analysis_section, category_analysis_section, progress_section, empty_table_html, execution_section, keyword_state ] ) # 필터 및 정렬 변경 이벤트 연결 - session_id 추가 category_filter.change( fn=filter_and_sort_table, inputs=[ state_df, category_filter, gr.Textbox(value="정렬 없음", visible=False), total_volume_sort, usage_count_sort, search_volume_filter, exclude_zero_volume, session_id ], outputs=[table_output] ) category_filter.change( fn=update_category_selection, inputs=[category_filter, session_id], outputs=[selected_category] ) total_volume_sort.change( fn=filter_and_sort_table, inputs=[ state_df, category_filter, gr.Textbox(value="정렬 없음", visible=False), total_volume_sort, usage_count_sort, search_volume_filter, exclude_zero_volume, session_id ], outputs=[table_output] ) usage_count_sort.change( fn=filter_and_sort_table, inputs=[ state_df, category_filter, gr.Textbox(value="정렬 없음", visible=False), total_volume_sort, usage_count_sort, search_volume_filter, exclude_zero_volume, session_id ], outputs=[table_output] ) search_volume_filter.change( fn=filter_and_sort_table, inputs=[ state_df, category_filter, gr.Textbox(value="정렬 없음", visible=False), total_volume_sort, usage_count_sort, search_volume_filter, exclude_zero_volume, session_id ], outputs=[table_output] ) exclude_zero_volume.change( fn=filter_and_sort_table, inputs=[ state_df, category_filter, gr.Textbox(value="정렬 없음", visible=False), total_volume_sort, usage_count_sort, search_volume_filter, exclude_zero_volume, session_id ], outputs=[table_output] ) # 카테고리 분석 버튼 이벤트 - session_id 추가 analyze_btn.click( fn=analyze_with_loading, inputs=[analysis_keywords, selected_category, state_df, session_id], outputs=[progress_section] ).then( fn=process_analyze_results, inputs=[analysis_keywords, selected_category, state_df, session_id], outputs=[analysis_result, download_output, analysis_output_section, progress_section] ) # 리셋 버튼 이벤트 연결 - session_id 추가 reset_btn.click( fn=reset_interface, inputs=[session_id], outputs=[ keyword, korean_only, exclude_zero_volume, apply_main_keyword, table_output, category_filter, category_filter, search_volume_filter, search_volume_filter, total_volume_sort, usage_count_sort, state_df, selected_category, selected_category, analysis_keywords, analysis_result, download_output, keyword_analysis_section, analysis_output_section, keyword_state ] ) return demo if __name__ == "__main__": # ========== 시작 시 전체 초기화 ========== logger.info("🚀 메인키워드 분석 애플리케이션 시작...") # 1. 첫 번째: 허깅페이스 임시 폴더 정리 및 환경 설정 app_temp_dir = cleanup_on_startup() # 2. 세션 정리 스케줄러 시작 start_session_cleanup_scheduler() # 3. API 설정 초기화 try: api_utils.initialize_api_configs() except Exception as e: logger.warning(f"API 설정 초기화 중 오류 (계속 진행): {e}") # 4. Gemini 모델 초기화 try: gemini_model = text_utils.get_gemini_model() except Exception as e: logger.warning(f"Gemini 모델 초기화 중 오류 (계속 진행): {e}") logger.info("===== 멀티유저 메인키워드 분석 Application Startup at %s =====", time.strftime("%Y-%m-%d %H:%M:%S")) logger.info(f"📁 임시 파일 저장 위치: {app_temp_dir}") # ========== 앱 실행 ========== try: app = create_app() app.launch( share=False, # 보안을 위해 share 비활성화 server_name="0.0.0.0", # 모든 IP에서 접근 허용 server_port=7860, # 포트 지정 max_threads=40, # 멀티유저를 위한 스레드 수 증가 auth=None, # 필요시 인증 추가 가능 show_error=True, # 에러 표시 quiet=False, # 로그 표시 favicon_path=None, # 파비콘 설정 ssl_verify=False # SSL 검증 비활성화 (개발용) ) except Exception as e: logger.error(f"애플리케이션 실행 실패: {e}") raise finally: # 애플리케이션 종료 시 정리 logger.info("🧹 애플리케이션 종료 - 최종 정리 작업...") try: cleanup_huggingface_temp_folders() if os.path.exists(app_temp_dir): shutil.rmtree(app_temp_dir, ignore_errors=True) logger.info("✅ 최종 정리 완료") except Exception as e: logger.error(f"최종 정리 중 오류: {e}")