diff --git a/backend/src/main/java/com/votogether/domain/member/entity/Member.java b/backend/src/main/java/com/votogether/domain/member/entity/Member.java index e9360903b..6b322287b 100644 --- a/backend/src/main/java/com/votogether/domain/member/entity/Member.java +++ b/backend/src/main/java/com/votogether/domain/member/entity/Member.java @@ -23,15 +23,13 @@ import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; -import lombok.ToString; import org.apache.commons.lang3.RandomStringUtils; -@Table(indexes = {@Index(columnList = "socialId, socialType")}) -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@EqualsAndHashCode(of = {"id"}) -@ToString @Getter @Entity +@EqualsAndHashCode(of = {"id"}, callSuper = false) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table(indexes = {@Index(columnList = "socialId, socialType")}) public class Member extends BaseEntity { private static final String INITIAL_NICKNAME_PREFIX = "익명의손님"; @@ -47,7 +45,6 @@ public class Member extends BaseEntity { @Column(length = 20) private Gender gender; - @Column private Integer birthYear; @Enumerated(value = EnumType.STRING) diff --git a/backend/src/main/java/com/votogether/domain/member/entity/vo/AgeRange.java b/backend/src/main/java/com/votogether/domain/member/entity/vo/AgeRange.java index 7e5e80846..b8dbc3efd 100644 --- a/backend/src/main/java/com/votogether/domain/member/entity/vo/AgeRange.java +++ b/backend/src/main/java/com/votogether/domain/member/entity/vo/AgeRange.java @@ -8,38 +8,28 @@ @Getter public enum AgeRange { - UNDER_TEENS("10대 미만", 1, 9), - TEENS("10대", 10, 19), - TWENTIES("20대", 20, 29), - THIRTIES("30대", 30, 39), - FORTIES("40대", 40, 49), - FIFTIES("50대", 50, 59), - OVER_SIXTIES("60대 이상", 60, 999), + UNDER_TEENS("10대 미만", 0), + TEENS("10대", 1), + TWENTIES("20대", 2), + THIRTIES("30대", 3), + FORTIES("40대", 4), + FIFTIES("50대", 5), + OVER_SIXTIES("60대 이상", 6), ; private final String name; - private final int startAge; - private final int endAge; + private final int ageGroup; - AgeRange( - final String name, - final int startAge, - final int endAge - ) { + AgeRange(final String name, final int ageGroup) { this.name = name; - this.startAge = startAge; - this.endAge = endAge; + this.ageGroup = ageGroup; } - public static AgeRange from(final int age) { + public static AgeRange from(final int ageGroup) { return Arrays.stream(AgeRange.values()) - .filter(ageRange -> ageRange.isBelong(age)) + .filter(ageRange -> ageRange.ageGroup == ageGroup) .findAny() .orElseThrow(() -> new BadRequestException(MemberExceptionType.INVALID_AGE)); } - private boolean isBelong(final int age) { - return this.startAge <= age && this.endAge >= age; - } - } diff --git a/backend/src/main/java/com/votogether/domain/member/entity/vo/Nickname.java b/backend/src/main/java/com/votogether/domain/member/entity/vo/Nickname.java index e4f397264..e77a60324 100644 --- a/backend/src/main/java/com/votogether/domain/member/entity/vo/Nickname.java +++ b/backend/src/main/java/com/votogether/domain/member/entity/vo/Nickname.java @@ -9,27 +9,28 @@ import lombok.Getter; import lombok.NoArgsConstructor; -@NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter @Embeddable +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class Nickname { private static final int MINIMUM_NICKNAME_LENGTH = 2; private static final int MAXIMUM_NICKNAME_LENGTH = 15; + private static final Pattern PATTERN = Pattern.compile("^[가-힣a-zA-Z0-9]+$"); @Column(name = "nickname", length = 20, unique = true, nullable = false) private String value; - public Nickname(final String nickname) { - validateNickname(nickname); - this.value = nickname; + public Nickname(final String value) { + validateNickname(value); + this.value = value; } - private void validateNickname(final String nickname) { - if (nickname.length() < MINIMUM_NICKNAME_LENGTH || nickname.length() > MAXIMUM_NICKNAME_LENGTH) { + private void validateNickname(final String value) { + if (value.length() < MINIMUM_NICKNAME_LENGTH || value.length() > MAXIMUM_NICKNAME_LENGTH) { throw new BadRequestException(MemberExceptionType.INVALID_NICKNAME_LENGTH); } - if (!Pattern.matches("^[가-힣a-zA-Z0-9]+$", nickname)) { + if (!PATTERN.matcher(value).matches()) { throw new BadRequestException(MemberExceptionType.INVALID_NICKNAME_LETTER); } } diff --git a/backend/src/main/java/com/votogether/domain/member/service/MemberService.java b/backend/src/main/java/com/votogether/domain/member/service/MemberService.java index 55705f9db..adc6bdb7f 100644 --- a/backend/src/main/java/com/votogether/domain/member/service/MemberService.java +++ b/backend/src/main/java/com/votogether/domain/member/service/MemberService.java @@ -116,7 +116,7 @@ private List deletePosts(final Member member) { } private List deleteComments(final Member member) { - final List comments = commentRepository.findAllByMember(member); + final List comments = commentRepository.findAllByWriter(member); final List commentIds = comments.stream() .map(Comment::getId) .toList(); diff --git a/backend/src/main/java/com/votogether/domain/post/controller/PostCommandController.java b/backend/src/main/java/com/votogether/domain/post/controller/PostCommandController.java new file mode 100644 index 000000000..b34687516 --- /dev/null +++ b/backend/src/main/java/com/votogether/domain/post/controller/PostCommandController.java @@ -0,0 +1,69 @@ +package com.votogether.domain.post.controller; + +import com.votogether.domain.member.entity.Member; +import com.votogether.domain.post.dto.request.post.PostCreateRequest; +import com.votogether.domain.post.dto.request.post.PostUpdateRequest; +import com.votogether.domain.post.service.PostCommandService; +import com.votogether.global.jwt.Auth; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Positive; +import java.net.URI; +import lombok.RequiredArgsConstructor; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Validated +@RequiredArgsConstructor +@RequestMapping("/posts") +@RestController +public class PostCommandController { + + private final PostCommandService postCommandService; + + @PostMapping(consumes = {MediaType.MULTIPART_FORM_DATA_VALUE, MediaType.APPLICATION_JSON_VALUE}) + public ResponseEntity createPost( + @ModelAttribute @Valid final PostCreateRequest postCreateRequest, + @Auth final Member loginMember + ) { + final Long postId = postCommandService.createPost(postCreateRequest, loginMember); + return ResponseEntity.created(URI.create("/posts/" + postId)).build(); + } + + @PutMapping(value = "/{postId}", consumes = {MediaType.MULTIPART_FORM_DATA_VALUE, MediaType.APPLICATION_JSON_VALUE}) + public ResponseEntity updatePost( + @PathVariable @Positive(message = "게시글 ID는 양의 정수만 가능합니다.") final Long postId, + @ModelAttribute @Valid final PostUpdateRequest postUpdateRequest, + @Auth final Member loginMember + ) { + postCommandService.updatePost(postId, postUpdateRequest, loginMember); + return ResponseEntity.ok().build(); + } + + @PatchMapping("/{postId}/close") + public ResponseEntity closePostEarly( + @PathVariable @Positive(message = "게시글 ID는 양의 정수만 가능합니다.") final Long postId, + @Auth final Member loginMember + ) { + postCommandService.closePostEarly(postId, loginMember); + return ResponseEntity.ok().build(); + } + + @DeleteMapping("/{postId}") + public ResponseEntity deletePost( + @PathVariable @Positive(message = "게시글 ID는 양의 정수만 가능합니다.") final Long postId, + @Auth final Member loginMember + ) { + postCommandService.deletePost(postId, loginMember); + return ResponseEntity.noContent().build(); + } + +} diff --git a/backend/src/main/java/com/votogether/domain/post/controller/PostCommandControllerDocs.java b/backend/src/main/java/com/votogether/domain/post/controller/PostCommandControllerDocs.java new file mode 100644 index 000000000..a9800bf15 --- /dev/null +++ b/backend/src/main/java/com/votogether/domain/post/controller/PostCommandControllerDocs.java @@ -0,0 +1,130 @@ +package com.votogether.domain.post.controller; + +import com.votogether.domain.member.entity.Member; +import com.votogether.domain.post.dto.request.post.PostCreateRequest; +import com.votogether.domain.post.dto.request.post.PostUpdateRequest; +import com.votogether.global.exception.ExceptionResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Positive; +import org.springframework.http.ResponseEntity; + +@Tag(name = "게시글 커맨드", description = "게시글 커맨드 API") +public interface PostCommandControllerDocs { + + @Operation(summary = "게시글 작성", description = "게시글을 작성한다.") + @ApiResponses({ + @ApiResponse( + responseCode = "201", + description = "게시글 작성 성공" + ), + @ApiResponse( + responseCode = "400", + description = "정상적이지 않은 요청", + content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) + ) + }) + ResponseEntity createPost( + @Valid final PostCreateRequest postCreateRequest, + final Member loginMember + ); + + @Operation(summary = "게시글 수정", description = "게시글을 수정한다.") + @ApiResponses({ + @ApiResponse( + responseCode = "200", + description = "게시글 수정 성공" + ), + @ApiResponse( + responseCode = "400", + description = """ + 1.게시글 ID가 양의 정수가 아닌 경우 + + 2.정상적이지 않은 요청 + + 3.게시글이 블라인드 처리된 경우 + + 4.게시글 작성자가 아닌 경우 + """, + content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) + ), + @ApiResponse( + responseCode = "404", + description = "존재하지 않는 게시글", + content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) + ) + }) + ResponseEntity updatePost( + @Parameter(description = "게시글 ID", example = "1") + @Positive(message = "게시글 ID는 양의 정수만 가능합니다.") final Long postId, + @Valid final PostUpdateRequest postUpdateRequest, + final Member loginMember + ); + + @Operation(summary = "게시글 조기 마감", description = "게시글을 조기 마감한다.") + @ApiResponses({ + @ApiResponse( + responseCode = "200", + description = "게시글 조기 마감 성공" + ), + @ApiResponse( + responseCode = "400", + description = """ + 1.게시글 ID가 양의 정수가 아닌 경우 + + 2.게시글이 블라인드 처리된 경우 + + 3.게시글 작성자가 아닌 경우 + """, + content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) + ), + @ApiResponse( + responseCode = "404", + description = "존재하지 않는 게시글", + content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) + ) + }) + ResponseEntity closePostEarly( + @Parameter(description = "게시글 ID", example = "1") + @Positive(message = "게시글 ID는 양의 정수만 가능합니다.") final Long postId, + final Member loginMember + ); + + @Operation(summary = "게시글 삭제", description = "게시글을 삭제한다.") + @ApiResponses({ + @ApiResponse( + responseCode = "204", + description = "게시글 삭제 성공" + ), + @ApiResponse( + responseCode = "400", + description = """ + 1.게시글 ID가 양의 정수가 아닌 경우 + + 2.게시글이 블라인드 처리된 경우 + + 3.게시글 작성자가 아닌 경우 + + 4.삭제할 수 없는 투표 수가 존재하는 게시글인 경우 + """, + content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) + ), + @ApiResponse( + responseCode = "404", + description = "존재하지 않는 게시글", + content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) + ) + }) + ResponseEntity deletePost( + @Parameter(description = "게시글 ID", example = "1") + @Positive(message = "게시글 ID는 양의 정수만 가능합니다.") final Long postId, + final Member loginMember + ); + +} diff --git a/backend/src/main/java/com/votogether/domain/post/controller/PostCommentController.java b/backend/src/main/java/com/votogether/domain/post/controller/PostCommentController.java index 0d2ef6e6b..3cc78842e 100644 --- a/backend/src/main/java/com/votogether/domain/post/controller/PostCommentController.java +++ b/backend/src/main/java/com/votogether/domain/post/controller/PostCommentController.java @@ -1,16 +1,18 @@ package com.votogether.domain.post.controller; import com.votogether.domain.member.entity.Member; -import com.votogether.domain.post.dto.request.comment.CommentRegisterRequest; +import com.votogether.domain.post.dto.request.comment.CommentCreateRequest; import com.votogether.domain.post.dto.request.comment.CommentUpdateRequest; import com.votogether.domain.post.dto.response.comment.CommentResponse; import com.votogether.domain.post.service.PostCommentService; import com.votogether.global.jwt.Auth; import jakarta.validation.Valid; +import jakarta.validation.constraints.Positive; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -20,6 +22,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +@Validated @RequiredArgsConstructor @RequestMapping("/posts") @RestController @@ -28,39 +31,41 @@ public class PostCommentController implements PostCommentControllerDocs { private final PostCommentService postCommentService; @GetMapping("/{postId}/comments") - public ResponseEntity> getComments(@PathVariable final Long postId) { + public ResponseEntity> getComments( + @PathVariable @Positive(message = "게시글 ID는 양의 정수만 가능합니다.") final Long postId + ) { final List response = postCommentService.getComments(postId); return ResponseEntity.ok(response); } @PostMapping("/{postId}/comments") public ResponseEntity createComment( - @PathVariable final Long postId, - @Valid @RequestBody CommentRegisterRequest commentRegisterRequest, - @Auth final Member member + @PathVariable @Positive(message = "게시글 ID는 양의 정수만 가능합니다.") final Long postId, + @RequestBody @Valid CommentCreateRequest commentCreateRequest, + @Auth final Member loginMember ) { - postCommentService.createComment(member, postId, commentRegisterRequest); + postCommentService.createComment(postId, commentCreateRequest, loginMember); return ResponseEntity.status(HttpStatus.CREATED).build(); } @PutMapping("/{postId}/comments/{commentId}") public ResponseEntity updateComment( - @PathVariable final Long postId, - @PathVariable final Long commentId, + @PathVariable @Positive(message = "게시글 ID는 양의 정수만 가능합니다.") final Long postId, + @PathVariable @Positive(message = "댓글 ID는 양의 정수만 가능합니다.") final Long commentId, @RequestBody @Valid final CommentUpdateRequest commentUpdateRequest, - @Auth final Member member + @Auth final Member loginMember ) { - postCommentService.updateComment(postId, commentId, commentUpdateRequest, member); + postCommentService.updateComment(postId, commentId, commentUpdateRequest, loginMember); return ResponseEntity.ok().build(); } @DeleteMapping("/{postId}/comments/{commentId}") public ResponseEntity deleteComment( - @PathVariable final Long postId, - @PathVariable final Long commentId, - @Auth final Member member + @PathVariable @Positive(message = "게시글 ID는 양의 정수만 가능합니다.") final Long postId, + @PathVariable @Positive(message = "댓글 ID는 양의 정수만 가능합니다.") final Long commentId, + @Auth final Member loginMember ) { - postCommentService.deleteComment(postId, commentId, member); + postCommentService.deleteComment(postId, commentId, loginMember); return ResponseEntity.noContent().build(); } diff --git a/backend/src/main/java/com/votogether/domain/post/controller/PostCommentControllerDocs.java b/backend/src/main/java/com/votogether/domain/post/controller/PostCommentControllerDocs.java index fecb9b3d8..c5572b0db 100644 --- a/backend/src/main/java/com/votogether/domain/post/controller/PostCommentControllerDocs.java +++ b/backend/src/main/java/com/votogether/domain/post/controller/PostCommentControllerDocs.java @@ -1,7 +1,7 @@ package com.votogether.domain.post.controller; import com.votogether.domain.member.entity.Member; -import com.votogether.domain.post.dto.request.comment.CommentRegisterRequest; +import com.votogether.domain.post.dto.request.comment.CommentCreateRequest; import com.votogether.domain.post.dto.request.comment.CommentUpdateRequest; import com.votogether.domain.post.dto.response.comment.CommentResponse; import com.votogether.global.exception.ExceptionResponse; @@ -12,6 +12,8 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Positive; import java.util.List; import org.springframework.http.ResponseEntity; @@ -20,7 +22,19 @@ public interface PostCommentControllerDocs { @Operation(summary = "게시글 댓글 목록 조회", description = "게시글 댓글 목록을 조회한다.") @ApiResponses({ - @ApiResponse(responseCode = "200", description = "게시글 댓글 목록 조회 성공"), + @ApiResponse( + responseCode = "200", + description = "게시글 댓글 목록 조회 성공" + ), + @ApiResponse( + responseCode = "400", + description = """ + 1.양의 정수가 아닌 게시글 ID + + 2.게시글이 블라인드 처리된 경우 + """, + content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) + ), @ApiResponse( responseCode = "404", description = "존재하지 않는 게시글", @@ -28,12 +42,27 @@ public interface PostCommentControllerDocs { ) }) ResponseEntity> getComments( - @Parameter(description = "댓글 작성 게시글 ID", example = "1") final Long postId + @Parameter(description = "댓글 작성 게시글 ID", example = "1") + @Positive(message = "게시글 ID는 양의 정수만 가능합니다.") final Long postId ); @Operation(summary = "게시글 댓글 작성", description = "게시글 댓글을 작성한다.") @ApiResponses({ - @ApiResponse(responseCode = "200", description = "게시글 댓글 작성 성공"), + @ApiResponse( + responseCode = "201", + description = "게시글 댓글 작성 성공" + ), + @ApiResponse( + responseCode = "400", + description = """ + 1.양의 정수가 아닌 게시글 ID + + 2.존재하지 않는 댓글 내용 + + 3.게시글이 블라인드 처리된 경우 + """, + content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) + ), @ApiResponse( responseCode = "404", description = "존재하지 않는 게시글", @@ -41,50 +70,95 @@ ResponseEntity> getComments( ) }) ResponseEntity createComment( - @Parameter(description = "댓글 작성 게시글 ID", example = "1") final Long postId, - final CommentRegisterRequest commentRegisterRequest, - final Member member + @Parameter(description = "댓글 작성 게시글 ID", example = "1") + @Positive(message = "게시글 ID는 양의 정수만 가능합니다.") final Long postId, + @Valid final CommentCreateRequest commentCreateRequest, + final Member loginMember ); @Operation(summary = "게시글 댓글 수정", description = "게시글 댓글을 수정한다.") @ApiResponses({ - @ApiResponse(responseCode = "200", description = "게시글 댓글 수정 성공"), + @ApiResponse( + responseCode = "200", + description = "게시글 댓글 수정 성공" + ), @ApiResponse( responseCode = "400", - description = "1.게시글에 속하지 않은 댓글\t\n2.올바르지 않은 댓글 작성자", + description = """ + 1.양의 정수가 아닌 게시글 ID + + 2.양의 정수가 아닌 댓글 ID + + 3.존재하지 않는 댓글 내용 + + 4.게시글이 블라인드 처리된 경우 + + 5.댓글이 블라인드 처리된 경우 + + 6.게시글의 댓글이 아닌 경우 + + 7.댓글 작성자가 아닌 경우 + """, content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) ), @ApiResponse( responseCode = "404", - description = "1.존재하지 않는 게시글\t\n2.존재하지 않는 댓글", + description = """ + 1.존재하지 않는 게시글 + + 2.존재하지 않는 댓글 + """, content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) ) }) ResponseEntity updateComment( - @Parameter(description = "게시글 ID", example = "1") final Long postId, - @Parameter(description = "댓글 ID", example = "1") final Long commentId, - final CommentUpdateRequest commentUpdateRequest, - final Member member + @Parameter(description = "게시글 ID", example = "1") + @Positive(message = "게시글 ID는 양의 정수만 가능합니다.") final Long postId, + @Parameter(description = "댓글 ID", example = "1") + @Positive(message = "댓글 ID는 양의 정수만 가능합니다.") final Long commentId, + @Valid final CommentUpdateRequest commentUpdateRequest, + final Member loginMember ); @Operation(summary = "게시글 댓글 삭제", description = "게시글 댓글을 삭제한다.") @ApiResponses({ - @ApiResponse(responseCode = "204", description = "게시글 댓글 삭제 성공"), + @ApiResponse( + responseCode = "204", + description = "게시글 댓글 삭제 성공" + ), @ApiResponse( responseCode = "400", - description = "1.게시글에 속하지 않은 댓글\t\n2.올바르지 않은 댓글 작성자", + description = """ + 1.양의 정수가 아닌 게시글 ID + + 2.양의 정수가 아닌 댓글 ID + + 3.게시글이 블라인드 처리된 경우 + + 4.댓글이 블라인드 처리된 경우 + + 5.게시글의 댓글이 아닌 경우 + + 6.댓글 작성자가 아닌 경우 + """, content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) ), @ApiResponse( responseCode = "404", - description = "1.존재하지 않는 게시글\t\n2.존재하지 않는 댓글", + description = """ + 1.존재하지 않는 게시글 + + 2.존재하지 않는 댓글 + """, content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) ) }) ResponseEntity deleteComment( - @Parameter(description = "게시글 ID", example = "1") final Long postId, - @Parameter(description = "댓글 ID", example = "1") final Long commentId, - final Member member + @Parameter(description = "게시글 ID", example = "1") + @Positive(message = "게시글 ID는 양의 정수만 가능합니다.") final Long postId, + @Parameter(description = "댓글 ID", example = "1") + @Positive(message = "댓글 ID는 양의 정수만 가능합니다.") final Long commentId, + final Member loginMember ); } diff --git a/backend/src/main/java/com/votogether/domain/post/controller/PostController.java b/backend/src/main/java/com/votogether/domain/post/controller/PostController.java deleted file mode 100644 index 076fc86c0..000000000 --- a/backend/src/main/java/com/votogether/domain/post/controller/PostController.java +++ /dev/null @@ -1,227 +0,0 @@ -package com.votogether.domain.post.controller; - -import com.votogether.domain.member.entity.Member; -import com.votogether.domain.post.dto.request.post.PostCreateRequest; -import com.votogether.domain.post.dto.request.post.PostUpdateRequest; -import com.votogether.domain.post.dto.response.post.PostDetailResponse; -import com.votogether.domain.post.dto.response.post.PostRankingResponse; -import com.votogether.domain.post.dto.response.post.PostResponse; -import com.votogether.domain.post.dto.response.vote.VoteOptionStatisticsResponse; -import com.votogether.domain.post.entity.vo.PostClosingType; -import com.votogether.domain.post.entity.vo.PostSortType; -import com.votogether.domain.post.service.PostService; -import com.votogether.global.jwt.Auth; -import jakarta.validation.Valid; -import java.net.URI; -import java.util.List; -import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PatchMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RequestPart; -import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.multipart.MultipartFile; - -@RequiredArgsConstructor -@RequestMapping("/posts") -@RestController -public class PostController implements PostControllerDocs { - - private final PostService postService; - - @GetMapping("/search") - public ResponseEntity> searchPostsWithKeyword( - @RequestParam final String keyword, - @RequestParam final int page, - @RequestParam final PostClosingType postClosingType, - @RequestParam final PostSortType postSortType, - @RequestParam(required = false, name = "category") final Long categoryId, - @Auth final Member member - ) { - final List responses = - postService.searchPostsWithKeyword(keyword, page, postClosingType, postSortType, categoryId, member); - return ResponseEntity.ok(responses); - } - - @GetMapping("/search/guest") - public ResponseEntity> searchPostsWithKeywordForGuest( - @RequestParam final String keyword, - @RequestParam final int page, - @RequestParam final PostClosingType postClosingType, - @RequestParam final PostSortType postSortType, - @RequestParam(required = false, name = "category") final Long categoryId - ) { - final List responses = - postService.searchPostsWithKeywordForGuest(keyword, page, postClosingType, postSortType, categoryId); - return ResponseEntity.ok(responses); - } - - @GetMapping("/me") - public ResponseEntity> getPostsByMe( - @RequestParam final int page, - @RequestParam final PostClosingType postClosingType, - @RequestParam final PostSortType postSortType, - @RequestParam(required = false, name = "category") final Long categoryId, - @Auth final Member member - ) { - final List responses = - postService.getPostsByWriter(page, postClosingType, postSortType, categoryId, member); - return ResponseEntity.ok(responses); - } - - @GetMapping - public ResponseEntity> getAllPost( - @RequestParam final int page, - @RequestParam final PostClosingType postClosingType, - @RequestParam final PostSortType postSortType, - @RequestParam(name = "category", required = false) final Long categoryId, - @Auth final Member member - ) { - final List responses = postService.getAllPostBySortTypeAndClosingTypeAndCategoryId( - page, - postClosingType, - postSortType, - categoryId, - member - ); - return ResponseEntity.ok(responses); - } - - @GetMapping("/guest") - public ResponseEntity> getPostsGuest( - @RequestParam final int page, - @RequestParam final PostClosingType postClosingType, - @RequestParam final PostSortType postSortType, - @RequestParam(required = false, name = "category") final Long categoryId - ) { - final List response = postService.getPostsGuest(page, postClosingType, postSortType, categoryId); - return ResponseEntity.ok(response); - } - - @GetMapping("{postId}") - public ResponseEntity getPost( - @PathVariable final Long postId, - @Auth final Member member - ) { - final PostDetailResponse response = postService.getPostById(postId, member); - return ResponseEntity.ok(response); - } - - @GetMapping("{postId}/guest") - public ResponseEntity getPostByGuest( - @PathVariable final Long postId - ) { - final PostDetailResponse response = postService.getPostById(postId, null); - return ResponseEntity.ok(response); - } - - @GetMapping("/{postId}/options") - public ResponseEntity getVoteStatistics( - @PathVariable final Long postId, - @Auth final Member member - ) { - final VoteOptionStatisticsResponse response = postService.getVoteStatistics(postId, member); - return ResponseEntity.ok(response); - } - - @GetMapping("/{postId}/options/{optionId}") - public ResponseEntity getVoteOptionStatistics( - @PathVariable final Long postId, - @PathVariable final Long optionId, - @Auth final Member member - ) { - final VoteOptionStatisticsResponse response = postService.getVoteOptionStatistics(postId, optionId, member); - return ResponseEntity.ok(response); - } - - @GetMapping("/votes/me") - public ResponseEntity> getPostsVotedByMe( - final int page, - final PostClosingType postClosingType, - final PostSortType postSortType, - @Auth final Member member - ) { - final List posts = postService.getPostsVotedByMember(page, postClosingType, postSortType, member); - return ResponseEntity.status(HttpStatus.OK).body(posts); - } - - @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE) - public ResponseEntity save( - @RequestPart @Valid final PostCreateRequest request, - @RequestPart final List contentImages, - @RequestPart final List optionImages, - @Auth final Member member - ) { - System.out.println("PostController.save"); - - System.out.println("contentImages = " + contentImages); - if (contentImages != null && !contentImages.isEmpty()) { - System.out.println("contentImages = " + contentImages.get(0).getOriginalFilename()); - } - - System.out.println("optionImages = " + optionImages); - if (optionImages != null && !optionImages.isEmpty()) { - System.out.println("optionImages1 = " + optionImages.get(0).getOriginalFilename()); - System.out.println("optionImages2 = " + optionImages.get(1).getOriginalFilename()); - } - final Long postId = postService.save(request, member, contentImages, optionImages); - return ResponseEntity.created(URI.create("/posts/" + postId)).build(); - } - - @GetMapping("ranking/popular/guest") - public ResponseEntity> getRanking() { - final List responses = postService.getRanking(); - return ResponseEntity.ok(responses); - } - - @PutMapping(value = "/{postId}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) - public ResponseEntity update( - @PathVariable final Long postId, - @RequestPart @Valid final PostUpdateRequest request, - @RequestPart final List contentImages, - @RequestPart final List optionImages, - @Auth final Member member - ) { - System.out.println("PostController.update"); - - System.out.println("contentImages = " + contentImages); - if (contentImages != null && !contentImages.isEmpty()) { - System.out.println("contentImages = " + contentImages.get(0).getOriginalFilename()); - } - - System.out.println("optionImages = " + optionImages); - if (optionImages != null && !optionImages.isEmpty()) { - System.out.println("optionImages1 = " + optionImages.get(0).getOriginalFilename()); - System.out.println("optionImages2 = " + optionImages.get(1).getOriginalFilename()); - } - - postService.update(postId, request, member, contentImages, optionImages); - return ResponseEntity.ok().build(); - } - - @PatchMapping("/{postId}/close") - public ResponseEntity closePostEarly( - @PathVariable final Long postId, - @Auth final Member loginMember - ) { - postService.closePostEarlyById(postId, loginMember); - return ResponseEntity.ok().build(); - } - - @DeleteMapping("/{postId}") - public ResponseEntity delete(@PathVariable final Long postId) { - postService.delete(postId); - return ResponseEntity.noContent().build(); - } - -} - - diff --git a/backend/src/main/java/com/votogether/domain/post/controller/PostControllerDocs.java b/backend/src/main/java/com/votogether/domain/post/controller/PostControllerDocs.java deleted file mode 100644 index e87b40d0f..000000000 --- a/backend/src/main/java/com/votogether/domain/post/controller/PostControllerDocs.java +++ /dev/null @@ -1,254 +0,0 @@ -package com.votogether.domain.post.controller; - -import com.votogether.domain.member.entity.Member; -import com.votogether.domain.post.dto.request.post.PostCreateRequest; -import com.votogether.domain.post.dto.request.post.PostUpdateRequest; -import com.votogether.domain.post.dto.response.post.PostDetailResponse; -import com.votogether.domain.post.dto.response.post.PostRankingResponse; -import com.votogether.domain.post.dto.response.post.PostResponse; -import com.votogether.domain.post.dto.response.vote.VoteOptionStatisticsResponse; -import com.votogether.domain.post.entity.vo.PostClosingType; -import com.votogether.domain.post.entity.vo.PostSortType; -import com.votogether.global.exception.ExceptionResponse; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import java.util.List; -import org.springframework.http.ResponseEntity; -import org.springframework.web.multipart.MultipartFile; - -@Tag(name = "게시글", description = "게시글 API") -public interface PostControllerDocs { - - @Operation(summary = "[회원] 전체 게시글 목록 조회", description = "[회원] 전체 게시글 목록을 조회한다.") - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "전체 게시글 목록 조회 성공"), - @ApiResponse( - responseCode = "400", - description = "잘못된 입력", - content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) - ) - }) - ResponseEntity> getAllPost( - @Parameter(description = "현재 페이지 위치", example = "0") final int page, - @Parameter(description = "게시글 마감 여부", example = "ALL") final PostClosingType postClosingType, - @Parameter(description = "게시글 정렬 기준", example = "HOT") final PostSortType postSortType, - @Parameter(description = "카테고리 ID", example = "1") final Long categoryId, - final Member member - ); - - @Operation(summary = "[비회원] 전체 게시글 목록 조회", description = "[비회원] 전체 게시글 목록을 조회한다.") - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "전체 게시글 목록 조회 성공"), - @ApiResponse( - responseCode = "400", - description = "잘못된 입력", - content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) - ) - }) - ResponseEntity> getPostsGuest( - @Parameter(description = "현재 페이지 위치", example = "0") final int page, - @Parameter(description = "게시글 마감 여부", example = "ALL") final PostClosingType postClosingType, - @Parameter(description = "게시글 정렬 기준", example = "HOT") final PostSortType postSortType, - @Parameter(description = "카테고리 ID", example = "1") final Long categoryId - ); - - @Operation(summary = "[회원] 게시글 상세 조회", description = "[회원] 게시글을 상세 조회 한다.") - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "게시글 상세 조회 성공"), - @ApiResponse( - responseCode = "404", - description = "존재하지 않는 게시글", - content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) - ) - }) - ResponseEntity getPost( - @Parameter(description = "게시글 ID", example = "1") final Long postId, - final Member member - ); - - @Operation(summary = "[비회원] 게시글 상세 조회", description = "[비회원] 게시글을 상세 조회 한다.") - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "게시글 상세 조회 성공"), - @ApiResponse( - responseCode = "404", - description = "존재하지 않는 게시글", - content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) - ) - }) - ResponseEntity getPostByGuest( - @Parameter(description = "게시글 ID", example = "1") final Long postId - ); - - @Operation(summary = "게시글 투표 통계 조회", description = "게시글 투표에 대한 전체 통계를 조회한다.") - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "게시글 투표 통계 조회 성공"), - @ApiResponse( - responseCode = "404", - description = "존재하지 않는 게시글", - content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) - ) - }) - ResponseEntity getVoteStatistics( - @Parameter(description = "게시글 ID", example = "1") final Long postId, - final Member member - ); - - @Operation(summary = "게시글 투표 선택지 통계 조회", description = "게시글 특정 투표 선택지에 대한 통계를 조회한다.") - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "게시글 투표 선택지 통계 조회 성공"), - @ApiResponse( - responseCode = "400", - description = "게시글에 속하지 않는 투표 옵션", - content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) - ), - @ApiResponse( - responseCode = "404", - description = "1.존재하지 않는 게시글\t\n2.존재하지 않는 게시글 투표 옵션", - content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) - ) - }) - ResponseEntity getVoteOptionStatistics( - @Parameter(description = "게시글 ID", example = "1") final Long postId, - @Parameter(description = "게시글 선택지 ID", example = "2") final Long optionId, - final Member member - ); - - @Operation(summary = "투표한 게시글 목록 조회", description = "투표한 게시글 목록을 조회한다.") - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "투표한 게시글 목록 조회 성공"), - @ApiResponse( - responseCode = "400", - description = "잘못된 입력", - content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) - ) - }) - ResponseEntity> getPostsVotedByMe( - @Parameter(description = "현재 페이지 위치", example = "0") final int page, - @Parameter(description = "게시글 마감 여부", example = "ALL") final PostClosingType postClosingType, - @Parameter(description = "게시글 정렬 기준", example = "HOT") final PostSortType postSortType, - final Member member - ); - - - @Operation(summary = "[회원] 게시글 검색", description = "[회원] 키워드를 통해 게시글을 검색한다.") - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "게시글 검색 성공"), - @ApiResponse( - responseCode = "400", - description = "잘못된 입력", - content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) - ) - }) - ResponseEntity> searchPostsWithKeyword( - @Parameter(description = "검색 키워드", example = "취업") final String keyword, - @Parameter(description = "현재 페이지 위치", example = "0") final int page, - @Parameter(description = "게시글 마감 여부", example = "ALL") final PostClosingType postClosingType, - @Parameter(description = "게시글 정렬 기준", example = "HOT") final PostSortType postSortType, - @Parameter(description = "카테고리 ID", example = "1") final Long categoryId, - final Member member - ); - - @Operation(summary = "[비회원] 게시글 검색", description = "[비회원] 키워드를 통해 게시글을 검색한다.") - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "게시글 검색 성공"), - @ApiResponse( - responseCode = "400", - description = "잘못된 입력", - content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) - ) - }) - ResponseEntity> searchPostsWithKeywordForGuest( - @Parameter(description = "검색 키워드", example = "취업") final String keyword, - @Parameter(description = "현재 페이지 위치", example = "0") final int page, - @Parameter(description = "게시글 마감 여부", example = "ALL") final PostClosingType postClosingType, - @Parameter(description = "게시글 정렬 기준", example = "HOT") final PostSortType postSortType, - @Parameter(description = "카테고리 ID", example = "1") final Long categoryId - ); - - @Operation(summary = "작성한 게시글 목록 조회", description = "작성한 게시글 목록을 조회한다.") - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "작성한 게시글 목록 조회 성공"), - @ApiResponse( - responseCode = "400", - description = "잘못된 입력", - content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) - ) - }) - ResponseEntity> getPostsByMe( - @Parameter(description = "현재 페이지 위치", example = "0") final int page, - @Parameter(description = "게시글 마감 여부", example = "ALL") final PostClosingType postClosingType, - @Parameter(description = "게시글 정렬 기준", example = "HOT") final PostSortType postSortType, - @Parameter(description = "카테고리 ID", example = "1") final Long categoryId, - final Member member - ); - - @Operation(summary = "인기 게시글 랭킹 조회", description = "인기 게시글 랭킹을 조회한다.") - @ApiResponse(responseCode = "200", description = "인기 게시글 랭킹 조회 성공") - ResponseEntity> getRanking(); - - @Operation(summary = "게시글 작성", description = "게시글을 작성한다.") - @ApiResponses({ - @ApiResponse(responseCode = "201", description = "게시글 작성 성공"), - @ApiResponse( - responseCode = "400", - description = "잘못된 입력입니다.", - content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) - ) - }) - ResponseEntity save( - final PostCreateRequest request, - final List contentImages, - final List optionImages, - final Member member - ); - - @Operation(summary = "게시글 수정", description = "게시글을 수정한다.") - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "게시글 수정 성공"), - @ApiResponse( - responseCode = "400", - description = "잘못된 입력", - content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) - ) - }) - ResponseEntity update( - @Parameter(description = "게시글 ID", example = "1") final Long postId, - final PostUpdateRequest request, - final List contentImages, - final List optionImages, - final Member member - ); - - @Operation(summary = "게시글 조기 마감", description = "게시글을 조기 마감한다.") - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "게시물 조기 마감 성공."), - @ApiResponse( - responseCode = "400", - description = "잘못된 입력", - content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) - ) - }) - ResponseEntity closePostEarly( - @Parameter(description = "게시글 ID", example = "1") final Long postId, - final Member loginMember - ); - - @Operation(summary = "게시글 삭제", description = "게시글을 삭제한다.") - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "게시물 삭제 성공"), - @ApiResponse( - responseCode = "400", - description = "잘못된 입력", - content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) - ) - }) - ResponseEntity delete( - @Parameter(description = "게시글 ID", example = "1") final Long postId - ); - -} diff --git a/backend/src/main/java/com/votogether/domain/post/controller/PostGuestController.java b/backend/src/main/java/com/votogether/domain/post/controller/PostGuestController.java new file mode 100644 index 000000000..13dc7ea57 --- /dev/null +++ b/backend/src/main/java/com/votogether/domain/post/controller/PostGuestController.java @@ -0,0 +1,57 @@ +package com.votogether.domain.post.controller; + +import com.votogether.domain.post.dto.response.post.PostResponse; +import com.votogether.domain.post.entity.vo.PostClosingType; +import com.votogether.domain.post.entity.vo.PostSortType; +import com.votogether.domain.post.service.PostGuestService; +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.PositiveOrZero; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@Validated +@RequiredArgsConstructor +@RequestMapping("/posts") +@RestController +public class PostGuestController implements PostGuestControllerDocs { + + private final PostGuestService postGuestService; + + @GetMapping("/guest") + public ResponseEntity> getPosts( + @RequestParam @PositiveOrZero(message = "페이지는 0이상 정수만 가능합니다.") final int page, + @RequestParam final PostClosingType postClosingType, + @RequestParam final PostSortType postSortType, + @RequestParam(name = "category", required = false) final Long categoryId + ) { + final List response = postGuestService.getPosts(page, postClosingType, postSortType, categoryId); + return ResponseEntity.ok(response); + } + + @GetMapping("{postId}/guest") + public ResponseEntity getPost( + @PathVariable @Positive(message = "게시글 ID는 양의 정수만 가능합니다.") final Long postId + ) { + final PostResponse response = postGuestService.getPost(postId); + return ResponseEntity.ok(response); + } + + @GetMapping("/search/guest") + public ResponseEntity> searchPosts( + @RequestParam final String keyword, + @RequestParam @PositiveOrZero(message = "페이지는 0이상 정수만 가능합니다.") final int page, + @RequestParam final PostClosingType postClosingType, + @RequestParam final PostSortType postSortType + ) { + final List responses = postGuestService.searchPosts(keyword, page, postClosingType, postSortType); + return ResponseEntity.ok(responses); + } + +} diff --git a/backend/src/main/java/com/votogether/domain/post/controller/PostGuestControllerDocs.java b/backend/src/main/java/com/votogether/domain/post/controller/PostGuestControllerDocs.java new file mode 100644 index 000000000..4580ad0ea --- /dev/null +++ b/backend/src/main/java/com/votogether/domain/post/controller/PostGuestControllerDocs.java @@ -0,0 +1,88 @@ +package com.votogether.domain.post.controller; + +import com.votogether.domain.post.dto.response.post.PostResponse; +import com.votogether.domain.post.entity.vo.PostClosingType; +import com.votogether.domain.post.entity.vo.PostSortType; +import com.votogether.global.exception.ExceptionResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.PositiveOrZero; +import java.util.List; +import org.springframework.http.ResponseEntity; + +@Tag(name = "비회원 게시글", description = "비회원 게시글 API") +public interface PostGuestControllerDocs { + + @Operation(summary = "비회원 게시글 목록 조회", description = "비회원이 게시글 목록을 조회한다.") + @ApiResponses({ + @ApiResponse( + responseCode = "200", + description = "비회원 게시글 목록 조회 성공" + ), + @ApiResponse( + responseCode = "400", + description = "0 이상의 정수가 아닌 페이지", + content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) + ) + }) + ResponseEntity> getPosts( + @Parameter(description = "현재 페이지 위치", example = "0") + @PositiveOrZero(message = "페이지는 0이상 정수만 가능합니다.") final int page, + @Parameter(description = "게시글 마감 여부", example = "ALL") final PostClosingType postClosingType, + @Parameter(description = "게시글 정렬 기준", example = "HOT") final PostSortType postSortType, + @Parameter(description = "카테고리 ID", example = "1") final Long categoryId + ); + + @Operation(summary = "비회원 게시글 상세 조회", description = "비회원이 게시글을 상세 조회한다.") + @ApiResponses({ + @ApiResponse( + responseCode = "200", + description = "비회원 게시글 상세 조회 성공" + ), + @ApiResponse( + responseCode = "400", + description = """ + 1.양의 정수가 아닌 게시글 ID + + 2.게시글이 블라인드 처리된 경우 + """, + content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) + ), + @ApiResponse( + responseCode = "404", + description = "존재하지 않는 게시글", + content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) + ) + }) + ResponseEntity getPost( + @Parameter(description = "게시글 ID", example = "1") + @Positive(message = "게시글 ID는 양의 정수만 가능합니다.") final Long postId + ); + + @Operation(summary = "비회원 게시글 검색", description = "비회원이 게시글을 검색한다.") + @ApiResponses({ + @ApiResponse( + responseCode = "200", + description = "비회원 게시글 검색 성공" + ), + @ApiResponse( + responseCode = "400", + description = "0 이상의 정수가 아닌 페이지", + content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) + ) + }) + ResponseEntity> searchPosts( + @Parameter(description = "검색 키워드", example = "votogether") final String keyword, + @Parameter(description = "현재 페이지 위치", example = "0") + @PositiveOrZero(message = "페이지는 0이상 정수만 가능합니다.") final int page, + @Parameter(description = "게시글 마감 여부", example = "ALL") final PostClosingType postClosingType, + @Parameter(description = "게시글 정렬 기준", example = "HOT") final PostSortType postSortType + ); + +} diff --git a/backend/src/main/java/com/votogether/domain/post/controller/PostQueryController.java b/backend/src/main/java/com/votogether/domain/post/controller/PostQueryController.java new file mode 100644 index 000000000..6f6bfe1a2 --- /dev/null +++ b/backend/src/main/java/com/votogether/domain/post/controller/PostQueryController.java @@ -0,0 +1,109 @@ +package com.votogether.domain.post.controller; + +import com.votogether.domain.member.entity.Member; +import com.votogether.domain.post.dto.response.post.PostResponse; +import com.votogether.domain.post.dto.response.vote.VoteOptionStatisticsResponse; +import com.votogether.domain.post.entity.vo.PostClosingType; +import com.votogether.domain.post.entity.vo.PostSortType; +import com.votogether.domain.post.service.PostQueryService; +import com.votogether.global.jwt.Auth; +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.PositiveOrZero; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@Validated +@RequiredArgsConstructor +@RequestMapping("/posts") +@RestController +public class PostQueryController implements PostQueryControllerDocs { + + private final PostQueryService postQueryService; + + @GetMapping + public ResponseEntity> getPosts( + @RequestParam @PositiveOrZero(message = "페이지는 0이상 정수만 가능합니다.") final int page, + @RequestParam final PostClosingType postClosingType, + @RequestParam final PostSortType postSortType, + @RequestParam(name = "category", required = false) final Long categoryId, + @Auth final Member loginMember + ) { + final List response = + postQueryService.getPosts(page, postClosingType, postSortType, categoryId, loginMember); + return ResponseEntity.ok(response); + } + + @GetMapping("{postId}") + public ResponseEntity getPost( + @PathVariable @Positive(message = "게시글 ID는 양의 정수만 가능합니다.") final Long postId, + @Auth final Member loginMember + ) { + final PostResponse response = postQueryService.getPost(postId, loginMember); + return ResponseEntity.ok(response); + } + + @GetMapping("/search") + public ResponseEntity> searchPosts( + @RequestParam final String keyword, + @RequestParam @PositiveOrZero(message = "페이지는 0이상 정수만 가능합니다.") final int page, + @RequestParam final PostClosingType postClosingType, + @RequestParam final PostSortType postSortType, + @Auth final Member loginMember + ) { + final List response = + postQueryService.searchPosts(keyword, page, postClosingType, postSortType, loginMember); + return ResponseEntity.ok(response); + } + + @GetMapping("/me") + public ResponseEntity> getPostsWrittenByMe( + @RequestParam @PositiveOrZero(message = "페이지는 0이상 정수만 가능합니다.") final int page, + @RequestParam final PostClosingType postClosingType, + @RequestParam final PostSortType postSortType, + @Auth final Member loginMember + ) { + final List response = + postQueryService.getPostsWrittenByMe(page, postClosingType, postSortType, loginMember); + return ResponseEntity.ok(response); + } + + @GetMapping("/votes/me") + public ResponseEntity> getPostsVotedByMe( + @RequestParam @PositiveOrZero(message = "페이지는 0이상 정수만 가능합니다.") final int page, + @RequestParam final PostClosingType postClosingType, + @RequestParam final PostSortType postSortType, + @Auth final Member loginMember + ) { + final List response = + postQueryService.getPostsVotedByMe(page, postClosingType, postSortType, loginMember); + return ResponseEntity.ok(response); + } + + @GetMapping("/{postId}/options") + public ResponseEntity getVoteStatistics( + @PathVariable @Positive(message = "게시글 ID는 양의 정수만 가능합니다.") final Long postId, + @Auth final Member loginMember + ) { + final VoteOptionStatisticsResponse response = postQueryService.getVoteStatistics(postId, loginMember); + return ResponseEntity.ok(response); + } + + @GetMapping("/{postId}/options/{optionId}") + public ResponseEntity getVoteOptionStatistics( + @PathVariable @Positive(message = "게시글 ID는 양의 정수만 가능합니다.") final Long postId, + @PathVariable @Positive(message = "게시글 옵션 ID는 양의 정수만 가능합니다.") final Long optionId, + @Auth final Member loginMember + ) { + final VoteOptionStatisticsResponse response = + postQueryService.getVoteOptionStatistics(postId, optionId, loginMember); + return ResponseEntity.ok(response); + } + +} diff --git a/backend/src/main/java/com/votogether/domain/post/controller/PostQueryControllerDocs.java b/backend/src/main/java/com/votogether/domain/post/controller/PostQueryControllerDocs.java new file mode 100644 index 000000000..c584d4a8b --- /dev/null +++ b/backend/src/main/java/com/votogether/domain/post/controller/PostQueryControllerDocs.java @@ -0,0 +1,201 @@ +package com.votogether.domain.post.controller; + +import com.votogether.domain.member.entity.Member; +import com.votogether.domain.post.dto.response.post.PostResponse; +import com.votogether.domain.post.dto.response.vote.VoteOptionStatisticsResponse; +import com.votogether.domain.post.entity.vo.PostClosingType; +import com.votogether.domain.post.entity.vo.PostSortType; +import com.votogether.global.exception.ExceptionResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.PositiveOrZero; +import java.util.List; +import org.springframework.http.ResponseEntity; + +@Tag(name = "게시글 조회", description = "게시글 조회 API") +public interface PostQueryControllerDocs { + + @Operation(summary = "게시글 목록 조회", description = "게시글 목록을 조회한다.") + @ApiResponses({ + @ApiResponse( + responseCode = "200", + description = "게시글 목록 조회 성공" + ), + @ApiResponse( + responseCode = "400", + description = "0이상의 정수가 아닌 페이지", + content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) + ) + }) + ResponseEntity> getPosts( + @Parameter(description = "현재 페이지 위치", example = "0") + @PositiveOrZero(message = "페이지는 0이상 정수만 가능합니다.") final int page, + @Parameter(description = "게시글 마감 여부", example = "ALL") final PostClosingType postClosingType, + @Parameter(description = "게시글 정렬 기준", example = "HOT") final PostSortType postSortType, + @Parameter(description = "카테고리 ID", example = "1") final Long categoryId, + final Member loginMember + ); + + @Operation(summary = "게시글 상세 조회", description = "게시글을 상세 조회한다.") + @ApiResponses({ + @ApiResponse( + responseCode = "200", + description = "게시글 상세 조회 성공" + ), + @ApiResponse( + responseCode = "400", + description = """ + 1.양의 정수가 아닌 게시글 ID + + 2.게시글이 블라인드 처리된 경우 + """, + content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) + ), + @ApiResponse( + responseCode = "404", + description = "존재하지 않는 게시글", + content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) + ) + }) + ResponseEntity getPost( + @Parameter(description = "게시글 ID", example = "1") + @Positive(message = "게시글 ID는 양의 정수만 가능합니다.") final Long postId, + final Member loginMember + ); + + @Operation(summary = "게시글 검색", description = "게시글을 검색한다.") + @ApiResponses({ + @ApiResponse( + responseCode = "200", + description = "게시글 검색 성공" + ), + @ApiResponse( + responseCode = "400", + description = "0이상의 정수가 아닌 페이지", + content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) + ) + }) + ResponseEntity> searchPosts( + @Parameter(description = "검색 키워드", example = "hello") final String keyword, + @Parameter(description = "현재 페이지 위치", example = "0") + @PositiveOrZero(message = "페이지는 0이상 정수만 가능합니다.") final int page, + @Parameter(description = "게시글 마감 여부", example = "ALL") final PostClosingType postClosingType, + @Parameter(description = "게시글 정렬 기준", example = "HOT") final PostSortType postSortType, + final Member loginMember + ); + + @Operation(summary = "내가 작성한 게시글 조회", description = "내가 작성한 게시글을 조회한다.") + @ApiResponses({ + @ApiResponse( + responseCode = "200", + description = "내가 작성한 게시글 조회 성공" + ), + @ApiResponse( + responseCode = "400", + description = "0이상의 정수가 아닌 페이지", + content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) + ) + }) + ResponseEntity> getPostsWrittenByMe( + @Parameter(description = "현재 페이지 위치", example = "0") + @PositiveOrZero(message = "페이지는 0이상 정수만 가능합니다.") final int page, + @Parameter(description = "게시글 마감 여부", example = "ALL") final PostClosingType postClosingType, + @Parameter(description = "게시글 정렬 기준", example = "HOT") final PostSortType postSortType, + final Member loginMember + ); + + @Operation(summary = "내가 투표한 게시글 조회", description = "내가 투표한 게시글을 조회한다.") + @ApiResponses({ + @ApiResponse( + responseCode = "200", + description = "내가 투표한 게시글 조회 성공" + ), + @ApiResponse( + responseCode = "400", + description = "0이상의 정수가 아닌 페이지", + content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) + ) + }) + ResponseEntity> getPostsVotedByMe( + @Parameter(description = "현재 페이지 위치", example = "0") + @PositiveOrZero(message = "페이지는 0이상 정수만 가능합니다.") final int page, + @Parameter(description = "게시글 마감 여부", example = "ALL") final PostClosingType postClosingType, + @Parameter(description = "게시글 정렬 기준", example = "HOT") final PostSortType postSortType, + final Member loginMember + ); + + @Operation(summary = "게시글 투표 통계 조회", description = "게시글 투표 통계를 조회한다.") + @ApiResponses({ + @ApiResponse( + responseCode = "200", + description = "게시글 투표 통계 조회 성공" + ), + @ApiResponse( + responseCode = "400", + description = """ + 1.양의 정수가 아닌 게시글 ID + + 2.게시글이 블라인드 처리된 경우 + + 3.게시글 작성자가 아닌 경우 + """, + content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) + ), + @ApiResponse( + responseCode = "404", + description = "존재하지 않는 게시글", + content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) + ) + }) + ResponseEntity getVoteStatistics( + @Parameter(description = "게시글 ID", example = "1") + @Positive(message = "게시글 ID는 양의 정수만 가능합니다.") final Long postId, + final Member loginMember + ); + + @Operation(summary = "게시글 투표 옵션 통계 조회", description = "게시글 투표 옵션 통계를 조회한다.") + @ApiResponses({ + @ApiResponse( + responseCode = "200", + description = "게시글 투표 옵션 통계 조회 성공" + ), + @ApiResponse( + responseCode = "400", + description = """ + 1.양의 정수가 아닌 게시글 ID + + 2.양의 정수가 아닌 게시글 투표 옵션 ID + + 3.게시글이 블라인드 처리된 경우 + + 4.게시글 작성자가 아닌 경우 + + 5.옵션이 해당 게시글에 속하지 않는 경우 + """, + content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) + ), + @ApiResponse( + responseCode = "404", + description = """ + 1.존재하지 않는 게시글 + + 2.존재하지 않는 게시글 투표 옵션 + """, + content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) + ) + }) + ResponseEntity getVoteOptionStatistics( + @Parameter(description = "게시글 ID", example = "1") + @Positive(message = "게시글 ID는 양의 정수만 가능합니다.") final Long postId, + @Parameter(description = "게시글 옵션 ID", example = "1") + @Positive(message = "게시글 옵션 ID는 양의 정수만 가능합니다.") final Long optionId, + final Member loginMember + ); + +} diff --git a/backend/src/main/java/com/votogether/domain/post/dto/request/comment/CommentRegisterRequest.java b/backend/src/main/java/com/votogether/domain/post/dto/request/comment/CommentCreateRequest.java similarity index 90% rename from backend/src/main/java/com/votogether/domain/post/dto/request/comment/CommentRegisterRequest.java rename to backend/src/main/java/com/votogether/domain/post/dto/request/comment/CommentCreateRequest.java index ef6f641f4..8a2a2275c 100644 --- a/backend/src/main/java/com/votogether/domain/post/dto/request/comment/CommentRegisterRequest.java +++ b/backend/src/main/java/com/votogether/domain/post/dto/request/comment/CommentCreateRequest.java @@ -4,7 +4,7 @@ import jakarta.validation.constraints.NotBlank; @Schema(description = "댓글 작성 요청") -public record CommentRegisterRequest( +public record CommentCreateRequest( @Schema(description = "댓글 내용", example = "hello") @NotBlank(message = "댓글 내용은 존재해야 합니다.") String content diff --git a/backend/src/main/java/com/votogether/domain/post/dto/request/post/PostCreateRequest.java b/backend/src/main/java/com/votogether/domain/post/dto/request/post/PostCreateRequest.java index 4bfc21708..8c7e91305 100644 --- a/backend/src/main/java/com/votogether/domain/post/dto/request/post/PostCreateRequest.java +++ b/backend/src/main/java/com/votogether/domain/post/dto/request/post/PostCreateRequest.java @@ -1,6 +1,5 @@ package com.votogether.domain.post.dto.request.post; -import com.fasterxml.jackson.annotation.JsonFormat; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; @@ -8,37 +7,44 @@ import jakarta.validation.constraints.Size; import java.time.LocalDateTime; import java.util.List; -import lombok.Builder; -import org.hibernate.validator.constraints.Length; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.web.multipart.MultipartFile; +@Getter +@Setter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor @Schema(description = "게시글 작성 요청") -@Builder -public record PostCreateRequest( - @Schema(description = "카테고리의 여러 아이디", example = "[1, 3]") - @Size(min = 1, message = "게시글에 해당하는 카테고리는 최소 1개 이상이어야 합니다.") - List categoryIds, - - @Schema(description = "게시글 제목", example = "title") - @NotBlank(message = "제목을 입력해주세요.") - @Length(max = 100, message = "제목은 최대 100자까지 입력 가능합니다.") - String title, - - @Schema(description = "게시글 내용", example = "content") - @NotBlank(message = "내용을 입력해주세요.") - @Length(max = 1000, message = "내용은 최대 1000자까지 입력 가능합니다.") - String content, - - @Schema(description = "이미지 URL", example = "http://asdasdsadsad.com") - String imageUrl, - - @Schema(description = "게시글의 여러 선택지") - @Valid - @NotNull(message = "선택지는 최소 2개 이상 등록해야 합니다.") - @Size(min = 2, max = 5, message = "선택지는 최소 2개, 최대 5개까지 등록 가능합니다.") - List postOptions, - - @Schema(description = "마감 기한", example = "2023-08-01 15:30") - @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm") - LocalDateTime deadline -) { +public class PostCreateRequest { + + @Schema(description = "카테고리 ID 목록", example = "[1, 2]") + @NotNull(message = "게시글 카테고리 ID가 등록되지 않았습니다.") + @Size(min = 1, message = "게시글 카테고리는 최소 1개 이상 등록이 필요합니다.") + private List categoryIds; + + @Schema(description = "게시글 제목", example = "보투게더에 대하여") + @NotBlank(message = "게시글 제목이 존재하지 않거나 공백만 존재합니다.") + private String title; + + @Schema(description = "게시글 내용", example = "보투게더 정말 재밌어요!") + @NotBlank(message = "게시글 내용이 존재하지 않거나 공백만 존재합니다.") + private String content; + + @Schema(description = "게시글 이미지 파일", example = "votogether.png") + private MultipartFile imageFile; + + @Schema(description = "게시글 옵션 작성 목록") + @Valid + private List postOptions; + + @Schema(description = "게시글 마감시간", example = "2023-08-01 12:25") + @NotNull(message = "게시글 마감시간을 등록하지 않았습니다.") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm") + private LocalDateTime deadline; + } diff --git a/backend/src/main/java/com/votogether/domain/post/dto/request/post/PostOptionCreateRequest.java b/backend/src/main/java/com/votogether/domain/post/dto/request/post/PostOptionCreateRequest.java index f5c703126..99d8c3f4b 100644 --- a/backend/src/main/java/com/votogether/domain/post/dto/request/post/PostOptionCreateRequest.java +++ b/backend/src/main/java/com/votogether/domain/post/dto/request/post/PostOptionCreateRequest.java @@ -2,18 +2,25 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; -import lombok.Builder; -import org.hibernate.validator.constraints.Length; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.springframework.web.multipart.MultipartFile; -@Schema(description = "게시글 선택지 요청") -@Builder -public record PostOptionCreateRequest( - @Schema(description = "선택지 내용", example = "content") - @NotBlank(message = "해당 선택지의 내용을 입력해주세요.") - @Length(max = 50, message = "선택지의 내용은 최대 50자까지 입력 가능합니다.") - String content, +@Getter +@Setter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Schema(description = "게시글 옵션 작성 요청") +public class PostOptionCreateRequest { + + @Schema(description = "게시글 옵션 내용", example = "1번 옵션") + @NotBlank(message = "게시글 옵션 내용이 존재하지 않거나 공백만 존재합니다.") + private String content; + + @Schema(description = "게시글 옵션 이미지 파일", example = "votogether.png") + private MultipartFile optionImage; - @Schema(description = "이미지 URL", example = "http://asdasdsadsad.com") - String imageUrl -) { } diff --git a/backend/src/main/java/com/votogether/domain/post/dto/request/post/PostOptionUpdateRequest.java b/backend/src/main/java/com/votogether/domain/post/dto/request/post/PostOptionUpdateRequest.java index a321d2993..3b9a7cb14 100644 --- a/backend/src/main/java/com/votogether/domain/post/dto/request/post/PostOptionUpdateRequest.java +++ b/backend/src/main/java/com/votogether/domain/post/dto/request/post/PostOptionUpdateRequest.java @@ -2,18 +2,31 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; -import lombok.Builder; -import org.hibernate.validator.constraints.Length; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.springframework.web.multipart.MultipartFile; -@Schema(description = "게시글 선택지 수정 요청") -@Builder -public record PostOptionUpdateRequest( - @Schema(description = "선택지 내용", example = "content") - @NotBlank(message = "해당 선택지의 내용을 입력해주세요.") - @Length(max = 50, message = "선택지의 내용은 최대 50자까지 입력 가능합니다.") - String content, +@Getter +@Setter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Schema(description = "게시글 옵션 작성 요청") +public class PostOptionUpdateRequest { + + @Schema(description = "게시글 옵션 ID", example = "1") + private Long id; + + @Schema(description = "게시글 옵션 내용", example = "1번 옵션") + @NotBlank(message = "게시글 옵션 내용이 존재하지 않거나 공백만 존재합니다.") + private String content; + + @Schema(description = "게시글 옵션 이미지 URL", example = "https://test.com/image.png") + private String imageUrl; + + @Schema(description = "게시글 옵션 이미지 파일", example = "votogether.png") + private MultipartFile imageFile; - @Schema(description = "이미지 URL", example = "http://asdasdsadsad.com") - String imageUrl -) { } diff --git a/backend/src/main/java/com/votogether/domain/post/dto/request/post/PostUpdateRequest.java b/backend/src/main/java/com/votogether/domain/post/dto/request/post/PostUpdateRequest.java index 1dfed74c8..d9c738577 100644 --- a/backend/src/main/java/com/votogether/domain/post/dto/request/post/PostUpdateRequest.java +++ b/backend/src/main/java/com/votogether/domain/post/dto/request/post/PostUpdateRequest.java @@ -1,6 +1,5 @@ package com.votogether.domain.post.dto.request.post; -import com.fasterxml.jackson.annotation.JsonFormat; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; @@ -8,37 +7,47 @@ import jakarta.validation.constraints.Size; import java.time.LocalDateTime; import java.util.List; -import lombok.Builder; -import org.hibernate.validator.constraints.Length; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.web.multipart.MultipartFile; +@Getter +@Setter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor @Schema(description = "게시글 수정 요청") -@Builder -public record PostUpdateRequest( - @Schema(description = "카테고리의 여러 아이디", example = "[0, 2]") - @Size(min = 1, message = "게시글에 해당하는 카테고리는 최소 1개 이상이어야 합니다.") - List categoryIds, - - @Schema(description = "게시글 제목", example = "title") - @NotBlank(message = "제목을 입력해주세요.") - @Length(max = 100, message = "제목은 최대 100자까지 입력 가능합니다.") - String title, - - @Schema(description = "게시글 내용", example = "content") - @NotBlank(message = "내용을 입력해주세요.") - @Length(max = 1000, message = "내용은 최대 1000자까지 입력 가능합니다.") - String content, - - @Schema(description = "이미지 URL", example = "http://asdasdsadsad.com") - String imageUrl, - - @Schema(description = "게시글의 여러 선택지") - @Valid - @NotNull(message = "선택지는 최소 2개 이상 등록해야 합니다.") - @Size(min = 2, max = 5, message = "선택지는 최소 2개, 최대 5개까지 등록 가능합니다.") - List postOptions, - - @Schema(description = "마감 기한", example = "2023-08-01 15:30") - @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm") - LocalDateTime deadline -) { +public class PostUpdateRequest { + + @Schema(description = "카테고리 ID 목록", example = "[1, 2]") + @NotNull(message = "게시글 카테고리 ID가 등록되지 않았습니다.") + @Size(min = 1, message = "게시글 카테고리는 최소 1개 이상 등록이 필요합니다.") + private List categoryIds; + + @Schema(description = "게시글 제목", example = "보투게더에 대하여") + @NotBlank(message = "게시글 제목이 존재하지 않거나 공백만 존재합니다.") + private String title; + + @Schema(description = "게시글 내용", example = "보투게더 정말 재밌어요!") + @NotBlank(message = "게시글 내용이 존재하지 않거나 공백만 존재합니다.") + private String content; + + @Schema(description = "게시글 이미지 URL", example = "https://test.com/image.png") + private String imageUrl; + + @Schema(description = "게시글 이미지 파일", example = "votogether.png") + private MultipartFile imageFile; + + @Schema(description = "게시글 옵션 작성 목록") + @Valid + private List postOptions; + + @Schema(description = "게시글 마감시간", example = "2023-08-01 12:25") + @NotNull(message = "게시글 마감시간을 등록하지 않았습니다.") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm") + private LocalDateTime deadline; + } diff --git a/backend/src/main/java/com/votogether/domain/post/dto/response/comment/CommentResponse.java b/backend/src/main/java/com/votogether/domain/post/dto/response/comment/CommentResponse.java index a1e14588f..917371c84 100644 --- a/backend/src/main/java/com/votogether/domain/post/dto/response/comment/CommentResponse.java +++ b/backend/src/main/java/com/votogether/domain/post/dto/response/comment/CommentResponse.java @@ -1,7 +1,6 @@ package com.votogether.domain.post.dto.response.comment; import com.fasterxml.jackson.annotation.JsonFormat; -import com.votogether.domain.member.entity.Member; import com.votogether.domain.post.entity.comment.Comment; import io.swagger.v3.oas.annotations.media.Schema; import java.time.LocalDateTime; @@ -11,17 +10,17 @@ public record CommentResponse( @Schema(description = "댓글 ID", example = "1") Long id, - @Schema(description = "댓글 작성자 회원") - CommentMember member, + @Schema(description = "댓글 작성자") + CommentWriterResponse member, @Schema(description = "댓글 내용", example = "재밌어요!") String content, - @Schema(description = "댓글 작성시각", example = "2023-08-01 10:56") + @Schema(description = "댓글 작성시간", example = "2023-08-01 10:56") @JsonFormat(pattern = "yyyy-MM-dd HH:mm") LocalDateTime createdAt, - @Schema(description = "댓글 수정시각", example = "2023-08-01 13:56") + @Schema(description = "댓글 수정시간", example = "2023-08-01 13:56") @JsonFormat(pattern = "yyyy-MM-dd HH:mm") LocalDateTime updatedAt ) { @@ -29,26 +28,10 @@ public record CommentResponse( public static CommentResponse from(final Comment comment) { return new CommentResponse( comment.getId(), - CommentMember.from(comment.getMember()), + CommentWriterResponse.from(comment.getWriter()), comment.getContent(), comment.getCreatedAt(), comment.getUpdatedAt() ); } - - @Schema(description = "댓글 작성자 회원") - record CommentMember( - @Schema(description = "댓글 작성자 회원 ID", example = "1") - Long id, - - @Schema(description = "댓글 작성자 회원 닉네임", example = "votogether") - String nickname - ) { - - public static CommentMember from(final Member member) { - return new CommentMember(member.getId(), member.getNickname()); - } - - } - } diff --git a/backend/src/main/java/com/votogether/domain/post/dto/response/comment/CommentWriterResponse.java b/backend/src/main/java/com/votogether/domain/post/dto/response/comment/CommentWriterResponse.java new file mode 100644 index 000000000..1b183f16e --- /dev/null +++ b/backend/src/main/java/com/votogether/domain/post/dto/response/comment/CommentWriterResponse.java @@ -0,0 +1,19 @@ +package com.votogether.domain.post.dto.response.comment; + +import com.votogether.domain.member.entity.Member; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "댓글 작성자 응답") +public record CommentWriterResponse( + @Schema(description = "댓글 작성자 ID", example = "1") + Long id, + + @Schema(description = "댓글 작성자 닉네임", example = "votogether") + String nickname +) { + + public static CommentWriterResponse from(final Member writer) { + return new CommentWriterResponse(writer.getId(), writer.getNickname()); + } + +} diff --git a/backend/src/main/java/com/votogether/domain/post/dto/response/post/CategoryResponse.java b/backend/src/main/java/com/votogether/domain/post/dto/response/post/CategoryResponse.java index 90635c851..993aa59fb 100644 --- a/backend/src/main/java/com/votogether/domain/post/dto/response/post/CategoryResponse.java +++ b/backend/src/main/java/com/votogether/domain/post/dto/response/post/CategoryResponse.java @@ -12,7 +12,7 @@ public record CategoryResponse( String name ) { - public static CategoryResponse of(Category category) { + public static CategoryResponse from(Category category) { return new CategoryResponse(category.getId(), category.getName()); } diff --git a/backend/src/main/java/com/votogether/domain/post/dto/response/post/PostDetailResponse.java b/backend/src/main/java/com/votogether/domain/post/dto/response/post/PostDetailResponse.java deleted file mode 100644 index 5ace2220e..000000000 --- a/backend/src/main/java/com/votogether/domain/post/dto/response/post/PostDetailResponse.java +++ /dev/null @@ -1,97 +0,0 @@ -package com.votogether.domain.post.dto.response.post; - -import com.fasterxml.jackson.annotation.JsonFormat; -import com.votogether.domain.member.entity.Member; -import com.votogether.domain.post.dto.response.vote.VoteDetailResponse; -import com.votogether.domain.post.entity.Post; -import com.votogether.domain.post.entity.PostBody; -import com.votogether.domain.post.entity.PostCategories; -import com.votogether.domain.post.entity.PostCategory; -import com.votogether.domain.post.entity.PostContentImage; -import io.swagger.v3.oas.annotations.media.Schema; -import java.time.LocalDateTime; -import java.util.List; - -@Schema(description = "게시글 상세 정보 응답") -public record PostDetailResponse( - @Schema(description = "게시글 ID", example = "1") - Long postId, - - @Schema(description = "작성자") - WriterResponse writer, - - @Schema(description = "게시글 제목", example = "이거 한번 투표해주세요") - String title, - - @Schema(description = "게시글 내용", example = "어떤게 더 맛있나요?") - String content, - - @Schema(description = "이미지 URL", example = "http://asdasdasd.com") - String imageUrl, - - @Schema(description = "카테고리 목록", example = "[1,2]") - List categories, - - @Schema(description = "게시글 생성시각", example = "2023-08-01 13:56") - @JsonFormat(pattern = "yyyy-MM-dd HH:mm") - LocalDateTime createdAt, - - @Schema(description = "게시글 마감기한", example = "2023-08-01 13:56") - @JsonFormat(pattern = "yyyy-MM-dd HH:mm") - LocalDateTime deadline, - - @Schema(description = "투표 통계 정보") - VoteDetailResponse voteInfo -) { - - public static PostDetailResponse of(final Post post, final Member loginMember) { - final Member writer = post.getWriter(); - final PostBody postBody = post.getPostBody(); - final List contentImages = postBody.getPostContentImages().getContentImages(); - final StringBuilder contentImageUrl = new StringBuilder(); - - if (!contentImages.isEmpty()) { - contentImageUrl.append(contentImages.get(0).getImageUrl()); - } - - final PostCategories postCategories = post.getPostCategories(); - return new PostDetailResponse( - post.getId(), - WriterResponse.of(writer.getId(), writer.getNickname()), - postBody.getTitle(), - postBody.getContent(), - contentImageUrl.toString(), - getCategories(postCategories.getPostCategories()), - post.getCreatedAt(), - post.getDeadline(), - VoteDetailResponse.of( - post.getSelectedOptionId(loginMember), - post.getFinalTotalVoteCount(loginMember), - getOptions(post, loginMember) - ) - ); - } - - private static List getCategories(final List postCategories) { - return postCategories.stream() - .map(PostCategory::getCategory) - .map(CategoryResponse::of) - .toList(); - } - - private static List getOptions( - final Post post, - final Member loginMember - ) { - return post.getPostOptions().getPostOptions().stream() - .map(postOption -> - PostOptionDetailResponse.of( - postOption, - post.isVisibleVoteResult(loginMember), - post.getFinalTotalVoteCount(loginMember) - ) - ) - .toList(); - } - -} diff --git a/backend/src/main/java/com/votogether/domain/post/dto/response/post/PostOptionDetailResponse.java b/backend/src/main/java/com/votogether/domain/post/dto/response/post/PostOptionDetailResponse.java deleted file mode 100644 index 8a4725bb8..000000000 --- a/backend/src/main/java/com/votogether/domain/post/dto/response/post/PostOptionDetailResponse.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.votogether.domain.post.dto.response.post; - -import com.votogether.domain.post.entity.PostOption; -import io.swagger.v3.oas.annotations.media.Schema; - -@Schema(description = "게시글 선택지 정보 응답") -public record PostOptionDetailResponse( - @Schema(description = "게시글 선택지 ID", example = "1") - Long optionId, - - @Schema(description = "게시글 선택지 내용", example = "짜장면") - String content, - - @Schema(description = "이미지 URL", example = "http://sdasdas.com") - String imageUrl, - - @Schema(description = "투표 개수", example = "4") - Integer voteCount, - - @Schema(description = "투표한 비율", example = "50.0") - Double votePercent -) { - - public static PostOptionDetailResponse of( - final PostOption postOption, - final Boolean isVisibleVoteResult, - final Long totalVoteCount - ) { - return new PostOptionDetailResponse( - postOption.getId(), - postOption.getContent(), - postOption.getImageUrl(), - postOption.getVoteCount(isVisibleVoteResult), - postOption.getVotePercent(totalVoteCount) - ); - } - -} diff --git a/backend/src/main/java/com/votogether/domain/post/dto/response/post/PostOptionResponse.java b/backend/src/main/java/com/votogether/domain/post/dto/response/post/PostOptionResponse.java deleted file mode 100644 index 4212cf49a..000000000 --- a/backend/src/main/java/com/votogether/domain/post/dto/response/post/PostOptionResponse.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.votogether.domain.post.dto.response.post; - -import com.votogether.domain.post.entity.Post; -import com.votogether.domain.post.entity.PostOption; -import io.swagger.v3.oas.annotations.media.Schema; - -@Schema(description = "게시글 선택지 정보 응답") -public record PostOptionResponse( - @Schema(description = "게시글 선택지 ID", example = "1") - Long optionId, - - @Schema(description = "게시글 선택지 내용", example = "짜장면") - String content, - - @Schema(description = "이미지 URL", example = "http://sdasdas.com") - String imageUrl, - - @Schema(description = "투표 개수", example = "4") - Integer voteCount, - - @Schema(description = "투표한 비율", example = "50.0") - Double votePercent -) { - - private static final int HIDDEN_COUNT = -1; - - public static PostOptionResponse of(final Post post, final PostOption postOption) { - return new PostOptionResponse( - postOption.getId(), - postOption.getContent(), - convertImageUrl(postOption.getImageUrl()), - post.isClosed() ? postOption.getVoteCount() : HIDDEN_COUNT, - postOption.getVotePercent(post.getTotalVoteCount()) - ); - } - - private static String convertImageUrl(final String imageUrl) { - return imageUrl == null ? "" : imageUrl; - } - - public static PostOptionResponse of( - final PostOption postOption, - final boolean isVisibleVoteResult, - final Long totalVoteCount - ) { - return new PostOptionResponse( - postOption.getId(), - postOption.getContent(), - convertImageUrl(postOption.getImageUrl()), - postOption.getVoteCount(isVisibleVoteResult), - postOption.getVotePercent(totalVoteCount) - ); - } - -} diff --git a/backend/src/main/java/com/votogether/domain/post/dto/response/post/PostOptionVoteResultResponse.java b/backend/src/main/java/com/votogether/domain/post/dto/response/post/PostOptionVoteResultResponse.java new file mode 100644 index 000000000..bf6e832c2 --- /dev/null +++ b/backend/src/main/java/com/votogether/domain/post/dto/response/post/PostOptionVoteResultResponse.java @@ -0,0 +1,94 @@ +package com.votogether.domain.post.dto.response.post; + +import com.votogether.domain.member.entity.Member; +import com.votogether.domain.post.entity.Post; +import com.votogether.domain.post.entity.PostOption; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "투표 옵션 투표 결과 응답") +public record PostOptionVoteResultResponse( + @Schema(description = "투표 옵션 ID", example = "1") + Long optionId, + + @Schema(description = "투표 옵션 내용", example = "짜장면") + String content, + + @Schema(description = "투표 옵션 이미지 URL", example = "http://sdasdas.com") + String imageUrl, + + @Schema(description = "투표 옵션 총 투표 수", example = "4") + long voteCount, + + @Schema(description = "투표 옵션 투표 비율", example = "50.0") + double votePercent +) { + + private static final int HIDDEN_COUNT = -1; + private static final String EMPTY_IMAGE_URL = ""; + + public static PostOptionVoteResultResponse ofUser( + final Member user, + final Post post, + final PostOption postOption, + final boolean isVoted, + final long totalVoteCount + ) { + final long voteCount = countVotesForUser(user, post, postOption, isVoted); + return new PostOptionVoteResultResponse( + postOption.getId(), + postOption.getContent(), + handleEmptyImageUrl(postOption.getImageUrl()), + voteCount, + calculateVotePercent(totalVoteCount, voteCount) + ); + } + + private static long countVotesForUser( + final Member user, + final Post post, + final PostOption postOption, + final boolean isVoted + ) { + if (post.isClosed() || post.isWriter(user) || isVoted) { + return postOption.getVoteCount(); + } + return HIDDEN_COUNT; + } + + private static String handleEmptyImageUrl(final String imageUrl) { + if (imageUrl == null) { + return EMPTY_IMAGE_URL; + } + return imageUrl; + } + + private static double calculateVotePercent(final long totalCount, final long voteCount) { + if (voteCount == HIDDEN_COUNT || totalCount == 0) { + return 0.0; + } + return ((double) voteCount / totalCount); + } + + public static PostOptionVoteResultResponse ofGuest( + final Post post, + final PostOption postOption, + final long totalVoteCount + ) { + final long voteCount = countVotesForGuest(post, postOption); + return new PostOptionVoteResultResponse( + postOption.getId(), + postOption.getContent(), + handleEmptyImageUrl(postOption.getImageUrl()), + voteCount, + calculateVotePercent(totalVoteCount, voteCount) + ); + } + + private static long countVotesForGuest(final Post post, final PostOption postOption) { + if (post.isClosed()) { + return postOption.getVoteCount(); + } + return HIDDEN_COUNT; + } + +} diff --git a/backend/src/main/java/com/votogether/domain/post/dto/response/post/PostResponse.java b/backend/src/main/java/com/votogether/domain/post/dto/response/post/PostResponse.java index d17d677d9..9a81fb8ea 100644 --- a/backend/src/main/java/com/votogether/domain/post/dto/response/post/PostResponse.java +++ b/backend/src/main/java/com/votogether/domain/post/dto/response/post/PostResponse.java @@ -2,22 +2,24 @@ import com.fasterxml.jackson.annotation.JsonFormat; import com.votogether.domain.member.entity.Member; -import com.votogether.domain.post.dto.response.vote.VoteResponse; import com.votogether.domain.post.entity.Post; -import com.votogether.domain.post.entity.PostBody; import com.votogether.domain.post.entity.PostCategory; import com.votogether.domain.post.entity.PostContentImage; +import com.votogether.domain.post.entity.PostOption; +import com.votogether.domain.vote.entity.Vote; import io.swagger.v3.oas.annotations.media.Schema; import java.time.LocalDateTime; +import java.util.Comparator; import java.util.List; +import java.util.Optional; -@Schema(description = "게시글에 관련한 데이터들입니다.") +@Schema(description = "게시글 응답") public record PostResponse( @Schema(description = "게시글 ID", example = "1") Long postId, - @Schema(description = "작성자") - WriterResponse writer, + @Schema(description = "게시글 작성자") + PostWriterResponse writer, @Schema(description = "게시글 제목", example = "이거 한번 투표해주세요") String title, @@ -25,100 +27,109 @@ public record PostResponse( @Schema(description = "게시글 내용", example = "어떤게 더 맛있나요?") String content, - @Schema(description = "이미지 URL", example = "http://asdasdasd.com") + @Schema(description = "게시글 이미지 URL", example = "http://asdasdasd.com") String imageUrl, - @Schema(description = "카테고리 목록", example = "[1,2]") + @Schema(description = "카테고리 목록") List categories, - @Schema(description = "게시글 생성시각", example = "2023-08-01 13:56") + @Schema(description = "게시글 생성시간", example = "2023-08-01 13:56") @JsonFormat(pattern = "yyyy-MM-dd HH:mm") LocalDateTime createdAt, - @Schema(description = "게시글 마감기한", example = "2023-08-01 13:56") + @Schema(description = "게시글 마감시한", example = "2023-08-01 13:56") @JsonFormat(pattern = "yyyy-MM-dd HH:mm") LocalDateTime deadline, - @Schema(description = "투표 통계 정보") - VoteResponse voteInfo -) { + @Schema(description = "게시글 이미지 수", example = "3") + int imageCount, - public static PostResponse of(final Post post, final Member loginMember) { - final Member writer = post.getWriter(); - final PostBody postBody = post.getPostBody(); - final List contentImages = postBody.getPostContentImages().getContentImages(); - final StringBuilder contentImageUrl = new StringBuilder(); + @Schema(description = "게시글 댓글 수", example = "23") + long commentCount, - if (!contentImages.isEmpty()) { - contentImageUrl.append(contentImages.get(0).getImageUrl()); - } + @Schema(description = "게시글 투표 결과") + PostVoteResultResponse voteInfo +) { + + private static final String EMPTY_IMAGE_URL = ""; + private static final int EMPTY_POST_CONTENT_IMAGE_COUNT = 0; + private static final int EXIST_POST_CONTENT_IMAGE_COUNT = 1; + public static PostResponse ofUser( + final Member user, + final Post post, + final List postCategories, + final PostContentImage postContentImage, + final List postOptions, + final Optional vote + ) { + postOptions.sort(Comparator.comparingInt(PostOption::getSequence)); return new PostResponse( post.getId(), - WriterResponse.of(writer.getId(), writer.getNickname()), - postBody.getTitle(), - postBody.getContent(), - convertImageUrl(contentImageUrl.toString()), - getCategories(post), + PostWriterResponse.from(post.getWriter()), + post.getTitle(), + post.getContent(), + handleEmptyImageUrl(postContentImage), + convertToResponses(postCategories), post.getCreatedAt(), post.getDeadline(), - VoteResponse.of( - post.getSelectedOptionId(loginMember), - post.getFinalTotalVoteCount(loginMember), - getOptions(post, loginMember) - ) + calculateImageCount(postContentImage, postOptions), + post.getCommentCount(), + PostVoteResultResponse.ofUser(user, post, postOptions, vote) ); } - private static String convertImageUrl(final String imageUrl) { - return imageUrl == null ? "" : imageUrl; + private static String handleEmptyImageUrl(final PostContentImage postContentImage) { + if (postContentImage == null) { + return EMPTY_IMAGE_URL; + } + return postContentImage.getImageUrl(); } - private static List getCategories(final Post post) { - return post.getPostCategories() - .getPostCategories() - .stream() + private static List convertToResponses(final List postCategories) { + return postCategories.stream() .map(PostCategory::getCategory) - .map(CategoryResponse::of) + .map(CategoryResponse::from) .toList(); } - private static List getOptions( - final Post post, - final Member loginMember + private static int calculateImageCount( + final PostContentImage postContentImage, + final List postOptions ) { - return post.getPostOptions() - .getPostOptions() - .stream() - .map(postOption -> - PostOptionResponse.of( - postOption, - post.isVisibleVoteResult(loginMember), - post.getFinalTotalVoteCount(loginMember) - ) - ) - .toList(); + int count = countEmptyPostContentImage(postContentImage); + count += (int) postOptions.stream() + .filter(postOption -> postOption.getImageUrl() != null) + .count(); + return count; } - public static PostResponse forGuest(final Post post) { - final PostBody postBody = post.getPostBody(); - final List contentImages = postBody.getPostContentImages().getContentImages(); - final StringBuilder contentImageUrl = new StringBuilder(); - - if (!contentImages.isEmpty()) { - contentImageUrl.append(contentImages.get(0).getImageUrl()); + private static int countEmptyPostContentImage(final PostContentImage postContentImage) { + if (postContentImage == null) { + return EMPTY_POST_CONTENT_IMAGE_COUNT; } + return EXIST_POST_CONTENT_IMAGE_COUNT; + } + public static PostResponse ofGuest( + final Post post, + final List postCategories, + final PostContentImage postContentImage, + final List postOptions + ) { + postOptions.sort(Comparator.comparingInt(PostOption::getSequence)); return new PostResponse( post.getId(), - WriterResponse.from(post.getWriter()), - post.getPostBody().getTitle(), - post.getPostBody().getContent(), - convertImageUrl(contentImageUrl.toString()), - getCategories(post), + PostWriterResponse.from(post.getWriter()), + post.getTitle(), + post.getContent(), + handleEmptyImageUrl(postContentImage), + convertToResponses(postCategories), post.getCreatedAt(), post.getDeadline(), - VoteResponse.forGuest(post) + calculateImageCount(postContentImage, postOptions), + post.getCommentCount(), + PostVoteResultResponse.ofGuest(post, postOptions) ); } diff --git a/backend/src/main/java/com/votogether/domain/post/dto/response/post/PostSummaryResponse.java b/backend/src/main/java/com/votogether/domain/post/dto/response/post/PostSummaryResponse.java index f90cb208d..c4130adbe 100644 --- a/backend/src/main/java/com/votogether/domain/post/dto/response/post/PostSummaryResponse.java +++ b/backend/src/main/java/com/votogether/domain/post/dto/response/post/PostSummaryResponse.java @@ -1,6 +1,7 @@ package com.votogether.domain.post.dto.response.post; import com.votogether.domain.post.entity.Post; +import com.votogether.domain.post.entity.PostOption; import io.swagger.v3.oas.annotations.media.Schema; @Schema(description = "게시글 간략 정보 응답") @@ -23,8 +24,15 @@ public static PostSummaryResponse from(final Post post) { post.getId(), post.getWriter().getNickname(), post.getPostBody().getTitle(), - post.getTotalVoteCount() + calculateTotalVoteCount(post) ); } + private static long calculateTotalVoteCount(final Post post) { + return post.getPostOptions() + .stream() + .mapToLong(PostOption::getVoteCount) + .sum(); + } + } diff --git a/backend/src/main/java/com/votogether/domain/post/dto/response/post/PostVoteResultResponse.java b/backend/src/main/java/com/votogether/domain/post/dto/response/post/PostVoteResultResponse.java new file mode 100644 index 000000000..1e18a67f3 --- /dev/null +++ b/backend/src/main/java/com/votogether/domain/post/dto/response/post/PostVoteResultResponse.java @@ -0,0 +1,103 @@ +package com.votogether.domain.post.dto.response.post; + +import com.votogether.domain.member.entity.Member; +import com.votogether.domain.post.entity.Post; +import com.votogether.domain.post.entity.PostOption; +import com.votogether.domain.vote.entity.Vote; +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; +import java.util.Optional; + +@Schema(description = "게시글 투표 결과 응답") +public record PostVoteResultResponse( + @Schema(description = "선택한 투표 옵션 ID", example = "1") + Long selectedOptionId, + + @Schema(description = "게시글 총 투표 수", example = "7") + long totalVoteCount, + + @Schema(description = "투표 옵션 투표 결과 목록") + List options +) { + + private static final int HIDDEN_COUNT = -1; + private static final Long NOT_SELECTED = 0L; + + public static PostVoteResultResponse ofUser( + final Member user, + final Post post, + final List postOptions, + final Optional vote + ) { + final long totalVoteCount = countTotalVoteCount(postOptions); + return new PostVoteResultResponse( + findSelectedOption(vote), + countVotesForUser(user, post, vote.isPresent(), totalVoteCount), + convertToUserResponses(user, post, postOptions, vote.isPresent(), totalVoteCount) + ); + } + + private static long countTotalVoteCount(final List postOptions) { + return postOptions.stream() + .mapToLong(PostOption::getVoteCount) + .sum(); + } + + private static Long findSelectedOption(final Optional vote) { + if (vote.isEmpty()) { + return NOT_SELECTED; + } + return vote.get().getPostOption().getId(); + } + + private static long countVotesForUser( + final Member user, + final Post post, + final boolean isVoted, + final long totalVoteCount + ) { + if (post.isClosed() || post.isWriter(user) || isVoted) { + return totalVoteCount; + } + return HIDDEN_COUNT; + } + + private static List convertToUserResponses( + final Member user, + final Post post, + final List postOptions, + final boolean isVoted, + final long totalVoteCount + ) { + return postOptions.stream() + .map(postOption -> PostOptionVoteResultResponse.ofUser(user, post, postOption, isVoted, totalVoteCount)) + .toList(); + } + + public static PostVoteResultResponse ofGuest(final Post post, final List postOptions) { + final long totalVoteCount = countTotalVoteCount(postOptions); + return new PostVoteResultResponse( + NOT_SELECTED, + countVotesForGuest(post, totalVoteCount), + convertToGuestResponses(post, postOptions, totalVoteCount) + ); + } + + private static long countVotesForGuest(final Post post, final long totalVoteCount) { + if (post.isClosed()) { + return totalVoteCount; + } + return HIDDEN_COUNT; + } + + private static List convertToGuestResponses( + final Post post, + final List postOptions, + final long totalVoteCount + ) { + return postOptions.stream() + .map(postOption -> PostOptionVoteResultResponse.ofGuest(post, postOption, totalVoteCount)) + .toList(); + } + +} diff --git a/backend/src/main/java/com/votogether/domain/post/dto/response/post/PostWriterResponse.java b/backend/src/main/java/com/votogether/domain/post/dto/response/post/PostWriterResponse.java new file mode 100644 index 000000000..09e4d3730 --- /dev/null +++ b/backend/src/main/java/com/votogether/domain/post/dto/response/post/PostWriterResponse.java @@ -0,0 +1,23 @@ +package com.votogether.domain.post.dto.response.post; + +import com.votogether.domain.member.entity.Member; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "게시글 작성자 응답") +public record PostWriterResponse( + @Schema(description = "게시글 작성자 ID", example = "1") + Long id, + + @Schema(description = "게시글 작성자 닉네임", example = "익명의닉네임2SDSDNKLNS") + String nickname +) { + + public static PostWriterResponse from(final Member member) { + return new PostWriterResponse(member.getId(), member.getNickname()); + } + + public static PostWriterResponse of(final Long id, final String nickname) { + return new PostWriterResponse(id, nickname); + } + +} diff --git a/backend/src/main/java/com/votogether/domain/post/dto/response/post/WriterResponse.java b/backend/src/main/java/com/votogether/domain/post/dto/response/post/WriterResponse.java deleted file mode 100644 index 985e4c872..000000000 --- a/backend/src/main/java/com/votogether/domain/post/dto/response/post/WriterResponse.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.votogether.domain.post.dto.response.post; - -import com.votogether.domain.member.entity.Member; -import io.swagger.v3.oas.annotations.media.Schema; - -@Schema(description = "작성자 응답") -public record WriterResponse( - @Schema(description = "작성자 ID", example = "1") - Long id, - - @Schema(description = "작성자 닉네임", example = "익명의닉네임2SDSDNKLNS") - String nickname -) { - - public static WriterResponse from(final Member member) { - return new WriterResponse(member.getId(), member.getNickname()); - } - - public static WriterResponse of(final Long id, final String nickname) { - return new WriterResponse(id, nickname); - } - -} diff --git a/backend/src/main/java/com/votogether/domain/post/dto/response/vote/VoteCountForAgeGroupResponse.java b/backend/src/main/java/com/votogether/domain/post/dto/response/vote/VoteCountForAgeGroupResponse.java index 62be5c3c3..d8d9f8745 100644 --- a/backend/src/main/java/com/votogether/domain/post/dto/response/vote/VoteCountForAgeGroupResponse.java +++ b/backend/src/main/java/com/votogether/domain/post/dto/response/vote/VoteCountForAgeGroupResponse.java @@ -1,5 +1,6 @@ package com.votogether.domain.post.dto.response.vote; +import com.votogether.domain.member.entity.vo.AgeRange; import com.votogether.domain.member.entity.vo.Gender; import io.swagger.v3.oas.annotations.media.Schema; import java.util.Map; @@ -18,12 +19,13 @@ public record VoteCountForAgeGroupResponse( @Schema(description = "여자 투표 수", example = "7") int femaleCount ) { - public static VoteCountForAgeGroupResponse of(final String ageGroup, final Map genderGroup) { + + public static VoteCountForAgeGroupResponse of(final AgeRange ageRange, final Map genderGroup) { final int maleCount = genderGroup.getOrDefault(Gender.MALE, 0L).intValue(); final int femaleCount = genderGroup.getOrDefault(Gender.FEMALE, 0L).intValue(); final int voteCount = maleCount + femaleCount; - return new VoteCountForAgeGroupResponse(ageGroup, voteCount, maleCount, femaleCount); + return new VoteCountForAgeGroupResponse(ageRange.getName(), voteCount, maleCount, femaleCount); } } diff --git a/backend/src/main/java/com/votogether/domain/post/dto/response/vote/VoteDetailResponse.java b/backend/src/main/java/com/votogether/domain/post/dto/response/vote/VoteDetailResponse.java deleted file mode 100644 index 0eabf9522..000000000 --- a/backend/src/main/java/com/votogether/domain/post/dto/response/vote/VoteDetailResponse.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.votogether.domain.post.dto.response.vote; - -import com.votogether.domain.post.dto.response.post.PostOptionDetailResponse; -import io.swagger.v3.oas.annotations.media.Schema; -import java.util.List; - -@Schema(description = "투표 상세 응답") -public record VoteDetailResponse( - @Schema(description = "선택지 ID", example = "1") - Long selectedOptionId, - - @Schema(description = "총 투표 수", example = "2") - Long totalVoteCount, - - @Schema(description = "선택지 상세 응답") - List options -) { - - public static VoteDetailResponse of( - final long selectedOptionId, - final long finalTotalVoteCount, - final List options - ) { - return new VoteDetailResponse(selectedOptionId, finalTotalVoteCount, options); - } - -} diff --git a/backend/src/main/java/com/votogether/domain/post/dto/response/vote/VoteOptionStatisticsResponse.java b/backend/src/main/java/com/votogether/domain/post/dto/response/vote/VoteOptionStatisticsResponse.java index 1b794e7f7..91c216667 100644 --- a/backend/src/main/java/com/votogether/domain/post/dto/response/vote/VoteOptionStatisticsResponse.java +++ b/backend/src/main/java/com/votogether/domain/post/dto/response/vote/VoteOptionStatisticsResponse.java @@ -2,12 +2,14 @@ import com.votogether.domain.member.entity.vo.AgeRange; import com.votogether.domain.member.entity.vo.Gender; +import com.votogether.domain.vote.repository.dto.VoteCountByAgeGroupAndGenderDto; import io.swagger.v3.oas.annotations.media.Schema; import java.util.Arrays; -import java.util.HashMap; +import java.util.EnumMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; - +import java.util.stream.Collectors; @Schema(description = "투표 선택지 통계 응답") public record VoteOptionStatisticsResponse( @@ -24,13 +26,13 @@ public record VoteOptionStatisticsResponse( List ageGroup ) { - public static VoteOptionStatisticsResponse from(final Map> voteStatusGroup) { + public static VoteOptionStatisticsResponse from(final List voteCounts) { + final Map> ageRangeGroup = formAgeRangeGroup(voteCounts); final List ageGroupStatistics = Arrays.stream(AgeRange.values()) - .map(AgeRange::getName) - .map(ageName -> + .map(ageRange -> VoteCountForAgeGroupResponse.of( - ageName, - voteStatusGroup.computeIfAbsent(ageName, ignore -> new HashMap<>()) + ageRange, + ageRangeGroup.computeIfAbsent(ageRange, ignore -> new EnumMap<>(Gender.class)) ) ) .toList(); @@ -43,4 +45,20 @@ public static VoteOptionStatisticsResponse from(final Map> formAgeRangeGroup( + final List voteCounts + ) { + return voteCounts.stream() + .collect(Collectors.groupingBy( + voteCount -> AgeRange.from(voteCount.ageGroup()), + LinkedHashMap::new, + Collectors.toMap( + VoteCountByAgeGroupAndGenderDto::gender, + VoteCountByAgeGroupAndGenderDto::voteCount, + (exist, replace) -> replace, + () -> new EnumMap<>(Gender.class) + ) + )); + } + } diff --git a/backend/src/main/java/com/votogether/domain/post/dto/response/vote/VoteResponse.java b/backend/src/main/java/com/votogether/domain/post/dto/response/vote/VoteResponse.java deleted file mode 100644 index fac8d92de..000000000 --- a/backend/src/main/java/com/votogether/domain/post/dto/response/vote/VoteResponse.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.votogether.domain.post.dto.response.vote; - -import com.votogether.domain.post.dto.response.post.PostOptionResponse; -import com.votogether.domain.post.entity.Post; -import io.swagger.v3.oas.annotations.media.Schema; -import java.util.List; - - -@Schema(description = "투표 응답") -public record VoteResponse( - @Schema(description = "선택지 ID", example = "1") - long selectedOptionId, - - @Schema(description = "총 투표 수", example = "7") - long totalVoteCount, - - @Schema(description = "선택지 옵션 응답") - List options -) { - - private static final int NOT_SELECTED = 0; - private static final int HIDDEN_COUNT = -1; - - public static VoteResponse of( - final long selectedOptionId, - final long finalTotalVoteCount, - final List options - ) { - return new VoteResponse(selectedOptionId, finalTotalVoteCount, options); - } - - public static VoteResponse forGuest(final Post post) { - return new VoteResponse( - NOT_SELECTED, - post.isClosed() ? post.getTotalVoteCount() : HIDDEN_COUNT, - listOfOptionsForGuest(post) - ); - } - - private static List listOfOptionsForGuest(final Post post) { - return post.getPostOptions().getPostOptions() - .stream() - .map(postOption -> PostOptionResponse.of(post, postOption)) - .toList(); - } - - @Override - public String toString() { - return "VoteInfoResponse{" + - "selectedOptionId=" + selectedOptionId + - ", totalVoteCount=" + totalVoteCount + - ", options=" + options + - '}'; - } - -} diff --git a/backend/src/main/java/com/votogether/domain/post/entity/Post.java b/backend/src/main/java/com/votogether/domain/post/entity/Post.java index 5e293b334..f3732fa66 100644 --- a/backend/src/main/java/com/votogether/domain/post/entity/Post.java +++ b/backend/src/main/java/com/votogether/domain/post/entity/Post.java @@ -5,10 +5,10 @@ import com.votogether.domain.member.entity.Member; import com.votogether.domain.post.entity.comment.Comment; import com.votogether.domain.post.exception.PostExceptionType; -import com.votogether.domain.report.exception.ReportExceptionType; +import com.votogether.domain.post.exception.PostOptionExceptionType; import com.votogether.domain.vote.entity.Vote; +import com.votogether.domain.vote.exception.VoteExceptionType; import com.votogether.global.exception.BadRequestException; -import jakarta.persistence.Basic; import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Embedded; @@ -24,18 +24,22 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; -import java.util.stream.IntStream; import lombok.AccessLevel; import lombok.Builder; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; import org.hibernate.annotations.Formula; -@NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter @Entity +@EqualsAndHashCode(of = {"id"}, callSuper = false) +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class Post extends BaseEntity { + private static final int MAXIMUM_POST_OPTION_SIZE = 5; + private static final long DELETE_VOTE_LIMIT = 20; + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -48,22 +52,22 @@ public class Post extends BaseEntity { private PostBody postBody; @Embedded - private PostCategories postCategories; - - @Embedded - private PostOptions postOptions; - - @Column(columnDefinition = "datetime(6)", nullable = false) - private LocalDateTime deadline; + private PostDeadline postDeadline; @Column(nullable = false) private boolean isHidden; - @Basic(fetch = FetchType.LAZY) - @Formula("(select count(v.id) from Vote v where v.post_option_id in " - + "(select po.id from Post_Option po where po.post_id = id)" - + ")") - private long totalVoteCount; + @Formula("(select count(*) from comment c where c.post_id = id)") + private long commentCount; + + @OneToMany(mappedBy = "post", cascade = CascadeType.PERSIST, orphanRemoval = true) + private List postCategories = new ArrayList<>(); + + @OneToMany(mappedBy = "post", cascade = CascadeType.PERSIST, orphanRemoval = true) + private List postContentImages = new ArrayList<>(); + + @OneToMany(mappedBy = "post", cascade = CascadeType.PERSIST, orphanRemoval = true) + private List postOptions = new ArrayList<>(); @OneToMany(mappedBy = "post", cascade = CascadeType.PERSIST, orphanRemoval = true) private List comments = new ArrayList<>(); @@ -71,43 +75,57 @@ public class Post extends BaseEntity { @Builder private Post( final Member writer, - final PostBody postBody, + final String title, + final String content, final LocalDateTime deadline ) { this.writer = writer; - this.postBody = postBody; - this.deadline = deadline; - this.postCategories = new PostCategories(); - this.postOptions = new PostOptions(); + this.postBody = new PostBody(title, content); + this.postDeadline = new PostDeadline(deadline); this.isHidden = false; } - public void mapCategories(final List categories) { - this.postCategories.mapPostAndCategories(this, categories); + public void addCategory(final Category category) { + final PostCategory postCategory = PostCategory.builder() + .post(this) + .category(category) + .build(); + this.postCategories.add(postCategory); } - public void mapPostOptionsByElements( - final List postOptionContents, - final List optionImageUrls - ) { - this.postOptions = PostOptions.of(this, postOptionContents, optionImageUrls); + public void addContentImage(final String path) { + final PostContentImage postContentImage = PostContentImage.builder() + .post(this) + .imageUrl(path) + .build(); + this.postContentImages.add(postContentImage); } - public void validateDeadlineNotExceedByMaximumDeadline(final int maximumDeadline) { - LocalDateTime maximumDeadlineFromNow = LocalDateTime.now().plusDays(maximumDeadline); - if (this.deadline.isAfter(maximumDeadlineFromNow)) { - throw new BadRequestException(PostExceptionType.DEADLINE_EXCEED_THREE_DAYS); - } + public void addPostOption(final PostOption postOption) { + postOption.setPost(this); + this.postOptions.add(postOption); } - public void validateWriter(final Member member) { - if (!Objects.equals(this.writer, member)) { - throw new BadRequestException(PostExceptionType.NOT_WRITER); - } + public void addComment(final Comment comment) { + comment.setPost(this); + this.comments.add(comment); + } + + public void removePostContentImage(final PostContentImage postContentImage) { + this.postContentImages.remove(postContentImage); + } + + public void removePostOption(final PostOption deletedOption) { + this.postOptions.remove(deletedOption); } - public long getSelectedOptionId(final Member member) { - return this.postOptions.getSelectedOptionId(member); + public void update(final String title, final String content, final LocalDateTime deadline) { + this.postBody = new PostBody(title, content); + this.postDeadline = new PostDeadline(deadline); + } + + public void closeEarly() { + this.postDeadline.close(); } public Vote makeVote(final Member voter, final PostOption postOption) { @@ -125,149 +143,66 @@ public Vote makeVote(final Member voter, final PostOption postOption) { public void validateDeadLine() { if (isClosed()) { - throw new BadRequestException(PostExceptionType.POST_CLOSED); + throw new BadRequestException(PostExceptionType.CLOSED); } } - public boolean isClosed() { - return deadline.isBefore(LocalDateTime.now()); - } - private void validateVoter(final Member voter) { if (Objects.equals(this.writer.getId(), voter.getId())) { - throw new BadRequestException(PostExceptionType.NOT_VOTER); + throw new BadRequestException(VoteExceptionType.WRITER_NOT_VOTE); } } private void validatePostOption(final PostOption postOption) { if (!hasPostOption(postOption)) { - throw new BadRequestException(PostExceptionType.POST_OPTION_NOT_FOUND); + throw new BadRequestException(PostOptionExceptionType.NOT_FOUND); } } private boolean hasPostOption(final PostOption postOption) { - return postOptions.contains(postOption); - } - - public void closeEarly() { - this.deadline = LocalDateTime.now(); + return this.postOptions.contains(postOption); } - public void addContentImage(final String contentImageUrl) { - this.postBody.addContentImage(this, contentImageUrl); + public boolean canDelete() { + final long totalVoteCount = this.postOptions.stream() + .mapToLong(PostOption::getVoteCount) + .sum(); + return totalVoteCount < DELETE_VOTE_LIMIT; } - public long getFinalTotalVoteCount(final Member loginMember) { - if (isVisibleVoteResult(loginMember)) { - return this.totalVoteCount; - } - - return -1L; + public boolean isWriter(final Member member) { + return Objects.equals(this.writer, member); } - public boolean isVisibleVoteResult(final Member member) { - return this.postOptions.getSelectedOptionId(member) != 0 - || this.writer.equals(member) - || isClosed(); + public boolean isClosed() { + return this.postDeadline.isClosed(); } public void blind() { this.isHidden = true; } - public void validateMine(final Member member) { - if (this.writer.equals(member)) { - throw new BadRequestException(ReportExceptionType.REPORT_MY_POST); - } - } - - public void validateHidden() { - if (this.isHidden) { - throw new BadRequestException(ReportExceptionType.ALREADY_HIDDEN_POST); - } - } - - public void addComment(final Comment comment) { - comments.add(comment); - comment.setPost(this); - } - - public void validatePossibleToDelete() { - if (this.totalVoteCount >= 20) { - throw new BadRequestException(PostExceptionType.CANNOT_DELETE_BECAUSE_MORE_THAN_TWENTY_VOTES); - } - } - - public void update( - final PostBody postBody, - final String oldContentImageUrl, - final List contentImageUrls, - final List categories, - final List postOptionContents, - final List oldPostOptionImageUrls, - final List postOptionImageUrls, - final LocalDateTime deadline - ) { - this.postBody.update(postBody, oldContentImageUrl, contentImageUrls); - this.postCategories.update(this, categories); - addAllPostOptions(postOptionContents, oldPostOptionImageUrls, postOptionImageUrls); - this.deadline = deadline; + public boolean isLimitOptionSize(final int size) { + return size <= MAXIMUM_POST_OPTION_SIZE; } - private void addAllPostOptions( - final List postOptionContents, - final List oldPostOptionImageUrls, - final List postOptionImageUrls - ) { - this.postOptions.addAll( - this, - postOptionContents, - getPostOptionImageUrls(oldPostOptionImageUrls, postOptionImageUrls) - ); + public String getTitle() { + return this.postBody.getTitle(); } - public void postOptionsClear() { - this.postOptions.clear(); + public String getContent() { + return this.postBody.getContent(); } - private List getPostOptionImageUrls( - final List oldPostOptionImageUrls, - final List postOptionImageUrls - ) { - return IntStream.range(0, postOptionImageUrls.size()) - .mapToObj(postOptionIndex -> - getPostOptionImageUrl( - postOptionIndex, - oldPostOptionImageUrls, - postOptionImageUrls - ) - ) - .toList(); - } - - private String getPostOptionImageUrl( - final int postOptionIndex, - final List oldPostOptionImageUrls, - final List postOptionImageUrls - ) { - final String postOptionImageUrl = postOptionImageUrls.get(postOptionIndex); - if (postOptionImageUrl.isEmpty()) { - return oldPostOptionImageUrls.get(postOptionIndex); - } - - return postOptionImageUrls.get(postOptionIndex); - } - - public void validateDeadLineToModify(final LocalDateTime deadlineToModify) { - if (getCreatedAt().plusDays(3).isBefore(deadlineToModify)) { - throw new BadRequestException(PostExceptionType.DEADLINE_EXCEED_THREE_DAYS); - } + public LocalDateTime getDeadline() { + return this.postDeadline.getDeadline(); } - public void validateExistVote() { - if (totalVoteCount > 0) { - throw new BadRequestException(PostExceptionType.VOTING_PROGRESS_NOT_EDITABLE); + public PostContentImage getFirstContentImage() { + if (this.postContentImages.isEmpty()) { + return null; } + return postContentImages.get(0); } } diff --git a/backend/src/main/java/com/votogether/domain/post/entity/PostBody.java b/backend/src/main/java/com/votogether/domain/post/entity/PostBody.java index 9e5e00217..af21d2862 100644 --- a/backend/src/main/java/com/votogether/domain/post/entity/PostBody.java +++ b/backend/src/main/java/com/votogether/domain/post/entity/PostBody.java @@ -1,58 +1,51 @@ package com.votogether.domain.post.entity; +import com.votogether.domain.post.exception.PostExceptionType; +import com.votogether.global.exception.BadRequestException; import jakarta.persistence.Column; import jakarta.persistence.Embeddable; -import jakarta.persistence.Embedded; -import java.util.List; import lombok.AccessLevel; -import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.springframework.util.StringUtils; -@NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter @Embeddable +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class PostBody { - @Column(length = 100, nullable = false) + private static final int MAXIMUM_TITLE_LENGTH = 100; + private static final int MAXIMUM_CONTENT_LENGTH = 1000; + + @Column(length = MAXIMUM_TITLE_LENGTH, nullable = false) private String title; - @Column(length = 1000, nullable = false) + @Column(length = MAXIMUM_CONTENT_LENGTH, nullable = false) private String content; - @Embedded - private PostContentImages postContentImages; - - @Builder - private PostBody(final String title, final String content) { + public PostBody(final String title, final String content) { + validateTitle(title); + validateContent(content); this.title = title; this.content = content; - this.postContentImages = new PostContentImages(); - } - - public void update( - final PostBody postBody, - final String oldContentImageUrl, - final List contentImageUrls - ) { - this.title = postBody.getTitle(); - this.content = postBody.getContent(); - this.postContentImages.update(getContentImageUrl(oldContentImageUrl, contentImageUrls)); } - private String getContentImageUrl( - final String oldContentImageUrl, - final List contentImageUrls - ) { - if (contentImageUrls.isEmpty()) { - return oldContentImageUrl; + private void validateTitle(final String title) { + if (!StringUtils.hasText(title)) { + throw new BadRequestException(PostExceptionType.TITLE_EMPTY); + } + if (title.length() > MAXIMUM_TITLE_LENGTH) { + throw new BadRequestException(PostExceptionType.TITLE_INVALID_LENGTH); } - - return contentImageUrls.get(0); } - public void addContentImage(final Post post, final String contentImageUrl) { - this.postContentImages.addContentImage(post, contentImageUrl); + private void validateContent(final String content) { + if (!StringUtils.hasText(content)) { + throw new BadRequestException(PostExceptionType.CONTENT_EMPTY); + } + if (content.length() > MAXIMUM_CONTENT_LENGTH) { + throw new BadRequestException(PostExceptionType.CONTENT_INVALID_LENGTH); + } } } diff --git a/backend/src/main/java/com/votogether/domain/post/entity/PostCategories.java b/backend/src/main/java/com/votogether/domain/post/entity/PostCategories.java deleted file mode 100644 index 9f43068bb..000000000 --- a/backend/src/main/java/com/votogether/domain/post/entity/PostCategories.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.votogether.domain.post.entity; - -import com.votogether.domain.category.entity.Category; -import jakarta.persistence.CascadeType; -import jakarta.persistence.Embeddable; -import jakarta.persistence.OneToMany; -import java.util.ArrayList; -import java.util.List; -import java.util.function.Predicate; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@Getter -@Embeddable -public class PostCategories { - - @OneToMany(mappedBy = "post", cascade = CascadeType.PERSIST, orphanRemoval = true) - private List postCategories = new ArrayList<>(); - - public void mapPostAndCategories(final Post post, final List categories) { - categories.forEach(category -> postCategories.add(createPostCategory(post, category))); - } - - private PostCategory createPostCategory(final Post post, final Category category) { - return PostCategory.builder() - .post(post) - .category(category) - .build(); - } - - public void update(final Post post, final List categories) { - postCategories.removeIf(Predicate.not(postCategory -> categories.contains(postCategory.getCategory()))); - - categories.stream() - .filter(this::isCategoryNotPresent) - .forEach(category -> this.postCategories.add(createPostCategory(post, category))); - } - - private boolean isCategoryNotPresent(Category category) { - return this.postCategories.stream() - .noneMatch(postCategory -> postCategory.getCategory().equals(category)); - } - -} diff --git a/backend/src/main/java/com/votogether/domain/post/entity/PostCategory.java b/backend/src/main/java/com/votogether/domain/post/entity/PostCategory.java index ef562207c..56fb9902b 100644 --- a/backend/src/main/java/com/votogether/domain/post/entity/PostCategory.java +++ b/backend/src/main/java/com/votogether/domain/post/entity/PostCategory.java @@ -13,13 +13,15 @@ import jakarta.persistence.UniqueConstraint; import lombok.AccessLevel; import lombok.Builder; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; -@Table(uniqueConstraints = {@UniqueConstraint(columnNames = {"post_id", "category_id"})}) -@NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter @Entity +@EqualsAndHashCode(of = {"id"}, callSuper = false) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table(uniqueConstraints = {@UniqueConstraint(columnNames = {"post_id", "category_id"})}) public class PostCategory extends BaseEntity { @Id @@ -40,4 +42,8 @@ private PostCategory(final Post post, final Category category) { this.category = category; } + public void updateCategory(final Category category) { + this.category = category; + } + } diff --git a/backend/src/main/java/com/votogether/domain/post/entity/PostContentImage.java b/backend/src/main/java/com/votogether/domain/post/entity/PostContentImage.java index d6e5307b2..fb21b834f 100644 --- a/backend/src/main/java/com/votogether/domain/post/entity/PostContentImage.java +++ b/backend/src/main/java/com/votogether/domain/post/entity/PostContentImage.java @@ -14,9 +14,9 @@ import lombok.Getter; import lombok.NoArgsConstructor; -@NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter @Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class PostContentImage extends BaseEntity { @Id @@ -31,7 +31,7 @@ public class PostContentImage extends BaseEntity { private String imageUrl; @Builder - public PostContentImage(final Post post, final String imageUrl) { + private PostContentImage(final Post post, final String imageUrl) { this.post = post; this.imageUrl = imageUrl; } diff --git a/backend/src/main/java/com/votogether/domain/post/entity/PostContentImages.java b/backend/src/main/java/com/votogether/domain/post/entity/PostContentImages.java deleted file mode 100644 index 40701594f..000000000 --- a/backend/src/main/java/com/votogether/domain/post/entity/PostContentImages.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.votogether.domain.post.entity; - -import jakarta.persistence.CascadeType; -import jakarta.persistence.Embeddable; -import jakarta.persistence.OneToMany; -import java.util.ArrayList; -import java.util.List; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@Getter -@Embeddable -public class PostContentImages { - - @OneToMany(mappedBy = "post", cascade = CascadeType.PERSIST, orphanRemoval = true) - private List contentImages = new ArrayList<>(); - - public void addContentImage(final Post post, final String contentImageUrl) { - this.contentImages.add(getPostContentImage(post, contentImageUrl)); - } - - private PostContentImage getPostContentImage(final Post post, final String contentImageUrl) { - return PostContentImage.builder() - .post(post) - .imageUrl(contentImageUrl) - .build(); - } - - public void update(final String imageUrl) { - this.contentImages.get(0).updateImageUrl(imageUrl); - } - -} diff --git a/backend/src/main/java/com/votogether/domain/post/entity/PostDeadline.java b/backend/src/main/java/com/votogether/domain/post/entity/PostDeadline.java new file mode 100644 index 000000000..92528ca2e --- /dev/null +++ b/backend/src/main/java/com/votogether/domain/post/entity/PostDeadline.java @@ -0,0 +1,45 @@ +package com.votogether.domain.post.entity; + +import com.votogether.domain.post.exception.PostExceptionType; +import com.votogether.global.exception.BadRequestException; +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Embeddable +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class PostDeadline { + + private static final long MAXIMUM_DEADLINE = 14; + + @Column(columnDefinition = "datetime(6)", nullable = false) + private LocalDateTime deadline; + + public PostDeadline(final LocalDateTime deadline) { + validate(deadline); + this.deadline = deadline; + } + + private void validate(final LocalDateTime deadline) { + final LocalDate now = LocalDateTime.now().toLocalDate(); + final LocalDate deadlineDate = deadline.toLocalDate(); + if (ChronoUnit.DAYS.between(now, deadlineDate) > MAXIMUM_DEADLINE) { + throw new BadRequestException(PostExceptionType.DEADLINE_EXCEED); + } + } + + public void close() { + this.deadline = LocalDateTime.now(); + } + + public boolean isClosed() { + return this.deadline.isBefore(LocalDateTime.now()); + } + +} diff --git a/backend/src/main/java/com/votogether/domain/post/entity/PostOption.java b/backend/src/main/java/com/votogether/domain/post/entity/PostOption.java index 8849c2e3f..d97a1cd91 100644 --- a/backend/src/main/java/com/votogether/domain/post/entity/PostOption.java +++ b/backend/src/main/java/com/votogether/domain/post/entity/PostOption.java @@ -1,10 +1,10 @@ package com.votogether.domain.post.entity; import com.votogether.domain.common.BaseEntity; -import com.votogether.domain.member.entity.Member; import com.votogether.domain.vote.entity.Vote; import jakarta.persistence.CascadeType; import jakarta.persistence.Column; +import jakarta.persistence.Embedded; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; @@ -20,32 +20,41 @@ import java.util.Objects; import lombok.AccessLevel; import lombok.Builder; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.Setter; +import org.hibernate.annotations.Formula; -@Table(uniqueConstraints = {@UniqueConstraint(columnNames = {"post_id", "sequence"})}) -@NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter @Entity +@EqualsAndHashCode(of = {"id"}, callSuper = false) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table(uniqueConstraints = {@UniqueConstraint(columnNames = {"post_id", "sequence"})}) public class PostOption extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @Setter @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "post_id", nullable = false) private Post post; + @Setter @Column(nullable = false) - private int sequence; + private Integer sequence; - @Column(length = 50, nullable = false) - private String content; + @Embedded + private PostOptionBody postOptionBody; @Column private String imageUrl; + @Formula("(select count(*) from vote v where v.post_option_id = id)") + private long voteCount; + @OneToMany(mappedBy = "postOption", cascade = CascadeType.PERSIST, orphanRemoval = true) private List votes = new ArrayList<>(); @@ -58,78 +67,26 @@ private PostOption( ) { this.post = post; this.sequence = sequence; - this.content = content; + this.postOptionBody = new PostOptionBody(content); this.imageUrl = imageUrl; } - public static PostOption of( - final String postOptionContent, - final Post post, - final int postOptionSequence, - final String optionImageUrl - ) { - if (!optionImageUrl.isEmpty()) { - return toPostOptionEntity(post, postOptionSequence, postOptionContent, optionImageUrl); - } - - return toPostOptionEntity(post, postOptionSequence, postOptionContent, ""); - } - - private static PostOption toPostOptionEntity( - final Post post, - final Integer postOptionSequence, - final String postOptionContent, - final String optionImageUrl - ) { - return PostOption.builder() - .post(post) - .sequence(postOptionSequence) - .content(postOptionContent) - .imageUrl(optionImageUrl) - .build(); - } - public void addVote(final Vote vote) { - this.votes.add(vote); vote.setPostOption(this); + this.votes.add(vote); } - public boolean hasMemberVote(final Member member) { - return votes.stream() - .anyMatch(vote -> vote.isVoteByMember(member)); + public void update(final String content, final String imageUrl) { + this.postOptionBody = new PostOptionBody(content); + this.imageUrl = imageUrl; } - public boolean isBelongsTo(final Post post) { + public boolean belongsTo(final Post post) { return Objects.equals(this.post.getId(), post.getId()); } - public int getVoteCount(final boolean isPostVoteByMember) { - final int votesCount = votes.size(); - if (isPostVoteByMember) { - return votesCount; - } - - return -1; - } - - public double getVotePercent(final long totalVoteCount) { - if (isPostVoteByMember(totalVoteCount)) { - return calculateVotePercent(totalVoteCount); - } - - return totalVoteCount; - } - - private boolean isPostVoteByMember(final long totalVoteCount) { - return totalVoteCount > 0; - } - - private double calculateVotePercent(final Long totalVoteCount) { - return ((double) this.votes.size() / totalVoteCount) * 100; - } - - public int getVoteCount() { - return this.votes.size(); + public String getContent() { + return this.postOptionBody.getContent(); } } diff --git a/backend/src/main/java/com/votogether/domain/post/entity/PostOptionBody.java b/backend/src/main/java/com/votogether/domain/post/entity/PostOptionBody.java new file mode 100644 index 000000000..8f68b94b4 --- /dev/null +++ b/backend/src/main/java/com/votogether/domain/post/entity/PostOptionBody.java @@ -0,0 +1,36 @@ +package com.votogether.domain.post.entity; + +import com.votogether.domain.post.exception.PostOptionExceptionType; +import com.votogether.global.exception.BadRequestException; +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.util.StringUtils; + +@Getter +@Embeddable +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class PostOptionBody { + + private static final int MAXIMUM_CONTENT_LENGTH = 50; + + @Column(length = MAXIMUM_CONTENT_LENGTH, nullable = false) + private String content; + + public PostOptionBody(final String content) { + validate(content); + this.content = content; + } + + private void validate(final String content) { + if (!StringUtils.hasText(content)) { + throw new BadRequestException(PostOptionExceptionType.CONTENT_EMPTY); + } + if (content.length() > MAXIMUM_CONTENT_LENGTH) { + throw new BadRequestException(PostOptionExceptionType.CONTENT_INVALID_LENGTH); + } + } + +} diff --git a/backend/src/main/java/com/votogether/domain/post/entity/PostOptions.java b/backend/src/main/java/com/votogether/domain/post/entity/PostOptions.java deleted file mode 100644 index fb19477e6..000000000 --- a/backend/src/main/java/com/votogether/domain/post/entity/PostOptions.java +++ /dev/null @@ -1,85 +0,0 @@ -package com.votogether.domain.post.entity; - -import com.votogether.domain.member.entity.Member; -import jakarta.persistence.CascadeType; -import jakarta.persistence.Embeddable; -import jakarta.persistence.OneToMany; -import java.util.ArrayList; -import java.util.List; -import java.util.stream.IntStream; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@NoArgsConstructor -@Getter -@Embeddable -public class PostOptions { - - private static final Integer FIRST_OPTION_SEQUENCE = 1; - - @OneToMany(mappedBy = "post", cascade = CascadeType.PERSIST, orphanRemoval = true) - private List postOptions = new ArrayList<>(); - - public static PostOptions of( - final Post post, - final List postOptionContents, - final List optionImageUrls - ) { - final PostOptions newInstance = new PostOptions(); - final List postOptions = getPostOptions(post, postOptionContents, optionImageUrls); - - newInstance.postOptions.addAll(postOptions); - return newInstance; - } - - private static List getPostOptions( - final Post post, - final List postOptionContents, - final List optionImageUrls - ) { - return IntStream.rangeClosed(FIRST_OPTION_SEQUENCE, postOptionContents.size()) - .mapToObj(postOptionSequence -> - toPostOption(post, postOptionContents, optionImageUrls, postOptionSequence) - ) - .toList(); - } - - private static PostOption toPostOption( - final Post post, - final List postOptionContents, - final List optionImageUrls, - final int postOptionSequence - ) { - return PostOption.of( - postOptionContents.get(postOptionSequence - 1), - post, - postOptionSequence, - optionImageUrls.get(postOptionSequence - 1) - ); - } - - public Boolean contains(final PostOption postOption) { - return postOptions.contains(postOption); - } - - public Long getSelectedOptionId(final Member member) { - return postOptions.stream() - .filter(postOption -> postOption.hasMemberVote(member)) - .findAny() - .map(PostOption::getId) - .orElse(0L); - } - - public void addAll( - final Post post, - final List postOptionContents, - final List postOptionImageUrls - ) { - this.postOptions.addAll(getPostOptions(post, postOptionContents, postOptionImageUrls)); - } - - public void clear() { - this.postOptions.clear(); - } - -} diff --git a/backend/src/main/java/com/votogether/domain/post/entity/comment/Comment.java b/backend/src/main/java/com/votogether/domain/post/entity/comment/Comment.java index 8f9250848..b58dd2722 100644 --- a/backend/src/main/java/com/votogether/domain/post/entity/comment/Comment.java +++ b/backend/src/main/java/com/votogether/domain/post/entity/comment/Comment.java @@ -3,9 +3,6 @@ import com.votogether.domain.common.BaseEntity; import com.votogether.domain.member.entity.Member; import com.votogether.domain.post.entity.Post; -import com.votogether.domain.post.exception.CommentExceptionType; -import com.votogether.domain.report.exception.ReportExceptionType; -import com.votogether.global.exception.BadRequestException; import jakarta.persistence.Column; import jakarta.persistence.Embedded; import jakarta.persistence.Entity; @@ -18,13 +15,15 @@ import java.util.Objects; import lombok.AccessLevel; import lombok.Builder; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -@NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter @Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@EqualsAndHashCode(of = {"id"}, callSuper = false) public class Comment extends BaseEntity { @Id @@ -38,7 +37,7 @@ public class Comment extends BaseEntity { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "member_id", nullable = false) - private Member member; + private Member writer; @Embedded private Content content; @@ -49,47 +48,31 @@ public class Comment extends BaseEntity { @Builder private Comment( final Post post, - final Member member, + final Member writer, final String content ) { this.post = post; - this.member = member; + this.writer = writer; this.content = new Content(content); this.isHidden = false; } - public void validateWriter(final Member member) { - if (!Objects.equals(this.member.getId(), member.getId())) { - throw new BadRequestException(CommentExceptionType.NOT_WRITER); - } - } - - public void validateBelong(final Post post) { - if (!Objects.equals(this.post.getId(), post.getId())) { - throw new BadRequestException(CommentExceptionType.NOT_BELONG_POST); - } - } - - public void blind() { - this.isHidden = true; - } - - public void validateMine(final Member reporter) { - if (this.member.equals(reporter)) { - throw new BadRequestException(ReportExceptionType.REPORT_MY_COMMENT); - } + public boolean belongsTo(final Post post) { + return Objects.equals(this.post, post); } - public void validateHidden() { - if (this.isHidden) { - throw new BadRequestException(ReportExceptionType.ALREADY_HIDDEN_COMMENT); - } + public boolean isWriter(final Member member) { + return Objects.equals(this.writer, member); } public void updateContent(final String content) { this.content = new Content(content); } + public void blind() { + this.isHidden = true; + } + public String getContent() { return content.getValue(); } diff --git a/backend/src/main/java/com/votogether/domain/post/entity/comment/Content.java b/backend/src/main/java/com/votogether/domain/post/entity/comment/Content.java index c994cfe2b..5691941da 100644 --- a/backend/src/main/java/com/votogether/domain/post/entity/comment/Content.java +++ b/backend/src/main/java/com/votogether/domain/post/entity/comment/Content.java @@ -8,9 +8,9 @@ import lombok.Getter; import lombok.NoArgsConstructor; -@NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter @Embeddable +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class Content { private static final int MAXIMUM_LENGTH = 500; @@ -25,7 +25,7 @@ public Content(final String value) { private void validate(final String content) { if (content.length() > MAXIMUM_LENGTH) { - throw new BadRequestException(CommentExceptionType.INVALID_CONTENT_LENGTH); + throw new BadRequestException(CommentExceptionType.INVALID_LENGTH); } } diff --git a/backend/src/main/java/com/votogether/domain/post/entity/vo/PostClosingType.java b/backend/src/main/java/com/votogether/domain/post/entity/vo/PostClosingType.java index d98d43b4a..d15f3c68c 100644 --- a/backend/src/main/java/com/votogether/domain/post/entity/vo/PostClosingType.java +++ b/backend/src/main/java/com/votogether/domain/post/entity/vo/PostClosingType.java @@ -4,6 +4,7 @@ public enum PostClosingType { ALL, PROGRESS, - CLOSED + CLOSED, + ; } diff --git a/backend/src/main/java/com/votogether/domain/post/entity/vo/PostSortType.java b/backend/src/main/java/com/votogether/domain/post/entity/vo/PostSortType.java index 97abc0cf5..299563463 100644 --- a/backend/src/main/java/com/votogether/domain/post/entity/vo/PostSortType.java +++ b/backend/src/main/java/com/votogether/domain/post/entity/vo/PostSortType.java @@ -1,28 +1,9 @@ package com.votogether.domain.post.entity.vo; -import lombok.Getter; -import org.springframework.data.domain.Sort; -import org.springframework.data.domain.Sort.Direction; - -@Getter public enum PostSortType { - LATEST( - Sort.by(Direction.DESC, "createdAt"), - Sort.by(Direction.DESC, "postOption.post.createdAt") - ), - - HOT( - Sort.by(Direction.DESC, "totalVoteCount"), - Sort.by(Direction.DESC, "postOption.post.totalVoteCount") - ); - - private final Sort postBaseSort; - private final Sort voteBaseSort; - - PostSortType(final Sort postBaseSort, final Sort voteBaseSort) { - this.postBaseSort = postBaseSort; - this.voteBaseSort = voteBaseSort; - } + LATEST, + HOT, + ; } diff --git a/backend/src/main/java/com/votogether/domain/post/exception/CommentExceptionType.java b/backend/src/main/java/com/votogether/domain/post/exception/CommentExceptionType.java index de350346f..72e4795ff 100644 --- a/backend/src/main/java/com/votogether/domain/post/exception/CommentExceptionType.java +++ b/backend/src/main/java/com/votogether/domain/post/exception/CommentExceptionType.java @@ -6,10 +6,13 @@ @Getter public enum CommentExceptionType implements ExceptionType { - INVALID_CONTENT_LENGTH(2000, "유효하지 않은 댓글 길이입니다."), - COMMENT_NOT_FOUND(2001, "해당 댓글이 존재하지 않습니다."), - NOT_BELONG_POST(2002, "댓글의 게시글 정보와 일치하지 않습니다."), - NOT_WRITER(2003, "댓글 작성자가 아닙니다."); + INVALID_LENGTH(600, "유효하지 않은 댓글 길이입니다."), + NOT_FOUND(601, "댓글이 존재하지 않습니다."), + NOT_BELONG_POST(602, "게시글의 댓글이 아닙니다."), + NOT_WRITER(603, "댓글 작성자가 아닙니다."), + IS_HIDDEN(604, "신고에 의해 숨겨진 댓글은 접근할 수 없습니다."), + REPORT_MINE(605, "본인 댓글은 신고할 수 없습니다."), + ; private final int code; private final String message; diff --git a/backend/src/main/java/com/votogether/domain/post/exception/PostExceptionType.java b/backend/src/main/java/com/votogether/domain/post/exception/PostExceptionType.java index 7e09bd98e..884538592 100644 --- a/backend/src/main/java/com/votogether/domain/post/exception/PostExceptionType.java +++ b/backend/src/main/java/com/votogether/domain/post/exception/PostExceptionType.java @@ -6,17 +6,20 @@ @Getter public enum PostExceptionType implements ExceptionType { - POST_NOT_FOUND(1000, "해당 게시글이 존재하지 않습니다."), - POST_OPTION_NOT_FOUND(1001, "해당 게시글 투표 옵션이 존재하지 않습니다."), - UNRELATED_POST_OPTION(1002, "게시글 투표 옵션이 게시글과 연관되어 있지 않습니다."), - NOT_WRITER(1003, "해당 게시글 작성자가 아닙니다."), - POST_CLOSED(1004, "게시글이 이미 마감되었습니다."), - POST_NOT_HALF_DEADLINE(1005, "게시글이 마감 시간까지 절반의 시간 이상이 지나지 않으면 조기마감을 할 수 없습니다."), - NOT_VOTER(1004, "해당 게시글 작성자는 투표할 수 없습니다."), - DEADLINE_EXCEED_THREE_DAYS(1005, "마감 기한은 생성 시간으로부터 3일을 초과할 수 없습니다."), - WRONG_IMAGE(1006, "이미지 저장에 실패했습니다. 다시 시도해주세요."), - CANNOT_DELETE_BECAUSE_MORE_THAN_TWENTY_VOTES(1007, "투표가 20개 이상이므로 해당 게시글을 삭제할 수 없습니다."), - VOTING_PROGRESS_NOT_EDITABLE(1008, "해당 게시글은 투표가 진행되어 수정할 수 없습니다."); + NOT_FOUND(500, "게시글이 존재하지 않습니다."), + NOT_WRITER(501, "게시글 작성자가 아닙니다."), + CLOSED(502, "게시글이 마감되었습니다."), + DEADLINE_EXCEED(503, "최대 마감기간을 초과하였습니다."), + WRONG_IMAGE(504, "이미지 저장에 실패했습니다. 다시 시도해주세요."), + FAIL_DELETE_EXCEED(505, "일정 투표 수 이상의 게시글은 삭제할 수 없습니다."), + FAIL_UPDATE_VOTED_POST(506, "투표된 게시글은 수정할 수 없습니다."), + TITLE_EMPTY(507, "게시글 제목은 비어있거나 공백일 수 없습니다."), + CONTENT_EMPTY(508, "게시글 내용은 비어있거나 공백일 수 없습니다."), + TITLE_INVALID_LENGTH(509, "게시글 제목 길이가 유효하지 않습니다."), + CONTENT_INVALID_LENGTH(510, "게시글 내용 길이가 유효하지 않습니다."), + IS_HIDDEN(511, "신고에 의해 숨겨진 게시글은 접근할 수 없습니다."), + REPORT_MINE(512, "본인 게시글은 신고할 수 없습니다."), + ; private final int code; private final String message; diff --git a/backend/src/main/java/com/votogether/domain/post/exception/PostOptionExceptionType.java b/backend/src/main/java/com/votogether/domain/post/exception/PostOptionExceptionType.java new file mode 100644 index 000000000..5e0045258 --- /dev/null +++ b/backend/src/main/java/com/votogether/domain/post/exception/PostOptionExceptionType.java @@ -0,0 +1,25 @@ +package com.votogether.domain.post.exception; + +import com.votogether.global.exception.ExceptionType; +import lombok.Getter; + +@Getter +public enum PostOptionExceptionType implements ExceptionType { + + NOT_FOUND(500, "게시글 투표 옵션이 존재하지 않습니다."), + UNRELATED_POST(501, "게시글의 투표 옵션이 아닙니다."), + CONTENT_EMPTY(502, "게시글 옵션 내용은 비어있거나 공백일 수 없습니다."), + CONTENT_INVALID_LENGTH(503, "게시글 옵션 내용 길이가 유효하지 않습니다."), + SIZE_EXCEED(504, "최대 게시글 옵션 개수를 초과하였습니다."), + DUPLICATE_UPDATE(505, "게시글 옵션을 중복해서 수정할 수 없습니다."), + ; + + private final int code; + private final String message; + + PostOptionExceptionType(final int code, final String message) { + this.code = code; + this.message = message; + } + +} diff --git a/backend/src/main/java/com/votogether/domain/post/repository/CommentRepository.java b/backend/src/main/java/com/votogether/domain/post/repository/CommentRepository.java index e4010b24b..2d28a9794 100644 --- a/backend/src/main/java/com/votogether/domain/post/repository/CommentRepository.java +++ b/backend/src/main/java/com/votogether/domain/post/repository/CommentRepository.java @@ -6,12 +6,19 @@ import java.util.List; 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; public interface CommentRepository extends JpaRepository { - @EntityGraph(attributePaths = {"member"}) + @EntityGraph(attributePaths = {"writer"}) List findAllByPostAndIsHiddenFalseOrderByCreatedAtAsc(final Post post); - List findAllByMember(final Member member); + List findAllByWriter(final Member writer); + + @Modifying(flushAutomatically = true, clearAutomatically = true) + @Query("delete from Comment c where c.post.id = :postId") + void deleteAllWithPostIdInBatch(@Param("postId") final Long postId); } diff --git a/backend/src/main/java/com/votogether/domain/post/repository/PostCategoryRepository.java b/backend/src/main/java/com/votogether/domain/post/repository/PostCategoryRepository.java index d6e61a244..1e7cc6f42 100644 --- a/backend/src/main/java/com/votogether/domain/post/repository/PostCategoryRepository.java +++ b/backend/src/main/java/com/votogether/domain/post/repository/PostCategoryRepository.java @@ -1,7 +1,21 @@ package com.votogether.domain.post.repository; +import com.votogether.domain.post.entity.Post; import com.votogether.domain.post.entity.PostCategory; +import java.util.List; +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; public interface PostCategoryRepository extends JpaRepository { + + @EntityGraph(attributePaths = {"category"}) + List findAllByPost(final Post post); + + @Modifying(flushAutomatically = true, clearAutomatically = true) + @Query("delete from PostCategory p where p.post.id = :postId") + void deleteAllWithPostIdInBatch(@Param("postId") final Long postId); + } diff --git a/backend/src/main/java/com/votogether/domain/post/repository/PostContentImageRepository.java b/backend/src/main/java/com/votogether/domain/post/repository/PostContentImageRepository.java index 32bb8f3f8..54d466e9d 100644 --- a/backend/src/main/java/com/votogether/domain/post/repository/PostContentImageRepository.java +++ b/backend/src/main/java/com/votogether/domain/post/repository/PostContentImageRepository.java @@ -2,6 +2,14 @@ import com.votogether.domain.post.entity.PostContentImage; 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; public interface PostContentImageRepository extends JpaRepository { + + @Modifying(flushAutomatically = true, clearAutomatically = true) + @Query("delete from PostContentImage p where p.post.id = :postId") + void deleteAllWithPostIdInBatch(@Param("postId") final Long postId); + } diff --git a/backend/src/main/java/com/votogether/domain/post/repository/PostCustomRepository.java b/backend/src/main/java/com/votogether/domain/post/repository/PostCustomRepository.java index 18d0f022e..015230e29 100644 --- a/backend/src/main/java/com/votogether/domain/post/repository/PostCustomRepository.java +++ b/backend/src/main/java/com/votogether/domain/post/repository/PostCustomRepository.java @@ -9,26 +9,31 @@ public interface PostCustomRepository { - List findAllByClosingTypeAndSortTypeAndCategoryId( + List findPostsWithFilteringAndPaging( final PostClosingType postClosingType, final PostSortType postSortType, final Long categoryId, final Pageable pageable ); - List findAllWithKeyword( + List findPostsByWriterWithFilteringAndPaging( + final Member writer, + final PostClosingType postClosingType, + final PostSortType postSortType, + final Pageable pageable + ); + + List findSearchPostsWithFilteringAndPaging( final String keyword, final PostClosingType postClosingType, final PostSortType postSortType, - final Long categoryId, final Pageable pageable ); - List findAllByWriterWithClosingTypeAndSortTypeAndCategoryId( - final Member writer, + List findPostsByVotedWithFilteringAndPaging( + final Member voter, final PostClosingType postClosingType, final PostSortType postSortType, - final Long categoryId, final Pageable pageable ); diff --git a/backend/src/main/java/com/votogether/domain/post/repository/PostCustomRepositoryImpl.java b/backend/src/main/java/com/votogether/domain/post/repository/PostCustomRepositoryImpl.java index e271c4932..d69c64a67 100644 --- a/backend/src/main/java/com/votogether/domain/post/repository/PostCustomRepositoryImpl.java +++ b/backend/src/main/java/com/votogether/domain/post/repository/PostCustomRepositoryImpl.java @@ -2,9 +2,13 @@ import static com.votogether.domain.post.entity.QPost.post; import static com.votogether.domain.post.entity.QPostCategory.postCategory; +import static com.votogether.domain.post.entity.QPostOption.postOption; +import static com.votogether.domain.vote.entity.QVote.vote; +import com.querydsl.core.types.Order; import com.querydsl.core.types.OrderSpecifier; import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.jpa.JPAExpressions; import com.querydsl.jpa.impl.JPAQueryFactory; import com.votogether.domain.member.entity.Member; import com.votogether.domain.post.entity.Post; @@ -24,17 +28,17 @@ public class PostCustomRepositoryImpl implements PostCustomRepository { private final JPAQueryFactory jpaQueryFactory; @Override - public List findAllByClosingTypeAndSortTypeAndCategoryId( + public List findPostsWithFilteringAndPaging( final PostClosingType postClosingType, final PostSortType postSortType, final Long categoryId, final Pageable pageable ) { return jpaQueryFactory - .selectFrom(post) - .distinct() + .selectDistinct(post) + .from(post) .join(post.writer).fetchJoin() - .leftJoin(post.postCategories.postCategories, postCategory) + .leftJoin(post.postCategories, postCategory) .where( categoryIdEq(categoryId), deadlineEq(postClosingType), @@ -47,20 +51,19 @@ public List findAllByClosingTypeAndSortTypeAndCategoryId( } @Override - public List findAllByWriterWithClosingTypeAndSortTypeAndCategoryId( + public List findPostsByWriterWithFilteringAndPaging( final Member writer, final PostClosingType postClosingType, final PostSortType postSortType, - final Long categoryId, final Pageable pageable ) { return jpaQueryFactory - .selectFrom(post) + .select(post) + .from(post) .join(post.writer).fetchJoin() .where( - categoryIdEq(categoryId), - deadlineEq(postClosingType), post.writer.eq(writer), + deadlineEq(postClosingType), post.isHidden.eq(false) ) .orderBy(orderBy(postSortType)) @@ -75,44 +78,43 @@ private BooleanExpression categoryIdEq(final Long categoryId) { private BooleanExpression deadlineEq(final PostClosingType postClosingType) { final LocalDateTime now = LocalDateTime.now(); - switch (postClosingType) { - case PROGRESS: - return post.deadline.after(now); - case CLOSED: - return post.deadline.before(now); - case ALL: - default: - return null; - } + return switch (postClosingType) { + case PROGRESS -> post.postDeadline.deadline.after(now); + case CLOSED -> post.postDeadline.deadline.before(now); + default -> null; + }; } private OrderSpecifier orderBy(final PostSortType postSortType) { - switch (postSortType) { - case LATEST: - return post.createdAt.desc(); - case HOT: - return post.totalVoteCount.desc(); - default: - return OrderByNull.DEFAULT; - } + return switch (postSortType) { + case LATEST -> post.createdAt.desc(); + case HOT -> new OrderSpecifier<>( + Order.DESC, + JPAExpressions.select(vote.id.count()) + .from(vote) + .where(vote.postOption.id.in( + JPAExpressions.select(postOption.id) + .from(postOption) + .where(postOption.post.id.eq(post.id)) + )) + ); + default -> OrderByNull.DEFAULT; + }; } @Override - public List findAllWithKeyword( + public List findSearchPostsWithFilteringAndPaging( final String keyword, final PostClosingType postClosingType, final PostSortType postSortType, - final Long categoryId, final Pageable pageable ) { return jpaQueryFactory - .selectFrom(post) - .distinct() + .select(post) + .from(post) .join(post.writer).fetchJoin() - .leftJoin(post.postCategories.postCategories, postCategory) .where( containsKeywordInTitleOrContent(keyword), - categoryIdEq(categoryId), deadlineEq(postClosingType), post.isHidden.eq(false) ) @@ -127,4 +129,28 @@ private BooleanExpression containsKeywordInTitleOrContent(final String keyword) .or(post.postBody.content.contains(keyword)); } + @Override + public List findPostsByVotedWithFilteringAndPaging( + final Member voter, + final PostClosingType postClosingType, + final PostSortType postSortType, + final Pageable pageable + ) { + return jpaQueryFactory + .selectDistinct(post) + .from(post) + .join(post.writer).fetchJoin() + .leftJoin(post.postOptions, postOption) + .leftJoin(postOption.votes, vote) + .where( + vote.member.eq(voter), + deadlineEq(postClosingType), + post.isHidden.eq(false) + ) + .orderBy(orderBy(postSortType)) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + } + } diff --git a/backend/src/main/java/com/votogether/domain/post/repository/PostOptionRepository.java b/backend/src/main/java/com/votogether/domain/post/repository/PostOptionRepository.java index e4507c0ec..fc63abf05 100644 --- a/backend/src/main/java/com/votogether/domain/post/repository/PostOptionRepository.java +++ b/backend/src/main/java/com/votogether/domain/post/repository/PostOptionRepository.java @@ -2,7 +2,14 @@ import com.votogether.domain.post.entity.PostOption; 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; public interface PostOptionRepository extends JpaRepository { + @Modifying(flushAutomatically = true, clearAutomatically = true) + @Query("delete from PostOption p where p.post.id = :postId") + void deleteAllWithPostIdInBatch(@Param("postId") final Long postId); + } diff --git a/backend/src/main/java/com/votogether/domain/post/repository/PostRepository.java b/backend/src/main/java/com/votogether/domain/post/repository/PostRepository.java index 15fb326d6..09ef525fa 100644 --- a/backend/src/main/java/com/votogether/domain/post/repository/PostRepository.java +++ b/backend/src/main/java/com/votogether/domain/post/repository/PostRepository.java @@ -2,24 +2,13 @@ import com.votogether.domain.member.entity.Member; import com.votogether.domain.post.entity.Post; -import java.time.LocalDateTime; import java.util.List; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Slice; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; public interface PostRepository extends JpaRepository, PostCustomRepository { - Slice findByDeadlineBefore(final LocalDateTime currentTime, final Pageable pageable); - - Slice findByDeadlineAfter(final LocalDateTime currentTime, final Pageable pageable); - - int countByWriter(final Member member); - - List findAllByWriter(final Member member); - @Query("SELECT COUNT(p)" + "FROM Member m " + "LEFT JOIN Post p ON m.id = p.writer.id AND p.writer IN :members " + @@ -27,15 +16,8 @@ public interface PostRepository extends JpaRepository, PostCustomRep "GROUP BY m.id") List findCountsByMembers(@Param("members") final List members); - @Query("SELECT v.postOption.post FROM Vote v WHERE v.member = :member " - + "AND v.postOption.post.deadline < CURRENT_TIMESTAMP") - Slice findClosedPostsVotedByMember(@Param("member") final Member member, final Pageable pageable); - - @Query("SELECT v.postOption.post FROM Vote v WHERE v.member = :member " - + "AND v.postOption.post.deadline > CURRENT_TIMESTAMP") - Slice findOpenPostsVotedByMember(@Param("member") final Member member, final Pageable pageable); + List findAllByWriter(final Member member); - @Query("SELECT v.postOption.post FROM Vote v WHERE v.member = :member ") - Slice findPostsVotedByMember(@Param("member") final Member member, final Pageable pageable); + int countByWriter(final Member member); } diff --git a/backend/src/main/java/com/votogether/domain/post/service/PostCommandService.java b/backend/src/main/java/com/votogether/domain/post/service/PostCommandService.java new file mode 100644 index 000000000..651b5760e --- /dev/null +++ b/backend/src/main/java/com/votogether/domain/post/service/PostCommandService.java @@ -0,0 +1,368 @@ +package com.votogether.domain.post.service; + +import com.votogether.domain.category.repository.CategoryRepository; +import com.votogether.domain.member.entity.Member; +import com.votogether.domain.post.dto.request.post.PostCreateRequest; +import com.votogether.domain.post.dto.request.post.PostOptionCreateRequest; +import com.votogether.domain.post.dto.request.post.PostOptionUpdateRequest; +import com.votogether.domain.post.dto.request.post.PostUpdateRequest; +import com.votogether.domain.post.entity.Post; +import com.votogether.domain.post.entity.PostContentImage; +import com.votogether.domain.post.entity.PostOption; +import com.votogether.domain.post.exception.PostExceptionType; +import com.votogether.domain.post.exception.PostOptionExceptionType; +import com.votogether.domain.post.repository.CommentRepository; +import com.votogether.domain.post.repository.PostCategoryRepository; +import com.votogether.domain.post.repository.PostContentImageRepository; +import com.votogether.domain.post.repository.PostOptionRepository; +import com.votogether.domain.post.repository.PostRepository; +import com.votogether.domain.report.entity.vo.ReportType; +import com.votogether.domain.report.repository.ReportRepository; +import com.votogether.domain.vote.repository.VoteRepository; +import com.votogether.global.exception.BadRequestException; +import com.votogether.global.exception.NotFoundException; +import com.votogether.infra.image.ImageUploader; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +@RequiredArgsConstructor +@Transactional +@Service +public class PostCommandService { + + private final ImageUploader imageUploader; + private final PostRepository postRepository; + private final CategoryRepository categoryRepository; + private final PostCategoryRepository postCategoryRepository; + private final PostContentImageRepository postContentImageRepository; + private final PostOptionRepository postOptionRepository; + private final VoteRepository voteRepository; + private final CommentRepository commentRepository; + private final ReportRepository reportRepository; + + public Long createPost(final PostCreateRequest postCreate, final Member loginMember) { + final Post post = Post.builder() + .writer(loginMember) + .title(postCreate.getTitle()) + .content(postCreate.getContent()) + .deadline(postCreate.getDeadline()) + .build(); + + addCategories(post, postCreate.getCategoryIds()); + addImage(post, postCreate.getImageFile()); + addPostOptions(post, postCreate.getPostOptions()); + + return postRepository.save(post).getId(); + } + + private void addCategories(final Post post, final List categoryIds) { + categoryRepository.findAllById(categoryIds).forEach(post::addCategory); + } + + private void addImage(final Post post, final MultipartFile multipartFile) { + final String imageUrl = imageUploader.upload(multipartFile); + if (Objects.nonNull(imageUrl)) { + post.addContentImage(imageUrl); + } + } + + private void addPostOptions(final Post post, final List postOptionsCreates) { + IntStream.range(0, postOptionsCreates.size()) + .mapToObj(index -> createPostOptionFromRequest(index + 1, postOptionsCreates.get(index))) + .forEach(post::addPostOption); + } + + private PostOption createPostOptionFromRequest(int sequence, final PostOptionCreateRequest postOptionCreate) { + final String imageUrl = imageUploader.upload(postOptionCreate.getOptionImage()); + return PostOption.builder() + .sequence(sequence) + .content(postOptionCreate.getContent()) + .imageUrl(imageUrl) + .build(); + } + + public void updatePost( + final Long postId, + final PostUpdateRequest postUpdate, + final Member loginMember + ) { + final Post post = postRepository.findById(postId) + .orElseThrow(() -> new NotFoundException(PostExceptionType.NOT_FOUND)); + validateHiddenPost(post); + validatePostWriter(post, loginMember); + validatePostOptionUpdateCount(post, postUpdate); + + updatePostOptions(post, postUpdate.getPostOptions()); + updatePost(post, postUpdate); + } + + private void updatePostOptions(final Post post, final List postOptionUpdates) { + final List postOptions = new ArrayList<>(post.getPostOptions()); + final Set postOptionIds = postOptions.stream() + .map(PostOption::getId) + .collect(Collectors.toSet()); + final List removePostOptions = extractPostOptionRemoves(postOptions, postOptionUpdates); + final Map updateOptionGroup = + extractPostOptionUpdates(postOptionIds, postOptionUpdates); + final List addPostOptions = + extractPostOptionAdds(postOptionIds, postOptionUpdates); + + removeOriginPostOptions(post, removePostOptions); + updateOriginPostOptions(post, updateOptionGroup); + addNewPostOptions(post, addPostOptions, post.getPostOptions().size()); + } + + private List extractPostOptionRemoves( + final List postOptions, + final List postOptionUpdates + ) { + final Set updateOptionIds = postOptionUpdates.stream() + .map(PostOptionUpdateRequest::getId) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + return postOptions.stream() + .filter(postOption -> !updateOptionIds.contains(postOption.getId())) + .toList(); + } + + private Map extractPostOptionUpdates( + final Set postOptionIds, + final List postOptionUpdates + ) { + return postOptionUpdates.stream() + .filter(postOptionUpdate -> + Objects.nonNull(postOptionUpdate.getId()) && postOptionIds.contains(postOptionUpdate.getId())) + .collect(Collectors.toMap( + PostOptionUpdateRequest::getId, + postOptionUpdate -> postOptionUpdate, + (exist, replace) -> { + throw new BadRequestException(PostOptionExceptionType.DUPLICATE_UPDATE); + }, + HashMap::new + )); + } + + private List extractPostOptionAdds( + final Set postOptionIds, + final List postOptionUpdates + ) { + validatePostOptionIds(postOptionIds, postOptionUpdates); + + return postOptionUpdates.stream() + .filter(postOptionUpdate -> postOptionUpdate.getId() == null) + .toList(); + } + + private static void validatePostOptionIds( + final Set postOptionIds, + final List postOptionUpdates + ) { + postOptionUpdates.stream() + .map(PostOptionUpdateRequest::getId) + .filter(Objects::nonNull) + .filter(id -> !postOptionIds.contains(id)) + .findFirst() + .ifPresent(id -> { + throw new BadRequestException(PostOptionExceptionType.UNRELATED_POST); + }); + } + + private void removeOriginPostOptions(final Post post, final List removePostOptions) { + for (final PostOption postOption : removePostOptions) { + imageUploader.delete(postOption.getImageUrl()); + post.removePostOption(postOption); + } + postOptionRepository.flush(); + } + + private void updateOriginPostOptions(final Post post, final Map updateOptionGroup) { + final List postOptions = post.getPostOptions(); + for (final PostOption postOption : postOptions) { + updatePostOption(postOption, updateOptionGroup.get(postOption.getId())); + } + for (int i = 0; i < postOptions.size(); i++) { + postOptions.get(i).setSequence(i + 1); + } + } + + private void updatePostOption(final PostOption postOption, final PostOptionUpdateRequest postOptionUpdate) { + final String imageUrl = updateImage( + postOption.getImageUrl(), + postOptionUpdate.getImageUrl(), + postOptionUpdate.getImageFile() + ); + postOption.update(postOptionUpdate.getContent(), imageUrl); + } + + private String updateImage( + final String originImageUrl, + final String clientUrl, + final MultipartFile multipartFile + ) { + final boolean hasOriginImage = Objects.nonNull(originImageUrl); + final boolean hasClientUrl = Objects.nonNull(clientUrl) && !clientUrl.isBlank(); + final boolean hasNewImage = multipartFile != null && !multipartFile.isEmpty(); + + if (hasOriginImage && (!hasClientUrl || hasNewImage)) { + imageUploader.delete(originImageUrl); + } + if (hasNewImage) { + return imageUploader.upload(multipartFile); + } + if (hasOriginImage && hasClientUrl) { + return originImageUrl; + } + return null; + } + + private void addNewPostOptions( + final Post post, + final List postOptionUpdates, + final int prevPostOptionSize + ) { + IntStream.range(0, postOptionUpdates.size()) + .mapToObj(index -> createNewPostOption(index, postOptionUpdates.get(index), prevPostOptionSize)) + .forEach(post::addPostOption); + } + + private PostOption createNewPostOption( + final int index, + final PostOptionUpdateRequest postOptionUpdate, + final int prevPostOptionSize + ) { + final String imageUrl = imageUploader.upload(postOptionUpdate.getImageFile()); + return PostOption.builder() + .sequence(index + 1 + prevPostOptionSize) + .content(postOptionUpdate.getContent()) + .imageUrl(imageUrl) + .build(); + } + + private void updatePost(final Post post, final PostUpdateRequest postUpdate) { + updatePostCategories(post, postUpdate.getCategoryIds()); + updatePostContentImage(post, postUpdate.getImageUrl(), postUpdate.getImageFile()); + post.update(postUpdate.getTitle(), postUpdate.getContent(), postUpdate.getDeadline()); + } + + private void updatePostCategories(final Post post, final List categoryIds) { + post.getPostCategories().clear(); + postCategoryRepository.flush(); + addCategories(post, categoryIds); + } + + private void updatePostContentImage( + final Post post, + final String imageUrl, + final MultipartFile multipartFile + ) { + final PostContentImage postContentImage = post.getFirstContentImage(); + final String currentImageUrl = postContentImage != null ? postContentImage.getImageUrl() : null; + final String updatedImageUrl = updateImage(currentImageUrl, imageUrl, multipartFile); + + if (postContentImage == null && updatedImageUrl != null) { + post.addContentImage(updatedImageUrl); + return; + } + updateExistingPostContentImage(post, postContentImage, updatedImageUrl); + } + + private void updateExistingPostContentImage( + final Post post, + final PostContentImage postContentImage, + final String updatedImageUrl + ) { + if (updatedImageUrl == null) { + post.removePostContentImage(postContentImage); + return; + } + postContentImage.updateImageUrl(updatedImageUrl); + } + + public void closePostEarly(final Long postId, final Member loginMember) { + final Post post = postRepository.findById(postId) + .orElseThrow(() -> new NotFoundException(PostExceptionType.NOT_FOUND)); + validateHiddenPost(post); + validatePostWriter(post, loginMember); + post.closeEarly(); + } + + public void deletePost(final Long postId, final Member loginMember) { + final Post post = postRepository.findById(postId) + .orElseThrow(() -> new NotFoundException(PostExceptionType.NOT_FOUND)); + validateHiddenPost(post); + validatePostWriter(post, loginMember); + validatePostDeletePossible(post); + + final List postOptions = post.getPostOptions(); + final List postContentImagePaths = post.getPostContentImages() + .stream() + .map(PostContentImage::getImageUrl) + .toList(); + + deleteVotes(postOptions); + deletePostOptions(postId, postOptions); + deleteComments(postId); + deletePost(postId, postContentImagePaths); + } + + private void deleteVotes(final List postOptions) { + final List postOptionIds = postOptions.stream() + .map(PostOption::getId) + .toList(); + voteRepository.deleteAllWithPostOptionIdsInBatch(postOptionIds); + } + + private void deletePostOptions(final Long postId, final List postOptions) { + postOptions.stream() + .map(PostOption::getImageUrl) + .forEach(imageUploader::delete); + postOptionRepository.deleteAllWithPostIdInBatch(postId); + } + + private void deleteComments(final Long postId) { + commentRepository.deleteAllWithPostIdInBatch(postId); + } + + private void deletePost(final Long postId, final List postContentImagePaths) { + postCategoryRepository.deleteAllWithPostIdInBatch(postId); + postContentImagePaths.forEach(imageUploader::delete); + postContentImageRepository.deleteAllWithPostIdInBatch(postId); + reportRepository.deleteAllWithReportTypeAndTargetIdInBatch(ReportType.POST, postId); + postRepository.deleteById(postId); + } + + private void validateHiddenPost(final Post post) { + if (post.isHidden()) { + throw new BadRequestException(PostExceptionType.IS_HIDDEN); + } + } + + private void validatePostWriter(final Post post, final Member member) { + if (!post.isWriter(member)) { + throw new BadRequestException(PostExceptionType.NOT_WRITER); + } + } + + private void validatePostOptionUpdateCount(final Post post, final PostUpdateRequest postUpdate) { + if (!post.isLimitOptionSize(postUpdate.getPostOptions().size())) { + throw new BadRequestException(PostOptionExceptionType.SIZE_EXCEED); + } + } + + private void validatePostDeletePossible(final Post post) { + if (!post.canDelete()) { + throw new BadRequestException(PostExceptionType.FAIL_DELETE_EXCEED); + } + } + +} diff --git a/backend/src/main/java/com/votogether/domain/post/service/PostCommentService.java b/backend/src/main/java/com/votogether/domain/post/service/PostCommentService.java index 183d205e3..c01ea6024 100644 --- a/backend/src/main/java/com/votogether/domain/post/service/PostCommentService.java +++ b/backend/src/main/java/com/votogether/domain/post/service/PostCommentService.java @@ -1,7 +1,7 @@ package com.votogether.domain.post.service; import com.votogether.domain.member.entity.Member; -import com.votogether.domain.post.dto.request.comment.CommentRegisterRequest; +import com.votogether.domain.post.dto.request.comment.CommentCreateRequest; import com.votogether.domain.post.dto.request.comment.CommentUpdateRequest; import com.votogether.domain.post.dto.response.comment.CommentResponse; import com.votogether.domain.post.entity.Post; @@ -10,6 +10,7 @@ import com.votogether.domain.post.exception.PostExceptionType; import com.votogether.domain.post.repository.CommentRepository; import com.votogether.domain.post.repository.PostRepository; +import com.votogether.global.exception.BadRequestException; import com.votogether.global.exception.NotFoundException; import java.util.List; import lombok.RequiredArgsConstructor; @@ -23,63 +24,92 @@ public class PostCommentService { private final PostRepository postRepository; private final CommentRepository commentRepository; + @Transactional(readOnly = true) + public List getComments(final Long postId) { + final Post post = postRepository.findById(postId) + .orElseThrow(() -> new NotFoundException(PostExceptionType.NOT_FOUND)); + validateHiddenPost(post); + + return commentRepository.findAllByPostAndIsHiddenFalseOrderByCreatedAtAsc(post) + .stream() + .map(CommentResponse::from) + .toList(); + } + @Transactional public void createComment( - final Member member, final Long postId, - final CommentRegisterRequest commentRegisterRequest + final CommentCreateRequest commentCreateRequest, + final Member loginMember ) { final Post post = postRepository.findById(postId) - .orElseThrow(() -> new NotFoundException(PostExceptionType.POST_NOT_FOUND)); + .orElseThrow(() -> new NotFoundException(PostExceptionType.NOT_FOUND)); + validateHiddenPost(post); final Comment comment = Comment.builder() - .member(member) - .content(commentRegisterRequest.content()) + .writer(loginMember) + .content(commentCreateRequest.content()) .build(); - post.addComment(comment); } - @Transactional(readOnly = true) - public List getComments(final Long postId) { - final Post post = postRepository.findById(postId) - .orElseThrow(() -> new NotFoundException(PostExceptionType.POST_NOT_FOUND)); - - return commentRepository.findAllByPostAndIsHiddenFalseOrderByCreatedAtAsc(post) - .stream() - .map(CommentResponse::from) - .toList(); - } - @Transactional public void updateComment( final Long postId, final Long commentId, final CommentUpdateRequest commentUpdateRequest, - final Member member + final Member loginMember ) { final Post post = postRepository.findById(postId) - .orElseThrow(() -> new NotFoundException(PostExceptionType.POST_NOT_FOUND)); + .orElseThrow(() -> new NotFoundException(PostExceptionType.NOT_FOUND)); final Comment comment = commentRepository.findById(commentId) - .orElseThrow(() -> new NotFoundException(CommentExceptionType.COMMENT_NOT_FOUND)); + .orElseThrow(() -> new NotFoundException(CommentExceptionType.NOT_FOUND)); - comment.validateBelong(post); - comment.validateWriter(member); + validateHiddenPost(post); + validateHiddenComment(comment); + validateBelongsToPost(comment, post); + validateWriter(comment, loginMember); comment.updateContent(commentUpdateRequest.content()); } @Transactional - public void deleteComment(final Long postId, final Long commentId, final Member member) { + public void deleteComment(final Long postId, final Long commentId, final Member loginMember) { final Post post = postRepository.findById(postId) - .orElseThrow(() -> new NotFoundException(PostExceptionType.POST_NOT_FOUND)); + .orElseThrow(() -> new NotFoundException(PostExceptionType.NOT_FOUND)); final Comment comment = commentRepository.findById(commentId) - .orElseThrow(() -> new NotFoundException(CommentExceptionType.COMMENT_NOT_FOUND)); + .orElseThrow(() -> new NotFoundException(CommentExceptionType.NOT_FOUND)); - comment.validateBelong(post); - comment.validateWriter(member); + validateHiddenPost(post); + validateHiddenComment(comment); + validateBelongsToPost(comment, post); + validateWriter(comment, loginMember); commentRepository.delete(comment); } + private void validateHiddenPost(final Post post) { + if (post.isHidden()) { + throw new BadRequestException(PostExceptionType.IS_HIDDEN); + } + } + + private void validateHiddenComment(final Comment comment) { + if (comment.isHidden()) { + throw new BadRequestException(CommentExceptionType.IS_HIDDEN); + } + } + + private void validateBelongsToPost(final Comment comment, final Post post) { + if (!comment.belongsTo(post)) { + throw new BadRequestException(CommentExceptionType.NOT_BELONG_POST); + } + } + + private void validateWriter(final Comment comment, final Member member) { + if (!comment.isWriter(member)) { + throw new BadRequestException(CommentExceptionType.NOT_WRITER); + } + } + } diff --git a/backend/src/main/java/com/votogether/domain/post/service/PostGuestService.java b/backend/src/main/java/com/votogether/domain/post/service/PostGuestService.java new file mode 100644 index 000000000..f92abe420 --- /dev/null +++ b/backend/src/main/java/com/votogether/domain/post/service/PostGuestService.java @@ -0,0 +1,87 @@ +package com.votogether.domain.post.service; + +import com.votogether.domain.post.dto.response.post.PostResponse; +import com.votogether.domain.post.entity.Post; +import com.votogether.domain.post.entity.vo.PostClosingType; +import com.votogether.domain.post.entity.vo.PostSortType; +import com.votogether.domain.post.exception.PostExceptionType; +import com.votogether.domain.post.repository.PostCategoryRepository; +import com.votogether.domain.post.repository.PostRepository; +import com.votogether.global.exception.BadRequestException; +import com.votogether.global.exception.NotFoundException; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Service +public class PostGuestService { + + private static final int BASIC_PAGE_SIZE = 10; + + private final PostRepository postRepository; + private final PostCategoryRepository postCategoryRepository; + + @Transactional(readOnly = true) + public List getPosts( + final int page, + final PostClosingType postClosingType, + final PostSortType postSortType, + final Long categoryId + ) { + final Pageable pageable = PageRequest.of(page, BASIC_PAGE_SIZE); + final List posts = + postRepository.findPostsWithFilteringAndPaging(postClosingType, postSortType, categoryId, pageable); + return convertToResponses(posts); + } + + @Transactional(readOnly = true) + public PostResponse getPost(final Long postId) { + final Post post = postRepository.findById(postId) + .orElseThrow(() -> new NotFoundException(PostExceptionType.NOT_FOUND)); + validateHiddenPost(post); + + return PostResponse.ofGuest( + post, + postCategoryRepository.findAllByPost(post), + post.getFirstContentImage(), + post.getPostOptions() + ); + } + + @Transactional(readOnly = true) + public List searchPosts( + final String keyword, + final int page, + final PostClosingType postClosingType, + final PostSortType postSortType + ) { + final Pageable pageable = PageRequest.of(page, BASIC_PAGE_SIZE); + final List posts = + postRepository.findSearchPostsWithFilteringAndPaging(keyword, postClosingType, postSortType, pageable); + return convertToResponses(posts); + } + + private void validateHiddenPost(final Post post) { + if (post.isHidden()) { + throw new BadRequestException(PostExceptionType.IS_HIDDEN); + } + } + + private List convertToResponses(final List posts) { + return posts.stream() + .map(post -> + PostResponse.ofGuest( + post, + postCategoryRepository.findAllByPost(post), + post.getFirstContentImage(), + post.getPostOptions() + ) + ) + .toList(); + } + +} diff --git a/backend/src/main/java/com/votogether/domain/post/service/PostQueryService.java b/backend/src/main/java/com/votogether/domain/post/service/PostQueryService.java new file mode 100644 index 000000000..309594ecc --- /dev/null +++ b/backend/src/main/java/com/votogether/domain/post/service/PostQueryService.java @@ -0,0 +1,179 @@ +package com.votogether.domain.post.service; + +import com.votogether.domain.member.entity.Member; +import com.votogether.domain.post.dto.response.post.PostResponse; +import com.votogether.domain.post.dto.response.vote.VoteOptionStatisticsResponse; +import com.votogether.domain.post.entity.Post; +import com.votogether.domain.post.entity.PostOption; +import com.votogether.domain.post.entity.vo.PostClosingType; +import com.votogether.domain.post.entity.vo.PostSortType; +import com.votogether.domain.post.exception.PostExceptionType; +import com.votogether.domain.post.exception.PostOptionExceptionType; +import com.votogether.domain.post.repository.PostCategoryRepository; +import com.votogether.domain.post.repository.PostOptionRepository; +import com.votogether.domain.post.repository.PostRepository; +import com.votogether.domain.vote.repository.VoteRepository; +import com.votogether.domain.vote.repository.dto.VoteCountByAgeGroupAndGenderDto; +import com.votogether.global.exception.BadRequestException; +import com.votogether.global.exception.NotFoundException; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Transactional(readOnly = true) +@Service +public class PostQueryService { + + private static final int BASIC_PAGE_SIZE = 10; + + private final PostRepository postRepository; + private final PostCategoryRepository postCategoryRepository; + private final PostOptionRepository postOptionRepository; + private final VoteRepository voteRepository; + + public List getPosts( + final int page, + final PostClosingType postClosingType, + final PostSortType postSortType, + final Long categoryId, + final Member loginMember + ) { + final Pageable pageable = PageRequest.of(page, BASIC_PAGE_SIZE); + final List posts = + postRepository.findPostsWithFilteringAndPaging(postClosingType, postSortType, categoryId, pageable); + return convertToResponses(posts, loginMember); + } + + public PostResponse getPost(final Long postId, final Member loginMember) { + final Post post = postRepository.findById(postId) + .orElseThrow(() -> new NotFoundException(PostExceptionType.NOT_FOUND)); + validateHiddenPost(post); + + return PostResponse.ofUser( + loginMember, + post, + postCategoryRepository.findAllByPost(post), + post.getFirstContentImage(), + post.getPostOptions(), + voteRepository.findByMemberAndPostOptionPost(loginMember, post) + ); + } + + public List searchPosts( + final String keyword, + final int page, + final PostClosingType postClosingType, + final PostSortType postSortType, + final Member loginMember + ) { + final Pageable pageable = PageRequest.of(page, BASIC_PAGE_SIZE); + final List posts = + postRepository.findSearchPostsWithFilteringAndPaging(keyword, postClosingType, postSortType, pageable); + return convertToResponses(posts, loginMember); + } + + public List getPostsWrittenByMe( + final int page, + final PostClosingType postClosingType, + final PostSortType postSortType, + final Member loginMember + ) { + final Pageable pageable = PageRequest.of(page, BASIC_PAGE_SIZE); + final List posts = postRepository.findPostsByWriterWithFilteringAndPaging( + loginMember, + postClosingType, + postSortType, + pageable + ); + return convertToResponses(posts, loginMember); + } + + public List getPostsVotedByMe( + final int page, + final PostClosingType postClosingType, + final PostSortType postSortType, + final Member loginMember + ) { + final Pageable pageable = PageRequest.of(page, BASIC_PAGE_SIZE); + final List posts = postRepository.findPostsByVotedWithFilteringAndPaging( + loginMember, + postClosingType, + postSortType, + pageable + ); + return convertToResponses(posts, loginMember); + } + + public VoteOptionStatisticsResponse getVoteStatistics(final Long postId, final Member loginMember) { + final Post post = postRepository.findById(postId) + .orElseThrow(() -> new NotFoundException(PostExceptionType.NOT_FOUND)); + validateHiddenPost(post); + validatePostWriter(post, loginMember); + + final List result = + voteRepository.findPostVoteCountByAgeGroupAndGender(post.getId()) + .stream() + .map(VoteCountByAgeGroupAndGenderDto::from) + .toList(); + return VoteOptionStatisticsResponse.from(result); + } + + public VoteOptionStatisticsResponse getVoteOptionStatistics( + final Long postId, + final Long optionId, + final Member loginMember + ) { + final Post post = postRepository.findById(postId) + .orElseThrow(() -> new NotFoundException(PostExceptionType.NOT_FOUND)); + final PostOption postOption = postOptionRepository.findById(optionId) + .orElseThrow(() -> new NotFoundException(PostOptionExceptionType.NOT_FOUND)); + validateHiddenPost(post); + validatePostWriter(post, loginMember); + validatePostOptionBelongPost(post, postOption); + + final List result = + voteRepository.findPostOptionVoteCountByAgeGroupAndGender(postOption.getId()) + .stream() + .map(VoteCountByAgeGroupAndGenderDto::from) + .toList(); + return VoteOptionStatisticsResponse.from(result); + } + + private void validateHiddenPost(final Post post) { + if (post.isHidden()) { + throw new BadRequestException(PostExceptionType.IS_HIDDEN); + } + } + + private void validatePostWriter(final Post post, final Member member) { + if (!post.isWriter(member)) { + throw new BadRequestException(PostExceptionType.NOT_WRITER); + } + } + + private void validatePostOptionBelongPost(final Post post, final PostOption postOption) { + if (!postOption.belongsTo(post)) { + throw new BadRequestException(PostOptionExceptionType.UNRELATED_POST); + } + } + + private List convertToResponses(final List posts, final Member member) { + return posts.stream() + .map(post -> + PostResponse.ofUser( + member, + post, + postCategoryRepository.findAllByPost(post), + post.getFirstContentImage(), + post.getPostOptions(), + voteRepository.findByMemberAndPostOptionPost(member, post) + ) + ) + .toList(); + } + +} diff --git a/backend/src/main/java/com/votogether/domain/post/service/PostService.java b/backend/src/main/java/com/votogether/domain/post/service/PostService.java deleted file mode 100644 index a658d0383..000000000 --- a/backend/src/main/java/com/votogether/domain/post/service/PostService.java +++ /dev/null @@ -1,410 +0,0 @@ -package com.votogether.domain.post.service; - -import com.votogether.domain.category.entity.Category; -import com.votogether.domain.category.repository.CategoryRepository; -import com.votogether.domain.member.entity.Member; -import com.votogether.domain.member.entity.vo.AgeRange; -import com.votogether.domain.member.entity.vo.Gender; -import com.votogether.domain.member.exception.MemberExceptionType; -import com.votogether.domain.post.dto.request.post.PostCreateRequest; -import com.votogether.domain.post.dto.request.post.PostOptionCreateRequest; -import com.votogether.domain.post.dto.request.post.PostOptionUpdateRequest; -import com.votogether.domain.post.dto.request.post.PostUpdateRequest; -import com.votogether.domain.post.dto.response.post.PostDetailResponse; -import com.votogether.domain.post.dto.response.post.PostRankingResponse; -import com.votogether.domain.post.dto.response.post.PostResponse; -import com.votogether.domain.post.dto.response.post.PostSummaryResponse; -import com.votogether.domain.post.dto.response.vote.VoteOptionStatisticsResponse; -import com.votogether.domain.post.entity.Post; -import com.votogether.domain.post.entity.PostBody; -import com.votogether.domain.post.entity.PostOption; -import com.votogether.domain.post.entity.vo.PostClosingType; -import com.votogether.domain.post.entity.vo.PostSortType; -import com.votogether.domain.post.exception.PostExceptionType; -import com.votogether.domain.post.repository.PostOptionRepository; -import com.votogether.domain.post.repository.PostRepository; -import com.votogether.domain.vote.repository.VoteRepository; -import com.votogether.domain.vote.repository.dto.VoteStatus; -import com.votogether.global.exception.BadRequestException; -import com.votogether.global.exception.NotFoundException; -import com.votogether.global.util.ImageUploader; -import java.time.LocalDate; -import java.util.EnumMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.function.BiFunction; -import java.util.function.Function; -import java.util.stream.Collectors; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Slice; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.multipart.MultipartFile; - -@Service -public class PostService { - - private static final int BASIC_PAGING_SIZE = 10; - private static final int MAXIMUM_DEADLINE = 3; - - private final Map>> postsVotedByMemberMapper; - private final PostRepository postRepository; - private final PostOptionRepository postOptionRepository; - private final CategoryRepository categoryRepository; - private final VoteRepository voteRepository; - - public PostService( - final PostRepository postRepository, - final PostOptionRepository postOptionRepository, - final CategoryRepository categoryRepository, - final VoteRepository voteRepository - ) { - this.postRepository = postRepository; - this.postOptionRepository = postOptionRepository; - this.categoryRepository = categoryRepository; - this.voteRepository = voteRepository; - this.postsVotedByMemberMapper = new EnumMap<>(PostClosingType.class); - initPostsVotedByMemberMapper(); - } - - private void initPostsVotedByMemberMapper() { - postsVotedByMemberMapper.put(PostClosingType.ALL, postRepository::findPostsVotedByMember); - postsVotedByMemberMapper.put(PostClosingType.PROGRESS, postRepository::findOpenPostsVotedByMember); - postsVotedByMemberMapper.put(PostClosingType.CLOSED, postRepository::findClosedPostsVotedByMember); - } - - @Transactional - public Long save( - final PostCreateRequest postCreateRequest, - final Member loginMember, - final List contentImages, - final List optionImages - ) { - final List categories = categoryRepository.findAllById(postCreateRequest.categoryIds()); - final Post post = toPostEntity(postCreateRequest, loginMember, contentImages, optionImages, categories); - post.validateDeadlineNotExceedByMaximumDeadline(MAXIMUM_DEADLINE); - - return postRepository.save(post).getId(); - } - - private Post toPostEntity( - final PostCreateRequest postCreateRequest, - final Member loginMember, - final List contentImages, - final List optionImages, - final List categories - ) { - final Post post = toPost(postCreateRequest, loginMember); - - post.mapPostOptionsByElements( - transformElements(postCreateRequest.postOptions(), PostOptionCreateRequest::content), - transformElements(optionImages, ImageUploader::upload) - ); - post.mapCategories(categories); - addContentImageIfPresent(post, contentImages); - - return post; - } - - private Post toPost( - final PostCreateRequest postCreateRequest, - final Member loginMember - ) { - return Post.builder() - .writer(loginMember) - .postBody(toPostBody(postCreateRequest.title(), postCreateRequest.content())) - .deadline(postCreateRequest.deadline()) - .build(); - } - - private PostBody toPostBody(final String title, final String content) { - return PostBody.builder() - .title(title) - .content(content) - .build(); - } - - private void addContentImageIfPresent(final Post post, final List contentImages) { - if (isContentImagesPresent(contentImages)) { - post.addContentImage(ImageUploader.upload(contentImages.get(0))); - } - } - - private boolean isContentImagesPresent(final List contentImages) { - return Objects.nonNull(contentImages) && !contentImages.isEmpty(); - } - - @Transactional(readOnly = true) - public List getAllPostBySortTypeAndClosingTypeAndCategoryId( - final int page, - final PostClosingType postClosingType, - final PostSortType postSortType, - final Long categoryId, - final Member loginMember - ) { - final Pageable pageable = PageRequest.of(page, BASIC_PAGING_SIZE, postSortType.getPostBaseSort()); - final List posts = postRepository.findAllByClosingTypeAndSortTypeAndCategoryId( - postClosingType, - postSortType, - categoryId, - pageable - ); - return posts.stream() - .map(post -> PostResponse.of(post, loginMember)) - .toList(); - } - - @Transactional(readOnly = true) - public List getPostsGuest( - final int page, - final PostClosingType postClosingType, - final PostSortType postSortType, - final Long categoryId - ) { - final Pageable pageable = PageRequest.of(page, BASIC_PAGING_SIZE); - final List posts = postRepository.findAllByClosingTypeAndSortTypeAndCategoryId( - postClosingType, - postSortType, - categoryId, - pageable - ); - - return transformElements(posts, PostResponse::forGuest); - } - - @Transactional(readOnly = true) - public PostDetailResponse getPostById(final Long postId, final Member loginMember) { - final Post post = postRepository.findById(postId) - .orElseThrow(() -> new NotFoundException(PostExceptionType.POST_NOT_FOUND)); - - return PostDetailResponse.of(post, loginMember); - } - - @Transactional(readOnly = true) - public VoteOptionStatisticsResponse getVoteStatistics(final long postId, final Member member) { - final Post post = postRepository.findById(postId) - .orElseThrow(() -> new NotFoundException(PostExceptionType.POST_NOT_FOUND)); - - post.validateWriter(member); - - final List voteStatuses = - voteRepository.findVoteCountByPostIdGroupByAgeRangeAndGender(post.getId()); - return VoteOptionStatisticsResponse.from(groupVoteStatus(voteStatuses)); - } - - @Transactional(readOnly = true) - public VoteOptionStatisticsResponse getVoteOptionStatistics( - final long postId, - final long optionId, - final Member member - ) { - final Post post = postRepository.findById(postId) - .orElseThrow(() -> new NotFoundException(PostExceptionType.POST_NOT_FOUND)); - final PostOption postOption = postOptionRepository.findById(optionId) - .orElseThrow(() -> new NotFoundException(PostExceptionType.POST_OPTION_NOT_FOUND)); - - if (!postOption.isBelongsTo(post)) { - throw new BadRequestException(PostExceptionType.UNRELATED_POST_OPTION); - } - post.validateWriter(member); - - final List voteStatuses = - voteRepository.findVoteCountByPostOptionIdGroupByAgeRangeAndGender(postOption.getId()); - return VoteOptionStatisticsResponse.from(groupVoteStatus(voteStatuses)); - } - - private Map> groupVoteStatus(final List voteStatuses) { - return voteStatuses.stream() - .collect(Collectors.groupingBy( - status -> groupAgeRange(status.birthYear()), - LinkedHashMap::new, - Collectors.groupingBy( - VoteStatus::gender, - LinkedHashMap::new, - Collectors.summingLong(VoteStatus::count) - ) - )); - } - - private String groupAgeRange(final Integer birthYear) { - final int age = LocalDate.now().getYear() - birthYear + 1; - if (age <= 0) { - throw new BadRequestException(MemberExceptionType.INVALID_AGE); - } - return AgeRange.from(age).getName(); - } - - @Transactional(readOnly = true) - public List getPostsVotedByMember( - final int page, - final PostClosingType postClosingType, - final PostSortType postSortType, - final Member member - ) { - final Pageable pageable = PageRequest.of(page, BASIC_PAGING_SIZE, postSortType.getVoteBaseSort()); - - Slice posts = postsVotedByMemberMapper.get(postClosingType).apply(member, pageable); - - return posts.stream() - .map(post -> PostResponse.of(post, member)) - .toList(); - } - - @Transactional - public void closePostEarlyById(final Long id, final Member loginMember) { - final Post post = postRepository.findById(id) - .orElseThrow(() -> new NotFoundException(PostExceptionType.POST_NOT_FOUND)); - - post.validateWriter(loginMember); - post.validateDeadLine(); - post.closeEarly(); - } - - @Transactional(readOnly = true) - public List getPostsByWriter( - final int page, - final PostClosingType postClosingType, - final PostSortType postSortType, - final Long categoryId, - final Member member - ) { - final Pageable pageable = PageRequest.of(page, BASIC_PAGING_SIZE); - final List posts = postRepository.findAllByWriterWithClosingTypeAndSortTypeAndCategoryId( - member, - postClosingType, - postSortType, - categoryId, - pageable - ); - - return posts.stream() - .map(post -> PostResponse.of(post, member)) - .toList(); - } - - public List searchPostsWithKeyword( - final String keyword, - final int page, - final PostClosingType postClosingType, - final PostSortType postSortType, - final Long categoryId, - final Member member - ) { - final Pageable pageable = PageRequest.of(page, BASIC_PAGING_SIZE); - final List posts = - postRepository.findAllWithKeyword(keyword, postClosingType, postSortType, categoryId, pageable); - - return posts.stream() - .map(post -> PostResponse.of(post, member)) - .toList(); - } - - public List searchPostsWithKeywordForGuest( - final String keyword, - final int page, - final PostClosingType postClosingType, - final PostSortType postSortType, - final Long categoryId - ) { - final Pageable pageable = PageRequest.of(page, BASIC_PAGING_SIZE); - final List posts = - postRepository.findAllWithKeyword(keyword, postClosingType, postSortType, categoryId, pageable); - - return posts.stream() - .map(PostResponse::forGuest) - .toList(); - } - - @Transactional - public void delete(final Long postId) { - final Post post = postRepository.findById(postId) - .orElseThrow(() -> new BadRequestException(PostExceptionType.POST_NOT_FOUND)); - post.validatePossibleToDelete(); - - postRepository.deleteById(postId); - } - - @Transactional - public void update( - final long postId, - final PostUpdateRequest request, - final Member member, - final List contentImages, - final List optionImages - ) { - final Post post = postRepository.findById(postId) - .orElseThrow(() -> new BadRequestException(PostExceptionType.POST_NOT_FOUND)); - - post.validateExistVote(); - post.validateWriter(member); - post.validateDeadLine(); - post.validateDeadLineToModify(request.deadline()); - - postOptionsInit(post); - post.update( - toPostBody(request.title(), request.content()), - request.imageUrl(), - transformElements(contentImages, ImageUploader::upload), - categoryRepository.findAllById(request.categoryIds()), - transformElements(request.postOptions(), PostOptionUpdateRequest::content), - transformElements(request.postOptions(), PostOptionUpdateRequest::imageUrl), - transformElements(optionImages, ImageUploader::upload), - request.deadline() - ); - } - - private void postOptionsInit(final Post post) { - post.postOptionsClear(); - postRepository.flush(); - } - - private List transformElements(final List elements, final Function process) { - return elements.stream() - .map(process) - .toList(); - } - - @Transactional(readOnly = true) - public List getRanking() { - final List posts = postRepository.findAllByClosingTypeAndSortTypeAndCategoryId( - PostClosingType.ALL, - PostSortType.HOT, - null, - PageRequest.of(0, BASIC_PAGING_SIZE) - ); - - final Map rankings = calculateRanking(posts); - - return rankings.entrySet().stream() - .map(entry -> - new PostRankingResponse( - entry.getValue(), - PostSummaryResponse.from(entry.getKey()) - ) - ) - .toList(); - } - - private Map calculateRanking(final List posts) { - final Map rankings = new LinkedHashMap<>(); - - int currentRanking = 1; - int previousRanking = -1; - long previousVoteCount = -1; - for (Post post : posts) { - final long currentVoteCount = post.getTotalVoteCount(); - final int ranking = (currentVoteCount == previousVoteCount) ? previousRanking : currentRanking; - rankings.put(post, ranking); - - previousRanking = ranking; - previousVoteCount = currentVoteCount; - currentRanking++; - } - - return rankings; - } -} - diff --git a/backend/src/main/java/com/votogether/domain/report/repository/ReportRepository.java b/backend/src/main/java/com/votogether/domain/report/repository/ReportRepository.java index c575552d0..85fc48e1a 100644 --- a/backend/src/main/java/com/votogether/domain/report/repository/ReportRepository.java +++ b/backend/src/main/java/com/votogether/domain/report/repository/ReportRepository.java @@ -6,6 +6,9 @@ import java.util.List; import java.util.Optional; 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; public interface ReportRepository extends JpaRepository { @@ -21,6 +24,11 @@ Optional findByMemberAndReportTypeAndTargetId( List findAllByReportTypeAndTargetId(final ReportType reportType, final Long targetId); - void deleteByReportTypeAndTargetId(final ReportType reportType, final Long targetId); + @Modifying(flushAutomatically = true, clearAutomatically = true) + @Query("delete from Report r where r.reportType = :reportType and r.targetId = :targetId") + void deleteAllWithReportTypeAndTargetIdInBatch( + @Param("reportType") ReportType reportType, + @Param("targetId") final Long targetId + ); } diff --git a/backend/src/main/java/com/votogether/domain/report/service/strategy/ReportCommentStrategy.java b/backend/src/main/java/com/votogether/domain/report/service/strategy/ReportCommentStrategy.java index 5fa576365..302f84626 100644 --- a/backend/src/main/java/com/votogether/domain/report/service/strategy/ReportCommentStrategy.java +++ b/backend/src/main/java/com/votogether/domain/report/service/strategy/ReportCommentStrategy.java @@ -7,6 +7,7 @@ import com.votogether.domain.report.dto.request.ReportRequest; import com.votogether.domain.report.exception.ReportExceptionType; import com.votogether.domain.report.repository.ReportRepository; +import com.votogether.global.exception.BadRequestException; import com.votogether.global.exception.NotFoundException; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; @@ -23,7 +24,7 @@ public class ReportCommentStrategy implements ReportStrategy { @Override public void report(final Member reporter, final ReportRequest request) { final Comment reportedComment = commentRepository.findById(request.id()) - .orElseThrow(() -> new NotFoundException(CommentExceptionType.COMMENT_NOT_FOUND)); + .orElseThrow(() -> new NotFoundException(CommentExceptionType.NOT_FOUND)); validateComment(reporter, request, reportedComment); saveReport(reporter, request, reportRepository); @@ -35,8 +36,8 @@ private void validateComment( final ReportRequest request, final Comment reportedComment ) { - reportedComment.validateMine(reporter); - reportedComment.validateHidden(); + validateHiddenComment(reportedComment); + validateCommentMine(reportedComment, reporter); validateDuplicatedReport( reporter, request, @@ -52,4 +53,16 @@ private void blindComment(final ReportRequest request, final Comment reportedCom } } + private void validateHiddenComment(final Comment comment) { + if (comment.isHidden()) { + throw new BadRequestException(CommentExceptionType.IS_HIDDEN); + } + } + + private void validateCommentMine(final Comment comment, final Member member) { + if (comment.isWriter(member)) { + throw new BadRequestException(CommentExceptionType.REPORT_MINE); + } + } + } diff --git a/backend/src/main/java/com/votogether/domain/report/service/strategy/ReportNicknameStrategy.java b/backend/src/main/java/com/votogether/domain/report/service/strategy/ReportNicknameStrategy.java index 7d3361fa9..c8714a42f 100644 --- a/backend/src/main/java/com/votogether/domain/report/service/strategy/ReportNicknameStrategy.java +++ b/backend/src/main/java/com/votogether/domain/report/service/strategy/ReportNicknameStrategy.java @@ -52,7 +52,7 @@ private void changeNicknameByReport(final Member reportedMember, final ReportReq final int reportCount = reportRepository.countByReportTypeAndTargetId(request.type(), reportedMember.getId()); if (reportCount >= NUMBER_OF_NICKNAME_CHANGE_REPORTS) { reportedMember.changeNicknameByReport(); - reportRepository.deleteByReportTypeAndTargetId(ReportType.NICKNAME, request.id()); + reportRepository.deleteAllWithReportTypeAndTargetIdInBatch(ReportType.NICKNAME, request.id()); } } diff --git a/backend/src/main/java/com/votogether/domain/report/service/strategy/ReportPostStrategy.java b/backend/src/main/java/com/votogether/domain/report/service/strategy/ReportPostStrategy.java index be85c7219..5ebc55d23 100644 --- a/backend/src/main/java/com/votogether/domain/report/service/strategy/ReportPostStrategy.java +++ b/backend/src/main/java/com/votogether/domain/report/service/strategy/ReportPostStrategy.java @@ -7,6 +7,7 @@ import com.votogether.domain.report.dto.request.ReportRequest; import com.votogether.domain.report.exception.ReportExceptionType; import com.votogether.domain.report.repository.ReportRepository; +import com.votogether.global.exception.BadRequestException; import com.votogether.global.exception.NotFoundException; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; @@ -23,7 +24,7 @@ public class ReportPostStrategy implements ReportStrategy { @Override public void report(final Member reporter, final ReportRequest request) { final Post reportedPost = postRepository.findById(request.id()) - .orElseThrow(() -> new NotFoundException(PostExceptionType.POST_NOT_FOUND)); + .orElseThrow(() -> new NotFoundException(PostExceptionType.NOT_FOUND)); validatePost(reporter, reportedPost, request); saveReport(reporter, request, reportRepository); @@ -35,8 +36,8 @@ private void validatePost( final Post reportedPost, final ReportRequest request ) { - reportedPost.validateMine(reporter); - reportedPost.validateHidden(); + validateHiddenPost(reportedPost); + validatePostMine(reportedPost, reporter); validateDuplicatedReport( reporter, request, @@ -52,4 +53,16 @@ private void blindPost(final ReportRequest request, final Post reportedPost) { } } + private void validateHiddenPost(final Post post) { + if (post.isHidden()) { + throw new BadRequestException(PostExceptionType.IS_HIDDEN); + } + } + + private void validatePostMine(final Post post, final Member member) { + if (post.isWriter(member)) { + throw new BadRequestException(PostExceptionType.REPORT_MINE); + } + } + } diff --git a/backend/src/main/java/com/votogether/domain/report/service/strategy/ReportStrategy.java b/backend/src/main/java/com/votogether/domain/report/service/strategy/ReportStrategy.java index 9b5f7c464..33b58a44e 100644 --- a/backend/src/main/java/com/votogether/domain/report/service/strategy/ReportStrategy.java +++ b/backend/src/main/java/com/votogether/domain/report/service/strategy/ReportStrategy.java @@ -17,7 +17,7 @@ default void validateDuplicatedReport( final ReportRequest request, final ReportExceptionType reportExceptionType, final ReportRepository reportRepository - ) { + ) { reportRepository.findByMemberAndReportTypeAndTargetId(reporter, request.type(), request.id()) .ifPresent(report -> { throw new BadRequestException(reportExceptionType); diff --git a/backend/src/main/java/com/votogether/domain/vote/exception/VoteExceptionType.java b/backend/src/main/java/com/votogether/domain/vote/exception/VoteExceptionType.java new file mode 100644 index 000000000..02ca4e3df --- /dev/null +++ b/backend/src/main/java/com/votogether/domain/vote/exception/VoteExceptionType.java @@ -0,0 +1,19 @@ +package com.votogether.domain.vote.exception; + +import com.votogether.global.exception.ExceptionType; +import lombok.Getter; + +@Getter +public enum VoteExceptionType implements ExceptionType { + + WRITER_NOT_VOTE(700, "해당 게시글 작성자는 투표할 수 없습니다."); + + private final int code; + private final String message; + + VoteExceptionType(final int code, final String message) { + this.code = code; + this.message = message; + } + +} diff --git a/backend/src/main/java/com/votogether/domain/vote/repository/VoteRepository.java b/backend/src/main/java/com/votogether/domain/vote/repository/VoteRepository.java index c508910dd..7a3697b71 100644 --- a/backend/src/main/java/com/votogether/domain/vote/repository/VoteRepository.java +++ b/backend/src/main/java/com/votogether/domain/vote/repository/VoteRepository.java @@ -1,45 +1,67 @@ package com.votogether.domain.vote.repository; import com.votogether.domain.member.entity.Member; +import com.votogether.domain.post.entity.Post; import com.votogether.domain.post.entity.PostOption; import com.votogether.domain.vote.entity.Vote; -import com.votogether.domain.vote.repository.dto.VoteStatus; +import com.votogether.domain.vote.repository.dto.VoteCountByAgeGroupAndGenderInterface; import java.util.List; import java.util.Optional; 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; public interface VoteRepository extends JpaRepository { - @Query("SELECT new com.votogether.domain.vote.repository.dto.VoteStatus(m.birthYear, m.gender, COUNT(v))" + - " FROM Vote v" + - " JOIN v.member m" + - " JOIN v.postOption p" + - " WHERE p.post.id = :postId" + - " GROUP BY m.birthYear, m.gender" + - " ORDER BY m.birthYear DESC" + @Query(value = "select" + + " case" + + " when YEAR(CURRENT_DATE) - m.birth_year < 10 then 0" + + " when YEAR(CURRENT_DATE) - m.birth_year < 20 then 1" + + " when YEAR(CURRENT_DATE) - m.birth_year < 30 then 2" + + " when YEAR(CURRENT_DATE) - m.birth_year < 40 then 3" + + " when YEAR(CURRENT_DATE) - m.birth_year < 50 then 4" + + " when YEAR(CURRENT_DATE) - m.birth_year < 60 then 5" + + " else 6 end as ageGroup," + + " m.gender as gender," + + " count(m.id) as voteCount" + + " from vote as v" + + " left join member as m on v.member_id = m.id" + + " left join post_option as p on v.post_option_id = p.id" + + " where p.post_id = :post_id" + + " group by" + + " ageGroup, m.gender" + + " order by" + + " ageGroup, m.gender desc", + nativeQuery = true ) - List findVoteCountByPostIdGroupByAgeRangeAndGender(@Param("postId") final Long postId); - - @Query("SELECT new com.votogether.domain.vote.repository.dto.VoteStatus(m.birthYear, m.gender, COUNT(v))" + - " FROM Vote v" + - " JOIN v.member m" + - " WHERE v.postOption.id = :postOptionId" + - " GROUP BY m.birthYear, m.gender" + - " ORDER BY m.birthYear DESC" - ) - List findVoteCountByPostOptionIdGroupByAgeRangeAndGender( - @Param("postOptionId") final Long postOptionId + List findPostVoteCountByAgeGroupAndGender( + @Param("post_id") final Long postId ); - Optional findByMemberAndPostOption(final Member member, final PostOption postOption); - - List findAllByMemberAndPostOptionIn(final Member member, final List postOptions); - - int countByMember(final Member member); - - List findAllByMember(final Member member); + @Query(value = "select" + + " case" + + " when YEAR(CURRENT_DATE) - m.birth_year < 10 then 0" + + " when YEAR(CURRENT_DATE) - m.birth_year < 20 then 1" + + " when YEAR(CURRENT_DATE) - m.birth_year < 30 then 2" + + " when YEAR(CURRENT_DATE) - m.birth_year < 40 then 3" + + " when YEAR(CURRENT_DATE) - m.birth_year < 50 then 4" + + " when YEAR(CURRENT_DATE) - m.birth_year < 60 then 5" + + " else 6 end as ageGroup," + + " m.gender as gender," + + " count(m.id) as voteCount" + + " from vote as v" + + " left join member as m on v.member_id = m.id" + + " where v.post_option_id = :post_option_id" + + " group by" + + " ageGroup, m.gender" + + " order by" + + " ageGroup, m.gender desc", + nativeQuery = true + ) + List findPostOptionVoteCountByAgeGroupAndGender( + @Param("post_option_id") final Long postOptionId + ); @Query("SELECT COUNT(v)" + "FROM Member m " + @@ -48,4 +70,18 @@ List findVoteCountByPostOptionIdGroupByAgeRangeAndGender( "GROUP BY m.id") List findCountsByMembers(@Param("members") final List members); + Optional findByMemberAndPostOptionPost(final Member member, final Post post); + + Optional findByMemberAndPostOption(final Member member, final PostOption postOption); + + List findAllByMemberAndPostOptionIn(final Member member, final List postOptions); + + List findAllByMember(final Member member); + + int countByMember(final Member member); + + @Modifying(flushAutomatically = true, clearAutomatically = true) + @Query("delete from Vote v where v.postOption.id in :postOptionIds") + void deleteAllWithPostOptionIdsInBatch(@Param("postOptionIds") final List postOptionIds); + } diff --git a/backend/src/main/java/com/votogether/domain/vote/repository/dto/VoteCountByAgeGroupAndGenderDto.java b/backend/src/main/java/com/votogether/domain/vote/repository/dto/VoteCountByAgeGroupAndGenderDto.java new file mode 100644 index 000000000..3bd0ac7d4 --- /dev/null +++ b/backend/src/main/java/com/votogether/domain/vote/repository/dto/VoteCountByAgeGroupAndGenderDto.java @@ -0,0 +1,22 @@ +package com.votogether.domain.vote.repository.dto; + +import com.querydsl.core.annotations.QueryProjection; +import com.votogether.domain.member.entity.vo.Gender; + +public record VoteCountByAgeGroupAndGenderDto(int ageGroup, Gender gender, long voteCount) { + + @QueryProjection + public VoteCountByAgeGroupAndGenderDto { + } + + public static VoteCountByAgeGroupAndGenderDto from( + final VoteCountByAgeGroupAndGenderInterface voteCountByAgeGroupAndGenderInterface + ) { + return new VoteCountByAgeGroupAndGenderDto( + voteCountByAgeGroupAndGenderInterface.getAgeGroup(), + voteCountByAgeGroupAndGenderInterface.getGender(), + voteCountByAgeGroupAndGenderInterface.getVoteCount() + ); + } + +} diff --git a/backend/src/main/java/com/votogether/domain/vote/repository/dto/VoteCountByAgeGroupAndGenderInterface.java b/backend/src/main/java/com/votogether/domain/vote/repository/dto/VoteCountByAgeGroupAndGenderInterface.java new file mode 100644 index 000000000..651d8afca --- /dev/null +++ b/backend/src/main/java/com/votogether/domain/vote/repository/dto/VoteCountByAgeGroupAndGenderInterface.java @@ -0,0 +1,13 @@ +package com.votogether.domain.vote.repository.dto; + +import com.votogether.domain.member.entity.vo.Gender; + +public interface VoteCountByAgeGroupAndGenderInterface { + + int getAgeGroup(); + + Gender getGender(); + + long getVoteCount(); + +} diff --git a/backend/src/main/java/com/votogether/domain/vote/service/VoteService.java b/backend/src/main/java/com/votogether/domain/vote/service/VoteService.java index 8273e0d7d..7e11e30e7 100644 --- a/backend/src/main/java/com/votogether/domain/vote/service/VoteService.java +++ b/backend/src/main/java/com/votogether/domain/vote/service/VoteService.java @@ -3,7 +3,6 @@ import com.votogether.domain.member.entity.Member; import com.votogether.domain.post.entity.Post; import com.votogether.domain.post.entity.PostOption; -import com.votogether.domain.post.entity.PostOptions; import com.votogether.domain.post.repository.PostOptionRepository; import com.votogether.domain.post.repository.PostRepository; import com.votogether.domain.vote.entity.Vote; @@ -41,9 +40,9 @@ public void vote( private void validateAlreadyVoted(final Member member, final Post post) { - final PostOptions postOptions = post.getPostOptions(); + final List postOptions = post.getPostOptions(); final List alreadyVoted = - voteRepository.findAllByMemberAndPostOptionIn(member, postOptions.getPostOptions()); + voteRepository.findAllByMemberAndPostOptionIn(member, postOptions); if (!alreadyVoted.isEmpty()) { throw new IllegalStateException("해당 게시물에는 이미 투표하였습니다."); } diff --git a/backend/src/main/java/com/votogether/global/exception/ErrorResponse.java b/backend/src/main/java/com/votogether/global/exception/ErrorResponse.java new file mode 100644 index 000000000..47f3c6293 --- /dev/null +++ b/backend/src/main/java/com/votogether/global/exception/ErrorResponse.java @@ -0,0 +1,22 @@ +package com.votogether.global.exception; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "요청 검증 에러 응답") +public record ErrorResponse( + @Schema(description = "검증 실패 필드", example = "id") + String fieldName, + + @Schema(description = "검증 실패 에러 메시지", example = "ID는 양수만 가능합니다.") + String message +) { + + @Override + public String toString() { + return "{" + + "fieldName='" + fieldName + '\'' + + ", message='" + message + '\'' + + '}'; + } + +} diff --git a/backend/src/main/java/com/votogether/global/exception/ExceptionResponse.java b/backend/src/main/java/com/votogether/global/exception/ExceptionResponse.java index 429791ebb..1ed7bba1d 100644 --- a/backend/src/main/java/com/votogether/global/exception/ExceptionResponse.java +++ b/backend/src/main/java/com/votogether/global/exception/ExceptionResponse.java @@ -6,6 +6,7 @@ public record ExceptionResponse( @Schema(description = "예외 코드", example = "-9999") int code, + @Schema(description = "예외 메시지", example = "알 수 없는 서버 예외가 발생하였습니다.") String message ) { diff --git a/backend/src/main/java/com/votogether/global/exception/GlobalExceptionHandler.java b/backend/src/main/java/com/votogether/global/exception/GlobalExceptionHandler.java index cd2bbe06d..3d82d87d2 100644 --- a/backend/src/main/java/com/votogether/global/exception/GlobalExceptionHandler.java +++ b/backend/src/main/java/com/votogether/global/exception/GlobalExceptionHandler.java @@ -1,12 +1,13 @@ package com.votogether.global.exception; import com.votogether.domain.post.exception.PostExceptionType; +import jakarta.validation.ConstraintViolationException; import java.util.List; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.validation.ObjectError; import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingServletRequestParameterException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; @@ -21,35 +22,58 @@ public class GlobalExceptionHandler { public ResponseEntity handleException(final Exception e) { log.error("[" + e.getClass() + "] : " + e.getMessage()); return ResponseEntity.internalServerError() - .body(new ExceptionResponse(-9999, "알 수 없는 서버 에러가 발생했습니다.")); + .body(new ExceptionResponse(100, "알 수 없는 서버 에러가 발생했습니다.")); + } + + @ExceptionHandler + public ResponseEntity handleMethodArgumentNotValidException( + final MethodArgumentNotValidException e + ) { + final List errorResponses = e.getBindingResult() + .getFieldErrors() + .stream() + .map(fieldError -> new ErrorResponse(fieldError.getField(), fieldError.getDefaultMessage())) + .toList(); + log.warn("[" + e.getClass() + "] " + errorResponses); + return ResponseEntity.badRequest() + .body(new ExceptionResponse(200, errorResponses.toString())); + } + + @ExceptionHandler(ConstraintViolationException.class) + public ResponseEntity constraintViolationException(final ConstraintViolationException e) { + final List errorResponses = e.getConstraintViolations() + .stream() + .map(error -> new ErrorResponse(error.getPropertyPath().toString(), error.getMessage())) + .toList(); + log.warn("[" + e.getClass() + "] " + errorResponses); + return ResponseEntity.badRequest() + .body(new ExceptionResponse(201, errorResponses.toString())); } @ExceptionHandler public ResponseEntity handleMethodArgumentTypeMismatchException( final MethodArgumentTypeMismatchException e ) { - final String errorMessage = String.format( - "%s는 %s 타입이 필요합니다.", - e.getPropertyName(), - e.getRequiredType().getSimpleName() + final ErrorResponse errorResponse = new ErrorResponse( + e.getName(), + e.getRequiredType().getSimpleName() + " 타입으로 변환할 수 없는 요청입니다." ); - log.warn("[" + e.getClass() + "] : " + errorMessage); + log.warn("[" + e.getClass() + "] " + errorResponse); return ResponseEntity.badRequest() - .body(new ExceptionResponse(-9998, errorMessage)); + .body(new ExceptionResponse(202, errorResponse.toString())); } @ExceptionHandler - public ResponseEntity handleMethodArgumentNotValidException( - final MethodArgumentNotValidException e + public ResponseEntity handleMissingServletRequestParameterException( + final MissingServletRequestParameterException e ) { - final List errorMessages = e.getBindingResult() - .getAllErrors() - .stream() - .map(ObjectError::getDefaultMessage) - .toList(); - log.warn("[" + e.getClass() + "] : " + errorMessages); + final ErrorResponse errorResponse = new ErrorResponse( + e.getParameterName(), + "파라미터가 필요합니다." + ); + log.warn("[" + e.getClass() + "] " + errorResponse); return ResponseEntity.badRequest() - .body(new ExceptionResponse(-9997, errorMessages.toString())); + .body(new ExceptionResponse(203, errorResponse.toString())); } @ExceptionHandler @@ -59,6 +83,13 @@ public ResponseEntity handleBadRequestException(final BadRequ .body(ExceptionResponse.from(e)); } + @ExceptionHandler + public ResponseEntity handleImageException(final ImageException e) { + log.warn("[" + e.getClass() + "] : " + e.getMessage()); + return ResponseEntity.badRequest() + .body(ExceptionResponse.from(e)); + } + @ExceptionHandler public ResponseEntity handleNotFoundException(final NotFoundException e) { log.warn("[" + e.getClass() + "] : " + e.getMessage()); @@ -68,10 +99,6 @@ public ResponseEntity handleNotFoundException(final NotFoundE @ExceptionHandler public ResponseEntity handleMultipartException(final MultipartException e) { - System.out.println("================================"); - System.out.println("GlobalExceptionHandler.handleMultipartException"); - e.printStackTrace(); - log.warn("[" + e.getClass() + "] : " + e.getMessage()); return ResponseEntity.status(HttpStatus.NOT_FOUND) .body(ExceptionResponse.from(new BadRequestException(PostExceptionType.WRONG_IMAGE))); @@ -81,10 +108,6 @@ public ResponseEntity handleMultipartException(final Multipar public ResponseEntity handleMissingServletRequestPartException( final MissingServletRequestPartException e ) { - System.out.println("================================"); - System.out.println("GlobalExceptionHandler.handleMissingServletRequestPartException"); - e.printStackTrace(); - log.warn("[" + e.getClass() + "] : " + e.getMessage()); return ResponseEntity.status(HttpStatus.NOT_FOUND) .body(ExceptionResponse.from(new BadRequestException(PostExceptionType.WRONG_IMAGE))); diff --git a/backend/src/main/java/com/votogether/global/exception/ImageException.java b/backend/src/main/java/com/votogether/global/exception/ImageException.java new file mode 100644 index 000000000..a0e566375 --- /dev/null +++ b/backend/src/main/java/com/votogether/global/exception/ImageException.java @@ -0,0 +1,9 @@ +package com.votogether.global.exception; + +public class ImageException extends BaseException { + + public ImageException(final ExceptionType exceptionType) { + super(exceptionType); + } + +} diff --git a/backend/src/main/java/com/votogether/global/util/ImageUploader.java b/backend/src/main/java/com/votogether/global/util/ImageUploader.java deleted file mode 100644 index a381b648f..000000000 --- a/backend/src/main/java/com/votogether/global/util/ImageUploader.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.votogether.global.util; - -import com.votogether.domain.post.exception.PostExceptionType; -import com.votogether.global.exception.BadRequestException; -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.time.LocalDateTime; -import java.time.ZoneOffset; -import lombok.AccessLevel; -import lombok.NoArgsConstructor; -import org.springframework.web.multipart.MultipartFile; - -@NoArgsConstructor(access = AccessLevel.PRIVATE) -public class ImageUploader { - - public static String upload(final MultipartFile image) { - if (image.getOriginalFilename().contains("없는사진")) { - return ""; - } - - final long milli = LocalDateTime.now().toInstant(ZoneOffset.UTC).toEpochMilli(); - - final String rootPath = new File("").getAbsolutePath() + File.separator; - final String imageDirPath = "static" + File.separator + "images" + File.separator; - final String imageFileName = milli + "_" + image.getOriginalFilename(); - - try { - File imageFolder = new File(rootPath + imageDirPath); - if (!imageFolder.exists()) { - imageFolder.mkdirs(); // Creates the directory if it does not exist - } - - Files.write(Paths.get(rootPath + imageDirPath + imageFileName), image.getBytes()); - } catch (IOException ignore) { - System.out.println("ImageUploader.upload"); - System.out.println("imageUrl = " + imageDirPath + imageFileName); - ignore.printStackTrace(); - throw new BadRequestException(PostExceptionType.WRONG_IMAGE); - } - - return imageDirPath + imageFileName; - } - -} diff --git a/backend/src/main/java/com/votogether/infra/image/ImageExceptionType.java b/backend/src/main/java/com/votogether/infra/image/ImageExceptionType.java new file mode 100644 index 000000000..d7beafd5b --- /dev/null +++ b/backend/src/main/java/com/votogether/infra/image/ImageExceptionType.java @@ -0,0 +1,25 @@ +package com.votogether.infra.image; + +import com.votogether.global.exception.ExceptionType; +import lombok.Getter; + +@Getter +public enum ImageExceptionType implements ExceptionType { + + INVALID_IMAGE_READ(1000, "이미지를 정상적으로 읽을 수 없습니다."), + IMAGE_NAME_BLANK(1001, "원본 이미지명이 존재하지 않습니다."), + IMAGE_FORMAT(1002, "이미지 확장자가 존재하지 않습니다."), + IMAGE_EXTENSION(1003, "이미지 파일만 업로드할 수 있습니다."), + IMAGE_TRANSFER(1004, "이미지 업로드에 실패했습니다."), + IMAGE_URL(1005, "이미지 URL을 확인할 수 없습니다."), + ; + + private final int code; + private final String message; + + ImageExceptionType(final int code, final String message) { + this.code = code; + this.message = message; + } + +} diff --git a/backend/src/main/java/com/votogether/infra/image/ImageName.java b/backend/src/main/java/com/votogether/infra/image/ImageName.java new file mode 100644 index 000000000..8e6c47b27 --- /dev/null +++ b/backend/src/main/java/com/votogether/infra/image/ImageName.java @@ -0,0 +1,40 @@ +package com.votogether.infra.image; + +import com.votogether.global.exception.ImageException; +import java.util.Set; +import java.util.UUID; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.springframework.util.StringUtils; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class ImageName { + + private static final String DOT = "."; + private static final String UNDER_BAR = "_"; + private static final Set IMAGE_EXTENSIONS = Set.of("jpeg", "jpg", "png", "webp"); + + public static String from(final String originalFilename) { + final String extension = StringUtils.getFilenameExtension(originalFilename); + validateFileName(originalFilename); + validateExtension(extension); + final String fileBaseName = UUID.randomUUID().toString().substring(0, 8); + return fileBaseName + UNDER_BAR + System.currentTimeMillis() + DOT + extension; + } + + private static void validateFileName(final String fileName) { + if (fileName == null || fileName.isBlank()) { + throw new ImageException(ImageExceptionType.IMAGE_NAME_BLANK); + } + } + + private static void validateExtension(final String extension) { + if (extension == null) { + throw new ImageException(ImageExceptionType.IMAGE_FORMAT); + } + if (!IMAGE_EXTENSIONS.contains(extension.toLowerCase())) { + throw new ImageException(ImageExceptionType.IMAGE_EXTENSION); + } + } + +} diff --git a/backend/src/main/java/com/votogether/infra/image/ImageUploader.java b/backend/src/main/java/com/votogether/infra/image/ImageUploader.java new file mode 100644 index 000000000..35bdff2d4 --- /dev/null +++ b/backend/src/main/java/com/votogether/infra/image/ImageUploader.java @@ -0,0 +1,11 @@ +package com.votogether.infra.image; + +import org.springframework.web.multipart.MultipartFile; + +public interface ImageUploader { + + String upload(final MultipartFile image); + + void delete(final String path); + +} diff --git a/backend/src/main/java/com/votogether/infra/image/LocalUploader.java b/backend/src/main/java/com/votogether/infra/image/LocalUploader.java new file mode 100644 index 000000000..7b192e34f --- /dev/null +++ b/backend/src/main/java/com/votogether/infra/image/LocalUploader.java @@ -0,0 +1,103 @@ +package com.votogether.infra.image; + +import com.votogether.global.exception.ImageException; +import java.io.BufferedInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import javax.imageio.ImageIO; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +@Component +public class LocalUploader implements ImageUploader { + + private static final String SLASH = File.separator; + private static final String SYSTEM_PATH = System.getProperty("user.dir"); + + private final String url; + private final String uploadDirectory; + + public LocalUploader( + @Value("${image.upload_url}") final String url, + @Value("${image.upload_directory}") final String uploadDirectory + ) { + this.url = url; + this.uploadDirectory = uploadDirectory; + } + + public String upload(final MultipartFile image) { + final File directory = loadDirectory(getImageStorePath()); + + if (isEmptyFileOrNotImage(image)) { + return null; + } + final String saveFileName = ImageName.from(image.getOriginalFilename()); + final File uploadPath = new File(directory, saveFileName); + transferFile(image, uploadPath); + return getImageFullPath(saveFileName); + } + + private String getImageStorePath() { + return SYSTEM_PATH + SLASH + uploadDirectory; + } + + private File loadDirectory(final String directoryLocation) { + final File directory = new File(directoryLocation); + if (!directory.exists()) { + directory.mkdirs(); + } + return directory; + } + + private boolean isEmptyFileOrNotImage(final MultipartFile multipartFile) { + return multipartFile == null || multipartFile.isEmpty() || isNotImageFile(multipartFile); + } + + private boolean isNotImageFile(final MultipartFile file) { + try (InputStream originalInputStream = new BufferedInputStream(file.getInputStream())) { + return ImageIO.read(originalInputStream) == null; + } catch (IOException e) { + throw new ImageException(ImageExceptionType.INVALID_IMAGE_READ); + } + } + + private void transferFile(final MultipartFile file, final File uploadPath) { + try { + file.transferTo(uploadPath); + } catch (IOException e) { + throw new ImageException(ImageExceptionType.IMAGE_TRANSFER); + } + } + + private String getImageFullPath(final String fileName) { + return url + SLASH + uploadDirectory + SLASH + fileName; + } + + public void delete(final String path) { + if (path == null || path.isEmpty()) { + return; + } + final String deletePath = getImageLocalPath(path); + final File file = new File(deletePath); + deleteFile(file); + } + + private String getImageLocalPath(final String fullPath) { + final int urlIndex = fullPath.lastIndexOf(url); + + if (urlIndex == -1) { + throw new ImageException(ImageExceptionType.IMAGE_URL); + } + final int urlNextIndex = urlIndex + url.length(); + return SYSTEM_PATH + fullPath.substring(urlNextIndex); + } + + private void deleteFile(final File file) { + if (file.exists()) { + file.delete(); + } + } + +} diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 7cd6afcc3..854cc0627 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -41,6 +41,10 @@ server: forward-headers-strategy: FRAMEWORK tomcat: max-http-form-post-size: 35MB + accept-count: ${ACCEPT_COUNT} + max-connections: ${MAX_CONNECTIONS} + threads: + max: ${THREADS_MAX} springdoc: swagger-ui: @@ -63,3 +67,7 @@ jwt: secret-key: ${SECRET_KEY} access-expiration-time: ${ACCESS_EXPIRATION_TIME} refresh-expiration-time: ${REFRESH_EXPIRATION_TIME} + +image: + upload_url: ${IMAGE_UPLOAD_URL} + upload_directory: ${IMAGE_UPLOAD_DIRECTORY} diff --git a/backend/src/main/resources/log4j2-dev.xml b/backend/src/main/resources/log4j2-dev.xml index d80bc2e55..617045c18 100644 --- a/backend/src/main/resources/log4j2-dev.xml +++ b/backend/src/main/resources/log4j2-dev.xml @@ -34,14 +34,6 @@ - - - - - - - - diff --git a/backend/src/main/resources/log4j2-local.xml b/backend/src/main/resources/log4j2-local.xml index 9f6d08bfa..a29bf1892 100644 --- a/backend/src/main/resources/log4j2-local.xml +++ b/backend/src/main/resources/log4j2-local.xml @@ -21,14 +21,6 @@ - - - - - - - - diff --git a/backend/src/test/java/com/votogether/domain/member/service/MemberServiceTest.java b/backend/src/test/java/com/votogether/domain/member/service/MemberServiceTest.java index d3507d63a..608b90518 100644 --- a/backend/src/test/java/com/votogether/domain/member/service/MemberServiceTest.java +++ b/backend/src/test/java/com/votogether/domain/member/service/MemberServiceTest.java @@ -14,7 +14,6 @@ import com.votogether.domain.member.repository.MemberCategoryRepository; import com.votogether.domain.member.repository.MemberRepository; import com.votogether.domain.post.entity.Post; -import com.votogether.domain.post.entity.PostBody; import com.votogether.domain.post.entity.comment.Comment; import com.votogether.domain.post.repository.CommentRepository; import com.votogether.domain.post.repository.PostRepository; @@ -24,9 +23,7 @@ import com.votogether.global.exception.BadRequestException; import com.votogether.test.annotation.ServiceTest; import com.votogether.test.fixtures.MemberFixtures; -import com.votogether.test.persister.MemberTestPersister; import com.votogether.test.persister.PostTestPersister; -import com.votogether.test.persister.VoteTestPersister; import jakarta.persistence.EntityManager; import java.time.LocalDateTime; import java.time.temporal.ChronoUnit; @@ -257,14 +254,10 @@ void successWithPost() { // given Member member = memberRepository.save(MemberFixtures.MALE_20.get()); - PostBody postBody = PostBody.builder() - .title("title") - .content("content") - .build(); - Post post = Post.builder() .writer(member) - .postBody(postBody) + .title("title") + .content("content") .deadline(LocalDateTime.now().plusDays(3L).truncatedTo(ChronoUnit.MINUTES)) .build(); @@ -287,14 +280,10 @@ void deleteOnlyMember() { Member member = memberRepository.save(MemberFixtures.MALE_20.get()); Member writer = memberRepository.save(MemberFixtures.FEMALE_20.get()); - PostBody postBody = PostBody.builder() - .title("title") - .content("content") - .build(); - Post post = Post.builder() .writer(writer) - .postBody(postBody) + .title("title") + .content("content") .deadline(LocalDateTime.now().plusDays(3L).truncatedTo(ChronoUnit.MINUTES)) .build(); @@ -306,7 +295,7 @@ void deleteOnlyMember() { // then assertAll( () -> assertThat(memberRepository.findAll()).hasSize(1), - () -> assertThat(memberRepository.findById(writer.getId()).get()).isEqualTo(writer), + () -> assertThat(memberRepository.findById(writer.getId())).contains(writer), () -> assertThat(postRepository.findAll()).hasSize(1) ); } @@ -372,14 +361,10 @@ void deleteWithReportedPost() { Member member = memberRepository.save(MemberFixtures.MALE_20.get()); Member reporter = memberRepository.save(MemberFixtures.MALE_10.get()); - PostBody postBody = PostBody.builder() - .title("title") - .content("content") - .build(); - Post post = Post.builder() .writer(member) - .postBody(postBody) + .title("title") + .content("content") .deadline(LocalDateTime.now().plusDays(3L).truncatedTo(ChronoUnit.MINUTES)) .build(); @@ -411,20 +396,16 @@ void deleteWithReportedComment() { Member member = memberRepository.save(MemberFixtures.MALE_20.get()); Member reporter = memberRepository.save(MemberFixtures.MALE_10.get()); - PostBody postBody = PostBody.builder() - .title("title") - .content("content") - .build(); - Post post = Post.builder() .writer(reporter) - .postBody(postBody) + .title("title") + .content("content") .deadline(LocalDateTime.now().plusDays(3L).truncatedTo(ChronoUnit.MINUTES)) .build(); Comment comment = Comment.builder() .post(post) - .member(member) + .writer(member) .content("댓글입니다.") .build(); diff --git a/backend/src/test/java/com/votogether/domain/post/controller/PostCommandControllerTest.java b/backend/src/test/java/com/votogether/domain/post/controller/PostCommandControllerTest.java new file mode 100644 index 000000000..873389f94 --- /dev/null +++ b/backend/src/test/java/com/votogether/domain/post/controller/PostCommandControllerTest.java @@ -0,0 +1,435 @@ +package com.votogether.domain.post.controller; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willDoNothing; + +import com.votogether.domain.member.entity.Member; +import com.votogether.domain.post.dto.request.post.PostCreateRequest; +import com.votogether.domain.post.dto.request.post.PostUpdateRequest; +import com.votogether.domain.post.service.PostCommandService; +import com.votogether.global.exception.GlobalExceptionHandler; +import com.votogether.test.ControllerTest; +import io.restassured.http.ContentType; +import io.restassured.module.mockmvc.RestAssuredMockMvc; +import java.io.File; +import java.time.LocalDateTime; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +@WebMvcTest(PostCommandController.class) +class PostCommandControllerTest extends ControllerTest { + + @MockBean + PostCommandService postCommandService; + + @BeforeEach + void setUp(WebApplicationContext webApplicationContext) { + RestAssuredMockMvc.standaloneSetup( + MockMvcBuilders + .standaloneSetup(new PostCommandController(postCommandService)) + .setControllerAdvice(GlobalExceptionHandler.class) + ); + RestAssuredMockMvc.webAppContextSetup(webApplicationContext); + } + + @Nested + @DisplayName("게시글 작성") + class CreatePost { + + @Test + @DisplayName("게시글 작성에 성공하면 201 응답을 반환한다.") + void return201success() throws Exception { + // given + mockingAuthArgumentResolver(); + given(postCommandService.createPost(any(PostCreateRequest.class), any(Member.class))).willReturn(1L); + String filePath = "src/test/resources/images/"; + String fileName = "testImage1.PNG"; + File file = new File(filePath + fileName); + + // when, then + RestAssuredMockMvc.given().log().all() + .headers(HttpHeaders.AUTHORIZATION, BEARER_TOKEN) + .contentType(ContentType.MULTIPART) + .param("categoryIds", List.of(1L, 2L)) + .param("title", "title") + .param("content", "content") + .multiPart("contentImage", file) + .param("postOptions[0].content", "Option Content") + .multiPart("postOptions[0].optionImage", file) + .param("deadline", LocalDateTime.now().toString()) + .when().post("/posts") + .then().log().all() + .status(HttpStatus.CREATED) + .header("Location", "/posts/1"); + } + + @Test + @DisplayName("카테고리 ID가 존재하지 않으면 400 응답을 반환한다.") + void return400nullCategory() throws Exception { + // given + mockingAuthArgumentResolver(); + + // when, then + RestAssuredMockMvc.given().log().all() + .headers(HttpHeaders.AUTHORIZATION, BEARER_TOKEN) + .contentType(ContentType.MULTIPART) + .param("title", "title") + .param("content", "content") + .param("postOptions[0].content", "Option Content") + .param("deadline", LocalDateTime.now().toString()) + .when().post("/posts") + .then().log().all() + .status(HttpStatus.BAD_REQUEST) + .body("code", equalTo(200)) + .body("message", containsString("게시글 카테고리 ID가 등록되지 않았습니다.")); + } + + @ParameterizedTest + @ValueSource(strings = {"", " "}) + @DisplayName("게시글 제목이 존재하지 않거나 공백이라면 400 응답을 반환한다.") + void emptyTitle(String title) throws Exception { + // given + mockingAuthArgumentResolver(); + + // when, then + RestAssuredMockMvc.given().log().all() + .headers(HttpHeaders.AUTHORIZATION, BEARER_TOKEN) + .contentType(ContentType.MULTIPART) + .param("categoryIds", List.of(1L)) + .param("title", title) + .param("content", "content") + .param("postOptions[0].content", "Option Content") + .param("deadline", LocalDateTime.now().toString()) + .when().post("/posts") + .then().log().all() + .status(HttpStatus.BAD_REQUEST) + .body("code", equalTo(200)) + .body("message", containsString("게시글 제목이 존재하지 않거나 공백만 존재합니다.")); + } + + @ParameterizedTest + @ValueSource(strings = {"", " "}) + @DisplayName("게시글 내용이 존재하지 않거나 공백이라면 400 응답을 반환한다.") + void emptyContent(String content) throws Exception { + // given + mockingAuthArgumentResolver(); + + // when, then + RestAssuredMockMvc.given().log().all() + .headers(HttpHeaders.AUTHORIZATION, BEARER_TOKEN) + .contentType(ContentType.MULTIPART) + .param("categoryIds", List.of(1L)) + .param("title", "title") + .param("content", content) + .param("postOptions[0].content", "Option Content") + .param("deadline", LocalDateTime.now().toString()) + .when().post("/posts") + .then().log().all() + .status(HttpStatus.BAD_REQUEST) + .body("code", equalTo(200)) + .body("message", containsString("게시글 내용이 존재하지 않거나 공백만 존재합니다.")); + } + + @Test + @DisplayName("게시글 마감 시간이 등록되지 않았으면 400 응답을 반환한다.") + void emptyDeadline() throws Exception { + // given + mockingAuthArgumentResolver(); + + // when, then + RestAssuredMockMvc.given().log().all() + .headers(HttpHeaders.AUTHORIZATION, BEARER_TOKEN) + .contentType(ContentType.MULTIPART) + .param("categoryIds", List.of(1L)) + .param("title", "title") + .param("content", "content") + .param("postOptions[0].content", "Option Content") + .when().post("/posts") + .then().log().all() + .status(HttpStatus.BAD_REQUEST) + .body("code", equalTo(200)) + .body("message", containsString("게시글 마감시간을 등록하지 않았습니다.")); + } + + @ParameterizedTest + @ValueSource(strings = {"", " "}) + @DisplayName("게시글 옵션 내용이 존재하지 않거나 공백이라면 400 응답을 반환한다.") + void emptyOptionContent(String optionContent) throws Exception { + // given + mockingAuthArgumentResolver(); + + // when, then + RestAssuredMockMvc.given().log().all() + .headers(HttpHeaders.AUTHORIZATION, BEARER_TOKEN) + .contentType(ContentType.MULTIPART) + .param("categoryIds", List.of(1L)) + .param("title", "title") + .param("content", "content") + .param("postOptions[0].content", optionContent) + .param("deadline", LocalDateTime.now().toString()) + .when().post("/posts") + .then().log().all() + .status(HttpStatus.BAD_REQUEST) + .body("code", equalTo(200)) + .body("message", containsString("게시글 옵션 내용이 존재하지 않거나 공백만 존재합니다.")); + } + + } + + @Nested + @DisplayName("게시글 수정") + class UpdatePost { + + @Test + @DisplayName("게시글 수정에 성공하면 200 응답을 반환한다.") + void return200success() throws Exception { + // given + mockingAuthArgumentResolver(); + willDoNothing().given(postCommandService) + .updatePost(anyLong(), any(PostUpdateRequest.class), any(Member.class)); + String filePath = "src/test/resources/images/"; + String fileName = "testImage1.PNG"; + File file = new File(filePath + fileName); + + // when, then + RestAssuredMockMvc.given().log().all() + .headers(HttpHeaders.AUTHORIZATION, BEARER_TOKEN) + .contentType(MediaType.MULTIPART_FORM_DATA) + .param("categoryIds", List.of(1L, 2L)) + .param("title", "title") + .param("content", "content") + .multiPart("contentImage", file) + .param("postOptions[0].content", "Option Content") + .multiPart("postOptions[0].optionImage", file) + .param("deadline", LocalDateTime.now().toString()) + .when().put("/posts/1") + .then().log().all() + .status(HttpStatus.OK); + } + + @Test + @DisplayName("게시글 ID가 양의 정수가 아니라면 400 응답을 반환한다.") + void return400postIdNegative() throws Exception { + // given + mockingAuthArgumentResolver(); + + // when, then + RestAssuredMockMvc.given().log().all() + .headers(HttpHeaders.AUTHORIZATION, BEARER_TOKEN) + .contentType(MediaType.MULTIPART_FORM_DATA) + .param("categoryIds", List.of(1L, 2L)) + .param("title", "title") + .param("content", "content") + .param("postOptions[0].content", "Option Content") + .param("deadline", LocalDateTime.now().toString()) + .when().put("/posts/-1") + .then().log().all() + .status(HttpStatus.BAD_REQUEST) + .body("code", equalTo(201)) + .body("message", containsString("게시글 ID는 양의 정수만 가능합니다.")); + } + + @Test + @DisplayName("카테고리 ID가 존재하지 않으면 400 응답을 반환한다.") + void return400EmptyCategory() throws Exception { + // given + mockingAuthArgumentResolver(); + + // when, then + RestAssuredMockMvc.given().log().all() + .headers(HttpHeaders.AUTHORIZATION, BEARER_TOKEN) + .contentType(MediaType.MULTIPART_FORM_DATA) + .param("title", "title") + .param("content", "content") + .param("postOptions[0].content", "Option Content") + .param("deadline", LocalDateTime.now().toString()) + .when().put("/posts/1") + .then().log().all() + .status(HttpStatus.BAD_REQUEST) + .body("code", equalTo(200)) + .body("message", containsString("게시글 카테고리 ID가 등록되지 않았습니다.")); + } + + @ParameterizedTest + @ValueSource(strings = {"", " "}) + @DisplayName("게시글 제목이 존재하지 않거나 공백이라면 400 응답을 반환한다.") + void emptyTitle(String title) throws Exception { + // given + mockingAuthArgumentResolver(); + + // when, then + RestAssuredMockMvc.given().log().all() + .headers(HttpHeaders.AUTHORIZATION, BEARER_TOKEN) + .contentType(MediaType.MULTIPART_FORM_DATA) + .param("categoryIds", List.of(1L, 2L)) + .param("title", title) + .param("content", "content") + .param("postOptions[0].content", "Option Content") + .param("deadline", LocalDateTime.now().toString()) + .when().put("/posts/1") + .then().log().all() + .status(HttpStatus.BAD_REQUEST) + .body("code", equalTo(200)) + .body("message", containsString("게시글 제목이 존재하지 않거나 공백만 존재합니다.")); + } + + @ParameterizedTest + @ValueSource(strings = {"", " "}) + @DisplayName("게시글 내용이 존재하지 않거나 공백이라면 400 응답을 반환한다.") + void emptyContent(String content) throws Exception { + // given + mockingAuthArgumentResolver(); + + // when, then + RestAssuredMockMvc.given().log().all() + .headers(HttpHeaders.AUTHORIZATION, BEARER_TOKEN) + .contentType(MediaType.MULTIPART_FORM_DATA) + .param("categoryIds", List.of(1L, 2L)) + .param("title", "title") + .param("content", content) + .param("postOptions[0].content", "Option Content") + .param("deadline", LocalDateTime.now().toString()) + .when().put("/posts/1") + .then().log().all() + .status(HttpStatus.BAD_REQUEST) + .body("code", equalTo(200)) + .body("message", containsString("게시글 내용이 존재하지 않거나 공백만 존재합니다.")); + } + + @Test + @DisplayName("게시글 마감 시간이 등록되지 않았으면 400 응답을 반환한다.") + void emptyDeadline() throws Exception { + // given + mockingAuthArgumentResolver(); + + // when, then + RestAssuredMockMvc.given().log().all() + .headers(HttpHeaders.AUTHORIZATION, BEARER_TOKEN) + .contentType(MediaType.MULTIPART_FORM_DATA) + .param("categoryIds", List.of(1L, 2L)) + .param("title", "title") + .param("content", "content") + .param("postOptions[0].content", "Option Content") + .when().put("/posts/1") + .then().log().all() + .status(HttpStatus.BAD_REQUEST) + .body("code", equalTo(200)) + .body("message", containsString("게시글 마감시간을 등록하지 않았습니다.")); + } + + @ParameterizedTest + @ValueSource(strings = {"", " "}) + @DisplayName("게시글 옵션 내용이 존재하지 않거나 공백이라면 400 응답을 반환한다.") + void emptyOptionContent(String optionContent) throws Exception { + // given + mockingAuthArgumentResolver(); + + // when, then + RestAssuredMockMvc.given().log().all() + .headers(HttpHeaders.AUTHORIZATION, BEARER_TOKEN) + .contentType(MediaType.MULTIPART_FORM_DATA) + .param("categoryIds", List.of(1L, 2L)) + .param("title", "title") + .param("content", "content") + .param("postOptions[0].content", optionContent) + .param("deadline", LocalDateTime.now().toString()) + .when().put("/posts/1") + .then().log().all() + .status(HttpStatus.BAD_REQUEST) + .body("code", equalTo(200)) + .body("message", containsString("게시글 옵션 내용이 존재하지 않거나 공백만 존재합니다.")); + } + + } + + @Nested + @DisplayName("게시글 조기 마감") + class ClosePostEarly { + + @Test + @DisplayName("게시글 조기 마감에 성공하면 200 응답을 반환한다.") + void return200success() throws Exception { + // given + mockingAuthArgumentResolver(); + willDoNothing().given(postCommandService).closePostEarly(anyLong(), any(Member.class)); + + // when, then + RestAssuredMockMvc.given().log().all() + .headers(HttpHeaders.AUTHORIZATION, BEARER_TOKEN) + .when().patch("/posts/{postId}/close", 1) + .then().log().all() + .status(HttpStatus.OK); + } + + @Test + @DisplayName("게시글 ID가 양의 정수가 아니라면 400 응답을 반환한다.") + void return400postIdNegative() throws Exception { + // given + mockingAuthArgumentResolver(); + + // when, then + RestAssuredMockMvc.given().log().all() + .headers(HttpHeaders.AUTHORIZATION, BEARER_TOKEN) + .when().patch("/posts/{postId}/close", -1) + .then().log().all() + .status(HttpStatus.BAD_REQUEST) + .body("code", equalTo(201)) + .body("message", containsString("게시글 ID는 양의 정수만 가능합니다.")); + } + + } + + @Nested + @DisplayName("게시글 삭제") + class DeletePost { + + @Test + @DisplayName("게시글 삭제에 성공하면 204 응답을 반환한다.") + void return204success() throws Exception { + // given + mockingAuthArgumentResolver(); + willDoNothing().given(postCommandService).deletePost(anyLong(), any(Member.class)); + + // when, then + RestAssuredMockMvc.given().log().all() + .headers(HttpHeaders.AUTHORIZATION, BEARER_TOKEN) + .when().delete("/posts/{postId}", 1) + .then().log().all() + .status(HttpStatus.NO_CONTENT); + } + + @Test + @DisplayName("게시글 ID가 양의 정수가 아니라면 400 응답을 반환한다.") + void return400postIdNegative() throws Exception { + // given + mockingAuthArgumentResolver(); + + // when, then + RestAssuredMockMvc.given().log().all() + .headers(HttpHeaders.AUTHORIZATION, BEARER_TOKEN) + .when().delete("/posts/{postId}", -1) + .then().log().all() + .status(HttpStatus.BAD_REQUEST) + .body("code", equalTo(201)) + .body("message", containsString("게시글 ID는 양의 정수만 가능합니다.")); + } + + } + +} diff --git a/backend/src/test/java/com/votogether/domain/post/controller/PostCommentControllerTest.java b/backend/src/test/java/com/votogether/domain/post/controller/PostCommentControllerTest.java index 3d51bb363..222c70b84 100644 --- a/backend/src/test/java/com/votogether/domain/post/controller/PostCommentControllerTest.java +++ b/backend/src/test/java/com/votogether/domain/post/controller/PostCommentControllerTest.java @@ -5,22 +5,17 @@ import static org.hamcrest.Matchers.equalTo; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.willDoNothing; import com.votogether.domain.member.entity.Member; -import com.votogether.domain.member.service.MemberService; -import com.votogether.domain.post.dto.request.comment.CommentRegisterRequest; +import com.votogether.domain.post.dto.request.comment.CommentCreateRequest; import com.votogether.domain.post.dto.request.comment.CommentUpdateRequest; import com.votogether.domain.post.dto.response.comment.CommentResponse; -import com.votogether.domain.post.entity.comment.Comment; +import com.votogether.domain.post.dto.response.comment.CommentWriterResponse; import com.votogether.domain.post.service.PostCommentService; import com.votogether.global.exception.GlobalExceptionHandler; -import com.votogether.global.jwt.JwtAuthenticationFilter; -import com.votogether.global.jwt.TokenPayload; -import com.votogether.global.jwt.TokenProcessor; -import com.votogether.test.fixtures.MemberFixtures; +import com.votogether.test.ControllerTest; import io.restassured.common.mapper.TypeRef; import io.restassured.module.mockmvc.RestAssuredMockMvc; import java.time.LocalDateTime; @@ -37,172 +32,178 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import org.springframework.test.util.ReflectionTestUtils; import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; @WebMvcTest(PostCommentController.class) -class PostCommentControllerTest { +class PostCommentControllerTest extends ControllerTest { @MockBean PostCommentService postCommentService; - @MockBean - MemberService memberService; - - @MockBean - TokenProcessor tokenProcessor; - @BeforeEach - void setUp() { + void setUp(WebApplicationContext webApplicationContext) { RestAssuredMockMvc.standaloneSetup( MockMvcBuilders .standaloneSetup(new PostCommentController(postCommentService)) .setControllerAdvice(GlobalExceptionHandler.class) - .addFilters(new JwtAuthenticationFilter(tokenProcessor)) ); + RestAssuredMockMvc.webAppContextSetup(webApplicationContext); } @Nested - @DisplayName("게시글 댓글 등록 시 ") - class CreateComment { + @DisplayName("게시글 댓글 목록 조회") + class GetComments { - @ParameterizedTest - @ValueSource(strings = {"@", "a", "가"}) - @DisplayName("ID로 변환할 수 없는 타입이라면 400을 응답한다.") - void invalidIDType(String id) throws Exception { + @Test + @DisplayName("정상적인 요청이라면 게시글 댓글 목록과 200 응답을 반환한다.") + void getComments() { // given - TokenPayload tokenPayload = new TokenPayload(1L, 1L, 1L); - given(tokenProcessor.resolveToken(anyString())).willReturn("token"); - given(tokenProcessor.parseToken(anyString())).willReturn(tokenPayload); - given(memberService.findById(anyLong())).willReturn(MemberFixtures.MALE_20.get()); + List response = List.of( + new CommentResponse( + 1L, + new CommentWriterResponse(1L, "votogetherA"), + "helloA", + LocalDateTime.now(), + LocalDateTime.now() + ), + new CommentResponse( + 2L, + new CommentWriterResponse(2L, "votogetherB"), + "helloB", + LocalDateTime.now(), + LocalDateTime.now() + ) + ); + given(postCommentService.getComments(anyLong())).willReturn(response); - // when, then + // when + List result = RestAssuredMockMvc.given().log().all() + .headers(HttpHeaders.AUTHORIZATION, BEARER_TOKEN) + .when().get("/posts/{postId}/comments", 1L) + .then().log().all() + .status(HttpStatus.OK) + .extract() + .as(new TypeRef<>() { + }); + + // then + assertThat(result).usingRecursiveComparison() + .ignoringFieldsOfTypes(LocalDateTime.class) + .isEqualTo(response); + } + + @ParameterizedTest + @ValueSource(strings = {"@", "a", "가"}) + @DisplayName("게시글 ID가 Long 타입으로 변환할 수 없는 값이라면 400 응답을 반환한다.") + void invalidPostIDType(String postId) { + // given, when, then RestAssuredMockMvc.given().log().all() - .headers(HttpHeaders.AUTHORIZATION, "Bearer token") - .when().post("/posts/{postId}/comments", id) + .headers(HttpHeaders.AUTHORIZATION, BEARER_TOKEN) + .when().get("/posts/{postId}/comments", postId) .then().log().all() .status(HttpStatus.BAD_REQUEST) - .body("code", equalTo(-9998)) - .body("message", equalTo("postId는 Long 타입이 필요합니다.")); + .body("code", equalTo(202)) + .body("message", containsString("Long 타입으로 변환할 수 없는 요청입니다.")); } @ParameterizedTest - @NullAndEmptySource - @DisplayName("댓글 내용이 존재하지 않으면 400을 응답한다.") - void emptyContent(String content) throws Exception { - // given - TokenPayload tokenPayload = new TokenPayload(1L, 1L, 1L); - given(tokenProcessor.resolveToken(anyString())).willReturn("token"); - given(tokenProcessor.parseToken(anyString())).willReturn(tokenPayload); - given(memberService.findById(anyLong())).willReturn(MemberFixtures.MALE_20.get()); - - CommentRegisterRequest commentRegisterRequest = new CommentRegisterRequest(content); - - // when, then + @ValueSource(longs = {-1L, 0}) + @DisplayName("게시글 ID가 양의 정수가 아니라면 400 응답을 반환한다.") + void notPositivePostId(Long postId) { + // given, when, then RestAssuredMockMvc.given().log().all() - .headers(HttpHeaders.AUTHORIZATION, "Bearer token") - .contentType(MediaType.APPLICATION_JSON) - .body(commentRegisterRequest) - .when().post("/posts/{postId}/comments", 1) + .headers(HttpHeaders.AUTHORIZATION, BEARER_TOKEN) + .when().get("/posts/{postId}/comments", postId) .then().log().all() .status(HttpStatus.BAD_REQUEST) - .body("code", equalTo(-9997)) - .body("message", containsString("댓글 내용은 존재해야 합니다.")); + .body("code", equalTo(201)) + .body("message", containsString("게시글 ID는 양의 정수만 가능합니다.")); } + } + + @Nested + @DisplayName("게시글 댓글 등록 시 ") + class CreateComment { + @Test - @DisplayName("댓글을 정상적으로 등록하면 201을 응답한다.") + @DisplayName("정상적인 요청이라면 댓글을 등록하고 201 응답을 반환한다.") void createComment() throws Exception { // given - TokenPayload tokenPayload = new TokenPayload(1L, 1L, 1L); - given(tokenProcessor.resolveToken(anyString())).willReturn("token"); - given(tokenProcessor.parseToken(anyString())).willReturn(tokenPayload); - given(memberService.findById(anyLong())).willReturn(MemberFixtures.MALE_20.get()); - - CommentRegisterRequest commentRegisterRequest = new CommentRegisterRequest("댓글입니다."); + mockingAuthArgumentResolver(); + CommentCreateRequest commentCreateRequest = new CommentCreateRequest("댓글입니다."); willDoNothing().given(postCommentService) - .createComment(any(Member.class), anyLong(), any(CommentRegisterRequest.class)); + .createComment(anyLong(), any(CommentCreateRequest.class), any(Member.class)); // when, then RestAssuredMockMvc.given().log().all() - .headers(HttpHeaders.AUTHORIZATION, "Bearer token") + .headers(HttpHeaders.AUTHORIZATION, BEARER_TOKEN) .contentType(MediaType.APPLICATION_JSON) - .body(commentRegisterRequest) + .body(commentCreateRequest) .when().post("/posts/{postId}/comments", 1) .then().log().all() .status(HttpStatus.CREATED); } - } - - @Nested - @DisplayName("게시글 댓글 목록 조회") - class GetComments { - @ParameterizedTest @ValueSource(strings = {"@", "a", "가"}) - @DisplayName("게시글 ID가 Long 타입으로 변환할 수 없는 값이라면 400을 응답한다.") - void invalidPostIDType(String postId) throws Exception { + @DisplayName("게시글 ID가 Long 타입으로 변환할 수 없는 값이라면 400 응답을 반환한다.") + void invalidIDType(String postId) throws Exception { // given - TokenPayload tokenPayload = new TokenPayload(1L, 1L, 1L); - given(tokenProcessor.resolveToken(anyString())).willReturn("token"); - given(tokenProcessor.parseToken(anyString())).willReturn(tokenPayload); - given(memberService.findById(anyLong())).willReturn(MemberFixtures.MALE_20.get()); + mockingAuthArgumentResolver(); + CommentCreateRequest commentCreateRequest = new CommentCreateRequest("hello"); // when, then RestAssuredMockMvc.given().log().all() - .headers(HttpHeaders.AUTHORIZATION, "Bearer token") - .when().get("/posts/{postId}/comments", postId) + .headers(HttpHeaders.AUTHORIZATION, BEARER_TOKEN) + .contentType(MediaType.APPLICATION_JSON) + .body(commentCreateRequest) + .when().post("/posts/{postId}/comments", postId) .then().log().all() .status(HttpStatus.BAD_REQUEST) - .body("code", equalTo(-9998)) - .body("message", containsString("postId는 Long 타입이 필요합니다.")); + .body("code", equalTo(202)) + .body("message", containsString("Long 타입으로 변환할 수 없는 요청입니다.")); } - @Test - @DisplayName("정상적인 요청이라면 게시글 댓글 목록을 조회한다.") - void getComments() throws Exception { + @ParameterizedTest + @ValueSource(longs = {-1L, 0}) + @DisplayName("게시글 ID가 양의 정수가 아니라면 400 응답을 반환한다.") + void notPositivePostId(Long postId) throws Exception { // given - TokenPayload tokenPayload = new TokenPayload(1L, 1L, 1L); - given(tokenProcessor.resolveToken(anyString())).willReturn("token"); - given(tokenProcessor.parseToken(anyString())).willReturn(tokenPayload); - given(memberService.findById(anyLong())).willReturn(MemberFixtures.MALE_20.get()); - - Member memberA = MemberFixtures.MALE_20.get(); - Member memberB = MemberFixtures.FEMALE_20.get(); - ReflectionTestUtils.setField(memberA, "id", 1L); - ReflectionTestUtils.setField(memberB, "id", 2L); - - Comment commentA = Comment.builder() - .member(memberA) - .content("commentA") - .build(); - Comment commentB = Comment.builder() - .member(memberB) - .content("commentA") - .build(); - LocalDateTime now = LocalDateTime.now(); - ReflectionTestUtils.setField(commentA, "createdAt", now); - ReflectionTestUtils.setField(commentB, "createdAt", now); - - CommentResponse commentResponseA = CommentResponse.from(commentA); - CommentResponse commentResponseB = CommentResponse.from(commentB); - given(postCommentService.getComments(anyLong())).willReturn(List.of(commentResponseA, commentResponseB)); + mockingAuthArgumentResolver(); + CommentCreateRequest commentCreateRequest = new CommentCreateRequest("hello"); - // when - List response = RestAssuredMockMvc.given().log().all() - //.headers(HttpHeaders.AUTHORIZATION, "Bearer token") - .when().get("/posts/{postId}/comments", 1L) + // when, then + RestAssuredMockMvc.given().log().all() + .headers(HttpHeaders.AUTHORIZATION, BEARER_TOKEN) + .contentType(MediaType.APPLICATION_JSON) + .body(commentCreateRequest) + .when().post("/posts/{postId}/comments", postId) .then().log().all() - .status(HttpStatus.OK) - .extract() - .as(new TypeRef>() { - }); + .status(HttpStatus.BAD_REQUEST) + .body("code", equalTo(201)) + .body("message", containsString("게시글 ID는 양의 정수만 가능합니다.")); + } - // then - assertThat(response).usingRecursiveComparison() - .ignoringFieldsOfTypes(LocalDateTime.class) - .isEqualTo(List.of(commentResponseA, commentResponseB)); + @ParameterizedTest + @NullAndEmptySource + @DisplayName("댓글 내용이 존재하지 않으면 400 응답을 반환한다.") + void emptyContent(String content) throws Exception { + // given + mockingAuthArgumentResolver(); + CommentCreateRequest commentCreateRequest = new CommentCreateRequest(content); + + // when, then + RestAssuredMockMvc.given().log().all() + .headers(HttpHeaders.AUTHORIZATION, BEARER_TOKEN) + .contentType(MediaType.APPLICATION_JSON) + .body(commentCreateRequest) + .when().post("/posts/{postId}/comments", 1) + .then().log().all() + .status(HttpStatus.BAD_REQUEST) + .body("code", equalTo(200)) + .body("message", containsString("댓글 내용은 존재해야 합니다.")); } } @@ -211,72 +212,122 @@ void getComments() throws Exception { @DisplayName("게시글 댓글 수정") class UpdateComment { + @Test + @DisplayName("정상적인 요청이라면 댓글을 수정하고 200 응답을 반환한다.") + void updateComment() throws Exception { + // given + mockingAuthArgumentResolver(); + CommentUpdateRequest request = new CommentUpdateRequest("hello"); + willDoNothing().given(postCommentService) + .updateComment(anyLong(), anyLong(), any(CommentUpdateRequest.class), any(Member.class)); + + // when, then + RestAssuredMockMvc.given().log().all() + .headers(HttpHeaders.AUTHORIZATION, BEARER_TOKEN) + .contentType(MediaType.APPLICATION_JSON) + .body(request) + .when().put("/posts/{postId}/comments/{commentId}", 1L, 1L) + .then().log().all() + .status(HttpStatus.OK); + } + @ParameterizedTest @ValueSource(strings = {"@", "a", "가"}) - @DisplayName("게시글 ID가 Long 타입으로 변환할 수 없는 값이라면 400을 응답한다.") + @DisplayName("게시글 ID가 Long 타입으로 변환할 수 없는 값이라면 400 응답을 반환한다.") void invalidPostIDType(String postId) throws Exception { // given - TokenPayload tokenPayload = new TokenPayload(1L, 1L, 1L); - given(tokenProcessor.resolveToken(anyString())).willReturn("token"); - given(tokenProcessor.parseToken(anyString())).willReturn(tokenPayload); - given(memberService.findById(anyLong())).willReturn(MemberFixtures.MALE_20.get()); + mockingAuthArgumentResolver(); CommentUpdateRequest request = new CommentUpdateRequest("hello"); // when, then RestAssuredMockMvc.given().log().all() - .headers(HttpHeaders.AUTHORIZATION, "Bearer token") + .headers(HttpHeaders.AUTHORIZATION, BEARER_TOKEN) .contentType(MediaType.APPLICATION_JSON) .body(request) .when().put("/posts/{postId}/comments/{commentId}", postId, 1L) .then().log().all() .status(HttpStatus.BAD_REQUEST) - .body("code", equalTo(-9998)) - .body("message", containsString("postId는 Long 타입이 필요합니다.")); + .body("code", equalTo(202)) + .body("message", containsString("Long 타입으로 변환할 수 없는 요청입니다.")); } @ParameterizedTest @ValueSource(strings = {"@", "a", "가"}) - @DisplayName("댓글 ID가 Long 타입으로 변환할 수 없는 값이라면 400을 응답한다.") + @DisplayName("댓글 ID가 Long 타입으로 변환할 수 없는 값이라면 400 응답을 반환한다.") void invalidCommentIDType(String commentId) throws Exception { // given - TokenPayload tokenPayload = new TokenPayload(1L, 1L, 1L); - given(tokenProcessor.resolveToken(anyString())).willReturn("token"); - given(tokenProcessor.parseToken(anyString())).willReturn(tokenPayload); - given(memberService.findById(anyLong())).willReturn(MemberFixtures.MALE_20.get()); + mockingAuthArgumentResolver(); CommentUpdateRequest request = new CommentUpdateRequest("hello"); // when, then RestAssuredMockMvc.given().log().all() - .headers(HttpHeaders.AUTHORIZATION, "Bearer token") + .headers(HttpHeaders.AUTHORIZATION, BEARER_TOKEN) .contentType(MediaType.APPLICATION_JSON) .body(request) .when().put("/posts/{postId}/comments/{commentId}", 1L, commentId) .then().log().all() .status(HttpStatus.BAD_REQUEST) - .body("code", equalTo(-9998)) - .body("message", containsString("commentId는 Long 타입이 필요합니다.")); + .body("code", equalTo(202)) + .body("message", containsString("Long 타입으로 변환할 수 없는 요청입니다.")); + } + + @ParameterizedTest + @ValueSource(longs = {-1L, 0}) + @DisplayName("게시글 ID가 양의 정수가 아니라면 400 응답을 반환한다.") + void notPositivePostId(Long postId) throws Exception { + // given + mockingAuthArgumentResolver(); + CommentUpdateRequest commentCreateRequest = new CommentUpdateRequest("hello"); + + // when, then + RestAssuredMockMvc.given().log().all() + .headers(HttpHeaders.AUTHORIZATION, BEARER_TOKEN) + .contentType(MediaType.APPLICATION_JSON) + .body(commentCreateRequest) + .when().put("/posts/{postId}/comments/{commentId}", postId, 1L) + .then().log().all() + .status(HttpStatus.BAD_REQUEST) + .body("code", equalTo(201)) + .body("message", containsString("게시글 ID는 양의 정수만 가능합니다.")); + } + + @ParameterizedTest + @ValueSource(longs = {-1L, 0}) + @DisplayName("댓글 ID가 양의 정수가 아니라면 400 응답을 반환한다.") + void notPositiveCommentId(Long commentId) throws Exception { + // given + mockingAuthArgumentResolver(); + CommentUpdateRequest commentCreateRequest = new CommentUpdateRequest("hello"); + + // when, then + RestAssuredMockMvc.given().log().all() + .headers(HttpHeaders.AUTHORIZATION, BEARER_TOKEN) + .contentType(MediaType.APPLICATION_JSON) + .body(commentCreateRequest) + .when().put("/posts/{postId}/comments/{commentId}", 1L, commentId) + .then().log().all() + .status(HttpStatus.BAD_REQUEST) + .body("code", equalTo(201)) + .body("message", containsString("댓글 ID는 양의 정수만 가능합니다.")); } @ParameterizedTest @NullAndEmptySource - @DisplayName("수정할 댓글 내용이 비어있거나 존재하지 않으면 400을 응답한다.") + @DisplayName("수정할 댓글 내용이 비어있거나 존재하지 않으면 400 응답을 반환한다.") void nullAndEmptyCommentContent(String content) throws Exception { // given - TokenPayload tokenPayload = new TokenPayload(1L, 1L, 1L); - given(tokenProcessor.resolveToken(anyString())).willReturn("token"); - given(tokenProcessor.parseToken(anyString())).willReturn(tokenPayload); - given(memberService.findById(anyLong())).willReturn(MemberFixtures.MALE_20.get()); + mockingAuthArgumentResolver(); CommentUpdateRequest request = new CommentUpdateRequest(content); // when, then RestAssuredMockMvc.given().log().all() - .headers(HttpHeaders.AUTHORIZATION, "Bearer token") + .headers(HttpHeaders.AUTHORIZATION, BEARER_TOKEN) .contentType(MediaType.APPLICATION_JSON) .body(request) .when().put("/posts/{postId}/comments/{commentId}", 1L, 1L) .then().log().all() .status(HttpStatus.BAD_REQUEST) - .body("code", equalTo(-9997)) + .body("code", equalTo(200)) .body("message", containsString("댓글 내용은 존재해야 합니다.")); } @@ -286,63 +337,87 @@ void nullAndEmptyCommentContent(String content) throws Exception { @DisplayName("게시글 댓글 삭제") class DeleteComment { + @Test + @DisplayName("정상적인 요청이라면 댓글을 삭제하고 204응답을 반환한다.") + void deleteComment() throws Exception { + // given + mockingAuthArgumentResolver(); + willDoNothing().given(postCommentService).deleteComment(anyLong(), anyLong(), any(Member.class)); + + // when, then + RestAssuredMockMvc.given().log().all() + .headers(HttpHeaders.AUTHORIZATION, BEARER_TOKEN) + .when().delete("/posts/{postId}/comments/{commentId}", 1L, 1L) + .then().log().all() + .status(HttpStatus.NO_CONTENT); + } + @ParameterizedTest @ValueSource(strings = {"@", "a", "가"}) - @DisplayName("게시글 ID가 Long 타입으로 변환할 수 없는 값이라면 400을 응답한다.") + @DisplayName("게시글 ID가 Long 타입으로 변환할 수 없는 값이라면 400 응답을 반환한다.") void invalidPostIDType(String postId) throws Exception { // given - TokenPayload tokenPayload = new TokenPayload(1L, 1L, 1L); - given(tokenProcessor.resolveToken(anyString())).willReturn("token"); - given(tokenProcessor.parseToken(anyString())).willReturn(tokenPayload); - given(memberService.findById(anyLong())).willReturn(MemberFixtures.MALE_20.get()); + mockingAuthArgumentResolver(); // when, then RestAssuredMockMvc.given().log().all() - .headers(HttpHeaders.AUTHORIZATION, "Bearer token") + .headers(HttpHeaders.AUTHORIZATION, BEARER_TOKEN) .when().delete("/posts/{postId}/comments/{commentId}", postId, 1L) .then().log().all() .status(HttpStatus.BAD_REQUEST) - .body("code", equalTo(-9998)) - .body("message", containsString("postId는 Long 타입이 필요합니다.")); + .body("code", equalTo(202)) + .body("message", containsString("Long 타입으로 변환할 수 없는 요청입니다.")); } @ParameterizedTest @ValueSource(strings = {"@", "a", "가"}) - @DisplayName("댓글 ID가 Long 타입으로 변환할 수 없는 값이라면 400을 응답한다.") + @DisplayName("댓글 ID가 Long 타입으로 변환할 수 없는 값이라면 400 응답을 반환한다.") void invalidCommentIDType(String commentId) throws Exception { // given - TokenPayload tokenPayload = new TokenPayload(1L, 1L, 1L); - given(tokenProcessor.resolveToken(anyString())).willReturn("token"); - given(tokenProcessor.parseToken(anyString())).willReturn(tokenPayload); - given(memberService.findById(anyLong())).willReturn(MemberFixtures.MALE_20.get()); + mockingAuthArgumentResolver(); // when, then RestAssuredMockMvc.given().log().all() - .headers(HttpHeaders.AUTHORIZATION, "Bearer token") + .headers(HttpHeaders.AUTHORIZATION, BEARER_TOKEN) .when().delete("/posts/{postId}/comments/{commentId}", 1L, commentId) .then().log().all() .status(HttpStatus.BAD_REQUEST) - .body("code", equalTo(-9998)) - .body("message", containsString("commentId는 Long 타입이 필요합니다.")); + .body("code", equalTo(202)) + .body("message", containsString("Long 타입으로 변환할 수 없는 요청입니다.")); } - @Test - @DisplayName("댓글을 정상적으로 삭제하면 204를 응답한다.") - void deleteComment() throws Exception { + @ParameterizedTest + @ValueSource(longs = {-1L, 0}) + @DisplayName("게시글 ID가 양의 정수가 아니라면 400 응답을 반환한다.") + void notPositivePostId(Long postId) throws Exception { // given - TokenPayload tokenPayload = new TokenPayload(1L, 1L, 1L); - given(tokenProcessor.resolveToken(anyString())).willReturn("token"); - given(tokenProcessor.parseToken(anyString())).willReturn(tokenPayload); - given(memberService.findById(anyLong())).willReturn(MemberFixtures.MALE_20.get()); + mockingAuthArgumentResolver(); - willDoNothing().given(postCommentService).deleteComment(anyLong(), anyLong(), any(Member.class)); + // when, then + RestAssuredMockMvc.given().log().all() + .headers(HttpHeaders.AUTHORIZATION, BEARER_TOKEN) + .when().delete("/posts/{postId}/comments/{commentId}", postId, 1L) + .then().log().all() + .status(HttpStatus.BAD_REQUEST) + .body("code", equalTo(201)) + .body("message", containsString("게시글 ID는 양의 정수만 가능합니다.")); + } + + @ParameterizedTest + @ValueSource(longs = {-1L, 0}) + @DisplayName("댓글 ID가 양의 정수가 아니라면 400 응답을 반환한다.") + void notPositiveCommentId(Long commentId) throws Exception { + // given + mockingAuthArgumentResolver(); // when, then RestAssuredMockMvc.given().log().all() - .headers(HttpHeaders.AUTHORIZATION, "Bearer token") - .when().delete("/posts/{postId}/comments/{commentId}", 1L, 1L) + .headers(HttpHeaders.AUTHORIZATION, BEARER_TOKEN) + .when().delete("/posts/{postId}/comments/{commentId}", 1L, commentId) .then().log().all() - .status(HttpStatus.NO_CONTENT); + .status(HttpStatus.BAD_REQUEST) + .body("code", equalTo(201)) + .body("message", containsString("댓글 ID는 양의 정수만 가능합니다.")); } } diff --git a/backend/src/test/java/com/votogether/domain/post/controller/PostControllerTest.java b/backend/src/test/java/com/votogether/domain/post/controller/PostControllerTest.java deleted file mode 100644 index ae3d1db20..000000000 --- a/backend/src/test/java/com/votogether/domain/post/controller/PostControllerTest.java +++ /dev/null @@ -1,903 +0,0 @@ -package com.votogether.domain.post.controller; - -import static com.votogether.test.fixtures.MemberFixtures.MALE_30; -import static org.assertj.core.api.Assertions.assertThat; -import static org.hamcrest.Matchers.startsWith; -import static org.junit.jupiter.api.Assertions.assertAll; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.anyList; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.isNull; -import static org.mockito.BDDMockito.given; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.votogether.domain.member.entity.Member; -import com.votogether.domain.member.service.MemberService; -import com.votogether.domain.post.dto.request.post.PostCreateRequest; -import com.votogether.domain.post.dto.request.post.PostOptionCreateRequest; -import com.votogether.domain.post.dto.request.post.PostOptionUpdateRequest; -import com.votogether.domain.post.dto.request.post.PostUpdateRequest; -import com.votogether.domain.post.dto.response.post.PostDetailResponse; -import com.votogether.domain.post.dto.response.post.PostRankingResponse; -import com.votogether.domain.post.dto.response.post.PostResponse; -import com.votogether.domain.post.dto.response.post.PostSummaryResponse; -import com.votogether.domain.post.dto.response.post.WriterResponse; -import com.votogether.domain.post.dto.response.vote.VoteCountForAgeGroupResponse; -import com.votogether.domain.post.dto.response.vote.VoteDetailResponse; -import com.votogether.domain.post.dto.response.vote.VoteOptionStatisticsResponse; -import com.votogether.domain.post.entity.Post; -import com.votogether.domain.post.entity.PostBody; -import com.votogether.domain.post.entity.vo.PostClosingType; -import com.votogether.domain.post.entity.vo.PostSortType; -import com.votogether.domain.post.service.PostService; -import com.votogether.global.exception.GlobalExceptionHandler; -import com.votogether.global.jwt.TokenPayload; -import com.votogether.global.jwt.TokenProcessor; -import com.votogether.test.fixtures.MemberFixtures; -import io.restassured.http.ContentType; -import io.restassured.module.mockmvc.RestAssuredMockMvc; -import io.restassured.module.mockmvc.response.MockMvcResponse; -import io.restassured.response.ExtractableResponse; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.time.LocalDateTime; -import java.time.temporal.ChronoUnit; -import java.util.List; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.core.ParameterizedTypeReference; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.test.util.ReflectionTestUtils; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.web.context.WebApplicationContext; - -@WebMvcTest(PostController.class) -class PostControllerTest { - - @Autowired - ObjectMapper mapper; - - @MockBean - TokenProcessor tokenProcessor; - - @MockBean - MemberService memberService; - - @MockBean - PostService postService; - - @BeforeEach - void setUp(final WebApplicationContext webApplicationContext) { - RestAssuredMockMvc.standaloneSetup( - MockMvcBuilders - .standaloneSetup(new PostController(postService)) - .setControllerAdvice(GlobalExceptionHandler.class) - ); - RestAssuredMockMvc.webAppContextSetup(webApplicationContext); - } - - @Test - @DisplayName("게시글을 작성한다") - void save() throws IOException { - // given - PostOptionCreateRequest postOptionCreateRequest1 = PostOptionCreateRequest.builder() - .content("optionContent1") - .build(); - - PostOptionCreateRequest postOptionCreateRequest2 = PostOptionCreateRequest.builder() - .content("optionContent2") - .build(); - - PostCreateRequest postCreateRequest = PostCreateRequest.builder() - .categoryIds(List.of(0L)) - .title("title") - .content("content") - .deadline(LocalDateTime.now().plusDays(2)) - .postOptions(List.of(postOptionCreateRequest1, postOptionCreateRequest2)) - .build(); - - String fileName1 = "testImage1.PNG"; - String resultFileName1 = "testResultImage1.PNG"; - String filePath1 = "src/test/resources/images/" + fileName1; - File file1 = new File(filePath1); - - String fileName2 = "testImage2.PNG"; - String resultFileName2 = "testResultImage2.PNG"; - String filePath2 = "src/test/resources/images/" + fileName2; - File file2 = new File(filePath2); - - String fileName3 = "testImage3.PNG"; - String resultFileName3 = "testResultImage3.PNG"; - String filePath3 = "src/test/resources/images/" + fileName3; - File file3 = new File(filePath3); - - String postRequestJson = mapper.writeValueAsString(postCreateRequest); - - long savedPostId = 1L; - given(postService.save(any(), any(), anyList(), anyList())).willReturn(savedPostId); - - TokenPayload tokenPayload = new TokenPayload(1L, 1L, 1L); - given(tokenProcessor.resolveToken(anyString())).willReturn("token"); - given(tokenProcessor.parseToken(anyString())).willReturn(tokenPayload); - given(memberService.findById(anyLong())).willReturn(MemberFixtures.FEMALE_20.get()); - - // when, then - String locationStartsWith = "/posts/"; - ExtractableResponse response = RestAssuredMockMvc.given().log().all() - .headers(HttpHeaders.AUTHORIZATION, "Bearer token") - .contentType(ContentType.MULTIPART) - .multiPart("request", postRequestJson, "application/json") - .multiPart("contentImages", resultFileName3, new FileInputStream(file3), "image/png") - .multiPart("optionImages", resultFileName1, new FileInputStream(file1), "image/png") - .multiPart("optionImages", resultFileName2, new FileInputStream(file2), "image/png") - .when().post("/posts") - .then().log().all() - .status(HttpStatus.CREATED) - .header("Location", startsWith(locationStartsWith)) - .extract(); - - String postId = response.header("Location").substring(locationStartsWith.length()); - assertThat(Long.parseLong(postId)).isEqualTo(savedPostId); - } - - @Test - @DisplayName("게시글을 등록 시, 유효성 검증에 위배되는 데이터를 전달하면 예외를 던진다.") - void throwExceptionBlankTitle() throws IOException { - // given - PostOptionCreateRequest postOptionCreateRequest1 = PostOptionCreateRequest.builder() - .content("optionContent1") - .build(); - - PostOptionCreateRequest postOptionCreateRequest2 = PostOptionCreateRequest.builder() - .content("optionContent2") - .build(); - - PostCreateRequest postCreateRequest = PostCreateRequest.builder() - .categoryIds(List.of(0L)) - .content("c".repeat(1001)) - .deadline(LocalDateTime.now().plusDays(2)) - .postOptions(List.of(postOptionCreateRequest1, postOptionCreateRequest2)) - .build(); - - String fileName1 = "testImage1.PNG"; - String resultFileName1 = "testResultImage1.PNG"; - String filePath1 = "src/test/resources/images/" + fileName1; - File file1 = new File(filePath1); - - String fileName2 = "testImage2.PNG"; - String resultFileName2 = "testResultImage2.PNG"; - String filePath2 = "src/test/resources/images/" + fileName2; - File file2 = new File(filePath2); - - String fileName3 = "testImage3.PNG"; - String resultFileName3 = "testResultImage3.PNG"; - String filePath3 = "src/test/resources/images/" + fileName3; - File file3 = new File(filePath3); - - String postRequestJson = mapper.writeValueAsString(postCreateRequest); - - long savedPostId = 1L; - given(postService.save(any(), any(), anyList(), anyList())).willReturn(savedPostId); - - TokenPayload tokenPayload = new TokenPayload(1L, 1L, 1L); - given(tokenProcessor.resolveToken(anyString())).willReturn("token"); - given(tokenProcessor.parseToken(anyString())).willReturn(tokenPayload); - given(memberService.findById(anyLong())).willReturn(MemberFixtures.FEMALE_20.get()); - - // when - ExtractableResponse response = RestAssuredMockMvc.given().log().all() - .headers(HttpHeaders.AUTHORIZATION, "Bearer token") - .contentType(ContentType.MULTIPART) - .multiPart("request", postRequestJson, "application/json") - .multiPart("contentImages", resultFileName3, new FileInputStream(file3), "image/png") - .multiPart("optionImages", resultFileName1, new FileInputStream(file1), "image/png") - .multiPart("optionImages", resultFileName2, new FileInputStream(file2), "image/png") - .when().post("/posts") - .then().log().all() - .extract(); - - // then - final String message = response.body().jsonPath().get("message").toString(); - assertAll( - () -> assertThat(message).contains("제목을 입력해주세요.", "내용은 최대 1000자까지 입력 가능합니다."), - () -> assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value()) - ); - } - - @Nested - @DisplayName("전체 게시글 목록 조회에서") - class GetAllPost { - - @Test - @DisplayName("page에 숫자가 아닌 다른 값이 들어온 경우 400을 반환한다.") - void invalidPage() throws Exception { - // given - TokenPayload tokenPayload = new TokenPayload(1L, 1L, 1L); - given(tokenProcessor.resolveToken(anyString())).willReturn("token"); - given(tokenProcessor.parseToken(anyString())).willReturn(tokenPayload); - given(memberService.findById(anyLong())).willReturn(MemberFixtures.FEMALE_20.get()); - - // when, then - RestAssuredMockMvc - .given().log().all() - .headers(HttpHeaders.AUTHORIZATION, "Bearer token") - .param("page", "abc") - .param("postClosingType", PostClosingType.CLOSED) - .param("postSortType", PostSortType.LATEST) - .when().get("/posts") - .then().log().all() - .status(HttpStatus.BAD_REQUEST); - } - - @Test - @DisplayName("PageClosingType이 아닌 다른 값이 들어온 경우 400을 반환한다.") - void invalidPostClosingType() throws Exception { - // given - TokenPayload tokenPayload = new TokenPayload(1L, 1L, 1L); - given(tokenProcessor.resolveToken(anyString())).willReturn("token"); - given(tokenProcessor.parseToken(anyString())).willReturn(tokenPayload); - given(memberService.findById(anyLong())).willReturn(MemberFixtures.FEMALE_20.get()); - - // when, then - RestAssuredMockMvc - .given().log().all() - .headers(HttpHeaders.AUTHORIZATION, "Bearer token") - .param("page", 0) - .param("postClosingType", "abc") - .param("postSortType", PostSortType.LATEST) - .when().get("/posts") - .then().log().all() - .status(HttpStatus.BAD_REQUEST); - } - - @Test - @DisplayName("PostSortType이 아닌 다른 값이 들어온 경우 400을 반환한다.") - void invalidPostSortType() throws Exception { - // given - TokenPayload tokenPayload = new TokenPayload(1L, 1L, 1L); - given(tokenProcessor.resolveToken(anyString())).willReturn("token"); - given(tokenProcessor.parseToken(anyString())).willReturn(tokenPayload); - given(memberService.findById(anyLong())).willReturn(MemberFixtures.FEMALE_20.get()); - - // when, then - RestAssuredMockMvc - .given().log().all() - .headers(HttpHeaders.AUTHORIZATION, "Bearer token") - .param("page", 0) - .param("postClosingType", PostClosingType.CLOSED) - .param("postSortType", "abc") - .when().get("/posts") - .then().log().all() - .status(HttpStatus.BAD_REQUEST); - } - - @Test - @DisplayName("정렬 유형, 마감 유형, 카테고리로 모든 게시물 조회한다") - void getAllPostBySortTypeAndClosingTypeAndCategoryId() throws Exception { - // given - PostBody postBody = PostBody.builder() - .title("title") - .content("content") - .build(); - - Post post = Post.builder() - .writer(MALE_30.get()) - .postBody(postBody) - .deadline(LocalDateTime.now().plusDays(3L)) - .build(); - - TokenPayload tokenPayload = new TokenPayload(1L, 1L, 1L); - given(tokenProcessor.resolveToken(anyString())).willReturn("token"); - given(tokenProcessor.parseToken(anyString())).willReturn(tokenPayload); - given(memberService.findById(anyLong())).willReturn(MemberFixtures.FEMALE_20.get()); - - given(postService.getAllPostBySortTypeAndClosingTypeAndCategoryId( - eq(0), - eq(PostClosingType.PROGRESS), - eq(PostSortType.LATEST), - anyLong(), - any(Member.class) - ) - ).willReturn(List.of(PostResponse.of(post, MALE_30.get()))); - - // when - List responses = RestAssuredMockMvc - .given().log().all() - .headers(HttpHeaders.AUTHORIZATION, "Bearer token") - .param("page", 0) - .param("postClosingType", PostClosingType.PROGRESS) - .param("postSortType", PostSortType.LATEST) - .param("category", 1L) - .when().get("/posts") - .then().log().all() - .contentType(ContentType.JSON) - .status(HttpStatus.OK) - .extract() - .as(new ParameterizedTypeReference>() { - }.getType()); - - // then - assertAll( - () -> assertThat(responses).isNotEmpty(), - () -> assertThat(responses).hasSize(1) - ); - } - - } - - - @Nested - @DisplayName("비회원 게시글 목록 조회") - class GetPostsGuest { - - @Test - @DisplayName("마감 시간 타입으로 변환할 수 없으면 400 상태를 응답한다.") - void invalidPostClosingType() { - RestAssuredMockMvc.given().log().all() - .param("page", 0) - .param("postClosingType", "hello") - .param("postSortType", PostSortType.LATEST) - .when().get("/posts/guest") - .then().log().all() - .contentType(ContentType.JSON) - .status(HttpStatus.BAD_REQUEST); - } - - @Test - @DisplayName("정렬 타입으로 변환할 수 없으면 400 상태를 반환한다.") - void invalidPostSortType() { - RestAssuredMockMvc.given().log().all() - .param("page", 0) - .param("postClosingType", PostClosingType.ALL) - .param("postSortType", "hello") - .when().get("/posts/guest") - .then().log().all() - .contentType(ContentType.JSON) - .status(HttpStatus.BAD_REQUEST); - } - - @Test - @DisplayName("카테고리가 없는 올바른 조회 요청이라면 게시글 목록 응답과 200 상태를 반환한다.") - void getPostsGuestWithoutCategory() { - PostBody postBody = PostBody.builder() - .title("title") - .content("content") - .build(); - - Post post = Post.builder() - .writer(MALE_30.get()) - .postBody(postBody) - .deadline(LocalDateTime.now().plusDays(3L)) - .build(); - - given(postService.getPostsGuest(anyInt(), any(PostClosingType.class), any(PostSortType.class), isNull())) - .willReturn(List.of(PostResponse.forGuest(post))); - - RestAssuredMockMvc.given().log().all() - .param("page", 0) - .param("postClosingType", PostClosingType.ALL) - .param("postSortType", PostSortType.LATEST) - .when().get("/posts/guest") - .then().log().all() - .contentType(ContentType.JSON) - .status(HttpStatus.OK); - } - - @Test - @DisplayName("카테고리가 있는 올바른 조회 요청이라면 게시글 목록 응답과 200 상태를 반환한다.") - void getPostsGuestWithCategory() { - PostBody postBody = PostBody.builder() - .title("title") - .content("content") - .build(); - - Post post = Post.builder() - .writer(MALE_30.get()) - .postBody(postBody) - .deadline(LocalDateTime.now().plusDays(3L)) - .build(); - - given(postService.getPostsGuest(anyInt(), any(PostClosingType.class), any(PostSortType.class), anyLong())) - .willReturn(List.of(PostResponse.forGuest(post))); - - RestAssuredMockMvc.given().log().all() - .param("page", 0) - .param("postClosingType", PostClosingType.ALL) - .param("postSortType", PostSortType.LATEST) - .param("category", 1L) - .when().get("/posts/guest") - .then().log().all() - .contentType(ContentType.JSON) - .status(HttpStatus.OK); - } - - } - - @Test - @DisplayName("한 게시글의 상세를 조회한다.") - void getPost() throws JsonProcessingException { - // given - long postId = 0L; - Member writer = MALE_30.get(); - LocalDateTime deadline = LocalDateTime.now().plusDays(3L); - - PostBody postBody = PostBody.builder() - .title("title") - .content("content") - .build(); - - Post post = Post.builder() - .writer(writer) - .postBody(postBody) - .deadline(deadline) - .build(); - - Member member = MemberFixtures.MALE_20.get(); - given(postService.getPostById(postId, member)).willReturn(PostDetailResponse.of(post, member)); - - TokenPayload tokenPayload = new TokenPayload(1L, 1L, 1L); - given(tokenProcessor.resolveToken(anyString())).willReturn("token"); - given(tokenProcessor.parseToken(anyString())).willReturn(tokenPayload); - given(memberService.findById(anyLong())).willReturn(MemberFixtures.FEMALE_20.get()); - - // when - String responseBody = RestAssuredMockMvc.given().log().all() - .headers(HttpHeaders.AUTHORIZATION, "Bearer token") - .when().get("/posts/{postId}", postId) - .then().log().all() - .contentType(ContentType.JSON) - .status(HttpStatus.OK) - .extract().asString(); - - PostDetailResponse response = mapper.readValue(responseBody, new TypeReference<>() { - }); - - // then - WriterResponse writerResponse = response.writer(); - VoteDetailResponse voteDetailResponse = response.voteInfo(); - - assertAll( - () -> assertThat(response.title()).isEqualTo("title"), - () -> assertThat(response.content()).isEqualTo("content"), - () -> assertThat(response.deadline()).isEqualTo(deadline.truncatedTo(ChronoUnit.MINUTES)), - () -> assertThat(writerResponse.id()).isEqualTo(member.getId()), - () -> assertThat(writerResponse.nickname()).isEqualTo("user9"), - () -> assertThat(voteDetailResponse.totalVoteCount()).isZero() - ); - } - - @Test - @DisplayName("비회원이 한 게시글을 상세 조회한다.") - void getPostByGuest() { - // given - long postId = 0L; - Member writer = MALE_30.get(); - LocalDateTime deadline = LocalDateTime.now().plusDays(3L); - - PostBody postBody = PostBody.builder() - .title("title") - .content("content") - .build(); - - Post post = Post.builder() - .writer(writer) - .postBody(postBody) - .deadline(deadline) - .build(); - - given(postService.getPostById(postId, null)).willReturn(PostDetailResponse.of(post, null)); - - // when - PostDetailResponse response = RestAssuredMockMvc.given().log().all() - .when().get("/posts/{postId}/guest", postId) - .then().log().all() - .contentType(ContentType.JSON) - .status(HttpStatus.OK) - .extract() - .as(PostDetailResponse.class); - - // then - WriterResponse writerResponse = response.writer(); - VoteDetailResponse voteDetailResponse = response.voteInfo(); - - assertAll( - () -> assertThat(response.title()).isEqualTo("title"), - () -> assertThat(response.content()).isEqualTo("content"), - () -> assertThat(response.deadline()).isEqualTo(deadline.truncatedTo(ChronoUnit.MINUTES)), - () -> assertThat(writerResponse.nickname()).isEqualTo("user9"), - () -> assertThat(voteDetailResponse.totalVoteCount()).isEqualTo(-1) - ); - } - - @Test - @DisplayName("게시글에 대한 전체 투표 통계를 조회한다.") - void getVoteStatistics() throws Exception { - // given - VoteOptionStatisticsResponse response = new VoteOptionStatisticsResponse( - 17, - 10, - 7, - List.of( - new VoteCountForAgeGroupResponse("10대 미만", 2, 1, 1), - new VoteCountForAgeGroupResponse("10대", 3, 1, 2), - new VoteCountForAgeGroupResponse("20대", 2, 2, 0), - new VoteCountForAgeGroupResponse("30대", 5, 3, 2), - new VoteCountForAgeGroupResponse("40대", 1, 1, 0), - new VoteCountForAgeGroupResponse("50대", 0, 0, 0), - new VoteCountForAgeGroupResponse("60대 이상", 4, 2, 2) - ) - ); - given(postService.getVoteStatistics(anyLong(), any(Member.class))).willReturn(response); - - TokenPayload tokenPayload = new TokenPayload(1L, 1L, 1L); - given(tokenProcessor.resolveToken(anyString())).willReturn("token"); - given(tokenProcessor.parseToken(anyString())).willReturn(tokenPayload); - given(memberService.findById(anyLong())).willReturn(MemberFixtures.FEMALE_20.get()); - - // when - VoteOptionStatisticsResponse result = RestAssuredMockMvc.given().log().all() - .headers(HttpHeaders.AUTHORIZATION, "Bearer token") - .when().get("/posts/{postId}/options", 1) - .then().log().all() - .status(HttpStatus.OK) - .extract() - .as(VoteOptionStatisticsResponse.class); - - // then - assertThat(result).usingRecursiveComparison().isEqualTo(response); - } - - @Test - @DisplayName("게시글 투표 옵션에 대한 투표 통계를 조회한다.") - void getVoteOptionStatistics() throws Exception { - // given - VoteOptionStatisticsResponse response = new VoteOptionStatisticsResponse( - 17, - 10, - 7, - List.of( - new VoteCountForAgeGroupResponse("10대 미만", 2, 1, 1), - new VoteCountForAgeGroupResponse("10대", 3, 1, 2), - new VoteCountForAgeGroupResponse("20대", 2, 2, 0), - new VoteCountForAgeGroupResponse("30대", 5, 3, 2), - new VoteCountForAgeGroupResponse("40대", 1, 1, 0), - new VoteCountForAgeGroupResponse("50대", 0, 0, 0), - new VoteCountForAgeGroupResponse("60대 이상", 4, 2, 2) - ) - ); - given(postService.getVoteOptionStatistics(anyLong(), anyLong(), any(Member.class))).willReturn(response); - - TokenPayload tokenPayload = new TokenPayload(1L, 1L, 1L); - given(tokenProcessor.resolveToken(anyString())).willReturn("token"); - given(tokenProcessor.parseToken(anyString())).willReturn(tokenPayload); - given(memberService.findById(anyLong())).willReturn(MemberFixtures.FEMALE_20.get()); - - // when - VoteOptionStatisticsResponse result = RestAssuredMockMvc.given().log().all() - .headers(HttpHeaders.AUTHORIZATION, "Bearer token") - .when().get("/posts/{postId}/options/{optionId}", 1, 1) - .then().log().all() - .status(HttpStatus.OK) - .extract() - .as(VoteOptionStatisticsResponse.class); - - // then - assertThat(result).usingRecursiveComparison().isEqualTo(response); - } - - @Test - @DisplayName("회원본인이 투표한 게시글 목록을 조회한다.") - void getPostsVotedByMember() throws Exception { - // given - PostBody postBody = PostBody.builder() - .title("title") - .content("content") - .build(); - - Post post = Post.builder() - .writer(MALE_30.get()) - .postBody(postBody) - .deadline(LocalDateTime.now().plusDays(3L).truncatedTo(ChronoUnit.MINUTES)) - .build(); - - PostResponse postResponse = PostResponse.of(post, MALE_30.get()); - - given(postService.getPostsVotedByMember(anyInt(), any(), any(), any(Member.class))) - .willReturn(List.of(postResponse)); - - TokenPayload tokenPayload = new TokenPayload(1L, 1L, 1L); - given(tokenProcessor.resolveToken(anyString())).willReturn("token"); - given(tokenProcessor.parseToken(anyString())).willReturn(tokenPayload); - given(memberService.findById(anyLong())).willReturn(MemberFixtures.FEMALE_20.get()); - - // when - List result = RestAssuredMockMvc.given().log().all() - .headers(HttpHeaders.AUTHORIZATION, "Bearer token") - .param("page", 0) - .param("postClosingType", PostClosingType.PROGRESS) - .param("postSortType", PostSortType.LATEST) - .when().get("/posts/votes/me") - .then().log().all() - .status(HttpStatus.OK) - .extract() - .as(new ParameterizedTypeReference>() { - }.getType()); - - // then - assertThat(result.get(0)).usingRecursiveComparison().isEqualTo(postResponse); - } - - @Test - @DisplayName("게시글을 조기 마감한다.") - void postClosedEarly() throws Exception { - // given - long postId = 1L; - - TokenPayload tokenPayload = new TokenPayload(1L, 1L, 1L); - given(tokenProcessor.resolveToken(anyString())).willReturn("token"); - given(tokenProcessor.parseToken(anyString())).willReturn(tokenPayload); - given(memberService.findById(anyLong())).willReturn(MemberFixtures.FEMALE_20.get()); - - // when - ExtractableResponse response = RestAssuredMockMvc.given().log().all() - .headers(HttpHeaders.AUTHORIZATION, "Bearer token") - .when().patch("/posts/{postId}/close", postId) - .then().log().all() - .extract(); - - // then - assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); - } - - @Test - @DisplayName("회원본인이 작성한 게시글 목록을 조회한다.") - void getPostsByWriter() throws JsonProcessingException { - // given - long postId = 1L; - Member member = MemberFixtures.FEMALE_20.get(); - - TokenPayload tokenPayload = new TokenPayload(1L, 1L, 1L); - given(tokenProcessor.resolveToken(anyString())).willReturn("token"); - given(tokenProcessor.parseToken(anyString())).willReturn(tokenPayload); - given(memberService.findById(anyLong())).willReturn(member); - - PostBody postBody = PostBody.builder() - .title("title") - .content("content") - .build(); - - Post post = Post.builder() - .writer(member) - .postBody(postBody) - .deadline(LocalDateTime.now().plusDays(3L).truncatedTo(ChronoUnit.MINUTES)) - .build(); - - PostResponse postResponse = PostResponse.of(post, member); - - given(postService.getPostsByWriter( - anyInt(), - any(PostClosingType.class), - any(PostSortType.class), - anyLong(), - any(Member.class)) - ).willReturn(List.of(postResponse)); - - // when - List result = RestAssuredMockMvc - .given().log().all() - .headers(HttpHeaders.AUTHORIZATION, "Bearer token") - .param("page", 0) - .param("postClosingType", PostClosingType.PROGRESS) - .param("postSortType", PostSortType.LATEST) - .param("category", 1L) - .when().get("/posts/me") - .then().log().all() - .status(HttpStatus.OK) - .extract() - .as(new ParameterizedTypeReference>() { - }.getType()); - - // then - assertAll( - () -> assertThat(result).hasSize(1), - () -> assertThat(result.get(0)).usingRecursiveComparison().isEqualTo(postResponse) - ); - } - - @Test - @DisplayName("(회원) 키워드를 통해 게시글 목록을 조회한다.") - void searchPostsWithKeyword() throws JsonProcessingException { - // given - long postId = 1L; - Member member = MemberFixtures.FEMALE_20.get(); - - TokenPayload tokenPayload = new TokenPayload(1L, 1L, 1L); - given(tokenProcessor.resolveToken(anyString())).willReturn("token"); - given(tokenProcessor.parseToken(anyString())).willReturn(tokenPayload); - given(memberService.findById(anyLong())).willReturn(member); - - PostBody postBody = PostBody.builder() - .title("title") - .content("content") - .build(); - - Post post = Post.builder() - .writer(MALE_30.get()) - .postBody(postBody) - .deadline(LocalDateTime.now().plusDays(3L).truncatedTo(ChronoUnit.MINUTES)) - .build(); - - PostResponse postResponse = PostResponse.of(post, member); - - given(postService.searchPostsWithKeyword(anyString(), anyInt(), any(), any(), anyLong(), any(Member.class))) - .willReturn(List.of(postResponse)); - - // when - List result = RestAssuredMockMvc.given().log().all() - .headers(HttpHeaders.AUTHORIZATION, "Bearer token") - .param("keyword", "하이") - .param("page", 0) - .param("postClosingType", PostClosingType.PROGRESS) - .param("postSortType", PostSortType.LATEST) - .param("category", 1L) - .when().get("/posts/search") - .then().log().all() - .status(HttpStatus.OK) - .extract() - .as(new ParameterizedTypeReference>() { - }.getType()); - - // then - assertThat(result.get(0)).usingRecursiveComparison().isEqualTo(postResponse); - } - - @Test - @DisplayName("(비회원) 키워드를 통해 게시글 목록을 조회한다.") - void searchPostsWithKeywordForGuest() { - // given - PostBody postBody = PostBody.builder() - .title("title") - .content("content") - .build(); - - Post post = Post.builder() - .writer(MALE_30.get()) - .postBody(postBody) - .deadline(LocalDateTime.now().plusDays(3L).truncatedTo(ChronoUnit.MINUTES)) - .build(); - - PostResponse postResponse = PostResponse.forGuest(post); - - given(postService.searchPostsWithKeywordForGuest( - anyString(), - anyInt(), - any(PostClosingType.class), - any(PostSortType.class), - anyLong()) - ).willReturn(List.of(postResponse)); - - // when - List result = RestAssuredMockMvc.given().log().all() - .param("keyword", "하이") - .param("page", 0) - .param("postClosingType", PostClosingType.PROGRESS) - .param("postSortType", PostSortType.LATEST) - .param("category", 1L) - .when().get("/posts/search/guest") - .then().log().all() - .status(HttpStatus.OK) - .extract() - .as(new ParameterizedTypeReference>() { - }.getType()); - - // then - assertThat(result.get(0)).usingRecursiveComparison().isEqualTo(postResponse); - } - - @Test - @DisplayName("게시글을 삭제한다.") - void delete() { - // given - long postId = 1L; - - // when, then - RestAssuredMockMvc.given().log().all() - .when().delete("/posts/{postId}", postId) - .then().log().all() - .assertThat() - .status(HttpStatus.NO_CONTENT); - } - - @Test - @DisplayName("게시글을 수정한다.") - void update() throws IOException { - // given - PostOptionUpdateRequest postOptionUpdateRequest1 = PostOptionUpdateRequest.builder() - .content("optionContent1") - .build(); - - PostOptionUpdateRequest postOptionUpdateRequest2 = PostOptionUpdateRequest.builder() - .content("optionContent2") - .build(); - - PostUpdateRequest postUpdateRequest = PostUpdateRequest.builder() - .categoryIds(List.of(0L)) - .title("title") - .content("content") - .deadline(LocalDateTime.now().plusDays(2)) - .postOptions(List.of(postOptionUpdateRequest1, postOptionUpdateRequest2)) - .build(); - - String fileName1 = "testImage1.PNG"; - String resultFileName1 = "testResultImage1.PNG"; - String filePath1 = "src/test/resources/images/" + fileName1; - File file1 = new File(filePath1); - - String fileName2 = "testImage2.PNG"; - String resultFileName2 = "testResultImage2.PNG"; - String filePath2 = "src/test/resources/images/" + fileName2; - File file2 = new File(filePath2); - - String fileName3 = "testImage3.PNG"; - String resultFileName3 = "testResultImage3.PNG"; - String filePath3 = "src/test/resources/images/" + fileName3; - File file3 = new File(filePath3); - - String postRequestJson = mapper.writeValueAsString(postUpdateRequest); - - TokenPayload tokenPayload = new TokenPayload(1L, 1L, 1L); - given(tokenProcessor.resolveToken(anyString())).willReturn("token"); - given(tokenProcessor.parseToken(anyString())).willReturn(tokenPayload); - given(memberService.findById(anyLong())).willReturn(MemberFixtures.FEMALE_20.get()); - - // when, then - RestAssuredMockMvc.given().log().all() - .headers(HttpHeaders.AUTHORIZATION, "Bearer token") - .contentType(ContentType.MULTIPART) - .multiPart("request", postRequestJson, "application/json") - .multiPart("contentImages", resultFileName3, new FileInputStream(file3), "image/png") - .multiPart("optionImages", resultFileName1, new FileInputStream(file1), "image/png") - .multiPart("optionImages", resultFileName2, new FileInputStream(file2), "image/png") - .when().put("/posts/1", 1) - .then().log().all() - .status(HttpStatus.OK); - } - - @Test - @DisplayName("인기 게시물 랭킹을 불러온다.") - void getRanking() { - // given - Post post = Post.builder() - .postBody(PostBody.builder().title("제목").build()) - .writer(MemberFixtures.MALE_10.get()) - .build(); - ReflectionTestUtils.setField(post, "id", 1L); - - PostRankingResponse postRankingResponse = new PostRankingResponse(1, PostSummaryResponse.from(post)); - given(postService.getRanking()).willReturn(List.of(postRankingResponse)); - - // when, then - List result = RestAssuredMockMvc.given().log().all() - .when().get("/posts/ranking/popular/guest") - .then().log().all() - .status(HttpStatus.OK) - .extract() - .as(new ParameterizedTypeReference>() { - }.getType()); - - assertThat(result).isEqualTo(List.of(postRankingResponse)); - } - -} diff --git a/backend/src/test/java/com/votogether/domain/post/controller/PostGuestControllerTest.java b/backend/src/test/java/com/votogether/domain/post/controller/PostGuestControllerTest.java new file mode 100644 index 000000000..ce5cb8299 --- /dev/null +++ b/backend/src/test/java/com/votogether/domain/post/controller/PostGuestControllerTest.java @@ -0,0 +1,220 @@ +package com.votogether.domain.post.controller; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; + +import com.votogether.domain.post.dto.response.post.CategoryResponse; +import com.votogether.domain.post.dto.response.post.PostOptionVoteResultResponse; +import com.votogether.domain.post.dto.response.post.PostResponse; +import com.votogether.domain.post.dto.response.post.PostVoteResultResponse; +import com.votogether.domain.post.dto.response.post.PostWriterResponse; +import com.votogether.domain.post.entity.vo.PostClosingType; +import com.votogether.domain.post.entity.vo.PostSortType; +import com.votogether.domain.post.service.PostGuestService; +import com.votogether.global.exception.GlobalExceptionHandler; +import com.votogether.test.ControllerTest; +import io.restassured.common.mapper.TypeRef; +import io.restassured.module.mockmvc.RestAssuredMockMvc; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.HttpStatus; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +@WebMvcTest(PostGuestController.class) +class PostGuestControllerTest extends ControllerTest { + + @MockBean + PostGuestService postGuestService; + + @BeforeEach + void setUp(WebApplicationContext webApplicationContext) { + RestAssuredMockMvc.standaloneSetup( + MockMvcBuilders + .standaloneSetup(new PostGuestController(postGuestService)) + .setControllerAdvice(GlobalExceptionHandler.class) + ); + RestAssuredMockMvc.webAppContextSetup(webApplicationContext); + } + + @Nested + @DisplayName("비회원 게시글 목록 조회") + class GuestGetPosts { + + @Test + @DisplayName("게시글 목록 조회에 성공하면 200 응답을 반환한다.") + void return200success() { + // given + List response = List.of(mockingPostResponse()); + given(postGuestService.getPosts(anyInt(), any(PostClosingType.class), any(PostSortType.class), any())) + .willReturn(response); + + // when + List result = RestAssuredMockMvc.given().log().all() + .param("page", 0) + .param("postClosingType", PostClosingType.ALL) + .param("postSortType", PostSortType.LATEST) + .when().get("/posts/guest") + .then().log().all() + .status(HttpStatus.OK) + .extract() + .as(new TypeRef<>() { + }); + + // then + assertThat(result).usingRecursiveComparison().isEqualTo(response); + } + + @Test + @DisplayName("페이지가 음의 정수라면 400 응답을 반환한다.") + void return400pageIsNegative() { + // given, when, then + RestAssuredMockMvc.given().log().all() + .param("page", -1) + .param("postClosingType", PostClosingType.ALL) + .param("postSortType", PostSortType.LATEST) + .when().get("/posts/guest") + .then().log().all() + .status(HttpStatus.BAD_REQUEST) + .body("code", equalTo(201)) + .body("message", containsString("페이지는 0이상 정수만 가능합니다.")); + } + + } + + @Nested + @DisplayName("비회원 게시글 상세 조회") + class GuestGetPost { + + @Test + @DisplayName("게시글 상세 조회에 성공하면 200 응답을 반환한다.") + void return200success() { + // given + PostResponse response = mockingPostResponse(); + given(postGuestService.getPost(anyLong())).willReturn(response); + + // when + PostResponse result = RestAssuredMockMvc.given().log().all() + .when().get("/posts/{postId}/guest", 1L) + .then().log().all() + .status(HttpStatus.OK) + .extract() + .as(new TypeRef<>() { + }); + + // then + assertThat(result).usingRecursiveComparison().isEqualTo(response); + } + + @ParameterizedTest + @ValueSource(longs = {-1, 0}) + @DisplayName("게시글 ID가 양의 정수가 아니라면 400 응답을 반환한다.") + void return400postIdIsNegativeOrZero(Long postId) { + // given, when, then + RestAssuredMockMvc.given().log().all() + .when().get("/posts/{postId}/guest", postId) + .then().log().all() + .status(HttpStatus.BAD_REQUEST) + .body("code", equalTo(201)) + .body("message", containsString("게시글 ID는 양의 정수만 가능합니다.")); + } + + } + + @Nested + @DisplayName("비회원 게시글 목록 검색") + class GuestSearchPosts { + + @Test + @DisplayName("게시글 목록 검색에 성공하면 200 응답을 반환한다.") + void return200success() { + // given + List response = List.of(mockingPostResponse()); + given(postGuestService.searchPosts( + anyString(), + anyInt(), + any(PostClosingType.class), + any(PostSortType.class) + )).willReturn(response); + + // when + List result = RestAssuredMockMvc.given().log().all() + .param("keyword", "votogether") + .param("page", 0) + .param("postClosingType", PostClosingType.ALL) + .param("postSortType", PostSortType.LATEST) + .when().get("/posts/search/guest") + .then().log().all() + .status(HttpStatus.OK) + .extract() + .as(new TypeRef<>() { + }); + + // then + assertThat(result).usingRecursiveComparison().isEqualTo(response); + } + + @Test + @DisplayName("페이지가 음의 정수라면 400 응답을 반환한다.") + void return400pageIsNegative() { + // given, when, then + RestAssuredMockMvc.given().log().all() + .param("keyword", "votogether") + .param("page", -1) + .param("postClosingType", PostClosingType.ALL) + .param("postSortType", PostSortType.LATEST) + .when().get("/posts/search/guest") + .then().log().all() + .status(HttpStatus.BAD_REQUEST) + .body("code", equalTo(201)) + .body("message", containsString("페이지는 0이상 정수만 가능합니다.")); + } + + } + + private PostResponse mockingPostResponse() { + return new PostResponse( + 1L, + new PostWriterResponse(1L, "votogether"), + "title", + "content", + "https://votogether.com/static/images/image.png", + List.of( + new CategoryResponse(1L, "develop") + ), + LocalDateTime.now().truncatedTo(ChronoUnit.MINUTES), + LocalDateTime.now().truncatedTo(ChronoUnit.MINUTES), + 3, + 15, + new PostVoteResultResponse( + 0L, + -1, + List.of( + new PostOptionVoteResultResponse( + 1L, + "content", + "image.png", + -1, + 0 + ) + ) + ) + ); + } + +} diff --git a/backend/src/test/java/com/votogether/domain/post/controller/PostQueryControllerTest.java b/backend/src/test/java/com/votogether/domain/post/controller/PostQueryControllerTest.java new file mode 100644 index 000000000..41bda7987 --- /dev/null +++ b/backend/src/test/java/com/votogether/domain/post/controller/PostQueryControllerTest.java @@ -0,0 +1,488 @@ +package com.votogether.domain.post.controller; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; + +import com.votogether.domain.member.entity.Member; +import com.votogether.domain.post.dto.response.post.CategoryResponse; +import com.votogether.domain.post.dto.response.post.PostOptionVoteResultResponse; +import com.votogether.domain.post.dto.response.post.PostResponse; +import com.votogether.domain.post.dto.response.post.PostVoteResultResponse; +import com.votogether.domain.post.dto.response.post.PostWriterResponse; +import com.votogether.domain.post.dto.response.vote.VoteCountForAgeGroupResponse; +import com.votogether.domain.post.dto.response.vote.VoteOptionStatisticsResponse; +import com.votogether.domain.post.entity.vo.PostClosingType; +import com.votogether.domain.post.entity.vo.PostSortType; +import com.votogether.domain.post.service.PostQueryService; +import com.votogether.global.exception.GlobalExceptionHandler; +import com.votogether.test.ControllerTest; +import io.restassured.common.mapper.TypeRef; +import io.restassured.module.mockmvc.RestAssuredMockMvc; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +@WebMvcTest(PostQueryController.class) +class PostQueryControllerTest extends ControllerTest { + + @MockBean + PostQueryService postQueryService; + + @BeforeEach + void setUp(WebApplicationContext webApplicationContext) { + RestAssuredMockMvc.standaloneSetup( + MockMvcBuilders + .standaloneSetup(new PostQueryController(postQueryService)) + .setControllerAdvice(GlobalExceptionHandler.class) + ); + RestAssuredMockMvc.webAppContextSetup(webApplicationContext); + } + + @Nested + @DisplayName("게시글 목록 조회") + class GetPosts { + + @Test + @DisplayName("게시글 목록 조회에 성공하면 200 응답을 반환한다.") + void return200success() throws Exception { + // given + mockingAuthArgumentResolver(); + List response = List.of(mockingPostResponse()); + given(postQueryService.getPosts( + anyInt(), + any(PostClosingType.class), + any(PostSortType.class), + any(), + any(Member.class)) + ).willReturn(response); + + // when + List result = RestAssuredMockMvc.given().log().all() + .headers(HttpHeaders.AUTHORIZATION, "Bearer token") + .param("page", 0) + .param("postClosingType", PostClosingType.ALL) + .param("postSortType", PostSortType.LATEST) + .when().get("/posts") + .then().log().all() + .status(HttpStatus.OK) + .extract() + .as(new TypeRef<>() { + }); + + // then + assertThat(result).usingRecursiveComparison().isEqualTo(response); + } + + @Test + @DisplayName("페이지가 음수라면 400 응답을 반환한다.") + void return400pageNegative() throws Exception { + // given + mockingAuthArgumentResolver(); + + // when, then + RestAssuredMockMvc.given().log().all() + .headers(HttpHeaders.AUTHORIZATION, "Bearer token") + .param("page", -1) + .param("postClosingType", PostClosingType.ALL) + .param("postSortType", PostSortType.LATEST) + .when().get("/posts") + .then().log().all() + .status(HttpStatus.BAD_REQUEST) + .body("code", equalTo(201)) + .body("message", containsString("페이지는 0이상 정수만 가능합니다.")); + } + + } + + @Nested + @DisplayName("게시글 상세 조회") + class GetPost { + + @Test + @DisplayName("게시글 상세 조회에 성공하면 200 응답을 반환한다.") + void return200success() throws Exception { + // given + mockingAuthArgumentResolver(); + PostResponse response = mockingPostResponse(); + given(postQueryService.getPost(anyLong(), any(Member.class))).willReturn(response); + + // when + PostResponse result = RestAssuredMockMvc.given().log().all() + .headers(HttpHeaders.AUTHORIZATION, "Bearer token") + .when().get("/posts/{postId}", 1) + .then().log().all() + .status(HttpStatus.OK) + .extract() + .as(PostResponse.class); + + // then + assertThat(result).usingRecursiveComparison().isEqualTo(response); + } + + @ParameterizedTest + @ValueSource(longs = {-1, 0}) + @DisplayName("게시글 ID가 양의 정수가 아니라면 400 응답을 반환한다.") + void return400postIdNegative(Long postId) throws Exception { + // given + mockingAuthArgumentResolver(); + + // when, then + RestAssuredMockMvc.given().log().all() + .headers(HttpHeaders.AUTHORIZATION, "Bearer token") + .when().get("/posts/{postId}", postId) + .then().log().all() + .status(HttpStatus.BAD_REQUEST) + .body("code", equalTo(201)) + .body("message", containsString("게시글 ID는 양의 정수만 가능합니다.")); + } + + } + + @Nested + @DisplayName("게시글 검색") + class SearchPosts { + + @Test + @DisplayName("게시글 검색에 성공하면 200 응답을 반환한다.") + void return200success() throws Exception { + // given + mockingAuthArgumentResolver(); + List response = List.of(mockingPostResponse()); + given(postQueryService.searchPosts( + anyString(), + anyInt(), + any(PostClosingType.class), + any(PostSortType.class), + any(Member.class) + )).willReturn(response); + + // when + List result = RestAssuredMockMvc.given().log().all() + .headers(HttpHeaders.AUTHORIZATION, "Bearer token") + .param("keyword", "hello") + .param("page", 0) + .param("postClosingType", PostClosingType.ALL) + .param("postSortType", PostSortType.LATEST) + .when().get("/posts/search") + .then().log().all() + .status(HttpStatus.OK) + .extract() + .as(new TypeRef<>() { + }); + + // then + assertThat(result).usingRecursiveComparison().isEqualTo(response); + } + + @Test + @DisplayName("페이지가 음수라면 400 응답을 반환한다.") + void return400pageNegative() throws Exception { + // given + mockingAuthArgumentResolver(); + + // when, then + RestAssuredMockMvc.given().log().all() + .headers(HttpHeaders.AUTHORIZATION, "Bearer token") + .param("keyword", "hello") + .param("page", -1) + .param("postClosingType", PostClosingType.ALL) + .param("postSortType", PostSortType.LATEST) + .when().get("/posts/search") + .then().log().all() + .status(HttpStatus.BAD_REQUEST) + .body("code", equalTo(201)) + .body("message", containsString("페이지는 0이상 정수만 가능합니다.")); + } + + } + + @Nested + @DisplayName("내가 쓴 게시글 조회") + class GetPostsWrittenByMe { + + @Test + @DisplayName("내가 쓴 게시글 조회에 성공하면 200 응답을 반환한다.") + void return200success() throws Exception { + // given + mockingAuthArgumentResolver(); + List response = List.of(mockingPostResponse()); + given(postQueryService.getPostsWrittenByMe( + anyInt(), + any(PostClosingType.class), + any(PostSortType.class), + any(Member.class) + )).willReturn(response); + + // when + List result = RestAssuredMockMvc.given().log().all() + .headers(HttpHeaders.AUTHORIZATION, "Bearer token") + .param("page", 0) + .param("postClosingType", PostClosingType.ALL) + .param("postSortType", PostSortType.LATEST) + .when().get("/posts/me") + .then().log().all() + .status(HttpStatus.OK) + .extract() + .as(new TypeRef<>() { + }); + + // then + assertThat(result).usingRecursiveComparison().isEqualTo(response); + } + + @Test + @DisplayName("페이지가 음수라면 400 응답을 반환한다.") + void return400pageNegative() throws Exception { + // given + mockingAuthArgumentResolver(); + + // when, then + RestAssuredMockMvc.given().log().all() + .headers(HttpHeaders.AUTHORIZATION, "Bearer token") + .param("page", -1) + .param("postClosingType", PostClosingType.ALL) + .param("postSortType", PostSortType.LATEST) + .when().get("/posts/me") + .then().log().all() + .status(HttpStatus.BAD_REQUEST) + .body("code", equalTo(201)) + .body("message", containsString("페이지는 0이상 정수만 가능합니다.")); + } + + } + + @Nested + @DisplayName("내가 투표한 게시글 조회") + class GetPostsVotedByMe { + + @Test + @DisplayName("내가 투표한 게시글 조회에 성공하면 200 응답을 반환한다.") + void return200success() throws Exception { + // given + mockingAuthArgumentResolver(); + List response = List.of(mockingPostResponse()); + given(postQueryService.getPostsVotedByMe( + anyInt(), + any(PostClosingType.class), + any(PostSortType.class), + any(Member.class) + )).willReturn(response); + + // when + List result = RestAssuredMockMvc.given().log().all() + .headers(HttpHeaders.AUTHORIZATION, "Bearer token") + .param("page", 0) + .param("postClosingType", PostClosingType.ALL) + .param("postSortType", PostSortType.LATEST) + .when().get("/posts/votes/me") + .then().log().all() + .status(HttpStatus.OK) + .extract() + .as(new TypeRef<>() { + }); + + // then + assertThat(result).usingRecursiveComparison().isEqualTo(response); + } + + @Test + @DisplayName("페이지가 음수라면 400 응답을 반환한다.") + void return400pageNegative() throws Exception { + // given + mockingAuthArgumentResolver(); + + // when, then + RestAssuredMockMvc.given().log().all() + .headers(HttpHeaders.AUTHORIZATION, "Bearer token") + .param("page", -1) + .param("postClosingType", PostClosingType.ALL) + .param("postSortType", PostSortType.LATEST) + .when().get("/posts/votes/me") + .then().log().all() + .status(HttpStatus.BAD_REQUEST) + .body("code", equalTo(201)) + .body("message", containsString("페이지는 0이상 정수만 가능합니다.")); + } + + } + + @Nested + @DisplayName("게시글 투표 통계 조회") + class GetPostVoteStatistics { + + @Test + @DisplayName("게시글 투표 통계 조회에 성공하면 200 응답을 반환한다.") + void return200success() throws Exception { + // given + mockingAuthArgumentResolver(); + VoteOptionStatisticsResponse response = mockingVoteOptionStatisticsResponse(); + given(postQueryService.getVoteStatistics(anyLong(), any(Member.class))).willReturn(response); + + // when + VoteOptionStatisticsResponse result = RestAssuredMockMvc.given().log().all() + .headers(HttpHeaders.AUTHORIZATION, "Bearer token") + .when().get("/posts/{postId}/options", 1L) + .then().log().all() + .status(HttpStatus.OK) + .extract() + .as(VoteOptionStatisticsResponse.class); + + // then + assertThat(result).usingRecursiveComparison().isEqualTo(response); + } + + @ParameterizedTest + @ValueSource(longs = {-1, 0}) + @DisplayName("게시글 ID가 양의 정수가 아니라면 400 응답을 반환한다.") + void return400postIdNegative(Long postId) throws Exception { + // given + mockingAuthArgumentResolver(); + + // when, then + RestAssuredMockMvc.given().log().all() + .headers(HttpHeaders.AUTHORIZATION, "Bearer token") + .when().get("/posts/{postId}/options", postId) + .then().log().all() + .status(HttpStatus.BAD_REQUEST) + .body("code", equalTo(201)) + .body("message", containsString("게시글 ID는 양의 정수만 가능합니다.")); + } + + } + + @Nested + @DisplayName("게시글 투표 옵션 통계 조회") + class GetPostOptionVoteStatistics { + + @Test + @DisplayName("게시글 투표 옵션 통계 조회에 성공하면 200 응답을 반환한다.") + void return200success() throws Exception { + // given + mockingAuthArgumentResolver(); + VoteOptionStatisticsResponse response = mockingVoteOptionStatisticsResponse(); + given(postQueryService.getVoteOptionStatistics( + anyLong(), + anyLong(), + any(Member.class) + )).willReturn(response); + + // when + VoteOptionStatisticsResponse result = RestAssuredMockMvc.given().log().all() + .headers(HttpHeaders.AUTHORIZATION, "Bearer token") + .when().get("/posts/{postId}/options/{optionId}", 1L, 1L) + .then().log().all() + .status(HttpStatus.OK) + .extract() + .as(VoteOptionStatisticsResponse.class); + + // then + assertThat(result).usingRecursiveComparison().isEqualTo(response); + } + + @ParameterizedTest + @ValueSource(longs = {-1, 0}) + @DisplayName("게시글 ID가 양의 정수가 아니라면 400 응답을 반환한다.") + void return400postIdNegative(Long postId) throws Exception { + // given + mockingAuthArgumentResolver(); + + // when, then + RestAssuredMockMvc.given().log().all() + .headers(HttpHeaders.AUTHORIZATION, "Bearer token") + .when().get("/posts/{postId}/options/{optionId}", postId, 1L) + .then().log().all() + .status(HttpStatus.BAD_REQUEST) + .body("code", equalTo(201)) + .body("message", containsString("게시글 ID는 양의 정수만 가능합니다.")); + } + + @ParameterizedTest + @ValueSource(longs = {-1, 0}) + @DisplayName("게시글 투표 옵션 ID가 양의 정수가 아니라면 400 응답을 반환한다.") + void return400postOptionIdNegative(Long postOptionId) throws Exception { + // given + mockingAuthArgumentResolver(); + + // when, then + RestAssuredMockMvc.given().log().all() + .headers(HttpHeaders.AUTHORIZATION, "Bearer token") + .when().get("/posts/{postId}/options/{optionId}", 1L, postOptionId) + .then().log().all() + .status(HttpStatus.BAD_REQUEST) + .body("code", equalTo(201)) + .body("message", containsString("게시글 옵션 ID는 양의 정수만 가능합니다.")); + } + + } + + private PostResponse mockingPostResponse() { + return new PostResponse( + 1L, + new PostWriterResponse(1L, "votogether"), + "title", + "content", + "https://votogether.com/static/images/image.png", + List.of( + new CategoryResponse(1L, "develop") + ), + LocalDateTime.now().truncatedTo(ChronoUnit.MINUTES), + LocalDateTime.now().truncatedTo(ChronoUnit.MINUTES), + 3, + 15, + new PostVoteResultResponse( + 0L, + -1, + List.of( + new PostOptionVoteResultResponse( + 1L, + "content", + "image.png", + -1, + 0 + ) + ) + ) + ); + } + + private VoteOptionStatisticsResponse mockingVoteOptionStatisticsResponse() { + return new VoteOptionStatisticsResponse( + 10, + 5, + 5, + List.of( + new VoteCountForAgeGroupResponse( + "10대", + 2, + 1, + 1 + ), + new VoteCountForAgeGroupResponse( + "20대", + 3, + 1, + 2 + ) + ) + ); + } + +} diff --git a/backend/src/test/java/com/votogether/domain/post/entity/PostBodyTest.java b/backend/src/test/java/com/votogether/domain/post/entity/PostBodyTest.java new file mode 100644 index 000000000..49b3a4bb1 --- /dev/null +++ b/backend/src/test/java/com/votogether/domain/post/entity/PostBodyTest.java @@ -0,0 +1,90 @@ +package com.votogether.domain.post.entity; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +import com.votogether.global.exception.BadRequestException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; + +class PostBodyTest { + + @Nested + @DisplayName("게시글 바디 생성") + class PostBodyCreate { + + @Test + @DisplayName("게시글 제목과 내용이 정상적이라면 게시글 바디를 생성한다.") + void success() { + // given + String title = "votogether"; + String content = "안녕 모두들 hello world!"; + + // when + PostBody postBody = new PostBody(title, content); + + // then + assertSoftly(softly -> { + softly.assertThat(postBody.getTitle()).isEqualTo(title); + softly.assertThat(postBody.getContent()).isEqualTo(content); + }); + } + + @ParameterizedTest + @NullAndEmptySource + @DisplayName("게시글 제목이 존재하지 않거나 공백이라면 예외를 던진다.") + void emptyTitle(String title) { + // given + String content = "안녕 모두들 hello world!"; + + // when, then + assertThatThrownBy(() -> new PostBody(title, content)) + .isInstanceOf(BadRequestException.class) + .hasMessage("게시글 제목은 비어있거나 공백일 수 없습니다."); + } + + @Test + @DisplayName("게시글 제목 길이가 유효하지 않으면 예외를 던진다.") + void invalidTitleLength() { + // given + String title = "a".repeat(101); + String content = "안녕 모두들 hello world!"; + + // when, then + assertThatThrownBy(() -> new PostBody(title, content)) + .isInstanceOf(BadRequestException.class) + .hasMessage("게시글 제목 길이가 유효하지 않습니다."); + } + + @ParameterizedTest + @NullAndEmptySource + @DisplayName("게시글 내용이 존재하지 않거나 공백이라면 예외를 던진다.") + void emptyContent(String content) { + // given + String title = "votogether"; + + // when, then + assertThatThrownBy(() -> new PostBody(title, content)) + .isInstanceOf(BadRequestException.class) + .hasMessage("게시글 내용은 비어있거나 공백일 수 없습니다."); + } + + @Test + @DisplayName("게시글 내용 길이가 유효하지 않으면 예외를 던진다.") + void invalidContentLength() { + // given + String title = "votogether"; + String content = "a".repeat(1001); + + // when, then + assertThatThrownBy(() -> new PostBody(title, content)) + .isInstanceOf(BadRequestException.class) + .hasMessage("게시글 내용 길이가 유효하지 않습니다."); + } + + } + +} diff --git a/backend/src/test/java/com/votogether/domain/post/entity/PostCategoriesTest.java b/backend/src/test/java/com/votogether/domain/post/entity/PostCategoriesTest.java deleted file mode 100644 index 271ebf660..000000000 --- a/backend/src/test/java/com/votogether/domain/post/entity/PostCategoriesTest.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.votogether.domain.post.entity; - -import static org.assertj.core.api.Assertions.assertThat; - -import com.votogether.domain.category.entity.Category; -import java.util.List; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -class PostCategoriesTest { - - @Test - @DisplayName("여러 Category를 전달하면 Post와 매핑되어 PostOptions를 생성한다") - void mapPostAndCategories() { - // given - PostCategories postCategories = new PostCategories(); - Post post = Post.builder().build(); - Category categoryA = Category.builder().build(); - Category categoryB = Category.builder().build(); - - List categories = List.of(categoryA, categoryB); - - // when - postCategories.mapPostAndCategories(post, categories); - - // then - assertThat(postCategories.getPostCategories()).hasSize(2); - } - -} diff --git a/backend/src/test/java/com/votogether/domain/post/entity/PostDeadlineTest.java b/backend/src/test/java/com/votogether/domain/post/entity/PostDeadlineTest.java new file mode 100644 index 000000000..60eb50d54 --- /dev/null +++ b/backend/src/test/java/com/votogether/domain/post/entity/PostDeadlineTest.java @@ -0,0 +1,77 @@ +package com.votogether.domain.post.entity; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.votogether.global.exception.BadRequestException; +import java.time.LocalDateTime; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +class PostDeadlineTest { + + @Nested + @DisplayName("게시글 마감시간 생성") + class PostDeadlineCreate { + + @Test + @DisplayName("게시글 마감시간이 정상적이라면 게시글 마감시간을 생성한다.") + void success() { + // given + LocalDateTime deadline = LocalDateTime.now().plusDays(14); + + // when + PostDeadline postDeadline = new PostDeadline(deadline); + + // then + assertThat(postDeadline.getDeadline()).isEqualTo(deadline); + } + + @Test + @DisplayName("게시글 마감시간이 최대 마감시간을 초과하면 예외를 던진다.") + void invalidDeadline() { + // given + LocalDateTime deadline = LocalDateTime.now().plusDays(15); + + // when, then + assertThatThrownBy(() -> new PostDeadline(deadline)) + .isInstanceOf(BadRequestException.class) + .hasMessage("최대 마감기간을 초과하였습니다."); + } + + } + + @Nested + @DisplayName("게시글 마감 여부 확인") + class IsClosed { + + @Test + @DisplayName("게시글이 마감되었으면 true 반환한다.") + void isClosed() { + // given + LocalDateTime deadline = LocalDateTime.now().minusDays(1); + + // when + PostDeadline postDeadline = new PostDeadline(deadline); + + // then + assertThat(postDeadline.isClosed()).isTrue(); + } + + @Test + @DisplayName("게시글이 마감되지 않았으면 false 반환한다.") + void isOpen() { + // given + LocalDateTime deadline = LocalDateTime.now().plusDays(1); + + // when + PostDeadline postDeadline = new PostDeadline(deadline); + + // then + assertThat(postDeadline.isClosed()).isFalse(); + } + + } + +} diff --git a/backend/src/test/java/com/votogether/domain/post/entity/PostOptionBodyTest.java b/backend/src/test/java/com/votogether/domain/post/entity/PostOptionBodyTest.java new file mode 100644 index 000000000..895ba0136 --- /dev/null +++ b/backend/src/test/java/com/votogether/domain/post/entity/PostOptionBodyTest.java @@ -0,0 +1,56 @@ +package com.votogether.domain.post.entity; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.votogether.global.exception.BadRequestException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; + +class PostOptionBodyTest { + + @Nested + @DisplayName("게시글 옵션 본문 생성") + class PostOptionBodyCreate { + + @Test + @DisplayName("게시글 옵션 본문이 정상적이라면 게시글 옵션 본문을 생성한다.") + void success() { + // given + String content = "votogether"; + + // when + PostOptionBody postOptionBody = new PostOptionBody(content); + + // then + assertThat(postOptionBody.getContent()).isEqualTo(content); + } + + @ParameterizedTest + @NullAndEmptySource + @DisplayName("게시글 옵션 본문이 존재하지 않거나 공백이라면 예외를 던진다.") + void emptyContent(String content) { + // given, when, then + assertThatThrownBy(() -> new PostOptionBody(content)) + .isInstanceOf(BadRequestException.class) + .hasMessage("게시글 옵션 내용은 비어있거나 공백일 수 없습니다."); + } + + @Test + @DisplayName("게시글 옵션 본문 길이가 유효하지 않으면 예외를 던진다.") + void invalidContentLength() { + // given + String content = "a".repeat(51); + + // when, then + assertThatThrownBy(() -> new PostOptionBody(content)) + .isInstanceOf(BadRequestException.class) + .hasMessage("게시글 옵션 내용 길이가 유효하지 않습니다."); + } + + } + +} diff --git a/backend/src/test/java/com/votogether/domain/post/entity/PostOptionTest.java b/backend/src/test/java/com/votogether/domain/post/entity/PostOptionTest.java index 0eb9d854c..a1563393c 100644 --- a/backend/src/test/java/com/votogether/domain/post/entity/PostOptionTest.java +++ b/backend/src/test/java/com/votogether/domain/post/entity/PostOptionTest.java @@ -1,28 +1,93 @@ package com.votogether.domain.post.entity; -import static com.votogether.test.fixtures.MemberFixtures.MALE_30; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.SoftAssertions.assertSoftly; -import com.votogether.domain.vote.entity.Vote; +import java.time.LocalDateTime; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.springframework.test.util.ReflectionTestUtils; class PostOptionTest { @Test - @DisplayName("해당 선택지를 현재 회원이 투표한 건지 확인한다") - void isVoteByMember() { + @DisplayName("게시글 옵션을 수정한다.") + void update() { // given - PostOption postOption = PostOption.builder().build(); - - Vote vote = Vote.builder().member(MALE_30.get()).build(); - postOption.getVotes().add(vote); + PostOption postOption = PostOption.builder() + .content("hello") + .imageUrl(null) + .build(); + String newContent = "votogether"; + String newImageUrl = "image.png"; // when - boolean isVoteByMember = postOption.hasMemberVote(MALE_30.get()); + postOption.update(newContent, newImageUrl); // then - assertThat(isVoteByMember).isTrue(); + assertSoftly(softly -> { + softly.assertThat(postOption.getContent()).isEqualTo(newContent); + softly.assertThat(postOption.getImageUrl()).isEqualTo(newImageUrl); + }); + } + + @Nested + @DisplayName("속한 게시글 확인") + class BelongsToPost { + + @Test + @DisplayName("게시글에 속해있으면 true 반환한다.") + void belongsTo() { + // given + Post post = Post.builder() + .title("hello") + .content("world") + .deadline(LocalDateTime.now()) + .build(); + PostOption postOption = PostOption.builder() + .post(post) + .content("hello") + .imageUrl(null) + .build(); + ReflectionTestUtils.setField(post, "id", 1L); + + // when + boolean result = postOption.belongsTo(post); + + // then + assertThat(result).isTrue(); + } + + @Test + @DisplayName("게시글에 속해있지 않으면 false 반환한다") + void notBelongsTo() { + // given + Post postA = Post.builder() + .title("hello") + .content("world") + .deadline(LocalDateTime.now()) + .build(); + Post postB = Post.builder() + .title("hello") + .content("world") + .deadline(LocalDateTime.now()) + .build(); + PostOption postOption = PostOption.builder() + .post(postB) + .content("hello") + .imageUrl(null) + .build(); + ReflectionTestUtils.setField(postA, "id", 1L); + ReflectionTestUtils.setField(postB, "id", 2L); + + // when + boolean result = postOption.belongsTo(postA); + + // then + assertThat(result).isFalse(); + } + } } diff --git a/backend/src/test/java/com/votogether/domain/post/entity/PostTest.java b/backend/src/test/java/com/votogether/domain/post/entity/PostTest.java index 9b04243ed..ee5f7c669 100644 --- a/backend/src/test/java/com/votogether/domain/post/entity/PostTest.java +++ b/backend/src/test/java/com/votogether/domain/post/entity/PostTest.java @@ -1,205 +1,244 @@ package com.votogether.domain.post.entity; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatNoException; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.junit.jupiter.api.Assertions.assertAll; +import static org.assertj.core.api.SoftAssertions.assertSoftly; -import com.votogether.domain.category.entity.Category; import com.votogether.domain.member.entity.Member; -import com.votogether.domain.post.exception.PostExceptionType; -import com.votogether.global.exception.BadRequestException; -import com.votogether.global.util.ImageUploader; import com.votogether.test.fixtures.MemberFixtures; -import java.nio.charset.StandardCharsets; import java.time.LocalDateTime; -import java.util.List; -import java.util.stream.Stream; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import org.springframework.http.MediaType; -import org.springframework.mock.web.MockMultipartFile; import org.springframework.test.util.ReflectionTestUtils; class PostTest { @Test - @DisplayName("여러 Category를 전달하면 Post와 매핑되어 PostOptions를 생성한다") - void mapCategories() { + @DisplayName("게시글 옵션을 삭제한다.") + void removePostOption() { // given - Post post = Post.builder().build(); - Category categoryA = Category.builder().build(); - Category categoryB = Category.builder().build(); - - List categories = List.of(categoryA, categoryB); + Post post = Post.builder() + .title("hello") + .content("world") + .deadline(LocalDateTime.now()) + .build(); + PostOption postOption = PostOption.builder() + .content("hello") + .imageUrl(null) + .build(); + post.addPostOption(postOption); + ReflectionTestUtils.setField(postOption, "id", 1L); // when - post.mapCategories(categories); + post.removePostOption(postOption); // then - PostCategories actualPostCategories = post.getPostCategories(); - assertThat(actualPostCategories.getPostCategories()).hasSize(2); + assertThat(post.getPostOptions()).isEmpty(); } @Test - @DisplayName("PostOption의 내용을 전달하면 Post와 PostOption이 매핑된다") - void mapPostOptionsByElements() { + @DisplayName("게시글을 수정한다.") + void update() { // given - Post post = Post.builder().build(); - - byte[] content = "Hello, World!".getBytes(StandardCharsets.UTF_8); - MockMultipartFile file1 = new MockMultipartFile( - "file1", - "hello1.txt", - MediaType.TEXT_PLAIN_VALUE, - content - ); - - MockMultipartFile file2 = new MockMultipartFile( - "file2", - "hello2.txt", - MediaType.TEXT_PLAIN_VALUE, - content - ); - - final List imageUrls = Stream.of(file1, file2) - .map(ImageUploader::upload) - .toList(); + Post post = Post.builder() + .title("hello") + .content("world") + .deadline(LocalDateTime.now()) + .build(); + String newTitle = "hello world"; + String newContent = "votogether"; + LocalDateTime newDeadline = LocalDateTime.now().plusDays(1); // when - post.mapPostOptionsByElements(List.of("content1", "content2"), imageUrls); + post.update(newTitle, newContent, newDeadline); // then - List postOptions = post.getPostOptions().getPostOptions(); - assertThat(postOptions).hasSize(2); + assertSoftly(softly -> { + softly.assertThat(post.getTitle()).isEqualTo(newTitle); + softly.assertThat(post.getContent()).isEqualTo(newContent); + softly.assertThat(post.getDeadline()).isEqualTo(newDeadline); + }); } - @Test - @DisplayName("게시글 작성 시, 게시글의 마감 기한이 현재 시간보다 3일 초과 여부에 따라 예외를 던질 지 결정한다.") - void throwExceptionIsWriter() { - // given - final Member writer = MemberFixtures.MALE_30.get(); - ReflectionTestUtils.setField(writer, "id", 1L); - - Post post1 = Post.builder() - .writer(writer) - .deadline(LocalDateTime.now().plusDays(4)) - .build(); + @Nested + @DisplayName("삭제 가능 여부 확인") + class CanDelete { + + @Test + @DisplayName("투표 수가 삭제 제한 개수보다 작으면 true 반환한다.") + void canDelete() { + // given + Post post = Post.builder() + .title("hello") + .content("world") + .deadline(LocalDateTime.now()) + .build(); + PostOption postOption = PostOption.builder() + .content("hello") + .imageUrl(null) + .build(); + ReflectionTestUtils.setField(postOption, "voteCount", 19); + post.addPostOption(postOption); + + // when + boolean result = post.canDelete(); + + // then + assertThat(result).isTrue(); + } + + @Test + @DisplayName("투표 수가 삭제 제한 개수보다 많으면 false 반환한다.") + void cannotDelete() { + // given + Post post = Post.builder() + .title("hello") + .content("world") + .deadline(LocalDateTime.now()) + .build(); + PostOption postOption = PostOption.builder() + .content("hello") + .imageUrl(null) + .build(); + ReflectionTestUtils.setField(postOption, "voteCount", 20); + post.addPostOption(postOption); + + // when + boolean result = post.canDelete(); + + // then + assertThat(result).isFalse(); + } - Post post2 = Post.builder() - .writer(writer) - .deadline(LocalDateTime.now().plusDays(2)) - .build(); - - // when, then - assertAll( - () -> assertThatThrownBy(() -> post1.validateDeadlineNotExceedByMaximumDeadline(3)) - .isInstanceOf(BadRequestException.class) - .hasMessage(PostExceptionType.DEADLINE_EXCEED_THREE_DAYS.getMessage()), - () -> assertThatNoException() - .isThrownBy(() -> post2.validateDeadlineNotExceedByMaximumDeadline(3)) - ); } - @Test - @DisplayName("게시글의 마감 여부에 따라 예외를 던질 지 결정한다.") - void throwExceptionIsDeadlinePassed() { - // given - final Member writer = MemberFixtures.MALE_30.get(); - ReflectionTestUtils.setField(writer, "id", 1L); - - Post post1 = Post.builder() - .writer(writer) - .deadline(LocalDateTime.of(2000, 1, 1, 1, 1)) - .build(); - - Post post2 = Post.builder() - .writer(writer) - .deadline(LocalDateTime.of(9999, 1, 1, 1, 1)) - .build(); + @Nested + @DisplayName("작성자 확인") + class IsWriter { + + @Test + @DisplayName("게시글 작성자라면 true 반환한다.") + void isWriter() { + // given + Member member = MemberFixtures.MALE_20.get(); + Post post = Post.builder() + .writer(member) + .title("hello") + .content("world") + .deadline(LocalDateTime.now()) + .build(); + ReflectionTestUtils.setField(member, "id", 1L); + + // when + boolean result = post.isWriter(member); + + // then + assertThat(result).isTrue(); + } + + @Test + @DisplayName("게시글 작성자가 아니라면 false 반환한다.") + void isNotWriter() { + // given + Member writer = MemberFixtures.MALE_20.get(); + Member member = MemberFixtures.MALE_30.get(); + Post post = Post.builder() + .writer(writer) + .title("hello") + .content("world") + .deadline(LocalDateTime.now()) + .build(); + ReflectionTestUtils.setField(writer, "id", 1L); + ReflectionTestUtils.setField(member, "id", 2L); + + // when + boolean result = post.isWriter(member); + + // then + assertThat(result).isFalse(); + } - // when, then - assertAll( - () -> assertThatThrownBy(post1::validateDeadLine) - .isInstanceOf(BadRequestException.class) - .hasMessage(PostExceptionType.POST_CLOSED.getMessage()), - () -> assertThatNoException() - .isThrownBy(post2::validateDeadLine) - ); } - @Test - @DisplayName("해당 게시글을 조기 마감 합니다.") - void closedEarly() { - // given - LocalDateTime deadline = LocalDateTime.of(2100, 1, 1, 0, 0); - Post post = Post.builder() - .deadline(deadline) - .build(); - - // when - post.closeEarly(); + @Nested + @DisplayName("게시글 블라인드") + class Blind { + + @Test + @DisplayName("게시글 생성 시 블라인드 되어 있지 않다.") + void initNotBlind() { + // given + Post post = Post.builder() + .title("hello") + .content("world") + .deadline(LocalDateTime.now()) + .build(); + + // when, then + assertThat(post.isHidden()).isFalse(); + } + + @Test + @DisplayName("게시글을 블라인드한다.") + void blind() { + // given + Post post = Post.builder() + .title("hello") + .content("world") + .deadline(LocalDateTime.now()) + .build(); + + // when + post.blind(); + + // then + assertThat(post.isHidden()).isTrue(); + } - // then - assertThat(post.getDeadline()).isBefore(deadline); } - @Test - @DisplayName("게시글을 수정한다.") - void update() { - // given - final Member writer = MemberFixtures.MALE_30.get(); - final PostBody postBody1 = PostBody.builder() - .title("title1") - .content("content1") - .build(); - final Post post = Post.builder() - .writer(writer) - .postBody(postBody1) - .deadline(LocalDateTime.now().plusDays(3)) - .build(); - post.addContentImage("없는사진"); - - final PostBody postBody2 = PostBody.builder() - .title("title2") - .content("content2") - .build(); - - Category categoryA = Category.builder().name("category1").build(); - Category categoryB = Category.builder().name("category2").build(); - - List categories = List.of(categoryA, categoryB); + @Nested + @DisplayName("게시글 첫번째 이미지 조회") + class GetFirstImage { + + @Test + @DisplayName("게시글 이미지가 존재하지 않으면 null 반환한다.") + void returnNull() { + // given + Post post = Post.builder() + .title("hello") + .content("world") + .deadline(LocalDateTime.now()) + .build(); + + // when + PostContentImage result = post.getFirstContentImage(); + + // then + assertThat(result).isNull(); + } + + @Test + @DisplayName("게시글 이미지가 존재하면 첫번째 이미지를 반환한다.") + void returnFirstImage() { + // given + Post post = Post.builder() + .title("hello") + .content("world") + .deadline(LocalDateTime.now()) + .build(); + post.addContentImage("image.png"); + + // when + PostContentImage result = post.getFirstContentImage(); + + // then + assertSoftly(softly -> { + softly.assertThat(result).isNotNull(); + softly.assertThat(result.getImageUrl()).isEqualTo("image.png"); + }); + } - // when - final LocalDateTime deadline = LocalDateTime.now().plusDays(2); - post.update( - postBody2, - "oldContentUrl", - List.of("newContentUrl2"), - categories, - List.of("option1", "option2"), - List.of("optionImage1", "optionImage2"), - List.of("newOptionImage1", "newOptionImage2"), - deadline - ); - - // then - final PostBody postBody = post.getPostBody(); - final PostContentImage postContentImage = postBody.getPostContentImages().getContentImages().get(0); - final List postOptions = post.getPostOptions().getPostOptions(); - final List postCategories = post.getPostCategories().getPostCategories(); - final LocalDateTime actualDeadline = post.getDeadline(); - assertAll( - () -> assertThat(postBody.getTitle()).isEqualTo("title2"), - () -> assertThat(postBody.getContent()).isEqualTo("content2"), - () -> assertThat(postContentImage.getImageUrl()).isEqualTo("newContentUrl2"), - () -> assertThat(postOptions.get(0).getContent()).isEqualTo("option1"), - () -> assertThat(postOptions.get(0).getImageUrl()).isEqualTo("newOptionImage1"), - () -> assertThat(postCategories.get(0).getCategory().getName()).isEqualTo("category1"), - () -> assertThat(actualDeadline).hasYear(deadline.getYear()), - () -> assertThat(actualDeadline).hasMonth(deadline.getMonth()), - () -> assertThat(actualDeadline).hasDayOfMonth(deadline.getDayOfMonth()) - ); } } diff --git a/backend/src/test/java/com/votogether/domain/post/entity/comment/CommentTest.java b/backend/src/test/java/com/votogether/domain/post/entity/comment/CommentTest.java index 8d3fc2e5e..33d83019c 100644 --- a/backend/src/test/java/com/votogether/domain/post/entity/comment/CommentTest.java +++ b/backend/src/test/java/com/votogether/domain/post/entity/comment/CommentTest.java @@ -1,38 +1,40 @@ package com.votogether.domain.post.entity.comment; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import com.votogether.domain.member.entity.Member; import com.votogether.domain.post.entity.Post; -import com.votogether.domain.post.entity.PostBody; import com.votogether.global.exception.BadRequestException; import com.votogether.test.fixtures.MemberFixtures; import java.time.LocalDateTime; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.test.util.ReflectionTestUtils; class CommentTest { + private Post generatePost() { + return Post.builder() + .writer(MemberFixtures.FEMALE_20.get()) + .title("title") + .content("content") + .deadline(LocalDateTime.now()) + .build(); + } + @Test @DisplayName("댓글 내용이 최대 글자를 초과하면 예외를 던진다.") void invalidContentLength() { // given String content = "a".repeat(501); - PostBody body = PostBody.builder() - .title("title") - .content("content") - .build(); - Post post = Post.builder() - .writer(MemberFixtures.FEMALE_20.get()) - .postBody(body) - .deadline(LocalDateTime.now()) - .build(); + Post post = generatePost(); // when, then assertThatThrownBy( () -> Comment.builder() - .member(MemberFixtures.MALE_20.get()) + .writer(MemberFixtures.MALE_20.get()) .post(post) .content(content) .build() @@ -41,94 +43,144 @@ void invalidContentLength() { .hasMessage("유효하지 않은 댓글 길이입니다."); } - @Test - @DisplayName("댓글 작성자가 아니라면 예외를 던진다.") - void invalidWriter() { - // given - Member member = MemberFixtures.FEMALE_20.get(); - PostBody body = PostBody.builder() - .title("title") - .content("content") - .build(); - Post post = Post.builder() - .writer(member) - .postBody(body) - .deadline(LocalDateTime.now()) - .build(); - Comment comment = Comment.builder() - .member(member) - .post(post) - .content("content") - .build(); + @Nested + @DisplayName("게시글 댓글 확인") + class PostBelongCheck { - ReflectionTestUtils.setField(member, "id", 1L); + @Test + @DisplayName("게시글의 댓글이라면 true 반환한다.") + void belongsToPost() { + // given + Post post = generatePost(); + Comment comment = Comment.builder() + .writer(MemberFixtures.MALE_20.get()) + .post(post) + .content("hello") + .build(); + + // when, then + assertThat(comment.belongsTo(post)).isTrue(); + } + + @Test + @DisplayName("게시글의 댓글이 아니라면 false 반환한다.") + void notBelongToPost() { + // given + Post post = generatePost(); + Post otherPost = generatePost(); + Comment comment = Comment.builder() + .writer(MemberFixtures.MALE_20.get()) + .post(post) + .content("hello") + .build(); + ReflectionTestUtils.setField(post, "id", 1L); + + // when, then + assertThat(comment.belongsTo(otherPost)).isFalse(); + } - // when, then - assertThatThrownBy(() -> comment.validateWriter(MemberFixtures.MALE_20.get())) - .isInstanceOf(BadRequestException.class) - .hasMessage("댓글 작성자가 아닙니다."); } - @Test - @DisplayName("작성되어 있는 게시글이 아니라면 예외를 던진다.") - void invalidPost() { - // given - Member member = MemberFixtures.FEMALE_20.get(); - PostBody bodyA = PostBody.builder() - .title("title") - .content("content") - .build(); - Post postA = Post.builder() - .writer(member) - .postBody(bodyA) - .deadline(LocalDateTime.now()) - .build(); - PostBody bodyB = PostBody.builder() - .title("title") - .content("content") - .build(); - Post postB = Post.builder() - .writer(member) - .postBody(bodyB) - .deadline(LocalDateTime.now()) - .build(); - Comment comment = Comment.builder() - .member(member) - .post(postA) - .content("content") - .build(); + @Nested + @DisplayName("댓글 작성자 확인") + class WriterCheck { - ReflectionTestUtils.setField(postA, "id", 1L); + @Test + @DisplayName("작성자라면 true 반환한다.") + void isWriter() { + // given + Member male20Member = MemberFixtures.MALE_20.get(); + Post post = generatePost(); + Comment comment = Comment.builder() + .writer(male20Member) + .post(post) + .content("hello") + .build(); + ReflectionTestUtils.setField(male20Member, "id", 1L); + + // when, then + assertThat(comment.isWriter(male20Member)).isTrue(); + } + + @Test + @DisplayName("작성자가 아니라면 false 반환한다.") + void isNotWriter() { + // given + Member male20Member = MemberFixtures.MALE_20.get(); + Member female20Member = MemberFixtures.FEMALE_20.get(); + Post post = generatePost(); + Comment comment = Comment.builder() + .writer(male20Member) + .post(post) + .content("hello") + .build(); + ReflectionTestUtils.setField(male20Member, "id", 1L); + + // when, then + assertThat(comment.isWriter(female20Member)).isFalse(); + } + + } + + @Nested + @DisplayName("댓글 내용 수정") + class UpdateContent { + + @Test + @DisplayName("유효한 댓글이라면 댓글 내용이 수정된다.") + void updateContent() { + // given + Post post = generatePost(); + Comment comment = Comment.builder() + .writer(MemberFixtures.MALE_20.get()) + .post(post) + .content("hello") + .build(); + String newContent = "votogether"; + + // when + comment.updateContent(newContent); + + // then + assertThat(comment.getContent()).isEqualTo("votogether"); + } + + @Test + @DisplayName("유효하지 않은 댓글이라면 예외를 던진다.") + void updateContentFail() { + // given + Post post = generatePost(); + Comment comment = Comment.builder() + .writer(MemberFixtures.MALE_20.get()) + .post(post) + .content("hello") + .build(); + String newContent = "a".repeat(501); + + // when, then + assertThatThrownBy(() -> comment.updateContent(newContent)) + .isInstanceOf(BadRequestException.class) + .hasMessage("유효하지 않은 댓글 길이입니다."); + } - // when, then - assertThatThrownBy(() -> comment.validateBelong(postB)) - .isInstanceOf(BadRequestException.class) - .hasMessage("댓글의 게시글 정보와 일치하지 않습니다."); } @Test - @DisplayName("댓글 수정 시 최대 글자를 초과하면 예외를 던진다.") - void updateContentWithInvalidContentLength() { + @DisplayName("댓글을 숨긴다.") + void blind() { // given - PostBody body = PostBody.builder() - .title("title") - .content("content") - .build(); - Post post = Post.builder() - .writer(MemberFixtures.FEMALE_20.get()) - .postBody(body) - .deadline(LocalDateTime.now()) - .build(); + Post post = generatePost(); Comment comment = Comment.builder() - .member(MemberFixtures.MALE_20.get()) + .writer(MemberFixtures.MALE_20.get()) .post(post) .content("hello") .build(); - // when, then - assertThatThrownBy(() -> comment.updateContent("a".repeat(501))) - .isInstanceOf(BadRequestException.class) - .hasMessage("유효하지 않은 댓글 길이입니다."); + // when + comment.blind(); + + // then + assertThat(comment.isHidden()).isTrue(); } } diff --git a/backend/src/test/java/com/votogether/domain/post/repository/CommentRepositoryTest.java b/backend/src/test/java/com/votogether/domain/post/repository/CommentRepositoryTest.java index a09f72d60..6b27a918a 100644 --- a/backend/src/test/java/com/votogether/domain/post/repository/CommentRepositoryTest.java +++ b/backend/src/test/java/com/votogether/domain/post/repository/CommentRepositoryTest.java @@ -3,66 +3,65 @@ import static org.assertj.core.api.Assertions.assertThat; import com.votogether.domain.member.entity.Member; -import com.votogether.domain.member.repository.MemberRepository; import com.votogether.domain.post.entity.Post; -import com.votogether.domain.post.entity.PostBody; import com.votogether.domain.post.entity.comment.Comment; -import com.votogether.test.annotation.RepositoryTest; -import com.votogether.test.fixtures.MemberFixtures; -import com.votogether.test.persister.PostTestPersister; -import java.time.LocalDateTime; +import com.votogether.test.RepositoryTest; import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -@RepositoryTest -class CommentRepositoryTest { +class CommentRepositoryTest extends RepositoryTest { @Autowired CommentRepository commentRepository; - @Autowired - MemberRepository memberRepository; + @Test + @DisplayName("게시글의 댓글 목록을 조회한다.") + void findAllByPost() { + // given + Member writer = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().writer(writer).save(); + Comment commentA = commentTestPersister.builder().post(post).writer(writer).save(); + Comment commentB = commentTestPersister.builder().post(post).writer(writer).save(); - @Autowired - PostRepository postRepository; + // when + List comments = commentRepository.findAllByPostAndIsHiddenFalseOrderByCreatedAtAsc(post); - @Autowired - PostTestPersister postTestPersister; + // then + assertThat(comments).containsExactly(commentA, commentB); + } @Test - @DisplayName("게시글의 댓글 목록을 조회한다.") - void findAllByPost() { + @DisplayName("작성자의 댓글 목록을 조회한다.") + void findAllByWriter() { // given - Member member = memberRepository.save(MemberFixtures.MALE_20.get()); + Member writer = memberTestPersister.builder().save(); + Comment commentA = commentTestPersister.builder().writer(writer).save(); + Comment commentB = commentTestPersister.builder().writer(writer).save(); - final Post post = postTestPersister.builder() - .writer(member) - .postBody(PostBody.builder().title("titleA").content("contentA").build()) - .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) - .save(); + // when + List comments = commentRepository.findAllByWriter(writer); - Comment commentA = commentRepository.save( - Comment.builder() - .member(member) - .post(post) - .content("commentA") - .build() - ); - Comment commentB = commentRepository.save( - Comment.builder() - .member(member) - .post(post) - .content("commentB") - .build() - ); + // then + assertThat(comments).containsExactly(commentA, commentB); + } + + @Test + @DisplayName("게시글의 모든 댓글을 삭제한다.") + void deleteAllWithPostIdInBatch() { + // given + Member writer = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().save(); + commentTestPersister.builder().writer(writer).post(post).save(); + commentTestPersister.builder().writer(writer).post(post).save(); + commentTestPersister.builder().writer(writer).post(post).save(); // when - List result = commentRepository.findAllByPostAndIsHiddenFalseOrderByCreatedAtAsc(post); + commentRepository.deleteAllWithPostIdInBatch(post.getId()); // then - assertThat(result).containsExactly(commentA, commentB); + assertThat(commentRepository.findAll()).isEmpty(); } } diff --git a/backend/src/test/java/com/votogether/domain/post/repository/PostCategoryRepositoryTest.java b/backend/src/test/java/com/votogether/domain/post/repository/PostCategoryRepositoryTest.java new file mode 100644 index 000000000..5a944b9c7 --- /dev/null +++ b/backend/src/test/java/com/votogether/domain/post/repository/PostCategoryRepositoryTest.java @@ -0,0 +1,48 @@ +package com.votogether.domain.post.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.votogether.domain.post.entity.Post; +import com.votogether.domain.post.entity.PostCategory; +import com.votogether.test.RepositoryTest; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +class PostCategoryRepositoryTest extends RepositoryTest { + + @Autowired + PostCategoryRepository postCategoryRepository; + + @Test + @DisplayName("게시글의 모든 게시글 카테고리 목록을 조회한다.") + void findAllPostCategoriesInPost() { + // given + Post post = postTestPersister.postBuilder().save(); + PostCategory postCategoryA = postTestPersister.postCategoryBuilder().post(post).save(); + PostCategory postCategoryB = postTestPersister.postCategoryBuilder().post(post).save(); + + // when + List result = postCategoryRepository.findAllByPost(post); + + // then + assertThat(result).containsExactly(postCategoryA, postCategoryB); + } + + @Test + @DisplayName("게시글의 모든 게시글 카테고리를 삭제한다.") + void deleteAllWithPostIdInBatch() { + // given + Post post = postTestPersister.postBuilder().save(); + postTestPersister.postCategoryBuilder().post(post).save(); + postTestPersister.postCategoryBuilder().post(post).save(); + + // when + postCategoryRepository.deleteAllWithPostIdInBatch(post.getId()); + + // then + assertThat(postCategoryRepository.findAll()).isEmpty(); + } + +} diff --git a/backend/src/test/java/com/votogether/domain/post/repository/PostContentImageRepositoryTest.java b/backend/src/test/java/com/votogether/domain/post/repository/PostContentImageRepositoryTest.java new file mode 100644 index 000000000..43b8dccb5 --- /dev/null +++ b/backend/src/test/java/com/votogether/domain/post/repository/PostContentImageRepositoryTest.java @@ -0,0 +1,31 @@ +package com.votogether.domain.post.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.votogether.domain.post.entity.Post; +import com.votogether.test.RepositoryTest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +class PostContentImageRepositoryTest extends RepositoryTest { + + @Autowired + PostContentImageRepository postContentImageRepository; + + @Test + @DisplayName("게시글의 모든 게시글 이미지를 삭제한다.") + void deleteAllWithPostIdInBatch() { + // given + Post post = postTestPersister.postBuilder().save(); + postTestPersister.postContentImageBuilder().post(post).save(); + postTestPersister.postContentImageBuilder().post(post).save(); + + // when + postContentImageRepository.deleteAllWithPostIdInBatch(post.getId()); + + // when + assertThat(postContentImageRepository.findAll()).isEmpty(); + } + +} diff --git a/backend/src/test/java/com/votogether/domain/post/repository/PostCustomRepositoryImplTest.java b/backend/src/test/java/com/votogether/domain/post/repository/PostCustomRepositoryImplTest.java new file mode 100644 index 000000000..3b4ed82b7 --- /dev/null +++ b/backend/src/test/java/com/votogether/domain/post/repository/PostCustomRepositoryImplTest.java @@ -0,0 +1,520 @@ +package com.votogether.domain.post.repository; + +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +import com.votogether.domain.category.entity.Category; +import com.votogether.domain.member.entity.Member; +import com.votogether.domain.post.entity.Post; +import com.votogether.domain.post.entity.PostOption; +import com.votogether.domain.post.entity.vo.PostClosingType; +import com.votogether.domain.post.entity.vo.PostSortType; +import com.votogether.test.RepositoryTest; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; + +class PostCustomRepositoryImplTest extends RepositoryTest { + + @Autowired + PostCustomRepositoryImpl postCustomRepository; + + @Nested + @DisplayName("마감시간, 정렬기준, 카테고리로 필터링하여 게시글 페이징 조회") + class FindPostsWithFilteringAndPaging { + + @Test + @DisplayName("마감되지 않은 게시글을 조회한다.") + void getPostsOpen() { + // given + LocalDateTime deadline = LocalDateTime.now().plusDays(1).truncatedTo(ChronoUnit.MINUTES); + Post post = postTestPersister.postBuilder().deadline(deadline).save(); + + // when + Pageable pageable = PageRequest.of(0, 10); + List result = postCustomRepository.findPostsWithFilteringAndPaging( + PostClosingType.PROGRESS, + PostSortType.LATEST, + null, + pageable + ); + + // then + assertSoftly(softly -> { + softly.assertThat(result).hasSize(1); + softly.assertThat(result).containsExactly(post); + }); + } + + @Test + @DisplayName("마감된 게시글을 조회한다.") + void getPostsClosed() { + // given + LocalDateTime deadline = LocalDateTime.now().minusDays(1).truncatedTo(ChronoUnit.MINUTES); + Post post = postTestPersister.postBuilder().deadline(deadline).save(); + + // when + Pageable pageable = PageRequest.of(0, 10); + List result = postCustomRepository.findPostsWithFilteringAndPaging( + PostClosingType.CLOSED, + PostSortType.LATEST, + null, + pageable + ); + + // then + assertSoftly(softly -> { + softly.assertThat(result).hasSize(1); + softly.assertThat(result).containsExactly(post); + }); + } + + @Test + @DisplayName("게시글을 최신순으로 조회한다.") + void getPostsByLatest() { + // given + Post postA = postTestPersister.postBuilder().save(); + Post postB = postTestPersister.postBuilder().save(); + + // when + Pageable pageable = PageRequest.of(0, 10); + List result = postCustomRepository.findPostsWithFilteringAndPaging( + PostClosingType.ALL, + PostSortType.LATEST, + null, + pageable + ); + + // then + assertSoftly(softly -> { + softly.assertThat(result).hasSize(2); + softly.assertThat(result).containsExactly(postB, postA); + }); + } + + @Test + @DisplayName("게시글을 인기순으로 조회한다.") + void getPostsByHot() { + // post + Post postA = postTestPersister.postBuilder().save(); + PostOption postOptionA = postTestPersister.postOptionBuilder().post(postA).sequence(1).save(); + voteTestPersister.builder().postOption(postOptionA).save(); + voteTestPersister.builder().postOption(postOptionA).save(); + Post postB = postTestPersister.postBuilder().save(); + PostOption postOptionB = postTestPersister.postOptionBuilder().post(postB).sequence(1).save(); + voteTestPersister.builder().postOption(postOptionB).save(); + + // when + Pageable pageable = PageRequest.of(0, 10); + List result = postCustomRepository.findPostsWithFilteringAndPaging( + PostClosingType.ALL, + PostSortType.HOT, + null, + pageable + ); + + // then + assertSoftly(softly -> { + softly.assertThat(result).hasSize(2); + softly.assertThat(result).containsExactly(postA, postB); + }); + } + + @Test + @DisplayName("특정 카테고리의 게시글을 조회한다.") + void getPostsTargetCategory() { + // given + Post post = postTestPersister.postBuilder().save(); + Category category = categoryTestPersister.builder().save(); + postTestPersister.postCategoryBuilder().post(post).category(category).save(); + + // when + Pageable pageable = PageRequest.of(0, 10); + List result = postCustomRepository.findPostsWithFilteringAndPaging( + PostClosingType.ALL, + PostSortType.LATEST, + category.getId(), + pageable + ); + + // then + assertSoftly(softly -> { + softly.assertThat(result).hasSize(1); + softly.assertThat(result).containsExactly(post); + }); + } + + @Test + @DisplayName("카테고리 구분없이 게시글을 조회한다.") + void getPostsAllCategory() { + // given + Post post = postTestPersister.postBuilder().save(); + Category category = categoryTestPersister.builder().save(); + postTestPersister.postCategoryBuilder().post(post).category(category).save(); + + // when + Pageable pageable = PageRequest.of(0, 10); + List result = postCustomRepository.findPostsWithFilteringAndPaging( + PostClosingType.ALL, + PostSortType.LATEST, + null, + pageable + ); + + // then + assertSoftly(softly -> { + softly.assertThat(result).hasSize(1); + softly.assertThat(result).containsExactly(post); + }); + } + + } + + @Nested + @DisplayName("작성자, 마감시간, 정렬기준으로 필터링하여 게시글 페이징 조회") + class FindPostsByWriterWithFilteringAndPaging { + + @Test + @DisplayName("마감되지 않은 게시글을 조회한다.") + void getPostsOpen() { + // given + LocalDateTime deadline = LocalDateTime.now().plusDays(1).truncatedTo(ChronoUnit.MINUTES); + Member member = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().writer(member).deadline(deadline).save(); + + // when + Pageable pageable = PageRequest.of(0, 10); + List result = postCustomRepository.findPostsByWriterWithFilteringAndPaging( + member, + PostClosingType.PROGRESS, + PostSortType.LATEST, + pageable + ); + + // then + assertSoftly(softly -> { + softly.assertThat(result).hasSize(1); + softly.assertThat(result).containsExactly(post); + }); + } + + @Test + @DisplayName("마감된 게시글을 조회한다.") + void getPostsClosed() { + // given + LocalDateTime deadline = LocalDateTime.now().minusDays(1).truncatedTo(ChronoUnit.MINUTES); + Member member = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().writer(member).deadline(deadline).save(); + + // when + Pageable pageable = PageRequest.of(0, 10); + List result = postCustomRepository.findPostsByWriterWithFilteringAndPaging( + member, + PostClosingType.CLOSED, + PostSortType.LATEST, + pageable + ); + + // then + assertSoftly(softly -> { + softly.assertThat(result).hasSize(1); + softly.assertThat(result).containsExactly(post); + }); + } + + @Test + @DisplayName("게시글을 최신순으로 조회한다.") + void getPostsByLatest() { + // given + Member member = memberTestPersister.builder().save(); + Post postA = postTestPersister.postBuilder().writer(member).save(); + Post postB = postTestPersister.postBuilder().writer(member).save(); + + // when + Pageable pageable = PageRequest.of(0, 10); + List result = postCustomRepository.findPostsByWriterWithFilteringAndPaging( + member, + PostClosingType.ALL, + PostSortType.LATEST, + pageable + ); + + // then + assertSoftly(softly -> { + softly.assertThat(result).hasSize(2); + softly.assertThat(result).containsExactly(postB, postA); + }); + } + + @Test + @DisplayName("게시글을 인기순으로 조회한다.") + void getPostsByHot() { + // post + Member member = memberTestPersister.builder().save(); + Post postA = postTestPersister.postBuilder().writer(member).save(); + PostOption postOptionA = postTestPersister.postOptionBuilder().post(postA).sequence(1).save(); + voteTestPersister.builder().postOption(postOptionA).save(); + voteTestPersister.builder().postOption(postOptionA).save(); + Post postB = postTestPersister.postBuilder().writer(member).save(); + PostOption postOptionB = postTestPersister.postOptionBuilder().post(postB).sequence(1).save(); + voteTestPersister.builder().postOption(postOptionB).save(); + + // when + Pageable pageable = PageRequest.of(0, 10); + List result = postCustomRepository.findPostsByWriterWithFilteringAndPaging( + member, + PostClosingType.ALL, + PostSortType.HOT, + pageable + ); + + // then + assertSoftly(softly -> { + softly.assertThat(result).hasSize(2); + softly.assertThat(result).containsExactly(postA, postB); + }); + } + + } + + @Nested + @DisplayName("키워드, 마감시간, 정렬기준으로 필터링하여 게시글 페이징 조회") + class FindSearchPostsWithFilteringAndPaging { + + @Test + @DisplayName("마감되지 않은 게시글을 조회한다.") + void getPostsOpen() { + // given + LocalDateTime deadline = LocalDateTime.now().plusDays(1).truncatedTo(ChronoUnit.MINUTES); + Member member = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder() + .writer(member) + .title("votogether") + .deadline(deadline) + .save(); + + // when + Pageable pageable = PageRequest.of(0, 10); + List result = postCustomRepository.findSearchPostsWithFilteringAndPaging( + "votogether", + PostClosingType.PROGRESS, + PostSortType.LATEST, + pageable + ); + + // then + assertSoftly(softly -> { + softly.assertThat(result).hasSize(1); + softly.assertThat(result).containsExactly(post); + }); + } + + @Test + @DisplayName("마감된 게시글을 조회한다.") + void getPostsClosed() { + // given + LocalDateTime deadline = LocalDateTime.now().minusDays(1).truncatedTo(ChronoUnit.MINUTES); + Member member = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder() + .writer(member) + .title("votogether") + .deadline(deadline).save(); + + // when + Pageable pageable = PageRequest.of(0, 10); + List result = postCustomRepository.findSearchPostsWithFilteringAndPaging( + "votogether", + PostClosingType.CLOSED, + PostSortType.LATEST, + pageable + ); + + // then + assertSoftly(softly -> { + softly.assertThat(result).hasSize(1); + softly.assertThat(result).containsExactly(post); + }); + } + + @Test + @DisplayName("게시글을 최신순으로 조회한다.") + void getPostsByLatest() { + // given + Member member = memberTestPersister.builder().save(); + Post postA = postTestPersister.postBuilder() + .writer(member) + .title("votogether1") + .save(); + Post postB = postTestPersister.postBuilder() + .writer(member) + .content("votogether2") + .save(); + + // when + Pageable pageable = PageRequest.of(0, 10); + List result = postCustomRepository.findSearchPostsWithFilteringAndPaging( + "votogether", + PostClosingType.ALL, + PostSortType.LATEST, + pageable + ); + + // then + assertSoftly(softly -> { + softly.assertThat(result).hasSize(2); + softly.assertThat(result).containsExactly(postB, postA); + }); + } + + @Test + @DisplayName("게시글을 인기순으로 조회한다.") + void getPostsByHot() { + // post + Member member = memberTestPersister.builder().save(); + Post postA = postTestPersister.postBuilder().writer(member).title("votogether1").save(); + PostOption postOptionA = postTestPersister.postOptionBuilder().post(postA).sequence(1).save(); + voteTestPersister.builder().postOption(postOptionA).save(); + voteTestPersister.builder().postOption(postOptionA).save(); + Post postB = postTestPersister.postBuilder().writer(member).content("votogether2").save(); + PostOption postOptionB = postTestPersister.postOptionBuilder().post(postB).sequence(1).save(); + voteTestPersister.builder().postOption(postOptionB).save(); + + // when + Pageable pageable = PageRequest.of(0, 10); + List result = postCustomRepository.findSearchPostsWithFilteringAndPaging( + "votogether", + PostClosingType.ALL, + PostSortType.HOT, + pageable + ); + + // then + assertSoftly(softly -> { + softly.assertThat(result).hasSize(2); + softly.assertThat(result).containsExactly(postA, postB); + }); + } + + } + + @Nested + @DisplayName("투표자, 마감시간, 정렬기준으로 필터링하여 게시글 페이징 조회") + class FindPostsByVotedWithFilteringAndPaging { + + @Test + @DisplayName("마감되지 않은 게시글을 조회한다.") + void getPostsOpen() { + // given + LocalDateTime deadline = LocalDateTime.now().plusDays(1).truncatedTo(ChronoUnit.MINUTES); + Member member = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().deadline(deadline).save(); + PostOption postOption = postTestPersister.postOptionBuilder().post(post).sequence(1).save(); + voteTestPersister.builder().postOption(postOption).member(member).save(); + + // when + Pageable pageable = PageRequest.of(0, 10); + List result = postCustomRepository.findPostsByVotedWithFilteringAndPaging( + member, + PostClosingType.PROGRESS, + PostSortType.LATEST, + pageable + ); + + // then + assertSoftly(softly -> { + softly.assertThat(result).hasSize(1); + softly.assertThat(result).containsExactly(post); + }); + } + + @Test + @DisplayName("마감된 게시글을 조회한다.") + void getPostsClosed() { + // given + LocalDateTime deadline = LocalDateTime.now().minusDays(1).truncatedTo(ChronoUnit.MINUTES); + Member member = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().deadline(deadline).save(); + PostOption postOption = postTestPersister.postOptionBuilder().post(post).sequence(1).save(); + voteTestPersister.builder().postOption(postOption).member(member).save(); + + // when + Pageable pageable = PageRequest.of(0, 10); + List result = postCustomRepository.findPostsByVotedWithFilteringAndPaging( + member, + PostClosingType.CLOSED, + PostSortType.LATEST, + pageable + ); + + // then + assertSoftly(softly -> { + softly.assertThat(result).hasSize(1); + softly.assertThat(result).containsExactly(post); + }); + } + + @Test + @DisplayName("게시글을 최신순으로 조회한다.") + void getPostsByLatest() { + // given + Member member = memberTestPersister.builder().save(); + Post postA = postTestPersister.postBuilder().writer(member).save(); + Post postB = postTestPersister.postBuilder().writer(member).save(); + PostOption postOptionA = postTestPersister.postOptionBuilder().post(postA).sequence(1).save(); + PostOption postOptionB = postTestPersister.postOptionBuilder().post(postB).sequence(1).save(); + voteTestPersister.builder().postOption(postOptionA).member(member).save(); + voteTestPersister.builder().postOption(postOptionB).member(member).save(); + + // when + Pageable pageable = PageRequest.of(0, 10); + List result = postCustomRepository.findPostsByVotedWithFilteringAndPaging( + member, + PostClosingType.ALL, + PostSortType.LATEST, + pageable + ); + + // then + assertSoftly(softly -> { + softly.assertThat(result).hasSize(2); + softly.assertThat(result).containsExactly(postB, postA); + }); + } + + @Test + @DisplayName("게시글을 인기순으로 조회한다.") + void getPostsByHot() { + // post + Member member = memberTestPersister.builder().save(); + Post postA = postTestPersister.postBuilder().save(); + PostOption postOptionA = postTestPersister.postOptionBuilder().post(postA).sequence(1).save(); + voteTestPersister.builder().postOption(postOptionA).member(member).save(); + voteTestPersister.builder().postOption(postOptionA).save(); + Post postB = postTestPersister.postBuilder().writer(member).save(); + PostOption postOptionB = postTestPersister.postOptionBuilder().post(postB).sequence(1).save(); + voteTestPersister.builder().postOption(postOptionB).member(member).save(); + + // when + Pageable pageable = PageRequest.of(0, 10); + List result = postCustomRepository.findPostsByVotedWithFilteringAndPaging( + member, + PostClosingType.ALL, + PostSortType.HOT, + pageable + ); + + // then + assertSoftly(softly -> { + softly.assertThat(result).hasSize(2); + softly.assertThat(result).containsExactly(postA, postB); + }); + } + + } + +} diff --git a/backend/src/test/java/com/votogether/domain/post/repository/PostOptionRepositoryTest.java b/backend/src/test/java/com/votogether/domain/post/repository/PostOptionRepositoryTest.java new file mode 100644 index 000000000..a9db8ddc6 --- /dev/null +++ b/backend/src/test/java/com/votogether/domain/post/repository/PostOptionRepositoryTest.java @@ -0,0 +1,31 @@ +package com.votogether.domain.post.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.votogether.domain.post.entity.Post; +import com.votogether.test.RepositoryTest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +class PostOptionRepositoryTest extends RepositoryTest { + + @Autowired + PostOptionRepository postOptionRepository; + + @Test + @DisplayName("게시글의 모든 게시글 옵션을 삭제한다.") + void deleteAllWithPostIdInBatch() { + // given + Post post = postTestPersister.postBuilder().save(); + postTestPersister.postOptionBuilder().post(post).sequence(1).save(); + postTestPersister.postOptionBuilder().post(post).sequence(2).save(); + + // when + postOptionRepository.deleteAllWithPostIdInBatch(post.getId()); + + // then + assertThat(postOptionRepository.findAll()).isEmpty(); + } + +} diff --git a/backend/src/test/java/com/votogether/domain/post/repository/PostRepositoryTest.java b/backend/src/test/java/com/votogether/domain/post/repository/PostRepositoryTest.java index 331233c12..84c506cbe 100644 --- a/backend/src/test/java/com/votogether/domain/post/repository/PostRepositoryTest.java +++ b/backend/src/test/java/com/votogether/domain/post/repository/PostRepositoryTest.java @@ -3,840 +3,67 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; -import com.votogether.domain.category.entity.Category; -import com.votogether.domain.category.repository.CategoryRepository; import com.votogether.domain.member.entity.Member; -import com.votogether.domain.member.entity.vo.Gender; -import com.votogether.domain.member.entity.vo.SocialType; -import com.votogether.domain.member.repository.MemberRepository; import com.votogether.domain.post.entity.Post; -import com.votogether.domain.post.entity.PostBody; -import com.votogether.domain.post.entity.PostCategory; -import com.votogether.domain.post.entity.PostOption; -import com.votogether.domain.post.entity.vo.PostClosingType; -import com.votogether.domain.post.entity.vo.PostSortType; -import com.votogether.domain.vote.entity.Vote; -import com.votogether.domain.vote.repository.VoteRepository; -import com.votogether.test.annotation.RepositoryTest; -import com.votogether.test.fixtures.MemberFixtures; -import com.votogether.test.persister.MemberTestPersister; -import com.votogether.test.persister.PostOptionTestPersister; -import com.votogether.test.persister.PostTestPersister; -import com.votogether.test.persister.VoteTestPersister; -import java.time.LocalDateTime; -import java.util.ArrayList; +import com.votogether.test.RepositoryTest; import java.util.List; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Slice; -@RepositoryTest -class PostRepositoryTest { - - @Autowired - MemberRepository memberRepository; +class PostRepositoryTest extends RepositoryTest { @Autowired PostRepository postRepository; - @Autowired - PostOptionRepository postOptionRepository; - - @Autowired - PostCategoryRepository postCategoryRepository; - - @Autowired - CategoryRepository categoryRepository; - - @Autowired - VoteRepository voteRepository; - - @Autowired - MemberTestPersister memberTestPersister; - - @Autowired - PostTestPersister postTestPersister; - - @Autowired - PostOptionTestPersister postOptionTestPersister; - - @Autowired - VoteTestPersister voteTestPersister; - - @Test - @DisplayName("Post를 저장한다") - void save() { - // given - final PostBody postBody = PostBody.builder() - .title("title") - .content("content") - .build(); - - final Member member = Member.builder() - .gender(Gender.MALE) - .socialType(SocialType.KAKAO) - .nickname("user1") - .birthYear(2000) - .socialId("kakao@gmail.com") - .build(); - - final Post post = Post.builder() - .writer(member) - .postBody(postBody) - .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) - .build(); - - memberRepository.save(member); - - // when - final Post savedPost = postRepository.save(post); - - // then - assertThat(savedPost).isNotNull(); - } - - @Test - @DisplayName("해당 멤버가 작성한 글의 개수를 확인한다.") - void countByMember() { - // given - Member member = Member.builder() - .nickname("user1") - .gender(Gender.MALE) - .birthYear(2000) - .socialType(SocialType.KAKAO) - .socialId("kakao@gmail.com") - .build(); - - PostBody postBody1 = PostBody.builder() - .title("title1") - .content("content1") - .build(); - - PostBody postBody2 = PostBody.builder() - .title("title2") - .content("content2") - .build(); - - Post post1 = Post.builder() - .writer(member) - .postBody(postBody1) - .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) - .build(); - - Post post2 = Post.builder() - .writer(member) - .postBody(postBody2) - .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) - .build(); - - memberRepository.save(member); - postRepository.save(post1); - postRepository.save(post2); - - // when - int numberOfPosts = postRepository.countByWriter(member); - - // then - assertThat(numberOfPosts).isEqualTo(2); - } - - @Nested - @DisplayName("마감 여부와 정렬 기준으로 페이징 처리하여 게시글 목록을 조회한다.") - class FindAllByClosingTypeAndSortType { - - private List members; - private List posts; - private List categories; - - @BeforeEach - void setUp() { - members = new ArrayList<>(); - posts = new ArrayList<>(); - categories = new ArrayList<>(); - - for (int i = 0; i < 11; i++) { - members.add(memberTestPersister.builder().save()); - } - - Category categoryA = Category.builder() - .name("개발") - .build(); - - Category categoryB = Category.builder() - .name("연애") - .build(); - - categories.add(categoryRepository.save(categoryA)); - categories.add(categoryRepository.save(categoryB)); - - for (int i = 2; i > 0; i--) { - Post closedPost = postTestPersister.builder() - .writer(members.get(members.size() - 1)) - .deadline(LocalDateTime.of(2022, 12, 25, 0, 0)) - .save(); - Post notClosedPost = postTestPersister.builder() - .writer(members.get(members.size() - 1)) - .deadline(LocalDateTime.of(3022, 12, 25, 0, 0)) - .save(); - - posts.add(closedPost); - posts.add(notClosedPost); - - generatePostCategory(closedPost, categories.get(0)); - generatePostCategory(notClosedPost, categories.get(0)); - - generatePostOptionAndVote(closedPost, i + 5); - generatePostOptionAndVote(notClosedPost, i + 7); - } - } - - private void generatePostCategory(Post post, Category category) { - PostCategory postCategory = PostCategory.builder() - .post(post) - .category(category) - .build(); - postCategoryRepository.save(postCategory); - } - - private void generatePostOptionAndVote(Post post, int voteCount) { - for (int j = 0; j < 5; j++) { - PostOption postOption = postOptionTestPersister.builder().sequence(j + 1).post(post).save(); - for (int k = 0; k < voteCount; k++) { - voteTestPersister.builder().member(members.get(k)).postOption(postOption).save(); - } - } - } - - @Test - @DisplayName("마감 여부와 상관없이 게시글 목록을 최신순으로 조회한다.") - void getAllPostsOrderByLatest() { - // given - Pageable pageable = PageRequest.of(0, 2); - - // when - List result = postRepository.findAllByClosingTypeAndSortTypeAndCategoryId( - PostClosingType.ALL, - PostSortType.LATEST, - null, - pageable - ); - - // then - assertThat(result).containsExactly(posts.get(3), posts.get(2)); - } - - @Test - @DisplayName("마감 여부와 상관없이 게시글 목록을 인기순으로 조회한다.") - void getAllPostsOrderByHot() { - // given - Pageable pageable = PageRequest.of(0, 2); - - // when - List result = postRepository.findAllByClosingTypeAndSortTypeAndCategoryId( - PostClosingType.ALL, - PostSortType.HOT, - null, - pageable - ); - - // then - assertThat(result).containsExactly(posts.get(1), posts.get(3)); - } - - @Test - @DisplayName("진행중인 게시글 목록을 최신순으로 조회한다.") - void getProgressPostsOrderByLatest() { - // given - Pageable pageable = PageRequest.of(0, 2); - - // when - List result = postRepository.findAllByClosingTypeAndSortTypeAndCategoryId( - PostClosingType.PROGRESS, - PostSortType.LATEST, - null, - pageable - ); - - // then - assertThat(result).containsExactly(posts.get(3), posts.get(1)); - } - - @Test - @DisplayName("진행중인 게시글 목록을 인기순으로 조회한다.") - void getProgressPostsOrderByHot() { - // given - Pageable pageable = PageRequest.of(0, 2); - - // when - List result = postRepository.findAllByClosingTypeAndSortTypeAndCategoryId( - PostClosingType.PROGRESS, - PostSortType.HOT, - null, - pageable - ); - - // then - assertThat(result).containsExactly(posts.get(1), posts.get(3)); - } - - @Test - @DisplayName("마감된 게시글 목록을 최신순으로 조회한다.") - void getClosedPostsOrderByLatest() { - // given - Pageable pageable = PageRequest.of(0, 2); - - // when - List result = postRepository.findAllByClosingTypeAndSortTypeAndCategoryId( - PostClosingType.CLOSED, - PostSortType.LATEST, - null, - pageable - ); - - // then - assertThat(result).containsExactly(posts.get(2), posts.get(0)); - } - - @Test - @DisplayName("마감된 게시글 목록을 인기순으로 조회한다.") - void getClosedPostsOrderByHot() { - // given - Pageable pageable = PageRequest.of(0, 2); - - // when - List result = postRepository.findAllByClosingTypeAndSortTypeAndCategoryId( - PostClosingType.CLOSED, - PostSortType.HOT, - null, - pageable - ); - - // then - assertThat(result).containsExactly(posts.get(0), posts.get(2)); - } - - @Test - @DisplayName("특정 카테고리의 진행중인 게시글을 인기순으로 조회한다.") - void getClosedPostsOrderByHotWithCategory() { - // given - Pageable pageable = PageRequest.of(0, 5); - - // when - List result = postRepository.findAllByClosingTypeAndSortTypeAndCategoryId( - PostClosingType.PROGRESS, - PostSortType.HOT, - categories.get(0).getId(), - pageable - ); - - // then - assertThat(result).containsExactly(posts.get(1), posts.get(3)); - } - - } - @Nested - @DisplayName("회원이 투표한 게시글 목록을 조회한다.") - class FindPostsVotedByMember { + @DisplayName("회원이 작성한 게시글 조회") + class FindPostsByWriter { @Test - @DisplayName("마감된 게시글 목록을 최신순으로 가져온다.") - void findClosedPostsVotedByMember() { + @DisplayName("작성한 게시글이 존재하면 게시글 목록을 조회한다.") + void findPostsExist() { // given - Member writer = memberRepository.save(MemberFixtures.MALE_20.get()); - Member member = memberRepository.save(MemberFixtures.MALE_10.get()); - - Post openPost = postRepository.save( - Post.builder() - .writer(writer) - .postBody(PostBody.builder().title("title").content("content").build()) - .deadline(LocalDateTime.of(3000, 7, 12, 0, 0)) - .build() - ); - PostOption postOption = postOptionRepository.save( - PostOption.builder() - .post(openPost) - .sequence(1) - .content("치킨") - .build() - ); - - Post closedPost = postRepository.save( - Post.builder() - .writer(writer) - .postBody(PostBody.builder().title("title").content("content").build()) - .deadline(LocalDateTime.of(1000, 7, 12, 0, 0)) - .build()); - - PostOption postOption1 = postOptionRepository.save( - PostOption.builder() - .post(closedPost) - .sequence(1) - .content("치킨") - .build() - ); - - Post closedPost1 = postRepository.save( - Post.builder() - .writer(writer) - .postBody(PostBody.builder().title("title").content("content").build()) - .deadline(LocalDateTime.of(1001, 7, 12, 0, 0)) - .build() - ); - PostOption postOption2 = postOptionRepository.save( - PostOption.builder() - .post(closedPost1) - .sequence(1) - .content("치킨") - .build() - ); - - voteRepository.save(Vote.builder().member(member).postOption(postOption).build()); - voteRepository.save(Vote.builder().member(member).postOption(postOption1).build()); - voteRepository.save(Vote.builder().member(member).postOption(postOption2).build()); + Member writer = memberTestPersister.builder().save(); + Post postA = postTestPersister.postBuilder().writer(writer).save(); + Post postB = postTestPersister.postBuilder().writer(writer).save(); // when - PageRequest pageRequest = PageRequest.of(0, 10, PostSortType.LATEST.getVoteBaseSort()); - Slice posts = postRepository.findClosedPostsVotedByMember(member, pageRequest); + List result = postRepository.findAllByWriter(writer); // then - assertAll( - () -> assertThat(posts).hasSize(2), - () -> assertThat(posts.getContent().get(0)).usingRecursiveComparison().isEqualTo(closedPost1) - ); + assertThat(result).containsExactly(postA, postB); } @Test - @DisplayName("마감되지 않은 게시글 목록을 투표순으로 가져온다.") - void findOpenPostsVotedByMember() { + @DisplayName("작성한 게시글이 존재하지 않으면 빈 목록을 조회한다.") + void findEmptyNotExist() { // given - Member writer = memberRepository.save(MemberFixtures.MALE_20.get()); - Member member = memberRepository.save(MemberFixtures.MALE_10.get()); - Member member1 = memberRepository.save(MemberFixtures.MALE_60.get()); - - Post openPost = postRepository.save( - Post.builder() - .writer(writer) - .postBody(PostBody.builder().title("title").content("content").build()) - .deadline(LocalDateTime.of(3000, 7, 12, 0, 0)) - .build() - ); - PostOption postOption = postOptionRepository.save( - PostOption.builder() - .post(openPost) - .sequence(1) - .content("치킨") - .build() - ); - - Post openPost1 = postRepository.save( - Post.builder() - .writer(writer) - .postBody(PostBody.builder().title("title").content("content").build()) - .deadline(LocalDateTime.of(3001, 7, 12, 0, 0)) - .build() - ); - PostOption postOption1 = postOptionRepository.save( - PostOption.builder() - .post(openPost1) - .sequence(1) - .content("치킨") - .build() - ); - - Post closedPost = postRepository.save( - Post.builder() - .writer(writer) - .postBody(PostBody.builder().title("title").content("content").build()) - .deadline(LocalDateTime.of(1000, 7, 12, 0, 0)) - .build() - ); - PostOption postOption2 = postOptionRepository.save( - PostOption.builder() - .post(closedPost) - .sequence(1) - .content("치킨") - .build() - ); - - voteRepository.save(Vote.builder().member(member).postOption(postOption).build()); - voteRepository.save(Vote.builder().member(member).postOption(postOption1).build()); - voteRepository.save(Vote.builder().member(member1).postOption(postOption1).build()); - voteRepository.save(Vote.builder().member(member).postOption(postOption2).build()); + Member writer = memberTestPersister.builder().save(); // when - PageRequest pageRequest = PageRequest.of(0, 10, PostSortType.HOT.getVoteBaseSort()); - Slice posts = postRepository.findOpenPostsVotedByMember(member, pageRequest); + List result = postRepository.findAllByWriter(writer); // then - assertAll( - () -> assertThat(posts).hasSize(2), - () -> assertThat(posts.getContent().get(0)).usingRecursiveComparison().isEqualTo(openPost1) - ); - } - - @Test - @DisplayName("모든 게시글 목록을 가져온다.") - void findPostsVotedByMember() { - // given - Member writer = memberRepository.save(MemberFixtures.MALE_20.get()); - Member member = memberRepository.save(MemberFixtures.MALE_10.get()); - - Post openPost = postRepository.save( - Post.builder() - .writer(writer) - .postBody(PostBody.builder().title("title").content("content").build()) - .deadline(LocalDateTime.of(3000, 7, 12, 0, 0)) - .build() - ); - PostOption postOption = postOptionRepository.save( - PostOption.builder() - .post(openPost) - .sequence(1) - .content("치킨") - .build() - ); - - Post closedPost = postRepository.save( - Post.builder() - .writer(writer) - .postBody(PostBody.builder().title("title").content("content").build()) - .deadline(LocalDateTime.of(1000, 7, 12, 0, 0)) - .build() - ); - PostOption postOption1 = postOptionRepository.save( - PostOption.builder() - .post(closedPost) - .sequence(1) - .content("치킨") - .build() - ); - - voteRepository.save(Vote.builder().member(member).postOption(postOption).build()); - voteRepository.save(Vote.builder().member(member).postOption(postOption1).build()); - - // when - PageRequest pageRequest = PageRequest.of(0, 10); - Slice posts = postRepository.findPostsVotedByMember(member, pageRequest); - - // then - assertThat(posts).hasSize(2); + assertThat(result).isEmpty(); } } - @Nested - @DisplayName("키워드 검색을 통해 게시글 목록을 조회한다.") - class FindingPostsByKeyword { - - Category development; - Category love; - - Post devClosedPost; - Post devOpenPost; - Post loveClosedPost; - Post loveOpenPost; - - @BeforeEach - void setUp() { - development = categoryRepository.save(Category.builder().name("개발").build()); - love = categoryRepository.save(Category.builder().name("연애").build()); - - devClosedPost = postTestPersister.builder() - .postBody(PostBody.builder().title("자바제목").content("자바내용").build()) - .deadline(LocalDateTime.of(1000, 7, 12, 0, 0)) - .save(); - postCategoryRepository.save(PostCategory.builder().post(devClosedPost).category(development).build()); - - devOpenPost = postTestPersister.builder() - .postBody(PostBody.builder().title("자바제목1").content("자바내용1").build()) - .deadline(LocalDateTime.of(3000, 7, 12, 0, 0)) - .save(); - postCategoryRepository.save(PostCategory.builder().post(devOpenPost).category(development).build()); - - loveClosedPost = postTestPersister.builder() - .postBody(PostBody.builder().title("커플제목").content("커플내용").build()) - .deadline(LocalDateTime.of(1000, 7, 12, 0, 0)) - .save(); - postCategoryRepository.save(PostCategory.builder().post(loveClosedPost).category(love).build()); - - loveOpenPost = postTestPersister.builder() - .postBody(PostBody.builder().title("커플제목1").content("커플내용1").build()) - .deadline(LocalDateTime.of(3000, 7, 12, 0, 0)) - .save(); - postCategoryRepository.save(PostCategory.builder().post(loveOpenPost).category(love).build()); - - } - - @Test - @DisplayName("특정 카테고리에 속한 게시글 목록을 키워드를 통해 검색한다.") - void searchPostsWithCategory() { - // when - List posts = postRepository.findAllWithKeyword( - "자바", - PostClosingType.ALL, - PostSortType.LATEST, - development.getId(), - PageRequest.of(0, 10) - ); - - //then - assertAll( - () -> assertThat(posts).hasSize(2), - () -> assertThat(posts.get(0)).isEqualTo(devOpenPost), - () -> assertThat(posts.get(1)).isEqualTo(devClosedPost) - ); - } - - @Test - @DisplayName("게시글 목록을 키워드를 통해 검색한다.(제목)") - void searchPostsWithKeywordInTitle() { - // when - List posts = postRepository.findAllWithKeyword( - "제목1", - PostClosingType.ALL, - PostSortType.LATEST, - null, - PageRequest.of(0, 10) - ); - - //then - assertAll( - () -> assertThat(posts).hasSize(2), - () -> assertThat(posts.get(0)).isEqualTo(loveOpenPost), - () -> assertThat(posts.get(1)).isEqualTo(devOpenPost) - ); - } - - @Test - @DisplayName("게시글 목록을 키워드를 통해 검색한다.(내용)") - void searchPostsWithKeywordInContent() { - // when - List posts = postRepository.findAllWithKeyword( - "내용", - PostClosingType.ALL, - PostSortType.LATEST, - null, - PageRequest.of(0, 10) - ); - - //then - assertThat(posts).hasSize(4); - } - - @Test - @DisplayName("게시글 목록을 키워드를 통해 검색한다.(제목 + 내용)") - void searchPostsWithKeywordInTitleAndContent() { - // when - List posts = postRepository.findAllWithKeyword( - "1", - PostClosingType.ALL, - PostSortType.LATEST, - null, - PageRequest.of(0, 10) - ); - - //then - assertAll( - () -> assertThat(posts).hasSize(2), - () -> assertThat(posts.get(0)).isEqualTo(loveOpenPost), - () -> assertThat(posts.get(1)).isEqualTo(devOpenPost) - ); - } - - } - - @Nested - @DisplayName("회원이 작성한 게시글 목록을 조회한다.") - class findPostsByWriter { - - Member writer; - Member voter; - Member voter1; - - Post openPost_V2; - Post openPost1_V1; - Post closedPost_V1; - Post closedPost1_V0; - - @BeforeEach - void setUp() { - writer = memberRepository.save(MemberFixtures.MALE_20.get()); - voter = memberRepository.save(MemberFixtures.FEMALE_OVER_90.get()); - voter1 = memberRepository.save(MemberFixtures.MALE_60.get()); - - openPost_V2 = postRepository.save( - Post.builder() - .writer(writer) - .postBody(PostBody.builder().title("title").content("content").build()) - .deadline(LocalDateTime.of(3000, 7, 12, 0, 0)) - .build() - ); - - PostOption postOption = postOptionRepository.save( - PostOption.builder() - .post(openPost_V2) - .sequence(1) - .content("치킨") - .build() - ); - voteRepository.save(Vote.builder().member(voter).postOption(postOption).build()); - voteRepository.save(Vote.builder().member(voter1).postOption(postOption).build()); - - openPost1_V1 = postRepository.save( - Post.builder() - .writer(writer) - .postBody(PostBody.builder().title("title").content("content").build()) - .deadline(LocalDateTime.of(3000, 7, 12, 0, 0)) - .build() - ); - - PostOption postOption3 = postOptionRepository.save( - PostOption.builder() - .post(openPost1_V1) - .sequence(1) - .content("치킨") - .build() - ); - voteRepository.save(Vote.builder().member(voter).postOption(postOption3).build()); - - closedPost_V1 = postRepository.save( - Post.builder() - .writer(writer) - .postBody(PostBody.builder().title("title").content("content").build()) - .deadline(LocalDateTime.of(1000, 7, 12, 0, 0)) - .build()); - - PostOption postOption1 = postOptionRepository.save( - PostOption.builder() - .post(closedPost_V1) - .sequence(1) - .content("치킨") - .build() - ); - voteRepository.save(Vote.builder().member(voter).postOption(postOption1).build()); - - closedPost1_V0 = postRepository.save( - Post.builder() - .writer(writer) - .postBody(PostBody.builder().title("title").content("content").build()) - .deadline(LocalDateTime.of(1001, 7, 12, 0, 0)) - .build() - ); - PostOption postOption2 = postOptionRepository.save( - PostOption.builder() - .post(closedPost1_V0) - .sequence(1) - .content("치킨") - .build() - ); - } - - @Test - @DisplayName("마감된 게시글을 최신순으로 가져온다.") - void findClosedPostsWithLatest() { - // when - List posts = postRepository.findAllByWriterWithClosingTypeAndSortTypeAndCategoryId( - writer, - PostClosingType.CLOSED, - PostSortType.LATEST, - null, - PageRequest.of(0, 10) - ); - - //then - assertAll( - () -> assertThat(posts).hasSize(2), - () -> assertThat(posts.get(0)).isEqualTo(closedPost1_V0), - () -> assertThat(posts.get(1)).isEqualTo(closedPost_V1) - ); - } - - @Test - @DisplayName("마감된 게시글을 투표순으로 가져온다.") - void findClosedPostsWithHot() { - // when - List posts = postRepository.findAllByWriterWithClosingTypeAndSortTypeAndCategoryId( - writer, - PostClosingType.CLOSED, - PostSortType.HOT, - null, - PageRequest.of(0, 10) - ); - - //then - assertAll( - () -> assertThat(posts).hasSize(2), - () -> assertThat(posts.get(0)).isEqualTo(closedPost_V1), - () -> assertThat(posts.get(1)).isEqualTo(closedPost1_V0) - ); - } - - @Test - @DisplayName("마감안된 게시글을 최신순으로 가져온다.") - void findOpenPostsWithLatest() { - // when - List posts = postRepository.findAllByWriterWithClosingTypeAndSortTypeAndCategoryId( - writer, - PostClosingType.PROGRESS, - PostSortType.LATEST, - null, - PageRequest.of(0, 10) - ); - - //then - assertAll( - () -> assertThat(posts).hasSize(2), - () -> assertThat(posts.get(0)).isEqualTo(openPost1_V1), - () -> assertThat(posts.get(1)).isEqualTo(openPost_V2) - ); - } - - @Test - @DisplayName("마감안된 게시글을 인기순으로 가져온다.") - void findOpenPostsWithHot() { - // when - List posts = postRepository.findAllByWriterWithClosingTypeAndSortTypeAndCategoryId( - writer, - PostClosingType.PROGRESS, - PostSortType.HOT, - null, - PageRequest.of(0, 10) - ); - - //then - assertAll( - () -> assertThat(posts).hasSize(2), - () -> assertThat(posts.get(0)).isEqualTo(openPost_V2), - () -> assertThat(posts.get(1)).isEqualTo(openPost1_V1) - ); - } - - @Test - @DisplayName("마감여부와 관계없이 게시글을 인기순으로 조회한다.") - void findPostsByHot() { - // when - List posts = postRepository.findAllByWriterWithClosingTypeAndSortTypeAndCategoryId( - writer, - PostClosingType.ALL, - PostSortType.HOT, - null, - PageRequest.of(0, 10) - ); + @Test + @DisplayName("회원이 작성한 게시글 수를 조회한다.") + void countPostsByWriter() { + // given + Member writer = memberTestPersister.builder().save(); + postTestPersister.postBuilder().writer(writer).save(); + postTestPersister.postBuilder().writer(writer).save(); - //then - assertAll( - () -> assertThat(posts).hasSize(4), - () -> assertThat(posts.get(0)).isEqualTo(openPost_V2), - () -> assertThat(posts.get(3)).isEqualTo(closedPost1_V0) - ); - } + // when + int result = postRepository.countByWriter(writer); + // then + assertThat(result).isEqualTo(2); } @Test @@ -847,9 +74,9 @@ void findCountsByMembers() { Member member1 = memberTestPersister.builder().save(); Member member2 = memberTestPersister.builder().save(); - postTestPersister.builder().writer(member).save(); - postTestPersister.builder().writer(member1).save(); - postTestPersister.builder().writer(member1).save(); + postTestPersister.postBuilder().writer(member).save(); + postTestPersister.postBuilder().writer(member1).save(); + postTestPersister.postBuilder().writer(member1).save(); // when List postCounts = postRepository.findCountsByMembers(List.of(member, member1, member2)); diff --git a/backend/src/test/java/com/votogether/domain/post/service/PostCommandServiceTest.java b/backend/src/test/java/com/votogether/domain/post/service/PostCommandServiceTest.java new file mode 100644 index 000000000..ffe273f53 --- /dev/null +++ b/backend/src/test/java/com/votogether/domain/post/service/PostCommandServiceTest.java @@ -0,0 +1,515 @@ +package com.votogether.domain.post.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +import com.votogether.domain.category.entity.Category; +import com.votogether.domain.member.entity.Member; +import com.votogether.domain.post.dto.request.post.PostCreateRequest; +import com.votogether.domain.post.dto.request.post.PostOptionCreateRequest; +import com.votogether.domain.post.dto.request.post.PostOptionUpdateRequest; +import com.votogether.domain.post.dto.request.post.PostUpdateRequest; +import com.votogether.domain.post.entity.Post; +import com.votogether.domain.post.entity.PostOption; +import com.votogether.domain.post.repository.PostCategoryRepository; +import com.votogether.domain.post.repository.PostContentImageRepository; +import com.votogether.domain.post.repository.PostOptionRepository; +import com.votogether.domain.post.repository.PostRepository; +import com.votogether.global.exception.BadRequestException; +import com.votogether.global.exception.ImageException; +import com.votogether.global.exception.NotFoundException; +import com.votogether.infra.image.ImageExceptionType; +import com.votogether.test.ServiceTest; +import jakarta.persistence.EntityManager; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.time.LocalDateTime; +import java.util.List; +import javax.imageio.ImageIO; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.web.multipart.MultipartFile; + +class PostCommandServiceTest extends ServiceTest { + + @Autowired + EntityManager em; + + @Autowired + PostCommandService postCommandService; + + @Autowired + PostRepository postRepository; + + @Autowired + PostCategoryRepository postCategoryRepository; + + @Autowired + PostContentImageRepository postContentImageRepository; + + @Autowired + PostOptionRepository postOptionRepository; + + @AfterEach + void tearDown( + @Value("${image.upload_directory}") String uploadDirectory + ) { + File folder = new File(uploadDirectory); + if (folder.exists()) { + deleteFileRecursive(folder); + } + } + + private static void deleteFileRecursive(File file) { + if (file.isDirectory()) { + File[] files = file.listFiles(); + if (files == null) { + return; + } + + for (File child : files) { + deleteFileRecursive(child); + } + } + file.delete(); + } + + @Test + @DisplayName("정상적인 요청이라면 게시글을 작성한다.") + void createPost() { + // given + Category categoryA = categoryTestPersister.builder().save(); + Category categoryB = categoryTestPersister.builder().save(); + PostCreateRequest postCreateRequest = mockingPostCreateRequest(List.of(categoryA.getId(), categoryB.getId())); + Member member = memberTestPersister.builder().save(); + + // when + Long postId = postCommandService.createPost(postCreateRequest, member); + + // then + Post post = postRepository.findById(postId).get(); + assertSoftly(softly -> { + softly.assertThat(post.getPostCategories()).hasSize(2); + softly.assertThat(post.getPostContentImages()).hasSize(1); + softly.assertThat(post.getPostOptions()).hasSize(2); + }); + } + + @Nested + @DisplayName("게시글 수정") + class UpdatePost { + + @Test + @DisplayName("수정된 게시글 옵션 개수가 같으면 옵션 내용이 수정된다.") + void updateSameSizeOptions() { + // given + Category categoryA = categoryTestPersister.builder().save(); + Category categoryB = categoryTestPersister.builder().save(); + PostCreateRequest postCreateRequest = mockingPostCreateRequest( + List.of( + categoryA.getId(), + categoryB.getId() + ) + ); + Member member = memberTestPersister.builder().save(); + Long postId = postCommandService.createPost(postCreateRequest, member); + PostUpdateRequest postUpdateRequest = new PostUpdateRequest( + List.of(categoryA.getId()), + "New Title", + "New Content", + "votogether.png", + mockingMultipartFile("votogether.jpg"), + List.of( + new PostOptionUpdateRequest( + null, + "option1", + "votogether.png", + null + ), + new PostOptionUpdateRequest( + null, + "option2", + null, + mockingMultipartFile("votogether.png") + ) + ), + LocalDateTime.now().plusDays(3) + ); + + // when + postCommandService.updatePost(postId, postUpdateRequest, member); + + // then + Post post = postRepository.findById(postId).get(); + assertSoftly(softly -> { + softly.assertThat(post.getPostCategories()).hasSize(1); + softly.assertThat(post.getPostContentImages()).hasSize(1); + softly.assertThat(post.getPostOptions()).hasSize(2); + }); + } + + @Test + @DisplayName("수정된 게시글 옵션 개수가 적으면 게시글 옵션이 삭제된다.") + void addPostOptions() { + // given + Category categoryA = categoryTestPersister.builder().save(); + Category categoryB = categoryTestPersister.builder().save(); + Category categoryC = categoryTestPersister.builder().save(); + PostCreateRequest postCreateRequest = mockingPostCreateRequest( + List.of( + categoryA.getId(), + categoryB.getId() + ) + ); + Member member = memberTestPersister.builder().save(); + Long postId = postCommandService.createPost(postCreateRequest, member); + PostUpdateRequest postUpdateRequest = new PostUpdateRequest( + List.of(categoryA.getId(), categoryB.getId(), categoryC.getId()), + "New Title", + "New Content", + null, + mockingMultipartFile("votogether.jpg"), + List.of( + new PostOptionUpdateRequest( + null, + "option1", + null, + null + ) + ), + LocalDateTime.now().plusDays(3) + ); + + // when + postCommandService.updatePost(postId, postUpdateRequest, member); + + // then + Post post = postRepository.findById(postId).get(); + assertSoftly(softly -> { + softly.assertThat(post.getPostCategories()).hasSize(3); + softly.assertThat(post.getPostContentImages()).hasSize(1); + softly.assertThat(post.getPostOptions()).hasSize(1); + }); + } + + @Test + @DisplayName("수정된 게시글 옵션 개수가 많으면 게시글 옵션이 추가된다.") + void removePostOptions() { + // given + Category categoryA = categoryTestPersister.builder().save(); + Category categoryB = categoryTestPersister.builder().save(); + PostCreateRequest postCreateRequest = mockingPostCreateRequest( + List.of( + categoryA.getId(), + categoryB.getId() + ) + ); + Member member = memberTestPersister.builder().save(); + Long postId = postCommandService.createPost(postCreateRequest, member); + PostUpdateRequest postUpdateRequest = new PostUpdateRequest( + List.of(categoryA.getId()), + "New Title", + "New Content", + null, + null, + List.of( + new PostOptionUpdateRequest( + null, + "option1", + "vogother.png", + null + ), + new PostOptionUpdateRequest( + null, + "option2", + "votogether.png", + mockingMultipartFile("votogether.jpg") + ), + new PostOptionUpdateRequest( + null, + "option3", + null, + null + ) + ), + LocalDateTime.now().plusDays(3) + ); + + // when + postCommandService.updatePost(postId, postUpdateRequest, member); + + // then + Post post = postRepository.findById(postId).get(); + assertSoftly(softly -> { + softly.assertThat(post.getPostCategories()).hasSize(1); + softly.assertThat(post.getPostContentImages()).hasSize(0); + softly.assertThat(post.getPostOptions()).hasSize(3); + }); + } + + @Test + @DisplayName("존재하지 않는 게시글이라면 예외를 던진다.") + void emptyPost() { + // given + Member member = memberTestPersister.builder().save(); + PostUpdateRequest postUpdateRequest = mockingPostUpdateRequest(); + + // when, then + assertThatThrownBy(() -> postCommandService.updatePost(-1L, postUpdateRequest, member)) + .isInstanceOf(NotFoundException.class) + .hasMessage("게시글이 존재하지 않습니다."); + } + + @Test + @DisplayName("블라인드된 게시글이라면 예외를 던진다.") + void blindPost() { + // given + Member member = memberTestPersister.builder().save(); + PostUpdateRequest postUpdateRequest = mockingPostUpdateRequest(); + Post post = postTestPersister.postBuilder().writer(member).save(); + post.blind(); + + // when, then + assertThatThrownBy(() -> postCommandService.updatePost(post.getId(), postUpdateRequest, member)) + .isInstanceOf(BadRequestException.class) + .hasMessage("신고에 의해 숨겨진 게시글은 접근할 수 없습니다."); + } + + @Test + @DisplayName("게시글 작성자가 아니라면 예외를 던진다.") + void postNotWriter() { + // given + Member member = memberTestPersister.builder().save(); + PostUpdateRequest postUpdateRequest = mockingPostUpdateRequest(); + Post post = postTestPersister.postBuilder().save(); + + // when, then + assertThatThrownBy(() -> postCommandService.updatePost(post.getId(), postUpdateRequest, member)) + .isInstanceOf(BadRequestException.class) + .hasMessage("게시글 작성자가 아닙니다."); + } + + } + + @Nested + @DisplayName("게시글 조기 마감") + class ClosePostEarly { + + @Test + @DisplayName("정상적인 요청이라면 게시글을 조기 마감한다.") + void success() { + // given + Member member = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().writer(member).save(); + + // when + postCommandService.closePostEarly(post.getId(), member); + + // then + assertThat(post.isClosed()).isTrue(); + } + + @Test + @DisplayName("존재하지 않는 게시글이라면 예외를 던진다.") + void emptyPost() { + // given + Member member = memberTestPersister.builder().save(); + + // when, then + assertThatThrownBy(() -> postCommandService.closePostEarly(-1L, member)) + .isInstanceOf(NotFoundException.class) + .hasMessage("게시글이 존재하지 않습니다."); + } + + @Test + @DisplayName("블라인드된 게시글이라면 예외를 던진다.") + void blindPost() { + // given + Member member = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().writer(member).save(); + post.blind(); + + // when, then + assertThatThrownBy(() -> postCommandService.closePostEarly(post.getId(), member)) + .isInstanceOf(BadRequestException.class) + .hasMessage("신고에 의해 숨겨진 게시글은 접근할 수 없습니다."); + } + + @Test + @DisplayName("게시글 작성자가 아니라면 예외를 던진다.") + void postNotWriter() { + // given + Member member = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().save(); + + // when, then + assertThatThrownBy(() -> postCommandService.closePostEarly(post.getId(), member)) + .isInstanceOf(BadRequestException.class) + .hasMessage("게시글 작성자가 아닙니다."); + } + + } + + @Nested + @DisplayName("게시글 삭제") + class DeletePost { + + @Test + @DisplayName("정상적인 요청이라면 게시글을 삭제한다.") + void success() { + // given + Category categoryA = categoryTestPersister.builder().save(); + Category categoryB = categoryTestPersister.builder().save(); + PostCreateRequest postCreateRequest = mockingPostCreateRequest( + List.of(categoryA.getId(), categoryB.getId())); + Member member = memberTestPersister.builder().save(); + Long postId = postCommandService.createPost(postCreateRequest, member); + + em.flush(); + em.clear(); + + // when + postCommandService.deletePost(postId, member); + + // then + assertSoftly(softly -> { + softly.assertThat(postRepository.findAll()).isEmpty(); + softly.assertThat(postCategoryRepository.findAll()).isEmpty(); + softly.assertThat(postContentImageRepository.findAll()).isEmpty(); + softly.assertThat(postOptionRepository.findAll()).isEmpty(); + }); + } + + @Test + @DisplayName("존재하지 않는 게시글이라면 예외를 던진다.") + void emptyPost() { + // given + Member member = memberTestPersister.builder().save(); + + // when, then + assertThatThrownBy(() -> postCommandService.deletePost(-1L, member)) + .isInstanceOf(NotFoundException.class) + .hasMessage("게시글이 존재하지 않습니다."); + } + + @Test + @DisplayName("블라인드된 게시글이라면 예외를 던진다.") + void blindPost() { + // given + Member member = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().writer(member).save(); + post.blind(); + + // when, then + assertThatThrownBy(() -> postCommandService.deletePost(post.getId(), member)) + .isInstanceOf(BadRequestException.class) + .hasMessage("신고에 의해 숨겨진 게시글은 접근할 수 없습니다."); + } + + @Test + @DisplayName("게시글 작성자가 아니라면 예외를 던진다.") + void postNotWriter() { + // given + Member member = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().save(); + + // when, then + assertThatThrownBy(() -> postCommandService.deletePost(post.getId(), member)) + .isInstanceOf(BadRequestException.class) + .hasMessage("게시글 작성자가 아닙니다."); + } + + @Test + @DisplayName("게시글을 삭제할 수 없는 상태라면 예외를 던진다.") + void invalidDelete() { + // given + Member member = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().writer(member).save(); + PostOption postOption = postTestPersister.postOptionBuilder().post(post).sequence(1).save(); + post.addPostOption(postOption); + ReflectionTestUtils.setField(postOption, "voteCount", 20); + + // when, then + assertThatThrownBy(() -> postCommandService.deletePost(post.getId(), member)) + .isInstanceOf(BadRequestException.class) + .hasMessage("일정 투표 수 이상의 게시글은 삭제할 수 없습니다."); + } + + } + + private PostCreateRequest mockingPostCreateRequest(List categoryIds) { + return new PostCreateRequest( + categoryIds, + "title", + "content", + mockingMultipartFile("votogether.png"), + List.of( + new PostOptionCreateRequest( + "option1", + mockingMultipartFile("votogether.png") + ), + new PostOptionCreateRequest( + "option2", + null + ) + ), + LocalDateTime.now().plusDays(3) + ); + } + + private PostUpdateRequest mockingPostUpdateRequest() { + return new PostUpdateRequest( + List.of(1L), + "New Title", + "New Content", + "votogether.png", + mockingMultipartFile("votogether.jpg"), + List.of( + new PostOptionUpdateRequest( + 1L, + "option1", + "votogether.png", + null + ), + new PostOptionUpdateRequest( + 2L, + "option2", + null, + mockingMultipartFile("votogether.png") + ) + ), + LocalDateTime.now().plusDays(3) + ); + } + + private MultipartFile mockingMultipartFile(String fileName) { + return new MockMultipartFile( + "images", + fileName, + MediaType.IMAGE_JPEG_VALUE, + generateMockImage() + ); + } + + private byte[] generateMockImage() { + BufferedImage image = new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB); + + try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) { + ImageIO.write(image, "jpg", byteArrayOutputStream); + return byteArrayOutputStream.toByteArray(); + } catch (IOException e) { + throw new ImageException(ImageExceptionType.IMAGE_TRANSFER); + } + } + +} diff --git a/backend/src/test/java/com/votogether/domain/post/service/PostCommentServiceTest.java b/backend/src/test/java/com/votogether/domain/post/service/PostCommentServiceTest.java index 749d74d3d..1c797abb5 100644 --- a/backend/src/test/java/com/votogether/domain/post/service/PostCommentServiceTest.java +++ b/backend/src/test/java/com/votogether/domain/post/service/PostCommentServiceTest.java @@ -4,127 +4,116 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import com.votogether.domain.member.entity.Member; -import com.votogether.domain.member.repository.MemberRepository; -import com.votogether.domain.post.dto.request.comment.CommentRegisterRequest; +import com.votogether.domain.post.dto.request.comment.CommentCreateRequest; import com.votogether.domain.post.dto.request.comment.CommentUpdateRequest; import com.votogether.domain.post.dto.response.comment.CommentResponse; import com.votogether.domain.post.entity.Post; -import com.votogether.domain.post.entity.PostBody; import com.votogether.domain.post.entity.comment.Comment; -import com.votogether.domain.post.repository.CommentRepository; -import com.votogether.domain.post.repository.PostRepository; import com.votogether.global.exception.BadRequestException; import com.votogether.global.exception.NotFoundException; -import com.votogether.test.annotation.ServiceTest; +import com.votogether.test.ServiceTest; import com.votogether.test.fixtures.MemberFixtures; -import java.time.LocalDateTime; import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -@ServiceTest -class PostCommentServiceTest { +class PostCommentServiceTest extends ServiceTest { @Autowired PostCommentService postCommentService; - @Autowired - MemberRepository memberRepository; + @Nested + @DisplayName("게시글 댓글 목록 조회") + class GetComments { - @Autowired - PostRepository postRepository; + @Test + @DisplayName("게시글 댓글 목록을 조회한다.") + void getComments() { + // given + Member writer = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().writer(writer).save(); + Comment commentA = commentTestPersister.builder().post(post).writer(writer).save(); + Comment commentB = commentTestPersister.builder().post(post).writer(writer).save(); - @Autowired - CommentRepository commentRepository; + // when + List response = postCommentService.getComments(post.getId()); - @Nested - @DisplayName("게시글 댓글 등록") - class CreateComment { + // then + assertThat(response).usingRecursiveComparison() + .isEqualTo(List.of(CommentResponse.from(commentA), CommentResponse.from(commentB))); + } @Test @DisplayName("존재하지 않는 게시글이라면 예외를 던진다.") void emptyPost() { + // given, when, then + assertThatThrownBy(() -> postCommentService.getComments(-1L)) + .isInstanceOf(NotFoundException.class) + .hasMessage("게시글이 존재하지 않습니다."); + } + + @Test + @DisplayName("블라인드된 게시글이라면 예외를 던진다.") + void blindPost() { // given - Member member = memberRepository.save(MemberFixtures.MALE_20.get()); - CommentRegisterRequest commentRegisterRequest = new CommentRegisterRequest("hello"); + Post post = postTestPersister.postBuilder().save(); + post.blind(); // when, then - assertThatThrownBy(() -> postCommentService.createComment(member, -1L, commentRegisterRequest)) - .isInstanceOf(NotFoundException.class) - .hasMessage("해당 게시글이 존재하지 않습니다."); + assertThatThrownBy(() -> postCommentService.getComments(post.getId())) + .isInstanceOf(BadRequestException.class) + .hasMessage("신고에 의해 숨겨진 게시글은 접근할 수 없습니다."); } + } + + @Nested + @DisplayName("게시글 댓글 등록") + class CreateComment { + @Test @DisplayName("게시글에 댓글을 등록한다.") void createComment() { // given - Member member = memberRepository.save(MemberFixtures.MALE_20.get()); - Post post = postRepository.save( - Post.builder() - .writer(member) - .postBody(PostBody.builder().title("title").content("content").build()) - .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) - .build() - ); - CommentRegisterRequest commentRegisterRequest = new CommentRegisterRequest("hello"); + Member member = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().writer(member).save(); + CommentCreateRequest commentCreateRequest = new CommentCreateRequest("hello"); // when - postCommentService.createComment(member, post.getId(), commentRegisterRequest); + postCommentService.createComment(post.getId(), commentCreateRequest, member); // then - assertThat(commentRepository.findAll()).hasSize(1); + assertThat(postCommentService.getComments(post.getId())).hasSize(1); } - } - - @Nested - @DisplayName("게시글 댓글 목록 조회") - class GetComments { - @Test @DisplayName("존재하지 않는 게시글이라면 예외를 던진다.") void emptyPost() { - // given, when, then - assertThatThrownBy(() -> postCommentService.getComments(-1L)) + // given + Member member = MemberFixtures.MALE_20.get(); + CommentCreateRequest commentCreateRequest = new CommentCreateRequest("hello"); + + // when, then + assertThatThrownBy(() -> postCommentService.createComment(-1L, commentCreateRequest, member)) .isInstanceOf(NotFoundException.class) - .hasMessage("해당 게시글이 존재하지 않습니다."); + .hasMessage("게시글이 존재하지 않습니다."); } @Test - @DisplayName("게시글 댓글 목록을 조회한다.") - void getComments() { + @DisplayName("블라인드된 게시글이라면 예외를 던진다.") + void blindPost() { // given - Member member = memberRepository.save(MemberFixtures.MALE_20.get()); - Post post = postRepository.save( - Post.builder() - .writer(member) - .postBody(PostBody.builder().title("titleA").content("contentA").build()) - .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) - .build() - ); - Comment commentA = commentRepository.save( - Comment.builder() - .member(member) - .post(post) - .content("commentA") - .build() - ); - Comment commentB = commentRepository.save( - Comment.builder() - .member(member) - .post(post) - .content("commentB") - .build() - ); + Member member = MemberFixtures.MALE_20.get(); + CommentCreateRequest commentCreateRequest = new CommentCreateRequest("hello"); + Post post = postTestPersister.postBuilder().save(); + post.blind(); - // when - List response = postCommentService.getComments(post.getId()); - - // then - assertThat(response).usingRecursiveComparison() - .isEqualTo(List.of(CommentResponse.from(commentA), CommentResponse.from(commentB))); + // when, then + assertThatThrownBy(() -> postCommentService.createComment(post.getId(), commentCreateRequest, member)) + .isInstanceOf(BadRequestException.class) + .hasMessage("신고에 의해 숨겨진 게시글은 접근할 수 없습니다."); } } @@ -133,251 +122,213 @@ void getComments() { @DisplayName("게시글 댓글 수정") class UpdateComment { + @Test + @DisplayName("게시글의 댓글을 수정한다.") + void deleteComment() { + // given + Member member = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().writer(member).save(); + Comment comment = commentTestPersister.builder().post(post).writer(member).save(); + CommentUpdateRequest request = new CommentUpdateRequest("hello"); + + // when + postCommentService.updateComment(post.getId(), comment.getId(), request, member); + + // then + assertThat(comment.getContent()).isEqualTo("hello"); + } + @Test @DisplayName("존재하지 않는 게시글이라면 예외를 던진다.") void emptyPost() { // given - Member member = memberRepository.save(MemberFixtures.MALE_20.get()); + Member member = MemberFixtures.MALE_20.get(); CommentUpdateRequest request = new CommentUpdateRequest("hello"); // when, then assertThatThrownBy(() -> postCommentService.updateComment(-1L, 1L, request, member)) .isInstanceOf(NotFoundException.class) - .hasMessage("해당 게시글이 존재하지 않습니다."); + .hasMessage("게시글이 존재하지 않습니다."); } @Test @DisplayName("존재하지 않는 댓글이라면 예외를 던진다.") void emptyComment() { // given - Member member = memberRepository.save(MemberFixtures.MALE_20.get()); - Post post = postRepository.save( - Post.builder() - .writer(member) - .postBody(PostBody.builder().title("title").content("content").build()) - .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) - .build() - ); + Member member = MemberFixtures.MALE_20.get(); + Post post = postTestPersister.postBuilder().save(); CommentUpdateRequest request = new CommentUpdateRequest("hello"); // when, then assertThatThrownBy(() -> postCommentService.updateComment(post.getId(), -1L, request, member)) .isInstanceOf(NotFoundException.class) - .hasMessage("해당 댓글이 존재하지 않습니다."); + .hasMessage("댓글이 존재하지 않습니다."); + } + + @Test + @DisplayName("블라인드된 게시글이라면 예외를 던진다.") + void blindPost() { + // given + Member member = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().writer(member).save(); + Comment comment = commentTestPersister.builder().post(post).writer(member).save(); + CommentUpdateRequest request = new CommentUpdateRequest("hello"); + post.blind(); + + // when, then + assertThatThrownBy(() -> postCommentService.updateComment(post.getId(), comment.getId(), request, member)) + .isInstanceOf(BadRequestException.class) + .hasMessage("신고에 의해 숨겨진 게시글은 접근할 수 없습니다."); + } + + @Test + @DisplayName("블라인드된 댓글이라면 예외를 던진다.") + void blindComment() { + // given + Member member = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().writer(member).save(); + Comment comment = commentTestPersister.builder().post(post).writer(member).save(); + CommentUpdateRequest request = new CommentUpdateRequest("hello"); + comment.blind(); + + // when, then + assertThatThrownBy(() -> postCommentService.updateComment(post.getId(), comment.getId(), request, member)) + .isInstanceOf(BadRequestException.class) + .hasMessage("신고에 의해 숨겨진 댓글은 접근할 수 없습니다."); } @Test @DisplayName("댓글의 게시글과 일치하지 않으면 예외를 던진다.") void invalidBelongPost() { // given - Member member = memberRepository.save(MemberFixtures.MALE_20.get()); - Post postA = postRepository.save( - Post.builder() - .writer(member) - .postBody(PostBody.builder().title("titleA").content("contentA").build()) - .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) - .build() - ); - Post postB = postRepository.save( - Post.builder() - .writer(member) - .postBody(PostBody.builder().title("titleB").content("contentB").build()) - .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) - .build() - ); - Comment comment = commentRepository.save( - Comment.builder() - .member(member) - .post(postA) - .content("comment") - .build() - ); + Member member = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().writer(member).save(); + Comment comment = commentTestPersister.builder().writer(member).save(); CommentUpdateRequest request = new CommentUpdateRequest("hello"); // when, then - assertThatThrownBy(() -> postCommentService.updateComment(postB.getId(), comment.getId(), request, member)) + assertThatThrownBy(() -> postCommentService.updateComment(post.getId(), comment.getId(), request, member)) .isInstanceOf(BadRequestException.class) - .hasMessage("댓글의 게시글 정보와 일치하지 않습니다."); + .hasMessage("게시글의 댓글이 아닙니다."); } @Test @DisplayName("댓글의 작성자가 아니라면 예외를 던진다.") void invalidWriter() { // given - Member memberA = memberRepository.save(MemberFixtures.MALE_20.get()); - Member memberB = memberRepository.save(MemberFixtures.FEMALE_20.get()); - Post post = postRepository.save( - Post.builder() - .writer(memberA) - .postBody(PostBody.builder().title("titleA").content("contentA").build()) - .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) - .build() - ); - Comment comment = commentRepository.save( - Comment.builder() - .member(memberB) - .post(post) - .content("comment") - .build() - ); + Member member = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().writer(member).save(); + Comment comment = commentTestPersister.builder().post(post).save(); CommentUpdateRequest request = new CommentUpdateRequest("hello"); // when, then - assertThatThrownBy(() -> postCommentService.updateComment(post.getId(), comment.getId(), request, memberA)) + assertThatThrownBy(() -> postCommentService.updateComment(post.getId(), comment.getId(), request, member)) .isInstanceOf(BadRequestException.class) .hasMessage("댓글 작성자가 아닙니다."); } + } + + @Nested + @DisplayName("게시글 댓글 삭제") + class DeleteComment { + @Test - @DisplayName("게시글의 댓글을 수정한다.") + @DisplayName("게시글의 댓글을 삭제한다.") void deleteComment() { // given - Member member = memberRepository.save(MemberFixtures.MALE_20.get()); - Post post = postRepository.save( - Post.builder() - .writer(member) - .postBody(PostBody.builder().title("titleA").content("contentA").build()) - .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) - .build() - ); - Comment comment = commentRepository.save( - Comment.builder() - .member(member) - .post(post) - .content("comment") - .build() - ); - CommentUpdateRequest request = new CommentUpdateRequest("hello"); + Member member = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().writer(member).save(); + Comment comment = commentTestPersister.builder().post(post).writer(member).save(); // when - postCommentService.updateComment(post.getId(), comment.getId(), request, member); + postCommentService.deleteComment(post.getId(), comment.getId(), member); // then - assertThat(comment.getContent()).isEqualTo("hello"); + assertThat(postCommentService.getComments(post.getId())).isEmpty(); } - } - - @Nested - @DisplayName("게시글 댓글 삭제") - class DeleteComment { - @Test @DisplayName("존재하지 않는 게시글이라면 예외를 던진다.") void emptyPost() { // given - Member member = memberRepository.save(MemberFixtures.MALE_20.get()); + Member member = MemberFixtures.MALE_20.get(); // when, then assertThatThrownBy(() -> postCommentService.deleteComment(-1L, 1L, member)) .isInstanceOf(NotFoundException.class) - .hasMessage("해당 게시글이 존재하지 않습니다."); + .hasMessage("게시글이 존재하지 않습니다."); } @Test @DisplayName("존재하지 않는 댓글이라면 예외를 던진다.") void emptyComment() { // given - Member member = memberRepository.save(MemberFixtures.MALE_20.get()); - Post post = postRepository.save( - Post.builder() - .writer(member) - .postBody(PostBody.builder().title("title").content("content").build()) - .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) - .build() - ); + Member member = MemberFixtures.MALE_20.get(); + Post post = postTestPersister.postBuilder().save(); // when, then assertThatThrownBy(() -> postCommentService.deleteComment(post.getId(), -1L, member)) .isInstanceOf(NotFoundException.class) - .hasMessage("해당 댓글이 존재하지 않습니다."); + .hasMessage("댓글이 존재하지 않습니다."); } @Test - @DisplayName("댓글의 게시글과 일치하지 않으면 예외를 던진다.") - void invalidBelongPost() { + @DisplayName("블라인드된 게시글이라면 예외를 던진다.") + void blindPost() { // given - Member member = memberRepository.save(MemberFixtures.MALE_20.get()); - Post postA = postRepository.save( - Post.builder() - .writer(member) - .postBody(PostBody.builder().title("titleA").content("contentA").build()) - .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) - .build() - ); - Post postB = postRepository.save( - Post.builder() - .writer(member) - .postBody(PostBody.builder().title("titleB").content("contentB").build()) - .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) - .build() - ); - Comment comment = commentRepository.save( - Comment.builder() - .member(member) - .post(postA) - .content("comment") - .build() - ); + Member member = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().writer(member).save(); + Comment comment = commentTestPersister.builder().post(post).writer(member).save(); + post.blind(); // when, then - assertThatThrownBy(() -> postCommentService.deleteComment(postB.getId(), comment.getId(), member)) + assertThatThrownBy(() -> postCommentService.deleteComment(post.getId(), comment.getId(), member)) .isInstanceOf(BadRequestException.class) - .hasMessage("댓글의 게시글 정보와 일치하지 않습니다."); + .hasMessage("신고에 의해 숨겨진 게시글은 접근할 수 없습니다."); } @Test - @DisplayName("댓글의 작성자가 아니라면 예외를 던진다.") - void invalidWriter() { + @DisplayName("블라인드된 댓글이라면 예외를 던진다.") + void blindComment() { // given - Member memberA = memberRepository.save(MemberFixtures.MALE_20.get()); - Member memberB = memberRepository.save(MemberFixtures.FEMALE_20.get()); - Post post = postRepository.save( - Post.builder() - .writer(memberA) - .postBody(PostBody.builder().title("titleA").content("contentA").build()) - .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) - .build() - ); - Comment comment = commentRepository.save( - Comment.builder() - .member(memberB) - .post(post) - .content("comment") - .build() - ); + Member member = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().writer(member).save(); + Comment comment = commentTestPersister.builder().post(post).writer(member).save(); + comment.blind(); // when, then - assertThatThrownBy(() -> postCommentService.deleteComment(post.getId(), comment.getId(), memberA)) + assertThatThrownBy(() -> postCommentService.deleteComment(post.getId(), comment.getId(), member)) .isInstanceOf(BadRequestException.class) - .hasMessage("댓글 작성자가 아닙니다."); + .hasMessage("신고에 의해 숨겨진 댓글은 접근할 수 없습니다."); } @Test - @DisplayName("게시글의 댓글을 삭제한다.") - void deleteComment() { + @DisplayName("댓글의 게시글과 일치하지 않으면 예외를 던진다.") + void invalidBelongPost() { // given - Member member = memberRepository.save(MemberFixtures.MALE_20.get()); - Post post = postRepository.save( - Post.builder() - .writer(member) - .postBody(PostBody.builder().title("titleA").content("contentA").build()) - .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) - .build() - ); - Comment comment = commentRepository.save( - Comment.builder() - .member(member) - .post(post) - .content("comment") - .build() - ); + Member member = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().writer(member).save(); + Comment comment = commentTestPersister.builder().writer(member).save(); - // when - postCommentService.deleteComment(post.getId(), comment.getId(), member); + // when, then + assertThatThrownBy(() -> postCommentService.deleteComment(post.getId(), comment.getId(), member)) + .isInstanceOf(BadRequestException.class) + .hasMessage("게시글의 댓글이 아닙니다."); + } - // then - assertThat(commentRepository.findAll()).isEmpty(); + @Test + @DisplayName("댓글의 작성자가 아니라면 예외를 던진다.") + void invalidWriter() { + // given + Member member = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().writer(member).save(); + Comment comment = commentTestPersister.builder().post(post).save(); + + // when, then + assertThatThrownBy(() -> postCommentService.deleteComment(post.getId(), comment.getId(), member)) + .isInstanceOf(BadRequestException.class) + .hasMessage("댓글 작성자가 아닙니다."); } } diff --git a/backend/src/test/java/com/votogether/domain/post/service/PostGuestServiceTest.java b/backend/src/test/java/com/votogether/domain/post/service/PostGuestServiceTest.java new file mode 100644 index 000000000..ffb2a0092 --- /dev/null +++ b/backend/src/test/java/com/votogether/domain/post/service/PostGuestServiceTest.java @@ -0,0 +1,237 @@ +package com.votogether.domain.post.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.votogether.domain.member.entity.Member; +import com.votogether.domain.post.dto.response.post.PostOptionVoteResultResponse; +import com.votogether.domain.post.dto.response.post.PostResponse; +import com.votogether.domain.post.dto.response.post.PostVoteResultResponse; +import com.votogether.domain.post.dto.response.post.PostWriterResponse; +import com.votogether.domain.post.entity.Post; +import com.votogether.domain.post.entity.PostOption; +import com.votogether.domain.post.entity.vo.PostClosingType; +import com.votogether.domain.post.entity.vo.PostSortType; +import com.votogether.domain.vote.entity.Vote; +import com.votogether.global.exception.BadRequestException; +import com.votogether.global.exception.NotFoundException; +import com.votogether.test.ServiceTest; +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.util.ReflectionTestUtils; + +class PostGuestServiceTest extends ServiceTest { + + @Autowired + PostGuestService postGuestService; + + @Nested + @DisplayName("비회원 게시글 목록 조회") + class GuestGetPosts { + + @Test + @DisplayName("마감된 게시글은 결과를 확인할 수 있다.") + void canCheckResultsClosedPosts() { + // given + LocalDateTime deadline = LocalDateTime.now().minusDays(1); + Member writer = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().deadline(deadline).writer(writer).save(); + PostOption postOption = postTestPersister.postOptionBuilder().post(post).sequence(1).save(); + Vote vote = voteTestPersister.builder().postOption(postOption).save(); + postOption.addVote(vote); + post.addPostOption(postOption); + ReflectionTestUtils.setField(postOption, "voteCount", 1); + + // when + List result = postGuestService.getPosts(0, PostClosingType.ALL, PostSortType.LATEST, null); + + // then + PostResponse expected = expectedResponse(post, writer, postOption, 0L, 1, 1, 1.0); + assertThat(result).usingRecursiveComparison().isEqualTo(List.of(expected)); + } + + @Test + @DisplayName("진행중인 게시글은 결과를 확인할 수 없다.") + void cannotCheckResultsOpenPosts() { + // given + LocalDateTime deadline = LocalDateTime.now().plusDays(1); + Member writer = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().deadline(deadline).writer(writer).save(); + PostOption postOption = postTestPersister.postOptionBuilder().post(post).sequence(1).save(); + Vote vote = voteTestPersister.builder().postOption(postOption).save(); + postOption.addVote(vote); + post.addPostOption(postOption); + + // when + List result = postGuestService.getPosts(0, PostClosingType.ALL, PostSortType.LATEST, null); + + // then + PostResponse expected = expectedResponse(post, writer, postOption, 0L, -1, -1, 0.0); + assertThat(result).usingRecursiveComparison().isEqualTo(List.of(expected)); + } + + } + + @Nested + @DisplayName("비회원 게시글 상세 조회") + class GuestGetPost { + + @Test + @DisplayName("마감된 게시글은 결과를 확인할 수 있다.") + void canCheckResultsClosedPost() { + // given + LocalDateTime deadline = LocalDateTime.now().minusDays(1); + Member writer = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().deadline(deadline).writer(writer).save(); + PostOption postOption = postTestPersister.postOptionBuilder().post(post).sequence(1).save(); + Vote vote = voteTestPersister.builder().postOption(postOption).save(); + postOption.addVote(vote); + post.addPostOption(postOption); + ReflectionTestUtils.setField(postOption, "voteCount", 1); + + // when + PostResponse result = postGuestService.getPost(post.getId()); + + // then + PostResponse expected = expectedResponse(post, writer, postOption, 0L, 1, 1, 1.0); + assertThat(result).usingRecursiveComparison().isEqualTo(expected); + } + + @Test + @DisplayName("진행중인 게시글은 결과를 확인할 수 없다.") + void cannotCheckResultsOpenPosts() { + // given + LocalDateTime deadline = LocalDateTime.now().plusDays(1); + Member writer = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().deadline(deadline).writer(writer).save(); + PostOption postOption = postTestPersister.postOptionBuilder().post(post).sequence(1).save(); + Vote vote = voteTestPersister.builder().postOption(postOption).save(); + postOption.addVote(vote); + post.addPostOption(postOption); + + // when + PostResponse result = postGuestService.getPost(post.getId()); + + // then + PostResponse expected = expectedResponse(post, writer, postOption, 0L, -1, -1, 0.0); + assertThat(result).usingRecursiveComparison().isEqualTo(expected); + } + + @Test + @DisplayName("존재하지 않는 게시글이라면 예외를 던진다.") + void throwExceptionNotFoundPost() { + // given + Long postId = -1L; + + // when, then + assertThatThrownBy(() -> postGuestService.getPost(postId)) + .isInstanceOf(NotFoundException.class) + .hasMessage("게시글이 존재하지 않습니다."); + } + + @Test + @DisplayName("블라인드된 게시글이라면 예외를 던진다.") + void throwExceptionBlindPost() { + // given + Post post = postTestPersister.postBuilder().save(); + post.blind(); + + // when, then + assertThatThrownBy(() -> postGuestService.getPost(post.getId())) + .isInstanceOf(BadRequestException.class) + .hasMessage("신고에 의해 숨겨진 게시글은 접근할 수 없습니다."); + } + + } + + @Nested + @DisplayName("비회원 게시글 목록 검색") + class GuestSearchPosts { + + @Test + @DisplayName("마감된 게시글은 결과를 확인할 수 있다.") + void canCheckResultsClosedPosts() { + // given + LocalDateTime deadline = LocalDateTime.now().minusDays(1); + Member writer = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().deadline(deadline).writer(writer).save(); + PostOption postOption = postTestPersister.postOptionBuilder().post(post).sequence(1).save(); + Vote vote = voteTestPersister.builder().postOption(postOption).save(); + postOption.addVote(vote); + post.addPostOption(postOption); + ReflectionTestUtils.setField(postOption, "voteCount", 1); + + // when + List result = + postGuestService.searchPosts(post.getTitle(), 0, PostClosingType.ALL, PostSortType.LATEST); + + // then + PostResponse expected = expectedResponse(post, writer, postOption, 0L, 1, 1, 1.0); + assertThat(result).usingRecursiveComparison().isEqualTo(List.of(expected)); + } + + @Test + @DisplayName("진행중인 게시글은 결과를 확인할 수 없다.") + void cannotCheckResultsOpenPosts() { + // given + LocalDateTime deadline = LocalDateTime.now().plusDays(1); + Member writer = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().deadline(deadline).writer(writer).save(); + PostOption postOption = postTestPersister.postOptionBuilder().post(post).sequence(1).save(); + Vote vote = voteTestPersister.builder().postOption(postOption).save(); + postOption.addVote(vote); + post.addPostOption(postOption); + + // when + List result = + postGuestService.searchPosts(post.getTitle(), 0, PostClosingType.ALL, PostSortType.LATEST); + + // then + PostResponse expected = expectedResponse(post, writer, postOption, 0L, -1, -1, 0.0); + assertThat(result).usingRecursiveComparison().isEqualTo(List.of(expected)); + } + + } + + private PostResponse expectedResponse( + final Post post, + final Member writer, + final PostOption postOption, + final long selectedOptionId, + final int totalVoteCount, + final int postOptionVoteCount, + final double votePercent + ) { + return new PostResponse( + post.getId(), + new PostWriterResponse(writer.getId(), writer.getNickname()), + post.getTitle(), + post.getContent(), + "", + Collections.emptyList(), + post.getCreatedAt(), + post.getDeadline(), + 0, + 0, + new PostVoteResultResponse( + selectedOptionId, + totalVoteCount, + List.of( + new PostOptionVoteResultResponse( + postOption.getId(), + postOption.getContent(), + "", + postOptionVoteCount, + votePercent + ) + ) + ) + ); + } + +} diff --git a/backend/src/test/java/com/votogether/domain/post/service/PostQueryServiceTest.java b/backend/src/test/java/com/votogether/domain/post/service/PostQueryServiceTest.java new file mode 100644 index 000000000..87a351b46 --- /dev/null +++ b/backend/src/test/java/com/votogether/domain/post/service/PostQueryServiceTest.java @@ -0,0 +1,699 @@ +package com.votogether.domain.post.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.votogether.domain.member.entity.Member; +import com.votogether.domain.member.entity.vo.AgeRange; +import com.votogether.domain.member.entity.vo.Gender; +import com.votogether.domain.post.dto.response.post.PostOptionVoteResultResponse; +import com.votogether.domain.post.dto.response.post.PostResponse; +import com.votogether.domain.post.dto.response.post.PostVoteResultResponse; +import com.votogether.domain.post.dto.response.post.PostWriterResponse; +import com.votogether.domain.post.dto.response.vote.VoteCountForAgeGroupResponse; +import com.votogether.domain.post.dto.response.vote.VoteOptionStatisticsResponse; +import com.votogether.domain.post.entity.Post; +import com.votogether.domain.post.entity.PostOption; +import com.votogether.domain.post.entity.vo.PostClosingType; +import com.votogether.domain.post.entity.vo.PostSortType; +import com.votogether.domain.vote.entity.Vote; +import com.votogether.global.exception.BadRequestException; +import com.votogether.global.exception.NotFoundException; +import com.votogether.test.ServiceTest; +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.util.ReflectionTestUtils; + +class PostQueryServiceTest extends ServiceTest { + + @Autowired + PostQueryService postQueryService; + + @Nested + @DisplayName("회원 게시글 목록 조회") + class FindPosts { + + @Test + @DisplayName("내가 쓴 게시글도 아니고 투표한 게시글도 아닐 때 진행중인 게시글은 결과를 확인할 수 없다.") + void findPostsOpen() { + // given + LocalDateTime deadline = LocalDateTime.now().plusDays(1); + Member writer = memberTestPersister.builder().save(); + Member member = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().deadline(deadline).writer(writer).save(); + PostOption postOption = postTestPersister.postOptionBuilder().post(post).sequence(1).save(); + post.addPostOption(postOption); + + // when + List result = + postQueryService.getPosts(0, PostClosingType.ALL, PostSortType.LATEST, null, member); + + // then + PostResponse expected = expectedResponse(post, writer, postOption, 0L, -1, -1, 0.0); + assertThat(result).usingRecursiveComparison().isEqualTo(List.of(expected)); + } + + @Test + @DisplayName("마감된 게시글은 결과를 확인할 수 있다.") + void findPostsClosed() { + // given + LocalDateTime deadline = LocalDateTime.now().minusDays(1); + Member writer = memberTestPersister.builder().save(); + Member member = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().deadline(deadline).writer(writer).save(); + PostOption postOption = postTestPersister.postOptionBuilder().post(post).sequence(1).save(); + Vote vote = voteTestPersister.builder().postOption(postOption).save(); + postOption.addVote(vote); + post.addPostOption(postOption); + ReflectionTestUtils.setField(postOption, "voteCount", 1); + + // when + List result = + postQueryService.getPosts(0, PostClosingType.ALL, PostSortType.LATEST, null, member); + + // then + PostResponse expected = expectedResponse(post, writer, postOption, 0L, 1, 1, 1.0); + assertThat(result).usingRecursiveComparison().isEqualTo(List.of(expected)); + } + + @Test + @DisplayName("내가 쓴 게시글은 결과를 확인할 수 있다.") + void findPostsWritten() { + // given + LocalDateTime deadline = LocalDateTime.now().plusDays(1); + Member member = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().deadline(deadline).writer(member).save(); + PostOption postOption = postTestPersister.postOptionBuilder().post(post).sequence(1).save(); + Vote vote = voteTestPersister.builder().postOption(postOption).save(); + postOption.addVote(vote); + post.addPostOption(postOption); + ReflectionTestUtils.setField(postOption, "voteCount", 1); + + // when + List result = + postQueryService.getPosts(0, PostClosingType.ALL, PostSortType.LATEST, null, member); + + // then + PostResponse expected = expectedResponse(post, member, postOption, 0L, 1, 1, 1.0); + assertThat(result).usingRecursiveComparison().isEqualTo(List.of(expected)); + } + + @Test + @DisplayName("투표한 게시글은 결과를 확인할 수 있다.") + void findPostsVoted() { + // given + LocalDateTime deadline = LocalDateTime.now().plusDays(1); + Member writer = memberTestPersister.builder().save(); + Member member = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().deadline(deadline).writer(writer).save(); + PostOption postOption = postTestPersister.postOptionBuilder().post(post).sequence(1).save(); + Vote vote = voteTestPersister.builder().postOption(postOption).member(member).save(); + postOption.addVote(vote); + post.addPostOption(postOption); + ReflectionTestUtils.setField(postOption, "voteCount", 1); + + // when + List result = + postQueryService.getPosts(0, PostClosingType.ALL, PostSortType.LATEST, null, member); + + // then + PostResponse expected = expectedResponse(post, writer, postOption, postOption.getId(), 1, 1, 1.0); + assertThat(result).usingRecursiveComparison().isEqualTo(List.of(expected)); + } + + } + + @Nested + @DisplayName("회원 게시글 조회") + class FindPost { + + @Test + @DisplayName("존재하지 않는 게시글이면 예외를 던진다.") + void throwExceptionNotExistPost() { + // given + Member member = memberTestPersister.builder().save(); + + // when, then + assertThatThrownBy(() -> postQueryService.getPost(-1L, member)) + .isInstanceOf(NotFoundException.class) + .hasMessage("게시글이 존재하지 않습니다."); + } + + @Test + @DisplayName("블라인드된 게시글이면 예외를 던진다.") + void throwExceptionBlindPost() { + // given + Member member = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().save(); + post.blind(); + + // when, then + assertThatThrownBy(() -> postQueryService.getPost(post.getId(), member)) + .isInstanceOf(BadRequestException.class) + .hasMessage("신고에 의해 숨겨진 게시글은 접근할 수 없습니다."); + } + + @Test + @DisplayName("내가 쓴 게시글도 아니고 투표한 게시글도 아닐 때 진행중인 게시글은 결과를 확인할 수 없다.") + void findPostsOpen() { + // given + LocalDateTime deadline = LocalDateTime.now().plusDays(1); + Member writer = memberTestPersister.builder().save(); + Member member = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().deadline(deadline).writer(writer).save(); + PostOption postOption = postTestPersister.postOptionBuilder().post(post).sequence(1).save(); + post.addPostOption(postOption); + + // when + PostResponse result = postQueryService.getPost(post.getId(), member); + + // then + PostResponse expected = expectedResponse(post, writer, postOption, 0L, -1, -1, 0.0); + assertThat(result).usingRecursiveComparison().isEqualTo(expected); + } + + @Test + @DisplayName("마감된 게시글은 결과를 확인할 수 있다.") + void findPostsClosed() { + // given + LocalDateTime deadline = LocalDateTime.now().minusDays(1); + Member writer = memberTestPersister.builder().save(); + Member member = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().deadline(deadline).writer(writer).save(); + PostOption postOption = postTestPersister.postOptionBuilder().post(post).sequence(1).save(); + Vote vote = voteTestPersister.builder().postOption(postOption).save(); + postOption.addVote(vote); + post.addPostOption(postOption); + ReflectionTestUtils.setField(postOption, "voteCount", 1); + + // when + PostResponse result = postQueryService.getPost(post.getId(), member); + + // then + PostResponse expected = expectedResponse(post, writer, postOption, 0L, 1, 1, 1.0); + assertThat(result).usingRecursiveComparison().isEqualTo(expected); + } + + @Test + @DisplayName("내가 쓴 게시글은 결과를 확인할 수 없다.") + void findPostsWritten() { + // given + LocalDateTime deadline = LocalDateTime.now().plusDays(1); + Member member = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().deadline(deadline).writer(member).save(); + PostOption postOption = postTestPersister.postOptionBuilder().post(post).sequence(1).save(); + Vote vote = voteTestPersister.builder().postOption(postOption).save(); + postOption.addVote(vote); + post.addPostOption(postOption); + ReflectionTestUtils.setField(postOption, "voteCount", 1); + + // when + PostResponse result = postQueryService.getPost(post.getId(), member); + + // then + PostResponse expected = expectedResponse(post, member, postOption, 0L, 1, 1, 1.0); + assertThat(result).usingRecursiveComparison().isEqualTo(expected); + } + + @Test + @DisplayName("투표한 게시글은 결과를 확인할 수 있다.") + void findPostsVoted() { + // given + LocalDateTime deadline = LocalDateTime.now().plusDays(1); + Member writer = memberTestPersister.builder().save(); + Member member = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().deadline(deadline).writer(writer).save(); + PostOption postOption = postTestPersister.postOptionBuilder().post(post).sequence(1).save(); + Vote vote = voteTestPersister.builder().postOption(postOption).member(member).save(); + postOption.addVote(vote); + post.addPostOption(postOption); + ReflectionTestUtils.setField(postOption, "voteCount", 1); + + // when + PostResponse result = postQueryService.getPost(post.getId(), member); + + // then + PostResponse expected = expectedResponse(post, writer, postOption, postOption.getId(), 1, 1, 1.0); + assertThat(result).usingRecursiveComparison().isEqualTo(expected); + } + + } + + @Nested + @DisplayName("회원 게시글 검색") + class SearchPosts { + + @Test + @DisplayName("내가 쓴 게시글도 아니고 투표한 게시글도 아닐 때 진행중인 게시글은 결과를 확인할 수 없다.") + void searchPostsOpen() { + // given + LocalDateTime deadline = LocalDateTime.now().plusDays(1); + Member writer = memberTestPersister.builder().save(); + Member member = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().deadline(deadline).writer(writer).save(); + PostOption postOption = postTestPersister.postOptionBuilder().post(post).sequence(1).save(); + post.addPostOption(postOption); + + // when + List result = + postQueryService.searchPosts(post.getTitle(), 0, PostClosingType.ALL, PostSortType.LATEST, member); + + // then + PostResponse expected = expectedResponse(post, writer, postOption, 0L, -1, -1, 0.0); + assertThat(result).usingRecursiveComparison().isEqualTo(List.of(expected)); + } + + @Test + @DisplayName("마감된 게시글은 결과를 확인할 수 있다.") + void searchPostsClosed() { + // given + LocalDateTime deadline = LocalDateTime.now().minusDays(1); + Member writer = memberTestPersister.builder().save(); + Member member = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().deadline(deadline).writer(writer).save(); + PostOption postOption = postTestPersister.postOptionBuilder().post(post).sequence(1).save(); + Vote vote = voteTestPersister.builder().postOption(postOption).save(); + postOption.addVote(vote); + post.addPostOption(postOption); + ReflectionTestUtils.setField(postOption, "voteCount", 1); + + // when + List result = + postQueryService.searchPosts(post.getTitle(), 0, PostClosingType.ALL, PostSortType.LATEST, member); + + // then + PostResponse expected = expectedResponse(post, writer, postOption, 0L, 1, 1, 1.0); + assertThat(result).usingRecursiveComparison().isEqualTo(List.of(expected)); + } + + @Test + @DisplayName("내가 쓴 게시글은 결과를 확인할 수 없다.") + void searchPostsWritten() { + // given + LocalDateTime deadline = LocalDateTime.now().plusDays(1); + Member member = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().deadline(deadline).writer(member).save(); + PostOption postOption = postTestPersister.postOptionBuilder().post(post).sequence(1).save(); + Vote vote = voteTestPersister.builder().postOption(postOption).save(); + postOption.addVote(vote); + post.addPostOption(postOption); + ReflectionTestUtils.setField(postOption, "voteCount", 1); + + // when + List result = + postQueryService.searchPosts(post.getTitle(), 0, PostClosingType.ALL, PostSortType.LATEST, member); + + // then + PostResponse expected = expectedResponse(post, member, postOption, 0L, 1, 1, 1.0); + assertThat(result).usingRecursiveComparison().isEqualTo(List.of(expected)); + } + + @Test + @DisplayName("투표한 게시글은 결과를 확인할 수 있다.") + void searchPostsVoted() { + // given + LocalDateTime deadline = LocalDateTime.now().plusDays(1); + Member writer = memberTestPersister.builder().save(); + Member member = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().deadline(deadline).writer(writer).save(); + PostOption postOption = postTestPersister.postOptionBuilder().post(post).sequence(1).save(); + Vote vote = voteTestPersister.builder().postOption(postOption).member(member).save(); + postOption.addVote(vote); + post.addPostOption(postOption); + ReflectionTestUtils.setField(postOption, "voteCount", 1); + + // when + List result = + postQueryService.searchPosts(post.getTitle(), 0, PostClosingType.ALL, PostSortType.LATEST, member); + + // then + PostResponse expected = expectedResponse(post, writer, postOption, postOption.getId(), 1, 1, 1.0); + assertThat(result).usingRecursiveComparison().isEqualTo(List.of(expected)); + } + + } + + @Test + @DisplayName("내가 쓴 게시글 목록을 조회한다.") + void findPostWritten() { + // given + LocalDateTime open = LocalDateTime.now().plusDays(1); + LocalDateTime closed = LocalDateTime.now().minusDays(1); + Member member = memberTestPersister.builder().save(); + Post postA = postTestPersister.postBuilder().deadline(open).writer(member).save(); + Post postB = postTestPersister.postBuilder().deadline(closed).writer(member).save(); + PostOption postOptionA = postTestPersister.postOptionBuilder().post(postA).sequence(1).save(); + PostOption postOptionB = postTestPersister.postOptionBuilder().post(postB).sequence(1).save(); + Vote voteA = voteTestPersister.builder().postOption(postOptionA).save(); + Vote voteB = voteTestPersister.builder().postOption(postOptionB).save(); + postOptionA.addVote(voteA); + postOptionB.addVote(voteB); + postA.addPostOption(postOptionA); + postB.addPostOption(postOptionB); + ReflectionTestUtils.setField(postOptionA, "voteCount", 1); + ReflectionTestUtils.setField(postOptionB, "voteCount", 1); + + // when + List result = + postQueryService.getPostsWrittenByMe(0, PostClosingType.ALL, PostSortType.LATEST, member); + + // then + PostResponse expectedA = expectedResponse(postA, member, postOptionA, 0L, 1, 1, 1.0); + PostResponse expectedB = expectedResponse(postB, member, postOptionB, 0L, 1, 1, 1.0); + assertThat(result).usingRecursiveComparison().isEqualTo(List.of(expectedB, expectedA)); + } + + @Test + @DisplayName("내가 투표한 게시글 목록을 조회한다.") + void findPostsVoted() { + // given + LocalDateTime open = LocalDateTime.now().plusDays(1); + LocalDateTime closed = LocalDateTime.now().minusDays(1); + Member member = memberTestPersister.builder().save(); + Member writer = memberTestPersister.builder().save(); + Post postA = postTestPersister.postBuilder().deadline(open).writer(writer).save(); + Post postB = postTestPersister.postBuilder().deadline(closed).writer(writer).save(); + PostOption postOptionA = postTestPersister.postOptionBuilder().post(postA).sequence(1).save(); + PostOption postOptionB = postTestPersister.postOptionBuilder().post(postB).sequence(1).save(); + Vote voteA = voteTestPersister.builder().postOption(postOptionA).member(member).save(); + Vote voteB = voteTestPersister.builder().postOption(postOptionB).member(member).save(); + postOptionA.addVote(voteA); + postOptionB.addVote(voteB); + postA.addPostOption(postOptionA); + postB.addPostOption(postOptionB); + ReflectionTestUtils.setField(postOptionA, "voteCount", 1); + ReflectionTestUtils.setField(postOptionB, "voteCount", 1); + + // when + List result = + postQueryService.getPostsVotedByMe(0, PostClosingType.ALL, PostSortType.LATEST, member); + + // then + PostResponse expectedA = expectedResponse(postA, writer, postOptionA, postOptionA.getId(), 1, 1, 1.0); + PostResponse expectedB = expectedResponse(postB, writer, postOptionB, postOptionB.getId(), 1, 1, 1.0); + assertThat(result).usingRecursiveComparison().isEqualTo(List.of(expectedB, expectedA)); + } + + @Nested + @DisplayName("게시글의 투표 통계 조회") + class GetPostVoteStatistics { + + @Test + @DisplayName("존재하지 않는 게시글이라면 예외를 던진다.") + void throwExceptionNotExist() { + // given + Member member = memberTestPersister.builder().save(); + + // when, then + assertThatThrownBy(() -> postQueryService.getVoteStatistics(-1L, member)) + .isInstanceOf(NotFoundException.class) + .hasMessage("게시글이 존재하지 않습니다."); + } + + @Test + @DisplayName("블라인드된 게시글이라면 예외를 던진다.") + void throwExceptionBlindPost() { + // given + Member member = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().writer(member).save(); + post.blind(); + + // when, then + assertThatThrownBy(() -> postQueryService.getVoteStatistics(post.getId(), member)) + .isInstanceOf(BadRequestException.class) + .hasMessage("신고에 의해 숨겨진 게시글은 접근할 수 없습니다."); + } + + @Test + @DisplayName("게시글 작성자가 아니라면 예외를 던진다.") + void throwExceptionNotWriter() { + // given + LocalDateTime open = LocalDateTime.now().plusDays(1); + Member member = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().deadline(open).save(); + + // when, then + assertThatThrownBy(() -> postQueryService.getVoteStatistics(post.getId(), member)) + .isInstanceOf(BadRequestException.class) + .hasMessage("게시글 작성자가 아닙니다."); + } + + @Test + @DisplayName("게시글 작성자라면 투표 통계를 조회한다.") + void getPostVoteStatistics() { + // given + LocalDateTime open = LocalDateTime.now().plusDays(1); + Member member = memberTestPersister.builder().save(); + Member voter = memberTestPersister.builder().birthYear(1993).gender(Gender.MALE).save(); + Post post = postTestPersister.postBuilder().deadline(open).writer(member).save(); + PostOption postOption = postTestPersister.postOptionBuilder().post(post).sequence(1).save(); + Vote vote = voteTestPersister.builder().postOption(postOption).member(voter).save(); + postOption.addVote(vote); + post.addPostOption(postOption); + + // when + VoteOptionStatisticsResponse result = postQueryService.getVoteStatistics(post.getId(), member); + + // then + VoteOptionStatisticsResponse expected = new VoteOptionStatisticsResponse( + 1, + 1, + 0, + List.of( + new VoteCountForAgeGroupResponse( + AgeRange.UNDER_TEENS.getName(), + 0, + 0, + 0 + ), + new VoteCountForAgeGroupResponse( + AgeRange.TEENS.getName(), + 0, + 0, + 0 + ), + new VoteCountForAgeGroupResponse( + AgeRange.TWENTIES.getName(), + 0, + 0, + 0 + ), + new VoteCountForAgeGroupResponse( + AgeRange.THIRTIES.getName(), + 1, + 1, + 0 + ), + new VoteCountForAgeGroupResponse( + AgeRange.FORTIES.getName(), + 0, + 0, + 0 + ), + new VoteCountForAgeGroupResponse( + AgeRange.FIFTIES.getName(), + 0, + 0, + 0 + ), + new VoteCountForAgeGroupResponse( + AgeRange.OVER_SIXTIES.getName(), + 0, + 0, + 0 + ) + ) + ); + assertThat(result).usingRecursiveComparison().isEqualTo(expected); + } + + } + + @Nested + @DisplayName("게시글 옵션의 투표 통계 조회") + class GetPostOptionVoteStatistics { + + @Test + @DisplayName("존재하지 않는 게시글이라면 예외를 던진다.") + void throwExceptionNotExistPost() { + // given + Member member = memberTestPersister.builder().save(); + + // when, then + assertThatThrownBy(() -> postQueryService.getVoteOptionStatistics(-1L, 1L, member)) + .isInstanceOf(NotFoundException.class) + .hasMessage("게시글이 존재하지 않습니다."); + } + + @Test + @DisplayName("블라인드된 게시글이라면 예외를 던진다.") + void throwExceptionBlindPost() { + // given + Member member = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().writer(member).save(); + PostOption postOption = postTestPersister.postOptionBuilder().post(post).sequence(1).save(); + post.blind(); + + // when, then + assertThatThrownBy(() -> postQueryService.getVoteOptionStatistics(post.getId(), postOption.getId(), member)) + .isInstanceOf(BadRequestException.class) + .hasMessage("신고에 의해 숨겨진 게시글은 접근할 수 없습니다."); + } + + @Test + @DisplayName("존재하지 않는 게시글 옵션이라면 예외를 던진다.") + void throwExceptionNotExistOption() { + // given + Member member = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().writer(member).save(); + + // when, then + assertThatThrownBy(() -> postQueryService.getVoteOptionStatistics(post.getId(), -1L, member)) + .isInstanceOf(NotFoundException.class) + .hasMessage("게시글 투표 옵션이 존재하지 않습니다."); + } + + @Test + @DisplayName("게시글 작성자가 아니라면 예외를 던진다.") + void throwExceptionNotWriter() { + // given + LocalDateTime open = LocalDateTime.now().plusDays(1); + Member member = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().deadline(open).save(); + PostOption postOption = postTestPersister.postOptionBuilder().post(post).sequence(1).save(); + + // when, then + assertThatThrownBy(() -> postQueryService.getVoteOptionStatistics(post.getId(), postOption.getId(), member)) + .isInstanceOf(BadRequestException.class) + .hasMessage("게시글 작성자가 아닙니다."); + } + + @Test + @DisplayName("게시글의 옵션이 아니라면 예외를 던진다.") + void throwExceptionNotBelongPost() { + // given + LocalDateTime open = LocalDateTime.now().plusDays(1); + Member member = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().deadline(open).writer(member).save(); + PostOption postOption = postTestPersister.postOptionBuilder().sequence(1).save(); + + // when, then + assertThatThrownBy(() -> postQueryService.getVoteOptionStatistics(post.getId(), postOption.getId(), member)) + .isInstanceOf(BadRequestException.class) + .hasMessage("게시글의 투표 옵션이 아닙니다."); + } + + @Test + @DisplayName("게시글 작성자라면 투표 통계를 조회한다.") + void getPostOptionVoteStatistics() { + // given + LocalDateTime open = LocalDateTime.now().plusDays(1); + Member member = memberTestPersister.builder().save(); + Member voter = memberTestPersister.builder().birthYear(1993).gender(Gender.MALE).save(); + Post post = postTestPersister.postBuilder().deadline(open).writer(member).save(); + PostOption postOption = postTestPersister.postOptionBuilder().post(post).sequence(1).save(); + Vote vote = voteTestPersister.builder().postOption(postOption).member(voter).save(); + postOption.addVote(vote); + post.addPostOption(postOption); + + // when + VoteOptionStatisticsResponse result = + postQueryService.getVoteOptionStatistics(post.getId(), postOption.getId(), member); + + // then + VoteOptionStatisticsResponse expected = new VoteOptionStatisticsResponse( + 1, + 1, + 0, + List.of( + new VoteCountForAgeGroupResponse( + AgeRange.UNDER_TEENS.getName(), + 0, + 0, + 0 + ), + new VoteCountForAgeGroupResponse( + AgeRange.TEENS.getName(), + 0, + 0, + 0 + ), + new VoteCountForAgeGroupResponse( + AgeRange.TWENTIES.getName(), + 0, + 0, + 0 + ), + new VoteCountForAgeGroupResponse( + AgeRange.THIRTIES.getName(), + 1, + 1, + 0 + ), + new VoteCountForAgeGroupResponse( + AgeRange.FORTIES.getName(), + 0, + 0, + 0 + ), + new VoteCountForAgeGroupResponse( + AgeRange.FIFTIES.getName(), + 0, + 0, + 0 + ), + new VoteCountForAgeGroupResponse( + AgeRange.OVER_SIXTIES.getName(), + 0, + 0, + 0 + ) + ) + ); + assertThat(result).usingRecursiveComparison().isEqualTo(expected); + } + + } + + private PostResponse expectedResponse( + final Post post, + final Member writer, + final PostOption postOption, + final long selectedOptionId, + final int totalVoteCount, + final int postOptionVoteCount, + final double votePercent + ) { + return new PostResponse( + post.getId(), + new PostWriterResponse(writer.getId(), writer.getNickname()), + post.getTitle(), + post.getContent(), + "", + Collections.emptyList(), + post.getCreatedAt(), + post.getDeadline(), + 0, + 0, + new PostVoteResultResponse( + selectedOptionId, + totalVoteCount, + List.of( + new PostOptionVoteResultResponse( + postOption.getId(), + postOption.getContent(), + "", + postOptionVoteCount, + votePercent + ) + ) + ) + ); + } + +} diff --git a/backend/src/test/java/com/votogether/domain/post/service/PostServiceTest.java b/backend/src/test/java/com/votogether/domain/post/service/PostServiceTest.java deleted file mode 100644 index 0bb486c11..000000000 --- a/backend/src/test/java/com/votogether/domain/post/service/PostServiceTest.java +++ /dev/null @@ -1,1287 +0,0 @@ -package com.votogether.domain.post.service; - -import static com.votogether.test.fixtures.MemberFixtures.FEMALE_10; -import static com.votogether.test.fixtures.MemberFixtures.FEMALE_70; -import static com.votogether.test.fixtures.MemberFixtures.FEMALE_80; -import static com.votogether.test.fixtures.MemberFixtures.MALE_10; -import static com.votogether.test.fixtures.MemberFixtures.MALE_20; -import static com.votogether.test.fixtures.MemberFixtures.MALE_30; -import static com.votogether.test.fixtures.MemberFixtures.MALE_60; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatNoException; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.junit.jupiter.api.Assertions.assertAll; - -import com.votogether.domain.category.entity.Category; -import com.votogether.domain.category.repository.CategoryRepository; -import com.votogether.domain.member.entity.Member; -import com.votogether.domain.member.entity.vo.Gender; -import com.votogether.domain.member.entity.vo.SocialType; -import com.votogether.domain.member.repository.MemberCategoryRepository; -import com.votogether.domain.member.repository.MemberRepository; -import com.votogether.domain.post.dto.request.comment.CommentRegisterRequest; -import com.votogether.domain.post.dto.request.post.PostCreateRequest; -import com.votogether.domain.post.dto.request.post.PostOptionCreateRequest; -import com.votogether.domain.post.dto.request.post.PostOptionUpdateRequest; -import com.votogether.domain.post.dto.request.post.PostUpdateRequest; -import com.votogether.domain.post.dto.response.post.CategoryResponse; -import com.votogether.domain.post.dto.response.post.PostDetailResponse; -import com.votogether.domain.post.dto.response.post.PostOptionDetailResponse; -import com.votogether.domain.post.dto.response.post.PostRankingResponse; -import com.votogether.domain.post.dto.response.post.PostResponse; -import com.votogether.domain.post.dto.response.post.WriterResponse; -import com.votogether.domain.post.dto.response.vote.VoteDetailResponse; -import com.votogether.domain.post.dto.response.vote.VoteOptionStatisticsResponse; -import com.votogether.domain.post.entity.Post; -import com.votogether.domain.post.entity.PostBody; -import com.votogether.domain.post.entity.PostCategory; -import com.votogether.domain.post.entity.PostContentImage; -import com.votogether.domain.post.entity.PostOption; -import com.votogether.domain.post.entity.PostOptions; -import com.votogether.domain.post.entity.comment.Comment; -import com.votogether.domain.post.entity.vo.PostClosingType; -import com.votogether.domain.post.entity.vo.PostSortType; -import com.votogether.domain.post.exception.PostExceptionType; -import com.votogether.domain.post.repository.CommentRepository; -import com.votogether.domain.post.repository.PostCategoryRepository; -import com.votogether.domain.post.repository.PostContentImageRepository; -import com.votogether.domain.post.repository.PostOptionRepository; -import com.votogether.domain.post.repository.PostRepository; -import com.votogether.domain.vote.entity.Vote; -import com.votogether.domain.vote.repository.VoteRepository; -import com.votogether.domain.vote.service.VoteService; -import com.votogether.global.exception.BadRequestException; -import com.votogether.global.exception.NotFoundException; -import com.votogether.test.annotation.ServiceTest; -import com.votogether.test.fixtures.CategoryFixtures; -import com.votogether.test.fixtures.MemberFixtures; -import com.votogether.test.persister.MemberTestPersister; -import com.votogether.test.persister.PostOptionTestPersister; -import com.votogether.test.persister.PostTestPersister; -import com.votogether.test.persister.VoteTestPersister; -import jakarta.persistence.EntityManager; -import java.io.FileInputStream; -import java.io.IOException; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.mock.web.MockMultipartFile; -import org.springframework.web.multipart.MultipartFile; - -@ServiceTest -class PostServiceTest { - - @Autowired - EntityManager entityManager; - - @Autowired - MemberRepository memberRepository; - - @Autowired - MemberCategoryRepository memberCategoryRepository; - - @Autowired - PostRepository postRepository; - - @Autowired - PostOptionRepository postOptionRepository; - - @Autowired - CategoryRepository categoryRepository; - - @Autowired - VoteRepository voteRepository; - - @Autowired - PostService postService; - - @Autowired - VoteService voteService; - - @Autowired - MemberTestPersister memberTestPersister; - - @Autowired - PostTestPersister postTestPersister; - - @Autowired - PostOptionTestPersister postOptionTestPersister; - - @Autowired - VoteTestPersister voteTestPersister; - - @Autowired - PostCategoryRepository postCategoryRepository; - - @Autowired - PostContentImageRepository postContentImageRepository; - - @Autowired - CommentRepository commentRepository; - - @Autowired - PostCommentService postCommentService; - - @Test - @DisplayName("게시글을 등록한다") - void save() throws IOException { - // given - Category category1 = categoryRepository.save(CategoryFixtures.DEVELOP.get()); - Category category2 = categoryRepository.save(CategoryFixtures.FOOD.get()); - Member member = memberRepository.save(MemberFixtures.MALE_20.get()); - - MockMultipartFile file1 = new MockMultipartFile("image1", "test1.png", "image/png", - new FileInputStream("src/test/resources/images/testImage1.PNG")); - MockMultipartFile file2 = new MockMultipartFile("image2", "test2.png", "image/png", - new FileInputStream("src/test/resources/images/testImage2.PNG")); - - MockMultipartFile file3 = new MockMultipartFile("image3", "test3.png", "image/png", - new FileInputStream("src/test/resources/images/testImage3.PNG")); - - PostCreateRequest postCreateRequest = PostCreateRequest.builder() - .categoryIds(List.of(category1.getId(), category2.getId())).title("title").content("content") - .postOptions(List.of(PostOptionCreateRequest.builder().content("option1").build(), - PostOptionCreateRequest.builder().content("option2").build())) - .deadline(LocalDateTime.now().plusDays(2)).build(); - - // when - Long savedPostId = postService.save(postCreateRequest, member, List.of(file3), List.of(file1, file2)); - - // then - assertThat(savedPostId).isNotNull(); - } - - @Nested - @DisplayName("게시글에 대한 투표 통계 조회 시 ") - class GetVoteStatistics { - - @Test - @DisplayName("게시글이 존재하지 않으면 예외를 던진다.") - void throwExceptionNonExistPost() { - // given, when, then - assertThatThrownBy(() -> postService.getVoteStatistics(-1L, MemberFixtures.MALE_20.get())).isInstanceOf( - NotFoundException.class).hasMessage("해당 게시글이 존재하지 않습니다."); - } - - @Test - @DisplayName("게시글 작성자가 아니라면 예외를 던진다.") - void throwExceptionNotWriter() { - // given - Member writer = memberRepository.save(MemberFixtures.MALE_20.get()); - Member reader = memberRepository.save(MemberFixtures.FEMALE_20.get()); - Post post = postRepository.save( - Post.builder().writer(writer).postBody(PostBody.builder().title("title").content("content").build()) - .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)).build()); - - // when, then - assertThatThrownBy(() -> postService.getVoteStatistics(post.getId(), reader)).isInstanceOf( - BadRequestException.class).hasMessage("해당 게시글 작성자가 아닙니다."); - } - - @Test - @DisplayName("전체 투표 통계를 조회한다.") - void getVoteStatistics() { - // given - Member femaleEarly10 = memberRepository.save(MemberFixtures.FEMALE_10.get()); - Member male10 = memberRepository.save(MemberFixtures.MALE_10.get()); - Member male60 = memberRepository.save(MemberFixtures.MALE_60.get()); - Member female70 = memberRepository.save(MemberFixtures.FEMALE_70.get()); - Member female80 = memberRepository.save(MemberFixtures.FEMALE_80.get()); - Member writer = memberRepository.save(MemberFixtures.MALE_20.get()); - - Post post = postRepository.save( - Post.builder().writer(writer).postBody(PostBody.builder().title("title").content("content").build()) - .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)).build()); - PostOption postOptionA = postOptionRepository.save( - PostOption.builder().post(post).sequence(1).content("치킨").build()); - PostOption postOptionB = postOptionRepository.save( - PostOption.builder().post(post).sequence(2).content("피자").build()); - - voteRepository.save(Vote.builder().member(femaleEarly10).postOption(postOptionA).build()); - voteRepository.save(Vote.builder().member(male10).postOption(postOptionB).build()); - voteRepository.save(Vote.builder().member(male60).postOption(postOptionA).build()); - voteRepository.save(Vote.builder().member(female70).postOption(postOptionB).build()); - voteRepository.save(Vote.builder().member(female80).postOption(postOptionA).build()); - - // when - VoteOptionStatisticsResponse response = postService.getVoteStatistics(post.getId(), writer); - - // then - assertAll(() -> assertThat(response.totalVoteCount()).isEqualTo(5), - () -> assertThat(response.totalMaleCount()).isEqualTo(2), - () -> assertThat(response.totalFemaleCount()).isEqualTo(3), - () -> assertThat(response.ageGroup()).hasSize(7), - () -> assertThat(response.ageGroup().get(1).ageGroup()).isEqualTo("10대"), - () -> assertThat(response.ageGroup().get(1).voteCount()).isEqualTo(2), - () -> assertThat(response.ageGroup().get(1).maleCount()).isEqualTo(1), - () -> assertThat(response.ageGroup().get(1).femaleCount()).isEqualTo(1)); - } - - } - - @Nested - @DisplayName("게시글 투표 옵션에 대한 투표 통계 조회 시 ") - class GetVoteOptionStatistics { - - @Test - @DisplayName("게시글이 존재하지 않으면 예외를 던진다.") - void throwExceptionNonExistPost() { - // given, when, then - assertThatThrownBy( - () -> postService.getVoteOptionStatistics(-1L, 1L, MemberFixtures.MALE_20.get())).isInstanceOf( - NotFoundException.class).hasMessage("해당 게시글이 존재하지 않습니다."); - } - - @Test - @DisplayName("게시글 투표 옵션이 존재하지 않으면 예외를 던진다.") - void throwExceptionNonExistOption() { - // given - Member writer = memberRepository.save(MemberFixtures.MALE_20.get()); - - Post post = postRepository.save( - Post.builder().writer(writer).postBody(PostBody.builder().title("title").content("content").build()) - .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)).build()); - - // when, then - assertThatThrownBy(() -> postService.getVoteOptionStatistics(post.getId(), -1L, writer)).isInstanceOf( - NotFoundException.class).hasMessage("해당 게시글 투표 옵션이 존재하지 않습니다."); - } - - @Test - @DisplayName("게시글 투표 옵션이 게시글에 속하지 않으면 예외를 던진다.") - void throwExceptionNotBelongToPost() { - // given - Member writer = memberRepository.save(MemberFixtures.MALE_20.get()); - - Post post1 = postRepository.save( - Post.builder().writer(writer).postBody(PostBody.builder().title("title").content("content").build()) - .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)).build()); - Post post2 = postRepository.save( - Post.builder().writer(writer).postBody(PostBody.builder().title("title").content("content").build()) - .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)).build()); - PostOption postOption = postOptionRepository.save( - PostOption.builder().post(post2).sequence(1).content("치킨").build()); - - // when, then - assertThatThrownBy( - () -> postService.getVoteOptionStatistics(post1.getId(), postOption.getId(), writer)).isInstanceOf( - BadRequestException.class).hasMessage("게시글 투표 옵션이 게시글과 연관되어 있지 않습니다."); - } - - @Test - @DisplayName("게시글 작성자가 아니라면 예외를 던진다.") - void throwExceptionNotWriter() { - // given - Member writer = memberRepository.save(MemberFixtures.MALE_20.get()); - Member reader = memberRepository.save(MemberFixtures.FEMALE_20.get()); - Post post = postRepository.save( - Post.builder().writer(writer).postBody(PostBody.builder().title("title").content("content").build()) - .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)).build()); - PostOption postOption = postOptionRepository.save( - PostOption.builder().post(post).sequence(1).content("치킨").build()); - - // when, then - assertThatThrownBy( - () -> postService.getVoteOptionStatistics(post.getId(), postOption.getId(), reader)).isInstanceOf( - BadRequestException.class).hasMessage("해당 게시글 작성자가 아닙니다."); - } - - @Test - @DisplayName("게시글 투표 옵션에 대한 투표 통계를 조회한다.") - void getVoteOptionStatistics() { - // given - Member female10 = memberRepository.save(FEMALE_10.get()); - Member male10 = memberRepository.save(MALE_10.get()); - Member male60 = memberRepository.save(MALE_60.get()); - Member female70 = memberRepository.save(FEMALE_70.get()); - Member female80 = memberRepository.save(FEMALE_80.get()); - Member writer = memberRepository.save(MALE_20.get()); - - Post post = postRepository.save( - Post.builder().writer(writer).postBody(PostBody.builder().title("title").content("content").build()) - .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)).build()); - PostOption postOption = postOptionRepository.save( - PostOption.builder().post(post).sequence(1).content("치킨").build()); - - voteRepository.save(Vote.builder().member(female10).postOption(postOption).build()); - voteRepository.save(Vote.builder().member(male10).postOption(postOption).build()); - voteRepository.save(Vote.builder().member(male60).postOption(postOption).build()); - voteRepository.save(Vote.builder().member(female70).postOption(postOption).build()); - voteRepository.save(Vote.builder().member(female80).postOption(postOption).build()); - - // when - VoteOptionStatisticsResponse response = postService.getVoteOptionStatistics(post.getId(), - postOption.getId(), writer); - - // then - assertAll(() -> assertThat(response.totalVoteCount()).isEqualTo(5), - () -> assertThat(response.totalMaleCount()).isEqualTo(2), - () -> assertThat(response.totalFemaleCount()).isEqualTo(3), - () -> assertThat(response.ageGroup()).hasSize(7), - () -> assertThat(response.ageGroup().get(1).ageGroup()).isEqualTo("10대"), - () -> assertThat(response.ageGroup().get(1).voteCount()).isEqualTo(2), - () -> assertThat(response.ageGroup().get(1).maleCount()).isEqualTo(1), - () -> assertThat(response.ageGroup().get(1).femaleCount()).isEqualTo(1)); - } - - } - - @Test - @DisplayName("해당 게시글을 조기 마감 합니다") - void postClosedEarlyById() { - // given - Member writer = memberRepository.save(MemberFixtures.MALE_30.get()); - LocalDateTime oldDeadline = LocalDateTime.now().plusDays(2); - Post post = postRepository.save( - Post.builder().writer(writer).postBody(PostBody.builder().title("title").content("content").build()) - .deadline(oldDeadline).build()); - - Post foundPost = postRepository.findById(post.getId()).get(); - - // when - postService.closePostEarlyById(post.getId(), writer); - - // then - assertAll(() -> assertThat(foundPost.getId()).isEqualTo(post.getId()), - () -> assertThat(foundPost.getDeadline()).isBefore(oldDeadline)); - } - - @Test - @DisplayName("해당 게시글을 조기 마감할 시, 작성자가 아니면 예외를 던진다.") - void throwExceptionNotWriterPostClosedEarly() { - // given - Member writer = memberRepository.save(MemberFixtures.MALE_30.get()); - LocalDateTime oldDeadline = LocalDateTime.of(2100, 7, 12, 0, 0); - Post post = postRepository.save( - Post.builder().writer(writer).postBody(PostBody.builder().title("title").content("content").build()) - .deadline(oldDeadline).build()); - - Post foundPost = postRepository.findById(post.getId()).get(); - - // when, then - assertThatThrownBy( - () -> postService.closePostEarlyById(foundPost.getId(), MemberFixtures.MALE_30.get())).isInstanceOf( - BadRequestException.class).hasMessage("해당 게시글 작성자가 아닙니다."); - } - - @Test - @DisplayName("해당 게시글을 조기 마감할 시, 마감이 된 게시글이면 예외를 던진다.") - void throwExceptionDeadLinePostClosedEarly() { - // given - Member writer = memberRepository.save(MemberFixtures.MALE_30.get()); - LocalDateTime oldDeadline = LocalDateTime.of(2000, 7, 12, 0, 0); - Post post = postRepository.save( - Post.builder().writer(writer).postBody(PostBody.builder().title("title").content("content").build()) - .deadline(oldDeadline).build()); - - Post foundPost = postRepository.findById(post.getId()).get(); - - // when, then - assertThatThrownBy(() -> postService.closePostEarlyById(foundPost.getId(), writer)).isInstanceOf( - BadRequestException.class).hasMessage("게시글이 이미 마감되었습니다."); - } - - @Test - @DisplayName("정렬 유형 및 마감 유형별로 모든 게시물 가져온다") - void getAllPostBySortTypeAndClosingType() { - // given - Member writer = MALE_30.get(); - memberRepository.save(writer); - - Member memberToAllPostVote = MALE_20.get(); - memberRepository.save(memberToAllPostVote); - - Category ca1 = Category.builder().name("ca1").build(); - Category ca2 = Category.builder().name("ca2").build(); - Category ca3 = Category.builder().name("ca3").build(); - - categoryRepository.saveAll(List.of(ca1, ca2, ca3)); - - MockMultipartFile file1 = new MockMultipartFile("file1", "hello1.txt", "text/plain", - "Hello, World!11".getBytes()); - - MockMultipartFile file2 = new MockMultipartFile("file2", "hello2.txt", "text/plain", - "Hello, World!22".getBytes()); - - MockMultipartFile file3 = new MockMultipartFile("file3", "hello3.txt", "text/plain", - "Hello, World!33".getBytes()); - - for (int postSequence = 30; postSequence > 0; postSequence--) { - List options = new ArrayList<>() { - { - add(PostOptionCreateRequest.builder().content("option1").build()); - add(PostOptionCreateRequest.builder().content("option2").build()); - } - }; - - List optionImages = new ArrayList<>() { - { - add(file1); - add(file2); - } - }; - - List contentImages = new ArrayList<>() { - { - add(file3); - } - }; - - if (postSequence % 2 == 0) { - MockMultipartFile file4 = new MockMultipartFile("file4", "hello4.txt", "text/plain", - "Hello, World!44".getBytes()); - - optionImages.add(file4); - options.add(PostOptionCreateRequest.builder().content("option3").build()); - } - - PostCreateRequest postCreateRequest = PostCreateRequest.builder().categoryIds(List.of(0L, 2L)) - .title("title" + postSequence).content("content" + postSequence).postOptions(options) - .deadline(LocalDateTime.now().plusDays(2)).build(); - - Long savedPostId = postService.save(postCreateRequest, writer, contentImages, optionImages); - Post post = postRepository.findById(savedPostId).get(); - - List postOptions = post.getPostOptions().getPostOptions(); - Long postOptionId = postOptions.get(0).getId(); - voteService.vote(memberToAllPostVote, post.getId(), postOptionId); - - for (int voteCount = 0; voteCount <= postSequence; voteCount++) { - Member memberToVote = Member.builder().nickname("Abel" + postSequence + voteCount).gender(Gender.MALE) - .birthYear(2000).socialType(SocialType.KAKAO).socialId("Abel" + postSequence + voteCount) - .build(); - - memberRepository.save(memberToVote); - - PostOption perPostOption = postOptions.get(voteCount % postOptions.size()); - voteService.vote(memberToVote, savedPostId, perPostOption.getId()); - } - } - - entityManager.clear(); - - // when - List responses = postService.getAllPostBySortTypeAndClosingTypeAndCategoryId(0, - PostClosingType.PROGRESS, PostSortType.HOT, null, memberToAllPostVote); - - // then - PostResponse firstResponse = responses.get(0); - PostResponse secondResponse = responses.get(1); - assertAll(() -> assertThat(firstResponse.voteInfo().options()).hasSize(3), - () -> assertThat(firstResponse.voteInfo().totalVoteCount()).isEqualTo(32), - () -> assertThat(secondResponse.voteInfo().options()).hasSize(2), - () -> assertThat(secondResponse.voteInfo().totalVoteCount()).isEqualTo(31)); - } - - @Test - @DisplayName("비회원 게시글 목록 조회 시 마감된 게시글은 결과를 확인할 수 있고, 진행중인 게시글은 결과를 확인할 수 없다.") - void getPostsGuest() { - // given - List voters = new ArrayList<>(); - Member writer = memberTestPersister.builder().save(); - - for (int i = 0; i < 5; i++) { - voters.add(memberTestPersister.builder().save()); - } - - Post closedPost = postTestPersister.builder().writer(writer).deadline(LocalDateTime.of(2022, 12, 25, 0, 0)) - .save(); - - for (int j = 0; j < 2; j++) { - PostOption postOption = postOptionTestPersister.builder().sequence(j + 1).post(closedPost).save(); - for (int k = 0; k < 4; k++) { - voteTestPersister.builder().member(voters.get(k)).postOption(postOption).save(); - } - } - - Post notClosedPost = postTestPersister.builder().writer(writer).deadline(LocalDateTime.of(3022, 12, 25, 0, 0)) - .save(); - - for (int j = 0; j < 2; j++) { - PostOption postOption = postOptionTestPersister.builder().sequence(j + 1).post(notClosedPost).save(); - for (int k = 0; k < 4; k++) { - voteTestPersister.builder().member(voters.get(k)).postOption(postOption).save(); - } - } - - entityManager.flush(); - entityManager.clear(); - - // when - List result = postService.getPostsGuest(0, PostClosingType.ALL, PostSortType.LATEST, null); - - // then - assertAll(() -> assertThat(result).hasSize(2), - () -> assertThat(result.get(0).voteInfo().totalVoteCount()).isEqualTo(-1), - () -> assertThat(result.get(0).voteInfo().options().get(0).voteCount()).isEqualTo(-1), - () -> assertThat(result.get(0).voteInfo().options().get(1).voteCount()).isEqualTo(-1), - () -> assertThat(result.get(1).voteInfo().totalVoteCount()).isEqualTo(8), - () -> assertThat(result.get(1).voteInfo().options().get(0).voteCount()).isEqualTo(4), - () -> assertThat(result.get(1).voteInfo().options().get(1).voteCount()).isEqualTo(4)); - } - - @Test - @DisplayName("한 게시글의 상세를 조회할 시, 작성자면 투표 결과를 알 수 있다.") - void getPostByWriter() throws IOException { - Category category1 = categoryRepository.save(CategoryFixtures.DEVELOP.get()); - Category category2 = categoryRepository.save(CategoryFixtures.FOOD.get()); - Member writer = memberRepository.save(MemberFixtures.MALE_20.get()); - - MockMultipartFile file1 = new MockMultipartFile("image1", "test1.png", "image/png", - new FileInputStream("src/test/resources/images/testImage1.PNG")); - MockMultipartFile file2 = new MockMultipartFile("image1", "test2.png", "image/png", - new FileInputStream("src/test/resources/images/testImage2.PNG")); - - LocalDateTime deadline = LocalDateTime.now().plusDays(3); - - PostOptionCreateRequest option1 = PostOptionCreateRequest.builder().content("option1").build(); - - PostOptionCreateRequest option2 = PostOptionCreateRequest.builder().content("option2").build(); - - PostCreateRequest postCreateRequest = PostCreateRequest.builder() - .categoryIds(List.of(category1.getId(), category2.getId())).title("title").content("content") - .postOptions(List.of(option1, option2)).deadline(deadline).build(); - - Long savedPostId = postService.save(postCreateRequest, writer, List.of(), List.of(file1, file2)); - - // when - PostDetailResponse response = postService.getPostById(savedPostId, writer); - - // then - List categories = response.categories(); - WriterResponse writerResponse = response.writer(); - VoteDetailResponse voteDetailResponse = response.voteInfo(); - List options = voteDetailResponse.options(); - - assertAll(() -> assertThat(response.postId()).isEqualTo(savedPostId), - () -> assertThat(response.title()).isEqualTo("title"), - () -> assertThat(response.content()).isEqualTo("content"), () -> assertThat(categories).hasSize(2), - () -> assertThat(categories.get(0).name()).isEqualTo("개발"), - () -> assertThat(writerResponse.id()).isEqualTo(writer.getId()), - () -> assertThat(writerResponse.nickname()).isEqualTo("user7"), - () -> assertThat(voteDetailResponse.totalVoteCount()).isZero(), () -> assertThat(options).hasSize(2), - () -> assertThat(options.get(0).imageUrl()).contains("test1.png")); - } - - @Test - @DisplayName("한 게시글의 상세를 조회할 시, 해당 게시글의 투표자면 결과를 알 수 있다.") - void getPostByVoter() throws IOException { - Category category1 = categoryRepository.save(CategoryFixtures.DEVELOP.get()); - Category category2 = categoryRepository.save(CategoryFixtures.FOOD.get()); - Member voter = memberRepository.save(MemberFixtures.MALE_20.get()); - Member writer = memberRepository.save(MemberFixtures.FEMALE_30.get()); - - MockMultipartFile file1 = new MockMultipartFile("image1", "test1.png", "image/png", - new FileInputStream("src/test/resources/images/testImage1.PNG")); - MockMultipartFile file2 = new MockMultipartFile("image1", "test2.png", "image/png", - new FileInputStream("src/test/resources/images/testImage2.PNG")); - - LocalDateTime deadline = LocalDateTime.now().plusDays(3); - - PostOptionCreateRequest option1 = PostOptionCreateRequest.builder().content("option1").build(); - - PostOptionCreateRequest option2 = PostOptionCreateRequest.builder().content("option2").build(); - - PostCreateRequest postCreateRequest = PostCreateRequest.builder() - .categoryIds(List.of(category1.getId(), category2.getId())).title("title").content("content") - .postOptions(List.of(option1, option2)).deadline(deadline).build(); - - Long savedPostId = postService.save(postCreateRequest, writer, List.of(), List.of(file1, file2)); - Post post = postRepository.findById(savedPostId).get(); - PostOptions postOptions = post.getPostOptions(); - PostOption postOption = postOptions.getPostOptions().get(0); - voteService.vote(voter, savedPostId, postOption.getId()); - - entityManager.clear(); - - // when - PostDetailResponse response = postService.getPostById(savedPostId, voter); - - // then - List categories = response.categories(); - WriterResponse writerResponse = response.writer(); - VoteDetailResponse voteDetailResponse = response.voteInfo(); - List options = voteDetailResponse.options(); - - assertAll(() -> assertThat(response.postId()).isEqualTo(savedPostId), - () -> assertThat(response.title()).isEqualTo("title"), - () -> assertThat(response.content()).isEqualTo("content"), () -> assertThat(categories).hasSize(2), - () -> assertThat(categories.get(0).name()).isEqualTo("개발"), - () -> assertThat(writerResponse.id()).isEqualTo(writer.getId()), - () -> assertThat(writerResponse.nickname()).isEqualTo("user10"), - () -> assertThat(voteDetailResponse.totalVoteCount()).isOne(), () -> assertThat(options).hasSize(2), - () -> assertThat(options.get(0).imageUrl()).contains("test1.png")); - } - - @Test - @DisplayName("한 게시글의 상세를 조회할 시, 마감된 게시글이면 투표 결과를 알 수 있다.") - void getClosedPost() throws IOException { - Category category1 = categoryRepository.save(CategoryFixtures.DEVELOP.get()); - Category category2 = categoryRepository.save(CategoryFixtures.FOOD.get()); - Member member = memberRepository.save(MemberFixtures.FEMALE_10.get()); - Member writer = memberRepository.save(MemberFixtures.MALE_20.get()); - - MockMultipartFile file1 = new MockMultipartFile("image1", "test1.png", "image/png", - new FileInputStream("src/test/resources/images/testImage1.PNG")); - MockMultipartFile file2 = new MockMultipartFile("image1", "test2.png", "image/png", - new FileInputStream("src/test/resources/images/testImage2.PNG")); - - LocalDateTime deadline = LocalDateTime.now(); - - PostOptionCreateRequest option1 = PostOptionCreateRequest.builder().content("option1").build(); - - PostOptionCreateRequest option2 = PostOptionCreateRequest.builder().content("option2").build(); - - PostCreateRequest postCreateRequest = PostCreateRequest.builder() - .categoryIds(List.of(category1.getId(), category2.getId())).title("title").content("content") - .postOptions(List.of(option1, option2)).deadline(deadline).build(); - - Long savedPostId = postService.save(postCreateRequest, writer, List.of(), List.of(file1, file2)); - - // when - PostDetailResponse response = postService.getPostById(savedPostId, member); - - // then - List categories = response.categories(); - WriterResponse writerResponse = response.writer(); - VoteDetailResponse voteDetailResponse = response.voteInfo(); - List options = voteDetailResponse.options(); - - assertAll(() -> assertThat(response.postId()).isEqualTo(savedPostId), - () -> assertThat(response.title()).isEqualTo("title"), - () -> assertThat(response.content()).isEqualTo("content"), () -> assertThat(categories).hasSize(2), - () -> assertThat(categories.get(0).name()).isEqualTo("개발"), - () -> assertThat(writerResponse.id()).isEqualTo(writer.getId()), - () -> assertThat(writerResponse.nickname()).isEqualTo("user7"), - () -> assertThat(voteDetailResponse.totalVoteCount()).isZero(), () -> assertThat(options).hasSize(2), - () -> assertThat(options.get(0).imageUrl()).contains("test1.png")); - } - - @Test - @DisplayName("한 게시글의 상세를 조회할 시, 작성자, 투표자, 마감된 게시글이 전부 아니면 투표 결과를 알 수 없다.") - void getPostInvisibleResult() throws IOException { - Category category1 = categoryRepository.save(CategoryFixtures.DEVELOP.get()); - Category category2 = categoryRepository.save(CategoryFixtures.FOOD.get()); - Member member = memberRepository.save(MemberFixtures.FEMALE_10.get()); - Member writer = memberRepository.save(MemberFixtures.MALE_20.get()); - - MockMultipartFile file1 = new MockMultipartFile("image1", "test1.png", "image/png", - new FileInputStream("src/test/resources/images/testImage1.PNG")); - MockMultipartFile file2 = new MockMultipartFile("image1", "test2.png", "image/png", - new FileInputStream("src/test/resources/images/testImage2.PNG")); - - LocalDateTime deadline = LocalDateTime.now().plusDays(3); - - PostOptionCreateRequest option1 = PostOptionCreateRequest.builder().content("option1").build(); - - PostOptionCreateRequest option2 = PostOptionCreateRequest.builder().content("option2").build(); - - PostCreateRequest postCreateRequest = PostCreateRequest.builder() - .categoryIds(List.of(category1.getId(), category2.getId())).title("title").content("content") - .postOptions(List.of(option1, option2)).deadline(deadline).build(); - - Long savedPostId = postService.save(postCreateRequest, writer, List.of(), List.of(file1, file2)); - - // when - PostDetailResponse response = postService.getPostById(savedPostId, member); - - // then - List categories = response.categories(); - WriterResponse writerResponse = response.writer(); - VoteDetailResponse voteDetailResponse = response.voteInfo(); - List options = voteDetailResponse.options(); - - assertAll(() -> assertThat(response.postId()).isEqualTo(savedPostId), - () -> assertThat(response.title()).isEqualTo("title"), - () -> assertThat(response.content()).isEqualTo("content"), () -> assertThat(categories).hasSize(2), - () -> assertThat(categories.get(0).name()).isEqualTo("개발"), - () -> assertThat(writerResponse.id()).isEqualTo(writer.getId()), - () -> assertThat(writerResponse.nickname()).isEqualTo("user7"), - () -> assertThat(voteDetailResponse.totalVoteCount()).isEqualTo(-1), - () -> assertThat(options).hasSize(2), - () -> assertThat(options.get(0).imageUrl()).contains("test1.png")); - } - - @Test - @DisplayName("존재하지 않은 게시글을 가져오려 할 시, 예외를 던진다.") - void throwExceptionNotFoundPost() { - // given - Member member = memberRepository.save(MemberFixtures.MALE_20.get()); - - // when, then - assertThatThrownBy(() -> postService.getPostById(1L, member)).isInstanceOf(NotFoundException.class) - .hasMessage(PostExceptionType.POST_NOT_FOUND.getMessage()); - } - - @Test - void delete() throws IOException { - // given - Category category1 = categoryRepository.save(CategoryFixtures.DEVELOP.get()); - Category category2 = categoryRepository.save(CategoryFixtures.FOOD.get()); - Member writer = memberRepository.save(MemberFixtures.MALE_20.get()); - - MockMultipartFile file1 = new MockMultipartFile("image1", "test1.png", "image/png", - new FileInputStream("src/test/resources/images/testImage1.PNG")); - MockMultipartFile file2 = new MockMultipartFile("image2", "test2.png", "image/png", - new FileInputStream("src/test/resources/images/testImage2.PNG")); - - MockMultipartFile file3 = new MockMultipartFile("image3", "test3.png", "image/png", - new FileInputStream("src/test/resources/images/testImage3.PNG")); - - PostCreateRequest postCreateRequest = PostCreateRequest.builder() - .categoryIds(List.of(category1.getId(), category2.getId())).title("title").content("content") - .postOptions(List.of(PostOptionCreateRequest.builder().content("option1").build(), - PostOptionCreateRequest.builder().content("option2").build())) - .deadline(LocalDateTime.now().plusDays(2)).build(); - - Long savedPostId = postService.save(postCreateRequest, writer, List.of(file3), List.of(file1, file2)); - final Post post = postRepository.findById(savedPostId).get(); - final List postOptions = post.getPostOptions().getPostOptions(); - - Member voter = MALE_30.get(); - memberRepository.save(voter); - PostOption perPostOption = postOptions.get(0); - voteService.vote(voter, savedPostId, perPostOption.getId()); - - CommentRegisterRequest commentRegisterRequest = new CommentRegisterRequest("hello"); - postCommentService.createComment(voter, post.getId(), commentRegisterRequest); - entityManager.flush(); - entityManager.clear(); - - final List postCategories = post.getPostCategories().getPostCategories(); - final PostBody postBody = post.getPostBody(); - final PostContentImage postContentImage = postBody.getPostContentImages().getContentImages().get(0); - final List votes = postOptions.stream().map(PostOption::getVotes).flatMap(Collection::stream).toList(); - final List comments = post.getComments(); - - // when, then - assertAll(() -> assertThatNoException().isThrownBy(() -> postService.delete(savedPostId)), - () -> assertThat(postCategoryRepository.findById(postCategories.get(0).getId())).isNotPresent(), - () -> assertThat(postContentImageRepository.findById(postContentImage.getId())).isNotPresent(), - () -> assertThat(postOptionRepository.findById(postOptions.get(0).getId())).isNotPresent(), - () -> assertThat(voteRepository.findById(votes.get(0).getId())).isNotPresent(), - () -> assertThat(commentRepository.findById(comments.get(0).getId())).isNotPresent()); - } - - @Test - @DisplayName("게시글을 삭제할 시, 투표가 20개 이상 진행된 게시글이면 예외를 던진다.") - void throwExceptionDeleteVoteOverTwenty() throws IOException { - // given - Category category1 = categoryRepository.save(CategoryFixtures.DEVELOP.get()); - Category category2 = categoryRepository.save(CategoryFixtures.FOOD.get()); - Member member = memberRepository.save(MemberFixtures.MALE_20.get()); - - MockMultipartFile file1 = new MockMultipartFile("image1", "test1.png", "image/png", - new FileInputStream("src/test/resources/images/testImage1.PNG")); - MockMultipartFile file2 = new MockMultipartFile("image2", "test2.png", "image/png", - new FileInputStream("src/test/resources/images/testImage2.PNG")); - - MockMultipartFile file3 = new MockMultipartFile("image3", "test3.png", "image/png", - new FileInputStream("src/test/resources/images/testImage3.PNG")); - - PostCreateRequest postCreateRequest = PostCreateRequest.builder() - .categoryIds(List.of(category1.getId(), category2.getId())).title("title").content("content") - .postOptions(List.of(PostOptionCreateRequest.builder().content("option1").build(), - PostOptionCreateRequest.builder().content("option2").build())) - .deadline(LocalDateTime.now().plusDays(2)).build(); - - Long savedPostId = postService.save(postCreateRequest, member, List.of(file3), List.of(file1, file2)); - final Post post = postRepository.findById(savedPostId).get(); - - for (int voteCount = 0; voteCount < 20; voteCount++) { - Member memberToVote = Member.builder().nickname("Abel" + voteCount).gender(Gender.MALE).birthYear(2000) - .socialType(SocialType.KAKAO).socialId("Abel" + voteCount).build(); - - memberRepository.save(memberToVote); - - final List postOptions = post.getPostOptions().getPostOptions(); - PostOption perPostOption = postOptions.get(voteCount % postOptions.size()); - voteService.vote(memberToVote, savedPostId, perPostOption.getId()); - } - - entityManager.clear(); - - // when, then - assertThatThrownBy(() -> postService.delete(savedPostId)).isInstanceOf(BadRequestException.class) - .hasMessage(PostExceptionType.CANNOT_DELETE_BECAUSE_MORE_THAN_TWENTY_VOTES.getMessage()); - } - - @Test - @DisplayName("게시글을 수정한다") - void update() throws IOException { - // given - Category category1 = categoryRepository.save(CategoryFixtures.DEVELOP.get()); - Member writer = memberRepository.save(MemberFixtures.MALE_20.get()); - - MockMultipartFile file1 = new MockMultipartFile("image1", "test1.png", "image/png", - new FileInputStream("src/test/resources/images/testImage1.PNG")); - MockMultipartFile file2 = new MockMultipartFile("image2", "test2.png", "image/png", - new FileInputStream("src/test/resources/images/testImage2.PNG")); - - MockMultipartFile file3 = new MockMultipartFile("image3", "test3.png", "image/png", - new FileInputStream("src/test/resources/images/testImage3.PNG")); - - PostCreateRequest postCreateRequest = PostCreateRequest.builder().categoryIds(List.of(category1.getId())) - .title("title").content("content").postOptions( - List.of(PostOptionCreateRequest.builder().content("option1").build(), - PostOptionCreateRequest.builder().content("option2").build())) - .deadline(LocalDateTime.now().plusDays(2)).build(); - - Long savedPostId = postService.save(postCreateRequest, writer, List.of(file3), List.of(file1, file2)); - - Category category2 = categoryRepository.save(CategoryFixtures.FOOD.get()); - MockMultipartFile file4 = new MockMultipartFile("image4", "test4.png", "image/png", - new FileInputStream("src/test/resources/images/testImage1.PNG")); - MockMultipartFile file5 = new MockMultipartFile("image5", "test5.png", "image/png", - new FileInputStream("src/test/resources/images/testImage2.PNG")); - - MockMultipartFile file6 = new MockMultipartFile("image6", "test6.png", "image/png", - new FileInputStream("src/test/resources/images/testImage3.PNG")); - - PostUpdateRequest postUpdateRequest = PostUpdateRequest.builder().categoryIds(List.of(category2.getId())) - .title("title2").content("content2").postOptions( - List.of(PostOptionUpdateRequest.builder().content("option3").build(), - PostOptionUpdateRequest.builder().content("option4").build())) - .deadline(LocalDateTime.now().plusDays(1)).build(); - - // when - postService.update(savedPostId, postUpdateRequest, writer, List.of(file4), List.of(file5, file6)); - - // then - final PostDetailResponse postDetailResponse = postService.getPostById(savedPostId, writer); - final List options = postDetailResponse.voteInfo().options(); - final List categories = postDetailResponse.categories(); - assertAll(() -> assertThat(postDetailResponse.title()).isEqualTo("title2"), - () -> assertThat(postDetailResponse.content()).isEqualTo("content2"), - () -> assertThat(postDetailResponse.imageUrl()).contains("test4.png"), - () -> assertThat(options.get(0).content()).isEqualTo("option3"), - () -> assertThat(options.get(1).content()).isEqualTo("option4"), - () -> assertThat(options.get(0).imageUrl()).contains("test5.png"), - () -> assertThat(options.get(1).imageUrl()).contains("test6.png"), - () -> assertThat(categories.get(0).name()).isEqualTo("음식")); - } - - @Test - @DisplayName("게시글 수정 시, 작성자가 아니면 예외를 던진다.") - void throwExceptionUpdateNotWriter() throws IOException { - // given - Category category1 = categoryRepository.save(CategoryFixtures.DEVELOP.get()); - Member member = memberRepository.save(MemberFixtures.MALE_30.get()); - Member writer = memberRepository.save(MemberFixtures.MALE_20.get()); - - MockMultipartFile file1 = new MockMultipartFile("image1", "test1.png", "image/png", - new FileInputStream("src/test/resources/images/testImage1.PNG")); - MockMultipartFile file2 = new MockMultipartFile("image2", "test2.png", "image/png", - new FileInputStream("src/test/resources/images/testImage2.PNG")); - - MockMultipartFile file3 = new MockMultipartFile("image3", "test3.png", "image/png", - new FileInputStream("src/test/resources/images/testImage3.PNG")); - - PostCreateRequest postCreateRequest = PostCreateRequest.builder().categoryIds(List.of(category1.getId())) - .title("title").content("content").postOptions( - List.of(PostOptionCreateRequest.builder().content("option1").build(), - PostOptionCreateRequest.builder().content("option2").build())) - .deadline(LocalDateTime.now().plusDays(2)).build(); - - Long savedPostId = postService.save(postCreateRequest, writer, List.of(file3), List.of(file1, file2)); - - Category category2 = categoryRepository.save(CategoryFixtures.FOOD.get()); - MockMultipartFile file4 = new MockMultipartFile("image4", "test4.png", "image/png", - new FileInputStream("src/test/resources/images/testImage1.PNG")); - MockMultipartFile file5 = new MockMultipartFile("image5", "test5.png", "image/png", - new FileInputStream("src/test/resources/images/testImage2.PNG")); - - MockMultipartFile file6 = new MockMultipartFile("image6", "test6.png", "image/png", - new FileInputStream("src/test/resources/images/testImage3.PNG")); - - PostUpdateRequest postUpdateRequest = PostUpdateRequest.builder().categoryIds(List.of(category2.getId())) - .title("title2").content("content2").postOptions( - List.of(PostOptionUpdateRequest.builder().content("option3").build(), - PostOptionUpdateRequest.builder().content("option4").build())) - .deadline(LocalDateTime.now().plusDays(1)).build(); - - // when, then - assertThatThrownBy(() -> postService.update(savedPostId, postUpdateRequest, member, List.of(file4), - List.of(file5, file6))).isInstanceOf(BadRequestException.class) - .hasMessage(PostExceptionType.NOT_WRITER.getMessage()); - } - - @Test - @DisplayName("게시글 수정 시, 마감된 게시글이면 예외를 던진다.") - void throwExceptionUpdateClosedPost() throws IOException { - // given - Category category1 = categoryRepository.save(CategoryFixtures.DEVELOP.get()); - Member writer = memberRepository.save(MemberFixtures.MALE_20.get()); - - MockMultipartFile file1 = new MockMultipartFile("image1", "test1.png", "image/png", - new FileInputStream("src/test/resources/images/testImage1.PNG")); - MockMultipartFile file2 = new MockMultipartFile("image2", "test2.png", "image/png", - new FileInputStream("src/test/resources/images/testImage2.PNG")); - - MockMultipartFile file3 = new MockMultipartFile("image3", "test3.png", "image/png", - new FileInputStream("src/test/resources/images/testImage3.PNG")); - - PostCreateRequest postCreateRequest = PostCreateRequest.builder().categoryIds(List.of(category1.getId())) - .title("title").content("content").postOptions( - List.of(PostOptionCreateRequest.builder().content("option1").build(), - PostOptionCreateRequest.builder().content("option2").build())) - .deadline(LocalDateTime.now()).build(); - - Long savedPostId = postService.save(postCreateRequest, writer, List.of(file3), List.of(file1, file2)); - - Category category2 = categoryRepository.save(CategoryFixtures.FOOD.get()); - MockMultipartFile file4 = new MockMultipartFile("image4", "test4.png", "image/png", - new FileInputStream("src/test/resources/images/testImage1.PNG")); - MockMultipartFile file5 = new MockMultipartFile("image5", "test5.png", "image/png", - new FileInputStream("src/test/resources/images/testImage2.PNG")); - - MockMultipartFile file6 = new MockMultipartFile("image6", "test6.png", "image/png", - new FileInputStream("src/test/resources/images/testImage3.PNG")); - - PostUpdateRequest postUpdateRequest = PostUpdateRequest.builder().categoryIds(List.of(category2.getId())) - .title("title2").content("content2").postOptions( - List.of(PostOptionUpdateRequest.builder().content("option3").build(), - PostOptionUpdateRequest.builder().content("option4").build())) - .deadline(LocalDateTime.now().plusDays(1)).build(); - - // when, then - assertThatThrownBy(() -> postService.update(savedPostId, postUpdateRequest, writer, List.of(file4), - List.of(file5, file6))).isInstanceOf(BadRequestException.class) - .hasMessage(PostExceptionType.POST_CLOSED.getMessage()); - } - - @Test - @DisplayName("게시글 수정 시, 수정할 마감 기한이 생성 날짜보다 3일 초과면 예외를 던진다.") - void throwExceptionUpdateDeadlineOver() throws IOException { - // given - Category category1 = categoryRepository.save(CategoryFixtures.DEVELOP.get()); - Member writer = memberRepository.save(MemberFixtures.MALE_20.get()); - - MockMultipartFile file1 = new MockMultipartFile("image1", "test1.png", "image/png", - new FileInputStream("src/test/resources/images/testImage1.PNG")); - MockMultipartFile file2 = new MockMultipartFile("image2", "test2.png", "image/png", - new FileInputStream("src/test/resources/images/testImage2.PNG")); - - MockMultipartFile file3 = new MockMultipartFile("image3", "test3.png", "image/png", - new FileInputStream("src/test/resources/images/testImage3.PNG")); - - PostCreateRequest postCreateRequest = PostCreateRequest.builder().categoryIds(List.of(category1.getId())) - .title("title").content("content").postOptions( - List.of(PostOptionCreateRequest.builder().content("option1").build(), - PostOptionCreateRequest.builder().content("option2").build())) - .deadline(LocalDateTime.now().plusDays(3)).build(); - - Long savedPostId = postService.save(postCreateRequest, writer, List.of(file3), List.of(file1, file2)); - - Category category2 = categoryRepository.save(CategoryFixtures.FOOD.get()); - MockMultipartFile file4 = new MockMultipartFile("image4", "test4.png", "image/png", - new FileInputStream("src/test/resources/images/testImage1.PNG")); - MockMultipartFile file5 = new MockMultipartFile("image5", "test5.png", "image/png", - new FileInputStream("src/test/resources/images/testImage2.PNG")); - - MockMultipartFile file6 = new MockMultipartFile("image6", "test6.png", "image/png", - new FileInputStream("src/test/resources/images/testImage3.PNG")); - - PostUpdateRequest postUpdateRequest = PostUpdateRequest.builder().categoryIds(List.of(category2.getId())) - .title("title2").content("content2").postOptions( - List.of(PostOptionUpdateRequest.builder().content("option3").build(), - PostOptionUpdateRequest.builder().content("option4").build())) - .deadline(LocalDateTime.now().plusDays(4)).build(); - - // when, then - assertThatThrownBy(() -> postService.update(savedPostId, postUpdateRequest, writer, List.of(file4), - List.of(file5, file6))).isInstanceOf(BadRequestException.class) - .hasMessage(PostExceptionType.DEADLINE_EXCEED_THREE_DAYS.getMessage()); - } - - @Test - @DisplayName("게시글을 수정할 시, 해당 게시글이 투표가 되어있으면 예외를 던진다.") - void throwExceptionUpdateVotingProgress() throws IOException { - // given - Category category1 = categoryRepository.save(CategoryFixtures.DEVELOP.get()); - Member writer = memberRepository.save(MemberFixtures.MALE_20.get()); - - MockMultipartFile file1 = new MockMultipartFile("image1", "test1.png", "image/png", - new FileInputStream("src/test/resources/images/testImage1.PNG")); - MockMultipartFile file2 = new MockMultipartFile("image2", "test2.png", "image/png", - new FileInputStream("src/test/resources/images/testImage2.PNG")); - - MockMultipartFile file3 = new MockMultipartFile("image3", "test3.png", "image/png", - new FileInputStream("src/test/resources/images/testImage3.PNG")); - - PostCreateRequest postCreateRequest = PostCreateRequest.builder().categoryIds(List.of(category1.getId())) - .title("title").content("content").postOptions( - List.of(PostOptionCreateRequest.builder().content("option1").build(), - PostOptionCreateRequest.builder().content("option2").build())) - .deadline(LocalDateTime.now().plusDays(2)).build(); - - Long savedPostId = postService.save(postCreateRequest, writer, List.of(file3), List.of(file1, file2)); - - Member memberToVote = Member.builder().nickname("Abel").gender(Gender.MALE).birthYear(2000) - .socialType(SocialType.KAKAO).socialId("Abel").build(); - - memberRepository.save(memberToVote); - - final Post post = postRepository.findById(savedPostId).get(); - final List postOptions = post.getPostOptions().getPostOptions(); - PostOption perPostOption = postOptions.get(0); - voteService.vote(memberToVote, savedPostId, perPostOption.getId()); - entityManager.clear(); - - Category category2 = categoryRepository.save(CategoryFixtures.FOOD.get()); - MockMultipartFile file4 = new MockMultipartFile("image4", "test4.png", "image/png", - new FileInputStream("src/test/resources/images/testImage1.PNG")); - MockMultipartFile file5 = new MockMultipartFile("image5", "test5.png", "image/png", - new FileInputStream("src/test/resources/images/testImage2.PNG")); - - MockMultipartFile file6 = new MockMultipartFile("image6", "test6.png", "image/png", - new FileInputStream("src/test/resources/images/testImage3.PNG")); - - PostUpdateRequest postUpdateRequest = PostUpdateRequest.builder().categoryIds(List.of(category2.getId())) - .title("title2").content("content2").postOptions( - List.of(PostOptionUpdateRequest.builder().content("option3").build(), - PostOptionUpdateRequest.builder().content("option4").build())) - .deadline(LocalDateTime.now().plusDays(1)).build(); - - // when, then - assertThatThrownBy(() -> postService.update(savedPostId, postUpdateRequest, writer, List.of(file4), - List.of(file5, file6))).isInstanceOf(BadRequestException.class) - .hasMessage(PostExceptionType.VOTING_PROGRESS_NOT_EDITABLE.getMessage()); - } - - @Test - @DisplayName("회원 본인이 작성한 게시글 목록을 가져온다.") - void getPostsByWriter() { - // given - Member writer = memberTestPersister.builder().save(); - Post post = postTestPersister.builder().writer(writer).save(); - PostOption postOption = postOptionTestPersister.builder().post(post).sequence(1).save(); - PostOption postOption1 = postOptionTestPersister.builder().post(post).sequence(2).save(); - voteTestPersister.builder().postOption(postOption).save(); - voteTestPersister.builder().postOption(postOption).save(); - voteTestPersister.builder().postOption(postOption1).save(); - - entityManager.flush(); - entityManager.clear(); - - // when - List responses = postService.getPostsByWriter(0, PostClosingType.ALL, PostSortType.LATEST, null, - writer); - - // then - assertAll(() -> assertThat(responses).hasSize(1), - () -> assertThat(responses.get(0).postId()).isEqualTo(post.getId()), - () -> assertThat(responses.get(0).writer().id()).isEqualTo(writer.getId()), - () -> assertThat(responses.get(0).voteInfo().totalVoteCount()).isEqualTo(3L)); - } - - @Test - @DisplayName("회원으로 키워드를 통해 게시글 목록을 검색한다.") - void getPostsByKeyword() { - Member member = memberRepository.save(MemberFixtures.MALE_20.get()); - Member member1 = memberRepository.save(MemberFixtures.MALE_30.get()); - - Post openPost = postTestPersister.builder().postBody(PostBody.builder().title("제목").content("키워요").build()) - .deadline(LocalDateTime.now().plusDays(3L)).save(); - Post openPost1 = postTestPersister.builder().postBody(PostBody.builder().title("키워드").content("안녕").build()) - .deadline(LocalDateTime.now().plusDays(3L)).save(); - - PostOption postOption = postOptionTestPersister.builder().post(openPost).save(); - PostOption postOption1 = postOptionTestPersister.builder().post(openPost1).save(); - voteTestPersister.builder().member(member).postOption(postOption).save(); - voteTestPersister.builder().member(member1).postOption(postOption1).save(); - - entityManager.flush(); - entityManager.clear(); - - // when - List responses = postService.searchPostsWithKeyword("키워", 0, PostClosingType.ALL, - PostSortType.LATEST, null, member); - - // then - assertAll(() -> assertThat(responses).hasSize(2), - () -> assertThat(responses.get(0).postId()).isEqualTo(openPost1.getId()), - () -> assertThat(responses.get(1).postId()).isEqualTo(openPost.getId()), - () -> assertThat(hasKeywordInPostResponse(responses.get(0), "키워")).isTrue(), - () -> assertThat(hasKeywordInPostResponse(responses.get(1), "키워")).isTrue(), - () -> assertThat(responses.get(0).voteInfo().totalVoteCount()).isEqualTo(-1L), - () -> assertThat(responses.get(1).voteInfo().totalVoteCount()).isEqualTo(1L)); - } - - private boolean hasKeywordInPostResponse(PostResponse postResponse, String keyword) { - return postResponse.title().contains(keyword) || postResponse.content().contains(keyword); - } - - @Test - @DisplayName("비회원으로 키워드를 통해 게시글 목록을 검색한다.") - void getPostsByKeywordForGuest() { - Member member = memberRepository.save(MemberFixtures.MALE_20.get()); - - Post closedPost = postTestPersister.builder().postBody(PostBody.builder().title("제목").content("키워요").build()) - .deadline(LocalDateTime.now().minusDays(3L)).save(); - Post openPost1 = postTestPersister.builder().postBody(PostBody.builder().title("키워드").content("안녕").build()) - .deadline(LocalDateTime.now().plusDays(3L)).save(); - - PostOption postOption = postOptionTestPersister.builder().post(closedPost).save(); - PostOption postOption1 = postOptionTestPersister.builder().post(openPost1).save(); - voteTestPersister.builder().member(member).postOption(postOption).save(); - voteTestPersister.builder().member(member).postOption(postOption1).save(); - - entityManager.flush(); - entityManager.clear(); - - // when - List responses = postService.searchPostsWithKeywordForGuest("키워", 0, PostClosingType.ALL, - PostSortType.LATEST, null); - - // then - assertAll(() -> assertThat(responses).hasSize(2), - () -> assertThat(responses.get(0).postId()).isEqualTo(openPost1.getId()), - () -> assertThat(responses.get(1).postId()).isEqualTo(closedPost.getId()), - () -> assertThat(hasKeywordInPostResponse(responses.get(0), "키워")).isTrue(), - () -> assertThat(hasKeywordInPostResponse(responses.get(1), "키워")).isTrue(), - () -> assertThat(responses.get(0).voteInfo().totalVoteCount()).isEqualTo(-1L), - () -> assertThat(responses.get(1).voteInfo().totalVoteCount()).isEqualTo(1L)); - } - - - @Nested - @DisplayName("상위 10개 인기 게시물을 조회한다.") - class Ranking { - - @Test - @DisplayName("중복 순위가 있는 경우") - void getRanking() { - // given - List posts = new ArrayList<>(); - List postOptions = new ArrayList<>(); - - for (int i = 0; i < 11; i++) { - Post post = postTestPersister.builder().save(); - PostOption postOption = postOptionTestPersister.builder().post(post).save(); - posts.add(post); - postOptions.add(postOption); - } - - for (int i = 0; i < 10; i++) { - for (int j = 0; j < i + 1; j++) { - voteTestPersister.builder().postOption(postOptions.get(i)).save(); - } - } - - voteTestPersister.builder().postOption(postOptions.get(10)).save(); - voteTestPersister.builder().postOption(postOptions.get(10)).save(); - voteTestPersister.builder().postOption(postOptions.get(10)).save(); - voteTestPersister.builder().postOption(postOptions.get(7)).save(); - - /* - index |0| |1| |2| |3| |4| |5| |6| |7| |8| |9| |10| - voteCount |1| |2| |3| |4| |5| |6| |7| |9| |9| |10| |3| - ranking |11| |10| |8| |7| |6| |5| |4| |2| |2| |1| |8| - */ - - entityManager.clear(); - entityManager.flush(); - - // when - List rankings = postService.getRanking(); - - // then - assertAll( - () -> assertThat(rankings).hasSize(10), - () -> assertThat(rankings.get(0).ranking()).isEqualTo(1), - () -> assertThat(rankings.get(1).ranking()).isEqualTo(2), - () -> assertThat(rankings.get(2).ranking()).isEqualTo(2), - () -> assertThat(rankings.get(3).ranking()).isEqualTo(4), - () -> assertThat(rankings.get(4).ranking()).isEqualTo(5), - () -> assertThat(rankings.get(5).ranking()).isEqualTo(6), - () -> assertThat(rankings.get(6).ranking()).isEqualTo(7), - () -> assertThat(rankings.get(7).ranking()).isEqualTo(8), - () -> assertThat(rankings.get(8).ranking()).isEqualTo(8), - () -> assertThat(rankings.get(9).ranking()).isEqualTo(10), - - () -> assertThat(rankings.get(0).postSummaryResponse().id()).isEqualTo(posts.get(9).getId()), - () -> assertThat(rankings.get(3).postSummaryResponse().id()).isEqualTo(posts.get(6).getId()), - () -> assertThat(rankings.get(4).postSummaryResponse().id()).isEqualTo(posts.get(5).getId()), - () -> assertThat(rankings.get(5).postSummaryResponse().id()).isEqualTo(posts.get(4).getId()), - () -> assertThat(rankings.get(6).postSummaryResponse().id()).isEqualTo(posts.get(3).getId()), - () -> assertThat(rankings.get(9).postSummaryResponse().id()).isEqualTo(posts.get(1).getId()), - () -> assertThat( - List.of(rankings.get(7).postSummaryResponse().id(), - rankings.get(8).postSummaryResponse().id()) - .containsAll(List.of(posts.get(10).getId(), posts.get(2).getId()))), - () -> assertThat( - List.of(rankings.get(1).postSummaryResponse().id(), - rankings.get(2).postSummaryResponse().id()) - .containsAll(List.of(posts.get(7).getId(), - posts.get(8).getId()))) - ); - } - - @Test - @DisplayName("중복 순위가 없는 경우") - void getRanking1() { - // given - List posts = new ArrayList<>(); - List postOptions = new ArrayList<>(); - - for (int i = 0; i < 10; i++) { - Post post = postTestPersister.builder().save(); - PostOption postOption = postOptionTestPersister.builder().post(post).save(); - posts.add(post); - postOptions.add(postOption); - } - - for (int i = 0; i < 10; i++) { - for (int j = 0; j < i + 1; j++) { - voteTestPersister.builder().postOption(postOptions.get(i)).save(); - } - } - - /* - index |0| |1| |2| |3| |4| |5| |6| |7| |8| |9| - voteCount |1| |2| |3| |4| |5| |6| |7| |8| |9| |10| - ranking |10| |9| |8| |7| |6| |5| |4| |3| |2| |1| - */ - - entityManager.clear(); - entityManager.flush(); - - // when - List rankings = postService.getRanking(); - - // then - assertAll( - () -> assertThat(rankings).hasSize(10), - () -> assertThat(rankings.get(0).ranking()).isEqualTo(1), - () -> assertThat(rankings.get(1).ranking()).isEqualTo(2), - () -> assertThat(rankings.get(2).ranking()).isEqualTo(3), - () -> assertThat(rankings.get(3).ranking()).isEqualTo(4), - () -> assertThat(rankings.get(4).ranking()).isEqualTo(5), - () -> assertThat(rankings.get(5).ranking()).isEqualTo(6), - () -> assertThat(rankings.get(6).ranking()).isEqualTo(7), - () -> assertThat(rankings.get(7).ranking()).isEqualTo(8), - () -> assertThat(rankings.get(8).ranking()).isEqualTo(9), - () -> assertThat(rankings.get(9).ranking()).isEqualTo(10), - - () -> assertThat(rankings.get(0).postSummaryResponse().id()).isEqualTo(posts.get(9).getId()), - () -> assertThat(rankings.get(1).postSummaryResponse().id()).isEqualTo(posts.get(8).getId()), - () -> assertThat(rankings.get(2).postSummaryResponse().id()).isEqualTo(posts.get(7).getId()), - () -> assertThat(rankings.get(3).postSummaryResponse().id()).isEqualTo(posts.get(6).getId()), - () -> assertThat(rankings.get(4).postSummaryResponse().id()).isEqualTo(posts.get(5).getId()), - () -> assertThat(rankings.get(5).postSummaryResponse().id()).isEqualTo(posts.get(4).getId()), - () -> assertThat(rankings.get(6).postSummaryResponse().id()).isEqualTo(posts.get(3).getId()), - () -> assertThat(rankings.get(7).postSummaryResponse().id()).isEqualTo(posts.get(2).getId()), - () -> assertThat(rankings.get(8).postSummaryResponse().id()).isEqualTo(posts.get(1).getId()), - () -> assertThat(rankings.get(9).postSummaryResponse().id()).isEqualTo(posts.get(0).getId()) - ); - } - - } - -} diff --git a/backend/src/test/java/com/votogether/domain/ranking/service/RankingServiceTest.java b/backend/src/test/java/com/votogether/domain/ranking/service/RankingServiceTest.java index cfeea9c15..99faa0bcb 100644 --- a/backend/src/test/java/com/votogether/domain/ranking/service/RankingServiceTest.java +++ b/backend/src/test/java/com/votogether/domain/ranking/service/RankingServiceTest.java @@ -5,34 +5,17 @@ import com.votogether.domain.member.entity.Member; import com.votogether.domain.ranking.dto.response.RankingResponse; -import com.votogether.test.annotation.ServiceTest; -import com.votogether.test.persister.MemberTestPersister; -import com.votogether.test.persister.PostOptionTestPersister; -import com.votogether.test.persister.PostTestPersister; -import com.votogether.test.persister.VoteTestPersister; +import com.votogether.test.ServiceTest; import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -@ServiceTest -class RankingServiceTest { +class RankingServiceTest extends ServiceTest { @Autowired RankingService rankingService; - @Autowired - MemberTestPersister memberTestPersister; - - @Autowired - PostTestPersister postTestPersister; - - @Autowired - PostOptionTestPersister postOptionTestPersister; - - @Autowired - VoteTestPersister voteTestPersister; - @Test @DisplayName("회원의 열정 랭킹을 조회한다.") void getMemberPassionRanking() { @@ -43,10 +26,10 @@ void getMemberPassionRanking() { Member memberD = memberTestPersister.builder().save(); Member memberE = memberTestPersister.builder().save(); - postTestPersister.builder().writer(memberA).save(); - postTestPersister.builder().writer(memberB).save(); - postTestPersister.builder().writer(memberC).save(); - postTestPersister.builder().writer(memberD).save(); + postTestPersister.postBuilder().writer(memberA).save(); + postTestPersister.postBuilder().writer(memberB).save(); + postTestPersister.postBuilder().writer(memberC).save(); + postTestPersister.postBuilder().writer(memberD).save(); voteTestPersister.builder().member(memberA).save(); voteTestPersister.builder().member(memberC).save(); @@ -81,11 +64,11 @@ void getPassionRankingTop10() { memberTestPersister.builder().save(); memberTestPersister.builder().save(); - postTestPersister.builder().writer(memberA).save(); - postTestPersister.builder().writer(memberB).save(); + postTestPersister.postBuilder().writer(memberA).save(); + postTestPersister.postBuilder().writer(memberB).save(); // when - final List rankings = rankingService.getPassionRanking(); + List rankings = rankingService.getPassionRanking(); // then assertAll( diff --git a/backend/src/test/java/com/votogether/domain/report/repository/ReportRepositoryTest.java b/backend/src/test/java/com/votogether/domain/report/repository/ReportRepositoryTest.java index 6e489bd15..48c88ce5c 100644 --- a/backend/src/test/java/com/votogether/domain/report/repository/ReportRepositoryTest.java +++ b/backend/src/test/java/com/votogether/domain/report/repository/ReportRepositoryTest.java @@ -1,71 +1,32 @@ package com.votogether.domain.report.repository; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertAll; +import static org.assertj.core.api.SoftAssertions.assertSoftly; import com.votogether.domain.member.entity.Member; -import com.votogether.domain.member.repository.MemberRepository; import com.votogether.domain.post.entity.Post; -import com.votogether.domain.post.entity.PostBody; -import com.votogether.domain.post.repository.PostRepository; import com.votogether.domain.report.entity.Report; import com.votogether.domain.report.entity.vo.ReportType; -import com.votogether.test.annotation.RepositoryTest; -import com.votogether.test.fixtures.MemberFixtures; -import com.votogether.test.persister.PostTestPersister; -import com.votogether.test.persister.ReportTestPersister; -import java.time.LocalDateTime; -import lombok.RequiredArgsConstructor; +import com.votogether.test.RepositoryTest; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -@RepositoryTest -class ReportRepositoryTest { +class ReportRepositoryTest extends RepositoryTest { @Autowired ReportRepository reportRepository; - @Autowired - MemberRepository memberRepository; - - @Autowired - PostRepository postRepository; - - @Autowired - PostTestPersister postTestPersister; - - @Autowired - ReportTestPersister reportTestPersister; - - @Test @DisplayName("회원, 신고타입, 대상ID를 통해서 신고 횟수를 반환한다.") void countByMemberAndReportTypeAndTargetId() { // given - Member member = MemberFixtures.MALE_20.get(); - ReportType reportType = ReportType.POST; - PostBody postBody = PostBody.builder() - .title("title") - .content("content") - .build(); - - memberRepository.save(member); - Post post = postTestPersister.builder() - .writer(member) - .postBody(postBody) - .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) - .save(); - - reportTestPersister.builder() - .member(member) - .reportType(reportType) - .targetId(post.getId()) - .reason("불건전한 게시글") - .save(); + memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().save(); + reportTestPersister.builder().reportType(ReportType.POST).targetId(post.getId()).save(); // when - int reportCount = reportRepository.countByReportTypeAndTargetId(reportType, post.getId()); + int reportCount = reportRepository.countByReportTypeAndTargetId(ReportType.POST, post.getId()); // then assertThat(reportCount).isEqualTo(1); @@ -75,26 +36,9 @@ void countByMemberAndReportTypeAndTargetId() { @DisplayName("회원, 신고유형, 신고대상ID를 통해 해당 신고정보를 반환한다.") void findByMemberAndReportTypeAndTargetId() { // given - Member member = MemberFixtures.FEMALE_30.get(); - - PostBody postBody = PostBody.builder() - .title("title") - .content("content") - .build(); - - memberRepository.save(member); - Post post = postTestPersister.builder() - .writer(member) - .postBody(postBody) - .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) - .save(); - - reportTestPersister.builder() - .member(member) - .reportType(ReportType.POST) - .targetId(post.getId()) - .reason("불건전한 게시글") - .save(); + Member member = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().save(); + reportTestPersister.builder().member(member).reportType(ReportType.POST).targetId(post.getId()).save(); // when Report actualReport = reportRepository.findByMemberAndReportTypeAndTargetId( @@ -104,41 +48,25 @@ void findByMemberAndReportTypeAndTargetId() { ).get(); // then - assertAll( - () -> assertThat(actualReport.getTargetId()).isEqualTo(post.getId()), - () -> assertThat(actualReport.getMember()).isEqualTo(member), - () -> assertThat(actualReport.getReportType()).isEqualTo(ReportType.POST) - ); + assertSoftly(softly -> { + softly.assertThat(actualReport.getTargetId()).isEqualTo(post.getId()); + softly.assertThat(actualReport.getMember()).isEqualTo(member); + softly.assertThat(actualReport.getReportType()).isEqualTo(ReportType.POST); + }); } @Test - @DisplayName("신고유형, 신고대상ID를 통해 관련된 신고정보를 모두 삭제한다.") - void deleteByReportTypeAndTargetId() { + @DisplayName("신고 유형, 신고대상ID를 통해 모든 신고 정보를 삭제한다.") + void deleteAllWithReportTypeAndTargetIdInBatch() { // given - Member member = MemberFixtures.FEMALE_30.get(); - Member reporterA = MemberFixtures.MALE_30.get(); - Member reporterB = MemberFixtures.FEMALE_20.get(); - - memberRepository.save(member); - memberRepository.save(reporterA); - memberRepository.save(reporterB); - - reportTestPersister.builder() - .member(reporterA) - .reportType(ReportType.NICKNAME) - .targetId(member.getId()) - .reason("불건전한 게시글") - .save(); - - reportTestPersister.builder() - .member(reporterB) - .reportType(ReportType.NICKNAME) - .targetId(member.getId()) - .reason("불건전한 게시글") - .save(); + Member reporterA = memberTestPersister.builder().save(); + Member reporterB = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().save(); + reportTestPersister.builder().member(reporterA).reportType(ReportType.POST).targetId(post.getId()).save(); + reportTestPersister.builder().member(reporterB).reportType(ReportType.POST).targetId(post.getId()).save(); // when - reportRepository.deleteByReportTypeAndTargetId(ReportType.NICKNAME, member.getId()); + reportRepository.deleteAllWithReportTypeAndTargetIdInBatch(ReportType.POST, post.getId()); // then assertThat(reportRepository.findAll()).isEmpty(); diff --git a/backend/src/test/java/com/votogether/domain/report/service/ReportCommandServiceTest.java b/backend/src/test/java/com/votogether/domain/report/service/ReportCommandServiceTest.java index 19853b5ae..851d33bbd 100644 --- a/backend/src/test/java/com/votogether/domain/report/service/ReportCommandServiceTest.java +++ b/backend/src/test/java/com/votogether/domain/report/service/ReportCommandServiceTest.java @@ -6,64 +6,35 @@ import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import com.votogether.domain.member.entity.Member; -import com.votogether.domain.member.repository.MemberRepository; import com.votogether.domain.post.dto.response.post.PostResponse; import com.votogether.domain.post.entity.Post; -import com.votogether.domain.post.entity.PostBody; import com.votogether.domain.post.entity.comment.Comment; -import com.votogether.domain.post.entity.comment.Content; import com.votogether.domain.post.entity.vo.PostClosingType; import com.votogether.domain.post.entity.vo.PostSortType; -import com.votogether.domain.post.repository.CommentRepository; -import com.votogether.domain.post.repository.PostRepository; import com.votogether.domain.post.service.PostCommentService; -import com.votogether.domain.post.service.PostService; +import com.votogether.domain.post.service.PostGuestService; import com.votogether.domain.report.dto.request.ReportRequest; import com.votogether.domain.report.entity.vo.ReportType; -import com.votogether.domain.report.repository.ReportRepository; import com.votogether.global.exception.BadRequestException; import com.votogether.global.exception.NotFoundException; -import com.votogether.test.annotation.ServiceTest; -import com.votogether.test.fixtures.MemberFixtures; -import com.votogether.test.persister.CommentTestPersister; -import com.votogether.test.persister.PostTestPersister; -import java.time.LocalDateTime; +import com.votogether.test.ServiceTest; import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -@ServiceTest -class ReportCommandServiceTest { +class ReportCommandServiceTest extends ServiceTest { @Autowired ReportCommandService reportCommandService; @Autowired - MemberRepository memberRepository; - - @Autowired - PostRepository postRepository; - - @Autowired - ReportRepository reportRepository; - - @Autowired - CommentRepository commentRepository; - - @Autowired - PostService postService; + PostGuestService postGuestService; @Autowired PostCommentService postCommentService; - @Autowired - PostTestPersister postTestPersister; - - @Autowired - CommentTestPersister commentTestPersister; - @Nested @DisplayName("게시글 신고기능은") class ReportPost { @@ -72,20 +43,8 @@ class ReportPost { @DisplayName("정상적으로 동작한다.") void reportPost() { // given - Member reporter = memberRepository.save(MemberFixtures.FEMALE_60.get()); - Member writer = memberRepository.save(MemberFixtures.FEMALE_30.get()); - - PostBody postBody = PostBody.builder() - .title("title") - .content("content") - .build(); - - Post post = postTestPersister.builder() - .writer(writer) - .postBody(postBody) - .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) - .save(); - + Member reporter = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().save(); ReportRequest request = new ReportRequest(ReportType.POST, post.getId(), "불건전한 게시글"); // when, then @@ -96,87 +55,50 @@ void reportPost() { @DisplayName("없는 투표글을 신고하는 경우 예외가 발생한다.") void reportNonExistPostThrowsException() { // given - Member writer = memberRepository.save(MemberFixtures.FEMALE_30.get()); - + Member writer = memberTestPersister.builder().save(); ReportRequest request = new ReportRequest(ReportType.POST, -1L, "불건전한 게시글"); // when, then assertThatThrownBy(() -> reportCommandService.report(writer, request)) .isInstanceOf(NotFoundException.class) - .hasMessage("해당 게시글이 존재하지 않습니다."); + .hasMessage("게시글이 존재하지 않습니다."); } @Test @DisplayName("자신의 투표글을 신고하는 경우 예외가 발생한다.") void reportOwnPostThrowsException() { // given - Member writer = memberRepository.save(MemberFixtures.FEMALE_30.get()); - - PostBody postBody = PostBody.builder() - .title("title") - .content("content") - .build(); - - Post post = postTestPersister.builder() - .writer(writer) - .postBody(postBody) - .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) - .save(); - + Member writer = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().writer(writer).save(); ReportRequest request = new ReportRequest(ReportType.POST, post.getId(), "불건전한 게시글"); // when, then assertThatThrownBy(() -> reportCommandService.report(writer, request)) .isInstanceOf(BadRequestException.class) - .hasMessage("자신의 게시글은 신고할 수 없습니다."); + .hasMessage("본인 게시글은 신고할 수 없습니다."); } @Test @DisplayName("블라인드 처리된 투표글을 신고하는 경우 예외가 발생한다.") void reportHiddenPost() { // given - Member reporter = memberRepository.save(MemberFixtures.FEMALE_60.get()); - Member writer = memberRepository.save(MemberFixtures.FEMALE_30.get()); - - PostBody postBody = PostBody.builder() - .title("title") - .content("content") - .build(); - - Post post = postTestPersister.builder() - .writer(writer) - .postBody(postBody) - .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) - .blind() - .save(); - + Member reporter = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().save(); + post.blind(); ReportRequest request = new ReportRequest(ReportType.POST, post.getId(), "불건전한 게시글"); // when, then - assertThatThrownBy(() -> reportCommandService.report(reporter, request)) .isInstanceOf(BadRequestException.class) - .hasMessage("이미 블라인드 처리된 글입니다."); + .hasMessage("신고에 의해 숨겨진 게시글은 접근할 수 없습니다."); } @Test @DisplayName("하나의 회원이 투표글을 중복하여 신고하면 예외를 던진다.") void reportDuplicated() { // given - Member reporter = memberRepository.save(MemberFixtures.FEMALE_20.get()); - Member writer = memberRepository.save(MemberFixtures.FEMALE_10.get()); - - PostBody postBody = PostBody.builder() - .title("title") - .content("content") - .build(); - - Post post = postTestPersister.builder() - .writer(writer) - .postBody(postBody) - .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) - .save(); - + Member reporter = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().save(); ReportRequest request = new ReportRequest(ReportType.POST, post.getId(), "불건전한 게시글"); // when @@ -192,24 +114,12 @@ void reportDuplicated() { @DisplayName("투표글 신고가 5회가 되면 블라인드 처리가 된다.") void reportAndBlind() { // given - Member reporter1 = memberRepository.save(MemberFixtures.FEMALE_20.get()); - Member reporter2 = memberRepository.save(MemberFixtures.FEMALE_30.get()); - Member reporter3 = memberRepository.save(MemberFixtures.FEMALE_40.get()); - Member reporter4 = memberRepository.save(MemberFixtures.FEMALE_50.get()); - Member reporter5 = memberRepository.save(MemberFixtures.FEMALE_60.get()); - Member writer = memberRepository.save(MemberFixtures.FEMALE_10.get()); - - PostBody postBody = PostBody.builder() - .title("title") - .content("content") - .build(); - - Post post = postTestPersister.builder() - .writer(writer) - .postBody(postBody) - .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) - .save(); - + Member reporter1 = memberTestPersister.builder().save(); + Member reporter2 = memberTestPersister.builder().save(); + Member reporter3 = memberTestPersister.builder().save(); + Member reporter4 = memberTestPersister.builder().save(); + Member reporter5 = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().save(); ReportRequest request = new ReportRequest(ReportType.POST, post.getId(), "불건전한 게시글"); // when @@ -220,13 +130,12 @@ void reportAndBlind() { reportCommandService.report(reporter5, request); // then - final List responses = postService.getPostsGuest( + final List responses = postGuestService.getPosts( 0, PostClosingType.ALL, PostSortType.HOT, null ); - assertAll( () -> assertThat(post.isHidden()).isTrue(), () -> assertThat(responses).isEmpty() @@ -243,26 +152,9 @@ class ReportComment { @DisplayName("정상적으로 동작한다.") void reportComment() { // given - Member reporter = memberRepository.save(MemberFixtures.FEMALE_60.get()); - Member writer = memberRepository.save(MemberFixtures.FEMALE_30.get()); - - PostBody postBody = PostBody.builder() - .title("title") - .content("content") - .build(); - - Post post = postTestPersister.builder() - .writer(writer) - .postBody(postBody) - .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) - .save(); - - Comment comment = commentTestPersister.builder() - .post(post) - .member(writer) - .content(new Content("으어어어어")) - .save(); - + Member reporter = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().save(); + Comment comment = commentTestPersister.builder().post(post).save(); ReportRequest request = new ReportRequest(ReportType.COMMENT, comment.getId(), "불건전한 게시글"); // when, then @@ -273,104 +165,54 @@ void reportComment() { @DisplayName("없는 댓글을 신고하는 경우 예외가 발생한다.") void reportNonExistCommentThrowsException() { // given - Member writer = memberRepository.save(MemberFixtures.FEMALE_30.get()); - + Member writer = memberTestPersister.builder().save(); ReportRequest request = new ReportRequest(ReportType.COMMENT, -1L, "불건전한 댓글"); // when, then assertThatThrownBy(() -> reportCommandService.report(writer, request)) .isInstanceOf(NotFoundException.class) - .hasMessage("해당 댓글이 존재하지 않습니다."); + .hasMessage("댓글이 존재하지 않습니다."); } @Test @DisplayName("자신의 댓글을 신고하는 경우 예외가 발생한다.") void reportOwnCommentThrowsException() { // given - Member writer = memberRepository.save(MemberFixtures.FEMALE_30.get()); - - PostBody postBody = PostBody.builder() - .title("title") - .content("content") - .build(); - - Post post = postTestPersister.builder() - .writer(writer) - .postBody(postBody) - .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) - .save(); - - Comment comment = commentTestPersister.builder() - .post(post) - .member(writer) - .content(new Content("으어어어어")) - .save(); - + Member writer = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().save(); + Comment comment = commentTestPersister.builder().post(post).writer(writer).save(); ReportRequest request = new ReportRequest(ReportType.COMMENT, comment.getId(), "불건전한 댓글"); // when, then assertThatThrownBy(() -> reportCommandService.report(writer, request)) .isInstanceOf(BadRequestException.class) - .hasMessage("자신의 댓글은 신고할 수 없습니다."); + .hasMessage("본인 댓글은 신고할 수 없습니다."); } @Test @DisplayName("블라인드 처리된 댓글을 신고하는 경우 예외가 발생한다.") void reportHiddenComment() { // given - Member reporter = memberRepository.save(MemberFixtures.FEMALE_60.get()); - Member writer = memberRepository.save(MemberFixtures.FEMALE_30.get()); - - PostBody postBody = PostBody.builder() - .title("title") - .content("content") - .build(); - - Post post = postTestPersister.builder() - .writer(writer) - .postBody(postBody) - .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) - .save(); - - Comment comment = commentTestPersister.builder() - .post(post) - .member(writer) - .content(new Content("으어어어어")) - .save(); - + Member reporter = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().save(); + Comment comment = commentTestPersister.builder().post(post).save(); + comment.blind(); ReportRequest request = new ReportRequest(ReportType.COMMENT, comment.getId(), "불건전한 댓글"); // when, then comment.blind(); assertThatThrownBy(() -> reportCommandService.report(reporter, request)) .isInstanceOf(BadRequestException.class) - .hasMessage("이미 블라인드 처리된 댓글입니다."); + .hasMessage("신고에 의해 숨겨진 댓글은 접근할 수 없습니다."); } @Test @DisplayName("하나의 회원이 댓글을 중복하여 신고하면 예외를 던진다.") void reportDuplicated() { // given - Member reporter = memberRepository.save(MemberFixtures.FEMALE_20.get()); - Member writer = memberRepository.save(MemberFixtures.FEMALE_10.get()); - - PostBody postBody = PostBody.builder() - .title("title") - .content("content") - .build(); - - Post post = postTestPersister.builder() - .writer(writer) - .postBody(postBody) - .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) - .save(); - - Comment comment = commentTestPersister.builder() - .post(post) - .member(writer) - .content(new Content("으어어어어")) - .save(); - + Member reporter = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().save(); + Comment comment = commentTestPersister.builder().post(post).save(); ReportRequest request = new ReportRequest(ReportType.COMMENT, comment.getId(), "불건전한 댓글"); // when @@ -386,30 +228,13 @@ void reportDuplicated() { @DisplayName("댓글 신고가 5회가 되면 블라인드 처리가 된다.") void reportAndBlind() { // given - Member reporter1 = memberRepository.save(MemberFixtures.FEMALE_20.get()); - Member reporter2 = memberRepository.save(MemberFixtures.FEMALE_30.get()); - Member reporter3 = memberRepository.save(MemberFixtures.FEMALE_40.get()); - Member reporter4 = memberRepository.save(MemberFixtures.FEMALE_50.get()); - Member reporter5 = memberRepository.save(MemberFixtures.FEMALE_60.get()); - Member writer = memberRepository.save(MemberFixtures.FEMALE_10.get()); - - PostBody postBody = PostBody.builder() - .title("title") - .content("content") - .build(); - - Post post = postTestPersister.builder() - .writer(writer) - .postBody(postBody) - .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) - .save(); - - Comment comment = commentTestPersister.builder() - .post(post) - .member(writer) - .content(new Content("으어어어어")) - .save(); - + Member reporter1 = memberTestPersister.builder().save(); + Member reporter2 = memberTestPersister.builder().save(); + Member reporter3 = memberTestPersister.builder().save(); + Member reporter4 = memberTestPersister.builder().save(); + Member reporter5 = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().save(); + Comment comment = commentTestPersister.builder().post(post).save(); ReportRequest request = new ReportRequest(ReportType.COMMENT, comment.getId(), "불건전한 댓글"); // when @@ -436,9 +261,8 @@ class ReportNickname { @DisplayName("정상적으로 동작한다.") void reportNickname() { // given - Member reporter = memberRepository.save(MemberFixtures.FEMALE_60.get()); - Member reported = memberRepository.save(MemberFixtures.FEMALE_30.get()); - + Member reporter = memberTestPersister.builder().save(); + Member reported = memberTestPersister.builder().save(); ReportRequest request = new ReportRequest(ReportType.NICKNAME, reported.getId(), "불건전한 닉네임"); // when, then @@ -449,8 +273,7 @@ void reportNickname() { @DisplayName("자신의 닉네임을 신고하는 경우 예외가 발생한다.") void reportOwnNicknameThrowsException() { // given - Member reporter = memberRepository.save(MemberFixtures.FEMALE_30.get()); - + Member reporter = memberTestPersister.builder().save(); ReportRequest request = new ReportRequest(ReportType.NICKNAME, reporter.getId(), "불건전한 닉네임"); // when, then @@ -463,9 +286,8 @@ void reportOwnNicknameThrowsException() { @DisplayName("하나의 회원이 다른 회원의 닉네임을 중복하여 신고하면 예외를 던진다.") void reportDuplicated() { // given - Member reporter = memberRepository.save(MemberFixtures.FEMALE_20.get()); - Member reported = memberRepository.save(MemberFixtures.FEMALE_10.get()); - + Member reporter = memberTestPersister.builder().save(); + Member reported = memberTestPersister.builder().save(); ReportRequest request = new ReportRequest(ReportType.NICKNAME, reported.getId(), "불건전한 닉네임"); // when @@ -481,11 +303,10 @@ void reportDuplicated() { @DisplayName("닉네임 신고가 3회가 되면 닉네임이 자동변경처리가 된다.") void reportAndBlind() { // given - Member reporter1 = memberRepository.save(MemberFixtures.FEMALE_20.get()); - Member reporter2 = memberRepository.save(MemberFixtures.FEMALE_30.get()); - Member reporter3 = memberRepository.save(MemberFixtures.FEMALE_40.get()); - Member reported = memberRepository.save(MemberFixtures.FEMALE_10.get()); - + Member reporter1 = memberTestPersister.builder().save(); + Member reporter2 = memberTestPersister.builder().save(); + Member reporter3 = memberTestPersister.builder().save(); + Member reported = memberTestPersister.builder().save(); ReportRequest request = new ReportRequest(ReportType.NICKNAME, reported.getId(), "불건전한 닉네임"); // when diff --git a/backend/src/test/java/com/votogether/domain/report/service/strategy/ReportCommentStrategyTest.java b/backend/src/test/java/com/votogether/domain/report/service/strategy/ReportCommentStrategyTest.java index bfd3deb04..3067999d5 100644 --- a/backend/src/test/java/com/votogether/domain/report/service/strategy/ReportCommentStrategyTest.java +++ b/backend/src/test/java/com/votogether/domain/report/service/strategy/ReportCommentStrategyTest.java @@ -6,68 +6,34 @@ import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import com.votogether.domain.member.entity.Member; -import com.votogether.domain.member.repository.MemberRepository; import com.votogether.domain.post.entity.Post; -import com.votogether.domain.post.entity.PostBody; import com.votogether.domain.post.entity.comment.Comment; -import com.votogether.domain.post.entity.comment.Content; import com.votogether.domain.post.service.PostCommentService; import com.votogether.domain.report.dto.request.ReportRequest; import com.votogether.domain.report.entity.vo.ReportType; import com.votogether.global.exception.BadRequestException; import com.votogether.global.exception.NotFoundException; -import com.votogether.test.annotation.ServiceTest; -import com.votogether.test.fixtures.MemberFixtures; -import com.votogether.test.persister.CommentTestPersister; -import com.votogether.test.persister.PostTestPersister; -import java.time.LocalDateTime; +import com.votogether.test.ServiceTest; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -@ServiceTest @DisplayName("댓글 신고기능은") -class ReportCommentStrategyTest { +class ReportCommentStrategyTest extends ServiceTest { @Autowired ReportCommentStrategy reportCommentStrategy; - @Autowired - MemberRepository memberRepository; - - @Autowired - PostTestPersister postTestPersister; - @Autowired PostCommentService postCommentService; - @Autowired - CommentTestPersister commentTestPersister; - @Test @DisplayName("정상적으로 동작한다.") void reportComment() { // given - Member reporter = memberRepository.save(MemberFixtures.FEMALE_60.get()); - Member writer = memberRepository.save(MemberFixtures.FEMALE_30.get()); - - PostBody postBody = PostBody.builder() - .title("title") - .content("content") - .build(); - - Post post = postTestPersister.builder() - .writer(writer) - .postBody(postBody) - .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) - .save(); - - Comment comment = commentTestPersister.builder() - .post(post) - .member(writer) - .content(new Content("으어어어어")) - .save(); - + Member reporter = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().save(); + Comment comment = commentTestPersister.builder().post(post).save(); ReportRequest request = new ReportRequest(ReportType.COMMENT, comment.getId(), "불건전한 게시글"); // when, then @@ -78,104 +44,53 @@ void reportComment() { @DisplayName("없는 댓글을 신고하는 경우 예외가 발생한다.") void reportNonExistCommentThrowsException() { // given - Member writer = memberRepository.save(MemberFixtures.FEMALE_30.get()); - + Member writer = memberTestPersister.builder().save(); ReportRequest request = new ReportRequest(ReportType.COMMENT, -1L, "불건전한 댓글"); // when, then assertThatThrownBy(() -> reportCommentStrategy.report(writer, request)) .isInstanceOf(NotFoundException.class) - .hasMessage("해당 댓글이 존재하지 않습니다."); + .hasMessage("댓글이 존재하지 않습니다."); } @Test @DisplayName("자신의 댓글을 신고하는 경우 예외가 발생한다.") void reportOwnCommentThrowsException() { // given - Member writer = memberRepository.save(MemberFixtures.FEMALE_30.get()); - - PostBody postBody = PostBody.builder() - .title("title") - .content("content") - .build(); - - Post post = postTestPersister.builder() - .writer(writer) - .postBody(postBody) - .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) - .save(); - - Comment comment = commentTestPersister.builder() - .post(post) - .member(writer) - .content(new Content("으어어어어")) - .save(); - + Member writer = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().save(); + Comment comment = commentTestPersister.builder().post(post).writer(writer).save(); ReportRequest request = new ReportRequest(ReportType.COMMENT, comment.getId(), "불건전한 댓글"); // when, then assertThatThrownBy(() -> reportCommentStrategy.report(writer, request)) .isInstanceOf(BadRequestException.class) - .hasMessage("자신의 댓글은 신고할 수 없습니다."); + .hasMessage("본인 댓글은 신고할 수 없습니다."); } @Test @DisplayName("블라인드 처리된 댓글을 신고하는 경우 예외가 발생한다.") void reportHiddenComment() { // given - Member reporter = memberRepository.save(MemberFixtures.FEMALE_60.get()); - Member writer = memberRepository.save(MemberFixtures.FEMALE_30.get()); - - PostBody postBody = PostBody.builder() - .title("title") - .content("content") - .build(); - - Post post = postTestPersister.builder() - .writer(writer) - .postBody(postBody) - .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) - .save(); - - Comment comment = commentTestPersister.builder() - .post(post) - .member(writer) - .content(new Content("으어어어어")) - .save(); - + Member reporter = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().save(); + Comment comment = commentTestPersister.builder().post(post).save(); ReportRequest request = new ReportRequest(ReportType.COMMENT, comment.getId(), "불건전한 댓글"); // when, then comment.blind(); assertThatThrownBy(() -> reportCommentStrategy.report(reporter, request)) .isInstanceOf(BadRequestException.class) - .hasMessage("이미 블라인드 처리된 댓글입니다."); + .hasMessage("신고에 의해 숨겨진 댓글은 접근할 수 없습니다."); } @Test @DisplayName("하나의 회원이 댓글을 중복하여 신고하면 예외를 던진다.") void reportDuplicated() { // given - Member reporter = memberRepository.save(MemberFixtures.FEMALE_20.get()); - Member writer = memberRepository.save(MemberFixtures.FEMALE_10.get()); - - PostBody postBody = PostBody.builder() - .title("title") - .content("content") - .build(); - - Post post = postTestPersister.builder() - .writer(writer) - .postBody(postBody) - .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) - .save(); - - Comment comment = commentTestPersister.builder() - .post(post) - .member(writer) - .content(new Content("으어어어어")) - .save(); - + Member reporter = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().save(); + Comment comment = commentTestPersister.builder().post(post).save(); ReportRequest request = new ReportRequest(ReportType.COMMENT, comment.getId(), "불건전한 댓글"); // when @@ -191,30 +106,13 @@ void reportDuplicated() { @DisplayName("댓글 신고가 5회가 되면 블라인드 처리가 된다.") void reportAndBlind() { // given - Member reporter1 = memberRepository.save(MemberFixtures.FEMALE_20.get()); - Member reporter2 = memberRepository.save(MemberFixtures.FEMALE_30.get()); - Member reporter3 = memberRepository.save(MemberFixtures.FEMALE_40.get()); - Member reporter4 = memberRepository.save(MemberFixtures.FEMALE_50.get()); - Member reporter5 = memberRepository.save(MemberFixtures.FEMALE_60.get()); - Member writer = memberRepository.save(MemberFixtures.FEMALE_10.get()); - - PostBody postBody = PostBody.builder() - .title("title") - .content("content") - .build(); - - Post post = postTestPersister.builder() - .writer(writer) - .postBody(postBody) - .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) - .save(); - - Comment comment = commentTestPersister.builder() - .post(post) - .member(writer) - .content(new Content("으어어어어")) - .save(); - + Member reporter1 = memberTestPersister.builder().save(); + Member reporter2 = memberTestPersister.builder().save(); + Member reporter3 = memberTestPersister.builder().save(); + Member reporter4 = memberTestPersister.builder().save(); + Member reporter5 = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().save(); + Comment comment = commentTestPersister.builder().post(post).save(); ReportRequest request = new ReportRequest(ReportType.COMMENT, comment.getId(), "불건전한 댓글"); // when diff --git a/backend/src/test/java/com/votogether/domain/report/service/strategy/ReportNicknameStrategyTest.java b/backend/src/test/java/com/votogether/domain/report/service/strategy/ReportNicknameStrategyTest.java index b8b47981f..9b873f3ed 100644 --- a/backend/src/test/java/com/votogether/domain/report/service/strategy/ReportNicknameStrategyTest.java +++ b/backend/src/test/java/com/votogether/domain/report/service/strategy/ReportNicknameStrategyTest.java @@ -2,7 +2,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import com.votogether.domain.member.entity.Member; import com.votogether.domain.member.repository.MemberRepository; diff --git a/backend/src/test/java/com/votogether/domain/report/service/strategy/ReportPostStrategyTest.java b/backend/src/test/java/com/votogether/domain/report/service/strategy/ReportPostStrategyTest.java index fee18d357..9b37c8bc1 100644 --- a/backend/src/test/java/com/votogether/domain/report/service/strategy/ReportPostStrategyTest.java +++ b/backend/src/test/java/com/votogether/domain/report/service/strategy/ReportPostStrategyTest.java @@ -2,63 +2,40 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import com.votogether.domain.member.entity.Member; -import com.votogether.domain.member.repository.MemberRepository; import com.votogether.domain.post.dto.response.post.PostResponse; import com.votogether.domain.post.entity.Post; -import com.votogether.domain.post.entity.PostBody; import com.votogether.domain.post.entity.vo.PostClosingType; import com.votogether.domain.post.entity.vo.PostSortType; -import com.votogether.domain.post.service.PostService; +import com.votogether.domain.post.service.PostGuestService; import com.votogether.domain.report.dto.request.ReportRequest; import com.votogether.domain.report.entity.vo.ReportType; import com.votogether.global.exception.BadRequestException; import com.votogether.global.exception.NotFoundException; -import com.votogether.test.annotation.ServiceTest; -import com.votogether.test.fixtures.MemberFixtures; -import com.votogether.test.persister.PostTestPersister; -import java.time.LocalDateTime; +import com.votogether.test.ServiceTest; import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -@ServiceTest @DisplayName("게시글 신고 기능은") -class ReportPostStrategyTest { +class ReportPostStrategyTest extends ServiceTest { @Autowired ReportPostStrategy reportPostStrategy; @Autowired - MemberRepository memberRepository; - - @Autowired - PostTestPersister postTestPersister; - - @Autowired - PostService postService; + PostGuestService postGuestService; @Test @DisplayName("정상적으로 동작한다.") void reportPost() { // given - Member reporter = memberRepository.save(MemberFixtures.FEMALE_60.get()); - Member writer = memberRepository.save(MemberFixtures.FEMALE_30.get()); - - PostBody postBody = PostBody.builder() - .title("title") - .content("content") - .build(); - - Post post = postTestPersister.builder() - .writer(writer) - .postBody(postBody) - .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) - .save(); - + Member reporter = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().save(); ReportRequest request = new ReportRequest(ReportType.POST, post.getId(), "불건전한 게시글"); // when, then @@ -69,87 +46,50 @@ void reportPost() { @DisplayName("없는 투표글을 신고하는 경우 예외가 발생한다.") void reportNonExistPostThrowsException() { // given - Member writer = memberRepository.save(MemberFixtures.FEMALE_30.get()); - + Member writer = memberTestPersister.builder().save(); ReportRequest request = new ReportRequest(ReportType.POST, -1L, "불건전한 게시글"); // when, then assertThatThrownBy(() -> reportPostStrategy.report(writer, request)) .isInstanceOf(NotFoundException.class) - .hasMessage("해당 게시글이 존재하지 않습니다."); + .hasMessage("게시글이 존재하지 않습니다."); } @Test @DisplayName("자신의 투표글을 신고하는 경우 예외가 발생한다.") void reportOwnPostThrowsException() { // given - Member writer = memberRepository.save(MemberFixtures.FEMALE_30.get()); - - PostBody postBody = PostBody.builder() - .title("title") - .content("content") - .build(); - - Post post = postTestPersister.builder() - .writer(writer) - .postBody(postBody) - .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) - .save(); - + Member writer = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().writer(writer).save(); ReportRequest request = new ReportRequest(ReportType.POST, post.getId(), "불건전한 게시글"); // when, then assertThatThrownBy(() -> reportPostStrategy.report(writer, request)) .isInstanceOf(BadRequestException.class) - .hasMessage("자신의 게시글은 신고할 수 없습니다."); + .hasMessage("본인 게시글은 신고할 수 없습니다."); } @Test @DisplayName("블라인드 처리된 투표글을 신고하는 경우 예외가 발생한다.") void reportHiddenPost() { // given - Member reporter = memberRepository.save(MemberFixtures.FEMALE_60.get()); - Member writer = memberRepository.save(MemberFixtures.FEMALE_30.get()); - - PostBody postBody = PostBody.builder() - .title("title") - .content("content") - .build(); - - Post post = postTestPersister.builder() - .writer(writer) - .postBody(postBody) - .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) - .blind() - .save(); - + Member reporter = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().save(); + post.blind(); ReportRequest request = new ReportRequest(ReportType.POST, post.getId(), "불건전한 게시글"); // when, then - assertThatThrownBy(() -> reportPostStrategy.report(reporter, request)) .isInstanceOf(BadRequestException.class) - .hasMessage("이미 블라인드 처리된 글입니다."); + .hasMessage("신고에 의해 숨겨진 게시글은 접근할 수 없습니다."); } @Test @DisplayName("하나의 회원이 투표글을 중복하여 신고하면 예외를 던진다.") void reportDuplicated() { // given - Member reporter = memberRepository.save(MemberFixtures.FEMALE_20.get()); - Member writer = memberRepository.save(MemberFixtures.FEMALE_10.get()); - - PostBody postBody = PostBody.builder() - .title("title") - .content("content") - .build(); - - Post post = postTestPersister.builder() - .writer(writer) - .postBody(postBody) - .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) - .save(); - + Member reporter = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().save(); ReportRequest request = new ReportRequest(ReportType.POST, post.getId(), "불건전한 게시글"); // when @@ -165,24 +105,12 @@ void reportDuplicated() { @DisplayName("투표글 신고가 5회가 되면 블라인드 처리가 된다.") void reportAndBlind() { // given - Member reporter1 = memberRepository.save(MemberFixtures.FEMALE_20.get()); - Member reporter2 = memberRepository.save(MemberFixtures.FEMALE_30.get()); - Member reporter3 = memberRepository.save(MemberFixtures.FEMALE_40.get()); - Member reporter4 = memberRepository.save(MemberFixtures.FEMALE_50.get()); - Member reporter5 = memberRepository.save(MemberFixtures.FEMALE_60.get()); - Member writer = memberRepository.save(MemberFixtures.FEMALE_10.get()); - - PostBody postBody = PostBody.builder() - .title("title") - .content("content") - .build(); - - Post post = postTestPersister.builder() - .writer(writer) - .postBody(postBody) - .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) - .save(); - + Member reporter1 = memberTestPersister.builder().save(); + Member reporter2 = memberTestPersister.builder().save(); + Member reporter3 = memberTestPersister.builder().save(); + Member reporter4 = memberTestPersister.builder().save(); + Member reporter5 = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().save(); ReportRequest request = new ReportRequest(ReportType.POST, post.getId(), "불건전한 게시글"); // when @@ -193,17 +121,16 @@ void reportAndBlind() { reportPostStrategy.report(reporter5, request); // then - final List responses = postService.getPostsGuest( + final List responses = postGuestService.getPosts( 0, PostClosingType.ALL, PostSortType.HOT, null ); - assertAll( () -> assertThat(post.isHidden()).isTrue(), () -> assertThat(responses).isEmpty() ); } - + } diff --git a/backend/src/test/java/com/votogether/domain/vote/repository/VoteRepositoryTest.java b/backend/src/test/java/com/votogether/domain/vote/repository/VoteRepositoryTest.java index 476e83f5c..9cc23b96b 100644 --- a/backend/src/test/java/com/votogether/domain/vote/repository/VoteRepositoryTest.java +++ b/backend/src/test/java/com/votogether/domain/vote/repository/VoteRepositoryTest.java @@ -5,294 +5,283 @@ import com.votogether.domain.member.entity.Member; import com.votogether.domain.member.entity.vo.Gender; -import com.votogether.domain.member.repository.MemberRepository; import com.votogether.domain.post.entity.Post; -import com.votogether.domain.post.entity.PostBody; import com.votogether.domain.post.entity.PostOption; -import com.votogether.domain.post.repository.PostOptionRepository; -import com.votogether.domain.post.repository.PostRepository; import com.votogether.domain.vote.entity.Vote; -import com.votogether.domain.vote.repository.dto.VoteStatus; -import com.votogether.test.annotation.RepositoryTest; -import com.votogether.test.fixtures.MemberFixtures; -import com.votogether.test.persister.MemberTestPersister; -import com.votogether.test.persister.VoteTestPersister; -import java.time.LocalDateTime; +import com.votogether.domain.vote.repository.dto.VoteCountByAgeGroupAndGenderDto; +import com.votogether.domain.vote.repository.dto.VoteCountByAgeGroupAndGenderInterface; +import com.votogether.test.RepositoryTest; import java.util.List; +import java.util.Optional; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -@RepositoryTest -class VoteRepositoryTest { - - @Autowired - MemberRepository memberRepository; - - @Autowired - PostRepository postRepository; +class VoteRepositoryTest extends RepositoryTest { @Autowired VoteRepository voteRepository; - @Autowired - PostOptionRepository postOptionRepository; - - @Autowired - VoteTestPersister voteTestPersister; - - @Autowired - MemberTestPersister memberTestPersister; - @Test - @DisplayName("투표를 저장한다.") - void save() { + @DisplayName("연령대, 성별로 그릅화하여 게시글 총 투표 수를 집계한다.") + void findPostVoteCountByAgeGroupAndGender() { // given - Member member = memberRepository.save(MemberFixtures.MALE_20.get()); - - Post post = postRepository.save( - Post.builder() - .writer(member) - .postBody(PostBody.builder().title("title").content("content").build()) - .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) - .build() - ); - PostOption postOption = postOptionRepository.save( - PostOption.builder() - .post(post) - .sequence(1) - .content("치킨") - .build() - ); - - Vote vote = Vote.builder() - .postOption(postOption) - .member(member) - .build(); + Member memberA = memberTestPersister.builder().birthYear(2014).gender(Gender.MALE).save(); // 9세 남성 + Member memberB = memberTestPersister.builder().birthYear(2013).gender(Gender.FEMALE).save(); // 10세 여성 + Member memberC = memberTestPersister.builder().birthYear(1990).gender(Gender.FEMALE).save(); // 33세 여성 + Member memberD = memberTestPersister.builder().birthYear(1986).gender(Gender.MALE).save(); // 37세 남성 + Member memberE = memberTestPersister.builder().birthYear(1963).gender(Gender.MALE).save(); // 60세 남성 + Member memberF = memberTestPersister.builder().birthYear(1951).gender(Gender.FEMALE).save(); // 72세 여성 + Member memberG = memberTestPersister.builder().birthYear(1936).gender(Gender.FEMALE).save(); // 87세 여성 + Member memberH = memberTestPersister.builder().birthYear(1924).gender(Gender.FEMALE).save(); // 99세 여성 + Post post = postTestPersister.postBuilder().save(); + PostOption postOptionA = postTestPersister.postOptionBuilder().post(post).sequence(1).save(); + PostOption postOptionB = postTestPersister.postOptionBuilder().post(post).sequence(2).save(); + voteTestPersister.builder().postOption(postOptionA).member(memberA).save(); + voteTestPersister.builder().postOption(postOptionA).member(memberB).save(); + voteTestPersister.builder().postOption(postOptionA).member(memberC).save(); + voteTestPersister.builder().postOption(postOptionA).member(memberD).save(); + voteTestPersister.builder().postOption(postOptionB).member(memberE).save(); + voteTestPersister.builder().postOption(postOptionB).member(memberF).save(); + voteTestPersister.builder().postOption(postOptionB).member(memberG).save(); + voteTestPersister.builder().postOption(postOptionB).member(memberH).save(); // when - voteRepository.save(vote); + List voteCounts = + voteRepository.findPostVoteCountByAgeGroupAndGender(post.getId()); // then - assertThat(vote.getId()).isNotNull(); + List result = voteCounts.stream() + .map(VoteCountByAgeGroupAndGenderDto::from) + .toList(); + List expected = List.of( + new VoteCountByAgeGroupAndGenderDto(0, Gender.MALE, 1), + new VoteCountByAgeGroupAndGenderDto(1, Gender.FEMALE, 1), + new VoteCountByAgeGroupAndGenderDto(3, Gender.MALE, 1), + new VoteCountByAgeGroupAndGenderDto(3, Gender.FEMALE, 1), + new VoteCountByAgeGroupAndGenderDto(6, Gender.MALE, 1), + new VoteCountByAgeGroupAndGenderDto(6, Gender.FEMALE, 3) + ); + assertThat(result).usingRecursiveComparison().isEqualTo(expected); } @Test - @DisplayName("멤버와 투표선택지를 통해 투표를 찾는다.") - void findByMemberAndPostOption() { + @DisplayName("연령대, 성별로 그릅화하여 게시글 옵션 총 투표 수를 집계한다.") + void findPostOptionVoteCountByAgeGroupAndGender() { // given - Member member = memberRepository.save(MemberFixtures.MALE_20.get()); - - Post post = postRepository.save( - Post.builder() - .writer(member) - .postBody(PostBody.builder().title("title").content("content").build()) - .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) - .build() - ); - PostOption postOption = postOptionRepository.save( - PostOption.builder() - .post(post) - .sequence(1) - .content("치킨") - .build() - ); - - Vote vote = voteRepository.save(Vote.builder() - .postOption(postOption) - .member(member) - .build()); + Member memberA = memberTestPersister.builder().birthYear(2014).gender(Gender.MALE).save(); // 9세 남성 + Member memberB = memberTestPersister.builder().birthYear(2013).gender(Gender.FEMALE).save(); // 10세 여성 + Member memberC = memberTestPersister.builder().birthYear(1990).gender(Gender.FEMALE).save(); // 33세 여성 + Member memberD = memberTestPersister.builder().birthYear(1986).gender(Gender.MALE).save(); // 37세 남성 + Post post = postTestPersister.postBuilder().save(); + PostOption postOption = postTestPersister.postOptionBuilder().post(post).sequence(1).save(); + voteTestPersister.builder().postOption(postOption).member(memberA).save(); + voteTestPersister.builder().postOption(postOption).member(memberB).save(); + voteTestPersister.builder().postOption(postOption).member(memberC).save(); + voteTestPersister.builder().postOption(postOption).member(memberD).save(); // when - Vote findVote = voteRepository.findByMemberAndPostOption(member, postOption).get(); + List voteCounts = + voteRepository.findPostOptionVoteCountByAgeGroupAndGender(postOption.getId()); // then - assertThat(findVote).isSameAs(vote); + List result = voteCounts.stream() + .map(VoteCountByAgeGroupAndGenderDto::from) + .toList(); + List expected = List.of( + new VoteCountByAgeGroupAndGenderDto(0, Gender.MALE, 1), + new VoteCountByAgeGroupAndGenderDto(1, Gender.FEMALE, 1), + new VoteCountByAgeGroupAndGenderDto(3, Gender.MALE, 1), + new VoteCountByAgeGroupAndGenderDto(3, Gender.FEMALE, 1) + ); + assertThat(result).usingRecursiveComparison().isEqualTo(expected); } - @Test - @DisplayName("멤버와 여러 투표선택지를 통해 투표를 찾는다.") - void findByMemberAndPostOptionIn() { - // given - Member member = memberRepository.save(MemberFixtures.MALE_20.get()); - - Post postA = postRepository.save( - Post.builder() - .writer(member) - .postBody(PostBody.builder().title("title").content("content").build()) - .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) - .build() - ); - PostOption postOptionA = postOptionRepository.save( - PostOption.builder() - .post(postA) - .sequence(1) - .content("치킨") - .build() - ); - Post postB = postRepository.save( - Post.builder() - .writer(member) - .postBody(PostBody.builder().title("title").content("content").build()) - .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) - .build() - ); - PostOption postOptionB = postOptionRepository.save( - PostOption.builder() - .post(postB) - .sequence(1) - .content("치킨") - .build() - ); + @Nested + @DisplayName("게시글의 회원 투표 조회") + class FindVoteByPostAndMember { - Vote voteA = Vote.builder() - .postOption(postOptionA) - .member(member) - .build(); - Vote voteB = Vote.builder() - .postOption(postOptionB) - .member(member) - .build(); - voteRepository.save(voteA); - voteRepository.save(voteB); + @Test + @DisplayName("투표가 존재하면 투표를 반환한다.") + void findVote() { + // given + Member member = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().save(); + PostOption postOption = postTestPersister.postOptionBuilder().post(post).sequence(1).save(); + voteTestPersister.builder().postOption(postOption).member(member).save(); - // when - List votes = voteRepository.findAllByMemberAndPostOptionIn(member, List.of(postOptionA, postOptionB)); + // when + Optional result = voteRepository.findByMemberAndPostOptionPost(member, post); + + // then + assertThat(result).isPresent(); + } + + @Test + @DisplayName("투표가 존재하지 않으면 빈 값을 반환한다.") + void findEmpty() { + // given + Member member = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().save(); + + // when + Optional result = voteRepository.findByMemberAndPostOptionPost(member, post); + + // then + assertThat(result).isNotPresent(); + } - // then - assertThat(votes).hasSize(2); } - @Test - @DisplayName("게시글의 연령대와 성별로 그룹화된 투표 통계를 조회한다.") - void findVoteCountByPostIdGroupByAgeRangeAndGender() { - // given - Member femaleEarly10 = memberRepository.save(MemberFixtures.FEMALE_10.get()); - Member male10 = memberRepository.save(MemberFixtures.MALE_10.get()); - Member male60 = memberRepository.save(MemberFixtures.MALE_60.get()); - Member female70 = memberRepository.save(MemberFixtures.FEMALE_70.get()); - Member female80 = memberRepository.save(MemberFixtures.FEMALE_80.get()); - Member writer = memberRepository.save(MemberFixtures.MALE_20.get()); - - Post post = postRepository.save( - Post.builder() - .writer(writer) - .postBody(PostBody.builder().title("title").content("content").build()) - .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) - .build() - ); - PostOption postOptionA = postOptionRepository.save( - PostOption.builder() - .post(post) - .sequence(1) - .content("치킨") - .build() - ); - PostOption postOptionB = postOptionRepository.save( - PostOption.builder() - .post(post) - .sequence(2) - .content("피자") - .build() - ); + @Nested + @DisplayName("게시글 옵션의 회원 투표 조회") + class FindVoteByPostOptionAndMember { + + @Test + @DisplayName("투표가 존재하면 투표를 반환한다.") + void findVote() { + // given + Member member = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().save(); + PostOption postOption = postTestPersister.postOptionBuilder().post(post).sequence(1).save(); + voteTestPersister.builder().postOption(postOption).member(member).save(); + + // when + Optional result = voteRepository.findByMemberAndPostOption(member, postOption); + + // then + assertThat(result).isPresent(); + } + + @Test + @DisplayName("투표가 존재하지 않으면 빈 값을 반환한다.") + void findEmpty() { + // given + Member member = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().save(); + PostOption postOption = postTestPersister.postOptionBuilder().post(post).sequence(1).save(); + + // when + Optional result = voteRepository.findByMemberAndPostOption(member, postOption); + + // then + assertThat(result).isNotPresent(); + } - voteRepository.save(Vote.builder().member(femaleEarly10).postOption(postOptionA).build()); - voteRepository.save(Vote.builder().member(male10).postOption(postOptionB).build()); - voteRepository.save(Vote.builder().member(male60).postOption(postOptionA).build()); - voteRepository.save(Vote.builder().member(female70).postOption(postOptionB).build()); - voteRepository.save(Vote.builder().member(female80).postOption(postOptionA).build()); + } - // when - List result = voteRepository.findVoteCountByPostIdGroupByAgeRangeAndGender(post.getId()); + @Nested + @DisplayName("게시글 옵션 목록의 회원 투표 조회") + class FindVoteByPostOptionsAndMember { + + @Test + @DisplayName("투표가 존재하면 투표를 반환한다.") + void findVote() { + // given + Member member = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().save(); + PostOption postOptionA = postTestPersister.postOptionBuilder().post(post).sequence(1).save(); + PostOption postOptionB = postTestPersister.postOptionBuilder().post(post).sequence(2).save(); + Vote vote = voteTestPersister.builder().postOption(postOptionB).member(member).save(); + + // when + List result = + voteRepository.findAllByMemberAndPostOptionIn(member, List.of(postOptionA, postOptionB)); + + // then + assertThat(result).containsExactly(vote); + } + + @Test + @DisplayName("투표가 존재하지 않으면 빈 값을 반환한다.") + void findEmpty() { + // given + Member member = memberTestPersister.builder().save(); + Post post = postTestPersister.postBuilder().save(); + PostOption postOptionA = postTestPersister.postOptionBuilder().post(post).sequence(1).save(); + PostOption postOptionB = postTestPersister.postOptionBuilder().post(post).sequence(2).save(); + + // when + List result = + voteRepository.findAllByMemberAndPostOptionIn(member, List.of(postOptionA, postOptionB)); + + // then + assertThat(result).isEmpty(); + } + + } + + @Nested + @DisplayName("회원의 모든 투표 조회") + class FindVotesByMember { + + @Test + @DisplayName("투표가 존재하면 모든 투표를 조회한다.") + void findVotes() { + // given + Member member = memberTestPersister.builder().save(); + PostOption postOptionA = postTestPersister.postOptionBuilder().sequence(1).save(); + PostOption postOptionB = postTestPersister.postOptionBuilder().sequence(1).save(); + Vote voteA = voteTestPersister.builder().postOption(postOptionA).member(member).save(); + Vote voteB = voteTestPersister.builder().postOption(postOptionB).member(member).save(); + + // when + List result = voteRepository.findAllByMember(member); + + // then + assertThat(result).containsExactly(voteA, voteB); + } + + @Test + @DisplayName("투표가 존재하지 않으면 빈 값을 반환한다.") + void findEmpty() { + // given + Member member = memberTestPersister.builder().save(); + + // when + List result = voteRepository.findAllByMember(member); + + // then + assertThat(result).isEmpty(); + } - // then - assertThat(result).containsExactly( - new VoteStatus(2005, Gender.FEMALE, 1), - new VoteStatus(2005, Gender.MALE, 1), - new VoteStatus(1955, Gender.MALE, 1), - new VoteStatus(1945, Gender.FEMALE, 1), - new VoteStatus(1935, Gender.FEMALE, 1) - ); } @Test - @DisplayName("게시글 투표 옵션의 연령대와 성별로 그룹화된 투표 통계를 조회한다.") - void findVoteCountByPostOptionIdGroupByAgeRangeAndGender() { + @DisplayName("회원의 투표 수를 조회한다.") + void countVotesByMember() { // given - Member femaleEarly10 = memberRepository.save(MemberFixtures.FEMALE_10.get()); - Member male10 = memberRepository.save(MemberFixtures.MALE_10.get()); - Member male60 = memberRepository.save(MemberFixtures.MALE_60.get()); - Member female70 = memberRepository.save(MemberFixtures.FEMALE_70.get()); - Member female80 = memberRepository.save(MemberFixtures.FEMALE_80.get()); - Member writer = memberRepository.save(MemberFixtures.MALE_20.get()); - - Post post = postRepository.save( - Post.builder() - .writer(writer) - .postBody(PostBody.builder().title("title").content("content").build()) - .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) - .build() - ); - PostOption postOption = postOptionRepository.save( - PostOption.builder() - .post(post) - .sequence(1) - .content("치킨") - .build() - ); - - voteRepository.save(Vote.builder().member(femaleEarly10).postOption(postOption).build()); - voteRepository.save(Vote.builder().member(male10).postOption(postOption).build()); - voteRepository.save(Vote.builder().member(male60).postOption(postOption).build()); - voteRepository.save(Vote.builder().member(female70).postOption(postOption).build()); - voteRepository.save(Vote.builder().member(female80).postOption(postOption).build()); + Member member = memberTestPersister.builder().save(); + PostOption postOptionA = postTestPersister.postOptionBuilder().sequence(1).save(); + PostOption postOptionB = postTestPersister.postOptionBuilder().sequence(1).save(); + voteTestPersister.builder().postOption(postOptionA).member(member).save(); + voteTestPersister.builder().postOption(postOptionB).member(member).save(); // when - List result = - voteRepository.findVoteCountByPostOptionIdGroupByAgeRangeAndGender(postOption.getId()); + int result = voteRepository.countByMember(member); // then - assertThat(result).containsExactly( - new VoteStatus(2005, Gender.FEMALE, 1), - new VoteStatus(2005, Gender.MALE, 1), - new VoteStatus(1955, Gender.MALE, 1), - new VoteStatus(1945, Gender.FEMALE, 1), - new VoteStatus(1935, Gender.FEMALE, 1) - ); + assertThat(result).isEqualTo(2); } @Test - @DisplayName("해당 회원이 투표한 개수를 반환한다.") - void countByMember() { + @DisplayName("게시글의 모든 투표를 삭제한다.") + void deleteAllWithPostIdInBatch() { // given - Member member = memberRepository.save(MemberFixtures.MALE_10.get()); - Member writer = memberRepository.save(MemberFixtures.MALE_20.get()); - Post post = postRepository.save( - Post.builder() - .writer(writer) - .postBody(PostBody.builder().title("title").content("content").build()) - .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) - .build() - ); - PostOption postOption = postOptionRepository.save( - PostOption.builder() - .post(post) - .sequence(1) - .content("치킨") - .build() - ); - Vote vote = Vote.builder() - .member(member) - .postOption(postOption) - .build(); - - voteRepository.save(vote); + Post post = postTestPersister.postBuilder().save(); + PostOption postOptionA = postTestPersister.postOptionBuilder().post(post).sequence(1).save(); + PostOption postOptionB = postTestPersister.postOptionBuilder().post(post).sequence(2).save(); + voteTestPersister.builder().postOption(postOptionA).save(); + voteTestPersister.builder().postOption(postOptionA).save(); + voteTestPersister.builder().postOption(postOptionB).save(); + voteTestPersister.builder().postOption(postOptionB).save(); // when - int numberOfVote = voteRepository.countByMember(member); + voteRepository.deleteAllWithPostOptionIdsInBatch(List.of(postOptionA.getId(), postOptionB.getId())); // then - assertThat(numberOfVote).isEqualTo(1); + assertThat(voteRepository.findAll()).isEmpty(); } @Test diff --git a/backend/src/test/java/com/votogether/infra/image/ImageNameTest.java b/backend/src/test/java/com/votogether/infra/image/ImageNameTest.java new file mode 100644 index 000000000..bb3a69b55 --- /dev/null +++ b/backend/src/test/java/com/votogether/infra/image/ImageNameTest.java @@ -0,0 +1,68 @@ +package com.votogether.infra.image; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.votogether.global.exception.ImageException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; + +class ImageNameTest { + + @Nested + @DisplayName("이미지명 생성") + class ImageNameCreate { + + @Test + @DisplayName("파일 이름이 정상적이면 이미지명을 생성한다.") + void success() { + // given + String fileName = "image.png"; + + // when + String result = ImageName.from(fileName); + + // then + assertThat(result).contains(".png"); + } + + @ParameterizedTest + @NullAndEmptySource + @DisplayName("파일 이름이 존재하지 않거나 비어있으면 예외를 던진다.") + void emptyFileName(String fileName) { + // given, when, then + assertThatThrownBy(() -> ImageName.from(fileName)) + .isInstanceOf(ImageException.class) + .hasMessage("원본 이미지명이 존재하지 않습니다."); + } + + @Test + @DisplayName("확장자가 존재하지 않으면 예외를 던진다") + void emptyExtension() { + // given + String fileName = "image"; + + // when, then + assertThatThrownBy(() -> ImageName.from(fileName)) + .isInstanceOf(ImageException.class) + .hasMessage("이미지 확장자가 존재하지 않습니다."); + } + + @Test + @DisplayName("확장자가 이미지 확장자가 아니라면 예외를 던진다.") + void invalidExtension() { + // given + String fileName = "image.txt"; + + // when, then + assertThatThrownBy(() -> ImageName.from(fileName)) + .isInstanceOf(ImageException.class) + .hasMessage("이미지 파일만 업로드할 수 있습니다."); + } + + } + +} diff --git a/backend/src/test/java/com/votogether/infra/image/LocalUploaderTest.java b/backend/src/test/java/com/votogether/infra/image/LocalUploaderTest.java new file mode 100644 index 000000000..3705b4c32 --- /dev/null +++ b/backend/src/test/java/com/votogether/infra/image/LocalUploaderTest.java @@ -0,0 +1,155 @@ +package com.votogether.infra.image; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.votogether.global.exception.ImageException; +import com.votogether.test.ServiceTest; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import javax.imageio.ImageIO; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.multipart.MultipartFile; + +class LocalUploaderTest extends ServiceTest { + + @Autowired + LocalUploader localUploader; + + @AfterEach + void tearDown( + @Value("${image.upload_directory}") String uploadDirectory + ) { + File folder = new File(uploadDirectory); + if (folder.exists()) { + deleteFileRecursive(folder); + } + } + + private static void deleteFileRecursive(File file) { + if (file.isDirectory()) { + File[] files = file.listFiles(); + if (files == null) { + return; + } + + for (File child : files) { + deleteFileRecursive(child); + } + } + file.delete(); + } + + @Nested + @DisplayName("이미지 업로드") + class ImageUpload { + + @Test + @DisplayName("정상적인 요청이라면 이미지를 업로드한다.") + void success() { + // given + MultipartFile multipartFile = mockingMultipartFile(); + + // when + String imagePath = localUploader.upload(multipartFile); + + // then + File file = new File(imagePath); + assertThat(file).exists(); + } + + @Test + @DisplayName("이미지 파일이 존재하지 않으면 null 이미지 경로를 반환한다.") + void nullImage() { + // given + MultipartFile multipartFile = null; + + // when + String imagePath = localUploader.upload(multipartFile); + + // then + assertThat(imagePath).isNull(); + } + + @Test + @DisplayName("이미지 파일이 비어있으면 null 이미지 경로를 반환한다.") + void emptyImage() { + // given + MultipartFile multipartFile = new MockMultipartFile( + "images", + "votogether.png", + MediaType.IMAGE_JPEG_VALUE, + "".getBytes() + ); + + // when + String imagePath = localUploader.upload(multipartFile); + + // then + assertThat(imagePath).isNull(); + } + + @Test + @DisplayName("이미지 파일이 아니라면 null 이미지 경로를 반환한다.") + void invalidImage() { + // given + MultipartFile multipartFile = new MockMultipartFile( + "images", + "votogether.png", + MediaType.TEXT_PLAIN_VALUE, + "hello".getBytes() + ); + + // when + String imagePath = localUploader.upload(multipartFile); + + // then + assertThat(imagePath).isNull(); + } + + } + + @Test + @DisplayName("이미지를 삭제한다.") + void deleteImage() { + // given + MultipartFile multipartFile = mockingMultipartFile(); + String imagePath = localUploader.upload(multipartFile); + + // when + localUploader.delete(imagePath); + + // then + File file = new File(imagePath); + assertThat(file).doesNotExist(); + } + + private MultipartFile mockingMultipartFile() { + return new MockMultipartFile( + "images", + "votogether.png", + MediaType.IMAGE_JPEG_VALUE, + generateMockImage() + ); + } + + private byte[] generateMockImage() { + BufferedImage image = new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB); + + try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) { + ImageIO.write(image, "jpg", byteArrayOutputStream); + return byteArrayOutputStream.toByteArray(); + } catch (IOException e) { + throw new ImageException(ImageExceptionType.IMAGE_TRANSFER); + } + } + +} diff --git a/backend/src/test/java/com/votogether/test/ControllerTest.java b/backend/src/test/java/com/votogether/test/ControllerTest.java new file mode 100644 index 000000000..a5e9d3508 --- /dev/null +++ b/backend/src/test/java/com/votogether/test/ControllerTest.java @@ -0,0 +1,30 @@ +package com.votogether.test; + +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; + +import com.votogether.domain.member.service.MemberService; +import com.votogether.global.jwt.TokenPayload; +import com.votogether.global.jwt.TokenProcessor; +import com.votogether.test.fixtures.MemberFixtures; +import org.springframework.boot.test.mock.mockito.MockBean; + +public class ControllerTest { + + protected static final String BEARER_TOKEN = "Bearer token"; + + @MockBean + TokenProcessor tokenProcessor; + + @MockBean + MemberService memberService; + + protected void mockingAuthArgumentResolver() throws Exception { + TokenPayload tokenPayload = new TokenPayload(1L, 1L, 1L); + given(tokenProcessor.resolveToken(anyString())).willReturn("token"); + given(tokenProcessor.parseToken(anyString())).willReturn(tokenPayload); + given(memberService.findById(anyLong())).willReturn(MemberFixtures.MALE_20.get()); + } + +} diff --git a/backend/src/test/java/com/votogether/test/RepositoryTest.java b/backend/src/test/java/com/votogether/test/RepositoryTest.java new file mode 100644 index 000000000..6bdc1a9dd --- /dev/null +++ b/backend/src/test/java/com/votogether/test/RepositoryTest.java @@ -0,0 +1,42 @@ +package com.votogether.test; + +import com.votogether.global.config.JpaConfig; +import com.votogether.global.config.QuerydslConfig; +import com.votogether.test.persister.CategoryTestPersister; +import com.votogether.test.persister.CommentTestPersister; +import com.votogether.test.persister.MemberTestPersister; +import com.votogether.test.persister.Persister; +import com.votogether.test.persister.PostTestPersister; +import com.votogether.test.persister.ReportTestPersister; +import com.votogether.test.persister.VoteTestPersister; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.FilterType; +import org.springframework.context.annotation.Import; + +@Import({JpaConfig.class, QuerydslConfig.class}) +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +@DataJpaTest(includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Persister.class)) +public class RepositoryTest { + + @Autowired + protected MemberTestPersister memberTestPersister; + + @Autowired + protected CategoryTestPersister categoryTestPersister; + + @Autowired + protected PostTestPersister postTestPersister; + + @Autowired + protected CommentTestPersister commentTestPersister; + + @Autowired + protected ReportTestPersister reportTestPersister; + + @Autowired + protected VoteTestPersister voteTestPersister; + +} diff --git a/backend/src/test/java/com/votogether/test/ServiceTest.java b/backend/src/test/java/com/votogether/test/ServiceTest.java new file mode 100644 index 000000000..64a3c3fd9 --- /dev/null +++ b/backend/src/test/java/com/votogether/test/ServiceTest.java @@ -0,0 +1,35 @@ +package com.votogether.test; + +import com.votogether.test.persister.CategoryTestPersister; +import com.votogether.test.persister.CommentTestPersister; +import com.votogether.test.persister.MemberTestPersister; +import com.votogether.test.persister.PostTestPersister; +import com.votogether.test.persister.ReportTestPersister; +import com.votogether.test.persister.VoteTestPersister; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; + +@Transactional +@SpringBootTest +public class ServiceTest { + + @Autowired + protected MemberTestPersister memberTestPersister; + + @Autowired + protected CategoryTestPersister categoryTestPersister; + + @Autowired + protected PostTestPersister postTestPersister; + + @Autowired + protected CommentTestPersister commentTestPersister; + + @Autowired + protected ReportTestPersister reportTestPersister; + + @Autowired + protected VoteTestPersister voteTestPersister; + +} diff --git a/backend/src/test/java/com/votogether/test/persister/CategoryTestPersister.java b/backend/src/test/java/com/votogether/test/persister/CategoryTestPersister.java new file mode 100644 index 000000000..59c1a5150 --- /dev/null +++ b/backend/src/test/java/com/votogether/test/persister/CategoryTestPersister.java @@ -0,0 +1,36 @@ +package com.votogether.test.persister; + +import com.votogether.domain.category.entity.Category; +import com.votogether.domain.category.repository.CategoryRepository; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.RandomStringUtils; + +@RequiredArgsConstructor +@Persister +public class CategoryTestPersister { + + private final CategoryRepository categoryRepository; + + public CategoryBuilder builder() { + return new CategoryBuilder(); + } + + public final class CategoryBuilder { + + private String name; + + public CategoryBuilder name(String name) { + this.name = name; + return this; + } + + public Category save() { + Category category = Category.builder() + .name(name == null ? RandomStringUtils.random(5, true, false) : name) + .build(); + return categoryRepository.save(category); + } + + } + +} diff --git a/backend/src/test/java/com/votogether/test/persister/CommentTestPersister.java b/backend/src/test/java/com/votogether/test/persister/CommentTestPersister.java index e01c3dd12..7a2961a22 100644 --- a/backend/src/test/java/com/votogether/test/persister/CommentTestPersister.java +++ b/backend/src/test/java/com/votogether/test/persister/CommentTestPersister.java @@ -3,9 +3,7 @@ import com.votogether.domain.member.entity.Member; import com.votogether.domain.post.entity.Post; import com.votogether.domain.post.entity.comment.Comment; -import com.votogether.domain.post.entity.comment.Content; import com.votogether.domain.post.repository.CommentRepository; -import com.votogether.test.fixtures.MemberFixtures; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor @@ -13,40 +11,43 @@ public class CommentTestPersister { private final CommentRepository commentRepository; + private final MemberTestPersister memberTestPersister; + private final PostTestPersister postTestPersister; public CommentBuilder builder() { - return new CommentTestPersister.CommentBuilder(); + return new CommentBuilder(); } public final class CommentBuilder { private Post post; - private Member member; - private Content content; + private Member writer; + private String content; public CommentBuilder post(Post post) { this.post = post; return this; } - public CommentBuilder member(Member member) { - this.member = member; + public CommentBuilder writer(Member writer) { + this.writer = writer; return this; } - public CommentBuilder content(Content content) { + public CommentBuilder content(String content) { this.content = content; return this; } public Comment save() { Comment comment = Comment.builder() - .post(post == null ? Post.builder().build() : post) - .member(member == null ? MemberFixtures.MALE_20.get() : member) - .content(content == null ? "content" : content.getValue()) + .post(post == null ? postTestPersister.postBuilder().save() : post) + .writer(writer == null ? memberTestPersister.builder().save() : writer) + .content(content == null ? "hello" : content) .build(); return commentRepository.save(comment); } + } } diff --git a/backend/src/test/java/com/votogether/test/persister/MemberTestPersister.java b/backend/src/test/java/com/votogether/test/persister/MemberTestPersister.java index 4e111c4bc..d446cc16e 100644 --- a/backend/src/test/java/com/votogether/test/persister/MemberTestPersister.java +++ b/backend/src/test/java/com/votogether/test/persister/MemberTestPersister.java @@ -35,8 +35,8 @@ public MemberBuilder gender(Gender gender) { return this; } - public MemberBuilder birthday(Integer birthday) { - this.birthYear = birthday; + public MemberBuilder birthYear(Integer birthYear) { + this.birthYear = birthYear; return this; } @@ -56,7 +56,7 @@ public Member save() { .gender(gender == null ? Gender.MALE : gender) .birthYear(birthYear == null ? 1995 : birthYear) .socialType(socialType == null ? SocialType.KAKAO : socialType) - .socialId(socialId == null ? "id" : socialId) + .socialId(socialId == null ? RandomStringUtils.random(10, true, true) : socialId) .build(); return memberRepository.save(member); } diff --git a/backend/src/test/java/com/votogether/test/persister/PostOptionTestPersister.java b/backend/src/test/java/com/votogether/test/persister/PostOptionTestPersister.java deleted file mode 100644 index f2eaa8347..000000000 --- a/backend/src/test/java/com/votogether/test/persister/PostOptionTestPersister.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.votogether.test.persister; - -import com.votogether.domain.post.entity.Post; -import com.votogether.domain.post.entity.PostOption; -import com.votogether.domain.post.repository.PostOptionRepository; -import lombok.RequiredArgsConstructor; -import org.apache.commons.lang3.RandomStringUtils; - -@RequiredArgsConstructor -@Persister -public class PostOptionTestPersister { - - private final PostTestPersister postTestPersister; - private final PostOptionRepository postOptionRepository; - - public PostOptionBuilder builder() { - return new PostOptionBuilder(); - } - - public final class PostOptionBuilder { - - private Post post; - private int sequence; - private String content; - private String imageUrl; - - public PostOptionBuilder post(Post post) { - this.post = post; - return this; - } - - public PostOptionBuilder sequence(int sequence) { - this.sequence = sequence; - return this; - } - - public PostOptionBuilder content(String content) { - this.content = content; - return this; - } - - public PostOptionBuilder imageUrl(String imageUrl) { - this.imageUrl = imageUrl; - return this; - } - - public PostOption save() { - PostOption postOption = PostOption.builder() - .post(post == null ? postTestPersister.builder().save() : post) - .sequence(sequence) - .content(content == null ? RandomStringUtils.random(10, true, true) : content) - .imageUrl(imageUrl) - .build(); - return postOptionRepository.save(postOption); - } - - } - -} diff --git a/backend/src/test/java/com/votogether/test/persister/PostTestPersister.java b/backend/src/test/java/com/votogether/test/persister/PostTestPersister.java index 2826bd6e9..92c67df63 100644 --- a/backend/src/test/java/com/votogether/test/persister/PostTestPersister.java +++ b/backend/src/test/java/com/votogether/test/persister/PostTestPersister.java @@ -1,29 +1,50 @@ package com.votogether.test.persister; +import com.votogether.domain.category.entity.Category; import com.votogether.domain.member.entity.Member; import com.votogether.domain.post.entity.Post; -import com.votogether.domain.post.entity.PostBody; +import com.votogether.domain.post.entity.PostCategory; +import com.votogether.domain.post.entity.PostContentImage; +import com.votogether.domain.post.entity.PostOption; +import com.votogether.domain.post.repository.PostCategoryRepository; +import com.votogether.domain.post.repository.PostContentImageRepository; +import com.votogether.domain.post.repository.PostOptionRepository; import com.votogether.domain.post.repository.PostRepository; -import com.votogether.domain.post.service.PostService; import java.time.LocalDateTime; import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; @RequiredArgsConstructor @Persister public class PostTestPersister { - private final MemberTestPersister memberTestPersister; private final PostRepository postRepository; + private final PostCategoryRepository postCategoryRepository; + private final PostContentImageRepository postContentImageRepository; + private final PostOptionRepository postOptionRepository; + private final MemberTestPersister memberTestPersister; + private final CategoryTestPersister categoryTestPersister; - public PostBuilder builder() { + public PostBuilder postBuilder() { return new PostBuilder(); } + public PostCategoryBuilder postCategoryBuilder() { + return new PostCategoryBuilder(); + } + + public PostContentImageBuilder postContentImageBuilder() { + return new PostContentImageBuilder(); + } + + public PostOptionBuilder postOptionBuilder() { + return new PostOptionBuilder(); + } + public final class PostBuilder { private Member writer; - private PostBody postBody; + private String title; + private String content; private LocalDateTime deadline; private boolean isHidden; @@ -32,8 +53,13 @@ public PostBuilder writer(Member writer) { return this; } - public PostBuilder postBody(PostBody postBody) { - this.postBody = postBody; + public PostBuilder title(String title) { + this.title = title; + return this; + } + + public PostBuilder content(String content) { + this.content = content; return this; } @@ -50,8 +76,9 @@ public PostBuilder blind() { public Post save() { Post post = Post.builder() .writer(writer == null ? memberTestPersister.builder().save() : writer) - .postBody(postBody == null ? generatePostBody() : postBody) - .deadline(deadline == null ? LocalDateTime.of(2100, 12, 25, 0, 0) : deadline) + .title(title == null ? "title" : title) + .content(content == null ? "content" : content) + .deadline(deadline == null ? LocalDateTime.now().plusDays(14) : deadline) .build(); if (isHidden) { post.blind(); @@ -60,11 +87,93 @@ public Post save() { return postRepository.save(post); } - private PostBody generatePostBody() { - return PostBody.builder() - .title("title") - .content("content") + } + + public final class PostCategoryBuilder { + + private Post post; + private Category category; + + public PostCategoryBuilder post(Post post) { + this.post = post; + return this; + } + + public PostCategoryBuilder category(Category category) { + this.category = category; + return this; + } + + public PostCategory save() { + PostCategory postCategory = PostCategory.builder() + .post(post == null ? postBuilder().save() : post) + .category(category == null ? categoryTestPersister.builder().save() : category) + .build(); + return postCategoryRepository.save(postCategory); + } + + } + + public final class PostContentImageBuilder { + + private Post post; + private String imageUrl; + + public PostContentImageBuilder post(Post post) { + this.post = post; + return this; + } + + public PostContentImageBuilder imageUrl(String imageUrl) { + this.imageUrl = imageUrl; + return this; + } + + public PostContentImage save() { + PostContentImage postContentImage = PostContentImage.builder() + .post(post == null ? postBuilder().save() : post) + .imageUrl(imageUrl == null ? "image.png" : imageUrl) + .build(); + return postContentImageRepository.save(postContentImage); + } + + } + + public final class PostOptionBuilder { + + private Post post; + private int sequence; + private String content; + private String imageUrl; + + public PostOptionBuilder post(Post post) { + this.post = post; + return this; + } + + public PostOptionBuilder sequence(int sequence) { + this.sequence = sequence; + return this; + } + + public PostOptionBuilder content(String content) { + this.content = content; + return this; + } + + public PostOptionBuilder imageUrl(String imageUrl) { + this.imageUrl = imageUrl; + return this; + } + + public PostOption save() { + PostOption postOption = PostOption.builder() + .post(post == null ? postBuilder().save() : post) + .sequence(sequence) + .content(content == null ? "content" : content) + .imageUrl(imageUrl) .build(); + return postOptionRepository.save(postOption); } } diff --git a/backend/src/test/java/com/votogether/test/persister/ReportTestPersister.java b/backend/src/test/java/com/votogether/test/persister/ReportTestPersister.java index a2b2c88a5..30c057db3 100644 --- a/backend/src/test/java/com/votogether/test/persister/ReportTestPersister.java +++ b/backend/src/test/java/com/votogether/test/persister/ReportTestPersister.java @@ -4,7 +4,6 @@ import com.votogether.domain.report.entity.Report; import com.votogether.domain.report.entity.vo.ReportType; import com.votogether.domain.report.repository.ReportRepository; -import java.time.LocalDateTime; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor @@ -15,7 +14,7 @@ public class ReportTestPersister { private final MemberTestPersister memberTestPersister; public ReportBuilder builder() { - return new ReportTestPersister.ReportBuilder(); + return new ReportBuilder(); } public final class ReportBuilder { @@ -25,22 +24,22 @@ public final class ReportBuilder { private Long targetId; private String reason; - public ReportBuilder member(final Member member) { + public ReportBuilder member(Member member) { this.member = member; return this; } - public ReportBuilder reportType(final ReportType reportType) { + public ReportBuilder reportType(ReportType reportType) { this.reportType = reportType; return this; } - public ReportBuilder targetId(final Long targetId) { + public ReportBuilder targetId(Long targetId) { this.targetId = targetId; return this; } - public ReportBuilder reason(final String reason) { + public ReportBuilder reason(String reason) { this.reason = reason; return this; } @@ -50,10 +49,11 @@ public Report save() { .member(member == null ? memberTestPersister.builder().save() : member) .reportType(reportType == null ? ReportType.POST : reportType) .targetId(targetId == null ? 1L : targetId) - .reason(reason == null ? "reason" : reason) + .reason(reason == null ? "invalid" : reason) .build(); - return reportRepository.save(report); } + } + } diff --git a/backend/src/test/java/com/votogether/test/persister/VoteTestPersister.java b/backend/src/test/java/com/votogether/test/persister/VoteTestPersister.java index 19d1af3fe..8901024e1 100644 --- a/backend/src/test/java/com/votogether/test/persister/VoteTestPersister.java +++ b/backend/src/test/java/com/votogether/test/persister/VoteTestPersister.java @@ -10,9 +10,9 @@ @Persister public class VoteTestPersister { - private final MemberTestPersister memberTestPersister; - private final PostOptionTestPersister postOptionTestPersister; private final VoteRepository voteRepository; + private final MemberTestPersister memberTestPersister; + private final PostTestPersister postTestPersister; public VoteBuilder builder() { return new VoteBuilder(); @@ -36,7 +36,7 @@ public VoteBuilder postOption(PostOption postOption) { public Vote save() { Vote vote = Vote.builder() .member(member == null ? memberTestPersister.builder().save() : member) - .postOption(postOption == null ? postOptionTestPersister.builder().save() : postOption) + .postOption(postOption == null ? postTestPersister.postOptionBuilder().save() : postOption) .build(); return voteRepository.save(vote); } diff --git a/backend/src/test/resources/application.yml b/backend/src/test/resources/application.yml index f0314148f..674d16ad2 100644 --- a/backend/src/test/resources/application.yml +++ b/backend/src/test/resources/application.yml @@ -19,8 +19,8 @@ spring: servlet: multipart: - max-file-size: 30MB - max-request-size: 30MB + max-file-size: 5MB + max-request-size: 35MB h2: console: @@ -38,6 +38,12 @@ logging: server: forward-headers-strategy: FRAMEWORK + tomcat: + max-http-form-post-size: 35MB + accept-count: 100 + max-connections: 8192 + threads: + max: 200 springdoc: swagger-ui: @@ -60,3 +66,7 @@ jwt: secret-key: abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabc access-expiration-time: 100000 refresh-expiration-time: 222222 + +image: + upload_url: ${user.dir} + upload_directory: static/images diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 6c761e1e2..efa2437e5 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -41,7 +41,6 @@ const App = () => ( - ); export default App;