4G23WAS2 / app.py
ssboost's picture
Update app.py
f00f5cb verified
import gradio as gr
import pandas as pd
import os
import time
import threading
import tempfile
import logging
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('category_analysis_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, "*category*.xlsx")))
session_files.extend(glob.glob(os.path.join(temp_dir, "*category*.csv")))
session_files.extend(glob.glob(os.path.join(temp_dir, "*analysis*.xlsx")))
session_files.extend(glob.glob(os.path.join(temp_dir, "*analysis*.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(), "category_analysis_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['CATEGORY_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('CATEGORY_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(time.time_ns())[-4:]
# μ• ν”Œλ¦¬μΌ€μ΄μ…˜ μ „μš© μž„μ‹œ 디렉토리 μ‚¬μš©
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 analyze_product_terms_wrapper(product_name, main_keyword, current_state, session_id):
"""μƒν’ˆλͺ… ν‚€μ›Œλ“œ 뢄석 래퍼 ν•¨μˆ˜ - μ„Έμ…˜ ID μΆ”κ°€"""
update_session_activity(session_id)
if not product_name:
return "μƒν’ˆλͺ…을 μž…λ ₯ν•΄μ£Όμ„Έμš”.", current_state, None, gr.update(visible=False)
# 뢄석 μˆ˜ν–‰ - HTML 결과와 ν‚€μ›Œλ“œ 뢄석 κ²°κ³Ό ν•¨κ»˜ λ°›κΈ°
result_html, keyword_results = category_analysis.analyze_product_terms(product_name, main_keyword)
# μƒˆλ‘œμš΄ μƒνƒœ 생성
if current_state is None or not isinstance(current_state, dict):
current_state = {}
# 뢄석 κ²°κ³Όλ₯Ό μƒνƒœμ— μΆ”κ°€
current_state["keyword_analysis_results"] = keyword_results
current_state["product_name"] = product_name
current_state["main_keyword"] = main_keyword
# μ„Έμ…˜λ³„ μ—‘μ…€ 파일 λ‹€μš΄λ‘œλ“œ - μžλ™ λ‹€μš΄λ‘œλ“œ
excel_path = download_analysis(current_state, session_id)
# 좜λ ₯ μ„Ήμ…˜ ν‘œμ‹œ
return result_html, current_state, excel_path, gr.update(visible=True)
def download_analysis(result, session_id):
"""μΉ΄ν…Œκ³ λ¦¬ 뢄석 κ²°κ³Ό λ‹€μš΄λ‘œλ“œ (μ„Έμ…˜λ³„)"""
update_session_activity(session_id)
if not result or not isinstance(result, dict):
logger.warning(f"μ„Έμ…˜ {session_id[:8]}... 뢄석 κ²°κ³Όκ°€ μ—†μŠ΅λ‹ˆλ‹€.")
return None
try:
# μƒν’ˆλͺ… 뢄석 κ²°κ³Όκ°€ μžˆλŠ”μ§€ 확인
if "keyword_analysis_results" in result:
logger.info(f"μ„Έμ…˜ {session_id[:8]}... ν‚€μ›Œλ“œ 뢄석 κ²°κ³Ό ν¬ν•¨ν•˜μ—¬ λ‹€μš΄λ‘œλ“œ: {len(result['keyword_analysis_results'])}개 ν‚€μ›Œλ“œ")
# μ„Έμ…˜λ³„ μž„μ‹œ 파일 생성
temp_filename = create_session_temp_file(session_id, '.xlsx')
# λ°μ΄ν„°ν”„λ ˆμž„ 생성
keywords = []
pc_volumes = []
mobile_volumes = []
total_volumes = []
ranges = []
category_items = []
for kw_result in result["keyword_analysis_results"]:
keywords.append(kw_result.get("ν‚€μ›Œλ“œ", ""))
pc_volumes.append(kw_result.get("PCκ²€μƒ‰λŸ‰", 0))
mobile_volumes.append(kw_result.get("λͺ¨λ°”μΌκ²€μƒ‰λŸ‰", 0))
total_volumes.append(kw_result.get("μ΄κ²€μƒ‰λŸ‰", 0))
ranges.append(kw_result.get("κ²€μƒ‰λŸ‰κ΅¬κ°„", ""))
category_items.append(kw_result.get("μΉ΄ν…Œκ³ λ¦¬ν•­λͺ©", ""))
# λ°μ΄ν„°ν”„λ ˆμž„μœΌλ‘œ λ³€ν™˜
df = pd.DataFrame({
"ν‚€μ›Œλ“œ": keywords,
"PCκ²€μƒ‰λŸ‰": pc_volumes,
"λͺ¨λ°”μΌκ²€μƒ‰λŸ‰": mobile_volumes,
"μ΄κ²€μƒ‰λŸ‰": total_volumes,
"κ²€μƒ‰λŸ‰κ΅¬κ°„": ranges,
"μΉ΄ν…Œκ³ λ¦¬ν•­λͺ©": category_items
})
with pd.ExcelWriter(temp_filename, engine="xlsxwriter") as writer:
df.to_excel(writer, sheet_name="μƒν’ˆλͺ… 검증 κ²°κ³Ό", index=False)
ws = writer.sheets["μƒν’ˆλͺ… 검증 κ²°κ³Ό"]
# μ€„λ°”κΏˆ + μœ„μͺ½ μ •λ ¬ μ„œμ‹
wrap_fmt = writer.book.add_format({
"text_wrap": True,
"valign": "top"
})
# Fμ—΄('μΉ΄ν…Œκ³ λ¦¬ν•­λͺ©') 전체에 μ„œμ‹ 적용 + μ—΄ λ„ˆλΉ„
ws.set_column("F:F", 40, wrap_fmt)
# μ—΄ λ„ˆλΉ„ μ„€μ •
worksheet = writer.sheets['μƒν’ˆλͺ… 검증 κ²°κ³Ό']
worksheet.set_column('A:A', 20) # ν‚€μ›Œλ“œ
worksheet.set_column('B:B', 12) # PCκ²€μƒ‰λŸ‰
worksheet.set_column('C:C', 12) # λͺ¨λ°”μΌκ²€μƒ‰λŸ‰
worksheet.set_column('D:D', 12) # μ΄κ²€μƒ‰λŸ‰
worksheet.set_column('E:E', 12) # κ²€μƒ‰λŸ‰κ΅¬κ°„
worksheet.set_column('F:F', 40) # μΉ΄ν…Œκ³ λ¦¬ν•­λͺ©
# 헀더 μ„œμ‹ μ§€μ •
header_format = writer.book.add_format({
'bold': True,
'bg_color': '#FB7F0D',
'color': 'white',
'border': 1
})
# 헀더에 μ„œμ‹ 적용
for col_num, value in enumerate(df.columns.values):
worksheet.write(0, col_num, value, header_format)
logger.info(f"μ„Έμ…˜ {session_id[:8]}... μ—‘μ…€ 파일 μ €μž₯ μ™„λ£Œ: {temp_filename}")
return temp_filename
else:
logger.warning(f"μ„Έμ…˜ {session_id[:8]}... ν‚€μ›Œλ“œ 뢄석 κ²°κ³Όκ°€ μ—†μŠ΅λ‹ˆλ‹€.")
return None
except Exception as e:
logger.error(f"μ„Έμ…˜ {session_id[:8]}... λ‹€μš΄λ‘œλ“œ 쀑 였λ₯˜ λ°œμƒ: {e}")
import traceback
logger.error(traceback.format_exc())
return None
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 (
"", # 메인 ν‚€μ›Œλ“œ μž…λ ₯
"", # μƒν’ˆλͺ… μž…λ ₯
"", # 뢄석 κ²°κ³Ό 좜λ ₯
None, # λ‹€μš΄λ‘œλ“œ 파일
None, # μƒνƒœ λ³€μˆ˜
gr.update(visible=False) # 뢄석 κ²°κ³Ό μ„Ήμ…˜
)
def product_analyze_with_loading(product_name, main_keyword, current_state, session_id):
"""λ‘œλ”© ν‘œμ‹œ ν•¨μˆ˜"""
update_session_activity(session_id)
return gr.update(visible=True)
def process_product_analyze(product_name, main_keyword, current_state, session_id):
"""μ‹€μ œ 뢄석 μˆ˜ν–‰"""
update_session_activity(session_id)
results = analyze_product_terms_wrapper(product_name, main_keyword, current_state, 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 μ•„μ΄μ½˜ 포함
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:
# CSS 파일이 μ—†λŠ” 경우 κΈ°λ³Έ μŠ€νƒ€μΌ μ‚¬μš©
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%; }
}
.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)
# μž…λ ₯ μ„Ήμ…˜
with gr.Column(elem_classes="custom-frame fade-in"):
gr.HTML('<div class="section-title"><i class="fas fa-tag"></i> μƒν’ˆλͺ… 뢄석 μž…λ ₯</div>')
# 메인 ν‚€μ›Œλ“œμ™€ μƒν’ˆλͺ… μž…λ ₯을 ν•œ 쀄에 배치
with gr.Row():
with gr.Column(scale=1):
main_keyword = gr.Textbox(
label="메인 ν‚€μ›Œλ“œ",
placeholder="예: μ˜€μ§•μ–΄"
)
with gr.Column(scale=1):
product_name = gr.Textbox(
label="μƒν’ˆλͺ…",
placeholder="예: μ†μ§ˆ μ˜€μ§•μ–΄ μ΄‰μ΄‰ν•œ 진미채"
)
# μ‹€ν–‰ μ„Ήμ…˜ - λ²„νŠΌ 톡합
with gr.Column(elem_classes="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_product_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 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", visible=False) as product_analysis_section:
gr.HTML('<div class="section-title"><i class="fas fa-table"></i> μƒν’ˆλͺ… ν‚€μ›Œλ“œ 뢄석 κ²°κ³Ό</div>')
# μƒν’ˆλͺ… 뢄석 κ²°κ³Ό
product_analysis_result = gr.HTML(elem_classes="fade-in")
# μ—‘μ…€ λ‹€μš΄λ‘œλ“œ 파일
download_file = gr.File(
label="뢄석 κ²°κ³Ό λ‹€μš΄λ‘œλ“œ",
visible=True
)
# μƒνƒœ μ €μž₯용 λ³€μˆ˜ - 뢄석 κ²°κ³Ό μ €μž₯
analysis_result_state = gr.State()
# μƒν’ˆλͺ… 뢄석 λ²„νŠΌ μ—°κ²° - λ‘œλ”© ν‘œμ‹œ ν›„ μžλ™ λ‹€μš΄λ‘œλ“œ (μ„Έμ…˜ ID μΆ”κ°€)
analyze_product_btn.click(
fn=product_analyze_with_loading,
inputs=[product_name, main_keyword, analysis_result_state, session_id],
outputs=[progress_section]
).then(
fn=process_product_analyze,
inputs=[product_name, main_keyword, analysis_result_state, session_id],
outputs=[
product_analysis_result, analysis_result_state,
download_file, product_analysis_section, progress_section
]
)
# 리셋 λ²„νŠΌ 이벀트 μ—°κ²° (μ„Έμ…˜ ID μΆ”κ°€)
reset_btn.click(
fn=reset_interface,
inputs=[session_id],
outputs=[
main_keyword, product_name, product_analysis_result,
download_file, analysis_result_state, product_analysis_section
]
)
return demo
if __name__ == "__main__":
# ========== μ‹œμž‘ μ‹œ 전체 μ΄ˆκΈ°ν™” ==========
logger.info("πŸš€ μΉ΄ν…Œκ³ λ¦¬ 뢄석 μ• ν”Œλ¦¬μΌ€μ΄μ…˜ μ‹œμž‘...")
# 1. 첫 번째: ν—ˆκΉ…νŽ˜μ΄μŠ€ μž„μ‹œ 폴더 정리 및 ν™˜κ²½ μ„€μ •
app_temp_dir = cleanup_on_startup()
# 2. μ„Έμ…˜ 정리 μŠ€μΌ€μ€„λŸ¬ μ‹œμž‘
start_session_cleanup_scheduler()
# 3. API μ„€μ • μ΄ˆκΈ°ν™”
api_utils.initialize_api_configs()
# 4. Gemini λͺ¨λΈ μ΄ˆκΈ°ν™”
gemini_model = text_utils.get_gemini_model()
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}")