졸업 프로젝트

도나·2024년 5월 20일
post-thumbnail

1. 프로젝트 기획

정확한 프로젝트 기획을 위해 사용자 기반의 요구사항 명세서를 먼저 작성해보았다.

User 요구사항 명세서

2. Django - BackEnd

1) 시작하기

  1. django 설치
pip install django
  1. Fairy-Taiary 라는 이름을 가진 프로젝트 생성
python -m startproject Fairy-Taiary
  1. diary라는 이름의 앱 생성
python manage.py startapp diary
  1. diary라는 앱이 생성되고 나면 Fairy-Taiary/settings.py에 들어가 diary(앱 이름)을 추가해준다. 앞으로 후에 우리가 앱을 추가할 때마다 방금처럼 INSTALLED_APPS에도 추가하는 과정을 함께해주면 된다.
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'diary'
]

2) DRF

DRF란?

Django Rest Framework의 약자로 Django를 기반으로 REST API 서버를 만들기 위한 라이브러리

DRF로는 CRUD(Create, Read, Update, Delete)를 구현하기 쉽기 때문에 DRF를 사용해보았다.

3) Diary

Diary app = 일기에 관한 로직

Viewset

Django REST Freamework를 사용하면 ViewSet을 사용해 클래스 안에서 관련있는 View들에 대한 로직을 합칠 수 있다.

views.py

Django에서 View란 Client에서 보낸 Request에 대해서 Response을 보내주는 역할을 한다. 세부적으로는 View는 Django 필요한 데이터를 모델에서 가져와서 적절히 가공하여 웹 페이지 결과를 만들도록 한다.

from rest_framework import mixins, status
from rest_framework.decorators import action
from rest_framework.viewsets import GenericViewSet
from rest_framework.generics import get_object_or_404

class DiaryViewSet(GenericViewSet,
                  mixins.ListModelMixin,
                  mixins.CreateModelMixin,
                  mixins.RetrieveModelMixin,
                  mixins.UpdateModelMixin,
                  mixins.DestroyModelMixin):

    permission_classes = [IsOwner]
    serializer_class = DiarySerializer
    queryset = Diary.objects.all()
    """
    Diary 작성에 관한 ViewSet
    
    """
    
    def filter_queryset(self,queryset):
        queryset = queryset.filter(user=self.request.user)
        return super().filter_queryset(queryset)
    
    
    def list(self, request, *args, **kwargs):
        """
        diary_list 작성한 일기 목록 조회 API
        
        ---
        
        """
        return super().list(request, *args, **kwargs)
    
    
    def create(self, request, *args, **kwargs):
        """
        diary_create 오늘의 일기를 작성하는 API
        
        ---
        ## 예시 request:
        
                {
                    "user": 1,
                    "title": "일기 제목",
                    "content": "일기 내용",
                    "is_open": true
                }
                
        ## 예시 response:
                201
                {
                    "id": 1,
                    "user": 1,
                    "title": "일기 제목",
                    "content": "일기 내용",
                    "is_open": true,
                    "registered_at": "2024-05-03T10:00:00Z",
                    "last_update_at": "2024-05-03T10:00:00Z"
                }
                400
                {'detail':  '오늘은 이미 일기를 작성했습니다.'}
        
        """
        now = datetime.now()
        date = now.date()        #날짜 설정
        diary_exists = Diary.objects.filter(user=request.user, registered_at__date=date).exists()
        if diary_exists:
            return Response({'detail': '오늘은 이미 일기를 작성했습니다.'}, status=status.HTTP_400_BAD_REQUEST)
        else:
            return super().create(request, *args, **kwargs)
    
    
    def retrieve(self, request, *args, **kwargs):
        '''
        diary_read 작성한 일기의 내용을 조회하는 API
        
        ---
        
        '''
        return super().retrieve(request, *args, **kwargs)
    
    
    def update(self, request, *args, **kwargs):
        '''
        diary_update 이미 작성한 일기의 내용을 수정하는 API
        
        ---
        이떄 이미지, emotion은 결과 삭제됨. 재생성 해야함.
        이미지 생성 : post image/
        emotion&응원문구 분석:  post emotion/
        music의 경우 diary_music으로 재추천 해야 함 : put /diary_music/{일기id}
        
        '''
        instance = self.get_object()
        
        emotions = Emotion.objects.filter(diary=instance)
        images = Image.objects.filter(diary=instance)
        
        emotions.delete()
        images.delete()
        
        return super().update(request, *args, **kwargs)
    
    
    def partial_update(self, request, *args, **kwargs):
        '''
        diary_partial_update 이미 작성한 일기의 내용을 수정하는 API
        
        ---
        이떄 이미지, emotion은 결과 삭제됨. 재생성 해야함.
        이미지 생성 : post image/
        emotion&응원문구 분석:  post emotion/
        music의 경우 diary_music으로 재추천 해야 함 : put /diary_music/{일기id}
        
        '''
        return super().partial_update(request, *args, **kwargs)
    
    
    def destroy(self, request, *args, **kwargs):
        '''
        diary_delete 작성한 일기를 삭제하는 API
        
        ---
        
        '''
        return super().destroy(request, *args, **kwargs)
  • create -> 생성 관련된 함수
  • retrieve -> 조회에 관련된 함수
  • update -> 수정에 관련된 함수
  • destroy -> 삭제에 관련된 함수

models.py

django framework 에서 데이터모델을 만들어주는 역할을 하고 있는 파일이다.
models.py 파일안에 클래스형으로 데이터 모델을 만들어주면 장고가 ORM(object-oriented-mapping)을 통해 데이터베이스에 데이터 모델을 생성해준다.

class Diary(models.Model):
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    music = models.ForeignKey(Music, on_delete=models.SET_NULL, blank=True, null=True)
    
    title = models.CharField(max_length = 30)
    content = models.TextField(blank=True)
    registered_at = models.DateTimeField(auto_now_add=True)
    last_update_at = models.DateTimeField(auto_now=True)
    is_open = models.BooleanField(default=False)
    
    class Meta: 
        managed = True
        db_table = 'diary'
  • title -> CharField = 문자를 담음
  • content -> TextField = 텍스트를 담음
  • registered_at. last_update_at -> DateTimeField = 날짜와 시간을 담음
  • is_open -> BooleanField = 참, 거짓을 담음
  • user, music -> Foreignkey 외래키로 설정

serializers.py

이 python 파일은 app 폴더에 직접 추가해야 한다.

serializers란?

serializer는 직렬화라는 뜻을 가진 단어. django에 저장되어 있는 모델 인스턴스를 REST API에서 사용하는 JSON의 형태로 바꿔주는 것을 말한다.

우리는 그중에서도 가장 간단한 ModelSerializer를 사용하였다.

ModelSerializer와 Serializer의 다른 점

  • 장고 모델에 따라 자동으로 필드 세트를 생성
  • serializer에 대한 unique_together 유효성 검사기 자동 생성
  • .create(), .update()도 default로 포함됨
from rest_framework import serializers
from .models import Diary
from recommend_music.models import Music
from recommend_music.serializers import MusicSerializer
from books.serializers import BookSerializer

class DiarySerializer(serializers.ModelSerializer):
    class Meta:
        model = Diary
        fields = ['id','user','title','content','registered_at','last_update_at', 'is_open']
        
        
class DiaryMusicSerializer(serializers.ModelSerializer):
    music = MusicSerializer(required=False)
    
    class Meta:
        model = Diary
        fields =['id', 'user', 'content', 'music']
        
    def update(self, instance, validated_data):
        music_data = validated_data.pop('music', None)
        instance = super().update(instance, validated_data)
        
        if music_data:
            music, _ = Music.objects.get_or_create(**music_data)
            instance.music = music
        
        return instance

        
class DiaryAdminSerializer(serializers.ModelSerializer):
    music = MusicSerializer(required=False)
    book = BookSerializer(required=False)

    class Meta:
        model = Diary
        fields = '__all__'
  • 보여줄 field를 각자 설정해야한다.

4) Books

Books app = 책에 관한 로직 (원하는 일기를 묶어 책으로 만들어준다)

  1. books이라는 이름의 앱을 생성해준다.
django manage.py startapp books

views.py

from rest_framework import mixins
from rest_framework.generics import get_object_or_404
from rest_framework.viewsets import GenericViewSet
from rest_framework.response import Response
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from rest_framework import status

from .models import *
from diaries.models import Diary
from diaries.serializers import *
from fairy_tairy.permissions import *
from .serializers import *
'''
다른 파일에서 함수들 불러오기
'''
class BookViewSet(GenericViewSet,
                  mixins.ListModelMixin,
                  mixins.CreateModelMixin,
                  mixins.RetrieveModelMixin,
                  mixins.UpdateModelMixin,
                  mixins.DestroyModelMixin):
    """
    일기를 하나로 묶어주는 Book(표지) API 
    """
    permission_classes = [IsOwner]

    serializer_class = BookSerializer
    queryset = Book.objects.all()

    def list(self, request, *args, **kwargs):
        """
        책(표지)의 list를 불러오는 API
        
        ---
        
        """
        return super().list(request, *args, **kwargs)

    def retrieve(self, request, *args, **kwargs):
        """
        book의 id를 통해 책의 표지 정보 조회하는 API
        
        ---
        일기를 하나로 묶어주는 Book(표지) API 
        """
        return super().retrieve(request, *args, **kwargs)
    
    def create(self, request, *args, **kwargs):
        """
        book의 id를 통해 책의 표지를 생성하는 API
        
        ---
        일기를 하나로 묶어주는 Book(표지) API 
        
        """
        return super().create(request, *args, **kwargs)
    
    def partial_update(self, request, *args, **kwargs):
        """
        책의 표지 정보를 업데이트하는 API
        
        ---
        일기를 하나로 묶어주는 Book(표지) API 
        
        """
        return super().partial_update(request, *args, **kwargs)
    
    def update(self, request, *args, **kwargs):
        """
        책의 표지 정보를 업데이트하는 API
        
        ---
        일기를 하나로 묶어주는 Book(표지) API 
        
        """
        return super().update(request, *args, **kwargs)


class PageViewSet(GenericViewSet,
                  mixins.ListModelMixin,
                  mixins.CreateModelMixin,
                  mixins.RetrieveModelMixin,
                  mixins.UpdateModelMixin,
                  mixins.DestroyModelMixin):
    
    permission_classes = [IsOwner]

    serializer_class = PageSerializer
    queryset = Page.objects.all()
    
    def filter_queryset(self,queryset):
        queryset = queryset.filter(diary__user=self.request.user)
        return super().filter_queryset(queryset)
    

    def destroy(self, request, *args, **kwargs):
        
        """
        책과 일기 사이의 연결 끊는 API
        
        ---
        """
        instance = self.get_object()
        instance.delete()  
        return Response(status=status.HTTP_200_OK)
    
    def retrieve(self, request, *args, **kwargs):
        """
        페이지의 ID로 책의 페이지를 조회하는 API
        
        ---
        
        ### id : Page의 ID
        """
        return super().retrieve(request, *args, **kwargs)


    def create(self, request, pk=None):
        """
        일기를 책과 연결함
        
        ---
        (일기,책)이 unigue해야 함
        
        ## 예시 request:
                {
                    "user":1,
                    "book": 1,
                    "diary": 100
                }
        ## 예시 response:
                201
                {
                    "id": 6,
                    "order": 2,
                    "user": 1,
                    "book": 1,
                    "diary": 100
                }
                404 
                
        """
        # 새 페이지를 만들고 요청된 데이터로 초기화.
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        self.perform_create(serializer)

        # 연결할 일기와 책의 정보를 가져옴
        diary_id = request.data.get('diary')
        book_id = request.data.get('book')
        user = request.user
        
        diary = get_object_or_404(Diary, id=diary_id, user=request.user)
        book = get_object_or_404(Book, id=book_id, user=request.user)
        
        page = serializer.save(diary=diary, book=book, user=user)
        # page = serializer.instance
        # last_page = Page.objects.filter(book=book).order_by('-order').first().order
        # page.diary = diary
        # page.book = book
        # # page.order = last_page+1
        # page.save()
        
        return Response(serializer.data, status=status.HTTP_201_CREATED)
  • 이 또한 위 diary app처럼 serializer와 DRF Viewsets을 사용하였다.

models.py

from django.db import models
from django.conf import settings
from diaries.models import Diary
from django.db.models.signals import pre_save, post_delete
from django.dispatch import receiver

# Create your models here.

class Book(models.Model):
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    book_title = models.CharField(max_length = 30)
    author = models.CharField(max_length = 30)
    description = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)
    is_open = models.BooleanField(default=False)
    
    class Meta: 
        managed = True
        db_table = 'book'


class Page(models.Model):
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    book=models.ForeignKey(Book, on_delete=models.CASCADE)
    diary=models.ForeignKey(Diary, on_delete=models.CASCADE)
    order = models.PositiveIntegerField(default=0, blank=False, null=False)
    
    class Meta:
        unique_together = ('book', 'diary')
        ordering = ['order']  # 순서대로 정렬
        managed = True
        db_table='page'
        
    def save(self, *args, **kwargs):
        if not self.order:
            # 새 페이지에 대한 순서를 설정합니다.
            last_page = Page.objects.filter(book=self.book, diary=self.diary).order_by('-order').first()
            if last_page:
                self.order = last_page.order + 1
            else:
                self.order = 1
        super(Page, self).save(*args, **kwargs)

Book 모델

  • user -> 외래키
  • book_title, author -> CharField 작가와 책 제목은 문자로 처리
  • description -> TextField 텍스트로 처리
  • created_at -> DateTimeField 생성 날짜와 시간 처리
  • is_open -> 공개 됐는지 참 거짓 유무로 처리 BooleanField

Page 모델

책-일기 를 ManyToMany(다대다 관례)로 처리하려고 했다. 책 하나에 여러 일기가 들어갈 수 있고 하나의 일기가 여러 책에 포함될 수 있기 때문이다.
허나 ManyToMany는 수정이 불가능해 실무에서는 절대 사용하지 말아야한다는 글을 보고 '중간 테이블 생성' 법으로 이를 해결하기로 했다.

중간 테이블 생성
중간 테이블은 예를 들어 책과 일기가 있을 때 이들을 직접 다대다로 연결하는 것이 아닌 그 중간에 테이블을 만들어주는 것을 의미한다.

책 - 페이지 - 일기
이런 식으로 중간 테이블을 만드는 것이다.

책과 페이지를 다대일. 페이지와 일기를 일대다. 로 만들어서 사실상의 다대다로 만들어주는 것.
예를 들어 Page 모델에 book과 diary 속성에 집중해보자.
book도 diary도 현재 Foreign Key (외래키)로 설정되어 각각 diary와 book 모델에서 불러오고 있다.

만약 1번 책에 1번 일기를 넣고 싶으면 이런식으로 page 테이블에 등록할 수 있다.
현재 page 테이블에는 1번 책에 1번 일기와 100번 일기가 들어있다는 정보가 포함되어 있다.

  • user -> 외래키
  • book, diary -> 모두 외래키
  • order -> PositiveIntergerField

serializers.py

from rest_framework import serializers
from .models import Book, Page

class BookSerializer(serializers.ModelSerializer):
    class Meta:
        model = Book
        fields = '__all__'
        
class PageSerializer(serializers.ModelSerializer):
    class Meta:
        model = Page
        fields = '__all__'

books의 serializer는 field를 all로 간단히 설정했다.

사실상 백엔드만 담당하여 프론트엔드는 적지 않도록 하겠습니다.

0개의 댓글