iPark Project
정규표현식
- 이번 프로젝트에서는 회원가입, 아이디 찾기, 비밀번호 변경에서
정규표현식
을 적용했다.
- 회원가입에서 정규표현식을 custom validator를 만들어 사용하면서 처음에 사용자가 입력한 값이 틀렸다면 에러 메세지를 통해 올바른 양식을 입력하도록 유도할 수 있었다.
회원가입
import re
from rest_framework import serializers
from user.models import User as UserModel
EMAIL = ("@naver.com", "@gmail.com", "@kakao.com", "@daum.net", "@nate.com", "@outlook.com")
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = UserModel
fields = ["username", "password", "fullname", "email", "phone", "birthday", "region", "join_date"]
extra_kwargs = {
"password": {"write_only": True},
}
def validate(self, data):
print(data)
correct_email = re.compile("^[a-zA-Z0-9+-_.]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$")
correct_phone = re.compile("(010)-\d{4}-\d{4}")
correct_password = re.compile("^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$")
correct_birthday = re.compile("\d{4}-\d{2}-\d{2}$")
email_input = correct_email.match(data.get("email", ""))
phone_input = correct_phone.match(data.get("phone", ""))
password_input = correct_password.match(data.get("password", ""))
birthday_input = correct_birthday.match(str(data.get("birthday", "")))
if data.get("username"):
if not len(data.get("username", "")) >= 6:
raise serializers.ValidationError(
detail={"error": "username의 길이는 6자리 이상이어야 합니다."})
if email_input == None:
raise serializers.ValidationError(
detail={"error": "이메일 형식이 올바르지 않습니다."})
else:
if not data.get("email", "").endswith(EMAIL):
raise serializers.ValidationError(
detail={"error": "네이버, 구글, 카카오, 다음, 네이트, 아웃룩 이메일만 가입할 수 있습니다."})
if password_input == None:
raise serializers.ValidationError(
detail={"error": "비밀번호는 8 자리 이상이며 최소 하나 이상의 영문자, 숫자, 특수문자가 필요합니다."})
if phone_input == None:
raise serializers.ValidationError(
detail={"error": "전화번호는 010-0000-0000 형식으로 작성해주시기 바랍니다."})
if birthday_input == None:
raise serializers.ValidationError(
detail={"error": "생년월일은 YYYY-MM-DD 형식으로 작성해주시기 바랍니다."})
return data
- 정규표현식은 만약 내가 정해놓은
correct_email
등과 다른 결과가 들어오면 None
값을 반환한다.(문자열 x)
- 이메일의 경우
endswith
를 사용해 내가 정해놓은 도메인을 가진 이메일이 아니라면 회원가입을 하지 못하도록 하였다. 물론 다양성 측면에서 한계가 있을 수 있지만, 현재 수준으로 이메일을 다루기에 적합다하 판단하여 위와 같이 상수 EMAIL
을 작성해 적용하였다.
- 한 가지 주의할 점은
birthday
다. birthday
는 모델을 만들 때 DateField
로 지정해 주었다. 그렇기 때문에 해당 값은 문자열로 들어오지 않는다.
- 따라서 문자열로 바꿔주지 않으면
TypeError: expected string or bytes-like object
라는 에러 메세지를 만나게 될 것이다.
- 당황하지 않고
str()
로 감싸주면 문제가 해결된다.
아이디 찾기
class FindUserInfoView(APIView):
def post(self, request):
correct_email = re.compile("^[a-zA-Z0-9+-_.]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$")
correct_phone = re.compile("\d{3}-\d{4}-\d{4}")
email_input = correct_email.match(request.data["email"])
phone_input = correct_phone.match(request.data["phone"])
if email_input == None or phone_input == None:
return Response({"message": "이메일 혹은 핸드폰 번호 양식이 올바르지 않습니다."}, status=status.HTTP_400_BAD_REQUEST)
else:
searched_username = UserModel.objects.get(Q(email=request.data["email"]) & Q(phone=request.data["phone"])).username
if searched_username:
return Response({"username" : searched_username}, status=status.HTTP_200_OK)
- 사용자가 본인의 아이디를 잊어버렸을 때 확인하는 데이터로 나는
email
과 phone
을 사용했다.
- 그 이유는 둘 다
unique
값이기 때문에 각 회원마다의 고유한 정보이기 때문이다.
- 이 정보들은 사용자가 다시 입력해야 하기 때문에 정규표현식을 한 번 더 사용해 주어 사용자가 잘못된 양식을 입력하면 다음 페이지로 넘어가지 못하도록 막았다.
- 사용자가 입력한 값들이 전부 맞을 시 쿼리를 날려야 하기 때문에
Q
를 사용해 주었다. 해당 값들과 일치하는 데이터를 search_username
에 담아주어 보내주는 방식으로 작성했다.
비밀번호 변경
class AlterPasswordView(APIView):
def post(self, request):
"""
1. 비밀번호를 변경할 사용자의 username, email을 입력받는다.
2. 해당 값을 통해 비밀번호를 변경할 user를 찾아준다.
3. 만약 user가 존재한다면 user 정보를 비밀번호 수정 페이지에서도 알 수 있도록 넘겨준다.
4. user가 존재하지 않는다면 "존재하지 않는 사용자입니다." 라는 메세지를 반환한다.
"""
correct_email = re.compile("^[a-zA-Z0-9+-_.]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$")
email_input = correct_email.match(request.data["email"])
if email_input == None:
return Response({"message": "이메일 형식에 맞게 작성해주세요."})
else:
user = UserModel.objects.get(Q(username=request.data["username"]) & Q(email=request.data["email"]))
if user:
return Response({"message": "비밀번호 변경 페이지로 이동합니다."}, status=status.HTTP_200_OK)
return Response({"message": "존재하지 않는 사용자입니다."}, status=status.HTTP_400_BAD_REQUEST)
def put(self, request):
"""
1. 사용자의 정보를 그대로 가져온다.
2. 새롭게 세팅할 비밀번호와 중복 확인용 비밀번호를 받는다.
3. 이 두 비밀번호가 정규표현식을 통과하고 일치한다면, UserSerializer에 request.data를 보내 custom updator를 통해 비밀번호를 update해준다.
"""
correct_password = re.compile("^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$")
first_password_input = correct_password.match(request.data["new_password"])
second_password_input = correct_password.match(request.data["rewrite_password"])
if first_password_input == None or second_password_input == None:
return Response({"message": "비밀번호를 제대로 입력해주세요."}, status=status.HTTP_400_BAD_REQUEST)
else:
if request.data["new_password"] == request.data["rewrite_password"]:
user = UserModel.objects.get(Q(username=request.data["username"]) & Q(email=request.data["email"]))
request.data["password"] = request.data["new_password"]
serializer = UserSerializer(user, data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
return Response({"message": "비밀번호 변경이 완료되었습니다! 다시 로그인해주세요."}, status=status.HTTP_200_OK)
return Response({"message": "두 비밀번호가 일치하지 않습니다."})
- 원래는 이메일 인증을 해보려 했는데, 구글 이메일 아이디와 비밀번호가 맞는데도 에러가 나 일단 다른 기능을 구현하기 위해 해당 방법을 선택했다. 추후 이메일 인증을 재도전할 예정이다.
- 내가 생각한 로직으로는
- 사용자가 비밀번호를 변경할 수 있는 권한이 있는지 알아내기 위해 사용자의 아이디와
이메일
을 입력하도록 했다.
- 유명한 사이트들은 이메일 또는 핸드폰 번호를 받는데, 나의 모델에서는 핸드폰 번호는 고유값이 아니기 때문에 이메일만 받자는 선택을 했다.
- 이메일을 받고 해당 이메일을 가진 사용자가 있다면 프론트에 있는
비밀번호 변경
(아직 자세한 네이밍은 프론트를 짜지 않아 정하지 않음) 버튼을 누르게 되면 비밀번호 변경 페이지로 보내주려고 한다.
- 해당 페이지로 넘어가면서 나는 사용자의 아이디와 이메일 데이터를 가져가 이곳에서도 user를 찾고, 수정이므로 해당 user 값을 instance로 넘겨줄 생각이었다.
- 현재 request.data에 들어있는 값들이 custom validator를 통과하기에 부족한 것 같고, 실제로 is_valid()를 넘어가지 못하고 있다.
해당 부분에 대한 수정이 필요하며, 현재 진행중이다.
테스트 코드 작성
- user app에서는 비밀번호 변경을 제외하고는 모두 작성해 보았다.
- 내용이 틀린 부분이 많겠지만, 테스트 결과는 모두 통과이다. 5개 정도에 0.369s면 짧은 시간은 아니지만 1초를 안넘겨서 다행이라 생각한다.
user/tests.py
from django.urls import reverse
from rest_framework.test import APITestCase
from rest_framework import status
from user.models import User as UserModel
from user.models import Region as RegionModel
class UserRegistrationTest(APITestCase):
@classmethod
def setUpTestData(cls):
cls.region = RegionModel.objects.bulk_create([RegionModel(region_name="강남구"),
RegionModel(region_name="강동구"),
RegionModel(region_name="강북구")])
def test_registration(self):
url = reverse("user_view")
user_data = {
"username" : "user10",
"password" : "1010abc!",
"fullname" : "user10",
"email" : "user10@gmail.com",
"phone" : "010-1010-1010",
"birthday" : "2022-07-13",
"region" : 2
}
response = self.client.post(url, user_data)
self.assertEqual(response.status_code, 200)
class UserLoginTest(APITestCase):
@classmethod
def setUpTestData(cls):
cls.user_data = {"username" : "user10", "password" : "1010abc!"}
cls.user = UserModel.objects.create_user("user10", "1010abc!")
def test_login(self):
url = reverse("token_obtain_pair")
user_data = {
"username" : "user10",
"password" : "1010abc!"
}
response = self.client.post(url, user_data)
self.assertEqual(response.status_code, 200)
class UserInfoModifyDeleteTest(APITestCase):
@classmethod
def setUpTestData(cls):
cls.region = RegionModel.objects.bulk_create([RegionModel(region_name="강남구"),
RegionModel(region_name="강동구"),
RegionModel(region_name="강북구")])
cls.user = UserModel.objects.create_user("user10", "1010abc!")
cls.login_data = {"username": "user10", "password" : "1010abc!"}
def setUp(self):
self.access_token = self.client.post(reverse("token_obtain_pair"), self.login_data).data["access"]
def test_modify_user_info(self):
url = reverse("user_view")
data_for_change = {
"password" : "2020abc!",
"fullname" : "user20",
"email" : "user20@gmail.com",
"phone" : "010-1010-1010",
"birthday" : "2022-07-13",
"region" : 3
}
response = self.client.put(
path=url,
data=data_for_change,
HTTP_AUTHORIZATION=f"Bearer {self.access_token}"
)
self.assertEqual(response.status_code, 200)
def test_delete_user(self):
url = reverse("user_view")
response = self.client.delete(url, HTTP_AUTHORIZATION=f"Bearer {self.access_token}")
self.assertEqual(response.data["message"], "회원탈퇴 성공")
class SearchUsernameTest(APITestCase):
@classmethod
def setUpTestData(cls):
user_data = {
"username" : "user10",
"password" : "1010abc!",
"fullname" : "user10",
"email" : "user10@gmail.com",
"phone" : "010-1010-1010",
"birthday" : "2022-07-13",
}
cls.user = UserModel.objects.create(**user_data)
def test_search_username(self):
url = reverse("myid_view")
data = {
"email" : "user10@gmail.com",
"phone" : "010-1010-1010"
}
response = self.client.post(url, data)
self.assertEqual(response.data["username"], "user10")
- 처음에 region 데이터를 만들 때 아무생각없이 그냥 create를 사용해 테스트용 db를 만들었었다. 그런데 3개나 등록했는데 pk 값으로 1 밖에 받아오지 못해 짜증나 구글링을 했더니
bulk_create
라는 좋은 친구가 있었다.
- 이것을 사용해 여러 개의 db를 생성할 수 있었다.
setUpTestData()
를 사용해 TestCase 자체가 끝날 때까지 데이터가 유지되도록 해보았다. 해당 코드의 대다수는 사실 TestCase 안에 method가 하나 정도 밖에 없어서 setUp()
을 사용해도 될 것같긴 하다.
- 이 부분에 대한 효율성은 알아보는 것이 좋을 것 같다고 생각한다.
- status code를 확인하는 것 이외에도 데이터 비교, 메세지 비교 등을 사용해 결과 확인의 폭을 넓히려 노력했다.
- 나는 Response를 할 때 메세지도 쓰긴 하지만 데이터를 더 많이 넘겨주는 경향이 있어 대다수 status code를 사용했다. 이 부분도 수정하려 노력할 것이다.