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("
📑 데이터 입력
") file_input = gr.File(label="원본 엑셀 파일 업로드", file_types=[".xlsx"]) year_radio = gr.Radio( choices=[f"{str(y)[-2:]}년" for y in range(datetime.datetime.now().year, datetime.datetime.now().year-5, -1)], label="분석년도 선택", value=f"{str(datetime.datetime.now().year)[-2:]}년" ) 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") # ─── 분석 결과: 4행 × 2열 ─── # 1행: ✨ 주요긍정리뷰 / ✨ 주요부정리뷰 with gr.Row(): with gr.Column(elem_classes="custom-frame"): gr.HTML("
✨ 주요긍정리뷰
") positive_output = gr.Textbox(label="긍정리뷰리스트 (20개)", lines=10) with gr.Column(elem_classes="custom-frame"): gr.HTML("
✨ 주요부정리뷰
") negative_output = gr.Textbox(label="부정리뷰리스트 (30개)", lines=10) # 2행: 📢 긍정리뷰 분석 / 📢 부정리뷰 분석 with gr.Row(): with gr.Column(elem_classes="custom-frame"): gr.HTML("
📢 긍정리뷰 분석
") positive_analysis_output = gr.Textbox(label="긍정리뷰 분석", lines=8) with gr.Column(elem_classes="custom-frame"): gr.HTML("
📢 부정리뷰 분석
") negative_analysis_output = gr.Textbox(label="부정리뷰 분석", lines=8) # 3행: 📊 니즈 원츠 분석 / 🔧 판매전략 수립 with gr.Row(): with gr.Column(elem_classes="custom-frame"): gr.HTML("
📊 니즈원츠분석
") insight_analysis_output = gr.Textbox(label="니즈원츠분석", lines=8) with gr.Column(elem_classes="custom-frame"): gr.HTML("
🔧 상품판매방향성
") strategy_analysis_output = gr.Textbox(label="상품판매방향성", lines=8) # 4행: 🔍 소싱전략 / 🖼️ 상세페이지 전략 with gr.Row(): with gr.Column(elem_classes="custom-frame"): gr.HTML("
🔍 소싱전략
") sourcing_analysis_output = gr.Textbox(label="소싱전략", lines=8) with gr.Column(elem_classes="custom-frame"): gr.HTML("
🖼️ 마케팅전략
") detail_page_analysis_output = gr.Textbox(label="마케팅전략", lines=8) # 상태 변수 analysis_state = gr.State() # 이벤트 핸들러 analyze_button.click( fn=call_analyze_options, inputs=[file_input, year_radio], outputs=[analysis_state, review_analysis_frame, top20_dropdown] ) review_button.click( fn=call_analyze_reviews, inputs=[top20_dropdown, analysis_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] ) ############################# # 직접 입력 분석 모드 ############################# 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 ) direct_negative_input = gr.Textbox( label="부정 리뷰 입력", placeholder="부정 리뷰를 여기에 입력하세요.(최대 8000자)", lines=10, max_length=8000 ) direct_review_button = gr.Button("리뷰 분석하기", elem_classes="custom-button") with gr.Column(elem_classes="custom-frame"): gr.HTML("
📑 분석보고서 다운로드
") direct_download_output = gr.File(label="분석 보고서 다운로드") # 2행: 📢 긍정리뷰 분석 / 📢 부정리뷰 분석 with gr.Row(): with gr.Column(elem_classes="custom-frame"): gr.HTML("
📢 긍정리뷰분석
") direct_positive_analysis_output = gr.Textbox( label="긍정리뷰분석", lines=8 ) with gr.Column(elem_classes="custom-frame"): gr.HTML("
📢 부정리뷰분석
") direct_negative_analysis_output = gr.Textbox( label="부정리뷰분석", lines=8 ) # 3행: 📊 니즈 원츠 분석 / 🔧 판매전략 수립 with gr.Row(): with gr.Column(elem_classes="custom-frame"): gr.HTML("
📊 니즈원츠분석
") direct_insight_analysis_output = gr.Textbox( label="니즈원츠분석", lines=8 ) with gr.Column(elem_classes="custom-frame"): gr.HTML("
🔧 상품판매방향성
") direct_strategy_analysis_output = gr.Textbox( label="상품판매방향성", lines=8 ) # 4행: 🔍 소싱전략 / 🖼️ 상세페이지 전략 with gr.Row(): with gr.Column(elem_classes="custom-frame"): gr.HTML("
🔍 소싱전략
") direct_sourcing_analysis_output = gr.Textbox( label="소싱전략", lines=8 ) with gr.Column(elem_classes="custom-frame"): gr.HTML("
🖼️ 마케팅전략
") direct_detail_page_analysis_output = gr.Textbox( label="마케팅전략", lines=8 ) # 이벤트 핸들러 direct_review_button.click( fn=call_direct_analyze, inputs=[direct_positive_input, direct_negative_input], 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] ) # 예시 적용 섹션 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") # 이벤트 핸들러 example_excel_button.click( fn=call_apply_excel_example, outputs=[file_input, year_radio] ) example_direct_button.click( fn=call_apply_direct_example, outputs=[direct_positive_input, direct_negative_input] ) if __name__ == "__main__": demo.launch()