diff --git a/src/main/java/com/ssafy/ssafsound/batch/config/PostBatchConfig.java b/src/main/java/com/ssafy/ssafsound/batch/config/PostBatchConfig.java index 5a04a4698..9c65eb14c 100644 --- a/src/main/java/com/ssafy/ssafsound/batch/config/PostBatchConfig.java +++ b/src/main/java/com/ssafy/ssafsound/batch/config/PostBatchConfig.java @@ -1,6 +1,7 @@ package com.ssafy.ssafsound.batch.config; import com.ssafy.ssafsound.batch.tasklet.PostTasklet; +import com.ssafy.ssafsound.domain.post.service.PostConstantProvider; import com.ssafy.ssafsound.domain.post.service.PostService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -21,6 +22,7 @@ public class PostBatchConfig { private final JobBuilderFactory jobBuilderFactory; private final StepBuilderFactory stepBuilderFactory; private final PostService postService; + private final PostConstantProvider postConstantProvider; @Bean @@ -40,6 +42,6 @@ public Step deleteHotPostStep() { @Bean public Tasklet deleteHotPostTasklet() { - return new PostTasklet(postService); + return new PostTasklet(postService, postConstantProvider); } } diff --git a/src/main/java/com/ssafy/ssafsound/batch/tasklet/PostTasklet.java b/src/main/java/com/ssafy/ssafsound/batch/tasklet/PostTasklet.java index 6abf4f422..77d2bf103 100644 --- a/src/main/java/com/ssafy/ssafsound/batch/tasklet/PostTasklet.java +++ b/src/main/java/com/ssafy/ssafsound/batch/tasklet/PostTasklet.java @@ -1,5 +1,6 @@ package com.ssafy.ssafsound.batch.tasklet; +import com.ssafy.ssafsound.domain.post.service.PostConstantProvider; import com.ssafy.ssafsound.domain.post.service.PostService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -10,14 +11,12 @@ import org.springframework.batch.core.scope.context.ChunkContext; import org.springframework.batch.core.step.tasklet.Tasklet; import org.springframework.batch.repeat.RepeatStatus; -import org.springframework.beans.factory.annotation.Value; @Slf4j @RequiredArgsConstructor public class PostTasklet implements Tasklet, StepExecutionListener { - @Value("${spring.constant.post.HOT_POST_LIKES_THRESHOLD}") - private Long HOT_POST_LIKES_THRESHOLD; private final PostService postService; + private final PostConstantProvider postConstantProvider; @Override public void beforeStep(StepExecution stepExecution) { @@ -25,7 +24,7 @@ public void beforeStep(StepExecution stepExecution) { @Override public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) { - postService.deleteHotPostsUnderThreshold(HOT_POST_LIKES_THRESHOLD); + postService.deleteHotPostsUnderThreshold(postConstantProvider.getHOT_POST_LIKES_THRESHOLD()); return RepeatStatus.FINISHED; } diff --git a/src/main/java/com/ssafy/ssafsound/domain/comment/domain/Comment.java b/src/main/java/com/ssafy/ssafsound/domain/comment/domain/Comment.java index 9ce1c141f..694e4a90e 100644 --- a/src/main/java/com/ssafy/ssafsound/domain/comment/domain/Comment.java +++ b/src/main/java/com/ssafy/ssafsound/domain/comment/domain/Comment.java @@ -1,6 +1,7 @@ package com.ssafy.ssafsound.domain.comment.domain; import com.ssafy.ssafsound.domain.BaseTimeEntity; +import com.ssafy.ssafsound.domain.comment.dto.PostCommentWriteReqDto; import com.ssafy.ssafsound.domain.member.domain.Member; import com.ssafy.ssafsound.domain.post.domain.Post; import lombok.AllArgsConstructor; diff --git a/src/main/java/com/ssafy/ssafsound/domain/comment/repository/CommentRepository.java b/src/main/java/com/ssafy/ssafsound/domain/comment/repository/CommentRepository.java index 6d1074955..6adee2a36 100644 --- a/src/main/java/com/ssafy/ssafsound/domain/comment/repository/CommentRepository.java +++ b/src/main/java/com/ssafy/ssafsound/domain/comment/repository/CommentRepository.java @@ -1,21 +1,29 @@ package com.ssafy.ssafsound.domain.comment.repository; -import com.ssafy.ssafsound.domain.comment.domain.Comment; -import org.springframework.data.domain.Pageable; +import java.util.List; + import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; -import java.util.List; +import com.ssafy.ssafsound.domain.comment.domain.Comment; @Repository public interface CommentRepository extends JpaRepository { - @Query("SELECT c FROM comment c " + - "JOIN FETCH c.commentNumber " + - "JOIN FETCH c.member " + - "JOIN FETCH c.commentGroup g " + - "WHERE c.post.id = :postId " + - "ORDER BY g.id ") - List findAllPostIdWithDetailsFetchOrderByCommentGroupId(@Param("postId") Long postId); + @Query("SELECT c FROM comment c " + + "JOIN FETCH c.commentNumber " + + "JOIN FETCH c.member " + + "JOIN FETCH c.commentGroup g " + + "WHERE c.post.id = :postId " + + "ORDER BY g.id ") + List findAllPostIdWithDetailsFetchOrderByCommentGroupId(@Param("postId") Long postId); + + @Modifying + @Query(value = "update comment " + + "set comment_group = :id " + + "where comment_id = :id", nativeQuery = true) + void updateByCommentGroup(@Param("id") Long id); + } diff --git a/src/main/java/com/ssafy/ssafsound/domain/comment/service/CommentService.java b/src/main/java/com/ssafy/ssafsound/domain/comment/service/CommentService.java index 3d83b35d2..8ac19cfe4 100644 --- a/src/main/java/com/ssafy/ssafsound/domain/comment/service/CommentService.java +++ b/src/main/java/com/ssafy/ssafsound/domain/comment/service/CommentService.java @@ -14,6 +14,7 @@ import com.ssafy.ssafsound.domain.member.exception.MemberErrorInfo; import com.ssafy.ssafsound.domain.member.exception.MemberException; import com.ssafy.ssafsound.domain.member.repository.MemberRepository; +import com.ssafy.ssafsound.domain.post.domain.Post; import com.ssafy.ssafsound.domain.post.dto.PostCommonLikeResDto; import com.ssafy.ssafsound.domain.post.exception.PostErrorInfo; import com.ssafy.ssafsound.domain.post.exception.PostException; @@ -69,7 +70,7 @@ public CommentIdElement writeComment(Long postId, Long loginMemberId, PostCommen .build(); comment = commentRepository.save(comment); - comment.setCommentGroup(comment); + commentRepository.updateByCommentGroup(comment.getId()); return new CommentIdElement(comment.getId()); } diff --git a/src/main/java/com/ssafy/ssafsound/domain/member/dto/AuthorElement.java b/src/main/java/com/ssafy/ssafsound/domain/member/dto/AuthorElement.java index 039e59ff9..165ff6814 100644 --- a/src/main/java/com/ssafy/ssafsound/domain/member/dto/AuthorElement.java +++ b/src/main/java/com/ssafy/ssafsound/domain/member/dto/AuthorElement.java @@ -29,9 +29,9 @@ public AuthorElement(Member member, Boolean anonymity, Long number) { public AuthorElement(Member member, Boolean anonymity) { this.memberId = anonymity? -1 : member.getId(); this.nickname = anonymity? "익명" : member.getNickname(); - this.memberRole = member.getRole().getRoleType(); - this.ssafyMember = member.getSsafyMember(); - this.isMajor = member.getMajor(); - this.ssafyInfo = SSAFYInfo.from(member); + this.memberRole = anonymity? null : member.getRole().getRoleType(); + this.ssafyMember = anonymity? null : member.getSsafyMember(); + this.isMajor = anonymity? null : member.getMajor(); + this.ssafyInfo = anonymity? null : SSAFYInfo.from(member); } } diff --git a/src/main/java/com/ssafy/ssafsound/domain/post/domain/HotPost.java b/src/main/java/com/ssafy/ssafsound/domain/post/domain/HotPost.java index 815588207..921a0a45b 100644 --- a/src/main/java/com/ssafy/ssafsound/domain/post/domain/HotPost.java +++ b/src/main/java/com/ssafy/ssafsound/domain/post/domain/HotPost.java @@ -1,26 +1,39 @@ package com.ssafy.ssafsound.domain.post.domain; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.OneToOne; + import com.ssafy.ssafsound.domain.BaseTimeEntity; + import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import javax.persistence.*; - -@Entity(name="hot_post") +@Entity(name = "hot_post") @Getter @Builder @NoArgsConstructor @AllArgsConstructor public class HotPost extends BaseTimeEntity { - @Id - @Column(name = "hot_post_id") - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; + @Id + @Column(name = "hot_post_id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @OneToOne + @JoinColumn(name = "post_id") + private Post post; - @OneToOne - @JoinColumn(name = "post_id") - private Post post; + public static HotPost from(Post post) { + return HotPost.builder() + .post(post) + .build(); + } } diff --git a/src/main/java/com/ssafy/ssafsound/domain/post/domain/PostLike.java b/src/main/java/com/ssafy/ssafsound/domain/post/domain/PostLike.java index ebec106a6..283b75218 100644 --- a/src/main/java/com/ssafy/ssafsound/domain/post/domain/PostLike.java +++ b/src/main/java/com/ssafy/ssafsound/domain/post/domain/PostLike.java @@ -1,31 +1,46 @@ package com.ssafy.ssafsound.domain.post.domain; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; + import com.ssafy.ssafsound.domain.BaseTimeEntity; import com.ssafy.ssafsound.domain.member.domain.Member; + import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import javax.persistence.*; - -@Entity(name="post_like") +@Entity(name = "post_like") @Getter @Builder @NoArgsConstructor @AllArgsConstructor public class PostLike extends BaseTimeEntity { - @Id - @Column(name = "post_like_id") - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; + @Id + @Column(name = "post_like_id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "post_id") + private Post post; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "post_id") - private Post post; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + private Member member; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "member_id") - private Member member; + public static PostLike of(Post post, Member member) { + return PostLike.builder() + .post(post) + .member(member) + .build(); + } } diff --git a/src/main/java/com/ssafy/ssafsound/domain/post/domain/PostScrap.java b/src/main/java/com/ssafy/ssafsound/domain/post/domain/PostScrap.java index 9aeb43cf7..a214500e0 100644 --- a/src/main/java/com/ssafy/ssafsound/domain/post/domain/PostScrap.java +++ b/src/main/java/com/ssafy/ssafsound/domain/post/domain/PostScrap.java @@ -1,30 +1,45 @@ package com.ssafy.ssafsound.domain.post.domain; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; + import com.ssafy.ssafsound.domain.member.domain.Member; + import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import javax.persistence.*; - -@Entity(name="post_scrap") +@Entity(name = "post_scrap") @Getter @Builder @NoArgsConstructor @AllArgsConstructor public class PostScrap { - @Id - @Column(name = "post_scrap_id") - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; + @Id + @Column(name = "post_scrap_id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "post_id") + private Post post; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "post_id") - private Post post; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + private Member member; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "member_id") - private Member member; + public static PostScrap of(Post post, Member member) { + return PostScrap.builder() + .post(post) + .member(member) + .build(); + } } diff --git a/src/main/java/com/ssafy/ssafsound/domain/post/dto/GetPostReqDto.java b/src/main/java/com/ssafy/ssafsound/domain/post/dto/GetPostReqDto.java index 2155a9c29..ae7150abe 100644 --- a/src/main/java/com/ssafy/ssafsound/domain/post/dto/GetPostReqDto.java +++ b/src/main/java/com/ssafy/ssafsound/domain/post/dto/GetPostReqDto.java @@ -1,5 +1,6 @@ package com.ssafy.ssafsound.domain.post.dto; +import lombok.Builder; import lombok.Getter; import lombok.Setter; @@ -7,6 +8,7 @@ @Getter @Setter +@Builder public class GetPostReqDto { private Long boardId; private Long cursor; diff --git a/src/main/java/com/ssafy/ssafsound/domain/post/repository/HotPostRepository.java b/src/main/java/com/ssafy/ssafsound/domain/post/repository/HotPostRepository.java index b3eb0c15c..9fac8a45d 100644 --- a/src/main/java/com/ssafy/ssafsound/domain/post/repository/HotPostRepository.java +++ b/src/main/java/com/ssafy/ssafsound/domain/post/repository/HotPostRepository.java @@ -23,4 +23,5 @@ public interface HotPostRepository extends JpaRepository, HotPost void deleteHotPostsUnderThreshold(@Param("threshold") Long threshold); Optional findByPostId(Long postId); + Boolean existsByPostId(Long postId); } diff --git a/src/main/java/com/ssafy/ssafsound/domain/post/service/PostConstantProvider.java b/src/main/java/com/ssafy/ssafsound/domain/post/service/PostConstantProvider.java new file mode 100644 index 000000000..e660258ee --- /dev/null +++ b/src/main/java/com/ssafy/ssafsound/domain/post/service/PostConstantProvider.java @@ -0,0 +1,14 @@ +package com.ssafy.ssafsound.domain.post.service; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.ConstructorBinding; + +@Getter +@RequiredArgsConstructor +@ConfigurationProperties(prefix = "spring.constant.post") +@ConstructorBinding +public class PostConstantProvider { + private final Long HOT_POST_LIKES_THRESHOLD; +} diff --git a/src/main/java/com/ssafy/ssafsound/domain/post/service/PostService.java b/src/main/java/com/ssafy/ssafsound/domain/post/service/PostService.java index c89be1bd5..310849c8b 100644 --- a/src/main/java/com/ssafy/ssafsound/domain/post/service/PostService.java +++ b/src/main/java/com/ssafy/ssafsound/domain/post/service/PostService.java @@ -1,5 +1,10 @@ package com.ssafy.ssafsound.domain.post.service; +import java.util.List; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + import com.ssafy.ssafsound.domain.board.domain.Board; import com.ssafy.ssafsound.domain.board.exception.BoardErrorInfo; import com.ssafy.ssafsound.domain.board.exception.BoardException; @@ -8,303 +13,315 @@ import com.ssafy.ssafsound.domain.member.exception.MemberErrorInfo; import com.ssafy.ssafsound.domain.member.exception.MemberException; import com.ssafy.ssafsound.domain.member.repository.MemberRepository; -import com.ssafy.ssafsound.domain.post.domain.*; +import com.ssafy.ssafsound.domain.post.domain.HotPost; +import com.ssafy.ssafsound.domain.post.domain.Post; +import com.ssafy.ssafsound.domain.post.domain.PostImage; +import com.ssafy.ssafsound.domain.post.domain.PostLike; +import com.ssafy.ssafsound.domain.post.domain.PostScrap; import com.ssafy.ssafsound.domain.post.dto.GetPostDetailResDto; +import com.ssafy.ssafsound.domain.post.dto.GetPostHotReqDto; +import com.ssafy.ssafsound.domain.post.dto.GetPostHotSearchReqDto; +import com.ssafy.ssafsound.domain.post.dto.GetPostMyReqDto; +import com.ssafy.ssafsound.domain.post.dto.GetPostReqDto; import com.ssafy.ssafsound.domain.post.dto.GetPostResDto; +import com.ssafy.ssafsound.domain.post.dto.GetPostSearchReqDto; +import com.ssafy.ssafsound.domain.post.dto.ImageInfo; +import com.ssafy.ssafsound.domain.post.dto.PostCommonLikeResDto; +import com.ssafy.ssafsound.domain.post.dto.PostIdElement; +import com.ssafy.ssafsound.domain.post.dto.PostPatchUpdateReqDto; +import com.ssafy.ssafsound.domain.post.dto.PostPostScrapResDto; +import com.ssafy.ssafsound.domain.post.dto.PostPostWriteReqDto; import com.ssafy.ssafsound.domain.post.exception.PostErrorInfo; import com.ssafy.ssafsound.domain.post.exception.PostException; -import com.ssafy.ssafsound.domain.post.repository.*; -import com.ssafy.ssafsound.domain.post.dto.*; +import com.ssafy.ssafsound.domain.post.repository.HotPostRepository; +import com.ssafy.ssafsound.domain.post.repository.PostImageRepository; +import com.ssafy.ssafsound.domain.post.repository.PostLikeRepository; +import com.ssafy.ssafsound.domain.post.repository.PostRepository; +import com.ssafy.ssafsound.domain.post.repository.PostScrapRepository; import com.ssafy.ssafsound.infra.exception.InfraException; import com.ssafy.ssafsound.infra.storage.service.AwsS3StorageService; + import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.List; @Service @Slf4j @RequiredArgsConstructor public class PostService { - @Value("${spring.constant.post.HOT_POST_LIKES_THRESHOLD}") - private Long HOT_POST_LIKES_THRESHOLD; - - private final BoardRepository boardRepository; - private final MemberRepository memberRepository; - - private final PostRepository postRepository; - private final PostLikeRepository postLikeRepository; - private final HotPostRepository hotPostRepository; - private final PostScrapRepository postScrapRepository; - private final PostImageRepository postImageRepository; - private final AwsS3StorageService awsS3StorageService; - - @Transactional(readOnly = true) - public GetPostResDto findPosts(GetPostReqDto getPostReqDto) { - Long boardId = getPostReqDto.getBoardId(); - Long cursor = getPostReqDto.getCursor(); - int size = getPostReqDto.getSize(); - - if (!boardRepository.existsById(boardId)) { - throw new BoardException(BoardErrorInfo.NO_BOARD); - } - - List posts = postRepository.findWithDetailsByBoardId(boardId, cursor, size); - return GetPostResDto.ofPosts(posts, size); - } - - @Transactional(readOnly = true) - public GetPostDetailResDto findPost(Long postId, Long loginMemberId) { - Post post = postRepository.findWithMemberAndPostImageFetchById(postId) - .orElseThrow(() -> new PostException(PostErrorInfo.NOT_FOUND_POST)); - - if (loginMemberId != null) { - Member member = memberRepository.findById(loginMemberId) - .orElseThrow(() -> new MemberException(MemberErrorInfo.MEMBER_NOT_FOUND_BY_ID)); - return GetPostDetailResDto.of(post, member); - } - - return GetPostDetailResDto.of(post, null); - } - - @Transactional - public PostCommonLikeResDto likePost(Long postId, Long loginMemberId) { - Member loginMember = memberRepository.findById(loginMemberId) - .orElseThrow(() -> new MemberException(MemberErrorInfo.MEMBER_NOT_FOUND_BY_ID)); - - PostLike postLike = postLikeRepository.findByPostIdAndMemberId(postId, loginMember.getId()) - .orElse(null); - - Integer likeCount = postLikeRepository.countByPostId(postId); - return togglePostLike(likeCount, postId, loginMember, postLike); - } - - private PostCommonLikeResDto togglePostLike(Integer likeCount, Long postId, Member loginMember, PostLike postLike) { - if (postLike != null) { - deleteLike(postLike); - return new PostCommonLikeResDto(likeCount - 1, false); - } - - saveLike(postId, loginMember); - if (isSelectedHotPost(postId)) { - saveHotPost(postId); - } - return new PostCommonLikeResDto(likeCount + 1, true); - } - - private void saveLike(Long postId, Member loginMember) { - PostLike postLike = PostLike.builder() - .post(postRepository.getReferenceById(postId)) - .member(loginMember) - .build(); - postLikeRepository.save(postLike); - } - - private void deleteLike(PostLike postLike) { - postLikeRepository.delete(postLike); - } - - - private boolean isSelectedHotPost(Long postId) { - return postLikeRepository.countByPostId(postId) >= HOT_POST_LIKES_THRESHOLD; - } - - private void saveHotPost(Long postId) { - HotPost hotPost = HotPost.builder() - .post(postRepository.findById(postId). - orElseThrow(() -> new PostException(PostErrorInfo.NOT_FOUND_POST))) - .build(); - hotPostRepository.save(hotPost); - } - - @Transactional - public void deleteHotPostsUnderThreshold(Long threshold) { - hotPostRepository.deleteHotPostsUnderThreshold(threshold); - } - - @Transactional - public PostPostScrapResDto scrapPost(Long postId, Long loginMemberId) { - Member loginMember = memberRepository.findById(loginMemberId) - .orElseThrow(() -> new MemberException(MemberErrorInfo.MEMBER_NOT_FOUND_BY_ID)); - - PostScrap postScrap = postScrapRepository.findByPostIdAndMemberId(postId, loginMember.getId()) - .orElse(null); - - Integer scrapCount = postScrapRepository.countByPostId(postId); - return togglePostScrap(scrapCount, postId, loginMember, postScrap); - } - - private PostPostScrapResDto togglePostScrap(Integer scrapCount, Long postId, Member loginMember, PostScrap postScrap) { - if (postScrap != null) { - deleteScrapIfAlreadyExists(postScrap); - return new PostPostScrapResDto(scrapCount - 1, false); - } - saveScrap(postId, loginMember); - return new PostPostScrapResDto(scrapCount + 1, true); - } - - private void saveScrap(Long postId, Member loginMember) { - PostScrap postScrap = PostScrap.builder() - .post(postRepository.getReferenceById(postId)) - .member(loginMember) - .build(); - postScrapRepository.save(postScrap); - } - - private void deleteScrapIfAlreadyExists(PostScrap postScrap) { - postScrapRepository.delete(postScrap); - } - - @Transactional - public PostIdElement writePost(Long boardId, Long loginMemberId, PostPostWriteReqDto postPostWriteReqDto) { - Board board = boardRepository.findById(boardId) - .orElseThrow(() -> new BoardException(BoardErrorInfo.NO_BOARD)); - - Member loginMember = memberRepository.findById(loginMemberId) - .orElseThrow(() -> new MemberException(MemberErrorInfo.MEMBER_NOT_FOUND_BY_ID)); - - List images = postPostWriteReqDto.getImages(); - - Post post = Post.builder() - .board(board) - .member(loginMember) - .title(postPostWriteReqDto.getTitle()) - .content(postPostWriteReqDto.getContent()) - .anonymity(postPostWriteReqDto.isAnonymity()) - .build(); - postRepository.save(post); - - if (images.size() > 0) { - for (ImageInfo image : images) { - PostImage postImage = PostImage.builder() - .post(post) - .imagePath(image.getImagePath()) - .imageUrl(image.getImageUrl()) - .build(); - postImageRepository.save(postImage); - } - } - return new PostIdElement(post.getId()); - } - - @Transactional - public PostIdElement deletePost(Long postId, Long loginMemberId) { - Post post = postRepository.findByIdWithMember(postId) - .orElseThrow(() -> new PostException(PostErrorInfo.NOT_FOUND_POST)); - - Member loginMember = memberRepository.findById(loginMemberId) - .orElseThrow(() -> new MemberException(MemberErrorInfo.MEMBER_NOT_FOUND_BY_ID)); - - if (!post.getMember().getId().equals(loginMember.getId())) { - throw new PostException((PostErrorInfo.UNAUTHORIZED_DELETE_POST)); - } - - deleteAllPostImages(post.getImages()); - postRepository.delete(post); - hotPostRepository.findByPostId(postId).ifPresent(hotPostRepository::delete); - return new PostIdElement(postId); - } - - private void deleteAllPostImages(List images) { - images.forEach(image -> { - try { - awsS3StorageService.deleteObject(image); - - } catch (InfraException e) { - log.error("이미지 삭제에 오류가 발생했습니다."); - } - postImageRepository.delete(image); - }); - } - - @Transactional - public PostIdElement updatePost(Long postId, Long loginMemberId, PostPatchUpdateReqDto postPatchUpdateReqDto) { - Post post = postRepository.findWithMemberAndPostImageFetchById(postId) - .orElseThrow(() -> new PostException(PostErrorInfo.NOT_FOUND_POST)); - - Member loginMember = memberRepository.findById(loginMemberId) - .orElseThrow(() -> new MemberException(MemberErrorInfo.MEMBER_NOT_FOUND_BY_ID)); - - if (!post.getMember().getId().equals(loginMember.getId())) { - throw new PostException(PostErrorInfo.UNAUTHORIZED_UPDATE_POST); - } - - post.updatePost(postPatchUpdateReqDto.getTitle(), postPatchUpdateReqDto.getContent(), postPatchUpdateReqDto.isAnonymity()); - postImageRepository.deleteAllInBatch(post.getImages()); - - List images = postPatchUpdateReqDto.getImages(); - if (images.size() > 0) { - for (ImageInfo image : images) { - PostImage postImage = PostImage.builder() - .post(post) - .imagePath(image.getImagePath()) - .imageUrl(image.getImageUrl()) - .build(); - postImageRepository.save(postImage); - } - } - return new PostIdElement(post.getId()); - } - - - @Transactional(readOnly = true) - public GetPostResDto findHotPosts(GetPostHotReqDto getPostHotReqDto) { - Long cursor = getPostHotReqDto.getCursor(); - int size = getPostHotReqDto.getSize(); - - List hotPosts = hotPostRepository.findHotPosts(cursor, size); - return GetPostResDto.ofHotPosts(hotPosts, size); - } - - @Transactional(readOnly = true) - public GetPostResDto findMyPosts(GetPostMyReqDto getPostMyReqDto, Long loginMemberId) { - Long cursor = getPostMyReqDto.getCursor(); - int size = getPostMyReqDto.getSize(); - - Member loginMember = memberRepository.findById(loginMemberId) - .orElseThrow(() -> new MemberException(MemberErrorInfo.MEMBER_NOT_FOUND_BY_ID)); - - List posts = postRepository.findWithDetailsByMemberId(loginMember.getId(), cursor, size); - return GetPostResDto.ofPosts(posts, size); - } - - @Transactional(readOnly = true) - public GetPostResDto findMyScrapPosts(GetPostMyReqDto getPostMyScrapReqDto, Long loginMemberId) { - Long cursor = getPostMyScrapReqDto.getCursor(); - int size = getPostMyScrapReqDto.getSize(); - - Member loginMember = memberRepository.findById(loginMemberId) - .orElseThrow(() -> new MemberException(MemberErrorInfo.MEMBER_NOT_FOUND_BY_ID)); - - List postScraps = postScrapRepository.findMyScrapPosts(loginMember.getId(), cursor, size); - return GetPostResDto.ofPostScraps(postScraps, size); - } - - @Transactional(readOnly = true) - public GetPostResDto searchPosts(GetPostSearchReqDto getPostSearchReqDto) { - Long boardId = getPostSearchReqDto.getBoardId(); - String keyword = getPostSearchReqDto.getKeyword(); - Long cursor = getPostSearchReqDto.getCursor(); - int size = getPostSearchReqDto.getSize(); - - if (!boardRepository.existsById(boardId)) { - throw new BoardException(BoardErrorInfo.NO_BOARD); - } - - List posts = postRepository.findWithDetailsFetchByBoardIdAndKeyword(boardId, keyword.replaceAll(" ", ""), cursor, size); - return GetPostResDto.ofPosts(posts, size); - } - - @Transactional(readOnly = true) - public GetPostResDto searchHotPosts(GetPostHotSearchReqDto getPostHotSearchReqDto) { - String keyword = getPostHotSearchReqDto.getKeyword(); - Long cursor = getPostHotSearchReqDto.getCursor(); - int size = getPostHotSearchReqDto.getSize(); - - List hotPosts = hotPostRepository.findHotPostsByKeyword(keyword.replaceAll(" ", ""), cursor, size); - return GetPostResDto.ofHotPosts(hotPosts, size); - } + + private final BoardRepository boardRepository; + private final MemberRepository memberRepository; + + private final PostRepository postRepository; + private final PostLikeRepository postLikeRepository; + private final HotPostRepository hotPostRepository; + private final PostScrapRepository postScrapRepository; + private final PostImageRepository postImageRepository; + private final AwsS3StorageService awsS3StorageService; + private final PostConstantProvider postConstantProvider; + + @Transactional(readOnly = true) + public GetPostResDto findPosts(GetPostReqDto getPostReqDto) { + Long boardId = getPostReqDto.getBoardId(); + Long cursor = getPostReqDto.getCursor(); + int size = getPostReqDto.getSize(); + + if (!boardRepository.existsById(boardId)) { + throw new BoardException(BoardErrorInfo.NO_BOARD); + } + + List posts = postRepository.findWithDetailsByBoardId(boardId, cursor, size); + return GetPostResDto.ofPosts(posts, size); + } + + @Transactional(readOnly = true) + public GetPostDetailResDto findPost(Long postId, Long loginMemberId) { + Post post = postRepository.findWithMemberAndPostImageFetchById(postId) + .orElseThrow(() -> new PostException(PostErrorInfo.NOT_FOUND_POST)); + + if (loginMemberId != null) { + Member member = memberRepository.findById(loginMemberId) + .orElseThrow(() -> new MemberException(MemberErrorInfo.MEMBER_NOT_FOUND_BY_ID)); + return GetPostDetailResDto.of(post, member); + } + + return GetPostDetailResDto.of(post, null); + } + + @Transactional + public PostCommonLikeResDto likePost(Long postId, Long loginMemberId) { + Member loginMember = memberRepository.findById(loginMemberId) + .orElseThrow(() -> new MemberException(MemberErrorInfo.MEMBER_NOT_FOUND_BY_ID)); + + Post post = postRepository.findById(postId) + .orElseThrow(() -> new PostException(PostErrorInfo.NOT_FOUND_POST)); + + PostLike postLike = postLikeRepository.findByPostIdAndMemberId(post.getId(), loginMember.getId()) + .orElse(null); + + Integer likeCount = postLikeRepository.countByPostId(post.getId()); + return togglePostLike(likeCount, post, loginMember, postLike); + } + + private PostCommonLikeResDto togglePostLike(Integer likeCount, Post post, Member loginMember, PostLike postLike) { + if (postLike != null) { + deleteLike(postLike); + return new PostCommonLikeResDto(likeCount - 1, false); + } + + saveLike(post, loginMember); + if (isSelectedHotPost(likeCount, post)) { + saveHotPost(post); + } + return new PostCommonLikeResDto(likeCount + 1, true); + } + + private void saveLike(Post post, Member loginMember) { + PostLike postLike = PostLike.of(post, loginMember); + postLikeRepository.save(postLike); + } + + private void deleteLike(PostLike postLike) { + postLikeRepository.delete(postLike); + } + + private boolean isSelectedHotPost(Integer likeCount, Post post) { + return likeCount + 1 == postConstantProvider.getHOT_POST_LIKES_THRESHOLD() && + !hotPostRepository.existsByPostId(post.getId()); + } + + private void saveHotPost(Post post) { + HotPost hotPost = HotPost.from(post); + hotPostRepository.save(hotPost); + } + + @Transactional + public void deleteHotPostsUnderThreshold(Long threshold) { + hotPostRepository.deleteHotPostsUnderThreshold(threshold); + } + + @Transactional + public PostPostScrapResDto scrapPost(Long postId, Long loginMemberId) { + Member loginMember = memberRepository.findById(loginMemberId) + .orElseThrow(() -> new MemberException(MemberErrorInfo.MEMBER_NOT_FOUND_BY_ID)); + + Post post = postRepository.findById(postId) + .orElseThrow(() -> new PostException(PostErrorInfo.NOT_FOUND_POST)); + + PostScrap postScrap = postScrapRepository.findByPostIdAndMemberId(post.getId(), loginMember.getId()) + .orElse(null); + + Integer scrapCount = postScrapRepository.countByPostId(post.getId()); + return togglePostScrap(scrapCount, post, loginMember, postScrap); + } + + private PostPostScrapResDto togglePostScrap(Integer scrapCount, Post post, Member loginMember, + PostScrap postScrap) { + if (postScrap != null) { + deleteScrapIfAlreadyExists(postScrap); + return new PostPostScrapResDto(scrapCount - 1, false); + } + saveScrap(post, loginMember); + return new PostPostScrapResDto(scrapCount + 1, true); + } + + private void saveScrap(Post post, Member loginMember) { + PostScrap postScrap = PostScrap.of(post, loginMember); + postScrapRepository.save(postScrap); + } + + private void deleteScrapIfAlreadyExists(PostScrap postScrap) { + postScrapRepository.delete(postScrap); + } + + @Transactional + public PostIdElement writePost(Long boardId, Long loginMemberId, PostPostWriteReqDto postPostWriteReqDto) { + Board board = boardRepository.findById(boardId) + .orElseThrow(() -> new BoardException(BoardErrorInfo.NO_BOARD)); + + Member loginMember = memberRepository.findById(loginMemberId) + .orElseThrow(() -> new MemberException(MemberErrorInfo.MEMBER_NOT_FOUND_BY_ID)); + + List images = postPostWriteReqDto.getImages(); + + Post post = Post.builder() + .board(board) + .member(loginMember) + .title(postPostWriteReqDto.getTitle()) + .content(postPostWriteReqDto.getContent()) + .anonymity(postPostWriteReqDto.isAnonymity()) + .build(); + postRepository.save(post); + + if (images.size() > 0) { + for (ImageInfo image : images) { + PostImage postImage = PostImage.builder() + .post(post) + .imagePath(image.getImagePath()) + .imageUrl(image.getImageUrl()) + .build(); + postImageRepository.save(postImage); + } + } + return new PostIdElement(post.getId()); + } + + @Transactional + public PostIdElement deletePost(Long postId, Long loginMemberId) { + Post post = postRepository.findByIdWithMember(postId) + .orElseThrow(() -> new PostException(PostErrorInfo.NOT_FOUND_POST)); + + Member loginMember = memberRepository.findById(loginMemberId) + .orElseThrow(() -> new MemberException(MemberErrorInfo.MEMBER_NOT_FOUND_BY_ID)); + + if (!post.getMember().getId().equals(loginMember.getId())) { + throw new PostException((PostErrorInfo.UNAUTHORIZED_DELETE_POST)); + } + + deleteAllPostImages(post.getImages()); + postRepository.delete(post); + hotPostRepository.findByPostId(postId).ifPresent(hotPostRepository::delete); + return new PostIdElement(postId); + } + + private void deleteAllPostImages(List images) { + images.forEach(image -> { + try { + awsS3StorageService.deleteObject(image); + + } catch (InfraException e) { + log.error("이미지 삭제에 오류가 발생했습니다."); + } + postImageRepository.delete(image); + }); + } + + @Transactional + public PostIdElement updatePost(Long postId, Long loginMemberId, PostPatchUpdateReqDto postPatchUpdateReqDto) { + Post post = postRepository.findWithMemberAndPostImageFetchById(postId) + .orElseThrow(() -> new PostException(PostErrorInfo.NOT_FOUND_POST)); + + Member loginMember = memberRepository.findById(loginMemberId) + .orElseThrow(() -> new MemberException(MemberErrorInfo.MEMBER_NOT_FOUND_BY_ID)); + + if (!post.getMember().getId().equals(loginMember.getId())) { + throw new PostException(PostErrorInfo.UNAUTHORIZED_UPDATE_POST); + } + + post.updatePost(postPatchUpdateReqDto.getTitle(), postPatchUpdateReqDto.getContent(), + postPatchUpdateReqDto.isAnonymity()); + postImageRepository.deleteAllInBatch(post.getImages()); + + List images = postPatchUpdateReqDto.getImages(); + if (images.size() > 0) { + for (ImageInfo image : images) { + PostImage postImage = PostImage.builder() + .post(post) + .imagePath(image.getImagePath()) + .imageUrl(image.getImageUrl()) + .build(); + postImageRepository.save(postImage); + } + } + return new PostIdElement(post.getId()); + } + + @Transactional(readOnly = true) + public GetPostResDto findHotPosts(GetPostHotReqDto getPostHotReqDto) { + Long cursor = getPostHotReqDto.getCursor(); + int size = getPostHotReqDto.getSize(); + + List hotPosts = hotPostRepository.findHotPosts(cursor, size); + return GetPostResDto.ofHotPosts(hotPosts, size); + } + + @Transactional(readOnly = true) + public GetPostResDto findMyPosts(GetPostMyReqDto getPostMyReqDto, Long loginMemberId) { + Long cursor = getPostMyReqDto.getCursor(); + int size = getPostMyReqDto.getSize(); + + Member loginMember = memberRepository.findById(loginMemberId) + .orElseThrow(() -> new MemberException(MemberErrorInfo.MEMBER_NOT_FOUND_BY_ID)); + + List posts = postRepository.findWithDetailsByMemberId(loginMember.getId(), cursor, size); + return GetPostResDto.ofPosts(posts, size); + } + + @Transactional(readOnly = true) + public GetPostResDto findMyScrapPosts(GetPostMyReqDto getPostMyScrapReqDto, Long loginMemberId) { + Long cursor = getPostMyScrapReqDto.getCursor(); + int size = getPostMyScrapReqDto.getSize(); + + Member loginMember = memberRepository.findById(loginMemberId) + .orElseThrow(() -> new MemberException(MemberErrorInfo.MEMBER_NOT_FOUND_BY_ID)); + + List postScraps = postScrapRepository.findMyScrapPosts(loginMember.getId(), cursor, size); + return GetPostResDto.ofPostScraps(postScraps, size); + } + + @Transactional(readOnly = true) + public GetPostResDto searchPosts(GetPostSearchReqDto getPostSearchReqDto) { + Long boardId = getPostSearchReqDto.getBoardId(); + String keyword = getPostSearchReqDto.getKeyword(); + Long cursor = getPostSearchReqDto.getCursor(); + int size = getPostSearchReqDto.getSize(); + + if (!boardRepository.existsById(boardId)) { + throw new BoardException(BoardErrorInfo.NO_BOARD); + } + + List posts = postRepository.findWithDetailsFetchByBoardIdAndKeyword(boardId, keyword.replaceAll(" ", ""), + cursor, size); + return GetPostResDto.ofPosts(posts, size); + } + + @Transactional(readOnly = true) + public GetPostResDto searchHotPosts(GetPostHotSearchReqDto getPostHotSearchReqDto) { + String keyword = getPostHotSearchReqDto.getKeyword(); + Long cursor = getPostHotSearchReqDto.getCursor(); + int size = getPostHotSearchReqDto.getSize(); + + List hotPosts = hotPostRepository.findHotPostsByKeyword(keyword.replaceAll(" ", ""), cursor, size); + return GetPostResDto.ofHotPosts(hotPosts, size); + } } diff --git a/src/main/java/com/ssafy/ssafsound/global/config/MagicNumberConfig.java b/src/main/java/com/ssafy/ssafsound/global/config/MagicNumberConfig.java index 890b7e6df..8fb96db6e 100644 --- a/src/main/java/com/ssafy/ssafsound/global/config/MagicNumberConfig.java +++ b/src/main/java/com/ssafy/ssafsound/global/config/MagicNumberConfig.java @@ -2,12 +2,13 @@ import com.ssafy.ssafsound.domain.member.service.MemberConstantProvider; import com.ssafy.ssafsound.domain.member.service.SemesterConstantProvider; +import com.ssafy.ssafsound.domain.post.service.PostConstantProvider; import lombok.RequiredArgsConstructor; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Configuration; @Configuration -@EnableConfigurationProperties({MemberConstantProvider.class, SemesterConstantProvider.class}) +@EnableConfigurationProperties({MemberConstantProvider.class, SemesterConstantProvider.class, PostConstantProvider.class}) @RequiredArgsConstructor public class MagicNumberConfig { } diff --git a/src/test/java/com/ssafy/ssafsound/domain/post/service/PostServiceTest.java b/src/test/java/com/ssafy/ssafsound/domain/post/service/PostServiceTest.java new file mode 100644 index 000000000..31a1834a9 --- /dev/null +++ b/src/test/java/com/ssafy/ssafsound/domain/post/service/PostServiceTest.java @@ -0,0 +1,478 @@ +package com.ssafy.ssafsound.domain.post.service; + +import static com.ssafy.ssafsound.global.util.fixture.PostFixture.*; +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.BDDMockito.*; + +import java.util.List; +import java.util.Optional; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.ssafy.ssafsound.domain.board.exception.BoardErrorInfo; +import com.ssafy.ssafsound.domain.board.exception.BoardException; +import com.ssafy.ssafsound.domain.board.repository.BoardRepository; +import com.ssafy.ssafsound.domain.member.domain.Member; +import com.ssafy.ssafsound.domain.member.exception.MemberErrorInfo; +import com.ssafy.ssafsound.domain.member.exception.MemberException; +import com.ssafy.ssafsound.domain.member.repository.MemberRepository; +import com.ssafy.ssafsound.domain.post.domain.Post; +import com.ssafy.ssafsound.domain.post.domain.PostLike; +import com.ssafy.ssafsound.domain.post.domain.PostScrap; +import com.ssafy.ssafsound.domain.post.dto.GetPostDetailResDto; +import com.ssafy.ssafsound.domain.post.dto.GetPostReqDto; +import com.ssafy.ssafsound.domain.post.dto.GetPostResDto; +import com.ssafy.ssafsound.domain.post.dto.PostCommonLikeResDto; +import com.ssafy.ssafsound.domain.post.dto.PostPostScrapResDto; +import com.ssafy.ssafsound.domain.post.exception.PostErrorInfo; +import com.ssafy.ssafsound.domain.post.exception.PostException; +import com.ssafy.ssafsound.domain.post.repository.HotPostRepository; +import com.ssafy.ssafsound.domain.post.repository.PostImageRepository; +import com.ssafy.ssafsound.domain.post.repository.PostLikeRepository; +import com.ssafy.ssafsound.domain.post.repository.PostRepository; +import com.ssafy.ssafsound.domain.post.repository.PostScrapRepository; +import com.ssafy.ssafsound.global.util.fixture.MemberFixture; +import com.ssafy.ssafsound.infra.storage.service.AwsS3StorageService; + +@ExtendWith(MockitoExtension.class) +class PostServiceTest { + + @Mock + private BoardRepository boardRepository; + + @Mock + private MemberRepository memberRepository; + + @Mock + private PostRepository postRepository; + + @Mock + private PostLikeRepository postLikeRepository; + + @Mock + private HotPostRepository hotPostRepository; + + @Mock + private PostScrapRepository postScrapRepository; + + @Mock + private PostImageRepository postImageRepository; + + @Mock + private AwsS3StorageService awsS3StorageService; + + @Mock + private PostConstantProvider postConstantProvider; + + @InjectMocks + private PostService postService; + + @Test + @DisplayName("유효한 boardId, cursor, size가 주어졌다면 게시글 목록 조회가 성공합니다.") + void Given_BoardIdAndCursorAndSize_When_findPosts_Then_Success() { + // given + GetPostReqDto getPostReqDto = GetPostReqDto.builder().boardId(1L).cursor(-1L).size(10).build(); + + List posts = List.of(POST_FIXTURE1, POST_FIXTURE2); + + given(boardRepository.existsById(getPostReqDto.getBoardId())).willReturn(true); + given(postRepository.findWithDetailsByBoardId(getPostReqDto.getBoardId(), getPostReqDto.getCursor(), + getPostReqDto.getSize())).willReturn(posts); + + // when + GetPostResDto response = postService.findPosts(getPostReqDto); + + // then + assertThat(response).usingRecursiveComparison() + .isEqualTo(GetPostResDto.ofPosts(posts, getPostReqDto.getSize())); + + // verify + verify(boardRepository, times(1)).existsById(getPostReqDto.getBoardId()); + verify(postRepository, times(1)).findWithDetailsByBoardId(getPostReqDto.getBoardId(), getPostReqDto.getCursor(), + getPostReqDto.getSize()); + } + + @Test + @DisplayName("유효하지 않은 boardId가 주어졌다면 게시글 목록 조회에 예외를 발생합니다.") + void Given_BoardId_When_findPosts_Then_Fail() { + // given + GetPostReqDto getPostReqDto = GetPostReqDto.builder().boardId(100L).cursor(-1L).size(10).build(); + + given(boardRepository.existsById(getPostReqDto.getBoardId())).willReturn(false); + + // when, then + BoardException exception = assertThrows(BoardException.class, () -> postService.findPosts(getPostReqDto)); + assertEquals(BoardErrorInfo.NO_BOARD, exception.getInfo()); + + // verify + verify(boardRepository, times(1)).existsById(getPostReqDto.getBoardId()); + } + + @Test + @DisplayName("로그인 시 유효한 postId와 loginMemberId가 주어졌다면 게시글 상세 보기가 성공합니다.") + void Given_PostIdAndLoginMemberId_When_findPost_Then_Success() { + // given + Post post = POST_FIXTURE1; + Member member = MemberFixture.GENERAL_MEMBER; + + given(postRepository.findWithMemberAndPostImageFetchById(post.getId())).willReturn(Optional.of(post)); + given(memberRepository.findById(member.getId())).willReturn(Optional.of(member)); + + // when + GetPostDetailResDto response = postService.findPost(post.getId(), member.getId()); + + // then + assertThat(response).usingRecursiveComparison().isEqualTo(GetPostDetailResDto.of(post, member)); + + // verify + verify(postRepository, times(1)).findWithMemberAndPostImageFetchById(post.getId()); + verify(memberRepository, times(1)).findById(member.getId()); + } + + @Test + @DisplayName("비 로그인 시 유효한 postId가 주어졌다면 게시글 상세 보기가 성공합니다.") + void Given_PostId_When_findPost_Then_Success() { + // given + Post post = POST_FIXTURE1; + + given(postRepository.findWithMemberAndPostImageFetchById(post.getId())).willReturn(Optional.of(post)); + + // when + GetPostDetailResDto response = postService.findPost(post.getId(), null); + + // then + assertThat(response).usingRecursiveComparison().isEqualTo(GetPostDetailResDto.of(post, null)); + + // verify + verify(postRepository, times(1)).findWithMemberAndPostImageFetchById(post.getId()); + + } + + @Test + @DisplayName("유효하지 않은 postId가 주어졌다면 게시글 상세보기에 예외를 발생합니다.") + void Given_InvalidPostId_When_findPost_Then_Success() { + // given + Long postId = 100L; + + given(postRepository.findWithMemberAndPostImageFetchById(postId)).willReturn(Optional.empty()); + + // when, then + PostException exception = assertThrows(PostException.class, () -> postService.findPost(postId, null)); + assertEquals(PostErrorInfo.NOT_FOUND_POST, exception.getInfo()); + + // verify + verify(postRepository, times(1)).findWithMemberAndPostImageFetchById(postId); + } + + @Test + @DisplayName("로그인 시 유효하지 않은 loginMemberId가 주어졌다면 게시글 상세보기에 예외를 발생합니다.") + void Given_InvalidLoginMemberId_When_findPost_Then_Success() { + // given + Post post = POST_FIXTURE1; + Long memberId = 100L; + + given(postRepository.findWithMemberAndPostImageFetchById(post.getId())).willReturn(Optional.of(post)); + given(memberRepository.findById(memberId)).willReturn(Optional.empty()); + + // when, then + MemberException exception = assertThrows(MemberException.class, + () -> postService.findPost(post.getId(), memberId)); + assertEquals(MemberErrorInfo.MEMBER_NOT_FOUND_BY_ID, exception.getInfo()); + + // verify + verify(postRepository, times(1)).findWithMemberAndPostImageFetchById(post.getId()); + verify(memberRepository, times(1)).findById(memberId); + } + + @Test + @DisplayName("게시글을 좋아요 하지 않았다면 좋아요가 저장됩니다.") + void Given_PostIdAndLoginMemberId_When_SaveLikePost_Then_Success() { + // given + Post post = POST_FIXTURE1; + Member member = MemberFixture.GENERAL_MEMBER; + int likeCount = 4; + + given(postRepository.findById(post.getId())).willReturn(Optional.of(post)); + given(memberRepository.findById(member.getId())).willReturn(Optional.of(member)); + given(postLikeRepository.findByPostIdAndMemberId(post.getId(), member.getId())).willReturn(Optional.empty()); + given(postLikeRepository.countByPostId(post.getId())).willReturn(likeCount); + given(postConstantProvider.getHOT_POST_LIKES_THRESHOLD()).willReturn(10L); + + // when + PostCommonLikeResDto response = postService.likePost(post.getId(), member.getId()); + + // then + assertThat(response).usingRecursiveComparison().isEqualTo(new PostCommonLikeResDto(likeCount + 1, true)); + + // verify + verify(postRepository, times(1)).findById(post.getId()); + verify(memberRepository, times(1)).findById(member.getId()); + verify(postLikeRepository, times(1)).findByPostIdAndMemberId(post.getId(), member.getId()); + verify(postLikeRepository, times(1)).countByPostId(post.getId()); + verify(postConstantProvider, times(1)).getHOT_POST_LIKES_THRESHOLD(); + verify(postLikeRepository, times(1)).save(any()); + + verify(hotPostRepository, times(0)).save(any()); + } + + @Test + @DisplayName("게시글을 이미 좋아요 했다면 좋아요가 취소됩니다.") + void Given_PostIdAndLoginMemberId_When_DeleteLikePost_Then_Success() { + // given + Post post = POST_FIXTURE1; + Member member = MemberFixture.GENERAL_MEMBER; + PostLike postLike = PostLike.of(post, member); + int likeCount = 4; + + given(postRepository.findById(post.getId())).willReturn(Optional.of(post)); + given(memberRepository.findById(member.getId())).willReturn(Optional.of(member)); + given(postLikeRepository.findByPostIdAndMemberId(post.getId(), member.getId())).willReturn( + Optional.of(postLike)); + given(postLikeRepository.countByPostId(post.getId())).willReturn(likeCount); + + // when + PostCommonLikeResDto response = postService.likePost(post.getId(), member.getId()); + + // then + assertThat(response).usingRecursiveComparison().isEqualTo(new PostCommonLikeResDto(likeCount - 1, false)); + + // verify + verify(postRepository, times(1)).findById(post.getId()); + verify(memberRepository, times(1)).findById(member.getId()); + verify(postLikeRepository, times(1)).findByPostIdAndMemberId(post.getId(), member.getId()); + verify(postLikeRepository, times(1)).countByPostId(post.getId()); + verify(postLikeRepository, times(1)).delete(any()); + } + + @Test + @DisplayName("좋아요가 특정 개수를 달성했다면 Hot 게시글로 등록됩니다.") + void Given_PostIdAndLoginMemberId_When_NotExistsHotPost_Then_Success() { + // given + Post post = POST_FIXTURE1; + Member member = MemberFixture.GENERAL_MEMBER; + int likeCount = 9; + + given(postRepository.findById(post.getId())).willReturn(Optional.of(post)); + given(memberRepository.findById(member.getId())).willReturn(Optional.of(member)); + given(postLikeRepository.findByPostIdAndMemberId(post.getId(), member.getId())).willReturn(Optional.empty()); + given(postLikeRepository.countByPostId(post.getId())).willReturn(likeCount); + given(postConstantProvider.getHOT_POST_LIKES_THRESHOLD()).willReturn(10L); + given(hotPostRepository.existsByPostId(post.getId())).willReturn(false); + + // when + PostCommonLikeResDto response = postService.likePost(post.getId(), member.getId()); + + // then + assertThat(response).usingRecursiveComparison().isEqualTo(new PostCommonLikeResDto(likeCount + 1, true)); + + // verify + verify(postRepository, times(1)).findById(post.getId()); + verify(memberRepository, times(1)).findById(member.getId()); + verify(postLikeRepository, times(1)).findByPostIdAndMemberId(post.getId(), member.getId()); + verify(postLikeRepository, times(1)).countByPostId(post.getId()); + verify(postConstantProvider, times(1)).getHOT_POST_LIKES_THRESHOLD(); + verify(hotPostRepository, times(1)).existsByPostId(post.getId()); + verify(hotPostRepository, times(1)).save(any()); + } + + @Test + @DisplayName("좋아요가 특정 개수를 달성했지만 이미 Hot 게시글이라면 등록되지 않습니다.") + void Given_PostIdAndLoginMemberId_When_ExistsHotPost_Then_Success() { + // given + Post post = POST_FIXTURE1; + Member member = MemberFixture.GENERAL_MEMBER; + int likeCount = 9; + + given(postRepository.findById(post.getId())).willReturn(Optional.of(post)); + given(memberRepository.findById(member.getId())).willReturn(Optional.of(member)); + given(postLikeRepository.findByPostIdAndMemberId(post.getId(), member.getId())).willReturn(Optional.empty()); + given(postLikeRepository.countByPostId(post.getId())).willReturn(likeCount); + given(postConstantProvider.getHOT_POST_LIKES_THRESHOLD()).willReturn(10L); + given(hotPostRepository.existsByPostId(post.getId())).willReturn(true); + + // when + PostCommonLikeResDto response = postService.likePost(post.getId(), member.getId()); + + // then + assertThat(response).usingRecursiveComparison().isEqualTo(new PostCommonLikeResDto(likeCount + 1, true)); + + // verify + verify(postRepository, times(1)).findById(post.getId()); + verify(memberRepository, times(1)).findById(member.getId()); + verify(postLikeRepository, times(1)).findByPostIdAndMemberId(post.getId(), member.getId()); + verify(postLikeRepository, times(1)).countByPostId(post.getId()); + verify(postConstantProvider, times(1)).getHOT_POST_LIKES_THRESHOLD(); + verify(hotPostRepository, times(1)).existsByPostId(post.getId()); + + verify(hotPostRepository, times(0)).save(any()); + } + + @Test + @DisplayName("유효하지 않은 loginMemberId가 주어졌다면 게시글 좋아요에 예외를 발생합니다.") + void Given_InvalidLoginMemberId_When_likePost_Then_Success() { + // given + Post post = POST_FIXTURE1; + Long memberId = -1L; + + given(memberRepository.findById(memberId)).willReturn(Optional.empty()); + + // when, then + MemberException exception = assertThrows(MemberException.class, + () -> postService.likePost(post.getId(), memberId)); + assertEquals(MemberErrorInfo.MEMBER_NOT_FOUND_BY_ID, exception.getInfo()); + + // verify + verify(memberRepository, times(1)).findById(memberId); + } + + @Test + @DisplayName("유효하지 않은 postId가 주어졌다면 게시글 좋아요에 예외를 발생합니다.") + void Given_InvalidPostId_When_likePost_Then_Success() { + // given + Long postId = -1L; + Member member = MemberFixture.GENERAL_MEMBER; + + given(memberRepository.findById(member.getId())).willReturn(Optional.of(member)); + given(postRepository.findById(postId)).willReturn(Optional.empty()); + + // when, then + PostException exception = assertThrows(PostException.class, () -> postService.likePost(postId, member.getId())); + assertEquals(PostErrorInfo.NOT_FOUND_POST, exception.getInfo()); + + // verify + verify(memberRepository, times(1)).findById(member.getId()); + verify(postRepository, times(1)).findById(postId); + } + + @Test + @DisplayName("게시글을 스크랩 하지 않았다면 스크랩이 저장됩니다.") + void Given_PostIdAndLoginMemberId_When_SaveScrapPost_Then_Success() { + // given + Member member = MemberFixture.GENERAL_MEMBER; + Post post = POST_FIXTURE1; + int scrapCount = 10; + + given(memberRepository.findById(member.getId())).willReturn(Optional.of(member)); + given(postRepository.findById(post.getId())).willReturn(Optional.of(post)); + given(postScrapRepository.findByPostIdAndMemberId(post.getId(), member.getId())).willReturn(Optional.empty()); + given(postScrapRepository.countByPostId(post.getId())).willReturn(scrapCount); + + // when + PostPostScrapResDto response = postService.scrapPost(post.getId(), member.getId()); + + // then + assertThat(response).usingRecursiveComparison().isEqualTo(new PostPostScrapResDto(scrapCount + 1, true)); + + // verify + verify(memberRepository, times(1)).findById(member.getId()); + verify(postRepository, times(1)).findById(post.getId()); + verify(postScrapRepository, times(1)).findByPostIdAndMemberId(post.getId(), member.getId()); + verify(postScrapRepository, times(1)).countByPostId(post.getId()); + verify(postScrapRepository, times(1)).save(any()); + } + + @Test + @DisplayName("게시글을 이미 스크랩 했다면 스크랩이 취소됩니다.") + void Given_PostIdAndLoginMemberId_When_DeleteScrapPost_Then_Success() { + // given + Member member = MemberFixture.GENERAL_MEMBER; + Post post = POST_FIXTURE1; + PostScrap postScrap = PostScrap.of(post, member); + int scrapCount = 10; + + given(memberRepository.findById(member.getId())).willReturn(Optional.of(member)); + given(postRepository.findById(post.getId())).willReturn(Optional.of(post)); + given(postScrapRepository.findByPostIdAndMemberId(post.getId(), member.getId())).willReturn( + Optional.of(postScrap)); + given(postScrapRepository.countByPostId(post.getId())).willReturn(scrapCount); + + // when + PostPostScrapResDto response = postService.scrapPost(post.getId(), member.getId()); + + // then + assertThat(response).usingRecursiveComparison().isEqualTo(new PostPostScrapResDto(scrapCount - 1, false)); + + // verify + verify(memberRepository, times(1)).findById(member.getId()); + verify(postRepository, times(1)).findById(post.getId()); + verify(postScrapRepository, times(1)).findByPostIdAndMemberId(post.getId(), member.getId()); + verify(postScrapRepository, times(1)).countByPostId(post.getId()); + verify(postScrapRepository, times(1)).delete(any()); + } + + @Test + @DisplayName("유효하지 않은 loginMemberId가 주어졌다면 게시글 스크랩에 예외를 발생합니다.") + void Given_InvalidLoginMemberId_When_scrapPost_Then_Success() { + // given + Long memberId = -101L; + Post post = POST_FIXTURE1; + + given(memberRepository.findById(memberId)).willReturn(Optional.empty()); + + // when, then + MemberException exception = assertThrows(MemberException.class, + () -> postService.scrapPost(post.getId(), memberId)); + assertEquals(MemberErrorInfo.MEMBER_NOT_FOUND_BY_ID, exception.getInfo()); + + // verify + verify(memberRepository, times(1)).findById(memberId); + } + + @Test + @DisplayName("유효하지 않은 postId가 주어졌다면 게시글 스크랩에 예외를 발생합니다.") + void Given_InvalidPostId_When_scrapPost_Then_Success() { + // given + Member member = MemberFixture.MEMBER_JAMES; + Long postId = 101021242313L; + + given(memberRepository.findById(member.getId())).willReturn(Optional.of(member)); + given(postRepository.findById(postId)).willReturn(Optional.empty()); + + // when, then + PostException exception = assertThrows(PostException.class, + () -> postService.scrapPost(postId, member.getId())); + assertEquals(PostErrorInfo.NOT_FOUND_POST, exception.getInfo()); + + // verify + verify(memberRepository, times(1)).findById(member.getId()); + verify(postRepository, times(1)).findById(postId); + } + + @Test + void writePost() { + } + + @Test + void deletePost() { + } + + @Test + void updatePost() { + } + + @Test + void findHotPosts() { + } + + @Test + void findMyPosts() { + } + + @Test + void findMyScrapPosts() { + } + + @Test + void searchPosts() { + } + + @Test + void searchHotPosts() { + } +} \ No newline at end of file