Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat] 채팅 기능 구현 #46

Merged
merged 5 commits into from
Sep 22, 2024
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
2 changes: 2 additions & 0 deletions .github/workflows/backend-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ jobs:
S3_BUCKET_NAME: ${{ secrets.S3_BUCKET_NAME }}
AWS_ACCESS_KEY: ${{ secrets.ACCESS_KEY_ID }}
AWS_SECRET_KEY: ${{ secrets.ACCESS_KEY_SECRET }}
MONGO_DB_URI: ${{ secrets.MONGO_DB_URI }}
MONGO_DB_DATABASE: ${{ secrets.MONGO_DB_DATABASE }}
with:
arguments: check
cache-read-only: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/develop' }}
Expand Down
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ dependencies {

//websocket
implementation 'org.springframework.boot:spring-boot-starter-websocket'

// mongoDB
implementation 'org.springframework.boot:spring-boot-starter-data-mongodb'
}

tasks.named('test') {
Expand Down
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);
}
}
30 changes: 30 additions & 0 deletions src/main/java/com/ku/covigator/controller/ChatController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.ku.covigator.controller;

import com.ku.covigator.domain.Chat;
import com.ku.covigator.dto.response.GetChatHistoryResponse;
import com.ku.covigator.service.ChatService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@Tag(name = "Chat", description = "채팅")
@RestController
@RequiredArgsConstructor
public class ChatController {

private final ChatService chatService;

@Operation(summary = "채팅 기록 조회")
@GetMapping("/chat/{course_id}")
public ResponseEntity<GetChatHistoryResponse> getChatHistory(@PathVariable(value = "course_id") Long courseId) {
List<Chat> chatList = chatService.getChatHistory(courseId);
return ResponseEntity.ok(GetChatHistoryResponse.from(chatList));
}

}
30 changes: 30 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,30 @@
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;

import java.util.Objects;

@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(Objects.requireNonNull(accessor.getSessionAttributes()).get("memberId").toString());
SaveMessageResponse saveMessageResponse = chatService.saveMessage(memberId, courseId, request.message());
simpleMessageSendingOperations.convertAndSend("/topic/chat/" + courseId, saveMessageResponse);
}
}
29 changes: 29 additions & 0 deletions src/main/java/com/ku/covigator/domain/Chat.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.ku.covigator.domain;

import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;

@Document(collection = "chat")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Chat {

@Id
private String id;
private Long courseId;
private String timestamp;
private String nickname;
private String message;

@Builder
public Chat(Long courseId, String timestamp, String nickname, String message) {
this.courseId = courseId;
this.timestamp = timestamp;
this.nickname = nickname;
this.message = message;
}
}
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,26 @@
package com.ku.covigator.dto.response;

import com.ku.covigator.domain.Chat;
import lombok.Builder;

import java.util.List;

public record GetChatHistoryResponse(List<ChatDto> chat) {

@Builder
public record ChatDto(String nickname, String timestamp, String message) {
}

public static GetChatHistoryResponse from(List<Chat> chatList) {

List<ChatDto> chatDtos = chatList.stream()
.map(chat -> ChatDto.builder()
.nickname(chat.getNickname())
.message(chat.getMessage())
.timestamp(chat.getTimestamp())
.build()
).toList();

return new GetChatHistoryResponse(chatDtos);
}
}
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();
}
}
11 changes: 11 additions & 0 deletions src/main/java/com/ku/covigator/repository/ChatRepository.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.ku.covigator.repository;

import com.ku.covigator.domain.Chat;
import org.springframework.data.mongodb.repository.MongoRepository;

import java.util.List;

public interface ChatRepository extends MongoRepository<Chat, Long> {

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

}
56 changes: 56 additions & 0 deletions src/main/java/com/ku/covigator/service/ChatService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
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
@RequiredArgsConstructor
public class ChatService {

private final ChatRepository chatRepository;
private final CourseRepository courseRepository;
private final MemberRepository memberRepository;

public List<Chat> getChatHistory(Long courseId) {

Course course = courseRepository.findById(courseId).orElseThrow(NotFoundCourseException::new);
return chatRepository.findChatByCourseIdOrderByTimestampAsc(course.getId());
}

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

}
4 changes: 4 additions & 0 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ spring:
multipart:
max-request-size: 5MB
max-file-size: 10MB
data:
mongodb:
uri: ${MONGO_DB_URI}
database: ${MONGO_DB_DATABASE}

security:
jwt:
Expand Down
66 changes: 66 additions & 0 deletions src/test/java/com/ku/covigator/controller/ChatControllerTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package com.ku.covigator.controller;

import com.ku.covigator.domain.Chat;
import com.ku.covigator.security.jwt.JwtAuthArgumentResolver;
import com.ku.covigator.security.jwt.JwtAuthInterceptor;
import com.ku.covigator.service.ChatService;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.test.web.servlet.MockMvc;

import java.util.List;

import static org.mockito.BDDMockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@ComponentScan("com.ku.covigator.support")
@WebMvcTest(controllers = ChatController.class)
class ChatControllerTest {

@Autowired
private MockMvc mockMvc;
@MockBean
private ChatService chatService;
@MockBean
private JwtAuthInterceptor jwtAuthInterceptor;
@MockBean
private JwtAuthArgumentResolver jwtAuthArgumentResolver;

@DisplayName("채팅 기록 조회를 요청한다.")
@Test
void getChatHistory() throws Exception {
//given
Long courseId = 1L;
List<Chat> chats = List.of(
Chat.builder()
.message("안녕")
.nickname("김코비")
.build(),
Chat.builder()
.message("안녕안녕")
.nickname("박코비")
.build()
);

given(chatService.getChatHistory(courseId)).willReturn(chats);

//when //then
mockMvc.perform(get("/chat/{course_id}", courseId))
.andDo(print())
.andExpectAll(
status().isOk(),
jsonPath("$.chat[0].message").value("안녕"),
jsonPath("$.chat[1].message").value("안녕안녕"),
jsonPath("$.chat[0].nickname").value("김코비"),
jsonPath("$.chat[1].nickname").value("박코비")
);
}

}
Loading
Loading