diff --git a/src/main/java/konkuk/thip/comment/adapter/in/web/CommentCommandController.java b/src/main/java/konkuk/thip/comment/adapter/in/web/CommentCommandController.java index 492856503..6f9977727 100644 --- a/src/main/java/konkuk/thip/comment/adapter/in/web/CommentCommandController.java +++ b/src/main/java/konkuk/thip/comment/adapter/in/web/CommentCommandController.java @@ -6,21 +6,19 @@ import jakarta.validation.Valid; import konkuk.thip.comment.adapter.in.web.request.CommentCreateRequest; import konkuk.thip.comment.adapter.in.web.request.CommentIsLikeRequest; +import konkuk.thip.comment.adapter.in.web.response.CommentDeleteResponse; import konkuk.thip.comment.adapter.in.web.response.CommentIdResponse; import konkuk.thip.comment.adapter.in.web.response.CommentIsLikeResponse; import konkuk.thip.comment.application.port.in.CommentCreateUseCase; +import konkuk.thip.comment.application.port.in.CommentDeleteUseCase; import konkuk.thip.comment.application.port.in.CommentLikeUseCase; import konkuk.thip.common.dto.BaseResponse; import konkuk.thip.common.security.annotation.UserId; import konkuk.thip.common.swagger.annotation.ExceptionDescription; import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; -import static konkuk.thip.common.swagger.SwaggerResponseDescription.CHANGE_COMMENT_LIKE_STATE; -import static konkuk.thip.common.swagger.SwaggerResponseDescription.COMMENT_CREATE; +import static konkuk.thip.common.swagger.SwaggerResponseDescription.*; @Tag(name = "Comment Command API", description = "댓글 상태변경 관련 API") @RestController @@ -29,6 +27,7 @@ public class CommentCommandController { private final CommentCreateUseCase commentCreateUseCase; private final CommentLikeUseCase commentLikeUseCase; + private final CommentDeleteUseCase commentDeleteUseCase; /** * 댓글/답글 작성 @@ -63,4 +62,16 @@ public BaseResponse likeComment( return BaseResponse.ok(CommentIsLikeResponse.of(commentLikeUseCase.changeLikeStatusComment(request.toCommand(userId, commentId)))); } + @Operation( + summary = "댓글 삭제", + description = "사용자가 댓글을 삭제합니다." + ) + @ExceptionDescription(COMMENT_DELETE) + @DeleteMapping("/comments/{commentId}") + public BaseResponse deleteComment( + @Parameter(description = "삭제하려는 댓글 ID", example = "1") @PathVariable("commentId") final Long commentId, + @Parameter(hidden = true) @UserId final Long userId) { + return BaseResponse.ok(CommentDeleteResponse.of(commentDeleteUseCase.deleteComment(commentId,userId))); + } + } diff --git a/src/main/java/konkuk/thip/comment/adapter/in/web/response/CommentDeleteResponse.java b/src/main/java/konkuk/thip/comment/adapter/in/web/response/CommentDeleteResponse.java new file mode 100644 index 000000000..440f3f76d --- /dev/null +++ b/src/main/java/konkuk/thip/comment/adapter/in/web/response/CommentDeleteResponse.java @@ -0,0 +1,7 @@ +package konkuk.thip.comment.adapter.in.web.response; + +public record CommentDeleteResponse(Long postId) { + public static CommentDeleteResponse of(Long postId) { + return new CommentDeleteResponse(postId); + } +} diff --git a/src/main/java/konkuk/thip/comment/adapter/out/jpa/CommentJpaEntity.java b/src/main/java/konkuk/thip/comment/adapter/out/jpa/CommentJpaEntity.java index 997285e0d..513e79b1f 100644 --- a/src/main/java/konkuk/thip/comment/adapter/out/jpa/CommentJpaEntity.java +++ b/src/main/java/konkuk/thip/comment/adapter/out/jpa/CommentJpaEntity.java @@ -8,6 +8,7 @@ import konkuk.thip.post.adapter.out.jpa.PostJpaEntity; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; import lombok.*; +import org.hibernate.annotations.SQLDelete; @Entity @Table(name = "comments") @@ -15,6 +16,7 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor @Builder +@SQLDelete(sql = "UPDATE comments SET status = 'INACTIVE' WHERE comment_id = ?") public class CommentJpaEntity extends BaseJpaEntity { @Id @@ -53,6 +55,7 @@ public class CommentJpaEntity extends BaseJpaEntity { public CommentJpaEntity updateFrom(Comment comment) { this.reportCount = comment.getReportCount(); this.likeCount = comment.getLikeCount(); + this.status = comment.getStatus(); return this; } diff --git a/src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentCommandPersistenceAdapter.java b/src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentCommandPersistenceAdapter.java index d19de9d45..24861fe43 100644 --- a/src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentCommandPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentCommandPersistenceAdapter.java @@ -18,6 +18,7 @@ import java.util.Optional; +import static konkuk.thip.common.entity.StatusType.ACTIVE; import static konkuk.thip.common.exception.code.ErrorCode.*; @Repository @@ -67,13 +68,12 @@ private PostJpaEntity findPostJpaEntity(PostType postType, Long postId) { @Override public Optional findById(Long id) { - return commentJpaRepository.findById(id) + return commentJpaRepository.findByCommentIdAndStatus(id, ACTIVE) .map(commentMapper::toDomainEntity); } @Override public void update(Comment comment) { - CommentJpaEntity commentJpaEntity = commentJpaRepository.findById(comment.getId()).orElseThrow( () -> new EntityNotFoundException(COMMENT_NOT_FOUND) ); @@ -81,4 +81,12 @@ public void update(Comment comment) { commentJpaRepository.save(commentJpaEntity.updateFrom(comment)); } + @Override + public void delete(Comment comment) { + CommentJpaEntity commentJpaEntity = commentJpaRepository.findById(comment.getId()).orElseThrow( + () -> new EntityNotFoundException(COMMENT_NOT_FOUND) + ); + commentJpaRepository.delete(commentJpaEntity); + } + } diff --git a/src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentLikeCommandPersistenceAdapter.java b/src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentLikeCommandPersistenceAdapter.java index 528b3aa4d..138e08c98 100644 --- a/src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentLikeCommandPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentLikeCommandPersistenceAdapter.java @@ -41,4 +41,10 @@ public void save(Long userId, Long commentId) { public void delete(Long userId, Long commentId) { commentLikeJpaRepository.deleteByUserIdAndCommentId(userId, commentId); } + + @Override + public void deleteAllByCommentId(Long commentId) { + commentLikeJpaRepository.deleteAllByCommentId(commentId); + } + } diff --git a/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentJpaRepository.java b/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentJpaRepository.java index 405e7e099..4e08628f9 100644 --- a/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentJpaRepository.java +++ b/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentJpaRepository.java @@ -1,8 +1,11 @@ package konkuk.thip.comment.adapter.out.persistence.repository; import konkuk.thip.comment.adapter.out.jpa.CommentJpaEntity; +import konkuk.thip.common.entity.StatusType; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + public interface CommentJpaRepository extends JpaRepository { - int countByPostJpaEntity_PostId(Long postId); + Optional findByCommentIdAndStatus(Long commentId, StatusType status); } diff --git a/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentLikeJpaRepository.java b/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentLikeJpaRepository.java index 1ec42c080..a79283150 100644 --- a/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentLikeJpaRepository.java +++ b/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentLikeJpaRepository.java @@ -9,13 +9,22 @@ import java.util.List; public interface CommentLikeJpaRepository extends JpaRepository { - @Query(value = "SELECT * FROM comment_likes WHERE user_id = :userId", nativeQuery = true) - List findAllByUserId(Long userId); + + @Query("SELECT cl FROM CommentLikeJpaEntity cl WHERE cl.userJpaEntity.userId = :userId") + List findAllByUserId(@Param("userId") Long userId); + @Modifying - @Query(value = "DELETE FROM comment_likes WHERE user_id = :userId AND comment_id = :commentId", nativeQuery = true) + @Query("DELETE FROM CommentLikeJpaEntity cl WHERE cl.userJpaEntity.userId = :userId AND cl.commentJpaEntity.commentId = :commentId") void deleteByUserIdAndCommentId(@Param("userId") Long userId, @Param("commentId") Long commentId); - @Query(value = "SELECT EXISTS(SELECT 1 FROM comment_likes WHERE user_id = :userId AND comment_id = :commentId)", nativeQuery = true) + @Query("SELECT CASE WHEN COUNT(cl) > 0 THEN true ELSE false END " + + "FROM CommentLikeJpaEntity cl " + + "WHERE cl.userJpaEntity.userId = :userId AND cl.commentJpaEntity.commentId = :commentId") boolean existsByUserIdAndCommentId(@Param("userId") Long userId, @Param("commentId") Long commentId); + + @Modifying + @Query("DELETE FROM CommentLikeJpaEntity cl WHERE cl.commentJpaEntity.commentId = :commentId") + void deleteAllByCommentId(@Param("commentId") Long commentId); + } \ No newline at end of file diff --git a/src/main/java/konkuk/thip/comment/application/port/in/CommentDeleteUseCase.java b/src/main/java/konkuk/thip/comment/application/port/in/CommentDeleteUseCase.java new file mode 100644 index 000000000..ab110db7c --- /dev/null +++ b/src/main/java/konkuk/thip/comment/application/port/in/CommentDeleteUseCase.java @@ -0,0 +1,6 @@ +package konkuk.thip.comment.application.port.in; + + +public interface CommentDeleteUseCase { + Long deleteComment(Long commentId, Long userId); +} diff --git a/src/main/java/konkuk/thip/comment/application/port/out/CommentCommandPort.java b/src/main/java/konkuk/thip/comment/application/port/out/CommentCommandPort.java index 22bff6d06..ffe41f1ac 100644 --- a/src/main/java/konkuk/thip/comment/application/port/out/CommentCommandPort.java +++ b/src/main/java/konkuk/thip/comment/application/port/out/CommentCommandPort.java @@ -21,4 +21,6 @@ default Comment getByIdOrThrow(Long id) { void update(Comment comment); + void delete(Comment comment); + } diff --git a/src/main/java/konkuk/thip/comment/application/port/out/CommentLikeCommandPort.java b/src/main/java/konkuk/thip/comment/application/port/out/CommentLikeCommandPort.java index bc8b2800e..d776cef26 100644 --- a/src/main/java/konkuk/thip/comment/application/port/out/CommentLikeCommandPort.java +++ b/src/main/java/konkuk/thip/comment/application/port/out/CommentLikeCommandPort.java @@ -4,4 +4,5 @@ public interface CommentLikeCommandPort { void save(Long userId, Long commentId); void delete(Long userId, Long commentId); + void deleteAllByCommentId(Long commentId); } diff --git a/src/main/java/konkuk/thip/comment/application/service/CommentCreateService.java b/src/main/java/konkuk/thip/comment/application/service/CommentCreateService.java index c15ab1345..f779766a7 100644 --- a/src/main/java/konkuk/thip/comment/application/service/CommentCreateService.java +++ b/src/main/java/konkuk/thip/comment/application/service/CommentCreateService.java @@ -7,14 +7,8 @@ import konkuk.thip.comment.domain.Comment; import konkuk.thip.common.exception.InvalidStateException; import konkuk.thip.common.post.CommentCountUpdatable; -import konkuk.thip.common.post.service.PostQueryService; -import konkuk.thip.feed.application.port.out.FeedCommandPort; -import konkuk.thip.feed.domain.Feed; +import konkuk.thip.common.post.service.PostHandler; import konkuk.thip.common.post.PostType; -import konkuk.thip.record.application.port.out.RecordCommandPort; -import konkuk.thip.record.domain.Record; -import konkuk.thip.vote.application.port.out.VoteCommandPort; -import konkuk.thip.vote.domain.Vote; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -27,11 +21,8 @@ public class CommentCreateService implements CommentCreateUseCase { private final CommentCommandPort commentCommandPort; - private final FeedCommandPort feedCommandPort; - private final RecordCommandPort recordCommandPort; - private final VoteCommandPort voteCommandPort; - private final PostQueryService postQueryService; + private final PostHandler postHandler; private final CommentAuthorizationValidator commentAuthorizationValidator; @Override @@ -43,7 +34,7 @@ public Long createComment(CommentCreateCommand command) { PostType type = PostType.from(command.postType()); // 2. 게시물 타입에 맞게 조회 - CommentCountUpdatable post = postQueryService.findPost(type, command.postId()); + CommentCountUpdatable post = postHandler.findPost(type, command.postId()); // 2-1. 게시글 타입에 따른 댓글 생성 권한 검증 commentAuthorizationValidator.validateUserCanAccessPostForComment(type, post, command.userId()); @@ -53,11 +44,13 @@ public Long createComment(CommentCreateCommand command) { // 3. 댓글 생성 Long commentId = createCommentDomain(command); + //TODO 게시물의 댓글 수 증가/감소 동시성 제어 로직 추가해야됨 + // 4. 게시글 댓글 수 증가 // 4-1. 도메인 게시물 댓글 수 증가 post.increaseCommentCount(); // 4-2 Jpa엔티티 게시물 댓글 수 증가 - updatePost(type, post); + postHandler.updatePost(type, post); return commentId; } @@ -85,12 +78,4 @@ private Long createCommentDomain(CommentCreateCommand command) { return commentCommandPort.save(comment); } - private void updatePost(PostType type, CommentCountUpdatable post) { - switch (type) { - case FEED -> feedCommandPort.update((Feed) post); - case RECORD -> recordCommandPort.update((Record) post); - case VOTE -> voteCommandPort.updateVote((Vote) post); - } - } - } diff --git a/src/main/java/konkuk/thip/comment/application/service/CommentDeleteService.java b/src/main/java/konkuk/thip/comment/application/service/CommentDeleteService.java new file mode 100644 index 000000000..c85e522e7 --- /dev/null +++ b/src/main/java/konkuk/thip/comment/application/service/CommentDeleteService.java @@ -0,0 +1,53 @@ +package konkuk.thip.comment.application.service; + +import jakarta.transaction.Transactional; +import konkuk.thip.comment.application.port.in.CommentDeleteUseCase; +import konkuk.thip.comment.application.port.out.CommentCommandPort; +import konkuk.thip.comment.application.port.out.CommentLikeCommandPort; +import konkuk.thip.comment.application.service.validator.CommentAuthorizationValidator; +import konkuk.thip.comment.domain.Comment; +import konkuk.thip.common.post.CommentCountUpdatable; +import konkuk.thip.common.post.service.PostHandler; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class CommentDeleteService implements CommentDeleteUseCase { + + private final CommentCommandPort commentCommandPort; + private final CommentLikeCommandPort commentLikeCommandPort; + + private final PostHandler postHandler; + private final CommentAuthorizationValidator commentAuthorizationValidator; + + @Override + @Transactional + public Long deleteComment(Long commentId, Long userId) { + + // 1. 댓글 조회 및 권한 검증 + Comment comment = commentCommandPort.getByIdOrThrow(commentId); + // 1-1. 게시글 타입에 따른 댓글 삭제 권한 검증 + CommentCountUpdatable post = postHandler.findPost(comment.getPostType(), comment.getTargetPostId()); + commentAuthorizationValidator.validateUserCanAccessPostForComment(comment.getPostType(), post, userId); + + // 2. 댓글 삭제 권한 검증 및 소프트 딜리트 + comment.validateDeletable(userId); + // 2-1. 댓글 좋아요 삭제 + commentLikeCommandPort.deleteAllByCommentId(commentId); + + // 3. 댓글 삭제 + commentCommandPort.delete(comment); + + //TODO 게시물의 댓글 수 증가/감소 동시성 제어 로직 추가해야됨 + + // 4. 게시글 댓글 수 감소 + // 4-1. 도메인 게시물 댓글 수 감소 + post.decreaseCommentCount(); + // 4-2 Jpa엔티티 게시물 댓글 수 감소 + postHandler.updatePost(comment.getPostType(), post); + + return post.getId(); + } + +} diff --git a/src/main/java/konkuk/thip/comment/application/service/CommentLikeService.java b/src/main/java/konkuk/thip/comment/application/service/CommentLikeService.java index 69533ce9c..cf601a84f 100644 --- a/src/main/java/konkuk/thip/comment/application/service/CommentLikeService.java +++ b/src/main/java/konkuk/thip/comment/application/service/CommentLikeService.java @@ -10,7 +10,7 @@ import konkuk.thip.comment.application.service.validator.CommentAuthorizationValidator; import konkuk.thip.comment.domain.Comment; import konkuk.thip.common.post.CommentCountUpdatable; -import konkuk.thip.common.post.service.PostQueryService; +import konkuk.thip.common.post.service.PostHandler; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -22,7 +22,7 @@ public class CommentLikeService implements CommentLikeUseCase { private final CommentLikeQueryPort commentLikeQueryPort; private final CommentLikeCommandPort commentLikeCommandPort; - private final PostQueryService postQueryService; + private final PostHandler postHandler; private final CommentAuthorizationValidator commentAuthorizationValidator; @Override @@ -32,7 +32,7 @@ public CommentIsLikeResult changeLikeStatusComment(CommentIsLikeCommand command) // 1. 댓글 조회 및 검증 (존재 여부) Comment comment = commentCommandPort.getByIdOrThrow(command.commentId()); // 1-1. 게시글 타입에 따른 댓글 좋아요 권한 검증 - CommentCountUpdatable post = postQueryService.findPost(comment.getPostType(), comment.getTargetPostId()); + CommentCountUpdatable post = postHandler.findPost(comment.getPostType(), comment.getTargetPostId()); commentAuthorizationValidator.validateUserCanAccessPostForComment(comment.getPostType(), post, command.userId()); // 2. 유저가 해당 댓글에 대해 좋아요 했는지 조회 diff --git a/src/main/java/konkuk/thip/comment/domain/Comment.java b/src/main/java/konkuk/thip/comment/domain/Comment.java index 865b73292..3aeb48ef9 100644 --- a/src/main/java/konkuk/thip/comment/domain/Comment.java +++ b/src/main/java/konkuk/thip/comment/domain/Comment.java @@ -141,4 +141,15 @@ public void validateCanUnlike(boolean alreadyLiked) { throw new InvalidStateException(COMMENT_NOT_LIKED_CANNOT_CANCEL); } } + + private boolean validateCreator(Long userId) { + return this.creatorId.equals(userId); + } + + public void validateDeletable(Long userId) { + if (!validateCreator(userId)) { + throw new InvalidStateException(COMMENT_DELETE_FORBIDDEN); + } + } + } diff --git a/src/main/java/konkuk/thip/common/entity/BaseJpaEntity.java b/src/main/java/konkuk/thip/common/entity/BaseJpaEntity.java index b1f619ef1..93ec51963 100644 --- a/src/main/java/konkuk/thip/common/entity/BaseJpaEntity.java +++ b/src/main/java/konkuk/thip/common/entity/BaseJpaEntity.java @@ -34,5 +34,5 @@ public abstract class BaseJpaEntity { @Enumerated(EnumType.STRING) @Column(nullable = false) - private StatusType status = StatusType.ACTIVE; + protected StatusType status = StatusType.ACTIVE; } diff --git a/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java b/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java index 5abd3ee2a..029e85f65 100644 --- a/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java +++ b/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java @@ -175,6 +175,9 @@ public enum ErrorCode implements ResponseCode { COMMENT_LIKE_COUNT_UNDERFLOW(HttpStatus.BAD_REQUEST, 190002, "좋아요 수는 0 이하로 감소할 수 없습니다."), COMMENT_ALREADY_LIKED(HttpStatus.BAD_REQUEST, 190003, "사용자가 이미 좋아요한 댓글입니다."), COMMENT_NOT_LIKED_CANNOT_CANCEL(HttpStatus.BAD_REQUEST, 190004, "사용자가 좋아요하지 않은 댓글은 좋아요 취소 할 수 없습니다."), + COMMENT_DELETE_FORBIDDEN(HttpStatus.FORBIDDEN, 190005, "댓글 삭제 권한이 없습니다."), + COMMENT_COUNT_UNDERFLOW(HttpStatus.BAD_REQUEST, 190007, "댓글 수는 0 이하로 감소할 수 없습니다."), + ; private final HttpStatus httpStatus; diff --git a/src/main/java/konkuk/thip/common/post/CommentCountUpdatable.java b/src/main/java/konkuk/thip/common/post/CommentCountUpdatable.java index 3ac397005..accccc90e 100644 --- a/src/main/java/konkuk/thip/common/post/CommentCountUpdatable.java +++ b/src/main/java/konkuk/thip/common/post/CommentCountUpdatable.java @@ -2,4 +2,6 @@ public interface CommentCountUpdatable { void increaseCommentCount(); + void decreaseCommentCount(); + Long getId(); } \ No newline at end of file diff --git a/src/main/java/konkuk/thip/common/post/service/PostQueryService.java b/src/main/java/konkuk/thip/common/post/service/PostHandler.java similarity index 62% rename from src/main/java/konkuk/thip/common/post/service/PostQueryService.java rename to src/main/java/konkuk/thip/common/post/service/PostHandler.java index 758c08c1e..06428b418 100644 --- a/src/main/java/konkuk/thip/common/post/service/PostQueryService.java +++ b/src/main/java/konkuk/thip/common/post/service/PostHandler.java @@ -1,16 +1,19 @@ package konkuk.thip.common.post.service; +import konkuk.thip.common.annotation.HelperService; import konkuk.thip.common.post.CommentCountUpdatable; import konkuk.thip.common.post.PostType; import konkuk.thip.feed.application.port.out.FeedCommandPort; +import konkuk.thip.feed.domain.Feed; import konkuk.thip.record.application.port.out.RecordCommandPort; +import konkuk.thip.record.domain.Record; import konkuk.thip.vote.application.port.out.VoteCommandPort; +import konkuk.thip.vote.domain.Vote; import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -@Service +@HelperService @RequiredArgsConstructor -public class PostQueryService { +public class PostHandler { private final FeedCommandPort feedCommandPort; private final RecordCommandPort recordCommandPort; @@ -23,4 +26,12 @@ public CommentCountUpdatable findPost(PostType type, Long postId) { case VOTE -> voteCommandPort.getByIdOrThrow(postId); }; } + + public void updatePost(PostType type, CommentCountUpdatable post) { + switch (type) { + case FEED -> feedCommandPort.update((Feed) post); + case RECORD -> recordCommandPort.update((Record) post); + case VOTE -> voteCommandPort.updateVote((Vote) post); + } + } } diff --git a/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java b/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java index 6b311ff82..1cdb32032 100644 --- a/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java +++ b/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java @@ -190,7 +190,20 @@ public enum SwaggerResponseDescription { VOTE_NOT_FOUND, COMMENT_ALREADY_LIKED, COMMENT_NOT_LIKED_CANNOT_CANCEL, - COMMENT_LIKE_COUNT_UNDERFLOW + COMMENT_LIKE_COUNT_UNDERFLOW, + FEED_ACCESS_FORBIDDEN, + ROOM_ACCESS_FORBIDDEN + ))), + COMMENT_DELETE(new LinkedHashSet<>(Set.of( + USER_NOT_FOUND, + COMMENT_NOT_FOUND, + FEED_NOT_FOUND, + RECORD_NOT_FOUND, + VOTE_NOT_FOUND, + COMMENT_DELETE_FORBIDDEN, + COMMENT_COUNT_UNDERFLOW, + FEED_ACCESS_FORBIDDEN, + ROOM_ACCESS_FORBIDDEN ))), // Book diff --git a/src/main/java/konkuk/thip/feed/adapter/in/web/FeedCommandController.java b/src/main/java/konkuk/thip/feed/adapter/in/web/FeedCommandController.java index 0513d0eae..70970ec72 100644 --- a/src/main/java/konkuk/thip/feed/adapter/in/web/FeedCommandController.java +++ b/src/main/java/konkuk/thip/feed/adapter/in/web/FeedCommandController.java @@ -46,8 +46,9 @@ public BaseResponse createFeed( } @Operation( - summary = "피드 수정 (책 빼고 변경가능)", - description = "사용자가 피드를 수정합니다." + summary = "피드 수정", + description = "사용자가 피드를 수정합니다. 책을 제외하고 모든 피드의 정보를 수정가능합니다.\n" + + "이미지는 삭제만 가능하며, 태그,이미지의 경우 수정 시 변경된 값 즉, DB에 존재해야하는 값들을 보내주시면 됩니다." ) @ExceptionDescription(FEED_UPDATE) @PatchMapping("/feeds/{feedId}") diff --git a/src/main/java/konkuk/thip/feed/adapter/in/web/request/FeedUpdateRequest.java b/src/main/java/konkuk/thip/feed/adapter/in/web/request/FeedUpdateRequest.java index d18433731..2a8310f15 100644 --- a/src/main/java/konkuk/thip/feed/adapter/in/web/request/FeedUpdateRequest.java +++ b/src/main/java/konkuk/thip/feed/adapter/in/web/request/FeedUpdateRequest.java @@ -1,17 +1,22 @@ package konkuk.thip.feed.adapter.in.web.request; +import io.swagger.v3.oas.annotations.media.Schema; import konkuk.thip.feed.application.port.in.dto.FeedUpdateCommand; import java.util.List; public record FeedUpdateRequest( + @Schema(description = "수정한 피드 내용", example = "이 책은 정말 좋습니다!") String contentBody, + @Schema(description = "수정한 방 공개 설정 여부 (true: 공개, false: 비공개)", example = "true") Boolean isPublic, + @Schema(description = "수정된 피드에 남아있는 태그들", example = "[\"한국소설\", \"외국소설\", \"시\"]") List tagList, + @Schema(description = "수정된 피드에 남아있는 이미지 URL들", example = "[\"https://img.domain.com/1.jpg\", \"https://img.domain.com/2.jpg\"]") List remainImageUrls ) { public FeedUpdateCommand toCommand(Long userId, Long feedId) { @@ -24,4 +29,4 @@ public FeedUpdateCommand toCommand(Long userId, Long feedId) { feedId ); } -} +} \ No newline at end of file diff --git a/src/main/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntity.java b/src/main/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntity.java index cd184408e..648ec3868 100644 --- a/src/main/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntity.java +++ b/src/main/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntity.java @@ -1,6 +1,7 @@ package konkuk.thip.feed.adapter.out.jpa; +import com.google.common.annotations.VisibleForTesting; import jakarta.persistence.*; import konkuk.thip.book.adapter.out.jpa.BookJpaEntity; import konkuk.thip.feed.domain.Feed; @@ -49,4 +50,10 @@ public void updateFrom(Feed feed) { this.likeCount = feed.getLikeCount(); this.commentCount = feed.getCommentCount(); } + + @VisibleForTesting + public void updateCommentCount(int commentCount) { + this.commentCount = commentCount; + } + } \ No newline at end of file diff --git a/src/main/java/konkuk/thip/feed/domain/Feed.java b/src/main/java/konkuk/thip/feed/domain/Feed.java index 943eeddfb..f828bb062 100644 --- a/src/main/java/konkuk/thip/feed/domain/Feed.java +++ b/src/main/java/konkuk/thip/feed/domain/Feed.java @@ -116,7 +116,7 @@ public void validateCreateComment(Long userId){ private void validateCreator(Long userId) { if (!this.creatorId.equals(userId)) { - throw new InvalidStateException(FEED_ACCESS_FORBIDDEN); + throw new InvalidStateException(FEED_ACCESS_FORBIDDEN, new IllegalArgumentException("피드 작성자만 피드를 수정할 수 있습니다.")); } } @@ -161,4 +161,16 @@ public void increaseCommentCount() { commentCount++; } + @Override + public void decreaseCommentCount() { + checkCommentCountNotUnderflow(); + commentCount--; + } + + private void checkCommentCountNotUnderflow() { + if (commentCount <= 0) { + throw new InvalidStateException(COMMENT_COUNT_UNDERFLOW); + } + } + } diff --git a/src/main/java/konkuk/thip/record/domain/Record.java b/src/main/java/konkuk/thip/record/domain/Record.java index f014765dc..fde6543b7 100644 --- a/src/main/java/konkuk/thip/record/domain/Record.java +++ b/src/main/java/konkuk/thip/record/domain/Record.java @@ -8,8 +8,7 @@ import lombok.Getter; import lombok.experimental.SuperBuilder; -import static konkuk.thip.common.exception.code.ErrorCode.INVALID_RECORD_PAGE_RANGE; -import static konkuk.thip.common.exception.code.ErrorCode.RECORD_CANNOT_BE_OVERVIEW; +import static konkuk.thip.common.exception.code.ErrorCode.*; @Getter @SuperBuilder @@ -79,4 +78,16 @@ public void validatePage(int totalPageCount) { public void increaseCommentCount() { commentCount++; } + + @Override + public void decreaseCommentCount() { + checkCommentCountNotUnderflow(); + commentCount--; + } + + private void checkCommentCountNotUnderflow() { + if (commentCount <= 0) { + throw new InvalidStateException(COMMENT_COUNT_UNDERFLOW); + } + } } diff --git a/src/main/java/konkuk/thip/vote/domain/Vote.java b/src/main/java/konkuk/thip/vote/domain/Vote.java index 700f94704..fd56781a0 100644 --- a/src/main/java/konkuk/thip/vote/domain/Vote.java +++ b/src/main/java/konkuk/thip/vote/domain/Vote.java @@ -8,8 +8,7 @@ import lombok.Getter; import lombok.experimental.SuperBuilder; -import static konkuk.thip.common.exception.code.ErrorCode.INVALID_VOTE_PAGE_RANGE; -import static konkuk.thip.common.exception.code.ErrorCode.VOTE_CANNOT_BE_OVERVIEW; +import static konkuk.thip.common.exception.code.ErrorCode.*; @Getter @SuperBuilder @@ -73,4 +72,16 @@ public void validatePage(int totalPageCount) { public void increaseCommentCount() { commentCount++; } + + @Override + public void decreaseCommentCount() { + checkCommentCountNotUnderflow(); + commentCount--; + } + + private void checkCommentCountNotUnderflow() { + if (commentCount <= 0) { + throw new InvalidStateException(COMMENT_COUNT_UNDERFLOW); + } + } } diff --git a/src/test/java/konkuk/thip/comment/adapter/in/web/CommentControllerTest.java b/src/test/java/konkuk/thip/comment/adapter/in/web/CommentCreateControllerTest.java similarity index 79% rename from src/test/java/konkuk/thip/comment/adapter/in/web/CommentControllerTest.java rename to src/test/java/konkuk/thip/comment/adapter/in/web/CommentCreateControllerTest.java index 027d11d13..22bd3bb63 100644 --- a/src/test/java/konkuk/thip/comment/adapter/in/web/CommentControllerTest.java +++ b/src/test/java/konkuk/thip/comment/adapter/in/web/CommentCreateControllerTest.java @@ -3,7 +3,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import konkuk.thip.book.adapter.out.jpa.BookJpaEntity; import konkuk.thip.book.adapter.out.persistence.repository.BookJpaRepository; -import konkuk.thip.comment.adapter.out.jpa.CommentJpaEntity; import konkuk.thip.comment.adapter.out.persistence.repository.CommentJpaRepository; import konkuk.thip.common.util.TestEntityFactory; import konkuk.thip.feed.adapter.out.jpa.FeedJpaEntity; @@ -39,7 +38,6 @@ import java.util.Map; import static konkuk.thip.common.exception.code.ErrorCode.*; -import static konkuk.thip.common.post.PostType.FEED; import static org.hamcrest.Matchers.containsString; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; @@ -51,7 +49,7 @@ @Transactional @DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) @DisplayName("[단위] 댓글 생성 api controller 단위 테스트") -class CommentControllerTest { +class CommentCreateControllerTest { @Autowired private MockMvc mockMvc; @@ -217,56 +215,5 @@ void voteNotFound() throws Exception { .andExpect(jsonPath("$.message", containsString("존재하지 않는 VOTE 입니다."))); } - @Test - @DisplayName("답글인데 parentId가 null일 경우 400 반환") - void replyWithoutParentId() throws Exception { - Map req = buildValidRequest(); - req.put("isReplyRequest", true); - req.put("parentId", null); // 필수인데 없음 - assertBadCommentCreateRequest(req, "답글 작성 시 parentId는 필수입니다."); - } - - @Test - @DisplayName("일반 댓글인데 parentId가 존재할 경우 400 반환") - void rootCommentWithParentId() throws Exception { - Map req = buildValidRequest(); - req.put("isReplyRequest", false); - req.put("parentId", 1L); // 있으면 안 됨 - assertBadCommentCreateRequest(req, "일반 댓글에는 parentId가 없어야 합니다."); - } - - @Test - @DisplayName("parentId가 존재하지만 댓글이 실제 존재하지 않을 때 400 반환") - void replyToNonExistentParent() throws Exception { - Map req = buildValidRequest(); - req.put("isReplyRequest", true); - req.put("parentId", 99999L); // 존재하지 않는 parent - assertBadCommentCreateRequest(req, "parentId에 해당하는 부모 댓글이 존재해야 합니다."); - } - - @Test - @DisplayName("댓글과 부모 댓글의 게시글이 일치하지 않을 경우 400 반환") - void parentPostMismatch() throws Exception { - - // 1. 부모 댓글을 FEED에 작성 - CommentJpaEntity parentComment = commentJpaRepository.save( - TestEntityFactory.createComment(feed, user, FEED) - ); - - // 2. 답글 요청은 RECORD에 대해 요청 - Map req = new HashMap<>(); - req.put("content", "게시글 불일치"); - req.put("isReplyRequest", true); - req.put("parentId", parentComment.getCommentId()); - req.put("postType", "record"); - - mockMvc.perform(post("/comments/{postId}", record.getPostId()) - .requestAttr("userId", user.getUserId()) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsBytes(req))) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.code").value(INVALID_COMMENT_CREATE.getCode())) - .andExpect(jsonPath("$.message", containsString("댓글과 부모 댓글의 게시글이 일치하지 않습니다."))); - } } } diff --git a/src/test/java/konkuk/thip/comment/adapter/in/web/CommentDeleteAPITest.java b/src/test/java/konkuk/thip/comment/adapter/in/web/CommentDeleteAPITest.java new file mode 100644 index 000000000..358b92b01 --- /dev/null +++ b/src/test/java/konkuk/thip/comment/adapter/in/web/CommentDeleteAPITest.java @@ -0,0 +1,172 @@ +package konkuk.thip.comment.adapter.in.web; + +import konkuk.thip.book.adapter.out.jpa.BookJpaEntity; +import konkuk.thip.book.adapter.out.persistence.repository.BookJpaRepository; +import konkuk.thip.comment.adapter.out.jpa.CommentJpaEntity; +import konkuk.thip.comment.adapter.out.persistence.repository.CommentJpaRepository; +import konkuk.thip.comment.adapter.out.persistence.repository.CommentLikeJpaRepository; +import konkuk.thip.common.util.TestEntityFactory; +import konkuk.thip.feed.adapter.out.jpa.FeedJpaEntity; +import konkuk.thip.feed.adapter.out.persistence.repository.FeedJpaRepository; +import konkuk.thip.record.adapter.out.jpa.RecordJpaEntity; +import konkuk.thip.record.adapter.out.persistence.repository.RecordJpaRepository; +import konkuk.thip.room.adapter.out.jpa.CategoryJpaEntity; +import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity; +import konkuk.thip.room.adapter.out.jpa.RoomParticipantRole; +import konkuk.thip.room.adapter.out.persistence.repository.RoomJpaRepository; +import konkuk.thip.room.adapter.out.persistence.repository.category.CategoryJpaRepository; +import konkuk.thip.room.adapter.out.persistence.repository.roomparticipant.RoomParticipantJpaRepository; +import konkuk.thip.user.adapter.out.jpa.AliasJpaEntity; +import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; +import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; +import konkuk.thip.user.adapter.out.persistence.repository.alias.AliasJpaRepository; +import konkuk.thip.vote.adapter.out.jpa.VoteJpaEntity; +import konkuk.thip.vote.adapter.out.persistence.repository.VoteJpaRepository; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; + +import static konkuk.thip.common.entity.StatusType.INACTIVE; +import static konkuk.thip.common.post.PostType.*; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@ActiveProfiles("test") +@AutoConfigureMockMvc(addFilters = false) +@DisplayName("[통합] 댓글 삭제 api 통합 테스트") +class CommentDeleteAPITest { + + @Autowired + private MockMvc mockMvc; + + @Autowired private AliasJpaRepository aliasJpaRepository; + @Autowired private UserJpaRepository userJpaRepository; + @Autowired private CategoryJpaRepository categoryJpaRepository; + @Autowired private BookJpaRepository bookJpaRepository; + @Autowired private FeedJpaRepository feedJpaRepository; + @Autowired private VoteJpaRepository voteJpaRepository; + @Autowired private RecordJpaRepository recordJpaRepository; + @Autowired private CommentJpaRepository commentJpaRepository; + @Autowired private RoomJpaRepository roomJpaRepository; + @Autowired private RoomParticipantJpaRepository roomParticipantJpaRepository; + @Autowired private CommentLikeJpaRepository commentLikeJpaRepository; + + private AliasJpaEntity alias; + private UserJpaEntity user; + private CategoryJpaEntity category; + private FeedJpaEntity feed; + private BookJpaEntity book; + private RecordJpaEntity record; + private VoteJpaEntity vote; + private RoomJpaEntity room; + + @BeforeEach + void setUp() { + alias = aliasJpaRepository.save(TestEntityFactory.createLiteratureAlias()); + user = userJpaRepository.save(TestEntityFactory.createUser(alias)); + category = categoryJpaRepository.save(TestEntityFactory.createLiteratureCategory(alias)); + book = bookJpaRepository.save(TestEntityFactory.createBookWithISBN("9788954682152")); + room = roomJpaRepository.save(TestEntityFactory.createRoom(book,category)); + feed = feedJpaRepository.save(TestEntityFactory.createFeed(user,book, true)); + record = recordJpaRepository.save(TestEntityFactory.createRecord(user,room)); + vote = voteJpaRepository.save(TestEntityFactory.createVote(user,room)); + roomParticipantJpaRepository.save(TestEntityFactory.createRoomParticipant(room, user, RoomParticipantRole.HOST, 0.0)); + } + + @AfterEach + void tearDown() { + recordJpaRepository.deleteAllInBatch(); + voteJpaRepository.deleteAllInBatch(); + commentLikeJpaRepository.deleteAll(); + commentJpaRepository.deleteAllInBatch(); + feedJpaRepository.deleteAllInBatch(); + roomParticipantJpaRepository.deleteAllInBatch(); + roomJpaRepository.deleteAllInBatch(); + bookJpaRepository.deleteAll(); + userJpaRepository.deleteAllInBatch(); + categoryJpaRepository.deleteAll(); + aliasJpaRepository.deleteAll(); + } + + @Test + @DisplayName("루트댓글을 삭제하면 [soft delete 처리]된다") + void deleteRootComment_success() throws Exception { + + // given + CommentJpaEntity comment = commentJpaRepository.save(TestEntityFactory.createComment(feed, user, FEED)); + feed.updateCommentCount(1); + feedJpaRepository.save(feed); + Long commentId = comment.getCommentId(); + + // when + mockMvc.perform(delete("/comments/{commentId}", commentId) + .requestAttr("userId", user.getUserId())) + .andExpect(status().isOk()); + + // then + assertThat(commentJpaRepository.findById(commentId)).isPresent(); + assertThat(commentJpaRepository.findById(commentId).get().getStatus()).isEqualTo(INACTIVE); + } + + @Test + @DisplayName("대댓글을 삭제하면 [soft delete 처리]된다") + void deleteReplyComment_success() throws Exception { + + // given: 부모 댓글/대댓글 생성 + CommentJpaEntity parent = commentJpaRepository.save(TestEntityFactory.createComment(feed, user, FEED)); + CommentJpaEntity reply = commentJpaRepository.save(TestEntityFactory.createReplyComment(feed, user, FEED, parent)); + feed.updateCommentCount(2); + feedJpaRepository.save(feed); + Long replyId = reply.getCommentId(); + + // when + mockMvc.perform(delete("/comments/{commentId}", replyId) + .requestAttr("userId", user.getUserId())) + .andExpect(status().isOk()); + + // then + assertThat(commentJpaRepository.findById(replyId)).isPresent(); + assertThat(commentJpaRepository.findById(replyId).get().getStatus()).isEqualTo(INACTIVE); + + } + + @Test + @DisplayName("댓글 삭제 시에 댓글을 성공적으로 삭제하면 댓글의 상태가 soft delete 처리되고" + + "게시물의 댓글 수가 1감소하고, 댓글에 달린 좋아요가 전체 삭제 된다.") + void deleteComment_success() throws Exception { + + // given + CommentJpaEntity comment = commentJpaRepository.save(TestEntityFactory.createComment(feed, user, FEED)); + commentLikeJpaRepository.save(TestEntityFactory.createCommentLike(comment, user)); + feed.updateCommentCount(1); + feedJpaRepository.save(feed); + Long commentId = comment.getCommentId(); + int beforeCount = feed.getCommentCount(); + + // when + mockMvc.perform(delete("/comments/{commentId}", commentId) + .requestAttr("userId", user.getUserId())) + .andExpect(status().isOk()); + + // then + assertThat(commentJpaRepository.findById(commentId)).isPresent(); + assertThat(commentJpaRepository.findById(commentId).get().getStatus()).isEqualTo(INACTIVE); + + // Feed 댓글수 감소 확인 + FeedJpaEntity updatedFeed = feedJpaRepository.findById(feed.getPostId()).get(); + assertThat(updatedFeed.getCommentCount()).isEqualTo(beforeCount - 1); + + // 댓글 좋아요가 모두 삭제됐는지 확인 + boolean like = commentLikeJpaRepository.existsByUserIdAndCommentId(user.getUserId(), commentId); + assertThat(like).isFalse(); + } + +} diff --git a/src/test/java/konkuk/thip/comment/domain/CommentTest.java b/src/test/java/konkuk/thip/comment/domain/CommentTest.java index 586ae0051..99425f8e7 100644 --- a/src/test/java/konkuk/thip/comment/domain/CommentTest.java +++ b/src/test/java/konkuk/thip/comment/domain/CommentTest.java @@ -4,7 +4,9 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import static konkuk.thip.common.exception.code.ErrorCode.COMMENT_LIKE_COUNT_UNDERFLOW; +import static konkuk.thip.common.entity.StatusType.ACTIVE; +import static konkuk.thip.common.entity.StatusType.INACTIVE; +import static konkuk.thip.common.exception.code.ErrorCode.*; import static konkuk.thip.common.post.PostType.FEED; import static org.junit.jupiter.api.Assertions.*; @@ -14,6 +16,7 @@ class CommentTest { private final String CONTENT = "댓글 본문"; private final Long CREATOR_ID = 1L; private final Long POST_ID = 100L; + private final Long OTHER_USER_ID = 2L; private Comment createParentComment(Long postId) { return Comment.builder() @@ -25,6 +28,22 @@ private Comment createParentComment(Long postId) { .parentCommentId(null) .reportCount(0) .likeCount(0) + .status(ACTIVE) + .build(); + } + + + private Comment createInactiveComment(Long postId) { + return Comment.builder() + .id(124L) //ID 임의 주입 + .content(CONTENT) + .targetPostId(postId) + .creatorId(CREATOR_ID) + .postType(FEED) + .parentCommentId(null) + .reportCount(0) + .likeCount(0) + .status(INACTIVE) .build(); } @@ -174,5 +193,4 @@ void updateLikeCount_likeFalse_underflow_throws() { assertEquals(COMMENT_LIKE_COUNT_UNDERFLOW, ex.getErrorCode()); } - } diff --git a/src/test/java/konkuk/thip/common/util/TestEntityFactory.java b/src/test/java/konkuk/thip/common/util/TestEntityFactory.java index 94e00078a..dd9fc44fe 100644 --- a/src/test/java/konkuk/thip/common/util/TestEntityFactory.java +++ b/src/test/java/konkuk/thip/common/util/TestEntityFactory.java @@ -185,6 +185,18 @@ public static CommentJpaEntity createComment(PostJpaEntity post, UserJpaEntity u .build(); } + public static CommentJpaEntity createReplyComment(PostJpaEntity post, UserJpaEntity user,PostType postType,CommentJpaEntity parentComment) { + return CommentJpaEntity.builder() + .content("댓글 내용") + .postJpaEntity(post) + .userJpaEntity(user) + .likeCount(0) + .reportCount(0) + .postType(postType) + .parent(parentComment) + .build(); + } + public static CommentLikeJpaEntity createCommentLike(CommentJpaEntity comment, UserJpaEntity user) { return CommentLikeJpaEntity.builder() .userJpaEntity(user) diff --git a/src/test/java/konkuk/thip/feed/domain/FeedTest.java b/src/test/java/konkuk/thip/feed/domain/FeedTest.java new file mode 100644 index 000000000..82da7f6c0 --- /dev/null +++ b/src/test/java/konkuk/thip/feed/domain/FeedTest.java @@ -0,0 +1,221 @@ +package konkuk.thip.feed.domain; + +import konkuk.thip.common.exception.InvalidStateException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static konkuk.thip.common.exception.code.ErrorCode.*; +import static konkuk.thip.feed.domain.Tag.BOOK_RECOMMEND; +import static org.junit.jupiter.api.Assertions.*; + +@DisplayName("[단위] Feed 도메인 테스트") +class FeedTest { + + private final Long CREATOR_ID = 1L; + private final Long OTHER_USER_ID = 2L; + + private Feed createPublicFeed() { + return Feed.builder() + .id(100L) + .creatorId(CREATOR_ID) + .content("공개 피드 입니다.") + .isPublic(true) + .tagList(List.of(Tag.from(BOOK_RECOMMEND.getValue()))) + .contentList(List.of(Content.builder() + .contentUrl("url1") + .targetPostId(100L).build() + + )) + .commentCount(1) + .build(); + } + + private Feed createNotCommentFeed() { + return Feed.builder() + .id(101L) + .creatorId(CREATOR_ID) + .content("댓글이 없는 공개피드입니다.") + .isPublic(true) + .commentCount(0) + .build(); + } + + private Feed createPrivateFeed() { + return Feed.builder() + .id(102L) + .creatorId(CREATOR_ID) + .content("비공개피드 입니다.") + .isPublic(false) + .commentCount(0) + .build(); + } + + @Test + @DisplayName("validateTags: 태그가 5개 초과 시 InvalidStateException이 발생한다.") + void validateTags_exceedsMax_throws() { + List tags = List.of("a", "b", "c", "d", "e", "f"); + + InvalidStateException ex = assertThrows(InvalidStateException.class, + () -> Feed.validateTags(tags)); + + assertEquals(INVALID_FEED_COMMAND, ex.getErrorCode()); + assertTrue(ex.getCause().getMessage().contains("최대 5개")); + } + + @Test + @DisplayName("validateTags: 중복 태그 있을 경우 InvalidStateException이 발생한다.") + void validateTags_withDuplicates_throws() { + List tags = List.of("a", "b", "a"); + + InvalidStateException ex = assertThrows(InvalidStateException.class, + () -> Feed.validateTags(tags)); + + assertEquals(INVALID_FEED_COMMAND, ex.getErrorCode()); + assertTrue(ex.getCause().getMessage().contains("중복")); + } + + + @Test + @DisplayName("validateImageCount: 3개 초과 이미지 업로드 시 InvalidStateException이 발생한다.") + void validateImageCount_exceedsMax_throws() { + int imageCount = 4; + + InvalidStateException ex = assertThrows(InvalidStateException.class, + () -> Feed.validateImageCount(imageCount)); + + assertEquals(INVALID_FEED_COMMAND, ex.getErrorCode()); + assertTrue(ex.getCause().getMessage().contains("최대 3개")); + } + + + @Test + @DisplayName("validateCreateComment: 공개 피드면 누구나 댓글을 작성 할 수 있다") + void validateCreateComment_publicFeed_passes() { + Feed feed = createPublicFeed(); + + assertDoesNotThrow(() -> feed.validateCreateComment(OTHER_USER_ID)); + } + + @Test + @DisplayName("validateCreateComment: 비공개 피드시에 작성자가 아닌 유저가 댓글을 작성하려고 하면 InvalidStateException이 발생한다.") + void validateCreateComment_nonCreatorOnPrivateFeed_throws() { + Feed feed = createPrivateFeed(); + + InvalidStateException ex = assertThrows(InvalidStateException.class, + () -> feed.validateCreateComment(OTHER_USER_ID)); + + assertEquals(FEED_ACCESS_FORBIDDEN, ex.getErrorCode()); + assertFalse(ex.getCause().getMessage().contains("비공개 글은 작성자만")); + } + + @Test + @DisplayName("validateCreateComment: 비공개 피드시에 작성자만 댓글을 작성 할 수 있다.") + void validateCreateComment_creatorOnPrivateFeed_passes() { + Feed feed = createPrivateFeed(); + + assertDoesNotThrow(() -> feed.validateCreateComment(CREATOR_ID)); + } + + + @Test + @DisplayName("updateContent: 작성자가 아닌 경우 피드 내용을 수정하려고 하면 InvalidStateException이 발생한다.") + void updateContent_byNonCreator_throws() { + Feed feed = createPublicFeed(); + String newContent = "새로운 피드 내용"; + + InvalidStateException ex = assertThrows(InvalidStateException.class, + () -> feed.updateContent(OTHER_USER_ID, newContent)); + + assertEquals(FEED_ACCESS_FORBIDDEN,ex.getErrorCode()); + assertTrue(ex.getCause().getMessage().contains("피드 작성자만")); + } + + + @Test + @DisplayName("updateVisibility: 작성자가 아닌 경우 피드 공개여부를 수정하려고 하면 InvalidStateException이 발생한다.") + void updateVisibility_byNonCreator_throws() { + Feed feed = createPublicFeed(); + + InvalidStateException ex = assertThrows(InvalidStateException.class, + () -> feed.updateVisibility(OTHER_USER_ID, false)); + + assertEquals(FEED_ACCESS_FORBIDDEN, ex.getErrorCode()); + } + + @Test + @DisplayName("updateTags: 작성자가 아닌 경우 피드의 태그를 수정하려고 하면 InvalidStateException이 발생한다.") + void updateTags_byNonCreator_throws(){ + Feed feed = createPublicFeed(); + List tags = List.of(BOOK_RECOMMEND.getValue()); + + InvalidStateException ex = assertThrows(InvalidStateException.class, + () -> feed.updateTags(OTHER_USER_ID, tags)); + + assertEquals(FEED_ACCESS_FORBIDDEN, ex.getErrorCode()); + } + + + @Test + @DisplayName("updateImages: 작성자가 아닌 경우 피드의 이미지를 수정하려고 하면 InvalidStateException이 발생한다.") + void updateImages_nonCreator_throws() { + Feed feed = createPublicFeed(); + List images = List.of("url1", "url2"); + + InvalidStateException ex = assertThrows(InvalidStateException.class, + () -> feed.updateImages(OTHER_USER_ID, images)); + + assertEquals(FEED_ACCESS_FORBIDDEN, ex.getErrorCode()); + } + + @Test + @DisplayName("validateOwnsImages: 피드 수정 시에 존재하지 않는 이미지 URL 포함하여 수정하려고 하면 InvalidStateException이 발생한다.") + void validateOwnsImages_withInvalidUrl_throws() { + Feed feed = createPublicFeed(); + + // feed.contentList에는 "url1"만 있음 + List candidateImageUrls = List.of("url1", "invalidUrl"); + + InvalidStateException ex = assertThrows(InvalidStateException.class, + () -> feed.validateOwnsImages(candidateImageUrls)); + + assertEquals(INVALID_FEED_COMMAND, ex.getErrorCode()); + assertTrue(ex.getCause().getMessage().contains("해당 이미지는 이 피드에 존재하지 않습니다")); + } + + + @Test + @DisplayName("increaseCommentCount: 피드의 댓글 수가 정상적으로 1 증가한다.") + void increaseCommentCount_increments() { + Feed feed = createPublicFeed(); + int before = feed.getCommentCount(); + + feed.increaseCommentCount(); + + assertEquals(before + 1, feed.getCommentCount()); + } + + @Test + @DisplayName("decreaseCommentCount: 피드의 댓글 수가 정상적으로 1 감소한다.") + void decreaseCommentCount_decrements() { + Feed feed = createPublicFeed(); + int before = feed.getCommentCount(); + + feed.decreaseCommentCount(); + + assertEquals(before - 1, feed.getCommentCount()); + } + + @Test + @DisplayName("decreaseCommentCount: 피드의 댓글 수가 0 이하로 내려가면 InvalidStateException이 발생한다.") + void decreaseCommentCount_belowZero_throws() { + Feed feed = createNotCommentFeed(); + + InvalidStateException ex = assertThrows(InvalidStateException.class, + () -> feed.decreaseCommentCount()); + + assertEquals(COMMENT_COUNT_UNDERFLOW, ex.getErrorCode()); + } + +} diff --git a/src/test/java/konkuk/thip/record/domain/RecordTest.java b/src/test/java/konkuk/thip/record/domain/RecordTest.java index 75b14fdf2..1a5485c4a 100644 --- a/src/test/java/konkuk/thip/record/domain/RecordTest.java +++ b/src/test/java/konkuk/thip/record/domain/RecordTest.java @@ -4,11 +4,41 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import static konkuk.thip.common.exception.code.ErrorCode.COMMENT_COUNT_UNDERFLOW; import static org.junit.jupiter.api.Assertions.*; @DisplayName("[단위] Record 도메인 테스트") class RecordTest { + private final Long CREATOR_ID = 1L; + + private Record createWithCommentRecord() { + return Record.builder() + .id(100L) + .content("댓글이 존재하는 기록입니다.") + .creatorId(CREATOR_ID) + .page(10) + .isOverview(false) + .likeCount(0) + .commentCount(1) + .roomId(100L) + .build(); + } + + private Record createNotCommentRecord() { + return Record.builder() + .id(100L) + .content("댓글이 존재하지 않는 기록입니다.") + .creatorId(CREATOR_ID) + .page(10) + .isOverview(false) + .likeCount(0) + .commentCount(0) + .roomId(100L) + .build(); + } + + @Test @DisplayName("validatePage: 유효한 페이지 범위일 때, 예외가 발생하지 않는다.") void validate_page_valid_range() { @@ -60,4 +90,39 @@ void validate_overview_page_is_not_book_page_count() { assertInstanceOf(IllegalArgumentException.class, ex.getCause()); assertTrue(ex.getCause().getMessage().contains("현재 페이지 = 15")); } + + @Test + @DisplayName("increaseCommentCount: 기록의 댓글 수가 정상적으로 1 증가한다.") + void increaseCommentCount_increments() { + Record record = createWithCommentRecord(); + int before = record.getCommentCount(); + + record.increaseCommentCount(); + + assertEquals(before + 1, record.getCommentCount()); + } + + @Test + @DisplayName("decreaseCommentCount: 기록의 댓글 수가 정상적으로 1 감소한다.") + void decreaseCommentCount_decrements() { + Record record = createWithCommentRecord(); + int before = record.getCommentCount(); + + record.decreaseCommentCount(); + + assertEquals(before - 1, record.getCommentCount()); + } + + @Test + @DisplayName("decreaseCommentCount: 기록의 댓글 수가 0 이하로 내려가면 InvalidStateException이 발생한다.") + void decreaseCommentCount_belowZero_throws() { + Record record = createNotCommentRecord(); + + InvalidStateException ex = assertThrows(InvalidStateException.class, + () -> record.decreaseCommentCount()); + + assertEquals(COMMENT_COUNT_UNDERFLOW, ex.getErrorCode()); + } + + } \ No newline at end of file diff --git a/src/test/java/konkuk/thip/vote/domain/VoteTest.java b/src/test/java/konkuk/thip/vote/domain/VoteTest.java index 8992a8dd5..95b871931 100644 --- a/src/test/java/konkuk/thip/vote/domain/VoteTest.java +++ b/src/test/java/konkuk/thip/vote/domain/VoteTest.java @@ -4,11 +4,41 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import static konkuk.thip.common.exception.code.ErrorCode.COMMENT_COUNT_UNDERFLOW; import static org.junit.jupiter.api.Assertions.*; @DisplayName("[단위] Vote 도메인 테스트") class VoteTest { + private final Long CREATOR_ID = 1L; + + private Vote createWithCommentVote() { + return Vote.builder() + .id(100L) + .content("댓글이 존재하는 투표입니다.") + .creatorId(CREATOR_ID) + .page(10) + .isOverview(false) + .likeCount(0) + .commentCount(1) + .roomId(100L) + .build(); + } + + private Vote createNotCommentVote() { + return Vote.builder() + .id(100L) + .content("댓글이 존재하지 않는 투표입니다.") + .creatorId(CREATOR_ID) + .page(10) + .isOverview(false) + .likeCount(0) + .commentCount(0) + .roomId(100L) + .build(); + } + + @Test @DisplayName("validatePage: 유효한 페이지 범위일 때, 예외가 발생하지 않는다.") void validate_page_valid_range() { @@ -60,4 +90,38 @@ void validate_overview_ratio_below_80_percent() { assertInstanceOf(IllegalStateException.class, ex.getCause()); assertTrue(ex.getCause().getMessage().contains("현재 진행률 = 75.00% (15/20)")); } + + @Test + @DisplayName("increaseCommentCount: 투표의 댓글 수가 정상적으로 1 증가한다.") + void increaseCommentCount_increments() { + Vote vote = createWithCommentVote(); + int before = vote.getCommentCount(); + + vote.increaseCommentCount(); + + assertEquals(before + 1, vote.getCommentCount()); + } + + @Test + @DisplayName("decreaseCommentCount: 투표의 댓글 수가 정상적으로 1 감소한다.") + void decreaseCommentCount_decrements() { + Vote vote = createWithCommentVote(); + int before = vote.getCommentCount(); + + vote.decreaseCommentCount(); + + assertEquals(before - 1, vote.getCommentCount()); + } + + @Test + @DisplayName("decreaseCommentCount: 투표의 댓글 수가 0 이하로 내려가면 InvalidStateException이 발생한다.") + void decreaseCommentCount_belowZero_throws() { + Vote vote = createNotCommentVote(); + + InvalidStateException ex = assertThrows(InvalidStateException.class, + () -> vote.decreaseCommentCount()); + + assertEquals(COMMENT_COUNT_UNDERFLOW, ex.getErrorCode()); + } + }