From 73ea4af5e12f2babde5fed9a1dcdf8d8f1abb6e2 Mon Sep 17 00:00:00 2001 From: jaeuk520 Date: Sun, 22 Sep 2024 21:03:10 +0900 Subject: [PATCH] =?UTF-8?q?[#33]=20feat:=20=EC=9B=B9=EC=86=8C=EC=BC=93=20?= =?UTF-8?q?=EC=97=B0=EB=8F=99=20=EB=B0=8F=20=EC=B1=84=ED=8C=85=20=EC=A0=84?= =?UTF-8?q?=EC=86=A1=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ku/covigator/config/WebSocketConfig.java | 38 ++++++++++++ .../controller/WebSocketController.java | 28 +++++++++ .../dto/request/ChatMessageRequest.java | 5 ++ .../dto/response/SaveMessageResponse.java | 17 ++++++ .../security/jwt/StompJwtInterceptor.java | 40 ++++++++++++ .../com/ku/covigator/service/ChatService.java | 33 +++++++++- .../ku/covigator/service/ChatServiceTest.java | 61 +++++++++++++++++++ 7 files changed, 221 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/ku/covigator/config/WebSocketConfig.java create mode 100644 src/main/java/com/ku/covigator/controller/WebSocketController.java create mode 100644 src/main/java/com/ku/covigator/dto/request/ChatMessageRequest.java create mode 100644 src/main/java/com/ku/covigator/dto/response/SaveMessageResponse.java create mode 100644 src/main/java/com/ku/covigator/security/jwt/StompJwtInterceptor.java diff --git a/src/main/java/com/ku/covigator/config/WebSocketConfig.java b/src/main/java/com/ku/covigator/config/WebSocketConfig.java new file mode 100644 index 0000000..29142be --- /dev/null +++ b/src/main/java/com/ku/covigator/config/WebSocketConfig.java @@ -0,0 +1,38 @@ +package com.ku.covigator.config; + +import com.ku.covigator.security.jwt.StompJwtInterceptor; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.simp.config.ChannelRegistration; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; + +@Configuration +@EnableWebSocketMessageBroker +@RequiredArgsConstructor +public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + + private final StompJwtInterceptor stompJwtInterceptor; + + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + + registry.addEndpoint("/ws-chat") + .setAllowedOriginPatterns("*") + .withSockJS(); + } + + @Override + public void configureMessageBroker(MessageBrokerRegistry registry) { + + registry.enableSimpleBroker("/topic"); + registry.setApplicationDestinationPrefixes("/app"); + } + + @Override + public void configureClientInboundChannel(ChannelRegistration registration) { + registration.interceptors(stompJwtInterceptor); + } +} \ No newline at end of file diff --git a/src/main/java/com/ku/covigator/controller/WebSocketController.java b/src/main/java/com/ku/covigator/controller/WebSocketController.java new file mode 100644 index 0000000..55067d6 --- /dev/null +++ b/src/main/java/com/ku/covigator/controller/WebSocketController.java @@ -0,0 +1,28 @@ +package com.ku.covigator.controller; + +import com.ku.covigator.dto.request.ChatMessageRequest; +import com.ku.covigator.dto.response.SaveMessageResponse; +import com.ku.covigator.service.ChatService; +import lombok.RequiredArgsConstructor; +import org.springframework.messaging.handler.annotation.*; +import org.springframework.messaging.simp.SimpMessageHeaderAccessor; +import org.springframework.messaging.simp.SimpMessageSendingOperations; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +public class WebSocketController { + + private final SimpMessageSendingOperations simpleMessageSendingOperations; + private final ChatService chatService; + + @MessageMapping("/chat/{course_id}") + @SendTo("/topic/chat/{course_id}") + public void sendMessage(@DestinationVariable(value = "course_id") Long courseId, + SimpMessageHeaderAccessor accessor, + @Payload ChatMessageRequest request) { + Long memberId = Long.parseLong(accessor.getSessionAttributes().get("memberId").toString()); + SaveMessageResponse saveMessageResponse = chatService.saveMessage(memberId, courseId, request.message()); + simpleMessageSendingOperations.convertAndSend("/topic/chat/" + courseId, saveMessageResponse); + } +} diff --git a/src/main/java/com/ku/covigator/dto/request/ChatMessageRequest.java b/src/main/java/com/ku/covigator/dto/request/ChatMessageRequest.java new file mode 100644 index 0000000..e1d5ab6 --- /dev/null +++ b/src/main/java/com/ku/covigator/dto/request/ChatMessageRequest.java @@ -0,0 +1,5 @@ +package com.ku.covigator.dto.request; + +public record ChatMessageRequest(String message) { + +} diff --git a/src/main/java/com/ku/covigator/dto/response/SaveMessageResponse.java b/src/main/java/com/ku/covigator/dto/response/SaveMessageResponse.java new file mode 100644 index 0000000..1b5811e --- /dev/null +++ b/src/main/java/com/ku/covigator/dto/response/SaveMessageResponse.java @@ -0,0 +1,17 @@ +package com.ku.covigator.dto.response; + +import lombok.Builder; + +import java.sql.Timestamp; + +@Builder +public record SaveMessageResponse(String sender, String message, String timestamp) { + + public static SaveMessageResponse of(String sender, String message) { + return SaveMessageResponse.builder() + .sender(sender) + .message(message) + .timestamp(String.valueOf(new Timestamp(System.currentTimeMillis()))) + .build(); + } +} diff --git a/src/main/java/com/ku/covigator/security/jwt/StompJwtInterceptor.java b/src/main/java/com/ku/covigator/security/jwt/StompJwtInterceptor.java new file mode 100644 index 0000000..812b7af --- /dev/null +++ b/src/main/java/com/ku/covigator/security/jwt/StompJwtInterceptor.java @@ -0,0 +1,40 @@ +package com.ku.covigator.security.jwt; + +import com.ku.covigator.exception.jwt.JwtExpiredException; +import com.ku.covigator.exception.jwt.JwtInvalidException; +import com.ku.covigator.exception.jwt.JwtNotFoundException; +import com.ku.covigator.exception.jwt.JwtUnsupportedTokenException; +import lombok.RequiredArgsConstructor; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.simp.stomp.StompCommand; +import org.springframework.messaging.simp.stomp.StompHeaderAccessor; +import org.springframework.messaging.support.ChannelInterceptor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class StompJwtInterceptor implements ChannelInterceptor { + + private final JwtProvider jwtProvider; + + @Override + public Message preSend(Message message, MessageChannel channel) { + + StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message); + + if (accessor.getCommand() == StompCommand.CONNECT) { + String authorizationHeader = accessor.getFirstNativeHeader("Authorization"); + + String token = jwtProvider.getTokenFromRequestHeader(authorizationHeader); + + if(jwtProvider.validateToken(token)) { + String memberId = jwtProvider.getPrincipal(token); + accessor.getSessionAttributes().put("memberId", memberId); + } + } + + return message; + } + +} \ No newline at end of file diff --git a/src/main/java/com/ku/covigator/service/ChatService.java b/src/main/java/com/ku/covigator/service/ChatService.java index 12e4f1a..49a3e3f 100644 --- a/src/main/java/com/ku/covigator/service/ChatService.java +++ b/src/main/java/com/ku/covigator/service/ChatService.java @@ -1,12 +1,18 @@ package com.ku.covigator.service; import com.ku.covigator.domain.Chat; +import com.ku.covigator.domain.Course; +import com.ku.covigator.domain.member.Member; +import com.ku.covigator.dto.response.SaveMessageResponse; import com.ku.covigator.exception.notfound.NotFoundCourseException; +import com.ku.covigator.exception.notfound.NotFoundMemberException; import com.ku.covigator.repository.ChatRepository; import com.ku.covigator.repository.CourseRepository; +import com.ku.covigator.repository.MemberRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import java.sql.Timestamp; import java.util.List; @Service @@ -15,11 +21,36 @@ public class ChatService { private final ChatRepository chatRepository; private final CourseRepository courseRepository; - + private final MemberRepository memberRepository; + public List getChatHistory(Long courseId) { courseRepository.findById(courseId).orElseThrow(NotFoundCourseException::new); return chatRepository.findChatByCourseIdOrderByTimestampAsc(courseId); } + public SaveMessageResponse saveMessage(Long memberId, Long courseId, String message) { + + Member member = memberRepository.findById(memberId) + .orElseThrow(NotFoundMemberException::new); + + Course course = courseRepository.findById(courseId) + .orElseThrow(NotFoundCourseException::new); + + Chat chat = buildChat(course.getId(), message, member); + + chatRepository.save(chat); + + return SaveMessageResponse.of(member.getNickname(), message); + } + + private Chat buildChat(Long courseId, String message, Member member) { + return Chat.builder() + .message(message) + .nickname(member.getNickname()) + .timestamp(String.valueOf(new Timestamp(System.currentTimeMillis()))) + .courseId(courseId) + .build(); + } + } diff --git a/src/test/java/com/ku/covigator/service/ChatServiceTest.java b/src/test/java/com/ku/covigator/service/ChatServiceTest.java index 747a567..39137b6 100644 --- a/src/test/java/com/ku/covigator/service/ChatServiceTest.java +++ b/src/test/java/com/ku/covigator/service/ChatServiceTest.java @@ -2,10 +2,16 @@ import com.ku.covigator.domain.Chat; import com.ku.covigator.domain.Course; +import com.ku.covigator.domain.member.Member; +import com.ku.covigator.domain.member.Platform; +import com.ku.covigator.dto.response.SaveMessageResponse; import com.ku.covigator.exception.notfound.NotFoundCourseException; +import com.ku.covigator.exception.notfound.NotFoundMemberException; import com.ku.covigator.repository.ChatRepository; import com.ku.covigator.repository.CourseRepository; +import com.ku.covigator.repository.MemberRepository; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -26,11 +32,14 @@ class ChatServiceTest { private ChatRepository chatRepository; @Autowired private CourseRepository courseRepository; + @Autowired + private MemberRepository memberRepository; @AfterEach void tearDown() { courseRepository.deleteAllInBatch(); chatRepository.deleteAll(); + memberRepository.deleteAllInBatch(); } @DisplayName("채팅 기록을 오래된 순으로 조회한다.") @@ -86,4 +95,56 @@ void getChatHistoryFailsWhenNotFoundCourse() { .isInstanceOf(NotFoundCourseException.class); } + @DisplayName("채팅 메시지를 저장한다.") + @Test + void saveMessage() { + //given + Member member = Member.builder() + .platform(Platform.KAKAO) + .nickname("김코비") + .email("covi@naver.com") + .build(); + Member savedMember = memberRepository.save(member); + + Course course = Course.builder() + .name("건대 풀코스") + .isPublic('Y') + .description("건대 핫플 리스트") + .build(); + Course savedCourse = courseRepository.save(course); + + //when + SaveMessageResponse response = chatService.saveMessage(savedMember.getId(), savedCourse.getId(), "여기 좋아요"); + + //then + Assertions.assertAll( + () -> assertThat(response.message()).isEqualTo("여기 좋아요"), + () -> assertThat(response.sender()).isEqualTo("김코비") + ); + } + + @DisplayName("존재하지 않는 회원에 대한 채팅 메시지 저장은 예외를 발생시킨다.") + @Test + void saveMessageFailsWhenMemberNotFound() { + + //when //then + assertThatThrownBy(() -> chatService.saveMessage(100L, 100L, "")) + .isInstanceOf(NotFoundMemberException.class); + } + + @DisplayName("존재하지 않는 코스에 대한 채팅 메시지 저장은 예외를 발생시킨다.") + @Test + void saveMessageFailsWhenCourseNotFound() { + Member member = Member.builder() + .platform(Platform.KAKAO) + .nickname("김코비") + .email("covi@naver.com") + .build(); + Member savedMember = memberRepository.save(member); + + //when //then + assertThatThrownBy(() -> chatService.saveMessage(savedMember.getId(), 100L, "")) + .isInstanceOf(NotFoundCourseException.class); + } + } \ No newline at end of file