Skip to content

Commit a06e2f5

Browse files
committed
feat: 충돌 해결(#90)
2 parents a14bfe7 + baaa3a3 commit a06e2f5

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+1192
-195
lines changed

be/build.gradle

+8
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,14 @@ dependencies {
5252

5353
// fcm
5454
implementation 'com.google.firebase:firebase-admin:9.2.0'
55+
56+
// websocket
57+
implementation 'org.springframework.boot:spring-boot-starter-websocket'
58+
implementation 'org.webjars:stomp-websocket:2.3.3-1'
59+
implementation 'org.webjars:sockjs-client:1.1.2'
60+
61+
// redis
62+
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
5563
}
5664

5765
tasks.named('bootBuildImage') {

be/src/main/java/yeonba/be/chatting/controller/ChattingController.java renamed to be/src/main/java/yeonba/be/chatting/controller/ChatController.java

+43-5
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,63 @@
55
import io.swagger.v3.oas.annotations.responses.ApiResponse;
66
import io.swagger.v3.oas.annotations.tags.Tag;
77
import java.util.List;
8+
import java.util.Set;
89
import lombok.RequiredArgsConstructor;
10+
import lombok.extern.slf4j.Slf4j;
11+
import org.springframework.data.redis.core.RedisTemplate;
12+
import org.springframework.data.redis.listener.ChannelTopic;
13+
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
14+
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;
915
import org.springframework.http.ResponseEntity;
16+
import org.springframework.messaging.handler.annotation.MessageMapping;
17+
import org.springframework.stereotype.Controller;
1018
import org.springframework.web.bind.annotation.GetMapping;
1119
import org.springframework.web.bind.annotation.PathVariable;
1220
import org.springframework.web.bind.annotation.PostMapping;
1321
import org.springframework.web.bind.annotation.RequestAttribute;
14-
import org.springframework.web.bind.annotation.RestController;
22+
import org.springframework.web.bind.annotation.ResponseBody;
23+
import yeonba.be.chatting.dto.request.ChatPublishRequest;
24+
import yeonba.be.chatting.dto.response.ChatMessageResponse;
1525
import yeonba.be.chatting.dto.response.ChatRoomResponse;
1626
import yeonba.be.chatting.service.ChatService;
1727
import yeonba.be.util.CustomResponse;
1828

1929
@Tag(name = "Chatting", description = "채팅 API")
20-
@RestController
30+
@Slf4j
31+
@Controller
2132
@RequiredArgsConstructor
22-
public class ChattingController {
33+
public class ChatController {
2334

2435
private final ChatService chatService;
2536

26-
@Operation(summary = "채팅 목록 조회", description = "자신이 참여 중인 채팅 목록을 조회할 수 있습니다.")
37+
@MessageMapping("/chat")
38+
public void chat(ChatPublishRequest request) {
39+
40+
log.info("chatting test log {}", request.getContent());
41+
42+
chatService.publish(request);
43+
}
44+
45+
@Operation(summary = "채팅 메시지 목록 조회", description = "특정 채팅방의 메시지 목록을 조회할 수 있습니다.")
46+
@ApiResponse(responseCode = "200", description = "채팅 메시지 목록 조회 성공")
47+
@ResponseBody
48+
@GetMapping("/chat-rooms/{roomId}/messages")
49+
public ResponseEntity<CustomResponse<List<ChatMessageResponse>>> getChatMessages(
50+
@RequestAttribute("userId") long userId,
51+
@Parameter(description = "채팅방 ID", example = "1")
52+
@PathVariable long roomId) {
53+
54+
List<ChatMessageResponse> response = chatService.getChatMessages(userId, roomId);
55+
56+
return ResponseEntity
57+
.ok()
58+
.body(new CustomResponse<>(response));
59+
}
60+
61+
@Operation(summary = "채팅방 목록 조회", description = "자신이 참여 중인 채팅 목록을 조회할 수 있습니다.")
2762
@ApiResponse(responseCode = "200", description = "참여 중인 채팅 목록 조회 성공")
28-
@GetMapping("/chattings")
63+
@ResponseBody
64+
@GetMapping("/chat-rooms")
2965
public ResponseEntity<CustomResponse<List<ChatRoomResponse>>> getChatRooms(
3066
@RequestAttribute("userId") long userId) {
3167

@@ -38,6 +74,7 @@ public ResponseEntity<CustomResponse<List<ChatRoomResponse>>> getChatRooms(
3874

3975
@Operation(summary = "채팅 요청", description = "다른 사용자에게 채팅을 요청할 수 있습니다.")
4076
@ApiResponse(responseCode = "200", description = "채팅 요청 정상 처리")
77+
@ResponseBody
4178
@PostMapping("/users/{partnerId}/chat")
4279
public ResponseEntity<CustomResponse<Void>> requestChat(
4380
@RequestAttribute("userId") long userId,
@@ -53,6 +90,7 @@ public ResponseEntity<CustomResponse<Void>> requestChat(
5390

5491
@Operation(summary = "채팅 요청 수락", description = "요청받은 채팅을 수락할 수 있습니다.")
5592
@ApiResponse(responseCode = "200", description = "채팅 요청 수락 정상 처리")
93+
@ResponseBody
5694
@PostMapping("/notifications/{notificationId}/chat")
5795
public ResponseEntity<CustomResponse<Void>> acceptRequestedChat(
5896
@RequestAttribute("userId") long userId,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package yeonba.be.chatting.dto.request;
2+
3+
import java.io.Serializable;
4+
import java.time.LocalDateTime;
5+
import lombok.Getter;
6+
7+
@Getter
8+
public class ChatPublishRequest implements Serializable {
9+
10+
private long roomId;
11+
private long userId;
12+
private String userName;
13+
private String content;
14+
private LocalDateTime sentAt;
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package yeonba.be.chatting.dto.request;
2+
3+
import java.io.Serializable;
4+
import java.time.LocalDateTime;
5+
import lombok.Getter;
6+
7+
@Getter
8+
public class ChatSubscribeResponse implements Serializable {
9+
10+
private long roomId;
11+
private long userId;
12+
private String userName;
13+
private String content;
14+
private LocalDateTime sentAt;
15+
}

be/src/main/java/yeonba/be/chatting/dto/request/ChattingSendMessageRequest.java

-17
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package yeonba.be.chatting.dto.response;
2+
3+
import java.time.LocalDateTime;
4+
import lombok.AllArgsConstructor;
5+
import lombok.Getter;
6+
7+
@Getter
8+
@AllArgsConstructor
9+
public class ChatMessageResponse {
10+
11+
private long userId;
12+
private String userName;
13+
private String content;
14+
private LocalDateTime sentAt;
15+
}

be/src/main/java/yeonba/be/chatting/entity/ChatMessage.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,13 @@ public class ChatMessage {
5353

5454
private LocalDateTime deletedAt;
5555

56-
public ChatMessage(ChatRoom chatRoom, User sender, User receiver, String content) {
56+
public ChatMessage(ChatRoom chatRoom, User sender, User receiver, String content, LocalDateTime sentAt) {
5757

5858
this.chatRoom = chatRoom;
5959
this.sender = sender;
6060
this.receiver = receiver;
6161
this.content = content;
62+
this.sentAt = sentAt;
6263
this.read = false;
6364
}
6465
}

be/src/main/java/yeonba/be/chatting/repository/chatmessage/ChatMessageCommand.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ public class ChatMessageCommand {
1010

1111
private final ChatMessageRepository chatMessageRepository;
1212

13-
public ChatMessage createChatMessage(ChatMessage message) {
13+
public ChatMessage save(ChatMessage message) {
1414

1515
return chatMessageRepository.save(message);
1616
}

be/src/main/java/yeonba/be/chatting/repository/chatmessage/ChatMessageQuery.java

+7
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package yeonba.be.chatting.repository.chatmessage;
22

3+
import java.util.List;
34
import lombok.RequiredArgsConstructor;
45
import org.springframework.stereotype.Component;
56
import yeonba.be.chatting.entity.ChatMessage;
7+
import yeonba.be.chatting.entity.ChatRoom;
68

79
@Component
810
@RequiredArgsConstructor
@@ -19,4 +21,9 @@ public int countUnreadMessagesByChatRoomId(long chatRoomId) {
1921

2022
return chatMessageRepository.countByChatRoomIdAndReadIsFalse(chatRoomId);
2123
}
24+
25+
public List<ChatMessage> findAllByChatRoom(ChatRoom chatRoom) {
26+
27+
return chatMessageRepository.findAllByChatRoomOrderBySentAtDesc(chatRoom);
28+
}
2229
}
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
package yeonba.be.chatting.repository.chatmessage;
22

3+
import java.util.List;
34
import org.springframework.data.jpa.repository.JpaRepository;
45
import org.springframework.stereotype.Repository;
56
import yeonba.be.chatting.entity.ChatMessage;
7+
import yeonba.be.chatting.entity.ChatRoom;
68

79
@Repository
810
public interface ChatMessageRepository extends JpaRepository<ChatMessage, Long> {
911

1012
ChatMessage findFirstByChatRoomIdOrderBySentAtDesc(long chatRoomId);
1113

1214
int countByChatRoomIdAndReadIsFalse(long chatRoomId);
15+
16+
List<ChatMessage> findAllByChatRoomOrderBySentAtDesc(ChatRoom chatRoom);
1317
}

be/src/main/java/yeonba/be/chatting/repository/chatroom/ChatRoomQuery.java

+11
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ public class ChatRoomQuery {
1515

1616
private final ChatRoomRepository chatRoomRepository;
1717

18+
public ChatRoom findById(long id) {
19+
20+
return chatRoomRepository.findById(id)
21+
.orElseThrow(() -> new GeneralException(NOT_FOUND_CHAT_ROOM));
22+
}
23+
1824
public List<ChatRoom> findAllBy(User user) {
1925

2026
return chatRoomRepository.findAllByUserAndActiveIsTrue(user);
@@ -25,4 +31,9 @@ public ChatRoom findBy(User sender, User receiver) {
2531
return chatRoomRepository.findBySenderAndReceiver(sender, receiver)
2632
.orElseThrow(() -> new GeneralException(NOT_FOUND_CHAT_ROOM));
2733
}
34+
35+
public boolean existsBy(User sender, User receiver) {
36+
37+
return chatRoomRepository.existsBySenderAndReceiverAndActiveIsTrue(sender, receiver);
38+
}
2839
}

be/src/main/java/yeonba/be/chatting/repository/chatroom/ChatRoomRepository.java

+4-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@
77
import yeonba.be.user.entity.User;
88

99
@Repository
10-
public interface ChatRoomRepository extends JpaRepository<ChatRoom, Long>, ChatRoomRepositoryCustom {
10+
public interface ChatRoomRepository extends JpaRepository<ChatRoom, Long>,
11+
ChatRoomRepositoryCustom {
1112

1213
Optional<ChatRoom> findBySenderAndReceiver(User sender, User receiver);
14+
15+
boolean existsBySenderAndReceiverAndActiveIsTrue(User sender, User receiver);
1316
}

be/src/main/java/yeonba/be/chatting/service/ChatService.java

+65-5
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,14 @@
44
import java.util.List;
55
import java.util.Optional;
66
import lombok.RequiredArgsConstructor;
7+
import lombok.extern.slf4j.Slf4j;
78
import org.springframework.context.ApplicationEventPublisher;
9+
import org.springframework.data.redis.listener.ChannelTopic;
10+
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
811
import org.springframework.stereotype.Service;
912
import org.springframework.transaction.annotation.Transactional;
13+
import yeonba.be.chatting.dto.request.ChatPublishRequest;
14+
import yeonba.be.chatting.dto.response.ChatMessageResponse;
1015
import yeonba.be.chatting.dto.response.ChatRoomResponse;
1116
import yeonba.be.chatting.entity.ChatMessage;
1217
import yeonba.be.chatting.entity.ChatRoom;
@@ -15,6 +20,7 @@
1520
import yeonba.be.chatting.repository.chatroom.ChatRoomCommand;
1621
import yeonba.be.chatting.repository.chatroom.ChatRoomQuery;
1722
import yeonba.be.exception.BlockException;
23+
import yeonba.be.exception.ChatException;
1824
import yeonba.be.exception.GeneralException;
1925
import yeonba.be.exception.NotificationException;
2026
import yeonba.be.notification.entity.Notification;
@@ -26,6 +32,7 @@
2632
import yeonba.be.user.repository.block.BlockQuery;
2733
import yeonba.be.user.repository.user.UserQuery;
2834

35+
@Slf4j
2936
@Service
3037
@RequiredArgsConstructor
3138
public class ChatService {
@@ -36,9 +43,47 @@ public class ChatService {
3643
private final ChatMessageQuery chatMessageQuery;
3744
private final UserQuery userQuery;
3845
private final BlockQuery blockQuery;
39-
private final NotificationQuery notificationQuey;
46+
private final NotificationQuery notificationQuery;
4047

4148
private final ApplicationEventPublisher eventPublisher;
49+
private final RedisChattingPublisher redisChattingPublisher;
50+
private final RedisChattingSubscriber adapter;
51+
private final RedisMessageListenerContainer container;
52+
53+
@Transactional
54+
public void publish(ChatPublishRequest request) {
55+
56+
ChatRoom chatRoom = chatRoomQuery.findById(request.getRoomId());
57+
User sender = userQuery.findById(request.getUserId());
58+
User receiver = chatRoom.getSender().equals(sender) ? chatRoom.getReceiver()
59+
: chatRoom.getSender();
60+
61+
// TODO: 메시지 Pub/Sub과 메시지 저장 로직 비동기 처리(id, user 등 request, response 변경 가능)
62+
redisChattingPublisher.publish(new ChannelTopic(String.valueOf(request.getRoomId())),
63+
request);
64+
chatMessageCommand.save(
65+
new ChatMessage(chatRoom, sender, receiver, request.getContent(), request.getSentAt()));
66+
}
67+
68+
@Transactional(readOnly = true)
69+
public List<ChatMessageResponse> getChatMessages(long userId, long roomId) {
70+
71+
User user = userQuery.findById(userId);
72+
73+
ChatRoom chatRoom = chatRoomQuery.findById(roomId);
74+
75+
if (!user.equals(chatRoom.getSender()) && !user.equals(chatRoom.getReceiver())) {
76+
throw new GeneralException(ChatException.NOT_YOUR_CHAT_ROOM);
77+
}
78+
79+
List<ChatMessage> chatMessages = chatMessageQuery.findAllByChatRoom(chatRoom);
80+
81+
return chatMessages.stream()
82+
.map(chatMessage -> new ChatMessageResponse(chatMessage.getSender().getId(),
83+
chatMessage.getSender().getNickname(),
84+
chatMessage.getContent(), chatMessage.getSentAt()))
85+
.toList();
86+
}
4287

4388
@Transactional(readOnly = true)
4489
public List<ChatRoomResponse> getChatRooms(long userId) {
@@ -79,6 +124,13 @@ public void requestChat(long senderId, long receiverId) {
79124
throw new GeneralException(BlockException.ALREADY_BLOCKED_USER);
80125
}
81126

127+
// 이미 채팅 중인 사용자인 지 검증
128+
boolean chatRoomExist =
129+
chatRoomQuery.existsBy(sender, receiver) || chatRoomQuery.existsBy(receiver, sender);
130+
if (chatRoomExist) {
131+
throw new GeneralException(ChatException.ALREADY_CHAT_USER);
132+
}
133+
82134
// 비활성화된 채팅방 생성
83135
chatRoomCommand.createChatRoom(new ChatRoom(sender, receiver));
84136

@@ -89,9 +141,10 @@ public void requestChat(long senderId, long receiverId) {
89141
eventPublisher.publishEvent(notificationSendEvent);
90142
}
91143

144+
@Transactional
92145
public void acceptRequestedChat(long userId, long notificationId) {
93146

94-
Notification notification = notificationQuey.findById(notificationId);
147+
Notification notification = notificationQuery.findById(notificationId);
95148

96149
// 채팅 요청 알림인지 검증
97150
if (!notification.getType().isChattingRequest()) {
@@ -105,15 +158,22 @@ public void acceptRequestedChat(long userId, long notificationId) {
105158
// 본인에게 온 채팅 요청인지 검증
106159
if (receiver.equals(userQuery.findById(userId))) {
107160

108-
throw new GeneralException(NotificationException.NOT_YOUR_CHATTING_REQUEST_NOTIFICATION);
161+
throw new GeneralException(
162+
NotificationException.NOT_YOUR_CHATTING_REQUEST_NOTIFICATION);
109163
}
110164

111165
// 채팅방 활성화
112166
ChatRoom chatRoom = chatRoomQuery.findBy(sender, receiver);
113167
chatRoom.activeRoom();
114168

115-
String activeRoom = "채팅방이 활성화되었습니다.";
116-
chatMessageCommand.createChatMessage(new ChatMessage(chatRoom, sender, receiver, activeRoom));
169+
String activeRoom = "채팅방이 생성되었습니다.";
170+
171+
chatMessageCommand.save(
172+
new ChatMessage(chatRoom, sender, receiver, activeRoom, LocalDateTime.now()));
173+
174+
// 메시지 수신을 위한 Redis Pub/Sub 구독
175+
container.addMessageListener(adapter, new ChannelTopic(String.valueOf(chatRoom.getId())));
176+
log.info("channel topic 생성 {}", chatRoom.getId());
117177

118178
NotificationSendEvent notificationSendEvent = new NotificationSendEvent(
119179
NotificationType.CHATTING_REQUEST_ACCEPTED, receiver, sender,

0 commit comments

Comments
 (0)