3gghdf5 / keyword_processor.py
ssboost's picture
Upload 15 files
106555b verified
"""
ν‚€μ›Œλ“œ 처리 κ΄€λ ¨ κΈ°λŠ₯ - μ•žλ’€ μ‘°ν•© 쀑 높은 κ²€μƒ‰λŸ‰λ§Œ 선택, μΉ΄ν…Œκ³ λ¦¬ ν•­λͺ© 제거
- ν‚€μ›Œλ“œ μΆ”μΆœ 및 μ‘°ν•©
- 검색 κ²°κ³Ό 처리
"""
import pandas as pd
import re
from collections import defaultdict, Counter
import text_utils
import keyword_search
import product_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)
def process_search_results(search_results, current_keyword="", exclude_zero_volume=True):
"""
검색 κ²°κ³Όμ—μ„œ ν‚€μ›Œλ“œμ™€ μΉ΄ν…Œκ³ λ¦¬ 정보 μΆ”μΆœ 및 처리 - μ•žλ’€ μ‘°ν•© 쀑 높은 κ²€μƒ‰λŸ‰λ§Œ 선택
Args:
search_results (dict): 검색 κ²°κ³Ό 정보
current_keyword (str): ν˜„μž¬ 검색 쀑인 ν‚€μ›Œλ“œ
exclude_zero_volume (bool): κ²€μƒ‰λŸ‰μ΄ 0인 ν‚€μ›Œλ“œ μ œμ™Έ μ—¬λΆ€
Returns:
dict: 처리된 결과
"""
logger.info("\n===== 검색 κ²°κ³Ό 처리 μ‹œμž‘ =====")
logger.info(f"ν˜„μž¬ ν‚€μ›Œλ“œ: '{current_keyword}'")
logger.info(f"κ²€μƒ‰λŸ‰ 0 ν‚€μ›Œλ“œ μ œμ™Έ: {exclude_zero_volume}")
if not search_results or not search_results.get("product_list"):
logger.warning("검색 κ²°κ³Όκ°€ μ—†μŠ΅λ‹ˆλ‹€.")
return {
"products_df": None,
"keywords_df": None,
"categories": ["전체 보기"],
"message": "검색 κ²°κ³Όκ°€ μ—†μŠ΅λ‹ˆλ‹€."
}
product_list = search_results["product_list"]
combo_candidates = search_results["combo_candidates"]
category_counter = search_results["category_counter"]
keyword_indices = search_results["keyword_indices"]
keyword_pairs = search_results.get("keyword_pairs", {}) # μ•žλ’€ μ‘°ν•© 정보
logger.info(f"검색 κ²°κ³Ό - μƒν’ˆ 수: {len(product_list)}개")
logger.info(f"검색 κ²°κ³Ό - μ‘°ν•© 후보 수: {len(combo_candidates)}개")
logger.info(f"검색 κ²°κ³Ό - μΉ΄ν…Œκ³ λ¦¬ 수: {len(category_counter)}개")
# μƒν’ˆ 정보 λ°μ΄ν„°ν”„λ ˆμž„ 생성
df_products = pd.DataFrame(product_list)
# API ν‚€μ›Œλ“œλ₯Ό UI ν‚€μ›Œλ“œλ‘œ λ³€ν™˜ν•˜λŠ” λ§€ν•‘ 생성
api_to_ui_keywords = {}
for api_keyword in combo_candidates.keys():
# API ν‚€μ›Œλ“œμ—μ„œ UI ν‚€μ›Œλ“œλ‘œ λ³€ν™˜
if current_keyword and current_keyword in api_keyword:
# 메인 ν‚€μ›Œλ“œ 자체인 경우
if api_keyword == current_keyword:
api_to_ui_keywords[api_keyword] = current_keyword
continue
# 메인 ν‚€μ›Œλ“œκ°€ 이미 ν¬ν•¨λœ 경우 (예: κ°‘μ˜€μ§•μ–΄, κ·€μ˜€μ§•μ–΄)
# 곡백 μžˆλŠ” ν˜•νƒœλ‘œ λ³€ν™˜
ui_keyword = api_keyword
# 곡백이 μ—†λŠ” ν˜•νƒœλΌλ©΄ μ μ ˆν•œ μœ„μΉ˜μ— 곡백 μΆ”κ°€
if " " not in api_keyword:
# 메인 ν‚€μ›Œλ“œ κΈ°μ€€μœΌλ‘œ 뢄리
if api_keyword.startswith(current_keyword):
# μ˜€μ§•μ–΄κ°‘ => μ˜€μ§•μ–΄ κ°‘
prefix = current_keyword
suffix = api_keyword[len(current_keyword):]
if suffix:
ui_keyword = f"{prefix} {suffix}"
elif api_keyword.endswith(current_keyword):
# κ°‘μ˜€μ§•μ–΄ => κ°‘ μ˜€μ§•μ–΄
prefix = api_keyword[:-len(current_keyword)]
suffix = current_keyword
if prefix:
ui_keyword = f"{prefix} {suffix}"
else:
# 메인 ν‚€μ›Œλ“œκ°€ 쀑간에 μžˆλŠ” 경우
idx = api_keyword.find(current_keyword)
if idx > 0:
prefix = api_keyword[:idx]
middle = current_keyword
suffix = api_keyword[idx+len(current_keyword):]
ui_keyword = f"{prefix} {middle}"
if suffix:
ui_keyword += f" {suffix}"
api_to_ui_keywords[api_keyword] = ui_keyword
else:
# 메인 ν‚€μ›Œλ“œκ°€ μ—†λŠ” 경우 - κ·ΈλŒ€λ‘œ μ‚¬μš©
api_to_ui_keywords[api_keyword] = api_keyword
# === μˆ˜μ •λœ λΆ€λΆ„: κ²€μƒ‰λŸ‰ 쑰회 ν›„ μ•žλ’€ μ‘°ν•© 쀑 높은 κ²ƒλ§Œ 선택 ===
logger.info(f"\nκ²€μƒ‰λŸ‰ 쑰회 λŒ€μƒ ν‚€μ›Œλ“œ 수: {len(combo_candidates)}개")
search_volumes = keyword_search.fetch_all_search_volumes(list(combo_candidates.keys()))
logger.info(f"κ²€μƒ‰λŸ‰ 쑰회 μ™„λ£Œ: {len(search_volumes)}개 κ²°κ³Ό")
# μ•žλ’€ μ‘°ν•© 쀑 높은 κ²€μƒ‰λŸ‰λ§Œ 선택
if keyword_pairs and current_keyword:
logger.info("\n=== μ•žλ’€ μ‘°ν•© 쀑 높은 κ²€μƒ‰λŸ‰ 선택 ===")
filtered_candidates = {}
# 메인 ν‚€μ›Œλ“œλŠ” 항상 포함
main_api = current_keyword.replace(" ", "")
if main_api in combo_candidates:
filtered_candidates[main_api] = combo_candidates[main_api]
logger.info(f"메인 ν‚€μ›Œλ“œ μœ μ§€: '{current_keyword}'")
# 메인 ν‚€μ›Œλ“œκ°€ ν¬ν•¨λœ 볡합어도 μœ μ§€
for api_kw, categories in combo_candidates.items():
ui_kw = api_to_ui_keywords[api_kw]
if current_keyword in ui_kw and api_kw != main_api and api_kw not in [pair_info["front"].replace(" ", "") for pair_info in keyword_pairs.values()] and api_kw not in [pair_info["back"].replace(" ", "") for pair_info in keyword_pairs.values()]:
filtered_candidates[api_kw] = categories
logger.info(f"메인 ν‚€μ›Œλ“œ 포함 볡합어 μœ μ§€: '{ui_kw}'")
# μ•žλ’€ μ‘°ν•© 비ꡐ
for base_word, pair_info in keyword_pairs.items():
front_kw = pair_info["front"] # "ν‚€μ›Œλ“œ λ©”μΈν‚€μ›Œλ“œ"
back_kw = pair_info["back"] # "λ©”μΈν‚€μ›Œλ“œ ν‚€μ›Œλ“œ"
front_api = front_kw.replace(" ", "")
back_api = back_kw.replace(" ", "")
front_vol = search_volumes.get(front_api, {}).get("μ΄κ²€μƒ‰λŸ‰", 0)
back_vol = search_volumes.get(back_api, {}).get("μ΄κ²€μƒ‰λŸ‰", 0)
# 높은 κ²€μƒ‰λŸ‰ 선택
if front_vol > back_vol:
selected_api = front_api
selected_kw = front_kw
selected_vol = front_vol
removed_kw = back_kw
removed_vol = back_vol
elif back_vol > front_vol:
selected_api = back_api
selected_kw = back_kw
selected_vol = back_vol
removed_kw = front_kw
removed_vol = front_vol
elif front_vol == back_vol and front_vol > 0:
# 같은 κ²€μƒ‰λŸ‰μ΄λ©΄ 더 μžμ—°μŠ€λŸ¬μš΄ μˆœμ„œ 선택 (λ©”μΈν‚€μ›Œλ“œκ°€ 뒀에 μ˜€λŠ” 것)
selected_api = back_api
selected_kw = back_kw
selected_vol = back_vol
removed_kw = front_kw
removed_vol = front_vol
else:
# λ‘˜ λ‹€ 0이면 μ œμ™Έ
logger.info(f" '{base_word}' μ‘°ν•©: λ‘˜ λ‹€ κ²€μƒ‰λŸ‰ 0으둜 μ œμ™Έ")
continue
# μ„ νƒλœ ν‚€μ›Œλ“œλ§Œ μΆ”κ°€
if selected_vol > 0 or not exclude_zero_volume:
filtered_candidates[selected_api] = combo_candidates[selected_api]
logger.info(f" '{base_word}' μ‘°ν•© 선택: '{selected_kw}' ({selected_vol:,}) > '{removed_kw}' ({removed_vol:,})")
else:
logger.info(f" '{base_word}' μ‘°ν•©: κ²€μƒ‰λŸ‰ 0으둜 μ œμ™Έ")
# ν•„ν„°λ§λœ μ‘°ν•©μœΌλ‘œ ꡐ체
combo_candidates = filtered_candidates
logger.info(f"μ•žλ’€ μ‘°ν•© 필터링 μ™„λ£Œ: {len(combo_candidates)}개 ν‚€μ›Œλ“œ 선택")
# κ²€μƒ‰λŸ‰ 0 ν‚€μ›Œλ“œ 톡계
zero_volume_count = sum(1 for vol in search_volumes.values() if vol.get("μ΄κ²€μƒ‰λŸ‰", 0) == 0)
logger.info(f"κ²€μƒ‰λŸ‰ 0인 ν‚€μ›Œλ“œ 수: {zero_volume_count}개 ({zero_volume_count/max(1, len(search_volumes))*100:.1f}%)")
# 쀑볡 ν‚€μ›Œλ“œ 제거λ₯Ό μœ„ν•œ μ •κ·œν™”λœ ν‚€μ›Œλ“œ μ§‘ν•©
normalized_keywords = {}
for api_keyword in combo_candidates.keys():
ui_keyword = api_to_ui_keywords[api_keyword]
# κ²€μƒ‰λŸ‰ 정보 κ°€μ Έμ˜€κΈ°
pc_count = 0
mobile_count = 0
total_count = 0
if api_keyword in search_volumes:
pc_count = search_volumes[api_keyword]["PCκ²€μƒ‰λŸ‰"]
mobile_count = search_volumes[api_keyword]["λͺ¨λ°”μΌκ²€μƒ‰λŸ‰"]
total_count = search_volumes[api_keyword]["μ΄κ²€μƒ‰λŸ‰"]
# κ²€μƒ‰λŸ‰ 0인 ν‚€μ›Œλ“œ μ œμ™Έ μ˜΅μ…˜ 적용
if exclude_zero_volume and total_count == 0:
logger.debug(f" - '{ui_keyword}' (API: '{api_keyword}') - κ²€μƒ‰λŸ‰ 0으둜 μ œμ™Έλ¨")
continue
# 1. 곡백을 κΈ°μ€€μœΌλ‘œ 단어 뢄리 ν›„ μ •λ ¬ν•΄ μ •κ·œν™” ν‚€ 생성
words = ui_keyword.split()
normalized = "".join(sorted(words))
# 2. 이미 μ •κ·œν™”λœ ν‚€μ›Œλ“œκ°€ 있으면 κ²€μƒ‰λŸ‰μ΄ 더 높은 것을 선택
if normalized in normalized_keywords:
existing_api_keyword, existing_ui_keyword, existing_total = normalized_keywords[normalized]
if total_count > existing_total:
logger.debug(f" - 쀑볡 ν‚€μ›Œλ“œ λŒ€μ²΄: '{existing_ui_keyword}' ({existing_total}) -> '{ui_keyword}' ({total_count})")
normalized_keywords[normalized] = (api_keyword, ui_keyword, total_count)
else:
logger.debug(f" - 쀑볡 ν‚€μ›Œλ“œ μ œμ™Έ: '{ui_keyword}' ({total_count}) < '{existing_ui_keyword}' ({existing_total})")
else:
normalized_keywords[normalized] = (api_keyword, ui_keyword, total_count)
logger.debug(f" - ν‚€μ›Œλ“œ μΆ”κ°€: '{ui_keyword}' (κ²€μƒ‰λŸ‰: {total_count})")
logger.info(f"\n쀑볡 제거 ν›„ ν‚€μ›Œλ“œ 수: {len(normalized_keywords)}개")
# 쀑볡이 제거된 ν‚€μ›Œλ“œλ§Œ 처리
final_combos = []
for normalized, (api_keyword, ui_keyword, total_count) in normalized_keywords.items():
# ν‚€μ›Œλ“œ 가독성 κ°œμ„  - fix_keyword_order ν•¨μˆ˜ 적용
readable = fix_keyword_order(ui_keyword, current_keyword)
# κ²€μƒ‰λŸ‰ 정보 κ°€μ Έμ˜€κΈ°
pc_count = 0
mobile_count = 0
if api_keyword in search_volumes:
pc_count = search_volumes[api_keyword]["PCκ²€μƒ‰λŸ‰"]
mobile_count = search_volumes[api_keyword]["λͺ¨λ°”μΌκ²€μƒ‰λŸ‰"]
total_count = search_volumes[api_keyword]["μ΄κ²€μƒ‰λŸ‰"]
# κ²€μƒ‰λŸ‰ ꡬ간 계산
search_volume_range = text_utils.get_search_volume_range(total_count)
# λ“±μž₯ μˆœμœ„ 및 횟수 계산
base_word = readable.replace(current_keyword, "").strip() if current_keyword else readable
ranks = []
if base_word in keyword_indices:
ranks = [idx + 1 for idx in keyword_indices[base_word]]
elif api_keyword in keyword_indices: # 메인 ν‚€μ›Œλ“œκ°€ ν¬ν•¨λœ 단어인 경우
ranks = [idx + 1 for idx in keyword_indices.get(api_keyword, [])]
ranks_str = ", ".join(map(str, ranks)) if ranks else "-"
usage_count = len(ranks)
# === μˆ˜μ •λœ λΆ€λΆ„: "μƒν’ˆ 등둝 μΉ΄ν…Œκ³ λ¦¬(μƒμœ„100μœ„)" ν•­λͺ© 제거 ===
# μΉ΄ν…Œκ³ λ¦¬ μ •λ³΄λŠ” λ‚΄λΆ€μ μœΌλ‘œλ§Œ μ‚¬μš©ν•˜κ³  ν…Œμ΄λΈ”μ—λŠ” ν‘œμ‹œν•˜μ§€ μ•ŠμŒ
final_combos.append({
"μ‘°ν•© ν‚€μ›Œλ“œ": readable.strip(),
"PCκ²€μƒ‰λŸ‰": pc_count,
"λͺ¨λ°”μΌκ²€μƒ‰λŸ‰": mobile_count,
"μ΄κ²€μƒ‰λŸ‰": total_count,
"κ²€μƒ‰λŸ‰κ΅¬κ°„": search_volume_range,
"ν‚€μ›Œλ“œ μ‚¬μš©μžμˆœμœ„": ranks_str,
"ν‚€μ›Œλ“œ μ‚¬μš©νšŸμˆ˜": usage_count
# "μƒν’ˆ 등둝 μΉ΄ν…Œκ³ λ¦¬(μƒμœ„100μœ„)" ν•­λͺ© 제거됨
})
# ν‚€μ›Œλ“œ 정보 λ°μ΄ν„°ν”„λ ˆμž„ 생성
df_keywords = pd.DataFrame(final_combos)
# κ²€μƒ‰λŸ‰ κΈ°μ€€μœΌλ‘œ λ‚΄λ¦Όμ°¨μˆœ μ •λ ¬
if not df_keywords.empty:
df_keywords = df_keywords.sort_values(by="μ΄κ²€μƒ‰λŸ‰", ascending=False)
# μˆœλ²ˆμ„ μœ„ν•΄ 인덱슀 리셋 (순차적 순번 보μž₯)
df_keywords = df_keywords.reset_index(drop=True)
# λ°μ΄ν„°ν”„λ ˆμž„ 생성 ν›„ λ‘œκΉ…
logger.info(f"\nμƒμ„±λœ ν‚€μ›Œλ“œ λ°μ΄ν„°ν”„λ ˆμž„ ν–‰ 수: {len(df_keywords)}")
if not df_keywords.empty:
logger.debug(f"λ°μ΄ν„°ν”„λ ˆμž„ μ—΄: {df_keywords.columns.tolist()}")
logger.info(f"총 {len(df_keywords)}개 ν‚€μ›Œλ“œ 생성 μ™„λ£Œ")
# μΉ΄ν…Œκ³ λ¦¬ 정보 가곡
category_with_counts = [f"{cat} ({category_counter[cat]})" for cat in sorted(category_counter.keys())]
category_with_counts.insert(0, "전체 보기")
logger.info(f"μΉ΄ν…Œκ³ λ¦¬ 수: {len(category_counter)}개")
logger.info("===== 검색 κ²°κ³Ό 처리 μ™„λ£Œ =====\n")
return {
"products_df": df_products,
"keywords_df": df_keywords,
"categories": category_with_counts,
"message": "βœ… 검색이 μ™„λ£Œλ˜μ—ˆμŠ΅λ‹ˆλ‹€. μ•„λž˜μ—μ„œ ν‚€μ›Œλ“œλ₯Ό ν™•μΈν•˜μ„Έμš”."
}
def filter_and_sort_table(df, selected_cat, keyword_sort, total_volume_sort, usage_count_sort, selected_volume_range, exclude_zero_volume=False):
"""ν…Œμ΄λΈ” 필터링 및 μ •λ ¬ ν•¨μˆ˜ (κ²€μƒ‰λŸ‰ 0 μ œμ™Έ κΈ°λŠ₯ μΆ”κ°€)"""
if df is None or df.empty:
return ""
# 필터링 적용
filtered = df.copy()
# μΉ΄ν…Œκ³ λ¦¬ ν•„ν„° 적용 (μΉ΄ν…Œκ³ λ¦¬ 열이 μ œκ±°λ˜μ—ˆμœΌλ―€λ‘œ 주석 처리)
# if selected_cat and selected_cat != "전체 보기":
# cat_name = selected_cat.rsplit(" (", 1)[0]
# filtered = filtered[filtered["κ΄€λ ¨ μΉ΄ν…Œκ³ λ¦¬"].str.contains(cat_name)]
# κ²€μƒ‰λŸ‰ ꡬ간 ν•„ν„° 적용
if selected_volume_range and selected_volume_range != "전체":
filtered = filtered[filtered["κ²€μƒ‰λŸ‰κ΅¬κ°„"] == selected_volume_range]
# κ²€μƒ‰λŸ‰ 0 μ œμ™Έ ν•„ν„° 적용
if exclude_zero_volume:
filtered = filtered[filtered["μ΄κ²€μƒ‰λŸ‰"] > 0]
logger.info(f"κ²€μƒ‰λŸ‰ 0 μ œμ™Έ ν•„ν„° 적용 - 남은 ν‚€μ›Œλ“œ 수: {len(filtered)}")
# μ •λ ¬ 적용
if keyword_sort != "μ •λ ¬ μ—†μŒ":
is_ascending = keyword_sort == "μ˜€λ¦„μ°¨μˆœ"
filtered = filtered.sort_values(by="μ‘°ν•© ν‚€μ›Œλ“œ", ascending=is_ascending)
if total_volume_sort != "μ •λ ¬ μ—†μŒ":
is_ascending = total_volume_sort == "μ˜€λ¦„μ°¨μˆœ"
filtered = filtered.sort_values(by="μ΄κ²€μƒ‰λŸ‰", ascending=is_ascending)
# ν‚€μ›Œλ“œ μ‚¬μš©νšŸμˆ˜ μ •λ ¬ 적용
if usage_count_sort != "μ •λ ¬ μ—†μŒ":
is_ascending = usage_count_sort == "μ˜€λ¦„μ°¨μˆœ"
filtered = filtered.sort_values(by="ν‚€μ›Œλ“œ μ‚¬μš©νšŸμˆ˜", ascending=is_ascending)
# λ°μ΄ν„°ν”„λ ˆμž„ λ‚΄μš© λ‘œκΉ…
logger.info(f"ν•„ν„° 적용 ν›„ - ν•„ν„°λ§λœ DataFrame ν–‰ 수: {len(filtered)}")
# μˆœλ²ˆμ„ 1λΆ€ν„° 순차적으둜 μœ μ§€ν•˜κΈ° μœ„ν•΄ ν–‰ 인덱슀 μž¬μ„€μ •
filtered = filtered.reset_index(drop=True)
from export_utils import create_table_without_checkboxes
# μˆœλ²ˆμ„ ν¬ν•¨ν•œ HTML ν…Œμ΄λΈ” 생성
html = create_table_without_checkboxes(filtered)
return html
def fix_keyword_order(keyword, main_keyword):
"""
ν‚€μ›Œλ“œ μˆœμ„œλ₯Ό μˆ˜μ •ν•˜λŠ” ν•¨μˆ˜ - ν•œκΈ€μ΄ μ•žμ— 였고 μ˜μ–΄/μˆ«μžκ°€ 뒀에 μ˜€λ„λ‘ 함
Args:
keyword (str): μˆ˜μ •ν•  ν‚€μ›Œλ“œ
main_keyword (str): 메인 ν‚€μ›Œλ“œ
Returns:
str: μˆœμ„œκ°€ μˆ˜μ •λœ ν‚€μ›Œλ“œ
"""
# 곡백 없이 숫자+μ˜μ–΄μ™€ ν•œκΈ€μ΄ λΆ™μ–΄μžˆλŠ” νŒ¨ν„΄ 처리
# 예: "300gμ˜€μ§•μ–΄" β†’ "μ˜€μ§•μ–΄ 300g"
pattern_combined = re.compile(r'^([0-9]+[a-zA-Z]*)([κ°€-힣]+.*)$')
match = pattern_combined.match(keyword)
if match:
number_part = match.group(1) # 숫자+μ˜μ–΄ λΆ€λΆ„
korean_part = match.group(2) # ν•œκΈ€ λΆ€λΆ„
fixed_keyword = f"{korean_part} {number_part}"
logger.debug(f"λΆ™μ–΄μžˆλŠ” νŒ¨ν„΄ μˆ˜μ •: '{keyword}' -> '{fixed_keyword}'")
return fixed_keyword
# 곡백으둜 λΆ„λ¦¬λœ 경우 처리
if ' ' in keyword:
parts = keyword.split()
# ν•œκΈ€ 포함 여뢀와 μ˜μ–΄/숫자 포함 μ—¬λΆ€λ₯Ό 각 λΆ€λΆ„λ³„λ‘œ 확인
korean_parts = []
non_korean_parts = []
for part in parts:
if re.search(r'[κ°€-힣]', part):
korean_parts.append(part) # ν•œκΈ€μ΄ ν¬ν•¨λœ λΆ€λΆ„
else:
non_korean_parts.append(part) # ν•œκΈ€μ΄ μ—†λŠ” λΆ€λΆ„ (μ˜μ–΄, 숫자, 기호 λ“±)
# ν•œκΈ€ 뢀뢄이 ν•˜λ‚˜λ„ μ—†κ±°λ‚˜ λΉ„ν•œκΈ€ 뢀뢄이 ν•˜λ‚˜λ„ μ—†μœΌλ©΄ κ·ΈλŒ€λ‘œ λ°˜ν™˜
if not korean_parts or not non_korean_parts:
return keyword
# ν•œκΈ€ 뢀뢄을 μ•žμœΌλ‘œ, λΉ„ν•œκΈ€ 뢀뢄을 λ’€λ‘œ 배치
fixed_keyword = " ".join(korean_parts + non_korean_parts)
# μ›λž˜ ν‚€μ›Œλ“œμ™€ λ‹€λ₯Έ κ²½μš°μ—λ§Œ 둜그 좜λ ₯
if fixed_keyword != keyword:
logger.debug(f"ν‚€μ›Œλ“œ μˆœμ„œ μˆ˜μ •: '{keyword}' -> '{fixed_keyword}'")
return fixed_keyword
return keyword