This commit is contained in:
hiondal
2025-06-15 13:52:26 +00:00
commit 6a5c411800
53 changed files with 15785 additions and 0 deletions
+161
View File
@@ -0,0 +1,161 @@
# app/utils/category_utils.py (수정된 버전)
import re
from typing import Optional
def extract_food_category(category_name: str) -> str:
"""
카테고리명에서 음식 종류를 추출합니다.
'음식점 > 한식 > 육류,고기'에서 '한식'을 추출
Args:
category_name: 전체 카테고리명
Returns:
추출된 음식 종류
"""
if not category_name:
return ""
# '>' 기준으로 분할하고 마지막 바로 전 요소 반환
parts = category_name.split('>')
if len(parts) >= 2:
food_category = parts[-2].strip() # 마지막 바로 전 값 사용
return food_category
elif len(parts) == 1:
return parts[0].strip() # 하나밖에 없으면 그것을 반환
return category_name.strip()
def normalize_category(category: str) -> str:
"""
카테고리를 정규화합니다.
Args:
category: 원본 카테고리
Returns:
정규화된 카테고리
"""
if not category:
return ""
# 공백 제거 및 소문자 변환
normalized = category.strip().lower()
# 특수문자 제거 (콤마, 슬래시 등은 유지)
normalized = re.sub(r'[^\w가-힣,/\s]', '', normalized)
return normalized
def is_similar_category(category1: str, category2: str) -> bool:
"""
두 카테고리가 유사한지 판단합니다.
Args:
category1: 첫 번째 카테고리
category2: 두 번째 카테고리
Returns:
유사 여부
"""
if not category1 or not category2:
return False
# 정규화
norm1 = normalize_category(category1)
norm2 = normalize_category(category2)
# 완전 일치
if norm1 == norm2:
return True
# 키워드 기반 유사성 검사
keywords1 = set(norm1.replace(',', ' ').replace('/', ' ').split())
keywords2 = set(norm2.replace(',', ' ').replace('/', ' ').split())
# 교집합이 하나 이상 있으면 유사한 것으로 판단
common_keywords = keywords1.intersection(keywords2)
return len(common_keywords) > 0
def extract_main_category(category_name: str) -> str:
"""
메인 카테고리를 추출합니다. (음식점 > 한식 에서 '한식' 추출)
Args:
category_name: 전체 카테고리명
Returns:
메인 카테고리
"""
if not category_name:
return ""
parts = category_name.split('>')
if len(parts) >= 2:
return parts[1].strip()
elif len(parts) == 1:
return parts[0].strip()
return ""
def build_search_query(region: str, food_category: str) -> str:
"""
검색 쿼리를 구성합니다. (수정된 버전 - 지역 정보 제외)
Args:
region: 지역 (사용하지 않음)
food_category: 음식 카테고리
Returns:
검색 쿼리 문자열 (음식 카테고리만 포함)
"""
# 콤마와 슬래시를 공백으로 변경하여 검색 키워드 생성
search_keywords = food_category.replace(',', ' ').replace('/', ' ')
# 불필요한 단어 제거
stop_words = ['음식점', '요리', '전문점', '맛집']
keywords = []
for keyword in search_keywords.split():
keyword = keyword.strip()
if keyword and keyword not in stop_words:
keywords.append(keyword)
# 키워드가 없으면 기본 검색어 사용
if not keywords:
keywords = ['음식점']
# 🔧 지역 정보는 포함하지 않고 음식 키워드만 반환
query = ' '.join(keywords)
return query.strip()
def clean_food_category_for_search(food_category: str) -> str:
"""
음식 카테고리를 검색용 키워드로 정리합니다.
Args:
food_category: 원본 음식 카테고리
Returns:
정리된 검색 키워드
"""
if not food_category:
return "음식점"
# 콤마와 슬래시를 공백으로 변경
cleaned = food_category.replace(',', ' ').replace('/', ' ')
# 불필요한 단어 제거
stop_words = ['음식점', '요리', '전문점', '맛집']
keywords = []
for keyword in cleaned.split():
keyword = keyword.strip()
if keyword and keyword not in stop_words:
keywords.append(keyword)
# 키워드가 없으면 기본 검색어 사용
if not keywords:
return "음식점"
return ' '.join(keywords)
+194
View File
@@ -0,0 +1,194 @@
# app/utils/data_utils.py
import json
import hashlib
from datetime import datetime
from typing import Dict, List, Any, Optional
def create_store_hash(store_id: str, store_name: str, region: str) -> str:
"""
가게의 고유 해시를 생성합니다.
Args:
store_id: 가게 ID
store_name: 가게명
region: 지역
Returns:
생성된 해시값
"""
combined = f"{store_id}_{store_name}_{region}"
return hashlib.md5(combined.encode('utf-8')).hexdigest()
def combine_store_and_reviews(store_info: Dict[str, Any], reviews: List[Dict[str, Any]]) -> str:
"""
가게 정보와 리뷰를 결합하여 JSON 문자열을 생성합니다.
Args:
store_info: 가게 정보
reviews: 리뷰 목록
Returns:
결합된 JSON 문자열
"""
combined_data = {
"store_info": store_info,
"reviews": reviews,
"review_summary": generate_review_summary(reviews),
"combined_at": datetime.now().isoformat()
}
return json.dumps(combined_data, ensure_ascii=False, separators=(',', ':'))
def generate_review_summary(reviews: List[Dict[str, Any]]) -> Dict[str, Any]:
"""
리뷰 목록에서 요약 정보를 생성합니다.
Args:
reviews: 리뷰 목록
Returns:
리뷰 요약 정보
"""
if not reviews:
return {
"total_reviews": 0,
"average_rating": 0.0,
"rating_distribution": {},
"common_keywords": [],
"sentiment_summary": {
"positive": 0,
"neutral": 0,
"negative": 0
}
}
# 기본 통계
total_reviews = len(reviews)
ratings = [review.get('rating', 0) for review in reviews if review.get('rating', 0) > 0]
average_rating = sum(ratings) / len(ratings) if ratings else 0.0
# 별점 분포
rating_distribution = {}
for rating in ratings:
rating_distribution[str(rating)] = rating_distribution.get(str(rating), 0) + 1
# 키워드 추출 (badges 기반)
keyword_counts = {}
for review in reviews:
badges = review.get('badges', [])
for badge in badges:
keyword_counts[badge] = keyword_counts.get(badge, 0) + 1
# 상위 키워드 추출
common_keywords = sorted(keyword_counts.items(), key=lambda x: x[1], reverse=True)[:10]
common_keywords = [keyword for keyword, count in common_keywords]
# 감정 분석 (간단한 별점 기반)
sentiment_summary = {
"positive": len([r for r in ratings if r >= 4]),
"neutral": len([r for r in ratings if r == 3]),
"negative": len([r for r in ratings if r <= 2])
}
return {
"total_reviews": total_reviews,
"average_rating": round(average_rating, 2),
"rating_distribution": rating_distribution,
"common_keywords": common_keywords,
"sentiment_summary": sentiment_summary,
"has_recent_reviews": any(
review.get('date', '') >= datetime.now().strftime('%Y.%m.%d')
for review in reviews[-10:] # 최근 10개 리뷰 확인
)
}
def extract_text_for_embedding(store_info: Dict[str, Any], reviews: List[Dict[str, Any]]) -> str:
"""
임베딩을 위한 텍스트를 추출합니다.
Args:
store_info: 가게 정보
reviews: 리뷰 목록
Returns:
임베딩용 텍스트
"""
# 가게 기본 정보
store_text = f"가게명: {store_info.get('place_name', '')}\n"
store_text += f"카테고리: {store_info.get('category_name', '')}\n"
store_text += f"주소: {store_info.get('address_name', '')}\n"
# 리뷰 내용 요약
review_contents = []
review_keywords = []
for review in reviews[:20]: # 최근 20개 리뷰만 사용
content = review.get('content', '').strip()
if content:
review_contents.append(content)
badges = review.get('badges', [])
review_keywords.extend(badges)
# 리뷰 텍스트 조합
if review_contents:
store_text += f"리뷰 내용: {' '.join(review_contents[:10])}\n" # 최대 10개 리뷰
# 키워드 조합
if review_keywords:
unique_keywords = list(set(review_keywords))
store_text += f"키워드: {', '.join(unique_keywords[:15])}\n" # 최대 15개 키워드
return store_text.strip()
def create_metadata(store_info: Dict[str, Any], food_category: str, region: str) -> Dict[str, Any]:
"""
Vector DB용 메타데이터를 생성합니다.
Args:
store_info: 가게 정보
food_category: 음식 카테고리
region: 지역
Returns:
메타데이터 딕셔너리
"""
return {
"store_id": store_info.get('id', ''),
"store_name": store_info.get('place_name', ''),
"food_category": food_category,
"region": region,
"category_name": store_info.get('category_name', ''),
"address": store_info.get('address_name', ''),
"phone": store_info.get('phone', ''),
"place_url": store_info.get('place_url', ''),
"x": store_info.get('x', ''), # 좌표를 개별 키로 분리
"y": store_info.get('y', ''), # 좌표를 개별 키로 분리
"last_updated": datetime.now().isoformat()
}
def is_duplicate_store(metadata1: Dict[str, Any], metadata2: Dict[str, Any]) -> bool:
"""
두 가게가 중복인지 확인합니다.
Args:
metadata1: 첫 번째 가게 메타데이터
metadata2: 두 번째 가게 메타데이터
Returns:
중복 여부
"""
# Store ID 기준 확인
if metadata1.get('store_id') and metadata2.get('store_id'):
return metadata1['store_id'] == metadata2['store_id']
# 가게명 + 주소 기준 확인
name1 = metadata1.get('store_name', '').strip()
name2 = metadata2.get('store_name', '').strip()
addr1 = metadata1.get('address', '').strip()
addr2 = metadata2.get('address', '').strip()
if name1 and name2 and addr1 and addr2:
return name1 == name2 and addr1 == addr2
return False