-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #6 from minchok125/feature/review
Review / Comment 구현
- Loading branch information
Showing
42 changed files
with
2,166 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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']) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
Oops, something went wrong.