This commit is contained in:
hiondal 2025-06-16 03:38:45 +00:00
parent ea36d7b221
commit 59b01d9630
3 changed files with 468 additions and 103 deletions

View File

@ -566,96 +566,214 @@ curl -X POST "http://vector-api.20.249.191.180.nip.io/action-recommendation" \
"success": true, "success": true,
"recommendation": { "recommendation": {
"summary": { "summary": {
"current_situation": "치킨 전문점으로 강남구 역삼동에 위치, 평균 별점 3.8점으로 동종업체 대비 개선 여지 존재", "current_situation": "일식 업종으로 매출 감소를 겪고 있으며, 경쟁이 치열한 고품질 시장에서 운영 중입니다. 업계 평균 평점은 4.23점이고, 경쟁업체들은 3.9-4.7점 범위의 평점을 보이고 있습니다.",
"key_insights": [ "key_insights": [
"배달 서비스 만족도가 경쟁업체 대비 낮음", "업계 공통 이슈로 고객 만족도 양극화 문제 존재",
"신메뉴 출시 주기가 3개월 이상으로 긴 편", "경쟁업체들은 가성비, 맛, 친절함에서 강점을 보임",
"온라인 리뷰 관리 시스템 부재" "고객 참여도와 일관성 있는 서비스 품질이 성공의 핵심 요소"
], ],
"priority_areas": ["배달 품질 개선", "메뉴 혁신"] "priority_areas": [
"서비스 일관성 개선",
"고객 만족도 안정화"
]
}, },
"action_plans": { "action_plans": {
"short_term": [ "short_term": [
{ {
"title": "배달 포장재 개선", "title": "서비스 표준화 매뉴얼 작성 및 적용",
"description": "보온성이 우수한 친환경 포장재로 교체하여 배달 만족도 향상", "description": "조리법, 서빙 방식, 고객 응대 등 모든 서비스 과정을 표준화하여 일관성 있는 품질 제공. 직원 교육 실시 및 체크리스트 활용",
"expected_impact": "배달 리뷰 평점 0.5점 상승 예상", "expected_impact": "고객 만족도 양극화 해소, 평점 0.3-0.5점 상승 예상",
"timeline": "2주", "timeline": "2-4주",
"cost": "월 50만원" "cost": "50-100만원 (교육비, 매뉴얼 제작비)"
},
{
"title": "고객 피드백 수집 시스템 구축",
"description": "테이블 QR코드를 통한 실시간 피드백 수집, 불만사항 즉시 대응 체계 마련. 주간 피드백 분석 및 개선사항 도출",
"expected_impact": "고객 불만 30% 감소, 재방문율 15% 증가",
"timeline": "1-2주",
"cost": "30-50만원 (QR코드 제작, 시스템 구축)"
} }
], ],
"mid_term": [ "mid_term": [
{ {
"title": "시즌 한정 메뉴 런칭", "title": "메뉴 최적화 및 시그니처 메뉴 개발",
"description": "여름 시즌 매운맛 신메뉴 2종 개발 및 SNS 마케팅", "description": "경쟁업체 대비 차별화된 시그니처 메뉴 3-5개 개발. 기존 메뉴 중 인기도 낮은 메뉴 정리하고 가성비 우수 메뉴 강화",
"expected_impact": "신규 고객 유입 20% 증가", "expected_impact": "평균 주문 금액 20% 증가, 고객 재방문율 25% 향상",
"timeline": "6주", "timeline": "2-3개월",
"cost": "초기 투자 300만원" "cost": "200-300만원 (메뉴 개발, 재료비, 마케팅)"
},
{
"title": "디지털 마케팅 강화",
"description": "네이버 플레이스, 구글 마이비즈니스 최적화. SNS 활용한 메뉴 홍보 및 고객 후기 관리. 온라인 주문 시스템 도입",
"expected_impact": "온라인 노출 50% 증가, 신규 고객 유입 30% 증가",
"timeline": "3-4개월",
"cost": "150-250만원 (마케팅비, 시스템 구축비)"
} }
], ],
"long_term": [ "long_term": [
{ {
"title": "브랜드 리뉴얼", "title": "고객 충성도 프로그램 구축",
"description": "매장 인테리어 개선 및 브랜드 아이덴티티 강화", "description": "멤버십 시스템 도입, 단골 고객 특별 혜택 제공, 생일/기념일 이벤트 운영. 고객 데이터베이스 구축 및 맞춤형 서비스 제공",
"expected_impact": "브랜드 인지도 향상, 객단가 15% 상승", "expected_impact": "고객 유지율 40% 향상, 월 매출 25-30% 증가",
"timeline": "4개월", "timeline": "6-8개월",
"cost": "1,500만원" "cost": "300-500만원 (시스템 개발, 운영비)"
},
{
"title": "브랜드 아이덴티티 강화 및 확장 전략",
"description": "독특한 브랜드 스토리 개발, 인테리어 리뉴얼, 패키징 디자인 개선. 향후 2호점 진출 또는 프랜차이즈 검토",
"expected_impact": "브랜드 인지도 향상, 프리미엄 가격 정책 가능, 사업 확장 기반 마련",
"timeline": "8-12개월",
"cost": "500-1000만원 (리뉴얼, 브랜딩 비용)"
} }
] ]
}, },
"implementation_tips": [ "implementation_tips": [
"배달앱 리뷰 모니터링 시스템 도입", "단기 계획부터 차근차근 실행하되, 서비스 일관성 개선을 최우선으로 진행하세요",
"경쟁업체 메뉴 트렌드 주기적 분석", "고객 피드백을 적극 수집하고 빠르게 개선사항에 반영하여 고객과의 소통을 강화하세요",
"고객 피드백 기반 개선사항 우선순위 설정" "경쟁업체의 강점(가성비, 맛, 친절)을 벤치마킹하되, 차별화 포인트를 명확히 설정하세요"
] ]
}, },
"error_message": null,
"input_data": { "input_data": {
"request_context": { "request_context": {
"store_id": "501745730", "store_id": "501745730",
"owner_request": "매출이 감소하고 있어서 메뉴 개선이 필요합니다.", "owner_request": "매출이 감소하고 있어서 개선 방안이 필요합니다.",
"analysis_timestamp": "2024-06-16T10:30:00" "analysis_timestamp": "2025-06-16T03:34:08.622763"
}, },
"market_intelligence": { "market_intelligence": {
"total_competitors": 7, "total_competitors": 3,
"industry_insights": [ "industry_insights": [
"업계 전반적으로 고객 만족도 개선 필요", "경쟁이 치열한 고품질 시장",
"고성과 업체 3개 벤치마킹 가능" "업계 공통 이슈: 고객 만족도 양극화 (일관성 부족)"
], ],
"performance_benchmarks": { "performance_benchmarks": {
"average_rating": 4.1, "average_rating": 4.23,
"review_volume_trend": "증가" "rating_range": {
"min": 3.9,
"max": 4.7
},
"industry_standard": "Above Average",
"review_activity": {
"average_reviews": 224,
"range": {
"min": 103,
"max": 412
}
}
} }
}, },
"competitive_insights": [ "competitive_insights": [
{ {
"rank": 1, "rank": 1,
"store_name": "○○치킨", "store_name": "",
"similarity_score": 0.892, "category": "일식",
"similarity_score": 0.389,
"performance_analysis": { "performance_analysis": {
"performance": { "performance": {
"rating": "4.3", "rating": "4.1",
"review_count": "156" "review_count": "156",
"status": "영업 중"
}, },
"feedback": { "feedback": {
"positive_aspects": ["맛", "친절", "빠른배달"], "positive_aspects": [
"negative_aspects": ["가격"] "가성비",
"맛",
"분위기"
],
"negative_aspects": [],
"recent_trends": [
"최근 만족도 상승"
],
"rating_pattern": "안정적 고만족"
}, },
"business_insights": { "insights": {
"key_finding": "신메뉴 런칭으로 리뷰 급증" "competitive_advantage": [
"높은 고객 만족도",
"활발한 고객 참여",
"맛 우수"
],
"critical_issues": [],
"improvement_opportunities": []
}
}
},
{
"rank": 2,
"store_name": "",
"category": "일식",
"similarity_score": 0.347,
"performance_analysis": {
"performance": {
"rating": "4.7",
"review_count": "412",
"status": "영업 중"
},
"feedback": {
"positive_aspects": [
"가성비",
"맛",
"친절",
"분위기"
],
"negative_aspects": [
"고객 만족도 양극화 (일관성 부족)"
],
"recent_trends": [
"최근 만족도 상승"
],
"rating_pattern": "안정적 고만족"
},
"insights": {
"competitive_advantage": [
"높은 고객 만족도",
"활발한 고객 참여",
"맛 우수",
"친절 우수"
],
"critical_issues": [],
"improvement_opportunities": []
}
}
},
{
"rank": 3,
"store_name": "",
"category": "일식",
"similarity_score": 0.314,
"performance_analysis": {
"performance": {
"rating": "3.9",
"review_count": "103",
"status": "영업 중"
},
"feedback": {
"positive_aspects": [
"가성비",
"맛",
"친절"
],
"negative_aspects": [],
"recent_trends": [],
"rating_pattern": "양극화 패턴"
},
"insights": {
"competitive_advantage": [
"활발한 고객 참여",
"맛 우수",
"친절 우수"
],
"critical_issues": [],
"improvement_opportunities": []
} }
} }
} }
], ],
"actionable_recommendations": { "actionable_recommendations": {
"immediate_actions": [ "immediate_actions": [
"배달 서비스 개선 (업계 5개 업체 공통 문제)" "고객 만족도 양극화 (일관성 부족) 개선 (업계 1개 업체 공통 문제)"
], ],
"strategic_improvements": [ "strategic_improvements": [],
"메뉴 다양성 확대" "benchmarking_targets": []
],
"benchmarking_targets": [
"○○치킨: 신메뉴 마케팅 전략"
]
} }
} }
} }

View File

@ -52,6 +52,8 @@ from app.services.vector_service import VectorService
from app.services.claude_service import ClaudeService from app.services.claude_service import ClaudeService
from app.utils.category_utils import extract_food_category from app.utils.category_utils import extract_food_category
import json
# 로깅 설정 # 로깅 설정
logging.basicConfig( logging.basicConfig(
level=logging.INFO, level=logging.INFO,
@ -434,7 +436,7 @@ async def find_reviews(
# 동종 업체 리뷰 수집 (본인 가게 제외) # 동종 업체 리뷰 수집 (본인 가게 제외)
similar_store_names = [] similar_store_names = []
max_similar_reviews = min(settings.MAX_REVIEWS_PER_RESTAURANT // 2, 20) # 절반 또는 최대 20개 max_similar_reviews = settings.MAX_REVIEWS_PER_RESTAURANT
for store in similar_stores[:settings.MAX_RESTAURANTS_PER_CATEGORY]: # 환경변수 활용 for store in similar_stores[:settings.MAX_RESTAURANTS_PER_CATEGORY]: # 환경변수 활용
store_info, reviews = await review_service.collect_store_reviews( store_info, reviews = await review_service.collect_store_reviews(
store.id, store.id,

View File

@ -147,74 +147,67 @@ class VectorService:
food_category: str, food_category: str,
region: str region: str
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""Vector Store를 구축합니다""" """Vector Store를 구축합니다 (기존 데이터 업데이트 포함)"""
if not self.is_ready(): if not self.is_ready():
raise Exception(f"VectorService가 준비되지 않음: {self.initialization_error}") raise Exception(f"VectorService가 준비되지 않음: {self.initialization_error}")
try: try:
logger.info("🚀 Vector Store 구축 시작") logger.info("🔧 Vector Store 구축 시작 (기존 데이터 업데이트 포함)...")
start_time = datetime.now()
processed_count = 0 # 1단계: 가게 분류 (새 가게 vs 업데이트 vs 중복)
documents = [] logger.info("🔍 가게 분류 중...")
embeddings = [] categorization = self.categorize_stores(review_results, region, food_category)
metadatas = []
ids = []
for store_id, store_info, reviews in review_results: # 2단계: 기존 가게 업데이트
try: update_result = {'updated_count': 0, 'skipped_count': 0}
# 텍스트 추출 및 임베딩 생성 if categorization['update_stores']:
text_for_embedding = extract_text_for_embedding(store_info, reviews) logger.info(f"🔄 {len(categorization['update_stores'])}개 기존 가게 업데이트 중...")
embedding = self.embedding_model.encode(text_for_embedding) update_result = self.update_existing_stores(
categorization['update_stores'], food_category, region
# 메타데이터 생성
metadata = create_metadata(store_info, food_category, region)
# 문서 ID 생성
document_id = create_store_hash(store_id, store_info.get('name', ''), region)
# 문서 데이터 생성
document_text = combine_store_and_reviews(store_info, reviews)
documents.append(document_text)
embeddings.append(embedding.tolist())
metadatas.append(metadata)
ids.append(document_id)
processed_count += 1
except Exception as e:
logger.warning(f"⚠️ 가게 {store_id} 처리 실패: {e}")
continue
# Vector DB에 저장
if documents:
self.collection.add(
documents=documents,
embeddings=embeddings,
metadatas=metadatas,
ids=ids
) )
logger.info(f"✅ Vector Store 구축 완료: {processed_count}개 문서 저장") # 3단계: 새 가게 추가
add_result = {'added_count': 0}
if categorization['new_stores']:
logger.info(f"{len(categorization['new_stores'])}개 새 가게 추가 중...")
add_result = self.add_new_stores(
categorization['new_stores'], food_category, region
)
return { # 4단계: 결과 정리
'success': True, total_processed = update_result['updated_count'] + add_result['added_count']
'processed_count': processed_count, execution_time = (datetime.now() - start_time).total_seconds()
'message': f"{processed_count}개 문서가 Vector DB에 저장되었습니다"
} result = {
else: 'success': True,
return { 'processed_count': total_processed, # ← 기존 API 호환성 유지
'success': False, 'execution_time': execution_time,
'error': '저장할 문서가 없습니다', 'summary': categorization['summary'],
'processed_count': 0 'operations': {
} 'new_stores_added': add_result['added_count'],
'existing_stores_updated': update_result['updated_count'],
'update_skipped': update_result.get('skipped_count', 0),
'duplicates_removed': categorization['summary']['duplicate_count']
},
'message': f"✅ Vector DB 처리 완료: 새 가게 {add_result['added_count']}개 추가, "
f"기존 가게 {update_result['updated_count']}개 업데이트"
}
logger.info(f"📊 Vector Store 구축 완료:")
logger.info(f" - 총 처리: {total_processed}")
logger.info(f" - 새 가게: {add_result['added_count']}")
logger.info(f" - 업데이트: {update_result['updated_count']}")
logger.info(f" - 실행 시간: {execution_time:.2f}")
return result
except Exception as e: except Exception as e:
logger.error(f"❌ Vector Store 구축 실패: {e}") logger.error(f"❌ Vector Store 구축 실패: {e}")
return { return {
'success': False, 'success': False,
'error': str(e), 'error': str(e),
'processed_count': 0 'processed_count': 0 # ← 기존 API 호환성 유지
} }
def search_similar_cases_improved(self, store_id: str, context: str, limit: int = 5) -> Optional[Dict[str, Any]]: def search_similar_cases_improved(self, store_id: str, context: str, limit: int = 5) -> Optional[Dict[str, Any]]:
@ -934,3 +927,255 @@ class VectorService:
'status': 'error', 'status': 'error',
'error': str(e) 'error': str(e)
} }
def get_existing_store_data(self, region: str = None, food_category: str = None) -> Dict[str, Dict[str, Any]]:
"""
Vector DB에서 기존 store 데이터를 가져옵니다.
Args:
region: 지역 필터 (선택사항)
food_category: 음식 카테고리 필터 (선택사항)
Returns:
{store_id: {metadata, document_id}} 형태의 딕셔너리
"""
try:
if not self.is_ready():
logger.warning("⚠️ VectorService가 준비되지 않음")
return {}
# 필터 조건 설정
where_condition = {}
if region and food_category:
where_condition = {
"$and": [
{"region": region},
{"food_category": food_category}
]
}
elif region:
where_condition = {"region": region}
elif food_category:
where_condition = {"food_category": food_category}
# 기존 데이터 조회
if where_condition:
results = self.collection.get(
where=where_condition,
include=['metadatas', 'documents']
)
else:
results = self.collection.get(
include=['metadatas', 'documents']
)
if not results or not results.get('metadatas'):
logger.info("📊 Vector DB에 기존 데이터 없음")
return {}
# store_id별로 기존 데이터 매핑
existing_data = {}
metadatas = results.get('metadatas', [])
ids = results.get('ids', [])
documents = results.get('documents', [])
for i, metadata in enumerate(metadatas):
if metadata and 'store_id' in metadata:
store_id = metadata['store_id']
existing_data[store_id] = {
'metadata': metadata,
'document_id': ids[i] if i < len(ids) else None,
'document': documents[i] if i < len(documents) else None,
'last_updated': metadata.get('last_updated', 'unknown')
}
logger.info(f"📊 기존 Vector DB에서 {len(existing_data)}개 store 데이터 발견")
return existing_data
except Exception as e:
logger.error(f"❌ 기존 store 데이터 조회 실패: {e}")
return {}
def categorize_stores(self, review_results: List[tuple], region: str, food_category: str) -> Dict[str, Any]:
"""
가게들을 가게/업데이트 대상/현재 배치 중복으로 분류
"""
# 기존 Vector DB 데이터 조회
existing_data = self.get_existing_store_data(region, food_category)
existing_store_ids = set(existing_data.keys())
# 분류 작업
new_stores = [] # 완전히 새로운 가게
update_stores = [] # 기존 가게 업데이트
current_duplicates = [] # 현재 배치 내 중복
seen_in_batch = set()
for store_id, store_info, reviews in review_results:
# 현재 배치 내 중복 체크
if store_id in seen_in_batch:
current_duplicates.append((store_id, store_info, reviews))
logger.info(f"🔄 현재 배치 중복 발견: {store_id}")
continue
seen_in_batch.add(store_id)
# 기존 DB에 있는지 확인
if store_id in existing_store_ids:
# 업데이트 대상
update_stores.append({
'store_id': store_id,
'new_data': (store_id, store_info, reviews),
'existing_data': existing_data[store_id]
})
logger.info(f"🔄 업데이트 대상: {store_id}")
else:
# 새 가게
new_stores.append((store_id, store_info, reviews))
logger.info(f"✨ 새 가게: {store_id}")
result = {
'new_stores': new_stores,
'update_stores': update_stores,
'current_duplicates': current_duplicates,
'existing_data': existing_data,
'summary': {
'total_input': len(review_results),
'new_count': len(new_stores),
'update_count': len(update_stores),
'duplicate_count': len(current_duplicates),
'will_process': len(new_stores) + len(update_stores)
}
}
logger.info(f"📊 가게 분류 완료:")
logger.info(f" - 새 가게: {len(new_stores)}")
logger.info(f" - 업데이트: {len(update_stores)}")
logger.info(f" - 현재 중복: {len(current_duplicates)}")
return result
def update_existing_stores(self, update_stores: List[Dict], food_category: str, region: str) -> Dict[str, Any]:
"""
기존 가게들을 업데이트
"""
updated_count = 0
skipped_count = 0
update_details = []
for update_item in update_stores:
store_id = update_item['store_id']
new_data = update_item['new_data']
existing_data = update_item['existing_data']
try:
_, store_info, reviews = new_data
# 새 임베딩 및 메타데이터 생성
text_for_embedding = extract_text_for_embedding(store_info, reviews)
if not text_for_embedding or len(text_for_embedding.strip()) < 10:
logger.warning(f"⚠️ {store_id}: 임베딩 텍스트 부족, 업데이트 스킵")
skipped_count += 1
continue
embedding = self.embedding_model.encode(text_for_embedding)
new_metadata = create_metadata(store_info, food_category, region)
document_text = combine_store_and_reviews(store_info, reviews)
# 기존 document_id 사용 (ID 일관성 유지)
document_id = existing_data['document_id']
# Vector DB 업데이트 (ChromaDB에서는 upsert 사용)
self.collection.upsert(
documents=[document_text],
embeddings=[embedding.tolist()],
metadatas=[new_metadata],
ids=[document_id]
)
updated_count += 1
update_details.append({
'store_id': store_id,
'document_id': document_id,
'previous_updated': existing_data['metadata'].get('last_updated', 'unknown'),
'new_updated': new_metadata['last_updated']
})
logger.info(f"{store_id} 업데이트 완료")
except Exception as e:
logger.error(f"{store_id} 업데이트 실패: {e}")
skipped_count += 1
continue
return {
'updated_count': updated_count,
'skipped_count': skipped_count,
'update_details': update_details
}
def add_new_stores(self, new_stores: List[tuple], food_category: str, region: str) -> Dict[str, Any]:
"""
가게들을 Vector DB에 추가
"""
if not new_stores:
return {'added_count': 0, 'add_details': []}
documents = []
embeddings = []
metadatas = []
ids = []
added_count = 0
add_details = []
for store_id, store_info, reviews in new_stores:
try:
# 임베딩용 텍스트 생성
text_for_embedding = extract_text_for_embedding(store_info, reviews)
if not text_for_embedding or len(text_for_embedding.strip()) < 10:
logger.warning(f"⚠️ {store_id}: 임베딩 텍스트 부족, 추가 스킵")
continue
# 임베딩 생성
embedding = self.embedding_model.encode(text_for_embedding)
# 메타데이터 생성
metadata = create_metadata(store_info, food_category, region)
# 문서 ID 생성
document_id = create_store_hash(store_id, store_info.get('name', ''), region)
# 문서 데이터 생성
document_text = combine_store_and_reviews(store_info, reviews)
documents.append(document_text)
embeddings.append(embedding.tolist())
metadatas.append(metadata)
ids.append(document_id)
add_details.append({
'store_id': store_id,
'document_id': document_id,
'added_at': metadata['last_updated']
})
added_count += 1
except Exception as e:
logger.warning(f"⚠️ {store_id} 추가 실패: {e}")
continue
# Vector DB에 새 가게들 추가
if documents:
self.collection.add(
documents=documents,
embeddings=embeddings,
metadatas=metadatas,
ids=ids
)
logger.info(f"{added_count}개 새 가게 추가 완료")
return {
'added_count': added_count,
'add_details': add_details
}