Download app.py from ssboost/vgv6d: direct link, hf CLI and curl.
- Browser
- Download file 45.8 kB
-
https://huggingface.co/spaces/ssboost/vgv6d/resolve/main/app.py
- Command line
-
hf download hf://spaces/ssboost/vgv6d/app.py
-
curl -L -o app.py https://huggingface.co/spaces/ssboost/vgv6d/resolve/main/app.py
45.8 kB
| import gradio as gr | |
| import pandas as pd | |
| import os | |
| import logging | |
| from datetime import datetime | |
| import pytz | |
| import time | |
| import tempfile | |
| import zipfile | |
| import re | |
| import json | |
| # ๋ก๊น ์ค์ | |
| logging.basicConfig(level=logging.WARNING, format='%(asctime)s - %(levelname)s - %(message)s') | |
| logger = logging.getLogger(__name__) | |
| # ์ธ๋ถ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ๋ก๊ทธ ๋นํ์ฑํ | |
| logging.getLogger('gradio').setLevel(logging.WARNING) | |
| logging.getLogger('gradio_client').setLevel(logging.WARNING) | |
| logging.getLogger('httpx').setLevel(logging.WARNING) | |
| logging.getLogger('urllib3').setLevel(logging.WARNING) | |
| # ===== API ํด๋ผ์ด์ธํธ ์ค์ ===== | |
| def get_api_client(): | |
| """ํ๊ฒฝ๋ณ์์์ API ์๋ํฌ์ธํธ๋ฅผ ๊ฐ์ ธ์ ํด๋ผ์ด์ธํธ ์์ฑ""" | |
| try: | |
| from gradio_client import Client | |
| # ํ๊ฒฝ๋ณ์์์ API ์๋ํฌ์ธํธ ๊ฐ์ ธ์ค๊ธฐ | |
| api_endpoint = os.getenv('API_ENDPOINT') | |
| if not api_endpoint: | |
| logger.error("API_ENDPOINT ํ๊ฒฝ๋ณ์๊ฐ ์ค์ ๋์ง ์์์ต๋๋ค.") | |
| raise ValueError("API_ENDPOINT ํ๊ฒฝ๋ณ์๊ฐ ์ค์ ๋์ง ์์์ต๋๋ค.") | |
| client = Client(api_endpoint) | |
| logger.info("์๊ฒฉ API ํด๋ผ์ด์ธํธ ์ด๊ธฐํ ์ฑ๊ณต") | |
| return client | |
| except Exception as e: | |
| logger.error(f"API ํด๋ผ์ด์ธํธ ์ด๊ธฐํ ์คํจ: {e}") | |
| return None | |
| # ===== ํ๊ตญ์๊ฐ ๊ด๋ จ ํจ์ ===== | |
| def get_korean_time(): | |
| """ํ๊ตญ์๊ฐ ๋ฐํ""" | |
| korea_tz = pytz.timezone('Asia/Seoul') | |
| return datetime.now(korea_tz) | |
| def format_korean_datetime(dt=None, format_type="filename"): | |
| """ํ๊ตญ์๊ฐ ํฌ๋งทํ """ | |
| if dt is None: | |
| dt = get_korean_time() | |
| if format_type == "filename": | |
| return dt.strftime("%y%m%d_%H%M") | |
| elif format_type == "display": | |
| return dt.strftime('%Y๋ %m์ %d์ผ %H์ %M๋ถ') | |
| elif format_type == "full": | |
| return dt.strftime('%Y-%m-%d %H:%M:%S') | |
| else: | |
| return dt.strftime("%y%m%d_%H%M") | |
| # ===== ๋ฐ์ดํฐ ์ฒ๋ฆฌ ๋ฐ ๊ฒ์ฆ ํจ์๋ค ===== | |
| def create_export_data_from_html(analysis_keyword, main_keyword, analysis_html, step1_data=None): | |
| """๋ถ์ HTML๊ณผ 1๋จ๊ณ ๋ฐ์ดํฐ๋ฅผ ๊ธฐ๋ฐ์ผ๋ก export์ฉ ๋ฐ์ดํฐ ๊ตฌ์กฐ ์์ฑ (๋๋ฏธ ๋ฐ์ดํฐ ์ ๊ฑฐ)""" | |
| logger.info("=== ๐ Export ๋ฐ์ดํฐ ๊ตฌ์กฐ ์์ฑ ์์ (๋๋ฏธ ๋ฐ์ดํฐ ์ ๊ฑฐ ๋ฒ์ ) ===") | |
| # ๊ธฐ๋ณธ export ๋ฐ์ดํฐ ๊ตฌ์กฐ | |
| export_data = { | |
| "main_keyword": main_keyword or analysis_keyword, | |
| "analysis_keyword": analysis_keyword, | |
| "analysis_html": analysis_html, | |
| "main_keywords_df": None, | |
| "related_keywords_df": None, | |
| "analysis_completed": True, | |
| "created_at": get_korean_time().isoformat() | |
| } | |
| # 1๋จ๊ณ ๋ฐ์ดํฐ์์ main_keywords_df ์ถ์ถ (์ค์ ๋ฐ์ดํฐ๋ง) | |
| if step1_data and isinstance(step1_data, dict): | |
| if "keywords_df" in step1_data: | |
| keywords_df = step1_data["keywords_df"] | |
| if isinstance(keywords_df, dict): | |
| try: | |
| export_data["main_keywords_df"] = pd.DataFrame(keywords_df) | |
| logger.info(f"โ 1๋จ๊ณ ํค์๋ ๋ฐ์ดํฐ๋ฅผ DataFrame์ผ๋ก ๋ณํ: {export_data['main_keywords_df'].shape}") | |
| except Exception as e: | |
| logger.warning(f"โ ๏ธ 1๋จ๊ณ ๋ฐ์ดํฐ ๋ณํ ์คํจ: {e}") | |
| export_data["main_keywords_df"] = None | |
| elif hasattr(keywords_df, 'shape'): | |
| export_data["main_keywords_df"] = keywords_df | |
| logger.info(f"โ 1๋จ๊ณ ํค์๋ DataFrame ์ฌ์ฉ: {keywords_df.shape}") | |
| else: | |
| logger.info("๐ 1๋จ๊ณ ํค์๋ ๋ฐ์ดํฐ๊ฐ ์ ํจํ์ง ์์ - None์ผ๋ก ์ ์ง") | |
| export_data["main_keywords_df"] = None | |
| # ๋ถ์ HTML์์ ์ฐ๊ด๊ฒ์์ด ์ ๋ณด ์ถ์ถ ์๋ (์ค์ ๋ฐ์ดํฐ๋ง) | |
| if analysis_html and "์ฐ๊ด๊ฒ์์ด ๋ถ์" in analysis_html: | |
| logger.info("๐ ๋ถ์ HTML์์ ์ฐ๊ด๊ฒ์์ด ์ ๋ณด ๋ฐ๊ฒฌ - ์ค์ ํ์ฑ ํ์") | |
| # ์ค์ HTML ํ์ฑ ๋ก์ง์ด ํ์ํ ๋ถ๋ถ | |
| # ํ์ฌ๋ ๋๋ฏธ ๋ฐ์ดํฐ ๋์ None์ผ๋ก ์ ์ง | |
| export_data["related_keywords_df"] = None | |
| logger.info("๐ก ์ค์ HTML ํ์ฑ ๋ก์ง ๊ตฌํ ํ์ - ์ฐ๊ด๊ฒ์์ด ๋ฐ์ดํฐ๋ None์ผ๋ก ์ ์ง") | |
| logger.info(f"๐ Export ๋ฐ์ดํฐ ๊ตฌ์กฐ ์์ฑ ์๋ฃ (๋๋ฏธ ๋ฐ์ดํฐ ์์):") | |
| logger.info(f" - analysis_keyword: {export_data['analysis_keyword']}") | |
| logger.info(f" - main_keywords_df: {export_data['main_keywords_df'].shape if export_data['main_keywords_df'] is not None else 'None'}") | |
| logger.info(f" - related_keywords_df: {export_data['related_keywords_df'].shape if export_data['related_keywords_df'] is not None else 'None'}") | |
| logger.info(f" - analysis_html: {len(str(export_data['analysis_html']))} ๋ฌธ์") | |
| return export_data | |
| def validate_and_repair_export_data(export_data): | |
| """Export ๋ฐ์ดํฐ ์ ํจ์ฑ ๊ฒ์ฌ ๋ฐ ๋ณต๊ตฌ (๋๋ฏธ ๋ฐ์ดํฐ ์ ๊ฑฐ)""" | |
| logger.info("๐ง Export ๋ฐ์ดํฐ ์ ํจ์ฑ ๊ฒ์ฌ ๋ฐ ๋ณต๊ตฌ ์์ (๋๋ฏธ ๋ฐ์ดํฐ ์ ๊ฑฐ ๋ฒ์ )") | |
| if not export_data or not isinstance(export_data, dict): | |
| logger.warning("โ ๏ธ Export ๋ฐ์ดํฐ๊ฐ ์๊ฑฐ๋ ๋์ ๋๋ฆฌ๊ฐ ์๋ - ๊ธฐ๋ณธ ๊ตฌ์กฐ ์์ฑ") | |
| return { | |
| "main_keyword": "๊ธฐ๋ณธํค์๋", | |
| "analysis_keyword": "๊ธฐ๋ณธ๋ถ์ํค์๋", | |
| "analysis_html": "<div>๊ธฐ๋ณธ ๋ถ์ ๊ฒฐ๊ณผ</div>", | |
| "main_keywords_df": None, # ๋๋ฏธ ๋ฐ์ดํฐ ๋์ None | |
| "related_keywords_df": None, # ๋๋ฏธ ๋ฐ์ดํฐ ๋์ None | |
| "analysis_completed": True | |
| } | |
| # ํ์ ํค๋ค ํ์ธ ๋ฐ ๋ณต๊ตฌ | |
| required_keys = { | |
| "analysis_keyword": "๋ถ์ํค์๋", | |
| "main_keyword": "๋ฉ์ธํค์๋", | |
| "analysis_html": "<div>๋ถ์ ์๋ฃ</div>", | |
| "analysis_completed": True | |
| } | |
| for key, default_value in required_keys.items(): | |
| if key not in export_data or not export_data[key]: | |
| export_data[key] = default_value | |
| logger.info(f"๐ง {key} ํค ๋ณต๊ตฌ: {default_value}") | |
| # DataFrame ๋ฐ์ดํฐ ๊ฒ์ฆ ๋ฐ ๋ณํ (๋๋ฏธ ๋ฐ์ดํฐ ์์ฑ ์ํจ) | |
| for df_key in ["main_keywords_df", "related_keywords_df"]: | |
| if df_key in export_data and export_data[df_key] is not None: | |
| df_data = export_data[df_key] | |
| # ๋์ ๋๋ฆฌ๋ฅผ DataFrame์ผ๋ก ๋ณํ | |
| if isinstance(df_data, dict): | |
| try: | |
| # ๋น ๋์ ๋๋ฆฌ๋ None์ผ๋ก ์ฒ๋ฆฌ | |
| if not df_data: | |
| export_data[df_key] = None | |
| logger.info(f"๐ {df_key} ๋น ๋์ ๋๋ฆฌ - None์ผ๋ก ์ค์ ") | |
| else: | |
| export_data[df_key] = pd.DataFrame(df_data) | |
| logger.info(f"โ {df_key} ๋์ ๋๋ฆฌ๋ฅผ DataFrame์ผ๋ก ๋ณํ ์ฑ๊ณต") | |
| except Exception as e: | |
| logger.warning(f"โ ๏ธ {df_key} ๋ณํ ์คํจ: {e}") | |
| export_data[df_key] = None | |
| elif not hasattr(df_data, 'shape'): | |
| logger.warning(f"โ ๏ธ {df_key}๊ฐ DataFrame์ด ์๋ - None์ผ๋ก ์ค์ ") | |
| export_data[df_key] = None | |
| logger.info("โ Export ๋ฐ์ดํฐ ์ ํจ์ฑ ๊ฒ์ฌ ๋ฐ ๋ณต๊ตฌ ์๋ฃ (๋๋ฏธ ๋ฐ์ดํฐ ์์)") | |
| return export_data | |
| # ===== ํ์ผ ์ถ๋ ฅ ํจ์๋ค ===== | |
| def create_timestamp_filename(analysis_keyword): | |
| """ํ์์คํฌํ๊ฐ ํฌํจ๋ ํ์ผ๋ช ์์ฑ - ํ๊ตญ์๊ฐ ์ ์ฉ""" | |
| timestamp = format_korean_datetime(format_type="filename") | |
| safe_keyword = re.sub(r'[^\w\s-]', '', analysis_keyword).strip() | |
| safe_keyword = re.sub(r'[-\s]+', '_', safe_keyword) | |
| return f"{safe_keyword}_{timestamp}_๋ถ์๊ฒฐ๊ณผ" | |
| def export_to_excel(main_keyword, main_keywords_df, analysis_keyword, related_keywords_df, filename_base): | |
| """์์ ํ์ผ๋ก ์ถ๋ ฅ (์ค์ ๋ฐ์ดํฐ๋ง)""" | |
| try: | |
| # ์ค์ ๋ฐ์ดํฐ๊ฐ ์๋์ง ํ์ธ | |
| has_main_data = main_keywords_df is not None and not main_keywords_df.empty | |
| has_related_data = related_keywords_df is not None and not related_keywords_df.empty | |
| if not has_main_data and not has_related_data: | |
| logger.info("๐ ์์ฑํ ๋ฐ์ดํฐ๊ฐ ์์ด ์์ ํ์ผ ์์ฑ ๊ฑด๋๋") | |
| return None | |
| excel_filename = f"{filename_base}.xlsx" | |
| excel_path = os.path.join(tempfile.gettempdir(), excel_filename) | |
| with pd.ExcelWriter(excel_path, engine='xlsxwriter') as writer: | |
| # ์ํฌ๋ถ๊ณผ ์ํฌ์ํธ ์คํ์ผ ์ค์ | |
| workbook = writer.book | |
| # ํค๋ ์คํ์ผ | |
| header_format = workbook.add_format({ | |
| 'bold': True, | |
| 'text_wrap': True, | |
| 'valign': 'top', | |
| 'fg_color': '#D7E4BC', | |
| 'border': 1 | |
| }) | |
| # ๋ฐ์ดํฐ ์คํ์ผ | |
| data_format = workbook.add_format({ | |
| 'text_wrap': True, | |
| 'valign': 'top', | |
| 'border': 1 | |
| }) | |
| # ์ซ์ ํฌ๋งท | |
| number_format = workbook.add_format({ | |
| 'num_format': '#,##0', | |
| 'text_wrap': True, | |
| 'valign': 'top', | |
| 'border': 1 | |
| }) | |
| # ์ฒซ ๋ฒ์งธ ์ํธ: ๋ฉ์ธํค์๋ ์กฐํฉํค์๋ (์ค์ ๋ฐ์ดํฐ๋ง) | |
| if has_main_data: | |
| main_keywords_df.to_excel(writer, sheet_name=f'{main_keyword}_์กฐํฉํค์๋', index=False) | |
| worksheet1 = writer.sheets[f'{main_keyword}_์กฐํฉํค์๋'] | |
| # ํค๋ ์คํ์ผ ์ ์ฉ | |
| for col_num, value in enumerate(main_keywords_df.columns.values): | |
| worksheet1.write(0, col_num, value, header_format) | |
| # ๋ฐ์ดํฐ ์คํ์ผ ์ ์ฉ | |
| for row_num in range(1, len(main_keywords_df) + 1): | |
| for col_num, value in enumerate(main_keywords_df.iloc[row_num-1]): | |
| if isinstance(value, (int, float)) and col_num in [1, 2, 3]: # ๊ฒ์๋ ์ปฌ๋ผ | |
| worksheet1.write(row_num, col_num, value, number_format) | |
| else: | |
| worksheet1.write(row_num, col_num, value, data_format) | |
| # ์ด ๋๋น ์๋ ์กฐ์ | |
| for i, col in enumerate(main_keywords_df.columns): | |
| max_len = max( | |
| main_keywords_df[col].astype(str).map(len).max(), | |
| len(str(col)) | |
| ) | |
| worksheet1.set_column(i, i, min(max_len + 2, 50)) | |
| logger.info(f"โ ๋ฉ์ธํค์๋ ์ํธ ์์ฑ: {main_keywords_df.shape}") | |
| # ๋ ๋ฒ์งธ ์ํธ: ๋ถ์ํค์๋ ์ฐ๊ด๊ฒ์์ด (์ค์ ๋ฐ์ดํฐ๋ง) | |
| if has_related_data: | |
| related_keywords_df.to_excel(writer, sheet_name=f'{analysis_keyword}_์ฐ๊ด๊ฒ์์ด', index=False) | |
| worksheet2 = writer.sheets[f'{analysis_keyword}_์ฐ๊ด๊ฒ์์ด'] | |
| # ํค๋ ์คํ์ผ ์ ์ฉ | |
| for col_num, value in enumerate(related_keywords_df.columns.values): | |
| worksheet2.write(0, col_num, value, header_format) | |
| # ๋ฐ์ดํฐ ์คํ์ผ ์ ์ฉ | |
| for row_num in range(1, len(related_keywords_df) + 1): | |
| for col_num, value in enumerate(related_keywords_df.iloc[row_num-1]): | |
| if isinstance(value, (int, float)) and col_num in [1, 2, 3]: # ๊ฒ์๋ ์ปฌ๋ผ | |
| worksheet2.write(row_num, col_num, value, number_format) | |
| else: | |
| worksheet2.write(row_num, col_num, value, data_format) | |
| # ์ด ๋๋น ์๋ ์กฐ์ | |
| for i, col in enumerate(related_keywords_df.columns): | |
| max_len = max( | |
| related_keywords_df[col].astype(str).map(len).max(), | |
| len(str(col)) | |
| ) | |
| worksheet2.set_column(i, i, min(max_len + 2, 50)) | |
| logger.info(f"โ ์ฐ๊ด๊ฒ์์ด ์ํธ ์์ฑ: {related_keywords_df.shape}") | |
| logger.info(f"์์ ํ์ผ ์์ฑ ์๋ฃ: {excel_path}") | |
| return excel_path | |
| except Exception as e: | |
| logger.error(f"์์ ํ์ผ ์์ฑ ์ค๋ฅ: {e}") | |
| return None | |
| def export_to_html(analysis_html, filename_base): | |
| """HTML ํ์ผ๋ก ์ถ๋ ฅ - ํ๊ตญ์๊ฐ ์ ์ฉ""" | |
| try: | |
| html_filename = f"{filename_base}.html" | |
| html_path = os.path.join(tempfile.gettempdir(), html_filename) | |
| # ํ๊ตญ์๊ฐ์ผ๋ก ์์ฑ ์๊ฐ ํ์ | |
| korean_time = format_korean_datetime(format_type="display") | |
| # ์์ ํ HTML ๋ฌธ์ ์์ฑ | |
| full_html = f""" | |
| <!DOCTYPE html> | |
| <html lang="ko"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>ํค์๋ ์ฌ์ถฉ๋ถ์ ๊ฒฐ๊ณผ</title> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"> | |
| <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/static/pretendard.css"> | |
| <style> | |
| body {{ | |
| font-family: 'Pretendard', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; | |
| margin: 0; | |
| padding: 20px; | |
| background-color: #f5f5f5; | |
| line-height: 1.6; | |
| }} | |
| .container {{ | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| background: white; | |
| border-radius: 12px; | |
| box-shadow: 0 4px 12px rgba(0,0,0,0.1); | |
| overflow: hidden; | |
| }} | |
| .header {{ | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| padding: 30px; | |
| text-align: center; | |
| }} | |
| .header h1 {{ | |
| margin: 0; | |
| font-size: 28px; | |
| font-weight: 700; | |
| }} | |
| .header p {{ | |
| margin: 10px 0 0 0; | |
| font-size: 16px; | |
| opacity: 0.9; | |
| }} | |
| .content {{ | |
| padding: 30px; | |
| }} | |
| .timestamp {{ | |
| text-align: center; | |
| padding: 20px; | |
| background: #f8f9fa; | |
| color: #6c757d; | |
| font-size: 14px; | |
| border-top: 1px solid #dee2e6; | |
| }} | |
| /* ์ฐจํธ ์คํ์ผ ๊ฐ์ */ | |
| .chart-container {{ | |
| margin: 20px 0; | |
| padding: 20px; | |
| background: white; | |
| border-radius: 8px; | |
| box-shadow: 0 2px 8px rgba(0,0,0,0.1); | |
| }} | |
| /* ๋ฐ์ํ ์คํ์ผ */ | |
| @media (max-width: 768px) {{ | |
| .container {{ | |
| margin: 10px; | |
| border-radius: 8px; | |
| }} | |
| .header {{ | |
| padding: 20px; | |
| }} | |
| .header h1 {{ | |
| font-size: 24px; | |
| }} | |
| .content {{ | |
| padding: 20px; | |
| }} | |
| }} | |
| /* ์ ๋๋ฉ์ด์ */ | |
| @keyframes spin {{ | |
| 0% {{ transform: rotate(0deg); }} | |
| 100% {{ transform: rotate(360deg); }} | |
| }} | |
| @keyframes progress {{ | |
| 0% {{ transform: translateX(-100%); }} | |
| 100% {{ transform: translateX(100%); }} | |
| }} | |
| /* ํ๋ฆฐํธ ์คํ์ผ */ | |
| @media print {{ | |
| body {{ | |
| background: white; | |
| padding: 0; | |
| }} | |
| .container {{ | |
| box-shadow: none; | |
| border-radius: 0; | |
| }} | |
| .header {{ | |
| background: #667eea !important; | |
| -webkit-print-color-adjust: exact; | |
| }} | |
| }} | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <div class="header"> | |
| <h1><i class="fas fa-chart-line"></i> ํค์๋ ์ฌ์ถฉ๋ถ์ ๊ฒฐ๊ณผ</h1> | |
| <p>AI ์ํ ์์ฑ ๋ถ์ ์์คํ v3.2 (๋๋ฏธ ๋ฐ์ดํฐ ์ ๊ฑฐ ๋ฒ์ )</p> | |
| </div> | |
| <div class="content"> | |
| {analysis_html} | |
| </div> | |
| <div class="timestamp"> | |
| <i class="fas fa-clock"></i> ์์ฑ ์๊ฐ: {korean_time} (ํ๊ตญ์๊ฐ) | |
| </div> | |
| </div> | |
| </body> | |
| </html> | |
| """ | |
| with open(html_path, 'w', encoding='utf-8') as f: | |
| f.write(full_html) | |
| logger.info(f"HTML ํ์ผ ์์ฑ ์๋ฃ: {html_path}") | |
| return html_path | |
| except Exception as e: | |
| logger.error(f"HTML ํ์ผ ์์ฑ ์ค๋ฅ: {e}") | |
| return None | |
| def create_zip_file(excel_path, html_path, filename_base): | |
| """์์ถ ํ์ผ ์์ฑ""" | |
| try: | |
| zip_filename = f"{filename_base}.zip" | |
| zip_path = os.path.join(tempfile.gettempdir(), zip_filename) | |
| files_added = 0 | |
| with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf: | |
| if excel_path and os.path.exists(excel_path): | |
| zipf.write(excel_path, f"{filename_base}.xlsx") | |
| logger.info(f"์์ ํ์ผ ์์ถ ์ถ๊ฐ: {filename_base}.xlsx") | |
| files_added += 1 | |
| if html_path and os.path.exists(html_path): | |
| zipf.write(html_path, f"{filename_base}.html") | |
| logger.info(f"HTML ํ์ผ ์์ถ ์ถ๊ฐ: {filename_base}.html") | |
| files_added += 1 | |
| if files_added == 0: | |
| logger.warning("์์ถํ ํ์ผ์ด ์์") | |
| return None | |
| logger.info(f"์์ถ ํ์ผ ์์ฑ ์๋ฃ: {zip_path} ({files_added}๊ฐ ํ์ผ)") | |
| return zip_path | |
| except Exception as e: | |
| logger.error(f"์์ถ ํ์ผ ์์ฑ ์ค๋ฅ: {e}") | |
| return None | |
| def export_analysis_results_enhanced(export_data): | |
| """๊ฐํ๋ ๋ถ์ ๊ฒฐ๊ณผ ์ถ๋ ฅ ๋ฉ์ธ ํจ์ (๋๋ฏธ ๋ฐ์ดํฐ ์ ๊ฑฐ)""" | |
| try: | |
| logger.info("=== ๐ ๊ฐํ๋ ์ถ๋ ฅ ํจ์ ์์ (๋๋ฏธ ๋ฐ์ดํฐ ์ ๊ฑฐ ๋ฒ์ ) ===") | |
| # ๋ฐ์ดํฐ ์ ํจ์ฑ ๊ฒ์ฌ ๋ฐ ๋ณต๊ตฌ | |
| export_data = validate_and_repair_export_data(export_data) | |
| analysis_keyword = export_data.get("analysis_keyword", "๊ธฐ๋ณธํค์๋") | |
| analysis_html = export_data.get("analysis_html", "<div>๋ถ์ ์๋ฃ</div>") | |
| main_keyword = export_data.get("main_keyword", analysis_keyword) | |
| main_keywords_df = export_data.get("main_keywords_df") | |
| related_keywords_df = export_data.get("related_keywords_df") | |
| logger.info(f"๐ ์ฒ๋ฆฌํ ๋ฐ์ดํฐ:") | |
| logger.info(f" - analysis_keyword: '{analysis_keyword}'") | |
| logger.info(f" - main_keyword: '{main_keyword}'") | |
| logger.info(f" - analysis_html: {len(str(analysis_html))} ๋ฌธ์") | |
| logger.info(f" - main_keywords_df: {main_keywords_df.shape if main_keywords_df is not None else 'None'}") | |
| logger.info(f" - related_keywords_df: {related_keywords_df.shape if related_keywords_df is not None else 'None'}") | |
| # ํ์ผ๋ช ์์ฑ (ํ๊ตญ์๊ฐ ์ ์ฉ) | |
| filename_base = create_timestamp_filename(analysis_keyword) | |
| logger.info(f"๐ ์ถ๋ ฅ ํ์ผ๋ช : {filename_base}") | |
| # HTML ํ์ผ์ ๋ถ์ ๊ฒฐ๊ณผ๊ฐ ์์ผ๋ฉด ์์ฑ | |
| html_path = None | |
| if analysis_html and len(str(analysis_html).strip()) > 20: # ์๋ฏธ์๋ HTML์ธ์ง ํ์ธ | |
| logger.info("๐ HTML ํ์ผ ์์ฑ ์์...") | |
| html_path = export_to_html(analysis_html, filename_base) | |
| if html_path: | |
| logger.info(f"โ HTML ํ์ผ ์์ฑ ์ฑ๊ณต: {html_path}") | |
| else: | |
| logger.error("โ HTML ํ์ผ ์์ฑ ์คํจ") | |
| else: | |
| logger.info("๐ ๋ถ์ HTML์ด ์์ด HTML ํ์ผ ์์ฑ ๊ฑด๋๋") | |
| # ์์ ํ์ผ ์์ฑ (์ค์ DataFrame์ด ์๋ ๊ฒฝ์ฐ๋ง) | |
| excel_path = None | |
| if (main_keywords_df is not None and not main_keywords_df.empty) or \ | |
| (related_keywords_df is not None and not related_keywords_df.empty): | |
| logger.info("๐ ์์ ํ์ผ ์์ฑ ์์...") | |
| excel_path = export_to_excel( | |
| main_keyword, | |
| main_keywords_df, | |
| analysis_keyword, | |
| related_keywords_df, | |
| filename_base | |
| ) | |
| if excel_path: | |
| logger.info(f"โ ์์ ํ์ผ ์์ฑ ์ฑ๊ณต: {excel_path}") | |
| else: | |
| logger.warning("โ ๏ธ ์์ ํ์ผ ์์ฑ ์คํจ") | |
| else: | |
| logger.info("๐ ์ค์ DataFrame ๋ฐ์ดํฐ๊ฐ ์์ด ์์ ํ์ผ ์์ฑ ์๋ต") | |
| # ์์ฑ๋ ํ์ผ์ด ์๋์ง ํ์ธ | |
| if not html_path and not excel_path: | |
| logger.warning("โ ๏ธ ์์ฑ๋ ํ์ผ์ด ์์") | |
| return None, "โ ๏ธ ์์ฑํ ์ ์๋ ๋ฐ์ดํฐ๊ฐ ์์ต๋๋ค. ๋ถ์์ ๋จผ์ ์๋ฃํด์ฃผ์ธ์." | |
| # ์์ถ ํ์ผ ์์ฑ | |
| logger.info("๐ฆ ์์ถ ํ์ผ ์์ฑ ์์...") | |
| zip_path = create_zip_file(excel_path, html_path, filename_base) | |
| if zip_path: | |
| file_types = [] | |
| if html_path: | |
| file_types.append("HTML") | |
| if excel_path: | |
| file_types.append("์์ ") | |
| file_list = " + ".join(file_types) | |
| logger.info(f"โ ์์ถ ํ์ผ ์์ฑ ์ฑ๊ณต: {zip_path} ({file_list})") | |
| return zip_path, f"โ ๋ถ์ ๊ฒฐ๊ณผ๊ฐ ์ฑ๊ณต์ ์ผ๋ก ์ถ๋ ฅ๋์์ต๋๋ค!\nํ์ผ๋ช : {filename_base}.zip\nํฌํจ ํ์ผ: {file_list}\n\n๐ก ๋๋ฏธ ๋ฐ์ดํฐ ์ ๊ฑฐ ๋ฒ์ - ์ค์ ๋ถ์ ๋ฐ์ดํฐ๋ง ํฌํจ๋ฉ๋๋ค." | |
| else: | |
| logger.error("โ ์์ถ ํ์ผ ์์ฑ ์คํจ") | |
| return None, "์์ถ ํ์ผ ์์ฑ์ ์คํจํ์ต๋๋ค." | |
| except Exception as e: | |
| logger.error(f"โ ๊ฐํ๋ ์ถ๋ ฅ ํจ์ ์ ์ฒด ์ค๋ฅ: {e}") | |
| import traceback | |
| logger.error(f"์คํ ํธ๋ ์ด์ค:\n{traceback.format_exc()}") | |
| return None, f"์ถ๋ ฅ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค: {str(e)}" | |
| # ===== ๋ก๋ฉ ์ ๋๋ฉ์ด์ ===== | |
| def create_loading_animation(): | |
| """๋ก๋ฉ ์ ๋๋ฉ์ด์ HTML""" | |
| return """ | |
| <div style="display: flex; flex-direction: column; align-items: center; padding: 40px; background: white; border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.1);"> | |
| <div style="width: 60px; height: 60px; border: 4px solid #f3f3f3; border-top: 4px solid #FB7F0D; border-radius: 50%; animation: spin 1s linear infinite; margin-bottom: 20px;"></div> | |
| <h3 style="color: #FB7F0D; margin: 10px 0; font-size: 18px;">๋ถ์ ์ค์ ๋๋ค...</h3> | |
| <p style="color: #666; margin: 5px 0; text-align: center;">์๊ฒฉ ์๋ฒ์์ ๋ฐ์ดํฐ๋ฅผ ์์งํ๊ณ AI๊ฐ ๋ถ์ํ๊ณ ์์ต๋๋ค.<br>์ ์๋ง ๊ธฐ๋ค๋ ค์ฃผ์ธ์.</p> | |
| <div style="width: 200px; height: 4px; background: #f0f0f0; border-radius: 2px; margin-top: 15px; overflow: hidden;"> | |
| <div style="width: 100%; height: 100%; background: linear-gradient(90deg, #FB7F0D, #ff9a8b); border-radius: 2px; animation: progress 2s ease-in-out infinite;"></div> | |
| </div> | |
| </div> | |
| <style> | |
| @keyframes spin { | |
| 0% { transform: rotate(0deg); } | |
| 100% { transform: rotate(360deg); } | |
| } | |
| @keyframes progress { | |
| 0% { transform: translateX(-100%); } | |
| 100% { transform: translateX(100%); } | |
| } | |
| </style> | |
| """ | |
| # ===== ์๋ฌ ์ฒ๋ฆฌ ํจ์ ===== | |
| def generate_error_response(error_message): | |
| """์๋ฌ ์๋ต ์์ฑ""" | |
| return f''' | |
| <div style="color: red; padding: 30px; text-align: center; width: 100%; | |
| background-color: #f8d7da; border-radius: 12px; border: 1px solid #f5c6cb;"> | |
| <h3 style="margin-bottom: 15px;">โ ์ฐ๊ฒฐ ์ค๋ฅ</h3> | |
| <p style="margin-bottom: 20px;">{error_message}</p> | |
| <div style="background: white; padding: 15px; border-radius: 8px; color: #333;"> | |
| <h4>ํด๊ฒฐ ๋ฐฉ๋ฒ:</h4> | |
| <ul style="text-align: left; padding-left: 20px;"> | |
| <li>๋คํธ์ํฌ ์ฐ๊ฒฐ์ ํ์ธํด์ฃผ์ธ์</li> | |
| <li>์๊ฒฉ ์๋ฒ ์ํ๋ฅผ ํ์ธํด์ฃผ์ธ์</li> | |
| <li>์ ์ ํ ๋ค์ ์๋ํด์ฃผ์ธ์</li> | |
| <li>๋ฌธ์ ๊ฐ ์ง์๋๋ฉด ๊ด๋ฆฌ์์๊ฒ ๋ฌธ์ํ์ธ์</li> | |
| </ul> | |
| </div> | |
| </div> | |
| ''' | |
| # ===== ์๊ฒฉ API ํธ์ถ ํจ์๋ค ===== | |
| def call_collect_data_api(keyword): | |
| """1๋จ๊ณ: ์ํ ๋ฐ์ดํฐ ์์ง API ํธ์ถ""" | |
| try: | |
| client = get_api_client() | |
| if not client: | |
| return generate_error_response("API ํด๋ผ์ด์ธํธ๋ฅผ ์ด๊ธฐํํ ์ ์์ต๋๋ค."), {} | |
| logger.info("์๊ฒฉ API ํธ์ถ: ์ํ ๋ฐ์ดํฐ ์์ง") | |
| result = client.predict( | |
| keyword=keyword, | |
| api_name="/on_collect_data" | |
| ) | |
| logger.info(f"๋ฐ์ดํฐ ์์ง API ๊ฒฐ๊ณผ ํ์ : {type(result)}") | |
| # ๊ฒฐ๊ณผ๊ฐ ํํ์ธ ๊ฒฝ์ฐ ์ฒซ ๋ฒ์งธ ์์๋ HTML, ๋ ๋ฒ์งธ๋ ์ธ์ ๋ฐ์ดํฐ | |
| if isinstance(result, tuple) and len(result) == 2: | |
| html_result, session_data = result | |
| # ์ธ์ ๋ฐ์ดํฐ๊ฐ ์ ๋๋ก ์๋์ง ํ์ธ | |
| if isinstance(session_data, dict): | |
| logger.info(f"๋ฐ์ดํฐ ์์ง ์ธ์ ๋ฐ์ดํฐ ์์ : {list(session_data.keys()) if session_data else '๋น ๋์ ๋๋ฆฌ'}") | |
| return html_result, session_data | |
| else: | |
| logger.warning("์ธ์ ๋ฐ์ดํฐ๊ฐ ๋์ ๋๋ฆฌ๊ฐ ์๋๋๋ค.") | |
| return html_result, {} | |
| else: | |
| logger.warning("์์๊ณผ ๋ค๋ฅธ ๋ฐ์ดํฐ ์์ง ๊ฒฐ๊ณผ ํํ") | |
| return str(result), {"keywords_collected": True} | |
| except Exception as e: | |
| logger.error(f"์ํ ๋ฐ์ดํฐ ์์ง API ํธ์ถ ์ค๋ฅ: {e}") | |
| return generate_error_response(f"์๊ฒฉ ์๋ฒ ์ฐ๊ฒฐ ์คํจ: {str(e)}"), {} | |
| def call_analyze_keyword_api_enhanced(analysis_keyword, base_keyword, keywords_data): | |
| """3๋จ๊ณ: ๊ฐํ๋ ํค์๋ ์ฌ์ถฉ๋ถ์ API ํธ์ถ (๋๋ฏธ ๋ฐ์ดํฐ ์ ๊ฑฐ)""" | |
| try: | |
| client = get_api_client() | |
| if not client: | |
| return generate_error_response("API ํด๋ผ์ด์ธํธ๋ฅผ ์ด๊ธฐํํ ์ ์์ต๋๋ค."), {} | |
| logger.info("=== ๐ ๊ฐํ๋ ํค์๋ ์ฌ์ถฉ๋ถ์ API ํธ์ถ (๋๋ฏธ ๋ฐ์ดํฐ ์ ๊ฑฐ) ===") | |
| logger.info(f"ํ๋ผ๋ฏธํฐ - analysis_keyword: '{analysis_keyword}'") | |
| logger.info(f"ํ๋ผ๋ฏธํฐ - base_keyword: '{base_keyword}'") | |
| logger.info(f"ํ๋ผ๋ฏธํฐ - keywords_data ํ์ : {type(keywords_data)}") | |
| # ์๊ฒฉ API ํธ์ถ | |
| result = client.predict( | |
| analysis_keyword, | |
| base_keyword, | |
| keywords_data, | |
| api_name="/on_analyze_keyword" | |
| ) | |
| logger.info(f"๐ก ์๊ฒฉ API ์๋ต ์์ :") | |
| logger.info(f" - ์๋ต ํ์ : {type(result)}") | |
| logger.info(f" - ์๋ต ๊ธธ์ด: {len(result) if hasattr(result, '__len__') else 'N/A'}") | |
| # ์๋ต ์ฒ๋ฆฌ ๋ฐ Export ๋ฐ์ดํฐ ๊ตฌ์กฐ ์์ฑ | |
| if isinstance(result, tuple) and len(result) == 2: | |
| html_result, remote_export_data = result | |
| logger.info(f"๐ ์๊ฒฉ export ๋ฐ์ดํฐ:") | |
| logger.info(f" - ํ์ : {type(remote_export_data)}") | |
| logger.info(f" - ํค๋ค: {list(remote_export_data.keys()) if isinstance(remote_export_data, dict) else 'None'}") | |
| # HTML ๊ฒฐ๊ณผ๊ฐ ์์ผ๋ฉด Export ๋ฐ์ดํฐ ๊ตฌ์กฐ ์์ฑ (๋๋ฏธ ๋ฐ์ดํฐ ์์ด) | |
| if html_result: | |
| logger.info("๐ง Export ๋ฐ์ดํฐ ๊ตฌ์กฐ ์์ฑ ์์ (๋๋ฏธ ๋ฐ์ดํฐ ์ ๊ฑฐ)") | |
| enhanced_export_data = create_export_data_from_html( | |
| analysis_keyword=analysis_keyword, | |
| main_keyword=base_keyword, | |
| analysis_html=html_result, | |
| step1_data=keywords_data | |
| ) | |
| # ์๊ฒฉ์์ ์จ ์ค์ ๋ฐ์ดํฐ๊ฐ ์์ผ๋ฉด ๋ณํฉ | |
| if isinstance(remote_export_data, dict) and remote_export_data: | |
| logger.info("๐ ์๊ฒฉ ์ค์ ๋ฐ์ดํฐ์ ๋ก์ปฌ ๋ฐ์ดํฐ ๋ณํฉ") | |
| for key, value in remote_export_data.items(): | |
| if value is not None and key in ["main_keywords_df", "related_keywords_df"]: | |
| # DataFrame ๋ฐ์ดํฐ๋ง ๊ฒ์ฆํ์ฌ ๋ณํฉ | |
| if isinstance(value, dict) and value: # ๋น ๋์ ๋๋ฆฌ๊ฐ ์๋ ๊ฒฝ์ฐ๋ง | |
| enhanced_export_data[key] = value | |
| logger.info(f" - {key} ์๊ฒฉ ์ค์ ๋ฐ์ดํฐ๋ก ์ ๋ฐ์ดํธ") | |
| elif hasattr(value, 'shape') and not value.empty: # DataFrame์ด๊ณ ๋น์ด์์ง ์์ ๊ฒฝ์ฐ | |
| enhanced_export_data[key] = value | |
| logger.info(f" - {key} ์๊ฒฉ DataFrame ๋ฐ์ดํฐ๋ก ์ ๋ฐ์ดํธ") | |
| elif value is not None and key not in ["main_keywords_df", "related_keywords_df"]: | |
| enhanced_export_data[key] = value | |
| logger.info(f" - {key} ์๊ฒฉ ๋ฐ์ดํฐ๋ก ์ ๋ฐ์ดํธ") | |
| logger.info(f"โ ์ต์ข Export ๋ฐ์ดํฐ ๊ตฌ์กฐ (๋๋ฏธ ๋ฐ์ดํฐ ์์):") | |
| logger.info(f" - ํค ๊ฐ์: {len(enhanced_export_data)}") | |
| logger.info(f" - ํค ๋ชฉ๋ก: {list(enhanced_export_data.keys())}") | |
| return html_result, enhanced_export_data | |
| else: | |
| logger.warning("โ ๏ธ HTML ๊ฒฐ๊ณผ๊ฐ ๋น์ด์์") | |
| return str(result), {} | |
| else: | |
| logger.warning("โ ๏ธ ์์๊ณผ ๋ค๋ฅธ API ์๋ต ํํ") | |
| # HTML๋ง ๋ฐํ๋ ๊ฒฝ์ฐ๋ ์ฒ๋ฆฌ | |
| if isinstance(result, str) and len(result) > 100: # HTML์ผ ๊ฐ๋ฅ์ฑ์ด ๋์ | |
| logger.info("๐ HTML ๋ฌธ์์ด๋ก ์ถ์ ๋๋ ์๋ต - Export ๋ฐ์ดํฐ ์์ฑ (๋๋ฏธ ๋ฐ์ดํฐ ์์ด)") | |
| enhanced_export_data = create_export_data_from_html( | |
| analysis_keyword=analysis_keyword, | |
| main_keyword=base_keyword, | |
| analysis_html=result, | |
| step1_data=keywords_data | |
| ) | |
| return result, enhanced_export_data | |
| else: | |
| return str(result), {} | |
| except Exception as e: | |
| logger.error(f"โ ํค์๋ ์ฌ์ถฉ๋ถ์ API ํธ์ถ ์ค๋ฅ: {e}") | |
| import traceback | |
| logger.error(f"์คํ ํธ๋ ์ด์ค:\n{traceback.format_exc()}") | |
| return generate_error_response(f"์๊ฒฉ ์๋ฒ ์ฐ๊ฒฐ ์คํจ: {str(e)}"), {} | |
| # ===== ๊ทธ๋ผ๋์ค ์ธํฐํ์ด์ค ===== | |
| def create_interface(): | |
| # CSS ์คํ์ผ๋ง (๊ธฐ์กด๊ณผ ๋์ผ) | |
| custom_css = """ | |
| /* ๊ธฐ์กด ๋คํฌ๋ชจ๋ ์๋ ๋ณ๊ฒฝ AI ์ํ ์์ฑ ๋ถ์ ์์คํ CSS */ | |
| :root { | |
| --primary-color: #FB7F0D; | |
| --secondary-color: #ff9a8b; | |
| --accent-color: #FF6B6B; | |
| --background-color: #FFFFFF; | |
| --card-bg: #ffffff; | |
| --input-bg: #ffffff; | |
| --text-color: #334155; | |
| --text-secondary: #64748b; | |
| --border-color: #dddddd; | |
| --border-light: #e5e5e5; | |
| --table-even-bg: #f3f3f3; | |
| --table-hover-bg: #f0f0f0; | |
| --shadow: 0 8px 30px rgba(251, 127, 13, 0.08); | |
| --shadow-light: 0 2px 4px rgba(0, 0, 0, 0.1); | |
| --border-radius: 18px; | |
| } | |
| @media (prefers-color-scheme: dark) { | |
| :root { | |
| --background-color: #1a1a1a; | |
| --card-bg: #2d2d2d; | |
| --input-bg: #2d2d2d; | |
| --text-color: #e5e5e5; | |
| --text-secondary: #a1a1aa; | |
| --border-color: #404040; | |
| --border-light: #525252; | |
| --table-even-bg: #333333; | |
| --table-hover-bg: #404040; | |
| --shadow: 0 8px 30px rgba(0, 0, 0, 0.3); | |
| --shadow-light: 0 2px 4px rgba(0, 0, 0, 0.2); | |
| } | |
| } | |
| body { | |
| font-family: 'Pretendard', 'Noto Sans KR', -apple-system, BlinkMacSystemFont, sans-serif; | |
| background-color: var(--background-color) !important; | |
| color: var(--text-color) !important; | |
| line-height: 1.6; | |
| margin: 0; | |
| padding: 0; | |
| transition: background-color 0.3s ease, color 0.3s ease; | |
| } | |
| .gradio-container { | |
| width: 100%; | |
| margin: 0 auto; | |
| padding: 20px; | |
| background-color: var(--background-color) !important; | |
| } | |
| .custom-frame { | |
| background-color: var(--card-bg) !important; | |
| border: 1px solid var(--border-light) !important; | |
| border-radius: var(--border-radius); | |
| padding: 20px; | |
| margin: 10px 0; | |
| box-shadow: var(--shadow) !important; | |
| color: var(--text-color) !important; | |
| } | |
| .custom-button { | |
| border-radius: 30px !important; | |
| background: var(--primary-color) !important; | |
| color: white !important; | |
| font-size: 18px !important; | |
| padding: 10px 20px !important; | |
| border: none; | |
| box-shadow: 0 4px 8px rgba(251, 127, 13, 0.25); | |
| transition: transform 0.3s ease; | |
| height: 45px !important; | |
| width: 100% !important; | |
| } | |
| .custom-button:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 6px 12px rgba(251, 127, 13, 0.3); | |
| } | |
| .export-button { | |
| background: linear-gradient(135deg, #28a745, #20c997) !important; | |
| color: white !important; | |
| border-radius: 25px !important; | |
| height: 50px !important; | |
| font-size: 17px !important; | |
| font-weight: bold !important; | |
| width: 100% !important; | |
| margin-top: 20px !important; | |
| } | |
| .section-title { | |
| display: flex; | |
| align-items: center; | |
| font-size: 20px; | |
| font-weight: 700; | |
| color: var(--text-color) !important; | |
| margin-bottom: 10px; | |
| padding-bottom: 5px; | |
| border-bottom: 2px solid var(--primary-color); | |
| font-family: 'Pretendard', 'Noto Sans KR', -apple-system, BlinkMacSystemFont, sans-serif; | |
| } | |
| .section-title img, .section-title i { | |
| margin-right: 10px; | |
| font-size: 20px; | |
| color: var(--primary-color); | |
| } | |
| .gr-input, .gr-text-input, .gr-sample-inputs, | |
| input[type="text"], input[type="number"], textarea, select { | |
| border-radius: var(--border-radius) !important; | |
| border: 1px solid var(--border-color) !important; | |
| padding: 12px !important; | |
| box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05) !important; | |
| transition: all 0.3s ease !important; | |
| background-color: var(--input-bg) !important; | |
| color: var(--text-color) !important; | |
| } | |
| .gr-input:focus, .gr-text-input:focus, | |
| input[type="text"]:focus, textarea:focus, select:focus { | |
| border-color: var(--primary-color) !important; | |
| outline: none !important; | |
| box-shadow: 0 0 0 2px rgba(251, 127, 13, 0.2) !important; | |
| } | |
| .fade-in { | |
| animation: fadeIn 0.5s ease-out; | |
| } | |
| @keyframes fadeIn { | |
| from { opacity: 0; transform: translateY(10px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| """ | |
| with gr.Blocks( | |
| css=custom_css, | |
| title="๐ AI ์ํ ์์ฑ ๋ถ์๊ธฐ v3.2 (๋๋ฏธ ๋ฐ์ดํฐ ์ ๊ฑฐ)", | |
| theme=gr.themes.Default(primary_hue="orange", secondary_hue="orange") | |
| ) as interface: | |
| # ํฐํธ ๋ฐ ์์ด์ฝ ๋ก๋ | |
| gr.HTML(""" | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"> | |
| <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/static/pretendard.css"> | |
| """) | |
| # ์ธ์ ๋ณ ์ํ ๋ณ์ | |
| keywords_data_state = gr.State() | |
| export_data_state = gr.State({}) | |
| # === UI ์ปดํฌ๋ํธ๋ค === | |
| with gr.Column(elem_classes="custom-frame fade-in"): | |
| gr.HTML('<div class="section-title"><i class="fas fa-search"></i> 1๋จ๊ณ: ๋ฉ์ธ ํค์๋ ์ ๋ ฅ</div>') | |
| keyword_input = gr.Textbox( | |
| label="์ํ ๋ฉ์ธํค์๋", | |
| placeholder="์: ์ฌ๋ฆฌํผ, ๋ฌด์ ์ด์ดํฐ, ํธ๋ํฌ๋ฆผ", | |
| value="", | |
| elem_id="keyword_input" | |
| ) | |
| collect_data_btn = gr.Button("1๋จ๊ณ: ์ํ ๋ฐ์ดํฐ ์์งํ๊ธฐ", elem_classes="custom-button", size="lg") | |
| with gr.Column(elem_classes="custom-frame fade-in"): | |
| gr.HTML('<div class="section-title"><i class="fas fa-database"></i> 2๋จ๊ณ: ์์ง๋ ํค์๋ ๋ชฉ๋ก</div>') | |
| keywords_result = gr.HTML() | |
| with gr.Column(elem_classes="custom-frame fade-in"): | |
| gr.HTML('<div class="section-title"><i class="fas fa-bullseye"></i> 3๋จ๊ณ: ๋ถ์ํ ํค์๋ ์ ํ</div>') | |
| analysis_keyword_input = gr.Textbox( | |
| label="๋ถ์ํ ํค์๋", | |
| placeholder="์ ๋ชฉ๋ก์์ ์ํ๋ ํค์๋๋ฅผ ์ ๋ ฅํ์ธ์ (์: ํต๊ตฝ ์ฌ๋ฆฌํผ)", | |
| value="", | |
| elem_id="analysis_keyword_input" | |
| ) | |
| analyze_keyword_btn = gr.Button("ํค์๋ ์ฌ์ถฉ๋ถ์ ํ๊ธฐ", elem_classes="custom-button", size="lg") | |
| with gr.Column(elem_classes="custom-frame fade-in"): | |
| gr.HTML('<div class="section-title"><i class="fas fa-chart-line"></i> ํค์๋ ์ฌ์ถฉ๋ถ์</div>') | |
| analysis_result = gr.HTML(label="ํค์๋ ์ฌ์ถฉ๋ถ์") | |
| with gr.Column(elem_classes="custom-frame fade-in"): | |
| gr.HTML('<div class="section-title"><i class="fas fa-download"></i> ๋ถ์ ๊ฒฐ๊ณผ ์ถ๋ ฅ</div>') | |
| gr.HTML(""" | |
| <div style="background: #e3f2fd; border-left: 4px solid #2196f3; padding: 15px; margin: 10px 0; border-radius: 5px;"> | |
| <h4 style="margin: 0 0 10px 0; color: #1976d2;"><i class="fas fa-info-circle"></i> ์ค์ ๋ฐ์ดํฐ ์ถ๋ ฅ ๋ฒ์ </h4> | |
| <p style="margin: 0; color: #1976d2; font-size: 14px;"> | |
| โข ๋ถ์๋ ๋ฐ์ดํฐ๋ฅผ ํ์ผ๋ก ์ถ๋ ฅ๋ฉ๋๋ค<br> | |
| </p> | |
| </div> | |
| """) | |
| export_btn = gr.Button("๐ ๋ถ์๊ฒฐ๊ณผ ์ถ๋ ฅํ๊ธฐ", elem_classes="export-button", size="lg") | |
| export_result = gr.HTML() | |
| download_file = gr.File(label="๋ค์ด๋ก๋", visible=False) | |
| # ===== ์ด๋ฒคํธ ํธ๋ค๋ฌ ===== | |
| def on_collect_data(keyword): | |
| if not keyword.strip(): | |
| return ("<div style='color: red; padding: 20px; text-align: center; width: 100%;'>ํค์๋๋ฅผ ์ ๋ ฅํด์ฃผ์ธ์.</div>", None) | |
| # ๋ก๋ฉ ์ํ ํ์ | |
| yield (create_loading_animation(), None) | |
| # ์๊ฒฉ API ํธ์ถ | |
| result_html, result_data = call_collect_data_api(keyword) | |
| yield (result_html, result_data) | |
| def on_analyze_keyword(analysis_keyword, base_keyword, keywords_data): | |
| if not analysis_keyword.strip(): | |
| return "<div style='color: red; padding: 20px; text-align: center; width: 100%;'>๋ถ์ํ ํค์๋๋ฅผ ์ ๋ ฅํด์ฃผ์ธ์.</div>", {} | |
| # ๋ก๋ฉ ์ํ ํ์ | |
| yield create_loading_animation(), {} | |
| # ๊ฐํ๋ API ํธ์ถ (๋๋ฏธ ๋ฐ์ดํฐ ์ ๊ฑฐ) | |
| html_result, enhanced_export_data = call_analyze_keyword_api_enhanced( | |
| analysis_keyword, base_keyword, keywords_data | |
| ) | |
| yield html_result, enhanced_export_data | |
| def on_export_results(export_data): | |
| """๊ฐํ๋ ๋ถ์ ๊ฒฐ๊ณผ ์ถ๋ ฅ ํธ๋ค๋ฌ (๋๋ฏธ ๋ฐ์ดํฐ ์ ๊ฑฐ)""" | |
| try: | |
| logger.info(f"๐ ์ ๋ ฅ export_data: {type(export_data)}") | |
| if isinstance(export_data, dict): | |
| logger.info(f"๐ export_data ํค๋ค: {list(export_data.keys())}") | |
| # ๊ฐํ๋ ์ถ๋ ฅ ํจ์ ํธ์ถ (๋๋ฏธ ๋ฐ์ดํฐ ์ ๊ฑฐ) | |
| zip_path, message = export_analysis_results_enhanced(export_data) | |
| if zip_path: | |
| success_html = f""" | |
| <div style="background: #d4edda; border: 1px solid #c3e6cb; padding: 20px; border-radius: 8px; margin: 10px 0;"> | |
| <h4 style="color: #155724; margin: 0 0 15px 0;"><i class="fas fa-check-circle"></i> ์ถ๋ ฅ ์๋ฃ!</h4> | |
| <p style="color: #155724; margin: 0; line-height: 1.6;"> | |
| {message}<br> | |
| <strong>๋ฐ์ดํฐ์ถ๋ ฅ:</strong><br> | |
| <br> | |
| <i class="fas fa-download"></i> ์๋ ๋ค์ด๋ก๋ ๋ฒํผ์ ํด๋ฆญํ์ฌ ํ์ผ์ ์ ์ฅํ์ธ์.<br> | |
| <small style="color: #666;">โฐ ํ๊ตญ์๊ฐ ๊ธฐ์ค์ผ๋ก ํ์ผ๋ช ์ด ์์ฑ๋ฉ๋๋ค.</small> | |
| </p> | |
| </div> | |
| """ | |
| return success_html, gr.update(value=zip_path, visible=True) | |
| else: | |
| error_html = f""" | |
| <div style="background: #f8d7da; border: 1px solid #f5c6cb; padding: 20px; border-radius: 8px; margin: 10px 0;"> | |
| <h4 style="color: #721c24; margin: 0 0 10px 0;"><i class="fas fa-exclamation-triangle"></i> ์ถ๋ ฅ ์คํจ</h4> | |
| <p style="color: #721c24; margin: 0;">{message}</p> | |
| <div style="margin-top: 15px; padding: 15px; background: white; border-radius: 5px;"> | |
| <h5 style="color: #721c24; margin: 0 0 10px 0;">๐ ๋๋ฒ๊น ์ ๋ณด:</h5> | |
| <ul style="color: #721c24; margin: 0; padding-left: 20px;"> | |
| <li>Export ๋ฐ์ดํฐ ํ์ : {type(export_data)}</li> | |
| <li>Export ๋ฐ์ดํฐ ์ ํจ์ฑ: {'์ ํจ' if export_data else '๋ฌดํจ'}</li> | |
| <li>ํค์๋ ์ฌ์ถฉ๋ถ์ ์ํ: {'์๋ฃ' if export_data.get('analysis_completed') else '๋ฏธ์๋ฃ'}</li> | |
| </ul> | |
| </div> | |
| </div> | |
| """ | |
| logger.error("โ ๊ฐํ๋ ์ถ๋ ฅ ์คํจ") | |
| return error_html, gr.update(visible=False) | |
| except Exception as e: | |
| logger.error(f"โ ๊ฐํ๋ ์ถ๋ ฅ ํธ๋ค๋ฌ ์ค๋ฅ: {e}") | |
| import traceback | |
| logger.error(f"์คํ ํธ๋ ์ด์ค:\n{traceback.format_exc()}") | |
| error_html = f""" | |
| <div style="background: #f8d7da; border: 1px solid #f5c6cb; padding: 20px; border-radius: 8px; margin: 10px 0;"> | |
| <h4 style="color: #721c24; margin: 0 0 10px 0;"><i class="fas fa-exclamation-triangle"></i> ์์คํ ์ค๋ฅ</h4> | |
| <p style="color: #721c24; margin: 0;">๊ฐํ๋ ์ถ๋ ฅ ์ค ์์คํ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค:</p> | |
| <code style="display: block; margin: 10px 0; padding: 10px; background: #f8f9fa; border-radius: 3px; color: #721c24;"> | |
| {type(e).__name__}: {str(e)} | |
| </code> | |
| <div style="margin-top: 15px; padding: 10px; background: #fff3cd; border-radius: 5px;"> | |
| <p style="margin: 0; color: #856404; font-size: 14px;"> | |
| ๐ก ์ค์ ๋ถ์ ๊ฒฐ๊ณผ๊ฐ ์์ด์ผ๋ง ํ์ผ์ด ์์ฑ๋ฉ๋๋ค. | |
| </p> | |
| </div> | |
| </div> | |
| """ | |
| return error_html, gr.update(visible=False) | |
| # ===== ์ด๋ฒคํธ ์ฐ๊ฒฐ ===== | |
| collect_data_btn.click( | |
| fn=on_collect_data, | |
| inputs=[keyword_input], | |
| outputs=[keywords_result, keywords_data_state], | |
| api_name="on_collect_data" | |
| ) | |
| analyze_keyword_btn.click( | |
| fn=on_analyze_keyword, | |
| inputs=[analysis_keyword_input, keyword_input, keywords_data_state], | |
| outputs=[analysis_result, export_data_state], | |
| api_name="on_analyze_keyword" | |
| ) | |
| export_btn.click( | |
| fn=on_export_results, | |
| inputs=[export_data_state], | |
| outputs=[export_result, download_file], | |
| api_name="on_export_results" | |
| ) | |
| return interface | |
| # ===== ๋ฉ์ธ ์คํ ===== | |
| if __name__ == "__main__": | |
| # pytz ๋ชจ๋ ์ค์น ํ์ธ | |
| try: | |
| import pytz | |
| logger.info("โ pytz ๋ชจ๋ ๋ก๋ ์ฑ๊ณต - ํ๊ตญ์๊ฐ ์ง์") | |
| except ImportError: | |
| logger.info("์์คํ ์๊ฐ์ ์ฌ์ฉํฉ๋๋ค.") | |
| # ์ฑ ์คํ | |
| app = create_interface() | |
| app.launch(server_name="0.0.0.0", server_port=7860, share=True) |