Skip to content

Commit

Permalink
[#33] feat: 웹소켓 연동 및 채팅 전송 기능 구현
Browse files Browse the repository at this point in the history
  • Loading branch information
jaeuk520 committed Sep 22, 2024
1 parent d1cf1c5 commit 73ea4af
Show file tree
Hide file tree
Showing 7 changed files with 221 additions and 1 deletion.
38 changes: 38 additions & 0 deletions src/main/java/com/ku/covigator/config/WebSocketConfig.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
28 changes: 28 additions & 0 deletions src/main/java/com/ku/covigator/controller/WebSocketController.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.ku.covigator.dto.request;

public record ChatMessageRequest(String message) {

}
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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;
}

}
33 changes: 32 additions & 1 deletion src/main/java/com/ku/covigator/service/ChatService.java
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -15,11 +21,36 @@ public class ChatService {

private final ChatRepository chatRepository;
private final CourseRepository courseRepository;

private final MemberRepository memberRepository;

public List<Chat> 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();
}

}
61 changes: 61 additions & 0 deletions src/test/java/com/ku/covigator/service/ChatServiceTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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("채팅 기록을 오래된 순으로 조회한다.")
Expand Down Expand Up @@ -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);
}

}

0 comments on commit 73ea4af

Please sign in to comment.