Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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<ChatResponse.Chat> chat(@RequestBody ChatRequest.Chat request) {
return SuccessResponse.ok(chatService.askChatBot(request));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.project.cloud.domain.chat.dto;

public class ChatRequest {

public record Chat(
String email,
String message
) {}
}
Original file line number Diff line number Diff line change
@@ -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<String> preferredParts,
String level,
String goal,
Integer frequencyPerWeek,
RoutineRequest routine
) {}
}
Original file line number Diff line number Diff line change
@@ -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<String, Object> body = new HashMap<>();
body.put("message", request.message());

// userData 맵 생성
Map<String, Object> 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<Map<String, Object>> httpEntity = new HttpEntity<>(body, headers);

ResponseEntity<String> 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<String> 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<RoutineRequest.RoutineItemDto> 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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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<ExerciseResponse.Detail> getExercise(@PathVariable Long exerciseId) {
return SuccessResponse.ok(exerciseService.getById(exerciseId));
}

@GetMapping
@Operation(summary = "전체 운동 리스트를 조회합니다.", description = "전체 운동 리스트를 조회합니다. 쿼리 파라미터로 target이 없으면 전체 운동 리스트, 아니면 해당하는 타겟의 운동 리스트를 검색합니다.")
public SuccessResponse<List<ExerciseResponse.Detail>> getAllExercises(@RequestParam(required = false) List<Target> target) {
return SuccessResponse.ok(exerciseService.getAllExercises(target));
}
}
Original file line number Diff line number Diff line change
@@ -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()
);
}
}
}
Original file line number Diff line number Diff line change
@@ -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<Exercise, Long> {
List<Exercise> findAllByTargetIn(List<Target> targets);
}
Original file line number Diff line number Diff line change
@@ -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<ExerciseResponse.Detail> getAllExercises(List<Target> targetList) {
List<Exercise> exercises;

if (targetList == null || targetList.isEmpty()) {
exercises = exerciseRepository.findAll();
} else {
exercises = exerciseRepository.findAllByTargetIn(targetList);
}

return exercises.stream()
.map(ExerciseResponse.Detail::from)
.toList();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,28 @@

@Getter
public class RoutineRequest {
private String name;
private List<RoutineItemDto> routineItems;
private final String name;
private final List<RoutineItemDto> routineItems;

public RoutineRequest(String name, List<RoutineItemDto> 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;
}
}
}