diff --git a/src/main/java/com/example/solidconnection/application/domain/Application.java b/src/main/java/com/example/solidconnection/application/domain/Application.java index b16e6433f..44dcc52f1 100644 --- a/src/main/java/com/example/solidconnection/application/domain/Application.java +++ b/src/main/java/com/example/solidconnection/application/domain/Application.java @@ -54,6 +54,9 @@ public class Application { @ManyToOne private UniversityInfoForApply secondChoiceUniversity; + @ManyToOne + private UniversityInfoForApply thirdChoiceUniversity; + @ManyToOne private SiteUser siteUser; @@ -77,12 +80,14 @@ public void updateGpaAndLanguageTest( public void updateUniversityChoice( UniversityInfoForApply firstChoiceUniversity, UniversityInfoForApply secondChoiceUniversity, + UniversityInfoForApply thirdChoiceUniversity, String nicknameForApply) { if (this.firstChoiceUniversity != null) { this.updateCount++; } this.firstChoiceUniversity = firstChoiceUniversity; this.secondChoiceUniversity = secondChoiceUniversity; + this.thirdChoiceUniversity = thirdChoiceUniversity; this.nicknameForApply = nicknameForApply; } } diff --git a/src/main/java/com/example/solidconnection/application/dto/ApplicationsResponse.java b/src/main/java/com/example/solidconnection/application/dto/ApplicationsResponse.java index fb93b7ff5..2e3025137 100644 --- a/src/main/java/com/example/solidconnection/application/dto/ApplicationsResponse.java +++ b/src/main/java/com/example/solidconnection/application/dto/ApplicationsResponse.java @@ -12,5 +12,8 @@ public record ApplicationsResponse( List firstChoice, @ArraySchema(arraySchema = @Schema(description = "2지망 대학에 지원한 지원자 목록")) - List secondChoice) { + List secondChoice, + + @ArraySchema(arraySchema = @Schema(description = "3지망 대학에 지원한 지원자 목록")) + List thirdChoice) { } diff --git a/src/main/java/com/example/solidconnection/application/dto/UniversityChoiceRequest.java b/src/main/java/com/example/solidconnection/application/dto/UniversityChoiceRequest.java index b179404c8..a76799571 100644 --- a/src/main/java/com/example/solidconnection/application/dto/UniversityChoiceRequest.java +++ b/src/main/java/com/example/solidconnection/application/dto/UniversityChoiceRequest.java @@ -11,5 +11,7 @@ public record UniversityChoiceRequest( Long firstChoiceUniversityId, @Schema(description = "2지망 대학교의 지원 정보 ID (선택사항)", example = "2", nullable = true) - Long secondChoiceUniversityId) { -} + Long secondChoiceUniversityId, + + @Schema(description = "3지망 대학교의 지원 정보 ID (선택사항)", example = "3", nullable = true) + Long thirdChoiceUniversityId) {} diff --git a/src/main/java/com/example/solidconnection/application/repository/ApplicationRepository.java b/src/main/java/com/example/solidconnection/application/repository/ApplicationRepository.java index 3cbf7c88f..34b714438 100644 --- a/src/main/java/com/example/solidconnection/application/repository/ApplicationRepository.java +++ b/src/main/java/com/example/solidconnection/application/repository/ApplicationRepository.java @@ -26,6 +26,8 @@ public interface ApplicationRepository extends JpaRepository List findAllBySecondChoiceUniversityAndVerifyStatus(UniversityInfoForApply secondChoiceUniversity, VerifyStatus verifyStatus); + List findAllByThirdChoiceUniversityAndVerifyStatus(UniversityInfoForApply thirdChoiceUniversity, VerifyStatus verifyStatus); + default Application getApplicationBySiteUser(SiteUser siteUser) { return findBySiteUser(siteUser) .orElseThrow(() -> new CustomException(APPLICATION_NOT_FOUND)); diff --git a/src/main/java/com/example/solidconnection/application/service/ApplicationQueryService.java b/src/main/java/com/example/solidconnection/application/service/ApplicationQueryService.java index 104114c5c..793cf28ca 100644 --- a/src/main/java/com/example/solidconnection/application/service/ApplicationQueryService.java +++ b/src/main/java/com/example/solidconnection/application/service/ApplicationQueryService.java @@ -55,7 +55,8 @@ public ApplicationsResponse getApplicants(String email, String regionCode, Strin // 1지망, 2지망 지원자들을 조회한다. List firstChoiceApplicants = getFirstChoiceApplicants(universities, siteUser); List secondChoiceApplicants = getSecondChoiceApplicants(universities, siteUser); - return new ApplicationsResponse(firstChoiceApplicants, secondChoiceApplicants); + List thirdChoiceApplicants = getThirdChoiceApplicants(universities, siteUser); + return new ApplicationsResponse(firstChoiceApplicants, secondChoiceApplicants, thirdChoiceApplicants); } private void validateSiteUserCanViewApplicants(SiteUser siteUser) { @@ -81,6 +82,14 @@ private List getSecondChoiceApplicants(List getThirdChoiceApplicants(List universities, SiteUser siteUser) { + return getApplicantsByChoice( + universities, + siteUser, + uia -> applicationRepository.findAllByThirdChoiceUniversityAndVerifyStatus(uia, VerifyStatus.APPROVED) + ); + } + private List getApplicantsByChoice( List searchedUniversities, SiteUser siteUser, diff --git a/src/main/java/com/example/solidconnection/application/service/ApplicationSubmissionService.java b/src/main/java/com/example/solidconnection/application/service/ApplicationSubmissionService.java index dc56e6da3..beec76a05 100644 --- a/src/main/java/com/example/solidconnection/application/service/ApplicationSubmissionService.java +++ b/src/main/java/com/example/solidconnection/application/service/ApplicationSubmissionService.java @@ -16,7 +16,9 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.HashSet; import java.util.Objects; +import java.util.Set; import static com.example.solidconnection.custom.exception.ErrorCode.APPLY_UPDATE_LIMIT_EXCEED; import static com.example.solidconnection.custom.exception.ErrorCode.CANT_APPLY_FOR_SAME_UNIVERSITY; @@ -61,7 +63,7 @@ public boolean submitScore(String email, ScoreRequest scoreRequest) { /* * 지망 대학교를 제출한다. - * - 첫번째 지망과 두번째 지망이 같은지 검증한다. + * - 지망 대학중 중복된 대학교가 있는지 검증한다. * - 지원 정보 제출 내역이 없다면, 지금의 프로세스(성적 제출 후 지망대학 제출)에 벗어나는 요청이므로 예외를 응답한다. * - 기존에 제출한 적이 있다면, 수정한다. * - 수정 횟수 제한을 초과하지 않았는지 검증한다. @@ -70,7 +72,7 @@ public boolean submitScore(String email, ScoreRequest scoreRequest) { * */ @Transactional public boolean submitUniversityChoice(String email, UniversityChoiceRequest universityChoiceRequest) { - validateFirstAndSecondChoiceIdDifferent(universityChoiceRequest); + validateNoDuplicateUniversityChoices(universityChoiceRequest); Application application = applicationRepository.findBySiteUser_Email(email) .orElseThrow(() -> new CustomException(SCORE_SHOULD_SUBMITTED_FIRST)); @@ -78,9 +80,11 @@ public boolean submitUniversityChoice(String email, UniversityChoiceRequest univ .getUniversityInfoForApplyByIdAndTerm(universityChoiceRequest.firstChoiceUniversityId(), term); UniversityInfoForApply secondChoiceUniversity = universityInfoForApplyRepository .getUniversityInfoForApplyByIdAndTerm(universityChoiceRequest.secondChoiceUniversityId(), term); + UniversityInfoForApply thirdChoiceUniversity = universityInfoForApplyRepository + .getUniversityInfoForApplyByIdAndTerm(universityChoiceRequest.thirdChoiceUniversityId(), term); validateUpdateLimitNotExceed(application); - application.updateUniversityChoice(firstChoiceUniversity, secondChoiceUniversity, getRandomNickname()); + application.updateUniversityChoice(firstChoiceUniversity, secondChoiceUniversity, thirdChoiceUniversity, getRandomNickname()); return true; } @@ -98,10 +102,14 @@ private void validateUpdateLimitNotExceed(Application application) { } } - private void validateFirstAndSecondChoiceIdDifferent(UniversityChoiceRequest universityChoiceRequest) { - if (Objects.equals( - universityChoiceRequest.firstChoiceUniversityId(), - universityChoiceRequest.secondChoiceUniversityId())) { + private void validateNoDuplicateUniversityChoices(UniversityChoiceRequest universityChoiceRequest) { + Set uniqueUniversityIds = new HashSet<>(); + + uniqueUniversityIds.add(universityChoiceRequest.firstChoiceUniversityId()); + uniqueUniversityIds.add(universityChoiceRequest.secondChoiceUniversityId()); + uniqueUniversityIds.add(universityChoiceRequest.thirdChoiceUniversityId()); + + if (uniqueUniversityIds.size() < 3) { throw new CustomException(CANT_APPLY_FOR_SAME_UNIVERSITY); } } diff --git a/src/main/java/com/example/solidconnection/board/service/BoardService.java b/src/main/java/com/example/solidconnection/board/service/BoardService.java index 3a74b919c..1ec5ac8b0 100644 --- a/src/main/java/com/example/solidconnection/board/service/BoardService.java +++ b/src/main/java/com/example/solidconnection/board/service/BoardService.java @@ -9,12 +9,15 @@ import com.example.solidconnection.type.BoardCode; import com.example.solidconnection.type.PostCategory; import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.EnumUtils; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.stream.Collectors; +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_POST_CATEGORY; + @Service @RequiredArgsConstructor public class BoardService { @@ -28,12 +31,11 @@ private String validateCode(String code) { } } - private PostCategory validatePostCategory(String postCategory) { - try { - return PostCategory.valueOf(postCategory); - } catch (IllegalArgumentException ex) { - throw new CustomException(ErrorCode.INVALID_POST_CATEGORY); + private PostCategory validatePostCategory(String category){ + if(!EnumUtils.isValidEnum(PostCategory.class, category)){ + throw new CustomException(INVALID_POST_CATEGORY); } + return PostCategory.valueOf(category); } @Transactional(readOnly = true) diff --git a/src/main/java/com/example/solidconnection/comment/controller/CommentController.java b/src/main/java/com/example/solidconnection/comment/controller/CommentController.java new file mode 100644 index 000000000..61bae1036 --- /dev/null +++ b/src/main/java/com/example/solidconnection/comment/controller/CommentController.java @@ -0,0 +1,62 @@ +package com.example.solidconnection.comment.controller; + +import com.example.solidconnection.comment.dto.*; +import com.example.solidconnection.comment.service.CommentService; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.security.SecurityRequirements; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.security.Principal; + +import static com.example.solidconnection.config.swagger.SwaggerConfig.ACCESS_TOKEN; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/posts") +@SecurityRequirements +@SecurityRequirement(name = ACCESS_TOKEN) +public class CommentController { + + private final CommentService commentService; + + @PostMapping("/{post_id}/comments") + public ResponseEntity createComment( + Principal principal, + @PathVariable("post_id") Long postId, + @Valid @RequestBody CommentCreateRequest commentCreateRequest + ) { + + CommentCreateResponse commentCreateResponse = commentService.createComment( + principal.getName(), postId, commentCreateRequest); + return ResponseEntity.ok().body(commentCreateResponse); + } + + @PatchMapping("/{post_id}/comments/{comment_id}") + public ResponseEntity updateComment( + Principal principal, + @PathVariable("post_id") Long postId, + @PathVariable("comment_id") Long commentId, + @Valid @RequestBody CommentUpdateRequest commentUpdateRequest + ) { + + CommentUpdateResponse commentUpdateResponse = commentService.updateComment( + principal.getName(), postId, commentId, commentUpdateRequest + ); + return ResponseEntity.ok().body(commentUpdateResponse); + } + + @DeleteMapping("/{post_id}/comments/{comment_id}") + public ResponseEntity deleteCommentById( + Principal principal, + @PathVariable("post_id") Long postId, + @PathVariable("comment_id") Long commentId + ) { + + CommentDeleteResponse commentDeleteResponse = commentService.deleteCommentById(principal.getName(), postId, commentId); + return ResponseEntity.ok().body(commentDeleteResponse); + } + +} diff --git a/src/main/java/com/example/solidconnection/comment/domain/Comment.java b/src/main/java/com/example/solidconnection/comment/domain/Comment.java new file mode 100644 index 000000000..774c01123 --- /dev/null +++ b/src/main/java/com/example/solidconnection/comment/domain/Comment.java @@ -0,0 +1,111 @@ +package com.example.solidconnection.comment.domain; + +import com.example.solidconnection.entity.common.BaseEntity; +import com.example.solidconnection.post.domain.Post; +import com.example.solidconnection.siteuser.domain.SiteUser; +import jakarta.persistence.*; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +@Entity +@Getter +@NoArgsConstructor +@EqualsAndHashCode(of = "id") +public class Comment extends BaseEntity { + + // for recursive query + @Transient + private int level; + + @Transient + private String path; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(length = 255) + private String content; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "post_id") + private Post post; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "site_user_id") + private SiteUser siteUser; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "parent_id") + private Comment parentComment; + + @OneToMany(mappedBy = "parentComment", cascade = CascadeType.ALL) + private List commentList = new ArrayList<>(); + + public Comment(String content) { + this.content = content; + } + + public void setParentCommentAndPostAndSiteUser(Comment parentComment, Post post, SiteUser siteUser) { + + if (this.parentComment != null) { + this.parentComment.getCommentList().remove(this); + } + this.parentComment = parentComment; + parentComment.getCommentList().add(this); + + if (this.post != null) { + this.post.getCommentList().remove(this); + } + this.post = post; + post.getCommentList().add(this); + + if (this.siteUser != null) { + this.siteUser.getCommentList().remove(this); + } + this.siteUser = siteUser; + siteUser.getCommentList().add(this); + } + + public void setPostAndSiteUser(Post post, SiteUser siteUser) { + + if (this.post != null) { + this.post.getCommentList().remove(this); + } + this.post = post; + post.getCommentList().add(this); + + if (this.siteUser != null) { + this.siteUser.getCommentList().remove(this); + } + this.siteUser = siteUser; + siteUser.getCommentList().add(this); + } + + public void resetPostAndSiteUserAndParentComment() { + if (this.post != null) { + this.post.getCommentList().remove(this); + this.post = null; + } + if (this.siteUser != null) { + this.siteUser.getCommentList().remove(this); + this.siteUser = null; + } + if (this.parentComment != null) { + this.parentComment.getCommentList().remove(this); + this.parentComment = null; + } + } + + public void updateContent(String content) { + this.content = content; + } + + public void deprecateComment() { + this.content = null; + } +} diff --git a/src/main/java/com/example/solidconnection/comment/dto/CommentCreateRequest.java b/src/main/java/com/example/solidconnection/comment/dto/CommentCreateRequest.java new file mode 100644 index 000000000..8cf57e360 --- /dev/null +++ b/src/main/java/com/example/solidconnection/comment/dto/CommentCreateRequest.java @@ -0,0 +1,28 @@ +package com.example.solidconnection.comment.dto; + +import com.example.solidconnection.comment.domain.Comment; +import com.example.solidconnection.post.domain.Post; +import com.example.solidconnection.siteuser.domain.SiteUser; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +public record CommentCreateRequest( + @NotBlank(message = "댓글 내용은 빈 값일 수 없습니다.") + @Size(min = 1, max = 255, message = "댓글 내용은 최소 1자 이상, 최대 255자 이하여야 합니다.") + String content, + Long parentId +) { + public Comment toEntity(SiteUser siteUser, Post post, Comment parentComment) { + + Comment comment = new Comment( + this.content + ); + + if (parentComment == null) { + comment.setPostAndSiteUser(post, siteUser); + } else { + comment.setParentCommentAndPostAndSiteUser(parentComment, post, siteUser); + } + return comment; + } +} diff --git a/src/main/java/com/example/solidconnection/comment/dto/CommentCreateResponse.java b/src/main/java/com/example/solidconnection/comment/dto/CommentCreateResponse.java new file mode 100644 index 000000000..60d7529c2 --- /dev/null +++ b/src/main/java/com/example/solidconnection/comment/dto/CommentCreateResponse.java @@ -0,0 +1,14 @@ +package com.example.solidconnection.comment.dto; + +import com.example.solidconnection.comment.domain.Comment; + +public record CommentCreateResponse( + Long id +) { + + public static CommentCreateResponse from(Comment comment) { + return new CommentCreateResponse( + comment.getId() + ); + } +} diff --git a/src/main/java/com/example/solidconnection/comment/dto/CommentDeleteResponse.java b/src/main/java/com/example/solidconnection/comment/dto/CommentDeleteResponse.java new file mode 100644 index 000000000..393e4fe8b --- /dev/null +++ b/src/main/java/com/example/solidconnection/comment/dto/CommentDeleteResponse.java @@ -0,0 +1,6 @@ +package com.example.solidconnection.comment.dto; + +public record CommentDeleteResponse( + Long id +) { +} diff --git a/src/main/java/com/example/solidconnection/comment/dto/CommentUpdateRequest.java b/src/main/java/com/example/solidconnection/comment/dto/CommentUpdateRequest.java new file mode 100644 index 000000000..23ae16118 --- /dev/null +++ b/src/main/java/com/example/solidconnection/comment/dto/CommentUpdateRequest.java @@ -0,0 +1,12 @@ +package com.example.solidconnection.comment.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +public record CommentUpdateRequest( + @NotBlank(message = "댓글 내용은 빈 값일 수 없습니다.") + @Size(min = 1, max = 255, message = "댓글 내용은 최소 1자 이상, 최대 255자 이하여야 합니다.") + String content +) { + +} diff --git a/src/main/java/com/example/solidconnection/comment/dto/CommentUpdateResponse.java b/src/main/java/com/example/solidconnection/comment/dto/CommentUpdateResponse.java new file mode 100644 index 000000000..b621ab111 --- /dev/null +++ b/src/main/java/com/example/solidconnection/comment/dto/CommentUpdateResponse.java @@ -0,0 +1,14 @@ +package com.example.solidconnection.comment.dto; + +import com.example.solidconnection.comment.domain.Comment; + +public record CommentUpdateResponse( + Long id +) { + + public static CommentUpdateResponse from(Comment comment) { + return new CommentUpdateResponse( + comment.getId() + ); + } +} diff --git a/src/main/java/com/example/solidconnection/comment/dto/PostFindCommentResponse.java b/src/main/java/com/example/solidconnection/comment/dto/PostFindCommentResponse.java index 75414f943..8524eb95a 100644 --- a/src/main/java/com/example/solidconnection/comment/dto/PostFindCommentResponse.java +++ b/src/main/java/com/example/solidconnection/comment/dto/PostFindCommentResponse.java @@ -1,6 +1,6 @@ package com.example.solidconnection.comment.dto; -import com.example.solidconnection.entity.Comment; +import com.example.solidconnection.comment.domain.Comment; import com.example.solidconnection.siteuser.dto.PostFindSiteUserResponse; import java.time.LocalDateTime; diff --git a/src/main/java/com/example/solidconnection/comment/repository/CommentRepository.java b/src/main/java/com/example/solidconnection/comment/repository/CommentRepository.java index 0b0d7152c..b78011903 100644 --- a/src/main/java/com/example/solidconnection/comment/repository/CommentRepository.java +++ b/src/main/java/com/example/solidconnection/comment/repository/CommentRepository.java @@ -1,12 +1,14 @@ package com.example.solidconnection.comment.repository; -import com.example.solidconnection.entity.Comment; +import com.example.solidconnection.comment.domain.Comment; +import com.example.solidconnection.custom.exception.CustomException; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import java.util.List; +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_COMMENT_ID; public interface CommentRepository extends JpaRepository { @Query(value = """ @@ -30,4 +32,8 @@ WITH RECURSIVE CommentTree AS ( """, nativeQuery = true) List findCommentTreeByPostId(@Param("postId") Long postId); + default Comment getById(Long id) { + return findById(id) + .orElseThrow(() -> new CustomException(INVALID_COMMENT_ID)); + } } diff --git a/src/main/java/com/example/solidconnection/comment/service/CommentService.java b/src/main/java/com/example/solidconnection/comment/service/CommentService.java index 9e32e4d32..8003ab26b 100644 --- a/src/main/java/com/example/solidconnection/comment/service/CommentService.java +++ b/src/main/java/com/example/solidconnection/comment/service/CommentService.java @@ -1,29 +1,101 @@ package com.example.solidconnection.comment.service; +import com.example.solidconnection.comment.dto.*; import com.example.solidconnection.comment.repository.CommentRepository; -import com.example.solidconnection.comment.dto.PostFindCommentResponse; -import com.example.solidconnection.entity.Comment; +import com.example.solidconnection.comment.domain.Comment; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.post.domain.Post; +import com.example.solidconnection.post.repository.PostRepository; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.util.List; +import java.util.Optional; import java.util.stream.Collectors; +import static com.example.solidconnection.custom.exception.ErrorCode.CAN_NOT_UPDATE_DEPRECATED_COMMENT; +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_POST_ACCESS; + @Service @RequiredArgsConstructor public class CommentService { private final CommentRepository commentRepository; + private final SiteUserRepository siteUserRepository; + private final PostRepository postRepository; private Boolean isOwner(Comment comment, String email) { return comment.getSiteUser().getEmail().equals(email); } + private void validateOwnership(Comment comment, String email) { + if (!comment.getSiteUser().getEmail().equals(email)) { + throw new CustomException(INVALID_POST_ACCESS); + } + } + private void validateDeprecated(Comment comment) { + if (comment.getContent() == null) { + throw new CustomException(CAN_NOT_UPDATE_DEPRECATED_COMMENT); + } + } + + @Transactional(readOnly = true) public List findCommentsByPostId(String email, Long postId) { return commentRepository.findCommentTreeByPostId(postId) .stream() .map(comment -> PostFindCommentResponse.from(isOwner(comment, email), comment)) .collect(Collectors.toList()); } + + @Transactional + public CommentCreateResponse createComment(String email, Long postId, CommentCreateRequest commentCreateRequest) { + + SiteUser siteUser = siteUserRepository.getByEmail(email); + Post post = postRepository.getById(postId); + + Comment parentComment = null; + if (commentCreateRequest.parentId() != null) { + parentComment = commentRepository.getById(commentCreateRequest.parentId()); + } + Comment createdComment = commentRepository.save(commentCreateRequest.toEntity(siteUser, post, parentComment)); + + return CommentCreateResponse.from(createdComment); + } + + @Transactional + public CommentUpdateResponse updateComment(String email, Long postId, Long commentId, CommentUpdateRequest commentUpdateRequest) { + + SiteUser siteUser = siteUserRepository.getByEmail(email); + Post post = postRepository.getById(postId); + Comment comment = commentRepository.getById(commentId); + validateDeprecated(comment); + validateOwnership(comment, email); + + comment.updateContent(commentUpdateRequest.content()); + + return CommentUpdateResponse.from(comment); + } + + @Transactional + public CommentDeleteResponse deleteCommentById(String email, Long postId, Long commentId) { + SiteUser siteUser = siteUserRepository.getByEmail(email); + Post post = postRepository.getById(postId); + Comment comment = commentRepository.getById(commentId); + validateOwnership(comment, email); + + if (comment.getCommentList().isEmpty()) { + // 하위 댓글이 없다면 삭제한다. + comment.resetPostAndSiteUserAndParentComment(); + commentRepository.deleteById(commentId); + } else { + // 하위 댓글 있으면 value만 null로 수정한다. + comment.deprecateComment(); + } + + return new CommentDeleteResponse(commentId); + } } diff --git a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java index 6caf9edc2..c8bc38acc 100644 --- a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java +++ b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java @@ -48,7 +48,7 @@ public enum ErrorCode { INVALID_TEST_TYPE(HttpStatus.BAD_REQUEST.value(), "지원하지 않은 어학 시험 종류입니다."), APPLICATION_NOT_APPROVED(HttpStatus.BAD_REQUEST.value(), "성적표가 인증되지 않았습니다."), APPLY_UPDATE_LIMIT_EXCEED(HttpStatus.BAD_REQUEST.value(), "지원 정보 수정은 " + APPLICATION_UPDATE_COUNT_LIMIT + "회까지만 가능합니다."), - CANT_APPLY_FOR_SAME_UNIVERSITY(HttpStatus.BAD_REQUEST.value(), "1, 2지망에 동일한 대학교를 입력할 수 없습니다."), + CANT_APPLY_FOR_SAME_UNIVERSITY(HttpStatus.BAD_REQUEST.value(), "1, 2, 3지망에 동일한 대학교를 입력할 수 없습니다."), CAN_NOT_CHANGE_NICKNAME_YET(HttpStatus.BAD_REQUEST.value(), "마지막 닉네임 변경으로부터 " + MIN_DAYS_BETWEEN_NICKNAME_CHANGES + "일이 지나지 않았습니다."), // community @@ -58,6 +58,11 @@ public enum ErrorCode { INVALID_POST_ACCESS(HttpStatus.BAD_REQUEST.value(), "자신의 게시글만 제어할 수 있습니다."), CAN_NOT_DELETE_OR_UPDATE_QUESTION(HttpStatus.BAD_REQUEST.value(), "질문글은 수정이나 삭제할 수 없습니다."), CAN_NOT_UPLOAD_MORE_THAN_FIVE_IMAGES(HttpStatus.BAD_REQUEST.value(), "5개 이상의 파일을 업로드할 수 없습니다."), + INVALID_COMMENT_ID(HttpStatus.BAD_REQUEST.value(), "존재하지 않는 댓글입니다."), + INVALID_COMMENT_ACCESS(HttpStatus.BAD_REQUEST.value(), "자신의 댓글만 제어할 수 있습니다."), + CAN_NOT_UPDATE_DEPRECATED_COMMENT(HttpStatus.BAD_REQUEST.value(),"이미 삭제된 댓글을 수정할 수 없습니다."), + INVALID_POST_LIKE(HttpStatus.BAD_REQUEST.value(), "존재하지 않는 게시글 좋아요입니다."), + DUPLICATE_POST_LIKE(HttpStatus.BAD_REQUEST.value(), "이미 좋아요한 게시글입니다."), // general JSON_PARSING_FAILED(HttpStatus.BAD_REQUEST.value(), "JSON 파싱을 할 수 없습니다."), diff --git a/src/main/java/com/example/solidconnection/entity/Comment.java b/src/main/java/com/example/solidconnection/entity/Comment.java deleted file mode 100644 index 7b4ad87d8..000000000 --- a/src/main/java/com/example/solidconnection/entity/Comment.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.example.solidconnection.entity; - -import com.example.solidconnection.entity.common.BaseEntity; -import com.example.solidconnection.post.domain.Post; -import com.example.solidconnection.siteuser.domain.SiteUser; -import jakarta.persistence.*; -import lombok.Getter; -import lombok.NoArgsConstructor; - -import java.util.ArrayList; -import java.util.List; - -@Entity -@Getter -@NoArgsConstructor -public class Comment extends BaseEntity { - - // for recursive query - @Transient - private int level; - - @Transient - private String path; - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(length = 255) - private String content; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "post_id") - private Post post; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "site_user_id") - private SiteUser siteUser; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "parent_id") - private Comment parentComment; - - @OneToMany(mappedBy = "parentComment", cascade = CascadeType.ALL) - private List commentList = new ArrayList<>(); -} diff --git a/src/main/java/com/example/solidconnection/entity/mapping/PostLike.java b/src/main/java/com/example/solidconnection/entity/mapping/PostLike.java deleted file mode 100644 index 074806838..000000000 --- a/src/main/java/com/example/solidconnection/entity/mapping/PostLike.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.example.solidconnection.entity.mapping; - -import com.example.solidconnection.post.domain.Post; -import com.example.solidconnection.siteuser.domain.SiteUser; -import jakarta.persistence.*; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Entity -@Getter -@NoArgsConstructor -public class PostLike { - @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 = "site_user_id") - private SiteUser siteUser; -} diff --git a/src/main/java/com/example/solidconnection/post/controller/PostController.java b/src/main/java/com/example/solidconnection/post/controller/PostController.java index 022ca8b61..c5b974082 100644 --- a/src/main/java/com/example/solidconnection/post/controller/PostController.java +++ b/src/main/java/com/example/solidconnection/post/controller/PostController.java @@ -4,6 +4,7 @@ import com.example.solidconnection.post.service.PostService; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.security.SecurityRequirements; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -28,7 +29,7 @@ public class PostController { public ResponseEntity createPost( Principal principal, @PathVariable("code") String code, - @RequestPart("postCreateRequest") PostCreateRequest postCreateRequest, + @Valid @RequestPart("postCreateRequest") PostCreateRequest postCreateRequest, @RequestParam(value = "file", required = false) List imageFile) { if (imageFile == null) { @@ -44,7 +45,7 @@ public ResponseEntity updatePost( Principal principal, @PathVariable("code") String code, @PathVariable("post_id") Long postId, - @RequestPart("postUpdateRequest") PostUpdateRequest postUpdateRequest, + @Valid @RequestPart("postUpdateRequest") PostUpdateRequest postUpdateRequest, @RequestParam(value = "file", required = false) List imageFile) { if (imageFile == null) { @@ -76,4 +77,26 @@ public ResponseEntity deletePostById( PostDeleteResponse postDeleteResponse = postService.deletePostById(principal.getName(), code, postId); return ResponseEntity.ok().body(postDeleteResponse); } + + @PostMapping(value = "/{code}/posts/{post_id}/like") + public ResponseEntity likePost( + Principal principal, + @PathVariable("code") String code, + @PathVariable("post_id") Long postId + ) { + + PostLikeResponse postLikeResponse = postService.likePost(principal.getName(), code, postId); + return ResponseEntity.ok().body(postLikeResponse); + } + + @DeleteMapping(value = "/{code}/posts/{post_id}/like") + public ResponseEntity dislikePost( + Principal principal, + @PathVariable("code") String code, + @PathVariable("post_id") Long postId + ) { + + PostDislikeResponse postDislikeResponse = postService.dislikePost(principal.getName(), code, postId); + return ResponseEntity.ok().body(postDislikeResponse); + } } diff --git a/src/main/java/com/example/solidconnection/post/domain/Post.java b/src/main/java/com/example/solidconnection/post/domain/Post.java index 646ac3995..287b255a8 100644 --- a/src/main/java/com/example/solidconnection/post/domain/Post.java +++ b/src/main/java/com/example/solidconnection/post/domain/Post.java @@ -1,10 +1,9 @@ package com.example.solidconnection.post.domain; import com.example.solidconnection.board.domain.Board; -import com.example.solidconnection.entity.Comment; +import com.example.solidconnection.comment.domain.Comment; import com.example.solidconnection.entity.PostImage; import com.example.solidconnection.entity.common.BaseEntity; -import com.example.solidconnection.entity.mapping.PostLike; import com.example.solidconnection.post.dto.PostUpdateRequest; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.type.PostCategory; diff --git a/src/main/java/com/example/solidconnection/post/domain/PostLike.java b/src/main/java/com/example/solidconnection/post/domain/PostLike.java new file mode 100644 index 000000000..0af621370 --- /dev/null +++ b/src/main/java/com/example/solidconnection/post/domain/PostLike.java @@ -0,0 +1,53 @@ +package com.example.solidconnection.post.domain; + +import com.example.solidconnection.siteuser.domain.SiteUser; +import jakarta.persistence.*; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor +@EqualsAndHashCode(of = "id") +public class PostLike { + @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 = "site_user_id") + private SiteUser siteUser; + + public void setPostAndSiteUser(Post post, SiteUser siteUser) { + + if (this.post != null) { + this.post.getPostLikeList().remove(this); + } + this.post = post; + post.getPostLikeList().add(this); + + if (this.siteUser != null) { + this.siteUser.getPostLikeList().remove(this); + } + this.siteUser = siteUser; + siteUser.getPostLikeList().add(this); + } + + public void resetPostAndSiteUser() { + + if (this.post != null) { + this.post.getPostLikeList().remove(this); + } + this.post = null; + + if (this.siteUser != null) { + this.siteUser.getPostLikeList().remove(this); + } + this.siteUser = null; + } +} diff --git a/src/main/java/com/example/solidconnection/post/dto/PostCreateRequest.java b/src/main/java/com/example/solidconnection/post/dto/PostCreateRequest.java index 13cd6469b..03ab79686 100644 --- a/src/main/java/com/example/solidconnection/post/dto/PostCreateRequest.java +++ b/src/main/java/com/example/solidconnection/post/dto/PostCreateRequest.java @@ -4,11 +4,20 @@ import com.example.solidconnection.post.domain.Post; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.type.PostCategory; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; public record PostCreateRequest( + @NotNull(message = "게시글 카테고리를 설정해주세요.") String postCategory, + @NotBlank(message = "게시글 제목은 빈 값일 수 없습니다.") + @Size(min = 1, max = 255, message = "댓글 내용은 최소 1자 이상, 최대 255자 이하여야 합니다.") String title, + @NotBlank(message = "게시글 내용은 빈 값일 수 없습니다.") + @Size(min = 1, max = 1000, message = "댓글 내용은 최소 1자 이상, 최대 255자 이하여야 합니다.") String content, + @NotNull(message = "게시글 질문여부를 설정해주세요.") Boolean isQuestion ) { diff --git a/src/main/java/com/example/solidconnection/post/dto/PostDislikeResponse.java b/src/main/java/com/example/solidconnection/post/dto/PostDislikeResponse.java new file mode 100644 index 000000000..14de9987d --- /dev/null +++ b/src/main/java/com/example/solidconnection/post/dto/PostDislikeResponse.java @@ -0,0 +1,15 @@ +package com.example.solidconnection.post.dto; + +import com.example.solidconnection.post.domain.Post; + +public record PostDislikeResponse( + Long likeCount, + Boolean isLiked +) { + public static PostDislikeResponse from(Post post) { + return new PostDislikeResponse( + post.getLikeCount(), + false + ); + } +} diff --git a/src/main/java/com/example/solidconnection/post/dto/PostFindResponse.java b/src/main/java/com/example/solidconnection/post/dto/PostFindResponse.java index bbde1ba91..7f5f703af 100644 --- a/src/main/java/com/example/solidconnection/post/dto/PostFindResponse.java +++ b/src/main/java/com/example/solidconnection/post/dto/PostFindResponse.java @@ -19,6 +19,7 @@ public record PostFindResponse( Integer commentCount, String postCategory, Boolean isOwner, + Boolean isLiked, LocalDateTime createdAt, LocalDateTime updatedAt, PostFindBoardResponse postFindBoardResponse, @@ -27,7 +28,7 @@ public record PostFindResponse( List postFindPostImageResponses ) { - public static PostFindResponse from(Post post, Boolean isOwner, PostFindBoardResponse postFindBoardResponse, + public static PostFindResponse from(Post post, Boolean isOwner, Boolean isLiked, PostFindBoardResponse postFindBoardResponse, PostFindSiteUserResponse postFindSiteUserResponse, List postFindCommentResponses, List postFindPostImageResponses @@ -42,6 +43,7 @@ public static PostFindResponse from(Post post, Boolean isOwner, PostFindBoardRes postFindCommentResponses.size(), String.valueOf(post.getCategory()), isOwner, + isLiked, post.getCreatedAt(), post.getUpdatedAt(), postFindBoardResponse, diff --git a/src/main/java/com/example/solidconnection/post/dto/PostLikeResponse.java b/src/main/java/com/example/solidconnection/post/dto/PostLikeResponse.java new file mode 100644 index 000000000..0ce14b175 --- /dev/null +++ b/src/main/java/com/example/solidconnection/post/dto/PostLikeResponse.java @@ -0,0 +1,17 @@ +package com.example.solidconnection.post.dto; + +import com.example.solidconnection.post.domain.Post; + +public record PostLikeResponse( + Long likeCount, + Boolean isLiked + + +) { + public static PostLikeResponse from(Post post) { + return new PostLikeResponse( + post.getLikeCount(), + true + ); + } +} diff --git a/src/main/java/com/example/solidconnection/post/dto/PostUpdateRequest.java b/src/main/java/com/example/solidconnection/post/dto/PostUpdateRequest.java index 9394932d7..b82b73685 100644 --- a/src/main/java/com/example/solidconnection/post/dto/PostUpdateRequest.java +++ b/src/main/java/com/example/solidconnection/post/dto/PostUpdateRequest.java @@ -1,8 +1,17 @@ package com.example.solidconnection.post.dto; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + public record PostUpdateRequest( + @NotNull(message = "게시글 카테고리를 설정해주세요.") String postCategory, + @NotBlank(message = "게시글 제목은 빈 값일 수 없습니다.") + @Size(min = 1, max = 255, message = "댓글 내용은 최소 1자 이상, 최대 255자 이하여야 합니다.") String title, + @NotBlank(message = "게시글 내용은 빈 값일 수 없습니다.") + @Size(min = 1, max = 1000, message = "댓글 내용은 최소 1자 이상, 최대 255자 이하여야 합니다.") String content ) { } diff --git a/src/main/java/com/example/solidconnection/post/repository/PostLikeRepository.java b/src/main/java/com/example/solidconnection/post/repository/PostLikeRepository.java new file mode 100644 index 000000000..398157c73 --- /dev/null +++ b/src/main/java/com/example/solidconnection/post/repository/PostLikeRepository.java @@ -0,0 +1,23 @@ +package com.example.solidconnection.post.repository; + +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.post.domain.PostLike; +import com.example.solidconnection.post.domain.Post; +import com.example.solidconnection.siteuser.domain.SiteUser; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_POST_LIKE; + +@Repository +public interface PostLikeRepository extends JpaRepository { + + Optional findPostLikeByPostAndSiteUser(Post post, SiteUser siteUser); + + default PostLike getByPostAndSiteUser(Post post, SiteUser siteUser) { + return findPostLikeByPostAndSiteUser(post, siteUser) + .orElseThrow(() -> new CustomException(INVALID_POST_LIKE)); + } +} diff --git a/src/main/java/com/example/solidconnection/post/repository/PostRepository.java b/src/main/java/com/example/solidconnection/post/repository/PostRepository.java index fda9cb166..f5c10875c 100644 --- a/src/main/java/com/example/solidconnection/post/repository/PostRepository.java +++ b/src/main/java/com/example/solidconnection/post/repository/PostRepository.java @@ -4,6 +4,9 @@ import com.example.solidconnection.post.domain.Post; import org.springframework.data.jpa.repository.EntityGraph; 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.Optional; @@ -25,4 +28,14 @@ default Post getById(Long id) { return findById(id) .orElseThrow(() -> new CustomException(INVALID_POST_ID)); } + + @Modifying(clearAutomatically = true, flushAutomatically = true) + @Query("UPDATE Post p SET p.likeCount = p.likeCount - 1 " + + "WHERE p.id = :postId AND p.likeCount > 0") + void decreaseLikeCount(@Param("postId") Long postId); + + @Modifying(clearAutomatically = true, flushAutomatically = true) + @Query("UPDATE Post p SET p.likeCount = p.likeCount + 1 " + + "WHERE p.id = :postId") + void increaseLikeCount(@Param("postId") Long postId); } diff --git a/src/main/java/com/example/solidconnection/post/service/PostService.java b/src/main/java/com/example/solidconnection/post/service/PostService.java index 52bd22310..4f9f77a73 100644 --- a/src/main/java/com/example/solidconnection/post/service/PostService.java +++ b/src/main/java/com/example/solidconnection/post/service/PostService.java @@ -8,6 +8,8 @@ import com.example.solidconnection.dto.*; import com.example.solidconnection.board.domain.Board; import com.example.solidconnection.entity.PostImage; +import com.example.solidconnection.post.domain.PostLike; +import com.example.solidconnection.post.repository.PostLikeRepository; import com.example.solidconnection.post.domain.Post; import com.example.solidconnection.post.dto.*; import com.example.solidconnection.post.repository.PostRepository; @@ -19,9 +21,12 @@ import com.example.solidconnection.siteuser.repository.SiteUserRepository; import com.example.solidconnection.type.BoardCode; import com.example.solidconnection.type.ImgType; +import com.example.solidconnection.type.PostCategory; import com.example.solidconnection.util.RedisUtils; import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.EnumUtils; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Isolation; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; @@ -39,6 +44,7 @@ public class PostService { private final CommentService commentService; private final RedisService redisService; private final RedisUtils redisUtils; + private final PostLikeRepository postLikeRepository; private String validateCode(String code) { try { @@ -69,16 +75,28 @@ private void validateQuestion(Post post) { } } + private void validatePostCategory(String category) { + if (!EnumUtils.isValidEnum(PostCategory.class, category) || category.equals(PostCategory.전체.toString())) { + throw new CustomException(INVALID_POST_CATEGORY); + } + } + private Boolean getIsOwner(Post post, String email) { return post.getSiteUser().getEmail().equals(email); } + private Boolean getIsLiked(Post post, SiteUser siteUser) { + return postLikeRepository.findPostLikeByPostAndSiteUser(post, siteUser) + .isPresent(); + } + @Transactional public PostCreateResponse createPost(String email, String code, PostCreateRequest postCreateRequest, List imageFile) { // 유효성 검증 String boardCode = validateCode(code); + validatePostCategory(postCreateRequest.postCategory()); validateFileSize(imageFile); // 객체 생성 @@ -137,7 +155,9 @@ public PostFindResponse findPostById(String email, String code, Long postId) { String boardCode = validateCode(code); Post post = postRepository.getByIdUsingEntityGraph(postId); + SiteUser siteUser = siteUserRepository.getByEmail(email); Boolean isOwner = getIsOwner(post, email); + Boolean isLiked = getIsLiked(post, siteUser); PostFindBoardResponse boardPostFindResultDTO = PostFindBoardResponse.from(post.getBoard()); PostFindSiteUserResponse siteUserPostFindResultDTO = PostFindSiteUserResponse.from(post.getSiteUser()); @@ -145,12 +165,12 @@ public PostFindResponse findPostById(String email, String code, Long postId) { List commentFindResultDTOList = commentService.findCommentsByPostId(email, postId); // caching && 어뷰징 방지 - if (redisService.isPresent(redisUtils.getValidatePostViewCountRedisKey(email,postId))) { - redisService.increaseViewCountSync(redisUtils.getPostViewCountRedisKey(postId)); + if (redisService.isPresent(redisUtils.getValidatePostViewCountRedisKey(email, postId))) { + redisService.increaseViewCount(redisUtils.getPostViewCountRedisKey(postId)); } return PostFindResponse.from( - post, isOwner, boardPostFindResultDTO, siteUserPostFindResultDTO, commentFindResultDTOList, postImageFindResultDTOList); + post, isOwner, isLiked, boardPostFindResultDTO, siteUserPostFindResultDTO, commentFindResultDTOList, postImageFindResultDTOList); } @Transactional @@ -169,4 +189,41 @@ public PostDeleteResponse deletePostById(String email, String code, Long postId) return new PostDeleteResponse(postId); } + + @Transactional(isolation = Isolation.READ_COMMITTED) + public PostLikeResponse likePost(String email, String code, Long postId) { + + String boardCode = validateCode(code); + Post post = postRepository.getById(postId); + SiteUser siteUser = siteUserRepository.getByEmail(email); + validateDuplicatePostLike(post, siteUser); + + PostLike postLike = new PostLike(); + postLike.setPostAndSiteUser(post, siteUser); + postLikeRepository.save(postLike); + postRepository.increaseLikeCount(post.getId()); + + return PostLikeResponse.from(postRepository.getById(postId)); // 실시간성을 위한 재조회 + } + + private void validateDuplicatePostLike(Post post, SiteUser siteUser) { + if (postLikeRepository.findPostLikeByPostAndSiteUser(post, siteUser).isPresent()) { + throw new CustomException(DUPLICATE_POST_LIKE); + } + } + + @Transactional(isolation = Isolation.READ_COMMITTED) + public PostDislikeResponse dislikePost(String email, String code, Long postId) { + + String boardCode = validateCode(code); + Post post = postRepository.getById(postId); + SiteUser siteUser = siteUserRepository.getByEmail(email); + + PostLike postLike = postLikeRepository.getByPostAndSiteUser(post, siteUser); + postLike.resetPostAndSiteUser(); + postLikeRepository.deleteById(postLike.getId()); + postRepository.decreaseLikeCount(post.getId()); + + return PostDislikeResponse.from(postRepository.getById(postId)); // 실시간성을 위한 재조회 + } } diff --git a/src/main/java/com/example/solidconnection/service/RedisService.java b/src/main/java/com/example/solidconnection/service/RedisService.java index 4776f1692..9816a264e 100644 --- a/src/main/java/com/example/solidconnection/service/RedisService.java +++ b/src/main/java/com/example/solidconnection/service/RedisService.java @@ -24,7 +24,7 @@ public RedisService(RedisTemplate redisTemplate, } // incr & set ttl -> lua - public void increaseViewCountSync(String key) { + public void increaseViewCount(String key) { redisTemplate.execute(incrViewCountLuaScript, Collections.singletonList(key), VIEW_COUNT_TTL.getValue()); } diff --git a/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java b/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java index 2cd7dc4ff..f3c870ceb 100644 --- a/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java +++ b/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java @@ -1,8 +1,8 @@ package com.example.solidconnection.siteuser.domain; -import com.example.solidconnection.entity.Comment; +import com.example.solidconnection.comment.domain.Comment; import com.example.solidconnection.post.domain.Post; -import com.example.solidconnection.entity.mapping.PostLike; +import com.example.solidconnection.post.domain.PostLike; import com.example.solidconnection.type.Gender; import com.example.solidconnection.type.PreparationStatus; import com.example.solidconnection.type.Role; @@ -59,7 +59,7 @@ public class SiteUser { @OneToMany(mappedBy = "siteUser", cascade = CascadeType.ALL, orphanRemoval = true) private List postList = new ArrayList<>(); - @OneToMany(mappedBy = "siteUser", cascade = CascadeType.ALL, orphanRemoval = true) + @OneToMany(mappedBy = "siteUser", cascade = CascadeType.ALL) private List commentList = new ArrayList<>(); @OneToMany(mappedBy = "siteUser", cascade = CascadeType.ALL, orphanRemoval = true) diff --git a/src/test/java/com/example/solidconnection/concurrency/PostLikeCountConcurrencyTest.java b/src/test/java/com/example/solidconnection/concurrency/PostLikeCountConcurrencyTest.java new file mode 100644 index 000000000..f07b6821c --- /dev/null +++ b/src/test/java/com/example/solidconnection/concurrency/PostLikeCountConcurrencyTest.java @@ -0,0 +1,141 @@ +package com.example.solidconnection.concurrency; + +import com.example.solidconnection.board.domain.Board; +import com.example.solidconnection.board.repository.BoardRepository; +import com.example.solidconnection.post.domain.Post; +import com.example.solidconnection.post.repository.PostRepository; +import com.example.solidconnection.post.service.PostService; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.type.Gender; +import com.example.solidconnection.type.PostCategory; +import com.example.solidconnection.type.PreparationStatus; +import com.example.solidconnection.type.Role; +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.beans.factory.annotation.Value; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@SpringBootTest +@ActiveProfiles("test") +@DisplayName("게시글 좋아요 동시성 테스트") +class PostLikeCountConcurrencyTest { + + @Autowired + private PostService postService; + @Autowired + private PostRepository postRepository; + @Autowired + private BoardRepository boardRepository; + @Autowired + private SiteUserRepository siteUserRepository; + + @Value("${view.count.scheduling.delay}") + private int SCHEDULING_DELAY_MS; + private int THREAD_NUMS = 1000; + private int THREAD_POOL_SIZE = 200; + private int TIMEOUT_SECONDS = 10; + + private Post post; + private Board board; + private SiteUser siteUser; + + @BeforeEach + void setUp() { + board = createBoard(); + boardRepository.save(board); + siteUser = createSiteUser(); + siteUserRepository.save(siteUser); + post = createPost(board, siteUser); + postRepository.save(post); + createSiteUsers(); + } + + private SiteUser createSiteUser() { + return new SiteUser( + "test@example.com", + "nickname", + "profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.MALE + ); + } + + private void createSiteUsers() { + for (int i = 0; i < 1000; i++) { + + SiteUser siteUser = new SiteUser( + "email" + i, + "nickname", + "profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.MALE + ); + siteUserRepository.save(siteUser); + } + } + + private Board createBoard() { + return new Board( + "FREE", "자유게시판"); + } + + private Post createPost(Board board, SiteUser siteUser) { + Post post = new Post( + "title", + "content", + false, + 0L, + 0L, + PostCategory.valueOf("자유") + ); + post.setBoardAndSiteUser(board, siteUser); + + return post; + } + + @Test + void 게시글_좋아요_동시성_문제를_해결한다() throws InterruptedException { + + ExecutorService executorService = Executors.newFixedThreadPool(THREAD_POOL_SIZE); + CountDownLatch doneSignal = new CountDownLatch(THREAD_NUMS); + + Long likeCount = postRepository.getById(post.getId()).getLikeCount(); + + for (int i = 0; i < THREAD_NUMS; i++) { + String email = "email" + i; + executorService.submit(() -> { + try { + postService.likePost(email, board.getCode(), post.getId()); + postService.dislikePost(email, board.getCode(), post.getId()); + } finally { + doneSignal.countDown(); + } + }); + } + + doneSignal.await(TIMEOUT_SECONDS, TimeUnit.SECONDS); + executorService.shutdown(); + boolean terminated = executorService.awaitTermination(TIMEOUT_SECONDS, TimeUnit.SECONDS); + if (!terminated) { + System.err.println("ExecutorService did not terminate in the expected time."); + } + + assertEquals(likeCount, postRepository.getById(post.getId()).getLikeCount()); + } + +} diff --git a/src/test/java/com/example/solidconnection/concurrency/PostViewCountConcurrencyTest.java b/src/test/java/com/example/solidconnection/concurrency/PostViewCountConcurrencyTest.java index 9eb936301..c2213993d 100644 --- a/src/test/java/com/example/solidconnection/concurrency/PostViewCountConcurrencyTest.java +++ b/src/test/java/com/example/solidconnection/concurrency/PostViewCountConcurrencyTest.java @@ -98,13 +98,15 @@ private Post createPost(Board board, SiteUser siteUser) { @Test public void 게시글을_조회할_때_조회수_동시성_문제를_해결한다() throws InterruptedException { + redisService.deleteKey(redisUtils.getValidatePostViewCountRedisKey(siteUser.getEmail(), post.getId())); + ExecutorService executorService = Executors.newFixedThreadPool(THREAD_POOL_SIZE); CountDownLatch doneSignal = new CountDownLatch(THREAD_NUMS); for (int i = 0; i < THREAD_NUMS; i++) { executorService.submit(() -> { try { - redisService.increaseViewCountSync(redisUtils.getPostViewCountRedisKey(post.getId())); + redisService.increaseViewCount(redisUtils.getPostViewCountRedisKey(post.getId())); } finally { doneSignal.countDown(); } @@ -118,7 +120,7 @@ private Post createPost(Board board, SiteUser siteUser) { System.err.println("ExecutorService did not terminate in the expected time."); } - Thread.sleep(SCHEDULING_DELAY_MS); + Thread.sleep(SCHEDULING_DELAY_MS+1000); assertEquals(THREAD_NUMS, postRepository.getById(post.getId()).getViewCount()); } @@ -136,7 +138,7 @@ private Post createPost(Board board, SiteUser siteUser) { try { boolean isFirstTime = redisService.isPresent(redisUtils.getValidatePostViewCountRedisKey(siteUser.getEmail(), post.getId())); if (isFirstTime) { - redisService.increaseViewCountSync(redisUtils.getPostViewCountRedisKey(post.getId())); + redisService.increaseViewCount(redisUtils.getPostViewCountRedisKey(post.getId())); } } finally { doneSignal.countDown(); @@ -149,7 +151,7 @@ private Post createPost(Board board, SiteUser siteUser) { try { boolean isFirstTime = redisService.isPresent(redisUtils.getValidatePostViewCountRedisKey(siteUser.getEmail(), post.getId())); if (isFirstTime) { - redisService.increaseViewCountSync(redisUtils.getPostViewCountRedisKey(post.getId())); + redisService.increaseViewCount(redisUtils.getPostViewCountRedisKey(post.getId())); } } finally { doneSignal.countDown(); @@ -164,7 +166,7 @@ private Post createPost(Board board, SiteUser siteUser) { System.err.println("ExecutorService did not terminate in the expected time."); } - Thread.sleep(SCHEDULING_DELAY_MS); + Thread.sleep(SCHEDULING_DELAY_MS+1000); assertEquals(2L, postRepository.getById(post.getId()).getViewCount()); } diff --git a/src/test/java/com/example/solidconnection/e2e/ApplicantsQueryTest.java b/src/test/java/com/example/solidconnection/e2e/ApplicantsQueryTest.java index 6ad77f58b..d609aa121 100644 --- a/src/test/java/com/example/solidconnection/e2e/ApplicantsQueryTest.java +++ b/src/test/java/com/example/solidconnection/e2e/ApplicantsQueryTest.java @@ -66,10 +66,10 @@ public void setUpUserAndToken() { 사용자1_지원정보 = new Application(사용자1, gpa, languageTest); 사용자2_지원정보 = new Application(사용자2, gpa, languageTest); 사용자3_지원정보 = new Application(사용자3, gpa, languageTest); - 나의_지원정보.updateUniversityChoice(괌대학_B_지원_정보, 괌대학_A_지원_정보, "0"); - 사용자1_지원정보.updateUniversityChoice(괌대학_A_지원_정보, 괌대학_B_지원_정보, "1"); - 사용자2_지원정보.updateUniversityChoice(메이지대학_지원_정보, 그라츠대학_지원_정보, "2"); - 사용자3_지원정보.updateUniversityChoice(네바다주립대학_라스베이거스_지원_정보, 그라츠공과대학_지원_정보, "3"); + 나의_지원정보.updateUniversityChoice(괌대학_B_지원_정보, 괌대학_A_지원_정보, 린츠_카톨릭대학_지원_정보, "0"); + 사용자1_지원정보.updateUniversityChoice(괌대학_A_지원_정보, 괌대학_B_지원_정보, 그라츠공과대학_지원_정보, "1"); + 사용자2_지원정보.updateUniversityChoice(메이지대학_지원_정보, 그라츠대학_지원_정보, 서던덴마크대학교_지원_정보, "2"); + 사용자3_지원정보.updateUniversityChoice(네바다주립대학_라스베이거스_지원_정보, 그라츠공과대학_지원_정보, 메이지대학_지원_정보, "3"); 나의_지원정보.setVerifyStatus(VerifyStatus.APPROVED); 사용자1_지원정보.setVerifyStatus(VerifyStatus.APPROVED); 사용자2_지원정보.setVerifyStatus(VerifyStatus.APPROVED); @@ -89,6 +89,7 @@ public void setUpUserAndToken() { List firstChoiceApplicants = response.firstChoice(); List secondChoiceApplicants = response.secondChoice(); + List thirdChoiceApplicants = response.thirdChoice(); assertThat(firstChoiceApplicants).containsAnyElementsOf(List.of( UniversityApplicantsResponse.of(괌대학_A_지원_정보, @@ -110,6 +111,16 @@ public void setUpUserAndToken() { UniversityApplicantsResponse.of(그라츠대학_지원_정보, List.of(ApplicantResponse.of(사용자2_지원정보, false))) )); + assertThat(thirdChoiceApplicants).containsAnyElementsOf(List.of( + UniversityApplicantsResponse.of(린츠_카톨릭대학_지원_정보, + List.of(ApplicantResponse.of(나의_지원정보, true))), + UniversityApplicantsResponse.of(서던덴마크대학교_지원_정보, + List.of(ApplicantResponse.of(사용자2_지원정보, false))), + UniversityApplicantsResponse.of(그라츠공과대학_지원_정보, + List.of(ApplicantResponse.of(사용자1_지원정보, false))), + UniversityApplicantsResponse.of(메이지대학_지원_정보, + List.of(ApplicantResponse.of(사용자3_지원정보, false))) + )); } @Test diff --git a/src/test/java/com/example/solidconnection/e2e/ApplicationSubmissionTest.java b/src/test/java/com/example/solidconnection/e2e/ApplicationSubmissionTest.java index f5443f805..0cbe37f35 100644 --- a/src/test/java/com/example/solidconnection/e2e/ApplicationSubmissionTest.java +++ b/src/test/java/com/example/solidconnection/e2e/ApplicationSubmissionTest.java @@ -119,7 +119,7 @@ public void setUpUserAndToken() { applicationRepository.save(new Application(siteUser, firstRequest.toGpa(), firstRequest.toLanguageTest())); // request - body 생성 및 요청 - UniversityChoiceRequest request = new UniversityChoiceRequest(그라츠대학_지원_정보.getId(), 코펜하겐IT대학_지원_정보.getId()); + UniversityChoiceRequest request = new UniversityChoiceRequest(그라츠대학_지원_정보.getId(), 코펜하겐IT대학_지원_정보.getId(), 메이지대학_지원_정보.getId()); RestAssured.given() .header("Authorization", "Bearer " + accessToken) .body(request) @@ -135,6 +135,7 @@ public void setUpUserAndToken() { () -> assertThat(application.getSiteUser().getId()).isEqualTo(siteUser.getId()), () -> assertThat(application.getFirstChoiceUniversity().getId()).isEqualTo(request.firstChoiceUniversityId()), () -> assertThat(application.getSecondChoiceUniversity().getId()).isEqualTo(request.secondChoiceUniversityId()), + () -> assertThat(application.getThirdChoiceUniversity().getId()).isEqualTo(request.thirdChoiceUniversityId()), () -> assertThat(application.getNicknameForApply()).isNotNull(), () -> assertThat(application.getVerifyStatus()).isEqualTo(VerifyStatus.PENDING), () -> assertThat(application.getUpdateCount()).isZero()); @@ -146,11 +147,11 @@ public void setUpUserAndToken() { ScoreRequest firstRequest = new ScoreRequest(LanguageTestType.TOEFL_IBT, "80", "languageTestReportUrl", 4.0, 4.5, "gpaReportUrl"); applicationRepository.save(new Application(siteUser, firstRequest.toGpa(), firstRequest.toLanguageTest())) - .updateUniversityChoice(괌대학_A_지원_정보, 괌대학_B_지원_정보, "nickname"); + .updateUniversityChoice(괌대학_A_지원_정보, 괌대학_B_지원_정보, 네바다주립대학_라스베이거스_지원_정보, "nickname"); Application initialApplication = applicationRepository.getApplicationBySiteUser(siteUser); // request - body 생성 및 요청 - UniversityChoiceRequest request = new UniversityChoiceRequest(그라츠대학_지원_정보.getId(), 코펜하겐IT대학_지원_정보.getId()); + UniversityChoiceRequest request = new UniversityChoiceRequest(그라츠대학_지원_정보.getId(), 코펜하겐IT대학_지원_정보.getId(), 메이지대학_지원_정보.getId()); RestAssured.given() .header("Authorization", "Bearer " + accessToken) .body(request) @@ -166,6 +167,7 @@ public void setUpUserAndToken() { () -> assertThat(updatedApplication.getSiteUser().getId()).isEqualTo(siteUser.getId()), () -> assertThat(updatedApplication.getFirstChoiceUniversity().getId()).isEqualTo(request.firstChoiceUniversityId()), () -> assertThat(updatedApplication.getSecondChoiceUniversity().getId()).isEqualTo(request.secondChoiceUniversityId()), + () -> assertThat(updatedApplication.getThirdChoiceUniversity().getId()).isEqualTo(request.thirdChoiceUniversityId()), () -> assertThat(updatedApplication.getNicknameForApply()).isNotNull(), () -> assertThat(updatedApplication.getVerifyStatus()).isEqualTo(initialApplication.getVerifyStatus()), () -> assertThat(updatedApplication.getUpdateCount()).isEqualTo(initialApplication.getUpdateCount())); @@ -181,12 +183,12 @@ public void setUpUserAndToken() { // setUp - 지망 대학을 한계까지 수정 for (int i = 0; i <= APPLICATION_UPDATE_COUNT_LIMIT; i++) { - initialApplication.updateUniversityChoice(괌대학_A_지원_정보, 괌대학_B_지원_정보, "nickname"); + initialApplication.updateUniversityChoice(괌대학_A_지원_정보, 괌대학_B_지원_정보, 네바다주립대학_라스베이거스_지원_정보, "nickname"); applicationRepository.save(initialApplication); } // request - body 생성 및 요청 - UniversityChoiceRequest request = new UniversityChoiceRequest(그라츠대학_지원_정보.getId(), 코펜하겐IT대학_지원_정보.getId()); + UniversityChoiceRequest request = new UniversityChoiceRequest(그라츠대학_지원_정보.getId(), 코펜하겐IT대학_지원_정보.getId(), 메이지대학_지원_정보.getId()); ErrorResponse errorResponse = RestAssured.given().log().all() .header("Authorization", "Bearer " + accessToken) .body(request) @@ -202,7 +204,41 @@ public void setUpUserAndToken() { @Test void 일지망_대학과_이지망_대학이_같으면_예외_응답을_반환한다() { // request - body 생성 및 요청 - UniversityChoiceRequest request = new UniversityChoiceRequest(그라츠대학_지원_정보.getId(), 그라츠대학_지원_정보.getId()); + UniversityChoiceRequest request = new UniversityChoiceRequest(그라츠대학_지원_정보.getId(), 그라츠대학_지원_정보.getId(), 메이지대학_지원_정보.getId()); + ErrorResponse errorResponse = RestAssured.given() + .header("Authorization", "Bearer " + accessToken) + .body(request) + .contentType("application/json") + .log().all() + .post("/application/university") + .then().log().all() + .statusCode(HttpStatus.BAD_REQUEST.value()) + .extract().as(ErrorResponse.class); + + assertThat(errorResponse.message()).isEqualTo(CANT_APPLY_FOR_SAME_UNIVERSITY.getMessage()); + } + + @Test + void 일지망_대학과_삼지망_대학이_같으면_예외_응답을_반환한다() { + // request - body 생성 및 요청 + UniversityChoiceRequest request = new UniversityChoiceRequest(그라츠대학_지원_정보.getId(), 코펜하겐IT대학_지원_정보.getId(), 그라츠대학_지원_정보.getId()); + ErrorResponse errorResponse = RestAssured.given() + .header("Authorization", "Bearer " + accessToken) + .body(request) + .contentType("application/json") + .log().all() + .post("/application/university") + .then().log().all() + .statusCode(HttpStatus.BAD_REQUEST.value()) + .extract().as(ErrorResponse.class); + + assertThat(errorResponse.message()).isEqualTo(CANT_APPLY_FOR_SAME_UNIVERSITY.getMessage()); + } + + @Test + void 이지망_대학과_삼지망_대학이_같으면_예외_응답을_반환한다() { + // request - body 생성 및 요청 + UniversityChoiceRequest request = new UniversityChoiceRequest(그라츠대학_지원_정보.getId(), 코펜하겐IT대학_지원_정보.getId(), 코펜하겐IT대학_지원_정보.getId()); ErrorResponse errorResponse = RestAssured.given() .header("Authorization", "Bearer " + accessToken) .body(request) diff --git a/src/test/java/com/example/solidconnection/e2e/VerifyStatusQueryTest.java b/src/test/java/com/example/solidconnection/e2e/VerifyStatusQueryTest.java index d0749f1f9..b4447247a 100644 --- a/src/test/java/com/example/solidconnection/e2e/VerifyStatusQueryTest.java +++ b/src/test/java/com/example/solidconnection/e2e/VerifyStatusQueryTest.java @@ -87,7 +87,7 @@ public void setUpUserAndToken() { void 성적과_대학을_모두_제출하고_승인을_기대라는_상태를_반환한다() { // setUp - 성적과 대학을 모두 제출한 상태 Application application = new Application(siteUser, createDummyGpa(), createDummyLanguageTest()); - application.updateUniversityChoice(괌대학_B_지원_정보, 괌대학_A_지원_정보, "닉네임"); + application.updateUniversityChoice(괌대학_B_지원_정보, 괌대학_A_지원_정보, 네바다주립대학_라스베이거스_지원_정보, "닉네임"); applicationRepository.save(application); // request - 요청 @@ -108,7 +108,7 @@ public void setUpUserAndToken() { void 성적과_대학을_모두_제출했지만_승인이_반려된_상태를_반환한다() { // setUp - 성적과 대학을 모두 제출했지만, 승인 거절 Application application = new Application(siteUser, createDummyGpa(), createDummyLanguageTest()); - application.updateUniversityChoice(괌대학_B_지원_정보, 괌대학_A_지원_정보, "닉네임"); + application.updateUniversityChoice(괌대학_B_지원_정보, 괌대학_A_지원_정보, 네바다주립대학_라스베이거스_지원_정보,"닉네임"); application.setVerifyStatus(VerifyStatus.REJECTED); applicationRepository.save(application); @@ -130,7 +130,7 @@ public void setUpUserAndToken() { void 성적과_대학을_모두_제출했으며_승인이_된_상태를_반환한다() { // setUp - 성적과 대학을 모두 제출했으며, 승인이 된 상태 Application application = new Application(siteUser, createDummyGpa(), createDummyLanguageTest()); - application.updateUniversityChoice(괌대학_B_지원_정보, 괌대학_A_지원_정보, "닉네임"); + application.updateUniversityChoice(괌대학_B_지원_정보, 괌대학_A_지원_정보, 네바다주립대학_라스베이거스_지원_정보, "닉네임"); application.setVerifyStatus(VerifyStatus.APPROVED); applicationRepository.save(application); diff --git a/src/test/java/com/example/solidconnection/unit/repository/BoardRepositoryTest.java b/src/test/java/com/example/solidconnection/unit/repository/BoardRepositoryTest.java index c540954b1..9ea7ee0d9 100644 --- a/src/test/java/com/example/solidconnection/unit/repository/BoardRepositoryTest.java +++ b/src/test/java/com/example/solidconnection/unit/repository/BoardRepositoryTest.java @@ -27,7 +27,7 @@ @DataJpaTest @ActiveProfiles("test") @DisplayName("게시판 레포지토리 테스트") -public class BoardRepositoryTest { +class BoardRepositoryTest { @Autowired private PostRepository postRepository; @Autowired diff --git a/src/test/java/com/example/solidconnection/unit/repository/CommentRepositoryTest.java b/src/test/java/com/example/solidconnection/unit/repository/CommentRepositoryTest.java new file mode 100644 index 000000000..7029dead9 --- /dev/null +++ b/src/test/java/com/example/solidconnection/unit/repository/CommentRepositoryTest.java @@ -0,0 +1,156 @@ +package com.example.solidconnection.unit.repository; + +import com.example.solidconnection.board.domain.Board; +import com.example.solidconnection.board.repository.BoardRepository; +import com.example.solidconnection.comment.domain.Comment; +import com.example.solidconnection.comment.repository.CommentRepository; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.post.domain.Post; +import com.example.solidconnection.post.repository.PostRepository; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.type.Gender; +import com.example.solidconnection.type.PostCategory; +import com.example.solidconnection.type.PreparationStatus; +import com.example.solidconnection.type.Role; +import jakarta.persistence.EntityManager; +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.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_COMMENT_ID; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + + +@SpringBootTest +@ActiveProfiles("local") +@DisplayName("댓글 레포지토리 테스트") +class CommentRepositoryTest { + @Autowired + private PostRepository postRepository; + @Autowired + private BoardRepository boardRepository; + @Autowired + private SiteUserRepository siteUserRepository; + @Autowired + private EntityManager entityManager; + @Autowired + private CommentRepository commentRepository; + + private Board board; + private SiteUser siteUser; + private Post post; + private Comment parentComment; + private Comment childComment; + + @BeforeEach + public void setUp() { + board = createBoard(); + boardRepository.save(board); + + siteUser = createSiteUser(); + siteUserRepository.save(siteUser); + + post = createPost(board, siteUser); + post = postRepository.save(post); + + parentComment = createParentComment(); + childComment = createChildComment(); + commentRepository.save(parentComment); + commentRepository.save(childComment); + + entityManager.flush(); + entityManager.clear(); + } + + private Board createBoard() { + return new Board( + "FREE", "자유게시판"); + } + + private SiteUser createSiteUser() { + return new SiteUser( + "test@example.com", + "nickname", + "profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.MALE + ); + } + + private Post createPost(Board board, SiteUser siteUser) { + Post post = new Post( + "title", + "content", + false, + 0L, + 0L, + PostCategory.valueOf("자유") + ); + post.setBoardAndSiteUser(board, siteUser); + return post; + } + + private Comment createParentComment() { + Comment comment = new Comment( + "parent" + ); + comment.setPostAndSiteUser(post, siteUser); + return comment; + } + + private Comment createChildComment() { + Comment comment = new Comment( + "child" + ); + comment.setParentCommentAndPostAndSiteUser(parentComment, post, siteUser); + return comment; + } + + @Test + @Transactional + public void 재귀쿼리로_댓글트리를_조회한다() { + // when + List commentTreeByPostId = commentRepository.findCommentTreeByPostId(post.getId()); + + // then + List expectedResponse = List.of(parentComment, childComment); + assertEquals(commentTreeByPostId, expectedResponse); + } + + @Test + @Transactional + public void 댓글을_조회한다() { + // when + Comment foundComment = commentRepository.getById(parentComment.getId()); + + // then + assertEquals(parentComment, foundComment); + } + + @Test + @Transactional + public void 댓글을_조회할_때_유효한_댓글이_아니라면_예외_응답을_반환한다() { + // given + Long invalidId = -1L; + + // when, then + CustomException exception = assertThrows(CustomException.class, () -> { + commentRepository.getById(invalidId); + }); + assertThat(exception.getMessage()) + .isEqualTo(INVALID_COMMENT_ID.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(INVALID_COMMENT_ID.getCode()); + } +} diff --git a/src/test/java/com/example/solidconnection/unit/repository/PostLikeRepositoryTest.java b/src/test/java/com/example/solidconnection/unit/repository/PostLikeRepositoryTest.java new file mode 100644 index 000000000..c39e28497 --- /dev/null +++ b/src/test/java/com/example/solidconnection/unit/repository/PostLikeRepositoryTest.java @@ -0,0 +1,122 @@ +package com.example.solidconnection.unit.repository; + +import com.example.solidconnection.board.domain.Board; +import com.example.solidconnection.board.repository.BoardRepository; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.post.domain.PostLike; +import com.example.solidconnection.post.repository.PostLikeRepository; +import com.example.solidconnection.post.domain.Post; +import com.example.solidconnection.post.repository.PostRepository; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.type.Gender; +import com.example.solidconnection.type.PostCategory; +import com.example.solidconnection.type.PreparationStatus; +import com.example.solidconnection.type.Role; +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.orm.jpa.DataJpaTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_POST_LIKE; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@DataJpaTest +@ActiveProfiles("test") +@DisplayName("게시글 좋아요 레포지토리 테스트") +class PostLikeRepositoryTest { + @Autowired + private PostRepository postRepository; + @Autowired + private BoardRepository boardRepository; + @Autowired + private SiteUserRepository siteUserRepository; + @Autowired + private PostLikeRepository postLikeRepository; + + private Post post; + private Board board; + private SiteUser siteUser; + private PostLike postLike; + + + @BeforeEach + void setUp() { + board = createBoard(); + boardRepository.save(board); + siteUser = createSiteUser(); + siteUserRepository.save(siteUser); + post = createPost(board, siteUser); + post = postRepository.save(post); + postLike = createPostLike(post, siteUser); + postLikeRepository.save(postLike); + } + + private SiteUser createSiteUser() { + return new SiteUser( + "test@example.com", + "nickname", + "profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.MALE + ); + } + + private Board createBoard() { + return new Board( + "FREE", "자유게시판"); + } + + private Post createPost(Board board, SiteUser siteUser) { + Post post = new Post( + "title", + "content", + false, + 0L, + 0L, + PostCategory.valueOf("자유") + ); + post.setBoardAndSiteUser(board, siteUser); + return post; + } + + private PostLike createPostLike(Post post, SiteUser siteUser) { + PostLike postLike = new PostLike(); + postLike.setPostAndSiteUser(post, siteUser); + return postLike; + } + + @Test + @Transactional + void 게시글_좋아요를_조회한다() { + // when + PostLike foundPostLike = postLikeRepository.getByPostAndSiteUser(post, siteUser); + + // then + assertEquals(foundPostLike, postLike); + } + + @Test + @Transactional + void 게시글_좋아요를_조회할_때_유효한_좋아요가_아니라면_예외_응답을_반환한다() { + // given + postLike.resetPostAndSiteUser(); + postLikeRepository.delete(postLike); + + // when, then + CustomException exception = assertThrows(CustomException.class, () -> { + postLikeRepository.getByPostAndSiteUser(post, siteUser); + }); + assertThat(exception.getMessage()) + .isEqualTo(INVALID_POST_LIKE.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(INVALID_POST_LIKE.getCode()); + } +} diff --git a/src/test/java/com/example/solidconnection/unit/repository/PostRepositoryTest.java b/src/test/java/com/example/solidconnection/unit/repository/PostRepositoryTest.java index b81ee952c..ecc2c4f6d 100644 --- a/src/test/java/com/example/solidconnection/unit/repository/PostRepositoryTest.java +++ b/src/test/java/com/example/solidconnection/unit/repository/PostRepositoryTest.java @@ -31,7 +31,7 @@ @DataJpaTest @ActiveProfiles("test") @DisplayName("게시글 레포지토리 테스트") -public class PostRepositoryTest { +class PostRepositoryTest { @Autowired private PostRepository postRepository; @Autowired @@ -44,7 +44,7 @@ public class PostRepositoryTest { private SiteUser siteUser; @BeforeEach - public void setUp() { + void setUp() { board = createBoard(); boardRepository.save(board); siteUser = createSiteUser(); @@ -92,7 +92,7 @@ private Post createPostWithImages(Board board, SiteUser siteUser) { @Test @Transactional - public void 게시글을_조회할_때_게시글_이미지는_즉시_로딩한다() { + void 게시글을_조회할_때_게시글_이미지는_즉시_로딩한다() { Post foundPost = postRepository.getByIdUsingEntityGraph(post.getId()); foundPost.getPostImageList().size(); // 추가쿼리 발생하지 않는다. @@ -101,7 +101,7 @@ private Post createPostWithImages(Board board, SiteUser siteUser) { @Test @Transactional - public void 게시글을_조회할_때_게시글_이미지는_즉시_로딩한다_유효한_게시글이_아니라면_예외_응답을_반환한다() { + void 게시글을_조회할_때_게시글_이미지는_즉시_로딩한다_유효한_게시글이_아니라면_예외_응답을_반환한다() { // given Long invalidId = -1L; @@ -117,7 +117,7 @@ private Post createPostWithImages(Board board, SiteUser siteUser) { @Test @Transactional - public void 게시글을_조회한다() { + void 게시글을_조회한다() { Post foundPost = postRepository.getById(post.getId()); assertEquals(post, foundPost); @@ -125,7 +125,7 @@ private Post createPostWithImages(Board board, SiteUser siteUser) { @Test @Transactional - public void 게시글을_조회할_때_유효한_게시글이_아니라면_예외_응답을_반환한다() { + void 게시글을_조회할_때_유효한_게시글이_아니라면_예외_응답을_반환한다() { Long invalidId = -1L; CustomException exception = assertThrows(CustomException.class, () -> { @@ -136,4 +136,33 @@ private Post createPostWithImages(Board board, SiteUser siteUser) { assertThat(exception.getCode()) .isEqualTo(INVALID_POST_ID.getCode()); } + + @Test + @Transactional + void 게시글_좋아요를_등록한다() { + // given + Long likeCount = post.getLikeCount(); + + // when + postRepository.increaseLikeCount(post.getId()); + + // then + Post response = postRepository.getById(post.getId()); + assertEquals(response.getLikeCount(), likeCount + 1); + } + + @Test + @Transactional + void 게시글_좋아요를_삭제한다() { + // given + Long likeCount = post.getLikeCount(); + postRepository.increaseLikeCount(post.getId()); + + // when + postRepository.decreaseLikeCount(post.getId()); + + // then + Post response = postRepository.getById(post.getId()); + assertEquals(response.getLikeCount(), likeCount); + } } diff --git a/src/test/java/com/example/solidconnection/unit/service/BoardServiceTest.java b/src/test/java/com/example/solidconnection/unit/service/BoardServiceTest.java index 710546c9b..18c37b807 100644 --- a/src/test/java/com/example/solidconnection/unit/service/BoardServiceTest.java +++ b/src/test/java/com/example/solidconnection/unit/service/BoardServiceTest.java @@ -27,7 +27,7 @@ @ExtendWith(MockitoExtension.class) @DisplayName("게시판 서비스 테스트") -public class BoardServiceTest { +class BoardServiceTest { @InjectMocks BoardService boardService; @Mock diff --git a/src/test/java/com/example/solidconnection/unit/service/CommentServiceTest.java b/src/test/java/com/example/solidconnection/unit/service/CommentServiceTest.java new file mode 100644 index 000000000..8a90b275b --- /dev/null +++ b/src/test/java/com/example/solidconnection/unit/service/CommentServiceTest.java @@ -0,0 +1,423 @@ +package com.example.solidconnection.unit.service; + +import com.example.solidconnection.board.domain.Board; +import com.example.solidconnection.comment.domain.Comment; +import com.example.solidconnection.comment.dto.*; +import com.example.solidconnection.comment.repository.CommentRepository; +import com.example.solidconnection.comment.service.CommentService; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.post.domain.Post; +import com.example.solidconnection.post.repository.PostRepository; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.type.*; +import org.junit.jupiter.api.BeforeEach; +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 java.util.List; +import java.util.stream.Collectors; + +import static com.example.solidconnection.custom.exception.ErrorCode.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +@DisplayName("댓글 서비스 테스트") +class CommentServiceTest { + @InjectMocks + CommentService commentService; + @Mock + PostRepository postRepository; + @Mock + SiteUserRepository siteUserRepository; + @Mock + CommentRepository commentRepository; + + private SiteUser siteUser; + private Board board; + private Post post; + private Comment parentComment_1; + private Comment parentComment_2; + private Comment p1s_childComment; + + + @BeforeEach + void setUp() { + siteUser = createSiteUser(); + board = createBoard(); + post = createPost(board, siteUser); + parentComment_1 = createParentComment(); + parentComment_2 = createParentComment(); + p1s_childComment = createChildComment(); + } + + private SiteUser createSiteUser() { + return new SiteUser( + "test@example.com", + "nickname", + "profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.MALE + ); + } + + private Board createBoard() { + return new Board( + "FREE", "자유게시판"); + } + + private Post createPost(Board board, SiteUser siteUser) { + Post post = new Post( + "title", + "content", + false, + 0L, + 0L, + PostCategory.valueOf("자유") + ); + post.setBoardAndSiteUser(board, siteUser); + return post; + } + + private Comment createParentComment() { + Comment comment = new Comment( + "parent" + ); + comment.setPostAndSiteUser(post, siteUser); + return comment; + } + + private Comment createChildComment() { + Comment comment = new Comment( + "child" + ); + comment.setParentCommentAndPostAndSiteUser(parentComment_1, post, siteUser); + return comment; + } + + /** + * 댓글 조회 + */ + + @Test + void 특정_게시글의_댓글들을_조회한다() { + // Given + List commentList = List.of(parentComment_1, p1s_childComment, parentComment_2); + when(commentRepository.findCommentTreeByPostId(post.getId())).thenReturn(commentList); + + // When + List postFindCommentResponses = commentService.findCommentsByPostId( + siteUser.getEmail(), post.getId()); + + // Then + List expectedResponse = commentList.stream() + .map(comment -> PostFindCommentResponse.from(isOwner(comment, siteUser.getEmail()), comment)) + .collect(Collectors.toList()); + assertEquals(postFindCommentResponses, expectedResponse); + } + + private Boolean isOwner(Comment comment, String email) { + return comment.getSiteUser().getEmail().equals(email); + } + + /** + * 댓글 등록 + */ + @Test + void 부모_댓글을_등록한다() { + // Given + CommentCreateRequest commentCreateRequest = new CommentCreateRequest( + "parent", null + ); + when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); + when(postRepository.getById(post.getId())).thenReturn(post); + when(commentRepository.save(any(Comment.class))).thenReturn(parentComment_1); + + // When + CommentCreateResponse commentCreateResponse = commentService.createComment( + siteUser.getEmail(), post.getId(), commentCreateRequest); + + // Then + assertEquals(commentCreateResponse, CommentCreateResponse.from(parentComment_1)); + verify(commentRepository, times(0)) + .getById(any(Long.class)); + verify(commentRepository, times(1)) + .save(commentCreateRequest.toEntity(siteUser, post, parentComment_1)); + } + + @Test + void 자식_댓글을_등록한다() { + // Given + Long parentCommentId = 1L; + CommentCreateRequest commentCreateRequest = new CommentCreateRequest( + "child", parentCommentId + ); + when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); + when(postRepository.getById(post.getId())).thenReturn(post); + when(commentRepository.getById(parentCommentId)).thenReturn(parentComment_1); + when(commentRepository.save(any(Comment.class))).thenReturn(p1s_childComment); + + // When + CommentCreateResponse commentCreateResponse = commentService.createComment( + siteUser.getEmail(), post.getId(), commentCreateRequest); + + // Then + assertEquals(commentCreateResponse, CommentCreateResponse.from(p1s_childComment)); + verify(commentRepository, times(1)) + .getById(parentCommentId); + verify(commentRepository, times(1)) + .save(commentCreateRequest.toEntity(siteUser, post, parentComment_1)); + } + + + @Test + void 댓글을_등록할_때_유효한_게시글이_아니라면_예외_응답을_반환한다() { + // Given + Long invalidPostId = -1L; + CommentCreateRequest commentCreateRequest = new CommentCreateRequest( + "child", null + ); + when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); + when(postRepository.getById(invalidPostId)).thenThrow(new CustomException(INVALID_POST_ID)); + + // When & Then + CustomException exception = assertThrows(CustomException.class, () -> + commentService.createComment(siteUser.getEmail(), invalidPostId, commentCreateRequest) + ); + assertThat(exception.getMessage()) + .isEqualTo(INVALID_POST_ID.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(INVALID_POST_ID.getCode()); + verify(commentRepository, times(0)) + .save(any(Comment.class)); + } + + @Test + void 댓글을_등록할_때_유효한_부모_댓글이_아니라면_예외_응답을_반환한다() { + // Given + Long invalidParentCommentId = -1L; + CommentCreateRequest commentCreateRequest = new CommentCreateRequest( + "child", invalidParentCommentId + ); + when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); + when(postRepository.getById(post.getId())).thenReturn(post); + when(commentRepository.getById(invalidParentCommentId)).thenThrow(new CustomException(INVALID_COMMENT_ID)); + + // When & Then + CustomException exception = assertThrows(CustomException.class, () -> + commentService.createComment(siteUser.getEmail(), post.getId(), commentCreateRequest) + ); + assertThat(exception.getMessage()) + .isEqualTo(INVALID_COMMENT_ID.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(INVALID_COMMENT_ID.getCode()); + verify(commentRepository, times(0)) + .save(any(Comment.class)); + } + + /** + * 댓글 수정 + */ + @Test + void 댓글을_수정한다() { + // Given + CommentUpdateRequest commentUpdateRequest = new CommentUpdateRequest( + "update" + ); + when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); + when(postRepository.getById(post.getId())).thenReturn(post); + when(commentRepository.getById(any())).thenReturn(parentComment_1); + + // When + CommentUpdateResponse commentUpdateResponse = commentService.updateComment( + siteUser.getEmail(), post.getId(), parentComment_1.getId(), commentUpdateRequest); + + // Then + assertEquals(commentUpdateResponse.id(), parentComment_1.getId()); + } + + @Test + void 댓글을_수정할_때_유효한_게시글이_아니라면_예외_응답을_반환한다() { + // Given + Long invalidPostId = -1L; + CommentUpdateRequest commentUpdateRequest = new CommentUpdateRequest( + "update" + ); + when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); + when(postRepository.getById(invalidPostId)).thenThrow(new CustomException(INVALID_POST_ID)); + + // When & Then + CustomException exception = assertThrows(CustomException.class, () -> + commentService.updateComment(siteUser.getEmail(), invalidPostId, parentComment_1.getId(), commentUpdateRequest) + ); + assertThat(exception.getMessage()) + .isEqualTo(INVALID_POST_ID.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(INVALID_POST_ID.getCode()); + } + + @Test + void 댓글을_수정할_때_유효한_댓글이_아니라면_예외_응답을_반환한다() { + // Given + Long invalidCommentId = -1L; + CommentUpdateRequest commentUpdateRequest = new CommentUpdateRequest( + "update" + ); + when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); + when(postRepository.getById(post.getId())).thenReturn(post); + when(commentRepository.getById(invalidCommentId)).thenThrow(new CustomException(INVALID_COMMENT_ID)); + + // When & Then + CustomException exception = assertThrows(CustomException.class, () -> + commentService.updateComment(siteUser.getEmail(), post.getId(), invalidCommentId, commentUpdateRequest) + ); + assertThat(exception.getMessage()) + .isEqualTo(INVALID_COMMENT_ID.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(INVALID_COMMENT_ID.getCode()); + } + + @Test + void 댓글을_수정할_때_이미_삭제된_댓글이라면_예외_응답을_반환한다() { + // Given + parentComment_1.deprecateComment(); + CommentUpdateRequest commentUpdateRequest = new CommentUpdateRequest( + "update" + ); + when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); + when(postRepository.getById(post.getId())).thenReturn(post); + when(commentRepository.getById(any())).thenReturn(parentComment_1); + + // When & Then + CustomException exception = assertThrows(CustomException.class, () -> + commentService.updateComment(siteUser.getEmail(), post.getId(), parentComment_1.getId(), commentUpdateRequest) + ); + assertThat(exception.getMessage()) + .isEqualTo(CAN_NOT_UPDATE_DEPRECATED_COMMENT.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(CAN_NOT_UPDATE_DEPRECATED_COMMENT.getCode()); + } + + @Test + void 댓글을_수정할_때_자신의_댓글이_아니라면_예외_응답을_반환한다() { + // Given + String invalidEmail = "invalidEmail@test.com"; + CommentUpdateRequest commentUpdateRequest = new CommentUpdateRequest( + "update" + ); + when(siteUserRepository.getByEmail(invalidEmail)).thenReturn(siteUser); + when(postRepository.getById(post.getId())).thenReturn(post); + when(commentRepository.getById(any())).thenReturn(parentComment_1); + + // When & Then + CustomException exception = assertThrows(CustomException.class, () -> + commentService.updateComment(invalidEmail, post.getId(), parentComment_1.getId(), commentUpdateRequest) + ); + assertThat(exception.getMessage()) + .isEqualTo(INVALID_POST_ACCESS.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(INVALID_POST_ACCESS.getCode()); + } + + /** + * 댓글 삭제 + */ + + @Test + void 댓글을_삭제한다_자식댓글_있음() { + // Given + Long parentCommentId = 1L; + when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); + when(postRepository.getById(post.getId())).thenReturn(post); + when(commentRepository.getById(any())).thenReturn(parentComment_1); + + // When + CommentDeleteResponse commentDeleteResponse = commentService.deleteCommentById( + siteUser.getEmail(), post.getId(), parentCommentId); + + // Then + assertEquals(parentComment_1.getContent(), null); + assertEquals(commentDeleteResponse.id(), parentCommentId); + verify(commentRepository, times(0)).deleteById(parentCommentId); + } + + @Test + void 댓글을_삭제한다_자식댓글_없음() { + // Given + Long childCommentId = 1L; + when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); + when(postRepository.getById(post.getId())).thenReturn(post); + when(commentRepository.getById(any())).thenReturn(p1s_childComment); + + // When + CommentDeleteResponse commentDeleteResponse = commentService.deleteCommentById( + siteUser.getEmail(), post.getId(), childCommentId); + + // Then + assertEquals(commentDeleteResponse.id(), childCommentId); + verify(commentRepository, times(1)).deleteById(childCommentId); + } + + @Test + void 댓글을_삭제할_때_유효한_게시글이_아니라면_예외_응답을_반환한다() { + // Given + Long invalidPostId = -1L; + when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); + when(postRepository.getById(invalidPostId)).thenThrow(new CustomException(INVALID_POST_ID)); + + // When & Then + CustomException exception = assertThrows(CustomException.class, () -> + commentService.deleteCommentById(siteUser.getEmail(), invalidPostId, parentComment_1.getId()) + ); + assertThat(exception.getMessage()) + .isEqualTo(INVALID_POST_ID.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(INVALID_POST_ID.getCode()); + } + + @Test + void 댓글을_삭제할_때_유효한_댓글이_아니라면_예외_응답을_반환한다() { + // Given + Long invalidCommentId = -1L; + when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); + when(postRepository.getById(post.getId())).thenReturn(post); + when(commentRepository.getById(invalidCommentId)).thenThrow(new CustomException(INVALID_COMMENT_ID)); + + // When & Then + CustomException exception = assertThrows(CustomException.class, () -> + commentService.deleteCommentById(siteUser.getEmail(), post.getId(), invalidCommentId) + ); + assertThat(exception.getMessage()) + .isEqualTo(INVALID_COMMENT_ID.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(INVALID_COMMENT_ID.getCode()); + } + + @Test + void 댓글을_삭제할_때_자신의_댓글이_아니라면_예외_응답을_반환한다() { + // Given + String invalidEmail = "invalidEmail@test.com"; + when(siteUserRepository.getByEmail(invalidEmail)).thenReturn(siteUser); + when(postRepository.getById(post.getId())).thenReturn(post); + when(commentRepository.getById(any())).thenReturn(parentComment_1); + + // When & Then + CustomException exception = assertThrows(CustomException.class, () -> + commentService.deleteCommentById(invalidEmail, post.getId(), parentComment_1.getId()) + ); + assertThat(exception.getMessage()) + .isEqualTo(INVALID_POST_ACCESS.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(INVALID_POST_ACCESS.getCode()); + } +} diff --git a/src/test/java/com/example/solidconnection/unit/service/PostServiceTest.java b/src/test/java/com/example/solidconnection/unit/service/PostServiceTest.java index c04c1485a..917d073f5 100644 --- a/src/test/java/com/example/solidconnection/unit/service/PostServiceTest.java +++ b/src/test/java/com/example/solidconnection/unit/service/PostServiceTest.java @@ -9,6 +9,8 @@ import com.example.solidconnection.custom.exception.ErrorCode; import com.example.solidconnection.dto.PostFindPostImageResponse; import com.example.solidconnection.entity.PostImage; +import com.example.solidconnection.post.domain.PostLike; +import com.example.solidconnection.post.repository.PostLikeRepository; import com.example.solidconnection.post.domain.Post; import com.example.solidconnection.post.dto.*; import com.example.solidconnection.post.repository.PostRepository; @@ -43,7 +45,7 @@ @ExtendWith(MockitoExtension.class) @DisplayName("게시글 서비스 테스트") -public class PostServiceTest { +class PostServiceTest { @InjectMocks PostService postService; @Mock @@ -53,6 +55,8 @@ public class PostServiceTest { @Mock BoardRepository boardRepository; @Mock + PostLikeRepository postLikeRepository; + @Mock S3Service s3Service; @Mock CommentService commentService; @@ -66,6 +70,7 @@ public class PostServiceTest { private Post post; private Post postWithImages; private Post questionPost; + private PostLike postLike; private List imageFiles; private List imageFilesWithMoreThanFiveFiles; private List uploadedFileUrlResponseList; @@ -81,6 +86,7 @@ void setUp() { post = createPost(board, siteUser); postWithImages = createPostWithImages(board, siteUser); questionPost = createQuestionPost(board, siteUser); + postLike = createPostLike(post, siteUser); } private SiteUser createSiteUser() { @@ -147,6 +153,12 @@ private Post createQuestionPost(Board board, SiteUser siteUser) { return post; } + private PostLike createPostLike(Post post, SiteUser siteUser) { + PostLike postLike = new PostLike(); + postLike.setPostAndSiteUser(post, siteUser); + return postLike; + } + private List createMockImageFiles() { List multipartFileList = new ArrayList<>(); multipartFileList.add(new MockMultipartFile("file1", "test1.png", @@ -241,6 +253,22 @@ private List createMockImageFilesWithMoreThanFiveFiles() { .isEqualTo(INVALID_BOARD_CODE.getCode()); } + @Test + void 게시글을_등록할_때_유효한_카테고리가_아니라면_예외_응답을_반환한다() { + // Given + String invalidPostCategory = "invalidPostCategory"; + PostCreateRequest postCreateRequest = new PostCreateRequest( + invalidPostCategory, "title", "content", false); + + // When & Then + CustomException exception = assertThrows(CustomException.class, () -> postService + .createPost(siteUser.getEmail(), board.getCode(), postCreateRequest, Collections.emptyList())); + assertThat(exception.getMessage()) + .isEqualTo(INVALID_POST_CATEGORY.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(INVALID_POST_CATEGORY.getCode()); + } + @Test void 게시글을_등록할_때_파일_수가_5개를_넘는다면_예외_응답을_반환한다() { // Given @@ -415,6 +443,8 @@ private List createMockImageFilesWithMoreThanFiveFiles() { // Given List commentFindResultDTOList = new ArrayList<>(); when(postRepository.getByIdUsingEntityGraph(post.getId())).thenReturn(post); + when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); + when(postLikeRepository.findPostLikeByPostAndSiteUser(post, siteUser)).thenReturn(Optional.empty()); when(commentService.findCommentsByPostId(siteUser.getEmail(), post.getId())).thenReturn(commentFindResultDTOList); // When @@ -424,6 +454,7 @@ private List createMockImageFilesWithMoreThanFiveFiles() { PostFindResponse expectedResponse = PostFindResponse.from( post, true, + false, PostFindBoardResponse.from(post.getBoard()), PostFindSiteUserResponse.from(post.getSiteUser()), commentFindResultDTOList, @@ -431,6 +462,8 @@ private List createMockImageFilesWithMoreThanFiveFiles() { ); assertEquals(expectedResponse, response); verify(postRepository, times(1)).getByIdUsingEntityGraph(post.getId()); + verify(siteUserRepository, times(1)).getByEmail(siteUser.getEmail()); + verify(postLikeRepository, times(1)).findPostLikeByPostAndSiteUser(post, siteUser); verify(commentService, times(1)).findCommentsByPostId(siteUser.getEmail(), post.getId()); } @@ -538,4 +571,125 @@ private List createMockImageFilesWithMoreThanFiveFiles() { assertThat(exception.getCode()) .isEqualTo(ErrorCode.CAN_NOT_DELETE_OR_UPDATE_QUESTION.getCode()); } + + /** + * 게시글 좋아요 + */ + @Test + void 게시글_좋아요를_등록한다() { + // Given + when(postRepository.getById(post.getId())).thenReturn(post); + when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); + + // When + PostLikeResponse postLikeResponse = postService.likePost(siteUser.getEmail(), board.getCode(), post.getId()); + + // Then + assertEquals(postLikeResponse, PostLikeResponse.from(post)); + verify(postLikeRepository, times(1)).save(any(PostLike.class)); + } + + @Test + void 게시글_좋아요를_등록할_때_중복된_좋아요라면_예외_응답을_반환한다() { + when(postRepository.getById(post.getId())).thenReturn(post); + when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); + when(postLikeRepository.findPostLikeByPostAndSiteUser(post, siteUser)).thenReturn(Optional.of(postLike)); + + // When & Then + CustomException exception = assertThrows(CustomException.class, () -> + postService.likePost(siteUser.getEmail(), board.getCode(), post.getId())); + assertThat(exception.getMessage()) + .isEqualTo(ErrorCode.DUPLICATE_POST_LIKE.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(ErrorCode.DUPLICATE_POST_LIKE.getCode()); + } + + @Test + void 게시글_좋아요를_등록할_때_유효한_게시판이_아니라면_예외_응답을_반환한다() { + // Given + String invalidBoardCode = "INVALID_CODE"; + + // When & Then + CustomException exception = assertThrows(CustomException.class, () -> + postService.likePost(siteUser.getEmail(), invalidBoardCode, post.getId())); + assertThat(exception.getMessage()) + .isEqualTo(ErrorCode.INVALID_BOARD_CODE.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(ErrorCode.INVALID_BOARD_CODE.getCode()); + } + + @Test + void 게시글_좋아요를_등록할_때_유효한_게시글이_아니라면_예외_응답을_반환한다() { + // Given + Long invalidPostId = -1L; + when(postRepository.getById(invalidPostId)).thenThrow(new CustomException(INVALID_POST_ID)); + + // When & Then + CustomException exception = assertThrows(CustomException.class, () -> + postService.likePost(siteUser.getEmail(), board.getCode(), invalidPostId)); + assertThat(exception.getMessage()) + .isEqualTo(INVALID_POST_ID.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(INVALID_POST_ID.getCode()); + } + + @Test + void 게시글_좋아요를_삭제한다() { + // Given + Long likeCount = post.getLikeCount(); + when(postRepository.getById(post.getId())).thenReturn(post); + when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); + when(postLikeRepository.getByPostAndSiteUser(post, siteUser)).thenReturn(postLike); + + // When + PostDislikeResponse postDislikeResponse = postService.dislikePost(siteUser.getEmail(), board.getCode(), post.getId()); + + // Then + assertEquals(postDislikeResponse, PostDislikeResponse.from(post)); + verify(postLikeRepository, times(1)).deleteById(post.getId()); + } + + @Test + void 게시글_좋아요를_삭제할_때_존재하지_않는_좋아요라면_예외_응답을_반환한다() { + when(postRepository.getById(post.getId())).thenReturn(post); + when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); + when(postLikeRepository.getByPostAndSiteUser(post, siteUser)).thenThrow(new CustomException(INVALID_POST_LIKE)); + + // When & Then + CustomException exception = assertThrows(CustomException.class, () -> + postService.dislikePost(siteUser.getEmail(), board.getCode(), post.getId())); + assertThat(exception.getMessage()) + .isEqualTo(ErrorCode.INVALID_POST_LIKE.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(ErrorCode.INVALID_POST_LIKE.getCode()); + } + + @Test + void 게시글_좋아요를_삭제할_때_유효한_게시판이_아니라면_예외_응답을_반환한다() { + // Given + String invalidBoardCode = "INVALID_CODE"; + + // When & Then + CustomException exception = assertThrows(CustomException.class, () -> + postService.dislikePost(siteUser.getEmail(), invalidBoardCode, post.getId())); + assertThat(exception.getMessage()) + .isEqualTo(ErrorCode.INVALID_BOARD_CODE.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(ErrorCode.INVALID_BOARD_CODE.getCode()); + } + + @Test + void 게시글_좋아요를_삭제할_때_유효한_게시글이_아니라면_예외_응답을_반환한다() { + // Given + Long invalidPostId = -1L; + when(postRepository.getById(invalidPostId)).thenThrow(new CustomException(INVALID_POST_ID)); + + // When & Then + CustomException exception = assertThrows(CustomException.class, () -> + postService.dislikePost(siteUser.getEmail(), board.getCode(), invalidPostId)); + assertThat(exception.getMessage()) + .isEqualTo(INVALID_POST_ID.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(INVALID_POST_ID.getCode()); + } }