Skip to content

Commit

Permalink
[BE] refactor: 꿀조합 랭킹 알고리즘 개선 (#755)
Browse files Browse the repository at this point in the history
* feat: 꿀조합 랭킹 점수 계산 로직 추가

* refactor: 꿀조합 랭킹 기능 수정

* test: 꿀조합 랭킹 관련 테스트 추가

* refactor: import 정렬

* test: 상황에 따른 꿀조합 랭킹 서비스 테스트 추가

* refactor: Objects import 추가

* fix: 충돌 해결
  • Loading branch information
Go-Jaecheol authored Oct 18, 2023
1 parent 9467d94 commit 0532bbe
Show file tree
Hide file tree
Showing 9 changed files with 259 additions and 19 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
package com.funeat.product.domain;

import javax.persistence.*;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class Category {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
import com.funeat.recipe.persistence.RecipeImageRepository;
import com.funeat.recipe.persistence.RecipeRepository;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
Expand All @@ -61,8 +62,8 @@
@Transactional(readOnly = true)
public class RecipeService {

private static final int THREE = 3;
private static final int TOP = 0;
private static final long RANKING_MINIMUM_FAVORITE_COUNT = 1L;
private static final int RANKING_SIZE = 3;
private static final int RECIPE_COMMENT_PAGE_SIZE = 10;
private static final int DEFAULT_CURSOR_PAGINATION_SIZE = 11;

Expand Down Expand Up @@ -206,9 +207,11 @@ public SearchRecipeResultsResponse getSearchResults(final String query, final Pa
}

public RankingRecipesResponse getTop3Recipes() {
final List<Recipe> recipes = recipeRepository.findRecipesByOrderByFavoriteCountDesc(PageRequest.of(TOP, THREE));
final List<Recipe> recipes = recipeRepository.findRecipesByFavoriteCountGreaterThanEqual(RANKING_MINIMUM_FAVORITE_COUNT);

final List<RankingRecipeDto> dtos = recipes.stream()
.sorted(Comparator.comparing(Recipe::calculateRankingScore).reversed())
.limit(RANKING_SIZE)
.map(recipe -> {
final List<RecipeImage> findRecipeImages = recipeImageRepository.findByRecipe(recipe);
final RecipeAuthorDto author = RecipeAuthorDto.toDto(recipe.getMember());
Expand Down
18 changes: 18 additions & 0 deletions backend/src/main/java/com/funeat/recipe/domain/Recipe.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.funeat.member.domain.Member;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
Expand All @@ -14,6 +15,8 @@
@Entity
public class Recipe {

private static final double RANKING_GRAVITY = 0.1;

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
Expand Down Expand Up @@ -48,6 +51,21 @@ public Recipe(final String title, final String content, final Member member,
this.favoriteCount = favoriteCount;
}

public Recipe(final String title, final String content, final Member member, final Long favoriteCount,
final LocalDateTime createdAt) {
this.title = title;
this.content = content;
this.member = member;
this.favoriteCount = favoriteCount;
this.createdAt = createdAt;
}

public Double calculateRankingScore() {
final long age = ChronoUnit.DAYS.between(createdAt, LocalDateTime.now());
final double denominator = Math.pow(age + 1.0, RANKING_GRAVITY);
return favoriteCount / denominator;
}

public void addFavoriteCount() {
this.favoriteCount++;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ public interface RecipeRepository extends JpaRepository<Recipe, Long> {
@Query("SELECT r FROM Recipe r LEFT JOIN ProductRecipe pr ON pr.product = :product WHERE pr.recipe.id = r.id")
Page<Recipe> findRecipesByProduct(final Product product, final Pageable pageable);

List<Recipe> findRecipesByOrderByFavoriteCountDesc(final Pageable pageable);

@Lock(PESSIMISTIC_WRITE)
@Query("SELECT r FROM Recipe r WHERE r.id=:id")
Optional<Recipe> findByIdForUpdate(final Long id);

List<Recipe> findRecipesByFavoriteCountGreaterThanEqual(final Long favoriteCount);
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,10 @@
import com.funeat.review.dto.SortingReviewRequest;
import com.funeat.review.dto.SortingReviewsResponse;
import java.net.URI;
import java.util.Objects;
import java.util.Optional;
import javax.validation.Valid;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PatchMapping;
Expand Down
6 changes: 6 additions & 0 deletions backend/src/test/java/com/funeat/fixture/RecipeFixture.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.funeat.recipe.domain.RecipeImage;
import com.funeat.recipe.dto.RecipeCreateRequest;
import com.funeat.recipe.dto.RecipeFavoriteRequest;
import java.time.LocalDateTime;
import java.util.List;

@SuppressWarnings("NonAsciiCharacters")
Expand Down Expand Up @@ -33,6 +34,11 @@ public class RecipeFixture {
return new Recipe("The most delicious recipes", "More rice, more rice, more rice.. Done!!", member, favoriteCount);
}

public static Recipe 레시피_생성(final Member member, final Long favoriteCount, final LocalDateTime createdAt) {
return new Recipe("The most delicious recipes", "More rice, more rice, more rice.. Done!!",
member, favoriteCount, createdAt);
}

public static RecipeFavorite 레시피_좋아요_생성(final Member member, final Recipe recipe, final Boolean favorite) {
return new RecipeFavorite(member, recipe, favorite);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,17 @@
import com.funeat.product.domain.CategoryType;
import com.funeat.product.domain.Product;
import com.funeat.product.exception.ProductException.ProductNotFoundException;
import com.funeat.recipe.dto.RankingRecipeDto;
import com.funeat.recipe.dto.RankingRecipesResponse;
import com.funeat.recipe.dto.RecipeAuthorDto;
import com.funeat.recipe.dto.RecipeCommentCondition;
import com.funeat.recipe.dto.RecipeCommentCreateRequest;
import com.funeat.recipe.dto.RecipeCommentResponse;
import com.funeat.recipe.dto.RecipeCreateRequest;
import com.funeat.recipe.dto.RecipeDetailResponse;
import com.funeat.recipe.dto.RecipeDto;
import com.funeat.recipe.exception.RecipeException.RecipeNotFoundException;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
Expand Down Expand Up @@ -551,6 +555,158 @@ class likeRecipe_실패_테스트 {
}
}

@Nested
class getTop3Recipes_성공_테스트 {

@Nested
class 꿀조합_개수에_대한_테스트 {

@Test
void 전체_꿀조합이_하나도_없어도_반환값은_있어야한다() {
// given
final var expected = RankingRecipesResponse.toResponse(Collections.emptyList());

// when
final var actual = recipeService.getTop3Recipes();

// then
assertThat(actual).usingRecursiveComparison()
.isEqualTo(expected);
}

@Test
void 랭킹_조건에_부합하는_꿀조합이_1개면_꿀조합이_1개_반환된다() {
// given
final var member = 멤버_멤버1_생성();
단일_멤버_저장(member);

final var now = LocalDateTime.now();
final var recipe = 레시피_생성(member, 2L, now);
단일_꿀조합_저장(recipe);

final var author = RecipeAuthorDto.toDto(member);
final var rankingRecipeDto = RankingRecipeDto.toDto(recipe, Collections.emptyList(), author);
final var rankingRecipesDtos = Collections.singletonList(rankingRecipeDto);
final var expected = RankingRecipesResponse.toResponse(rankingRecipesDtos);

// when
final var actual = recipeService.getTop3Recipes();

// then
assertThat(actual).usingRecursiveComparison()
.isEqualTo(expected);
}

@Test
void 랭킹_조건에_부합하는_꿀조합이_2개면_꿀조합이_2개_반환된다() {
// given
final var member = 멤버_멤버1_생성();
단일_멤버_저장(member);

final var now = LocalDateTime.now();
final var recipe1 = 레시피_생성(member, 2L, now.minusDays(1L));
final var recipe2 = 레시피_생성(member, 2L, now);
복수_꿀조합_저장(recipe1, recipe2);

final var author = RecipeAuthorDto.toDto(member);
final var rankingRecipeDto1 = RankingRecipeDto.toDto(recipe1, Collections.emptyList(), author);
final var rankingRecipeDto2 = RankingRecipeDto.toDto(recipe2, Collections.emptyList(), author);
final var rankingRecipesDtos = List.of(rankingRecipeDto2, rankingRecipeDto1);
final var expected = RankingRecipesResponse.toResponse(rankingRecipesDtos);

// when
final var actual = recipeService.getTop3Recipes();

// then
assertThat(actual).usingRecursiveComparison()
.isEqualTo(expected);
}

@Test
void 전체_꿀조합_중_랭킹이_높은_상위_3개_꿀조합을_구할_수_있다() {
// given
final var member = 멤버_멤버1_생성();
단일_멤버_저장(member);

final var now = LocalDateTime.now();
final var recipe1 = 레시피_생성(member, 4L, now.minusDays(10L));
final var recipe2 = 레시피_생성(member, 6L, now.minusDays(10L));
final var recipe3 = 레시피_생성(member, 5L, now);
final var recipe4 = 레시피_생성(member, 6L, now);
복수_꿀조합_저장(recipe1, recipe2, recipe3, recipe4);

final var author = RecipeAuthorDto.toDto(member);
final var rankingRecipeDto1 = RankingRecipeDto.toDto(recipe1, Collections.emptyList(), author);
final var rankingRecipeDto2 = RankingRecipeDto.toDto(recipe2, Collections.emptyList(), author);
final var rankingRecipeDto3 = RankingRecipeDto.toDto(recipe3, Collections.emptyList(), author);
final var rankingRecipeDto4 = RankingRecipeDto.toDto(recipe4, Collections.emptyList(), author);
final var rankingRecipesDtos = List.of(rankingRecipeDto4, rankingRecipeDto3, rankingRecipeDto2);
final var expected = RankingRecipesResponse.toResponse(rankingRecipesDtos);

// when
final var actual = recipeService.getTop3Recipes();

// then
assertThat(actual).usingRecursiveComparison()
.isEqualTo(expected);
}
}

@Nested
class 꿀조합_랭킹_점수에_대한_테스트 {

@Test
void 꿀조합_좋아요_수가_같으면_최근_생성된_꿀조합의_랭킹을_더_높게_반환한다() {
// given
final var member = 멤버_멤버1_생성();
단일_멤버_저장(member);

final var now = LocalDateTime.now();
final var recipe1 = 레시피_생성(member, 10L, now.minusDays(9L));
final var recipe2 = 레시피_생성(member, 10L, now.minusDays(4L));
복수_꿀조합_저장(recipe1, recipe2);

final var author = RecipeAuthorDto.toDto(member);
final var rankingRecipeDto1 = RankingRecipeDto.toDto(recipe1, Collections.emptyList(), author);
final var rankingRecipeDto2 = RankingRecipeDto.toDto(recipe2, Collections.emptyList(), author);
final var rankingRecipesDtos = List.of(rankingRecipeDto2, rankingRecipeDto1);
final var expected = RankingRecipesResponse.toResponse(rankingRecipesDtos);

// when
final var actual = recipeService.getTop3Recipes();

// then
assertThat(actual).usingRecursiveComparison()
.isEqualTo(expected);
}

@Test
void 꿀조합_생성_일자가_같으면_좋아요_수가_많은_꿀조합의_랭킹을_더_높게_반환한다() {
// given
final var member = 멤버_멤버1_생성();
단일_멤버_저장(member);

final var now = LocalDateTime.now();
final var recipe1 = 레시피_생성(member, 2L, now.minusDays(1L));
final var recipe2 = 레시피_생성(member, 4L, now.minusDays(1L));
복수_꿀조합_저장(recipe1, recipe2);

final var author = RecipeAuthorDto.toDto(member);
final var rankingRecipeDto1 = RankingRecipeDto.toDto(recipe1, Collections.emptyList(), author);
final var rankingRecipeDto2 = RankingRecipeDto.toDto(recipe2, Collections.emptyList(), author);
final var rankingRecipesDtos = List.of(rankingRecipeDto2, rankingRecipeDto1);
final var expected = RankingRecipesResponse.toResponse(rankingRecipesDtos);

// when
final var actual = recipeService.getTop3Recipes();

// then
assertThat(actual).usingRecursiveComparison()
.isEqualTo(expected);
}
}
}

@Nested
class writeCommentOfRecipe_성공_테스트 {

Expand Down
36 changes: 36 additions & 0 deletions backend/src/test/java/com/funeat/recipe/domain/RecipeTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.funeat.recipe.domain;

import static com.funeat.fixture.MemberFixture.멤버_멤버1_생성;
import static com.funeat.fixture.RecipeFixture.레시피_생성;
import static org.assertj.core.api.Assertions.assertThat;

import java.time.LocalDateTime;
import org.junit.jupiter.api.DisplayNameGeneration;
import org.junit.jupiter.api.DisplayNameGenerator;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

@SuppressWarnings("NonAsciiCharacters")
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
class RecipeTest {

@Nested
class calculateRankingScore_성공_테스트 {

@Test
void 꿀조합_좋아요_수와_꿀조합_생성_시간으로_해당_꿀조합의_랭킹_점수를_구할_수_있다() {
// given
final var member = 멤버_멤버1_생성();
final var favoriteCount = 4L;
final var recipe = 레시피_생성(member, favoriteCount, LocalDateTime.now().minusDays(1L));

final var expected = favoriteCount / Math.pow(2.0, 0.1);

// when
final var actual = recipe.calculateRankingScore();

// then
assertThat(actual).isEqualTo(expected);
}
}
}
Loading

0 comments on commit 0532bbe

Please sign in to comment.