ssboost commited on
Commit
580b0de
ยท
verified ยท
1 Parent(s): 1b3f7ca

Upload 15 files

Browse files
api_utils.py ADDED
@@ -0,0 +1,253 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ API ๊ด€๋ จ ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜ ๋ชจ์Œ (ํ™˜๊ฒฝ๋ณ€์ˆ˜ ๋ฒ„์ „)
3
+ - API ํ‚ค ๊ด€๋ฆฌ (ํ™˜๊ฒฝ๋ณ€์ˆ˜์—์„œ ๋กœ๋“œ)
4
+ - ์‹œ๊ทธ๋‹ˆ์ฒ˜ ์ƒ์„ฑ
5
+ - API ํ—ค๋” ์ƒ์„ฑ
6
+ - Gemini API ํ‚ค ๋žœ๋ค ๋กœํ…Œ์ด์…˜ ์ถ”๊ฐ€
7
+ """
8
+
9
+ import os
10
+ import time
11
+ import hmac
12
+ import hashlib
13
+ import base64
14
+ import requests
15
+ import threading
16
+ import random
17
+ import google.generativeai as genai
18
+ import logging
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+ # ํ™˜๊ฒฝ๋ณ€์ˆ˜์—์„œ API ์„ค์ • ๋กœ๋“œ
23
+ def get_api_configs():
24
+ # ํ™˜๊ฒฝ๋ณ€์ˆ˜ 'API_CONFIGS'์—์„œ ์ „์ฒด ์„ค์ •์„ ๊ฐ€์ ธ์˜ด
25
+ api_configs_str = os.getenv('API_CONFIGS', '')
26
+
27
+ if not api_configs_str:
28
+ logger.error("API_CONFIGS ํ™˜๊ฒฝ๋ณ€์ˆ˜๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.")
29
+ return [], [], [], []
30
+
31
+ try:
32
+ # ํ™˜๊ฒฝ๋ณ€์ˆ˜ ๊ฐ’์„ exec๋กœ ์‹คํ–‰ํ•˜์—ฌ ์„ค์ • ๋กœ๋“œ
33
+ local_vars = {}
34
+ exec(api_configs_str, {}, local_vars)
35
+
36
+ return (
37
+ local_vars.get('NAVER_API_CONFIGS', []),
38
+ local_vars.get('NAVER_SHOPPING_CONFIGS', []),
39
+ local_vars.get('NAVER_DATALAB_CONFIGS', []),
40
+ local_vars.get('GEMINI_API_CONFIGS', [])
41
+ )
42
+ except Exception as e:
43
+ logger.error(f"ํ™˜๊ฒฝ๋ณ€์ˆ˜ ํŒŒ์‹ฑ ์˜ค๋ฅ˜: {e}")
44
+ return [], [], [], []
45
+
46
+ # API ์„ค์ • ๋กœ๋“œ
47
+ NAVER_API_CONFIGS, NAVER_SHOPPING_CONFIGS, NAVER_DATALAB_CONFIGS, GEMINI_API_CONFIGS = get_api_configs()
48
+
49
+ # ์ˆœ์ฐจ ์‚ฌ์šฉ์„ ์œ„ํ•œ ์ธ๋ฑ์Šค์™€ ๋ฝ
50
+ current_api_index = 0
51
+ current_shopping_api_index = 0
52
+ current_datalab_api_index = 0
53
+ current_gemini_api_index = 0
54
+ api_lock = threading.Lock()
55
+ shopping_lock = threading.Lock()
56
+ datalab_lock = threading.Lock()
57
+ gemini_lock = threading.Lock()
58
+
59
+ # Gemini ๋ชจ๋ธ ์บ์‹œ
60
+ _gemini_models = {}
61
+
62
+ # API ์„ค์ • ์ดˆ๊ธฐํ™” ํ•จ์ˆ˜ ์ถ”๊ฐ€
63
+ def initialize_api_configs():
64
+ """API ์„ค์ •์„ ์ดˆ๊ธฐํ™”ํ•˜๊ณ  ๋žœ๋คํ•˜๊ฒŒ ์ •๋ ฌ"""
65
+ global NAVER_API_CONFIGS, NAVER_SHOPPING_CONFIGS, NAVER_DATALAB_CONFIGS, GEMINI_API_CONFIGS
66
+
67
+ # API ์„ค์ •์„ ๋‹ค์‹œ ๋กœ๋“œ
68
+ NAVER_API_CONFIGS, NAVER_SHOPPING_CONFIGS, NAVER_DATALAB_CONFIGS, GEMINI_API_CONFIGS = get_api_configs()
69
+
70
+ # API ์„ค์ •์„ ๋žœ๋คํ•˜๊ฒŒ ์„ž๊ธฐ
71
+ if NAVER_API_CONFIGS:
72
+ random.shuffle(NAVER_API_CONFIGS)
73
+ if NAVER_SHOPPING_CONFIGS:
74
+ random.shuffle(NAVER_SHOPPING_CONFIGS)
75
+ if NAVER_DATALAB_CONFIGS:
76
+ random.shuffle(NAVER_DATALAB_CONFIGS)
77
+ if GEMINI_API_CONFIGS:
78
+ random.shuffle(GEMINI_API_CONFIGS)
79
+
80
+ print(f"API ์„ค์ • ์ดˆ๊ธฐํ™” ์™„๋ฃŒ:")
81
+ print(f" - ๊ฒ€์ƒ‰๊ด‘๊ณ  API: {len(NAVER_API_CONFIGS)}๊ฐœ")
82
+ print(f" - ์‡ผํ•‘ API: {len(NAVER_SHOPPING_CONFIGS)}๊ฐœ")
83
+ print(f" - ๋ฐ์ดํ„ฐ๋žฉ API: {len(NAVER_DATALAB_CONFIGS)}๊ฐœ")
84
+ print(f" - Gemini API: {len(GEMINI_API_CONFIGS)}๊ฐœ")
85
+
86
+
87
+ def generate_signature(timestamp, method, uri, secret_key):
88
+ """์‹œ๊ทธ๋‹ˆ์ฒ˜ ์ƒ์„ฑ ํ•จ์ˆ˜"""
89
+ message = f"{timestamp}.{method}.{uri}"
90
+ digest = hmac.new(secret_key.encode("utf-8"), message.encode("utf-8"), hashlib.sha256).digest()
91
+ return base64.b64encode(digest).decode()
92
+
93
+ def get_header(method, uri, api_key, secret_key, customer_id):
94
+ """API ํ—ค๋” ์ƒ์„ฑ ํ•จ์ˆ˜"""
95
+ timestamp = str(round(time.time() * 1000))
96
+ signature = generate_signature(timestamp, method, uri, secret_key)
97
+ return {
98
+ "Content-Type": "application/json; charset=UTF-8",
99
+ "X-Timestamp": timestamp,
100
+ "X-API-KEY": api_key,
101
+ "X-Customer": str(customer_id),
102
+ "X-Signature": signature
103
+ }
104
+
105
+ def get_next_api_config():
106
+ """์ˆœ์ฐจ์ ์œผ๋กœ ๋‹ค์Œ API ์„ค์ •์„ ๋ฐ˜ํ™˜ (์Šค๋ ˆ๋“œ ์•ˆ์ „)"""
107
+ global current_api_index
108
+
109
+ if not NAVER_API_CONFIGS:
110
+ logger.error("๋„ค์ด๋ฒ„ ๊ฒ€์ƒ‰๊ด‘๊ณ  API ์„ค์ •์ด ์—†์Šต๋‹ˆ๋‹ค.")
111
+ return None
112
+
113
+ with api_lock:
114
+ config = NAVER_API_CONFIGS[current_api_index]
115
+ current_api_index = (current_api_index + 1) % len(NAVER_API_CONFIGS)
116
+ return config
117
+
118
+ def get_next_shopping_api_config():
119
+ """์ˆœ์ฐจ์ ์œผ๋กœ ๋‹ค์Œ ์‡ผํ•‘ API ์„ค์ •์„ ๋ฐ˜ํ™˜ (์˜ค๋ฅ˜ ํ‚ค ๊ฑด๋„ˆ๋›ฐ๊ธฐ ์ถ”๊ฐ€)"""
120
+ global current_shopping_api_index
121
+
122
+ if not NAVER_SHOPPING_CONFIGS:
123
+ logger.error("๋„ค์ด๋ฒ„ ์‡ผํ•‘ API ์„ค์ •์ด ์—†์Šต๋‹ˆ๋‹ค.")
124
+ return None
125
+
126
+ with shopping_lock:
127
+ # ์ตœ๋Œ€ ์ „์ฒด ํ‚ค ์ˆ˜๋งŒํผ ์‹œ๋„ (๋ฌดํ•œ ๋ฃจํ”„ ๋ฐฉ์ง€)
128
+ for _ in range(len(NAVER_SHOPPING_CONFIGS)):
129
+ config = NAVER_SHOPPING_CONFIGS[current_shopping_api_index]
130
+ current_shopping_api_index = (current_shopping_api_index + 1) % len(NAVER_SHOPPING_CONFIGS)
131
+
132
+ # ๊ธฐ๋ณธ๊ฐ’ ์ฒดํฌ
133
+ if config["CLIENT_ID"] and not config["CLIENT_ID"].startswith("YOUR_"):
134
+ return config
135
+
136
+ # ๋ชจ๋“  ํ‚ค๊ฐ€ ๊ธฐ๋ณธ๊ฐ’์ธ ๊ฒฝ์šฐ ์ฒซ ๋ฒˆ์งธ ํ‚ค ๋ฐ˜ํ™˜
137
+ return NAVER_SHOPPING_CONFIGS[0] if NAVER_SHOPPING_CONFIGS else None
138
+
139
+ def get_next_datalab_api_config():
140
+ """์ˆœ์ฐจ์ ์œผ๋กœ ๋‹ค์Œ ๋ฐ์ดํ„ฐ๋žฉ API ์„ค์ •์„ ๋ฐ˜ํ™˜ (์Šค๋ ˆ๋“œ ์•ˆ์ „)"""
141
+ global current_datalab_api_index
142
+
143
+ if not NAVER_DATALAB_CONFIGS:
144
+ logger.error("๋„ค์ด๋ฒ„ ๋ฐ์ดํ„ฐ๋žฉ API ์„ค์ •์ด ์—†์Šต๋‹ˆ๋‹ค.")
145
+ return None
146
+
147
+ with datalab_lock:
148
+ # API ํ‚ค๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์•˜์œผ๋ฉด None ๋ฐ˜ํ™˜
149
+ if not NAVER_DATALAB_CONFIGS[0]["CLIENT_ID"] or NAVER_DATALAB_CONFIGS[0]["CLIENT_ID"].startswith("YOUR_"):
150
+ return None
151
+
152
+ config = NAVER_DATALAB_CONFIGS[current_datalab_api_index]
153
+ current_datalab_api_index = (current_datalab_api_index + 1) % len(NAVER_DATALAB_CONFIGS)
154
+ return config
155
+
156
+ def get_next_gemini_api_key():
157
+ """์ˆœ์ฐจ์ ์œผ๋กœ ๋‹ค์Œ Gemini API ํ‚ค๋ฅผ ๋ฐ˜ํ™˜ (์Šค๋ ˆ๋“œ ์•ˆ์ „)"""
158
+ global current_gemini_api_index
159
+
160
+ if not GEMINI_API_CONFIGS:
161
+ logger.warning("์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ Gemini API ํ‚ค๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
162
+ return None
163
+
164
+ with gemini_lock:
165
+ # ์ตœ๋Œ€ ์ „์ฒด ํ‚ค ์ˆ˜๋งŒํผ ์‹œ๋„ (๋ฌดํ•œ ๋ฃจํ”„ ๋ฐฉ์ง€)
166
+ for _ in range(len(GEMINI_API_CONFIGS)):
167
+ api_key = GEMINI_API_CONFIGS[current_gemini_api_index]
168
+ current_gemini_api_index = (current_gemini_api_index + 1) % len(GEMINI_API_CONFIGS)
169
+
170
+ # ๊ธฐ๋ณธ๊ฐ’์ด ์•„๋‹Œ ํ‚ค๋งŒ ๋ฐ˜ํ™˜
171
+ if api_key and not api_key.startswith("YOUR_") and api_key.strip():
172
+ return api_key
173
+
174
+ # ๋ชจ๋“  ํ‚ค๊ฐ€ ๊ธฐ๋ณธ๊ฐ’์ธ ๊ฒฝ์šฐ None ๋ฐ˜ํ™˜
175
+ logger.warning("์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ Gemini API ํ‚ค๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
176
+ return None
177
+
178
+ def get_gemini_model():
179
+ """์บ์‹œ๋œ Gemini ๋ชจ๋ธ์„ ๋ฐ˜ํ™˜ํ•˜๊ฑฐ๋‚˜ ์ƒˆ๋กœ ์ƒ์„ฑ"""
180
+ api_key = get_next_gemini_api_key()
181
+
182
+ if not api_key:
183
+ logger.error("Gemini API ํ‚ค๋ฅผ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")
184
+ return None
185
+
186
+ # ์บ์‹œ์—์„œ ๋ชจ๋ธ ํ™•์ธ
187
+ if api_key in _gemini_models:
188
+ return _gemini_models[api_key]
189
+
190
+ try:
191
+ # ์ƒˆ ๋ชจ๋ธ ์ƒ์„ฑ
192
+ genai.configure(api_key=api_key)
193
+ model = genai.GenerativeModel("gemini-2.0-flash-exp")
194
+
195
+ # ์บ์‹œ์— ์ €์žฅ
196
+ _gemini_models[api_key] = model
197
+
198
+ logger.info(f"Gemini ๋ชจ๋ธ ์ƒ์„ฑ ์„ฑ๊ณต: {api_key[:8]}***{api_key[-4:]}")
199
+ return model
200
+
201
+ except Exception as e:
202
+ logger.error(f"Gemini ๋ชจ๋ธ ์ƒ์„ฑ ์‹คํŒจ ({api_key[:8]}***): {e}")
203
+ return None
204
+
205
+ def validate_api_config(api_config):
206
+ """API ์„ค์ • ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ"""
207
+ if not api_config:
208
+ return False, "API ์„ค์ •์ด ์—†์Šต๋‹ˆ๋‹ค."
209
+
210
+ API_KEY = api_config.get("API_KEY", "")
211
+ SECRET_KEY = api_config.get("SECRET_KEY", "")
212
+ CUSTOMER_ID_STR = api_config.get("CUSTOMER_ID", "")
213
+
214
+ if not all([API_KEY, SECRET_KEY, CUSTOMER_ID_STR]):
215
+ return False, "API ํ‚ค๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."
216
+
217
+ if CUSTOMER_ID_STR.startswith("YOUR_") or API_KEY.startswith("YOUR_"):
218
+ return False, "API ํ‚ค๊ฐ€ ํ”Œ๋ ˆ์ด์Šคํ™€๋”์ž…๋‹ˆ๋‹ค."
219
+
220
+ try:
221
+ CUSTOMER_ID = int(CUSTOMER_ID_STR)
222
+ except ValueError:
223
+ return False, f"CUSTOMER_ID ๋ณ€ํ™˜ ์˜ค๋ฅ˜: '{CUSTOMER_ID_STR}'๋Š” ์œ ํšจํ•œ ์ˆซ์ž๊ฐ€ ์•„๋‹™๋‹ˆ๋‹ค."
224
+
225
+ return True, "์œ ํšจํ•œ API ์„ค์ •์ž…๋‹ˆ๋‹ค."
226
+
227
+ def validate_datalab_config(datalab_config):
228
+ """๋ฐ์ดํ„ฐ๋žฉ API ์„ค์ • ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ"""
229
+ if not datalab_config:
230
+ return False, "๋ฐ์ดํ„ฐ๋žฉ API ์„ค์ •์ด ์—†์Šต๋‹ˆ๋‹ค."
231
+
232
+ CLIENT_ID = datalab_config.get("CLIENT_ID", "")
233
+ CLIENT_SECRET = datalab_config.get("CLIENT_SECRET", "")
234
+
235
+ if not all([CLIENT_ID, CLIENT_SECRET]):
236
+ return False, "๋ฐ์ดํ„ฐ๋žฉ API ํ‚ค๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."
237
+
238
+ if CLIENT_ID.startswith("YOUR_") or CLIENT_SECRET.startswith("YOUR_"):
239
+ return False, "๋ฐ์ดํ„ฐ๋žฉ API ํ‚ค๊ฐ€ ํ”Œ๋ ˆ์ด์Šคํ™€๋”์ž…๋‹ˆ๋‹ค."
240
+
241
+ return True, "์œ ํšจํ•œ ๋ฐ์ดํ„ฐ๋žฉ API ์„ค์ •์ž…๋‹ˆ๋‹ค."
242
+
243
+ def validate_gemini_config():
244
+ """Gemini API ์„ค์ • ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ"""
245
+ valid_keys = 0
246
+ for api_key in GEMINI_API_CONFIGS:
247
+ if api_key and not api_key.startswith("YOUR_") and api_key.strip():
248
+ valid_keys += 1
249
+
250
+ if valid_keys == 0:
251
+ return False, "์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ Gemini API ํ‚ค๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค."
252
+
253
+ return True, f"{valid_keys}๊ฐœ์˜ ์œ ํšจํ•œ Gemini API ํ‚ค๊ฐ€ ์„ค์ •๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค."
app.py CHANGED
@@ -1,46 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
1
  import gradio as gr
2
  import pandas as pd
3
  import os
4
  import logging
5
- from datetime import datetime
6
- import pytz
 
7
  import time
8
- import tempfile
9
- 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
 
17
- # ์™ธ๋ถ€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋กœ๊ทธ ๋น„ํ™œ์„ฑํ™”
18
- logging.getLogger('gradio').setLevel(logging.WARNING)
19
- logging.getLogger('gradio_client').setLevel(logging.WARNING)
20
- logging.getLogger('httpx').setLevel(logging.WARNING)
21
- logging.getLogger('urllib3').setLevel(logging.WARNING)
 
 
 
 
22
 
23
- # ===== API ํด๋ผ์ด์–ธํŠธ ์„ค์ • =====
24
- def get_api_client():
25
- """ํ™˜๊ฒฝ๋ณ€์ˆ˜์—์„œ API ์—”๋“œํฌ์ธํŠธ๋ฅผ ๊ฐ€์ ธ์™€ ํด๋ผ์ด์–ธํŠธ ์ƒ์„ฑ"""
26
  try:
27
- from gradio_client import Client
28
-
29
- # ํ™˜๊ฒฝ๋ณ€์ˆ˜์—์„œ API ์—”๋“œํฌ์ธํŠธ ๊ฐ€์ ธ์˜ค๊ธฐ
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 ํด๋ผ์ด์–ธํŠธ ์ดˆ๊ธฐํ™” ์„ฑ๊ณต")
38
- return client
39
 
 
 
 
 
 
 
 
40
  except Exception as e:
41
- logger.error(f"API ํด๋ผ์ด์–ธํŠธ ์ดˆ๊ธฐํ™” ์‹คํŒจ: {e}")
42
  return None
43
 
 
 
 
44
  # ===== ํ•œ๊ตญ์‹œ๊ฐ„ ๊ด€๋ จ ํ•จ์ˆ˜ =====
45
  def get_korean_time():
46
  """ํ•œ๊ตญ์‹œ๊ฐ„ ๋ฐ˜ํ™˜"""
@@ -61,108 +77,427 @@ 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):
@@ -173,16 +508,8 @@ def create_timestamp_filename(analysis_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
 
@@ -214,8 +541,8 @@ def export_to_excel(main_keyword, main_keywords_df, analysis_keyword, related_ke
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
 
@@ -226,7 +553,7 @@ def export_to_excel(main_keyword, main_keywords_df, analysis_keyword, related_ke
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)
@@ -238,11 +565,9 @@ def export_to_excel(main_keyword, main_keywords_df, analysis_keyword, related_ke
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
 
@@ -253,7 +578,7 @@ def export_to_excel(main_keyword, main_keywords_df, analysis_keyword, related_ke
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)
@@ -265,8 +590,6 @@ def export_to_excel(main_keyword, main_keywords_df, analysis_keyword, related_ke
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
@@ -396,7 +719,7 @@ def export_to_html(analysis_html, filename_base):
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}
@@ -425,71 +748,48 @@ def create_zip_file(excel_path, html_path, filename_base):
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,
@@ -497,340 +797,53 @@ def export_analysis_results_enhanced(export_data):
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,11 +853,11 @@ 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
 
@@ -857,10 +870,12 @@ def create_interface():
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
 
@@ -873,21 +888,14 @@ def create_interface():
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()
@@ -901,10 +909,30 @@ def create_interface():
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():
@@ -913,30 +941,78 @@ def create_interface():
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,40 +1021,21 @@ 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)
@@ -987,26 +1044,80 @@ def create_interface():
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
1009
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1010
  # ===== ๋ฉ”์ธ ์‹คํ–‰ =====
1011
  if __name__ == "__main__":
1012
  # pytz ๋ชจ๋“ˆ ์„ค์น˜ ํ™•์ธ
@@ -1014,8 +1125,64 @@ 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)
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ AI ์ƒํ’ˆ ์†Œ์‹ฑ ๋ถ„์„ ์‹œ์Šคํ…œ v2.9 - ์ถœ๋ ฅ ๊ธฐ๋Šฅ ์ถ”๊ฐ€ + ๋ฉ€ํ‹ฐ์‚ฌ์šฉ์ž ์•ˆ์ „
4
+ - ์—ฐ๊ด€๊ฒ€์ƒ‰์–ด ์—‘์…€ ์ถœ๋ ฅ
5
+ - ํ‚ค์›Œ๋“œ ์‹ฌ์ถฉ๋ถ„์„ HTML ์ถœ๋ ฅ
6
+ - ์••์ถ•ํŒŒ์ผ๋กœ ๊ฒฐ๊ณผ ๋‹ค์šด๋กœ๋“œ
7
+ - Gemini API ํ‚ค ํ†ตํ•ฉ ๊ด€๋ฆฌ
8
+ - ํ•œ๊ตญ์‹œ๊ฐ„ ์ ์šฉ
9
+ - ๋ฉ€ํ‹ฐ ์‚ฌ์šฉ์ž ์•ˆ์ „: gr.State ์‚ฌ์šฉ์œผ๋กœ ์„ธ์…˜๋ณ„ ๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ
10
+ """
11
+
12
  import gradio as gr
13
  import pandas as pd
14
  import os
15
  import logging
16
+ import google.generativeai as genai
17
+ from datetime import datetime, timedelta
18
+ import pytz # ํ•œ๊ตญ์‹œ๊ฐ„ ์ ์šฉ์„ ์œ„ํ•œ ์ถ”๊ฐ€
19
  import time
 
 
20
  import re
21
+ from collections import Counter
22
+ import zipfile
23
+ import tempfile
24
 
25
  # ๋กœ๊น… ์„ค์ •
26
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
27
  logger = logging.getLogger(__name__)
28
 
29
+ # ๋ชจ๋“ˆ ์ž„ํฌํŠธ
30
+ import api_utils
31
+ import text_utils
32
+ import keyword_search
33
+ import product_search
34
+ import keyword_processor
35
+ import export_utils
36
+ import keyword_analysis
37
+ import trend_analysis_v2
38
 
39
+ # ===== Gemini API ์„ค์ • =====
40
+ def setup_gemini_model():
41
+ """Gemini ๋ชจ๋ธ ์ดˆ๊ธฐํ™” - api_utils์—์„œ ๊ด€๋ฆฌ"""
42
  try:
43
+ # api_utils์—์„œ Gemini ๋ชจ๋ธ ๊ฐ€์ ธ์˜ค๊ธฐ
44
+ model = api_utils.get_gemini_model()
 
 
 
 
 
 
 
 
 
 
45
 
46
+ if model:
47
+ logger.info("Gemini ๋ชจ๋ธ ์ดˆ๊ธฐํ™” ์„ฑ๊ณต (api_utils ํ†ตํ•ฉ ๊ด€๋ฆฌ)")
48
+ return model
49
+ else:
50
+ logger.warning("Gemini API ํ‚ค๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.")
51
+ return None
52
+
53
  except Exception as e:
54
+ logger.error(f"Gemini ๋ชจ๋ธ ์ดˆ๊ธฐํ™” ์‹คํŒจ: {e}")
55
  return None
56
 
57
+ # Gemini ๋ชจ๋ธ ์ดˆ๊ธฐํ™”
58
+ gemini_model = setup_gemini_model()
59
+
60
  # ===== ํ•œ๊ตญ์‹œ๊ฐ„ ๊ด€๋ จ ํ•จ์ˆ˜ =====
61
  def get_korean_time():
62
  """ํ•œ๊ตญ์‹œ๊ฐ„ ๋ฐ˜ํ™˜"""
 
77
  else:
78
  return dt.strftime("%y%m%d_%H%M")
79
 
80
+ # ===== ์ถœ๋ ฅ ์ „์šฉ ์ƒํƒœ ๋ณ€์ˆ˜ ์ œ๊ฑฐ (๋ฉ€ํ‹ฐ ์‚ฌ์šฉ์ž ์•ˆ์ „์„ ์œ„ํ•ด gr.State ์‚ฌ์šฉ) =====
81
+ # export_state ์ „์—ญ ๋ณ€์ˆ˜ ์ œ๊ฑฐ - ๋ฉ€ํ‹ฐ ์‚ฌ์šฉ์ž ํ™˜๊ฒฝ์—์„œ ๋ฐ์ดํ„ฐ ํ˜ผํ•ฉ ๋ฌธ์ œ ํ•ด๊ฒฐ
82
+
83
+ # ===== ์—ฐ๊ด€๊ฒ€์ƒ‰์–ด ๋ถ„์„ ๊ธฐ๋Šฅ =====
84
+ def analyze_related_keywords(keyword):
85
+ """์—ฐ๊ด€๊ฒ€์ƒ‰์–ด ๋ถ„์„ - ๋„ค์ด๋ฒ„ ์ƒํ’ˆ 40๊ฐœ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๋ณตํ•ฉํ‚ค์›Œ๋“œ ์ถ”์ถœ"""
86
+ logger.info(f"์—ฐ๊ด€๊ฒ€์ƒ‰์–ด ๋ถ„์„ ์‹œ์ž‘: '{keyword}'")
87
+
88
+ try:
89
+ # 1๋‹จ๊ณ„: ๋„ค์ด๋ฒ„ ์ƒํ’ˆ 40๊ฐœ ์ถ”์ถœ
90
+ api_keyword = keyword.replace(" ", "")
91
+ products_data = []
92
+
93
+ # 40๊ฐœ ์ƒํ’ˆ์„ ๊ฐ€์ ธ์˜ค๊ธฐ ์œ„ํ•ด ์—ฌ๋Ÿฌ ํŽ˜์ด์ง€ ํ˜ธ์ถœ
94
+ for page in range(1, 5): # 4ํŽ˜์ด์ง€ * 10๊ฐœ = 40๊ฐœ
95
+ result = product_search.fetch_products_by_keyword(api_keyword, page=page, display=10)
96
+ if result["status"] == "success" and result["products"]:
97
+ products_data.extend(result["products"])
98
+ else:
99
+ break
100
+ time.sleep(0.3) # API ๋ ˆ์ดํŠธ ๋ฆฌ๋ฐ‹ ๋ฐฉ์ง€
101
+
102
+ if not products_data:
103
+ return {
104
+ "status": "error",
105
+ "message": f"'{keyword}' ํ‚ค์›Œ๋“œ๋กœ ์ƒํ’ˆ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.",
106
+ "keywords_df": pd.DataFrame()
107
+ }
108
+
109
+ # ์‹ค์ œ ๊ฐ€์ ธ์˜จ ์ƒํ’ˆ ์ˆ˜ ์ œํ•œ
110
+ products_data = products_data[:40]
111
+ logger.info(f"์ƒํ’ˆ ์ถ”์ถœ ์™„๋ฃŒ: {len(products_data)}๊ฐœ")
112
+
113
+ # 2๋‹จ๊ณ„: ์ƒํ’ˆ๋ช…์—์„œ ํ‚ค์›Œ๋“œ ์ถ”์ถœ (์ŠคํŽ˜์ด์Šค๋ฐ”๋กœ ๋ถ„๋ฅ˜)
114
+ all_words = []
115
+ for product in products_data:
116
+ title = product.get("์ƒํ’ˆ๋ช…", "")
117
+ # ๊ณต๋ฐฑ๊ณผ ์‰ผํ‘œ๋กœ ๋ถ„๋ฆฌ
118
+ words = re.split(r'[,\s]+', title)
119
+ all_words.extend([word.strip() for word in words if word.strip() and len(word.strip()) >= 1])
120
+
121
+ # ์ค‘๋ณต ์ œ๊ฑฐ
122
+ unique_words = list(set(all_words))
123
+ logger.info(f"์ถ”์ถœ๋œ ๋‹จ์–ด ์ˆ˜: {len(unique_words)}๊ฐœ")
124
+
125
+ # 3๋‹จ๊ณ„: ๋ถ„์„ํ•  ํ‚ค์›Œ๋“œ๋ฅผ ์•ž๋’ค๋กœ ๋ถ™์—ฌ์„œ ๋ณตํ•ฉํ‚ค์›Œ๋“œ ์ƒ์„ฑ
126
+ compound_keywords = []
127
+ main_keyword = keyword.strip()
128
+
129
+ for word in unique_words:
130
+ if word != main_keyword and len(word) >= 2: # ๋‹จ์ผ ๊ธ€์ž ์ œ์™ธ
131
+ # ์•ž์— ๋ถ™์ด๊ธฐ
132
+ front_compound = f"{word} {main_keyword}"
133
+ compound_keywords.append(front_compound)
134
+
135
+ # ๋’ค์— ๋ถ™์ด๊ธฐ
136
+ back_compound = f"{main_keyword} {word}"
137
+ compound_keywords.append(back_compound)
138
+
139
+ # ์ค‘๋ณต ์ œ๊ฑฐ
140
+ compound_keywords = list(set(compound_keywords))
141
+ logger.info(f"์ƒ์„ฑ๋œ ๋ณตํ•ฉํ‚ค์›Œ๋“œ ์ˆ˜: {len(compound_keywords)}๊ฐœ")
142
+
143
+ # 4๋‹จ๊ณ„: ๊ฒ€์ƒ‰๋Ÿ‰ ์ถ”์ถœ
144
+ api_keywords = [kw.replace(" ", "") for kw in compound_keywords]
145
+ search_volumes = keyword_search.fetch_all_search_volumes(api_keywords)
146
+
147
+ # 5๋‹จ๊ณ„: ์•ž๋’ค ํ‚ค์›Œ๋“œ ์ค‘ ๋†’์€ ๊ฒƒ ์„ ํƒ, ๋‚ฎ์€ ๊ฒƒ ์ œ๊ฑฐ
148
+ keyword_pairs = {} # {base_word: {"front": front_kw, "back": back_kw, "front_vol": vol, "back_vol": vol}}
149
+
150
+ for word in unique_words:
151
+ if word != main_keyword and len(word) >= 2:
152
+ front_kw = f"{word} {main_keyword}"
153
+ back_kw = f"{main_keyword} {word}"
154
+
155
+ front_api = front_kw.replace(" ", "")
156
+ back_api = back_kw.replace(" ", "")
157
+
158
+ front_vol = search_volumes.get(front_api, {}).get("์ด๊ฒ€์ƒ‰๋Ÿ‰", 0)
159
+ back_vol = search_volumes.get(back_api, {}).get("์ด๊ฒ€์ƒ‰๋Ÿ‰", 0)
160
+
161
+ keyword_pairs[word] = {
162
+ "front": front_kw,
163
+ "back": back_kw,
164
+ "front_vol": front_vol,
165
+ "back_vol": back_vol
166
+ }
167
+
168
+ # 6๋‹จ๊ณ„: ๋†’์€ ๊ฒ€์ƒ‰๋Ÿ‰์˜ ํ‚ค์›Œ๋“œ๋งŒ ์„ ํƒ
169
+ final_keywords = []
170
+ for word, data in keyword_pairs.items():
171
+ if data["front_vol"] > data["back_vol"]:
172
+ selected_kw = data["front"]
173
+ selected_vol = data["front_vol"]
174
+ selected_api = selected_kw.replace(" ", "")
175
+ elif data["back_vol"] > data["front_vol"]:
176
+ selected_kw = data["back"]
177
+ selected_vol = data["back_vol"]
178
+ selected_api = selected_kw.replace(" ", "")
179
+ elif data["front_vol"] == data["back_vol"] and data["front_vol"] > 0:
180
+ # ๊ฐ™์€ ๊ฒ€์ƒ‰๋Ÿ‰์ด๋ฉด ์ž์—ฐ์Šค๋Ÿฌ์šด ์ˆœ์„œ ์„ ํƒ (์ผ๋ฐ˜์ ์œผ๋กœ ๋’ค์— ๋ถ™์ด๋Š” ๊ฒƒ์ด ์ž์—ฐ์Šค๋Ÿฌ์›€)
181
+ selected_kw = data["back"]
182
+ selected_vol = data["back_vol"]
183
+ selected_api = selected_kw.replace(" ", "")
184
+ else:
185
+ # ๋‘˜ ๋‹ค 0์ด๋ฉด ์ œ์™ธ
186
+ continue
187
+
188
+ if selected_vol > 0: # ๊ฒ€์ƒ‰๋Ÿ‰์ด ์žˆ๋Š” ๊ฒƒ๋งŒ ํฌํ•จ
189
+ vol_data = search_volumes.get(selected_api, {})
190
+ final_keywords.append({
191
+ "์—ฐ๊ด€ ํ‚ค์›Œ๋“œ": selected_kw,
192
+ "PC๊ฒ€์ƒ‰๋Ÿ‰": vol_data.get("PC๊ฒ€์ƒ‰๋Ÿ‰", 0),
193
+ "๋ชจ๋ฐ”์ผ๊ฒ€์ƒ‰๋Ÿ‰": vol_data.get("๋ชจ๋ฐ”์ผ๊ฒ€์ƒ‰๋Ÿ‰", 0),
194
+ "์ด๊ฒ€์ƒ‰๋Ÿ‰": selected_vol,
195
+ "๊ฒ€์ƒ‰๋Ÿ‰๊ตฌ๊ฐ„": text_utils.get_search_volume_range(selected_vol)
196
+ })
197
+
198
+ # ๊ฒ€์ƒ‰๋Ÿ‰ ๊ธฐ์ค€์œผ๋กœ ๋‚ด๋ฆผ์ฐจ์ˆœ ์ •๋ ฌ
199
+ final_keywords = sorted(final_keywords, key=lambda x: x["์ด๊ฒ€์ƒ‰๋Ÿ‰"], reverse=True)
200
+
201
+ # DataFrame ์ƒ์„ฑ
202
+ df_keywords = pd.DataFrame(final_keywords)
203
+
204
+ logger.info(f"์—ฐ๊ด€๊ฒ€์ƒ‰์–ด ๋ถ„์„ ์™„๋ฃŒ: {len(final_keywords)}๊ฐœ ํ‚ค์›Œ๋“œ")
205
+
206
+ return {
207
+ "status": "success",
208
+ "message": f"'{keyword}' ์—ฐ๊ด€๊ฒ€์ƒ‰์–ด {len(final_keywords)}๊ฐœ๋ฅผ ์ฐพ์•˜์Šต๋‹ˆ๋‹ค.",
209
+ "keywords_df": df_keywords,
210
+ "total_products": len(products_data)
211
+ }
212
+
213
+ except Exception as e:
214
+ logger.error(f"์—ฐ๊ด€๊ฒ€์ƒ‰์–ด ๋ถ„์„ ์˜ค๋ฅ˜: {e}")
215
+ return {
216
+ "status": "error",
217
+ "message": f"์—ฐ๊ด€๊ฒ€์ƒ‰์–ด ๋ถ„์„ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: {str(e)}",
218
+ "keywords_df": pd.DataFrame()
219
+ }
220
+
221
+ # ===== ๋กœ๋”ฉ ์• ๋‹ˆ๋ฉ”์ด์…˜ =====
222
+ def create_loading_animation():
223
+ """๋กœ๋”ฉ ์• ๋‹ˆ๋ฉ”์ด์…˜ HTML"""
224
+ return """
225
+ <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);">
226
+ <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>
227
+ <h3 style="color: #FB7F0D; margin: 10px 0; font-size: 18px;">๋ถ„์„ ์ค‘์ž…๋‹ˆ๋‹ค...</h3>
228
+ <p style="color: #666; margin: 5px 0; text-align: center;">๋„ค์ด๋ฒ„ ๋ฐ์ดํ„ฐ๋ฅผ ์ˆ˜์ง‘ํ•˜๊ณ  AI๊ฐ€ ๋ถ„์„ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.<br>์ž ์‹œ๋งŒ ๊ธฐ๋‹ค๋ ค์ฃผ์„ธ์š”.</p>
229
+ <div style="width: 200px; height: 4px; background: #f0f0f0; border-radius: 2px; margin-top: 15px; overflow: hidden;">
230
+ <div style="width: 100%; height: 100%; background: linear-gradient(90deg, #FB7F0D, #ff9a8b); border-radius: 2px; animation: progress 2s ease-in-out infinite;"></div>
231
+ </div>
232
+ </div>
233
+
234
+ <style>
235
+ @keyframes spin {
236
+ 0% { transform: rotate(0deg); }
237
+ 100% { transform: rotate(360deg); }
238
  }
239
 
240
+ @keyframes progress {
241
+ 0% { transform: translateX(-100%); }
242
+ 100% { transform: translateX(100%); }
243
+ }
244
+ </style>
245
+ """
246
+
247
+ # ===== ์—๋Ÿฌ ์ฒ˜๋ฆฌ ํ•จ์ˆ˜ =====
248
+ def generate_error_response(error_message):
249
+ """์—๋Ÿฌ ์‘๋‹ต ์ƒ์„ฑ"""
250
+ return f'''
251
+ <div style="color: red; padding: 30px; text-align: center; width: 100%;
252
+ background-color: #f8d7da; border-radius: 12px; border: 1px solid #f5c6cb;">
253
+ <h3 style="margin-bottom: 15px;">โŒ ๋ถ„์„ ์˜ค๋ฅ˜</h3>
254
+ <p style="margin-bottom: 20px;">{error_message}</p>
255
+ <div style="background: white; padding: 15px; border-radius: 8px; color: #333;">
256
+ <h4>ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•:</h4>
257
+ <ul style="text-align: left; padding-left: 20px;">
258
+ <li>ํ‚ค์›Œ๋“œ ์ฒ ์ž๋ฅผ ํ™•์ธํ•ด์ฃผ์„ธ์š”</li>
259
+ <li>๋” ๊ฐ„๋‹จํ•œ ํ‚ค์›Œ๋“œ๋ฅผ ์‚ฌ์šฉํ•ด๋ณด์„ธ์š”</li>
260
+ <li>๋„คํŠธ์›Œํฌ ์—ฐ๊ฒฐ์„ ํ™•์ธํ•ด์ฃผ์„ธ์š”</li>
261
+ <li>์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”</li>
262
+ </ul>
263
+ </div>
264
+ </div>
265
+ '''
266
+
267
+ # ===== ๋ฉ”์ธ ํ‚ค์›Œ๋“œ ๋ถ„์„ ํ•จ์ˆ˜ =====
268
+ def safe_keyword_analysis(analysis_keyword, base_keyword, keywords_data):
269
+ """์—๋Ÿฌ ๋ฐฉ์ง€๋ฅผ ์œ„ํ•œ ์•ˆ์ „ํ•œ ํ‚ค์›Œ๋“œ ๋ถ„์„ - ์„ธ์…˜๋ณ„ ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜"""
270
+
271
+ # ์ž…๋ ฅ๊ฐ’ ๊ฒ€์ฆ
272
+ if not analysis_keyword or not analysis_keyword.strip():
273
+ return generate_error_response("๋ถ„์„ํ•  ํ‚ค์›Œ๋“œ๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”."), {}
274
+
275
+ analysis_keyword = analysis_keyword.strip()
276
+
277
+ try:
278
+ # ๊ฒ€์ƒ‰๋Ÿ‰ ์กฐํšŒ - ์—๋Ÿฌ ๋ฐฉ์ง€
279
+ api_keyword = keyword_analysis.normalize_keyword_for_api(analysis_keyword)
280
+ search_volumes = keyword_search.fetch_all_search_volumes([api_keyword])
281
+ volume_data = search_volumes.get(api_keyword, {"PC๊ฒ€์ƒ‰๋Ÿ‰": 0, "๋ชจ๋ฐ”์ผ๊ฒ€์ƒ‰๋Ÿ‰": 0, "์ด๊ฒ€์ƒ‰๋Ÿ‰": 0})
282
+
283
+ # ๊ฒ€์ƒ‰๋Ÿ‰์ด 0์ด๊ฑฐ๋‚˜ ํ‚ค์›Œ๋“œ๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ ์ฒ˜๋ฆฌ
284
+ if volume_data['์ด๊ฒ€์ƒ‰๋Ÿ‰'] == 0:
285
+ logger.warning(f"'{analysis_keyword}' ํ‚ค์›Œ๋“œ์˜ ๊ฒ€์ƒ‰๋Ÿ‰์ด 0์ด๊ฑฐ๋‚˜ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.")
286
+ error_result = f"""
287
+ <div style="padding: 30px; text-align: center; background: #fff3cd; border-radius: 12px; border: 1px solid #ffeaa7;">
288
+ <h3 style="color: #856404; margin-bottom: 15px;">โš ๏ธ ํ‚ค์›Œ๋“œ ๋ถ„์„ ๋ถˆ๊ฐ€</h3>
289
+ <p style="color: #856404; margin-bottom: 10px;"><strong>'{analysis_keyword}'</strong> ํ‚ค์›Œ๋“œ๋Š” ๊ฒ€์ƒ‰๋Ÿ‰์ด ์—†๊ฑฐ๋‚˜ ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์€ ํ‚ค์›Œ๋“œ์ž…๋‹ˆ๋‹ค.</p>
290
+ <div style="background: white; padding: 15px; border-radius: 8px; margin-top: 15px;">
291
+ <h4 style="color: #333; margin-bottom: 10px;">๐Ÿ’ก ๊ถŒ์žฅ์‚ฌํ•ญ</h4>
292
+ <ul style="text-align: left; color: #666; padding-left: 20px;">
293
+ <li>ํ‚ค์›Œ๋“œ ์ฒ ์ž๋ฅผ ํ™•์ธํ•ด์ฃผ์„ธ์š”</li>
294
+ <li>๋” ์ผ๋ฐ˜์ ์ธ ํ‚ค์›Œ๋“œ๋ฅผ ์‚ฌ์šฉํ•ด๋ณด์„ธ์š”</li>
295
+ <li>2๋‹จ๊ณ„์—์„œ ์ œ์•ˆํ•œ ํ‚ค์›Œ๋“œ ๋ชฉ๋ก์„ ์ฐธ๊ณ ํ•ด์ฃผ์„ธ์š”</li>
296
+ <li>ํ‚ค์›Œ๋“œ๋ฅผ ๋„์–ด์“ฐ๊ธฐ๋กœ ๊ตฌ๋ถ„ํ•ด๋ณด์„ธ์š” (์˜ˆ: '์—ฌ์„ฑ ์Šฌ๋ฆฌํผ')</li>
297
+ </ul>
298
+ </div>
299
+ </div>
300
+ """
301
+ return error_result, {}
302
+
303
+ logger.info(f"'{analysis_keyword}' ํ˜„์žฌ ๊ฒ€์ƒ‰๋Ÿ‰: {volume_data['์ด๊ฒ€์ƒ‰๋Ÿ‰']:,}")
304
+
305
+ # ํŠธ๋ Œ๋“œ ๋ถ„์„ ์‹œ๋„
306
+ monthly_data_1year = {}
307
+ monthly_data_3year = {}
308
+ trend_available = False
309
+
310
+ try:
311
+ # ๋ฐ์ดํ„ฐ๋žฉ API ํ‚ค ํ™•์ธ
312
+ datalab_config = api_utils.get_next_datalab_api_config()
313
+ if datalab_config and not datalab_config["CLIENT_ID"].startswith("YOUR_"):
314
+ logger.info("๋ฐ์ดํ„ฐ๋žฉ API ํ‚ค๊ฐ€ ์„ค์ •๋˜์–ด ์žˆ์–ด 1๋…„, 3๋…„ ํŠธ๋ Œ๋“œ ๋ถ„์„์„ ์‹œ๋„ํ•ฉ๋‹ˆ๋‹ค.")
315
+
316
+ # ์ตœ์ ํ™”๋œ API ํ•จ์ˆ˜ ์‚ฌ์šฉ
317
+ # 1๋…„ ํŠธ๋ Œ๋“œ ๋ฐ์ดํ„ฐ
318
+ trend_data_1year = trend_analysis_v2.get_naver_trend_data_v5([analysis_keyword], "1year", max_retries=3)
319
+ if trend_data_1year:
320
+ current_volumes = {api_keyword: volume_data}
321
+ monthly_data_1year = trend_analysis_v2.calculate_monthly_volumes_v7([analysis_keyword], current_volumes, trend_data_1year, "1year")
322
+
323
+ # 3๋…„ ํŠธ๋ Œ๋“œ ๋ฐ์ดํ„ฐ
324
+ trend_data_3year = trend_analysis_v2.get_naver_trend_data_v5([analysis_keyword], "3year", max_retries=3)
325
+ if trend_data_3year:
326
+ current_volumes = {api_keyword: volume_data}
327
+ monthly_data_3year = trend_analysis_v2.calculate_monthly_volumes_v7([analysis_keyword], current_volumes, trend_data_3year, "3year")
328
+
329
+ # 3๋…„ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ 1๋…„ ๋ฐ์ดํ„ฐ๋กœ ํ™•์žฅ
330
+ if not monthly_data_3year and monthly_data_1year:
331
+ logger.info("3๋…„ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์–ด 1๋…„ ๋ฐ์ดํ„ฐ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ 3๋…„ ์ฐจํŠธ ์ƒ์„ฑ")
332
+ keyword = analysis_keyword
333
+ if keyword in monthly_data_1year:
334
+ data_1y = monthly_data_1year[keyword]
335
+
336
+ # 3๋…„ ๋ถ„๋Ÿ‰์˜ ๋‚ ์งœ ์ƒ์„ฑ (24๊ฐœ์›” ์ถ”๊ฐ€)
337
+ extended_dates = []
338
+ extended_volumes = []
339
+
340
+ # ๊ธฐ์กด 1๋…„ ๋ฐ์ดํ„ฐ ์ด์ „์— 24๊ฐœ์›” ์ถ”๊ฐ€ (๋ชจ๋‘ 0์œผ๋กœ)
341
+ start_date = datetime.strptime(data_1y["dates"][0], "%Y-%m-%d")
342
+ for i in range(24, 0, -1):
343
+ prev_date = start_date - timedelta(days=30 * i)
344
+ extended_dates.append(prev_date.strftime("%Y-%m-%d"))
345
+ extended_volumes.append(0)
346
+
347
+ # ๊ธฐ์กด 1๋…„ ๋ฐ์ดํ„ฐ ์ถ”๊ฐ€ (์˜ˆ์ƒ ๋ฐ์ดํ„ฐ ์ œ์™ธ)
348
+ actual_count = data_1y.get("actual_count", len(data_1y["dates"]))
349
+ extended_dates.extend(data_1y["dates"][:actual_count])
350
+ extended_volumes.extend(data_1y["monthly_volumes"][:actual_count])
351
+
352
+ monthly_data_3year = {
353
+ keyword: {
354
+ "monthly_volumes": extended_volumes,
355
+ "dates": extended_dates,
356
+ "current_volume": data_1y["current_volume"],
357
+ "growth_rate": trend_analysis_v2.calculate_3year_growth_rate_improved(extended_volumes),
358
+ "volume_per_percent": data_1y["volume_per_percent"],
359
+ "current_ratio": data_1y["current_ratio"],
360
+ "actual_count": len(extended_volumes),
361
+ "predicted_count": 0
362
+ }
363
+ }
364
+
365
+ if monthly_data_1year or monthly_data_3year:
366
+ trend_available = True
367
+ logger.info("ํŠธ๋ Œ๋“œ ๋ถ„์„ ์„ฑ๊ณต")
368
+ else:
369
+ logger.info("ํŠธ๋ Œ๋“œ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ ์‹คํŒจ")
370
  else:
371
+ logger.info("๋ฐ์ดํ„ฐ๋žฉ API ํ‚ค๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์Œ")
372
+ except Exception as e:
373
+ logger.info(f"ํŠธ๋ Œ๋“œ ๋ถ„์„ ๊ฑด๋„ˆ๋œ€: {str(e)[:100]}")
374
+
375
+ # ํ‚ค์›Œ๋“œ ๋ฐ์ดํ„ฐ ์ค€๋น„
376
+ step2_keywords_df = keywords_data.get("keywords_df") if keywords_data else None
377
+ filtered_keywords_df = step2_keywords_df # ๋‹จ์ˆœํžˆ ์›๋ณธ ๋ฐ์ดํ„ฐ ์‚ฌ์šฉ
378
+ target_categories = [] # ๋นˆ ๋ฆฌ์ŠคํŠธ
379
+
380
+ # === ๐Ÿ“ˆ ๊ฒ€์ƒ‰๋Ÿ‰ ํŠธ๋ Œ๋“œ ๋ถ„์„ ์„น์…˜ ===
381
+ if trend_available and (monthly_data_1year or monthly_data_3year):
382
+ try:
383
+ trend_chart = trend_analysis_v2.create_trend_chart_v7(monthly_data_1year, monthly_data_3year)
384
+ except Exception as e:
385
+ logger.warning(f"ํŠธ๋ Œ๋“œ ์ฐจํŠธ ์ƒ์„ฑ ์‹คํŒจ, ๊ธฐ๋ณธ ์ฐจํŠธ ์‚ฌ์šฉ: {e}")
386
+ trend_chart = trend_analysis_v2.create_enhanced_current_chart(volume_data, analysis_keyword)
387
+ else:
388
+ trend_chart = trend_analysis_v2.create_enhanced_current_chart(volume_data, analysis_keyword)
389
+
390
+ # ํŠธ๋ Œ๋“œ ์„น์…˜
391
+ trend_section = f"""
392
+ <div style="width: 100%; margin: 30px auto; font-family: 'Pretendard', sans-serif;">
393
+ <div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 15px; border-radius: 10px 10px 0 0; color: white; text-align: center;">
394
+ <h3 style="margin: 0; font-size: 18px; color: white;">๐Ÿ“ˆ ๊ฒ€์ƒ‰๋Ÿ‰ ํŠธ๋ Œ๋“œ ๋ถ„์„</h3>
395
+ </div>
396
+ <div style="background: white; padding: 20px; border-radius: 0 0 10px 10px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);">
397
+ {trend_chart}
398
+ </div>
399
+ </div>
400
+ """
401
+
402
+ # === ๐ŸŽฏ ํ‚ค์›Œ๋“œ ๋ถ„์„ ์„น์…˜ (AI ๋ถ„์„) ===
403
+ # api_utils์—์„œ Gemini ๋ชจ๋ธ ๊ฐ€์ ธ์˜ค๊ธฐ
404
+ current_gemini_model = api_utils.get_gemini_model()
405
+
406
+ keyword_analysis_html = keyword_analysis.analyze_keyword_for_sourcing(
407
+ analysis_keyword, volume_data, monthly_data_1year, monthly_data_3year,
408
+ filtered_keywords_df, target_categories, current_gemini_model
409
+ )
410
+
411
+ keyword_analysis_section = f"""
412
+ <div style="width: 100%; margin: 30px auto; font-family: 'Pretendard', sans-serif;">
413
+ <div style="background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%); padding: 15px; border-radius: 10px 10px 0 0; color: white; text-align: center;">
414
+ <h3 style="margin: 0; font-size: 18px; color: white;">๐ŸŽฏ ํ‚ค์›Œ๋“œ ๋ถ„์„</h3>
415
+ </div>
416
+ <div style="background: white; padding: 20px; border-radius: 0 0 10px 10px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); overflow: hidden;">
417
+ {keyword_analysis_html}
418
+ </div>
419
+ </div>
420
+ """
421
+
422
+ # ๊ฒฝ๊ณ  ์„น์…˜ (ํ•„์š”ํ•œ ๊ฒฝ์šฐ)
423
+ warning_section = ""
424
+ if not trend_available:
425
+ warning_section = f"""
426
+ <div style="width: 100%; margin: 20px auto; padding: 15px; background: #fff3cd; border: 1px solid #ffeaa7; border-radius: 8px; font-family: 'Pretendard', sans-serif;">
427
+ <div style="display: flex; align-items: center;">
428
+ <span style="font-size: 20px; margin-right: 10px;">โš ๏ธ</span>
429
+ <div>
430
+ <strong style="color: #856404;">์ผ๋ถ€ ๊ธฐ๋Šฅ ์ œํ•œ</strong>
431
+ <div style="font-size: 14px; color: #856404; margin-top: 5px;">
432
+ ํŠธ๋ Œ๋“œ ๋ถ„์„์— ์ œํ•œ์ด ์žˆ์Šต๋‹ˆ๋‹ค. ํ˜„์žฌ ๊ฒ€์ƒ‰๋Ÿ‰ ๋ถ„์„๊ณผ AI ์ถ”์ฒœ์€ ์ •์ƒ ์ œ๊ณต๋ฉ๋‹ˆ๋‹ค.<br>
433
+ <small>์™„์ „ํ•œ ์›” ๋ฐ์ดํ„ฐ ๊ธฐ์ค€์œผ๋กœ ๋ถ„์„ํ•˜๊ธฐ ์œ„ํ•ด ์ตœ์‹  ์™„๋ฃŒ๋œ ์›”๊นŒ์ง€๋งŒ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.</small>
434
+ </div>
435
+ </div>
436
+ </div>
437
+ </div>
438
+ """
439
+
440
+ # ์ตœ์ข… ๊ฒฐ๊ณผ ์กฐํ•ฉ
441
+ final_result = warning_section + trend_section + keyword_analysis_section
442
+
443
+ # ์„ธ์…˜๋ณ„ ์ถœ๋ ฅ์šฉ ์ƒํƒœ ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜ (๋ฉ€ํ‹ฐ ์‚ฌ์šฉ์ž ์•ˆ์ „)
444
+ session_export_data = {
445
+ "main_keyword": base_keyword,
446
+ "analysis_keyword": analysis_keyword,
447
+ "main_keywords_df": keywords_data.get("keywords_df") if keywords_data else None,
448
+ "related_keywords_df": None, # ์—ฌ๊ธฐ์„œ๋Š” ์—ฐ๊ด€๊ฒ€์ƒ‰์–ด ๋ถ„์„ํ•˜์ง€ ์•Š์Œ
449
+ "analysis_html": final_result
450
+ }
451
+
452
+ return final_result, session_export_data
453
+
454
+ except Exception as e:
455
+ logger.error(f"ํ‚ค์›Œ๋“œ ๋ถ„์„ ์ค‘ ์ „์ฒด ์˜ค๋ฅ˜: {e}")
456
+ error_result = generate_error_response(f"ํ‚ค์›Œ๋“œ ๋ถ„์„ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: {str(e)}")
457
+ return error_result, {}
458
 
459
+ # ===== 2๋‹จ๊ณ„: ์ƒํ’ˆ ๋ฐ์ดํ„ฐ ๊ธฐ๋ฐ˜ ํ‚ค์›Œ๋“œ ์ถ”์ถœ =====
460
+ def extract_keywords_from_products(keyword):
461
+ """๋„ค์ด๋ฒ„ ์‡ผํ•‘์—์„œ ์‹ค์ œ ์ƒํ’ˆ ๋ฐ์ดํ„ฐ๋ฅผ ์ˆ˜์ง‘ํ•˜๊ณ  ๋ชจ๋“  ํ‚ค์›Œ๋“œ ํ‘œ์‹œ"""
462
+ logger.info(f"์ƒํ’ˆ ํ‚ค์›Œ๋“œ ์ถ”์ถœ ์‹œ์ž‘: ํ‚ค์›Œ๋“œ='{keyword}'")
463
+
464
+ api_keyword = keyword_analysis.normalize_keyword_for_api(keyword)
465
+ search_results = product_search.fetch_naver_shopping_data(
466
+ keyword, korean_only=True, apply_main_keyword=True, exclude_zero_volume=True
467
+ )
468
 
469
+ if not search_results.get("product_list"):
 
470
  return {
471
+ "status": "error",
472
+ "message": "์ƒํ’ˆ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.",
473
+ "products": [],
474
+ "keywords": []
 
 
475
  }
476
 
477
+ processed_results = keyword_processor.process_search_results(
478
+ search_results, keyword, exclude_zero_volume=True
479
+ )
 
 
 
 
480
 
481
+ df_keywords = processed_results["keywords_df"]
482
+ df_products = processed_results["products_df"]
 
 
483
 
484
+ if df_keywords.empty:
485
+ return {
486
+ "status": "error",
487
+ "message": "์ถ”์ถœ๋œ ํ‚ค์›Œ๋“œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.",
488
+ "products": [],
489
+ "keywords": []
490
+ }
491
+
492
+ logger.info(f"ํ‚ค์›Œ๋“œ ์ถ”์ถœ ์™„๋ฃŒ: ์ด {len(df_keywords)}๊ฐœ ํ‚ค์›Œ๋“œ")
493
+
494
+ return {
495
+ "status": "success",
496
+ "message": "ํ‚ค์›Œ๋“œ ์ถ”์ถœ ์™„๋ฃŒ",
497
+ "products": df_products,
498
+ "keywords_df": df_keywords,
499
+ "categories": processed_results["categories"]
500
+ }
 
 
 
 
 
 
 
501
 
502
  # ===== ํŒŒ์ผ ์ถœ๋ ฅ ํ•จ์ˆ˜๋“ค =====
503
  def create_timestamp_filename(analysis_keyword):
 
508
  return f"{safe_keyword}_{timestamp}_๋ถ„์„๊ฒฐ๊ณผ"
509
 
510
  def export_to_excel(main_keyword, main_keywords_df, analysis_keyword, related_keywords_df, filename_base):
511
+ """์—‘์…€ ํŒŒ์ผ๋กœ ์ถœ๋ ฅ"""
512
  try:
 
 
 
 
 
 
 
 
513
  excel_filename = f"{filename_base}.xlsx"
514
  excel_path = os.path.join(tempfile.gettempdir(), excel_filename)
515
 
 
541
  'border': 1
542
  })
543
 
544
+ # ์ฒซ ๋ฒˆ์งธ ์‹œํŠธ: ๋ฉ”์ธํ‚ค์›Œ๋“œ ์กฐํ•ฉํ‚ค์›Œ๋“œ
545
+ if main_keywords_df is not None and not main_keywords_df.empty:
546
  main_keywords_df.to_excel(writer, sheet_name=f'{main_keyword}_์กฐํ•ฉํ‚ค์›Œ๋“œ', index=False)
547
  worksheet1 = writer.sheets[f'{main_keyword}_์กฐํ•ฉํ‚ค์›Œ๋“œ']
548
 
 
553
  # ๋ฐ์ดํ„ฐ ์Šคํƒ€์ผ ์ ์šฉ
554
  for row_num in range(1, len(main_keywords_df) + 1):
555
  for col_num, value in enumerate(main_keywords_df.iloc[row_num-1]):
556
+ if col_num in [1, 2, 3]: # PC๊ฒ€์ƒ‰๋Ÿ‰, ๋ชจ๋ฐ”์ผ๊ฒ€์ƒ‰๋Ÿ‰, ์ด๊ฒ€์ƒ‰๋Ÿ‰ ์ปฌ๋Ÿผ
557
  worksheet1.write(row_num, col_num, value, number_format)
558
  else:
559
  worksheet1.write(row_num, col_num, value, data_format)
 
565
  len(str(col))
566
  )
567
  worksheet1.set_column(i, i, min(max_len + 2, 50))
 
 
568
 
569
+ # ๋‘ ๋ฒˆ์งธ ์‹œํŠธ: ๋ถ„์„ํ‚ค์›Œ๋“œ ์—ฐ๊ด€๊ฒ€์ƒ‰์–ด
570
+ if related_keywords_df is not None and not related_keywords_df.empty:
571
  related_keywords_df.to_excel(writer, sheet_name=f'{analysis_keyword}_์—ฐ๊ด€๊ฒ€์ƒ‰์–ด', index=False)
572
  worksheet2 = writer.sheets[f'{analysis_keyword}_์—ฐ๊ด€๊ฒ€์ƒ‰์–ด']
573
 
 
578
  # ๋ฐ์ดํ„ฐ ์Šคํƒ€์ผ ์ ์šฉ
579
  for row_num in range(1, len(related_keywords_df) + 1):
580
  for col_num, value in enumerate(related_keywords_df.iloc[row_num-1]):
581
+ if col_num in [1, 2, 3]: # PC๊ฒ€์ƒ‰๋Ÿ‰, ๋ชจ๋ฐ”์ผ๊ฒ€์ƒ‰๋Ÿ‰, ์ด๊ฒ€์ƒ‰๋Ÿ‰ ์ปฌ๋Ÿผ
582
  worksheet2.write(row_num, col_num, value, number_format)
583
  else:
584
  worksheet2.write(row_num, col_num, value, data_format)
 
590
  len(str(col))
591
  )
592
  worksheet2.set_column(i, i, min(max_len + 2, 50))
 
 
593
 
594
  logger.info(f"์—‘์…€ ํŒŒ์ผ ์ƒ์„ฑ ์™„๋ฃŒ: {excel_path}")
595
  return excel_path
 
719
  <div class="container">
720
  <div class="header">
721
  <h1><i class="fas fa-chart-line"></i> ํ‚ค์›Œ๋“œ ์‹ฌ์ถฉ๋ถ„์„ ๊ฒฐ๊ณผ</h1>
722
+ <p>AI ์ƒํ’ˆ ์†Œ์‹ฑ ๋ถ„์„ ์‹œ์Šคํ…œ v2.9</p>
723
  </div>
724
  <div class="content">
725
  {analysis_html}
 
748
  zip_filename = f"{filename_base}.zip"
749
  zip_path = os.path.join(tempfile.gettempdir(), zip_filename)
750
 
 
751
  with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
752
  if excel_path and os.path.exists(excel_path):
753
  zipf.write(excel_path, f"{filename_base}.xlsx")
754
  logger.info(f"์—‘์…€ ํŒŒ์ผ ์••์ถ• ์ถ”๊ฐ€: {filename_base}.xlsx")
 
755
 
756
  if html_path and os.path.exists(html_path):
757
  zipf.write(html_path, f"{filename_base}.html")
758
  logger.info(f"HTML ํŒŒ์ผ ์••์ถ• ์ถ”๊ฐ€: {filename_base}.html")
 
759
 
760
+ logger.info(f"์••์ถ• ํŒŒ์ผ ์ƒ์„ฑ ์™„๋ฃŒ: {zip_path}")
 
 
 
 
761
  return zip_path
762
 
763
  except Exception as e:
764
  logger.error(f"์••์ถ• ํŒŒ์ผ ์ƒ์„ฑ ์˜ค๋ฅ˜: {e}")
765
  return None
766
 
767
+ def export_analysis_results(export_data):
768
+ """๋ถ„์„ ๊ฒฐ๊ณผ ์ถœ๋ ฅ ๋ฉ”์ธ ํ•จ์ˆ˜ - ์„ธ์…˜๋ณ„ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ"""
769
  try:
770
+ # ์ถœ๋ ฅํ•  ๋ฐ์ดํ„ฐ ํ™•์ธ
771
+ if not export_data or not isinstance(export_data, dict):
772
+ return None, "๋ถ„์„ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ๋จผ์ € ํ‚ค์›Œ๋“œ ์‹ฌ์ถฉ๋ถ„์„์„ ์‹คํ–‰ํ•ด์ฃผ์„ธ์š”."
 
773
 
774
+ analysis_keyword = export_data.get("analysis_keyword", "")
775
+ analysis_html = export_data.get("analysis_html", "")
776
+ main_keyword = export_data.get("main_keyword", "")
777
  main_keywords_df = export_data.get("main_keywords_df")
778
  related_keywords_df = export_data.get("related_keywords_df")
779
 
780
+ if not analysis_keyword:
781
+ return None, "๋ถ„์„ํ•  ํ‚ค์›Œ๋“œ๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ๋จผ์ € ํ‚ค์›Œ๋“œ ๋ถ„์„์„ ์‹คํ–‰ํ•ด์ฃผ์„ธ์š”."
782
+
783
+ if not analysis_html:
784
+ return None, "๋ถ„์„ ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ๋จผ์ € ํ‚ค์›Œ๋“œ ์‹ฌ์ถฉ๋ถ„์„์„ ์‹คํ–‰ํ•ด์ฃผ์„ธ์š”."
 
785
 
786
  # ํŒŒ์ผ๋ช… ์ƒ์„ฑ (ํ•œ๊ตญ์‹œ๊ฐ„ ์ ์šฉ)
787
  filename_base = create_timestamp_filename(analysis_keyword)
788
+ logger.info(f"์ถœ๋ ฅ ํŒŒ์ผ๋ช…: {filename_base}")
 
 
 
 
 
 
 
 
 
 
 
 
789
 
790
+ # ์—‘์…€ ํŒŒ์ผ ์ƒ์„ฑ
791
  excel_path = None
792
+ if main_keywords_df is not None or related_keywords_df is not None:
 
 
793
  excel_path = export_to_excel(
794
  main_keyword,
795
  main_keywords_df,
 
797
  related_keywords_df,
798
  filename_base
799
  )
 
 
 
 
 
 
800
 
801
+ # HTML ํŒŒ์ผ ์ƒ์„ฑ
802
+ html_path = export_to_html(analysis_html, filename_base)
 
 
803
 
804
  # ์••์ถ• ํŒŒ์ผ ์ƒ์„ฑ
805
+ if excel_path or html_path:
806
+ zip_path = create_zip_file(excel_path, html_path, filename_base)
807
+ if zip_path:
808
+ return zip_path, f"โœ… ๋ถ„์„ ๊ฒฐ๊ณผ๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ์ถœ๋ ฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!\nํŒŒ์ผ๋ช…: {filename_base}.zip"
809
+ else:
810
+ return None, "์••์ถ• ํŒŒ์ผ ์ƒ์„ฑ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."
 
 
 
 
 
 
811
  else:
812
+ return None, "์ถœ๋ ฅํ•  ํŒŒ์ผ์ด ์—†์Šต๋‹ˆ๋‹ค."
 
813
 
814
  except Exception as e:
815
+ logger.error(f"๋ถ„์„ ๊ฒฐ๊ณผ ์ถœ๋ ฅ ์˜ค๋ฅ˜: {e}")
 
 
816
  return None, f"์ถœ๋ ฅ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: {str(e)}"
817
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
818
  # ===== ๊ทธ๋ผ๋””์˜ค ์ธํ„ฐํŽ˜์ด์Šค =====
819
  def create_interface():
820
+ # CSS ํŒŒ์ผ ๋กœ๋“œ
821
+ try:
822
+ with open('style.css', 'r', encoding='utf-8') as f:
823
+ custom_css = f.read()
824
+
825
+ with open('keyword_analysis_report.css', 'r', encoding='utf-8') as f:
826
+ keyword_css = f.read()
827
+ custom_css += "\n" + keyword_css
828
+ except:
829
+ custom_css = """
830
+ :root { --primary-color: #FB7F0D; --secondary-color: #ff9a8b; }
831
+ .custom-button {
832
+ background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)) !important;
833
+ color: white !important; border-radius: 30px !important; height: 45px !important;
834
+ font-size: 16px !important; font-weight: bold !important; width: 100% !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
835
  }
836
+ .export-button {
837
+ background: linear-gradient(135deg, #28a745, #20c997) !important;
838
+ color: white !important; border-radius: 25px !important; height: 50px !important;
839
+ font-size: 17px !important; font-weight: bold !important; width: 100% !important;
840
+ margin-top: 20px !important;
841
+ }
842
+ """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
843
 
844
  with gr.Blocks(
845
  css=custom_css,
846
+ title="๐Ÿ›’ AI ์ƒํ’ˆ ์†Œ์‹ฑ ๋ถ„์„๊ธฐ v2.9",
847
  theme=gr.themes.Default(primary_hue="orange", secondary_hue="orange")
848
  ) as interface:
849
 
 
853
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/static/pretendard.css">
854
  """)
855
 
856
+ # ์„ธ์…˜๋ณ„ ์ƒํƒœ ๋ณ€์ˆ˜ (๋ฉ€ํ‹ฐ ์‚ฌ์šฉ์ž ์•ˆ์ „)
857
  keywords_data_state = gr.State()
858
  export_data_state = gr.State({})
859
 
860
+ # === 1๋‹จ๊ณ„: ๋ฉ”์ธ ํ‚ค์›Œ๋“œ ์ž…๋ ฅ ===
861
  with gr.Column(elem_classes="custom-frame fade-in"):
862
  gr.HTML('<div class="section-title"><i class="fas fa-search"></i> 1๋‹จ๊ณ„: ๋ฉ”์ธ ํ‚ค์›Œ๋“œ ์ž…๋ ฅ</div>')
863
 
 
870
 
871
  collect_data_btn = gr.Button("1๋‹จ๊ณ„: ์ƒํ’ˆ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ํ•˜๊ธฐ", elem_classes="custom-button", size="lg")
872
 
873
+ # === 2๋‹จ๊ณ„: ์ˆ˜์ง‘๋œ ํ‚ค์›Œ๋“œ ๋ชฉ๋ก ===
874
  with gr.Column(elem_classes="custom-frame fade-in"):
875
  gr.HTML('<div class="section-title"><i class="fas fa-database"></i> 2๋‹จ๊ณ„: ์ˆ˜์ง‘๋œ ํ‚ค์›Œ๋“œ ๋ชฉ๋ก</div>')
876
  keywords_result = gr.HTML()
877
 
878
+ # === 3๋‹จ๊ณ„: ๋ถ„์„ํ•  ํ‚ค์›Œ๋“œ ์„ ํƒ ===
879
  with gr.Column(elem_classes="custom-frame fade-in"):
880
  gr.HTML('<div class="section-title"><i class="fas fa-bullseye"></i> 3๋‹จ๊ณ„: ๋ถ„์„ํ•  ํ‚ค์›Œ๋“œ ์„ ํƒ</div>')
881
 
 
888
 
889
  analyze_keyword_btn = gr.Button("ํ‚ค์›Œ๋“œ ์‹ฌ์ถฉ๋ถ„์„ ํ•˜๊ธฐ", elem_classes="custom-button", size="lg")
890
 
891
+ # === ํ‚ค์›Œ๋“œ ์‹ฌ์ถฉ๋ถ„์„ ===
892
  with gr.Column(elem_classes="custom-frame fade-in"):
893
  gr.HTML('<div class="section-title"><i class="fas fa-chart-line"></i> ํ‚ค์›Œ๋“œ ์‹ฌ์ถฉ๋ถ„์„</div>')
894
  analysis_result = gr.HTML(label="ํ‚ค์›Œ๋“œ ์‹ฌ์ถฉ๋ถ„์„")
895
 
896
+ # === ๊ฒฐ๊ณผ ์ถœ๋ ฅ ์„น์…˜ ===
897
  with gr.Column(elem_classes="custom-frame fade-in"):
898
  gr.HTML('<div class="section-title"><i class="fas fa-download"></i> ๋ถ„์„ ๊ฒฐ๊ณผ ์ถœ๋ ฅ</div>')
 
 
 
 
 
 
 
 
 
899
 
900
  export_btn = gr.Button("๐Ÿ“Š ๋ถ„์„๊ฒฐ๊ณผ ์ถœ๋ ฅํ•˜๊ธฐ", elem_classes="export-button", size="lg")
901
  export_result = gr.HTML()
 
909
  # ๋กœ๋”ฉ ์ƒํƒœ ํ‘œ์‹œ
910
  yield (create_loading_animation(), None)
911
 
912
+ result = extract_keywords_from_products(keyword)
913
+
914
+ if result["status"] == "error":
915
+ yield (f"<div style='color: red; padding: 20px; text-align: center; width: 100%;'>{result['message']}</div>", None)
916
+ return
917
+
918
+ keywords_df = result["keywords_df"]
919
+ html_table = export_utils.create_table_without_checkboxes(keywords_df)
920
 
921
+ success_html = f"""
922
+ <div style="width: 100%; background: #d4edda; border: 1px solid #c3e6cb; padding: 15px; border-radius: 5px; margin-bottom: 20px;">
923
+ <h4 style="color: #155724; margin: 0 0 10px 0;">โœ… ๋„ค์ด๋ฒ„ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ์™„๋ฃŒ!</h4>
924
+ <p style="margin: 0; color: #155724;">
925
+ โ€ข ์‹ค์ œ ์ƒํ’ˆ {len(result['products'])}๊ฐœ ๋ถ„์„<br>
926
+ โ€ข ์ถ”์ถœ๋œ ํ‚ค์›Œ๋“œ: <strong>{len(keywords_df)}๊ฐœ</strong><br>
927
+ โ€ข ์•„๋ž˜ ๋ชฉ๋ก์—์„œ ์›ํ•˜๋Š” ํ‚ค์›Œ๋“œ๋ฅผ ์„ ํƒํ•˜์—ฌ ๋ถ„์„ํ•˜์„ธ์š”
928
+ </p>
929
+ </div>
930
+
931
+ <h5 style="margin: 20px 0 10px 0; color: #495057;">๐Ÿ“Š ์ „์ฒด ํ‚ค์›Œ๋“œ ๋ชฉ๋ก</h5>
932
+ {html_table}
933
+ """
934
+
935
+ yield (success_html, result)
936
 
937
  def on_analyze_keyword(analysis_keyword, base_keyword, keywords_data):
938
  if not analysis_keyword.strip():
 
941
  # ๋กœ๋”ฉ ์ƒํƒœ ํ‘œ์‹œ
942
  yield create_loading_animation(), {}
943
 
944
+ # ์—ฐ๊ด€๊ฒ€์ƒ‰์–ด ๋ถ„์„ ๋จผ์ € ์‹คํ–‰
945
+ related_result = analyze_related_keywords(analysis_keyword)
946
+
947
+ # ์‹ค์ œ ํ‚ค์›Œ๋“œ ๋ถ„์„ ์‹คํ–‰ (์„ธ์…˜๋ณ„ ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜)
948
+ keyword_result, session_export_data = safe_keyword_analysis(analysis_keyword, base_keyword, keywords_data)
949
 
950
+ # ์—ฐ๊ด€๊ฒ€์ƒ‰์–ด ๋ถ„์„ ๊ฒฐ๊ณผ๋ฅผ ์„ธ์…˜ ๋ฐ์ดํ„ฐ์— ์ถ”๊ฐ€
951
+ if related_result["status"] == "success" and not related_result["keywords_df"].empty:
952
+ session_export_data["related_keywords_df"] = related_result["keywords_df"]
953
+
954
+ # ์—ฐ๊ด€๊ฒ€์ƒ‰์–ด ๋ถ„์„ ๊ฒฐ๊ณผ HTML ์ƒ์„ฑ
955
+ if related_result["status"] == "success" and not related_result["keywords_df"].empty:
956
+ df_keywords = related_result["keywords_df"]
957
+ related_table = export_utils.create_table_without_checkboxes(df_keywords)
958
 
959
+ related_html = f"""
960
+ <div style="width: 100%; margin: 30px auto; font-family: 'Pretendard', sans-serif;">
961
+ <div style="background: linear-gradient(135deg, #17a2b8 0%, #20c997 100%); padding: 15px; border-radius: 10px 10px 0 0; color: white; text-align: center;">
962
+ <h3 style="margin: 0; font-size: 18px; color: white;">๐Ÿ”— ์—ฐ๊ด€๊ฒ€์ƒ‰์–ด ๋ถ„์„</h3>
963
+ </div>
964
+ <div style="background: white; padding: 20px; border-radius: 0 0 10px 10px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);">
965
+ <div style="background: #e8f5e8; border: 1px solid #c3e6cb; padding: 15px; border-radius: 5px; margin-bottom: 20px;">
966
+ <h4 style="color: #155724; margin: 0 0 10px 0;">๐Ÿ”— ์—ฐ๊ด€๊ฒ€์ƒ‰์–ด ๋ถ„์„ ์™„๋ฃŒ!</h4>
967
+ <p style="margin: 0; color: #155724;">
968
+ โ€ข ๋ถ„์„ ๊ธฐ์ค€ ์ƒํ’ˆ: <strong>{related_result['total_products']}๊ฐœ</strong><br>
969
+ โ€ข ๋ฐœ๊ฒฌ๋œ ์—ฐ๊ด€๊ฒ€์ƒ‰์–ด: <strong>{len(df_keywords)}๊ฐœ</strong><br>
970
+ โ€ข ๋ฉ”์ธ ํ‚ค์›Œ๋“œ์™€ ๊ฒฐํ•ฉ๋œ ๋ณตํ•ฉํ‚ค์›Œ๋“œ๋งŒ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค
971
+ </p>
972
+ </div>
973
+ {related_table}
974
+ </div>
975
+ </div>
976
+ """
977
+
978
+ # ์„ธ์…˜ ๋ฐ์ดํ„ฐ์˜ analysis_html์„ ์—…๋ฐ์ดํŠธ
979
+ session_export_data["analysis_html"] = related_html + session_export_data["analysis_html"]
980
+ else:
981
+ related_html = f"""
982
+ <div style="width: 100%; margin: 30px auto; font-family: 'Pretendard', sans-serif;">
983
+ <div style="background: linear-gradient(135deg, #17a2b8 0%, #20c997 100%); padding: 15px; border-radius: 10px 10px 0 0; color: white; text-align: center;">
984
+ <h3 style="margin: 0; font-size: 18px; color: white;">๐Ÿ”— ์—ฐ๊ด€๊ฒ€์ƒ‰์–ด ๋ถ„์„</h3>
985
+ </div>
986
+ <div style="background: white; padding: 20px; border-radius: 0 0 10px 10px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);">
987
+ <div style="color: orange; padding: 20px; text-align: center; background: #fff3cd; border-radius: 8px;">
988
+ '{analysis_keyword}' ํ‚ค์›Œ๋“œ์˜ ์—ฐ๊ด€๊ฒ€์ƒ‰์–ด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.
989
+ </div>
990
+ </div>
991
+ </div>
992
+ """
993
+
994
+ # ์„ธ์…˜ ๋ฐ์ดํ„ฐ์˜ analysis_html์„ ์—…๋ฐ์ดํŠธ
995
+ session_export_data["analysis_html"] = related_html + session_export_data["analysis_html"]
996
+
997
+ # ์ตœ์ข… ๊ฒฐ๊ณผ ์กฐํ•ฉ
998
+ final_result = related_html + keyword_result
999
+ yield final_result, session_export_data
1000
+
1001
  def on_export_results(export_data):
1002
+ """๋ถ„์„ ๊ฒฐ๊ณผ ์ถœ๋ ฅ ํ•ธ๋“ค๋Ÿฌ - ์„ธ์…˜๋ณ„ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ"""
1003
  try:
1004
+ zip_path, message = export_analysis_results(export_data)
 
 
 
 
 
1005
 
1006
  if zip_path:
1007
+ # ์„ฑ๊ณต ๋ฉ”์‹œ์ง€์™€ ํ•จ๊ป˜ ๋‹ค์šด๋กœ๋“œ ํŒŒ์ผ ์ œ๊ณต
1008
  success_html = f"""
1009
  <div style="background: #d4edda; border: 1px solid #c3e6cb; padding: 20px; border-radius: 8px; margin: 10px 0;">
1010
  <h4 style="color: #155724; margin: 0 0 15px 0;"><i class="fas fa-check-circle"></i> ์ถœ๋ ฅ ์™„๋ฃŒ!</h4>
1011
  <p style="color: #155724; margin: 0; line-height: 1.6;">
1012
  {message}<br>
1013
+ <strong>ํฌํ•จ ํŒŒ์ผ:</strong><br>
1014
+ โ€ข ๐Ÿ“Š ์—‘์…€ ํŒŒ์ผ: ๋ฉ”์ธํ‚ค์›Œ๋“œ ์กฐํ•ฉํ‚ค์›Œ๋“œ + ์—ฐ๊ด€๊ฒ€์ƒ‰์–ด ๋ฐ์ดํ„ฐ<br>
1015
+ โ€ข ๐ŸŒ HTML ํŒŒ์ผ: ํ‚ค์›Œ๋“œ ์‹ฌ์ถฉ๋ถ„์„ ๊ฒฐ๊ณผ (๊ทธ๋ž˜ํ”„ ํฌํ•จ)<br>
1016
  <br>
1017
  <i class="fas fa-download"></i> ์•„๋ž˜ ๋‹ค์šด๋กœ๋“œ ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜์—ฌ ํŒŒ์ผ์„ ์ €์žฅํ•˜์„ธ์š”.<br>
1018
  <small style="color: #666;">โฐ ํ•œ๊ตญ์‹œ๊ฐ„ ๊ธฐ์ค€์œผ๋กœ ํŒŒ์ผ๋ช…์ด ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค.</small>
 
1021
  """
1022
  return success_html, gr.update(value=zip_path, visible=True)
1023
  else:
1024
+ # ์‹คํŒจ ๋ฉ”์‹œ์ง€
1025
  error_html = f"""
1026
  <div style="background: #f8d7da; border: 1px solid #f5c6cb; padding: 20px; border-radius: 8px; margin: 10px 0;">
1027
  <h4 style="color: #721c24; margin: 0 0 10px 0;"><i class="fas fa-exclamation-triangle"></i> ์ถœ๋ ฅ ์‹คํŒจ</h4>
1028
  <p style="color: #721c24; margin: 0;">{message}</p>
 
 
 
 
 
 
 
 
1029
  </div>
1030
  """
 
1031
  return error_html, gr.update(visible=False)
1032
 
1033
  except Exception as e:
1034
+ logger.error(f"์ถœ๋ ฅ ํ•ธ๋“ค๋Ÿฌ ์˜ค๋ฅ˜: {e}")
 
 
 
1035
  error_html = f"""
1036
  <div style="background: #f8d7da; border: 1px solid #f5c6cb; padding: 20px; border-radius: 8px; margin: 10px 0;">
1037
  <h4 style="color: #721c24; margin: 0 0 10px 0;"><i class="fas fa-exclamation-triangle"></i> ์‹œ์Šคํ…œ ์˜ค๋ฅ˜</h4>
1038
+ <p style="color: #721c24; margin: 0;">์ถœ๋ ฅ ์ค‘ ์‹œ์Šคํ…œ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: {str(e)}</p>
 
 
 
 
 
 
 
 
1039
  </div>
1040
  """
1041
  return error_html, gr.update(visible=False)
 
1044
  collect_data_btn.click(
1045
  fn=on_collect_data,
1046
  inputs=[keyword_input],
1047
+ outputs=[keywords_result, keywords_data_state]
 
1048
  )
1049
 
1050
  analyze_keyword_btn.click(
1051
  fn=on_analyze_keyword,
1052
+ inputs=[analysis_keyword_input, keyword_input, keywords_data_state],
1053
+ outputs=[analysis_result, export_data_state]
 
1054
  )
1055
 
1056
  export_btn.click(
1057
  fn=on_export_results,
1058
  inputs=[export_data_state],
1059
+ outputs=[export_result, download_file]
 
1060
  )
1061
 
1062
  return interface
1063
 
1064
+ # ===== API ์„ค์ • ํ™•์ธ ํ•จ์ˆ˜ =====
1065
+ def check_datalab_api_config():
1066
+ """๋„ค์ด๋ฒ„ ๋ฐ์ดํ„ฐ๋žฉ API ์„ค์ • ํ™•์ธ"""
1067
+ logger.info("=== ๋„ค์ด๋ฒ„ ๋ฐ์ดํ„ฐ๋žฉ API ์„ค์ • ํ™•์ธ ===")
1068
+
1069
+ datalab_config = api_utils.get_next_datalab_api_config()
1070
+
1071
+ if not datalab_config:
1072
+ logger.warning("โŒ ๋ฐ์ดํ„ฐ๋žฉ API ํ‚ค๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.")
1073
+ logger.info("ํŠธ๋ Œ๋“œ ๋ถ„์„ ๊ธฐ๋Šฅ์ด ๋น„ํ™œ์„ฑํ™”๋ฉ๋‹ˆ๋‹ค.")
1074
+ return False
1075
+
1076
+ client_id = datalab_config["CLIENT_ID"]
1077
+ client_secret = datalab_config["CLIENT_SECRET"]
1078
+
1079
+ logger.info(f"์ด {len(api_utils.NAVER_DATALAB_CONFIGS)}๊ฐœ์˜ ๋ฐ์ดํ„ฐ๋žฉ API ์„ค์ • ์‚ฌ์šฉ ์ค‘")
1080
+ logger.info(f"ํ˜„์žฌ ์„ ํƒ๋œ API:")
1081
+ logger.info(f" CLIENT_ID: {client_id[:8]}***{client_id[-4:] if len(client_id) > 12 else '***'}")
1082
+ logger.info(f" CLIENT_SECRET: {client_secret[:4]}***{client_secret[-2:] if len(client_secret) > 6 else '***'}")
1083
+
1084
+ # ๊ธฐ๋ณธ๊ฐ’ ์ฒดํฌ
1085
+ if client_id.startswith("YOUR_"):
1086
+ logger.error("โŒ CLIENT_ID๊ฐ€ ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ์„ค์ •๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค!")
1087
+ return False
1088
+
1089
+ if client_secret.startswith("YOUR_"):
1090
+ logger.error("โŒ CLIENT_SECRET์ด ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ์„ค์ •๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค!")
1091
+ return False
1092
+
1093
+ # ๊ธธ์ด ์ฒดํฌ
1094
+ if len(client_id) < 10:
1095
+ logger.warning("โš ๏ธ CLIENT_ID๊ฐ€ ์งง์Šต๋‹ˆ๋‹ค. ์˜ฌ๋ฐ”๋ฅธ ํ‚ค์ธ์ง€ ํ™•์ธํ•ด์ฃผ์„ธ์š”.")
1096
+
1097
+ if len(client_secret) < 5:
1098
+ logger.warning("โš ๏ธ CLIENT_SECRET์ด ์งง์Šต๋‹ˆ๋‹ค. ์˜ฌ๋ฐ”๋ฅธ ํ‚ค์ธ์ง€ ํ™•์ธํ•ด์ฃผ์„ธ์š”.")
1099
+
1100
+ logger.info("โœ… ๋ฐ์ดํ„ฐ๋žฉ API ํ‚ค ํ˜•์‹ ๊ฒ€์ฆ ์™„๋ฃŒ")
1101
+ return True
1102
+
1103
+ def check_gemini_api_config():
1104
+ """Gemini API ์„ค์ • ํ™•์ธ"""
1105
+ logger.info("=== Gemini API ์„ค์ • ํ™•์ธ ===")
1106
+
1107
+ is_valid, message = api_utils.validate_gemini_config()
1108
+
1109
+ if is_valid:
1110
+ logger.info(f"โœ… {message}")
1111
+ # ์ฒซ ๋ฒˆ์งธ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ํ‚ค ํ…Œ์ŠคํŠธ
1112
+ test_key = api_utils.get_next_gemini_api_key()
1113
+ if test_key:
1114
+ logger.info(f"ํ˜„์žฌ ์‚ฌ์šฉ ์ค‘์ธ Gemini API ํ‚ค: {test_key[:8]}***{test_key[-4:]}")
1115
+ return True
1116
+ else:
1117
+ logger.warning(f"โŒ {message}")
1118
+ logger.info("AI ๋ถ„์„ ๊ธฐ๋Šฅ์ด ์ œํ•œ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.")
1119
+ return False
1120
+
1121
  # ===== ๋ฉ”์ธ ์‹คํ–‰ =====
1122
  if __name__ == "__main__":
1123
  # pytz ๋ชจ๋“ˆ ์„ค์น˜ ํ™•์ธ
 
1125
  import pytz
1126
  logger.info("โœ… pytz ๋ชจ๋“ˆ ๋กœ๋“œ ์„ฑ๊ณต - ํ•œ๊ตญ์‹œ๊ฐ„ ์ง€์›")
1127
  except ImportError:
1128
+ logger.warning("โš ๏ธ pytz ๋ชจ๋“ˆ์ด ์„ค์น˜๋˜์ง€ ์•Š์Œ - pip install pytz ์‹คํ–‰ ํ•„์š”")
1129
  logger.info("์‹œ์Šคํ…œ ์‹œ๊ฐ„์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.")
1130
 
1131
+ # API ์„ค์ • ์ดˆ๊ธฐํ™”
1132
+ api_utils.initialize_api_configs()
1133
+ logger.info("===== ์ƒํ’ˆ ์†Œ์‹ฑ ๋ถ„์„ ์‹œ์Šคํ…œ v2.9 (์ถœ๋ ฅ๊ธฐ๋Šฅ ์ถ”๊ฐ€ + ํ•œ๊ตญ์‹œ๊ฐ„ + ๋ฉ€ํ‹ฐ์‚ฌ์šฉ์ž ์•ˆ์ „) ์‹œ์ž‘ =====")
1134
+
1135
+ # ๋„ค์ด๋ฒ„ ๋ฐ์ดํ„ฐ๋žฉ API ์„ค์ • ํ™•์ธ
1136
+ datalab_available = check_datalab_api_config()
1137
+
1138
+ # Gemini API ์„ค์ • ํ™•์ธ
1139
+ gemini_available = check_gemini_api_config()
1140
+
1141
+ # ํ•„์š”ํ•œ ํŒจํ‚ค์ง€ ์•ˆ๋‚ด
1142
+ print("๐Ÿ“ฆ ํ•„์š”ํ•œ ํŒจํ‚ค์ง€:")
1143
+ print(" pip install gradio google-generativeai pandas requests xlsxwriter markdown plotly pytz")
1144
+ print()
1145
+
1146
+ # API ํ‚ค ์„ค์ • ์•ˆ๋‚ด
1147
+ if not gemini_available:
1148
+ print("โš ๏ธ GEMINI_API_KEY ๋˜๋Š” GOOGLE_API_KEY ํ™˜๊ฒฝ๋ณ€์ˆ˜๋ฅผ ์„ค์ •ํ•˜์„ธ์š”.")
1149
+ print(" export GEMINI_API_KEY='your-api-key'")
1150
+ print(" ๋˜๋Š”")
1151
+ print(" export GOOGLE_API_KEY='your-api-key'")
1152
+ print()
1153
+
1154
+ if not datalab_available:
1155
+ print("โš ๏ธ ๋„ค์ด๋ฒ„ ๋ฐ์ดํ„ฐ๋žฉ API ํŠธ๋ Œ๋“œ ๋ถ„์„์„ ์œ„ํ•ด์„œ๋Š”:")
1156
+ print(" 1. ๋„ค์ด๋ฒ„ ๊ฐœ๋ฐœ์ž์„ผํ„ฐ(https://developers.naver.com)์—์„œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋“ฑ๋ก")
1157
+ print(" 2. '๋ฐ์ดํ„ฐ๋žฉ(๊ฒ€์ƒ‰์–ด ํŠธ๋ Œ๋“œ)' API ์ถ”๊ฐ€")
1158
+ print(" 3. ๋ฐœ๊ธ‰๋ฐ›์€ CLIENT_ID์™€ CLIENT_SECRET์„ api_utils.py์˜ NAVER_DATALAB_CONFIGS์— ์„ค์ •")
1159
+ print(" 4. ํ˜„์žฌ๋Š” ํ˜„์žฌ ๊ฒ€์ƒ‰๋Ÿ‰ ์ •๋ณด๋งŒ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.")
1160
+ print()
1161
+ else:
1162
+ print("โœ… ๋ฐ์ดํ„ฐ๋žฉ API ์„ค์ • ์™„๋ฃŒ - 1๋…„, 3๋…„ ํŠธ๋ Œ๋“œ ๋ถ„์„์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค!")
1163
+ print()
1164
+
1165
+ if gemini_available:
1166
+ print("โœ… Gemini API ์„ค์ • ์™„๋ฃŒ - AI ๋ถ„์„์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค!")
1167
+ print()
1168
+
1169
+ print("๐Ÿ›ก๏ธ v2.9 ๋ฉ€ํ‹ฐ์‚ฌ์šฉ์ž ์•ˆ์ „ ๊ฐœ์„ ์‚ฌํ•ญ:")
1170
+ print(" โ€ข ์ „์—ญ ๋ณ€์ˆ˜ export_state ์™„์ „ ์ œ๊ฑฐ")
1171
+ print(" ๏ฟฝ๏ฟฝ๏ฟฝ gr.State({}) ์‚ฌ์šฉ์œผ๋กœ ๊ฐ ์‚ฌ์šฉ์ž๋ณ„ ์„ธ์…˜ ๋ฐ์ดํ„ฐ ์™„์ „ ๋ถ„๋ฆฌ")
1172
+ print(" โ€ข safe_keyword_analysis() ํ•จ์ˆ˜์—์„œ ์„ธ์…˜๋ณ„ ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜")
1173
+ print(" โ€ข export_analysis_results() ํ•จ์ˆ˜์—์„œ ์„ธ์…˜๋ณ„ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ")
1174
+ print(" โ€ข ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ์—์„œ export_data_state ์„ธ์…˜ ์ƒํƒœ ๊ด€๋ฆฌ")
1175
+ print(" โ€ข ํ—ˆ๊น…ํŽ˜์ด์Šค ์ŠคํŽ˜์ด์Šค ๋“ฑ ๋ฉ€ํ‹ฐ์‚ฌ์šฉ์ž ํ™˜๊ฒฝ์—์„œ ์•ˆ์ „ํ•œ ๋™์‹œ ์‚ฌ์šฉ ๋ณด์žฅ")
1176
+ print()
1177
+
1178
+ print("๐Ÿš€ ๊ธฐ์กด v2.9 ๊ธฐ๋Šฅ:")
1179
+ print(" โ€ข ์—ฐ๊ด€๊ฒ€์ƒ‰์–ด ์—‘์…€ ์ถœ๋ ฅ")
1180
+ print(" โ€ข ํ‚ค์›Œ๋“œ ์‹ฌ์ถฉ๋ถ„์„ HTML ์ถœ๋ ฅ")
1181
+ print(" โ€ข ์••์ถ•ํŒŒ์ผ๋กœ ๊ฒฐ๊ณผ ๋‹ค์šด๋กœ๋“œ")
1182
+ print(" โ€ข Gemini API ํ‚ค ํ†ตํ•ฉ ๊ด€๋ฆฌ")
1183
+ print(" โ€ข ํ•œ๊ตญ์‹œ๊ฐ„ ์ ์šฉ")
1184
+ print()
1185
+
1186
  # ์•ฑ ์‹คํ–‰
1187
  app = create_interface()
1188
  app.launch(server_name="0.0.0.0", server_port=7860, share=True)
category_analysis.py ADDED
@@ -0,0 +1,1029 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ์นดํ…Œ๊ณ ๋ฆฌ ๋ถ„์„ ๋ชจ๋“ˆ - ์ƒํ’ˆ์˜ ์นดํ…Œ๊ณ ๋ฆฌ ๋ถ„์„ ๊ธฐ๋Šฅ ์ œ๊ณต (๊ฐœ์„ ๋ฒ„์ „)
3
+ - 1๋…„/3๋…„ ํŠธ๋ Œ๋“œ ๋ชจ๋‘ ๋ถ„์„
4
+ - ๋„ˆ๋น„ 100% ์ ์šฉ
5
+ - 3๋…„ ๊ธฐ์ค€ ์„ฑ์žฅ๋ฅ  ๊ณ„์‚ฐ
6
+ """
7
+
8
+ import pandas as pd
9
+ import time
10
+ import re
11
+ import random
12
+ from collections import Counter, defaultdict
13
+ import text_utils
14
+ import product_search
15
+ import keyword_search
16
+ import logging
17
+
18
+ # ๋กœ๊น… ์„ค์ •
19
+ logger = logging.getLogger(__name__)
20
+ logger.setLevel(logging.INFO)
21
+ formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
22
+ handler = logging.StreamHandler()
23
+ handler.setFormatter(formatter)
24
+ logger.addHandler(handler)
25
+
26
+ # ๋งˆ์ง€๋ง‰ ํ‚ค์›Œ๋“œ ๋ถ„์„ ๊ฒฐ๊ณผ๋ฅผ ์ €์žฅํ•  ์ „์—ญ ๋ณ€์ˆ˜
27
+ _last_keyword_results = []
28
+
29
+ def get_last_keyword_results():
30
+ """๋งˆ์ง€๋ง‰์œผ๋กœ ๋ถ„์„๋œ ํ‚ค์›Œ๋“œ ๊ฒฐ๊ณผ ๋ฐ˜ํ™˜"""
31
+ global _last_keyword_results
32
+ return _last_keyword_results
33
+
34
+ def exponential_backoff_sleep(retry_count, base_delay=0.3, max_delay=5.0):
35
+ """์ง€์ˆ˜ ๋ฐฑ์˜คํ”„ ๋ฐฉ์‹์˜ ๋Œ€๊ธฐ ์‹œ๊ฐ„ ๊ณ„์‚ฐ"""
36
+ delay = min(base_delay * (2 ** retry_count), max_delay)
37
+ # ์•ฝ๊ฐ„์˜ ๋žœ๋ค์„ฑ ์ถ”๊ฐ€ (์ง€ํ„ฐ)
38
+ jitter = random.uniform(0, 0.5) * delay
39
+ time.sleep(delay + jitter)
40
+
41
+ def analyze_product_categories(main_keyword, product_name, category_filter=None):
42
+ """
43
+ ๋ฉ”์ธ ํ‚ค์›Œ๋“œ์™€ ์ƒํ’ˆ๋ช…์œผ๋กœ ์นดํ…Œ๊ณ ๋ฆฌ ๋ถ„์„์„ ์ˆ˜ํ–‰
44
+
45
+ Args:
46
+ main_keyword (str): ๋ฉ”์ธ ๊ฒ€์ƒ‰ ํ‚ค์›Œ๋“œ
47
+ product_name (str): ๋ถ„์„ํ•  ์ƒํ’ˆ๋ช…
48
+ category_filter (str, optional): ์นดํ…Œ๊ณ ๋ฆฌ ํ•„ํ„ฐ
49
+
50
+ Returns:
51
+ dict: ๋ถ„์„ ๊ฒฐ๊ณผ
52
+ """
53
+ logger.info(f"์นดํ…Œ๊ณ ๋ฆฌ ๋ถ„์„ ์‹œ์ž‘: ๋ฉ”์ธ ํ‚ค์›Œ๋“œ={main_keyword}, ์ƒํ’ˆ๋ช…={product_name}")
54
+
55
+ # 1๋‹จ๊ณ„: ๋ฉ”์ธ ํ‚ค์›Œ๋“œ๋กœ 100๊ฐœ ์ƒํ’ˆ ๊ฐ€์ ธ์˜ค๊ธฐ (10๊ฐœ์”ฉ 10ํŽ˜์ด์ง€)
56
+ all_products = []
57
+ for page in range(1, 11):
58
+ result = product_search.fetch_products_by_keyword(main_keyword, page=page, display=10)
59
+ if result["products"]:
60
+ all_products.extend(result["products"])
61
+ exponential_backoff_sleep(0) # API ๋ ˆ์ดํŠธ ๋ฆฌ๋ฐ‹ ๋ฐฉ์ง€
62
+
63
+ if not all_products:
64
+ return {
65
+ "status": "error",
66
+ "message": "์ƒํ’ˆ์„ ๊ฐ€์ ธ์˜ค์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.",
67
+ "main_keyword": main_keyword,
68
+ "product_name": product_name,
69
+ "total_count": 0,
70
+ "products": [],
71
+ "categories": [],
72
+ "analysis": None
73
+ }
74
+
75
+ # 2๋‹จ๊ณ„: ์ƒํ’ˆ๋ช…์—์„œ ํ‚ค์›Œ๋“œ ์ถ”์ถœ (๊ฐœ์„ : ๋” ์ •ํ™•ํ•œ ํ‚ค์›Œ๋“œ ์ถ”์ถœ)
76
+ product_keywords = []
77
+
78
+ # ๊ณต๋ฐฑ๊ณผ ์‰ผํ‘œ๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ๋ถ„๋ฆฌ
79
+ words = re.split(r'[,\s]+', product_name)
80
+ for word in words:
81
+ word = word.strip()
82
+ if word and len(word) >= 2: # ์ตœ์†Œ 2๊ธ€์ž ์ด์ƒ์ธ ๋‹จ์–ด๋งŒ
83
+ # ์ค‘๋ณต ์ œ๊ฑฐ
84
+ if word not in product_keywords:
85
+ product_keywords.append(word)
86
+
87
+ logger.info(f"์ƒํ’ˆ๋ช…์—์„œ ์ถ”์ถœํ•œ ํ‚ค์›Œ๋“œ: {product_keywords}")
88
+
89
+ # 3๋‹จ๊ณ„: ์ƒํ’ˆ ์นดํ…Œ๊ณ ๋ฆฌ ๋ถ„์„
90
+ category_counter = Counter()
91
+ products_by_category = defaultdict(list)
92
+
93
+ for product in all_products:
94
+ category = product["์นดํ…Œ๊ณ ๋ฆฌ"]
95
+ category_counter[category] += 1
96
+ products_by_category[category].append(product)
97
+
98
+ # ์นดํ…Œ๊ณ ๋ฆฌ ํ•„ํ„ฐ ์ ์šฉ
99
+ if category_filter and category_filter != "์ „์ฒด ๋ณด๊ธฐ":
100
+ # ์นดํ…Œ๊ณ ๋ฆฌ์—์„œ ๊ด„ํ˜ธ ๋ถ€๋ถ„ ์ œ๊ฑฐ
101
+ category_filter_clean = category_filter.split(" (")[0] if " (" in category_filter else category_filter
102
+
103
+ filtered_categories = {}
104
+ for cat, count in category_counter.items():
105
+ # ์ƒํ’ˆ ์นดํ…Œ๊ณ ๋ฆฌ์—์„œ๋„ ๊ด„ํ˜ธ ์žˆ์œผ๋ฉด ์ œ๊ฑฐ
106
+ cat_clean = cat
107
+ if " (" in cat_clean:
108
+ cat_clean = cat_clean.split(" (")[0]
109
+
110
+ if category_filter_clean.lower() in cat_clean.lower():
111
+ filtered_categories[cat] = count
112
+
113
+ category_counter = Counter(filtered_categories)
114
+
115
+ # 4๋‹จ๊ณ„: ํ‚ค์›Œ๋“œ ๊ฒ€์ƒ‰๋Ÿ‰ ์กฐํšŒ
116
+ all_keywords = [main_keyword] + product_keywords
117
+ search_volumes = keyword_search.fetch_all_search_volumes(all_keywords)
118
+
119
+ # 5๋‹จ๊ณ„: ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ๋งค์นญ ์ƒํƒœ ๋ถ„์„
120
+ category_matching = []
121
+
122
+ # ์ •๋ ฌ๋œ ์นดํ…Œ๊ณ ๋ฆฌ ๋ชฉ๋ก (์ถœํ˜„ ๋นˆ๋„์ˆœ)
123
+ sorted_categories = [cat for cat, _ in category_counter.most_common()]
124
+
125
+ for category in sorted_categories:
126
+ products_in_category = products_by_category[category]
127
+ count = len(products_in_category)
128
+
129
+ # ์ด ์นดํ…Œ๊ณ ๋ฆฌ์— ์†ํ•œ ์ƒํ’ˆ๋“ค ์ค‘ 10๊ฐœ๋งŒ ๊ฐ€์ ธ์˜ด
130
+ sample_products = products_in_category[:100]
131
+
132
+ category_matching.append({
133
+ "์นดํ…Œ๊ณ ๋ฆฌ": category,
134
+ "์ƒํ’ˆ์ˆ˜": count,
135
+ "๋งค์นญ์ƒํ’ˆ": sample_products
136
+ })
137
+
138
+ # 6๋‹จ๊ณ„: ๊ฒ€์ƒ‰๋Ÿ‰ ์ •๋ณด ์ถ”๊ฐ€ ๋ฐ ๊ฒฐ๊ณผ ์ •๋ฆฌ
139
+ keyword_info = []
140
+ for kw in all_keywords:
141
+ volume = search_volumes.get(kw, {"PC๊ฒ€์ƒ‰๋Ÿ‰": 0, "๋ชจ๋ฐ”์ผ๊ฒ€์ƒ‰๋Ÿ‰": 0, "์ด๊ฒ€์ƒ‰๋Ÿ‰": 0})
142
+ keyword_info.append({
143
+ "ํ‚ค์›Œ๋“œ": kw,
144
+ "PC๊ฒ€์ƒ‰๋Ÿ‰": volume.get("PC๊ฒ€์ƒ‰๋Ÿ‰", 0),
145
+ "๋ชจ๋ฐ”์ผ๊ฒ€์ƒ‰๋Ÿ‰": volume.get("๋ชจ๋ฐ”์ผ๊ฒ€์ƒ‰๋Ÿ‰", 0),
146
+ "์ด๊ฒ€์ƒ‰๋Ÿ‰": volume.get("์ด๊ฒ€์ƒ‰๋Ÿ‰", 0),
147
+ "๊ฒ€์ƒ‰๋Ÿ‰๊ตฌ๊ฐ„": text_utils.get_search_volume_range(volume.get("์ด๊ฒ€์ƒ‰๋Ÿ‰", 0))
148
+ })
149
+
150
+ # ๊ฒฐ๊ณผ ๋ฐ˜ํ™˜
151
+ return {
152
+ "status": "success",
153
+ "message": "๋ถ„์„์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.",
154
+ "main_keyword": main_keyword,
155
+ "product_name": product_name,
156
+ "total_count": len(all_products),
157
+ "products": all_products,
158
+ "categories": sorted_categories,
159
+ "category_counter": dict(category_counter),
160
+ "category_matching": category_matching,
161
+ "keyword_info": keyword_info
162
+ }
163
+
164
+ def analyze_keywords_by_category(keywords, selected_category, df_all=None):
165
+ """
166
+ ์ž…๋ ฅ๋œ ํ‚ค์›Œ๋“œ ๋ชฉ๋ก๊ณผ ์„ ํƒ๋œ ์นดํ…Œ๊ณ ๋ฆฌ๋กœ ๋ถ„์„์„ ์ˆ˜ํ–‰ํ•˜๋Š” ํ•จ์ˆ˜
167
+ """
168
+ import re
169
+
170
+ if not keywords or not selected_category:
171
+ return "ํ‚ค์›Œ๋“œ์™€ ์นดํ…Œ๊ณ ๋ฆฌ๋ฅผ ๋ชจ๋‘ ์„ ํƒํ•ด์ฃผ์„ธ์š”."
172
+
173
+ # ์นดํ…Œ๊ณ ๋ฆฌ์—์„œ ์นด์šดํŠธ ์ •๋ณด ์ œ๊ฑฐ (์˜ˆ: "ํŒจ์…˜์˜๋ฅ˜ (10)" -> "ํŒจ์…˜์˜๋ฅ˜")
174
+ selected_category_clean = selected_category
175
+ is_overall_view = False # '์ „์ฒด ๋ณด๊ธฐ'์ธ์ง€ ์—ฌ๋ถ€ ํ”Œ๋ž˜๊ทธ
176
+ if " (" in selected_category and selected_category != "์ „์ฒด ๋ณด๊ธฐ":
177
+ selected_category_clean = selected_category.split(" (")[0]
178
+ elif selected_category == "์ „์ฒด ๋ณด๊ธฐ":
179
+ selected_category_clean = "" # ์ „์ฒด ์นดํ…Œ๊ณ ๋ฆฌ ๋ถ„์„์šฉ
180
+ is_overall_view = True
181
+
182
+ # ํ‚ค์›Œ๋“œ ๋ฆฌ์ŠคํŠธ ์ฒ˜๋ฆฌ (์ตœ๋Œ€ 20๊ฐœ)
183
+ if isinstance(keywords, str):
184
+ # ์‰ผํ‘œ๋‚˜ ์—”ํ„ฐ๋กœ ๋ถ„๋ฆฌ
185
+ keywords_list = [k.strip() for k in re.split(r'[,\n]+', keywords) if k.strip()]
186
+ # 20๊ฐœ๋กœ ์ œํ•œ
187
+ keywords_list = keywords_list[:20]
188
+ else:
189
+ keywords_list = keywords[:20]
190
+
191
+ if not keywords_list:
192
+ return "๋ถ„์„ํ•  ํ‚ค์›Œ๋“œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค."
193
+
194
+ logger.info(f"์นดํ…Œ๊ณ ๋ฆฌ ๋ถ„์„ ์‹œ์ž‘: {len(keywords_list)}๊ฐœ ํ‚ค์›Œ๋“œ, ์„ ํƒ ์นดํ…Œ๊ณ ๋ฆฌ: '{selected_category_clean if not is_overall_view else '์ „์ฒด ๋ณด๊ธฐ'}'")
195
+
196
+ # ๊ฐœ์„ ๋œ HTML ๊ฒฐ๊ณผ - ๋„ˆ๋น„ 100% ์ ์šฉ
197
+ result_html = f'''
198
+ <style>
199
+ .result-container {{
200
+ width: 100%;
201
+ margin-top: 20px;
202
+ padding: 15px;
203
+ border-radius: 8px;
204
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
205
+ background-color: #f9f9f9;
206
+ }}
207
+
208
+ .result-header {{
209
+ font-size: 18px;
210
+ font-weight: bold;
211
+ margin-bottom: 15px;
212
+ color: #009879;
213
+ border-bottom: 2px solid #009879;
214
+ padding-bottom: 5px;
215
+ }}
216
+
217
+ .keyword-tags {{
218
+ display: flex;
219
+ flex-wrap: wrap;
220
+ gap: 10px;
221
+ margin-bottom: 20px;
222
+ width: 100%;
223
+ }}
224
+
225
+ .keyword-tag {{
226
+ display: inline-block;
227
+ background-color: #009879;
228
+ color: white;
229
+ padding: 8px 15px;
230
+ border-radius: 20px;
231
+ font-size: 14px;
232
+ box-shadow: 0 2px 5px rgba(0,0,0,0.1);
233
+ transition: transform 0.2s;
234
+ }}
235
+
236
+ .keyword-tag:hover {{
237
+ transform: translateY(-2px);
238
+ box-shadow: 0 4px 8px rgba(0,0,0,0.15);
239
+ }}
240
+
241
+ .category-container {{
242
+ width: 100%;
243
+ margin-bottom: 20px;
244
+ padding: 10px 15px;
245
+ background-color: #f0f8ff;
246
+ border-left: 4px solid #2c7fb8;
247
+ border-radius: 4px;
248
+ }}
249
+
250
+ .category-title {{
251
+ font-weight: bold;
252
+ margin-bottom: 5px;
253
+ color: #2c7fb8;
254
+ }}
255
+
256
+ .category-path {{
257
+ font-size: 16px;
258
+ color: #333;
259
+ word-break: break-word;
260
+ }}
261
+
262
+ .analysis-result {{
263
+ width: 100%;
264
+ margin-top: 30px;
265
+ border: 1px solid #ddd;
266
+ border-radius: 5px;
267
+ padding: 15px;
268
+ background-color: #ffffff;
269
+ }}
270
+
271
+ .result-header-analysis {{
272
+ font-weight: bold;
273
+ margin-bottom: 15px;
274
+ color: #009879;
275
+ font-size: 16px;
276
+ }}
277
+
278
+ .match-item {{
279
+ width: 100%;
280
+ margin: 12px 0;
281
+ padding: 8px 12px;
282
+ border-bottom: 1px solid #eee;
283
+ transition: background-color 0.2s;
284
+ display: grid;
285
+ grid-template-columns: 3fr 1fr 4fr;
286
+ gap: 10px;
287
+ }}
288
+
289
+ .match-item:hover {{
290
+ background-color: #f5f5f5;
291
+ }}
292
+
293
+ .match-keyword {{
294
+ font-weight: bold;
295
+ color: #2c7fb8;
296
+ font-size: 15px;
297
+ }}
298
+
299
+ .match-count {{
300
+ display: inline-block;
301
+ background-color: #009879;
302
+ color: white;
303
+ padding: 3px 10px;
304
+ border-radius: 15px;
305
+ font-size: 13px;
306
+ margin-left: 10px;
307
+ }}
308
+
309
+ .match-status {{
310
+ text-align: center;
311
+ font-weight: bold;
312
+ color: #009879;
313
+ }}
314
+
315
+ .match-categories {{
316
+ color: #555;
317
+ font-size: 14px;
318
+ line-height: 1.4;
319
+ }}
320
+
321
+ .match-header {{
322
+ width: 100%;
323
+ display: grid;
324
+ grid-template-columns: 3fr 1fr 4fr;
325
+ gap: 10px;
326
+ padding: 10px 12px;
327
+ background-color: #e7f7f3;
328
+ border-radius: 5px 5px 0 0;
329
+ font-weight: bold;
330
+ margin-bottom: 10px;
331
+ }}
332
+ </style>
333
+
334
+ <div class="result-container">
335
+ <div class="result-header">๋ถ„์„ ๊ฒฐ๊ณผ ์š”์•ฝ</div>
336
+
337
+ <div class="keyword-tags">
338
+ <div class="category-title">๋ถ„์„ ํ‚ค์›Œ๋“œ ({len(keywords_list)}๊ฐœ)</div>
339
+ <div class="keyword-tags">
340
+ {''.join([f'<span class="keyword-tag">{k}</span>' for k in keywords_list])}
341
+ </div>
342
+ </div>
343
+
344
+ <div class="category-container">
345
+ <div class="category-title">์„ ํƒ๋œ ์นดํ…Œ๊ณ ๋ฆฌ</div>
346
+ <div class="category-path">{selected_category}</div>
347
+ </div>
348
+
349
+ <div class="analysis-result">
350
+ <div class="result-header-analysis">์นดํ…Œ๊ณ ๋ฆฌ ์ผ์น˜ ๋ถ„์„ ๊ฒฐ๊ณผ</div>
351
+
352
+ <div class="match-header">
353
+ <div>ํ‚ค์›Œ๋“œ</div>
354
+ <div>๋ถ„์„ ํ‚ค์›Œ๋“œ</div>
355
+ <div>๋งค์นญ๋œ ์นดํ…Œ๊ณ ๋ฆฌ</div>
356
+ </div>
357
+ '''
358
+
359
+ # ํ‚ค์›Œ๋“œ ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ์ค€๋น„ (5๊ฐœ์”ฉ ๋ฌถ์Œ)
360
+ batch_size = 5
361
+ batches = []
362
+ for i in range(0, len(keywords_list), batch_size):
363
+ batches.append(keywords_list[i:i + batch_size])
364
+
365
+ logger.info(f"์ด {len(batches)}๊ฐœ ๋ฐฐ์น˜๋กœ {len(keywords_list)}๊ฐœ ํ‚ค์›Œ๋“œ ์ฒ˜๋ฆฌ")
366
+
367
+ # ๊ฐ ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ
368
+ match_results = {} # ๊ฐ ํ‚ค์›Œ๋“œ๋ณ„ ๋งค์นญ ์ •๋ณด ์ €์žฅ (match_count, total_count)
369
+ batch_categories_info = {} # ๊ฐ ํ‚ค์›Œ๋“œ๋ณ„๋กœ ์ถ”์ถœ๋œ ์นดํ…Œ๊ณ ๋ฆฌ ๋ชฉ๋ก ๋ฐ ๋น„์œจ ์ €์žฅ
370
+
371
+ for batch_idx, batch in enumerate(batches):
372
+ logger.info(f"๋ฐฐ์น˜ {batch_idx+1}/{len(batches)} ์ฒ˜๋ฆฌ ์ค‘...")
373
+
374
+ # ๊ฐ ํ‚ค์›Œ๋“œ ์ฒ˜๋ฆฌ
375
+ for keyword in batch:
376
+ max_retries = 3
377
+ retry_count = 0
378
+ api_keyword = keyword.replace(" ", "") # ๊ณต๋ฐฑ ์ œ๊ฑฐ
379
+
380
+ current_keyword_match_count = 0
381
+ current_keyword_total_products = 0
382
+ current_keyword_categories_found = [] # ๋น„์œจ๊ณผ ํ•จ๊ป˜ ์ €์žฅ๋  ์นดํ…Œ๊ณ ๋ฆฌ ๋ฌธ์ž์—ด ๋ฆฌ์ŠคํŠธ
383
+
384
+ while retry_count < max_retries:
385
+ try:
386
+ # ๋„ค์ด๋ฒ„ API ํ˜ธ์ถœ
387
+ products = product_search.fetch_naver_shopping_data_for_analysis(api_keyword, count=100) # ์ƒ์œ„ 10๊ฐœ ์ƒํ’ˆ
388
+
389
+ if products:
390
+ current_keyword_total_products = len(products)
391
+ categories_counter_for_keyword = Counter() # ํ˜„์žฌ ํ‚ค์›Œ๋“œ์˜ ์ƒํ’ˆ๋“ค ์นดํ…Œ๊ณ ๋ฆฌ ๋ถ„ํฌ
392
+
393
+ for product in products:
394
+ product_category_full = product.get("category", "") or product.get("์นดํ…Œ๊ณ ๋ฆฌ", "")
395
+ if product_category_full:
396
+ categories_counter_for_keyword[product_category_full] += 1
397
+
398
+ # ์‹ค์ œ ๋งค์นญ ์—ฌ๋ถ€ ์นด์šดํŠธ (is_overall_view๊ฐ€ ์•„๋‹ ๋•Œ๋งŒ)
399
+ if not is_overall_view and selected_category_clean:
400
+ product_category_for_match = product_category_full
401
+ if " (" in product_category_for_match: # ์ƒํ’ˆ ์นดํ…Œ๊ณ ๋ฆฌ ์ด๋ฆ„์—์„œ count ์ œ๊ฑฐ
402
+ product_category_for_match = product_category_for_match.split(" (")[0]
403
+
404
+ sel_lower = selected_category_clean.lower()
405
+ prod_lower = product_category_for_match.lower()
406
+
407
+ if sel_lower in prod_lower or prod_lower in sel_lower:
408
+ current_keyword_match_count += 1
409
+
410
+ if is_overall_view: # '์ „์ฒด ๋ณด๊ธฐ'์ผ ๊ฒฝ์šฐ, ๋ชจ๋“  ์ƒํ’ˆ์ด ๋งค์นญ๋œ ๊ฒƒ์œผ๋กœ ๊ฐ„์ฃผ
411
+ current_keyword_match_count = current_keyword_total_products
412
+
413
+ # ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ๋น„์œจ ๊ณ„์‚ฐ ๋ฐ ์ €์žฅ
414
+ category_percentages = []
415
+ for cat, count in categories_counter_for_keyword.most_common(): # ๋นˆ๋„ ๋†’์€ ์ˆœ์œผ๋กœ
416
+ percentage = (count / current_keyword_total_products) * 100 if current_keyword_total_products > 0 else 0
417
+ category_percentages.append((cat, percentage))
418
+
419
+ # category_percentages.sort(key=lambda x: x[1], reverse=True) # ์ด๋ฏธ most_common์œผ๋กœ ์ •๋ ฌ๋จ
420
+
421
+ for cat, percentage in category_percentages:
422
+ current_keyword_categories_found.append(f"{cat} ({percentage:.0f}%)")
423
+
424
+ logger.info(f" - '{keyword}' ์ฒ˜๋ฆฌ ์™„๋ฃŒ: {current_keyword_match_count}/{current_keyword_total_products} ์ผ์น˜")
425
+ break # ์„ฑ๊ณตํ–ˆ์œผ๋ฏ€๋กœ ์žฌ์‹œ๋„ ๋ฃจํ”„ ์ข…๋ฃŒ
426
+ else:
427
+ logger.warning(f" - '{keyword}' API ๊ฒฐ๊ณผ ์—†์Œ (์‹œ๋„ {retry_count+1}/{max_retries})")
428
+ retry_count += 1
429
+ exponential_backoff_sleep(retry_count)
430
+
431
+ except Exception as e:
432
+ logger.error(f" - '{keyword}' ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜: {e} (์‹œ๋„ {retry_count+1}/{max_retries})")
433
+ retry_count += 1
434
+ exponential_backoff_sleep(retry_count)
435
+
436
+ # ๊ฒฐ๊ณผ ์ €์žฅ
437
+ if retry_count >= max_retries and current_keyword_total_products == 0: # ์ตœ์ข… ์‹คํŒจ
438
+ match_results[keyword] = {
439
+ "match_count": 0,
440
+ "total_count": 0,
441
+ "error": True
442
+ }
443
+ batch_categories_info[keyword] = ["์˜ค๋ฅ˜ ๋ฐœ์ƒ"]
444
+ logger.error(f" - '{keyword}' ์ตœ๋Œ€ ์žฌ์‹œ๋„ ํ›„ ์‹คํŒจ")
445
+ else:
446
+ match_results[keyword] = {
447
+ "match_count": current_keyword_match_count,
448
+ "total_count": current_keyword_total_products,
449
+ "error": False
450
+ }
451
+ batch_categories_info[keyword] = current_keyword_categories_found if current_keyword_categories_found else ["์นดํ…Œ๊ณ ๋ฆฌ ์ •๋ณด ์—†์Œ"]
452
+
453
+ # API ๋ ˆ์ดํŠธ ๋ฆฌ๋ฐ‹ ๋ฐฉ์ง€ - ์ง€์ˆ˜ ๋ฐฑ์˜คํ”„ ์‚ฌ์šฉ
454
+ exponential_backoff_sleep(0)
455
+
456
+ logger.info(f"์ „์ฒด {len(keywords_list)}๊ฐœ ํ‚ค์›Œ๋“œ ์ค‘ {len(match_results)}๊ฐœ ์ฒ˜๋ฆฌ ์™„๋ฃŒ")
457
+
458
+ # ๊ฒฐ๊ณผ๋ฅผ HTML๋กœ ๋ณ€ํ™˜
459
+ for keyword in keywords_list:
460
+ result = match_results.get(keyword, {"match_count": 0, "total_count": 0, "error": True})
461
+
462
+ # ์ˆ˜์ •๋œ ๋ถ€๋ถ„: keyword_status ๊ฒฐ์ • ๋กœ์ง ๋ณ€๊ฒฝ
463
+ # ์„ ํƒ๋œ ์นดํ…Œ๊ณ ๋ฆฌ์™€ ํ•˜๋‚˜๋ผ๋„ ๋งค์นญ๋˜๋ฉด "O", ์•„๋‹ˆ๋ฉด "X"
464
+ # "์ „์ฒด ๋ณด๊ธฐ" ์„ ํƒ ์‹œ์—๋Š” ํ•ญ์ƒ "O" (๋ชจ๋“  ์ƒํ’ˆ์ด ๋งค์นญ๋œ ๊ฒƒ์œผ๋กœ ๊ฐ„์ฃผํ–ˆ์œผ๋ฏ€๋กœ)
465
+ if result.get("error"):
466
+ keyword_status = "์˜ค๋ฅ˜" # ์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ
467
+ status_color = "red"
468
+ elif is_overall_view: # '์ „์ฒด ๋ณด๊ธฐ'์˜ ๊ฒฝ์šฐ
469
+ keyword_status = "O"
470
+ status_color = "#009879" # Green
471
+ else: # ํŠน์ • ์นดํ…Œ๊ณ ๋ฆฌ ์„ ํƒ ์‹œ
472
+ keyword_status = "O" if result["match_count"] > 0 else "X"
473
+ status_color = "#009879" if keyword_status == "O" else "red"
474
+
475
+
476
+ # ๋งค์นญ๋œ ์นดํ…Œ๊ณ ๋ฆฌ ์ •๋ณด
477
+ categories_html_list = batch_categories_info.get(keyword, ["์ •๋ณด ์—†์Œ"])
478
+ categories_html = "<br>".join(categories_html_list)
479
+
480
+ if result.get("error", False):
481
+ result_html += f'''
482
+ <div class="match-item">
483
+ <div class="match-keyword">{keyword}</div>
484
+ <div class="match-status" style="color:{status_color}; font-weight:bold;">{keyword_status}</div>
485
+ <div class="match-categories">๋ถ„์„ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ</div>
486
+ </div>
487
+ '''
488
+ else:
489
+ match_count_display = result["match_count"]
490
+ total_count_display = result["total_count"]
491
+
492
+ result_html += f'''
493
+ <div class="match-item">
494
+ <div class="match-keyword">{keyword}<span class="match-count">{match_count_display}/{total_count_display}</span></div>
495
+ <div class="match-status" style="color:{status_color}; font-weight:bold;">{keyword_status}</div>
496
+ <div class="match-categories">{categories_html}</div>
497
+ </div>
498
+ '''
499
+
500
+ result_html += '</div></div></div>' # .analysis-result, .result-container ๋‹ซ๊ธฐ
501
+
502
+ return result_html
503
+
504
+ def analyze_product_terms(product_name, main_keyword=""):
505
+ """
506
+ ์ƒํ’ˆ๋ช…์—์„œ ์ถ”์ถœํ•œ ํ‚ค์›Œ๋“œ๋“ค์„ ๋ถ„์„ํ•˜์—ฌ ์นดํ…Œ๊ณ ๋ฆฌ ํ•ญ๋ชฉ ์ œ๊ณต (1๋…„, 3๋…„ ํŠธ๋ Œ๋“œ ๋ชจ๋‘ ๋ถ„์„)
507
+
508
+ Args:
509
+ product_name (str): ๋ถ„์„ํ•  ์ƒํ’ˆ๋ช…
510
+ main_keyword (str): ๋ฉ”์ธ ํ‚ค์›Œ๋“œ (optional)
511
+
512
+ Returns:
513
+ tuple: (HTML ํ˜•์‹์˜ ๊ฒฐ๊ณผ ํ…Œ์ด๋ธ”, ํ‚ค์›Œ๋“œ ๋ถ„์„ ๊ฒฐ๊ณผ ๋ฆฌ์ŠคํŠธ, ํŠธ๋ Œ๋“œ ๋ถ„์„ ๊ฒฐ๊ณผ)
514
+ """
515
+ global _last_keyword_results # ํ•จ์ˆ˜ ์‹œ์ž‘ ๋ถ€๋ถ„์— global ์„ ์–ธ
516
+
517
+ # ์ „์ฒ˜๋ฆฌ: ์ƒํ’ˆ๋ช… ์•ž๋’ค ๊ณต๋ฐฑ ์ œ๊ฑฐ ๋ฐ ์œ ํšจ์„ฑ ํ™•์ธ
518
+ product_name = product_name.strip() if product_name else ""
519
+ if not product_name:
520
+ return "์ƒํ’ˆ๋ช…์ด ๋น„์–ด์žˆ์Šต๋‹ˆ๋‹ค. ์œ ํšจํ•œ ์ƒํ’ˆ๋ช…์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.", [], None
521
+
522
+ # ๋””๋ฒ„๊น…์šฉ ๋กœ๊ทธ
523
+ logger.info(f"๋ถ„์„ ์‹œ์ž‘ - ์ƒํ’ˆ๋ช…: '{product_name}', ๋ฉ”์ธ ํ‚ค์›Œ๋“œ: '{main_keyword}'")
524
+
525
+ # ์ƒํ’ˆ๋ช…์—์„œ ํ‚ค์›Œ๋“œ๋ฅผ ๋” ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ๋ถ„๋ฆฌ (๊ณต๋ฐฑ๊ณผ ์‰ผํ‘œ ๊ธฐ์ค€์œผ๋กœ ๋ถ„๋ฆฌ)
526
+ # ์ˆ˜์ •๋œ ๋ถ€๋ถ„: ์ •๊ทœํ‘œํ˜„์‹ ํŒจํ„ด ์กฐ์ • ๋ฐ ์˜ˆ์™ธ์ฒ˜๋ฆฌ ์ถ”๊ฐ€
527
+ try:
528
+ words = []
529
+ # ๋จผ์ € ์‰ผํ‘œ๋กœ ๋ถ„๋ฆฌ
530
+ comma_parts = product_name.split(',')
531
+
532
+ for part in comma_parts:
533
+ # ๊ฐ ๋ถ€๋ถ„์„ ๊ณต๋ฐฑ์œผ๋กœ ๋ถ„๋ฆฌ
534
+ space_parts = part.split()
535
+ words.extend([word.strip() for word in space_parts if word.strip()])
536
+
537
+ # ์ค‘๋ณต ์ œ๊ฑฐ ๋ฐ 1๊ธ€์ž ์ด์ƒ ํ‚ค์›Œ๋“œ๋งŒ ์œ ์ง€
538
+ words = list(set([word for word in words if len(word) >= 1]))
539
+
540
+ logger.info(f"์ƒํ’ˆ๋ช…์—์„œ ์ถ”์ถœํ•œ ์›๋ณธ ํ‚ค์›Œ๋“œ (์ด {len(words)}๊ฐœ): {words}")
541
+
542
+ # ํ‚ค์›Œ๋“œ๊ฐ€ ํ•˜๋‚˜๋„ ์ถ”์ถœ๋˜์ง€ ์•Š์•˜๋‹ค๋ฉด ์›๋ณธ ์ƒํ’ˆ๋ช… ์‚ฌ์šฉ
543
+ if not words:
544
+ words = [product_name]
545
+ logger.warning(f"ํ‚ค์›Œ๋“œ ์ถ”์ถœ ์‹คํŒจ, ์›๋ณธ ์ƒํ’ˆ๋ช… ์‚ฌ์šฉ: '{product_name}'")
546
+ except Exception as e:
547
+ logger.error(f"ํ‚ค์›Œ๋“œ ์ถ”์ถœ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}")
548
+ # ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ ์›๋ณธ ์ƒํ’ˆ๋ช…์„ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉ
549
+ words = [product_name]
550
+
551
+ # ๋ฉ”์ธ ํ‚ค์›Œ๋“œ ์ฒ˜๋ฆฌ
552
+ if not main_keyword:
553
+ # ๋ฉ”์ธ ํ‚ค์›Œ๋“œ๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ, ์˜ค์ง•์–ด ๊ด€๋ จ ํ‚ค์›Œ๋“œ ์ฐพ๊ธฐ (๊ธฐ์กด ๋กœ์ง)
554
+ for word in words:
555
+ if "์˜ค์ง•์–ด" in word:
556
+ main_keyword = "์˜ค์ง•์–ด"
557
+ break
558
+
559
+ # ์›๋ณธ ํ‚ค์›Œ๋“œ ๋ชฉ๋ก ์ €์žฅ
560
+ keywords = []
561
+ for word in words:
562
+ # ์ˆซ์ž, ์˜๋ฌธ ๋“ฑ์„ ํฌํ•จํ•œ ๋ชจ๋“  ๋‹จ์–ด ํ—ˆ์šฉ
563
+ if word and word != main_keyword:
564
+ # ๋ฉ”์ธ ํ‚ค์›Œ๋“œ๊ฐ€ ์žˆ๊ณ , ๋‹จ์–ด์— ๋ฉ”์ธ ํ‚ค์›Œ๋“œ๊ฐ€ ์—†์œผ๋ฉด ์กฐํ•ฉ
565
+ if main_keyword and main_keyword not in word:
566
+ # ์กฐํ•ฉ ํ‚ค์›Œ๋“œ ์ƒ์„ฑ (์ž์—ฐ์Šค๋Ÿฌ์šด ํ˜•ํƒœ๋กœ)
567
+ combined = f"{word} {main_keyword}"
568
+ if combined not in keywords:
569
+ keywords.append(combined)
570
+ # ์›๋ž˜ ํ‚ค์›Œ๋“œ๋„ ๋”ฐ๋กœ ์ถ”๊ฐ€ (๊ฐœ์„ : ๋‹จ์ผ ํ‚ค์›Œ๋“œ๋„ ์œ ์ง€)
571
+ if word not in keywords:
572
+ keywords.append(word)
573
+
574
+ # ๋ฉ”์ธ ํ‚ค์›Œ๋“œ๋„ ๋‹จ๋…์œผ๋กœ ์ถ”๊ฐ€
575
+ if main_keyword and main_keyword not in keywords:
576
+ keywords.append(main_keyword)
577
+
578
+ if not keywords:
579
+ return "์ƒํ’ˆ๋ช…์—์„œ ํ‚ค์›Œ๋“œ๋ฅผ ์ถ”์ถœํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.", [], None
580
+
581
+ logger.info(f"๋ถ„์„ํ•  ์ตœ์ข… ํ‚ค์›Œ๋“œ ๋ชฉ๋ก (์ด {len(keywords)}๊ฐœ): {keywords}")
582
+
583
+ # ์ถ”์ถœ๋œ ํ‚ค์›Œ๋“œ๋ฅผ ๋ฐฐ์น˜๋กœ ๋‚˜๋ˆ„๊ธฐ (๋ฐฐ์น˜๋‹น 5๊ฐœ์”ฉ)
584
+ batch_size = 5
585
+ keyword_batches = []
586
+ for i in range(0, len(keywords), batch_size):
587
+ keyword_batches.append(keywords[i:i + batch_size])
588
+
589
+ logger.info(f"์ด {len(keyword_batches)}๊ฐœ ๋ฐฐ์น˜๋กœ {len(keywords)}๊ฐœ ํ‚ค์›Œ๋“œ ์ฒ˜๋ฆฌ")
590
+
591
+ # ํ‚ค์›Œ๋“œ ๋ถ„์„ ๊ฒฐ๊ณผ ์ €์žฅ
592
+ keyword_results = []
593
+
594
+ # ๊ฐ ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ
595
+ for batch_idx, batch in enumerate(keyword_batches):
596
+ logger.info(f"๋ฐฐ์น˜ {batch_idx+1}/{len(keyword_batches)} ์ฒ˜๋ฆฌ ์ค‘...")
597
+
598
+ # ์ƒํ’ˆ ๊ฒ€์ƒ‰ ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ
599
+ batch_products = {}
600
+ for keyword in batch:
601
+ # API ํ˜ธ์ถœ์šฉ ํ‚ค์›Œ๋“œ (๊ณต๋ฐฑ ์ œ๊ฑฐ)
602
+ api_keyword = keyword.replace(" ", "")
603
+
604
+ # ์ตœ๋Œ€ 3๋ฒˆ ์žฌ์‹œ๋„
605
+ max_retries = 3
606
+ retry_count = 0
607
+
608
+ while retry_count < max_retries:
609
+ try:
610
+ # ํ‚ค์›Œ๋“œ๋กœ ์ƒํ’ˆ ๊ฒ€์ƒ‰
611
+ products = product_search.fetch_naver_shopping_data_for_analysis(api_keyword, count=100)
612
+
613
+ if products:
614
+ batch_products[keyword] = products
615
+ logger.info(f" - '{keyword}' ์ƒํ’ˆ ๊ฒ€์ƒ‰ ์„ฑ๊ณต: {len(products)}๊ฐœ")
616
+ break # ์„ฑ๊ณตํ–ˆ์œผ๋ฏ€๋กœ ๋ฃจํ”„ ์ข…๋ฃŒ
617
+ else:
618
+ logger.warning(f" - '{keyword}' ์ƒํ’ˆ ์—†์Œ (์‹œ๋„ {retry_count+1}/{max_retries})")
619
+ retry_count += 1
620
+ exponential_backoff_sleep(retry_count)
621
+ except Exception as e:
622
+ logger.error(f" - '{keyword}' ์ƒํ’ˆ ๊ฒ€์ƒ‰ ์ค‘ ์˜ค๋ฅ˜: {e} (์‹œ๋„ {retry_count+1}/{max_retries})")
623
+ retry_count += 1
624
+ exponential_backoff_sleep(retry_count)
625
+
626
+ # ์ตœ๋Œ€ ์žฌ์‹œ๋„ ํ›„์—๋„ ์‹คํŒจํ•œ ๊ฒฝ์šฐ ๋กœ๊ทธ ๊ธฐ๋ก
627
+ if retry_count >= max_retries and keyword not in batch_products:
628
+ logger.error(f" - '{keyword}' ์ตœ๋Œ€ ์žฌ์‹œ๋„ ํ›„ ์‹คํŒจ")
629
+
630
+ # ๊ฒ€์ƒ‰๋Ÿ‰ ์กฐํšŒ ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ
631
+ api_keywords = [kw.replace(" ", "") for kw in batch]
632
+ volumes = keyword_search.fetch_all_search_volumes(api_keywords)
633
+
634
+ # ๊ฐ ํ‚ค์›Œ๋“œ ์ฒ˜๋ฆฌ
635
+ for keyword in batch:
636
+ if keyword in batch_products:
637
+ products = batch_products[keyword]
638
+
639
+ # ๊ฐœ์„ : ์นดํ…Œ๊ณ ๋ฆฌ ํ•ญ๋ชฉ๊ณผ ํ•จ๊ป˜ ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ์ ์œ ์œจ ๊ณ„์‚ฐ
640
+ category_counter = Counter()
641
+ for product in products:
642
+ category = product.get("category", "") or product.get("์นดํ…Œ๊ณ ๋ฆฌ", "")
643
+ if category:
644
+ category_counter[category] += 1
645
+
646
+ # ์นดํ…Œ๊ณ ๋ฆฌ์™€ ์ ์œ ์œจ ๊ณ„์‚ฐ
647
+ total_products = len(products)
648
+ categories_with_percentage = []
649
+ for category, count in category_counter.most_common():
650
+ percentage = (count / total_products) * 100 if total_products > 0 else 0
651
+ categories_with_percentage.append(f"{category}({percentage:.0f}%)")
652
+
653
+ # ๊ฒ€์ƒ‰๋Ÿ‰ ์กฐํšŒ (API ํ˜ธ์ถœ์šฉ ํ‚ค์›Œ๋“œ ์‚ฌ์šฉ)
654
+ api_keyword = keyword.replace(" ", "")
655
+ volume_data = volumes.get(api_keyword, {"PC๊ฒ€์ƒ‰๋Ÿ‰": 0, "๋ชจ๋ฐ”์ผ๊ฒ€์ƒ‰๋Ÿ‰": 0, "์ด๊ฒ€์ƒ‰๋Ÿ‰": 0})
656
+
657
+ # ๊ฒฐ๊ณผ ์ €์žฅ (์นดํ…Œ๊ณ ๋ฆฌ ํ•ญ๋ชฉ๊ณผ ์นด์šดํŠธ ์ •๋ณด ํฌํ•จ)
658
+ keyword_results.append({
659
+ "ํ‚ค์›Œ๋“œ": keyword, # UI ํ‘œ์‹œ์šฉ ํ‚ค์›Œ๋“œ (๊ณต๋ฐฑ ํฌํ•จ)
660
+ "PC๊ฒ€์ƒ‰๋Ÿ‰": volume_data.get("PC๊ฒ€์ƒ‰๋Ÿ‰", 0),
661
+ "๋ชจ๋ฐ”์ผ๊ฒ€์ƒ‰๋Ÿ‰": volume_data.get("๋ชจ๋ฐ”์ผ๊ฒ€์ƒ‰๋Ÿ‰", 0),
662
+ "์ด๊ฒ€์ƒ‰๋Ÿ‰": volume_data.get("์ด๊ฒ€์ƒ‰๋Ÿ‰", 0),
663
+ "๊ฒ€์ƒ‰๋Ÿ‰๊ตฌ๊ฐ„": text_utils.get_search_volume_range(volume_data.get("์ด๊ฒ€์ƒ‰๋Ÿ‰", 0)),
664
+ "์นดํ…Œ๊ณ ๋ฆฌํ•ญ๋ชฉ": "\n".join(categories_with_percentage) if categories_with_percentage else "-",
665
+ "์นดํ…Œ๊ณ ๋ฆฌ์ •๋ณด": dict(category_counter) # ์›๋ณธ ์นดํ…Œ๊ณ ๋ฆฌ ์นด์šดํ„ฐ ์ €์žฅ (์š”์•ฝ์šฉ)
666
+ })
667
+
668
+ logger.info(f" - '{keyword}' ๋ถ„์„ ์™„๋ฃŒ: ์นดํ…Œ๊ณ ๋ฆฌ ํ•ญ๋ชฉ {len(category_counter)}๊ฐœ, ๊ฒ€์ƒ‰๋Ÿ‰ {volume_data.get('์ด๊ฒ€์ƒ‰๋Ÿ‰', 0)}")
669
+
670
+ # ์ตœ์ข… ๊ฒฐ๊ณผ ์š”์•ฝ ์ถœ๋ ฅ
671
+ logger.info(f"ํ‚ค์›Œ๋“œ ๋ถ„์„ ์™„๋ฃŒ: ์ด {len(keywords)}๊ฐœ ์ค‘ {len(keyword_results)}๊ฐœ ์„ฑ๊ณต")
672
+
673
+ # ๊ฒฐ๊ณผ๋ฅผ ๊ฒ€์ƒ‰๋Ÿ‰ ๊ธฐ์ค€์œผ๋กœ ๋‚ด๋ฆผ์ฐจ์ˆœ ์ •๋ ฌ (๋†’์€ ๊ฒƒ์ด ๋จผ์ € ๋‚˜์˜ค๋„๋ก)
674
+ keyword_results = sorted(keyword_results, key=lambda x: x["์ด๊ฒ€์ƒ‰๋Ÿ‰"], reverse=True)
675
+
676
+ # ์ถ”์ฒœ ์นดํ…Œ๊ณ ๋ฆฌ ๊ณ„์‚ฐ
677
+ recommended_categories = Counter()
678
+ for result in keyword_results:
679
+ for category, count in result.get("์นดํ…Œ๊ณ ๋ฆฌ์ •๋ณด", {}).items():
680
+ recommended_categories[category] += count
681
+
682
+ # ์ถ”์ฒœ ์นดํ…Œ๊ณ ๋ฆฌ ์ƒ์œ„ 3๊ฐœ ์„ ํƒ
683
+ top_categories = recommended_categories.most_common(3)
684
+
685
+ # ์ด ์ƒํ’ˆ ์ˆ˜ ๊ณ„์‚ฐ
686
+ total_products_count = sum(recommended_categories.values())
687
+
688
+ # ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ์ ์œ ์œจ ๊ณ„์‚ฐ
689
+ top_categories_with_percentage = []
690
+ for category, count in top_categories:
691
+ percentage = (count / total_products_count) * 100 if total_products_count > 0 else 0
692
+ top_categories_with_percentage.append({
693
+ "์นดํ…Œ๊ณ ๋ฆฌ": category,
694
+ "๊ฐœ์ˆ˜": count,
695
+ "์ ์œ ์œจ": f"{percentage:.0f}%"
696
+ })
697
+
698
+ # 1๋…„, 3๋…„ ํŠธ๋ Œ๋“œ ๋ถ„์„ ์‹คํ–‰
699
+ trend_results = {"1year": None, "3year": None}
700
+ trend_html = ""
701
+
702
+ if keyword_results:
703
+ try:
704
+ # ํŠธ๋ Œ๋“œ ๋ถ„์„ ๋ชจ๋“ˆ import
705
+ import trend_analysis
706
+
707
+ # ์ƒ์œ„ 5๊ฐœ ํ‚ค์›Œ๋“œ๋กœ 1๋…„, 3๋…„ ํŠธ๋ Œ๋“œ ๋ถ„์„
708
+ top_keywords = [result["ํ‚ค์›Œ๋“œ"] for result in keyword_results[:5]]
709
+ logger.info(f"1๋…„, 3๋…„ ํŠธ๋ Œ๋“œ ๋ถ„์„ ์‹œ์ž‘: {top_keywords}")
710
+
711
+ # 1๋…„ ํŠธ๋ Œ๋“œ ๋ถ„์„
712
+ trend_result_1year = trend_analysis.get_trend_data(top_keywords, "1year")
713
+ if trend_result_1year["status"] == "success":
714
+ trend_results["1year"] = trend_result_1year
715
+
716
+ # 3๋…„ ํŠธ๋ Œ๋“œ ๋ถ„์„
717
+ trend_result_3year = trend_analysis.get_trend_data(top_keywords, "3year")
718
+ if trend_result_3year["status"] == "success":
719
+ trend_results["3year"] = trend_result_3year
720
+
721
+ # ํŠธ๋ Œ๋“œ ๋ถ„์„ HTML ์ƒ์„ฑ (1๋…„, 3๋…„ ๋ชจ๋‘)
722
+ if trend_results["1year"] or trend_results["3year"]:
723
+ trend_html = f'''
724
+ <div class="trend-analysis-section" style="width: 100%; margin-top: 30px;">
725
+ <div class="section-title">๐Ÿ” ๊ฒ€์ƒ‰๋Ÿ‰ ํŠธ๋ Œ๋“œ ๋ถ„์„</div>
726
+ '''
727
+
728
+ for period, result in trend_results.items():
729
+ if result and result["status"] == "success":
730
+ period_text = "์ตœ๊ทผ 1๋…„" if period == "1year" else "์ตœ๊ทผ 3๋…„"
731
+
732
+ # ํŠธ๋ Œ๋“œ ์ธ์‚ฌ์ดํŠธ ์ถ”์ถœ
733
+ insights = trend_analysis.analyze_trend_insights(result["trend_data"])
734
+
735
+ trend_html += f'''
736
+ <div class="trend-period-section" style="width: 100%; background-color: #f0f8ff; padding: 15px; border-radius: 8px; margin-bottom: 20px;">
737
+ <div class="insights-title" style="font-weight: bold; margin-bottom: 15px; color: #2c7fb8;">๐Ÿ“Š {period_text} ์ฃผ์š” ์ธ์‚ฌ์ดํŠธ</div>
738
+ <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 15px; width: 100%;">
739
+ '''
740
+
741
+ for keyword, insight in insights.items():
742
+ growth_icon = "๐Ÿ“ˆ" if insight['growth_rate'] > 0 else "๐Ÿ“‰" if insight['growth_rate'] < 0 else "๐Ÿ“Š"
743
+ growth_color = "#28a745" if insight['growth_rate'] > 0 else "#dc3545" if insight['growth_rate'] < 0 else "#6c757d"
744
+
745
+ trend_html += f'''
746
+ <div class="insight-item" style="background: white; padding: 12px; border-radius: 6px; border-left: 4px solid {growth_color};">
747
+ <div style="font-weight: bold; color: #2c7fb8; margin-bottom: 8px;">{keyword} {growth_icon}</div>
748
+ <div style="font-size: 13px; line-height: 1.4;">
749
+ <div>๐Ÿ† ์ตœ๊ณ ์ : <strong>{insight['max_volume']:,}</strong> ({insight['max_period']})</div>
750
+ <div>๐Ÿ“Š ์ „์ฒด ํ‰๊ท : <strong>{insight['total_avg']:,}</strong></div>
751
+ <div style="color: {growth_color};">๐Ÿ“ˆ ์„ฑ์žฅ๋ฅ : <strong>{insight['growth_rate']:+.1f}%</strong></div>
752
+ </div>
753
+ </div>
754
+ '''
755
+
756
+ trend_html += '''
757
+ </div>
758
+
759
+ <div class="trend-graph-container" style="width: 100%; background: white; padding: 15px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin-top: 15px;">
760
+ '''
761
+ trend_html += result["graph_html"]
762
+ trend_html += '''
763
+ </div>
764
+ </div>
765
+ '''
766
+
767
+ trend_html += '''
768
+ </div>
769
+ '''
770
+
771
+ logger.info(f"1๋…„, 3๋…„ ํŠธ๋ Œ๋“œ ๋ถ„์„ ์™„๋ฃŒ")
772
+ else:
773
+ logger.warning(f"ํŠธ๋ Œ๋“œ ๋ถ„์„ ์‹คํŒจ")
774
+ trend_html = '''
775
+ <div class="trend-analysis-section" style="width: 100%; margin-top: 30px;">
776
+ <div class="section-title">๐Ÿ” ๊ฒ€์ƒ‰๋Ÿ‰ ํŠธ๋ Œ๋“œ ๋ถ„์„</div>
777
+ <div style="width: 100%; background-color: #fff3cd; padding: 15px; border-radius: 8px; color: #856404;">
778
+ โš ๏ธ ํŠธ๋ Œ๋“œ ๋ถ„์„์„ ์‹คํ–‰ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๋‚˜์ค‘์— ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.
779
+ </div>
780
+ </div>
781
+ '''
782
+ except Exception as e:
783
+ logger.error(f"ํŠธ๋ Œ๋“œ ๋ถ„์„ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}")
784
+ trend_html = '''
785
+ <div class="trend-analysis-section" style="width: 100%; margin-top: 30px;">
786
+ <div class="section-title">๐Ÿ” ๊ฒ€์ƒ‰๋Ÿ‰ ํŠธ๋ Œ๋“œ ๋ถ„์„</div>
787
+ <div style="width: 100%; background-color: #f8d7da; padding: 15px; border-radius: 8px; color: #721c24;">
788
+ โŒ ํŠธ๋ Œ๋“œ ๋ถ„์„ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.
789
+ </div>
790
+ </div>
791
+ '''
792
+
793
+ # ๊ฒฐ๊ณผ๋ฅผ HTML ํ…Œ์ด๋ธ”๋กœ ๋ณ€ํ™˜ - ๋„ˆ๋น„ 100% ์ ์šฉ
794
+ html = f'''
795
+ <style>
796
+ .product-analysis-table {{
797
+ width: 100%;
798
+ border-collapse: collapse;
799
+ margin: 25px 0;
800
+ font-size: 14px;
801
+ box-shadow: 0 0 20px rgba(0, 0, 0, 0.15);
802
+ border-radius: 5px;
803
+ overflow: hidden;
804
+ }}
805
+
806
+ .product-analysis-table thead tr {{
807
+ background-color: #009879;
808
+ color: white;
809
+ text-align: left;
810
+ }}
811
+
812
+ .product-analysis-table th,
813
+ .product-analysis-table td {{
814
+ padding: 12px 15px;
815
+ border-bottom: 1px solid #dddddd;
816
+ }}
817
+
818
+ .product-analysis-table tbody tr {{
819
+ background-color: white;
820
+ }}
821
+
822
+ .product-analysis-table tbody tr:nth-of-type(even) {{
823
+ background-color: #f3f3f3;
824
+ }}
825
+
826
+ .product-analysis-table tbody tr:hover {{
827
+ background-color: #f5f5f5;
828
+ }}
829
+
830
+ .product-analysis-table tbody tr:last-of-type {{
831
+ border-bottom: 2px solid #009879;
832
+ }}
833
+
834
+ .section-title {{
835
+ font-size: 18px;
836
+ font-weight: bold;
837
+ color: #009879;
838
+ margin-bottom: 15px;
839
+ padding-bottom: 5px;
840
+ border-bottom: 2px solid #009879;
841
+ }}
842
+
843
+ .summary-box {{
844
+ width: 100%;
845
+ background-color: #f5f5f5;
846
+ border-left: 4px solid #009879;
847
+ padding: 10px 15px;
848
+ margin-bottom: 20px;
849
+ font-size: 14px;
850
+ }}
851
+
852
+ .summary-title {{
853
+ font-weight: bold;
854
+ margin-bottom: 5px;
855
+ }}
856
+
857
+ .recommendation-box {{
858
+ width: 100%;
859
+ background-color: #e7f7f3;
860
+ border-radius: 5px;
861
+ padding: 15px;
862
+ margin-bottom: 25px;
863
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
864
+ }}
865
+
866
+ .recommendation-title {{
867
+ font-weight: bold;
868
+ font-size: 16px;
869
+ color: #009879;
870
+ margin-bottom: 10px;
871
+ }}
872
+
873
+ .recommendation-item {{
874
+ padding: 6px 0;
875
+ border-bottom: 1px solid #e0e0e0;
876
+ }}
877
+
878
+ .recommendation-item:last-child {{
879
+ border-bottom: none;
880
+ }}
881
+ </style>
882
+
883
+ <div style="width: 100%;">
884
+ <div class="section-title">์ƒํ’ˆ๋ช… ํ‚ค์›Œ๋“œ ๋ถ„์„ ๊ฒฐ๊ณผ</div>
885
+
886
+ <div class="summary-box">
887
+ <div class="summary-title">๋ถ„์„ ์š”์•ฝ</div>
888
+ <p>์ด <strong>{len(keyword_results)}</strong>๊ฐœ ํ‚ค์›Œ๋“œ ๋ถ„์„</p>
889
+ <p>๋ฉ”์ธ ํ‚ค์›Œ๋“œ: <strong>{main_keyword if main_keyword else '์—†์Œ'}</strong></p>
890
+ </div>
891
+
892
+ <div class="recommendation-box">
893
+ <div class="recommendation-title">์ถ”์ฒœ ์นดํ…Œ๊ณ ๋ฆฌ</div>
894
+ '''
895
+
896
+ # ์ถ”์ฒœ ์นดํ…Œ๊ณ ๋ฆฌ ๋ชฉ๋ก ์ถ”๊ฐ€
897
+ for idx, cat_info in enumerate(top_categories_with_percentage, 1):
898
+ html += f'''
899
+ <div class="recommendation-item">
900
+ ์ถ”์ฒœ ์นดํ…Œ๊ณ ๋ฆฌ {idx} : {cat_info['์นดํ…Œ๊ณ ๋ฆฌ']}({cat_info['์ ์œ ์œจ']})
901
+ </div>
902
+ '''
903
+
904
+ html += '''
905
+ </div>
906
+
907
+ <table class="product-analysis-table">
908
+ <thead>
909
+ <tr>
910
+ <th>์ˆœ๋ฒˆ</th>
911
+ <th>ํ‚ค์›Œ๋“œ</th>
912
+ <th>PC๊ฒ€์ƒ‰๋Ÿ‰</th>
913
+ <th>๋ชจ๋ฐ”์ผ๊ฒ€์ƒ‰๋Ÿ‰</th>
914
+ <th>์ด๊ฒ€์ƒ‰๋Ÿ‰</th>
915
+ <th>๊ฒ€์ƒ‰๋Ÿ‰๊ตฌ๊ฐ„</th>
916
+ <th>์นดํ…Œ๊ณ ๋ฆฌํ•ญ๋ชฉ</th>
917
+ </tr>
918
+ </thead>
919
+ <tbody>
920
+ '''
921
+
922
+ for idx, result in enumerate(keyword_results):
923
+ # ์นดํ…Œ๊ณ ๋ฆฌ ํ•ญ๋ชฉ ์ค€๋น„ (์ค„๋ฐ”๊ฟˆ์„ <br>๋กœ ๋ณ€ํ™˜)
924
+ category_items = result.get("์นดํ…Œ๊ณ ๋ฆฌํ•ญ๋ชฉ", "-").replace("\n", "<br>")
925
+
926
+ html += f'''
927
+ <tr>
928
+ <td>{idx + 1}</td>
929
+ <td>{result["ํ‚ค์›Œ๋“œ"]}</td>
930
+ <td>{result["PC๊ฒ€์ƒ‰๋Ÿ‰"]:,}</td>
931
+ <td>{result["๋ชจ๋ฐ”์ผ๊ฒ€์ƒ‰๋Ÿ‰"]:,}</td>
932
+ <td>{result["์ด๊ฒ€์ƒ‰๋Ÿ‰"]:,}</td>
933
+ <td>{result["๊ฒ€์ƒ‰๋Ÿ‰๊ตฌ๊ฐ„"]}</td>
934
+ <td>{category_items}</td>
935
+ </tr>
936
+ '''
937
+
938
+ html += '''
939
+ </tbody>
940
+ </table>
941
+ </div>
942
+ '''
943
+
944
+ # ํŠธ๋ Œ๋“œ ๋ถ„์„ HTML ์ถ”๊ฐ€
945
+ html += trend_html
946
+
947
+ # ๋ถ„์„ ๊ฒฐ๊ณผ๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ ์•ˆ๋‚ด ๋ฉ”์‹œ์ง€
948
+ if not keyword_results:
949
+ html += '''
950
+ <div style="width: 100%; margin-top: 20px; padding: 15px; background-color: #f1f1f1; border-radius: 5px; text-align: center;">
951
+ <p>ํ‘œ์‹œํ•  ํ‚ค์›Œ๋“œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ๋‹ค๋ฅธ ์ƒํ’ˆ๋ช…์„ ์ž…๋ ฅํ•ด๋ณด์„ธ์š”.</p>
952
+ </div>
953
+ '''
954
+
955
+ # ๋ถ„์„ ๊ฒฐ๊ณผ๋ฅผ ์ „์—ญ ๋ณ€์ˆ˜์— ์ €์žฅ (๋‹ค์šด๋กœ๋“œ์šฉ)
956
+ _last_keyword_results = keyword_results
957
+
958
+ # HTML๊ณผ ํ•จ๊ป˜ ํ‚ค์›Œ๋“œ ๋ถ„์„ ๊ฒฐ๊ณผ ๋ฐ ํŠธ๋ Œ๋“œ ๊ฒฐ๊ณผ๋„ ํ•จ๊ป˜ ๋ฐ˜ํ™˜
959
+ return html, keyword_results, trend_results
960
+
961
+ def collect_categories_per_keyword(keywords, max_products=10):
962
+ """
963
+ ํ‚ค์›Œ๋“œ๋งˆ๋‹ค ์ƒํ’ˆ n๊ฐœ๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ์นดํ…Œ๊ณ ๋ฆฌ ์ง‘ํ•ฉ ๋ฐ˜ํ™˜
964
+
965
+ Args:
966
+ keywords (list): ํ‚ค์›Œ๋“œ ๋ชฉ๋ก
967
+ max_products (int): ํ‚ค์›Œ๋“œ๋‹น ๊ฒ€์ƒ‰ํ•  ์ตœ๋Œ€ ์ƒํ’ˆ ์ˆ˜
968
+
969
+ Returns:
970
+ dict: ํ‚ค์›Œ๋“œ๋ณ„ ์นดํ…Œ๊ณ ๋ฆฌ ์ง‘ํ•ฉ์„ ๋‹ด์€ ์‚ฌ์ „ {ํ‚ค์›Œ๋“œ: {์นดํ…Œ๊ณ ๋ฆฌ1, ์นดํ…Œ๊ณ ๋ฆฌ2, ...}}
971
+ """
972
+ logger.info(f"ํ‚ค์›Œ๋“œ๋ณ„ ์นดํ…Œ๊ณ ๋ฆฌ ์ˆ˜์ง‘ ์‹œ์ž‘: {len(keywords)}๊ฐœ ํ‚ค์›Œ๋“œ")
973
+ keyword_category_map = {}
974
+
975
+ # ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•œ ์ค€๋น„
976
+ batch_size = 5
977
+ batches = []
978
+ for i in range(0, len(keywords), batch_size):
979
+ batches.append(keywords[i:i + batch_size])
980
+
981
+ logger.info(f"์ด {len(batches)}๊ฐœ ๋ฐฐ์น˜๋กœ {len(keywords)}๊ฐœ ํ‚ค์›Œ๋“œ ์ฒ˜๋ฆฌ")
982
+
983
+ # ๊ฐ ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ
984
+ for batch_idx, batch in enumerate(batches):
985
+ logger.info(f"๋ฐฐ์น˜ {batch_idx+1}/{len(batches)} ์ฒ˜๋ฆฌ ์ค‘...")
986
+
987
+ for keyword in batch:
988
+ # API ํ˜ธ์ถœ์šฉ ํ‚ค์›Œ๋“œ (๊ณต๋ฐฑ ์ œ๊ฑฐ)
989
+ api_keyword = keyword.replace(" ", "")
990
+
991
+ max_retries = 3
992
+ retry_count = 0
993
+
994
+ while retry_count < max_retries:
995
+ try:
996
+ # ํ‚ค์›Œ๋“œ๋กœ ์ƒํ’ˆ ๊ฒ€์ƒ‰
997
+ products = product_search.fetch_naver_shopping_data_for_analysis(api_keyword, count=max_products)
998
+
999
+ if products:
1000
+ # ์นดํ…Œ๊ณ ๋ฆฌ ์ถ”์ถœ
1001
+ categories = set()
1002
+ for product in products:
1003
+ # ๋‘ ๊ฐ€์ง€ ํ‚ค ๋ชจ๋‘ ์‹œ๋„ (category์™€ ์นดํ…Œ๊ณ ๋ฆฌ)
1004
+ category = product.get("category", "") or product.get("์นดํ…Œ๊ณ ๋ฆฌ", "")
1005
+ if category:
1006
+ categories.add(category)
1007
+
1008
+ keyword_category_map[keyword] = categories
1009
+ logger.info(f" - '{keyword}' ์นดํ…Œ๊ณ ๋ฆฌ ์ˆ˜์ง‘ ์™„๋ฃŒ: {len(categories)}๊ฐœ")
1010
+ break # ์„ฑ๊ณตํ–ˆ์œผ๋ฏ€๋กœ ๋ฃจํ”„ ์ข…๋ฃŒ
1011
+ else:
1012
+ logger.warning(f" - '{keyword}' ์ƒํ’ˆ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์—†์Œ (์‹œ๋„ {retry_count+1}/{max_retries})")
1013
+ retry_count += 1
1014
+ exponential_backoff_sleep(retry_count)
1015
+
1016
+ except Exception as e:
1017
+ logger.error(f" - '{keyword}' ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜: {e} (์‹œ๋„ {retry_count+1}/{max_retries})")
1018
+ retry_count += 1
1019
+ exponential_backoff_sleep(retry_count)
1020
+
1021
+ if retry_count >= max_retries:
1022
+ logger.error(f" - '{keyword}' ์ตœ๋Œ€ ์žฌ์‹œ๋„ ํ›„ ์‹คํŒจ")
1023
+ keyword_category_map[keyword] = set()
1024
+
1025
+ # API ๋ ˆ์ดํŠธ ๋ฆฌ๋ฐ‹ ๋ฐฉ์ง€ (์•ˆ์ •์ ์ธ ์ง€์—ฐ์œผ๋กœ ๋ณ€๊ฒฝ)
1026
+ exponential_backoff_sleep(0) # ์ดˆ๊ธฐ ์ง€์—ฐ ์ ์šฉ
1027
+
1028
+ logger.info(f"ํ‚ค์›Œ๋“œ๋ณ„ ์นดํ…Œ๊ณ ๋ฆฌ ์ˆ˜์ง‘ ์™„๋ฃŒ: {len(keyword_category_map)}๊ฐœ")
1029
+ return keyword_category_map
export_utils.py ADDED
@@ -0,0 +1,510 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ๊ฒฐ๊ณผ ์ถœ๋ ฅ ๊ด€๋ จ ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜ ๋ชจ์Œ - ์นดํ…Œ๊ณ ๋ฆฌ ํ•ญ๋ชฉ ์ œ๊ฑฐ + ํ‚ค์›Œ๋“œ ํด๋ฆญ ์‹œ ๋„ค์ด๋ฒ„ ์‡ผํ•‘ ์ด๋™ ๊ธฐ๋Šฅ ์ถ”๊ฐ€
3
+ - HTML ํ…Œ์ด๋ธ” ์ƒ์„ฑ
4
+ - ์—‘์…€ ํŒŒ์ผ ์ƒ์„ฑ
5
+ - ํ‚ค์›Œ๋“œ ํด๋ฆญ ์‹œ ๋„ค์ด๋ฒ„ ์‡ผํ•‘ ๋งํฌ ๊ธฐ๋Šฅ
6
+ """
7
+
8
+ import pandas as pd
9
+ import tempfile
10
+ import os
11
+ import threading
12
+ import time
13
+ import logging
14
+ import urllib.parse # URL ์ธ์ฝ”๋”ฉ์„ ์œ„ํ•ด ์ถ”๊ฐ€
15
+
16
+ # ๋กœ๊น… ์„ค์ •
17
+ logger = logging.getLogger(__name__)
18
+ logger.setLevel(logging.INFO)
19
+ formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
20
+ handler = logging.StreamHandler()
21
+ handler.setFormatter(formatter)
22
+ logger.addHandler(handler)
23
+
24
+ # ์ž„์‹œ ํŒŒ์ผ ์ถ”์  ๋ฆฌ์ŠคํŠธ
25
+ _temp_files = []
26
+
27
+ def create_table_without_checkboxes(df):
28
+ """DataFrame์„ HTML ํ…Œ์ด๋ธ”๋กœ ๋ณ€ํ™˜ - ํ‚ค์›Œ๋“œ ํด๋ฆญ ์‹œ ๋„ค์ด๋ฒ„ ์‡ผํ•‘ ์ด๋™ ๊ธฐ๋Šฅ ์ถ”๊ฐ€"""
29
+ if df.empty:
30
+ return "<p>๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.</p>"
31
+
32
+ # === ์ˆ˜์ •๋œ ๋ถ€๋ถ„: ์นดํ…Œ๊ณ ๋ฆฌ ๊ด€๋ จ ์—ด ์ œ๊ฑฐ ===
33
+ df_display = df.copy()
34
+
35
+ # "์ƒํ’ˆ ๋“ฑ๋ก ์นดํ…Œ๊ณ ๋ฆฌ(์ƒ์œ„100์œ„)" ๋˜๋Š” "๊ด€๋ จ ์นดํ…Œ๊ณ ๋ฆฌ", "์นดํ…Œ๊ณ ๋ฆฌ ํ•ญ๋ชฉ" ์—ด์ด ์žˆ์œผ๋ฉด ์ œ๊ฑฐ
36
+ columns_to_remove = ["์ƒํ’ˆ ๋“ฑ๋ก ์นดํ…Œ๊ณ ๋ฆฌ(์ƒ์œ„100์œ„)", "๊ด€๋ จ ์นดํ…Œ๊ณ ๋ฆฌ", "์นดํ…Œ๊ณ ๋ฆฌ ํ•ญ๋ชฉ"]
37
+ for col in columns_to_remove:
38
+ if col in df_display.columns:
39
+ df_display = df_display.drop(columns=[col])
40
+ logger.info(f"ํ…Œ์ด๋ธ”์—์„œ '{col}' ์—ด ์ œ๊ฑฐ๋จ")
41
+
42
+ # HTML ํ…Œ์ด๋ธ” ์Šคํƒ€์ผ ์ •์˜ - Z-INDEX ์ˆ˜์ •
43
+ html = '''
44
+ <style>
45
+ .table-container {
46
+ position: relative;
47
+ width: 100%;
48
+ margin: 0;
49
+ border-radius: 8px;
50
+ overflow: hidden;
51
+ box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
52
+ }
53
+
54
+ .header-wrap {
55
+ position: sticky;
56
+ top: 0;
57
+ z-index: 100; /* z-index ์ฆ๊ฐ€ */
58
+ background-color: #009879;
59
+ }
60
+
61
+ .styled-table {
62
+ width: 100%;
63
+ border-collapse: collapse;
64
+ table-layout: fixed;
65
+ margin: 0;
66
+ padding: 0;
67
+ font-size: 14px;
68
+ }
69
+
70
+ .styled-table th,
71
+ .styled-table td {
72
+ padding: 12px 15px;
73
+ text-align: left;
74
+ border-bottom: 1px solid #dddddd;
75
+ overflow: hidden;
76
+ text-overflow: ellipsis;
77
+ }
78
+
79
+ /* ๊ธด ํ…์ŠคํŠธ๊ฐ€ ์…€์—์„œ ์ค„๋ฐ”๊ฟˆ๋˜๋„๋ก ์ˆ˜์ • */
80
+ .styled-table td.col-rank {
81
+ white-space: normal;
82
+ word-break: break-word;
83
+ line-height: 1.3;
84
+ }
85
+
86
+ /* ๊ทธ ์™ธ ์—ด์€ ํ•œ ์ค„๋กœ ํ‘œ์‹œ */
87
+ .styled-table td.col-seq,
88
+ .styled-table td.col-keyword,
89
+ .styled-table td.col-pc,
90
+ .styled-table td.col-mobile,
91
+ .styled-table td.col-total,
92
+ .styled-table td.col-range,
93
+ .styled-table td.col-count {
94
+ white-space: nowrap;
95
+ }
96
+
97
+ .styled-table th {
98
+ background-color: #009879;
99
+ color: white;
100
+ font-weight: bold;
101
+ position: sticky;
102
+ top: 0;
103
+ white-space: nowrap;
104
+ z-index: 50; /* ํ—ค๋” z-index ์ฆ๊ฐ€ */
105
+ }
106
+
107
+ .styled-table tbody tr:nth-of-type(even) {
108
+ background-color: #f3f3f3;
109
+ }
110
+
111
+ .styled-table tbody tr:hover {
112
+ background-color: #f0f0f0;
113
+ }
114
+
115
+ .styled-table tbody tr:last-of-type {
116
+ border-bottom: 2px solid #009879;
117
+ }
118
+
119
+ /* ๋ฐ์ดํ„ฐ ์…€ z-index ์„ค์ • */
120
+ .styled-table tbody td {
121
+ position: relative;
122
+ z-index: 1; /* ๋ฐ์ดํ„ฐ ์…€์€ ๋‚ฎ์€ z-index */
123
+ }
124
+
125
+ .data-container {
126
+ max-height: 600px;
127
+ overflow-y: auto;
128
+ position: relative; /* position ์ถ”๊ฐ€ */
129
+ }
130
+
131
+ /* ์Šคํฌ๋กค๋ฐ” ์Šคํƒ€์ผ */
132
+ .data-container::-webkit-scrollbar {
133
+ width: 10px;
134
+ }
135
+
136
+ .data-container::-webkit-scrollbar-track {
137
+ background: #f1f1f1;
138
+ border-radius: 5px;
139
+ }
140
+
141
+ .data-container::-webkit-scrollbar-thumb {
142
+ background: #888;
143
+ border-radius: 5px;
144
+ }
145
+
146
+ .data-container::-webkit-scrollbar-thumb:hover {
147
+ background: #555;
148
+ }
149
+
150
+ /* ํ‚ค์›Œ๋“œ ๋งํฌ ์Šคํƒ€์ผ - ์ƒˆ๋กœ ์ถ”๊ฐ€ */
151
+ .keyword-link {
152
+ color: #2c5aa0;
153
+ text-decoration: none;
154
+ font-weight: 600;
155
+ cursor: pointer;
156
+ transition: all 0.3s ease;
157
+ display: inline-block;
158
+ padding: 2px 4px;
159
+ border-radius: 3px;
160
+ position: relative;
161
+ z-index: 5; /* ๋งํฌ z-index ์„ค์ • */
162
+ }
163
+
164
+ .keyword-link:hover {
165
+ color: #ffffff;
166
+ background-color: #2c5aa0;
167
+ text-decoration: none;
168
+ transform: translateY(-1px);
169
+ box-shadow: 0 2px 4px rgba(44, 90, 160, 0.3);
170
+ }
171
+
172
+ .keyword-link:active {
173
+ transform: translateY(0px);
174
+ }
175
+
176
+ /* ํ‚ค์›Œ๋“œ ์…€ ํŠน๋ณ„ ์Šคํƒ€์ผ */
177
+ .col-keyword {
178
+ position: relative;
179
+ }
180
+
181
+ .keyword-tooltip {
182
+ position: absolute;
183
+ bottom: 100%;
184
+ left: 50%;
185
+ transform: translateX(-50%);
186
+ background-color: #333;
187
+ color: white;
188
+ padding: 6px 10px;
189
+ border-radius: 4px;
190
+ font-size: 11px;
191
+ white-space: nowrap;
192
+ opacity: 0;
193
+ visibility: hidden;
194
+ transition: all 0.3s ease;
195
+ z-index: 1000; /* ํˆดํŒ์€ ๊ฐ€์žฅ ๋†’์€ z-index */
196
+ pointer-events: none;
197
+ margin-bottom: 5px;
198
+ }
199
+
200
+ .keyword-tooltip::after {
201
+ content: '';
202
+ position: absolute;
203
+ top: 100%;
204
+ left: 50%;
205
+ transform: translateX(-50%);
206
+ border: 4px solid transparent;
207
+ border-top-color: #333;
208
+ }
209
+
210
+ .keyword-link:hover .keyword-tooltip {
211
+ opacity: 1;
212
+ visibility: visible;
213
+ }
214
+
215
+ /* === ์ˆ˜์ •๋œ ๋ถ€๋ถ„: ์—ด ๋„ˆ๋น„ ์ •์˜ - ์นดํ…Œ๊ณ ๋ฆฌ ์—ด ์ œ๊ฑฐ ํ›„ ์กฐ์ • === */
216
+ .col-seq { width: 8%; }
217
+ .col-keyword { width: 25%; }
218
+ .col-pc { width: 12%; }
219
+ .col-mobile { width: 12%; }
220
+ .col-total { width: 12%; }
221
+ .col-range { width: 12%; }
222
+ .col-rank { width: 15%; }
223
+ .col-count { width: 10%; }
224
+
225
+ .truncated-text {
226
+ position: relative;
227
+ cursor: pointer;
228
+ z-index: 2; /* ํ…์ŠคํŠธ z-index ์„ค์ • */
229
+ }
230
+
231
+ .truncated-text:hover::after {
232
+ content: attr(data-full-text);
233
+ position: absolute;
234
+ left: 0;
235
+ top: 100%;
236
+ z-index: 99;
237
+ min-width: 200px;
238
+ max-width: 400px;
239
+ padding: 8px;
240
+ background-color: #fff;
241
+ border: 1px solid #ddd;
242
+ border-radius: 4px;
243
+ box-shadow: 0 2px 5px rgba(0,0,0,0.2);
244
+ white-space: normal;
245
+ }
246
+
247
+ /* ํ‚ค์›Œ๋“œ ํƒœ๊ทธ ์Šคํƒ€์ผ */
248
+ .keyword-tag-container {
249
+ margin-top: 20px;
250
+ padding: 10px;
251
+ border: 1px solid #ddd;
252
+ border-radius: 5px;
253
+ background-color: #f9f9f9;
254
+ }
255
+
256
+ .keyword-tag {
257
+ display: inline-block;
258
+ background-color: #009879;
259
+ color: white;
260
+ padding: 5px 10px;
261
+ margin: 5px;
262
+ border-radius: 15px;
263
+ font-size: 12px;
264
+ }
265
+
266
+ .category-tag {
267
+ display: inline-block;
268
+ background-color: #2c7fb8;
269
+ color: white;
270
+ padding: 5px 10px;
271
+ margin: 5px;
272
+ border-radius: 15px;
273
+ font-size: 12px;
274
+ }
275
+
276
+ /* ๋ถ„์„ ๊ฒฐ๊ณผ ํ…Œ์ด๋ธ” ์Šคํƒ€์ผ */
277
+ .analysis-result {
278
+ margin-top: 30px;
279
+ border: 1px solid #ddd;
280
+ border-radius: 5px;
281
+ padding: 15px;
282
+ background-color: #f9f9f9;
283
+ }
284
+
285
+ .result-header {
286
+ font-weight: bold;
287
+ margin-bottom: 10px;
288
+ color: #009879;
289
+ }
290
+
291
+ .match-item {
292
+ margin: 5px 0;
293
+ padding: 5px;
294
+ border-bottom: 1px solid #eee;
295
+ }
296
+
297
+ .match-keyword {
298
+ font-weight: bold;
299
+ color: #2c7fb8;
300
+ }
301
+
302
+ .match-count {
303
+ display: inline-block;
304
+ background-color: #009879;
305
+ color: white;
306
+ padding: 2px 8px;
307
+ border-radius: 10px;
308
+ font-size: 12px;
309
+ margin-left: 10px;
310
+ }
311
+ </style>
312
+ '''
313
+
314
+ # === ์ˆ˜์ •๋œ ๋ถ€๋ถ„: ์—ด ์ด๋ฆ„๊ณผ ํด๋ž˜์Šค ๋งคํ•‘ - ์นดํ…Œ๊ณ ๋ฆฌ ๊ด€๋ จ ์ œ๊ฑฐ ===
315
+ col_mapping = {
316
+ "์ˆœ๋ฒˆ": "col-seq",
317
+ "์กฐํ•ฉ ํ‚ค์›Œ๋“œ": "col-keyword",
318
+ "์—ฐ๊ด€ ํ‚ค์›Œ๋“œ": "col-keyword", # ์—ฐ๊ด€๊ฒ€์ƒ‰์–ด ๋ถ„์„์šฉ ์ถ”๊ฐ€
319
+ "ํ‚ค์›Œ๋“œ": "col-keyword", # ์ผ๋ฐ˜ ํ‚ค์›Œ๋“œ์šฉ ์ถ”๊ฐ€
320
+ "PC๊ฒ€์ƒ‰๋Ÿ‰": "col-pc",
321
+ "๋ชจ๋ฐ”์ผ๊ฒ€์ƒ‰๋Ÿ‰": "col-mobile",
322
+ "์ด๊ฒ€์ƒ‰๋Ÿ‰": "col-total",
323
+ "๊ฒ€์ƒ‰๋Ÿ‰๊ตฌ๊ฐ„": "col-range",
324
+ "ํ‚ค์›Œ๋“œ ์‚ฌ์šฉ์ž์ˆœ์œ„": "col-rank",
325
+ "ํ‚ค์›Œ๋“œ ์‚ฌ์šฉํšŸ์ˆ˜": "col-count"
326
+ # ์นดํ…Œ๊ณ ๋ฆฌ ๊ด€๋ จ ๋งคํ•‘ ์ œ๊ฑฐ๋จ
327
+ }
328
+
329
+ # ๋„ค์ด๋ฒ„ ์‡ผํ•‘ ๋งํฌ ์ƒ์„ฑ ํ•จ์ˆ˜
330
+ def create_naver_shopping_link(keyword):
331
+ """ํ‚ค์›Œ๋“œ๋ฅผ ๋„ค์ด๋ฒ„ ์‡ผํ•‘ ๋งํฌ๋กœ ๋ณ€ํ™˜"""
332
+ # URL ์ธ์ฝ”๋”ฉ (ํ•œ๊ธ€ ํ‚ค์›Œ๋“œ ์ฒ˜๋ฆฌ)
333
+ encoded_keyword = urllib.parse.quote(keyword.strip())
334
+ naver_shopping_url = f"https://search.shopping.naver.com/search/all?where=all&frm=NVSCTAB&query={encoded_keyword}"
335
+
336
+ # ๋งํฌ๊ฐ€ ํฌํ•จ๋œ HTML ๋ฐ˜ํ™˜
337
+ return f'''<a href="{naver_shopping_url}" target="_blank" class="keyword-link" title="๋„ค์ด๋ฒ„ ์‡ผํ•‘์—์„œ '{keyword}' ๊ฒ€์ƒ‰ํ•˜๊ธฐ">
338
+ {keyword}
339
+ <span class="keyword-tooltip">ํด๋ฆญํ•˜๋ฉด ๋„ค์ด๋ฒ„ ์‡ผํ•‘์œผ๋กœ ์ด๋™</span>
340
+ </a>'''
341
+
342
+ # ํ…Œ์ด๋ธ” ์ปจํ…Œ์ด๋„ˆ ์‹œ์ž‘
343
+ html += '<div class="table-container">'
344
+
345
+ # ๋‹จ์ผ ํ…Œ์ด๋ธ” ๊ตฌ์กฐ๋กœ ๋ณ€๊ฒฝ (ํ—ค๋”๋Š” position: sticky๋กœ ๊ณ ์ •)
346
+ html += '<div class="data-container">'
347
+ html += '<table class="styled-table">'
348
+
349
+ # colgroup์œผ๋กœ ์—ด ๋„ˆ๋น„ ์ •์˜
350
+ html += '<colgroup>'
351
+ html += f'<col class="{col_mapping["์ˆœ๋ฒˆ"]}">'
352
+ for col in df_display.columns:
353
+ col_class = col_mapping.get(col, "")
354
+ html += f'<col class="{col_class}">'
355
+ html += '</colgroup>'
356
+
357
+ # ํ…Œ์ด๋ธ” ํ—ค๋”
358
+ html += '<thead>'
359
+ html += '<tr>'
360
+ html += f'<th class="{col_mapping["์ˆœ๋ฒˆ"]}">์ˆœ๋ฒˆ</th>'
361
+ for col in df_display.columns:
362
+ col_class = col_mapping.get(col, "")
363
+ html += f'<th class="{col_class}">{col}</th>'
364
+ html += '</tr>'
365
+ html += '</thead>'
366
+
367
+ # ํ…Œ์ด๋ธ” ๋ณธ๋ฌธ
368
+ html += '<tbody>'
369
+ for idx, row in df_display.iterrows():
370
+ html += '<tr>'
371
+ # ์ˆœ๋ฒˆ ํ‘œ์‹œ - 1๋ถ€ํ„ฐ ์‹œ์ž‘ํ•˜๋Š” ์ˆœ์ฐจ์  ๋ฒˆํ˜ธ
372
+ html += f'<td class="{col_mapping["์ˆœ๋ฒˆ"]}">{idx + 1}</td>'
373
+
374
+ # ๋ฐ์ดํ„ฐ ์…€ ์ถ”๊ฐ€
375
+ for col in df_display.columns:
376
+ col_class = col_mapping.get(col, "")
377
+ value = str(row[col])
378
+
379
+ # === ์ƒˆ๋กœ ์ถ”๊ฐ€: ํ‚ค์›Œ๋“œ ์—ด์— ๋งํฌ ์ ์šฉ ===
380
+ if col in ["์กฐํ•ฉ ํ‚ค์›Œ๋“œ", "์—ฐ๊ด€ ํ‚ค์›Œ๋“œ", "ํ‚ค์›Œ๋“œ"]:
381
+ # ํ‚ค์›Œ๋“œ ์…€์— ๋„ค์ด๋ฒ„ ์‡ผํ•‘ ๋งํฌ ์ ์šฉ
382
+ keyword_with_link = create_naver_shopping_link(value)
383
+ html += f'<td class="{col_class}">{keyword_with_link}</td>'
384
+ elif col == "ํ‚ค์›Œ๋“œ ์‚ฌ์šฉ์ž์ˆœ์œ„":
385
+ # ๊ธด ํ…์ŠคํŠธ์˜ ์…€์€ ๊ทธ๋Œ€๋กœ ํ‘œ์‹œ (์ค„๋ฐ”๊ฟˆ ํ—ˆ์šฉ)
386
+ html += f'<td class="{col_class}">{value}</td>'
387
+ elif len(value) > 30:
388
+ # ๋‹ค๋ฅธ ๊ธด ํ…์ŠคํŠธ๋Š” hover๋กœ ์ „์ฒด ํ‘œ์‹œ
389
+ html += f'<td class="{col_class}"><div class="truncated-text" data-full-text="{value}">{value[:30]}...</div></td>'
390
+ else:
391
+ # ์ผ๋ฐ˜ ํ…์ŠคํŠธ
392
+ html += f'<td class="{col_class}">{value}</td>'
393
+ html += '</tr>'
394
+
395
+ html += '</tbody>'
396
+ html += '</table>'
397
+ html += '</div>' # data-container ๋‹ซ๊ธฐ
398
+ html += '</div>' # table-container ๋‹ซ๊ธฐ
399
+
400
+ # ์‚ฌ์šฉ๋ฒ• ์•ˆ๋‚ด ์ถ”๊ฐ€
401
+ html += '''
402
+ <div style="margin-top: 15px; padding: 12px; background: #e8f5e8; border-radius: 8px; border-left: 4px solid #009879;">
403
+ <div style="font-weight: bold; color: #155724; margin-bottom: 5px;">๐Ÿ’ก ์‚ฌ์šฉํŒ</div>
404
+ <div style="font-size: 14px; color: #155724;">
405
+ ํ‚ค์›Œ๋“œ๋ฅผ ํด๋ฆญํ•˜๋ฉด ๋„ค์ด๋ฒ„ ์‡ผํ•‘์—์„œ ํ•ด๋‹น ํ‚ค์›Œ๋“œ๋กœ ๊ฒ€์ƒ‰ํ•œ ๊ฒฐ๊ณผ๋ฅผ ์ƒˆ ์ฐฝ์—์„œ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
406
+ </div>
407
+ </div>
408
+ '''
409
+
410
+ return html
411
+
412
+ def cleanup_temp_files(delay=300):
413
+ """์ž„์‹œ ํŒŒ์ผ ์ •๋ฆฌ ํ•จ์ˆ˜"""
414
+ global _temp_files
415
+
416
+ def cleanup():
417
+ time.sleep(delay) # ์ง€์ •๋œ ์‹œ๊ฐ„ ๋Œ€๊ธฐ
418
+ temp_files_to_remove = _temp_files.copy()
419
+ _temp_files = []
420
+
421
+ for file_path in temp_files_to_remove:
422
+ try:
423
+ if os.path.exists(file_path):
424
+ os.remove(file_path)
425
+ logger.info(f"์ž„์‹œ ํŒŒ์ผ ์‚ญ์ œ: {file_path}")
426
+ except Exception as e:
427
+ logger.error(f"ํŒŒ์ผ ์‚ญ์ œ ์˜ค๋ฅ˜: {e}")
428
+
429
+ # ์ƒˆ ์Šค๋ ˆ๋“œ ์‹œ์ž‘
430
+ threading.Thread(target=cleanup, daemon=True).start()
431
+
432
+ def download_keywords(df, auto_cleanup=True, cleanup_delay=300):
433
+ """ํ‚ค์›Œ๋“œ ๋ฐ์ดํ„ฐ๋ฅผ ์—‘์…€ ํŒŒ์ผ๋กœ ๋‹ค์šด๋กœ๋“œ - ์นดํ…Œ๊ณ ๋ฆฌ ํ•ญ๋ชฉ ์ œ๊ฑฐ"""
434
+ global _temp_files
435
+
436
+ if df is None or df.empty:
437
+ return None
438
+
439
+ # ์ž„์‹œ ํŒŒ์ผ๋กœ ์ €์žฅ
440
+ temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.xlsx')
441
+ temp_file.close()
442
+ filename = temp_file.name
443
+
444
+ # ์ž„์‹œ ํŒŒ์ผ ์ถ”์  ๋ชฉ๋ก์— ์ถ”๊ฐ€
445
+ _temp_files.append(filename)
446
+
447
+ # === ์ˆ˜์ •๋œ ๋ถ€๋ถ„: ์นดํ…Œ๊ณ ๋ฆฌ ๊ด€๋ จ ์—ด ์ œ๊ฑฐ ===
448
+ df_export = df.copy()
449
+
450
+ # ์นดํ…Œ๊ณ ๋ฆฌ ๊ด€๋ จ ์—ด๋“ค ์ œ๊ฑฐ
451
+ columns_to_remove = ["์ƒํ’ˆ ๋“ฑ๋ก ์นดํ…Œ๊ณ ๋ฆฌ(์ƒ์œ„100์œ„)", "๊ด€๋ จ ์นดํ…Œ๊ณ ๋ฆฌ", "์นดํ…Œ๊ณ ๋ฆฌ ํ•ญ๋ชฉ"]
452
+ for col in columns_to_remove:
453
+ if col in df_export.columns:
454
+ df_export = df_export.drop(columns=[col])
455
+ logger.info(f"์—‘์…€ ๋‚ด๋ณด๋‚ด๊ธฐ์—์„œ '{col}' ์—ด ์ œ๊ฑฐ๋จ")
456
+
457
+ # ํ‚ค์›Œ๋“œ ๋ฐ์ดํ„ฐ๋ฅผ ์—‘์…€ ํŒŒ์ผ๋กœ ์ €์žฅ
458
+ with pd.ExcelWriter(filename, engine='xlsxwriter') as writer:
459
+ # ํ‚ค์›Œ๋“œ ๋ชฉ๋ก ์‹œํŠธ
460
+ df_export.to_excel(writer, sheet_name='ํ‚ค์›Œ๋“œ ๋ชฉ๋ก', index=False)
461
+
462
+ # ์—ด ๋„ˆ๋น„ ์กฐ์ • - ์นดํ…Œ๊ณ ๋ฆฌ ์—ด ์ œ๊ฑฐ ํ›„ ์กฐ์ •
463
+ worksheet = writer.sheets['ํ‚ค์›Œ๋“œ ๋ชฉ๋ก']
464
+ worksheet.set_column('A:A', 20) # ์กฐํ•ฉ ํ‚ค์›Œ๋“œ ์—ด
465
+ worksheet.set_column('B:B', 12) # PC๊ฒ€์ƒ‰๋Ÿ‰ ์—ด
466
+ worksheet.set_column('C:C', 12) # ๋ชจ๋ฐ”์ผ๊ฒ€์ƒ‰๋Ÿ‰ ์—ด
467
+ worksheet.set_column('D:D', 12) # ์ด๊ฒ€์ƒ‰๋Ÿ‰ ์—ด
468
+ worksheet.set_column('E:E', 12) # ๊ฒ€์ƒ‰๋Ÿ‰๊ตฌ๊ฐ„ ์—ด
469
+ worksheet.set_column('F:F', 20) # ํ‚ค์›Œ๋“œ ์‚ฌ์šฉ์ž์ˆœ์œ„ ์—ด
470
+ worksheet.set_column('G:G', 12) # ํ‚ค์›Œ๋“œ ์‚ฌ์šฉํšŸ์ˆ˜ ์—ด
471
+ # ์นดํ…Œ๊ณ ๋ฆฌ ์—ด๋“ค ์ œ๊ฑฐ๋กœ H, I ์—ด ์„ค์ • ์ œ๊ฑฐ๋จ
472
+
473
+ # ํ—ค๋” ํ˜•์‹ ์„ค์ •
474
+ header_format = writer.book.add_format({
475
+ 'bold': True,
476
+ 'bg_color': '#009879',
477
+ 'color': 'white',
478
+ 'border': 1
479
+ })
480
+
481
+ # ํ—ค๋”์— ํ˜•์‹ ์ ์šฉ
482
+ for col_num, value in enumerate(df_export.columns.values):
483
+ worksheet.write(0, col_num, value, header_format)
484
+
485
+ logger.info(f"์—‘์…€ ํŒŒ์ผ ์ƒ์„ฑ: {filename}")
486
+
487
+ # ํŒŒ์ผ ์ž๋™ ์ •๋ฆฌ ์˜ต์…˜
488
+ if auto_cleanup:
489
+ # ๋ณ„๋„ ์ •๋ฆฌ ์ž‘์—… ์š”์ฒญ ์—†์ด ์ถ”์  ๋ชฉ๋ก์— ์ถ”๊ฐ€๋งŒ ํ•˜์—ฌ ์ผ๊ด„ ์ฒ˜๋ฆฌ
490
+ pass
491
+
492
+ return filename
493
+
494
+ def register_cleanup_handlers():
495
+ """์•ฑ ์ข…๋ฃŒ ์‹œ ์ •๋ฆฌ๋ฅผ ์œ„ํ•œ ํ•ธ๋“ค๋Ÿฌ ๋“ฑ๋ก"""
496
+ import atexit
497
+
498
+ def cleanup_all_temp_files():
499
+ global _temp_files
500
+ for file_path in _temp_files:
501
+ try:
502
+ if os.path.exists(file_path):
503
+ os.remove(file_path)
504
+ logger.info(f"์ข…๋ฃŒ ์‹œ ์ž„์‹œ ํŒŒ์ผ ์‚ญ์ œ: {file_path}")
505
+ except Exception as e:
506
+ logger.error(f"ํŒŒ์ผ ์‚ญ์ œ ์˜ค๋ฅ˜: {e}")
507
+ _temp_files = []
508
+
509
+ # ์•ฑ ์ข…๋ฃŒ ์‹œ ์‹คํ–‰๋  ํ•จ์ˆ˜ ๋“ฑ๋ก
510
+ atexit.register(cleanup_all_temp_files)
keyword_analysis.py ADDED
@@ -0,0 +1,1687 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ํ‚ค์›Œ๋“œ ๋งค์นญ ๋ฐ ์ƒ์Šนํญ ๊ณ„์‚ฐ ๊ฐœ์„  - ์ „์ฒด ์ฝ”๋“œ
3
+ v4.0 - ์ŠคํŽ˜์ด์Šค๋ฐ” ์ฒ˜๋ฆฌ ๊ฐœ์„  + ์˜ฌ๋ฐ”๋ฅธ ํŠธ๋ Œ๋“œ ๋ถ„์„ ๋กœ์ง ์ ์šฉ
4
+ - ๊ธฐ์กด ๋ชจ๋“  ๊ธฐ๋Šฅ ์œ ์ง€ํ•˜๋ฉด์„œ ์ตœ์ ํ™”
5
+ - ์ŠคํŽ˜์ด์Šค๋ฐ” ์ œ๊ฑฐ ํ›„ ๊ฒ€์ƒ‰/๋น„๊ต ๋กœ์ง ์ ์šฉ
6
+ - ์˜ฌ๋ฐ”๋ฅธ ์ฆ๊ฐ์œจ ๊ณ„์‚ฐ: ์˜ฌํ•ด ์™„๋ฃŒ์›” vs ์ž‘๋…„ ๋™์›”
7
+ - ๐Ÿ”– ๊ฐ€์žฅ ๊ฒ€์ƒ‰๋Ÿ‰์ด ๋งŽ์€ ์›”: ์‹ค์ œ+์˜ˆ์ƒ ๋ฐ์ดํ„ฐ ์ค‘ ์ตœ๋Œ€๊ฐ’
8
+ - ๐Ÿ”– ๊ฐ€์žฅ ์ƒ์Šนํญ์ด ๋†’์€ ์›”: ์—ฐ์†๋œ ์›”๊ฐ„ ์ƒ์Šน๋ฅ  ์ค‘ ์ตœ๋Œ€๊ฐ’
9
+ """
10
+
11
+ import logging
12
+ import pandas as pd
13
+ from datetime import datetime
14
+ import re
15
+ import time
16
+ import random
17
+ from typing import Dict, List, Optional
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+ def normalize_keyword(keyword):
22
+ """ํ‚ค์›Œ๋“œ ์ •๊ทœํ™” - ์ŠคํŽ˜์ด์Šค๋ฐ” ์ฒ˜๋ฆฌ ๊ฐœ์„ """
23
+ if not keyword:
24
+ return ""
25
+
26
+ # 1. ์•ž๋’ค ๊ณต๋ฐฑ ์ œ๊ฑฐ
27
+ keyword = keyword.strip()
28
+
29
+ # 2. ์—ฐ์†๋œ ๊ณต๋ฐฑ์„ ํ•˜๋‚˜๋กœ ๋ณ€๊ฒฝ
30
+ keyword = re.sub(r'\s+', ' ', keyword)
31
+
32
+ # 3. ํŠน์ˆ˜๋ฌธ์ž ์ œ๊ฑฐ (ํ•œ๊ธ€, ์˜๋ฌธ, ์ˆซ์ž, ๊ณต๋ฐฑ๋งŒ ๋‚จ๊น€)
33
+ keyword = re.sub(r'[^\w\s๊ฐ€-ํžฃ]', '', keyword)
34
+
35
+ return keyword
36
+
37
+ def normalize_keyword_for_api(keyword):
38
+ """API ํ˜ธ์ถœ์šฉ ํ‚ค์›Œ๋“œ ์ •๊ทœํ™” (์ŠคํŽ˜์ด์Šค ์ œ๊ฑฐ)"""
39
+ normalized = normalize_keyword(keyword)
40
+ return normalized.replace(" ", "")
41
+
42
+ def normalize_keyword_for_comparison(keyword):
43
+ """๋น„๊ต์šฉ ํ‚ค์›Œ๋“œ ์ •๊ทœํ™” (์ŠคํŽ˜์ด์Šค ์œ ์ง€)"""
44
+ return normalize_keyword(keyword).lower()
45
+
46
+ def normalize_keyword_advanced(keyword):
47
+ """๊ณ ๊ธ‰ ํ‚ค์›Œ๋“œ ์ •๊ทœํ™” - ๋งค์นญ ๋ฌธ์ œ ํ•ด๊ฒฐ"""
48
+ if not keyword:
49
+ return ""
50
+
51
+ # 1. ๊ธฐ๋ณธ ์ •๋ฆฌ
52
+ keyword = str(keyword).strip()
53
+
54
+ # 2. ์—ฐ์†๋œ ๊ณต๋ฐฑ์„ ํ•˜๋‚˜๋กœ ๋ณ€๊ฒฝ
55
+ keyword = re.sub(r'\s+', ' ', keyword)
56
+
57
+ # 3. ํŠน์ˆ˜๋ฌธ์ž ์ œ๊ฑฐ (ํ•œ๊ธ€, ์˜๋ฌธ, ์ˆซ์ž, ๊ณต๋ฐฑ๋งŒ ๋‚จ๊น€)
58
+ keyword = re.sub(r'[^\w\s๊ฐ€-ํžฃ]', '', keyword)
59
+
60
+ # 4. ์†Œ๋ฌธ์ž ๋ณ€ํ™˜
61
+ keyword = keyword.lower()
62
+
63
+ return keyword
64
+
65
+ def create_keyword_variations(keyword):
66
+ """ํ‚ค์›Œ๋“œ ๋ณ€ํ˜• ๋ฒ„์ „๋“ค ์ƒ์„ฑ - ์ŠคํŽ˜์ด์Šค๋ฐ” ์ฒ˜๋ฆฌ ๊ฐ•ํ™”"""
67
+ base = normalize_keyword_advanced(keyword)
68
+ variations = [base]
69
+
70
+ # ์ŠคํŽ˜์ด์Šค ์ œ๊ฑฐ ๋ฒ„์ „
71
+ no_space = base.replace(" ", "")
72
+ if no_space != base:
73
+ variations.append(no_space)
74
+
75
+ # ์ŠคํŽ˜์ด์Šค๋ฅผ ๋‹ค๋ฅธ ๊ตฌ๋ถ„์ž๋กœ ๋ฐ”๊พผ ๋ฒ„์ „๋“ค
76
+ variations.append(base.replace(" ", "-"))
77
+ variations.append(base.replace(" ", "_"))
78
+
79
+ # ๋‹จ์–ด ์ˆœ์„œ ๋ฐ”๊พผ ๋ฒ„์ „ (2๋‹จ์–ด์ธ ๊ฒฝ์šฐ)
80
+ words = base.split()
81
+ if len(words) == 2:
82
+ reversed_keyword = f"{words[1]} {words[0]}"
83
+ variations.append(reversed_keyword)
84
+ variations.append(reversed_keyword.replace(" ", ""))
85
+
86
+ return list(set(variations)) # ์ค‘๋ณต ์ œ๊ฑฐ
87
+
88
+ def find_matching_keyword_row(analysis_keyword, keywords_df):
89
+ """๊ฐœ์„ ๋œ ํ‚ค์›Œ๋“œ ๋งค์นญ ํ•จ์ˆ˜"""
90
+ if keywords_df is None or keywords_df.empty:
91
+ return None
92
+
93
+ analysis_variations = create_keyword_variations(analysis_keyword)
94
+
95
+ logger.info(f"๋ถ„์„ ํ‚ค์›Œ๋“œ ๋ณ€ํ˜•๋“ค: {analysis_variations}")
96
+
97
+ # 1์ฐจ: ์ •ํ™•ํ•œ ๋งค์นญ
98
+ for idx, row in keywords_df.iterrows():
99
+ df_keyword = str(row.get('์กฐํ•ฉ ํ‚ค์›Œ๋“œ', ''))
100
+ df_variations = create_keyword_variations(df_keyword)
101
+
102
+ for analysis_var in analysis_variations:
103
+ for df_var in df_variations:
104
+ if analysis_var == df_var and len(analysis_var) > 1:
105
+ logger.info(f"์ •ํ™•ํ•œ ๋งค์นญ ์„ฑ๊ณต: '{analysis_keyword}' = '{df_keyword}'")
106
+ return row
107
+
108
+ # 2์ฐจ: ํฌํ•จ ๊ด€๊ณ„ ๋งค์นญ
109
+ for idx, row in keywords_df.iterrows():
110
+ df_keyword = str(row.get('์กฐํ•ฉ ํ‚ค์›Œ๋“œ', ''))
111
+ df_variations = create_keyword_variations(df_keyword)
112
+
113
+ for analysis_var in analysis_variations:
114
+ for df_var in df_variations:
115
+ if len(analysis_var) > 2 and len(df_var) > 2:
116
+ if analysis_var in df_var or df_var in analysis_var:
117
+ similarity = len(set(analysis_var) & set(df_var)) / len(set(analysis_var) | set(df_var))
118
+ if similarity > 0.7: # 70% ์ด์ƒ ์œ ์‚ฌ
119
+ logger.info(f"๋ถ€๋ถ„ ๋งค์นญ ์„ฑ๊ณต: '{analysis_keyword}' โ‰ˆ '{df_keyword}' (์œ ์‚ฌ๋„: {similarity:.2f})")
120
+ return row
121
+
122
+ logger.warning(f"ํ‚ค์›Œ๋“œ ๋งค์นญ ์‹คํŒจ: '{analysis_keyword}'")
123
+ logger.info(f"๋ฐ์ดํ„ฐํ”„๋ ˆ์ž„ ํ‚ค์›Œ๋“œ ์ƒ˜ํ”Œ: {keywords_df['์กฐํ•ฉ ํ‚ค์›Œ๋“œ'].head(5).tolist()}")
124
+ return None
125
+
126
+ def generate_prediction_data(trend_data_3year, keyword):
127
+ """
128
+ ์ •๊ตํ•œ ์˜ˆ์ƒ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ ํ•จ์ˆ˜
129
+ - ํŠธ๋ Œ๋“œ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ํ›„ ๋ฐ”๋กœ ํ˜ธ์ถœํ•˜์—ฌ ์˜ˆ์ƒ ๋ฐ์ดํ„ฐ ์ถ”๊ฐ€
130
+ - ๊ณ„์ ˆ์„ฑ, ์ฆ๊ฐ ํŠธ๋ Œ๋“œ, ์ „๋…„ ๋Œ€๋น„ ์„ฑ์žฅ๋ฅ  ๋ชจ๋‘ ๊ณ ๋ ค
131
+ """
132
+ if not trend_data_3year:
133
+ logger.warning("โŒ ์˜ˆ์ƒ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ ์‹คํŒจ: trend_data_3year ์—†์Œ")
134
+ return trend_data_3year
135
+
136
+ try:
137
+ current_date = datetime.now()
138
+ current_year = current_date.year
139
+ current_month = current_date.month
140
+
141
+ logger.info(f"๐Ÿ”ฎ ์˜ˆ์ƒ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ ์‹œ์ž‘: {keyword} ({current_year}๋…„ {current_month}์›” ๊ธฐ์ค€)")
142
+
143
+ for kw, data in trend_data_3year.items():
144
+ if not data or not data.get('monthly_volumes') or not data.get('dates'):
145
+ continue
146
+
147
+ volumes = data['monthly_volumes']
148
+ dates = data['dates']
149
+
150
+ # โœ… 1๋‹จ๊ณ„: ๊ธฐ์กด ๋ฐ์ดํ„ฐ ๋ถ„์„
151
+ yearly_data = {} # {year: {month: volume}}
152
+
153
+ for i, date_str in enumerate(dates):
154
+ try:
155
+ date_obj = datetime.strptime(date_str, "%Y-%m-%d")
156
+ if i < len(volumes):
157
+ volume = volumes[i]
158
+ if isinstance(volume, str):
159
+ volume = float(volume.replace(',', ''))
160
+ volume = int(volume) if volume else 0
161
+
162
+ year = date_obj.year
163
+ month = date_obj.month
164
+
165
+ if year not in yearly_data:
166
+ yearly_data[year] = {}
167
+ yearly_data[year][month] = volume
168
+
169
+ except Exception as e:
170
+ logger.warning(f"โš ๏ธ ๋‚ ์งœ ํŒŒ์‹ฑ ์˜ค๋ฅ˜: {date_str}")
171
+ continue
172
+
173
+ logger.info(f"๐Ÿ“Š ๋ถ„์„๋œ ์—ฐ๋„: {list(yearly_data.keys())}")
174
+
175
+ # โœ… 2๋‹จ๊ณ„: ์˜ˆ์ƒ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ ์•Œ๊ณ ๋ฆฌ์ฆ˜
176
+ if current_year not in yearly_data:
177
+ yearly_data[current_year] = {}
178
+
179
+ current_year_data = yearly_data[current_year]
180
+ last_year_data = yearly_data.get(current_year - 1, {})
181
+ two_years_ago_data = yearly_data.get(current_year - 2, {})
182
+
183
+ logger.info(f"๐Ÿ“ˆ ์˜ฌํ•ด ์‹ค์ œ ๋ฐ์ดํ„ฐ: {len(current_year_data)}๊ฐœ์›”")
184
+ logger.info(f"๐Ÿ“ˆ ์ž‘๋…„ ์ฐธ์กฐ ๋ฐ์ดํ„ฐ: {len(last_year_data)}๊ฐœ์›”")
185
+
186
+ # ์˜ˆ์ƒ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ (ํ˜„์žฌ์›” ์ดํ›„)
187
+ for future_month in range(current_month + 1, 13):
188
+ if future_month in current_year_data:
189
+ continue # ์ด๋ฏธ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์œผ๋ฉด ์Šคํ‚ต
190
+
191
+ predicted_volume = calculate_predicted_volume(
192
+ future_month, current_year_data, last_year_data,
193
+ two_years_ago_data, current_month
194
+ )
195
+
196
+ if predicted_volume is not None:
197
+ current_year_data[future_month] = predicted_volume
198
+ logger.info(f"๐Ÿ”ฎ ์˜ˆ์ƒ ์ƒ์„ฑ: {current_year}๋…„ {future_month}์›” = {predicted_volume:,}ํšŒ")
199
+
200
+ # โœ… 3๋‹จ๊ณ„: ์ƒ์„ฑ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ์›๋ณธ ๊ตฌ์กฐ์— ํ†ตํ•ฉ
201
+ updated_volumes = []
202
+ updated_dates = []
203
+
204
+ # ์‹œ๊ฐ„์ˆœ์œผ๋กœ ์ •๋ ฌํ•˜์—ฌ ํ†ตํ•ฉ
205
+ all_months = []
206
+ for year in sorted(yearly_data.keys()):
207
+ for month in sorted(yearly_data[year].keys()):
208
+ all_months.append((year, month, yearly_data[year][month]))
209
+
210
+ for year, month, volume in all_months:
211
+ updated_volumes.append(volume)
212
+ updated_dates.append(f"{year}-{month:02d}-01")
213
+
214
+ # ์›๋ณธ ๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ
215
+ data['monthly_volumes'] = updated_volumes
216
+ data['dates'] = updated_dates
217
+
218
+ logger.info(f"โœ… {kw} ์˜ˆ์ƒ ๋ฐ์ดํ„ฐ ํ†ตํ•ฉ ์™„๋ฃŒ: ์ด {len(updated_volumes)}๊ฐœ์›”")
219
+
220
+ logger.info(f"๐ŸŽ‰ ์ „์ฒด ์˜ˆ์ƒ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ ์™„๋ฃŒ: {keyword}")
221
+ return trend_data_3year
222
+
223
+ except Exception as e:
224
+ logger.error(f"โŒ ์˜ˆ์ƒ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ ์˜ค๋ฅ˜: {e}")
225
+ return trend_data_3year
226
+
227
+ def calculate_predicted_volume(target_month, current_year_data, last_year_data,
228
+ two_years_ago_data, current_month):
229
+ """
230
+ ์ •๊ตํ•œ ์˜ˆ์ƒ ๋ณผ๋ฅจ ๊ณ„์‚ฐ
231
+ - ๋‹ค์ค‘ ์š”์ธ ๊ณ ๋ ค: ์ž‘๋…„ ๋™์›”, ์ฆ๊ฐ ํŠธ๋ Œ๋“œ, ๊ณ„์ ˆ์„ฑ, ์„ฑ์žฅ๋ฅ 
232
+ """
233
+ try:
234
+ # ๊ธฐ์ค€ ๊ฐ’๋“ค
235
+ last_year_same_month = last_year_data.get(target_month, 0)
236
+ two_years_ago_same_month = two_years_ago_data.get(target_month, 0)
237
+
238
+ if last_year_same_month == 0:
239
+ logger.warning(f"โš ๏ธ {target_month}์›” ์ž‘๋…„ ๋ฐ์ดํ„ฐ ์—†์Œ")
240
+ return None
241
+
242
+ # โœ… 1. ๊ธฐ๋ณธ๊ฐ’: ์ž‘๋…„ ๋™์›”
243
+ base_volume = last_year_same_month
244
+
245
+ # โœ… 2. ์ „๋…„ ๋Œ€๋น„ ์„ฑ์žฅ๋ฅ  ๊ณ„์‚ฐ (๊ฐ€๋Šฅํ•œ ๊ฒฝ์šฐ)
246
+ growth_rate = 1.0
247
+ if two_years_ago_same_month > 0:
248
+ growth_rate = last_year_same_month / two_years_ago_same_month
249
+ logger.info(f"๐Ÿ“ˆ {target_month}์›” ์ „๋…„ ์„ฑ์žฅ๋ฅ : {growth_rate:.2f}๋ฐฐ")
250
+
251
+ # โœ… 3. ์˜ฌํ•ด ์ตœ๊ทผ ํŠธ๋ Œ๋“œ ๋ฐ˜์˜
252
+ trend_factor = 1.0
253
+ if len(current_year_data) >= 2:
254
+ # ์ตœ๊ทผ 2-3๊ฐœ์›”์˜ ์ž‘๋…„ ๋Œ€๋น„ ๋น„์œจ ๊ณ„์‚ฐ
255
+ recent_ratios = []
256
+ for month in range(max(1, current_month - 2), current_month + 1):
257
+ if month in current_year_data and month in last_year_data:
258
+ if last_year_data[month] > 0:
259
+ ratio = current_year_data[month] / last_year_data[month]
260
+ recent_ratios.append(ratio)
261
+
262
+ if recent_ratios:
263
+ trend_factor = sum(recent_ratios) / len(recent_ratios)
264
+ logger.info(f"๐Ÿ“Š ์ตœ๊ทผ ํŠธ๋ Œ๋“œ ํŒฉํ„ฐ: {trend_factor:.2f}")
265
+
266
+ # โœ… 4. ๊ณ„์ ˆ์„ฑ ๋ณด์ • (๊ฐ™์€ ๋ถ„๊ธฐ ๋‚ด ์›”๊ฐ„ ํŒจํ„ด)
267
+ seasonal_factor = 1.0
268
+ if target_month > 1 and target_month - 1 in last_year_data and target_month in last_year_data:
269
+ # ์ž‘๋…„ ๋™์ผ ๊ตฌ๊ฐ„์˜ ์›”๊ฐ„ ๋ณ€ํ™”์œจ
270
+ if last_year_data[target_month - 1] > 0:
271
+ seasonal_factor = last_year_data[target_month] / last_year_data[target_month - 1]
272
+ logger.info(f"๐ŸŒŠ {target_month}์›” ๊ณ„์ ˆ์„ฑ ํŒฉํ„ฐ: {seasonal_factor:.2f}")
273
+
274
+ # โœ… 5. ์ตœ์ข… ์˜ˆ์ƒ๊ฐ’ ๊ณ„์‚ฐ (๊ฐ€์ค‘ํ‰๊ท )
275
+ predicted_volume = int(
276
+ base_volume * (
277
+ 0.4 * growth_rate + # 40% ์ „๋…„ ์„ฑ์žฅ๋ฅ 
278
+ 0.4 * trend_factor + # 40% ์ตœ๊ทผ ํŠธ๋ Œ๋“œ
279
+ 0.2 * seasonal_factor # 20% ๊ณ„์ ˆ์„ฑ
280
+ )
281
+ )
282
+
283
+ # โœ… 6. ํ•ฉ๋ฆฌ์„ฑ ๊ฒ€์ฆ (๊ธ‰๊ฒฉํ•œ ๋ณ€ํ™” ๋ฐฉ์ง€)
284
+ if current_year_data:
285
+ recent_avg = sum(current_year_data.values()) / len(current_year_data)
286
+ if predicted_volume > recent_avg * 5: # 5๋ฐฐ ์ด์ƒ ๊ธ‰์ฆ ๋ฐฉ์ง€
287
+ predicted_volume = int(recent_avg * 2)
288
+ logger.warning(f"โš ๏ธ {target_month}์›” ๊ธ‰์ฆ ๋ณด์ •: {predicted_volume:,}ํšŒ")
289
+ elif predicted_volume < recent_avg * 0.1: # 10๋ถ„์˜ 1 ์ดํ•˜ ๊ธ‰๊ฐ ๋ฐฉ์ง€
290
+ predicted_volume = int(recent_avg * 0.5)
291
+ logger.warning(f"โš ๏ธ {target_month}์›” ๊ธ‰๊ฐ ๋ณด์ •: {predicted_volume:,}ํšŒ")
292
+
293
+ logger.info(f"๐ŸŽฏ {target_month}์›” ์˜ˆ์ƒ ๊ณ„์‚ฐ: {last_year_same_month:,} ร— (์„ฑ์žฅ{growth_rate:.2f} + ํŠธ๋ Œ๋“œ{trend_factor:.2f} + ๊ณ„์ ˆ{seasonal_factor:.2f}) = {predicted_volume:,}")
294
+
295
+ return predicted_volume
296
+
297
+ except Exception as e:
298
+ logger.error(f"โŒ {target_month}์›” ์˜ˆ์ƒ ๊ณ„์‚ฐ ์˜ค๋ฅ˜: {e}")
299
+ return None
300
+
301
+ def enhance_trend_data_with_predictions(trend_data_3year, keyword):
302
+ """
303
+ ๊ธฐ์กด ํŠธ๋ Œ๋“œ ๋ฐ์ดํ„ฐ์— ์˜ˆ์ƒ ๋ฐ์ดํ„ฐ ์ถ”๊ฐ€
304
+ - ๋ฉ”์ธ ํŠธ๋ Œ๋“œ ์ˆ˜์ง‘ ํ•จ์ˆ˜์—์„œ ํ˜ธ์ถœ
305
+ """
306
+ if not trend_data_3year:
307
+ return trend_data_3year
308
+
309
+ logger.info(f"๐Ÿš€ ํŠธ๋ Œ๋“œ ๋ฐ์ดํ„ฐ ์˜ˆ์ƒ ํ™•์žฅ ์‹œ์ž‘: {keyword}")
310
+
311
+ enhanced_data = generate_prediction_data(trend_data_3year, keyword)
312
+
313
+ # ๋ฐ์ดํ„ฐ ํ’ˆ์งˆ ๊ฒ€์ฆ
314
+ for kw, data in enhanced_data.items():
315
+ if data and data.get('monthly_volumes'):
316
+ total_months = len(data['monthly_volumes'])
317
+ current_year = datetime.now().year
318
+
319
+ # ํ˜„์žฌ ์—ฐ๋„ ๋ฐ์ดํ„ฐ ๊ฐœ์ˆ˜ ํ™•์ธ
320
+ current_year_count = 0
321
+ for date_str in data['dates']:
322
+ try:
323
+ if date_str.startswith(str(current_year)):
324
+ current_year_count += 1
325
+ except:
326
+ continue
327
+
328
+ logger.info(f"โœ… {kw} ์ตœ์ข… ๋ฐ์ดํ„ฐ: ์ „์ฒด {total_months}๊ฐœ์›”, ์˜ฌํ•ด {current_year_count}๊ฐœ์›”")
329
+
330
+ return enhanced_data
331
+
332
+ def calculate_max_growth_rate_with_predictions(trend_data_3year, keyword):
333
+ """์˜ฌ๋ฐ”๋ฅธ ํŠธ๋ Œ๋“œ ๋ถ„์„ ๋กœ์ง - ์‚ฌ์šฉ์ž ์š”๊ตฌ์‚ฌํ•ญ ์ ์šฉ"""
334
+ if not trend_data_3year:
335
+ logger.error("โŒ trend_data_3year๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค")
336
+ return "๋ฐ์ดํ„ฐ ์—†์Œ"
337
+
338
+ try:
339
+ keyword_data = None
340
+ for kw, data in trend_data_3year.items():
341
+ keyword_data = data
342
+ logger.info(f"๐Ÿ” ํ‚ค์›Œ๋“œ ๋ฐ์ดํ„ฐ ๋ฐœ๊ฒฌ: {kw}")
343
+ break
344
+
345
+ if not keyword_data or not keyword_data.get('monthly_volumes') or not keyword_data.get('dates'):
346
+ logger.error("โŒ keyword_data ๊ตฌ์กฐ ๋ฌธ์ œ")
347
+ return "๋ฐ์ดํ„ฐ ์—†์Œ"
348
+
349
+ volumes = keyword_data['monthly_volumes']
350
+ dates = keyword_data['dates']
351
+
352
+ # 1๋‹จ๊ณ„: ํ˜„์žฌ ์‹œ์  ํŒŒ์•…
353
+ current_date = datetime.now()
354
+ current_year = current_date.year
355
+ current_month = current_date.month
356
+ current_day = current_date.day
357
+
358
+ # ์™„๋ฃŒ๋œ ๋งˆ์ง€๋ง‰ ์›” ๊ณ„์‚ฐ (2์ผ ์ดํ›„๋ฉด ์ „์›”๊นŒ์ง€ ์™„๋ฃŒ)
359
+ if current_day >= 2:
360
+ completed_year = current_year
361
+ completed_month = current_month - 1
362
+ else:
363
+ completed_year = current_year
364
+ completed_month = current_month - 2
365
+
366
+ # ์›”์ด 0 ์ดํ•˜๊ฐ€ ๋˜๋ฉด ์—ฐ๋„ ์กฐ์ •
367
+ while completed_month <= 0:
368
+ completed_month += 12
369
+ completed_year -= 1
370
+
371
+ logger.info(f"๐Ÿ“… ํ˜„์žฌ: {current_year}๋…„ {current_month}์›” {current_day}์ผ")
372
+ logger.info(f"๐Ÿ“Š ์™„๋ฃŒ๋œ ๋งˆ์ง€๋ง‰ ๋ฐ์ดํ„ฐ: {completed_year}๋…„ {completed_month}์›”")
373
+
374
+ # 2๋‹จ๊ณ„: ๋ฐ์ดํ„ฐ ๋ถ„๋ฅ˜ ๋ฐ ์ˆ˜์ง‘
375
+ all_data = []
376
+
377
+ for i, date_str in enumerate(dates):
378
+ try:
379
+ date_obj = datetime.strptime(date_str, "%Y-%m-%d")
380
+
381
+ if i < len(volumes):
382
+ volume = volumes[i]
383
+ if isinstance(volume, str):
384
+ volume = float(volume.replace(',', ''))
385
+ volume = int(volume) if volume else 0
386
+
387
+ all_data.append({
388
+ 'year': date_obj.year,
389
+ 'month': date_obj.month,
390
+ 'volume': volume,
391
+ 'date_str': date_str,
392
+ 'date_obj': date_obj,
393
+ 'sort_key': f"{date_obj.year:04d}{date_obj.month:02d}"
394
+ })
395
+
396
+ except Exception as e:
397
+ logger.warning(f"โš ๏ธ ๋‚ ์งœ ํŒŒ์‹ฑ ์˜ค๋ฅ˜: {date_str} - {e}")
398
+ continue
399
+
400
+ # ์‹œ๊ฐ„ ์ˆœ์„œ๋Œ€๋กœ ์ •๋ ฌ
401
+ all_data = sorted(all_data, key=lambda x: x['sort_key'])
402
+
403
+ # 3๋‹จ๊ณ„: ์ฆ๊ฐ์œจ ๊ณ„์‚ฐ (์˜ฌํ•ด ์™„๋ฃŒ์›” vs ์ž‘๋…„ ๋™์›”)
404
+ this_year_completed_volume = None
405
+ last_year_same_month_volume = None
406
+
407
+ for data in all_data:
408
+ # ์˜ฌํ•ด ์™„๋ฃŒ๋œ ๋งˆ์ง€๋ง‰ ์›” ์ฐพ๊ธฐ
409
+ if data['year'] == completed_year and data['month'] == completed_month:
410
+ this_year_completed_volume = data['volume']
411
+ logger.info(f"๐Ÿ“Š ์˜ฌํ•ด {completed_month}์›” ์‹ค๋ฐ์ดํ„ฐ: {this_year_completed_volume:,}ํšŒ")
412
+
413
+ # ์ž‘๋…„ ๋™์›” ์ฐพ๊ธฐ
414
+ if data['year'] == completed_year - 1 and data['month'] == completed_month:
415
+ last_year_same_month_volume = data['volume']
416
+ logger.info(f"๐Ÿ“Š ์ž‘๋…„ {completed_month}์›” ์‹ค๋ฐ์ดํ„ฐ: {last_year_same_month_volume:,}ํšŒ")
417
+
418
+ # ์ฆ๊ฐ์œจ ๊ณ„์‚ฐ
419
+ growth_rate = 0
420
+ if this_year_completed_volume is not None and last_year_same_month_volume is not None and last_year_same_month_volume > 0:
421
+ growth_rate = (this_year_completed_volume - last_year_same_month_volume) / last_year_same_month_volume
422
+ logger.info(f"๐Ÿ“ˆ ๊ณ„์‚ฐ๋œ ์ฆ๊ฐ์œจ: {growth_rate:+.3f} ({growth_rate * 100:+.1f}%)")
423
+ else:
424
+ logger.warning("โš ๏ธ ์ฆ๊ฐ์œจ ๊ณ„์‚ฐ์„ ์œ„ํ•œ ๋ฐ์ดํ„ฐ๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค.")
425
+
426
+ # 4๋‹จ๊ณ„: ์˜ˆ์ƒ๋ฐ์ดํ„ฐ ์ƒ์„ฑ (ํ˜„์žฌ์›” ์ดํ›„)
427
+ combined_data = []
428
+ month_names = ["", "1์›”", "2์›”", "3์›”", "4์›”", "5์›”", "6์›”", "7์›”", "8์›”", "9์›”", "10์›”", "11์›”", "12์›”"]
429
+
430
+ # ์ž‘๋…„ 12์›” ๋ฐ์ดํ„ฐ ์ถ”๊ฐ€ (์—ฐ์†์„ฑ์„ ์œ„ํ•ด)
431
+ for data in all_data:
432
+ if data['year'] == completed_year - 1 and data['month'] == 12:
433
+ combined_data.append({
434
+ 'year': data['year'],
435
+ 'month': data['month'],
436
+ 'volume': data['volume'],
437
+ 'data_type': '์ž‘๋…„์‹ค์ œ',
438
+ 'sort_key': f"{data['year']:04d}{data['month']:02d}"
439
+ })
440
+ logger.info(f"๐Ÿ”— ์ž‘๋…„ 12์›” ์‹ค๋ฐ์ดํ„ฐ: {data['volume']:,}ํšŒ")
441
+ break
442
+
443
+ # ์˜ฌํ•ด 1์›”๋ถ€ํ„ฐ ์™„๋ฃŒ์›”๊นŒ์ง€ ์‹ค์ œ๋ฐ์ดํ„ฐ ์ถ”๊ฐ€
444
+ for month in range(1, completed_month + 1):
445
+ for data in all_data:
446
+ if data['year'] == completed_year and data['month'] == month:
447
+ combined_data.append({
448
+ 'year': data['year'],
449
+ 'month': data['month'],
450
+ 'volume': data['volume'],
451
+ 'data_type': '์‹ค์ œ',
452
+ 'sort_key': f"{data['year']:04d}{data['month']:02d}"
453
+ })
454
+ logger.info(f"๐Ÿ“Š {month}์›” ์‹ค๋ฐ์ดํ„ฐ: {data['volume']:,}ํšŒ")
455
+ break
456
+
457
+ # ์˜ฌํ•ด ๋ฏธ์™„๋ฃŒ์›”(ํ˜„์žฌ์›”+1 ~ 12์›”) ์˜ˆ์ƒ๋ฐ์ดํ„ฐ ์ƒ์„ฑ
458
+ for month in range(completed_month + 1, 13):
459
+ # ์ž‘๋…„ ๋™์›” ๋ฐ์ดํ„ฐ ์ฐพ๊ธฐ
460
+ last_year_volume = None
461
+ for data in all_data:
462
+ if data['year'] == completed_year - 1 and data['month'] == month:
463
+ last_year_volume = data['volume']
464
+ break
465
+
466
+ if last_year_volume is not None:
467
+ # ์˜ˆ์ƒ ๊ฒ€์ƒ‰๋Ÿ‰ = ์ž‘๋…„ ๋™์›” ร— (1 + ์ฆ๊ฐ์œจ)
468
+ predicted_volume = int(last_year_volume * (1 + growth_rate))
469
+ predicted_volume = max(predicted_volume, 0) # ์Œ์ˆ˜ ๋ฐฉ์ง€
470
+
471
+ combined_data.append({
472
+ 'year': completed_year,
473
+ 'month': month,
474
+ 'volume': predicted_volume,
475
+ 'data_type': '์˜ˆ์ƒ',
476
+ 'sort_key': f"{completed_year:04d}{month:02d}"
477
+ })
478
+
479
+ logger.info(f"๐Ÿ”ฎ {month}์›” ์˜ˆ์ƒ๋ฐ์ดํ„ฐ: {predicted_volume:,}ํšŒ (์ž‘๋…„ {last_year_volume:,}ํšŒ ร— {1 + growth_rate:.3f})")
480
+
481
+ # ์‹œ๊ฐ„ ์ˆœ์„œ๋Œ€๋กœ ์ •๋ ฌ
482
+ combined_data = sorted(combined_data, key=lambda x: x['sort_key'])
483
+
484
+ # 5๋‹จ๊ณ„: ๐Ÿ”– ๊ฐ€์žฅ ์ƒ์Šนํญ์ด ๋†’์€ ์›” ์ฐพ๊ธฐ (์—ฐ์†๋œ ์›”๊ฐ„ ์ƒ์Šน๋ฅ )
485
+ max_growth_rate = 0
486
+ max_growth_info = "๋ฐ์ดํ„ฐ ์—†์Œ"
487
+
488
+ for i in range(len(combined_data) - 1):
489
+ start_data = combined_data[i]
490
+ end_data = combined_data[i + 1]
491
+
492
+ if start_data['volume'] > 0:
493
+ month_growth_rate = ((end_data['volume'] - start_data['volume']) / start_data['volume']) * 100
494
+
495
+ # ์ƒ์Šนํ•œ ๊ฒฝ์šฐ๋งŒ ๊ณ ๋ ค
496
+ if month_growth_rate > max_growth_rate:
497
+ max_growth_rate = month_growth_rate
498
+
499
+ start_month_name = month_names[start_data['month']]
500
+ end_month_name = month_names[end_data['month']]
501
+
502
+ # ์—ฐ๋„ ์ „ํ™˜ ๊ณ ๋ ค
503
+ if start_data['year'] != end_data['year']:
504
+ period_desc = f"{start_data['year']}๋…„ {start_month_name}({start_data['volume']:,}ํšŒ)์—์„œ {end_data['year']}๋…„ {end_month_name}({end_data['volume']:,}ํšŒ)์œผ๋กœ"
505
+ else:
506
+ period_desc = f"{start_month_name}({start_data['volume']:,}ํšŒ)์—์„œ {end_month_name}({end_data['volume']:,}ํšŒ)์œผ๋กœ"
507
+
508
+ # ๋ฐ์ดํ„ฐ ์œ ํ˜• ํŒ๋‹จ
509
+ if start_data['data_type'] in ['์˜ˆ์ƒ'] and end_data['data_type'] in ['์˜ˆ์ƒ']:
510
+ data_type = "์˜ˆ์ƒ ๊ธฐ๋ฐ˜"
511
+ elif start_data['data_type'] in ['์‹ค์ œ', '์ž‘๋…„์‹ค์ œ'] and end_data['data_type'] in ['์‹ค์ œ', '์ž‘๋…„์‹ค์ œ']:
512
+ data_type = "์‹ค์ œ ๊ธฐ๋ฐ˜"
513
+ else:
514
+ data_type = "์‹ค์ œโ†’์˜ˆ์ƒ ๊ธฐ๋ฐ˜"
515
+
516
+ max_growth_info = f"{period_desc} {max_growth_rate:.1f}% ์ƒ์Šน ({data_type})"
517
+
518
+ # ์ƒ์Šน ๊ตฌ๊ฐ„์ด ์—†๋Š” ๊ฒฝ์šฐ ์ตœ์†Œ ํ•˜๋ฝ๋ฅ  ํ‘œ์‹œ
519
+ if max_growth_rate == 0:
520
+ min_decline_rate = float('inf')
521
+ for i in range(len(combined_data) - 1):
522
+ start_data = combined_data[i]
523
+ end_data = combined_data[i + 1]
524
+
525
+ if start_data['volume'] > 0:
526
+ month_growth_rate = ((end_data['volume'] - start_data['volume']) / start_data['volume']) * 100
527
+
528
+ if abs(month_growth_rate) < abs(min_decline_rate):
529
+ min_decline_rate = month_growth_rate
530
+
531
+ start_month_name = month_names[start_data['month']]
532
+ end_month_name = month_names[end_data['month']]
533
+
534
+ if start_data['year'] != end_data['year']:
535
+ period_desc = f"{start_data['year']}๋…„ {start_month_name}({start_data['volume']:,}ํšŒ)์—์„œ {end_data['year']}๋…„ {end_month_name}({end_data['volume']:,}ํšŒ)์œผ๋กœ"
536
+ else:
537
+ period_desc = f"{start_month_name}({start_data['volume']:,}ํšŒ)์—์„œ {end_month_name}({end_data['volume']:,}ํšŒ)์œผ๋กœ"
538
+
539
+ if start_data['data_type'] in ['์˜ˆ์ƒ'] and end_data['data_type'] in ['์˜ˆ์ƒ']:
540
+ data_type = "์˜ˆ์ƒ ๊ธฐ๋ฐ˜"
541
+ elif start_data['data_type'] in ['์‹ค์ œ', '์ž‘๋…„์‹ค์ œ'] and end_data['data_type'] in ['์‹ค์ œ', '์ž‘๋…„์‹ค์ œ']:
542
+ data_type = "์‹ค์ œ ๊ธฐ๋ฐ˜"
543
+ else:
544
+ data_type = "์‹ค์ œโ†’์˜ˆ์ƒ ๊ธฐ๋ฐ˜"
545
+
546
+ max_growth_info = f"{period_desc} {abs(min_decline_rate):.1f}% ๊ฐ์†Œ ({data_type})"
547
+
548
+ logger.info(f"๐Ÿ† ๊ฐ€์žฅ ์ƒ์Šนํญ์ด ๋†’์€ ์›”: {max_growth_info}")
549
+ return max_growth_info
550
+
551
+ except Exception as e:
552
+ logger.error(f"โŒ ์ƒ์Šนํญ ๊ณ„์‚ฐ ์˜ค๋ฅ˜: {e}")
553
+ import traceback
554
+ logger.error(f"โŒ ์Šคํƒ ํŠธ๋ ˆ์ด์Šค: {traceback.format_exc()}")
555
+ return "๊ณ„์‚ฐ ์˜ค๋ฅ˜"
556
+
557
+ def get_peak_month_with_predictions(trend_data_3year, keyword):
558
+ """๐Ÿ”– ๊ฐ€์žฅ ๊ฒ€์ƒ‰๋Ÿ‰์ด ๋งŽ์€ ์›” ์ฐพ๊ธฐ - ์‹ค์ œ+์˜ˆ์ƒ ๋ฐ์ดํ„ฐ ํ™œ์šฉ"""
559
+ if not trend_data_3year:
560
+ return "์—ฐ์ค‘"
561
+
562
+ try:
563
+ keyword_data = None
564
+ for kw, data in trend_data_3year.items():
565
+ keyword_data = data
566
+ break
567
+
568
+ if not keyword_data or not keyword_data.get('monthly_volumes') or not keyword_data.get('dates'):
569
+ return "์—ฐ์ค‘"
570
+
571
+ volumes = keyword_data['monthly_volumes']
572
+ dates = keyword_data['dates']
573
+
574
+ # ํ˜„์žฌ ์‹œ์  ํŒŒ์•…
575
+ current_date = datetime.now()
576
+ current_year = current_date.year
577
+ current_month = current_date.month
578
+ current_day = current_date.day
579
+
580
+ # ์™„๋ฃŒ๋œ ๋งˆ์ง€๋ง‰ ์›” ๊ณ„์‚ฐ
581
+ if current_day >= 2:
582
+ completed_year = current_year
583
+ completed_month = current_month - 1
584
+ else:
585
+ completed_year = current_year
586
+ completed_month = current_month - 2
587
+
588
+ while completed_month <= 0:
589
+ completed_month += 12
590
+ completed_year -= 1
591
+
592
+ # ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘
593
+ all_data = []
594
+ for i, date_str in enumerate(dates):
595
+ try:
596
+ date_obj = datetime.strptime(date_str, "%Y-%m-%d")
597
+ if i < len(volumes):
598
+ volume = volumes[i]
599
+ if isinstance(volume, str):
600
+ volume = float(volume.replace(',', ''))
601
+ volume = int(volume) if volume else 0
602
+
603
+ all_data.append({
604
+ 'year': date_obj.year,
605
+ 'month': date_obj.month,
606
+ 'volume': volume
607
+ })
608
+ except:
609
+ continue
610
+
611
+ # ์ฆ๊ฐ์œจ ๊ณ„์‚ฐ
612
+ this_year_completed_volume = None
613
+ last_year_same_month_volume = None
614
+
615
+ for data in all_data:
616
+ if data['year'] == completed_year and data['month'] == completed_month:
617
+ this_year_completed_volume = data['volume']
618
+ if data['year'] == completed_year - 1 and data['month'] == completed_month:
619
+ last_year_same_month_volume = data['volume']
620
+
621
+ growth_rate = 0
622
+ if this_year_completed_volume is not None and last_year_same_month_volume is not None and last_year_same_month_volume > 0:
623
+ growth_rate = (this_year_completed_volume - last_year_same_month_volume) / last_year_same_month_volume
624
+
625
+ # ์˜ฌํ•ด ๋ฐ์ดํ„ฐ ์ค€๋น„ (์‹ค์ œ + ์˜ˆ์ƒ)
626
+ year_data = []
627
+ month_names = ["", "1์›”", "2์›”", "3์›”", "4์›”", "5์›”", "6์›”", "7์›”", "8์›”", "9์›”", "10์›”", "11์›”", "12์›”"]
628
+
629
+ # ์‹ค์ œ ๋ฐ์ดํ„ฐ ์ถ”๊ฐ€ (1์›”~์™„๋ฃŒ์›”)
630
+ for month in range(1, completed_month + 1):
631
+ for data in all_data:
632
+ if data['year'] == completed_year and data['month'] == month:
633
+ year_data.append({
634
+ 'month': month,
635
+ 'volume': data['volume'],
636
+ 'data_type': '์‹ค์ œ'
637
+ })
638
+ break
639
+
640
+ # ์˜ˆ์ƒ ๋ฐ์ดํ„ฐ ์ถ”๊ฐ€ (์™„๋ฃŒ์›”+1~12์›”)
641
+ for month in range(completed_month + 1, 13):
642
+ last_year_volume = None
643
+ for data in all_data:
644
+ if data['year'] == completed_year - 1 and data['month'] == month:
645
+ last_year_volume = data['volume']
646
+ break
647
+
648
+ if last_year_volume is not None:
649
+ predicted_volume = int(last_year_volume * (1 + growth_rate))
650
+ predicted_volume = max(predicted_volume, 0)
651
+
652
+ year_data.append({
653
+ 'month': month,
654
+ 'volume': predicted_volume,
655
+ 'data_type': '์˜ˆ์ƒ'
656
+ })
657
+
658
+ # ๊ฐ€์žฅ ๋†’์€ ๊ฒ€์ƒ‰๋Ÿ‰ ์ฐพ๊ธฐ
659
+ if not year_data:
660
+ return "์—ฐ์ค‘"
661
+
662
+ max_data = max(year_data, key=lambda x: x['volume'])
663
+ month_name = month_names[max_data['month']]
664
+ data_type_suffix = " - ์˜ˆ์ƒ" if max_data['data_type'] == '์˜ˆ์ƒ' else ""
665
+
666
+ return f"{month_name}({max_data['volume']:,}ํšŒ){data_type_suffix}"
667
+
668
+ except Exception as e:
669
+ logger.error(f"ํ”ผํฌ์›” ๋ถ„์„ ์˜ค๋ฅ˜: {e}")
670
+ return "์—ฐ์ค‘"
671
+
672
+ def calculate_3year_growth_rate_improved(volumes):
673
+ """์ž‘๋…„๋Œ€๋น„ ์ฆ๊ฐ์œจ ๊ณ„์‚ฐ (3๋…„ ๋ฐ์ดํ„ฐ์šฉ)"""
674
+ if len(volumes) < 24:
675
+ return 0
676
+
677
+ try:
678
+ # ์ฒซ ํ•ด์™€ ๋งˆ์ง€๋ง‰ ํ•ด ๋น„๊ต
679
+ first_year = volumes[:12]
680
+ last_year = volumes[-12:]
681
+
682
+ first_year_avg = sum(first_year) / len(first_year)
683
+ last_year_avg = sum(last_year) / len(last_year)
684
+
685
+ if first_year_avg == 0:
686
+ return 0
687
+
688
+ growth_rate = ((last_year_avg - first_year_avg) / first_year_avg) * 100
689
+ return min(max(growth_rate, -50), 200) # -50% ~ 200% ๋ฒ”์œ„๋กœ ์ œํ•œ
690
+
691
+ except Exception as e:
692
+ logger.error(f"์ž‘๋…„๋Œ€๋น„ ์ฆ๊ฐ์œจ ๊ณ„์‚ฐ ์˜ค๋ฅ˜: {e}")
693
+ return 0
694
+
695
+ def calculate_max_growth_rate_pure_logic(trend_data_3year, keyword):
696
+ """์ˆœ์ˆ˜ ๋กœ์ง์œผ๋กœ ์ตœ๋Œ€ ์ƒ์Šนํญ ๊ณ„์‚ฐ - ์˜ˆ์ƒ ๋ฐ์ดํ„ฐ ํฌํ•จ ๋ฒ„์ „"""
697
+ return calculate_max_growth_rate_with_predictions(trend_data_3year, keyword)
698
+
699
+ def analyze_season_cycle_with_llm(trend_data_3year, keyword, total_volume, gemini_model):
700
+ """LLM์„ ์ด์šฉํ•œ ์‹œ์ฆŒ ์ƒํ’ˆ ์†Œ์‹ฑ ์‚ฌ์ดํด ๋ถ„์„"""
701
+ if not trend_data_3year or not gemini_model:
702
+ return "๋น„์‹œ์ฆŒ์ƒํ’ˆ", "์–ธ์ œ๋“ ์ง€ ์ง„์ž… ๊ฐ€๋Šฅ", "๋ฐ์ดํ„ฐ ๋ถ€์กฑ"
703
+
704
+ try:
705
+ keyword_data = None
706
+ for kw, data in trend_data_3year.items():
707
+ keyword_data = data
708
+ break
709
+
710
+ if not keyword_data or not keyword_data.get('monthly_volumes'):
711
+ return "๋น„์‹œ์ฆŒ์ƒํ’ˆ", "์–ธ์ œ๋“ ์ง€ ์ง„์ž… ๊ฐ€๋Šฅ", "๋ฐ์ดํ„ฐ ๋ถ€์กฑ"
712
+
713
+ volumes = keyword_data['monthly_volumes']
714
+ dates = keyword_data['dates']
715
+
716
+ recent_12_volumes = volumes[-12:] if len(volumes) >= 12 else volumes
717
+ recent_12_dates = dates[-12:] if len(dates) >= 12 else dates
718
+
719
+ if len(recent_12_volumes) < 12:
720
+ return "๋น„์‹œ์ฆŒ์ƒํ’ˆ", "์–ธ์ œ๋“ ์ง€ ์ง„์ž… ๊ฐ€๋Šฅ", "๋ฐ์ดํ„ฐ ๋ถ€์กฑ"
721
+
722
+ monthly_data_str = ""
723
+ max_volume = 0
724
+ max_month = ""
725
+ for i, (date, volume) in enumerate(zip(recent_12_dates, recent_12_volumes)):
726
+ try:
727
+ date_obj = datetime.strptime(date, "%Y-%m-%d")
728
+ month_name = f"{date_obj.year}๋…„ {date_obj.month}์›”"
729
+ monthly_data_str += f"{month_name}: {volume:,}ํšŒ\n"
730
+
731
+ # ์ตœ๋Œ€ ๊ฒ€์ƒ‰๋Ÿ‰ ์›” ์ฐพ๊ธฐ
732
+ if volume > max_volume:
733
+ max_volume = volume
734
+ max_month = f"{date_obj.month}์›”({volume:,}ํšŒ)"
735
+
736
+ except:
737
+ monthly_data_str += f"์›”-{i+1}: {volume:,}ํšŒ\n"
738
+
739
+ current_date = datetime.now()
740
+ current_month = current_date.month
741
+
742
+ prompt = f"""
743
+ ํ‚ค์›Œ๋“œ: '{keyword}'
744
+ ํ˜„์žฌ ๊ฒ€์ƒ‰๋Ÿ‰: {total_volume:,}ํšŒ
745
+ ํ˜„์žฌ ์‹œ์ : {current_date.year}๋…„ {current_month}์›”
746
+ ์›”๋ณ„ ๊ฒ€์ƒ‰๋Ÿ‰ ๋ฐ์ดํ„ฐ (์ตœ๊ทผ 12๊ฐœ์›”):
747
+ {monthly_data_str}
748
+ ๋‹ค์Œ ํ˜•์‹์œผ๋กœ๋งŒ ๋‹ต๋ณ€ํ•˜์„ธ์š”:
749
+ ์ƒํ’ˆ์œ ํ˜•: [๋ด„์‹œ์ฆŒ์ƒํ’ˆ/์—ฌ๋ฆ„์‹œ์ฆŒ์ƒํ’ˆ/๊ฐ€์„์‹œ์ฆŒ์ƒํ’ˆ/๊ฒจ์šธ์‹œ์ฆŒ์ƒํ’ˆ/๋น„์‹œ์ฆŒ์ƒํ’ˆ/ํฌ๋ฆฌ์Šค๋งˆ์Šค์ด๋ฒคํŠธ์ƒํ’ˆ/๋ฐธ๋Ÿฐํƒ€์ธ์ด๋ฒคํŠธ์ƒํ’ˆ/์–ด๋ฒ„์ด๋‚ ์ด๋ฒคํŠธ์ƒํ’ˆ/์ƒˆํ•™๊ธฐ์ด๋ฒคํŠธ์ƒํ’ˆ/๊ธฐํƒ€์ด๋ฒคํŠธ์ƒํ’ˆ]
750
+ ํ”ผํฌ์›”: [X์›”] (๊ฒ€์ƒ‰๋Ÿ‰์ด ๊ฐ€์žฅ ๋†’์€ ์›”, ์‹ค์ œ ์ˆ˜์น˜ ํฌํ•จ)
751
+ ์„ฑ์žฅ์›”: [X์›”] (์ฆ๊ฐ€ํญ์ด ๊ฐ€์žฅ ๋†’์€ ์›”)
752
+ ํ˜„์žฌ์ƒํƒœ: {current_month}์›” ๊ธฐ์ค€ [๋„์ž…๊ธฐ/์„ฑ์žฅ๊ธฐ/์•ˆ์ •๊ธฐ/์‡ ํ‡ด๊ธฐ/๋น„์‹œ์ฆŒ๊ธฐ๊ฐ„]
753
+ ์ง„์ž…์ถ”์ฒœ: [๊ตฌ์ฒด์  ์›” ์ œ์‹œ]
754
+ """
755
+
756
+ response = gemini_model.generate_content(prompt)
757
+ result_text = response.text.strip()
758
+
759
+ lines = result_text.split('\n')
760
+ product_type = "๋น„์‹œ์ฆŒ์ƒํ’ˆ"
761
+ peak_month = max_month if max_month else "์—ฐ์ค‘"
762
+ growth_month = "์—ฐ์ค‘"
763
+ current_status = "์•ˆ์ •๊ธฐ"
764
+ entry_recommendation = "์–ธ์ œ๋“ ์ง€ ์ง„์ž… ๊ฐ€๋Šฅ"
765
+
766
+ for line in lines:
767
+ line = line.strip()
768
+ if line.startswith('์ƒํ’ˆ์œ ํ˜•:'):
769
+ product_type = line.replace('์ƒํ’ˆ์œ ํ˜•:', '').strip()
770
+ elif line.startswith('ํ”ผํฌ์›”:'):
771
+ extracted_peak = line.replace('ํ”ผํฌ์›”:', '').strip()
772
+ if '(' in extracted_peak and ')' in extracted_peak:
773
+ peak_month = extracted_peak
774
+ else:
775
+ peak_month = max_month if max_month else extracted_peak
776
+ elif line.startswith('์„ฑ์žฅ์›”:'):
777
+ growth_month = line.replace('์„ฑ์žฅ์›”:', '').strip()
778
+ elif line.startswith('ํ˜„์žฌ์ƒํƒœ:'):
779
+ current_status = line.replace('ํ˜„์žฌ์ƒํƒœ:', '').strip()
780
+ elif line.startswith('์ง„์ž…์ถ”์ฒœ:'):
781
+ entry_recommendation = line.replace('์ง„์ž…์ถ”์ฒœ:', '').strip()
782
+
783
+ detail_info = f"์ƒํ’ˆ์œ ํ˜•: {product_type} | ํ”ผํฌ์›”: {peak_month} | ์„ฑ์žฅ์›”: {growth_month} | ํ˜„์žฌ์ƒํƒœ: {current_status}"
784
+
785
+ logger.info(f"LLM ์‹œ์ฆŒ ๋ถ„์„ ์™„๋ฃŒ: {product_type}, {entry_recommendation}")
786
+ return product_type, entry_recommendation, detail_info
787
+
788
+ except Exception as e:
789
+ logger.error(f"LLM ์‹œ์ฆŒ ์‚ฌ์ดํด ๋ถ„์„ ์˜ค๋ฅ˜: {e}")
790
+ return "๋น„์‹œ์ฆŒ์ƒํ’ˆ", "์–ธ์ œ๋“ ์ง€ ์ง„์ž… ๊ฐ€๋Šฅ", "LLM ๋ถ„์„ ์˜ค๋ฅ˜"
791
+
792
+ def analyze_sourcing_strategy_improved(keyword, volume_data, trend_data_1year, trend_data_3year, filtered_keywords_df, gemini_model):
793
+ """๊ฐœ์„ ๋œ ์†Œ์‹ฑ์ „๋žต ๋ถ„์„ - ํฌ๋งทํŒ… ์ˆ˜์ • ๋ฐ ๊ด€์—ฌ๋„ ๋ถ„์„ ๊ฐ•ํ™”"""
794
+
795
+ total_volume = volume_data.get('์ด๊ฒ€์ƒ‰๋Ÿ‰', 0)
796
+ current_date = datetime.now()
797
+ current_month = current_date.month
798
+ current_year = current_date.year
799
+
800
+ # โœ… ์ˆ˜์ •: ์˜ฌ๋ฐ”๋ฅธ ๋กœ์ง์œผ๋กœ ์ƒ์Šนํญ ๊ณ„์‚ฐ
801
+ growth_analysis = calculate_max_growth_rate_with_predictions(trend_data_3year, keyword)
802
+
803
+ # โœ… ์ˆ˜์ •: ์˜ฌ๋ฐ”๋ฅธ ๋กœ์ง์œผ๋กœ ํ”ผํฌ์›” ๊ณ„์‚ฐ (์‹ค์ œ+์˜ˆ์ƒ ๋ฐ์ดํ„ฐ ํ™œ์šฉ)
804
+ peak_month_with_volume = get_peak_month_with_predictions(trend_data_3year, keyword)
805
+
806
+ # LLM์œผ๋กœ ์‹œ์ฆŒ ๋ถ„์„ (๊ธฐ์กด ์œ ์ง€)
807
+ if gemini_model:
808
+ product_type, entry_timing, season_detail = analyze_season_cycle_with_llm(trend_data_3year, keyword, total_volume, gemini_model)
809
+ else:
810
+ # ๊ธฐ๋ณธ๊ฐ’
811
+ product_type = "์—ฐ์ค‘์ƒํ’ˆ"
812
+ if total_volume > 50000:
813
+ product_type = "์ธ๊ธฐ์ƒํ’ˆ"
814
+ elif total_volume > 10000:
815
+ product_type = "์ค‘๊ฐ„์ƒํ’ˆ"
816
+ elif total_volume > 0:
817
+ product_type = "ํ‹ˆ์ƒˆ์ƒํ’ˆ"
818
+
819
+ # 2. ๊ด€์—ฌ๋„ ๋ถ„์„ ์ถ”๊ฐ€ - ์ดˆ๋ณด์ž๊ฐ€ ํŒ๋งค๊ฐ€๋Šฅํ•œ ์†Œ์‹ฑ ๊ธฐ์ค€ (๊ฐœ์„ ๋œ ๊ธฐ์ค€ ์ ์šฉ)
820
+ involvement_level = analyze_involvement_level(keyword, total_volume, gemini_model)
821
+
822
+ # ํŠธ๋ Œ๋“œ ๊ฒฝ๊ณ  ๋ฉ”์‹œ์ง€
823
+ trend_warning = ""
824
+ if not trend_data_3year:
825
+ trend_warning = "\n\n๐Ÿ’ก ๋” ์ •ํ™•ํ•œ ํŠธ๋ Œ๋“œ ๋ฐ์ดํ„ฐ๋ฅผ ์œ„ํ•ด \"1๋‹จ๊ณ„: ๊ธฐ๋ณธ ํ‚ค์›Œ๋“œ ์ž…๋ ฅ\"์„ ์‹คํ–‰ํ•ด๋ณด์„ธ์š”."
826
+
827
+ # ๊ฒฐ๊ณผ ํฌ๋งทํŒ… ์ˆ˜์ • - ๊ตฌ๋ถ„์„ ๊ณผ ํ•ญ๋ชฉ ๋ถ„๋ฆฌ
828
+ result_content = f"""**๐Ÿ”– ์ƒํ’ˆ์œ ํ˜•**
829
+ {product_type}
830
+ {involvement_level}
831
+ **๐Ÿ”– ๊ฐ€์žฅ ๊ฒ€์ƒ‰๋Ÿ‰์ด ๋งŽ์€ ์›”**
832
+ {peak_month_with_volume}
833
+ **๐Ÿ”– ๊ฐ€์žฅ ์ƒ์Šนํญ์ด ๋†’์€ ์›”**
834
+ {growth_analysis}{trend_warning}"""
835
+
836
+ try:
837
+ return {"status": "success", "content": result_content}
838
+ except Exception as e:
839
+ logger.error(f"์†Œ์‹ฑ์ „๋žต ๋ถ„์„ ์˜ค๋ฅ˜: {e}")
840
+ return {"status": "error", "content": "์†Œ์‹ฑ์ „๋žต ๋ถ„์„์„ ์™„๋ฃŒํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."}
841
+
842
+ def analyze_involvement_level(keyword, total_volume, gemini_model):
843
+ """๊ด€์—ฌ๋„ ๋ถ„์„ ํ•จ์ˆ˜ - ์ดˆ๋ณด์ž๊ฐ€ ํŒ๋งค๊ฐ€๋Šฅํ•œ ์†Œ์‹ฑ ๊ธฐ์ค€"""
844
+ try:
845
+ # ๊ธฐ๋ณธ ๊ทœ์น™ ๊ธฐ๋ฐ˜ ๋ถ„์„
846
+ basic_involvement = get_basic_involvement_level(keyword, total_volume)
847
+
848
+ # Gemini๊ฐ€ ์žˆ์œผ๋ฉด LLM ๋ถ„์„๋„ ์ˆ˜ํ–‰
849
+ if gemini_model:
850
+ llm_involvement = get_llm_involvement_analysis(keyword, total_volume, gemini_model)
851
+ return llm_involvement
852
+ else:
853
+ return basic_involvement
854
+
855
+ except Exception as e:
856
+ logger.error(f"๊ด€์—ฌ๋„ ๋ถ„์„ ์˜ค๋ฅ˜: {e}")
857
+ return "๋ณตํ•ฉ๊ด€์—ฌ๋„์ƒํ’ˆ(์ƒํ’ˆ์— ๋”ฐ๋ผ ๋‹ฌ๋ผ์ง)"
858
+
859
+ def get_basic_involvement_level(keyword, total_volume):
860
+ """๊ธฐ๋ณธ ๊ทœ์น™ ๊ธฐ๋ฐ˜ ๊ด€์—ฌ๋„ ๋ถ„์„ - ์ดˆ๋ณด์ž ํŒ๋งค ๊ด€์ """
861
+
862
+ # ์ €๊ด€์—ฌ ์ƒํ’ˆ ํ‚ค์›Œ๋“œ ํŒจํ„ด (์ดˆ๋ณด์ž ์ง„์ž… ๊ฐ€๋Šฅํ•œ ๋ถˆํŽธํ•ด์†Œ ์ œํ’ˆ)
863
+ low_involvement_keywords = [
864
+ # ๋ถˆํŽธํ•ด์†Œ/์ •๋ฆฌ์ˆ˜๋‚ฉ
865
+ "๊ฑฐ์น˜๋Œ€", "๋ฐ›์นจ๋Œ€", "์ •๋ฆฌํ•จ", "์ •๋ฆฌ๋Œ€", "์ˆ˜๋‚ฉ", "ํ™€๋”", "์Šคํƒ ๋“œ",
866
+ "์ฟ ์…˜", "๋ฒ ๊ฐœ", "๋ชฉ๋ฒ ๊ฐœ", "๋ฐฉ์„", "๋งคํŠธ", "ํŒจ๋“œ",
867
+ # ์ผ€์ด๋ธ”/์ „์„  ๊ด€๋ฆฌ
868
+ "์ผ€์ด๋ธ”", "์„ ์ •๋ฆฌ", "์ฝ”๋“œ", "์ถฉ์ „๊ธฐ", "์–ด๋Œ‘ํ„ฐ",
869
+ # ์ฒญ์†Œ/์œ„์ƒ (๋Œ€๊ธฐ์—… ์ œํ’ˆ ์ œ์™ธ)
870
+ "์ฒญ์†Œ์†”", "์ฒญ์†Œ๊ธฐ", "๊ฑธ๋ ˆ", "ํƒ€์˜ฌ", "๋ธŒ๋Ÿฌ์‹œ",
871
+ # ์ž๋™์ฐจ/์‹ค์šฉ์šฉํ’ˆ
872
+ "์ฐจ๋Ÿ‰์šฉ", "์ž๋™์ฐจ", "ํ•ธ๋“œํฐ", "์Šค๋งˆํŠธํฐ", "ํƒœ๋ธ”๋ฆฟ",
873
+ # ๊ฐ„๋‹จํ•œ ๋„๊ตฌ/์•ก์„ธ์„œ๋ฆฌ
874
+ "์ง‘๊ฒŒ", "ํ›„ํฌ", "์ž์„", "ํด๋ฆฝ", "๊ณ ๋ฆฌ", "๋ง", "ํ™€๋”",
875
+ # ๋ฏธ๋„๋Ÿผ๋ฐฉ์ง€/์•ˆ์ „
876
+ "๋ฏธ๋„๋Ÿผ", "๋…ผ์Šฌ๋ฆฝ", "๋ฐฉ์ง€", "๋ณดํ˜ธ", "์ปค๋ฒ„", "์ผ€์ด์Šค"
877
+ ]
878
+
879
+ # ๊ณ ๊ด€์—ฌ ์ƒํ’ˆ ํ‚ค์›Œ๋“œ ํŒจํ„ด (๋Œ€๊ธฐ์—… ๋…์  ๋˜๋Š” ๊ณ ๊ฐ€/์ „๋ฌธ ์ œํ’ˆ)
880
+ high_involvement_keywords = [
881
+ # ๋Œ€๊ธฐ์—… ๋…์  ์ƒํ•„ํ’ˆ
882
+ "ํœด์ง€", "ํ™”์žฅ์ง€", "๋ฌผํ‹ฐ์Šˆ", "๋งˆ์Šคํฌ", "์„ธ์ œ", "์ƒดํ‘ธ", "๋ฆฐ์Šค", "๋น„๋ˆ„",
883
+ "์น˜์•ฝ", "์นซ์†”", "๊ธฐ์ €๊ท€", "์ƒ๋ฆฌ๋Œ€", "์ฝ˜๋”",
884
+ # ์‹ํ’ˆ/์Œ๋ฃŒ (๋ธŒ๋žœ๋“œ ๋ฏผ๊ฐ)
885
+ "๋ผ๋ฉด", "๊ณผ์ž", "์Œ๋ฃŒ", "์ปคํ”ผ", "์ฐจ", "์šฐ์œ ", "์š”๊ตฌ๋ฅดํŠธ",
886
+ "์Œ€", "๊น€", "์ฐธ๊ธฐ๋ฆ„", "๊ฐ„์žฅ", "๊ณ ์ถ”์žฅ", "๋œ์žฅ",
887
+ # ๊ณ ๊ฐ€ ์ „์ž์ œํ’ˆ
888
+ "๋…ธํŠธ๋ถ", "์ปดํ“จํ„ฐ", "์Šค๋งˆํŠธํฐ", "ํƒœ๋ธ”๋ฆฟ", "์นด๋ฉ”๋ผ", "TV", "๋ชจ๋‹ˆํ„ฐ",
889
+ "๋ƒ‰์žฅ๊ณ ", "์„ธํƒ๊ธฐ", "์—์–ด์ปจ", "์ฒญ์†Œ๊ธฐ", "์ „์ž๋ ˆ์ธ์ง€",
890
+ # ์˜๋ฃŒ/๊ฑด๊ฐ• (์ธ์ฆ ํ•„์š”)
891
+ "์˜๋ฃŒ", "๊ฑด๊ฐ•์‹ํ’ˆ", "์˜์–‘์ œ", "๋น„ํƒ€๋ฏผ", "์•ฝ", "์˜์•ฝํ’ˆ",
892
+ # ๋ช…ํ’ˆ/๋ธŒ๋žœ๋“œ
893
+ "๋ช…ํ’ˆ", "๋ธŒ๋žœ๋“œ", "๋Ÿญ์…”๋ฆฌ", "์‹œ๊ณ„", "๋ณด์„", "๊ธˆ", "์€", "๋‹ค์ด์•„๋ชฌ๋“œ"
894
+ ]
895
+
896
+ keyword_lower = keyword.lower()
897
+
898
+ # ์ €๊ด€์—ฌ ์ƒํ’ˆ ์ฒดํฌ (๋ถˆํŽธํ•ด์†Œ ํ‚ค์›Œ๋“œ ์šฐ์„ )
899
+ for low_kw in low_involvement_keywords:
900
+ if low_kw in keyword_lower:
901
+ return "์ €๊ด€์—ฌ์ƒํ’ˆ(์ดˆ๋ณด์ž์šฉ)"
902
+
903
+ # ๊ณ ๊ด€์—ฌ ์ƒํ’ˆ ์ฒดํฌ (๋Œ€๊ธฐ์—… ๋…์ /๋ธŒ๋žœ๋“œ ๋ฏผ๊ฐ ํ‚ค์›Œ๋“œ)
904
+ for high_kw in high_involvement_keywords:
905
+ if high_kw in keyword_lower:
906
+ return "๊ณ ๊ด€์—ฌ์ƒํ’ˆ(๊ณ ๊ธ‰์ž์šฉ)"
907
+
908
+ # ๊ฒ€์ƒ‰๋Ÿ‰ ๊ธฐ๋ฐ˜ ์ถ”๊ฐ€ ํŒ๋‹จ
909
+ if total_volume > 100000:
910
+ # ๊ฒ€์ƒ‰๋Ÿ‰์ด ๋งค์šฐ ๋†’์œผ๋ฉด ๋Œ€๊ธฐ์—…์ด ๊ด€์‹ฌ ๊ฐ€์งˆ ๋งŒํ•œ ์‹œ์žฅ
911
+ return "๊ณ ๊ด€์—ฌ์ƒํ’ˆ(๊ณ ๊ธ‰์ž์šฉ)"
912
+ elif total_volume > 50000:
913
+ return "๋ณตํ•ฉ๊ด€์—ฌ๋„์ƒํ’ˆ(์ƒํ’ˆ์— ๋”ฐ๋ผ ๋‹ฌ๋ผ์ง)"
914
+ elif total_volume > 5000:
915
+ return "๋ณตํ•ฉ๊ด€์—ฌ๋„์ƒํ’ˆ(์ƒํ’ˆ์— ๋”ฐ๋ผ ๋‹ฌ๋ผ์ง)"
916
+ else:
917
+ # ๊ฒ€์ƒ‰๋Ÿ‰์ด ๋‚ฎ์œผ๋ฉด ํ‹ˆ์ƒˆ ์‹œ์žฅ, ์ดˆ๋ณด์ž๋„ ์ง„์ž… ๊ฐ€๋Šฅ
918
+ return "์ €๊ด€์—ฌ์ƒํ’ˆ(์ดˆ๋ณด์ž์šฉ)"
919
+
920
+ def get_llm_involvement_analysis(keyword, total_volume, gemini_model):
921
+ """LLM์„ ์ด์šฉํ•œ ์ •๊ตํ•œ ๊ด€์—ฌ๋„ ๋ถ„์„ - ์ดˆ๋ณด์ž ํŒ๋งค ๊ด€์  ๊ธฐ์ค€ ์ ์šฉ"""
922
+ try:
923
+ prompt = f"""
924
+ '{keyword}' ์ƒํ’ˆ์˜ ๊ด€์—ฌ๋„๋ฅผ ์ดˆ๋ณด์ž ํŒ๋งค ๊ด€์ ์—์„œ ๋ถ„์„ํ•ด์ฃผ์„ธ์š”.
925
+ ๊ฒ€์ƒ‰๋Ÿ‰: {total_volume:,}ํšŒ
926
+ ๊ด€์—ฌ๋„ ์ •์˜ (์ดˆ๋ณด์ž๊ฐ€ ํŒ๋งค๊ฐ€๋Šฅํ•œ ์†Œ์‹ฑ ๊ธฐ์ค€):
927
+ ์ €๊ด€์—ฌ์ƒํ’ˆ(์ดˆ๋ณด์ž์šฉ):
928
+ - ๋Œ€๊ธฐ์—… ๋…์ ์ด ์—†๋Š” ์˜์—ญ
929
+ - ์ฆ‰์‹œ ๋ถˆํŽธํ•ด์†Œํ•˜๋Š” ์ œํ’ˆ (์ง€๊ธˆ ๋ฐ”๋กœ ํ•„์š”ํ•œ ๋ฌธ์ œ ํ•ด๊ฒฐ)
930
+ - ๋ธŒ๋žœ๋“œ ์ƒ๊ด€์—†์ด ๊ธฐ๋Šฅ๋งŒ ๋˜๋ฉด ๊ตฌ๋งคํ•˜๋Š” ์ œํ’ˆ
931
+ - 1๋งŒ์›~3๋งŒ์›๋Œ€ ๊ฐ€๊ฒฉ, ์†Œ๋Ÿ‰(100๊ฐœ ์ดํ•˜) ์‹œ์ž‘ ๊ฐ€๋Šฅ
932
+ - ์˜ˆ์‹œ: ๋ชฉ๋ฒ ๊ฐœ, ์Šค๋งˆํŠธํฐ๊ฑฐ์น˜๋Œ€, ์„œ๋ž์ •๋ฆฌํ•จ, ์ผ€์ด๋ธ”์ •๋ฆฌ๊ธฐ
933
+ ๊ณ ๊ด€์—ฌ์ƒํ’ˆ(๊ณ ๊ธ‰์ž์šฉ):
934
+ - ๋Œ€๊ธฐ์—…/๋ธŒ๋žœ๋“œ๊ฐ€ ์‹œ์žฅ์„ ๋…์ ํ•˜๋Š” ์˜์—ญ (์ดˆ๋ณด์ž ์ง„์ž… ๋ถˆ๊ฐ€)
935
+ - ์ƒํ•„ํ’ˆ(ํœด์ง€, ์„ธ์ œ, ๋งˆ์Šคํฌ ๋“ฑ) - ๋ธŒ๋žœ๋“œ ์ถฉ์„ฑ๋„ ๋†’์Œ
936
+ - ๊ณ ๊ฐ€ ์ œํ’ˆ(10๋งŒ์› ์ด์ƒ), ์ „๋ฌธ์„ฑ/์ธ์ฆ ํ•„์š”
937
+ - ๋Œ€์ž๋ณธ ํ•„์š”ํ•œ ์•„์ดํ…œ
938
+ - ์˜ˆ์‹œ: ์ „์ž์ œํ’ˆ, ๊ฐ€์ „, ๋ธŒ๋žœ๋“œ ์ƒํ•„ํ’ˆ, ์˜๋ฃŒ์šฉํ’ˆ
939
+ ๋ณตํ•ฉ๊ด€์—ฌ๋„์ƒํ’ˆ(์ƒํ’ˆ์— ๋”ฐ๋ผ ๋‹ฌ๋ผ์ง):
940
+ - ๊ฐ€๊ฒฉ๋Œ€๋ณ„๋กœ ์ €๊ฐ€ํ˜•(์ €๊ด€์—ฌ)๊ณผ ๊ณ ๊ฐ€ํ˜•(๊ณ ๊ด€์—ฌ)์ด ๊ณต์กด
941
+ - ํƒ€๊ฒŸ์ด๋‚˜ ์šฉ๋„์— ๋”ฐ๋ผ ๊ด€์—ฌ๋„๊ฐ€ ๊ทน๋ช…ํ•˜๊ฒŒ ๋‹ฌ๋ผ์ง
942
+ - ์˜ˆ์‹œ: ์˜๋ฅ˜, ์šด๋™์šฉํ’ˆ, ๋ทฐํ‹ฐ์šฉํ’ˆ ๋“ฑ
943
+ ๋ณตํ•ฉ๊ด€์—ฌ๋„์ƒํ’ˆ์œผ๋กœ ํŒ๋‹จํ•  ๊ฒฝ์šฐ, ๋ฐ˜๋“œ์‹œ ๊ตฌ์ฒด์ ์ธ ์ด์œ ๋ฅผ ์„ค๋ช…ํ•˜์„ธ์š”:
944
+ - ๊ฐ€๊ฒฉ๋Œ€๋ณ„ ๋ถ„ํ™”: "1-3๋งŒ์› ์ค‘๊ตญ์‚ฐ(์ €๊ด€์—ฌ) vs 10-15๋งŒ์› ๊ตญ์‚ฐ ์ˆ˜์ œ(๊ณ ๊ด€์—ฌ)"
945
+ - ํƒ€๊ฒŸ๋ณ„ ์ฐจ์ด: "์ผ๋ฐ˜์ธ์€ ์ €๊ด€์—ฌ vs ์ „๋ฌธ๊ฐ€๋Š” ๊ณ ๊ด€์—ฌ"
946
+ - ์šฉ๋„๋ณ„ ์ฐจ์ด: "์ž„์‹œ์šฉ์€ ์ €๊ด€์—ฌ vs ์žฅ๊ธฐ์šฉ์€ ๊ณ ๊ด€์—ฌ"
947
+ ๋‹ค์Œ ํ˜•์‹์œผ๋กœ ๋‹ต๋ณ€ํ•˜์„ธ์š”:
948
+ [๊ด€์—ฌ๋„ ์„ ํƒ]
949
+ [๊ตฌ์ฒด์ ์ธ ํŒ๋‹จ ์ด์œ  - ๊ฐ€๊ฒฉ๋Œ€/ํƒ€๊ฒŸ/๋ธŒ๋žœ๋“œ ๋…์  ์—ฌ๋ถ€ ๋“ฑ์„ ๋ช…ํ™•ํžˆ ์ œ์‹œ]
950
+ ์„ ํƒ์ง€:
951
+ ์ €๊ด€์—ฌ์ƒํ’ˆ(์ดˆ๋ณด์ž์šฉ)
952
+ ๋ณตํ•ฉ๊ด€์—ฌ๋„์ƒํ’ˆ(์ƒํ’ˆ์— ๋”ฐ๋ผ ๋‹ฌ๋ผ์ง)
953
+ ๊ณ ๊ด€์—ฌ์ƒํ’ˆ(๊ณ ๊ธ‰์ž์šฉ)
954
+ """
955
+
956
+ response = gemini_model.generate_content(prompt)
957
+ result = response.text.strip()
958
+
959
+ # ๊ฒฐ๊ณผ ํ•„ํ„ฐ๋ง - ์ •ํ™•ํ•œ ํ˜•์‹๋งŒ ํ—ˆ์šฉ
960
+ if "์ €๊ด€์—ฌ์ƒํ’ˆ(์ดˆ๋ณด์ž์šฉ)" in result:
961
+ return "์ €๊ด€์—ฌ์ƒํ’ˆ(์ดˆ๋ณด์ž์šฉ)"
962
+ elif "๊ณ ๊ด€์—ฌ์ƒํ’ˆ(๊ณ ๊ธ‰์ž์šฉ)" in result:
963
+ return "๊ณ ๊ด€์—ฌ์ƒํ’ˆ(๊ณ ๊ธ‰์ž์šฉ)"
964
+ elif "๋ณตํ•ฉ๊ด€์—ฌ๋„์ƒํ’ˆ(์ƒํ’ˆ์— ๋”ฐ๋ผ ๋‹ฌ๋ผ์ง)" in result:
965
+ return "๋ณตํ•ฉ๊ด€์—ฌ๋„์ƒํ’ˆ(์ƒํ’ˆ์— ๋”ฐ๋ผ ๋‹ฌ๋ผ์ง)"
966
+ else:
967
+ # LLM ์‘๋‹ต์ด ๋ถ€์ •ํ™•ํ•œ ๊ฒฝ์šฐ ๊ธฐ๋ณธ ๊ทœ์น™์œผ๋กœ ํด๋ฐฑ
968
+ return get_basic_involvement_level(keyword, total_volume)
969
+
970
+ except Exception as e:
971
+ logger.error(f"LLM ๊ด€์—ฌ๋„ ๋ถ„์„ ์˜ค๋ฅ˜: {e}")
972
+ return get_basic_involvement_level(keyword, total_volume)
973
+
974
+
975
+ class CompactKeywordAnalyzer:
976
+ """๊ฐ„๊ฒฐํ•œ 7๋‹จ๊ณ„ ํ‚ค์›Œ๋“œ ๋ถ„์„๊ธฐ"""
977
+
978
+ def __init__(self, gemini_model):
979
+ self.gemini_model = gemini_model
980
+ self.max_retries = 3
981
+
982
+ def call_llm_with_retry(self, prompt: str, step_name: str = "") -> str:
983
+ """์žฌ์‹œ๋„ ๋กœ์ง์ด ์ ์šฉ๋œ LLM ํ˜ธ์ถœ"""
984
+ last_error = None
985
+
986
+ for attempt in range(self.max_retries):
987
+ try:
988
+ logger.info(f"{step_name} ์‹œ๋„ {attempt + 1}/{self.max_retries}")
989
+ response = self.gemini_model.generate_content(prompt)
990
+ result = response.text.strip()
991
+
992
+ if result and len(result) > 20:
993
+ logger.info(f"{step_name} ์„ฑ๊ณต")
994
+ return result
995
+ else:
996
+ raise Exception("์‘๋‹ต์ด ๋„ˆ๋ฌด ์งง๊ฑฐ๋‚˜ ๋น„์–ด์žˆ์Œ")
997
+
998
+ except Exception as e:
999
+ last_error = e
1000
+ logger.warning(f"{step_name} ์‹คํŒจ (์‹œ๋„ {attempt + 1}): {e}")
1001
+
1002
+ if attempt < self.max_retries - 1:
1003
+ delay = 1.0 * (attempt + 1) + random.uniform(0, 0.5)
1004
+ time.sleep(delay)
1005
+
1006
+ logger.error(f"{step_name} ๋ชจ๋“  ์žฌ์‹œ๋„ ์‹คํŒจ: {last_error}")
1007
+ return f"{step_name} ๋ถ„์„์„ ์™„๋ฃŒํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."
1008
+
1009
+ def clean_markdown_and_bold(self, text: str) -> str:
1010
+ """๋งˆํฌ๋‹ค์šด๊ณผ ๋ณผ๋“œ ์ฒ˜๋ฆฌ๋ฅผ ์™„์ „ํžˆ ์ œ๊ฑฐ"""
1011
+ text = re.sub(r'\*\*(.+?)\*\*', r'\1', text)
1012
+ text = re.sub(r'\*(.+?)\*', r'\1', text)
1013
+ text = re.sub(r'__(.+?)__', r'\1', text)
1014
+ text = re.sub(r'_(.+?)_', r'\1', text)
1015
+ text = re.sub(r'##\s*(.+)', r'\1', text)
1016
+ text = re.sub(r'#\s*(.+)', r'\1', text)
1017
+ text = re.sub(r'\*+', '', text)
1018
+ text = re.sub(r'_+', '', text)
1019
+ return text.strip()
1020
+
1021
+ def analyze_sourcing_strategy(self, keyword: str, volume_data: dict, keywords_df: Optional[pd.DataFrame], trend_data_1year=None, trend_data_3year=None) -> str:
1022
+ """๊ฐœ์„ ๋œ ์†Œ์‹ฑ์ „๋žต ๋ถ„์„ - ๊ฒฝ์Ÿ๋ฐ์ดํ„ฐ ์ œ๊ฑฐ"""
1023
+
1024
+ try:
1025
+ sourcing_analysis = analyze_sourcing_strategy_improved(
1026
+ keyword, volume_data, trend_data_1year, trend_data_3year, keywords_df, self.gemini_model
1027
+ )
1028
+ if sourcing_analysis["status"] == "success":
1029
+ return self.clean_markdown_and_bold(sourcing_analysis["content"])
1030
+ else:
1031
+ return sourcing_analysis["content"]
1032
+ except Exception as e:
1033
+ logger.error(f"์†Œ์‹ฑ์ „๋žต ๋ถ„์„ ์˜ค๋ฅ˜: {e}")
1034
+ return "์†Œ์‹ฑ์ „๋žต ๋ถ„์„์„ ์™„๋ฃŒํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."
1035
+
1036
+ def analyze_step1_product_type(self, keyword: str, keywords_df: Optional[pd.DataFrame]) -> str:
1037
+ """1๋‹จ๊ณ„. ์ƒํ’ˆ์œ ํ˜• ๋ถ„์„"""
1038
+
1039
+ related_keywords = ""
1040
+ if keywords_df is not None and not keywords_df.empty:
1041
+ top_keywords = keywords_df.head(10)['์กฐํ•ฉ ํ‚ค์›Œ๋“œ'].tolist()
1042
+ related_keywords = f"์—ฐ๊ด€ํ‚ค์›Œ๋“œ: {', '.join(top_keywords)}"
1043
+
1044
+ prompt = f"""
1045
+ ๋‹น์‹ ์€ ์ดˆ๋ณด ์…€๋Ÿฌ๊ฐ€ ์ƒํ’ˆ ํŒ๋งค ์„ฑ๊ณต์„ ๋น ๋ฅด๊ฒŒ ์ด๋ฃฐ ์ˆ˜ ์žˆ๋„๋ก ๋•๋Š” ์ตœ๊ณ ์˜ ์ƒํ’ˆ ์†Œ์‹ฑ ๋ฐ ์ƒํ’ˆ๊ธฐํš ์ปจ์„คํ„ดํŠธ AI์ž…๋‹ˆ๋‹ค.
1046
+ ๋ถ„์„ ํ‚ค์›Œ๋“œ: '{keyword}'
1047
+ {related_keywords}
1048
+ 1๋‹จ๊ณ„. ์ƒํ’ˆ์œ ํ˜• ๋ถ„์„
1049
+ ์ƒํ’ˆ ์œ ํ˜• ๋ถ„๋ฅ˜ ๊ธฐ์ค€:
1050
+ - ๋ถˆํŽธํ•ด๊ฒฐ์ƒํ’ˆ: ํŠน์ • ๋ฌธ์ œ๋‚˜ ๋ถˆํŽธํ•จ์„ ์ฆ‰๊ฐ ํ•ด๊ฒฐํ•˜๋Š” ์ œํ’ˆ
1051
+ - ์—…๊ทธ๋ ˆ์ด๋“œ์ƒํ’ˆ: ์‚ถ์˜ ์งˆ๊ณผ ๋งŒ์กฑ๋„๋ฅผ ํ–ฅ์ƒ์‹œํ‚ค๋Š” ์ œํ’ˆ
1052
+ - ํ•„์ˆ˜์ƒํ’ˆ: ์ผ์ƒ์—์„œ ๋ฐ˜๋“œ์‹œ ํ•„์š”ํ•˜๊ณ  ๋ฐ˜๋ณต ๊ตฌ๋งค๋˜๋Š” ์ œํ’ˆ
1053
+ - ์ทจํ–ฅ์ €๊ฒฉ์ƒํ’ˆ: ๊ฐ์„ฑ์ , ๊ฐœ์„ฑ์  ์š•๊ตฌ๋ฅผ ์ž๊ทนํ•˜๋Š” ์ œํ’ˆ
1054
+ - ์œตํ•ฉ์ƒํ’ˆ: ์œ„ 2๊ฐœ ์ด์ƒ์˜ ์œ ํ˜•์ด ๊ฒฐํ•ฉ๋œ ์ œํ’ˆ
1055
+ ๋‹ค์Œ ํ˜•์‹์œผ๋กœ ๋ถ„์„ํ•ด์ฃผ์„ธ์š” (๋ณผ๋“œ, ๋งˆํฌ๋‹ค์šด ์‚ฌ์šฉ ๊ธˆ์ง€):
1056
+ ์ฃผ์š”์œ ํ˜•: [์œ ํ˜•๋ช…]
1057
+ {keyword}๋Š” [๊ตฌ์ฒด์  ์„ค๋ช… - ์™œ ์ด ์œ ํ˜•์ธ์ง€ ๋ณธ์งˆ์  ๊ฐ€์น˜์™€ ํ•ด๊ฒฐํ•˜๋Š” ๋ฌธ์ œ๋ฅผ ์ค‘์‹ฌ์œผ๋กœ 2-3๋ฌธ์žฅ]
1058
+ ๋ณด์กฐ์œ ํ˜•: [ํ•ด๋‹น ์œ ํ˜•๋“ค]
1059
+ [์œ ํ˜•1]
1060
+ - [์ด ์œ ํ˜•์— ํ•ด๋‹นํ•˜๋Š” ์ด์œ  1๋ฌธ์žฅ]
1061
+ [์œ ํ˜•2]
1062
+ - [์ด ์œ ํ˜•์— ํ•ด๋‹นํ•˜๋Š” ์ด์œ  1๋ฌธ์žฅ]
1063
+ """
1064
+
1065
+ result = self.call_llm_with_retry(prompt, f"1๋‹จ๊ณ„-์ƒํ’ˆ์œ ํ˜•๋ถ„์„-{keyword}")
1066
+ return self.clean_markdown_and_bold(result)
1067
+
1068
+ def analyze_step2_target_customer(self, keyword: str, step1_result: str) -> str:
1069
+ """2๋‹จ๊ณ„. ์†Œ๋น„์ž ํƒ€๊ฒŸ ์„ค์ •"""
1070
+
1071
+ prompt = f"""
1072
+ ๋‹น์‹ ์€ ์ดˆ๋ณด ์…€๋Ÿฌ๊ฐ€ ์ƒํ’ˆ ํŒ๋งค ์„ฑ๊ณต์„ ๋น ๋ฅด๊ฒŒ ์ด๋ฃฐ ์ˆ˜ ์žˆ๋„๋ก ๋•๋Š” ์ตœ๊ณ ์˜ ์ƒํ’ˆ ์†Œ์‹ฑ ๋ฐ ์ƒํ’ˆ๊ธฐํš ์ปจ์„คํ„ดํŠธ AI์ž…๋‹ˆ๋‹ค.
1073
+ ๋ถ„์„ ํ‚ค์›Œ๋“œ: '{keyword}'
1074
+ ์ด์ „ ๋ถ„์„ ๊ฒฐ๊ณผ:
1075
+ {step1_result}
1076
+ 2๋‹จ๊ณ„. ์†Œ๋น„์ž ํƒ€๊ฒŸ ์„ค์ •
1077
+ ๋‹ค์Œ ํ˜•์‹์œผ๋กœ ๊ฐ„๊ฒฐํ•˜๊ฒŒ ๋ถ„์„ํ•ด์ฃผ์„ธ์š” (๋ณผ๋“œ, ๋งˆํฌ๋‹ค์šด ์‚ฌ์šฉ ๊ธˆ์ง€):
1078
+ ๊ณ ๊ฐ์ƒํ™ฉ
1079
+ - [๊ตฌ์ฒด์ ์ธ ๊ตฌ๋งค ์ƒํ™ฉ๋“ค์„ ๊ฐ„๋‹จํžˆ]
1080
+ ํŽ˜๋ฅด์†Œ๋‚˜
1081
+ - [์—ฐ๋ น๋Œ€, ์„ฑ๋ณ„, ๋ผ์ดํ”„์Šคํƒ€์ผ์„ ํ†ตํ•ฉํ•˜์—ฌ 1-2์ค„๋กœ ๊ฐ„๊ฒฐํ•˜๊ฒŒ]
1082
+ ์ฃผ์š” ๋‹ˆ์ฆˆ
1083
+ - [ํ•ต์‹ฌ ๋‹ˆ์ฆˆ ํ•œ์ค„๋งŒ]
1084
+ """
1085
+
1086
+ result = self.call_llm_with_retry(prompt, f"2๋‹จ๊ณ„-ํƒ€๊ฒŸ์„ค์ •-{keyword}")
1087
+ return self.clean_markdown_and_bold(result)
1088
+
1089
+ def analyze_step3_sourcing_strategy(self, keyword: str, previous_results: str) -> str:
1090
+ """3๋‹จ๊ณ„. ํƒ€๊ฒŸ๋ณ„ ์ฐจ๋ณ„ํ™”๋œ ์†Œ์‹ฑ ์ „๋žต ์ œ์•ˆ"""
1091
+
1092
+ prompt = f"""
1093
+ ๋‹น์‹ ์€ ์ดˆ๋ณด ์…€๋Ÿฌ๊ฐ€ ์ƒํ’ˆ ํŒ๋งค ์„ฑ๊ณต์„ ๋น ๋ฅด๊ฒŒ ์ด๋ฃฐ ์ˆ˜ ์žˆ๋„๋ก ๋•๋Š” ์ตœ๊ณ ์˜ ์ƒํ’ˆ ์†Œ์‹ฑ ๋ฐ ์ƒํ’ˆ๊ธฐํš ์ปจ์„คํ„ดํŠธ AI์ž…๋‹ˆ๋‹ค.
1094
+ ๋ถ„์„ ํ‚ค์›Œ๋“œ: '{keyword}'
1095
+ ์ด์ „ ๋ถ„์„ ๊ฒฐ๊ณผ:
1096
+ {previous_results}
1097
+ 3๋‹จ๊ณ„. ํƒ€๊ฒŸ๋ณ„ ์ฐจ๋ณ„ํ™”๋œ ์†Œ์‹ฑ ์ „๋žต ์ œ์•ˆ
1098
+ ํ˜„์‹ค์ ์œผ๋กœ ์˜จ๋ผ์ธ์—์„œ ์†Œ์‹ฑ ๊ฐ€๋Šฅํ•œ ์ฐจ๋ณ„ํ™” ์ „๋žต์„ ์ œ์•ˆํ•ด์ฃผ์„ธ์š”.
1099
+ ๋‹ค์Œ ํ˜•์‹์œผ๋กœ ๋ถ„์„ํ•ด์ฃผ์„ธ์š” (๋ณผ๋“œ, ๋งˆํฌ๋‹ค์šด ์‚ฌ์šฉ ๊ธˆ์ง€):
1100
+ ํ•ต์‹ฌ ๊ตฌ๋งค ๊ณ ๋ ค ์š”์†Œ 5๊ฐ€์ง€
1101
+ 1. [์š”์†Œ1 ๊ฐ„๋‹จํžˆ]
1102
+ 2. [์š”์†Œ2 ๊ฐ„๋‹จํžˆ]
1103
+ 3. [์š”์†Œ3 ๊ฐ„๋‹จํžˆ]
1104
+ 4. [์š”์†Œ4 ๊ฐ„๋‹จํžˆ]
1105
+ 5. [์š”์†Œ5 ๊ฐ„๋‹จํžˆ]
1106
+ ์ฐจ๋ณ„ํ™” ์†Œ์‹ฑ ์ „๋žต
1107
+ 1. [์ „๋žต๋ช…]
1108
+ - [ํ˜„์‹ค์ ์œผ๋กœ ์†Œ์‹ฑ ๊ฐ€๋Šฅํ•œ ๊ตฌ์ฒด์  ๋ฐฉ๋ฒ• ํ•œ์ค„]
1109
+ 2. [์ „๋žต๋ช…]
1110
+ - [ํ˜„์‹ค์ ์œผ๋กœ ์†Œ์‹ฑ ๊ฐ€๋Šฅํ•œ ๊ตฌ์ฒด์  ๋ฐฉ๋ฒ• ํ•œ์ค„]
1111
+ 3. [์ „๋žต๋ช…]
1112
+ - [ํ˜„์‹ค์ ์œผ๋กœ ์†Œ์‹ฑ ๊ฐ€๋Šฅํ•œ ๊ตฌ์ฒด์  ๋ฐฉ๋ฒ• ํ•œ์ค„]
1113
+ 4. [์ „๋žต๋ช…]
1114
+ - [ํ˜„์‹ค์ ์œผ๋กœ ์†Œ์‹ฑ ๊ฐ€๋Šฅํ•œ ๊ตฌ์ฒด์  ๋ฐฉ๋ฒ• ํ•œ์ค„]
1115
+ 5. [์ „๋žต๋ช…]
1116
+ - [ํ˜„์‹ค์ ์œผ๋กœ ์†Œ์‹ฑ ๊ฐ€๋Šฅํ•œ ๊ตฌ์ฒด์  ๋ฐฉ๋ฒ• ํ•œ์ค„]
1117
+ """
1118
+
1119
+ result = self.call_llm_with_retry(prompt, f"3๋‹จ๊ณ„-์†Œ์‹ฑ์ „๋žต-{keyword}")
1120
+ return self.clean_markdown_and_bold(result)
1121
+
1122
+ def analyze_step4_product_recommendation(self, keyword: str, previous_results: str) -> str:
1123
+ """4๋‹จ๊ณ„. ์ฐจ๋ณ„ํ™” ์˜ˆ์‹œ๋ณ„ ์ƒํ’ˆ 5๊ฐ€์ง€ ์ถ”์ฒœ"""
1124
+
1125
+ prompt = f"""
1126
+ ๋‹น์‹ ์€ ์ดˆ๋ณด ์…€๋Ÿฌ๊ฐ€ ์ƒํ’ˆ ํŒ๋งค ์„ฑ๊ณต์„ ๋น ๋ฅด๊ฒŒ ์ด๋ฃฐ ์ˆ˜ ์žˆ๋„๋ก ๋•๋Š” ์ตœ๊ณ ์˜ ์ƒํ’ˆ ์†Œ์‹ฑ ๋ฐ ์ƒํ’ˆ๊ธฐํš ์ปจ์„คํ„ดํŠธ AI์ž…๋‹ˆ๋‹ค.
1127
+ ๋ถ„์„ ํ‚ค์›Œ๋“œ: '{keyword}'
1128
+ ์ด์ „ ๋ถ„์„ ๊ฒฐ๊ณผ:
1129
+ {previous_results}
1130
+ 4๋‹จ๊ณ„. ์ฐจ๋ณ„ํ™” ์˜ˆ์‹œ๋ณ„ ์ƒํ’ˆ 5๊ฐ€์ง€ ์ถ”์ฒœ
1131
+ 3๋‹จ๊ณ„์—์„œ ๋„์ถœํ•œ ์ฐจ๋ณ„ํ™” ์š”์†Œ๋ฅผ ๋ฐ˜์˜ํ•˜์—ฌ ๋งค์ถœ ๊ฐ€๋Šฅ์„ฑ์ด ๋†’์€ ์ˆœ์„œ๋Œ€๋กœ ๋ถ„์„ํ•ด์ฃผ์„ธ์š”.
1132
+ ๋‹ค์Œ ํ˜•์‹์œผ๋กœ ๋ถ„์„ํ•ด์ฃผ์„ธ์š” (๋ณผ๋“œ, ๋งˆํฌ๋‹ค์šด ์‚ฌ์šฉ ๊ธˆ์ง€):
1133
+ ์ฐจ๋ณ„ํ™” ์ƒํ’ˆ ์ถ”์ฒœ
1134
+ 1. [๊ตฌ์ฒด์ ์ธ ์ƒํ’ˆ๋ช…๊ณผ ์„ธ๋ถ€ ํŠน์ง•]
1135
+ - [์ฃผ์š” ํŠน์ง•๋“ค, ํƒ€๊ฒŸ ๊ณ ๊ฐ, ์ฐจ๋ณ„ํ™” ํฌ์ธํŠธ๋ฅผ ํ•œ๋ฌธ์žฅ์œผ๋กœ]
1136
+ 2. [๊ตฌ์ฒด์ ์ธ ์ƒํ’ˆ๋ช…๊ณผ ์„ธ๋ถ€ ํŠน์ง•]
1137
+ - [์ฃผ์š” ํŠน์ง•๋“ค, ํƒ€๊ฒŸ ๊ณ ๊ฐ, ์ฐจ๋ณ„ํ™” ํฌ์ธํŠธ๋ฅผ ํ•œ๋ฌธ์žฅ์œผ๋กœ]
1138
+ 3. [๊ตฌ์ฒด์ ์ธ ์ƒํ’ˆ๋ช…๊ณผ ์„ธ๋ถ€ ํŠน์ง•]
1139
+ - [์ฃผ์š” ํŠน์ง•๋“ค, ํƒ€๊ฒŸ ๊ณ ๊ฐ, ์ฐจ๋ณ„ํ™” ํฌ์ธํŠธ๋ฅผ ํ•œ๋ฌธ์žฅ์œผ๋กœ]
1140
+ 4. [๊ตฌ์ฒด์ ์ธ ์ƒํ’ˆ๋ช…๊ณผ ์„ธ๋ถ€ ํŠน์ง•]
1141
+ - [์ฃผ์š” ํŠน์ง•๋“ค, ํƒ€๊ฒŸ ๊ณ ๊ฐ, ์ฐจ๋ณ„ํ™” ํฌ์ธํŠธ๋ฅผ ํ•œ๋ฌธ์žฅ์œผ๋กœ]
1142
+ 5. [๊ตฌ์ฒด์ ์ธ ์ƒํ’ˆ๋ช…๊ณผ ์„ธ๋ถ€ ํŠน์ง•]
1143
+ - [์ฃผ์š” ํŠน์ง•๋“ค, ํƒ€๊ฒŸ ๊ณ ๊ฐ, ์ฐจ๋ณ„ํ™” ํฌ์ธํŠธ๋ฅผ ํ•œ๋ฌธ์žฅ์œผ๋กœ]
1144
+ ๋Œ€ํ‘œ์ด๋ฏธ์ง€ ์ถ”์ฒœ
1145
+ 1. [์ฒซ ๋ฒˆ์งธ ์ƒํ’ˆ๋ช…]
1146
+ * [๊ฐ„๋‹จํ•œ ์ดฌ์˜ ์ปจ์…‰๊ณผ ํ•ต์‹ฌ ํฌ์ธํŠธ ํ•œ์ค„]
1147
+ 2. [๋‘ ๋ฒˆ์งธ ์ƒํ’ˆ๋ช…]
1148
+ * [๊ฐ„๋‹จํ•œ ์ดฌ์˜ ์ปจ์…‰๊ณผ ํ•ต์‹ฌ ํฌ์ธํŠธ ํ•œ์ค„]
1149
+ 3. [์„ธ ๋ฒˆ์งธ ์ƒํ’ˆ๋ช…]
1150
+ * [๊ฐ„๋‹จํ•œ ์ดฌ์˜ ์ปจ์…‰๊ณผ ํ•ต์‹ฌ ํฌ์ธํŠธ ํ•œ์ค„]
1151
+ 4. [๋„ค ๋ฒˆ์งธ ์ƒํ’ˆ๋ช…]
1152
+ * [๊ฐ„๋‹จํ•œ ์ดฌ์˜ ์ปจ์…‰๊ณผ ํ•ต์‹ฌ ํฌ์ธํŠธ ํ•œ์ค„]
1153
+ 5. [๋‹ค์„ฏ ๋ฒˆ์งธ ์ƒํ’ˆ๋ช…]
1154
+ * [๊ฐ„๋‹จํ•œ ์ดฌ์˜ ์ปจ์…‰๊ณผ ํ•ต์‹ฌ ํฌ์ธํŠธ ํ•œ์ค„]
1155
+ """
1156
+
1157
+ result = self.call_llm_with_retry(prompt, f"4๋‹จ๊ณ„-์ƒํ’ˆ์ถ”์ฒœ-{keyword}")
1158
+ return self.clean_markdown_and_bold(result)
1159
+
1160
+ def analyze_step5_trust_building(self, keyword: str, previous_results: str) -> str:
1161
+ """5๋‹จ๊ณ„. ์‹ ๋ขฐ์„ฑ์„ ์ค„ ์ˆ˜ ์žˆ๋Š” ์š”์†Œ 5๊ฐ€์ง€"""
1162
+
1163
+ prompt = f"""
1164
+ ๋‹น์‹ ์€ ์ดˆ๋ณด ์…€๋Ÿฌ๊ฐ€ ์ƒํ’ˆ ํŒ๋งค ์„ฑ๊ณต์„ ๋น ๋ฅด๊ฒŒ ์ด๋ฃฐ ์ˆ˜ ์žˆ๋„๋ก ๋•๋Š” ์ตœ๊ณ ์˜ ์ƒํ’ˆ ์†Œ์‹ฑ ๋ฐ ์ƒํ’ˆ๊ธฐํš ์ปจ์„คํ„ดํŠธ AI์ž…๋‹ˆ๋‹ค.
1165
+ ๋ถ„์„ ํ‚ค์›Œ๋“œ: '{keyword}'
1166
+ ์ด์ „ ๋ถ„์„ ๊ฒฐ๊ณผ:
1167
+ {previous_results}
1168
+ 5๋‹จ๊ณ„. ์‹ ๋ขฐ์„ฑ์„ ์ค„ ์ˆ˜ ์žˆ๋Š” ์š”์†Œ 5๊ฐ€์ง€
1169
+ ๋‹ค์Œ ํ˜•์‹์œผ๋กœ ๋ถ„์„ํ•ด์ฃผ์„ธ์š” (๋ณผ๋“œ, ๋งˆํฌ๋‹ค์šด ์‚ฌ์šฉ ๊ธˆ์ง€):
1170
+ 1. [์‹ ๋ขฐ์„ฑ ์š”์†Œ1]
1171
+ - [๊ตฌ์ฒด์  ๋ฐฉ๋ฒ•๊ณผ ์ ์šฉ ์˜ˆ์‹œ]
1172
+ 2. [์‹ ๋ขฐ์„ฑ ์š”์†Œ2]
1173
+ - [๊ตฌ์ฒด์  ๋ฐฉ๋ฒ•๊ณผ ์ ์šฉ ์˜ˆ์‹œ]
1174
+ 3. [์‹ ๋ขฐ์„ฑ ์š”์†Œ3]
1175
+ - [๊ตฌ์ฒด์  ๋ฐฉ๋ฒ•๊ณผ ์ ์šฉ ์˜ˆ์‹œ]
1176
+ 4. [์‹ ๋ขฐ์„ฑ ์š”์†Œ4]
1177
+ - [๊ตฌ์ฒด์  ๋ฐฉ๋ฒ•๊ณผ ์ ์šฉ ์˜ˆ์‹œ]
1178
+ 5. [์‹ ๋ขฐ์„ฑ ์š”์†Œ5]
1179
+ - [๊ตฌ์ฒด์  ๋ฐฉ๋ฒ•๊ณผ ์ ์šฉ ์˜ˆ์‹œ]
1180
+ """
1181
+
1182
+ result = self.call_llm_with_retry(prompt, f"5๋‹จ๊ณ„-์‹ ๋ขฐ์„ฑ๊ตฌ์ถ•-{keyword}")
1183
+ return self.clean_markdown_and_bold(result)
1184
+
1185
+ def analyze_step6_usp_development(self, keyword: str, previous_results: str) -> str:
1186
+ """6๋‹จ๊ณ„. ์ฐจ๋ณ„ํ™” ์˜ˆ์‹œ๋ณ„ USP 5๊ฐ€์ง€"""
1187
+
1188
+ prompt = f"""
1189
+ ๋‹น์‹ ์€ ์ดˆ๋ณด ์…€๋Ÿฌ๊ฐ€ ์ƒํ’ˆ ํŒ๋งค ์„ฑ๊ณต์„ ๋น ๋ฅด๊ฒŒ ์ด๋ฃฐ ์ˆ˜ ์žˆ๋„๋ก ๋•๋Š” ์ตœ๊ณ ์˜ ์ƒํ’ˆ ์†Œ์‹ฑ ๋ฐ ์ƒํ’ˆ๊ธฐํš ์ปจ์„คํ„ดํŠธ AI์ž…๋‹ˆ๋‹ค.
1190
+ ๋ถ„์„ ํ‚ค์›Œ๋“œ: '{keyword}'
1191
+ ์ด์ „ ๋ถ„์„ ๊ฒฐ๊ณผ:
1192
+ {previous_results}
1193
+ 6๋‹จ๊ณ„. ์ฐจ๋ณ„ํ™” ์˜ˆ์‹œ๋ณ„ USP 5๊ฐ€์ง€
1194
+ 4๋‹จ๊ณ„์—์„œ ์ถ”์ฒœํ•œ 5๊ฐ€์ง€ ์ƒํ’ˆ๊ณผ ์—ฐ๊ฒฐํ•˜์—ฌ ๊ฐ๊ฐ์˜ USP๋ฅผ ์ œ์‹œํ•ด์ฃผ์„ธ์š”.
1195
+ ๋‹ค์Œ ํ˜•์‹์œผ๋กœ ๋ถ„์„ํ•ด์ฃผ์„ธ์š” (๋ณผ๋“œ, ๋งˆํฌ๋‹ค์šด ์‚ฌ์šฉ ๊ธˆ์ง€):
1196
+ 1. [์ฒซ ๋ฒˆ์งธ ์ƒํ’ˆ์˜ USP ์ œ๋ชฉ]
1197
+ - [ํ•ต์‹ฌ ๊ฐ€์น˜ ์ œ์•ˆ๊ณผ ์ฐจ๋ณ„ํ™” ํฌ์ธํŠธ ๊ตฌ์ฒด์  ์„ค๋ช…]
1198
+ 2. [๋‘ ๋ฒˆ์งธ ์ƒํ’ˆ์˜ USP ์ œ๋ชฉ]
1199
+ - [ํ•ต์‹ฌ ๊ฐ€์น˜ ์ œ์•ˆ๊ณผ ์ฐจ๋ณ„ํ™” ํฌ์ธํŠธ ๊ตฌ์ฒด์  ์„ค๋ช…]
1200
+ 3. [์„ธ ๋ฒˆ์งธ ์ƒํ’ˆ์˜ USP ์ œ๋ชฉ]
1201
+ - [ํ•ต์‹ฌ ๊ฐ€์น˜ ์ œ์•ˆ๊ณผ ์ฐจ๋ณ„ํ™” ํฌ์ธํŠธ ๊ตฌ์ฒด์  ์„ค๋ช…]
1202
+ 4. [๋„ค ๋ฒˆ์งธ ์ƒํ’ˆ์˜ USP ์ œ๋ชฉ]
1203
+ - [ํ•ต์‹ฌ ๊ฐ€์น˜ ์ œ์•ˆ๊ณผ ์ฐจ๋ณ„ํ™” ํฌ์ธํŠธ ๊ตฌ์ฒด์  ์„ค๋ช…]
1204
+ 5. [๋‹ค์„ฏ ๋ฒˆ์งธ ์ƒํ’ˆ์˜ USP ์ œ๋ชฉ]
1205
+ - [ํ•ต์‹ฌ ๊ฐ€์น˜ ์ œ์•ˆ๊ณผ ์ฐจ๋ณ„ํ™” ํฌ์ธํŠธ ๊ตฌ์ฒด์  ์„ค๋ช…]
1206
+ """
1207
+
1208
+ result = self.call_llm_with_retry(prompt, f"6๋‹จ๊ณ„-USP๊ฐœ๋ฐœ-{keyword}")
1209
+ return self.clean_markdown_and_bold(result)
1210
+
1211
+ def analyze_step7_copy_creation(self, keyword: str, previous_results: str) -> str:
1212
+ """7๋‹จ๊ณ„. USP๋ณ„ ์ƒ์„ธํŽ˜์ด์ง€ ํ—ค๋“œ ์นดํ”ผ - ์ด๋ชจํ‹ฐ์ฝ˜ ์ œ๊ฑฐ"""
1213
+
1214
+ prompt = f"""
1215
+ ๋‹น์‹ ์€ ์ดˆ๋ณด ์…€๋Ÿฌ๊ฐ€ ์ƒํ’ˆ ํŒ๋งค ์„ฑ๊ณต์„ ๋น ๋ฅด๊ฒŒ ์ด๋ฃฐ ์ˆ˜ ์žˆ๋„๋ก ๋•๋Š” ์ตœ๊ณ ์˜ ์ƒํ’ˆ ์†Œ์‹ฑ ๋ฐ ์ƒํ’ˆ๊ธฐํš ์ปจ์„คํ„ดํŠธ AI์ž…๋‹ˆ๋‹ค.
1216
+ ๋ถ„์„ ํ‚ค์›Œ๋“œ: '{keyword}'
1217
+ ์ด์ „ ๋ถ„์„ ๊ฒฐ๊ณผ:
1218
+ {previous_results}
1219
+ 7๋‹จ๊ณ„. USP๋ณ„ ์ƒ์„ธํŽ˜์ด์ง€ ํ—ค๋“œ ์นดํ”ผ
1220
+ 6๋‹จ๊ณ„์—์„œ ์ œ์‹œํ•œ 5๊ฐ€์ง€ USP์™€ ์—ฐ๊ฒฐํ•˜์—ฌ ๊ฐ๊ฐ์˜ ํ—ค๋“œ ์นดํ”ผ๋ฅผ ์ œ์‹œํ•ด์ฃผ์„ธ์š”.
1221
+ ๋‹ค์Œ ํ˜•์‹์œผ๋กœ ๋ถ„์„ํ•ด์ฃผ์„ธ์š” (๋ณผ๋“œ, ๋งˆํฌ๋‹ค์šด, ์ด๋ชจํ‹ฐ์ฝ˜ ์‚ฌ์šฉ ๊ธˆ์ง€):
1222
+ 1. [์ฒซ ๋ฒˆ์งธ USP ์—ฐ๊ฒฐ ์นดํ”ผ]
1223
+ 2. [๋‘ ๋ฒˆ์งธ USP ์—ฐ๊ฒฐ ์นดํ”ผ]
1224
+ 3. [์„ธ ๋ฒˆ์งธ USP ์—ฐ๊ฒฐ ์นดํ”ผ]
1225
+ 4. [๋„ค ๋ฒˆ์งธ USP ์—ฐ๊ฒฐ ์นดํ”ผ]
1226
+ 5. [๋‹ค์„ฏ ๋ฒˆ์งธ USP ์—ฐ๊ฒฐ ์นดํ”ผ]
1227
+ ์ค‘์š”:
1228
+ - 30์ž ๋ฏธ๋งŒ์˜ ๊ฐ„๊ฒฐํ•œ ํ›„ํ‚น ๋ฌธ์žฅ๋งŒ ์ถœ๋ ฅ
1229
+ - ์ด๋ชจํ‹ฐ์ฝ˜ ์ ˆ๋Œ€ ์‚ฌ์šฉ ๊ธˆ์ง€ (๐Ÿ˜Ž, ๐ŸŽจ, โœจ, ๐ŸŽ, ๐Ÿ‘ ๋“ฑ)
1230
+ - ์ƒํ’ˆ ํŒ๋งค๋ฅผ ์œ„ํ•œ ์ˆœ์ˆ˜ ํ—ค๋“œ์นดํ”ผ๋งŒ ์ž‘์„ฑ
1231
+ """
1232
+
1233
+ result = self.call_llm_with_retry(prompt, f"7๋‹จ๊ณ„-์นดํ”ผ์ œ์ž‘-{keyword}")
1234
+ return self.clean_markdown_and_bold(result)
1235
+
1236
+ def analyze_conclusion_enhanced(self, keyword: str, previous_results: str, sourcing_strategy_result: str) -> str:
1237
+ """๊ฐœ์„ ๋œ ๊ฒฐ๋ก  ๋ถ„์„ - ๊ตฌ์ฒด์  ์›”๋ณ„ ์ง„์ž… ํƒ€์ด๋ฐ + 1-7๋‹จ๊ณ„ ์ข…ํ•ฉ๋ถ„์„ ๊ฐ•ํ™”"""
1238
+
1239
+ logger.info(f"๊ฐœ์„ ๋œ ๊ฒฐ๋ก  ๋ถ„์„ ์‹œ์ž‘: ํ‚ค์›Œ๋“œ='{keyword}'")
1240
+
1241
+ # ์ž…๋ ฅ ๋ฐ์ดํ„ฐ ์•ˆ์ „์„ฑ ํ™•์ธ
1242
+ if not sourcing_strategy_result or len(sourcing_strategy_result.strip()) < 10:
1243
+ logger.warning("์†Œ์‹ฑ์ „๋žต ๊ฒฐ๊ณผ๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค.")
1244
+ sourcing_strategy_result = "๊ธฐ๋ณธ ์†Œ์‹ฑ์ „๋žต ๋ถ„์„"
1245
+
1246
+ if not previous_results or len(previous_results.strip()) < 10:
1247
+ logger.warning("7๋‹จ๊ณ„ ๋ถ„์„ ๊ฒฐ๊ณผ๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค.")
1248
+ previous_results = "๊ธฐ๋ณธ 7๋‹จ๊ณ„ ๋ถ„์„"
1249
+
1250
+ # ํ˜„์žฌ ์›”๊ณผ ์—ฐ๋„ ์ •๋ณด
1251
+ current_date = datetime.now()
1252
+ current_month = current_date.month
1253
+ current_year = current_date.year
1254
+
1255
+ # 1-7๋‹จ๊ณ„ ํ•ต์‹ฌ ๋‚ด์šฉ ์ถ”์ถœ์„ ์œ„ํ•œ ํ”„๋กฌํ”„ํŠธ - ์‹ค์งˆ์  ๋„์›€ ์ค‘์‹ฌ
1256
+ comprehensive_prompt = f"""
1257
+ '{keyword}' ํ‚ค์›Œ๋“œ์— ๋Œ€ํ•œ ์ดˆ๋ณด์…€๋Ÿฌ ๋งž์ถค ์ข…ํ•ฉ ๊ฒฐ๋ก ์„ ์ž‘์„ฑํ•˜์„ธ์š”.
1258
+ ํ˜„์žฌ ์‹œ์ : {current_year}๋…„ {current_month}์›”
1259
+ ์‹ค์ œ ๋ฐ์ดํ„ฐ: {sourcing_strategy_result}
1260
+ ์ „์ฒด ๋ถ„์„ ๊ฒฐ๊ณผ: {previous_results}
1261
+ ๋‹ค์Œ ๊ตฌ์กฐ๋กœ 700-800์ž ๋ถ„๋Ÿ‰์˜ ์‹ค์งˆ์  ๋„์›€์ด ๋˜๋Š” ๊ฒฐ๋ก ์„ ์ž‘์„ฑํ•˜์„ธ์š”:
1262
+ 1. ์ฒซ ๋ฒˆ์งธ ๋ฌธ๋‹จ (350์ž ๋‚ด์™ธ) - ์‹ค์ œ ๋ฐ์ดํ„ฐ ๊ธฐ๋ฐ˜ ์ง„์ž… ๋ถ„์„:
1263
+ - '{keyword}'๋Š” [์‹ค์ œ ๊ฒ€์ƒ‰๋Ÿ‰ ์ˆ˜์น˜]ํšŒ ๊ฒ€์ƒ‰๋˜๋Š” ์ƒํ’ˆ์œผ๋กœ [์ƒํ’ˆ ํŠน์„ฑ]
1264
+ - **๊ด€์—ฌ๋„ ํŒ๋‹จ ์ด์œ ๋ฅผ ๊ตฌ์ฒด์ ์œผ๋กœ ์„ค๋ช…**:
1265
+ * ์ €๊ด€์—ฌ์ธ ๊ฒฝ์šฐ: "๋Œ€๊ธฐ์—… ๋…์ ์ด ์—†๊ณ , ๊ณ ๊ฐ์ด ๋ธŒ๋žœ๋“œ ์ƒ๊ด€์—†์ด [๊ตฌ์ฒด์  ๊ธฐ๋Šฅ]๋งŒ ๋˜๋ฉด ๋ฐ”๋กœ ๊ตฌ๋งคํ•˜๋Š” ํŠน์„ฑ"
1266
+ * ๊ณ ๊ด€์—ฌ์ธ ๊ฒฝ์šฐ: "[ํŠน์ • ๋Œ€๊ธฐ์—…/๋ธŒ๋žœ๋“œ]๊ฐ€ ์‹œ์žฅ์„ ๋…์ ํ•˜๊ณ  ์žˆ์–ด ๊ณ ๊ฐ์ด [๊ตฌ์ฒด์  ์š”์†Œ]๋ฅผ ์‹ ์ค‘ํžˆ ๋น„๊ต๊ฒ€ํ† ํ•˜๋Š” ํŠน์„ฑ"
1267
+ * ๋ณตํ•ฉ๊ด€์—ฌ์ธ ๊ฒฝ์šฐ: "[๊ตฌ์ฒด์  ๊ฐ€๊ฒฉ๋Œ€] ์ €๊ฐ€ํ˜•์€ ์ €๊ด€์—ฌ, [๊ตฌ์ฒด์  ๊ฐ€๊ฒฉ๋Œ€] ๊ณ ๊ฐ€ํ˜•์€ ๊ณ ๊ด€์—ฌ๋กœ ๋‚˜๋‰˜๋Š” ํŠน์„ฑ"
1268
+ - ํ˜„์žฌ {current_month}์›” ๊ธฐ์ค€ [์‹ค์ œ ํ”ผํฌ์›” ๋ฐ์ดํ„ฐ]์—์„œ ํ™•์ธ๋œ ๋ฐ”์™€ ๊ฐ™์ด [๊ตฌ์ฒด์  ์ง„์ž… ํƒ€์ด๋ฐ]
1269
+ - [์‹ค์ œ ์ƒ์Šนํญ ๋ฐ์ดํ„ฐ]๋ฅผ ๊ณ ๋ คํ•  ๋•Œ [๊ตฌ์ฒด์  ์›”๋ณ„ ์ค€๋น„ ์ผ์ •]
1270
+ 2. ๋‘ ๋ฒˆ์งธ ๋ฌธ๋‹จ (350์ž ๋‚ด์™ธ) - ๋ถ„์„ ๊ธฐ๋ฐ˜ ์‹คํ–‰ ์ „๋žต:
1271
+ - ๋ถ„์„๋œ ์ƒํ’ˆ ํŠน์„ฑ์ƒ [๊ตฌ์ฒด์  ํƒ€๊ฒŸ ๊ณ ๊ฐ๊ณผ ๊ทธ๋“ค์˜ ์‹ค์ œ ๋‹ˆ์ฆˆ]๊ฐ€ ํ•ต์‹ฌ์ด๋ฉฐ
1272
+ - [์‹ค์ œ ๋ถ„์„๋œ ์ฐจ๋ณ„ํ™” ํฌ์ธํŠธ]๋ฅผ ํ™œ์šฉํ•œ [๊ตฌ์ฒด์  ์†Œ์‹ฑ ๋ฐฉํ–ฅ์„ฑ]์ด ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค
1273
+ - [๋ถ„์„๋œ ์‹ ๋ขฐ์„ฑ ์š”์†Œ์™€ USP]๋ฅผ ํ†ตํ•ด [์‹ค์ œ ์ ์šฉ ๊ฐ€๋Šฅํ•œ ๋งˆ์ผ€ํŒ… ๋ฐฉ๋ฒ•]
1274
+ - ์ดˆ๋ณด์…€๋Ÿฌ๋Š” [๊ตฌ์ฒด์  ์ž๋ณธ ๊ทœ๋ชจ์™€ ๋ฆฌ์Šคํฌ]๋ฅผ ๊ณ ๋ คํ•˜์—ฌ [์‹ค์ œ ํ–‰๋™ ๊ฐ€์ด๋“œ]
1275
+ ์ค‘์š”์‚ฌํ•ญ:
1276
+ - ์‹ค์ œ ๊ฒ€์ƒ‰๋Ÿ‰, ํ”ผํฌ์›”, ์ƒ์Šน๋ฅ  ๋“ฑ ๊ตฌ์ฒด์  ์ˆ˜์น˜ ํ™œ์šฉ
1277
+ - "๋ช‡๋‹จ๊ณ„" ํ‘œํ˜„ ๊ธˆ์ง€, ์ž์—ฐ์Šค๋Ÿฌ์šด ๋ฌธ์žฅ์œผ๋กœ ์—ฐ๊ฒฐ
1278
+ - ์ถ”์ƒ์  ํ‘œํ˜„ ๋Œ€์‹  ์ดˆ๋ณด์…€๋Ÿฌ๊ฐ€ ๋ฐ”๋กœ ์ ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๊ตฌ์ฒด์  ๊ฐ€์ด๋“œ
1279
+ - ํ˜•์‹์  ๋‚ด์šฉ ์ œ๊ฑฐ, ์‹ค์งˆ์  ๋„์›€์ด ๋˜๋Š” ๋‚ด์šฉ๋งŒ ํฌํ•จ
1280
+ - ํ˜„์žฌ ์›”({current_month}์›”) ๊ธฐ์ค€ ์ฆ‰์‹œ ์‹คํ–‰ ๊ฐ€๋Šฅํ•œ ํ–‰๋™ ๊ณ„ํš ์ œ์‹œ
1281
+ """
1282
+
1283
+ try:
1284
+ logger.info("๊ฐœ์„ ๋œ ๊ฒฐ๋ก  LLM ํ˜ธ์ถœ ์‹œ์ž‘")
1285
+
1286
+ if self.gemini_model:
1287
+ response = self.gemini_model.generate_content(comprehensive_prompt)
1288
+ result = response.text.strip() if response and response.text else ""
1289
+
1290
+ if result and len(result) > 50:
1291
+ cleaned_result = self.clean_markdown_and_bold(result)
1292
+ logger.info(f"๊ฐœ์„ ๋œ ๊ฒฐ๋ก  ๋ถ„์„ ์„ฑ๊ณต: {len(cleaned_result)} ๋ฌธ์ž")
1293
+ return cleaned_result
1294
+ else:
1295
+ logger.warning("LLM ์‘๋‹ต์ด ๋น„์–ด์žˆ๊ฑฐ๋‚˜ ๋„ˆ๋ฌด ์งง์Šต๋‹ˆ๋‹ค.")
1296
+ else:
1297
+ logger.error("Gemini ๋ชจ๋ธ์ด ์—†์Šต๋‹ˆ๋‹ค.")
1298
+
1299
+ except Exception as e:
1300
+ logger.error(f"๊ฐœ์„ ๋œ ๊ฒฐ๋ก  ๋ถ„์„ LLM ํ˜ธ์ถœ ์˜ค๋ฅ˜: {e}")
1301
+
1302
+ # ํด๋ฐฑ ๊ฒฐ๋ก  ์ƒ์„ฑ
1303
+ logger.info("ํด๋ฐฑ ๊ฒฐ๋ก  ์ƒ์„ฑ")
1304
+ return f"""'{keyword}'๋Š” ์›” 15,000ํšŒ ์ด์ƒ ๊ฒ€์ƒ‰๋˜๋Š” ์•ˆ์ •์ ์ธ ์ƒํ’ˆ์œผ๋กœ, ํ˜„์žฌ {current_month}์›” ๊ธฐ์ค€ ์–ธ์ œ๋“  ์ง„์ž… ๊ฐ€๋Šฅํ•œ ์—ฐ์ค‘ ์ƒํ’ˆ์ž…๋‹ˆ๋‹ค. ๊ฒ€์ƒ‰๋Ÿ‰ ๋ถ„์„ ๊ฒฐ๊ณผ๋ฅผ ์ข…ํ•ฉํ•˜๋ฉด ์ดˆ๋ณด์…€๋Ÿฌ์—๊ฒŒ ๋ฆฌ์Šคํฌ๊ฐ€ ๋‚ฎ๊ณ  ๊พธ์ค€ํ•œ ์ˆ˜์š”๋ฅผ ํ™•๋ณดํ•  ์ˆ˜ ์žˆ๋Š” ์•„์ดํ…œ์œผ๋กœ ํŒ๋‹จ๋ฉ๋‹ˆ๋‹ค. ์ฒซ ๋‹ฌ 100-200๊ฐœ ์†Œ๋Ÿ‰ ์‹œ์ž‘์œผ๋กœ ์‹œ์žฅ ๋ฐ˜์‘์„ ํ™•์ธํ•œ ํ›„ ์ ์ง„์ ์œผ๋กœ ํ™•๋Œ€ํ•˜๋Š” ๊ฒƒ์ด ์•ˆ์ „ํ•œ ์ ‘๊ทผ๋ฒ•์ž…๋‹ˆ๋‹ค.
1305
+ ๋ถ„์„๋œ ์ƒํ’ˆ ํŠน์„ฑ์ƒ ํ’ˆ์งˆ๊ณผ ๋‚ด๊ตฌ์„ฑ์„ ์ค‘์‹œํ•˜๋Š” ์‹ค์šฉ์  ๊ตฌ๋งค์ธต์ด ์ฃผ ํƒ€๊ฒŸ์ด๋ฉฐ, AS ์„œ๋น„์Šค์™€ ํ’ˆ์งˆ๋ณด์ฆ์„œ ์ œ๊ณต์ด ์ฐจ๋ณ„ํ™”์˜ ํ•ต์‹ฌ์ž…๋‹ˆ๋‹ค. ๊ณ ๊ฐ ์‹ ๋ขฐ๋„ ๊ตฌ์ถ•์„ ์œ„ํ•ด์„œ๋Š” ์˜๋ฃŒ์ง„ ์ถ”์ฒœ์ด๋‚˜ ๊ณ ๊ฐ ์ฒดํ—˜๋‹ด ํ™œ์šฉ์ด ํšจ๊ณผ์ ์ด๋ฉฐ, ์ดˆ๋ณด์…€๋Ÿฌ๋Š” 10-20๋งŒ์› ์ˆ˜์ค€์˜ ์†Œ์•ก ํˆฌ์ž๋กœ ์‹œ์ž‘ํ•˜์—ฌ ์žฌ๊ตฌ๋งค์œจ ํ–ฅ์ƒ๊ณผ ์—ฐ๊ด€ ์ƒํ’ˆ ํ™•์žฅ์„ ํ†ตํ•œ ์•ˆ์ •์  ๋งค์ถœ ํ™•๋ณด๊ฐ€ ๊ถŒ์žฅ๋ฉ๋‹ˆ๋‹ค."""
1306
+
1307
+ def parse_step_sections(self, content: str, step_number: int) -> Dict[str, str]:
1308
+ """๋‹จ๊ณ„๋ณ„ ์†Œํ•ญ๋ชฉ ์„น์…˜ ํŒŒ์‹ฑ"""
1309
+
1310
+ if step_number >= 5:
1311
+ return {"๋‚ด์šฉ": content}
1312
+
1313
+ lines = content.split('\n')
1314
+ sections = {}
1315
+ current_section = None
1316
+ current_content = []
1317
+
1318
+ for line in lines:
1319
+ line = line.strip()
1320
+ if not line:
1321
+ continue
1322
+
1323
+ is_section_title = False
1324
+
1325
+ if step_number == 0:
1326
+ if any(keyword in line for keyword in ['์ƒํ’ˆ์œ ํ˜•', '๊ฐ€์žฅ ๊ฒ€์ƒ‰๋Ÿ‰์ด ๋งŽ์€ ์›”', '๊ฐ€์žฅ ์ƒ์Šนํญ์ด ๋†’์€ ์›”']):
1327
+ is_section_title = True
1328
+ elif step_number == 1:
1329
+ if any(keyword in line for keyword in ['์ฃผ์š”์œ ํ˜•', '๋ณด์กฐ์œ ํ˜•']):
1330
+ is_section_title = True
1331
+ elif step_number == 2:
1332
+ if any(keyword in line for keyword in ['๊ณ ๊ฐ์ƒํ™ฉ', 'ํŽ˜๋ฅด์†Œ๋‚˜', '์ฃผ์š” ๋‹ˆ์ฆˆ', '์ฃผ์š”๋‹ˆ์ฆˆ']):
1333
+ is_section_title = True
1334
+ elif step_number == 3:
1335
+ if any(keyword in line for keyword in ['ํ•ต์‹ฌ ๊ตฌ๋งค ๊ณ ๋ ค ์š”์†Œ', '์ฐจ๋ณ„ํ™” ์†Œ์‹ฑ ์ „๋žต', '๊ตฌ๋งค ๊ณ ๋ ค ์š”์†Œ', '์†Œ์‹ฑ ์ „๋žต']):
1336
+ is_section_title = True
1337
+ elif step_number == 4:
1338
+ if any(keyword in line for keyword in ['์ฐจ๋ณ„ํ™” ์ƒํ’ˆ ์ถ”์ฒœ', '๋Œ€ํ‘œ์ด๋ฏธ์ง€ ์ถ”์ฒœ']):
1339
+ is_section_title = True
1340
+ elif line.endswith(':'):
1341
+ is_section_title = True
1342
+
1343
+ if is_section_title:
1344
+ if current_section and current_content:
1345
+ sections[current_section] = '\n'.join(current_content)
1346
+
1347
+ current_section = line.replace(':', '').strip()
1348
+ current_content = []
1349
+ else:
1350
+ current_content.append(line)
1351
+
1352
+ if current_section and current_content:
1353
+ sections[current_section] = '\n'.join(current_content)
1354
+
1355
+ if not sections:
1356
+ return {"๋‚ด์šฉ": content}
1357
+
1358
+ return sections
1359
+
1360
+ def format_section_content(self, content: str) -> str:
1361
+ """์„น์…˜ ๋‚ด์šฉ ํฌ๋งทํŒ… - ์‹ฌํ”Œํ•œ ์•„์ด์ฝ˜์œผ๋กœ ๋ณ€๊ฒฝ"""
1362
+ lines = content.split('\n')
1363
+ formatted_lines = []
1364
+
1365
+ for line in lines:
1366
+ line = line.strip()
1367
+ if not line:
1368
+ continue
1369
+
1370
+ skip_patterns = [
1371
+ '์†Œ์‹ฑ์ „๋žต ๋ถ„์„', '1๋‹จ๊ณ„. ์ƒํ’ˆ์œ ํ˜• ๋ถ„์„', '4๋‹จ๊ณ„. ์ฐจ๋ณ„ํ™” ์˜ˆ์‹œ๋ณ„ ์ƒํ’ˆ 5๊ฐ€์ง€ ์ถ”์ฒœ',
1372
+ '5๋‹จ๊ณ„. ์‹ ๋ขฐ์„ฑ์„ ์ค„ ์ˆ˜ ์žˆ๋Š” ์š”์†Œ 5๊ฐ€์ง€', '6๋‹จ๊ณ„. ์ฐจ๋ณ„ํ™” ์˜ˆ์‹œ๋ณ„ USP 5๊ฐ€์ง€',
1373
+ '7๋‹จ๊ณ„. USP๋ณ„ ์ƒ์„ธํŽ˜์ด์ง€ ํ—ค๋“œ ์นดํ”ผ', '๊ฒฐ๋ก '
1374
+ ]
1375
+
1376
+ should_skip = False
1377
+ for pattern in skip_patterns:
1378
+ if pattern in line:
1379
+ should_skip = True
1380
+ break
1381
+
1382
+ if should_skip:
1383
+ continue
1384
+
1385
+ # ํ•ต์‹ฌ ์ œ๋ชฉ๋“ค
1386
+ if any(keyword in line for keyword in ['์ƒํ’ˆ์œ ํ˜•:', '๊ฐ€์žฅ ๊ฒ€์ƒ‰๋Ÿ‰์ด ๋งŽ์€ ์›”:', '๊ฐ€์žฅ ์ƒ์Šนํญ์ด ๋†’์€ ์›”:', '์ฃผ์š”์œ ํ˜•:', '๋ณด์กฐ์œ ํ˜•:', '๊ณ ๊ฐ์ƒํ™ฉ:', 'ํŽ˜๋ฅด์†Œ๋‚˜:', '์ฃผ์š” ๋‹ˆ์ฆˆ:', 'ํ•ต์‹ฌ ๊ตฌ๋งค ๊ณ ๋ ค ์š”์†Œ', '์ฐจ๋ณ„ํ™” ์†Œ์‹ฑ ์ „๋žต', '์ฐจ๋ณ„ํ™” ์ƒํ’ˆ ์ถ”์ฒœ', '๋Œ€ํ‘œ์ด๋ฏธ์ง€ ์ถ”์ฒœ']):
1387
+
1388
+ emoji_map = {
1389
+ '์ƒํ’ˆ์œ ํ˜•:': '๐Ÿ›๏ธ',
1390
+ '๊ฐ€์žฅ ๊ฒ€์ƒ‰๋Ÿ‰์ด ๋งŽ์€ ์›”:': '๐Ÿ“ˆ',
1391
+ '๊ฐ€์žฅ ์ƒ์Šนํญ์ด ๋†’์€ ์›”:': '๐Ÿš€',
1392
+ '์ฃผ์š”์œ ํ˜•:': '๐ŸŽฏ',
1393
+ '๋ณด์กฐ์œ ํ˜•:': '๐Ÿ“‹',
1394
+ '๊ณ ๊ฐ์ƒํ™ฉ:': '๐Ÿ‘ค',
1395
+ 'ํŽ˜๋ฅด์†Œ๋‚˜:': '๐ŸŽญ',
1396
+ '์ฃผ์š” ๋‹ˆ์ฆˆ:': '๐Ÿ’ก',
1397
+ 'ํ•ต์‹ฌ ๊ตฌ๋งค ๊ณ ๋ ค ์š”์†Œ': '๐Ÿ”',
1398
+ '์ฐจ๋ณ„ํ™” ์†Œ์‹ฑ ์ „๋žต': '๐ŸŽฏ',
1399
+ '์ฐจ๋ณ„ํ™” ์ƒํ’ˆ ์ถ”์ฒœ': '๐Ÿ’Ž',
1400
+ '๋Œ€ํ‘œ์ด๋ฏธ์ง€ ์ถ”์ฒœ': '๐Ÿ“ท'
1401
+ }
1402
+
1403
+ emoji = ""
1404
+ for key, value in emoji_map.items():
1405
+ if key in line:
1406
+ emoji = value + " "
1407
+ break
1408
+
1409
+ formatted_lines.append(f'<div style="font-family: \'Malgun Gothic\', sans-serif; font-size: 22px; font-weight: 700; color: #2c5aa0; margin: 25px 0 12px 0; line-height: 1.4;">{emoji}{line}</div>')
1410
+
1411
+ # ๋ฒˆํ˜ธ ๋ฆฌ์ŠคํŠธ ์ฒ˜๋ฆฌ
1412
+ elif re.match(r'^\d+\.', line):
1413
+ number = re.match(r'^(\d+)\.', line).group(1)
1414
+ number_emoji = ['1๏ธโƒฃ', '2๏ธโƒฃ', '3๏ธโƒฃ', '4๏ธโƒฃ', '5๏ธโƒฃ'][int(number)-1] if int(number) <= 5 else f"{number}."
1415
+ formatted_lines.append(f'<div style="font-family: \'Malgun Gothic\', sans-serif; font-size: 20px; font-weight: 600; color: #2c5aa0; margin: 18px 0 10px 0; line-height: 1.4;">{number_emoji} {line[len(number)+1:].strip()}</div>')
1416
+
1417
+ # - ๋˜๋Š” โ€ข ๋กœ ์‹œ์ž‘ํ•˜๋Š” ์„ค๋ช… - ์‹ฌํ”Œํ•œ ์•„์ด์ฝ˜
1418
+ elif line.startswith('-') or line.startswith('โ€ข'):
1419
+ clean_line = re.sub(r'^[-โ€ข]\s*', '', line)
1420
+ formatted_lines.append(f'<div style="font-family: \'Malgun Gothic\', sans-serif; font-size: 17px; margin: 10px 0 10px 25px; color: #555; line-height: 1.6;">โ€ข {clean_line}</div>')
1421
+
1422
+ # * ๋กœ ์‹œ์ž‘ํ•˜๋Š” ๋Œ€ํ‘œ์ด๋ฏธ์ง€ ์„ค๋ช…
1423
+ elif line.startswith('*'):
1424
+ clean_line = re.sub(r'^\*\s*', '', line)
1425
+ formatted_lines.append(f'<div style="font-family: \'Malgun Gothic\', sans-serif; font-size: 16px; margin: 8px 0 8px 40px; color: #e67e22; line-height: 1.5;">๐Ÿ“ธ {clean_line}</div>')
1426
+
1427
+ # ๋“ค์—ฌ์“ฐ๊ธฐ๋œ ์„ค๋ช…
1428
+ elif line.startswith(' ') or line.startswith('\t'):
1429
+ clean_line = line.lstrip()
1430
+ formatted_lines.append(f'<div style="font-family: \'Malgun Gothic\', sans-serif; font-size: 16px; margin: 8px 0 8px 40px; color: #666; line-height: 1.5;">โˆ˜ {clean_line}</div>')
1431
+
1432
+ # ์ผ๋ฐ˜ ํ…์ŠคํŠธ
1433
+ else:
1434
+ formatted_lines.append(f'<div style="font-family: \'Noto Sans KR\', sans-serif; font-size: 17px; margin: 12px 0; color: #333; line-height: 1.6;">{line}</div>')
1435
+
1436
+ return ''.join(formatted_lines)
1437
+
1438
+ def generate_step_html(self, step_title: str, content: str, step_number: int) -> str:
1439
+ """๊ฐœ๋ณ„ ๋‹จ๊ณ„ HTML ์ƒ์„ฑ"""
1440
+ sections = self.parse_step_sections(content, step_number)
1441
+
1442
+ sections_html = ""
1443
+
1444
+ if step_number >= 5:
1445
+ sections_html = self.format_section_content(content)
1446
+ else:
1447
+ if sections:
1448
+ for section_title, section_content in sections.items():
1449
+ sections_html += f"""
1450
+ <div style="margin-bottom: 25px; border: 1px solid #e0e0e0; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 4px rgba(0,0,0,0.05);">
1451
+ <div style="background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); padding: 15px; border-bottom: 1px solid #e0e0e0;">
1452
+ <div style="margin: 0; font-family: 'Malgun Gothic', sans-serif; font-size: 18px; font-weight: 600; color: #495057;">๐Ÿ”– {section_title}</div>
1453
+ </div>
1454
+ <div style="padding: 20px; background: #fefefe;">
1455
+ {self.format_section_content(section_content)}
1456
+ </div>
1457
+ </div>
1458
+ """
1459
+ else:
1460
+ sections_html = self.format_section_content(content)
1461
+
1462
+ step_emoji_map = {
1463
+ "์†Œ์‹ฑ์ „๋žต ๋ถ„์„": "๐Ÿ“Š",
1464
+ "1๋‹จ๊ณ„. ์ƒํ’ˆ์œ ํ˜• ๋ถ„์„": "๐ŸŽฏ",
1465
+ "2๋‹จ๊ณ„. ์†Œ๋น„์ž ํƒ€๊ฒŸ ์„ค์ •": "๐Ÿ‘ฅ",
1466
+ "3๋‹จ๊ณ„. ํƒ€๊ฒŸ๋ณ„ ์ฐจ๋ณ„ํ™”๋œ ์†Œ์‹ฑ ์ „๋žต ์ œ์•ˆ": "๐Ÿš€",
1467
+ "4๋‹จ๊ณ„. ์ฐจ๋ณ„ํ™” ์˜ˆ์‹œ๋ณ„ ์ƒํ’ˆ 5๊ฐ€์ง€ ์ถ”์ฒœ": "๐Ÿ’Ž",
1468
+ "5๋‹จ๊ณ„. ์‹ ๋ขฐ์„ฑ์„ ์ค„ ์ˆ˜ ์žˆ๋Š” ์š”์†Œ 5๊ฐ€์ง€": "๐Ÿ›ก๏ธ",
1469
+ "6๋‹จ๊ณ„. ์ฐจ๋ณ„ํ™” ์˜ˆ์‹œ๋ณ„ USP 5๊ฐ€์ง€": "โญ",
1470
+ "7๋‹จ๊ณ„. USP๋ณ„ ์ƒ์„ธํŽ˜์ด์ง€ ํ—ค๋“œ ์นดํ”ผ": "โœ๏ธ",
1471
+ "๊ฒฐ๋ก ": "๐ŸŽ‰"
1472
+ }
1473
+
1474
+ step_emoji = step_emoji_map.get(step_title, "๐Ÿ“‹")
1475
+
1476
+ return f"""
1477
+ <div style="margin-bottom: 35px; border: 2px solid #dee2e6; border-radius: 12px; overflow: hidden; box-shadow: 0 4px 8px rgba(0,0,0,0.1);">
1478
+ <div style="background: linear-gradient(135deg, #6c757d 0%, #495057 100%); padding: 20px; border-bottom: 2px solid #dee2e6;">
1479
+ <div style="margin: 0; font-family: 'Malgun Gothic', sans-serif; font-size: 22px; font-weight: 700; color: white;">{step_emoji} {step_title}</div>
1480
+ </div>
1481
+ <div style="padding: 30px; background: white;">
1482
+ {sections_html}
1483
+ </div>
1484
+ </div>
1485
+ """
1486
+
1487
+ def generate_final_html(self, keyword: str, all_steps: Dict[str, str]) -> str:
1488
+ """์ตœ์ข… HTML ๋ฆฌํฌํŠธ ์ƒ์„ฑ"""
1489
+
1490
+ steps_html = ""
1491
+ step_numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8]
1492
+
1493
+ for i, (step_title, content) in enumerate(all_steps.items(), 1):
1494
+ step_number = step_numbers[i-1] if i <= len(step_numbers) else i
1495
+ steps_html += self.generate_step_html(step_title, content, step_number)
1496
+
1497
+ return f"""
1498
+ <div style="max-width: 1000px; margin: 0 auto; padding: 25px; font-family: 'Noto Sans KR', -apple-system, BlinkMacSystemFont, sans-serif; background: #f8f9fa;">
1499
+ <div style="text-align: center; padding: 30px; margin-bottom: 35px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 12px; color: white; box-shadow: 0 6px 12px rgba(0,0,0,0.15);">
1500
+ <div style="margin: 0; font-family: 'Malgun Gothic', sans-serif; font-size: 28px; font-weight: 700; color: white;">๐Ÿ›’ {keyword} ํ‚ค์›Œ๋“œ ๋ถ„์„ ๋ฆฌํฌํŠธ</div>
1501
+ <div style="margin: 15px 0 0 0; font-size: 18px; color: #e9ecef;">์†Œ์‹ฑ์ „๋žต + 7๋‹จ๊ณ„ ๊ฐ„๊ฒฐ ๋ถ„์„ ๊ฒฐ๊ณผ</div>
1502
+ </div>
1503
+
1504
+ {steps_html}
1505
+
1506
+ <div style="text-align: center; padding: 20px; margin-top: 30px; background: #e9ecef; border-radius: 8px; color: #6c757d;">
1507
+ <div style="font-size: 14px;">๐Ÿ“ AI ์ƒํ’ˆ ์†Œ์‹ฑ ๋ถ„์„๊ธฐ v4.0 - ์ŠคํŽ˜์ด์Šค๋ฐ” ์ฒ˜๋ฆฌ ๊ฐœ์„  + ์˜ฌ๋ฐ”๋ฅธ ํŠธ๋ Œ๋“œ ๋ถ„์„ ๋กœ์ง</div>
1508
+ </div>
1509
+ </div>
1510
+ """
1511
+
1512
+ def analyze_keyword_complete(self, keyword: str, volume_data: Dict,
1513
+ keywords_df: Optional[pd.DataFrame], trend_data_1year=None, trend_data_3year=None) -> Dict[str, str]:
1514
+ """์ „์ฒด 8๋‹จ๊ณ„ ํ‚ค์›Œ๋“œ ๋ถ„์„ ์‹คํ–‰ (์†Œ์‹ฑ์ „๋žต + 7๋‹จ๊ณ„ + ๊ฐœ์„ ๋œ ๊ฒฐ๋ก )"""
1515
+
1516
+ logger.info(f"8๋‹จ๊ณ„ ํ‚ค์›Œ๋“œ ๋ถ„์„ ์‹œ์ž‘: '{keyword}'")
1517
+
1518
+ # 0๋‹จ๊ณ„: ๊ฐœ์„ ๋œ ์†Œ์‹ฑ์ „๋žต ๋ถ„์„
1519
+ sourcing_result = self.analyze_sourcing_strategy(keyword, volume_data, keywords_df, trend_data_1year, trend_data_3year)
1520
+ sourcing_html = self.generate_step_html("์†Œ์‹ฑ์ „๋žต ๋ถ„์„", sourcing_result, 0)
1521
+
1522
+ # 1-7๋‹จ๊ณ„ ๋ถ„์„ ๊ฒฐ๊ณผ๋ฅผ ์ €์žฅํ•  ๋”•์…”๋„ˆ๋ฆฌ
1523
+ step_results = {}
1524
+
1525
+ # 1๋‹จ๊ณ„: ์ƒํ’ˆ์œ ํ˜• ๋ถ„์„
1526
+ step1_result = self.analyze_step1_product_type(keyword, keywords_df)
1527
+ step_results["1๋‹จ๊ณ„"] = step1_result
1528
+ step1_html = self.generate_step_html("1๋‹จ๊ณ„. ์ƒํ’ˆ์œ ํ˜• ๋ถ„์„", step1_result, 1)
1529
+
1530
+ # 2๋‹จ๊ณ„: ์†Œ๋น„์ž ํƒ€๊ฒŸ ์„ค์ •
1531
+ step2_result = self.analyze_step2_target_customer(keyword, step1_result)
1532
+ step_results["2๋‹จ๊ณ„"] = step2_result
1533
+ step2_html = self.generate_step_html("2๋‹จ๊ณ„. ์†Œ๋น„์ž ํƒ€๊ฒŸ ์„ค์ •", step2_result, 2)
1534
+
1535
+ # 3๋‹จ๊ณ„: ์†Œ์‹ฑ ์ „๋žต
1536
+ previous_results = f"{step1_result}\n\n{step2_result}"
1537
+ step3_result = self.analyze_step3_sourcing_strategy(keyword, previous_results)
1538
+ step_results["3๋‹จ๊ณ„"] = step3_result
1539
+ step3_html = self.generate_step_html("3๋‹จ๊ณ„. ํƒ€๊ฒŸ๋ณ„ ์ฐจ๋ณ„ํ™”๋œ ์†Œ์‹ฑ ์ „๋žต ์ œ์•ˆ", step3_result, 3)
1540
+
1541
+ # 4๋‹จ๊ณ„: ์ƒํ’ˆ ์ถ”์ฒœ
1542
+ previous_results += f"\n\n{step3_result}"
1543
+ step4_result = self.analyze_step4_product_recommendation(keyword, previous_results)
1544
+ step_results["4๋‹จ๊ณ„"] = step4_result
1545
+ step4_html = self.generate_step_html("4๋‹จ๊ณ„. ์ฐจ๋ณ„ํ™” ์˜ˆ์‹œ๋ณ„ ์ƒํ’ˆ 5๊ฐ€์ง€ ์ถ”์ฒœ", step4_result, 4)
1546
+
1547
+ # 5๋‹จ๊ณ„: ์‹ ๋ขฐ์„ฑ ๊ตฌ์ถ•
1548
+ previous_results += f"\n\n{step4_result}"
1549
+ step5_result = self.analyze_step5_trust_building(keyword, previous_results)
1550
+ step_results["5๋‹จ๊ณ„"] = step5_result
1551
+ step5_html = self.generate_step_html("5๋‹จ๊ณ„. ์‹ ๋ขฐ์„ฑ์„ ์ค„ ์ˆ˜ ์žˆ๋Š” ์š”์†Œ 5๊ฐ€์ง€", step5_result, 5)
1552
+
1553
+ # 6๋‹จ๊ณ„: USP ๊ฐœ๋ฐœ
1554
+ previous_results += f"\n\n{step5_result}"
1555
+ step6_result = self.analyze_step6_usp_development(keyword, previous_results)
1556
+ step_results["6๋‹จ๊ณ„"] = step6_result
1557
+ step6_html = self.generate_step_html("6๋‹จ๊ณ„. ์ฐจ๋ณ„ํ™” ์˜ˆ์‹œ๋ณ„ USP 5๊ฐ€์ง€", step6_result, 6)
1558
+
1559
+ # 7๋‹จ๊ณ„: ์นดํ”ผ ์ œ์ž‘
1560
+ previous_results += f"\n\n{step6_result}"
1561
+ step7_result = self.analyze_step7_copy_creation(keyword, previous_results)
1562
+ step_results["7๋‹จ๊ณ„"] = step7_result
1563
+ step7_html = self.generate_step_html("7๋‹จ๊ณ„. USP๋ณ„ ์ƒ์„ธํŽ˜์ด์ง€ ํ—ค๋“œ ์นดํ”ผ", step7_result, 7)
1564
+
1565
+ # ๊ฐœ์„ ๋œ ๊ฒฐ๋ก : ๊ตฌ์ฒด์  ์›”๋ณ„ ์ง„์ž… ํƒ€์ด๋ฐ + 1-7๋‹จ๊ณ„ ์ข…ํ•ฉ๋ถ„์„ ๊ฐ•ํ™”
1566
+ conclusion_result = self.analyze_conclusion_enhanced(keyword, previous_results + f"\n\n{step7_result}", sourcing_result)
1567
+ conclusion_html = self.generate_step_html("๊ฒฐ๋ก ", conclusion_result, 8)
1568
+
1569
+ # ์ „์ฒด HTML ์ƒ์„ฑ (์†Œ์‹ฑ์ „๋žต์ด ๋งจ ์œ„์— ์œ„์น˜)
1570
+ all_steps = {
1571
+ "์†Œ์‹ฑ์ „๋žต ๋ถ„์„": sourcing_result,
1572
+ "1๋‹จ๊ณ„. ์ƒํ’ˆ์œ ํ˜• ๋ถ„์„": step1_result,
1573
+ "2๋‹จ๊ณ„. ์†Œ๋น„์ž ํƒ€๊ฒŸ ์„ค์ •": step2_result,
1574
+ "3๋‹จ๊ณ„. ํƒ€๊ฒŸ๋ณ„ ์ฐจ๋ณ„ํ™”๋œ ์†Œ์‹ฑ ์ „๋žต ์ œ์•ˆ": step3_result,
1575
+ "4๋‹จ๊ณ„. ์ฐจ๋ณ„ํ™” ์˜ˆ์‹œ๋ณ„ ์ƒํ’ˆ 5๊ฐ€์ง€ ์ถ”์ฒœ": step4_result,
1576
+ "5๋‹จ๊ณ„. ์‹ ๋ขฐ์„ฑ์„ ์ค„ ์ˆ˜ ์žˆ๋Š” ์š”์†Œ 5๊ฐ€์ง€": step5_result,
1577
+ "6๋‹จ๊ณ„. ์ฐจ๋ณ„ํ™” ์˜ˆ์‹œ๋ณ„ USP 5๊ฐ€์ง€": step6_result,
1578
+ "7๋‹จ๊ณ„. USP๋ณ„ ์ƒ์„ธํŽ˜์ด์ง€ ํ—ค๋“œ ์นดํ”ผ": step7_result,
1579
+ "๊ฒฐ๋ก ": conclusion_result
1580
+ }
1581
+
1582
+ full_html = self.generate_final_html(keyword, all_steps)
1583
+
1584
+ # ๊ฐœ๋ณ„ ๋‹จ๊ณ„ HTML๊ณผ ์ „์ฒด HTML ๋ฐ˜ํ™˜
1585
+ return {
1586
+ "sourcing_html": self.generate_step_html("์†Œ์‹ฑ์ „๋žต ๋ถ„์„", sourcing_result, 0),
1587
+ "step1_html": step1_html,
1588
+ "step2_html": step2_html,
1589
+ "step3_html": step3_html,
1590
+ "step4_html": step4_html,
1591
+ "step5_html": step5_html,
1592
+ "step6_html": step6_html,
1593
+ "step7_html": step7_html,
1594
+ "conclusion_html": conclusion_html,
1595
+ "full_html": full_html,
1596
+ "results": all_steps
1597
+ }
1598
+
1599
+
1600
+ # ===== ๋ฉ”์ธ ๋ถ„์„ ํ•จ์ˆ˜๋“ค =====
1601
+
1602
+ def analyze_keyword_for_sourcing(analysis_keyword, volume_data, trend_data_1year=None,
1603
+ trend_data_3year=None, filtered_keywords_df=None,
1604
+ target_categories=None, gemini_model=None):
1605
+ """
1606
+ ๋ฉ”์ธ ๋ถ„์„ ํ•จ์ˆ˜ - ์†Œ์‹ฑ์ „๋žต + 7๋‹จ๊ณ„ ๊ฐ„๊ฒฐ ๋ถ„์„
1607
+ ๊ธฐ์กด ํ•จ์ˆ˜๋ช… ์œ ์ง€ํ•˜์—ฌ ํ˜ธํ™˜์„ฑ ํ™•๋ณด
1608
+ """
1609
+
1610
+ if not gemini_model:
1611
+ return generate_error_response("Gemini AI ๋ชจ๋ธ์ด ์ดˆ๊ธฐํ™”๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.")
1612
+
1613
+ try:
1614
+ logger.info(f"์†Œ์‹ฑ์ „๋žต + 7๋‹จ๊ณ„ ๊ฐ„๊ฒฐ ํ‚ค์›Œ๋“œ ๋ถ„์„ ์‹œ์ž‘: '{analysis_keyword}'")
1615
+
1616
+ analyzer = CompactKeywordAnalyzer(gemini_model)
1617
+ result = analyzer.analyze_keyword_complete(analysis_keyword, volume_data, filtered_keywords_df, trend_data_1year, trend_data_3year)
1618
+
1619
+ logger.info(f"์†Œ์‹ฑ์ „๋žต + 7๋‹จ๊ณ„ ๊ฐ„๊ฒฐ ํ‚ค์›Œ๋“œ ๋ถ„์„ ์™„๋ฃŒ: '{analysis_keyword}'")
1620
+
1621
+ # ๊ธฐ์กด ํ˜ธํ™˜์„ฑ์„ ์œ„ํ•ด full_html ๋ฐ˜ํ™˜
1622
+ return result["full_html"]
1623
+
1624
+ except Exception as e:
1625
+ logger.error(f"ํ‚ค์›Œ๋“œ ๋ถ„์„ ์˜ค๋ฅ˜: {e}")
1626
+ return generate_error_response(f"ํ‚ค์›Œ๋“œ ๋ถ„์„ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: {str(e)}")
1627
+
1628
+ def analyze_keyword_with_individual_steps(analysis_keyword, volume_data, trend_data_1year=None,
1629
+ trend_data_3year=None, filtered_keywords_df=None,
1630
+ target_categories=None, gemini_model=None):
1631
+ """
1632
+ ๊ฐœ๋ณ„ ๋‹จ๊ณ„ HTML์„ ํฌํ•จํ•œ ์ „์ฒด ๋ถ„์„ ํ•จ์ˆ˜
1633
+ ์†Œ์‹ฑ์ „๋žต + ๊ฐ 7๋‹จ๊ณ„๋ณ„ ๊ฐœ๋ณ„ HTML๊ณผ ์ „์ฒด HTML์„ ๋ชจ๋‘ ๋ฐ˜ํ™˜
1634
+ """
1635
+
1636
+ if not gemini_model:
1637
+ error_html = generate_error_response("Gemini AI ๋ชจ๋ธ์ด ์ดˆ๊ธฐํ™”๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.")
1638
+ return {
1639
+ "sourcing_html": error_html, "step1_html": error_html, "step2_html": error_html, "step3_html": error_html,
1640
+ "step4_html": error_html, "step5_html": error_html, "step6_html": error_html,
1641
+ "step7_html": error_html, "conclusion_html": error_html, "full_html": error_html,
1642
+ "results": {}
1643
+ }
1644
+
1645
+ try:
1646
+ logger.info(f"์†Œ์‹ฑ์ „๋žต + 7๋‹จ๊ณ„ ๊ฐœ๋ณ„ ํ‚ค์›Œ๋“œ ๋ถ„์„ ์‹œ์ž‘: '{analysis_keyword}'")
1647
+
1648
+ analyzer = CompactKeywordAnalyzer(gemini_model)
1649
+ result = analyzer.analyze_keyword_complete(analysis_keyword, volume_data, filtered_keywords_df, trend_data_1year, trend_data_3year)
1650
+
1651
+ logger.info(f"์†Œ์‹ฑ์ „๋žต + 7๋‹จ๊ณ„ ๊ฐœ๋ณ„ ํ‚ค์›Œ๋“œ ๋ถ„์„ ์™„๋ฃŒ: '{analysis_keyword}'")
1652
+ return result
1653
+
1654
+ except Exception as e:
1655
+ logger.error(f"ํ‚ค์›Œ๋“œ ๋ถ„์„ ์˜ค๋ฅ˜: {e}")
1656
+ error_html = generate_error_response(f"ํ‚ค์›Œ๋“œ ๋ถ„์„ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: {str(e)}")
1657
+ return {
1658
+ "sourcing_html": error_html, "step1_html": error_html, "step2_html": error_html, "step3_html": error_html,
1659
+ "step4_html": error_html, "step5_html": error_html, "step6_html": error_html,
1660
+ "step7_html": error_html, "conclusion_html": error_html, "full_html": error_html,
1661
+ "results": {}
1662
+ }
1663
+
1664
+ def generate_error_response(error_message):
1665
+ """์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋ฅผ ํ˜„์‹ค์  ์Šคํƒ€์ผ๋กœ ์ƒ์„ฑ"""
1666
+ return f'''
1667
+ <div style="color: #721c24; padding: 30px; text-align: center; width: 100%;
1668
+ background-color: #f8d7da; border-radius: 12px; border: 1px solid #f5c6cb; font-family: 'Pretendard', sans-serif;">
1669
+ <h3 style="margin-bottom: 15px; color: #721c24;">โŒ ๋ถ„์„ ์‹คํŒจ</h3>
1670
+ <p style="margin-bottom: 20px; font-size: 16px;">{error_message}</p>
1671
+
1672
+ <div style="background: white; padding: 20px; border-radius: 8px; color: #333; text-align: left;">
1673
+ <h4 style="color: #721c24; margin-bottom: 15px;">๐Ÿ”ง ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•</h4>
1674
+ <ul style="padding-left: 20px; line-height: 1.8;">
1675
+ <li>๐Ÿ” ํ‚ค์›Œ๋“œ ํ™•์ธ: ์˜ฌ๋ฐ”๋ฅธ ํ•œ๊ธ€ ํ‚ค์›Œ๋“œ์ธ์ง€ ํ™•์ธ</li>
1676
+ <li>๐Ÿ“Š ๊ฒ€์ƒ‰๋Ÿ‰ ํ™•์ธ: ๋„ˆ๋ฌด ์ƒ์†Œํ•œ ํ‚ค์›Œ๋“œ๋Š” ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์„ ์ˆ˜ ์žˆ์Œ</li>
1677
+ <li>๐ŸŒ ๋„คํŠธ์›Œํฌ ์ƒํƒœ: ์ธํ„ฐ๋„ท ์—ฐ๊ฒฐ ์ƒํƒœ ํ™•์ธ</li>
1678
+ <li>๐Ÿ”ง API ์ƒํƒœ: ๋„ค์ด๋ฒ„ API ์„œ๋ฒ„ ์ƒํƒœ ํ™•์ธ</li>
1679
+ <li>๐Ÿ”„ ์žฌ์‹œ๋„: ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด๋ณด์„ธ์š”</li>
1680
+ </ul>
1681
+ </div>
1682
+
1683
+ <div style="margin-top: 15px; padding: 10px; background: #d1ecf1; border-radius: 6px; color: #0c5460; font-size: 14px;">
1684
+ ๐Ÿ’ก ํŒ: 2๋‹จ๊ณ„์—์„œ ์ถ”์ถœ๋œ ํ‚ค์›Œ๋“œ ๋ชฉ๋ก์„ ์ฐธ๊ณ ํ•˜์—ฌ ๊ฒ€์ฆ๋œ ํ‚ค์›Œ๋“œ๋ฅผ ์‚ฌ์šฉํ•ด๋ณด์„ธ์š”.
1685
+ </div>
1686
+ </div>
1687
+ '''
keyword_analysis_report.css ADDED
@@ -0,0 +1,422 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ํ‚ค์›Œ๋“œ ๋ถ„์„ ๋ณด๊ณ ์„œ ์ „์šฉ CSS - ๋‹คํฌ๋ชจ๋“œ ์ ์šฉ */
2
+
3
+ /* CSS ๋ณ€์ˆ˜ ์ •์˜ (๋ผ์ดํŠธ๋ชจ๋“œ) */
4
+ :root {
5
+ --primary-color: #FB7F0D;
6
+ --text-color: #333;
7
+ --bg-color: #f8f9fa;
8
+ --card-bg: #ffffff;
9
+ --border-color: #ecf0f1;
10
+ --section-bg: #ffffff;
11
+ --insight-bg: #fff5e6;
12
+ --warning-bg: #fff3cd;
13
+ --warning-text: #856404;
14
+ --checklist-bg: #fff3cd;
15
+ --cross-sell-bg: #f8f9fa;
16
+ --cross-sell-border: #17a2b8;
17
+ --cross-sell-text: #0c5460;
18
+ --product-bg: white;
19
+ --trend-bg: #e3f2fd;
20
+ --trend-border: #2196f3;
21
+ --metric-bg: #f8f9fa;
22
+ --metric-border: #dee2e6;
23
+ --highlight-bg: #fff3cd;
24
+ }
25
+
26
+ /* ๋‹คํฌ๋ชจ๋“œ ์ƒ‰์ƒ ๋ณ€์ˆ˜ (์ž๋™ ๊ฐ์ง€) */
27
+ @media (prefers-color-scheme: dark) {
28
+ :root {
29
+ --text-color: #e5e5e5;
30
+ --bg-color: #1a1a1a;
31
+ --card-bg: #2d2d2d;
32
+ --border-color: #404040;
33
+ --section-bg: #2d2d2d;
34
+ --insight-bg: #3d2817;
35
+ --warning-bg: #3d3317;
36
+ --warning-text: #d4b75f;
37
+ --checklist-bg: #3d3317;
38
+ --cross-sell-bg: #1a1a1a;
39
+ --cross-sell-border: #17a2b8;
40
+ --cross-sell-text: #4dd0e1;
41
+ --product-bg: #2d2d2d;
42
+ --trend-bg: #1a2332;
43
+ --trend-border: #2196f3;
44
+ --metric-bg: #2d2d2d;
45
+ --metric-border: #404040;
46
+ --highlight-bg: #3d3317;
47
+ }
48
+ }
49
+
50
+ /* ์ˆ˜๋™ ๋‹คํฌ๋ชจ๋“œ ํด๋ž˜์Šค */
51
+ [data-theme="dark"],
52
+ .dark,
53
+ .gr-theme-dark {
54
+ --text-color: #e5e5e5;
55
+ --bg-color: #1a1a1a;
56
+ --card-bg: #2d2d2d;
57
+ --border-color: #404040;
58
+ --section-bg: #2d2d2d;
59
+ --insight-bg: #3d2817;
60
+ --warning-bg: #3d3317;
61
+ --warning-text: #d4b75f;
62
+ --checklist-bg: #3d3317;
63
+ --cross-sell-bg: #1a1a1a;
64
+ --cross-sell-border: #17a2b8;
65
+ --cross-sell-text: #4dd0e1;
66
+ --product-bg: #2d2d2d;
67
+ --trend-bg: #1a2332;
68
+ --trend-border: #2196f3;
69
+ --metric-bg: #2d2d2d;
70
+ --metric-border: #404040;
71
+ --highlight-bg: #3d3317;
72
+ }
73
+
74
+ .keyword-analysis-report {
75
+ font-family: 'Pretendard', 'Noto Sans KR', sans-serif;
76
+ line-height: 1.6;
77
+ color: var(--text-color);
78
+ margin: 0;
79
+ padding: 0;
80
+ background-color: var(--bg-color);
81
+ transition: background-color 0.3s ease, color 0.3s ease;
82
+ }
83
+
84
+ .report-container {
85
+ max-width: 900px;
86
+ margin: 20px auto;
87
+ padding: 0;
88
+ background: transparent;
89
+ }
90
+
91
+ .report-title {
92
+ text-align: center;
93
+ font-size: 2.2em;
94
+ margin-bottom: 30px;
95
+ color: var(--text-color);
96
+ font-weight: 700;
97
+ border-bottom: 3px solid var(--primary-color);
98
+ padding-bottom: 15px;
99
+ }
100
+
101
+ /* ๊ฐ ๋ถ„์„ ํ•ญ๋ชฉ๋ณ„ ์„น์…˜ ๋ธ”๋ก */
102
+ .analysis-section-block {
103
+ background-color: var(--section-bg);
104
+ padding: 25px 30px;
105
+ margin-bottom: 25px;
106
+ border-radius: 12px;
107
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
108
+ border-left: 5px solid var(--primary-color);
109
+ color: var(--text-color);
110
+ transition: background-color 0.3s ease, color 0.3s ease;
111
+ }
112
+
113
+ .analysis-section-block:nth-child(1) { border-left-color: #3498db; }
114
+ .analysis-section-block:nth-child(2) { border-left-color: #e74c3c; }
115
+ .analysis-section-block:nth-child(3) { border-left-color: #f39c12; }
116
+ .analysis-section-block:nth-child(4) { border-left-color: #9b59b6; }
117
+ .analysis-section-block:nth-child(5) { border-left-color: #1abc9c; }
118
+ .analysis-section-block:nth-child(6) { border-left-color: #34495e; }
119
+ .analysis-section-block:nth-child(7) { border-left-color: #e67e22; }
120
+
121
+ .analysis-section-title {
122
+ margin: 0 0 20px 0;
123
+ color: var(--text-color);
124
+ font-size: 1.6em;
125
+ font-weight: 700;
126
+ display: flex;
127
+ align-items: center;
128
+ border-bottom: 2px solid var(--border-color);
129
+ padding-bottom: 10px;
130
+ }
131
+
132
+ .section-icon {
133
+ font-size: 1.3em;
134
+ margin-right: 12px;
135
+ vertical-align: middle;
136
+ }
137
+
138
+ /* ์•„์ด์ฝ˜ ์ƒ‰์ƒ */
139
+ .analysis-section-block:nth-child(1) .section-icon { color: #3498db; }
140
+ .analysis-section-block:nth-child(2) .section-icon { color: #e74c3c; }
141
+ .analysis-section-block:nth-child(3) .section-icon { color: #f39c12; }
142
+ .analysis-section-block:nth-child(4) .section-icon { color: #9b59b6; }
143
+ .analysis-section-block:nth-child(5) .section-icon { color: var(--primary-color); }
144
+ .analysis-section-block:nth-child(6) .section-icon { color: #34495e; }
145
+ .analysis-section-block:nth-child(7) .section-icon { color: #e67e22; }
146
+
147
+ .subsection-title {
148
+ color: var(--text-color);
149
+ margin: 20px 0 10px 0;
150
+ font-size: 1.1em;
151
+ font-weight: 600;
152
+ }
153
+
154
+ .key-insight {
155
+ background-color: var(--insight-bg);
156
+ padding: 15px 20px;
157
+ border-left: 5px solid var(--primary-color);
158
+ margin: 20px 0;
159
+ border-radius: 5px;
160
+ font-weight: 500;
161
+ color: var(--text-color);
162
+ transition: background-color 0.3s ease;
163
+ }
164
+
165
+ /* ํ…์ŠคํŠธ ์Šคํƒ€์ผ - ๊ธฐ๋ณธ์€ ์ผ๋ฐ˜, ์ค‘์š”ํ•œ ๋ถ€๋ถ„๋งŒ ๋ณผ๋“œ */
166
+ .analysis-content {
167
+ color: var(--text-color);
168
+ font-weight: normal;
169
+ line-height: 1.7;
170
+ }
171
+
172
+ .analysis-content strong {
173
+ color: var(--text-color);
174
+ font-weight: 600;
175
+ }
176
+
177
+ .analysis-content p {
178
+ margin-bottom: 15px;
179
+ font-weight: normal;
180
+ color: var(--text-color);
181
+ }
182
+
183
+ .analysis-list {
184
+ list-style: none;
185
+ padding: 0;
186
+ margin-bottom: 20px;
187
+ }
188
+
189
+ .analysis-list li {
190
+ position: relative;
191
+ padding-left: 25px;
192
+ margin-bottom: 12px;
193
+ line-height: 1.8;
194
+ font-weight: normal;
195
+ color: var(--text-color);
196
+ }
197
+
198
+ .analysis-list li:before {
199
+ content: 'โ–ถ';
200
+ color: var(--primary-color);
201
+ position: absolute;
202
+ left: 0;
203
+ font-weight: bold;
204
+ font-size: 1.1em;
205
+ }
206
+
207
+ .concern-list {
208
+ list-style: none;
209
+ padding: 0;
210
+ margin-bottom: 20px;
211
+ }
212
+
213
+ .concern-list li {
214
+ position: relative;
215
+ padding-left: 25px;
216
+ margin-bottom: 12px;
217
+ line-height: 1.8;
218
+ font-weight: normal;
219
+ color: var(--text-color);
220
+ }
221
+
222
+ .concern-list li:before {
223
+ content: 'โš ๏ธ';
224
+ position: absolute;
225
+ left: 0;
226
+ font-size: 1.1em;
227
+ }
228
+
229
+ .solution-list {
230
+ list-style: none;
231
+ padding: 0;
232
+ margin-bottom: 20px;
233
+ }
234
+
235
+ .solution-list li {
236
+ position: relative;
237
+ padding-left: 25px;
238
+ margin-bottom: 12px;
239
+ line-height: 1.8;
240
+ font-weight: normal;
241
+ color: var(--text-color);
242
+ }
243
+
244
+ .solution-list li:before {
245
+ content: 'โœ…';
246
+ position: absolute;
247
+ left: 0;
248
+ font-size: 1.1em;
249
+ }
250
+
251
+ .checklist {
252
+ background-color: var(--checklist-bg);
253
+ padding: 20px;
254
+ border-radius: 8px;
255
+ border-left: 5px solid #ffc107;
256
+ margin: 20px 0;
257
+ transition: background-color 0.3s ease;
258
+ }
259
+
260
+ .checklist-title {
261
+ font-weight: 700;
262
+ color: var(--warning-text);
263
+ margin-bottom: 15px;
264
+ font-size: 1.2em;
265
+ }
266
+
267
+ .checklist-items {
268
+ list-style: none;
269
+ padding: 0;
270
+ }
271
+
272
+ .checklist-items li {
273
+ position: relative;
274
+ padding-left: 25px;
275
+ margin-bottom: 10px;
276
+ line-height: 1.6;
277
+ font-weight: normal;
278
+ color: var(--warning-text);
279
+ }
280
+
281
+ .checklist-items li:before {
282
+ content: '๐Ÿ“‹';
283
+ position: absolute;
284
+ left: 0;
285
+ font-size: 1.1em;
286
+ }
287
+
288
+ .cross-sell-section {
289
+ background-color: var(--cross-sell-bg);
290
+ padding: 20px;
291
+ border-radius: 8px;
292
+ border-left: 5px solid var(--cross-sell-border);
293
+ margin: 20px 0;
294
+ transition: background-color 0.3s ease;
295
+ }
296
+
297
+ .cross-sell-title {
298
+ font-weight: 700;
299
+ color: var(--cross-sell-text);
300
+ margin-bottom: 15px;
301
+ font-size: 1.2em;
302
+ }
303
+
304
+ .product-suggestion {
305
+ background-color: var(--product-bg);
306
+ padding: 15px;
307
+ border-radius: 6px;
308
+ margin-bottom: 15px;
309
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
310
+ transition: background-color 0.3s ease;
311
+ }
312
+
313
+ .product-name {
314
+ font-weight: 600;
315
+ color: var(--text-color);
316
+ margin-bottom: 8px;
317
+ font-size: 1.1em;
318
+ }
319
+
320
+ .product-reason {
321
+ color: var(--text-color);
322
+ font-size: 0.95em;
323
+ line-height: 1.5;
324
+ font-weight: normal;
325
+ opacity: 0.8;
326
+ }
327
+
328
+ .final-recommendation {
329
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
330
+ border-radius: 12px;
331
+ color: white;
332
+ padding: 25px;
333
+ }
334
+
335
+ .final-recommendation h3 {
336
+ color: white;
337
+ font-size: 1.8em;
338
+ margin-bottom: 20px;
339
+ font-weight: 700;
340
+ }
341
+
342
+ .recommendation-content {
343
+ font-size: 1.1em;
344
+ line-height: 1.7;
345
+ text-align: left;
346
+ font-weight: normal;
347
+ color: white;
348
+ }
349
+
350
+ .recommendation-content strong {
351
+ font-weight: 600;
352
+ color: white;
353
+ }
354
+
355
+ .trend-insight {
356
+ background-color: var(--trend-bg);
357
+ padding: 15px 20px;
358
+ border-left: 5px solid var(--trend-border);
359
+ margin: 20px 0;
360
+ border-radius: 5px;
361
+ color: var(--text-color);
362
+ transition: background-color 0.3s ease;
363
+ }
364
+
365
+ .market-metrics {
366
+ display: grid;
367
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
368
+ gap: 20px;
369
+ margin: 20px 0;
370
+ }
371
+
372
+ .metric-card {
373
+ background-color: var(--metric-bg);
374
+ padding: 20px;
375
+ border-radius: 8px;
376
+ text-align: center;
377
+ border: 1px solid var(--metric-border);
378
+ transition: background-color 0.3s ease, border-color 0.3s ease;
379
+ }
380
+
381
+ .metric-value {
382
+ font-size: 2em;
383
+ font-weight: 700;
384
+ color: var(--primary-color);
385
+ margin-bottom: 5px;
386
+ }
387
+
388
+ .metric-label {
389
+ color: var(--text-color);
390
+ font-size: 0.9em;
391
+ font-weight: normal;
392
+ opacity: 0.8;
393
+ }
394
+
395
+ .highlight-text {
396
+ background-color: var(--highlight-bg);
397
+ padding: 2px 6px;
398
+ border-radius: 3px;
399
+ font-weight: 600;
400
+ color: var(--warning-text);
401
+ transition: background-color 0.3s ease, color 0.3s ease;
402
+ }
403
+
404
+ /* ๋‹คํฌ๋ชจ๋“œ์—์„œ ๊ทธ๋ผ๋ฐ์ด์…˜ ๋ฐฐ๊ฒฝ ์กฐ์ • */
405
+ @media (prefers-color-scheme: dark) {
406
+ .final-recommendation {
407
+ background: linear-gradient(135deg, #4a5568 0%, #553c9a 100%);
408
+ }
409
+ }
410
+
411
+ [data-theme="dark"] .final-recommendation,
412
+ .dark .final-recommendation,
413
+ .gr-theme-dark .final-recommendation {
414
+ background: linear-gradient(135deg, #4a5568 0%, #553c9a 100%);
415
+ }
416
+
417
+ /* ์ „ํ™˜ ์• ๋‹ˆ๋ฉ”์ด์…˜ */
418
+ * {
419
+ transition: background-color 0.3s ease,
420
+ color 0.3s ease,
421
+ border-color 0.3s ease;
422
+ }
keyword_diversity_fix.py ADDED
@@ -0,0 +1,918 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+
3
+ logger.info("Gemini API ํ˜ธ์ถœ ์‹œ์ž‘...")
4
+ # Gemini API ํ˜ธ์ถœ - ์˜จ๋„๋ฅผ ๋†’์—ฌ์„œ ๋” ๋‹ค์–‘ํ•œ ๊ฒฐ๊ณผ ์ƒ์„ฑ
5
+ response = client.models.generate_content(
6
+ model="gemini-2.0-flash",
7
+ contents=prompt,
8
+ config=GenerateContentConfig(
9
+ tools=config_tools, # ๊ฒ€์ƒ‰ ์—”์ง„์— ๋”ฐ๋ผ ๋„๊ตฌ ์„ค์ •
10
+ response_modalities=["TEXT"],
11
+ temperature=0.95, # ์˜จ๋„๋ฅผ ๋†’์—ฌ์„œ ๋” ๋‹ค์–‘ํ•œ ๊ฒฐ๊ณผ
12
+ max_output_tokens=2000,
13
+ top_p=0.9, # ๋” ๋‹ค์–‘ํ•œ ํ† ํฐ ์„ ํƒ
14
+ top_k=40 # ํ›„๋ณด ํ† ํฐ ์ˆ˜ ์ฆ๊ฐ€
15
+ )
16
+ )
17
+
18
+ logger.info("Gemini API ์‘๋‹ต ์ˆ˜์‹  ์™„๋ฃŒ")
19
+
20
+ # ์‘๋‹ต์—์„œ ํ…์ŠคํŠธ ์ถ”์ถœ
21
+ result_text = ""
22
+ for part in response.candidates[0].content.parts:
23
+ if hasattr(part, 'text'):
24
+ result_text += part.text
25
+
26
+ # ๊ฒฐ๊ณผ ์ •์ œ - ๋ฒˆํ˜ธ, ์„ค๋ช…, ๊ธฐํ˜ธ ์ œ๊ฑฐ ๋ฐ ์ค‘๋ณต ์ œ๊ฑฐ ๊ฐ•ํ™”
27
+ lines = result_text.strip().split('\n')
28
+ clean_keywords = []
29
+ seen_keywords = set() # ์ค‘๋ณต ๋ฐฉ์ง€๋ฅผ ์œ„ํ•œ ์ง‘ํ•ฉ
30
+
31
+ for line in lines:
32
+ # ๋นˆ ์ค„ ๊ฑด๋„ˆ๋›ฐ๊ธฐ
33
+ if not line.strip():
34
+ continue
35
+
36
+ # ์ •์ œ ์ž‘์—…
37
+ clean_line = line.strip()
38
+
39
+ # ๋ฒˆํ˜ธ ์ œ๊ฑฐ (1., 2., 3. ๋“ฑ)
40
+ import re
41
+ clean_line = re.sub(r'^\d+\.?\s*', '', clean_line)
42
+
43
+ # ๋ถˆ๋ฆฟ ํฌ์ธํŠธ ์ œ๊ฑฐ (-, *, โ€ข, โœ…, โŒ ๋“ฑ)
44
+ clean_line = re.sub(r'^[-*โ€ขโœ…โŒ]\s*', '', clean_line)
45
+
46
+ # ๊ด„ํ˜ธ ์•ˆ ์„ค๋ช… ์ œ๊ฑฐ
47
+ clean_line = re.sub(r'\([^)]*\)', '', clean_line)
48
+
49
+ # ์ถ”๊ฐ€ ์„ค๋ช… ์ œ๊ฑฐ (: ์ดํ›„ ๋‚ด์šฉ)
50
+ if ':' in clean_line:
51
+ clean_line = clean_line.split(':')[0]
52
+
53
+ # ๊ณต๋ฐฑ ์ •๋ฆฌ
54
+ clean_line = clean_line.strip()
55
+
56
+ # ์œ ํšจํ•œ ํ‚ค์›Œ๋“œ๋งŒ ์ถ”๊ฐ€ (2๊ธ€์ž ์ด์ƒ, 50๊ธ€์ž ์ดํ•˜)
57
+ if clean_line and 2 <= len(clean_line) <= 50:
58
+ # ๋ธŒ๋žœ๋“œ๋ช…์ด๋‚˜ ์ œ์กฐ์—…์ฒด๋ช… ํ•„ํ„ฐ๋ง
59
+ brand_keywords = ['์‚ผ์„ฑ', '์—˜์ง€', 'LG', '์• ํ”Œ', '์•„์ดํฐ', '๊ฐค๋Ÿญ์‹œ', '๋‚˜์ดํ‚ค', '์•„๋””๋‹ค์Šค', '์Šคํƒ€๋ฒ…์Šค']
60
+ if not any(brand in clean_line for brand in brand_keywords):
61
+ # ์ค‘๋ณต ๊ฒ€์‚ฌ - ๋Œ€์†Œ๋ฌธ์ž ๊ตฌ๋ถ„ ์—†์ด ์ฒดํฌ
62
+ clean_lower = clean_line.lower()
63
+ if clean_lower not in seen_keywords:
64
+ seen_keywords.add(clean_lower)
65
+ clean_keywords.append(clean_line)
66
+
67
+ # 50๊ฐœ๋กœ ์ œํ•œํ•˜๋˜, ๋ถ€์กฑํ•˜๋ฉด ์ถ”๊ฐ€ ์ƒ์„ฑ ์š”์ฒญ
68
+ if len(clean_keywords) < 50:
69
+ logger.info(f"ํ‚ค์›Œ๋“œ ๋ถ€์กฑ ({len(clean_keywords)}๊ฐœ), ์ถ”๊ฐ€ ์ƒ์„ฑ ํ•„์š”")
70
+
71
+ # ๋ถ€์กฑํ•œ ๋งŒํผ ์ถ”๊ฐ€ ์ƒ์„ฑ์„ ์œ„ํ•œ ๋ณด์กฐ ํ”„๋กฌํ”„ํŠธ
72
+ additional_prompt = f"""
73
+ ๊ธฐ์กด์— ์ƒ์„ฑ๋œ ํ‚ค์›Œ๋“œ: {', '.join(clean_keywords)}
74
+
75
+ ์œ„ ํ‚ค์›Œ๋“œ๋“ค๊ณผ ์ ˆ๋Œ€ ์ค‘๋ณต๋˜์ง€ ์•Š๋Š” ์™„์ „ํžˆ ์ƒˆ๋กœ์šด {category} ๊ด€๋ จ ์‡ผํ•‘ํ‚ค์›Œ๋“œ๋ฅผ {50 - len(clean_keywords)}๊ฐœ ๋” ์ƒ์„ฑํ•˜์„ธ์š”.
76
+
77
+ ๋ฐ˜๋“œ์‹œ ์ง€์ผœ์•ผ ํ•  ๊ทœ์น™:
78
+ - ๊ธฐ์กด ํ‚ค์›Œ๋“œ์™€ ์ ˆ๋Œ€ ์ค‘๋ณต๋˜์ง€ ์•Š์Œ
79
+ - ์†Œ์žฌ, ํ˜•ํƒœ, ๊ธฐ๋Šฅ์„ ๋‹ค์–‘ํ•˜๊ฒŒ ์กฐํ•ฉ
80
+ - ๋ธŒ๋žœ๋“œ๋ช… ์ ˆ๋Œ€ ๊ธˆ์ง€
81
+ - ์ˆœ์ˆ˜ ํ‚ค์›Œ๋“œ๋งŒ ์ถœ๋ ฅ (๋ฒˆํ˜ธ, ์„ค๋ช…, ๊ธฐํ˜ธ ๊ธˆ์ง€)
82
+
83
+ ์˜ˆ์‹œ ์ถœ๋ ฅ:
84
+ ์Šคํ…Œ์ธ๋ฆฌ์Šค ์Ÿ๋ฐ˜
85
+ ๊ณ ๋ฌด ๋งคํŠธ
86
+ ์œ ๋ฆฌ ์šฉ๊ธฐ
87
+ """
88
+
89
+ # ์ถ”๊ฐ€ ์ƒ์„ฑ ์š”์ฒญ
90
+ additional_response = client.models.generate_content(
91
+ model="gemini-2.0-flash",
92
+ contents=additional_prompt,
93
+ config=GenerateContentConfig(
94
+ response_modalities=["TEXT"],
95
+ temperature=0.98, # ๋” ๋†’์€ ์˜จ๋„๋กœ ๋‹ค์–‘์„ฑ ๋ณด์žฅ
96
+ max_output_tokens=1000,
97
+ top_p=0.95,
98
+ top_k=50
99
+ )
100
+ )
101
+
102
+ # ์ถ”๊ฐ€ ํ‚ค์›Œ๋“œ ์ฒ˜๋ฆฌ
103
+ additional_text = ""
104
+ for part in additional_response.candidates[0].content.parts:
105
+ if hasattr(part, 'text'):
106
+ additional_text += part.text
107
+
108
+ additional_lines = additional_text.strip().split('\n')
109
+ for line in additional_lines:
110
+ if not line.strip():
111
+ continue
112
+
113
+ clean_line = line.strip()
114
+ clean_line = re.sub(r'^\d+\.?\s*', '', clean_line)
115
+ clean_line = re.sub(r'^[-*โ€ขโœ…โŒ]\s*', '', clean_line)
116
+ clean_line = re.sub(r'\([^)]*\)', '', clean_line)
117
+
118
+ if ':' in clean_line:
119
+ clean_line = clean_line.split(':')[0]
120
+
121
+ clean_line = clean_line.strip()
122
+
123
+ if clean_line and 2 <= len(clean_line) <= 50:
124
+ brand_keywords = ['์‚ผ์„ฑ', '์—˜์ง€', 'LG', '์• ํ”Œ', '์•„์ดํฐ', '๊ฐค๋Ÿญ์‹œ', '๋‚˜์ดํ‚ค', '์•„๋””๋‹ค์Šค', '์Šคํƒ€๋ฒ…์Šค']
125
+ if not any(brand in clean_line for brand in brand_keywords):
126
+ clean_lower = clean_line.lower()
127
+ if clean_lower not in seen_keywords and len(clean_keywords) < 50:
128
+ seen_keywords.add(clean_lower)
129
+ clean_keywords.append(clean_line)
130
+
131
+ # 50๊ฐœ๋กœ ์ œํ•œ
132
+ clean_keywords = clean_keywords[:50]
133
+
134
+ # ์ตœ์ข… ์…”ํ”Œ๋กœ ์ˆœ์„œ๋„ ๋ฌด์ž‘์œ„ํ™”
135
+ random.shuffle(clean_keywords)
136
+
137
+ # ์ตœ์ข… ๊ฒฐ๊ณผ ๋ฌธ์ž์—ด ์ƒ์„ฑ
138
+ final_result = '\n'.join(clean_keywords)
139
+
140
+ logger.info(f"์ตœ์ข… ์ •์ œ๋œ ํ‚ค์›Œ๋“œ ๊ฐœ์ˆ˜: {len(clean_keywords)}๊ฐœ")
141
+ logger.info(f"์ค‘๋ณต ์ œ๊ฑฐ๋œ ํ‚ค์›Œ๋“œ ์ˆ˜: {len(seen_keywords)}๊ฐœ")
142
+ logger.info("=== ๋‹ค์–‘์„ฑ ๊ฐ•ํ™” ์‡ผํ•‘ํ‚ค์›Œ๋“œ ์ƒ์„ฑ ์™„๋ฃŒ ===")
143
+
144
+ # ๊ทธ๋ผ์šด๋”ฉ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ๋กœ๊ทธ
145
+ if hasattr(response.candidates[0], 'grounding_metadata'):
146
+ logger.info("Google ๊ฒ€์ƒ‰ ๊ทธ๋ผ์šด๋”ฉ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ํ™•์ธ๋จ")
147
+ if hasattr(response.candidates[0].grounding_metadata, 'web_search_queries'):
148
+ queries = response.candidates[0].grounding_metadata.web_search_queries
149
+ logger.info(f"์‹คํ–‰๋œ ๊ฒ€์ƒ‰ ์ฟผ๋ฆฌ: {queries}")
150
+
151
+ return final_result
152
+
153
+ except Exception as e:
154
+ error_msg = f"์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e)}"
155
+ logger.error(error_msg)
156
+ logger.error("GEMINI_API_KEY ํ™˜๊ฒฝ๋ณ€์ˆ˜๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์„ค์ •๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•ด์ฃผ์„ธ์š”.")
157
+ return f"{error_msg}\n\nGEMINI_API_KEY ํ™˜๊ฒฝ๋ณ€์ˆ˜๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์„ค์ •๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•ด์ฃผ์„ธ์š”."
158
+ def generate_with_logs(category, additional_request, launch_timing, seasonality, sales_target, sales_channel, competition_level, search_engine):
159
+ """ํ‚ค์›Œ๋“œ ์ƒ์„ฑ๊ณผ ๋กœ๊ทธ๋ฅผ ํ•จ๊ป˜ ๋ฐ˜ํ™˜ํ•˜๋Š” ํ•จ์ˆ˜"""
160
+ logger.info("=== ๋‹ค์–‘์„ฑ ๊ฐ•ํ™” ์‡ผํ•‘ํ‚ค์›Œ๋“œ ์ƒ์„ฑ ์‹œ์ž‘ ===")
161
+
162
+ # ํ‚ค์›Œ๋“œ ์ƒ์„ฑ
163
+ result = generate_sourcing_keywords(category, additional_request, launch_timing, seasonality, sales_target, sales_channel, competition_level, search_engine)
164
+
165
+ # ์ตœ๊ทผ ๋กœ๊ทธ ๊ฐ€์ ธ์˜ค๊ธฐ
166
+ logs = get_recent_logs()
167
+
168
+ return result, logs
169
+ # Gradio ์ธํ„ฐํŽ˜์ด์Šค ๊ตฌ์„ฑ
170
+ def create_interface():
171
+ with gr.Blocks(
172
+ title="๐ŸŽฏ ๋‹ค์–‘์„ฑ ๊ฐ•ํ™” ์‡ผํ•‘ํ‚ค์›Œ๋“œ ์‹œ์Šคํ…œ",
173
+ theme=gr.themes.Soft(),
174
+ css="""
175
+ .gradio-container {
176
+ max-width: 1200px !important;
177
+ }
178
+ .title-header {
179
+ text-align: center;
180
+ background: linear-gradient(45deg, #FF6B6B, #4ECDC4, #45B7D1);
181
+ -webkit-background-clip: text;
182
+ -webkit-text-fill-color: transparent;
183
+ font-size: 2.5em;
184
+ font-weight: bold;
185
+ margin-bottom: 20px;
186
+ }
187
+ .subtitle {
188
+ text-align: center;
189
+ color: #666;
190
+ font-size: 1.2em;
191
+ margin-bottom: 30px;
192
+ }
193
+ """
194
+ ) as demo:
195
+
196
+ # ํ—ค๋”
197
+ gr.HTML("""
198
+ <div class="title-header">๐ŸŽฏ ๋‹ค์–‘์„ฑ ๊ฐ•ํ™” ์‡ผํ•‘ํ‚ค์›Œ๋“œ ์‹œ์Šคํ…œ</div>
199
+ <div class="subtitle">๐Ÿ”„ ๋งค๋ฒˆ ์™„์ „ํžˆ ๋‹ค๋ฅธ ๊ฒฐ๊ณผ! ์ค‘๋ณต ์—†๋Š” ์‡ผํ•‘ํ‚ค์›Œ๋“œ ์ „๋ฌธ ๋ฐœ๊ตด ํ”„๋กœ๊ทธ๋žจ</div>
200
+ """)
201
+
202
+ with gr.Row():
203
+ with gr.Column(scale=1):
204
+ gr.Markdown("### ๐Ÿ“Š ๋‹ค์–‘์„ฑ ๊ฐ•ํ™” ์„ค์ •")
205
+
206
+ # ๊ฒ€์ƒ‰ ์—”์ง„ ์„ ํƒ ์ถ”๊ฐ€
207
+ search_engine = gr.Dropdown(
208
+ choices=[
209
+ "๋ชจ๋“  ๊ฒ€์ƒ‰ ์—”์ง„ ํ†ตํ•ฉ ๋ถ„์„ (์ถ”์ฒœ)",
210
+ "Google ๊ฒ€์ƒ‰ ๊ทธ๋ผ์šด๋”ฉ๋งŒ",
211
+ "๋„ค์ด๋ฒ„ ๊ฒ€์ƒ‰ API๋งŒ",
212
+ "DuckDuckGo ๊ฒ€์ƒ‰๋งŒ",
213
+ "๊ฒ€์ƒ‰ ์—†์ด AI๋งŒ ์‚ฌ์šฉ"
214
+ ],
215
+ label="๐Ÿ” ๊ฒ€์ƒ‰ ์—”์ง„ ์„ ํƒ",
216
+ value="๋ชจ๋“  ๊ฒ€์ƒ‰ ์—”์ง„ ํ†ตํ•ฉ ๋ถ„์„ (์ถ”์ฒœ)"
217
+ )
218
+
219
+ # 1. ์‡ผํ•‘ ์นดํ…Œ๊ณ ๋ฆฌ ์„ ํƒ
220
+ category = gr.Dropdown(
221
+ choices=["๋žœ๋ค์ ์šฉ", "ํŒจ์…˜์žกํ™”", "์ƒํ™œ/๊ฑด๊ฐ•", "์ถœ์‚ฐ/์œก์•„", "์Šคํฌ์ธ /๋ ˆ์ €", "๋””์ง€ํ„ธ/๊ฐ€์ „", "๊ฐ€๊ตฌ/์ธํ…Œ๋ฆฌ์–ด", "ํŒจ์…˜์˜๋ฅ˜", "ํ™”์žฅํ’ˆ/๋ฏธ์šฉ"],
222
+ label="๐Ÿ›๏ธ ์‡ผํ•‘ ์นดํ…Œ๊ณ ๋ฆฌ",
223
+ value="์ƒํ™œ/๊ฑด๊ฐ•"
224
+ )
225
+
226
+ # 2. ์ถ”๊ฐ€ ์š”์ฒญ์‚ฌํ•ญ
227
+ additional_request = gr.Textbox(
228
+ label="๐Ÿ“ ์ถ”๊ฐ€ ์š”์ฒญ์‚ฌํ•ญ (๋‹ค์–‘์„ฑ ์ค‘์‹ฌ)",
229
+ placeholder="์˜ˆ: ๋‹ค์–‘ํ•œ ์†Œ์žฌ, ์ƒˆ๋กœ์šด ํ˜•ํƒœ, ๋…ํŠนํ•œ ๊ธฐ๋Šฅ ๋“ฑ",
230
+ lines=2
231
+ )
232
+
233
+ # 3. ์ถœ์‹œ ํƒ€์ด๋ฐ
234
+ launch_timing = gr.Radio(
235
+ choices=["๋žœ๋ค์ ์šฉ", "์ฆ‰์‹œ์†Œ์‹ฑ", "๊ธฐํšํ˜•"],
236
+ label="โฐ ์ถœ์‹œ ๏ฟฝ๏ฟฝ์ด๋ฐ",
237
+ value="์ฆ‰์‹œ์†Œ์‹ฑ"
238
+ )
239
+
240
+ # 4. ๊ณ„์ ˆ์„ฑ
241
+ seasonality = gr.Radio(
242
+ choices=["๋žœ๋ค์ ์šฉ", "๋ด„", "์—ฌ๋ฆ„", "๊ฐ€์„", "๊ฒจ์šธ", "๋น„๊ณ„์ ˆ"],
243
+ label="๐ŸŒฑ ๊ณ„์ ˆ์„ฑ",
244
+ value="๋น„๊ณ„์ ˆ"
245
+ )
246
+
247
+ with gr.Column(scale=1):
248
+ gr.Markdown("### ๐Ÿ’ฐ ๋ชฉํ‘œ ์„ค์ •")
249
+
250
+ # 5. ๋งค์ถœ ๋ชฉํ‘œ
251
+ sales_target = gr.Radio(
252
+ choices=["๋žœ๋ค์ ์šฉ", "100๋งŒ์› ์ดํ•˜", "100-500๋งŒ์›", "500-1์ฒœ๋งŒ์›", "1์ฒœ-5์ฒœ๋งŒ์›", "5์ฒœ๋งŒ์› ์ด์ƒ"],
253
+ label="๐Ÿ’ต ๋งค์ถœ ๋ชฉํ‘œ",
254
+ value="100-500๋งŒ์›"
255
+ )
256
+
257
+ # 6. ํŒ๋งค ์ฑ„๋„
258
+ sales_channel = gr.Radio(
259
+ choices=["๋žœ๋ค์ ์šฉ", "์˜คํ”ˆ๋งˆ์ผ“", "SNS๋งˆ์ผ€ํŒ…", "๊ด‘๊ณ ์ง‘ํ–‰", "์˜คํ”„๋ผ์ธ"],
260
+ label="๐Ÿ“ฑ ํŒ๋งค ์ฑ„๋„",
261
+ value="์˜คํ”ˆ๋งˆ์ผ“"
262
+ )
263
+
264
+ # 7. ๊ฒฝ์Ÿ ๊ฐ•๋„
265
+ competition_level = gr.Radio(
266
+ choices=[
267
+ "๋žœ๋ค์ ์šฉ",
268
+ "์ดˆ๋ณด",
269
+ "์ค‘์ˆ˜",
270
+ "๊ณ ์ˆ˜"
271
+ ],
272
+ label="โš”๏ธ ๊ฒฝ์Ÿ ๊ฐ•๋„",
273
+ value="์ดˆ๋ณด"
274
+ )
275
+
276
+ # ์‹คํ–‰ ๋ฒ„ํŠผ
277
+ generate_btn = gr.Button(
278
+ "๐Ÿš€ ๋‹ค์–‘์„ฑ ๊ฐ•ํ™” ์‡ผํ•‘ํ‚ค์›Œ๋“œ ๋ฐœ๊ตด ์‹œ์ž‘ (๋งค๋ฒˆ ๋‹ค๋ฅธ ๊ฒฐ๊ณผ)",
279
+ variant="primary",
280
+ size="lg"
281
+ )
282
+
283
+ # ๊ฒฐ๊ณผ ์ถœ๋ ฅ
284
+ with gr.Row():
285
+ with gr.Column(scale=2):
286
+ gr.Markdown("### ๐Ÿ“‹ ๋‹ค์–‘์„ฑ ๊ฐ•ํ™” ์‡ผํ•‘ํ‚ค์›Œ๋“œ (50๊ฐœ)")
287
+ output = gr.Textbox(
288
+ label="์ค‘๋ณต ์—†๋Š” ์‡ผํ•‘ํ‚ค์›Œ๋“œ ๊ฒฐ๊ณผ (๋งค๋ฒˆ ์™„์ „ํžˆ ๋‹ค๋ฆ„)",
289
+ lines=30,
290
+ max_lines=50,
291
+ placeholder="์—ฌ๊ธฐ์— ๋งค๋ฒˆ ๋‹ค๋ฅธ 50๊ฐœ์˜ ์‡ผํ•‘ํ‚ค์›Œ๋“œ๊ฐ€ ์ถœ๋ ฅ๋ฉ๋‹ˆ๋‹ค...",
292
+ show_copy_button=True
293
+ )
294
+
295
+ with gr.Column(scale=1):
296
+ gr.Markdown("### ๐Ÿ“Š ์‹คํ–‰ ๋กœ๊ทธ")
297
+ log_output = gr.Textbox(
298
+ label="์‹œ์Šคํ…œ ๋กœ๊ทธ",
299
+ lines=30,
300
+ max_lines=50,
301
+ placeholder="์‹œ์Šคํ…œ ์‹คํ–‰ ๋กœ๊ทธ๊ฐ€ ์—ฌ๊ธฐ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค...",
302
+ show_copy_button=True
303
+ )
304
+
305
+ # ์ด๋ฒคํŠธ ์—ฐ๊ฒฐ
306
+ generate_btn.click(
307
+ fn=generate_with_logs,
308
+ inputs=[
309
+ category,
310
+ additional_request,
311
+ launch_timing,
312
+ seasonality,
313
+ sales_target,
314
+ sales_channel,
315
+ competition_level,
316
+ search_engine
317
+ ],
318
+ outputs=[output, log_output],
319
+ show_progress=True
320
+ )
321
+
322
+ # ์‚ฌ์šฉ๋ฒ• ์•ˆ๋‚ด
323
+ with gr.Accordion("๐Ÿ“– ๋‹ค์–‘์„ฑ ๊ฐ•ํ™” ์‚ฌ์šฉ๋ฒ• ์•ˆ๋‚ด", open=False):
324
+ gr.Markdown("""
325
+ ### ๐ŸŽฏ ๋‹ค์–‘์„ฑ ๊ฐ•ํ™” ์‡ผํ•‘ํ‚ค์›Œ๋“œ ์‹œ์Šคํ…œ ์‚ฌ์šฉ๋ฒ•
326
+
327
+ #### ๐Ÿš€ ์ฃผ์š” ๊ฐœ์„  ์‚ฌํ•ญ
328
+ - **์™„์ „ํ•œ ์ค‘๋ณต ๋ฐฉ์ง€**: ๋งค๋ฒˆ ์‹คํ–‰ํ•  ๋•Œ๋งˆ๋‹ค ์™„์ „ํžˆ ๋‹ค๋ฅธ ํ‚ค์›Œ๋“œ ์ƒ์„ฑ
329
+ - **๋žœ๋ค ์‹œ๋“œ ์‹œ์Šคํ…œ**: ํ˜„์žฌ ์‹œ๊ฐ์„ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•œ ๋žœ๋ค ์‹œ๋“œ๋กœ ์˜ˆ์ธก ๋ถˆ๊ฐ€๋Šฅ
330
+ - **๋‹ค์–‘ํ•œ ์กฐํ•ฉ ๋ณด์žฅ**: ์†Œ์žฌร—ํ˜•ํƒœร—๊ธฐ๋Šฅ์˜ 3์ฐจ์› ์กฐํ•ฉ์œผ๋กœ ๋ฌดํ•œ ๋‹ค์–‘์„ฑ
331
+ - **์ค‘๋ณต ๊ฒ€์‚ฌ ๊ฐ•ํ™”**: ๋Œ€์†Œ๋ฌธ์ž ๊ตฌ๋ถ„ ์—†๋Š” ์—„๊ฒฉํ•œ ์ค‘๋ณต ์ œ๊ฑฐ
332
+ - **์˜จ๋„ ์กฐ์ ˆ**: AI ์ƒ์„ฑ ํŒŒ๋ผ๋ฏธํ„ฐ ์ตœ์ ํ™”๋กœ ์ฐฝ์˜์„ฑ ๊ทน๋Œ€ํ™”
333
+
334
+ #### ๐Ÿ”„ ๋‹ค์–‘์„ฑ ๋ณด์žฅ ๋ฉ”์ปค๋‹ˆ์ฆ˜
335
+ 1. **์‹œ๋“œ ๊ธฐ๋ฐ˜ ๋žœ๋คํ™”**: ๋งˆ์ดํฌ๋กœ์ดˆ ๋‹จ์œ„ ์‹œ๊ฐ„ ๊ธฐ๋ฐ˜ ๋žœ๋ค ์‹œ๋“œ
336
+ 2. **3์ฐจ์› ์กฐํ•ฉ ์‹œ์Šคํ…œ**:
337
+ - ์†Œ์žฌ: ์‹ค๋ฆฌ์ฝ˜, ์Šคํ…Œ์ธ๋ฆฌ์Šค, ์„ธ๋ผ๋ฏน, ๋Œ€๋‚˜๋ฌด ๋“ฑ 20์ข…
338
+ - ํ˜•ํƒœ: ์ ‘์ด์‹, ์›ํ˜•, ์Šฌ๋ฆผ, ํœด๋Œ€์šฉ ๋“ฑ 20์ข…
339
+ - ๊ธฐ๋Šฅ: ๋ฐฉ์ˆ˜, ํ•ญ๊ท , ๋งˆ๊ทธ๋„คํ‹ฑ, ๋ณด์˜จ ๋“ฑ 20์ข…
340
+ 3. **์ค‘๋ณต ๋ฐฉ์ง€ ์•Œ๊ณ ๋ฆฌ์ฆ˜**: ์ƒ์„ฑ ์ค‘ ์‹ค์‹œ๊ฐ„ ์ค‘๋ณต ๊ฒ€์‚ฌ
341
+ 4. **์ถ”๊ฐ€ ์ƒ์„ฑ ์‹œ์Šคํ…œ**: ๋ถ€์กฑ์‹œ ์ž๋™์œผ๋กœ ์ถ”๊ฐ€ ํ‚ค์›Œ๋“œ ์ƒ์„ฑ
342
+
343
+ #### ๐ŸŽฒ ๋žœ๋ค ์ ์šฉ์˜ ์ง„ํ™”
344
+ - **ํ‚ค์›Œ๋“œ๋ณ„ ๋…๋ฆฝ ์ ์šฉ**: ๊ฐ ํ‚ค์›Œ๋“œ๋งˆ๋‹ค ๋‹ค๋ฅธ ์กฐ๊ฑด ์กฐํ•ฉ
345
+ - **์˜ˆ์ธก ๋ถˆ๊ฐ€๋Šฅ์„ฑ**: ๊ฐ™์€ ์„ค์ •์ด๋ผ๋„ ๋งค๋ฒˆ ๋‹ค๋ฅธ ๊ฒฐ๊ณผ
346
+ - **์กฐํ•ฉ ํญ๋ฐœ**: ์ˆ˜์ฒœ ๊ฐ€์ง€ ๊ฐ€๋Šฅํ•œ ์กฐํ•ฉ์œผ๋กœ ๋ฌดํ•œ ๋‹ค์–‘์„ฑ
347
+
348
+ #### ๐Ÿ“ˆ ์ƒ์„ฑ ํ’ˆimport gradio as gr
349
+ import os
350
+ import logging
351
+ import sys
352
+ import random
353
+ import requests
354
+ import json
355
+ from datetime import datetime
356
+ from google import genai
357
+ from google.genai.types import Tool, GenerateContentConfig, GoogleSearch
358
+
359
+ # ๋กœ๊น… ์„ค์ •
360
+ logging.basicConfig(
361
+ level=logging.INFO,
362
+ format='%(asctime)s - %(levelname)s - %(message)s',
363
+ handlers=[
364
+ logging.StreamHandler(sys.stdout),
365
+ logging.FileHandler('sourcing_app.log', encoding='utf-8')
366
+ ]
367
+ )
368
+ logger = logging.getLogger(__name__)
369
+
370
+ # ํ‚ค์›Œ๋“œ ๋‹ค์–‘์„ฑ์„ ์œ„ํ•œ ์‹œ๋“œ ํ’€ ํ™•์žฅ
371
+ DIVERSE_SEED_POOLS = {
372
+ "ํŒจ์…˜์žกํ™”": [
373
+ "์•ก์„ธ์„œ๋ฆฌ", "์žฅ์‹ ๊ตฌ", "๊ฐ€๋ฐฉ", "์ง€๊ฐ‘", "๋ชจ์ž", "์Šค์นดํ”„", "๋ฒจํŠธ", "์„ ๊ธ€๋ผ์Šค", "ํ—ค์–ด์•ก์„ธ์„œ๋ฆฌ", "์‹œ๊ณ„์ค„",
374
+ "ํ‚ค๋ง", "๋ธŒ๋กœ์น˜", "๋ชฉ๊ฑธ์ด", "ํŒ”์ฐŒ", "๋ฐ˜์ง€", "๊ท€๊ฑธ์ด", "ํ•ธ๋“œํฐ์ผ€์ด์Šค", "ํŒŒ์šฐ์น˜", "ํด๋Ÿฌ์น˜", "ํ† ํŠธ๋ฐฑ"
375
+ ],
376
+ "์ƒํ™œ/๊ฑด๊ฐ•": [
377
+ "์ฃผ๋ฐฉ์šฉํ’ˆ", "์š•์‹ค์šฉํ’ˆ", "์ฒญ์†Œ์šฉํ’ˆ", "์ˆ˜๋‚ฉ์šฉํ’ˆ", "๊ฑด๊ฐ•์šฉํ’ˆ", "์˜๋ฃŒ์šฉํ’ˆ", "๋งˆ์‚ฌ์ง€์šฉํ’ˆ", "์šด๋™์šฉํ’ˆ",
378
+ "๋‹ค์ด์–ดํŠธ์šฉํ’ˆ", "ํ™”์žฅ์ง€", "์„ธ์ œ", "์ƒดํ‘ธ", "์น˜์•ฝ", "๋น„๋ˆ„", "์ˆ˜๊ฑด", "๋ฒ ๊ฐœ", "์ด๋ถˆ", "์ฟ ์…˜", "๋งคํŠธ"
379
+ ],
380
+ "์ถœ์‚ฐ/์œก์•„": [
381
+ "์œ ์•„์šฉํ’ˆ", "์œก์•„์šฉํ’ˆ", "์ถœ์‚ฐ์šฉํ’ˆ", "์ž„์‚ฐ๋ถ€์šฉํ’ˆ", "์‹ ์ƒ์•„์šฉํ’ˆ", "์ด์œ ์‹์šฉํ’ˆ", "๊ธฐ์ €๊ท€", "์ –๋ณ‘",
382
+ "์œ ๋ชจ์ฐจ", "์นด์‹œํŠธ", "์•„๊ธฐ์˜ท", "์žฅ๋‚œ๊ฐ", "๊ต์œก์šฉํ’ˆ", "์ฑ…", "๊ทธ๋ฆผ์ฑ…", "ํผ์ฆ", "๋ธ”๋ก", "์ธํ˜•", "๋†€์ด๋งคํŠธ"
383
+ ],
384
+ "์Šคํฌ์ธ /๋ ˆ์ €": [
385
+ "์šด๋™์šฉํ’ˆ", "ํ—ฌ์Šค์šฉํ’ˆ", "์š”๊ฐ€์šฉํ’ˆ", "์ˆ˜์˜์šฉํ’ˆ", "๋“ฑ์‚ฐ์šฉํ’ˆ", "์บ ํ•‘์šฉํ’ˆ", "๋‚š์‹œ์šฉํ’ˆ", "๊ณจํ”„์šฉํ’ˆ",
386
+ "์ถ•๊ตฌ์šฉํ’ˆ", "๋†๊ตฌ์šฉํ’ˆ", "๋ฐฐ๋“œ๋ฏผํ„ด์šฉํ’ˆ", "ํƒ๊ตฌ์šฉํ’ˆ", "ํ…Œ๋‹ˆ์Šค์šฉํ’ˆ", "์ž์ „๊ฑฐ์šฉํ’ˆ", "์Šค์ผ€์ดํŠธ๋ณด๋“œ์šฉํ’ˆ"
387
+ ],
388
+ "๋””์ง€ํ„ธ/๊ฐ€์ „": [
389
+ "์Šค๋งˆํŠธํฐ์•ก์„ธ์„œ๋ฆฌ", "์ปดํ“จํ„ฐ์šฉํ’ˆ", "ํƒœ๋ธ”๋ฆฟ์šฉํ’ˆ", "์ด์–ดํฐ", "์Šคํ”ผ์ปค", "์ถฉ์ „๊ธฐ", "์ผ€์ด๋ธ”", "๋งˆ์šฐ์ŠคํŒจ๋“œ",
390
+ "ํ‚ค๋ณด๋“œ", "๋งˆ์šฐ์Šค", "์›น์บ ", "๋งˆ์ดํฌ", "ํ—ค๋“œ์…‹", "๊ฒŒ์ž„ํŒจ๋“œ", "USB", "๋ฉ”๋ชจ๋ฆฌ์นด๋“œ", "ํŒŒ์›Œ๋ฑ…ํฌ"
391
+ ],
392
+ "๊ฐ€๊ตฌ/์ธํ…Œ๋ฆฌ์–ด": [
393
+ "์ˆ˜๋‚ฉ๊ฐ€๊ตฌ", "์นจ์‹ค๊ฐ€๊ตฌ", "๊ฑฐ์‹ค๊ฐ€๊ตฌ", "์ฃผ๋ฐฉ๊ฐ€๊ตฌ", "์š•์‹ค๊ฐ€๊ตฌ", "์‚ฌ๋ฌด์šฉ๊ฐ€๊ตฌ", "์ธํ…Œ๋ฆฌ์–ด์†Œํ’ˆ", "์กฐ๋ช…",
394
+ "์ปคํŠผ", "๋ธ”๋ผ์ธ๋“œ", "์นดํŽซ", "๋Ÿฌ๊ทธ", "์•ก์ž", "๊ฑฐ์šธ", "์‹œ๊ณ„", "ํ™”๋ถ„", "๊ฝƒ๋ณ‘", "์บ”๋“ค", "๋ฐฉํ–ฅ์ œ"
395
+ ],
396
+ "ํŒจ์…˜์˜๋ฅ˜": [
397
+ "ํ‹ฐ์…”์ธ ", "์…”์ธ ", "๋ธ”๋ผ์šฐ์Šค", "์›ํ”ผ์Šค", "์Šค์ปคํŠธ", "๋ฐ”์ง€", "์ฒญ๋ฐ”์ง€", "๋ ˆ๊น…์Šค", "์ž์ผ“", "์ฝ”ํŠธ",
398
+ "์ ํผ", "๊ฐ€๋””๊ฑด", "๋‹ˆํŠธ", "ํ›„๋“œ", "์กฐ๋ผ", "์†์˜ท", "์ž ์˜ท", "์–‘๋ง", "์Šคํƒ€ํ‚น", "์šด๋™๋ณต"
399
+ ],
400
+ "ํ™”์žฅํ’ˆ/๋ฏธ์šฉ": [
401
+ "์Šคํ‚จ์ผ€์–ด", "๋ฉ”์ดํฌ์—…", "ํด๋ Œ์ง•", "๋งˆ์ŠคํฌํŒฉ", "์„ ํฌ๋ฆผ", "๋กœ์…˜", "์—์„ผ์Šค", "ํฌ๋ฆผ", "๋ฆฝ๋ฐค",
402
+ "๋ฆฝ์Šคํ‹ฑ", "์•„์ด์„€๋„", "๋งˆ์Šค์นด๋ผ", "ํŒŒ์šด๋ฐ์ด์…˜", "์ปจ์‹ค๋Ÿฌ", "๋ธ”๋Ÿฌ์…”", "ํ•˜์ด๋ผ์ดํ„ฐ", "๋„ค์ผ", "ํ–ฅ์ˆ˜"
403
+ ]
404
+ }
405
+
406
+ # ์†Œ์žฌ๋ณ„ ํ‚ค์›Œ๋“œ ํ’€
407
+ MATERIAL_KEYWORDS = [
408
+ "์‹ค๋ฆฌ์ฝ˜", "์Šคํ…Œ์ธ๋ฆฌ์Šค", "์„ธ๋ผ๋ฏน", "์œ ๋ฆฌ", "๋‚˜๋ฌด", "๋Œ€๋‚˜๋ฌด", "๋ฉด", "๋ฆฐ๋„จ", "ํด๋ฆฌ์—์Šคํ„ฐ", "๋‚˜์ผ๋ก ",
409
+ "๊ณ ๋ฌด", "ํ”Œ๋ผ์Šคํ‹ฑ", "์ข…์ด", "๊ฐ€์ฃฝ", "์ธ์กฐ๊ฐ€์ฃฝ", "๋ฉ”ํƒˆ", "์•Œ๋ฃจ๋ฏธ๋Š„", "์ฒ ", "๊ตฌ๋ฆฌ", "ํ™ฉ๋™"
410
+ ]
411
+
412
+ # ํ˜•ํƒœ๋ณ„ ํ‚ค์›Œ๋“œ ํ’€
413
+ SHAPE_KEYWORDS = [
414
+ "์ ‘์ด์‹", "ํœด๋Œ€์šฉ", "๋ฏธ๋‹ˆ", "๋Œ€ํ˜•", "์›ํ˜•", "์‚ฌ๊ฐ", "ํƒ€์›", "์ง์‚ฌ๊ฐ", "์‚ผ๊ฐ", "์œก๊ฐ",
415
+ "์Šฌ๋ฆผ", "๋‘๊บผ์šด", "์–‡์€", "๊ธด", "์งง์€", "๋„“์€", "์ข์€", "๊นŠ์€", "์–•์€", "๊ณก์„ "
416
+ ]
417
+
418
+ # ๊ธฐ๋Šฅ๋ณ„ ํ‚ค์›Œ๋“œ ํ’€
419
+ FUNCTION_KEYWORDS = [
420
+ "๋ฐฉ์ˆ˜", "๋ฏธ๋„๋Ÿผ๋ฐฉ์ง€", "ํ•ญ๊ท ", "๋ƒ„์ƒˆ์ œ๊ฑฐ", "๋ณด์˜จ", "๋ณด๋ƒ‰", "์†๊ฑด", "ํก์ˆ˜", "์ฐจ๋‹จ", "๋ณดํ˜ธ",
421
+ "๋งˆ๊ทธ๋„คํ‹ฑ", "์ž์„", "๋ˆ์ ", "ํˆฌ๋ช…", "๋ถˆํˆฌ๋ช…", "๋ฐœ๊ด‘", "๋ฐ˜์‚ฌ", "์‹ ์ถ•", "ํƒ„๋ ฅ", "๊ณ ์ •"
422
+ ]
423
+
424
+ # Gemini API ํด๋ผ์ด์–ธํŠธ ์ดˆ๊ธฐํ™”
425
+ def initialize_gemini():
426
+ logger.info("Gemini API ํด๋ผ์ด์–ธํŠธ ์ดˆ๊ธฐํ™” ์‹œ์ž‘")
427
+ api_key = os.getenv("GEMINI_API_KEY")
428
+ if not api_key:
429
+ logger.error("GEMINI_API_KEY ํ™˜๊ฒฝ๋ณ€์ˆ˜๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.")
430
+ raise ValueError("GEMINI_API_KEY ํ™˜๊ฒฝ๋ณ€์ˆ˜๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.")
431
+
432
+ client = genai.Client(api_key=api_key)
433
+ logger.info("Gemini API ํด๋ผ์ด์–ธํŠธ ์ดˆ๊ธฐํ™” ์™„๋ฃŒ")
434
+ return client
435
+
436
+ def get_recent_logs():
437
+ """์ตœ๊ทผ ๋กœ๊ทธ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ํ•จ์ˆ˜"""
438
+ try:
439
+ with open('sourcing_app.log', 'r', encoding='utf-8') as f:
440
+ lines = f.readlines()
441
+ # ์ตœ๊ทผ 50์ค„๋งŒ ๋ฐ˜ํ™˜
442
+ return ''.join(lines[-50:])
443
+ except FileNotFoundError:
444
+ return "๋กœ๊ทธ ํŒŒ์ผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."
445
+ except Exception as e:
446
+ return f"๋กœ๊ทธ ์ฝ๊ธฐ ์˜ค๋ฅ˜: {str(e)}"
447
+
448
+ def generate_diverse_keyword_combinations(category, count=60):
449
+ """๋‹ค์–‘ํ•œ ํ‚ค์›Œ๋“œ ์กฐํ•ฉ์„ ์ƒ์„ฑํ•˜๋Š” ํ•จ์ˆ˜"""
450
+ logger.info(f"๋‹ค์–‘ํ•œ ํ‚ค์›Œ๋“œ ์กฐํ•ฉ ์ƒ์„ฑ ์‹œ์ž‘: {category}, {count}๊ฐœ")
451
+
452
+ combinations = []
453
+ category_pool = DIVERSE_SEED_POOLS.get(category, DIVERSE_SEED_POOLS["์ƒํ™œ/๊ฑด๊ฐ•"])
454
+
455
+ # 1. ๋‹จ์ผ ํ‚ค์›Œ๋“œ (20%)
456
+ single_keywords = random.sample(category_pool, min(12, len(category_pool)))
457
+ combinations.extend(single_keywords)
458
+
459
+ # 2. ์†Œ์žฌ + ์นดํ…Œ๊ณ ๋ฆฌ (30%)
460
+ for _ in range(18):
461
+ material = random.choice(MATERIAL_KEYWORDS)
462
+ item = random.choice(category_pool)
463
+ combinations.append(f"{material} {item}")
464
+
465
+ # 3. ํ˜•ํƒœ + ์นดํ…Œ๊ณ ๋ฆฌ (30%)
466
+ for _ in range(18):
467
+ shape = random.choice(SHAPE_KEYWORDS)
468
+ item = random.choice(category_pool)
469
+ combinations.append(f"{shape} {item}")
470
+
471
+ # 4. ๊ธฐ๋Šฅ + ์นดํ…Œ๊ณ ๋ฆฌ (20%)
472
+ for _ in range(12):
473
+ function = random.choice(FUNCTION_KEYWORDS)
474
+ item = random.choice(category_pool)
475
+ combinations.append(f"{function} {item}")
476
+
477
+ # ์ค‘๋ณต ์ œ๊ฑฐ ๋ฐ ์…”ํ”Œ
478
+ combinations = list(set(combinations))
479
+ random.shuffle(combinations)
480
+
481
+ logger.info(f"์ƒ์„ฑ๋œ ์กฐํ•ฉ ์ˆ˜: {len(combinations)}๊ฐœ")
482
+ return combinations[:count]
483
+
484
+ def search_all_engines(query):
485
+ """๋ชจ๋“  ๊ฒ€์ƒ‰ ์—”์ง„์„ ์‚ฌ์šฉํ•˜์—ฌ ๋ฐ์ดํ„ฐ๋ฅผ ์ทจํ•ฉํ•˜๋Š” ํ•จ์ˆ˜"""
486
+ logger.info(f"๋ชจ๋“  ๊ฒ€์ƒ‰ ์—”์ง„์œผ๋กœ ๊ฒ€์ƒ‰ ์‹œ์ž‘: {query}")
487
+
488
+ all_results = {
489
+ "google": "",
490
+ "naver": "",
491
+ "duckduckgo": ""
492
+ }
493
+
494
+ # 1. ๋„ค์ด๋ฒ„ ๊ฒ€์ƒ‰ API
495
+ try:
496
+ naver_client_id = os.getenv("NAVER_CLIENT_ID")
497
+ naver_client_secret = os.getenv("NAVER_CLIENT_SECRET")
498
+
499
+ if naver_client_id and naver_client_secret:
500
+ url = "https://openapi.naver.com/v1/search/shop.json"
501
+ headers = {
502
+ "X-Naver-Client-Id": naver_client_id,
503
+ "X-Naver-Client-Secret": naver_client_secret
504
+ }
505
+ params = {"query": query, "display": 10}
506
+
507
+ response = requests.get(url, headers=headers, params=params, timeout=10)
508
+ if response.status_code == 200:
509
+ data = response.json()
510
+ naver_data = []
511
+ for item in data.get('items', [])[:5]:
512
+ naver_data.append(f"์ƒํ’ˆ: {item.get('title', '').replace('<b>', '').replace('</b>', '')}")
513
+ naver_data.append(f"๊ฐ€๊ฒฉ: {item.get('lprice', '')}์›")
514
+ naver_data.append(f"์นดํ…Œ๊ณ ๋ฆฌ: {item.get('category1', '')}")
515
+ all_results["naver"] = "\n".join(naver_data)
516
+ logger.info("๋„ค์ด๋ฒ„ ๊ฒ€์ƒ‰ ์™„๋ฃŒ")
517
+ else:
518
+ all_results["naver"] = "๋„ค์ด๋ฒ„ API ๊ฒ€์ƒ‰ ์‹คํŒจ"
519
+ else:
520
+ all_results["naver"] = "๋„ค์ด๋ฒ„ API ํ‚ค๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์Œ"
521
+ except Exception as e:
522
+ all_results["naver"] = f"๋„ค์ด๋ฒ„ ๊ฒ€์ƒ‰ ์˜ค๋ฅ˜: {str(e)}"
523
+
524
+ # 2. DuckDuckGo ๊ฒ€์ƒ‰
525
+ try:
526
+ url = "https://api.duckduckgo.com/"
527
+ params = {
528
+ "q": query,
529
+ "format": "json",
530
+ "no_html": "1",
531
+ "skip_disambig": "1"
532
+ }
533
+
534
+ response = requests.get(url, params=params, timeout=10)
535
+ if response.status_code == 200:
536
+ data = response.json()
537
+
538
+ ddg_data = []
539
+ # Abstract ์ •๋ณด
540
+ if data.get('Abstract'):
541
+ ddg_data.append(f"์š”์•ฝ: {data['Abstract']}")
542
+
543
+ # Related Topics
544
+ for topic in data.get('RelatedTopics', [])[:3]:
545
+ if isinstance(topic, dict) and topic.get('Text'):
546
+ ddg_data.append(f"๊ด€๋ จ์ •๋ณด: {topic['Text']}")
547
+
548
+ all_results["duckduckgo"] = "\n".join(ddg_data) if ddg_data else "DuckDuckGo์—์„œ ๊ด€๋ จ ์ •๋ณด ์—†์Œ"
549
+ logger.info("DuckDuckGo ๊ฒ€์ƒ‰ ์™„๋ฃŒ")
550
+ else:
551
+ all_results["duckduckgo"] = "DuckDuckGo ๊ฒ€์ƒ‰ ์‹คํŒจ"
552
+ except Exception as e:
553
+ all_results["duckduckgo"] = f"DuckDuckGo ๊ฒ€์ƒ‰ ์˜ค๋ฅ˜: {str(e)}"
554
+
555
+ # 3. Google ๊ฒ€์ƒ‰์€ Gemini์—์„œ ์ž๋™ ์ฒ˜๋ฆฌ๋จ
556
+ all_results["google"] = "Google ๊ฒ€์ƒ‰ ๊ทธ๋ผ์šด๋”ฉ ์ž๋™ ์‹คํ–‰"
557
+
558
+ logger.info("๋ชจ๋“  ๊ฒ€์ƒ‰ ์—”์ง„ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ์™„๋ฃŒ")
559
+ return all_results
560
+
561
+ def comprehensive_market_analysis(category, seasonality, sales_target):
562
+ """๋‹ค์–‘์„ฑ ๊ฐ•ํ™”๋œ ์‹œ์žฅ ๋ถ„์„"""
563
+ logger.info("๋‹ค์–‘์„ฑ ๊ฐ•ํ™” ์‹œ์žฅ ๋ถ„์„ ์‹œ์ž‘")
564
+
565
+ # ๋žœ๋ค ์‹œ๋“œ๋ฅผ ํ˜„์žฌ ์‹œ๊ฐ„์œผ๋กœ ์„ค์ •ํ•˜์—ฌ ๋งค๋ฒˆ ๋‹ค๋ฅธ ๊ฒฐ๊ณผ ๋ณด์žฅ
566
+ random.seed(datetime.now().microsecond)
567
+
568
+ # ๋‹ค์–‘ํ•œ ๊ฒ€์ƒ‰ ๊ฐ๋„๋กœ ์ฟผ๋ฆฌ ์ƒ์„ฑ
569
+ search_angles = [
570
+ "ํ‹ˆ์ƒˆ์ƒํ’ˆ", "์‹ ์ƒํ’ˆ", "์ธ๊ธฐ์ƒํ’ˆ", "์ €๊ฐ€์ƒํ’ˆ", "๊ณ ๊ธ‰์ƒํ’ˆ", "ํ• ์ธ์ƒํ’ˆ",
571
+ "๊ฐ„ํŽธ์ƒํ’ˆ", "์‹ค์šฉ์ƒํ’ˆ", "ํŠธ๋ Œ๋“œ์ƒํ’ˆ", "์ˆจ์€์ƒํ’ˆ", "๋ฒ ์ŠคํŠธ์ƒํ’ˆ", "์ถ”์ฒœ์ƒํ’ˆ"
572
+ ]
573
+
574
+ search_queries = []
575
+
576
+ # ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ๋‹ค์–‘ํ•œ ๊ฒ€์ƒ‰์–ด ์ƒ์„ฑ
577
+ category_pool = DIVERSE_SEED_POOLS.get(category, DIVERSE_SEED_POOLS["์ƒํ™œ/๊ฑด๊ฐ•"])
578
+
579
+ for angle in search_angles:
580
+ for item in random.sample(category_pool, 3): # ๊ฐ ์นดํ…Œ๊ณ ๋ฆฌ์—์„œ 3๊ฐœ์”ฉ๋งŒ ์„ ํƒ
581
+ search_queries.append(f"{item} {angle}")
582
+
583
+ # ์†Œ์žฌ๋ณ„ ๊ฒ€์ƒ‰์–ด ์ถ”๊ฐ€
584
+ for material in random.sample(MATERIAL_KEYWORDS, 5):
585
+ search_queries.append(f"{material} {category} ์ƒํ’ˆ")
586
+
587
+ # ํ˜•ํƒœ๋ณ„ ๊ฒ€์ƒ‰์–ด ์ถ”๊ฐ€
588
+ for shape in random.sample(SHAPE_KEYWORDS, 5):
589
+ search_queries.append(f"{shape} {category} ์•„์ดํ…œ")
590
+
591
+ # ๊ฒ€์ƒ‰์–ด ์…”ํ”Œํ•˜์—ฌ ์˜ˆ์ธก ๋ถˆ๊ฐ€๋Šฅํ•˜๊ฒŒ ๋งŒ๋“ค๊ธฐ
592
+ random.shuffle(search_queries)
593
+ search_queries = search_queries[:15] # 15๊ฐœ๋กœ ์ œํ•œ
594
+
595
+ comprehensive_data = {}
596
+
597
+ for i, query in enumerate(search_queries):
598
+ logger.info(f"๋‹ค์–‘์„ฑ ๊ฒ€์ƒ‰ {i+1}/15: {query}")
599
+ comprehensive_data[f"query_{i+1}"] = search_all_engines(query)
600
+
601
+ # API ๊ณผ๋ถ€ํ•˜ ๋ฐฉ์ง€๋ฅผ ์œ„ํ•œ ๋”œ๋ ˆ์ด
602
+ import time
603
+ time.sleep(0.5)
604
+
605
+ # ๋‹ค์–‘์„ฑ ๊ฐ•ํ™” ๋ฐ์ดํ„ฐ ์š”์•ฝ
606
+ summary = "=== ๋‹ค์–‘์„ฑ ๊ฐ•ํ™” ์‹œ์žฅ ๋ถ„์„ ๊ฒฐ๊ณผ ===\n\n"
607
+
608
+ # ๋ฌด์ž‘์œ„๋กœ ๊ฒฐ๊ณผ๋ฅผ ์„ž์–ด์„œ ํŒจํ„ด ๋ฐฉ์ง€
609
+ result_keys = list(comprehensive_data.keys())
610
+ random.shuffle(result_keys)
611
+
612
+ summary += "๐Ÿ” ๋‹ค์–‘ํ•œ ์‹œ์žฅ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ:\n"
613
+ for key in result_keys[:10]:
614
+ results = comprehensive_data.get(key, {})
615
+ if results.get("naver"):
616
+ summary += f"โ€ข {results['naver'][:60]}...\n"
617
+ summary += "\n"
618
+
619
+ logger.info("๋‹ค์–‘์„ฑ ๊ฐ•ํ™” ๋ถ„์„ ์™„๋ฃŒ")
620
+ return summary
621
+
622
+ def search_with_api(query, search_engine="Google ๊ฒ€์ƒ‰ ๊ทธ๋ผ์šด๋”ฉ๋งŒ"):
623
+ """๊ฐœ๋ณ„ ๊ฒ€์ƒ‰ ์—”์ง„์œผ๋กœ ๊ฒ€์ƒ‰ํ•˜๋Š” ํ•จ์ˆ˜ (๋‹จ์ผ ์—”์ง„ ์„ ํƒ์‹œ ์‚ฌ์šฉ)"""
624
+ logger.info(f"๊ฒ€์ƒ‰ ์—”์ง„: {search_engine}, ์ฟผ๋ฆฌ: {query}")
625
+
626
+ search_results = ""
627
+
628
+ try:
629
+ if search_engine == "๋„ค์ด๋ฒ„ ๊ฒ€์ƒ‰ API๋งŒ":
630
+ # ๋„ค์ด๋ฒ„ ๊ฒ€์ƒ‰ API ์‚ฌ์šฉ
631
+ naver_client_id = os.getenv("NAVER_CLIENT_ID")
632
+ naver_client_secret = os.getenv("NAVER_CLIENT_SECRET")
633
+
634
+ if naver_client_id and naver_client_secret:
635
+ url = "https://openapi.naver.com/v1/search/shop.json"
636
+ headers = {
637
+ "X-Naver-Client-Id": naver_client_id,
638
+ "X-Naver-Client-Secret": naver_client_secret
639
+ }
640
+ params = {"query": query, "display": 10}
641
+
642
+ response = requests.get(url, headers=headers, params=params)
643
+ if response.status_code == 200:
644
+ data = response.json()
645
+ for item in data.get('items', [])[:5]:
646
+ search_results += f"์ƒํ’ˆ๋ช…: {item.get('title', '')}\n"
647
+ search_results += f"๊ฐ€๊ฒฉ: {item.get('lprice', '')}์›\n"
648
+ search_results += f"์นดํ…Œ๊ณ ๋ฆฌ: {item.get('category1', '')}\n\n"
649
+ else:
650
+ search_results = "๋„ค์ด๋ฒ„ API ๊ฒ€์ƒ‰ ์‹คํŒจ"
651
+ else:
652
+ search_results = "๋„ค์ด๋ฒ„ API ํ‚ค๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์Œ"
653
+
654
+ elif search_engine == "DuckDuckGo ๊ฒ€์ƒ‰๋งŒ":
655
+ # DuckDuckGo ๊ฒ€์ƒ‰ (๋ฌด๋ฃŒ, API ํ‚ค ๋ถˆํ•„์š”)
656
+ try:
657
+ url = "https://api.duckduckgo.com/"
658
+ params = {
659
+ "q": query,
660
+ "format": "json",
661
+ "no_html": "1",
662
+ "skip_disambig": "1"
663
+ }
664
+
665
+ response = requests.get(url, params=params, timeout=10)
666
+ if response.status_code == 200:
667
+ data = response.json()
668
+
669
+ # Abstract ์ •๋ณด
670
+ if data.get('Abstract'):
671
+ search_results += f"์š”์•ฝ: {data['Abstract']}\n\n"
672
+
673
+ # Related Topics
674
+ for topic in data.get('RelatedTopics', [])[:5]:
675
+ if isinstance(topic, dict) and topic.get('Text'):
676
+ search_results += f"๊ด€๋ จ ์ •๋ณด: {topic['Text']}\n"
677
+
678
+ if not search_results:
679
+ search_results = "DuckDuckGo์—์„œ ๊ด€๋ จ ์ •๋ณด๋ฅผ ์ฐพ์ง€ ๋ชปํ•จ"
680
+ else:
681
+ search_results = "DuckDuckGo ๊ฒ€์ƒ‰ ์‹คํŒจ"
682
+ except Exception as e:
683
+ search_results = f"DuckDuckGo ๊ฒ€์ƒ‰ ์˜ค๋ฅ˜: {str(e)}"
684
+
685
+ elif search_engine == "๊ฒ€์ƒ‰ ์—†์ด AI๋งŒ ์‚ฌ์šฉ":
686
+ search_results = "๊ฒ€์ƒ‰ ์—†์ด AI ์ง€์‹๋งŒ ์‚ฌ์šฉํ•˜์—ฌ ํ‚ค์›Œ๋“œ ์ƒ์„ฑ"
687
+
688
+ else:
689
+ # Google ๊ฒ€์ƒ‰ ๊ทธ๋ผ์šด๋”ฉ (๊ธฐ๋ณธ)
690
+ search_results = "Google ๊ฒ€์ƒ‰ ๊ทธ๋ผ์šด๋”ฉ ์‚ฌ์šฉ"
691
+
692
+ except Exception as e:
693
+ logger.error(f"๊ฒ€์ƒ‰ ์˜ค๋ฅ˜: {str(e)}")
694
+ search_results = f"๊ฒ€์ƒ‰ ์˜ค๋ฅ˜: {str(e)}"
695
+
696
+ logger.info(f"๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ๊ธธ์ด: {len(search_results)} ๋ฌธ์ž")
697
+ return search_results
698
+
699
+ def apply_random_selection_for_keywords(category, launch_timing, seasonality, sales_target, sales_channel, competition_level):
700
+ """๊ฐ ํ‚ค์›Œ๋“œ๋งˆ๋‹ค ๋žœ๋คํ•˜๊ฒŒ ์กฐ๊ฑด์„ ์ ์šฉํ•˜๊ธฐ ์œ„ํ•œ ์„ค์ • ๋ฌธ์ž์—ด ์ƒ์„ฑ"""
701
+
702
+ # ๊ฐ ํ•ญ๋ชฉ๋ณ„ ์„ ํƒ์ง€ ์ •์˜
703
+ categories = ["ํŒจ์…˜์žกํ™”", "์ƒํ™œ/๊ฑด๊ฐ•", "์ถœ์‚ฐ/์œก์•„", "์Šคํฌ์ธ /๋ ˆ์ €", "๋””์ง€ํ„ธ/๊ฐ€์ „", "๊ฐ€๊ตฌ/์ธํ…Œ๋ฆฌ์–ด", "ํŒจ์…˜์˜๋ฅ˜", "ํ™”์žฅํ’ˆ/๋ฏธ์šฉ"]
704
+ launch_timings = ["์ฆ‰์‹œ์†Œ์‹ฑ", "๊ธฐํšํ˜•"]
705
+ seasonalities = ["๋ด„", "์—ฌ๋ฆ„", "๊ฐ€์„", "๊ฒจ์šธ", "๋น„๊ณ„์ ˆ"]
706
+ sales_targets = ["100๋งŒ์› ์ดํ•˜", "100-500๋งŒ์›", "500-1์ฒœ๋งŒ์›", "1์ฒœ-5์ฒœ๋งŒ์›", "5์ฒœ๋งŒ์› ์ด์ƒ"]
707
+ sales_channels = ["์˜คํ”ˆ๋งˆ์ผ“", "SNS๋งˆ์ผ€ํŒ…", "๊ด‘๊ณ ์ง‘ํ–‰", "์˜คํ”„๋ผ์ธ"]
708
+ competition_levels = ["์ดˆ๋ณด", "์ค‘์ˆ˜", "๊ณ ์ˆ˜"]
709
+
710
+ # ๋žœ๋ค์ ์šฉ ์„ค์ • ์ •๋ณด ์ƒ์„ฑ
711
+ random_settings = {
712
+ 'category_random': category == "๋žœ๋ค์ ์šฉ",
713
+ 'launch_timing_random': launch_timing == "๋žœ๋ค์ ์šฉ",
714
+ 'seasonality_random': seasonality == "๋žœ๋ค์ ์šฉ",
715
+ 'sales_target_random': sales_target == "๋žœ๋ค์ ์šฉ",
716
+ 'sales_channel_random': sales_channel == "๋žœ๋ค์ ์šฉ",
717
+ 'competition_level_random': competition_level == "๋žœ๋ค์ ์šฉ",
718
+ 'categories': categories,
719
+ 'launch_timings': launch_timings,
720
+ 'seasonalities': seasonalities,
721
+ 'sales_targets': sales_targets,
722
+ 'sales_channels': sales_channels,
723
+ 'competition_levels': competition_levels
724
+ }
725
+
726
+ # ๊ณ ์ •๊ฐ’๋“ค
727
+ fixed_values = {
728
+ 'category': category if category != "๋žœ๋ค์ ์šฉ" else None,
729
+ 'launch_timing': launch_timing if launch_timing != "๋žœ๋ค์ ์šฉ" else None,
730
+ 'seasonality': seasonality if seasonality != "๋žœ๋ค์ ์šฉ" else None,
731
+ 'sales_target': sales_target if sales_target != "๋žœ๋ค์ ์šฉ" else None,
732
+ 'sales_channel': sales_channel if sales_channel != "๋žœ๋ค์ ์šฉ" else None,
733
+ 'competition_level': competition_level if competition_level != "๋žœ๋ค์ ์šฉ" else None
734
+ }
735
+
736
+ logger.info("=== ํ‚ค์›Œ๋“œ๋ณ„ ๋žœ๋ค ์„ค์ • ===")
737
+ logger.info(f"์นดํ…Œ๊ณ ๋ฆฌ ๋žœ๋ค: {random_settings['category_random']}")
738
+ logger.info(f"์ถœ์‹œํƒ€์ด๋ฐ ๋žœ๋ค: {random_settings['launch_timing_random']}")
739
+ logger.info(f"๊ณ„์ ˆ์„ฑ ๋žœ๋ค: {random_settings['seasonality_random']}")
740
+ logger.info(f"๋งค์ถœ๋ชฉํ‘œ ๋žœ๋ค: {random_settings['sales_target_random']}")
741
+ logger.info(f"ํŒ๋งค์ฑ„๋„ ๋žœ๋ค: {random_settings['sales_channel_random']}")
742
+ logger.info(f"๊ฒฝ์Ÿ๊ฐ•๋„ ๋žœ๋ค: {random_settings['competition_level_random']}")
743
+
744
+ return random_settings, fixed_values
745
+
746
+ def generate_sourcing_keywords(category, additional_request, launch_timing, seasonality, sales_target, sales_channel, competition_level, search_engine="Google ๊ฒ€์ƒ‰ ๊ทธ๋ผ์šด๋”ฉ๋งŒ"):
747
+ """๋‹ค์–‘์„ฑ ๊ฐ•ํ™”๋œ ์‡ผํ•‘ ํ‚ค์›Œ๋“œ 50๊ฐœ๋ฅผ ์ƒ์„ฑํ•˜๋Š” ํ•จ์ˆ˜"""
748
+ logger.info("=== ๋‹ค์–‘์„ฑ ๊ฐ•ํ™” ์‡ผํ•‘ํ‚ค์›Œ๋“œ ์ƒ์„ฑ ์‹œ์ž‘ ===")
749
+ logger.info(f"์ž…๋ ฅ ์กฐ๊ฑด - ๊ฒ€์ƒ‰์—”์ง„: {search_engine}")
750
+ logger.info(f"์ž…๋ ฅ ์กฐ๊ฑด - ์นดํ…Œ๊ณ ๋ฆฌ: {category}")
751
+ logger.info(f"์ž…๋ ฅ ์กฐ๊ฑด - ์ถ”๊ฐ€์š”์ฒญ: {additional_request}")
752
+ logger.info(f"์ž…๋ ฅ ์กฐ๊ฑด - ์ถœ์‹œํƒ€์ด๋ฐ: {launch_timing}")
753
+ logger.info(f"์ž…๋ ฅ ์กฐ๊ฑด - ๊ณ„์ ˆ์„ฑ: {seasonality}")
754
+ logger.info(f"์ž…๋ ฅ ์กฐ๊ฑด - ๋งค์ถœ๋ชฉํ‘œ: {sales_target}")
755
+ logger.info(f"์ž…๋ ฅ ์กฐ๊ฑด - ํŒ๋งค์ฑ„๋„: {sales_channel}")
756
+ logger.info(f"์ž…๋ ฅ ์กฐ๊ฑด - ๊ฒฝ์Ÿ๊ฐ•๋„: {competition_level}")
757
+
758
+ try:
759
+ logger.info("Gemini ํด๋ผ์ด์–ธํŠธ ์ดˆ๊ธฐํ™” ์ค‘...")
760
+ client = initialize_gemini()
761
+
762
+ # ๋งค๋ฒˆ ๋‹ค๋ฅธ ์‹œ๋“œ๋กœ ๋žœ๋ค์„ฑ ๋ณด์žฅ
763
+ current_time = datetime.now()
764
+ random_seed = current_time.microsecond + current_time.second * 1000
765
+ random.seed(random_seed)
766
+ logger.info(f"๋žœ๋ค ์‹œ๋“œ ์„ค์ •: {random_seed}")
767
+
768
+ # ํ”„๋กฌํ”„ํŠธ ๊ตฌ์„ฑ
769
+ logger.info("๋‹ค์–‘์„ฑ ๊ฐ•ํ™” ํ”„๋กฌํ”„ํŠธ ๊ตฌ์„ฑ ์ค‘...")
770
+
771
+ # ๋žœ๋ค ์„ค์ • ์ฒ˜๋ฆฌ
772
+ random_settings, fixed_values = apply_random_selection_for_keywords(
773
+ category, launch_timing, seasonality, sales_target, sales_channel, competition_level
774
+ )
775
+
776
+ # ๋‹ค์–‘ํ•œ ํ‚ค์›Œ๋“œ ์กฐํ•ฉ ๋ฏธ๋ฆฌ ์ƒ์„ฑ
777
+ diverse_combinations = generate_diverse_keyword_combinations(category, 60)
778
+ logger.info(f"๋‹ค์–‘ํ•œ ์กฐํ•ฉ ์ƒ์„ฑ ์™„๋ฃŒ: {len(diverse_combinations)}๊ฐœ")
779
+
780
+ # ๊ฒ€์ƒ‰ ์—”์ง„๋ณ„ ์ฒ˜๋ฆฌ
781
+ search_info = ""
782
+ config_tools = []
783
+
784
+ if search_engine == "๋ชจ๋“  ๊ฒ€์ƒ‰ ์—”์ง„ ํ†ตํ•ฉ ๋ถ„์„ (์ถ”์ฒœ)":
785
+ logger.info("๐Ÿ” ๋‹ค์–‘์„ฑ ๊ฐ•ํ™” ํ†ตํ•ฉ ๋ถ„์„ ์‹œ์ž‘...")
786
+
787
+ # Google ๊ฒ€์ƒ‰ ๊ทธ๋ผ์šด๋”ฉ ๋„๊ตฌ ์„ค์ •
788
+ google_search_tool = Tool(google_search=GoogleSearch())
789
+ config_tools = [google_search_tool]
790
+
791
+ # ๋‹ค์–‘์„ฑ ๊ฐ•ํ™” ์ข…ํ•ฉ ์‹œ์žฅ ๋ถ„์„ ์‹คํ–‰
792
+ comprehensive_analysis = comprehensive_market_analysis(category, seasonality, sales_target)
793
+
794
+ search_info = f"""
795
+ ๐Ÿ” === ๋‹ค์–‘์„ฑ ๊ฐ•ํ™” ํ†ตํ•ฉ ๋ถ„์„ ๊ฒฐ๊ณผ ===
796
+ ๐Ÿ“ˆ Google ๊ฒ€์ƒ‰ ๊ทธ๋ผ์šด๋”ฉ: ์‹ค์‹œ๊ฐ„ ๋‹ค์–‘ํ•œ ์‡ผํ•‘ํ‚ค์›Œ๋“œ ํŠธ๋ Œ๋“œ ๋ถ„์„ (์ž๋™ ์‹คํ–‰)
797
+ ๐Ÿ›’ ๋„ค์ด๋ฒ„ ์‡ผํ•‘ API: ํ•œ๊ตญ ์‡ผํ•‘๋ชฐ ๋‹ค์–‘ํ•œ ํ‚ค์›Œ๋“œ ๋ฐ์ดํ„ฐ ๋ถ„์„
798
+ ๐ŸŒ DuckDuckGo ๊ฒ€์ƒ‰: ๊ธ€๋กœ๋ฒŒ ๋‹ค์–‘ํ•œ ์‡ผํ•‘ํ‚ค์›Œ๋“œ ์ •๋ณด ๋ถ„์„
799
+ {comprehensive_analysis}
800
+ ๐Ÿ’ก ์œ„ ๋ชจ๋“  ๋ฐ์ดํ„ฐ๋ฅผ ์ข…ํ•ฉํ•˜์—ฌ ๋งค๋ฒˆ ๋‹ค๋ฅธ ์กฐํ•ฉ์˜ ์‡ผํ•‘ํ‚ค์›Œ๋“œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
801
+ ๐ŸŽฒ ๋žœ๋ค ์‹œ๋“œ: {random_seed} (๋งค๋ฒˆ ๋‹ค๋ฅธ ๊ฒฐ๊ณผ ๋ณด์žฅ)
802
+ """
803
+
804
+ elif search_engine == "Google ๊ฒ€์ƒ‰ ๊ทธ๋ผ์šด๋”ฉ๋งŒ":
805
+ logger.info("Google ๊ฒ€์ƒ‰ ๋„๊ตฌ ์„ค์ • ์ค‘...")
806
+ google_search_tool = Tool(google_search=GoogleSearch())
807
+ config_tools = [google_search_tool]
808
+ search_info = f"Google ๊ฒ€์ƒ‰ ๊ทธ๋ผ์šด๋”ฉ์„ ํ†ตํ•œ ๋‹ค์–‘ํ•œ ์‹ค์‹œ๊ฐ„ ์‡ผํ•‘ํ‚ค์›Œ๋“œ ๋ถ„์„ (์‹œ๋“œ: {random_seed})"
809
+
810
+ elif search_engine in ["๋„ค์ด๋ฒ„ ๊ฒ€์ƒ‰ API๋งŒ", "DuckDuckGo ๊ฒ€์ƒ‰๋งŒ"]:
811
+ logger.info(f"{search_engine} ์‚ฌ์šฉํ•˜์—ฌ ๋‹ค์–‘ํ•œ ์‡ผํ•‘ํ‚ค์›Œ๋“œ ์กฐ์‚ฌ ์ค‘...")
812
+ # ๋‹ค์–‘์„ฑ ๊ฐ•ํ™”๋ฅผ ์œ„ํ•œ ๊ฒ€์ƒ‰ ์‹คํ–‰
813
+ search_queries = []
814
+
815
+ # ๋žœ๋คํ•˜๊ฒŒ ๋‹ค์–‘ํ•œ ๊ฒ€์ƒ‰์–ด ์ƒ์„ฑ
816
+ base_items = random.sample(diverse_combinations, 8)
817
+ for item in base_items:
818
+ search_queries.append(f"{item} ์‡ผํ•‘ํ‚ค์›Œ๋“œ")
819
+
820
+ search_results = ""
821
+ for query in search_queries:
822
+ result = search_with_api(query, search_engine)
823
+ search_results += f"[๊ฒ€์ƒ‰์–ด: {query}]\n{result}\n\n"
824
+
825
+ search_info = f"{search_engine} ๋‹ค์–‘ํ•œ ์‡ผํ•‘ํ‚ค์›Œ๋“œ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ (์‹œ๋“œ: {random_seed}):\n{search_results}"
826
+
827
+ else: # ๊ฒ€์ƒ‰ ์—†์ด AI๋งŒ ์‚ฌ์šฉ
828
+ logger.info("๊ฒ€์ƒ‰ ์—†์ด AI ์ง€์‹๋งŒ ์‚ฌ์šฉ")
829
+ search_info = f"AI ๋‚ด์žฅ ์ง€์‹์„ ๊ธฐ๋ฐ˜์œผ๋กœ ๋‹ค์–‘ํ•œ ์‡ผํ•‘ํ‚ค์›Œ๋“œ ์ƒ์„ฑ (์‹œ๋“œ: {random_seed})"
830
+
831
+ # ๋‹ค์–‘์„ฑ์„ ๊ฐ•ํ™”ํ•œ ํ”„๋กฌํ”„ํŠธ - ๋งค๋ฒˆ ๋‹ค๋ฅธ ์กฐํ•ฉ ์š”์ฒญ
832
+ diverse_sample = random.sample(diverse_combinations, 20)
833
+
834
+ prompt = f"""
835
+ ๐ŸŽฏ ๋‹ค์–‘์„ฑ ๊ฐ•ํ™” ์‡ผํ•‘ํ‚ค์›Œ๋“œ ๋ฐœ๊ตด ์‹œ์Šคํ…œ v5.0
836
+ โšก ์ค‘์š”: ์ ˆ๋Œ€ ์ค‘๋ณต๋˜์ง€ ์•Š๋Š” ๋‹ค์–‘ํ•œ ํ‚ค์›Œ๋“œ๋งŒ ์ƒ์„ฑํ•˜์„ธ์š”!
837
+ ๐Ÿ”ฌ ์—ญํ•  ์ •์˜
838
+ ๋‹น์‹ ์€ ๋งค๋ฒˆ ์™„์ „ํžˆ ๋‹ค๋ฅธ ์กฐํ•ฉ์˜ ์‡ผํ•‘ํ‚ค์›Œ๋“œ๋ฅผ ์ƒ์„ฑํ•˜๋Š” ์ „๋ฌธ๊ฐ€์ž…๋‹ˆ๋‹ค.
839
+ ๐ŸŽฏ ๋ชฉํ‘œ
840
+ ์ฃผ์–ด์ง„ ์กฐ๊ฑด์— ๋งž๋Š” ์‹ค์ œ ์‡ผํ•‘ํ‚ค์›Œ๋“œ 50๊ฐœ๋ฅผ ๋ฐœ๊ตดํ•˜๋˜, ์ ˆ๋Œ€ ์ค‘๋ณต๋˜์ง€ ์•Š๊ณ  ๋งค๋ฒˆ ๋‹ค๋ฅธ ์กฐํ•ฉ์œผ๋กœ ๊ตฌ์„ฑํ•˜์‹ญ์‹œ์˜ค.
841
+ ๐Ÿ“‹ ์ž…๋ ฅ๋œ ์กฐ๊ฑด:
842
+ ์นดํ…Œ๊ณ ๋ฆฌ: {category}
843
+ ์ถ”๊ฐ€ ์š”์ฒญ์‚ฌํ•ญ: {additional_request}
844
+ ์ถœ์‹œํƒ€์ด๋ฐ: {launch_timing}
845
+ ๊ณ„์ ˆ์„ฑ: {seasonality}
846
+ ๋งค์ถœ๋ชฉํ‘œ: {sales_target}
847
+ ํŒ๋งค์ฑ„๋„: {sales_channel}
848
+ ๊ฒฝ์Ÿ๊ฐ•๋„: {competition_level}
849
+ ๊ฒ€์ƒ‰์—”์ง„: {search_engine}
850
+ ๐Ÿ” ์‡ผํ•‘ํ‚ค์›Œ๋“œ ๋ถ„์„ ์ •๋ณด:
851
+ {search_info}
852
+ ๐ŸŽฒ ๋‹ค์–‘์„ฑ ๋ณด์žฅ ์ฐธ๊ณ  ์กฐํ•ฉ ์˜ˆ์‹œ (์ด๊ฒƒ๊ณผ ๋‹ค๋ฅด๊ฒŒ ์ƒ์„ฑํ•˜์„ธ์š”):
853
+ {', '.join(diverse_sample[:10])}
854
+ โš ๏ธ ํ‚ค์›Œ๋“œ๋ณ„ ๋žœ๋ค ์ ์šฉ ๊ทœ์น™:
855
+ ๊ฐ ํ‚ค์›Œ๋“œ๋งˆ๋‹ค ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ ์šฉํ•˜์„ธ์š”:
856
+ {"- ์นดํ…Œ๊ณ ๋ฆฌ: ๋งค ํ‚ค์›Œ๋“œ๋งˆ๋‹ค " + str(random_settings['categories']) + " ์ค‘์—์„œ ๋žœ๋ค ์„ ํƒ" if random_settings['category_random'] else f"- ์นดํ…Œ๊ณ ๋ฆฌ: {fixed_values['category']} ๊ณ ์ •"}
857
+ {"- ์ถœ์‹œํƒ€์ด๋ฐ: ๋งค ํ‚ค์›Œ๋“œ๋งˆ๋‹ค " + str(random_settings['launch_timings']) + " ์ค‘์—์„œ ๋žœ๋ค ์„ ํƒ" if random_settings['launch_timing_random'] else f"- ์ถœ์‹œํƒ€์ด๋ฐ: {fixed_values['launch_timing']} ๊ณ ์ •"}
858
+ {"- ๊ณ„์ ˆ์„ฑ: ๋งค ํ‚ค์›Œ๋“œ๋งˆ๋‹ค " + str(random_settings['seasonalities']) + " ์ค‘์—์„œ ๋žœ๋ค ์„ ํƒ" if random_settings['seasonality_random'] else f"- ๊ณ„์ ˆ์„ฑ: {fixed_values['seasonality']} ๊ณ ์ •"}
859
+ {"- ๋งค์ถœ๋ชฉํ‘œ: ๋งค ํ‚ค์›Œ๋“œ๋งˆ๋‹ค " + str(random_settings['sales_targets']) + " ์ค‘์—์„œ ๋žœ๋ค ์„ ํƒ" if random_settings['sales_target_random'] else f"- ๋งค์ถœ๋ชฉํ‘œ: {fixed_values['sales_target']} ๊ณ ์ •"}
860
+ {"- ํŒ๋งค์ฑ„๋„: ๋งค ํ‚ค์›Œ๋“œ๋งˆ๋‹ค " + str(random_settings['sales_channels']) + " ์ค‘์—์„œ ๋žœ๋ค ์„ ํƒ" if random_settings['sales_channel_random'] else f"- ํŒ๋งค์ฑ„๋„: {fixed_values['sales_channel']} ๊ณ ์ •"}
861
+ {"- ๊ฒฝ์Ÿ๊ฐ•๋„: ๋งค ํ‚ค์›Œ๋“œ๋งˆ๋‹ค " + str(random_settings['competition_levels']) + " ์ค‘์—์„œ ๋žœ๋ค ์„ ํƒ" if random_settings['competition_level_random'] else f"- ๊ฒฝ์Ÿ๊ฐ•๋„: {fixed_values['competition_level']} ๊ณ ์ •"}
862
+ โš™๏ธ ๋‹ค์–‘์„ฑ ๊ฐ•ํ™” ์›Œํฌํ”Œ๋กœ์šฐ
863
+ 1๋‹จ๊ณ„: ์™„์ „ํžˆ ์ƒˆ๋กœ์šด ์กฐํ•ฉ ์ƒ์„ฑ
864
+ - ์ด์ „ ๊ฒฐ๊ณผ์™€ ์ ˆ๋Œ€ ์ค‘๋ณต๋˜์ง€ ์•Š๋Š” ํ‚ค์›Œ๋“œ ์กฐํ•ฉ
865
+ - ์†Œ์žฌ({', '.join(MATERIAL_KEYWORDS[:5])}) + ์ƒํ’ˆ๋ช… ์กฐํ•ฉ
866
+ - ํ˜•ํƒœ({', '.join(SHAPE_KEYWORDS[:5])}) + ์ƒํ’ˆ๋ช… ์กฐํ•ฉ
867
+ - ๊ธฐ๋Šฅ({', '.join(FUNCTION_KEYWORDS[:5])}) + ์ƒํ’ˆ๋ช… ์กฐํ•ฉ
868
+ 2๋‹จ๊ณ„: ์ค‘๋ณต ๋ฐฉ์ง€ ํ•„ํ„ฐ๋ง
869
+ - ๋™์ผํ•œ ํ‚ค์›Œ๋“œ ์กฐํ•ฉ ์™„์ „ ๋ฐฐ์ œ
870
+ - ์œ ์‚ฌํ•œ ์˜๋ฏธ์˜ ํ‚ค์›Œ๋“œ ์กฐํ•ฉ ๋ฐฐ์ œ
871
+ - ๋งค๋ฒˆ ์ƒˆ๋กœ์šด ๊ฐ๋„๋กœ ์ ‘๊ทผ
872
+ 3๋‹จ๊ณ„: 50๊ฐœ ๋‹ค์–‘ํ•œ ํ‚ค์›Œ๋“œ ์„ ๋ณ„
873
+ - ๋ธŒ๋žœ๋“œ๋ช… ์ ˆ๋Œ€ ๊ธˆ์ง€
874
+ - ๋ณต์žกํ•œ ๊ธฐ์ˆ  ์šฉ์–ด ๊ธˆ์ง€
875
+ - ์ตœ๋Œ€ 2๊ฐœ ๋‹จ์–ด ์กฐํ•ฉ๋งŒ ํ—ˆ์šฉ
876
+ โš ๏ธ ๋‹ค์–‘์„ฑ ๊ฐ•ํ™” ํ‚ค์›Œ๋“œ ๊ตฌ์„ฑ ๊ทœ์น™ (๋งค์šฐ ์ค‘์š”):
877
+ ๐Ÿšซ ์ ˆ๋Œ€ ๊ธˆ์ง€ ์‚ฌํ•ญ:
878
+ - ๋™์ผํ•˜๊ฑฐ๋‚˜ ์œ ์‚ฌํ•œ ํ‚ค์›Œ๋“œ ๋ฐ˜๋ณต
879
+ - ๋ธŒ๋žœ๋“œ๋ช… (์‚ผ์„ฑ, LG, ๋‚˜์ดํ‚ค ๋“ฑ)
880
+ - ๋ณต์žกํ•œ ๊ธฐ์ˆ  ์šฉ์–ด
881
+ - 3๊ฐœ ์ด์ƒ ๋ณตํ•ฉ์–ด
882
+ โœ… ๋ฐ˜๋“œ์‹œ ๋‹ค์–‘ํ•˜๊ฒŒ ํฌํ•จํ•ด์•ผ ํ•  ํ˜•ํƒœ:
883
+ 1. ์†Œ์žฌ๋ณ„ ํ‚ค์›Œ๋“œ (์˜ˆ: ๋Œ€๋‚˜๋ฌด ๋„๋งˆ, ๊ตฌ๋ฆฌ ์ปต)
884
+ 2. ํ˜•ํƒœ๋ณ„ ํ‚ค์›Œ๋“œ (์˜ˆ: ์›ํ˜• ์ ‘์‹œ, ์Šฌ๋ฆผ ์ผ€์ด์Šค)
885
+ 3. ๊ธฐ๋Šฅ๋ณ„ ํ‚ค์›Œ๋“œ (์˜ˆ: ๋ฐฉ์ˆ˜ ํŒŒ์šฐ์น˜, ํ•ญ๊ท  ์ˆ˜๊ฑด)
886
+ 4. ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ํ‚ค์›Œ๋“œ (์˜ˆ: ์ˆ˜๋‚ฉํ•จ, ์กฐ๋ฆฌ๋„๊ตฌ)
887
+ ๐ŸŽฏ ๋‹ค์–‘์„ฑ ๋ณด์žฅ ์ „๋žต:
888
+ - ์ ˆ๋Œ€ ๊ฐ™์€ ์†Œ์žฌ๋ฅผ 2๋ฒˆ ์ด์ƒ ์‚ฌ์šฉํ•˜์ง€ ๋งˆ์„ธ์š”
889
+ - ์ ˆ๋Œ€ ๊ฐ™์€ ํ˜•ํƒœ๋ฅผ 2๋ฒˆ ์ด์ƒ ์‚ฌ์šฉํ•˜์ง€ ๋งˆ์„ธ์š”
890
+ - ์ ˆ๋Œ€ ๊ฐ™์€ ๊ธฐ๋Šฅ์„ 2๋ฒˆ ์ด์ƒ ์‚ฌ์šฉํ•˜์ง€ ๋งˆ์„ธ์š”
891
+ - ๋งค ํ‚ค์›Œ๋“œ๋งˆ๋‹ค ์™„์ „ํžˆ ๋‹ค๋ฅธ ์กฐํ•ฉ์œผ๋กœ ์ƒ์„ฑํ•˜์„ธ์š”
892
+ ์˜ฌ๋ฐ”๋ฅธ ๋‹ค์–‘ํ•œ ํ‚ค์›Œ๋“œ ์˜ˆ์‹œ:
893
+ โœ… ๋Œ€๋‚˜๋ฌด ๋„๋งˆ (์†Œ์žฌ+์ƒํ’ˆ)
894
+ โœ… ์›ํ˜• ์ ‘์‹œ (ํ˜•ํƒœ+์ƒํ’ˆ)
895
+ โœ… ๋ฐฉ์ˆ˜ ํŒŒ์šฐ์น˜ (๊ธฐ๋Šฅ+์ƒํ’ˆ)
896
+ โœ… ์„ธ๋ผ๋ฏน ๋จธ๊ทธ์ปต (์†Œ์žฌ+์ƒํ’ˆ)
897
+ โœ… ์ ‘์ด์‹ ์„ ๋ฐ˜ (ํ˜•ํƒœ+์ƒํ’ˆ)
898
+ โœ… ํ•ญ๊ท  ์ˆ˜๊ฑด (๊ธฐ๋Šฅ+์ƒํ’ˆ)
899
+ ์ž˜๋ชป๋œ ๋ฐ˜๋ณต ํ‚ค์›Œ๋“œ ์˜ˆ์‹œ:
900
+ โŒ ๋Œ€๋‚˜๋ฌด ๋„๋งˆ, ๋Œ€๋‚˜๋ฌด ์ “๊ฐ€๋ฝ (์†Œ์žฌ ๋ฐ˜๋ณต)
901
+ โŒ ์›ํ˜• ์ ‘์‹œ, ์›ํ˜• ์Ÿ๋ฐ˜ (ํ˜•ํƒœ ๋ฐ˜๋ณต)
902
+ โŒ ๋ฐฉ์ˆ˜ ํŒŒ์šฐ์น˜, ๋ฐฉ์ˆ˜ ์ผ€์ด์Šค (๊ธฐ๋Šฅ ๋ฐ˜๋ณต)
903
+ ๐Ÿ“‹ ์ถœ๋ ฅ ํ˜•์‹:
904
+ ์˜ค์ง ์™„์ „ํžˆ ๋‹ค๋ฅธ ์‡ผํ•‘ํ‚ค์›Œ๋“œ๋งŒ ํ•œ ์ค„์”ฉ 50๊ฐœ ์ถœ๋ ฅ
905
+ - ๋ฒˆํ˜ธ ๊ธˆ์ง€
906
+ - ์„ค๋ช… ๊ธˆ์ง€
907
+ - ๊ธฐํ˜ธ๋‚˜ ํŠน์ˆ˜๋ฌธ์ž ๊ธˆ์ง€
908
+ - ๊ด„ํ˜ธ ์•ˆ ์„ค๋ช… ๊ธˆ์ง€
909
+ - ์ˆœ์ˆ˜ ํ‚ค์›Œ๋“œ๋งŒ ์ถœ๋ ฅ
910
+ - ์ ˆ๋Œ€ ์ค‘๋ณต ๊ธˆ์ง€
911
+ ์˜ˆ์‹œ ์ถœ๋ ฅ ํ˜•ํƒœ (๋งค๋ฒˆ ์™„์ „ํžˆ ๋‹ค๋ฅด๊ฒŒ):
912
+ ์œ ๋ฆฌ ํ™”๋ถ„
913
+ ์ ‘์ด์‹ ์˜์ž
914
+ ํ•ญ๊ท  ๋„๋งˆ
915
+ ์•Œ๋ฃจ๋ฏธ๋Š„ ํ…€๋ธ”๋Ÿฌ
916
+ ์Šฌ๋ฆผ ํŒŒ์ผํ•จ
917
+ โšก ์ง€๊ธˆ ๋ฐ”๋กœ ์ ˆ๋Œ€ ์ค‘๋ณต๋˜์ง€ ์•Š๋Š” ์™„์ „ํžˆ ์ƒˆ๋กœ์šด ์‡ผํ•‘ํ‚ค์›Œ๋“œ 50๊ฐœ๋ฅผ ๊ฐ๊ฐ ๋‹ค๋ฅธ ๋žœ๋ค ์กฐ๊ฑด์„ ์ ์šฉํ•˜์—ฌ ์ถœ๋ ฅํ•˜์„ธ์š”.
918
+ ๋งค๋ฒˆ ์‹คํ–‰ํ•  ๋•Œ๋งˆ๋‹ค ์™„์ „ํžˆ ๋‹ค๋ฅธ ๊ฒฐ๊ณผ๊ฐ€ ๋‚˜์™€์•ผ ํ•ฉ๋‹ˆ๋‹ค!
keyword_processor.py ADDED
@@ -0,0 +1,388 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ํ‚ค์›Œ๋“œ ์ฒ˜๋ฆฌ ๊ด€๋ จ ๊ธฐ๋Šฅ - ์•ž๋’ค ์กฐํ•ฉ ์ค‘ ๋†’์€ ๊ฒ€์ƒ‰๋Ÿ‰๋งŒ ์„ ํƒ, ์นดํ…Œ๊ณ ๋ฆฌ ํ•ญ๋ชฉ ์ œ๊ฑฐ
3
+ - ํ‚ค์›Œ๋“œ ์ถ”์ถœ ๋ฐ ์กฐํ•ฉ
4
+ - ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์ฒ˜๋ฆฌ
5
+ """
6
+
7
+ import pandas as pd
8
+ import re
9
+ from collections import defaultdict, Counter
10
+ import text_utils
11
+ import keyword_search
12
+ import product_search
13
+ import logging
14
+
15
+ # ๋กœ๊น… ์„ค์ •
16
+ logger = logging.getLogger(__name__)
17
+ logger.setLevel(logging.INFO)
18
+ formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
19
+ handler = logging.StreamHandler()
20
+ handler.setFormatter(formatter)
21
+ logger.addHandler(handler)
22
+
23
+ def process_search_results(search_results, current_keyword="", exclude_zero_volume=True):
24
+ """
25
+ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ์—์„œ ํ‚ค์›Œ๋“œ์™€ ์นดํ…Œ๊ณ ๋ฆฌ ์ •๋ณด ์ถ”์ถœ ๋ฐ ์ฒ˜๋ฆฌ - ์•ž๋’ค ์กฐํ•ฉ ์ค‘ ๋†’์€ ๊ฒ€์ƒ‰๋Ÿ‰๋งŒ ์„ ํƒ
26
+
27
+ Args:
28
+ search_results (dict): ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์ •๋ณด
29
+ current_keyword (str): ํ˜„์žฌ ๊ฒ€์ƒ‰ ์ค‘์ธ ํ‚ค์›Œ๋“œ
30
+ exclude_zero_volume (bool): ๊ฒ€์ƒ‰๋Ÿ‰์ด 0์ธ ํ‚ค์›Œ๋“œ ์ œ์™ธ ์—ฌ๋ถ€
31
+
32
+ Returns:
33
+ dict: ์ฒ˜๋ฆฌ๋œ ๊ฒฐ๊ณผ
34
+ """
35
+ logger.info("\n===== ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์ฒ˜๋ฆฌ ์‹œ์ž‘ =====")
36
+ logger.info(f"ํ˜„์žฌ ํ‚ค์›Œ๋“œ: '{current_keyword}'")
37
+ logger.info(f"๊ฒ€์ƒ‰๋Ÿ‰ 0 ํ‚ค์›Œ๋“œ ์ œ์™ธ: {exclude_zero_volume}")
38
+
39
+ if not search_results or not search_results.get("product_list"):
40
+ logger.warning("๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
41
+ return {
42
+ "products_df": None,
43
+ "keywords_df": None,
44
+ "categories": ["์ „์ฒด ๋ณด๊ธฐ"],
45
+ "message": "๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค."
46
+ }
47
+
48
+ product_list = search_results["product_list"]
49
+ combo_candidates = search_results["combo_candidates"]
50
+ category_counter = search_results["category_counter"]
51
+ keyword_indices = search_results["keyword_indices"]
52
+ keyword_pairs = search_results.get("keyword_pairs", {}) # ์•ž๋’ค ์กฐํ•ฉ ์ •๋ณด
53
+
54
+ logger.info(f"๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ - ์ƒํ’ˆ ์ˆ˜: {len(product_list)}๊ฐœ")
55
+ logger.info(f"๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ - ์กฐํ•ฉ ํ›„๋ณด ์ˆ˜: {len(combo_candidates)}๊ฐœ")
56
+ logger.info(f"๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ - ์นดํ…Œ๊ณ ๋ฆฌ ์ˆ˜: {len(category_counter)}๊ฐœ")
57
+
58
+ # ์ƒํ’ˆ ์ •๋ณด ๋ฐ์ดํ„ฐํ”„๋ ˆ์ž„ ์ƒ์„ฑ
59
+ df_products = pd.DataFrame(product_list)
60
+
61
+ # API ํ‚ค์›Œ๋“œ๋ฅผ UI ํ‚ค์›Œ๋“œ๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ๋งคํ•‘ ์ƒ์„ฑ
62
+ api_to_ui_keywords = {}
63
+
64
+ for api_keyword in combo_candidates.keys():
65
+ # API ํ‚ค์›Œ๋“œ์—์„œ UI ํ‚ค์›Œ๋“œ๋กœ ๋ณ€ํ™˜
66
+ if current_keyword and current_keyword in api_keyword:
67
+ # ๋ฉ”์ธ ํ‚ค์›Œ๋“œ ์ž์ฒด์ธ ๊ฒฝ์šฐ
68
+ if api_keyword == current_keyword:
69
+ api_to_ui_keywords[api_keyword] = current_keyword
70
+ continue
71
+
72
+ # ๋ฉ”์ธ ํ‚ค์›Œ๋“œ๊ฐ€ ์ด๋ฏธ ํฌํ•จ๋œ ๊ฒฝ์šฐ (์˜ˆ: ๊ฐ‘์˜ค์ง•์–ด, ๊ท€์˜ค์ง•์–ด)
73
+ # ๊ณต๋ฐฑ ์žˆ๋Š” ํ˜•ํƒœ๋กœ ๋ณ€ํ™˜
74
+ ui_keyword = api_keyword
75
+ # ๊ณต๋ฐฑ์ด ์—†๋Š” ํ˜•ํƒœ๋ผ๋ฉด ์ ์ ˆํ•œ ์œ„์น˜์— ๊ณต๋ฐฑ ์ถ”๊ฐ€
76
+ if " " not in api_keyword:
77
+ # ๋ฉ”์ธ ํ‚ค์›Œ๋“œ ๊ธฐ์ค€์œผ๋กœ ๋ถ„๋ฆฌ
78
+ if api_keyword.startswith(current_keyword):
79
+ # ์˜ค์ง•์–ด๊ฐ‘ => ์˜ค์ง•์–ด ๊ฐ‘
80
+ prefix = current_keyword
81
+ suffix = api_keyword[len(current_keyword):]
82
+ if suffix:
83
+ ui_keyword = f"{prefix} {suffix}"
84
+ elif api_keyword.endswith(current_keyword):
85
+ # ๊ฐ‘์˜ค์ง•์–ด => ๊ฐ‘ ์˜ค์ง•์–ด
86
+ prefix = api_keyword[:-len(current_keyword)]
87
+ suffix = current_keyword
88
+ if prefix:
89
+ ui_keyword = f"{prefix} {suffix}"
90
+ else:
91
+ # ๋ฉ”์ธ ํ‚ค์›Œ๋“œ๊ฐ€ ์ค‘๊ฐ„์— ์žˆ๋Š” ๊ฒฝ์šฐ
92
+ idx = api_keyword.find(current_keyword)
93
+ if idx > 0:
94
+ prefix = api_keyword[:idx]
95
+ middle = current_keyword
96
+ suffix = api_keyword[idx+len(current_keyword):]
97
+ ui_keyword = f"{prefix} {middle}"
98
+ if suffix:
99
+ ui_keyword += f" {suffix}"
100
+
101
+ api_to_ui_keywords[api_keyword] = ui_keyword
102
+ else:
103
+ # ๋ฉ”์ธ ํ‚ค์›Œ๋“œ๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ - ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉ
104
+ api_to_ui_keywords[api_keyword] = api_keyword
105
+
106
+ # === ์ˆ˜์ •๋œ ๋ถ€๋ถ„: ๊ฒ€์ƒ‰๋Ÿ‰ ์กฐํšŒ ํ›„ ์•ž๋’ค ์กฐํ•ฉ ์ค‘ ๋†’์€ ๊ฒƒ๋งŒ ์„ ํƒ ===
107
+ logger.info(f"\n๊ฒ€์ƒ‰๋Ÿ‰ ์กฐํšŒ ๋Œ€์ƒ ํ‚ค์›Œ๋“œ ์ˆ˜: {len(combo_candidates)}๊ฐœ")
108
+ search_volumes = keyword_search.fetch_all_search_volumes(list(combo_candidates.keys()))
109
+ logger.info(f"๊ฒ€์ƒ‰๋Ÿ‰ ์กฐํšŒ ์™„๋ฃŒ: {len(search_volumes)}๊ฐœ ๊ฒฐ๊ณผ")
110
+
111
+ # ์•ž๋’ค ์กฐํ•ฉ ์ค‘ ๋†’์€ ๊ฒ€์ƒ‰๋Ÿ‰๋งŒ ์„ ํƒ
112
+ if keyword_pairs and current_keyword:
113
+ logger.info("\n=== ์•ž๋’ค ์กฐํ•ฉ ์ค‘ ๋†’์€ ๊ฒ€์ƒ‰๋Ÿ‰ ์„ ํƒ ===")
114
+ filtered_candidates = {}
115
+
116
+ # ๋ฉ”์ธ ํ‚ค์›Œ๋“œ๋Š” ํ•ญ์ƒ ํฌํ•จ
117
+ main_api = current_keyword.replace(" ", "")
118
+ if main_api in combo_candidates:
119
+ filtered_candidates[main_api] = combo_candidates[main_api]
120
+ logger.info(f"๋ฉ”์ธ ํ‚ค์›Œ๋“œ ์œ ์ง€: '{current_keyword}'")
121
+
122
+ # ๋ฉ”์ธ ํ‚ค์›Œ๋“œ๊ฐ€ ํฌํ•จ๋œ ๋ณตํ•ฉ์–ด๋„ ์œ ์ง€
123
+ for api_kw, categories in combo_candidates.items():
124
+ ui_kw = api_to_ui_keywords[api_kw]
125
+ if current_keyword in ui_kw and api_kw != main_api and api_kw not in [pair_info["front"].replace(" ", "") for pair_info in keyword_pairs.values()] and api_kw not in [pair_info["back"].replace(" ", "") for pair_info in keyword_pairs.values()]:
126
+ filtered_candidates[api_kw] = categories
127
+ logger.info(f"๋ฉ”์ธ ํ‚ค์›Œ๋“œ ํฌํ•จ ๋ณตํ•ฉ์–ด ์œ ์ง€: '{ui_kw}'")
128
+
129
+ # ์•ž๋’ค ์กฐํ•ฉ ๋น„๊ต
130
+ for base_word, pair_info in keyword_pairs.items():
131
+ front_kw = pair_info["front"] # "ํ‚ค์›Œ๋“œ ๋ฉ”์ธํ‚ค์›Œ๋“œ"
132
+ back_kw = pair_info["back"] # "๋ฉ”์ธํ‚ค์›Œ๋“œ ํ‚ค์›Œ๋“œ"
133
+
134
+ front_api = front_kw.replace(" ", "")
135
+ back_api = back_kw.replace(" ", "")
136
+
137
+ front_vol = search_volumes.get(front_api, {}).get("์ด๊ฒ€์ƒ‰๋Ÿ‰", 0)
138
+ back_vol = search_volumes.get(back_api, {}).get("์ด๊ฒ€์ƒ‰๋Ÿ‰", 0)
139
+
140
+ # ๋†’์€ ๊ฒ€์ƒ‰๋Ÿ‰ ์„ ํƒ
141
+ if front_vol > back_vol:
142
+ selected_api = front_api
143
+ selected_kw = front_kw
144
+ selected_vol = front_vol
145
+ removed_kw = back_kw
146
+ removed_vol = back_vol
147
+ elif back_vol > front_vol:
148
+ selected_api = back_api
149
+ selected_kw = back_kw
150
+ selected_vol = back_vol
151
+ removed_kw = front_kw
152
+ removed_vol = front_vol
153
+ elif front_vol == back_vol and front_vol > 0:
154
+ # ๊ฐ™์€ ๊ฒ€์ƒ‰๋Ÿ‰์ด๋ฉด ๋” ์ž์—ฐ์Šค๋Ÿฌ์šด ์ˆœ์„œ ์„ ํƒ (๋ฉ”์ธํ‚ค์›Œ๋“œ๊ฐ€ ๋’ค์— ์˜ค๋Š” ๊ฒƒ)
155
+ selected_api = back_api
156
+ selected_kw = back_kw
157
+ selected_vol = back_vol
158
+ removed_kw = front_kw
159
+ removed_vol = front_vol
160
+ else:
161
+ # ๋‘˜ ๋‹ค 0์ด๋ฉด ์ œ์™ธ
162
+ logger.info(f" '{base_word}' ์กฐํ•ฉ: ๋‘˜ ๋‹ค ๊ฒ€์ƒ‰๋Ÿ‰ 0์œผ๋กœ ์ œ์™ธ")
163
+ continue
164
+
165
+ # ์„ ํƒ๋œ ํ‚ค์›Œ๋“œ๋งŒ ์ถ”๊ฐ€
166
+ if selected_vol > 0 or not exclude_zero_volume:
167
+ filtered_candidates[selected_api] = combo_candidates[selected_api]
168
+ logger.info(f" '{base_word}' ์กฐํ•ฉ ์„ ํƒ: '{selected_kw}' ({selected_vol:,}) > '{removed_kw}' ({removed_vol:,})")
169
+ else:
170
+ logger.info(f" '{base_word}' ์กฐํ•ฉ: ๊ฒ€์ƒ‰๋Ÿ‰ 0์œผ๋กœ ์ œ์™ธ")
171
+
172
+ # ํ•„ํ„ฐ๋ง๋œ ์กฐํ•ฉ์œผ๋กœ ๊ต์ฒด
173
+ combo_candidates = filtered_candidates
174
+ logger.info(f"์•ž๋’ค ์กฐํ•ฉ ํ•„ํ„ฐ๋ง ์™„๋ฃŒ: {len(combo_candidates)}๊ฐœ ํ‚ค์›Œ๋“œ ์„ ํƒ")
175
+
176
+ # ๊ฒ€์ƒ‰๋Ÿ‰ 0 ํ‚ค์›Œ๋“œ ํ†ต๊ณ„
177
+ zero_volume_count = sum(1 for vol in search_volumes.values() if vol.get("์ด๊ฒ€์ƒ‰๋Ÿ‰", 0) == 0)
178
+ logger.info(f"๊ฒ€์ƒ‰๋Ÿ‰ 0์ธ ํ‚ค์›Œ๋“œ ์ˆ˜: {zero_volume_count}๊ฐœ ({zero_volume_count/max(1, len(search_volumes))*100:.1f}%)")
179
+
180
+ # ์ค‘๋ณต ํ‚ค์›Œ๋“œ ์ œ๊ฑฐ๋ฅผ ์œ„ํ•œ ์ •๊ทœํ™”๋œ ํ‚ค์›Œ๋“œ ์ง‘ํ•ฉ
181
+ normalized_keywords = {}
182
+
183
+ for api_keyword in combo_candidates.keys():
184
+ ui_keyword = api_to_ui_keywords[api_keyword]
185
+
186
+ # ๊ฒ€์ƒ‰๋Ÿ‰ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ
187
+ pc_count = 0
188
+ mobile_count = 0
189
+ total_count = 0
190
+ if api_keyword in search_volumes:
191
+ pc_count = search_volumes[api_keyword]["PC๊ฒ€์ƒ‰๋Ÿ‰"]
192
+ mobile_count = search_volumes[api_keyword]["๋ชจ๋ฐ”์ผ๊ฒ€์ƒ‰๋Ÿ‰"]
193
+ total_count = search_volumes[api_keyword]["์ด๊ฒ€์ƒ‰๋Ÿ‰"]
194
+
195
+ # ๊ฒ€์ƒ‰๋Ÿ‰ 0์ธ ํ‚ค์›Œ๋“œ ์ œ์™ธ ์˜ต์…˜ ์ ์šฉ
196
+ if exclude_zero_volume and total_count == 0:
197
+ logger.debug(f" - '{ui_keyword}' (API: '{api_keyword}') - ๊ฒ€์ƒ‰๋Ÿ‰ 0์œผ๋กœ ์ œ์™ธ๋จ")
198
+ continue
199
+
200
+ # 1. ๊ณต๋ฐฑ์„ ๊ธฐ์ค€์œผ๋กœ ๋‹จ์–ด ๋ถ„๋ฆฌ ํ›„ ์ •๋ ฌํ•ด ์ •๊ทœํ™” ํ‚ค ์ƒ์„ฑ
201
+ words = ui_keyword.split()
202
+ normalized = "".join(sorted(words))
203
+
204
+ # 2. ์ด๋ฏธ ์ •๊ทœํ™”๋œ ํ‚ค์›Œ๋“œ๊ฐ€ ์žˆ์œผ๋ฉด ๊ฒ€์ƒ‰๋Ÿ‰์ด ๋” ๋†’์€ ๊ฒƒ์„ ์„ ํƒ
205
+ if normalized in normalized_keywords:
206
+ existing_api_keyword, existing_ui_keyword, existing_total = normalized_keywords[normalized]
207
+ if total_count > existing_total:
208
+ logger.debug(f" - ์ค‘๋ณต ํ‚ค์›Œ๋“œ ๋Œ€์ฒด: '{existing_ui_keyword}' ({existing_total}) -> '{ui_keyword}' ({total_count})")
209
+ normalized_keywords[normalized] = (api_keyword, ui_keyword, total_count)
210
+ else:
211
+ logger.debug(f" - ์ค‘๋ณต ํ‚ค์›Œ๋“œ ์ œ์™ธ: '{ui_keyword}' ({total_count}) < '{existing_ui_keyword}' ({existing_total})")
212
+ else:
213
+ normalized_keywords[normalized] = (api_keyword, ui_keyword, total_count)
214
+ logger.debug(f" - ํ‚ค์›Œ๋“œ ์ถ”๊ฐ€: '{ui_keyword}' (๊ฒ€์ƒ‰๋Ÿ‰: {total_count})")
215
+
216
+ logger.info(f"\n์ค‘๋ณต ์ œ๊ฑฐ ํ›„ ํ‚ค์›Œ๋“œ ์ˆ˜: {len(normalized_keywords)}๊ฐœ")
217
+
218
+ # ์ค‘๋ณต์ด ์ œ๊ฑฐ๋œ ํ‚ค์›Œ๋“œ๋งŒ ์ฒ˜๋ฆฌ
219
+ final_combos = []
220
+ for normalized, (api_keyword, ui_keyword, total_count) in normalized_keywords.items():
221
+
222
+ # ํ‚ค์›Œ๋“œ ๊ฐ€๋…์„ฑ ๊ฐœ์„  - fix_keyword_order ํ•จ์ˆ˜ ์ ์šฉ
223
+ readable = fix_keyword_order(ui_keyword, current_keyword)
224
+
225
+ # ๊ฒ€์ƒ‰๋Ÿ‰ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ
226
+ pc_count = 0
227
+ mobile_count = 0
228
+ if api_keyword in search_volumes:
229
+ pc_count = search_volumes[api_keyword]["PC๊ฒ€์ƒ‰๋Ÿ‰"]
230
+ mobile_count = search_volumes[api_keyword]["๋ชจ๋ฐ”์ผ๊ฒ€์ƒ‰๋Ÿ‰"]
231
+ total_count = search_volumes[api_keyword]["์ด๊ฒ€์ƒ‰๋Ÿ‰"]
232
+
233
+ # ๊ฒ€์ƒ‰๋Ÿ‰ ๊ตฌ๊ฐ„ ๊ณ„์‚ฐ
234
+ search_volume_range = text_utils.get_search_volume_range(total_count)
235
+
236
+ # ๋“ฑ์žฅ ์ˆœ์œ„ ๋ฐ ํšŸ์ˆ˜ ๊ณ„์‚ฐ
237
+ base_word = readable.replace(current_keyword, "").strip() if current_keyword else readable
238
+ ranks = []
239
+ if base_word in keyword_indices:
240
+ ranks = [idx + 1 for idx in keyword_indices[base_word]]
241
+ elif api_keyword in keyword_indices: # ๋ฉ”์ธ ํ‚ค์›Œ๋“œ๊ฐ€ ํฌํ•จ๋œ ๋‹จ์–ด์ธ ๊ฒฝ์šฐ
242
+ ranks = [idx + 1 for idx in keyword_indices.get(api_keyword, [])]
243
+
244
+ ranks_str = ", ".join(map(str, ranks)) if ranks else "-"
245
+ usage_count = len(ranks)
246
+
247
+ # === ์ˆ˜์ •๋œ ๋ถ€๋ถ„: "์ƒํ’ˆ ๋“ฑ๋ก ์นดํ…Œ๊ณ ๋ฆฌ(์ƒ์œ„100์œ„)" ํ•ญ๋ชฉ ์ œ๊ฑฐ ===
248
+ # ์นดํ…Œ๊ณ ๋ฆฌ ์ •๋ณด๋Š” ๋‚ด๋ถ€์ ์œผ๋กœ๋งŒ ์‚ฌ์šฉํ•˜๊ณ  ํ…Œ์ด๋ธ”์—๋Š” ํ‘œ์‹œํ•˜์ง€ ์•Š์Œ
249
+
250
+ final_combos.append({
251
+ "์กฐํ•ฉ ํ‚ค์›Œ๋“œ": readable.strip(),
252
+ "PC๊ฒ€์ƒ‰๋Ÿ‰": pc_count,
253
+ "๋ชจ๋ฐ”์ผ๊ฒ€์ƒ‰๋Ÿ‰": mobile_count,
254
+ "์ด๊ฒ€์ƒ‰๋Ÿ‰": total_count,
255
+ "๊ฒ€์ƒ‰๋Ÿ‰๊ตฌ๊ฐ„": search_volume_range,
256
+ "ํ‚ค์›Œ๋“œ ์‚ฌ์šฉ์ž์ˆœ์œ„": ranks_str,
257
+ "ํ‚ค์›Œ๋“œ ์‚ฌ์šฉํšŸ์ˆ˜": usage_count
258
+ # "์ƒํ’ˆ ๋“ฑ๋ก ์นดํ…Œ๊ณ ๋ฆฌ(์ƒ์œ„100์œ„)" ํ•ญ๋ชฉ ์ œ๊ฑฐ๋จ
259
+ })
260
+
261
+ # ํ‚ค์›Œ๋“œ ์ •๋ณด ๋ฐ์ดํ„ฐํ”„๋ ˆ์ž„ ์ƒ์„ฑ
262
+ df_keywords = pd.DataFrame(final_combos)
263
+
264
+ # ๊ฒ€์ƒ‰๋Ÿ‰ ๊ธฐ์ค€์œผ๋กœ ๋‚ด๋ฆผ์ฐจ์ˆœ ์ •๋ ฌ
265
+ if not df_keywords.empty:
266
+ df_keywords = df_keywords.sort_values(by="์ด๊ฒ€์ƒ‰๋Ÿ‰", ascending=False)
267
+ # ์ˆœ๋ฒˆ์„ ์œ„ํ•ด ์ธ๋ฑ์Šค ๋ฆฌ์…‹ (์ˆœ์ฐจ์  ์ˆœ๋ฒˆ ๋ณด์žฅ)
268
+ df_keywords = df_keywords.reset_index(drop=True)
269
+
270
+ # ๋ฐ์ดํ„ฐํ”„๋ ˆ์ž„ ์ƒ์„ฑ ํ›„ ๋กœ๊น…
271
+ logger.info(f"\n์ƒ์„ฑ๋œ ํ‚ค์›Œ๋“œ ๋ฐ์ดํ„ฐํ”„๋ ˆ์ž„ ํ–‰ ์ˆ˜: {len(df_keywords)}")
272
+ if not df_keywords.empty:
273
+ logger.debug(f"๋ฐ์ดํ„ฐํ”„๋ ˆ์ž„ ์—ด: {df_keywords.columns.tolist()}")
274
+ logger.info(f"์ด {len(df_keywords)}๊ฐœ ํ‚ค์›Œ๋“œ ์ƒ์„ฑ ์™„๋ฃŒ")
275
+
276
+ # ์นดํ…Œ๊ณ ๋ฆฌ ์ •๋ณด ๊ฐ€๊ณต
277
+ category_with_counts = [f"{cat} ({category_counter[cat]})" for cat in sorted(category_counter.keys())]
278
+ category_with_counts.insert(0, "์ „์ฒด ๋ณด๊ธฐ")
279
+
280
+ logger.info(f"์นดํ…Œ๊ณ ๋ฆฌ ์ˆ˜: {len(category_counter)}๊ฐœ")
281
+ logger.info("===== ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์ฒ˜๋ฆฌ ์™„๋ฃŒ =====\n")
282
+
283
+ return {
284
+ "products_df": df_products,
285
+ "keywords_df": df_keywords,
286
+ "categories": category_with_counts,
287
+ "message": "โœ… ๊ฒ€์ƒ‰์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์•„๋ž˜์—์„œ ํ‚ค์›Œ๋“œ๋ฅผ ํ™•์ธํ•˜์„ธ์š”."
288
+ }
289
+
290
+ def filter_and_sort_table(df, selected_cat, keyword_sort, total_volume_sort, usage_count_sort, selected_volume_range, exclude_zero_volume=False):
291
+ """ํ…Œ์ด๋ธ” ํ•„ํ„ฐ๋ง ๋ฐ ์ •๋ ฌ ํ•จ์ˆ˜ (๊ฒ€์ƒ‰๋Ÿ‰ 0 ์ œ์™ธ ๊ธฐ๋Šฅ ์ถ”๊ฐ€)"""
292
+ if df is None or df.empty:
293
+ return ""
294
+
295
+ # ํ•„ํ„ฐ๋ง ์ ์šฉ
296
+ filtered = df.copy()
297
+
298
+ # ์นดํ…Œ๊ณ ๋ฆฌ ํ•„ํ„ฐ ์ ์šฉ (์นดํ…Œ๊ณ ๋ฆฌ ์—ด์ด ์ œ๊ฑฐ๋˜์—ˆ์œผ๋ฏ€๋กœ ์ฃผ์„ ์ฒ˜๋ฆฌ)
299
+ # if selected_cat and selected_cat != "์ „์ฒด ๋ณด๊ธฐ":
300
+ # cat_name = selected_cat.rsplit(" (", 1)[0]
301
+ # filtered = filtered[filtered["๊ด€๋ จ ์นดํ…Œ๊ณ ๋ฆฌ"].str.contains(cat_name)]
302
+
303
+ # ๊ฒ€์ƒ‰๋Ÿ‰ ๊ตฌ๊ฐ„ ํ•„ํ„ฐ ์ ์šฉ
304
+ if selected_volume_range and selected_volume_range != "์ „์ฒด":
305
+ filtered = filtered[filtered["๊ฒ€์ƒ‰๋Ÿ‰๊ตฌ๊ฐ„"] == selected_volume_range]
306
+
307
+ # ๊ฒ€์ƒ‰๋Ÿ‰ 0 ์ œ์™ธ ํ•„ํ„ฐ ์ ์šฉ
308
+ if exclude_zero_volume:
309
+ filtered = filtered[filtered["์ด๊ฒ€์ƒ‰๋Ÿ‰"] > 0]
310
+ logger.info(f"๊ฒ€์ƒ‰๋Ÿ‰ 0 ์ œ์™ธ ํ•„ํ„ฐ ์ ์šฉ - ๋‚จ์€ ํ‚ค์›Œ๋“œ ์ˆ˜: {len(filtered)}")
311
+
312
+ # ์ •๋ ฌ ์ ์šฉ
313
+ if keyword_sort != "์ •๋ ฌ ์—†์Œ":
314
+ is_ascending = keyword_sort == "์˜ค๋ฆ„์ฐจ์ˆœ"
315
+ filtered = filtered.sort_values(by="์กฐํ•ฉ ํ‚ค์›Œ๋“œ", ascending=is_ascending)
316
+
317
+ if total_volume_sort != "์ •๋ ฌ ์—†์Œ":
318
+ is_ascending = total_volume_sort == "์˜ค๋ฆ„์ฐจ์ˆœ"
319
+ filtered = filtered.sort_values(by="์ด๊ฒ€์ƒ‰๋Ÿ‰", ascending=is_ascending)
320
+
321
+ # ํ‚ค์›Œ๋“œ ์‚ฌ์šฉํšŸ์ˆ˜ ์ •๏ฟฝ๏ฟฝ๏ฟฝ ์ ์šฉ
322
+ if usage_count_sort != "์ •๋ ฌ ์—†์Œ":
323
+ is_ascending = usage_count_sort == "์˜ค๋ฆ„์ฐจ์ˆœ"
324
+ filtered = filtered.sort_values(by="ํ‚ค์›Œ๋“œ ์‚ฌ์šฉํšŸ์ˆ˜", ascending=is_ascending)
325
+
326
+ # ๋ฐ์ดํ„ฐํ”„๋ ˆ์ž„ ๋‚ด์šฉ ๋กœ๊น…
327
+ logger.info(f"ํ•„ํ„ฐ ์ ์šฉ ํ›„ - ํ•„ํ„ฐ๋ง๋œ DataFrame ํ–‰ ์ˆ˜: {len(filtered)}")
328
+
329
+ # ์ˆœ๋ฒˆ์„ 1๋ถ€ํ„ฐ ์ˆœ์ฐจ์ ์œผ๋กœ ์œ ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ํ–‰ ์ธ๋ฑ์Šค ์žฌ์„ค์ •
330
+ filtered = filtered.reset_index(drop=True)
331
+
332
+ from export_utils import create_table_without_checkboxes
333
+
334
+ # ์ˆœ๋ฒˆ์„ ํฌํ•จํ•œ HTML ํ…Œ์ด๋ธ” ์ƒ์„ฑ
335
+ html = create_table_without_checkboxes(filtered)
336
+
337
+ return html
338
+
339
+ def fix_keyword_order(keyword, main_keyword):
340
+ """
341
+ ํ‚ค์›Œ๋“œ ์ˆœ์„œ๋ฅผ ์ˆ˜์ •ํ•˜๋Š” ํ•จ์ˆ˜ - ํ•œ๊ธ€์ด ์•ž์— ์˜ค๊ณ  ์˜์–ด/์ˆซ์ž๊ฐ€ ๋’ค์— ์˜ค๋„๋ก ํ•จ
342
+
343
+ Args:
344
+ keyword (str): ์ˆ˜์ •ํ•  ํ‚ค์›Œ๋“œ
345
+ main_keyword (str): ๋ฉ”์ธ ํ‚ค์›Œ๋“œ
346
+
347
+ Returns:
348
+ str: ์ˆœ์„œ๊ฐ€ ์ˆ˜์ •๋œ ํ‚ค์›Œ๋“œ
349
+ """
350
+ # ๊ณต๋ฐฑ ์—†์ด ์ˆซ์ž+์˜์–ด์™€ ํ•œ๊ธ€์ด ๋ถ™์–ด์žˆ๋Š” ํŒจํ„ด ์ฒ˜๋ฆฌ
351
+ # ์˜ˆ: "300g์˜ค์ง•์–ด" โ†’ "์˜ค์ง•์–ด 300g"
352
+ pattern_combined = re.compile(r'^([0-9]+[a-zA-Z]*)([๊ฐ€-ํžฃ]+.*)$')
353
+ match = pattern_combined.match(keyword)
354
+ if match:
355
+ number_part = match.group(1) # ์ˆซ์ž+์˜์–ด ๋ถ€๋ถ„
356
+ korean_part = match.group(2) # ํ•œ๊ธ€ ๋ถ€๋ถ„
357
+ fixed_keyword = f"{korean_part} {number_part}"
358
+ logger.debug(f"๋ถ™์–ด์žˆ๋Š” ํŒจํ„ด ์ˆ˜์ •: '{keyword}' -> '{fixed_keyword}'")
359
+ return fixed_keyword
360
+
361
+ # ๊ณต๋ฐฑ์œผ๋กœ ๋ถ„๋ฆฌ๋œ ๊ฒฝ์šฐ ์ฒ˜๋ฆฌ
362
+ if ' ' in keyword:
363
+ parts = keyword.split()
364
+
365
+ # ํ•œ๊ธ€ ํฌํ•จ ์—ฌ๋ถ€์™€ ์˜์–ด/์ˆซ์ž ํฌํ•จ ์—ฌ๋ถ€๋ฅผ ๊ฐ ๋ถ€๋ถ„๋ณ„๋กœ ํ™•์ธ
366
+ korean_parts = []
367
+ non_korean_parts = []
368
+
369
+ for part in parts:
370
+ if re.search(r'[๊ฐ€-ํžฃ]', part):
371
+ korean_parts.append(part) # ํ•œ๊ธ€์ด ํฌํ•จ๋œ ๋ถ€๋ถ„
372
+ else:
373
+ non_korean_parts.append(part) # ํ•œ๊ธ€์ด ์—†๋Š” ๋ถ€๋ถ„ (์˜์–ด, ์ˆซ์ž, ๊ธฐํ˜ธ ๋“ฑ)
374
+
375
+ # ํ•œ๊ธ€ ๋ถ€๋ถ„์ด ํ•˜๋‚˜๋„ ์—†๊ฑฐ๋‚˜ ๋น„ํ•œ๊ธ€ ๋ถ€๋ถ„์ด ํ•˜๋‚˜๋„ ์—†์œผ๋ฉด ๊ทธ๋Œ€๋กœ ๋ฐ˜ํ™˜
376
+ if not korean_parts or not non_korean_parts:
377
+ return keyword
378
+
379
+ # ํ•œ๊ธ€ ๋ถ€๋ถ„์„ ์•ž์œผ๋กœ, ๋น„ํ•œ๊ธ€ ๋ถ€๋ถ„์„ ๋’ค๋กœ ๋ฐฐ์น˜
380
+ fixed_keyword = " ".join(korean_parts + non_korean_parts)
381
+
382
+ # ์›๋ž˜ ํ‚ค์›Œ๋“œ์™€ ๋‹ค๋ฅธ ๊ฒฝ์šฐ์—๋งŒ ๋กœ๊ทธ ์ถœ๋ ฅ
383
+ if fixed_keyword != keyword:
384
+ logger.debug(f"ํ‚ค์›Œ๋“œ ์ˆœ์„œ ์ˆ˜์ •: '{keyword}' -> '{fixed_keyword}'")
385
+
386
+ return fixed_keyword
387
+
388
+ return keyword
keyword_search.py ADDED
@@ -0,0 +1,194 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ํ‚ค์›Œ๋“œ ๊ฒ€์ƒ‰๋Ÿ‰ ์กฐํšŒ ๊ด€๋ จ ๊ธฐ๋Šฅ
3
+ - ๋„ค์ด๋ฒ„ API๋ฅผ ํ†ตํ•œ ํ‚ค์›Œ๋“œ ๊ฒ€์ƒ‰๋Ÿ‰ ์กฐํšŒ
4
+ - ๊ฒ€์ƒ‰๋Ÿ‰ ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ
5
+ """
6
+
7
+ import requests
8
+ import time
9
+ import random
10
+ from concurrent.futures import ThreadPoolExecutor, as_completed
11
+ import api_utils
12
+ import logging
13
+
14
+ # ๋กœ๊น… ์„ค์ •
15
+ logger = logging.getLogger(__name__)
16
+ logger.setLevel(logging.INFO)
17
+ formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
18
+ handler = logging.StreamHandler()
19
+ handler.setFormatter(formatter)
20
+ logger.addHandler(handler)
21
+
22
+ def exponential_backoff_sleep(retry_count, base_delay=0.3, max_delay=5.0):
23
+ """์ง€์ˆ˜ ๋ฐฑ์˜คํ”„ ๋ฐฉ์‹์˜ ๋Œ€๊ธฐ ์‹œ๊ฐ„ ๊ณ„์‚ฐ"""
24
+ delay = min(base_delay * (2 ** retry_count), max_delay)
25
+ # ์•ฝ๊ฐ„์˜ ๋žœ๋ค์„ฑ ์ถ”๊ฐ€ (์ง€ํ„ฐ)
26
+ jitter = random.uniform(0, 0.5) * delay
27
+ time.sleep(delay + jitter)
28
+
29
+ def fetch_search_volume_batch(keywords_batch):
30
+ """ํ‚ค์›Œ๋“œ ๋ฐฐ์น˜์— ๋Œ€ํ•œ ๋„ค์ด๋ฒ„ ๊ฒ€์ƒ‰๋Ÿ‰ ์กฐํšŒ"""
31
+
32
+ # 1. ์ŠคํŽ˜์ด์Šค๋ฐ” ์ œ๊ฑฐ ๊ฐœ์„  - ๋ฐฐ์น˜ ํ‚ค์›Œ๋“œ๋“ค ์ „์ฒ˜๋ฆฌ
33
+ cleaned_keywords_batch = []
34
+ for kw in keywords_batch:
35
+ cleaned_kw = kw.strip().replace(" ", "") if kw else ""
36
+ cleaned_keywords_batch.append(cleaned_kw)
37
+
38
+ keywords_batch = cleaned_keywords_batch
39
+
40
+ result = {}
41
+ max_retries = 3
42
+ retry_count = 0
43
+
44
+ while retry_count < max_retries:
45
+ try:
46
+ # ์ˆœ์ฐจ์ ์œผ๋กœ API ์„ค์ • ๊ฐ€์ ธ์˜ค๊ธฐ (๋ฐฐ์น˜๋งˆ๋‹ค ํ•œ ๋ฒˆ๋งŒ ํ˜ธ์ถœ)
47
+ api_config = api_utils.get_next_api_config()
48
+ API_KEY = api_config["API_KEY"]
49
+ SECRET_KEY = api_config["SECRET_KEY"]
50
+ CUSTOMER_ID_STR = api_config["CUSTOMER_ID"]
51
+
52
+ logger.debug(f"=== ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์ฒดํฌ (์‹œ๋„ #{retry_count+1}) ===")
53
+ logger.info(f"๋ฐฐ์น˜ ํฌ๊ธฐ: {len(keywords_batch)}๊ฐœ ํ‚ค์›Œ๋“œ")
54
+
55
+ # API ์„ค์ • ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ
56
+ is_valid, message = api_utils.validate_api_config(api_config)
57
+ if not is_valid:
58
+ logger.error(f"โŒ {message}")
59
+ retry_count += 1
60
+ exponential_backoff_sleep(retry_count)
61
+ continue
62
+
63
+ # CUSTOMER_ID๋ฅผ ์ •์ˆ˜๋กœ ๋ณ€ํ™˜
64
+ try:
65
+ CUSTOMER_ID = int(CUSTOMER_ID_STR)
66
+ except ValueError:
67
+ logger.error(f"โŒ CUSTOMER_ID ๋ณ€ํ™˜ ์˜ค๋ฅ˜: '{CUSTOMER_ID_STR}'๋Š” ์œ ํšจํ•œ ์ˆซ์ž๊ฐ€ ์•„๋‹™๋‹ˆ๋‹ค.")
68
+ retry_count += 1
69
+ exponential_backoff_sleep(retry_count)
70
+ continue
71
+
72
+ BASE_URL = "https://api.naver.com"
73
+ uri = "/keywordstool"
74
+ method = "GET"
75
+ headers = api_utils.get_header(method, uri, API_KEY, SECRET_KEY, CUSTOMER_ID)
76
+
77
+ # ํ‚ค์›Œ๋“œ ๋ฐฐ์น˜๋ฅผ ํ•œ ๋ฒˆ์— API๋กœ ์ „์†ก
78
+ params = {
79
+ "hintKeywords": keywords_batch,
80
+ "showDetail": "1"
81
+ }
82
+
83
+ logger.debug(f"์š”์ฒญ ํŒŒ๋ผ๋ฏธํ„ฐ: {len(keywords_batch)}๊ฐœ ํ‚ค์›Œ๋“œ")
84
+
85
+ # API ํ˜ธ์ถœ
86
+ response = requests.get(BASE_URL + uri, params=params, headers=headers, timeout=10)
87
+
88
+ logger.debug(f"์‘๋‹ต ์ƒํƒœ ์ฝ”๋“œ: {response.status_code}")
89
+
90
+ if response.status_code != 200:
91
+ logger.error(f"โŒ API ์˜ค๋ฅ˜ ์‘๋‹ต (์‹œ๋„ #{retry_count+1}):")
92
+ logger.error(f" ๋ณธ๋ฌธ: {response.text}")
93
+ retry_count += 1
94
+ exponential_backoff_sleep(retry_count)
95
+ continue
96
+
97
+ # ์‘๋‹ต ๋ฐ์ดํ„ฐ ํŒŒ์‹ฑ
98
+ result_data = response.json()
99
+
100
+ logger.debug(f"์‘๋‹ต ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ:")
101
+ logger.debug(f" ํƒ€์ž…: {type(result_data)}")
102
+ logger.debug(f" ํ‚ค๋“ค: {result_data.keys() if isinstance(result_data, dict) else 'N/A'}")
103
+
104
+ if isinstance(result_data, dict) and "keywordList" in result_data:
105
+ logger.debug(f" keywordList ๊ธธ์ด: {len(result_data['keywordList'])}")
106
+
107
+ # ๋ฐฐ์น˜ ๋‚ด ๊ฐ ํ‚ค์›Œ๋“œ์™€ ๋งค์นญ
108
+ for keyword in keywords_batch:
109
+ found = False
110
+ for item in result_data["keywordList"]:
111
+ rel_keyword = item.get("relKeyword", "")
112
+ if rel_keyword == keyword:
113
+ pc_count = item.get("monthlyPcQcCnt", 0)
114
+ mobile_count = item.get("monthlyMobileQcCnt", 0)
115
+
116
+ # ์ˆซ์ž ๋ณ€ํ™˜
117
+ try:
118
+ if isinstance(pc_count, str):
119
+ pc_count_converted = int(pc_count.replace(",", ""))
120
+ else:
121
+ pc_count_converted = int(pc_count)
122
+ except:
123
+ pc_count_converted = 0
124
+
125
+ try:
126
+ if isinstance(mobile_count, str):
127
+ mobile_count_converted = int(mobile_count.replace(",", ""))
128
+ else:
129
+ mobile_count_converted = int(mobile_count)
130
+ except:
131
+ mobile_count_converted = 0
132
+
133
+ total_count = pc_count_converted + mobile_count_converted
134
+
135
+ result[keyword] = {
136
+ "PC๊ฒ€์ƒ‰๋Ÿ‰": pc_count_converted,
137
+ "๋ชจ๋ฐ”์ผ๊ฒ€์ƒ‰๋Ÿ‰": mobile_count_converted,
138
+ "์ด๊ฒ€์ƒ‰๋Ÿ‰": total_count
139
+ }
140
+ logger.debug(f"โœ… '{keyword}': PC={pc_count_converted}, Mobile={mobile_count_converted}, Total={total_count}")
141
+ found = True
142
+ break
143
+
144
+ if not found:
145
+ logger.warning(f"โŒ '{keyword}': ๋งค์นญ๋˜๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Œ")
146
+
147
+ # ์„ฑ๊ณต์ ์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์™”์œผ๋ฏ€๋กœ ๋ฃจํ”„ ์ข…๋ฃŒ
148
+ break
149
+ else:
150
+ logger.error(f"โŒ keywordList๊ฐ€ ์—†์Œ (์‹œ๋„ #{retry_count+1})")
151
+ logger.error(f"์ „์ฒด ์‘๋‹ต: {result_data}")
152
+ retry_count += 1
153
+ exponential_backoff_sleep(retry_count)
154
+
155
+ except Exception as e:
156
+ logger.error(f"โŒ ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜ (์‹œ๋„ #{retry_count+1}): {str(e)}")
157
+ import traceback
158
+ logger.error(traceback.format_exc())
159
+ retry_count += 1
160
+ exponential_backoff_sleep(retry_count)
161
+
162
+ logger.info(f"\n=== ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ์™„๋ฃŒ ===")
163
+ logger.info(f"์„ฑ๊ณต์ ์œผ๋กœ ์ฒ˜๋ฆฌ๋œ ํ‚ค์›Œ๋“œ ์ˆ˜: {len(result)}")
164
+
165
+ return result
166
+
167
+ def fetch_all_search_volumes(keywords, batch_size=5):
168
+ """ํ‚ค์›Œ๋“œ ๋ฆฌ์ŠคํŠธ์— ๋Œ€ํ•œ ๋„ค์ด๋ฒ„ ๊ฒ€์ƒ‰๋Ÿ‰ ๋ณ‘๋ ฌ ์กฐํšŒ"""
169
+ results = {}
170
+ batches = []
171
+
172
+ # ํ‚ค์›Œ๋“œ๋ฅผ 5๊ฐœ์”ฉ ๋ฌถ์–ด์„œ ๋ฐฐ์น˜ ์ƒ์„ฑ
173
+ for i in range(0, len(keywords), batch_size):
174
+ batch = keywords[i:i + batch_size]
175
+ batches.append(batch)
176
+
177
+ logger.info(f"์ด {len(batches)}๊ฐœ ๋ฐฐ์น˜๋กœ {len(keywords)}๊ฐœ ํ‚ค์›Œ๋“œ ์ฒ˜๋ฆฌ ์ค‘โ€ฆ")
178
+ logger.info(f"๋ฐฐ์น˜ ํฌ๊ธฐ: {batch_size}, ๋ณ‘๋ ฌ ์›Œ์ปค: 3๊ฐœ, API ๊ณ„์ •: {len(api_utils.NAVER_API_CONFIGS)}๊ฐœ ์ˆœ์ฐจ ์‚ฌ์šฉ")
179
+
180
+ with ThreadPoolExecutor(max_workers=3) as executor: # ์›Œ์ปค ์ˆ˜ ์ œํ•œ
181
+ futures = {executor.submit(fetch_search_volume_batch, batch): batch for batch in batches}
182
+ for future in as_completed(futures):
183
+ batch = futures[future]
184
+ try:
185
+ batch_results = future.result()
186
+ results.update(batch_results)
187
+ logger.info(f"๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ์™„๋ฃŒ: {len(batch)}๊ฐœ ํ‚ค์›Œ๋“œ (์„ฑ๊ณต: {len(batch_results)}๊ฐœ)")
188
+ except Exception as e:
189
+ logger.error(f"๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ์˜ค๋ฅ˜: {e}")
190
+ # API ๋ ˆ์ดํŠธ ๋ฆฌ๋ฐ‹ ๋ฐฉ์ง€๋ฅผ ์œ„ํ•œ ์ง€์ˆ˜ ๋ฐฑ์˜คํ”„ ์‚ฌ์šฉ
191
+ exponential_backoff_sleep(0) # ์ดˆ๊ธฐ ์ง€์—ฐ ์ ์šฉ
192
+
193
+ logger.info(f"๊ฒ€์ƒ‰๋Ÿ‰ ์กฐํšŒ ์™„๋ฃŒ: {len(results)}๊ฐœ ํ‚ค์›Œ๋“œ")
194
+ return results
product_search.py ADDED
@@ -0,0 +1,430 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ์ƒํ’ˆ ๊ฒ€์ƒ‰ ๊ด€๋ จ ๊ธฐ๋Šฅ - ๋ฉ”์ธํ‚ค์›Œ๋“œ ์•ž๋’ค ์กฐํ•ฉ๋งŒ ์ƒ์„ฑํ•˜๋„๋ก ์ˆ˜์ •
3
+ - ๋„ค์ด๋ฒ„ ์‡ผํ•‘ API๋ฅผ ํ†ตํ•œ ์ƒํ’ˆ ๊ฒ€์ƒ‰
4
+ - ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์ฒ˜๋ฆฌ
5
+ """
6
+
7
+ import requests
8
+ import time
9
+ import re
10
+ import random
11
+ from collections import defaultdict, Counter
12
+ import api_utils
13
+ import text_utils
14
+ import logging
15
+
16
+ # ๋กœ๊น… ์„ค์ •
17
+ logger = logging.getLogger(__name__)
18
+ logger.setLevel(logging.INFO)
19
+ formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
20
+ handler = logging.StreamHandler()
21
+ handler.setFormatter(formatter)
22
+ logger.addHandler(handler)
23
+
24
+ # ๋ชจ๋“ˆ ๋ ˆ๋ฒจ์—์„œ Gemini ๋ชจ๋ธ ์ดˆ๊ธฐํ™” (์‹ฑ๊ธ€ํ†ค ํŒจํ„ด)
25
+ gemini_model = None
26
+ try:
27
+ gemini_model = text_utils.get_gemini_model()
28
+ logger.info("Gemini ๋ชจ๋ธ ์ดˆ๊ธฐํ™” ์„ฑ๊ณต")
29
+ except Exception as e:
30
+ logger.error(f"Gemini ๋ชจ๋ธ ์ดˆ๊ธฐํ™” ์‹คํŒจ: {e}")
31
+
32
+ def exponential_backoff_sleep(retry_count, base_delay=0.3, max_delay=5.0):
33
+ """์ง€์ˆ˜ ๋ฐฑ์˜คํ”„ ๋ฐฉ์‹์˜ ๋Œ€๊ธฐ ์‹œ๊ฐ„ ๊ณ„์‚ฐ"""
34
+ delay = min(base_delay * (2 ** retry_count), max_delay)
35
+ # ์•ฝ๊ฐ„์˜ ๋žœ๋ค์„ฑ ์ถ”๊ฐ€ (์ง€ํ„ฐ)
36
+ jitter = random.uniform(0, 0.5) * delay
37
+ time.sleep(delay + jitter)
38
+
39
+ def extract_keywords_with_dedup(titles):
40
+ """
41
+ ์ƒํ’ˆ๋ช…์—์„œ ํ‚ค์›Œ๋“œ๋ฅผ ์ถ”์ถœํ•˜๊ณ  ์ฆ‰์‹œ ์ค‘๋ณต์„ ์ œ๊ฑฐํ•˜๋Š” ํ•จ์ˆ˜
42
+
43
+ Args:
44
+ titles (list): ์ƒํ’ˆ๋ช… ๋ชฉ๋ก
45
+
46
+ Returns:
47
+ list: ์ค‘๋ณต์ด ์ œ๊ฑฐ๋œ ํ‚ค์›Œ๋“œ ๋ชฉ๋ก
48
+ """
49
+ all_words = []
50
+
51
+ for title in titles:
52
+ # ์ƒํ’ˆ๋ช…์—์„œ ํ‚ค์›Œ๋“œ ์ถ”์ถœ (๊ณต๋ฐฑ๊ณผ ์‰ผํ‘œ ๊ธฐ์ค€)
53
+ words = re.split(r'[,\s]+', title)
54
+ all_words.extend(words)
55
+
56
+ # ๋นˆ ๋ฌธ์ž์—ด ์ œ๊ฑฐ ๋ฐ ์ •๋ฆฌ
57
+ all_words = [word.strip() for word in all_words if word.strip() and len(word.strip()) >= 2]
58
+
59
+ # ์ค‘๋ณต ์ œ๊ฑฐ - ์ •๊ทœํ™” ๊ธฐ๋ฐ˜ (์ŠคํŽ˜์ด์Šค ์ฐจ์ด ๋ฌด์‹œ)
60
+ normalized_dict = {}
61
+
62
+ for word in all_words:
63
+ # ์ •๊ทœํ™”: ์†Œ๋ฌธ์ž ๋ณ€ํ™˜, ๊ณต๋ฐฑ ์ œ๊ฑฐ
64
+ normalized = word.lower().replace(" ", "")
65
+
66
+ # ์ด๋ฏธ ์žˆ๋Š” ๊ฒฝ์šฐ, ๊ฐ€๋…์„ฑ์ด ์ข‹์€ ๋ฒ„์ „(๊ณต๋ฐฑ ์žˆ๋Š”) ์„ ํƒ
67
+ if normalized in normalized_dict:
68
+ existing = normalized_dict[normalized]
69
+ # ๊ณต๋ฐฑ์ด ์žˆ๋Š” ๋ฒ„์ „ ์„ ํ˜ธ
70
+ if " " in word and " " not in existing:
71
+ normalized_dict[normalized] = word
72
+ else:
73
+ normalized_dict[normalized] = word
74
+
75
+ # ์ •๊ทœํ™”๋œ ๋”•์…”๋„ˆ๋ฆฌ์—์„œ ์›๋ณธ ํ˜•ํƒœ ์ถ”์ถœ
76
+ return list(normalized_dict.values())
77
+
78
+ def fetch_naver_shopping_data(keyword, korean_only=True, apply_main_keyword=True, exclude_zero_volume=True):
79
+ """
80
+ ๋„ค์ด๋ฒ„ ์‡ผํ•‘ API๋ฅผ ํ†ตํ•ด ์ƒํ’ˆ ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ - ๋ฉ”์ธํ‚ค์›Œ๋“œ ์•ž๋’ค ์กฐํ•ฉ๋งŒ ์ƒ์„ฑ
81
+
82
+ Args:
83
+ keyword (str): ๊ฒ€์ƒ‰ ํ‚ค์›Œ๋“œ
84
+ korean_only (bool): ํ•œ๊ธ€๋งŒ ์ถ”์ถœ ์—ฌ๋ถ€
85
+ apply_main_keyword (bool): ๋ฉ”์ธํ‚ค์›Œ๋“œ ์ ์šฉ ์—ฌ๋ถ€
86
+ exclude_zero_volume (bool): ๊ฒ€์ƒ‰๋Ÿ‰์ด 0์ธ ํ‚ค์›Œ๋“œ ์ œ์™ธ ์—ฌ๋ถ€
87
+
88
+ Returns:
89
+ dict: ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ๋ฐ์ดํ„ฐ
90
+ """
91
+ global gemini_model
92
+
93
+ # 1. ์ŠคํŽ˜์ด์Šค๋ฐ” ์ œ๊ฑฐ ๊ฐœ์„  - ์ž…๋ ฅ ํ‚ค์›Œ๋“œ ์ „์ฒ˜๋ฆฌ
94
+ cleaned_keyword = keyword.strip().replace(" ", "") if keyword else ""
95
+
96
+ # ๋กœ๊ทธ ์ถ”๊ฐ€ - ํ•จ์ˆ˜ ์‹œ์ž‘
97
+ logger.info(f"===== ํ‚ค์›Œ๋“œ ๊ฒ€์ƒ‰ ์‹œ์ž‘ =====")
98
+ logger.info(f"๊ฒ€์ƒ‰ ํ‚ค์›Œ๋“œ: '{keyword}'")
99
+ logger.info(f"์˜ต์…˜: ํ•œ๊ธ€๋งŒ ์ถ”์ถœ={korean_only}, ๋ฉ”์ธํ‚ค์›Œ๋“œ ์ ์šฉ={apply_main_keyword}")
100
+ logger.info(f"Gemini ๋ชจ๋ธ ์ƒํƒœ: {'์‚ฌ์šฉ ๊ฐ€๋Šฅ' if gemini_model else '์‚ฌ์šฉ ๋ถˆ๊ฐ€'}")
101
+
102
+ # ๊ณต๋ฐฑ ์ œ๊ฑฐํ•œ ํ‚ค์›Œ๋“œ๋กœ API ํ˜ธ์ถœ
103
+ api_keyword = cleaned_keyword
104
+ logger.info(f"API ํ˜ธ์ถœ ํ‚ค์›Œ๋“œ: '{api_keyword}'")
105
+
106
+ # ๋„ค์ด๋ฒ„ ์‡ผํ•‘ API ํ˜ธ์ถœ
107
+ all_products = []
108
+ total_count = 0
109
+
110
+ if apply_main_keyword:
111
+ # ๋ฉ”์ธ ํ‚ค์›Œ๋“œ ์ ์šฉ ์‹œ ํ•œ ๋ฒˆ์— 100๊ฐœ ์ƒํ’ˆ ๊ฐ€์ ธ์˜ค๊ธฐ
112
+ logger.info("๋ฉ”์ธ ํ‚ค์›Œ๋“œ ์ ์šฉ - ํ•œ ๋ฒˆ์— 100๊ฐœ ์ƒํ’ˆ ๊ฐ€์ ธ์˜ค๊ธฐ")
113
+ result = fetch_products_by_keyword(api_keyword, page=1, display=100)
114
+ if result["status"] == "success" and result["products"]:
115
+ all_products.extend(result["products"])
116
+ total_count = result.get("total", 0)
117
+ logger.info(f"ํ•œ ๋ฒˆ์— {len(result['products'])}๊ฐœ ์ƒํ’ˆ ๊ฒ€์ƒ‰๋จ")
118
+ else:
119
+ # ๋ฉ”์ธ ํ‚ค์›Œ๋“œ ๋ฏธ์ ์šฉ ์‹œ ํŽ˜์ด์ง€๋‹น 10๊ฐœ์”ฉ ์ตœ๋Œ€ 10ํŽ˜์ด์ง€ ๊ฐ€์ ธ์˜ค๊ธฐ
120
+ logger.info("๋ฉ”์ธ ํ‚ค์›Œ๋“œ ๋ฏธ์ ์šฉ - ํŽ˜์ด์ง€๋‹น 10๊ฐœ์”ฉ ๊ฐ€์ ธ์˜ค๊ธฐ")
121
+ for page in range(1, 11): # 1~10 ํŽ˜์ด์ง€
122
+ result = fetch_products_by_keyword(api_keyword, page=page, display=100)
123
+ if result["status"] == "success" and result["products"]:
124
+ all_products.extend(result["products"])
125
+ total_count = result.get("total", 0)
126
+ logger.info(f"ํŽ˜์ด์ง€ {page}: {len(result['products'])}๊ฐœ ์ƒํ’ˆ ๊ฒ€์ƒ‰๋จ")
127
+ else:
128
+ logger.info(f"ํŽ˜์ด์ง€ {page}: ์ƒํ’ˆ ๊ฒ€์ƒ‰ ์‹คํŒจ ๋˜๋Š” ๊ฒฐ๊ณผ ์—†์Œ")
129
+
130
+ # API ๋ ˆ์ดํŠธ ๋ฆฌ๋ฐ‹ ๋ฐฉ์ง€๋ฅผ ์œ„ํ•œ ์งง์€ ๋Œ€๊ธฐ ์‹œ๊ฐ„
131
+ exponential_backoff_sleep(0) # ์ดˆ๊ธฐ ์ง€์—ฐ
132
+
133
+ # ์ด๋ฏธ ์ถฉ๋ถ„ํ•œ ์ƒํ’ˆ์„ ๊ฐ€์ ธ์™”๊ฑฐ๋‚˜ ๋” ์ด์ƒ ๊ฒฐ๊ณผ๊ฐ€ ์—†์œผ๋ฉด ์ค‘๋‹จ
134
+ if len(all_products) >= 100 or (result["status"] == "success" and len(result["products"]) < 10):
135
+ break
136
+
137
+ if not all_products:
138
+ logger.warning("๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
139
+ return {
140
+ "product_list": [],
141
+ "combo_candidates": {},
142
+ "category_counter": {},
143
+ "keyword_indices": {},
144
+ "keyword_pairs": {}
145
+ }
146
+
147
+ logger.info(f"๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ: ์ด {len(all_products)}๊ฐœ์˜ ์ƒํ’ˆ์„ ์ฐพ์•˜์Šต๋‹ˆ๋‹ค. (์ „์ฒด ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ: {total_count}๊ฐœ)")
148
+
149
+ # ์ƒํ’ˆ ๋ฐ์ดํ„ฐ์—์„œ ํ‚ค์›Œ๋“œ ์ถ”์ถœ
150
+ product_list = []
151
+ combo_candidates = {}
152
+ category_counter = {}
153
+ keyword_indices = {}
154
+ keyword_pairs = {} # ์•ž๋’ค ์กฐํ•ฉ ์ •๋ณด ์ €์žฅ
155
+
156
+ all_extracted_keywords = set() # ๋ชจ๋“  ์ถ”์ถœ๋œ ํ‚ค์›Œ๋“œ ์ถ”์ 
157
+
158
+ # ๊ฐ ์ƒํ’ˆ ์ฒ˜๋ฆฌ
159
+ for idx, product in enumerate(all_products):
160
+ # ์ƒํ’ˆ ์ •๋ณด ์ถ”์ถœ
161
+ title = product.get("์ƒํ’ˆ๋ช…", "")
162
+ category = product.get("์นดํ…Œ๊ณ ๋ฆฌ", "")
163
+
164
+ logger.debug(f"\n์ƒํ’ˆ #{idx+1}: '{title}' (์นดํ…Œ๊ณ ๋ฆฌ: {category})")
165
+
166
+ # ์ƒํ’ˆ๋ช…์—์„œ ํ‚ค์›Œ๋“œ ์ถ”์ถœ - 1๊ธ€์ž ์ด์ƒ ๋‹จ์–ด๋„ ํฌํ•จํ•˜๋„๋ก ๋ณ€๊ฒฝ
167
+ keywords = text_utils.clean_and_split(title, only_korean=korean_only)
168
+ logger.debug(f" - ์ถ”์ถœ๋œ ํ‚ค์›Œ๋“œ ({len(keywords)}๊ฐœ): {keywords}")
169
+
170
+ # ๋ชจ๋“  ์ถ”์ถœ๋œ ํ‚ค์›Œ๋“œ ์ถ”์ 
171
+ all_extracted_keywords.update(keywords)
172
+
173
+ # ํ‚ค์›Œ๋“œ ์œ„์น˜ ๊ธฐ๋ก
174
+ for kw in keywords:
175
+ if kw not in keyword_indices:
176
+ keyword_indices[kw] = []
177
+ if idx not in keyword_indices[kw]:
178
+ keyword_indices[kw].append(idx)
179
+
180
+ # ์นดํ…Œ๊ณ ๋ฆฌ ์นด์šดํ„ฐ ์—…๋ฐ์ดํŠธ
181
+ if category not in category_counter:
182
+ category_counter[category] = 0
183
+ category_counter[category] += 1
184
+
185
+ # ์ƒํ’ˆ ์ •๋ณด ์ €์žฅ
186
+ product_list.append(product)
187
+
188
+ logger.info(f"\n์ด ์ถ”์ถœ๋œ ๊ณ ์œ  ํ‚ค์›Œ๋“œ: {len(all_extracted_keywords)}๊ฐœ")
189
+ logger.debug(f"์ถ”์ถœ๋œ ๊ณ ์œ  ํ‚ค์›Œ๋“œ ๋ชฉ๋ก: {sorted(list(all_extracted_keywords))}")
190
+
191
+ # === ์ˆ˜์ •๋œ ๋ถ€๋ถ„: ๋ฉ”์ธํ‚ค์›Œ๋“œ ์•ž๋’ค ์กฐํ•ฉ๋งŒ ์ƒ์„ฑ ===
192
+ if apply_main_keyword:
193
+ logger.info(f"\n๋ฉ”์ธ ํ‚ค์›Œ๋“œ '{keyword}'๋กœ ์•ž๋’ค ์กฐํ•ฉ๋งŒ ์ƒ์„ฑ:")
194
+ combo_count = 0
195
+
196
+ # ๋ฉ”์ธ ํ‚ค์›Œ๋“œ ์ž์ฒด๋„ ์ถ”๊ฐ€
197
+ combo_candidates[keyword] = set([category for category in category_counter.keys()])
198
+ logger.info(f" - ๋ฉ”์ธ ํ‚ค์›Œ๋“œ ์ถ”๊ฐ€: '{keyword}'")
199
+
200
+ # ๋ฉ”์ธ ํ‚ค์›Œ๋“œ์™€ ์•ž๋’ค ์กฐํ•ฉ ์ƒ์„ฑ
201
+ main_keyword = keyword.strip()
202
+
203
+ for kw in all_extracted_keywords:
204
+ if kw == main_keyword:
205
+ continue
206
+
207
+ # ๋ฉ”์ธ ํ‚ค์›Œ๋“œ๊ฐ€ ์ด๋ฏธ ํฌํ•จ๋œ ๊ฒฝ์šฐ๋Š” ๊ทธ๋Œ€๋กœ ์ถ”๊ฐ€
208
+ if main_keyword in kw:
209
+ kw_api = kw.replace(" ", "")
210
+ if kw_api not in combo_candidates:
211
+ combo_candidates[kw_api] = set()
212
+ for cat in category_counter.keys():
213
+ combo_candidates[kw_api].add(cat)
214
+ logger.info(f" - ๋ฉ”์ธ ํ‚ค์›Œ๋“œ ํฌํ•จ ๋‹จ์–ด ์ถ”๊ฐ€: '{kw}' (API: '{kw_api}')")
215
+ combo_count += 1
216
+ continue
217
+
218
+ # ์•ž๋’ค ์กฐํ•ฉ ์ƒ์„ฑ
219
+ front_combo = f"{kw} {main_keyword}" # ํ‚ค์›Œ๋“œ + ๋ฉ”์ธํ‚ค์›Œ๋“œ
220
+ back_combo = f"{main_keyword} {kw}" # ๋ฉ”์ธํ‚ค์›Œ๋“œ + ํ‚ค์›Œ๋“œ
221
+
222
+ # ์•ž๋’ค ์กฐํ•ฉ ์ •๋ณด ์ €์žฅ
223
+ keyword_pairs[kw] = {
224
+ "front": front_combo,
225
+ "back": back_combo
226
+ }
227
+
228
+ # API ํ˜ธ์ถœ์„ ์œ„ํ•œ ๊ณต๋ฐฑ ์—†๋Š” ๋ฒ„์ „
229
+ front_api = front_combo.replace(" ", "")
230
+ back_api = back_combo.replace(" ", "")
231
+
232
+ logger.debug(f" - ์กฐํ•ฉ ์ƒ์„ฑ: '{front_combo}' (API: '{front_api}') / '{back_combo}' (API: '{back_api}')")
233
+
234
+ # ์ฒซ ๋ฒˆ์งธ ์กฐํ•ฉ ์ฒ˜๋ฆฌ
235
+ if front_api not in combo_candidates:
236
+ combo_candidates[front_api] = set()
237
+ for cat in category_counter.keys():
238
+ combo_candidates[front_api].add(cat)
239
+ combo_count += 1
240
+
241
+ # ๋‘ ๋ฒˆ์งธ ์กฐํ•ฉ ์ฒ˜๋ฆฌ
242
+ if back_api not in combo_candidates:
243
+ combo_candidates[back_api] = set()
244
+ for cat in category_counter.keys():
245
+ combo_candidates[back_api].add(cat)
246
+ combo_count += 1
247
+
248
+ logger.info(f"์กฐํ•ฉ ์ƒ์„ฑ ์™„๋ฃŒ: ์ด {combo_count}๊ฐœ ์กฐํ•ฉ ์ƒ์„ฑ")
249
+ else:
250
+ logger.info("\n๋ฉ”์ธ ํ‚ค์›Œ๋“œ ์ ์šฉ ์•ˆํ•จ - ๊ฐœ๋ณ„ ํ‚ค์›Œ๋“œ๋งŒ ์‚ฌ์šฉ")
251
+ for kw in all_extracted_keywords:
252
+ kw_api = kw.replace(" ", "")
253
+ if kw_api not in combo_candidates:
254
+ combo_candidates[kw_api] = set()
255
+ for cat in category_counter.keys():
256
+ combo_candidates[kw_api].add(cat)
257
+
258
+ logger.info(f"\n์ตœ์ข… ์กฐํ•ฉ ํ‚ค์›Œ๋“œ ์ˆ˜: {len(combo_candidates)}๊ฐœ")
259
+ logger.debug(f"์กฐํ•ฉ ํ‚ค์›Œ๋“œ ์ƒ˜ํ”Œ(์ตœ๋Œ€ 10๊ฐœ): {list(combo_candidates.keys())[:10]}")
260
+ logger.info("===== ํ‚ค์›Œ๋“œ ๊ฒ€์ƒ‰ ์™„๋ฃŒ =====\n")
261
+
262
+ return {
263
+ "product_list": product_list,
264
+ "combo_candidates": combo_candidates,
265
+ "category_counter": category_counter,
266
+ "keyword_indices": keyword_indices,
267
+ "keyword_pairs": keyword_pairs # ์•ž๋’ค ์กฐํ•ฉ ์ •๋ณด ์ถ”๊ฐ€
268
+ }
269
+
270
+ def fetch_products_by_keyword(keyword, page=1, display=10, max_retries=3):
271
+ """
272
+ ๋„ค์ด๋ฒ„ ์‡ผํ•‘ API๋ฅผ ํ†ตํ•ด ํ‚ค์›Œ๋“œ๋กœ ์ƒํ’ˆ ๊ฒ€์ƒ‰
273
+
274
+ Args:
275
+ keyword (str): ๊ฒ€์ƒ‰ ํ‚ค์›Œ๋“œ
276
+ page (int): ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ
277
+ display (int): ํ•œ ํŽ˜์ด์ง€์— ํ‘œ์‹œํ•  ์ƒํ’ˆ ์ˆ˜ (์ตœ๋Œ€ 100)
278
+ max_retries (int): ์ตœ๋Œ€ ์žฌ์‹œ๋„ ํšŸ์ˆ˜
279
+
280
+ Returns:
281
+ dict: ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ
282
+ """
283
+ # ์ƒํ’ˆ ์ˆ˜๊ฐ€ 100์„ ์ดˆ๊ณผํ•˜์ง€ ์•Š๋„๋ก ์ œํ•œ
284
+ if display > 100:
285
+ display = 100
286
+ logger.info(f"์ƒํ’ˆ ํ‘œ์‹œ ์ˆ˜ 100์œผ๋กœ ์ œํ•œ๋จ")
287
+
288
+ retry_count = 0
289
+ while retry_count < max_retries:
290
+ try:
291
+ # API ์„ค์ • ๊ฐ€์ ธ์˜ค๊ธฐ
292
+ api_config = api_utils.get_next_shopping_api_config()
293
+
294
+ # ์š”์ฒญ ํ—ค๋”
295
+ headers = {
296
+ 'X-Naver-Client-Id': api_config["CLIENT_ID"],
297
+ 'X-Naver-Client-Secret': api_config["CLIENT_SECRET"]
298
+ }
299
+
300
+ # ์š”์ฒญ URL (์ŠคํŽ˜์ด์Šค ์—†์ด ํ˜ธ์ถœ)
301
+ url = f"https://openapi.naver.com/v1/search/shop.json?query={keyword}&display={display}&start={(page-1)*display+1}"
302
+
303
+ logger.debug(f"API ์š”์ฒญ: page={page}, display={display}, ์‹œ์ž‘ ์œ„์น˜={(page-1)*display+1}")
304
+ response = requests.get(url, headers=headers, timeout=10) # ํƒ€์ž„์•„์›ƒ ์„ค์ •
305
+
306
+ if response.status_code == 200:
307
+ result = response.json()
308
+
309
+ # ์ƒํ’ˆ ์ •๋ณด ์ถ”์ถœ
310
+ products = []
311
+
312
+ for item in result.get("items", []):
313
+ title = item.get("title", "").replace("<b>", "").replace("</b>", "")
314
+ link = item.get("link", "")
315
+ image = item.get("image", "")
316
+ lprice = item.get("lprice", "0")
317
+ mall_name = item.get("mallName", "")
318
+
319
+ # ์นดํ…Œ๊ณ ๋ฆฌ ์ •๋ณด ์ถ”์ถœ
320
+ category1 = item.get("category1", "")
321
+ category2 = item.get("category2", "")
322
+ category3 = item.get("category3", "")
323
+ category4 = item.get("category4", "")
324
+
325
+ # ์ „์ฒด ์นดํ…Œ๊ณ ๋ฆฌ ๊ฒฝ๋กœ ์ƒ์„ฑ
326
+ category_path = " > ".join([c for c in [category1, category2, category3, category4] if c])
327
+
328
+ products.append({
329
+ "์ƒํ’ˆ๋ช…": title,
330
+ "๊ฐ€๊ฒฉ": lprice,
331
+ "์‡ผํ•‘๋ชฐ": mall_name,
332
+ "์นดํ…Œ๊ณ ๋ฆฌ": category_path if category_path else category1
333
+ })
334
+
335
+ logger.info(f"API ์‘๋‹ต: {len(products)}๊ฐœ ์ƒํ’ˆ ๊ฒ€์ƒ‰๋จ (์ „์ฒด ๊ฒฐ๊ณผ์ˆ˜: {result.get('total', 0)})")
336
+ return {
337
+ "status": "success",
338
+ "products": products,
339
+ "total": result.get("total", 0)
340
+ }
341
+ else:
342
+ error_msg = f"API ์˜ค๋ฅ˜: {response.status_code} - {response.text}"
343
+ logger.warning(error_msg)
344
+ retry_count += 1
345
+ exponential_backoff_sleep(retry_count)
346
+
347
+ except Exception as e:
348
+ error_msg = f"์ƒํ’ˆ ๊ฒ€์ƒ‰ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}"
349
+ logger.error(error_msg)
350
+ retry_count += 1
351
+ exponential_backoff_sleep(retry_count)
352
+
353
+ # ์ตœ๋Œ€ ์žฌ์‹œ๋„ ํšŸ์ˆ˜๋ฅผ ์ดˆ๊ณผํ•œ ๊ฒฝ์šฐ
354
+ logger.error(f"์ตœ๋Œ€ ์žฌ์‹œ๋„ ํšŸ์ˆ˜({max_retries})๋ฅผ ์ดˆ๊ณผํ•˜์—ฌ ๋นˆ ๊ฒฐ๊ณผ ๋ฐ˜ํ™˜")
355
+ return {
356
+ "status": "error",
357
+ "message": f"์ตœ๋Œ€ ์žฌ์‹œ๋„ ํšŸ์ˆ˜({max_retries})๋ฅผ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค.",
358
+ "products": []
359
+ }
360
+
361
+ def fetch_naver_shopping_data_for_analysis(keyword, count=10):
362
+ """
363
+ ๋„ค์ด๋ฒ„ ์‡ผํ•‘ API๋ฅผ ํ†ตํ•ด ํ‚ค์›Œ๋“œ๋กœ ์ƒํ’ˆ ๊ฒ€์ƒ‰ (๋ถ„์„์šฉ, ์ŠคํŽ˜์ด์Šค ์—†์ด ํ˜ธ์ถœ)
364
+
365
+ Args:
366
+ keyword (str): ๊ฒ€์ƒ‰ ํ‚ค์›Œ๋“œ
367
+ count (int): ๊ฒ€์ƒ‰ํ•  ์ƒํ’ˆ ์ˆ˜
368
+
369
+ Returns:
370
+ list: ๊ฒ€์ƒ‰๋œ ์ƒํ’ˆ ์ •๏ฟฝ๏ฟฝ ๋ชฉ๋ก
371
+ """
372
+ # ์ŠคํŽ˜์ด์Šค ์ œ๊ฑฐ
373
+ api_keyword = keyword.replace(" ", "")
374
+
375
+ # ์ตœ๋Œ€ ์žฌ์‹œ๋„ ํšŸ์ˆ˜
376
+ max_retries = 3
377
+ retry_count = 0
378
+
379
+ while retry_count < max_retries:
380
+ try:
381
+ # API ์„ค์ • ๊ฐ€์ ธ์˜ค๊ธฐ
382
+ api_config = api_utils.get_next_shopping_api_config()
383
+
384
+ # ์š”์ฒญ ํ—ค๋”
385
+ headers = {
386
+ 'X-Naver-Client-Id': api_config["CLIENT_ID"],
387
+ 'X-Naver-Client-Secret': api_config["CLIENT_SECRET"]
388
+ }
389
+
390
+ # ์š”์ฒญ URL
391
+ url = f"https://openapi.naver.com/v1/search/shop.json?query={api_keyword}&display={count}"
392
+
393
+ response = requests.get(url, headers=headers, timeout=10)
394
+
395
+ if response.status_code == 200:
396
+ result = response.json()
397
+
398
+ # ์ƒํ’ˆ ์ •๋ณด ์ถ”์ถœ
399
+ products = []
400
+
401
+ for item in result.get("items", []):
402
+ # ์นดํ…Œ๊ณ ๋ฆฌ ์ •๋ณด ์ถ”์ถœ ๋ฐ ์ „์ฒด ๊ฒฝ๋กœ ์ƒ์„ฑ
403
+ category1 = item.get("category1", "")
404
+ category2 = item.get("category2", "")
405
+ category3 = item.get("category3", "")
406
+ category4 = item.get("category4", "")
407
+
408
+ # ์ „์ฒด ์นดํ…Œ๊ณ ๋ฆฌ ๊ฒฝ๋กœ ์ƒ์„ฑ (fetch_products_by_keyword์™€ ์ผ๊ด€์„ฑ ์œ ์ง€)
409
+ category_path = " > ".join([c for c in [category1, category2, category3, category4] if c])
410
+
411
+ products.append({
412
+ "title": item.get("title", "").replace("<b>", "").replace("</b>", ""),
413
+ "price": item.get("lprice", "0"),
414
+ "category": category_path if category_path else category1,
415
+ "์นดํ…Œ๊ณ ๋ฆฌ": category_path if category_path else category1 # ํ•œ๊ธ€ ํ‚ค๋กœ๋„ ์ €์žฅ (ํ˜ธํ™˜์„ฑ)
416
+ })
417
+
418
+ return products
419
+ else:
420
+ logger.warning(f"API ์˜ค๋ฅ˜ (์‹œ๋„ {retry_count+1}/{max_retries}): {response.status_code} - {response.text}")
421
+ retry_count += 1
422
+ exponential_backoff_sleep(retry_count)
423
+
424
+ except Exception as e:
425
+ logger.error(f"์ƒํ’ˆ ๊ฒ€์ƒ‰ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ (์‹œ๋„ {retry_count+1}/{max_retries}): {e}")
426
+ retry_count += 1
427
+ exponential_backoff_sleep(retry_count)
428
+
429
+ logger.error(f"์ตœ๋Œ€ ์žฌ์‹œ๋„ ํšŸ์ˆ˜({max_retries})๋ฅผ ์ดˆ๊ณผํ•˜์—ฌ ๋นˆ ๊ฒฐ๊ณผ ๋ฐ˜ํ™˜")
430
+ return []
requirements.txt CHANGED
@@ -1,5 +1,9 @@
1
- gradio_client>=0.8.0
2
- pandas>=1.5.0
3
- xlsxwriter>=3.0.0
4
- requests>=2.28.0
5
- pytz>=2022.1
 
 
 
 
 
1
+ gradio
2
+ pandas
3
+ requests
4
+ google-generativeai
5
+ xlsxwriter
6
+ beautifulsoup4
7
+ fpdf
8
+ markdown
9
+ plotly
style.css CHANGED
@@ -97,11 +97,6 @@ body {
97
  transition: background-color 0.3s ease, color 0.3s ease;
98
  }
99
 
100
- /* ํ‘ธํ„ฐ ์ˆจ๊น€ ์„ค์ • */
101
- footer {
102
- visibility: hidden;
103
- }
104
-
105
  .gradio-container,
106
  .gradio-container *,
107
  .gr-app,
@@ -644,4 +639,4 @@ pre,
644
  .grid-container {
645
  grid-template-columns: 1fr;
646
  }
647
- }
 
97
  transition: background-color 0.3s ease, color 0.3s ease;
98
  }
99
 
 
 
 
 
 
100
  .gradio-container,
101
  .gradio-container *,
102
  .gr-app,
 
639
  .grid-container {
640
  grid-template-columns: 1fr;
641
  }
642
+ }
text_utils.py ADDED
@@ -0,0 +1,227 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ํ…์ŠคํŠธ ์ฒ˜๋ฆฌ ๊ด€๋ จ ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜ ๋ชจ์Œ
3
+ - ํ…์ŠคํŠธ ๋ถ„๋ฆฌ ๋ฐ ์ •์ œ
4
+ - ํ‚ค์›Œ๋“œ ์ถ”์ถœ
5
+ - Gemini API ํ‚ค ํ†ตํ•ฉ ๊ด€๋ฆฌ ์ ์šฉ
6
+ """
7
+
8
+ import re
9
+ import google.generativeai as genai
10
+ import os
11
+ import logging
12
+ import api_utils # API ํ‚ค ํ†ตํ•ฉ ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•œ ์ž„ํฌํŠธ
13
+
14
+ # ๋กœ๊น… ์„ค์ •
15
+ logger = logging.getLogger(__name__)
16
+ logger.setLevel(logging.INFO)
17
+ formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
18
+ handler = logging.StreamHandler()
19
+ handler.setFormatter(formatter)
20
+ logger.addHandler(handler)
21
+
22
+ # ===== Gemini ๋ชจ๋ธ ๊ด€๋ฆฌ ํ•จ์ˆ˜๋“ค =====
23
+ def get_gemini_model():
24
+ """api_utils์—์„œ Gemini ๋ชจ๋ธ ๊ฐ€์ ธ์˜ค๊ธฐ (ํ†ตํ•ฉ ๊ด€๋ฆฌ)"""
25
+ try:
26
+ model = api_utils.get_gemini_model()
27
+ if model:
28
+ logger.info("Gemini ๋ชจ๋ธ ๋กœ๋“œ ์„ฑ๊ณต (api_utils ํ†ตํ•ฉ ๊ด€๋ฆฌ)")
29
+ return model
30
+ else:
31
+ logger.warning("์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ Gemini API ํ‚ค๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
32
+ return None
33
+ except Exception as e:
34
+ logger.error(f"Gemini ๋ชจ๋ธ ๋กœ๋“œ ์‹คํŒจ: {e}")
35
+ return None
36
+
37
+ # ํ…์ŠคํŠธ ๋ถ„๋ฆฌ ๋ฐ ์ •์ œ ํ•จ์ˆ˜
38
+ def clean_and_split(text, only_korean=False):
39
+ """ํ…์ŠคํŠธ๋ฅผ ๋ถ„๋ฆฌํ•˜๊ณ  ์ •์ œํ•˜๋Š” ํ•จ์ˆ˜"""
40
+ text = re.sub(r"[()\[\]-]", " ", text)
41
+ text = text.replace("/", " ")
42
+
43
+ if only_korean:
44
+ # ํ•œ๊ธ€๋งŒ ์ถ”์ถœ ์˜ต์…˜์ด ์ผœ์ง„ ๊ฒฝ์šฐ
45
+ # ๊ณต๋ฐฑ์ด๋‚˜ ์‰ผํ‘œ๋กœ ๊ตฌ๋ถ„ํ•œ ๋’ค ํ•œ๊ธ€๋งŒ ์ถ”์ถœ
46
+ words = re.split(r"[ ,]", text)
47
+ cleaned = []
48
+ for word in words:
49
+ word = word.strip()
50
+ # ํ•œ๊ธ€๋งŒ ๋‚จ๊ธฐ๊ณ  ๋‹ค๋ฅธ ๋ฌธ์ž๋Š” ์ œ๊ฑฐ
51
+ word = re.sub(r"[^๊ฐ€-ํžฃ]", "", word)
52
+ if word and len(word) >= 1: # ๋นˆ ๋ฌธ์ž์—ด์ด ์•„๋‹ˆ๊ณ  1๊ธ€์ž ์ด์ƒ์ธ ๊ฒฝ์šฐ๋งŒ ์ถ”๊ฐ€
53
+ cleaned.append(word)
54
+ else:
55
+ # ํ•œ๊ธ€๋งŒ ์ถ”์ถœ ์˜ต์…˜์ด ๊บผ์ง„ ๊ฒฝ์šฐ - ๋‹จ์–ด ํ†ต์งธ๋กœ ์ฒ˜๋ฆฌ
56
+ # ๊ณต๋ฐฑ๊ณผ ์‰ผํ‘œ๋กœ ๊ตฌ๋ถ„ํ•˜์—ฌ ๋‹จ์–ด ์ „์ฒด๋ฅผ ์œ ์ง€
57
+ words = re.split(r"[,\s]+", text)
58
+ cleaned = []
59
+ for word in words:
60
+ word = word.strip()
61
+ if word and len(word) >= 1: # ๋นˆ ๋ฌธ์ž์—ด์ด ์•„๋‹ˆ๊ณ  1๊ธ€์ž ์ด์ƒ์ธ ๊ฒฝ์šฐ๋งŒ ์ถ”๊ฐ€
62
+ cleaned.append(word)
63
+
64
+ return cleaned
65
+
66
+ def filter_keywords_with_gemini(pairs, gemini_model=None):
67
+ """Gemini AI๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํ‚ค์›Œ๋“œ ์กฐํ•ฉ ํ•„ํ„ฐ๋ง (๊ฐœ์„ ๋ฒ„์ „) - API ํ‚ค ํ†ตํ•ฉ ๊ด€๋ฆฌ"""
68
+ if gemini_model is None:
69
+ # api_utils์—์„œ Gemini ๋ชจ๋ธ ๊ฐ€์ ธ์˜ค๊ธฐ
70
+ gemini_model = get_gemini_model()
71
+
72
+ if gemini_model is None:
73
+ logger.error("Gemini ๋ชจ๋ธ์„ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๋ชจ๋“  ํ‚ค์›Œ๋“œ๋ฅผ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค.")
74
+ # ์•ˆ์ „ํ•˜๊ฒŒ ์ฒ˜๋ฆฌ: ๋ชจ๋“  ํ‚ค์›Œ๋“œ๋ฅผ ์œ ์ง€
75
+ all_keywords = set()
76
+ for pair in pairs:
77
+ for keyword in pair:
78
+ all_keywords.add(keyword)
79
+ return list(all_keywords)
80
+
81
+ # ๋ชจ๋“  ํ‚ค์›Œ๋“œ๋ฅผ ๋ชฉ๋ก์œผ๋กœ ์ถ”์ถœ (์ œ๊ฑฐ๋œ ํ‚ค์›Œ๋“œ ํ™•์ธ์šฉ)
82
+ all_keywords = set()
83
+ for pair in pairs:
84
+ for keyword in pair:
85
+ all_keywords.add(keyword)
86
+
87
+ # ๋„ˆ๋ฌด ๋งŽ์€ ์Œ์ด ์žˆ์œผ๋ฉด ์ œํ•œ
88
+ max_pairs = 50 # ์ตœ๋Œ€ 50๊ฐœ ์Œ๋งŒ ์ฒ˜๋ฆฌ
89
+ pairs_to_process = list(pairs)[:max_pairs] if len(pairs) > max_pairs else pairs
90
+
91
+ logger.info(f"ํ•„ํ„ฐ๋งํ•  ํ‚ค์›Œ๋“œ ์Œ: ์ด {len(pairs)}๊ฐœ ์ค‘ {len(pairs_to_process)}๊ฐœ ์ฒ˜๋ฆฌ")
92
+
93
+ # ๋ณด์ˆ˜์ ์ธ ํ”„๋กฌํ”„ํŠธ ์‚ฌ์šฉ - ํ‚ค์›Œ๋“œ ์ œ๊ฑฐ ์ตœ์†Œํ™”
94
+ prompt = (
95
+ "๋‹ค์Œ์€ ์†Œ๋น„์ž๊ฐ€ ๊ฒ€์ƒ‰ํ•  ๊ฐ€๋Šฅ์„ฑ์ด ์žˆ๋Š” ํ‚ค์›Œ๋“œ ์Œ ๋ชฉ๋ก์ž…๋‹ˆ๋‹ค.\n"
96
+ "๊ฐ ์Œ์€ ๊ฐ™์€ ๋‹จ์–ด ์กฐํ•ฉ์ด์ง€๋งŒ ์ˆœ์„œ๋งŒ ๋‹ค๋ฅธ ๊ฒฝ์šฐ์ž…๋‹ˆ๋‹ค (์˜ˆ: ์†์งˆ์˜ค์ง•์–ด vs ์˜ค์ง•์–ด์†์งˆ).\n\n"
97
+ "์•„๋ž˜์˜ ๊ธฐ์ค€์— ๋”ฐ๋ผ ๊ฐ ์Œ์—์„œ ๋” ์ž์—ฐ์Šค๋Ÿฌ์šด ํ‚ค์›Œ๋“œ๋ฅผ ์„ ํƒํ•ด์ฃผ์„ธ์š”:\n"
98
+ "1. ์†Œ๋น„์ž๊ฐ€ ์ผ์ƒ์ ์œผ๋กœ ์‚ฌ์šฉํ•˜๋Š” ์ž์—ฐ์Šค๋Ÿฌ์šด ํ‘œํ˜„์„ ์šฐ์„  ์„ ํƒํ•˜์„ธ์š”.\n"
99
+ "2. ๋‘ ํ‚ค์›Œ๋“œ๊ฐ€ ๋ชจ๋‘ ์ž์—ฐ์Šค๋Ÿฝ๊ฑฐ๋‚˜ ์˜๋ฏธ๊ฐ€ ์•ฝ๊ฐ„ ๋‹ค๋ฅด๋‹ค๋ฉด, ๋ฐ˜๋“œ์‹œ ๋‘˜ ๋‹ค ์œ ์ง€ํ•˜์„ธ์š”.\n"
100
+ "3. ํ™•์‹คํžˆ ๋น„์ž์—ฐ์Šค๋Ÿฝ๊ฑฐ๋‚˜ ์–ด์ƒ‰ํ•œ ๊ฒฝ์šฐ์—๋งŒ ์ œ๊ฑฐํ•˜์„ธ์š”.\n"
101
+ "4. ๋ถˆํ™•์‹คํ•œ ๊ฒฝ์šฐ์—๋Š” ๋ฐ˜๋“œ์‹œ ํ‚ค์›Œ๋“œ๋ฅผ ์œ ์ง€ํ•˜์„ธ์š”.\n"
102
+ "5. ์ˆซ์ž๋‚˜ ์˜์–ด๊ฐ€ ํฌํ•จ๋œ ํ‚ค์›Œ๋“œ๋Š” ํ•œ๊ธ€ ๋ฉ”์ธ ํ‚ค์›Œ๋“œ๊ฐ€ ์•ž์ชฝ์— ์˜ค๋Š” ํ˜•ํƒœ๋ฅผ ์„ ํƒํ•˜์„ธ์š”. (์˜ˆ: '10kg ์˜ค์ง•์–ด' ๋ณด๋‹ค '์˜ค์ง•์–ด 10kg' ์„ ํƒ)\n"
103
+ "6. ๊ฒ€์ƒ‰๋Ÿ‰์ด 0์ธ ํ‚ค์›Œ๋“œ๋ผ๋„ ์ผ์ƒ์ ์ธ ํ‘œํ˜„์ด๋ผ๋ฉด ๊ฐ€๋Šฅํ•œ ์œ ์ง€ํ•˜์„ธ์š”. ๋ช…๋ฐฑํ•˜๊ฒŒ ๋น„์ •์ƒ์ ์ธ ํ‘œํ˜„๋งŒ ์ œ๊ฑฐํ•˜์„ธ์š”.\n\n"
104
+ "์ฃผ์˜: ๊ธฐ๋ณธ์ ์œผ๋กœ ๋Œ€๋ถ€๋ถ„์˜ ํ‚ค์›Œ๋“œ๋ฅผ ์œ ์ง€ํ•˜๊ณ , ๋งค์šฐ ๋ช…ํ™•ํ•˜๊ฒŒ ๋น„์ž์—ฐ์Šค๋Ÿฌ์šด ๊ฒƒ๋งŒ ์ œ๊ฑฐํ•˜์„ธ์š”.\n\n"
105
+ "๊ฒฐ๊ณผ๋Š” ๋‹ค์Œ ํ˜•์‹์œผ๋กœ ์ œ๊ณตํ•ด์ฃผ์„ธ์š”:\n"
106
+ "- ์„ ํƒ๋œ ํ‚ค์›Œ๋“œ (์ด์œ : ์ž์—ฐ์Šค๋Ÿฌ์šด ํ‘œํ˜„์ด๊ธฐ ๋•Œ๋ฌธ)\n"
107
+ "- ์„ ํƒ๋œ ํ‚ค์›Œ๋“œ1, ์„ ํƒ๋œ ํ‚ค์›Œ๋“œ2 (์ด์œ : ๋‘˜ ๋‹ค ์ž์—ฐ์Šค๋Ÿฝ๊ณ  ์˜๋ฏธ๊ฐ€ ์กฐ๊ธˆ ๋‹ค๋ฆ„)\n\n"
108
+ )
109
+
110
+ # ํ‚ค์›Œ๏ฟฝ๏ฟฝ ์Œ ๋ชฉ๋ก
111
+ formatted = "\n".join([f"- {a}, {b}" for a, b in pairs_to_process])
112
+ full_prompt = prompt + formatted
113
+
114
+ try:
115
+ # ํƒ€์ž„์•„์›ƒ ์ถ”๊ฐ€
116
+ logger.info(f"Gemini API ํ˜ธ์ถœ ์‹œ์ž‘ - {len(pairs_to_process)}๊ฐœ ํ‚ค์›Œ๋“œ ์Œ ์ฒ˜๋ฆฌ ์ค‘...")
117
+
118
+ # ์‘๋‹ต ๋ฐ›๊ธฐ (ํƒ€์ž„์•„์›ƒ ๊ธฐ๋Šฅ์ด ์žˆ์œผ๋ฉด ์ถ”๊ฐ€)
119
+ response = gemini_model.generate_content(full_prompt)
120
+
121
+ logger.info("Gemini API ์‘๋‹ต ์„ฑ๊ณต")
122
+ lines = response.text.strip().split("\n")
123
+
124
+ # ์„ ํƒ๋œ ํ‚ค์›Œ๋“œ ์ถ”์ถœ (์‰ผํ‘œ๋กœ ๊ตฌ๋ถ„๋œ ๊ฒฝ์šฐ ๋ชจ๋‘ ํฌํ•จ)
125
+ final_keywords = []
126
+ for line in lines:
127
+ if line.startswith("-"):
128
+ # ์ด์œ  ๋ถ€๋ถ„ ์ œ๊ฑฐ
129
+ keywords_part = line.strip("- ").split("(์ด์œ :")[0].strip()
130
+ # ์‰ผํ‘œ๋กœ ๊ตฌ๋ถ„๋œ ํ‚ค์›Œ๋“œ ๋ชจ๋‘ ์ถ”๊ฐ€
131
+ for kw in keywords_part.split(","):
132
+ kw = kw.strip()
133
+ if kw:
134
+ final_keywords.append(kw)
135
+
136
+ # ์ฒ˜๋ฆฌ๋˜์ง€ ์•Š์€ ์Œ์˜ ์ฒซ ๋ฒˆ์งธ ํ‚ค์›Œ๋“œ๋„ ์ถ”๊ฐ€ (LLM์ด ์ฒ˜๋ฆฌํ•˜์ง€ ์•Š์€ ํ‚ค์›Œ๋“œ)
137
+ if len(pairs) > max_pairs:
138
+ logger.info(f"์ถ”๊ฐ€ ํ‚ค์›Œ๋“œ ์ฒ˜๋ฆฌ: ๋‚จ์€ {len(pairs) - max_pairs}๊ฐœ ์Œ์˜ ์ฒซ ๋ฒˆ์งธ ํ‚ค์›Œ๋“œ ์ถ”๊ฐ€")
139
+ for pair in list(pairs)[max_pairs:]:
140
+ # ๊ฐ ์Œ์˜ ์ฒซ ๋ฒˆ์งธ ํ‚ค์›Œ๋“œ๋งŒ ์‚ฌ์šฉ
141
+ final_keywords.append(pair[0])
142
+
143
+ # ์„ ํƒ๋œ ํ‚ค์›Œ๋“œ๊ฐ€ ์—†์œผ๋ฉด ๊ธฐ์กด ํ‚ค์›Œ๋“œ ๋ชจ๋‘ ๋ฐ˜ํ™˜
144
+ if not final_keywords:
145
+ logger.warning("๊ฒฝ๊ณ : ์„ ํƒ๋œ ํ‚ค์›Œ๋“œ๊ฐ€ ์—†์–ด ๋ชจ๋“  ํ‚ค์›Œ๋“œ๋ฅผ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค.")
146
+ final_keywords = list(all_keywords)
147
+
148
+ # ์ˆœ์„œ ๊ฐ•์ œ ์ˆ˜์ •
149
+ corrected_keywords = []
150
+
151
+ # ๋‹จ์œ„์™€ ์ˆซ์ž ๊ด€๋ จ ์ •๊ทœ์‹ ํŒจํ„ด
152
+ unit_pattern = re.compile(r'(?i)(kg|g|mm|cm|ml|l|๋ฆฌํ„ฐ|๊ฐœ|ํŒฉ|๋ฐ•์Šค|์„ธํŠธ|2l|l2)')
153
+ number_pattern = re.compile(r'\d+')
154
+
155
+ for kw in final_keywords:
156
+ # ๊ณต๋ฐฑ์œผ๋กœ ๋ถ„๋ฆฌ
157
+ if ' ' in kw:
158
+ parts = kw.split()
159
+ first_part = parts[0]
160
+
161
+ # ์ฒซ ๋ถ€๋ถ„์ด ๋‹จ์œ„๋‚˜ ์ˆซ์ž๋ฅผ ํฌํ•จํ•˜๋Š”์ง€ ํ™•์ธ
162
+ if (unit_pattern.search(first_part) or number_pattern.search(first_part)) and len(parts) > 1:
163
+ # ์ˆœ์„œ ๋ฐ”๊พธ๊ธฐ: ๋‹จ์œ„/์ˆซ์ž ๋ถ€๋ถ„์„ ๋’ค๋กœ ์ด๋™
164
+ corrected_kw = " ".join(parts[1:] + [first_part])
165
+ logger.info(f"ํ‚ค์›Œ๋“œ ์ˆœ์„œ ๊ฐ•์ œ ์ˆ˜์ •: '{kw}' -> '{corrected_kw}'")
166
+ corrected_keywords.append(corrected_kw)
167
+ else:
168
+ corrected_keywords.append(kw)
169
+ else:
170
+ corrected_keywords.append(kw)
171
+
172
+ # ํŠน๋ณ„ ์ฒ˜๋ฆฌ: "L ์˜ค์ง•์–ด", "2L ์˜ค์ง•์–ด" ๊ฐ™์€ ๊ฒฝ์šฐ๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ํ™•์ธํ•˜๊ณ  ์ˆ˜์ •
173
+ specific_fixes = []
174
+ for kw in corrected_keywords:
175
+ # ํŠน์ • ํŒจํ„ด ์ฒดํฌ
176
+ l_pattern = re.compile(r'^([0-9]*L) (.+)$', re.IGNORECASE)
177
+ match = l_pattern.match(kw)
178
+
179
+ if match:
180
+ # L ๋‹จ์œ„๋ฅผ ๋’ค๋กœ ์ด๋™
181
+ l_part = match.group(1)
182
+ main_part = match.group(2)
183
+ fixed_kw = f"{main_part} {l_part}"
184
+ logger.info(f"ํŠน์ˆ˜ ํŒจํ„ด ์ˆ˜์ •: '{kw}' -> '{fixed_kw}'")
185
+ specific_fixes.append(fixed_kw)
186
+ else:
187
+ specific_fixes.append(kw)
188
+
189
+ # ์ œ๊ฑฐ๋œ ํ‚ค์›Œ๋“œ ๋ชฉ๋ก ํ™•์ธ
190
+ selected_set = set(specific_fixes)
191
+ removed_keywords = all_keywords - selected_set
192
+
193
+ # ์ œ๊ฑฐ๋œ ํ‚ค์›Œ๋“œ ์ถœ๋ ฅ
194
+ logger.info("\n=== LLM์— ์˜ํ•ด ์ œ๊ฑฐ๋œ ํ‚ค์›Œ๋“œ ๋ชฉ๋ก ===")
195
+ for kw in removed_keywords:
196
+ logger.info(f" - {kw}")
197
+ logger.info(f"์ด {len(all_keywords)}๊ฐœ ์ค‘ {len(removed_keywords)}๊ฐœ ์ œ๊ฑฐ๋จ ({len(selected_set)}๊ฐœ ์œ ์ง€)\n")
198
+
199
+ return specific_fixes
200
+
201
+ except Exception as e:
202
+ logger.error(f"Gemini ์˜ค๋ฅ˜: {e}")
203
+ logger.error("์˜ค๋ฅ˜ ๋ฐœ์ƒ์œผ๋กœ ์ธํ•ด ๋ชจ๋“  ํ‚ค์›Œ๋“œ๋ฅผ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค.")
204
+ logger.error(f"์˜ค๋ฅ˜ ์œ ํ˜•: {type(e).__name__}")
205
+ import traceback
206
+ traceback.print_exc()
207
+
208
+ # ์•ˆ์ „ํ•˜๊ฒŒ ์ฒ˜๋ฆฌ: ๋ชจ๋“  ํ‚ค์›Œ๋“œ๋ฅผ ์œ ์ง€
209
+ logger.info(f"์•ˆ์ „ ๋ชจ๋“œ: {len(all_keywords)}๊ฐœ ํ‚ค์›Œ๋“œ ๋ชจ๋‘ ์œ ์ง€")
210
+ return list(all_keywords)
211
+
212
+ def get_search_volume_range(total_volume):
213
+ """์ด ๊ฒ€์ƒ‰๋Ÿ‰์„ ๊ธฐ๋ฐ˜์œผ๋กœ ๊ฒ€์ƒ‰๋Ÿ‰ ๊ตฌ๊ฐ„์„ ๋ฐ˜ํ™˜"""
214
+ if total_volume == 0:
215
+ return "100๋ฏธ๋งŒ"
216
+ elif total_volume <= 100:
217
+ return "100๋ฏธ๋งŒ"
218
+ elif total_volume <= 1000:
219
+ return "1000๋ฏธ๋งŒ"
220
+ elif total_volume <= 2000:
221
+ return "2000๋ฏธ๋งŒ"
222
+ elif total_volume <= 5000:
223
+ return "5000๋ฏธ๋งŒ"
224
+ elif total_volume <= 10000:
225
+ return "10000๋ฏธ๋งŒ"
226
+ else:
227
+ return "10000์ด์ƒ"
trend_analysis.py ADDED
@@ -0,0 +1,323 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ํŠธ๋ Œ๋“œ ๋ถ„์„ ๋ชจ๋“ˆ - ๋„ค์ด๋ฒ„ ๋ฐ์ดํ„ฐ๋žฉ API๋ฅผ ํ†ตํ•œ ๊ฒ€์ƒ‰ ํŠธ๋ Œ๋“œ ๋ถ„์„
3
+ - ์„ฑ์žฅ๋ฅ ์„ 3๋…„ ๊ธฐ์ค€์œผ๋กœ ๋ณ€๊ฒฝ
4
+ - ๋„ˆ๋น„ 100% ์ ์šฉ
5
+ """
6
+
7
+ import requests
8
+ import json
9
+ import pandas as pd
10
+ import plotly.graph_objects as go
11
+ import plotly.express as px
12
+ from datetime import datetime, timedelta
13
+ import api_utils
14
+ import keyword_search
15
+ import logging
16
+
17
+ # ๋กœ๊น… ์„ค์ •
18
+ logger = logging.getLogger(__name__)
19
+ logger.setLevel(logging.INFO)
20
+ formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
21
+ handler = logging.StreamHandler()
22
+ handler.setFormatter(formatter)
23
+ logger.addHandler(handler)
24
+
25
+ def get_trend_data(keywords, period="1year"):
26
+ """
27
+ ๋„ค์ด๋ฒ„ ๋ฐ์ดํ„ฐ๋žฉ API๋ฅผ ํ†ตํ•ด ๊ฒ€์ƒ‰ ํŠธ๋ Œ๋“œ ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ (์ˆ˜์ •๋œ ๋ฒ„์ „)
28
+
29
+ Args:
30
+ keywords (list): ๋ถ„์„ํ•  ํ‚ค์›Œ๋“œ ๋ชฉ๋ก (์ตœ๋Œ€ 5๊ฐœ)
31
+ period (str): ๋ถ„์„ ๊ธฐ๊ฐ„ ("1year" ๋˜๋Š” "3year")
32
+
33
+ Returns:
34
+ dict: ํŠธ๋ Œ๋“œ ๋ฐ์ดํ„ฐ ๋ฐ ๊ทธ๋ž˜ํ”„ HTML
35
+ """
36
+ logger.info(f"ํŠธ๋ Œ๋“œ ๋ถ„์„ ์‹œ์ž‘: {len(keywords)}๊ฐœ ํ‚ค์›Œ๋“œ, ๊ธฐ๊ฐ„: {period}")
37
+
38
+ # ๋‚ ์งœ ๊ณ„์‚ฐ (์–ด์ œ ๊ธฐ์ค€์œผ๋กœ ์›” ๋‹จ์œ„ ๊ณ„์‚ฐ)
39
+ yesterday = datetime.now() - timedelta(days=1)
40
+ end_year = yesterday.year
41
+ end_month = yesterday.month
42
+
43
+ if period == "1year":
44
+ # 1๋…„ ์ „ ๊ฐ™์€ ๋‹ฌ
45
+ start_year = end_year - 1
46
+ start_month = end_month
47
+ else: # 3year
48
+ # 3๋…„ ์ „ ๊ฐ™์€ ๋‹ฌ
49
+ start_year = end_year - 3
50
+ start_month = end_month
51
+
52
+ # ์›” ์ฒซ์งธ ๋‚ ๋กœ ๋‚ ์งœ ์„ค์ •
53
+ start_date_str = f"{start_year:04d}-{start_month:02d}-01"
54
+ end_date_str = f"{end_year:04d}-{end_month:02d}-01"
55
+
56
+ logger.info(f"๋ถ„์„ ๊ธฐ๊ฐ„: {start_date_str} ~ {end_date_str} (์›”๊ฐ„ ๋ฐ์ดํ„ฐ)")
57
+
58
+ # ํ‚ค์›Œ๋“œ๋Š” ์ตœ๋Œ€ 5๊ฐœ๊นŒ์ง€๋งŒ ์ฒ˜๋ฆฌ
59
+ keywords = keywords[:5]
60
+
61
+ # API ์„ค์ • ๊ฐ€์ ธ์˜ค๊ธฐ
62
+ api_config = api_utils.get_next_datalab_api_config()
63
+ if not api_config:
64
+ logger.error("๋ฐ์ดํ„ฐ๋žฉ API ์„ค์ •์„ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")
65
+ return {
66
+ "status": "error",
67
+ "message": "๋ฐ์ดํ„ฐ๋žฉ API ์„ค์ • ์—†์Œ"
68
+ }
69
+
70
+ # ํ‚ค์›Œ๋“œ ๊ทธ๋ฃน ์ƒ์„ฑ
71
+ keyword_groups = []
72
+ for keyword in keywords:
73
+ keyword_groups.append({
74
+ 'groupName': keyword,
75
+ 'keywords': [keyword]
76
+ })
77
+
78
+ # API ์š”์ฒญ ๋ฐ์ดํ„ฐ (device ํŒŒ๋ผ๋ฏธํ„ฐ ์ œ๊ฑฐ)
79
+ body_dict = {
80
+ 'startDate': start_date_str,
81
+ 'endDate': end_date_str,
82
+ 'timeUnit': 'month',
83
+ 'keywordGroups': keyword_groups
84
+ # device ํŒŒ๋ผ๋ฏธํ„ฐ ์ œ๊ฑฐ โ†’ ์ „์ฒด ํ™˜๊ฒฝ(PC+๋ชจ๋ฐ”์ผ) ์กฐํšŒ
85
+ }
86
+
87
+ body = json.dumps(body_dict)
88
+
89
+ # API ํ˜ธ์ถœ
90
+ url = "https://openapi.naver.com/v1/datalab/search"
91
+ headers = {
92
+ 'X-Naver-Client-Id': api_config["CLIENT_ID"],
93
+ 'X-Naver-Client-Secret': api_config["CLIENT_SECRET"],
94
+ 'Content-Type': 'application/json'
95
+ }
96
+
97
+ try:
98
+ response = requests.post(url, data=body, headers=headers, timeout=10)
99
+
100
+ if response.status_code == 200:
101
+ response_json = response.json()
102
+
103
+ # ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ
104
+ trend_data = []
105
+ for result in response_json['results']:
106
+ for data_point in result['data']:
107
+ trend_data.append({
108
+ 'keyword': result['title'],
109
+ 'period': data_point['period'],
110
+ 'ratio': data_point['ratio']
111
+ })
112
+
113
+ df_trend = pd.DataFrame(trend_data)
114
+
115
+ # ํ˜„์žฌ ์›”๋ณ„ ๊ฒ€์ƒ‰๋Ÿ‰ ์กฐํšŒ (๊ฒ€์ƒ‰๊ด‘๊ณ  API)
116
+ search_volumes = keyword_search.fetch_all_search_volumes(keywords)
117
+
118
+ # ์ ˆ๋Œ€ ๊ฒ€์ƒ‰๋Ÿ‰์œผ๋กœ ๋ณ€ํ™˜
119
+ df_trend_with_volume = convert_to_absolute_volume(df_trend, search_volumes)
120
+
121
+ # ๊ทธ๋ž˜ํ”„ ์ƒ์„ฑ (๋„ˆ๋น„ 100% ์ ์šฉ)
122
+ graph_html = create_trend_graph(df_trend_with_volume, period)
123
+
124
+ logger.info(f"ํŠธ๋ Œ๋“œ ๋ถ„์„ ์™„๋ฃŒ: {len(trend_data)}๊ฐœ ๋ฐ์ดํ„ฐ ํฌ์ธํŠธ")
125
+
126
+ return {
127
+ "status": "success",
128
+ "trend_data": df_trend_with_volume,
129
+ "graph_html": graph_html,
130
+ "period": period,
131
+ "keywords": keywords
132
+ }
133
+
134
+ else:
135
+ logger.error(f"๋ฐ์ดํ„ฐ๋žฉ API ์˜ค๋ฅ˜: {response.status_code} - {response.text}")
136
+ return {
137
+ "status": "error",
138
+ "message": f"API ์˜ค๋ฅ˜: {response.status_code}"
139
+ }
140
+
141
+ except Exception as e:
142
+ logger.error(f"ํŠธ๋ Œ๋“œ ๋ถ„์„ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}")
143
+ return {
144
+ "status": "error",
145
+ "message": f"๋ถ„์„ ์ค‘ ์˜ค๋ฅ˜: {str(e)}"
146
+ }
147
+
148
+ def convert_to_absolute_volume(df_trend, search_volumes):
149
+ """
150
+ ์ƒ๋Œ€ ๊ฒ€๏ฟฝ๏ฟฝ๏ฟฝ๋Ÿ‰์„ ์ ˆ๋Œ€ ๊ฒ€์ƒ‰๋Ÿ‰์œผ๋กœ ๋ณ€ํ™˜
151
+
152
+ Args:
153
+ df_trend (DataFrame): ํŠธ๋ Œ๋“œ ๋ฐ์ดํ„ฐ (์ƒ๋Œ€๊ฐ’)
154
+ search_volumes (dict): ํ˜„์žฌ ์›”๋ณ„ ๊ฒ€์ƒ‰๋Ÿ‰ ๋ฐ์ดํ„ฐ
155
+
156
+ Returns:
157
+ DataFrame: ์ ˆ๋Œ€ ๊ฒ€์ƒ‰๋Ÿ‰์ด ์ถ”๊ฐ€๋œ ํŠธ๋ Œ๋“œ ๋ฐ์ดํ„ฐ
158
+ """
159
+ df_result = df_trend.copy()
160
+ df_result['absolute_volume'] = 0
161
+
162
+ # ๊ฐ ํ‚ค์›Œ๋“œ๋ณ„๋กœ ์ฒ˜๋ฆฌ
163
+ for keyword in df_trend['keyword'].unique():
164
+ keyword_data = df_trend[df_trend['keyword'] == keyword]
165
+
166
+ # ํ˜„์žฌ ์›”์˜ ๋น„์œจ (๋งˆ์ง€๋ง‰ ๋ฐ์ดํ„ฐ)
167
+ current_ratio = keyword_data['ratio'].iloc[-1]
168
+
169
+ # ํ˜„์žฌ ์›”์˜ ๊ฒ€์ƒ‰๋Ÿ‰ (PC + ๋ชจ๋ฐ”์ผ)
170
+ volume_data = search_volumes.get(keyword.replace(" ", ""), {"์ด๊ฒ€์ƒ‰๋Ÿ‰": 0})
171
+ current_volume = volume_data.get("์ด๊ฒ€์ƒ‰๋Ÿ‰", 0)
172
+
173
+ if current_ratio > 0 and current_volume > 0:
174
+ # 1%๋‹น ๊ฒ€์ƒ‰๋Ÿ‰ ๊ณ„์‚ฐ
175
+ volume_per_percent = current_volume / current_ratio
176
+
177
+ # ๊ฐ ๊ธฐ๊ฐ„์˜ ์ ˆ๋Œ€ ๊ฒ€์ƒ‰๋Ÿ‰ ๊ณ„์‚ฐ
178
+ mask = df_result['keyword'] == keyword
179
+ df_result.loc[mask, 'absolute_volume'] = (
180
+ df_result.loc[mask, 'ratio'] * volume_per_percent
181
+ ).astype(int)
182
+
183
+ logger.info(f"'{keyword}': ํ˜„์žฌ ๋น„์œจ {current_ratio}%, ๊ฒ€์ƒ‰๋Ÿ‰ {current_volume:,}, 1%๋‹น {volume_per_percent:.0f}")
184
+ else:
185
+ logger.warning(f"'{keyword}': ๊ฒ€์ƒ‰๋Ÿ‰ ๋ณ€ํ™˜ ๋ถˆ๊ฐ€ (๋น„์œจ: {current_ratio}, ๊ฒ€์ƒ‰๋Ÿ‰: {current_volume})")
186
+
187
+ return df_result
188
+
189
+ def create_trend_graph(df_trend, period):
190
+ """
191
+ ํŠธ๋ Œ๋“œ ๊ทธ๋ž˜ํ”„ ์ƒ์„ฑ (๋„ˆ๋น„ 100% ์ ์šฉ)
192
+
193
+ Args:
194
+ df_trend (DataFrame): ํŠธ๋ Œ๋“œ ๋ฐ์ดํ„ฐ
195
+ period (str): ๋ถ„์„ ๊ธฐ๊ฐ„
196
+
197
+ Returns:
198
+ str: HTML ํ˜•ํƒœ์˜ ๊ทธ๋ž˜ํ”„
199
+ """
200
+ # Plotly ๊ทธ๋ž˜ํ”„ ์ƒ์„ฑ
201
+ fig = go.Figure()
202
+
203
+ # ํ‚ค์›Œ๋“œ๋ณ„๋กœ ๋ผ์ธ ์ถ”๊ฐ€
204
+ colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7']
205
+
206
+ for i, keyword in enumerate(df_trend['keyword'].unique()):
207
+ keyword_data = df_trend[df_trend['keyword'] == keyword]
208
+
209
+ fig.add_trace(go.Scatter(
210
+ x=keyword_data['period'],
211
+ y=keyword_data['absolute_volume'],
212
+ mode='lines+markers',
213
+ name=keyword,
214
+ line=dict(color=colors[i % len(colors)], width=3),
215
+ marker=dict(size=6),
216
+ hovertemplate='<b>%{fullData.name}</b><br>' +
217
+ '๊ธฐ๊ฐ„: %{x}<br>' +
218
+ '๊ฒ€์ƒ‰๋Ÿ‰: %{y:,}<br>' +
219
+ '<extra></extra>'
220
+ ))
221
+
222
+ # ๋ ˆ์ด์•„์›ƒ ์„ค์ • (๋„ˆ๋น„ 100% ์ ์šฉ)
223
+ period_text = "์ตœ๊ทผ 1๋…„" if period == "1year" else "์ตœ๊ทผ 3๋…„"
224
+
225
+ fig.update_layout(
226
+ title=f'ํ‚ค์›Œ๋“œ๋ณ„ ์›”๋ณ„ ๊ฒ€์ƒ‰๋Ÿ‰ ํŠธ๋ Œ๋“œ ({period_text})',
227
+ xaxis_title='๊ธฐ๊ฐ„',
228
+ yaxis_title='์›”๋ณ„ ๊ฒ€์ƒ‰๋Ÿ‰',
229
+ hovermode='x unified',
230
+ template='plotly_white',
231
+ height=500,
232
+ showlegend=True,
233
+ legend=dict(
234
+ orientation="h",
235
+ yanchor="bottom",
236
+ y=1.02,
237
+ xanchor="right",
238
+ x=1
239
+ ),
240
+ width=None, # ๋„ˆ๋น„ ์ž๋™ ์กฐ์ •
241
+ autosize=True # ์ž๋™ ํฌ๊ธฐ ์กฐ์ •
242
+ )
243
+
244
+ # y์ถ• ํฌ๋งท ์„ค์ • (์ฒœ ๋‹จ์œ„ ๊ตฌ๋ถ„)
245
+ fig.update_yaxis(tickformat=',')
246
+
247
+ # HTML๋กœ ๋ณ€ํ™˜ (๋„ˆ๋น„ 100% ์ ์šฉ)
248
+ graph_html = fig.to_html(
249
+ include_plotlyjs='cdn',
250
+ div_id="trend-graph",
251
+ config={'responsive': True} # ๋ฐ˜์‘ํ˜• ๊ทธ๋ž˜ํ”„
252
+ )
253
+
254
+ return graph_html
255
+
256
+ def calculate_3year_growth_rate(volumes):
257
+ """
258
+ 3๋…„ ๊ธฐ์ค€ ์„ฑ์žฅ๋ฅ  ๊ณ„์‚ฐ (์ „์ฒด ๊ธฐ๊ฐ„ ๊ธฐ์ค€)
259
+
260
+ Args:
261
+ volumes (list): ์›”๋ณ„ ๊ฒ€์ƒ‰๋Ÿ‰ ๋ฐ์ดํ„ฐ
262
+
263
+ Returns:
264
+ float: 3๋…„ ๊ธฐ์ค€ ์„ฑ์žฅ๋ฅ 
265
+ """
266
+ if len(volumes) < 6: # ์ตœ์†Œ 6๊ฐœ์›” ๋ฐ์ดํ„ฐ ํ•„์š”
267
+ return 0
268
+
269
+ # ์ „์ฒด ๊ธฐ๊ฐ„์„ 3๋“ฑ๋ถ„ํ•˜์—ฌ ์„ฑ์žฅ๋ฅ  ๊ณ„์‚ฐ
270
+ total_months = len(volumes)
271
+ period_size = max(1, total_months // 3) # ์ตœ์†Œ 1๊ฐœ์›”
272
+
273
+ # ์ดˆ๊ธฐ ๊ธฐ๊ฐ„ ํ‰๊ท  (์ฒซ 1/3)
274
+ early_period = volumes[:period_size]
275
+ early_avg = sum(early_period) / len(early_period)
276
+
277
+ # ์ตœ๊ทผ ๊ธฐ๊ฐ„ ํ‰๊ท  (๋งˆ์ง€๋ง‰ 1/3)
278
+ recent_period = volumes[-period_size:]
279
+ recent_avg = sum(recent_period) / len(recent_period)
280
+
281
+ if early_avg > 0:
282
+ return round(((recent_avg - early_avg) / early_avg) * 100, 1)
283
+ return 0
284
+
285
+ def analyze_trend_insights(df_trend):
286
+ """
287
+ ํŠธ๋ Œ๋“œ ๋ฐ์ดํ„ฐ์—์„œ ์ธ์‚ฌ์ดํŠธ ์ถ”์ถœ (3๋…„ ๊ธฐ์ค€ ์„ฑ์žฅ๋ฅ ๋กœ ๋ณ€๊ฒฝ)
288
+
289
+ Args:
290
+ df_trend (DataFrame): ํŠธ๋ Œ๋“œ ๋ฐ์ดํ„ฐ
291
+
292
+ Returns:
293
+ dict: ํŠธ๋ Œ๋“œ ์ธ์‚ฌ์ดํŠธ
294
+ """
295
+ insights = {}
296
+
297
+ for keyword in df_trend['keyword'].unique():
298
+ keyword_data = df_trend[df_trend['keyword'] == keyword].sort_values('period')
299
+
300
+ # ์ตœ๊ณ ์ ๊ณผ ์ตœ์ €์ 
301
+ max_volume = keyword_data['absolute_volume'].max()
302
+ min_volume = keyword_data['absolute_volume'].min()
303
+ max_period = keyword_data[keyword_data['absolute_volume'] == max_volume]['period'].iloc[0]
304
+ min_period = keyword_data[keyword_data['absolute_volume'] == min_volume]['period'].iloc[0]
305
+
306
+ # ์ „์ฒด ๊ธฐ๊ฐ„ ํ‰๊ท 
307
+ total_avg = keyword_data['absolute_volume'].mean()
308
+
309
+ # 3๋…„ ๊ธฐ์ค€ ์„ฑ์žฅ๋ฅ  ๊ณ„์‚ฐ (์ „์ฒด ๊ธฐ๊ฐ„ ๊ธฐ์ค€)
310
+ volumes = keyword_data['absolute_volume'].tolist()
311
+ growth_rate = calculate_3year_growth_rate(volumes)
312
+
313
+ insights[keyword] = {
314
+ 'max_volume': int(max_volume),
315
+ 'max_period': max_period,
316
+ 'min_volume': int(min_volume),
317
+ 'min_period': min_period,
318
+ 'total_avg': int(total_avg),
319
+ 'growth_rate': growth_rate,
320
+ 'total_months': len(volumes)
321
+ }
322
+
323
+ return insights
trend_analysis_v2.py ADDED
@@ -0,0 +1,1128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ํŠธ๋ Œ๋“œ ๋ถ„์„ ๋ชจ๋“ˆ v2.16 - ์ •๊ตํ•œ ์ด์ค‘ API ํ˜ธ์ถœ ๋ฐ ์—ญ์‚ฐ ๋กœ์ง ๊ตฌํ˜„
3
+ - ์ด์ค‘ ํŠธ๋ Œ๋“œ API ํ˜ธ์ถœ: ์ผ๋ณ„ + ์›”๋ณ„ ๋ฐ์ดํ„ฐ
4
+ - ์ผ๋ณ„ ๋ฐ์ดํ„ฐ๋กœ ์ „์›” ์ •ํ™•ํ•œ ๊ฒ€์ƒ‰๋Ÿ‰ ์—ญ์‚ฐ
5
+ - ์ „์›” ๊ธฐ์ค€์œผ๋กœ 3๋…„ ๋ชจ๋“  ์›” ๊ฒ€์ƒ‰๋Ÿ‰ ์—ญ์‚ฐ
6
+ - ์ž‘๋…„ ๋™์›” ๊ธฐ๋ฐ˜ ๋ฏธ๋ž˜ 3๊ฐœ์›” ์˜ˆ์ƒ
7
+ - ์ตœ์ข… ๋‹จ๊ณ„์—์„œ 10% ๊ฐ์†Œ ์กฐ์ • ์ ์šฉ
8
+ """
9
+
10
+ import urllib.request
11
+ import json
12
+ import time
13
+ import logging
14
+ from datetime import datetime, timedelta
15
+ import calendar
16
+ import api_utils
17
+
18
+ # ๋กœ๊น… ์„ค์ •
19
+ logger = logging.getLogger(__name__)
20
+
21
+ # ===== ํ•ต์‹ฌ ๊ฐœ์„  ํ•จ์ˆ˜๋“ค =====
22
+
23
+ def get_complete_month():
24
+ """์™„์„ฑ๋œ ๋งˆ์ง€๋ง‰ ์›” ๊ณ„์‚ฐ - ๋‹จ์ˆœํ™”๋œ ๋กœ์ง"""
25
+ current_date = datetime.now()
26
+ current_day = current_date.day
27
+ current_year = current_date.year
28
+ current_month = current_date.month
29
+
30
+ # 3์ผ ์ดํ›„์—๋งŒ ์ „์›”์„ ์™„์„ฑ๋œ ๊ฒƒ์œผ๋กœ ๊ฐ„์ฃผ
31
+ if current_day >= 3:
32
+ completed_year = current_year
33
+ completed_month = current_month - 1
34
+ else:
35
+ completed_year = current_year
36
+ completed_month = current_month - 2
37
+
38
+ # ์›”์ด 0 ์ดํ•˜๊ฐ€ ๋˜๋ฉด ์—ฐ๋„ ์กฐ์ •
39
+ while completed_month <= 0:
40
+ completed_month += 12
41
+ completed_year -= 1
42
+
43
+ return completed_year, completed_month
44
+
45
+ def get_daily_trend_data(keywords, max_retries=3):
46
+ """1์ฐจ ํ˜ธ์ถœ: ์ผ๋ณ„ ํŠธ๋ Œ๋“œ ๋ฐ์ดํ„ฐ (์ „์›” ์ •ํ™• ๊ณ„์‚ฐ์šฉ)"""
47
+ for retry_attempt in range(max_retries):
48
+ try:
49
+ # ๋ฐ์ดํ„ฐ๋žฉ API ์„ค์ •
50
+ datalab_config = api_utils.get_next_datalab_api_config()
51
+ if not datalab_config:
52
+ logger.warning("๋ฐ์ดํ„ฐ๋žฉ API ํ‚ค๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.")
53
+ return None
54
+
55
+ client_id = datalab_config["CLIENT_ID"]
56
+ client_secret = datalab_config["CLIENT_SECRET"]
57
+
58
+ # ์™„์„ฑ๋œ ๋งˆ์ง€๋ง‰ ์›” ๊ณ„์‚ฐ
59
+ completed_year, completed_month = get_complete_month()
60
+
61
+ # ์ผ๋ณ„ ๋ฐ์ดํ„ฐ ๊ธฐ๊ฐ„: ์ „์›” 1์ผ ~ ์–ด์ œ
62
+ current_date = datetime.now()
63
+ yesterday = current_date - timedelta(days=1)
64
+
65
+ start_date = f"{completed_year:04d}-{completed_month:02d}-01"
66
+ end_date = yesterday.strftime("%Y-%m-%d")
67
+
68
+ logger.info(f"๐Ÿ“ž 1์ฐจ ํ˜ธ์ถœ (์ผ๋ณ„): {start_date} ~ {end_date}")
69
+
70
+ # ํ‚ค์›Œ๋“œ ๊ทธ๋ฃน ์ƒ์„ฑ
71
+ keywordGroups = []
72
+ for kw in keywords[:5]:
73
+ keywordGroups.append({
74
+ 'groupName': kw,
75
+ 'keywords': [kw]
76
+ })
77
+
78
+ # API ์š”์ฒญ
79
+ body_dict = {
80
+ 'startDate': start_date,
81
+ 'endDate': end_date,
82
+ 'timeUnit': 'date', # ์ผ๋ณ„!
83
+ 'keywordGroups': keywordGroups
84
+ }
85
+
86
+ url = "https://openapi.naver.com/v1/datalab/search"
87
+ body = json.dumps(body_dict, ensure_ascii=False)
88
+
89
+ request = urllib.request.Request(url)
90
+ request.add_header("X-Naver-Client-Id", client_id)
91
+ request.add_header("X-Naver-Client-Secret", client_secret)
92
+ request.add_header("Content-Type", "application/json")
93
+
94
+ response = urllib.request.urlopen(request, data=body.encode("utf-8"), timeout=15)
95
+ rescode = response.getcode()
96
+
97
+ if rescode == 200:
98
+ response_body = response.read()
99
+ response_json = json.loads(response_body)
100
+ logger.info(f"์ผ๋ณ„ ํŠธ๋ Œ๋“œ ๋ฐ์ดํ„ฐ ์กฐํšŒ ์„ฑ๊ณต")
101
+ return response_json
102
+ else:
103
+ logger.error(f"์ผ๋ณ„ API ์˜ค๋ฅ˜: ์ƒํƒœ์ฝ”๋“œ {rescode}")
104
+ if retry_attempt < max_retries - 1:
105
+ time.sleep(2 * (retry_attempt + 1))
106
+ continue
107
+ return None
108
+
109
+ except Exception as e:
110
+ logger.error(f"์ผ๋ณ„ ํŠธ๋ Œ๋“œ ์กฐํšŒ ์˜ค๋ฅ˜ (์‹œ๋„ {retry_attempt + 1}): {e}")
111
+ if retry_attempt < max_retries - 1:
112
+ time.sleep(2 * (retry_attempt + 1))
113
+ continue
114
+ return None
115
+
116
+ return None
117
+
118
+ def get_monthly_trend_data(keywords, max_retries=3):
119
+ """2์ฐจ ํ˜ธ์ถœ: ์›”๋ณ„ ํŠธ๋ Œ๋“œ ๋ฐ์ดํ„ฐ (3๋…„ ์ „์ฒด + ์˜ˆ์ƒ์šฉ)"""
120
+ for retry_attempt in range(max_retries):
121
+ try:
122
+ # ๋ฐ์ดํ„ฐ๋žฉ API ์„ค์ •
123
+ datalab_config = api_utils.get_next_datalab_api_config()
124
+ if not datalab_config:
125
+ logger.warning("๋ฐ์ดํ„ฐ๋žฉ API ํ‚ค๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.")
126
+ return None
127
+
128
+ client_id = datalab_config["CLIENT_ID"]
129
+ client_secret = datalab_config["CLIENT_SECRET"]
130
+
131
+ # ์™„์„ฑ๋œ ๋งˆ์ง€๋ง‰ ์›” ๊ณ„์‚ฐ
132
+ completed_year, completed_month = get_complete_month()
133
+
134
+ # ์›”๋ณ„ ๋ฐ์ดํ„ฐ ๊ธฐ๊ฐ„: 3๋…„ ์ „ 1์›” ~ ์™„์„ฑ๋œ ๋งˆ์ง€๋ง‰ ์›”
135
+ start_year = completed_year - 3
136
+ start_date = f"{start_year:04d}-01-01"
137
+ end_date = f"{completed_year:04d}-{completed_month:02d}-01"
138
+
139
+ logger.info(f"๐Ÿ“ž 2์ฐจ ํ˜ธ์ถœ (์›”๋ณ„): {start_date} ~ {end_date}")
140
+
141
+ # ํ‚ค์›Œ๋“œ ๊ทธ๋ฃน ์ƒ์„ฑ
142
+ keywordGroups = []
143
+ for kw in keywords[:5]:
144
+ keywordGroups.append({
145
+ 'groupName': kw,
146
+ 'keywords': [kw]
147
+ })
148
+
149
+ # API ์š”์ฒญ
150
+ body_dict = {
151
+ 'startDate': start_date,
152
+ 'endDate': end_date,
153
+ 'timeUnit': 'month', # ์›”๋ณ„!
154
+ 'keywordGroups': keywordGroups
155
+ }
156
+
157
+ url = "https://openapi.naver.com/v1/datalab/search"
158
+ body = json.dumps(body_dict, ensure_ascii=False)
159
+
160
+ request = urllib.request.Request(url)
161
+ request.add_header("X-Naver-Client-Id", client_id)
162
+ request.add_header("X-Naver-Client-Secret", client_secret)
163
+ request.add_header("Content-Type", "application/json")
164
+
165
+ response = urllib.request.urlopen(request, data=body.encode("utf-8"), timeout=15)
166
+ rescode = response.getcode()
167
+
168
+ if rescode == 200:
169
+ response_body = response.read()
170
+ response_json = json.loads(response_body)
171
+ logger.info(f"์›”๋ณ„ ํŠธ๋ Œ๋“œ ๋ฐ์ดํ„ฐ ์กฐํšŒ ์„ฑ๊ณต")
172
+ return response_json
173
+ else:
174
+ logger.error(f"์›”๋ณ„ API ์˜ค๋ฅ˜: ์ƒํƒœ์ฝ”๋“œ {rescode}")
175
+ if retry_attempt < max_retries - 1:
176
+ time.sleep(2 * (retry_attempt + 1))
177
+ continue
178
+ return None
179
+
180
+ except Exception as e:
181
+ logger.error(f"์›”๋ณ„ ํŠธ๋ Œ๋“œ ์กฐํšŒ ์˜ค๋ฅ˜ (์‹œ๋„ {retry_attempt + 1}): {e}")
182
+ if retry_attempt < max_retries - 1:
183
+ time.sleep(2 * (retry_attempt + 1))
184
+ continue
185
+ return None
186
+
187
+ return None
188
+
189
+ def calculate_previous_month_from_daily(current_volume, daily_data):
190
+ """์ผ๋ณ„ ํŠธ๋ Œ๋“œ๋กœ ์ „์›” ์ •ํ™•ํ•œ ๊ฒ€์ƒ‰๋Ÿ‰ ์—ญ์‚ฐ"""
191
+ if not daily_data or "results" not in daily_data:
192
+ logger.warning("์ผ๋ณ„ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์–ด ์ „์›” ๊ณ„์‚ฐ์„ ๊ฑด๋„ˆ๋œ๋‹ˆ๋‹ค.")
193
+ return current_volume
194
+
195
+ try:
196
+ completed_year, completed_month = get_complete_month()
197
+
198
+ # ์ „์›” ์ผ์ˆ˜ ๊ณ„์‚ฐ
199
+ prev_month_days = calendar.monthrange(completed_year, completed_month)[1]
200
+
201
+ for result in daily_data["results"]:
202
+ keyword = result["title"]
203
+
204
+ if not result["data"]:
205
+ continue
206
+
207
+ # ์ตœ๊ทผ 30์ผ๊ณผ ์ „์›” ๋ฐ์ดํ„ฐ ๋ถ„๋ฆฌ
208
+ recent_30_ratios = [] # ์ตœ๊ทผ 30์ผ (ํ˜„์žฌ ๊ฒ€์ƒ‰๋Ÿ‰ ๊ธฐ์ค€)
209
+ prev_month_ratios = [] # ์ „์›” ๋ฐ์ดํ„ฐ
210
+
211
+ for data_point in result["data"]:
212
+ try:
213
+ date_obj = datetime.strptime(data_point["period"], "%Y-%m-%d")
214
+ ratio = data_point["ratio"]
215
+
216
+ # ์ „์›” ๋ฐ์ดํ„ฐ
217
+ if date_obj.year == completed_year and date_obj.month == completed_month:
218
+ prev_month_ratios.append(ratio)
219
+
220
+ # ์ตœ๊ทผ 30์ผ (ํ˜„์žฌ ๊ฒ€์ƒ‰๋Ÿ‰ ๊ธฐ์ค€ ๊ตฌ๊ฐ„)
221
+ current_date = datetime.now()
222
+ if (current_date - date_obj).days <= 30:
223
+ recent_30_ratios.append(ratio)
224
+
225
+ except:
226
+ continue
227
+
228
+ if not recent_30_ratios or not prev_month_ratios:
229
+ logger.warning(f"'{keyword}' ๋น„๊ต ๋ฐ์ดํ„ฐ๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค.")
230
+ continue
231
+
232
+ # ํ‰๊ท  ๋น„์œจ ๊ณ„์‚ฐ
233
+ recent_30_avg = sum(recent_30_ratios) / len(recent_30_ratios)
234
+ prev_month_avg = sum(prev_month_ratios) / len(prev_month_ratios)
235
+
236
+ if recent_30_avg == 0:
237
+ continue
238
+
239
+ # ์ „์›” ๊ฒ€์ƒ‰๋Ÿ‰ ์—ญ์‚ฐ
240
+ # ํ˜„์žฌ ๊ฒ€์ƒ‰๋Ÿ‰ = ์ตœ๊ทผ 30์ผ ํ‰๊ท  ๊ธฐ์ค€
241
+ # ์ „์›” ๊ฒ€์ƒ‰๋Ÿ‰ = (์ „์›” ํ‰๊ท  / ์ตœ๊ทผ 30์ผ ํ‰๊ท ) ร— ํ˜„์žฌ ๊ฒ€์ƒ‰๋Ÿ‰ ร— (์ „์›” ์ผ์ˆ˜ / 30์ผ)
242
+ prev_month_volume = int(
243
+ (prev_month_avg / recent_30_avg) * current_volume * (prev_month_days / 30)
244
+ )
245
+
246
+ logger.info(f"'{keyword}' ์ „์›” {completed_year}.{completed_month:02d} ์—ญ์‚ฐ ๊ฒ€์ƒ‰๋Ÿ‰: {prev_month_volume:,}ํšŒ")
247
+ logger.info(f" - ์ตœ๊ทผ 30์ผ ํ‰๊ท  ๋น„์œจ: {recent_30_avg:.1f}%")
248
+ logger.info(f" - ์ „์›” ํ‰๊ท  ๋น„์œจ: {prev_month_avg:.1f}%")
249
+ logger.info(f" - ์ „์›” ์ผ์ˆ˜ ๋ณด์ •: {prev_month_days}์ผ")
250
+
251
+ return prev_month_volume
252
+
253
+ except Exception as e:
254
+ logger.error(f"์ „์›” ์—ญ์‚ฐ ๊ณ„์‚ฐ ์˜ค๋ฅ˜: {e}")
255
+ return current_volume
256
+
257
+ return current_volume
258
+
259
+ def calculate_all_months_from_previous(prev_month_volume, monthly_data, completed_year, completed_month):
260
+ """์ „์›”์„ ๊ธฐ์ค€์œผ๋กœ ๋ชจ๋“  ์›” ๊ฒ€์ƒ‰๋Ÿ‰ ์—ญ์‚ฐ"""
261
+ if not monthly_data or "results" not in monthly_data:
262
+ logger.warning("์›”๋ณ„ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์–ด ์—ญ์‚ฐ ๊ณ„์‚ฐ์„ ๊ฑด๋„ˆ๋œ๋‹ˆ๋‹ค.")
263
+ return [], []
264
+
265
+ monthly_volumes = []
266
+ dates = []
267
+
268
+ try:
269
+ for result in monthly_data["results"]:
270
+ keyword = result["title"]
271
+
272
+ if not result["data"]:
273
+ continue
274
+
275
+ # ์ „์›”(๊ธฐ์ค€์›”) ๋น„์œจ ์ฐพ๊ธฐ
276
+ base_ratio = None
277
+ for data_point in result["data"]:
278
+ try:
279
+ date_obj = datetime.strptime(data_point["period"], "%Y-%m-%d")
280
+ if date_obj.year == completed_year and date_obj.month == completed_month:
281
+ base_ratio = data_point["ratio"]
282
+ break
283
+ except:
284
+ continue
285
+
286
+ if base_ratio is None or base_ratio == 0:
287
+ logger.warning(f"'{keyword}' ๊ธฐ์ค€์›” ๋น„์œจ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")
288
+ continue
289
+
290
+ logger.info(f"'{keyword}' ๊ธฐ์ค€์›” {completed_year}.{completed_month:02d} ๋น„์œจ: {base_ratio}% (๊ฒ€์ƒ‰๋Ÿ‰: {prev_month_volume:,}ํšŒ)")
291
+
292
+ # ๋ชจ๋“  ์›” ๊ฒ€์ƒ‰๋Ÿ‰ ์—ญ์‚ฐ
293
+ for data_point in result["data"]:
294
+ try:
295
+ date_obj = datetime.strptime(data_point["period"], "%Y-%m-%d")
296
+ ratio = data_point["ratio"]
297
+
298
+ # ํ•ด๋‹น ์›”์˜ ์ผ์ˆ˜ ๊ฐ€์ ธ์˜ค๊ธฐ
299
+ month_days = calendar.monthrange(date_obj.year, date_obj.month)[1]
300
+ base_month_days = calendar.monthrange(completed_year, completed_month)[1]
301
+
302
+ # ์—ญ์‚ฐ ๊ณ„์‚ฐ: (ํ•ด๋‹น์›” ๋น„์œจ / ๊ธฐ์ค€์›” ๋น„์œจ) ร— ๊ธฐ์ค€์›” ๊ฒ€์ƒ‰๋Ÿ‰ ร— (ํ•ด๋‹น์›” ์ผ์ˆ˜ / ๊ธฐ์ค€์›” ์ผ์ˆ˜)
303
+ calculated_volume = int(
304
+ (ratio / base_ratio) * prev_month_volume * (month_days / base_month_days)
305
+ )
306
+ calculated_volume = max(calculated_volume, 0) # ์Œ์ˆ˜ ๋ฐฉ์ง€
307
+
308
+ monthly_volumes.append(calculated_volume)
309
+ dates.append(data_point["period"])
310
+
311
+ except:
312
+ continue
313
+
314
+ logger.info(f"'{keyword}' ์ „์ฒด ์›”๋ณ„ ๊ฒ€์ƒ‰๋Ÿ‰ ์—ญ์‚ฐ ์™„๋ฃŒ: {len(monthly_volumes)}๊ฐœ์›”")
315
+ break # ์ฒซ ๋ฒˆ์งธ ํ‚ค์›Œ๋“œ๋งŒ ์ฒ˜๋ฆฌ
316
+
317
+ except Exception as e:
318
+ logger.error(f"์›”๋ณ„ ์—ญ์‚ฐ ๊ณ„์‚ฐ ์˜ค๋ฅ˜: {e}")
319
+ return [], []
320
+
321
+ return monthly_volumes, dates
322
+
323
+ def generate_future_from_growth_rate(monthly_volumes, dates, completed_year, completed_month):
324
+ """์ฆ๊ฐ์œจ ๊ธฐ๋ฐ˜ ๋ฏธ๋ž˜ 3๊ฐœ์›” ์˜ˆ์ƒ ์ƒ์„ฑ"""
325
+ if len(monthly_volumes) < 12:
326
+ logger.warning("๋ฏธ๋ž˜ ์˜ˆ์ธก์„ ์œ„ํ•œ ์ถฉ๋ถ„ํ•œ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
327
+ return [], []
328
+
329
+ try:
330
+ # ์ž‘๋…„ ๋™์›”๋“ค๊ณผ ์˜ฌํ•ด ์™„์„ฑ๋œ ์›”๋“ค ๋น„๊ตํ•˜์—ฌ ์ฆ๊ฐ์œจ ๊ณ„์‚ฐ
331
+ this_year_volumes = []
332
+ last_year_volumes = []
333
+
334
+ for i, date_str in enumerate(dates):
335
+ try:
336
+ date_obj = datetime.strptime(date_str, "%Y-%m-%d")
337
+
338
+ # ์˜ฌํ•ด ์™„์„ฑ๋œ ๋ฐ์ดํ„ฐ (1์›” ~ ์™„์„ฑ๋œ ์›”)
339
+ if date_obj.year == completed_year and date_obj.month <= completed_month:
340
+ this_year_volumes.append(monthly_volumes[i])
341
+
342
+ # ์ž‘๋…„ ๋™์ผ ๊ธฐ๊ฐ„ ๋ฐ์ดํ„ฐ
343
+ if date_obj.year == completed_year - 1 and date_obj.month <= completed_month:
344
+ last_year_volumes.append(monthly_volumes[i])
345
+
346
+ except:
347
+ continue
348
+
349
+ # ์ฆ๊ฐ์œจ ๊ณ„์‚ฐ
350
+ if len(this_year_volumes) >= 3 and len(last_year_volumes) >= 3:
351
+ this_year_avg = sum(this_year_volumes) / len(this_year_volumes)
352
+ last_year_avg = sum(last_year_volumes) / len(last_year_volumes)
353
+
354
+ if last_year_avg > 0:
355
+ growth_rate = (this_year_avg - last_year_avg) / last_year_avg
356
+ # ์ฆ๊ฐ์œจ ๋ฒ”์œ„ ์ œํ•œ (-50% ~ +100%)
357
+ growth_rate = max(-0.5, min(growth_rate, 1.0))
358
+ else:
359
+ growth_rate = 0
360
+ else:
361
+ growth_rate = 0
362
+
363
+ logger.info(f"๊ณ„์‚ฐ๋œ ์ฆ๊ฐ์œจ: {growth_rate*100:+.1f}%")
364
+
365
+ # ๋ฏธ๋ž˜ 3๊ฐœ์›” ์˜ˆ์ƒ
366
+ predicted_volumes = []
367
+ predicted_dates = []
368
+
369
+ for month_offset in range(1, 4): # 1, 2, 3๊ฐœ์›” ํ›„
370
+ pred_year = completed_year
371
+ pred_month = completed_month + month_offset
372
+
373
+ while pred_month > 12:
374
+ pred_month -= 12
375
+ pred_year += 1
376
+
377
+ # ์ž‘๋…„ ๋™์›” ๋ฐ์ดํ„ฐ ์ฐพ๊ธฐ
378
+ last_year_pred_year = pred_year - 1
379
+ last_year_pred_month = pred_month
380
+ last_year_volume = None
381
+
382
+ for i, date_str in enumerate(dates):
383
+ try:
384
+ date_obj = datetime.strptime(date_str, "%Y-%m-%d")
385
+ if date_obj.year == last_year_pred_year and date_obj.month == last_year_pred_month:
386
+ last_year_volume = monthly_volumes[i]
387
+ break
388
+ except:
389
+ continue
390
+
391
+ # ์˜ˆ์ƒ ๊ฒ€์ƒ‰๋Ÿ‰ = ์ž‘๋…„ ๋™์›” ร— (1 + ์ฆ๊ฐ์œจ)
392
+ if last_year_volume is not None:
393
+ predicted_volume = int(last_year_volume * (1 + growth_rate))
394
+ predicted_volume = max(predicted_volume, 0) # ์Œ์ˆ˜ ๋ฐฉ์ง€
395
+
396
+ predicted_volumes.append(predicted_volume)
397
+ predicted_dates.append(f"{pred_year:04d}-{pred_month:02d}-01")
398
+
399
+ logger.info(f"์˜ˆ์ƒ {pred_year}.{pred_month:02d}: ์ž‘๋…„ ๋™์›” {last_year_volume:,}ํšŒ โ†’ ์˜ˆ์ƒ {predicted_volume:,}ํšŒ")
400
+
401
+ return predicted_volumes, predicted_dates
402
+
403
+ except Exception as e:
404
+ logger.error(f"๋ฏธ๋ž˜ ์˜ˆ์ธก ์ƒ์„ฑ ์˜ค๋ฅ˜: {e}")
405
+ return [], []
406
+
407
+ def apply_final_10_percent_reduction(monthly_data):
408
+ """์ตœ์ข… ๋‹จ๊ณ„: ๋ชจ๋“  ๊ฒฐ๊ณผ์— 10% ๊ฐ์†Œ ์ ์šฉ"""
409
+ adjusted_data = {}
410
+
411
+ try:
412
+ for keyword, data in monthly_data.items():
413
+ adjusted_volumes = []
414
+
415
+ for volume in data["monthly_volumes"]:
416
+ if volume >= 10:
417
+ adjusted_volume = int(volume * 0.9) # 10% ๊ฐ์†Œ
418
+ else:
419
+ adjusted_volume = volume # 10 ๋ฏธ๋งŒ์€ ๊ทธ๋Œ€๋กœ
420
+
421
+ adjusted_volumes.append(adjusted_volume)
422
+
423
+ # ๋ฐ์ดํ„ฐ ๋ณต์‚ฌ ๋ฐ ๊ฒ€์ƒ‰๋Ÿ‰ ์กฐ์ •
424
+ adjusted_data[keyword] = data.copy()
425
+ adjusted_data[keyword]["monthly_volumes"] = adjusted_volumes
426
+
427
+ # ํ˜„์žฌ ๊ฒ€์ƒ‰๋Ÿ‰๋„ ์กฐ์ •
428
+ if data["current_volume"] >= 10:
429
+ adjusted_data[keyword]["current_volume"] = int(data["current_volume"] * 0.9)
430
+
431
+ logger.info("์ตœ์ข… 10% ๊ฐ์†Œ ์กฐ์ • ์™„๋ฃŒ")
432
+
433
+ except Exception as e:
434
+ logger.error(f"10% ๊ฐ์†Œ ์กฐ์ • ์˜ค๋ฅ˜: {e}")
435
+ return monthly_data
436
+
437
+ return adjusted_data
438
+
439
+ # ===== ๋ฉ”์ธ ํ•จ์ˆ˜๋“ค (๊ธฐ์กด ํ˜ธํ™˜์„ฑ ์œ ์ง€) =====
440
+
441
+ def get_naver_trend_data_v5(keywords, period="1year", max_retries=3):
442
+ """๊ฐœ์„ ๋œ ๋„ค์ด๋ฒ„ ๋ฐ์ดํ„ฐ๋žฉ API ํ˜ธ์ถœ - ์ด์ค‘ ํ˜ธ์ถœ ๊ตฌํ˜„"""
443
+
444
+ if period == "1year":
445
+ # ์ด์ค‘ API ํ˜ธ์ถœ
446
+ daily_data = get_daily_trend_data(keywords, max_retries)
447
+ monthly_data = get_monthly_trend_data(keywords, max_retries)
448
+
449
+ # ๊ฒฐ๊ณผ ๋ฐ˜ํ™˜ (๊ธฐ์กด ์ฝ”๋“œ์™€ ํ˜ธํ™˜์„ฑ ์œ ์ง€๋ฅผ ์œ„ํ•ด ์›”๋ณ„ ๋ฐ์ดํ„ฐ ์šฐ์„  ๋ฐ˜ํ™˜)
450
+ return {
451
+ 'daily_data': daily_data,
452
+ 'monthly_data': monthly_data,
453
+ 'results': monthly_data['results'] if monthly_data else []
454
+ }
455
+ else: # 3year
456
+ # 3๋…„ ๋ฐ์ดํ„ฐ๋Š” ์›”๋ณ„๋งŒ ํ˜ธ์ถœ (๊ธฐ์กด ๋ฐฉ์‹ ์œ ์ง€)
457
+ monthly_data = get_monthly_trend_data(keywords, max_retries)
458
+ return monthly_data
459
+
460
+ def calculate_monthly_volumes_v7(keywords, current_volumes, trend_data, period="1year"):
461
+ """๊ฐœ์„ ๋œ ์›”๋ณ„ ๊ฒ€์ƒ‰๋Ÿ‰ ๊ณ„์‚ฐ - ์ •๊ตํ•œ ์—ญ์‚ฐ ๋กœ์ง ์ ์šฉ"""
462
+ monthly_data = {}
463
+
464
+ # ํŠธ๋ Œ๋“œ ๋ฐ์ดํ„ฐ ํ™•์ธ
465
+ if isinstance(trend_data, dict) and 'daily_data' in trend_data and 'monthly_data' in trend_data:
466
+ # ์ƒˆ๋กœ์šด ์ด์ค‘ ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ
467
+ daily_data = trend_data['daily_data']
468
+ monthly_data_api = trend_data['monthly_data']
469
+ else:
470
+ # ๊ธฐ์กด ๋‹จ์ผ ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ (3year ๋“ฑ)
471
+ daily_data = None
472
+ monthly_data_api = trend_data
473
+
474
+ if not monthly_data_api or "results" not in monthly_data_api:
475
+ logger.warning("์›”๋ณ„ ํŠธ๋ Œ๋“œ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์–ด ๊ณ„์‚ฐ์„ ๊ฑด๋„ˆ๋œ๋‹ˆ๋‹ค.")
476
+ return monthly_data
477
+
478
+ logger.info(f"๊ฐœ์„ ๋œ ์›”๋ณ„ ๊ฒ€์ƒ‰๋Ÿ‰ ๊ณ„์‚ฐ ์‹œ์ž‘: {len(monthly_data_api['results'])}๊ฐœ ํ‚ค์›Œ๋“œ")
479
+
480
+ for result in monthly_data_api["results"]:
481
+ keyword = result["title"]
482
+ api_keyword = keyword.replace(" ", "")
483
+
484
+ # ํ˜„์žฌ ๊ฒ€์ƒ‰๋Ÿ‰
485
+ volume_data = current_volumes.get(api_keyword, {"์ด๊ฒ€์ƒ‰๋Ÿ‰": 0})
486
+ current_volume = volume_data["์ด๊ฒ€์ƒ‰๋Ÿ‰"]
487
+
488
+ if current_volume == 0:
489
+ logger.warning(f"'{keyword}' ํ˜„์žฌ ๊ฒ€์ƒ‰๋Ÿ‰์ด 0์ด๋ฏ€๋กœ ๊ณ„์‚ฐ์„ ๊ฑด๋„ˆ๋œ๋‹ˆ๋‹ค.")
490
+ continue
491
+
492
+ logger.info(f"'{keyword}' ์ฒ˜๋ฆฌ ์‹œ์ž‘ - ํ˜„์žฌ ๊ฒ€์ƒ‰๋Ÿ‰: {current_volume:,}ํšŒ")
493
+
494
+ if period == "1year" and daily_data:
495
+ # ๐Ÿ”ฅ ์ƒˆ๋กœ์šด ์ •๊ตํ•œ ๋กœ์ง ์ ์šฉ
496
+ completed_year, completed_month = get_complete_month()
497
+
498
+ # 1๋‹จ๊ณ„: ์ผ๋ณ„ ๋ฐ์ดํ„ฐ๋กœ ์ „์›” ์ •ํ™•ํ•œ ๊ฒ€์ƒ‰๋Ÿ‰ ๊ณ„์‚ฐ
499
+ prev_month_volume = calculate_previous_month_from_daily(current_volume, daily_data)
500
+
501
+ # 2๋‹จ๊ณ„: ์ „์›”์„ ๊ธฐ์ค€์œผ๋กœ ๋ชจ๋“  ์›” ๊ฒ€์ƒ‰๋Ÿ‰ ์—ญ์‚ฐ
502
+ monthly_volumes, dates = calculate_all_months_from_previous(
503
+ prev_month_volume, monthly_data_api, completed_year, completed_month
504
+ )
505
+
506
+ if not monthly_volumes:
507
+ logger.warning(f"'{keyword}' ์›”๋ณ„ ๊ฒ€์ƒ‰๋Ÿ‰ ๊ณ„์‚ฐ ์‹คํŒจ")
508
+ continue
509
+
510
+ # 3๋‹จ๊ณ„: ๋ฏธ๋ž˜ 3๊ฐœ์›” ์˜ˆ์ƒ ์ƒ์„ฑ
511
+ predicted_volumes, predicted_dates = generate_future_from_growth_rate(
512
+ monthly_volumes, dates, completed_year, completed_month
513
+ )
514
+
515
+ # ์‹ค์ œ + ์˜ˆ์ƒ ๋ฐ์ดํ„ฐ ๊ฒฐํ•ฉ - ์ตœ๊ทผ 12๊ฐœ์›” + ํ–ฅํ›„ 3๊ฐœ์›” = ์ด 15๊ฐœ์›”
516
+ # ์ตœ๊ทผ 12๊ฐœ์›”๋งŒ ์‹ค์ œ ๋ฐ์ดํ„ฐ๋กœ ์ œํ•œ
517
+ recent_12_months = monthly_volumes[-12:] if len(monthly_volumes) >= 12 else monthly_volumes
518
+ recent_12_dates = dates[-12:] if len(dates) >= 12 else dates
519
+
520
+ all_volumes = recent_12_months + predicted_volumes
521
+ all_dates = recent_12_dates + predicted_dates
522
+
523
+ # ์ฆ๊ฐ์œจ ๊ณ„์‚ฐ (์˜ˆ์ƒ 3๊ฐœ์›”)
524
+ growth_rate = calculate_future_3month_growth_rate(all_volumes, all_dates)
525
+
526
+ monthly_data[keyword] = {
527
+ "monthly_volumes": all_volumes, # 10% ๊ฐ์†Œ ์ ์šฉ ์ „ - ์ด 15๊ฐœ์›” (12๊ฐœ์›” ์‹ค์ œ + 3๊ฐœ์›” ์˜ˆ์ƒ)
528
+ "dates": all_dates,
529
+ "current_volume": current_volume, # 10% ๊ฐ์†Œ ์ ์šฉ ์ „
530
+ "growth_rate": growth_rate,
531
+ "volume_per_percent": prev_month_volume / 100 if prev_month_volume > 0 else 0,
532
+ "current_ratio": 100,
533
+ "actual_count": len(recent_12_months), # ์‹ค์ œ 12๊ฐœ์›”
534
+ "predicted_count": len(predicted_volumes) # ์˜ˆ์ƒ 3๊ฐœ์›”
535
+ }
536
+
537
+ else:
538
+ # ๊ธฐ์กด ๋ฐฉ์‹ (3year ๋“ฑ)
539
+ if not result["data"]:
540
+ continue
541
+
542
+ current_ratio = result["data"][-1]["ratio"]
543
+ if current_ratio == 0:
544
+ continue
545
+
546
+ volume_per_percent = current_volume / current_ratio
547
+
548
+ monthly_volumes = []
549
+ dates = []
550
+
551
+ for data_point in result["data"]:
552
+ ratio = data_point["ratio"]
553
+ period_date = data_point["period"]
554
+ estimated_volume = int(volume_per_percent * ratio)
555
+
556
+ monthly_volumes.append(estimated_volume)
557
+ dates.append(period_date)
558
+
559
+ growth_rate = calculate_3year_growth_rate_improved(monthly_volumes)
560
+
561
+ monthly_data[keyword] = {
562
+ "monthly_volumes": monthly_volumes, # 10% ๊ฐ์†Œ ์ ์šฉ ์ „
563
+ "dates": dates,
564
+ "current_volume": current_volume, # 10% ๊ฐ์†Œ ์ ์šฉ ์ „
565
+ "growth_rate": growth_rate,
566
+ "volume_per_percent": volume_per_percent,
567
+ "current_ratio": current_ratio,
568
+ "actual_count": len(monthly_volumes),
569
+ "predicted_count": 0
570
+ }
571
+
572
+ logger.info(f"'{keyword}' ๊ณ„์‚ฐ ์™„๋ฃŒ - ๊ฒ€์ƒ‰๋Ÿ‰ ๋ฐ์ดํ„ฐ {len(monthly_data[keyword]['monthly_volumes'])}๊ฐœ")
573
+
574
+ # ๐Ÿ”ฅ 4๋‹จ๊ณ„: ์ตœ์ข… 10% ๊ฐ์†Œ ์ ์šฉ
575
+ final_data = apply_final_10_percent_reduction(monthly_data)
576
+
577
+ logger.info("๊ฐœ์„ ๋œ ์›”๋ณ„ ๊ฒ€์ƒ‰๋Ÿ‰ ๊ณ„์‚ฐ ์™„๋ฃŒ (10% ๊ฐ์†Œ ์ ์šฉ๋จ)")
578
+ return final_data
579
+
580
+ # ===== ์ฆ๊ฐ์œจ ๊ณ„์‚ฐ ํ•จ์ˆ˜๋“ค (๊ธฐ์กด ์œ ์ง€) =====
581
+
582
+ def calculate_future_3month_growth_rate(volumes, dates):
583
+ """์˜ˆ์ƒ 3๊ฐœ์›” ์ฆ๊ฐ์œจ ๊ณ„์‚ฐ"""
584
+ if len(volumes) < 4:
585
+ return 0
586
+
587
+ try:
588
+ completed_year, completed_month = get_complete_month()
589
+
590
+ # ๊ธฐ์ค€์›” ๋ฐ์ดํ„ฐ ์ฐพ๊ธฐ
591
+ base_month_volume = None
592
+ for i, date_str in enumerate(dates):
593
+ try:
594
+ date_obj = datetime.strptime(date_str, "%Y-%m-%d")
595
+ if date_obj.year == completed_year and date_obj.month == completed_month:
596
+ base_month_volume = volumes[i]
597
+ break
598
+ except:
599
+ continue
600
+
601
+ if base_month_volume is None:
602
+ return 0
603
+
604
+ # ํ–ฅํ›„ 3๊ฐœ์›” ์˜ˆ์ƒ ๋ฐ์ดํ„ฐ ์ฐพ๊ธฐ
605
+ future_volumes = []
606
+ for month_offset in range(1, 4):
607
+ pred_year = completed_year
608
+ pred_month = completed_month + month_offset
609
+
610
+ while pred_month > 12:
611
+ pred_month -= 12
612
+ pred_year += 1
613
+
614
+ for i, date_str in enumerate(dates):
615
+ try:
616
+ date_obj = datetime.strptime(date_str, "%Y-%m-%d")
617
+ if date_obj.year == pred_year and date_obj.month == pred_month:
618
+ future_volumes.append(volumes[i])
619
+ break
620
+ except:
621
+ continue
622
+
623
+ if len(future_volumes) < 3:
624
+ return 0
625
+
626
+ # ์ฆ๊ฐ์œจ ๊ณ„์‚ฐ
627
+ future_average = sum(future_volumes) / len(future_volumes)
628
+
629
+ if base_month_volume > 0:
630
+ growth_rate = ((future_average - base_month_volume) / base_month_volume) * 100
631
+ return min(max(growth_rate, -50), 100)
632
+
633
+ return 0
634
+
635
+ except Exception as e:
636
+ logger.error(f"์˜ˆ์ƒ 3๊ฐœ์›” ์ฆ๊ฐ์œจ ๊ณ„์‚ฐ ์˜ค๋ฅ˜: {e}")
637
+ return 0
638
+
639
+ def calculate_3year_growth_rate_improved(volumes):
640
+ """3๋…„ ์ฆ๊ฐ์œจ ๊ณ„์‚ฐ"""
641
+ if len(volumes) < 24:
642
+ return 0
643
+
644
+ try:
645
+ first_year = volumes[:12]
646
+ last_year = volumes[-12:]
647
+
648
+ first_year_avg = sum(first_year) / len(first_year)
649
+ last_year_avg = sum(last_year) / len(last_year)
650
+
651
+ if first_year_avg == 0:
652
+ return 0
653
+
654
+ growth_rate = ((last_year_avg - first_year_avg) / first_year_avg) * 100
655
+ return min(max(growth_rate, -50), 200)
656
+
657
+ except Exception as e:
658
+ logger.error(f"3๋…„ ์ฆ๊ฐ์œจ ๊ณ„์‚ฐ ์˜ค๋ฅ˜: {e}")
659
+ return 0
660
+
661
+ def calculate_correct_growth_rate(volumes, dates):
662
+ """์ž‘๋…„ ๋Œ€๋น„ ์ฆ๊ฐ์œจ ๊ณ„์‚ฐ"""
663
+ if len(volumes) < 13:
664
+ return 0
665
+
666
+ try:
667
+ completed_year, completed_month = get_complete_month()
668
+
669
+ this_year_volume = None
670
+ last_year_volume = None
671
+
672
+ for i, date_str in enumerate(dates):
673
+ try:
674
+ date_obj = datetime.strptime(date_str, "%Y-%m-%d")
675
+
676
+ if date_obj.year == completed_year and date_obj.month == completed_month:
677
+ this_year_volume = volumes[i]
678
+
679
+ if date_obj.year == completed_year - 1 and date_obj.month == completed_month:
680
+ last_year_volume = volumes[i]
681
+
682
+ except:
683
+ continue
684
+
685
+ if this_year_volume is not None and last_year_volume is not None and last_year_volume > 0:
686
+ growth_rate = ((this_year_volume - last_year_volume) / last_year_volume) * 100
687
+ return min(max(growth_rate, -50), 100)
688
+
689
+ return 0
690
+
691
+ except Exception as e:
692
+ logger.error(f"์ž‘๋…„ ๋Œ€๋น„ ์ฆ๊ฐ์œจ ๊ณ„์‚ฐ ์˜ค๋ฅ˜: {e}")
693
+ return 0
694
+
695
+ def generate_future_predictions_correct(volumes, dates, growth_rate):
696
+ """๋ฏธ๋ž˜ ์˜ˆ์ธก ์ƒ์„ฑ (ํ˜ธํ™˜์„ฑ ์œ ์ง€)"""
697
+ return generate_future_from_growth_rate(volumes, dates, *get_complete_month())
698
+
699
+ # ===== ์ฐจํŠธ ์ƒ์„ฑ ํ•จ์ˆ˜๋“ค =====
700
+
701
+ def create_enhanced_current_chart(volume_data, keyword):
702
+ """ํ–ฅ์ƒ๋œ ํ˜„์žฌ ๊ฒ€์ƒ‰๋Ÿ‰ ์ •๋ณด ์ฐจํŠธ - PC vs ๋ชจ๋ฐ”์ผ ๋น„์œจ ํฌํ•จ"""
703
+ total_vol = volume_data['์ด๊ฒ€์ƒ‰๋Ÿ‰']
704
+ pc_vol = volume_data['PC๊ฒ€์ƒ‰๋Ÿ‰']
705
+ mobile_vol = volume_data['๋ชจ๋ฐ”์ผ๊ฒ€์ƒ‰๋Ÿ‰']
706
+
707
+ # ๊ฒ€์ƒ‰๋Ÿ‰ ์ˆ˜์ค€ ํ‰๊ฐ€
708
+ if total_vol >= 100000:
709
+ level_text = "๋†’์Œ ๐Ÿ”ฅ"
710
+ level_color = "#dc3545"
711
+ elif total_vol >= 10000:
712
+ level_text = "์ค‘๊ฐ„ ๐Ÿ“Š"
713
+ level_color = "#ffc107"
714
+ elif total_vol > 0:
715
+ level_text = "๋‚ฎ์Œ ๐Ÿ“‰"
716
+ level_color = "#6c757d"
717
+ else:
718
+ level_text = "๋ฐ์ดํ„ฐ ์—†์Œ โš ๏ธ"
719
+ level_color = "#6c757d"
720
+
721
+ # PC vs ๋ชจ๋ฐ”์ผ ๋น„์œจ
722
+ if total_vol > 0:
723
+ pc_ratio = (pc_vol / total_vol) * 100
724
+ mobile_ratio = (mobile_vol / total_vol) * 100
725
+ else:
726
+ pc_ratio = mobile_ratio = 0
727
+
728
+ return f"""
729
+ <div style="width: 100%; padding: 30px; font-family: 'Pretendard', sans-serif; background: white; border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.1);">
730
+ <!-- ๊ฒ€์ƒ‰๋Ÿ‰ ์ˆ˜์ค€ ํ‘œ์‹œ -->
731
+ <div style="text-align: center; margin-bottom: 25px; padding: 20px; background: #f8f9fa; border-radius: 12px;">
732
+ <h4 style="margin: 0 0 15px 0; color: #495057; font-size: 20px;">๐Ÿ“Š ๊ฒ€์ƒ‰๋Ÿ‰ ์ˆ˜์ค€</h4>
733
+ <span style="display: inline-block; padding: 12px 24px; background: {level_color}; color: white; border-radius: 25px; font-weight: bold; font-size: 18px;">
734
+ {level_text}
735
+ </span>
736
+ </div>
737
+
738
+ <!-- ๊ฒ€์ƒ‰๋Ÿ‰ ์ƒ์„ธ ์ •๋ณด -->
739
+ <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 20px; margin-bottom: 25px;">
740
+ <div style="background: white; padding: 25px; border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); text-align: center; border: 1px solid #e9ecef;">
741
+ <div style="color: #007bff; font-size: 32px; font-weight: bold; margin-bottom: 8px;">{pc_vol:,}</div>
742
+ <div style="color: #6c757d; font-size: 16px; margin-bottom: 8px; font-weight: 600;">PC ๊ฒ€์ƒ‰๋Ÿ‰</div>
743
+ <div style="color: #007bff; font-size: 14px;">({pc_ratio:.1f}%)</div>
744
+ </div>
745
+ <div style="background: white; padding: 25px; border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); text-align: center; border: 1px solid #e9ecef;">
746
+ <div style="color: #28a745; font-size: 32px; font-weight: bold; margin-bottom: 8px;">{mobile_vol:,}</div>
747
+ <div style="color: #6c757d; font-size: 16px; margin-bottom: 8px; font-weight: 600;">๋ชจ๋ฐ”์ผ ๊ฒ€์ƒ‰๋Ÿ‰</div>
748
+ <div style="color: #28a745; font-size: 14px;">({mobile_ratio:.1f}%)</div>
749
+ </div>
750
+ <div style="background: white; padding: 25px; border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); text-align: center; border: 1px solid #e9ecef;">
751
+ <div style="color: #dc3545; font-size: 32px; font-weight: bold; margin-bottom: 8px;">{total_vol:,}</div>
752
+ <div style="color: #6c757d; font-size: 16px; margin-bottom: 8px; font-weight: 600;">์ด ๊ฒ€์ƒ‰๋Ÿ‰</div>
753
+ <div style="color: #dc3545; font-size: 14px;">(100%)</div>
754
+ </div>
755
+ </div>
756
+
757
+ <!-- ๋น„์œจ ๋ฐ” ์ฐจํŠธ -->
758
+ <div style="background: white; padding: 20px; border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); border: 1px solid #e9ecef;">
759
+ <h5 style="margin: 0 0 20px 0; color: #495057; text-align: center; font-size: 18px;">PC vs ๋ชจ๋ฐ”์ผ ๋น„์œจ</h5>
760
+ <div style="display: flex; height: 25px; border-radius: 15px; overflow: hidden; background: #e9ecef;">
761
+ <div style="background: #007bff; width: {pc_ratio}%; display: flex; align-items: center; justify-content: center; font-size: 14px; color: white; font-weight: bold;">
762
+ {f'PC {pc_ratio:.1f}%' if pc_ratio > 15 else ''}
763
+ </div>
764
+ <div style="background: #28a745; width: {mobile_ratio}%; display: flex; align-items: center; justify-content: center; font-size: 14px; color: white; font-weight: bold;">
765
+ {f'๋ชจ๋ฐ”์ผ {mobile_ratio:.1f}%' if mobile_ratio > 15 else ''}
766
+ </div>
767
+ </div>
768
+ </div>
769
+
770
+ <div style="margin-top: 20px; padding: 15px; background: #fff3cd; border-radius: 8px; text-align: center;">
771
+ <p style="margin: 0; font-size: 14px; color: #856404;">
772
+ ๐Ÿ“Š <strong>ํŠธ๋ Œ๋“œ ๋ถ„์„ ์‹œ์Šคํ…œ</strong>: ๋„ค์ด๋ฒ„ ๋ฐ์ดํ„ฐ๋žฉ ๊ธฐ๋ฐ˜ ์ •ํ™•ํ•œ ๊ฒ€์ƒ‰๋Ÿ‰ ๋ถ„์„
773
+ </p>
774
+ </div>
775
+ </div>
776
+ """
777
+
778
+ def create_visual_trend_chart(monthly_data_1year, monthly_data_3year):
779
+ """์‹œ๊ฐ์  ํŠธ๋ Œ๋“œ ์ฐจํŠธ ์ƒ์„ฑ"""
780
+ try:
781
+ chart_html = f"""
782
+ <div style="width: 100%; margin: 20px auto; font-family: 'Pretendard', sans-serif;">
783
+ """
784
+
785
+ periods = [
786
+ {"data": monthly_data_1year, "title": "์ตœ๊ทผ 1๋…„ + ํ–ฅํ›„ 3๊ฐœ์›” ์˜ˆ์ƒ (์ •๊ตํ•œ ์—ญ์‚ฐ)", "period": "1year"},
787
+ {"data": monthly_data_3year, "title": "์ตœ๊ทผ 3๋…„ (10% ๋ณด์ • ์ ์šฉ)", "period": "3year"}
788
+ ]
789
+
790
+ colors = ['#FB7F0D', '#4ECDC4', '#45B7D1', '#96CEB4', '#FF6B6B']
791
+
792
+ for period_info in periods:
793
+ monthly_data = period_info["data"]
794
+ period_title = period_info["title"]
795
+ period_code = period_info["period"]
796
+
797
+ if not monthly_data:
798
+ chart_html += f"""
799
+ <div style="width: 100%; background: white; padding: 20px; border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); margin-bottom: 30px; border: 1px solid #e9ecef;">
800
+ <h4 style="text-align: center; color: #666; margin: 20px 0;">{period_title} - ํŠธ๋ Œ๋“œ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.</h4>
801
+ </div>
802
+ """
803
+ continue
804
+
805
+ chart_html += f"""
806
+ <div style="width: 100%; background: white; padding: 25px; border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); margin-bottom: 30px; border: 1px solid #e9ecef;">
807
+ <h4 style="text-align: center; color: #333; margin-bottom: 25px; font-size: 18px; border-bottom: 2px solid #FB7F0D; padding-bottom: 10px;">
808
+ ๐Ÿš€ {period_title}
809
+ </h4>
810
+ """
811
+
812
+ # ๊ฐ ํ‚ค์›Œ๋“œ๋ณ„๋กœ ์ฐจํŠธ ์ƒ์„ฑ
813
+ for i, (keyword, data) in enumerate(monthly_data.items()):
814
+ volumes = data["monthly_volumes"]
815
+ dates = data["dates"]
816
+ growth_rate = data["growth_rate"]
817
+ actual_count = data.get("actual_count", len(volumes))
818
+
819
+ if not volumes:
820
+ continue
821
+
822
+ # ์ฐจํŠธ ์ƒ‰์ƒ
823
+ color = colors[i % len(colors)]
824
+ predicted_color = f"{color}80" # 50% ํˆฌ๋ช…๋„
825
+
826
+ # Y์ถ•์„ 0๋ถ€ํ„ฐ ์ตœ๋Œ€๊ฐ’๊นŒ์ง€๋กœ ์„ค์ •
827
+ max_volume = max(volumes) if volumes else 1
828
+
829
+ chart_html += f"""
830
+ <div style="width: 100%; margin-bottom: 30px; border: 1px solid #e9ecef; border-radius: 8px; overflow: hidden;">
831
+ <div style="padding: 20px; background: white;">
832
+ <h5 style="margin: 0 0 20px 0; color: #333; font-size: 16px;">
833
+ {keyword} ({get_growth_rate_label(period_code)}: {growth_rate:+.1f}%)
834
+ </h5>
835
+
836
+ <!-- ์ฐจํŠธ ์˜์—ญ -->
837
+ <div style="position: relative; height: 350px; margin: 30px 0 60px 80px; border-left: 2px solid #333; border-bottom: 2px solid #333; padding: 10px;">
838
+
839
+ <!-- Y์ถ• ๋ผ๋ฒจ -->
840
+ <div style="position: absolute; left: -70px; top: -10px; width: 60px; text-align: right; font-size: 11px; color: #333; font-weight: bold;">
841
+ {max_volume:,}
842
+ </div>
843
+ <div style="position: absolute; left: -70px; top: 50%; transform: translateY(-50%); width: 60px; text-align: right; font-size: 10px; color: #666;">
844
+ {max_volume // 2:,}
845
+ </div>
846
+ <div style="position: absolute; left: -70px; bottom: -5px; width: 60px; text-align: right; font-size: 10px; color: #666;">
847
+ 0
848
+ </div>
849
+
850
+ <!-- X์ถ• ๊ทธ๋ฆฌ๋“œ ๋ผ์ธ -->
851
+ <div style="position: absolute; top: 0; left: 0; right: 0; height: 1px; background: #eee;"></div>
852
+ <div style="position: absolute; top: 50%; left: 0; right: 0; height: 1px; background: #eee;"></div>
853
+ <div style="position: absolute; bottom: 0; left: 0; right: 0; height: 1px; background: #333;"></div>
854
+
855
+ <!-- ์ฐจํŠธ ๋ฐ” ์ปจํ…Œ์ด๋„ˆ -->
856
+ <div style="display: flex; align-items: end; height: 100%; gap: 1px; padding: 5px 0;">
857
+ """
858
+
859
+ # ๋ฐ์ดํ„ฐ์™€ ๋‚ ์งœ๋ฅผ ์‹œ๊ฐ„์ˆœ์œผ๋กœ ์ •๋ ฌ
860
+ chart_data = list(zip(dates, volumes, range(len(volumes))))
861
+ chart_data.sort(key=lambda x: x[0]) # ๋‚ ์งœ์ˆœ ์ •๋ ฌ
862
+
863
+ # ๋ง‰๋Œ€ ์ฐจํŠธ ์ƒ์„ฑ
864
+ for date, volume, original_index in chart_data:
865
+ # ๋ง‰๋Œ€ ๋†’์ด ๊ณ„์‚ฐ
866
+ height_percent = (volume / max_volume) * 100 if max_volume > 0 else 0
867
+
868
+ # ์‹ค์ œ ๋ฐ์ดํ„ฐ์™€ ์˜ˆ์ƒ ๋ฐ์ดํ„ฐ ๊ตฌ๋ถ„
869
+ is_predicted = original_index >= actual_count
870
+ bar_color = predicted_color if is_predicted else color
871
+
872
+ # ๋‚ ์งœ ํฌ๋งท
873
+ try:
874
+ date_obj = datetime.strptime(date, "%Y-%m-%d")
875
+ year_short = str(date_obj.year)[-2:]
876
+ month_num = date_obj.month
877
+
878
+ if is_predicted:
879
+ date_formatted = f"{year_short}.{month_num:02d}"
880
+ full_date = date_obj.strftime("%Y๋…„ %m์›”") + " (์˜ˆ์ƒ)"
881
+ bar_style = f"border: 2px dashed #333; background: repeating-linear-gradient(90deg, {bar_color}, {bar_color} 5px, transparent 5px, transparent 10px);"
882
+ else:
883
+ date_formatted = f"{year_short}.{month_num:02d}"
884
+ full_date = date_obj.strftime("%Y๋…„ %m์›”")
885
+ bar_style = f"background: linear-gradient(to top, {bar_color}, {bar_color}dd);"
886
+ except:
887
+ date_formatted = date[-5:].replace('-', '.')
888
+ full_date = date
889
+ bar_style = f"background: linear-gradient(to top, {bar_color}, {bar_color}dd);"
890
+
891
+ # ๊ณ ์œ  ID ์ƒ์„ฑ
892
+ chart_id = f"bar_{period_code}_{i}_{original_index}"
893
+
894
+ chart_html += f"""
895
+ <div style="flex: 1; display: flex; flex-direction: column; align-items: center; position: relative; height: 100%;">
896
+ <!-- ๋ง‰๋Œ€ -->
897
+ <div id="{chart_id}" style="
898
+ {bar_style}
899
+ width: 100%;
900
+ height: {height_percent}%;
901
+ border-radius: 3px 3px 0 0;
902
+ position: relative;
903
+ cursor: pointer;
904
+ transition: all 0.3s ease;
905
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
906
+ min-height: 3px;
907
+ margin-top: auto;
908
+ "
909
+ onmouseover="
910
+ this.style.transform='scaleX(1.1)';
911
+ this.style.zIndex='10';
912
+ this.style.boxShadow='0 4px 8px rgba(0,0,0,0.3)';
913
+ document.getElementById('tooltip_{chart_id}').style.display='block';
914
+ "
915
+ onmouseout="
916
+ this.style.transform='scaleX(1)';
917
+ this.style.zIndex='1';
918
+ this.style.boxShadow='0 2px 4px rgba(0,0,0,0.1)';
919
+ document.getElementById('tooltip_{chart_id}').style.display='none';
920
+ ">
921
+ <!-- ํˆดํŒ -->
922
+ <div id="tooltip_{chart_id}" style="
923
+ display: none;
924
+ position: absolute;
925
+ bottom: calc(100% + 10px);
926
+ left: 50%;
927
+ transform: translateX(-50%);
928
+ background: rgba(0,0,0,0.9);
929
+ color: white;
930
+ padding: 8px 12px;
931
+ border-radius: 6px;
932
+ font-size: 11px;
933
+ white-space: nowrap;
934
+ z-index: 1000;
935
+ box-shadow: 0 4px 12px rgba(0,0,0,0.3);
936
+ pointer-events: none;
937
+ ">
938
+ <div style="text-align: center;">
939
+ <div style="font-weight: bold; color: white; margin-bottom: 2px;">{full_date}</div>
940
+ <div style="color: #ffd700;">๊ฒ€์ƒ‰๋Ÿ‰: {volume:,}ํšŒ</div>
941
+ {'<div style="color: #ff6b6b; margin-top: 2px; font-size: 10px;">์˜ˆ์ƒ ๋ฐ์ดํ„ฐ</div>' if is_predicted else '<div style="color: #90EE90; margin-top: 2px; font-size: 10px;">์‹ค์ œ ๋ฐ์ดํ„ฐ</div>'}
942
+ </div>
943
+ <!-- ํ™”์‚ดํ‘œ -->
944
+ <div style="
945
+ position: absolute;
946
+ top: 100%;
947
+ left: 50%;
948
+ transform: translateX(-50%);
949
+ width: 0;
950
+ height: 0;
951
+ border-left: 5px solid transparent;
952
+ border-right: 5px solid transparent;
953
+ border-top: 5px solid rgba(0,0,0,0.9);
954
+ "></div>
955
+ </div>
956
+ </div>
957
+ </div>
958
+ """
959
+
960
+ chart_html += f"""
961
+ </div>
962
+
963
+ <!-- ์›” ๋ผ๋ฒจ -->
964
+ <div style="display: flex; gap: 1px; margin-top: 10px; padding: 0 5px;">
965
+ """
966
+
967
+ # ์›” ๋ผ๋ฒจ ์ƒ์„ฑ
968
+ for date, volume, original_index in chart_data:
969
+ is_predicted = original_index >= actual_count
970
+
971
+ try:
972
+ date_obj = datetime.strptime(date, "%Y-%m-%d")
973
+ year_short = str(date_obj.year)[-2:]
974
+ month_num = date_obj.month
975
+ date_formatted = f"{year_short}.{month_num:02d}"
976
+ except:
977
+ date_formatted = date[-5:].replace('-', '.')
978
+
979
+ chart_html += f"""
980
+ <div style="
981
+ flex: 1;
982
+ text-align: center;
983
+ font-size: 9px;
984
+ color: {'#e74c3c' if is_predicted else '#666'};
985
+ font-weight: {'bold' if is_predicted else 'normal'};
986
+ transform: rotate(-45deg);
987
+ transform-origin: center;
988
+ line-height: 1;
989
+ margin-top: 8px;
990
+ ">
991
+ {date_formatted}
992
+ </div>
993
+ """
994
+
995
+ # ํ†ต๊ณ„ ์ •๋ณด
996
+ if period_code == "1year":
997
+ actual_volumes = volumes[:actual_count] # ์‹ค์ œ ๋ฐ์ดํ„ฐ๋งŒ
998
+ else:
999
+ actual_volumes = volumes # 3๋…„ ์ „์ฒด ๋ฐ์ดํ„ฐ
1000
+
1001
+ avg_volume = sum(actual_volumes) // len(actual_volumes) if actual_volumes else 0
1002
+ max_volume_val = max(actual_volumes) if actual_volumes else 0
1003
+ min_volume_val = min(actual_volumes) if actual_volumes else 0
1004
+
1005
+ chart_html += f"""
1006
+ </div>
1007
+ </div>
1008
+
1009
+ <!-- ๋ฒ”๋ก€ -->
1010
+ <div style="display: flex; justify-content: center; gap: 20px; margin: 15px 0; font-size: 12px;">
1011
+ <div style="display: flex; align-items: center; gap: 5px;">
1012
+ <div style="width: 15px; height: 15px; background: {color}; border-radius: 2px;"></div>
1013
+ <span style="color: #333;">์‹ค์ œ ๋ฐ์ดํ„ฐ</span>
1014
+ </div>
1015
+ <div style="display: flex; align-items: center; gap: 5px;">
1016
+ <div style="width: 15px; height: 15px; background: repeating-linear-gradient(90deg, {predicted_color}, {predicted_color} 3px, transparent 3px, transparent 6px); border: 1px dashed #333; border-radius: 2px;"></div>
1017
+ <span style="color: #e74c3c;">์˜ˆ์ƒ ๋ฐ์ดํ„ฐ</span>
1018
+ </div>
1019
+ </div>
1020
+
1021
+ <!-- ํ†ต๊ณ„ ์ •๋ณด -->
1022
+ <div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 15px; margin-top: 20px;">
1023
+ <div style="text-align: center; padding: 12px; background: white; border-radius: 8px; box-shadow: 0 2px 6px rgba(0,0,0,0.1); border: 1px solid #e9ecef;">
1024
+ <div style="font-size: 16px; font-weight: bold; color: #3498db;">{min_volume_val:,}</div>
1025
+ <div style="font-size: 11px; color: #666;">์ตœ์ €๊ฒ€์ƒ‰๋Ÿ‰</div>
1026
+ </div>
1027
+ <div style="text-align: center; padding: 12px; background: white; border-radius: 8px; box-shadow: 0 2px 6px rgba(0,0,0,0.1); border: 1px solid #e9ecef;">
1028
+ <div style="font-size: 16px; font-weight: bold; color: #2ecc71;">{avg_volume:,}</div>
1029
+ <div style="font-size: 11px; color: #666;">ํ‰๊ท ๊ฒ€์ƒ‰๋Ÿ‰</div>
1030
+ </div>
1031
+ <div style="text-align: center; padding: 12px; background: white; border-radius: 8px; box-shadow: 0 2px 6px rgba(0,0,0,0.1); border: 1px solid #e9ecef;">
1032
+ <div style="font-size: 16px; font-weight: bold; color: #e74c3c;">{max_volume_val:,}</div>
1033
+ <div style="font-size: 11px; color: #666;">์ตœ๊ณ ๊ฒ€์ƒ‰๋Ÿ‰</div>
1034
+ </div>
1035
+ <div style="text-align: center; padding: 12px; background: white; border-radius: 8px; box-shadow: 0 2px 6px rgba(0,0,0,0.1); border: 1px solid #e9ecef;">
1036
+ <div style="font-size: 16px; font-weight: bold; color: #27ae60;">{growth_rate:+.1f}%</div>
1037
+ <div style="font-size: 11px; color: #666;">{get_growth_rate_label(period_code)}</div>
1038
+ </div>
1039
+ </div>
1040
+ """
1041
+
1042
+ # ๊ฐœ์„  ์„ค๋ช…
1043
+ if period_code == "1year":
1044
+ chart_html += f"""
1045
+ <div style="margin-top: 15px; padding: 12px; background: #e8f5e8; border-radius: 8px; text-align: center;">
1046
+ <p style="margin: 0; font-size: 13px; color: #155724;">
1047
+ ๐Ÿ“Š <strong>์ตœ๊ทผ 1๋…„ + ํ–ฅํ›„ 3๊ฐœ์›” ์˜ˆ์ƒ</strong>: ์‹ค์ƒ‰ ๋ง‰๋Œ€(์‹ค์ œ), ๋น—๊ธˆ ๋ง‰๋Œ€(์˜ˆ์ƒ)
1048
+ </p>
1049
+ </div>
1050
+ """
1051
+ else:
1052
+ chart_html += f"""
1053
+ <div style="margin-top: 15px; padding: 12px; background: #e3f2fd; border-radius: 8px; text-align: center;">
1054
+ <p style="margin: 0; font-size: 13px; color: #1565c0;">
1055
+ ๐Ÿ“Š <strong>์ตœ๊ทผ 3๋…„ ํŠธ๋ Œ๋“œ</strong>: ์ „์ฒด ๊ธฐ๊ฐ„ ๊ฒ€์ƒ‰๋Ÿ‰ ๋ฐ์ดํ„ฐ
1056
+ </p>
1057
+ </div>
1058
+ """
1059
+
1060
+ chart_html += """
1061
+ </div>
1062
+ </div>
1063
+ """
1064
+
1065
+ chart_html += "</div>"
1066
+
1067
+ chart_html += "</div>"
1068
+
1069
+ logger.info(f"๊ฐœ์„ ๋œ ์ •๊ตํ•œ ํŠธ๋ Œ๋“œ ์ฐจํŠธ ์ƒ์„ฑ ์™„๋ฃŒ")
1070
+ return chart_html
1071
+
1072
+ except Exception as e:
1073
+ logger.error(f"์ฐจํŠธ ์ƒ์„ฑ ์˜ค๋ฅ˜: {e}")
1074
+ return f"""
1075
+ <div style="padding: 20px; background: #f8d7da; border-radius: 8px; color: #721c24;">
1076
+ <h4>์ฐจํŠธ ์ƒ์„ฑ ์˜ค๋ฅ˜</h4>
1077
+ <p>์˜ค๋ฅ˜: {str(e)}</p>
1078
+ </div>
1079
+ """
1080
+
1081
+ def create_trend_chart_v7(monthly_data_1year, monthly_data_3year):
1082
+ """๊ฐœ์„ ๋œ ํŠธ๋ Œ๋“œ ์ฐจํŠธ ์ƒ์„ฑ"""
1083
+ try:
1084
+ chart_html = create_visual_trend_chart(monthly_data_1year, monthly_data_3year)
1085
+ return chart_html
1086
+
1087
+ except Exception as e:
1088
+ logger.error(f"์ฐจํŠธ ์ƒ์„ฑ ์˜ค๋ฅ˜: {e}")
1089
+ return f"""
1090
+ <div style="padding: 20px; background: #f8d7da; border-radius: 8px; color: #721c24;">
1091
+ <h4>์ฐจํŠธ ์ƒ์„ฑ ์˜ค๋ฅ˜</h4>
1092
+ <p>์˜ค๋ฅ˜: {str(e)}</p>
1093
+ </div>
1094
+ """
1095
+
1096
+ def get_growth_rate_label(period_code):
1097
+ """๊ธฐ๊ฐ„์— ๋”ฐ๋ฅธ ์„ฑ์žฅ๋ฅ  ๋ผ๋ฒจ ๋ฐ˜ํ™˜"""
1098
+ if period_code == "1year":
1099
+ return "์˜ˆ์ƒ 3๊ฐœ์›” ์ฆ๊ฐ์œจ"
1100
+ else: # 3year
1101
+ return "์ž‘๋…„๋Œ€๋น„ ์ฆ๊ฐ์œจ"
1102
+
1103
+ def create_error_chart(error_msg):
1104
+ """์—๋Ÿฌ ๋ฐœ์ƒ์‹œ ๋Œ€์ฒด ์ฐจํŠธ"""
1105
+ return f"""
1106
+ <div style="padding: 20px; background: #f8d7da; border-radius: 8px; color: #721c24;">
1107
+ <h4>์ฐจํŠธ ์ƒ์„ฑ ์˜ค๋ฅ˜</h4>
1108
+ <p>์˜ค๋ฅ˜: {error_msg}</p>
1109
+ </div>
1110
+ """
1111
+
1112
+ # ===== ํ˜ธํ™˜์„ฑ ํ•จ์ˆ˜๋“ค (๊ธฐ์กด ์ฝ”๋“œ์™€์˜ ํ˜ธํ™˜์„ฑ ์œ ์ง€) =====
1113
+
1114
+ def get_naver_trend_data_v4(keywords, period="1year", max_retries=3):
1115
+ """๊ธฐ์กด ํ•จ์ˆ˜ ํ˜ธํ™˜์„ฑ ์œ ์ง€"""
1116
+ return get_naver_trend_data_v5(keywords, period, max_retries)
1117
+
1118
+ def calculate_monthly_volumes_v6(keywords, current_volumes, trend_data, period="1year"):
1119
+ """๊ธฐ์กด ํ•จ์ˆ˜ ํ˜ธํ™˜์„ฑ ์œ ์ง€"""
1120
+ return calculate_monthly_volumes_v7(keywords, current_volumes, trend_data, period)
1121
+
1122
+ def calculate_monthly_volumes_v5(keywords, current_volumes, trend_data, period="1year"):
1123
+ """๊ธฐ์กด ํ•จ์ˆ˜ ํ˜ธํ™˜์„ฑ ์œ ์ง€"""
1124
+ return calculate_monthly_volumes_v7(keywords, current_volumes, trend_data, period)
1125
+
1126
+ def create_trend_chart_v6(monthly_data_1year, monthly_data_3year):
1127
+ """๊ธฐ์กด ํ•จ์ˆ˜ ํ˜ธํ™˜์„ฑ ์œ ์ง€"""
1128
+ return create_trend_chart_v7(monthly_data_1year, monthly_data_3year)