독서 리뷰 페이지 01

·2023년 10월 6일
0

웹 프로젝트

목록 보기
5/18

내가 맡은 역할

  • 메인페이지
    • 책 추천 카드 배너, 인기 리뷰, 키워드 등을 볼 수 있음
    • 키워드(태그) 클릭 시 해당 태그가 달려있는 책 리스트를 볼 수 있음
    • 우측 상단의 프로필 이미지를 클릭해 마이페이지 / 좋아한 책 / 좋아한 리뷰 / 로그아웃 기능을 이용할 수 있음
    • 메뉴에는 책의 카테고리가 표시되어 있고, 카테고리를 누를 시 해당 카테고리에 해당하는 책 리스트를 볼 수 있음
  • 상세페이지
    • 책 표지, 제목, 작가, 출판사, 평균 별점, 리뷰 수를 상단에서 볼 수 있음
    • 아래에서 구매처와 한 줄 리뷰 확인 가능
    • 책을 쓴 작가가 마음에 들 시 작가 팔로우 가능
  • 태그 검색

모델 구축

우선 erd 모델에 맞게 장고에 모델을 구축해주었다.

book/models.py

from django.db import models
from django.urls import reverse
from taggit.managers import TaggableManager

# Create your models here.


class Author(models.Model):
    name = models.CharField(max_length=100)

    def __str__(self) -> str:
        return self.name


class Category(models.Model):
    name = models.CharField(max_length=100)

    def __str__(self):
        return self.name

    class Meta:
        verbose_name_plural = "Categories"  # 없으면 admin 페이지에 Categorys 라고 표시됨


class Book(models.Model):
    title = models.CharField(max_length=100)
    content = models.TextField()
    cover = models.ImageField(blank=True, upload_to="media/bookCover")
    author = models.ForeignKey(Author, null=True, on_delete=models.SET_NULL)
    category = models.ForeignKey(
        Category, blank=True, null=True, on_delete=models.SET_NULL
    )
    intro = models.TextField()
    tags = TaggableManager()
    likes = models.ManyToManyField("user.User", related_name="liked_books", blank=True)

    def __str__(self) -> str:
        return self.title


num_choices = zip(range(0, 6), range(0, 6))


class Review(models.Model):
    title = models.CharField(max_length=250, default="")
    author = models.ForeignKey("user.User", on_delete=models.CASCADE)
    book = models.ForeignKey(
        Book, on_delete=models.CASCADE
    )  # 역참조이므로 related_at 을 써주지 않아도 review_set 이 디폴트로 있음
    content = models.TextField()
    star = models.IntegerField(blank=True, null=True, choices=num_choices)
    likes = models.ManyToManyField(
        "user.User", related_name="liked_reviews", blank=True
    )
    created_at = models.DateField(auto_now_add=True)
    updated_at = models.DateField(auto_now=True)

카테고리와 작가는 해당 책이 없어도 별개로 존재하므로 책과 별개의 엔티티로 만들어주었다. 처음에 카테고리-책 은 M : N 으로 설정할까 1 : N 으로 설정할까 고민을 많이 했는데, 내가 자주 사용하는 서점 사이트인 리디북스를 보면 카테고리를 세부적으로 설정하기 보다는 주장르로 카테고리를 하나 설정해두고 태그로 세부 장르를 표시해두는 것 같아 그 방식을 채택했다.

user/models.py

from django.db import models
from django.contrib.auth.models import BaseUserManager, AbstractBaseUser
from book.models import Author


class UserManager(BaseUserManager):
    def create_user(self, email, username, nickname, birthday, profile, password=None):
        """
        Creates and saves a User with the given email, date of
        birth and password.
        """

        if not email:
            raise ValueError("Users must have email")

        user = self.model(
            email=self.normalize_email(email),
            username=username,
            nickname=nickname,
            birthday=birthday,
            profile=profile,
        )

        user.set_password(password)
        user.save(using=self._db)
        return user

    def create_superuser(self, username, nickname, email, birthday, password=None):
        """
        Creates and saves a superuser with the given email, date of
        birth and password.
        """
        user = self.create_user(
            email,
            username,
            nickname,
            birthday,
            password=password,
        )
        user.is_admin = True
        user.save(using=self._db)
        return user


class User(AbstractBaseUser):
    email = models.EmailField(
        verbose_name="email address",
        max_length=255,
        unique=True,
    )
    username = models.CharField(max_length=100)

    nickname = models.CharField(max_length=100)

    profile = models.ImageField(blank=True, upload_to="media/userProfile")

    birthday = models.DateField(blank=True)
    join_date = models.DateField(auto_now_add=True)

    follower = models.ManyToManyField(
        Author, symmetrical=False, related_name="followee", blank=True
    )

    is_active = models.BooleanField(default=True)
    is_admin = models.BooleanField(default=False)

    objects = UserManager()

    USERNAME_FIELD = "email"
    REQUIRED_FIELDS = ["username", "nickname", "birthday"]

    def __str__(self):
        return self.email

    def has_perm(self, perm, obj=None):
        "Does the user have a specific permission?"
        # Simplest possible answer: Yes, always
        return True

    def has_module_perms(self, app_label):
        "Does the user have permissions to view the app `app_label`?"
        # Simplest possible answer: Yes, always
        return True

    @property
    def is_staff(self):
        "Is the user a member of staff?"
        # Simplest possible answer: All admins are staff
        return self.is_admin

유저모델은 설정해둔 필드에 맞게 커스터마이징을 조금 해줬다. 이미지필드를 추가해줬으므로 admin.py 도 수정했다.

user/admin.py

from django import forms
from django.contrib import admin
from django.contrib.auth.models import Group
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.contrib.auth.forms import ReadOnlyPasswordHashField
from django.core.exceptions import ValidationError

from .models import User


class UserCreationForm(forms.ModelForm):
    """A form for creating new users. Includes all the required
    fields, plus a repeated password."""

    password1 = forms.CharField(label="Password", widget=forms.PasswordInput)
    password2 = forms.CharField(
        label="Password confirmation", widget=forms.PasswordInput
    )

    profile = forms.ImageField(label="Profile Picture", required=False)

    class Meta:
        model = User
        fields = ["email", "username", "nickname", "birthday", "profile"]

    def clean_password2(self):
        # Check that the two password entries match
        password1 = self.cleaned_data.get("password1")
        password2 = self.cleaned_data.get("password2")
        if password1 and password2 and password1 != password2:
            raise ValidationError("Passwords don't match")
        return password2

    def save(self, commit=True):
        # Save the provided password in hashed format
        user = super().save(commit=False)
        user.set_password(self.cleaned_data["password1"])
        if commit:
            user.save()
        return user


class UserChangeForm(forms.ModelForm):
    """A form for updating users. Includes all the fields on
    the user, but replaces the password field with admin's
    disabled password hash display field.
    """

    password = ReadOnlyPasswordHashField()

    class Meta:
        model = User
        fields = [
            "email",
            "password",
            "username",
            "nickname",
            "birthday",
            "profile",
            "is_active",
            "is_admin",
        ]


class UserAdmin(BaseUserAdmin):
    # The forms to add and change user instances
    form = UserChangeForm
    add_form = UserCreationForm

    # The fields to be used in displaying the User model.
    # These override the definitions on the base UserAdmin
    # that reference specific fields on auth.User.
    list_display = ["username", "email", "nickname", "is_admin"]
    list_filter = ["is_admin"]
    fieldsets = [
        (
            None,
            {
                "fields": [
                    "username",
                    "nickname",
                    "email",
                    "password",
                    "follower",
                ],
            },
        ),
        ("Personal info", {"fields": ["birthday"]}),
        ("Permissions", {"fields": ["is_admin"]}),
    ]
    # add_fieldsets is not a standard ModelAdmin attribute. UserAdmin
    # overrides get_fieldsets to use this attribute when creating a user.
    add_fieldsets = [
        (
            None,
            {
                "classes": ["wide"],
                "fields": [
                    "email",
                    "username",
                    "nickname",
                    "profile",
                    "birthday",
                    "follower",
                    "password1",
                    "password2",
                ],
            },
        ),
    ]
    search_fields = ["email"]
    ordering = ["email"]
    filter_horizontal = []


# Now register the new UserAdmin...
admin.site.register(User, UserAdmin)
# ... and, since we're not using Django's built-in permissions,
# unregister the Group model from admin.
admin.site.unregister(Group)

이미지 필드인 profile 을 받는 폼을 따로 만들어준 뒤 add_fieldsets 에 profile 을 추가했다.

url 설정 및 views.py

설정해둔 API 에 맞춰 urls.py 에 url 과 API 클래스들을 미리 써뒀다.

user/urls.py

from rest_framework_simplejwt.views import (
    TokenObtainPairView,
    TokenRefreshView,
)
from django.urls import path

urlpatterns = [
    path("api/token/", TokenObtainPairView.as_view(), name="token_obtain_pair"),
    path("api/token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
]

book/urls.py

from django.urls import path
from . import views

urlpatterns = [
    path("mainpage/", views.Main().as_view(), name="mainpage"),
    path("book/<int:book_id>/", views.BookDetail.as_view(), name="detail-book"),
    path(
        "book/<int:book_id>/review/", views.ReviewCreate.as_view(), name="review-create"
    ),
    path(
        "book/<int:book_id>/review/<int:review_id>/",
        views.ReviewUpdate.as_view(),
        name="review-update",
    ),
]

이제 views.py 에 클래스를 미리 설정해두면 기본 세팅은 끝이다.

from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.generics import get_object_or_404
from rest_framework import status, permissions
from .models import Book, Review, Author, TaggableManager
from .serializers import (
    BookSerializer,
    ReviewSerializer,
    BookTagSerializer,
    ReviewCreateSerializer,
)
from taggit.serializers import TagListSerializerField, TaggitSerializer
from django.views.generic import TemplateView, ListView
from taggit.models import Tag


# Create your views here.
class Main(APIView):
    def get(self, request):
        # 추천 책
        books = Book.objects.all()
        serializer = BookSerializer(books, many=True)

        # 인기 리뷰
        reviews = Book.objects.all().order_by("likes").values()
        re_serializer = ReviewSerializer(reviews, many=True)

        # 태그
        tags = Tag.objects.all()
        tag_serializer = TaggitSerializer(tags)

        return Response(serializer.data, status=status.HTTP_200_OK)


class BookDetail(APIView):
    def get(self, request, book_id):
        book = get_object_or_404(id=book_id)
        serializer = BookTagSerializer(book)

        return Response(serializer.data, status=status.HTTP_200_OK)


class ReviewCreate(APIView):
    def post(self, request, book_id):
        serializer = ReviewCreateSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save(author=request.user, book_id=book_id)
            return Response(serializer.data, status=status.HTTP_200_OK)
        else:
            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)


class ReviewUpdate(APIView):
    def put(self, request, book_id, review_id):
        pass

    def delete(self, request, book_id, review_id):
        pass


# html로 태그 전달
class TagCloudTV(TemplateView):
    # 모든 태그 추출
    template_name = "taggit/tag_cloud_view.html"


class TaggedObjectLV(ListView):
    # 특정 태그가 있는 책들
    template_name = "taggit/tag_with_book.html"
    model = Book

    def get_queryset(self):
        return Book.objects.filter(tags__name=self.kwargs.get("tags"))

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context["tags"] = self.kwargs["tags"]
        return context
profile
공부 중

0개의 댓글