diff --git a/src/main/java/store/storymate/storymatebackend/chatting/config/SocketHandler.java b/src/main/java/store/storymate/storymatebackend/chatting/config/SocketHandler.java index 91e0d22..ee69ffe 100644 --- a/src/main/java/store/storymate/storymatebackend/chatting/config/SocketHandler.java +++ b/src/main/java/store/storymate/storymatebackend/chatting/config/SocketHandler.java @@ -15,7 +15,6 @@ import store.storymate.storymatebackend.chatting.application.ChatMessageService; import store.storymate.storymatebackend.chatting.application.ChatRoomService; import store.storymate.storymatebackend.chatting.domain.ChatRoom; -import store.storymate.storymatebackend.chatting.domain.repository.ChatRoomRepository; import store.storymate.storymatebackend.member.application.MemberService; import store.storymate.storymatebackend.member.domain.Member; @@ -30,81 +29,87 @@ public class SocketHandler extends TextWebSocketHandler { @Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { - String payload = message.getPayload(); - String[] data = payload.split(":", 4); // ":" 기준으로 메시지 분리 (sender:roomId:content) + ParsedMessage parsedMessage = parseMessage(message.getPayload()); + if (parsedMessage == null) { + session.sendMessage( + new TextMessage("Invalid message format. Use 'sender:roomId:bookName:message' format.")); + return; + } - if (data.length < 4) { - session.sendMessage(new TextMessage("Invalid message format. Use 'sender:roomId:bookName:message' format.")); + // 메시지 카운트 검증 및 감소 + if (!validateAndDecreaseMessageCount(parsedMessage.roomId(), session)) { return; } - String sender = data[0]; // 발신자 - String roomId = data[1]; // 채팅방 ID - String bookTitle = data[2]; // 책 제목 - String content = data[3]; // 메시지 내용 + // 사용자 메시지 저장 + saveUserMessage(parsedMessage); + + // AI 응답 처리 및 저장 + processAiResponse(parsedMessage); + } + + private ParsedMessage parseMessage(String payload) { + String[] data = payload.split(":", 4); + if (data.length < 4) { + return null; + } + return new ParsedMessage(data[0], data[1], data[2], data[3]); + } - // 0. 메시지를 보낸 사용자의 messageCount 감소 + private boolean validateAndDecreaseMessageCount(String roomId, WebSocketSession session) throws Exception { Optional chatRoomOptional = chatRoomService.findChatRoomById(Long.parseLong(roomId)); if (chatRoomOptional.isPresent()) { ChatRoom chatRoom = chatRoomOptional.get(); Member member = chatRoom.getMember(); - if (member != null) { - Long messageCount = member.getMessageCount(); // ✅ 현재 messageCount 조회 - - if (messageCount <= 0) { - session.sendMessage(new TextMessage("⚠️ 메시지를 보낼 수 없습니다. 남은 메시지 횟수가 없습니다.")); - return; - } - - memberService.decreaseMessageCount(member.getId()); + if (member != null && member.getMessageCount() <= 0) { + session.sendMessage(new TextMessage("⚠️ 메시지를 보낼 수 없습니다. 남은 메시지 횟수가 없습니다.")); + return false; } + memberService.decreaseMessageCount(member.getId()); } + return true; + } - // 1. 메시지 저장 - ChatMessageSaveReqDto chatMessageSaveMemberReqDto = - ChatMessageSaveReqDto.builder() - .roomId(roomId) - .sender(sender) - .content(content) - .build(); + private void saveUserMessage(ParsedMessage parsedMessage) throws Exception { + ChatMessageSaveReqDto chatMessageDto = ChatMessageSaveReqDto.builder() + .roomId(parsedMessage.roomId()) + .sender(parsedMessage.sender()) + .content(parsedMessage.content()) + .build(); - chatMessageService.saveMessage(chatMessageSaveMemberReqDto); + chatMessageService.saveMessage(chatMessageDto); + + broadcastMessage(parsedMessage.roomId(), parsedMessage.sender(), parsedMessage.content()); + } - // 2. AI 서버에 메시지 보내고 응답 받기 - String charactersName = chatRoomService.getCharacterName(Long.parseLong(roomId)); + private void processAiResponse(ParsedMessage parsedMessage) throws Exception { + String charactersName = chatRoomService.getCharacterName(Long.parseLong(parsedMessage.roomId())); - ChatMessageSaveReqDto chatMessageAi = - ChatMessageSaveReqDto.builder() - .roomId(roomId) - .sender(charactersName) - .content(content) - .bookTitle(bookTitle) - .build(); + ChatMessageSaveReqDto chatMessageAi = ChatMessageSaveReqDto.builder() + .roomId(parsedMessage.roomId()) + .sender(charactersName) + .content(parsedMessage.content()) + .bookTitle(parsedMessage.bookTitle()) + .build(); String aiResponse = chatMessageService.callAiApi(chatMessageAi); - // 3. AI 응답 저장 - ChatMessageSaveReqDto chatMessageSaveAiReqDto = - ChatMessageSaveReqDto.builder() - .roomId(roomId) - .sender(charactersName) - .content(aiResponse) - .build(); + ChatMessageSaveReqDto chatMessageSaveAiReqDto = ChatMessageSaveReqDto.builder() + .roomId(parsedMessage.roomId()) + .sender(charactersName) + .content(aiResponse) + .build(); chatMessageService.saveMessage(chatMessageSaveAiReqDto); - - // 4. 사용자 메시지와 AI 응답을 모두 채팅방에 전송 - broadcastMessageToRoom(roomId, sender + ": " + content); - broadcastMessageToRoom(roomId, charactersName + ": " + aiResponse); + broadcastMessage(parsedMessage.roomId(), charactersName, aiResponse); } - // 채팅방의 모든 사용자에게 메시지 전송 - private void broadcastMessageToRoom(String roomId, String message) throws Exception { + private void broadcastMessage(String roomId, String sender, String content) throws Exception { if (chatRooms.containsKey(roomId)) { for (WebSocketSession webSocketSession : chatRooms.get(roomId)) { if (webSocketSession.isOpen()) { - webSocketSession.sendMessage(new TextMessage(message)); + webSocketSession.sendMessage(new TextMessage(sender + ": " + content)); } } } @@ -114,7 +119,6 @@ private void broadcastMessageToRoom(String roomId, String message) throws Except public void afterConnectionEstablished(WebSocketSession session) { String uri = session.getUri().toString(); String roomId = uri.substring(uri.lastIndexOf("/") + 1); - chatRooms.computeIfAbsent(roomId, k -> new ArrayList<>()).add(session); } @@ -122,4 +126,7 @@ public void afterConnectionEstablished(WebSocketSession session) { public void afterConnectionClosed(WebSocketSession session, CloseStatus status) { chatRooms.forEach((roomId, sessions) -> sessions.remove(session)); } + + private record ParsedMessage(String sender, String roomId, String bookTitle, String content) { + } } diff --git a/src/main/java/store/storymate/storymatebackend/quiz/application/QuizApiClient.java b/src/main/java/store/storymate/storymatebackend/quiz/application/QuizApiClient.java new file mode 100644 index 0000000..85a45b2 --- /dev/null +++ b/src/main/java/store/storymate/storymatebackend/quiz/application/QuizApiClient.java @@ -0,0 +1,81 @@ +package store.storymate.storymatebackend.quiz.application; + +import jakarta.annotation.PostConstruct; +import java.util.HashMap; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatusCode; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.util.UriComponentsBuilder; +import reactor.core.publisher.Mono; +import store.storymate.storymatebackend.quiz.api.dto.request.QuizAnswerReqDto; +import store.storymate.storymatebackend.quiz.api.dto.request.QuizQuestionReqDto; +import store.storymate.storymatebackend.quiz.api.dto.response.QuizAnswerResDto; +import store.storymate.storymatebackend.quiz.api.dto.response.QuizQuestionResDto; +import store.storymate.storymatebackend.quiz.exception.AiQuizQuestionException; + +@Component +@RequiredArgsConstructor +public class QuizApiClient { + + private final WebClient.Builder webClientBuilder; + + @Value("${ai.characters}") + private String baseUrl; + + private WebClient webClient; + + @PostConstruct + private void initWebClient() { + this.webClient = webClientBuilder.baseUrl(baseUrl).build(); + } + + public QuizQuestionResDto fetchQuizQuestion(QuizQuestionReqDto requestDto) { + Map requestBody = new HashMap<>(); + requestBody.put("character_name", requestDto.characterName()); + requestBody.put("quiz_type", requestDto.quizType()); + requestBody.put("book_title", requestDto.bookTitle()); + + String encodedUri = UriComponentsBuilder.fromPath("/quiz_question") + .encode() + .toUriString(); + + return webClient.post() + .uri(encodedUri) + .bodyValue(requestBody) + .retrieve() + .onStatus(HttpStatusCode::isError, clientResponse -> + clientResponse.bodyToMono(String.class) + .map(AiQuizQuestionException::new) + .flatMap(Mono::error) + ) + .bodyToMono(QuizQuestionResDto.class) + .block(); + } + + public QuizAnswerResDto evaluateQuizAnswer(QuizAnswerReqDto requestDto) { + Map requestBody = new HashMap<>(); + requestBody.put("book_title", requestDto.bookTitle()); + requestBody.put("character_name", requestDto.characterName()); + requestBody.put("quiz_type", requestDto.quizType()); + requestBody.put("user_answer", requestDto.userAnswer()); + + String encodedUri = UriComponentsBuilder.fromPath("/evaluate_quiz") + .encode() + .toUriString(); + + return webClient.post() + .uri(encodedUri) + .bodyValue(requestBody) + .retrieve() + .onStatus(HttpStatusCode::isError, clientResponse -> + clientResponse.bodyToMono(String.class) + .map(AiQuizQuestionException::new) + .flatMap(Mono::error) + ) + .bodyToMono(QuizAnswerResDto.class) + .block(); + } +} diff --git a/src/main/java/store/storymate/storymatebackend/quiz/application/QuizService.java b/src/main/java/store/storymate/storymatebackend/quiz/application/QuizService.java index b1abb9b..0c50bcf 100644 --- a/src/main/java/store/storymate/storymatebackend/quiz/application/QuizService.java +++ b/src/main/java/store/storymate/storymatebackend/quiz/application/QuizService.java @@ -1,16 +1,8 @@ package store.storymate.storymatebackend.quiz.application; -import jakarta.annotation.PostConstruct; -import java.util.HashMap; -import java.util.Map; import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.http.HttpStatusCode; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.reactive.function.client.WebClient; -import org.springframework.web.util.UriComponentsBuilder; -import reactor.core.publisher.Mono; import store.storymate.storymatebackend.global.util.MemberUtil; import store.storymate.storymatebackend.member.domain.Member; import store.storymate.storymatebackend.quiz.api.dto.request.QuizAnswerReqDto; @@ -18,77 +10,26 @@ import store.storymate.storymatebackend.quiz.api.dto.response.QuizAnswerResDto; import store.storymate.storymatebackend.quiz.api.dto.response.QuizQuestionResDto; import store.storymate.storymatebackend.quiz.domain.CorrectAnswerType; -import store.storymate.storymatebackend.quiz.exception.AiQuizQuestionException; @Service @Transactional(readOnly = true) @RequiredArgsConstructor public class QuizService { - private WebClient webClient; + private final QuizApiClient quizApiClient; private final MemberUtil memberUtil; - @Value("${ai.characters}") - private String baseUrl; - - @PostConstruct - private void initWebClient() { - this.webClient = WebClient.builder() - .baseUrl(baseUrl) - .build(); - } - // AI 문제 받는 기능 public QuizQuestionResDto callAiQuestionApi(QuizQuestionReqDto quizQuestionReqDto) { - Map requestBody = new HashMap<>(); - requestBody.put("character_name", quizQuestionReqDto.characterName()); - requestBody.put("quiz_type", quizQuestionReqDto.quizType()); - requestBody.put("book_title", quizQuestionReqDto.bookTitle()); - - String encodedUri = UriComponentsBuilder.fromPath("/quiz_question") - .encode() - .toUriString(); - - return webClient.post() - .uri(encodedUri) - .bodyValue(requestBody) - .retrieve() - .onStatus(HttpStatusCode::isError, clientResponse -> - clientResponse.bodyToMono(String.class) - .map(AiQuizQuestionException::new) - .flatMap(Mono::error) - ) - .bodyToMono(QuizQuestionResDto.class) - .block(); + return quizApiClient.fetchQuizQuestion(quizQuestionReqDto); } @Transactional - public QuizAnswerResDto callAiAnswerApi(QuizAnswerReqDto quizQuestionReqDto) { - Map requestBody = new HashMap<>(); - requestBody.put("book_title", quizQuestionReqDto.bookTitle()); - requestBody.put("character_name", quizQuestionReqDto.characterName()); - requestBody.put("quiz_type", quizQuestionReqDto.quizType()); - requestBody.put("user_answer", quizQuestionReqDto.userAnswer()); - - String encodedUri = UriComponentsBuilder.fromPath("/evaluate_quiz") - .encode() - .toUriString(); - - QuizAnswerResDto response = webClient.post() - .uri(encodedUri) - .bodyValue(requestBody) - .retrieve() - .onStatus(HttpStatusCode::isError, clientResponse -> - clientResponse.bodyToMono(String.class) - .map(AiQuizQuestionException::new) - .flatMap(Mono::error) - ) - .bodyToMono(QuizAnswerResDto.class) - .block(); - - Member member = memberUtil.getCurrentMember(); + public QuizAnswerResDto callAiAnswerApi(QuizAnswerReqDto quizAnswerReqDto) { + QuizAnswerResDto response = quizApiClient.evaluateQuizAnswer(quizAnswerReqDto); if (response != null) { + Member member = memberUtil.getCurrentMember(); CorrectAnswerType answerType = CorrectAnswerType.fromString(response.correct()); if (answerType != null) {