From 01230a9aefb7853100d2f5ff2deb2566ec0b0e67 Mon Sep 17 00:00:00 2001 From: frombunny Date: Wed, 20 Aug 2025 13:43:32 +0900 Subject: [PATCH 1/7] =?UTF-8?q?:sparkles:=20feat:=20RestTemplateConfig=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/config/RestTemplateConfig.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 src/main/java/com/example/Centralthon/global/config/RestTemplateConfig.java diff --git a/src/main/java/com/example/Centralthon/global/config/RestTemplateConfig.java b/src/main/java/com/example/Centralthon/global/config/RestTemplateConfig.java new file mode 100644 index 0000000..bf28572 --- /dev/null +++ b/src/main/java/com/example/Centralthon/global/config/RestTemplateConfig.java @@ -0,0 +1,15 @@ +package com.example.Centralthon.global.config; + +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class RestTemplateConfig { + + @Bean + public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) { + return restTemplateBuilder.build(); + } +} From b80f9bf010dcffa1be37e49d649e3eeb5c46019b Mon Sep 17 00:00:00 2001 From: frombunny Date: Wed, 20 Aug 2025 13:55:30 +0900 Subject: [PATCH 2/7] =?UTF-8?q?:sparkles:=20feat:=20AiClient=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20=EB=B0=8F=20Ai=20=EC=84=9C=EB=B2=84=EC=99=80=20?= =?UTF-8?q?=ED=86=B5=EC=8B=A0=20=EC=8B=9C=20=EC=98=88=EC=99=B8=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/config/RestTemplateConfig.java | 2 +- .../global/external/ai/client/AiClient.java | 21 +++++++++++++++++++ .../AiCommunicationFailedException.java | 7 +++++++ .../external/exception/AiErrorCode.java | 15 +++++++++++++ 4 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/example/Centralthon/global/external/ai/client/AiClient.java create mode 100644 src/main/java/com/example/Centralthon/global/external/exception/AiCommunicationFailedException.java create mode 100644 src/main/java/com/example/Centralthon/global/external/exception/AiErrorCode.java diff --git a/src/main/java/com/example/Centralthon/global/config/RestTemplateConfig.java b/src/main/java/com/example/Centralthon/global/config/RestTemplateConfig.java index bf28572..99c3f33 100644 --- a/src/main/java/com/example/Centralthon/global/config/RestTemplateConfig.java +++ b/src/main/java/com/example/Centralthon/global/config/RestTemplateConfig.java @@ -12,4 +12,4 @@ public class RestTemplateConfig { public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) { return restTemplateBuilder.build(); } -} +} \ No newline at end of file diff --git a/src/main/java/com/example/Centralthon/global/external/ai/client/AiClient.java b/src/main/java/com/example/Centralthon/global/external/ai/client/AiClient.java new file mode 100644 index 0000000..deeae33 --- /dev/null +++ b/src/main/java/com/example/Centralthon/global/external/ai/client/AiClient.java @@ -0,0 +1,21 @@ +package com.example.Centralthon.global.external.ai.client; + +import com.example.Centralthon.global.external.exception.AiCommunicationFailedException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +@Component +@RequiredArgsConstructor +public class AiClient { + private final RestTemplate restTemplate; + + public T postForObject(String url, Object request, Class responseType) { + try{ + return restTemplate.postForObject(url, request, responseType); + } catch (Exception e){ + throw new AiCommunicationFailedException(); + } + + } +} diff --git a/src/main/java/com/example/Centralthon/global/external/exception/AiCommunicationFailedException.java b/src/main/java/com/example/Centralthon/global/external/exception/AiCommunicationFailedException.java new file mode 100644 index 0000000..e79aa76 --- /dev/null +++ b/src/main/java/com/example/Centralthon/global/external/exception/AiCommunicationFailedException.java @@ -0,0 +1,7 @@ +package com.example.Centralthon.global.external.exception; + +import com.example.Centralthon.global.exception.BaseException; + +public class AiCommunicationFailedException extends BaseException { + public AiCommunicationFailedException() {super(AiErrorCode.AI_COMMUNICATION_FAILED);} +} \ No newline at end of file diff --git a/src/main/java/com/example/Centralthon/global/external/exception/AiErrorCode.java b/src/main/java/com/example/Centralthon/global/external/exception/AiErrorCode.java new file mode 100644 index 0000000..b9422c1 --- /dev/null +++ b/src/main/java/com/example/Centralthon/global/external/exception/AiErrorCode.java @@ -0,0 +1,15 @@ +package com.example.Centralthon.global.external.exception; + +import com.example.Centralthon.global.response.code.BaseResponseCode; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum AiErrorCode implements BaseResponseCode { + AI_COMMUNICATION_FAILED("AI_500", 500, "AI 서버 호출에 실패했습니다."); + + private final String code; + private final int httpStatus; + private final String message; +} From ea4f69dccbbfd01b3efa00349edbd196c91af561 Mon Sep 17 00:00:00 2001 From: frombunny Date: Wed, 20 Aug 2025 16:51:31 +0900 Subject: [PATCH 3/7] =?UTF-8?q?:sparkles:=20feat:=20AI=20=EC=84=9C?= =?UTF-8?q?=EB=B2=84=EC=99=80=EC=9D=98=20=ED=86=B5=EC=8B=A0=20=EC=84=9C?= =?UTF-8?q?=EB=B9=84=EC=8A=A4=20=EC=BD=94=EB=93=9C=20=EA=B5=AC=EC=A1=B0=20?= =?UTF-8?q?=EC=84=A4=EA=B3=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/external/ai/service/AiService.java | 11 ++++++++++ .../external/ai/service/AiServiceImpl.java | 21 +++++++++++++++++++ .../ai/web/dto/GetMenusByThemeReq.java | 4 ++++ .../ai/web/dto/GetMenusByThemeRes.java | 4 ++++ .../global/external/ai/web/dto/GetTipReq.java | 4 ++++ .../global/external/ai/web/dto/GetTipRes.java | 4 ++++ 6 files changed, 48 insertions(+) create mode 100644 src/main/java/com/example/Centralthon/global/external/ai/service/AiService.java create mode 100644 src/main/java/com/example/Centralthon/global/external/ai/service/AiServiceImpl.java create mode 100644 src/main/java/com/example/Centralthon/global/external/ai/web/dto/GetMenusByThemeReq.java create mode 100644 src/main/java/com/example/Centralthon/global/external/ai/web/dto/GetMenusByThemeRes.java create mode 100644 src/main/java/com/example/Centralthon/global/external/ai/web/dto/GetTipReq.java create mode 100644 src/main/java/com/example/Centralthon/global/external/ai/web/dto/GetTipRes.java diff --git a/src/main/java/com/example/Centralthon/global/external/ai/service/AiService.java b/src/main/java/com/example/Centralthon/global/external/ai/service/AiService.java new file mode 100644 index 0000000..4dc40ce --- /dev/null +++ b/src/main/java/com/example/Centralthon/global/external/ai/service/AiService.java @@ -0,0 +1,11 @@ +package com.example.Centralthon.global.external.ai.service; + +import com.example.Centralthon.global.external.ai.web.dto.GetMenusByThemeReq; +import com.example.Centralthon.global.external.ai.web.dto.GetMenusByThemeRes; +import com.example.Centralthon.global.external.ai.web.dto.GetTipReq; +import com.example.Centralthon.global.external.ai.web.dto.GetTipRes; + +public interface AiService { + GetTipRes getTip(GetTipReq getTipReq); + GetMenusByThemeRes getMenuByTheme(GetMenusByThemeReq getMenuByThemeReq); +} diff --git a/src/main/java/com/example/Centralthon/global/external/ai/service/AiServiceImpl.java b/src/main/java/com/example/Centralthon/global/external/ai/service/AiServiceImpl.java new file mode 100644 index 0000000..b709e13 --- /dev/null +++ b/src/main/java/com/example/Centralthon/global/external/ai/service/AiServiceImpl.java @@ -0,0 +1,21 @@ +package com.example.Centralthon.global.external.ai.service; + +import com.example.Centralthon.global.external.ai.web.dto.GetMenusByThemeReq; +import com.example.Centralthon.global.external.ai.web.dto.GetMenusByThemeRes; +import com.example.Centralthon.global.external.ai.web.dto.GetTipReq; +import com.example.Centralthon.global.external.ai.web.dto.GetTipRes; +import org.springframework.stereotype.Service; + +@Service +public class AiServiceImpl implements AiService { + + @Override + public GetTipRes getTip(GetTipReq getTipReq) { + return null; + } + + @Override + public GetMenusByThemeRes getMenuByTheme(GetMenusByThemeReq getMenuByThemeReq) { + return null; + } +} diff --git a/src/main/java/com/example/Centralthon/global/external/ai/web/dto/GetMenusByThemeReq.java b/src/main/java/com/example/Centralthon/global/external/ai/web/dto/GetMenusByThemeReq.java new file mode 100644 index 0000000..5fed5de --- /dev/null +++ b/src/main/java/com/example/Centralthon/global/external/ai/web/dto/GetMenusByThemeReq.java @@ -0,0 +1,4 @@ +package com.example.Centralthon.global.external.ai.web.dto; + +public class GetMenusByThemeReq { +} diff --git a/src/main/java/com/example/Centralthon/global/external/ai/web/dto/GetMenusByThemeRes.java b/src/main/java/com/example/Centralthon/global/external/ai/web/dto/GetMenusByThemeRes.java new file mode 100644 index 0000000..23bff4f --- /dev/null +++ b/src/main/java/com/example/Centralthon/global/external/ai/web/dto/GetMenusByThemeRes.java @@ -0,0 +1,4 @@ +package com.example.Centralthon.global.external.ai.web.dto; + +public record GetMenusByThemeRes() { +} diff --git a/src/main/java/com/example/Centralthon/global/external/ai/web/dto/GetTipReq.java b/src/main/java/com/example/Centralthon/global/external/ai/web/dto/GetTipReq.java new file mode 100644 index 0000000..4bccd42 --- /dev/null +++ b/src/main/java/com/example/Centralthon/global/external/ai/web/dto/GetTipReq.java @@ -0,0 +1,4 @@ +package com.example.Centralthon.global.external.ai.web.dto; + +public class GetTipReq { +} diff --git a/src/main/java/com/example/Centralthon/global/external/ai/web/dto/GetTipRes.java b/src/main/java/com/example/Centralthon/global/external/ai/web/dto/GetTipRes.java new file mode 100644 index 0000000..24da81b --- /dev/null +++ b/src/main/java/com/example/Centralthon/global/external/ai/web/dto/GetTipRes.java @@ -0,0 +1,4 @@ +package com.example.Centralthon.global.external.ai.web.dto; + +public record GetTipRes() { +} From 9348a653753151213241422ed3d83ac144ec2ef5 Mon Sep 17 00:00:00 2001 From: frombunny Date: Fri, 22 Aug 2025 02:23:59 +0900 Subject: [PATCH 4/7] =?UTF-8?q?:sparkles:=20feat:=20GetTipReq,=20GetTipRes?= =?UTF-8?q?=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/external/ai/web/dto/GetMenusByThemeRes.java | 5 ++++- .../Centralthon/global/external/ai/web/dto/GetTipReq.java | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/example/Centralthon/global/external/ai/web/dto/GetMenusByThemeRes.java b/src/main/java/com/example/Centralthon/global/external/ai/web/dto/GetMenusByThemeRes.java index 23bff4f..7faaaf6 100644 --- a/src/main/java/com/example/Centralthon/global/external/ai/web/dto/GetMenusByThemeRes.java +++ b/src/main/java/com/example/Centralthon/global/external/ai/web/dto/GetMenusByThemeRes.java @@ -1,4 +1,7 @@ package com.example.Centralthon.global.external.ai.web.dto; -public record GetMenusByThemeRes() { +public record GetMenusByThemeRes( + String title, + String content +) { } diff --git a/src/main/java/com/example/Centralthon/global/external/ai/web/dto/GetTipReq.java b/src/main/java/com/example/Centralthon/global/external/ai/web/dto/GetTipReq.java index 4bccd42..d7f0130 100644 --- a/src/main/java/com/example/Centralthon/global/external/ai/web/dto/GetTipReq.java +++ b/src/main/java/com/example/Centralthon/global/external/ai/web/dto/GetTipReq.java @@ -1,4 +1,7 @@ package com.example.Centralthon.global.external.ai.web.dto; +import java.util.List; + public class GetTipReq { + List items; } From 73d1c0b33259e66062961539e1b5c5021caba7a9 Mon Sep 17 00:00:00 2001 From: frombunny Date: Fri, 22 Aug 2025 03:13:35 +0900 Subject: [PATCH 5/7] =?UTF-8?q?:sparkles:=20feat:=20=EC=95=8C=EB=9C=B0?= =?UTF-8?q?=EB=B0=98=EC=B0=AC=ED=8C=81=20FastAPI=EC=99=80=20=EC=97=B0?= =?UTF-8?q?=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/menu/service/MenuService.java | 5 ++++ .../domain/menu/service/MenuServiceImpl.java | 18 +++++++++++ .../domain/menu/web/controller/MenuApi.java | 4 +++ .../menu/web/controller/MenuController.java | 9 ++++++ .../global/external/ai/service/AiService.java | 6 ++-- .../external/ai/service/AiServiceImpl.java | 30 +++++++++++++++++-- .../ai/web/dto/GetMenusByThemeRes.java | 2 -- .../global/external/ai/web/dto/GetTipReq.java | 7 ++++- .../global/external/ai/web/dto/GetTipRes.java | 5 +++- .../external/response/ExternalResponse.java | 8 +++++ 10 files changed, 85 insertions(+), 9 deletions(-) create mode 100644 src/main/java/com/example/Centralthon/global/external/response/ExternalResponse.java diff --git a/src/main/java/com/example/Centralthon/domain/menu/service/MenuService.java b/src/main/java/com/example/Centralthon/domain/menu/service/MenuService.java index 812fe1b..96d8a22 100644 --- a/src/main/java/com/example/Centralthon/domain/menu/service/MenuService.java +++ b/src/main/java/com/example/Centralthon/domain/menu/service/MenuService.java @@ -3,6 +3,9 @@ import com.example.Centralthon.domain.menu.web.dto.*; import com.example.Centralthon.domain.menu.web.dto.NearbyMenusRes; import com.example.Centralthon.domain.menu.web.dto.StoresByMenuRes; +import com.example.Centralthon.global.external.ai.web.dto.GetTipReq; +import com.example.Centralthon.global.external.ai.web.dto.GetTipRes; + import java.util.List; public interface MenuService { @@ -11,4 +14,6 @@ public interface MenuService { List storesByMenu(String name, double lat, double lng); List details(MenuIdsReq menus); + + List getTips(GetTipReq getTipReq); } diff --git a/src/main/java/com/example/Centralthon/domain/menu/service/MenuServiceImpl.java b/src/main/java/com/example/Centralthon/domain/menu/service/MenuServiceImpl.java index 25eca1e..bae4b2f 100644 --- a/src/main/java/com/example/Centralthon/domain/menu/service/MenuServiceImpl.java +++ b/src/main/java/com/example/Centralthon/domain/menu/service/MenuServiceImpl.java @@ -5,9 +5,15 @@ import com.example.Centralthon.domain.menu.repository.MenuRepository; import com.example.Centralthon.domain.menu.web.dto.*; import com.example.Centralthon.domain.store.entity.Store; +import com.example.Centralthon.global.external.ai.service.AiService; +import com.example.Centralthon.global.external.ai.service.AiServiceImpl; +import com.example.Centralthon.global.external.ai.web.dto.GetTipReq; +import com.example.Centralthon.global.external.ai.web.dto.GetTipRes; +import com.example.Centralthon.global.external.exception.AiCommunicationFailedException; import com.example.Centralthon.global.util.geo.BoundingBox; import com.example.Centralthon.global.util.geo.GeoUtils; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -23,9 +29,11 @@ import static com.example.Centralthon.global.util.geo.GeoUtils.calculateDistance; @Service +@Slf4j @RequiredArgsConstructor public class MenuServiceImpl implements MenuService { private final MenuRepository menuRepository; + private final AiService aiService; // 주어진 위도, 경도를 중심으로 반경 2km 이내의 메뉴 조회 private List findMenusWithinRadius(double latitude, double longitude) { @@ -89,4 +97,14 @@ public List details(MenuIdsReq menus) { .toList(); } + @Override + public List getTips(GetTipReq getTipReq) { + try{ + return aiService.getTipFromAi(getTipReq); + } catch (Exception e){ + log.error("AI 서버 호출 실패 {}", e.getMessage()); + throw new AiCommunicationFailedException(); + } + } + } diff --git a/src/main/java/com/example/Centralthon/domain/menu/web/controller/MenuApi.java b/src/main/java/com/example/Centralthon/domain/menu/web/controller/MenuApi.java index 95c51b4..f6c611c 100644 --- a/src/main/java/com/example/Centralthon/domain/menu/web/controller/MenuApi.java +++ b/src/main/java/com/example/Centralthon/domain/menu/web/controller/MenuApi.java @@ -4,6 +4,8 @@ import com.example.Centralthon.domain.menu.web.dto.MenuIdsReq; import com.example.Centralthon.domain.menu.web.dto.NearbyMenusRes; import com.example.Centralthon.domain.menu.web.dto.StoresByMenuRes; +import com.example.Centralthon.global.external.ai.web.dto.GetTipReq; +import com.example.Centralthon.global.external.ai.web.dto.GetTipRes; import com.example.Centralthon.global.response.SuccessResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -155,4 +157,6 @@ ResponseEntity>> storesByMenu( ) ) ResponseEntity>> details(@RequestBody @Valid MenuIdsReq menus); + + ResponseEntity>> getTips(@RequestBody @Valid GetTipReq getTipReq); } diff --git a/src/main/java/com/example/Centralthon/domain/menu/web/controller/MenuController.java b/src/main/java/com/example/Centralthon/domain/menu/web/controller/MenuController.java index 3598235..23b01f8 100644 --- a/src/main/java/com/example/Centralthon/domain/menu/web/controller/MenuController.java +++ b/src/main/java/com/example/Centralthon/domain/menu/web/controller/MenuController.java @@ -4,6 +4,8 @@ import com.example.Centralthon.domain.menu.web.dto.*; import com.example.Centralthon.domain.order.web.controller.OrderApi; +import com.example.Centralthon.global.external.ai.web.dto.GetTipReq; +import com.example.Centralthon.global.external.ai.web.dto.GetTipRes; import com.example.Centralthon.global.response.SuccessResponse; import jakarta.validation.Valid; @@ -58,4 +60,11 @@ public ResponseEntity>> details(@RequestBod return ResponseEntity.status(HttpStatus.OK).body(SuccessResponse.from(menuList)); } + + // 알뜰 반찬 팁 조회 + @PostMapping("/tips") + public ResponseEntity>> getTips(@RequestBody @Valid GetTipReq getTipReq){ + List tips = menuService.getTips(getTipReq); + return ResponseEntity.status(HttpStatus.OK).body(SuccessResponse.from(tips)); + } } diff --git a/src/main/java/com/example/Centralthon/global/external/ai/service/AiService.java b/src/main/java/com/example/Centralthon/global/external/ai/service/AiService.java index 4dc40ce..d7cb6c2 100644 --- a/src/main/java/com/example/Centralthon/global/external/ai/service/AiService.java +++ b/src/main/java/com/example/Centralthon/global/external/ai/service/AiService.java @@ -5,7 +5,9 @@ import com.example.Centralthon.global.external.ai.web.dto.GetTipReq; import com.example.Centralthon.global.external.ai.web.dto.GetTipRes; +import java.util.List; + public interface AiService { - GetTipRes getTip(GetTipReq getTipReq); - GetMenusByThemeRes getMenuByTheme(GetMenusByThemeReq getMenuByThemeReq); + List getTipFromAi(GetTipReq getTipReq); + GetMenusByThemeRes getMenuByThemeFromAi(GetMenusByThemeReq getMenuByThemeReq); } diff --git a/src/main/java/com/example/Centralthon/global/external/ai/service/AiServiceImpl.java b/src/main/java/com/example/Centralthon/global/external/ai/service/AiServiceImpl.java index b709e13..f513173 100644 --- a/src/main/java/com/example/Centralthon/global/external/ai/service/AiServiceImpl.java +++ b/src/main/java/com/example/Centralthon/global/external/ai/service/AiServiceImpl.java @@ -4,18 +4,42 @@ import com.example.Centralthon.global.external.ai.web.dto.GetMenusByThemeRes; import com.example.Centralthon.global.external.ai.web.dto.GetTipReq; import com.example.Centralthon.global.external.ai.web.dto.GetTipRes; +import com.example.Centralthon.global.external.response.ExternalResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import java.util.List; @Service +@RequiredArgsConstructor public class AiServiceImpl implements AiService { + private final RestTemplate restTemplate; + + @Value("${AI_SERVER_URL}") + private String baseUrl; + @Override - public GetTipRes getTip(GetTipReq getTipReq) { - return null; + public List getTipFromAi(GetTipReq getTipReq) { + String tipUrl = baseUrl + "tip"; + ResponseEntity>> response = restTemplate.exchange( + tipUrl, + HttpMethod.POST, + new HttpEntity<>(getTipReq), + new ParameterizedTypeReference>>() {} + ); + + return response.getBody().data(); } @Override - public GetMenusByThemeRes getMenuByTheme(GetMenusByThemeReq getMenuByThemeReq) { + public GetMenusByThemeRes getMenuByThemeFromAi(GetMenusByThemeReq getMenuByThemeReq) { return null; } } diff --git a/src/main/java/com/example/Centralthon/global/external/ai/web/dto/GetMenusByThemeRes.java b/src/main/java/com/example/Centralthon/global/external/ai/web/dto/GetMenusByThemeRes.java index 7faaaf6..7ebf31a 100644 --- a/src/main/java/com/example/Centralthon/global/external/ai/web/dto/GetMenusByThemeRes.java +++ b/src/main/java/com/example/Centralthon/global/external/ai/web/dto/GetMenusByThemeRes.java @@ -1,7 +1,5 @@ package com.example.Centralthon.global.external.ai.web.dto; public record GetMenusByThemeRes( - String title, - String content ) { } diff --git a/src/main/java/com/example/Centralthon/global/external/ai/web/dto/GetTipReq.java b/src/main/java/com/example/Centralthon/global/external/ai/web/dto/GetTipReq.java index d7f0130..21d0847 100644 --- a/src/main/java/com/example/Centralthon/global/external/ai/web/dto/GetTipReq.java +++ b/src/main/java/com/example/Centralthon/global/external/ai/web/dto/GetTipReq.java @@ -1,7 +1,12 @@ package com.example.Centralthon.global.external.ai.web.dto; +import lombok.Getter; +import lombok.Setter; + import java.util.List; +@Getter +@Setter public class GetTipReq { - List items; + List menus; } diff --git a/src/main/java/com/example/Centralthon/global/external/ai/web/dto/GetTipRes.java b/src/main/java/com/example/Centralthon/global/external/ai/web/dto/GetTipRes.java index 24da81b..a825ad7 100644 --- a/src/main/java/com/example/Centralthon/global/external/ai/web/dto/GetTipRes.java +++ b/src/main/java/com/example/Centralthon/global/external/ai/web/dto/GetTipRes.java @@ -1,4 +1,7 @@ package com.example.Centralthon.global.external.ai.web.dto; -public record GetTipRes() { +public record GetTipRes( + String title, + String content +) { } diff --git a/src/main/java/com/example/Centralthon/global/external/response/ExternalResponse.java b/src/main/java/com/example/Centralthon/global/external/response/ExternalResponse.java new file mode 100644 index 0000000..ffcfc86 --- /dev/null +++ b/src/main/java/com/example/Centralthon/global/external/response/ExternalResponse.java @@ -0,0 +1,8 @@ +package com.example.Centralthon.global.external.response; + +public record ExternalResponse( + boolean isSuccess, + int httpStatus, + T data, + String timeStamp +) {} From 199ea225d95b63cef338952e4de233db1ae94007 Mon Sep 17 00:00:00 2001 From: frombunny Date: Fri, 22 Aug 2025 14:37:14 +0900 Subject: [PATCH 6/7] =?UTF-8?q?:sparkles:=20feat:=20AI=20=EB=A9=94?= =?UTF-8?q?=EB=89=B4=20=EC=B6=94=EC=B2=9C=20FastAPI=EC=99=80=20=EC=97=B0?= =?UTF-8?q?=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/menu/entity/enums/Concept.java | 33 +++++++++++++++ .../domain/menu/service/MenuService.java | 3 ++ .../domain/menu/service/MenuServiceImpl.java | 41 +++++++++++++++---- .../menu/web/controller/MenuController.java | 11 +++-- .../menu/web/dto/GetRecommendedMenusReq.java | 28 +++++++++++++ .../exception/GlobalExceptionHandler.java | 4 +- .../global/external/ai/service/AiService.java | 6 +-- .../external/ai/service/AiServiceImpl.java | 16 +++++--- .../ai/web/dto/GetMenusByConceptReq.java | 17 ++++++++ .../ai/web/dto/GetMenusByConceptRes.java | 11 +++++ .../ai/web/dto/GetMenusByConceptWrapper.java | 9 ++++ .../ai/web/dto/GetMenusByThemeReq.java | 4 -- .../ai/web/dto/GetMenusByThemeRes.java | 5 --- 13 files changed, 157 insertions(+), 31 deletions(-) create mode 100644 src/main/java/com/example/Centralthon/domain/menu/entity/enums/Concept.java create mode 100644 src/main/java/com/example/Centralthon/domain/menu/web/dto/GetRecommendedMenusReq.java create mode 100644 src/main/java/com/example/Centralthon/global/external/ai/web/dto/GetMenusByConceptReq.java create mode 100644 src/main/java/com/example/Centralthon/global/external/ai/web/dto/GetMenusByConceptRes.java create mode 100644 src/main/java/com/example/Centralthon/global/external/ai/web/dto/GetMenusByConceptWrapper.java delete mode 100644 src/main/java/com/example/Centralthon/global/external/ai/web/dto/GetMenusByThemeReq.java delete mode 100644 src/main/java/com/example/Centralthon/global/external/ai/web/dto/GetMenusByThemeRes.java diff --git a/src/main/java/com/example/Centralthon/domain/menu/entity/enums/Concept.java b/src/main/java/com/example/Centralthon/domain/menu/entity/enums/Concept.java new file mode 100644 index 0000000..2223747 --- /dev/null +++ b/src/main/java/com/example/Centralthon/domain/menu/entity/enums/Concept.java @@ -0,0 +1,33 @@ +package com.example.Centralthon.domain.menu.entity.enums; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum Concept { + DIET("diet"), + KETO("keto"), + LOW_SODIUM("low_sodium"), + GLYCEMIC("glycemic"), + BULKING("bulking"); + + private final String value; + + @JsonValue + public String getValue(){ + return value; + } + + @JsonCreator + public static Concept fromValue(String value){ + for (Concept concept : Concept.values()){ + if (concept.getValue().equals(value)){ + return concept; + } + } + throw new IllegalArgumentException(); + } +} diff --git a/src/main/java/com/example/Centralthon/domain/menu/service/MenuService.java b/src/main/java/com/example/Centralthon/domain/menu/service/MenuService.java index 96d8a22..a944d55 100644 --- a/src/main/java/com/example/Centralthon/domain/menu/service/MenuService.java +++ b/src/main/java/com/example/Centralthon/domain/menu/service/MenuService.java @@ -3,6 +3,7 @@ import com.example.Centralthon.domain.menu.web.dto.*; import com.example.Centralthon.domain.menu.web.dto.NearbyMenusRes; import com.example.Centralthon.domain.menu.web.dto.StoresByMenuRes; +import com.example.Centralthon.domain.menu.web.dto.GetRecommendedMenusReq; import com.example.Centralthon.global.external.ai.web.dto.GetTipReq; import com.example.Centralthon.global.external.ai.web.dto.GetTipRes; @@ -15,5 +16,7 @@ public interface MenuService { List details(MenuIdsReq menus); + List getRecommendedMenus(GetRecommendedMenusReq getRecommendedMenusReq); + List getTips(GetTipReq getTipReq); } diff --git a/src/main/java/com/example/Centralthon/domain/menu/service/MenuServiceImpl.java b/src/main/java/com/example/Centralthon/domain/menu/service/MenuServiceImpl.java index bae4b2f..24fb7d3 100644 --- a/src/main/java/com/example/Centralthon/domain/menu/service/MenuServiceImpl.java +++ b/src/main/java/com/example/Centralthon/domain/menu/service/MenuServiceImpl.java @@ -4,9 +4,10 @@ import com.example.Centralthon.domain.menu.exception.MenuNotFoundException; import com.example.Centralthon.domain.menu.repository.MenuRepository; import com.example.Centralthon.domain.menu.web.dto.*; -import com.example.Centralthon.domain.store.entity.Store; +import com.example.Centralthon.domain.menu.web.dto.GetRecommendedMenusReq; import com.example.Centralthon.global.external.ai.service.AiService; -import com.example.Centralthon.global.external.ai.service.AiServiceImpl; +import com.example.Centralthon.global.external.ai.web.dto.GetMenusByConceptReq; +import com.example.Centralthon.global.external.ai.web.dto.GetMenusByConceptRes; import com.example.Centralthon.global.external.ai.web.dto.GetTipReq; import com.example.Centralthon.global.external.ai.web.dto.GetTipRes; import com.example.Centralthon.global.external.exception.AiCommunicationFailedException; @@ -19,14 +20,8 @@ import java.time.LocalDateTime; import java.util.*; -import java.util.stream.Collectors; -import java.util.Comparator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; import static com.example.Centralthon.global.util.geo.GeoUtils.calculateBoundingBox; -import static com.example.Centralthon.global.util.geo.GeoUtils.calculateDistance; @Service @Slf4j @@ -97,6 +92,34 @@ public List details(MenuIdsReq menus) { .toList(); } + @Override + public List getRecommendedMenus(GetRecommendedMenusReq getRecommendedMenusReq){ + // 2km 이내 메뉴 조회 + List menus = findMenusWithinRadius(getRecommendedMenusReq.getLatitude(), getRecommendedMenusReq.getLongitude()); + + // 중복 제거한 메뉴 목록 + Map uniqueMenus = new LinkedHashMap<>(); + for (Menu menu : menus) { + uniqueMenus.putIfAbsent(menu.getName(), menu); + } + + List menuList = new ArrayList<>(uniqueMenus.keySet()); + List getMenusByConceptResList = aiService.getMenuByConceptFromAi( + GetMenusByConceptReq.builder() + .concept(getRecommendedMenusReq.getConcept()) + .count(getRecommendedMenusReq.getCount()) + .items(menuList) + .build() + ); + + return getMenusByConceptResList.stream() + .map(res -> { + Menu matchedMenu = uniqueMenus.get(res.inputMenu()); + return NearbyMenusRes.from(matchedMenu); + }) + .toList(); + } + @Override public List getTips(GetTipReq getTipReq) { try{ @@ -107,4 +130,6 @@ public List getTips(GetTipReq getTipReq) { } } + + } diff --git a/src/main/java/com/example/Centralthon/domain/menu/web/controller/MenuController.java b/src/main/java/com/example/Centralthon/domain/menu/web/controller/MenuController.java index 23b01f8..d4567b0 100644 --- a/src/main/java/com/example/Centralthon/domain/menu/web/controller/MenuController.java +++ b/src/main/java/com/example/Centralthon/domain/menu/web/controller/MenuController.java @@ -3,7 +3,7 @@ import com.example.Centralthon.domain.menu.service.MenuService; import com.example.Centralthon.domain.menu.web.dto.*; -import com.example.Centralthon.domain.order.web.controller.OrderApi; +import com.example.Centralthon.domain.menu.web.dto.GetRecommendedMenusReq; import com.example.Centralthon.global.external.ai.web.dto.GetTipReq; import com.example.Centralthon.global.external.ai.web.dto.GetTipRes; import com.example.Centralthon.global.response.SuccessResponse; @@ -11,7 +11,6 @@ import com.example.Centralthon.domain.menu.web.dto.NearbyMenusRes; import com.example.Centralthon.domain.menu.web.dto.StoresByMenuRes; -import com.example.Centralthon.global.response.SuccessResponse; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; @@ -19,7 +18,6 @@ import org.springframework.web.bind.annotation.*; import java.util.List; -import java.util.Map; @RestController @RequestMapping("/api/menus") @@ -61,6 +59,13 @@ public ResponseEntity>> details(@RequestBod return ResponseEntity.status(HttpStatus.OK).body(SuccessResponse.from(menuList)); } + // 컨셉별 메뉴 추천 + @PostMapping("/recommend") + public ResponseEntity>> recommend(@RequestBody @Valid GetRecommendedMenusReq getRecommendedMenusReq){ + List menusList = menuService.getRecommendedMenus(getRecommendedMenusReq); + return ResponseEntity.status(HttpStatus.OK).body(SuccessResponse.from(menusList)); + } + // 알뜰 반찬 팁 조회 @PostMapping("/tips") public ResponseEntity>> getTips(@RequestBody @Valid GetTipReq getTipReq){ diff --git a/src/main/java/com/example/Centralthon/domain/menu/web/dto/GetRecommendedMenusReq.java b/src/main/java/com/example/Centralthon/domain/menu/web/dto/GetRecommendedMenusReq.java new file mode 100644 index 0000000..2161ba2 --- /dev/null +++ b/src/main/java/com/example/Centralthon/domain/menu/web/dto/GetRecommendedMenusReq.java @@ -0,0 +1,28 @@ +package com.example.Centralthon.domain.menu.web.dto; + +import com.example.Centralthon.domain.menu.entity.enums.Concept; +import jakarta.validation.constraints.*; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class GetRecommendedMenusReq { + @DecimalMin(value = "-90.0", message = "위도는 -90 이상이어야 합니다.") + @DecimalMax(value = "90.0", message = "위도는 90 이하이어야 합니다.") + @NotNull(message = "위도는 필수값입니다.") + double latitude; + + @DecimalMin(value = "-180.0", message = "경도는 -180 이상이어야 합니다.") + @DecimalMax(value = "180.0", message = "경도는 180 이하이어야 합니다.") + @NotNull(message = "경도는 필수값입니다.") + double longitude; + + @NotNull(message = "컨셉은 필수 값입니다.") + Concept concept; + + @Min(value = 1, message = "최소 1개 이상 조회해야 합니다.") + @Max(value = 50, message = "최대 50개까지 조회할 수 있습니다.") + @NotNull(message = "숫자는 필수 값입니다.") + int count; // 몇 개 조회할지 +} diff --git a/src/main/java/com/example/Centralthon/global/exception/GlobalExceptionHandler.java b/src/main/java/com/example/Centralthon/global/exception/GlobalExceptionHandler.java index 9b779b1..68c1624 100644 --- a/src/main/java/com/example/Centralthon/global/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/example/Centralthon/global/exception/GlobalExceptionHandler.java @@ -42,7 +42,7 @@ public ResponseEntity> handleBindException(BindException e) { return ResponseEntity.status(errorREsponse.getHttpStatus()).body(errorREsponse); } - //ReqeustBody 등으로 전달 받은 JSON 바디의 파싱이 실패 했을 때 + // ReqeustBody 등으로 전달 받은 JSON 바디의 파싱이 실패 했을 때 @ExceptionHandler(HttpMessageNotReadableException.class) public ResponseEntity> handleHttpMessageNotReadableException(HttpMessageNotReadableException e) { log.error("HttpMessageNotReadableException : {}", e.getMessage(), e); @@ -66,7 +66,7 @@ public ResponseEntity> handleMissingServletRequestPartException return ResponseEntity.status(errorResponse.getHttpStatus()).body(errorResponse); } - //지원하지 않는 HTTP 메소드를 호출할 경우 + // 지원하지 않는 HTTP 메소드를 호출할 경우 @ExceptionHandler(HttpRequestMethodNotSupportedException.class) public ResponseEntity> handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) { log.error("HttpRequestMethodNotSupportedException : {}", e.getMessage(), e); diff --git a/src/main/java/com/example/Centralthon/global/external/ai/service/AiService.java b/src/main/java/com/example/Centralthon/global/external/ai/service/AiService.java index d7cb6c2..9d28173 100644 --- a/src/main/java/com/example/Centralthon/global/external/ai/service/AiService.java +++ b/src/main/java/com/example/Centralthon/global/external/ai/service/AiService.java @@ -1,7 +1,7 @@ package com.example.Centralthon.global.external.ai.service; -import com.example.Centralthon.global.external.ai.web.dto.GetMenusByThemeReq; -import com.example.Centralthon.global.external.ai.web.dto.GetMenusByThemeRes; +import com.example.Centralthon.global.external.ai.web.dto.GetMenusByConceptReq; +import com.example.Centralthon.global.external.ai.web.dto.GetMenusByConceptRes; import com.example.Centralthon.global.external.ai.web.dto.GetTipReq; import com.example.Centralthon.global.external.ai.web.dto.GetTipRes; @@ -9,5 +9,5 @@ public interface AiService { List getTipFromAi(GetTipReq getTipReq); - GetMenusByThemeRes getMenuByThemeFromAi(GetMenusByThemeReq getMenuByThemeReq); + List getMenuByConceptFromAi(GetMenusByConceptReq getMenuByThemeReq); } diff --git a/src/main/java/com/example/Centralthon/global/external/ai/service/AiServiceImpl.java b/src/main/java/com/example/Centralthon/global/external/ai/service/AiServiceImpl.java index f513173..7d0e570 100644 --- a/src/main/java/com/example/Centralthon/global/external/ai/service/AiServiceImpl.java +++ b/src/main/java/com/example/Centralthon/global/external/ai/service/AiServiceImpl.java @@ -1,9 +1,6 @@ package com.example.Centralthon.global.external.ai.service; -import com.example.Centralthon.global.external.ai.web.dto.GetMenusByThemeReq; -import com.example.Centralthon.global.external.ai.web.dto.GetMenusByThemeRes; -import com.example.Centralthon.global.external.ai.web.dto.GetTipReq; -import com.example.Centralthon.global.external.ai.web.dto.GetTipRes; +import com.example.Centralthon.global.external.ai.web.dto.*; import com.example.Centralthon.global.external.response.ExternalResponse; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; @@ -39,7 +36,14 @@ public List getTipFromAi(GetTipReq getTipReq) { } @Override - public GetMenusByThemeRes getMenuByThemeFromAi(GetMenusByThemeReq getMenuByThemeReq) { - return null; + public List getMenuByConceptFromAi(GetMenusByConceptReq getMenuByThemeReq) { + String recommendUrl = baseUrl + "menus/recommend"; + ResponseEntity> response = restTemplate.exchange( + recommendUrl, + HttpMethod.POST, + new HttpEntity<>(getMenuByThemeReq), + new ParameterizedTypeReference>() {} + ); + return response.getBody().data().items(); } } diff --git a/src/main/java/com/example/Centralthon/global/external/ai/web/dto/GetMenusByConceptReq.java b/src/main/java/com/example/Centralthon/global/external/ai/web/dto/GetMenusByConceptReq.java new file mode 100644 index 0000000..c3ab000 --- /dev/null +++ b/src/main/java/com/example/Centralthon/global/external/ai/web/dto/GetMenusByConceptReq.java @@ -0,0 +1,17 @@ +package com.example.Centralthon.global.external.ai.web.dto; + +import com.example.Centralthon.domain.menu.entity.enums.Concept; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +@Getter +@Setter +@Builder +public class GetMenusByConceptReq { + Concept concept; + int count; + List items; +} diff --git a/src/main/java/com/example/Centralthon/global/external/ai/web/dto/GetMenusByConceptRes.java b/src/main/java/com/example/Centralthon/global/external/ai/web/dto/GetMenusByConceptRes.java new file mode 100644 index 0000000..d38da00 --- /dev/null +++ b/src/main/java/com/example/Centralthon/global/external/ai/web/dto/GetMenusByConceptRes.java @@ -0,0 +1,11 @@ +package com.example.Centralthon.global.external.ai.web.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public record GetMenusByConceptRes( + @JsonProperty("input_menu") String inputMenu, + @JsonProperty("matched_name") String matchedName, + float similarity, + float suitability +) { +} diff --git a/src/main/java/com/example/Centralthon/global/external/ai/web/dto/GetMenusByConceptWrapper.java b/src/main/java/com/example/Centralthon/global/external/ai/web/dto/GetMenusByConceptWrapper.java new file mode 100644 index 0000000..dd493ed --- /dev/null +++ b/src/main/java/com/example/Centralthon/global/external/ai/web/dto/GetMenusByConceptWrapper.java @@ -0,0 +1,9 @@ +package com.example.Centralthon.global.external.ai.web.dto; + +import java.util.List; + +public record GetMenusByConceptWrapper( + String concept, + int count, + List items +) {} diff --git a/src/main/java/com/example/Centralthon/global/external/ai/web/dto/GetMenusByThemeReq.java b/src/main/java/com/example/Centralthon/global/external/ai/web/dto/GetMenusByThemeReq.java deleted file mode 100644 index 5fed5de..0000000 --- a/src/main/java/com/example/Centralthon/global/external/ai/web/dto/GetMenusByThemeReq.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.example.Centralthon.global.external.ai.web.dto; - -public class GetMenusByThemeReq { -} diff --git a/src/main/java/com/example/Centralthon/global/external/ai/web/dto/GetMenusByThemeRes.java b/src/main/java/com/example/Centralthon/global/external/ai/web/dto/GetMenusByThemeRes.java deleted file mode 100644 index 7ebf31a..0000000 --- a/src/main/java/com/example/Centralthon/global/external/ai/web/dto/GetMenusByThemeRes.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.Centralthon.global.external.ai.web.dto; - -public record GetMenusByThemeRes( -) { -} From 2a98e96e6e0573f02c383e6c43e945ae2f61e22a Mon Sep 17 00:00:00 2001 From: frombunny Date: Fri, 22 Aug 2025 21:52:27 +0900 Subject: [PATCH 7/7] =?UTF-8?q?:memo:=20docs:=20=EC=8A=A4=EC=9B=A8?= =?UTF-8?q?=EA=B1=B0=20=EB=AA=85=EC=84=B8=20=EC=B6=94=EA=B0=80=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/menu/web/controller/MenuApi.java | 116 +++++++++++++++++- .../menu/web/controller/MenuController.java | 2 + .../global/external/ai/web/dto/GetTipReq.java | 4 + 3 files changed, 117 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/example/Centralthon/domain/menu/web/controller/MenuApi.java b/src/main/java/com/example/Centralthon/domain/menu/web/controller/MenuApi.java index f6c611c..c9797ae 100644 --- a/src/main/java/com/example/Centralthon/domain/menu/web/controller/MenuApi.java +++ b/src/main/java/com/example/Centralthon/domain/menu/web/controller/MenuApi.java @@ -1,9 +1,6 @@ package com.example.Centralthon.domain.menu.web.controller; -import com.example.Centralthon.domain.menu.web.dto.MenuDetailsRes; -import com.example.Centralthon.domain.menu.web.dto.MenuIdsReq; -import com.example.Centralthon.domain.menu.web.dto.NearbyMenusRes; -import com.example.Centralthon.domain.menu.web.dto.StoresByMenuRes; +import com.example.Centralthon.domain.menu.web.dto.*; import com.example.Centralthon.global.external.ai.web.dto.GetTipReq; import com.example.Centralthon.global.external.ai.web.dto.GetTipRes; import com.example.Centralthon.global.response.SuccessResponse; @@ -158,5 +155,114 @@ ResponseEntity>> storesByMenu( ) ResponseEntity>> details(@RequestBody @Valid MenuIdsReq menus); - ResponseEntity>> getTips(@RequestBody @Valid GetTipReq getTipReq); + + + @Operation( + summary = "메뉴 Tip 조회", + description = "입력한 메뉴 이름 배열을 기반으로 AI 추천 Tip(활용법/조리법 등)을 반환합니다.
" + + "{title, content} 리스트를 반환합니다." + ) + @ApiResponse( + responseCode = "200", + description = "메뉴 Tip 조회 성공", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = SuccessResponse.class), + examples = @ExampleObject( + name = "SUCCESS_200", + value = """ + { + "timestamp": "2025-08-22 03:11:13", + "code": "SUCCESS_200", + "httpStatus": 200, + "message": "호출에 성공하였습니다.", + "data": [ + { + "title": "콩나물의 변신, 국물 한 스푼", + "content": "콩나물무침을 육수에 넣고 끓이면 시원한 콩나물국이 됩니다." + }, + { + "title": "감자볶음, 크리스피한 감자튀김으로", + "content": "감자볶음을 잘게 썰어 튀김가루에 묻혀서 튀기면 바삭한 감자튀김이 됩니다." + }, + { + "title": "애호박 볶음, 달콤한 애호박전으로", + "content": "애호박볶음을 반죽에 섞어 팬에 부치면 맛있는 애호박전이 됩니다." + } + ], + "isSuccess": true + } + """ + ) + ) + ) + ResponseEntity>> getTips( + @io.swagger.v3.oas.annotations.parameters.RequestBody( + description = "메뉴 이름 배열 요청", + required = true, + content = @Content( + schema = @Schema(implementation = GetTipReq.class), + examples = @ExampleObject( + value = """ + { + "menus": ["콩나물무침", "감자볶음", "애호박볶음"] + } + """ + ) + ) + ) + @Valid @RequestBody GetTipReq getTipReq); + + @Operation( + summary = "메뉴 추천", + description = "사용자 위치(latitude, longitude)와 컨셉(concept)을 기반으로 맞춤 메뉴를 추천합니다.
" + + "concept는 다음 중 하나만 선택할 수 있습니다: `diet`, `keto`, `low_sodium`, `bulking`, `glycemic`" + ) + @ApiResponse( + responseCode = "200", + description = "메뉴 추천 성공", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = SuccessResponse.class), + examples = @ExampleObject( + name = "SUCCESS_200", + value = """ + { + "timestamp": "2025-08-22 14:36:04", + "code": "SUCCESS_200", + "httpStatus": 200, + "message": "호출에 성공하였습니다.", + "data": [ + { + "name": "돼지 목살 스테이크", + "category": "STIR_FRY" + }, + ... + ], + "isSuccess": true + } + """ + ) + ) + ) + ResponseEntity>> recommend( + @io.swagger.v3.oas.annotations.parameters.RequestBody( + description = "사용자 위치와 추천 컨셉 요청", + required = true, + content = @Content( + schema = @Schema(implementation = GetRecommendedMenusReq.class), + examples = @ExampleObject( + value = """ + { + "latitude": 37.4752, + "longitude": 127.050, + "concept": "keto", + "count": 15 + } + """ + ) + ) + ) + @Valid @RequestBody GetRecommendedMenusReq getRecommendedMenusReq); + } diff --git a/src/main/java/com/example/Centralthon/domain/menu/web/controller/MenuController.java b/src/main/java/com/example/Centralthon/domain/menu/web/controller/MenuController.java index d4567b0..7f068ab 100644 --- a/src/main/java/com/example/Centralthon/domain/menu/web/controller/MenuController.java +++ b/src/main/java/com/example/Centralthon/domain/menu/web/controller/MenuController.java @@ -61,6 +61,7 @@ public ResponseEntity>> details(@RequestBod // 컨셉별 메뉴 추천 @PostMapping("/recommend") + @Override public ResponseEntity>> recommend(@RequestBody @Valid GetRecommendedMenusReq getRecommendedMenusReq){ List menusList = menuService.getRecommendedMenus(getRecommendedMenusReq); return ResponseEntity.status(HttpStatus.OK).body(SuccessResponse.from(menusList)); @@ -68,6 +69,7 @@ public ResponseEntity>> recommend(@RequestB // 알뜰 반찬 팁 조회 @PostMapping("/tips") + @Override public ResponseEntity>> getTips(@RequestBody @Valid GetTipReq getTipReq){ List tips = menuService.getTips(getTipReq); return ResponseEntity.status(HttpStatus.OK).body(SuccessResponse.from(tips)); diff --git a/src/main/java/com/example/Centralthon/global/external/ai/web/dto/GetTipReq.java b/src/main/java/com/example/Centralthon/global/external/ai/web/dto/GetTipReq.java index 21d0847..1b92b2f 100644 --- a/src/main/java/com/example/Centralthon/global/external/ai/web/dto/GetTipReq.java +++ b/src/main/java/com/example/Centralthon/global/external/ai/web/dto/GetTipReq.java @@ -1,5 +1,7 @@ package com.example.Centralthon.global.external.ai.web.dto; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; import lombok.Getter; import lombok.Setter; @@ -8,5 +10,7 @@ @Getter @Setter public class GetTipReq { + @NotNull(message = "메뉴 목록은 필수 값입니다.") + @NotEmpty(message = "메뉴 목록은 비어있을 수 없습니다.") List menus; }