CoolFace
Apppublic

ssboost/4G23WAS3

sourceHugging Faceupdated 1y agoView on Hugging Face
0likes
app.py722 linesDownload Raw Back to root
1import gradio as gr2import pandas as pd3import os4import time5import threading6import tempfile7import logging8import uuid9import shutil10import glob11from datetime import datetime12import sys13import types14 15# 로깅 설정16logging.basicConfig(17    level=logging.INFO,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 25logger = logging.getLogger(__name__)26 27# 환경변수에서 모듈 코드 로드 및 동적 생성28def load_module_from_env(module_name, env_var_name):29    """환경변수에서 모듈 코드를 로드하여 동적으로 모듈 생성"""30    try:31        module_code = os.getenv(env_var_name)32        if not module_code:33            raise ValueError(f"환경변수 {env_var_name}가 설정되지 않았습니다.")34        35        # 새 모듈 생성36        module = types.ModuleType(module_name)37        38        # 모듈에 필요한 기본 임포트들 추가39        module.__dict__.update({40            'os': __import__('os'),41            'time': __import__('time'),42            'logging': __import__('logging'),43            'pandas': __import__('pandas'),44            'requests': __import__('requests'),45            'tempfile': __import__('tempfile'),46            'threading': __import__('threading'),47            're': __import__('re'),48            'random': __import__('random'),49            'uuid': __import__('uuid'),50            'shutil': __import__('shutil'),51            'glob': __import__('glob'),52            'datetime': __import__('datetime'),53            'types': __import__('types'),54            'collections': __import__('collections'),55            'Counter': __import__('collections').Counter,56            'defaultdict': __import__('collections').defaultdict,57            'hmac': __import__('hmac'),58            'hashlib': __import__('hashlib'),59            'base64': __import__('base64'),60        })61        62        # 코드 실행63        exec(module_code, module.__dict__)64        65        # 시스템 모듈에 등록66        sys.modules[module_name] = module67        68        logger.info(f"✅ 모듈 {module_name} 로드 완료")69        return module70        71    except Exception as e:72        logger.error(f"❌ 모듈 {module_name} 로드 실패: {e}")73        raise74 75# 필요한 모듈들을 환경변수에서 로드76logger.info("🔄 모듈 로드 시작...")77 78try:79    # 1. api_utils 모듈 로드80    api_utils = load_module_from_env('api_utils', 'API_UTILS_CODE')81    82    # 2. text_utils 모듈 로드 (다른 모듈들이 의존하므로 먼저 로드)83    text_utils = load_module_from_env('text_utils', 'TEXT_UTILS_CODE')84    85    # 3. keyword_search 모듈 로드86    keyword_search = load_module_from_env('keyword_search', 'KEYWORD_SEARCH_CODE')87    88    # 4. product_search 모듈 로드 (text_utils, keyword_search 의존)89    product_search_module = load_module_from_env('product_search', 'PRODUCT_SEARCH_CODE')90    # product_search 모듈에 의존성 주입91    product_search_module.api_utils = api_utils92    product_search_module.text_utils = text_utils93    product_search = product_search_module94    95    # 5. keyword_processor 모듈 로드96    keyword_processor_module = load_module_from_env('keyword_processor', 'KEYWORD_PROCESSOR_CODE')97    # keyword_processor 모듈에 의존성 주입98    keyword_processor_module.text_utils = text_utils99    keyword_processor_module.keyword_search = keyword_search100    keyword_processor_module.product_search = product_search101    keyword_processor = keyword_processor_module102    103    # 6. export_utils 모듈 로드104    export_utils = load_module_from_env('export_utils', 'EXPORT_UTILS_CODE')105    106    # 7. category_analysis 모듈 로드 (모든 모듈 의존)107    category_analysis_module = load_module_from_env('category_analysis', 'CATEGORY_ANALYSIS_CODE')108    # category_analysis 모듈에 의존성 주입109    category_analysis_module.text_utils = text_utils110    category_analysis_module.product_search = product_search111    category_analysis_module.keyword_search = keyword_search112    category_analysis = category_analysis_module113    114    logger.info("✅ 모든 모듈 로드 완료")115    116except Exception as e:117    logger.error(f"❌ 모듈 로드 중 치명적 오류: {e}")118    logger.error("필요한 환경변수들이 설정되었는지 확인하세요:")119    logger.error("- API_UTILS_CODE")120    logger.error("- TEXT_UTILS_CODE") 121    logger.error("- KEYWORD_SEARCH_CODE")122    logger.error("- PRODUCT_SEARCH_CODE")123    logger.error("- KEYWORD_PROCESSOR_CODE")124    logger.error("- EXPORT_UTILS_CODE")125    logger.error("- CATEGORY_ANALYSIS_CODE")126    raise127 128# 세션별 임시 파일 관리를 위한 딕셔너리129session_temp_files = {}130session_data = {}131 132def cleanup_huggingface_temp_folders():133    """허깅페이스 임시 폴더 초기 정리"""134    try:135        # 일반적인 임시 디렉토리들136        temp_dirs = [137            tempfile.gettempdir(),138            "/tmp",139            "/var/tmp",140            os.path.join(os.getcwd(), "temp"),141            os.path.join(os.getcwd(), "tmp"),142            "/gradio_cached_examples",143            "/flagged"144        ]145        146        cleanup_count = 0147        148        for temp_dir in temp_dirs:149            if os.path.exists(temp_dir):150                try: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                    161                    for file_path in session_files:162                        try:163                            # 파일이 1시간 이상 오래된 경우만 삭제164                            if os.path.getmtime(file_path) < time.time() - 3600:165                                os.remove(file_path)166                                cleanup_count += 1167                                logger.info(f"초기 정리: 오래된 임시 파일 삭제 - {file_path}")168                        except Exception as e:169                            logger.warning(f"파일 삭제 실패 (무시됨): {file_path} - {e}")170                            171                except Exception as e:172                    logger.warning(f"임시 디렉토리 정리 실패 (무시됨): {temp_dir} - {e}")173        174        logger.info(f"✅ 허깅페이스 임시 폴더 초기 정리 완료 - {cleanup_count}개 파일 삭제")175        176        # Gradio 캐시 폴더도 정리177        try:178            gradio_temp_dir = os.path.join(os.getcwd(), "gradio_cached_examples")179            if os.path.exists(gradio_temp_dir):180                shutil.rmtree(gradio_temp_dir, ignore_errors=True)181                logger.info("Gradio 캐시 폴더 정리 완료")182        except Exception as e:183            logger.warning(f"Gradio 캐시 폴더 정리 실패 (무시됨): {e}")184            185    except Exception as e:186        logger.error(f"초기 임시 폴더 정리 중 오류 (계속 진행): {e}")187 188def setup_clean_temp_environment():189    """깨끗한 임시 환경 설정"""190    try:191        # 1. 기존 임시 파일들 정리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_dir202        203        logger.info(f"✅ 애플리케이션 전용 임시 디렉토리 설정: {app_temp_dir}")204        205        return app_temp_dir206        207    except Exception as e:208        logger.error(f"임시 환경 설정 실패: {e}")209        return tempfile.gettempdir()210 211def get_app_temp_dir():212    """애플리케이션 전용 임시 디렉토리 반환"""213    return os.environ.get('CATEGORY_APP_TEMP', tempfile.gettempdir())214 215def get_session_id():216    """세션 ID 생성"""217    return str(uuid.uuid4())218 219def cleanup_session_files(session_id, delay=300):220    """세션별 임시 파일 정리 함수"""221    def cleanup():222        time.sleep(delay)223        if session_id in session_temp_files:224            files_to_remove = session_temp_files[session_id].copy()225            del session_temp_files[session_id]226            227            for file_path in files_to_remove:228                try:229                    if os.path.exists(file_path):230                        os.remove(file_path)231                        logger.info(f"세션 {session_id[:8]}... 임시 파일 삭제: {file_path}")232                except Exception as e:233                    logger.error(f"세션 {session_id[:8]}... 파일 삭제 오류: {e}")234    235    threading.Thread(target=cleanup, daemon=True).start()236 237def register_session_file(session_id, file_path):238    """세션별 파일 등록"""239    if session_id not in session_temp_files:240        session_temp_files[session_id] = []241    session_temp_files[session_id].append(file_path)242 243def cleanup_old_sessions():244    """오래된 세션 데이터 정리"""245    current_time = time.time()246    sessions_to_remove = []247    248    for session_id, data in session_data.items():249        if current_time - data.get('last_activity', 0) > 3600:  # 1시간 초과250            sessions_to_remove.append(session_id)251    252    for session_id in sessions_to_remove:253        # 파일 정리254        if session_id in session_temp_files:255            for file_path in session_temp_files[session_id]:256                try:257                    if os.path.exists(file_path):258                        os.remove(file_path)259                        logger.info(f"오래된 세션 {session_id[:8]}... 파일 삭제: {file_path}")260                except Exception as e:261                    logger.error(f"오래된 세션 파일 삭제 오류: {e}")262            del session_temp_files[session_id]263        264        # 세션 데이터 정리265        if session_id in session_data:266            del session_data[session_id]267            logger.info(f"오래된 세션 데이터 삭제: {session_id[:8]}...")268 269def update_session_activity(session_id):270    """세션 활동 시간 업데이트"""271    if session_id not in session_data:272        session_data[session_id] = {}273    session_data[session_id]['last_activity'] = time.time()274 275def 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()282    filename = f"session_{session_id[:8]}_{timestamp}_{random_suffix}{suffix}"283    temp_file_path = os.path.join(temp_dir, filename)284    285    # 빈 파일 생성286    with open(temp_file_path, 'w') as f:287        pass288    289    register_session_file(session_id, temp_file_path)290    return temp_file_path291 292def 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_results308    current_state["product_name"] = product_name309    current_state["main_keyword"] = main_keyword310    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 317def 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 None324    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_items357            })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': 1388                })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_filename396        else:397            logger.warning(f"세션 {session_id[:8]}... 키워드 분석 결과가 없습니다.")398            return None399    except Exception as e:400        logger.error(f"세션 {session_id[:8]}... 다운로드 중 오류 발생: {e}")401        import traceback402        logger.error(traceback.format_exc())403        return None404 405def reset_interface(session_id):406    """인터페이스 리셋 함수 - 세션별 데이터 초기화"""407    update_session_activity(session_id)408    409    # 세션별 임시 파일 정리410    if session_id in session_temp_files:411        for file_path in session_temp_files[session_id]:412            try:413                if os.path.exists(file_path):414                    os.remove(file_path)415                    logger.info(f"세션 {session_id[:8]}... 리셋 시 파일 삭제: {file_path}")416            except Exception as e:417                logger.error(f"세션 {session_id[:8]}... 리셋 시 파일 삭제 오류: {e}")418        session_temp_files[session_id] = []419    420    return (421        "",  # 메인 키워드 입력422        "",  # 상품명 입력423        "",  # 분석 결과 출력424        None,  # 다운로드 파일425        None,  # 상태 변수426        gr.update(visible=False)  # 분석 결과 섹션427    )428 429def 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 434def 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# 세션 정리 스케줄러442def start_session_cleanup_scheduler():443    """세션 정리 스케줄러 시작"""444    def cleanup_scheduler():445        while True:446            time.sleep(600)  # 10분마다 실행447            cleanup_old_sessions()448            # 추가로 허깅페이스 임시 폴더도 주기적 정리449            cleanup_huggingface_temp_folders()450    451    threading.Thread(target=cleanup_scheduler, daemon=True).start()452 453def cleanup_on_startup():454    """애플리케이션 시작 시 전체 정리"""455    logger.info("🧹 카테고리 분석 애플리케이션 시작 - 초기 정리 작업 시작...")456    457    # 1. 허깅페이스 임시 폴더 정리458    cleanup_huggingface_temp_folders()459    460    # 2. 깨끗한 임시 환경 설정461    app_temp_dir = setup_clean_temp_environment()462    463    # 3. 전역 변수 초기화464    global session_temp_files, session_data465    session_temp_files.clear()466    session_data.clear()467    468    logger.info(f"✅ 초기 정리 작업 완료 - 앱 전용 디렉토리: {app_temp_dir}")469    470    return app_temp_dir471 472# Gradio 인터페이스 생성473def 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">478    <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap">479    """480    481    # CSS 파일 로드482    try: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;490            --secondary-color: #ff9a8b;491        }492        .custom-button {493            background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)) !important;494            color: white !important;495            border-radius: 30px !important;496            height: 45px !important;497            font-size: 16px !important;498            font-weight: bold !important;499            width: 100% !important;500            text-align: center !important;501            display: flex !important;502            align-items: center !important;503            justify-content: center !important;504        }505        .reset-button {506            background: linear-gradient(135deg, #6c757d, #495057) !important;507            color: white !important;508            border-radius: 30px !important;509            height: 45px !important;510            font-size: 16px !important;511            font-weight: bold !important;512            width: 100% !important;513            text-align: center !important;514            display: flex !important;515            align-items: center !important;516            justify-content: center !important;517        }518        .section-title {519            border-bottom: 2px solid #FB7F0D;520            font-weight: bold;521            padding-bottom: 5px;522            margin-bottom: 15px;523        }524        .loading-indicator {525            display: flex;526            align-items: center;527            justify-content: center;528            padding: 15px;529            background-color: #f8f9fa;530            border-radius: 5px;531            margin: 10px 0;532            border: 1px solid #ddd;533        }534        .loading-spinner {535            border: 4px solid rgba(0, 0, 0, 0.1);536            width: 24px;537            height: 24px;538            border-radius: 50%;539            border-left-color: #FB7F0D;540            animation: spin 1s linear infinite;541            margin-right: 10px;542        }543        @keyframes spin {544            0% { transform: rotate(0deg); }545            100% { transform: rotate(360deg); }546        }547        .progress-bar {548            height: 10px;549            background-color: #FB7F0D;550            border-radius: 5px;551            width: 0%;552            animation: progressAnim 2s ease-in-out infinite;553        }554        @keyframes progressAnim {555            0% { width: 10%; }556            50% { width: 70%; }557            100% { width: 10%; }558        }559        .execution-section {560            margin-top: 20px;561            background-color: #f9f9f9;562            border-radius: 8px;563            padding: 15px;564            border: 1px solid #e5e5e5;565        }566        .session-info {567            background-color: #e8f4f8;568            padding: 8px 12px;569            border-radius: 4px;570            font-size: 12px;571            color: #0c5460;572            margin-bottom: 10px;573            text-align: center;574        }575        """576    577    with gr.Blocks(css=custom_css, theme=gr.themes.Default(578        primary_hue="orange",579        secondary_hue="orange",580        font=[gr.themes.GoogleFont("Noto Sans KR"), "ui-sans-serif", "system-ui"]581    )) as demo:582        gr.HTML(fontawesome_html)583        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>631                </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=True646            )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_section662            ]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_section672            ]673        )674                675    return demo676 677if __name__ == "__main__":678    # ========== 시작 시 전체 초기화 ==========679    logger.info("🚀 카테고리 분석 애플리케이션 시작...")680    681    # 1. 첫 번째: 허깅페이스 임시 폴더 정리 및 환경 설정682    app_temp_dir = cleanup_on_startup()683    684    # 2. 세션 정리 스케줄러 시작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    # ========== 앱 실행 ==========697    try:698        app = create_app()699        app.launch(700            share=False,  # 보안을 위해 share 비활성화701            server_name="0.0.0.0",  # 모든 IP에서 접근 허용702            server_port=7860,  # 포트 지정703            max_threads=40,  # 멀티유저를 위한 스레드 수 증가704            auth=None,  # 필요시 인증 추가 가능705            show_error=True,  # 에러 표시706            quiet=False,  # 로그 표시707            favicon_path=None,  # 파비콘 설정708            ssl_verify=False  # SSL 검증 비활성화 (개발용)709        )710    except Exception as e:711        logger.error(f"애플리케이션 실행 실패: {e}")712        raise713    finally:714        # 애플리케이션 종료 시 정리715        logger.info("🧹 애플리케이션 종료 - 최종 정리 작업...")716        try:717            cleanup_huggingface_temp_folders()718            if os.path.exists(app_temp_dir):719                shutil.rmtree(app_temp_dir, ignore_errors=True)720            logger.info("✅ 최종 정리 완료")721        except Exception as e:722            logger.error(f"최종 정리 중 오류: {e}")