3gghdf5 / category_analysis.py
ssboost's picture
Upload 15 files
106555b verified
"""
์นดํ…Œ๊ณ ๋ฆฌ ๋ถ„์„ ๋ชจ๋“ˆ - ์ƒํ’ˆ์˜ ์นดํ…Œ๊ณ ๋ฆฌ ๋ถ„์„ ๊ธฐ๋Šฅ ์ œ๊ณต (๊ฐœ์„ ๋ฒ„์ „)
- 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}")
# 1๋‹จ๊ณ„: ๋ฉ”์ธ ํ‚ค์›Œ๋“œ๋กœ 100๊ฐœ ์ƒํ’ˆ ๊ฐ€์ ธ์˜ค๊ธฐ (10๊ฐœ์”ฉ 10ํŽ˜์ด์ง€)
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) # API ๋ ˆ์ดํŠธ ๋ฆฌ๋ฐ‹ ๋ฐฉ์ง€
if not all_products:
return {
"status": "error",
"message": "์ƒํ’ˆ์„ ๊ฐ€์ ธ์˜ค์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.",
"main_keyword": main_keyword,
"product_name": product_name,
"total_count": 0,
"products": [],
"categories": [],
"analysis": None
}
# 2๋‹จ๊ณ„: ์ƒํ’ˆ๋ช…์—์„œ ํ‚ค์›Œ๋“œ ์ถ”์ถœ (๊ฐœ์„ : ๋” ์ •ํ™•ํ•œ ํ‚ค์›Œ๋“œ ์ถ”์ถœ)
product_keywords = []
# ๊ณต๋ฐฑ๊ณผ ์‰ผํ‘œ๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ๋ถ„๋ฆฌ
words = re.split(r'[,\s]+', product_name)
for word in words:
word = word.strip()
if word and len(word) >= 2: # ์ตœ์†Œ 2๊ธ€์ž ์ด์ƒ์ธ ๋‹จ์–ด๋งŒ
# ์ค‘๋ณต ์ œ๊ฑฐ
if word not in product_keywords:
product_keywords.append(word)
logger.info(f"์ƒํ’ˆ๋ช…์—์„œ ์ถ”์ถœํ•œ ํ‚ค์›Œ๋“œ: {product_keywords}")
# 3๋‹จ๊ณ„: ์ƒํ’ˆ ์นดํ…Œ๊ณ ๋ฆฌ ๋ถ„์„
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)
# 4๋‹จ๊ณ„: ํ‚ค์›Œ๋“œ ๊ฒ€์ƒ‰๋Ÿ‰ ์กฐํšŒ
all_keywords = [main_keyword] + product_keywords
search_volumes = keyword_search.fetch_all_search_volumes(all_keywords)
# 5๋‹จ๊ณ„: ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ๋งค์นญ ์ƒํƒœ ๋ถ„์„
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)
# ์ด ์นดํ…Œ๊ณ ๋ฆฌ์— ์†ํ•œ ์ƒํ’ˆ๋“ค ์ค‘ 10๊ฐœ๋งŒ ๊ฐ€์ ธ์˜ด
sample_products = products_in_category[:100]
category_matching.append({
"์นดํ…Œ๊ณ ๋ฆฌ": category,
"์ƒํ’ˆ์ˆ˜": count,
"๋งค์นญ์ƒํ’ˆ": sample_products
})
# 6๋‹จ๊ณ„: ๊ฒ€์ƒ‰๋Ÿ‰ ์ •๋ณด ์ถ”๊ฐ€ ๋ฐ ๊ฒฐ๊ณผ ์ •๋ฆฌ
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 "ํ‚ค์›Œ๋“œ์™€ ์นดํ…Œ๊ณ ๋ฆฌ๋ฅผ ๋ชจ๋‘ ์„ ํƒํ•ด์ฃผ์„ธ์š”."
# ์นดํ…Œ๊ณ ๋ฆฌ์—์„œ ์นด์šดํŠธ ์ •๋ณด ์ œ๊ฑฐ (์˜ˆ: "ํŒจ์…˜์˜๋ฅ˜ (10)" -> "ํŒจ์…˜์˜๋ฅ˜")
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
# ํ‚ค์›Œ๋“œ ๋ฆฌ์ŠคํŠธ ์ฒ˜๋ฆฌ (์ตœ๋Œ€ 20๊ฐœ)
if isinstance(keywords, str):
# ์‰ผํ‘œ๋‚˜ ์—”ํ„ฐ๋กœ ๋ถ„๋ฆฌ
keywords_list = [k.strip() for k in re.split(r'[,\n]+', keywords) if k.strip()]
# 20๊ฐœ๋กœ ์ œํ•œ
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 '์ „์ฒด ๋ณด๊ธฐ'}'")
# ๊ฐœ์„ ๋œ HTML ๊ฒฐ๊ณผ - ๋„ˆ๋น„ 100% ์ ์šฉ
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>
'''
# ํ‚ค์›Œ๋“œ ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ์ค€๋น„ (5๊ฐœ์”ฉ ๋ฌถ์Œ)
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 = {} # ๊ฐ ํ‚ค์›Œ๋“œ๋ณ„ ๋งค์นญ ์ •๋ณด ์ €์žฅ (match_count, total_count)
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:
# ๋„ค์ด๋ฒ„ API ํ˜ธ์ถœ
products = product_search.fetch_naver_shopping_data_for_analysis(api_keyword, count=100) # ์ƒ์œ„ 10๊ฐœ ์ƒํ’ˆ
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
# ์‹ค์ œ ๋งค์นญ ์—ฌ๋ถ€ ์นด์šดํŠธ (is_overall_view๊ฐ€ ์•„๋‹ ๋•Œ๋งŒ)
if not is_overall_view and selected_category_clean:
product_category_for_match = product_category_full
if " (" in product_category_for_match: # ์ƒํ’ˆ ์นดํ…Œ๊ณ ๋ฆฌ ์ด๋ฆ„์—์„œ count ์ œ๊ฑฐ
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))
# category_percentages.sort(key=lambda x: x[1], reverse=True) # ์ด๋ฏธ most_common์œผ๋กœ ์ •๋ ฌ๋จ
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 ["์นดํ…Œ๊ณ ๋ฆฌ ์ •๋ณด ์—†์Œ"]
# API ๋ ˆ์ดํŠธ ๋ฆฌ๋ฐ‹ ๋ฐฉ์ง€ - ์ง€์ˆ˜ ๋ฐฑ์˜คํ”„ ์‚ฌ์šฉ
exponential_backoff_sleep(0)
logger.info(f"์ „์ฒด {len(keywords_list)}๊ฐœ ํ‚ค์›Œ๋“œ ์ค‘ {len(match_results)}๊ฐœ ์ฒ˜๋ฆฌ ์™„๋ฃŒ")
# ๊ฒฐ๊ณผ๋ฅผ HTML๋กœ ๋ณ€ํ™˜
for keyword in keywords_list:
result = match_results.get(keyword, {"match_count": 0, "total_count": 0, "error": True})
# ์ˆ˜์ •๋œ ๋ถ€๋ถ„: keyword_status ๊ฒฐ์ • ๋กœ์ง ๋ณ€๊ฒฝ
# ์„ ํƒ๋œ ์นดํ…Œ๊ณ ๋ฆฌ์™€ ํ•˜๋‚˜๋ผ๋„ ๋งค์นญ๋˜๋ฉด "O", ์•„๋‹ˆ๋ฉด "X"
# "์ „์ฒด ๋ณด๊ธฐ" ์„ ํƒ ์‹œ์—๋Š” ํ•ญ์ƒ "O" (๋ชจ๋“  ์ƒํ’ˆ์ด ๋งค์นญ๋œ ๊ฒƒ์œผ๋กœ ๊ฐ„์ฃผํ–ˆ์œผ๋ฏ€๋กœ)
if result.get("error"):
keyword_status = "์˜ค๋ฅ˜" # ์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ
status_color = "red"
elif is_overall_view: # '์ „์ฒด ๋ณด๊ธฐ'์˜ ๊ฒฝ์šฐ
keyword_status = "O"
status_color = "#009879" # Green
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>' # .analysis-result, .result-container ๋‹ซ๊ธฐ
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 # ํ•จ์ˆ˜ ์‹œ์ž‘ ๋ถ€๋ถ„์— global ์„ ์–ธ
# ์ „์ฒ˜๋ฆฌ: ์ƒํ’ˆ๋ช… ์•ž๋’ค ๊ณต๋ฐฑ ์ œ๊ฑฐ ๋ฐ ์œ ํšจ์„ฑ ํ™•์ธ
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()])
# ์ค‘๋ณต ์ œ๊ฑฐ ๋ฐ 1๊ธ€์ž ์ด์ƒ ํ‚ค์›Œ๋“œ๋งŒ ์œ ์ง€
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}")
# ์ถ”์ถœ๋œ ํ‚ค์›Œ๋“œ๋ฅผ ๋ฐฐ์น˜๋กœ ๋‚˜๋ˆ„๊ธฐ (๋ฐฐ์น˜๋‹น 5๊ฐœ์”ฉ)
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 ํ˜ธ์ถœ์šฉ ํ‚ค์›Œ๋“œ (๊ณต๋ฐฑ ์ œ๊ฑฐ)
api_keyword = keyword.replace(" ", "")
# ์ตœ๋Œ€ 3๋ฒˆ ์žฌ์‹œ๋„
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 ํ˜ธ์ถœ์šฉ ํ‚ค์›Œ๋“œ ์‚ฌ์šฉ)
api_keyword = keyword.replace(" ", "")
volume_data = volumes.get(api_keyword, {"PC๊ฒ€์ƒ‰๋Ÿ‰": 0, "๋ชจ๋ฐ”์ผ๊ฒ€์ƒ‰๋Ÿ‰": 0, "์ด๊ฒ€์ƒ‰๋Ÿ‰": 0})
# ๊ฒฐ๊ณผ ์ €์žฅ (์นดํ…Œ๊ณ ๋ฆฌ ํ•ญ๋ชฉ๊ณผ ์นด์šดํŠธ ์ •๋ณด ํฌํ•จ)
keyword_results.append({
"ํ‚ค์›Œ๋“œ": keyword, # UI ํ‘œ์‹œ์šฉ ํ‚ค์›Œ๋“œ (๊ณต๋ฐฑ ํฌํ•จ)
"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
# ์ถ”์ฒœ ์นดํ…Œ๊ณ ๋ฆฌ ์ƒ์œ„ 3๊ฐœ ์„ ํƒ
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}%"
})
# 1๋…„, 3๋…„ ํŠธ๋ Œ๋“œ ๋ถ„์„ ์‹คํ–‰
trend_results = {"1year": None, "3year": None}
trend_html = ""
if keyword_results:
try:
# ํŠธ๋ Œ๋“œ ๋ถ„์„ ๋ชจ๋“ˆ import
import trend_analysis
# ์ƒ์œ„ 5๊ฐœ ํ‚ค์›Œ๋“œ๋กœ 1๋…„, 3๋…„ ํŠธ๋ Œ๋“œ ๋ถ„์„
top_keywords = [result["ํ‚ค์›Œ๋“œ"] for result in keyword_results[:5]]
logger.info(f"1๋…„, 3๋…„ ํŠธ๋ Œ๋“œ ๋ถ„์„ ์‹œ์ž‘: {top_keywords}")
# 1๋…„ ํŠธ๋ Œ๋“œ ๋ถ„์„
trend_result_1year = trend_analysis.get_trend_data(top_keywords, "1year")
if trend_result_1year["status"] == "success":
trend_results["1year"] = trend_result_1year
# 3๋…„ ํŠธ๋ Œ๋“œ ๋ถ„์„
trend_result_3year = trend_analysis.get_trend_data(top_keywords, "3year")
if trend_result_3year["status"] == "success":
trend_results["3year"] = trend_result_3year
# ํŠธ๋ Œ๋“œ ๋ถ„์„ HTML ์ƒ์„ฑ (1๋…„, 3๋…„ ๋ชจ๋‘)
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 ํ…Œ์ด๋ธ”๋กœ ๋ณ€ํ™˜ - ๋„ˆ๋น„ 100% ์ ์šฉ
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):
# ์นดํ…Œ๊ณ ๋ฆฌ ํ•ญ๋ชฉ ์ค€๋น„ (์ค„๋ฐ”๊ฟˆ์„ <br>๋กœ ๋ณ€ํ™˜)
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 ์ถ”๊ฐ€
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
# HTML๊ณผ ํ•จ๊ป˜ ํ‚ค์›Œ๋“œ ๋ถ„์„ ๊ฒฐ๊ณผ ๋ฐ ํŠธ๋ Œ๋“œ ๊ฒฐ๊ณผ๋„ ํ•จ๊ป˜ ๋ฐ˜ํ™˜
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 ํ˜ธ์ถœ์šฉ ํ‚ค์›Œ๋“œ (๊ณต๋ฐฑ ์ œ๊ฑฐ)
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์™€ ์นดํ…Œ๊ณ ๋ฆฌ)
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()
# API ๋ ˆ์ดํŠธ ๋ฆฌ๋ฐ‹ ๋ฐฉ์ง€ (์•ˆ์ •์ ์ธ ์ง€์—ฐ์œผ๋กœ ๋ณ€๊ฒฝ)
exponential_backoff_sleep(0) # ์ดˆ๊ธฐ ์ง€์—ฐ ์ ์šฉ
logger.info(f"ํ‚ค์›Œ๋“œ๋ณ„ ์นดํ…Œ๊ณ ๋ฆฌ ์ˆ˜์ง‘ ์™„๋ฃŒ: {len(keyword_category_map)}๊ฐœ")
return keyword_category_map