4G23WAS2 / app.py
ssboost's picture
Update app.py
d7fb11e verified
raw
history blame
33.5 kB
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
from gradio_client import Client
# ν™˜κ²½λ³€μˆ˜ λ‘œλ“œλ₯Ό μœ„ν•œ dotenv μ‚¬μš©
try:
from dotenv import load_dotenv
load_dotenv()
except ImportError:
pass
# λ‘œκΉ… μ„€μ •
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.StreamHandler(),
logging.FileHandler('control_tower_app.log', mode='a')
]
)
logger = logging.getLogger(__name__)
# API ν΄λΌμ΄μ–ΈνŠΈ μ„€μ •
def get_api_client():
"""ν™˜κ²½λ³€μˆ˜μ—μ„œ API μ—”λ“œν¬μΈνŠΈλ₯Ό 가져와 ν΄λΌμ΄μ–ΈνŠΈ 생성"""
endpoint = os.getenv('API_ENDPOINT', '').strip()
# 디버깅을 μœ„ν•œ ν™˜κ²½λ³€μˆ˜ 체크 (μ‹€μ œ 값은 λ‘œκ·Έμ— 남기지 μ•ŠμŒ)
if not endpoint:
logger.error("API_ENDPOINT ν™˜κ²½λ³€μˆ˜κ°€ μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€.")
logger.info("μ‚¬μš© κ°€λŠ₯ν•œ ν™˜κ²½λ³€μˆ˜λ“€:")
for key in os.environ.keys():
if 'API' in key.upper():
logger.info(f" {key}: {'섀정됨' if os.environ[key] else 'λΉ„μ–΄μžˆμŒ'}")
raise ValueError("API_ENDPOINT ν™˜κ²½λ³€μˆ˜κ°€ μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€.")
if endpoint.startswith('#') or len(endpoint) == 0:
logger.error("API_ENDPOINT ν™˜κ²½λ³€μˆ˜κ°€ μ˜¬λ°”λ₯΄μ§€ μ•Šμ€ ν˜•μ‹μž…λ‹ˆλ‹€.")
raise ValueError("API_ENDPOINT ν™˜κ²½λ³€μˆ˜κ°€ μ˜¬λ°”λ₯΄κ²Œ μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€.")
# μ—”λ“œν¬μΈνŠΈ ν˜•μ‹ 검증
if '/' not in endpoint:
logger.error("API_ENDPOINTλŠ” 'username/repo-name' ν˜•μ‹μ΄μ–΄μ•Ό ν•©λ‹ˆλ‹€.")
raise ValueError("API_ENDPOINT ν˜•μ‹μ΄ μ˜¬λ°”λ₯΄μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.")
return Client(endpoint)
# μ„Έμ…˜λ³„ μž„μ‹œ 파일 관리λ₯Ό μœ„ν•œ λ”•μ…”λ„ˆλ¦¬
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(), "control_tower_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['CONTROL_TOWER_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('CONTROL_TOWER_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):
"""ν‚€μ›Œλ“œ 검색 및 처리 래퍼 ν•¨μˆ˜ (API ν΄λΌμ΄μ–ΈνŠΈ μ‚¬μš©)"""
update_session_activity(session_id)
try:
client = get_api_client()
result = client.predict(
keyword=keyword,
korean_only=korean_only,
apply_main_keyword=apply_main_keyword_option,
exclude_zero_volume=exclude_zero_volume,
api_name="/process_search_results"
)
# API 결과 처리
table_html, cat_choices, vol_choices, selected_cat, download_file = result
# 둜컬 파일둜 λ‹€μš΄λ‘œλ“œ 파일 볡사
local_file = None
if download_file:
local_file = create_session_temp_file(session_id, '.xlsx')
shutil.copy(download_file, local_file)
return (
gr.update(value=table_html),
gr.update(choices=cat_choices),
gr.update(choices=vol_choices),
None, # DataFrame은 ν΄λΌμ΄μ–ΈνŠΈμ—μ„œ κ΄€λ¦¬ν•˜μ§€ μ•ŠμŒ
gr.update(choices=cat_choices, value=selected_cat),
local_file,
gr.update(visible=True),
gr.update(visible=True),
keyword
)
except Exception as e:
logger.error(f"API 호좜 였λ₯˜: {e}")
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),
keyword
)
def analyze_with_auto_download(analysis_keywords, selected_category, state_df, session_id):
"""μΉ΄ν…Œκ³ λ¦¬ 일치 뢄석 μ‹€ν–‰ 및 μžλ™ λ‹€μš΄λ‘œλ“œ (API ν΄λΌμ΄μ–ΈνŠΈ μ‚¬μš©)"""
update_session_activity(session_id)
try:
client = get_api_client()
result = client.predict(
analysis_keywords=analysis_keywords,
selected_category=selected_category,
api_name="/process_analyze_results"
)
analysis_result, download_file = result
# 둜컬 파일둜 λ‹€μš΄λ‘œλ“œ 파일 볡사
local_file = None
if download_file:
local_file = create_session_temp_file(session_id, '.xlsx')
shutil.copy(download_file, local_file)
return analysis_result, local_file, gr.update(visible=True)
except Exception as e:
logger.error(f"뢄석 API 호좜 였λ₯˜: {e}")
return "뢄석 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€. λ‹€μ‹œ μ‹œλ„ν•΄μ£Όμ„Έμš”.", None, gr.update(visible=False)
def filter_and_sort_table(df, selected_cat, keyword_sort, total_volume_sort, usage_count_sort, selected_volume_range, exclude_zero_volume, session_id):
"""ν…Œμ΄λΈ” 필터링 및 μ •λ ¬ ν•¨μˆ˜ (API ν΄λΌμ΄μ–ΈνŠΈ μ‚¬μš©)"""
update_session_activity(session_id)
try:
client = get_api_client()
result = client.predict(
selected_cat=selected_cat,
keyword_sort=keyword_sort,
total_volume_sort=total_volume_sort,
usage_count_sort=usage_count_sort,
selected_volume_range=selected_volume_range,
exclude_zero_volume=exclude_zero_volume,
api_name="/filter_and_sort_table"
)
return result
except Exception as e:
logger.error(f"필터링 API 호좜 였λ₯˜: {e}")
return ""
def update_category_selection(selected_cat, session_id):
"""μΉ΄ν…Œκ³ λ¦¬ ν•„ν„° 선택 μ‹œ 뢄석할 μΉ΄ν…Œκ³ λ¦¬λ„ 같은 κ°’μœΌλ‘œ μ—…λ°μ΄νŠΈ"""
update_session_activity(session_id)
try:
client = get_api_client()
result = client.predict(
selected_cat=selected_cat,
api_name="/update_category_selection"
)
return gr.update(value=result)
except Exception as e:
logger.error(f"μΉ΄ν…Œκ³ λ¦¬ 선택 API 호좜 였λ₯˜: {e}")
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] = []
try:
client = get_api_client()
result = client.predict(
api_name="/reset_interface"
)
# API κ²°κ³Όλ₯Ό κ·ΈλŒ€λ‘œ λ°˜ν™˜
return result
except Exception as e:
logger.error(f"리셋 API 호좜 였λ₯˜: {e}")
return (
"", # 검색 ν‚€μ›Œλ“œ
True, # ν•œκΈ€λ§Œ μΆ”μΆœ
False, # κ²€μƒ‰λŸ‰ 0 ν‚€μ›Œλ“œ μ œμ™Έ
"λ©”μΈν‚€μ›Œλ“œ 적용", # μ‘°ν•© 방식
"", # HTML ν…Œμ΄λΈ”
["전체 보기"], # μΉ΄ν…Œκ³ λ¦¬ ν•„ν„°
"전체 보기", # μΉ΄ν…Œκ³ λ¦¬ ν•„ν„° 선택
["전체"], # κ²€μƒ‰λŸ‰ ꡬ간 ν•„ν„°
"전체", # κ²€μƒ‰λŸ‰ ꡬ간 선택
"μ •λ ¬ μ—†μŒ", # μ΄κ²€μƒ‰λŸ‰ μ •λ ¬
"μ •λ ¬ μ—†μŒ", # ν‚€μ›Œλ“œ μ‚¬μš©νšŸμˆ˜ μ •λ ¬
["전체 보기"], # 뢄석할 μΉ΄ν…Œκ³ λ¦¬
"전체 보기", # 뢄석할 μΉ΄ν…Œκ³ λ¦¬ 선택
"", # ν‚€μ›Œλ“œ μž…λ ₯
"", # 뢄석 κ²°κ³Ό
None # λ‹€μš΄λ‘œλ“œ 파일
)
# 래퍼 ν•¨μˆ˜λ“€
def search_with_loading(keyword, korean_only, apply_main_keyword, exclude_zero_volume, session_id):
update_session_activity(session_id)
try:
client = get_api_client()
result = client.predict(
keyword=keyword,
korean_only=korean_only,
apply_main_keyword=apply_main_keyword,
exclude_zero_volume=exclude_zero_volume,
api_name="/search_with_loading"
)
return (
gr.update(visible=True),
gr.update(visible=False)
)
except Exception as e:
logger.error(f"검색 λ‘œλ”© API 호좜 였λ₯˜: {e}")
return (
gr.update(visible=False),
gr.update(visible=True)
)
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 excel is not None:
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)
try:
client = get_api_client()
result = client.predict(
analysis_keywords=analysis_keywords,
selected_category=selected_category,
api_name="/analyze_with_loading"
)
return gr.update(visible=True)
except Exception as e:
logger.error(f"뢄석 λ‘œλ”© API 호좜 였λ₯˜: {e}")
return gr.update(visible=False)
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 = ""
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,
selected_category, selected_category,
analysis_keywords, analysis_result, download_output
]
)
return demo
if __name__ == "__main__":
# ========== μ‹œμž‘ μ‹œ 전체 μ΄ˆκΈ°ν™” ==========
logger.info("πŸš€ 컨트둀 νƒ€μ›Œ μ• ν”Œλ¦¬μΌ€μ΄μ…˜ μ‹œμž‘...")
# 1. 첫 번째: ν—ˆκΉ…νŽ˜μ΄μŠ€ μž„μ‹œ 폴더 정리 및 ν™˜κ²½ μ„€μ •
app_temp_dir = cleanup_on_startup()
# 2. μ„Έμ…˜ 정리 μŠ€μΌ€μ€„λŸ¬ μ‹œμž‘
start_session_cleanup_scheduler()
# 3. API μ—”λ“œν¬μΈνŠΈ 확인
try:
client = get_api_client()
logger.info("βœ… API ν΄λΌμ΄μ–ΈνŠΈ μ—°κ²° 확인 μ™„λ£Œ")
except Exception as e:
logger.error(f"❌ API ν΄λΌμ΄μ–ΈνŠΈ μ—°κ²° μ‹€νŒ¨: {e}")
raise
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}")