Skip to content

Commit

Permalink
Merge pull request #85 from coffee-meet/feat/#79
Browse files Browse the repository at this point in the history
채팅방 생성 및 채팅 기능 구현
  • Loading branch information
yumyeonghan authored Nov 6, 2023
2 parents 95cdade + 4767225 commit 8bc6a17
Show file tree
Hide file tree
Showing 24 changed files with 534 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public enum AuthErrorCode implements ErrorCode {
INVALID_LOGIN_TYPE("A000", "지원하지 않는 로그인 타입입니다."),
AUTHENTICATION_FAILED("A001", "인증이 실패했습니다."),
AUTHORIZATION_FAILED("A003", "인가에 실패했습니다."),
HEADER_NOT_FOUND("A004", "헤더에 인증 코드가 없습니다."),
ALREADY_REGISTERED("A009", "이미 가입된 사용자입니다."),
DELETED_USER("A010", "탈퇴한 지 30일이 지난 사용자입니다. 다시 회원가입해 주세요.");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import coffeemeet.server.common.domain.BaseEntity;
import coffeemeet.server.common.execption.InvalidInputException;
import coffeemeet.server.user.domain.User;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
Expand Down Expand Up @@ -38,10 +39,16 @@ public class ChattingMessage extends BaseEntity {
@JoinColumn(name = "chatting_room_id", nullable = false)
private ChattingRoom chattingRoom;

public ChattingMessage(@NonNull String message, @NonNull ChattingRoom chattingRoom) {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;

public ChattingMessage(@NonNull String message, @NonNull ChattingRoom chattingRoom,
@NonNull User user) {
validateMessage(message);
this.message = message;
this.chattingRoom = chattingRoom;
this.user = user;
}

private void validateMessage(String message) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,13 @@
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Getter
@Table(name = "chatting_rooms")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@NoArgsConstructor
public class ChattingRoom extends BaseEntity {

@Id
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package coffeemeet.server.chatting.current.implement;

import coffeemeet.server.chatting.current.domain.ChattingMessage;
import coffeemeet.server.chatting.current.domain.ChattingRoom;
import coffeemeet.server.chatting.current.infrastructure.ChattingMessageRepository;
import coffeemeet.server.user.domain.User;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

@Component
@Transactional
@RequiredArgsConstructor
public class ChattingMessageCommand {

private final ChattingMessageRepository chattingMessageRepository;

public ChattingMessage saveChattingMessage(String content, ChattingRoom chattingRoom, User user) {
return chattingMessageRepository.save(new ChattingMessage(content, chattingRoom, user));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package coffeemeet.server.chatting.current.implement;

import coffeemeet.server.chatting.current.domain.ChattingRoom;
import coffeemeet.server.chatting.current.infrastructure.ChattingRoomRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

@Component
@Transactional
@RequiredArgsConstructor
public class ChattingRoomCommand {

private final ChattingRoomRepository chattingRoomRepository;

public ChattingRoom saveChattingRoom() {
return chattingRoomRepository.save(new ChattingRoom());
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package coffeemeet.server.chatting.current.implement;

import static coffeemeet.server.chatting.exception.ChattingErrorCode.CHATTING_ROOM_NOT_FOUND;

import coffeemeet.server.chatting.current.domain.ChattingRoom;
import coffeemeet.server.chatting.current.infrastructure.ChattingRoomRepository;
import coffeemeet.server.common.execption.InvalidInputException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

@Component
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class ChattingRoomQuery {

private static final String CHATTING_ROOM_NOT_FOUND_MESSAGE = "(%s)번 채팅방을 찾을 수 없습니다.";

private final ChattingRoomRepository chattingRoomRepository;

public ChattingRoom getChattingRoomById(Long roomId) {
return chattingRoomRepository.findById(roomId)
.orElseThrow(() -> new InvalidInputException(
CHATTING_ROOM_NOT_FOUND,
String.format(CHATTING_ROOM_NOT_FOUND_MESSAGE, roomId)
));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package coffeemeet.server.chatting.current.infrastructure;

import coffeemeet.server.chatting.current.domain.ChattingMessage;
import org.springframework.data.jpa.repository.JpaRepository;

public interface ChattingMessageRepository extends JpaRepository<ChattingMessage, Long> {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package coffeemeet.server.chatting.current.infrastructure;

import coffeemeet.server.chatting.current.domain.ChattingRoom;
import org.springframework.data.jpa.repository.JpaRepository;

public interface ChattingRoomRepository extends JpaRepository<ChattingRoom, Long> {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package coffeemeet.server.chatting.current.presentation;

import coffeemeet.server.chatting.current.presentation.dto.ChatStomp;
import coffeemeet.server.chatting.current.service.ChattingMessageService;
import jakarta.validation.Valid;
import java.util.HashMap;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.springframework.context.event.EventListener;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.messaging.simp.SimpMessageSendingOperations;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.socket.messaging.SessionConnectEvent;
import org.springframework.web.socket.messaging.SessionDisconnectEvent;

@RestController
@RequiredArgsConstructor
public class ChattingMessageController {

private final SimpMessageSendingOperations simpMessageSendingOperations;
private final ChattingMessageService chattingMessageService;
private final Map<String, Long> sessions = new HashMap<>();

@EventListener(SessionConnectEvent.class)
public void onConnect(SessionConnectEvent event) {
String sessionId = String.valueOf(event.getMessage().getHeaders().get("simpSessionId"));
String userId = String.valueOf(event.getMessage().getHeaders().get("nativeHeaders"))
.split("userId=\\[")[1].split("]")[0];
sessions.put(sessionId, Long.valueOf(userId));
}

@EventListener(SessionDisconnectEvent.class)
public void onDisconnect(SessionDisconnectEvent event) {
sessions.remove(event.getSessionId());
}

@MessageMapping("/chatting/messages")
public void message(@Valid ChatStomp.Request request, SimpMessageHeaderAccessor accessor) {
Long userId = sessions.get(accessor.getSessionId());
chattingMessageService.createChattingMessage(request.roomId(), request.content(), userId);
simpMessageSendingOperations.convertAndSend("/sub/chatting/room/" + request.roomId(),
request.content());
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package coffeemeet.server.chatting.current.presentation.dto;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;

public sealed interface ChatStomp permits ChatStomp.Request {

record Request(
@NotNull
Long roomId,
@NotBlank
String content
) implements ChatStomp {

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package coffeemeet.server.chatting.current.service;

import coffeemeet.server.chatting.current.domain.ChattingRoom;
import coffeemeet.server.chatting.current.implement.ChattingMessageCommand;
import coffeemeet.server.chatting.current.implement.ChattingRoomQuery;
import coffeemeet.server.user.domain.User;
import coffeemeet.server.user.implement.UserQuery;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class ChattingMessageService {

private final ChattingMessageCommand chattingMessageCommand;
private final ChattingRoomQuery chattingRoomQuery;
private final UserQuery userQuery;

public void createChattingMessage(Long roomId, String content, Long userId) {
User user = userQuery.getUserById(userId);
ChattingRoom chattingRoom = chattingRoomQuery.getChattingRoomById(roomId);
chattingMessageCommand.saveChattingMessage(content, chattingRoom, user);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package coffeemeet.server.chatting.current.service;

import coffeemeet.server.chatting.current.implement.ChattingRoomCommand;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class ChattingRoomService {

private final ChattingRoomCommand chattingRoomCommand;

public void createChattingRoom() {
chattingRoomCommand.saveChattingRoom();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
@RequiredArgsConstructor
public enum ChattingErrorCode implements ErrorCode {
INVALID_MESSAGE("CM000", "유효하지 않은 메세지 형식입니다."),
;
CHATTING_ROOM_NOT_FOUND("CR004", "채팅방을 찾을 수 없습니다.");

private final String errorCode;
private final String message;
Expand Down
38 changes: 38 additions & 0 deletions src/main/java/coffeemeet/server/common/config/ChattingConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package coffeemeet.server.common.config;

import coffeemeet.server.auth.domain.JwtTokenProvider;
import coffeemeet.server.auth.implement.RefreshTokenQuery;
import coffeemeet.server.common.presentation.interceptor.StompInterceptor;
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 ChattingConfig implements WebSocketMessageBrokerConfigurer {

private final JwtTokenProvider jwtTokenProvider;
private final RefreshTokenQuery refreshTokenQuery;

@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/stomp").setAllowedOriginPatterns("*");
}

@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/sub");
registry.setApplicationDestinationPrefixes("/pub");
}

@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(new StompInterceptor(jwtTokenProvider, refreshTokenQuery));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public enum GlobalErrorCode implements ErrorCode {

VALIDATION_ERROR("G000", "유효하지 않은 입력입니다."),
INVALID_S3_URL("G004", "유효하지 않은 s3 url 입니다."),
STOMP_ACCESSOR_NOT_FOUND("G004", "웹소켓 연결을 할 수 없습니다."),
INTERNAL_SERVER_ERROR("G050", "예상치 못한 오류입니다.");

private final String code;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import coffeemeet.server.common.execption.DataLengthExceededException;
import coffeemeet.server.common.execption.GlobalErrorCode;
import coffeemeet.server.common.execption.InvalidAuthException;
import coffeemeet.server.common.execption.InvalidInputException;
import coffeemeet.server.common.execption.MissMatchException;
import coffeemeet.server.common.execption.NotFoundException;
import lombok.RequiredArgsConstructor;
Expand Down Expand Up @@ -42,6 +43,13 @@ public ResponseEntity<ErrorResponse> handleException(InvalidAuthException except
.body(ErrorResponse.of(exception.getErrorCode()));
}

@ExceptionHandler(InvalidInputException.class)
public ResponseEntity<ErrorResponse> handleException(InvalidInputException exception) {
log.info(exception.getMessage(), exception);
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(ErrorResponse.of(exception.getErrorCode()));
}

@ExceptionHandler(MissMatchException.class)
public ResponseEntity<ErrorResponse> handleException(MissMatchException exception) {
log.info(exception.getMessage(), exception);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package coffeemeet.server.common.presentation.interceptor;

import static coffeemeet.server.auth.exception.AuthErrorCode.AUTHENTICATION_FAILED;
import static coffeemeet.server.common.execption.GlobalErrorCode.STOMP_ACCESSOR_NOT_FOUND;

import coffeemeet.server.auth.domain.JwtTokenProvider;
import coffeemeet.server.auth.implement.RefreshTokenQuery;
import coffeemeet.server.common.execption.InvalidAuthException;
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.messaging.support.MessageHeaderAccessor;

@RequiredArgsConstructor
public class StompInterceptor implements ChannelInterceptor {

private static final String STOMP_ACCESSOR_NOT_FOUND_MESSAGE = "stomp header accessor를 찾을 수 없습니다.";
private static final String HEADER_AUTHENTICATION_FAILED_MESSAGE = "(%s)는 잘못된 권한 헤더입니다.";

private final JwtTokenProvider jwtTokenProvider;
private final RefreshTokenQuery refreshTokenQuery;

@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor headerAccessor = MessageHeaderAccessor.getAccessor(message,
StompHeaderAccessor.class);
if (headerAccessor == null) {
throw new InvalidAuthException(
STOMP_ACCESSOR_NOT_FOUND,
STOMP_ACCESSOR_NOT_FOUND_MESSAGE
);
}

if (headerAccessor.getCommand() == StompCommand.CONNECT) {
String authHeader = String.valueOf(headerAccessor.getNativeHeader("Authorization"));
if (authHeader != null && authHeader.startsWith("Bearer ", 1)) {
String token = authHeader.substring(7, authHeader.length() - 1);
Long userId = jwtTokenProvider.extractUserId(token);
refreshTokenQuery.getRefreshToken(userId);
headerAccessor.addNativeHeader("userId", String.valueOf(userId));
} else {
throw new InvalidAuthException(
AUTHENTICATION_FAILED,
String.format(HEADER_AUTHENTICATION_FAILED_MESSAGE, authHeader)
);
}
}
return message;
}

}
Loading

0 comments on commit 8bc6a17

Please sign in to comment.