diff --git a/app/user/api/routers/v1/auth.py b/app/user/api/routers/v1/auth.py index ddccf3d..43f8268 100644 --- a/app/user/api/routers/v1/auth.py +++ b/app/user/api/routers/v1/auth.py @@ -4,6 +4,7 @@ 카카오 로그인, 토큰 갱신, 로그아웃, 내 정보 조회 엔드포인트를 제공합니다. """ +import logging from typing import Optional from fastapi import APIRouter, Depends, Header, Request, status @@ -12,6 +13,8 @@ from sqlalchemy.ext.asyncio import AsyncSession from config import prj_settings from app.database.session import get_session + +logger = logging.getLogger(__name__) from app.user.dependencies import get_current_user from app.user.models import User from app.user.schemas.user_schema import ( @@ -40,7 +43,9 @@ async def kakao_login() -> KakaoLoginResponse: 프론트엔드에서 이 URL로 사용자를 리다이렉트하면 카카오 로그인 페이지가 표시됩니다. """ + logger.info("[ROUTER] 카카오 로그인 URL 요청") auth_url = kakao_client.get_authorization_url() + logger.debug(f"[ROUTER] 카카오 인증 URL 생성 완료 - auth_url: {auth_url}") return KakaoLoginResponse(auth_url=auth_url) @@ -63,6 +68,8 @@ async def kakao_callback( 신규 사용자인 경우 자동으로 회원가입이 처리됩니다. """ + logger.info(f"[ROUTER] 카카오 콜백 수신 - code: {code[:20]}...") + # 클라이언트 IP 추출 ip_address = request.client.host if request.client else None @@ -71,6 +78,8 @@ async def kakao_callback( if forwarded_for: ip_address = forwarded_for.split(",")[0].strip() + logger.debug(f"[ROUTER] 클라이언트 정보 - ip: {ip_address}, user_agent: {user_agent}") + result = await auth_service.kakao_login( code=code, session=session, @@ -84,6 +93,7 @@ async def kakao_callback( f"?access_token={result.access_token}" f"&refresh_token={result.refresh_token}" ) + logger.info(f"[ROUTER] 카카오 콜백 완료, 프론트엔드로 리다이렉트 - redirect_url: {redirect_url[:50]}...") return RedirectResponse(url=redirect_url, status_code=302) @@ -119,6 +129,8 @@ async def kakao_verify( 신규 사용자인 경우 자동으로 회원가입이 처리됩니다. """ + logger.info(f"[ROUTER] 카카오 인가 코드 검증 요청 - code: {body.code[:20]}...") + # 클라이언트 IP 추출 ip_address = request.client.host if request.client else None @@ -127,13 +139,18 @@ async def kakao_verify( if forwarded_for: ip_address = forwarded_for.split(",")[0].strip() - return await auth_service.kakao_login( + logger.debug(f"[ROUTER] 클라이언트 정보 - ip: {ip_address}, user_agent: {user_agent}") + + result = await auth_service.kakao_login( code=body.code, session=session, user_agent=user_agent, ip_address=ip_address, ) + logger.info(f"[ROUTER] 카카오 인가 코드 검증 완료 - user_id: {result.user.id}, is_new_user: {result.user.is_new_user}") + return result + @router.post( "/refresh", diff --git a/app/user/services/auth.py b/app/user/services/auth.py index c0198ed..7f39595 100644 --- a/app/user/services/auth.py +++ b/app/user/services/auth.py @@ -71,24 +71,36 @@ class AuthService: Returns: LoginResponse: 토큰 및 사용자 정보 """ + logger.info(f"[AUTH] 카카오 로그인 시작 - code: {code[:20]}..., ip: {ip_address}") + # 1. 카카오 토큰 획득 + logger.info("[AUTH] 1단계: 카카오 토큰 획득 시작") kakao_token = await kakao_client.get_access_token(code) + logger.debug(f"[AUTH] 카카오 토큰 획득 완료 - token_type: {kakao_token.token_type}") # 2. 카카오 사용자 정보 조회 + logger.info("[AUTH] 2단계: 카카오 사용자 정보 조회 시작") kakao_user_info = await kakao_client.get_user_info(kakao_token.access_token) + logger.debug(f"[AUTH] 카카오 사용자 정보 조회 완료 - kakao_id: {kakao_user_info.id}") # 3. 사용자 조회 또는 생성 + logger.info("[AUTH] 3단계: 사용자 조회/생성 시작") user, is_new_user = await self._get_or_create_user(kakao_user_info, session) + logger.info(f"[AUTH] 사용자 처리 완료 - user_id: {user.id}, is_new_user: {is_new_user}") # 4. 비활성화 계정 체크 if not user.is_active: + logger.error(f"[AUTH] 비활성화 계정 접근 시도 - user_id: {user.id}") raise UserInactiveError() # 5. JWT 토큰 생성 + logger.info("[AUTH] 5단계: JWT 토큰 생성 시작") access_token = create_access_token(user.id) refresh_token = create_refresh_token(user.id) + logger.debug(f"[AUTH] JWT 토큰 생성 완료 - user_id: {user.id}") # 6. 리프레시 토큰 DB 저장 + logger.info("[AUTH] 6단계: 리프레시 토큰 저장 시작") await self._save_refresh_token( user_id=user.id, token=refresh_token, @@ -96,11 +108,16 @@ class AuthService: user_agent=user_agent, ip_address=ip_address, ) + logger.debug(f"[AUTH] 리프레시 토큰 저장 완료 - user_id: {user.id}") # 7. 마지막 로그인 시간 업데이트 user.last_login_at = datetime.now(timezone.utc) await session.commit() + redirect_url = f"https://{prj_settings.PROJECT_DOMAIN}" + logger.info(f"[AUTH] 카카오 로그인 완료 - user_id: {user.id}, redirect_url: {redirect_url}") + logger.debug(f"[AUTH] 응답 토큰 정보 - access_token: {access_token[:30]}..., refresh_token: {refresh_token[:30]}...") + return LoginResponse( access_token=access_token, refresh_token=refresh_token, @@ -113,7 +130,7 @@ class AuthService: profile_image_url=user.profile_image_url, is_new_user=is_new_user, ), - redirect_url=f"{prj_settings.PROJECT_DOMAIN}", + redirect_url=redirect_url, ) async def refresh_tokens( diff --git a/app/user/services/kakao.py b/app/user/services/kakao.py index eee3a6c..7231573 100644 --- a/app/user/services/kakao.py +++ b/app/user/services/kakao.py @@ -4,10 +4,14 @@ 카카오 로그인 인증 흐름을 처리하는 클라이언트입니다. """ +import logging + import aiohttp from config import kakao_settings +logger = logging.getLogger(__name__) + from app.user.exceptions import KakaoAPIError, KakaoAuthFailedError from app.user.schemas.user_schema import KakaoTokenResponse, KakaoUserInfo @@ -39,12 +43,15 @@ class KakaoOAuthClient: Returns: 카카오 OAuth 인증 페이지 URL """ - return ( + auth_url = ( f"{self.AUTH_URL}" f"?client_id={self.client_id}" f"&redirect_uri={self.redirect_uri}" f"&response_type=code" ) + logger.info(f"[KAKAO] 인증 URL 생성 - redirect_uri: {self.redirect_uri}") + logger.debug(f"[KAKAO] 인증 URL 상세 - auth_url: {auth_url}") + return auth_url async def get_access_token(self, code: str) -> KakaoTokenResponse: """ @@ -60,6 +67,7 @@ class KakaoOAuthClient: KakaoAuthFailedError: 토큰 발급 실패 시 KakaoAPIError: API 호출 오류 시 """ + logger.info(f"[KAKAO] 액세스 토큰 요청 시작 - code: {code[:20]}...") try: async with aiohttp.ClientSession() as session: data = { @@ -72,20 +80,27 @@ class KakaoOAuthClient: if self.client_secret: data["client_secret"] = self.client_secret + logger.debug(f"[KAKAO] 토큰 요청 데이터 - redirect_uri: {self.redirect_uri}, client_id: {self.client_id[:10]}...") + async with session.post(self.TOKEN_URL, data=data) as response: result = await response.json() + logger.debug(f"[KAKAO] 토큰 응답 상태 - status: {response.status}") if "error" in result: error_desc = result.get( "error_description", result.get("error", "알 수 없는 오류") ) + logger.error(f"[KAKAO] 토큰 발급 실패 - error: {result.get('error')}, description: {error_desc}") raise KakaoAuthFailedError(f"카카오 토큰 발급 실패: {error_desc}") + logger.info("[KAKAO] 액세스 토큰 발급 성공") + logger.debug(f"[KAKAO] 토큰 정보 - token_type: {result.get('token_type')}, expires_in: {result.get('expires_in')}") return KakaoTokenResponse(**result) except KakaoAuthFailedError: raise except Exception as e: + logger.error(f"[KAKAO] API 호출 오류 - error: {str(e)}") raise KakaoAPIError(f"카카오 API 호출 중 오류 발생: {str(e)}") async def get_user_info(self, access_token: str) -> KakaoUserInfo: @@ -102,21 +117,31 @@ class KakaoOAuthClient: KakaoAuthFailedError: 사용자 정보 조회 실패 시 KakaoAPIError: API 호출 오류 시 """ + logger.info("[KAKAO] 사용자 정보 조회 시작") try: async with aiohttp.ClientSession() as session: headers = {"Authorization": f"Bearer {access_token}"} async with session.get(self.USER_INFO_URL, headers=headers) as response: result = await response.json() + logger.debug(f"[KAKAO] 사용자 정보 응답 상태 - status: {response.status}") if "id" not in result: + logger.error(f"[KAKAO] 사용자 정보 조회 실패 - response: {result}") raise KakaoAuthFailedError("카카오 사용자 정보를 가져올 수 없습니다.") + kakao_id = result.get("id") + kakao_account = result.get("kakao_account", {}) + profile = kakao_account.get("profile", {}) + + logger.info(f"[KAKAO] 사용자 정보 조회 성공 - kakao_id: {kakao_id}") + logger.debug(f"[KAKAO] 사용자 상세 정보 - nickname: {profile.get('nickname')}, email: {kakao_account.get('email')}") return KakaoUserInfo(**result) except KakaoAuthFailedError: raise except Exception as e: + logger.error(f"[KAKAO] API 호출 오류 - error: {str(e)}") raise KakaoAPIError(f"카카오 API 호출 중 오류 발생: {str(e)}")