
poetry env info --path
# 현재 연결된 가상환경의 경로를 확인합니다.
rm -rf $(poetry env info --path)
# 확인된 가상환경 폴더를 통째로 삭제하여 깨끗한 상태로 만듭니다.
poetry install --no-root
# 가상환경을 새로 생성하고 pyproject.toml에 적힌 모든 라이브러리를 다시 설치합니다.
apps/User/serializers/signup_serializer.py:1: error: Skipping analyzing "rest_framework": module is installed, but missing library stubs or py.typed marker [import-untyped]
apps/User/serializers/signup_serializer.py:1: note: See https://mypy.readthedocs.io/en/stable/running_mypy.html#missing-imports
apps/User/views/signup_view.py:2: error: Skipping analyzing "rest_framework.views": module is installed, but missing library stubs or py.typed marker [import-untyped]
apps/User/views/signup_view.py:3: error: Skipping analyzing "rest_framework.response": module is installed, but missing library stubs or py.typed marker [import-untyped]
apps/User/views/signup_view.py:4: error: Skipping analyzing "rest_framework": module is installed, but missing library stubs or py.typed marker [import-untyped]
Found 4 errors in 2 files (checked 55 source files)
[tool.mypy]
# mypy가 사용할 외부 플러그인 목록에 장고 플러그인을 지정하여 활성화함
plugins = ["mypy_django_plugin.main"]
——————————————————————————————————————[비교]—————————————————————————————————————————
[tool.mypy]
# mypy가 사용할 외부 플러그인 목록에 장고 플러그인을 지정하여 활성화함
plugins = [
"mypy_django_plugin.main", # Django 전용 분석 플러그인
"mypy_drf_plugin.main" # Django REST Framework 전용 분석 플러그인
]
class UserLoginSerializer(serializers.Serializer):
# 아이디 입력을 검증하기 위한 문자열 필드
username = serializers.CharField()
# 비밀번호를 검증하되 응답 데이터에는 노출되지 않도록 쓰기 전용으로 선언
password = serializers.CharField(write_only=True)
class LoginService:
"""사용자의 아이디와 비밀번호를 받아 검증하는 메서드"""
def authenticate_user(validated_data):
# 장고 내부의 인증 시스템을 호출하여 사용자 존재 여부와 비밀번호 일치를 확인
user = authenticate(username=validated_data['username'], password=validated_data['password'])
# 인증된 사용자 객체가 정상적으로 반환되었는지 검사하는 조건문
if user:
# JSON Web Token(리프레시 토큰 및 액세스 토큰)을 새로 생성
refresh = RefreshToken.for_user(user)
# 생성된 토큰 정보와 사용자 객체를 딕셔너리로 묶어 반환 준비
return {
# 액세스 토큰을 추출하고 문자열로 형변환하여 딕셔너리에 담기
'access': str(refresh.access_token),
# 리프레시 토큰 자체도 문자열로 형변환하여 딕셔너리에 함께 담기
'refresh': str(refresh),
# 후속 처리에서 활용할 수 있도록 인증된 사용자 객체도 담음
'user': user
}
return None
class UserLoginView(APIView):
"""클라이언트가 POST 방식으로 데이터를 전송할 때 실행되는 메서드"""
def post(self, request):
# 1. 입력 데이터 검증
serializer = UserLoginSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
# 2. 서비스 레이어 호출
result = LoginService.authenticate_user(serializer.validated_data)
# 3. 서비스 레이어에서 성공적인 결과(토큰 데이터)가 돌아왔는지 확인
if result:
# 4. 성공했을 경우 클라이언트에게 보낼 응답 데이터를 구성하여 반환
return Response(
{
# 로그인이 성공했다는 안내 문구
"message": "로그인에 성공했습니다.",
# 발급받은 액세스 토큰을 응답 본문에 포함
"access": result['access'],
# 발급받은 리프레시 토큰을 응답 본문에 포함
"refresh": result['refresh']
},
status=status.HTTP_200_OK,
)
# 5. 인증 결과가 실패인 경우 실행되는 부분
return Response(
{"message": "아이디 또는 비밀번호가 잘못되었습니다."},
status=status.HTTP_401_UNAUTHORIZED,
)
THIRD_PARTY_APPS: list[str] = [
"rest_framework",
"drf_spectacular",
]
——————————————————————————————————————[비교]—————————————————————————————————————————
THIRD_PARTY_APPS: list[str] = [
"rest_framework",
"drf_spectacular",
"rest_framework_simplejwt",
]
REST_FRAMEWORK = {
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
}
——————————————————————————————————————[비교]—————————————————————————————————————————
REST_FRAMEWORK = {
# 기존에 작성하신 스펙타큘러 자동화 스키마 설정입니다.
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
# API 요청이 들어올 때 누구인지 확인하는 기본 인증 방식들을 지정합니다.
"DEFAULT_AUTHENTICATION_CLASSES": (
# JWT 토큰을 해독하여 올바른 사용자인지 검증하는 라이브러리 클래스를 등록합니다.
"rest_framework_simplejwt.authentication.JWTAuthentication",
),
}


from rest_framework import serializers
class UserLogoutSerializer(serializers.Serializer):
refresh = serializers.CharField(
help_text="로그인 시 발급받은 refresh 토큰을 입력하세요."
)
class UserLogoutView(APIView):
"""사용자의 로그아웃 요청을 처리하는 뷰 클래스를 선언"""
permission_classes = [IsAuthenticated]
@extend_schema(
tags=["회원관리"],
summary="로그아웃",
request=UserLogoutSerializer,
)
def post(self, request):
# 1. 입력데이터 검증
serializer = UserLogoutSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
try:
# 2. 클라이언트가 보낸 요청 데이터 중 리프레시 토큰 문자열을 찾아냄
refresh_token = request.data["refresh"]
# 3. 추출한 문자열을 코드로 조작 가능한 토큰 객체로 변환
token = RefreshToken(refresh_token)
# 4. 해당 토큰을 서버의 블랙리스트에 등록하여 더 이상 인증 수단으로 쓰지 못하게 만듬
token.blacklist()
# 5. 처리가 무사히 완료되면 성공 메시지와 함께 응답을 생성하여 반환
return Response({"message": "성공적으로 로그아웃 되었습니다."}, status=status.HTTP_205_RESET_CONTENT)
# 6. 오류 발생 시
except Exception as e:
print(f"로그아웃 토큰 처리 에러: {e}")
return Response({"message": "유효하지 않은 토큰입니다."}, status=status.HTTP_400_BAD_REQUEST)
THIRD_PARTY_APPS: list[str] = [
"rest_framework",
"drf_spectacular",
"rest_framework_simplejwt",
]
——————————————————————————————————————[비교]—————————————————————————————————————————
THIRD_PARTY_APPS: list[str] = [
"rest_framework",
"drf_spectacular",
"rest_framework_simplejwt",
"rest_framework_simplejwt.token_blacklist",
]

class UserWithdrawalView(APIView):
"""회원 탈퇴 요청을 처리하기 위한 뷰 클래스"""
permission_classes = [IsAuthenticated]
@extend_schema(
tags=["회원관리"],
summary="회원탈퇴"
)
def delete(self, request):
# 1. 액세스 토큰을 통해 신원이 확인된 사용자 객체를 요청 데이터에서 꺼내옴
user = request.user
# 2. 사용자 객체의 활성화 상태 속성을 거짓(False)으로 바꾸어 논리적 삭제 처리
user.is_active = False
# 3. 변경된 활성 상태 속성을 데이터베이스에 최종적으로 기록하고 저장
user.save()
return Response({"message": "회원 탈퇴가 완료되었습니다."}, status=status.HTTP_204_NO_CONTENT)
class UserWithdrawalView(APIView):
"""회원 탈퇴 요청을 처리하기 위한 뷰 클래스"""
permission_classes = [IsAuthenticated]
@extend_schema(
tags=["회원관리"],
summary="회원탈퇴"
)
def delete(self, request):
# 1. 액세스 토큰을 통해 신원이 확인된 사용자 객체를 요청 데이터에서 꺼내옴
user = request.user
# 2. 사용자 객체의 활성화 상태 속성을 거짓(False)으로 바꾸어 논리적 삭제 처리
user.is_active = False
# 3. 탈퇴 처리가 진행되는 현재 시간을 숫자로 이루어진 초 단위 값(타임스탬프)으로 변환
current_time = int(timezone.now().timestamp())
# 4. 삭제되었음을 알리는 문구, 현재 시간, 유저의 고유 번호를 엮어 새로운 아이디를 만듬
user.username = f"deleted_{current_time}_{user.id}"
# 5. 닉네임 역시 나중에 다른 사람이 사용할 수 있도록 임의의 문자열로 덮어써서 자리를 비워줌
user.nickname = f"deleted_{current_time}_{user.id}"
# 6. 변경된 활성 상태 속성을 데이터베이스에 최종적으로 기록하고 저장
user.save()
return Response({"message": "회원 탈퇴가 완료되었습니다."}, status=status.HTTP_204_NO_CONTENT)

docker-compose exec backend python manage.py makemigrations
docker-compose exec backend python manage.py migrate
class User(AbstractUser):
nickname = models.CharField(max_length=50, unique=True)
push_enabled = models.BooleanField(default=True)
——————————————————————————————————————[비교]—————————————————————————————————————————
class User(AbstractUser):
nickname = models.CharField(max_length=50)
push_enabled = models.BooleanField(default=True)
