#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 카카오맵 리뷰 분석 API (로깅 강화 버전) ConfigMap과 Secret을 활용한 Kubernetes 배포용 app/main.py """ import os from fastapi import FastAPI, HTTPException, BackgroundTasks from fastapi.responses import HTMLResponse from pydantic import BaseModel, Field from typing import Optional, List, Dict, Any import asyncio import threading import json from datetime import datetime, timedelta import logging import sys import subprocess import platform # 기존 분석기 import from bs4 import BeautifulSoup import re import time # ============================================================================= # .env 파일 로딩 (다른 import보다 먼저) # ============================================================================= from dotenv import load_dotenv # .env 파일에서 환경변수 로드 load_dotenv() # ============================================================================= # 로깅 설정 (가장 먼저) # ============================================================================= logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', handlers=[ logging.StreamHandler(sys.stdout) ] ) logger = logging.getLogger(__name__) # ============================================================================= # 시스템 정보 로깅 # ============================================================================= logger.info("="*60) logger.info("카카오맵 리뷰 분석 API 시작") logger.info("="*60) logger.info(f"Python 버전: {sys.version}") logger.info(f"플랫폼: {platform.platform()}") logger.info(f"아키텍처: {platform.architecture()}") logger.info(f"현재 작업 디렉토리: {os.getcwd()}") # Python 경로 확인 logger.info("Python 경로 정보:") for i, path in enumerate(sys.path): logger.info(f" [{i}] {path}") # ============================================================================= # 패키지 설치 상태 확인 # ============================================================================= def check_package_installation(): """설치된 패키지 확인""" logger.info("패키지 설치 상태 확인 중...") required_packages = [ 'selenium', 'webdriver-manager', 'beautifulsoup4', 'fastapi', 'uvicorn', 'pydantic' ] for package in required_packages: try: result = subprocess.run([sys.executable, '-m', 'pip', 'show', package], capture_output=True, text=True) if result.returncode == 0: version_line = [line for line in result.stdout.split('\n') if line.startswith('Version:')] version = version_line[0].split(':')[1].strip() if version_line else 'Unknown' logger.info(f"✅ {package}: {version}") else: logger.error(f"❌ {package}: 설치되지 않음") except Exception as e: logger.error(f"❌ {package} 확인 실패: {e}") check_package_installation() # ============================================================================= # Selenium 관련 import (세분화된 로깅) # ============================================================================= logger.info("Selenium 관련 모듈 import 시작...") SELENIUM_AVAILABLE = False SELENIUM_IMPORT_ERRORS = [] # 각 모듈을 개별적으로 import하여 어디서 실패하는지 확인 selenium_modules = [ ('selenium', 'selenium'), ('selenium.webdriver', 'webdriver'), ('selenium.webdriver.chrome.options', 'Options'), ('selenium.webdriver.common.by', 'By'), ('selenium.webdriver.support.ui', 'WebDriverWait'), ('selenium.webdriver.support', 'expected_conditions as EC'), ('webdriver_manager.chrome', 'ChromeDriverManager'), ('selenium.webdriver.chrome.service', 'Service') ] imported_modules = {} for module_path, import_name in selenium_modules: try: logger.info(f" 📦 {module_path}.{import_name} import 시도...") if import_name == 'selenium': import selenium imported_modules['selenium'] = selenium logger.info(f" ✅ selenium 버전: {selenium.__version__}") elif import_name == 'webdriver': from selenium import webdriver imported_modules['webdriver'] = webdriver logger.info(f" ✅ webdriver import 성공") elif import_name == 'Options': from selenium.webdriver.chrome.options import Options imported_modules['Options'] = Options logger.info(f" ✅ Options import 성공") elif import_name == 'By': from selenium.webdriver.common.by import By imported_modules['By'] = By logger.info(f" ✅ By import 성공") elif import_name == 'WebDriverWait': from selenium.webdriver.support.ui import WebDriverWait imported_modules['WebDriverWait'] = WebDriverWait logger.info(f" ✅ WebDriverWait import 성공") elif import_name == 'expected_conditions as EC': from selenium.webdriver.support import expected_conditions as EC imported_modules['EC'] = EC logger.info(f" ✅ expected_conditions import 성공") elif import_name == 'ChromeDriverManager': from webdriver_manager.chrome import ChromeDriverManager imported_modules['ChromeDriverManager'] = ChromeDriverManager logger.info(f" ✅ ChromeDriverManager import 성공") elif import_name == 'Service': from selenium.webdriver.chrome.service import Service imported_modules['Service'] = Service logger.info(f" ✅ Service import 성공") except ImportError as e: error_msg = f"{module_path}.{import_name} import 실패: {e}" logger.error(f" ❌ {error_msg}") SELENIUM_IMPORT_ERRORS.append(error_msg) except Exception as e: error_msg = f"{module_path}.{import_name} import 중 예외 발생: {e}" logger.error(f" ❌ {error_msg}") SELENIUM_IMPORT_ERRORS.append(error_msg) # 모든 모듈이 성공적으로 import되었는지 확인 required_modules = ['webdriver', 'Options', 'By', 'WebDriverWait', 'EC', 'ChromeDriverManager', 'Service'] missing_modules = [mod for mod in required_modules if mod not in imported_modules] if missing_modules: logger.error(f"❌ 누락된 모듈들: {missing_modules}") SELENIUM_AVAILABLE = False else: logger.info("✅ 모든 Selenium 모듈 import 성공!") SELENIUM_AVAILABLE = True # 전역 변수로 설정 webdriver = imported_modules['webdriver'] Options = imported_modules['Options'] By = imported_modules['By'] WebDriverWait = imported_modules['WebDriverWait'] EC = imported_modules['EC'] ChromeDriverManager = imported_modules['ChromeDriverManager'] Service = imported_modules['Service'] logger.info(f"Selenium 사용 가능 여부: {SELENIUM_AVAILABLE}") if SELENIUM_IMPORT_ERRORS: logger.error("Import 오류 목록:") for error in SELENIUM_IMPORT_ERRORS: logger.error(f" - {error}") # ============================================================================= # Chrome 설치 상태 확인 # ============================================================================= def check_chrome_installation(): """Chrome 브라우저 설치 상태 확인""" logger.info("Chrome 브라우저 설치 상태 확인 중...") chrome_paths = [ '/usr/bin/google-chrome', '/usr/bin/google-chrome-stable', '/usr/bin/chromium-browser', '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe', 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe' ] chrome_found = False for path in chrome_paths: if os.path.exists(path): logger.info(f" ✅ Chrome 발견: {path}") chrome_found = True # 버전 확인 시도 try: result = subprocess.run([path, '--version'], capture_output=True, text=True, timeout=5) if result.returncode == 0: logger.info(f" 📋 Chrome 버전: {result.stdout.strip()}") except Exception as e: logger.warning(f" ⚠️ Chrome 버전 확인 실패: {e}") break if not chrome_found: logger.error(" ❌ Chrome 브라우저가 설치되지 않음") # 시스템 명령어로 확인 시도 try: result = subprocess.run(['which', 'google-chrome'], capture_output=True, text=True) if result.returncode == 0: logger.info(f" ✅ PATH에서 Chrome 발견: {result.stdout.strip()}") chrome_found = True except: pass try: result = subprocess.run(['google-chrome', '--version'], capture_output=True, text=True, timeout=5) if result.returncode == 0: logger.info(f" ✅ 명령어로 Chrome 실행 가능: {result.stdout.strip()}") chrome_found = True except Exception as e: logger.error(f" ❌ Chrome 명령어 실행 실패: {e}") return chrome_found chrome_available = check_chrome_installation() # ============================================================================= # 환경 변수 설정 # ============================================================================= class Config: """환경 변수 기반 설정 클래스""" # 애플리케이션 메타데이터 APP_TITLE = os.getenv("APP_TITLE", "카카오맵 리뷰 분석 API") APP_VERSION = os.getenv("APP_VERSION", "1.0.0") APP_DESCRIPTION = os.getenv("APP_DESCRIPTION", "교육 목적 전용 - 실제 서비스 사용 금지") # 서버 설정 HOST = os.getenv("HOST", "0.0.0.0") PORT = int(os.getenv("PORT", "8000")) WORKERS = int(os.getenv("WORKERS", "1")) LOG_LEVEL = os.getenv("LOG_LEVEL", "info") # API 기본값 DEFAULT_MAX_TIME = int(os.getenv("DEFAULT_MAX_TIME", "300")) DEFAULT_DAYS_LIMIT = int(os.getenv("DEFAULT_DAYS_LIMIT", "7")) MAX_DAYS_LIMIT = int(os.getenv("MAX_DAYS_LIMIT", "365")) MIN_MAX_TIME = int(os.getenv("MIN_MAX_TIME", "60")) MAX_MAX_TIME = int(os.getenv("MAX_MAX_TIME", "1800")) # Chrome 옵션 (ConfigMap에서 멀티라인 문자열로 받음) CHROME_OPTIONS_RAW = os.getenv("CHROME_OPTIONS", """ --headless --no-sandbox --disable-dev-shm-usage --disable-gpu --window-size=1920,1080 --disable-extensions --disable-plugins --disable-usb-keyboard-detect --no-first-run --no-default-browser-check --disable-logging --log-level=3 """) @property def CHROME_OPTIONS_LIST(self): """Chrome 옵션을 리스트로 파싱""" return [opt.strip() for opt in self.CHROME_OPTIONS_RAW.strip().split('\n') if opt.strip()] # 스크롤링 설정 SCROLL_CHECK_INTERVAL = int(os.getenv("SCROLL_CHECK_INTERVAL", "3")) SCROLL_NO_CHANGE_LIMIT = int(os.getenv("SCROLL_NO_CHANGE_LIMIT", "8")) SCROLL_WAIT_TIME_SHORT = float(os.getenv("SCROLL_WAIT_TIME_SHORT", "1.5")) SCROLL_WAIT_TIME_LONG = float(os.getenv("SCROLL_WAIT_TIME_LONG", "2.0")) # 법적 경고 LEGAL_WARNING_ENABLED = os.getenv("LEGAL_WARNING_ENABLED", "true").lower() == "true" CONTACT_EMAIL = os.getenv("CONTACT_EMAIL", "admin@example.com") # 건강 체크 HEALTH_CHECK_TIMEOUT = int(os.getenv("HEALTH_CHECK_TIMEOUT", "5")) # Secret 값들 (현재는 사용하지 않지만 향후 확장용) EXTERNAL_API_KEY = os.getenv("EXTERNAL_API_KEY", "") DB_USERNAME = os.getenv("DB_USERNAME", "") DB_PASSWORD = os.getenv("DB_PASSWORD", "") JWT_SECRET = os.getenv("JWT_SECRET", "") # 설정 인스턴스 config = Config() logger.info("환경 설정 로드 완료:") logger.info(f" - Chrome 옵션 개수: {len(config.CHROME_OPTIONS_LIST)}") logger.info(f" - 기본 최대 시간: {config.DEFAULT_MAX_TIME}초") logger.info(f" - 로그 레벨: {config.LOG_LEVEL}") # FastAPI 앱 초기화 (환경변수 사용) app = FastAPI( title=config.APP_TITLE, description=f""" **⚠️ 중요 법적 경고사항 ⚠️** {config.APP_DESCRIPTION} **진단 정보:** - Selenium 사용 가능: {SELENIUM_AVAILABLE} - Chrome 설치됨: {chrome_available} - Import 오류 개수: {len(SELENIUM_IMPORT_ERRORS)} **법적 위험:** - 카카오 이용약관 위반 → 계정 정지, 법적 조치 - 개인정보보호법(PIPA) 위반 → 과태료 최대 3억원 - 저작권 및 데이터베이스권 침해 → 손해배상 - 업무방해죄 → 5년 이하 징역 또는 1천500만원 이하 벌금 **합법적 대안:** - 카카오 공식 API 활용 (developers.kakao.com) - 점주 대상 자체 가게 관리 서비스 개발 - 사용자 동의 기반 데이터 수집 앱 - 카카오와 정식 파트너십 체결 **이 API를 실제 서비스에 사용하지 마세요!** **환경:** 로컬 개발 **버전:** {config.APP_VERSION} **연락처:** {config.CONTACT_EMAIL} """, version=config.APP_VERSION, contact={ "name": "관리자", "email": config.CONTACT_EMAIL }, license_info={ "name": "Educational Use Only", "url": "https://developers.kakao.com" } ) # Pydantic 모델 정의 (기존과 동일) class ReviewAnalysisRequest(BaseModel): """리뷰 분석 요청 모델""" store_id: str = Field( ..., description="카카오맵 가게 ID (예: 501745730)", example="501745730", min_length=1, max_length=20 ) days_limit: Optional[int] = Field( None, description=f"며칠 이후의 리뷰만 수집할지 (None이면 모든 날짜, 최대 {config.MAX_DAYS_LIMIT}일)", example=config.DEFAULT_DAYS_LIMIT, ge=1, le=config.MAX_DAYS_LIMIT ) max_time: int = Field( config.DEFAULT_MAX_TIME, description=f"최대 스크롤 시간(초, {config.MIN_MAX_TIME}-{config.MAX_MAX_TIME}초)", example=config.DEFAULT_MAX_TIME, ge=config.MIN_MAX_TIME, le=config.MAX_MAX_TIME ) class ReviewerStats(BaseModel): """리뷰어 통계 정보""" reviews: Optional[int] = None average_rating: Optional[float] = None followers: Optional[int] = None class ReviewData(BaseModel): """개별 리뷰 데이터""" reviewer_name: str reviewer_level: str reviewer_stats: ReviewerStats rating: int date: str content: str badges: List[str] likes: int photo_count: int has_photos: bool class StoreInfo(BaseModel): """가게 정보 (수정됨: id 필드 추가)""" id: str = Field(description="가게 ID") name: str category: str rating: str review_count: str status: str address: str class DateFilter(BaseModel): """날짜 필터 정보""" cutoff_date: Optional[str] = None filtered: bool class ReviewAnalysisResponse(BaseModel): """리뷰 분석 응답 모델""" success: bool = Field(description="분석 성공 여부") message: str = Field(description="응답 메시지") store_info: Optional[StoreInfo] = None reviews: List[ReviewData] = [] analysis_date: str = Field(description="분석 수행 날짜시간") total_reviews: int = Field(description="수집된 총 리뷰 수") analysis_method: str = "selenium" date_filter: DateFilter execution_time: float = Field(description="실행 시간(초)") class ErrorResponse(BaseModel): """에러 응답 모델""" success: bool = False error: str message: str timestamp: str # 분석기 클래스 (로깅 강화) class KakaoReviewAnalyzerAPI: """로깅 강화된 카카오맵 리뷰 분석기""" def __init__(self): self.store_info = {} self.reviews = [] self.cutoff_date = None self.config = config logger.info("KakaoReviewAnalyzerAPI 인스턴스 생성 완료") def analyze_by_store_id(self, store_id: str, days_limit: Optional[int] = None, max_scroll_time: int = None): """Selenium을 사용한 동적 리뷰 분석 (ChromeDriver 경로 수정)""" logger.info("="*60) logger.info("analyze_by_store_id 함수 시작") logger.info("="*60) if max_scroll_time is None: max_scroll_time = self.config.DEFAULT_MAX_TIME logger.info(f"입력 파라미터:") logger.info(f" - store_id: {store_id}") logger.info(f" - days_limit: {days_limit}") logger.info(f" - max_scroll_time: {max_scroll_time}") # 날짜 제한 설정 if days_limit is not None: self.cutoff_date = datetime.now() - timedelta(days=days_limit) logger.info(f"수집 기준일 설정: {self.cutoff_date.strftime('%Y.%m.%d')} 이후 리뷰만 수집") else: self.cutoff_date = None logger.info("날짜 제한 없음: 모든 날짜의 리뷰 수집") target_url = f"https://place.map.kakao.com/{store_id}#comment" logger.info(f"대상 URL 생성: {target_url}") driver = None temp_profile = None step_counter = 0 try: # Step 1: Chrome 옵션 설정 step_counter += 1 logger.info(f"[단계 {step_counter}] Chrome 옵션 설정 중...") if not SELENIUM_AVAILABLE: raise Exception(f"Selenium을 사용할 수 없음. Import 오류: {SELENIUM_IMPORT_ERRORS}") options = Options() logger.info(f" Chrome Options 객체 생성 완료") for i, option in enumerate(self.config.CHROME_OPTIONS_LIST): logger.info(f" 옵션 [{i+1}] 추가: {option}") options.add_argument(option) # User Agent 추가 user_agent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' logger.info(f" User Agent 설정: {user_agent}") options.add_argument(f'--user-agent={user_agent}') # 추가 최적화 옵션들 experimental_options = ['enable-logging'] logger.info(f" 실험적 옵션 제외: {experimental_options}") options.add_experimental_option('excludeSwitches', experimental_options) options.add_experimental_option('useAutomationExtension', False) logger.info(f" Chrome 옵션 설정 완료 (총 {len(self.config.CHROME_OPTIONS_LIST)+1}개)") # Step 2: WebDriver 초기화 (ChromeDriverManager 사용 중단) step_counter += 1 logger.info(f"[단계 {step_counter}] Chrome WebDriver 초기화 중...") # 🔧 시스템에 설치된 ChromeDriver 사용 chromedriver_paths = [ "/usr/local/bin/chromedriver", # Dockerfile에서 설치한 경로 "/usr/bin/chromedriver", "/opt/chromedriver/chromedriver", "./chromedriver" ] driver_path = None for path in chromedriver_paths: if os.path.exists(path) and os.access(path, os.X_OK): logger.info(f" ChromeDriver 발견: {path}") # 버전 확인 try: import subprocess result = subprocess.run([path, "--version"], capture_output=True, text=True, timeout=5) if result.returncode == 0: logger.info(f" ChromeDriver 버전: {result.stdout.strip()}") driver_path = path break except Exception as version_error: logger.warning(f" {path} 버전 확인 실패: {version_error}") continue if not driver_path: # 마지막 수단: ChromeDriverManager 사용하되 올바른 실행 파일 찾기 logger.info(" 시스템 ChromeDriver 없음 - ChromeDriverManager 사용...") try: # 🔧 ChromeDriverManager 개선 os.environ['WDM_LOCAL'] = '/tmp/.wdm' downloaded_path = ChromeDriverManager(cache_valid_range=7).install() logger.info(f" 다운로드된 경로: {downloaded_path}") # 다운로드된 디렉토리에서 실제 실행 파일 찾기 download_dir = os.path.dirname(downloaded_path) possible_paths = [ os.path.join(download_dir, "chromedriver"), os.path.join(download_dir, "chromedriver-linux64", "chromedriver"), downloaded_path ] for candidate_path in possible_paths: if os.path.exists(candidate_path) and os.access(candidate_path, os.X_OK): # 파일이 실제 실행 파일인지 확인 (텍스트 파일이 아닌지) try: with open(candidate_path, 'rb') as f: header = f.read(4) # ELF 헤더 확인 (리눅스 실행 파일) if header.startswith(b'\x7fELF'): logger.info(f" 실행 가능한 ChromeDriver 발견: {candidate_path}") driver_path = candidate_path break except Exception as file_check_error: logger.warning(f" 파일 확인 실패: {candidate_path} - {file_check_error}") continue if not driver_path: raise Exception(f"다운로드된 디렉토리에서 실행 가능한 chromedriver를 찾을 수 없음: {download_dir}") except Exception as cdm_error: logger.error(f" ChromeDriverManager 실패: {cdm_error}") raise Exception(f"ChromeDriver를 찾을 수 없음. 시스템에 chromedriver를 설치하거나 Docker 이미지를 다시 빌드하세요.") # Service 객체 생성 logger.info(f" ChromeDriver 경로 사용: {driver_path}") service = Service(driver_path) # 🔧 Chrome 실행 환경 설정 logger.info(" Chrome 실행 환경 설정...") # 임시 프로필 디렉토리 생성 import tempfile temp_profile = tempfile.mkdtemp(prefix='chrome_profile_') logger.info(f" 임시 Chrome 프로필 생성: {temp_profile}") # Chrome 옵션에 프로필 경로 및 추가 안정성 옵션 추가 options.add_argument(f'--user-data-dir={temp_profile}') options.add_argument('--disable-web-security') options.add_argument('--allow-running-insecure-content') options.add_argument('--disable-blink-features=AutomationControlled') options.add_experimental_option("excludeSwitches", ["enable-automation"]) options.add_experimental_option('useAutomationExtension', False) logger.info(" 추가 안정성 옵션 적용 완료") # WebDriver 생성 logger.info(" Chrome WebDriver 인스턴스 생성 중...") try: driver = webdriver.Chrome(service=service, options=options) logger.info(" Chrome WebDriver 객체 생성 완료") # 🔧 즉시 연결 상태 확인 logger.info(" Chrome 연결 상태 확인...") driver.set_page_load_timeout(30) driver.set_script_timeout(30) driver.implicitly_wait(10) # 기본 페이지로 이동하여 연결 테스트 logger.info(" 연결 테스트용 빈 페이지 로드...") driver.get("data:text/html,
{config.APP_DESCRIPTION}
합법적 대안을 사용하세요:
버전: {config.APP_VERSION}
환경: 로컬 개발
연락처: {config.CONTACT_EMAIL}
POST /analyze - 리뷰 분석 수행
{{
"store_id": "501745730",
"days_limit": {config.DEFAULT_DAYS_LIMIT},
"max_time": {config.DEFAULT_MAX_TIME}
}}
"""
@app.post(
"/analyze",
response_model=ReviewAnalysisResponse,
summary="카카오맵 리뷰 분석 (로깅 강화)",
description=f"""
**⚠️ {config.APP_DESCRIPTION}**
**진단 정보:**
- Selenium 사용 가능: {SELENIUM_AVAILABLE}
- Chrome 설치됨: {chrome_available}
- Import 오류 개수: {len(SELENIUM_IMPORT_ERRORS)}
지정한 카카오맵 가게의 리뷰를 분석합니다.
**주요 기능:**
- 실시간 리뷰 수집 및 분석
- 날짜 범위 필터링 (최대 {config.MAX_DAYS_LIMIT}일)
- 감정 분석 및 통계
- JSON 형태 응답
- 상세한 로깅 및 디버그 정보
**응답 시간:** 설정에 따라 {config.MIN_MAX_TIME//60}-{config.MAX_MAX_TIME//60}분 소요
**환경:** 로컬 개발 버전
""",
responses={
200: {"description": "분석 성공", "model": ReviewAnalysisResponse},
400: {"description": "잘못된 요청", "model": ErrorResponse},
500: {"description": "서버 오류", "model": ErrorResponse}
}
)
async def analyze_reviews(request: ReviewAnalysisRequest):
"""리뷰 분석 API (수정됨: environment 섹션 제거)"""
logger.info("="*60)
logger.info("analyze_reviews API 호출")
logger.info("="*60)
logger.info(f"요청 파라미터: {request}")
# Selenium 사용 가능성 확인
if not SELENIUM_AVAILABLE:
error_detail = {
"success": False,
"error": "SELENIUM_NOT_AVAILABLE",
"message": "Selenium이 설치되지 않았습니다. pip install selenium webdriver-manager",
"timestamp": datetime.now().isoformat(),
"debug_info": {
"import_errors": SELENIUM_IMPORT_ERRORS,
"python_version": sys.version,
"platform": platform.platform(),
"chrome_available": chrome_available
}
}
logger.error("Selenium 사용 불가능!")
logger.error(f"Import 오류: {SELENIUM_IMPORT_ERRORS}")
raise HTTPException(status_code=500, detail=error_detail)
# Chrome 사용 가능성 확인
if not chrome_available:
error_detail = {
"success": False,
"error": "CHROME_NOT_AVAILABLE",
"message": "Chrome 브라우저가 설치되지 않았습니다.",
"timestamp": datetime.now().isoformat()
}
logger.error("Chrome 브라우저 사용 불가능!")
raise HTTPException(status_code=500, detail=error_detail)
start_time = time.time()
try:
logger.info(f"리뷰 분석 시작: store_id={request.store_id}, days_limit={request.days_limit}, max_time={request.max_time}")
# 분석기 실행
def run_analysis():
analyzer = KakaoReviewAnalyzerAPI()
return analyzer.analyze_by_store_id(
store_id=request.store_id,
days_limit=request.days_limit,
max_scroll_time=request.max_time
)
# 스레드에서 실행
loop = asyncio.get_event_loop()
store_info, reviews = await loop.run_in_executor(None, run_analysis)
execution_time = time.time() - start_time
# 응답 데이터 구성 (environment 섹션 제거됨)
response_data = ReviewAnalysisResponse(
success=True,
message="분석이 성공적으로 완료되었습니다.",
store_info=StoreInfo(**store_info) if store_info else None,
reviews=[
ReviewData(
reviewer_name=review['reviewer_name'],
reviewer_level=review['reviewer_level'],
reviewer_stats=ReviewerStats(**review['reviewer_stats']),
rating=review['rating'],
date=review['date'],
content=review['content'],
badges=review['badges'],
likes=review['likes'],
photo_count=review['photo_count'],
has_photos=review['has_photos']
) for review in reviews
],
analysis_date=datetime.now().isoformat(),
total_reviews=len(reviews),
date_filter=DateFilter(
cutoff_date=(datetime.now() - timedelta(days=request.days_limit)).isoformat() if request.days_limit else None,
filtered=request.days_limit is not None
),
execution_time=execution_time
)
logger.info(f"분석 완료: {len(reviews)}개 리뷰, {execution_time:.1f}초 소요")
logger.info("="*60)
return response_data
except Exception as e:
execution_time = time.time() - start_time
logger.error(f"분석 실패: {str(e)}")
logger.error("="*60)
raise HTTPException(
status_code=500,
detail={
"success": False,
"error": "ANALYSIS_FAILED",
"message": f"리뷰 분석 중 오류가 발생했습니다: {str(e)}",
"timestamp": datetime.now().isoformat(),
"execution_time": execution_time,
"debug_info": {
"selenium_available": SELENIUM_AVAILABLE,
"chrome_available": chrome_available,
"import_errors": SELENIUM_IMPORT_ERRORS,
"request_params": {
"store_id": request.store_id,
"days_limit": request.days_limit,
"max_time": request.max_time
}
}
}
)
@app.get("/diagnostic", summary="상세 진단 정보", description="시스템 상태를 상세히 진단합니다.")
async def diagnostic():
"""상세 진단 정보 제공"""
# 패키지 버전 확인
package_info = {}
required_packages = ['selenium', 'webdriver-manager', 'beautifulsoup4', 'fastapi', 'uvicorn', 'pydantic']
for package in required_packages:
try:
result = subprocess.run([sys.executable, '-m', 'pip', 'show', package],
capture_output=True, text=True)
if result.returncode == 0:
version_line = [line for line in result.stdout.split('\n') if line.startswith('Version:')]
version = version_line[0].split(':')[1].strip() if version_line else 'Unknown'
package_info[package] = {"installed": True, "version": version}
else:
package_info[package] = {"installed": False, "version": None}
except Exception as e:
package_info[package] = {"installed": False, "error": str(e)}
return {
"system_info": {
"python_version": sys.version,
"platform": platform.platform(),
"architecture": platform.architecture(),
"current_directory": os.getcwd(),
"python_path": sys.path[:5] # 처음 5개만
},
"selenium_info": {
"available": SELENIUM_AVAILABLE,
"import_errors": SELENIUM_IMPORT_ERRORS,
"imported_modules": list(imported_modules.keys()) if SELENIUM_AVAILABLE else []
},
"chrome_info": {
"available": chrome_available,
"common_paths_checked": [
'/usr/bin/google-chrome',
'/usr/bin/google-chrome-stable',
'/usr/bin/chromium-browser',
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'
]
},
"package_info": package_info,
"timestamp": datetime.now().isoformat()
}
@app.get(
"/health",
summary="헬스 체크",
description="API 서버 상태를 확인합니다."
)
async def health_check():
"""헬스 체크 엔드포인트 (진단 정보 포함)"""
return {
"status": "healthy",
"timestamp": datetime.now().isoformat(),
"selenium_available": SELENIUM_AVAILABLE,
"chrome_available": chrome_available,
"version": config.APP_VERSION,
"message": f"{config.APP_TITLE}이 정상 작동 중입니다." if SELENIUM_AVAILABLE else "Selenium 모듈 문제로 제한된 기능만 사용 가능합니다."
}
@app.get("/config", summary="환경 설정 확인", description="현재 적용된 환경 변수 설정을 확인합니다.")
async def get_config():
"""환경 설정 확인 엔드포인트"""
return {
"app_info": {
"title": config.APP_TITLE,
"version": config.APP_VERSION,
"description": config.APP_DESCRIPTION
},
"server_config": {
"host": config.HOST,
"port": config.PORT,
"workers": config.WORKERS,
"log_level": config.LOG_LEVEL
},
"api_defaults": {
"default_max_time": config.DEFAULT_MAX_TIME,
"default_days_limit": config.DEFAULT_DAYS_LIMIT,
"max_days_limit": config.MAX_DAYS_LIMIT,
"min_max_time": config.MIN_MAX_TIME,
"max_max_time": config.MAX_MAX_TIME
},
"scroll_settings": {
"check_interval": config.SCROLL_CHECK_INTERVAL,
"no_change_limit": config.SCROLL_NO_CHANGE_LIMIT,
"wait_time_short": config.SCROLL_WAIT_TIME_SHORT,
"wait_time_long": config.SCROLL_WAIT_TIME_LONG
},
"chrome_options": {
"count": len(config.CHROME_OPTIONS_LIST),
"options": config.CHROME_OPTIONS_LIST
},
"features": {
"legal_warning_enabled": config.LEGAL_WARNING_ENABLED,
"contact_email": config.CONTACT_EMAIL,
"health_check_timeout": config.HEALTH_CHECK_TIMEOUT
},
"diagnosis": {
"selenium_available": SELENIUM_AVAILABLE,
"chrome_available": chrome_available,
"import_error_count": len(SELENIUM_IMPORT_ERRORS)
},
"timestamp": datetime.now().isoformat()
}
@app.get("/legal-warning", summary="법적 경고사항", description="API 사용 시 주의해야 할 법적 사항을 안내합니다.")
async def legal_warning():
"""법적 경고사항 (환경변수 활용)"""
return {
"warning": f"⚠️ {config.APP_DESCRIPTION}",
"legal_risks": [
"카카오 이용약관 위반 → 계정 정지, 법적 조치",
"개인정보보호법(PIPA) 위반 → 과태료 최대 3억원",
"저작권 및 데이터베이스권 침해 → 손해배상",
"업무방해죄 → 5년 이하 징역 또는 1천500만원 이하 벌금"
],
"legal_alternatives": [
"카카오 공식 API 활용 (developers.kakao.com)",
"점주 대상 자체 가게 관리 서비스 개발",
"사용자 동의 기반 데이터 수집 앱",
"카카오와 정식 파트너십 체결"
],
"message": "이 API를 실제 서비스에 사용하지 마세요!",
"contact": config.CONTACT_EMAIL,
"version": config.APP_VERSION,
"timestamp": datetime.now().isoformat()
}
if __name__ == "__main__":
import uvicorn
print("🚨 " + "="*60)
print(" ⚠️ 중요 법적 경고사항 ⚠️")
print("="*64)
print(f"❌ {config.APP_DESCRIPTION}")
print("❌ 실제 웹사이트 크롤링은 불법입니다.")
print("✅ 카카오 공식 API를 사용하세요: developers.kakao.com")
print("="*64)
print()
print(f"🚀 {config.APP_TITLE} 서버 시작")
print(f"📊 진단 정보:")
print(f" - Selenium 사용 가능: {SELENIUM_AVAILABLE}")
print(f" - Chrome 설치됨: {chrome_available}")
print(f" - Import 오류: {len(SELENIUM_IMPORT_ERRORS)}개")
print()
print(f"📚 Swagger 문서: http://{config.HOST}:{config.PORT}/docs")
print(f"📖 ReDoc 문서: http://{config.HOST}:{config.PORT}/redoc")
print(f"🏠 메인 페이지: http://{config.HOST}:{config.PORT}/")
print(f"🔧 진단 정보: http://{config.HOST}:{config.PORT}/diagnostic")
print()
uvicorn.run(
app,
host=config.HOST,
port=config.PORT,
log_level=config.LOG_LEVEL
)