Skip to content

Commit

Permalink
Merge pull request #43 from CAUSOLDOUTMEN/feature/42-feat-foodservice
Browse files Browse the repository at this point in the history
�Feat: 유저 주간 랭킹 기능 구현 (#42)
  • Loading branch information
synoti21 authored Oct 30, 2023
2 parents c051944 + 113074b commit b6e6d92
Show file tree
Hide file tree
Showing 8 changed files with 225 additions and 16 deletions.
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));
}
}

0 comments on commit b6e6d92

Please sign in to comment.