From 3bf561fab4b363cb7b85e406e12d33fbf17dcdda Mon Sep 17 00:00:00 2001 From: EunjinWoo Date: Mon, 2 Jun 2025 15:27:03 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=EC=9A=B4=EB=8F=99=20=EB=8B=A8?= =?UTF-8?q?=EA=B1=B4=20=EC=A1=B0=ED=9A=8C=20api=20=EA=B5=AC=ED=98=84=20#21?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ExerciseController.java | 20 +++++++++++++++ .../domain/exercise/dto/ExerciseResponse.java | 25 +++++++++++++++++++ .../exercise/service/ExerciseService.java | 25 +++++++++++++++++++ 3 files changed, 70 insertions(+) create mode 100644 cloud/src/main/java/com/project/cloud/domain/exercise/controller/ExerciseController.java create mode 100644 cloud/src/main/java/com/project/cloud/domain/exercise/dto/ExerciseResponse.java create mode 100644 cloud/src/main/java/com/project/cloud/domain/exercise/service/ExerciseService.java diff --git a/cloud/src/main/java/com/project/cloud/domain/exercise/controller/ExerciseController.java b/cloud/src/main/java/com/project/cloud/domain/exercise/controller/ExerciseController.java new file mode 100644 index 0000000..aa34548 --- /dev/null +++ b/cloud/src/main/java/com/project/cloud/domain/exercise/controller/ExerciseController.java @@ -0,0 +1,20 @@ +package com.project.cloud.domain.exercise.controller; + +import com.project.cloud.domain.exercise.dto.ExerciseResponse; +import com.project.cloud.domain.exercise.service.ExerciseService; +import com.project.cloud.global.response.SuccessResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/exercises") +@RequiredArgsConstructor +public class ExerciseController { + + private final ExerciseService exerciseService; + + @GetMapping("/{exerciseId}") + public SuccessResponse getExercise(@PathVariable Long exerciseId) { + return SuccessResponse.ok(exerciseService.getById(exerciseId)); + } +} diff --git a/cloud/src/main/java/com/project/cloud/domain/exercise/dto/ExerciseResponse.java b/cloud/src/main/java/com/project/cloud/domain/exercise/dto/ExerciseResponse.java new file mode 100644 index 0000000..9c6907a --- /dev/null +++ b/cloud/src/main/java/com/project/cloud/domain/exercise/dto/ExerciseResponse.java @@ -0,0 +1,25 @@ +package com.project.cloud.domain.exercise.dto; + +import com.project.cloud.domain.exercise.entity.Exercise; +import com.project.cloud.domain.exercise.enumerate.Target; + +public class ExerciseResponse { + + public record Detail( + Long exerciseId, + String name, + String imageUrl, + String link, + Target target + ) { + public static Detail from(Exercise exercise) { + return new Detail( + exercise.getId(), + exercise.getName(), + exercise.getImage(), + exercise.getLink(), + exercise.getTarget() + ); + } + } +} diff --git a/cloud/src/main/java/com/project/cloud/domain/exercise/service/ExerciseService.java b/cloud/src/main/java/com/project/cloud/domain/exercise/service/ExerciseService.java new file mode 100644 index 0000000..e7801da --- /dev/null +++ b/cloud/src/main/java/com/project/cloud/domain/exercise/service/ExerciseService.java @@ -0,0 +1,25 @@ +package com.project.cloud.domain.exercise.service; + +import com.project.cloud.domain.exercise.dto.ExerciseResponse; +import com.project.cloud.domain.exercise.entity.Exercise; +import com.project.cloud.domain.exercise.repository.ExerciseRepository; +import com.project.cloud.global.exception.CustomException; +import com.project.cloud.global.exception.ErrorCode; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class ExerciseService { + + private final ExerciseRepository exerciseRepository; + + public ExerciseResponse.Detail getById(Long exerciseId) { + Exercise exercise = exerciseRepository.findById(exerciseId) + .orElseThrow(() -> new CustomException(ErrorCode.EXERCISE_NOT_FOUND)); + + return ExerciseResponse.Detail.from(exercise); + } +} From c5576e280dbb4258e02d95ee83b9cbdddc7425ba Mon Sep 17 00:00:00 2001 From: EunjinWoo Date: Mon, 2 Jun 2025 16:14:06 +0900 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20=EC=9A=B4=EB=8F=99=20=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=A1=B0=ED=9A=8C=20=EB=B0=8F=20=ED=83=80?= =?UTF-8?q?=EA=B2=9F=EC=9C=BC=EB=A1=9C=20=EA=B2=80=EC=83=89=20api=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20#21?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exercise/controller/ExerciseController.java | 10 ++++++++++ .../exercise/repository/ExerciseRepository.java | 4 ++++ .../exercise/service/ExerciseService.java | 17 +++++++++++++++++ 3 files changed, 31 insertions(+) diff --git a/cloud/src/main/java/com/project/cloud/domain/exercise/controller/ExerciseController.java b/cloud/src/main/java/com/project/cloud/domain/exercise/controller/ExerciseController.java index aa34548..bdae0e5 100644 --- a/cloud/src/main/java/com/project/cloud/domain/exercise/controller/ExerciseController.java +++ b/cloud/src/main/java/com/project/cloud/domain/exercise/controller/ExerciseController.java @@ -1,11 +1,15 @@ package com.project.cloud.domain.exercise.controller; import com.project.cloud.domain.exercise.dto.ExerciseResponse; +import com.project.cloud.domain.exercise.enumerate.Target; import com.project.cloud.domain.exercise.service.ExerciseService; import com.project.cloud.global.response.SuccessResponse; +import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; +import java.util.List; + @RestController @RequestMapping("/exercises") @RequiredArgsConstructor @@ -17,4 +21,10 @@ public class ExerciseController { public SuccessResponse getExercise(@PathVariable Long exerciseId) { return SuccessResponse.ok(exerciseService.getById(exerciseId)); } + + @GetMapping + @Operation(summary = "전체 운동 리스트를 조회합니다.", description = "전체 운동 리스트를 조회합니다. 쿼리 파라미터로 target이 없으면 전체 운동 리스트, 아니면 해당하는 타겟의 운동 리스트를 검색합니다.") + public SuccessResponse> getAllExercises(@RequestParam(required = false) List target) { + return SuccessResponse.ok(exerciseService.getAllExercises(target)); + } } diff --git a/cloud/src/main/java/com/project/cloud/domain/exercise/repository/ExerciseRepository.java b/cloud/src/main/java/com/project/cloud/domain/exercise/repository/ExerciseRepository.java index c73cb6d..4b397ad 100644 --- a/cloud/src/main/java/com/project/cloud/domain/exercise/repository/ExerciseRepository.java +++ b/cloud/src/main/java/com/project/cloud/domain/exercise/repository/ExerciseRepository.java @@ -1,9 +1,13 @@ package com.project.cloud.domain.exercise.repository; import com.project.cloud.domain.exercise.entity.Exercise; +import com.project.cloud.domain.exercise.enumerate.Target; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import java.util.List; + @Repository public interface ExerciseRepository extends JpaRepository { + List findAllByTargetIn(List targets); } diff --git a/cloud/src/main/java/com/project/cloud/domain/exercise/service/ExerciseService.java b/cloud/src/main/java/com/project/cloud/domain/exercise/service/ExerciseService.java index e7801da..8cb060b 100644 --- a/cloud/src/main/java/com/project/cloud/domain/exercise/service/ExerciseService.java +++ b/cloud/src/main/java/com/project/cloud/domain/exercise/service/ExerciseService.java @@ -2,6 +2,7 @@ import com.project.cloud.domain.exercise.dto.ExerciseResponse; import com.project.cloud.domain.exercise.entity.Exercise; +import com.project.cloud.domain.exercise.enumerate.Target; import com.project.cloud.domain.exercise.repository.ExerciseRepository; import com.project.cloud.global.exception.CustomException; import com.project.cloud.global.exception.ErrorCode; @@ -9,6 +10,8 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.List; + @Service @RequiredArgsConstructor @Transactional(readOnly = true) @@ -22,4 +25,18 @@ public ExerciseResponse.Detail getById(Long exerciseId) { return ExerciseResponse.Detail.from(exercise); } + + public List getAllExercises(List targetList) { + List exercises; + + if (targetList == null || targetList.isEmpty()) { + exercises = exerciseRepository.findAll(); + } else { + exercises = exerciseRepository.findAllByTargetIn(targetList); + } + + return exercises.stream() + .map(ExerciseResponse.Detail::from) + .toList(); + } } From e1420ef62b60382b1e2257eae096803cb5a41257 Mon Sep 17 00:00:00 2001 From: EunjinWoo Date: Tue, 3 Jun 2025 21:48:00 +0900 Subject: [PATCH 3/3] =?UTF-8?q?feat:=20=EC=B1=97=EB=B4=87=20api=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20#22?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/controller/ChatController.java | 24 +++ .../cloud/domain/chat/dto/ChatRequest.java | 9 ++ .../cloud/domain/chat/dto/ChatResponse.java | 37 +++++ .../domain/chat/service/ChatService.java | 141 ++++++++++++++++++ .../routine/dto/request/RoutineRequest.java | 27 +++- 5 files changed, 231 insertions(+), 7 deletions(-) create mode 100644 cloud/src/main/java/com/project/cloud/domain/chat/controller/ChatController.java create mode 100644 cloud/src/main/java/com/project/cloud/domain/chat/dto/ChatRequest.java create mode 100644 cloud/src/main/java/com/project/cloud/domain/chat/dto/ChatResponse.java create mode 100644 cloud/src/main/java/com/project/cloud/domain/chat/service/ChatService.java diff --git a/cloud/src/main/java/com/project/cloud/domain/chat/controller/ChatController.java b/cloud/src/main/java/com/project/cloud/domain/chat/controller/ChatController.java new file mode 100644 index 0000000..c86e6aa --- /dev/null +++ b/cloud/src/main/java/com/project/cloud/domain/chat/controller/ChatController.java @@ -0,0 +1,24 @@ +package com.project.cloud.domain.chat.controller; + +import com.project.cloud.domain.chat.dto.ChatRequest; +import com.project.cloud.domain.chat.dto.ChatResponse; +import com.project.cloud.domain.chat.service.ChatService; +import com.project.cloud.global.response.SuccessResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/chat") +@RequiredArgsConstructor +public class ChatController { + + private final ChatService chatService; + + @PostMapping + public SuccessResponse chat(@RequestBody ChatRequest.Chat request) { + return SuccessResponse.ok(chatService.askChatBot(request)); + } +} diff --git a/cloud/src/main/java/com/project/cloud/domain/chat/dto/ChatRequest.java b/cloud/src/main/java/com/project/cloud/domain/chat/dto/ChatRequest.java new file mode 100644 index 0000000..aa46664 --- /dev/null +++ b/cloud/src/main/java/com/project/cloud/domain/chat/dto/ChatRequest.java @@ -0,0 +1,9 @@ +package com.project.cloud.domain.chat.dto; + +public class ChatRequest { + + public record Chat( + String email, + String message + ) {} +} diff --git a/cloud/src/main/java/com/project/cloud/domain/chat/dto/ChatResponse.java b/cloud/src/main/java/com/project/cloud/domain/chat/dto/ChatResponse.java new file mode 100644 index 0000000..f5d0090 --- /dev/null +++ b/cloud/src/main/java/com/project/cloud/domain/chat/dto/ChatResponse.java @@ -0,0 +1,37 @@ +package com.project.cloud.domain.chat.dto; + +import com.project.cloud.domain.routine.dto.request.RoutineRequest; + +import java.util.List; + +public class ChatResponse { + + public record Chat ( + String type, + Qa qa, + Routine routine + ) { + public static Chat from(String type, Qa qa, Routine routine) { + if (type.equals("qa")) { + return new Chat(type, qa, null); + } + + return new Chat(type, null, routine); + } + } + + public record Qa ( + String type, + String question, + String answer + ) {} + + public record Routine ( + String type, + List preferredParts, + String level, + String goal, + Integer frequencyPerWeek, + RoutineRequest routine + ) {} +} diff --git a/cloud/src/main/java/com/project/cloud/domain/chat/service/ChatService.java b/cloud/src/main/java/com/project/cloud/domain/chat/service/ChatService.java new file mode 100644 index 0000000..2cda599 --- /dev/null +++ b/cloud/src/main/java/com/project/cloud/domain/chat/service/ChatService.java @@ -0,0 +1,141 @@ +package com.project.cloud.domain.chat.service; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.project.cloud.domain.chat.dto.ChatRequest; +import com.project.cloud.domain.chat.dto.ChatResponse; +import com.project.cloud.domain.routine.dto.request.RoutineRequest; +import com.project.cloud.domain.user.entity.User; +import com.project.cloud.domain.user.repository.UserRepository; +import com.project.cloud.domain.user.service.UserService; +import com.project.cloud.global.exception.CustomException; +import com.project.cloud.global.exception.ErrorCode; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.*; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.client.RestTemplate; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class ChatService { + + private final ObjectMapper objectMapper; + private final RestTemplate restTemplate; + private final UserRepository userRepository; + + @Value("${ai.chatbot-url}") private String chatbotUrl; + + public ChatResponse.Chat askChatBot(ChatRequest.Chat request) { + + User user = userRepository.findByEmail(request.email()).orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_EXIST)); + + // AI 챗봇 서버로 보낼 JSON 바디 생성 + Map body = new HashMap<>(); + body.put("message", request.message()); + + // userData 맵 생성 + Map userData = new HashMap<>(); + userData.put("goal", user.getGoal().name().toLowerCase()); + userData.put("preferred_parts", + user.getBodyPartStats().stream() + .map(stat -> stat.getBodyPart().getName()) + .toList() + ); + userData.put("level", user.getWorkoutLevel().name().toLowerCase()); + userData.put("gender", user.getGender().name()); + userData.put("weight", user.getWeight()); + userData.put("top_k", 3); + + body.put("userData", userData); + + // RestTemplate으로 챗봇 서버에 POST 요청 + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + + HttpEntity> httpEntity = new HttpEntity<>(body, headers); + + ResponseEntity respEntity; + try { + respEntity = restTemplate.exchange( + chatbotUrl, + HttpMethod.POST, + httpEntity, + String.class + ); + } catch (Exception e) { + // 네트워크 오류 등, 챗봇 서버 요청 실패 시 + ChatResponse.Qa errorQa = new ChatResponse.Qa("error", null, "챗봇 서버 요청 실패: " + e.getMessage()); + return ChatResponse.Chat.from("error", errorQa, null); + } + + String responseJson = respEntity.getBody(); + + // ChatResponse.Chat 객체 생성/반환 + try { + JsonNode root = objectMapper.readTree(responseJson); + String type = root.path("type").asText(); + + if ("qa".equals(type)) { + String question = root.path("question").asText(null); + String answer = root.path("answer").asText(null); + + ChatResponse.Qa qa = new ChatResponse.Qa(type, question, answer); + return ChatResponse.Chat.from(type, qa, null); + } + else if ("routine".equals(type)) { + List preferredParts = new ArrayList<>(); + root.path("preferred_parts") + .forEach(node -> preferredParts.add(node.asText())); + + String level = root.path("level").asText(null); + String goal = root.path("goal").asText(null); + Integer frequencyPerWeek = root.path("frequency_per_week").asInt(); + + // "routine" 오브젝트 내부 파싱 + JsonNode routineNode = root.path("routine"); + String routineName = routineNode.path("name").asText(null); + + // routineItems 배열 파싱 + List items = new ArrayList<>(); + routineNode.path("routineItems").forEach(itemNode -> { + Long exerciseId = itemNode.path("exerciseId").asLong(); + Integer sets = itemNode.path("sets").asInt(); + Integer reps = itemNode.path("reps").asInt(); + Integer weight = itemNode.path("weight").asInt(); + Integer order = itemNode.path("order").asInt(); + + items.add(new RoutineRequest.RoutineItemDto(exerciseId, sets, reps, weight, order)); + }); + + // RoutineRequest 생성 + RoutineRequest rr = new RoutineRequest(routineName, items); + + ChatResponse.Routine routine = new ChatResponse.Routine( + type, + preferredParts, + level, + goal, + frequencyPerWeek, + rr + ); + return ChatResponse.Chat.from(type, null, routine); + } + else { + ChatResponse.Qa errorQa = new ChatResponse.Qa("error", null, "알 수 없는 응답 형식(type=" + type + ")"); + return ChatResponse.Chat.from("error", errorQa, null); + } + } + catch (Exception e) { + ChatResponse.Qa errorQa = new ChatResponse.Qa("error", null, "JSON 파싱 오류: " + e.getMessage()); + return ChatResponse.Chat.from("error", errorQa, null); + } + } +} diff --git a/cloud/src/main/java/com/project/cloud/domain/routine/dto/request/RoutineRequest.java b/cloud/src/main/java/com/project/cloud/domain/routine/dto/request/RoutineRequest.java index b30abde..491b353 100644 --- a/cloud/src/main/java/com/project/cloud/domain/routine/dto/request/RoutineRequest.java +++ b/cloud/src/main/java/com/project/cloud/domain/routine/dto/request/RoutineRequest.java @@ -6,15 +6,28 @@ @Getter public class RoutineRequest { - private String name; - private List routineItems; + private final String name; + private final List routineItems; + + public RoutineRequest(String name, List routineItems) { + this.name = name; + this.routineItems = routineItems; + } @Getter public static class RoutineItemDto{ - private Long exerciseId; - private Integer sets; - private Integer reps; - private Integer weight; - private Integer order; + private final Long exerciseId; + private final Integer sets; + private final Integer reps; + private final Integer weight; + private final Integer order; + + public RoutineItemDto(Long exerciseId, Integer sets, Integer reps, Integer weight, Integer order) { + this.exerciseId = exerciseId; + this.sets = sets; + this.reps = reps; + this.weight = weight; + this.order = order; + } } }