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/exercise/controller/ExerciseController.java b/cloud/src/main/java/com/project/cloud/domain/exercise/controller/ExerciseController.java new file mode 100644 index 0000000..bdae0e5 --- /dev/null +++ b/cloud/src/main/java/com/project/cloud/domain/exercise/controller/ExerciseController.java @@ -0,0 +1,30 @@ +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 +public class ExerciseController { + + private final ExerciseService exerciseService; + + @GetMapping("/{exerciseId}") + 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/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/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 new file mode 100644 index 0000000..8cb060b --- /dev/null +++ b/cloud/src/main/java/com/project/cloud/domain/exercise/service/ExerciseService.java @@ -0,0 +1,42 @@ +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.enumerate.Target; +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; + +import java.util.List; + +@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); + } + + 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(); + } +} 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; + } } }