Skip to content

Commit

Permalink
✨ [Feat] 당 성분이 비슷한 음료 추천 기능 구현 (#70)
Browse files Browse the repository at this point in the history
  • Loading branch information
soochangoforit authored Jul 15, 2023
1 parent c12dbc7 commit ecc267e
Show file tree
Hide file tree
Showing 10 changed files with 222 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,20 @@
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.depromeet.oversweet.annotation.SecurityExclusion;
import com.depromeet.oversweet.drink.dto.response.DrinkDailySugarStatisticsResponse;
import com.depromeet.oversweet.drink.dto.response.DrinkDetailInfoResponse;
import com.depromeet.oversweet.drink.dto.response.DrinkRecommendResponse;
import com.depromeet.oversweet.drink.dto.response.DrinkRedisInfo;
import com.depromeet.oversweet.drink.dto.response.DrinkWeeklySugarStatisticsResponse;
import com.depromeet.oversweet.drink.service.DrinkDailyStatisticsService;
import com.depromeet.oversweet.drink.service.DrinkDetailSearchService;
import com.depromeet.oversweet.drink.service.DrinkRecommendService;
import com.depromeet.oversweet.drink.service.DrinkRedisService;
import com.depromeet.oversweet.drink.service.DrinkWeeklyStatisticsService;
import com.depromeet.oversweet.response.DataResponse;
Expand All @@ -47,6 +50,7 @@ public class DrinkController {
private final DrinkWeeklyStatisticsService drinkWeeklyStatisticsService;
private final DrinkDetailSearchService drinkDetailSearchService;
private final DrinkRedisService drinkRedisService;
private final DrinkRecommendService drinkRecommendService;

/**
* 유저 하루(데일리) 먹은 당 통계 및 음료 목록 조회.
Expand Down Expand Up @@ -107,4 +111,17 @@ public ResponseEntity<DataResponse<List<DrinkRedisInfo>>> getOrCreateDrinkAtRedi
return ResponseEntity.ok()
.body(DataResponse.of(OK, "레디스에 저장된 음료 목록 조회 성공", drinks));
}


/**
* 음료 사이즈 기준으로 당 성분이 비슷한 음료 추천
*/
@Operation(summary = "음료 사이즈 기준으로 당 성분이 비슷한 음료 추천", description = "음료 사이즈 기준으로 당 성분이 비슷한 음료 추천")
@ApiResponses(@ApiResponse(responseCode = "200", description = "음료 사이즈 기준으로 당 성분이 비슷한 음료 추천"))
@SecurityExclusion
@GetMapping("/recommend/{drinkId}")
public ResponseEntity<DataResponse<DrinkRecommendResponse>> recommendDrink(@PathVariable Long drinkId) {
DrinkRecommendResponse response = drinkRecommendService.recommendDrink(drinkId);
return ResponseEntity.ok().body(DataResponse.of(OK, "음료 사이즈 기준으로 당 성분이 비슷한 음료 추천 성공", response));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.depromeet.oversweet.drink.dto.response;

import com.depromeet.oversweet.drink.vo.SugarDiffState;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;

@Getter
public class DrinkRecommendInfo {

@Schema(description = "음료 id")
private final Long id;

@Schema(description = "음료 이름")
private final String name;

@Schema(description = "음료 이미지 url")
private final String imageUrl;

@Schema(description = "당 성분 차이")
private final int sugarDifference;

@Schema(description = "당 성분 차이 상태")
private final SugarDiffState status;

public DrinkRecommendInfo(final Long id, final String name, final String imageUrl, final int sugarDifference, final SugarDiffState status) {
this.id = id;
this.name = name;
this.imageUrl = imageUrl;
this.sugarDifference = sugarDifference;
this.status = status;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.depromeet.oversweet.drink.dto.response;

import static java.util.stream.Collectors.toList;

import java.util.List;

import com.depromeet.oversweet.domain.drink.entity.DrinkEntity;
import com.depromeet.oversweet.drink.vo.SugarDiffState;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;

@Getter
public class DrinkRecommendResponse {

@Schema(description = "추천 음료 리스트")
private final List<DrinkRecommendInfo> drinks;

private DrinkRecommendResponse(final List<DrinkRecommendInfo> drinks) {
this.drinks = drinks;
}

public static DrinkRecommendResponse from(int mainSugar, List<DrinkEntity> recommendDrinks) {
List<DrinkRecommendInfo> recommendInfo = recommendDrinks.stream()
.map(drink -> {
int diff = Math.abs(mainSugar - drink.getSugar());
SugarDiffState state = SugarDiffState.from(mainSugar, drink.getSugar());
return new DrinkRecommendInfo(drink.getId(), drink.getName(), drink.getImageUrl(), diff, state);
})
.collect(toList());

return new DrinkRecommendResponse(recommendInfo);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.depromeet.oversweet.drink.service;

import java.util.List;

import org.springframework.stereotype.Service;

import com.depromeet.oversweet.domain.drink.entity.DrinkEntity;
import com.depromeet.oversweet.domain.drink.repository.FindDrinkRepository;
import com.depromeet.oversweet.drink.dto.response.DrinkRecommendResponse;

import lombok.RequiredArgsConstructor;

/**
* 음료 추천 Service
*/
@Service
@RequiredArgsConstructor
public class DrinkRecommendService {

private final FindDrinkRepository findDrinkRepository;

/**
* 당 성분이 비슷한 음료 추천
*
* @param drinkId 음료 id
* @return 추천 음료 리스트
*/
public DrinkRecommendResponse recommendDrink(final Long drinkId) {
// 음료 조회
DrinkEntity drinkEntity = findDrinkRepository.findDrinkById(drinkId);

// 음료 당 성분 기준 +-5 범위의 당 추출
int minSugar = drinkEntity.getMinSugarForRecommend();
int maxSugar = drinkEntity.getMaxSugarForRecommend();

// 조회된 음료의 프랜차이즈 and 당 추출이 +-5 범위 내의 음료 조회
List<DrinkEntity> recommendDrinks = findDrinkRepository.findDrinkBetweenSugar(minSugar, maxSugar);

return DrinkRecommendResponse.from(drinkEntity.getSugar(), recommendDrinks);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.depromeet.oversweet.drink.vo;

public enum SugarDiffState {

MINUS, PLUS, SAME;


public static SugarDiffState from(final int mainSugar, final int sugar) {
if (mainSugar > sugar) {
return MINUS;
}

if (mainSugar < sugar) {
return PLUS;
}

return SAME;
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
package com.depromeet.oversweet.security.config;

import com.depromeet.oversweet.security.filter.ExceptionFilter;
import com.depromeet.oversweet.security.filter.JwtAuthenticationFilter;
import com.depromeet.oversweet.security.handler.JwtAuthenticationEntryPointHandler;
import com.depromeet.oversweet.security.jwt.JwtTokenProvider;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
Expand All @@ -18,6 +12,14 @@
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import com.depromeet.oversweet.security.filter.ExceptionFilter;
import com.depromeet.oversweet.security.filter.JwtAuthenticationFilter;
import com.depromeet.oversweet.security.handler.JwtAuthenticationEntryPointHandler;
import com.depromeet.oversweet.security.jwt.JwtTokenProvider;
import com.fasterxml.jackson.databind.ObjectMapper;

import lombok.RequiredArgsConstructor;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,20 @@
import com.depromeet.oversweet.domain.common.entity.BaseTimeEntity;
import com.depromeet.oversweet.domain.drink.enums.DrinkCategory;
import com.depromeet.oversweet.domain.franchise.entity.FranchiseEntity;
import jakarta.persistence.*;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;
import jakarta.persistence.ForeignKey;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Index;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
Expand Down Expand Up @@ -62,4 +75,12 @@ public DrinkEntity(final Long id, final String name, final FranchiseEntity franc
this.category = category;
this.isMinimum = isMinimum;
}

public int getMinSugarForRecommend() {
return this.sugar - 5;
}

public int getMaxSugarForRecommend() {
return this.sugar + 5;
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
package com.depromeet.oversweet.domain.drink.repository;

import com.depromeet.oversweet.domain.drink.dto.DrinkInfoWithScrapStatus;
import com.depromeet.oversweet.domain.drink.dto.DrinkSimpleInfo;
import com.depromeet.oversweet.domain.drink.entity.DrinkEntity;
import java.util.List;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.List;
import com.depromeet.oversweet.domain.drink.dto.DrinkInfoWithScrapStatus;
import com.depromeet.oversweet.domain.drink.dto.DrinkSimpleInfo;
import com.depromeet.oversweet.domain.drink.entity.DrinkEntity;

public interface DrinkJpaRepository extends JpaRepository<DrinkEntity, Long> {

Expand All @@ -22,4 +23,8 @@ public interface DrinkJpaRepository extends JpaRepository<DrinkEntity, Long> {

@Query(value = "select d.id, d.name from drink d", nativeQuery = true)
List<DrinkSimpleInfo> findAllDrinkSimpleInfo();

List<DrinkEntity> findTop30BySugarBetween(int minSugar, int maxSugar);


}
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package com.depromeet.oversweet.domain.drink.repository;

import java.util.List;

import com.depromeet.oversweet.domain.drink.dto.DrinkInfoWithScrapStatus;
import com.depromeet.oversweet.domain.drink.entity.DrinkEntity;

import java.util.List;

/**
* 음료 정보 조회 Interface
*/
Expand All @@ -14,4 +14,6 @@ public interface FindDrinkRepository {
List<DrinkInfoWithScrapStatus> findDrinkDetail(final Long memberId, final Long franchiseId, final String drinkName);

void checkDrinkExist(final Long franchiseId, final String drinkName);

List<DrinkEntity> findDrinkBetweenSugar(int minSugar, int maxSugar);
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
package com.depromeet.oversweet.domain.drink.repository;

import com.depromeet.oversweet.domain.drink.dto.DrinkInfoWithScrapStatus;
import com.depromeet.oversweet.domain.drink.entity.DrinkEntity;
import com.depromeet.oversweet.exception.drink.NotFoundDrinkException;
import lombok.RequiredArgsConstructor;
import static com.depromeet.oversweet.exception.ErrorCode.NOT_FOUND_DRINK;

import java.util.Collections;
import java.util.List;

import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import com.depromeet.oversweet.domain.drink.dto.DrinkInfoWithScrapStatus;
import com.depromeet.oversweet.domain.drink.entity.DrinkEntity;
import com.depromeet.oversweet.exception.drink.NotFoundDrinkException;

import static com.depromeet.oversweet.exception.ErrorCode.NOT_FOUND_DRINK;
import lombok.RequiredArgsConstructor;

/**
* 음료 정보 조회 Interface 구현체
Expand Down Expand Up @@ -38,27 +41,48 @@ public DrinkEntity findDrinkById(final Long drinkId) {
* 프랜차이즈 정보 및 즐겨 찾기 여부를 함께 조회한다.
*
* @param franchiseId 상세 보기를 원하는 음료의 프랜차이즈 ID
* @param drinkName 상세 보기를 원하는 음료의 이름
* @param drinkName 상세 보기를 원하는 음료의 이름
*/
@Override
@Transactional(readOnly = true)
public List<DrinkInfoWithScrapStatus> findDrinkDetail(final Long memberId, final Long franchiseId, final String drinkName) {
return drinkJpaRepository.findDrinkWithBookmarkStatus(memberId, franchiseId, drinkName);
return drinkJpaRepository.findDrinkWithBookmarkStatus(memberId, franchiseId, drinkName);
}

/**
* 음료가 존재하는지 확인
*
* @param franchiseId 음료의 프랜차이즈 ID
* @param drinkName 음료 이름
* @param drinkName 음료 이름
*/
@Override
@Transactional(readOnly = true)
public void checkDrinkExist(final Long franchiseId, final String drinkName) {
if (drinkJpaRepository.findByFranchiseIdAndName(franchiseId, drinkName)
.isEmpty()){
.isEmpty()) {
throw new NotFoundDrinkException(NOT_FOUND_DRINK);
}
}

/**
* 당 성분이 비슷한 음료 추천
* top 30을 찾고 랜덤으로 섞어서 5개를 추천한다.
*
* @param minSugar 최소 당 성분
* @param maxSugar 최대 당 성분
* @return 당 성분이 비슷한 음료 5개
*/
@Override
@Transactional(readOnly = true)
public List<DrinkEntity> findDrinkBetweenSugar(final int minSugar, final int maxSugar) {
// 비슷한 음료 30개 정도를 조회한고 나서 랜덤으로 섞는다.
List<DrinkEntity> drinks = drinkJpaRepository.findTop30BySugarBetween(minSugar, maxSugar);
Collections.shuffle(drinks);

// 비슷한 음료가 5개보다 적은 경우
int count = Math.min(drinks.size(), 5);

return drinks.subList(0, count);
}

}

0 comments on commit ecc267e

Please sign in to comment.