[1st Project] IKEA - 로그인/ 회원가입

김광일·2022년 3월 13일
0

PROJECT

목록 보기
2/5
post-thumbnail

초기 세팅과 ERD 작성 이후, 내가 가장 먼저 맡은 일은 그 ERD를 토대로 모델 작성 그리고 로그인 및 회원가입이었다.
모델 작성을 살펴보면, usersproducts 2개로 앱을 나눠서 작성을 하였다.

1. Models.py

# users > models.py
from django.db import models

class User(models.Model):
    full_name    = models.CharField(max_length=40)
    email        = models.EmailField(max_length=50, unique=True)
    password     = models.CharField(max_length=100)
    membership   = models.BooleanField(default=False)
    address      = models.CharField(max_length=255)
    phone_number = models.CharField(max_length=20)
    gender       = models.ForeignKey('Gender', on_delete=models.CASCADE)
    created_at   = models.DateTimeField(auto_now_add=True)
    updated_at   = models.DateTimeField(auto_now=True)

    class Meta:
        db_table = 'users'


class Gender(models.Model):
    gender = models.CharField(max_length=10)

    class Meta:
        db_table = 'genders'


class OrderStatus(models.Model):
    status = models.CharField(max_length=30)

    class Meta:
        db_table = 'order_status'


class OrderProduct(models.Model):
    quantity            = models.IntegerField()
    order_status        = models.ForeignKey('OrderStatus', on_delete=models.CASCADE)
    user                = models.ForeignKey('User', on_delete=models.CASCADE)
    product_information = models.ForeignKey('products.ProductInformation', on_delete=models.CASCADE)
    created_at          = models.DateTimeField(auto_now_add=True)
    updated_at          = models.DateTimeField(auto_now=True)

    class Meta:
        db_table = 'order_products'


class Cart(models.Model):
    quantity            = models.IntegerField()
    user                = models.ForeignKey('User', on_delete=models.CASCADE)
    product_information = models.ForeignKey('products.ProductInformation', on_delete=models.CASCADE)
    created_at          = models.DateTimeField(auto_now_add=True)
    updated_at          = models.DateTimeField(auto_now=True)

    class Meta:
        db_table = 'carts'


class Review(models.Model):
    rating     = models.IntegerField()
    comment    = models.TextField()
    user       = models.ForeignKey('User', on_delete=models.PROTECT)
    product    = models.ForeignKey('products.Product', on_delete=models.CASCADE)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        db_table = 'reviews'


# products > models.py
from django.db import models

class Product(models.Model):
    name          = models.CharField(max_length=50)
    price         = models.DecimalField(max_digits=12, decimal_places=2)
    description   = models.TextField()
    sub_category  = models.ForeignKey('SubCategory', on_delete=models.CASCADE)
    discount      = models.ForeignKey('Discount', on_delete=models.CASCADE,default='')
    created_at    = models.DateTimeField(auto_now_add=True)

    class Meta:
        db_table = 'products'

class Discount(models.Model):
    rate=models.IntegerField(default=0)
    type=models.CharField(max_length=200)
    class Meta:
        db_table = 'discounts'

class Image(models.Model):
    image_url = models.CharField(max_length=200)
    product   = models.ForeignKey('Product', on_delete=models.CASCADE)

    class Meta:
        db_table = 'images'


class SubCategory(models.Model):
    name          = models.CharField(max_length=50)
    main_category = models.ForeignKey('MainCategory', on_delete=models.CASCADE)
    description = models.TextField(default='')
    image_url = models.CharField(max_length=200,null=True)
    
    class Meta:
        db_table = 'sub_categories'


class MainCategory(models.Model):
    name = models.CharField(max_length=50)
    
    class Meta:
        db_table = 'main_categories'


class ProductInformation(models.Model):
    product         = models.ForeignKey('Product', on_delete=models.CASCADE)
    store           = models.ForeignKey('Store', on_delete=models.CASCADE)
    color           = models.ForeignKey('Color', on_delete=models.CASCADE)
    size            = models.ForeignKey('Size', on_delete=models.CASCADE)
    remaining_stock = models.IntegerField()
    
    class Meta:
        db_table = 'product_informations'


class Store(models.Model):
    name = models.CharField(max_length=50)
    
    class Meta:
        db_table = 'stores'


class Color(models.Model):
    name = models.CharField(max_length=50)
    
    class Meta:
        db_table = 'colors'


class Size(models.Model):
    size = models.CharField(max_length=50)
    
    class Meta:
        db_table = 'sizes'

user의 내용 중, 그냥 유저 안에 하나로 합칠 수도 있지만 gender를 따로 빼서 정규화를 진행한 이유로는, 만약 1천명의 유저 정보가 담겨있는 상황에 남자, 여자의 이름을 male, female 로 변경해야하는 상황이 생긴다면 어떻게 할 것인가? 지금 당장은 테이블이 많아져 귀찮을 수 있지만, 이러한 상황에는 gender 테이블만 변경을 하면 되기 때문에 복잡한 작업이 아니게 된다.
이러한 이유로 이런 부분들은 가능하다면 정규화를 시켜줄 필요성이 다분해 보인다.


2. 회원가입

# users > views.py
class SignUpView(View):
    def post(self, request):
        try:
            data = json.loads(request.body)

            validation_result = is_valid(data)
            
            if not validation_result:
                return JsonResponse({"message" : "INVALID_INPUT_INFORMATION"}, status = 400)
            
            full_name         = data["full_name"]
            email             = data["email"]
            membership        = data["membership"]
            address           = data["address"]
            phone_number      = data["phone_number"]
            gender_id         = data["gender_id"]
            password          = data["password"]
            hashed_password   = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')

            User.objects.create(
                full_name    = full_name,
                email        = email,
                password     = hashed_password,
                membership   = membership,
                address      = address,
                phone_number = phone_number,
                gender_id    = gender_id
            )
            
            return HttpResponse(status = 201)

        except KeyError:
            return JsonResponse({"message" : "KEYERROR"}, status = 400)
            
# users > utils.py
def is_valid(data):
    email    = data["email"]
    password = data["password"]

    validation_email    = re.match(r"^(\w+[+-_.]?\w?)+@([\w+-_.]+[.][\w+-_.]+)$", email)
    validation_password = re.match(r"^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%^&?*])([a-zA-Z0-9!@#$%^&?*]){8,}$", password)

    if User.objects.filter(email = email).exists():
        return False

    if not validation_email:
        return False

    if not validation_password:
        return False

    return True

회원가입은 특별해 보일 것은 없지만, 여기서 기억하고 넘어갈 부분은 두가지인 것 같다.
첫째로, 위에서 gender를 정규화 시켰던 이유와 비슷한 맥락인 것 같다.
바로 is_valid 함수이다. 만약 내가 유효성 검사를 해야하는 부분이 세분하게 나뉘어 10가지가 넘어간다고 봤을 때, views.py 안에서 다 표현하기에는 가독성이 많이 떨어질 것이다. 때문에, 함수를 따로 만들어 그 안에서 유효성 검사를 진행하는 것이다.
  그리고 이 유효성 검사를 통과하지 못했을 경우 굳이 모든 데이터를 변수에 담고, 비밀번호를 암호화 시킨 후 실패했다는 메세지를 리턴하기 보다는, 바로 에러를 처리해주는 것이 효율적이다. 그래서 맨 위에서 data를 받자마자 함수를 선언하여 유효성 검사를 진행하였다.
둘째로, HttpResponse 사용이다.
프로젝트에 앞서 진행했던 위스타그램에서 리턴값은 모두 JsonResponse로 메세지와 함께 반환을 했었는데, 이번엔 HttpResponse 를 사용하여 201상태코드만 반환을 하였다. 이는 201 이라는 http status 안에는 무언가 정보가 생성되었다는 뜻을 내포하고 있기에 사용 가능한 부분이다.
만약 bad request 라는 포괄적인 에러를 함축하고 있는 400 에러의 경우엔 어떤 문제가 있는지 JsonReponse 를 사용해 에러와 함꼐 반환해주는 것이 옳은 방법처럼 보인다.


3. 로그인

class LogInView(View):
    def post(self, request):
        try:
            data = json.loads(request.body)
            
            email        = data["email"]
            password     = data["password"]
            user         = User.objects.get(email = email)
            payload      = {'id' : user.id, 'exp' : datetime.now() + timedelta(hours=1)}  #1
            access_token = jwt.encode(payload, SECRET_KEY, ALGORITHM)
            
            if not bcrypt.checkpw(password.encode('utf-8'), user.password.encode('utf-8')):
                return JsonResponse({"message" : "INVALID_PASSWORD"}, status = 400)
                            
            return JsonResponse({"access_token" : access_token}, status = 200)

        except KeyError:
            return JsonResponse({"message" : "KEYERROR"}, status = 400)

        except User.DoesNotExist:  #2
            return JsonResponse({"message" : "INVALID_USER"}, status = 400)

로그인에서도 기억하고 넘어가야 할 사실을 적어본다.
먼저 #1 부분에서 보이고 싶은 것은, 토큰의 만료기한 설정이다.
jwt의 만료기한 설정에 대해서 이리저리 찾아보다면 access tokenrefresh token 에 관한 내용을 심심찮게 발견할 수 있는데, 이는 그의 기초가 되는 만료기한 설정 부분이라고 생각을 했다.
필자의 경우 hours=1 이 부분에서 s를 빼먹어서 에러가 계속 발생하였다. 혹 이 글을 보는 다른 사람이 있다면 그런 실수는 범하지 않기를 바란다.
또 한가지 더 보자면 {'id': users.id.. 이 부분의 키값 id는 후에 로그인데코레이터에서 사용될 키값이기 때문에 헷갈리면 안 된다.

다음으로 #2에 대해서 보면, 크게 전할 바가 있다기 보다는 앞서 포스팅했던
Model.DoesNotExist에 관한 부분이라 다시 상기할 겸 적어보았다.
user 를 이메일을 사용하여 get 할 때, 그 정보가 맞지 않은 경우 반환하는 에러이다.

profile
부족함 없이 공부하자

0개의 댓글