-
Notifications
You must be signed in to change notification settings - Fork 1
[FIX] 방 관리 기능 개선 및 버그 수정 (+ 채팅 refactor) #93
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
The head ref may contain hidden characters: "fix/#91/\uBC29-\uAD00\uB9AC-\uAE30\uB2A5-\uAC1C\uC120-\uBC0F-\uBC84\uADF8-\uC218\uC815"
Changes from 16 commits
92b4722
9a72c6f
5e8d971
d99c0e9
20e4356
a6ff163
678453a
b7cf5a8
62aeb59
9a668dc
66a1704
579bcd0
1c0583e
9d8afeb
5ce9ead
db34317
b408a75
47affd1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| package com.project.dorumdorum.domain.chat.application.dto.response; | ||
|
|
||
| public record NotificationMessage( | ||
| NotificationType type, | ||
| String roomNo, | ||
| String chatRoomNo | ||
| ) { | ||
| public static NotificationMessage roomDeleted(String roomNo, String chatRoomNo) { | ||
| return new NotificationMessage(NotificationType.ROOM_DELETED, roomNo, chatRoomNo); | ||
| } | ||
|
|
||
| public static NotificationMessage kicked(String roomNo, String chatRoomNo) { | ||
| return new NotificationMessage(NotificationType.KICKED_FROM_ROOM, roomNo, chatRoomNo); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| package com.project.dorumdorum.domain.chat.application.dto.response; | ||
|
|
||
| import com.fasterxml.jackson.annotation.JsonValue; | ||
|
|
||
| public enum NotificationType { | ||
| ROOM_DELETED("ROOM_DELETED"), | ||
| KICKED_FROM_ROOM("KICKED_FROM_ROOM"); | ||
|
|
||
| private final String value; | ||
|
|
||
| NotificationType(String value) { | ||
| this.value = value; | ||
| } | ||
|
|
||
| @JsonValue | ||
| public String getValue() { | ||
| return value; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| package com.project.dorumdorum.domain.chat.application.event; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| public record ChatWebSocketNotificationEvent( | ||
| List<BroadcastTask> broadcasts, | ||
| List<UserNotifyTask> userNotifications | ||
| ) { | ||
| public record BroadcastTask(String chatRoomNo, Object payload) {} | ||
|
|
||
| public record UserNotifyTask(String userNo, Object payload) {} | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| package com.project.dorumdorum.domain.chat.application.event; | ||
|
|
||
| import com.project.dorumdorum.domain.chat.infra.websocket.ChatWebSocketSendService; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.scheduling.annotation.Async; | ||
| import org.springframework.stereotype.Component; | ||
| import org.springframework.transaction.event.TransactionPhase; | ||
| import org.springframework.transaction.event.TransactionalEventListener; | ||
|
|
||
| @Component | ||
| @RequiredArgsConstructor | ||
| public class ChatWebSocketNotificationEventListener { | ||
|
|
||
| private final ChatWebSocketSendService chatWebSocketSendService; | ||
|
|
||
| @Async("notificationExecutor") | ||
| @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT, fallbackExecution = false) | ||
| public void handle(ChatWebSocketNotificationEvent event) { | ||
| event.userNotifications().forEach(task -> | ||
| chatWebSocketSendService.notifyUser(task.userNo(), task.payload())); | ||
| event.broadcasts().forEach(task -> | ||
| chatWebSocketSendService.broadcast(task.chatRoomNo(), task.payload())); | ||
| } | ||
|
Comment on lines
+16
to
+23
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial 부분 실패 시 후속 태스크가 누락될 수 있는 점 확인 부탁드립니다.
방어적으로 각 태스크 전송을 try/catch로 감싸서 개별 실패가 후속 태스크에 영향을 주지 않도록 하면 조금 더 견고해집니다. ♻️ 제안 수정안- event.broadcasts().forEach(task ->
- chatWebSocketSendService.broadcast(task.chatRoomNo(), task.payload()));
- event.userNotifications().forEach(task ->
- chatWebSocketSendService.notifyUser(task.userNo(), task.payload()));
+ event.broadcasts().forEach(task -> {
+ try {
+ chatWebSocketSendService.broadcast(task.chatRoomNo(), task.payload());
+ } catch (Exception e) {
+ log.warn("[Chat] broadcast 전파 실패. chatRoomNo={}", task.chatRoomNo(), e);
+ }
+ });
+ event.userNotifications().forEach(task -> {
+ try {
+ chatWebSocketSendService.notifyUser(task.userNo(), task.payload());
+ } catch (Exception e) {
+ log.warn("[Chat] notifyUser 전파 실패. userNo={}", task.userNo(), e);
+ }
+ });(로깅을 위해 🤖 Prompt for AI Agents |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| package com.project.dorumdorum.domain.chat.application.event; | ||
|
|
||
| import com.project.dorumdorum.domain.chat.application.dto.response.NotificationMessage; | ||
| import com.project.dorumdorum.domain.chat.domain.entity.ChatRoom; | ||
| import com.project.dorumdorum.domain.chat.domain.entity.ChatRoomMember; | ||
| import com.project.dorumdorum.domain.chat.domain.service.ChatMessageService; | ||
| import com.project.dorumdorum.domain.chat.domain.service.ChatRoomMemberService; | ||
| import com.project.dorumdorum.domain.chat.domain.service.ChatRoomService; | ||
| import com.project.dorumdorum.domain.room.application.event.RoomDeletedEvent; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.context.ApplicationEventPublisher; | ||
| import org.springframework.stereotype.Component; | ||
| import org.springframework.transaction.event.TransactionPhase; | ||
| import org.springframework.transaction.event.TransactionalEventListener; | ||
|
|
||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
|
|
||
| @Component | ||
| @RequiredArgsConstructor | ||
| public class RoomDeletedEventListener { | ||
|
|
||
| private final ChatRoomService chatRoomService; | ||
| private final ChatRoomMemberService chatRoomMemberService; | ||
| private final ChatMessageService chatMessageService; | ||
| private final ApplicationEventPublisher eventPublisher; | ||
|
|
||
| /** | ||
| * 방 삭제(RoomDeletedEvent) → GROUP·DIRECT 채팅방 전체 삭제 처리 | ||
| * 발행: DeleteRoomUseCase (room 도메인 담당자) | ||
| * | ||
| * BEFORE_COMMIT: 부모 트랜잭션(DeleteRoomUseCase)에 참여하여 방 삭제 + 채팅방 삭제를 하나의 트랜잭션으로 처리. | ||
| * 채팅방 멤버 전원에게 /queue/notification으로 NotificationMessage 전달. | ||
| * /topic/chat-room/{chatRoomNo}는 ChatMessageResponse 전용 채널이므로 여기서 사용하지 않음. | ||
| */ | ||
| @TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT) | ||
| public void handle(RoomDeletedEvent event) { | ||
| List<ChatRoom> chatRooms = chatRoomService.findAllByRoomNo(event.roomNo()); | ||
| List<ChatWebSocketNotificationEvent.UserNotifyTask> userNotifications = new ArrayList<>(); | ||
|
|
||
| for (ChatRoom chatRoom : chatRooms) { | ||
| NotificationMessage notification = NotificationMessage.roomDeleted(event.roomNo(), chatRoom.getChatRoomNo()); | ||
|
|
||
|
Comment on lines
+42
to
+43
|
||
| // 삭제 전 멤버 조회 → 전원에게 /queue/notification 개인 알림 | ||
| List<ChatRoomMember> members = chatRoomMemberService.findByChatRoom(chatRoom); | ||
| for (ChatRoomMember member : members) { | ||
| userNotifications.add(new ChatWebSocketNotificationEvent.UserNotifyTask( | ||
| member.getUserNo(), notification)); | ||
| } | ||
|
|
||
| chatMessageService.deleteAllByChatRoom(chatRoom.getChatRoomNo()); | ||
| chatRoomMemberService.deleteAllByChatRoom(chatRoom); | ||
| chatRoomService.deleteByChatRoomNo(chatRoom.getChatRoomNo()); | ||
| } | ||
|
|
||
| if (!userNotifications.isEmpty()) { | ||
| eventPublisher.publishEvent(new ChatWebSocketNotificationEvent(List.of(), userNotifications)); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| package com.project.dorumdorum.domain.chat.infra.websocket; | ||
|
|
||
| import lombok.RequiredArgsConstructor; | ||
| import lombok.extern.slf4j.Slf4j; | ||
| import org.springframework.messaging.simp.SimpMessagingTemplate; | ||
| import org.springframework.retry.annotation.Backoff; | ||
| import org.springframework.retry.annotation.Recover; | ||
| import org.springframework.retry.annotation.Retryable; | ||
| import org.springframework.stereotype.Service; | ||
|
|
||
| @Slf4j | ||
| @Service | ||
| @RequiredArgsConstructor | ||
| public class ChatWebSocketSendService { | ||
|
|
||
| private final SimpMessagingTemplate messagingTemplate; | ||
|
|
||
| @Retryable(retryFor = Exception.class, maxAttempts = 3, backoff = @Backoff(delay = 1000, multiplier = 2)) | ||
| public void broadcast(String chatRoomNo, Object payload) { | ||
| messagingTemplate.convertAndSend("/topic/chat-room/" + chatRoomNo, payload); | ||
| } | ||
|
|
||
| @Retryable(retryFor = Exception.class, maxAttempts = 3, backoff = @Backoff(delay = 1000, multiplier = 2)) | ||
| public void notifyUser(String userNo, Object payload) { | ||
| messagingTemplate.convertAndSendToUser(userNo, "/queue/notification", payload); | ||
| } | ||
|
|
||
| // broadcast / notifyUser 모두 (String, Object) 시그니처이므로 단일 recover로 처리 | ||
| @Recover | ||
| public void recoverSend(Exception e, String recipient, Object payload) { | ||
| log.warn("[Chat] WebSocket 알림 최종 실패. recipient={}", recipient, e); | ||
| } | ||
|
Comment on lines
+18
to
+32
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# `@EnableRetry` 설정 여부 확인
rg -nP '@EnableRetry\b' --type=javaRepository: DorumDorum/BE Length of output: 136 🏁 Script executed: # DorumdorumApplication.java에서 `@EnableRetry` 설정 확인
cat -n src/main/java/com/project/dorumdorum/DorumdorumApplication.java | head -30Repository: DorumDorum/BE Length of output: 983 🏁 Script executed: # ChatWebSocketSendService.java 전체 내용 확인
cat -n src/main/java/com/project/dorumdorum/domain/chat/infra/websocket/ChatWebSocketSendService.javaRepository: DorumDorum/BE Length of output: 1613 🏁 Script executed: # Spring Retry가 지원하는 예외 타입과 messaging 관련 예외 검색
rg -i 'messageconversion|messagedelivery|exception' --type=java src/main/java/com/project/dorumdorum/domain/chat/ -A 2Repository: DorumDorum/BE Length of output: 13024 재시도 정책 범위와 다행히
🤖 Prompt for AI Agents |
||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.