[METMIXA_영화 추천 웹사이트] 5. 프로젝트 진행 - Backend(Django)

logg·2021년 7월 14일
0
post-thumbnail

개요


ERD 설계가 끝난 후 Django를 활용하여 서버를 구성하는 작업을 진행하였다. 진행하면서 기록해놓으면 좋을 사항들만 추려내서 글로 작성하려고 한다.

0. 구조


  • api(프로젝트)
  • account(계정 앱)
  • movie(서비스 관련 앱)

1. MtoM에서 원하는 필드 추가하는 법


from django.db import models

class Doctor(models.Model):
    name = models.TextField()

    def __str__(self):
        return f'{self.pk}번 의사 {self.name}'

class Patient(models.Model):
    name = models.TextField()

    def __str__(self):
        return f'{self.pk}번 환자 {self.name}'

# 직접 중개모델을 생성
class Reservation(models.Model):
    doctor = models.ForeignKey(Doctor, on_delete=models.CASCADE, related_name='reservations')
    patient = models.ForeignKey(Patient, on_delete=models.CASCADE, related_name='reservations')
    message = models.CharField(max_length=50)

    def __str__(self):
        return f'{self.doctor_id}번 의사의 {self.patient_id}번 환자'
  • Reservation.objects.create(doctor=doctor인스턴스, patient=patient인스턴스, message=문자열)
  • Doctor.reservations.get(patient=patient1)


2. DB 구축 API에서 장르-영화, 영화-감독, 영화-배우 간 M:N 관계 설정해주기


  • 문제 상황
    • tmdb API를 통해 정보를 받아와 장르, 영화, 감독, 배우 테이블을 bulk_create 를 통해 구성
    • 테이블 간의 M:N관계를 설정해주어야 하나 bulk_create 에서 create 작업을 통째로 하기 때문에 각각의 생성된 객체에 중개모델 데이터를 넣어주기 위한 add 작업 불가
Director.objects.bulk_create(
		[Director(
	      name = data.get('name'),
	      original_name = data.get('original_name'),
	      ) for data in req.get('crew') if not Director.objects.filter(name=data.get('name')).exists() and data.get('job') == 'Director']
)
  • 해결
    • bulk_create 가 실행속도가 빠르나, 중개모델에 데이터를 넣어줘야 하는 우리의 상황에서는 적절하지 않은 방법임을 깨닫고 다음의 방법을 통해 해결
      1. for 문을 통해 각각의 정보를 순회하며 Model.objects.create 로 객체를 하나씩 생성
      2. 반환되는 객체를 변수에 담아 변수.중개모델manager.add(모델) 로 중개모델 데이터 추가
for data in req.get('crew'):
		if not Director.objects.filter(name=data.get('name')).exists() and data.get('job') == 'Director':
		    director = Director.objects.create(name=data.get('name'), original_name=data.get('original_name'))
		    director.movies.add(movie)


3. Movie 정보를 받아올 때 DB에 영화가 존재하는지에 따라 업데이트/생성 분기


  • 구현 목표

    • API를 통해 Movie 정보를 받아올 때 분기가 발생
      1. 받아온 정보가 DB의 내용과 중복되는 경우 기존의 영화 정보 업데이트
      2. 중복되지 않는 경우 새로 영화 정보 추가
  • 방법

    • objects 매니저의 filterexists()를 활용해 현재 영화 정보가 DB에 존재하는지 확인
    • 존재하면 title 을 사용해 기존 영화 정보 객체를 가져와 popularity, tmdb_vote_sum, tmdb_vote_cnt 필드값 업데이트
    • 존재하지 않으면 create를 사용해 새로운 정보 객체 생성 후 저장
...
# 이미 DB에 있는 영화면 정보를 업데이트
if Movie.objects.filter(title=data.get('title')).exists():
    movie = Movie.objects.get(title=data.get('title'))
    movie.popularity = data.get('popularity')
    movie.tmdb_vote_sum = data.get('vote_average') * data.get('vote_count')
    movie.tmdb_vote_cnt = data.get('vote_count')
    movie.save()
# DB에 없는 영화면 새로 생성
else:
	movie = Movie.objects.create(...)


4. 사용자 API


  • 사용자 API
    • 데코레이터를 통해 인증된 사용자만 사용 가능, 일부는 관리자만 사용 가능
@authentication_classes([JSONWebTokenAuthentication]) # JWT가 유효한지
@permission_classes([IsAuthenticated]) # 인증 여부를 확인
  • 추가로 accounts/views.py/profile 함수의 경우 데코레이터가 없을 시 미인증 에러 발생

  • 포토티켓은 Vue에서의 infinite scroll 구현을 위해 django의 Paginator를 사용하여 페이지를 구분해서 결과 반환

    • Profile Page에서 무한 스크롤 진행 시 최대 페이지 수를 넘겨도 계속 무한 스크롤 되는 이슈 발생
      • views.py에서 최대 페이지 수 정보를 추가해서 넘겨서 → vue에서 해당 정보를 이용해서 현재 페이지 수최대 페이지 수 이하일 때만 무한 스크롤 기능 작동하게 함
  • 추천 알고리즘 기능을 위해서 사용자가 영화에 평점을 주는 동시에 유저-장르-점수 중개테이블에 데이터 추가

    1. rate 함수를 GET, POST, PUT에 따라 분기처리

    2. POST는 단 한 번만 실행되어야 한다(사용자는 한 영화에 한 개의 평점만 생성할 수 있음)

      Vue에서 if문 분기처리 필요(GET을 하고 인스턴스가 존재하면 PUT, 존재하지 않으면 POST)

    3. rate 함수 실행과 동시에 유저-장르- 점수 중개테이블을 채우는 기능이 작동

# rate GET 방식일 때 내부에서 실행되는 함수
# 1. movie에 대한 장르 아이디들을 받아옴
genres = Genre.objects.filter(movies__pk=movie_pk)
    if request.method == 'GET':
        serializer = RateSerializer(rate)
        return Response(serializer.data)
# rate POST 방식일 때 내부에서 실행되는 함수
# 2. for문을 통해 각 장르 아이디들에 대해서 유저장르점수 객체를 만듦
for genre in genres:
    serializer_algo = RecommendAlgoScoreSerializer(data=request.data)
    if serializer_algo.is_valid(raise_exception=True):
        serializer_algo.save(genre=genre, user=request.user)
# rate POST 방식일 때 내부에서 실행되는 함수
# 3. for문을 통해 각 장르 아이디들에 대해 유저장르점수 객체 업데이트
for genre in genres:
		recommend_algo_score = RecommendAlgoScore.objects.filter(user__pk=request.user.pk, genre__pk=genre.pk).first()
    serializer_algo = RecommendAlgoScoreSerializer(recommend_algo_score, data=request.data)
    if serializer_algo.is_valid(raise_exception=True):
	    serializer_algo.save()

5. 추천 알고리즘


  • random.choices 를 활용하여 각 장르에 가중치를 주고 랜덤한 장르를 6개 뽑아서 추천(중복 가능)

    • choices는 중복을 허용하기 때문에 뽑히는 장르들 또한 중복값이 존재(ex. 12 12 23 24)
    • 이 또한 가중치를 통한 랜덤의 묘미(가중치가 높은 장르만 뽑힐 수도 있음)
  • 사용자가 각 장르에 준 평점의 누적합을 구해 각 장르의 가중치 설정

    • RecommendAlgoScore 테이블에 유저가 영화에 평점을 줄 때마다 해당 영화가 가진 장르에 대한 유저의 평점 값이 저장
  • 뽑은 장르들에 대해 for 문으로 순회하며 각 장르에 해당하는 영화를 인기있는 영화부터 차례로 25개씩 가져온 쿼리셋을 합집합

    • 만약, 한 영화가 뽑힌 장르들을 여러개 가지고 있다면 이 영화는 여러번 쿼리셋에 담김. 이를 통해 자연스럽게 뽑힌 장르들을 중복으로 가지고 있는 영화들이 쿼리셋을 차지하고 적게 가지고 있는 영화들이 담기지 않게 됨
@api_view(['GET'])
@authentication_classes([JSONWebTokenAuthentication])
@permission_classes([IsAuthenticated])
def movie_list(request):
    mode = request.GET.get('mode')
    # 추천 0. mode - 알고리즘
    if mode == 'algorithm':
        weight = []
        # 19가지 장르의 pk
        for id in range(1, 20):
            genre_weight = RecommendAlgoScore.objects.filter(user__pk=request.user.pk, genre__pk=id).aggregate(Sum('rate'))['rate__sum']
            weight.append(genre_weight if genre_weight else 1)

        recommend_genre_ids = random.choices([28, 12, 16, 35, 80, 99, 18, 10751, 14, 36, 27, 10402, 9648, 10749, 878, 10770, 53, 10752, 37], weight, k=6)

        movies = Movie.objects.none()
        for genre_id in recommend_genre_ids:
            temp_movies = Movie.objects.filter(genres__tmdb_genre_id=genre_id)[:25]
            movies = movies|temp_movies

6. 영화 조회 및 검색


  • Select 박스를 통해 사용자가 선택한 방식에 따라 영화들을 뿌려줌
    • 위의 추천 알고리즘 코드에 이어서 작성된 부분
    • ORM문에서 filter 를 사용할 때 lookup 을 사용해서 M:N관계에서의 정보들을 원하는대로 뽑아올 수 있음
# 조회 1. mode - 최신순, 평점순, 인기순
    elif mode in ('release_date', 'vote_average', 'popularity'):
        if mode != 'vote_average':
            movies = Movie.objects.order_by(f'-{mode}')[:100]
        else:
            movies = Movie.objects.annotate(vote_average=(F('tmdb_vote_sum') + F('our_vote_sum')) / (F('tmdb_vote_cnt') + F('our_vote_cnt'))).order_by('-vote_average')[:100]
    elif mode in ('director', 'actor', 'title'):
    # 조회 2. mode - 감독별, 배우별, 영화명별
        inputValue = request.GET.get('inputValue')
        if mode == 'director':
            # MtoM 관계에서 원하는 조건을 가지는 영화들을 가져오는 방법
            # https://docs.djangoproject.com/en/3.2/topics/db/examples/many_to_many/
            movies = Movie.objects.filter(Q(directors__name__icontains=inputValue)|Q(directors__original_name__icontains=inputValue)).distinct()[:100]
        elif mode == 'actor':
            movies = Movie.objects.filter(Q(actors__name__icontains=inputValue)|Q(actors__original_name__icontains=inputValue)).distinct()[:100]
        else:
            # 한글 제목이나 원본 제목이 사용자의 입력(inputValue)를 포함하는 영화들을 반환(대소문자 구분하지 않음)
            movies = Movie.objects.filter(Q(title__icontains=inputValue)|Q(original_title__icontains=inputValue))[:100]
    # 조회 3. mode - 장르별
    elif mode == 'genre':
        inputGenre = request.GET.get('inputGenre')
        movies = Movie.objects.filter(genres__tmdb_genre_id=inputGenre)[:100]

    serializer = MovieListSerializer(movies, many=True)
    return Response(serializer.data)
profile
logg

0개의 댓글

관련 채용 정보