ssboost commited on
Commit
09221d2
·
verified ·
1 Parent(s): 7286af5

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +517 -361
app.py CHANGED
@@ -1,29 +1,33 @@
 
1
  import gradio as gr
2
- import os
3
- import tempfile
4
  import datetime
 
 
 
 
 
5
  import pytz
6
- from gradio_client import Client, handle_file
7
- import dotenv
8
 
9
- # 환경변수 로드
10
- dotenv.load_dotenv()
 
 
11
 
12
- # API 엔드포인트를 환경변수에서 가져옴 (로그에 출력하지 않음)
13
- API_ENDPOINT = os.getenv("API_ENDPOINT")
14
- if not API_ENDPOINT:
15
- raise ValueError("API_ENDPOINT 환경변수가 설정되지 않았습니다.")
16
 
17
- # 클라이언트 초기화 (로그에 출력하지 않음)
18
- client = Client(API_ENDPOINT)
19
-
20
- # ===================== 사용 가이드 HTML 정의 =====================
21
  fontawesome_link = """
22
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" crossorigin="anonymous" referrerpolicy="no-referrer" />
23
  """
24
 
25
- # ===================== CSS 스타일 정의 =====================
26
- custom_css = """
27
  :root {
28
  --primary-color: #FB7F0D;
29
  --secondary-color: #ff9a8b;
@@ -34,40 +38,7 @@ custom_css = """
34
  --border-radius: 18px;
35
  --shadow: 0 8px 30px rgba(251, 127, 13, 0.08);
36
  }
37
- /* ── 탭 내부 패널 배경 제거 ── */
38
- .gr-tabs-panel {
39
- background-color: var(--background-color) !important;
40
- box-shadow: none !important;
41
- }
42
- .gr-tabs-panel::before,
43
- .gr-tabs-panel::after {
44
- display: none !important;
45
- content: none !important;
46
- }
47
- /* ── 그룹 래퍼 배경 완전 제거 ── */
48
- .custom-section-group,
49
- .gr-block.gr-group {
50
- background-color: var(--background-color) !important;
51
- box-shadow: none !important;
52
- }
53
- .custom-section-group::before,
54
- .custom-section-group::after,
55
- .gr-block.gr-group::before,
56
- .gr-block.gr-group::after {
57
- display: none !important;
58
- content: none !important;
59
- }
60
- /* 그룹 컨테이너 배경을 아이보리로, 그림자 제거 */
61
- .custom-section-group {
62
- background-color: var(--background-color) !important;
63
- box-shadow: none !important;
64
- }
65
- /* 상단·하단에 그려지는 회색 캡(둥근 모서리) 제거 */
66
- .custom-section-group::before,
67
- .custom-section-group::after {
68
- display: none !important;
69
- content: none !important;
70
- }
71
  body {
72
  font-family: 'Pretendard', 'Noto Sans KR', -apple-system, BlinkMacSystemFont, sans-serif;
73
  background-color: var(--background-color);
@@ -76,33 +47,36 @@ body {
76
  margin: 0;
77
  padding: 0;
78
  }
 
79
  .gradio-container {
80
- width: 100%; /* 전체 너비 100% 고정 */
81
  margin: 0 auto;
82
  padding: 20px;
83
  background-color: var(--background-color);
84
  }
85
- /* 헤더 스타일 - 주황색 박스 형태로 변경 */
86
  .custom-header {
87
- background: #FF7F00; /* 단색 주황색 */
88
  padding: 2rem;
89
- border-radius: 15px; /* 라운드 처리를 약하게 조정 */
90
  margin-bottom: 20px;
91
  box-shadow: var(--shadow);
92
  text-align: center;
93
  }
 
94
  .custom-header h1 {
95
  margin: 0;
96
  font-size: 2.5rem;
97
  font-weight: 700;
98
- color: black; /* 글자색을 검은색으로 변경 */
99
  }
 
100
  .custom-header p {
101
  margin: 10px 0 0;
102
  font-size: 1.2rem;
103
- color: black; /* 소제목도 검은색으로 변경 */
104
  }
105
- /* 콘텐츠 박스 (프레임) 스타일 */
106
  .custom-frame {
107
  background-color: var(--card-bg);
108
  border: 1px solid rgba(0, 0, 0, 0.04);
@@ -111,16 +85,7 @@ body {
111
  margin: 10px 0;
112
  box-shadow: var(--shadow);
113
  }
114
- /* 섹션 그룹 스타일 - 회색 배경 완전 제거 */
115
- .custom-section-group {
116
- margin-top: 20px;
117
- padding: 0;
118
- border: none;
119
- border-radius: 0;
120
- background-color: var(--background-color); /* 회색 → 아이보리(전체 배경색) */
121
- box-shadow: none !important; /* 혹시 남아있는 그림자도 같이 제거 */
122
- }
123
- /* 버튼 스타일 - 글자 크기 18px */
124
  .custom-button {
125
  border-radius: 30px !important;
126
  background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)) !important;
@@ -131,11 +96,12 @@ body {
131
  box-shadow: 0 4px 8px rgba(251, 127, 13, 0.25);
132
  transition: transform 0.3s ease;
133
  }
 
134
  .custom-button:hover {
135
  transform: translateY(-2px);
136
  box-shadow: 0 6px 12px rgba(251, 127, 13, 0.3);
137
  }
138
- /* 제목 스타일 (모든 항목명이 동일하게 custom-title 클래스로) */
139
  .custom-title {
140
  font-size: 28px;
141
  font-weight: bold;
@@ -144,328 +110,518 @@ body {
144
  border-bottom: 2px solid var(--primary-color);
145
  padding-bottom: 5px;
146
  }
147
- /* 사용 가이드 스타일 추가 */
148
- .guide-container {
149
- background-color: var(--card-bg);
150
- border-radius: var(--border-radius);
151
- box-shadow: var(--shadow);
152
- padding: 1.5rem;
153
- margin-bottom: 1.5rem;
154
- border: 1px solid rgba(0, 0, 0, 0.04);
155
  }
156
- .guide-title {
157
- font-size: 1.5rem;
158
- font-weight: 700;
159
- color: var(--primary-color);
160
- margin-bottom: 1.5rem;
161
- padding-bottom: 0.5rem;
162
- border-bottom: 2px solid var(--primary-color);
163
- display: flex;
164
- align-items: center;
165
  }
166
- .guide-title i {
167
- margin-right: 0.8rem;
168
- font-size: 1.5rem;
 
 
 
 
169
  }
170
- .guide-item {
171
- display: flex;
172
- margin-bottom: 1rem;
173
- align-items: flex-start;
 
174
  }
175
- .guide-number {
176
- background-color: var(--primary-color);
177
- color: white;
178
- width: 25px;
179
- height: 25px;
180
- border-radius: 50%;
181
- display: flex;
182
- align-items: center;
183
- justify-content: center;
184
- font-weight: bold;
185
- margin-right: 10px;
186
- flex-shrink: 0;
187
  }
188
- .guide-text {
189
- flex: 1;
190
- line-height: 1.6;
 
191
  }
192
- .guide-text a {
193
- color: var(--primary-color);
194
- text-decoration: underline;
195
- font-weight: 600;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
196
  }
197
- """
198
 
199
- # ===================== API 호출 함수들 =====================
200
- def call_analyze_options(uploaded_file, selected_year):
201
- """옵션 분석 API 호출"""
202
- try:
203
- if uploaded_file is None:
204
- return None, gr.update(visible=False), gr.update(choices=["전체옵션분석"], value="전체옵션분석")
205
-
206
- # API 호출
207
- result = client.predict(
208
- uploaded_file=handle_file(uploaded_file),
209
- selected_year=selected_year,
210
- api_name="/on_click_analyze_options"
211
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
212
 
213
- # 결과 처리
214
- choices = result if isinstance(result, list) else ["전체옵션분석"]
 
215
 
216
- return "success", gr.update(visible=True), gr.update(choices=choices, value=choices[0] if choices else "전체옵션분석")
217
- except Exception as e:
218
- print(f"옵션 분석 API 호출 중 오류: {e}")
219
- return None, gr.update(visible=False), gr.update(choices=["전체옵션분석"], value="전체옵션분석")
 
220
 
221
- def call_analyze_reviews(selected_option, analysis_state):
222
- """리뷰 분석 API 호출"""
223
- try:
224
- if analysis_state is None:
225
- return None, "", "", "", "", "", "", "", ""
 
 
 
 
 
 
226
 
227
- # API 호출
228
- result = client.predict(
229
- selected_option=selected_option,
230
- api_name="/on_click_analyze_reviews"
231
- )
232
 
233
- # 결과 언패킹
234
- if isinstance(result, tuple) and len(result) >= 9:
235
- return result[0], result[1], result[2], result[3], result[4], result[5], result[6], result[7], result[8]
236
- else:
237
- return None, "", "", "", "", "", "", "", ""
238
- except Exception as e:
239
- print(f"리뷰 분석 API 호출 중 오류: {e}")
240
- return None, "", "", "", "", "", "", "", ""
241
-
242
- def call_direct_analyze(positive_input, negative_input):
243
- """직접 입력 분석 API 호출"""
244
- try:
245
- # API 호출
246
- result = client.predict(
247
- positive_input=positive_input,
248
- negative_input=negative_input,
249
- api_name="/on_click_direct_analyze"
250
- )
251
 
252
- # 결과 언패킹
253
- if isinstance(result, tuple) and len(result) >= 7:
254
- return result[0], result[1], result[2], result[3], result[4], result[5], result[6]
255
- else:
256
- return None, "", "", "", "", "", ""
257
- except Exception as e:
258
- print(f"직접 입력 분석 API 호출 중 오류: {e}")
259
- return None, "", "", "", "", "", ""
260
-
261
- def call_apply_excel_example():
262
- """엑셀 예시 적용 API 호출"""
263
- try:
264
- result = client.predict(api_name="/apply_excel_example")
265
- if isinstance(result, tuple) and len(result) >= 2:
266
- return result[0], result[1]
267
- else:
268
- return None, gr.update()
269
- except Exception as e:
270
- print(f"엑셀 예시 적용 API 호출 중 오류: {e}")
271
- return None, gr.update()
272
-
273
- def call_apply_direct_example():
274
- """직접 입력 예시 적용 API 호출"""
275
- try:
276
- result = client.predict(api_name="/apply_direct_example")
277
- if isinstance(result, tuple) and len(result) >= 2:
278
- return result[0], result[1]
279
- else:
280
- return "", ""
281
- except Exception as e:
282
- print(f"직접 입력 예시 적용 API 호출 중 오류: {e}")
283
- return "", ""
284
-
285
- # ===================== Gradio UI 구성 =====================
286
- demo = gr.Blocks(css=custom_css, theme=gr.themes.Default(
287
- primary_hue="orange",
288
- secondary_hue="orange",
289
- font=[gr.themes.GoogleFont("Noto Sans KR"), "ui-sans-serif", "system-ui"]
290
- ))
291
-
292
- with demo:
293
- gr.HTML(fontawesome_link)
294
-
295
- # 탭 구성: 엑셀 분석 모드와 직접 입력 분석 모드
296
- with gr.Tabs() as tabs:
297
- #############################
298
- # 엑셀 분석 모드
299
- #############################
300
- with gr.TabItem("💾 스마트스토어 엑셀리뷰데이터 활용"):
301
- # 좌측: 데이터 입력 섹션
302
- with gr.Row():
303
- with gr.Column(elem_classes="custom-frame"):
304
- gr.HTML("<div class='custom-title'>📑 데이터 입력</div>")
305
- file_input = gr.File(label="원본 엑셀 파일 업로드", file_types=[".xlsx"])
306
- year_radio = gr.Radio(
307
- choices=[f"{str(y)[-2:]}년" for y in range(datetime.datetime.now().year, datetime.datetime.now().year-5, -1)],
308
- label="분석년도 선택",
309
- value=f"{str(datetime.datetime.now().year)[-2:]}년"
310
  )
311
- analyze_button = gr.Button("옵션 분석하기", elem_classes="custom-button")
312
- with gr.Column(elem_classes="custom-frame"):
313
- gr.HTML("<div class='custom-title'>📑 분석보고서 다운로드</div>")
314
- download_final_output = gr.File(label="보고서 다운로드")
 
 
 
 
 
 
315
 
316
- # 리뷰분석 섹션
317
- with gr.Column(elem_classes="custom-frame", visible=False) as review_analysis_frame:
318
- gr.HTML("<div class='custom-title'>📑 리뷰분석</div>")
319
- top20_dropdown = gr.Dropdown(
320
- label="아이템옵션 분석",
321
- choices=["전체옵션분석"],
322
- value="전체옵션분석"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
323
  )
324
- review_button = gr.Button("리뷰 분석하기", elem_classes="custom-button")
325
-
326
- # ─── 분석 결과: 4행 × 2열 ───
327
- # 1행: ✨ 주요긍정리뷰 / ✨ 주요부정리뷰
328
- with gr.Row():
329
- with gr.Column(elem_classes="custom-frame"):
330
- gr.HTML("<div class='custom-title'>✨ 주요긍정리뷰</div>")
331
- positive_output = gr.Textbox(label="긍정리뷰리스트 (20개)", lines=10)
332
- with gr.Column(elem_classes="custom-frame"):
333
- gr.HTML("<div class='custom-title'>✨ 주요부정리뷰</div>")
334
- negative_output = gr.Textbox(label="부정리뷰리스트 (30개)", lines=10)
335
-
336
- # 2행: 📢 긍정리뷰 분석 / 📢 부정리뷰 분석
337
- with gr.Row():
338
- with gr.Column(elem_classes="custom-frame"):
339
- gr.HTML("<div class='custom-title'>📢 긍정리뷰 분석</div>")
340
- positive_analysis_output = gr.Textbox(label="긍정리뷰 분석", lines=8)
341
- with gr.Column(elem_classes="custom-frame"):
342
- gr.HTML("<div class='custom-title'>📢 부정리뷰 분석</div>")
343
- negative_analysis_output = gr.Textbox(label="부정리뷰 분석", lines=8)
344
-
345
- # 3행: 📊 니즈 원츠 분석 / 🔧 판매전략 수립
346
- with gr.Row():
347
- with gr.Column(elem_classes="custom-frame"):
348
- gr.HTML("<div class='custom-title'>📊 니즈원츠분석</div>")
349
- insight_analysis_output = gr.Textbox(label="니즈원츠분석", lines=8)
350
- with gr.Column(elem_classes="custom-frame"):
351
- gr.HTML("<div class='custom-title'>🔧 상품판매방향성</div>")
352
- strategy_analysis_output = gr.Textbox(label="상품판매방향성", lines=8)
353
 
354
- # 4행: 🔍 소싱전략 / 🖼️ 상세페이지 전략
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
355
  with gr.Row():
356
- with gr.Column(elem_classes="custom-frame"):
357
- gr.HTML("<div class='custom-title'>🔍 소싱전략</div>")
358
- sourcing_analysis_output = gr.Textbox(label="소싱전략", lines=8)
359
- with gr.Column(elem_classes="custom-frame"):
360
- gr.HTML("<div class='custom-title'>🖼️ 마케팅전략</div>")
361
- detail_page_analysis_output = gr.Textbox(label="마케팅전략", lines=8)
362
 
363
- # 상태 변수
364
- analysis_state = gr.State()
 
 
 
365
 
366
- # 이벤트 핸들러
367
- analyze_button.click(
368
- fn=call_analyze_options,
369
- inputs=[file_input, year_radio],
370
- outputs=[analysis_state, review_analysis_frame, top20_dropdown]
371
  )
372
 
373
- review_button.click(
374
- fn=call_analyze_reviews,
375
- inputs=[top20_dropdown, analysis_state],
376
- outputs=[download_final_output, positive_output, negative_output,
377
- positive_analysis_output, negative_analysis_output,
378
- insight_analysis_output, strategy_analysis_output,
379
- sourcing_analysis_output, detail_page_analysis_output]
 
 
380
  )
381
-
382
- #############################
383
- # 직접 입력 분석 모드
384
- #############################
385
- with gr.TabItem("📖 직접 입력한 자료활용"):
386
- with gr.Row():
387
- with gr.Column(elem_classes="custom-frame"):
388
- gr.HTML("<div class='custom-title'>📝 리뷰 직접 입력</div>")
389
- direct_positive_input = gr.Textbox(
390
- label="긍정 리뷰 입력",
391
- placeholder="긍정 리뷰를 여기에 입력하세요.(최대 8000자)",
392
- lines=10, max_length=8000
393
- )
394
- direct_negative_input = gr.Textbox(
395
- label="부정 리뷰 입력",
396
- placeholder="부정 리뷰를 여기에 입력하세요.(최대 8000자)",
397
- lines=10, max_length=8000
398
- )
399
- direct_review_button = gr.Button("리뷰 분석하기", elem_classes="custom-button")
400
- with gr.Column(elem_classes="custom-frame"):
401
- gr.HTML("<div class='custom-title'>📑 분석보고서 다운로드</div>")
402
- direct_download_output = gr.File(label="분석 보고서 다운로드")
403
-
404
- # 2행: 📢 긍정리뷰 분석 / 📢 부정리뷰 분석
405
- with gr.Row():
406
- with gr.Column(elem_classes="custom-frame"):
407
- gr.HTML("<div class='custom-title'>📢 긍정리뷰분석</div>")
408
- direct_positive_analysis_output = gr.Textbox(
409
- label="긍정리뷰분석", lines=8
410
- )
411
- with gr.Column(elem_classes="custom-frame"):
412
- gr.HTML("<div class='custom-title'>📢 부정리뷰분석</div>")
413
- direct_negative_analysis_output = gr.Textbox(
414
- label="부정리뷰분석", lines=8
415
- )
416
-
417
- # 3행: 📊 니즈 원츠 분석 / 🔧 판매전략 수립
418
- with gr.Row():
419
- with gr.Column(elem_classes="custom-frame"):
420
- gr.HTML("<div class='custom-title'>📊 니즈원츠분석</div>")
421
- direct_insight_analysis_output = gr.Textbox(
422
- label="니즈원츠분석", lines=8
423
- )
424
- with gr.Column(elem_classes="custom-frame"):
425
- gr.HTML("<div class='custom-title'>🔧 상품판매방향성</div>")
426
- direct_strategy_analysis_output = gr.Textbox(
427
- label="상품판매방향성", lines=8
428
- )
429
 
430
- # 4행: 🔍 소싱전략 / 🖼️ 상세페이지 전략
431
- with gr.Row():
432
- with gr.Column(elem_classes="custom-frame"):
433
- gr.HTML("<div class='custom-title'>🔍 소싱전략</div>")
434
- direct_sourcing_analysis_output = gr.Textbox(
435
- label="소싱전략", lines=8
436
- )
437
- with gr.Column(elem_classes="custom-frame"):
438
- gr.HTML("<div class='custom-title'>🖼️ 마케팅전략</div>")
439
- direct_detail_page_analysis_output = gr.Textbox(
440
- label="마케팅전략", lines=8
441
- )
 
 
442
 
443
- # 이벤트 핸들러
444
- direct_review_button.click(
445
- fn=call_direct_analyze,
446
- inputs=[direct_positive_input, direct_negative_input],
447
- outputs=[direct_download_output, direct_positive_analysis_output, direct_negative_analysis_output,
448
- direct_insight_analysis_output, direct_strategy_analysis_output,
449
- direct_sourcing_analysis_output, direct_detail_page_analysis_output]
 
 
 
 
 
 
 
 
 
 
 
450
  )
451
-
452
- # 예시 적용 섹션
453
- with gr.Column(elem_classes="custom-frame"):
454
- gr.HTML("<div class='custom-title'>📚 예시 적용하기</div>")
455
- with gr.Row():
456
- example_excel_button = gr.Button("📊 엑셀 분석 예시 적용하기", elem_classes="custom-button")
457
- example_direct_button = gr.Button("📝 직접 입력 예시 적용하기", elem_classes="custom-button")
458
 
459
- # 이벤트 핸들러
460
- example_excel_button.click(
461
- fn=call_apply_excel_example,
462
- outputs=[file_input, year_radio]
463
- )
 
 
 
 
 
464
 
465
- example_direct_button.click(
466
- fn=call_apply_direct_example,
467
- outputs=[direct_positive_input, direct_negative_input]
 
 
 
 
 
 
 
 
 
 
 
 
 
468
  )
 
 
469
 
470
  if __name__ == "__main__":
471
- demo.launch()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #app.py
2
  import gradio as gr
3
+ import pandas as pd
4
+ import openpyxl
5
  import datetime
6
+ import tempfile
7
+ import os
8
+ import uuid
9
+ import time
10
+ from openpyxl.utils.dataframe import dataframe_to_rows
11
  import pytz
12
+ import random
13
+ import google.generativeai as genai
14
 
15
+ # 환경변수에서 코드 불러오기
16
+ api_manager_code = os.getenv('API_MANAGER_CODE', '')
17
+ styles_code = os.getenv('STYLES_CODE', '')
18
+ review_analyzer_code = os.getenv('REVIEW_ANALYZER_CODE', '')
19
 
20
+ # 동적으로 코드 실행하여 클래스 정의
21
+ exec(api_manager_code)
22
+ exec(review_analyzer_code)
 
23
 
24
+ # 스타일 정보 불러오기
 
 
 
25
  fontawesome_link = """
26
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" crossorigin="anonymous" referrerpolicy="no-referrer" />
27
  """
28
 
29
+ # 환경변수에서 CSS 불러오기
30
+ custom_css = os.getenv('CUSTOM_CSS', '''
31
  :root {
32
  --primary-color: #FB7F0D;
33
  --secondary-color: #ff9a8b;
 
38
  --border-radius: 18px;
39
  --shadow: 0 8px 30px rgba(251, 127, 13, 0.08);
40
  }
41
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  body {
43
  font-family: 'Pretendard', 'Noto Sans KR', -apple-system, BlinkMacSystemFont, sans-serif;
44
  background-color: var(--background-color);
 
47
  margin: 0;
48
  padding: 0;
49
  }
50
+
51
  .gradio-container {
52
+ width: 100%;
53
  margin: 0 auto;
54
  padding: 20px;
55
  background-color: var(--background-color);
56
  }
57
+
58
  .custom-header {
59
+ background: #FF7F00;
60
  padding: 2rem;
61
+ border-radius: 15px;
62
  margin-bottom: 20px;
63
  box-shadow: var(--shadow);
64
  text-align: center;
65
  }
66
+
67
  .custom-header h1 {
68
  margin: 0;
69
  font-size: 2.5rem;
70
  font-weight: 700;
71
+ color: black;
72
  }
73
+
74
  .custom-header p {
75
  margin: 10px 0 0;
76
  font-size: 1.2rem;
77
+ color: black;
78
  }
79
+
80
  .custom-frame {
81
  background-color: var(--card-bg);
82
  border: 1px solid rgba(0, 0, 0, 0.04);
 
85
  margin: 10px 0;
86
  box-shadow: var(--shadow);
87
  }
88
+
 
 
 
 
 
 
 
 
 
89
  .custom-button {
90
  border-radius: 30px !important;
91
  background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)) !important;
 
96
  box-shadow: 0 4px 8px rgba(251, 127, 13, 0.25);
97
  transition: transform 0.3s ease;
98
  }
99
+
100
  .custom-button:hover {
101
  transform: translateY(-2px);
102
  box-shadow: 0 6px 12px rgba(251, 127, 13, 0.3);
103
  }
104
+
105
  .custom-title {
106
  font-size: 28px;
107
  font-weight: bold;
 
110
  border-bottom: 2px solid var(--primary-color);
111
  padding-bottom: 5px;
112
  }
113
+
114
+ .gr-tabs-panel {
115
+ background-color: var(--background-color) !important;
116
+ box-shadow: none !important;
 
 
 
 
117
  }
118
+
119
+ .custom-section-group {
120
+ background-color: var(--background-color) !important;
121
+ box-shadow: none !important;
 
 
 
 
 
122
  }
123
+
124
+ .gr-textbox textarea,
125
+ .gr-textbox input {
126
+ border-radius: 12px !important;
127
+ border: 2px solid rgba(251, 127, 13, 0.2) !important;
128
+ font-size: 14px;
129
+ transition: border-color 0.3s ease;
130
  }
131
+
132
+ .gr-textbox textarea:focus,
133
+ .gr-textbox input:focus {
134
+ border-color: var(--primary-color) !important;
135
+ box-shadow: 0 0 0 3px rgba(251, 127, 13, 0.1) !important;
136
  }
137
+
138
+ .gr-file {
139
+ border-radius: var(--border-radius) !important;
140
+ border: 2px dashed var(--primary-color) !important;
141
+ background-color: rgba(251, 127, 13, 0.05) !important;
142
+ transition: all 0.3s ease;
 
 
 
 
 
 
143
  }
144
+
145
+ .gr-file:hover {
146
+ background-color: rgba(251, 127, 13, 0.1) !important;
147
+ border-color: var(--secondary-color) !important;
148
  }
149
+
150
+ @media (max-width: 768px) {
151
+ .gradio-container {
152
+ padding: 10px;
153
+ }
154
+
155
+ .custom-header h1 {
156
+ font-size: 2rem;
157
+ }
158
+
159
+ .custom-title {
160
+ font-size: 24px;
161
+ }
162
+
163
+ .custom-frame {
164
+ padding: 15px;
165
+ margin: 5px 0;
166
+ }
167
+
168
+ .custom-button {
169
+ font-size: 16px !important;
170
+ padding: 8px 16px !important;
171
+ }
172
  }
173
+ ''')
174
 
175
+ class SessionManager:
176
+ """세션별 데이터 관리"""
177
+ def __init__(self):
178
+ self.sessions = {}
179
+
180
+ def create_session(self):
181
+ """새로운 세션 생성"""
182
+ session_id = str(uuid.uuid4())
183
+ self.sessions[session_id] = {
184
+ 'created_at': time.time(),
185
+ 'data': {},
186
+ 'temp_files': []
187
+ }
188
+ return session_id
189
+
190
+ def get_session_data(self, session_id, key, default=None):
191
+ """세션 데이터 조회"""
192
+ if session_id in self.sessions:
193
+ return self.sessions[session_id]['data'].get(key, default)
194
+ return default
195
+
196
+ def set_session_data(self, session_id, key, value):
197
+ """세션 데이터 저장"""
198
+ if session_id in self.sessions:
199
+ self.sessions[session_id]['data'][key] = value
200
+
201
+ def add_temp_file(self, session_id, file_path):
202
+ """세션에 임시 파일 추가"""
203
+ if session_id in self.sessions:
204
+ self.sessions[session_id]['temp_files'].append(file_path)
205
+
206
+ def cleanup_session(self, session_id):
207
+ """세션 정리"""
208
+ if session_id in self.sessions:
209
+ # 임시 파일들 삭제
210
+ for file_path in self.sessions[session_id]['temp_files']:
211
+ try:
212
+ if os.path.exists(file_path):
213
+ os.remove(file_path)
214
+ except Exception as e:
215
+ print(f"파일 삭제 오류: {e}")
216
+
217
+ # 세션 데이터 삭제
218
+ del self.sessions[session_id]
219
+
220
+ def cleanup_old_sessions(self, max_age_hours=2):
221
+ """오래된 세션 정리 (2시간 이상)"""
222
+ current_time = time.time()
223
+ old_sessions = []
224
 
225
+ for session_id, session_data in self.sessions.items():
226
+ if (current_time - session_data['created_at']) > (max_age_hours * 3600):
227
+ old_sessions.append(session_id)
228
 
229
+ for session_id in old_sessions:
230
+ self.cleanup_session(session_id)
231
+
232
+ # 전역 세션 매니저
233
+ session_manager = SessionManager()
234
 
235
+ def create_app():
236
+ """메인 Gradio 애플리케이션 생성"""
237
+
238
+ demo = gr.Blocks(css=custom_css, theme=gr.themes.Default(
239
+ primary_hue="orange",
240
+ secondary_hue="orange",
241
+ font=[gr.themes.GoogleFont("Noto Sans KR"), "ui-sans-serif", "system-ui"]
242
+ ))
243
+
244
+ with demo:
245
+ gr.HTML(fontawesome_link)
246
 
247
+ # 세션 ID를 숨겨진 상태로 관리
248
+ session_id_state = gr.State(value="")
 
 
 
249
 
250
+ # 세션 초기화 함수
251
+ def initialize_session():
252
+ session_id = session_manager.create_session()
253
+ # 각 세션마다 새로운 분석기 인스턴스 생성
254
+ api_manager = APIManager()
255
+ analyzer = ReviewAnalyzer(api_manager)
256
+ session_manager.set_session_data(session_id, 'analyzer', analyzer)
257
+ return session_id
 
 
 
 
 
 
 
 
 
 
258
 
259
+ # 세션별 분석기 가져오기
260
+ def get_session_analyzer(session_id):
261
+ if not session_id:
262
+ session_id = initialize_session()
263
+
264
+ analyzer = session_manager.get_session_data(session_id, 'analyzer')
265
+ if analyzer is None:
266
+ api_manager = APIManager()
267
+ analyzer = ReviewAnalyzer(api_manager)
268
+ session_manager.set_session_data(session_id, 'analyzer', analyzer)
269
+
270
+ return analyzer, session_id
271
+
272
+ # 구성
273
+ with gr.Tabs() as tabs:
274
+ #############################
275
+ # 엑셀 분석 모드
276
+ #############################
277
+ with gr.TabItem("💾 스마트스토어 엑셀리뷰데이터 활용"):
278
+ with gr.Row():
279
+ with gr.Column(elem_classes="custom-frame"):
280
+ gr.HTML("<div class='custom-title'>📁 데이터 입력</div>")
281
+ file_input = gr.File(label="원본 엑셀 파일 업로드", file_types=[".xlsx"])
282
+ year_radio = gr.Radio(
283
+ choices=[f"{str(y)[-2:]}년" for y in range(2025, 2020, -1)],
284
+ label="분석년도 선택",
285
+ value="25년"
286
+ )
287
+ analyze_button = gr.Button("옵션 분석하기", elem_classes="custom-button")
288
+ with gr.Column(elem_classes="custom-frame"):
289
+ gr.HTML("<div class='custom-title'>📁 분석보고서 다운로드</div>")
290
+ download_final_output = gr.File(label="보고서 다운로드")
291
+
292
+ # 리뷰분석 섹션
293
+ with gr.Column(elem_classes="custom-frame", visible=False) as review_analysis_frame:
294
+ gr.HTML("<div class='custom-title'>📁 리뷰분석</div>")
295
+ top20_dropdown = gr.Dropdown(
296
+ label="아이템옵션 분석",
297
+ choices=["전체옵션분석"],
298
+ value="전체옵션분석"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
299
  )
300
+ review_button = gr.Button("리뷰 분석하기", elem_classes="custom-button")
301
+
302
+ # 분석 결과 섹션들
303
+ with gr.Row():
304
+ with gr.Column(elem_classes="custom-frame"):
305
+ gr.HTML("<div class='custom-title'>✨ 주요긍정리뷰</div>")
306
+ positive_output = gr.Textbox(label="긍정리뷰리스트 (20개)", lines=10, value="")
307
+ with gr.Column(elem_classes="custom-frame"):
308
+ gr.HTML("<div class='custom-title'>✨ 주요부정리뷰</div>")
309
+ negative_output = gr.Textbox(label="부정리뷰리스트 (30개)", lines=10, value="")
310
 
311
+ with gr.Row():
312
+ with gr.Column(elem_classes="custom-frame"):
313
+ gr.HTML("<div class='custom-title'>📢 긍정리뷰 분석</div>")
314
+ positive_analysis_output = gr.Textbox(label="긍정리뷰 분석", lines=8, value="")
315
+ with gr.Column(elem_classes="custom-frame"):
316
+ gr.HTML("<div class='custom-title'>📢 부정리뷰 분석</div>")
317
+ negative_analysis_output = gr.Textbox(label="부정리뷰 분석", lines=8, value="")
318
+
319
+ with gr.Row():
320
+ with gr.Column(elem_classes="custom-frame"):
321
+ gr.HTML("<div class='custom-title'>📊 니즈원인 분석</div>")
322
+ insight_analysis_output = gr.Textbox(label="니즈원인 분석", lines=8, value="")
323
+ with gr.Column(elem_classes="custom-frame"):
324
+ gr.HTML("<div class='custom-title'>🔧 상품판매방향성</div>")
325
+ strategy_analysis_output = gr.Textbox(label="상품판매방향성", lines=8, value="")
326
+
327
+ with gr.Row():
328
+ with gr.Column(elem_classes="custom-frame"):
329
+ gr.HTML("<div class='custom-title'>📝 소싱전략</div>")
330
+ sourcing_analysis_output = gr.Textbox(label="소싱전략", lines=8, value="")
331
+ with gr.Column(elem_classes="custom-frame"):
332
+ gr.HTML("<div class='custom-title'>🖼️ 마케팅전략</div>")
333
+ detail_page_analysis_output = gr.Textbox(label="마케팅전략", lines=8, value="")
334
+
335
+ # 세션별 상태 관리를 위한 숨겨진 상태
336
+ partial_file_state = gr.State(value=None)
337
+
338
+ # 옵션 분석 이벤트 핸들러
339
+ def on_analyze_options(uploaded_file, selected_year, session_id):
340
+ analyzer, session_id = get_session_analyzer(session_id)
341
+
342
+ try:
343
+ result = analyzer.analyze_options(uploaded_file, selected_year)
344
+
345
+ print(f"analyze_options 결과 타입: {type(result)}")
346
+ print(f"analyze_options 결과 내용: {result}")
347
+
348
+ partial_file = None
349
+ top20_list = ["전체옵션분석"]
350
+
351
+ if result is not None:
352
+ if isinstance(result, (list, tuple)) and len(result) >= 2:
353
+ partial_file = result[0]
354
+ top20_list = result[1] if result[1] else ["전체옵션분석"]
355
+ elif hasattr(result, '__getitem__'):
356
+ try:
357
+ partial_file = result[0]
358
+ top20_list = result[1] if len(result) > 1 else ["전체옵션분석"]
359
+ except (IndexError, KeyError, TypeError):
360
+ print("결과 인덱싱 실패, 기본값 사용")
361
+
362
+ if not isinstance(top20_list, list):
363
+ top20_list = ["전체옵션분석"]
364
+
365
+ if len(top20_list) == 0:
366
+ top20_list = ["전체옵션분석"]
367
+
368
+ if partial_file:
369
+ session_manager.add_temp_file(session_id, partial_file)
370
+
371
+ return (
372
+ partial_file,
373
+ gr.update(visible=True if partial_file else False),
374
+ gr.update(choices=top20_list, value=top20_list[0]),
375
+ session_id
376
+ )
377
+ except Exception as e:
378
+ print(f"옵션 분석 오류: {e}")
379
+ import traceback
380
+ traceback.print_exc()
381
+ return None, gr.update(visible=False), gr.update(choices=["전체옵션분석"], value="전체옵션분석"), session_id
382
+
383
+ analyze_button.click(
384
+ fn=on_analyze_options,
385
+ inputs=[file_input, year_radio, session_id_state],
386
+ outputs=[partial_file_state, review_analysis_frame, top20_dropdown, session_id_state]
387
+ )
388
+
389
+ # 리뷰 분석 이벤트 핸들러
390
+ def on_analyze_reviews(partial_file, selected_option, session_id):
391
+ analyzer, session_id = get_session_analyzer(session_id)
392
+
393
+ try:
394
+ result = analyzer.analyze_reviews(partial_file, selected_option)
395
+
396
+ if isinstance(result, tuple) and len(result) >= 9:
397
+ results = result
398
+ else:
399
+ results = (None, "", "", "", "", "", "", "", "")
400
+
401
+ final_file = results[0]
402
+ if final_file:
403
+ session_manager.add_temp_file(session_id, final_file)
404
+
405
+ return results + (session_id,)
406
+ except Exception as e:
407
+ print(f"리뷰 분석 오류: {e}")
408
+ import traceback
409
+ traceback.print_exc()
410
+ return (None, "", "", "", "", "", "", "", "", session_id)
411
+
412
+ review_button.click(
413
+ fn=on_analyze_reviews,
414
+ inputs=[partial_file_state, top20_dropdown, session_id_state],
415
+ outputs=[download_final_output, positive_output, negative_output,
416
+ positive_analysis_output, negative_analysis_output,
417
+ insight_analysis_output, strategy_analysis_output,
418
+ sourcing_analysis_output, detail_page_analysis_output, session_id_state]
419
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
420
 
421
+ #############################
422
+ # 직접 입력 분석 모드
423
+ #############################
424
+ with gr.TabItem("📖 직접 입력한 자료활용"):
425
+ with gr.Row():
426
+ with gr.Column(elem_classes="custom-frame"):
427
+ gr.HTML("<div class='custom-title'>📝 리뷰 직접 입력</div>")
428
+ direct_positive_input = gr.Textbox(
429
+ label="긍정 리뷰 입력",
430
+ placeholder="긍정 리뷰를 여기에 입력하세요.(최대 8000자)",
431
+ lines=10, max_length=8000, value=""
432
+ )
433
+ direct_negative_input = gr.Textbox(
434
+ label="부정 리뷰 입력",
435
+ placeholder="부정 리뷰를 여기에 입력하세요.(최대 8000자)",
436
+ lines=10, max_length=8000, value=""
437
+ )
438
+ direct_review_button = gr.Button("리뷰 분석하기", elem_classes="custom-button")
439
+ with gr.Column(elem_classes="custom-frame"):
440
+ gr.HTML("<div class='custom-title'>📁 분석보고서 다운로드</div>")
441
+ direct_download_output = gr.File(label="분석 보고서 다운로드")
442
+
443
+ # 직접 입력 분석 결과
444
+ with gr.Row():
445
+ with gr.Column(elem_classes="custom-frame"):
446
+ gr.HTML("<div class='custom-title'>📢 긍정리뷰분석</div>")
447
+ direct_positive_analysis_output = gr.Textbox(
448
+ label="긍정리뷰분석", lines=8, value=""
449
+ )
450
+ with gr.Column(elem_classes="custom-frame"):
451
+ gr.HTML("<div class='custom-title'>📢 부정리뷰분석</div>")
452
+ direct_negative_analysis_output = gr.Textbox(
453
+ label="부정리뷰분석", lines=8, value=""
454
+ )
455
+
456
+ with gr.Row():
457
+ with gr.Column(elem_classes="custom-frame"):
458
+ gr.HTML("<div class='custom-title'>📊 니즈원인 분석</div>")
459
+ direct_insight_analysis_output = gr.Textbox(
460
+ label="니즈원인 분석", lines=8, value=""
461
+ )
462
+ with gr.Column(elem_classes="custom-frame"):
463
+ gr.HTML("<div class='custom-title'>🔧 상품판매방향성</div>")
464
+ direct_strategy_analysis_output = gr.Textbox(
465
+ label="상품판매방향성", lines=8, value=""
466
+ )
467
+
468
+ with gr.Row():
469
+ with gr.Column(elem_classes="custom-frame"):
470
+ gr.HTML("<div class='custom-title'>📝 소싱전략</div>")
471
+ direct_sourcing_analysis_output = gr.Textbox(
472
+ label="��싱전략", lines=8, value=""
473
+ )
474
+ with gr.Column(elem_classes="custom-frame"):
475
+ gr.HTML("<div class='custom-title'>🖼️ 마케팅전략</div>")
476
+ direct_detail_page_analysis_output = gr.Textbox(
477
+ label="마케팅전략", lines=8, value=""
478
+ )
479
+
480
+ # 직접 입력 분석 이벤트 핸들러
481
+ def on_direct_analyze(positive_input, negative_input, session_id):
482
+ analyzer, session_id = get_session_analyzer(session_id)
483
+
484
+ try:
485
+ result = analyzer.analyze_direct_reviews(positive_input, negative_input)
486
+
487
+ if isinstance(result, tuple) and len(result) >= 9:
488
+ results = result
489
+ else:
490
+ results = (None, "", "", "", "", "", "", "", "")
491
+
492
+ final_file = results[0]
493
+ if final_file:
494
+ session_manager.add_temp_file(session_id, final_file)
495
+
496
+ return results + (session_id,)
497
+ except Exception as e:
498
+ print(f"직접 분석 오류: {e}")
499
+ import traceback
500
+ traceback.print_exc()
501
+ return (None, "", "", "", "", "", "", "", "", session_id)
502
+
503
+ direct_review_button.click(
504
+ fn=on_direct_analyze,
505
+ inputs=[direct_positive_input, direct_negative_input, session_id_state],
506
+ outputs=[direct_download_output, direct_positive_analysis_output, direct_negative_analysis_output,
507
+ direct_insight_analysis_output, direct_strategy_analysis_output,
508
+ direct_sourcing_analysis_output, direct_detail_page_analysis_output, session_id_state]
509
+ )
510
+
511
+ # 예시 적용 섹션
512
+ with gr.Column(elem_classes="custom-frame"):
513
+ gr.HTML("<div class='custom-title'>📚 예시 적용하기</div>")
514
  with gr.Row():
515
+ example_excel_button = gr.Button("📊 엑셀 분석 예시 적용하기", elem_classes="custom-button")
516
+ example_direct_button = gr.Button("📝 직접 입력 예시 적용하기", elem_classes="custom-button")
517
+ clear_all_button = gr.Button("🗑️ 전체 초기화", elem_classes="custom-button")
 
 
 
518
 
519
+ # 예시 적용 이벤트
520
+ def apply_excel_example(session_id):
521
+ analyzer, session_id = get_session_analyzer(session_id)
522
+ excel_file, year = analyzer.apply_excel_example()
523
+ return excel_file, year, session_id
524
 
525
+ example_excel_button.click(
526
+ fn=apply_excel_example,
527
+ inputs=[session_id_state],
528
+ outputs=[file_input, year_radio, session_id_state]
 
529
  )
530
 
531
+ def apply_direct_example(session_id):
532
+ analyzer, session_id = get_session_analyzer(session_id)
533
+ positive_text, negative_text = analyzer.apply_direct_example()
534
+ return positive_text, negative_text, session_id
535
+
536
+ example_direct_button.click(
537
+ fn=apply_direct_example,
538
+ inputs=[session_id_state],
539
+ outputs=[direct_positive_input, direct_negative_input, session_id_state]
540
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
541
 
542
+ # 전체 초기화 기능
543
+ def clear_all_data(session_id):
544
+ if session_id:
545
+ session_manager.cleanup_session(session_id)
546
+
547
+ new_session_id = initialize_session()
548
+
549
+ return (
550
+ None, "25년", gr.update(visible=False),
551
+ ["전체옵션분석"], "전체옵션분석", None,
552
+ "", "", "", "", "", "", "", "",
553
+ "", "", None, "", "", "", "", "", "",
554
+ new_session_id
555
+ )
556
 
557
+ clear_all_button.click(
558
+ fn=clear_all_data,
559
+ inputs=[session_id_state],
560
+ outputs=[
561
+ file_input, year_radio, review_analysis_frame,
562
+ top20_dropdown, top20_dropdown,
563
+ download_final_output,
564
+ positive_output, negative_output,
565
+ positive_analysis_output, negative_analysis_output,
566
+ insight_analysis_output, strategy_analysis_output,
567
+ sourcing_analysis_output, detail_page_analysis_output,
568
+ direct_positive_input, direct_negative_input,
569
+ direct_download_output,
570
+ direct_positive_analysis_output, direct_negative_analysis_output,
571
+ direct_insight_analysis_output, direct_strategy_analysis_output,
572
+ direct_sourcing_analysis_output, direct_detail_page_analysis_output,
573
+ session_id_state
574
+ ]
575
  )
 
 
 
 
 
 
 
576
 
577
+ # 페이지 로드 시 완전한 초기화
578
+ def init_on_load():
579
+ session_id = initialize_session()
580
+
581
+ return (
582
+ session_id, None, "25년", gr.update(visible=False),
583
+ gr.update(choices=["전체옵션분석"], value="전체옵션분석"), None,
584
+ "", "", "", "", "", "", "", "",
585
+ "", "", None, "", "", "", "", "", ""
586
+ )
587
 
588
+ demo.load(
589
+ fn=init_on_load,
590
+ outputs=[
591
+ session_id_state,
592
+ file_input, year_radio, review_analysis_frame, top20_dropdown,
593
+ download_final_output,
594
+ positive_output, negative_output,
595
+ positive_analysis_output, negative_analysis_output,
596
+ insight_analysis_output, strategy_analysis_output,
597
+ sourcing_analysis_output, detail_page_analysis_output,
598
+ direct_positive_input, direct_negative_input,
599
+ direct_download_output,
600
+ direct_positive_analysis_output, direct_negative_analysis_output,
601
+ direct_insight_analysis_output, direct_strategy_analysis_output,
602
+ direct_sourcing_analysis_output, direct_detail_page_analysis_output
603
+ ]
604
  )
605
+
606
+ return demo
607
 
608
  if __name__ == "__main__":
609
+ app = create_app()
610
+
611
+ # 주기적으로 오래된 세션 정리
612
+ import threading
613
+ def cleanup_timer():
614
+ while True:
615
+ time.sleep(3600)
616
+ session_manager.cleanup_old_sessions()
617
+
618
+ cleanup_thread = threading.Thread(target=cleanup_timer, daemon=True)
619
+ cleanup_thread.start()
620
+
621
+ app.launch(
622
+ share=False,
623
+ server_name="0.0.0.0",
624
+ server_port=7860,
625
+ show_error=True,
626
+ debug=False
627
+ )