4G23WAS3 / app.py
ssboost's picture
Update app.py
cd74a76 verified
raw
history blame
33.8 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
import requests
import json
from dotenv import load_dotenv
# ν™˜κ²½λ³€μˆ˜ λ‘œλ“œ
load_dotenv()
# λ‘œκΉ… μ„€μ • (API 정보 μ™„μ „ 차단)
logging.basicConfig(level=logging.WARNING)
logger = logging.getLogger(__name__)
# μ„Έμ…˜λ³„ μž„μ‹œ 파일 관리λ₯Ό μœ„ν•œ λ”•μ…”λ„ˆλ¦¬
session_temp_files = {}
session_data = {}
def get_api_client():
"""ν™˜κ²½λ³€μˆ˜μ—μ„œ API μ—”λ“œν¬μΈνŠΈλ₯Ό 가져와 μš”μ²­ ν•¨μˆ˜ 생성"""
endpoint = os.getenv('API_ENDPOINT')
if not endpoint:
raise ValueError("API_ENDPOINT ν™˜κ²½λ³€μˆ˜κ°€ ν•„μš”ν•©λ‹ˆλ‹€.")
def make_request(api_name, **kwargs):
try:
# gradio_client와 λ™μΌν•œ λ°©μ‹μœΌλ‘œ API 호좜
if not endpoint.startswith('http'):
base_url = f"https://{endpoint}.hf.space"
else:
base_url = endpoint
# Gradio API μ—”λ“œν¬μΈνŠΈ ν˜•μ‹ λ§žμΆ”κΈ°
url = f"{base_url}/call{api_name}"
# λ§€κ°œλ³€μˆ˜λ₯Ό μˆœμ„œλŒ€λ‘œ λ°°μ—΄λ‘œ λ³€ν™˜
if api_name == "/process_search_results":
data = [kwargs.get('keyword', ''), kwargs.get('korean_only', True),
kwargs.get('apply_main_keyword', 'λ©”μΈν‚€μ›Œλ“œ 적용'), kwargs.get('exclude_zero_volume', False)]
elif api_name == "/search_with_loading":
data = [kwargs.get('keyword', ''), kwargs.get('korean_only', True),
kwargs.get('apply_main_keyword', 'λ©”μΈν‚€μ›Œλ“œ 적용'), kwargs.get('exclude_zero_volume', False)]
elif api_name == "/filter_and_sort_table":
data = [kwargs.get('selected_cat', '전체 보기'), kwargs.get('keyword_sort', 'μ •λ ¬ μ—†μŒ'),
kwargs.get('total_volume_sort', 'μ •λ ¬ μ—†μŒ'), kwargs.get('usage_count_sort', 'μ •λ ¬ μ—†μŒ'),
kwargs.get('selected_volume_range', '전체'), kwargs.get('exclude_zero_volume', False)]
elif api_name == "/update_category_selection":
data = [kwargs.get('selected_cat', '전체 보기')]
elif api_name == "/process_analyze_results":
data = [kwargs.get('analysis_keywords', ''), kwargs.get('selected_category', '전체 보기')]
elif api_name == "/analyze_with_loading":
data = [kwargs.get('analysis_keywords', ''), kwargs.get('selected_category', '전체 보기')]
elif api_name == "/reset_interface":
data = []
elif api_name == "/get_session_id":
data = []
else:
data = []
response = requests.post(url, json={"data": data}, timeout=60)
if response.status_code == 200:
result = response.json()
return result.get('data', [])
else:
raise Exception(f"API 호좜 μ‹€νŒ¨: {response.status_code}")
except Exception as e:
raise Exception(f"API μ—°κ²° 였λ₯˜: {str(e)}")
return type('APIClient', (), {'predict': lambda self, **kwargs: make_request(kwargs.pop('api_name'), **kwargs)})()
def cleanup_huggingface_temp_folders():
"""ν—ˆκΉ…νŽ˜μ΄μŠ€ μž„μ‹œ 폴더 초기 정리"""
try:
temp_dirs = [tempfile.gettempdir(), "/tmp", "/var/tmp"]
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")))
for file_path in session_files:
try:
if os.path.getmtime(file_path) < time.time() - 3600:
os.remove(file_path)
cleanup_count += 1
except Exception:
pass
except Exception:
pass
logger.info(f"βœ… μž„μ‹œ 폴더 정리 μ™„λ£Œ - {cleanup_count}개 파일 μ‚­μ œ")
except Exception as e:
logger.error(f"μž„μ‹œ 폴더 정리 쀑 였λ₯˜: {e}")
def setup_clean_temp_environment():
"""κΉ¨λ—ν•œ μž„μ‹œ ν™˜κ²½ μ„€μ •"""
try:
cleanup_huggingface_temp_folders()
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)
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 생성"""
try:
client = get_api_client()
result = client.predict(api_name="/get_session_id")
return result[0] if result else str(uuid.uuid4())
except Exception:
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:
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 search_with_loading(keyword, korean_only, apply_main_keyword, exclude_zero_volume):
"""원본 API: /search_with_loading"""
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 result[0] if result else ""
except Exception as e:
logger.error(f"search_with_loading API 호좜 였λ₯˜: {e}")
return ""
def process_search_results(keyword, korean_only, apply_main_keyword, exclude_zero_volume):
"""원본 API: /process_search_results"""
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="/process_search_results"
)
# κ²°κ³Ό μ•ˆμ „ν•˜κ²Œ 처리
if len(result) >= 5:
table_html, cat_choices, vol_choices, selected_cat, download_file = result[:5]
# λ‹€μš΄λ‘œλ“œ 파일이 μžˆλŠ” 경우 둜컬둜 볡사
local_download_file = None
if download_file:
session_id = get_session_id()
local_download_file = create_session_temp_file(session_id, '.xlsx')
try:
shutil.copy2(download_file, local_download_file)
except Exception as e:
logger.error(f"파일 볡사 였λ₯˜: {e}")
local_download_file = None
return table_html, cat_choices, vol_choices, selected_cat, local_download_file
else:
return (
"<p>검색 κ²°κ³Όκ°€ μ—†μŠ΅λ‹ˆλ‹€.</p>",
["전체 보기"], ["전체"], "전체 보기", None
)
except Exception as e:
logger.error(f"process_search_results API 호좜 였λ₯˜: {e}")
return (
"<p>μ„œλΉ„μŠ€ 연결에 λ¬Έμ œκ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€. μž μ‹œ ν›„ λ‹€μ‹œ μ‹œλ„ν•΄μ£Όμ„Έμš”.</p>",
["전체 보기"], ["전체"], "전체 보기", None
)
def filter_and_sort_table(selected_cat, keyword_sort, total_volume_sort, usage_count_sort, selected_volume_range, exclude_zero_volume):
"""원본 API: /filter_and_sort_table"""
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[0] if result else ""
except Exception as e:
logger.error(f"filter_and_sort_table API 호좜 였λ₯˜: {e}")
return "<p>필터링 μ„œλΉ„μŠ€ 연결에 λ¬Έμ œκ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.</p>"
def update_category_selection(selected_cat):
"""원본 API: /update_category_selection"""
try:
client = get_api_client()
result = client.predict(
selected_cat=selected_cat,
api_name="/update_category_selection"
)
return gr.update(value=result[0] if result else selected_cat)
except Exception as e:
logger.error(f"update_category_selection API 호좜 였λ₯˜: {e}")
return gr.update(value=selected_cat)
def analyze_with_loading(analysis_keywords, selected_category):
"""원본 API: /analyze_with_loading"""
try:
client = get_api_client()
result = client.predict(
analysis_keywords=analysis_keywords,
selected_category=selected_category,
api_name="/analyze_with_loading"
)
return result[0] if result else ""
except Exception as e:
logger.error(f"analyze_with_loading API 호좜 였λ₯˜: {e}")
return ""
def process_analyze_results(analysis_keywords, selected_category):
"""원본 API: /process_analyze_results"""
try:
client = get_api_client()
result = client.predict(
analysis_keywords=analysis_keywords,
selected_category=selected_category,
api_name="/process_analyze_results"
)
if len(result) >= 2:
analysis_result, download_file = result[:2]
# λ‹€μš΄λ‘œλ“œ 파일이 μžˆλŠ” 경우 둜컬둜 볡사
local_download_file = None
if download_file:
session_id = get_session_id()
local_download_file = create_session_temp_file(session_id, '.xlsx')
try:
shutil.copy2(download_file, local_download_file)
except Exception as e:
logger.error(f"뢄석 κ²°κ³Ό 파일 볡사 였λ₯˜: {e}")
local_download_file = None
return analysis_result, local_download_file
else:
return "뢄석 κ²°κ³Όκ°€ μ—†μŠ΅λ‹ˆλ‹€.", None
except Exception as e:
logger.error(f"process_analyze_results API 호좜 였λ₯˜: {e}")
return "뢄석 μ„œλΉ„μŠ€ 연결에 λ¬Έμ œκ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€. μž μ‹œ ν›„ λ‹€μ‹œ μ‹œλ„ν•΄μ£Όμ„Έμš”.", None
def reset_interface():
"""원본 API: /reset_interface"""
try:
client = get_api_client()
result = client.predict(api_name="/reset_interface")
return result if result else get_default_reset_values()
except Exception as e:
logger.error(f"reset_interface API 호좜 였λ₯˜: {e}")
return get_default_reset_values()
def get_default_reset_values():
"""κΈ°λ³Έ 리셋 κ°’ λ°˜ν™˜"""
return (
"", True, False, "λ©”μΈν‚€μ›Œλ“œ 적용", "", ["전체 보기"], "전체 보기",
["전체"], "전체", "μ •λ ¬ μ—†μŒ", "μ •λ ¬ μ—†μŒ", ["전체 보기"], "전체 보기",
"", "", None
)
# UI 처리 래퍼 ν•¨μˆ˜λ“€
def wrapper_search_with_loading(keyword, korean_only, apply_main_keyword, exclude_zero_volume):
"""검색 λ‘œλ”© UI 처리"""
search_with_loading(keyword, korean_only, apply_main_keyword, exclude_zero_volume)
return (
gr.update(visible=True), # progress_section
gr.update(visible=False) # empty_table_html
)
def wrapper_process_search_results(keyword, korean_only, apply_main_keyword, exclude_zero_volume):
"""검색 κ²°κ³Ό 처리 UI"""
result = process_search_results(keyword, korean_only, apply_main_keyword, exclude_zero_volume)
table_html, cat_choices, vol_choices, selected_cat, download_file = result
# UI ν‘œμ‹œ μ—¬λΆ€ κ²°μ •
if table_html and "검색 κ²°κ³Όκ°€ μ—†μŠ΅λ‹ˆλ‹€" not in table_html and "λ¬Έμ œκ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€" not in table_html:
keyword_section_visibility = True
category_section_visibility = True
empty_placeholder_vis = False
execution_section_visibility = True
else:
keyword_section_visibility = False
category_section_visibility = False
empty_placeholder_vis = True
execution_section_visibility = False
# κ°€μƒμ˜ state_df
state_df = pd.DataFrame()
return (
table_html, # table_output
cat_choices, # category_filter choices
vol_choices, # search_volume_filter choices
state_df, # state_df
selected_cat, # selected_category value
download_file, # download_output
gr.update(visible=keyword_section_visibility), # keyword_analysis_section
gr.update(visible=category_section_visibility), # category_analysis_section
gr.update(visible=False), # progress_section
gr.update(visible=empty_placeholder_vis), # empty_table_html
gr.update(visible=execution_section_visibility), # execution_section
keyword # keyword_state
)
def wrapper_analyze_with_loading(analysis_keywords, selected_category, state_df):
"""뢄석 λ‘œλ”© UI 처리"""
analyze_with_loading(analysis_keywords, selected_category)
return gr.update(visible=True) # progress_section
def wrapper_process_analyze_results(analysis_keywords, selected_category, state_df):
"""뢄석 κ²°κ³Ό 처리 UI"""
analysis_result, download_file = process_analyze_results(analysis_keywords, selected_category)
return (
analysis_result, # analysis_result
download_file, # download_output
gr.update(visible=True), # analysis_output_section
gr.update(visible=False) # progress_section
)
# μ„Έμ…˜ 정리 μŠ€μΌ€μ€„λŸ¬
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("🧹 컨트둀 νƒ€μ›Œ μ• ν”Œλ¦¬μΌ€μ΄μ…˜ μ‹œμž‘ - 초기 정리 μž‘μ—… μ‹œμž‘...")
cleanup_huggingface_temp_folders()
app_temp_dir = setup_clean_temp_environment()
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;
}
"""
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)
# ν‚€μ›Œλ“œ μƒνƒœ 관리
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()
# 이벀트 μ—°κ²°
search_btn.click(
fn=wrapper_search_with_loading,
inputs=[keyword, korean_only, apply_main_keyword, exclude_zero_volume],
outputs=[progress_section, empty_table_html]
).then(
fn=wrapper_process_search_results,
inputs=[keyword, korean_only, apply_main_keyword, exclude_zero_volume],
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
]
)
# ν•„ν„° 및 μ •λ ¬ λ³€κ²½ 이벀트 μ—°κ²°
category_filter.change(
fn=filter_and_sort_table,
inputs=[
category_filter, gr.Textbox(value="μ •λ ¬ μ—†μŒ", visible=False),
total_volume_sort, usage_count_sort,
search_volume_filter, exclude_zero_volume
],
outputs=[table_output]
)
category_filter.change(
fn=update_category_selection,
inputs=[category_filter],
outputs=[selected_category]
)
total_volume_sort.change(
fn=filter_and_sort_table,
inputs=[
category_filter, gr.Textbox(value="μ •λ ¬ μ—†μŒ", visible=False),
total_volume_sort, usage_count_sort,
search_volume_filter, exclude_zero_volume
],
outputs=[table_output]
)
usage_count_sort.change(
fn=filter_and_sort_table,
inputs=[
category_filter, gr.Textbox(value="μ •λ ¬ μ—†μŒ", visible=False),
total_volume_sort, usage_count_sort,
search_volume_filter, exclude_zero_volume
],
outputs=[table_output]
)
search_volume_filter.change(
fn=filter_and_sort_table,
inputs=[
category_filter, gr.Textbox(value="μ •λ ¬ μ—†μŒ", visible=False),
total_volume_sort, usage_count_sort,
search_volume_filter, exclude_zero_volume
],
outputs=[table_output]
)
exclude_zero_volume.change(
fn=filter_and_sort_table,
inputs=[
category_filter, gr.Textbox(value="μ •λ ¬ μ—†μŒ", visible=False),
total_volume_sort, usage_count_sort,
search_volume_filter, exclude_zero_volume
],
outputs=[table_output]
)
# μΉ΄ν…Œκ³ λ¦¬ 뢄석 λ²„νŠΌ 이벀트
analyze_btn.click(
fn=wrapper_analyze_with_loading,
inputs=[analysis_keywords, selected_category, state_df],
outputs=[progress_section]
).then(
fn=wrapper_process_analyze_results,
inputs=[analysis_keywords, selected_category, state_df],
outputs=[analysis_result, download_output, analysis_output_section, progress_section]
)
# 리셋 λ²„νŠΌ 이벀트 μ—°κ²°
reset_btn.click(
fn=reset_interface,
inputs=[],
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__":
# ========== μ‹œμž‘ μ‹œ 전체 μ΄ˆκΈ°ν™” ==========
print("===== Application Startup at %s =====" % time.strftime("%Y-%m-%d %H:%M:%S"))
logger.info("πŸš€ 컨트둀 νƒ€μ›Œ μ• ν”Œλ¦¬μΌ€μ΄μ…˜ μ‹œμž‘...")
# 1. 첫 번째: ν—ˆκΉ…νŽ˜μ΄μŠ€ μž„μ‹œ 폴더 정리 및 ν™˜κ²½ μ„€μ •
app_temp_dir = cleanup_on_startup()
# 2. μ„Έμ…˜ 정리 μŠ€μΌ€μ€„λŸ¬ μ‹œμž‘
start_session_cleanup_scheduler()
# 3. API μ—°κ²° ν…ŒμŠ€νŠΈ
try:
test_client = get_api_client()
logger.info("βœ… API μ—°κ²° ν…ŒμŠ€νŠΈ 성곡")
except Exception as e:
logger.error("❌ API μ—°κ²° μ‹€νŒ¨ - ν™˜κ²½λ³€μˆ˜ API_ENDPOINTλ₯Ό ν™•μΈν•˜μ„Έμš”")
print("❌ API_ENDPOINT ν™˜κ²½λ³€μˆ˜κ°€ μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€.")
print("πŸ’‘ .env νŒŒμΌμ— λ‹€μŒκ³Ό 같이 μ„€μ •ν•˜μ„Έμš”:")
print("API_ENDPOINT=your-endpoint-here")
raise SystemExit(1)
logger.info("===== 컨트둀 νƒ€μ›Œ μ• ν”Œλ¦¬μΌ€μ΄μ…˜ μ‹œμž‘ μ™„λ£Œ at %s =====", time.strftime("%Y-%m-%d %H:%M:%S"))
logger.info(f"πŸ“ μž„μ‹œ 파일 μ €μž₯ μœ„μΉ˜: {app_temp_dir}")
# ========== μ•± μ‹€ν–‰ ==========
try:
app = create_app()
print("πŸš€ Gradio μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ΄ μ‹œμž‘λ©λ‹ˆλ‹€...")
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 검증 λΉ„ν™œμ„±ν™” (개발용)
inbrowser=False, # μžλ™ λΈŒλΌμš°μ € μ—΄κΈ° λΉ„ν™œμ„±ν™”
prevent_thread_lock=False # μŠ€λ ˆλ“œ 잠금 λ°©μ§€ λΉ„ν™œμ„±ν™”
)
except Exception as e:
logger.error(f"μ• ν”Œλ¦¬μΌ€μ΄μ…˜ μ‹€ν–‰ μ‹€νŒ¨: {e}")
print(f"❌ μ• ν”Œλ¦¬μΌ€μ΄μ…˜ μ‹€ν–‰ μ‹€νŒ¨: {e}")
raise SystemExit(1)
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}")