diff --git a/server-config b/server-config index c223453..f316cac 160000 --- a/server-config +++ b/server-config @@ -1 +1 @@ -Subproject commit c223453e895a2f34714cfdd1ae5df2135edccb24 +Subproject commit f316cac4a97a3a0328f02b80109d7e6cbec43790 diff --git a/src/main/java/com/chooz/common/config/CommonConfig.java b/src/main/java/com/chooz/common/config/CommonConfig.java index d675804..420f07c 100644 --- a/src/main/java/com/chooz/common/config/CommonConfig.java +++ b/src/main/java/com/chooz/common/config/CommonConfig.java @@ -1,19 +1,29 @@ package com.chooz.common.config; +import jakarta.annotation.PostConstruct; import org.springframework.boot.context.properties.ConfigurationPropertiesScan; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.annotation.EnableScheduling; import java.time.Clock; +import java.time.LocalDateTime; +import java.util.TimeZone; @Configuration @EnableScheduling @ConfigurationPropertiesScan(basePackages = "com.chooz") public class CommonConfig { + @PostConstruct + public void init() { + TimeZone.setDefault(TimeZone.getTimeZone("Asia/Seoul")); + } + @Bean public Clock clock() { + System.out.println("LocalDateTime.now() = " + LocalDateTime.now()); return Clock.systemDefaultZone(); } + } diff --git a/src/main/java/com/chooz/common/dev/DataInitializer.java b/src/main/java/com/chooz/common/dev/DataInitializer.java index 7c88d7a..c7a72dd 100644 --- a/src/main/java/com/chooz/common/dev/DataInitializer.java +++ b/src/main/java/com/chooz/common/dev/DataInitializer.java @@ -19,6 +19,7 @@ import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; +import java.time.LocalDateTime; import java.util.List; @Profile({"!prod", "!test"}) @@ -62,6 +63,15 @@ public void init() { "shareUrl", PollOption.create(PollType.SINGLE, Scope.PUBLIC, CommentActive.OPEN), CloseOption.create(CloseType.VOTER, null, 2))); + postRepository.save(Post.create( + user.getId(), + "title", + "description", + "imageUrl", + List.of(PollChoice.create("title1", "imageUrl1"), PollChoice.create("title1", "imageUrl1")), + "shareUrl", + PollOption.create(PollType.SINGLE, Scope.PUBLIC, CommentActive.OPEN), + new CloseOption(CloseType.DATE, LocalDateTime.now().plusMinutes(5), null))); // TokenResponse tokenResponse = jwtService.createToken(new JwtClaim(testUser.getId(), testUser.getRole())); // TokenPair tokenPair = tokenResponse.tokenPair(); // System.out.println("accessToken = " + tokenPair.accessToken()); diff --git a/src/main/java/com/chooz/post/application/PostQueryService.java b/src/main/java/com/chooz/post/application/PostQueryService.java index f49ad40..41d3974 100644 --- a/src/main/java/com/chooz/post/application/PostQueryService.java +++ b/src/main/java/com/chooz/post/application/PostQueryService.java @@ -32,6 +32,7 @@ import org.springframework.transaction.annotation.Transactional; import java.util.Comparator; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; diff --git a/src/main/java/com/chooz/post/domain/CloseOption.java b/src/main/java/com/chooz/post/domain/CloseOption.java index 367468f..d1f3dcc 100644 --- a/src/main/java/com/chooz/post/domain/CloseOption.java +++ b/src/main/java/com/chooz/post/domain/CloseOption.java @@ -36,39 +36,30 @@ public CloseOption(CloseType closeType, LocalDateTime closedAt, Integer maxVoter } public static CloseOption create(CloseType closeType, LocalDateTime closedAt, Integer maxVoterCount) { - validateCloseOption(closeType, closedAt, maxVoterCount); - return new CloseOption(closeType, closedAt, maxVoterCount); - } - - private static void validateCloseOption(CloseType closeType, LocalDateTime closedAt, Integer maxVoterCount) { switch (closeType) { - case SELF -> validateSelfCloseType(closedAt, maxVoterCount); - case DATE -> validateDateCloseType(closedAt, maxVoterCount); - case VOTER -> validateVoterCloseType(closedAt, maxVoterCount); + case SELF -> { + return new CloseOption(closeType, null, null); + } + case DATE -> { + validateDateCloseType(closedAt); + return new CloseOption(closeType, closedAt, null); + } + case VOTER -> { + validateVoterCloseType(maxVoterCount); + return new CloseOption(closeType, closedAt, maxVoterCount); + } default -> throw new BadRequestException(ErrorCode.INVALID_CLOSE_OPTION); } } - private static void validateSelfCloseType(LocalDateTime closedAt, Integer maxVoterCount) { - if (Objects.nonNull(closedAt) || Objects.nonNull(maxVoterCount)) { - throw new BadRequestException(ErrorCode.INVALID_SELF_CLOSE_OPTION); - } - } - - private static void validateVoterCloseType(LocalDateTime closedAt, Integer maxVoterCount) { - if (Objects.nonNull(closedAt) || Objects.isNull(maxVoterCount)) { - throw new BadRequestException(ErrorCode.INVALID_VOTER_CLOSE_OPTION); - } - if (maxVoterCount < 1 || maxVoterCount > 999) { + private static void validateVoterCloseType(Integer maxVoterCount) { + if (Objects.isNull(maxVoterCount) || (maxVoterCount < 1 || maxVoterCount > 999)) { throw new BadRequestException(ErrorCode.INVALID_VOTER_CLOSE_OPTION); } } - private static void validateDateCloseType(LocalDateTime closedAt, Integer maxVoterCount) { - if (Objects.isNull(closedAt) || Objects.nonNull(maxVoterCount)) { - throw new BadRequestException(ErrorCode.INVALID_DATE_CLOSE_OPTION); - } - if (closedAt.isBefore(LocalDateTime.now().plusHours(1))) { + private static void validateDateCloseType(LocalDateTime closedAt) { + if (Objects.isNull(closedAt) || closedAt.isBefore(LocalDateTime.now().plusHours(1))) { throw new BadRequestException(ErrorCode.INVALID_DATE_CLOSE_OPTION); } } diff --git a/src/main/java/com/chooz/post/persistence/PostJpaRepository.java b/src/main/java/com/chooz/post/persistence/PostJpaRepository.java index 70a88f6..0c9f6a4 100644 --- a/src/main/java/com/chooz/post/persistence/PostJpaRepository.java +++ b/src/main/java/com/chooz/post/persistence/PostJpaRepository.java @@ -71,25 +71,5 @@ public interface PostJpaRepository extends JpaRepository { ) Optional findCommentActiveByPostId(@Param("postId") Long postId); - @Query(""" - select new com.chooz.post.application.dto.PostWithVoteCount( - p, - count(distinct v2.userId) - ) - from Post p - inner join Vote v on v.postId = p.id and v.userId = :userId - left join Vote v2 on v2.postId = p.id - where (:postId is null or p.id < :postId) - AND p.deleted = false - group by p - order by p.id desc - """ - ) - Slice findVotedPostsWithVoteCount( - @Param("userId") Long userId, - @Param("postId") Long postId, - Pageable pageable - ); - Optional findByIdAndUserIdAndDeletedFalse(Long postId, Long userId); } diff --git a/src/main/java/com/chooz/post/persistence/PostQueryDslRepository.java b/src/main/java/com/chooz/post/persistence/PostQueryDslRepository.java index 10c1994..9f6d6fe 100644 --- a/src/main/java/com/chooz/post/persistence/PostQueryDslRepository.java +++ b/src/main/java/com/chooz/post/persistence/PostQueryDslRepository.java @@ -77,7 +77,10 @@ public Slice findFeed(Long postId, Pageable pageable) { JPAExpressions .select(vote.userId.countDistinct()) .from(vote) - .where(vote.postId.eq(post.id)), + .where( + vote.postId.eq(post.id), + vote.deleted.isFalse() + ), JPAExpressions .select(comment.count()) .from(comment) @@ -121,7 +124,10 @@ public Slice findPostsWithVoteCountByUserId(Long userId, Long JPAExpressions .select(vote.userId.countDistinct()) .from(vote) - .where(vote.postId.eq(post.id)) + .where( + vote.postId.eq(post.id), + vote.deleted.isFalse() + ) )) .from(post) .where( @@ -153,9 +159,12 @@ public Slice findVotedPostsWithVoteCount(Long userId, Long po .select(new QPostWithVoteCount( post, JPAExpressions - .select(vote.userId.countDistinct()) + .select(vote.userId.count()) .from(vote) - .where(vote.postId.eq(post.id)) + .where( + vote.postId.eq(post.id), + vote.deleted.isFalse() + ) )) .from(post) .where( diff --git a/src/main/java/com/chooz/vote/application/RatioCalculator.java b/src/main/java/com/chooz/vote/application/RatioCalculator.java index b7f771c..f7a6b74 100644 --- a/src/main/java/com/chooz/vote/application/RatioCalculator.java +++ b/src/main/java/com/chooz/vote/application/RatioCalculator.java @@ -10,13 +10,13 @@ public class RatioCalculator { public String calculate(long totalVoteCount, long voteCount) { if (totalVoteCount == 0) { - return "0.0"; + return "0"; } BigDecimal totalCount = new BigDecimal(totalVoteCount); BigDecimal count = new BigDecimal(voteCount); - BigDecimal bigDecimal = count.divide(totalCount, 3, RoundingMode.HALF_UP) + BigDecimal bigDecimal = count.divide(totalCount, 2, RoundingMode.HALF_UP) .multiply(new BigDecimal(100)); - return String.format("%.1f", bigDecimal); + return String.valueOf(bigDecimal.intValue()); } public String calculate(int totalVoteCount, long voteCount) { diff --git a/src/main/java/com/chooz/vote/application/VoteResultReader.java b/src/main/java/com/chooz/vote/application/VoteResultReader.java index 7c6462d..d1f4c1c 100644 --- a/src/main/java/com/chooz/vote/application/VoteResultReader.java +++ b/src/main/java/com/chooz/vote/application/VoteResultReader.java @@ -23,7 +23,8 @@ public List getVoteResult(List totalVoteList, Post pos Map pollChoiceVoteCountMap = getPollChoiceVoteCountMap(totalVoteList, post); return pollChoiceVoteCountMap.entrySet().stream() .map(entry -> getVoteResultResponse(entry, totalVoteCount)) - .sorted(Comparator.comparingLong(VoteResultResponse::voteCount).reversed()) + .sorted(Comparator.comparing(VoteResultResponse::voteCount, Comparator.reverseOrder()) + .thenComparing(VoteResultResponse::id)) .toList(); } diff --git a/src/test/java/com/chooz/post/application/PostQueryServiceTest.java b/src/test/java/com/chooz/post/application/PostQueryServiceTest.java index e9c1d00..d071888 100644 --- a/src/test/java/com/chooz/post/application/PostQueryServiceTest.java +++ b/src/test/java/com/chooz/post/application/PostQueryServiceTest.java @@ -5,9 +5,12 @@ import com.chooz.common.dto.CursorBasePaginatedResponse; import com.chooz.post.domain.*; import com.chooz.post.presentation.dto.FeedResponse; +import com.chooz.post.presentation.dto.MyPagePostResponse; import com.chooz.post.presentation.dto.PollChoiceVoteResponse; import com.chooz.post.presentation.dto.PostResponse; import com.chooz.support.IntegrationTest; +import com.chooz.support.fixture.PostFixture; +import com.chooz.support.fixture.UserFixture; import com.chooz.support.fixture.VoteFixture; import com.chooz.thumbnail.domain.ThumbnailRepository; import com.chooz.user.domain.User; @@ -178,6 +181,74 @@ void findFeed() throws Exception { ); } + @Test + @DisplayName("투표 현황 조회 - 중복 투표") + void findVotedPosts_multiple() { + //given + User user = userRepository.save(UserFixture.createDefaultUser()); + Post post = postRepository.save(PostFixture.createPostBuilder() + .userId(user.getId()) + .pollOption(PostFixture.multiplePollOption()) + .build()); + //유저1 선택지 1, 2 복수 투표 + voteRepository.save(VoteFixture.createDefaultVote(user.getId(), post.getId(), post.getPollChoices().get(0).getId())); + voteRepository.save(VoteFixture.createDefaultVote(user.getId(), post.getId(), post.getPollChoices().get(1).getId())); + + //when + var response = postService.findVotedPosts(user.getId(), null, 10); + + //then + List data = response.data(); + assertAll( + () -> assertThat(response.data()).hasSize(1), + () -> assertThat(response.hasNext()).isFalse(), + + () -> assertThat(data.getFirst().id()).isEqualTo(post.getId()), + () -> assertThat(data.getFirst().title()).isEqualTo(post.getTitle()), + + () -> assertThat(data.getFirst().postVoteInfo().mostVotedPollChoice().title()).isEqualTo(post.getPollChoices().get(0).getTitle()), + () -> assertThat(data.getFirst().postVoteInfo().totalVoterCount()).isEqualTo(2), + () -> assertThat(data.getFirst().postVoteInfo().mostVotedPollChoice().voteCount()).isEqualTo(1), + () -> assertThat(data.getFirst().postVoteInfo().mostVotedPollChoice().voteRatio()).isEqualTo("50") + ); + } + + @Test + @DisplayName("투표 현황 조회 - 중복 투표2") + void findVotedPosts_multiple2() { + //given + User user = userRepository.save(UserFixture.createDefaultUser()); + User user2 = userRepository.save(UserFixture.createDefaultUser()); + Post post = postRepository.save(PostFixture.createPostBuilder() + .userId(user.getId()) + .pollOption(PostFixture.multiplePollOption()) + .build()); + //유저1 선택지 1, 2 복수 투표 + voteRepository.save(VoteFixture.createDefaultVote(user.getId(), post.getId(), post.getPollChoices().get(0).getId())); + voteRepository.save(VoteFixture.createDefaultVote(user.getId(), post.getId(), post.getPollChoices().get(1).getId())); + + //유저2 선택지 1 단일 투표 + voteRepository.save(VoteFixture.createDefaultVote(user.getId(), post.getId(), post.getPollChoices().get(0).getId())); + + //when + var response = postService.findVotedPosts(user.getId(), null, 10); + + //then + List data = response.data(); + assertAll( + () -> assertThat(response.data()).hasSize(1), + () -> assertThat(response.hasNext()).isFalse(), + + () -> assertThat(data.getFirst().id()).isEqualTo(post.getId()), + () -> assertThat(data.getFirst().title()).isEqualTo(post.getTitle()), + + () -> assertThat(data.getFirst().postVoteInfo().mostVotedPollChoice().title()).isEqualTo(post.getPollChoices().get(0).getTitle()), + () -> assertThat(data.getFirst().postVoteInfo().totalVoterCount()).isEqualTo(3), + () -> assertThat(data.getFirst().postVoteInfo().mostVotedPollChoice().voteCount()).isEqualTo(2), + () -> assertThat(data.getFirst().postVoteInfo().mostVotedPollChoice().voteRatio()).isEqualTo("67") + ); + } + private List createPosts(User user, int size) { List posts = new ArrayList<>(); for (int i = 0; i < size; i ++) { diff --git a/src/test/java/com/chooz/post/domain/CloseOptionTest.java b/src/test/java/com/chooz/post/domain/CloseOptionTest.java index dc7c4e5..0b7c4b8 100644 --- a/src/test/java/com/chooz/post/domain/CloseOptionTest.java +++ b/src/test/java/com/chooz/post/domain/CloseOptionTest.java @@ -22,40 +22,6 @@ void create() throws Exception { assertDoesNotThrow(() -> CloseOption.create(CloseType.VOTER, null, 5)); } - @Test - @DisplayName("마감 옵션 생성 실패 - null") - void createException_null() throws Exception { - // CloseType가 SELF인 경우 - assertThatThrownBy(() -> CloseOption.create(SELF, LocalDateTime.now(), null)) - .isInstanceOf(BadRequestException.class) - .hasMessage(ErrorCode.INVALID_SELF_CLOSE_OPTION.getMessage()); - assertThatThrownBy(() -> CloseOption.create(SELF, null, 2)) - .isInstanceOf(BadRequestException.class) - .hasMessage(ErrorCode.INVALID_SELF_CLOSE_OPTION.getMessage()); - - // CloseType가 DATE인 경우 - assertThatThrownBy(() -> CloseOption.create(DATE, null, null)) - .isInstanceOf(BadRequestException.class) - .hasMessage(ErrorCode.INVALID_DATE_CLOSE_OPTION.getMessage()); - assertThatThrownBy(() -> CloseOption.create(DATE, null, 2)) - .isInstanceOf(BadRequestException.class) - .hasMessage(ErrorCode.INVALID_DATE_CLOSE_OPTION.getMessage()); - assertThatThrownBy(() -> CloseOption.create(DATE, LocalDateTime.now().plusDays(1), 2)) - .isInstanceOf(BadRequestException.class) - .hasMessage(ErrorCode.INVALID_DATE_CLOSE_OPTION.getMessage()); - - // CloseType가 VOTER인 경우 - assertThatThrownBy(() -> CloseOption.create(CloseType.VOTER, null, null)) - .isInstanceOf(BadRequestException.class) - .hasMessage(ErrorCode.INVALID_VOTER_CLOSE_OPTION.getMessage()); - assertThatThrownBy(() -> CloseOption.create(CloseType.VOTER, LocalDateTime.now(), null)) - .isInstanceOf(BadRequestException.class) - .hasMessage(ErrorCode.INVALID_VOTER_CLOSE_OPTION.getMessage()); - assertThatThrownBy(() -> CloseOption.create(CloseType.VOTER, LocalDateTime.now(), 2)) - .isInstanceOf(BadRequestException.class) - .hasMessage(ErrorCode.INVALID_VOTER_CLOSE_OPTION.getMessage()); - } - @Test @DisplayName("시간 마감 옵션 생성 실패 - 마감시간이 1시간 이내인 경우") void createDateCloseOptionException() throws Exception { diff --git a/src/test/java/com/chooz/support/fixture/PostFixture.java b/src/test/java/com/chooz/support/fixture/PostFixture.java index 0b98f70..25958fd 100644 --- a/src/test/java/com/chooz/support/fixture/PostFixture.java +++ b/src/test/java/com/chooz/support/fixture/PostFixture.java @@ -73,4 +73,10 @@ public static PollOption.PollOptionBuilder createPollOptionBuilder() { .scope(Scope.PUBLIC) .commentActive(CommentActive.OPEN); } + + public static PollOption multiplePollOption() { + return createPollOptionBuilder() + .pollType(PollType.MULTIPLE) + .build(); + } } diff --git a/src/test/java/com/chooz/vote/application/RatioCalculatorTest.java b/src/test/java/com/chooz/vote/application/RatioCalculatorTest.java index 99fee09..3893e71 100644 --- a/src/test/java/com/chooz/vote/application/RatioCalculatorTest.java +++ b/src/test/java/com/chooz/vote/application/RatioCalculatorTest.java @@ -17,7 +17,7 @@ void setUp() { } @ParameterizedTest(name = "{index}: totalVoteCount={0}, voteCount={1} => result={2}") - @CsvSource({"3, 2, 66.7", "3, 1, 33.3", "4, 2, 50.0", "4, 3, 75.0", "0, 0, 0.0", "1, 0, 0.0", "1, 1, 100.0", "10, 7, 70.0", "10, 3, 30.0"}) + @CsvSource({"3, 2, 67", "3, 1, 33", "4, 2, 50", "4, 3, 75", "0, 0, 0", "1, 0, 0", "1, 1, 100", "10, 7, 70", "10, 3, 30"}) @DisplayName("비율 계산") void calculate(int totalVoteCount, int voteCount, String result) throws Exception { //given diff --git a/src/test/java/com/chooz/vote/application/VoteServiceTest.java b/src/test/java/com/chooz/vote/application/VoteServiceTest.java index df11443..b541ae3 100644 --- a/src/test/java/com/chooz/vote/application/VoteServiceTest.java +++ b/src/test/java/com/chooz/vote/application/VoteServiceTest.java @@ -303,12 +303,42 @@ void findVoteResult() { () -> assertThat(response.getFirst().id()).isEqualTo(post.getPollChoices().get(1).getId()), () -> assertThat(response.getFirst().title()).isEqualTo(post.getPollChoices().get(1).getTitle()), () -> assertThat(response.getFirst().voteCount()).isEqualTo(1), - () -> assertThat(response.getFirst().voteRatio()).isEqualTo("100.0"), + () -> assertThat(response.getFirst().voteRatio()).isEqualTo("100"), () -> assertThat(response.get(1).id()).isEqualTo(post.getPollChoices().getFirst().getId()), () -> assertThat(response.get(1).title()).isEqualTo(post.getPollChoices().getFirst().getTitle()), () -> assertThat(response.get(1).voteCount()).isEqualTo(0), - () -> assertThat(response.get(1).voteRatio()).isEqualTo("0.0") + () -> assertThat(response.get(1).voteRatio()).isEqualTo("0") + ); + } + + @Test + @DisplayName("투표 현황 조회 - 중복 투표") + void findVoteResult_multiple() { + //given + User user = userRepository.save(UserFixture.createDefaultUser()); + Post post = postRepository.save(PostFixture.createPostBuilder() + .userId(user.getId()) + .pollOption(PostFixture.multiplePollOption()) + .build()); + voteRepository.save(VoteFixture.createDefaultVote(user.getId(), post.getId(), post.getPollChoices().get(0).getId())); + voteRepository.save(VoteFixture.createDefaultVote(user.getId(), post.getId(), post.getPollChoices().get(1).getId())); + + //when + var response = voteService.findVoteResult(user.getId(), post.getId()); + + //then + assertAll( + () -> assertThat(response).hasSize(2), + () -> assertThat(response.getFirst().id()).isEqualTo(post.getPollChoices().get(0).getId()), + () -> assertThat(response.getFirst().title()).isEqualTo(post.getPollChoices().get(0).getTitle()), + () -> assertThat(response.getFirst().voteCount()).isEqualTo(1), + () -> assertThat(response.getFirst().voteRatio()).isEqualTo("50"), + + () -> assertThat(response.get(1).id()).isEqualTo(post.getPollChoices().get(1).getId()), + () -> assertThat(response.get(1).title()).isEqualTo(post.getPollChoices().get(1).getTitle()), + () -> assertThat(response.get(1).voteCount()).isEqualTo(1), + () -> assertThat(response.get(1).voteRatio()).isEqualTo("50") ); } diff --git a/src/test/java/com/chooz/vote/presentation/VoteControllerTest.java b/src/test/java/com/chooz/vote/presentation/VoteControllerTest.java index 0ed01b2..2e16f55 100644 --- a/src/test/java/com/chooz/vote/presentation/VoteControllerTest.java +++ b/src/test/java/com/chooz/vote/presentation/VoteControllerTest.java @@ -64,8 +64,8 @@ void vote() throws Exception { void findVoteResult() throws Exception { //given var response = List.of( - new VoteResultResponse(1L, "title1", "http://example.com/image/1", 2, "66.7"), - new VoteResultResponse(2L, "title2", "http://example.com/image/2", 1, "33.3") + new VoteResultResponse(1L, "title1", "http://example.com/image/1", 2, "67"), + new VoteResultResponse(2L, "title2", "http://example.com/image/2", 1, "33") ); given(voteService.findVoteResult(1L, 1L)) .willReturn(response);