F23ASFGGD1 / app.py
ssboost's picture
Update app.py
9889788 verified
import gradio as gr
import pandas as pd
import os
import time
import threading
import tempfile
import logging
import random
import uuid
import shutil
import glob
from datetime import datetime
import sys
import types
# λ‘œκΉ… μ„€μ •
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.StreamHandler(),
logging.FileHandler('main_keyword_app.log', mode='a')
]
)
logger = logging.getLogger(__name__)
# ν™˜κ²½λ³€μˆ˜μ—μ„œ λͺ¨λ“ˆ μ½”λ“œ λ‘œλ“œ 및 동적 생성
def load_module_from_env(module_name, env_var_name):
"""ν™˜κ²½λ³€μˆ˜μ—μ„œ λͺ¨λ“ˆ μ½”λ“œλ₯Ό λ‘œλ“œν•˜μ—¬ λ™μ μœΌλ‘œ λͺ¨λ“ˆ 생성"""
try:
module_code = os.getenv(env_var_name)
if not module_code:
raise ValueError(f"ν™˜κ²½λ³€μˆ˜ {env_var_name}κ°€ μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€.")
# μƒˆ λͺ¨λ“ˆ 생성
module = types.ModuleType(module_name)
# λͺ¨λ“ˆμ— ν•„μš”ν•œ κΈ°λ³Έ μž„ν¬νŠΈλ“€ μΆ”κ°€
module.__dict__.update({
'os': __import__('os'),
'time': __import__('time'),
'logging': __import__('logging'),
'pandas': __import__('pandas'),
'requests': __import__('requests'),
'tempfile': __import__('tempfile'),
'threading': __import__('threading'),
're': __import__('re'),
'random': __import__('random'),
'uuid': __import__('uuid'),
'shutil': __import__('shutil'),
'glob': __import__('glob'),
'datetime': __import__('datetime'),
'types': __import__('types'),
'collections': __import__('collections'),
'Counter': __import__('collections').Counter,
'defaultdict': __import__('collections').defaultdict,
'hmac': __import__('hmac'),
'hashlib': __import__('hashlib'),
'base64': __import__('base64'),
})
# μ½”λ“œ μ‹€ν–‰
exec(module_code, module.__dict__)
# μ‹œμŠ€ν…œ λͺ¨λ“ˆμ— 등둝
sys.modules[module_name] = module
logger.info(f"βœ… λͺ¨λ“ˆ {module_name} λ‘œλ“œ μ™„λ£Œ")
return module
except Exception as e:
logger.error(f"❌ λͺ¨λ“ˆ {module_name} λ‘œλ“œ μ‹€νŒ¨: {e}")
raise
# ν•„μš”ν•œ λͺ¨λ“ˆλ“€μ„ ν™˜κ²½λ³€μˆ˜μ—μ„œ λ‘œλ“œ
logger.info("πŸ”„ λͺ¨λ“ˆ λ‘œλ“œ μ‹œμž‘...")
try:
# 1. api_utils λͺ¨λ“ˆ λ‘œλ“œ
api_utils = load_module_from_env('api_utils', 'API_UTILS_CODE')
# 2. text_utils λͺ¨λ“ˆ λ‘œλ“œ (λ‹€λ₯Έ λͺ¨λ“ˆλ“€μ΄ μ˜μ‘΄ν•˜λ―€λ‘œ λ¨Όμ € λ‘œλ“œ)
text_utils = load_module_from_env('text_utils', 'TEXT_UTILS_CODE')
# 3. keyword_search λͺ¨λ“ˆ λ‘œλ“œ
keyword_search = load_module_from_env('keyword_search', 'KEYWORD_SEARCH_CODE')
# 4. product_search λͺ¨λ“ˆ λ‘œλ“œ (text_utils, keyword_search 의쑴)
product_search_module = load_module_from_env('product_search', 'PRODUCT_SEARCH_CODE')
# product_search λͺ¨λ“ˆμ— μ˜μ‘΄μ„± μ£Όμž…
product_search_module.api_utils = api_utils
product_search_module.text_utils = text_utils
product_search = product_search_module
# 5. keyword_processor λͺ¨λ“ˆ λ‘œλ“œ
keyword_processor_module = load_module_from_env('keyword_processor', 'KEYWORD_PROCESSOR_CODE')
# keyword_processor λͺ¨λ“ˆμ— μ˜μ‘΄μ„± μ£Όμž…
keyword_processor_module.text_utils = text_utils
keyword_processor_module.keyword_search = keyword_search
keyword_processor_module.product_search = product_search
keyword_processor = keyword_processor_module
# 6. export_utils λͺ¨λ“ˆ λ‘œλ“œ
export_utils = load_module_from_env('export_utils', 'EXPORT_UTILS_CODE')
# 7. category_analysis λͺ¨λ“ˆ λ‘œλ“œ (λͺ¨λ“  λͺ¨λ“ˆ 의쑴)
category_analysis_module = load_module_from_env('category_analysis', 'CATEGORY_ANALYSIS_CODE')
# category_analysis λͺ¨λ“ˆμ— μ˜μ‘΄μ„± μ£Όμž…
category_analysis_module.text_utils = text_utils
category_analysis_module.product_search = product_search
category_analysis_module.keyword_search = keyword_search
category_analysis = category_analysis_module
logger.info("βœ… λͺ¨λ“  λͺ¨λ“ˆ λ‘œλ“œ μ™„λ£Œ")
except Exception as e:
logger.error(f"❌ λͺ¨λ“ˆ λ‘œλ“œ 쀑 치λͺ…적 였λ₯˜: {e}")
logger.error("ν•„μš”ν•œ ν™˜κ²½λ³€μˆ˜λ“€μ΄ μ„€μ •λ˜μ—ˆλŠ”μ§€ ν™•μΈν•˜μ„Έμš”:")
logger.error("- API_UTILS_CODE")
logger.error("- TEXT_UTILS_CODE")
logger.error("- KEYWORD_SEARCH_CODE")
logger.error("- PRODUCT_SEARCH_CODE")
logger.error("- KEYWORD_PROCESSOR_CODE")
logger.error("- EXPORT_UTILS_CODE")
logger.error("- CATEGORY_ANALYSIS_CODE")
raise
# μ„Έμ…˜λ³„ μž„μ‹œ 파일 관리λ₯Ό μœ„ν•œ λ”•μ…”λ„ˆλ¦¬
session_temp_files = {}
session_data = {}
def cleanup_huggingface_temp_folders():
"""ν—ˆκΉ…νŽ˜μ΄μŠ€ μž„μ‹œ 폴더 초기 정리"""
try:
# 일반적인 μž„μ‹œ 디렉토리듀
temp_dirs = [
tempfile.gettempdir(),
"/tmp",
"/var/tmp",
os.path.join(os.getcwd(), "temp"),
os.path.join(os.getcwd(), "tmp"),
"/gradio_cached_examples",
"/flagged"
]
cleanup_count = 0
for temp_dir in temp_dirs:
if os.path.exists(temp_dir):
try:
# κΈ°μ‘΄ μ„Έμ…˜ νŒŒμΌλ“€ 정리
session_files = glob.glob(os.path.join(temp_dir, "session_*.xlsx"))
session_files.extend(glob.glob(os.path.join(temp_dir, "session_*.csv")))
session_files.extend(glob.glob(os.path.join(temp_dir, "*keyword*.xlsx")))
session_files.extend(glob.glob(os.path.join(temp_dir, "*keyword*.csv")))
session_files.extend(glob.glob(os.path.join(temp_dir, "tmp*.xlsx")))
session_files.extend(glob.glob(os.path.join(temp_dir, "tmp*.csv")))
for file_path in session_files:
try:
# 파일이 1μ‹œκ°„ 이상 였래된 경우만 μ‚­μ œ
if os.path.getmtime(file_path) < time.time() - 3600:
os.remove(file_path)
cleanup_count += 1
logger.info(f"초기 정리: 였래된 μž„μ‹œ 파일 μ‚­μ œ - {file_path}")
except Exception as e:
logger.warning(f"파일 μ‚­μ œ μ‹€νŒ¨ (λ¬΄μ‹œλ¨): {file_path} - {e}")
except Exception as e:
logger.warning(f"μž„μ‹œ 디렉토리 정리 μ‹€νŒ¨ (λ¬΄μ‹œλ¨): {temp_dir} - {e}")
logger.info(f"βœ… ν—ˆκΉ…νŽ˜μ΄μŠ€ μž„μ‹œ 폴더 초기 정리 μ™„λ£Œ - {cleanup_count}개 파일 μ‚­μ œ")
# Gradio μΊμ‹œ 폴더도 정리
try:
gradio_temp_dir = os.path.join(os.getcwd(), "gradio_cached_examples")
if os.path.exists(gradio_temp_dir):
shutil.rmtree(gradio_temp_dir, ignore_errors=True)
logger.info("Gradio μΊμ‹œ 폴더 정리 μ™„λ£Œ")
except Exception as e:
logger.warning(f"Gradio μΊμ‹œ 폴더 정리 μ‹€νŒ¨ (λ¬΄μ‹œλ¨): {e}")
except Exception as e:
logger.error(f"초기 μž„μ‹œ 폴더 정리 쀑 였λ₯˜ (계속 μ§„ν–‰): {e}")
def setup_clean_temp_environment():
"""κΉ¨λ—ν•œ μž„μ‹œ ν™˜κ²½ μ„€μ •"""
try:
# 1. κΈ°μ‘΄ μž„μ‹œ νŒŒμΌλ“€ 정리
cleanup_huggingface_temp_folders()
# 2. μ• ν”Œλ¦¬μΌ€μ΄μ…˜ μ „μš© μž„μ‹œ 디렉토리 생성
app_temp_dir = os.path.join(tempfile.gettempdir(), "keyword_app")
if os.path.exists(app_temp_dir):
shutil.rmtree(app_temp_dir, ignore_errors=True)
os.makedirs(app_temp_dir, exist_ok=True)
# 3. ν™˜κ²½ λ³€μˆ˜ μ„€μ • (μž„μ‹œ 디렉토리 μ§€μ •)
os.environ['KEYWORD_APP_TEMP'] = app_temp_dir
logger.info(f"βœ… μ• ν”Œλ¦¬μΌ€μ΄μ…˜ μ „μš© μž„μ‹œ 디렉토리 μ„€μ •: {app_temp_dir}")
return app_temp_dir
except Exception as e:
logger.error(f"μž„μ‹œ ν™˜κ²½ μ„€μ • μ‹€νŒ¨: {e}")
return tempfile.gettempdir()
def get_app_temp_dir():
"""μ• ν”Œλ¦¬μΌ€μ΄μ…˜ μ „μš© μž„μ‹œ 디렉토리 λ°˜ν™˜"""
return os.environ.get('KEYWORD_APP_TEMP', tempfile.gettempdir())
def get_session_id():
"""μ„Έμ…˜ ID 생성"""
return str(uuid.uuid4())
def cleanup_session_files(session_id, delay=300):
"""μ„Έμ…˜λ³„ μž„μ‹œ 파일 정리 ν•¨μˆ˜"""
def cleanup():
time.sleep(delay)
if session_id in session_temp_files:
files_to_remove = session_temp_files[session_id].copy()
del session_temp_files[session_id]
for file_path in files_to_remove:
try:
if os.path.exists(file_path):
os.remove(file_path)
logger.info(f"μ„Έμ…˜ {session_id[:8]}... μž„μ‹œ 파일 μ‚­μ œ: {file_path}")
except Exception as e:
logger.error(f"μ„Έμ…˜ {session_id[:8]}... 파일 μ‚­μ œ 였λ₯˜: {e}")
threading.Thread(target=cleanup, daemon=True).start()
def register_session_file(session_id, file_path):
"""μ„Έμ…˜λ³„ 파일 등둝"""
if session_id not in session_temp_files:
session_temp_files[session_id] = []
session_temp_files[session_id].append(file_path)
def cleanup_old_sessions():
"""였래된 μ„Έμ…˜ 데이터 정리"""
current_time = time.time()
sessions_to_remove = []
for session_id, data in session_data.items():
if current_time - data.get('last_activity', 0) > 3600: # 1μ‹œκ°„ 초과
sessions_to_remove.append(session_id)
for session_id in sessions_to_remove:
# 파일 정리
if session_id in session_temp_files:
for file_path in session_temp_files[session_id]:
try:
if os.path.exists(file_path):
os.remove(file_path)
logger.info(f"였래된 μ„Έμ…˜ {session_id[:8]}... 파일 μ‚­μ œ: {file_path}")
except Exception as e:
logger.error(f"였래된 μ„Έμ…˜ 파일 μ‚­μ œ 였λ₯˜: {e}")
del session_temp_files[session_id]
# μ„Έμ…˜ 데이터 정리
if session_id in session_data:
del session_data[session_id]
logger.info(f"였래된 μ„Έμ…˜ 데이터 μ‚­μ œ: {session_id[:8]}...")
def update_session_activity(session_id):
"""μ„Έμ…˜ ν™œλ™ μ‹œκ°„ μ—…λ°μ΄νŠΈ"""
if session_id not in session_data:
session_data[session_id] = {}
session_data[session_id]['last_activity'] = time.time()
def create_session_temp_file(session_id, suffix='.xlsx'):
"""μ„Έμ…˜λ³„ μž„μ‹œ 파일 생성 (μ „μš© 디렉토리 μ‚¬μš©)"""
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
random_suffix = str(random.randint(1000, 9999))
# μ• ν”Œλ¦¬μΌ€μ΄μ…˜ μ „μš© μž„μ‹œ 디렉토리 μ‚¬μš©
temp_dir = get_app_temp_dir()
filename = f"session_{session_id[:8]}_{timestamp}_{random_suffix}{suffix}"
temp_file_path = os.path.join(temp_dir, filename)
# 빈 파일 생성
with open(temp_file_path, 'w') as f:
pass
register_session_file(session_id, temp_file_path)
return temp_file_path
def wrapper_modified(keyword, korean_only, apply_main_keyword_option, exclude_zero_volume, session_id):
"""ν‚€μ›Œλ“œ 검색 및 처리 래퍼 ν•¨μˆ˜ (μ„Έμ…˜ ID μΆ”κ°€)"""
update_session_activity(session_id)
# ν˜„μž¬ ν‚€μ›Œλ“œ μ‚¬μš© (μ„Έμ…˜λ³„λ‘œ 관리)
current_keyword = keyword
# ν‚€μ›Œλ“œκ°€ λΉ„μ–΄μžˆλŠ” 경우 처리
if not keyword:
return (gr.update(value=""), gr.update(choices=["전체 보기"]), gr.update(choices=["전체"]),
None, gr.update(choices=["전체 보기"], value="전체 보기"), None,
gr.update(visible=False), gr.update(visible=False), current_keyword)
# 넀이버 μ‡Όν•‘ API 검색 μˆ˜ν–‰
search_results = product_search.fetch_naver_shopping_data(keyword, korean_only, apply_main_keyword_option == "λ©”μΈν‚€μ›Œλ“œ 적용")
# 검색 κ²°κ³Όκ°€ μ—†λŠ” 경우
if not search_results.get("product_list"):
return (gr.update(value="<p>검색 κ²°κ³Όκ°€ μ—†μŠ΅λ‹ˆλ‹€. λ‹€λ₯Έ ν‚€μ›Œλ“œλ‘œ μ‹œλ„ν•΄λ³΄μ„Έμš”.</p>"),
gr.update(choices=["전체 보기"]), gr.update(choices=["전체"]),
None, gr.update(choices=["전체 보기"], value="전체 보기"), None,
gr.update(visible=False), gr.update(visible=False), current_keyword)
# 검색 κ²°κ³Ό 처리 - ν‚€μ›Œλ“œ 전달 및 κ²€μƒ‰λŸ‰ 0 ν‚€μ›Œλ“œ μ œμ™Έ μ˜΅μ…˜ 전달
result = keyword_processor.process_search_results(search_results, current_keyword, exclude_zero_volume)
df_products = result["products_df"]
df_keywords = result["keywords_df"]
category_list = result["categories"]
if df_keywords.empty:
return (gr.update(value="<p>μΆ”μΆœλœ ν‚€μ›Œλ“œκ°€ μ—†μŠ΅λ‹ˆλ‹€. λ‹€λ₯Έ μ˜΅μ…˜μœΌλ‘œ μ‹œλ„ν•΄λ³΄μ„Έμš”.</p>"),
gr.update(choices=["전체 보기"]), gr.update(choices=["전체"]),
df_keywords, gr.update(choices=["전체 보기"], value="전체 보기"), None,
gr.update(visible=False), gr.update(visible=False), current_keyword)
# HTML ν…Œμ΄λΈ” 생성
html = export_utils.create_table_without_checkboxes(df_keywords)
# 필터링을 μœ„ν•œ 고유 κ°’ 리슀트 생성
volume_range_choices = ["전체"] + sorted(df_keywords["κ²€μƒ‰λŸ‰κ΅¬κ°„"].unique().tolist())
# 뢄석할 μΉ΄ν…Œκ³ λ¦¬ λ“œλ‘­λ‹€μš΄λ„ 같은 μ„ νƒμ§€λ‘œ μ—…λ°μ΄νŠΈ
first_category = category_list[0] if category_list else "전체 보기"
# μ„Έμ…˜λ³„ μ—‘μ…€ 파일 생성
excel_path = create_session_excel_file(df_keywords, session_id)
# 뢄석 μ„Ήμ…˜ ν‘œμ‹œ
return (gr.update(value=html), gr.update(choices=category_list), gr.update(choices=volume_range_choices),
df_keywords, gr.update(choices=category_list, value=first_category), excel_path,
gr.update(visible=True), gr.update(visible=True), current_keyword)
def create_session_excel_file(df, session_id):
"""μ„Έμ…˜λ³„ μ—‘μ…€ 파일 생성"""
try:
excel_path = create_session_temp_file(session_id, '.xlsx')
df.to_excel(excel_path, index=False, engine='openpyxl')
logger.info(f"μ„Έμ…˜ {session_id[:8]}... μ—‘μ…€ 파일 생성: {excel_path}")
return excel_path
except Exception as e:
logger.error(f"μ„Έμ…˜λ³„ μ—‘μ…€ 파일 생성 였λ₯˜: {e}")
return None
def analyze_with_auto_download(analysis_keywords, selected_category, state_df, session_id):
"""μΉ΄ν…Œκ³ λ¦¬ 일치 뢄석 μ‹€ν–‰ 및 μžλ™ λ‹€μš΄λ‘œλ“œ (μ„Έμ…˜ ID μΆ”κ°€)"""
update_session_activity(session_id)
# 뢄석할 ν‚€μ›Œλ“œλ‚˜ μΉ΄ν…Œκ³ λ¦¬κ°€ μ—†λŠ” 경우
if not analysis_keywords or not selected_category:
return "ν‚€μ›Œλ“œμ™€ μΉ΄ν…Œκ³ λ¦¬λ₯Ό λͺ¨λ‘ μ„ νƒν•΄μ£Όμ„Έμš”.", None, gr.update(visible=False)
# 뢄석 μ‹€ν–‰ - 동적 λ‘œλ”©λœ category_analysis λͺ¨λ“ˆ μ‚¬μš©
analysis_result = category_analysis.analyze_keywords_by_category(analysis_keywords, selected_category, state_df)
# μ„Έμ…˜λ³„ μ—‘μ…€ 파일 생성
excel_path = create_session_excel_file(state_df, session_id)
# 뢄석 κ²°κ³Ό 좜λ ₯ μ„Ήμ…˜ ν‘œμ‹œ
return analysis_result, excel_path, gr.update(visible=True)
def filter_and_sort_table(df, selected_cat, keyword_sort, total_volume_sort, usage_count_sort, selected_volume_range, exclude_zero_volume, session_id):
"""ν…Œμ΄λΈ” 필터링 및 μ •λ ¬ ν•¨μˆ˜ (μ„Έμ…˜ ID μΆ”κ°€)"""
update_session_activity(session_id)
if df is None or df.empty:
return ""
# 필터링 적용
filtered_df = df.copy()
# μΉ΄ν…Œκ³ λ¦¬ ν•„ν„° 적용
if selected_cat and selected_cat != "전체 보기":
cat_name_to_filter = selected_cat.rsplit(" (", 1)[0]
filtered_df = filtered_df[filtered_df["κ΄€λ ¨ μΉ΄ν…Œκ³ λ¦¬"].astype(str).str.contains(cat_name_to_filter, case=False, na=False)]
def get_filtered_category_display(current_categories_str):
if pd.isna(current_categories_str):
return ""
categories = str(current_categories_str).split('\n')
matched_categories = [cat for cat in categories if cat_name_to_filter.lower() in cat.lower()]
if matched_categories:
return "\n".join(matched_categories)
return current_categories_str
filtered_df['κ΄€λ ¨ μΉ΄ν…Œκ³ λ¦¬'] = filtered_df['κ΄€λ ¨ μΉ΄ν…Œκ³ λ¦¬'].apply(get_filtered_category_display)
# κ²€μƒ‰λŸ‰ ꡬ간 ν•„ν„° 적용
if selected_volume_range and selected_volume_range != "전체":
filtered_df = filtered_df[filtered_df["κ²€μƒ‰λŸ‰κ΅¬κ°„"] == selected_volume_range]
# κ²€μƒ‰λŸ‰ 0 μ œμ™Έ ν•„ν„° 적용
if exclude_zero_volume:
filtered_df = filtered_df[filtered_df["μ΄κ²€μƒ‰λŸ‰"] > 0]
logger.info(f"μ„Έμ…˜ {session_id[:8]}... κ²€μƒ‰λŸ‰ 0 μ œμ™Έ ν•„ν„° 적용 - 남은 ν‚€μ›Œλ“œ 수: {len(filtered_df)}")
# μ •λ ¬ 적용
if keyword_sort != "μ •λ ¬ μ—†μŒ":
is_ascending = keyword_sort == "μ˜€λ¦„μ°¨μˆœ"
filtered_df = filtered_df.sort_values(by="μ‘°ν•© ν‚€μ›Œλ“œ", ascending=is_ascending)
if total_volume_sort != "μ •λ ¬ μ—†μŒ":
is_ascending = total_volume_sort == "μ˜€λ¦„μ°¨μˆœ"
filtered_df = filtered_df.sort_values(by="μ΄κ²€μƒ‰λŸ‰", ascending=is_ascending)
# ν‚€μ›Œλ“œ μ‚¬μš©νšŸμˆ˜ μ •λ ¬ 적용
if usage_count_sort != "μ •λ ¬ μ—†μŒ":
is_ascending = usage_count_sort == "μ˜€λ¦„μ°¨μˆœ"
filtered_df = filtered_df.sort_values(by="ν‚€μ›Œλ“œ μ‚¬μš©νšŸμˆ˜", ascending=is_ascending)
# μˆœλ²ˆμ„ 1λΆ€ν„° 순차적으둜 μœ μ§€ν•˜κΈ° μœ„ν•΄ ν–‰ 인덱슀 μž¬μ„€μ •
filtered_df = filtered_df.reset_index(drop=True)
# μˆœλ²ˆμ„ ν¬ν•¨ν•œ HTML ν…Œμ΄λΈ” 생성
html = export_utils.create_table_without_checkboxes(filtered_df)
return html
def update_category_selection(selected_cat, session_id):
"""μΉ΄ν…Œκ³ λ¦¬ ν•„ν„° 선택 μ‹œ 뢄석할 μΉ΄ν…Œκ³ λ¦¬λ„ 같은 κ°’μœΌλ‘œ μ—…λ°μ΄νŠΈ"""
update_session_activity(session_id)
logger.debug(f"μ„Έμ…˜ {session_id[:8]}... μΉ΄ν…Œκ³ λ¦¬ 선택 λ³€κ²½: {selected_cat}")
return gr.update(value=selected_cat)
def reset_interface(session_id):
"""μΈν„°νŽ˜μ΄μŠ€ 리셋 ν•¨μˆ˜ - μ„Έμ…˜λ³„ 데이터 μ΄ˆκΈ°ν™”"""
update_session_activity(session_id)
# μ„Έμ…˜λ³„ μž„μ‹œ 파일 정리
if session_id in session_temp_files:
for file_path in session_temp_files[session_id]:
try:
if os.path.exists(file_path):
os.remove(file_path)
logger.info(f"μ„Έμ…˜ {session_id[:8]}... 리셋 μ‹œ 파일 μ‚­μ œ: {file_path}")
except Exception as e:
logger.error(f"μ„Έμ…˜ {session_id[:8]}... 리셋 μ‹œ 파일 μ‚­μ œ 였λ₯˜: {e}")
session_temp_files[session_id] = []
return (
"", # 검색 ν‚€μ›Œλ“œ
True, # ν•œκΈ€λ§Œ μΆ”μΆœ
False, # κ²€μƒ‰λŸ‰ 0 ν‚€μ›Œλ“œ μ œμ™Έ
"λ©”μΈν‚€μ›Œλ“œ 적용", # μ‘°ν•© 방식
"", # HTML ν…Œμ΄λΈ”
["전체 보기"], # μΉ΄ν…Œκ³ λ¦¬ ν•„ν„°
"전체 보기", # μΉ΄ν…Œκ³ λ¦¬ ν•„ν„° 선택
["전체"], # κ²€μƒ‰λŸ‰ ꡬ간 ν•„ν„°
"전체", # κ²€μƒ‰λŸ‰ ꡬ간 선택
"μ •λ ¬ μ—†μŒ", # μ΄κ²€μƒ‰λŸ‰ μ •λ ¬
"μ •λ ¬ μ—†μŒ", # ν‚€μ›Œλ“œ μ‚¬μš©νšŸμˆ˜ μ •λ ¬
None, # μƒνƒœ DataFrame
["전체 보기"], # 뢄석할 μΉ΄ν…Œκ³ λ¦¬
"전체 보기", # 뢄석할 μΉ΄ν…Œκ³ λ¦¬ 선택
"", # ν‚€μ›Œλ“œ μž…λ ₯
"", # 뢄석 κ²°κ³Ό
None, # λ‹€μš΄λ‘œλ“œ 파일
gr.update(visible=False), # ν‚€μ›Œλ“œ 뢄석 μ„Ήμ…˜
gr.update(visible=False), # 뢄석 κ²°κ³Ό 좜λ ₯ μ„Ήμ…˜
"" # ν‚€μ›Œλ“œ μƒνƒœ
)
# 래퍼 ν•¨μˆ˜λ“€λ„ μ„Έμ…˜ ID μΆ”κ°€
def search_with_loading(keyword, korean_only, apply_main_keyword, exclude_zero_volume, session_id):
update_session_activity(session_id)
return (
gr.update(visible=True),
gr.update(visible=False)
)
def process_search_results(keyword, korean_only, apply_main_keyword, exclude_zero_volume, session_id):
update_session_activity(session_id)
result = wrapper_modified(keyword, korean_only, apply_main_keyword, exclude_zero_volume, session_id)
table_html, cat_choices, vol_choices, df, selected_cat, excel, keyword_section_vis, cat_section_vis, new_keyword_state = result
if not isinstance(df, type(None)) and not df.empty:
empty_placeholder_vis = False
keyword_section_visibility = True
execution_section_visibility = True
else:
empty_placeholder_vis = True
keyword_section_visibility = False
execution_section_visibility = False
return (
table_html, cat_choices, vol_choices, df, selected_cat, excel,
gr.update(visible=keyword_section_visibility),
gr.update(visible=cat_section_vis),
gr.update(visible=False),
gr.update(visible=empty_placeholder_vis),
gr.update(visible=execution_section_visibility),
new_keyword_state
)
def analyze_with_loading(analysis_keywords, selected_category, state_df, session_id):
update_session_activity(session_id)
return gr.update(visible=True)
def process_analyze_results(analysis_keywords, selected_category, state_df, session_id):
update_session_activity(session_id)
results = analyze_with_auto_download(analysis_keywords, selected_category, state_df, session_id)
return results + (gr.update(visible=False),)
# μ„Έμ…˜ 정리 μŠ€μΌ€μ€„λŸ¬
def start_session_cleanup_scheduler():
"""μ„Έμ…˜ 정리 μŠ€μΌ€μ€„λŸ¬ μ‹œμž‘"""
def cleanup_scheduler():
while True:
time.sleep(600) # 10λΆ„λ§ˆλ‹€ μ‹€ν–‰
cleanup_old_sessions()
# μΆ”κ°€λ‘œ ν—ˆκΉ…νŽ˜μ΄μŠ€ μž„μ‹œ 폴더도 주기적 정리
cleanup_huggingface_temp_folders()
threading.Thread(target=cleanup_scheduler, daemon=True).start()
def cleanup_on_startup():
"""μ• ν”Œλ¦¬μΌ€μ΄μ…˜ μ‹œμž‘ μ‹œ 전체 정리"""
logger.info("🧹 μ• ν”Œλ¦¬μΌ€μ΄μ…˜ μ‹œμž‘ - 초기 정리 μž‘μ—… μ‹œμž‘...")
# 1. ν—ˆκΉ…νŽ˜μ΄μŠ€ μž„μ‹œ 폴더 정리
cleanup_huggingface_temp_folders()
# 2. κΉ¨λ—ν•œ μž„μ‹œ ν™˜κ²½ μ„€μ •
app_temp_dir = setup_clean_temp_environment()
# 3. μ „μ—­ λ³€μˆ˜ μ΄ˆκΈ°ν™”
global session_temp_files, session_data
session_temp_files.clear()
session_data.clear()
logger.info(f"βœ… 초기 정리 μž‘μ—… μ™„λ£Œ - μ•± μ „μš© 디렉토리: {app_temp_dir}")
return app_temp_dir
# Gradio μΈν„°νŽ˜μ΄μŠ€ 생성
def create_app():
fontawesome_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">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap">
"""
# 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;
text-align: center !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
}
.reset-button {
background: linear-gradient(135deg, #6c757d, #495057) !important;
color: white !important;
border-radius: 30px !important;
height: 45px !important;
font-size: 16px !important;
font-weight: bold !important;
width: 100% !important;
text-align: center !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
}
.section-title {
border-bottom: 2px solid #FB7F0D;
font-weight: bold;
padding-bottom: 5px;
margin-bottom: 15px;
}
.loading-indicator {
display: flex;
align-items: center;
justify-content: center;
padding: 15px;
background-color: #f8f9fa;
border-radius: 5px;
margin: 10px 0;
border: 1px solid #ddd;
}
.loading-spinner {
border: 4px solid rgba(0, 0, 0, 0.1);
width: 24px;
height: 24px;
border-radius: 50%;
border-left-color: #FB7F0D;
animation: spin 1s linear infinite;
margin-right: 10px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.progress-bar {
height: 10px;
background-color: #FB7F0D;
border-radius: 5px;
width: 0%;
animation: progressAnim 2s ease-in-out infinite;
}
@keyframes progressAnim {
0% { width: 10%; }
50% { width: 70%; }
100% { width: 10%; }
}
.empty-table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
margin-top: 20px;
}
.empty-table th {
background-color: #FB7F0D;
color: white;
text-align: left;
padding: 12px;
border: 1px solid #ddd;
}
.empty-table td {
padding: 10px;
border: 1px solid #ddd;
text-align: center;
color: #999;
}
.button-container {
margin-top: 20px;
display: flex;
gap: 15px;
}
.execution-section {
margin-top: 20px;
background-color: #f9f9f9;
border-radius: 8px;
padding: 15px;
border: 1px solid #e5e5e5;
}
.session-info {
background-color: #e8f4f8;
padding: 8px 12px;
border-radius: 4px;
font-size: 12px;
color: #0c5460;
margin-bottom: 10px;
text-align: center;
}
"""
with gr.Blocks(css=custom_css, theme=gr.themes.Default(
primary_hue="orange",
secondary_hue="orange",
font=[gr.themes.GoogleFont("Noto Sans KR"), "ui-sans-serif", "system-ui"]
)) as demo:
gr.HTML(fontawesome_html)
# μ„Έμ…˜ ID μƒνƒœ (각 μ‚¬μš©μžλ³„λ‘œ 고유)
session_id = gr.State(get_session_id)
# ν‚€μ›Œλ“œ μƒνƒœ 관리
keyword_state = gr.State("")
# μž…λ ₯ μ„Ήμ…˜
with gr.Column(elem_classes="custom-frame fade-in"):
gr.HTML('<div class="section-title"><i class="fas fa-search"></i> 검색 μž…λ ₯</div>')
with gr.Row():
with gr.Column(scale=1):
keyword = gr.Textbox(
label="메인 ν‚€μ›Œλ“œ",
placeholder="예: μ˜€μ§•μ–΄"
)
with gr.Column(scale=1):
search_btn = gr.Button(
"λ©”μΈν‚€μ›Œλ“œ 뢄석",
elem_classes="custom-button"
)
with gr.Accordion("μ˜΅μ…˜ μ„€μ •", open=False):
with gr.Row():
with gr.Column(scale=1):
korean_only = gr.Checkbox(
label="ν•œκΈ€λ§Œ μΆ”μΆœ",
value=True
)
with gr.Column(scale=1):
exclude_zero_volume = gr.Checkbox(
label="κ²€μƒ‰λŸ‰ 0 ν‚€μ›Œλ“œ μ œμ™Έ",
value=False
)
with gr.Row():
with gr.Column(scale=1):
apply_main_keyword = gr.Radio(
["λ©”μΈν‚€μ›Œλ“œ 적용", "λ©”μΈν‚€μ›Œλ“œ 미적용"],
label="μ‘°ν•© 방식",
value="λ©”μΈν‚€μ›Œλ“œ 적용"
)
with gr.Column(scale=1):
gr.HTML("")
# μ§„ν–‰ μƒνƒœ ν‘œμ‹œ μ„Ήμ…˜
with gr.Column(elem_classes="custom-frame fade-in", visible=False) as progress_section:
gr.HTML('<div class="section-title"><i class="fas fa-spinner"></i> 뢄석 μ§„ν–‰ μƒνƒœ</div>')
progress_html = gr.HTML("""
<div style="padding: 15px; background-color: #f9f9f9; border-radius: 5px; margin: 10px 0; border: 1px solid #ddd;">
<div style="margin-bottom: 10px; display: flex; align-items: center;">
<i class="fas fa-spinner fa-spin" style="color: #FB7F0D; margin-right: 10px;"></i>
<span>ν‚€μ›Œλ“œ 데이터λ₯Ό λΆ„μ„μ€‘μž…λ‹ˆλ‹€. μž μ‹œλ§Œ κΈ°λ‹€λ €μ£Όμ„Έμš”...</span>
</div>
<div style="background-color: #e9ecef; height: 10px; border-radius: 5px; overflow: hidden;">
<div class="progress-bar"></div>
</div>
</div>
""")
# λ©”μΈν‚€μ›Œλ“œ 뢄석 κ²°κ³Ό μ„Ήμ…˜
with gr.Column(elem_classes="custom-frame fade-in") as main_keyword_section:
gr.HTML('<div class="section-title"><i class="fas fa-table"></i> λ©”μΈν‚€μ›Œλ“œ 뢄석 κ²°κ³Ό</div>')
empty_table_html = gr.HTML("""
<table class="empty-table">
<thead>
<tr>
<th>순번</th>
<th>μ‘°ν•© ν‚€μ›Œλ“œ</th>
<th>PCκ²€μƒ‰λŸ‰</th>
<th>λͺ¨λ°”μΌκ²€μƒ‰λŸ‰</th>
<th>μ΄κ²€μƒ‰λŸ‰</th>
<th>κ²€μƒ‰λŸ‰κ΅¬κ°„</th>
<th>ν‚€μ›Œλ“œ μ‚¬μš©μžμˆœμœ„</th>
<th>ν‚€μ›Œλ“œ μ‚¬μš©νšŸμˆ˜</th>
<th>μƒν’ˆ 등둝 μΉ΄ν…Œκ³ λ¦¬</th>
</tr>
</thead>
<tbody>
<tr>
<td colspan="9" style="padding: 30px; text-align: center;">
검색을 μ‹€ν–‰ν•˜λ©΄ 여기에 κ²°κ³Όκ°€ ν‘œμ‹œλ©λ‹ˆλ‹€
</td>
</tr>
</tbody>
</table>
""")
with gr.Column(visible=False) as keyword_analysis_section:
with gr.Row():
with gr.Column(scale=1):
category_filter = gr.Dropdown(
choices=["전체 보기"],
label="μΉ΄ν…Œκ³ λ¦¬ ν•„ν„°",
value="전체 보기",
interactive=True
)
with gr.Column(scale=1):
total_volume_sort = gr.Dropdown(
choices=["μ •λ ¬ μ—†μŒ", "μ˜€λ¦„μ°¨μˆœ", "λ‚΄λ¦Όμ°¨μˆœ"],
label="μ΄κ²€μƒ‰λŸ‰ μ •λ ¬",
value="μ •λ ¬ μ—†μŒ",
interactive=True
)
with gr.Row():
with gr.Column(scale=1):
search_volume_filter = gr.Dropdown(
choices=["전체"],
label="κ²€μƒ‰λŸ‰ ꡬ간 ν•„ν„°",
value="전체",
interactive=True
)
with gr.Column(scale=1):
usage_count_sort = gr.Dropdown(
choices=["μ •λ ¬ μ—†μŒ", "μ˜€λ¦„μ°¨μˆœ", "λ‚΄λ¦Όμ°¨μˆœ"],
label="ν‚€μ›Œλ“œ μ‚¬μš©νšŸμˆ˜ μ •λ ¬",
value="μ •λ ¬ μ—†μŒ",
interactive=True
)
gr.HTML("<div class='data-container' id='table_container'></div>")
table_output = gr.HTML(elem_classes="fade-in")
# μΉ΄ν…Œκ³ λ¦¬ 뢄석 μ„Ήμ…˜
with gr.Column(elem_classes="custom-frame fade-in", visible=False) as category_analysis_section:
gr.HTML('<div class="section-title"><i class="fas fa-chart-bar"></i> ν‚€μ›Œλ“œ 뢄석</div>')
with gr.Row():
with gr.Column(scale=1):
analysis_keywords = gr.Textbox(
label="ν‚€μ›Œλ“œ μž…λ ₯ (μ΅œλŒ€ 20개, μ‰Όν‘œ λ˜λŠ” μ—”ν„°λ‘œ ꡬ뢄)",
placeholder="예: μ˜€μ§•μ–΄λ³ΆμŒ, μ˜€μ§•μ–΄ μ†μ§ˆ, μ˜€μ§•μ–΄ μš”λ¦¬...",
lines=5
)
with gr.Column(scale=1):
selected_category = gr.Dropdown(
label="뢄석할 μΉ΄ν…Œκ³ λ¦¬(뢄석 μ „ λ°˜λ“œμ‹œ μ„ νƒν•΄μ£Όμ„Έμš”)",
choices=["전체 보기"],
value="전체 보기",
interactive=True
)
# μ‹€ν–‰ μ„Ήμ…˜
with gr.Column(elem_classes="execution-section", visible=False) as execution_section:
gr.HTML('<div class="section-title"><i class="fas fa-play-circle"></i> μ‹€ν–‰</div>')
with gr.Row():
with gr.Column(scale=1):
analyze_btn = gr.Button(
"μΉ΄ν…Œκ³ λ¦¬ 일치 뢄석",
elem_classes=["execution-button", "primary-button"]
)
with gr.Column(scale=1):
reset_btn = gr.Button(
"λͺ¨λ“  μž…λ ₯ μ΄ˆκΈ°ν™”",
elem_classes=["execution-button", "secondary-button"]
)
# 뢄석 κ²°κ³Ό 좜λ ₯ μ„Ήμ…˜
with gr.Column(elem_classes="custom-frame fade-in", visible=False) as analysis_output_section:
gr.HTML('<div class="section-title"><i class="fas fa-list-ul"></i> 뢄석 κ²°κ³Ό μš”μ•½</div>')
analysis_result = gr.HTML(elem_classes="fade-in")
with gr.Row():
download_output = gr.File(
label="ν‚€μ›Œλ“œ λͺ©λ‘ λ‹€μš΄λ‘œλ“œ",
visible=True
)
# μƒνƒœ μ €μž₯용 λ³€μˆ˜
state_df = gr.State()
# 이벀트 μ—°κ²° - λͺ¨λ“  ν•¨μˆ˜μ— session_id μΆ”κ°€
search_btn.click(
fn=search_with_loading,
inputs=[keyword, korean_only, apply_main_keyword, exclude_zero_volume, session_id],
outputs=[progress_section, empty_table_html]
).then(
fn=process_search_results,
inputs=[keyword, korean_only, apply_main_keyword, exclude_zero_volume, session_id],
outputs=[
table_output, category_filter, search_volume_filter,
state_df, selected_category, download_output,
keyword_analysis_section, category_analysis_section,
progress_section, empty_table_html, execution_section,
keyword_state
]
)
# ν•„ν„° 및 μ •λ ¬ λ³€κ²½ 이벀트 μ—°κ²° - session_id μΆ”κ°€
category_filter.change(
fn=filter_and_sort_table,
inputs=[
state_df, category_filter, gr.Textbox(value="μ •λ ¬ μ—†μŒ", visible=False),
total_volume_sort, usage_count_sort,
search_volume_filter, exclude_zero_volume, session_id
],
outputs=[table_output]
)
category_filter.change(
fn=update_category_selection,
inputs=[category_filter, session_id],
outputs=[selected_category]
)
total_volume_sort.change(
fn=filter_and_sort_table,
inputs=[
state_df, category_filter, gr.Textbox(value="μ •λ ¬ μ—†μŒ", visible=False),
total_volume_sort, usage_count_sort,
search_volume_filter, exclude_zero_volume, session_id
],
outputs=[table_output]
)
usage_count_sort.change(
fn=filter_and_sort_table,
inputs=[
state_df, category_filter, gr.Textbox(value="μ •λ ¬ μ—†μŒ", visible=False),
total_volume_sort, usage_count_sort,
search_volume_filter, exclude_zero_volume, session_id
],
outputs=[table_output]
)
search_volume_filter.change(
fn=filter_and_sort_table,
inputs=[
state_df, category_filter, gr.Textbox(value="μ •λ ¬ μ—†μŒ", visible=False),
total_volume_sort, usage_count_sort,
search_volume_filter, exclude_zero_volume, session_id
],
outputs=[table_output]
)
exclude_zero_volume.change(
fn=filter_and_sort_table,
inputs=[
state_df, category_filter, gr.Textbox(value="μ •λ ¬ μ—†μŒ", visible=False),
total_volume_sort, usage_count_sort,
search_volume_filter, exclude_zero_volume, session_id
],
outputs=[table_output]
)
# μΉ΄ν…Œκ³ λ¦¬ 뢄석 λ²„νŠΌ 이벀트 - session_id μΆ”κ°€
analyze_btn.click(
fn=analyze_with_loading,
inputs=[analysis_keywords, selected_category, state_df, session_id],
outputs=[progress_section]
).then(
fn=process_analyze_results,
inputs=[analysis_keywords, selected_category, state_df, session_id],
outputs=[analysis_result, download_output, analysis_output_section, progress_section]
)
# 리셋 λ²„νŠΌ 이벀트 μ—°κ²° - session_id μΆ”κ°€
reset_btn.click(
fn=reset_interface,
inputs=[session_id],
outputs=[
keyword, korean_only, exclude_zero_volume, apply_main_keyword,
table_output, category_filter, category_filter,
search_volume_filter, search_volume_filter,
total_volume_sort, usage_count_sort,
state_df, selected_category, selected_category,
analysis_keywords, analysis_result, download_output,
keyword_analysis_section, analysis_output_section,
keyword_state
]
)
return demo
if __name__ == "__main__":
# ========== μ‹œμž‘ μ‹œ 전체 μ΄ˆκΈ°ν™” ==========
logger.info("πŸš€ λ©”μΈν‚€μ›Œλ“œ 뢄석 μ• ν”Œλ¦¬μΌ€μ΄μ…˜ μ‹œμž‘...")
# 1. 첫 번째: ν—ˆκΉ…νŽ˜μ΄μŠ€ μž„μ‹œ 폴더 정리 및 ν™˜κ²½ μ„€μ •
app_temp_dir = cleanup_on_startup()
# 2. μ„Έμ…˜ 정리 μŠ€μΌ€μ€„λŸ¬ μ‹œμž‘
start_session_cleanup_scheduler()
# 3. API μ„€μ • μ΄ˆκΈ°ν™”
try:
api_utils.initialize_api_configs()
except Exception as e:
logger.warning(f"API μ„€μ • μ΄ˆκΈ°ν™” 쀑 였λ₯˜ (계속 μ§„ν–‰): {e}")
# 4. Gemini λͺ¨λΈ μ΄ˆκΈ°ν™”
try:
gemini_model = text_utils.get_gemini_model()
except Exception as e:
logger.warning(f"Gemini λͺ¨λΈ μ΄ˆκΈ°ν™” 쀑 였λ₯˜ (계속 μ§„ν–‰): {e}")
logger.info("===== λ©€ν‹°μœ μ € λ©”μΈν‚€μ›Œλ“œ 뢄석 Application Startup at %s =====", time.strftime("%Y-%m-%d %H:%M:%S"))
logger.info(f"πŸ“ μž„μ‹œ 파일 μ €μž₯ μœ„μΉ˜: {app_temp_dir}")
# ========== μ•± μ‹€ν–‰ ==========
try:
app = create_app()
app.launch(
share=False, # λ³΄μ•ˆμ„ μœ„ν•΄ share λΉ„ν™œμ„±ν™”
server_name="0.0.0.0", # λͺ¨λ“  IPμ—μ„œ μ ‘κ·Ό ν—ˆμš©
server_port=7860, # 포트 μ§€μ •
max_threads=40, # λ©€ν‹°μœ μ €λ₯Ό μœ„ν•œ μŠ€λ ˆλ“œ 수 증가
auth=None, # ν•„μš”μ‹œ 인증 μΆ”κ°€ κ°€λŠ₯
show_error=True, # μ—λŸ¬ ν‘œμ‹œ
quiet=False, # 둜그 ν‘œμ‹œ
favicon_path=None, # νŒŒλΉ„μ½˜ μ„€μ •
ssl_verify=False # SSL 검증 λΉ„ν™œμ„±ν™” (개발용)
)
except Exception as e:
logger.error(f"μ• ν”Œλ¦¬μΌ€μ΄μ…˜ μ‹€ν–‰ μ‹€νŒ¨: {e}")
raise
finally:
# μ• ν”Œλ¦¬μΌ€μ΄μ…˜ μ’…λ£Œ μ‹œ 정리
logger.info("🧹 μ• ν”Œλ¦¬μΌ€μ΄μ…˜ μ’…λ£Œ - μ΅œμ’… 정리 μž‘μ—…...")
try:
cleanup_huggingface_temp_folders()
if os.path.exists(app_temp_dir):
shutil.rmtree(app_temp_dir, ignore_errors=True)
logger.info("βœ… μ΅œμ’… 정리 μ™„λ£Œ")
except Exception as e:
logger.error(f"μ΅œμ’… 정리 쀑 였λ₯˜: {e}")