Skip to content

Commit

Permalink
Merge pull request #6 from minchok125/feature/review
Browse files Browse the repository at this point in the history
Review / Comment 구현
  • Loading branch information
minchok125 authored Jan 9, 2025
2 parents 26e0648 + bc927eb commit 9aa4495
Show file tree
Hide file tree
Showing 42 changed files with 2,166 additions and 11 deletions.
3 changes: 0 additions & 3 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,6 @@ name: deploy to EC2 using docker
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
types: [closed] # PR이 닫힐 때(머지 포함)


env:
Expand Down
3 changes: 2 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ RUN pip install poetry
RUN apt update && apt install -y default-libmysqlclient-dev && apt clean

COPY pyproject.toml poetry.lock ./
RUN poetry install --no-dev
COPY README.md ./README.md
RUN poetry install --no-root

COPY watchapedia ./watchapedia
COPY alembic.ini ./alembic.ini
313 changes: 311 additions & 2 deletions poetry.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ python-jose = "^3.3.0"
passlib = "^1.7.4"
pyjwt = "^2.10.1"
bcrypt = "4.0.1"
selenium = "^4.27.1"
cryptography = "^44.0.0"


[build-system]
Expand Down
8 changes: 7 additions & 1 deletion watchapedia/api.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
from fastapi import APIRouter
from watchapedia.app.user.views import user_router
from watchapedia.app.movie.views import movie_router
from watchapedia.app.review.views import review_router
from watchapedia.app.comment.views import comment_router

api_router = APIRouter()

api_router.include_router(user_router, prefix='/users', tags=['users'])
api_router.include_router(user_router, prefix='/users', tags=['users'])
api_router.include_router(movie_router, prefix='/movies', tags=['movies'])
api_router.include_router(review_router, prefix='/reviews', tags=['reviews'])
api_router.include_router(comment_router, prefix='/comments', tags=['comments'])
13 changes: 13 additions & 0 deletions watchapedia/app/comment/dto/requests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from pydantic import BaseModel
from watchapedia.common.errors import InvalidFieldFormatError
from typing import Annotated
from pydantic.functional_validators import AfterValidator

def validate_content(value: str | None) -> str | None:
# content 필드는 500자 이하여야 함
if value is None or len(value) > 500:
raise InvalidFieldFormatError("content")
return value

class CommentRequest(BaseModel):
content: Annotated[str, AfterValidator(validate_content)]
14 changes: 14 additions & 0 deletions watchapedia/app/comment/dto/responses.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from pydantic import BaseModel
from datetime import datetime
from watchapedia.app.user.models import User
from watchapedia.app.comment.models import Comment

class CommentResponse(BaseModel):
id: int
user_id: int
user_name: str
review_id: int
content: str
likes_count: int
created_at: datetime

9 changes: 9 additions & 0 deletions watchapedia/app/comment/errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from fastapi import HTTPException

class RedundantCommentError(HTTPException):
def __init__(self):
super().__init__(status_code=403, detail="User already made a comment")

class CommentNotFoundError(HTTPException):
def __init__(self):
super().__init__(status_code=404, detail="Comment not found")
34 changes: 34 additions & 0 deletions watchapedia/app/comment/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from sqlalchemy import Integer, String, ForeignKey, Float
from sqlalchemy.orm import relationship, Mapped, mapped_column
from datetime import datetime
from watchapedia.database.common import Base
from sqlalchemy import DateTime

class Comment(Base):
__tablename__ = 'comment'

id: Mapped[int] = mapped_column(Integer, primary_key=True)
content: Mapped[str] = mapped_column(String(500), nullable=False)
likes_count: Mapped[int] = mapped_column(Integer, nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False)

user_id: Mapped[int] = mapped_column(
Integer, ForeignKey("user.id"), nullable=False
)
user: Mapped["User"] = relationship("User", back_populates="comments")

review_id: Mapped[int] = mapped_column(
Integer, ForeignKey("review.id"), nullable=False
)
review: Mapped["Review"] = relationship("Review", back_populates="comments")

class UserLikesComment(Base):
__tablename__ = 'user_likes_comment'

id: Mapped[int] = mapped_column(Integer, primary_key=True)
user_id: Mapped[int] = mapped_column(
Integer, ForeignKey("user.id"), nullable=False
)
comment_id: Mapped[int] = mapped_column(
Integer, ForeignKey("comment.id"), nullable=False
)
77 changes: 77 additions & 0 deletions watchapedia/app/comment/repository.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
from sqlalchemy import select
from sqlalchemy.orm import Session
from fastapi import Depends
from watchapedia.database.connection import get_db_session
from typing import Annotated, Sequence
from datetime import datetime
from watchapedia.app.comment.models import Comment, UserLikesComment
from watchapedia.app.comment.errors import CommentNotFoundError

class CommentRepository():
def __init__(self, session: Annotated[Session, Depends(get_db_session)]) -> None:
self.session = session

def get_comment_by_user_and_review(self, user_id: int, review_id: int) -> Comment | None:
get_comment_query = select(Comment).filter(
(Comment.user_id == user_id)
& (Comment.review_id == review_id)
)

return self.session.scalar(get_comment_query)

def create_comment(self, user_id: int, review_id: int, content: str, created_at) -> Comment:
comment = Comment(
user_id=user_id,
review_id=review_id,
content=content,
likes_count=0,
created_at=created_at
)
self.session.add(comment)
self.session.flush()

comment = self.get_comment_by_user_and_review(user_id, review_id)

return comment

def update_comment(self, comment, content: str) -> Comment:
comment.content = content
self.session.flush()

return comment

def get_comments(self, review_id: int) -> Sequence[Comment]:
comments_list_query = select(Comment).where(Comment.review_id == review_id)
return self.session.scalars(comments_list_query).all()

def get_comment_by_comment_id(self, comment_id: int) -> Comment:
comment = self.session.get(Comment, comment_id)
if comment is None :
raise CommentNotFoundError()

return comment

def like_comment(self, user_id: int, comment: Comment) -> Comment:
get_like_query = select(UserLikesComment).filter(
(UserLikesComment.user_id == user_id)
& (UserLikesComment.comment_id == comment.id)
)
user_likes_comment = self.session.scalar(get_like_query)

if user_likes_comment is None :
user_likes_comment = UserLikesComment(
user_id=user_id,
comment_id=comment.id,
)
self.session.add(user_likes_comment)

comment.likes_count += 1

else :
self.session.delete(user_likes_comment)

comment.likes_count -= 1

self.session.flush()

return comment
63 changes: 63 additions & 0 deletions watchapedia/app/comment/service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
from typing import Annotated
from fastapi import Depends
from watchapedia.common.errors import PermissionDeniedError
from watchapedia.app.review.repository import ReviewRepository
from watchapedia.app.review.errors import ReviewNotFoundError
from watchapedia.app.comment.dto.responses import CommentResponse
from watchapedia.app.comment.repository import CommentRepository
from watchapedia.app.comment.models import Comment
from watchapedia.app.comment.errors import RedundantCommentError, CommentNotFoundError
from datetime import datetime

class CommentService:
def __init__(self,
review_repository: Annotated[ReviewRepository, Depends()],
comment_repository: Annotated[CommentRepository, Depends()]
) -> None:
self.review_repository = review_repository
self.comment_repository = comment_repository

def create_comment(self, user_id: int, review_id: int, content: str) -> CommentResponse:
review = self.review_repository.get_review_by_review_id(review_id)
if review is None :
raise ReviewNotFoundError()

new_comment = self.comment_repository.create_comment(user_id=user_id, review_id=review_id,
content=content, created_at=datetime.now())

return self._process_comment_response(new_comment)

def update_comment(self, user_id: int, comment_id: int, content: str) -> CommentResponse:
comment = self.comment_repository.get_comment_by_comment_id(comment_id)
if not comment.user_id == user_id :
raise PermissionDeniedError()

updated_comment = self.comment_repository.update_comment(comment, content=content)
return self._process_comment_response(updated_comment)

def list_comments(self, review_id: int) -> list[CommentResponse]:
review = self.review_repository.get_review_by_review_id(review_id)
if review is None :
raise ReviewNotFoundError()

comments = self.comment_repository.get_comments(review_id)
return [self._process_comment_response(comment) for comment in comments]

def like_comment(self, user_id: int, comment_id: int) -> CommentResponse :
comment = self.comment_repository.get_comment_by_comment_id(comment_id)
if comment is None :
raise CommentNotFoundError()

updated_comment = self.comment_repository.like_comment(user_id, comment)
return self._process_comment_response(updated_comment)

def _process_comment_response(self, comment: Comment) -> CommentResponse:
return CommentResponse(
id=comment.id,
user_id=comment.user.id,
user_name=comment.user.username,
review_id=comment.review_id,
content=comment.content,
likes_count=comment.likes_count,
created_at=comment.created_at
)
68 changes: 68 additions & 0 deletions watchapedia/app/comment/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
from fastapi import APIRouter, Depends
from fastapi.responses import JSONResponse
from typing import Annotated
from datetime import datetime
from watchapedia.app.user.views import login_with_header
from watchapedia.app.user.models import User
from watchapedia.app.comment.dto.requests import CommentRequest
from watchapedia.app.comment.dto.responses import CommentResponse
from watchapedia.app.comment.models import Comment
from watchapedia.app.comment.service import CommentService
from watchapedia.app.comment.errors import *

comment_router = APIRouter()

@comment_router.post('/{review_id}',
status_code=201,
summary="코멘트 작성",
description="review_id, content를 받아 코멘트를 작성하고 성공 시 username을 포함하여 코멘트를 반환합니다.",
)
def create_comment(
user: Annotated[User, Depends(login_with_header)],
comment_service: Annotated[CommentService, Depends()],
review_id: int,
comment: CommentRequest,
) -> CommentResponse:
commentresponse = comment_service.create_comment(user.id, review_id, comment.content)
return commentresponse

@comment_router.patch('/{comment_id}',
status_code=200,
summary="코멘트 수정",
description="comment_id와 content를 받아 코멘트를 수정하고 반환합니다.",
response_model=CommentResponse
)
def update_comment(
user: Annotated[User, Depends(login_with_header)],
comment_service: Annotated[CommentService, Depends()],
comment_id: int,
comment: CommentRequest,
):
return comment_service.update_comment(
user.id, comment_id, comment.content
)

@comment_router.get('/{review_id}',
status_code=200,
summary="코멘트 출력",
description="review_id를 받아 해당 리뷰에 달린 코멘트들을 반환합니다",
response_model=list[CommentResponse]
)
def get_comments(
review_id: int,
comment_service: Annotated[CommentService, Depends()],
):
return comment_service.list_comments(review_id)

@comment_router.patch('/like/{comment_id}',
status_code=200,
summary="코멘트 추천/취소",
description="comment_id를 받아 추천되어 있지 않으면 추천하고, 추천되어 있으면 취소합니다.",
response_model=CommentResponse
)
def like_comment(
user: Annotated[User, Depends(login_with_header)],
comment_id: int,
comment_service: Annotated[CommentService, Depends()],
):
return comment_service.like_comment(user.id, comment_id)
5 changes: 5 additions & 0 deletions watchapedia/app/country/errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from fastapi import HTTPException

class CountryAlreadyExistsError(HTTPException):
def __init__(self):
super().__init__(status_code=409, detail="Country already exists")
20 changes: 20 additions & 0 deletions watchapedia/app/country/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from sqlalchemy import Integer, String, ForeignKey
from sqlalchemy.orm import relationship, Mapped, mapped_column
from watchapedia.database.common import Base
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from watchapedia.app.movie.models import Movie

class MovieCountry(Base):
__tablename__ = "movie_country"

movie_id: Mapped[int] = mapped_column(Integer, ForeignKey("movie.id"), nullable=False, primary_key=True)
country_id: Mapped[int] = mapped_column(Integer, ForeignKey("country.id"), nullable=False, primary_key=True)

class Country(Base):
__tablename__ = "country"

id: Mapped[int] = mapped_column(Integer, primary_key=True)
name: Mapped[str] = mapped_column(String(50), nullable=False)

movies: Mapped[list["Movie"]] = relationship(secondary="movie_country", back_populates="countries")
32 changes: 32 additions & 0 deletions watchapedia/app/country/repository.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from sqlalchemy import select
from sqlalchemy.orm import Session
from fastapi import Depends
from watchapedia.database.connection import get_db_session
from typing import Annotated
from watchapedia.app.country.models import Country
from watchapedia.app.movie.models import Movie
from watchapedia.app.country.errors import CountryAlreadyExistsError

class CountryRepository():
def __init__(self, session: Annotated[Session, Depends(get_db_session)]) -> None:
self.session = session

def add_country(self, name: str) -> Country:
if self.get_country_by_country_name(name):
raise CountryAlreadyExistsError()
country = Country(name=name)
self.session.add(country)
self.session.flush()
return country

def add_country_with_movie(self, name: str, movie: Movie) -> None:
country = self.get_country_by_country_name(name)
if not country:
country = Country(name=name)
self.session.add(country)
country.movies.append(movie)
self.session.flush()

def get_country_by_country_name(self, name: str) -> Country | None:
get_country_query = select(Country).filter(Country.name == name)
return self.session.scalar(get_country_query)
Loading

0 comments on commit 9aa4495

Please sign in to comment.