-
[PROJECT] METABOXDEV/🔍 PROJECT 2021. 10. 4. 17:35
🟪 METABOX PROJECT
로고는 영현님께서 제작하셨다..! 역시 디자이너란 예술가에 가까운 영역인듯하다! MEGABOX 홈페이지를 모티브로 한 사이트입니다.
사이트에 접속한 모든 유저들은 상영중인 영화리스트와 각 영화에 대한 사용자들의 포스트를 볼 수 있습니다.
메타박스 회원만 예매를 할 수 있으며, 관람한 영화에 대해서 포스트를 작성할 수 있습니다.
🚀 Back-end : Github-Repository
🚀 Front-end : Github-Repository1. 팀원 소개
<사진삽입>
📦김영현님, 김동준님, 이송현님
📦윤현묵님, 송치헌님, 주종민
2) 작업기간
2021.09.13 - 2021.10.1
3) 시연영상
4) 실행환경
- MacOSX
- Ubuntu
- Conda 4.10.3 version
- Django 3.2.6 version
- MySQL 5.7 version
- mysqlclient 2.0.3 version
- Django-cors-headers 3.7.0 version
5) 기술스택
- FE : html, styled-compnents, javascript, react
- BE : Python, Django, MySQL
6) 협업툴
- Slack
- Git & Github
- Trello
- Postman
4) 역할
FE
김영현님
• [MoviePost] : pagination, 무비포스트 상세 modal
• [MoviePostWrite], 이미지 파일을 포함한 포스트 작성, 50자 이내
• [Modal] : 로그인/회원가입, 소셜 로그인/회원가입
• [Footer]
• 로고 디자인
김동준님
• [Nav] : 기본 라우팅, 사이트맵, 검색 창 클릭 이벤트
• [Main] : 영화 리스트 pagination / 정렬 (평점/개봉일/보고싶어요)
• [Details] : radar, block, line 그래프를 이용하여 영화 주요 정보 표현
• Libraries : chart.js
이송현님
• [Booking]예매 화면의 상단 캘린더 로직달력 아이콘 클릭 시, 테이블 형태의 캘린더 fold/unfold로직 구현
• 메가박스의 빠른 예매 기본 프로세스와 동일한 로직 구현
• Libraries : date-fns, React Date PickerBE
윤현묵님
[Movie]
•영화 목록 조회 기능구현
•Pagenation 영화상세정보 조회기능
•구현영화 찜 기능(좋아요 기능)
•영화 목록 정렬 및 검색 기능 구현(최신 순, 평점 순, 좋아요 순 등)
[DB Upload]
•CSV 파일을 이용한 Script DB Uploader 기능 구현
주종민님
[User]
• [Sign up] : METABOX 회원가입구현
• [Sign in] : 일반 로그인 기능
• [Social Log-in] 소셜 로그인 (카카오 로그인) 기능 구현
• [JWT] : Json Web Token 발급 및 토큰 만료기능 구현
송치헌
[Booking]
• 상영 날짜, 영화, 영화관 별로 상영 시간을 필터링하는 기능 구현
• 좌석 설정 기능 구현
• 예매 내역 조회 기능 구현
[Authorization]
• python decorator를 이용한 인가 기능 구현(로그인 데코레이터)
공통구현
[MoviePost](영화 리뷰)
• 리뷰 작성 기능 구현(인가된 유저만 작성 가능
• 모든 리뷰 조회 기능 구현
• 리뷰 정렬 기능 구현(최신 순, 공감 순)
• Pagenation
프로젝트에 앞서 배우는 입장으로 다시 돌아가 이전 프로젝트때 구현하지 못했던 기능을 일부러 선택하기로 결정했다1. 구현페이지 ☑️
✅ 메인 페이지
메인 페이지는 동준님께서 구현해주셧다. 레이아웃 잡는 작업이 진짜 힘들었을텐데 정말 너무나 잘해주셨다! 막판에 페이지네이션 요구가 있었는데 뚝딱뚝딱 바로 수정사항을 반영해주셨다! 너무나 감사했다
✅ 회원가입 및 로그인
회원가입과 로그인 기능은 영현님(FE)과 내가 구현했다!
프론트엔드에서 인증코드와 토큰을 받아오고
백엔드에서 받은 토큰을 통해 카카오 자원서버에 요청하는 로직이였다.
처음에 OAuth 개념없이 하다보니 백엔드에서 모두 인증코드를 통해 가져오는 것이라 생각했는데, 프론트에서 인증코드와 토큰을 가져와주고 백엔드에서 자원을 요청하는 것이 좋다는 피드백을 받고 나서 로직을 바꿨다.
프론트와의 소통이 중요하다는 것을 이번기회에 또 한번 느꼈다.
✅ 전체 리스트 페이지
동준님과 현묵님께서 구현해주신 전체리스트 페이지 레이아웃이다. 레이아웃이 너무나 깔끔하게 구현이 되었다!
검색기능과 평점순, 개봉일순, 보고싶은 순으로 필터링도 구현이 되었다.
✅ 상세페이지
동준님과 현묵님의
✅ 빠른예매
송현님과 치헌님께서 구현해주신 상세페이지이다. 깔끔한 레이아웃과 효율적인 통신으로 정말 구현이 잘된 페이지이다.
✅ 리뷰기능
1. 사이트 분석 - 백엔드 관점
백엔드 관점에서 데이터가 어떻게 분류되어있는지 분석해보자
영화 - 전체영화, N스크린, 큐레이션, 무비포스트
예매 - 빠른예매, 상영시간표, 더부티크 프라이빗 예매
극장 - 전체극장 , 특별관
이벤트 - 진행중인 이벤트, 지난 이벤트, 당첨자 발표
스토어
혜택 - 메가박스 멤버, 제휴/할인
내가 구현하고자 했던 회원가입과 로그인을 살펴보자.
일반회원가입과 로그인은 이전 시간에 배웠던 회원가입과 로그인 기능구현을 쓰면 되고,
문제는 소셜 로그인 구현에 있었다.
프론트에게 받을 정보에는
우선 카카오 인증코드를 통해 받은 카카오 서버입장권인 토큰을 헤더로 받아오는 것이다.
2. 데이터관점에서 기능별로 모델링
우선 영화 사이트에서 백엔드 기능을 3가지로 분류살펴보면
회원가입과 로그인을 담당하는 User,
전체리스트와 상세페이지를 구현하는 Movie,
영화를 예매하고 저장하는 Booking,
영화 리뷰를 쓰고 저장하는 MoviePost(Review)
테이블을 구성했다.
특히나 무비포스트에서 이미지 데이터 세팅에서 각 영화별로 이미지를 가져와야했기 떄문에 데이터셋을 만들때 무비포스트 ID 값을 기준으로 데이터를 집어넣고 시작했다.
3. 주요 구현 기능
1. 로직 흐름( 인 & 아웃 )
프론트가 요청(인)을 하면 백엔드가 아웃을 해야한다.
전체 리스트 페이지에서 요청하는 데이터를 살펴보자.
IN
인증코드로 받아낸 카카오 서버입장권인 토큰
OUT
카카오 서버에 등록된 회원정보( 이메일, 이름, 연령대, 카카오 id)2. 소셜 로그인 뷰 작성
View.py
import json, re, requests, bcrypt, jwt import random from django.views import View from django.http import JsonResponse from datetime import datetime, timedelta from my_settings import SECRET_KEY, ALGORITHM from users.models import User class KakaoSignInView(View): def post(self, request): try: access_token = request.headers["Authorization"] response = requests.get('https://kapi.kakao.com/v2/user/me', headers = ({"Authorization" : f'Bearer {access_token}'})).json() email = response["kakao_account"]["email"] name = response["kakao_account"]["profile"]["nickname"] kakao_id = response["id"] birthday = response["kakao_account"]["birthday"] age_range = response["kakao_account"]["age_range"] birth_min = int(list(age_range.split('~'))[0]) birth_max = int(list(age_range.split('~'))[1]) month = birthday[:2] day = birthday[2:] birth_year = 2021-(random.randrange(birth_min, birth_max+1)) print(birth_year) birth_day = f'{birth_year}-{month}-{day}' if User.objects.filter(kakao_id=kakao_id).exists(): user = User.objects.get(kakao_id = kakao_id) token = jwt.encode({"id" : user.id, 'exp':datetime.utcnow() + timedelta(days=3)}, SECRET_KEY, algorithm= ALGORITHM) if not User.objects.filter(kakao_id=kakao_id).exists(): User.objects.create( kakao_id = kakao_id, email = email, name = name, birth_day = birth_day ) user = User.objects.get(kakao_id = kakao_id) token = jwt.encode({"id" : user.id, 'exp':datetime.utcnow() + timedelta(days=3)}, SECRET_KEY, algorithm= ALGORITHM) return JsonResponse({"MESSASGE" :"로그인 성공!", 'token' : token}, status = 200) except KeyError: return JsonResponse({"MESSAGE":"KEY_ERROR"}, status = 400) except ValueError: return JsonResponse({"MESSAGE": "VALUE_ERROR"}, status = 400) ====================================================================================== 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" : "DUPLICATED 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"], birth_day = data["birth_day"], email = data["email"], password = decoded_password ) return JsonResponse({"MESSAGE" : "SUCCESS"}, status = 201) 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_PASSWORD"}, status = 401) access_token = jwt.encode({"id": user.id , 'exp':datetime.utcnow() + timedelta(days=3)}, 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)
urls.py
from django.urls import path from users.views import SignUpView, SignInView, KakaoSignInView urlpatterns = [ path('/sign-up', SignUpView.as_view()), path('/sign-in', SignInView.as_view()), path('/sign-in/kakao', KakaoSignInView.as_view()), ]
test.py
from django.test import TestCase, Client from unittest.mock import MagicMock, patch from .models import User class KakaoLoginTest(TestCase): def setUp(self): User.objects.create( id = 1, email = 'twotwo@naver.com', birth_day = '1994-02-02', phone_number = '010-2222-2222', name = '두번째님', kakao_id = '222222222', ) def tearDown(self): User.objects.all().delete() @patch("users.views.requests") def test_kakao_signin_success(self, mocked_requests): client = Client() class MockedResponse: def json(self): return { "id": 222222222, "kakao_account": { "profile" : {"nickname": "두번째님"}, "email" : "twotwo@kakao.com", "age_range" : "20~29", "birthday" : "0202", } } mocked_requests.get = MagicMock(return_value = MockedResponse()) headers = {"HTTP_Authorization": "Fake_access_token"} response = client.post("/users/sign-in/kakao", content_type = 'application/json' , **headers) self.assertEqual(response.status_code, 200) @patch("users.views.requests") def test_kakao_signup_success(self, mocked_requests): client = Client() class MockedResponse: def json(self): return { "id": 12345678, "kakao_account": { "profile" : {"nickname": "회원가입님"}, "email" : "signup@kakao.com", "age_range" : "20~29", "birthday" : "0301", } } mocked_requests.get = MagicMock(return_value = MockedResponse()) headers = {"HTTP_Authorization": "Fake_access_token"} response = client.post("/users/sign-in/kakao", content_type = 'application/json', **headers) self.assertEqual(response.status_code, 200) @patch("users.views.requests") def test_kakao_login_fail(self, mocked_requests): client = Client() class MockedResponse: def json(self): return { } mocked_requests.get = MagicMock(return_value = MockedResponse()) headers = {"HTTP_Authorization" : "Fake_access_token"} response = client.post('/users/sign-in/kakao', content_type='application/json', **headers) self.assertEqual(response.status_code, 400)
🚨Modelling 과정에서 User - birthday 값에서 null true을 고려하지 않았다!
Model 작성 중 당연히 카카오 정보에는 생년월일 포함된 정보가 나올 줄 알았지만, 생년월일까지 알기 위해선 비즈니스 계정이 필요했고, 따라서 년도가 빠진 생일만을 얻게 되었다. 불완전한 정보를 처리해야하기때문에 생년월일에 대한 로직에 대한 처리가 필요했다.
영화에서 가장 중요한건 성인인지 아닌지 판별하기 위해서 연령대를 기준으로 성인인지 판별하려고 했다.
이런 데이터 처리는 불완전한 정보만 양성하기 때문에 좋지않다!
따라서 앞으로 프로젝트 기간 중 불완전한 정보를 갖게 된다면 프론트에게 추가 필요정보를 요청하는 것이 좋다는 것을 알게 되었다.
birthday = response["kakao_account"]["birthday"] age_range = response["kakao_account"]["age_range"] birth_min = int(list(age_range.split('~'))[0]) birth_max = int(list(age_range.split('~'))[1]) month = birthday[:2] day = birthday[2:] birth_year = 2021-(random.randrange(birth_min, birth_max+1)) birth_day = f'{birth_year}-{month}-{day}'
3. 기억에 남는 코드
🚀Model에서 null = True 의 중요성
🚀형식에 맞게 카카오 자원 서버에 보낸뒤 json 형태로 반환해서 저장하는 것
requests.get('https://kapi.kakao.com/v2/user/me', headers = ({"Authorization" : f'Bearer {access_token}'})).json()
🚀Unit test에서 헤더에 Authorization이 아닌 HTTP_Authoriztion, post로 보낼때, '/ ' 붙이는 것
headers = {"HTTP_Authorization" : "Fake_access_token"}
response = client.post('/users/sign-in/kakao', content_type='application/json')
🚀token의 만료시간을 두어 보안성을 높인 것
5. 프로젝트를 회고하며.
> 추석이 중간이 껴있어 이번 프로젝트에서 많은 것을 할 수 있다고 생각했지만, 시간이 정말 부족했다. 애자일하게 진행되려고 했으나 소셜로그인 개념을 잡고 시작할때 3일정도 걸리고 이를 코드로 구현하기까지 1주일의 시간이 지체되었다. 좀더 애자일하게 진행되었다면, 일반로그인과 회원가입 먼저 끝내고 데코레이터 까지 마친 뒤 소셜 로그인으로 넘어가는 것이 지금 생각해보면 맞는 순서인 듯하다.
특히나 유닛테스트 과정에서 카카오 자원서버인척 하는 MockedResponse 개념도 처음 접하고 unit test 자체도 처음이다 보니 1차프로젝트보다 로직 완성이 지체되었지만, unit test의 필요성을 많이 느끼게 된 프로젝트였다.
이번프로젝트를 무사히 마치면서 팀원분들에게 모두 감사함을 전하고 싶다. 특히나 시간이 마지막까지 부족한 상황에서 당황하지 않고 침착하게 끝까지 백엔드 로직을 수정하고 고민하신 치헌님, 묵묵하게 맡은 일은 끝내시고 언제나 팀원들을 챙겨주신 현묵님, 프로젝트 기간 압도적인 실력으로 프론트엔드와 백엔드를 연결해주신 동준님, 정말 어려운 기능임에도 끝까지 구현해주신 송현님, 그리고 마지막까지 백엔드 로직 구현이 늦어지는 상황 속에서 당황하지 않고 프로젝트의 끝을 잘 마무리해주신 영현님 정말 감사드립니다
'코딩 > 🔍 PROJECT' 카테고리의 다른 글
[PROJECT] LAFESTA COMMERCE SITE (0) 2021.09.19 [PROJECT] 서울 아파트가격과 인프라개수 간의 상관관계 분석 (0) 2021.03.04