-
Notifications
You must be signed in to change notification settings - Fork 4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
선택지 통계 조회 기능 구현 #83
선택지 통계 조회 기능 구현 #83
Changes from 9 commits
428293a
5eb92f2
14bbcb1
59444e1
e15c148
d5fb3e4
9e7ab3a
3a0adfd
3a81d84
8b66134
67dd311
9b54935
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -35,3 +35,6 @@ out/ | |
|
||
### VS Code ### | ||
.vscode/ | ||
|
||
### local environment test files ### | ||
src/main/generated |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
package com.votogether.domain.post.dto.response; | ||
|
||
import com.votogether.domain.member.entity.Gender; | ||
import java.util.Map; | ||
|
||
public record VoteCountForAgeGroupResponse(String ageGroup, int voteCount, int maleCount, int femaleCount) { | ||
|
||
public static VoteCountForAgeGroupResponse of(final String ageGroup, final Map<Gender, Long> 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); | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
package com.votogether.domain.post.dto.response; | ||
|
||
import com.votogether.domain.member.entity.Gender; | ||
import java.util.Arrays; | ||
import java.util.HashMap; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.stream.Collectors; | ||
import lombok.Getter; | ||
|
||
public record VoteOptionStatisticsResponse( | ||
int totalVoteCount, | ||
int totalMaleCount, | ||
int totalFemaleCount, | ||
List<VoteCountForAgeGroupResponse> ageGroup | ||
) { | ||
|
||
public static VoteOptionStatisticsResponse from(final Map<String, Map<Gender, Long>> voteStatusGroup) { | ||
final List<VoteCountForAgeGroupResponse> ageGroupStatistics = Arrays.stream(AgeBracket.values()) | ||
.map(ageBracket -> { | ||
final Map<Gender, Long> genderVotes = | ||
voteStatusGroup.getOrDefault(ageBracket.getAge(), new HashMap<>()); | ||
return VoteCountForAgeGroupResponse.of(ageBracket.getBracket(), genderVotes); | ||
}) | ||
.collect(Collectors.toList()); | ||
|
||
final int totalVoteCount = ageGroupStatistics.stream().mapToInt(VoteCountForAgeGroupResponse::voteCount).sum(); | ||
final int totalMaleCount = ageGroupStatistics.stream().mapToInt(VoteCountForAgeGroupResponse::maleCount).sum(); | ||
final int totalFemaleCount = | ||
ageGroupStatistics.stream().mapToInt(VoteCountForAgeGroupResponse::femaleCount).sum(); | ||
|
||
return new VoteOptionStatisticsResponse(totalVoteCount, totalMaleCount, totalFemaleCount, ageGroupStatistics); | ||
} | ||
|
||
@Getter | ||
enum AgeBracket { | ||
|
||
UNDER_TEN("1~9", "10대 미만"), | ||
TEN("10~19", "10대"), | ||
TWENTY("20~29", "20대"), | ||
THIRTY("30~39", "30대"), | ||
FORTY("40~49", "40대"), | ||
FIFTY("50~59", "50대"), | ||
OVER_SIXTY("60~", "60 이상"), | ||
; | ||
|
||
private final String age; | ||
private final String bracket; | ||
|
||
AgeBracket(final String age, final String bracket) { | ||
this.age = age; | ||
this.bracket = bracket; | ||
} | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
package com.votogether.domain.post.exception; | ||
|
||
import com.votogether.exception.ExceptionType; | ||
import lombok.Getter; | ||
|
||
@Getter | ||
public enum PostExceptionType implements ExceptionType { | ||
|
||
POST_NOT_FOUND(1000, "해당 게시글이 존재하지 않습니다."), | ||
POST_OPTION_NOT_FOUND(1001, "해당 게시글 투표 옵션이 존재하지 않습니다."), | ||
UNRELATED_POST_OPTION(1002, "게시글 투표 옵션이 게시글과 연관되어 있지 않습니다."), | ||
; | ||
|
||
private final int code; | ||
private final String message; | ||
|
||
PostExceptionType(final int code, final String message) { | ||
this.code = code; | ||
this.message = message; | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,27 +2,41 @@ | |
|
||
import com.votogether.domain.category.entity.Category; | ||
import com.votogether.domain.category.repository.CategoryRepository; | ||
import com.votogether.domain.member.entity.Gender; | ||
import com.votogether.domain.member.entity.Member; | ||
import com.votogether.domain.member.repository.MemberRepository; | ||
import com.votogether.domain.post.dto.request.PostCreateRequest; | ||
import com.votogether.domain.post.dto.response.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.exception.PostExceptionType; | ||
import com.votogether.domain.post.repository.PostOptionRepository; | ||
import com.votogether.domain.post.repository.PostRepository; | ||
import com.votogether.domain.vote.dto.VoteStatus; | ||
import com.votogether.domain.vote.repository.VoteRepository; | ||
import com.votogether.exception.BadRequestException; | ||
import com.votogether.exception.NotFoundException; | ||
import java.util.HashMap; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.stream.Collectors; | ||
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 PostService { | ||
|
||
private final PostRepository postRepository; | ||
private final PostOptionRepository postOptionRepository; | ||
private final CategoryRepository categoryRepository; | ||
private final MemberRepository memberRepository; | ||
private final VoteRepository voteRepository; | ||
|
||
@Transactional | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Q : 혹시 메서드가 아직 2개 뿐이어서 아직은 굳이 클래스에 붙일 필요가 없다고 생각하신건지 궁금합니다! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 저도 처음에는 클래스 상단에 하지만 해당 방법을 사용하면서 어떤 메서드는 붙이고 어떤 메서드는 붙이지 않아 정작 필요한 경우에도 어노테이션을 붙이지 않는 실수가 많이 나오게 되었습니다. 실제로 붙이지 않아서 문제 상황을 겪은 경험도 있었습니다 🥲 따라서 조금 더 주의깊게 어노테이션을 붙이고자 각 메서드를 붙이는 방법을 고려해보게 되었고, 각 메서드가 어떤 트랜잭션 설정에서 동작하는지도 명시적으로 보여줄 수 있어서 각 메서드에 붙이는 방법으로 변경해서 적용 중에 있습니다. 이 부분도 팀 프로젝트에서 맞추면 되는 컨벤션이라 생각하기에 함께 얘기해보면 좋을 것 같아요 :) |
||
public Long save( | ||
final PostCreateRequest postCreateRequest, | ||
final Member member, | ||
|
@@ -62,4 +76,46 @@ private PostBody toPostBody(final PostCreateRequest postCreateRequest) { | |
.build(); | ||
} | ||
|
||
@Transactional(readOnly = true) | ||
public VoteOptionStatisticsResponse getVoteOptionStatistics(final Long postId, final Long optionId) { | ||
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); | ||
} | ||
|
||
final List<VoteStatus> voteStatuses = | ||
voteRepository.findVoteCountByPostOptionIdGroupByAgeRangeAndGender(postOption.getId()); | ||
return VoteOptionStatisticsResponse.from(groupVoteStatus(voteStatuses)); | ||
} | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 회원가입 관련 기능이랑 합쳐지고 리팩토링 진행 예정입니다 :) |
||
private Map<String, Map<Gender, Long>> groupVoteStatus(final List<VoteStatus> voteStatuses) { | ||
return voteStatuses.stream() | ||
.collect(Collectors.groupingBy( | ||
status -> groupAgeRange(status.ageRange()), | ||
HashMap::new, | ||
Collectors.groupingBy( | ||
VoteStatus::gender, | ||
HashMap::new, | ||
Collectors.summingLong(VoteStatus::count) | ||
) | ||
)); | ||
} | ||
|
||
private String groupAgeRange(final String ageRange) { | ||
final List<String> teens = List.of("10~14", "15~19"); | ||
final List<String> over60 = List.of("60~69", "70~79", "80~89", "90~"); | ||
|
||
if (teens.contains(ageRange)) { | ||
return "10~19"; | ||
} | ||
if (over60.contains(ageRange)) { | ||
return "60~"; | ||
} | ||
return ageRange; | ||
} | ||
|
||
Comment on lines
+95
to
+120
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 그런 방법도 고려해볼 수 있을 것 같습니다! 그럼 해당 일급 컬렉션은 어떤 패키지에 속하게 되는걸까요? 🤔 |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
package com.votogether.domain.vote.dto; | ||
|
||
import com.votogether.domain.member.entity.Gender; | ||
|
||
public record VoteStatus(String ageRange, Gender gender, long count) { | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,13 +2,23 @@ | |
|
||
import com.votogether.domain.member.entity.Member; | ||
import com.votogether.domain.post.entity.PostOption; | ||
import com.votogether.domain.vote.dto.VoteStatus; | ||
import com.votogether.domain.vote.entity.Vote; | ||
import java.util.List; | ||
import java.util.Optional; | ||
import org.springframework.data.jpa.repository.JpaRepository; | ||
import org.springframework.data.jpa.repository.Query; | ||
import org.springframework.data.repository.query.Param; | ||
|
||
public interface VoteRepository extends JpaRepository<Vote, Long> { | ||
|
||
@Query("SELECT new com.votogether.domain.vote.dto.VoteStatus(m.ageRange, m.gender, COUNT(v)) " + | ||
"FROM Vote v " + | ||
"JOIN v.member m " + | ||
"WHERE v.postOption.id = :postOptionId " + | ||
"GROUP BY m.ageRange, m.gender") | ||
List<VoteStatus> findVoteCountByPostOptionIdGroupByAgeRangeAndGender(@Param("postOptionId") Long postOptionId); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Q @param의 name 속성은 파라미터 명이 일치하면 생략할 수 있는 것으로 알고 있는데, 속성을 넣어준 이유가 있나요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 아벨이 말씀하신대로 JPQL에 사용하는 값이 변수명과 동일한 경우 생략할 수 있습니다 :) |
||
|
||
Optional<Vote> findByMemberAndPostOption(final Member member, final PostOption postOption); | ||
|
||
List<Vote> findByMemberAndPostOptionIn(final Member member, final List<PostOption> postOptions); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree
: 이렇게 확인하면 Post에서 postOption을 확인하는 방법과 달리 조회 쿼리가 안나가도 되니까 더 좋은거같아요There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
그 부분을 고려하고 개발을 진행했는데 알아봐주시는군요 👍🏻