Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BE] 제품에 대한 사용자 통계 기능을 구현 #292

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 12 additions & 15 deletions backend/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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")
}
}
4 changes: 4 additions & 0 deletions backend/src/docs/asciidoc/products.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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);
}

Expand All @@ -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, Double> careerLevel = calculateWithCareerLevel(productId);
final Map<JobType, Double> jobType = calculateWithJobType(productId);

return ProductStatisticsResponse.of(careerLevel, jobType);
}

private Map<JobType, Double> calculateWithJobType(final Long productId) {
final List<JobTypeCount> jobTypeCounts = reviewRepository.findJobTypeCountByProductId(productId);
final MemberInfoStatistics<JobTypeCount, JobType> jobTypeStatistics = new MemberInfoStatistics<>(jobTypeCounts);
return jobTypeStatistics.calculateStatistics(JobType.values());
}

private Map<CareerLevel, Double> calculateWithCareerLevel(final Long productId) {
final List<CareerLevelCount> careerLevelCounts = reviewRepository.findCareerLevelCountByProductId(productId);
final MemberInfoStatistics<CareerLevelCount, CareerLevel> careerLevelStatistics = new MemberInfoStatistics<>(
careerLevelCounts);
return careerLevelStatistics.calculateStatistics(CareerLevel.values());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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();
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.woowacourse.f12.domain.member;

public enum CareerLevel {
public enum CareerLevel implements MemberInfo {

NONE,
JUNIOR,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,10 @@
import lombok.Getter;

@Getter
public enum JobType {
public enum JobType implements MemberInfo {

FRONTEND,
BACKEND,
MOBILE,
ETC
;
ETC;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.woowacourse.f12.domain.member;

public interface MemberInfo {

}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.woowacourse.f12.domain.review;

public interface Countable {

Enum getValue();

long getCount();
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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<E extends Countable, T extends MemberInfo> {

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<E> elements;

public MemberInfoStatistics(final List<E> elements) {
this.elements = elements;
}

public Map<T, Double> 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

public interface ReviewRepository extends JpaRepository<Review, Long> {
public interface ReviewRepository extends JpaRepository<Review, Long>, ReviewRepositoryCustom {

@Query("select r from Review r where r.product.id = :productId")
Slice<Review> findPageByProductId(Long productId, Pageable pageable);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.woowacourse.f12.domain.review;

import java.util.List;

public interface ReviewRepositoryCustom {

List<CareerLevelCount> findCareerLevelCountByProductId(Long productId);

List<JobTypeCount> findJobTypeCountByProductId(Long productId);
}
Original file line number Diff line number Diff line change
@@ -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<CareerLevelCount> findCareerLevelCountByProductId(final Long productId) {
final JPAQuery<CareerLevelCount> 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<JobTypeCount> findJobTypeCountByProductId(final Long productId) {
final JPAQuery<JobTypeCount> 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();
}
}
Original file line number Diff line number Diff line change
@@ -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<CareerLevelConstant, Double> careerLevel;
private Map<JobTypeConstant, Double> jobType;

private ProductStatisticsResponse() {
}

private ProductStatisticsResponse(final Map<CareerLevelConstant, Double> careerLevel,
final Map<JobTypeConstant, Double> jobType) {
this.careerLevel = careerLevel;
this.jobType = jobType;
}

public static ProductStatisticsResponse of(final Map<CareerLevel, Double> enumCareerLevel,
final Map<JobType, Double> enumJobType) {
final Map<CareerLevelConstant, Double> careerLevel = enumCareerLevel.entrySet()
.stream()
.collect(Collectors.toMap(entry -> CareerLevelConstant.from(entry.getKey()), Entry::getValue));
final Map<JobTypeConstant, Double> jobType = enumJobType.entrySet()
.stream()
.collect(Collectors.toMap(entry -> JobTypeConstant.from(entry.getKey()), Entry::getValue));
return new ProductStatisticsResponse(careerLevel, jobType);
}
}
Loading