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를 공유하면 훨씬 효율적일 것 같다.
'Project' 카테고리의 다른 글
[Wecode] 밀리의서재 <내일채움공책> 1차 리팩토링 (0) | 2021.12.03 |
---|---|
[Pre-Onboarding] 8percent 회고록 (0) | 2021.11.14 |
[Pre-Onboarding] 원티드랩 회고록 (0) | 2021.11.10 |
[Wecode] 밀리의서재 <내일채움공책> 회고록 (0) | 2021.10.01 |