diff --git a/backend/build.gradle b/backend/build.gradle index 28b3abd3..2e2428d8 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -53,20 +53,12 @@ ext { set('snippetsDir', file("build/generated-snippets")) } -task copySubmodule(type: Copy) { - copy { - from 'security' - include "*.yml" - into 'src/main/resources' - } -} - tasks.named('test') { outputs.dir snippetsDir useJUnitPlatform() } -tasks.named('asciidoctor') { +asciidoctor { configurations 'asciidoctorExtensions' inputs.dir snippetsDir sources { @@ -76,13 +68,18 @@ tasks.named('asciidoctor') { dependsOn test } -task copyDocument(type: Copy) { - dependsOn asciidoctor - - from file("${asciidoctor.outputDir}") - into file("src/main/resources/static") +task copySubmodule(type: Copy) { + copy { + from 'security' + include "*.yml" + into 'src/main/resources' + } } bootJar { - dependsOn copyDocument + dependsOn asciidoctor + copy { + from file("${asciidoctor.outputDir}") + into file("src/main/resources/static") + } } diff --git a/backend/src/docs/asciidoc/products.adoc b/backend/src/docs/asciidoc/products.adoc index b01142e2..7ec7fe51 100644 --- a/backend/src/docs/asciidoc/products.adoc +++ b/backend/src/docs/asciidoc/products.adoc @@ -23,3 +23,7 @@ operation::products-get[snippets='http-request,http-response'] - 없을 경우 등록 순서 operation::products-page-get[snippets='http-request,http-response'] + +=== 특정 제품의 사용자 통계를 조회 + +operation::products-member-statistics-get[snippets='http-request,http-response'] diff --git a/backend/src/main/java/com/woowacourse/f12/application/product/ProductService.java b/backend/src/main/java/com/woowacourse/f12/application/product/ProductService.java index c1df4d3a..3f0dc4cc 100644 --- a/backend/src/main/java/com/woowacourse/f12/application/product/ProductService.java +++ b/backend/src/main/java/com/woowacourse/f12/application/product/ProductService.java @@ -1,12 +1,21 @@ package com.woowacourse.f12.application.product; +import com.woowacourse.f12.domain.member.CareerLevel; +import com.woowacourse.f12.domain.member.JobType; import com.woowacourse.f12.domain.product.Category; import com.woowacourse.f12.domain.product.Product; import com.woowacourse.f12.domain.product.ProductRepository; +import com.woowacourse.f12.domain.review.CareerLevelCount; +import com.woowacourse.f12.domain.review.JobTypeCount; +import com.woowacourse.f12.domain.review.MemberInfoStatistics; +import com.woowacourse.f12.domain.review.ReviewRepository; import com.woowacourse.f12.dto.response.product.ProductPageResponse; import com.woowacourse.f12.dto.response.product.ProductResponse; -import com.woowacourse.f12.exception.notfound.KeyboardNotFoundException; +import com.woowacourse.f12.dto.response.product.ProductStatisticsResponse; +import com.woowacourse.f12.exception.notfound.ProductNotFoundException; import com.woowacourse.f12.presentation.product.CategoryConstant; +import java.util.List; +import java.util.Map; import java.util.Objects; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; @@ -17,14 +26,16 @@ public class ProductService { private final ProductRepository productRepository; + private final ReviewRepository reviewRepository; - public ProductService(final ProductRepository productRepository) { + public ProductService(final ProductRepository productRepository, final ReviewRepository reviewRepository) { this.productRepository = productRepository; + this.reviewRepository = reviewRepository; } public ProductResponse findById(final Long id) { final Product product = productRepository.findById(id) - .orElseThrow(KeyboardNotFoundException::new); + .orElseThrow(ProductNotFoundException::new); return ProductResponse.from(product); } @@ -36,4 +47,27 @@ public ProductPageResponse findPage(final CategoryConstant categoryConstant, fin final Category category = categoryConstant.toCategory(); return ProductPageResponse.from(productRepository.findPageByCategory(category, pageable)); } + + public ProductStatisticsResponse calculateMemberStatisticsById(final Long productId) { + if (!productRepository.existsById(productId)) { + throw new ProductNotFoundException(); + } + final Map careerLevel = calculateWithCareerLevel(productId); + final Map jobType = calculateWithJobType(productId); + + return ProductStatisticsResponse.of(careerLevel, jobType); + } + + private Map calculateWithJobType(final Long productId) { + final List jobTypeCounts = reviewRepository.findJobTypeCountByProductId(productId); + final MemberInfoStatistics jobTypeStatistics = new MemberInfoStatistics<>(jobTypeCounts); + return jobTypeStatistics.calculateStatistics(JobType.values()); + } + + private Map calculateWithCareerLevel(final Long productId) { + final List careerLevelCounts = reviewRepository.findCareerLevelCountByProductId(productId); + final MemberInfoStatistics careerLevelStatistics = new MemberInfoStatistics<>( + careerLevelCounts); + return careerLevelStatistics.calculateStatistics(CareerLevel.values()); + } } diff --git a/backend/src/main/java/com/woowacourse/f12/application/review/ReviewService.java b/backend/src/main/java/com/woowacourse/f12/application/review/ReviewService.java index 6797d0f8..e65b4f7b 100644 --- a/backend/src/main/java/com/woowacourse/f12/application/review/ReviewService.java +++ b/backend/src/main/java/com/woowacourse/f12/application/review/ReviewService.java @@ -13,8 +13,8 @@ import com.woowacourse.f12.dto.response.review.ReviewWithProductPageResponse; import com.woowacourse.f12.exception.badrequest.AlreadyWrittenReviewException; import com.woowacourse.f12.exception.forbidden.NotAuthorException; -import com.woowacourse.f12.exception.notfound.KeyboardNotFoundException; import com.woowacourse.f12.exception.notfound.MemberNotFoundException; +import com.woowacourse.f12.exception.notfound.ProductNotFoundException; import com.woowacourse.f12.exception.notfound.ReviewNotFoundException; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; @@ -45,7 +45,7 @@ public Long saveReviewAndInventoryProduct(final Long productId, final Long membe final Member member = memberRepository.findById(memberId) .orElseThrow(MemberNotFoundException::new); final Product product = productRepository.findById(productId) - .orElseThrow(KeyboardNotFoundException::new); + .orElseThrow(ProductNotFoundException::new); final Long reviewId = saveReview(reviewRequest, member, product); saveInventoryProduct(member, product); return reviewId; @@ -83,7 +83,7 @@ public ReviewPageResponse findPageByProductId(final Long productId, final Pageab private void validateKeyboardExists(final Long productId) { if (!productRepository.existsById(productId)) { - throw new KeyboardNotFoundException(); + throw new ProductNotFoundException(); } } diff --git a/backend/src/main/java/com/woowacourse/f12/domain/member/CareerLevel.java b/backend/src/main/java/com/woowacourse/f12/domain/member/CareerLevel.java index 65bef14c..136872dc 100644 --- a/backend/src/main/java/com/woowacourse/f12/domain/member/CareerLevel.java +++ b/backend/src/main/java/com/woowacourse/f12/domain/member/CareerLevel.java @@ -1,6 +1,6 @@ package com.woowacourse.f12.domain.member; -public enum CareerLevel { +public enum CareerLevel implements MemberInfo { NONE, JUNIOR, diff --git a/backend/src/main/java/com/woowacourse/f12/domain/member/JobType.java b/backend/src/main/java/com/woowacourse/f12/domain/member/JobType.java index 8b2cff9d..111160fd 100644 --- a/backend/src/main/java/com/woowacourse/f12/domain/member/JobType.java +++ b/backend/src/main/java/com/woowacourse/f12/domain/member/JobType.java @@ -3,11 +3,10 @@ import lombok.Getter; @Getter -public enum JobType { +public enum JobType implements MemberInfo { FRONTEND, BACKEND, MOBILE, - ETC - ; + ETC; } diff --git a/backend/src/main/java/com/woowacourse/f12/domain/member/MemberInfo.java b/backend/src/main/java/com/woowacourse/f12/domain/member/MemberInfo.java new file mode 100644 index 00000000..8e5b6ffc --- /dev/null +++ b/backend/src/main/java/com/woowacourse/f12/domain/member/MemberInfo.java @@ -0,0 +1,5 @@ +package com.woowacourse.f12.domain.member; + +public interface MemberInfo { + +} diff --git a/backend/src/main/java/com/woowacourse/f12/domain/review/CareerLevelCount.java b/backend/src/main/java/com/woowacourse/f12/domain/review/CareerLevelCount.java new file mode 100644 index 00000000..54f807db --- /dev/null +++ b/backend/src/main/java/com/woowacourse/f12/domain/review/CareerLevelCount.java @@ -0,0 +1,26 @@ +package com.woowacourse.f12.domain.review; + +import com.querydsl.core.annotations.QueryProjection; +import com.woowacourse.f12.domain.member.CareerLevel; + +public class CareerLevelCount implements Countable { + + private final CareerLevel careerLevel; + private final long count; + + @QueryProjection + public CareerLevelCount(final CareerLevel careerLevel, final long count) { + this.careerLevel = careerLevel; + this.count = count; + } + + @Override + public Enum getValue() { + return careerLevel; + } + + @Override + public long getCount() { + return count; + } +} diff --git a/backend/src/main/java/com/woowacourse/f12/domain/review/Countable.java b/backend/src/main/java/com/woowacourse/f12/domain/review/Countable.java new file mode 100644 index 00000000..7f4416c1 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/f12/domain/review/Countable.java @@ -0,0 +1,8 @@ +package com.woowacourse.f12.domain.review; + +public interface Countable { + + Enum getValue(); + + long getCount(); +} diff --git a/backend/src/main/java/com/woowacourse/f12/domain/review/JobTypeCount.java b/backend/src/main/java/com/woowacourse/f12/domain/review/JobTypeCount.java new file mode 100644 index 00000000..42dd19c3 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/f12/domain/review/JobTypeCount.java @@ -0,0 +1,26 @@ +package com.woowacourse.f12.domain.review; + +import com.querydsl.core.annotations.QueryProjection; +import com.woowacourse.f12.domain.member.JobType; + +public class JobTypeCount implements Countable { + + private final JobType jobType; + private final long count; + + @QueryProjection + public JobTypeCount(final JobType jobType, final long count) { + this.jobType = jobType; + this.count = count; + } + + @Override + public Enum getValue() { + return jobType; + } + + @Override + public long getCount() { + return count; + } +} diff --git a/backend/src/main/java/com/woowacourse/f12/domain/review/MemberInfoStatistics.java b/backend/src/main/java/com/woowacourse/f12/domain/review/MemberInfoStatistics.java new file mode 100644 index 00000000..934eed8a --- /dev/null +++ b/backend/src/main/java/com/woowacourse/f12/domain/review/MemberInfoStatistics.java @@ -0,0 +1,46 @@ +package com.woowacourse.f12.domain.review; + +import com.woowacourse.f12.domain.member.MemberInfo; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class MemberInfoStatistics { + + private static final int DECIMAL_PLACE = 2; + private static final double DEFAULT_VALUE = 0.00; + private static final double DECIMAL = 10.0; + private final List elements; + + public MemberInfoStatistics(final List elements) { + this.elements = elements; + } + + public Map calculateStatistics(final T[] values) { + final long totalCount = calculateTotalCount(); + return Arrays.stream(values) + .collect(Collectors.toMap(Function.identity(), + memberInfo -> calculateProportion(memberInfo, totalCount))); + } + + private long calculateTotalCount() { + return elements.stream() + .mapToLong(Countable::getCount) + .sum(); + } + + private Double calculateProportion(final T t, final long totalCount) { + return elements.stream() + .filter(it -> it.getValue().equals(t)) + .findAny() + .map(countable -> round(countable.getCount() / (double) totalCount)) + .orElse(DEFAULT_VALUE); + } + + private double round(double number) { + final double operand = Math.pow(DECIMAL, DECIMAL_PLACE); + return Math.round(number * operand) / operand; + } +} diff --git a/backend/src/main/java/com/woowacourse/f12/domain/review/ReviewRepository.java b/backend/src/main/java/com/woowacourse/f12/domain/review/ReviewRepository.java index f532c63f..0ce444c4 100644 --- a/backend/src/main/java/com/woowacourse/f12/domain/review/ReviewRepository.java +++ b/backend/src/main/java/com/woowacourse/f12/domain/review/ReviewRepository.java @@ -7,7 +7,7 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; -public interface ReviewRepository extends JpaRepository { +public interface ReviewRepository extends JpaRepository, ReviewRepositoryCustom { @Query("select r from Review r where r.product.id = :productId") Slice findPageByProductId(Long productId, Pageable pageable); diff --git a/backend/src/main/java/com/woowacourse/f12/domain/review/ReviewRepositoryCustom.java b/backend/src/main/java/com/woowacourse/f12/domain/review/ReviewRepositoryCustom.java new file mode 100644 index 00000000..d4b5948d --- /dev/null +++ b/backend/src/main/java/com/woowacourse/f12/domain/review/ReviewRepositoryCustom.java @@ -0,0 +1,10 @@ +package com.woowacourse.f12.domain.review; + +import java.util.List; + +public interface ReviewRepositoryCustom { + + List findCareerLevelCountByProductId(Long productId); + + List findJobTypeCountByProductId(Long productId); +} diff --git a/backend/src/main/java/com/woowacourse/f12/domain/review/ReviewRepositoryCustomImpl.java b/backend/src/main/java/com/woowacourse/f12/domain/review/ReviewRepositoryCustomImpl.java new file mode 100644 index 00000000..b280eca1 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/f12/domain/review/ReviewRepositoryCustomImpl.java @@ -0,0 +1,46 @@ +package com.woowacourse.f12.domain.review; + +import com.querydsl.jpa.impl.JPAQuery; +import com.querydsl.jpa.impl.JPAQueryFactory; +import com.woowacourse.f12.domain.member.QMember; +import com.woowacourse.f12.domain.product.Product; +import com.woowacourse.f12.domain.product.QProduct; +import java.util.List; +import org.springframework.data.jpa.repository.support.QuerydslRepositorySupport; + +public class ReviewRepositoryCustomImpl extends QuerydslRepositorySupport implements ReviewRepositoryCustom { + + private final QProduct product = QProduct.product; + private final QReview review = QReview.review; + private final QMember member = QMember.member; + private final JPAQueryFactory jpaQueryFactory; + + public ReviewRepositoryCustomImpl(final JPAQueryFactory jpaQueryFactory) { + super(Product.class); + this.jpaQueryFactory = jpaQueryFactory; + } + + @Override + public List findCareerLevelCountByProductId(final Long productId) { + final JPAQuery jpaQuery = jpaQueryFactory.from(review) + .innerJoin(review.member, member) + .where(review.product.id.eq(productId)) + .groupBy(member.careerLevel) + .select( + new QCareerLevelCount(member.careerLevel, member.id.count()) + ); + return jpaQuery.fetch(); + } + + @Override + public List findJobTypeCountByProductId(final Long productId) { + final JPAQuery jpaQuery = jpaQueryFactory.from(review) + .innerJoin(review.member, member) + .where(review.product.id.eq(productId)) + .groupBy(member.jobType) + .select( + new QJobTypeCount(member.jobType, member.id.count()) + ); + return jpaQuery.fetch(); + } +} diff --git a/backend/src/main/java/com/woowacourse/f12/dto/response/product/ProductStatisticsResponse.java b/backend/src/main/java/com/woowacourse/f12/dto/response/product/ProductStatisticsResponse.java new file mode 100644 index 00000000..a283cdee --- /dev/null +++ b/backend/src/main/java/com/woowacourse/f12/dto/response/product/ProductStatisticsResponse.java @@ -0,0 +1,37 @@ +package com.woowacourse.f12.dto.response.product; + +import com.woowacourse.f12.domain.member.CareerLevel; +import com.woowacourse.f12.domain.member.JobType; +import com.woowacourse.f12.dto.CareerLevelConstant; +import com.woowacourse.f12.dto.JobTypeConstant; +import java.util.Map; +import java.util.Map.Entry; +import java.util.stream.Collectors; +import lombok.Getter; + +@Getter +public class ProductStatisticsResponse { + + private Map careerLevel; + private Map jobType; + + private ProductStatisticsResponse() { + } + + private ProductStatisticsResponse(final Map careerLevel, + final Map jobType) { + this.careerLevel = careerLevel; + this.jobType = jobType; + } + + public static ProductStatisticsResponse of(final Map enumCareerLevel, + final Map enumJobType) { + final Map careerLevel = enumCareerLevel.entrySet() + .stream() + .collect(Collectors.toMap(entry -> CareerLevelConstant.from(entry.getKey()), Entry::getValue)); + final Map jobType = enumJobType.entrySet() + .stream() + .collect(Collectors.toMap(entry -> JobTypeConstant.from(entry.getKey()), Entry::getValue)); + return new ProductStatisticsResponse(careerLevel, jobType); + } +} diff --git a/backend/src/main/java/com/woowacourse/f12/exception/notfound/KeyboardNotFoundException.java b/backend/src/main/java/com/woowacourse/f12/exception/notfound/KeyboardNotFoundException.java deleted file mode 100644 index ba10aa49..00000000 --- a/backend/src/main/java/com/woowacourse/f12/exception/notfound/KeyboardNotFoundException.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.woowacourse.f12.exception.notfound; - -public class KeyboardNotFoundException extends NotFoundException { - - public KeyboardNotFoundException() { - super("키보드를 찾을 수 없습니다."); - } -} diff --git a/backend/src/main/java/com/woowacourse/f12/exception/notfound/ProductNotFoundException.java b/backend/src/main/java/com/woowacourse/f12/exception/notfound/ProductNotFoundException.java new file mode 100644 index 00000000..b2a83f30 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/f12/exception/notfound/ProductNotFoundException.java @@ -0,0 +1,8 @@ +package com.woowacourse.f12.exception.notfound; + +public class ProductNotFoundException extends NotFoundException { + + public ProductNotFoundException() { + super("제품을 찾을 수 없습니다."); + } +} diff --git a/backend/src/main/java/com/woowacourse/f12/presentation/product/ProductController.java b/backend/src/main/java/com/woowacourse/f12/presentation/product/ProductController.java index a8f01473..73190c40 100644 --- a/backend/src/main/java/com/woowacourse/f12/presentation/product/ProductController.java +++ b/backend/src/main/java/com/woowacourse/f12/presentation/product/ProductController.java @@ -3,6 +3,7 @@ import com.woowacourse.f12.application.product.ProductService; import com.woowacourse.f12.dto.response.product.ProductPageResponse; import com.woowacourse.f12.dto.response.product.ProductResponse; +import com.woowacourse.f12.dto.response.product.ProductStatisticsResponse; import org.springframework.data.domain.Pageable; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -31,4 +32,9 @@ public ResponseEntity showPage(@RequestParam(required = fal public ResponseEntity show(@PathVariable final Long id) { return ResponseEntity.ok().body(productService.findById(id)); } + + @GetMapping("/{id}/statistics") + public ResponseEntity showStatistics(@PathVariable final Long id) { + return ResponseEntity.ok().body(productService.calculateMemberStatisticsById(id)); + } } diff --git a/backend/src/test/java/com/woowacourse/f12/acceptance/ProductAcceptanceTest.java b/backend/src/test/java/com/woowacourse/f12/acceptance/ProductAcceptanceTest.java index 80117248..47cf9c79 100644 --- a/backend/src/test/java/com/woowacourse/f12/acceptance/ProductAcceptanceTest.java +++ b/backend/src/test/java/com/woowacourse/f12/acceptance/ProductAcceptanceTest.java @@ -2,8 +2,18 @@ import static com.woowacourse.f12.acceptance.support.LoginUtil.로그인을_한다; import static com.woowacourse.f12.acceptance.support.RestAssuredRequestUtil.GET_요청을_보낸다; +import static com.woowacourse.f12.acceptance.support.RestAssuredRequestUtil.로그인된_상태로_PATCH_요청을_보낸다; +import static com.woowacourse.f12.dto.CareerLevelConstant.JUNIOR_CONSTANT; +import static com.woowacourse.f12.dto.CareerLevelConstant.MID_LEVEL_CONSTANT; +import static com.woowacourse.f12.dto.CareerLevelConstant.NONE_CONSTANT; +import static com.woowacourse.f12.dto.CareerLevelConstant.SENIOR_CONSTANT; +import static com.woowacourse.f12.dto.JobTypeConstant.BACKEND_CONSTANT; +import static com.woowacourse.f12.dto.JobTypeConstant.ETC_CONSTANT; +import static com.woowacourse.f12.dto.JobTypeConstant.FRONTEND_CONSTANT; +import static com.woowacourse.f12.dto.JobTypeConstant.MOBILE_CONSTANT; import static com.woowacourse.f12.presentation.product.CategoryConstant.KEYBOARD_CONSTANT; import static com.woowacourse.f12.support.GitHubProfileFixtures.CORINNE_GITHUB; +import static com.woowacourse.f12.support.GitHubProfileFixtures.MINCHO_GITHUB; import static com.woowacourse.f12.support.ProductFixture.KEYBOARD_1; import static com.woowacourse.f12.support.ProductFixture.KEYBOARD_2; import static com.woowacourse.f12.support.ProductFixture.MOUSE_1; @@ -15,11 +25,15 @@ import com.woowacourse.f12.domain.product.Product; import com.woowacourse.f12.domain.product.ProductRepository; +import com.woowacourse.f12.dto.request.member.MemberRequest; +import com.woowacourse.f12.dto.response.auth.LoginResponse; import com.woowacourse.f12.dto.response.product.ProductPageResponse; import com.woowacourse.f12.dto.response.product.ProductResponse; +import com.woowacourse.f12.dto.response.product.ProductStatisticsResponse; import com.woowacourse.f12.presentation.product.CategoryConstant; import io.restassured.response.ExtractableResponse; import io.restassured.response.Response; +import java.util.Map; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; @@ -134,6 +148,39 @@ class ProductAcceptanceTest extends AcceptanceTest { ); } + @Test + void 제품의_사용자_통계를_조회한다() { + // given + Product product = 제품을_저장한다(KEYBOARD_1.생성()); + + LoginResponse firstLoginResponse = 로그인을_한다(MINCHO_GITHUB.getCode()); + String firstToken = firstLoginResponse.getToken(); + MemberRequest firstMemberRequest = new MemberRequest(SENIOR_CONSTANT, BACKEND_CONSTANT); + 로그인된_상태로_PATCH_요청을_보낸다("/api/v1/members/me", firstToken, firstMemberRequest); + REVIEW_RATING_5.작성_요청을_보낸다(product.getId(), firstToken); + + LoginResponse secondLoginResponse = 로그인을_한다(CORINNE_GITHUB.getCode()); + String secondToken = secondLoginResponse.getToken(); + MemberRequest secondMemberRequest = new MemberRequest(JUNIOR_CONSTANT, FRONTEND_CONSTANT); + 로그인된_상태로_PATCH_요청을_보낸다("/api/v1/members/me", secondToken, secondMemberRequest); + REVIEW_RATING_5.작성_요청을_보낸다(product.getId(), secondToken); + + // when + ExtractableResponse response = GET_요청을_보낸다("/api/v1/products/" + product.getId() + "/statistics"); + ProductStatisticsResponse productStatisticsResponse = response.as(ProductStatisticsResponse.class); + + // then + assertAll( + () -> assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()), + () -> assertThat(productStatisticsResponse.getCareerLevel()).usingRecursiveComparison() + .isEqualTo(Map.of(NONE_CONSTANT, 0.0, JUNIOR_CONSTANT, 0.5, MID_LEVEL_CONSTANT, 0.0, + SENIOR_CONSTANT, 0.5)), + () -> assertThat(productStatisticsResponse.getJobType()).usingRecursiveComparison() + .isEqualTo(Map.of(FRONTEND_CONSTANT, 0.5, BACKEND_CONSTANT, 0.5, MOBILE_CONSTANT, 0.0, + ETC_CONSTANT, 0.0)) + ); + } + private Product 제품을_저장한다(Product product) { return productRepository.save(product); } diff --git a/backend/src/test/java/com/woowacourse/f12/application/product/ProductServiceTest.java b/backend/src/test/java/com/woowacourse/f12/application/product/ProductServiceTest.java index e36ae2b0..c6f18489 100644 --- a/backend/src/test/java/com/woowacourse/f12/application/product/ProductServiceTest.java +++ b/backend/src/test/java/com/woowacourse/f12/application/product/ProductServiceTest.java @@ -1,5 +1,17 @@ package com.woowacourse.f12.application.product; +import static com.woowacourse.f12.domain.member.CareerLevel.JUNIOR; +import static com.woowacourse.f12.domain.member.CareerLevel.MID_LEVEL; +import static com.woowacourse.f12.domain.member.JobType.BACKEND; +import static com.woowacourse.f12.domain.member.JobType.ETC; +import static com.woowacourse.f12.dto.CareerLevelConstant.JUNIOR_CONSTANT; +import static com.woowacourse.f12.dto.CareerLevelConstant.MID_LEVEL_CONSTANT; +import static com.woowacourse.f12.dto.CareerLevelConstant.NONE_CONSTANT; +import static com.woowacourse.f12.dto.CareerLevelConstant.SENIOR_CONSTANT; +import static com.woowacourse.f12.dto.JobTypeConstant.BACKEND_CONSTANT; +import static com.woowacourse.f12.dto.JobTypeConstant.ETC_CONSTANT; +import static com.woowacourse.f12.dto.JobTypeConstant.FRONTEND_CONSTANT; +import static com.woowacourse.f12.dto.JobTypeConstant.MOBILE_CONSTANT; import static com.woowacourse.f12.support.ProductFixture.KEYBOARD_1; import static com.woowacourse.f12.support.ProductFixture.MOUSE_1; import static org.assertj.core.api.Assertions.assertThat; @@ -14,11 +26,16 @@ import com.woowacourse.f12.domain.product.Category; import com.woowacourse.f12.domain.product.Product; import com.woowacourse.f12.domain.product.ProductRepository; +import com.woowacourse.f12.domain.review.CareerLevelCount; +import com.woowacourse.f12.domain.review.JobTypeCount; +import com.woowacourse.f12.domain.review.ReviewRepository; import com.woowacourse.f12.dto.response.product.ProductPageResponse; import com.woowacourse.f12.dto.response.product.ProductResponse; -import com.woowacourse.f12.exception.notfound.KeyboardNotFoundException; +import com.woowacourse.f12.dto.response.product.ProductStatisticsResponse; +import com.woowacourse.f12.exception.notfound.ProductNotFoundException; import com.woowacourse.f12.presentation.product.CategoryConstant; import java.util.List; +import java.util.Map; import java.util.Optional; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -35,6 +52,9 @@ class ProductServiceTest { @Mock private ProductRepository productRepository; + @Mock + private ReviewRepository reviewRepository; + @InjectMocks private ProductService productService; @@ -66,7 +86,7 @@ class ProductServiceTest { // when then assertAll( () -> assertThatThrownBy(() -> productService.findById(1L)) - .isExactlyInstanceOf(KeyboardNotFoundException.class), + .isExactlyInstanceOf(ProductNotFoundException.class), () -> verify(productRepository).findById(1L) ); } @@ -113,4 +133,32 @@ class ProductServiceTest { .containsOnly(ProductResponse.from(product)) ); } + + @Test + void 특정_제품의_사용자의_연차와_직군의_비율을_반환한다() { + // given + Long productId = 1L; + given(productRepository.existsById(productId)) + .willReturn(true); + given(reviewRepository.findCareerLevelCountByProductId(productId)) + .willReturn(List.of(new CareerLevelCount(JUNIOR, 1), new CareerLevelCount(MID_LEVEL, 1))); + given(reviewRepository.findJobTypeCountByProductId(productId)) + .willReturn(List.of(new JobTypeCount(BACKEND, 1), new JobTypeCount(ETC, 1))); + + // when + ProductStatisticsResponse productStatisticsResponse = productService.calculateMemberStatisticsById(productId); + + // then + assertAll( + () -> verify(productRepository).existsById(productId), + () -> verify(reviewRepository).findCareerLevelCountByProductId(productId), + () -> verify(reviewRepository).findJobTypeCountByProductId(productId), + () -> assertThat(productStatisticsResponse.getCareerLevel()).usingRecursiveComparison() + .isEqualTo(Map.of(NONE_CONSTANT, 0.0, JUNIOR_CONSTANT, 0.5, MID_LEVEL_CONSTANT, 0.5, + SENIOR_CONSTANT, 0.0)), + () -> assertThat(productStatisticsResponse.getJobType()).usingRecursiveComparison() + .isEqualTo(Map.of(FRONTEND_CONSTANT, 0.0, BACKEND_CONSTANT, 0.5, MOBILE_CONSTANT, 0.0, + ETC_CONSTANT, 0.5)) + ); + } } diff --git a/backend/src/test/java/com/woowacourse/f12/application/review/ReviewServiceTest.java b/backend/src/test/java/com/woowacourse/f12/application/review/ReviewServiceTest.java index f70c24f7..b355add8 100644 --- a/backend/src/test/java/com/woowacourse/f12/application/review/ReviewServiceTest.java +++ b/backend/src/test/java/com/woowacourse/f12/application/review/ReviewServiceTest.java @@ -30,8 +30,8 @@ import com.woowacourse.f12.dto.response.review.ReviewWithProductResponse; import com.woowacourse.f12.exception.badrequest.AlreadyWrittenReviewException; import com.woowacourse.f12.exception.forbidden.NotAuthorException; -import com.woowacourse.f12.exception.notfound.KeyboardNotFoundException; import com.woowacourse.f12.exception.notfound.MemberNotFoundException; +import com.woowacourse.f12.exception.notfound.ProductNotFoundException; import com.woowacourse.f12.exception.notfound.ReviewNotFoundException; import java.util.List; import java.util.Optional; @@ -135,7 +135,7 @@ class ReviewServiceTest { // when, then assertAll( () -> assertThatThrownBy(() -> reviewService.saveReviewAndInventoryProduct(1L, 1L, reviewRequest)) - .isExactlyInstanceOf(KeyboardNotFoundException.class), + .isExactlyInstanceOf(ProductNotFoundException.class), () -> verify(memberRepository).findById(memberId), () -> verify(productRepository).findById(productId), () -> verify(reviewRepository, times(0)).save(any(Review.class)) @@ -208,7 +208,7 @@ class ReviewServiceTest { // when, then assertAll( () -> assertThatThrownBy(() -> reviewService.findPageByProductId(0L, pageable)) - .isExactlyInstanceOf(KeyboardNotFoundException.class), + .isExactlyInstanceOf(ProductNotFoundException.class), () -> verify(productRepository).existsById(0L) ); } diff --git a/backend/src/test/java/com/woowacourse/f12/documentation/product/ProductDocumentation.java b/backend/src/test/java/com/woowacourse/f12/documentation/product/ProductDocumentation.java index df20b5ca..61a66711 100644 --- a/backend/src/test/java/com/woowacourse/f12/documentation/product/ProductDocumentation.java +++ b/backend/src/test/java/com/woowacourse/f12/documentation/product/ProductDocumentation.java @@ -1,8 +1,17 @@ package com.woowacourse.f12.documentation.product; +import static com.woowacourse.f12.domain.member.CareerLevel.JUNIOR; +import static com.woowacourse.f12.domain.member.CareerLevel.MID_LEVEL; +import static com.woowacourse.f12.domain.member.CareerLevel.NONE; +import static com.woowacourse.f12.domain.member.CareerLevel.SENIOR; +import static com.woowacourse.f12.domain.member.JobType.BACKEND; +import static com.woowacourse.f12.domain.member.JobType.ETC; +import static com.woowacourse.f12.domain.member.JobType.FRONTEND; +import static com.woowacourse.f12.domain.member.JobType.MOBILE; import static com.woowacourse.f12.presentation.product.CategoryConstant.KEYBOARD_CONSTANT; import static com.woowacourse.f12.support.ProductFixture.KEYBOARD_1; import static com.woowacourse.f12.support.ProductFixture.KEYBOARD_2; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.BDDMockito.given; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; @@ -11,11 +20,15 @@ import com.woowacourse.f12.application.product.ProductService; import com.woowacourse.f12.documentation.Documentation; +import com.woowacourse.f12.domain.member.CareerLevel; +import com.woowacourse.f12.domain.member.JobType; import com.woowacourse.f12.domain.product.Product; import com.woowacourse.f12.dto.response.product.ProductPageResponse; import com.woowacourse.f12.dto.response.product.ProductResponse; +import com.woowacourse.f12.dto.response.product.ProductStatisticsResponse; import com.woowacourse.f12.presentation.product.ProductController; import java.util.List; +import java.util.Map; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; @@ -78,4 +91,25 @@ class ProductDocumentation extends Documentation { document("products-page-get") ); } + + @Test + void 특정_제품에_대한_사용자_통계_조회_API_문서화() throws Exception { + // given + Map careerLevel = Map.of(NONE, 0.0, JUNIOR, 0.5, + MID_LEVEL, 0.0, SENIOR, 0.5); + Map jobType = Map.of(FRONTEND, 0.33, BACKEND, + 0.33, MOBILE, 0.33, ETC, 0.0); + given(productService.calculateMemberStatisticsById(anyLong())) + .willReturn(ProductStatisticsResponse.of(careerLevel, jobType)); + + // when + ResultActions resultActions = mockMvc.perform( + get("/api/v1/products/1/statistics") + ); + + // then + resultActions.andExpect(status().isOk()) + .andDo(document("products-member-statistics-get")) + .andDo(print()); + } } diff --git a/backend/src/test/java/com/woowacourse/f12/domain/review/MemberInfoStatisticsTest.java b/backend/src/test/java/com/woowacourse/f12/domain/review/MemberInfoStatisticsTest.java new file mode 100644 index 00000000..f4d526fe --- /dev/null +++ b/backend/src/test/java/com/woowacourse/f12/domain/review/MemberInfoStatisticsTest.java @@ -0,0 +1,51 @@ +package com.woowacourse.f12.domain.review; + +import static com.woowacourse.f12.domain.member.CareerLevel.JUNIOR; +import static com.woowacourse.f12.domain.member.CareerLevel.MID_LEVEL; +import static com.woowacourse.f12.domain.member.CareerLevel.NONE; +import static com.woowacourse.f12.domain.member.CareerLevel.SENIOR; +import static com.woowacourse.f12.domain.member.JobType.BACKEND; +import static com.woowacourse.f12.domain.member.JobType.ETC; +import static com.woowacourse.f12.domain.member.JobType.FRONTEND; +import static com.woowacourse.f12.domain.member.JobType.MOBILE; +import static org.assertj.core.api.Assertions.assertThat; + +import com.woowacourse.f12.domain.member.CareerLevel; +import com.woowacourse.f12.domain.member.JobType; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class MemberInfoStatisticsTest { + + @Test + void 연차에_대한_비율을_계산하여_반환한다() { + // given + List careerLevelCounts = List.of(new CareerLevelCount(JUNIOR, 2), + new CareerLevelCount(SENIOR, 2)); + MemberInfoStatistics memberInfoStatistics = new MemberInfoStatistics<>( + careerLevelCounts); + + // when + Map result = memberInfoStatistics.calculateStatistics(CareerLevel.values()); + + // then + assertThat(result).usingRecursiveComparison() + .isEqualTo(Map.of(JUNIOR, 0.50, SENIOR, 0.50, NONE, 0.00, MID_LEVEL, 0.00)); + } + + @Test + void 직군에_대한_비율을_계산하여_반환한다() { + // given + List jobTypeCounts = List.of(new JobTypeCount(BACKEND, 1), new JobTypeCount(FRONTEND, 1), + new JobTypeCount(MOBILE, 1)); + MemberInfoStatistics memberInfoStatistics = new MemberInfoStatistics<>(jobTypeCounts); + + // when + Map result = memberInfoStatistics.calculateStatistics(JobType.values()); + + // then + assertThat(result).usingRecursiveComparison() + .isEqualTo(Map.of(BACKEND, 0.33, FRONTEND, 0.33, MOBILE, 0.33, ETC, 0.0)); + } +} diff --git a/backend/src/test/java/com/woowacourse/f12/domain/review/ReviewRepositoryTest.java b/backend/src/test/java/com/woowacourse/f12/domain/review/ReviewRepositoryTest.java index 26dc595b..98ab561f 100644 --- a/backend/src/test/java/com/woowacourse/f12/domain/review/ReviewRepositoryTest.java +++ b/backend/src/test/java/com/woowacourse/f12/domain/review/ReviewRepositoryTest.java @@ -1,8 +1,15 @@ package com.woowacourse.f12.domain.review; +import static com.woowacourse.f12.domain.member.CareerLevel.JUNIOR; +import static com.woowacourse.f12.domain.member.CareerLevel.SENIOR; +import static com.woowacourse.f12.domain.member.JobType.BACKEND; +import static com.woowacourse.f12.domain.member.JobType.MOBILE; +import static com.woowacourse.f12.support.MemberFixtures.CORINNE; +import static com.woowacourse.f12.support.MemberFixtures.MINCHO; import static com.woowacourse.f12.support.ProductFixture.KEYBOARD_1; import static com.woowacourse.f12.support.ProductFixture.KEYBOARD_2; -import static com.woowacourse.f12.support.MemberFixtures.CORINNE; +import static com.woowacourse.f12.support.ReviewFixtures.REVIEW_RATING_1; +import static com.woowacourse.f12.support.ReviewFixtures.REVIEW_RATING_2; import static com.woowacourse.f12.support.ReviewFixtures.REVIEW_RATING_4; import static com.woowacourse.f12.support.ReviewFixtures.REVIEW_RATING_5; import static org.assertj.core.api.Assertions.assertThat; @@ -14,6 +21,7 @@ import com.woowacourse.f12.domain.member.MemberRepository; import com.woowacourse.f12.domain.product.Product; import com.woowacourse.f12.domain.product.ProductRepository; +import java.util.List; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import org.junit.jupiter.api.Test; @@ -145,6 +153,71 @@ class ReviewRepositoryTest { assertThat(actual).isTrue(); } + @Test + void 제품에_대한_사용자_연차의_총_개수를_반환한다() { + // given + Product product = 제품_저장(KEYBOARD_1.생성()); + + Member corinne = CORINNE.생성(); + corinne.updateCareerLevel(SENIOR); + corinne.updateJobType(MOBILE); + corinne = memberRepository.save(corinne); + + Member mincho = MINCHO.생성(); + mincho.updateCareerLevel(JUNIOR); + mincho.updateJobType(BACKEND); + mincho = memberRepository.save(mincho); + + 리뷰_저장(REVIEW_RATING_2.작성(product, corinne)); + 리뷰_저장(REVIEW_RATING_1.작성(product, mincho)); + + // when + List careerLevelCounts = reviewRepository.findCareerLevelCountByProductId( + product.getId()); + + // then + assertThat(careerLevelCounts).usingRecursiveFieldByFieldElementComparator() + .hasSize(2) + .containsOnly( + new CareerLevelCount(JUNIOR, 1), + new CareerLevelCount(SENIOR, 1) + ); + } + + @Test + void 제품에_대한_사용자_직군의_총_개수를_반환한다() { + // given + Product product = 제품_저장(KEYBOARD_1.생성()); + + Member corinne = CORINNE.생성(); + corinne.updateCareerLevel(SENIOR); + corinne.updateJobType(MOBILE); + corinne = memberRepository.save(corinne); + + Member mincho = MINCHO.생성(); + mincho.updateCareerLevel(JUNIOR); + mincho.updateJobType(BACKEND); + mincho = memberRepository.save(mincho); + + 리뷰_저장(REVIEW_RATING_2.작성(product, corinne)); + 리뷰_저장(REVIEW_RATING_1.작성(product, mincho)); + + // when + List jobTypeCounts = reviewRepository.findJobTypeCountByProductId(product.getId()); + + // then + assertThat(jobTypeCounts).usingRecursiveFieldByFieldElementComparator() + .hasSize(2) + .containsOnly( + new JobTypeCount(MOBILE, 1), + new JobTypeCount(BACKEND, 1) + ); + } + + private Product 제품_저장(Product product) { + return productRepository.save(product); + } + private Review 리뷰_저장(Review review) { return reviewRepository.save(review); } diff --git a/backend/src/test/java/com/woowacourse/f12/presentation/product/ProductControllerTest.java b/backend/src/test/java/com/woowacourse/f12/presentation/product/ProductControllerTest.java index e88db29f..08db0187 100644 --- a/backend/src/test/java/com/woowacourse/f12/presentation/product/ProductControllerTest.java +++ b/backend/src/test/java/com/woowacourse/f12/presentation/product/ProductControllerTest.java @@ -1,5 +1,13 @@ package com.woowacourse.f12.presentation.product; +import static com.woowacourse.f12.domain.member.CareerLevel.JUNIOR; +import static com.woowacourse.f12.domain.member.CareerLevel.MID_LEVEL; +import static com.woowacourse.f12.domain.member.CareerLevel.NONE; +import static com.woowacourse.f12.domain.member.CareerLevel.SENIOR; +import static com.woowacourse.f12.domain.member.JobType.BACKEND; +import static com.woowacourse.f12.domain.member.JobType.ETC; +import static com.woowacourse.f12.domain.member.JobType.FRONTEND; +import static com.woowacourse.f12.domain.member.JobType.MOBILE; import static com.woowacourse.f12.presentation.product.CategoryConstant.KEYBOARD_CONSTANT; import static com.woowacourse.f12.support.ProductFixture.KEYBOARD_1; import static org.mockito.ArgumentMatchers.anyLong; @@ -14,11 +22,15 @@ import com.woowacourse.f12.application.auth.JwtProvider; import com.woowacourse.f12.application.product.ProductService; +import com.woowacourse.f12.domain.member.CareerLevel; +import com.woowacourse.f12.domain.member.JobType; import com.woowacourse.f12.dto.response.product.ProductPageResponse; import com.woowacourse.f12.dto.response.product.ProductResponse; -import com.woowacourse.f12.exception.notfound.KeyboardNotFoundException; +import com.woowacourse.f12.dto.response.product.ProductStatisticsResponse; +import com.woowacourse.f12.exception.notfound.ProductNotFoundException; import com.woowacourse.f12.support.AuthTokenExtractor; import java.util.List; +import java.util.Map; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; @@ -105,7 +117,7 @@ class ProductControllerTest { void 제품_단일_조회_실패_존재_하지_않는_아이디() throws Exception { // given given(productService.findById(anyLong())) - .willThrow(new KeyboardNotFoundException()); + .willThrow(new ProductNotFoundException()); // when mockMvc.perform(get("/api/v1/products/0")) @@ -115,4 +127,38 @@ class ProductControllerTest { // then verify(productService).findById(0L); } + + @Test + void 특정_제품에_대한_사용자_통계_조회_성공() throws Exception { + // given + Map careerLevel = Map.of(NONE, 0.0, JUNIOR, 0.5, + MID_LEVEL, 0.0, SENIOR, 0.5); + Map jobType = Map.of(FRONTEND, 0.5, BACKEND, + 0.5, MOBILE, 0.0, ETC, 0.0); + given(productService.calculateMemberStatisticsById(anyLong())) + .willReturn(ProductStatisticsResponse.of(careerLevel, jobType)); + + // when + mockMvc.perform(get("/api/v1/products/1/statistics")) + .andExpect(status().isOk()) + .andDo(print()); + + // then + verify(productService).calculateMemberStatisticsById(1L); + } + + @Test + void 특정_제품에_대한_사용자_통계_조회_실패_제품이_존재하지_않을_경우() throws Exception { + // given + given(productService.calculateMemberStatisticsById(anyLong())) + .willThrow(new ProductNotFoundException()); + + // when + mockMvc.perform(get("/api/v1/products/1/statistics")) + .andExpect(status().isNotFound()) + .andDo(print()); + + // then + verify(productService).calculateMemberStatisticsById(1L); + } } diff --git a/backend/src/test/java/com/woowacourse/f12/presentation/review/ReviewControllerTest.java b/backend/src/test/java/com/woowacourse/f12/presentation/review/ReviewControllerTest.java index ade6daa7..21662356 100644 --- a/backend/src/test/java/com/woowacourse/f12/presentation/review/ReviewControllerTest.java +++ b/backend/src/test/java/com/woowacourse/f12/presentation/review/ReviewControllerTest.java @@ -1,7 +1,7 @@ package com.woowacourse.f12.presentation.review; -import static com.woowacourse.f12.support.ProductFixture.KEYBOARD_1; import static com.woowacourse.f12.support.MemberFixtures.CORINNE; +import static com.woowacourse.f12.support.ProductFixture.KEYBOARD_1; import static com.woowacourse.f12.support.ReviewFixtures.REVIEW_RATING_5; import static org.junit.jupiter.api.Assertions.assertAll; import static org.mockito.ArgumentMatchers.any; @@ -30,8 +30,8 @@ import com.woowacourse.f12.exception.badrequest.InvalidContentLengthException; import com.woowacourse.f12.exception.badrequest.InvalidRatingValueException; import com.woowacourse.f12.exception.forbidden.NotAuthorException; -import com.woowacourse.f12.exception.notfound.KeyboardNotFoundException; import com.woowacourse.f12.exception.notfound.MemberNotFoundException; +import com.woowacourse.f12.exception.notfound.ProductNotFoundException; import com.woowacourse.f12.exception.notfound.ReviewNotFoundException; import com.woowacourse.f12.support.AuthTokenExtractor; import java.util.HashMap; @@ -261,7 +261,7 @@ public ReviewControllerTest() { given(jwtProvider.getPayload(authorizationHeader)) .willReturn("1"); given(reviewService.saveReviewAndInventoryProduct(anyLong(), anyLong(), any(ReviewRequest.class))) - .willThrow(new KeyboardNotFoundException()); + .willThrow(new ProductNotFoundException()); // when mockMvc.perform( @@ -374,7 +374,7 @@ public ReviewControllerTest() { void 특정_상품의_리뷰_페이지_조회_실패_상품_존재_하지않음() throws Exception { // given given(reviewService.findPageByProductId(anyLong(), any(Pageable.class))) - .willThrow(new KeyboardNotFoundException()); + .willThrow(new ProductNotFoundException()); // when mockMvc.perform(get("/api/v1/products/" + 0L + "/reviews?size=150&page=0&sort=rating,desc"))