Skip to content

Commit 455aaae

Browse files
authored
Merge branch 'dev' into Feat/102
2 parents 85e3d1d + 53d6c9e commit 455aaae

17 files changed

+1222
-61
lines changed

src/main/java/com/back/domain/chat/room/controller/RoomChatApiController.java

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,36 @@
11
package com.back.domain.chat.room.controller;
22

3+
import com.back.domain.chat.room.dto.ChatClearRequest;
4+
import com.back.domain.chat.room.dto.ChatClearResponse;
5+
import com.back.domain.chat.room.dto.ChatClearedNotification;
36
import com.back.domain.chat.room.dto.RoomChatPageResponse;
47
import com.back.domain.chat.room.service.RoomChatService;
58
import com.back.global.common.dto.RsData;
69
import com.back.global.security.user.CustomUserDetails;
710
import io.swagger.v3.oas.annotations.Operation;
811
import io.swagger.v3.oas.annotations.tags.Tag;
12+
import jakarta.validation.Valid;
913
import lombok.RequiredArgsConstructor;
1014
import org.springframework.format.annotation.DateTimeFormat;
1115
import org.springframework.http.HttpStatus;
1216
import org.springframework.http.ResponseEntity;
17+
import org.springframework.messaging.simp.SimpMessagingTemplate;
1318
import org.springframework.security.core.annotation.AuthenticationPrincipal;
1419
import org.springframework.web.bind.annotation.*;
1520

1621
import java.time.LocalDateTime;
1722

1823
@RestController
1924
@RequiredArgsConstructor
20-
@RequestMapping("/api")
21-
@Tag(name = "RoomChat API", description = "스터디룸 채팅 메시지 조회 관련 API")
25+
@RequestMapping("/api/rooms/{roomId}/messages")
26+
@Tag(name = "RoomChat API", description = "스터디룸 채팅 메시지 관련 API")
2227
public class RoomChatApiController {
2328

2429
private final RoomChatService roomChatService;
30+
private final SimpMessagingTemplate messagingTemplate;
2531

2632
// 방 채팅 메시지 조회 (페이징, 특정 시간 이전 메시지)
27-
@GetMapping("/rooms/{roomId}/messages")
33+
@GetMapping
2834
@Operation(summary = "스터디룸 채팅방 메시지 목록 조회", description = "특정 채팅방의 이전 메시지 기록을 페이징하여 조회합니다.")
2935
public ResponseEntity<RsData<RoomChatPageResponse>> getRoomChatMessages(
3036
@PathVariable Long roomId,
@@ -40,4 +46,48 @@ public ResponseEntity<RsData<RoomChatPageResponse>> getRoomChatMessages(
4046
.body(RsData.success("채팅 기록 조회 성공", chatHistory));
4147
}
4248

49+
// 방 채팅 메시지 일괄 삭제 (방장, 부방장 권한)
50+
@DeleteMapping
51+
@Operation(
52+
summary = "스터디룸 채팅 일괄 삭제",
53+
description = "방장 또는 부방장이 해당 방의 모든 채팅 메시지를 삭제합니다. 실행 후 실시간으로 모든 방 멤버에게 알림이 전송됩니다."
54+
)
55+
public ResponseEntity<RsData<ChatClearResponse>> clearRoomMessages(
56+
@PathVariable Long roomId,
57+
@Valid @RequestBody ChatClearRequest request,
58+
@AuthenticationPrincipal CustomUserDetails userDetails) {
59+
60+
// 삭제 전 메시지 수 조회
61+
int messageCount = roomChatService.getRoomChatCount(roomId);
62+
63+
// 채팅 일괄 삭제 실행
64+
ChatClearedNotification.ClearedByDto clearedByInfo =
65+
roomChatService.clearRoomChat(roomId, userDetails.getUserId());
66+
67+
// 응답 데이터 생성
68+
ChatClearResponse responseData = ChatClearResponse.create(
69+
roomId,
70+
messageCount,
71+
clearedByInfo.userId(),
72+
clearedByInfo.nickname(),
73+
clearedByInfo.role()
74+
);
75+
76+
// WebSocket을 통해 실시간 알림 전송
77+
ChatClearedNotification notification = ChatClearedNotification.create(
78+
roomId,
79+
messageCount,
80+
clearedByInfo.userId(),
81+
clearedByInfo.nickname(),
82+
clearedByInfo.profileImageUrl(),
83+
clearedByInfo.role()
84+
);
85+
86+
messagingTemplate.convertAndSend("/topic/room/" + roomId + "/chat-cleared", notification);
87+
88+
return ResponseEntity
89+
.status(HttpStatus.OK)
90+
.body(RsData.success("채팅 메시지 일괄 삭제 완료", responseData));
91+
}
92+
4393
}

src/main/java/com/back/domain/chat/room/controller/RoomChatWebSocketController.java

Lines changed: 74 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,39 @@
11
package com.back.domain.chat.room.controller;
22

3+
import com.back.domain.chat.room.dto.ChatClearRequest;
4+
import com.back.domain.chat.room.dto.ChatClearedNotification;
35
import com.back.domain.studyroom.entity.RoomChatMessage;
46
import com.back.domain.chat.room.dto.RoomChatMessageDto;
7+
import com.back.global.exception.CustomException;
58
import com.back.global.security.user.CustomUserDetails;
6-
import com.back.global.websocket.dto.WebSocketErrorResponse;
79
import com.back.domain.chat.room.service.RoomChatService;
10+
import com.back.global.websocket.util.WebSocketErrorHelper;
811
import io.swagger.v3.oas.annotations.tags.Tag;
912
import lombok.RequiredArgsConstructor;
13+
import lombok.extern.slf4j.Slf4j;
1014
import org.springframework.messaging.handler.annotation.DestinationVariable;
1115
import org.springframework.messaging.handler.annotation.MessageMapping;
16+
import org.springframework.messaging.handler.annotation.Payload;
1217
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
1318
import org.springframework.messaging.simp.SimpMessagingTemplate;
1419
import org.springframework.security.core.Authentication;
1520
import org.springframework.stereotype.Controller;
1621

1722
import java.security.Principal;
1823

24+
@Slf4j
1925
@Controller
2026
@RequiredArgsConstructor
2127
@Tag(name = "RoomChat WebSocket", description = "STOMP를 이용한 실시간 채팅 WebSocket 컨트롤러 (Swagger에서 직접 테스트 불가)")
2228
public class RoomChatWebSocketController {
2329

2430
private final RoomChatService roomChatService;
2531
private final SimpMessagingTemplate messagingTemplate;
32+
private final WebSocketErrorHelper errorHelper;
2633

2734
/**
2835
* 방 채팅 메시지 처리
2936
* 클라이언트가 /app/chat/room/{roomId}로 메시지 전송 시 호출
30-
*
31-
* @param roomId 스터디룸 ID
32-
* @param chatMessage 채팅 메시지 (content, messageType, attachmentId)
33-
* @param headerAccessor WebSocket 헤더 정보
34-
* @param principal 인증된 사용자 정보
3537
*/
3638
@MessageMapping("/chat/room/{roomId}")
3739
public void handleRoomChat(@DestinationVariable Long roomId,
@@ -43,7 +45,7 @@ public void handleRoomChat(@DestinationVariable Long roomId,
4345
// WebSocket에서 인증된 사용자 정보 추출
4446
CustomUserDetails userDetails = extractUserDetails(principal);
4547
if (userDetails == null) {
46-
sendErrorToUser(headerAccessor.getSessionId(), "WS_UNAUTHORIZED", "인증이 필요합니다");
48+
errorHelper.sendUnauthorizedError(headerAccessor.getSessionId());
4749
return;
4850
}
4951

@@ -75,16 +77,74 @@ public void handleRoomChat(@DestinationVariable Long roomId,
7577
// 해당 방의 모든 구독자에게 브로드캐스트
7678
messagingTemplate.convertAndSend("/topic/room/" + roomId, responseMessage);
7779

80+
} catch (CustomException e) {
81+
log.warn("채팅 메시지 처리 실패 - roomId: {}, error: {}", roomId, e.getMessage());
82+
errorHelper.sendCustomExceptionToUser(headerAccessor.getSessionId(), e);
83+
7884
} catch (Exception e) {
79-
// 에러 응답을 해당 사용자에게만 전송
80-
WebSocketErrorResponse errorResponse = WebSocketErrorResponse.create(
81-
"WS_ROOM_NOT_FOUND",
82-
"존재하지 않는 방입니다"
85+
log.error("채팅 메시지 처리 중 예상치 못한 오류 발생 - roomId: {}", roomId, e);
86+
errorHelper.sendGenericErrorToUser(headerAccessor.getSessionId(), e, "메시지 전송 중 오류가 발생했습니다");
87+
}
88+
}
89+
90+
/**
91+
* 스터디룸 채팅 일괄 삭제 처리
92+
* 클라이언트가 /app/chat/room/{roomId}/clear로 삭제 요청 시 호출
93+
*/
94+
@MessageMapping("/chat/room/{roomId}/clear")
95+
public void clearRoomChat(@DestinationVariable Long roomId,
96+
@Payload ChatClearRequest request,
97+
SimpMessageHeaderAccessor headerAccessor,
98+
Principal principal) {
99+
100+
try {
101+
log.info("WebSocket 채팅 일괄 삭제 요청 - roomId: {}", roomId);
102+
103+
// 사용자 인증 확인
104+
CustomUserDetails userDetails = extractUserDetails(principal);
105+
if (userDetails == null) {
106+
errorHelper.sendUnauthorizedError(headerAccessor.getSessionId());
107+
return;
108+
}
109+
110+
// 삭제 확인 메시지 검증
111+
if (!request.isValidConfirmMessage()) {
112+
errorHelper.sendErrorToUser(headerAccessor.getSessionId(), "WS_011",
113+
"삭제 확인 메시지가 일치하지 않습니다");
114+
return;
115+
}
116+
117+
Long currentUserId = userDetails.getUserId();
118+
119+
// 삭제 전에 메시지 수 먼저 조회 (삭제 후에는 0이 되므로)
120+
int deletedCount = roomChatService.getRoomChatCount(roomId);
121+
122+
// 채팅 일괄 삭제 실행
123+
ChatClearedNotification.ClearedByDto clearedByInfo = roomChatService.clearRoomChat(roomId, currentUserId);
124+
125+
// 알림 생성
126+
ChatClearedNotification notification = ChatClearedNotification.create(
127+
roomId,
128+
deletedCount, // 삭제 전에 조회한 수 사용
129+
clearedByInfo.userId(),
130+
clearedByInfo.nickname(),
131+
clearedByInfo.profileImageUrl(),
132+
clearedByInfo.role()
83133
);
84134

85-
// 에러를 발생시킨 사용자에게만 전송
86-
String sessionId = headerAccessor.getSessionId();
87-
messagingTemplate.convertAndSendToUser(sessionId, "/queue/errors", errorResponse);
135+
// 해당 방의 모든 구독자에게 브로드캐스트
136+
messagingTemplate.convertAndSend("/topic/room/" + roomId + "/chat-cleared", notification);
137+
138+
log.info("WebSocket 채팅 일괄 삭제 완료 - roomId: {}, deletedCount: {}, userId: {}",
139+
roomId, deletedCount, currentUserId);
140+
141+
} catch (CustomException e) {
142+
log.warn("채팅 삭제 실패 - roomId: {}, error: {}", roomId, e.getMessage());
143+
errorHelper.sendCustomExceptionToUser(headerAccessor.getSessionId(), e);
144+
145+
} catch (Exception e) {
146+
log.error("채팅 일괄 삭제 중 예상치 못한 오류 발생 - roomId: {}", roomId, e);
147+
errorHelper.sendGenericErrorToUser(headerAccessor.getSessionId(), e, "채팅 삭제 중 오류가 발생했습니다");
88148
}
89149
}
90150

@@ -99,10 +159,4 @@ private CustomUserDetails extractUserDetails(Principal principal) {
99159
return null;
100160
}
101161

102-
// 특정 사용자에게 에러 메시지 전송
103-
private void sendErrorToUser(String sessionId, String errorCode, String errorMessage) {
104-
WebSocketErrorResponse errorResponse = WebSocketErrorResponse.create(errorCode, errorMessage);
105-
messagingTemplate.convertAndSendToUser(sessionId, "/queue/errors", errorResponse);
106-
}
107-
108162
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.back.domain.chat.room.dto;
2+
3+
import jakarta.validation.constraints.NotBlank;
4+
5+
public record ChatClearRequest(
6+
@NotBlank(message = "삭제 확인 메시지는 필수입니다")
7+
String confirmMessage
8+
) {
9+
10+
// 확인 메시지 상수
11+
public static final String REQUIRED_CONFIRM_MESSAGE = "모든 채팅을 삭제하겠습니다";
12+
13+
// 확인 메시지인지 검증
14+
public boolean isValidConfirmMessage() {
15+
return REQUIRED_CONFIRM_MESSAGE.equals(confirmMessage);
16+
}
17+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package com.back.domain.chat.room.dto;
2+
3+
public record ChatClearResponse(
4+
Long roomId,
5+
Integer deletedCount,
6+
java.time.LocalDateTime clearedAt,
7+
ClearedByDto clearedBy
8+
) {
9+
10+
public record ClearedByDto(
11+
Long userId,
12+
String nickname,
13+
String role
14+
) {}
15+
16+
// 성공 응답 생성 헬퍼
17+
public static ChatClearResponse create(Long roomId, int deletedCount,
18+
Long userId, String nickname, String role) {
19+
return new ChatClearResponse(
20+
roomId,
21+
deletedCount,
22+
java.time.LocalDateTime.now(),
23+
new ClearedByDto(userId, nickname, role)
24+
);
25+
}
26+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package com.back.domain.chat.room.dto;
2+
3+
/**
4+
* WebSocket 브로드캐스트용 채팅 삭제 알림 DTO
5+
*/
6+
public record ChatClearedNotification(
7+
String type,
8+
Long roomId,
9+
java.time.LocalDateTime clearedAt,
10+
ClearedByDto clearedBy,
11+
Integer deletedCount,
12+
String message
13+
) {
14+
15+
public record ClearedByDto(
16+
Long userId,
17+
String nickname,
18+
String profileImageUrl,
19+
String role
20+
) {}
21+
22+
// 알림 생성 헬퍼
23+
public static ChatClearedNotification create(Long roomId, int deletedCount,
24+
Long userId, String nickname, String profileImageUrl, String role) {
25+
ClearedByDto clearedBy = new ClearedByDto(userId, nickname, profileImageUrl, role);
26+
String message = nickname + "님이 모든 채팅을 삭제했습니다.";
27+
28+
return new ChatClearedNotification(
29+
"CHAT_CLEARED",
30+
roomId,
31+
java.time.LocalDateTime.now(),
32+
clearedBy,
33+
deletedCount,
34+
message
35+
);
36+
}
37+
}

0 commit comments

Comments
 (0)