|
|
""" |
|
|
์นดํ
๊ณ ๋ฆฌ ๋ถ์ ๋ชจ๋ - ์ํ์ ์นดํ
๊ณ ๋ฆฌ ๋ถ์ ๊ธฐ๋ฅ ์ ๊ณต (๊ฐ์ ๋ฒ์ ) |
|
|
- 1๋
/3๋
ํธ๋ ๋ ๋ชจ๋ ๋ถ์ |
|
|
- ๋๋น 100% ์ ์ฉ |
|
|
- 3๋
๊ธฐ์ค ์ฑ์ฅ๋ฅ ๊ณ์ฐ |
|
|
""" |
|
|
|
|
|
import pandas as pd |
|
|
import time |
|
|
import re |
|
|
import random |
|
|
from collections import Counter, defaultdict |
|
|
import text_utils |
|
|
import product_search |
|
|
import keyword_search |
|
|
import logging |
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__) |
|
|
logger.setLevel(logging.INFO) |
|
|
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') |
|
|
handler = logging.StreamHandler() |
|
|
handler.setFormatter(formatter) |
|
|
logger.addHandler(handler) |
|
|
|
|
|
|
|
|
_last_keyword_results = [] |
|
|
|
|
|
def get_last_keyword_results(): |
|
|
"""๋ง์ง๋ง์ผ๋ก ๋ถ์๋ ํค์๋ ๊ฒฐ๊ณผ ๋ฐํ""" |
|
|
global _last_keyword_results |
|
|
return _last_keyword_results |
|
|
|
|
|
def exponential_backoff_sleep(retry_count, base_delay=0.3, max_delay=5.0): |
|
|
"""์ง์ ๋ฐฑ์คํ ๋ฐฉ์์ ๋๊ธฐ ์๊ฐ ๊ณ์ฐ""" |
|
|
delay = min(base_delay * (2 ** retry_count), max_delay) |
|
|
|
|
|
jitter = random.uniform(0, 0.5) * delay |
|
|
time.sleep(delay + jitter) |
|
|
|
|
|
def analyze_product_categories(main_keyword, product_name, category_filter=None): |
|
|
""" |
|
|
๋ฉ์ธ ํค์๋์ ์ํ๋ช
์ผ๋ก ์นดํ
๊ณ ๋ฆฌ ๋ถ์์ ์ํ |
|
|
|
|
|
Args: |
|
|
main_keyword (str): ๋ฉ์ธ ๊ฒ์ ํค์๋ |
|
|
product_name (str): ๋ถ์ํ ์ํ๋ช
|
|
|
category_filter (str, optional): ์นดํ
๊ณ ๋ฆฌ ํํฐ |
|
|
|
|
|
Returns: |
|
|
dict: ๋ถ์ ๊ฒฐ๊ณผ |
|
|
""" |
|
|
logger.info(f"์นดํ
๊ณ ๋ฆฌ ๋ถ์ ์์: ๋ฉ์ธ ํค์๋={main_keyword}, ์ํ๋ช
={product_name}") |
|
|
|
|
|
|
|
|
all_products = [] |
|
|
for page in range(1, 11): |
|
|
result = product_search.fetch_products_by_keyword(main_keyword, page=page, display=10) |
|
|
if result["products"]: |
|
|
all_products.extend(result["products"]) |
|
|
exponential_backoff_sleep(0) |
|
|
|
|
|
if not all_products: |
|
|
return { |
|
|
"status": "error", |
|
|
"message": "์ํ์ ๊ฐ์ ธ์ค์ง ๋ชปํ์ต๋๋ค.", |
|
|
"main_keyword": main_keyword, |
|
|
"product_name": product_name, |
|
|
"total_count": 0, |
|
|
"products": [], |
|
|
"categories": [], |
|
|
"analysis": None |
|
|
} |
|
|
|
|
|
|
|
|
product_keywords = [] |
|
|
|
|
|
|
|
|
words = re.split(r'[,\s]+', product_name) |
|
|
for word in words: |
|
|
word = word.strip() |
|
|
if word and len(word) >= 2: |
|
|
|
|
|
if word not in product_keywords: |
|
|
product_keywords.append(word) |
|
|
|
|
|
logger.info(f"์ํ๋ช
์์ ์ถ์ถํ ํค์๋: {product_keywords}") |
|
|
|
|
|
|
|
|
category_counter = Counter() |
|
|
products_by_category = defaultdict(list) |
|
|
|
|
|
for product in all_products: |
|
|
category = product["์นดํ
๊ณ ๋ฆฌ"] |
|
|
category_counter[category] += 1 |
|
|
products_by_category[category].append(product) |
|
|
|
|
|
|
|
|
if category_filter and category_filter != "์ ์ฒด ๋ณด๊ธฐ": |
|
|
|
|
|
category_filter_clean = category_filter.split(" (")[0] if " (" in category_filter else category_filter |
|
|
|
|
|
filtered_categories = {} |
|
|
for cat, count in category_counter.items(): |
|
|
|
|
|
cat_clean = cat |
|
|
if " (" in cat_clean: |
|
|
cat_clean = cat_clean.split(" (")[0] |
|
|
|
|
|
if category_filter_clean.lower() in cat_clean.lower(): |
|
|
filtered_categories[cat] = count |
|
|
|
|
|
category_counter = Counter(filtered_categories) |
|
|
|
|
|
|
|
|
all_keywords = [main_keyword] + product_keywords |
|
|
search_volumes = keyword_search.fetch_all_search_volumes(all_keywords) |
|
|
|
|
|
|
|
|
category_matching = [] |
|
|
|
|
|
|
|
|
sorted_categories = [cat for cat, _ in category_counter.most_common()] |
|
|
|
|
|
for category in sorted_categories: |
|
|
products_in_category = products_by_category[category] |
|
|
count = len(products_in_category) |
|
|
|
|
|
|
|
|
sample_products = products_in_category[:100] |
|
|
|
|
|
category_matching.append({ |
|
|
"์นดํ
๊ณ ๋ฆฌ": category, |
|
|
"์ํ์": count, |
|
|
"๋งค์นญ์ํ": sample_products |
|
|
}) |
|
|
|
|
|
|
|
|
keyword_info = [] |
|
|
for kw in all_keywords: |
|
|
volume = search_volumes.get(kw, {"PC๊ฒ์๋": 0, "๋ชจ๋ฐ์ผ๊ฒ์๋": 0, "์ด๊ฒ์๋": 0}) |
|
|
keyword_info.append({ |
|
|
"ํค์๋": kw, |
|
|
"PC๊ฒ์๋": volume.get("PC๊ฒ์๋", 0), |
|
|
"๋ชจ๋ฐ์ผ๊ฒ์๋": volume.get("๋ชจ๋ฐ์ผ๊ฒ์๋", 0), |
|
|
"์ด๊ฒ์๋": volume.get("์ด๊ฒ์๋", 0), |
|
|
"๊ฒ์๋๊ตฌ๊ฐ": text_utils.get_search_volume_range(volume.get("์ด๊ฒ์๋", 0)) |
|
|
}) |
|
|
|
|
|
|
|
|
return { |
|
|
"status": "success", |
|
|
"message": "๋ถ์์ด ์๋ฃ๋์์ต๋๋ค.", |
|
|
"main_keyword": main_keyword, |
|
|
"product_name": product_name, |
|
|
"total_count": len(all_products), |
|
|
"products": all_products, |
|
|
"categories": sorted_categories, |
|
|
"category_counter": dict(category_counter), |
|
|
"category_matching": category_matching, |
|
|
"keyword_info": keyword_info |
|
|
} |
|
|
|
|
|
def analyze_keywords_by_category(keywords, selected_category, df_all=None): |
|
|
""" |
|
|
์
๋ ฅ๋ ํค์๋ ๋ชฉ๋ก๊ณผ ์ ํ๋ ์นดํ
๊ณ ๋ฆฌ๋ก ๋ถ์์ ์ํํ๋ ํจ์ |
|
|
""" |
|
|
import re |
|
|
|
|
|
if not keywords or not selected_category: |
|
|
return "ํค์๋์ ์นดํ
๊ณ ๋ฆฌ๋ฅผ ๋ชจ๋ ์ ํํด์ฃผ์ธ์." |
|
|
|
|
|
|
|
|
selected_category_clean = selected_category |
|
|
is_overall_view = False |
|
|
if " (" in selected_category and selected_category != "์ ์ฒด ๋ณด๊ธฐ": |
|
|
selected_category_clean = selected_category.split(" (")[0] |
|
|
elif selected_category == "์ ์ฒด ๋ณด๊ธฐ": |
|
|
selected_category_clean = "" |
|
|
is_overall_view = True |
|
|
|
|
|
|
|
|
if isinstance(keywords, str): |
|
|
|
|
|
keywords_list = [k.strip() for k in re.split(r'[,\n]+', keywords) if k.strip()] |
|
|
|
|
|
keywords_list = keywords_list[:20] |
|
|
else: |
|
|
keywords_list = keywords[:20] |
|
|
|
|
|
if not keywords_list: |
|
|
return "๋ถ์ํ ํค์๋๊ฐ ์์ต๋๋ค." |
|
|
|
|
|
logger.info(f"์นดํ
๊ณ ๋ฆฌ ๋ถ์ ์์: {len(keywords_list)}๊ฐ ํค์๋, ์ ํ ์นดํ
๊ณ ๋ฆฌ: '{selected_category_clean if not is_overall_view else '์ ์ฒด ๋ณด๊ธฐ'}'") |
|
|
|
|
|
|
|
|
result_html = f''' |
|
|
<style> |
|
|
.result-container {{ |
|
|
width: 100%; |
|
|
margin-top: 20px; |
|
|
padding: 15px; |
|
|
border-radius: 8px; |
|
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); |
|
|
background-color: #f9f9f9; |
|
|
}} |
|
|
|
|
|
.result-header {{ |
|
|
font-size: 18px; |
|
|
font-weight: bold; |
|
|
margin-bottom: 15px; |
|
|
color: #009879; |
|
|
border-bottom: 2px solid #009879; |
|
|
padding-bottom: 5px; |
|
|
}} |
|
|
|
|
|
.keyword-tags {{ |
|
|
display: flex; |
|
|
flex-wrap: wrap; |
|
|
gap: 10px; |
|
|
margin-bottom: 20px; |
|
|
width: 100%; |
|
|
}} |
|
|
|
|
|
.keyword-tag {{ |
|
|
display: inline-block; |
|
|
background-color: #009879; |
|
|
color: white; |
|
|
padding: 8px 15px; |
|
|
border-radius: 20px; |
|
|
font-size: 14px; |
|
|
box-shadow: 0 2px 5px rgba(0,0,0,0.1); |
|
|
transition: transform 0.2s; |
|
|
}} |
|
|
|
|
|
.keyword-tag:hover {{ |
|
|
transform: translateY(-2px); |
|
|
box-shadow: 0 4px 8px rgba(0,0,0,0.15); |
|
|
}} |
|
|
|
|
|
.category-container {{ |
|
|
width: 100%; |
|
|
margin-bottom: 20px; |
|
|
padding: 10px 15px; |
|
|
background-color: #f0f8ff; |
|
|
border-left: 4px solid #2c7fb8; |
|
|
border-radius: 4px; |
|
|
}} |
|
|
|
|
|
.category-title {{ |
|
|
font-weight: bold; |
|
|
margin-bottom: 5px; |
|
|
color: #2c7fb8; |
|
|
}} |
|
|
|
|
|
.category-path {{ |
|
|
font-size: 16px; |
|
|
color: #333; |
|
|
word-break: break-word; |
|
|
}} |
|
|
|
|
|
.analysis-result {{ |
|
|
width: 100%; |
|
|
margin-top: 30px; |
|
|
border: 1px solid #ddd; |
|
|
border-radius: 5px; |
|
|
padding: 15px; |
|
|
background-color: #ffffff; |
|
|
}} |
|
|
|
|
|
.result-header-analysis {{ |
|
|
font-weight: bold; |
|
|
margin-bottom: 15px; |
|
|
color: #009879; |
|
|
font-size: 16px; |
|
|
}} |
|
|
|
|
|
.match-item {{ |
|
|
width: 100%; |
|
|
margin: 12px 0; |
|
|
padding: 8px 12px; |
|
|
border-bottom: 1px solid #eee; |
|
|
transition: background-color 0.2s; |
|
|
display: grid; |
|
|
grid-template-columns: 3fr 1fr 4fr; |
|
|
gap: 10px; |
|
|
}} |
|
|
|
|
|
.match-item:hover {{ |
|
|
background-color: #f5f5f5; |
|
|
}} |
|
|
|
|
|
.match-keyword {{ |
|
|
font-weight: bold; |
|
|
color: #2c7fb8; |
|
|
font-size: 15px; |
|
|
}} |
|
|
|
|
|
.match-count {{ |
|
|
display: inline-block; |
|
|
background-color: #009879; |
|
|
color: white; |
|
|
padding: 3px 10px; |
|
|
border-radius: 15px; |
|
|
font-size: 13px; |
|
|
margin-left: 10px; |
|
|
}} |
|
|
|
|
|
.match-status {{ |
|
|
text-align: center; |
|
|
font-weight: bold; |
|
|
color: #009879; |
|
|
}} |
|
|
|
|
|
.match-categories {{ |
|
|
color: #555; |
|
|
font-size: 14px; |
|
|
line-height: 1.4; |
|
|
}} |
|
|
|
|
|
.match-header {{ |
|
|
width: 100%; |
|
|
display: grid; |
|
|
grid-template-columns: 3fr 1fr 4fr; |
|
|
gap: 10px; |
|
|
padding: 10px 12px; |
|
|
background-color: #e7f7f3; |
|
|
border-radius: 5px 5px 0 0; |
|
|
font-weight: bold; |
|
|
margin-bottom: 10px; |
|
|
}} |
|
|
</style> |
|
|
|
|
|
<div class="result-container"> |
|
|
<div class="result-header">๋ถ์ ๊ฒฐ๊ณผ ์์ฝ</div> |
|
|
|
|
|
<div class="keyword-tags"> |
|
|
<div class="category-title">๋ถ์ ํค์๋ ({len(keywords_list)}๊ฐ)</div> |
|
|
<div class="keyword-tags"> |
|
|
{''.join([f'<span class="keyword-tag">{k}</span>' for k in keywords_list])} |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="category-container"> |
|
|
<div class="category-title">์ ํ๋ ์นดํ
๊ณ ๋ฆฌ</div> |
|
|
<div class="category-path">{selected_category}</div> |
|
|
</div> |
|
|
|
|
|
<div class="analysis-result"> |
|
|
<div class="result-header-analysis">์นดํ
๊ณ ๋ฆฌ ์ผ์น ๋ถ์ ๊ฒฐ๊ณผ</div> |
|
|
|
|
|
<div class="match-header"> |
|
|
<div>ํค์๋</div> |
|
|
<div>๋ถ์ ํค์๋</div> |
|
|
<div>๋งค์นญ๋ ์นดํ
๊ณ ๋ฆฌ</div> |
|
|
</div> |
|
|
''' |
|
|
|
|
|
|
|
|
batch_size = 5 |
|
|
batches = [] |
|
|
for i in range(0, len(keywords_list), batch_size): |
|
|
batches.append(keywords_list[i:i + batch_size]) |
|
|
|
|
|
logger.info(f"์ด {len(batches)}๊ฐ ๋ฐฐ์น๋ก {len(keywords_list)}๊ฐ ํค์๋ ์ฒ๋ฆฌ") |
|
|
|
|
|
|
|
|
match_results = {} |
|
|
batch_categories_info = {} |
|
|
|
|
|
for batch_idx, batch in enumerate(batches): |
|
|
logger.info(f"๋ฐฐ์น {batch_idx+1}/{len(batches)} ์ฒ๋ฆฌ ์ค...") |
|
|
|
|
|
|
|
|
for keyword in batch: |
|
|
max_retries = 3 |
|
|
retry_count = 0 |
|
|
api_keyword = keyword.replace(" ", "") |
|
|
|
|
|
current_keyword_match_count = 0 |
|
|
current_keyword_total_products = 0 |
|
|
current_keyword_categories_found = [] |
|
|
|
|
|
while retry_count < max_retries: |
|
|
try: |
|
|
|
|
|
products = product_search.fetch_naver_shopping_data_for_analysis(api_keyword, count=100) |
|
|
|
|
|
if products: |
|
|
current_keyword_total_products = len(products) |
|
|
categories_counter_for_keyword = Counter() |
|
|
|
|
|
for product in products: |
|
|
product_category_full = product.get("category", "") or product.get("์นดํ
๊ณ ๋ฆฌ", "") |
|
|
if product_category_full: |
|
|
categories_counter_for_keyword[product_category_full] += 1 |
|
|
|
|
|
|
|
|
if not is_overall_view and selected_category_clean: |
|
|
product_category_for_match = product_category_full |
|
|
if " (" in product_category_for_match: |
|
|
product_category_for_match = product_category_for_match.split(" (")[0] |
|
|
|
|
|
sel_lower = selected_category_clean.lower() |
|
|
prod_lower = product_category_for_match.lower() |
|
|
|
|
|
if sel_lower in prod_lower or prod_lower in sel_lower: |
|
|
current_keyword_match_count += 1 |
|
|
|
|
|
if is_overall_view: |
|
|
current_keyword_match_count = current_keyword_total_products |
|
|
|
|
|
|
|
|
category_percentages = [] |
|
|
for cat, count in categories_counter_for_keyword.most_common(): |
|
|
percentage = (count / current_keyword_total_products) * 100 if current_keyword_total_products > 0 else 0 |
|
|
category_percentages.append((cat, percentage)) |
|
|
|
|
|
|
|
|
|
|
|
for cat, percentage in category_percentages: |
|
|
current_keyword_categories_found.append(f"{cat} ({percentage:.0f}%)") |
|
|
|
|
|
logger.info(f" - '{keyword}' ์ฒ๋ฆฌ ์๋ฃ: {current_keyword_match_count}/{current_keyword_total_products} ์ผ์น") |
|
|
break |
|
|
else: |
|
|
logger.warning(f" - '{keyword}' API ๊ฒฐ๊ณผ ์์ (์๋ {retry_count+1}/{max_retries})") |
|
|
retry_count += 1 |
|
|
exponential_backoff_sleep(retry_count) |
|
|
|
|
|
except Exception as e: |
|
|
logger.error(f" - '{keyword}' ์ฒ๋ฆฌ ์ค ์ค๋ฅ: {e} (์๋ {retry_count+1}/{max_retries})") |
|
|
retry_count += 1 |
|
|
exponential_backoff_sleep(retry_count) |
|
|
|
|
|
|
|
|
if retry_count >= max_retries and current_keyword_total_products == 0: |
|
|
match_results[keyword] = { |
|
|
"match_count": 0, |
|
|
"total_count": 0, |
|
|
"error": True |
|
|
} |
|
|
batch_categories_info[keyword] = ["์ค๋ฅ ๋ฐ์"] |
|
|
logger.error(f" - '{keyword}' ์ต๋ ์ฌ์๋ ํ ์คํจ") |
|
|
else: |
|
|
match_results[keyword] = { |
|
|
"match_count": current_keyword_match_count, |
|
|
"total_count": current_keyword_total_products, |
|
|
"error": False |
|
|
} |
|
|
batch_categories_info[keyword] = current_keyword_categories_found if current_keyword_categories_found else ["์นดํ
๊ณ ๋ฆฌ ์ ๋ณด ์์"] |
|
|
|
|
|
|
|
|
exponential_backoff_sleep(0) |
|
|
|
|
|
logger.info(f"์ ์ฒด {len(keywords_list)}๊ฐ ํค์๋ ์ค {len(match_results)}๊ฐ ์ฒ๋ฆฌ ์๋ฃ") |
|
|
|
|
|
|
|
|
for keyword in keywords_list: |
|
|
result = match_results.get(keyword, {"match_count": 0, "total_count": 0, "error": True}) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if result.get("error"): |
|
|
keyword_status = "์ค๋ฅ" |
|
|
status_color = "red" |
|
|
elif is_overall_view: |
|
|
keyword_status = "O" |
|
|
status_color = "#009879" |
|
|
else: |
|
|
keyword_status = "O" if result["match_count"] > 0 else "X" |
|
|
status_color = "#009879" if keyword_status == "O" else "red" |
|
|
|
|
|
|
|
|
|
|
|
categories_html_list = batch_categories_info.get(keyword, ["์ ๋ณด ์์"]) |
|
|
categories_html = "<br>".join(categories_html_list) |
|
|
|
|
|
if result.get("error", False): |
|
|
result_html += f''' |
|
|
<div class="match-item"> |
|
|
<div class="match-keyword">{keyword}</div> |
|
|
<div class="match-status" style="color:{status_color}; font-weight:bold;">{keyword_status}</div> |
|
|
<div class="match-categories">๋ถ์ ์ค ์ค๋ฅ ๋ฐ์</div> |
|
|
</div> |
|
|
''' |
|
|
else: |
|
|
match_count_display = result["match_count"] |
|
|
total_count_display = result["total_count"] |
|
|
|
|
|
result_html += f''' |
|
|
<div class="match-item"> |
|
|
<div class="match-keyword">{keyword}<span class="match-count">{match_count_display}/{total_count_display}</span></div> |
|
|
<div class="match-status" style="color:{status_color}; font-weight:bold;">{keyword_status}</div> |
|
|
<div class="match-categories">{categories_html}</div> |
|
|
</div> |
|
|
''' |
|
|
|
|
|
result_html += '</div></div></div>' |
|
|
|
|
|
return result_html |
|
|
|
|
|
def analyze_product_terms(product_name, main_keyword=""): |
|
|
""" |
|
|
์ํ๋ช
์์ ์ถ์ถํ ํค์๋๋ค์ ๋ถ์ํ์ฌ ์นดํ
๊ณ ๋ฆฌ ํญ๋ชฉ ์ ๊ณต (1๋
, 3๋
ํธ๋ ๋ ๋ชจ๋ ๋ถ์) |
|
|
|
|
|
Args: |
|
|
product_name (str): ๋ถ์ํ ์ํ๋ช
|
|
|
main_keyword (str): ๋ฉ์ธ ํค์๋ (optional) |
|
|
|
|
|
Returns: |
|
|
tuple: (HTML ํ์์ ๊ฒฐ๊ณผ ํ
์ด๋ธ, ํค์๋ ๋ถ์ ๊ฒฐ๊ณผ ๋ฆฌ์คํธ, ํธ๋ ๋ ๋ถ์ ๊ฒฐ๊ณผ) |
|
|
""" |
|
|
global _last_keyword_results |
|
|
|
|
|
|
|
|
product_name = product_name.strip() if product_name else "" |
|
|
if not product_name: |
|
|
return "์ํ๋ช
์ด ๋น์ด์์ต๋๋ค. ์ ํจํ ์ํ๋ช
์ ์
๋ ฅํด์ฃผ์ธ์.", [], None |
|
|
|
|
|
|
|
|
logger.info(f"๋ถ์ ์์ - ์ํ๋ช
: '{product_name}', ๋ฉ์ธ ํค์๋: '{main_keyword}'") |
|
|
|
|
|
|
|
|
|
|
|
try: |
|
|
words = [] |
|
|
|
|
|
comma_parts = product_name.split(',') |
|
|
|
|
|
for part in comma_parts: |
|
|
|
|
|
space_parts = part.split() |
|
|
words.extend([word.strip() for word in space_parts if word.strip()]) |
|
|
|
|
|
|
|
|
words = list(set([word for word in words if len(word) >= 1])) |
|
|
|
|
|
logger.info(f"์ํ๋ช
์์ ์ถ์ถํ ์๋ณธ ํค์๋ (์ด {len(words)}๊ฐ): {words}") |
|
|
|
|
|
|
|
|
if not words: |
|
|
words = [product_name] |
|
|
logger.warning(f"ํค์๋ ์ถ์ถ ์คํจ, ์๋ณธ ์ํ๋ช
์ฌ์ฉ: '{product_name}'") |
|
|
except Exception as e: |
|
|
logger.error(f"ํค์๋ ์ถ์ถ ์ค ์ค๋ฅ ๋ฐ์: {e}") |
|
|
|
|
|
words = [product_name] |
|
|
|
|
|
|
|
|
if not main_keyword: |
|
|
|
|
|
for word in words: |
|
|
if "์ค์ง์ด" in word: |
|
|
main_keyword = "์ค์ง์ด" |
|
|
break |
|
|
|
|
|
|
|
|
keywords = [] |
|
|
for word in words: |
|
|
|
|
|
if word and word != main_keyword: |
|
|
|
|
|
if main_keyword and main_keyword not in word: |
|
|
|
|
|
combined = f"{word} {main_keyword}" |
|
|
if combined not in keywords: |
|
|
keywords.append(combined) |
|
|
|
|
|
if word not in keywords: |
|
|
keywords.append(word) |
|
|
|
|
|
|
|
|
if main_keyword and main_keyword not in keywords: |
|
|
keywords.append(main_keyword) |
|
|
|
|
|
if not keywords: |
|
|
return "์ํ๋ช
์์ ํค์๋๋ฅผ ์ถ์ถํ ์ ์์ต๋๋ค.", [], None |
|
|
|
|
|
logger.info(f"๋ถ์ํ ์ต์ข
ํค์๋ ๋ชฉ๋ก (์ด {len(keywords)}๊ฐ): {keywords}") |
|
|
|
|
|
|
|
|
batch_size = 5 |
|
|
keyword_batches = [] |
|
|
for i in range(0, len(keywords), batch_size): |
|
|
keyword_batches.append(keywords[i:i + batch_size]) |
|
|
|
|
|
logger.info(f"์ด {len(keyword_batches)}๊ฐ ๋ฐฐ์น๋ก {len(keywords)}๊ฐ ํค์๋ ์ฒ๋ฆฌ") |
|
|
|
|
|
|
|
|
keyword_results = [] |
|
|
|
|
|
|
|
|
for batch_idx, batch in enumerate(keyword_batches): |
|
|
logger.info(f"๋ฐฐ์น {batch_idx+1}/{len(keyword_batches)} ์ฒ๋ฆฌ ์ค...") |
|
|
|
|
|
|
|
|
batch_products = {} |
|
|
for keyword in batch: |
|
|
|
|
|
api_keyword = keyword.replace(" ", "") |
|
|
|
|
|
|
|
|
max_retries = 3 |
|
|
retry_count = 0 |
|
|
|
|
|
while retry_count < max_retries: |
|
|
try: |
|
|
|
|
|
products = product_search.fetch_naver_shopping_data_for_analysis(api_keyword, count=100) |
|
|
|
|
|
if products: |
|
|
batch_products[keyword] = products |
|
|
logger.info(f" - '{keyword}' ์ํ ๊ฒ์ ์ฑ๊ณต: {len(products)}๊ฐ") |
|
|
break |
|
|
else: |
|
|
logger.warning(f" - '{keyword}' ์ํ ์์ (์๋ {retry_count+1}/{max_retries})") |
|
|
retry_count += 1 |
|
|
exponential_backoff_sleep(retry_count) |
|
|
except Exception as e: |
|
|
logger.error(f" - '{keyword}' ์ํ ๊ฒ์ ์ค ์ค๋ฅ: {e} (์๋ {retry_count+1}/{max_retries})") |
|
|
retry_count += 1 |
|
|
exponential_backoff_sleep(retry_count) |
|
|
|
|
|
|
|
|
if retry_count >= max_retries and keyword not in batch_products: |
|
|
logger.error(f" - '{keyword}' ์ต๋ ์ฌ์๋ ํ ์คํจ") |
|
|
|
|
|
|
|
|
api_keywords = [kw.replace(" ", "") for kw in batch] |
|
|
volumes = keyword_search.fetch_all_search_volumes(api_keywords) |
|
|
|
|
|
|
|
|
for keyword in batch: |
|
|
if keyword in batch_products: |
|
|
products = batch_products[keyword] |
|
|
|
|
|
|
|
|
category_counter = Counter() |
|
|
for product in products: |
|
|
category = product.get("category", "") or product.get("์นดํ
๊ณ ๋ฆฌ", "") |
|
|
if category: |
|
|
category_counter[category] += 1 |
|
|
|
|
|
|
|
|
total_products = len(products) |
|
|
categories_with_percentage = [] |
|
|
for category, count in category_counter.most_common(): |
|
|
percentage = (count / total_products) * 100 if total_products > 0 else 0 |
|
|
categories_with_percentage.append(f"{category}({percentage:.0f}%)") |
|
|
|
|
|
|
|
|
api_keyword = keyword.replace(" ", "") |
|
|
volume_data = volumes.get(api_keyword, {"PC๊ฒ์๋": 0, "๋ชจ๋ฐ์ผ๊ฒ์๋": 0, "์ด๊ฒ์๋": 0}) |
|
|
|
|
|
|
|
|
keyword_results.append({ |
|
|
"ํค์๋": keyword, |
|
|
"PC๊ฒ์๋": volume_data.get("PC๊ฒ์๋", 0), |
|
|
"๋ชจ๋ฐ์ผ๊ฒ์๋": volume_data.get("๋ชจ๋ฐ์ผ๊ฒ์๋", 0), |
|
|
"์ด๊ฒ์๋": volume_data.get("์ด๊ฒ์๋", 0), |
|
|
"๊ฒ์๋๊ตฌ๊ฐ": text_utils.get_search_volume_range(volume_data.get("์ด๊ฒ์๋", 0)), |
|
|
"์นดํ
๊ณ ๋ฆฌํญ๋ชฉ": "\n".join(categories_with_percentage) if categories_with_percentage else "-", |
|
|
"์นดํ
๊ณ ๋ฆฌ์ ๋ณด": dict(category_counter) |
|
|
}) |
|
|
|
|
|
logger.info(f" - '{keyword}' ๋ถ์ ์๋ฃ: ์นดํ
๊ณ ๋ฆฌ ํญ๋ชฉ {len(category_counter)}๊ฐ, ๊ฒ์๋ {volume_data.get('์ด๊ฒ์๋', 0)}") |
|
|
|
|
|
|
|
|
logger.info(f"ํค์๋ ๋ถ์ ์๋ฃ: ์ด {len(keywords)}๊ฐ ์ค {len(keyword_results)}๊ฐ ์ฑ๊ณต") |
|
|
|
|
|
|
|
|
keyword_results = sorted(keyword_results, key=lambda x: x["์ด๊ฒ์๋"], reverse=True) |
|
|
|
|
|
|
|
|
recommended_categories = Counter() |
|
|
for result in keyword_results: |
|
|
for category, count in result.get("์นดํ
๊ณ ๋ฆฌ์ ๋ณด", {}).items(): |
|
|
recommended_categories[category] += count |
|
|
|
|
|
|
|
|
top_categories = recommended_categories.most_common(3) |
|
|
|
|
|
|
|
|
total_products_count = sum(recommended_categories.values()) |
|
|
|
|
|
|
|
|
top_categories_with_percentage = [] |
|
|
for category, count in top_categories: |
|
|
percentage = (count / total_products_count) * 100 if total_products_count > 0 else 0 |
|
|
top_categories_with_percentage.append({ |
|
|
"์นดํ
๊ณ ๋ฆฌ": category, |
|
|
"๊ฐ์": count, |
|
|
"์ ์ ์จ": f"{percentage:.0f}%" |
|
|
}) |
|
|
|
|
|
|
|
|
trend_results = {"1year": None, "3year": None} |
|
|
trend_html = "" |
|
|
|
|
|
if keyword_results: |
|
|
try: |
|
|
|
|
|
import trend_analysis |
|
|
|
|
|
|
|
|
top_keywords = [result["ํค์๋"] for result in keyword_results[:5]] |
|
|
logger.info(f"1๋
, 3๋
ํธ๋ ๋ ๋ถ์ ์์: {top_keywords}") |
|
|
|
|
|
|
|
|
trend_result_1year = trend_analysis.get_trend_data(top_keywords, "1year") |
|
|
if trend_result_1year["status"] == "success": |
|
|
trend_results["1year"] = trend_result_1year |
|
|
|
|
|
|
|
|
trend_result_3year = trend_analysis.get_trend_data(top_keywords, "3year") |
|
|
if trend_result_3year["status"] == "success": |
|
|
trend_results["3year"] = trend_result_3year |
|
|
|
|
|
|
|
|
if trend_results["1year"] or trend_results["3year"]: |
|
|
trend_html = f''' |
|
|
<div class="trend-analysis-section" style="width: 100%; margin-top: 30px;"> |
|
|
<div class="section-title">๐ ๊ฒ์๋ ํธ๋ ๋ ๋ถ์</div> |
|
|
''' |
|
|
|
|
|
for period, result in trend_results.items(): |
|
|
if result and result["status"] == "success": |
|
|
period_text = "์ต๊ทผ 1๋
" if period == "1year" else "์ต๊ทผ 3๋
" |
|
|
|
|
|
|
|
|
insights = trend_analysis.analyze_trend_insights(result["trend_data"]) |
|
|
|
|
|
trend_html += f''' |
|
|
<div class="trend-period-section" style="width: 100%; background-color: #f0f8ff; padding: 15px; border-radius: 8px; margin-bottom: 20px;"> |
|
|
<div class="insights-title" style="font-weight: bold; margin-bottom: 15px; color: #2c7fb8;">๐ {period_text} ์ฃผ์ ์ธ์ฌ์ดํธ</div> |
|
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 15px; width: 100%;"> |
|
|
''' |
|
|
|
|
|
for keyword, insight in insights.items(): |
|
|
growth_icon = "๐" if insight['growth_rate'] > 0 else "๐" if insight['growth_rate'] < 0 else "๐" |
|
|
growth_color = "#28a745" if insight['growth_rate'] > 0 else "#dc3545" if insight['growth_rate'] < 0 else "#6c757d" |
|
|
|
|
|
trend_html += f''' |
|
|
<div class="insight-item" style="background: white; padding: 12px; border-radius: 6px; border-left: 4px solid {growth_color};"> |
|
|
<div style="font-weight: bold; color: #2c7fb8; margin-bottom: 8px;">{keyword} {growth_icon}</div> |
|
|
<div style="font-size: 13px; line-height: 1.4;"> |
|
|
<div>๐ ์ต๊ณ ์ : <strong>{insight['max_volume']:,}</strong> ({insight['max_period']})</div> |
|
|
<div>๐ ์ ์ฒด ํ๊ท : <strong>{insight['total_avg']:,}</strong></div> |
|
|
<div style="color: {growth_color};">๐ ์ฑ์ฅ๋ฅ : <strong>{insight['growth_rate']:+.1f}%</strong></div> |
|
|
</div> |
|
|
</div> |
|
|
''' |
|
|
|
|
|
trend_html += ''' |
|
|
</div> |
|
|
|
|
|
<div class="trend-graph-container" style="width: 100%; background: white; padding: 15px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin-top: 15px;"> |
|
|
''' |
|
|
trend_html += result["graph_html"] |
|
|
trend_html += ''' |
|
|
</div> |
|
|
</div> |
|
|
''' |
|
|
|
|
|
trend_html += ''' |
|
|
</div> |
|
|
''' |
|
|
|
|
|
logger.info(f"1๋
, 3๋
ํธ๋ ๋ ๋ถ์ ์๋ฃ") |
|
|
else: |
|
|
logger.warning(f"ํธ๋ ๋ ๋ถ์ ์คํจ") |
|
|
trend_html = ''' |
|
|
<div class="trend-analysis-section" style="width: 100%; margin-top: 30px;"> |
|
|
<div class="section-title">๐ ๊ฒ์๋ ํธ๋ ๋ ๋ถ์</div> |
|
|
<div style="width: 100%; background-color: #fff3cd; padding: 15px; border-radius: 8px; color: #856404;"> |
|
|
โ ๏ธ ํธ๋ ๋ ๋ถ์์ ์คํํ ์ ์์ต๋๋ค. ๋์ค์ ๋ค์ ์๋ํด์ฃผ์ธ์. |
|
|
</div> |
|
|
</div> |
|
|
''' |
|
|
except Exception as e: |
|
|
logger.error(f"ํธ๋ ๋ ๋ถ์ ์ค ์ค๋ฅ ๋ฐ์: {e}") |
|
|
trend_html = ''' |
|
|
<div class="trend-analysis-section" style="width: 100%; margin-top: 30px;"> |
|
|
<div class="section-title">๐ ๊ฒ์๋ ํธ๋ ๋ ๋ถ์</div> |
|
|
<div style="width: 100%; background-color: #f8d7da; padding: 15px; border-radius: 8px; color: #721c24;"> |
|
|
โ ํธ๋ ๋ ๋ถ์ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค. |
|
|
</div> |
|
|
</div> |
|
|
''' |
|
|
|
|
|
|
|
|
html = f''' |
|
|
<style> |
|
|
.product-analysis-table {{ |
|
|
width: 100%; |
|
|
border-collapse: collapse; |
|
|
margin: 25px 0; |
|
|
font-size: 14px; |
|
|
box-shadow: 0 0 20px rgba(0, 0, 0, 0.15); |
|
|
border-radius: 5px; |
|
|
overflow: hidden; |
|
|
}} |
|
|
|
|
|
.product-analysis-table thead tr {{ |
|
|
background-color: #009879; |
|
|
color: white; |
|
|
text-align: left; |
|
|
}} |
|
|
|
|
|
.product-analysis-table th, |
|
|
.product-analysis-table td {{ |
|
|
padding: 12px 15px; |
|
|
border-bottom: 1px solid #dddddd; |
|
|
}} |
|
|
|
|
|
.product-analysis-table tbody tr {{ |
|
|
background-color: white; |
|
|
}} |
|
|
|
|
|
.product-analysis-table tbody tr:nth-of-type(even) {{ |
|
|
background-color: #f3f3f3; |
|
|
}} |
|
|
|
|
|
.product-analysis-table tbody tr:hover {{ |
|
|
background-color: #f5f5f5; |
|
|
}} |
|
|
|
|
|
.product-analysis-table tbody tr:last-of-type {{ |
|
|
border-bottom: 2px solid #009879; |
|
|
}} |
|
|
|
|
|
.section-title {{ |
|
|
font-size: 18px; |
|
|
font-weight: bold; |
|
|
color: #009879; |
|
|
margin-bottom: 15px; |
|
|
padding-bottom: 5px; |
|
|
border-bottom: 2px solid #009879; |
|
|
}} |
|
|
|
|
|
.summary-box {{ |
|
|
width: 100%; |
|
|
background-color: #f5f5f5; |
|
|
border-left: 4px solid #009879; |
|
|
padding: 10px 15px; |
|
|
margin-bottom: 20px; |
|
|
font-size: 14px; |
|
|
}} |
|
|
|
|
|
.summary-title {{ |
|
|
font-weight: bold; |
|
|
margin-bottom: 5px; |
|
|
}} |
|
|
|
|
|
.recommendation-box {{ |
|
|
width: 100%; |
|
|
background-color: #e7f7f3; |
|
|
border-radius: 5px; |
|
|
padding: 15px; |
|
|
margin-bottom: 25px; |
|
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); |
|
|
}} |
|
|
|
|
|
.recommendation-title {{ |
|
|
font-weight: bold; |
|
|
font-size: 16px; |
|
|
color: #009879; |
|
|
margin-bottom: 10px; |
|
|
}} |
|
|
|
|
|
.recommendation-item {{ |
|
|
padding: 6px 0; |
|
|
border-bottom: 1px solid #e0e0e0; |
|
|
}} |
|
|
|
|
|
.recommendation-item:last-child {{ |
|
|
border-bottom: none; |
|
|
}} |
|
|
</style> |
|
|
|
|
|
<div style="width: 100%;"> |
|
|
<div class="section-title">์ํ๋ช
ํค์๋ ๋ถ์ ๊ฒฐ๊ณผ</div> |
|
|
|
|
|
<div class="summary-box"> |
|
|
<div class="summary-title">๋ถ์ ์์ฝ</div> |
|
|
<p>์ด <strong>{len(keyword_results)}</strong>๊ฐ ํค์๋ ๋ถ์</p> |
|
|
<p>๋ฉ์ธ ํค์๋: <strong>{main_keyword if main_keyword else '์์'}</strong></p> |
|
|
</div> |
|
|
|
|
|
<div class="recommendation-box"> |
|
|
<div class="recommendation-title">์ถ์ฒ ์นดํ
๊ณ ๋ฆฌ</div> |
|
|
''' |
|
|
|
|
|
|
|
|
for idx, cat_info in enumerate(top_categories_with_percentage, 1): |
|
|
html += f''' |
|
|
<div class="recommendation-item"> |
|
|
์ถ์ฒ ์นดํ
๊ณ ๋ฆฌ {idx} : {cat_info['์นดํ
๊ณ ๋ฆฌ']}({cat_info['์ ์ ์จ']}) |
|
|
</div> |
|
|
''' |
|
|
|
|
|
html += ''' |
|
|
</div> |
|
|
|
|
|
<table class="product-analysis-table"> |
|
|
<thead> |
|
|
<tr> |
|
|
<th>์๋ฒ</th> |
|
|
<th>ํค์๋</th> |
|
|
<th>PC๊ฒ์๋</th> |
|
|
<th>๋ชจ๋ฐ์ผ๊ฒ์๋</th> |
|
|
<th>์ด๊ฒ์๋</th> |
|
|
<th>๊ฒ์๋๊ตฌ๊ฐ</th> |
|
|
<th>์นดํ
๊ณ ๋ฆฌํญ๋ชฉ</th> |
|
|
</tr> |
|
|
</thead> |
|
|
<tbody> |
|
|
''' |
|
|
|
|
|
for idx, result in enumerate(keyword_results): |
|
|
|
|
|
category_items = result.get("์นดํ
๊ณ ๋ฆฌํญ๋ชฉ", "-").replace("\n", "<br>") |
|
|
|
|
|
html += f''' |
|
|
<tr> |
|
|
<td>{idx + 1}</td> |
|
|
<td>{result["ํค์๋"]}</td> |
|
|
<td>{result["PC๊ฒ์๋"]:,}</td> |
|
|
<td>{result["๋ชจ๋ฐ์ผ๊ฒ์๋"]:,}</td> |
|
|
<td>{result["์ด๊ฒ์๋"]:,}</td> |
|
|
<td>{result["๊ฒ์๋๊ตฌ๊ฐ"]}</td> |
|
|
<td>{category_items}</td> |
|
|
</tr> |
|
|
''' |
|
|
|
|
|
html += ''' |
|
|
</tbody> |
|
|
</table> |
|
|
</div> |
|
|
''' |
|
|
|
|
|
|
|
|
html += trend_html |
|
|
|
|
|
|
|
|
if not keyword_results: |
|
|
html += ''' |
|
|
<div style="width: 100%; margin-top: 20px; padding: 15px; background-color: #f1f1f1; border-radius: 5px; text-align: center;"> |
|
|
<p>ํ์ํ ํค์๋๊ฐ ์์ต๋๋ค. ๋ค๋ฅธ ์ํ๋ช
์ ์
๋ ฅํด๋ณด์ธ์.</p> |
|
|
</div> |
|
|
''' |
|
|
|
|
|
|
|
|
_last_keyword_results = keyword_results |
|
|
|
|
|
|
|
|
return html, keyword_results, trend_results |
|
|
|
|
|
def collect_categories_per_keyword(keywords, max_products=10): |
|
|
""" |
|
|
ํค์๋๋ง๋ค ์ํ n๊ฐ๋ฅผ ํธ์ถํ์ฌ ์นดํ
๊ณ ๋ฆฌ ์งํฉ ๋ฐํ |
|
|
|
|
|
Args: |
|
|
keywords (list): ํค์๋ ๋ชฉ๋ก |
|
|
max_products (int): ํค์๋๋น ๊ฒ์ํ ์ต๋ ์ํ ์ |
|
|
|
|
|
Returns: |
|
|
dict: ํค์๋๋ณ ์นดํ
๊ณ ๋ฆฌ ์งํฉ์ ๋ด์ ์ฌ์ {ํค์๋: {์นดํ
๊ณ ๋ฆฌ1, ์นดํ
๊ณ ๋ฆฌ2, ...}} |
|
|
""" |
|
|
logger.info(f"ํค์๋๋ณ ์นดํ
๊ณ ๋ฆฌ ์์ง ์์: {len(keywords)}๊ฐ ํค์๋") |
|
|
keyword_category_map = {} |
|
|
|
|
|
|
|
|
batch_size = 5 |
|
|
batches = [] |
|
|
for i in range(0, len(keywords), batch_size): |
|
|
batches.append(keywords[i:i + batch_size]) |
|
|
|
|
|
logger.info(f"์ด {len(batches)}๊ฐ ๋ฐฐ์น๋ก {len(keywords)}๊ฐ ํค์๋ ์ฒ๋ฆฌ") |
|
|
|
|
|
|
|
|
for batch_idx, batch in enumerate(batches): |
|
|
logger.info(f"๋ฐฐ์น {batch_idx+1}/{len(batches)} ์ฒ๋ฆฌ ์ค...") |
|
|
|
|
|
for keyword in batch: |
|
|
|
|
|
api_keyword = keyword.replace(" ", "") |
|
|
|
|
|
max_retries = 3 |
|
|
retry_count = 0 |
|
|
|
|
|
while retry_count < max_retries: |
|
|
try: |
|
|
|
|
|
products = product_search.fetch_naver_shopping_data_for_analysis(api_keyword, count=max_products) |
|
|
|
|
|
if products: |
|
|
|
|
|
categories = set() |
|
|
for product in products: |
|
|
|
|
|
category = product.get("category", "") or product.get("์นดํ
๊ณ ๋ฆฌ", "") |
|
|
if category: |
|
|
categories.add(category) |
|
|
|
|
|
keyword_category_map[keyword] = categories |
|
|
logger.info(f" - '{keyword}' ์นดํ
๊ณ ๋ฆฌ ์์ง ์๋ฃ: {len(categories)}๊ฐ") |
|
|
break |
|
|
else: |
|
|
logger.warning(f" - '{keyword}' ์ํ ๊ฒ์ ๊ฒฐ๊ณผ ์์ (์๋ {retry_count+1}/{max_retries})") |
|
|
retry_count += 1 |
|
|
exponential_backoff_sleep(retry_count) |
|
|
|
|
|
except Exception as e: |
|
|
logger.error(f" - '{keyword}' ์ฒ๋ฆฌ ์ค ์ค๋ฅ: {e} (์๋ {retry_count+1}/{max_retries})") |
|
|
retry_count += 1 |
|
|
exponential_backoff_sleep(retry_count) |
|
|
|
|
|
if retry_count >= max_retries: |
|
|
logger.error(f" - '{keyword}' ์ต๋ ์ฌ์๋ ํ ์คํจ") |
|
|
keyword_category_map[keyword] = set() |
|
|
|
|
|
|
|
|
exponential_backoff_sleep(0) |
|
|
|
|
|
logger.info(f"ํค์๋๋ณ ์นดํ
๊ณ ๋ฆฌ ์์ง ์๋ฃ: {len(keyword_category_map)}๊ฐ") |
|
|
return keyword_category_map |