Skip to content

Commit 0a4ce1c

Browse files
authored
Merge pull request #18 from Leets-Official/feat/#17/실시간-채팅-기능-구현
[feat] 실시간 채팅 기능 구현
2 parents c093e0a + 26712b2 commit 0a4ce1c

File tree

26 files changed

+781
-76
lines changed

26 files changed

+781
-76
lines changed

build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ dependencies {
7474

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

77+
// Websocket
78+
implementation 'org.springframework.boot:spring-boot-starter-websocket'
7779
}
7880

7981
tasks.named('test') {
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package com.leets.xcellentbe.domain.chatRoom.controller;
2+
3+
import java.util.List;
4+
import java.util.UUID;
5+
6+
import org.springframework.http.HttpStatus;
7+
import org.springframework.http.ResponseEntity;
8+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
9+
import org.springframework.security.core.userdetails.UserDetails;
10+
import org.springframework.web.bind.annotation.GetMapping;
11+
import org.springframework.web.bind.annotation.PatchMapping;
12+
import org.springframework.web.bind.annotation.PathVariable;
13+
import org.springframework.web.bind.annotation.PostMapping;
14+
import org.springframework.web.bind.annotation.RequestBody;
15+
import org.springframework.web.bind.annotation.RequestMapping;
16+
import org.springframework.web.bind.annotation.RestController;
17+
18+
import com.leets.xcellentbe.domain.chatRoom.dto.ChatRoomDto;
19+
import com.leets.xcellentbe.domain.chatRoom.service.ChatRoomService;
20+
import com.leets.xcellentbe.domain.dm.dto.request.DMRequest;
21+
import com.leets.xcellentbe.domain.dm.dto.response.DMResponse;
22+
import com.leets.xcellentbe.global.response.GlobalResponseDto;
23+
24+
import io.swagger.v3.oas.annotations.Operation;
25+
import lombok.RequiredArgsConstructor;
26+
27+
@RestController
28+
@RequestMapping("/api/chat-room")
29+
@RequiredArgsConstructor
30+
public class ChatRoomController {
31+
32+
private final ChatRoomService chatRoomService;
33+
34+
@PostMapping()
35+
@Operation(summary = "채팅방 생성", description = "채팅방을 생성합니다.")
36+
public ResponseEntity<GlobalResponseDto<DMResponse>> createChatRoom(@RequestBody DMRequest dmRequest,
37+
@AuthenticationPrincipal UserDetails userDetails) {
38+
return ResponseEntity.status(HttpStatus.CREATED)
39+
.body(GlobalResponseDto.success(chatRoomService.createChatRoom(dmRequest, userDetails)));
40+
}
41+
42+
@GetMapping("/all")
43+
@Operation(summary = "사용자 채팅방 전체 조회", description = "사용자의 모든 채팅방을 조회합니다.")
44+
public ResponseEntity<GlobalResponseDto<List<DMResponse>>> findAllChatRoomByUser(
45+
@AuthenticationPrincipal UserDetails userDetails) {
46+
return ResponseEntity.status(HttpStatus.OK)
47+
.body(GlobalResponseDto.success(chatRoomService.findAllChatRoomByUser(userDetails)));
48+
}
49+
50+
@GetMapping("/{chatRoomId}")
51+
@Operation(summary = "사용자 채팅방 조회", description = "사용자의 채팅방을 조회합니다.")
52+
public ResponseEntity<GlobalResponseDto<ChatRoomDto>> findChatRoom(@PathVariable UUID chatRoomId,
53+
@AuthenticationPrincipal UserDetails userDetails) {
54+
return ResponseEntity.status(HttpStatus.OK)
55+
.body(GlobalResponseDto.success(chatRoomService.findChatRoom(chatRoomId, userDetails)));
56+
}
57+
58+
@PatchMapping("/{chatRoomId}")
59+
@Operation(summary = "채팅방 삭제", description = "채팅방을 삭제합니다.")
60+
public ResponseEntity<GlobalResponseDto<String>> deleteChatRoom(@PathVariable UUID chatRoomId,
61+
@AuthenticationPrincipal UserDetails userDetails) {
62+
return ResponseEntity.status(HttpStatus.OK)
63+
.body(GlobalResponseDto.success(chatRoomService.deleteChatRoom(chatRoomId, userDetails)));
64+
}
65+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package com.leets.xcellentbe.domain.chatRoom.domain;
2+
3+
import java.util.UUID;
4+
5+
import com.leets.xcellentbe.domain.shared.BaseTimeEntity;
6+
import com.leets.xcellentbe.domain.shared.DeletedStatus;
7+
import com.leets.xcellentbe.domain.user.domain.User;
8+
9+
import jakarta.persistence.Column;
10+
import jakarta.persistence.Entity;
11+
import jakarta.persistence.EnumType;
12+
import jakarta.persistence.Enumerated;
13+
import jakarta.persistence.FetchType;
14+
import jakarta.persistence.GeneratedValue;
15+
import jakarta.persistence.GenerationType;
16+
import jakarta.persistence.Id;
17+
import jakarta.persistence.JoinColumn;
18+
import jakarta.persistence.ManyToOne;
19+
import jakarta.validation.constraints.NotNull;
20+
import lombok.AccessLevel;
21+
import lombok.Builder;
22+
import lombok.Getter;
23+
import lombok.NoArgsConstructor;
24+
25+
@Entity
26+
@Getter
27+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
28+
public class ChatRoom extends BaseTimeEntity {
29+
30+
@Id
31+
@Column(name = "chatRoom_id")
32+
@GeneratedValue(strategy = GenerationType.UUID)
33+
private UUID chatRoomId;
34+
35+
@ManyToOne(fetch = FetchType.LAZY)
36+
@JoinColumn(name = "sender_id")
37+
private User sender;
38+
39+
@ManyToOne(fetch = FetchType.LAZY)
40+
@JoinColumn(name = "receiver_id")
41+
private User receiver;
42+
43+
@Column
44+
private String lastMessage;
45+
46+
@NotNull
47+
@Column
48+
@Enumerated(EnumType.STRING)
49+
private DeletedStatus deletedStatus;
50+
51+
public static ChatRoom create(User sender, User receiver) {
52+
return ChatRoom.builder()
53+
.sender(sender)
54+
.receiver(receiver)
55+
.build();
56+
}
57+
58+
@Builder
59+
private ChatRoom(User sender, User receiver) {
60+
this.sender = sender;
61+
this.receiver = receiver;
62+
this.deletedStatus = DeletedStatus.NOT_DELETED;
63+
}
64+
65+
public void updateReceiver(User receiver) {
66+
this.receiver = receiver;
67+
}
68+
69+
public void updateLastMessage(String lastMessage) {
70+
this.lastMessage = lastMessage;
71+
}
72+
73+
public void delete() {
74+
this.deletedStatus = DeletedStatus.DELETED;
75+
}
76+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package com.leets.xcellentbe.domain.chatRoom.domain.repository;
2+
3+
import java.util.List;
4+
import java.util.Optional;
5+
import java.util.UUID;
6+
7+
import org.springframework.data.jpa.repository.JpaRepository;
8+
9+
import com.leets.xcellentbe.domain.chatRoom.domain.ChatRoom;
10+
import com.leets.xcellentbe.domain.shared.DeletedStatus;
11+
import com.leets.xcellentbe.domain.user.domain.User;
12+
13+
public interface ChatRoomRepository extends JpaRepository<ChatRoom, UUID> {
14+
15+
List<ChatRoom> findBySenderOrReceiverAndDeletedStatusNot(User sender, User receiver, DeletedStatus deletedStatus);
16+
17+
Optional<ChatRoom> findByChatRoomIdAndSenderOrChatRoomIdAndReceiverAndDeletedStatusNot(UUID ChatRoomId, User sender,
18+
UUID ChatRoomId1, User receiver, DeletedStatus deletedStatus);
19+
20+
ChatRoom findBySenderAndReceiverAndDeletedStatusNot(User sender, User receiver, DeletedStatus deletedStatus);
21+
22+
Optional<ChatRoom> findByChatRoomIdAndDeletedStatusNot(UUID charRoomId, DeletedStatus deletedStatus);
23+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.leets.xcellentbe.domain.chatRoom.dto;
2+
3+
import java.io.Serial;
4+
import java.io.Serializable;
5+
import java.util.UUID;
6+
7+
import com.fasterxml.jackson.annotation.JsonInclude;
8+
import com.leets.xcellentbe.domain.dm.dto.request.DMRequest;
9+
import com.leets.xcellentbe.domain.user.domain.User;
10+
11+
@JsonInclude(JsonInclude.Include.NON_NULL)
12+
public record ChatRoomDto(UUID chatRoomId, Long senderId, Long receiverId) implements Serializable {
13+
14+
@Serial
15+
private static final long serialVersionUID = 6494678977089006639L;
16+
17+
public static ChatRoomDto of(DMRequest dmRequest, User user) {
18+
return new ChatRoomDto(null, user.getUserId(), dmRequest.receiverId());
19+
}
20+
21+
public static ChatRoomDto of(UUID chatRoomId, User sender, User receiver) {
22+
return new ChatRoomDto(chatRoomId, sender.getUserId(), receiver.getUserId());
23+
}
24+
}
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
package com.leets.xcellentbe.domain.chatroom.domain.exception;
1+
package com.leets.xcellentbe.domain.chatRoom.exception;
22

33
import com.leets.xcellentbe.global.error.ErrorCode;
44
import com.leets.xcellentbe.global.error.exception.CommonException;
55

66
public class ChatRoomNotFoundException extends CommonException {
7-
public ChatRoomNotFoundException() {
87

8+
public ChatRoomNotFoundException() {
99
super(ErrorCode.CHAT_ROOM_NOT_FOUND);
1010
}
1111
}
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
package com.leets.xcellentbe.domain.chatRoom.service;
2+
3+
import java.util.ArrayList;
4+
import java.util.HashMap;
5+
import java.util.List;
6+
import java.util.Map;
7+
import java.util.UUID;
8+
9+
import org.springframework.data.redis.core.HashOperations;
10+
import org.springframework.data.redis.core.RedisTemplate;
11+
import org.springframework.data.redis.listener.ChannelTopic;
12+
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
13+
import org.springframework.security.core.userdetails.UserDetails;
14+
import org.springframework.stereotype.Service;
15+
16+
import com.leets.xcellentbe.domain.chatRoom.domain.ChatRoom;
17+
import com.leets.xcellentbe.domain.chatRoom.domain.repository.ChatRoomRepository;
18+
import com.leets.xcellentbe.domain.chatRoom.dto.ChatRoomDto;
19+
import com.leets.xcellentbe.domain.chatRoom.exception.ChatRoomNotFoundException;
20+
import com.leets.xcellentbe.domain.dm.domain.DM;
21+
import com.leets.xcellentbe.domain.dm.domain.repository.DMRepository;
22+
import com.leets.xcellentbe.domain.dm.dto.request.DMRequest;
23+
import com.leets.xcellentbe.domain.dm.dto.response.DMResponse;
24+
import com.leets.xcellentbe.domain.dm.redis.RedisSubscriber;
25+
import com.leets.xcellentbe.domain.shared.DeletedStatus;
26+
import com.leets.xcellentbe.domain.user.domain.User;
27+
import com.leets.xcellentbe.domain.user.domain.repository.UserRepository;
28+
import com.leets.xcellentbe.domain.user.exception.UserNotFoundException;
29+
30+
import jakarta.annotation.PostConstruct;
31+
import lombok.RequiredArgsConstructor;
32+
33+
@Service
34+
@RequiredArgsConstructor
35+
public class ChatRoomService {
36+
37+
private final ChatRoomRepository chatRoomRepository;
38+
private final DMRepository dmRepository;
39+
private final UserRepository userRepository;
40+
private final RedisMessageListenerContainer redisMessageListener;
41+
private final RedisSubscriber redisSubscriber;
42+
43+
private static final String Message_Rooms = "MESSAGE_ROOM";
44+
private final RedisTemplate<String, Object> redisTemplate;
45+
private HashOperations<String, String, ChatRoomDto> opsHashMessageRoom;
46+
47+
private Map<String, ChannelTopic> topics;
48+
49+
@PostConstruct
50+
private void init() {
51+
opsHashMessageRoom = redisTemplate.opsForHash();
52+
topics = new HashMap<>();
53+
}
54+
55+
public DMResponse createChatRoom(DMRequest dmRequest, UserDetails userDetails) {
56+
User sender = userRepository.findByEmail(userDetails.getUsername()).orElseThrow(UserNotFoundException::new);
57+
58+
User receiver = userRepository.findById(dmRequest.receiverId()).orElseThrow(UserNotFoundException::new);
59+
60+
ChatRoom chatRoom = chatRoomRepository.findBySenderAndReceiverAndDeletedStatusNot(sender, receiver,
61+
DeletedStatus.DELETED);
62+
63+
if ((chatRoom == null) || (!sender.equals(chatRoom.getSender()) && !receiver.equals(
64+
chatRoom.getReceiver()))) {
65+
ChatRoomDto chatRoomDto = ChatRoomDto.of(dmRequest, sender);
66+
opsHashMessageRoom.put(Message_Rooms, sender.getUserName(), chatRoomDto);
67+
68+
chatRoom = chatRoomRepository.save(ChatRoom.create(sender, receiver));
69+
70+
return DMResponse.from(chatRoom);
71+
} else {
72+
return DMResponse.from(chatRoom.getChatRoomId());
73+
}
74+
}
75+
76+
public List<DMResponse> findAllChatRoomByUser(UserDetails userDetails) {
77+
User user = userRepository.findByEmail(userDetails.getUsername()).orElseThrow(UserNotFoundException::new);
78+
79+
List<ChatRoom> chatRooms = chatRoomRepository.findBySenderOrReceiverAndDeletedStatusNot(user, user,
80+
DeletedStatus.DELETED);
81+
82+
List<DMResponse> dmResponses = new ArrayList<>();
83+
84+
for (ChatRoom chatRoom : chatRooms) {
85+
DMResponse messageRoomDto;
86+
87+
messageRoomDto = DMResponse.of(chatRoom.getChatRoomId(), chatRoom.getSender(), chatRoom.getReceiver());
88+
89+
DM latestMessage = dmRepository.findTopByChatRoomAndDeletedStatusNotOrderByCreatedAtDesc(chatRoom,
90+
DeletedStatus.DELETED);
91+
92+
if (latestMessage != null) {
93+
messageRoomDto.updateLatestMessageCreatedAt(latestMessage.getCreatedAt());
94+
messageRoomDto.updateLatestMessageContent(latestMessage.getMessage());
95+
}
96+
97+
dmResponses.add(messageRoomDto);
98+
}
99+
100+
return dmResponses;
101+
}
102+
103+
public ChatRoomDto findChatRoom(UUID chatRoomId, UserDetails userDetails) {
104+
User user = userRepository.findByEmail(userDetails.getUsername()).orElseThrow(UserNotFoundException::new);
105+
106+
ChatRoom chatRoom = chatRoomRepository.findByChatRoomIdAndDeletedStatusNot(chatRoomId, DeletedStatus.DELETED)
107+
.orElseThrow(ChatRoomNotFoundException::new);
108+
109+
User receiver = chatRoom.getReceiver();
110+
111+
chatRoom = chatRoomRepository.findByChatRoomIdAndSenderOrChatRoomIdAndReceiverAndDeletedStatusNot(chatRoomId,
112+
user, chatRoomId, receiver, DeletedStatus.DELETED).orElseThrow(ChatRoomNotFoundException::new);
113+
114+
return ChatRoomDto.of(chatRoom.getChatRoomId(), chatRoom.getSender(), chatRoom.getReceiver());
115+
}
116+
117+
public String deleteChatRoom(UUID chatRoomId, UserDetails userDetails) {
118+
User user = userRepository.findByEmail(userDetails.getUsername()).orElseThrow(UserNotFoundException::new);
119+
120+
ChatRoom chatRoom = chatRoomRepository.findByChatRoomIdAndSenderOrChatRoomIdAndReceiverAndDeletedStatusNot(
121+
chatRoomId, user, chatRoomId, user, DeletedStatus.DELETED).orElseThrow(ChatRoomNotFoundException::new);
122+
123+
chatRoom.delete();
124+
chatRoomRepository.save(chatRoom);
125+
126+
if (user.equals(chatRoom.getSender())) {
127+
opsHashMessageRoom.delete(Message_Rooms, chatRoom.getChatRoomId());
128+
}
129+
130+
return "대화방을 삭제했습니다.";
131+
}
132+
133+
public void enterChatRoom(Long receiverId) {
134+
String receiverName = userRepository.findById(receiverId).orElseThrow(UserNotFoundException::new).getUserName();
135+
ChannelTopic topic = topics.get(receiverName);
136+
137+
if (topic == null) {
138+
topic = new ChannelTopic(receiverName);
139+
redisMessageListener.addMessageListener(redisSubscriber, topic);
140+
topics.put(receiverName, topic);
141+
}
142+
}
143+
144+
public ChannelTopic getTopic(Long receiverId) {
145+
String receiverName = userRepository.findById(receiverId).orElseThrow(UserNotFoundException::new).getUserName();
146+
return topics.get(receiverName);
147+
}
148+
}

src/main/java/com/leets/xcellentbe/domain/chatroom/domain/Chatroom.java

Lines changed: 0 additions & 30 deletions
This file was deleted.

0 commit comments

Comments
 (0)