본문 바로가기

Project

[Wecode] 라코스테 <Lafesta> 회고록

1. 팀원 소개

▶ Front-end : 이송현, 정도영, 최파란별 → https://github.com/wecode-bootcamp-korea/24-1st-LaFesta-frontend

▶ Back-end : 박지원, 주종민, 하예준 → https://github.com/wecode-bootcamp-korea/24-1st-LaFesta-backend

라코스테 프로젝트 팀원

2. 프로젝트 기간

▶ 2021.08.30(월) ~ 2021.09.10(금)

 

3. 사용 기술 스택

▶ Front-end :  HTML, SCSS, JS, React

▶ Back-end : Python, Django, MySQL

 

4. 주요 구현 기능

▶ 기능별 구분 

로그인 , 회원가입 기능 구현

제품 상세 페이지

제품 전체 리스트 페이지(가격 & 타입 & 색 & 핏 필터링 기능)

제품 검색 기능 구현 

장바구니 기능 구현

 

▶ 페이지별 구분

메인 페이지 구현

상세 페이지 구현

상세 페이지 알림 창 구현

5. 나의 역할

▶  로그인 & 회원가입 API / 데코레이터 함수(장바구니 기능 시 필요) / 검색 기능 구현

 

▶ 모델링 자료

 

▶ 코드 리뷰

[ 회원가입 view.py ]

∨ 이메일 & 비밀번호 정규표현식 사용하여 유효성 검사

∨ Bcrypt 암호화를 사용하여 비밀번호 저장

class SignupView(View):
    def post(self, request):
        data = json.loads(request.body)
        REGEX_EMAIL = re.compile("^[a-zA-Z0-9+-_.]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$")
        REGEX_PASSWORD = re.compile(
            "^(?=.*[A-Za-z])(?=.*\d)(?=.*[$@$!%*#?&])[A-Za-z\d$@$!%*#?&]{8,}$"
        )

        password = data["password"]

        if User.objects.filter(email=data["email"]).exists():
            return JsonResponse({"MESSAGE": "ALREADY EXISTED EMAIL"}, status=400)

        if not REGEX_EMAIL.match(data["email"]):
            return JsonResponse({"MESSAGE": "EMAIL_ERROR"}, status=400)

        if not REGEX_PASSWORD.match(data["password"]):
            return JsonResponse({"MESSAGE": "PASSWORD_ERROR"}, status=400)

        hashed_password = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt())
        decoded_password = hashed_password.decode("utf-8")

        User.objects.create(
            name=data["name"],
            gender=data["gender"],
            email=data["email"],
            phone_number=data["phone_number"],
            password=decoded_password,
            birthday=data["birthday"],
        )
        return JsonResponse({"MESSAGE": "SUCCESS"}, status=201)

 

[ 로그인 view.py ]

∨ JWT 을 사용하여 로그인 기능 구현

+ 추가로 응답 값에 로그인 시 유저이름을 반환하여, 프론트에서 유저이름을 확인할 수 있도록 협의.

class SigninView(View):
    def post(self, request):
        data = json.loads(request.body)

        try:
            if not User.objects.filter(email=data["email"]).exists():
                return JsonResponse({"MESSAGE": "INVALID_USER"}, status=401)

            user = User.objects.get(email=data["email"])

            if not bcrypt.checkpw(
                data["password"].encode("utf-8"), user.password.encode("utf-8")
            ):
                return JsonResponse({"MESSAGE": "INVALID_USER"}, status=401)
            
            access_token = jwt.encode({"id": user.id }, MY_SECRET_KEY , algorithm="HS256")
            return JsonResponse({"MESSAGE": "SUCCESS", 'token' : access_token, "user_name" : user.name}, status=200)

        except KeyError:
            return JsonResponse({"MESSAGE": "KEY_ERROR"}, status=400)

처음에는 아래와 같이 코드를 짰으나 팀원들의 도움으로 위와 같이 리팩토링을 하였다.

※ 보완점 :  if 문 안의 if 문을 피하기 위하여 수정 & user라는 변수를 사용하여 가독성을 높임

        try:
            if not User.objects.filter(email=data['email']).exists():
                return JsonResponse({"MESSAGE": "INVALID_USER"}, status=401)

            user = User.objects.get(email = data['email'])

            if not bcrypt.checkpw(data['password'].encode('utf-8'), user.password.encode('utf-8')):
                return JsonResponse({"MESSAGE": "INVALID_USER"}, status=401)
            
            access_token = jwt.encode({"id": user.id }, MY_SECRET_KEY , algorithm="HS256")
            return JsonResponse({"MESSAGE": "SUCCESS", 'token' : access_token}, status=200)

        except KeyError:
            return JsonResponse({"MESSAGE": "KEY_ERROR"}, status=400)

 

[ login_decorator 함수 ]

∨ 주요 로직

: 로그인했을 때 토큰을 프론트에게 발급하면 토큰을 웹 브라우저에 가지고 있다가 로그인 데코레이터 함수를 호출하면, 유저의 정보가 올바른지 확인하기 위해 그 토큰을 해체하는 과정

1. Authorization 에 담김 부분을 access_token에 저장

2. 유저의 id에 접근해서 request.user 라는 변수에 담는다. 다른 앱에서 해당 변수를 사용한다.

※ 주의!! 인코딩일 때는 algorithm='HS256' 디코딩일 때는 algorithms='HS256'

def login_decorator(func): 
    def wrapper(self,request, *args, **kwargs):
        try : 
            access_token = request.headers.get('Authorization', None)    
            payload = jwt.decode(access_token, MY_SECRET_KEY, algorithms='HS256')  
            request.user = User.objects.get(id=payload['id'])                                                           
        except jwt.exceptions.DecodeError:                                     
            return JsonResponse({'message' : 'INVALID_TOKEN' }, status=400)

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

        return func(self, request, *args, **kwargs)

    return wrapper

[ 검색 기능 구현 ]

∨ keyword 라는 변수를 프론트에서 받아서, 상품 이름에  검색 키워드가 존재하는지의 여부를 판단한다.

Q객체를 사용해서 필터링을 하였다. 검색 이외의 나머지 기능은 종민님께서 구현해주셨다. 

※ 주의!! icontains 는 대소문자 구분하지 않고, contains는 대소문자 구분

 

class ProductListView(View):
    def get(self, request):
        try:
            type_id = request.GET.get("typeId", None)
            color_id = request.GET.getlist("colorId",None) 
            fit_id = request.GET.getlist("fitId", None)
            search_keyword = request.GET.get("keyword", None)

            order = request.GET.get("order",'id')
            page = int(request.GET.get("page", 1))
            
            q = Q()
            if type_id:
                q.add(Q(type_id__in = type_id), Q.AND)    
            if color_id:
                q.add(Q(colors__in = color_id), Q.AND)
            if fit_id:
                q.add(Q(fit__in = fit_id), Q.AND)
            if search_keyword:
                q &= Q(name__icontains=search_keyword)

            products = Product.objects.filter(q).order_by(order)

처음에는 아래와 같이 구현하였으나, 위와 같이 Q객체를 사용해서 리팩토링하였다.

class SearchView(View):
    def get(self, request):
        search_keyword = request.GET.get('keyword', None)
        if not search_keyword:
            return JsonResponse({"MESSAGE" : "NO KEYWORD"}, status=400)
        
        products = Product.objects.filter(name__icontains=search_keyword)

        if not products.exists():
            return JsonResponse({"message": "PRODUCT NOT FOUND"}, status=204)

6. 프로젝트를 마치며

▶ 일정관리와 협업을 어떻게 하였나?

 

∨ 우리 팀은 매일 11시쯤 Daily standup meeting을 진행하였는데,

처음이라 서서 미팅을 짧게 하는 것이 어색하기도 했지만 예준님의 많이 주도해주셔서 점차 편안하게 진행을 하게 되었다.

TRELLO 라는 툴을 사용하여 일정을 관리했다. Github 에 merger가 완료되면 done에 티켓을 옮기는 기준으로 프로젝트의 진행사항을 체크했고, 중요하게 이야기되는 부분을 문서화하지 않으니 잘 기억이 나지 않는 문제점이 있어서, 간단하게 회의를 기록하는 식으로 TRELLO를 활용했다.

 

▶ 라코스테 프로젝트를 마치며

처음 팀 프로젝트라서 내 역할을 잘 해낼 수 있을까?라는 고민도 많이 하고, 부담감을 느꼈다. 하지만 팀원들이 있어서 다행이라는 생각이 들었다. 같은 목표를 가지고 2주 동안 서로 다독이며 달려왔던 시간들이 기억에 많이 남을 것 같다.

송현님, 도영님, 파란별님, 종민님, 예준님 모두 감사합니다!!

 

+ 추가로, 기억에 남는 부분

프론트랑 처음으로 통신을 하다 보니까 어떤 부분들을 협의하고 진행해야 하는지 잘 감이 오지 않았다.

파란별님이랑 로그인, 회원가입 API를 맞춰보게 되었는데, 키값을 공유하지 않고 진행을 해서 당황했었고,

현업에 가서는 절대로 실수해서는 안 될 기본적인 것이라는 느꼈다...!!

다음에는 API 문서 작성도 직접 해서 구두가 아닌 문서로 API를 공유하면 훨씬 효율적일 것 같다.