ssboost commited on
Commit
bd9e196
·
verified ·
1 Parent(s): 1b3f7ca

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +239 -658
app.py CHANGED
@@ -10,7 +10,7 @@ import zipfile
10
  import re
11
  import json
12
 
13
- # 로깅 설정
14
  logging.basicConfig(level=logging.WARNING, format='%(asctime)s - %(levelname)s - %(message)s')
15
  logger = logging.getLogger(__name__)
16
 
@@ -30,8 +30,8 @@ def get_api_client():
30
  api_endpoint = os.getenv('API_ENDPOINT')
31
 
32
  if not api_endpoint:
33
- logger.error("API_ENDPOINT 환경변수가 설정되지 않았습니다.")
34
- raise ValueError("API_ENDPOINT 환경변수가 설정되지 않았습니다.")
35
 
36
  client = Client(api_endpoint)
37
  logger.info("원격 API 클라이언트 초기화 성공")
@@ -44,8 +44,11 @@ def get_api_client():
44
  # ===== 한국시간 관련 함수 =====
45
  def get_korean_time():
46
  """한국시간 반환"""
47
- korea_tz = pytz.timezone('Asia/Seoul')
48
- return datetime.now(korea_tz)
 
 
 
49
 
50
  def format_korean_datetime(dt=None, format_type="filename"):
51
  """한국시간 포맷팅"""
@@ -61,108 +64,51 @@ def format_korean_datetime(dt=None, format_type="filename"):
61
  else:
62
  return dt.strftime("%y%m%d_%H%M")
63
 
64
- # ===== 데이터 처리 및 검증 함수들 =====
65
- def create_export_data_from_html(analysis_keyword, main_keyword, analysis_html, step1_data=None):
66
- """분석 HTML과 1단계 데이터를 기반으로 export용 데이터 구조 생성 (더미 데이터 제거)"""
67
- logger.info("=== 📊 Export 데이터 구조 생성 시작 (더미 데이터 제거 버전) ===")
 
 
 
 
 
 
 
 
68
 
69
- # 기본 export 데이터 구조
70
- export_data = {
71
- "main_keyword": main_keyword or analysis_keyword,
72
- "analysis_keyword": analysis_keyword,
73
- "analysis_html": analysis_html,
74
- "main_keywords_df": None,
75
- "related_keywords_df": None,
76
- "analysis_completed": True,
77
- "created_at": get_korean_time().isoformat()
78
  }
79
 
80
- # 1단계 데이터에서 main_keywords_df 추출 (실제 데이터만)
81
- if step1_data and isinstance(step1_data, dict):
82
- if "keywords_df" in step1_data:
83
- keywords_df = step1_data["keywords_df"]
84
- if isinstance(keywords_df, dict):
85
- try:
86
- export_data["main_keywords_df"] = pd.DataFrame(keywords_df)
87
- logger.info(f"✅ 1단계 키워드 데이터를 DataFrame으로 변환: {export_data['main_keywords_df'].shape}")
88
- except Exception as e:
89
- logger.warning(f"⚠️ 1단계 데이터 변환 실패: {e}")
90
- export_data["main_keywords_df"] = None
91
- elif hasattr(keywords_df, 'shape'):
92
- export_data["main_keywords_df"] = keywords_df
93
- logger.info(f"✅ 1단계 키워드 DataFrame 사용: {keywords_df.shape}")
94
- else:
95
- logger.info("📋 1단계 키워드 데이터가 유효하지 않음 - None으로 유지")
96
- export_data["main_keywords_df"] = None
97
-
98
- # 분석 HTML에서 연관검색어 정보 추출 시도 (실제 데이터만)
99
- if analysis_html and "연관검색어 분석" in analysis_html:
100
- logger.info("🔍 분석 HTML에서 연관검색어 정보 발견 - 실제 파싱 필요")
101
- # 실제 HTML 파싱 로직이 필요한 부분
102
- # 현재는 더미 데이터 대신 None으로 유지
103
- export_data["related_keywords_df"] = None
104
- logger.info("💡 실제 HTML 파싱 로직 구현 필요 - 연관검색어 데이터는 None으로 유지")
105
-
106
- logger.info(f"📊 Export 데이터 구조 생성 완료 (더미 데이터 없음):")
107
- logger.info(f" - analysis_keyword: {export_data['analysis_keyword']}")
108
- logger.info(f" - main_keywords_df: {export_data['main_keywords_df'].shape if export_data['main_keywords_df'] is not None else 'None'}")
109
- logger.info(f" - related_keywords_df: {export_data['related_keywords_df'].shape if export_data['related_keywords_df'] is not None else 'None'}")
110
- logger.info(f" - analysis_html: {len(str(export_data['analysis_html']))} 문자")
111
-
112
- return export_data
113
-
114
- def validate_and_repair_export_data(export_data):
115
- """Export 데이터 유효성 검사 및 복구 (더미 데이터 제거)"""
116
- logger.info("🔧 Export 데이터 유효성 검사 및 복구 시작 (더미 데이터 제거 버전)")
117
-
118
- if not export_data or not isinstance(export_data, dict):
119
- logger.warning("⚠️ Export 데이터가 없거나 딕셔너리가 아님 - 기본 구조 생성")
120
- return {
121
- "main_keyword": "기본키워드",
122
- "analysis_keyword": "기본분석키워드",
123
- "analysis_html": "<div>기본 분석 결과</div>",
124
- "main_keywords_df": None, # 더미 데이터 대신 None
125
- "related_keywords_df": None, # 더미 데이터 대신 None
126
- "analysis_completed": True
127
- }
128
-
129
- # 필수 키들 확인 및 복구
130
- required_keys = {
131
- "analysis_keyword": "분석키워드",
132
- "main_keyword": "메인키워드",
133
- "analysis_html": "<div>분석 완료</div>",
134
- "analysis_completed": True
135
  }
136
-
137
- for key, default_value in required_keys.items():
138
- if key not in export_data or not export_data[key]:
139
- export_data[key] = default_value
140
- logger.info(f"🔧 {key} 키 복구: {default_value}")
141
-
142
- # DataFrame 데이터 검증 및 변환 (더미 데이터 생성 안함)
143
- for df_key in ["main_keywords_df", "related_keywords_df"]:
144
- if df_key in export_data and export_data[df_key] is not None:
145
- df_data = export_data[df_key]
146
-
147
- # 딕셔너리를 DataFrame으로 변환
148
- if isinstance(df_data, dict):
149
- try:
150
- # 딕셔너리는 None으로 처리
151
- if not df_data:
152
- export_data[df_key] = None
153
- logger.info(f"📋 {df_key} 딕셔너리 - None으로 설정")
154
- else:
155
- export_data[df_key] = pd.DataFrame(df_data)
156
- logger.info(f"✅ {df_key} 딕셔너리를 DataFrame으로 변환 성공")
157
- except Exception as e:
158
- logger.warning(f"⚠️ {df_key} 변환 실패: {e}")
159
- export_data[df_key] = None
160
- elif not hasattr(df_data, 'shape'):
161
- logger.warning(f"⚠️ {df_key}가 DataFrame이 아님 - None으로 설정")
162
- export_data[df_key] = None
163
-
164
- logger.info("✅ Export 데이터 유효성 검사 및 복구 완료 (더미 데이터 없음)")
165
- return export_data
166
 
167
  # ===== 파일 출력 함수들 =====
168
  def create_timestamp_filename(analysis_keyword):
@@ -172,109 +118,6 @@ def create_timestamp_filename(analysis_keyword):
172
  safe_keyword = re.sub(r'[-\s]+', '_', safe_keyword)
173
  return f"{safe_keyword}_{timestamp}_분석결과"
174
 
175
- def export_to_excel(main_keyword, main_keywords_df, analysis_keyword, related_keywords_df, filename_base):
176
- """엑셀 파일로 출력 (실제 데이터만)"""
177
- try:
178
- # 실제 데이터가 있는지 확인
179
- has_main_data = main_keywords_df is not None and not main_keywords_df.empty
180
- has_related_data = related_keywords_df is not None and not related_keywords_df.empty
181
-
182
- if not has_main_data and not has_related_data:
183
- logger.info("📋 생성할 데이터가 없어 엑셀 파일 생성 건너뜀")
184
- return None
185
-
186
- excel_filename = f"{filename_base}.xlsx"
187
- excel_path = os.path.join(tempfile.gettempdir(), excel_filename)
188
-
189
- with pd.ExcelWriter(excel_path, engine='xlsxwriter') as writer:
190
- # 워크북과 워크시트 스타일 설정
191
- workbook = writer.book
192
-
193
- # 헤더 스타일
194
- header_format = workbook.add_format({
195
- 'bold': True,
196
- 'text_wrap': True,
197
- 'valign': 'top',
198
- 'fg_color': '#D7E4BC',
199
- 'border': 1
200
- })
201
-
202
- # 데이터 스타일
203
- data_format = workbook.add_format({
204
- 'text_wrap': True,
205
- 'valign': 'top',
206
- 'border': 1
207
- })
208
-
209
- # 숫자 포맷
210
- number_format = workbook.add_format({
211
- 'num_format': '#,##0',
212
- 'text_wrap': True,
213
- 'valign': 'top',
214
- 'border': 1
215
- })
216
-
217
- # 첫 번째 시트: 메인키워드 조합키워드 (실제 데이터만)
218
- if has_main_data:
219
- main_keywords_df.to_excel(writer, sheet_name=f'{main_keyword}_조합키워드', index=False)
220
- worksheet1 = writer.sheets[f'{main_keyword}_조합키워드']
221
-
222
- # 헤더 스타일 적용
223
- for col_num, value in enumerate(main_keywords_df.columns.values):
224
- worksheet1.write(0, col_num, value, header_format)
225
-
226
- # 데이터 스타일 적용
227
- for row_num in range(1, len(main_keywords_df) + 1):
228
- for col_num, value in enumerate(main_keywords_df.iloc[row_num-1]):
229
- if isinstance(value, (int, float)) and col_num in [1, 2, 3]: # 검색량 컬럼
230
- worksheet1.write(row_num, col_num, value, number_format)
231
- else:
232
- worksheet1.write(row_num, col_num, value, data_format)
233
-
234
- # 열 너비 자동 조정
235
- for i, col in enumerate(main_keywords_df.columns):
236
- max_len = max(
237
- main_keywords_df[col].astype(str).map(len).max(),
238
- len(str(col))
239
- )
240
- worksheet1.set_column(i, i, min(max_len + 2, 50))
241
-
242
- logger.info(f"✅ 메인키워드 시트 생성: {main_keywords_df.shape}")
243
-
244
- # 두 번째 시트: 분석키워드 연관검색어 (실제 데이터만)
245
- if has_related_data:
246
- related_keywords_df.to_excel(writer, sheet_name=f'{analysis_keyword}_연관검색어', index=False)
247
- worksheet2 = writer.sheets[f'{analysis_keyword}_연관검색어']
248
-
249
- # 헤더 스타일 적용
250
- for col_num, value in enumerate(related_keywords_df.columns.values):
251
- worksheet2.write(0, col_num, value, header_format)
252
-
253
- # 데이터 스타일 적용
254
- for row_num in range(1, len(related_keywords_df) + 1):
255
- for col_num, value in enumerate(related_keywords_df.iloc[row_num-1]):
256
- if isinstance(value, (int, float)) and col_num in [1, 2, 3]: # 검색량 컬럼
257
- worksheet2.write(row_num, col_num, value, number_format)
258
- else:
259
- worksheet2.write(row_num, col_num, value, data_format)
260
-
261
- # 열 너비 자동 조정
262
- for i, col in enumerate(related_keywords_df.columns):
263
- max_len = max(
264
- related_keywords_df[col].astype(str).map(len).max(),
265
- len(str(col))
266
- )
267
- worksheet2.set_column(i, i, min(max_len + 2, 50))
268
-
269
- logger.info(f"✅ 연관검색어 시트 생성: {related_keywords_df.shape}")
270
-
271
- logger.info(f"엑셀 파일 생성 완료: {excel_path}")
272
- return excel_path
273
-
274
- except Exception as e:
275
- logger.error(f"엑셀 파일 생성 오류: {e}")
276
- return None
277
-
278
  def export_to_html(analysis_html, filename_base):
279
  """HTML 파일로 출력 - 한국시간 적용"""
280
  try:
@@ -291,7 +134,7 @@ def export_to_html(analysis_html, filename_base):
291
  <head>
292
  <meta charset="UTF-8">
293
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
294
- <title>키워드 심충분석 결과</title>
295
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
296
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/static/pretendard.css">
297
  <style>
@@ -395,8 +238,8 @@ def export_to_html(analysis_html, filename_base):
395
  <body>
396
  <div class="container">
397
  <div class="header">
398
- <h1><i class="fas fa-chart-line"></i> 키워드 심충분석 결과</h1>
399
- <p>AI 상품 소싱 분석 시스템 v3.2 (더미 데이터 제거 버전)</p>
400
  </div>
401
  <div class="content">
402
  {analysis_html}
@@ -419,418 +262,170 @@ def export_to_html(analysis_html, filename_base):
419
  logger.error(f"HTML 파일 생성 오류: {e}")
420
  return None
421
 
422
- def create_zip_file(excel_path, html_path, filename_base):
423
- """압축 파일 생성"""
424
  try:
425
  zip_filename = f"{filename_base}.zip"
426
  zip_path = os.path.join(tempfile.gettempdir(), zip_filename)
427
 
428
- files_added = 0
429
  with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
430
- if excel_path and os.path.exists(excel_path):
431
- zipf.write(excel_path, f"{filename_base}.xlsx")
432
- logger.info(f"엑셀 파일 압축 추가: {filename_base}.xlsx")
433
- files_added += 1
434
-
435
  if html_path and os.path.exists(html_path):
436
  zipf.write(html_path, f"{filename_base}.html")
437
  logger.info(f"HTML 파일 압축 추가: {filename_base}.html")
438
- files_added += 1
439
-
440
- if files_added == 0:
441
- logger.warning("압축할 파일이 없음")
442
- return None
443
 
444
- logger.info(f"압축 파일 생성 완료: {zip_path} ({files_added}개 파일)")
445
  return zip_path
446
 
447
  except Exception as e:
448
  logger.error(f"압축 파일 생성 오류: {e}")
449
  return None
450
 
451
- def export_analysis_results_enhanced(export_data):
452
- """강화된 분석 결과 출력 메인 함수 (더미 데이터 제거)"""
453
  try:
454
- logger.info("=== 📊 강화된 출력 함수 시작 (더미 데이터 제거 버전) ===")
 
 
455
 
456
- # 데이터 유효성 검사 및 복구
457
- export_data = validate_and_repair_export_data(export_data)
458
 
459
- analysis_keyword = export_data.get("analysis_keyword", "기본키워드")
460
- analysis_html = export_data.get("analysis_html", "<div>분석 완료</div>")
461
- main_keyword = export_data.get("main_keyword", analysis_keyword)
462
- main_keywords_df = export_data.get("main_keywords_df")
463
- related_keywords_df = export_data.get("related_keywords_df")
464
 
465
- logger.info(f"🔍 처리할 데이터:")
466
- logger.info(f" - analysis_keyword: '{analysis_keyword}'")
467
- logger.info(f" - main_keyword: '{main_keyword}'")
468
- logger.info(f" - analysis_html: {len(str(analysis_html))} 문자")
469
- logger.info(f" - main_keywords_df: {main_keywords_df.shape if main_keywords_df is not None else 'None'}")
470
- logger.info(f" - related_keywords_df: {related_keywords_df.shape if related_keywords_df is not None else 'None'}")
471
 
472
  # 파일명 생성 (한국시간 적용)
473
  filename_base = create_timestamp_filename(analysis_keyword)
474
- logger.info(f"📁 출력 파일명: {filename_base}")
475
-
476
- # HTML 파일은 분석 결과가 있으면 생성
477
- html_path = None
478
- if analysis_html and len(str(analysis_html).strip()) > 20: # 의미있는 HTML인지 확인
479
- logger.info("🌐 HTML 파일 생성 시작...")
480
- html_path = export_to_html(analysis_html, filename_base)
481
- if html_path:
482
- logger.info(f"✅ HTML 파일 생성 성공: {html_path}")
483
- else:
484
- logger.error("❌ HTML 파일 생성 실패")
485
- else:
486
- logger.info("📄 분석 HTML이 없어 HTML 파일 생성 건너뜀")
487
-
488
- # 엑셀 파일 생성 (실제 DataFrame이 있는 경우만)
489
- excel_path = None
490
- if (main_keywords_df is not None and not main_keywords_df.empty) or \
491
- (related_keywords_df is not None and not related_keywords_df.empty):
492
- logger.info("📊 엑셀 파일 생성 시작...")
493
- excel_path = export_to_excel(
494
- main_keyword,
495
- main_keywords_df,
496
- analysis_keyword,
497
- related_keywords_df,
498
- filename_base
499
- )
500
- if excel_path:
501
- logger.info(f"✅ 엑셀 파일 생성 성공: {excel_path}")
502
- else:
503
- logger.warning("⚠️ 엑셀 파일 생성 실패")
504
- else:
505
- logger.info("📊 실제 DataFrame 데이터가 없어 엑셀 파일 생성 생략")
506
 
507
- # 생성된 파일이 있는지 확인
508
- if not html_path and not excel_path:
509
- logger.warning("⚠️ 생성된 파일이 없음")
510
- return None, "⚠️ 생성할 수 있는 데이터가 없습니다. 분석을 먼저 완료해주세요."
511
 
512
  # 압축 파일 생성
513
- logger.info("📦 압축 파일 생성 시작...")
514
- zip_path = create_zip_file(excel_path, html_path, filename_base)
515
- if zip_path:
516
- file_types = []
517
- if html_path:
518
- file_types.append("HTML")
519
- if excel_path:
520
- file_types.append("엑셀")
521
-
522
- file_list = " + ".join(file_types)
523
- logger.info(f"✅ 압축 파일 생성 성공: {zip_path} ({file_list})")
524
- return zip_path, f"✅ 분석 결과가 성공적으로 출력되었습니다!\n파일명: {filename_base}.zip\n포함 파일: {file_list}\n\n💡 더미 데이터 제거 버전 - 실제 분석 데이터만 포함됩니다."
525
  else:
526
- logger.error("❌ 압축 파일 생성 실패")
527
- return None, "압축 파일 생성에 실패했습니다."
528
 
529
  except Exception as e:
530
- logger.error(f" 강화된 출력 함수 전체 오류: {e}")
531
- import traceback
532
- logger.error(f"스택 트레이스:\n{traceback.format_exc()}")
533
  return None, f"출력 중 오류가 발생했습니다: {str(e)}"
534
 
535
- # ===== 로딩 애니메이션 =====
536
- def create_loading_animation():
537
- """로딩 애니메이션 HTML"""
538
- return """
539
- <div style="display: flex; flex-direction: column; align-items: center; padding: 40px; background: white; border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.1);">
540
- <div style="width: 60px; height: 60px; border: 4px solid #f3f3f3; border-top: 4px solid #FB7F0D; border-radius: 50%; animation: spin 1s linear infinite; margin-bottom: 20px;"></div>
541
- <h3 style="color: #FB7F0D; margin: 10px 0; font-size: 18px;">분석 중입니다...</h3>
542
- <p style="color: #666; margin: 5px 0; text-align: center;">원격 서버에서 데이터를 수집하고 AI가 분석하고 있습니다.<br>잠시만 기다려주세요.</p>
543
- <div style="width: 200px; height: 4px; background: #f0f0f0; border-radius: 2px; margin-top: 15px; overflow: hidden;">
544
- <div style="width: 100%; height: 100%; background: linear-gradient(90deg, #FB7F0D, #ff9a8b); border-radius: 2px; animation: progress 2s ease-in-out infinite;"></div>
545
- </div>
546
- </div>
547
-
548
- <style>
549
- @keyframes spin {
550
- 0% { transform: rotate(0deg); }
551
- 100% { transform: rotate(360deg); }
552
- }
553
-
554
- @keyframes progress {
555
- 0% { transform: translateX(-100%); }
556
- 100% { transform: translateX(100%); }
557
- }
558
- </style>
559
- """
560
-
561
- # ===== 에러 처리 함수 =====
562
- def generate_error_response(error_message):
563
- """에러 응답 생성"""
564
- return f'''
565
- <div style="color: red; padding: 30px; text-align: center; width: 100%;
566
- background-color: #f8d7da; border-radius: 12px; border: 1px solid #f5c6cb;">
567
- <h3 style="margin-bottom: 15px;">❌ 연결 오류</h3>
568
- <p style="margin-bottom: 20px;">{error_message}</p>
569
- <div style="background: white; padding: 15px; border-radius: 8px; color: #333;">
570
- <h4>해결 방법:</h4>
571
- <ul style="text-align: left; padding-left: 20px;">
572
- <li>네트워크 연결을 확인해주세요</li>
573
- <li>원격 서버 상태를 확인해주세요</li>
574
- <li>잠시 후 다시 시도해주세요</li>
575
- <li>문제가 지속되면 관리자에게 문의하세요</li>
576
- </ul>
577
- </div>
578
- </div>
579
- '''
580
-
581
  # ===== 원격 API 호출 함수들 =====
582
- def call_collect_data_api(keyword):
583
- """1단계: 상품 데이터 수집 API 호출"""
584
  try:
585
  client = get_api_client()
586
  if not client:
587
  return generate_error_response("API 클라이언트를 초기화할 수 없습니다."), {}
588
 
589
- logger.info("원격 API 호출: 상품 데이터 수집")
590
  result = client.predict(
591
- keyword=keyword,
592
- api_name="/on_collect_data"
593
  )
594
 
595
- logger.info(f"데이터 수집 API 결과 타입: {type(result)}")
596
-
597
- # 결과가 튜플인 경우 번째 요소는 HTML, 두 번째는 세션 데이터
598
- if isinstance(result, tuple) and len(result) == 2:
599
- html_result, session_data = result
600
-
601
- # 세션 데이터가 제대로 있는지 확인
602
- if isinstance(session_data, dict):
603
- logger.info(f"데이터 수집 세션 데이터 수신: {list(session_data.keys()) if session_data else '빈 딕셔너리'}")
604
- return html_result, session_data
605
- else:
606
- logger.warning("세션 데이터가 딕셔너리가 아닙니다.")
607
- return html_result, {}
608
  else:
609
- logger.warning("예상과 다른 데이터 수집 결과 형태")
610
- return str(result), {"keywords_collected": True}
611
 
612
  except Exception as e:
613
- logger.error(f"상품 데이터 수집 API 호출 오류: {e}")
614
  return generate_error_response(f"원격 서버 연결 실패: {str(e)}"), {}
615
 
616
- def call_analyze_keyword_api_enhanced(analysis_keyword, base_keyword, keywords_data):
617
- """3단계: 강화된 키워드 심충분석 API 호출 (더미 데이터 제거)"""
618
  try:
619
  client = get_api_client()
620
  if not client:
621
- return generate_error_response("API 클라이언트를 초기화할 수 없습니다."), {}
622
-
623
- logger.info("=== 🚀 강화된 키워드 심충분석 API 호출 (더미 데이터 제거) ===")
624
- logger.info(f"파라미터 - analysis_keyword: '{analysis_keyword}'")
625
- logger.info(f"파라미터 - base_keyword: '{base_keyword}'")
626
- logger.info(f"파라미터 - keywords_data 타입: {type(keywords_data)}")
627
 
628
- # 원격 API 호출
629
  result = client.predict(
630
- analysis_keyword,
631
- base_keyword,
632
- keywords_data,
633
- api_name="/on_analyze_keyword"
634
  )
635
 
636
- logger.info(f"📡 원격 API 응답 수신:")
637
- logger.info(f" - 응답 타입: {type(result)}")
638
- logger.info(f" - 응답 길이: {len(result) if hasattr(result, '__len__') else 'N/A'}")
639
 
640
- # 응답 처리 Export 데이터 구조 생성
641
  if isinstance(result, tuple) and len(result) == 2:
642
- html_result, remote_export_data = result
643
-
644
- logger.info(f"📊 원격 export 데이터:")
645
- logger.info(f" - 타입: {type(remote_export_data)}")
646
- logger.info(f" - 키들: {list(remote_export_data.keys()) if isinstance(remote_export_data, dict) else 'None'}")
647
-
648
- # HTML 결과가 있으면 Export 데이터 구조 생성 (더미 데이터 없이)
649
- if html_result:
650
- logger.info("🔧 Export 데이터 구조 생성 시작 (더미 데이터 제거)")
651
- enhanced_export_data = create_export_data_from_html(
652
- analysis_keyword=analysis_keyword,
653
- main_keyword=base_keyword,
654
- analysis_html=html_result,
655
- step1_data=keywords_data
656
- )
657
-
658
- # 원격에서 온 실제 데이터가 있으면 병합
659
- if isinstance(remote_export_data, dict) and remote_export_data:
660
- logger.info("🔗 원격 실제 데이터와 로컬 데이터 병합")
661
- for key, value in remote_export_data.items():
662
- if value is not None and key in ["main_keywords_df", "related_keywords_df"]:
663
- # DataFrame 데이터만 검증하여 병합
664
- if isinstance(value, dict) and value: # 빈 딕셔너리가 아닌 경우만
665
- enhanced_export_data[key] = value
666
- logger.info(f" - {key} 원격 실제 데이터로 업데이트")
667
- elif hasattr(value, 'shape') and not value.empty: # DataFrame이고 비어있지 않은 경우
668
- enhanced_export_data[key] = value
669
- logger.info(f" - {key} 원격 DataFrame 데이터로 업데이트")
670
- elif value is not None and key not in ["main_keywords_df", "related_keywords_df"]:
671
- enhanced_export_data[key] = value
672
- logger.info(f" - {key} 원격 데이터로 업데이트")
673
-
674
- logger.info(f"✅ 최종 Export 데이터 구조 (더미 데이터 없음):")
675
- logger.info(f" - 키 개수: {len(enhanced_export_data)}")
676
- logger.info(f" - 키 목록: {list(enhanced_export_data.keys())}")
677
-
678
- return html_result, enhanced_export_data
679
  else:
680
- logger.warning("⚠️ HTML 결과가 비어있음")
681
- return str(result), {}
682
  else:
683
- logger.warning("⚠️ 예상과 다른 API 응답 형태")
684
- # HTML만 반환된 경우도 처리
685
- if isinstance(result, str) and len(result) > 100: # HTML일 가능성이 높음
686
- logger.info("📄 HTML 문자열로 추정되는 응답 - Export 데이터 생성 (더미 데이터 없이)")
687
- enhanced_export_data = create_export_data_from_html(
688
- analysis_keyword=analysis_keyword,
689
- main_keyword=base_keyword,
690
- analysis_html=result,
691
- step1_data=keywords_data
692
- )
693
- return result, enhanced_export_data
694
- else:
695
- return str(result), {}
696
 
697
  except Exception as e:
698
- logger.error(f" 키워드 심충분석 API 호출 오류: {e}")
699
- import traceback
700
- logger.error(f"스택 트레이스:\n{traceback.format_exc()}")
701
- return generate_error_response(f"원격 서버 연결 실패: {str(e)}"), {}
702
 
703
  # ===== 그라디오 인터페이스 =====
704
  def create_interface():
705
- # CSS 스타일링 (기존과 동일)
706
- custom_css = """
707
- /* 기존 다크모드 자동 변경 AI 상품 소싱 분석 시스템 CSS */
708
- :root {
709
- --primary-color: #FB7F0D;
710
- --secondary-color: #ff9a8b;
711
- --accent-color: #FF6B6B;
712
- --background-color: #FFFFFF;
713
- --card-bg: #ffffff;
714
- --input-bg: #ffffff;
715
- --text-color: #334155;
716
- --text-secondary: #64748b;
717
- --border-color: #dddddd;
718
- --border-light: #e5e5e5;
719
- --table-even-bg: #f3f3f3;
720
- --table-hover-bg: #f0f0f0;
721
- --shadow: 0 8px 30px rgba(251, 127, 13, 0.08);
722
- --shadow-light: 0 2px 4px rgba(0, 0, 0, 0.1);
723
- --border-radius: 18px;
724
- }
725
- @media (prefers-color-scheme: dark) {
726
- :root {
727
- --background-color: #1a1a1a;
728
- --card-bg: #2d2d2d;
729
- --input-bg: #2d2d2d;
730
- --text-color: #e5e5e5;
731
- --text-secondary: #a1a1aa;
732
- --border-color: #404040;
733
- --border-light: #525252;
734
- --table-even-bg: #333333;
735
- --table-hover-bg: #404040;
736
- --shadow: 0 8px 30px rgba(0, 0, 0, 0.3);
737
- --shadow-light: 0 2px 4px rgba(0, 0, 0, 0.2);
738
  }
739
- }
740
- body {
741
- font-family: 'Pretendard', 'Noto Sans KR', -apple-system, BlinkMacSystemFont, sans-serif;
742
- background-color: var(--background-color) !important;
743
- color: var(--text-color) !important;
744
- line-height: 1.6;
745
- margin: 0;
746
- padding: 0;
747
- transition: background-color 0.3s ease, color 0.3s ease;
748
- }
749
- .gradio-container {
750
- width: 100%;
751
- margin: 0 auto;
752
- padding: 20px;
753
- background-color: var(--background-color) !important;
754
- }
755
- .custom-frame {
756
- background-color: var(--card-bg) !important;
757
- border: 1px solid var(--border-light) !important;
758
- border-radius: var(--border-radius);
759
- padding: 20px;
760
- margin: 10px 0;
761
- box-shadow: var(--shadow) !important;
762
- color: var(--text-color) !important;
763
- }
764
- .custom-button {
765
- border-radius: 30px !important;
766
- background: var(--primary-color) !important;
767
- color: white !important;
768
- font-size: 18px !important;
769
- padding: 10px 20px !important;
770
- border: none;
771
- box-shadow: 0 4px 8px rgba(251, 127, 13, 0.25);
772
- transition: transform 0.3s ease;
773
- height: 45px !important;
774
- width: 100% !important;
775
- }
776
- .custom-button:hover {
777
- transform: translateY(-2px);
778
- box-shadow: 0 6px 12px rgba(251, 127, 13, 0.3);
779
- }
780
- .export-button {
781
- background: linear-gradient(135deg, #28a745, #20c997) !important;
782
- color: white !important;
783
- border-radius: 25px !important;
784
- height: 50px !important;
785
- font-size: 17px !important;
786
- font-weight: bold !important;
787
- width: 100% !important;
788
- margin-top: 20px !important;
789
- }
790
- .section-title {
791
- display: flex;
792
- align-items: center;
793
- font-size: 20px;
794
- font-weight: 700;
795
- color: var(--text-color) !important;
796
- margin-bottom: 10px;
797
- padding-bottom: 5px;
798
- border-bottom: 2px solid var(--primary-color);
799
- font-family: 'Pretendard', 'Noto Sans KR', -apple-system, BlinkMacSystemFont, sans-serif;
800
- }
801
- .section-title img, .section-title i {
802
- margin-right: 10px;
803
- font-size: 20px;
804
- color: var(--primary-color);
805
- }
806
- .gr-input, .gr-text-input, .gr-sample-inputs,
807
- input[type="text"], input[type="number"], textarea, select {
808
- border-radius: var(--border-radius) !important;
809
- border: 1px solid var(--border-color) !important;
810
- padding: 12px !important;
811
- box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05) !important;
812
- transition: all 0.3s ease !important;
813
- background-color: var(--input-bg) !important;
814
- color: var(--text-color) !important;
815
- }
816
- .gr-input:focus, .gr-text-input:focus,
817
- input[type="text"]:focus, textarea:focus, select:focus {
818
- border-color: var(--primary-color) !important;
819
- outline: none !important;
820
- box-shadow: 0 0 0 2px rgba(251, 127, 13, 0.2) !important;
821
- }
822
- .fade-in {
823
- animation: fadeIn 0.5s ease-out;
824
- }
825
- @keyframes fadeIn {
826
- from { opacity: 0; transform: translateY(10px); }
827
- to { opacity: 1; transform: translateY(0); }
828
- }
829
- """
830
 
831
  with gr.Blocks(
832
  css=custom_css,
833
- title="🛒 AI 상품 소싱 분석기 v3.2 (더미 데이터 제거)",
834
  theme=gr.themes.Default(primary_hue="orange", secondary_hue="orange")
835
  ) as interface:
836
 
@@ -840,103 +435,64 @@ def create_interface():
840
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/static/pretendard.css">
841
  """)
842
 
843
- # 세션별 상태 변수
844
- keywords_data_state = gr.State()
845
  export_data_state = gr.State({})
846
 
847
- # === UI 컴포넌트들 ===
848
- with gr.Column(elem_classes="custom-frame fade-in"):
849
- gr.HTML('<div class="section-title"><i class="fas fa-search"></i> 1단계: 메인 키워드 입력</div>')
850
-
851
- keyword_input = gr.Textbox(
852
- label="상품 메인키워드",
853
- placeholder="예: 슬리퍼, 무선이어폰, 핸드크림",
854
- value="",
855
- elem_id="keyword_input"
856
- )
857
-
858
- collect_data_btn = gr.Button("1단계: 상품 데이터 수집하기", elem_classes="custom-button", size="lg")
859
-
860
  with gr.Column(elem_classes="custom-frame fade-in"):
861
- gr.HTML('<div class="section-title"><i class="fas fa-database"></i> 2단계: 수집된 키워드 목록</div>')
862
- keywords_result = gr.HTML()
863
-
864
- with gr.Column(elem_classes="custom-frame fade-in"):
865
- gr.HTML('<div class="section-title"><i class="fas fa-bullseye"></i> 3단계: 분석할 키워드 선택</div>')
866
 
867
  analysis_keyword_input = gr.Textbox(
868
  label="분석할 키워드",
869
- placeholder=" 목록에서 원하는 키워드를 입력하세요 (예: 통굽 슬리퍼)",
870
  value="",
871
  elem_id="analysis_keyword_input"
872
  )
873
 
874
- analyze_keyword_btn = gr.Button("키워드 심충분석 하기", elem_classes="custom-button", size="lg")
875
 
 
876
  with gr.Column(elem_classes="custom-frame fade-in"):
877
- gr.HTML('<div class="section-title"><i class="fas fa-chart-line"></i> 키워드 심충분석</div>')
878
- analysis_result = gr.HTML(label="키워드 심충분석")
879
 
 
880
  with gr.Column(elem_classes="custom-frame fade-in"):
881
  gr.HTML('<div class="section-title"><i class="fas fa-download"></i> 분석 결과 출력</div>')
882
-
883
- gr.HTML("""
884
- <div style="background: #e3f2fd; border-left: 4px solid #2196f3; padding: 15px; margin: 10px 0; border-radius: 5px;">
885
- <h4 style="margin: 0 0 10px 0; color: #1976d2;"><i class="fas fa-info-circle"></i> 실제 데이터 출력 버전</h4>
886
- <p style="margin: 0; color: #1976d2; font-size: 14px;">
887
- • 분석된 데이터를 파일로 출력됩니다<br>
888
- </p>
889
- </div>
890
- """)
891
 
892
  export_btn = gr.Button("📊 분석결과 출력하기", elem_classes="export-button", size="lg")
893
  export_result = gr.HTML()
894
  download_file = gr.File(label="다운로드", visible=False)
895
 
896
  # ===== 이벤트 핸들러 =====
897
- def on_collect_data(keyword):
898
- if not keyword.strip():
899
- return ("<div style='color: red; padding: 20px; text-align: center; width: 100%;'>키워드를 입력해주세요.</div>", None)
900
-
901
- # 로딩 상태 표시
902
- yield (create_loading_animation(), None)
903
-
904
- # 원격 API 호출
905
- result_html, result_data = call_collect_data_api(keyword)
906
-
907
- yield (result_html, result_data)
908
-
909
- def on_analyze_keyword(analysis_keyword, base_keyword, keywords_data):
910
  if not analysis_keyword.strip():
911
- return "<div style='color: red; padding: 20px; text-align: center; width: 100%;'>분석할 키워드를 입력해주세요.</div>", {}
912
 
913
  # 로딩 상태 표시
914
  yield create_loading_animation(), {}
915
 
916
- # 강화된 API 호출 (더미 데이터 제거)
917
- html_result, enhanced_export_data = call_analyze_keyword_api_enhanced(
918
- analysis_keyword, base_keyword, keywords_data
919
- )
920
 
921
- yield html_result, enhanced_export_data
922
-
 
923
  def on_export_results(export_data):
924
- """강화된 분석 결과 출력 핸들러 (더미 데이터 제거)"""
925
  try:
926
- logger.info(f"📊 입력 export_data: {type(export_data)}")
927
- if isinstance(export_data, dict):
928
- logger.info(f"📋 export_data 키들: {list(export_data.keys())}")
929
-
930
- # 강화된 출력 함수 호출 (더미 데이터 제거)
931
- zip_path, message = export_analysis_results_enhanced(export_data)
932
 
933
  if zip_path:
 
934
  success_html = f"""
935
  <div style="background: #d4edda; border: 1px solid #c3e6cb; padding: 20px; border-radius: 8px; margin: 10px 0;">
936
  <h4 style="color: #155724; margin: 0 0 15px 0;"><i class="fas fa-check-circle"></i> 출력 완료!</h4>
937
  <p style="color: #155724; margin: 0; line-height: 1.6;">
938
  {message}<br>
939
- <strong>데이터출력:</strong><br>
 
940
  <br>
941
  <i class="fas fa-download"></i> 아래 다운로드 버튼을 클릭하여 파일을 저장하세요.<br>
942
  <small style="color: #666;">⏰ 한국시간 기준으로 파일명이 생성됩니다.</small>
@@ -945,64 +501,60 @@ def create_interface():
945
  """
946
  return success_html, gr.update(value=zip_path, visible=True)
947
  else:
948
- error_html = f"""
949
- <div style="background: #f8d7da; border: 1px solid #f5c6cb; padding: 20px; border-radius: 8px; margin: 10px 0;">
950
- <h4 style="color: #721c24; margin: 0 0 10px 0;"><i class="fas fa-exclamation-triangle"></i> 출력 실패</h4>
951
- <p style="color: #721c24; margin: 0;">{message}</p>
952
- <div style="margin-top: 15px; padding: 15px; background: white; border-radius: 5px;">
953
- <h5 style="color: #721c24; margin: 0 0 10px 0;">🔍 디버깅 정보:</h5>
954
- <ul style="color: #721c24; margin: 0; padding-left: 20px;">
955
- <li>Export 데이터 타입: {type(export_data)}</li>
956
- <li>Export 데이터 유효성: {'유효' if export_data else '무효'}</li>
957
- <li>키워드 심충분석 상태: {'완료' if export_data.get('analysis_completed') else '미완료'}</li>
958
- </ul>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
959
  </div>
960
- </div>
961
- """
962
- logger.error("❌ 강화된 출력 실패")
963
- return error_html, gr.update(visible=False)
964
 
965
  except Exception as e:
966
- logger.error(f"❌ 강화된 출력 핸들러 오류: {e}")
967
- import traceback
968
- logger.error(f"스택 트레이스:\n{traceback.format_exc()}")
969
-
970
  error_html = f"""
971
  <div style="background: #f8d7da; border: 1px solid #f5c6cb; padding: 20px; border-radius: 8px; margin: 10px 0;">
972
  <h4 style="color: #721c24; margin: 0 0 10px 0;"><i class="fas fa-exclamation-triangle"></i> 시스템 오류</h4>
973
- <p style="color: #721c24; margin: 0;">강화된 출력 중 시스템 오류가 발생했습니다:</p>
974
- <code style="display: block; margin: 10px 0; padding: 10px; background: #f8f9fa; border-radius: 3px; color: #721c24;">
975
- {type(e).__name__}: {str(e)}
976
- </code>
977
- <div style="margin-top: 15px; padding: 10px; background: #fff3cd; border-radius: 5px;">
978
- <p style="margin: 0; color: #856404; font-size: 14px;">
979
- 💡 실제 분석 결과가 있어야만 파일이 생성됩니다.
980
- </p>
981
- </div>
982
  </div>
983
  """
984
  return error_html, gr.update(visible=False)
985
 
986
  # ===== 이벤트 연결 =====
987
- collect_data_btn.click(
988
- fn=on_collect_data,
989
- inputs=[keyword_input],
990
- outputs=[keywords_result, keywords_data_state],
991
- api_name="on_collect_data"
992
- )
993
-
994
  analyze_keyword_btn.click(
995
  fn=on_analyze_keyword,
996
- inputs=[analysis_keyword_input, keyword_input, keywords_data_state],
997
- outputs=[analysis_result, export_data_state],
998
- api_name="on_analyze_keyword"
999
  )
1000
 
1001
  export_btn.click(
1002
  fn=on_export_results,
1003
  inputs=[export_data_state],
1004
- outputs=[export_result, download_file],
1005
- api_name="on_export_results"
1006
  )
1007
 
1008
  return interface
@@ -1014,8 +566,37 @@ if __name__ == "__main__":
1014
  import pytz
1015
  logger.info("✅ pytz 모듈 로드 성공 - 한국시간 지원")
1016
  except ImportError:
 
1017
  logger.info("시스템 시간을 사용합니다.")
1018
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1019
  # 앱 실행
1020
  app = create_interface()
1021
  app.launch(server_name="0.0.0.0", server_port=7860, share=True)
 
10
  import re
11
  import json
12
 
13
+ # 로깅 설정 - 클라이언트 정보 숨김
14
  logging.basicConfig(level=logging.WARNING, format='%(asctime)s - %(levelname)s - %(message)s')
15
  logger = logging.getLogger(__name__)
16
 
 
30
  api_endpoint = os.getenv('API_ENDPOINT')
31
 
32
  if not api_endpoint:
33
+ logger.error("API 엔드포인트가 설정되지 않았습니다.")
34
+ raise ValueError("API 엔드포인트가 설정되지 않았습니다.")
35
 
36
  client = Client(api_endpoint)
37
  logger.info("원격 API 클라이언트 초기화 성공")
 
44
  # ===== 한국시간 관련 함수 =====
45
  def get_korean_time():
46
  """한국시간 반환"""
47
+ try:
48
+ korea_tz = pytz.timezone('Asia/Seoul')
49
+ return datetime.now(korea_tz)
50
+ except:
51
+ return datetime.now()
52
 
53
  def format_korean_datetime(dt=None, format_type="filename"):
54
  """한국시간 포맷팅"""
 
64
  else:
65
  return dt.strftime("%y%m%d_%H%M")
66
 
67
+ # ===== 로딩 애니메이션 =====
68
+ def create_loading_animation():
69
+ """로딩 애니메이션 HTML"""
70
+ return """
71
+ <div style="display: flex; flex-direction: column; align-items: center; padding: 40px; background: white; border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.1);">
72
+ <div style="width: 60px; height: 60px; border: 4px solid #f3f3f3; border-top: 4px solid #FB7F0D; border-radius: 50%; animation: spin 1s linear infinite; margin-bottom: 20px;"></div>
73
+ <h3 style="color: #FB7F0D; margin: 10px 0; font-size: 18px;">분석 중입니다...</h3>
74
+ <p style="color: #666; margin: 5px 0; text-align: center;">원격 서버에서 데이터를 수집하고 AI가 분석하고 있습니다.<br>잠시만 기다려주세요.</p>
75
+ <div style="width: 200px; height: 4px; background: #f0f0f0; border-radius: 2px; margin-top: 15px; overflow: hidden;">
76
+ <div style="width: 100%; height: 100%; background: linear-gradient(90deg, #FB7F0D, #ff9a8b); border-radius: 2px; animation: progress 2s ease-in-out infinite;"></div>
77
+ </div>
78
+ </div>
79
 
80
+ <style>
81
+ @keyframes spin {
82
+ 0% { transform: rotate(0deg); }
83
+ 100% { transform: rotate(360deg); }
 
 
 
 
 
84
  }
85
 
86
+ @keyframes progress {
87
+ 0% { transform: translateX(-100%); }
88
+ 100% { transform: translateX(100%); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  }
90
+ </style>
91
+ """
92
+
93
+ # ===== 에러 처리 함수 =====
94
+ def generate_error_response(error_message):
95
+ """에러 응답 생성"""
96
+ return f'''
97
+ <div style="color: red; padding: 30px; text-align: center; width: 100%;
98
+ background-color: #f8d7da; border-radius: 12px; border: 1px solid #f5c6cb;">
99
+ <h3 style="margin-bottom: 15px;">❌ 연결 오류</h3>
100
+ <p style="margin-bottom: 20px;">{error_message}</p>
101
+ <div style="background: white; padding: 15px; border-radius: 8px; color: #333;">
102
+ <h4>해결 방법:</h4>
103
+ <ul style="text-align: left; padding-left: 20px;">
104
+ <li>네트워크 연결을 확인해주세요</li>
105
+ <li>원격 서버 상태를 확인해주세요</li>
106
+ <li>잠시 다시 시도해주세요</li>
107
+ <li>문제가 지속되면 관리자에게 문의하세요</li>
108
+ </ul>
109
+ </div>
110
+ </div>
111
+ '''
 
 
 
 
 
 
 
 
112
 
113
  # ===== 파일 출력 함수들 =====
114
  def create_timestamp_filename(analysis_keyword):
 
118
  safe_keyword = re.sub(r'[-\s]+', '_', safe_keyword)
119
  return f"{safe_keyword}_{timestamp}_분석결과"
120
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
  def export_to_html(analysis_html, filename_base):
122
  """HTML 파일로 출력 - 한국시간 적용"""
123
  try:
 
134
  <head>
135
  <meta charset="UTF-8">
136
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
137
+ <title>키워드 심층분석 결과</title>
138
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
139
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/static/pretendard.css">
140
  <style>
 
238
  <body>
239
  <div class="container">
240
  <div class="header">
241
+ <h1><i class="fas fa-chart-line"></i> 키워드 심층분석 결과</h1>
242
+ <p>AI 상품 소싱 분석 시스템 v2.10</p>
243
  </div>
244
  <div class="content">
245
  {analysis_html}
 
262
  logger.error(f"HTML 파일 생성 오류: {e}")
263
  return None
264
 
265
+ def create_zip_file(html_path, filename_base):
266
+ """압축 파일 생성 (HTML만)"""
267
  try:
268
  zip_filename = f"{filename_base}.zip"
269
  zip_path = os.path.join(tempfile.gettempdir(), zip_filename)
270
 
 
271
  with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
 
 
 
 
 
272
  if html_path and os.path.exists(html_path):
273
  zipf.write(html_path, f"{filename_base}.html")
274
  logger.info(f"HTML 파일 압축 추가: {filename_base}.html")
 
 
 
 
 
275
 
276
+ logger.info(f"압축 파일 생성 완료: {zip_path}")
277
  return zip_path
278
 
279
  except Exception as e:
280
  logger.error(f"압축 파일 생성 오류: {e}")
281
  return None
282
 
283
+ def export_analysis_results(export_data):
284
+ """분석 결과 출력 메인 함수 - 세션별 데이터 처리"""
285
  try:
286
+ # 출력할 데이터 확인
287
+ if not export_data or not isinstance(export_data, dict):
288
+ return None, "분석 데이터가 없습니다. 먼저 키워드 심층분석을 실행해주세요."
289
 
290
+ analysis_keyword = export_data.get("analysis_keyword", "")
291
+ analysis_html = export_data.get("analysis_html", "")
292
 
293
+ if not analysis_keyword:
294
+ return None, "분석할 키워드가 설정되지 않았습니다. 먼저 키워드 분석을 실행해주세요."
 
 
 
295
 
296
+ if not analysis_html:
297
+ return None, "분석 결과가 없습니다. 먼저 키워드 심층분석을 실행해주세요."
 
 
 
 
298
 
299
  # 파일명 생성 (한국시간 적용)
300
  filename_base = create_timestamp_filename(analysis_keyword)
301
+ logger.info(f"출력 파일명: {filename_base}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
302
 
303
+ # HTML 파일 생성
304
+ html_path = export_to_html(analysis_html, filename_base)
 
 
305
 
306
  # 압축 파일 생성
307
+ if html_path:
308
+ zip_path = create_zip_file(html_path, filename_base)
309
+ if zip_path:
310
+ return zip_path, f"✅ 분석 결과가 성공적으로 출력되었습니다!\n파일명: {filename_base}.zip"
311
+ else:
312
+ return None, "압축 파일 생성에 실패했습니다."
 
 
 
 
 
 
313
  else:
314
+ return None, "출력할 파일이 없습니다."
 
315
 
316
  except Exception as e:
317
+ logger.error(f"분석 결과 출력 오류: {e}")
 
 
318
  return None, f"출력 중 오류가 발생했습니다: {str(e)}"
319
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
320
  # ===== 원격 API 호출 함수들 =====
321
+ def call_analyze_keyword_api(analysis_keyword):
322
+ """키워드 심층분석 API 호출"""
323
  try:
324
  client = get_api_client()
325
  if not client:
326
  return generate_error_response("API 클라이언트를 초기화할 수 없습니다."), {}
327
 
328
+ logger.info("원격 API 호출: 키워드 심층분석")
329
  result = client.predict(
330
+ analysis_keyword=analysis_keyword,
331
+ api_name="/on_analyze_keyword"
332
  )
333
 
334
+ logger.info(f"키워드 분석 API 결과 타입: {type(result)}")
335
+
336
+ # 분석 결과로 export 데이터 생성
337
+ if isinstance(result, str) and len(result) > 100:
338
+ export_data = {
339
+ "analysis_keyword": analysis_keyword,
340
+ "analysis_html": result,
341
+ "analysis_completed": True,
342
+ "created_at": get_korean_time().isoformat()
343
+ }
344
+ return result, export_data
 
 
345
  else:
346
+ return str(result), {}
 
347
 
348
  except Exception as e:
349
+ logger.error(f"키워드 심층분석 API 호출 오류: {e}")
350
  return generate_error_response(f"원격 서버 연결 실패: {str(e)}"), {}
351
 
352
+ def call_export_results_api(export_data):
353
+ """분석 결과 출력 API 호출"""
354
  try:
355
  client = get_api_client()
356
  if not client:
357
+ return None, "API 클라이언트를 초기화할 수 없습니다."
 
 
 
 
 
358
 
359
+ logger.info("원격 API 호출: 분석 결과 출력")
360
  result = client.predict(
361
+ api_name="/on_export_results"
 
 
 
362
  )
363
 
364
+ logger.info(f"출력 API 결과 타입: {type(result)}")
 
 
365
 
366
+ # 결과가 튜플인 경우 번째 요소는 메시지, 두 번째는 파일
367
  if isinstance(result, tuple) and len(result) == 2:
368
+ message, file_path = result
369
+ if file_path:
370
+ return file_path, message
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
371
  else:
372
+ return None, message
 
373
  else:
374
+ return None, str(result)
 
 
 
 
 
 
 
 
 
 
 
 
375
 
376
  except Exception as e:
377
+ logger.error(f"분석 결과 출력 API 호출 오류: {e}")
378
+ return None, f"원격 서버 연결 실패: {str(e)}"
 
 
379
 
380
  # ===== 그라디오 인터페이스 =====
381
  def create_interface():
382
+ # CSS 파일 로드
383
+ try:
384
+ with open('style.css', 'r', encoding='utf-8') as f:
385
+ custom_css = f.read()
386
+ except:
387
+ custom_css = """
388
+ :root { --primary-color: #FB7F0D; --secondary-color: #ff9a8b; }
389
+ .custom-button {
390
+ background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)) !important;
391
+ color: white !important; border-radius: 30px !important; height: 45px !important;
392
+ font-size: 16px !important; font-weight: bold !important; width: 100% !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
393
  }
394
+ .export-button {
395
+ background: linear-gradient(135deg, #28a745, #20c997) !important;
396
+ color: white !important; border-radius: 25px !important; height: 50px !important;
397
+ font-size: 17px !important; font-weight: bold !important; width: 100% !important;
398
+ margin-top: 20px !important;
399
+ }
400
+ .custom-frame {
401
+ background-color: white !important;
402
+ border: 1px solid #e5e5e5 !important;
403
+ border-radius: 18px;
404
+ padding: 20px;
405
+ margin: 10px 0;
406
+ box-shadow: 0 8px 30px rgba(251, 127, 13, 0.08) !important;
407
+ }
408
+ .section-title {
409
+ display: flex;
410
+ align-items: center;
411
+ font-size: 20px;
412
+ font-weight: 700;
413
+ color: #334155 !important;
414
+ margin-bottom: 10px;
415
+ padding-bottom: 5px;
416
+ border-bottom: 2px solid var(--primary-color);
417
+ font-family: 'Pretendard', 'Noto Sans KR', -apple-system, BlinkMacSystemFont, sans-serif;
418
+ }
419
+ .section-title img, .section-title i {
420
+ margin-right: 10px;
421
+ font-size: 20px;
422
+ color: var(--primary-color);
423
+ }
424
+ """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
425
 
426
  with gr.Blocks(
427
  css=custom_css,
428
+ title="🛒 AI 상품 소싱 분석기 v2.10",
429
  theme=gr.themes.Default(primary_hue="orange", secondary_hue="orange")
430
  ) as interface:
431
 
 
435
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/static/pretendard.css">
436
  """)
437
 
438
+ # 세션별 상태 관리 (멀티 사용자 안전)
 
439
  export_data_state = gr.State({})
440
 
441
+ # === 키워드 심층분석 입력 ===
 
 
 
 
 
 
 
 
 
 
 
 
442
  with gr.Column(elem_classes="custom-frame fade-in"):
443
+ gr.HTML('<div class="section-title"><i class="fas fa-bullseye"></i> 키워드 심층분석 입력</div>')
 
 
 
 
444
 
445
  analysis_keyword_input = gr.Textbox(
446
  label="분석할 키워드",
447
+ placeholder="심층 분석할 키워드를 입력하세요 (예: 통굽 슬리퍼)",
448
  value="",
449
  elem_id="analysis_keyword_input"
450
  )
451
 
452
+ analyze_keyword_btn = gr.Button("키워드 심층분석 하기", elem_classes="custom-button", size="lg")
453
 
454
+ # === 키워드 심층분석 ===
455
  with gr.Column(elem_classes="custom-frame fade-in"):
456
+ gr.HTML('<div class="section-title"><i class="fas fa-chart-line"></i> 키워드 심층분석</div>')
457
+ analysis_result = gr.HTML(label="키워드 심층분석")
458
 
459
+ # === 결과 출력 섹션 ===
460
  with gr.Column(elem_classes="custom-frame fade-in"):
461
  gr.HTML('<div class="section-title"><i class="fas fa-download"></i> 분석 결과 출력</div>')
 
 
 
 
 
 
 
 
 
462
 
463
  export_btn = gr.Button("📊 분석결과 출력하기", elem_classes="export-button", size="lg")
464
  export_result = gr.HTML()
465
  download_file = gr.File(label="다운로드", visible=False)
466
 
467
  # ===== 이벤트 핸들러 =====
468
+ def on_analyze_keyword(analysis_keyword):
 
 
 
 
 
 
 
 
 
 
 
 
469
  if not analysis_keyword.strip():
470
+ return "분석할 키워드를 입력해주세요.", {}
471
 
472
  # 로딩 상태 표시
473
  yield create_loading_animation(), {}
474
 
475
+ # 실제 키워드 분석 실행
476
+ keyword_result, session_export_data = call_analyze_keyword_api(analysis_keyword)
 
 
477
 
478
+ # 📈 검색량 트렌드 분석과 🎯 키워드 분석 표시
479
+ yield keyword_result, session_export_data
480
+
481
  def on_export_results(export_data):
482
+ """분석 결과 출력 핸들러 - 세션별 데이터 처리"""
483
  try:
484
+ # 로컬 출력 시도
485
+ zip_path, message = export_analysis_results(export_data)
 
 
 
 
486
 
487
  if zip_path:
488
+ # 성공 메시지와 함께 다운로드 파일 제공
489
  success_html = f"""
490
  <div style="background: #d4edda; border: 1px solid #c3e6cb; padding: 20px; border-radius: 8px; margin: 10px 0;">
491
  <h4 style="color: #155724; margin: 0 0 15px 0;"><i class="fas fa-check-circle"></i> 출력 완료!</h4>
492
  <p style="color: #155724; margin: 0; line-height: 1.6;">
493
  {message}<br>
494
+ <strong>포함 파일:</strong><br>
495
+ • 🌐 HTML 파일: 키워드 심층분석 결과 (그래프 포함)<br>
496
  <br>
497
  <i class="fas fa-download"></i> 아래 다운로드 버튼을 클릭하여 파일을 저장하세요.<br>
498
  <small style="color: #666;">⏰ 한국시간 기준으로 파일명이 생성됩니다.</small>
 
501
  """
502
  return success_html, gr.update(value=zip_path, visible=True)
503
  else:
504
+ # 로컬 출력 실패시 원격 API 시도
505
+ try:
506
+ remote_file, remote_message = call_export_results_api(export_data)
507
+ if remote_file:
508
+ success_html = f"""
509
+ <div style="background: #d4edda; border: 1px solid #c3e6cb; padding: 20px; border-radius: 8px; margin: 10px 0;">
510
+ <h4 style="color: #155724; margin: 0 0 15px 0;"><i class="fas fa-check-circle"></i> 원격 출력 완료!</h4>
511
+ <p style="color: #155724; margin: 0; line-height: 1.6;">
512
+ {remote_message}<br>
513
+ <i class="fas fa-download"></i> 아래 다운로드 버튼을 클릭하여 파일을 저장하세요.
514
+ </p>
515
+ </div>
516
+ """
517
+ return success_html, gr.update(value=remote_file, visible=True)
518
+ else:
519
+ # 실패 메시지
520
+ error_html = f"""
521
+ <div style="background: #f8d7da; border: 1px solid #f5c6cb; padding: 20px; border-radius: 8px; margin: 10px 0;">
522
+ <h4 style="color: #721c24; margin: 0 0 10px 0;"><i class="fas fa-exclamation-triangle"></i> 출력 실패</h4>
523
+ <p style="color: #721c24; margin: 0;">{message}<br>원격 출력도 실패: {remote_message}</p>
524
+ </div>
525
+ """
526
+ return error_html, gr.update(visible=False)
527
+ except:
528
+ # 원격 API도 실패
529
+ error_html = f"""
530
+ <div style="background: #f8d7da; border: 1px solid #f5c6cb; padding: 20px; border-radius: 8px; margin: 10px 0;">
531
+ <h4 style="color: #721c24; margin: 0 0 10px 0;"><i class="fas fa-exclamation-triangle"></i> 출력 실패</h4>
532
+ <p style="color: #721c24; margin: 0;">{message}</p>
533
  </div>
534
+ """
535
+ return error_html, gr.update(visible=False)
 
 
536
 
537
  except Exception as e:
538
+ logger.error(f"출력 핸들러 오류: {e}")
 
 
 
539
  error_html = f"""
540
  <div style="background: #f8d7da; border: 1px solid #f5c6cb; padding: 20px; border-radius: 8px; margin: 10px 0;">
541
  <h4 style="color: #721c24; margin: 0 0 10px 0;"><i class="fas fa-exclamation-triangle"></i> 시스템 오류</h4>
542
+ <p style="color: #721c24; margin: 0;">출력 중 시스템 오류가 발생했습니다: {str(e)}</p>
 
 
 
 
 
 
 
 
543
  </div>
544
  """
545
  return error_html, gr.update(visible=False)
546
 
547
  # ===== 이벤트 연결 =====
 
 
 
 
 
 
 
548
  analyze_keyword_btn.click(
549
  fn=on_analyze_keyword,
550
+ inputs=[analysis_keyword_input],
551
+ outputs=[analysis_result, export_data_state]
 
552
  )
553
 
554
  export_btn.click(
555
  fn=on_export_results,
556
  inputs=[export_data_state],
557
+ outputs=[export_result, download_file]
 
558
  )
559
 
560
  return interface
 
566
  import pytz
567
  logger.info("✅ pytz 모듈 로드 성공 - 한국시간 지원")
568
  except ImportError:
569
+ logger.warning("⚠️ pytz 모듈이 설치되지 않음 - pip install pytz 실행 필요")
570
  logger.info("시스템 시간을 사용합니다.")
571
 
572
+ logger.info("===== 상품 소싱 분석 시스템 v2.10 (컨트롤 타워 버전) 시작 =====")
573
+
574
+ # 필요한 패키지 안내
575
+ print("📦 필요한 패키지:")
576
+ print(" pip install gradio gradio_client pandas pytz")
577
+ print()
578
+
579
+ # API 엔드포인트 설정 안내
580
+ api_endpoint = os.getenv('API_ENDPOINT')
581
+ if not api_endpoint:
582
+ print("⚠️ API_ENDPOINT 환경변수를 설정하세요.")
583
+ print(" export API_ENDPOINT='your-endpoint-url'")
584
+ print()
585
+ else:
586
+ print("✅ API 엔드포인트 설정 완료!")
587
+ print()
588
+
589
+ print("🚀 v2.10 컨트롤 타워 버전 특징:")
590
+ print(" • 허깅페이스 그라디오 엔드포인트 활용")
591
+ print(" • 완전히 동일한 UI와 기능 구현")
592
+ print(" • 📈 검색량 트렌드 분석과 🎯 키워드 분석 표시")
593
+ print(" • ✅ 출력 기능: HTML 파일 생성 및 ZIP 다운로드")
594
+ print(" • ✅ 한국시간 기준 파일명 생성")
595
+ print(" • ✅ 멀티 사용자 안전: gr.State로 세션별 데이터 관리")
596
+ print(" • 🔒 클라이언트 정보 환경변수로 완전 숨김")
597
+ print(" • 원격 서버와 로컬 처리 하이브리드 방식")
598
+ print()
599
+
600
  # 앱 실행
601
  app = create_interface()
602
  app.launch(server_name="0.0.0.0", server_port=7860, share=True)