#app.py import gradio as gr import pandas as pd import openpyxl import datetime import tempfile import os import uuid import time from openpyxl.utils.dataframe import dataframe_to_rows import pytz import random import google.generativeai as genai # 환경변수에서 코드 불러오기 api_manager_code = os.getenv('API_MANAGER_CODE', '') styles_code = os.getenv('STYLES_CODE', '') review_analyzer_code = os.getenv('REVIEW_ANALYZER_CODE', '') # 동적으로 코드 실행하여 클래스 정의 exec(api_manager_code) exec(review_analyzer_code) # 스타일 정보 불러오기 fontawesome_link = """ """ # 환경변수에서 CSS 불러오기 custom_css = os.getenv('CUSTOM_CSS', ''' :root { --primary-color: #FB7F0D; --secondary-color: #ff9a8b; --accent-color: #FF6B6B; --background-color: #FFF3E9; --card-bg: #ffffff; --text-color: #334155; --border-radius: 18px; --shadow: 0 8px 30px rgba(251, 127, 13, 0.08); } body { font-family: 'Pretendard', 'Noto Sans KR', -apple-system, BlinkMacSystemFont, sans-serif; background-color: var(--background-color); color: var(--text-color); line-height: 1.6; margin: 0; padding: 0; } .gradio-container { width: 100%; margin: 0 auto; padding: 20px; background-color: var(--background-color); } .custom-header { background: #FF7F00; padding: 2rem; border-radius: 15px; margin-bottom: 20px; box-shadow: var(--shadow); text-align: center; } .custom-header h1 { margin: 0; font-size: 2.5rem; font-weight: 700; color: black; } .custom-header p { margin: 10px 0 0; font-size: 1.2rem; color: black; } .custom-frame { background-color: var(--card-bg); border: 1px solid rgba(0, 0, 0, 0.04); border-radius: var(--border-radius); padding: 20px; margin: 10px 0; box-shadow: var(--shadow); } .custom-button { border-radius: 30px !important; background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)) !important; color: white !important; font-size: 18px !important; padding: 10px 20px !important; border: none; box-shadow: 0 4px 8px rgba(251, 127, 13, 0.25); transition: transform 0.3s ease; } .custom-button:hover { transform: translateY(-2px); box-shadow: 0 6px 12px rgba(251, 127, 13, 0.3); } .custom-title { font-size: 28px; font-weight: bold; margin-bottom: 10px; color: var(--text-color); border-bottom: 2px solid var(--primary-color); padding-bottom: 5px; } .gr-tabs-panel { background-color: var(--background-color) !important; box-shadow: none !important; } .custom-section-group { background-color: var(--background-color) !important; box-shadow: none !important; } .gr-textbox textarea, .gr-textbox input { border-radius: 12px !important; border: 2px solid rgba(251, 127, 13, 0.2) !important; font-size: 14px; transition: border-color 0.3s ease; } .gr-textbox textarea:focus, .gr-textbox input:focus { border-color: var(--primary-color) !important; box-shadow: 0 0 0 3px rgba(251, 127, 13, 0.1) !important; } .gr-file { border-radius: var(--border-radius) !important; border: 2px dashed var(--primary-color) !important; background-color: rgba(251, 127, 13, 0.05) !important; transition: all 0.3s ease; } .gr-file:hover { background-color: rgba(251, 127, 13, 0.1) !important; border-color: var(--secondary-color) !important; } @media (max-width: 768px) { .gradio-container { padding: 10px; } .custom-header h1 { font-size: 2rem; } .custom-title { font-size: 24px; } .custom-frame { padding: 15px; margin: 5px 0; } .custom-button { font-size: 16px !important; padding: 8px 16px !important; } } ''') class SessionManager: """세션별 데이터 관리""" def __init__(self): self.sessions = {} def create_session(self): """새로운 세션 생성""" session_id = str(uuid.uuid4()) self.sessions[session_id] = { 'created_at': time.time(), 'data': {}, 'temp_files': [] } return session_id def get_session_data(self, session_id, key, default=None): """세션 데이터 조회""" if session_id in self.sessions: return self.sessions[session_id]['data'].get(key, default) return default def set_session_data(self, session_id, key, value): """세션 데이터 저장""" if session_id in self.sessions: self.sessions[session_id]['data'][key] = value def add_temp_file(self, session_id, file_path): """세션에 임시 파일 추가""" if session_id in self.sessions: self.sessions[session_id]['temp_files'].append(file_path) def cleanup_session(self, session_id): """세션 정리""" if session_id in self.sessions: # 임시 파일들 삭제 for file_path in self.sessions[session_id]['temp_files']: try: if os.path.exists(file_path): os.remove(file_path) except Exception as e: print(f"파일 삭제 오류: {e}") # 세션 데이터 삭제 del self.sessions[session_id] def cleanup_old_sessions(self, max_age_hours=2): """오래된 세션 정리 (2시간 이상)""" current_time = time.time() old_sessions = [] for session_id, session_data in self.sessions.items(): if (current_time - session_data['created_at']) > (max_age_hours * 3600): old_sessions.append(session_id) for session_id in old_sessions: self.cleanup_session(session_id) # 전역 세션 매니저 session_manager = SessionManager() def create_app(): """메인 Gradio 애플리케이션 생성""" demo = 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"] )) with demo: gr.HTML(fontawesome_link) # 세션 ID를 숨겨진 상태로 관리 session_id_state = gr.State(value="") # 세션 초기화 함수 def initialize_session(): session_id = session_manager.create_session() # 각 세션마다 새로운 분석기 인스턴스 생성 api_manager = APIManager() analyzer = ReviewAnalyzer(api_manager) session_manager.set_session_data(session_id, 'analyzer', analyzer) return session_id # 세션별 분석기 가져오기 def get_session_analyzer(session_id): if not session_id: session_id = initialize_session() analyzer = session_manager.get_session_data(session_id, 'analyzer') if analyzer is None: api_manager = APIManager() analyzer = ReviewAnalyzer(api_manager) session_manager.set_session_data(session_id, 'analyzer', analyzer) return analyzer, session_id # 탭 구성 with gr.Tabs() as tabs: ############################# # 엑셀 분석 모드 ############################# with gr.TabItem("💾 스마트스토어 엑셀리뷰데이터 활용"): with gr.Row(): with gr.Column(elem_classes="custom-frame"): gr.HTML("
📁 데이터 입력
") file_input = gr.File(label="원본 엑셀 파일 업로드", file_types=[".xlsx"]) year_radio = gr.Radio( choices=[f"{str(y)[-2:]}년" for y in range(2025, 2020, -1)], label="분석년도 선택", value="25년" ) analyze_button = gr.Button("옵션 분석하기", elem_classes="custom-button") with gr.Column(elem_classes="custom-frame"): gr.HTML("
📁 분석보고서 다운로드
") download_final_output = gr.File(label="보고서 다운로드") # 리뷰분석 섹션 with gr.Column(elem_classes="custom-frame", visible=False) as review_analysis_frame: gr.HTML("
📁 리뷰분석
") top20_dropdown = gr.Dropdown( label="아이템옵션 분석", choices=["전체옵션분석"], value="전체옵션분석" ) review_button = gr.Button("리뷰 분석하기", elem_classes="custom-button") # 분석 결과 섹션들 with gr.Row(): with gr.Column(elem_classes="custom-frame"): gr.HTML("
✨ 주요긍정리뷰
") positive_output = gr.Textbox(label="긍정리뷰리스트 (20개)", lines=10, value="") with gr.Column(elem_classes="custom-frame"): gr.HTML("
✨ 주요부정리뷰
") negative_output = gr.Textbox(label="부정리뷰리스트 (30개)", lines=10, value="") with gr.Row(): with gr.Column(elem_classes="custom-frame"): gr.HTML("
📢 긍정리뷰 분석
") positive_analysis_output = gr.Textbox(label="긍정리뷰 분석", lines=8, value="") with gr.Column(elem_classes="custom-frame"): gr.HTML("
📢 부정리뷰 분석
") negative_analysis_output = gr.Textbox(label="부정리뷰 분석", lines=8, value="") with gr.Row(): with gr.Column(elem_classes="custom-frame"): gr.HTML("
📊 니즈원인 분석
") insight_analysis_output = gr.Textbox(label="니즈원인 분석", lines=8, value="") with gr.Column(elem_classes="custom-frame"): gr.HTML("
🔧 상품판매방향성
") strategy_analysis_output = gr.Textbox(label="상품판매방향성", lines=8, value="") with gr.Row(): with gr.Column(elem_classes="custom-frame"): gr.HTML("
📝 소싱전략
") sourcing_analysis_output = gr.Textbox(label="소싱전략", lines=8, value="") with gr.Column(elem_classes="custom-frame"): gr.HTML("
🖼️ 마케팅전략
") detail_page_analysis_output = gr.Textbox(label="마케팅전략", lines=8, value="") # 세션별 상태 관리를 위한 숨겨진 상태 partial_file_state = gr.State(value=None) # 옵션 분석 이벤트 핸들러 def on_analyze_options(uploaded_file, selected_year, session_id): analyzer, session_id = get_session_analyzer(session_id) try: result = analyzer.analyze_options(uploaded_file, selected_year) print(f"analyze_options 결과 타입: {type(result)}") print(f"analyze_options 결과 내용: {result}") partial_file = None top20_list = ["전체옵션분석"] if result is not None: if isinstance(result, (list, tuple)) and len(result) >= 2: partial_file = result[0] top20_list = result[1] if result[1] else ["전체옵션분석"] elif hasattr(result, '__getitem__'): try: partial_file = result[0] top20_list = result[1] if len(result) > 1 else ["전체옵션분석"] except (IndexError, KeyError, TypeError): print("결과 인덱싱 실패, 기본값 사용") if not isinstance(top20_list, list): top20_list = ["전체옵션분석"] if len(top20_list) == 0: top20_list = ["전체옵션분석"] if partial_file: session_manager.add_temp_file(session_id, partial_file) return ( partial_file, gr.update(visible=True if partial_file else False), gr.update(choices=top20_list, value=top20_list[0]), session_id ) except Exception as e: print(f"옵션 분석 오류: {e}") import traceback traceback.print_exc() return None, gr.update(visible=False), gr.update(choices=["전체옵션분석"], value="전체옵션분석"), session_id analyze_button.click( fn=on_analyze_options, inputs=[file_input, year_radio, session_id_state], outputs=[partial_file_state, review_analysis_frame, top20_dropdown, session_id_state] ) # 리뷰 분석 이벤트 핸들러 def on_analyze_reviews(partial_file, selected_option, session_id): analyzer, session_id = get_session_analyzer(session_id) try: result = analyzer.analyze_reviews(partial_file, selected_option) if isinstance(result, tuple) and len(result) >= 9: results = result else: results = (None, "", "", "", "", "", "", "", "") final_file = results[0] if final_file: session_manager.add_temp_file(session_id, final_file) return results + (session_id,) except Exception as e: print(f"리뷰 분석 오류: {e}") import traceback traceback.print_exc() return (None, "", "", "", "", "", "", "", "", session_id) review_button.click( fn=on_analyze_reviews, inputs=[partial_file_state, top20_dropdown, session_id_state], outputs=[download_final_output, positive_output, negative_output, positive_analysis_output, negative_analysis_output, insight_analysis_output, strategy_analysis_output, sourcing_analysis_output, detail_page_analysis_output, session_id_state] ) ############################# # 직접 입력 분석 모드 ############################# with gr.TabItem("📖 직접 입력한 자료활용"): with gr.Row(): with gr.Column(elem_classes="custom-frame"): gr.HTML("
📝 리뷰 직접 입력
") direct_positive_input = gr.Textbox( label="긍정 리뷰 입력", placeholder="긍정 리뷰를 여기에 입력하세요.(최대 8000자)", lines=10, max_length=8000, value="" ) direct_negative_input = gr.Textbox( label="부정 리뷰 입력", placeholder="부정 리뷰를 여기에 입력하세요.(최대 8000자)", lines=10, max_length=8000, value="" ) direct_review_button = gr.Button("리뷰 분석하기", elem_classes="custom-button") with gr.Column(elem_classes="custom-frame"): gr.HTML("
📁 분석보고서 다운로드
") direct_download_output = gr.File(label="분석 보고서 다운로드") # 직접 입력 분석 결과 with gr.Row(): with gr.Column(elem_classes="custom-frame"): gr.HTML("
📢 긍정리뷰분석
") direct_positive_analysis_output = gr.Textbox( label="긍정리뷰분석", lines=8, value="" ) with gr.Column(elem_classes="custom-frame"): gr.HTML("
📢 부정리뷰분석
") direct_negative_analysis_output = gr.Textbox( label="부정리뷰분석", lines=8, value="" ) with gr.Row(): with gr.Column(elem_classes="custom-frame"): gr.HTML("
📊 니즈원인 분석
") direct_insight_analysis_output = gr.Textbox( label="니즈원인 분석", lines=8, value="" ) with gr.Column(elem_classes="custom-frame"): gr.HTML("
🔧 상품판매방향성
") direct_strategy_analysis_output = gr.Textbox( label="상품판매방향성", lines=8, value="" ) with gr.Row(): with gr.Column(elem_classes="custom-frame"): gr.HTML("
📝 소싱전략
") direct_sourcing_analysis_output = gr.Textbox( label="소싱전략", lines=8, value="" ) with gr.Column(elem_classes="custom-frame"): gr.HTML("
🖼️ 마케팅전략
") direct_detail_page_analysis_output = gr.Textbox( label="마케팅전략", lines=8, value="" ) # 직접 입력 분석 이벤트 핸들러 def on_direct_analyze(positive_input, negative_input, session_id): analyzer, session_id = get_session_analyzer(session_id) try: result = analyzer.analyze_direct_reviews(positive_input, negative_input) if isinstance(result, tuple) and len(result) >= 9: results = result else: results = (None, "", "", "", "", "", "", "", "") final_file = results[0] if final_file: session_manager.add_temp_file(session_id, final_file) return results + (session_id,) except Exception as e: print(f"직접 분석 오류: {e}") import traceback traceback.print_exc() return (None, "", "", "", "", "", "", "", "", session_id) direct_review_button.click( fn=on_direct_analyze, inputs=[direct_positive_input, direct_negative_input, session_id_state], outputs=[direct_download_output, direct_positive_analysis_output, direct_negative_analysis_output, direct_insight_analysis_output, direct_strategy_analysis_output, direct_sourcing_analysis_output, direct_detail_page_analysis_output, session_id_state] ) # 예시 적용 섹션 with gr.Column(elem_classes="custom-frame"): gr.HTML("
📚 예시 적용하기
") with gr.Row(): example_excel_button = gr.Button("📊 엑셀 분석 예시 적용하기", elem_classes="custom-button") example_direct_button = gr.Button("📝 직접 입력 예시 적용하기", elem_classes="custom-button") clear_all_button = gr.Button("🗑️ 전체 초기화", elem_classes="custom-button") # 예시 적용 이벤트 def apply_excel_example(session_id): analyzer, session_id = get_session_analyzer(session_id) excel_file, year = analyzer.apply_excel_example() return excel_file, year, session_id example_excel_button.click( fn=apply_excel_example, inputs=[session_id_state], outputs=[file_input, year_radio, session_id_state] ) def apply_direct_example(session_id): analyzer, session_id = get_session_analyzer(session_id) positive_text, negative_text = analyzer.apply_direct_example() return positive_text, negative_text, session_id example_direct_button.click( fn=apply_direct_example, inputs=[session_id_state], outputs=[direct_positive_input, direct_negative_input, session_id_state] ) # 전체 초기화 기능 def clear_all_data(session_id): if session_id: session_manager.cleanup_session(session_id) new_session_id = initialize_session() return ( None, "25년", gr.update(visible=False), ["전체옵션분석"], "전체옵션분석", None, "", "", "", "", "", "", "", "", "", "", None, "", "", "", "", "", "", new_session_id ) clear_all_button.click( fn=clear_all_data, inputs=[session_id_state], outputs=[ file_input, year_radio, review_analysis_frame, top20_dropdown, top20_dropdown, download_final_output, positive_output, negative_output, positive_analysis_output, negative_analysis_output, insight_analysis_output, strategy_analysis_output, sourcing_analysis_output, detail_page_analysis_output, direct_positive_input, direct_negative_input, direct_download_output, direct_positive_analysis_output, direct_negative_analysis_output, direct_insight_analysis_output, direct_strategy_analysis_output, direct_sourcing_analysis_output, direct_detail_page_analysis_output, session_id_state ] ) # 페이지 로드 시 완전한 초기화 def init_on_load(): session_id = initialize_session() return ( session_id, None, "25년", gr.update(visible=False), gr.update(choices=["전체옵션분석"], value="전체옵션분석"), None, "", "", "", "", "", "", "", "", "", "", None, "", "", "", "", "", "" ) demo.load( fn=init_on_load, outputs=[ session_id_state, file_input, year_radio, review_analysis_frame, top20_dropdown, download_final_output, positive_output, negative_output, positive_analysis_output, negative_analysis_output, insight_analysis_output, strategy_analysis_output, sourcing_analysis_output, detail_page_analysis_output, direct_positive_input, direct_negative_input, direct_download_output, direct_positive_analysis_output, direct_negative_analysis_output, direct_insight_analysis_output, direct_strategy_analysis_output, direct_sourcing_analysis_output, direct_detail_page_analysis_output ] ) return demo if __name__ == "__main__": app = create_app() # 주기적으로 오래된 세션 정리 import threading def cleanup_timer(): while True: time.sleep(3600) session_manager.cleanup_old_sessions() cleanup_thread = threading.Thread(target=cleanup_timer, daemon=True) cleanup_thread.start() app.launch( share=False, server_name="0.0.0.0", server_port=7860, show_error=True, debug=False )