Project 1. Marketkurly clone | 회원가입 엔드포인트 구현

Hyeonju L.·2020년 12월 25일
1

Project

목록 보기
6/8
post-thumbnail

마켓컬리 웹사이트 클론에서 user와 관련된 부분을 맡게되었다.
위스타그램에서 회원가입 및 로그인 엔드포인트를 구현해보기는 했지만 마켓컬리는 그보다 더 많은 정보를 입력해야하고 주소입력, 이용약관 동의 등이 있어 조금 더 복잡하다.

실제 구현 화면

1. 데이터모델링

사용자 정보를 입력받고 관리하기 위해 Users 테이블을 만들고, Users 테이블을 기준으로 성별, 회원등급, 이용약관동의, 주소 테이블을 one to many 관계로 엮었다.
이용약관 동의(Accept Terms and Conditions) 테이블에는 필수사항을 제외하고 선택사항(마케팅 정보동의 등)만을 넣었다. 주소(Adderess) 테이블에는 주소지와 boolean 값을 넣을 열(is_active)을 만들어 해당 주소지가 기본 배송지인지 아닌지를 선택할 수 있게끔 만들었다.

2. 회원가입 API

1) 정규표현식

마켓컬리 회원가입 시 등록하는 아이디와 비밀번호 조건을 위해 정규표현식을 이용했다. 정규표현식을 요구하는 사항이 많아 정규식을 찾고 이해하는데 며칠이 걸려서 고생했던 기억😢️

아이디

 '^[a-z]{6,}$'                   # 영문 6자 이상
 '^(?=.*[0-9])(?=.*[a-z]).{6,}$' # 영문+숫자 6자 이상

비밀번호

'^(?=.*[0-9])(?=.*[a-zA-Z]).{10,}$'             # 영문+숫자
'^(?=.*[!@#$%^&*()_+])(?=.*[a-zA-Z]).{10,}$'    # 영문+특수
'^(?=.*[0-9])(?=.*[!@#$%^&*()_+]).{10,}$'       # 숫자+특수
'^(?=.*[0-9])(?=.*[!@#$%^&*()_+])(?=.*[a-zA-Z]).{10,}$' # 영문+숫자

이메일

'^[a-zA-Z0-9+-_.]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$' # @, . 포함

휴대폰번호

'^\d{3}?[-]\d{3,4}?[-]\d{4}$'

2) 정규표현식을 이용한 코드 구현

Account (ID)

def validate_account(self, account):
        REGEX_ACCOUNT_1 = '^[a-z]{6,}$'                 
        REGEX_ACCOUNT_2 = '^(?=.*[0-9])(?=.*[a-z]).{6,}$'
	      if not re.match(REGEX_ACCOUNT_1, account) and not re.match(REGEX_ACCOUNT_2, account)
                    return False
              return True

코드 리팩토링

re.match(REGEX_ACCOUNT_1, account) 는 이 자체로 True or False로 판단이 가능하기 때문에 if, return을 중복해서 쓸 필요가 없다는 피드백을 받았다. 그래서 아래와 같이 수정하고 코드를 여섯 줄에서 네 줄로 줄일 수 있었다.

def validate_account(self, account):
        REGEX_ACCOUNT_1 = '^[a-z]{6,}$'                 
        REGEX_ACCOUNT_2 = '^(?=.*[0-9])(?=.*[a-z]).{6,}$'
	    
        return re.match(REGEX_ACCOUNT_1, account) or re.match(REGEX_ACCOUNT_2, account)

3) post method

validate_account, validate_password, validate_email, validate_phone_number 메소드 뒤에 post method를 선언했다. validation에 부합하지 않으면 에러메시지를 반환하고, 그렇지 않으면 회원가입이 진행된다.

구현사항

  • 에러처리
    - 아이디가 존재하는 경우 USER_EXIST 메시지 리턴
    - 아이디 조건에 부합하지 않는 경우 INVALID_ACCOUNT 메시지 리턴
    - 비밀번호/이메일/휴대폰번호 역시 조건에 부합하지 않는 경우 INVALID 메시지 리턴
  • 비밀번호 암호화 (bcrypt)
  • transaction() 기능
    - 회원가입 시 User와 Address를 동시에 생성해야 하는데 Address 생성 시 컴퓨터가 꺼지거나 하는 돌발상황 발생 시 User 객체만 생기고 Address 객체는 생성되지 않는 문제가 발생할 수 있다. 따라서 하나가 실패하면 다른 하나도 성공하면 안된다. 이 과정을 보장하기 위해 transaction 기능 이용. 즉, 아래 구문에선 user_model.save(), address.save() 중 하나만 실패해도 모든 commit rollback
  • 에러 메시지를 담은 예외 인스턴스 객체를 e로 받아 사용 - 이 변수를 통해서 추가적인 정보 출력 가능(e.args[0])
def post(self, request):
        data = json.loads(request.body)

        try:
            if User.objects.filter(account=data['account']).exists():
                return JsonResponse({"message":"USER_EXIST"}, status=400)

            if not self.validate_account(data['account']):
                return JsonResponse({"message":"INVALID_ACCOUNT"}, status=400)

            if not self.validate_password(data['password']):
                return JsonResponse({"message": "INVALID_PW"}, status=400)

            if not self.validate_email(data['email']):
                return JsonResponse({"message":"INVALID_EMAIL"}, status=400)

            if not self.validate_phone_number(data['phone_number']):
                return JsonResponse({"message":"INVALID_PHONE_NUMBER"}, status=400)

            hashed_pw = bcrypt.hashpw(data['password'].encode('utf-8'), bcrypt.gensalt()).decode('utf-8')

            birth_date  = data.get('birth_date')
            recommender = data.get('recommender')
            event_name  = data.get('event_name')

            with transaction.atomic():   
                user_model = User(
                    account             = data['account'],
                    password            = hashed_pw,
                    name                = data['name'],
                    email               = data['email'],
                    phone_number        = data['phone_number'],
                    gender_id           = data['gender_id'],
                    birth_date          = birth_date,
                    recommender         = recommender,
                    event_name          = event_name,
                    grade_id            = 1,
                    terms_and_condition = TermsAndCondition(id=1),
                    mileage             = 0
                    )

                user_model.save()

                Address(
                    name      = data['address'],
                    user_id   = user_model.id,
                    is_active = True
                    ).save()
                
                return JsonResponse({"message":"SUCCESS"}, status=201)

        except KeyError as e:
            return JsonResponse({"message":"KEY_ERROR =>" + e.args[0]}, status=400)

4) 아이디/이메일 중복 검사

에러처리

  • 아이디가 입력되지 않은 경우 ACCOUNT_NOT_ENTERED 메시지 리턴
  • 아이디가 존재하는 경우 ACCOUNT_EXIST 리턴
  • 아이디가 조건에 부합하지 않는 경우 INVALID_ACCOUNT 리턴
  • 기타에러 발생 시 KEY_ERROR 리턴
# 아이디 중복검사 코드이며, 이메일 중복검사 코드도 동일하게 구현
class CheckAccountView(View):
    def post(self, request):
        data = json.loads(request.body)

        REGEX_ACCOUNT_1 = '^[a-z]{6,}$'                      
        REGEX_ACCOUNT_2 = '^(?=.*[0-9])(?=.*[a-z]).{6,}$' 

        try:
            if user_model.account == '':
                return JsonResponse({"message":"ACCOUNT_NOT_ENTERED"}, status=400)
            
            if User.objects.filter(account=data['account']).exists():
                return JsonResponse({"message":"ACCOUNT_EXIST"}, status=400)
            
            return re.match(REGEX_ACCOUNT_1, account) or re.match(REGEX_ACCOUNT_2, account)

            return JsonResponse({"message":"SUCCESS"}, status=200)
    
        except KeyError:
            return JsonResponse({"message":"KEY_ERROR"}, status=400)
profile
What you think, you become. What you feel, you attract. What you imagine, you create.

0개의 댓글