F23ASFGGD / app.py
ssboost's picture
Update app.py
d6c8aa6 verified
raw
history blame
32.7 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
# λ‘œκΉ… μ„€μ •
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_client():
# ν™˜κ²½λ³€μˆ˜μ—μ„œ API μ—”λ“œν¬μΈνŠΈ 읽기
endpoint = os.environ.get('API_ENDPOINT', '')
# ν™˜κ²½λ³€μˆ˜μ— 파일 λ‚΄μš©μ΄λ‚˜ λΆˆν•„μš”ν•œ ν…μŠ€νŠΈκ°€ λ“€μ–΄μ˜¨ 경우 정리
if endpoint:
# μ€„λ°”κΏˆμœΌλ‘œ λΆ„λ¦¬ν•΄μ„œ 첫 번째 μœ νš¨ν•œ 라인 μ°ΎκΈ°
lines = endpoint.split('\n')
for line in lines:
line = line.strip()
# μ£Όμ„μ΄λ‚˜ λΉ„μ–΄μžˆλŠ” 라인 μ œμ™Έ
if line and not line.startswith('#') and '/' in line:
# API_ENDPOINT= 같은 ν‚€κ°’ 제거
if '=' in line:
line = line.split('=', 1)[1].strip()
# λ”°μ˜΄ν‘œ 제거
line = line.strip('"\'')
if line and '/' in line and len(line) < 50:
return Client(line)
raise ValueError("μ˜¬λ°”λ₯Έ API_ENDPOINTλ₯Ό μ„€μ •ν•΄μ£Όμ„Έμš” (예: username/repo-name)")
# μ„Έμ…˜λ³„ μž„μ‹œ 파일 관리λ₯Ό μœ„ν•œ λ”•μ…”λ„ˆλ¦¬
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:
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}개 파일 μ‚­μ œ")
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:
cleanup_huggingface_temp_folders()
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)
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:
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_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 응닡 확인 및 처리 (6개 κ°’)
logger.info(f"API 응닡 νƒ€μž…: {type(result)}, 길이: {len(result) if isinstance(result, (list, tuple)) else 'N/A'}")
logger.info(f"API 응닡 λ‚΄μš©: {result}")
if isinstance(result, (list, tuple)) and len(result) >= 6:
table_html, cat_choices, vol_choices, selected_cat, download_file, extra = result[:6]
logger.info(f"table_html νƒ€μž…: {type(table_html)}")
logger.info(f"cat_choices: {cat_choices}")
logger.info(f"vol_choices: {vol_choices}")
logger.info(f"selected_cat: {selected_cat}")
logger.info(f"download_file: {download_file}")
elif isinstance(result, (list, tuple)) and len(result) >= 5:
table_html, cat_choices, vol_choices, selected_cat, download_file = result[:5]
extra = None
else:
# 응닡이 μ˜ˆμƒκ³Ό λ‹€λ₯Έ 경우 κΈ°λ³Έκ°’ μ‚¬μš©
logger.warning(f"μ˜ˆμƒκ³Ό λ‹€λ₯Έ API 응닡: {result}")
table_html = "<p>검색 κ²°κ³Όλ₯Ό μ²˜λ¦¬ν•˜λŠ” 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.</p>"
cat_choices = ["전체 보기"]
vol_choices = ["전체"]
selected_cat = "전체 보기"
download_file = None
# table_html 처리 (dict인 경우 value μΆ”μΆœ)
if isinstance(table_html, dict) and 'value' in table_html:
table_html = table_html['value']
elif isinstance(table_html, dict):
table_html = str(table_html) # dictλ₯Ό λ¬Έμžμ—΄λ‘œ λ³€ν™˜
# choices ν˜•μ‹ 처리 (쀑첩 리슀트인 경우 첫 번째 κ°’λ§Œ μ‚¬μš©)
if isinstance(cat_choices, dict) and 'choices' in cat_choices:
cat_choices = [choice[0] if isinstance(choice, list) else choice for choice in cat_choices['choices']]
elif isinstance(cat_choices, list) and cat_choices and isinstance(cat_choices[0], list):
cat_choices = [choice[0] for choice in cat_choices]
if isinstance(vol_choices, dict) and 'choices' in vol_choices:
vol_choices = [choice[0] if isinstance(choice, list) else choice for choice in vol_choices['choices']]
elif isinstance(vol_choices, list) and vol_choices and isinstance(vol_choices[0], list):
vol_choices = [choice[0] for choice in vol_choices]
# selected_cat 처리
if isinstance(selected_cat, dict) and 'value' in selected_cat:
selected_cat = selected_cat['value']
elif isinstance(selected_cat, list):
selected_cat = selected_cat[0] if selected_cat else "전체 보기"
logger.info(f"처리된 cat_choices: {cat_choices}")
logger.info(f"처리된 vol_choices: {vol_choices}")
logger.info(f"처리된 selected_cat: {selected_cat}")
local_file = None
if download_file:
try:
local_file = create_session_temp_file(session_id, '.xlsx')
shutil.copy(download_file, local_file)
logger.info(f"파일 볡사 μ™„λ£Œ: {local_file}")
except Exception as file_error:
logger.error(f"파일 볡사 였λ₯˜: {file_error}")
return (
table_html,
gr.update(choices=cat_choices),
gr.update(choices=vol_choices),
None,
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}")
import traceback
logger.error(f"상세 였λ₯˜: {traceback.format_exc()}")
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_client()
result = client.predict(
analysis_keywords=analysis_keywords,
selected_category=selected_category,
api_name="/process_analyze_results"
)
# API 응닡 확인 및 처리
logger.info(f"뢄석 API 응닡 νƒ€μž…: {type(result)}, 길이: {len(result) if isinstance(result, (list, tuple)) else 'N/A'}")
if isinstance(result, (list, tuple)) and len(result) >= 2:
analysis_result, download_file = result[:2]
elif isinstance(result, str):
analysis_result = result
download_file = None
else:
logger.warning(f"μ˜ˆμƒκ³Ό λ‹€λ₯Έ 뢄석 API 응닡: {result}")
analysis_result = "뢄석 κ²°κ³Όλ₯Ό μ²˜λ¦¬ν•˜λŠ” 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€."
download_file = None
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_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)
logger.info(f"μΉ΄ν…Œκ³ λ¦¬ 선택 λ³€κ²½: {selected_cat}")
# λ‘œμ»¬μ—μ„œ 직접 처리 (API 호좜 없이)
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_client()
result = client.predict(api_name="/reset_interface")
return result
except Exception as e:
logger.error(f"리셋 API 호좜 였λ₯˜: {e}")
return (
"", True, False, "λ©”μΈν‚€μ›Œλ“œ 적용", "", ["전체 보기"], "전체 보기",
["전체"], "전체", "μ •λ ¬ μ—†μŒ", "μ •λ ¬ μ—†μŒ", None, ["전체 보기"],
"전체 보기", "", "", None, gr.update(visible=False),
gr.update(visible=False), ""
)
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 table_html and table_html.get('value') if isinstance(table_html, dict) else table_html:
empty_placeholder_vis = False
keyword_section_visibility = True
execution_section_visibility = True
logger.info("ν…Œμ΄λΈ” 데이터가 있음 - μ„Ήμ…˜λ“€μ„ ν‘œμ‹œν•©λ‹ˆλ‹€")
else:
empty_placeholder_vis = True
keyword_section_visibility = False
execution_section_visibility = False
logger.info("ν…Œμ΄λΈ” 데이터가 μ—†μŒ - κΈ°λ³Έ μƒνƒœλ₯Ό μœ μ§€ν•©λ‹ˆλ‹€")
return (
table_html, cat_choices, vol_choices, df, selected_cat, excel,
gr.update(visible=keyword_section_visibility),
gr.update(visible=True), # μΉ΄ν…Œκ³ λ¦¬ 뢄석 μ„Ήμ…˜μ€ 항상 ν‘œμ‹œ
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)
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
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">
"""
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)
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()
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, # selected_categoryκ°€ λΆ„μ„μš© μΉ΄ν…Œκ³ λ¦¬
keyword_analysis_section, category_analysis_section,
progress_section, empty_table_html, execution_section,
keyword_state
]
)
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]
)
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]
)
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("πŸš€ λ©”μΈν‚€μ›Œλ“œ 뢄석 μ• ν”Œλ¦¬μΌ€μ΄μ…˜ μ‹œμž‘...")
app_temp_dir = cleanup_on_startup()
start_session_cleanup_scheduler()
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,
server_name="0.0.0.0",
server_port=7860,
max_threads=40,
auth=None,
show_error=True,
quiet=False,
favicon_path=None,
ssl_verify=False
)
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}")