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

채팅 기능 구현 #85

Merged
merged 5 commits into from
Sep 22, 2023
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
5 changes: 5 additions & 0 deletions be/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ dependencies {

//redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'

// webSocket
implementation 'org.springframework.boot:spring-boot-starter-websocket'
implementation 'org.webjars:stomp-websocket:2.3.4'
implementation 'org.springframework.security:spring-security-messaging'
}

tasks.named('test') {
Expand Down
42 changes: 42 additions & 0 deletions be/src/main/java/kr/codesquad/chat/controller/ChatController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package kr.codesquad.chat.controller;

import javax.servlet.http.HttpServletRequest;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import kr.codesquad.chat.dto.request.ChatMessageRequest;
import kr.codesquad.chat.dto.request.ChatRoomCreateRequest;
import kr.codesquad.chat.dto.request.SendMessageRequest;
import kr.codesquad.chat.dto.response.ChatRoomCreateResponse;
import kr.codesquad.chat.service.ChatService;
import kr.codesquad.util.Constants;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@RestController
public class ChatController {

private final ChatService chatService;

@PostMapping("/api/chatrooms")
public ResponseEntity<ChatRoomCreateResponse> createChatRoom(
@RequestBody ChatRoomCreateRequest chatRoomCreateRequest,
HttpServletRequest request) {
String loginId = (String)request.getAttribute(Constants.LOGIN_ID);
return ResponseEntity.status(HttpStatus.CREATED)
.body(chatService.createRoom(chatRoomCreateRequest, loginId));
}

@MessageMapping("/chatrooms/{chatRoomId}")
public void message(ChatMessageRequest message, @PathVariable Long chatRoomId) {
chatService.sendMessage(message, chatRoomId);
}
}
26 changes: 26 additions & 0 deletions be/src/main/java/kr/codesquad/chat/dto/ChatMapper.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package kr.codesquad.chat.dto;

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;

import kr.codesquad.chat.dto.request.ChatMessageRequest;
import kr.codesquad.chat.dto.request.ChatRoomCreateRequest;
import kr.codesquad.chat.dto.request.SendMessageRequest;
import kr.codesquad.chat.dto.response.ChatRoomCreateResponse;
import kr.codesquad.chat.entity.ChatMessage;
import kr.codesquad.chat.entity.ChatRoom;

@Mapper
public interface ChatMapper {
ChatMapper INSTANCE = Mappers.getMapper(ChatMapper.class);
@Mapping(target = "senderId", source = "userId")
ChatRoom toChatRoom(ChatRoomCreateRequest chatRoomCreateRequest, Long userId);

@Mapping(target = "chatRoomId", source = "id")
ChatRoomCreateResponse toChatRoomCreateResponse(ChatRoom chatRoom);

ChatMessage toChatMessage(ChatMessageRequest chatMessageRequest, Long chatRoomId);

SendMessageRequest toSendMessageRequest(ChatMessage chatMessage, Long senderId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package kr.codesquad.chat.dto.request;

import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class ChatMessageRequest {

private Long senderId;
private String content;

@Builder
public ChatMessageRequest(Long senderId, String content) {
this.senderId = senderId;
this.content = content;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package kr.codesquad.chat.dto.request;

import lombok.Getter;

@Getter
public class ChatRoomCreateRequest {
private Long itemId;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package kr.codesquad.chat.dto.request;

import java.time.LocalDateTime;
import java.time.ZonedDateTime;

import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class SendMessageRequest {
private Long chatRoomId;
private Long senderId;
private String content;
private LocalDateTime createdAt;

@Builder
public SendMessageRequest(Long chatRoomId, Long senderId, String content, ZonedDateTime createdAt) {
this.chatRoomId = chatRoomId;
this.senderId = senderId;
this.content = content;
this.createdAt = createdAt.toLocalDateTime();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package kr.codesquad.chat.dto.response;

import lombok.Builder;
import lombok.Getter;

@Getter
@Builder
public class ChatRoomCreateResponse {
private final Long chatRoomId;
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,19 @@
@Getter
@Entity
@NoArgsConstructor
public class Message extends TimeStamped {
public class ChatMessage extends TimeStamped {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private Long chatId;
private Long chatRoomId;
@Column(nullable = false, length = 200)
private String content;

@Builder
public Message(Long id, Long chatId, String content) {
public ChatMessage(Long id, Long chatRoomId, String content) {
this.id = id;
this.chatId = chatId;
this.chatRoomId = chatRoomId;
this.content = content;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,19 @@
@Getter
@Entity
@NoArgsConstructor
public class Chat {
public class ChatRoom {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private Long itemId;
@Column(nullable = false, length = 45)
@Column(length = 45)
private String lastMessageId;
@Column(nullable = false)
private Long senderId;

@Builder
public Chat(Long id, Long itemId, String lastMessageId, Long senderId) {
public ChatRoom(Long id, Long itemId, String lastMessageId, Long senderId) {
this.id = id;
this.itemId = itemId;
this.lastMessageId = lastMessageId;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package kr.codesquad.chat.repository;

import org.springframework.data.jpa.repository.JpaRepository;

import kr.codesquad.chat.entity.ChatMessage;

public interface ChatMessageRepository extends JpaRepository<ChatMessage, Long> {

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

import org.springframework.data.jpa.repository.JpaRepository;

import kr.codesquad.chat.entity.Chat;
import kr.codesquad.chat.entity.ChatRoom;

public interface ChatRepository extends JpaRepository<Chat, Long> {
public interface ChatRoomRepository extends JpaRepository<ChatRoom, Long> {
int countByItemId(Long itemId);
}

This file was deleted.

50 changes: 50 additions & 0 deletions be/src/main/java/kr/codesquad/chat/service/ChatService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package kr.codesquad.chat.service;

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.fasterxml.jackson.databind.ObjectMapper;

import kr.codesquad.chat.dto.ChatMapper;
import kr.codesquad.chat.dto.request.ChatMessageRequest;
import kr.codesquad.chat.dto.request.ChatRoomCreateRequest;
import kr.codesquad.chat.dto.request.SendMessageRequest;
import kr.codesquad.chat.dto.response.ChatRoomCreateResponse;
import kr.codesquad.chat.entity.ChatMessage;
import kr.codesquad.chat.repository.ChatMessageRepository;
import kr.codesquad.chat.repository.ChatRoomRepository;
import kr.codesquad.user.entity.User;
import kr.codesquad.user.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@RequiredArgsConstructor
@Service
public class ChatService {

private final ChatRoomRepository chatRoomRepository;
private final ChatMessageRepository chatMessageRepository;
private final UserRepository userRepository;
private final ObjectMapper objectMapper;
private final ChannelTopic channelTopic;
private final RedisTemplate redisTemplate;

public ChatRoomCreateResponse createRoom(ChatRoomCreateRequest chatRoomCreateRequest, String loginId) {
User user = userRepository.findByLoginId(loginId);
return ChatMapper.INSTANCE.toChatRoomCreateResponse(
chatRoomRepository.save(ChatMapper.INSTANCE.toChatRoom(chatRoomCreateRequest, user.getId())));
}

@Transactional
public void sendMessage(ChatMessageRequest chatMessageRequest, Long chatRoomId) {

//채팅 생성 및 저장
ChatMessage chatMessage = chatMessageRepository.save(ChatMapper.INSTANCE.toChatMessage(chatMessageRequest, chatRoomId));
String topic = channelTopic.getTopic();

redisTemplate.convertAndSend(topic, ChatMapper.INSTANCE.toSendMessageRequest(chatMessage, chatMessageRequest.getSenderId()));
}
}
36 changes: 36 additions & 0 deletions be/src/main/java/kr/codesquad/chat/service/RedisSubscriber.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package kr.codesquad.chat.service;

import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.messaging.simp.SimpMessageSendingOperations;
import org.springframework.stereotype.Service;

import com.fasterxml.jackson.databind.ObjectMapper;

import kr.codesquad.chat.dto.request.SendMessageRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@RequiredArgsConstructor
@Service
public class RedisSubscriber implements MessageListener {
private final ObjectMapper objectMapper;
private final RedisTemplate redisTemplate;
private final SimpMessageSendingOperations messagingTemplate;

@Override
public void onMessage(Message message, byte[] pattern) {
try {
String publishMessage = (String)redisTemplate.getStringSerializer().deserialize(message.getBody());

SendMessageRequest roomMessage = objectMapper.readValue(publishMessage, SendMessageRequest.class);

messagingTemplate.convertAndSend("/sub/chatrooms/" + roomMessage.getChatRoomId(), roomMessage);

} catch (Exception e) {
throw new RuntimeException("뭐");
}
}
}
32 changes: 29 additions & 3 deletions be/src/main/java/kr/codesquad/core/config/RedisConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,15 @@
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import kr.codesquad.redis.RedisProperties;
import kr.codesquad.chat.service.RedisSubscriber;
import kr.codesquad.user.redis.RedisProperties;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
Expand All @@ -28,8 +33,29 @@ public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory());
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());

redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(String.class));
return redisTemplate;
}

@Bean
public RedisMessageListenerContainer redisMessageListenerContainer(
RedisConnectionFactory connectionFactory,
MessageListenerAdapter listenerAdapter,
ChannelTopic channelTopic
) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
container.addMessageListener(listenerAdapter, channelTopic);
return container;
}

@Bean
public MessageListenerAdapter listenerAdapter(RedisSubscriber subscriber) {
return new MessageListenerAdapter(subscriber, "onMessage");
}

@Bean
public ChannelTopic channelTopic() { // (4)
return new ChannelTopic("chatRoom");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ protected SecurityFilterChain configure(HttpSecurity http) throws Exception {
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/h2-console/**", "/api/locations**").permitAll()
.antMatchers("/h2-console/**", "/api/locations**", "/api/ws/**").permitAll()
.antMatchers(HttpMethod.POST, "/api/users").permitAll()
.anyRequest()
.authenticated() //다른 요청은 인증 필요함
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package kr.codesquad.core.config;

import org.springframework.security.config.annotation.web.messaging.MessageSecurityMetadataSourceRegistry;
import org.springframework.security.config.annotation.web.socket.AbstractSecurityWebSocketMessageBrokerConfigurer;

public class SecurityWebSocketConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {

@Override
protected void configureInbound(MessageSecurityMetadataSourceRegistry message) {
message
.nullDestMatcher().permitAll()
.simpDestMatchers("/api/ws/pub/**").authenticated()
.simpSubscribeDestMatchers("/api/ws/sub/**").authenticated()
.anyMessage().denyAll();
}

@Override
protected boolean sameOriginDisabled() {
// CSRF 비활성화
return true;
}
}
Loading