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

Feat: 유저 주간 랭킹 기능 구현 (#42) #43

Merged
merged 4 commits into from
Oct 30, 2023
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.diareat.diareat.food.dto;

import lombok.AllArgsConstructor;
import lombok.Getter;

import java.util.List;

@Getter
@AllArgsConstructor
public class ResponseAnalysisDto { // 그래프 + 점수에 사용되는 DTO

private double totalScore;
private List<Double> calorieLastSevenDays; // 최근 7일간의 칼로리 (7개 과거부터 나열)
private List<Double> calorieLastFourWeek; // 최근 4주간의 칼로리 (4개 과거부터 나열)
private List<Double> carbohydrateLastSevenDays; // 최근 7일간의 탄수화물
private List<Double> carbohydrateLastFourWeek; // 최근 4주간의 탄수화물
private List<Double> proteinLastSevenDays; // 최근 7일간의 단백질
private List<Double> proteinLastFourWeek; // 최근 4주간의 단백질
private List<Double> fatLastSevenDays; // 최근 7일간의 지방
private List<Double> fatLastFourWeek; // 최근 4주간의 지방

public static ResponseAnalysisDto of(double totalScore, List<Double> calorieLastSevenDays, List<Double> calorieLastFourWeek, List<Double> carbohydrateLastSevenDays, List<Double> carbohydrateLastFourWeek, List<Double> proteinLastSevenDays, List<Double> proteinLastFourWeek, List<Double> fatLastSevenDays, List<Double> fatLastFourWeek) {
return new ResponseAnalysisDto(totalScore, calorieLastSevenDays, calorieLastFourWeek, carbohydrateLastSevenDays, carbohydrateLastFourWeek, proteinLastSevenDays, proteinLastFourWeek, fatLastSevenDays, fatLastFourWeek);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.diareat.diareat.food.dto;

import lombok.AllArgsConstructor;
import lombok.Getter;

import java.util.List;

@Getter
@AllArgsConstructor
public class ResponseScoreBestWorstDto { // 일기 분석 자세히보기에 사용되는 DTO

private double calorieScore;
private double carbohydrateScore;
private double proteinScore;
private double fatScore;
private double totalScore;
private List<ResponseSimpleFoodDto> best;
private List<ResponseSimpleFoodDto> worst;

public static ResponseScoreBestWorstDto of(double calorieScore, double carbohydrateScore, double proteinScore, double fatScore, double totalScore, List<ResponseSimpleFoodDto> best, List<ResponseSimpleFoodDto> worst) {
return new ResponseScoreBestWorstDto(calorieScore, carbohydrateScore, proteinScore, fatScore, totalScore, best, worst);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.diareat.diareat.food.dto;

import com.diareat.diareat.food.domain.Food;
import lombok.AllArgsConstructor;
import lombok.Getter;

import java.time.LocalDate;

@Getter
@AllArgsConstructor
public class ResponseSimpleFoodDto { // Best 3 and Worst 3에 사용될 객체

private String name;
private double calorie;
private double carbohydrate;
private double protein;
private double fat;
private LocalDate date;

public static ResponseSimpleFoodDto of(String name, double calorie, double carbohydrate, double protein, double fat, LocalDate date) {
return new ResponseSimpleFoodDto(name, calorie, carbohydrate, protein, fat, date);
}

public static ResponseSimpleFoodDto from(Food food) {
return new ResponseSimpleFoodDto(food.getName(), food.getBaseNutrition().getKcal(), food.getBaseNutrition().getCarbohydrate(),
food.getBaseNutrition().getProtein(), food.getBaseNutrition().getFat(), food.getDate());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@

public interface FavoriteFoodRepository extends JpaRepository<FavoriteFood, Long> {
List<FavoriteFood> findAllByUserId(Long userId);
boolean existsByFoodId(Long foodId); // 이미 즐겨찾기에 추가된 음식인지 확인하기 위함
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@

public interface FoodRepository extends JpaRepository<Food, Long> {
List<Food> findAllByUserIdAndDate(Long userId, LocalDate date); //유저가 특정 날짜에 먹은 음식 반환
List<Food> findAllByUserIdAndDateBetween(Long userId, LocalDate startDate, LocalDate endDate); //유저가 특정 기간 내에 먹은 음식 반환
List<Food> findAllByUserIdAndDateBetween(Long userId, LocalDate startDate, LocalDate endDate); // 유저가 특정 기간 내에 먹은 음식 반환
}
114 changes: 100 additions & 14 deletions src/main/java/com/diareat/diareat/food/service/FoodService.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import com.diareat.diareat.food.repository.FoodRepository;
import com.diareat.diareat.user.domain.BaseNutrition;
import com.diareat.diareat.user.domain.User;
import com.diareat.diareat.user.dto.ResponseRankUserDto;
import com.diareat.diareat.user.repository.FollowRepository;
import com.diareat.diareat.user.repository.UserRepository;
import com.diareat.diareat.util.api.ResponseCode;
import com.diareat.diareat.util.exception.FavoriteException;
Expand All @@ -16,9 +18,9 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.DayOfWeek;
import java.time.LocalDate;
import java.util.Comparator;
import java.util.List;
import java.util.*;
import java.util.stream.Collectors;

@RequiredArgsConstructor
Expand All @@ -27,7 +29,7 @@ public class FoodService {

private final FoodRepository foodRepository; // 유저:음식은 1:다
private final FavoriteFoodRepository favoriteFoodRepository; // 유저:즐찾음식은 1:다

private final FollowRepository followRepository;
private final UserRepository userRepository;

// 촬영 후, 음식 정보 저장
Expand Down Expand Up @@ -65,9 +67,9 @@ public void deleteFood(Long foodId) {
// 즐겨찾기에 음식 저장
@Transactional
public Long saveFavoriteFood(CreateFavoriteFoodDto createFavoriteFoodDto) {

User user = getUserById(createFavoriteFoodDto.getUserId());

if (favoriteFoodRepository.existsByFoodId(createFavoriteFoodDto.getFoodId()))
throw new FavoriteException(ResponseCode.FAVORITE_ALREADY_EXIST);
FavoriteFood favoriteFood = FavoriteFood.createFavoriteFood(createFavoriteFoodDto.getName(), user, createFavoriteFoodDto.getBaseNutrition());
return favoriteFoodRepository.save(favoriteFood).getId();
}
Expand Down Expand Up @@ -169,22 +171,76 @@ public ResponseFoodRankDto getWorstFoodByWeek(Long userId) {
return ResponseFoodRankDto.of(userId, worst3FoodDtoList, endDate, false);
}

private User getUserById(Long userId){
// 잔여 기능 구현 부분

// 유저의 구체적인 점수 현황과 Best3, Worst3 조회

// 유저의 일기 분석 그래프 데이터 및 식습관 totalScore 조회


@Transactional(readOnly = true)
// 유저의 식습관 점수를 기반으로 한 주간 랭킹 조회
public List<ResponseRankUserDto> getUserRankByWeek(Long userId) {
List<ResponseRankUserDto> rankUserDtos = new ArrayList<>();
List<User> users = followRepository.findAllByFromUser(userId); // 유저의 팔로우 유저 명단
rankUserDtos.add(calculateUserScoreThisWeek(getUserById(userId), LocalDate.now().with(DayOfWeek.MONDAY), LocalDate.now()));
if (users.isEmpty()) { // 팔로우한 유저가 없는 경우 본인의 점수 및 정보만 반환함
return rankUserDtos;
}

// 팔로우한 유저들의 점수를 계산하여 rankUserDtos에 추가
for (User user : users) {
ResponseRankUserDto userDto = calculateUserScoreThisWeek(user, LocalDate.now().with(DayOfWeek.MONDAY), LocalDate.now());
rankUserDtos.add(userDto);
}

// 식습관 총점 기준 내림차순 정렬
rankUserDtos.sort(Comparator.comparing(ResponseRankUserDto::getTotalScore).reversed());
return rankUserDtos;
}


private User getUserById(Long userId) {
return userRepository.findById(userId)
.orElseThrow(() -> new UserException(ResponseCode.USER_NOT_FOUND));
}

private Food getFoodById(Long foodId){
private Food getFoodById(Long foodId) {
return foodRepository.findById(foodId)
.orElseThrow(() -> new FoodException(ResponseCode.FOOD_NOT_FOUND));
}

private FavoriteFood getFavoriteFoodById(Long foodId){
private FavoriteFood getFavoriteFoodById(Long foodId) {
return favoriteFoodRepository.findById(foodId)
.orElseThrow(() -> new FoodException(ResponseCode.FOOD_NOT_FOUND));
}

private ResponseNutritionSumByDateDto calculateNutritionSumAndRatio(Long userId, List<Food> foodList, LocalDate checkDate, int nutritionSumType){
private ResponseRankUserDto calculateUserScoreThisWeek(User targetUser, LocalDate startDate, LocalDate endDate) {
Map<LocalDate, List<BaseNutrition>> maps = getNutritionSumByDateMap(targetUser.getId(), startDate, endDate);
double kcalScore = 0.0;
double carbohydrateScore = 0.0;
double proteinScore = 0.0;
double fatScore = 0.0;
double totalScore;

for (LocalDate date : maps.keySet()) {
// 해당 날짜에 먹은 음식들의 영양성분 총합 계산
int totalKcal = maps.get(date).stream().mapToInt(BaseNutrition::getKcal).sum();
int totalCarbohydrate = maps.get(date).stream().mapToInt(BaseNutrition::getCarbohydrate).sum();
int totalProtein = maps.get(date).stream().mapToInt(BaseNutrition::getProtein).sum();
int totalFat = maps.get(date).stream().mapToInt(BaseNutrition::getFat).sum();

// 기준섭취량 대비 섭취 비율에 매핑되는 식습관 점수 계산
proteinScore += calculateNutriRatioAndScore(totalProtein, targetUser.getBaseNutrition().getProtein(), 0);
fatScore += calculateNutriRatioAndScore(totalFat, targetUser.getBaseNutrition().getFat(), 1);
carbohydrateScore += calculateNutriRatioAndScore(totalCarbohydrate, targetUser.getBaseNutrition().getCarbohydrate(), 1);
kcalScore += calculateNutriRatioAndScore(totalKcal, targetUser.getBaseNutrition().getKcal(), 1);
}
totalScore = (kcalScore + carbohydrateScore + proteinScore + fatScore);
return ResponseRankUserDto.of(targetUser.getId(), targetUser.getName(), targetUser.getImage(), kcalScore, carbohydrateScore, proteinScore, fatScore, totalScore);
}

private ResponseNutritionSumByDateDto calculateNutritionSumAndRatio(Long userId, List<Food> foodList, LocalDate checkDate, int nutritionSumType) {
User targetUser = getUserById(userId);
int totalKcal = 0;
int totalCarbohydrate = 0;
Expand All @@ -199,12 +255,12 @@ private ResponseNutritionSumByDateDto calculateNutritionSumAndRatio(Long userId,
totalFat += targetFoodNutrition.getFat();
}

double ratioKcal = Math.round((((double) totalKcal /(double) targetUser.getBaseNutrition().getKcal())*100.0)*10.0)/10.0;
double ratioCarbohydrate = Math.round((((double) totalCarbohydrate /(double) targetUser.getBaseNutrition().getCarbohydrate())*100.0)*10.0)/10.0;
double ratioProtein = Math.round((((double) totalProtein /(double) targetUser.getBaseNutrition().getProtein())*100.0)*10.0)/10.0;
double ratioFat =Math.round((((double) totalFat /(double) targetUser.getBaseNutrition().getFat())*100.0)*10.0)/10.0;
double ratioKcal = Math.round((((double) totalKcal / (double) targetUser.getBaseNutrition().getKcal()) * 100.0) * 10.0) / 10.0;
double ratioCarbohydrate = Math.round((((double) totalCarbohydrate / (double) targetUser.getBaseNutrition().getCarbohydrate()) * 100.0) * 10.0) / 10.0;
double ratioProtein = Math.round((((double) totalProtein / (double) targetUser.getBaseNutrition().getProtein()) * 100.0) * 10.0) / 10.0;
double ratioFat = Math.round((((double) totalFat / (double) targetUser.getBaseNutrition().getFat()) * 100.0) * 10.0) / 10.0;

return ResponseNutritionSumByDateDto.of(userId, checkDate, nutritionSumType, totalKcal,totalCarbohydrate, totalProtein, totalFat, ratioKcal, ratioCarbohydrate, ratioProtein, ratioFat);
return ResponseNutritionSumByDateDto.of(userId, checkDate, nutritionSumType, totalKcal, totalCarbohydrate, totalProtein, totalFat, ratioKcal, ratioCarbohydrate, ratioProtein, ratioFat);
}

private void validateUser(Long userId) {
Expand All @@ -222,6 +278,36 @@ private void validateFavoriteFood(Long favoriteFoodId) {
throw new FavoriteException(ResponseCode.FAVORITE_NOT_FOUND);
}

// 1주일동안 먹은 음식들의 영양성분 총합을 요일을 Key로 한 Map을 통해 반환
private HashMap<LocalDate, List<BaseNutrition>> getNutritionSumByDateMap(Long userId, LocalDate startDate, LocalDate endDate) {
HashMap<LocalDate, List<BaseNutrition>> maps = new HashMap<>();
List<Food> foodList = foodRepository.findAllByUserIdAndDateBetween(userId, startDate, endDate);
for (Food food : foodList) {
if (maps.containsKey(food.getDate())) {
maps.get(food.getDate()).add(food.getBaseNutrition());
} else {
List<BaseNutrition> baseNutritions = new ArrayList<>();
baseNutritions.add(food.getBaseNutrition());
maps.put(food.getDate(), baseNutritions);
}
}
return maps;
}

// 영양성분 총합 대비 기준섭취량 비율을 계산하여 성분별 식습관 점수를 반환
private double calculateNutriRatioAndScore(double total, double standard, int type) {
double ratio = Math.round(((total / standard) * 100.0) * 10.0) / 10.0;
if (type == 0) { // 단백질
if (ratio < 100.0) return ratio;
else if (ratio <= 150) return 100;
else return (-2 * ratio + 400 < 0) ? 0 : (-2 * ratio + 400);
} else { // 칼탄지
double gradient = 1.11; // (9분의 10)
if (ratio < 90.0) return ratio * gradient;
else if (ratio <= 110) return 100;
else return (-gradient * (ratio - 200) < 0) ? 0 : (-gradient * (ratio - 200));
}
}

/**
* 메서드 구현 유의사항
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public enum ResponseCode {
USER_NAME_ALREADY_EXIST(HttpStatus.CONFLICT, false, "이미 존재하는 닉네임입니다."),
FOLLOWED_ALREADY(HttpStatus.CONFLICT, false, "이미 팔로우한 사용자입니다."),
UNFOLLOWED_ALREADY(HttpStatus.CONFLICT, false, "이미 언팔로우한 사용자입니다."),
FOOD_ALREADY_EXIST(HttpStatus.CONFLICT, false, "이미 즐겨찾기에 존재하는 음식입니다."),
FAVORITE_ALREADY_EXIST(HttpStatus.CONFLICT, false, "이미 즐겨찾기에 존재하는 음식입니다."),

// 500 Internal Server Error
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, false, "서버에 오류가 발생하였습니다."),
Expand Down
46 changes: 46 additions & 0 deletions src/test/java/com/diareat/diareat/service/FoodServiceTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import com.diareat.diareat.food.service.FoodService;
import com.diareat.diareat.user.domain.BaseNutrition;
import com.diareat.diareat.user.domain.User;
import com.diareat.diareat.user.dto.ResponseRankUserDto;
import com.diareat.diareat.user.repository.FollowRepository;
import com.diareat.diareat.user.repository.UserRepository;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
Expand All @@ -23,6 +25,7 @@

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
Expand All @@ -43,6 +46,9 @@ class FoodServiceTest {
@Mock
UserRepository userRepository;

@Mock
FollowRepository followRepository;

@DisplayName("음식 정보 저장")
@Test
void testSaveAndGetFood() { // 음식 정보 저장 및 해당 날짜 음식 리스트 불러오기
Expand Down Expand Up @@ -309,4 +315,44 @@ void getWorst3FoodsTest() {
assertEquals("Food4", top3Foods.get(1).getName());
assertEquals("Food5", top3Foods.get(2).getName());
}

@Test
void getUserRankByWeek() {
// given
User user1 = User.createUser("testUser1", "testImage","testPassword", 180, 80, 0, 18, BaseNutrition.createNutrition(1000,100,100,100));
User user2 = User.createUser("testUser2", "testImage","testPassword", 180, 80, 0, 18, BaseNutrition.createNutrition(1000,100,100,100));
user1.setId(1L);
user2.setId(2L);

Food food1 = Food.createFood( "Food1", user1, BaseNutrition.createNutrition(1000, 100 ,100, 100));
Food food2 = Food.createFood( "Food2", user2, BaseNutrition.createNutrition(2000, 110 ,50, 90));

given(userRepository.findById(user1.getId())).willReturn(Optional.of(user1));
given(followRepository.findAllByFromUser(user1.getId())).willReturn(List.of(user2));
given(foodRepository.findAllByUserIdAndDateBetween(eq(1L), any(LocalDate.class), any(LocalDate.class))).willReturn(List.of(food1));
given(foodRepository.findAllByUserIdAndDateBetween(eq(2L), any(LocalDate.class), any(LocalDate.class))).willReturn(List.of(food2));

// when
List<ResponseRankUserDto> response = foodService.getUserRankByWeek(user1.getId());

// then
assertEquals(2, response.size());
assertEquals("testUser1", response.get(0).getName());
assertEquals("testUser2", response.get(1).getName());
assertEquals(100, response.get(0).getCalorieScore());
assertEquals(100, response.get(0).getCarbohydrateScore());
assertEquals(100, response.get(0).getProteinScore());
assertEquals(100, response.get(0).getFatScore());
assertEquals(400, response.get(0).getTotalScore());

assertEquals(0, response.get(1).getCalorieScore());
assertEquals(100, response.get(1).getCarbohydrateScore());
assertEquals(50, response.get(1).getProteinScore());
assertEquals(100, response.get(1).getFatScore());
assertEquals(250, response.get(1).getTotalScore());
verify(userRepository, times(1)).findById(user1.getId());
verify(followRepository, times(1)).findAllByFromUser(user1.getId());
verify(foodRepository, times(1)).findAllByUserIdAndDateBetween(eq(1L), any(LocalDate.class), any(LocalDate.class));
verify(foodRepository, times(1)).findAllByUserIdAndDateBetween(eq(2L), any(LocalDate.class), any(LocalDate.class));
}
}