f23f43gf / app.py
ssboost's picture
Update app.py
1b3f7ca verified
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)