Skip to content

Commit

Permalink
Merge pull request #57 from kookmin-sw/feat/chatreadapi
Browse files Browse the repository at this point in the history
[Feat/Refactor] 채팅 부가기능 API 작성
  • Loading branch information
imjanghyeok authored May 10, 2024
2 parents 14c7f8c + 151919c commit 19f7f11
Show file tree
Hide file tree
Showing 27 changed files with 731 additions and 110 deletions.
16 changes: 15 additions & 1 deletion src/main/java/capstone/facefriend/chat/config/ChatConfig.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package capstone.facefriend.chat.config;

import capstone.facefriend.chat.controller.interceptor.FilterChannelInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
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.EnableWebSocket;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
Expand All @@ -12,6 +15,13 @@
@EnableWebSocketMessageBroker
public class ChatConfig implements WebSocketMessageBrokerConfigurer {

@Autowired
private FilterChannelInterceptor filterChannelInterceptor;

public ChatConfig(FilterChannelInterceptor filterChannelInterceptor) {
this.filterChannelInterceptor = filterChannelInterceptor;
}

// sockJS Fallback을 이용해 노출할 endpoint 설정
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
Expand All @@ -31,5 +41,9 @@ public void configureMessageBroker(MessageBrokerRegistry registry) {
// 클라이언트->서버로 발행하는 메세지에 대한 endpoint 설정 : 구독에 대한 메세지
registry.setApplicationDestinationPrefixes("/pub");
}
}

@Override
public void configureClientInboundChannel(ChannelRegistration registration){
registration.interceptors(filterChannelInterceptor);
}
}
13 changes: 10 additions & 3 deletions src/main/java/capstone/facefriend/chat/config/RedisConfig.java
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
package capstone.facefriend.chat.config;


import capstone.facefriend.chat.service.RedisSubscriber;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
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;

@Configuration
@RequiredArgsConstructor
@EnableRedisRepositories
public class RedisConfig {
@Value("${spring.data.redis.host}")
private String redisHost;
Expand Down Expand Up @@ -68,6 +68,13 @@ public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisC
return redisTemplate;
}

@Bean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
StringRedisTemplate redisTemplate = new StringRedisTemplate();
redisTemplate.setConnectionFactory(redisConnectionFactory);
return redisTemplate;
}

@Bean
public MessageListenerAdapter listenerAdapter(RedisSubscriber subscriber) {
return new MessageListenerAdapter(subscriber, "sendMessage");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@

import capstone.facefriend.auth.controller.support.AuthMember;
import capstone.facefriend.chat.service.ChatRoomService;
import capstone.facefriend.chat.service.dto.chatroom.ChatRoomEnterResponse;
import capstone.facefriend.chat.service.dto.chatroom.ChatRoomExitResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

Expand All @@ -22,4 +23,30 @@ ResponseEntity<Map<String, Object>> getChatRoomList(
) {
return ResponseEntity.ok(chatRoomService.getChatRoomList(memberId));
}

@PostMapping("/room/{roomId}/enter")
public ResponseEntity<ChatRoomEnterResponse> enterChatRoom(
@PathVariable("roomId") Long roomId,
@AuthMember Long memberId,
@RequestParam(required = false, defaultValue = "0", value = "page") int pageNo
){
return ResponseEntity.ok(chatRoomService.enterRoom(roomId, memberId));
}

@PostMapping("/room/{roomId}/exit")
public ResponseEntity<ChatRoomExitResponse> exitChatRoom(
@PathVariable("roomId") Long roomId,
@AuthMember Long memberId
){
return ResponseEntity.ok(chatRoomService.exitRoom(roomId, memberId));
}

@PostMapping("/room/{roomId}/left")
public ResponseEntity<String> leftChatRoom(
@PathVariable("roomId") Long roomId,
@AuthMember Long memberId
){
return ResponseEntity.ok(chatRoomService.leftRoom(roomId, memberId));
}

}
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
package capstone.facefriend.chat.controller;

import capstone.facefriend.auth.controller.support.AuthMember;
import capstone.facefriend.auth.infrastructure.JwtProvider;
import capstone.facefriend.chat.service.MessageService;
import capstone.facefriend.chat.service.dto.heart.HeartReplyRequest;
import capstone.facefriend.chat.service.dto.message.MessageRequest;
import capstone.facefriend.chat.service.dto.heart.SendHeartRequest;
import capstone.facefriend.chat.service.MessageService;
import capstone.facefriend.chat.service.dto.message.MessageListResponse;
import capstone.facefriend.chat.service.dto.message.MessageRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.ResponseEntity;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@Slf4j
Expand All @@ -21,15 +23,36 @@ public class MessageController {

private static final String BEARER_PREFIX = "Bearer ";
private final MessageService messageService;
private final MappingJackson2HttpMessageConverter converter;
private final JwtProvider jwtProvider;

@MessageMapping("/test")
@SendTo("/sub/test")
public String test() {
return "테스트 메시지";
@MessageMapping("/stomp/connect")
public void enterApp(
StompHeaderAccessor headerAccessor
){
String authorizationHeader = headerAccessor.getFirstNativeHeader("Authorization");
String token = authorizationHeader.substring(BEARER_PREFIX.length());
Long memberId = jwtProvider.extractId(token);
messageService.enterApplication(memberId);
}

@PostMapping("/stomp/disconnect")
public String exitApp(
@AuthMember Long memberId
){
String msg = messageService.exitApplication(memberId);
return msg;
}

@GetMapping("/chat/{roomId}/messages")
public ResponseEntity<List<MessageListResponse>> getMessagesPage(
@PathVariable("roomId") Long roomId,
@AuthMember Long memberId,
@RequestParam(required = false, defaultValue = "1", value = "page") int pageNo
){
return ResponseEntity.ok(messageService.getMessagePage(roomId, memberId,pageNo));
}


@MessageMapping("/chat/messages")
public void message(
StompHeaderAccessor headerAccessor,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package capstone.facefriend.chat.controller.interceptor;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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.messaging.support.MessageHeaderAccessor;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
@Slf4j
public class FilterChannelInterceptor implements ChannelInterceptor {

private static final String BEARER_PREFIX = "Bearer ";

@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {

log.info("Stomp Handler 실행");
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
StompHeaderAccessor headerAccessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);

StompCommand command = headerAccessor.getCommand();
String destination = headerAccessor.getDestination();
String subscribeUrl = headerAccessor.getSubscriptionId();
String sessionId = headerAccessor.getSessionId();
Object payload = message.getPayload();
String messageHeaders = String.valueOf(accessor.getMessageHeaders());
String messageType = String.valueOf(accessor.getMessageType());

log.info("Command: {}", command);
log.info("Destination: {}", destination);
log.info("Subscription ID: {}", subscribeUrl);
log.info("Session ID: {}", sessionId);
log.info("Payload: {}", payload.toString());
log.info("Message Type: {}", messageType);
log.info("Message Headers: {}", messageHeaders.toString());


String authorizationHeader = headerAccessor.getFirstNativeHeader("Authorization");
if (headerAccessor.getCommand() == StompCommand.CONNECT) {
log.info("Command: {}", command);
log.info("Destination: {}", destination);
log.info("Subscription ID: {}", subscribeUrl);
log.info("Session ID: {}", sessionId);
log.info("Payload: {}", payload.toString());
log.info("Message Type: {}", messageType);
log.info("Message Headers: {}", messageHeaders.toString());
// String token = authorizationHeader.substring(BEARER_PREFIX.length());
// log.info("token: {}", token);
// if (token == null) try {
// throw new AccessDeniedException("");
// } catch (AccessDeniedException e) {
// throw new RuntimeException(e);
// }
}

if (headerAccessor.getCommand() == StompCommand.SUBSCRIBE) {
log.info("Command: {}", command);
log.info("Destination: {}", destination);
log.info("Subscription ID: {}", subscribeUrl);
log.info("Session ID: {}", sessionId);
log.info("Payload: {}", payload.toString());
log.info("Message Type: {}", messageType);
log.info("Message Headers: {}", messageHeaders.toString());
}

if (headerAccessor.getCommand() == StompCommand.DISCONNECT) {
log.info("Command: {}", command);
log.info("Destination: {}", destination);
log.info("Subscription ID: {}", subscribeUrl);
log.info("Session ID: {}", sessionId);
log.info("Payload: {}", payload.toString());
log.info("Message Type: {}", messageType);
log.info("Message Headers: {}", messageHeaders.toString());
}


if (headerAccessor.getCommand() == StompCommand.SEND) {
log.info("Command: {}", command);
log.info("Destination: {}", destination);
log.info("Subscription ID: {}", subscribeUrl);
log.info("Session ID: {}", sessionId);
log.info("Payload: {}", payload.toString());
log.info("Message Type: {}", messageType);
log.info("Message Headers: {}", messageHeaders.toString());
}


return message;
}
}
25 changes: 25 additions & 0 deletions src/main/java/capstone/facefriend/chat/domain/ChatRoomInfo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package capstone.facefriend.chat.domain;

import jakarta.persistence.Column;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;

import java.time.LocalDateTime;

@Getter
@Setter
@NoArgsConstructor
@RedisHash("ChatRoom")
@Slf4j
public class ChatRoomInfo {
@Id
private String chatRoomInfoId;

@Column(name = "enterTime", nullable = false)
private LocalDateTime enterTime;

}
19 changes: 19 additions & 0 deletions src/main/java/capstone/facefriend/chat/domain/SocketInfo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package capstone.facefriend.chat.domain;

import jakarta.persistence.Column;
import lombok.*;
import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;

import java.time.LocalDateTime;

@Data
@NoArgsConstructor
@RedisHash("SocketInfo")
public class SocketInfo {
@Id
private Long memberId;

@Column(name = "connectTime", nullable = false)
private LocalDateTime connectTime;
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,7 @@ public enum ChatExceptionType implements ExceptionType {
INVALID_ACCESS(Status.FORBIDDEN, 5003, "본인의 계정이 아닙니다."),
UNAUTHORIZED(Status.UNAUTHORIZED, 5005, "접근 정보가 잘못되었습니다."),
ALREADY_CHATROOM(Status.BAD_REQUEST, 5006, "이미 존재하는 채팅방입니다."),
WRONG_PASSWORD(Status.BAD_REQUEST, 5007, "잘못된 비밀번호입니다."),
EXPIRED_ACCESS_TOKEN(Status.BAD_REQUEST, 5008, "만료된 액세스 토큰이므로 재발급해야 합니다."),
INVALID_ACCESS_TOKEN(Status.BAD_REQUEST, 5009, "유효하지 않은 액세스 토큰이므로 재발급해야 합니다."),
INVALID_REFRESH_TOKEN(Status.BAD_REQUEST, 5010, "유효하지 않은 리프레시 토큰입니다. 토큰 재발급이 불가능합니다."),
ACCESS_TOKEN_IS_IN_BLACKLIST(Status.BAD_REQUEST, 5011, "액세스 토큰이 로그아웃 처리되었습니다. 재로그인하시기 바랍니다."),
NOT_VERIFIED(Status.BAD_REQUEST, 5012, "본인 인증을 먼저 완료해야 합니다."),
PASSWORDS_NOT_EQUAL(Status.BAD_REQUEST, 5013, "재설정하는 비밀번호들이 동일하지 않습니다."),
WRONG_TEMPORARY_PASSWORD(Status.BAD_REQUEST, 5014, "임시 비밀번호가 올바르지 않습니다."),
NOT_FOUND_GENDER(Status.NOT_FOUND, 5015, "일치하는 성별이 없습니다.")

;
private final Status status;
private final int exceptionCode;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
package capstone.facefriend.chat.repository;

import capstone.facefriend.chat.domain.ChatMessage;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;

import java.time.LocalDateTime;
import java.util.List;


public interface ChatMessageRepository extends JpaRepository<ChatMessage, Long> {
ChatMessage findFirstByChatRoomIdOrderBySendTimeDesc(Long roomId);
ChatMessage save(ChatMessage chatMessage);

List<ChatMessage> findChatMessagesByChatRoom_IdAndSendTimeBefore(Long roomId, LocalDateTime time, Pageable pageable);
List<ChatMessage> findChatMessagesByChatRoomId(Long roomId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package capstone.facefriend.chat.repository;

import capstone.facefriend.chat.domain.ChatRoomInfo;
import org.springframework.data.repository.CrudRepository;

import java.util.Optional;

public interface ChatRoomInfoRedisRepository extends CrudRepository<ChatRoomInfo, String> {
Optional<ChatRoomInfo> findById(String chatRoomInfoId);
}
Loading

0 comments on commit 19f7f11

Please sign in to comment.