위코드 1차프로젝트를 무사히 마친뒤 바로 2차 프로젝트가 시작되었다. 얼떨결에 내가 제안했던 사이트가 프로젝트로 선정되며 2차에서는 PM으로 참여하게되었다. 아이디어 발표당시 아이디어스와 클래스101두가지를 동시에 제안했었는데, 논의결과 클래스101으로 결정되었다. 2차라서 1차보다 많이 구현하고싶었는데, 의외의(?)요소로 많이 시간이 소비된 프로젝트였다. 프론트,백 모두 여러모로 많이 고생했던 프로젝트, 그래서 더 정이 많이가던 프로젝트로 기억된다
FrontEnd
HTML(JSX) / JavaScript (ES6) / React (CRA 세팅) / Styled-Component / Hooks(useState / useEffect / useRef) / Redux / React-Router / asiox
BackEnd
Python / Django / CORS Header / Bcrypt / PyJWT / MySQL / AqueryTool (데이터베이스 모델링) / Postman,Httpie (API 관리) / AWS(서버 및 DATABASE관리) / Django-seed및 Faker
협업 도구
Slack / Git + GitHub / Trello를 이용, 일정관리 및 작업 현황 확인
내가 구현한건 ⭐️표시
1. 모델링 구축
1차 프로젝트는 프로젝트에서 사용되는 부분만 모델링을 했는데, 2차에서는 사이트 전체를 모두 모델링했다. 전체적인 사이트의 흐름을 볼 수 있어서 좋았던것 같다. 그리고 1차의 아쉬운 부분이던 모델링에 투자한 시간을 2차에서는 많이 단축할 수 있었다. Good👍
2.회원가입 & 로그인 (SignUp & SignIn) 페이지
3.유저 마이페이지
4.스토어 메인화면 페이지⭐️
5.크리에이터 센터 페이지
로그인한 유저 기준으로 클래스 좋아요 기능을 만들었다. 로그인이 되지 않은 경우 @데코레이터
에서 에러가 나기때문에 따로 로직을 작성하진 않았다. 클래스101의 경우 좋아요 반영은되지만 위의 gif처럼 바로 ❤️아이콘, 숫자가 반영되지는 않았는데 함께한 프론트 민아님⭐️이 바로바로 반영될 수 있게 만들어주셨다.
#클래스 좋아요 기능
class ProductLikeView(View):
@id_auth
def post(self, request):
try:
data = json.loads(request.body)
user = request.user
product = Product.objects.get(id = data['product'])
if ProductLike.objects.filter(user = user, product = product).exists():
return JsonResponse({'message':'ALREADY_LIKE'},status = 400)
ProductLike.objects.create(user = user, product = product)
return JsonResponse({"message":"SUCCESS"},status = 201)
except Product.DoesNotExist:
return JsonResponse({"message":"DOESNOTEXIST"}, status=404)
except KeyError:
return JsonResponse({"message":"KEY_ERROR"}, status=400)
except json.JSONDecodeError:
return JsonResponse({"message":"INVALID_DATA"}, status=400)
#좋아요 취소
@id_auth
def delete(self, request):
try:
data = json.loads(request.body)
user = request.user
product = Product.objects.get(id = data['product'])
if ProductLike.objects.filter(user_id = user, product_id = product).exists():
ProductLike.objects.get(user = user, product = product).delete()
return JsonResponse({'message':'SUCCESS'}, status = 204)
return JsonResponse({'message':'NOT_EXIST'}, status = 400)
except Exception as ex:
return JsonResponse({"message":"ERROR_" + ex.args[0]}, status=400)
좋아요에 대한 에러부분을 처음에는
except Exception as ex:
return JsonResponse({"message":"ERROR_" + ex.args[0]}, status=400)
로 전부 처리했었는데, 좋지못하다는 동기분 피드백으로 다시 수정했다(고마워요 승재님!)
좋아요 취소부분에대한
except
가 너무 시간이 촉박해서 예외부분 처리를 다 하지 못했는데, 이부분 꼭 리팩토링을 하고싶다...🙏 (기업협업끝나면...)
메인페이지에서 오픈된 클래스 & 좋아요 높은 기준으로 상위 10개정도를 뽑아내는 로직을 작성했다.
#like 기준 리스트 출력
class OrderByLikeView(View):
def get(self, request):
[1] products = Product.objects.select_related('sub_category','creator', 'creator__user','sub_category__category').prefetch_related('course_set','course_set__course_status','course_set__courserequest_set','productlike_set').filter(course__course_status_id = 1)
[2] if not products.exists():
return JsonResponse({'message':'PRODUCT_NOT_FOUND'},status=404)
results = [{
'id' : product.id,
'alt' : product.name,
'cover_image' : product.course_set.get().courserequest_set.all()[0].thumbnail,
'sub_category' : product.sub_category.name,
'course_status' : product.course_set.get().course_status.name,
'creator' : product.creator.user.nickname,
'name' : product.name,
'price' : product.price,
'like' : product.productlike_set.filter(product_id = product.id).count(),
'created_at' : product.course_set.get().courserequest_set.all()[0].created_at
}for product in products]
[3] result = sorted(results, key=lambda x:x['like'], reverse=True)
return JsonResponse({'message':'SUCCESS', 'results': result}, status=200)
[1] : ClassStatus
테이블이 존재하는데, 여기서 id=1
을 opened class
로 정해두어 id=1
인 값을 먼저 뽑아내는 로직을 작성했다
[2] : 해당되는 상품이 없는 경우를 먼저 if문
으로 정의한뒤 에러메세지가 뜨게했다
[3] : lambda
를 이용해서(처음써봤다!!) product
에서'like'
값을 기준으로 필터링이 되게했다. 정렬은 reverse=True
로 역순으로 결과값이 출력되게 하여 높은값기준으로 나올 수 있게 작성했다.
<얼리버드>
#얼리버드 기준
class EarlybirdView(View):
def get(self, request):
[1] products = Product.objects.prefetch_related('productlike_set').filter(course__course_status_id = 2)
얼리버드의 경우 필터값만 변하는거라 간단하게 작성했다
[1] : ClassStatus
테이블에서earlybird
의id
값을 필터링하게 했다.
<최근 업데이트된 클래스>
class CourseListView(View):
def get(self, request, category_pk):
[1] products = Product.objects.prefetch_related('course_set','course_set__course_status','course_set__courserequest_set','productlike_set').select_related('sub_category','creator', 'creator__user','sub_category__category').filter(sub_category__category_id = category_pk, course__course_status_id = 1).order_by('-id')
[1] : 생성된 날짜가 오픈된 날이라고 생각하고 order_by
매서드를 이용했다. 그리고 정렬 기준을 id
로 잡았다. 그냥 id
면 생성날짜가 오래된순으로 정렬되므로 반대로 적용하기 위해 -id
라고 입력해주었다
1 차프로젝트에서는 데이터베이스에 하나하나 값을 넣는 용자짓(?)을 했었다.
근데 이번에는 엄청 많은 유저와 실질적인 좋아요 반영이 필요한 상황이다보니 csv
혹은 faker
를 사용했어야했는데 병민님의 조언으로 faker
를 이용해서 데이터를 만들었다.
물론 실질적으로 이름변경을 대대적으로 하긴했는데 그래도 시간단축이 어마어마하게 되어서 완전 편했다
단언컨데 나뿐만아니라 모두의 발목을 잡은 존재는 이녀석때문이라고 말할수 있다🙃
2차 프로젝트에서 가장 많은 고난과 역경을 알려준 녀석이였다. 쉽게 생각하면 한없이 쉬운 존재이나.. 다르게 생각하면 정말 어려운 존재였다. httpie
혹은 프론트와 맞춰보면서 해결하면 되는것이 아닌가? 라고 생각하던 내 생각을 산산히 부숴준 UnitTest
.. 정말 이해하는데 오래걸렸고, 자료도 구글링하기 어려웠던 기억이 있다...🙄 로직은 완성된지 한참됬는데 UnitTest
통과를 위해 많은 시간을 투자해서 결국 다음기능으로 나아가지 못했다..ㅠㅠ
class ProductLikeViewTest(TransactionTestCase):
def setUp(self):
UserGrade.objects.create(
id = 1,
name = '기본',
description = '기본회원입니다'
)
User.objects.create(
id = 1,
name = '테스트',
email = 'test@naver.com',
password = 'asdfasdf',
nickname = '테스트',
grade_id = 1
)
ApplyPath.objects.create(
id = 1,
reason = '사이트발견'
)
Creator.objects.create(
id = 1,
user_id = 1,
apply_path_id = 1,
information = '테스트용입니다'
)
Category.objects.create(
id = 1,
name = '카테고리')
SubCategory.objects.create(
id = 1,
name = '서브카테고리',
category_id = 1
)
Product.objects.create(
id = 1,
name = '테스트',
creator_id = 1,
price = 1000,
sub_category_id = 1,
refund_policy = '환불안됨',
)
CourseStatus.objects.create(
id = 2,
name = '오픈'
)
CourseLevel.objects.create(
id = 1,
name = '초급'
)
Course.objects.create(
id = 1,
kit_description = 'test',
course_status_id = 2,
benefit = 'test',
product_id = 1,
level_id = 1
)
def tearDown(self):
UserGrade.objects.all().delete()
User.objects.all().delete()
ApplyPath.objects.all().delete()
Creator.objects.all().delete()
Category.objects.all().delete()
SubCategory.objects.all().delete()
Product.objects.all().delete()
CourseStatus.objects.all().delete()
CourseLevel.objects.all().delete()
Course.objects.all().delete()
CourseRequest.objects.all().delete()
def test_product_like_post_success(self):
client = Client()
payload = {"id":1}
headers = {"HTTP_AUTHORIZATION" : jwt.encode(payload, SECRET, algorithm=ALGORITHM)}
data = {
"user": 1,
"product": 1
}
response = self.client.post('/product/like',data, content_type="application/json", **headers)
self.assertEqual(response.status_code,201)
self.assertEqual(response.json(),
{'message': 'SUCCESS'}
)
def test_product_like_post_doesnotexist_fail(self):
client = Client()
payload = {"id":1}
headers = {"HTTP_AUTHORIZATION" : jwt.encode(payload, SECRET, algorithm=ALGORITHM)}
data = {
"user": 1,
"product": 3
}
response = self.client.post('/product/like',data, content_type="application/json", **headers)
self.assertEqual(response.status_code, 404)
self.assertEqual(response.json(),
{"message" : "DOESNOTEXIST"}
)
def test_product_like_post_invealid_user_fail(self):
client = Client()
payload = {"id":2}
headers = {"HTTP_AUTHORIZATION" : jwt.encode(payload, SECRET, algorithm=ALGORITHM)}
data = {
"user": 1,
"product": 1
}
response = self.client.post('/product/like',data, content_type="application/json", **headers)
self.assertEqual(response.status_code, 401)
self.assertEqual(response.json(), {
"MESSAGE" : "INVALID_USER"
})
def test_product_like_post_keyerror_fail(self):
client = Client()
payload = {"id":1}
headers = {"HTTP_AUTHORIZATION" : jwt.encode(payload, SECRET, algorithm=ALGORITHM)}
data = {
"user": 1,
"products": 1
}
response = self.client.post('/product/like',data, content_type="application/json", **headers)
self.assertEqual(response.status_code, 400)
self.assertEqual(response.json(), {
"message" : "KEY_ERROR"
})
좋아요에 대한 Unittest이다.. 정~~말 시간 많이 걸렸다. 너무 많이 고생했고, 그만큼 애착이 있다.
이 OK를 받기위해.. 많은 시간이 걸렸다...
1.Prefetch_related/Select_related를 격파했다. 나는야 테이블 연결의 여왕😎
1차 프로젝트에서는 로그인과 장바구니 기능구현을 담당해서 이 부분을 공부할 기회가 없었는데, 이번에 만나게 되었다.
원래 기존에 작성한 로직은 이러했다
category = Category.objects.get(id = category_pk)
subcategory = SubCategory.objects.filter(category = category)
products = Product.objects.prefetch_related('course_set','course_set__courserequest_set').select_related('sub_category','creator', 'creator__user').filter(sub_category__in = subcategory)
물론 prefetch_related
&select_related
를 사용했으나 이럴경우 시간이 많이 걸린다 👉 각각 변수에 맞는 데이터를 한번씩 다 가져오니까! 시간이 오래걸린다.
해당부분을 한줄로 줄여보라는 피드백을 받고 엄청나게 머리 싸매며 고생한 결과
products = Product.objects.select_related('sub_category','creator', 'creator__user','sub_category__category').prefetch_related('course_set','course_set__course_status','course_set__courserequest_set','productlike_set').filter(sub_category__category_id = category_pk)
한번에 9개의 테이블을 한번에 불러올 수 있었다(강태공). 데이터를 불러오는 시간이 단축되었다 넘모좋아!!!!🥰
더이상 무섭지않다. 역참조-역참조, 역참조-정참조!!
2.Django, Views.py를 작성할땐 장고shell을 이용하자
얼리버드 필터링에 관련되어 반드시 course_status
의 id값을 가져와야하는 상황이 있었다.
이때
course_status
는Product
테이블 기준으로
Product 👉 CourseSet 👉 CourseStatus (역참조-역참조)
prefetch_related
를 사용해 course_set__course_status
로 테이블을 연결하는건 문제없이 진행된 상황이였는데, Filter
에서 course_set__course_status_id
라고 하면 계속 에러가 나는것!!!🤬 필터링 관련 구글링을 해도 이부분에 대한 이야기가 잘 없어서 새벽내내 다른팀 백엔드와 머리싸매고 원인을 찾다가 포기하고 집에가려던 찰나!
아 가기전에 일단 다 쳐보기나 해보자..
course__course_status_id
말도 안되는데 한번 쳐보자
실행되더라..🤭...
헐....
장고 shell에서 filter값을 잘못 칠경우(예시 :
course_set__course_status_id
) 필터에서 선택할수있는 키워드를 친.히 알려주는데 보면 course가 있다!!
이부분은 장고 shell을 이용하지 않고 그냥 로직을 짜면 절대 알 수 없는 부분이더라.. 보통 prefetch로 참조한 애들은 __set
을 쓰면 filter에서도 __set
을 쓰니까! 정말 큰 공부가 되었다.
스스로 해결하려 노력한점
멘토바라기 석주는 1차에서는 모르는걸 바로 멘토님 질문하는 징징이였다..
이번에는 정말 스스로 해결하기위해 노력했다. 물론 동기분들의 도움을 많이 구했지만, 서로서로 돕고사는것 아니겠나!🤔
정답을 찾아가는 과정에서 많은것을 배운것 같다
멘탈관리
1차에서 폭주하던 나였지만 2차에서는 조용히 팀원으로 참여하자~라고 생각했지만, 아뿔사...PM당첨🤭
팀원모두 1차에서 못했던걸 2차에서 꼭 구현하겠다는 마음으로 온거라 이부분을 잘 리드할 수 있을지 덜컥 겁이났다. 진행상황을 전체적으로 파악하기보다는 내 업무에 집중하는데도 벅찬 실력이라 처음부터 팀원들에게 이부분에 대한 도움요청을 했다. 그리고 스스로 욕심을 많이 버렸다. 어설프게 많이 구현하는것보다 하나를 제대로 해보자라고 생각하고 임해서, 마지막까지 멘탈이 흔들린적은 크게 없었던것 같다
정말정말 중요한.. 체력💪
위코드 첫달, 그리고 1차 프로젝트까지는 정신력으로 체력을 극복할수 있었다.
오피스에서 밤새고, 새벽6시에 위워크로 향하던게 당연하던 나날이였는데, OMG, 2차때는 그게 안되더라
알람소리를 못듣고, 코드를 치면서 졸기시작했다. 의지로 해결할 수 없는 체력의 문제가 대두되면서 체력을 미리 키워놓지 못한 나를 원망했다😭 위코드 수료하고, 코로나가 진정되면.. 꼭 운동 다시 시작해야겠다...
스케줄 관리의 미흡함
프로젝트 중에도 나는 부가적으로 공부할 것들이 산더미처럼 쌓인상황이었다. 새로운 지식을 접하면 이를 이해하고 블로그에 문서화해 내것으로 만들 시간이 필요한데, 이걸 전혀 못했다.. 체력적인것과도 연관이 될 수 있는데, 잠을 줄이고 그시간에 공부를 하면 되니까! 근데 그걸 전혀 못했고, 내 블로그는 치킨계를 위한 포스팅외에는 텅텅빈 공간이 되어버렸다.. 너무너무 아쉽고 스스로에게 화가나는 부분이다😭
1차는 내가 홍일점이였는데 2차는 정반대였다. 한명(잭잭🐤)을 제외하곤 이미 친한사람들이라 친해지는 과정이 전혀 필요가 없었다. 너무나 화목했고, 너무나 즐거웠다. 프론트에서는 새로운 기술을 적용하느라, 백엔드에서는 UnitTest등의 문제로 프로젝트의 진행은 1차보다 느렸지만, 더 알찼다고 생각한다.
다들 체력적으로 참 힘들었을텐데, 잘따라와줘서 너무너무 고맙다는 마음뿐이다🙇♀️
벌써 2차는 이 글 작성 기준으로 일주일이나 지났고, 다들 벌써 기업협업을 나가있다. 각자의 자리에서 다들 화이팅했으면❤️