Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

Expand All @@ -27,7 +26,6 @@
public class RoomChatApiController {

private final RoomChatService roomChatService;
private final SimpMessagingTemplate messagingTemplate;

// 방 채팅 메시지 조회 (페이징, 특정 시간 이전 메시지)
@GetMapping
Expand Down Expand Up @@ -73,18 +71,6 @@ public ResponseEntity<RsData<ChatClearResponse>> clearRoomMessages(
clearedByInfo.role()
);

// WebSocket을 통해 실시간 알림 전송
ChatClearedNotification notification = ChatClearedNotification.create(
roomId,
messageCount,
clearedByInfo.userId(),
clearedByInfo.nickname(),
clearedByInfo.profileImageUrl(),
clearedByInfo.role()
);

messagingTemplate.convertAndSend("/topic/room/" + roomId + "/chat-cleared", notification);

return ResponseEntity
.status(HttpStatus.OK)
.body(RsData.success("채팅 메시지 일괄 삭제 완료", responseData));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,36 +1,33 @@
package com.back.domain.chat.room.controller;

import com.back.domain.chat.room.dto.ChatClearRequest;
import com.back.domain.chat.room.dto.ChatClearedNotification;
import com.back.domain.chat.room.dto.RoomChatMessageRequest;
import com.back.domain.chat.room.dto.RoomChatMessageResponse;
import com.back.domain.studyroom.entity.RoomChatMessage;
import com.back.domain.chat.room.dto.RoomChatMessageDto;
import com.back.global.exception.CustomException;
import com.back.global.exception.ErrorCode;
import com.back.global.security.user.CustomUserDetails;
import com.back.domain.chat.room.service.RoomChatService;
import com.back.global.websocket.util.WebSocketAuthHelper;
import com.back.global.websocket.util.WebSocketErrorHelper;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.messaging.handler.annotation.DestinationVariable;
import org.springframework.messaging.handler.annotation.MessageExceptionHandler;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Controller;

import java.security.Principal;

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

private final RoomChatService roomChatService;
private final SimpMessagingTemplate messagingTemplate;
private final WebSocketAuthHelper authHelper;
private final WebSocketErrorHelper errorHelper;

/**
Expand All @@ -39,117 +36,30 @@ public class RoomChatWebSocketController {
*/
@MessageMapping("/chat/room/{roomId}")
public void handleRoomChat(@DestinationVariable Long roomId,
RoomChatMessageDto chatMessage,
SimpMessageHeaderAccessor headerAccessor,
@Payload RoomChatMessageRequest request,
Principal principal) {

try {
// WebSocket에서 인증된 사용자 정보 추출
CustomUserDetails userDetails = authHelper.extractUserDetails(principal);

if (userDetails == null) {
errorHelper.sendUnauthorizedError(headerAccessor.getSessionId());
return;
}

Long currentUserId = userDetails.getUserId();
String currentUserNickname = userDetails.getUsername();

// 메시지 정보 보완
RoomChatMessageDto enrichedMessage = chatMessage
.withRoomId(roomId)
.withUserId(currentUserId)
.withNickname(currentUserNickname);

// DB에 메시지 저장
RoomChatMessage savedMessage = roomChatService.saveRoomChatMessage(enrichedMessage);

// 저장된 메시지 정보로 응답 DTO 생성
RoomChatMessageDto responseMessage = RoomChatMessageDto.createResponse(
savedMessage.getId(),
roomId,
savedMessage.getUser().getId(),
savedMessage.getUser().getNickname(),
savedMessage.getUser().getProfileImageUrl(),
savedMessage.getContent(),
chatMessage.messageType(),
null, // 텍스트 채팅에서는 null
savedMessage.getCreatedAt()
);

// 해당 방의 모든 구독자에게 브로드캐스트
messagingTemplate.convertAndSend("/topic/room/" + roomId, responseMessage);

} catch (CustomException e) {
log.warn("채팅 메시지 처리 실패 - roomId: {}, error: {}", roomId, e.getMessage());
errorHelper.sendCustomExceptionToUser(headerAccessor.getSessionId(), e);

} catch (Exception e) {
log.error("채팅 메시지 처리 중 예상치 못한 오류 발생 - roomId: {}", roomId, e);
errorHelper.sendGenericErrorToUser(headerAccessor.getSessionId(), e, "메시지 전송 중 오류가 발생했습니다");
CustomUserDetails userDetails = WebSocketAuthHelper.extractUserDetails(principal);
if (userDetails == null) {
// 인증 정보가 없는 경우 예외 처리
throw new CustomException(ErrorCode.UNAUTHORIZED);
}
}

/**
* 스터디룸 채팅 일괄 삭제 처리
* 클라이언트가 /app/chat/room/{roomId}/clear로 삭제 요청 시 호출
*/
@MessageMapping("/chat/room/{roomId}/clear")
public void clearRoomChat(@DestinationVariable Long roomId,
@Payload ChatClearRequest request,
SimpMessageHeaderAccessor headerAccessor,
Principal principal) {

try {
log.info("WebSocket 채팅 일괄 삭제 요청 - roomId: {}", roomId);
RoomChatMessage savedMessage = roomChatService.saveRoomChatMessage(
roomId,
userDetails.getUserId(),
request
);

// 사용자 인증 확인
CustomUserDetails userDetails = authHelper.extractUserDetails(principal);

if (userDetails == null) {
errorHelper.sendUnauthorizedError(headerAccessor.getSessionId());
return;
}

// 삭제 확인 메시지 검증
if (!request.isValidConfirmMessage()) {
errorHelper.sendErrorToUser(headerAccessor.getSessionId(), "WS_011",
"삭제 확인 메시지가 일치하지 않습니다");
return;
}

Long currentUserId = userDetails.getUserId();

// 삭제 전에 메시지 수 먼저 조회 (삭제 후에는 0이 되므로)
int deletedCount = roomChatService.getRoomChatCount(roomId);

// 채팅 일괄 삭제 실행
ChatClearedNotification.ClearedByDto clearedByInfo = roomChatService.clearRoomChat(roomId, currentUserId);

// 알림 생성
ChatClearedNotification notification = ChatClearedNotification.create(
roomId,
deletedCount, // 삭제 전에 조회한 수 사용
clearedByInfo.userId(),
clearedByInfo.nickname(),
clearedByInfo.profileImageUrl(),
clearedByInfo.role()
);

// 해당 방의 모든 구독자에게 브로드캐스트
messagingTemplate.convertAndSend("/topic/room/" + roomId + "/chat-cleared", notification);

log.info("WebSocket 채팅 일괄 삭제 완료 - roomId: {}, deletedCount: {}, userId: {}",
roomId, deletedCount, currentUserId);

} catch (CustomException e) {
log.warn("채팅 삭제 실패 - roomId: {}, error: {}", roomId, e.getMessage());
errorHelper.sendCustomExceptionToUser(headerAccessor.getSessionId(), e);

} catch (Exception e) {
log.error("채팅 일괄 삭제 중 예상치 못한 오류 발생 - roomId: {}", roomId, e);
errorHelper.sendGenericErrorToUser(headerAccessor.getSessionId(), e, "채팅 삭제 중 오류가 발생했습니다");
}
// 응답 DTO 생성 및 브로드캐스트
RoomChatMessageResponse responseMessage = RoomChatMessageResponse.from(savedMessage);
messagingTemplate.convertAndSend("/topic/room/" + roomId, responseMessage);
}

// 채팅 메시지 처리 중 발생하는 예외 중앙 처리
@MessageExceptionHandler(CustomException.class)
public void handleChatException(CustomException e, SimpMessageHeaderAccessor headerAccessor) {
log.warn("채팅 메시지 처리 실패 - SessionId: {}, Error: {}", headerAccessor.getSessionId(), e.getMessage());
errorHelper.sendCustomExceptionToUser(headerAccessor.getSessionId(), e);
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.back.domain.chat.room.dto;

public record RoomChatMessageRequest(
String content
) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.back.domain.chat.room.dto;

import com.back.domain.studyroom.entity.RoomChatMessage;

import java.time.LocalDateTime;

public record RoomChatMessageResponse(
Long messageId,
Long roomId,
Long userId,
String nickname,
String profileImageUrl,
String content,
LocalDateTime createdAt
) {
public static RoomChatMessageResponse from(RoomChatMessage entity) {
return new RoomChatMessageResponse(
entity.getId(),
entity.getRoom().getId(),
entity.getUser().getId(),
entity.getUser().getNickname(),
entity.getUser().getProfileImageUrl(),
entity.getContent(),
entity.getCreatedAt()
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import java.util.List;

public record RoomChatPageResponse(
List<RoomChatMessageDto> content,
List<RoomChatMessageResponse> content,
PageableDto pageable,
long totalElements
) {
Expand All @@ -18,7 +18,7 @@ public record PageableDto(
// 페이지 응답 생성
public static RoomChatPageResponse from(
org.springframework.data.domain.Page<?> page,
List<RoomChatMessageDto> convertedContent) {
List<RoomChatMessageResponse> convertedContent) {

return new RoomChatPageResponse(
convertedContent,
Expand All @@ -41,7 +41,7 @@ public static RoomChatPageResponse empty(int page, int size) {
}

// 단일 페이지 응답 생성 (테스트용)
public static RoomChatPageResponse of(List<RoomChatMessageDto> content,
public static RoomChatPageResponse of(List<RoomChatMessageResponse> content,
int page,
int size,
boolean hasNext,
Expand Down
Loading