[위코드 2차 프로젝트] 클래스101 클론코딩 후기

여주링·2021년 1월 16일
4

Project

목록 보기
6/6
post-thumbnail

🌷AnotherClass101

🌷Project Overview

위코드 1차프로젝트를 무사히 마친뒤 바로 2차 프로젝트가 시작되었다. 얼떨결에 내가 제안했던 사이트가 프로젝트로 선정되며 2차에서는 PM으로 참여하게되었다. 아이디어 발표당시 아이디어스클래스101두가지를 동시에 제안했었는데, 논의결과 클래스101으로 결정되었다. 2차라서 1차보다 많이 구현하고싶었는데, 의외의(?)요소로 많이 시간이 소비된 프로젝트였다. 프론트,백 모두 여러모로 많이 고생했던 프로젝트, 그래서 더 정이 많이가던 프로젝트로 기억된다

  • 진행기간 : 2020년 12월 28일 ~ 2021년 1월 8일
  • 프로젝트 인원 : Frontend 3명, Backend 3명

🌷기술 스택

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를 이용, 일정관리 및 작업 현황 확인

🌷구현 기능(Backend기준)

내가 구현한건 ⭐️표시

1. 모델링 구축

  • Django-seed, Faker를 활용한 테스트용 데이터를 자동 생성하여 API 테스트에 활용⭐️
  • AWS EC2 서버 연동및 RDS를 통한 데이터베이스 관리⭐️

    1차 프로젝트는 프로젝트에서 사용되는 부분만 모델링을 했는데, 2차에서는 사이트 전체를 모두 모델링했다. 전체적인 사이트의 흐름을 볼 수 있어서 좋았던것 같다. 그리고 1차의 아쉬운 부분이던 모델링에 투자한 시간을 2차에서는 많이 단축할 수 있었다. Good👍

2.회원가입 & 로그인 (SignUp & SignIn) 페이지

  • bcrypt를 사용한 암호화
  • JWT 로그인 구현 및 @decorator를 이용해서 토큰 인증
  • Email&닉네임 정규화를 통한 Validation적용
  • 회원가입시 문자인증과정 적용
  • 소셜로그인 구현완료
  • 로그인 및 회원가입 UnitTest 완료

3.유저 마이페이지

  • AWS S3와 연동
  • S3에 이미지 파일 저장 및 URL생성, 데이터베이스 저장기능 적용
  • 유저 정보 변경시 데이터베이스에 적용하기

4.스토어 메인화면 페이지⭐️

  • @decorator 적용후 유저별 좋아요 기능 구현⭐️
  • 인기순 필터링 적용한 강의 리스트 API작성⭐️
  • 얼리버드(오픈예정인 강의) 필터링 적용한 강의 리스트 API작성⭐️
  • 최근 업데이트순 필터링 적용한 강의 리스트 API작성완료⭐️
  • 리스트및 좋아요 관련 UnitTest 완료⭐️

5.크리에이터 센터 페이지

  • 크리에이터가 생성 중인 강의가 있을 경우 해당 정보를 불러오고, 없을 경우 새로운 강의를 생성하도록 구현
  • 임의의 여러 장의 사진을 S3 서버에 올려서 순서에 맞게 처리할 수 있도록 API 구현
  • 강의 카테고리, 난이도 등을 지정하여 저장하는 API 구현
  • 크리에이터 관련 UnitTest 완료

🌷결과 화면

1.좋아요 기능

로그인한 유저 기준으로 클래스 좋아요 기능을 만들었다. 로그인이 되지 않은 경우 @데코레이터에서 에러가 나기때문에 따로 로직을 작성하진 않았다. 클래스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가 너무 시간이 촉박해서 예외부분 처리를 다 하지 못했는데, 이부분 꼭 리팩토링을 하고싶다...🙏 (기업협업끝나면...)

2. 메인페이지 - 좋아요가 많은 기준으로 필터링

메인페이지에서 오픈된 클래스 & 좋아요 높은 기준으로 상위 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=1opened class로 정해두어 id=1인 값을 먼저 뽑아내는 로직을 작성했다
[2] : 해당되는 상품이 없는 경우를 먼저 if문으로 정의한뒤 에러메세지가 뜨게했다
[3] : lambda를 이용해서(처음써봤다!!) product에서'like'값을 기준으로 필터링이 되게했다. 정렬은 reverse=True로 역순으로 결과값이 출력되게 하여 높은값기준으로 나올 수 있게 작성했다.

3. 메인페이지 - 얼리버드 & 최근에 업데이트 기준으로 필터링

<얼리버드>

#얼리버드 기준
class EarlybirdView(View):
    def get(self, request):
   [1]     products = Product.objects.prefetch_related('productlike_set').filter(course__course_status_id = 2)

얼리버드의 경우 필터값만 변하는거라 간단하게 작성했다
[1] : ClassStatus테이블에서earlybirdid값을 필터링하게 했다.

<최근 업데이트된 클래스>

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라고 입력해주었다

4.DATABASE Faker 적용

1 차프로젝트에서는 데이터베이스에 하나하나 값을 넣는 용자짓(?)을 했었다.
근데 이번에는 엄청 많은 유저와 실질적인 좋아요 반영이 필요한 상황이다보니 csv혹은 faker를 사용했어야했는데 병민님의 조언으로 faker 를 이용해서 데이터를 만들었다.
물론 실질적으로 이름변경을 대대적으로 하긴했는데 그래도 시간단축이 어마어마하게 되어서 완전 편했다

5. Unit Test🙃

단언컨데 나뿐만아니라 모두의 발목을 잡은 존재는 이녀석때문이라고 말할수 있다🙃

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_statusProduct테이블 기준으로
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차는 이 글 작성 기준으로 일주일이나 지났고, 다들 벌써 기업협업을 나가있다. 각자의 자리에서 다들 화이팅했으면❤️

profile
🌱Backend Developer👩‍💻

0개의 댓글