p763nx9tf / app.py
ssboost's picture
Create app.py
a502bd6 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 μ—”λ“œν¬μΈνŠΈκ°€ μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€.")
raise ValueError("API μ—”λ“œν¬μΈνŠΈκ°€ μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€.")
client = Client(api_endpoint)
logger.info("원격 API ν΄λΌμ΄μ–ΈνŠΈ μ΄ˆκΈ°ν™” 성곡")
return client
except Exception as e:
logger.error(f"API ν΄λΌμ΄μ–ΈνŠΈ μ΄ˆκΈ°ν™” μ‹€νŒ¨: {e}")
return None
# ===== ν•œκ΅­μ‹œκ°„ κ΄€λ ¨ ν•¨μˆ˜ =====
def get_korean_time():
"""ν•œκ΅­μ‹œκ°„ λ°˜ν™˜"""
try:
korea_tz = pytz.timezone('Asia/Seoul')
return datetime.now(korea_tz)
except:
return datetime.now()
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_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>
'''
# ===== 파일 좜λ ₯ ν•¨μˆ˜λ“€ =====
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_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 μƒν’ˆ μ†Œμ‹± 뢄석 μ‹œμŠ€ν…œ v2.10</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(html_path, filename_base):
"""μ••μΆ• 파일 생성 (HTML만)"""
try:
zip_filename = f"{filename_base}.zip"
zip_path = os.path.join(tempfile.gettempdir(), zip_filename)
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
if html_path and os.path.exists(html_path):
zipf.write(html_path, f"{filename_base}.html")
logger.info(f"HTML 파일 μ••μΆ• μΆ”κ°€: {filename_base}.html")
logger.info(f"μ••μΆ• 파일 생성 μ™„λ£Œ: {zip_path}")
return zip_path
except Exception as e:
logger.error(f"μ••μΆ• 파일 생성 였λ₯˜: {e}")
return None
def export_analysis_results(export_data):
"""뢄석 κ²°κ³Ό 좜λ ₯ 메인 ν•¨μˆ˜ - μ„Έμ…˜λ³„ 데이터 처리"""
try:
# 좜λ ₯ν•  데이터 확인
if not export_data or not isinstance(export_data, dict):
return None, "뢄석 데이터가 μ—†μŠ΅λ‹ˆλ‹€. λ¨Όμ € ν‚€μ›Œλ“œ 심측뢄석을 μ‹€ν–‰ν•΄μ£Όμ„Έμš”."
analysis_keyword = export_data.get("analysis_keyword", "")
analysis_html = export_data.get("analysis_html", "")
if not analysis_keyword:
return None, "뢄석할 ν‚€μ›Œλ“œκ°€ μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€. λ¨Όμ € ν‚€μ›Œλ“œ 뢄석을 μ‹€ν–‰ν•΄μ£Όμ„Έμš”."
if not analysis_html:
return None, "뢄석 κ²°κ³Όκ°€ μ—†μŠ΅λ‹ˆλ‹€. λ¨Όμ € ν‚€μ›Œλ“œ 심측뢄석을 μ‹€ν–‰ν•΄μ£Όμ„Έμš”."
# 파일λͺ… 생성 (ν•œκ΅­μ‹œκ°„ 적용)
filename_base = create_timestamp_filename(analysis_keyword)
logger.info(f"좜λ ₯ 파일λͺ…: {filename_base}")
# HTML 파일 생성
html_path = export_to_html(analysis_html, filename_base)
# μ••μΆ• 파일 생성
if html_path:
zip_path = create_zip_file(html_path, filename_base)
if zip_path:
return zip_path, f"βœ… 뢄석 κ²°κ³Όκ°€ μ„±κ³΅μ μœΌλ‘œ 좜λ ₯λ˜μ—ˆμŠ΅λ‹ˆλ‹€!\n파일λͺ…: {filename_base}.zip"
else:
return None, "μ••μΆ• 파일 생성에 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€."
else:
return None, "좜λ ₯ν•  파일이 μ—†μŠ΅λ‹ˆλ‹€."
except Exception as e:
logger.error(f"뢄석 κ²°κ³Ό 좜λ ₯ 였λ₯˜: {e}")
return None, f"좜λ ₯ 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€: {str(e)}"
# ===== 원격 API 호좜 ν•¨μˆ˜λ“€ =====
def call_analyze_keyword_api(analysis_keyword):
"""ν‚€μ›Œλ“œ 심측뢄석 API 호좜"""
try:
client = get_api_client()
if not client:
return generate_error_response("API ν΄λΌμ΄μ–ΈνŠΈλ₯Ό μ΄ˆκΈ°ν™”ν•  수 μ—†μŠ΅λ‹ˆλ‹€."), {}
logger.info("원격 API 호좜: ν‚€μ›Œλ“œ 심측뢄석")
result = client.predict(
analysis_keyword=analysis_keyword,
api_name="/on_analyze_keyword"
)
logger.info(f"ν‚€μ›Œλ“œ 뢄석 API κ²°κ³Ό νƒ€μž…: {type(result)}")
# 뢄석 결과둜 export 데이터 생성
if isinstance(result, str) and len(result) > 100:
export_data = {
"analysis_keyword": analysis_keyword,
"analysis_html": result,
"analysis_completed": True,
"created_at": get_korean_time().isoformat()
}
return result, export_data
else:
return str(result), {}
except Exception as e:
logger.error(f"ν‚€μ›Œλ“œ 심측뢄석 API 호좜 였λ₯˜: {e}")
return generate_error_response(f"원격 μ„œλ²„ μ—°κ²° μ‹€νŒ¨: {str(e)}"), {}
def call_export_results_api(export_data):
"""뢄석 κ²°κ³Ό 좜λ ₯ API 호좜"""
try:
client = get_api_client()
if not client:
return None, "API ν΄λΌμ΄μ–ΈνŠΈλ₯Ό μ΄ˆκΈ°ν™”ν•  수 μ—†μŠ΅λ‹ˆλ‹€."
logger.info("원격 API 호좜: 뢄석 κ²°κ³Ό 좜λ ₯")
result = client.predict(
api_name="/on_export_results"
)
logger.info(f"좜λ ₯ API κ²°κ³Ό νƒ€μž…: {type(result)}")
# κ²°κ³Όκ°€ νŠœν”ŒμΈ 경우 첫 번째 μš”μ†ŒλŠ” λ©”μ‹œμ§€, 두 λ²ˆμ§ΈλŠ” 파일
if isinstance(result, tuple) and len(result) == 2:
message, file_path = result
if file_path:
return file_path, message
else:
return None, message
else:
return None, str(result)
except Exception as e:
logger.error(f"뢄석 κ²°κ³Ό 좜λ ₯ API 호좜 였λ₯˜: {e}")
return None, f"원격 μ„œλ²„ μ—°κ²° μ‹€νŒ¨: {str(e)}"
# ===== κ·ΈλΌλ””μ˜€ μΈν„°νŽ˜μ΄μŠ€ =====
def create_interface():
# CSS 파일 λ‘œλ“œ
try:
with open('style.css', 'r', encoding='utf-8') as f:
custom_css = f.read()
except:
custom_css = """
:root { --primary-color: #FB7F0D; --secondary-color: #ff9a8b; }
.custom-button {
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)) !important;
color: white !important; border-radius: 30px !important; height: 45px !important;
font-size: 16px !important; font-weight: bold !important; width: 100% !important;
}
.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;
}
.custom-frame {
background-color: white !important;
border: 1px solid #e5e5e5 !important;
border-radius: 18px;
padding: 20px;
margin: 10px 0;
box-shadow: 0 8px 30px rgba(251, 127, 13, 0.08) !important;
}
.section-title {
display: flex;
align-items: center;
font-size: 20px;
font-weight: 700;
color: #334155 !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);
}
"""
with gr.Blocks(
css=custom_css,
title="πŸ›’ AI μƒν’ˆ μ†Œμ‹± 뢄석기 v2.10",
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">
""")
# μ„Έμ…˜λ³„ μƒνƒœ 관리 (λ©€ν‹° μ‚¬μš©μž μ•ˆμ „)
export_data_state = gr.State({})
# === ν‚€μ›Œλ“œ 심측뢄석 μž…λ ₯ ===
with gr.Column(elem_classes="custom-frame fade-in"):
gr.HTML('<div class="section-title"><i class="fas fa-bullseye"></i> ν‚€μ›Œλ“œ 심측뢄석 μž…λ ₯</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>')
export_btn = gr.Button("πŸ“Š 뢄석결과 좜λ ₯ν•˜κΈ°", elem_classes="export-button", size="lg")
export_result = gr.HTML()
download_file = gr.File(label="λ‹€μš΄λ‘œλ“œ", visible=False)
# ===== 이벀트 ν•Έλ“€λŸ¬ =====
def on_analyze_keyword(analysis_keyword):
if not analysis_keyword.strip():
return "뢄석할 ν‚€μ›Œλ“œλ₯Ό μž…λ ₯ν•΄μ£Όμ„Έμš”.", {}
# λ‘œλ”© μƒνƒœ ν‘œμ‹œ
yield create_loading_animation(), {}
# μ‹€μ œ ν‚€μ›Œλ“œ 뢄석 μ‹€ν–‰
keyword_result, session_export_data = call_analyze_keyword_api(analysis_keyword)
# πŸ“ˆ κ²€μƒ‰λŸ‰ νŠΈλ Œλ“œ 뢄석과 🎯 ν‚€μ›Œλ“œ 뢄석 ν‘œμ‹œ
yield keyword_result, session_export_data
def on_export_results(export_data):
"""뢄석 κ²°κ³Ό 좜λ ₯ ν•Έλ“€λŸ¬ - μ„Έμ…˜λ³„ 데이터 처리"""
try:
# 둜컬 좜λ ₯ μ‹œλ„
zip_path, message = export_analysis_results(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>
β€’ 🌐 HTML 파일: ν‚€μ›Œλ“œ 심측뢄석 κ²°κ³Ό (κ·Έλž˜ν”„ 포함)<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:
# 둜컬 좜λ ₯ μ‹€νŒ¨μ‹œ 원격 API μ‹œλ„
try:
remote_file, remote_message = call_export_results_api(export_data)
if remote_file:
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;">
{remote_message}<br>
<i class="fas fa-download"></i> μ•„λž˜ λ‹€μš΄λ‘œλ“œ λ²„νŠΌμ„ ν΄λ¦­ν•˜μ—¬ νŒŒμΌμ„ μ €μž₯ν•˜μ„Έμš”.
</p>
</div>
"""
return success_html, gr.update(value=remote_file, 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}<br>원격 좜λ ₯도 μ‹€νŒ¨: {remote_message}</p>
</div>
"""
return error_html, gr.update(visible=False)
except:
# 원격 API도 μ‹€νŒ¨
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>
"""
return error_html, gr.update(visible=False)
except Exception as e:
logger.error(f"좜λ ₯ ν•Έλ“€λŸ¬ 였λ₯˜: {e}")
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;">좜λ ₯ 쀑 μ‹œμŠ€ν…œ 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€: {str(e)}</p>
</div>
"""
return error_html, gr.update(visible=False)
# ===== 이벀트 μ—°κ²° =====
analyze_keyword_btn.click(
fn=on_analyze_keyword,
inputs=[analysis_keyword_input],
outputs=[analysis_result, export_data_state]
)
export_btn.click(
fn=on_export_results,
inputs=[export_data_state],
outputs=[export_result, download_file]
)
return interface
# ===== 메인 μ‹€ν–‰ =====
if __name__ == "__main__":
# pytz λͺ¨λ“ˆ μ„€μΉ˜ 확인
try:
import pytz
logger.info("βœ… pytz λͺ¨λ“ˆ λ‘œλ“œ 성곡 - ν•œκ΅­μ‹œκ°„ 지원")
except ImportError:
logger.warning("⚠️ pytz λͺ¨λ“ˆμ΄ μ„€μΉ˜λ˜μ§€ μ•ŠμŒ - pip install pytz μ‹€ν–‰ ν•„μš”")
logger.info("μ‹œμŠ€ν…œ μ‹œκ°„μ„ μ‚¬μš©ν•©λ‹ˆλ‹€.")
logger.info("===== μƒν’ˆ μ†Œμ‹± 뢄석 μ‹œμŠ€ν…œ v2.10 (컨트둀 νƒ€μ›Œ 버전) μ‹œμž‘ =====")
# ν•„μš”ν•œ νŒ¨ν‚€μ§€ μ•ˆλ‚΄
print("πŸ“¦ ν•„μš”ν•œ νŒ¨ν‚€μ§€:")
print(" pip install gradio gradio_client pandas pytz")
print()
# API μ—”λ“œν¬μΈνŠΈ μ„€μ • μ•ˆλ‚΄
api_endpoint = os.getenv('API_ENDPOINT')
if not api_endpoint:
print("⚠️ API_ENDPOINT ν™˜κ²½λ³€μˆ˜λ₯Ό μ„€μ •ν•˜μ„Έμš”.")
print(" export API_ENDPOINT='your-endpoint-url'")
print()
else:
print("βœ… API μ—”λ“œν¬μΈνŠΈ μ„€μ • μ™„λ£Œ!")
print()
print("πŸš€ v2.10 컨트둀 νƒ€μ›Œ 버전 νŠΉμ§•:")
print(" β€’ ν—ˆκΉ…νŽ˜μ΄μŠ€ κ·ΈλΌλ””μ˜€ μ—”λ“œν¬μΈνŠΈ ν™œμš©")
print(" β€’ μ™„μ „νžˆ λ™μΌν•œ UI와 κΈ°λŠ₯ κ΅¬ν˜„")
print(" β€’ πŸ“ˆ κ²€μƒ‰λŸ‰ νŠΈλ Œλ“œ 뢄석과 🎯 ν‚€μ›Œλ“œ 뢄석 ν‘œμ‹œ")
print(" β€’ βœ… 좜λ ₯ κΈ°λŠ₯: HTML 파일 생성 및 ZIP λ‹€μš΄λ‘œλ“œ")
print(" β€’ βœ… ν•œκ΅­μ‹œκ°„ κΈ°μ€€ 파일λͺ… 생성")
print(" β€’ βœ… λ©€ν‹° μ‚¬μš©μž μ•ˆμ „: gr.State둜 μ„Έμ…˜λ³„ 데이터 관리")
print(" β€’ πŸ”’ ν΄λΌμ΄μ–ΈνŠΈ 정보 ν™˜κ²½λ³€μˆ˜λ‘œ μ™„μ „ μˆ¨κΉ€")
print(" β€’ 원격 μ„œλ²„μ™€ 둜컬 처리 ν•˜μ΄λΈŒλ¦¬λ“œ 방식")
print()
# μ•± μ‹€ν–‰
app = create_interface()
app.launch(server_name="0.0.0.0", server_port=7860, share=True)