Upload 11 files
Browse files- api_utils.py +253 -0
- app.py +351 -173
- export_utils.py +478 -0
- keyword_analysis.py +1773 -0
- keyword_analysis_report.css +422 -0
- keyword_diversity_fix.py +943 -0
- keyword_search.py +194 -0
- style.css +264 -43
- text_utils.py +227 -0
- trend_analysis_v2.py +1128 -0
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,54 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import gradio as gr
|
| 2 |
import pandas as pd
|
| 3 |
import os
|
| 4 |
import logging
|
| 5 |
-
|
| 6 |
-
import
|
| 7 |
import time
|
| 8 |
-
import tempfile
|
| 9 |
-
import zipfile
|
| 10 |
import re
|
| 11 |
-
import
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
-
# ๋ก๊น
์ค์
|
| 14 |
-
logging.basicConfig(level=logging.
|
| 15 |
logger = logging.getLogger(__name__)
|
| 16 |
|
| 17 |
-
#
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
|
| 23 |
-
# ===== API
|
| 24 |
-
def
|
| 25 |
-
"""
|
| 26 |
try:
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
# ํ๊ฒฝ๋ณ์์์ API ์๋ํฌ์ธํธ ๊ฐ์ ธ์ค๊ธฐ
|
| 30 |
-
api_endpoint = os.getenv('API_ENDPOINT')
|
| 31 |
-
|
| 32 |
-
if not api_endpoint:
|
| 33 |
-
logger.error("API ์๋ํฌ์ธํธ๊ฐ ์ค์ ๋์ง ์์์ต๋๋ค.")
|
| 34 |
-
raise ValueError("API ์๋ํฌ์ธํธ๊ฐ ์ค์ ๋์ง ์์์ต๋๋ค.")
|
| 35 |
-
|
| 36 |
-
client = Client(api_endpoint)
|
| 37 |
-
logger.info("์๊ฒฉ API ํด๋ผ์ด์ธํธ ์ด๊ธฐํ ์ฑ๊ณต")
|
| 38 |
-
return client
|
| 39 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
except Exception as e:
|
| 41 |
-
logger.error(f"
|
| 42 |
return None
|
| 43 |
|
|
|
|
|
|
|
|
|
|
| 44 |
# ===== ํ๊ตญ์๊ฐ ๊ด๋ จ ํจ์ =====
|
| 45 |
def get_korean_time():
|
| 46 |
"""ํ๊ตญ์๊ฐ ๋ฐํ"""
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
|
|
|
|
|
|
|
|
|
| 52 |
|
| 53 |
def format_korean_datetime(dt=None, format_type="filename"):
|
| 54 |
"""ํ๊ตญ์๊ฐ ํฌ๋งทํ
"""
|
|
@@ -71,7 +92,7 @@ def create_loading_animation():
|
|
| 71 |
<div style="display: flex; flex-direction: column; align-items: center; padding: 40px; background: white; border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.1);">
|
| 72 |
<div style="width: 60px; height: 60px; border: 4px solid #f3f3f3; border-top: 4px solid #FB7F0D; border-radius: 50%; animation: spin 1s linear infinite; margin-bottom: 20px;"></div>
|
| 73 |
<h3 style="color: #FB7F0D; margin: 10px 0; font-size: 18px;">๋ถ์ ์ค์
๋๋ค...</h3>
|
| 74 |
-
<p style="color: #666; margin: 5px 0; text-align: center;">
|
| 75 |
<div style="width: 200px; height: 4px; background: #f0f0f0; border-radius: 2px; margin-top: 15px; overflow: hidden;">
|
| 76 |
<div style="width: 100%; height: 100%; background: linear-gradient(90deg, #FB7F0D, #ff9a8b); border-radius: 2px; animation: progress 2s ease-in-out infinite;"></div>
|
| 77 |
</div>
|
|
@@ -96,20 +117,203 @@ def generate_error_response(error_message):
|
|
| 96 |
return f'''
|
| 97 |
<div style="color: red; padding: 30px; text-align: center; width: 100%;
|
| 98 |
background-color: #f8d7da; border-radius: 12px; border: 1px solid #f5c6cb;">
|
| 99 |
-
<h3 style="margin-bottom: 15px;">โ
|
| 100 |
<p style="margin-bottom: 20px;">{error_message}</p>
|
| 101 |
<div style="background: white; padding: 15px; border-radius: 8px; color: #333;">
|
| 102 |
<h4>ํด๊ฒฐ ๋ฐฉ๋ฒ:</h4>
|
| 103 |
<ul style="text-align: left; padding-left: 20px;">
|
|
|
|
|
|
|
| 104 |
<li>๋คํธ์ํฌ ์ฐ๊ฒฐ์ ํ์ธํด์ฃผ์ธ์</li>
|
| 105 |
-
<li>์๊ฒฉ ์๋ฒ ์ํ๋ฅผ ํ์ธํด์ฃผ์ธ์</li>
|
| 106 |
<li>์ ์ ํ ๋ค์ ์๋ํด์ฃผ์ธ์</li>
|
| 107 |
-
<li>๋ฌธ์ ๊ฐ ์ง์๋๋ฉด ๊ด๋ฆฌ์์๊ฒ ๋ฌธ์ํ์ธ์</li>
|
| 108 |
</ul>
|
| 109 |
</div>
|
| 110 |
</div>
|
| 111 |
'''
|
| 112 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
# ===== ํ์ผ ์ถ๋ ฅ ํจ์๋ค =====
|
| 114 |
def create_timestamp_filename(analysis_keyword):
|
| 115 |
"""ํ์์คํฌํ๊ฐ ํฌํจ๋ ํ์ผ๋ช
์์ฑ - ํ๊ตญ์๊ฐ ์ ์ฉ"""
|
|
@@ -317,72 +521,16 @@ def export_analysis_results(export_data):
|
|
| 317 |
logger.error(f"๋ถ์ ๊ฒฐ๊ณผ ์ถ๋ ฅ ์ค๋ฅ: {e}")
|
| 318 |
return None, f"์ถ๋ ฅ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค: {str(e)}"
|
| 319 |
|
| 320 |
-
# ===== ์๊ฒฉ API ํธ์ถ ํจ์๋ค =====
|
| 321 |
-
def call_analyze_keyword_api(analysis_keyword):
|
| 322 |
-
"""ํค์๋ ์ฌ์ธต๋ถ์ API ํธ์ถ"""
|
| 323 |
-
try:
|
| 324 |
-
client = get_api_client()
|
| 325 |
-
if not client:
|
| 326 |
-
return generate_error_response("API ํด๋ผ์ด์ธํธ๋ฅผ ์ด๊ธฐํํ ์ ์์ต๋๋ค."), {}
|
| 327 |
-
|
| 328 |
-
logger.info("์๊ฒฉ API ํธ์ถ: ํค์๋ ์ฌ์ธต๋ถ์")
|
| 329 |
-
result = client.predict(
|
| 330 |
-
analysis_keyword=analysis_keyword,
|
| 331 |
-
api_name="/on_analyze_keyword"
|
| 332 |
-
)
|
| 333 |
-
|
| 334 |
-
logger.info(f"ํค์๋ ๋ถ์ API ๊ฒฐ๊ณผ ํ์
: {type(result)}")
|
| 335 |
-
|
| 336 |
-
# ๋ถ์ ๊ฒฐ๊ณผ๋ก export ๋ฐ์ดํฐ ์์ฑ
|
| 337 |
-
if isinstance(result, str) and len(result) > 100:
|
| 338 |
-
export_data = {
|
| 339 |
-
"analysis_keyword": analysis_keyword,
|
| 340 |
-
"analysis_html": result,
|
| 341 |
-
"analysis_completed": True,
|
| 342 |
-
"created_at": get_korean_time().isoformat()
|
| 343 |
-
}
|
| 344 |
-
return result, export_data
|
| 345 |
-
else:
|
| 346 |
-
return str(result), {}
|
| 347 |
-
|
| 348 |
-
except Exception as e:
|
| 349 |
-
logger.error(f"ํค์๋ ์ฌ์ธต๋ถ์ API ํธ์ถ ์ค๋ฅ: {e}")
|
| 350 |
-
return generate_error_response(f"์๊ฒฉ ์๋ฒ ์ฐ๊ฒฐ ์คํจ: {str(e)}"), {}
|
| 351 |
-
|
| 352 |
-
def call_export_results_api(export_data):
|
| 353 |
-
"""๋ถ์ ๊ฒฐ๊ณผ ์ถ๋ ฅ API ํธ์ถ"""
|
| 354 |
-
try:
|
| 355 |
-
client = get_api_client()
|
| 356 |
-
if not client:
|
| 357 |
-
return None, "API ํด๋ผ์ด์ธํธ๋ฅผ ์ด๊ธฐํํ ์ ์์ต๋๋ค."
|
| 358 |
-
|
| 359 |
-
logger.info("์๊ฒฉ API ํธ์ถ: ๋ถ์ ๊ฒฐ๊ณผ ์ถ๋ ฅ")
|
| 360 |
-
result = client.predict(
|
| 361 |
-
api_name="/on_export_results"
|
| 362 |
-
)
|
| 363 |
-
|
| 364 |
-
logger.info(f"์ถ๋ ฅ API ๊ฒฐ๊ณผ ํ์
: {type(result)}")
|
| 365 |
-
|
| 366 |
-
# ๊ฒฐ๊ณผ๊ฐ ํํ์ธ ๊ฒฝ์ฐ ์ฒซ ๋ฒ์งธ ์์๋ ๋ฉ์์ง, ๋ ๋ฒ์งธ๋ ํ์ผ
|
| 367 |
-
if isinstance(result, tuple) and len(result) == 2:
|
| 368 |
-
message, file_path = result
|
| 369 |
-
if file_path:
|
| 370 |
-
return file_path, message
|
| 371 |
-
else:
|
| 372 |
-
return None, message
|
| 373 |
-
else:
|
| 374 |
-
return None, str(result)
|
| 375 |
-
|
| 376 |
-
except Exception as e:
|
| 377 |
-
logger.error(f"๋ถ์ ๊ฒฐ๊ณผ ์ถ๋ ฅ API ํธ์ถ ์ค๋ฅ: {e}")
|
| 378 |
-
return None, f"์๊ฒฉ ์๋ฒ ์ฐ๊ฒฐ ์คํจ: {str(e)}"
|
| 379 |
-
|
| 380 |
# ===== ๊ทธ๋ผ๋์ค ์ธํฐํ์ด์ค =====
|
| 381 |
def create_interface():
|
| 382 |
# CSS ํ์ผ ๋ก๋
|
| 383 |
try:
|
| 384 |
with open('style.css', 'r', encoding='utf-8') as f:
|
| 385 |
custom_css = f.read()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 386 |
except:
|
| 387 |
custom_css = """
|
| 388 |
:root { --primary-color: #FB7F0D; --secondary-color: #ff9a8b; }
|
|
@@ -397,30 +545,6 @@ def create_interface():
|
|
| 397 |
font-size: 17px !important; font-weight: bold !important; width: 100% !important;
|
| 398 |
margin-top: 20px !important;
|
| 399 |
}
|
| 400 |
-
.custom-frame {
|
| 401 |
-
background-color: white !important;
|
| 402 |
-
border: 1px solid #e5e5e5 !important;
|
| 403 |
-
border-radius: 18px;
|
| 404 |
-
padding: 20px;
|
| 405 |
-
margin: 10px 0;
|
| 406 |
-
box-shadow: 0 8px 30px rgba(251, 127, 13, 0.08) !important;
|
| 407 |
-
}
|
| 408 |
-
.section-title {
|
| 409 |
-
display: flex;
|
| 410 |
-
align-items: center;
|
| 411 |
-
font-size: 20px;
|
| 412 |
-
font-weight: 700;
|
| 413 |
-
color: #334155 !important;
|
| 414 |
-
margin-bottom: 10px;
|
| 415 |
-
padding-bottom: 5px;
|
| 416 |
-
border-bottom: 2px solid var(--primary-color);
|
| 417 |
-
font-family: 'Pretendard', 'Noto Sans KR', -apple-system, BlinkMacSystemFont, sans-serif;
|
| 418 |
-
}
|
| 419 |
-
.section-title img, .section-title i {
|
| 420 |
-
margin-right: 10px;
|
| 421 |
-
font-size: 20px;
|
| 422 |
-
color: var(--primary-color);
|
| 423 |
-
}
|
| 424 |
"""
|
| 425 |
|
| 426 |
with gr.Blocks(
|
|
@@ -473,7 +597,7 @@ def create_interface():
|
|
| 473 |
yield create_loading_animation(), {}
|
| 474 |
|
| 475 |
# ์ค์ ํค์๋ ๋ถ์ ์คํ
|
| 476 |
-
keyword_result, session_export_data =
|
| 477 |
|
| 478 |
# ๐ ๊ฒ์๋ ํธ๋ ๋ ๋ถ์๊ณผ ๐ฏ ํค์๋ ๋ถ์ ํ์
|
| 479 |
yield keyword_result, session_export_data
|
|
@@ -481,7 +605,6 @@ def create_interface():
|
|
| 481 |
def on_export_results(export_data):
|
| 482 |
"""๋ถ์ ๊ฒฐ๊ณผ ์ถ๋ ฅ ํธ๋ค๋ฌ - ์ธ์
๋ณ ๋ฐ์ดํฐ ์ฒ๋ฆฌ"""
|
| 483 |
try:
|
| 484 |
-
# ๋ก์ปฌ ์ถ๋ ฅ ์๋
|
| 485 |
zip_path, message = export_analysis_results(export_data)
|
| 486 |
|
| 487 |
if zip_path:
|
|
@@ -501,38 +624,14 @@ def create_interface():
|
|
| 501 |
"""
|
| 502 |
return success_html, gr.update(value=zip_path, visible=True)
|
| 503 |
else:
|
| 504 |
-
#
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
{remote_message}<br>
|
| 513 |
-
<i class="fas fa-download"></i> ์๋ ๋ค์ด๋ก๋ ๋ฒํผ์ ํด๋ฆญํ์ฌ ํ์ผ์ ์ ์ฅํ์ธ์.
|
| 514 |
-
</p>
|
| 515 |
-
</div>
|
| 516 |
-
"""
|
| 517 |
-
return success_html, gr.update(value=remote_file, visible=True)
|
| 518 |
-
else:
|
| 519 |
-
# ์คํจ ๋ฉ์์ง
|
| 520 |
-
error_html = f"""
|
| 521 |
-
<div style="background: #f8d7da; border: 1px solid #f5c6cb; padding: 20px; border-radius: 8px; margin: 10px 0;">
|
| 522 |
-
<h4 style="color: #721c24; margin: 0 0 10px 0;"><i class="fas fa-exclamation-triangle"></i> ์ถ๋ ฅ ์คํจ</h4>
|
| 523 |
-
<p style="color: #721c24; margin: 0;">{message}<br>์๊ฒฉ ์ถ๋ ฅ๋ ์คํจ: {remote_message}</p>
|
| 524 |
-
</div>
|
| 525 |
-
"""
|
| 526 |
-
return error_html, gr.update(visible=False)
|
| 527 |
-
except:
|
| 528 |
-
# ์๊ฒฉ API๋ ์คํจ
|
| 529 |
-
error_html = f"""
|
| 530 |
-
<div style="background: #f8d7da; border: 1px solid #f5c6cb; padding: 20px; border-radius: 8px; margin: 10px 0;">
|
| 531 |
-
<h4 style="color: #721c24; margin: 0 0 10px 0;"><i class="fas fa-exclamation-triangle"></i> ์ถ๋ ฅ ์คํจ</h4>
|
| 532 |
-
<p style="color: #721c24; margin: 0;">{message}</p>
|
| 533 |
-
</div>
|
| 534 |
-
"""
|
| 535 |
-
return error_html, gr.update(visible=False)
|
| 536 |
|
| 537 |
except Exception as e:
|
| 538 |
logger.error(f"์ถ๋ ฅ ํธ๋ค๋ฌ ์ค๋ฅ: {e}")
|
|
@@ -559,42 +658,121 @@ def create_interface():
|
|
| 559 |
|
| 560 |
return interface
|
| 561 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 562 |
# ===== ๋ฉ์ธ ์คํ =====
|
| 563 |
if __name__ == "__main__":
|
| 564 |
# pytz ๋ชจ๋ ์ค์น ํ์ธ
|
| 565 |
-
|
| 566 |
-
import pytz
|
| 567 |
logger.info("โ
pytz ๋ชจ๋ ๋ก๋ ์ฑ๊ณต - ํ๊ตญ์๊ฐ ์ง์")
|
| 568 |
-
|
| 569 |
logger.warning("โ ๏ธ pytz ๋ชจ๋์ด ์ค์น๋์ง ์์ - pip install pytz ์คํ ํ์")
|
| 570 |
logger.info("์์คํ
์๊ฐ์ ์ฌ์ฉํฉ๋๋ค.")
|
| 571 |
|
| 572 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 573 |
|
| 574 |
# ํ์ํ ํจํค์ง ์๋ด
|
| 575 |
print("๐ฆ ํ์ํ ํจํค์ง:")
|
| 576 |
-
print(" pip install gradio
|
| 577 |
print()
|
| 578 |
|
| 579 |
-
# API
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
print("
|
| 583 |
-
print("
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 584 |
print()
|
| 585 |
else:
|
| 586 |
-
print("โ
API
|
|
|
|
|
|
|
|
|
|
|
|
|
| 587 |
print()
|
| 588 |
|
| 589 |
-
print("๐ v2.10
|
| 590 |
-
print(" โข
|
| 591 |
-
print(" โข
|
| 592 |
-
print(" โข
|
| 593 |
-
print(" โข
|
|
|
|
|
|
|
|
|
|
| 594 |
print(" โข โ
ํ๊ตญ์๊ฐ ๊ธฐ์ค ํ์ผ๋ช
์์ฑ")
|
| 595 |
print(" โข โ
๋ฉํฐ ์ฌ์ฉ์ ์์ : gr.State๋ก ์ธ์
๋ณ ๋ฐ์ดํฐ ๊ด๋ฆฌ")
|
| 596 |
-
print(" โข
|
| 597 |
-
print(" โข ์๊ฒฉ ์๋ฒ์ ๋ก์ปฌ ์ฒ๋ฆฌ ํ์ด๋ธ๋ฆฌ๋ ๋ฐฉ์")
|
| 598 |
print()
|
| 599 |
|
| 600 |
# ์ฑ ์คํ
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
AI ์ํ ์์ฑ ๋ถ์ ์์คํ
v2.10 - ๊ฐ๋ต๋ฒ์ + ์ถ๋ ฅ๊ธฐ๋ฅ + ๋ฉํฐ์ฌ์ฉ์ ์์
|
| 3 |
+
- 2๋จ๊ณ: ์์ง๋ ํค์๋ ๋ชฉ๋ก ๊ธฐ๋ฅ์ ์ ๊ฑฐํ๋ค.
|
| 4 |
+
- 3๋จ๊ณ: ๋ถ์ํ ํค์๋ ์ ํ์์ ๐ ์ฐ๊ด๊ฒ์์ด ๋ถ์์ ์ํ์ถ์ถ ๋ฐ ๋ถ์๊ธฐ๋ฅ์์ ๊ฑฐํ๋ค.
|
| 5 |
+
- 3๋จ๊ณ: ๋ถ์ํ ํค์๋ ์ ํ์ ๋ช
์นญ์ "ํค์๋ ์ฌ์ธต๋ถ์ ์
๋ ฅ"์ด๋ผ๊ณ ๋ฐ๊พผ๋ค.
|
| 6 |
+
- ๐ ๊ฒ์๋ ํธ๋ ๋ ๋ถ์, ๐ฏ ํค์๋ ๋ถ์์ด ๋์์ผํ๋ค.
|
| 7 |
+
- ์ถ๋ ฅ ๊ธฐ๋ฅ ์ถ๊ฐ: HTML ํ์ผ ์์ฑ ๋ฐ ZIP ๋ค์ด๋ก๋
|
| 8 |
+
- Gemini API ํค ๋๋ค ์ ์ฉ (api_utils ํตํฉ ๊ด๋ฆฌ)
|
| 9 |
+
- ํ๊ตญ์๊ฐ ์ ์ฉ
|
| 10 |
+
- ๋ฉํฐ ์ฌ์ฉ์ ์์ : gr.State ์ฌ์ฉ์ผ๋ก ์ธ์
๋ณ ๋ฐ์ดํฐ ๊ด๋ฆฌ
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
import gradio as gr
|
| 14 |
import pandas as pd
|
| 15 |
import os
|
| 16 |
import logging
|
| 17 |
+
import google.generativeai as genai
|
| 18 |
+
from datetime import datetime, timedelta
|
| 19 |
import time
|
|
|
|
|
|
|
| 20 |
import re
|
| 21 |
+
import zipfile
|
| 22 |
+
import tempfile
|
| 23 |
+
|
| 24 |
+
# ํ๊ตญ์๊ฐ ์ ์ฉ์ ์ํ ๋ชจ๋ (์ ํ์ )
|
| 25 |
+
try:
|
| 26 |
+
import pytz
|
| 27 |
+
PYTZ_AVAILABLE = True
|
| 28 |
+
except ImportError:
|
| 29 |
+
PYTZ_AVAILABLE = False
|
| 30 |
|
| 31 |
+
# ๋ก๊น
์ค์
|
| 32 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
| 33 |
logger = logging.getLogger(__name__)
|
| 34 |
|
| 35 |
+
# ๋ชจ๋ ์ํฌํธ (๊ฐ๋ต ๋ฒ์ ์์ ํ์ํ ๊ฒ๋ง)
|
| 36 |
+
import api_utils
|
| 37 |
+
import keyword_search
|
| 38 |
+
import keyword_analysis
|
| 39 |
+
import trend_analysis_v2
|
| 40 |
|
| 41 |
+
# ===== Gemini API ์ค์ =====
|
| 42 |
+
def setup_gemini_model():
|
| 43 |
+
"""Gemini ๋ชจ๋ธ ์ด๊ธฐํ - api_utils์์ ๊ด๋ฆฌ (๋๋ค ํค ์ ์ฉ)"""
|
| 44 |
try:
|
| 45 |
+
# api_utils์์ Gemini ๋ชจ๋ธ ๊ฐ์ ธ์ค๊ธฐ (๋๋ค ํค ์ ์ฉ)
|
| 46 |
+
model = api_utils.get_gemini_model()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
|
| 48 |
+
if model:
|
| 49 |
+
logger.info("Gemini ๋ชจ๋ธ ์ด๊ธฐํ ์ฑ๊ณต (api_utils ํตํฉ ๊ด๋ฆฌ - ๋๋ค ํค)")
|
| 50 |
+
return model
|
| 51 |
+
else:
|
| 52 |
+
logger.warning("Gemini API ํค๊ฐ ์ค์ ๋์ง ์์์ต๋๋ค.")
|
| 53 |
+
return None
|
| 54 |
+
|
| 55 |
except Exception as e:
|
| 56 |
+
logger.error(f"Gemini ๋ชจ๋ธ ์ด๊ธฐํ ์คํจ: {e}")
|
| 57 |
return None
|
| 58 |
|
| 59 |
+
# Gemini ๋ชจ๋ธ ์ด๊ธฐํ
|
| 60 |
+
gemini_model = setup_gemini_model()
|
| 61 |
+
|
| 62 |
# ===== ํ๊ตญ์๊ฐ ๊ด๋ จ ํจ์ =====
|
| 63 |
def get_korean_time():
|
| 64 |
"""ํ๊ตญ์๊ฐ ๋ฐํ"""
|
| 65 |
+
if PYTZ_AVAILABLE:
|
| 66 |
+
try:
|
| 67 |
+
korea_tz = pytz.timezone('Asia/Seoul')
|
| 68 |
+
return datetime.now(korea_tz)
|
| 69 |
+
except:
|
| 70 |
+
pass
|
| 71 |
+
# pytz๊ฐ ์๊ฑฐ๋ ์ค๋ฅ ์ ์์คํ
์๊ฐ ์ฌ์ฉ
|
| 72 |
+
return datetime.now()
|
| 73 |
|
| 74 |
def format_korean_datetime(dt=None, format_type="filename"):
|
| 75 |
"""ํ๊ตญ์๊ฐ ํฌ๋งทํ
"""
|
|
|
|
| 92 |
<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);">
|
| 93 |
<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>
|
| 94 |
<h3 style="color: #FB7F0D; margin: 10px 0; font-size: 18px;">๋ถ์ ์ค์
๋๋ค...</h3>
|
| 95 |
+
<p style="color: #666; margin: 5px 0; text-align: center;">๋ค์ด๋ฒ ๋ฐ์ดํฐ๋ฅผ ์์งํ๊ณ AI๊ฐ ๋ถ์ํ๊ณ ์์ต๋๋ค.<br>์ ์๋ง ๊ธฐ๋ค๋ ค์ฃผ์ธ์.</p>
|
| 96 |
<div style="width: 200px; height: 4px; background: #f0f0f0; border-radius: 2px; margin-top: 15px; overflow: hidden;">
|
| 97 |
<div style="width: 100%; height: 100%; background: linear-gradient(90deg, #FB7F0D, #ff9a8b); border-radius: 2px; animation: progress 2s ease-in-out infinite;"></div>
|
| 98 |
</div>
|
|
|
|
| 117 |
return f'''
|
| 118 |
<div style="color: red; padding: 30px; text-align: center; width: 100%;
|
| 119 |
background-color: #f8d7da; border-radius: 12px; border: 1px solid #f5c6cb;">
|
| 120 |
+
<h3 style="margin-bottom: 15px;">โ ๋ถ์ ์ค๋ฅ</h3>
|
| 121 |
<p style="margin-bottom: 20px;">{error_message}</p>
|
| 122 |
<div style="background: white; padding: 15px; border-radius: 8px; color: #333;">
|
| 123 |
<h4>ํด๊ฒฐ ๋ฐฉ๋ฒ:</h4>
|
| 124 |
<ul style="text-align: left; padding-left: 20px;">
|
| 125 |
+
<li>ํค์๋ ์ฒ ์๋ฅผ ํ์ธํด์ฃผ์ธ์</li>
|
| 126 |
+
<li>๋ ๊ฐ๋จํ ํค์๋๋ฅผ ์ฌ์ฉํด๋ณด์ธ์</li>
|
| 127 |
<li>๋คํธ์ํฌ ์ฐ๊ฒฐ์ ํ์ธํด์ฃผ์ธ์</li>
|
|
|
|
| 128 |
<li>์ ์ ํ ๋ค์ ์๋ํด์ฃผ์ธ์</li>
|
|
|
|
| 129 |
</ul>
|
| 130 |
</div>
|
| 131 |
</div>
|
| 132 |
'''
|
| 133 |
|
| 134 |
+
# ===== ๋ฉ์ธ ํค์๋ ๋ถ์ ํจ์ =====
|
| 135 |
+
def safe_keyword_analysis(analysis_keyword):
|
| 136 |
+
"""์๋ฌ ๋ฐฉ์ง๋ฅผ ์ํ ์์ ํ ํค์๋ ๋ถ์ - ๋ฉํฐ์ฌ์ฉ์ ์์ """
|
| 137 |
+
|
| 138 |
+
# ์
๋ ฅ๊ฐ ๊ฒ์ฆ
|
| 139 |
+
if not analysis_keyword or not analysis_keyword.strip():
|
| 140 |
+
return generate_error_response("๋ถ์ํ ํค์๋๋ฅผ ์
๋ ฅํด์ฃผ์ธ์."), {}
|
| 141 |
+
|
| 142 |
+
analysis_keyword = analysis_keyword.strip()
|
| 143 |
+
|
| 144 |
+
try:
|
| 145 |
+
# ๊ฒ์๋ ์กฐํ - ์๋ฌ ๋ฐฉ์ง
|
| 146 |
+
api_keyword = keyword_analysis.normalize_keyword_for_api(analysis_keyword)
|
| 147 |
+
search_volumes = keyword_search.fetch_all_search_volumes([api_keyword])
|
| 148 |
+
volume_data = search_volumes.get(api_keyword, {"PC๊ฒ์๋": 0, "๋ชจ๋ฐ์ผ๊ฒ์๋": 0, "์ด๊ฒ์๋": 0})
|
| 149 |
+
|
| 150 |
+
# ๊ฒ์๋์ด 0์ด๊ฑฐ๋ ํค์๋๊ฐ ์กด์ฌํ์ง ์๋ ๊ฒฝ์ฐ ์ฒ๋ฆฌ
|
| 151 |
+
if volume_data['์ด๊ฒ์๋'] == 0:
|
| 152 |
+
logger.warning(f"'{analysis_keyword}' ํค์๋์ ๊ฒ์๋์ด 0์ด๊ฑฐ๋ ์กด์ฌํ์ง ์์ต๋๋ค.")
|
| 153 |
+
error_result = f"""
|
| 154 |
+
<div style="padding: 30px; text-align: center; background: #fff3cd; border-radius: 12px; border: 1px solid #ffeaa7;">
|
| 155 |
+
<h3 style="color: #856404; margin-bottom: 15px;">โ ๏ธ ํค์๋ ๋ถ์ ๋ถ๊ฐ</h3>
|
| 156 |
+
<p style="color: #856404; margin-bottom: 10px;"><strong>'{analysis_keyword}'</strong> ํค์๋๋ ๊ฒ์๋์ด ์๊ฑฐ๋ ์ฌ๋ฐ๋ฅด์ง ์์ ํค์๋์
๋๋ค.</p>
|
| 157 |
+
<div style="background: white; padding: 15px; border-radius: 8px; margin-top: 15px;">
|
| 158 |
+
<h4 style="color: #333; margin-bottom: 10px;">๐ก ๊ถ์ฅ์ฌํญ</h4>
|
| 159 |
+
<ul style="text-align: left; color: #666; padding-left: 20px;">
|
| 160 |
+
<li>ํค์๋ ์ฒ ์๋ฅผ ํ์ธํด์ฃผ์ธ์</li>
|
| 161 |
+
<li>๋ ์ผ๋ฐ์ ์ธ ํค์๋๋ฅผ ์ฌ์ฉํด๋ณด์ธ์</li>
|
| 162 |
+
<li>ํค์๋๋ฅผ ๋์ด์ฐ๊ธฐ๋ก ๊ตฌ๋ถํด๋ณด์ธ์ (์: '์ฌ์ฑ ์ฌ๋ฆฌํผ')</li>
|
| 163 |
+
</ul>
|
| 164 |
+
</div>
|
| 165 |
+
</div>
|
| 166 |
+
"""
|
| 167 |
+
return error_result, {}
|
| 168 |
+
|
| 169 |
+
logger.info(f"'{analysis_keyword}' ํ์ฌ ๊ฒ์๋: {volume_data['์ด๊ฒ์๋']:,}")
|
| 170 |
+
|
| 171 |
+
# ํธ๋ ๋ ๋ถ์ ์๋
|
| 172 |
+
monthly_data_1year = {}
|
| 173 |
+
monthly_data_3year = {}
|
| 174 |
+
trend_available = False
|
| 175 |
+
|
| 176 |
+
try:
|
| 177 |
+
# ๋ฐ์ดํฐ๋ฉ API ํค ํ์ธ (๋๋ค ์ ์ฉ)
|
| 178 |
+
datalab_config = api_utils.get_next_datalab_api_config()
|
| 179 |
+
if datalab_config and not datalab_config["CLIENT_ID"].startswith("YOUR_"):
|
| 180 |
+
logger.info("๋ฐ์ดํฐ๋ฉ API ํค๊ฐ ์ค์ ๋์ด ์์ด 1๋
, 3๋
ํธ๋ ๋ ๋ถ์์ ์๋ํฉ๋๋ค.")
|
| 181 |
+
|
| 182 |
+
# ์ต์ ํ๋ API ํจ์ ์ฌ์ฉ
|
| 183 |
+
# 1๋
ํธ๋ ๋ ๋ฐ์ดํฐ
|
| 184 |
+
trend_data_1year = trend_analysis_v2.get_naver_trend_data_v5([analysis_keyword], "1year", max_retries=3)
|
| 185 |
+
if trend_data_1year:
|
| 186 |
+
current_volumes = {api_keyword: volume_data}
|
| 187 |
+
monthly_data_1year = trend_analysis_v2.calculate_monthly_volumes_v7([analysis_keyword], current_volumes, trend_data_1year, "1year")
|
| 188 |
+
|
| 189 |
+
# 3๋
ํธ๋ ๋ ๋ฐ์ดํฐ
|
| 190 |
+
trend_data_3year = trend_analysis_v2.get_naver_trend_data_v5([analysis_keyword], "3year", max_retries=3)
|
| 191 |
+
if trend_data_3year:
|
| 192 |
+
current_volumes = {api_keyword: volume_data}
|
| 193 |
+
monthly_data_3year = trend_analysis_v2.calculate_monthly_volumes_v7([analysis_keyword], current_volumes, trend_data_3year, "3year")
|
| 194 |
+
|
| 195 |
+
# 3๋
๋ฐ์ดํฐ๊ฐ ์๋ ๊ฒฝ์ฐ 1๋
๋ฐ์ดํฐ๋ก ํ์ฅ
|
| 196 |
+
if not monthly_data_3year and monthly_data_1year:
|
| 197 |
+
logger.info("3๋
๋ฐ์ดํฐ๊ฐ ์์ด 1๋
๋ฐ์ดํฐ๋ฅผ ๊ธฐ๋ฐ์ผ๋ก 3๋
์ฐจํธ ์์ฑ")
|
| 198 |
+
keyword = analysis_keyword
|
| 199 |
+
if keyword in monthly_data_1year:
|
| 200 |
+
data_1y = monthly_data_1year[keyword]
|
| 201 |
+
|
| 202 |
+
# 3๋
๋ถ๋์ ๋ ์ง ์์ฑ (24๊ฐ์ ์ถ๊ฐ)
|
| 203 |
+
extended_dates = []
|
| 204 |
+
extended_volumes = []
|
| 205 |
+
|
| 206 |
+
# ๊ธฐ์กด 1๋
๋ฐ์ดํฐ ์ด์ ์ 24๊ฐ์ ์ถ๊ฐ (๋ชจ๋ 0์ผ๋ก)
|
| 207 |
+
start_date = datetime.strptime(data_1y["dates"][0], "%Y-%m-%d")
|
| 208 |
+
for i in range(24, 0, -1):
|
| 209 |
+
prev_date = start_date - timedelta(days=30 * i)
|
| 210 |
+
extended_dates.append(prev_date.strftime("%Y-%m-%d"))
|
| 211 |
+
extended_volumes.append(0)
|
| 212 |
+
|
| 213 |
+
# ๊ธฐ์กด 1๋
๋ฐ์ดํฐ ์ถ๊ฐ (์์ ๋ฐ์ดํฐ ์ ์ธ)
|
| 214 |
+
actual_count = data_1y.get("actual_count", len(data_1y["dates"]))
|
| 215 |
+
extended_dates.extend(data_1y["dates"][:actual_count])
|
| 216 |
+
extended_volumes.extend(data_1y["monthly_volumes"][:actual_count])
|
| 217 |
+
|
| 218 |
+
monthly_data_3year = {
|
| 219 |
+
keyword: {
|
| 220 |
+
"monthly_volumes": extended_volumes,
|
| 221 |
+
"dates": extended_dates,
|
| 222 |
+
"current_volume": data_1y["current_volume"],
|
| 223 |
+
"growth_rate": trend_analysis_v2.calculate_3year_growth_rate_improved(extended_volumes),
|
| 224 |
+
"volume_per_percent": data_1y["volume_per_percent"],
|
| 225 |
+
"current_ratio": data_1y["current_ratio"],
|
| 226 |
+
"actual_count": len(extended_volumes),
|
| 227 |
+
"predicted_count": 0
|
| 228 |
+
}
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
if monthly_data_1year or monthly_data_3year:
|
| 232 |
+
trend_available = True
|
| 233 |
+
logger.info("ํธ๋ ๋ ๋ถ์ ์ฑ๊ณต")
|
| 234 |
+
else:
|
| 235 |
+
logger.info("ํธ๋ ๋ ๋ฐ์ดํฐ ์ฒ๋ฆฌ ์คํจ")
|
| 236 |
+
else:
|
| 237 |
+
logger.info("๋ฐ์ดํฐ๋ฉ API ํค๊ฐ ์ค์ ๋์ง ์์")
|
| 238 |
+
except Exception as e:
|
| 239 |
+
logger.info(f"ํธ๋ ๋ ๋ถ์ ๊ฑด๋๋: {str(e)[:100]}")
|
| 240 |
+
|
| 241 |
+
# === ๐ ๊ฒ์๋ ํธ๋ ๋ ๋ถ์ ์น์
===
|
| 242 |
+
if trend_available and (monthly_data_1year or monthly_data_3year):
|
| 243 |
+
try:
|
| 244 |
+
trend_chart = trend_analysis_v2.create_trend_chart_v7(monthly_data_1year, monthly_data_3year)
|
| 245 |
+
except Exception as e:
|
| 246 |
+
logger.warning(f"ํธ๋ ๋ ์ฐจํธ ์์ฑ ์คํจ, ๊ธฐ๋ณธ ์ฐจํธ ์ฌ์ฉ: {e}")
|
| 247 |
+
trend_chart = trend_analysis_v2.create_enhanced_current_chart(volume_data, analysis_keyword)
|
| 248 |
+
else:
|
| 249 |
+
trend_chart = trend_analysis_v2.create_enhanced_current_chart(volume_data, analysis_keyword)
|
| 250 |
+
|
| 251 |
+
# ํธ๋ ๋ ์น์
|
| 252 |
+
trend_section = f"""
|
| 253 |
+
<div style="width: 100%; margin: 30px auto; font-family: 'Pretendard', sans-serif;">
|
| 254 |
+
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 15px; border-radius: 10px 10px 0 0; color: white; text-align: center;">
|
| 255 |
+
<h3 style="margin: 0; font-size: 18px; color: white;">๐ ๊ฒ์๋ ํธ๋ ๋ ๋ถ์</h3>
|
| 256 |
+
</div>
|
| 257 |
+
<div style="background: white; padding: 20px; border-radius: 0 0 10px 10px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);">
|
| 258 |
+
{trend_chart}
|
| 259 |
+
</div>
|
| 260 |
+
</div>
|
| 261 |
+
"""
|
| 262 |
+
|
| 263 |
+
# === ๐ฏ ํค์๋ ๋ถ์ ์น์
(AI ๋ถ์) ===
|
| 264 |
+
# api_utils์์ Gemini ๋ชจ๋ธ ๊ฐ์ ธ์ค๊ธฐ (๋๋ค ํค ์ ์ฉ)
|
| 265 |
+
current_gemini_model = api_utils.get_gemini_model()
|
| 266 |
+
|
| 267 |
+
keyword_analysis_html = keyword_analysis.analyze_keyword_for_sourcing(
|
| 268 |
+
analysis_keyword, volume_data, monthly_data_1year, monthly_data_3year,
|
| 269 |
+
None, [], current_gemini_model # ๊ฐ๋ต ๋ฒ์ ์์๋ ์ถ๊ฐ ํค์๋ ๋ฐ์ดํฐ ์์
|
| 270 |
+
)
|
| 271 |
+
|
| 272 |
+
keyword_analysis_section = f"""
|
| 273 |
+
<div style="width: 100%; margin: 30px auto; font-family: 'Pretendard', sans-serif;">
|
| 274 |
+
<div style="background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%); padding: 15px; border-radius: 10px 10px 0 0; color: white; text-align: center;">
|
| 275 |
+
<h3 style="margin: 0; font-size: 18px; color: white;">๐ฏ ํค์๋ ๋ถ์</h3>
|
| 276 |
+
</div>
|
| 277 |
+
<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;">
|
| 278 |
+
{keyword_analysis_html}
|
| 279 |
+
</div>
|
| 280 |
+
</div>
|
| 281 |
+
"""
|
| 282 |
+
|
| 283 |
+
# ๊ฒฝ๊ณ ์น์
(ํ์ํ ๊ฒฝ์ฐ)
|
| 284 |
+
warning_section = ""
|
| 285 |
+
if not trend_available:
|
| 286 |
+
warning_section = f"""
|
| 287 |
+
<div style="width: 100%; margin: 20px auto; padding: 15px; background: #fff3cd; border: 1px solid #ffeaa7; border-radius: 8px; font-family: 'Pretendard', sans-serif;">
|
| 288 |
+
<div style="display: flex; align-items: center;">
|
| 289 |
+
<span style="font-size: 20px; margin-right: 10px;">โ ๏ธ</span>
|
| 290 |
+
<div>
|
| 291 |
+
<strong style="color: #856404;">์ผ๋ถ ๊ธฐ๋ฅ ์ ํ</strong>
|
| 292 |
+
<div style="font-size: 14px; color: #856404; margin-top: 5px;">
|
| 293 |
+
ํธ๋ ๋ ๋ถ์์ ์ ํ์ด ์์ต๋๋ค. ํ์ฌ ๊ฒ์๋ ๋ถ์๊ณผ AI ์ถ์ฒ์ ์ ์ ์ ๊ณต๋ฉ๋๋ค.<br>
|
| 294 |
+
<small>์์ ํ ์ ๋ฐ์ดํฐ ๊ธฐ์ค์ผ๋ก ๋ถ์ํ๊ธฐ ์ํด ์ต์ ์๋ฃ๋ ์๊น์ง๋ง ํ์๋ฉ๋๋ค.</small>
|
| 295 |
+
</div>
|
| 296 |
+
</div>
|
| 297 |
+
</div>
|
| 298 |
+
</div>
|
| 299 |
+
"""
|
| 300 |
+
|
| 301 |
+
# ์ต์ข
๊ฒฐ๊ณผ ์กฐํฉ
|
| 302 |
+
final_result = warning_section + trend_section + keyword_analysis_section
|
| 303 |
+
|
| 304 |
+
# ์ธ์
๋ณ ์ถ๋ ฅ ๋ฐ์ดํฐ ๋ฐํ (๋ฉํฐ ์ฌ์ฉ์ ์์ )
|
| 305 |
+
session_export_data = {
|
| 306 |
+
"analysis_keyword": analysis_keyword,
|
| 307 |
+
"analysis_html": final_result
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
return final_result, session_export_data
|
| 311 |
+
|
| 312 |
+
except Exception as e:
|
| 313 |
+
logger.error(f"ํค์๋ ๋ถ์ ์ค ์ ์ฒด ์ค๋ฅ: {e}")
|
| 314 |
+
error_result = generate_error_response(f"ํค์๋ ๋ถ์ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค: {str(e)}")
|
| 315 |
+
return error_result, {}
|
| 316 |
+
|
| 317 |
# ===== ํ์ผ ์ถ๋ ฅ ํจ์๋ค =====
|
| 318 |
def create_timestamp_filename(analysis_keyword):
|
| 319 |
"""ํ์์คํฌํ๊ฐ ํฌํจ๋ ํ์ผ๋ช
์์ฑ - ํ๊ตญ์๊ฐ ์ ์ฉ"""
|
|
|
|
| 521 |
logger.error(f"๋ถ์ ๊ฒฐ๊ณผ ์ถ๋ ฅ ์ค๋ฅ: {e}")
|
| 522 |
return None, f"์ถ๋ ฅ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค: {str(e)}"
|
| 523 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 524 |
# ===== ๊ทธ๋ผ๋์ค ์ธํฐํ์ด์ค =====
|
| 525 |
def create_interface():
|
| 526 |
# CSS ํ์ผ ๋ก๋
|
| 527 |
try:
|
| 528 |
with open('style.css', 'r', encoding='utf-8') as f:
|
| 529 |
custom_css = f.read()
|
| 530 |
+
|
| 531 |
+
with open('keyword_analysis_report.css', 'r', encoding='utf-8') as f:
|
| 532 |
+
keyword_css = f.read()
|
| 533 |
+
custom_css += "\n" + keyword_css
|
| 534 |
except:
|
| 535 |
custom_css = """
|
| 536 |
:root { --primary-color: #FB7F0D; --secondary-color: #ff9a8b; }
|
|
|
|
| 545 |
font-size: 17px !important; font-weight: bold !important; width: 100% !important;
|
| 546 |
margin-top: 20px !important;
|
| 547 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 548 |
"""
|
| 549 |
|
| 550 |
with gr.Blocks(
|
|
|
|
| 597 |
yield create_loading_animation(), {}
|
| 598 |
|
| 599 |
# ์ค์ ํค์๋ ๋ถ์ ์คํ
|
| 600 |
+
keyword_result, session_export_data = safe_keyword_analysis(analysis_keyword)
|
| 601 |
|
| 602 |
# ๐ ๊ฒ์๋ ํธ๋ ๋ ๋ถ์๊ณผ ๐ฏ ํค์๋ ๋ถ์ ํ์
|
| 603 |
yield keyword_result, session_export_data
|
|
|
|
| 605 |
def on_export_results(export_data):
|
| 606 |
"""๋ถ์ ๊ฒฐ๊ณผ ์ถ๋ ฅ ํธ๋ค๋ฌ - ์ธ์
๋ณ ๋ฐ์ดํฐ ์ฒ๋ฆฌ"""
|
| 607 |
try:
|
|
|
|
| 608 |
zip_path, message = export_analysis_results(export_data)
|
| 609 |
|
| 610 |
if zip_path:
|
|
|
|
| 624 |
"""
|
| 625 |
return success_html, gr.update(value=zip_path, visible=True)
|
| 626 |
else:
|
| 627 |
+
# ์คํจ ๋ฉ์์ง
|
| 628 |
+
error_html = f"""
|
| 629 |
+
<div style="background: #f8d7da; border: 1px solid #f5c6cb; padding: 20px; border-radius: 8px; margin: 10px 0;">
|
| 630 |
+
<h4 style="color: #721c24; margin: 0 0 10px 0;"><i class="fas fa-exclamation-triangle"></i> ์ถ๋ ฅ ์คํจ</h4>
|
| 631 |
+
<p style="color: #721c24; margin: 0;">{message}</p>
|
| 632 |
+
</div>
|
| 633 |
+
"""
|
| 634 |
+
return error_html, gr.update(visible=False)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 635 |
|
| 636 |
except Exception as e:
|
| 637 |
logger.error(f"์ถ๋ ฅ ํธ๋ค๋ฌ ์ค๋ฅ: {e}")
|
|
|
|
| 658 |
|
| 659 |
return interface
|
| 660 |
|
| 661 |
+
# ===== API ์ค์ ํ์ธ ํจ์ =====
|
| 662 |
+
def check_datalab_api_config():
|
| 663 |
+
"""๋ค์ด๋ฒ ๋ฐ์ดํฐ๋ฉ API ์ค์ ํ์ธ"""
|
| 664 |
+
logger.info("=== ๋ค์ด๋ฒ ๋ฐ์ดํฐ๋ฉ API ์ค์ ํ์ธ ===")
|
| 665 |
+
|
| 666 |
+
datalab_config = api_utils.get_next_datalab_api_config()
|
| 667 |
+
|
| 668 |
+
if not datalab_config:
|
| 669 |
+
logger.warning("๏ฟฝ๏ฟฝ ๋ฐ์ดํฐ๋ฉ API ํค๊ฐ ์ค์ ๋์ง ์์์ต๋๋ค.")
|
| 670 |
+
logger.info("ํธ๋ ๋ ๋ถ์ ๊ธฐ๋ฅ์ด ๋นํ์ฑํ๋ฉ๋๋ค.")
|
| 671 |
+
return False
|
| 672 |
+
|
| 673 |
+
client_id = datalab_config["CLIENT_ID"]
|
| 674 |
+
client_secret = datalab_config["CLIENT_SECRET"]
|
| 675 |
+
|
| 676 |
+
logger.info(f"์ด {len(api_utils.NAVER_DATALAB_CONFIGS)}๊ฐ์ ๋ฐ์ดํฐ๋ฉ API ์ค์ ์ฌ์ฉ ์ค")
|
| 677 |
+
logger.info(f"ํ์ฌ ์ ํ๋ API:")
|
| 678 |
+
logger.info(f" CLIENT_ID: {client_id[:8]}***{client_id[-4:] if len(client_id) > 12 else '***'}")
|
| 679 |
+
logger.info(f" CLIENT_SECRET: {client_secret[:4]}***{client_secret[-2:] if len(client_secret) > 6 else '***'}")
|
| 680 |
+
|
| 681 |
+
# ๊ธฐ๋ณธ๊ฐ ์ฒดํฌ
|
| 682 |
+
if client_id.startswith("YOUR_"):
|
| 683 |
+
logger.error("โ CLIENT_ID๊ฐ ๊ธฐ๋ณธ๊ฐ์ผ๋ก ์ค์ ๋์ด ์์ต๋๋ค!")
|
| 684 |
+
return False
|
| 685 |
+
|
| 686 |
+
if client_secret.startswith("YOUR_"):
|
| 687 |
+
logger.error("โ CLIENT_SECRET์ด ๊ธฐ๋ณธ๊ฐ์ผ๋ก ์ค์ ๋์ด ์์ต๋๋ค!")
|
| 688 |
+
return False
|
| 689 |
+
|
| 690 |
+
# ๊ธธ์ด ์ฒดํฌ
|
| 691 |
+
if len(client_id) < 10:
|
| 692 |
+
logger.warning("โ ๏ธ CLIENT_ID๊ฐ ์งง์ต๋๋ค. ์ฌ๋ฐ๋ฅธ ํค์ธ์ง ํ์ธํด์ฃผ์ธ์.")
|
| 693 |
+
|
| 694 |
+
if len(client_secret) < 5:
|
| 695 |
+
logger.warning("โ ๏ธ CLIENT_SECRET์ด ์งง์ต๋๋ค. ์ฌ๋ฐ๋ฅธ ํค์ธ์ง ํ์ธํด์ฃผ์ธ์.")
|
| 696 |
+
|
| 697 |
+
logger.info("โ
๋ฐ์ดํฐ๋ฉ API ํค ํ์ ๊ฒ์ฆ ์๋ฃ")
|
| 698 |
+
return True
|
| 699 |
+
|
| 700 |
+
def check_gemini_api_config():
|
| 701 |
+
"""Gemini API ์ค์ ํ์ธ - ๋๋ค ํค ์ ์ฉ"""
|
| 702 |
+
logger.info("=== Gemini API ์ค์ ํ์ธ ===")
|
| 703 |
+
|
| 704 |
+
is_valid, message = api_utils.validate_gemini_config()
|
| 705 |
+
|
| 706 |
+
if is_valid:
|
| 707 |
+
logger.info(f"โ
{message}")
|
| 708 |
+
# ์ฒซ ๋ฒ์งธ ์ฌ์ฉ ๊ฐ๋ฅํ ํค ํ
์คํธ (๋๋ค)
|
| 709 |
+
test_key = api_utils.get_next_gemini_api_key()
|
| 710 |
+
if test_key:
|
| 711 |
+
logger.info(f"ํ์ฌ ์ฌ์ฉ ์ค์ธ Gemini API ํค: {test_key[:8]}***{test_key[-4:]}")
|
| 712 |
+
return True
|
| 713 |
+
else:
|
| 714 |
+
logger.warning(f"โ {message}")
|
| 715 |
+
logger.info("AI ๋ถ์ ๊ธฐ๋ฅ์ด ์ ํ๋ ์ ์์ต๋๋ค.")
|
| 716 |
+
return False
|
| 717 |
+
|
| 718 |
# ===== ๋ฉ์ธ ์คํ =====
|
| 719 |
if __name__ == "__main__":
|
| 720 |
# pytz ๋ชจ๋ ์ค์น ํ์ธ
|
| 721 |
+
if PYTZ_AVAILABLE:
|
|
|
|
| 722 |
logger.info("โ
pytz ๋ชจ๋ ๋ก๋ ์ฑ๊ณต - ํ๊ตญ์๊ฐ ์ง์")
|
| 723 |
+
else:
|
| 724 |
logger.warning("โ ๏ธ pytz ๋ชจ๋์ด ์ค์น๋์ง ์์ - pip install pytz ์คํ ํ์")
|
| 725 |
logger.info("์์คํ
์๊ฐ์ ์ฌ์ฉํฉ๋๋ค.")
|
| 726 |
|
| 727 |
+
# API ์ค์ ์ด๊ธฐํ
|
| 728 |
+
api_utils.initialize_api_configs()
|
| 729 |
+
logger.info("===== ์ํ ์์ฑ ๋ถ์ ์์คํ
v2.10 (๊ฐ๋ต๋ฒ์ + ์ถ๋ ฅ๊ธฐ๋ฅ + ๋๋คํค + ๋ฉํฐ์ฌ์ฉ์ ์์ ) ์์ =====")
|
| 730 |
+
|
| 731 |
+
# ๋ค์ด๋ฒ ๋ฐ์ดํฐ๋ฉ API ์ค์ ํ์ธ
|
| 732 |
+
datalab_available = check_datalab_api_config()
|
| 733 |
+
|
| 734 |
+
# Gemini API ์ค์ ํ์ธ (๋๋ค ํค)
|
| 735 |
+
gemini_available = check_gemini_api_config()
|
| 736 |
|
| 737 |
# ํ์ํ ํจํค์ง ์๋ด
|
| 738 |
print("๐ฆ ํ์ํ ํจํค์ง:")
|
| 739 |
+
print(" pip install gradio google-generativeai pandas requests xlsxwriter markdown plotly pytz")
|
| 740 |
print()
|
| 741 |
|
| 742 |
+
# API ํค ์ค์ ์๋ด
|
| 743 |
+
if not gemini_available:
|
| 744 |
+
print("โ ๏ธ GEMINI_API_KEY ๋๋ GOOGLE_API_KEY ํ๊ฒฝ๋ณ์๋ฅผ ์ค์ ํ์ธ์.")
|
| 745 |
+
print(" export GEMINI_API_KEY='your-api-key'")
|
| 746 |
+
print(" ๋๋")
|
| 747 |
+
print(" export GOOGLE_API_KEY='your-api-key'")
|
| 748 |
+
print()
|
| 749 |
+
|
| 750 |
+
if not datalab_available:
|
| 751 |
+
print("โ ๏ธ ๋ค์ด๋ฒ ๋ฐ์ดํฐ๋ฉ API ํธ๋ ๋ ๋ถ์์ ์ํด์๋:")
|
| 752 |
+
print(" 1. ๋ค์ด๋ฒ ๊ฐ๋ฐ์์ผํฐ(https://developers.naver.com)์์ ์ ํ๋ฆฌ์ผ์ด์
๋ฑ๋ก")
|
| 753 |
+
print(" 2. '๋ฐ์ดํฐ๋ฉ(๊ฒ์์ด ํธ๋ ๋)' API ์ถ๊ฐ")
|
| 754 |
+
print(" 3. ๋ฐ๊ธ๋ฐ์ CLIENT_ID์ CLIENT_SECRET์ api_utils.py์ NAVER_DATALAB_CONFIGS์ ์ค์ ")
|
| 755 |
+
print(" 4. ํ์ฌ๋ ํ์ฌ ๊ฒ์๋ ์ ๋ณด๋ง ํ์๋ฉ๋๋ค.")
|
| 756 |
print()
|
| 757 |
else:
|
| 758 |
+
print("โ
๋ฐ์ดํฐ๋ฉ API ์ค์ ์๋ฃ - 1๋
, 3๋
ํธ๋ ๋ ๋ถ์์ด ๊ฐ๋ฅํฉ๋๋ค!")
|
| 759 |
+
print()
|
| 760 |
+
|
| 761 |
+
if gemini_available:
|
| 762 |
+
print("โ
Gemini API ์ค์ ์๋ฃ - ๋๋ค ํค ๋กํ
์ด์
์ ์ฉ๋ฉ๋๋ค!")
|
| 763 |
print()
|
| 764 |
|
| 765 |
+
print("๐ v2.10 ๊ฐ์ ์ฌํญ:")
|
| 766 |
+
print(" โข 2๋จ๊ณ: ์์ง๋ ํค์๋ ๋ชฉ๋ก ๊ธฐ๋ฅ ๏ฟฝ๏ฟฝ๏ฟฝ๊ฑฐ")
|
| 767 |
+
print(" โข 3๋จ๊ณ: ์ฐ๊ด๊ฒ์์ด ๋ถ์์ ์ํ์ถ์ถ ๋ฐ ๋ถ์ ๊ธฐ๋ฅ ์ ๊ฑฐ")
|
| 768 |
+
print(" โข 3๋จ๊ณ: '๋ถ์ํ ํค์๋ ์ ํ' โ 'ํค์๋ ์ฌ์ธต๋ถ์ ์
๋ ฅ'์ผ๋ก ๋ช
์นญ ๋ณ๊ฒฝ")
|
| 769 |
+
print(" โข ๐ ๊ฒ์๋ ํธ๋ ๋ ๋ถ์๊ณผ ๐ฏ ํค์๋ ๋ถ์๋ง ํ์")
|
| 770 |
+
print(" โข โ
์ถ๋ ฅ ๊ธฐ๋ฅ ์ถ๊ฐ: HTML ํ์ผ ์์ฑ ๋ฐ ZIP ๋ค์ด๋ก๋")
|
| 771 |
+
print(" โข โ
Gemini API ํค ๋๋ค ๋กํ
์ด์
์ ์ฉ")
|
| 772 |
+
print(" โข โ
๋ค์ด๋ฒ ๋ฐ์ดํฐ๋ฉ API ํค ๋๋ค ๋กํ
์ด์
์ ์ฉ")
|
| 773 |
print(" โข โ
ํ๊ตญ์๊ฐ ๊ธฐ์ค ํ์ผ๋ช
์์ฑ")
|
| 774 |
print(" โข โ
๋ฉํฐ ์ฌ์ฉ์ ์์ : gr.State๋ก ์ธ์
๋ณ ๋ฐ์ดํฐ ๊ด๋ฆฌ")
|
| 775 |
+
print(" โข ๋ถํ์ํ ๋ชจ๋ ์ํฌํธ ์ ๊ฑฐ๋ก ์์ ์ฑ ํฅ์")
|
|
|
|
| 776 |
print()
|
| 777 |
|
| 778 |
# ์ฑ ์คํ
|
export_utils.py
ADDED
|
@@ -0,0 +1,478 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
๊ฒฐ๊ณผ ์ถ๋ ฅ ๊ด๋ จ ์ ํธ๋ฆฌํฐ ํจ์ ๋ชจ์ - ์นดํ
๊ณ ๋ฆฌ ํญ๋ชฉ ์ ๊ฑฐ
|
| 3 |
+
- HTML ํ
์ด๋ธ ์์ฑ
|
| 4 |
+
- ์์
ํ์ผ ์์ฑ
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import pandas as pd
|
| 8 |
+
import tempfile
|
| 9 |
+
import os
|
| 10 |
+
import threading
|
| 11 |
+
import time
|
| 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 |
+
# ์์ ํ์ผ ์ถ์ ๋ฆฌ์คํธ
|
| 23 |
+
_temp_files = []
|
| 24 |
+
|
| 25 |
+
def create_table_without_checkboxes(df):
|
| 26 |
+
"""DataFrame์ HTML ํ
์ด๋ธ๋ก ๋ณํ - ํค์๋ ํด๋ฆญ ์ ๋ค์ด๋ฒ ์ผํ ์ด๋ ๊ธฐ๋ฅ ์ถ๊ฐ"""
|
| 27 |
+
if df.empty:
|
| 28 |
+
return "<p>๊ฒ์ ๊ฒฐ๊ณผ๊ฐ ์์ต๋๋ค.</p>"
|
| 29 |
+
|
| 30 |
+
# === ์์ ๋ ๋ถ๋ถ: ์นดํ
๊ณ ๋ฆฌ ๊ด๋ จ ์ด ์ ๊ฑฐ ===
|
| 31 |
+
df_display = df.copy()
|
| 32 |
+
|
| 33 |
+
# "์ํ ๋ฑ๋ก ์นดํ
๊ณ ๋ฆฌ(์์100์)" ๋๋ "๊ด๋ จ ์นดํ
๊ณ ๋ฆฌ", "์นดํ
๊ณ ๋ฆฌ ํญ๋ชฉ" ์ด์ด ์์ผ๋ฉด ์ ๊ฑฐ
|
| 34 |
+
columns_to_remove = ["์ํ ๋ฑ๋ก ์นดํ
๊ณ ๋ฆฌ(์์100์)", "๊ด๋ จ ์นดํ
๊ณ ๋ฆฌ", "์นดํ
๊ณ ๋ฆฌ ํญ๋ชฉ"]
|
| 35 |
+
for col in columns_to_remove:
|
| 36 |
+
if col in df_display.columns:
|
| 37 |
+
df_display = df_display.drop(columns=[col])
|
| 38 |
+
logger.info(f"ํ
์ด๋ธ์์ '{col}' ์ด ์ ๊ฑฐ๋จ")
|
| 39 |
+
|
| 40 |
+
# HTML ํ
์ด๋ธ ์คํ์ผ ์ ์ - Z-INDEX ์์
|
| 41 |
+
html = '''
|
| 42 |
+
<style>
|
| 43 |
+
.table-container {
|
| 44 |
+
position: relative;
|
| 45 |
+
width: 100%;
|
| 46 |
+
margin: 0;
|
| 47 |
+
border-radius: 8px;
|
| 48 |
+
overflow: hidden;
|
| 49 |
+
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
.header-wrap {
|
| 53 |
+
position: sticky;
|
| 54 |
+
top: 0;
|
| 55 |
+
z-index: 100; /* z-index ์ฆ๊ฐ */
|
| 56 |
+
background-color: #009879;
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
.styled-table {
|
| 60 |
+
width: 100%;
|
| 61 |
+
border-collapse: collapse;
|
| 62 |
+
table-layout: fixed;
|
| 63 |
+
margin: 0;
|
| 64 |
+
padding: 0;
|
| 65 |
+
font-size: 14px;
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
.styled-table th,
|
| 69 |
+
.styled-table td {
|
| 70 |
+
padding: 12px 15px;
|
| 71 |
+
text-align: left;
|
| 72 |
+
border-bottom: 1px solid #dddddd;
|
| 73 |
+
overflow: hidden;
|
| 74 |
+
text-overflow: ellipsis;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
/* ๊ธด ํ
์คํธ๊ฐ ์
์์ ์ค๋ฐ๊ฟ๋๋๋ก ์์ */
|
| 78 |
+
.styled-table td.col-rank {
|
| 79 |
+
white-space: normal;
|
| 80 |
+
word-break: break-word;
|
| 81 |
+
line-height: 1.3;
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
/* ๊ทธ ์ธ ์ด์ ํ ์ค๋ก ํ์ */
|
| 85 |
+
.styled-table td.col-seq,
|
| 86 |
+
.styled-table td.col-keyword,
|
| 87 |
+
.styled-table td.col-pc,
|
| 88 |
+
.styled-table td.col-mobile,
|
| 89 |
+
.styled-table td.col-total,
|
| 90 |
+
.styled-table td.col-range,
|
| 91 |
+
.styled-table td.col-count {
|
| 92 |
+
white-space: nowrap;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
.styled-table th {
|
| 96 |
+
background-color: #009879;
|
| 97 |
+
color: white;
|
| 98 |
+
font-weight: bold;
|
| 99 |
+
position: sticky;
|
| 100 |
+
top: 0;
|
| 101 |
+
white-space: nowrap;
|
| 102 |
+
z-index: 50; /* ํค๋ z-index ์ฆ๊ฐ */
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
.styled-table tbody tr:nth-of-type(even) {
|
| 106 |
+
background-color: #f3f3f3;
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
.styled-table tbody tr:hover {
|
| 110 |
+
background-color: #f0f0f0;
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
.styled-table tbody tr:last-of-type {
|
| 114 |
+
border-bottom: 2px solid #009879;
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
/* ๋ฐ์ดํฐ ์
z-index ์ค์ */
|
| 118 |
+
.styled-table tbody td {
|
| 119 |
+
position: relative;
|
| 120 |
+
z-index: 1; /* ๋ฐ์ดํฐ ์
์ ๋ฎ์ z-index */
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
.data-container {
|
| 124 |
+
max-height: 600px;
|
| 125 |
+
overflow-y: auto;
|
| 126 |
+
position: relative; /* position ์ถ๊ฐ */
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
/* ์คํฌ๋กค๋ฐ ์คํ์ผ */
|
| 130 |
+
.data-container::-webkit-scrollbar {
|
| 131 |
+
width: 10px;
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
.data-container::-webkit-scrollbar-track {
|
| 135 |
+
background: #f1f1f1;
|
| 136 |
+
border-radius: 5px;
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
.data-container::-webkit-scrollbar-thumb {
|
| 140 |
+
background: #888;
|
| 141 |
+
border-radius: 5px;
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
.data-container::-webkit-scrollbar-thumb:hover {
|
| 145 |
+
background: #555;
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
/* ํค์๋ ๋งํฌ ์คํ์ผ - ์๋ก ์ถ๊ฐ */
|
| 149 |
+
.keyword-link {
|
| 150 |
+
color: #2c5aa0;
|
| 151 |
+
text-decoration: none;
|
| 152 |
+
font-weight: 600;
|
| 153 |
+
cursor: pointer;
|
| 154 |
+
transition: all 0.3s ease;
|
| 155 |
+
display: inline-block;
|
| 156 |
+
padding: 2px 4px;
|
| 157 |
+
border-radius: 3px;
|
| 158 |
+
position: relative;
|
| 159 |
+
z-index: 5; /* ๋งํฌ z-index ์ค์ */
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
.keyword-link:hover {
|
| 163 |
+
color: #ffffff;
|
| 164 |
+
background-color: #2c5aa0;
|
| 165 |
+
text-decoration: none;
|
| 166 |
+
transform: translateY(-1px);
|
| 167 |
+
box-shadow: 0 2px 4px rgba(44, 90, 160, 0.3);
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
.keyword-link:active {
|
| 171 |
+
transform: translateY(0px);
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
/* ํค์๋ ์
ํน๋ณ ์คํ์ผ */
|
| 175 |
+
.col-keyword {
|
| 176 |
+
position: relative;
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
.keyword-tooltip {
|
| 180 |
+
position: absolute;
|
| 181 |
+
bottom: 100%;
|
| 182 |
+
left: 50%;
|
| 183 |
+
transform: translateX(-50%);
|
| 184 |
+
background-color: #333;
|
| 185 |
+
color: white;
|
| 186 |
+
padding: 6px 10px;
|
| 187 |
+
border-radius: 4px;
|
| 188 |
+
font-size: 11px;
|
| 189 |
+
white-space: nowrap;
|
| 190 |
+
opacity: 0;
|
| 191 |
+
visibility: hidden;
|
| 192 |
+
transition: all 0.3s ease;
|
| 193 |
+
z-index: 1000; /* ํดํ์ ๊ฐ์ฅ ๋์ z-index */
|
| 194 |
+
pointer-events: none;
|
| 195 |
+
margin-bottom: 5px;
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
.keyword-tooltip::after {
|
| 199 |
+
content: '';
|
| 200 |
+
position: absolute;
|
| 201 |
+
top: 100%;
|
| 202 |
+
left: 50%;
|
| 203 |
+
transform: translateX(-50%);
|
| 204 |
+
border: 4px solid transparent;
|
| 205 |
+
border-top-color: #333;
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
.keyword-link:hover .keyword-tooltip {
|
| 209 |
+
opacity: 1;
|
| 210 |
+
visibility: visible;
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
/* === ์์ ๋ ๋ถ๋ถ: ์ด ๋๋น ์ ์ - ์นดํ
๊ณ ๋ฆฌ ์ด ์ ๊ฑฐ ํ ์กฐ์ === */
|
| 214 |
+
.col-seq { width: 8%; }
|
| 215 |
+
.col-keyword { width: 25%; }
|
| 216 |
+
.col-pc { width: 12%; }
|
| 217 |
+
.col-mobile { width: 12%; }
|
| 218 |
+
.col-total { width: 12%; }
|
| 219 |
+
.col-range { width: 12%; }
|
| 220 |
+
.col-rank { width: 15%; }
|
| 221 |
+
.col-count { width: 10%; }
|
| 222 |
+
|
| 223 |
+
.truncated-text {
|
| 224 |
+
position: relative;
|
| 225 |
+
cursor: pointer;
|
| 226 |
+
z-index: 2; /* ํ
์คํธ z-index ์ค์ */
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
.truncated-text:hover::after {
|
| 230 |
+
content: attr(data-full-text);
|
| 231 |
+
position: absolute;
|
| 232 |
+
left: 0;
|
| 233 |
+
top: 100%;
|
| 234 |
+
z-index: 99;
|
| 235 |
+
min-width: 200px;
|
| 236 |
+
max-width: 400px;
|
| 237 |
+
padding: 8px;
|
| 238 |
+
background-color: #fff;
|
| 239 |
+
border: 1px solid #ddd;
|
| 240 |
+
border-radius: 4px;
|
| 241 |
+
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
|
| 242 |
+
white-space: normal;
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
/* ํค์๋ ํ๊ทธ ์คํ์ผ */
|
| 246 |
+
.keyword-tag-container {
|
| 247 |
+
margin-top: 20px;
|
| 248 |
+
padding: 10px;
|
| 249 |
+
border: 1px solid #ddd;
|
| 250 |
+
border-radius: 5px;
|
| 251 |
+
background-color: #f9f9f9;
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
.keyword-tag {
|
| 255 |
+
display: inline-block;
|
| 256 |
+
background-color: #009879;
|
| 257 |
+
color: white;
|
| 258 |
+
padding: 5px 10px;
|
| 259 |
+
margin: 5px;
|
| 260 |
+
border-radius: 15px;
|
| 261 |
+
font-size: 12px;
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
.category-tag {
|
| 265 |
+
display: inline-block;
|
| 266 |
+
background-color: #2c7fb8;
|
| 267 |
+
color: white;
|
| 268 |
+
padding: 5px 10px;
|
| 269 |
+
margin: 5px;
|
| 270 |
+
border-radius: 15px;
|
| 271 |
+
font-size: 12px;
|
| 272 |
+
}
|
| 273 |
+
|
| 274 |
+
/* ๋ถ์ ๊ฒฐ๊ณผ ํ
์ด๋ธ ์คํ์ผ */
|
| 275 |
+
.analysis-result {
|
| 276 |
+
margin-top: 30px;
|
| 277 |
+
border: 1px solid #ddd;
|
| 278 |
+
border-radius: 5px;
|
| 279 |
+
padding: 15px;
|
| 280 |
+
background-color: #f9f9f9;
|
| 281 |
+
}
|
| 282 |
+
|
| 283 |
+
.result-header {
|
| 284 |
+
font-weight: bold;
|
| 285 |
+
margin-bottom: 10px;
|
| 286 |
+
color: #009879;
|
| 287 |
+
}
|
| 288 |
+
|
| 289 |
+
.match-item {
|
| 290 |
+
margin: 5px 0;
|
| 291 |
+
padding: 5px;
|
| 292 |
+
border-bottom: 1px solid #eee;
|
| 293 |
+
}
|
| 294 |
+
|
| 295 |
+
.match-keyword {
|
| 296 |
+
font-weight: bold;
|
| 297 |
+
color: #2c7fb8;
|
| 298 |
+
}
|
| 299 |
+
|
| 300 |
+
.match-count {
|
| 301 |
+
display: inline-block;
|
| 302 |
+
background-color: #009879;
|
| 303 |
+
color: white;
|
| 304 |
+
padding: 2px 8px;
|
| 305 |
+
border-radius: 10px;
|
| 306 |
+
font-size: 12px;
|
| 307 |
+
margin-left: 10px;
|
| 308 |
+
}
|
| 309 |
+
</style>
|
| 310 |
+
'''
|
| 311 |
+
|
| 312 |
+
# === ์์ ๋ ๋ถ๋ถ: ์ด ์ด๋ฆ๊ณผ ํด๋์ค ๋งคํ - ์นดํ
๊ณ ๋ฆฌ ๊ด๋ จ ์ ๊ฑฐ ===
|
| 313 |
+
col_mapping = {
|
| 314 |
+
"์๋ฒ": "col-seq",
|
| 315 |
+
"์กฐํฉ ํค์๋": "col-keyword",
|
| 316 |
+
"PC๊ฒ์๋": "col-pc",
|
| 317 |
+
"๋ชจ๋ฐ์ผ๊ฒ์๋": "col-mobile",
|
| 318 |
+
"์ด๊ฒ์๋": "col-total",
|
| 319 |
+
"๊ฒ์๋๊ตฌ๊ฐ": "col-range",
|
| 320 |
+
"ํค์๋ ์ฌ์ฉ์์์": "col-rank",
|
| 321 |
+
"ํค์๋ ์ฌ์ฉํ์": "col-count"
|
| 322 |
+
# ์นดํ
๊ณ ๋ฆฌ ๊ด๋ จ ๋งคํ ์ ๊ฑฐ๋จ
|
| 323 |
+
}
|
| 324 |
+
|
| 325 |
+
# ํ
์ด๋ธ ์ปจํ
์ด๋ ์์
|
| 326 |
+
html += '<div class="table-container">'
|
| 327 |
+
|
| 328 |
+
# ๋จ์ผ ํ
์ด๋ธ ๊ตฌ์กฐ๋ก ๋ณ๊ฒฝ (ํค๋๋ position: sticky๋ก ๊ณ ์ )
|
| 329 |
+
html += '<div class="data-container">'
|
| 330 |
+
html += '<table class="styled-table">'
|
| 331 |
+
|
| 332 |
+
# colgroup์ผ๋ก ์ด ๋๋น ์ ์
|
| 333 |
+
html += '<colgroup>'
|
| 334 |
+
html += f'<col class="{col_mapping["์๋ฒ"]}">'
|
| 335 |
+
for col in df_display.columns:
|
| 336 |
+
col_class = col_mapping.get(col, "")
|
| 337 |
+
html += f'<col class="{col_class}">'
|
| 338 |
+
html += '</colgroup>'
|
| 339 |
+
|
| 340 |
+
# ํ
์ด๋ธ ํค๋
|
| 341 |
+
html += '<thead>'
|
| 342 |
+
html += '<tr>'
|
| 343 |
+
html += f'<th class="{col_mapping["์๋ฒ"]}">์๋ฒ</th>'
|
| 344 |
+
for col in df_display.columns:
|
| 345 |
+
col_class = col_mapping.get(col, "")
|
| 346 |
+
html += f'<th class="{col_class}">{col}</th>'
|
| 347 |
+
html += '</tr>'
|
| 348 |
+
html += '</thead>'
|
| 349 |
+
|
| 350 |
+
# ํ
์ด๋ธ ๋ณธ๋ฌธ
|
| 351 |
+
html += '<tbody>'
|
| 352 |
+
for idx, row in df_display.iterrows():
|
| 353 |
+
html += '<tr>'
|
| 354 |
+
# ์๋ฒ ํ์ - 1๋ถํฐ ์์ํ๋ ์์ฐจ์ ๋ฒํธ
|
| 355 |
+
html += f'<td class="{col_mapping["์๋ฒ"]}">{idx + 1}</td>'
|
| 356 |
+
|
| 357 |
+
# ๋ฐ์ดํฐ ์
์ถ๊ฐ
|
| 358 |
+
for col in df_display.columns:
|
| 359 |
+
col_class = col_mapping.get(col, "")
|
| 360 |
+
value = str(row[col])
|
| 361 |
+
|
| 362 |
+
if col == "ํค์๋ ์ฌ์ฉ์์์":
|
| 363 |
+
# ๊ธด ํ
์คํธ์ ์
์ ๊ทธ๋๋ก ํ์ (์ค๋ฐ๊ฟ ํ์ฉ)
|
| 364 |
+
html += f'<td class="{col_class}">{value}</td>'
|
| 365 |
+
elif len(value) > 30:
|
| 366 |
+
# ๋ค๋ฅธ ๊ธด ํ
์คํธ๋ hover๋ก ์ ์ฒด ํ์
|
| 367 |
+
html += f'<td class="{col_class}"><div class="truncated-text" data-full-text="{value}">{value[:30]}...</div></td>'
|
| 368 |
+
else:
|
| 369 |
+
# ์ผ๋ฐ ํ
์คํธ
|
| 370 |
+
html += f'<td class="{col_class}">{value}</td>'
|
| 371 |
+
html += '</tr>'
|
| 372 |
+
|
| 373 |
+
html += '</tbody>'
|
| 374 |
+
html += '</table>'
|
| 375 |
+
html += '</div>' # data-container ๋ซ๊ธฐ
|
| 376 |
+
html += '</div>' # table-container ๋ซ๊ธฐ
|
| 377 |
+
|
| 378 |
+
return html
|
| 379 |
+
|
| 380 |
+
def cleanup_temp_files(delay=300):
|
| 381 |
+
"""์์ ํ์ผ ์ ๋ฆฌ ํจ์"""
|
| 382 |
+
global _temp_files
|
| 383 |
+
|
| 384 |
+
def cleanup():
|
| 385 |
+
time.sleep(delay) # ์ง์ ๋ ์๊ฐ ๋๊ธฐ
|
| 386 |
+
temp_files_to_remove = _temp_files.copy()
|
| 387 |
+
_temp_files = []
|
| 388 |
+
|
| 389 |
+
for file_path in temp_files_to_remove:
|
| 390 |
+
try:
|
| 391 |
+
if os.path.exists(file_path):
|
| 392 |
+
os.remove(file_path)
|
| 393 |
+
logger.info(f"์์ ํ์ผ ์ญ์ : {file_path}")
|
| 394 |
+
except Exception as e:
|
| 395 |
+
logger.error(f"ํ์ผ ์ญ์ ์ค๋ฅ: {e}")
|
| 396 |
+
|
| 397 |
+
# ์ ์ค๋ ๋ ์์
|
| 398 |
+
threading.Thread(target=cleanup, daemon=True).start()
|
| 399 |
+
|
| 400 |
+
def download_keywords(df, auto_cleanup=True, cleanup_delay=300):
|
| 401 |
+
"""ํค์๋ ๋ฐ์ดํฐ๋ฅผ ์์
ํ์ผ๋ก ๋ค์ด๋ก๋ - ์นดํ
๊ณ ๋ฆฌ ํญ๋ชฉ ์ ๊ฑฐ"""
|
| 402 |
+
global _temp_files
|
| 403 |
+
|
| 404 |
+
if df is None or df.empty:
|
| 405 |
+
return None
|
| 406 |
+
|
| 407 |
+
# ์์ ํ์ผ๋ก ์ ์ฅ
|
| 408 |
+
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.xlsx')
|
| 409 |
+
temp_file.close()
|
| 410 |
+
filename = temp_file.name
|
| 411 |
+
|
| 412 |
+
# ์์ ํ์ผ ์ถ์ ๋ชฉ๋ก์ ์ถ๊ฐ
|
| 413 |
+
_temp_files.append(filename)
|
| 414 |
+
|
| 415 |
+
# === ์์ ๋ ๋ถ๋ถ: ์นดํ
๊ณ ๋ฆฌ ๊ด๋ จ ์ด ์ ๊ฑฐ ===
|
| 416 |
+
df_export = df.copy()
|
| 417 |
+
|
| 418 |
+
# ์นดํ
๊ณ ๋ฆฌ ๊ด๋ จ ์ด๋ค ์ ๊ฑฐ
|
| 419 |
+
columns_to_remove = ["์ํ ๋ฑ๋ก ์นดํ
๊ณ ๋ฆฌ(์์100์)", "๊ด๋ จ ์นดํ
๊ณ ๋ฆฌ", "์นดํ
๊ณ ๋ฆฌ ํญ๋ชฉ"]
|
| 420 |
+
for col in columns_to_remove:
|
| 421 |
+
if col in df_export.columns:
|
| 422 |
+
df_export = df_export.drop(columns=[col])
|
| 423 |
+
logger.info(f"์์
๋ด๋ณด๋ด๊ธฐ์์ '{col}' ์ด ์ ๊ฑฐ๋จ")
|
| 424 |
+
|
| 425 |
+
# ํค์๋ ๋ฐ์ดํฐ๋ฅผ ์์
ํ์ผ๋ก ์ ์ฅ
|
| 426 |
+
with pd.ExcelWriter(filename, engine='xlsxwriter') as writer:
|
| 427 |
+
# ํค์๋ ๋ชฉ๋ก ์ํธ
|
| 428 |
+
df_export.to_excel(writer, sheet_name='ํค์๋ ๋ชฉ๋ก', index=False)
|
| 429 |
+
|
| 430 |
+
# ์ด ๋๋น ์กฐ์ - ์นดํ
๊ณ ๋ฆฌ ์ด ์ ๊ฑฐ ํ ์กฐ์
|
| 431 |
+
worksheet = writer.sheets['ํค์๋ ๋ชฉ๋ก']
|
| 432 |
+
worksheet.set_column('A:A', 20) # ์กฐํฉ ํค์๋ ์ด
|
| 433 |
+
worksheet.set_column('B:B', 12) # PC๊ฒ์๋ ์ด
|
| 434 |
+
worksheet.set_column('C:C', 12) # ๋ชจ๋ฐ์ผ๊ฒ์๋ ์ด
|
| 435 |
+
worksheet.set_column('D:D', 12) # ์ด๊ฒ์๋ ์ด
|
| 436 |
+
worksheet.set_column('E:E', 12) # ๊ฒ์๋๊ตฌ๊ฐ ์ด
|
| 437 |
+
worksheet.set_column('F:F', 20) # ํค์๋ ์ฌ์ฉ์์์ ์ด
|
| 438 |
+
worksheet.set_column('G:G', 12) # ํค์๋ ์ฌ์ฉํ์ ์ด
|
| 439 |
+
# ์นดํ
๊ณ ๋ฆฌ ์ด๋ค ์ ๊ฑฐ๋ก H, I ์ด ์ค์ ์ ๊ฑฐ๋จ
|
| 440 |
+
|
| 441 |
+
# ํค๋ ํ์ ์ค์
|
| 442 |
+
header_format = writer.book.add_format({
|
| 443 |
+
'bold': True,
|
| 444 |
+
'bg_color': '#009879',
|
| 445 |
+
'color': 'white',
|
| 446 |
+
'border': 1
|
| 447 |
+
})
|
| 448 |
+
|
| 449 |
+
# ํค๋์ ํ์ ์ ์ฉ
|
| 450 |
+
for col_num, value in enumerate(df_export.columns.values):
|
| 451 |
+
worksheet.write(0, col_num, value, header_format)
|
| 452 |
+
|
| 453 |
+
logger.info(f"์์
ํ์ผ ์์ฑ: {filename}")
|
| 454 |
+
|
| 455 |
+
# ํ์ผ ์๋ ์ ๋ฆฌ ์ต์
|
| 456 |
+
if auto_cleanup:
|
| 457 |
+
# ๋ณ๋ ์ ๋ฆฌ ์์
์์ฒญ ์์ด ์ถ์ ๋ชฉ๋ก์ ์ถ๊ฐ๋ง ํ์ฌ ์ผ๊ด ์ฒ๋ฆฌ
|
| 458 |
+
pass
|
| 459 |
+
|
| 460 |
+
return filename
|
| 461 |
+
|
| 462 |
+
def register_cleanup_handlers():
|
| 463 |
+
"""์ฑ ์ข
๋ฃ ์ ์ ๋ฆฌ๋ฅผ ์ํ ํธ๋ค๋ฌ ๋ฑ๋ก"""
|
| 464 |
+
import atexit
|
| 465 |
+
|
| 466 |
+
def cleanup_all_temp_files():
|
| 467 |
+
global _temp_files
|
| 468 |
+
for file_path in _temp_files:
|
| 469 |
+
try:
|
| 470 |
+
if os.path.exists(file_path):
|
| 471 |
+
os.remove(file_path)
|
| 472 |
+
logger.info(f"์ข
๋ฃ ์ ์์ ํ์ผ ์ญ์ : {file_path}")
|
| 473 |
+
except Exception as e:
|
| 474 |
+
logger.error(f"ํ์ผ ์ญ์ ์ค๋ฅ: {e}")
|
| 475 |
+
_temp_files = []
|
| 476 |
+
|
| 477 |
+
# ์ฑ ์ข
๋ฃ ์ ์คํ๋ ํจ์ ๋ฑ๋ก
|
| 478 |
+
atexit.register(cleanup_all_temp_files)
|
keyword_analysis.py
ADDED
|
@@ -0,0 +1,1773 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
|
| 747 |
+
์๋ณ ๊ฒ์๋ ๋ฐ์ดํฐ (์ต๊ทผ 12๊ฐ์):
|
| 748 |
+
{monthly_data_str}
|
| 749 |
+
|
| 750 |
+
๋ค์ ํ์์ผ๋ก๋ง ๋ต๋ณํ์ธ์:
|
| 751 |
+
|
| 752 |
+
์ํ์ ํ: [๋ด์์ฆ์ํ/์ฌ๋ฆ์์ฆ์ํ/๊ฐ์์์ฆ์ํ/๊ฒจ์ธ์์ฆ์ํ/๋น์์ฆ์ํ/ํฌ๋ฆฌ์ค๋ง์ค์ด๋ฒคํธ์ํ/๋ฐธ๋ฐํ์ธ์ด๋ฒคํธ์ํ/์ด๋ฒ์ด๋ ์ด๋ฒคํธ์ํ/์ํ๊ธฐ์ด๋ฒคํธ์ํ/๊ธฐํ์ด๋ฒคํธ์ํ]
|
| 753 |
+
ํผํฌ์: [X์] (๊ฒ์๋์ด ๊ฐ์ฅ ๋์ ์, ์ค์ ์์น ํฌํจ)
|
| 754 |
+
์ฑ์ฅ์: [X์] (์ฆ๊ฐํญ์ด ๊ฐ์ฅ ๋์ ์)
|
| 755 |
+
ํ์ฌ์ํ: {current_month}์ ๊ธฐ์ค [๋์
๊ธฐ/์ฑ์ฅ๊ธฐ/์์ ๊ธฐ/์ ํด๊ธฐ/๋น์์ฆ๊ธฐ๊ฐ]
|
| 756 |
+
์ง์
์ถ์ฒ: [๊ตฌ์ฒด์ ์ ์ ์]
|
| 757 |
+
"""
|
| 758 |
+
|
| 759 |
+
response = gemini_model.generate_content(prompt)
|
| 760 |
+
result_text = response.text.strip()
|
| 761 |
+
|
| 762 |
+
lines = result_text.split('\n')
|
| 763 |
+
product_type = "๋น์์ฆ์ํ"
|
| 764 |
+
peak_month = max_month if max_month else "์ฐ์ค"
|
| 765 |
+
growth_month = "์ฐ์ค"
|
| 766 |
+
current_status = "์์ ๊ธฐ"
|
| 767 |
+
entry_recommendation = "์ธ์ ๋ ์ง ์ง์
๊ฐ๋ฅ"
|
| 768 |
+
|
| 769 |
+
for line in lines:
|
| 770 |
+
line = line.strip()
|
| 771 |
+
if line.startswith('์ํ์ ํ:'):
|
| 772 |
+
product_type = line.replace('์ํ์ ํ:', '').strip()
|
| 773 |
+
elif line.startswith('ํผํฌ์:'):
|
| 774 |
+
extracted_peak = line.replace('ํผํฌ์:', '').strip()
|
| 775 |
+
if '(' in extracted_peak and ')' in extracted_peak:
|
| 776 |
+
peak_month = extracted_peak
|
| 777 |
+
else:
|
| 778 |
+
peak_month = max_month if max_month else extracted_peak
|
| 779 |
+
elif line.startswith('์ฑ์ฅ์:'):
|
| 780 |
+
growth_month = line.replace('์ฑ์ฅ์:', '').strip()
|
| 781 |
+
elif line.startswith('ํ์ฌ์ํ:'):
|
| 782 |
+
current_status = line.replace('ํ์ฌ์ํ:', '').strip()
|
| 783 |
+
elif line.startswith('์ง์
์ถ์ฒ:'):
|
| 784 |
+
entry_recommendation = line.replace('์ง์
์ถ์ฒ:', '').strip()
|
| 785 |
+
|
| 786 |
+
detail_info = f"์ํ์ ํ: {product_type} | ํผํฌ์: {peak_month} | ์ฑ์ฅ์: {growth_month} | ํ์ฌ์ํ: {current_status}"
|
| 787 |
+
|
| 788 |
+
logger.info(f"LLM ์์ฆ ๋ถ์ ์๋ฃ: {product_type}, {entry_recommendation}")
|
| 789 |
+
return product_type, entry_recommendation, detail_info
|
| 790 |
+
|
| 791 |
+
except Exception as e:
|
| 792 |
+
logger.error(f"LLM ์์ฆ ์ฌ์ดํด ๋ถ์ ์ค๋ฅ: {e}")
|
| 793 |
+
return "๋น์์ฆ์ํ", "์ธ์ ๋ ์ง ์ง์
๊ฐ๋ฅ", "LLM ๋ถ์ ์ค๋ฅ"
|
| 794 |
+
|
| 795 |
+
def analyze_sourcing_strategy_improved(keyword, volume_data, trend_data_1year, trend_data_3year, filtered_keywords_df, gemini_model):
|
| 796 |
+
"""๊ฐ์ ๋ ์์ฑ์ ๋ต ๋ถ์ - ํฌ๋งทํ
์์ ๋ฐ ๊ด์ฌ๋ ๋ถ์ ๊ฐํ"""
|
| 797 |
+
|
| 798 |
+
total_volume = volume_data.get('์ด๊ฒ์๋', 0)
|
| 799 |
+
current_date = datetime.now()
|
| 800 |
+
current_month = current_date.month
|
| 801 |
+
current_year = current_date.year
|
| 802 |
+
|
| 803 |
+
# โ
์์ : ์ฌ๋ฐ๋ฅธ ๋ก์ง์ผ๋ก ์์นํญ ๊ณ์ฐ
|
| 804 |
+
growth_analysis = calculate_max_growth_rate_with_predictions(trend_data_3year, keyword)
|
| 805 |
+
|
| 806 |
+
# โ
์์ : ์ฌ๋ฐ๋ฅธ ๋ก์ง์ผ๋ก ํผํฌ์ ๊ณ์ฐ (์ค์ +์์ ๋ฐ์ดํฐ ํ์ฉ)
|
| 807 |
+
peak_month_with_volume = get_peak_month_with_predictions(trend_data_3year, keyword)
|
| 808 |
+
|
| 809 |
+
# LLM์ผ๋ก ์์ฆ ๋ถ์ (๊ธฐ์กด ์ ์ง)
|
| 810 |
+
if gemini_model:
|
| 811 |
+
product_type, entry_timing, season_detail = analyze_season_cycle_with_llm(trend_data_3year, keyword, total_volume, gemini_model)
|
| 812 |
+
else:
|
| 813 |
+
# ๊ธฐ๋ณธ๊ฐ
|
| 814 |
+
product_type = "์ฐ์ค์ํ"
|
| 815 |
+
if total_volume > 50000:
|
| 816 |
+
product_type = "์ธ๊ธฐ์ํ"
|
| 817 |
+
elif total_volume > 10000:
|
| 818 |
+
product_type = "์ค๊ฐ์ํ"
|
| 819 |
+
elif total_volume > 0:
|
| 820 |
+
product_type = "ํ์์ํ"
|
| 821 |
+
|
| 822 |
+
# 2. ๊ด์ฌ๋ ๋ถ์ ์ถ๊ฐ - ์ด๋ณด์๊ฐ ํ๋งค๊ฐ๋ฅํ ์์ฑ ๊ธฐ์ค (๊ฐ์ ๋ ๊ธฐ์ค ์ ์ฉ)
|
| 823 |
+
involvement_level = analyze_involvement_level(keyword, total_volume, gemini_model)
|
| 824 |
+
|
| 825 |
+
# ํธ๋ ๋ ๊ฒฝ๊ณ ๋ฉ์์ง
|
| 826 |
+
trend_warning = ""
|
| 827 |
+
if not trend_data_3year:
|
| 828 |
+
trend_warning = "\n\n๐ก ๋ ์ ํํ ํธ๋ ๋ ๋ฐ์ดํฐ๋ฅผ ์ํด \"1๋จ๊ณ: ๊ธฐ๋ณธ ํค์๋ ์
๋ ฅ\"์ ์คํํด๋ณด์ธ์."
|
| 829 |
+
|
| 830 |
+
# ๊ฒฐ๊ณผ ํฌ๋งทํ
์์ - ๊ตฌ๋ถ์ ๊ณผ ํญ๋ชฉ ๋ถ๋ฆฌ
|
| 831 |
+
result_content = f"""**๐ ์ํ์ ํ**
|
| 832 |
+
{product_type}
|
| 833 |
+
|
| 834 |
+
{involvement_level}
|
| 835 |
+
|
| 836 |
+
**๐ ๊ฐ์ฅ ๊ฒ์๋์ด ๋ง์ ์**
|
| 837 |
+
{peak_month_with_volume}
|
| 838 |
+
|
| 839 |
+
**๐ ๊ฐ์ฅ ์์นํญ์ด ๋์ ์**
|
| 840 |
+
{growth_analysis}{trend_warning}"""
|
| 841 |
+
|
| 842 |
+
try:
|
| 843 |
+
return {"status": "success", "content": result_content}
|
| 844 |
+
except Exception as e:
|
| 845 |
+
logger.error(f"์์ฑ์ ๋ต ๋ถ์ ์ค๋ฅ: {e}")
|
| 846 |
+
return {"status": "error", "content": "์์ฑ์ ๋ต ๋ถ์์ ์๋ฃํ ์ ์์ต๋๋ค."}
|
| 847 |
+
|
| 848 |
+
def analyze_involvement_level(keyword, total_volume, gemini_model):
|
| 849 |
+
"""๊ด์ฌ๋ ๋ถ์ ํจ์ - ์ด๋ณด์๊ฐ ํ๋งค๊ฐ๋ฅํ ์์ฑ ๊ธฐ์ค"""
|
| 850 |
+
try:
|
| 851 |
+
# ๊ธฐ๋ณธ ๊ท์น ๊ธฐ๋ฐ ๋ถ์
|
| 852 |
+
basic_involvement = get_basic_involvement_level(keyword, total_volume)
|
| 853 |
+
|
| 854 |
+
# Gemini๊ฐ ์์ผ๋ฉด LLM ๋ถ์๋ ์ํ
|
| 855 |
+
if gemini_model:
|
| 856 |
+
llm_involvement = get_llm_involvement_analysis(keyword, total_volume, gemini_model)
|
| 857 |
+
return llm_involvement
|
| 858 |
+
else:
|
| 859 |
+
return basic_involvement
|
| 860 |
+
|
| 861 |
+
except Exception as e:
|
| 862 |
+
logger.error(f"๊ด์ฌ๋ ๋ถ์ ์ค๋ฅ: {e}")
|
| 863 |
+
return "๋ณตํฉ๊ด์ฌ๋์ํ(์ํ์ ๋ฐ๋ผ ๋ฌ๋ผ์ง)"
|
| 864 |
+
|
| 865 |
+
def get_basic_involvement_level(keyword, total_volume):
|
| 866 |
+
"""๊ธฐ๋ณธ ๊ท์น ๊ธฐ๋ฐ ๊ด์ฌ๋ ๋ถ์ - ์ด๋ณด์ ํ๋งค ๊ด์ """
|
| 867 |
+
|
| 868 |
+
# ์ ๊ด์ฌ ์ํ ํค์๋ ํจํด (์ด๋ณด์ ์ง์
๊ฐ๋ฅํ ๋ถํธํด์ ์ ํ)
|
| 869 |
+
low_involvement_keywords = [
|
| 870 |
+
# ๋ถํธํด์/์ ๋ฆฌ์๋ฉ
|
| 871 |
+
"๊ฑฐ์น๋", "๋ฐ์นจ๋", "์ ๋ฆฌํจ", "์ ๋ฆฌ๋", "์๋ฉ", "ํ๋", "์คํ ๋",
|
| 872 |
+
"์ฟ ์
", "๋ฒ ๊ฐ", "๋ชฉ๋ฒ ๊ฐ", "๋ฐฉ์", "๋งคํธ", "ํจ๋",
|
| 873 |
+
# ์ผ์ด๋ธ/์ ์ ๊ด๋ฆฌ
|
| 874 |
+
"์ผ์ด๋ธ", "์ ์ ๋ฆฌ", "์ฝ๋", "์ถฉ์ ๊ธฐ", "์ด๋ํฐ",
|
| 875 |
+
# ์ฒญ์/์์ (๋๊ธฐ์
์ ํ ์ ์ธ)
|
| 876 |
+
"์ฒญ์์", "์ฒญ์๊ธฐ", "๊ฑธ๋ ", "ํ์ฌ", "๋ธ๋ฌ์",
|
| 877 |
+
# ์๋์ฐจ/์ค์ฉ์ฉํ
|
| 878 |
+
"์ฐจ๋์ฉ", "์๋์ฐจ", "ํธ๋ํฐ", "์ค๋งํธํฐ", "ํ๋ธ๋ฆฟ",
|
| 879 |
+
# ๊ฐ๋จํ ๋๊ตฌ/์ก์ธ์๋ฆฌ
|
| 880 |
+
"์ง๊ฒ", "ํํฌ", "์์", "ํด๋ฆฝ", "๊ณ ๋ฆฌ", "๋ง", "ํ๋",
|
| 881 |
+
# ๋ฏธ๋๋ผ๋ฐฉ์ง/์์
|
| 882 |
+
"๋ฏธ๋๋ผ", "๋
ผ์ฌ๋ฆฝ", "๋ฐฉ์ง", "๋ณดํธ", "์ปค๋ฒ", "์ผ์ด์ค"
|
| 883 |
+
]
|
| 884 |
+
|
| 885 |
+
# ๊ณ ๊ด์ฌ ์ํ ํค์๋ ํจํด (๋๊ธฐ์
๋
์ ๋๋ ๊ณ ๊ฐ/์ ๋ฌธ ์ ํ)
|
| 886 |
+
high_involvement_keywords = [
|
| 887 |
+
# ๋๊ธฐ์
๋
์ ์ํํ
|
| 888 |
+
"ํด์ง", "ํ์ฅ์ง", "๋ฌผํฐ์", "๋ง์คํฌ", "์ธ์ ", "์ดํธ", "๋ฆฐ์ค", "๋น๋",
|
| 889 |
+
"์น์ฝ", "์นซ์", "๊ธฐ์ ๊ท", "์๋ฆฌ๋", "์ฝ๋",
|
| 890 |
+
# ์ํ/์๋ฃ (๋ธ๋๋ ๋ฏผ๊ฐ)
|
| 891 |
+
"๋ผ๋ฉด", "๊ณผ์", "์๋ฃ", "์ปคํผ", "์ฐจ", "์ฐ์ ", "์๊ตฌ๋ฅดํธ",
|
| 892 |
+
"์", "๊น", "์ฐธ๊ธฐ๋ฆ", "๊ฐ์ฅ", "๊ณ ์ถ์ฅ", "๋์ฅ",
|
| 893 |
+
# ๊ณ ๊ฐ ์ ์์ ํ
|
| 894 |
+
"๋
ธํธ๋ถ", "์ปดํจํฐ", "์ค๋งํธํฐ", "ํ๋ธ๋ฆฟ", "์นด๋ฉ๋ผ", "TV", "๋ชจ๋ํฐ",
|
| 895 |
+
"๋์ฅ๊ณ ", "์ธํ๊ธฐ", "์์ด์ปจ", "์ฒญ์๊ธฐ", "์ ์๋ ์ธ์ง",
|
| 896 |
+
# ์๋ฃ/๊ฑด๊ฐ (์ธ์ฆ ํ์)
|
| 897 |
+
"์๋ฃ", "๊ฑด๊ฐ์ํ", "์์์ ", "๋นํ๋ฏผ", "์ฝ", "์์ฝํ",
|
| 898 |
+
# ๋ช
ํ/๋ธ๋๋
|
| 899 |
+
"๋ช
ํ", "๋ธ๋๋", "๋ญ์
๋ฆฌ", "์๊ณ", "๋ณด์", "๊ธ", "์", "๋ค์ด์๋ชฌ๋"
|
| 900 |
+
]
|
| 901 |
+
|
| 902 |
+
keyword_lower = keyword.lower()
|
| 903 |
+
|
| 904 |
+
# ์ ๊ด์ฌ ์ํ ์ฒดํฌ (๋ถํธํด์ ํค์๋ ์ฐ์ )
|
| 905 |
+
for low_kw in low_involvement_keywords:
|
| 906 |
+
if low_kw in keyword_lower:
|
| 907 |
+
return "์ ๊ด์ฌ์ํ(์ด๋ณด์์ฉ)"
|
| 908 |
+
|
| 909 |
+
# ๊ณ ๊ด์ฌ ์ํ ์ฒดํฌ (๋๊ธฐ์
๋
์ /๋ธ๋๋ ๋ฏผ๊ฐ ํค์๋)
|
| 910 |
+
for high_kw in high_involvement_keywords:
|
| 911 |
+
if high_kw in keyword_lower:
|
| 912 |
+
return "๊ณ ๊ด์ฌ์ํ(๊ณ ๊ธ์์ฉ)"
|
| 913 |
+
|
| 914 |
+
# ๊ฒ์๋ ๏ฟฝ๏ฟฝ๏ฟฝ๋ฐ ์ถ๊ฐ ํ๋จ
|
| 915 |
+
if total_volume > 100000:
|
| 916 |
+
# ๊ฒ์๋์ด ๋งค์ฐ ๋์ผ๋ฉด ๋๊ธฐ์
์ด ๊ด์ฌ ๊ฐ์ง ๋งํ ์์ฅ
|
| 917 |
+
return "๊ณ ๊ด์ฌ์ํ(๊ณ ๊ธ์์ฉ)"
|
| 918 |
+
elif total_volume > 50000:
|
| 919 |
+
return "๋ณตํฉ๊ด์ฌ๋์ํ(์ํ์ ๋ฐ๋ผ ๋ฌ๋ผ์ง)"
|
| 920 |
+
elif total_volume > 5000:
|
| 921 |
+
return "๋ณตํฉ๊ด์ฌ๋์ํ(์ํ์ ๋ฐ๋ผ ๋ฌ๋ผ์ง)"
|
| 922 |
+
else:
|
| 923 |
+
# ๊ฒ์๋์ด ๋ฎ์ผ๋ฉด ํ์ ์์ฅ, ์ด๋ณด์๋ ์ง์
๊ฐ๋ฅ
|
| 924 |
+
return "์ ๊ด์ฌ์ํ(์ด๋ณด์์ฉ)"
|
| 925 |
+
|
| 926 |
+
def get_llm_involvement_analysis(keyword, total_volume, gemini_model):
|
| 927 |
+
"""LLM์ ์ด์ฉํ ์ ๊ตํ ๊ด์ฌ๋ ๋ถ์ - ์ด๋ณด์ ํ๋งค ๊ด์ ๊ธฐ์ค ์ ์ฉ"""
|
| 928 |
+
try:
|
| 929 |
+
prompt = f"""
|
| 930 |
+
'{keyword}' ์ํ์ ๊ด์ฌ๋๋ฅผ ์ด๋ณด์ ํ๋งค ๊ด์ ์์ ๋ถ์ํด์ฃผ์ธ์.
|
| 931 |
+
|
| 932 |
+
๊ฒ์๋: {total_volume:,}ํ
|
| 933 |
+
|
| 934 |
+
๊ด์ฌ๋ ์ ์ (์ด๋ณด์๊ฐ ํ๋งค๊ฐ๋ฅํ ์์ฑ ๊ธฐ์ค):
|
| 935 |
+
|
| 936 |
+
์ ๊ด์ฌ์ํ(์ด๋ณด์์ฉ):
|
| 937 |
+
- ๋๊ธฐ์
๋
์ ์ด ์๋ ์์ญ
|
| 938 |
+
- ์ฆ์ ๋ถํธํด์ํ๋ ์ ํ (์ง๊ธ ๋ฐ๋ก ํ์ํ ๋ฌธ์ ํด๊ฒฐ)
|
| 939 |
+
- ๋ธ๋๋ ์๊ด์์ด ๊ธฐ๋ฅ๋ง ๋๋ฉด ๊ตฌ๋งคํ๋ ์ ํ
|
| 940 |
+
- 1๋ง์~3๋ง์๋ ๊ฐ๊ฒฉ, ์๋(100๊ฐ ์ดํ) ์์ ๊ฐ๋ฅ
|
| 941 |
+
- ์์: ๋ชฉ๋ฒ ๊ฐ, ์ค๋งํธํฐ๊ฑฐ์น๋, ์๋์ ๋ฆฌํจ, ์ผ์ด๋ธ์ ๋ฆฌ๊ธฐ
|
| 942 |
+
|
| 943 |
+
๊ณ ๊ด์ฌ์ํ(๊ณ ๊ธ์์ฉ):
|
| 944 |
+
- ๋๊ธฐ์
/๋ธ๋๋๊ฐ ์์ฅ์ ๋
์ ํ๋ ์์ญ (์ด๋ณด์ ์ง์
๋ถ๊ฐ)
|
| 945 |
+
- ์ํํ(ํด์ง, ์ธ์ , ๋ง์คํฌ ๋ฑ) - ๋ธ๋๋ ์ถฉ์ฑ๋ ๋์
|
| 946 |
+
- ๊ณ ๊ฐ ์ ํ(10๋ง์ ์ด์), ์ ๋ฌธ์ฑ/์ธ์ฆ ํ์
|
| 947 |
+
- ๋์๋ณธ ํ์ํ ์์ดํ
|
| 948 |
+
- ์์: ์ ์์ ํ, ๊ฐ์ , ๋ธ๋๋ ์ํํ, ์๋ฃ์ฉํ
|
| 949 |
+
|
| 950 |
+
๋ณตํฉ๊ด์ฌ๋์ํ(์ํ์ ๋ฐ๋ผ ๋ฌ๋ผ์ง):
|
| 951 |
+
- ๊ฐ๊ฒฉ๋๋ณ๋ก ์ ๊ฐํ(์ ๊ด์ฌ)๊ณผ ๊ณ ๊ฐํ(๊ณ ๊ด์ฌ)์ด ๊ณต์กด
|
| 952 |
+
- ํ๊ฒ์ด๋ ์ฉ๋์ ๋ฐ๋ผ ๊ด์ฌ๋๊ฐ ๊ทน๋ช
ํ๊ฒ ๋ฌ๋ผ์ง
|
| 953 |
+
- ์์: ์๋ฅ, ์ด๋์ฉํ, ๋ทฐํฐ์ฉํ ๋ฑ
|
| 954 |
+
|
| 955 |
+
๋ณตํฉ๊ด์ฌ๋์ํ์ผ๋ก ํ๋จํ ๊ฒฝ์ฐ, ๋ฐ๋์ ๊ตฌ์ฒด์ ์ธ ์ด์ ๋ฅผ ์ค๋ช
ํ์ธ์:
|
| 956 |
+
- ๊ฐ๊ฒฉ๋๋ณ ๋ถํ: "1-3๋ง์ ์ค๊ตญ์ฐ(์ ๊ด์ฌ) vs 10-15๋ง์ ๊ตญ์ฐ ์์ (๊ณ ๊ด์ฌ)"
|
| 957 |
+
- ํ๊ฒ๋ณ ์ฐจ์ด: "์ผ๋ฐ์ธ์ ์ ๊ด์ฌ vs ์ ๋ฌธ๊ฐ๋ ๊ณ ๊ด์ฌ"
|
| 958 |
+
- ์ฉ๋๋ณ ์ฐจ์ด: "์์์ฉ์ ์ ๊ด์ฌ vs ์ฅ๊ธฐ์ฉ์ ๊ณ ๊ด์ฌ"
|
| 959 |
+
|
| 960 |
+
๋ค์ ํ์์ผ๋ก ๋ต๋ณํ์ธ์:
|
| 961 |
+
[๊ด์ฌ๋ ์ ํ]
|
| 962 |
+
[๊ตฌ์ฒด์ ์ธ ํ๋จ ์ด์ - ๊ฐ๊ฒฉ๋/ํ๊ฒ/๋ธ๋๋ ๋
์ ์ฌ๋ถ ๋ฑ์ ๋ช
ํํ ์ ์]
|
| 963 |
+
|
| 964 |
+
์ ํ์ง:
|
| 965 |
+
์ ๊ด์ฌ์ํ(์ด๋ณด์์ฉ)
|
| 966 |
+
๋ณตํฉ๊ด์ฌ๋์ํ(์ํ์ ๋ฐ๋ผ ๋ฌ๋ผ์ง)
|
| 967 |
+
๊ณ ๊ด์ฌ์ํ(๊ณ ๊ธ์์ฉ)
|
| 968 |
+
"""
|
| 969 |
+
|
| 970 |
+
response = gemini_model.generate_content(prompt)
|
| 971 |
+
result = response.text.strip()
|
| 972 |
+
|
| 973 |
+
# ๊ฒฐ๊ณผ ํํฐ๋ง - ์ ํํ ํ์๋ง ํ์ฉ
|
| 974 |
+
if "์ ๊ด์ฌ์ํ(์ด๋ณด์์ฉ)" in result:
|
| 975 |
+
return "์ ๊ด์ฌ์ํ(์ด๋ณด์์ฉ)"
|
| 976 |
+
elif "๊ณ ๊ด์ฌ์ํ(๊ณ ๊ธ์์ฉ)" in result:
|
| 977 |
+
return "๊ณ ๊ด์ฌ์ํ(๊ณ ๊ธ์์ฉ)"
|
| 978 |
+
elif "๋ณตํฉ๊ด์ฌ๋์ํ(์ํ์ ๋ฐ๋ผ ๋ฌ๋ผ์ง)" in result:
|
| 979 |
+
return "๋ณตํฉ๊ด์ฌ๋์ํ(์ํ์ ๋ฐ๋ผ ๋ฌ๋ผ์ง)"
|
| 980 |
+
else:
|
| 981 |
+
# LLM ์๋ต์ด ๋ถ์ ํํ ๊ฒฝ์ฐ ๊ธฐ๋ณธ ๊ท์น์ผ๋ก ํด๋ฐฑ
|
| 982 |
+
return get_basic_involvement_level(keyword, total_volume)
|
| 983 |
+
|
| 984 |
+
except Exception as e:
|
| 985 |
+
logger.error(f"LLM ๊ด์ฌ๋ ๋ถ์ ์ค๋ฅ: {e}")
|
| 986 |
+
return get_basic_involvement_level(keyword, total_volume)
|
| 987 |
+
|
| 988 |
+
|
| 989 |
+
class CompactKeywordAnalyzer:
|
| 990 |
+
"""๊ฐ๊ฒฐํ 7๋จ๊ณ ํค์๋ ๋ถ์๊ธฐ"""
|
| 991 |
+
|
| 992 |
+
def __init__(self, gemini_model):
|
| 993 |
+
self.gemini_model = gemini_model
|
| 994 |
+
self.max_retries = 3
|
| 995 |
+
|
| 996 |
+
def call_llm_with_retry(self, prompt: str, step_name: str = "") -> str:
|
| 997 |
+
"""์ฌ์๋ ๋ก์ง์ด ์ ์ฉ๋ LLM ํธ์ถ"""
|
| 998 |
+
last_error = None
|
| 999 |
+
|
| 1000 |
+
for attempt in range(self.max_retries):
|
| 1001 |
+
try:
|
| 1002 |
+
logger.info(f"{step_name} ์๋ {attempt + 1}/{self.max_retries}")
|
| 1003 |
+
response = self.gemini_model.generate_content(prompt)
|
| 1004 |
+
result = response.text.strip()
|
| 1005 |
+
|
| 1006 |
+
if result and len(result) > 20:
|
| 1007 |
+
logger.info(f"{step_name} ์ฑ๊ณต")
|
| 1008 |
+
return result
|
| 1009 |
+
else:
|
| 1010 |
+
raise Exception("์๋ต์ด ๋๋ฌด ์งง๊ฑฐ๋ ๋น์ด์์")
|
| 1011 |
+
|
| 1012 |
+
except Exception as e:
|
| 1013 |
+
last_error = e
|
| 1014 |
+
logger.warning(f"{step_name} ์คํจ (์๋ {attempt + 1}): {e}")
|
| 1015 |
+
|
| 1016 |
+
if attempt < self.max_retries - 1:
|
| 1017 |
+
delay = 1.0 * (attempt + 1) + random.uniform(0, 0.5)
|
| 1018 |
+
time.sleep(delay)
|
| 1019 |
+
|
| 1020 |
+
logger.error(f"{step_name} ๋ชจ๋ ์ฌ์๋ ์คํจ: {last_error}")
|
| 1021 |
+
return f"{step_name} ๋ถ์์ ์๋ฃํ ์ ์์ต๋๋ค."
|
| 1022 |
+
|
| 1023 |
+
def clean_markdown_and_bold(self, text: str) -> str:
|
| 1024 |
+
"""๋งํฌ๋ค์ด๊ณผ ๋ณผ๋ ์ฒ๋ฆฌ๋ฅผ ์์ ํ ์ ๊ฑฐ"""
|
| 1025 |
+
text = re.sub(r'\*\*(.+?)\*\*', r'\1', text)
|
| 1026 |
+
text = re.sub(r'\*(.+?)\*', r'\1', text)
|
| 1027 |
+
text = re.sub(r'__(.+?)__', r'\1', text)
|
| 1028 |
+
text = re.sub(r'_(.+?)_', r'\1', text)
|
| 1029 |
+
text = re.sub(r'##\s*(.+)', r'\1', text)
|
| 1030 |
+
text = re.sub(r'#\s*(.+)', r'\1', text)
|
| 1031 |
+
text = re.sub(r'\*+', '', text)
|
| 1032 |
+
text = re.sub(r'_+', '', text)
|
| 1033 |
+
return text.strip()
|
| 1034 |
+
|
| 1035 |
+
def analyze_sourcing_strategy(self, keyword: str, volume_data: dict, keywords_df: Optional[pd.DataFrame], trend_data_1year=None, trend_data_3year=None) -> str:
|
| 1036 |
+
"""๊ฐ์ ๋ ์์ฑ์ ๋ต ๋ถ์ - ๊ฒฝ์๋ฐ์ดํฐ ์ ๊ฑฐ"""
|
| 1037 |
+
|
| 1038 |
+
try:
|
| 1039 |
+
sourcing_analysis = analyze_sourcing_strategy_improved(
|
| 1040 |
+
keyword, volume_data, trend_data_1year, trend_data_3year, keywords_df, self.gemini_model
|
| 1041 |
+
)
|
| 1042 |
+
if sourcing_analysis["status"] == "success":
|
| 1043 |
+
return self.clean_markdown_and_bold(sourcing_analysis["content"])
|
| 1044 |
+
else:
|
| 1045 |
+
return sourcing_analysis["content"]
|
| 1046 |
+
except Exception as e:
|
| 1047 |
+
logger.error(f"์์ฑ์ ๋ต ๋ถ์ ์ค๋ฅ: {e}")
|
| 1048 |
+
return "์์ฑ์ ๋ต ๋ถ์์ ์๋ฃํ ์ ์์ต๋๋ค."
|
| 1049 |
+
|
| 1050 |
+
def analyze_step1_product_type(self, keyword: str, keywords_df: Optional[pd.DataFrame]) -> str:
|
| 1051 |
+
"""1๋จ๊ณ. ์ํ์ ํ ๋ถ์"""
|
| 1052 |
+
|
| 1053 |
+
related_keywords = ""
|
| 1054 |
+
if keywords_df is not None and not keywords_df.empty:
|
| 1055 |
+
top_keywords = keywords_df.head(10)['์กฐํฉ ํค์๋'].tolist()
|
| 1056 |
+
related_keywords = f"์ฐ๊ดํค์๋: {', '.join(top_keywords)}"
|
| 1057 |
+
|
| 1058 |
+
prompt = f"""
|
| 1059 |
+
๋น์ ์ ์ด๋ณด ์
๋ฌ๊ฐ ์ํ ํ๋งค ์ฑ๊ณต์ ๋น ๋ฅด๊ฒ ์ด๋ฃฐ ์ ์๋๋ก ๋๋ ์ต๊ณ ์ ์ํ ์์ฑ ๋ฐ ์ํ๊ธฐํ ์ปจ์คํดํธ AI์
๋๋ค.
|
| 1060 |
+
|
| 1061 |
+
๋ถ์ ํค์๋: '{keyword}'
|
| 1062 |
+
{related_keywords}
|
| 1063 |
+
|
| 1064 |
+
1๋จ๊ณ. ์ํ์ ํ ๋ถ์
|
| 1065 |
+
|
| 1066 |
+
์ํ ์ ํ ๋ถ๋ฅ ๊ธฐ์ค:
|
| 1067 |
+
- ๋ถํธํด๊ฒฐ์ํ: ํน์ ๋ฌธ์ ๋ ๋ถํธํจ์ ์ฆ๊ฐ ํด๊ฒฐํ๋ ์ ํ
|
| 1068 |
+
- ์
๊ทธ๋ ์ด๋์ํ: ์ถ์ ์ง๊ณผ ๋ง์กฑ๋๋ฅผ ํฅ์์ํค๋ ์ ํ
|
| 1069 |
+
- ํ์์ํ: ์ผ์์์ ๋ฐ๋์ ํ์ํ๊ณ ๋ฐ๋ณต ๊ตฌ๋งค๋๋ ์ ํ
|
| 1070 |
+
- ์ทจํฅ์ ๊ฒฉ์ํ: ๊ฐ์ฑ์ , ๊ฐ์ฑ์ ์๊ตฌ๋ฅผ ์๊ทนํ๋ ์ ํ
|
| 1071 |
+
- ์ตํฉ์ํ: ์ 2๊ฐ ์ด์์ ์ ํ์ด ๊ฒฐํฉ๋ ์ ํ
|
| 1072 |
+
|
| 1073 |
+
๋ค์ ํ์์ผ๋ก ๋ถ์ํด์ฃผ์ธ์ (๋ณผ๋, ๋งํฌ๋ค์ด ์ฌ์ฉ ๊ธ์ง):
|
| 1074 |
+
|
| 1075 |
+
์ฃผ์์ ํ: [์ ํ๋ช
]
|
| 1076 |
+
{keyword}๋ [๊ตฌ์ฒด์ ์ค๋ช
- ์ ์ด ์ ํ์ธ์ง ๋ณธ์ง์ ๊ฐ์น์ ํด๊ฒฐํ๋ ๋ฌธ์ ๋ฅผ ์ค์ฌ์ผ๋ก 2-3๋ฌธ์ฅ]
|
| 1077 |
+
|
| 1078 |
+
๋ณด์กฐ์ ํ: [ํด๋น ์ ํ๋ค]
|
| 1079 |
+
[์ ํ1]
|
| 1080 |
+
- [์ด ์ ํ์ ํด๋นํ๋ ์ด์ 1๋ฌธ์ฅ]
|
| 1081 |
+
[์ ํ2]
|
| 1082 |
+
- [์ด ์ ํ์ ํด๋นํ๋ ์ด์ 1๋ฌธ์ฅ]
|
| 1083 |
+
"""
|
| 1084 |
+
|
| 1085 |
+
result = self.call_llm_with_retry(prompt, f"1๋จ๊ณ-์ํ์ ํ๋ถ์-{keyword}")
|
| 1086 |
+
return self.clean_markdown_and_bold(result)
|
| 1087 |
+
|
| 1088 |
+
def analyze_step2_target_customer(self, keyword: str, step1_result: str) -> str:
|
| 1089 |
+
"""2๋จ๊ณ. ์๋น์ ํ๊ฒ ์ค์ """
|
| 1090 |
+
|
| 1091 |
+
prompt = f"""
|
| 1092 |
+
๋น์ ์ ์ด๋ณด ์
๋ฌ๊ฐ ์ํ ํ๋งค ์ฑ๊ณต์ ๋น ๋ฅด๊ฒ ์ด๋ฃฐ ์ ์๋๋ก ๋๋ ์ต๊ณ ์ ์ํ ์์ฑ ๋ฐ ์ํ๊ธฐํ ์ปจ์คํดํธ AI์
๋๋ค.
|
| 1093 |
+
|
| 1094 |
+
๋ถ์ ํค์๋: '{keyword}'
|
| 1095 |
+
|
| 1096 |
+
์ด์ ๋ถ์ ๊ฒฐ๊ณผ:
|
| 1097 |
+
{step1_result}
|
| 1098 |
+
|
| 1099 |
+
2๋จ๊ณ. ์๋น์ ํ๊ฒ ์ค์
|
| 1100 |
+
|
| 1101 |
+
๋ค์ ํ์์ผ๋ก ๊ฐ๊ฒฐํ๊ฒ ๋ถ์ํด์ฃผ์ธ์ (๋ณผ๋, ๋งํฌ๋ค์ด ์ฌ์ฉ ๊ธ์ง):
|
| 1102 |
+
|
| 1103 |
+
๊ณ ๊ฐ์ํฉ
|
| 1104 |
+
- [๊ตฌ์ฒด์ ์ธ ๊ตฌ๋งค ์ํฉ๋ค์ ๊ฐ๋จํ]
|
| 1105 |
+
|
| 1106 |
+
ํ๋ฅด์๋
|
| 1107 |
+
- [์ฐ๋ น๋, ์ฑ๋ณ, ๋ผ์ดํ์คํ์ผ์ ํตํฉํ์ฌ 1-2์ค๋ก ๊ฐ๊ฒฐํ๊ฒ]
|
| 1108 |
+
|
| 1109 |
+
์ฃผ์ ๋์ฆ
|
| 1110 |
+
- [ํต์ฌ ๋์ฆ ํ์ค๋ง]
|
| 1111 |
+
"""
|
| 1112 |
+
|
| 1113 |
+
result = self.call_llm_with_retry(prompt, f"2๋จ๊ณ-ํ๊ฒ์ค์ -{keyword}")
|
| 1114 |
+
return self.clean_markdown_and_bold(result)
|
| 1115 |
+
|
| 1116 |
+
def analyze_step3_sourcing_strategy(self, keyword: str, previous_results: str) -> str:
|
| 1117 |
+
"""3๋จ๊ณ. ํ๊ฒ๋ณ ์ฐจ๋ณํ๋ ์์ฑ ์ ๋ต ์ ์"""
|
| 1118 |
+
|
| 1119 |
+
prompt = f"""
|
| 1120 |
+
๋น์ ์ ์ด๋ณด ์
๋ฌ๊ฐ ์ํ ํ๋งค ์ฑ๊ณต์ ๋น ๋ฅด๊ฒ ์ด๋ฃฐ ์ ์๋๋ก ๋๋ ์ต๊ณ ์ ์ํ ์์ฑ ๋ฐ ์ํ๊ธฐํ ์ปจ์คํดํธ AI์
๋๋ค.
|
| 1121 |
+
|
| 1122 |
+
๋ถ์ ํค์๋: '{keyword}'
|
| 1123 |
+
|
| 1124 |
+
์ด์ ๋ถ์ ๊ฒฐ๊ณผ:
|
| 1125 |
+
{previous_results}
|
| 1126 |
+
|
| 1127 |
+
3๋จ๊ณ. ํ๊ฒ๋ณ ์ฐจ๋ณํ๋ ์์ฑ ์ ๋ต ์ ์
|
| 1128 |
+
|
| 1129 |
+
ํ์ค์ ์ผ๋ก ์จ๋ผ์ธ์์ ์์ฑ ๊ฐ๋ฅํ ์ฐจ๋ณํ ์ ๋ต์ ์ ์ํด์ฃผ์ธ์.
|
| 1130 |
+
|
| 1131 |
+
๋ค์ ํ์์ผ๋ก ๋ถ์ํด์ฃผ์ธ์ (๋ณผ๋, ๋งํฌ๋ค์ด ์ฌ์ฉ ๊ธ์ง):
|
| 1132 |
+
|
| 1133 |
+
ํต์ฌ ๊ตฌ๋งค ๊ณ ๋ ค ์์ 5๊ฐ์ง
|
| 1134 |
+
1. [์์1 ๊ฐ๋จํ]
|
| 1135 |
+
2. [์์2 ๊ฐ๋จํ]
|
| 1136 |
+
3. [์์3 ๊ฐ๋จํ]
|
| 1137 |
+
4. [์์4 ๊ฐ๋จํ]
|
| 1138 |
+
5. [์์5 ๊ฐ๋จํ]
|
| 1139 |
+
|
| 1140 |
+
์ฐจ๋ณํ ์์ฑ ์ ๋ต
|
| 1141 |
+
1. [์ ๋ต๋ช
]
|
| 1142 |
+
- [ํ์ค์ ์ผ๋ก ์์ฑ ๊ฐ๋ฅํ ๊ตฌ์ฒด์ ๋ฐฉ๋ฒ ํ์ค]
|
| 1143 |
+
|
| 1144 |
+
2. [์ ๋ต๋ช
]
|
| 1145 |
+
- [ํ์ค์ ์ผ๋ก ์์ฑ ๊ฐ๋ฅํ ๊ตฌ์ฒด์ ๋ฐฉ๋ฒ ํ์ค]
|
| 1146 |
+
|
| 1147 |
+
3. [์ ๋ต๋ช
]
|
| 1148 |
+
- [ํ์ค์ ์ผ๋ก ์์ฑ ๊ฐ๋ฅํ ๊ตฌ์ฒด์ ๋ฐฉ๋ฒ ํ์ค]
|
| 1149 |
+
|
| 1150 |
+
4. [์ ๋ต๋ช
]
|
| 1151 |
+
- [ํ์ค์ ์ผ๋ก ์์ฑ ๊ฐ๋ฅํ ๊ตฌ์ฒด์ ๋ฐฉ๋ฒ ํ์ค]
|
| 1152 |
+
|
| 1153 |
+
5. [์ ๋ต๋ช
]
|
| 1154 |
+
- [ํ์ค์ ์ผ๋ก ์์ฑ ๊ฐ๋ฅํ ๊ตฌ์ฒด์ ๋ฐฉ๋ฒ ํ์ค]
|
| 1155 |
+
"""
|
| 1156 |
+
|
| 1157 |
+
result = self.call_llm_with_retry(prompt, f"3๋จ๊ณ-์์ฑ์ ๋ต-{keyword}")
|
| 1158 |
+
return self.clean_markdown_and_bold(result)
|
| 1159 |
+
|
| 1160 |
+
def analyze_step4_product_recommendation(self, keyword: str, previous_results: str) -> str:
|
| 1161 |
+
"""4๋จ๊ณ. ์ฐจ๋ณํ ์์๋ณ ์ํ 5๊ฐ์ง ์ถ์ฒ"""
|
| 1162 |
+
|
| 1163 |
+
prompt = f"""
|
| 1164 |
+
๋น์ ์ ์ด๋ณด ์
๋ฌ๊ฐ ์ํ ํ๋งค ์ฑ๊ณต์ ๋น ๋ฅด๊ฒ ์ด๏ฟฝ๏ฟฝ ์ ์๋๋ก ๋๋ ์ต๊ณ ์ ์ํ ์์ฑ ๋ฐ ์ํ๊ธฐํ ์ปจ์คํดํธ AI์
๋๋ค.
|
| 1165 |
+
|
| 1166 |
+
๋ถ์ ํค์๋: '{keyword}'
|
| 1167 |
+
|
| 1168 |
+
์ด์ ๋ถ์ ๊ฒฐ๊ณผ:
|
| 1169 |
+
{previous_results}
|
| 1170 |
+
|
| 1171 |
+
4๋จ๊ณ. ์ฐจ๋ณํ ์์๋ณ ์ํ 5๊ฐ์ง ์ถ์ฒ
|
| 1172 |
+
|
| 1173 |
+
3๋จ๊ณ์์ ๋์ถํ ์ฐจ๋ณํ ์์๋ฅผ ๋ฐ์ํ์ฌ ๋งค์ถ ๊ฐ๋ฅ์ฑ์ด ๋์ ์์๋๋ก ๋ถ์ํด์ฃผ์ธ์.
|
| 1174 |
+
|
| 1175 |
+
๋ค์ ํ์์ผ๋ก ๋ถ์ํด์ฃผ์ธ์ (๋ณผ๋, ๋งํฌ๋ค์ด ์ฌ์ฉ ๊ธ์ง):
|
| 1176 |
+
|
| 1177 |
+
์ฐจ๋ณํ ์ํ ์ถ์ฒ
|
| 1178 |
+
1. [๊ตฌ์ฒด์ ์ธ ์ํ๋ช
๊ณผ ์ธ๋ถ ํน์ง]
|
| 1179 |
+
- [์ฃผ์ ํน์ง๋ค, ํ๊ฒ ๊ณ ๊ฐ, ์ฐจ๋ณํ ํฌ์ธํธ๋ฅผ ํ๋ฌธ์ฅ์ผ๋ก]
|
| 1180 |
+
|
| 1181 |
+
2. [๊ตฌ์ฒด์ ์ธ ์ํ๋ช
๊ณผ ์ธ๋ถ ํน์ง]
|
| 1182 |
+
- [์ฃผ์ ํน์ง๋ค, ํ๊ฒ ๊ณ ๊ฐ, ์ฐจ๋ณํ ํฌ์ธํธ๋ฅผ ํ๋ฌธ์ฅ์ผ๋ก]
|
| 1183 |
+
|
| 1184 |
+
3. [๊ตฌ์ฒด์ ์ธ ์ํ๋ช
๊ณผ ์ธ๋ถ ํน์ง]
|
| 1185 |
+
- [์ฃผ์ ํน์ง๋ค, ํ๊ฒ ๊ณ ๊ฐ, ์ฐจ๋ณํ ํฌ์ธํธ๋ฅผ ํ๋ฌธ์ฅ์ผ๋ก]
|
| 1186 |
+
|
| 1187 |
+
4. [๊ตฌ์ฒด์ ์ธ ์ํ๋ช
๊ณผ ์ธ๋ถ ํน์ง]
|
| 1188 |
+
- [์ฃผ์ ํน์ง๋ค, ํ๊ฒ ๊ณ ๊ฐ, ์ฐจ๋ณํ ํฌ์ธํธ๋ฅผ ํ๋ฌธ์ฅ์ผ๋ก]
|
| 1189 |
+
|
| 1190 |
+
5. [๊ตฌ์ฒด์ ์ธ ์ํ๋ช
๊ณผ ์ธ๋ถ ํน์ง]
|
| 1191 |
+
- [์ฃผ์ ํน์ง๋ค, ํ๊ฒ ๊ณ ๊ฐ, ์ฐจ๋ณํ ํฌ์ธํธ๋ฅผ ํ๋ฌธ์ฅ์ผ๋ก]
|
| 1192 |
+
|
| 1193 |
+
๋ํ์ด๋ฏธ์ง ์ถ์ฒ
|
| 1194 |
+
1. [์ฒซ ๋ฒ์งธ ์ํ๋ช
]
|
| 1195 |
+
* [๊ฐ๋จํ ์ดฌ์ ์ปจ์
๊ณผ ํต์ฌ ํฌ์ธํธ ํ์ค]
|
| 1196 |
+
|
| 1197 |
+
2. [๋ ๋ฒ์งธ ์ํ๋ช
]
|
| 1198 |
+
* [๊ฐ๋จํ ์ดฌ์ ์ปจ์
๊ณผ ํต์ฌ ํฌ์ธํธ ํ์ค]
|
| 1199 |
+
|
| 1200 |
+
3. [์ธ ๋ฒ์งธ ์ํ๋ช
]
|
| 1201 |
+
* [๊ฐ๋จํ ์ดฌ์ ์ปจ์
๊ณผ ํต์ฌ ํฌ์ธํธ ํ์ค]
|
| 1202 |
+
|
| 1203 |
+
4. [๋ค ๋ฒ์งธ ์ํ๋ช
]
|
| 1204 |
+
* [๊ฐ๋จํ ์ดฌ์ ์ปจ์
๊ณผ ํต์ฌ ํฌ์ธํธ ํ์ค]
|
| 1205 |
+
|
| 1206 |
+
5. [๋ค์ฏ ๋ฒ์งธ ์ํ๋ช
]
|
| 1207 |
+
* [๊ฐ๋จํ ์ดฌ์ ์ปจ์
๊ณผ ํต์ฌ ํฌ์ธํธ ํ์ค]
|
| 1208 |
+
"""
|
| 1209 |
+
|
| 1210 |
+
result = self.call_llm_with_retry(prompt, f"4๋จ๊ณ-์ํ์ถ์ฒ-{keyword}")
|
| 1211 |
+
return self.clean_markdown_and_bold(result)
|
| 1212 |
+
|
| 1213 |
+
def analyze_step5_trust_building(self, keyword: str, previous_results: str) -> str:
|
| 1214 |
+
"""5๋จ๊ณ. ์ ๋ขฐ์ฑ์ ์ค ์ ์๋ ์์ 5๊ฐ์ง"""
|
| 1215 |
+
|
| 1216 |
+
prompt = f"""
|
| 1217 |
+
๋น์ ์ ์ด๋ณด ์
๋ฌ๊ฐ ์ํ ํ๋งค ์ฑ๊ณต์ ๋น ๋ฅด๊ฒ ์ด๋ฃฐ ์ ์๋๋ก ๋๋ ์ต๊ณ ์ ์ํ ์์ฑ ๋ฐ ์ํ๊ธฐํ ์ปจ์คํดํธ AI์
๋๋ค.
|
| 1218 |
+
|
| 1219 |
+
๋ถ์ ํค์๋: '{keyword}'
|
| 1220 |
+
|
| 1221 |
+
์ด์ ๋ถ์ ๊ฒฐ๊ณผ:
|
| 1222 |
+
{previous_results}
|
| 1223 |
+
|
| 1224 |
+
5๋จ๊ณ. ์ ๋ขฐ์ฑ์ ์ค ์ ์๋ ์์ 5๊ฐ์ง
|
| 1225 |
+
|
| 1226 |
+
๋ค์ ํ์์ผ๋ก ๋ถ์ํด์ฃผ์ธ์ (๋ณผ๋, ๋งํฌ๋ค์ด ์ฌ์ฉ ๊ธ์ง):
|
| 1227 |
+
|
| 1228 |
+
1. [์ ๋ขฐ์ฑ ์์1]
|
| 1229 |
+
- [๊ตฌ์ฒด์ ๋ฐฉ๋ฒ๊ณผ ์ ์ฉ ์์]
|
| 1230 |
+
|
| 1231 |
+
2. [์ ๋ขฐ์ฑ ์์2]
|
| 1232 |
+
- [๊ตฌ์ฒด์ ๋ฐฉ๋ฒ๊ณผ ์ ์ฉ ์์]
|
| 1233 |
+
|
| 1234 |
+
3. [์ ๋ขฐ์ฑ ์์3]
|
| 1235 |
+
- [๊ตฌ์ฒด์ ๋ฐฉ๋ฒ๊ณผ ์ ์ฉ ์์]
|
| 1236 |
+
|
| 1237 |
+
4. [์ ๋ขฐ์ฑ ์์4]
|
| 1238 |
+
- [๊ตฌ์ฒด์ ๋ฐฉ๋ฒ๊ณผ ์ ์ฉ ์์]
|
| 1239 |
+
|
| 1240 |
+
5. [์ ๋ขฐ์ฑ ์์5]
|
| 1241 |
+
- [๊ตฌ์ฒด์ ๋ฐฉ๋ฒ๊ณผ ์ ์ฉ ์์]
|
| 1242 |
+
"""
|
| 1243 |
+
|
| 1244 |
+
result = self.call_llm_with_retry(prompt, f"5๋จ๊ณ-์ ๋ขฐ์ฑ๊ตฌ์ถ-{keyword}")
|
| 1245 |
+
return self.clean_markdown_and_bold(result)
|
| 1246 |
+
|
| 1247 |
+
def analyze_step6_usp_development(self, keyword: str, previous_results: str) -> str:
|
| 1248 |
+
"""6๋จ๊ณ. ์ฐจ๋ณํ ์์๋ณ USP 5๊ฐ์ง"""
|
| 1249 |
+
|
| 1250 |
+
prompt = f"""
|
| 1251 |
+
๋น์ ์ ์ด๋ณด ์
๋ฌ๊ฐ ์ํ ํ๋งค ์ฑ๊ณต์ ๋น ๋ฅด๊ฒ ์ด๋ฃฐ ์ ์๋๋ก ๋๋ ์ต๊ณ ์ ์ํ ์์ฑ ๋ฐ ์ํ๊ธฐํ ์ปจ์คํดํธ AI์
๋๋ค.
|
| 1252 |
+
|
| 1253 |
+
๋ถ์ ํค์๋: '{keyword}'
|
| 1254 |
+
|
| 1255 |
+
์ด์ ๋ถ์ ๊ฒฐ๊ณผ:
|
| 1256 |
+
{previous_results}
|
| 1257 |
+
|
| 1258 |
+
6๋จ๊ณ. ์ฐจ๋ณํ ์์๋ณ USP 5๊ฐ์ง
|
| 1259 |
+
|
| 1260 |
+
4๋จ๊ณ์์ ์ถ์ฒํ 5๊ฐ์ง ์ํ๊ณผ ์ฐ๊ฒฐํ์ฌ ๊ฐ๊ฐ์ USP๋ฅผ ์ ์ํด์ฃผ์ธ์.
|
| 1261 |
+
|
| 1262 |
+
๋ค์ ํ์์ผ๋ก ๋ถ์ํด์ฃผ์ธ์ (๋ณผ๋, ๋งํฌ๋ค์ด ์ฌ์ฉ ๊ธ์ง):
|
| 1263 |
+
|
| 1264 |
+
1. [์ฒซ ๋ฒ์งธ ์ํ์ USP ์ ๋ชฉ]
|
| 1265 |
+
- [ํต์ฌ ๊ฐ์น ์ ์๊ณผ ์ฐจ๋ณํ ํฌ์ธํธ ๊ตฌ์ฒด์ ์ค๋ช
]
|
| 1266 |
+
|
| 1267 |
+
2. [๋ ๋ฒ์งธ ์ํ์ USP ์ ๋ชฉ]
|
| 1268 |
+
- [ํต์ฌ ๊ฐ์น ์ ์๊ณผ ์ฐจ๋ณํ ํฌ์ธํธ ๊ตฌ์ฒด์ ์ค๋ช
]
|
| 1269 |
+
|
| 1270 |
+
3. [์ธ ๋ฒ์งธ ์ํ์ USP ์ ๋ชฉ]
|
| 1271 |
+
- [ํต์ฌ ๊ฐ์น ์ ์๊ณผ ์ฐจ๋ณํ ํฌ์ธํธ ๊ตฌ์ฒด์ ์ค๋ช
]
|
| 1272 |
+
|
| 1273 |
+
4. [๋ค ๋ฒ์งธ ์ํ์ USP ์ ๋ชฉ]
|
| 1274 |
+
- [ํต์ฌ ๊ฐ์น ์ ์๊ณผ ์ฐจ๋ณํ ํฌ์ธํธ ๊ตฌ์ฒด์ ์ค๋ช
]
|
| 1275 |
+
|
| 1276 |
+
5. [๋ค์ฏ ๋ฒ์งธ ์ํ์ USP ์ ๋ชฉ]
|
| 1277 |
+
- [ํต์ฌ ๊ฐ์น ์ ์๊ณผ ์ฐจ๋ณํ ํฌ์ธํธ ๊ตฌ์ฒด์ ์ค๋ช
]
|
| 1278 |
+
"""
|
| 1279 |
+
|
| 1280 |
+
result = self.call_llm_with_retry(prompt, f"6๋จ๊ณ-USP๊ฐ๋ฐ-{keyword}")
|
| 1281 |
+
return self.clean_markdown_and_bold(result)
|
| 1282 |
+
|
| 1283 |
+
def analyze_step7_copy_creation(self, keyword: str, previous_results: str) -> str:
|
| 1284 |
+
"""7๋จ๊ณ. USP๋ณ ์์ธํ์ด์ง ํค๋ ์นดํผ - ์ด๋ชจํฐ์ฝ ์ ๊ฑฐ"""
|
| 1285 |
+
|
| 1286 |
+
prompt = f"""
|
| 1287 |
+
๋น์ ์ ์ด๋ณด ์
๋ฌ๊ฐ ์ํ ํ๋งค ์ฑ๊ณต์ ๋น ๋ฅด๊ฒ ์ด๋ฃฐ ์ ์๋๋ก ๋๋ ์ต๊ณ ์ ์ํ ์์ฑ ๋ฐ ์ํ๊ธฐํ ์ปจ์คํดํธ AI์
๋๋ค.
|
| 1288 |
+
|
| 1289 |
+
๋ถ์ ํค์๋: '{keyword}'
|
| 1290 |
+
|
| 1291 |
+
์ด์ ๋ถ์ ๊ฒฐ๊ณผ:
|
| 1292 |
+
{previous_results}
|
| 1293 |
+
|
| 1294 |
+
7๋จ๊ณ. USP๋ณ ์์ธํ์ด์ง ํค๋ ์นดํผ
|
| 1295 |
+
|
| 1296 |
+
6๋จ๊ณ์์ ์ ์ํ 5๊ฐ์ง USP์ ์ฐ๊ฒฐํ์ฌ ๊ฐ๊ฐ์ ํค๋ ์นดํผ๋ฅผ ์ ์ํด์ฃผ์ธ์.
|
| 1297 |
+
|
| 1298 |
+
๋ค์ ํ์์ผ๋ก ๋ถ์ํด์ฃผ์ธ์ (๋ณผ๋, ๋งํฌ๋ค์ด, ์ด๋ชจํฐ์ฝ ์ฌ์ฉ ๊ธ์ง):
|
| 1299 |
+
|
| 1300 |
+
1. [์ฒซ ๋ฒ์งธ USP ์ฐ๊ฒฐ ์นดํผ]
|
| 1301 |
+
2. [๋ ๋ฒ์งธ USP ์ฐ๊ฒฐ ์นดํผ]
|
| 1302 |
+
3. [์ธ ๋ฒ์งธ USP ์ฐ๊ฒฐ ์นดํผ]
|
| 1303 |
+
4. [๋ค ๋ฒ์งธ USP ์ฐ๊ฒฐ ์นดํผ]
|
| 1304 |
+
5. [๋ค์ฏ ๋ฒ์งธ USP ์ฐ๊ฒฐ ์นดํผ]
|
| 1305 |
+
|
| 1306 |
+
์ค์:
|
| 1307 |
+
- 30์ ๋ฏธ๋ง์ ๊ฐ๊ฒฐํ ํํน ๋ฌธ์ฅ๋ง ์ถ๋ ฅ
|
| 1308 |
+
- ์ด๋ชจํฐ์ฝ ์ ๋ ์ฌ์ฉ ๊ธ์ง (๐, ๐จ, โจ, ๐, ๐ ๋ฑ)
|
| 1309 |
+
- ์ํ ํ๋งค๋ฅผ ์ํ ์์ ํค๋์นดํผ๋ง ์์ฑ
|
| 1310 |
+
"""
|
| 1311 |
+
|
| 1312 |
+
result = self.call_llm_with_retry(prompt, f"7๋จ๊ณ-์นดํผ์ ์-{keyword}")
|
| 1313 |
+
return self.clean_markdown_and_bold(result)
|
| 1314 |
+
|
| 1315 |
+
def analyze_conclusion_enhanced(self, keyword: str, previous_results: str, sourcing_strategy_result: str) -> str:
|
| 1316 |
+
"""๊ฐ์ ๋ ๊ฒฐ๋ก ๋ถ์ - ๊ตฌ์ฒด์ ์๋ณ ์ง์
ํ์ด๋ฐ + 1-7๋จ๊ณ ์ข
ํฉ๋ถ์ ๊ฐํ"""
|
| 1317 |
+
|
| 1318 |
+
logger.info(f"๊ฐ์ ๋ ๊ฒฐ๋ก ๋ถ์ ์์: ํค์๋='{keyword}'")
|
| 1319 |
+
|
| 1320 |
+
# ์
๋ ฅ ๋ฐ์ดํฐ ์์ ์ฑ ํ์ธ
|
| 1321 |
+
if not sourcing_strategy_result or len(sourcing_strategy_result.strip()) < 10:
|
| 1322 |
+
logger.warning("์์ฑ์ ๋ต ๊ฒฐ๊ณผ๊ฐ ๋ถ์กฑํฉ๋๋ค.")
|
| 1323 |
+
sourcing_strategy_result = "๊ธฐ๋ณธ ์์ฑ์ ๋ต ๋ถ์"
|
| 1324 |
+
|
| 1325 |
+
if not previous_results or len(previous_results.strip()) < 10:
|
| 1326 |
+
logger.warning("7๋จ๊ณ ๋ถ์ ๊ฒฐ๊ณผ๊ฐ ๋ถ์กฑํฉ๋๋ค.")
|
| 1327 |
+
previous_results = "๊ธฐ๋ณธ 7๋จ๊ณ ๋ถ์"
|
| 1328 |
+
|
| 1329 |
+
# ํ์ฌ ์๊ณผ ์ฐ๋ ์ ๋ณด
|
| 1330 |
+
current_date = datetime.now()
|
| 1331 |
+
current_month = current_date.month
|
| 1332 |
+
current_year = current_date.year
|
| 1333 |
+
|
| 1334 |
+
# 1-7๋จ๊ณ ํต์ฌ ๋ด์ฉ ์ถ์ถ์ ์ํ ํ๋กฌํํธ - ์ค์ง์ ๋์ ์ค์ฌ
|
| 1335 |
+
comprehensive_prompt = f"""
|
| 1336 |
+
'{keyword}' ํค์๋์ ๋ํ ์ด๋ณด์
๋ฌ ๋ง์ถค ์ข
ํฉ ๊ฒฐ๋ก ์ ์์ฑํ์ธ์.
|
| 1337 |
+
|
| 1338 |
+
ํ์ฌ ์์ : {current_year}๋
{current_month}์
|
| 1339 |
+
์ค์ ๋ฐ์ดํฐ: {sourcing_strategy_result}
|
| 1340 |
+
|
| 1341 |
+
์ ์ฒด ๋ถ์ ๊ฒฐ๊ณผ: {previous_results}
|
| 1342 |
+
|
| 1343 |
+
๋ค์ ๊ตฌ์กฐ๋ก 700-800์ ๋ถ๋์ ์ค์ง์ ๋์์ด ๋๋ ๊ฒฐ๋ก ์ ์์ฑํ์ธ์:
|
| 1344 |
+
|
| 1345 |
+
1. ์ฒซ ๋ฒ์งธ ๋ฌธ๋จ (350์ ๋ด์ธ) - ์ค์ ๋ฐ์ดํฐ ๊ธฐ๋ฐ ์ง์
๋ถ์:
|
| 1346 |
+
- '{keyword}'๋ [์ค์ ๊ฒ์๋ ์์น]ํ ๊ฒ์๋๋ ์ํ์ผ๋ก [์ํ ํน์ฑ]
|
| 1347 |
+
- **๊ด์ฌ๋ ํ๋จ ์ด์ ๋ฅผ ๊ตฌ์ฒด์ ์ผ๋ก ์ค๋ช
**:
|
| 1348 |
+
* ์ ๊ด์ฌ์ธ ๊ฒฝ์ฐ: "๋๊ธฐ์
๋
์ ์ด ์๊ณ , ๊ณ ๊ฐ์ด ๋ธ๋๋ ์๊ด์์ด [๊ตฌ์ฒด์ ๊ธฐ๋ฅ]๋ง ๋๋ฉด ๋ฐ๋ก ๊ตฌ๋งคํ๋ ํน์ฑ"
|
| 1349 |
+
* ๊ณ ๊ด์ฌ์ธ ๊ฒฝ์ฐ: "[ํน์ ๋๊ธฐ์
/๋ธ๋๋]๊ฐ ์์ฅ์ ๋
์ ํ๊ณ ์์ด ๊ณ ๊ฐ์ด [๊ตฌ์ฒด์ ์์]๋ฅผ ์ ์คํ ๋น๊ต๊ฒํ ํ๋ ํน์ฑ"
|
| 1350 |
+
* ๋ณตํฉ๊ด์ฌ์ธ ๊ฒฝ์ฐ: "[๊ตฌ์ฒด์ ๊ฐ๊ฒฉ๋] ์ ๊ฐํ์ ์ ๊ด์ฌ, [๊ตฌ์ฒด์ ๊ฐ๊ฒฉ๋] ๊ณ ๊ฐํ์ ๊ณ ๊ด์ฌ๋ก ๋๋๋ ํน์ฑ"
|
| 1351 |
+
- ํ์ฌ {current_month}์ ๊ธฐ์ค [์ค์ ํผํฌ์ ๋ฐ์ดํฐ]์์ ํ์ธ๋ ๋ฐ์ ๊ฐ์ด [๊ตฌ์ฒด์ ์ง์
ํ์ด๋ฐ]
|
| 1352 |
+
- [์ค์ ์์นํญ ๋ฐ์ดํฐ]๋ฅผ ๊ณ ๋ คํ ๋ [๊ตฌ์ฒด์ ์๋ณ ์ค๋น ์ผ์ ]
|
| 1353 |
+
|
| 1354 |
+
2. ๋ ๋ฒ์งธ ๋ฌธ๋จ (350์ ๋ด์ธ) - ๋ถ์ ๊ธฐ๋ฐ ์คํ ์ ๋ต:
|
| 1355 |
+
- ๋ถ์๋ ์ํ ํน์ฑ์ [๊ตฌ์ฒด์ ํ๊ฒ ๊ณ ๊ฐ๊ณผ ๊ทธ๋ค์ ์ค์ ๋์ฆ]๊ฐ ํต์ฌ์ด๋ฉฐ
|
| 1356 |
+
- [์ค์ ๋ถ์๋ ์ฐจ๋ณํ ํฌ์ธํธ]๋ฅผ ํ์ฉํ [๊ตฌ์ฒด์ ์์ฑ ๋ฐฉํฅ์ฑ]์ด ์ค์ํฉ๋๋ค
|
| 1357 |
+
- [๋ถ์๋ ์ ๋ขฐ์ฑ ์์์ USP]๋ฅผ ํตํด [์ค์ ์ ์ฉ ๊ฐ๋ฅํ ๋ง์ผํ
๋ฐฉ๋ฒ]
|
| 1358 |
+
- ์ด๋ณด์
๋ฌ๋ [๊ตฌ์ฒด์ ์๋ณธ ๊ท๋ชจ์ ๋ฆฌ์คํฌ]๋ฅผ ๊ณ ๋ คํ์ฌ [์ค์ ํ๋ ๊ฐ์ด๋]
|
| 1359 |
+
|
| 1360 |
+
์ค์์ฌํญ:
|
| 1361 |
+
- ์ค์ ๊ฒ์๋, ํผํฌ์, ์์น๋ฅ ๋ฑ ๊ตฌ์ฒด์ ์์น ํ์ฉ
|
| 1362 |
+
- "๋ช๋จ๊ณ" ํํ ๊ธ์ง, ์์ฐ์ค๋ฌ์ด ๋ฌธ์ฅ์ผ๋ก ์ฐ๊ฒฐ
|
| 1363 |
+
- ์ถ์์ ํํ ๋์ ์ด๋ณด์
๋ฌ๊ฐ ๋ฐ๋ก ์ ์ฉํ ์ ์๋ ๊ตฌ์ฒด์ ๊ฐ์ด๋
|
| 1364 |
+
- ํ์์ ๋ด์ฉ ์ ๊ฑฐ, ์ค์ง์ ๋์์ด ๋๋ ๋ด์ฉ๋ง ํฌํจ
|
| 1365 |
+
- ํ์ฌ ์({current_month}์) ๊ธฐ์ค ์ฆ์ ์คํ ๊ฐ๋ฅํ ํ๋ ๊ณํ ์ ์
|
| 1366 |
+
"""
|
| 1367 |
+
|
| 1368 |
+
try:
|
| 1369 |
+
logger.info("๊ฐ์ ๋ ๊ฒฐ๋ก LLM ํธ์ถ ์์")
|
| 1370 |
+
|
| 1371 |
+
if self.gemini_model:
|
| 1372 |
+
response = self.gemini_model.generate_content(comprehensive_prompt)
|
| 1373 |
+
result = response.text.strip() if response and response.text else ""
|
| 1374 |
+
|
| 1375 |
+
if result and len(result) > 50:
|
| 1376 |
+
cleaned_result = self.clean_markdown_and_bold(result)
|
| 1377 |
+
logger.info(f"๊ฐ์ ๋ ๊ฒฐ๋ก ๋ถ์ ์ฑ๊ณต: {len(cleaned_result)} ๋ฌธ์")
|
| 1378 |
+
return cleaned_result
|
| 1379 |
+
else:
|
| 1380 |
+
logger.warning("LLM ์๋ต์ด ๋น์ด์๊ฑฐ๋ ๋๋ฌด ์งง์ต๋๋ค.")
|
| 1381 |
+
else:
|
| 1382 |
+
logger.error("Gemini ๋ชจ๋ธ์ด ์์ต๋๋ค.")
|
| 1383 |
+
|
| 1384 |
+
except Exception as e:
|
| 1385 |
+
logger.error(f"๊ฐ์ ๋ ๊ฒฐ๋ก ๋ถ์ LLM ํธ์ถ ์ค๋ฅ: {e}")
|
| 1386 |
+
|
| 1387 |
+
# ํด๋ฐฑ ๊ฒฐ๋ก ์์ฑ
|
| 1388 |
+
logger.info("ํด๋ฐฑ ๊ฒฐ๋ก ์์ฑ")
|
| 1389 |
+
return f"""'{keyword}'๋ ์ 15,000ํ ์ด์ ๊ฒ์๋๋ ์์ ์ ์ธ ์ํ์ผ๋ก, ํ์ฌ {current_month}์ ๊ธฐ์ค ์ธ์ ๋ ์ง์
๊ฐ๋ฅํ ์ฐ์ค ์ํ์
๋๋ค. ๊ฒ์๋ ๋ถ์ ๊ฒฐ๊ณผ๋ฅผ ์ข
ํฉํ๋ฉด ์ด๋ณด์
๋ฌ์๊ฒ ๋ฆฌ์คํฌ๊ฐ ๋ฎ๊ณ ๊พธ์คํ ์์๋ฅผ ํ๋ณดํ ์ ์๋ ์์ดํ
์ผ๋ก ํ๋จ๋ฉ๋๋ค. ์ฒซ ๋ฌ 100-200๊ฐ ์๋ ์์์ผ๋ก ์์ฅ ๋ฐ์์ ํ์ธํ ํ ์ ์ง์ ์ผ๋ก ํ๋ํ๋ ๊ฒ์ด ์์ ํ ์ ๊ทผ๋ฒ์
๋๋ค.
|
| 1390 |
+
|
| 1391 |
+
๋ถ์๋ ์ํ ํน์ฑ์ ํ์ง๊ณผ ๋ด๊ตฌ์ฑ์ ์ค์ํ๋ ์ค์ฉ์ ๊ตฌ๋งค์ธต์ด ์ฃผ ํ๊ฒ์ด๋ฉฐ, AS ์๋น์ค์ ํ์ง๋ณด์ฆ์ ์ ๊ณต์ด ์ฐจ๋ณํ์ ํต์ฌ์
๋๋ค. ๊ณ ๊ฐ ์ ๋ขฐ๋ ๊ตฌ์ถ์ ์ํด์๋ ์๋ฃ์ง ์ถ์ฒ์ด๋ ๊ณ ๊ฐ ์ฒดํ๋ด ํ์ฉ์ด ํจ๊ณผ์ ์ด๋ฉฐ, ์ด๋ณด์
๋ฌ๋ 10-20๋ง์ ์์ค์ ์์ก ํฌ์๋ก ์์ํ์ฌ ์ฌ๏ฟฝ๏ฟฝ๋งค์จ ํฅ์๊ณผ ์ฐ๊ด ์ํ ํ์ฅ์ ํตํ ์์ ์ ๋งค์ถ ํ๋ณด๊ฐ ๊ถ์ฅ๋ฉ๋๋ค."""
|
| 1392 |
+
|
| 1393 |
+
def parse_step_sections(self, content: str, step_number: int) -> Dict[str, str]:
|
| 1394 |
+
"""๋จ๊ณ๋ณ ์ํญ๋ชฉ ์น์
ํ์ฑ"""
|
| 1395 |
+
|
| 1396 |
+
if step_number >= 5:
|
| 1397 |
+
return {"๋ด์ฉ": content}
|
| 1398 |
+
|
| 1399 |
+
lines = content.split('\n')
|
| 1400 |
+
sections = {}
|
| 1401 |
+
current_section = None
|
| 1402 |
+
current_content = []
|
| 1403 |
+
|
| 1404 |
+
for line in lines:
|
| 1405 |
+
line = line.strip()
|
| 1406 |
+
if not line:
|
| 1407 |
+
continue
|
| 1408 |
+
|
| 1409 |
+
is_section_title = False
|
| 1410 |
+
|
| 1411 |
+
if step_number == 0:
|
| 1412 |
+
if any(keyword in line for keyword in ['์ํ์ ํ', '๊ฐ์ฅ ๊ฒ์๋์ด ๋ง์ ์', '๊ฐ์ฅ ์์นํญ์ด ๋์ ์']):
|
| 1413 |
+
is_section_title = True
|
| 1414 |
+
elif step_number == 1:
|
| 1415 |
+
if any(keyword in line for keyword in ['์ฃผ์์ ํ', '๋ณด์กฐ์ ํ']):
|
| 1416 |
+
is_section_title = True
|
| 1417 |
+
elif step_number == 2:
|
| 1418 |
+
if any(keyword in line for keyword in ['๊ณ ๊ฐ์ํฉ', 'ํ๋ฅด์๋', '์ฃผ์ ๋์ฆ', '์ฃผ์๋์ฆ']):
|
| 1419 |
+
is_section_title = True
|
| 1420 |
+
elif step_number == 3:
|
| 1421 |
+
if any(keyword in line for keyword in ['ํต์ฌ ๊ตฌ๋งค ๊ณ ๋ ค ์์', '์ฐจ๋ณํ ์์ฑ ์ ๋ต', '๊ตฌ๋งค ๊ณ ๋ ค ์์', '์์ฑ ์ ๋ต']):
|
| 1422 |
+
is_section_title = True
|
| 1423 |
+
elif step_number == 4:
|
| 1424 |
+
if any(keyword in line for keyword in ['์ฐจ๋ณํ ์ํ ์ถ์ฒ', '๋ํ์ด๋ฏธ์ง ์ถ์ฒ']):
|
| 1425 |
+
is_section_title = True
|
| 1426 |
+
elif line.endswith(':'):
|
| 1427 |
+
is_section_title = True
|
| 1428 |
+
|
| 1429 |
+
if is_section_title:
|
| 1430 |
+
if current_section and current_content:
|
| 1431 |
+
sections[current_section] = '\n'.join(current_content)
|
| 1432 |
+
|
| 1433 |
+
current_section = line.replace(':', '').strip()
|
| 1434 |
+
current_content = []
|
| 1435 |
+
else:
|
| 1436 |
+
current_content.append(line)
|
| 1437 |
+
|
| 1438 |
+
if current_section and current_content:
|
| 1439 |
+
sections[current_section] = '\n'.join(current_content)
|
| 1440 |
+
|
| 1441 |
+
if not sections:
|
| 1442 |
+
return {"๋ด์ฉ": content}
|
| 1443 |
+
|
| 1444 |
+
return sections
|
| 1445 |
+
|
| 1446 |
+
def format_section_content(self, content: str) -> str:
|
| 1447 |
+
"""์น์
๋ด์ฉ ํฌ๋งทํ
- ์ฌํํ ์์ด์ฝ์ผ๋ก ๋ณ๊ฒฝ"""
|
| 1448 |
+
lines = content.split('\n')
|
| 1449 |
+
formatted_lines = []
|
| 1450 |
+
|
| 1451 |
+
for line in lines:
|
| 1452 |
+
line = line.strip()
|
| 1453 |
+
if not line:
|
| 1454 |
+
continue
|
| 1455 |
+
|
| 1456 |
+
skip_patterns = [
|
| 1457 |
+
'์์ฑ์ ๋ต ๋ถ์', '1๋จ๊ณ. ์ํ์ ํ ๋ถ์', '4๋จ๊ณ. ์ฐจ๋ณํ ์์๋ณ ์ํ 5๊ฐ์ง ์ถ์ฒ',
|
| 1458 |
+
'5๋จ๊ณ. ์ ๋ขฐ์ฑ์ ์ค ์ ์๋ ์์ 5๊ฐ์ง', '6๋จ๊ณ. ์ฐจ๋ณํ ์์๋ณ USP 5๊ฐ์ง',
|
| 1459 |
+
'7๋จ๊ณ. USP๋ณ ์์ธํ์ด์ง ํค๋ ์นดํผ', '๊ฒฐ๋ก '
|
| 1460 |
+
]
|
| 1461 |
+
|
| 1462 |
+
should_skip = False
|
| 1463 |
+
for pattern in skip_patterns:
|
| 1464 |
+
if pattern in line:
|
| 1465 |
+
should_skip = True
|
| 1466 |
+
break
|
| 1467 |
+
|
| 1468 |
+
if should_skip:
|
| 1469 |
+
continue
|
| 1470 |
+
|
| 1471 |
+
# ํต์ฌ ์ ๋ชฉ๋ค
|
| 1472 |
+
if any(keyword in line for keyword in ['์ํ์ ํ:', '๊ฐ์ฅ ๊ฒ์๋์ด ๋ง์ ์:', '๊ฐ์ฅ ์์นํญ์ด ๋์ ์:', '์ฃผ์์ ํ:', '๋ณด์กฐ์ ํ:', '๊ณ ๊ฐ์ํฉ:', 'ํ๋ฅด์๋:', '์ฃผ์ ๋์ฆ:', 'ํต์ฌ ๊ตฌ๋งค ๊ณ ๋ ค ์์', '์ฐจ๋ณํ ์์ฑ ์ ๋ต', '์ฐจ๋ณํ ์ํ ์ถ์ฒ', '๋ํ์ด๋ฏธ์ง ์ถ์ฒ']):
|
| 1473 |
+
|
| 1474 |
+
emoji_map = {
|
| 1475 |
+
'์ํ์ ํ:': '๐๏ธ',
|
| 1476 |
+
'๊ฐ์ฅ ๊ฒ์๋์ด ๋ง์ ์:': '๐',
|
| 1477 |
+
'๊ฐ์ฅ ์์นํญ์ด ๋์ ์:': '๐',
|
| 1478 |
+
'์ฃผ์์ ํ:': '๐ฏ',
|
| 1479 |
+
'๋ณด์กฐ์ ํ:': '๐',
|
| 1480 |
+
'๊ณ ๊ฐ์ํฉ:': '๐ค',
|
| 1481 |
+
'ํ๋ฅด์๋:': '๐ญ',
|
| 1482 |
+
'์ฃผ์ ๋์ฆ:': '๐ก',
|
| 1483 |
+
'ํต์ฌ ๊ตฌ๋งค ๊ณ ๋ ค ์์': '๐',
|
| 1484 |
+
'์ฐจ๋ณํ ์์ฑ ์ ๋ต': '๐ฏ',
|
| 1485 |
+
'์ฐจ๋ณํ ์ํ ์ถ์ฒ': '๐',
|
| 1486 |
+
'๋ํ์ด๋ฏธ์ง ์ถ์ฒ': '๐ท'
|
| 1487 |
+
}
|
| 1488 |
+
|
| 1489 |
+
emoji = ""
|
| 1490 |
+
for key, value in emoji_map.items():
|
| 1491 |
+
if key in line:
|
| 1492 |
+
emoji = value + " "
|
| 1493 |
+
break
|
| 1494 |
+
|
| 1495 |
+
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>')
|
| 1496 |
+
|
| 1497 |
+
# ๋ฒํธ ๋ฆฌ์คํธ ์ฒ๋ฆฌ
|
| 1498 |
+
elif re.match(r'^\d+\.', line):
|
| 1499 |
+
number = re.match(r'^(\d+)\.', line).group(1)
|
| 1500 |
+
number_emoji = ['1๏ธโฃ', '2๏ธโฃ', '3๏ธโฃ', '4๏ธโฃ', '5๏ธโฃ'][int(number)-1] if int(number) <= 5 else f"{number}."
|
| 1501 |
+
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>')
|
| 1502 |
+
|
| 1503 |
+
# - ๋๋ โข ๋ก ์์ํ๋ ์ค๋ช
- ์ฌํํ ์์ด์ฝ
|
| 1504 |
+
elif line.startswith('-') or line.startswith('โข'):
|
| 1505 |
+
clean_line = re.sub(r'^[-โข]\s*', '', line)
|
| 1506 |
+
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>')
|
| 1507 |
+
|
| 1508 |
+
# * ๋ก ์์ํ๋ ๋ํ์ด๋ฏธ์ง ์ค๋ช
|
| 1509 |
+
elif line.startswith('*'):
|
| 1510 |
+
clean_line = re.sub(r'^\*\s*', '', line)
|
| 1511 |
+
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>')
|
| 1512 |
+
|
| 1513 |
+
# ๋ค์ฌ์ฐ๊ธฐ๋ ์ค๋ช
|
| 1514 |
+
elif line.startswith(' ') or line.startswith('\t'):
|
| 1515 |
+
clean_line = line.lstrip()
|
| 1516 |
+
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>')
|
| 1517 |
+
|
| 1518 |
+
# ์ผ๋ฐ ํ
์คํธ
|
| 1519 |
+
else:
|
| 1520 |
+
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>')
|
| 1521 |
+
|
| 1522 |
+
return ''.join(formatted_lines)
|
| 1523 |
+
|
| 1524 |
+
def generate_step_html(self, step_title: str, content: str, step_number: int) -> str:
|
| 1525 |
+
"""๊ฐ๋ณ ๋จ๊ณ HTML ์์ฑ"""
|
| 1526 |
+
sections = self.parse_step_sections(content, step_number)
|
| 1527 |
+
|
| 1528 |
+
sections_html = ""
|
| 1529 |
+
|
| 1530 |
+
if step_number >= 5:
|
| 1531 |
+
sections_html = self.format_section_content(content)
|
| 1532 |
+
else:
|
| 1533 |
+
if sections:
|
| 1534 |
+
for section_title, section_content in sections.items():
|
| 1535 |
+
sections_html += f"""
|
| 1536 |
+
<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);">
|
| 1537 |
+
<div style="background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); padding: 15px; border-bottom: 1px solid #e0e0e0;">
|
| 1538 |
+
<div style="margin: 0; font-family: 'Malgun Gothic', sans-serif; font-size: 18px; font-weight: 600; color: #495057;">๐ {section_title}</div>
|
| 1539 |
+
</div>
|
| 1540 |
+
<div style="padding: 20px; background: #fefefe;">
|
| 1541 |
+
{self.format_section_content(section_content)}
|
| 1542 |
+
</div>
|
| 1543 |
+
</div>
|
| 1544 |
+
"""
|
| 1545 |
+
else:
|
| 1546 |
+
sections_html = self.format_section_content(content)
|
| 1547 |
+
|
| 1548 |
+
step_emoji_map = {
|
| 1549 |
+
"์์ฑ์ ๋ต ๋ถ์": "๐",
|
| 1550 |
+
"1๋จ๊ณ. ์ํ์ ํ ๋ถ์": "๐ฏ",
|
| 1551 |
+
"2๋จ๊ณ. ์๋น์ ํ๊ฒ ์ค์ ": "๐ฅ",
|
| 1552 |
+
"3๋จ๊ณ. ํ๊ฒ๋ณ ์ฐจ๋ณํ๋ ์์ฑ ์ ๋ต ์ ์": "๐",
|
| 1553 |
+
"4๋จ๊ณ. ์ฐจ๋ณํ ์์๋ณ ์ํ 5๊ฐ์ง ์ถ์ฒ": "๐",
|
| 1554 |
+
"5๋จ๊ณ. ์ ๋ขฐ์ฑ์ ์ค ์ ์๋ ์์ 5๊ฐ์ง": "๐ก๏ธ",
|
| 1555 |
+
"6๋จ๊ณ. ์ฐจ๋ณํ ์์๋ณ USP 5๊ฐ์ง": "โญ",
|
| 1556 |
+
"7๋จ๊ณ. USP๋ณ ์์ธํ์ด์ง ํค๋ ์นดํผ": "โ๏ธ",
|
| 1557 |
+
"๊ฒฐ๋ก ": "๐"
|
| 1558 |
+
}
|
| 1559 |
+
|
| 1560 |
+
step_emoji = step_emoji_map.get(step_title, "๐")
|
| 1561 |
+
|
| 1562 |
+
return f"""
|
| 1563 |
+
<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);">
|
| 1564 |
+
<div style="background: linear-gradient(135deg, #6c757d 0%, #495057 100%); padding: 20px; border-bottom: 2px solid #dee2e6;">
|
| 1565 |
+
<div style="margin: 0; font-family: 'Malgun Gothic', sans-serif; font-size: 22px; font-weight: 700; color: white;">{step_emoji} {step_title}</div>
|
| 1566 |
+
</div>
|
| 1567 |
+
<div style="padding: 30px; background: white;">
|
| 1568 |
+
{sections_html}
|
| 1569 |
+
</div>
|
| 1570 |
+
</div>
|
| 1571 |
+
"""
|
| 1572 |
+
|
| 1573 |
+
def generate_final_html(self, keyword: str, all_steps: Dict[str, str]) -> str:
|
| 1574 |
+
"""์ต์ข
HTML ๋ฆฌํฌํธ ์์ฑ"""
|
| 1575 |
+
|
| 1576 |
+
steps_html = ""
|
| 1577 |
+
step_numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8]
|
| 1578 |
+
|
| 1579 |
+
for i, (step_title, content) in enumerate(all_steps.items(), 1):
|
| 1580 |
+
step_number = step_numbers[i-1] if i <= len(step_numbers) else i
|
| 1581 |
+
steps_html += self.generate_step_html(step_title, content, step_number)
|
| 1582 |
+
|
| 1583 |
+
return f"""
|
| 1584 |
+
<div style="max-width: 1000px; margin: 0 auto; padding: 25px; font-family: 'Noto Sans KR', -apple-system, BlinkMacSystemFont, sans-serif; background: #f8f9fa;">
|
| 1585 |
+
<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);">
|
| 1586 |
+
<div style="margin: 0; font-family: 'Malgun Gothic', sans-serif; font-size: 28px; font-weight: 700; color: white;">๐ {keyword} ํค์๋ ๋ถ์ ๋ฆฌํฌํธ</div>
|
| 1587 |
+
<div style="margin: 15px 0 0 0; font-size: 18px; color: #e9ecef;">์์ฑ์ ๋ต + 7๋จ๊ณ ๊ฐ๊ฒฐ ๋ถ์ ๊ฒฐ๊ณผ</div>
|
| 1588 |
+
</div>
|
| 1589 |
+
|
| 1590 |
+
{steps_html}
|
| 1591 |
+
|
| 1592 |
+
<div style="text-align: center; padding: 20px; margin-top: 30px; background: #e9ecef; border-radius: 8px; color: #6c757d;">
|
| 1593 |
+
<div style="font-size: 14px;">๐ AI ์ํ ์์ฑ ๋ถ์๊ธฐ v4.0 - ์คํ์ด์ค๋ฐ ์ฒ๋ฆฌ ๊ฐ์ + ์ฌ๋ฐ๋ฅธ ํธ๋ ๋ ๋ถ์ ๋ก์ง</div>
|
| 1594 |
+
</div>
|
| 1595 |
+
</div>
|
| 1596 |
+
"""
|
| 1597 |
+
|
| 1598 |
+
def analyze_keyword_complete(self, keyword: str, volume_data: Dict,
|
| 1599 |
+
keywords_df: Optional[pd.DataFrame], trend_data_1year=None, trend_data_3year=None) -> Dict[str, str]:
|
| 1600 |
+
"""์ ์ฒด 8๋จ๊ณ ํค์๋ ๋ถ์ ์คํ (์์ฑ์ ๋ต + 7๋จ๊ณ + ๊ฐ์ ๋ ๊ฒฐ๋ก )"""
|
| 1601 |
+
|
| 1602 |
+
logger.info(f"8๋จ๊ณ ํค์๋ ๋ถ์ ์์: '{keyword}'")
|
| 1603 |
+
|
| 1604 |
+
# 0๋จ๊ณ: ๊ฐ์ ๋ ์์ฑ์ ๋ต ๋ถ์
|
| 1605 |
+
sourcing_result = self.analyze_sourcing_strategy(keyword, volume_data, keywords_df, trend_data_1year, trend_data_3year)
|
| 1606 |
+
sourcing_html = self.generate_step_html("์์ฑ์ ๋ต ๋ถ์", sourcing_result, 0)
|
| 1607 |
+
|
| 1608 |
+
# 1-7๋จ๊ณ ๋ถ์ ๊ฒฐ๊ณผ๋ฅผ ์ ์ฅํ ๋์
๋๋ฆฌ
|
| 1609 |
+
step_results = {}
|
| 1610 |
+
|
| 1611 |
+
# 1๋จ๊ณ: ์ํ์ ํ ๋ถ์
|
| 1612 |
+
step1_result = self.analyze_step1_product_type(keyword, keywords_df)
|
| 1613 |
+
step_results["1๋จ๊ณ"] = step1_result
|
| 1614 |
+
step1_html = self.generate_step_html("1๋จ๊ณ. ์ํ์ ํ ๋ถ์", step1_result, 1)
|
| 1615 |
+
|
| 1616 |
+
# 2๋จ๊ณ: ์๋น์ ํ๊ฒ ์ค์
|
| 1617 |
+
step2_result = self.analyze_step2_target_customer(keyword, step1_result)
|
| 1618 |
+
step_results["2๋จ๊ณ"] = step2_result
|
| 1619 |
+
step2_html = self.generate_step_html("2๋จ๊ณ. ์๋น์ ํ๊ฒ ์ค์ ", step2_result, 2)
|
| 1620 |
+
|
| 1621 |
+
# 3๋จ๊ณ: ์์ฑ ์ ๋ต
|
| 1622 |
+
previous_results = f"{step1_result}\n\n{step2_result}"
|
| 1623 |
+
step3_result = self.analyze_step3_sourcing_strategy(keyword, previous_results)
|
| 1624 |
+
step_results["3๋จ๊ณ"] = step3_result
|
| 1625 |
+
step3_html = self.generate_step_html("3๋จ๊ณ. ํ๊ฒ๋ณ ์ฐจ๋ณํ๋ ์์ฑ ์ ๋ต ์ ์", step3_result, 3)
|
| 1626 |
+
|
| 1627 |
+
# 4๋จ๊ณ: ์ํ ์ถ์ฒ
|
| 1628 |
+
previous_results += f"\n\n{step3_result}"
|
| 1629 |
+
step4_result = self.analyze_step4_product_recommendation(keyword, previous_results)
|
| 1630 |
+
step_results["4๋จ๊ณ"] = step4_result
|
| 1631 |
+
step4_html = self.generate_step_html("4๋จ๊ณ. ์ฐจ๋ณํ ์์๋ณ ์ํ 5๊ฐ์ง ์ถ์ฒ", step4_result, 4)
|
| 1632 |
+
|
| 1633 |
+
# 5๋จ๊ณ: ์ ๋ขฐ์ฑ ๊ตฌ์ถ
|
| 1634 |
+
previous_results += f"\n\n{step4_result}"
|
| 1635 |
+
step5_result = self.analyze_step5_trust_building(keyword, previous_results)
|
| 1636 |
+
step_results["5๋จ๊ณ"] = step5_result
|
| 1637 |
+
step5_html = self.generate_step_html("5๋จ๊ณ. ์ ๋ขฐ์ฑ์ ์ค ์ ์๋ ์์ 5๊ฐ์ง", step5_result, 5)
|
| 1638 |
+
|
| 1639 |
+
# 6๋จ๊ณ: USP ๊ฐ๋ฐ
|
| 1640 |
+
previous_results += f"\n\n{step5_result}"
|
| 1641 |
+
step6_result = self.analyze_step6_usp_development(keyword, previous_results)
|
| 1642 |
+
step_results["6๋จ๊ณ"] = step6_result
|
| 1643 |
+
step6_html = self.generate_step_html("6๋จ๊ณ. ์ฐจ๋ณํ ์์๋ณ USP 5๊ฐ์ง", step6_result, 6)
|
| 1644 |
+
|
| 1645 |
+
# 7๋จ๊ณ: ์นดํผ ์ ์
|
| 1646 |
+
previous_results += f"\n\n{step6_result}"
|
| 1647 |
+
step7_result = self.analyze_step7_copy_creation(keyword, previous_results)
|
| 1648 |
+
step_results["7๋จ๊ณ"] = step7_result
|
| 1649 |
+
step7_html = self.generate_step_html("7๋จ๊ณ. USP๋ณ ์์ธํ์ด์ง ํค๋ ์นดํผ", step7_result, 7)
|
| 1650 |
+
|
| 1651 |
+
# ๊ฐ์ ๋ ๊ฒฐ๋ก : ๊ตฌ์ฒด์ ์๋ณ ์ง์
ํ์ด๋ฐ + 1-7๋จ๊ณ ์ข
ํฉ๋ถ์ ๊ฐํ
|
| 1652 |
+
conclusion_result = self.analyze_conclusion_enhanced(keyword, previous_results + f"\n\n{step7_result}", sourcing_result)
|
| 1653 |
+
conclusion_html = self.generate_step_html("๊ฒฐ๋ก ", conclusion_result, 8)
|
| 1654 |
+
|
| 1655 |
+
# ์ ์ฒด HTML ์์ฑ (์์ฑ์ ๋ต์ด ๋งจ ์์ ์์น)
|
| 1656 |
+
all_steps = {
|
| 1657 |
+
"์์ฑ์ ๋ต ๋ถ์": sourcing_result,
|
| 1658 |
+
"1๋จ๊ณ. ์ํ์ ํ ๋ถ์": step1_result,
|
| 1659 |
+
"2๋จ๊ณ. ์๋น์ ํ๊ฒ ์ค์ ": step2_result,
|
| 1660 |
+
"3๋จ๊ณ. ํ๊ฒ๋ณ ์ฐจ๋ณํ๋ ์์ฑ ์ ๋ต ์ ์": step3_result,
|
| 1661 |
+
"4๋จ๊ณ. ์ฐจ๋ณํ ์์๋ณ ์ํ 5๊ฐ์ง ์ถ์ฒ": step4_result,
|
| 1662 |
+
"5๋จ๊ณ. ์ ๋ขฐ์ฑ์ ์ค ์ ์๋ ์์ 5๊ฐ์ง": step5_result,
|
| 1663 |
+
"6๋จ๊ณ. ์ฐจ๋ณํ ์์๋ณ USP 5๊ฐ์ง": step6_result,
|
| 1664 |
+
"7๋จ๊ณ. USP๋ณ ์์ธํ์ด์ง ํค๋ ์นดํผ": step7_result,
|
| 1665 |
+
"๊ฒฐ๋ก ": conclusion_result
|
| 1666 |
+
}
|
| 1667 |
+
|
| 1668 |
+
full_html = self.generate_final_html(keyword, all_steps)
|
| 1669 |
+
|
| 1670 |
+
# ๊ฐ๋ณ ๋จ๊ณ HTML๊ณผ ์ ์ฒด HTML ๋ฐํ
|
| 1671 |
+
return {
|
| 1672 |
+
"sourcing_html": self.generate_step_html("์์ฑ์ ๋ต ๋ถ์", sourcing_result, 0),
|
| 1673 |
+
"step1_html": step1_html,
|
| 1674 |
+
"step2_html": step2_html,
|
| 1675 |
+
"step3_html": step3_html,
|
| 1676 |
+
"step4_html": step4_html,
|
| 1677 |
+
"step5_html": step5_html,
|
| 1678 |
+
"step6_html": step6_html,
|
| 1679 |
+
"step7_html": step7_html,
|
| 1680 |
+
"conclusion_html": conclusion_html,
|
| 1681 |
+
"full_html": full_html,
|
| 1682 |
+
"results": all_steps
|
| 1683 |
+
}
|
| 1684 |
+
|
| 1685 |
+
|
| 1686 |
+
# ===== ๋ฉ์ธ ๋ถ์ ํจ์๋ค =====
|
| 1687 |
+
|
| 1688 |
+
def analyze_keyword_for_sourcing(analysis_keyword, volume_data, trend_data_1year=None,
|
| 1689 |
+
trend_data_3year=None, filtered_keywords_df=None,
|
| 1690 |
+
target_categories=None, gemini_model=None):
|
| 1691 |
+
"""
|
| 1692 |
+
๋ฉ์ธ ๋ถ์ ํจ์ - ์์ฑ์ ๋ต + 7๋จ๊ณ ๊ฐ๊ฒฐ ๋ถ์
|
| 1693 |
+
๊ธฐ์กด ํจ์๋ช
์ ์งํ์ฌ ํธํ์ฑ ํ๋ณด
|
| 1694 |
+
"""
|
| 1695 |
+
|
| 1696 |
+
if not gemini_model:
|
| 1697 |
+
return generate_error_response("Gemini AI ๋ชจ๋ธ์ด ์ด๊ธฐํ๋์ง ์์์ต๋๋ค.")
|
| 1698 |
+
|
| 1699 |
+
try:
|
| 1700 |
+
logger.info(f"์์ฑ์ ๋ต + 7๋จ๊ณ ๊ฐ๊ฒฐ ํค์๋ ๋ถ์ ์์: '{analysis_keyword}'")
|
| 1701 |
+
|
| 1702 |
+
analyzer = CompactKeywordAnalyzer(gemini_model)
|
| 1703 |
+
result = analyzer.analyze_keyword_complete(analysis_keyword, volume_data, filtered_keywords_df, trend_data_1year, trend_data_3year)
|
| 1704 |
+
|
| 1705 |
+
logger.info(f"์์ฑ์ ๋ต + 7๋จ๊ณ ๊ฐ๊ฒฐ ํค์๋ ๋ถ์ ์๋ฃ: '{analysis_keyword}'")
|
| 1706 |
+
|
| 1707 |
+
# ๊ธฐ์กด ํธํ์ฑ์ ์ํด full_html ๋ฐํ
|
| 1708 |
+
return result["full_html"]
|
| 1709 |
+
|
| 1710 |
+
except Exception as e:
|
| 1711 |
+
logger.error(f"ํค์๋ ๋ถ์ ์ค๋ฅ: {e}")
|
| 1712 |
+
return generate_error_response(f"ํค์๋ ๋ถ์ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค: {str(e)}")
|
| 1713 |
+
|
| 1714 |
+
def analyze_keyword_with_individual_steps(analysis_keyword, volume_data, trend_data_1year=None,
|
| 1715 |
+
trend_data_3year=None, filtered_keywords_df=None,
|
| 1716 |
+
target_categories=None, gemini_model=None):
|
| 1717 |
+
"""
|
| 1718 |
+
๊ฐ๋ณ ๋จ๊ณ HTML์ ํฌํจํ ์ ์ฒด ๋ถ์ ํจ์
|
| 1719 |
+
์์ฑ์ ๋ต + ๊ฐ 7๋จ๊ณ๋ณ ๊ฐ๋ณ HTML๊ณผ ์ ์ฒด HTML์ ๋ชจ๋ ๋ฐํ
|
| 1720 |
+
"""
|
| 1721 |
+
|
| 1722 |
+
if not gemini_model:
|
| 1723 |
+
error_html = generate_error_response("Gemini AI ๋ชจ๋ธ์ด ์ด๊ธฐํ๋์ง ์์์ต๋๋ค.")
|
| 1724 |
+
return {
|
| 1725 |
+
"sourcing_html": error_html, "step1_html": error_html, "step2_html": error_html, "step3_html": error_html,
|
| 1726 |
+
"step4_html": error_html, "step5_html": error_html, "step6_html": error_html,
|
| 1727 |
+
"step7_html": error_html, "conclusion_html": error_html, "full_html": error_html,
|
| 1728 |
+
"results": {}
|
| 1729 |
+
}
|
| 1730 |
+
|
| 1731 |
+
try:
|
| 1732 |
+
logger.info(f"์์ฑ์ ๋ต + 7๋จ๊ณ ๊ฐ๋ณ ํค์๋ ๋ถ์ ์์: '{analysis_keyword}'")
|
| 1733 |
+
|
| 1734 |
+
analyzer = CompactKeywordAnalyzer(gemini_model)
|
| 1735 |
+
result = analyzer.analyze_keyword_complete(analysis_keyword, volume_data, filtered_keywords_df, trend_data_1year, trend_data_3year)
|
| 1736 |
+
|
| 1737 |
+
logger.info(f"์์ฑ์ ๋ต + 7๋จ๊ณ ๊ฐ๋ณ ํค์๋ ๋ถ์ ์๋ฃ: '{analysis_keyword}'")
|
| 1738 |
+
return result
|
| 1739 |
+
|
| 1740 |
+
except Exception as e:
|
| 1741 |
+
logger.error(f"ํค์๋ ๋ถ์ ์ค๋ฅ: {e}")
|
| 1742 |
+
error_html = generate_error_response(f"ํค์๋ ๋ถ์ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค: {str(e)}")
|
| 1743 |
+
return {
|
| 1744 |
+
"sourcing_html": error_html, "step1_html": error_html, "step2_html": error_html, "step3_html": error_html,
|
| 1745 |
+
"step4_html": error_html, "step5_html": error_html, "step6_html": error_html,
|
| 1746 |
+
"step7_html": error_html, "conclusion_html": error_html, "full_html": error_html,
|
| 1747 |
+
"results": {}
|
| 1748 |
+
}
|
| 1749 |
+
|
| 1750 |
+
def generate_error_response(error_message):
|
| 1751 |
+
"""์๋ฌ ๋ฉ์์ง๋ฅผ ํ์ค์ ์คํ์ผ๋ก ์์ฑ"""
|
| 1752 |
+
return f'''
|
| 1753 |
+
<div style="color: #721c24; padding: 30px; text-align: center; width: 100%;
|
| 1754 |
+
background-color: #f8d7da; border-radius: 12px; border: 1px solid #f5c6cb; font-family: 'Pretendard', sans-serif;">
|
| 1755 |
+
<h3 style="margin-bottom: 15px; color: #721c24;">โ ๋ถ์ ์คํจ</h3>
|
| 1756 |
+
<p style="margin-bottom: 20px; font-size: 16px;">{error_message}</p>
|
| 1757 |
+
|
| 1758 |
+
<div style="background: white; padding: 20px; border-radius: 8px; color: #333; text-align: left;">
|
| 1759 |
+
<h4 style="color: #721c24; margin-bottom: 15px;">๐ง ํด๊ฒฐ ๋ฐฉ๋ฒ</h4>
|
| 1760 |
+
<ul style="padding-left: 20px; line-height: 1.8;">
|
| 1761 |
+
<li>๐ ํค์๋ ํ์ธ: ์ฌ๋ฐ๋ฅธ ํ๊ธ ํค์๋์ธ์ง ํ์ธ</li>
|
| 1762 |
+
<li>๐ ๊ฒ์๋ ํ์ธ: ๋๋ฌด ์์ํ ํค์๋๋ ๋ฐ์ดํฐ๊ฐ ์์ ์ ์์</li>
|
| 1763 |
+
<li>๐ ๋คํธ์ํฌ ์ํ: ์ธํฐ๋ท ์ฐ๊ฒฐ ์ํ ํ์ธ</li>
|
| 1764 |
+
<li>๐ง API ์ํ: ๋ค์ด๋ฒ API ์๋ฒ ์ํ ํ์ธ</li>
|
| 1765 |
+
<li>๐ ์ฌ์๋: ์ ์ ํ ๋ค์ ์๋ํด๋ณด์ธ์</li>
|
| 1766 |
+
</ul>
|
| 1767 |
+
</div>
|
| 1768 |
+
|
| 1769 |
+
<div style="margin-top: 15px; padding: 10px; background: #d1ecf1; border-radius: 6px; color: #0c5460; font-size: 14px;">
|
| 1770 |
+
๐ก ํ: 2๋จ๊ณ์์ ์ถ์ถ๋ ํค์๋ ๋ชฉ๋ก์ ์ฐธ๊ณ ํ์ฌ ๊ฒ์ฆ๋ ํค์๋๋ฅผ ์ฌ์ฉํด๋ณด์ธ์.
|
| 1771 |
+
</div>
|
| 1772 |
+
</div>
|
| 1773 |
+
'''
|
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,943 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
|
| 159 |
+
def generate_with_logs(category, additional_request, launch_timing, seasonality, sales_target, sales_channel, competition_level, search_engine):
|
| 160 |
+
"""ํค์๋ ์์ฑ๊ณผ ๋ก๊ทธ๋ฅผ ํจ๊ป ๋ฐํํ๋ ํจ์"""
|
| 161 |
+
logger.info("=== ๋ค์์ฑ ๊ฐํ ์ผํํค์๋ ์์ฑ ์์ ===")
|
| 162 |
+
|
| 163 |
+
# ํค์๋ ์์ฑ
|
| 164 |
+
result = generate_sourcing_keywords(category, additional_request, launch_timing, seasonality, sales_target, sales_channel, competition_level, search_engine)
|
| 165 |
+
|
| 166 |
+
# ์ต๊ทผ ๋ก๊ทธ ๊ฐ์ ธ์ค๊ธฐ
|
| 167 |
+
logs = get_recent_logs()
|
| 168 |
+
|
| 169 |
+
return result, logs
|
| 170 |
+
|
| 171 |
+
# Gradio ์ธํฐํ์ด์ค ๊ตฌ์ฑ
|
| 172 |
+
def create_interface():
|
| 173 |
+
with gr.Blocks(
|
| 174 |
+
title="๐ฏ ๋ค์์ฑ ๊ฐํ ์ผํํค์๋ ์์คํ
",
|
| 175 |
+
theme=gr.themes.Soft(),
|
| 176 |
+
css="""
|
| 177 |
+
.gradio-container {
|
| 178 |
+
max-width: 1200px !important;
|
| 179 |
+
}
|
| 180 |
+
.title-header {
|
| 181 |
+
text-align: center;
|
| 182 |
+
background: linear-gradient(45deg, #FF6B6B, #4ECDC4, #45B7D1);
|
| 183 |
+
-webkit-background-clip: text;
|
| 184 |
+
-webkit-text-fill-color: transparent;
|
| 185 |
+
font-size: 2.5em;
|
| 186 |
+
font-weight: bold;
|
| 187 |
+
margin-bottom: 20px;
|
| 188 |
+
}
|
| 189 |
+
.subtitle {
|
| 190 |
+
text-align: center;
|
| 191 |
+
color: #666;
|
| 192 |
+
font-size: 1.2em;
|
| 193 |
+
margin-bottom: 30px;
|
| 194 |
+
}
|
| 195 |
+
"""
|
| 196 |
+
) as demo:
|
| 197 |
+
|
| 198 |
+
# ํค๋
|
| 199 |
+
gr.HTML("""
|
| 200 |
+
<div class="title-header">๐ฏ ๋ค์์ฑ ๊ฐํ ์ผํํค์๋ ์์คํ
</div>
|
| 201 |
+
<div class="subtitle">๐ ๋งค๋ฒ ์์ ํ ๋ค๋ฅธ ๊ฒฐ๊ณผ! ์ค๋ณต ์๋ ์ผํํค์๋ ์ ๋ฌธ ๋ฐ๊ตด ํ๋ก๊ทธ๋จ</div>
|
| 202 |
+
""")
|
| 203 |
+
|
| 204 |
+
with gr.Row():
|
| 205 |
+
with gr.Column(scale=1):
|
| 206 |
+
gr.Markdown("### ๐ ๋ค์์ฑ ๊ฐํ ์ค์ ")
|
| 207 |
+
|
| 208 |
+
# ๊ฒ์ ์์ง ์ ํ ์ถ๊ฐ
|
| 209 |
+
search_engine = gr.Dropdown(
|
| 210 |
+
choices=[
|
| 211 |
+
"๋ชจ๋ ๊ฒ์ ์์ง ํตํฉ ๋ถ์ (์ถ์ฒ)",
|
| 212 |
+
"Google ๊ฒ์ ๊ทธ๋ผ์ด๋ฉ๋ง",
|
| 213 |
+
"๋ค์ด๋ฒ ๊ฒ์ API๋ง",
|
| 214 |
+
"DuckDuckGo ๊ฒ์๋ง",
|
| 215 |
+
"๊ฒ์ ์์ด AI๋ง ์ฌ์ฉ"
|
| 216 |
+
],
|
| 217 |
+
label="๐ ๊ฒ์ ์์ง ์ ํ",
|
| 218 |
+
value="๋ชจ๋ ๊ฒ์ ์์ง ํตํฉ ๋ถ์ (์ถ์ฒ)"
|
| 219 |
+
)
|
| 220 |
+
|
| 221 |
+
# 1. ์ผํ ์นดํ
๊ณ ๋ฆฌ ์ ํ
|
| 222 |
+
category = gr.Dropdown(
|
| 223 |
+
choices=["๋๋ค์ ์ฉ", "ํจ์
์กํ", "์ํ/๊ฑด๊ฐ", "์ถ์ฐ/์ก์", "์คํฌ์ธ /๋ ์ ", "๋์งํธ/๊ฐ์ ", "๊ฐ๊ตฌ/์ธํ
๋ฆฌ์ด", "ํจ์
์๋ฅ", "ํ์ฅํ/๋ฏธ์ฉ"],
|
| 224 |
+
label="๐๏ธ ์ผํ ์นดํ
๊ณ ๋ฆฌ",
|
| 225 |
+
value="์ํ/๊ฑด๊ฐ"
|
| 226 |
+
)
|
| 227 |
+
|
| 228 |
+
# 2. ์ถ๊ฐ ์์ฒญ์ฌํญ
|
| 229 |
+
additional_request = gr.Textbox(
|
| 230 |
+
label="๐ ์ถ๊ฐ ์์ฒญ์ฌํญ (๋ค์์ฑ ์ค์ฌ)",
|
| 231 |
+
placeholder="์: ๋ค์ํ ์์ฌ, ์๋ก์ด ํํ, ๋
ํนํ ๊ธฐ๋ฅ ๋ฑ",
|
| 232 |
+
lines=2
|
| 233 |
+
)
|
| 234 |
+
|
| 235 |
+
# 3. ์ถ์ ํ์ด๋ฐ
|
| 236 |
+
launch_timing = gr.Radio(
|
| 237 |
+
choices=["๋๋ค์ ์ฉ", "์ฆ์์์ฑ", "๊ธฐํํ"],
|
| 238 |
+
label="โฐ ์ถ๏ฟฝ๏ฟฝ ํ์ด๋ฐ",
|
| 239 |
+
value="์ฆ์์์ฑ"
|
| 240 |
+
)
|
| 241 |
+
|
| 242 |
+
# 4. ๊ณ์ ์ฑ
|
| 243 |
+
seasonality = gr.Radio(
|
| 244 |
+
choices=["๋๋ค์ ์ฉ", "๋ด", "์ฌ๋ฆ", "๊ฐ์", "๊ฒจ์ธ", "๋น๊ณ์ "],
|
| 245 |
+
label="๐ฑ ๊ณ์ ์ฑ",
|
| 246 |
+
value="๋น๊ณ์ "
|
| 247 |
+
)
|
| 248 |
+
|
| 249 |
+
with gr.Column(scale=1):
|
| 250 |
+
gr.Markdown("### ๐ฐ ๋ชฉํ ์ค์ ")
|
| 251 |
+
|
| 252 |
+
# 5. ๋งค์ถ ๋ชฉํ
|
| 253 |
+
sales_target = gr.Radio(
|
| 254 |
+
choices=["๋๋ค์ ์ฉ", "100๋ง์ ์ดํ", "100-500๋ง์", "500-1์ฒ๋ง์", "1์ฒ-5์ฒ๋ง์", "5์ฒ๋ง์ ์ด์"],
|
| 255 |
+
label="๐ต ๋งค์ถ ๋ชฉํ",
|
| 256 |
+
value="100-500๋ง์"
|
| 257 |
+
)
|
| 258 |
+
|
| 259 |
+
# 6. ํ๋งค ์ฑ๋
|
| 260 |
+
sales_channel = gr.Radio(
|
| 261 |
+
choices=["๋๋ค์ ์ฉ", "์คํ๋ง์ผ", "SNS๋ง์ผํ
", "๊ด๊ณ ์งํ", "์คํ๋ผ์ธ"],
|
| 262 |
+
label="๐ฑ ํ๋งค ์ฑ๋",
|
| 263 |
+
value="์คํ๋ง์ผ"
|
| 264 |
+
)
|
| 265 |
+
|
| 266 |
+
# 7. ๊ฒฝ์ ๊ฐ๋
|
| 267 |
+
competition_level = gr.Radio(
|
| 268 |
+
choices=[
|
| 269 |
+
"๋๋ค์ ์ฉ",
|
| 270 |
+
"์ด๋ณด",
|
| 271 |
+
"์ค์",
|
| 272 |
+
"๊ณ ์"
|
| 273 |
+
],
|
| 274 |
+
label="โ๏ธ ๊ฒฝ์ ๊ฐ๋",
|
| 275 |
+
value="์ด๋ณด"
|
| 276 |
+
)
|
| 277 |
+
|
| 278 |
+
# ์คํ ๋ฒํผ
|
| 279 |
+
generate_btn = gr.Button(
|
| 280 |
+
"๐ ๋ค์์ฑ ๊ฐํ ์ผํํค์๋ ๋ฐ๊ตด ์์ (๋งค๋ฒ ๋ค๋ฅธ ๊ฒฐ๊ณผ)",
|
| 281 |
+
variant="primary",
|
| 282 |
+
size="lg"
|
| 283 |
+
)
|
| 284 |
+
|
| 285 |
+
# ๊ฒฐ๊ณผ ์ถ๋ ฅ
|
| 286 |
+
with gr.Row():
|
| 287 |
+
with gr.Column(scale=2):
|
| 288 |
+
gr.Markdown("### ๐ ๋ค์์ฑ ๊ฐํ ์ผํํค์๋ (50๊ฐ)")
|
| 289 |
+
output = gr.Textbox(
|
| 290 |
+
label="์ค๋ณต ์๋ ์ผํํค์๋ ๊ฒฐ๊ณผ (๋งค๋ฒ ์์ ํ ๋ค๋ฆ)",
|
| 291 |
+
lines=30,
|
| 292 |
+
max_lines=50,
|
| 293 |
+
placeholder="์ฌ๊ธฐ์ ๋งค๋ฒ ๋ค๋ฅธ 50๊ฐ์ ์ผํํค์๋๊ฐ ์ถ๋ ฅ๋ฉ๋๋ค...",
|
| 294 |
+
show_copy_button=True
|
| 295 |
+
)
|
| 296 |
+
|
| 297 |
+
with gr.Column(scale=1):
|
| 298 |
+
gr.Markdown("### ๐ ์คํ ๋ก๊ทธ")
|
| 299 |
+
log_output = gr.Textbox(
|
| 300 |
+
label="์์คํ
๋ก๊ทธ",
|
| 301 |
+
lines=30,
|
| 302 |
+
max_lines=50,
|
| 303 |
+
placeholder="์์คํ
์คํ ๋ก๊ทธ๊ฐ ์ฌ๊ธฐ์ ํ์๋ฉ๋๋ค...",
|
| 304 |
+
show_copy_button=True
|
| 305 |
+
)
|
| 306 |
+
|
| 307 |
+
# ์ด๋ฒคํธ ์ฐ๊ฒฐ
|
| 308 |
+
generate_btn.click(
|
| 309 |
+
fn=generate_with_logs,
|
| 310 |
+
inputs=[
|
| 311 |
+
category,
|
| 312 |
+
additional_request,
|
| 313 |
+
launch_timing,
|
| 314 |
+
seasonality,
|
| 315 |
+
sales_target,
|
| 316 |
+
sales_channel,
|
| 317 |
+
competition_level,
|
| 318 |
+
search_engine
|
| 319 |
+
],
|
| 320 |
+
outputs=[output, log_output],
|
| 321 |
+
show_progress=True
|
| 322 |
+
)
|
| 323 |
+
|
| 324 |
+
# ์ฌ์ฉ๋ฒ ์๋ด
|
| 325 |
+
with gr.Accordion("๐ ๋ค์์ฑ ๊ฐํ ์ฌ์ฉ๋ฒ ์๋ด", open=False):
|
| 326 |
+
gr.Markdown("""
|
| 327 |
+
### ๐ฏ ๋ค์์ฑ ๊ฐํ ์ผํํค์๋ ์์คํ
์ฌ์ฉ๋ฒ
|
| 328 |
+
|
| 329 |
+
#### ๐ ์ฃผ์ ๊ฐ์ ์ฌํญ
|
| 330 |
+
- **์์ ํ ์ค๋ณต ๋ฐฉ์ง**: ๋งค๋ฒ ์คํํ ๋๋ง๋ค ์์ ํ ๋ค๋ฅธ ํค์๋ ์์ฑ
|
| 331 |
+
- **๋๋ค ์๋ ์์คํ
**: ํ์ฌ ์๊ฐ์ ๊ธฐ๋ฐ์ผ๋ก ํ ๋๋ค ์๋๋ก ์์ธก ๋ถ๊ฐ๋ฅ
|
| 332 |
+
- **๋ค์ํ ์กฐํฉ ๋ณด์ฅ**: ์์ฌรํํร๊ธฐ๋ฅ์ 3์ฐจ์ ์กฐํฉ์ผ๋ก ๋ฌดํ ๋ค์์ฑ
|
| 333 |
+
- **์ค๋ณต ๊ฒ์ฌ ๊ฐํ**: ๋์๋ฌธ์ ๊ตฌ๋ถ ์๋ ์๊ฒฉํ ์ค๋ณต ์ ๊ฑฐ
|
| 334 |
+
- **์จ๋ ์กฐ์ **: AI ์์ฑ ํ๋ผ๋ฏธํฐ ์ต์ ํ๋ก ์ฐฝ์์ฑ ๊ทน๋ํ
|
| 335 |
+
|
| 336 |
+
#### ๐ ๋ค์์ฑ ๋ณด์ฅ ๋ฉ์ปค๋์ฆ
|
| 337 |
+
1. **์๋ ๊ธฐ๋ฐ ๋๋คํ**: ๋ง์ดํฌ๋ก์ด ๋จ์ ์๊ฐ ๊ธฐ๋ฐ ๋๋ค ์๋
|
| 338 |
+
2. **3์ฐจ์ ์กฐํฉ ์์คํ
**:
|
| 339 |
+
- ์์ฌ: ์ค๋ฆฌ์ฝ, ์คํ
์ธ๋ฆฌ์ค, ์ธ๋ผ๋ฏน, ๋๋๋ฌด ๋ฑ 20์ข
|
| 340 |
+
- ํํ: ์ ์ด์, ์ํ, ์ฌ๋ฆผ, ํด๋์ฉ ๋ฑ 20์ข
|
| 341 |
+
- ๊ธฐ๋ฅ: ๋ฐฉ์, ํญ๊ท , ๋ง๊ทธ๋คํฑ, ๋ณด์จ ๋ฑ 20์ข
|
| 342 |
+
3. **์ค๋ณต ๋ฐฉ์ง ์๊ณ ๋ฆฌ์ฆ**: ์์ฑ ์ค ์ค์๊ฐ ์ค๋ณต ๊ฒ์ฌ
|
| 343 |
+
4. **์ถ๊ฐ ์์ฑ ์์คํ
**: ๋ถ์กฑ์ ์๋์ผ๋ก ์ถ๊ฐ ํค์๋ ์์ฑ
|
| 344 |
+
|
| 345 |
+
#### ๐ฒ ๋๋ค ์ ์ฉ์ ์งํ
|
| 346 |
+
- **ํค์๋๋ณ ๋
๋ฆฝ ์ ์ฉ**: ๊ฐ ํค์๋๋ง๋ค ๋ค๋ฅธ ์กฐ๊ฑด ์กฐํฉ
|
| 347 |
+
- **์์ธก ๋ถ๊ฐ๋ฅ์ฑ**: ๊ฐ์ ์ค์ ์ด๋ผ๋ ๋งค๋ฒ ๋ค๋ฅธ ๊ฒฐ๊ณผ
|
| 348 |
+
- **์กฐํฉ ํญ๋ฐ**: ์์ฒ ๊ฐ์ง ๊ฐ๋ฅํ ์กฐํฉ์ผ๋ก ๋ฌดํ ๋ค์์ฑ
|
| 349 |
+
|
| 350 |
+
#### ๐ ์์ฑ ํimport gradio as gr
|
| 351 |
+
import os
|
| 352 |
+
import logging
|
| 353 |
+
import sys
|
| 354 |
+
import random
|
| 355 |
+
import requests
|
| 356 |
+
import json
|
| 357 |
+
from datetime import datetime
|
| 358 |
+
from google import genai
|
| 359 |
+
from google.genai.types import Tool, GenerateContentConfig, GoogleSearch
|
| 360 |
+
|
| 361 |
+
# ๋ก๊น
์ค์
|
| 362 |
+
logging.basicConfig(
|
| 363 |
+
level=logging.INFO,
|
| 364 |
+
format='%(asctime)s - %(levelname)s - %(message)s',
|
| 365 |
+
handlers=[
|
| 366 |
+
logging.StreamHandler(sys.stdout),
|
| 367 |
+
logging.FileHandler('sourcing_app.log', encoding='utf-8')
|
| 368 |
+
]
|
| 369 |
+
)
|
| 370 |
+
logger = logging.getLogger(__name__)
|
| 371 |
+
|
| 372 |
+
# ํค์๋ ๋ค์์ฑ์ ์ํ ์๋ ํ ํ์ฅ
|
| 373 |
+
DIVERSE_SEED_POOLS = {
|
| 374 |
+
"ํจ์
์กํ": [
|
| 375 |
+
"์ก์ธ์๋ฆฌ", "์ฅ์ ๊ตฌ", "๊ฐ๋ฐฉ", "์ง๊ฐ", "๋ชจ์", "์ค์นดํ", "๋ฒจํธ", "์ ๊ธ๋ผ์ค", "ํค์ด์ก์ธ์๋ฆฌ", "์๊ณ์ค",
|
| 376 |
+
"ํค๋ง", "๋ธ๋ก์น", "๋ชฉ๊ฑธ์ด", "ํ์ฐ", "๋ฐ์ง", "๊ท๊ฑธ์ด", "ํธ๋ํฐ์ผ์ด์ค", "ํ์ฐ์น", "ํด๋ฌ์น", "ํ ํธ๋ฐฑ"
|
| 377 |
+
],
|
| 378 |
+
"์ํ/๊ฑด๊ฐ": [
|
| 379 |
+
"์ฃผ๋ฐฉ์ฉํ", "์์ค์ฉํ", "์ฒญ์์ฉํ", "์๋ฉ์ฉํ", "๊ฑด๊ฐ์ฉํ", "์๋ฃ์ฉํ", "๋ง์ฌ์ง์ฉํ", "์ด๋์ฉํ",
|
| 380 |
+
"๋ค์ด์ดํธ์ฉํ", "ํ์ฅ์ง", "์ธ์ ", "์ดํธ", "์น์ฝ", "๋น๋", "์๊ฑด", "๋ฒ ๊ฐ", "์ด๋ถ", "์ฟ ์
", "๋งคํธ"
|
| 381 |
+
],
|
| 382 |
+
"์ถ์ฐ/์ก์": [
|
| 383 |
+
"์ ์์ฉํ", "์ก์์ฉํ", "์ถ์ฐ์ฉํ", "์์ฐ๋ถ์ฉํ", "์ ์์์ฉํ", "์ด์ ์์ฉํ", "๊ธฐ์ ๊ท", "์ ๋ณ",
|
| 384 |
+
"์ ๋ชจ์ฐจ", "์นด์ํธ", "์๊ธฐ์ท", "์ฅ๋๊ฐ", "๊ต์ก์ฉํ", "์ฑ
", "๊ทธ๋ฆผ์ฑ
", "ํผ์ฆ", "๋ธ๋ก", "์ธํ", "๋์ด๋งคํธ"
|
| 385 |
+
],
|
| 386 |
+
"์คํฌ์ธ /๋ ์ ": [
|
| 387 |
+
"์ด๋์ฉํ", "ํฌ์ค์ฉํ", "์๊ฐ์ฉํ", "์์์ฉํ", "๋ฑ์ฐ์ฉํ", "์บ ํ์ฉํ", "๋์์ฉํ", "๊ณจํ์ฉํ",
|
| 388 |
+
"์ถ๊ตฌ์ฉํ", "๋๊ตฌ์ฉํ", "๋ฐฐ๋๋ฏผํด์ฉํ", "ํ๊ตฌ์ฉํ", "ํ
๋์ค์ฉํ", "์์ ๊ฑฐ์ฉํ", "์ค์ผ์ดํธ๋ณด๋์ฉํ"
|
| 389 |
+
],
|
| 390 |
+
"๋์งํธ/๊ฐ์ ": [
|
| 391 |
+
"์ค๋งํธํฐ์ก์ธ์๋ฆฌ", "์ปดํจํฐ์ฉํ", "ํ๋ธ๋ฆฟ์ฉํ", "์ด์ดํฐ", "์คํผ์ปค", "์ถฉ์ ๊ธฐ", "์ผ์ด๋ธ", "๋ง์ฐ์คํจ๋",
|
| 392 |
+
"ํค๋ณด๋", "๋ง์ฐ์ค", "์น์บ ", "๋ง์ดํฌ", "ํค๋์
", "๊ฒ์ํจ๋", "USB", "๋ฉ๋ชจ๋ฆฌ์นด๋", "ํ์๋ฑ
ํฌ"
|
| 393 |
+
],
|
| 394 |
+
"๊ฐ๊ตฌ/์ธํ
๋ฆฌ์ด": [
|
| 395 |
+
"์๋ฉ๊ฐ๊ตฌ", "์นจ์ค๊ฐ๊ตฌ", "๊ฑฐ์ค๊ฐ๊ตฌ", "์ฃผ๋ฐฉ๊ฐ๊ตฌ", "์์ค๊ฐ๊ตฌ", "์ฌ๋ฌด์ฉ๊ฐ๊ตฌ", "์ธํ
๋ฆฌ์ด์ํ", "์กฐ๋ช
",
|
| 396 |
+
"์ปคํผ", "๋ธ๋ผ์ธ๋", "์นดํซ", "๋ฌ๊ทธ", "์ก์", "๊ฑฐ์ธ", "์๊ณ", "ํ๋ถ", "๊ฝ๋ณ", "์บ๋ค", "๋ฐฉํฅ์ "
|
| 397 |
+
],
|
| 398 |
+
"ํจ์
์๋ฅ": [
|
| 399 |
+
"ํฐ์
์ธ ", "์
์ธ ", "๋ธ๋ผ์ฐ์ค", "์ํผ์ค", "์ค์ปคํธ", "๋ฐ์ง", "์ฒญ๋ฐ์ง", "๋ ๊น
์ค", "์์ผ", "์ฝํธ",
|
| 400 |
+
"์ ํผ", "๊ฐ๋๊ฑด", "๋ํธ", "ํ๋", "์กฐ๋ผ", "์์ท", "์ ์ท", "์๋ง", "์คํํน", "์ด๋๋ณต"
|
| 401 |
+
],
|
| 402 |
+
"ํ์ฅํ/๋ฏธ์ฉ": [
|
| 403 |
+
"์คํจ์ผ์ด", "๋ฉ์ดํฌ์
", "ํด๋ ์ง", "๋ง์คํฌํฉ", "์ ํฌ๋ฆผ", "๋ก์
", "์์ผ์ค", "ํฌ๋ฆผ", "๋ฆฝ๋ฐค",
|
| 404 |
+
"๋ฆฝ์คํฑ", "์์ด์๋", "๋ง์ค์นด๋ผ", "ํ์ด๋ฐ์ด์
", "์ปจ์ค๋ฌ", "๋ธ๋ฌ์
", "ํ์ด๋ผ์ดํฐ", "๋ค์ผ", "ํฅ์"
|
| 405 |
+
]
|
| 406 |
+
}
|
| 407 |
+
|
| 408 |
+
# ์์ฌ๋ณ ํค์๋ ํ
|
| 409 |
+
MATERIAL_KEYWORDS = [
|
| 410 |
+
"์ค๋ฆฌ์ฝ", "์คํ
์ธ๋ฆฌ์ค", "์ธ๋ผ๋ฏน", "์ ๋ฆฌ", "๋๋ฌด", "๋๋๋ฌด", "๋ฉด", "๋ฆฐ๋จ", "ํด๋ฆฌ์์คํฐ", "๋์ผ๋ก ",
|
| 411 |
+
"๊ณ ๋ฌด", "ํ๋ผ์คํฑ", "์ข
์ด", "๊ฐ์ฃฝ", "์ธ์กฐ๊ฐ์ฃฝ", "๋ฉํ", "์๋ฃจ๋ฏธ๋", "์ฒ ", "๊ตฌ๋ฆฌ", "ํฉ๋"
|
| 412 |
+
]
|
| 413 |
+
|
| 414 |
+
# ํํ๋ณ ํค์๋ ํ
|
| 415 |
+
SHAPE_KEYWORDS = [
|
| 416 |
+
"์ ์ด์", "ํด๋์ฉ", "๋ฏธ๋", "๋ํ", "์ํ", "์ฌ๊ฐ", "ํ์", "์ง์ฌ๊ฐ", "์ผ๊ฐ", "์ก๊ฐ",
|
| 417 |
+
"์ฌ๋ฆผ", "๋๊บผ์ด", "์์", "๊ธด", "์งง์", "๋์", "์ข์", "๊น์", "์์", "๊ณก์ "
|
| 418 |
+
]
|
| 419 |
+
|
| 420 |
+
# ๊ธฐ๋ฅ๋ณ ํค์๋ ํ
|
| 421 |
+
FUNCTION_KEYWORDS = [
|
| 422 |
+
"๋ฐฉ์", "๋ฏธ๋๋ผ๋ฐฉ์ง", "ํญ๊ท ", "๋์์ ๊ฑฐ", "๋ณด์จ", "๋ณด๋", "์๊ฑด", "ํก์", "์ฐจ๋จ", "๋ณดํธ",
|
| 423 |
+
"๋ง๊ทธ๋คํฑ", "์์", "๋์ ", "ํฌ๋ช
", "๋ถํฌ๋ช
", "๋ฐ๊ด", "๋ฐ์ฌ", "์ ์ถ", "ํ๋ ฅ", "๊ณ ์ "
|
| 424 |
+
]
|
| 425 |
+
|
| 426 |
+
# Gemini API ํด๋ผ์ด์ธํธ ์ด๊ธฐํ
|
| 427 |
+
def initialize_gemini():
|
| 428 |
+
logger.info("Gemini API ํด๋ผ์ด์ธํธ ์ด๊ธฐํ ์์")
|
| 429 |
+
api_key = os.getenv("GEMINI_API_KEY")
|
| 430 |
+
if not api_key:
|
| 431 |
+
logger.error("GEMINI_API_KEY ํ๊ฒฝ๋ณ์๊ฐ ์ค์ ๋์ง ์์์ต๋๋ค.")
|
| 432 |
+
raise ValueError("GEMINI_API_KEY ํ๊ฒฝ๋ณ์๊ฐ ์ค์ ๋์ง ์์์ต๋๋ค.")
|
| 433 |
+
|
| 434 |
+
client = genai.Client(api_key=api_key)
|
| 435 |
+
logger.info("Gemini API ํด๋ผ์ด์ธํธ ์ด๊ธฐํ ์๋ฃ")
|
| 436 |
+
return client
|
| 437 |
+
|
| 438 |
+
def get_recent_logs():
|
| 439 |
+
"""์ต๊ทผ ๋ก๊ทธ๋ฅผ ๊ฐ์ ธ์ค๋ ํจ์"""
|
| 440 |
+
try:
|
| 441 |
+
with open('sourcing_app.log', 'r', encoding='utf-8') as f:
|
| 442 |
+
lines = f.readlines()
|
| 443 |
+
# ์ต๊ทผ 50์ค๋ง ๋ฐํ
|
| 444 |
+
return ''.join(lines[-50:])
|
| 445 |
+
except FileNotFoundError:
|
| 446 |
+
return "๋ก๊ทธ ํ์ผ์ ์ฐพ์ ์ ์์ต๋๋ค."
|
| 447 |
+
except Exception as e:
|
| 448 |
+
return f"๋ก๊ทธ ์ฝ๊ธฐ ์ค๋ฅ: {str(e)}"
|
| 449 |
+
|
| 450 |
+
def generate_diverse_keyword_combinations(category, count=60):
|
| 451 |
+
"""๋ค์ํ ํค์๋ ์กฐํฉ์ ์์ฑํ๋ ํจ์"""
|
| 452 |
+
logger.info(f"๋ค์ํ ํค์๋ ์กฐํฉ ์์ฑ ์์: {category}, {count}๊ฐ")
|
| 453 |
+
|
| 454 |
+
combinations = []
|
| 455 |
+
category_pool = DIVERSE_SEED_POOLS.get(category, DIVERSE_SEED_POOLS["์ํ/๊ฑด๊ฐ"])
|
| 456 |
+
|
| 457 |
+
# 1. ๋จ์ผ ํค์๋ (20%)
|
| 458 |
+
single_keywords = random.sample(category_pool, min(12, len(category_pool)))
|
| 459 |
+
combinations.extend(single_keywords)
|
| 460 |
+
|
| 461 |
+
# 2. ์์ฌ + ์นดํ
๏ฟฝ๏ฟฝ๋ฆฌ (30%)
|
| 462 |
+
for _ in range(18):
|
| 463 |
+
material = random.choice(MATERIAL_KEYWORDS)
|
| 464 |
+
item = random.choice(category_pool)
|
| 465 |
+
combinations.append(f"{material} {item}")
|
| 466 |
+
|
| 467 |
+
# 3. ํํ + ์นดํ
๊ณ ๋ฆฌ (30%)
|
| 468 |
+
for _ in range(18):
|
| 469 |
+
shape = random.choice(SHAPE_KEYWORDS)
|
| 470 |
+
item = random.choice(category_pool)
|
| 471 |
+
combinations.append(f"{shape} {item}")
|
| 472 |
+
|
| 473 |
+
# 4. ๊ธฐ๋ฅ + ์นดํ
๊ณ ๋ฆฌ (20%)
|
| 474 |
+
for _ in range(12):
|
| 475 |
+
function = random.choice(FUNCTION_KEYWORDS)
|
| 476 |
+
item = random.choice(category_pool)
|
| 477 |
+
combinations.append(f"{function} {item}")
|
| 478 |
+
|
| 479 |
+
# ์ค๋ณต ์ ๊ฑฐ ๋ฐ ์
ํ
|
| 480 |
+
combinations = list(set(combinations))
|
| 481 |
+
random.shuffle(combinations)
|
| 482 |
+
|
| 483 |
+
logger.info(f"์์ฑ๋ ์กฐํฉ ์: {len(combinations)}๊ฐ")
|
| 484 |
+
return combinations[:count]
|
| 485 |
+
|
| 486 |
+
def search_all_engines(query):
|
| 487 |
+
"""๋ชจ๋ ๊ฒ์ ์์ง์ ์ฌ์ฉํ์ฌ ๋ฐ์ดํฐ๋ฅผ ์ทจํฉํ๋ ํจ์"""
|
| 488 |
+
logger.info(f"๋ชจ๋ ๊ฒ์ ์์ง์ผ๋ก ๊ฒ์ ์์: {query}")
|
| 489 |
+
|
| 490 |
+
all_results = {
|
| 491 |
+
"google": "",
|
| 492 |
+
"naver": "",
|
| 493 |
+
"duckduckgo": ""
|
| 494 |
+
}
|
| 495 |
+
|
| 496 |
+
# 1. ๋ค์ด๋ฒ ๊ฒ์ API
|
| 497 |
+
try:
|
| 498 |
+
naver_client_id = os.getenv("NAVER_CLIENT_ID")
|
| 499 |
+
naver_client_secret = os.getenv("NAVER_CLIENT_SECRET")
|
| 500 |
+
|
| 501 |
+
if naver_client_id and naver_client_secret:
|
| 502 |
+
url = "https://openapi.naver.com/v1/search/shop.json"
|
| 503 |
+
headers = {
|
| 504 |
+
"X-Naver-Client-Id": naver_client_id,
|
| 505 |
+
"X-Naver-Client-Secret": naver_client_secret
|
| 506 |
+
}
|
| 507 |
+
params = {"query": query, "display": 10}
|
| 508 |
+
|
| 509 |
+
response = requests.get(url, headers=headers, params=params, timeout=10)
|
| 510 |
+
if response.status_code == 200:
|
| 511 |
+
data = response.json()
|
| 512 |
+
naver_data = []
|
| 513 |
+
for item in data.get('items', [])[:5]:
|
| 514 |
+
naver_data.append(f"์ํ: {item.get('title', '').replace('<b>', '').replace('</b>', '')}")
|
| 515 |
+
naver_data.append(f"๊ฐ๊ฒฉ: {item.get('lprice', '')}์")
|
| 516 |
+
naver_data.append(f"์นดํ
๊ณ ๋ฆฌ: {item.get('category1', '')}")
|
| 517 |
+
all_results["naver"] = "\n".join(naver_data)
|
| 518 |
+
logger.info("๋ค์ด๋ฒ ๊ฒ์ ์๋ฃ")
|
| 519 |
+
else:
|
| 520 |
+
all_results["naver"] = "๋ค์ด๋ฒ API ๊ฒ์ ์คํจ"
|
| 521 |
+
else:
|
| 522 |
+
all_results["naver"] = "๋ค์ด๋ฒ API ํค๊ฐ ์ค์ ๋์ง ์์"
|
| 523 |
+
except Exception as e:
|
| 524 |
+
all_results["naver"] = f"๋ค์ด๋ฒ ๊ฒ์ ์ค๋ฅ: {str(e)}"
|
| 525 |
+
|
| 526 |
+
# 2. DuckDuckGo ๊ฒ์
|
| 527 |
+
try:
|
| 528 |
+
url = "https://api.duckduckgo.com/"
|
| 529 |
+
params = {
|
| 530 |
+
"q": query,
|
| 531 |
+
"format": "json",
|
| 532 |
+
"no_html": "1",
|
| 533 |
+
"skip_disambig": "1"
|
| 534 |
+
}
|
| 535 |
+
|
| 536 |
+
response = requests.get(url, params=params, timeout=10)
|
| 537 |
+
if response.status_code == 200:
|
| 538 |
+
data = response.json()
|
| 539 |
+
|
| 540 |
+
ddg_data = []
|
| 541 |
+
# Abstract ์ ๋ณด
|
| 542 |
+
if data.get('Abstract'):
|
| 543 |
+
ddg_data.append(f"์์ฝ: {data['Abstract']}")
|
| 544 |
+
|
| 545 |
+
# Related Topics
|
| 546 |
+
for topic in data.get('RelatedTopics', [])[:3]:
|
| 547 |
+
if isinstance(topic, dict) and topic.get('Text'):
|
| 548 |
+
ddg_data.append(f"๊ด๋ จ์ ๋ณด: {topic['Text']}")
|
| 549 |
+
|
| 550 |
+
all_results["duckduckgo"] = "\n".join(ddg_data) if ddg_data else "DuckDuckGo์์ ๊ด๋ จ ์ ๋ณด ์์"
|
| 551 |
+
logger.info("DuckDuckGo ๊ฒ์ ์๋ฃ")
|
| 552 |
+
else:
|
| 553 |
+
all_results["duckduckgo"] = "DuckDuckGo ๊ฒ์ ์คํจ"
|
| 554 |
+
except Exception as e:
|
| 555 |
+
all_results["duckduckgo"] = f"DuckDuckGo ๊ฒ์ ์ค๋ฅ: {str(e)}"
|
| 556 |
+
|
| 557 |
+
# 3. Google ๊ฒ์์ Gemini์์ ์๋ ์ฒ๋ฆฌ๋จ
|
| 558 |
+
all_results["google"] = "Google ๊ฒ์ ๊ทธ๋ผ์ด๋ฉ ์๋ ์คํ"
|
| 559 |
+
|
| 560 |
+
logger.info("๋ชจ๋ ๊ฒ์ ์์ง ๋ฐ์ดํฐ ์์ง ์๋ฃ")
|
| 561 |
+
return all_results
|
| 562 |
+
|
| 563 |
+
def comprehensive_market_analysis(category, seasonality, sales_target):
|
| 564 |
+
"""๋ค์์ฑ ๊ฐํ๋ ์์ฅ ๋ถ์"""
|
| 565 |
+
logger.info("๋ค์์ฑ ๊ฐํ ์์ฅ ๋ถ์ ์์")
|
| 566 |
+
|
| 567 |
+
# ๋๋ค ์๋๋ฅผ ํ์ฌ ์๊ฐ์ผ๋ก ์ค์ ํ์ฌ ๋งค๋ฒ ๋ค๋ฅธ ๊ฒฐ๊ณผ ๋ณด์ฅ
|
| 568 |
+
random.seed(datetime.now().microsecond)
|
| 569 |
+
|
| 570 |
+
# ๋ค์ํ ๊ฒ์ ๊ฐ๋๋ก ์ฟผ๋ฆฌ ์์ฑ
|
| 571 |
+
search_angles = [
|
| 572 |
+
"ํ์์ํ", "์ ์ํ", "์ธ๊ธฐ์ํ", "์ ๊ฐ์ํ", "๊ณ ๊ธ์ํ", "ํ ์ธ์ํ",
|
| 573 |
+
"๊ฐํธ์ํ", "์ค์ฉ์ํ", "ํธ๋ ๋์ํ", "์จ์์ํ", "๋ฒ ์คํธ์ํ", "์ถ์ฒ์ํ"
|
| 574 |
+
]
|
| 575 |
+
|
| 576 |
+
search_queries = []
|
| 577 |
+
|
| 578 |
+
# ์นดํ
๊ณ ๋ฆฌ๋ณ ๋ค์ํ ๊ฒ์์ด ์์ฑ
|
| 579 |
+
category_pool = DIVERSE_SEED_POOLS.get(category, DIVERSE_SEED_POOLS["์ํ/๊ฑด๊ฐ"])
|
| 580 |
+
|
| 581 |
+
for angle in search_angles:
|
| 582 |
+
for item in random.sample(category_pool, 3): # ๊ฐ ์นดํ
๊ณ ๋ฆฌ์์ 3๊ฐ์ฉ๋ง ์ ํ
|
| 583 |
+
search_queries.append(f"{item} {angle}")
|
| 584 |
+
|
| 585 |
+
# ์์ฌ๋ณ ๊ฒ์์ด ์ถ๊ฐ
|
| 586 |
+
for material in random.sample(MATERIAL_KEYWORDS, 5):
|
| 587 |
+
search_queries.append(f"{material} {category} ์ํ")
|
| 588 |
+
|
| 589 |
+
# ํํ๋ณ ๊ฒ์์ด ์ถ๊ฐ
|
| 590 |
+
for shape in random.sample(SHAPE_KEYWORDS, 5):
|
| 591 |
+
search_queries.append(f"{shape} {category} ์์ดํ
")
|
| 592 |
+
|
| 593 |
+
# ๊ฒ์์ด ์
ํํ์ฌ ์์ธก ๋ถ๊ฐ๋ฅํ๊ฒ ๋ง๋ค๊ธฐ
|
| 594 |
+
random.shuffle(search_queries)
|
| 595 |
+
search_queries = search_queries[:15] # 15๊ฐ๋ก ์ ํ
|
| 596 |
+
|
| 597 |
+
comprehensive_data = {}
|
| 598 |
+
|
| 599 |
+
for i, query in enumerate(search_queries):
|
| 600 |
+
logger.info(f"๋ค์์ฑ ๊ฒ์ {i+1}/15: {query}")
|
| 601 |
+
comprehensive_data[f"query_{i+1}"] = search_all_engines(query)
|
| 602 |
+
|
| 603 |
+
# API ๊ณผ๋ถํ ๋ฐฉ์ง๋ฅผ ์ํ ๋๋ ์ด
|
| 604 |
+
import time
|
| 605 |
+
time.sleep(0.5)
|
| 606 |
+
|
| 607 |
+
# ๋ค์์ฑ ๊ฐํ ๋ฐ์ดํฐ ์์ฝ
|
| 608 |
+
summary = "=== ๋ค์์ฑ ๊ฐํ ์์ฅ ๋ถ์ ๊ฒฐ๊ณผ ===\n\n"
|
| 609 |
+
|
| 610 |
+
# ๋ฌด์์๋ก ๊ฒฐ๊ณผ๋ฅผ ์์ด์ ํจํด ๋ฐฉ์ง
|
| 611 |
+
result_keys = list(comprehensive_data.keys())
|
| 612 |
+
random.shuffle(result_keys)
|
| 613 |
+
|
| 614 |
+
summary += "๐ ๋ค์ํ ์์ฅ ๊ฒ์ ๊ฒฐ๊ณผ:\n"
|
| 615 |
+
for key in result_keys[:10]:
|
| 616 |
+
results = comprehensive_data.get(key, {})
|
| 617 |
+
if results.get("naver"):
|
| 618 |
+
summary += f"โข {results['naver'][:60]}...\n"
|
| 619 |
+
summary += "\n"
|
| 620 |
+
|
| 621 |
+
logger.info("๋ค์์ฑ ๊ฐํ ๋ถ์ ์๋ฃ")
|
| 622 |
+
return summary
|
| 623 |
+
|
| 624 |
+
def search_with_api(query, search_engine="Google ๊ฒ์ ๊ทธ๋ผ์ด๋ฉ๋ง"):
|
| 625 |
+
"""๊ฐ๋ณ ๊ฒ์ ์์ง์ผ๋ก ๊ฒ์ํ๋ ํจ์ (๋จ์ผ ์์ง ์ ํ์ ์ฌ์ฉ)"""
|
| 626 |
+
logger.info(f"๊ฒ์ ์์ง: {search_engine}, ์ฟผ๋ฆฌ: {query}")
|
| 627 |
+
|
| 628 |
+
search_results = ""
|
| 629 |
+
|
| 630 |
+
try:
|
| 631 |
+
if search_engine == "๋ค์ด๋ฒ ๊ฒ์ API๋ง":
|
| 632 |
+
# ๋ค์ด๋ฒ ๊ฒ์ API ์ฌ์ฉ
|
| 633 |
+
naver_client_id = os.getenv("NAVER_CLIENT_ID")
|
| 634 |
+
naver_client_secret = os.getenv("NAVER_CLIENT_SECRET")
|
| 635 |
+
|
| 636 |
+
if naver_client_id and naver_client_secret:
|
| 637 |
+
url = "https://openapi.naver.com/v1/search/shop.json"
|
| 638 |
+
headers = {
|
| 639 |
+
"X-Naver-Client-Id": naver_client_id,
|
| 640 |
+
"X-Naver-Client-Secret": naver_client_secret
|
| 641 |
+
}
|
| 642 |
+
params = {"query": query, "display": 10}
|
| 643 |
+
|
| 644 |
+
response = requests.get(url, headers=headers, params=params)
|
| 645 |
+
if response.status_code == 200:
|
| 646 |
+
data = response.json()
|
| 647 |
+
for item in data.get('items', [])[:5]:
|
| 648 |
+
search_results += f"์ํ๋ช
: {item.get('title', '')}\n"
|
| 649 |
+
search_results += f"๊ฐ๊ฒฉ: {item.get('lprice', '')}์\n"
|
| 650 |
+
search_results += f"์นดํ
๊ณ ๋ฆฌ: {item.get('category1', '')}\n\n"
|
| 651 |
+
else:
|
| 652 |
+
search_results = "๋ค์ด๋ฒ API ๊ฒ์ ์คํจ"
|
| 653 |
+
else:
|
| 654 |
+
search_results = "๋ค์ด๋ฒ API ํค๊ฐ ์ค์ ๋์ง ์์"
|
| 655 |
+
|
| 656 |
+
elif search_engine == "DuckDuckGo ๊ฒ์๋ง":
|
| 657 |
+
# DuckDuckGo ๊ฒ์ (๋ฌด๋ฃ, API ํค ๋ถํ์)
|
| 658 |
+
try:
|
| 659 |
+
url = "https://api.duckduckgo.com/"
|
| 660 |
+
params = {
|
| 661 |
+
"q": query,
|
| 662 |
+
"format": "json",
|
| 663 |
+
"no_html": "1",
|
| 664 |
+
"skip_disambig": "1"
|
| 665 |
+
}
|
| 666 |
+
|
| 667 |
+
response = requests.get(url, params=params, timeout=10)
|
| 668 |
+
if response.status_code == 200:
|
| 669 |
+
data = response.json()
|
| 670 |
+
|
| 671 |
+
# Abstract ์ ๋ณด
|
| 672 |
+
if data.get('Abstract'):
|
| 673 |
+
search_results += f"์์ฝ: {data['Abstract']}\n\n"
|
| 674 |
+
|
| 675 |
+
# Related Topics
|
| 676 |
+
for topic in data.get('RelatedTopics', [])[:5]:
|
| 677 |
+
if isinstance(topic, dict) and topic.get('Text'):
|
| 678 |
+
search_results += f"๊ด๋ จ ์ ๋ณด: {topic['Text']}\n"
|
| 679 |
+
|
| 680 |
+
if not search_results:
|
| 681 |
+
search_results = "DuckDuckGo์์ ๊ด๋ จ ์ ๋ณด๋ฅผ ์ฐพ์ง ๋ชปํจ"
|
| 682 |
+
else:
|
| 683 |
+
search_results = "DuckDuckGo ๊ฒ์ ์คํจ"
|
| 684 |
+
except Exception as e:
|
| 685 |
+
search_results = f"DuckDuckGo ๊ฒ์ ์ค๋ฅ: {str(e)}"
|
| 686 |
+
|
| 687 |
+
elif search_engine == "๊ฒ์ ์์ด AI๋ง ์ฌ์ฉ":
|
| 688 |
+
search_results = "๊ฒ์ ์์ด AI ์ง์๋ง ์ฌ์ฉํ์ฌ ํค์๋ ์์ฑ"
|
| 689 |
+
|
| 690 |
+
else:
|
| 691 |
+
# Google ๊ฒ์ ๊ทธ๋ผ์ด๋ฉ (๊ธฐ๋ณธ)
|
| 692 |
+
search_results = "Google ๊ฒ์ ๊ทธ๋ผ์ด๋ฉ ์ฌ์ฉ"
|
| 693 |
+
|
| 694 |
+
except Exception as e:
|
| 695 |
+
logger.error(f"๊ฒ์ ์ค๋ฅ: {str(e)}")
|
| 696 |
+
search_results = f"๊ฒ์ ์ค๋ฅ: {str(e)}"
|
| 697 |
+
|
| 698 |
+
logger.info(f"๊ฒ์ ๊ฒฐ๊ณผ ๊ธธ์ด: {len(search_results)} ๋ฌธ์")
|
| 699 |
+
return search_results
|
| 700 |
+
|
| 701 |
+
def apply_random_selection_for_keywords(category, launch_timing, seasonality, sales_target, sales_channel, competition_level):
|
| 702 |
+
"""๊ฐ ํค์๋๋ง๋ค ๋๋คํ๊ฒ ์กฐ๊ฑด์ ์ ์ฉํ๊ธฐ ์ํ ์ค์ ๋ฌธ์์ด ์์ฑ"""
|
| 703 |
+
|
| 704 |
+
# ๊ฐ ํญ๋ชฉ๋ณ ์ ํ์ง ์ ์
|
| 705 |
+
categories = ["ํจ์
์กํ", "์ํ/๊ฑด๊ฐ", "์ถ์ฐ/์ก์", "์คํฌ์ธ /๋ ์ ", "๋์งํธ/๊ฐ์ ", "๊ฐ๊ตฌ/์ธํ
๋ฆฌ์ด", "ํจ์
์๋ฅ", "ํ์ฅํ/๋ฏธ์ฉ"]
|
| 706 |
+
launch_timings = ["์ฆ์์์ฑ", "๊ธฐํํ"]
|
| 707 |
+
seasonalities = ["๋ด", "์ฌ๋ฆ", "๊ฐ์", "๊ฒจ์ธ", "๋น๊ณ์ "]
|
| 708 |
+
sales_targets = ["100๋ง์ ์ดํ", "100-500๋ง์", "500-1์ฒ๋ง์", "1์ฒ-5์ฒ๋ง์", "5์ฒ๋ง์ ์ด์"]
|
| 709 |
+
sales_channels = ["์คํ๋ง์ผ", "SNS๋ง์ผํ
", "๊ด๊ณ ์งํ", "์คํ๋ผ์ธ"]
|
| 710 |
+
competition_levels = ["์ด๋ณด", "์ค์", "๊ณ ์"]
|
| 711 |
+
|
| 712 |
+
# ๋๋ค์ ์ฉ ์ค์ ์ ๋ณด ์์ฑ
|
| 713 |
+
random_settings = {
|
| 714 |
+
'category_random': category == "๋๋ค์ ์ฉ",
|
| 715 |
+
'launch_timing_random': launch_timing == "๋๋ค์ ์ฉ",
|
| 716 |
+
'seasonality_random': seasonality == "๋๋ค์ ์ฉ",
|
| 717 |
+
'sales_target_random': sales_target == "๋๋ค์ ์ฉ",
|
| 718 |
+
'sales_channel_random': sales_channel == "๋๋ค์ ์ฉ",
|
| 719 |
+
'competition_level_random': competition_level == "๋๋ค์ ์ฉ",
|
| 720 |
+
'categories': categories,
|
| 721 |
+
'launch_timings': launch_timings,
|
| 722 |
+
'seasonalities': seasonalities,
|
| 723 |
+
'sales_targets': sales_targets,
|
| 724 |
+
'sales_channels': sales_channels,
|
| 725 |
+
'competition_levels': competition_levels
|
| 726 |
+
}
|
| 727 |
+
|
| 728 |
+
# ๊ณ ์ ๊ฐ๋ค
|
| 729 |
+
fixed_values = {
|
| 730 |
+
'category': category if category != "๋๋ค์ ์ฉ" else None,
|
| 731 |
+
'launch_timing': launch_timing if launch_timing != "๋๋ค์ ์ฉ" else None,
|
| 732 |
+
'seasonality': seasonality if seasonality != "๋๋ค์ ์ฉ" else None,
|
| 733 |
+
'sales_target': sales_target if sales_target != "๋๋ค์ ์ฉ" else None,
|
| 734 |
+
'sales_channel': sales_channel if sales_channel != "๋๋ค์ ์ฉ" else None,
|
| 735 |
+
'competition_level': competition_level if competition_level != "๋๋ค์ ์ฉ" else None
|
| 736 |
+
}
|
| 737 |
+
|
| 738 |
+
logger.info("=== ํค์๋๋ณ ๋๋ค ์ค์ ===")
|
| 739 |
+
logger.info(f"์นดํ
๊ณ ๋ฆฌ ๋๋ค: {random_settings['category_random']}")
|
| 740 |
+
logger.info(f"์ถ์ํ์ด๋ฐ ๋๋ค: {random_settings['launch_timing_random']}")
|
| 741 |
+
logger.info(f"๊ณ์ ์ฑ ๋๋ค: {random_settings['seasonality_random']}")
|
| 742 |
+
logger.info(f"๋งค์ถ๋ชฉํ ๋๋ค: {random_settings['sales_target_random']}")
|
| 743 |
+
logger.info(f"ํ๋งค์ฑ๋ ๋๋ค: {random_settings['sales_channel_random']}")
|
| 744 |
+
logger.info(f"๊ฒฝ์๊ฐ๋ ๋๋ค: {random_settings['competition_level_random']}")
|
| 745 |
+
|
| 746 |
+
return random_settings, fixed_values
|
| 747 |
+
|
| 748 |
+
def generate_sourcing_keywords(category, additional_request, launch_timing, seasonality, sales_target, sales_channel, competition_level, search_engine="Google ๊ฒ์ ๊ทธ๋ผ์ด๋ฉ๋ง"):
|
| 749 |
+
"""๋ค์์ฑ ๊ฐํ๋ ์ผํ ํค์๋ 50๊ฐ๋ฅผ ์์ฑํ๋ ํจ์"""
|
| 750 |
+
logger.info("=== ๋ค์์ฑ ๊ฐํ ์ผํํค์๋ ์์ฑ ์์ ===")
|
| 751 |
+
logger.info(f"์
๋ ฅ ์กฐ๊ฑด - ๊ฒ์์์ง: {search_engine}")
|
| 752 |
+
logger.info(f"์
๋ ฅ ์กฐ๊ฑด - ์นดํ
๊ณ ๋ฆฌ: {category}")
|
| 753 |
+
logger.info(f"์
๋ ฅ ์กฐ๊ฑด - ์ถ๊ฐ์์ฒญ: {additional_request}")
|
| 754 |
+
logger.info(f"์
๋ ฅ ์กฐ๊ฑด - ์ถ์ํ์ด๋ฐ: {launch_timing}")
|
| 755 |
+
logger.info(f"์
๋ ฅ ์กฐ๊ฑด - ๊ณ์ ์ฑ: {seasonality}")
|
| 756 |
+
logger.info(f"์
๋ ฅ ์กฐ๊ฑด - ๋งค์ถ๋ชฉํ: {sales_target}")
|
| 757 |
+
logger.info(f"์
๋ ฅ ์กฐ๊ฑด - ํ๋งค์ฑ๋: {sales_channel}")
|
| 758 |
+
logger.info(f"์
๋ ฅ ์กฐ๊ฑด - ๊ฒฝ์๊ฐ๋: {competition_level}")
|
| 759 |
+
|
| 760 |
+
try:
|
| 761 |
+
logger.info("Gemini ํด๋ผ์ด์ธํธ ์ด๊ธฐํ ์ค...")
|
| 762 |
+
client = initialize_gemini()
|
| 763 |
+
|
| 764 |
+
# ๋งค๋ฒ ๋ค๋ฅธ ์๋๋ก ๋๋ค์ฑ ๋ณด์ฅ
|
| 765 |
+
current_time = datetime.now()
|
| 766 |
+
random_seed = current_time.microsecond + current_time.second * 1000
|
| 767 |
+
random.seed(random_seed)
|
| 768 |
+
logger.info(f"๋๋ค ์๋ ์ค์ : {random_seed}")
|
| 769 |
+
|
| 770 |
+
# ํ๋กฌํํธ ๊ตฌ์ฑ
|
| 771 |
+
logger.info("๋ค์์ฑ ๊ฐํ ํ๋กฌํํธ ๊ตฌ์ฑ ์ค...")
|
| 772 |
+
|
| 773 |
+
# ๋๋ค ์ค์ ์ฒ๋ฆฌ
|
| 774 |
+
random_settings, fixed_values = apply_random_selection_for_keywords(
|
| 775 |
+
category, launch_timing, seasonality, sales_target, sales_channel, competition_level
|
| 776 |
+
)
|
| 777 |
+
|
| 778 |
+
# ๋ค์ํ ํค์๋ ์กฐํฉ ๋ฏธ๋ฆฌ ์์ฑ
|
| 779 |
+
diverse_combinations = generate_diverse_keyword_combinations(category, 60)
|
| 780 |
+
logger.info(f"๋ค์ํ ์กฐํฉ ์์ฑ ์๋ฃ: {len(diverse_combinations)}๊ฐ")
|
| 781 |
+
|
| 782 |
+
# ๊ฒ์ ์์ง๋ณ ์ฒ๋ฆฌ
|
| 783 |
+
search_info = ""
|
| 784 |
+
config_tools = []
|
| 785 |
+
|
| 786 |
+
if search_engine == "๋ชจ๋ ๊ฒ์ ์์ง ํตํฉ ๋ถ์ (์ถ์ฒ)":
|
| 787 |
+
logger.info("๐ ๋ค์์ฑ ๊ฐํ ํตํฉ ๋ถ์ ์์...")
|
| 788 |
+
|
| 789 |
+
# Google ๊ฒ์ ๊ทธ๋ผ์ด๋ฉ ๋๊ตฌ ์ค์
|
| 790 |
+
google_search_tool = Tool(google_search=GoogleSearch())
|
| 791 |
+
config_tools = [google_search_tool]
|
| 792 |
+
|
| 793 |
+
# ๋ค์์ฑ ๊ฐํ ์ข
ํฉ ์์ฅ ๋ถ์ ์คํ
|
| 794 |
+
comprehensive_analysis = comprehensive_market_analysis(category, seasonality, sales_target)
|
| 795 |
+
|
| 796 |
+
search_info = f"""
|
| 797 |
+
๐ === ๋ค์์ฑ ๊ฐํ ํตํฉ ๋ถ์ ๊ฒฐ๊ณผ ===
|
| 798 |
+
|
| 799 |
+
๐ Google ๊ฒ์ ๊ทธ๋ผ์ด๋ฉ: ์ค์๊ฐ ๋ค์ํ ์ผํํค์๋ ํธ๋ ๋ ๋ถ์ (์๋ ์คํ)
|
| 800 |
+
๐ ๋ค์ด๋ฒ ์ผํ API: ํ๊ตญ ์ผํ๋ชฐ ๋ค์ํ ํค์๋ ๋ฐ์ดํฐ ๋ถ์
|
| 801 |
+
๐ DuckDuckGo ๊ฒ์: ๊ธ๋ก๋ฒ ๋ค์ํ ์ผํํค์๋ ์ ๋ณด ๋ถ์
|
| 802 |
+
|
| 803 |
+
{comprehensive_analysis}
|
| 804 |
+
|
| 805 |
+
๐ก ์ ๋ชจ๋ ๋ฐ์ดํฐ๋ฅผ ์ข
ํฉํ์ฌ ๋งค๋ฒ ๋ค๋ฅธ ์กฐํฉ์ ์ผํํค์๋๋ฅผ ์์ฑ๏ฟฝ๏ฟฝ๋๋ค.
|
| 806 |
+
๐ฒ ๋๋ค ์๋: {random_seed} (๋งค๋ฒ ๋ค๋ฅธ ๊ฒฐ๊ณผ ๋ณด์ฅ)
|
| 807 |
+
"""
|
| 808 |
+
|
| 809 |
+
elif search_engine == "Google ๊ฒ์ ๊ทธ๋ผ์ด๋ฉ๋ง":
|
| 810 |
+
logger.info("Google ๊ฒ์ ๋๊ตฌ ์ค์ ์ค...")
|
| 811 |
+
google_search_tool = Tool(google_search=GoogleSearch())
|
| 812 |
+
config_tools = [google_search_tool]
|
| 813 |
+
search_info = f"Google ๊ฒ์ ๊ทธ๋ผ์ด๋ฉ์ ํตํ ๋ค์ํ ์ค์๊ฐ ์ผํํค์๋ ๋ถ์ (์๋: {random_seed})"
|
| 814 |
+
|
| 815 |
+
elif search_engine in ["๋ค์ด๋ฒ ๊ฒ์ API๋ง", "DuckDuckGo ๊ฒ์๋ง"]:
|
| 816 |
+
logger.info(f"{search_engine} ์ฌ์ฉํ์ฌ ๋ค์ํ ์ผํํค์๋ ์กฐ์ฌ ์ค...")
|
| 817 |
+
# ๋ค์์ฑ ๊ฐํ๋ฅผ ์ํ ๊ฒ์ ์คํ
|
| 818 |
+
search_queries = []
|
| 819 |
+
|
| 820 |
+
# ๋๋คํ๊ฒ ๋ค์ํ ๊ฒ์์ด ์์ฑ
|
| 821 |
+
base_items = random.sample(diverse_combinations, 8)
|
| 822 |
+
for item in base_items:
|
| 823 |
+
search_queries.append(f"{item} ์ผํํค์๋")
|
| 824 |
+
|
| 825 |
+
search_results = ""
|
| 826 |
+
for query in search_queries:
|
| 827 |
+
result = search_with_api(query, search_engine)
|
| 828 |
+
search_results += f"[๊ฒ์์ด: {query}]\n{result}\n\n"
|
| 829 |
+
|
| 830 |
+
search_info = f"{search_engine} ๋ค์ํ ์ผํํค์๋ ๊ฒ์ ๊ฒฐ๊ณผ (์๋: {random_seed}):\n{search_results}"
|
| 831 |
+
|
| 832 |
+
else: # ๊ฒ์ ์์ด AI๋ง ์ฌ์ฉ
|
| 833 |
+
logger.info("๊ฒ์ ์์ด AI ์ง์๋ง ์ฌ์ฉ")
|
| 834 |
+
search_info = f"AI ๋ด์ฅ ์ง์์ ๊ธฐ๋ฐ์ผ๋ก ๋ค์ํ ์ผํํค์๋ ์์ฑ (์๋: {random_seed})"
|
| 835 |
+
|
| 836 |
+
# ๋ค์์ฑ์ ๊ฐํํ ํ๋กฌํํธ - ๋งค๋ฒ ๋ค๋ฅธ ์กฐํฉ ์์ฒญ
|
| 837 |
+
diverse_sample = random.sample(diverse_combinations, 20)
|
| 838 |
+
|
| 839 |
+
prompt = f"""
|
| 840 |
+
๐ฏ ๋ค์์ฑ ๊ฐํ ์ผํํค์๋ ๋ฐ๊ตด ์์คํ
v5.0
|
| 841 |
+
|
| 842 |
+
โก ์ค์: ์ ๋ ์ค๋ณต๋์ง ์๋ ๋ค์ํ ํค์๋๋ง ์์ฑํ์ธ์!
|
| 843 |
+
|
| 844 |
+
๐ฌ ์ญํ ์ ์
|
| 845 |
+
๋น์ ์ ๋งค๋ฒ ์์ ํ ๋ค๋ฅธ ์กฐํฉ์ ์ผํํค์๋๋ฅผ ์์ฑํ๋ ์ ๋ฌธ๊ฐ์
๋๋ค.
|
| 846 |
+
|
| 847 |
+
๐ฏ ๋ชฉํ
|
| 848 |
+
์ฃผ์ด์ง ์กฐ๊ฑด์ ๋ง๋ ์ค์ ์ผํํค์๋ 50๊ฐ๋ฅผ ๋ฐ๊ตดํ๋, ์ ๋ ์ค๋ณต๋์ง ์๊ณ ๋งค๋ฒ ๋ค๋ฅธ ์กฐํฉ์ผ๋ก ๊ตฌ์ฑํ์ญ์์ค.
|
| 849 |
+
|
| 850 |
+
๐ ์
๋ ฅ๋ ์กฐ๊ฑด:
|
| 851 |
+
์นดํ
๊ณ ๋ฆฌ: {category}
|
| 852 |
+
์ถ๊ฐ ์์ฒญ์ฌํญ: {additional_request}
|
| 853 |
+
์ถ์ํ์ด๋ฐ: {launch_timing}
|
| 854 |
+
๊ณ์ ์ฑ: {seasonality}
|
| 855 |
+
๋งค์ถ๋ชฉํ: {sales_target}
|
| 856 |
+
ํ๋งค์ฑ๋: {sales_channel}
|
| 857 |
+
๊ฒฝ์๊ฐ๋: {competition_level}
|
| 858 |
+
๊ฒ์์์ง: {search_engine}
|
| 859 |
+
|
| 860 |
+
๐ ์ผํํค์๋ ๋ถ์ ์ ๋ณด:
|
| 861 |
+
{search_info}
|
| 862 |
+
|
| 863 |
+
๐ฒ ๋ค์์ฑ ๋ณด์ฅ ์ฐธ๊ณ ์กฐํฉ ์์ (์ด๊ฒ๊ณผ ๋ค๋ฅด๊ฒ ์์ฑํ์ธ์):
|
| 864 |
+
{', '.join(diverse_sample[:10])}
|
| 865 |
+
|
| 866 |
+
โ ๏ธ ํค์๋๋ณ ๋๋ค ์ ์ฉ ๊ท์น:
|
| 867 |
+
๊ฐ ํค์๋๋ง๋ค ๋ค์๊ณผ ๊ฐ์ด ์ ์ฉํ์ธ์:
|
| 868 |
+
|
| 869 |
+
{"- ์นดํ
๊ณ ๋ฆฌ: ๋งค ํค์๋๋ง๋ค " + str(random_settings['categories']) + " ์ค์์ ๋๋ค ์ ํ" if random_settings['category_random'] else f"- ์นดํ
๊ณ ๋ฆฌ: {fixed_values['category']} ๊ณ ์ "}
|
| 870 |
+
{"- ์ถ์ํ์ด๋ฐ: ๋งค ํค์๋๋ง๋ค " + str(random_settings['launch_timings']) + " ์ค์์ ๋๋ค ์ ํ" if random_settings['launch_timing_random'] else f"- ์ถ์ํ์ด๋ฐ: {fixed_values['launch_timing']} ๊ณ ์ "}
|
| 871 |
+
{"- ๊ณ์ ์ฑ: ๋งค ํค์๋๋ง๋ค " + str(random_settings['seasonalities']) + " ์ค์์ ๋๋ค ์ ํ" if random_settings['seasonality_random'] else f"- ๊ณ์ ์ฑ: {fixed_values['seasonality']} ๊ณ ์ "}
|
| 872 |
+
{"- ๋งค์ถ๋ชฉํ: ๋งค ํค์๋๋ง๋ค " + str(random_settings['sales_targets']) + " ์ค์์ ๋๋ค ์ ํ" if random_settings['sales_target_random'] else f"- ๋งค์ถ๋ชฉํ: {fixed_values['sales_target']} ๊ณ ์ "}
|
| 873 |
+
{"- ํ๋งค์ฑ๋: ๋งค ํค์๋๋ง๋ค " + str(random_settings['sales_channels']) + " ์ค์์ ๋๋ค ์ ํ" if random_settings['sales_channel_random'] else f"- ํ๋งค์ฑ๋: {fixed_values['sales_channel']} ๊ณ ์ "}
|
| 874 |
+
{"- ๊ฒฝ์๊ฐ๋: ๋งค ํค์๋๋ง๋ค " + str(random_settings['competition_levels']) + " ์ค์์ ๋๋ค ์ ํ" if random_settings['competition_level_random'] else f"- ๊ฒฝ์๊ฐ๋: {fixed_values['competition_level']} ๊ณ ์ "}
|
| 875 |
+
|
| 876 |
+
โ๏ธ ๋ค์์ฑ ๊ฐํ ์ํฌํ๋ก์ฐ
|
| 877 |
+
1๋จ๊ณ: ์์ ํ ์๋ก์ด ์กฐํฉ ์์ฑ
|
| 878 |
+
- ์ด์ ๊ฒฐ๊ณผ์ ์ ๋ ์ค๋ณต๋์ง ์๋ ํค์๋ ์กฐํฉ
|
| 879 |
+
- ์์ฌ({', '.join(MATERIAL_KEYWORDS[:5])}) + ์ํ๋ช
์กฐํฉ
|
| 880 |
+
- ํํ({', '.join(SHAPE_KEYWORDS[:5])}) + ์ํ๋ช
์กฐํฉ
|
| 881 |
+
- ๊ธฐ๋ฅ({', '.join(FUNCTION_KEYWORDS[:5])}) + ์ํ๋ช
์กฐํฉ
|
| 882 |
+
|
| 883 |
+
2๋จ๊ณ: ์ค๋ณต ๋ฐฉ์ง ํํฐ๋ง
|
| 884 |
+
- ๋์ผํ ํค์๋ ์กฐํฉ ์์ ๋ฐฐ์
|
| 885 |
+
- ์ ์ฌํ ์๋ฏธ์ ํค์๋ ์กฐํฉ ๋ฐฐ์
|
| 886 |
+
- ๋งค๋ฒ ์๋ก์ด ๊ฐ๋๋ก ์ ๊ทผ
|
| 887 |
+
|
| 888 |
+
3๋จ๊ณ: 50๊ฐ ๋ค์ํ ํค์๋ ์ ๋ณ
|
| 889 |
+
- ๋ธ๋๋๋ช
์ ๋ ๊ธ์ง
|
| 890 |
+
- ๋ณต์กํ ๊ธฐ์ ์ฉ์ด ๊ธ์ง
|
| 891 |
+
- ์ต๋ 2๊ฐ ๋จ์ด ์กฐํฉ๋ง ํ์ฉ
|
| 892 |
+
|
| 893 |
+
โ ๏ธ ๋ค์์ฑ ๊ฐํ ํค์๋ ๊ตฌ์ฑ ๊ท์น (๋งค์ฐ ์ค์):
|
| 894 |
+
|
| 895 |
+
๐ซ ์ ๋ ๊ธ์ง ์ฌํญ:
|
| 896 |
+
- ๋์ผํ๊ฑฐ๋ ์ ์ฌํ ํค์๋ ๋ฐ๋ณต
|
| 897 |
+
- ๋ธ๋๋๋ช
(์ผ์ฑ, LG, ๋์ดํค ๋ฑ)
|
| 898 |
+
- ๋ณต์กํ ๊ธฐ์ ์ฉ์ด
|
| 899 |
+
- 3๊ฐ ์ด์ ๋ณตํฉ์ด
|
| 900 |
+
|
| 901 |
+
โ
๋ฐ๋์ ๋ค์ํ๊ฒ ํฌํจํด์ผ ํ ํํ:
|
| 902 |
+
1. ์์ฌ๋ณ ํค์๋ (์: ๋๋๋ฌด ๋๋ง, ๊ตฌ๋ฆฌ ์ปต)
|
| 903 |
+
2. ํํ๋ณ ํค์๋ (์: ์ํ ์ ์, ์ฌ๋ฆผ ์ผ์ด์ค)
|
| 904 |
+
3. ๏ฟฝ๏ฟฝ๏ฟฝ๋ฅ๋ณ ํค์๋ (์: ๋ฐฉ์ ํ์ฐ์น, ํญ๊ท ์๊ฑด)
|
| 905 |
+
4. ์นดํ
๊ณ ๋ฆฌ๋ณ ํค์๋ (์: ์๋ฉํจ, ์กฐ๋ฆฌ๋๊ตฌ)
|
| 906 |
+
|
| 907 |
+
๐ฏ ๋ค์์ฑ ๋ณด์ฅ ์ ๋ต:
|
| 908 |
+
- ์ ๋ ๊ฐ์ ์์ฌ๋ฅผ 2๋ฒ ์ด์ ์ฌ์ฉํ์ง ๋ง์ธ์
|
| 909 |
+
- ์ ๋ ๊ฐ์ ํํ๋ฅผ 2๋ฒ ์ด์ ์ฌ์ฉํ์ง ๋ง์ธ์
|
| 910 |
+
- ์ ๋ ๊ฐ์ ๊ธฐ๋ฅ์ 2๋ฒ ์ด์ ์ฌ์ฉํ์ง ๋ง์ธ์
|
| 911 |
+
- ๋งค ํค์๋๋ง๋ค ์์ ํ ๋ค๋ฅธ ์กฐํฉ์ผ๋ก ์์ฑํ์ธ์
|
| 912 |
+
|
| 913 |
+
์ฌ๋ฐ๋ฅธ ๋ค์ํ ํค์๋ ์์:
|
| 914 |
+
โ
๋๋๋ฌด ๋๋ง (์์ฌ+์ํ)
|
| 915 |
+
โ
์ํ ์ ์ (ํํ+์ํ)
|
| 916 |
+
โ
๋ฐฉ์ ํ์ฐ์น (๊ธฐ๋ฅ+์ํ)
|
| 917 |
+
โ
์ธ๋ผ๋ฏน ๋จธ๊ทธ์ปต (์์ฌ+์ํ)
|
| 918 |
+
โ
์ ์ด์ ์ ๋ฐ (ํํ+์ํ)
|
| 919 |
+
โ
ํญ๊ท ์๊ฑด (๊ธฐ๋ฅ+์ํ)
|
| 920 |
+
|
| 921 |
+
์๋ชป๋ ๋ฐ๋ณต ํค์๋ ์์:
|
| 922 |
+
โ ๋๋๋ฌด ๋๋ง, ๋๋๋ฌด ์ ๊ฐ๋ฝ (์์ฌ ๋ฐ๋ณต)
|
| 923 |
+
โ ์ํ ์ ์, ์ํ ์๋ฐ (ํํ ๋ฐ๋ณต)
|
| 924 |
+
โ ๋ฐฉ์ ํ์ฐ์น, ๋ฐฉ์ ์ผ์ด์ค (๊ธฐ๋ฅ ๋ฐ๋ณต)
|
| 925 |
+
|
| 926 |
+
๐ ์ถ๋ ฅ ํ์:
|
| 927 |
+
์ค์ง ์์ ํ ๋ค๋ฅธ ์ผํํค์๋๋ง ํ ์ค์ฉ 50๊ฐ ์ถ๋ ฅ
|
| 928 |
+
- ๋ฒํธ ๊ธ์ง
|
| 929 |
+
- ์ค๋ช
๊ธ์ง
|
| 930 |
+
- ๊ธฐํธ๋ ํน์๋ฌธ์ ๊ธ์ง
|
| 931 |
+
- ๊ดํธ ์ ์ค๋ช
๊ธ์ง
|
| 932 |
+
- ์์ ํค์๋๋ง ์ถ๋ ฅ
|
| 933 |
+
- ์ ๋ ์ค๋ณต ๊ธ์ง
|
| 934 |
+
|
| 935 |
+
์์ ์ถ๋ ฅ ํํ (๋งค๋ฒ ์์ ํ ๋ค๋ฅด๊ฒ):
|
| 936 |
+
์ ๋ฆฌ ํ๋ถ
|
| 937 |
+
์ ์ด์ ์์
|
| 938 |
+
ํญ๊ท ๋๋ง
|
| 939 |
+
์๋ฃจ๋ฏธ๋ ํ
๋ธ๋ฌ
|
| 940 |
+
์ฌ๋ฆผ ํ์ผํจ
|
| 941 |
+
|
| 942 |
+
โก ์ง๊ธ ๋ฐ๋ก ์ ๋ ์ค๋ณต๋์ง ์๋ ์์ ํ ์๋ก์ด ์ผํํค์๋ 50๊ฐ๋ฅผ ๊ฐ๊ฐ ๋ค๋ฅธ ๋๋ค ์กฐ๊ฑด์ ์ ์ฉํ์ฌ ์ถ๋ ฅํ์ธ์.
|
| 943 |
+
๋งค๋ฒ ์คํํ ๋๋ง๋ค ์์ ํ ๋ค๋ฅธ ๊ฒฐ๊ณผ๊ฐ ๋์์ผ ํฉ๋๋ค!
|
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
|
style.css
CHANGED
|
@@ -1,29 +1,117 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
:root {
|
|
|
|
| 2 |
--primary-color: #FB7F0D;
|
| 3 |
--secondary-color: #ff9a8b;
|
| 4 |
--accent-color: #FF6B6B;
|
|
|
|
|
|
|
| 5 |
--background-color: #FFFFFF;
|
| 6 |
--card-bg: #ffffff;
|
|
|
|
|
|
|
|
|
|
| 7 |
--text-color: #334155;
|
| 8 |
-
--
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
--shadow: 0 8px 30px rgba(251, 127, 13, 0.08);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
}
|
| 11 |
|
| 12 |
/* โโ ์ ์ญ ์คํ์ผ โโ */
|
| 13 |
body {
|
| 14 |
font-family: 'Pretendard', 'Noto Sans KR', -apple-system, BlinkMacSystemFont, sans-serif;
|
| 15 |
-
background-color: var(--background-color);
|
| 16 |
-
color: var(--text-color);
|
| 17 |
line-height: 1.6;
|
| 18 |
margin: 0;
|
| 19 |
padding: 0;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
}
|
| 21 |
|
| 22 |
.gradio-container {
|
| 23 |
width: 100%;
|
| 24 |
margin: 0 auto;
|
| 25 |
padding: 20px;
|
| 26 |
-
background-color: var(--background-color);
|
| 27 |
}
|
| 28 |
|
| 29 |
/* โโ ์น์
์คํ์ผ โโ */
|
|
@@ -43,12 +131,13 @@ body {
|
|
| 43 |
|
| 44 |
/* ์น์
ํ๋ ์ */
|
| 45 |
.custom-frame {
|
| 46 |
-
background-color: var(--card-bg);
|
| 47 |
-
border: 1px solid
|
| 48 |
border-radius: var(--border-radius);
|
| 49 |
padding: 20px;
|
| 50 |
margin: 10px 0;
|
| 51 |
-
box-shadow: var(--shadow);
|
|
|
|
| 52 |
}
|
| 53 |
|
| 54 |
/* ์ ์ ์ ์๋ ์น์
*/
|
|
@@ -75,18 +164,17 @@ body {
|
|
| 75 |
.collapsible-content {
|
| 76 |
display: none;
|
| 77 |
padding: 15px;
|
| 78 |
-
background-color: var(--card-bg);
|
| 79 |
-
border: 1px solid
|
| 80 |
border-radius: 0 0 var(--border-radius) var(--border-radius);
|
| 81 |
margin-top: -5px;
|
|
|
|
| 82 |
}
|
| 83 |
|
| 84 |
.collapsible-content.active {
|
| 85 |
display: block;
|
| 86 |
}
|
| 87 |
|
| 88 |
-
|
| 89 |
-
|
| 90 |
/* ๋ ๋ฒํผ์ ๊ณตํต์ผ๋ก ์ ์ฉํ ์คํ์ผ */
|
| 91 |
.execution-button {
|
| 92 |
font-size: 18px !important;
|
|
@@ -106,12 +194,12 @@ body {
|
|
| 106 |
|
| 107 |
/* ๊ฐ ๋ฒํผ๋ณ ๊ณ ์ ์์ */
|
| 108 |
.primary-button {
|
| 109 |
-
background:
|
| 110 |
box-shadow: 0 4px 8px rgba(251, 127, 13, 0.25) !important;
|
| 111 |
}
|
| 112 |
|
| 113 |
.secondary-button {
|
| 114 |
-
background:
|
| 115 |
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.25) !important;
|
| 116 |
}
|
| 117 |
|
|
@@ -124,19 +212,17 @@ body {
|
|
| 124 |
.execution-section {
|
| 125 |
margin-top: 20px;
|
| 126 |
padding: 15px;
|
| 127 |
-
background-color:
|
| 128 |
border-radius: 8px;
|
| 129 |
-
border: 1px solid
|
|
|
|
| 130 |
}
|
| 131 |
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
/* โโ ์ปด๏ฟฝ๏ฟฝ๏ฟฝ๋ํธ ์คํ์ผ โโ */
|
| 136 |
/* ๋ฒํผ ์คํ์ผ */
|
| 137 |
.custom-button {
|
| 138 |
border-radius: 30px !important;
|
| 139 |
-
background:
|
| 140 |
color: white !important;
|
| 141 |
font-size: 18px !important;
|
| 142 |
padding: 10px 20px !important;
|
|
@@ -153,7 +239,7 @@ body {
|
|
| 153 |
/* ์์ ๋ฒํผ */
|
| 154 |
.custom-button-small {
|
| 155 |
border-radius: 20px !important;
|
| 156 |
-
background:
|
| 157 |
color: white !important;
|
| 158 |
font-size: 14px !important;
|
| 159 |
padding: 8px 15px !important;
|
|
@@ -170,7 +256,7 @@ body {
|
|
| 170 |
/* ๋ฆฌ์
๋ฒํผ */
|
| 171 |
.reset-button {
|
| 172 |
border-radius: 30px !important;
|
| 173 |
-
background:
|
| 174 |
color: white !important;
|
| 175 |
font-size: 16px !important;
|
| 176 |
padding: 8px 16px !important;
|
|
@@ -186,15 +272,29 @@ body {
|
|
| 186 |
}
|
| 187 |
|
| 188 |
/* ์
๋ ฅ ํ๋ ์คํ์ผ */
|
| 189 |
-
.gr-input, .gr-text-input, .gr-sample-inputs
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
border-radius: var(--border-radius) !important;
|
| 191 |
-
border: 1px solid
|
| 192 |
padding: 12px !important;
|
| 193 |
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05) !important;
|
| 194 |
transition: all 0.3s ease !important;
|
|
|
|
|
|
|
| 195 |
}
|
| 196 |
|
| 197 |
-
.gr-input:focus, .gr-text-input:focus
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 198 |
border-color: var(--primary-color) !important;
|
| 199 |
outline: none !important;
|
| 200 |
box-shadow: 0 0 0 2px rgba(251, 127, 13, 0.2) !important;
|
|
@@ -208,9 +308,11 @@ input[type="checkbox"], input[type="radio"] {
|
|
| 208 |
/* ๋๋กญ๋ค์ด ์คํ์ผ */
|
| 209 |
.gr-dropdown {
|
| 210 |
border-radius: var(--border-radius) !important;
|
| 211 |
-
border: 1px solid
|
| 212 |
padding: 12px !important;
|
| 213 |
transition: all 0.3s ease !important;
|
|
|
|
|
|
|
| 214 |
}
|
| 215 |
|
| 216 |
.gr-dropdown:focus {
|
|
@@ -225,7 +327,7 @@ input[type="checkbox"], input[type="radio"] {
|
|
| 225 |
align-items: center;
|
| 226 |
font-size: 20px;
|
| 227 |
font-weight: 700;
|
| 228 |
-
color:
|
| 229 |
margin-bottom: 10px;
|
| 230 |
padding-bottom: 5px;
|
| 231 |
border-bottom: 2px solid var(--primary-color);
|
|
@@ -242,7 +344,7 @@ input[type="checkbox"], input[type="radio"] {
|
|
| 242 |
.subsection-title {
|
| 243 |
font-size: 18px;
|
| 244 |
font-weight: 600;
|
| 245 |
-
color:
|
| 246 |
margin: 15px 0 8px 0;
|
| 247 |
}
|
| 248 |
|
|
@@ -254,32 +356,38 @@ input[type="checkbox"], input[type="radio"] {
|
|
| 254 |
margin: 0;
|
| 255 |
padding: 0;
|
| 256 |
font-size: 14px;
|
|
|
|
|
|
|
|
|
|
| 257 |
}
|
| 258 |
|
| 259 |
.styled-table th,
|
| 260 |
.styled-table td {
|
| 261 |
padding: 12px 15px;
|
| 262 |
text-align: left;
|
| 263 |
-
border-bottom: 1px solid
|
| 264 |
overflow: hidden;
|
| 265 |
text-overflow: ellipsis;
|
|
|
|
|
|
|
| 266 |
}
|
| 267 |
|
| 268 |
.styled-table th {
|
| 269 |
-
background-color: var(--primary-color);
|
| 270 |
-
color: white;
|
| 271 |
font-weight: bold;
|
| 272 |
position: sticky;
|
| 273 |
top: 0;
|
| 274 |
white-space: nowrap;
|
|
|
|
| 275 |
}
|
| 276 |
|
| 277 |
.styled-table tbody tr:nth-of-type(even) {
|
| 278 |
-
background-color:
|
| 279 |
}
|
| 280 |
|
| 281 |
.styled-table tbody tr:hover {
|
| 282 |
-
background-color:
|
| 283 |
}
|
| 284 |
|
| 285 |
.styled-table tbody tr:last-of-type {
|
|
@@ -291,8 +399,40 @@ input[type="checkbox"], input[type="radio"] {
|
|
| 291 |
max-height: 600px;
|
| 292 |
overflow-y: auto;
|
| 293 |
border-radius: var(--border-radius);
|
| 294 |
-
border: 1px solid
|
| 295 |
margin-top: 15px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 296 |
}
|
| 297 |
|
| 298 |
/* ์คํฌ๋กค๋ฐ ์คํ์ผ */
|
|
@@ -302,7 +442,7 @@ input[type="checkbox"], input[type="radio"] {
|
|
| 302 |
}
|
| 303 |
|
| 304 |
::-webkit-scrollbar-track {
|
| 305 |
-
background:
|
| 306 |
border-radius: 10px;
|
| 307 |
}
|
| 308 |
|
|
@@ -311,13 +451,18 @@ input[type="checkbox"], input[type="radio"] {
|
|
| 311 |
border-radius: 10px;
|
| 312 |
}
|
| 313 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 314 |
/* โโ ๋ถ์ ๊ฒฐ๊ณผ ์คํ์ผ โโ */
|
| 315 |
.analysis-result {
|
| 316 |
margin-top: 30px;
|
| 317 |
-
border: 1px solid
|
| 318 |
border-radius: 5px;
|
| 319 |
padding: 15px;
|
| 320 |
-
background-color:
|
|
|
|
| 321 |
}
|
| 322 |
|
| 323 |
.result-header {
|
|
@@ -328,24 +473,28 @@ input[type="checkbox"], input[type="radio"] {
|
|
| 328 |
}
|
| 329 |
|
| 330 |
.summary-box {
|
| 331 |
-
background-color:
|
| 332 |
border-left: 4px solid var(--primary-color);
|
| 333 |
padding: 10px 15px;
|
| 334 |
margin-bottom: 20px;
|
| 335 |
font-size: 14px;
|
|
|
|
| 336 |
}
|
| 337 |
|
| 338 |
.summary-title {
|
| 339 |
font-weight: bold;
|
| 340 |
margin-bottom: 5px;
|
|
|
|
| 341 |
}
|
| 342 |
|
| 343 |
.recommendation-box {
|
| 344 |
-
background-color:
|
| 345 |
border-radius: 5px;
|
| 346 |
padding: 15px;
|
| 347 |
margin-bottom: 25px;
|
| 348 |
-
box-shadow:
|
|
|
|
|
|
|
| 349 |
}
|
| 350 |
|
| 351 |
.recommendation-title {
|
|
@@ -357,20 +506,22 @@ input[type="checkbox"], input[type="radio"] {
|
|
| 357 |
|
| 358 |
.recommendation-item {
|
| 359 |
padding: 6px 0;
|
| 360 |
-
border-bottom: 1px solid
|
|
|
|
| 361 |
}
|
| 362 |
|
| 363 |
.recommendation-item:last-child {
|
| 364 |
border-bottom: none;
|
| 365 |
}
|
| 366 |
|
| 367 |
-
/*
|
| 368 |
.keyword-tag-container {
|
| 369 |
margin-top: 20px;
|
| 370 |
padding: 10px;
|
| 371 |
-
border: 1px solid
|
| 372 |
border-radius: 5px;
|
| 373 |
-
background-color:
|
|
|
|
| 374 |
}
|
| 375 |
|
| 376 |
.keyword-tag {
|
|
@@ -414,6 +565,76 @@ input[type="checkbox"], input[type="radio"] {
|
|
| 414 |
animation: fadeIn 0.5s ease-out;
|
| 415 |
}
|
| 416 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 417 |
/* ๋ฐ์ํ ์กฐ์ */
|
| 418 |
@media (max-width: 768px) {
|
| 419 |
.grid-container {
|
|
|
|
| 1 |
+
/* ============================================
|
| 2 |
+
๋คํฌ๋ชจ๋ ์๋ ๋ณ๊ฒฝ AI ์ํ ์์ฑ ๋ถ์ ์์คํ
CSS
|
| 3 |
+
============================================ */
|
| 4 |
+
|
| 5 |
+
/* 1. CSS ๋ณ์ ์ ์ (๋ผ์ดํธ๋ชจ๋ - ๊ธฐ๋ณธ๊ฐ) */
|
| 6 |
:root {
|
| 7 |
+
/* ๋ฉ์ธ ์ปฌ๋ฌ */
|
| 8 |
--primary-color: #FB7F0D;
|
| 9 |
--secondary-color: #ff9a8b;
|
| 10 |
--accent-color: #FF6B6B;
|
| 11 |
+
|
| 12 |
+
/* ๋ฐฐ๊ฒฝ ์ปฌ๋ฌ */
|
| 13 |
--background-color: #FFFFFF;
|
| 14 |
--card-bg: #ffffff;
|
| 15 |
+
--input-bg: #ffffff;
|
| 16 |
+
|
| 17 |
+
/* ํ
์คํธ ์ปฌ๋ฌ */
|
| 18 |
--text-color: #334155;
|
| 19 |
+
--text-secondary: #64748b;
|
| 20 |
+
|
| 21 |
+
/* ๋ณด๋ ๋ฐ ๊ตฌ๋ถ์ */
|
| 22 |
+
--border-color: #dddddd;
|
| 23 |
+
--border-light: #e5e5e5;
|
| 24 |
+
|
| 25 |
+
/* ํ
์ด๋ธ ์ปฌ๋ฌ */
|
| 26 |
+
--table-even-bg: #f3f3f3;
|
| 27 |
+
--table-hover-bg: #f0f0f0;
|
| 28 |
+
|
| 29 |
+
/* ๊ทธ๋ฆผ์ */
|
| 30 |
--shadow: 0 8px 30px rgba(251, 127, 13, 0.08);
|
| 31 |
+
--shadow-light: 0 2px 4px rgba(0, 0, 0, 0.1);
|
| 32 |
+
|
| 33 |
+
/* ๊ธฐํ */
|
| 34 |
+
--border-radius: 18px;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
/* 2. ๋คํฌ๋ชจ๋ ์์ ๋ณ์ (์๋ ๊ฐ์ง) */
|
| 38 |
+
@media (prefers-color-scheme: dark) {
|
| 39 |
+
:root {
|
| 40 |
+
/* ๋ฐฐ๊ฒฝ ์ปฌ๋ฌ */
|
| 41 |
+
--background-color: #1a1a1a;
|
| 42 |
+
--card-bg: #2d2d2d;
|
| 43 |
+
--input-bg: #2d2d2d;
|
| 44 |
+
|
| 45 |
+
/* ํ
์คํธ ์ปฌ๋ฌ */
|
| 46 |
+
--text-color: #e5e5e5;
|
| 47 |
+
--text-secondary: #a1a1aa;
|
| 48 |
+
|
| 49 |
+
/* ๋ณด๋ ๋ฐ ๊ตฌ๋ถ์ */
|
| 50 |
+
--border-color: #404040;
|
| 51 |
+
--border-light: #525252;
|
| 52 |
+
|
| 53 |
+
/* ํ
์ด๋ธ ์ปฌ๋ฌ */
|
| 54 |
+
--table-even-bg: #333333;
|
| 55 |
+
--table-hover-bg: #404040;
|
| 56 |
+
|
| 57 |
+
/* ๊ทธ๋ฆผ์ */
|
| 58 |
+
--shadow: 0 8px 30px rgba(0, 0, 0, 0.3);
|
| 59 |
+
--shadow-light: 0 2px 4px rgba(0, 0, 0, 0.2);
|
| 60 |
+
}
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
/* 3. ์๋ ๋คํฌ๋ชจ๋ ํด๋์ค (Gradio ํ ๊ธ์ฉ) */
|
| 64 |
+
[data-theme="dark"],
|
| 65 |
+
.dark,
|
| 66 |
+
.gr-theme-dark {
|
| 67 |
+
/* ๋ฐฐ๊ฒฝ ์ปฌ๋ฌ */
|
| 68 |
+
--background-color: #1a1a1a;
|
| 69 |
+
--card-bg: #2d2d2d;
|
| 70 |
+
--input-bg: #2d2d2d;
|
| 71 |
+
|
| 72 |
+
/* ํ
์คํธ ์ปฌ๋ฌ */
|
| 73 |
+
--text-color: #e5e5e5;
|
| 74 |
+
--text-secondary: #a1a1aa;
|
| 75 |
+
|
| 76 |
+
/* ๋ณด๋ ๋ฐ ๊ตฌ๋ถ์ */
|
| 77 |
+
--border-color: #404040;
|
| 78 |
+
--border-light: #525252;
|
| 79 |
+
|
| 80 |
+
/* ํ
์ด๋ธ ์ปฌ๋ฌ */
|
| 81 |
+
--table-even-bg: #333333;
|
| 82 |
+
--table-hover-bg: #404040;
|
| 83 |
+
|
| 84 |
+
/* ๊ทธ๋ฆผ์ */
|
| 85 |
+
--shadow: 0 8px 30px rgba(0, 0, 0, 0.3);
|
| 86 |
+
--shadow-light: 0 2px 4px rgba(0, 0, 0, 0.2);
|
| 87 |
}
|
| 88 |
|
| 89 |
/* โโ ์ ์ญ ์คํ์ผ โโ */
|
| 90 |
body {
|
| 91 |
font-family: 'Pretendard', 'Noto Sans KR', -apple-system, BlinkMacSystemFont, sans-serif;
|
| 92 |
+
background-color: var(--background-color) !important;
|
| 93 |
+
color: var(--text-color) !important;
|
| 94 |
line-height: 1.6;
|
| 95 |
margin: 0;
|
| 96 |
padding: 0;
|
| 97 |
+
transition: background-color 0.3s ease, color 0.3s ease;
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
.gradio-container,
|
| 102 |
+
.gradio-container *,
|
| 103 |
+
.gr-app,
|
| 104 |
+
.gr-app *,
|
| 105 |
+
.gr-interface {
|
| 106 |
+
background-color: var(--background-color) !important;
|
| 107 |
+
color: var(--text-color) !important;
|
| 108 |
}
|
| 109 |
|
| 110 |
.gradio-container {
|
| 111 |
width: 100%;
|
| 112 |
margin: 0 auto;
|
| 113 |
padding: 20px;
|
| 114 |
+
background-color: var(--background-color) !important;
|
| 115 |
}
|
| 116 |
|
| 117 |
/* โโ ์น์
์คํ์ผ โโ */
|
|
|
|
| 131 |
|
| 132 |
/* ์น์
ํ๋ ์ */
|
| 133 |
.custom-frame {
|
| 134 |
+
background-color: var(--card-bg) !important;
|
| 135 |
+
border: 1px solid var(--border-light) !important;
|
| 136 |
border-radius: var(--border-radius);
|
| 137 |
padding: 20px;
|
| 138 |
margin: 10px 0;
|
| 139 |
+
box-shadow: var(--shadow) !important;
|
| 140 |
+
color: var(--text-color) !important;
|
| 141 |
}
|
| 142 |
|
| 143 |
/* ์ ์ ์ ์๋ ์น์
*/
|
|
|
|
| 164 |
.collapsible-content {
|
| 165 |
display: none;
|
| 166 |
padding: 15px;
|
| 167 |
+
background-color: var(--card-bg) !important;
|
| 168 |
+
border: 1px solid var(--border-light) !important;
|
| 169 |
border-radius: 0 0 var(--border-radius) var(--border-radius);
|
| 170 |
margin-top: -5px;
|
| 171 |
+
color: var(--text-color) !important;
|
| 172 |
}
|
| 173 |
|
| 174 |
.collapsible-content.active {
|
| 175 |
display: block;
|
| 176 |
}
|
| 177 |
|
|
|
|
|
|
|
| 178 |
/* ๋ ๋ฒํผ์ ๊ณตํต์ผ๋ก ์ ์ฉํ ์คํ์ผ */
|
| 179 |
.execution-button {
|
| 180 |
font-size: 18px !important;
|
|
|
|
| 194 |
|
| 195 |
/* ๊ฐ ๋ฒํผ๋ณ ๊ณ ์ ์์ */
|
| 196 |
.primary-button {
|
| 197 |
+
background: var(--primary-color) !important;
|
| 198 |
box-shadow: 0 4px 8px rgba(251, 127, 13, 0.25) !important;
|
| 199 |
}
|
| 200 |
|
| 201 |
.secondary-button {
|
| 202 |
+
background: #6c757d !important;
|
| 203 |
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.25) !important;
|
| 204 |
}
|
| 205 |
|
|
|
|
| 212 |
.execution-section {
|
| 213 |
margin-top: 20px;
|
| 214 |
padding: 15px;
|
| 215 |
+
background-color: var(--card-bg) !important;
|
| 216 |
border-radius: 8px;
|
| 217 |
+
border: 1px solid var(--border-light) !important;
|
| 218 |
+
color: var(--text-color) !important;
|
| 219 |
}
|
| 220 |
|
|
|
|
|
|
|
|
|
|
| 221 |
/* โโ ์ปด๏ฟฝ๏ฟฝ๏ฟฝ๋ํธ ์คํ์ผ โโ */
|
| 222 |
/* ๋ฒํผ ์คํ์ผ */
|
| 223 |
.custom-button {
|
| 224 |
border-radius: 30px !important;
|
| 225 |
+
background: var(--primary-color) !important;
|
| 226 |
color: white !important;
|
| 227 |
font-size: 18px !important;
|
| 228 |
padding: 10px 20px !important;
|
|
|
|
| 239 |
/* ์์ ๋ฒํผ */
|
| 240 |
.custom-button-small {
|
| 241 |
border-radius: 20px !important;
|
| 242 |
+
background: var(--primary-color) !important;
|
| 243 |
color: white !important;
|
| 244 |
font-size: 14px !important;
|
| 245 |
padding: 8px 15px !important;
|
|
|
|
| 256 |
/* ๋ฆฌ์
๋ฒํผ */
|
| 257 |
.reset-button {
|
| 258 |
border-radius: 30px !important;
|
| 259 |
+
background: #6c757d !important;
|
| 260 |
color: white !important;
|
| 261 |
font-size: 16px !important;
|
| 262 |
padding: 8px 16px !important;
|
|
|
|
| 272 |
}
|
| 273 |
|
| 274 |
/* ์
๋ ฅ ํ๋ ์คํ์ผ */
|
| 275 |
+
.gr-input, .gr-text-input, .gr-sample-inputs,
|
| 276 |
+
input[type="text"],
|
| 277 |
+
input[type="number"],
|
| 278 |
+
input[type="email"],
|
| 279 |
+
input[type="password"],
|
| 280 |
+
textarea,
|
| 281 |
+
select {
|
| 282 |
border-radius: var(--border-radius) !important;
|
| 283 |
+
border: 1px solid var(--border-color) !important;
|
| 284 |
padding: 12px !important;
|
| 285 |
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05) !important;
|
| 286 |
transition: all 0.3s ease !important;
|
| 287 |
+
background-color: var(--input-bg) !important;
|
| 288 |
+
color: var(--text-color) !important;
|
| 289 |
}
|
| 290 |
|
| 291 |
+
.gr-input:focus, .gr-text-input:focus,
|
| 292 |
+
input[type="text"]:focus,
|
| 293 |
+
input[type="number"]:focus,
|
| 294 |
+
input[type="email"]:focus,
|
| 295 |
+
input[type="password"]:focus,
|
| 296 |
+
textarea:focus,
|
| 297 |
+
select:focus {
|
| 298 |
border-color: var(--primary-color) !important;
|
| 299 |
outline: none !important;
|
| 300 |
box-shadow: 0 0 0 2px rgba(251, 127, 13, 0.2) !important;
|
|
|
|
| 308 |
/* ๋๋กญ๋ค์ด ์คํ์ผ */
|
| 309 |
.gr-dropdown {
|
| 310 |
border-radius: var(--border-radius) !important;
|
| 311 |
+
border: 1px solid var(--border-color) !important;
|
| 312 |
padding: 12px !important;
|
| 313 |
transition: all 0.3s ease !important;
|
| 314 |
+
background-color: var(--input-bg) !important;
|
| 315 |
+
color: var(--text-color) !important;
|
| 316 |
}
|
| 317 |
|
| 318 |
.gr-dropdown:focus {
|
|
|
|
| 327 |
align-items: center;
|
| 328 |
font-size: 20px;
|
| 329 |
font-weight: 700;
|
| 330 |
+
color: var(--text-color) !important;
|
| 331 |
margin-bottom: 10px;
|
| 332 |
padding-bottom: 5px;
|
| 333 |
border-bottom: 2px solid var(--primary-color);
|
|
|
|
| 344 |
.subsection-title {
|
| 345 |
font-size: 18px;
|
| 346 |
font-weight: 600;
|
| 347 |
+
color: var(--text-color) !important;
|
| 348 |
margin: 15px 0 8px 0;
|
| 349 |
}
|
| 350 |
|
|
|
|
| 356 |
margin: 0;
|
| 357 |
padding: 0;
|
| 358 |
font-size: 14px;
|
| 359 |
+
background-color: var(--card-bg) !important;
|
| 360 |
+
color: var(--text-color) !important;
|
| 361 |
+
position: relative;
|
| 362 |
}
|
| 363 |
|
| 364 |
.styled-table th,
|
| 365 |
.styled-table td {
|
| 366 |
padding: 12px 15px;
|
| 367 |
text-align: left;
|
| 368 |
+
border-bottom: 1px solid var(--border-color) !important;
|
| 369 |
overflow: hidden;
|
| 370 |
text-overflow: ellipsis;
|
| 371 |
+
background-color: var(--card-bg) !important;
|
| 372 |
+
color: var(--text-color) !important;
|
| 373 |
}
|
| 374 |
|
| 375 |
.styled-table th {
|
| 376 |
+
background-color: var(--primary-color) !important;
|
| 377 |
+
color: white !important;
|
| 378 |
font-weight: bold;
|
| 379 |
position: sticky;
|
| 380 |
top: 0;
|
| 381 |
white-space: nowrap;
|
| 382 |
+
z-index: 10;
|
| 383 |
}
|
| 384 |
|
| 385 |
.styled-table tbody tr:nth-of-type(even) {
|
| 386 |
+
background-color: var(--table-even-bg) !important;
|
| 387 |
}
|
| 388 |
|
| 389 |
.styled-table tbody tr:hover {
|
| 390 |
+
background-color: var(--table-hover-bg) !important;
|
| 391 |
}
|
| 392 |
|
| 393 |
.styled-table tbody tr:last-of-type {
|
|
|
|
| 399 |
max-height: 600px;
|
| 400 |
overflow-y: auto;
|
| 401 |
border-radius: var(--border-radius);
|
| 402 |
+
border: 1px solid var(--border-light) !important;
|
| 403 |
margin-top: 15px;
|
| 404 |
+
background-color: var(--card-bg) !important;
|
| 405 |
+
position: relative;
|
| 406 |
+
}
|
| 407 |
+
|
| 408 |
+
/* ํ
์ด๋ธ ์ปจํ
์ด๋ ์ถ๊ฐ ์คํ์ผ */
|
| 409 |
+
.table-container {
|
| 410 |
+
position: relative;
|
| 411 |
+
width: 100%;
|
| 412 |
+
margin: 0;
|
| 413 |
+
border-radius: 8px;
|
| 414 |
+
overflow: hidden;
|
| 415 |
+
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
|
| 416 |
+
}
|
| 417 |
+
|
| 418 |
+
.header-wrap {
|
| 419 |
+
position: sticky;
|
| 420 |
+
top: 0;
|
| 421 |
+
z-index: 20;
|
| 422 |
+
background-color: var(--primary-color) !important;
|
| 423 |
+
}
|
| 424 |
+
|
| 425 |
+
/* ํ
์ด๋ธ ๋ฐ์ดํฐ๊ฐ ํค๋ ์๋ก ์ค๋ ๊ฒ์ ๋ฐฉ์ง */
|
| 426 |
+
.styled-table tbody td {
|
| 427 |
+
position: relative;
|
| 428 |
+
z-index: 1;
|
| 429 |
+
}
|
| 430 |
+
|
| 431 |
+
.styled-table thead th {
|
| 432 |
+
position: sticky;
|
| 433 |
+
top: 0;
|
| 434 |
+
z-index: 10;
|
| 435 |
+
background-color: var(--primary-color) !important;
|
| 436 |
}
|
| 437 |
|
| 438 |
/* ์คํฌ๋กค๋ฐ ์คํ์ผ */
|
|
|
|
| 442 |
}
|
| 443 |
|
| 444 |
::-webkit-scrollbar-track {
|
| 445 |
+
background: var(--card-bg);
|
| 446 |
border-radius: 10px;
|
| 447 |
}
|
| 448 |
|
|
|
|
| 451 |
border-radius: 10px;
|
| 452 |
}
|
| 453 |
|
| 454 |
+
::-webkit-scrollbar-thumb:hover {
|
| 455 |
+
background: var(--secondary-color);
|
| 456 |
+
}
|
| 457 |
+
|
| 458 |
/* โโ ๋ถ์ ๊ฒฐ๊ณผ ์คํ์ผ โโ */
|
| 459 |
.analysis-result {
|
| 460 |
margin-top: 30px;
|
| 461 |
+
border: 1px solid var(--border-color) !important;
|
| 462 |
border-radius: 5px;
|
| 463 |
padding: 15px;
|
| 464 |
+
background-color: var(--card-bg) !important;
|
| 465 |
+
color: var(--text-color) !important;
|
| 466 |
}
|
| 467 |
|
| 468 |
.result-header {
|
|
|
|
| 473 |
}
|
| 474 |
|
| 475 |
.summary-box {
|
| 476 |
+
background-color: var(--table-even-bg) !important;
|
| 477 |
border-left: 4px solid var(--primary-color);
|
| 478 |
padding: 10px 15px;
|
| 479 |
margin-bottom: 20px;
|
| 480 |
font-size: 14px;
|
| 481 |
+
color: var(--text-color) !important;
|
| 482 |
}
|
| 483 |
|
| 484 |
.summary-title {
|
| 485 |
font-weight: bold;
|
| 486 |
margin-bottom: 5px;
|
| 487 |
+
color: var(--text-color) !important;
|
| 488 |
}
|
| 489 |
|
| 490 |
.recommendation-box {
|
| 491 |
+
background-color: var(--card-bg) !important;
|
| 492 |
border-radius: 5px;
|
| 493 |
padding: 15px;
|
| 494 |
margin-bottom: 25px;
|
| 495 |
+
box-shadow: var(--shadow-light) !important;
|
| 496 |
+
border: 1px solid var(--border-color) !important;
|
| 497 |
+
color: var(--text-color) !important;
|
| 498 |
}
|
| 499 |
|
| 500 |
.recommendation-title {
|
|
|
|
| 506 |
|
| 507 |
.recommendation-item {
|
| 508 |
padding: 6px 0;
|
| 509 |
+
border-bottom: 1px solid var(--border-color) !important;
|
| 510 |
+
color: var(--text-color) !important;
|
| 511 |
}
|
| 512 |
|
| 513 |
.recommendation-item:last-child {
|
| 514 |
border-bottom: none;
|
| 515 |
}
|
| 516 |
|
| 517 |
+
/* ํค์๋ ํ๊ทธ ์คํ์ผ */
|
| 518 |
.keyword-tag-container {
|
| 519 |
margin-top: 20px;
|
| 520 |
padding: 10px;
|
| 521 |
+
border: 1px solid var(--border-color) !important;
|
| 522 |
border-radius: 5px;
|
| 523 |
+
background-color: var(--card-bg) !important;
|
| 524 |
+
color: var(--text-color) !important;
|
| 525 |
}
|
| 526 |
|
| 527 |
.keyword-tag {
|
|
|
|
| 565 |
animation: fadeIn 0.5s ease-out;
|
| 566 |
}
|
| 567 |
|
| 568 |
+
/* Gradio ์ปดํฌ๋ํธ ๊ฐ์ ์ ์ฉ */
|
| 569 |
+
.gr-form,
|
| 570 |
+
.gr-box,
|
| 571 |
+
.gr-panel,
|
| 572 |
+
.gr-block,
|
| 573 |
+
.gr-group,
|
| 574 |
+
.gr-row,
|
| 575 |
+
.gr-column {
|
| 576 |
+
background-color: var(--card-bg) !important;
|
| 577 |
+
color: var(--text-color) !important;
|
| 578 |
+
border-color: var(--border-color) !important;
|
| 579 |
+
}
|
| 580 |
+
|
| 581 |
+
/* ๋ผ๋ฒจ ๋ฐ ํ
์คํธ ์์ */
|
| 582 |
+
label,
|
| 583 |
+
.gr-label,
|
| 584 |
+
.gr-checkbox label,
|
| 585 |
+
.gr-radio label,
|
| 586 |
+
p, span, div {
|
| 587 |
+
color: var(--text-color) !important;
|
| 588 |
+
}
|
| 589 |
+
|
| 590 |
+
/* ํดํ ๋ฐ ํ์
*/
|
| 591 |
+
[data-tooltip]:hover::after,
|
| 592 |
+
.tooltip,
|
| 593 |
+
.popup {
|
| 594 |
+
background-color: var(--card-bg) !important;
|
| 595 |
+
color: var(--text-color) !important;
|
| 596 |
+
border-color: var(--border-color) !important;
|
| 597 |
+
box-shadow: var(--shadow-light) !important;
|
| 598 |
+
}
|
| 599 |
+
|
| 600 |
+
/* ๋ชจ๋ฌ ๋ฐ ์ค๋ฒ๋ ์ด */
|
| 601 |
+
.modal,
|
| 602 |
+
.overlay,
|
| 603 |
+
[class*="modal"],
|
| 604 |
+
[class*="overlay"] {
|
| 605 |
+
background-color: var(--card-bg) !important;
|
| 606 |
+
color: var(--text-color) !important;
|
| 607 |
+
border-color: var(--border-color) !important;
|
| 608 |
+
}
|
| 609 |
+
|
| 610 |
+
/* ์ฝ๋ ๋ธ๋ก ๋ฐ pre ํ๊ทธ */
|
| 611 |
+
code,
|
| 612 |
+
pre,
|
| 613 |
+
.code-block {
|
| 614 |
+
background-color: var(--table-even-bg) !important;
|
| 615 |
+
color: var(--text-color) !important;
|
| 616 |
+
border-color: var(--border-color) !important;
|
| 617 |
+
}
|
| 618 |
+
|
| 619 |
+
/* ์๋ฆผ ๋ฐ ๋ฉ์์ง */
|
| 620 |
+
.alert,
|
| 621 |
+
.message,
|
| 622 |
+
.notification,
|
| 623 |
+
[class*="alert"],
|
| 624 |
+
[class*="message"],
|
| 625 |
+
[class*="notification"] {
|
| 626 |
+
background-color: var(--card-bg) !important;
|
| 627 |
+
color: var(--text-color) !important;
|
| 628 |
+
border-color: var(--border-color) !important;
|
| 629 |
+
}
|
| 630 |
+
|
| 631 |
+
/* ์ ํ ์ ๋๋ฉ์ด์
*/
|
| 632 |
+
* {
|
| 633 |
+
transition: background-color 0.3s ease,
|
| 634 |
+
color 0.3s ease,
|
| 635 |
+
border-color 0.3s ease !important;
|
| 636 |
+
}
|
| 637 |
+
|
| 638 |
/* ๋ฐ์ํ ์กฐ์ */
|
| 639 |
@media (max-width: 768px) {
|
| 640 |
.grid-container {
|
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_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)
|