p763nx9tf / keyword_analysis.py
ssboost's picture
Upload 11 files
1271db4 verified
"""
ํ‚ค์›Œ๋“œ ๋งค์นญ ๋ฐ ์ƒ์Šนํญ ๊ณ„์‚ฐ ๊ฐœ์„  - ์ „์ฒด ์ฝ”๋“œ
v4.0 - ์ŠคํŽ˜์ด์Šค๋ฐ” ์ฒ˜๋ฆฌ ๊ฐœ์„  + ์˜ฌ๋ฐ”๋ฅธ ํŠธ๋ Œ๋“œ ๋ถ„์„ ๋กœ์ง ์ ์šฉ
- ๊ธฐ์กด ๋ชจ๋“  ๊ธฐ๋Šฅ ์œ ์ง€ํ•˜๋ฉด์„œ ์ตœ์ ํ™”
- ์ŠคํŽ˜์ด์Šค๋ฐ” ์ œ๊ฑฐ ํ›„ ๊ฒ€์ƒ‰/๋น„๊ต ๋กœ์ง ์ ์šฉ
- ์˜ฌ๋ฐ”๋ฅธ ์ฆ๊ฐ์œจ ๊ณ„์‚ฐ: ์˜ฌํ•ด ์™„๋ฃŒ์›” vs ์ž‘๋…„ ๋™์›”
- ๐Ÿ”– ๊ฐ€์žฅ ๊ฒ€์ƒ‰๋Ÿ‰์ด ๋งŽ์€ ์›”: ์‹ค์ œ+์˜ˆ์ƒ ๋ฐ์ดํ„ฐ ์ค‘ ์ตœ๋Œ€๊ฐ’
- ๐Ÿ”– ๊ฐ€์žฅ ์ƒ์Šนํญ์ด ๋†’์€ ์›”: ์—ฐ์†๋œ ์›”๊ฐ„ ์ƒ์Šน๋ฅ  ์ค‘ ์ตœ๋Œ€๊ฐ’
"""
import logging
import pandas as pd
from datetime import datetime
import re
import time
import random
from typing import Dict, List, Optional
logger = logging.getLogger(__name__)
def normalize_keyword(keyword):
"""ํ‚ค์›Œ๋“œ ์ •๊ทœํ™” - ์ŠคํŽ˜์ด์Šค๋ฐ” ์ฒ˜๋ฆฌ ๊ฐœ์„ """
if not keyword:
return ""
# 1. ์•ž๋’ค ๊ณต๋ฐฑ ์ œ๊ฑฐ
keyword = keyword.strip()
# 2. ์—ฐ์†๋œ ๊ณต๋ฐฑ์„ ํ•˜๋‚˜๋กœ ๋ณ€๊ฒฝ
keyword = re.sub(r'\s+', ' ', keyword)
# 3. ํŠน์ˆ˜๋ฌธ์ž ์ œ๊ฑฐ (ํ•œ๊ธ€, ์˜๋ฌธ, ์ˆซ์ž, ๊ณต๋ฐฑ๋งŒ ๋‚จ๊น€)
keyword = re.sub(r'[^\w\s๊ฐ€-ํžฃ]', '', keyword)
return keyword
def normalize_keyword_for_api(keyword):
"""API ํ˜ธ์ถœ์šฉ ํ‚ค์›Œ๋“œ ์ •๊ทœํ™” (์ŠคํŽ˜์ด์Šค ์ œ๊ฑฐ)"""
normalized = normalize_keyword(keyword)
return normalized.replace(" ", "")
def normalize_keyword_for_comparison(keyword):
"""๋น„๊ต์šฉ ํ‚ค์›Œ๋“œ ์ •๊ทœํ™” (์ŠคํŽ˜์ด์Šค ์œ ์ง€)"""
return normalize_keyword(keyword).lower()
def normalize_keyword_advanced(keyword):
"""๊ณ ๊ธ‰ ํ‚ค์›Œ๋“œ ์ •๊ทœํ™” - ๋งค์นญ ๋ฌธ์ œ ํ•ด๊ฒฐ"""
if not keyword:
return ""
# 1. ๊ธฐ๋ณธ ์ •๋ฆฌ
keyword = str(keyword).strip()
# 2. ์—ฐ์†๋œ ๊ณต๋ฐฑ์„ ํ•˜๋‚˜๋กœ ๋ณ€๊ฒฝ
keyword = re.sub(r'\s+', ' ', keyword)
# 3. ํŠน์ˆ˜๋ฌธ์ž ์ œ๊ฑฐ (ํ•œ๊ธ€, ์˜๋ฌธ, ์ˆซ์ž, ๊ณต๋ฐฑ๋งŒ ๋‚จ๊น€)
keyword = re.sub(r'[^\w\s๊ฐ€-ํžฃ]', '', keyword)
# 4. ์†Œ๋ฌธ์ž ๋ณ€ํ™˜
keyword = keyword.lower()
return keyword
def create_keyword_variations(keyword):
"""ํ‚ค์›Œ๋“œ ๋ณ€ํ˜• ๋ฒ„์ „๋“ค ์ƒ์„ฑ - ์ŠคํŽ˜์ด์Šค๋ฐ” ์ฒ˜๋ฆฌ ๊ฐ•ํ™”"""
base = normalize_keyword_advanced(keyword)
variations = [base]
# ์ŠคํŽ˜์ด์Šค ์ œ๊ฑฐ ๋ฒ„์ „
no_space = base.replace(" ", "")
if no_space != base:
variations.append(no_space)
# ์ŠคํŽ˜์ด์Šค๋ฅผ ๋‹ค๋ฅธ ๊ตฌ๋ถ„์ž๋กœ ๋ฐ”๊พผ ๋ฒ„์ „๋“ค
variations.append(base.replace(" ", "-"))
variations.append(base.replace(" ", "_"))
# ๋‹จ์–ด ์ˆœ์„œ ๋ฐ”๊พผ ๋ฒ„์ „ (2๋‹จ์–ด์ธ ๊ฒฝ์šฐ)
words = base.split()
if len(words) == 2:
reversed_keyword = f"{words[1]} {words[0]}"
variations.append(reversed_keyword)
variations.append(reversed_keyword.replace(" ", ""))
return list(set(variations)) # ์ค‘๋ณต ์ œ๊ฑฐ
def find_matching_keyword_row(analysis_keyword, keywords_df):
"""๊ฐœ์„ ๋œ ํ‚ค์›Œ๋“œ ๋งค์นญ ํ•จ์ˆ˜"""
if keywords_df is None or keywords_df.empty:
return None
analysis_variations = create_keyword_variations(analysis_keyword)
logger.info(f"๋ถ„์„ ํ‚ค์›Œ๋“œ ๋ณ€ํ˜•๋“ค: {analysis_variations}")
# 1์ฐจ: ์ •ํ™•ํ•œ ๋งค์นญ
for idx, row in keywords_df.iterrows():
df_keyword = str(row.get('์กฐํ•ฉ ํ‚ค์›Œ๋“œ', ''))
df_variations = create_keyword_variations(df_keyword)
for analysis_var in analysis_variations:
for df_var in df_variations:
if analysis_var == df_var and len(analysis_var) > 1:
logger.info(f"์ •ํ™•ํ•œ ๋งค์นญ ์„ฑ๊ณต: '{analysis_keyword}' = '{df_keyword}'")
return row
# 2์ฐจ: ํฌํ•จ ๊ด€๊ณ„ ๋งค์นญ
for idx, row in keywords_df.iterrows():
df_keyword = str(row.get('์กฐํ•ฉ ํ‚ค์›Œ๋“œ', ''))
df_variations = create_keyword_variations(df_keyword)
for analysis_var in analysis_variations:
for df_var in df_variations:
if len(analysis_var) > 2 and len(df_var) > 2:
if analysis_var in df_var or df_var in analysis_var:
similarity = len(set(analysis_var) & set(df_var)) / len(set(analysis_var) | set(df_var))
if similarity > 0.7: # 70% ์ด์ƒ ์œ ์‚ฌ
logger.info(f"๋ถ€๋ถ„ ๋งค์นญ ์„ฑ๊ณต: '{analysis_keyword}' โ‰ˆ '{df_keyword}' (์œ ์‚ฌ๋„: {similarity:.2f})")
return row
logger.warning(f"ํ‚ค์›Œ๋“œ ๋งค์นญ ์‹คํŒจ: '{analysis_keyword}'")
logger.info(f"๋ฐ์ดํ„ฐํ”„๋ ˆ์ž„ ํ‚ค์›Œ๋“œ ์ƒ˜ํ”Œ: {keywords_df['์กฐํ•ฉ ํ‚ค์›Œ๋“œ'].head(5).tolist()}")
return None
def generate_prediction_data(trend_data_3year, keyword):
"""
์ •๊ตํ•œ ์˜ˆ์ƒ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ ํ•จ์ˆ˜
- ํŠธ๋ Œ๋“œ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ํ›„ ๋ฐ”๋กœ ํ˜ธ์ถœํ•˜์—ฌ ์˜ˆ์ƒ ๋ฐ์ดํ„ฐ ์ถ”๊ฐ€
- ๊ณ„์ ˆ์„ฑ, ์ฆ๊ฐ ํŠธ๋ Œ๋“œ, ์ „๋…„ ๋Œ€๋น„ ์„ฑ์žฅ๋ฅ  ๋ชจ๋‘ ๊ณ ๋ ค
"""
if not trend_data_3year:
logger.warning("โŒ ์˜ˆ์ƒ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ ์‹คํŒจ: trend_data_3year ์—†์Œ")
return trend_data_3year
try:
current_date = datetime.now()
current_year = current_date.year
current_month = current_date.month
logger.info(f"๐Ÿ”ฎ ์˜ˆ์ƒ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ ์‹œ์ž‘: {keyword} ({current_year}๋…„ {current_month}์›” ๊ธฐ์ค€)")
for kw, data in trend_data_3year.items():
if not data or not data.get('monthly_volumes') or not data.get('dates'):
continue
volumes = data['monthly_volumes']
dates = data['dates']
# โœ… 1๋‹จ๊ณ„: ๊ธฐ์กด ๋ฐ์ดํ„ฐ ๋ถ„์„
yearly_data = {} # {year: {month: volume}}
for i, date_str in enumerate(dates):
try:
date_obj = datetime.strptime(date_str, "%Y-%m-%d")
if i < len(volumes):
volume = volumes[i]
if isinstance(volume, str):
volume = float(volume.replace(',', ''))
volume = int(volume) if volume else 0
year = date_obj.year
month = date_obj.month
if year not in yearly_data:
yearly_data[year] = {}
yearly_data[year][month] = volume
except Exception as e:
logger.warning(f"โš ๏ธ ๋‚ ์งœ ํŒŒ์‹ฑ ์˜ค๋ฅ˜: {date_str}")
continue
logger.info(f"๐Ÿ“Š ๋ถ„์„๋œ ์—ฐ๋„: {list(yearly_data.keys())}")
# โœ… 2๋‹จ๊ณ„: ์˜ˆ์ƒ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ ์•Œ๊ณ ๋ฆฌ์ฆ˜
if current_year not in yearly_data:
yearly_data[current_year] = {}
current_year_data = yearly_data[current_year]
last_year_data = yearly_data.get(current_year - 1, {})
two_years_ago_data = yearly_data.get(current_year - 2, {})
logger.info(f"๐Ÿ“ˆ ์˜ฌํ•ด ์‹ค์ œ ๋ฐ์ดํ„ฐ: {len(current_year_data)}๊ฐœ์›”")
logger.info(f"๐Ÿ“ˆ ์ž‘๋…„ ์ฐธ์กฐ ๋ฐ์ดํ„ฐ: {len(last_year_data)}๊ฐœ์›”")
# ์˜ˆ์ƒ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ (ํ˜„์žฌ์›” ์ดํ›„)
for future_month in range(current_month + 1, 13):
if future_month in current_year_data:
continue # ์ด๋ฏธ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์œผ๋ฉด ์Šคํ‚ต
predicted_volume = calculate_predicted_volume(
future_month, current_year_data, last_year_data,
two_years_ago_data, current_month
)
if predicted_volume is not None:
current_year_data[future_month] = predicted_volume
logger.info(f"๐Ÿ”ฎ ์˜ˆ์ƒ ์ƒ์„ฑ: {current_year}๋…„ {future_month}์›” = {predicted_volume:,}ํšŒ")
# โœ… 3๋‹จ๊ณ„: ์ƒ์„ฑ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ์›๋ณธ ๊ตฌ์กฐ์— ํ†ตํ•ฉ
updated_volumes = []
updated_dates = []
# ์‹œ๊ฐ„์ˆœ์œผ๋กœ ์ •๋ ฌํ•˜์—ฌ ํ†ตํ•ฉ
all_months = []
for year in sorted(yearly_data.keys()):
for month in sorted(yearly_data[year].keys()):
all_months.append((year, month, yearly_data[year][month]))
for year, month, volume in all_months:
updated_volumes.append(volume)
updated_dates.append(f"{year}-{month:02d}-01")
# ์›๋ณธ ๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ
data['monthly_volumes'] = updated_volumes
data['dates'] = updated_dates
logger.info(f"โœ… {kw} ์˜ˆ์ƒ ๋ฐ์ดํ„ฐ ํ†ตํ•ฉ ์™„๋ฃŒ: ์ด {len(updated_volumes)}๊ฐœ์›”")
logger.info(f"๐ŸŽ‰ ์ „์ฒด ์˜ˆ์ƒ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ ์™„๋ฃŒ: {keyword}")
return trend_data_3year
except Exception as e:
logger.error(f"โŒ ์˜ˆ์ƒ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ ์˜ค๋ฅ˜: {e}")
return trend_data_3year
def calculate_predicted_volume(target_month, current_year_data, last_year_data,
two_years_ago_data, current_month):
"""
์ •๊ตํ•œ ์˜ˆ์ƒ ๋ณผ๋ฅจ ๊ณ„์‚ฐ
- ๋‹ค์ค‘ ์š”์ธ ๊ณ ๋ ค: ์ž‘๋…„ ๋™์›”, ์ฆ๊ฐ ํŠธ๋ Œ๋“œ, ๊ณ„์ ˆ์„ฑ, ์„ฑ์žฅ๋ฅ 
"""
try:
# ๊ธฐ์ค€ ๊ฐ’๋“ค
last_year_same_month = last_year_data.get(target_month, 0)
two_years_ago_same_month = two_years_ago_data.get(target_month, 0)
if last_year_same_month == 0:
logger.warning(f"โš ๏ธ {target_month}์›” ์ž‘๋…„ ๋ฐ์ดํ„ฐ ์—†์Œ")
return None
# โœ… 1. ๊ธฐ๋ณธ๊ฐ’: ์ž‘๋…„ ๋™์›”
base_volume = last_year_same_month
# โœ… 2. ์ „๋…„ ๋Œ€๋น„ ์„ฑ์žฅ๋ฅ  ๊ณ„์‚ฐ (๊ฐ€๋Šฅํ•œ ๊ฒฝ์šฐ)
growth_rate = 1.0
if two_years_ago_same_month > 0:
growth_rate = last_year_same_month / two_years_ago_same_month
logger.info(f"๐Ÿ“ˆ {target_month}์›” ์ „๋…„ ์„ฑ์žฅ๋ฅ : {growth_rate:.2f}๋ฐฐ")
# โœ… 3. ์˜ฌํ•ด ์ตœ๊ทผ ํŠธ๋ Œ๋“œ ๋ฐ˜์˜
trend_factor = 1.0
if len(current_year_data) >= 2:
# ์ตœ๊ทผ 2-3๊ฐœ์›”์˜ ์ž‘๋…„ ๋Œ€๋น„ ๋น„์œจ ๊ณ„์‚ฐ
recent_ratios = []
for month in range(max(1, current_month - 2), current_month + 1):
if month in current_year_data and month in last_year_data:
if last_year_data[month] > 0:
ratio = current_year_data[month] / last_year_data[month]
recent_ratios.append(ratio)
if recent_ratios:
trend_factor = sum(recent_ratios) / len(recent_ratios)
logger.info(f"๐Ÿ“Š ์ตœ๊ทผ ํŠธ๋ Œ๋“œ ํŒฉํ„ฐ: {trend_factor:.2f}")
# โœ… 4. ๊ณ„์ ˆ์„ฑ ๋ณด์ • (๊ฐ™์€ ๋ถ„๊ธฐ ๋‚ด ์›”๊ฐ„ ํŒจํ„ด)
seasonal_factor = 1.0
if target_month > 1 and target_month - 1 in last_year_data and target_month in last_year_data:
# ์ž‘๋…„ ๋™์ผ ๊ตฌ๊ฐ„์˜ ์›”๊ฐ„ ๋ณ€ํ™”์œจ
if last_year_data[target_month - 1] > 0:
seasonal_factor = last_year_data[target_month] / last_year_data[target_month - 1]
logger.info(f"๐ŸŒŠ {target_month}์›” ๊ณ„์ ˆ์„ฑ ํŒฉํ„ฐ: {seasonal_factor:.2f}")
# โœ… 5. ์ตœ์ข… ์˜ˆ์ƒ๊ฐ’ ๊ณ„์‚ฐ (๊ฐ€์ค‘ํ‰๊ท )
predicted_volume = int(
base_volume * (
0.4 * growth_rate + # 40% ์ „๋…„ ์„ฑ์žฅ๋ฅ 
0.4 * trend_factor + # 40% ์ตœ๊ทผ ํŠธ๋ Œ๋“œ
0.2 * seasonal_factor # 20% ๊ณ„์ ˆ์„ฑ
)
)
# โœ… 6. ํ•ฉ๋ฆฌ์„ฑ ๊ฒ€์ฆ (๊ธ‰๊ฒฉํ•œ ๋ณ€ํ™” ๋ฐฉ์ง€)
if current_year_data:
recent_avg = sum(current_year_data.values()) / len(current_year_data)
if predicted_volume > recent_avg * 5: # 5๋ฐฐ ์ด์ƒ ๊ธ‰์ฆ ๋ฐฉ์ง€
predicted_volume = int(recent_avg * 2)
logger.warning(f"โš ๏ธ {target_month}์›” ๊ธ‰์ฆ ๋ณด์ •: {predicted_volume:,}ํšŒ")
elif predicted_volume < recent_avg * 0.1: # 10๋ถ„์˜ 1 ์ดํ•˜ ๊ธ‰๊ฐ ๋ฐฉ์ง€
predicted_volume = int(recent_avg * 0.5)
logger.warning(f"โš ๏ธ {target_month}์›” ๊ธ‰๊ฐ ๋ณด์ •: {predicted_volume:,}ํšŒ")
logger.info(f"๐ŸŽฏ {target_month}์›” ์˜ˆ์ƒ ๊ณ„์‚ฐ: {last_year_same_month:,} ร— (์„ฑ์žฅ{growth_rate:.2f} + ํŠธ๋ Œ๋“œ{trend_factor:.2f} + ๊ณ„์ ˆ{seasonal_factor:.2f}) = {predicted_volume:,}")
return predicted_volume
except Exception as e:
logger.error(f"โŒ {target_month}์›” ์˜ˆ์ƒ ๊ณ„์‚ฐ ์˜ค๋ฅ˜: {e}")
return None
def enhance_trend_data_with_predictions(trend_data_3year, keyword):
"""
๊ธฐ์กด ํŠธ๋ Œ๋“œ ๋ฐ์ดํ„ฐ์— ์˜ˆ์ƒ ๋ฐ์ดํ„ฐ ์ถ”๊ฐ€
- ๋ฉ”์ธ ํŠธ๋ Œ๋“œ ์ˆ˜์ง‘ ํ•จ์ˆ˜์—์„œ ํ˜ธ์ถœ
"""
if not trend_data_3year:
return trend_data_3year
logger.info(f"๐Ÿš€ ํŠธ๋ Œ๋“œ ๋ฐ์ดํ„ฐ ์˜ˆ์ƒ ํ™•์žฅ ์‹œ์ž‘: {keyword}")
enhanced_data = generate_prediction_data(trend_data_3year, keyword)
# ๋ฐ์ดํ„ฐ ํ’ˆ์งˆ ๊ฒ€์ฆ
for kw, data in enhanced_data.items():
if data and data.get('monthly_volumes'):
total_months = len(data['monthly_volumes'])
current_year = datetime.now().year
# ํ˜„์žฌ ์—ฐ๋„ ๋ฐ์ดํ„ฐ ๊ฐœ์ˆ˜ ํ™•์ธ
current_year_count = 0
for date_str in data['dates']:
try:
if date_str.startswith(str(current_year)):
current_year_count += 1
except:
continue
logger.info(f"โœ… {kw} ์ตœ์ข… ๋ฐ์ดํ„ฐ: ์ „์ฒด {total_months}๊ฐœ์›”, ์˜ฌํ•ด {current_year_count}๊ฐœ์›”")
return enhanced_data
def calculate_max_growth_rate_with_predictions(trend_data_3year, keyword):
"""์˜ฌ๋ฐ”๋ฅธ ํŠธ๋ Œ๋“œ ๋ถ„์„ ๋กœ์ง - ์‚ฌ์šฉ์ž ์š”๊ตฌ์‚ฌํ•ญ ์ ์šฉ"""
if not trend_data_3year:
logger.error("โŒ trend_data_3year๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค")
return "๋ฐ์ดํ„ฐ ์—†์Œ"
try:
keyword_data = None
for kw, data in trend_data_3year.items():
keyword_data = data
logger.info(f"๐Ÿ” ํ‚ค์›Œ๋“œ ๋ฐ์ดํ„ฐ ๋ฐœ๊ฒฌ: {kw}")
break
if not keyword_data or not keyword_data.get('monthly_volumes') or not keyword_data.get('dates'):
logger.error("โŒ keyword_data ๊ตฌ์กฐ ๋ฌธ์ œ")
return "๋ฐ์ดํ„ฐ ์—†์Œ"
volumes = keyword_data['monthly_volumes']
dates = keyword_data['dates']
# 1๋‹จ๊ณ„: ํ˜„์žฌ ์‹œ์  ํŒŒ์•…
current_date = datetime.now()
current_year = current_date.year
current_month = current_date.month
current_day = current_date.day
# ์™„๋ฃŒ๋œ ๋งˆ์ง€๋ง‰ ์›” ๊ณ„์‚ฐ (2์ผ ์ดํ›„๋ฉด ์ „์›”๊นŒ์ง€ ์™„๋ฃŒ)
if current_day >= 2:
completed_year = current_year
completed_month = current_month - 1
else:
completed_year = current_year
completed_month = current_month - 2
# ์›”์ด 0 ์ดํ•˜๊ฐ€ ๋˜๋ฉด ์—ฐ๋„ ์กฐ์ •
while completed_month <= 0:
completed_month += 12
completed_year -= 1
logger.info(f"๐Ÿ“… ํ˜„์žฌ: {current_year}๋…„ {current_month}์›” {current_day}์ผ")
logger.info(f"๐Ÿ“Š ์™„๋ฃŒ๋œ ๋งˆ์ง€๋ง‰ ๋ฐ์ดํ„ฐ: {completed_year}๋…„ {completed_month}์›”")
# 2๋‹จ๊ณ„: ๋ฐ์ดํ„ฐ ๋ถ„๋ฅ˜ ๋ฐ ์ˆ˜์ง‘
all_data = []
for i, date_str in enumerate(dates):
try:
date_obj = datetime.strptime(date_str, "%Y-%m-%d")
if i < len(volumes):
volume = volumes[i]
if isinstance(volume, str):
volume = float(volume.replace(',', ''))
volume = int(volume) if volume else 0
all_data.append({
'year': date_obj.year,
'month': date_obj.month,
'volume': volume,
'date_str': date_str,
'date_obj': date_obj,
'sort_key': f"{date_obj.year:04d}{date_obj.month:02d}"
})
except Exception as e:
logger.warning(f"โš ๏ธ ๋‚ ์งœ ํŒŒ์‹ฑ ์˜ค๋ฅ˜: {date_str} - {e}")
continue
# ์‹œ๊ฐ„ ์ˆœ์„œ๋Œ€๋กœ ์ •๋ ฌ
all_data = sorted(all_data, key=lambda x: x['sort_key'])
# 3๋‹จ๊ณ„: ์ฆ๊ฐ์œจ ๊ณ„์‚ฐ (์˜ฌํ•ด ์™„๋ฃŒ์›” vs ์ž‘๋…„ ๋™์›”)
this_year_completed_volume = None
last_year_same_month_volume = None
for data in all_data:
# ์˜ฌํ•ด ์™„๋ฃŒ๋œ ๋งˆ์ง€๋ง‰ ์›” ์ฐพ๊ธฐ
if data['year'] == completed_year and data['month'] == completed_month:
this_year_completed_volume = data['volume']
logger.info(f"๐Ÿ“Š ์˜ฌํ•ด {completed_month}์›” ์‹ค๋ฐ์ดํ„ฐ: {this_year_completed_volume:,}ํšŒ")
# ์ž‘๋…„ ๋™์›” ์ฐพ๊ธฐ
if data['year'] == completed_year - 1 and data['month'] == completed_month:
last_year_same_month_volume = data['volume']
logger.info(f"๐Ÿ“Š ์ž‘๋…„ {completed_month}์›” ์‹ค๋ฐ์ดํ„ฐ: {last_year_same_month_volume:,}ํšŒ")
# ์ฆ๊ฐ์œจ ๊ณ„์‚ฐ
growth_rate = 0
if this_year_completed_volume is not None and last_year_same_month_volume is not None and last_year_same_month_volume > 0:
growth_rate = (this_year_completed_volume - last_year_same_month_volume) / last_year_same_month_volume
logger.info(f"๐Ÿ“ˆ ๊ณ„์‚ฐ๋œ ์ฆ๊ฐ์œจ: {growth_rate:+.3f} ({growth_rate * 100:+.1f}%)")
else:
logger.warning("โš ๏ธ ์ฆ๊ฐ์œจ ๊ณ„์‚ฐ์„ ์œ„ํ•œ ๋ฐ์ดํ„ฐ๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค.")
# 4๋‹จ๊ณ„: ์˜ˆ์ƒ๋ฐ์ดํ„ฐ ์ƒ์„ฑ (ํ˜„์žฌ์›” ์ดํ›„)
combined_data = []
month_names = ["", "1์›”", "2์›”", "3์›”", "4์›”", "5์›”", "6์›”", "7์›”", "8์›”", "9์›”", "10์›”", "11์›”", "12์›”"]
# ์ž‘๋…„ 12์›” ๋ฐ์ดํ„ฐ ์ถ”๊ฐ€ (์—ฐ์†์„ฑ์„ ์œ„ํ•ด)
for data in all_data:
if data['year'] == completed_year - 1 and data['month'] == 12:
combined_data.append({
'year': data['year'],
'month': data['month'],
'volume': data['volume'],
'data_type': '์ž‘๋…„์‹ค์ œ',
'sort_key': f"{data['year']:04d}{data['month']:02d}"
})
logger.info(f"๐Ÿ”— ์ž‘๋…„ 12์›” ์‹ค๋ฐ์ดํ„ฐ: {data['volume']:,}ํšŒ")
break
# ์˜ฌํ•ด 1์›”๋ถ€ํ„ฐ ์™„๋ฃŒ์›”๊นŒ์ง€ ์‹ค์ œ๋ฐ์ดํ„ฐ ์ถ”๊ฐ€
for month in range(1, completed_month + 1):
for data in all_data:
if data['year'] == completed_year and data['month'] == month:
combined_data.append({
'year': data['year'],
'month': data['month'],
'volume': data['volume'],
'data_type': '์‹ค์ œ',
'sort_key': f"{data['year']:04d}{data['month']:02d}"
})
logger.info(f"๐Ÿ“Š {month}์›” ์‹ค๋ฐ์ดํ„ฐ: {data['volume']:,}ํšŒ")
break
# ์˜ฌํ•ด ๋ฏธ์™„๋ฃŒ์›”(ํ˜„์žฌ์›”+1 ~ 12์›”) ์˜ˆ์ƒ๋ฐ์ดํ„ฐ ์ƒ์„ฑ
for month in range(completed_month + 1, 13):
# ์ž‘๋…„ ๋™์›” ๋ฐ์ดํ„ฐ ์ฐพ๊ธฐ
last_year_volume = None
for data in all_data:
if data['year'] == completed_year - 1 and data['month'] == month:
last_year_volume = data['volume']
break
if last_year_volume is not None:
# ์˜ˆ์ƒ ๊ฒ€์ƒ‰๋Ÿ‰ = ์ž‘๋…„ ๋™์›” ร— (1 + ์ฆ๊ฐ์œจ)
predicted_volume = int(last_year_volume * (1 + growth_rate))
predicted_volume = max(predicted_volume, 0) # ์Œ์ˆ˜ ๋ฐฉ์ง€
combined_data.append({
'year': completed_year,
'month': month,
'volume': predicted_volume,
'data_type': '์˜ˆ์ƒ',
'sort_key': f"{completed_year:04d}{month:02d}"
})
logger.info(f"๐Ÿ”ฎ {month}์›” ์˜ˆ์ƒ๋ฐ์ดํ„ฐ: {predicted_volume:,}ํšŒ (์ž‘๋…„ {last_year_volume:,}ํšŒ ร— {1 + growth_rate:.3f})")
# ์‹œ๊ฐ„ ์ˆœ์„œ๋Œ€๋กœ ์ •๋ ฌ
combined_data = sorted(combined_data, key=lambda x: x['sort_key'])
# 5๋‹จ๊ณ„: ๐Ÿ”– ๊ฐ€์žฅ ์ƒ์Šนํญ์ด ๋†’์€ ์›” ์ฐพ๊ธฐ (์—ฐ์†๋œ ์›”๊ฐ„ ์ƒ์Šน๋ฅ )
max_growth_rate = 0
max_growth_info = "๋ฐ์ดํ„ฐ ์—†์Œ"
for i in range(len(combined_data) - 1):
start_data = combined_data[i]
end_data = combined_data[i + 1]
if start_data['volume'] > 0:
month_growth_rate = ((end_data['volume'] - start_data['volume']) / start_data['volume']) * 100
# ์ƒ์Šนํ•œ ๊ฒฝ์šฐ๋งŒ ๊ณ ๋ ค
if month_growth_rate > max_growth_rate:
max_growth_rate = month_growth_rate
start_month_name = month_names[start_data['month']]
end_month_name = month_names[end_data['month']]
# ์—ฐ๋„ ์ „ํ™˜ ๊ณ ๋ ค
if start_data['year'] != end_data['year']:
period_desc = f"{start_data['year']}๋…„ {start_month_name}({start_data['volume']:,}ํšŒ)์—์„œ {end_data['year']}๋…„ {end_month_name}({end_data['volume']:,}ํšŒ)์œผ๋กœ"
else:
period_desc = f"{start_month_name}({start_data['volume']:,}ํšŒ)์—์„œ {end_month_name}({end_data['volume']:,}ํšŒ)์œผ๋กœ"
# ๋ฐ์ดํ„ฐ ์œ ํ˜• ํŒ๋‹จ
if start_data['data_type'] in ['์˜ˆ์ƒ'] and end_data['data_type'] in ['์˜ˆ์ƒ']:
data_type = "์˜ˆ์ƒ ๊ธฐ๋ฐ˜"
elif start_data['data_type'] in ['์‹ค์ œ', '์ž‘๋…„์‹ค์ œ'] and end_data['data_type'] in ['์‹ค์ œ', '์ž‘๋…„์‹ค์ œ']:
data_type = "์‹ค์ œ ๊ธฐ๋ฐ˜"
else:
data_type = "์‹ค์ œโ†’์˜ˆ์ƒ ๊ธฐ๋ฐ˜"
max_growth_info = f"{period_desc} {max_growth_rate:.1f}% ์ƒ์Šน ({data_type})"
# ์ƒ์Šน ๊ตฌ๊ฐ„์ด ์—†๋Š” ๊ฒฝ์šฐ ์ตœ์†Œ ํ•˜๋ฝ๋ฅ  ํ‘œ์‹œ
if max_growth_rate == 0:
min_decline_rate = float('inf')
for i in range(len(combined_data) - 1):
start_data = combined_data[i]
end_data = combined_data[i + 1]
if start_data['volume'] > 0:
month_growth_rate = ((end_data['volume'] - start_data['volume']) / start_data['volume']) * 100
if abs(month_growth_rate) < abs(min_decline_rate):
min_decline_rate = month_growth_rate
start_month_name = month_names[start_data['month']]
end_month_name = month_names[end_data['month']]
if start_data['year'] != end_data['year']:
period_desc = f"{start_data['year']}๋…„ {start_month_name}({start_data['volume']:,}ํšŒ)์—์„œ {end_data['year']}๋…„ {end_month_name}({end_data['volume']:,}ํšŒ)์œผ๋กœ"
else:
period_desc = f"{start_month_name}({start_data['volume']:,}ํšŒ)์—์„œ {end_month_name}({end_data['volume']:,}ํšŒ)์œผ๋กœ"
if start_data['data_type'] in ['์˜ˆ์ƒ'] and end_data['data_type'] in ['์˜ˆ์ƒ']:
data_type = "์˜ˆ์ƒ ๊ธฐ๋ฐ˜"
elif start_data['data_type'] in ['์‹ค์ œ', '์ž‘๋…„์‹ค์ œ'] and end_data['data_type'] in ['์‹ค์ œ', '์ž‘๋…„์‹ค์ œ']:
data_type = "์‹ค์ œ ๊ธฐ๋ฐ˜"
else:
data_type = "์‹ค์ œโ†’์˜ˆ์ƒ ๊ธฐ๋ฐ˜"
max_growth_info = f"{period_desc} {abs(min_decline_rate):.1f}% ๊ฐ์†Œ ({data_type})"
logger.info(f"๐Ÿ† ๊ฐ€์žฅ ์ƒ์Šนํญ์ด ๋†’์€ ์›”: {max_growth_info}")
return max_growth_info
except Exception as e:
logger.error(f"โŒ ์ƒ์Šนํญ ๊ณ„์‚ฐ ์˜ค๋ฅ˜: {e}")
import traceback
logger.error(f"โŒ ์Šคํƒ ํŠธ๋ ˆ์ด์Šค: {traceback.format_exc()}")
return "๊ณ„์‚ฐ ์˜ค๋ฅ˜"
def get_peak_month_with_predictions(trend_data_3year, keyword):
"""๐Ÿ”– ๊ฐ€์žฅ ๊ฒ€์ƒ‰๋Ÿ‰์ด ๋งŽ์€ ์›” ์ฐพ๊ธฐ - ์‹ค์ œ+์˜ˆ์ƒ ๋ฐ์ดํ„ฐ ํ™œ์šฉ"""
if not trend_data_3year:
return "์—ฐ์ค‘"
try:
keyword_data = None
for kw, data in trend_data_3year.items():
keyword_data = data
break
if not keyword_data or not keyword_data.get('monthly_volumes') or not keyword_data.get('dates'):
return "์—ฐ์ค‘"
volumes = keyword_data['monthly_volumes']
dates = keyword_data['dates']
# ํ˜„์žฌ ์‹œ์  ํŒŒ์•…
current_date = datetime.now()
current_year = current_date.year
current_month = current_date.month
current_day = current_date.day
# ์™„๋ฃŒ๋œ ๋งˆ์ง€๋ง‰ ์›” ๊ณ„์‚ฐ
if current_day >= 2:
completed_year = current_year
completed_month = current_month - 1
else:
completed_year = current_year
completed_month = current_month - 2
while completed_month <= 0:
completed_month += 12
completed_year -= 1
# ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘
all_data = []
for i, date_str in enumerate(dates):
try:
date_obj = datetime.strptime(date_str, "%Y-%m-%d")
if i < len(volumes):
volume = volumes[i]
if isinstance(volume, str):
volume = float(volume.replace(',', ''))
volume = int(volume) if volume else 0
all_data.append({
'year': date_obj.year,
'month': date_obj.month,
'volume': volume
})
except:
continue
# ์ฆ๊ฐ์œจ ๊ณ„์‚ฐ
this_year_completed_volume = None
last_year_same_month_volume = None
for data in all_data:
if data['year'] == completed_year and data['month'] == completed_month:
this_year_completed_volume = data['volume']
if data['year'] == completed_year - 1 and data['month'] == completed_month:
last_year_same_month_volume = data['volume']
growth_rate = 0
if this_year_completed_volume is not None and last_year_same_month_volume is not None and last_year_same_month_volume > 0:
growth_rate = (this_year_completed_volume - last_year_same_month_volume) / last_year_same_month_volume
# ์˜ฌํ•ด ๋ฐ์ดํ„ฐ ์ค€๋น„ (์‹ค์ œ + ์˜ˆ์ƒ)
year_data = []
month_names = ["", "1์›”", "2์›”", "3์›”", "4์›”", "5์›”", "6์›”", "7์›”", "8์›”", "9์›”", "10์›”", "11์›”", "12์›”"]
# ์‹ค์ œ ๋ฐ์ดํ„ฐ ์ถ”๊ฐ€ (1์›”~์™„๋ฃŒ์›”)
for month in range(1, completed_month + 1):
for data in all_data:
if data['year'] == completed_year and data['month'] == month:
year_data.append({
'month': month,
'volume': data['volume'],
'data_type': '์‹ค์ œ'
})
break
# ์˜ˆ์ƒ ๋ฐ์ดํ„ฐ ์ถ”๊ฐ€ (์™„๋ฃŒ์›”+1~12์›”)
for month in range(completed_month + 1, 13):
last_year_volume = None
for data in all_data:
if data['year'] == completed_year - 1 and data['month'] == month:
last_year_volume = data['volume']
break
if last_year_volume is not None:
predicted_volume = int(last_year_volume * (1 + growth_rate))
predicted_volume = max(predicted_volume, 0)
year_data.append({
'month': month,
'volume': predicted_volume,
'data_type': '์˜ˆ์ƒ'
})
# ๊ฐ€์žฅ ๋†’์€ ๊ฒ€์ƒ‰๋Ÿ‰ ์ฐพ๊ธฐ
if not year_data:
return "์—ฐ์ค‘"
max_data = max(year_data, key=lambda x: x['volume'])
month_name = month_names[max_data['month']]
data_type_suffix = " - ์˜ˆ์ƒ" if max_data['data_type'] == '์˜ˆ์ƒ' else ""
return f"{month_name}({max_data['volume']:,}ํšŒ){data_type_suffix}"
except Exception as e:
logger.error(f"ํ”ผํฌ์›” ๋ถ„์„ ์˜ค๋ฅ˜: {e}")
return "์—ฐ์ค‘"
def calculate_3year_growth_rate_improved(volumes):
"""์ž‘๋…„๋Œ€๋น„ ์ฆ๊ฐ์œจ ๊ณ„์‚ฐ (3๋…„ ๋ฐ์ดํ„ฐ์šฉ)"""
if len(volumes) < 24:
return 0
try:
# ์ฒซ ํ•ด์™€ ๋งˆ์ง€๋ง‰ ํ•ด ๋น„๊ต
first_year = volumes[:12]
last_year = volumes[-12:]
first_year_avg = sum(first_year) / len(first_year)
last_year_avg = sum(last_year) / len(last_year)
if first_year_avg == 0:
return 0
growth_rate = ((last_year_avg - first_year_avg) / first_year_avg) * 100
return min(max(growth_rate, -50), 200) # -50% ~ 200% ๋ฒ”์œ„๋กœ ์ œํ•œ
except Exception as e:
logger.error(f"์ž‘๋…„๋Œ€๋น„ ์ฆ๊ฐ์œจ ๊ณ„์‚ฐ ์˜ค๋ฅ˜: {e}")
return 0
def calculate_max_growth_rate_pure_logic(trend_data_3year, keyword):
"""์ˆœ์ˆ˜ ๋กœ์ง์œผ๋กœ ์ตœ๋Œ€ ์ƒ์Šนํญ ๊ณ„์‚ฐ - ์˜ˆ์ƒ ๋ฐ์ดํ„ฐ ํฌํ•จ ๋ฒ„์ „"""
return calculate_max_growth_rate_with_predictions(trend_data_3year, keyword)
def analyze_season_cycle_with_llm(trend_data_3year, keyword, total_volume, gemini_model):
"""LLM์„ ์ด์šฉํ•œ ์‹œ์ฆŒ ์ƒํ’ˆ ์†Œ์‹ฑ ์‚ฌ์ดํด ๋ถ„์„"""
if not trend_data_3year or not gemini_model:
return "๋น„์‹œ์ฆŒ์ƒํ’ˆ", "์–ธ์ œ๋“ ์ง€ ์ง„์ž… ๊ฐ€๋Šฅ", "๋ฐ์ดํ„ฐ ๋ถ€์กฑ"
try:
keyword_data = None
for kw, data in trend_data_3year.items():
keyword_data = data
break
if not keyword_data or not keyword_data.get('monthly_volumes'):
return "๋น„์‹œ์ฆŒ์ƒํ’ˆ", "์–ธ์ œ๋“ ์ง€ ์ง„์ž… ๊ฐ€๋Šฅ", "๋ฐ์ดํ„ฐ ๋ถ€์กฑ"
volumes = keyword_data['monthly_volumes']
dates = keyword_data['dates']
recent_12_volumes = volumes[-12:] if len(volumes) >= 12 else volumes
recent_12_dates = dates[-12:] if len(dates) >= 12 else dates
if len(recent_12_volumes) < 12:
return "๋น„์‹œ์ฆŒ์ƒํ’ˆ", "์–ธ์ œ๋“ ์ง€ ์ง„์ž… ๊ฐ€๋Šฅ", "๋ฐ์ดํ„ฐ ๋ถ€์กฑ"
monthly_data_str = ""
max_volume = 0
max_month = ""
for i, (date, volume) in enumerate(zip(recent_12_dates, recent_12_volumes)):
try:
date_obj = datetime.strptime(date, "%Y-%m-%d")
month_name = f"{date_obj.year}๋…„ {date_obj.month}์›”"
monthly_data_str += f"{month_name}: {volume:,}ํšŒ\n"
# ์ตœ๋Œ€ ๊ฒ€์ƒ‰๋Ÿ‰ ์›” ์ฐพ๊ธฐ
if volume > max_volume:
max_volume = volume
max_month = f"{date_obj.month}์›”({volume:,}ํšŒ)"
except:
monthly_data_str += f"์›”-{i+1}: {volume:,}ํšŒ\n"
current_date = datetime.now()
current_month = current_date.month
prompt = f"""
ํ‚ค์›Œ๋“œ: '{keyword}'
ํ˜„์žฌ ๊ฒ€์ƒ‰๋Ÿ‰: {total_volume:,}ํšŒ
ํ˜„์žฌ ์‹œ์ : {current_date.year}๋…„ {current_month}์›”
์›”๋ณ„ ๊ฒ€์ƒ‰๋Ÿ‰ ๋ฐ์ดํ„ฐ (์ตœ๊ทผ 12๊ฐœ์›”):
{monthly_data_str}
๋‹ค์Œ ํ˜•์‹์œผ๋กœ๋งŒ ๋‹ต๋ณ€ํ•˜์„ธ์š”:
์ƒํ’ˆ์œ ํ˜•: [๋ด„์‹œ์ฆŒ์ƒํ’ˆ/์—ฌ๋ฆ„์‹œ์ฆŒ์ƒํ’ˆ/๊ฐ€์„์‹œ์ฆŒ์ƒํ’ˆ/๊ฒจ์šธ์‹œ์ฆŒ์ƒํ’ˆ/๋น„์‹œ์ฆŒ์ƒํ’ˆ/ํฌ๋ฆฌ์Šค๋งˆ์Šค์ด๋ฒคํŠธ์ƒํ’ˆ/๋ฐธ๋Ÿฐํƒ€์ธ์ด๋ฒคํŠธ์ƒํ’ˆ/์–ด๋ฒ„์ด๋‚ ์ด๋ฒคํŠธ์ƒํ’ˆ/์ƒˆํ•™๊ธฐ์ด๋ฒคํŠธ์ƒํ’ˆ/๊ธฐํƒ€์ด๋ฒคํŠธ์ƒํ’ˆ]
ํ”ผํฌ์›”: [X์›”] (๊ฒ€์ƒ‰๋Ÿ‰์ด ๊ฐ€์žฅ ๋†’์€ ์›”, ์‹ค์ œ ์ˆ˜์น˜ ํฌํ•จ)
์„ฑ์žฅ์›”: [X์›”] (์ฆ๊ฐ€ํญ์ด ๊ฐ€์žฅ ๋†’์€ ์›”)
ํ˜„์žฌ์ƒํƒœ: {current_month}์›” ๊ธฐ์ค€ [๋„์ž…๊ธฐ/์„ฑ์žฅ๊ธฐ/์•ˆ์ •๊ธฐ/์‡ ํ‡ด๊ธฐ/๋น„์‹œ์ฆŒ๊ธฐ๊ฐ„]
์ง„์ž…์ถ”์ฒœ: [๊ตฌ์ฒด์  ์›” ์ œ์‹œ]
"""
response = gemini_model.generate_content(prompt)
result_text = response.text.strip()
lines = result_text.split('\n')
product_type = "๋น„์‹œ์ฆŒ์ƒํ’ˆ"
peak_month = max_month if max_month else "์—ฐ์ค‘"
growth_month = "์—ฐ์ค‘"
current_status = "์•ˆ์ •๊ธฐ"
entry_recommendation = "์–ธ์ œ๋“ ์ง€ ์ง„์ž… ๊ฐ€๋Šฅ"
for line in lines:
line = line.strip()
if line.startswith('์ƒํ’ˆ์œ ํ˜•:'):
product_type = line.replace('์ƒํ’ˆ์œ ํ˜•:', '').strip()
elif line.startswith('ํ”ผํฌ์›”:'):
extracted_peak = line.replace('ํ”ผํฌ์›”:', '').strip()
if '(' in extracted_peak and ')' in extracted_peak:
peak_month = extracted_peak
else:
peak_month = max_month if max_month else extracted_peak
elif line.startswith('์„ฑ์žฅ์›”:'):
growth_month = line.replace('์„ฑ์žฅ์›”:', '').strip()
elif line.startswith('ํ˜„์žฌ์ƒํƒœ:'):
current_status = line.replace('ํ˜„์žฌ์ƒํƒœ:', '').strip()
elif line.startswith('์ง„์ž…์ถ”์ฒœ:'):
entry_recommendation = line.replace('์ง„์ž…์ถ”์ฒœ:', '').strip()
detail_info = f"์ƒํ’ˆ์œ ํ˜•: {product_type} | ํ”ผํฌ์›”: {peak_month} | ์„ฑ์žฅ์›”: {growth_month} | ํ˜„์žฌ์ƒํƒœ: {current_status}"
logger.info(f"LLM ์‹œ์ฆŒ ๋ถ„์„ ์™„๋ฃŒ: {product_type}, {entry_recommendation}")
return product_type, entry_recommendation, detail_info
except Exception as e:
logger.error(f"LLM ์‹œ์ฆŒ ์‚ฌ์ดํด ๋ถ„์„ ์˜ค๋ฅ˜: {e}")
return "๋น„์‹œ์ฆŒ์ƒํ’ˆ", "์–ธ์ œ๋“ ์ง€ ์ง„์ž… ๊ฐ€๋Šฅ", "LLM ๋ถ„์„ ์˜ค๋ฅ˜"
def analyze_sourcing_strategy_improved(keyword, volume_data, trend_data_1year, trend_data_3year, filtered_keywords_df, gemini_model):
"""๊ฐœ์„ ๋œ ์†Œ์‹ฑ์ „๋žต ๋ถ„์„ - ํฌ๋งทํŒ… ์ˆ˜์ • ๋ฐ ๊ด€์—ฌ๋„ ๋ถ„์„ ๊ฐ•ํ™”"""
total_volume = volume_data.get('์ด๊ฒ€์ƒ‰๋Ÿ‰', 0)
current_date = datetime.now()
current_month = current_date.month
current_year = current_date.year
# โœ… ์ˆ˜์ •: ์˜ฌ๋ฐ”๋ฅธ ๋กœ์ง์œผ๋กœ ์ƒ์Šนํญ ๊ณ„์‚ฐ
growth_analysis = calculate_max_growth_rate_with_predictions(trend_data_3year, keyword)
# โœ… ์ˆ˜์ •: ์˜ฌ๋ฐ”๋ฅธ ๋กœ์ง์œผ๋กœ ํ”ผํฌ์›” ๊ณ„์‚ฐ (์‹ค์ œ+์˜ˆ์ƒ ๋ฐ์ดํ„ฐ ํ™œ์šฉ)
peak_month_with_volume = get_peak_month_with_predictions(trend_data_3year, keyword)
# LLM์œผ๋กœ ์‹œ์ฆŒ ๋ถ„์„ (๊ธฐ์กด ์œ ์ง€)
if gemini_model:
product_type, entry_timing, season_detail = analyze_season_cycle_with_llm(trend_data_3year, keyword, total_volume, gemini_model)
else:
# ๊ธฐ๋ณธ๊ฐ’
product_type = "์—ฐ์ค‘์ƒํ’ˆ"
if total_volume > 50000:
product_type = "์ธ๊ธฐ์ƒํ’ˆ"
elif total_volume > 10000:
product_type = "์ค‘๊ฐ„์ƒํ’ˆ"
elif total_volume > 0:
product_type = "ํ‹ˆ์ƒˆ์ƒํ’ˆ"
# 2. ๊ด€์—ฌ๋„ ๋ถ„์„ ์ถ”๊ฐ€ - ์ดˆ๋ณด์ž๊ฐ€ ํŒ๋งค๊ฐ€๋Šฅํ•œ ์†Œ์‹ฑ ๊ธฐ์ค€ (๊ฐœ์„ ๋œ ๊ธฐ์ค€ ์ ์šฉ)
involvement_level = analyze_involvement_level(keyword, total_volume, gemini_model)
# ํŠธ๋ Œ๋“œ ๊ฒฝ๊ณ  ๋ฉ”์‹œ์ง€
trend_warning = ""
if not trend_data_3year:
trend_warning = "\n\n๐Ÿ’ก ๋” ์ •ํ™•ํ•œ ํŠธ๋ Œ๋“œ ๋ฐ์ดํ„ฐ๋ฅผ ์œ„ํ•ด \"1๋‹จ๊ณ„: ๊ธฐ๋ณธ ํ‚ค์›Œ๋“œ ์ž…๋ ฅ\"์„ ์‹คํ–‰ํ•ด๋ณด์„ธ์š”."
# ๊ฒฐ๊ณผ ํฌ๋งทํŒ… ์ˆ˜์ • - ๊ตฌ๋ถ„์„ ๊ณผ ํ•ญ๋ชฉ ๋ถ„๋ฆฌ
result_content = f"""**๐Ÿ”– ์ƒํ’ˆ์œ ํ˜•**
{product_type}
{involvement_level}
**๐Ÿ”– ๊ฐ€์žฅ ๊ฒ€์ƒ‰๋Ÿ‰์ด ๋งŽ์€ ์›”**
{peak_month_with_volume}
**๐Ÿ”– ๊ฐ€์žฅ ์ƒ์Šนํญ์ด ๋†’์€ ์›”**
{growth_analysis}{trend_warning}"""
try:
return {"status": "success", "content": result_content}
except Exception as e:
logger.error(f"์†Œ์‹ฑ์ „๋žต ๋ถ„์„ ์˜ค๋ฅ˜: {e}")
return {"status": "error", "content": "์†Œ์‹ฑ์ „๋žต ๋ถ„์„์„ ์™„๋ฃŒํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."}
def analyze_involvement_level(keyword, total_volume, gemini_model):
"""๊ด€์—ฌ๋„ ๋ถ„์„ ํ•จ์ˆ˜ - ์ดˆ๋ณด์ž๊ฐ€ ํŒ๋งค๊ฐ€๋Šฅํ•œ ์†Œ์‹ฑ ๊ธฐ์ค€"""
try:
# ๊ธฐ๋ณธ ๊ทœ์น™ ๊ธฐ๋ฐ˜ ๋ถ„์„
basic_involvement = get_basic_involvement_level(keyword, total_volume)
# Gemini๊ฐ€ ์žˆ์œผ๋ฉด LLM ๋ถ„์„๋„ ์ˆ˜ํ–‰
if gemini_model:
llm_involvement = get_llm_involvement_analysis(keyword, total_volume, gemini_model)
return llm_involvement
else:
return basic_involvement
except Exception as e:
logger.error(f"๊ด€์—ฌ๋„ ๋ถ„์„ ์˜ค๋ฅ˜: {e}")
return "๋ณตํ•ฉ๊ด€์—ฌ๋„์ƒํ’ˆ(์ƒํ’ˆ์— ๋”ฐ๋ผ ๋‹ฌ๋ผ์ง)"
def get_basic_involvement_level(keyword, total_volume):
"""๊ธฐ๋ณธ ๊ทœ์น™ ๊ธฐ๋ฐ˜ ๊ด€์—ฌ๋„ ๋ถ„์„ - ์ดˆ๋ณด์ž ํŒ๋งค ๊ด€์ """
# ์ €๊ด€์—ฌ ์ƒํ’ˆ ํ‚ค์›Œ๋“œ ํŒจํ„ด (์ดˆ๋ณด์ž ์ง„์ž… ๊ฐ€๋Šฅํ•œ ๋ถˆํŽธํ•ด์†Œ ์ œํ’ˆ)
low_involvement_keywords = [
# ๋ถˆํŽธํ•ด์†Œ/์ •๋ฆฌ์ˆ˜๋‚ฉ
"๊ฑฐ์น˜๋Œ€", "๋ฐ›์นจ๋Œ€", "์ •๋ฆฌํ•จ", "์ •๋ฆฌ๋Œ€", "์ˆ˜๋‚ฉ", "ํ™€๋”", "์Šคํƒ ๋“œ",
"์ฟ ์…˜", "๋ฒ ๊ฐœ", "๋ชฉ๋ฒ ๊ฐœ", "๋ฐฉ์„", "๋งคํŠธ", "ํŒจ๋“œ",
# ์ผ€์ด๋ธ”/์ „์„  ๊ด€๋ฆฌ
"์ผ€์ด๋ธ”", "์„ ์ •๋ฆฌ", "์ฝ”๋“œ", "์ถฉ์ „๊ธฐ", "์–ด๋Œ‘ํ„ฐ",
# ์ฒญ์†Œ/์œ„์ƒ (๋Œ€๊ธฐ์—… ์ œํ’ˆ ์ œ์™ธ)
"์ฒญ์†Œ์†”", "์ฒญ์†Œ๊ธฐ", "๊ฑธ๋ ˆ", "ํƒ€์˜ฌ", "๋ธŒ๋Ÿฌ์‹œ",
# ์ž๋™์ฐจ/์‹ค์šฉ์šฉํ’ˆ
"์ฐจ๋Ÿ‰์šฉ", "์ž๋™์ฐจ", "ํ•ธ๋“œํฐ", "์Šค๋งˆํŠธํฐ", "ํƒœ๋ธ”๋ฆฟ",
# ๊ฐ„๋‹จํ•œ ๋„๊ตฌ/์•ก์„ธ์„œ๋ฆฌ
"์ง‘๊ฒŒ", "ํ›„ํฌ", "์ž์„", "ํด๋ฆฝ", "๊ณ ๋ฆฌ", "๋ง", "ํ™€๋”",
# ๋ฏธ๋„๋Ÿผ๋ฐฉ์ง€/์•ˆ์ „
"๋ฏธ๋„๋Ÿผ", "๋…ผ์Šฌ๋ฆฝ", "๋ฐฉ์ง€", "๋ณดํ˜ธ", "์ปค๋ฒ„", "์ผ€์ด์Šค"
]
# ๊ณ ๊ด€์—ฌ ์ƒํ’ˆ ํ‚ค์›Œ๋“œ ํŒจํ„ด (๋Œ€๊ธฐ์—… ๋…์  ๋˜๋Š” ๊ณ ๊ฐ€/์ „๋ฌธ ์ œํ’ˆ)
high_involvement_keywords = [
# ๋Œ€๊ธฐ์—… ๋…์  ์ƒํ•„ํ’ˆ
"ํœด์ง€", "ํ™”์žฅ์ง€", "๋ฌผํ‹ฐ์Šˆ", "๋งˆ์Šคํฌ", "์„ธ์ œ", "์ƒดํ‘ธ", "๋ฆฐ์Šค", "๋น„๋ˆ„",
"์น˜์•ฝ", "์นซ์†”", "๊ธฐ์ €๊ท€", "์ƒ๋ฆฌ๋Œ€", "์ฝ˜๋”",
# ์‹ํ’ˆ/์Œ๋ฃŒ (๋ธŒ๋žœ๋“œ ๋ฏผ๊ฐ)
"๋ผ๋ฉด", "๊ณผ์ž", "์Œ๋ฃŒ", "์ปคํ”ผ", "์ฐจ", "์šฐ์œ ", "์š”๊ตฌ๋ฅดํŠธ",
"์Œ€", "๊น€", "์ฐธ๊ธฐ๋ฆ„", "๊ฐ„์žฅ", "๊ณ ์ถ”์žฅ", "๋œ์žฅ",
# ๊ณ ๊ฐ€ ์ „์ž์ œํ’ˆ
"๋…ธํŠธ๋ถ", "์ปดํ“จํ„ฐ", "์Šค๋งˆํŠธํฐ", "ํƒœ๋ธ”๋ฆฟ", "์นด๋ฉ”๋ผ", "TV", "๋ชจ๋‹ˆํ„ฐ",
"๋ƒ‰์žฅ๊ณ ", "์„ธํƒ๊ธฐ", "์—์–ด์ปจ", "์ฒญ์†Œ๊ธฐ", "์ „์ž๋ ˆ์ธ์ง€",
# ์˜๋ฃŒ/๊ฑด๊ฐ• (์ธ์ฆ ํ•„์š”)
"์˜๋ฃŒ", "๊ฑด๊ฐ•์‹ํ’ˆ", "์˜์–‘์ œ", "๋น„ํƒ€๋ฏผ", "์•ฝ", "์˜์•ฝํ’ˆ",
# ๋ช…ํ’ˆ/๋ธŒ๋žœ๋“œ
"๋ช…ํ’ˆ", "๋ธŒ๋žœ๋“œ", "๋Ÿญ์…”๋ฆฌ", "์‹œ๊ณ„", "๋ณด์„", "๊ธˆ", "์€", "๋‹ค์ด์•„๋ชฌ๋“œ"
]
keyword_lower = keyword.lower()
# ์ €๊ด€์—ฌ ์ƒํ’ˆ ์ฒดํฌ (๋ถˆํŽธํ•ด์†Œ ํ‚ค์›Œ๋“œ ์šฐ์„ )
for low_kw in low_involvement_keywords:
if low_kw in keyword_lower:
return "์ €๊ด€์—ฌ์ƒํ’ˆ(์ดˆ๋ณด์ž์šฉ)"
# ๊ณ ๊ด€์—ฌ ์ƒํ’ˆ ์ฒดํฌ (๋Œ€๊ธฐ์—… ๋…์ /๋ธŒ๋žœ๋“œ ๋ฏผ๊ฐ ํ‚ค์›Œ๋“œ)
for high_kw in high_involvement_keywords:
if high_kw in keyword_lower:
return "๊ณ ๊ด€์—ฌ์ƒํ’ˆ(๊ณ ๊ธ‰์ž์šฉ)"
# ๊ฒ€์ƒ‰๋Ÿ‰ ๊ธฐ๋ฐ˜ ์ถ”๊ฐ€ ํŒ๋‹จ
if total_volume > 100000:
# ๊ฒ€์ƒ‰๋Ÿ‰์ด ๋งค์šฐ ๋†’์œผ๋ฉด ๋Œ€๊ธฐ์—…์ด ๊ด€์‹ฌ ๊ฐ€์งˆ ๋งŒํ•œ ์‹œ์žฅ
return "๊ณ ๊ด€์—ฌ์ƒํ’ˆ(๊ณ ๊ธ‰์ž์šฉ)"
elif total_volume > 50000:
return "๋ณตํ•ฉ๊ด€์—ฌ๋„์ƒํ’ˆ(์ƒํ’ˆ์— ๋”ฐ๋ผ ๋‹ฌ๋ผ์ง)"
elif total_volume > 5000:
return "๋ณตํ•ฉ๊ด€์—ฌ๋„์ƒํ’ˆ(์ƒํ’ˆ์— ๋”ฐ๋ผ ๋‹ฌ๋ผ์ง)"
else:
# ๊ฒ€์ƒ‰๋Ÿ‰์ด ๋‚ฎ์œผ๋ฉด ํ‹ˆ์ƒˆ ์‹œ์žฅ, ์ดˆ๋ณด์ž๋„ ์ง„์ž… ๊ฐ€๋Šฅ
return "์ €๊ด€์—ฌ์ƒํ’ˆ(์ดˆ๋ณด์ž์šฉ)"
def get_llm_involvement_analysis(keyword, total_volume, gemini_model):
"""LLM์„ ์ด์šฉํ•œ ์ •๊ตํ•œ ๊ด€์—ฌ๋„ ๋ถ„์„ - ์ดˆ๋ณด์ž ํŒ๋งค ๊ด€์  ๊ธฐ์ค€ ์ ์šฉ"""
try:
prompt = f"""
'{keyword}' ์ƒํ’ˆ์˜ ๊ด€์—ฌ๋„๋ฅผ ์ดˆ๋ณด์ž ํŒ๋งค ๊ด€์ ์—์„œ ๋ถ„์„ํ•ด์ฃผ์„ธ์š”.
๊ฒ€์ƒ‰๋Ÿ‰: {total_volume:,}ํšŒ
๊ด€์—ฌ๋„ ์ •์˜ (์ดˆ๋ณด์ž๊ฐ€ ํŒ๋งค๊ฐ€๋Šฅํ•œ ์†Œ์‹ฑ ๊ธฐ์ค€):
์ €๊ด€์—ฌ์ƒํ’ˆ(์ดˆ๋ณด์ž์šฉ):
- ๋Œ€๊ธฐ์—… ๋…์ ์ด ์—†๋Š” ์˜์—ญ
- ์ฆ‰์‹œ ๋ถˆํŽธํ•ด์†Œํ•˜๋Š” ์ œํ’ˆ (์ง€๊ธˆ ๋ฐ”๋กœ ํ•„์š”ํ•œ ๋ฌธ์ œ ํ•ด๊ฒฐ)
- ๋ธŒ๋žœ๋“œ ์ƒ๊ด€์—†์ด ๊ธฐ๋Šฅ๋งŒ ๋˜๋ฉด ๊ตฌ๋งคํ•˜๋Š” ์ œํ’ˆ
- 1๋งŒ์›~3๋งŒ์›๋Œ€ ๊ฐ€๊ฒฉ, ์†Œ๋Ÿ‰(100๊ฐœ ์ดํ•˜) ์‹œ์ž‘ ๊ฐ€๋Šฅ
- ์˜ˆ์‹œ: ๋ชฉ๋ฒ ๊ฐœ, ์Šค๋งˆํŠธํฐ๊ฑฐ์น˜๋Œ€, ์„œ๋ž์ •๋ฆฌํ•จ, ์ผ€์ด๋ธ”์ •๋ฆฌ๊ธฐ
๊ณ ๊ด€์—ฌ์ƒํ’ˆ(๊ณ ๊ธ‰์ž์šฉ):
- ๋Œ€๊ธฐ์—…/๋ธŒ๋žœ๋“œ๊ฐ€ ์‹œ์žฅ์„ ๋…์ ํ•˜๋Š” ์˜์—ญ (์ดˆ๋ณด์ž ์ง„์ž… ๋ถˆ๊ฐ€)
- ์ƒํ•„ํ’ˆ(ํœด์ง€, ์„ธ์ œ, ๋งˆ์Šคํฌ ๋“ฑ) - ๋ธŒ๋žœ๋“œ ์ถฉ์„ฑ๋„ ๋†’์Œ
- ๊ณ ๊ฐ€ ์ œํ’ˆ(10๋งŒ์› ์ด์ƒ), ์ „๋ฌธ์„ฑ/์ธ์ฆ ํ•„์š”
- ๋Œ€์ž๋ณธ ํ•„์š”ํ•œ ์•„์ดํ…œ
- ์˜ˆ์‹œ: ์ „์ž์ œํ’ˆ, ๊ฐ€์ „, ๋ธŒ๋žœ๋“œ ์ƒํ•„ํ’ˆ, ์˜๋ฃŒ์šฉํ’ˆ
๋ณตํ•ฉ๊ด€์—ฌ๋„์ƒํ’ˆ(์ƒํ’ˆ์— ๋”ฐ๋ผ ๋‹ฌ๋ผ์ง):
- ๊ฐ€๊ฒฉ๋Œ€๋ณ„๋กœ ์ €๊ฐ€ํ˜•(์ €๊ด€์—ฌ)๊ณผ ๊ณ ๊ฐ€ํ˜•(๊ณ ๊ด€์—ฌ)์ด ๊ณต์กด
- ํƒ€๊ฒŸ์ด๋‚˜ ์šฉ๋„์— ๋”ฐ๋ผ ๊ด€์—ฌ๋„๊ฐ€ ๊ทน๋ช…ํ•˜๊ฒŒ ๋‹ฌ๋ผ์ง
- ์˜ˆ์‹œ: ์˜๋ฅ˜, ์šด๋™์šฉํ’ˆ, ๋ทฐํ‹ฐ์šฉํ’ˆ ๋“ฑ
๋ณตํ•ฉ๊ด€์—ฌ๋„์ƒํ’ˆ์œผ๋กœ ํŒ๋‹จํ•  ๊ฒฝ์šฐ, ๋ฐ˜๋“œ์‹œ ๊ตฌ์ฒด์ ์ธ ์ด์œ ๋ฅผ ์„ค๋ช…ํ•˜์„ธ์š”:
- ๊ฐ€๊ฒฉ๋Œ€๋ณ„ ๋ถ„ํ™”: "1-3๋งŒ์› ์ค‘๊ตญ์‚ฐ(์ €๊ด€์—ฌ) vs 10-15๋งŒ์› ๊ตญ์‚ฐ ์ˆ˜์ œ(๊ณ ๊ด€์—ฌ)"
- ํƒ€๊ฒŸ๋ณ„ ์ฐจ์ด: "์ผ๋ฐ˜์ธ์€ ์ €๊ด€์—ฌ vs ์ „๋ฌธ๊ฐ€๋Š” ๊ณ ๊ด€์—ฌ"
- ์šฉ๋„๋ณ„ ์ฐจ์ด: "์ž„์‹œ์šฉ์€ ์ €๊ด€์—ฌ vs ์žฅ๊ธฐ์šฉ์€ ๊ณ ๊ด€์—ฌ"
๋‹ค์Œ ํ˜•์‹์œผ๋กœ ๋‹ต๋ณ€ํ•˜์„ธ์š”:
[๊ด€์—ฌ๋„ ์„ ํƒ]
[๊ตฌ์ฒด์ ์ธ ํŒ๋‹จ ์ด์œ  - ๊ฐ€๊ฒฉ๋Œ€/ํƒ€๊ฒŸ/๋ธŒ๋žœ๋“œ ๋…์  ์—ฌ๋ถ€ ๋“ฑ์„ ๋ช…ํ™•ํžˆ ์ œ์‹œ]
์„ ํƒ์ง€:
์ €๊ด€์—ฌ์ƒํ’ˆ(์ดˆ๋ณด์ž์šฉ)
๋ณตํ•ฉ๊ด€์—ฌ๋„์ƒํ’ˆ(์ƒํ’ˆ์— ๋”ฐ๋ผ ๋‹ฌ๋ผ์ง)
๊ณ ๊ด€์—ฌ์ƒํ’ˆ(๊ณ ๊ธ‰์ž์šฉ)
"""
response = gemini_model.generate_content(prompt)
result = response.text.strip()
# ๊ฒฐ๊ณผ ํ•„ํ„ฐ๋ง - ์ •ํ™•ํ•œ ํ˜•์‹๋งŒ ํ—ˆ์šฉ
if "์ €๊ด€์—ฌ์ƒํ’ˆ(์ดˆ๋ณด์ž์šฉ)" in result:
return "์ €๊ด€์—ฌ์ƒํ’ˆ(์ดˆ๋ณด์ž์šฉ)"
elif "๊ณ ๊ด€์—ฌ์ƒํ’ˆ(๊ณ ๊ธ‰์ž์šฉ)" in result:
return "๊ณ ๊ด€์—ฌ์ƒํ’ˆ(๊ณ ๊ธ‰์ž์šฉ)"
elif "๋ณตํ•ฉ๊ด€์—ฌ๋„์ƒํ’ˆ(์ƒํ’ˆ์— ๋”ฐ๋ผ ๋‹ฌ๋ผ์ง)" in result:
return "๋ณตํ•ฉ๊ด€์—ฌ๋„์ƒํ’ˆ(์ƒํ’ˆ์— ๋”ฐ๋ผ ๋‹ฌ๋ผ์ง)"
else:
# LLM ์‘๋‹ต์ด ๋ถ€์ •ํ™•ํ•œ ๊ฒฝ์šฐ ๊ธฐ๋ณธ ๊ทœ์น™์œผ๋กœ ํด๋ฐฑ
return get_basic_involvement_level(keyword, total_volume)
except Exception as e:
logger.error(f"LLM ๊ด€์—ฌ๋„ ๋ถ„์„ ์˜ค๋ฅ˜: {e}")
return get_basic_involvement_level(keyword, total_volume)
class CompactKeywordAnalyzer:
"""๊ฐ„๊ฒฐํ•œ 7๋‹จ๊ณ„ ํ‚ค์›Œ๋“œ ๋ถ„์„๊ธฐ"""
def __init__(self, gemini_model):
self.gemini_model = gemini_model
self.max_retries = 3
def call_llm_with_retry(self, prompt: str, step_name: str = "") -> str:
"""์žฌ์‹œ๋„ ๋กœ์ง์ด ์ ์šฉ๋œ LLM ํ˜ธ์ถœ"""
last_error = None
for attempt in range(self.max_retries):
try:
logger.info(f"{step_name} ์‹œ๋„ {attempt + 1}/{self.max_retries}")
response = self.gemini_model.generate_content(prompt)
result = response.text.strip()
if result and len(result) > 20:
logger.info(f"{step_name} ์„ฑ๊ณต")
return result
else:
raise Exception("์‘๋‹ต์ด ๋„ˆ๋ฌด ์งง๊ฑฐ๋‚˜ ๋น„์–ด์žˆ์Œ")
except Exception as e:
last_error = e
logger.warning(f"{step_name} ์‹คํŒจ (์‹œ๋„ {attempt + 1}): {e}")
if attempt < self.max_retries - 1:
delay = 1.0 * (attempt + 1) + random.uniform(0, 0.5)
time.sleep(delay)
logger.error(f"{step_name} ๋ชจ๋“  ์žฌ์‹œ๋„ ์‹คํŒจ: {last_error}")
return f"{step_name} ๋ถ„์„์„ ์™„๋ฃŒํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."
def clean_markdown_and_bold(self, text: str) -> str:
"""๋งˆํฌ๋‹ค์šด๊ณผ ๋ณผ๋“œ ์ฒ˜๋ฆฌ๋ฅผ ์™„์ „ํžˆ ์ œ๊ฑฐ"""
text = re.sub(r'\*\*(.+?)\*\*', r'\1', text)
text = re.sub(r'\*(.+?)\*', r'\1', text)
text = re.sub(r'__(.+?)__', r'\1', text)
text = re.sub(r'_(.+?)_', r'\1', text)
text = re.sub(r'##\s*(.+)', r'\1', text)
text = re.sub(r'#\s*(.+)', r'\1', text)
text = re.sub(r'\*+', '', text)
text = re.sub(r'_+', '', text)
return text.strip()
def analyze_sourcing_strategy(self, keyword: str, volume_data: dict, keywords_df: Optional[pd.DataFrame], trend_data_1year=None, trend_data_3year=None) -> str:
"""๊ฐœ์„ ๋œ ์†Œ์‹ฑ์ „๋žต ๋ถ„์„ - ๊ฒฝ์Ÿ๋ฐ์ดํ„ฐ ์ œ๊ฑฐ"""
try:
sourcing_analysis = analyze_sourcing_strategy_improved(
keyword, volume_data, trend_data_1year, trend_data_3year, keywords_df, self.gemini_model
)
if sourcing_analysis["status"] == "success":
return self.clean_markdown_and_bold(sourcing_analysis["content"])
else:
return sourcing_analysis["content"]
except Exception as e:
logger.error(f"์†Œ์‹ฑ์ „๋žต ๋ถ„์„ ์˜ค๋ฅ˜: {e}")
return "์†Œ์‹ฑ์ „๋žต ๋ถ„์„์„ ์™„๋ฃŒํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."
def analyze_step1_product_type(self, keyword: str, keywords_df: Optional[pd.DataFrame]) -> str:
"""1๋‹จ๊ณ„. ์ƒํ’ˆ์œ ํ˜• ๋ถ„์„"""
related_keywords = ""
if keywords_df is not None and not keywords_df.empty:
top_keywords = keywords_df.head(10)['์กฐํ•ฉ ํ‚ค์›Œ๋“œ'].tolist()
related_keywords = f"์—ฐ๊ด€ํ‚ค์›Œ๋“œ: {', '.join(top_keywords)}"
prompt = f"""
๋‹น์‹ ์€ ์ดˆ๋ณด ์…€๋Ÿฌ๊ฐ€ ์ƒํ’ˆ ํŒ๋งค ์„ฑ๊ณต์„ ๋น ๋ฅด๊ฒŒ ์ด๋ฃฐ ์ˆ˜ ์žˆ๋„๋ก ๋•๋Š” ์ตœ๊ณ ์˜ ์ƒํ’ˆ ์†Œ์‹ฑ ๋ฐ ์ƒํ’ˆ๊ธฐํš ์ปจ์„คํ„ดํŠธ AI์ž…๋‹ˆ๋‹ค.
๋ถ„์„ ํ‚ค์›Œ๋“œ: '{keyword}'
{related_keywords}
1๋‹จ๊ณ„. ์ƒํ’ˆ์œ ํ˜• ๋ถ„์„
์ƒํ’ˆ ์œ ํ˜• ๋ถ„๋ฅ˜ ๊ธฐ์ค€:
- ๋ถˆํŽธํ•ด๊ฒฐ์ƒํ’ˆ: ํŠน์ • ๋ฌธ์ œ๋‚˜ ๋ถˆํŽธํ•จ์„ ์ฆ‰๊ฐ ํ•ด๊ฒฐํ•˜๋Š” ์ œํ’ˆ
- ์—…๊ทธ๋ ˆ์ด๋“œ์ƒํ’ˆ: ์‚ถ์˜ ์งˆ๊ณผ ๋งŒ์กฑ๋„๋ฅผ ํ–ฅ์ƒ์‹œํ‚ค๋Š” ์ œํ’ˆ
- ํ•„์ˆ˜์ƒํ’ˆ: ์ผ์ƒ์—์„œ ๋ฐ˜๋“œ์‹œ ํ•„์š”ํ•˜๊ณ  ๋ฐ˜๋ณต ๊ตฌ๋งค๋˜๋Š” ์ œํ’ˆ
- ์ทจํ–ฅ์ €๊ฒฉ์ƒํ’ˆ: ๊ฐ์„ฑ์ , ๊ฐœ์„ฑ์  ์š•๊ตฌ๋ฅผ ์ž๊ทนํ•˜๋Š” ์ œํ’ˆ
- ์œตํ•ฉ์ƒํ’ˆ: ์œ„ 2๊ฐœ ์ด์ƒ์˜ ์œ ํ˜•์ด ๊ฒฐํ•ฉ๋œ ์ œํ’ˆ
๋‹ค์Œ ํ˜•์‹์œผ๋กœ ๋ถ„์„ํ•ด์ฃผ์„ธ์š” (๋ณผ๋“œ, ๋งˆํฌ๋‹ค์šด ์‚ฌ์šฉ ๊ธˆ์ง€):
์ฃผ์š”์œ ํ˜•: [์œ ํ˜•๋ช…]
{keyword}๋Š” [๊ตฌ์ฒด์  ์„ค๋ช… - ์™œ ์ด ์œ ํ˜•์ธ์ง€ ๋ณธ์งˆ์  ๊ฐ€์น˜์™€ ํ•ด๊ฒฐํ•˜๋Š” ๋ฌธ์ œ๋ฅผ ์ค‘์‹ฌ์œผ๋กœ 2-3๋ฌธ์žฅ]
๋ณด์กฐ์œ ํ˜•: [ํ•ด๋‹น ์œ ํ˜•๋“ค]
[์œ ํ˜•1]
- [์ด ์œ ํ˜•์— ํ•ด๋‹นํ•˜๋Š” ์ด์œ  1๋ฌธ์žฅ]
[์œ ํ˜•2]
- [์ด ์œ ํ˜•์— ํ•ด๋‹นํ•˜๋Š” ์ด์œ  1๋ฌธ์žฅ]
"""
result = self.call_llm_with_retry(prompt, f"1๋‹จ๊ณ„-์ƒํ’ˆ์œ ํ˜•๋ถ„์„-{keyword}")
return self.clean_markdown_and_bold(result)
def analyze_step2_target_customer(self, keyword: str, step1_result: str) -> str:
"""2๋‹จ๊ณ„. ์†Œ๋น„์ž ํƒ€๊ฒŸ ์„ค์ •"""
prompt = f"""
๋‹น์‹ ์€ ์ดˆ๋ณด ์…€๋Ÿฌ๊ฐ€ ์ƒํ’ˆ ํŒ๋งค ์„ฑ๊ณต์„ ๋น ๋ฅด๊ฒŒ ์ด๋ฃฐ ์ˆ˜ ์žˆ๋„๋ก ๋•๋Š” ์ตœ๊ณ ์˜ ์ƒํ’ˆ ์†Œ์‹ฑ ๋ฐ ์ƒํ’ˆ๊ธฐํš ์ปจ์„คํ„ดํŠธ AI์ž…๋‹ˆ๋‹ค.
๋ถ„์„ ํ‚ค์›Œ๋“œ: '{keyword}'
์ด์ „ ๋ถ„์„ ๊ฒฐ๊ณผ:
{step1_result}
2๋‹จ๊ณ„. ์†Œ๋น„์ž ํƒ€๊ฒŸ ์„ค์ •
๋‹ค์Œ ํ˜•์‹์œผ๋กœ ๊ฐ„๊ฒฐํ•˜๊ฒŒ ๋ถ„์„ํ•ด์ฃผ์„ธ์š” (๋ณผ๋“œ, ๋งˆํฌ๋‹ค์šด ์‚ฌ์šฉ ๊ธˆ์ง€):
๊ณ ๊ฐ์ƒํ™ฉ
- [๊ตฌ์ฒด์ ์ธ ๊ตฌ๋งค ์ƒํ™ฉ๋“ค์„ ๊ฐ„๋‹จํžˆ]
ํŽ˜๋ฅด์†Œ๋‚˜
- [์—ฐ๋ น๋Œ€, ์„ฑ๋ณ„, ๋ผ์ดํ”„์Šคํƒ€์ผ์„ ํ†ตํ•ฉํ•˜์—ฌ 1-2์ค„๋กœ ๊ฐ„๊ฒฐํ•˜๊ฒŒ]
์ฃผ์š” ๋‹ˆ์ฆˆ
- [ํ•ต์‹ฌ ๋‹ˆ์ฆˆ ํ•œ์ค„๋งŒ]
"""
result = self.call_llm_with_retry(prompt, f"2๋‹จ๊ณ„-ํƒ€๊ฒŸ์„ค์ •-{keyword}")
return self.clean_markdown_and_bold(result)
def analyze_step3_sourcing_strategy(self, keyword: str, previous_results: str) -> str:
"""3๋‹จ๊ณ„. ํƒ€๊ฒŸ๋ณ„ ์ฐจ๋ณ„ํ™”๋œ ์†Œ์‹ฑ ์ „๋žต ์ œ์•ˆ"""
prompt = f"""
๋‹น์‹ ์€ ์ดˆ๋ณด ์…€๋Ÿฌ๊ฐ€ ์ƒํ’ˆ ํŒ๋งค ์„ฑ๊ณต์„ ๋น ๋ฅด๊ฒŒ ์ด๋ฃฐ ์ˆ˜ ์žˆ๋„๋ก ๋•๋Š” ์ตœ๊ณ ์˜ ์ƒํ’ˆ ์†Œ์‹ฑ ๋ฐ ์ƒํ’ˆ๊ธฐํš ์ปจ์„คํ„ดํŠธ AI์ž…๋‹ˆ๋‹ค.
๋ถ„์„ ํ‚ค์›Œ๋“œ: '{keyword}'
์ด์ „ ๋ถ„์„ ๊ฒฐ๊ณผ:
{previous_results}
3๋‹จ๊ณ„. ํƒ€๊ฒŸ๋ณ„ ์ฐจ๋ณ„ํ™”๋œ ์†Œ์‹ฑ ์ „๋žต ์ œ์•ˆ
ํ˜„์‹ค์ ์œผ๋กœ ์˜จ๋ผ์ธ์—์„œ ์†Œ์‹ฑ ๊ฐ€๋Šฅํ•œ ์ฐจ๋ณ„ํ™” ์ „๋žต์„ ์ œ์•ˆํ•ด์ฃผ์„ธ์š”.
๋‹ค์Œ ํ˜•์‹์œผ๋กœ ๋ถ„์„ํ•ด์ฃผ์„ธ์š” (๋ณผ๋“œ, ๋งˆํฌ๋‹ค์šด ์‚ฌ์šฉ ๊ธˆ์ง€):
ํ•ต์‹ฌ ๊ตฌ๋งค ๊ณ ๋ ค ์š”์†Œ 5๊ฐ€์ง€
1. [์š”์†Œ1 ๊ฐ„๋‹จํžˆ]
2. [์š”์†Œ2 ๊ฐ„๋‹จํžˆ]
3. [์š”์†Œ3 ๊ฐ„๋‹จํžˆ]
4. [์š”์†Œ4 ๊ฐ„๋‹จํžˆ]
5. [์š”์†Œ5 ๊ฐ„๋‹จํžˆ]
์ฐจ๋ณ„ํ™” ์†Œ์‹ฑ ์ „๋žต
1. [์ „๋žต๋ช…]
- [ํ˜„์‹ค์ ์œผ๋กœ ์†Œ์‹ฑ ๊ฐ€๋Šฅํ•œ ๊ตฌ์ฒด์  ๋ฐฉ๋ฒ• ํ•œ์ค„]
2. [์ „๋žต๋ช…]
- [ํ˜„์‹ค์ ์œผ๋กœ ์†Œ์‹ฑ ๊ฐ€๋Šฅํ•œ ๊ตฌ์ฒด์  ๋ฐฉ๋ฒ• ํ•œ์ค„]
3. [์ „๋žต๋ช…]
- [ํ˜„์‹ค์ ์œผ๋กœ ์†Œ์‹ฑ ๊ฐ€๋Šฅํ•œ ๊ตฌ์ฒด์  ๋ฐฉ๋ฒ• ํ•œ์ค„]
4. [์ „๋žต๋ช…]
- [ํ˜„์‹ค์ ์œผ๋กœ ์†Œ์‹ฑ ๊ฐ€๋Šฅํ•œ ๊ตฌ์ฒด์  ๋ฐฉ๋ฒ• ํ•œ์ค„]
5. [์ „๋žต๋ช…]
- [ํ˜„์‹ค์ ์œผ๋กœ ์†Œ์‹ฑ ๊ฐ€๋Šฅํ•œ ๊ตฌ์ฒด์  ๋ฐฉ๋ฒ• ํ•œ์ค„]
"""
result = self.call_llm_with_retry(prompt, f"3๋‹จ๊ณ„-์†Œ์‹ฑ์ „๋žต-{keyword}")
return self.clean_markdown_and_bold(result)
def analyze_step4_product_recommendation(self, keyword: str, previous_results: str) -> str:
"""4๋‹จ๊ณ„. ์ฐจ๋ณ„ํ™” ์˜ˆ์‹œ๋ณ„ ์ƒํ’ˆ 5๊ฐ€์ง€ ์ถ”์ฒœ"""
prompt = f"""
๋‹น์‹ ์€ ์ดˆ๋ณด ์…€๋Ÿฌ๊ฐ€ ์ƒํ’ˆ ํŒ๋งค ์„ฑ๊ณต์„ ๋น ๋ฅด๊ฒŒ ์ด๋ฃฐ ์ˆ˜ ์žˆ๋„๋ก ๋•๋Š” ์ตœ๊ณ ์˜ ์ƒํ’ˆ ์†Œ์‹ฑ ๋ฐ ์ƒํ’ˆ๊ธฐํš ์ปจ์„คํ„ดํŠธ AI์ž…๋‹ˆ๋‹ค.
๋ถ„์„ ํ‚ค์›Œ๋“œ: '{keyword}'
์ด์ „ ๋ถ„์„ ๊ฒฐ๊ณผ:
{previous_results}
4๋‹จ๊ณ„. ์ฐจ๋ณ„ํ™” ์˜ˆ์‹œ๋ณ„ ์ƒํ’ˆ 5๊ฐ€์ง€ ์ถ”์ฒœ
3๋‹จ๊ณ„์—์„œ ๋„์ถœํ•œ ์ฐจ๋ณ„ํ™” ์š”์†Œ๋ฅผ ๋ฐ˜์˜ํ•˜์—ฌ ๋งค์ถœ ๊ฐ€๋Šฅ์„ฑ์ด ๋†’์€ ์ˆœ์„œ๋Œ€๋กœ ๋ถ„์„ํ•ด์ฃผ์„ธ์š”.
๋‹ค์Œ ํ˜•์‹์œผ๋กœ ๋ถ„์„ํ•ด์ฃผ์„ธ์š” (๋ณผ๋“œ, ๋งˆํฌ๋‹ค์šด ์‚ฌ์šฉ ๊ธˆ์ง€):
์ฐจ๋ณ„ํ™” ์ƒํ’ˆ ์ถ”์ฒœ
1. [๊ตฌ์ฒด์ ์ธ ์ƒํ’ˆ๋ช…๊ณผ ์„ธ๋ถ€ ํŠน์ง•]
- [์ฃผ์š” ํŠน์ง•๋“ค, ํƒ€๊ฒŸ ๊ณ ๊ฐ, ์ฐจ๋ณ„ํ™” ํฌ์ธํŠธ๋ฅผ ํ•œ๋ฌธ์žฅ์œผ๋กœ]
2. [๊ตฌ์ฒด์ ์ธ ์ƒํ’ˆ๋ช…๊ณผ ์„ธ๋ถ€ ํŠน์ง•]
- [์ฃผ์š” ํŠน์ง•๋“ค, ํƒ€๊ฒŸ ๊ณ ๊ฐ, ์ฐจ๋ณ„ํ™” ํฌ์ธํŠธ๋ฅผ ํ•œ๋ฌธ์žฅ์œผ๋กœ]
3. [๊ตฌ์ฒด์ ์ธ ์ƒํ’ˆ๋ช…๊ณผ ์„ธ๋ถ€ ํŠน์ง•]
- [์ฃผ์š” ํŠน์ง•๋“ค, ํƒ€๊ฒŸ ๊ณ ๊ฐ, ์ฐจ๋ณ„ํ™” ํฌ์ธํŠธ๋ฅผ ํ•œ๋ฌธ์žฅ์œผ๋กœ]
4. [๊ตฌ์ฒด์ ์ธ ์ƒํ’ˆ๋ช…๊ณผ ์„ธ๋ถ€ ํŠน์ง•]
- [์ฃผ์š” ํŠน์ง•๋“ค, ํƒ€๊ฒŸ ๊ณ ๊ฐ, ์ฐจ๋ณ„ํ™” ํฌ์ธํŠธ๋ฅผ ํ•œ๋ฌธ์žฅ์œผ๋กœ]
5. [๊ตฌ์ฒด์ ์ธ ์ƒํ’ˆ๋ช…๊ณผ ์„ธ๋ถ€ ํŠน์ง•]
- [์ฃผ์š” ํŠน์ง•๋“ค, ํƒ€๊ฒŸ ๊ณ ๊ฐ, ์ฐจ๋ณ„ํ™” ํฌ์ธํŠธ๋ฅผ ํ•œ๋ฌธ์žฅ์œผ๋กœ]
๋Œ€ํ‘œ์ด๋ฏธ์ง€ ์ถ”์ฒœ
1. [์ฒซ ๋ฒˆ์งธ ์ƒํ’ˆ๋ช…]
* [๊ฐ„๋‹จํ•œ ์ดฌ์˜ ์ปจ์…‰๊ณผ ํ•ต์‹ฌ ํฌ์ธํŠธ ํ•œ์ค„]
2. [๋‘ ๋ฒˆ์งธ ์ƒํ’ˆ๋ช…]
* [๊ฐ„๋‹จํ•œ ์ดฌ์˜ ์ปจ์…‰๊ณผ ํ•ต์‹ฌ ํฌ์ธํŠธ ํ•œ์ค„]
3. [์„ธ ๋ฒˆ์งธ ์ƒํ’ˆ๋ช…]
* [๊ฐ„๋‹จํ•œ ์ดฌ์˜ ์ปจ์…‰๊ณผ ํ•ต์‹ฌ ํฌ์ธํŠธ ํ•œ์ค„]
4. [๋„ค ๋ฒˆ์งธ ์ƒํ’ˆ๋ช…]
* [๊ฐ„๋‹จํ•œ ์ดฌ์˜ ์ปจ์…‰๊ณผ ํ•ต์‹ฌ ํฌ์ธํŠธ ํ•œ์ค„]
5. [๋‹ค์„ฏ ๋ฒˆ์งธ ์ƒํ’ˆ๋ช…]
* [๊ฐ„๋‹จํ•œ ์ดฌ์˜ ์ปจ์…‰๊ณผ ํ•ต์‹ฌ ํฌ์ธํŠธ ํ•œ์ค„]
"""
result = self.call_llm_with_retry(prompt, f"4๋‹จ๊ณ„-์ƒํ’ˆ์ถ”์ฒœ-{keyword}")
return self.clean_markdown_and_bold(result)
def analyze_step5_trust_building(self, keyword: str, previous_results: str) -> str:
"""5๋‹จ๊ณ„. ์‹ ๋ขฐ์„ฑ์„ ์ค„ ์ˆ˜ ์žˆ๋Š” ์š”์†Œ 5๊ฐ€์ง€"""
prompt = f"""
๋‹น์‹ ์€ ์ดˆ๋ณด ์…€๋Ÿฌ๊ฐ€ ์ƒํ’ˆ ํŒ๋งค ์„ฑ๊ณต์„ ๋น ๋ฅด๊ฒŒ ์ด๋ฃฐ ์ˆ˜ ์žˆ๋„๋ก ๋•๋Š” ์ตœ๊ณ ์˜ ์ƒํ’ˆ ์†Œ์‹ฑ ๋ฐ ์ƒํ’ˆ๊ธฐํš ์ปจ์„คํ„ดํŠธ AI์ž…๋‹ˆ๋‹ค.
๋ถ„์„ ํ‚ค์›Œ๋“œ: '{keyword}'
์ด์ „ ๋ถ„์„ ๊ฒฐ๊ณผ:
{previous_results}
5๋‹จ๊ณ„. ์‹ ๋ขฐ์„ฑ์„ ์ค„ ์ˆ˜ ์žˆ๋Š” ์š”์†Œ 5๊ฐ€์ง€
๋‹ค์Œ ํ˜•์‹์œผ๋กœ ๋ถ„์„ํ•ด์ฃผ์„ธ์š” (๋ณผ๋“œ, ๋งˆํฌ๋‹ค์šด ์‚ฌ์šฉ ๊ธˆ์ง€):
1. [์‹ ๋ขฐ์„ฑ ์š”์†Œ1]
- [๊ตฌ์ฒด์  ๋ฐฉ๋ฒ•๊ณผ ์ ์šฉ ์˜ˆ์‹œ]
2. [์‹ ๋ขฐ์„ฑ ์š”์†Œ2]
- [๊ตฌ์ฒด์  ๋ฐฉ๋ฒ•๊ณผ ์ ์šฉ ์˜ˆ์‹œ]
3. [์‹ ๋ขฐ์„ฑ ์š”์†Œ3]
- [๊ตฌ์ฒด์  ๋ฐฉ๋ฒ•๊ณผ ์ ์šฉ ์˜ˆ์‹œ]
4. [์‹ ๋ขฐ์„ฑ ์š”์†Œ4]
- [๊ตฌ์ฒด์  ๋ฐฉ๋ฒ•๊ณผ ์ ์šฉ ์˜ˆ์‹œ]
5. [์‹ ๋ขฐ์„ฑ ์š”์†Œ5]
- [๊ตฌ์ฒด์  ๋ฐฉ๋ฒ•๊ณผ ์ ์šฉ ์˜ˆ์‹œ]
"""
result = self.call_llm_with_retry(prompt, f"5๋‹จ๊ณ„-์‹ ๋ขฐ์„ฑ๊ตฌ์ถ•-{keyword}")
return self.clean_markdown_and_bold(result)
def analyze_step6_usp_development(self, keyword: str, previous_results: str) -> str:
"""6๋‹จ๊ณ„. ์ฐจ๋ณ„ํ™” ์˜ˆ์‹œ๋ณ„ USP 5๊ฐ€์ง€"""
prompt = f"""
๋‹น์‹ ์€ ์ดˆ๋ณด ์…€๋Ÿฌ๊ฐ€ ์ƒํ’ˆ ํŒ๋งค ์„ฑ๊ณต์„ ๋น ๋ฅด๊ฒŒ ์ด๋ฃฐ ์ˆ˜ ์žˆ๋„๋ก ๋•๋Š” ์ตœ๊ณ ์˜ ์ƒํ’ˆ ์†Œ์‹ฑ ๋ฐ ์ƒํ’ˆ๊ธฐํš ์ปจ์„คํ„ดํŠธ AI์ž…๋‹ˆ๋‹ค.
๋ถ„์„ ํ‚ค์›Œ๋“œ: '{keyword}'
์ด์ „ ๋ถ„์„ ๊ฒฐ๊ณผ:
{previous_results}
6๋‹จ๊ณ„. ์ฐจ๋ณ„ํ™” ์˜ˆ์‹œ๋ณ„ USP 5๊ฐ€์ง€
4๋‹จ๊ณ„์—์„œ ์ถ”์ฒœํ•œ 5๊ฐ€์ง€ ์ƒํ’ˆ๊ณผ ์—ฐ๊ฒฐํ•˜์—ฌ ๊ฐ๊ฐ์˜ USP๋ฅผ ์ œ์‹œํ•ด์ฃผ์„ธ์š”.
๋‹ค์Œ ํ˜•์‹์œผ๋กœ ๋ถ„์„ํ•ด์ฃผ์„ธ์š” (๋ณผ๋“œ, ๋งˆํฌ๋‹ค์šด ์‚ฌ์šฉ ๊ธˆ์ง€):
1. [์ฒซ ๋ฒˆ์งธ ์ƒํ’ˆ์˜ USP ์ œ๋ชฉ]
- [ํ•ต์‹ฌ ๊ฐ€์น˜ ์ œ์•ˆ๊ณผ ์ฐจ๋ณ„ํ™” ํฌ์ธํŠธ ๊ตฌ์ฒด์  ์„ค๋ช…]
2. [๋‘ ๋ฒˆ์งธ ์ƒํ’ˆ์˜ USP ์ œ๋ชฉ]
- [ํ•ต์‹ฌ ๊ฐ€์น˜ ์ œ์•ˆ๊ณผ ์ฐจ๋ณ„ํ™” ํฌ์ธํŠธ ๊ตฌ์ฒด์  ์„ค๋ช…]
3. [์„ธ ๋ฒˆ์งธ ์ƒํ’ˆ์˜ USP ์ œ๋ชฉ]
- [ํ•ต์‹ฌ ๊ฐ€์น˜ ์ œ์•ˆ๊ณผ ์ฐจ๋ณ„ํ™” ํฌ์ธํŠธ ๊ตฌ์ฒด์  ์„ค๋ช…]
4. [๋„ค ๋ฒˆ์งธ ์ƒํ’ˆ์˜ USP ์ œ๋ชฉ]
- [ํ•ต์‹ฌ ๊ฐ€์น˜ ์ œ์•ˆ๊ณผ ์ฐจ๋ณ„ํ™” ํฌ์ธํŠธ ๊ตฌ์ฒด์  ์„ค๋ช…]
5. [๋‹ค์„ฏ ๋ฒˆ์งธ ์ƒํ’ˆ์˜ USP ์ œ๋ชฉ]
- [ํ•ต์‹ฌ ๊ฐ€์น˜ ์ œ์•ˆ๊ณผ ์ฐจ๋ณ„ํ™” ํฌ์ธํŠธ ๊ตฌ์ฒด์  ์„ค๋ช…]
"""
result = self.call_llm_with_retry(prompt, f"6๋‹จ๊ณ„-USP๊ฐœ๋ฐœ-{keyword}")
return self.clean_markdown_and_bold(result)
def analyze_step7_copy_creation(self, keyword: str, previous_results: str) -> str:
"""7๋‹จ๊ณ„. USP๋ณ„ ์ƒ์„ธํŽ˜์ด์ง€ ํ—ค๋“œ ์นดํ”ผ - ์ด๋ชจํ‹ฐ์ฝ˜ ์ œ๊ฑฐ"""
prompt = f"""
๋‹น์‹ ์€ ์ดˆ๋ณด ์…€๋Ÿฌ๊ฐ€ ์ƒํ’ˆ ํŒ๋งค ์„ฑ๊ณต์„ ๋น ๋ฅด๊ฒŒ ์ด๋ฃฐ ์ˆ˜ ์žˆ๋„๋ก ๋•๋Š” ์ตœ๊ณ ์˜ ์ƒํ’ˆ ์†Œ์‹ฑ ๋ฐ ์ƒํ’ˆ๊ธฐํš ์ปจ์„คํ„ดํŠธ AI์ž…๋‹ˆ๋‹ค.
๋ถ„์„ ํ‚ค์›Œ๋“œ: '{keyword}'
์ด์ „ ๋ถ„์„ ๊ฒฐ๊ณผ:
{previous_results}
7๋‹จ๊ณ„. USP๋ณ„ ์ƒ์„ธํŽ˜์ด์ง€ ํ—ค๋“œ ์นดํ”ผ
6๋‹จ๊ณ„์—์„œ ์ œ์‹œํ•œ 5๊ฐ€์ง€ USP์™€ ์—ฐ๊ฒฐํ•˜์—ฌ ๊ฐ๊ฐ์˜ ํ—ค๋“œ ์นดํ”ผ๋ฅผ ์ œ์‹œํ•ด์ฃผ์„ธ์š”.
๋‹ค์Œ ํ˜•์‹์œผ๋กœ ๋ถ„์„ํ•ด์ฃผ์„ธ์š” (๋ณผ๋“œ, ๋งˆํฌ๋‹ค์šด, ์ด๋ชจํ‹ฐ์ฝ˜ ์‚ฌ์šฉ ๊ธˆ์ง€):
1. [์ฒซ ๋ฒˆ์งธ USP ์—ฐ๊ฒฐ ์นดํ”ผ]
2. [๋‘ ๋ฒˆ์งธ USP ์—ฐ๊ฒฐ ์นดํ”ผ]
3. [์„ธ ๋ฒˆ์งธ USP ์—ฐ๊ฒฐ ์นดํ”ผ]
4. [๋„ค ๋ฒˆ์งธ USP ์—ฐ๊ฒฐ ์นดํ”ผ]
5. [๋‹ค์„ฏ ๋ฒˆ์งธ USP ์—ฐ๊ฒฐ ์นดํ”ผ]
์ค‘์š”:
- 30์ž ๋ฏธ๋งŒ์˜ ๊ฐ„๊ฒฐํ•œ ํ›„ํ‚น ๋ฌธ์žฅ๋งŒ ์ถœ๋ ฅ
- ์ด๋ชจํ‹ฐ์ฝ˜ ์ ˆ๋Œ€ ์‚ฌ์šฉ ๊ธˆ์ง€ (๐Ÿ˜Ž, ๐ŸŽจ, โœจ, ๐ŸŽ, ๐Ÿ‘ ๋“ฑ)
- ์ƒํ’ˆ ํŒ๋งค๋ฅผ ์œ„ํ•œ ์ˆœ์ˆ˜ ํ—ค๋“œ์นดํ”ผ๋งŒ ์ž‘์„ฑ
"""
result = self.call_llm_with_retry(prompt, f"7๋‹จ๊ณ„-์นดํ”ผ์ œ์ž‘-{keyword}")
return self.clean_markdown_and_bold(result)
def analyze_conclusion_enhanced(self, keyword: str, previous_results: str, sourcing_strategy_result: str) -> str:
"""๊ฐœ์„ ๋œ ๊ฒฐ๋ก  ๋ถ„์„ - ๊ตฌ์ฒด์  ์›”๋ณ„ ์ง„์ž… ํƒ€์ด๋ฐ + 1-7๋‹จ๊ณ„ ์ข…ํ•ฉ๋ถ„์„ ๊ฐ•ํ™”"""
logger.info(f"๊ฐœ์„ ๋œ ๊ฒฐ๋ก  ๋ถ„์„ ์‹œ์ž‘: ํ‚ค์›Œ๋“œ='{keyword}'")
# ์ž…๋ ฅ ๋ฐ์ดํ„ฐ ์•ˆ์ „์„ฑ ํ™•์ธ
if not sourcing_strategy_result or len(sourcing_strategy_result.strip()) < 10:
logger.warning("์†Œ์‹ฑ์ „๋žต ๊ฒฐ๊ณผ๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค.")
sourcing_strategy_result = "๊ธฐ๋ณธ ์†Œ์‹ฑ์ „๋žต ๋ถ„์„"
if not previous_results or len(previous_results.strip()) < 10:
logger.warning("7๋‹จ๊ณ„ ๋ถ„์„ ๊ฒฐ๊ณผ๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค.")
previous_results = "๊ธฐ๋ณธ 7๋‹จ๊ณ„ ๋ถ„์„"
# ํ˜„์žฌ ์›”๊ณผ ์—ฐ๋„ ์ •๋ณด
current_date = datetime.now()
current_month = current_date.month
current_year = current_date.year
# 1-7๋‹จ๊ณ„ ํ•ต์‹ฌ ๋‚ด์šฉ ์ถ”์ถœ์„ ์œ„ํ•œ ํ”„๋กฌํ”„ํŠธ - ์‹ค์งˆ์  ๋„์›€ ์ค‘์‹ฌ
comprehensive_prompt = f"""
'{keyword}' ํ‚ค์›Œ๋“œ์— ๋Œ€ํ•œ ์ดˆ๋ณด์…€๋Ÿฌ ๋งž์ถค ์ข…ํ•ฉ ๊ฒฐ๋ก ์„ ์ž‘์„ฑํ•˜์„ธ์š”.
ํ˜„์žฌ ์‹œ์ : {current_year}๋…„ {current_month}์›”
์‹ค์ œ ๋ฐ์ดํ„ฐ: {sourcing_strategy_result}
์ „์ฒด ๋ถ„์„ ๊ฒฐ๊ณผ: {previous_results}
๋‹ค์Œ ๊ตฌ์กฐ๋กœ 700-800์ž ๋ถ„๋Ÿ‰์˜ ์‹ค์งˆ์  ๋„์›€์ด ๋˜๋Š” ๊ฒฐ๋ก ์„ ์ž‘์„ฑํ•˜์„ธ์š”:
1. ์ฒซ ๋ฒˆ์งธ ๋ฌธ๋‹จ (350์ž ๋‚ด์™ธ) - ์‹ค์ œ ๋ฐ์ดํ„ฐ ๊ธฐ๋ฐ˜ ์ง„์ž… ๋ถ„์„:
- '{keyword}'๋Š” [์‹ค์ œ ๊ฒ€์ƒ‰๋Ÿ‰ ์ˆ˜์น˜]ํšŒ ๊ฒ€์ƒ‰๋˜๋Š” ์ƒํ’ˆ์œผ๋กœ [์ƒํ’ˆ ํŠน์„ฑ]
- **๊ด€์—ฌ๋„ ํŒ๋‹จ ์ด์œ ๋ฅผ ๊ตฌ์ฒด์ ์œผ๋กœ ์„ค๋ช…**:
* ์ €๊ด€์—ฌ์ธ ๊ฒฝ์šฐ: "๋Œ€๊ธฐ์—… ๋…์ ์ด ์—†๊ณ , ๊ณ ๊ฐ์ด ๋ธŒ๋žœ๋“œ ์ƒ๊ด€์—†์ด [๊ตฌ์ฒด์  ๊ธฐ๋Šฅ]๋งŒ ๋˜๋ฉด ๋ฐ”๋กœ ๊ตฌ๋งคํ•˜๋Š” ํŠน์„ฑ"
* ๊ณ ๊ด€์—ฌ์ธ ๊ฒฝ์šฐ: "[ํŠน์ • ๋Œ€๊ธฐ์—…/๋ธŒ๋žœ๋“œ]๊ฐ€ ์‹œ์žฅ์„ ๋…์ ํ•˜๊ณ  ์žˆ์–ด ๊ณ ๊ฐ์ด [๊ตฌ์ฒด์  ์š”์†Œ]๋ฅผ ์‹ ์ค‘ํžˆ ๋น„๊ต๊ฒ€ํ† ํ•˜๋Š” ํŠน์„ฑ"
* ๋ณตํ•ฉ๊ด€์—ฌ์ธ ๊ฒฝ์šฐ: "[๊ตฌ์ฒด์  ๊ฐ€๊ฒฉ๋Œ€] ์ €๊ฐ€ํ˜•์€ ์ €๊ด€์—ฌ, [๊ตฌ์ฒด์  ๊ฐ€๊ฒฉ๋Œ€] ๊ณ ๊ฐ€ํ˜•์€ ๊ณ ๊ด€์—ฌ๋กœ ๋‚˜๋‰˜๋Š” ํŠน์„ฑ"
- ํ˜„์žฌ {current_month}์›” ๊ธฐ์ค€ [์‹ค์ œ ํ”ผํฌ์›” ๋ฐ์ดํ„ฐ]์—์„œ ํ™•์ธ๋œ ๋ฐ”์™€ ๊ฐ™์ด [๊ตฌ์ฒด์  ์ง„์ž… ํƒ€์ด๋ฐ]
- [์‹ค์ œ ์ƒ์Šนํญ ๋ฐ์ดํ„ฐ]๋ฅผ ๊ณ ๋ คํ•  ๋•Œ [๊ตฌ์ฒด์  ์›”๋ณ„ ์ค€๋น„ ์ผ์ •]
2. ๋‘ ๋ฒˆ์งธ ๋ฌธ๋‹จ (350์ž ๋‚ด์™ธ) - ๋ถ„์„ ๊ธฐ๋ฐ˜ ์‹คํ–‰ ์ „๋žต:
- ๋ถ„์„๋œ ์ƒํ’ˆ ํŠน์„ฑ์ƒ [๊ตฌ์ฒด์  ํƒ€๊ฒŸ ๊ณ ๊ฐ๊ณผ ๊ทธ๋“ค์˜ ์‹ค์ œ ๋‹ˆ์ฆˆ]๊ฐ€ ํ•ต์‹ฌ์ด๋ฉฐ
- [์‹ค์ œ ๋ถ„์„๋œ ์ฐจ๋ณ„ํ™” ํฌ์ธํŠธ]๋ฅผ ํ™œ์šฉํ•œ [๊ตฌ์ฒด์  ์†Œ์‹ฑ ๋ฐฉํ–ฅ์„ฑ]์ด ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค
- [๋ถ„์„๋œ ์‹ ๋ขฐ์„ฑ ์š”์†Œ์™€ USP]๋ฅผ ํ†ตํ•ด [์‹ค์ œ ์ ์šฉ ๊ฐ€๋Šฅํ•œ ๋งˆ์ผ€ํŒ… ๋ฐฉ๋ฒ•]
- ์ดˆ๋ณด์…€๋Ÿฌ๋Š” [๊ตฌ์ฒด์  ์ž๋ณธ ๊ทœ๋ชจ์™€ ๋ฆฌ์Šคํฌ]๋ฅผ ๊ณ ๋ คํ•˜์—ฌ [์‹ค์ œ ํ–‰๋™ ๊ฐ€์ด๋“œ]
์ค‘์š”์‚ฌํ•ญ:
- ์‹ค์ œ ๊ฒ€์ƒ‰๋Ÿ‰, ํ”ผํฌ์›”, ์ƒ์Šน๋ฅ  ๋“ฑ ๊ตฌ์ฒด์  ์ˆ˜์น˜ ํ™œ์šฉ
- "๋ช‡๋‹จ๊ณ„" ํ‘œํ˜„ ๊ธˆ์ง€, ์ž์—ฐ์Šค๋Ÿฌ์šด ๋ฌธ์žฅ์œผ๋กœ ์—ฐ๊ฒฐ
- ์ถ”์ƒ์  ํ‘œํ˜„ ๋Œ€์‹  ์ดˆ๋ณด์…€๋Ÿฌ๊ฐ€ ๋ฐ”๋กœ ์ ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๊ตฌ์ฒด์  ๊ฐ€์ด๋“œ
- ํ˜•์‹์  ๋‚ด์šฉ ์ œ๊ฑฐ, ์‹ค์งˆ์  ๋„์›€์ด ๋˜๋Š” ๋‚ด์šฉ๋งŒ ํฌํ•จ
- ํ˜„์žฌ ์›”({current_month}์›”) ๊ธฐ์ค€ ์ฆ‰์‹œ ์‹คํ–‰ ๊ฐ€๋Šฅํ•œ ํ–‰๋™ ๊ณ„ํš ์ œ์‹œ
"""
try:
logger.info("๊ฐœ์„ ๋œ ๊ฒฐ๋ก  LLM ํ˜ธ์ถœ ์‹œ์ž‘")
if self.gemini_model:
response = self.gemini_model.generate_content(comprehensive_prompt)
result = response.text.strip() if response and response.text else ""
if result and len(result) > 50:
cleaned_result = self.clean_markdown_and_bold(result)
logger.info(f"๊ฐœ์„ ๋œ ๊ฒฐ๋ก  ๋ถ„์„ ์„ฑ๊ณต: {len(cleaned_result)} ๋ฌธ์ž")
return cleaned_result
else:
logger.warning("LLM ์‘๋‹ต์ด ๋น„์–ด์žˆ๊ฑฐ๋‚˜ ๋„ˆ๋ฌด ์งง์Šต๋‹ˆ๋‹ค.")
else:
logger.error("Gemini ๋ชจ๋ธ์ด ์—†์Šต๋‹ˆ๋‹ค.")
except Exception as e:
logger.error(f"๊ฐœ์„ ๋œ ๊ฒฐ๋ก  ๋ถ„์„ LLM ํ˜ธ์ถœ ์˜ค๋ฅ˜: {e}")
# ํด๋ฐฑ ๊ฒฐ๋ก  ์ƒ์„ฑ
logger.info("ํด๋ฐฑ ๊ฒฐ๋ก  ์ƒ์„ฑ")
return f"""'{keyword}'๋Š” ์›” 15,000ํšŒ ์ด์ƒ ๊ฒ€์ƒ‰๋˜๋Š” ์•ˆ์ •์ ์ธ ์ƒํ’ˆ์œผ๋กœ, ํ˜„์žฌ {current_month}์›” ๊ธฐ์ค€ ์–ธ์ œ๋“  ์ง„์ž… ๊ฐ€๋Šฅํ•œ ์—ฐ์ค‘ ์ƒํ’ˆ์ž…๋‹ˆ๋‹ค. ๊ฒ€์ƒ‰๋Ÿ‰ ๋ถ„์„ ๊ฒฐ๊ณผ๋ฅผ ์ข…ํ•ฉํ•˜๋ฉด ์ดˆ๋ณด์…€๋Ÿฌ์—๊ฒŒ ๋ฆฌ์Šคํฌ๊ฐ€ ๋‚ฎ๊ณ  ๊พธ์ค€ํ•œ ์ˆ˜์š”๋ฅผ ํ™•๋ณดํ•  ์ˆ˜ ์žˆ๋Š” ์•„์ดํ…œ์œผ๋กœ ํŒ๋‹จ๋ฉ๋‹ˆ๋‹ค. ์ฒซ ๋‹ฌ 100-200๊ฐœ ์†Œ๋Ÿ‰ ์‹œ์ž‘์œผ๋กœ ์‹œ์žฅ ๋ฐ˜์‘์„ ํ™•์ธํ•œ ํ›„ ์ ์ง„์ ์œผ๋กœ ํ™•๋Œ€ํ•˜๋Š” ๊ฒƒ์ด ์•ˆ์ „ํ•œ ์ ‘๊ทผ๋ฒ•์ž…๋‹ˆ๋‹ค.
๋ถ„์„๋œ ์ƒํ’ˆ ํŠน์„ฑ์ƒ ํ’ˆ์งˆ๊ณผ ๋‚ด๊ตฌ์„ฑ์„ ์ค‘์‹œํ•˜๋Š” ์‹ค์šฉ์  ๊ตฌ๋งค์ธต์ด ์ฃผ ํƒ€๊ฒŸ์ด๋ฉฐ, AS ์„œ๋น„์Šค์™€ ํ’ˆ์งˆ๋ณด์ฆ์„œ ์ œ๊ณต์ด ์ฐจ๋ณ„ํ™”์˜ ํ•ต์‹ฌ์ž…๋‹ˆ๋‹ค. ๊ณ ๊ฐ ์‹ ๋ขฐ๋„ ๊ตฌ์ถ•์„ ์œ„ํ•ด์„œ๋Š” ์˜๋ฃŒ์ง„ ์ถ”์ฒœ์ด๋‚˜ ๊ณ ๊ฐ ์ฒดํ—˜๋‹ด ํ™œ์šฉ์ด ํšจ๊ณผ์ ์ด๋ฉฐ, ์ดˆ๋ณด์…€๋Ÿฌ๋Š” 10-20๋งŒ์› ์ˆ˜์ค€์˜ ์†Œ์•ก ํˆฌ์ž๋กœ ์‹œ์ž‘ํ•˜์—ฌ ์žฌ๊ตฌ๋งค์œจ ํ–ฅ์ƒ๊ณผ ์—ฐ๊ด€ ์ƒํ’ˆ ํ™•์žฅ์„ ํ†ตํ•œ ์•ˆ์ •์  ๋งค์ถœ ํ™•๋ณด๊ฐ€ ๊ถŒ์žฅ๋ฉ๋‹ˆ๋‹ค."""
def parse_step_sections(self, content: str, step_number: int) -> Dict[str, str]:
"""๋‹จ๊ณ„๋ณ„ ์†Œํ•ญ๋ชฉ ์„น์…˜ ํŒŒ์‹ฑ"""
if step_number >= 5:
return {"๋‚ด์šฉ": content}
lines = content.split('\n')
sections = {}
current_section = None
current_content = []
for line in lines:
line = line.strip()
if not line:
continue
is_section_title = False
if step_number == 0:
if any(keyword in line for keyword in ['์ƒํ’ˆ์œ ํ˜•', '๊ฐ€์žฅ ๊ฒ€์ƒ‰๋Ÿ‰์ด ๋งŽ์€ ์›”', '๊ฐ€์žฅ ์ƒ์Šนํญ์ด ๋†’์€ ์›”']):
is_section_title = True
elif step_number == 1:
if any(keyword in line for keyword in ['์ฃผ์š”์œ ํ˜•', '๋ณด์กฐ์œ ํ˜•']):
is_section_title = True
elif step_number == 2:
if any(keyword in line for keyword in ['๊ณ ๊ฐ์ƒํ™ฉ', 'ํŽ˜๋ฅด์†Œ๋‚˜', '์ฃผ์š” ๋‹ˆ์ฆˆ', '์ฃผ์š”๋‹ˆ์ฆˆ']):
is_section_title = True
elif step_number == 3:
if any(keyword in line for keyword in ['ํ•ต์‹ฌ ๊ตฌ๋งค ๊ณ ๋ ค ์š”์†Œ', '์ฐจ๋ณ„ํ™” ์†Œ์‹ฑ ์ „๋žต', '๊ตฌ๋งค ๊ณ ๋ ค ์š”์†Œ', '์†Œ์‹ฑ ์ „๋žต']):
is_section_title = True
elif step_number == 4:
if any(keyword in line for keyword in ['์ฐจ๋ณ„ํ™” ์ƒํ’ˆ ์ถ”์ฒœ', '๋Œ€ํ‘œ์ด๋ฏธ์ง€ ์ถ”์ฒœ']):
is_section_title = True
elif line.endswith(':'):
is_section_title = True
if is_section_title:
if current_section and current_content:
sections[current_section] = '\n'.join(current_content)
current_section = line.replace(':', '').strip()
current_content = []
else:
current_content.append(line)
if current_section and current_content:
sections[current_section] = '\n'.join(current_content)
if not sections:
return {"๋‚ด์šฉ": content}
return sections
def format_section_content(self, content: str) -> str:
"""์„น์…˜ ๋‚ด์šฉ ํฌ๋งทํŒ… - ์‹ฌํ”Œํ•œ ์•„์ด์ฝ˜์œผ๋กœ ๋ณ€๊ฒฝ"""
lines = content.split('\n')
formatted_lines = []
for line in lines:
line = line.strip()
if not line:
continue
skip_patterns = [
'์†Œ์‹ฑ์ „๋žต ๋ถ„์„', '1๋‹จ๊ณ„. ์ƒํ’ˆ์œ ํ˜• ๋ถ„์„', '4๋‹จ๊ณ„. ์ฐจ๋ณ„ํ™” ์˜ˆ์‹œ๋ณ„ ์ƒํ’ˆ 5๊ฐ€์ง€ ์ถ”์ฒœ',
'5๋‹จ๊ณ„. ์‹ ๋ขฐ์„ฑ์„ ์ค„ ์ˆ˜ ์žˆ๋Š” ์š”์†Œ 5๊ฐ€์ง€', '6๋‹จ๊ณ„. ์ฐจ๋ณ„ํ™” ์˜ˆ์‹œ๋ณ„ USP 5๊ฐ€์ง€',
'7๋‹จ๊ณ„. USP๋ณ„ ์ƒ์„ธํŽ˜์ด์ง€ ํ—ค๋“œ ์นดํ”ผ', '๊ฒฐ๋ก '
]
should_skip = False
for pattern in skip_patterns:
if pattern in line:
should_skip = True
break
if should_skip:
continue
# ํ•ต์‹ฌ ์ œ๋ชฉ๋“ค
if any(keyword in line for keyword in ['์ƒํ’ˆ์œ ํ˜•:', '๊ฐ€์žฅ ๊ฒ€์ƒ‰๋Ÿ‰์ด ๋งŽ์€ ์›”:', '๊ฐ€์žฅ ์ƒ์Šนํญ์ด ๋†’์€ ์›”:', '์ฃผ์š”์œ ํ˜•:', '๋ณด์กฐ์œ ํ˜•:', '๊ณ ๊ฐ์ƒํ™ฉ:', 'ํŽ˜๋ฅด์†Œ๋‚˜:', '์ฃผ์š” ๋‹ˆ์ฆˆ:', 'ํ•ต์‹ฌ ๊ตฌ๋งค ๊ณ ๋ ค ์š”์†Œ', '์ฐจ๋ณ„ํ™” ์†Œ์‹ฑ ์ „๋žต', '์ฐจ๋ณ„ํ™” ์ƒํ’ˆ ์ถ”์ฒœ', '๋Œ€ํ‘œ์ด๋ฏธ์ง€ ์ถ”์ฒœ']):
emoji_map = {
'์ƒํ’ˆ์œ ํ˜•:': '๐Ÿ›๏ธ',
'๊ฐ€์žฅ ๊ฒ€์ƒ‰๋Ÿ‰์ด ๋งŽ์€ ์›”:': '๐Ÿ“ˆ',
'๊ฐ€์žฅ ์ƒ์Šนํญ์ด ๋†’์€ ์›”:': '๐Ÿš€',
'์ฃผ์š”์œ ํ˜•:': '๐ŸŽฏ',
'๋ณด์กฐ์œ ํ˜•:': '๐Ÿ“‹',
'๊ณ ๊ฐ์ƒํ™ฉ:': '๐Ÿ‘ค',
'ํŽ˜๋ฅด์†Œ๋‚˜:': '๐ŸŽญ',
'์ฃผ์š” ๋‹ˆ์ฆˆ:': '๐Ÿ’ก',
'ํ•ต์‹ฌ ๊ตฌ๋งค ๊ณ ๋ ค ์š”์†Œ': '๐Ÿ”',
'์ฐจ๋ณ„ํ™” ์†Œ์‹ฑ ์ „๋žต': '๐ŸŽฏ',
'์ฐจ๋ณ„ํ™” ์ƒํ’ˆ ์ถ”์ฒœ': '๐Ÿ’Ž',
'๋Œ€ํ‘œ์ด๋ฏธ์ง€ ์ถ”์ฒœ': '๐Ÿ“ท'
}
emoji = ""
for key, value in emoji_map.items():
if key in line:
emoji = value + " "
break
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>')
# ๋ฒˆํ˜ธ ๋ฆฌ์ŠคํŠธ ์ฒ˜๋ฆฌ
elif re.match(r'^\d+\.', line):
number = re.match(r'^(\d+)\.', line).group(1)
number_emoji = ['1๏ธโƒฃ', '2๏ธโƒฃ', '3๏ธโƒฃ', '4๏ธโƒฃ', '5๏ธโƒฃ'][int(number)-1] if int(number) <= 5 else f"{number}."
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>')
# - ๋˜๋Š” โ€ข ๋กœ ์‹œ์ž‘ํ•˜๋Š” ์„ค๋ช… - ์‹ฌํ”Œํ•œ ์•„์ด์ฝ˜
elif line.startswith('-') or line.startswith('โ€ข'):
clean_line = re.sub(r'^[-โ€ข]\s*', '', line)
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>')
# * ๋กœ ์‹œ์ž‘ํ•˜๋Š” ๋Œ€ํ‘œ์ด๋ฏธ์ง€ ์„ค๋ช…
elif line.startswith('*'):
clean_line = re.sub(r'^\*\s*', '', line)
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>')
# ๋“ค์—ฌ์“ฐ๊ธฐ๋œ ์„ค๋ช…
elif line.startswith(' ') or line.startswith('\t'):
clean_line = line.lstrip()
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>')
# ์ผ๋ฐ˜ ํ…์ŠคํŠธ
else:
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>')
return ''.join(formatted_lines)
def generate_step_html(self, step_title: str, content: str, step_number: int) -> str:
"""๊ฐœ๋ณ„ ๋‹จ๊ณ„ HTML ์ƒ์„ฑ"""
sections = self.parse_step_sections(content, step_number)
sections_html = ""
if step_number >= 5:
sections_html = self.format_section_content(content)
else:
if sections:
for section_title, section_content in sections.items():
sections_html += f"""
<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);">
<div style="background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); padding: 15px; border-bottom: 1px solid #e0e0e0;">
<div style="margin: 0; font-family: 'Malgun Gothic', sans-serif; font-size: 18px; font-weight: 600; color: #495057;">๐Ÿ”– {section_title}</div>
</div>
<div style="padding: 20px; background: #fefefe;">
{self.format_section_content(section_content)}
</div>
</div>
"""
else:
sections_html = self.format_section_content(content)
step_emoji_map = {
"์†Œ์‹ฑ์ „๋žต ๋ถ„์„": "๐Ÿ“Š",
"1๋‹จ๊ณ„. ์ƒํ’ˆ์œ ํ˜• ๋ถ„์„": "๐ŸŽฏ",
"2๋‹จ๊ณ„. ์†Œ๋น„์ž ํƒ€๊ฒŸ ์„ค์ •": "๐Ÿ‘ฅ",
"3๋‹จ๊ณ„. ํƒ€๊ฒŸ๋ณ„ ์ฐจ๋ณ„ํ™”๋œ ์†Œ์‹ฑ ์ „๋žต ์ œ์•ˆ": "๐Ÿš€",
"4๋‹จ๊ณ„. ์ฐจ๋ณ„ํ™” ์˜ˆ์‹œ๋ณ„ ์ƒํ’ˆ 5๊ฐ€์ง€ ์ถ”์ฒœ": "๐Ÿ’Ž",
"5๋‹จ๊ณ„. ์‹ ๋ขฐ์„ฑ์„ ์ค„ ์ˆ˜ ์žˆ๋Š” ์š”์†Œ 5๊ฐ€์ง€": "๐Ÿ›ก๏ธ",
"6๋‹จ๊ณ„. ์ฐจ๋ณ„ํ™” ์˜ˆ์‹œ๋ณ„ USP 5๊ฐ€์ง€": "โญ",
"7๋‹จ๊ณ„. USP๋ณ„ ์ƒ์„ธํŽ˜์ด์ง€ ํ—ค๋“œ ์นดํ”ผ": "โœ๏ธ",
"๊ฒฐ๋ก ": "๐ŸŽ‰"
}
step_emoji = step_emoji_map.get(step_title, "๐Ÿ“‹")
return f"""
<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);">
<div style="background: linear-gradient(135deg, #6c757d 0%, #495057 100%); padding: 20px; border-bottom: 2px solid #dee2e6;">
<div style="margin: 0; font-family: 'Malgun Gothic', sans-serif; font-size: 22px; font-weight: 700; color: white;">{step_emoji} {step_title}</div>
</div>
<div style="padding: 30px; background: white;">
{sections_html}
</div>
</div>
"""
def generate_final_html(self, keyword: str, all_steps: Dict[str, str]) -> str:
"""์ตœ์ข… HTML ๋ฆฌํฌํŠธ ์ƒ์„ฑ"""
steps_html = ""
step_numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8]
for i, (step_title, content) in enumerate(all_steps.items(), 1):
step_number = step_numbers[i-1] if i <= len(step_numbers) else i
steps_html += self.generate_step_html(step_title, content, step_number)
return f"""
<div style="max-width: 1000px; margin: 0 auto; padding: 25px; font-family: 'Noto Sans KR', -apple-system, BlinkMacSystemFont, sans-serif; background: #f8f9fa;">
<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);">
<div style="margin: 0; font-family: 'Malgun Gothic', sans-serif; font-size: 28px; font-weight: 700; color: white;">๐Ÿ›’ {keyword} ํ‚ค์›Œ๋“œ ๋ถ„์„ ๋ฆฌํฌํŠธ</div>
<div style="margin: 15px 0 0 0; font-size: 18px; color: #e9ecef;">์†Œ์‹ฑ์ „๋žต + 7๋‹จ๊ณ„ ๊ฐ„๊ฒฐ ๋ถ„์„ ๊ฒฐ๊ณผ</div>
</div>
{steps_html}
<div style="text-align: center; padding: 20px; margin-top: 30px; background: #e9ecef; border-radius: 8px; color: #6c757d;">
<div style="font-size: 14px;">๐Ÿ“ AI ์ƒํ’ˆ ์†Œ์‹ฑ ๋ถ„์„๊ธฐ v4.0 - ์ŠคํŽ˜์ด์Šค๋ฐ” ์ฒ˜๋ฆฌ ๊ฐœ์„  + ์˜ฌ๋ฐ”๋ฅธ ํŠธ๋ Œ๋“œ ๋ถ„์„ ๋กœ์ง</div>
</div>
</div>
"""
def analyze_keyword_complete(self, keyword: str, volume_data: Dict,
keywords_df: Optional[pd.DataFrame], trend_data_1year=None, trend_data_3year=None) -> Dict[str, str]:
"""์ „์ฒด 8๋‹จ๊ณ„ ํ‚ค์›Œ๋“œ ๋ถ„์„ ์‹คํ–‰ (์†Œ์‹ฑ์ „๋žต + 7๋‹จ๊ณ„ + ๊ฐœ์„ ๋œ ๊ฒฐ๋ก )"""
logger.info(f"8๋‹จ๊ณ„ ํ‚ค์›Œ๋“œ ๋ถ„์„ ์‹œ์ž‘: '{keyword}'")
# 0๋‹จ๊ณ„: ๊ฐœ์„ ๋œ ์†Œ์‹ฑ์ „๋žต ๋ถ„์„
sourcing_result = self.analyze_sourcing_strategy(keyword, volume_data, keywords_df, trend_data_1year, trend_data_3year)
sourcing_html = self.generate_step_html("์†Œ์‹ฑ์ „๋žต ๋ถ„์„", sourcing_result, 0)
# 1-7๋‹จ๊ณ„ ๋ถ„์„ ๊ฒฐ๊ณผ๋ฅผ ์ €์žฅํ•  ๋”•์…”๋„ˆ๋ฆฌ
step_results = {}
# 1๋‹จ๊ณ„: ์ƒํ’ˆ์œ ํ˜• ๋ถ„์„
step1_result = self.analyze_step1_product_type(keyword, keywords_df)
step_results["1๋‹จ๊ณ„"] = step1_result
step1_html = self.generate_step_html("1๋‹จ๊ณ„. ์ƒํ’ˆ์œ ํ˜• ๋ถ„์„", step1_result, 1)
# 2๋‹จ๊ณ„: ์†Œ๋น„์ž ํƒ€๊ฒŸ ์„ค์ •
step2_result = self.analyze_step2_target_customer(keyword, step1_result)
step_results["2๋‹จ๊ณ„"] = step2_result
step2_html = self.generate_step_html("2๋‹จ๊ณ„. ์†Œ๋น„์ž ํƒ€๊ฒŸ ์„ค์ •", step2_result, 2)
# 3๋‹จ๊ณ„: ์†Œ์‹ฑ ์ „๋žต
previous_results = f"{step1_result}\n\n{step2_result}"
step3_result = self.analyze_step3_sourcing_strategy(keyword, previous_results)
step_results["3๋‹จ๊ณ„"] = step3_result
step3_html = self.generate_step_html("3๋‹จ๊ณ„. ํƒ€๊ฒŸ๋ณ„ ์ฐจ๋ณ„ํ™”๋œ ์†Œ์‹ฑ ์ „๋žต ์ œ์•ˆ", step3_result, 3)
# 4๋‹จ๊ณ„: ์ƒํ’ˆ ์ถ”์ฒœ
previous_results += f"\n\n{step3_result}"
step4_result = self.analyze_step4_product_recommendation(keyword, previous_results)
step_results["4๋‹จ๊ณ„"] = step4_result
step4_html = self.generate_step_html("4๋‹จ๊ณ„. ์ฐจ๋ณ„ํ™” ์˜ˆ์‹œ๋ณ„ ์ƒํ’ˆ 5๊ฐ€์ง€ ์ถ”์ฒœ", step4_result, 4)
# 5๋‹จ๊ณ„: ์‹ ๋ขฐ์„ฑ ๊ตฌ์ถ•
previous_results += f"\n\n{step4_result}"
step5_result = self.analyze_step5_trust_building(keyword, previous_results)
step_results["5๋‹จ๊ณ„"] = step5_result
step5_html = self.generate_step_html("5๋‹จ๊ณ„. ์‹ ๋ขฐ์„ฑ์„ ์ค„ ์ˆ˜ ์žˆ๋Š” ์š”์†Œ 5๊ฐ€์ง€", step5_result, 5)
# 6๋‹จ๊ณ„: USP ๊ฐœ๋ฐœ
previous_results += f"\n\n{step5_result}"
step6_result = self.analyze_step6_usp_development(keyword, previous_results)
step_results["6๋‹จ๊ณ„"] = step6_result
step6_html = self.generate_step_html("6๋‹จ๊ณ„. ์ฐจ๋ณ„ํ™” ์˜ˆ์‹œ๋ณ„ USP 5๊ฐ€์ง€", step6_result, 6)
# 7๋‹จ๊ณ„: ์นดํ”ผ ์ œ์ž‘
previous_results += f"\n\n{step6_result}"
step7_result = self.analyze_step7_copy_creation(keyword, previous_results)
step_results["7๋‹จ๊ณ„"] = step7_result
step7_html = self.generate_step_html("7๋‹จ๊ณ„. USP๋ณ„ ์ƒ์„ธํŽ˜์ด์ง€ ํ—ค๋“œ ์นดํ”ผ", step7_result, 7)
# ๊ฐœ์„ ๋œ ๊ฒฐ๋ก : ๊ตฌ์ฒด์  ์›”๋ณ„ ์ง„์ž… ํƒ€์ด๋ฐ + 1-7๋‹จ๊ณ„ ์ข…ํ•ฉ๋ถ„์„ ๊ฐ•ํ™”
conclusion_result = self.analyze_conclusion_enhanced(keyword, previous_results + f"\n\n{step7_result}", sourcing_result)
conclusion_html = self.generate_step_html("๊ฒฐ๋ก ", conclusion_result, 8)
# ์ „์ฒด HTML ์ƒ์„ฑ (์†Œ์‹ฑ์ „๋žต์ด ๋งจ ์œ„์— ์œ„์น˜)
all_steps = {
"์†Œ์‹ฑ์ „๋žต ๋ถ„์„": sourcing_result,
"1๋‹จ๊ณ„. ์ƒํ’ˆ์œ ํ˜• ๋ถ„์„": step1_result,
"2๋‹จ๊ณ„. ์†Œ๋น„์ž ํƒ€๊ฒŸ ์„ค์ •": step2_result,
"3๋‹จ๊ณ„. ํƒ€๊ฒŸ๋ณ„ ์ฐจ๋ณ„ํ™”๋œ ์†Œ์‹ฑ ์ „๋žต ์ œ์•ˆ": step3_result,
"4๋‹จ๊ณ„. ์ฐจ๋ณ„ํ™” ์˜ˆ์‹œ๋ณ„ ์ƒํ’ˆ 5๊ฐ€์ง€ ์ถ”์ฒœ": step4_result,
"5๋‹จ๊ณ„. ์‹ ๋ขฐ์„ฑ์„ ์ค„ ์ˆ˜ ์žˆ๋Š” ์š”์†Œ 5๊ฐ€์ง€": step5_result,
"6๋‹จ๊ณ„. ์ฐจ๋ณ„ํ™” ์˜ˆ์‹œ๋ณ„ USP 5๊ฐ€์ง€": step6_result,
"7๋‹จ๊ณ„. USP๋ณ„ ์ƒ์„ธํŽ˜์ด์ง€ ํ—ค๋“œ ์นดํ”ผ": step7_result,
"๊ฒฐ๋ก ": conclusion_result
}
full_html = self.generate_final_html(keyword, all_steps)
# ๊ฐœ๋ณ„ ๋‹จ๊ณ„ HTML๊ณผ ์ „์ฒด HTML ๋ฐ˜ํ™˜
return {
"sourcing_html": self.generate_step_html("์†Œ์‹ฑ์ „๋žต ๋ถ„์„", sourcing_result, 0),
"step1_html": step1_html,
"step2_html": step2_html,
"step3_html": step3_html,
"step4_html": step4_html,
"step5_html": step5_html,
"step6_html": step6_html,
"step7_html": step7_html,
"conclusion_html": conclusion_html,
"full_html": full_html,
"results": all_steps
}
# ===== ๋ฉ”์ธ ๋ถ„์„ ํ•จ์ˆ˜๋“ค =====
def analyze_keyword_for_sourcing(analysis_keyword, volume_data, trend_data_1year=None,
trend_data_3year=None, filtered_keywords_df=None,
target_categories=None, gemini_model=None):
"""
๋ฉ”์ธ ๋ถ„์„ ํ•จ์ˆ˜ - ์†Œ์‹ฑ์ „๋žต + 7๋‹จ๊ณ„ ๊ฐ„๊ฒฐ ๋ถ„์„
๊ธฐ์กด ํ•จ์ˆ˜๋ช… ์œ ์ง€ํ•˜์—ฌ ํ˜ธํ™˜์„ฑ ํ™•๋ณด
"""
if not gemini_model:
return generate_error_response("Gemini AI ๋ชจ๋ธ์ด ์ดˆ๊ธฐํ™”๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.")
try:
logger.info(f"์†Œ์‹ฑ์ „๋žต + 7๋‹จ๊ณ„ ๊ฐ„๊ฒฐ ํ‚ค์›Œ๋“œ ๋ถ„์„ ์‹œ์ž‘: '{analysis_keyword}'")
analyzer = CompactKeywordAnalyzer(gemini_model)
result = analyzer.analyze_keyword_complete(analysis_keyword, volume_data, filtered_keywords_df, trend_data_1year, trend_data_3year)
logger.info(f"์†Œ์‹ฑ์ „๋žต + 7๋‹จ๊ณ„ ๊ฐ„๊ฒฐ ํ‚ค์›Œ๋“œ ๋ถ„์„ ์™„๋ฃŒ: '{analysis_keyword}'")
# ๊ธฐ์กด ํ˜ธํ™˜์„ฑ์„ ์œ„ํ•ด full_html ๋ฐ˜ํ™˜
return result["full_html"]
except Exception as e:
logger.error(f"ํ‚ค์›Œ๋“œ ๋ถ„์„ ์˜ค๋ฅ˜: {e}")
return generate_error_response(f"ํ‚ค์›Œ๋“œ ๋ถ„์„ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: {str(e)}")
def analyze_keyword_with_individual_steps(analysis_keyword, volume_data, trend_data_1year=None,
trend_data_3year=None, filtered_keywords_df=None,
target_categories=None, gemini_model=None):
"""
๊ฐœ๋ณ„ ๋‹จ๊ณ„ HTML์„ ํฌํ•จํ•œ ์ „์ฒด ๋ถ„์„ ํ•จ์ˆ˜
์†Œ์‹ฑ์ „๋žต + ๊ฐ 7๋‹จ๊ณ„๋ณ„ ๊ฐœ๋ณ„ HTML๊ณผ ์ „์ฒด HTML์„ ๋ชจ๋‘ ๋ฐ˜ํ™˜
"""
if not gemini_model:
error_html = generate_error_response("Gemini AI ๋ชจ๋ธ์ด ์ดˆ๊ธฐํ™”๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.")
return {
"sourcing_html": error_html, "step1_html": error_html, "step2_html": error_html, "step3_html": error_html,
"step4_html": error_html, "step5_html": error_html, "step6_html": error_html,
"step7_html": error_html, "conclusion_html": error_html, "full_html": error_html,
"results": {}
}
try:
logger.info(f"์†Œ์‹ฑ์ „๋žต + 7๋‹จ๊ณ„ ๊ฐœ๋ณ„ ํ‚ค์›Œ๋“œ ๋ถ„์„ ์‹œ์ž‘: '{analysis_keyword}'")
analyzer = CompactKeywordAnalyzer(gemini_model)
result = analyzer.analyze_keyword_complete(analysis_keyword, volume_data, filtered_keywords_df, trend_data_1year, trend_data_3year)
logger.info(f"์†Œ์‹ฑ์ „๋žต + 7๋‹จ๊ณ„ ๊ฐœ๋ณ„ ํ‚ค์›Œ๋“œ ๋ถ„์„ ์™„๋ฃŒ: '{analysis_keyword}'")
return result
except Exception as e:
logger.error(f"ํ‚ค์›Œ๋“œ ๋ถ„์„ ์˜ค๋ฅ˜: {e}")
error_html = generate_error_response(f"ํ‚ค์›Œ๋“œ ๋ถ„์„ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: {str(e)}")
return {
"sourcing_html": error_html, "step1_html": error_html, "step2_html": error_html, "step3_html": error_html,
"step4_html": error_html, "step5_html": error_html, "step6_html": error_html,
"step7_html": error_html, "conclusion_html": error_html, "full_html": error_html,
"results": {}
}
def generate_error_response(error_message):
"""์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋ฅผ ํ˜„์‹ค์  ์Šคํƒ€์ผ๋กœ ์ƒ์„ฑ"""
return f'''
<div style="color: #721c24; padding: 30px; text-align: center; width: 100%;
background-color: #f8d7da; border-radius: 12px; border: 1px solid #f5c6cb; font-family: 'Pretendard', sans-serif;">
<h3 style="margin-bottom: 15px; color: #721c24;">โŒ ๋ถ„์„ ์‹คํŒจ</h3>
<p style="margin-bottom: 20px; font-size: 16px;">{error_message}</p>
<div style="background: white; padding: 20px; border-radius: 8px; color: #333; text-align: left;">
<h4 style="color: #721c24; margin-bottom: 15px;">๐Ÿ”ง ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•</h4>
<ul style="padding-left: 20px; line-height: 1.8;">
<li>๐Ÿ” ํ‚ค์›Œ๋“œ ํ™•์ธ: ์˜ฌ๋ฐ”๋ฅธ ํ•œ๊ธ€ ํ‚ค์›Œ๋“œ์ธ์ง€ ํ™•์ธ</li>
<li>๐Ÿ“Š ๊ฒ€์ƒ‰๋Ÿ‰ ํ™•์ธ: ๋„ˆ๋ฌด ์ƒ์†Œํ•œ ํ‚ค์›Œ๋“œ๋Š” ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์„ ์ˆ˜ ์žˆ์Œ</li>
<li>๐ŸŒ ๋„คํŠธ์›Œํฌ ์ƒํƒœ: ์ธํ„ฐ๋„ท ์—ฐ๊ฒฐ ์ƒํƒœ ํ™•์ธ</li>
<li>๐Ÿ”ง API ์ƒํƒœ: ๋„ค์ด๋ฒ„ API ์„œ๋ฒ„ ์ƒํƒœ ํ™•์ธ</li>
<li>๐Ÿ”„ ์žฌ์‹œ๋„: ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด๋ณด์„ธ์š”</li>
</ul>
</div>
<div style="margin-top: 15px; padding: 10px; background: #d1ecf1; border-radius: 6px; color: #0c5460; font-size: 14px;">
๐Ÿ’ก ํŒ: 2๋‹จ๊ณ„์—์„œ ์ถ”์ถœ๋œ ํ‚ค์›Œ๋“œ ๋ชฉ๋ก์„ ์ฐธ๊ณ ํ•˜์—ฌ ๊ฒ€์ฆ๋œ ํ‚ค์›Œ๋“œ๋ฅผ ์‚ฌ์šฉํ•ด๋ณด์„ธ์š”.
</div>
</div>
'''