Skip to content

Commit 199ea22

Browse files
committed
✨ feat: AI 메뉴 추천 FastAPI와 연동
1 parent 73d1c0b commit 199ea22

File tree

13 files changed

+157
-31
lines changed

13 files changed

+157
-31
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package com.example.Centralthon.domain.menu.entity.enums;
2+
3+
import com.fasterxml.jackson.annotation.JsonCreator;
4+
import com.fasterxml.jackson.annotation.JsonValue;
5+
import lombok.Getter;
6+
import lombok.RequiredArgsConstructor;
7+
8+
@Getter
9+
@RequiredArgsConstructor
10+
public enum Concept {
11+
DIET("diet"),
12+
KETO("keto"),
13+
LOW_SODIUM("low_sodium"),
14+
GLYCEMIC("glycemic"),
15+
BULKING("bulking");
16+
17+
private final String value;
18+
19+
@JsonValue
20+
public String getValue(){
21+
return value;
22+
}
23+
24+
@JsonCreator
25+
public static Concept fromValue(String value){
26+
for (Concept concept : Concept.values()){
27+
if (concept.getValue().equals(value)){
28+
return concept;
29+
}
30+
}
31+
throw new IllegalArgumentException();
32+
}
33+
}

src/main/java/com/example/Centralthon/domain/menu/service/MenuService.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import com.example.Centralthon.domain.menu.web.dto.*;
44
import com.example.Centralthon.domain.menu.web.dto.NearbyMenusRes;
55
import com.example.Centralthon.domain.menu.web.dto.StoresByMenuRes;
6+
import com.example.Centralthon.domain.menu.web.dto.GetRecommendedMenusReq;
67
import com.example.Centralthon.global.external.ai.web.dto.GetTipReq;
78
import com.example.Centralthon.global.external.ai.web.dto.GetTipRes;
89

@@ -15,5 +16,7 @@ public interface MenuService {
1516

1617
List<MenuDetailsRes> details(MenuIdsReq menus);
1718

19+
List<NearbyMenusRes> getRecommendedMenus(GetRecommendedMenusReq getRecommendedMenusReq);
20+
1821
List<GetTipRes> getTips(GetTipReq getTipReq);
1922
}

src/main/java/com/example/Centralthon/domain/menu/service/MenuServiceImpl.java

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44
import com.example.Centralthon.domain.menu.exception.MenuNotFoundException;
55
import com.example.Centralthon.domain.menu.repository.MenuRepository;
66
import com.example.Centralthon.domain.menu.web.dto.*;
7-
import com.example.Centralthon.domain.store.entity.Store;
7+
import com.example.Centralthon.domain.menu.web.dto.GetRecommendedMenusReq;
88
import com.example.Centralthon.global.external.ai.service.AiService;
9-
import com.example.Centralthon.global.external.ai.service.AiServiceImpl;
9+
import com.example.Centralthon.global.external.ai.web.dto.GetMenusByConceptReq;
10+
import com.example.Centralthon.global.external.ai.web.dto.GetMenusByConceptRes;
1011
import com.example.Centralthon.global.external.ai.web.dto.GetTipReq;
1112
import com.example.Centralthon.global.external.ai.web.dto.GetTipRes;
1213
import com.example.Centralthon.global.external.exception.AiCommunicationFailedException;
@@ -19,14 +20,8 @@
1920

2021
import java.time.LocalDateTime;
2122
import java.util.*;
22-
import java.util.stream.Collectors;
23-
import java.util.Comparator;
24-
import java.util.LinkedHashMap;
25-
import java.util.List;
26-
import java.util.Map;
2723

2824
import static com.example.Centralthon.global.util.geo.GeoUtils.calculateBoundingBox;
29-
import static com.example.Centralthon.global.util.geo.GeoUtils.calculateDistance;
3025

3126
@Service
3227
@Slf4j
@@ -97,6 +92,34 @@ public List<MenuDetailsRes> details(MenuIdsReq menus) {
9792
.toList();
9893
}
9994

95+
@Override
96+
public List<NearbyMenusRes> getRecommendedMenus(GetRecommendedMenusReq getRecommendedMenusReq){
97+
// 2km 이내 메뉴 조회
98+
List<Menu> menus = findMenusWithinRadius(getRecommendedMenusReq.getLatitude(), getRecommendedMenusReq.getLongitude());
99+
100+
// 중복 제거한 메뉴 목록
101+
Map<String, Menu> uniqueMenus = new LinkedHashMap<>();
102+
for (Menu menu : menus) {
103+
uniqueMenus.putIfAbsent(menu.getName(), menu);
104+
}
105+
106+
List<String> menuList = new ArrayList<>(uniqueMenus.keySet());
107+
List<GetMenusByConceptRes> getMenusByConceptResList = aiService.getMenuByConceptFromAi(
108+
GetMenusByConceptReq.builder()
109+
.concept(getRecommendedMenusReq.getConcept())
110+
.count(getRecommendedMenusReq.getCount())
111+
.items(menuList)
112+
.build()
113+
);
114+
115+
return getMenusByConceptResList.stream()
116+
.map(res -> {
117+
Menu matchedMenu = uniqueMenus.get(res.inputMenu());
118+
return NearbyMenusRes.from(matchedMenu);
119+
})
120+
.toList();
121+
}
122+
100123
@Override
101124
public List<GetTipRes> getTips(GetTipReq getTipReq) {
102125
try{
@@ -107,4 +130,6 @@ public List<GetTipRes> getTips(GetTipReq getTipReq) {
107130
}
108131
}
109132

133+
134+
110135
}

src/main/java/com/example/Centralthon/domain/menu/web/controller/MenuController.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,21 @@
33
import com.example.Centralthon.domain.menu.service.MenuService;
44

55
import com.example.Centralthon.domain.menu.web.dto.*;
6-
import com.example.Centralthon.domain.order.web.controller.OrderApi;
6+
import com.example.Centralthon.domain.menu.web.dto.GetRecommendedMenusReq;
77
import com.example.Centralthon.global.external.ai.web.dto.GetTipReq;
88
import com.example.Centralthon.global.external.ai.web.dto.GetTipRes;
99
import com.example.Centralthon.global.response.SuccessResponse;
1010
import jakarta.validation.Valid;
1111

1212
import com.example.Centralthon.domain.menu.web.dto.NearbyMenusRes;
1313
import com.example.Centralthon.domain.menu.web.dto.StoresByMenuRes;
14-
import com.example.Centralthon.global.response.SuccessResponse;
1514

1615
import lombok.RequiredArgsConstructor;
1716
import org.springframework.http.HttpStatus;
1817
import org.springframework.http.ResponseEntity;
1918
import org.springframework.web.bind.annotation.*;
2019

2120
import java.util.List;
22-
import java.util.Map;
2321

2422
@RestController
2523
@RequestMapping("/api/menus")
@@ -61,6 +59,13 @@ public ResponseEntity<SuccessResponse<List<MenuDetailsRes>>> details(@RequestBod
6159
return ResponseEntity.status(HttpStatus.OK).body(SuccessResponse.from(menuList));
6260
}
6361

62+
// 컨셉별 메뉴 추천
63+
@PostMapping("/recommend")
64+
public ResponseEntity<SuccessResponse<List<NearbyMenusRes>>> recommend(@RequestBody @Valid GetRecommendedMenusReq getRecommendedMenusReq){
65+
List<NearbyMenusRes> menusList = menuService.getRecommendedMenus(getRecommendedMenusReq);
66+
return ResponseEntity.status(HttpStatus.OK).body(SuccessResponse.from(menusList));
67+
}
68+
6469
// 알뜰 반찬 팁 조회
6570
@PostMapping("/tips")
6671
public ResponseEntity<SuccessResponse<List<GetTipRes>>> getTips(@RequestBody @Valid GetTipReq getTipReq){
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package com.example.Centralthon.domain.menu.web.dto;
2+
3+
import com.example.Centralthon.domain.menu.entity.enums.Concept;
4+
import jakarta.validation.constraints.*;
5+
import lombok.Getter;
6+
import lombok.Setter;
7+
8+
@Getter
9+
@Setter
10+
public class GetRecommendedMenusReq {
11+
@DecimalMin(value = "-90.0", message = "위도는 -90 이상이어야 합니다.")
12+
@DecimalMax(value = "90.0", message = "위도는 90 이하이어야 합니다.")
13+
@NotNull(message = "위도는 필수값입니다.")
14+
double latitude;
15+
16+
@DecimalMin(value = "-180.0", message = "경도는 -180 이상이어야 합니다.")
17+
@DecimalMax(value = "180.0", message = "경도는 180 이하이어야 합니다.")
18+
@NotNull(message = "경도는 필수값입니다.")
19+
double longitude;
20+
21+
@NotNull(message = "컨셉은 필수 값입니다.")
22+
Concept concept;
23+
24+
@Min(value = 1, message = "최소 1개 이상 조회해야 합니다.")
25+
@Max(value = 50, message = "최대 50개까지 조회할 수 있습니다.")
26+
@NotNull(message = "숫자는 필수 값입니다.")
27+
int count; // 몇 개 조회할지
28+
}

src/main/java/com/example/Centralthon/global/exception/GlobalExceptionHandler.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public ResponseEntity<ErrorResponse<?>> handleBindException(BindException e) {
4242
return ResponseEntity.status(errorREsponse.getHttpStatus()).body(errorREsponse);
4343
}
4444

45-
//ReqeustBody 등으로 전달 받은 JSON 바디의 파싱이 실패 했을 때
45+
// ReqeustBody 등으로 전달 받은 JSON 바디의 파싱이 실패 했을 때
4646
@ExceptionHandler(HttpMessageNotReadableException.class)
4747
public ResponseEntity<ErrorResponse<?>> handleHttpMessageNotReadableException(HttpMessageNotReadableException e) {
4848
log.error("HttpMessageNotReadableException : {}", e.getMessage(), e);
@@ -66,7 +66,7 @@ public ResponseEntity<ErrorResponse<?>> handleMissingServletRequestPartException
6666
return ResponseEntity.status(errorResponse.getHttpStatus()).body(errorResponse);
6767
}
6868

69-
//지원하지 않는 HTTP 메소드를 호출할 경우
69+
// 지원하지 않는 HTTP 메소드를 호출할 경우
7070
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
7171
public ResponseEntity<ErrorResponse<?>> handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) {
7272
log.error("HttpRequestMethodNotSupportedException : {}", e.getMessage(), e);
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
package com.example.Centralthon.global.external.ai.service;
22

3-
import com.example.Centralthon.global.external.ai.web.dto.GetMenusByThemeReq;
4-
import com.example.Centralthon.global.external.ai.web.dto.GetMenusByThemeRes;
3+
import com.example.Centralthon.global.external.ai.web.dto.GetMenusByConceptReq;
4+
import com.example.Centralthon.global.external.ai.web.dto.GetMenusByConceptRes;
55
import com.example.Centralthon.global.external.ai.web.dto.GetTipReq;
66
import com.example.Centralthon.global.external.ai.web.dto.GetTipRes;
77

88
import java.util.List;
99

1010
public interface AiService {
1111
List<GetTipRes> getTipFromAi(GetTipReq getTipReq);
12-
GetMenusByThemeRes getMenuByThemeFromAi(GetMenusByThemeReq getMenuByThemeReq);
12+
List<GetMenusByConceptRes> getMenuByConceptFromAi(GetMenusByConceptReq getMenuByThemeReq);
1313
}

src/main/java/com/example/Centralthon/global/external/ai/service/AiServiceImpl.java

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
package com.example.Centralthon.global.external.ai.service;
22

3-
import com.example.Centralthon.global.external.ai.web.dto.GetMenusByThemeReq;
4-
import com.example.Centralthon.global.external.ai.web.dto.GetMenusByThemeRes;
5-
import com.example.Centralthon.global.external.ai.web.dto.GetTipReq;
6-
import com.example.Centralthon.global.external.ai.web.dto.GetTipRes;
3+
import com.example.Centralthon.global.external.ai.web.dto.*;
74
import com.example.Centralthon.global.external.response.ExternalResponse;
85
import lombok.RequiredArgsConstructor;
96
import org.springframework.beans.factory.annotation.Value;
@@ -39,7 +36,14 @@ public List<GetTipRes> getTipFromAi(GetTipReq getTipReq) {
3936
}
4037

4138
@Override
42-
public GetMenusByThemeRes getMenuByThemeFromAi(GetMenusByThemeReq getMenuByThemeReq) {
43-
return null;
39+
public List<GetMenusByConceptRes> getMenuByConceptFromAi(GetMenusByConceptReq getMenuByThemeReq) {
40+
String recommendUrl = baseUrl + "menus/recommend";
41+
ResponseEntity<ExternalResponse<GetMenusByConceptWrapper>> response = restTemplate.exchange(
42+
recommendUrl,
43+
HttpMethod.POST,
44+
new HttpEntity<>(getMenuByThemeReq),
45+
new ParameterizedTypeReference<ExternalResponse<GetMenusByConceptWrapper>>() {}
46+
);
47+
return response.getBody().data().items();
4448
}
4549
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.example.Centralthon.global.external.ai.web.dto;
2+
3+
import com.example.Centralthon.domain.menu.entity.enums.Concept;
4+
import lombok.Builder;
5+
import lombok.Getter;
6+
import lombok.Setter;
7+
8+
import java.util.List;
9+
10+
@Getter
11+
@Setter
12+
@Builder
13+
public class GetMenusByConceptReq {
14+
Concept concept;
15+
int count;
16+
List<String> items;
17+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.example.Centralthon.global.external.ai.web.dto;
2+
3+
import com.fasterxml.jackson.annotation.JsonProperty;
4+
5+
public record GetMenusByConceptRes(
6+
@JsonProperty("input_menu") String inputMenu,
7+
@JsonProperty("matched_name") String matchedName,
8+
float similarity,
9+
float suitability
10+
) {
11+
}

0 commit comments

Comments
 (0)