geo5g4gfj / app.py
ssboost's picture
Upload 11 files
111636c verified
"""
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
# ===== Gemini API μ„€μ • =====
def setup_gemini_model():
"""Gemini λͺ¨λΈ μ΄ˆκΈ°ν™” - api_utilsμ—μ„œ 관리 (랜덀 ν‚€ 적용)"""
try:
# api_utilsμ—μ„œ Gemini λͺ¨λΈ κ°€μ Έμ˜€κΈ° (랜덀 ν‚€ 적용)
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 λͺ¨λΈ μ΄ˆκΈ°ν™”
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
# pytzκ°€ μ—†κ±°λ‚˜ 였λ₯˜ μ‹œ μ‹œμŠ€ν…œ μ‹œκ°„ μ‚¬μš©
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})
# κ²€μƒ‰λŸ‰μ΄ 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:
# λ°μ΄ν„°λž© API ν‚€ 확인 (랜덀 적용)
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λ…„ νŠΈλ Œλ“œ 뢄석을 μ‹œλ„ν•©λ‹ˆλ‹€.")
# μ΅œμ ν™”λœ API ν•¨μˆ˜ μ‚¬μš©
# 1λ…„ νŠΈλ Œλ“œ 데이터
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")
# 3λ…„ νŠΈλ Œλ“œ 데이터
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")
# 3λ…„ 데이터가 μ—†λŠ” 경우 1λ…„ λ°μ΄ν„°λ‘œ ν™•μž₯
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]
# 3λ…„ λΆ„λŸ‰μ˜ λ‚ μ§œ 생성 (24κ°œμ›” μΆ”κ°€)
extended_dates = []
extended_volumes = []
# κΈ°μ‘΄ 1λ…„ 데이터 이전에 24κ°œμ›” μΆ”κ°€ (λͺ¨λ‘ 0으둜)
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)
# κΈ°μ‘΄ 1λ…„ 데이터 μΆ”κ°€ (μ˜ˆμƒ 데이터 μ œμ™Έ)
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>
"""
# === 🎯 ν‚€μ›Œλ“œ 뢄석 μ„Ήμ…˜ (AI 뢄석) ===
# api_utilsμ—μ„œ Gemini λͺ¨λΈ κ°€μ Έμ˜€κΈ° (랜덀 ν‚€ 적용)
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")
# μ™„μ „ν•œ 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)}"
# ===== κ·ΈλΌλ””μ˜€ μΈν„°νŽ˜μ΄μŠ€ =====
def create_interface():
# CSS 파일 λ‘œλ“œ
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
# ===== API μ„€μ • 확인 ν•¨μˆ˜ =====
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__":
# pytz λͺ¨λ“ˆ μ„€μΉ˜ 확인
if PYTZ_AVAILABLE:
logger.info("βœ… pytz λͺ¨λ“ˆ λ‘œλ“œ 성곡 - ν•œκ΅­μ‹œκ°„ 지원")
else:
logger.warning("⚠️ pytz λͺ¨λ“ˆμ΄ μ„€μΉ˜λ˜μ§€ μ•ŠμŒ - pip install pytz μ‹€ν–‰ ν•„μš”")
logger.info("μ‹œμŠ€ν…œ μ‹œκ°„μ„ μ‚¬μš©ν•©λ‹ˆλ‹€.")
# API μ„€μ • μ΄ˆκΈ°ν™”
api_utils.initialize_api_configs()
logger.info("===== μƒν’ˆ μ†Œμ‹± 뢄석 μ‹œμŠ€ν…œ v2.10 (κ°„λž΅λ²„μ „ + 좜λ ₯κΈ°λŠ₯ + λžœλ€ν‚€ + λ©€ν‹°μ‚¬μš©μž μ•ˆμ „) μ‹œμž‘ =====")
# 넀이버 λ°μ΄ν„°λž© API μ„€μ • 확인
datalab_available = check_datalab_api_config()
# Gemini API μ„€μ • 확인 (랜덀 ν‚€)
gemini_available = check_gemini_api_config()
# ν•„μš”ν•œ νŒ¨ν‚€μ§€ μ•ˆλ‚΄
print("πŸ“¦ ν•„μš”ν•œ νŒ¨ν‚€μ§€:")
print(" pip install gradio google-generativeai pandas requests xlsxwriter markdown plotly pytz")
print()
# API ν‚€ μ„€μ • μ•ˆλ‚΄
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)