Skip to content

Commit

Permalink
Merge pull request #18 from Leets-Official/feat/#17/실시간-채팅-기능-구현
Browse files Browse the repository at this point in the history
[feat] 실시간 채팅 기능 구현
  • Loading branch information
yechan-kim authored Nov 6, 2024
2 parents c093e0a + 26712b2 commit 0a4ce1c
Show file tree
Hide file tree
Showing 26 changed files with 781 additions and 76 deletions.
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ dependencies {

implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.13.0'

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

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.leets.xcellentbe.domain.chatRoom.controller;

import java.util.List;
import java.util.UUID;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
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.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.leets.xcellentbe.domain.chatRoom.dto.ChatRoomDto;
import com.leets.xcellentbe.domain.chatRoom.service.ChatRoomService;
import com.leets.xcellentbe.domain.dm.dto.request.DMRequest;
import com.leets.xcellentbe.domain.dm.dto.response.DMResponse;
import com.leets.xcellentbe.global.response.GlobalResponseDto;

import io.swagger.v3.oas.annotations.Operation;
import lombok.RequiredArgsConstructor;

@RestController
@RequestMapping("/api/chat-room")
@RequiredArgsConstructor
public class ChatRoomController {

private final ChatRoomService chatRoomService;

@PostMapping()
@Operation(summary = "채팅방 생성", description = "채팅방을 생성합니다.")
public ResponseEntity<GlobalResponseDto<DMResponse>> createChatRoom(@RequestBody DMRequest dmRequest,
@AuthenticationPrincipal UserDetails userDetails) {
return ResponseEntity.status(HttpStatus.CREATED)
.body(GlobalResponseDto.success(chatRoomService.createChatRoom(dmRequest, userDetails)));
}

@GetMapping("/all")
@Operation(summary = "사용자 채팅방 전체 조회", description = "사용자의 모든 채팅방을 조회합니다.")
public ResponseEntity<GlobalResponseDto<List<DMResponse>>> findAllChatRoomByUser(
@AuthenticationPrincipal UserDetails userDetails) {
return ResponseEntity.status(HttpStatus.OK)
.body(GlobalResponseDto.success(chatRoomService.findAllChatRoomByUser(userDetails)));
}

@GetMapping("/{chatRoomId}")
@Operation(summary = "사용자 채팅방 조회", description = "사용자의 채팅방을 조회합니다.")
public ResponseEntity<GlobalResponseDto<ChatRoomDto>> findChatRoom(@PathVariable UUID chatRoomId,
@AuthenticationPrincipal UserDetails userDetails) {
return ResponseEntity.status(HttpStatus.OK)
.body(GlobalResponseDto.success(chatRoomService.findChatRoom(chatRoomId, userDetails)));
}

@PatchMapping("/{chatRoomId}")
@Operation(summary = "채팅방 삭제", description = "채팅방을 삭제합니다.")
public ResponseEntity<GlobalResponseDto<String>> deleteChatRoom(@PathVariable UUID chatRoomId,
@AuthenticationPrincipal UserDetails userDetails) {
return ResponseEntity.status(HttpStatus.OK)
.body(GlobalResponseDto.success(chatRoomService.deleteChatRoom(chatRoomId, userDetails)));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package com.leets.xcellentbe.domain.chatRoom.domain;

import java.util.UUID;

import com.leets.xcellentbe.domain.shared.BaseTimeEntity;
import com.leets.xcellentbe.domain.shared.DeletedStatus;
import com.leets.xcellentbe.domain.user.domain.User;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.validation.constraints.NotNull;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class ChatRoom extends BaseTimeEntity {

@Id
@Column(name = "chatRoom_id")
@GeneratedValue(strategy = GenerationType.UUID)
private UUID chatRoomId;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "sender_id")
private User sender;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "receiver_id")
private User receiver;

@Column
private String lastMessage;

@NotNull
@Column
@Enumerated(EnumType.STRING)
private DeletedStatus deletedStatus;

public static ChatRoom create(User sender, User receiver) {
return ChatRoom.builder()
.sender(sender)
.receiver(receiver)
.build();
}

@Builder
private ChatRoom(User sender, User receiver) {
this.sender = sender;
this.receiver = receiver;
this.deletedStatus = DeletedStatus.NOT_DELETED;
}

public void updateReceiver(User receiver) {
this.receiver = receiver;
}

public void updateLastMessage(String lastMessage) {
this.lastMessage = lastMessage;
}

public void delete() {
this.deletedStatus = DeletedStatus.DELETED;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.leets.xcellentbe.domain.chatRoom.domain.repository;

import java.util.List;
import java.util.Optional;
import java.util.UUID;

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

import com.leets.xcellentbe.domain.chatRoom.domain.ChatRoom;
import com.leets.xcellentbe.domain.shared.DeletedStatus;
import com.leets.xcellentbe.domain.user.domain.User;

public interface ChatRoomRepository extends JpaRepository<ChatRoom, UUID> {

List<ChatRoom> findBySenderOrReceiverAndDeletedStatusNot(User sender, User receiver, DeletedStatus deletedStatus);

Optional<ChatRoom> findByChatRoomIdAndSenderOrChatRoomIdAndReceiverAndDeletedStatusNot(UUID ChatRoomId, User sender,
UUID ChatRoomId1, User receiver, DeletedStatus deletedStatus);

ChatRoom findBySenderAndReceiverAndDeletedStatusNot(User sender, User receiver, DeletedStatus deletedStatus);

Optional<ChatRoom> findByChatRoomIdAndDeletedStatusNot(UUID charRoomId, DeletedStatus deletedStatus);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.leets.xcellentbe.domain.chatRoom.dto;

import java.io.Serial;
import java.io.Serializable;
import java.util.UUID;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.leets.xcellentbe.domain.dm.dto.request.DMRequest;
import com.leets.xcellentbe.domain.user.domain.User;

@JsonInclude(JsonInclude.Include.NON_NULL)
public record ChatRoomDto(UUID chatRoomId, Long senderId, Long receiverId) implements Serializable {

@Serial
private static final long serialVersionUID = 6494678977089006639L;

public static ChatRoomDto of(DMRequest dmRequest, User user) {
return new ChatRoomDto(null, user.getUserId(), dmRequest.receiverId());
}

public static ChatRoomDto of(UUID chatRoomId, User sender, User receiver) {
return new ChatRoomDto(chatRoomId, sender.getUserId(), receiver.getUserId());
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package com.leets.xcellentbe.domain.chatroom.domain.exception;
package com.leets.xcellentbe.domain.chatRoom.exception;

import com.leets.xcellentbe.global.error.ErrorCode;
import com.leets.xcellentbe.global.error.exception.CommonException;

public class ChatRoomNotFoundException extends CommonException {
public ChatRoomNotFoundException() {

public ChatRoomNotFoundException() {
super(ErrorCode.CHAT_ROOM_NOT_FOUND);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package com.leets.xcellentbe.domain.chatRoom.service;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;

import com.leets.xcellentbe.domain.chatRoom.domain.ChatRoom;
import com.leets.xcellentbe.domain.chatRoom.domain.repository.ChatRoomRepository;
import com.leets.xcellentbe.domain.chatRoom.dto.ChatRoomDto;
import com.leets.xcellentbe.domain.chatRoom.exception.ChatRoomNotFoundException;
import com.leets.xcellentbe.domain.dm.domain.DM;
import com.leets.xcellentbe.domain.dm.domain.repository.DMRepository;
import com.leets.xcellentbe.domain.dm.dto.request.DMRequest;
import com.leets.xcellentbe.domain.dm.dto.response.DMResponse;
import com.leets.xcellentbe.domain.dm.redis.RedisSubscriber;
import com.leets.xcellentbe.domain.shared.DeletedStatus;
import com.leets.xcellentbe.domain.user.domain.User;
import com.leets.xcellentbe.domain.user.domain.repository.UserRepository;
import com.leets.xcellentbe.domain.user.exception.UserNotFoundException;

import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class ChatRoomService {

private final ChatRoomRepository chatRoomRepository;
private final DMRepository dmRepository;
private final UserRepository userRepository;
private final RedisMessageListenerContainer redisMessageListener;
private final RedisSubscriber redisSubscriber;

private static final String Message_Rooms = "MESSAGE_ROOM";
private final RedisTemplate<String, Object> redisTemplate;
private HashOperations<String, String, ChatRoomDto> opsHashMessageRoom;

private Map<String, ChannelTopic> topics;

@PostConstruct
private void init() {
opsHashMessageRoom = redisTemplate.opsForHash();
topics = new HashMap<>();
}

public DMResponse createChatRoom(DMRequest dmRequest, UserDetails userDetails) {
User sender = userRepository.findByEmail(userDetails.getUsername()).orElseThrow(UserNotFoundException::new);

User receiver = userRepository.findById(dmRequest.receiverId()).orElseThrow(UserNotFoundException::new);

ChatRoom chatRoom = chatRoomRepository.findBySenderAndReceiverAndDeletedStatusNot(sender, receiver,
DeletedStatus.DELETED);

if ((chatRoom == null) || (!sender.equals(chatRoom.getSender()) && !receiver.equals(
chatRoom.getReceiver()))) {
ChatRoomDto chatRoomDto = ChatRoomDto.of(dmRequest, sender);
opsHashMessageRoom.put(Message_Rooms, sender.getUserName(), chatRoomDto);

chatRoom = chatRoomRepository.save(ChatRoom.create(sender, receiver));

return DMResponse.from(chatRoom);
} else {
return DMResponse.from(chatRoom.getChatRoomId());
}
}

public List<DMResponse> findAllChatRoomByUser(UserDetails userDetails) {
User user = userRepository.findByEmail(userDetails.getUsername()).orElseThrow(UserNotFoundException::new);

List<ChatRoom> chatRooms = chatRoomRepository.findBySenderOrReceiverAndDeletedStatusNot(user, user,
DeletedStatus.DELETED);

List<DMResponse> dmResponses = new ArrayList<>();

for (ChatRoom chatRoom : chatRooms) {
DMResponse messageRoomDto;

messageRoomDto = DMResponse.of(chatRoom.getChatRoomId(), chatRoom.getSender(), chatRoom.getReceiver());

DM latestMessage = dmRepository.findTopByChatRoomAndDeletedStatusNotOrderByCreatedAtDesc(chatRoom,
DeletedStatus.DELETED);

if (latestMessage != null) {
messageRoomDto.updateLatestMessageCreatedAt(latestMessage.getCreatedAt());
messageRoomDto.updateLatestMessageContent(latestMessage.getMessage());
}

dmResponses.add(messageRoomDto);
}

return dmResponses;
}

public ChatRoomDto findChatRoom(UUID chatRoomId, UserDetails userDetails) {
User user = userRepository.findByEmail(userDetails.getUsername()).orElseThrow(UserNotFoundException::new);

ChatRoom chatRoom = chatRoomRepository.findByChatRoomIdAndDeletedStatusNot(chatRoomId, DeletedStatus.DELETED)
.orElseThrow(ChatRoomNotFoundException::new);

User receiver = chatRoom.getReceiver();

chatRoom = chatRoomRepository.findByChatRoomIdAndSenderOrChatRoomIdAndReceiverAndDeletedStatusNot(chatRoomId,
user, chatRoomId, receiver, DeletedStatus.DELETED).orElseThrow(ChatRoomNotFoundException::new);

return ChatRoomDto.of(chatRoom.getChatRoomId(), chatRoom.getSender(), chatRoom.getReceiver());
}

public String deleteChatRoom(UUID chatRoomId, UserDetails userDetails) {
User user = userRepository.findByEmail(userDetails.getUsername()).orElseThrow(UserNotFoundException::new);

ChatRoom chatRoom = chatRoomRepository.findByChatRoomIdAndSenderOrChatRoomIdAndReceiverAndDeletedStatusNot(
chatRoomId, user, chatRoomId, user, DeletedStatus.DELETED).orElseThrow(ChatRoomNotFoundException::new);

chatRoom.delete();
chatRoomRepository.save(chatRoom);

if (user.equals(chatRoom.getSender())) {
opsHashMessageRoom.delete(Message_Rooms, chatRoom.getChatRoomId());
}

return "대화방을 삭제했습니다.";
}

public void enterChatRoom(Long receiverId) {
String receiverName = userRepository.findById(receiverId).orElseThrow(UserNotFoundException::new).getUserName();
ChannelTopic topic = topics.get(receiverName);

if (topic == null) {
topic = new ChannelTopic(receiverName);
redisMessageListener.addMessageListener(redisSubscriber, topic);
topics.put(receiverName, topic);
}
}

public ChannelTopic getTopic(Long receiverId) {
String receiverName = userRepository.findById(receiverId).orElseThrow(UserNotFoundException::new).getUserName();
return topics.get(receiverName);
}
}

This file was deleted.

Loading

0 comments on commit 0a4ce1c

Please sign in to comment.