diff --git a/app/core/exceptions.py b/app/core/exceptions.py index abf8b26..4a4e277 100644 --- a/app/core/exceptions.py +++ b/app/core/exceptions.py @@ -291,15 +291,22 @@ def add_exception_handlers(app: FastAPI): # SocialException 핸들러 추가 from app.social.exceptions import SocialException + from app.social.exceptions import TokenExpiredError + @app.exception_handler(SocialException) def social_exception_handler(request: Request, exc: SocialException) -> Response: logger.debug(f"Handled SocialException: {exc.__class__.__name__} - {exc.message}") + content = { + "detail": exc.message, + "code": exc.code, + } + # TokenExpiredError인 경우 재연동 정보 추가 + if isinstance(exc, TokenExpiredError): + content["platform"] = exc.platform + content["reconnect_url"] = f"/social/oauth/{exc.platform}/connect" return JSONResponse( status_code=exc.status_code, - content={ - "detail": exc.message, - "code": exc.code, - }, + content=content, ) @app.exception_handler(status.HTTP_500_INTERNAL_SERVER_ERROR) diff --git a/app/social/oauth/youtube.py b/app/social/oauth/youtube.py index 9d8a53a..e2b89e2 100644 --- a/app/social/oauth/youtube.py +++ b/app/social/oauth/youtube.py @@ -59,7 +59,7 @@ class YouTubeOAuthClient(BaseOAuthClient): "response_type": "code", "scope": " ".join(YOUTUBE_SCOPES), "access_type": "offline", # refresh_token 받기 위해 필요 - "prompt": "select_account", # 계정 선택만 표시 (이전 동의 유지) + "prompt": "select_account", # 계정 선택만 표시 (동의 화면은 최초 1회만) "state": state, } url = f"{self.AUTHORIZATION_URL}?{urlencode(params)}" diff --git a/app/social/services.py b/app/social/services.py index c48d014..498eab7 100644 --- a/app/social/services.py +++ b/app/social/services.py @@ -498,34 +498,33 @@ class SocialAccountService: Raises: TokenExpiredError: 토큰 갱신 실패 시 (재연동 필요) """ - # refresh_token이 없으면 갱신 불가 → 재연동 필요 - if not account.refresh_token: - logger.warning( - f"[SOCIAL] refresh_token 없음, 재연동 필요 - account_id: {account.id}" - ) - raise TokenExpiredError(platform=account.platform) - - # 만료 시간 확인 (만료 10분 전이면 갱신, 만료 시간 없어도 갱신 시도) - should_refresh = False + # 만료 시간 확인 + is_expired = False if account.token_expires_at is None: - logger.info( - f"[SOCIAL] token_expires_at 없음, 갱신 시도 - account_id: {account.id}" - ) - should_refresh = True + is_expired = True else: - # DB datetime은 naive, now()는 aware이므로 naive로 통일하여 비교 current_time = now().replace(tzinfo=None) buffer_time = current_time + timedelta(minutes=10) if account.token_expires_at <= buffer_time: - logger.info( - f"[SOCIAL] 토큰 만료 임박, 갱신 시작 - account_id: {account.id}" - ) - should_refresh = True + is_expired = True - if should_refresh: - return await self._refresh_account_token(account, session) + # 아직 유효하면 그대로 사용 + if not is_expired: + return account.access_token - return account.access_token + # 만료됐는데 refresh_token이 없으면 재연동 필요 + if not account.refresh_token: + logger.warning( + f"[SOCIAL] access_token 만료 + refresh_token 없음, 재연동 필요 - " + f"account_id: {account.id}" + ) + raise TokenExpiredError(platform=account.platform) + + # refresh_token으로 갱신 + logger.info( + f"[SOCIAL] 토큰 만료 임박, 갱신 시작 - account_id: {account.id}" + ) + return await self._refresh_account_token(account, session) async def _refresh_account_token( self,