#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
)