ai-review/vector/app/main.py
2025-06-17 12:30:32 +09:00

735 lines
29 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# vector/app/main.py
import os
import sys
# =============================================================================
# .env 파일 로딩 (다른 import보다 먼저)
# =============================================================================
def is_kubernetes_env():
"""Kubernetes 환경 감지"""
return (
os.path.exists('/var/run/secrets/kubernetes.io/serviceaccount') or
os.getenv('KUBERNETES_SERVICE_HOST') or
os.getenv('ENVIRONMENT') == 'production'
)
# 조건부 dotenv 로딩
if not is_kubernetes_env():
try:
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from dotenv import load_dotenv
load_dotenv()
print("✅ 로컬 개발환경: .env 파일 로딩")
except ImportError:
print("⚠️ python-dotenv 없음, 환경변수만 사용")
else:
print(" Kubernetes/Production: ConfigMap/Secret 사용")
import logging
from contextlib import asynccontextmanager
from datetime import datetime
from typing import Optional
import asyncio
from fastapi import FastAPI, HTTPException, Depends, Path
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field
# 프로젝트 모듈 import
from app.config.settings import settings
from app.models.restaurant_models import RestaurantInfo
from app.models.vector_models import (
VectorBuildRequest, VectorBuildResponse,
ActionRecommendationRequest, ActionRecommendationResponse,
ActionRecommendationSimpleResponse,
VectorDBStatusResponse, VectorDBStatus,
FindReviewsResponse, StoredDataInfo
)
from app.services.restaurant_service import RestaurantService
from app.services.review_service import ReviewService
from app.services.vector_service import VectorService
from app.services.claude_service import ClaudeService
from app.utils.category_utils import extract_food_category
import json
# 로깅 설정
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[logging.StreamHandler(sys.stdout)]
)
logger = logging.getLogger(__name__)
# 🔧 전역 변수 대신 애플리케이션 상태로 관리
app_state = {
"vector_service": None,
"restaurant_service": None,
"review_service": None,
"claude_service": None,
"initialization_errors": {},
"startup_completed": False
}
# 추가 모델 정의 (find-reviews API용)
class FindReviewsRequest(BaseModel):
"""리뷰 검색 요청 모델"""
region: str = Field(
...,
description="지역 (시군구 + 읍면동)",
example="서울특별시 강남구 역삼동"
)
store_name: str = Field(
...,
description="가게명",
example="맛있는 한식당"
)
class RestaurantInfo(BaseModel):
"""음식점 정보 모델"""
id: str = Field(description="카카오 장소 ID")
place_name: str = Field(description="장소명")
category_name: str = Field(description="카테고리명")
address_name: str = Field(description="전체 주소")
phone: str = Field(description="전화번호")
place_url: str = Field(description="장소 상세페이지 URL")
x: str = Field(description="X 좌표값 (경도)")
y: str = Field(description="Y 좌표값 (위도)")
@asynccontextmanager
async def lifespan(app: FastAPI):
"""🔧 애플리케이션 생명주기 관리 - 안전한 서비스 초기화"""
# 🚀 Startup 이벤트
logger.info("🚀 Vector API 서비스 시작 중...")
startup_start_time = datetime.now()
# 각 서비스 안전하게 초기화
services_to_init = [
("restaurant_service", RestaurantService, "Restaurant API 서비스"),
("review_service", ReviewService, "Review API 서비스"),
("claude_service", ClaudeService, "Claude AI 서비스"),
("vector_service", VectorService, "Vector DB 서비스") # 마지막에 초기화
]
initialized_count = 0
for service_key, service_class, service_name in services_to_init:
try:
logger.info(f"🔧 {service_name} 초기화 중...")
app_state[service_key] = service_class()
logger.info(f"{service_name} 초기화 완료")
initialized_count += 1
except Exception as e:
logger.error(f"{service_name} 초기화 실패: {e}")
app_state["initialization_errors"][service_key] = str(e)
# 🔧 중요: 서비스 초기화 실패해도 앱은 시작 (헬스체크에서 확인)
continue
startup_time = (datetime.now() - startup_start_time).total_seconds()
app_state["startup_completed"] = True
logger.info(f"✅ Vector API 서비스 시작 완료!")
logger.info(f"📊 초기화 결과: {initialized_count}/{len(services_to_init)}개 서비스 성공")
logger.info(f"⏱️ 시작 소요시간: {startup_time:.2f}")
if app_state["initialization_errors"]:
logger.warning(f"⚠️ 초기화 실패 서비스: {list(app_state['initialization_errors'].keys())}")
yield
# 🛑 Shutdown 이벤트
logger.info("🛑 Vector API 서비스 종료 중...")
# 리소스 정리
for service_key in ["vector_service", "restaurant_service", "review_service", "claude_service"]:
if app_state[service_key] is not None:
try:
# 서비스별 정리 작업이 있다면 여기서 수행
logger.info(f"🔧 {service_key} 정리 중...")
except Exception as e:
logger.warning(f"⚠️ {service_key} 정리 실패: {e}")
finally:
app_state[service_key] = None
app_state["startup_completed"] = False
logger.info("✅ Vector API 서비스 종료 완료")
# 🔧 FastAPI 앱 초기화 (lifespan 이벤트 포함)
app = FastAPI(
title=settings.APP_TITLE,
description=f"""
{settings.APP_DESCRIPTION}
**주요 기능:**
- 지역과 가게명으로 대상 가게 찾기
- 동종 업체 리뷰 수집 및 분석
- Vector DB 구축 및 관리
- Claude AI 기반 액션 추천
- 영속적 Vector DB 저장
**API 연동:**
- Restaurant API: {settings.get_restaurant_api_url() if hasattr(settings, 'get_restaurant_api_url') else 'N/A'}
- Review API: {settings.get_review_api_url() if hasattr(settings, 'get_review_api_url') else 'N/A'}
- Claude AI API: {settings.CLAUDE_MODEL}
**Vector DB:**
- 경로: {settings.VECTOR_DB_PATH}
- 컬렉션: {settings.VECTOR_DB_COLLECTION}
- 임베딩 모델: {settings.EMBEDDING_MODEL}
**버전:** {settings.APP_VERSION}
""",
version=settings.APP_VERSION,
contact={
"name": "개발팀",
"email": "admin@example.com"
},
lifespan=lifespan # 🔧 lifespan 이벤트 등록
)
# CORS 설정
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# 🔧 Dependency Injection - 서비스 제공자들
def get_vector_service() -> VectorService:
"""VectorService 의존성 주입"""
if app_state["vector_service"] is None:
error_msg = app_state["initialization_errors"].get("vector_service")
if error_msg:
raise HTTPException(
status_code=503,
detail=f"Vector service not available: {error_msg}"
)
# 런타임에 재시도
try:
logger.info("🔧 VectorService 런타임 초기화 시도...")
app_state["vector_service"] = VectorService()
logger.info("✅ VectorService 런타임 초기화 성공")
except Exception as e:
logger.error(f"❌ VectorService 런타임 초기화 실패: {e}")
raise HTTPException(
status_code=503,
detail=f"Vector service initialization failed: {str(e)}"
)
return app_state["vector_service"]
def get_restaurant_service() -> RestaurantService:
"""RestaurantService 의존성 주입"""
if app_state["restaurant_service"] is None:
error_msg = app_state["initialization_errors"].get("restaurant_service")
if error_msg:
raise HTTPException(
status_code=503,
detail=f"Restaurant service not available: {error_msg}"
)
try:
app_state["restaurant_service"] = RestaurantService()
except Exception as e:
raise HTTPException(status_code=503, detail=str(e))
return app_state["restaurant_service"]
def get_review_service() -> ReviewService:
"""ReviewService 의존성 주입"""
if app_state["review_service"] is None:
error_msg = app_state["initialization_errors"].get("review_service")
if error_msg:
raise HTTPException(
status_code=503,
detail=f"Review service not available: {error_msg}"
)
try:
app_state["review_service"] = ReviewService()
except Exception as e:
raise HTTPException(status_code=503, detail=str(e))
return app_state["review_service"]
def get_claude_service() -> ClaudeService:
"""ClaudeService 의존성 주입"""
if app_state["claude_service"] is None:
error_msg = app_state["initialization_errors"].get("claude_service")
if error_msg:
raise HTTPException(
status_code=503,
detail=f"Claude service not available: {error_msg}"
)
try:
app_state["claude_service"] = ClaudeService()
except Exception as e:
raise HTTPException(status_code=503, detail=str(e))
return app_state["claude_service"]
@app.get("/", response_class=HTMLResponse, include_in_schema=False)
async def root():
"""메인 페이지"""
# 🔧 안전한 DB 상태 조회
try:
vector_service = app_state.get("vector_service")
if vector_service:
db_status = vector_service.get_db_status()
else:
db_status = {
'collection_name': settings.VECTOR_DB_COLLECTION,
'total_documents': 0,
'total_stores': 0,
'db_path': settings.VECTOR_DB_PATH,
'status': 'not_initialized'
}
except Exception as e:
logger.warning(f"DB 상태 조회 실패: {e}")
db_status = {'status': 'error', 'error': str(e)}
return f"""
<html>
<head>
<title>{settings.APP_TITLE}</title>
<style>
body {{ font-family: Arial, sans-serif; margin: 40px; background-color: #f5f5f5; }}
.container {{ max-width: 800px; margin: 0 auto; background: white; padding: 30px; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }}
h1 {{ color: #333; text-align: center; }}
.status {{ background: #e7f3ff; padding: 15px; border-radius: 5px; margin: 20px 0; }}
.info {{ background: #f8f9fa; padding: 15px; border-radius: 5px; margin: 20px 0; }}
.link {{ display: inline-block; margin: 10px; padding: 10px 20px; background: #007bff; color: white; text-decoration: none; border-radius: 5px; }}
.link:hover {{ background: #0056b3; }}
pre {{ background: #f8f9fa; padding: 15px; border-radius: 5px; overflow-x: auto; }}
ul {{ list-style-type: none; padding: 0; }}
li {{ margin: 5px 0; }}
</style>
</head>
<body>
<div class="container">
<h1>🍽️ {settings.APP_TITLE}</h1>
<p>{settings.APP_DESCRIPTION}</p>
<div class="status">
<h2>📊 Vector DB 상태</h2>
<ul>
<li><strong>컬렉션:</strong> {db_status.get('collection_name', 'N/A')}</li>
<li><strong>총 문서:</strong> {db_status.get('total_documents', 0)}</li>
<li><strong>총 가게:</strong> {db_status.get('total_stores', 0)}</li>
<li><strong>상태:</strong> {db_status.get('status', 'unknown')}</li>
</ul>
</div>
<div class="info">
<h2>🔧 시스템 구성</h2>
<ul>
<li><strong>Claude Model:</strong> {settings.CLAUDE_MODEL}</li>
<li><strong>Embedding Model:</strong> {settings.EMBEDDING_MODEL}</li>
<li><strong>Vector DB Path:</strong> {settings.VECTOR_DB_PATH}</li>
<li><strong>환경:</strong> {'Kubernetes' if hasattr(settings, 'IS_K8S_ENV') and settings.IS_K8S_ENV else 'Local'}</li>
</ul>
</div>
<h2>📚 API 문서</h2>
<a href="/docs" class="link">Swagger UI 문서</a>
<a href="/redoc" class="link">ReDoc 문서</a>
<a href="/health" class="link">헬스 체크</a>
<a href="/vector-status" class="link">Vector DB 상태</a>
<h2>🛠️ 사용 방법</h2>
<p><strong>POST /find-reviews</strong> - 리뷰 검색 및 Vector DB 저장 (본인 가게 우선)</p>
<pre>
{{
"region": "서울특별시 강남구 역삼동",
"store_name": "맛있는 한식당"
}}
</pre>
<p><strong>POST /action-recommendation-simple</strong> - 간소화된 액션 추천 요청</p>
<pre>
{{
"store_id": "12345",
"context": "매출이 감소하고 있어서 개선이 필요합니다"
}}
</pre>
</div>
</body>
</html>
"""
@app.post("/find-reviews", response_model=FindReviewsResponse)
async def find_reviews(
request: VectorBuildRequest, # VectorBuildRequest 재사용
vector_service: VectorService = Depends(get_vector_service),
restaurant_service: RestaurantService = Depends(get_restaurant_service),
review_service: ReviewService = Depends(get_review_service)
):
"""
지역과 가게명으로 리뷰를 찾아 Vector DB에 저장하고,
실제 저장된 JSON 구조를 응답으로 리턴합니다.
🔥 본인 가게 리뷰는 반드시 포함됩니다.
Note: force_rebuild 파라미터는 이 API에서는 무시됩니다.
"""
start_time = datetime.now()
logger.info(f"🔍 리뷰 검색 요청: {request.region} - {request.store_name}")
# force_rebuild 파라미터는 find-reviews에서는 무시
# (VectorBuildRequest를 재사용하지만 이 기능은 사용하지 않음)
try:
# 1단계: 본인 가게 검색
logger.info("1단계: 본인 가게 검색 중...")
target_restaurant = await restaurant_service.find_store_by_name_and_region(
request.region, request.store_name
)
if not target_restaurant:
logger.error(f"❌ 본인 가게를 찾을 수 없음: {request.store_name}")
return FindReviewsResponse(
success=False,
message=f"처리 중 오류가 발생했습니다: {str(e)}",
target_store=None,
food_category=None,
total_reviews=0,
total_stores=0,
execution_time=execution_time,
stored_data=None
)
logger.info(f"✅ 본인 가게 찾기 성공: {target_restaurant.place_name}")
# 2단계: 음식 카테고리 추출
food_category = extract_food_category(target_restaurant.category_name)
logger.info(f"🍽️ 추출된 음식 카테고리: {food_category}")
# 3단계: 본인 가게 리뷰 수집 (우선순위 1)
logger.info("3단계: 본인 가게 리뷰 수집 중...")
target_store_info, target_reviews = await review_service.collect_store_reviews(
target_restaurant.id, settings.MAX_REVIEWS_PER_RESTAURANT
)
if not target_reviews:
logger.warning("⚠️ 본인 가게 리뷰가 없습니다")
# 4단계: 동종 업체 검색
logger.info("4단계: 동종 업체 검색 중...")
similar_stores = await restaurant_service.find_similar_stores(
request.region,
food_category,
max_count=settings.MAX_RESTAURANTS_PER_CATEGORY # 50 → 환경변수
)
# 5단계: 동종 업체 리뷰 수집
logger.info("5단계: 동종 업체 리뷰 수집 중...")
review_results = []
# 본인 가게를 첫 번째로 추가
if target_store_info and target_reviews:
review_results.append((target_restaurant.id, target_store_info, target_reviews))
# 동종 업체 리뷰 수집 (본인 가게 제외)
similar_store_names = []
max_similar_reviews = settings.MAX_REVIEWS_PER_RESTAURANT
for store in similar_stores[:settings.MAX_RESTAURANTS_PER_CATEGORY]: # 환경변수 활용
store_info, reviews = await review_service.collect_store_reviews(
store.id,
max_reviews=max_similar_reviews # 동종업체는 더 적게
)
if store_info and reviews:
review_results.append((store.id, store_info, reviews))
similar_store_names.append(store.place_name)
# 6단계: Vector DB 저장 및 저장 데이터 수집
logger.info("6단계: Vector DB 저장 중...")
try:
target_store_info_dict = {
'id': target_restaurant.id,
'place_name': target_restaurant.place_name,
'category_name': target_restaurant.category_name,
'address_name': target_restaurant.address_name,
'phone': target_restaurant.phone,
'place_url': target_restaurant.place_url,
'x': target_restaurant.x,
'y': target_restaurant.y
}
# Vector DB에 저장하기 전에 저장될 데이터 구조 생성
stored_data = {}
# combine_store_and_reviews 함수를 사용하여 실제 저장될 JSON 구조 생성
from app.utils.data_utils import combine_store_and_reviews
import json
for store_id, store_info, reviews in review_results:
# Vector DB에 저장되는 실제 JSON 구조 생성
json_data = combine_store_and_reviews(store_info, reviews)
parsed_data = json.loads(json_data)
store_key = store_info.get('name', store_id)
stored_data[store_key] = StoredDataInfo(
store_info=parsed_data['store_info'],
reviews=parsed_data['reviews'],
review_summary=parsed_data['review_summary'],
combined_at=parsed_data['combined_at']
)
# Vector DB에 저장
vector_result = await vector_service.build_vector_store(
target_store_info_dict, review_results, food_category, request.region
)
if not vector_result.get('success', False):
raise Exception(f"Vector DB 저장 실패: {vector_result.get('error', 'Unknown error')}")
logger.info("✅ Vector DB 구축 완료")
except Exception as e:
logger.error(f"❌ Vector DB 구축 실패: {e}")
return FindReviewsResponse(
success=False,
message=f"Vector DB 구축 중 오류가 발생했습니다: {str(e)}",
target_store={
'id': target_restaurant.id,
'place_name': target_restaurant.place_name,
'category_name': target_restaurant.category_name,
'address_name': target_restaurant.address_name,
'phone': target_restaurant.phone,
'place_url': target_restaurant.place_url,
'x': target_restaurant.x,
'y': target_restaurant.y
},
food_category=food_category
)
# 성공 응답 - Vector DB 저장 데이터 포함
total_reviews = sum(len(reviews) for _, _, reviews in review_results)
execution_time = (datetime.now() - start_time).total_seconds()
return FindReviewsResponse(
success=True,
message=f"✅ 본인 가게 '{target_restaurant.place_name}' 리뷰 분석 완료! "
f"{total_reviews}개 리뷰, {len(review_results)}개 업체 분석됨",
target_store={
'id': target_restaurant.id,
'place_name': target_restaurant.place_name,
'category_name': target_restaurant.category_name,
'address_name': target_restaurant.address_name,
'phone': target_restaurant.phone,
'place_url': target_restaurant.place_url,
'x': target_restaurant.x,
'y': target_restaurant.y
},
food_category=food_category,
total_reviews=total_reviews,
total_stores=len(review_results),
execution_time=execution_time,
stored_data=stored_data
)
except Exception as e:
execution_time = (datetime.now() - start_time).total_seconds()
logger.error(f"❌ find_reviews 처리 실패: {str(e)}")
return FindReviewsResponse(
success=False,
message=f"처리 중 오류가 발생했습니다: {str(e)}",
execution_time=execution_time
)
@app.post(
"/action-recommendation",
response_model=ActionRecommendationSimpleResponse,
summary="간소화된 액션 추천 요청",
description="JSON 추천 결과만 반환하는 최적화된 엔드포인트 + INPUT 데이터 포함"
)
async def action_recommendation_simple(
request: ActionRecommendationRequest,
claude_service: ClaudeService = Depends(get_claude_service),
vector_service: VectorService = Depends(get_vector_service)
):
"""🧠 최적화된 Claude AI 액션 추천 API - JSON만 반환 + INPUT 데이터 포함"""
try:
logger.info(f"간소화된 액션 추천 요청: store_id={request.store_id}")
# 1단계: Vector DB에서 최적화된 컨텍스트 조회 (개선된 버전)
context_data = None
structured_input = None
try:
# 개선된 검색 메소드 사용 - 이제 Dict 반환
context_data_dict = vector_service.search_similar_cases_improved(
request.store_id,
request.context
)
if context_data_dict:
# Claude에게 전달할 텍스트 형태로 변환
context_data = json.dumps(context_data_dict, ensure_ascii=False, indent=2)
# API 응답에 포함할 구조화된 INPUT 데이터
structured_input = context_data_dict
except Exception as e:
logger.warning(f"Vector DB 조회 실패 (계속 진행): {e}")
# 2단계: Claude AI 호출 - JSON만 추출
try:
# 컨텍스트 구성
full_context = f"가게 ID: {request.store_id}\n점주 요청: {request.context}"
# Claude AI 호출
claude_response, parsed_json = await claude_service.generate_action_recommendations(
context=full_context,
additional_context=context_data
)
if not parsed_json:
# JSON 파싱 실패시 재시도
logger.warning("JSON 파싱 실패 - 재시도")
parsed_json = claude_service.parse_recommendation_response(claude_response)
if not parsed_json:
raise Exception("Claude AI 응답을 JSON으로 파싱할 수 없습니다")
return ActionRecommendationSimpleResponse(
success=True,
recommendation=parsed_json,
input_data=structured_input # 새로 추가: Claude에게 전달된 INPUT 데이터
)
except Exception as e:
logger.error(f"Claude AI 호출 실패: {e}")
return ActionRecommendationSimpleResponse(
success=False,
error_message=f"AI 추천 생성 중 오류: {str(e)}",
input_data=structured_input # 실패해도 INPUT 데이터는 포함
)
except Exception as e:
logger.error(f"액션 추천 처리 실패: {e}")
return ActionRecommendationSimpleResponse(
success=False,
error_message=f"서버 내부 오류: {str(e)}",
input_data=None
)
@app.get(
"/vector-status",
response_model=VectorDBStatusResponse,
summary="Vector DB 상태 조회",
description="Vector DB의 현재 상태를 조회합니다."
)
async def get_vector_status(vector_service: VectorService = Depends(get_vector_service)):
"""Vector DB 상태를 조회합니다."""
try:
db_status = vector_service.get_db_status()
status = VectorDBStatus(
collection_name=db_status['collection_name'],
total_documents=db_status['total_documents'],
total_stores=db_status['total_stores'],
db_path=db_status['db_path'],
last_updated=db_status.get('last_updated')
)
return VectorDBStatusResponse(
success=True,
status=status,
message="Vector DB 상태 조회 성공"
)
except Exception as e:
logger.error(f"Vector DB 상태 조회 실패: {e}")
return VectorDBStatusResponse(
success=False,
status=VectorDBStatus(
collection_name="unknown",
total_documents=0,
total_stores=0,
db_path="unknown"
),
message=f"상태 조회 실패: {str(e)}"
)
@app.get("/health")
async def health_check():
"""헬스체크 엔드포인트"""
return {
"status": "healthy",
"timestamp": datetime.now().isoformat(),
"services": {
"vector_service": app_state["vector_service"] is not None,
"restaurant_service": app_state["restaurant_service"] is not None,
"review_service": app_state["review_service"] is not None,
"claude_service": app_state["claude_service"] is not None,
},
"initialization_errors": app_state["initialization_errors"]
}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host=settings.HOST, port=settings.PORT)
@app.get(
"/store/{store_id}",
response_model=StoreInfoResponse,
summary="매장 정보 조회",
description="store_id를 이용하여 Vector DB에서 매장 기본정보와 리뷰 정보를 조회합니다"
)
async def get_store_info(
store_id: str = Path(..., description="조회할 매장 ID", example="501745730"),
vector_service: VectorService = Depends(get_vector_service)
):
"""🏪 store_id로 매장 정보 조회 API"""
start_time = datetime.now()
try:
logger.info(f"🔍 매장 정보 조회 요청: store_id={store_id}")
# Vector DB에서 매장 정보 조회
store_data = vector_service.get_store_by_id(store_id)
execution_time = (datetime.now() - start_time).total_seconds()
if not store_data:
return StoreInfoResponse(
success=False,
message=f"매장 정보를 찾을 수 없습니다: store_id={store_id}",
store_id=store_id,
execution_time=execution_time
)
# 성공 응답
store_info = store_data.get('store_info', {})
reviews = store_data.get('reviews', [])
review_summary = store_data.get('review_summary', {})
metadata = store_data.get('metadata', {})
return StoreInfoResponse(
success=True,
message=f"매장 정보 조회 성공: {store_info.get('place_name', store_id)}",
store_id=store_id,
store_info=store_info,
reviews=reviews,
review_summary=review_summary,
metadata=metadata,
last_updated=store_data.get('combined_at'),
total_reviews=len(reviews),
execution_time=execution_time
)
except Exception as e:
execution_time = (datetime.now() - start_time).total_seconds()
logger.error(f"❌ 매장 정보 조회 실패: store_id={store_id}, error={e}")
return StoreInfoResponse(
success=False,
message=f"매장 정보 조회 중 오류 발생: {str(e)}",
store_id=store_id,
execution_time=execution_time
)