fg54wre / app.py
ssboost's picture
Update app.py
37b4640 verified
raw
history blame
46.2 kB
-*- coding: utf-8 -*-
"""
AI ์ƒํ’ˆ ์†Œ์‹ฑ ๋ถ„์„ ์‹œ์Šคํ…œ v3.2 - ์ปจํŠธ๋กค ํƒ€์›Œ (๋”๋ฏธ ๋ฐ์ดํ„ฐ ์ œ๊ฑฐ ๋ฒ„์ „)
- ํ—ˆ๊น…ํŽ˜์ด์Šค ๊ทธ๋ผ๋””์˜ค ์—”๋“œํฌ์ธํŠธ ํ™œ์šฉ
- ๋ถ„์„ ๊ฒฐ๊ณผ HTML ๊ธฐ๋ฐ˜ ์™„์ „ํ•œ ํŒŒ์ผ ์ƒ์„ฑ ์‹œ์Šคํ…œ
- ๋ฐ์ดํ„ฐ ํ๋ฆ„ ๊ฐœ์„  ๋ฐ fallback ๋กœ์ง ๊ฐ•ํ™”
- ํ•œ๊ตญ์‹œ๊ฐ„ ์ฒ˜๋ฆฌ ๋ฐ ํŒŒ์ผ๋ช… ์ƒ์„ฑ ๋กœ์ง ํฌํ•จ
- ๋”๋ฏธ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ ๋กœ์ง ์™„์ „ ์ œ๊ฑฐ
"""
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)