import gradio as gr import os import tempfile import datetime import pytz from gradio_client import Client, handle_file import dotenv # 환경변수 로드 dotenv.load_dotenv() # API 엔드포인트를 환경변수에서 가져옴 (로그에 출력하지 않음) API_ENDPOINT = os.getenv("API_ENDPOINT") if not API_ENDPOINT: raise ValueError("API_ENDPOINT 환경변수가 설정되지 않았습니다.") # 클라이언트 초기화 (로그에 출력하지 않음) client = Client(API_ENDPOINT) # ===================== 사용 가이드 HTML 정의 ===================== fontawesome_link = """ """ # ===================== CSS 스타일 정의 ===================== 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); } /* ── 탭 내부 패널 배경 제거 ── */ .gr-tabs-panel { background-color: var(--background-color) !important; box-shadow: none !important; } .gr-tabs-panel::before, .gr-tabs-panel::after { display: none !important; content: none !important; } /* ── 그룹 래퍼 배경 완전 제거 ── */ .custom-section-group, .gr-block.gr-group { background-color: var(--background-color) !important; box-shadow: none !important; } .custom-section-group::before, .custom-section-group::after, .gr-block.gr-group::before, .gr-block.gr-group::after { display: none !important; content: none !important; } /* 그룹 컨테이너 배경을 아이보리로, 그림자 제거 */ .custom-section-group { background-color: var(--background-color) !important; box-shadow: none !important; } /* 상단·하단에 그려지는 회색 캡(둥근 모서리) 제거 */ .custom-section-group::before, .custom-section-group::after { display: none !important; content: none !important; } 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%; /* 전체 너비 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-section-group { margin-top: 20px; padding: 0; border: none; border-radius: 0; background-color: var(--background-color); /* 회색 → 아이보리(전체 배경색) */ box-shadow: none !important; /* 혹시 남아있는 그림자도 같이 제거 */ } /* 버튼 스타일 - 글자 크기 18px */ .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 클래스로) */ .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; } /* 사용 가이드 스타일 추가 */ .guide-container { background-color: var(--card-bg); border-radius: var(--border-radius); box-shadow: var(--shadow); padding: 1.5rem; margin-bottom: 1.5rem; border: 1px solid rgba(0, 0, 0, 0.04); } .guide-title { font-size: 1.5rem; font-weight: 700; color: var(--primary-color); margin-bottom: 1.5rem; padding-bottom: 0.5rem; border-bottom: 2px solid var(--primary-color); display: flex; align-items: center; } .guide-title i { margin-right: 0.8rem; font-size: 1.5rem; } .guide-item { display: flex; margin-bottom: 1rem; align-items: flex-start; } .guide-number { background-color: var(--primary-color); color: white; width: 25px; height: 25px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: bold; margin-right: 10px; flex-shrink: 0; } .guide-text { flex: 1; line-height: 1.6; } .guide-text a { color: var(--primary-color); text-decoration: underline; font-weight: 600; } """ # ===================== API 호출 함수들 ===================== def call_analyze_options(uploaded_file, selected_year): """옵션 분석 API 호출""" try: if uploaded_file is None: return None, gr.update(visible=False), gr.update(choices=["전체옵션분석"], value="전체옵션분석") # API 호출 result = client.predict( uploaded_file=handle_file(uploaded_file), selected_year=selected_year, api_name="/on_click_analyze_options" ) # 결과 처리 choices = result if isinstance(result, list) else ["전체옵션분석"] return "success", gr.update(visible=True), gr.update(choices=choices, value=choices[0] if choices else "전체옵션분석") except Exception as e: print(f"옵션 분석 API 호출 중 오류: {e}") return None, gr.update(visible=False), gr.update(choices=["전체옵션분석"], value="전체옵션분석") def call_analyze_reviews(selected_option, analysis_state): """리뷰 분석 API 호출""" try: if analysis_state is None: return None, "", "", "", "", "", "", "", "" # API 호출 result = client.predict( selected_option=selected_option, api_name="/on_click_analyze_reviews" ) # 결과 언패킹 if isinstance(result, tuple) and len(result) >= 9: return result[0], result[1], result[2], result[3], result[4], result[5], result[6], result[7], result[8] else: return None, "", "", "", "", "", "", "", "" except Exception as e: print(f"리뷰 분석 API 호출 중 오류: {e}") return None, "", "", "", "", "", "", "", "" def call_direct_analyze(positive_input, negative_input): """직접 입력 분석 API 호출""" try: # API 호출 result = client.predict( positive_input=positive_input, negative_input=negative_input, api_name="/on_click_direct_analyze" ) # 결과 언패킹 if isinstance(result, tuple) and len(result) >= 7: return result[0], result[1], result[2], result[3], result[4], result[5], result[6] else: return None, "", "", "", "", "", "" except Exception as e: print(f"직접 입력 분석 API 호출 중 오류: {e}") return None, "", "", "", "", "", "" def call_apply_excel_example(): """엑셀 예시 적용 API 호출""" try: result = client.predict(api_name="/apply_excel_example") if isinstance(result, tuple) and len(result) >= 2: return result[0], result[1] else: return None, gr.update() except Exception as e: print(f"엑셀 예시 적용 API 호출 중 오류: {e}") return None, gr.update() def call_apply_direct_example(): """직접 입력 예시 적용 API 호출""" try: result = client.predict(api_name="/apply_direct_example") if isinstance(result, tuple) and len(result) >= 2: return result[0], result[1] else: return "", "" except Exception as e: print(f"직접 입력 예시 적용 API 호출 중 오류: {e}") return "", "" # ===================== Gradio UI 구성 ===================== 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) # 탭 구성: 엑셀 분석 모드와 직접 입력 분석 모드 with gr.Tabs() as tabs: ############################# # 엑셀 분석 모드 ############################# with gr.TabItem("💾 스마트스토어 엑셀리뷰데이터 활용"): # 좌측: 데이터 입력 섹션 with gr.Row(): with gr.Column(elem_classes="custom-frame"): gr.HTML("