Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
Tags
- 파이썬
- 코딩테스트
- 주니어개발자역량강화
- print("""
- 항해
- 파이썬 클래스
- 주니어개발자멘토링
- fatal:not a git repository
- not a git repository
- print sep
- 개발자스터디
- 파이썬 int()
- 백준
- 개발자사이드프로젝트
- 99클럽 #99일지 #코딩테스트 #개발자스터디 #항해 #til
- Fatal Python error: init_fs_encoding: failed to get the Python codec of the filesystem encoding
- 파이썬 map 함수
- 파이썬 |
- 10430번
- MomentumParameters
- 99일지
- 파이썬 sep
- EnvCommandError
- Til
- 항해플러스
- cp949
- 항해99
- 코딩부트캠프후기
- 99클럽
- vscode cp949
Archives
- Today
- Total
선발대
[원티드] 2주차 과제: 페이히어 / 가계부 내역 CRUD, 테스트 코드 본문
과제 진행기간
- 2022년 7월 4일(월) ~ 2022년 7월 8일(금)
과제 요구사항
- 고객은 이메일, 비밀번호 입력을 통해 회원가입을 할 수 있음
- 고객은 로그인 이후 가계부 관련 아래의 행동을 할 수 있음.
- CREATE: 가계부에 오늘 사용한 돈의 금액, 관련 메모를 남길 수 있음
- UPDATE: 가계부에서 수정을 원하는 내역은 금액, 메모를 수정할 수 있음
- DELETE: 가계부에서 원하는 내역은 삭제할 수 있음
- DELETE_FLAG: 삭제한 내역은 언제든지 다시 복구할 수 있음
- LIST: 가계부에서 이제까지 기록한 가계부 리스트를 볼 수 있음
- DETAIL: 가계부의 상세한 세부 내역을 볼 수 있음
- 고객은 회원 가입 이후, 로그인, 로그아웃 할 수 있음
- 로그인하지 않은 고객은 가계부 내역에 대한 접근 제한 처리가 되어야 함.
기능구현 목표
- 회원가입 구현
- 로그인, 로그아웃 구현
- 가계부 Create(POST) 구현
- 가계부 Read(GET) 구현
- 가계부 Update(PATCH) 구현
- 가계부 Delete 구현(Delete/Delete_Flag)
- 가계부 List 기능 구현(사용 중인 가계부/삭제된 가계부)
- 가계부 Detail 기능 구현
- 사용자 권한 처리 구현(JWT를 활용한 Permission 설정)
- JWT(DRF-SimpleJwt) 활용
- 배포(AWS & Docker(Nginx+gunicorn+MySQL))
- Swagger를 활용한 API 기능 문서화
ERD
API 명세서
공부한 내용
- 이번에는 가계부 내역의 CRUD를 만드는 파트를 맡았다.
- 가계부는 여러 종류가 있다. (예: 1월 가계부, 2월 가계부, ...)
- 그 가계부마다 여러 세부 내역들이 있는데, 내가 맡은 파트가 이런 내역의 CRUD를 구현하는 것이었다.
- 테스트 코드도 동시에 작성해야 했기 때문에 부지런히 해야 했다.
- 서로 같은 views.py, serializers.py를 작성하는 경우가 종종 있어 충돌도 많았다.
- 그래서 각 앱마다 views, serializers, tests 폴더를 만들고 그 안에 각자의 파일을 작성하기로 했다.
작성한 코드
01. account_books/urls.py
from django.urls import path
from account_books.views.account_book_views import AccountBookView
from account_books.views.account_category_views import AcoountCategoryView
from .views.account_book_detail_views import AccountBookDetailView
app_name = 'account-book'
urlpatterns = [
path('account_category', AcoountCategoryView.as_view()),
path('account_category/<int:account_category_id>', AcoountCategoryView.as_view()),
path('account_category/toggle_active/<int:account_category_id>', AcoountCategoryView.toggle_active),
path('account-books', AccountBookView.as_view()),
path('account-books/<int:book_id>', AccountBookView.as_view()),
path('account-books/toggle_delete/<int:book_id>', AccountBookView.toggle_active),
path('account-books/deleted_list', AccountBookView.deleted_list),
path('account-books/toggle_delete/<int:book_id>', AccountBookView.deleted_patch),
path('account-books/<int:book_id>/accounts', AccountBookDetailView.as_view(), name='book_details'),
path('account-books/<int:book_id>/accounts/<int:accounts_id>', AccountBookDetailView.as_view(), name='book_details'),
path('account-books/<int:book_id>/accounts/detail/<int:accounts_id>', AccountBookDetailView.detail, name='book_detail',),
path('account-books/<int:book_id>/accounts/<int:accounts_id>/toggle_active', AccountBookDetailView.toggle_active, name='book_details_deleted',),
]
02. account_books/views/account_book_detail_views.py
- 가계부 내역 전체 리스트
- 가계부 내역 생성
- 가계부 내역 수정
- 가계부 내역 상세조회
- 가계부 내역 삭제, 복구: delete_flag를 이용해서 정말 삭제하는 게 아니라, 상태만 변경하였다.
from account_books.models import AccountBook, AccountCategory, AccountDetail
from account_books.serializers import AccountDetailPostSerializer, AccountDetailSerializer
from drf_yasg.utils import swagger_auto_schema
from rest_framework import status
from rest_framework.decorators import api_view
from rest_framework.generics import get_object_or_404
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
class AccountBookDetailView(APIView):
'''가계부 내역 구현 View
writer : 나
date : 2022-07-08
가계부 내역의 CRUD 구현
GET: 가계부 내역 전체 리스트
POST: 가계부 내역 생성
PATCH: 가계부 내역 수정
detail.GET: 가계부 내역 상세조회
toggle_active.PATCH: 가계부 내역 삭제, 복구
'''
serializer_class = AccountDetailSerializer
permission_classes = [IsAuthenticated]
# 가계부 내역 전체 리스트: 가계부 id를 받고, 해당 가계부의 전체 내역을 보여줍니다. + 쿼리스트링으로 delete_flag 여부에 따른 리스트 필터링 가능.
@swagger_auto_schema(responses={200: AccountDetailSerializer})
def get(self, request, book_id):
account_book = get_object_or_404(AccountBook, id=book_id, user=request.user.id)
account_details = account_book.account_details.filter(delete_flag=request.GET.get('deleted', False))
serializer = AccountDetailSerializer(account_details, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
# 가계부 내역 생성: 가계부 id를 받고, 해당 가계부에서 내역을 작성합니다. + 해당하는 카테고리가 없을 경우 에러가 발생합니다.
@swagger_auto_schema(request_body=AccountDetailPostSerializer, responses={200: AccountDetailPostSerializer})
def post(self, request, book_id):
# 카테고리 id로 객체를 찾아서 가져옵니다. / 유효하지 않는 카테고리일 경우 404 에러가 나옵니다.
category = AccountCategory.objects.filter(
user=request.user.id, id=request.data.get('account_category'), delete_flag=False
).first()
serializer = AccountDetailPostSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
# 가계부 내역 수정: 가계부 id, 가계부 내역 id를 받고 해당되는 단일 내역을 수정합니다.
@swagger_auto_schema(request_body=AccountDetailPostSerializer, responses={200: AccountDetailPostSerializer})
def patch(self, request, book_id, accounts_id):
account_book = get_object_or_404(AccountBook, id=book_id, user=request.user.id) # 가계부
account_detail = get_object_or_404(AccountDetail, account_book=account_book.id, id=accounts_id) # 가계부 내역
serializer = AccountDetailPostSerializer(account_detail, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
# 가계부 내역 상세조회: 가계부 id, 가계부 내역 id를 받고 해당되는 단일 내역을 보여줍니다.
@api_view(['GET'])
@swagger_auto_schema(responses={200: AccountDetailPostSerializer})
def detail(request, book_id, accounts_id):
account_book = get_object_or_404(AccountBook, id=book_id, user=request.user.id) # 가계부
account_detail = get_object_or_404(AccountDetail, account_book=account_book.id, id=accounts_id) # 가계부 내역
serializer = AccountDetailSerializer(account_detail)
return Response(serializer.data, status=status.HTTP_200_OK)
# 가계부 내역 삭제, 복구: delete_flag 상태만 변경해서 삭제를 구현했습니다.
@api_view(['PATCH'])
@swagger_auto_schema(request_body={}, responses={200: {}})
def toggle_active(request, book_id, accounts_id):
account_book = get_object_or_404(AccountBook, id=book_id, user=request.user.id) # 가계부
account_detail = get_object_or_404(AccountDetail, account_book=account_book.id, id=accounts_id) # 가계부 내역
account_detail.toggle_active()
return Response(account_detail.delete_message, status=status.HTTP_200_OK)
03. account_books/models.py
- account_type에서 choices를 사용해서 수입, 지출을 구분했다.
- 미리 작성해둔 TimeStampModel을 상속받아서 각 모델마다 created_at, updated_at 이 자동으로 작성되었다.
- delete_flag로 삭제 상태만 변경하였다.
# 가계부 상세내용
class AccountDetail(TimeStampModel, DeleteFlag):
Type = [
('0', "지출"),
('1', "수입"),
]
account_category = models.ForeignKey(
'AccountCategory', related_name='account_details', verbose_name='카테고리', null=True, on_delete=models.DO_NOTHING
)
account_book = models.ForeignKey('AccountBook', related_name='account_details', verbose_name='가계부', on_delete=models.CASCADE)
written_date = models.DateTimeField()
price = models.DecimalField(max_digits=9, decimal_places=0)
description = models.CharField(blank=True, null=True, max_length=255)
account_type = models.CharField(choices=Type, default='0', max_length=10) # 수입, 지출 구분
delete_flag = models.BooleanField(default=False)
class Meta:
db_table = 'account_detail'
def __str__(self):
return f'book_name: {self.account_book.book_name} / price: {self.price}'
04. account_books/tests/test_account_books_detail.py
- 위의 views.py에서 작성했던 5가지 기능들을 전부 성공, 실패로 나누어서 테스트하였다.
- 주로 테스트는 상태 코드가 일치하는지를 기준으로 진행하였다.
import json
from account_books.models import AccountBook, AccountCategory, AccountDetail
from account_books.views.account_book_detail_views import AccountBookDetailView
from django.urls import reverse
from payhere.test_models import LoginTestModel
from rest_framework import status
from rest_framework.test import APIClient, APIRequestFactory, APITestCase
from users.models import User
class AccountBookDetailTestCase(APITestCase):
'''
Writer: 나
Date: 2022-07-06
가계부 내역의 CRUD 구현
GET: 가계부 내역 전체 리스트
POST: 가계부 내역 생성
PATCH: 가계부 내역 수정
detail.GET: 가계부 내역 상세조회
toggle_active.PATCH: 가계부 내역 삭제, 복구
'''
# 초기 데이터 생성
def setUp(self):
# 유저 생성
self.user = User.objects.create_user(nickname='test2', email='test2@gmail.com', password='test2@A!')
self.login_test = LoginTestModel(self.user)
# 가계부 생성
self.account_book = AccountBook.objects.create(
user=self.user, book_name='test', budget='10000', delete_flag=False
)
# 카테고리 생성
self.account_category = AccountCategory.objects.create(user=self.user, category_name='식비')
# 가계부 내역 생성
self.account_book_detail = AccountDetail.objects.create(
written_date='2022-07-06T15:07:35+09:00',
price=1500,
description="subway",
account_type=1,
account_category=self.account_category,
account_book=self.account_book,
)
# [성공] 가계부 내역의 전체 리스트를 조회합니다.
def test_success_get_account_book_list(self):
url = '/account-books//accounts'
response = self.login_test.login_user_case(
self.account_book.id, view=AccountBookDetailView.as_view(), url=url, method='get'
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
# [성공] 가계부 내역에서 1개의 내역을 생성합니다.
def test_success_post_account_book_object(self):
data = {
'written_date': '2022-07-06T15:07:35+09:00',
'price': 1500,
'account_type': 1,
'account_category': self.account_category.id,
'account_book': self.account_book.id,
}
url = '/account-books//accounts'
response = self.login_test.login_user_case(
self.account_book.id, view=AccountBookDetailView.as_view(), url=url, method='post', data=data
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
# [실패] 가계부 내역에서 1개의 내역을 생성합니다. / 빈 data를 보내서 테스트합니다.
def test_fail_post_account_book_object(self):
data = {}
url = '/account-books//accounts'
response = self.login_test.login_user_case(
self.account_book.id, view=AccountBookDetailView.as_view(), url=url, method='post', data=data
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
# [성공] 가계부 내역 수정: 가계부 id, 가계부 내역 id를 받고 해당되는 단일 내역 수정을 테스트합니다.
def test_success_patch_account_book_object(self):
data = {
'written_date': '2022-07-06T15:07:35+09:00',
'price': 50000,
'description': "train",
'account_type': 1,
'account_category': self.account_category.id,
'account_book': self.account_book.id,
}
url = 'account-books//accounts/detail/'
response = self.login_test.login_user_case(
self.account_book.id,
self.account_book_detail.id,
view=AccountBookDetailView.as_view(),
url=url,
method='patch',
data=data,
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
# [실패] 가계부 내역 수정: 가계부 id, 가계부 내역 id를 받고 해당되는 단일 내역 수정을 테스트합니다. / 빈 데이터를 보내서 테스트 합니다.
def test_fail_patch_account_book_object(self):
data = {}
url = 'account-books//accounts/detail/'
response = self.login_test.login_user_case(
self.account_book.id,
self.account_book_detail.id,
view=AccountBookDetailView.as_view(),
url=url,
method='patch',
data=data,
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
# [성공] 가계부 내역 상세조회: 가계부 id, 가계부 내역 id를 받고 해당되는 단일 내역을 테스트합니다.
def test_success_get_account_book_object(self):
url = 'account-books//accounts/detail/'
response = self.login_test.login_user_case(
self.account_book.id,
self.account_book_detail.id,
view=AccountBookDetailView.detail,
url=url,
method='get',
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
# [실패] 가계부 내역 상세조회: 가계부 id, 가계부 내역 id를 받고 해당되는 단일 내역을 테스트합니다. / 존재하지 않는 book_id, accounts_id로 테스트합니다.
def test_fail_get_account_book_object(self):
url = 'account-books//accounts/detail/'
response = self.login_test.login_user_case(0, 0, view=AccountBookDetailView.detail, url=url, method='get')
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
# [성공] 가계부 내역 삭제: 가계부 id, 가계부 내역 id를 받고 해당되는 단일 내역 삭제를 테스트합니다. (delete_flag 상태만 변경됩니다.)
def test_success_patch_delete_flag(self):
url = 'account-books//accounts//deleted'
# 한번에 삭제, 복구 둘 다 테스트하게 됩니다.
# (1) 삭제 혹은 복구를 테스트합니다.
response = self.login_test.login_user_case(
self.account_book.id,
self.account_book_detail.id,
view=AccountBookDetailView.toggle_active,
url=url,
method='patch',
)
self.account_book_detail.refresh_from_db()
self.assertEqual(self.account_book_detail.delete_flag, True)
self.assertEqual(response.status_code, status.HTTP_200_OK)
# (1) 위에서 했던 테스트의 반대 행동을 테스트 합니다.
response = self.login_test.login_user_case(
self.account_book.id,
self.account_book_detail.id,
view=AccountBookDetailView.toggle_active,
url=url,
method='patch',
)
self.account_book_detail.refresh_from_db()
self.assertEqual(self.account_book_detail.delete_flag, False)
self.assertEqual(response.status_code, status.HTTP_200_OK)
후기
- 테스트 코드 작성이 아직 익숙하지 않아서 초반에 많이 어려웠는데 팀원 분들 덕분에 기한 내 작성할 수 있었다.
- 이번 프로젝트는 Docker를 이용해서 웹 서버와 DB를 띄웠다. Docker-compose 사용법을 익힐 수 있었다.
- 최종 프로젝트도 배포되었다. 배포는 내가 담당하지 않아서 아직 잘 모르지만 다음 프로젝트 때 더 배우고 싶다.
- 늦게까지 작업이 진행되어서 자고 바로 다음날 아침부터 이어서 하는 경우가 종종 있었는데
- 그렇게 열어둔 브라우저 창만 40개가 넘고 기록이 많이 밀렸다. 작업 틈틈이라도 기록하는 습관이 필요하다.
- 프로젝트를 하면서 좋은 점 중 하나는 내가 모르는 부분을 알 수 있는 점이다.
- 이번 세션을 통해 JWT 토큰에 대해서 이해할 수 있게 되었다.
'원티드 > 기업 과제' 카테고리의 다른 글
[원티드] 1주차 과제: (주)랩큐 / 테스트 코드 (0) | 2022.07.03 |
---|
Comments