|
""" |
|
AI μν μμ± λΆμ μμ€ν
v2.10 - κ°λ΅λ²μ + μΆλ ₯κΈ°λ₯ + λ©ν°μ¬μ©μ μμ |
|
- 2λ¨κ³: μμ§λ ν€μλ λͺ©λ‘ κΈ°λ₯μ μ κ±°νλ€. |
|
- 3λ¨κ³: λΆμν ν€μλ μ νμμ π μ°κ΄κ²μμ΄ λΆμμ μνμΆμΆ λ° λΆμκΈ°λ₯μμ κ±°νλ€. |
|
- 3λ¨κ³: λΆμν ν€μλ μ νμ λͺ
μΉμ "ν€μλ μ¬μΈ΅λΆμ μ
λ ₯"μ΄λΌκ³ λ°κΎΌλ€. |
|
- π κ²μλ νΈλ λ λΆμ, π― ν€μλ λΆμμ΄ λμμΌνλ€. |
|
- μΆλ ₯ κΈ°λ₯ μΆκ°: HTML νμΌ μμ± λ° ZIP λ€μ΄λ‘λ |
|
- Gemini API ν€ λλ€ μ μ© (api_utils ν΅ν© κ΄λ¦¬) |
|
- νκ΅μκ° μ μ© |
|
- λ©ν° μ¬μ©μ μμ : gr.State μ¬μ©μΌλ‘ μΈμ
λ³ λ°μ΄ν° κ΄λ¦¬ |
|
""" |
|
|
|
import gradio as gr |
|
import pandas as pd |
|
import os |
|
import logging |
|
import google.generativeai as genai |
|
from datetime import datetime, timedelta |
|
import time |
|
import re |
|
import zipfile |
|
import tempfile |
|
|
|
|
|
try: |
|
import pytz |
|
PYTZ_AVAILABLE = True |
|
except ImportError: |
|
PYTZ_AVAILABLE = False |
|
|
|
|
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') |
|
logger = logging.getLogger(__name__) |
|
|
|
|
|
import api_utils |
|
import keyword_search |
|
import keyword_analysis |
|
import trend_analysis_v2 |
|
|
|
|
|
def setup_gemini_model(): |
|
"""Gemini λͺ¨λΈ μ΄κΈ°ν - api_utilsμμ κ΄λ¦¬ (λλ€ ν€ μ μ©)""" |
|
try: |
|
|
|
model = api_utils.get_gemini_model() |
|
|
|
if model: |
|
logger.info("Gemini λͺ¨λΈ μ΄κΈ°ν μ±κ³΅ (api_utils ν΅ν© κ΄λ¦¬ - λλ€ ν€)") |
|
return model |
|
else: |
|
logger.warning("Gemini API ν€κ° μ€μ λμ§ μμμ΅λλ€.") |
|
return None |
|
|
|
except Exception as e: |
|
logger.error(f"Gemini λͺ¨λΈ μ΄κΈ°ν μ€ν¨: {e}") |
|
return None |
|
|
|
|
|
gemini_model = setup_gemini_model() |
|
|
|
|
|
def get_korean_time(): |
|
"""νκ΅μκ° λ°ν""" |
|
if PYTZ_AVAILABLE: |
|
try: |
|
korea_tz = pytz.timezone('Asia/Seoul') |
|
return datetime.now(korea_tz) |
|
except: |
|
pass |
|
|
|
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 safe_keyword_analysis(analysis_keyword): |
|
"""μλ¬ λ°©μ§λ₯Ό μν μμ ν ν€μλ λΆμ - λ©ν°μ¬μ©μ μμ """ |
|
|
|
|
|
if not analysis_keyword or not analysis_keyword.strip(): |
|
return generate_error_response("λΆμν ν€μλλ₯Ό μ
λ ₯ν΄μ£ΌμΈμ."), {} |
|
|
|
analysis_keyword = analysis_keyword.strip() |
|
|
|
try: |
|
|
|
api_keyword = keyword_analysis.normalize_keyword_for_api(analysis_keyword) |
|
search_volumes = keyword_search.fetch_all_search_volumes([api_keyword]) |
|
volume_data = search_volumes.get(api_keyword, {"PCκ²μλ": 0, "λͺ¨λ°μΌκ²μλ": 0, "μ΄κ²μλ": 0}) |
|
|
|
|
|
if volume_data['μ΄κ²μλ'] == 0: |
|
logger.warning(f"'{analysis_keyword}' ν€μλμ κ²μλμ΄ 0μ΄κ±°λ μ‘΄μ¬νμ§ μμ΅λλ€.") |
|
error_result = f""" |
|
<div style="padding: 30px; text-align: center; background: #fff3cd; border-radius: 12px; border: 1px solid #ffeaa7;"> |
|
<h3 style="color: #856404; margin-bottom: 15px;">β οΈ ν€μλ λΆμ λΆκ°</h3> |
|
<p style="color: #856404; margin-bottom: 10px;"><strong>'{analysis_keyword}'</strong> ν€μλλ κ²μλμ΄ μκ±°λ μ¬λ°λ₯΄μ§ μμ ν€μλμ
λλ€.</p> |
|
<div style="background: white; padding: 15px; border-radius: 8px; margin-top: 15px;"> |
|
<h4 style="color: #333; margin-bottom: 10px;">π‘ κΆμ₯μ¬ν</h4> |
|
<ul style="text-align: left; color: #666; padding-left: 20px;"> |
|
<li>ν€μλ μ² μλ₯Ό νμΈν΄μ£ΌμΈμ</li> |
|
<li>λ μΌλ°μ μΈ ν€μλλ₯Ό μ¬μ©ν΄λ³΄μΈμ</li> |
|
<li>ν€μλλ₯Ό λμ΄μ°κΈ°λ‘ ꡬλΆν΄λ³΄μΈμ (μ: 'μ¬μ± μ¬λ¦¬νΌ')</li> |
|
</ul> |
|
</div> |
|
</div> |
|
""" |
|
return error_result, {} |
|
|
|
logger.info(f"'{analysis_keyword}' νμ¬ κ²μλ: {volume_data['μ΄κ²μλ']:,}") |
|
|
|
|
|
monthly_data_1year = {} |
|
monthly_data_3year = {} |
|
trend_available = False |
|
|
|
try: |
|
|
|
datalab_config = api_utils.get_next_datalab_api_config() |
|
if datalab_config and not datalab_config["CLIENT_ID"].startswith("YOUR_"): |
|
logger.info("λ°μ΄ν°λ© API ν€κ° μ€μ λμ΄ μμ΄ 1λ
, 3λ
νΈλ λ λΆμμ μλν©λλ€.") |
|
|
|
|
|
|
|
trend_data_1year = trend_analysis_v2.get_naver_trend_data_v5([analysis_keyword], "1year", max_retries=3) |
|
if trend_data_1year: |
|
current_volumes = {api_keyword: volume_data} |
|
monthly_data_1year = trend_analysis_v2.calculate_monthly_volumes_v7([analysis_keyword], current_volumes, trend_data_1year, "1year") |
|
|
|
|
|
trend_data_3year = trend_analysis_v2.get_naver_trend_data_v5([analysis_keyword], "3year", max_retries=3) |
|
if trend_data_3year: |
|
current_volumes = {api_keyword: volume_data} |
|
monthly_data_3year = trend_analysis_v2.calculate_monthly_volumes_v7([analysis_keyword], current_volumes, trend_data_3year, "3year") |
|
|
|
|
|
if not monthly_data_3year and monthly_data_1year: |
|
logger.info("3λ
λ°μ΄ν°κ° μμ΄ 1λ
λ°μ΄ν°λ₯Ό κΈ°λ°μΌλ‘ 3λ
μ°¨νΈ μμ±") |
|
keyword = analysis_keyword |
|
if keyword in monthly_data_1year: |
|
data_1y = monthly_data_1year[keyword] |
|
|
|
|
|
extended_dates = [] |
|
extended_volumes = [] |
|
|
|
|
|
start_date = datetime.strptime(data_1y["dates"][0], "%Y-%m-%d") |
|
for i in range(24, 0, -1): |
|
prev_date = start_date - timedelta(days=30 * i) |
|
extended_dates.append(prev_date.strftime("%Y-%m-%d")) |
|
extended_volumes.append(0) |
|
|
|
|
|
actual_count = data_1y.get("actual_count", len(data_1y["dates"])) |
|
extended_dates.extend(data_1y["dates"][:actual_count]) |
|
extended_volumes.extend(data_1y["monthly_volumes"][:actual_count]) |
|
|
|
monthly_data_3year = { |
|
keyword: { |
|
"monthly_volumes": extended_volumes, |
|
"dates": extended_dates, |
|
"current_volume": data_1y["current_volume"], |
|
"growth_rate": trend_analysis_v2.calculate_3year_growth_rate_improved(extended_volumes), |
|
"volume_per_percent": data_1y["volume_per_percent"], |
|
"current_ratio": data_1y["current_ratio"], |
|
"actual_count": len(extended_volumes), |
|
"predicted_count": 0 |
|
} |
|
} |
|
|
|
if monthly_data_1year or monthly_data_3year: |
|
trend_available = True |
|
logger.info("νΈλ λ λΆμ μ±κ³΅") |
|
else: |
|
logger.info("νΈλ λ λ°μ΄ν° μ²λ¦¬ μ€ν¨") |
|
else: |
|
logger.info("λ°μ΄ν°λ© API ν€κ° μ€μ λμ§ μμ") |
|
except Exception as e: |
|
logger.info(f"νΈλ λ λΆμ 건λλ: {str(e)[:100]}") |
|
|
|
|
|
if trend_available and (monthly_data_1year or monthly_data_3year): |
|
try: |
|
trend_chart = trend_analysis_v2.create_trend_chart_v7(monthly_data_1year, monthly_data_3year) |
|
except Exception as e: |
|
logger.warning(f"νΈλ λ μ°¨νΈ μμ± μ€ν¨, κΈ°λ³Έ μ°¨νΈ μ¬μ©: {e}") |
|
trend_chart = trend_analysis_v2.create_enhanced_current_chart(volume_data, analysis_keyword) |
|
else: |
|
trend_chart = trend_analysis_v2.create_enhanced_current_chart(volume_data, analysis_keyword) |
|
|
|
|
|
trend_section = f""" |
|
<div style="width: 100%; margin: 30px auto; font-family: 'Pretendard', sans-serif;"> |
|
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 15px; border-radius: 10px 10px 0 0; color: white; text-align: center;"> |
|
<h3 style="margin: 0; font-size: 18px; color: white;">π κ²μλ νΈλ λ λΆμ</h3> |
|
</div> |
|
<div style="background: white; padding: 20px; border-radius: 0 0 10px 10px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);"> |
|
{trend_chart} |
|
</div> |
|
</div> |
|
""" |
|
|
|
|
|
|
|
current_gemini_model = api_utils.get_gemini_model() |
|
|
|
keyword_analysis_html = keyword_analysis.analyze_keyword_for_sourcing( |
|
analysis_keyword, volume_data, monthly_data_1year, monthly_data_3year, |
|
None, [], current_gemini_model |
|
) |
|
|
|
keyword_analysis_section = f""" |
|
<div style="width: 100%; margin: 30px auto; font-family: 'Pretendard', sans-serif;"> |
|
<div style="background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%); padding: 15px; border-radius: 10px 10px 0 0; color: white; text-align: center;"> |
|
<h3 style="margin: 0; font-size: 18px; color: white;">π― ν€μλ λΆμ</h3> |
|
</div> |
|
<div style="background: white; padding: 20px; border-radius: 0 0 10px 10px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); overflow: hidden;"> |
|
{keyword_analysis_html} |
|
</div> |
|
</div> |
|
""" |
|
|
|
|
|
warning_section = "" |
|
if not trend_available: |
|
warning_section = f""" |
|
<div style="width: 100%; margin: 20px auto; padding: 15px; background: #fff3cd; border: 1px solid #ffeaa7; border-radius: 8px; font-family: 'Pretendard', sans-serif;"> |
|
<div style="display: flex; align-items: center;"> |
|
<span style="font-size: 20px; margin-right: 10px;">β οΈ</span> |
|
<div> |
|
<strong style="color: #856404;">μΌλΆ κΈ°λ₯ μ ν</strong> |
|
<div style="font-size: 14px; color: #856404; margin-top: 5px;"> |
|
νΈλ λ λΆμμ μ νμ΄ μμ΅λλ€. νμ¬ κ²μλ λΆμκ³Ό AI μΆμ²μ μ μ μ 곡λ©λλ€.<br> |
|
<small>μμ ν μ λ°μ΄ν° κΈ°μ€μΌλ‘ λΆμνκΈ° μν΄ μ΅μ μλ£λ μκΉμ§λ§ νμλ©λλ€.</small> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
""" |
|
|
|
|
|
final_result = warning_section + trend_section + keyword_analysis_section |
|
|
|
|
|
session_export_data = { |
|
"analysis_keyword": analysis_keyword, |
|
"analysis_html": final_result |
|
} |
|
|
|
return final_result, session_export_data |
|
|
|
except Exception as e: |
|
logger.error(f"ν€μλ λΆμ μ€ μ 체 μ€λ₯: {e}") |
|
error_result = generate_error_response(f"ν€μλ λΆμ μ€ μ€λ₯κ° λ°μνμ΅λλ€: {str(e)}") |
|
return error_result, {} |
|
|
|
|
|
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") |
|
|
|
|
|
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_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)}" |
|
|
|
|
|
def create_interface(): |
|
|
|
try: |
|
with open('style.css', 'r', encoding='utf-8') as f: |
|
custom_css = f.read() |
|
|
|
with open('keyword_analysis_report.css', 'r', encoding='utf-8') as f: |
|
keyword_css = f.read() |
|
custom_css += "\n" + keyword_css |
|
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; |
|
} |
|
""" |
|
|
|
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 = safe_keyword_analysis(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: |
|
|
|
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 |
|
|
|
|
|
def check_datalab_api_config(): |
|
"""λ€μ΄λ² λ°μ΄ν°λ© API μ€μ νμΈ""" |
|
logger.info("=== λ€μ΄λ² λ°μ΄ν°λ© API μ€μ νμΈ ===") |
|
|
|
datalab_config = api_utils.get_next_datalab_api_config() |
|
|
|
if not datalab_config: |
|
logger.warning("β λ°μ΄ν°λ© API ν€κ° μ€μ λμ§ μμμ΅λλ€.") |
|
logger.info("νΈλ λ λΆμ κΈ°λ₯μ΄ λΉνμ±νλ©λλ€.") |
|
return False |
|
|
|
client_id = datalab_config["CLIENT_ID"] |
|
client_secret = datalab_config["CLIENT_SECRET"] |
|
|
|
logger.info(f"μ΄ {len(api_utils.NAVER_DATALAB_CONFIGS)}κ°μ λ°μ΄ν°λ© API μ€μ μ¬μ© μ€") |
|
logger.info(f"νμ¬ μ νλ API:") |
|
logger.info(f" CLIENT_ID: {client_id[:8]}***{client_id[-4:] if len(client_id) > 12 else '***'}") |
|
logger.info(f" CLIENT_SECRET: {client_secret[:4]}***{client_secret[-2:] if len(client_secret) > 6 else '***'}") |
|
|
|
|
|
if client_id.startswith("YOUR_"): |
|
logger.error("β CLIENT_IDκ° κΈ°λ³Έκ°μΌλ‘ μ€μ λμ΄ μμ΅λλ€!") |
|
return False |
|
|
|
if client_secret.startswith("YOUR_"): |
|
logger.error("β CLIENT_SECRETμ΄ κΈ°λ³Έκ°μΌλ‘ μ€μ λμ΄ μμ΅λλ€!") |
|
return False |
|
|
|
|
|
if len(client_id) < 10: |
|
logger.warning("β οΈ CLIENT_IDκ° μ§§μ΅λλ€. μ¬λ°λ₯Έ ν€μΈμ§ νμΈν΄μ£ΌμΈμ.") |
|
|
|
if len(client_secret) < 5: |
|
logger.warning("β οΈ CLIENT_SECRETμ΄ μ§§μ΅λλ€. μ¬λ°λ₯Έ ν€μΈμ§ νμΈν΄μ£ΌμΈμ.") |
|
|
|
logger.info("β
λ°μ΄ν°λ© API ν€ νμ κ²μ¦ μλ£") |
|
return True |
|
|
|
def check_gemini_api_config(): |
|
"""Gemini API μ€μ νμΈ - λλ€ ν€ μ μ©""" |
|
logger.info("=== Gemini API μ€μ νμΈ ===") |
|
|
|
is_valid, message = api_utils.validate_gemini_config() |
|
|
|
if is_valid: |
|
logger.info(f"β
{message}") |
|
|
|
test_key = api_utils.get_next_gemini_api_key() |
|
if test_key: |
|
logger.info(f"νμ¬ μ¬μ© μ€μΈ Gemini API ν€: {test_key[:8]}***{test_key[-4:]}") |
|
return True |
|
else: |
|
logger.warning(f"β {message}") |
|
logger.info("AI λΆμ κΈ°λ₯μ΄ μ νλ μ μμ΅λλ€.") |
|
return False |
|
|
|
|
|
if __name__ == "__main__": |
|
|
|
if PYTZ_AVAILABLE: |
|
logger.info("β
pytz λͺ¨λ λ‘λ μ±κ³΅ - νκ΅μκ° μ§μ") |
|
else: |
|
logger.warning("β οΈ pytz λͺ¨λμ΄ μ€μΉλμ§ μμ - pip install pytz μ€ν νμ") |
|
logger.info("μμ€ν
μκ°μ μ¬μ©ν©λλ€.") |
|
|
|
|
|
api_utils.initialize_api_configs() |
|
logger.info("===== μν μμ± λΆμ μμ€ν
v2.10 (κ°λ΅λ²μ + μΆλ ₯κΈ°λ₯ + λλ€ν€ + λ©ν°μ¬μ©μ μμ ) μμ =====") |
|
|
|
|
|
datalab_available = check_datalab_api_config() |
|
|
|
|
|
gemini_available = check_gemini_api_config() |
|
|
|
|
|
print("π¦ νμν ν¨ν€μ§:") |
|
print(" pip install gradio google-generativeai pandas requests xlsxwriter markdown plotly pytz") |
|
print() |
|
|
|
|
|
if not gemini_available: |
|
print("β οΈ GEMINI_API_KEY λλ GOOGLE_API_KEY νκ²½λ³μλ₯Ό μ€μ νμΈμ.") |
|
print(" export GEMINI_API_KEY='your-api-key'") |
|
print(" λλ") |
|
print(" export GOOGLE_API_KEY='your-api-key'") |
|
print() |
|
|
|
if not datalab_available: |
|
print("β οΈ λ€μ΄λ² λ°μ΄ν°λ© API νΈλ λ λΆμμ μν΄μλ:") |
|
print(" 1. λ€μ΄λ² κ°λ°μμΌν°(https://developers.naver.com)μμ μ ν리μΌμ΄μ
λ±λ‘") |
|
print(" 2. 'λ°μ΄ν°λ©(κ²μμ΄ νΈλ λ)' API μΆκ°") |
|
print(" 3. λ°κΈλ°μ CLIENT_IDμ CLIENT_SECRETμ api_utils.pyμ NAVER_DATALAB_CONFIGSμ μ€μ ") |
|
print(" 4. νμ¬λ νμ¬ κ²μλ μ λ³΄λ§ νμλ©λλ€.") |
|
print() |
|
else: |
|
print("β
λ°μ΄ν°λ© API μ€μ μλ£ - 1λ
, 3λ
νΈλ λ λΆμμ΄ κ°λ₯ν©λλ€!") |
|
print() |
|
|
|
if gemini_available: |
|
print("β
Gemini API μ€μ μλ£ - λλ€ ν€ λ‘ν
μ΄μ
μ μ©λ©λλ€!") |
|
print() |
|
|
|
print("π v2.10 κ°μ μ¬ν:") |
|
print(" β’ 2λ¨κ³: μμ§λ ν€μλ λͺ©λ‘ κΈ°λ₯ μ κ±°") |
|
print(" β’ 3λ¨κ³: μ°κ΄κ²μμ΄ λΆμμ μνμΆμΆ λ° λΆμ κΈ°λ₯ μ κ±°") |
|
print(" β’ 3λ¨κ³: 'λΆμν ν€μλ μ ν' β 'ν€μλ μ¬μΈ΅λΆμ μ
λ ₯'μΌλ‘ λͺ
μΉ λ³κ²½") |
|
print(" β’ π κ²μλ νΈλ λ λΆμκ³Ό π― ν€μλ λΆμλ§ νμ") |
|
print(" β’ β
μΆλ ₯ κΈ°λ₯ μΆκ°: HTML νμΌ μμ± λ° ZIP λ€μ΄λ‘λ") |
|
print(" β’ β
Gemini API ν€ λλ€ λ‘ν
μ΄μ
μ μ©") |
|
print(" β’ β
λ€μ΄λ² λ°μ΄ν°λ© API ν€ λλ€ λ‘ν
μ΄μ
μ μ©") |
|
print(" β’ β
νκ΅μκ° κΈ°μ€ νμΌλͺ
μμ±") |
|
print(" β’ β
λ©ν° μ¬μ©μ μμ : gr.Stateλ‘ μΈμ
λ³ λ°μ΄ν° κ΄λ¦¬") |
|
print(" β’ λΆνμν λͺ¨λ μν¬νΈ μ κ±°λ‘ μμ μ± ν₯μ") |
|
print() |
|
|
|
|
|
app = create_interface() |
|
app.launch(server_name="0.0.0.0", server_port=7860, share=True) |