ssboost commited on
Commit
37b4640
Β·
verified Β·
1 Parent(s): 74ca5a0

Update app.py

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