# 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 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 # 로깅 설정 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 좌표값 (위도)") class FindReviewsResponse(BaseModel): """리뷰 검색 응답 모델""" success: bool = Field(description="검색 성공 여부") message: str = Field(description="응답 메시지") target_store: RestaurantInfo = Field(description="대상 가게 정보") total_stores: int = Field(description="수집된 총 가게 수") total_reviews: int = Field(description="수집된 총 리뷰 수") food_category: str = Field(description="추출된 음식 카테고리") region: str = Field(description="검색 지역") @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"""
{settings.APP_DESCRIPTION}
POST /find-reviews - 리뷰 검색 및 Vector DB 저장 (본인 가게 우선)
{{
"region": "서울특별시 강남구 역삼동",
"store_name": "맛있는 한식당"
}}
POST /action-recommendation-simple - 간소화된 액션 추천 요청
{{
"store_id": "12345",
"context": "매출이 감소하고 있어서 개선이 필요합니다"
}}