From 95b492eecf2eb20bc1833ceb4d7b082c18a58213 Mon Sep 17 00:00:00 2001 From: Jun-Hyeok Sin Date: Wed, 2 Aug 2023 11:22:10 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B2=8C=EC=8B=9C=EA=B8=80=20=EC=A1=B0?= =?UTF-8?q?=EA=B8=B0=20=EB=A7=88=EA=B0=90=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#115)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: (#95) 필요없는 테스트 클래스 삭제 * feat: (#95) 해당 게시글 조기 마감 기능 구현 * refactor: (#95) API 성공 시, swagger 표시를 201에서 200으로 수정 * refactor: (#95) swagger 500 에러 설명은 생략 * refactor: (#95) Post 클래스 마지막 줄 개행 * refactor: (#95) PostService 클래스 마지막 줄 개행 * refactor: (#95) 작성자인 경우만 조기 마감이 가능하도록 구현 * refactor: (#95) 조기 마감 할 시, 본인 게시글인지, 마감되지 않은 게시글인지, 마감 시간까지 절반 시간이 지난 것에 대한 예외처리 구현 * test: (#95) 게시글 조기 마감 시, 유효성 검증에 대한 테스트 코드 추가 * refactor: (#95) PathVariable 값인 id의 변수명을 postId로 더 명확하게 개선 * refactor: (#95) path parameter를 사용하여 테스트 코드의 url을 더 직관적으로 개선 * refactor: (#95) PostServiceTest의 코드에서 finded 단어를 found로 개선 * refactor: (#95) 조기 마감하는 메서드 명들을 더 알맞은 단어로 개선 --- .../domain/member/entity/Member.java | 1 - .../post/controller/PostController.java | 15 +++ .../votogether/domain/post/entity/Post.java | 44 +++++---- .../post/exception/PostExceptionType.java | 5 +- .../domain/post/service/PostService.java | 11 +++ .../post/controller/PostControllerTest.java | 16 +++ .../domain/post/entity/PostTest.java | 80 +++++++++++---- .../post/integrated/IntegrationTest.java | 19 ---- .../domain/post/service/PostServiceTest.java | 98 ++++++++++++++++++- 9 files changed, 227 insertions(+), 62 deletions(-) delete mode 100644 backend/src/test/java/com/votogether/domain/post/integrated/IntegrationTest.java 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 961351f46..f89d03543 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 @@ -19,7 +19,6 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) @EqualsAndHashCode(of = {"id"}) -@ToString @Getter @Entity public class Member extends BaseEntity { 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 index 1a07ef8ab..85bd24fc9 100644 --- a/backend/src/main/java/com/votogether/domain/post/controller/PostController.java +++ b/backend/src/main/java/com/votogether/domain/post/controller/PostController.java @@ -19,6 +19,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; 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.RequestMapping; @@ -98,5 +99,19 @@ public ResponseEntity getVoteOptionStatistics( return ResponseEntity.ok(response); } + @Operation(summary = "게시글 조기 마감", description = "게시글을 조기 마감한다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "게시물이 조기 마감 되었습니다."), + @ApiResponse(responseCode = "400", description = "잘못된 입력입니다.") + }) + @PatchMapping("/{postId}/close") + public ResponseEntity closePostEarly( + @PathVariable final Long postId, + @Auth final Member loginMember + ) { + postService.closePostEarlyById(postId, loginMember); + return ResponseEntity.ok().build(); + } + } 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 e18faa91b..287e3a822 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 @@ -19,6 +19,7 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.OneToMany; +import java.time.Duration; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; @@ -106,10 +107,6 @@ private List toPostOptions( .toList(); } - public boolean hasPostOption(final PostOption postOption) { - return postOptions.contains(postOption); - } - public void validateDeadlineNotExceedByMaximumDeadline(final int maximumDeadline) { LocalDateTime maximumDeadlineFromNow = LocalDateTime.now().plusDays(maximumDeadline); if (this.deadline.isAfter(maximumDeadlineFromNow)) { @@ -118,15 +115,11 @@ public void validateDeadlineNotExceedByMaximumDeadline(final int maximumDeadline } public void validateWriter(final Member member) { - if (!Objects.equals(this.writer.getId(), member.getId())) { + if (!Objects.equals(this.writer, member)) { throw new BadRequestException(PostExceptionType.NOT_WRITER); } } - public boolean isClosed() { - return deadline.isBefore(LocalDateTime.now()); - } - public Vote makeVote(final Member voter, final PostOption postOption) { validateDeadLine(); validateVoter(voter); @@ -140,26 +133,43 @@ public Vote makeVote(final Member voter, final PostOption postOption) { return vote; } + public void validateDeadLine() { + if (isClosed()) { + throw new BadRequestException(PostExceptionType.POST_CLOSED); + } + } + + private 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); } } - private void validateDeadLine() { - if (isClosed()) { - throw new IllegalStateException("게시글이 이미 마감되었습니다."); + private void validatePostOption(final PostOption postOption) { + if (!hasPostOption(postOption)) { + throw new BadRequestException(PostExceptionType.POST_OPTION_NOT_FOUND); } } - private void validatePostOption(final PostOption postOption) { - if (!hasPostOption(postOption)) { - throw new IllegalArgumentException("해당 게시글에서 존재하지 않는 선택지 입니다."); + private boolean hasPostOption(final PostOption postOption) { + return postOptions.contains(postOption); + } + + public void validateHalfDeadLine() { + final Duration betweenDuration = Duration.between(getCreatedAt(), this.deadline); + final LocalDateTime midpoint = getCreatedAt().plus(betweenDuration.dividedBy(2)); + + if (midpoint.isAfter(LocalDateTime.now())) { + throw new BadRequestException(PostExceptionType.POST_NOT_HALF_DEADLINE); } } - public boolean isWriter(final Member member) { - return Objects.equals(this.writer, member); + public void closeEarly() { + this.deadline = LocalDateTime.now(); } public void addContentImage(final String contentImageUrl) { 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 969b58627..14895fac4 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 @@ -10,9 +10,12 @@ public enum PostExceptionType implements ExceptionType { 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, "이미지 저장에 실패했습니다. 다시 시도해주세요."); + WRONG_IMAGE(1006, "이미지 저장에 실패했습니다. 다시 시도해주세요."), + ; private final int code; private final String message; 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 index 852e03af2..6293b530c 100644 --- a/backend/src/main/java/com/votogether/domain/post/service/PostService.java +++ b/backend/src/main/java/com/votogether/domain/post/service/PostService.java @@ -213,4 +213,15 @@ private String groupAgeRange(final String ageRange) { return ageRange; } + @Transactional + public void closePostEarlyById(final Long id, final Member loginMember) { + final Post post = postRepository.findById(id) + .orElseThrow(() -> new IllegalArgumentException("해당 게시글은 존재하지 않습니다.")); + + post.validateWriter(loginMember); + post.validateDeadLine(); + post.validateHalfDeadLine(); + post.closeEarly(); + } + } 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 index 86056c2b2..6d94cb919 100644 --- a/backend/src/test/java/com/votogether/domain/post/controller/PostControllerTest.java +++ b/backend/src/test/java/com/votogether/domain/post/controller/PostControllerTest.java @@ -297,4 +297,20 @@ void getVoteOptionStatistics() { assertThat(result).usingRecursiveComparison().isEqualTo(response); } + @Test + @DisplayName("게시글을 조기 마감 합니다") + void postClosedEarly() { + // given + long postId = 1L; + + // when + ExtractableResponse response = RestAssuredMockMvc.given().log().all() + .when().patch("/posts/{postId}/close", postId) + .then().log().all() + .extract(); + + // then + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); + } + } 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 77112a29d..8c8bcdf26 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,6 +1,5 @@ package com.votogether.domain.post.entity; -import static com.votogether.fixtures.MemberFixtures.MALE_30; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatNoException; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -14,6 +13,7 @@ import com.votogether.fixtures.MemberFixtures; import java.nio.charset.StandardCharsets; import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; import java.util.List; import java.util.stream.Stream; import org.junit.jupiter.api.DisplayName; @@ -103,41 +103,79 @@ void throwExceptionIsWriter() { } @Test - @DisplayName("게시글의 작성자 여부를 확인한다.") - void isWriter() { + @DisplayName("게시글의 마감 여부에 따라 예외를 던질 지 결정한다.") + void throwExceptionIsDeadlinePassed() { // given - Post post = Post.builder() - .writer(MALE_30.get()) + 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(); - // when - boolean result1 = post.isWriter(MALE_30.get()); + Post post2 = Post.builder() + .writer(writer) + .deadline(LocalDateTime.of(9999, 1, 1, 1, 1)) + .build(); - // then - assertThat(result1).isTrue(); + // when, then + assertAll( + () -> assertThatThrownBy(post1::validateDeadLine) + .isInstanceOf(BadRequestException.class) + .hasMessage(PostExceptionType.POST_CLOSED.getMessage()), + () -> assertThatNoException() + .isThrownBy(post2::validateDeadLine) + ); } @Test - @DisplayName("게시글의 마감 여부를 확인한다.") - void isClosed() { + @DisplayName("게시글의 마감까지 절반의 시간을 넘겼는 지에 따라 예외를 던질 지 결정한다.") + void throwExceptionIsHalfToTheDeadline() { // given - Post postA = Post.builder() - .deadline(LocalDateTime.of(2022, 1, 1, 0, 0)) + final Member writer = MemberFixtures.MALE_30.get(); + ReflectionTestUtils.setField(writer, "id", 1L); + + Post post1 = Post.builder() + .writer(writer) + .deadline(LocalDateTime.of(9999, 1, 1, 1, 1)) .build(); + ReflectionTestUtils.setField(post1, "createdAt", LocalDateTime.now()); + + Post post2 = Post.builder() + .writer(writer) + .deadline(LocalDateTime.now().plus(100, ChronoUnit.MILLIS)) + .build(); + ReflectionTestUtils.setField(post2, "createdAt", LocalDateTime.now()); + + // when, then + assertAll( + () -> assertThatThrownBy(post1::validateHalfDeadLine) + .isInstanceOf(BadRequestException.class) + .hasMessage(PostExceptionType.POST_NOT_HALF_DEADLINE.getMessage()), + () -> { + Thread.sleep(50); + assertThatNoException() + .isThrownBy(post2::validateHalfDeadLine); - Post postB = Post.builder() - .deadline(LocalDateTime.of(3222, 1, 1, 0, 0)) + } + ); + } + + @Test + @DisplayName("해당 게시글을 조기 마감 합니다.") + void closedEarly() { + // given + LocalDateTime deadline = LocalDateTime.of(2100, 1, 1, 0, 0); + Post post = Post.builder() + .deadline(deadline) .build(); // when - boolean resultA = postA.isClosed(); - boolean resultB = postB.isClosed(); + post.closeEarly(); // then - assertAll( - () -> assertThat(resultA).isTrue(), - () -> assertThat(resultB).isFalse() - ); + assertThat(post.getDeadline()).isBefore(deadline); } } diff --git a/backend/src/test/java/com/votogether/domain/post/integrated/IntegrationTest.java b/backend/src/test/java/com/votogether/domain/post/integrated/IntegrationTest.java deleted file mode 100644 index 00c59cb31..000000000 --- a/backend/src/test/java/com/votogether/domain/post/integrated/IntegrationTest.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.votogether.domain.post.integrated; - -import io.restassured.RestAssured; -import org.junit.jupiter.api.BeforeEach; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.web.server.LocalServerPort; - -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -public class IntegrationTest { - - @LocalServerPort - int port; - - @BeforeEach - public void setUp() { - RestAssured.port = port; - } - -} 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 index effedc8a5..33b14a3af 100644 --- a/backend/src/test/java/com/votogether/domain/post/service/PostServiceTest.java +++ b/backend/src/test/java/com/votogether/domain/post/service/PostServiceTest.java @@ -19,8 +19,8 @@ import com.votogether.domain.member.entity.SocialType; import com.votogether.domain.member.repository.MemberCategoryRepository; import com.votogether.domain.member.repository.MemberRepository; -import com.votogether.domain.post.dto.request.PostOptionCreateRequest; import com.votogether.domain.post.dto.request.PostCreateRequest; +import com.votogether.domain.post.dto.request.PostOptionCreateRequest; import com.votogether.domain.post.dto.response.PostResponse; import com.votogether.domain.post.dto.response.VoteOptionStatisticsResponse; import com.votogether.domain.post.entity.Post; @@ -41,6 +41,7 @@ import java.io.FileInputStream; import java.io.IOException; import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.List; import org.junit.jupiter.api.DisplayName; @@ -365,6 +366,99 @@ void getVoteOptionStatistics() { } + @Test + @DisplayName("해당 게시글을 조기 마감 합니다") + void postClosedEarlyById() throws InterruptedException { + // given + Member writer = memberRepository.save(MemberFixtures.MALE_30.get()); + LocalDateTime oldDeadline = LocalDateTime.now().plus(100, ChronoUnit.MILLIS); + 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(); + Thread.sleep(50); + + // 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 throwExceptionHalfDeadLinePostClosedEarly() { + // given + Member writer = memberRepository.save(MemberFixtures.MALE_30.get()); + LocalDateTime oldDeadline = LocalDateTime.now().plusMinutes(1); + 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() { @@ -424,8 +518,6 @@ void getAllPostBySortTypeAndClosingType() { } }; - - List optionImages = new ArrayList<>() { { add(file1);