diff --git a/backend/.gitignore b/backend/.gitignore index c2065bc26..b0a520940 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -35,3 +35,6 @@ out/ ### VS Code ### .vscode/ + +### local environment test files ### +src/main/generated diff --git a/backend/build.gradle b/backend/build.gradle index 9c783a287..bc67c419e 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -27,14 +27,13 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.1.0' - testImplementation 'io.rest-assured:rest-assured' - testImplementation 'io.rest-assured:spring-mock-mvc' - runtimeOnly 'com.h2database:h2' compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' + testImplementation 'io.rest-assured:rest-assured' + testImplementation 'io.rest-assured:spring-mock-mvc' testImplementation 'org.springframework.boot:spring-boot-starter-test' } diff --git a/backend/src/main/java/com/votogether/config/SwaggerBeanConfig.java b/backend/src/main/java/com/votogether/config/SwaggerBeanConfig.java index 6240795ba..2828ed5d0 100644 --- a/backend/src/main/java/com/votogether/config/SwaggerBeanConfig.java +++ b/backend/src/main/java/com/votogether/config/SwaggerBeanConfig.java @@ -8,7 +8,7 @@ @Configuration public class SwaggerBeanConfig { - + public SwaggerBeanConfig(final MappingJackson2HttpMessageConverter converter) { final List supportedMediaTypes = new ArrayList<>(converter.getSupportedMediaTypes()); supportedMediaTypes.add(new MediaType("application", "octet-stream")); 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 d21e3dc94..38de6b63e 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 @@ -8,7 +8,6 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; -import java.time.LocalDateTime; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; @@ -23,7 +22,7 @@ public class Member extends BaseEntity { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column(length = 15, nullable = false) + @Column(length = 15, unique = true, nullable = false) private String nickname; @Enumerated(value = EnumType.STRING) @@ -31,7 +30,10 @@ public class Member extends BaseEntity { private Gender gender; @Column(nullable = false) - private LocalDateTime birthDate; + private String ageRange; + + @Column(nullable = false) + private String birthday; @Enumerated(value = EnumType.STRING) @Column(length = 20, nullable = false) @@ -46,15 +48,17 @@ public class Member extends BaseEntity { @Builder private Member( final String nickname, - final LocalDateTime birthDate, final Gender gender, + final String ageRange, + final String birthday, final SocialType socialType, final String socialId, final Integer point ) { this.nickname = nickname; - this.birthDate = birthDate; this.gender = gender; + this.ageRange = ageRange; + this.birthday = birthday; this.socialType = socialType; this.socialId = socialId; this.point = point; 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 245e3a2ca..134ffd6f5 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 @@ -4,17 +4,19 @@ import com.votogether.domain.member.entity.Member; import com.votogether.domain.member.entity.SocialType; import com.votogether.domain.post.dto.request.PostCreateRequest; +import com.votogether.domain.post.dto.response.VoteOptionStatisticsResponse; import com.votogether.domain.post.service.PostService; import io.swagger.v3.oas.annotations.Operation; 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.net.URI; -import java.time.LocalDateTime; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestPart; @@ -42,17 +44,33 @@ public ResponseEntity save( ) { // TODO : 일단 돌아가게 하기 위한 member 저장 (실제 어플에선 삭제될 코드) final Member member = Member.builder() - .socialType(SocialType.GOOGLE) - .socialId("tjdtls690") .nickname("Abel") .gender(Gender.MALE) + .birthday("0718") + .ageRange("10~14") + .socialType(SocialType.GOOGLE) + .socialId("tjdtls690") .point(100) - .birthDate(LocalDateTime.now()) .build(); final Long postId = postService.save(request, member, images); return ResponseEntity.created(URI.create("/posts/" + postId)).build(); } + @Operation(summary = "게시글 투표 선택지 통계 조회", description = "게시글 특정 투표 선택지에 대한 통계를 조회한다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "게시글 투표 선택지 통계 조회 성공"), + @ApiResponse(responseCode = "400", description = "게시글 투표 옵션이 게시글에 속하지 않아 조회 실패"), + @ApiResponse(responseCode = "404", description = "존재하지 않는 게시글이거나 게시글 투표 옵션") + }) + @GetMapping(value = "/{postId}/options/{optionId}") + public ResponseEntity getVoteOptionStatistics( + @PathVariable final Long postId, + @PathVariable final Long optionId + ) { + final VoteOptionStatisticsResponse response = postService.getVoteOptionStatistics(postId, optionId); + return ResponseEntity.ok(response); + } + } diff --git a/backend/src/main/java/com/votogether/domain/post/dto/request/PostCreateRequest.java b/backend/src/main/java/com/votogether/domain/post/dto/request/PostCreateRequest.java index 1e11c8139..93cc6c972 100644 --- a/backend/src/main/java/com/votogether/domain/post/dto/request/PostCreateRequest.java +++ b/backend/src/main/java/com/votogether/domain/post/dto/request/PostCreateRequest.java @@ -8,7 +8,7 @@ @Schema(name = "게시글 관련 데이터", description = "게시글에 관련한 데이터들입니다.") @Builder -public record PostCreateRequest ( +public record PostCreateRequest( List categoryIds, String title, String content, @@ -16,6 +16,6 @@ public record PostCreateRequest ( @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm") LocalDateTime deadline -){ +) { } diff --git a/backend/src/main/java/com/votogether/domain/post/dto/response/VoteCountForAgeGroupResponse.java b/backend/src/main/java/com/votogether/domain/post/dto/response/VoteCountForAgeGroupResponse.java new file mode 100644 index 000000000..cf6e2a625 --- /dev/null +++ b/backend/src/main/java/com/votogether/domain/post/dto/response/VoteCountForAgeGroupResponse.java @@ -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 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); + } + +} diff --git a/backend/src/main/java/com/votogether/domain/post/dto/response/VoteOptionStatisticsResponse.java b/backend/src/main/java/com/votogether/domain/post/dto/response/VoteOptionStatisticsResponse.java new file mode 100644 index 000000000..5b537046e --- /dev/null +++ b/backend/src/main/java/com/votogether/domain/post/dto/response/VoteOptionStatisticsResponse.java @@ -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 ageGroup +) { + + public static VoteOptionStatisticsResponse from(final Map> voteStatusGroup) { + final List ageGroupStatistics = Arrays.stream(AgeBracket.values()) + .map(ageBracket -> { + final Map 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; + } + } + +} 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 f2d922c49..d9fdd6c19 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 @@ -13,7 +13,7 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; -import java.util.List; +import java.util.Objects; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; @@ -94,4 +94,8 @@ private static PostOption toPostOptionEntity( .build(); } + public boolean isBelongsTo(final Post post) { + return Objects.equals(this.post.getId(), post.getId()); + } + } 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 new file mode 100644 index 000000000..cb8c4fdc4 --- /dev/null +++ b/backend/src/main/java/com/votogether/domain/post/exception/PostExceptionType.java @@ -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; + } + +} 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 d43083d70..1bdd91117 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 @@ -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 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 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.ageRange()), + HashMap::new, + Collectors.groupingBy( + VoteStatus::gender, + HashMap::new, + Collectors.summingLong(VoteStatus::count) + ) + )); + } + + private String groupAgeRange(final String ageRange) { + final List teens = List.of("10~14", "15~19"); + final List 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; + } + } diff --git a/backend/src/main/java/com/votogether/domain/vote/dto/VoteStatus.java b/backend/src/main/java/com/votogether/domain/vote/dto/VoteStatus.java new file mode 100644 index 000000000..eaf4f5916 --- /dev/null +++ b/backend/src/main/java/com/votogether/domain/vote/dto/VoteStatus.java @@ -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) { +} 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 d896c8dbe..4b4ce3df1 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 @@ -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; public interface VoteRepository extends JpaRepository { + @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 findVoteCountByPostOptionIdGroupByAgeRangeAndGender(Long postOptionId); + Optional findByMemberAndPostOption(final Member member, final PostOption postOption); List findByMemberAndPostOptionIn(final Member member, final List postOptions); 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 57436f43e..5e9eaee80 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 @@ -43,7 +43,8 @@ public void vote( private void validateAlreadyVoted(final Member member, final Post post) { final PostOptions postOptions = post.getPostOptions(); - final List alreadyVoted = voteRepository.findByMemberAndPostOptionIn(member, postOptions.getPostOptions()); + final List alreadyVoted = + voteRepository.findByMemberAndPostOptionIn(member, postOptions.getPostOptions()); if (!alreadyVoted.isEmpty()) { throw new IllegalStateException("해당 게시물에는 이미 투표하였습니다."); } diff --git a/backend/src/main/java/com/votogether/exception/BadRequestException.java b/backend/src/main/java/com/votogether/exception/BadRequestException.java new file mode 100644 index 000000000..f58142bcc --- /dev/null +++ b/backend/src/main/java/com/votogether/exception/BadRequestException.java @@ -0,0 +1,9 @@ +package com.votogether.exception; + +public class BadRequestException extends BaseException { + + public BadRequestException(final ExceptionType exceptionType) { + super(exceptionType); + } + +} diff --git a/backend/src/main/java/com/votogether/exception/BaseException.java b/backend/src/main/java/com/votogether/exception/BaseException.java new file mode 100644 index 000000000..0c9754569 --- /dev/null +++ b/backend/src/main/java/com/votogether/exception/BaseException.java @@ -0,0 +1,15 @@ +package com.votogether.exception; + +import lombok.Getter; + +@Getter +public class BaseException extends RuntimeException { + + private final ExceptionType exceptionType; + + public BaseException(final ExceptionType exceptionType) { + super(exceptionType.getMessage()); + this.exceptionType = exceptionType; + } + +} diff --git a/backend/src/main/java/com/votogether/exception/ExceptionResponse.java b/backend/src/main/java/com/votogether/exception/ExceptionResponse.java new file mode 100644 index 000000000..0172aa7d8 --- /dev/null +++ b/backend/src/main/java/com/votogether/exception/ExceptionResponse.java @@ -0,0 +1,10 @@ +package com.votogether.exception; + +public record ExceptionResponse(int code, String message) { + + public static ExceptionResponse from(final BaseException e) { + final ExceptionType exceptionType = e.getExceptionType(); + return new ExceptionResponse(exceptionType.getCode(), exceptionType.getMessage()); + } + +} diff --git a/backend/src/main/java/com/votogether/exception/ExceptionType.java b/backend/src/main/java/com/votogether/exception/ExceptionType.java new file mode 100644 index 000000000..6bf70c687 --- /dev/null +++ b/backend/src/main/java/com/votogether/exception/ExceptionType.java @@ -0,0 +1,9 @@ +package com.votogether.exception; + +public interface ExceptionType { + + int getCode(); + + String getMessage(); + +} diff --git a/backend/src/main/java/com/votogether/exception/GlobalExceptionHandler.java b/backend/src/main/java/com/votogether/exception/GlobalExceptionHandler.java new file mode 100644 index 000000000..bd6241793 --- /dev/null +++ b/backend/src/main/java/com/votogether/exception/GlobalExceptionHandler.java @@ -0,0 +1,29 @@ +package com.votogether.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler + public ResponseEntity handleException(final Exception e) { + return ResponseEntity.internalServerError() + .body(new ExceptionResponse(-9999, "알 수 없는 서버 에러가 발생했습니다.")); + } + + @ExceptionHandler + public ResponseEntity handleBadRequestException(final BadRequestException e) { + return ResponseEntity.badRequest() + .body(ExceptionResponse.from(e)); + } + + @ExceptionHandler + public ResponseEntity handleNotFoundException(final NotFoundException e) { + return ResponseEntity.status(HttpStatus.NOT_FOUND) + .body(ExceptionResponse.from(e)); + } + +} diff --git a/backend/src/main/java/com/votogether/exception/NotFoundException.java b/backend/src/main/java/com/votogether/exception/NotFoundException.java new file mode 100644 index 000000000..f9b7ba599 --- /dev/null +++ b/backend/src/main/java/com/votogether/exception/NotFoundException.java @@ -0,0 +1,9 @@ +package com.votogether.exception; + +public class NotFoundException extends BaseException { + + public NotFoundException(final ExceptionType exceptionType) { + super(exceptionType); + } + +} diff --git a/backend/src/test/java/com/votogether/domain/RepositoryTest.java b/backend/src/test/java/com/votogether/RepositoryTest.java similarity index 95% rename from backend/src/test/java/com/votogether/domain/RepositoryTest.java rename to backend/src/test/java/com/votogether/RepositoryTest.java index 46d80f0c1..4e66396ed 100644 --- a/backend/src/test/java/com/votogether/domain/RepositoryTest.java +++ b/backend/src/test/java/com/votogether/RepositoryTest.java @@ -1,4 +1,4 @@ -package com.votogether.domain; +package com.votogether; import com.votogether.config.JpaConfig; import java.lang.annotation.ElementType; @@ -9,10 +9,10 @@ import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.context.annotation.Import; -@DataJpaTest -@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) -@Import(JpaConfig.class) @Target(value = ElementType.TYPE) @Retention(value = RetentionPolicy.RUNTIME) +@Import(JpaConfig.class) +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +@DataJpaTest public @interface RepositoryTest { } diff --git a/backend/src/test/java/com/votogether/ServiceTest.java b/backend/src/test/java/com/votogether/ServiceTest.java new file mode 100644 index 000000000..85c8394b3 --- /dev/null +++ b/backend/src/test/java/com/votogether/ServiceTest.java @@ -0,0 +1,15 @@ +package com.votogether; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; + +@Target(value = ElementType.TYPE) +@Retention(value = RetentionPolicy.RUNTIME) +@Transactional +@SpringBootTest +public @interface ServiceTest { +} diff --git a/backend/src/test/java/com/votogether/domain/category/repository/CategoryRepositoryTest.java b/backend/src/test/java/com/votogether/domain/category/repository/CategoryRepositoryTest.java index 9b6c14070..c9a592516 100644 --- a/backend/src/test/java/com/votogether/domain/category/repository/CategoryRepositoryTest.java +++ b/backend/src/test/java/com/votogether/domain/category/repository/CategoryRepositoryTest.java @@ -4,7 +4,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertAll; -import com.votogether.domain.RepositoryTest; +import com.votogether.RepositoryTest; import com.votogether.domain.category.entity.Category; import java.util.List; import org.junit.jupiter.api.DisplayName; diff --git a/backend/src/test/java/com/votogether/domain/category/service/CategoryServiceTest.java b/backend/src/test/java/com/votogether/domain/category/service/CategoryServiceTest.java index 7deaf4c0b..6b4ec181b 100644 --- a/backend/src/test/java/com/votogether/domain/category/service/CategoryServiceTest.java +++ b/backend/src/test/java/com/votogether/domain/category/service/CategoryServiceTest.java @@ -4,27 +4,23 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertAll; +import com.votogether.ServiceTest; import com.votogether.domain.category.dto.response.CategoryResponse; 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.entity.MemberCategory; -import com.votogether.domain.member.entity.SocialType; import com.votogether.domain.member.repository.MemberCategoryRepository; import com.votogether.domain.member.repository.MemberRepository; -import java.time.LocalDateTime; +import com.votogether.fixtures.MemberFixtures; 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; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.transaction.annotation.Transactional; -@SpringBootTest -@Transactional +@ServiceTest class CategoryServiceTest { @Autowired @@ -64,21 +60,13 @@ void getAllCategories() { @DisplayName("선호하는 카테고리를 선호 카테고리 목록에 추가한다.") void addFavoriteCategory() { // given + Member member = memberRepository.save(MemberFixtures.MALE_20); + Category category = Category.builder() .name("개발") .build(); - Member member = Member.builder() - .gender(Gender.MALE) - .point(0) - .socialType(SocialType.GOOGLE) - .nickname("user1") - .socialId("kakao@gmail.com") - .birthDate(LocalDateTime.of(1995, 7, 12, 0, 0)) - .build(); - categoryRepository.save(category); - memberRepository.save(member); Long categoryId = category.getId(); @@ -102,26 +90,19 @@ class Deleting { @DisplayName("선호하는 카테고리를 선호 카테고리 목록에 삭제한다.") void removeFavoriteCategory() { // given + Member member = memberRepository.save(MemberFixtures.MALE_20); + Category category = Category.builder() .name("개발") .build(); - Member member = Member.builder() - .gender(Gender.MALE) - .point(0) - .socialType(SocialType.GOOGLE) - .nickname("user1") - .socialId("kakao@gmail.com") - .birthDate(LocalDateTime.of(1995, 7, 12, 0, 0)) - .build(); - MemberCategory memberCategory = MemberCategory.builder() .member(member) .category(category) .build(); categoryRepository.save(category); - memberRepository.save(member); + memberCategoryRepository.save(memberCategory); Long categoryId = category.getId(); @@ -139,21 +120,13 @@ void removeFavoriteCategory() { @DisplayName("선호하는 카테고리에 없는 카테고리를 삭제하는 경우 예외가 발생한다.") void removeFavoriteCategoryException() { // given + Member member = memberRepository.save(MemberFixtures.MALE_20); + Category category = Category.builder() .name("개발") .build(); - Member member = Member.builder() - .gender(Gender.MALE) - .point(0) - .socialType(SocialType.GOOGLE) - .nickname("user1") - .socialId("kakao@gmail.com") - .birthDate(LocalDateTime.of(1995, 7, 12, 0, 0)) - .build(); - categoryRepository.save(category); - memberRepository.save(member); // when, then assertThatThrownBy(() -> categoryService.removeFavoriteCategory(member, category.getId())) diff --git a/backend/src/test/java/com/votogether/domain/member/repository/MemberCategoryRepositoryTest.java b/backend/src/test/java/com/votogether/domain/member/repository/MemberCategoryRepositoryTest.java index db1eecbc7..9a83cd1ab 100644 --- a/backend/src/test/java/com/votogether/domain/member/repository/MemberCategoryRepositoryTest.java +++ b/backend/src/test/java/com/votogether/domain/member/repository/MemberCategoryRepositoryTest.java @@ -3,14 +3,14 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; -import com.votogether.domain.RepositoryTest; +import com.votogether.RepositoryTest; 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.entity.MemberCategory; import com.votogether.domain.member.entity.SocialType; -import java.time.LocalDateTime; +import com.votogether.fixtures.MemberFixtures; import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -37,13 +37,13 @@ void save() { .build(); Member member = Member.builder() + .nickname("user1") .gender(Gender.MALE) - .point(0) + .birthday("0718") + .ageRange("10~14") .socialType(SocialType.GOOGLE) - .nickname("user1") .socialId("kakao@gmail.com") - .birthDate( - LocalDateTime.of(1995, 07, 12, 00, 00)) + .point(0) .build(); categoryRepository.save(category); @@ -71,13 +71,13 @@ void findByMemberAndCategory() { .build(); Member member = Member.builder() + .nickname("user1") .gender(Gender.MALE) - .point(0) + .birthday("0718") + .ageRange("10~14") .socialType(SocialType.GOOGLE) - .nickname("user1") .socialId("kakao@gmail.com") - .birthDate( - LocalDateTime.of(1995, 07, 12, 00, 00)) + .point(0) .build(); MemberCategory memberCategory = MemberCategory.builder() @@ -100,6 +100,8 @@ void findByMemberAndCategory() { @DisplayName("멤버를 통해 멤버 카테고리 목록을 조회힌다.") void findByMember() { // given + Member member = memberRepository.save(MemberFixtures.MALE_20); + Category category = Category.builder() .name("개발") .build(); @@ -108,16 +110,6 @@ void findByMember() { .name("음식") .build(); - Member member = Member.builder() - .gender(Gender.MALE) - .point(0) - .socialType(SocialType.GOOGLE) - .nickname("user1") - .socialId("kakao@gmail.com") - .birthDate( - LocalDateTime.of(1995, 07, 12, 00, 00)) - .build(); - MemberCategory memberCategory = MemberCategory.builder() .member(member) .category(category) @@ -130,7 +122,7 @@ void findByMember() { categoryRepository.save(category); categoryRepository.save(category1); - memberRepository.save(member); + memberCategoryRepository.save(memberCategory); memberCategoryRepository.save(memberCategory1); 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 b45ecd072..0c6d23723 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 @@ -4,10 +4,13 @@ import static org.hamcrest.Matchers.startsWith; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.BDDMockito.given; import com.fasterxml.jackson.databind.ObjectMapper; import com.votogether.domain.post.dto.request.PostCreateRequest; +import com.votogether.domain.post.dto.response.VoteCountForAgeGroupResponse; +import com.votogether.domain.post.dto.response.VoteOptionStatisticsResponse; import com.votogether.domain.post.service.PostService; import io.restassured.http.ContentType; import io.restassured.module.mockmvc.RestAssuredMockMvc; @@ -16,9 +19,7 @@ import java.io.File; import java.io.FileInputStream; import java.io.IOException; -import java.time.LocalDateTime; -import java.util.Collection; -import java.util.Collections; +import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -41,27 +42,27 @@ void setUp() { @DisplayName("게시글을 등록한다") void save() throws IOException { // given - final PostCreateRequest postCreateRequest = PostCreateRequest.builder().build(); + PostCreateRequest postCreateRequest = PostCreateRequest.builder().build(); - final String fileName1 = "testImage1.PNG"; - final String resultFileName1 = "testResultImage1.PNG"; - final String filePath1 = "src/test/resources/images/" + fileName1; + String fileName1 = "testImage1.PNG"; + String resultFileName1 = "testResultImage1.PNG"; + String filePath1 = "src/test/resources/images/" + fileName1; File file1 = new File(filePath1); - final String fileName2 = "testImage2.PNG"; - final String resultFileName2 = "testResultImage2.PNG"; - final String filePath2 = "src/test/resources/images/" + fileName2; + String fileName2 = "testImage2.PNG"; + String resultFileName2 = "testResultImage2.PNG"; + String filePath2 = "src/test/resources/images/" + fileName2; File file2 = new File(filePath2); - final ObjectMapper objectMapper = new ObjectMapper(); - final String postRequestJson = objectMapper.writeValueAsString(postCreateRequest); + ObjectMapper objectMapper = new ObjectMapper(); + String postRequestJson = objectMapper.writeValueAsString(postCreateRequest); - final long savedPostId = 1L; + long savedPostId = 1L; given(postService.save(any(), any(), anyList())).willReturn(savedPostId); // when, then - final String locationStartsWith = "/posts/"; - final ExtractableResponse response = RestAssuredMockMvc.given().log().all() + String locationStartsWith = "/posts/"; + ExtractableResponse response = RestAssuredMockMvc.given().log().all() .contentType(ContentType.MULTIPART) .multiPart("request", postRequestJson, "application/json") .multiPart("images", resultFileName1, new FileInputStream(file1), "image/png") @@ -72,8 +73,40 @@ void save() throws IOException { .header("Location", startsWith(locationStartsWith)) .extract(); - final String postId = response.header("Location").substring(locationStartsWith.length()); + String postId = response.header("Location").substring(locationStartsWith.length()); assertThat(Long.parseLong(postId)).isEqualTo(savedPostId); } + @Test + @DisplayName("게시글 투표 옵션에 대한 투표 통계를 조회한다.") + void getVoteOptionStatistics() { + // 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())).willReturn(response); + + // when + VoteOptionStatisticsResponse result = RestAssuredMockMvc.given().log().all() + .when().get("/posts/{postId}/options/{optionId}", 1, 1) + .then().log().all() + .status(HttpStatus.OK) + .extract() + .as(VoteOptionStatisticsResponse.class); + + // then + assertThat(result).usingRecursiveComparison().isEqualTo(response); + } + } 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 d961feb2a..593498bff 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 @@ -44,8 +44,6 @@ void isWriter() { .socialType(SocialType.GOOGLE) .nickname("user1") .socialId("kakao@gmail.com") - .birthDate( - LocalDateTime.of(1995, 7, 12, 0, 0)) .build(); Member member2 = Member.builder() 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 2c594a4ea..0a8021b3b 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 @@ -2,7 +2,7 @@ import static org.assertj.core.api.Assertions.assertThat; -import com.votogether.config.JpaConfig; +import com.votogether.RepositoryTest; import com.votogether.domain.member.entity.Gender; import com.votogether.domain.member.entity.Member; import com.votogether.domain.member.entity.SocialType; @@ -13,13 +13,8 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.context.annotation.Import; -@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) -@DataJpaTest -@Import(JpaConfig.class) +@RepositoryTest class PostRepositoryTest { @Autowired @@ -38,12 +33,13 @@ void save() { .build(); final Member member = Member.builder() + .nickname("user1") .gender(Gender.MALE) - .point(0) + .birthday("0718") + .ageRange("10~14") .socialType(SocialType.GOOGLE) - .nickname("user1") .socialId("kakao@gmail.com") - .birthDate(LocalDateTime.of(1993, 7, 12, 0, 0)) + .point(0) .build(); final Post post = Post.builder() 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 f81973156..457374ed8 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 @@ -1,68 +1,210 @@ package com.votogether.domain.post.service; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyCollection; -import static org.mockito.BDDMockito.given; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; +import com.votogether.ServiceTest; 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.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.repository.PostOptionRepository; import com.votogether.domain.post.repository.PostRepository; -import java.util.Collections; +import com.votogether.domain.vote.entity.Vote; +import com.votogether.domain.vote.repository.VoteRepository; +import com.votogether.exception.BadRequestException; +import com.votogether.exception.NotFoundException; +import com.votogether.fixtures.CategoryFixtures; +import com.votogether.fixtures.MemberFixtures; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +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.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.mock.web.MockMultipartFile; -@ExtendWith(MockitoExtension.class) +@ServiceTest class PostServiceTest { - @Mock + @Autowired + private PostService postService; + + @Autowired + private MemberRepository memberRepository; + + @Autowired private PostRepository postRepository; - @Mock - private CategoryRepository categoryRepository; + @Autowired + private PostOptionRepository postOptionRepository; - @Mock - private MemberRepository memberRepository; + @Autowired + private CategoryRepository categoryRepository; - @InjectMocks - private PostService postService; + @Autowired + private VoteRepository voteRepository; @Test @DisplayName("게시글을 등록한다") - void save() { + void save() throws IOException { // given - final Category category1 = Category.builder().build(); - final Category category2 = Category.builder().build(); - final List categories = List.of(category1, category2); - final Member member = Member.builder().build(); - - final Post post = Post.builder().build(); - Long fakeMemberId = 1L; - ReflectionTestUtils.setField(post, "id", fakeMemberId); + Category category1 = categoryRepository.save(CategoryFixtures.DEVELOP); + Category category2 = categoryRepository.save(CategoryFixtures.FOOD); + Member member = memberRepository.save(MemberFixtures.MALE_20); - given(categoryRepository.findAllById(anyCollection())).willReturn(categories); - given(postRepository.save(any())).willReturn(post); + MockMultipartFile file1 = new MockMultipartFile( + "image1", + "test.png", + "image/png", + new FileInputStream(new File("src/test/resources/images/testImage1.PNG")) + ); + MockMultipartFile file2 = new MockMultipartFile( + "image1", + "test.png", + "image/png", + new FileInputStream(new File("src/test/resources/images/testImage2.PNG")) + ); - final PostCreateRequest postCreateRequest = PostCreateRequest.builder() - .categoryIds(Collections.emptyList()) - .postOptionContents(Collections.emptyList()) + PostCreateRequest postCreateRequest = PostCreateRequest.builder() + .categoryIds(List.of(category1.getId(), category2.getId())) + .title("title") + .content("content") + .postOptionContents(List.of("피자", "치킨")) + .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) .build(); // when - final Long savedPostId = postService.save(postCreateRequest, member, Collections.emptyList()); + Long savedPostId = postService.save(postCreateRequest, member, List.of(file1, file2)); // then - assertThat(savedPostId).isOne(); + assertThat(savedPostId).isNotNull(); + } + + @Nested + @DisplayName("게시글 투표 옵션에 대한 투표 통계 조회 시 ") + class GetVoteOptionStatistics { + + @Test + @DisplayName("게시글이 존재하지 않으면 예외를 던진다.") + void throwExceptionNonExistPost() { + // given, when, then + assertThatThrownBy(() -> postService.getVoteOptionStatistics(-1L, 1L)) + .isInstanceOf(NotFoundException.class) + .hasMessage("해당 게시글이 존재하지 않습니다."); + } + + @Test + @DisplayName("게시글 투표 옵션이 존재하지 않으면 예외를 던진다.") + void throwExceptionNonExistOption() { + // given + Member writer = memberRepository.save(MemberFixtures.MALE_20); + + Post post = postRepository.save( + Post.builder() + .member(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)) + .isInstanceOf(NotFoundException.class) + .hasMessage("해당 게시글 투표 옵션이 존재하지 않습니다."); + } + + @Test + @DisplayName("게시글 투표 옵션이 게시글에 속하지 않으면 예외를 던진다.") + void throwExceptionNotBelongToPost() { + // given + Member writer = memberRepository.save(MemberFixtures.MALE_20); + + Post post1 = postRepository.save( + Post.builder() + .member(writer) + .postBody(PostBody.builder().title("title").content("content").build()) + .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) + .build() + ); + Post post2 = postRepository.save( + Post.builder() + .member(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())) + .isInstanceOf(BadRequestException.class) + .hasMessage("게시글 투표 옵션이 게시글과 연관되어 있지 않습니다."); + } + + @Test + @DisplayName("게시글 투표 옵션에 대한 투표 통계를 조회한다.") + void getVoteOptionStatistics() { + // given + Member femaleEarly10 = memberRepository.save(MemberFixtures.FEMALE_EARLY_10); + Member maleLate10 = memberRepository.save(MemberFixtures.MALE_LATE_10); + Member male60 = memberRepository.save(MemberFixtures.MALE_60); + Member female70 = memberRepository.save(MemberFixtures.FEMALE_70); + Member female80 = memberRepository.save(MemberFixtures.FEMALE_80); + Member writer = memberRepository.save(MemberFixtures.MALE_20); + + Post post = postRepository.save( + Post.builder() + .member(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(maleLate10).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()); + + // 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) + ); + } } } 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 ecc7a1f3b..9ba55c82e 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 @@ -2,7 +2,7 @@ import static org.assertj.core.api.Assertions.assertThat; -import com.votogether.config.JpaConfig; +import com.votogether.RepositoryTest; import com.votogether.domain.member.entity.Gender; import com.votogether.domain.member.entity.Member; import com.votogether.domain.member.entity.SocialType; @@ -12,19 +12,16 @@ 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.dto.VoteStatus; import com.votogether.domain.vote.entity.Vote; +import com.votogether.fixtures.MemberFixtures; import java.time.LocalDateTime; import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.context.annotation.Import; -@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) -@DataJpaTest -@Import(JpaConfig.class) +@RepositoryTest class VoteRepositoryTest { @Autowired @@ -40,13 +37,13 @@ class VoteRepositoryTest { PostOptionRepository postOptionRepository; Member member = Member.builder() + .nickname("user1") .gender(Gender.MALE) - .point(0) + .birthday("0718") + .ageRange("10~14") .socialType(SocialType.GOOGLE) - .nickname("user1") .socialId("kakao@gmail.com") - .birthDate( - LocalDateTime.of(1995, 7, 12, 0, 0)) + .point(0) .build(); Post post1 = Post.builder() @@ -56,17 +53,18 @@ class VoteRepositoryTest { .member(member) .build(); + PostOption postOption1 = PostOption.builder() + .post(post1) + .sequence(1) + .content("content1") + .build(); + Post post2 = Post.builder() .postBody(PostBody.builder().title("title2").content("content2").build()) .deadline( LocalDateTime.of(3023, 7, 12, 0, 0)) .member(member) .build(); - PostOption postOption1 = PostOption.builder() - .post(post1) - .sequence(1) - .content("content1") - .build(); PostOption postOption2 = PostOption.builder() .post(post2) @@ -142,4 +140,50 @@ void findByMemberAndPostOptionIn() { assertThat(votes).hasSize(2); } + @Test + @DisplayName("게시글 투표 옵션의 연령대와 성별로 그룹화된 투표 통계를 조회한다.") + void findVoteCountByPostOptionIdGroupByAgeRangeAndGender() { + // given + Member femaleEarly10 = memberRepository.save(MemberFixtures.FEMALE_EARLY_10); + Member maleLate10 = memberRepository.save(MemberFixtures.MALE_LATE_10); + Member male60 = memberRepository.save(MemberFixtures.MALE_60); + Member female70 = memberRepository.save(MemberFixtures.FEMALE_70); + Member female80 = memberRepository.save(MemberFixtures.FEMALE_80); + Member writer = memberRepository.save(MemberFixtures.MALE_20); + + Post post = postRepository.save( + Post.builder() + .member(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(maleLate10).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 + List result = + voteRepository.findVoteCountByPostOptionIdGroupByAgeRangeAndGender(postOption.getId()); + + // then + assertThat(result).containsExactly( + new VoteStatus("10~14", Gender.FEMALE, 1), + new VoteStatus("15~19", Gender.MALE, 1), + new VoteStatus("60~69", Gender.MALE, 1), + new VoteStatus("70~79", Gender.FEMALE, 1), + new VoteStatus("80~89", Gender.FEMALE, 1) + ); + } + } diff --git a/backend/src/test/java/com/votogether/fixtures/CategoryFixtures.java b/backend/src/test/java/com/votogether/fixtures/CategoryFixtures.java new file mode 100644 index 000000000..f8ae8a8ac --- /dev/null +++ b/backend/src/test/java/com/votogether/fixtures/CategoryFixtures.java @@ -0,0 +1,15 @@ +package com.votogether.fixtures; + +import com.votogether.domain.category.entity.Category; + +public class CategoryFixtures { + + public static final Category DEVELOP = Category.builder() + .name("개발") + .build(); + + public static final Category FOOD = Category.builder() + .name("음식") + .build(); + +} diff --git a/backend/src/test/java/com/votogether/fixtures/MemberFixtures.java b/backend/src/test/java/com/votogether/fixtures/MemberFixtures.java new file mode 100644 index 000000000..86bc9571a --- /dev/null +++ b/backend/src/test/java/com/votogether/fixtures/MemberFixtures.java @@ -0,0 +1,229 @@ +package com.votogether.fixtures; + +import com.votogether.domain.member.entity.Gender; +import com.votogether.domain.member.entity.Member; +import com.votogether.domain.member.entity.SocialType; + +public class MemberFixtures { + + public static final Member MALE_UNDER_10 = Member.builder() + .nickname("user1") + .gender(Gender.MALE) + .birthday("1225") + .ageRange("1~9") + .socialType(SocialType.GOOGLE) + .socialId("user1@gmail.com") + .point(0) + .build(); + + public static final Member FEMALE_UNDER_10 = Member.builder() + .nickname("user2") + .gender(Gender.FEMALE) + .birthday("1225") + .ageRange("1~9") + .socialType(SocialType.GOOGLE) + .socialId("user2@gmail.com") + .point(0) + .build(); + + public static final Member MALE_EARLY_10 = Member.builder() + .nickname("user3") + .gender(Gender.MALE) + .birthday("1225") + .ageRange("10~14") + .socialType(SocialType.GOOGLE) + .socialId("user3@gmail.com") + .point(0) + .build(); + + public static final Member FEMALE_EARLY_10 = Member.builder() + .nickname("user4") + .gender(Gender.FEMALE) + .birthday("1225") + .ageRange("10~14") + .socialType(SocialType.GOOGLE) + .socialId("user4@gmail.com") + .point(0) + .build(); + + public static final Member MALE_LATE_10 = Member.builder() + .nickname("user5") + .gender(Gender.MALE) + .birthday("1225") + .ageRange("15~19") + .socialType(SocialType.GOOGLE) + .socialId("user5@gmail.com") + .point(0) + .build(); + + public static final Member FEMALE_LATE_10 = Member.builder() + .nickname("user6") + .gender(Gender.FEMALE) + .birthday("1225") + .ageRange("15~19") + .socialType(SocialType.GOOGLE) + .socialId("user6@gmail.com") + .point(0) + .build(); + + public static final Member MALE_20 = Member.builder() + .nickname("user7") + .gender(Gender.MALE) + .birthday("1225") + .ageRange("20~29") + .socialType(SocialType.GOOGLE) + .socialId("user7@gmail.com") + .point(0) + .build(); + + public static final Member FEMALE_20 = Member.builder() + .nickname("user8") + .gender(Gender.FEMALE) + .birthday("1225") + .ageRange("20~29") + .socialType(SocialType.GOOGLE) + .socialId("user8@gmail.com") + .point(0) + .build(); + + public static final Member MALE_30 = Member.builder() + .nickname("user9") + .gender(Gender.MALE) + .birthday("1225") + .ageRange("30~39") + .socialType(SocialType.GOOGLE) + .socialId("user9@gmail.com") + .point(0) + .build(); + + public static final Member FEMALE_30 = Member.builder() + .nickname("user10") + .gender(Gender.FEMALE) + .birthday("1225") + .ageRange("30~39") + .socialType(SocialType.GOOGLE) + .socialId("user10@gmail.com") + .point(0) + .build(); + + public static final Member MALE_40 = Member.builder() + .nickname("user11") + .gender(Gender.MALE) + .birthday("1225") + .ageRange("40~49") + .socialType(SocialType.GOOGLE) + .socialId("user11@gmail.com") + .point(0) + .build(); + + public static final Member FEMALE_40 = Member.builder() + .nickname("user12") + .gender(Gender.FEMALE) + .birthday("1225") + .ageRange("40~49") + .socialType(SocialType.GOOGLE) + .socialId("user12@gmail.com") + .point(0) + .build(); + + public static final Member MALE_50 = Member.builder() + .nickname("user13") + .gender(Gender.MALE) + .birthday("1225") + .ageRange("50~59") + .socialType(SocialType.GOOGLE) + .socialId("user13@gmail.com") + .point(0) + .build(); + + public static final Member FEMALE_50 = Member.builder() + .nickname("user14") + .gender(Gender.FEMALE) + .birthday("1225") + .ageRange("50~59") + .socialType(SocialType.GOOGLE) + .socialId("user14@gmail.com") + .point(0) + .build(); + + public static final Member MALE_60 = Member.builder() + .nickname("user15") + .gender(Gender.MALE) + .birthday("1225") + .ageRange("60~69") + .socialType(SocialType.GOOGLE) + .socialId("user15@gmail.com") + .point(0) + .build(); + + public static final Member FEMALE_60 = Member.builder() + .nickname("user16") + .gender(Gender.FEMALE) + .birthday("1225") + .ageRange("60~69") + .socialType(SocialType.GOOGLE) + .socialId("user16@gmail.com") + .point(0) + .build(); + + public static final Member MALE_70 = Member.builder() + .nickname("user17") + .gender(Gender.MALE) + .birthday("1225") + .ageRange("70~79") + .socialType(SocialType.GOOGLE) + .socialId("user17@gmail.com") + .point(0) + .build(); + + public static final Member FEMALE_70 = Member.builder() + .nickname("user18") + .gender(Gender.FEMALE) + .birthday("1225") + .ageRange("70~79") + .socialType(SocialType.GOOGLE) + .socialId("user18@gmail.com") + .point(0) + .build(); + + public static final Member MALE_80 = Member.builder() + .nickname("user19") + .gender(Gender.MALE) + .birthday("1225") + .ageRange("80~89") + .socialType(SocialType.GOOGLE) + .socialId("user19@gmail.com") + .point(0) + .build(); + + public static final Member FEMALE_80 = Member.builder() + .nickname("user20") + .gender(Gender.FEMALE) + .birthday("1225") + .ageRange("80~89") + .socialType(SocialType.GOOGLE) + .socialId("user20@gmail.com") + .point(0) + .build(); + + public static final Member MALE_OVER_90 = Member.builder() + .nickname("user21") + .gender(Gender.MALE) + .birthday("1225") + .ageRange("90~") + .socialType(SocialType.GOOGLE) + .socialId("user21@gmail.com") + .point(0) + .build(); + + public static final Member FEMALE_OVER_90 = Member.builder() + .nickname("user22") + .gender(Gender.FEMALE) + .birthday("1225") + .ageRange("90~") + .socialType(SocialType.GOOGLE) + .socialId("user22@gmail.com") + .point(0) + .build(); + +}