diff --git a/src/main/java/com/back/domain/chat/dm/controller/PrivateChatWebSocketController.java b/src/main/java/com/back/domain/chat/dm/controller/PrivateChatWebSocketController.java new file mode 100644 index 00000000..df61904a --- /dev/null +++ b/src/main/java/com/back/domain/chat/dm/controller/PrivateChatWebSocketController.java @@ -0,0 +1,4 @@ +package com.back.domain.chat.dm.controller; + +public class PrivateChatWebSocketController { +} diff --git a/src/main/java/com/back/domain/chat/dto/ChatMessageDto.java b/src/main/java/com/back/domain/chat/dto/ChatMessageDto.java deleted file mode 100644 index a8e6de43..00000000 --- a/src/main/java/com/back/domain/chat/dto/ChatMessageDto.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.back.domain.chat.dto; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.time.LocalDateTime; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class ChatMessageDto { - - // WebSocket Request - private String content; - private String messageType; - private Long attachmentId; - - // WebSocket Response - private Long messageId; - private Long roomId; - private Long userId; - private String nickname; - private String profileImageUrl; - private AttachmentDto attachment; - private LocalDateTime createdAt; - - // 첨부파일 DTO (나중에 파일 기능 구현 시 사용) - @Data - @Builder - @NoArgsConstructor - @AllArgsConstructor - public static class AttachmentDto { - private Long id; - private String originalName; - private String url; - private Long size; - private String mimeType; - } - - // 텍스트 채팅 요청 생성 헬퍼 - public static ChatMessageDto createRequest(String content, String messageType) { - return ChatMessageDto.builder() - .content(content) - .messageType(messageType) - .attachmentId(null) // 텍스트 채팅에서는 null - .build(); - } -} \ No newline at end of file diff --git a/src/main/java/com/back/domain/chat/dto/ChatPageResponse.java b/src/main/java/com/back/domain/chat/dto/ChatPageResponse.java deleted file mode 100644 index 09285ac3..00000000 --- a/src/main/java/com/back/domain/chat/dto/ChatPageResponse.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.back.domain.chat.dto; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.util.List; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class ChatPageResponse { - - private List content; - private PageableDto pageable; - private long totalElements; - - // 페이징 정보 DTO - @Data - @Builder - @NoArgsConstructor - @AllArgsConstructor - public static class PageableDto { - private int page; - private int size; - private boolean hasNext; - } - - // Page -> ChatPageResponse 변환 헬퍼 - public static ChatPageResponse from(org.springframework.data.domain.Page page) { - return ChatPageResponse.builder() - .content(page.getContent()) - .pageable(PageableDto.builder() - .page(page.getNumber()) - .size(page.getSize()) - .hasNext(page.hasNext()) - .build()) - .totalElements(page.getTotalElements()) - .build(); - } -} \ No newline at end of file diff --git a/src/main/java/com/back/domain/chat/controller/ChatApiController.java b/src/main/java/com/back/domain/chat/room/controller/RoomChatApiController.java similarity index 63% rename from src/main/java/com/back/domain/chat/controller/ChatApiController.java rename to src/main/java/com/back/domain/chat/room/controller/RoomChatApiController.java index eff80066..ab00178f 100644 --- a/src/main/java/com/back/domain/chat/controller/ChatApiController.java +++ b/src/main/java/com/back/domain/chat/room/controller/RoomChatApiController.java @@ -1,7 +1,7 @@ -package com.back.domain.chat.controller; +package com.back.domain.chat.room.controller; -import com.back.domain.chat.dto.ChatPageResponse; -import com.back.domain.chat.service.ChatService; +import com.back.domain.chat.room.dto.RoomChatPageResponse; +import com.back.domain.chat.room.service.RoomChatService; import com.back.global.common.dto.RsData; import com.back.global.security.CustomUserDetails; import io.swagger.v3.oas.annotations.Operation; @@ -18,22 +18,22 @@ @RestController @RequiredArgsConstructor @RequestMapping("/api") -@Tag(name = "Chat API", description = "채팅 메시지 조회 관련 API") -public class ChatApiController { +@Tag(name = "RoomChat API", description = "스터디룸 채팅 메시지 조회 관련 API") +public class RoomChatApiController { - private final ChatService chatService; + private final RoomChatService roomChatService; // 방 채팅 메시지 조회 (페이징, 특정 시간 이전 메시지) @GetMapping("/rooms/{roomId}/messages") - @Operation(summary = "채팅방 메시지 목록 조회", description = "특정 채팅방의 이전 메시지 기록을 페이징하여 조회합니다.") - public ResponseEntity> getRoomChatMessages( + @Operation(summary = "스터디룸 채팅방 메시지 목록 조회", description = "특정 채팅방의 이전 메시지 기록을 페이징하여 조회합니다.") + public ResponseEntity> getRoomChatMessages( @PathVariable Long roomId, @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "20") int size, @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime before, @AuthenticationPrincipal CustomUserDetails userDetails) { - ChatPageResponse chatHistory = chatService.getRoomChatHistory(roomId, page, size, before); + RoomChatPageResponse chatHistory = roomChatService.getRoomChatHistory(roomId, page, size, before); return ResponseEntity .status(HttpStatus.OK) diff --git a/src/main/java/com/back/domain/chat/controller/ChatWebSocketController.java b/src/main/java/com/back/domain/chat/room/controller/RoomChatWebSocketController.java similarity index 72% rename from src/main/java/com/back/domain/chat/controller/ChatWebSocketController.java rename to src/main/java/com/back/domain/chat/room/controller/RoomChatWebSocketController.java index 29e4bc8b..bc2bfa5b 100644 --- a/src/main/java/com/back/domain/chat/controller/ChatWebSocketController.java +++ b/src/main/java/com/back/domain/chat/room/controller/RoomChatWebSocketController.java @@ -1,10 +1,10 @@ -package com.back.domain.chat.controller; +package com.back.domain.chat.room.controller; import com.back.domain.studyroom.entity.RoomChatMessage; -import com.back.domain.chat.dto.ChatMessageDto; +import com.back.domain.chat.room.dto.RoomChatMessageDto; import com.back.global.security.CustomUserDetails; import com.back.global.websocket.dto.WebSocketErrorResponse; -import com.back.domain.chat.service.ChatService; +import com.back.domain.chat.room.service.RoomChatService; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.messaging.handler.annotation.DestinationVariable; @@ -18,10 +18,10 @@ @Controller @RequiredArgsConstructor -@Tag(name = "Chat WebSocket", description = "STOMP를 이용한 실시간 채팅 WebSocket 컨트롤러 (Swagger에서 직접 테스트 불가)") -public class ChatWebSocketController { +@Tag(name = "RoomChat WebSocket", description = "STOMP를 이용한 실시간 채팅 WebSocket 컨트롤러 (Swagger에서 직접 테스트 불가)") +public class RoomChatWebSocketController { - private final ChatService chatService; + private final RoomChatService roomChatService; private final SimpMessagingTemplate messagingTemplate; /** @@ -35,7 +35,7 @@ public class ChatWebSocketController { */ @MessageMapping("/chat/room/{roomId}") public void handleRoomChat(@DestinationVariable Long roomId, - ChatMessageDto chatMessage, + RoomChatMessageDto chatMessage, SimpMessageHeaderAccessor headerAccessor, Principal principal) { @@ -51,25 +51,26 @@ public void handleRoomChat(@DestinationVariable Long roomId, String currentUserNickname = userDetails.getUsername(); // 메시지 정보 보완 - chatMessage.setRoomId(roomId); - chatMessage.setUserId(currentUserId); - chatMessage.setNickname(currentUserNickname); + RoomChatMessageDto enrichedMessage = chatMessage + .withRoomId(roomId) + .withUserId(currentUserId) + .withNickname(currentUserNickname); // DB에 메시지 저장 - RoomChatMessage savedMessage = chatService.saveRoomChatMessage(chatMessage); + RoomChatMessage savedMessage = roomChatService.saveRoomChatMessage(enrichedMessage); // 저장된 메시지 정보로 응답 DTO 생성 - ChatMessageDto responseMessage = ChatMessageDto.builder() - .messageId(savedMessage.getId()) - .roomId(roomId) - .userId(savedMessage.getUser().getId()) - .nickname(savedMessage.getUser().getNickname()) - .profileImageUrl(savedMessage.getUser().getProfileImageUrl()) - .content(savedMessage.getContent()) - .messageType(chatMessage.getMessageType()) - .attachment(null) // 텍스트 채팅에서는 null - .createdAt(savedMessage.getCreatedAt()) - .build(); + 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); diff --git a/src/main/java/com/back/domain/chat/room/dto/RoomChatMessageDto.java b/src/main/java/com/back/domain/chat/room/dto/RoomChatMessageDto.java new file mode 100644 index 00000000..42b4fb17 --- /dev/null +++ b/src/main/java/com/back/domain/chat/room/dto/RoomChatMessageDto.java @@ -0,0 +1,69 @@ +package com.back.domain.chat.room.dto; + +import java.time.LocalDateTime; + +public record RoomChatMessageDto( + // WebSocket Request + String content, + String messageType, + Long attachmentId, + + // WebSocket Response + Long messageId, + Long roomId, + Long userId, + String nickname, + String profileImageUrl, + AttachmentDto attachment, + LocalDateTime createdAt +) { + + // 첨부파일 DTO (나중에 파일 기능 구현 시 사용) + public record AttachmentDto( + Long id, + String originalName, + String url, + Long size, + String mimeType + ) {} + + // 텍스트 채팅 요청 생성 헬퍼 + public static RoomChatMessageDto createRequest(String content, String messageType) { + return new RoomChatMessageDto( + content, + messageType, + null, // attachmentId - 텍스트 채팅에서는 null + null, // messageId + null, // roomId + null, // userId + null, // nickname + null, // profileImageUrl + null, // attachment + null // createdAt + ); + } + + // 필드 업데이트 + public RoomChatMessageDto withRoomId(Long roomId) { + return new RoomChatMessageDto(content, messageType, attachmentId, messageId, roomId, userId, nickname, profileImageUrl, attachment, createdAt); + } + + public RoomChatMessageDto withUserId(Long userId) { + return new RoomChatMessageDto(content, messageType, attachmentId, messageId, roomId, userId, nickname, profileImageUrl, attachment, createdAt); + } + + public RoomChatMessageDto withNickname(String nickname) { + return new RoomChatMessageDto(content, messageType, attachmentId, messageId, roomId, userId, nickname, profileImageUrl, attachment, createdAt); + } + + // Response용 생성자 + public static RoomChatMessageDto createResponse( + Long messageId, Long roomId, Long userId, String nickname, + String profileImageUrl, String content, String messageType, + AttachmentDto attachment, LocalDateTime createdAt) { + return new RoomChatMessageDto( + content, messageType, null, // attachmentId는 request용이므로 null + messageId, roomId, userId, nickname, profileImageUrl, attachment, createdAt + ); + } +} diff --git a/src/main/java/com/back/domain/chat/room/dto/RoomChatPageResponse.java b/src/main/java/com/back/domain/chat/room/dto/RoomChatPageResponse.java new file mode 100644 index 00000000..2489b721 --- /dev/null +++ b/src/main/java/com/back/domain/chat/room/dto/RoomChatPageResponse.java @@ -0,0 +1,55 @@ +package com.back.domain.chat.room.dto; + +import java.util.List; + +public record RoomChatPageResponse( + List content, + PageableDto pageable, + long totalElements +) { + + // 페이지 정보 DTO + public record PageableDto( + int page, + int size, + boolean hasNext + ) {} + + // 페이지 응답 생성 + public static RoomChatPageResponse from( + org.springframework.data.domain.Page page, + List convertedContent) { + + return new RoomChatPageResponse( + convertedContent, + new PageableDto( + page.getNumber(), + page.getSize(), + page.hasNext() + ), + page.getTotalElements() + ); + } + + // 빈 페이지 응답 생성 + public static RoomChatPageResponse empty(int page, int size) { + return new RoomChatPageResponse( + List.of(), + new PageableDto(page, size, false), + 0L + ); + } + + // 단일 페이지 응답 생성 (테스트용) + public static RoomChatPageResponse of(List content, + int page, + int size, + boolean hasNext, + long totalElements) { + return new RoomChatPageResponse( + content, + new PageableDto(page, size, hasNext), + totalElements + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/back/domain/chat/service/ChatService.java b/src/main/java/com/back/domain/chat/room/service/RoomChatService.java similarity index 67% rename from src/main/java/com/back/domain/chat/service/ChatService.java rename to src/main/java/com/back/domain/chat/room/service/RoomChatService.java index 7ca68100..f994c79e 100644 --- a/src/main/java/com/back/domain/chat/service/ChatService.java +++ b/src/main/java/com/back/domain/chat/room/service/RoomChatService.java @@ -1,4 +1,4 @@ -package com.back.domain.chat.service; +package com.back.domain.chat.room.service; import com.back.domain.studyroom.entity.Room; import com.back.domain.studyroom.entity.RoomChatMessage; @@ -6,8 +6,8 @@ import com.back.domain.studyroom.repository.RoomRepository; import com.back.domain.user.entity.User; import com.back.domain.user.repository.UserRepository; -import com.back.domain.chat.dto.ChatMessageDto; -import com.back.domain.chat.dto.ChatPageResponse; +import com.back.domain.chat.room.dto.RoomChatMessageDto; +import com.back.domain.chat.room.dto.RoomChatPageResponse; import com.back.global.exception.CustomException; import com.back.global.exception.ErrorCode; import com.back.global.security.CurrentUser; @@ -19,11 +19,12 @@ import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; +import java.util.List; @Service @RequiredArgsConstructor @Transactional(readOnly = true) -public class ChatService { +public class RoomChatService { private final RoomChatMessageRepository roomChatMessageRepository; private final RoomRepository roomRepository; @@ -36,25 +37,25 @@ public class ChatService { // 방 채팅 메시지 저장 @Transactional - public RoomChatMessage saveRoomChatMessage(ChatMessageDto chatMessageDto) { + public RoomChatMessage saveRoomChatMessage(RoomChatMessageDto roomChatMessageDto) { // 방 존재 여부 확인 - Room room = roomRepository.findById(chatMessageDto.getRoomId()) + Room room = roomRepository.findById(roomChatMessageDto.roomId()) .orElseThrow(() -> new CustomException(ErrorCode.ROOM_NOT_FOUND)); // 사용자 존재 여부 확인 - User user = userRepository.findById(chatMessageDto.getUserId()) + User user = userRepository.findById(roomChatMessageDto.userId()) .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); // RoomChatMessage 엔티티 생성 및 저장 - RoomChatMessage message = new RoomChatMessage(room, user, chatMessageDto.getContent()); + RoomChatMessage message = new RoomChatMessage(room, user, roomChatMessageDto.content()); RoomChatMessage savedMessage = roomChatMessageRepository.save(message); return savedMessage; } // 방 채팅 기록 조회 - public ChatPageResponse getRoomChatHistory(Long roomId, int page, int size, LocalDateTime before) { + public RoomChatPageResponse getRoomChatHistory(Long roomId, int page, int size, LocalDateTime before) { // 방 존재 여부 확인 roomRepository.findById(roomId) @@ -73,9 +74,12 @@ public ChatPageResponse getRoomChatHistory(Long roomId, int page, int size, Loca messagesPage = roomChatMessageRepository.findMessagesByRoomId(roomId, pageable); } - Page dtoPage = messagesPage.map(this::convertToDto); + List convertedContent = messagesPage.getContent() + .stream() + .map(this::convertToDto) + .toList(); - return ChatPageResponse.from(dtoPage); + return RoomChatPageResponse.from(messagesPage, convertedContent); } // size 값 검증 및 최대값 제한 @@ -87,18 +91,18 @@ private int validateAndLimitPageSize(int size) { } // 메시지 엔티티를 DTO로 변환 - private ChatMessageDto convertToDto(RoomChatMessage message) { - return ChatMessageDto.builder() - .messageId(message.getId()) - .roomId(message.getRoom().getId()) - .userId(message.getUser().getId()) - .nickname(message.getUser().getNickname()) - .profileImageUrl(message.getUser().getProfileImageUrl()) - .content(message.getContent()) - .messageType("TEXT") // 현재는 텍스트만 지원 - .attachment(null) // 텍스트 채팅에서는 null - .createdAt(message.getCreatedAt()) - .build(); + private RoomChatMessageDto convertToDto(RoomChatMessage message) { + return RoomChatMessageDto.createResponse( + message.getId(), + message.getRoom().getId(), + message.getUser().getId(), + message.getUser().getNickname(), + message.getUser().getProfileImageUrl(), + message.getContent(), + "TEXT", // 현재는 텍스트만 지원 + null, // 텍스트 채팅에서는 null + message.getCreatedAt() + ); } } \ No newline at end of file diff --git a/src/main/java/com/back/domain/studyroom/entity/RoomChatMessage.java b/src/main/java/com/back/domain/studyroom/entity/RoomChatMessage.java index f4f0ea21..7e12d667 100644 --- a/src/main/java/com/back/domain/studyroom/entity/RoomChatMessage.java +++ b/src/main/java/com/back/domain/studyroom/entity/RoomChatMessage.java @@ -6,8 +6,6 @@ import jakarta.persistence.FetchType; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; -import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.experimental.SuperBuilder; @@ -16,7 +14,6 @@ @Getter @SuperBuilder @NoArgsConstructor -@AllArgsConstructor public class RoomChatMessage extends BaseEntity { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "room_id") @@ -28,4 +25,9 @@ public class RoomChatMessage extends BaseEntity { private String content; + public RoomChatMessage(Room room, User user, String content) { + this.room = room; + this.user = user; + this.content = content; + } } diff --git a/src/main/java/com/back/global/security/SecurityConfig.java b/src/main/java/com/back/global/security/SecurityConfig.java index 0ea336d4..0a20b6c8 100644 --- a/src/main/java/com/back/global/security/SecurityConfig.java +++ b/src/main/java/com/back/global/security/SecurityConfig.java @@ -29,6 +29,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .authorizeHttpRequests( auth -> auth .requestMatchers("/api/auth/**").permitAll() + .requestMatchers("/api/ws/**").permitAll() .requestMatchers("/api/rooms/**").permitAll() // 테스트용 임시 허용 .requestMatchers("/","/swagger-ui/**", "/v3/api-docs/**").permitAll() // Swagger 허용 .requestMatchers("/h2-console/**").permitAll() // H2 Console 허용 diff --git a/src/main/java/com/back/global/websocket/controller/WebSocketApiController.java b/src/main/java/com/back/global/websocket/controller/WebSocketApiController.java index 32b479fa..bde4453a 100644 --- a/src/main/java/com/back/global/websocket/controller/WebSocketApiController.java +++ b/src/main/java/com/back/global/websocket/controller/WebSocketApiController.java @@ -16,7 +16,7 @@ @RestController @RequestMapping("api/ws") @RequiredArgsConstructor -@Tag(name = "WebSocket REST API", description = "WebSocket 서버 상태 확인 및 실시간 연결 정보 제공 API") +@Tag(name = "WebSocket REST API", description = "WebSocket 서버 상태 확인 + 실시간 연결 정보 제공 API") public class WebSocketApiController { private final WebSocketSessionManager sessionManager; @@ -55,7 +55,6 @@ public ResponseEntity>> getConnectionInfo() { connectionInfo.put("stompVersion", "1.2"); connectionInfo.put("heartbeatInterval", "5분"); connectionInfo.put("sessionTTL", "10분"); - connectionInfo.put("description", "RoomController와 협력하여 실시간 온라인 상태 관리"); connectionInfo.put("subscribeTopics", Map.of( "roomChat", "/topic/rooms/{roomId}/chat", "privateMessage", "/user/queue/messages", diff --git a/src/main/java/com/back/global/websocket/controller/WebSocketMessageController.java b/src/main/java/com/back/global/websocket/controller/WebSocketMessageController.java index ac43d7c7..f84b78a5 100644 --- a/src/main/java/com/back/global/websocket/controller/WebSocketMessageController.java +++ b/src/main/java/com/back/global/websocket/controller/WebSocketMessageController.java @@ -21,10 +21,10 @@ public class WebSocketMessageController { @MessageMapping("/heartbeat") public void handleHeartbeat(@Payload HeartbeatMessage message) { try { - if (message.getUserId() != null) { + if (message.userId() != null) { // TTL 10분으로 연장 - sessionManager.updateLastActivity(message.getUserId()); - log.debug("Heartbeat 처리 완료 - 사용자: {}", message.getUserId()); + sessionManager.updateLastActivity(message.userId()); + log.debug("Heartbeat 처리 완료 - 사용자: {}", message.userId()); } else { log.warn("유효하지 않은 Heartbeat 메시지 수신: userId가 null"); } @@ -40,9 +40,9 @@ public void handleHeartbeat(@Payload HeartbeatMessage message) { @MessageMapping("/rooms/{roomId}/join") public void handleJoinRoom(@DestinationVariable Long roomId, @Payload HeartbeatMessage message) { try { - if (message.getUserId() != null) { - sessionManager.joinRoom(message.getUserId(), roomId); - log.info("STOMP 방 입장 처리 완료 - 사용자: {}, 방: {}", message.getUserId(), roomId); + if (message.userId() != null) { + sessionManager.joinRoom(message.userId(), roomId); + log.info("STOMP 방 입장 처리 완료 - 사용자: {}, 방: {}", message.userId(), roomId); } else { log.warn("유효하지 않은 방 입장 요청: userId가 null"); } @@ -57,9 +57,9 @@ public void handleJoinRoom(@DestinationVariable Long roomId, @Payload HeartbeatM @MessageMapping("/rooms/{roomId}/leave") public void handleLeaveRoom(@DestinationVariable Long roomId, @Payload HeartbeatMessage message) { try { - if (message.getUserId() != null) { - sessionManager.leaveRoom(message.getUserId(), roomId); - log.info("STOMP 방 퇴장 처리 완료 - 사용자: {}, 방: {}", message.getUserId(), roomId); + if (message.userId() != null) { + sessionManager.leaveRoom(message.userId(), roomId); + log.info("STOMP 방 퇴장 처리 완료 - 사용자: {}, 방: {}", message.userId(), roomId); } else { log.warn("유효하지 않은 방 퇴장 요청: userId가 null"); } @@ -74,9 +74,9 @@ public void handleLeaveRoom(@DestinationVariable Long roomId, @Payload Heartbeat @MessageMapping("/activity") public void handleActivity(@Payload HeartbeatMessage message) { try { - if (message.getUserId() != null) { - sessionManager.updateLastActivity(message.getUserId()); - log.debug("사용자 활동 신호 처리 완료 - 사용자: {}", message.getUserId()); + if (message.userId() != null) { + sessionManager.updateLastActivity(message.userId()); + log.debug("사용자 활동 신호 처리 완료 - 사용자: {}", message.userId()); } } catch (CustomException e) { log.error("활동 신호 처리 실패: {}", e.getMessage()); diff --git a/src/main/java/com/back/global/websocket/dto/HeartbeatMessage.java b/src/main/java/com/back/global/websocket/dto/HeartbeatMessage.java index f5e99fbb..b490a9c6 100644 --- a/src/main/java/com/back/global/websocket/dto/HeartbeatMessage.java +++ b/src/main/java/com/back/global/websocket/dto/HeartbeatMessage.java @@ -1,12 +1,5 @@ package com.back.global.websocket.dto; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -@AllArgsConstructor -public class HeartbeatMessage { - private Long userId; -} +public record HeartbeatMessage( + Long userId +) {} diff --git a/src/main/java/com/back/global/websocket/dto/WebSocketErrorResponse.java b/src/main/java/com/back/global/websocket/dto/WebSocketErrorResponse.java index 581186ab..2878cc58 100644 --- a/src/main/java/com/back/global/websocket/dto/WebSocketErrorResponse.java +++ b/src/main/java/com/back/global/websocket/dto/WebSocketErrorResponse.java @@ -1,40 +1,24 @@ package com.back.global.websocket.dto; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - import java.time.LocalDateTime; -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class WebSocketErrorResponse { - - private String type = "ERROR"; - private ErrorDto error; - private LocalDateTime timestamp; - - @Data - @Builder - @NoArgsConstructor - @AllArgsConstructor - public static class ErrorDto { - private String code; - private String message; - } +public record WebSocketErrorResponse( + String type, + ErrorDto error, + LocalDateTime timestamp +) { + + public record ErrorDto( + String code, + String message + ) {} // 에러 응답 생성 헬퍼 public static WebSocketErrorResponse create(String code, String message) { - return WebSocketErrorResponse.builder() - .type("ERROR") - .error(ErrorDto.builder() - .code(code) - .message(message) - .build()) - .timestamp(LocalDateTime.now()) - .build(); + return new WebSocketErrorResponse( + "ERROR", + new ErrorDto(code, message), + LocalDateTime.now() + ); } -} \ No newline at end of file +} diff --git a/src/main/java/com/back/global/websocket/dto/WebSocketSessionInfo.java b/src/main/java/com/back/global/websocket/dto/WebSocketSessionInfo.java index b77fa369..fded1959 100644 --- a/src/main/java/com/back/global/websocket/dto/WebSocketSessionInfo.java +++ b/src/main/java/com/back/global/websocket/dto/WebSocketSessionInfo.java @@ -1,20 +1,67 @@ package com.back.global.websocket.dto; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - import java.time.LocalDateTime; -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class WebSocketSessionInfo { - private Long userId; - private String sessionId; - private LocalDateTime connectedAt; - private LocalDateTime lastActiveAt; - private Long currentRoomId; // 현재 참여 중인 방 ID +public record WebSocketSessionInfo( + Long userId, + String sessionId, + LocalDateTime connectedAt, + LocalDateTime lastActiveAt, + Long currentRoomId +) { + + // 새로운 세션 생성 + public static WebSocketSessionInfo createNewSession(Long userId, String sessionId) { + LocalDateTime now = LocalDateTime.now(); + return new WebSocketSessionInfo( + userId, + sessionId, + now, // connectedAt + now, // lastActiveAt + null // currentRoomId + ); + } + + // 활동 시간 업데이트 + public WebSocketSessionInfo withUpdatedActivity() { + return new WebSocketSessionInfo( + userId, + sessionId, + connectedAt, + LocalDateTime.now(), + currentRoomId + ); + } + + // 방 입장 시 정보 업데이트 + public WebSocketSessionInfo withRoomId(Long newRoomId) { + return new WebSocketSessionInfo( + userId, + sessionId, + connectedAt, + LocalDateTime.now(), + newRoomId + ); + } + + // 방 퇴장 시 정보 업데이트 + public WebSocketSessionInfo withoutRoom() { + return new WebSocketSessionInfo( + userId, + sessionId, + connectedAt, + LocalDateTime.now(), + null + ); + } + + // 특정 방에 입장해 있는지 확인 + public boolean isInRoom(Long roomId) { + return currentRoomId != null && currentRoomId.equals(roomId); + } + + // 어떤 방에도 입장해 있는지 확인 + public boolean isInAnyRoom() { + return currentRoomId != null; + } } \ No newline at end of file diff --git a/src/main/java/com/back/global/websocket/dto/WebSocketStatusResponse.java b/src/main/java/com/back/global/websocket/dto/WebSocketStatusResponse.java index c32f409c..1377d57d 100644 --- a/src/main/java/com/back/global/websocket/dto/WebSocketStatusResponse.java +++ b/src/main/java/com/back/global/websocket/dto/WebSocketStatusResponse.java @@ -1,20 +1,22 @@ package com.back.global.websocket.dto; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - import java.time.LocalDateTime; -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class WebSocketStatusResponse { - private boolean isConnected; - private LocalDateTime connectedAt; - private String sessionId; - private Long currentRoomId; - private LocalDateTime lastActiveAt; +public record WebSocketStatusResponse( + boolean isConnected, + LocalDateTime connectedAt, + String sessionId, + Long currentRoomId, + LocalDateTime lastActiveAt +) { + + // 연결된 상태 응답 생성 + public static WebSocketStatusResponse connected(String sessionId, Long currentRoomId, LocalDateTime connectedAt, LocalDateTime lastActiveAt) { + return new WebSocketStatusResponse(true, connectedAt, sessionId, currentRoomId, lastActiveAt); + } + + // 연결 끊긴 상태 응답 생성 + public static WebSocketStatusResponse disconnected() { + return new WebSocketStatusResponse(false, null, null, null, null); + } } diff --git a/src/main/java/com/back/global/websocket/service/WebSocketSessionManager.java b/src/main/java/com/back/global/websocket/service/WebSocketSessionManager.java index c4abbba7..18f48476 100644 --- a/src/main/java/com/back/global/websocket/service/WebSocketSessionManager.java +++ b/src/main/java/com/back/global/websocket/service/WebSocketSessionManager.java @@ -9,7 +9,6 @@ import org.springframework.stereotype.Service; import java.time.Duration; -import java.time.LocalDateTime; import java.util.Set; import java.util.stream.Collectors; @@ -31,12 +30,7 @@ public class WebSocketSessionManager { // 사용자 세션 추가 (연결 시 호출) public void addSession(Long userId, String sessionId) { try { - WebSocketSessionInfo sessionInfo = WebSocketSessionInfo.builder() - .userId(userId) - .sessionId(sessionId) - .connectedAt(LocalDateTime.now()) - .lastActiveAt(LocalDateTime.now()) - .build(); + WebSocketSessionInfo sessionInfo = WebSocketSessionInfo.createNewSession(userId, sessionId); String userKey = USER_SESSION_KEY.replace("{}", userId.toString()); String sessionKey = SESSION_USER_KEY.replace("{}", sessionId); @@ -44,7 +38,7 @@ public void addSession(Long userId, String sessionId) { // 기존 세션이 있다면 제거 (중복 연결 방지) WebSocketSessionInfo existingSession = getSessionInfo(userId); if (existingSession != null) { - removeSessionInternal(existingSession.getSessionId()); + removeSessionInternal(existingSession.sessionId()); log.info("기존 세션 제거 후 새 세션 등록 - 사용자: {}", userId); } @@ -72,7 +66,7 @@ public boolean isUserConnected(Long userId) { } } - // 사용자 세션 정보 조회 + // 사용자 세션 정보 조회 public WebSocketSessionInfo getSessionInfo(Long userId) { try { String userKey = USER_SESSION_KEY.replace("{}", userId.toString()); @@ -100,11 +94,12 @@ public void updateLastActivity(Long userId) { WebSocketSessionInfo sessionInfo = getSessionInfo(userId); if (sessionInfo != null) { // 마지막 활동 시간 업데이트 - sessionInfo.setLastActiveAt(LocalDateTime.now()); + WebSocketSessionInfo updatedSessionInfo = sessionInfo.withUpdatedActivity(); String userKey = USER_SESSION_KEY.replace("{}", userId.toString()); - // TTL 10분으로 연장 (Heartbeat의 핵심!) - redisTemplate.opsForValue().set(userKey, sessionInfo, Duration.ofMinutes(SESSION_TTL_MINUTES)); + + // TTL 10분으로 연장 + redisTemplate.opsForValue().set(userKey, updatedSessionInfo, Duration.ofMinutes(SESSION_TTL_MINUTES)); log.debug("사용자 활동 시간 업데이트 완료 - 사용자: {}, TTL 연장", userId); } else { @@ -125,16 +120,15 @@ public void joinRoom(Long userId, Long roomId) { WebSocketSessionInfo sessionInfo = getSessionInfo(userId); if (sessionInfo != null) { // 기존 방에서 퇴장 - if (sessionInfo.getCurrentRoomId() != null) { - leaveRoom(userId, sessionInfo.getCurrentRoomId()); + if (sessionInfo.currentRoomId() != null) { + leaveRoom(userId, sessionInfo.currentRoomId()); } // 새 방 정보 업데이트 - sessionInfo.setCurrentRoomId(roomId); - sessionInfo.setLastActiveAt(LocalDateTime.now()); + WebSocketSessionInfo updatedSessionInfo = sessionInfo.withRoomId(roomId); String userKey = USER_SESSION_KEY.replace("{}", userId.toString()); - redisTemplate.opsForValue().set(userKey, sessionInfo, Duration.ofMinutes(SESSION_TTL_MINUTES)); + redisTemplate.opsForValue().set(userKey, updatedSessionInfo, Duration.ofMinutes(SESSION_TTL_MINUTES)); // 방 참여자 목록에 추가 String roomUsersKey = ROOM_USERS_KEY.replace("{}", roomId.toString()); @@ -158,11 +152,11 @@ public void leaveRoom(Long userId, Long roomId) { try { WebSocketSessionInfo sessionInfo = getSessionInfo(userId); if (sessionInfo != null) { - sessionInfo.setCurrentRoomId(null); - sessionInfo.setLastActiveAt(LocalDateTime.now()); + // 방 정보 제거 + WebSocketSessionInfo updatedSessionInfo = sessionInfo.withoutRoom(); String userKey = USER_SESSION_KEY.replace("{}", userId.toString()); - redisTemplate.opsForValue().set(userKey, sessionInfo, Duration.ofMinutes(SESSION_TTL_MINUTES)); + redisTemplate.opsForValue().set(userKey, updatedSessionInfo, Duration.ofMinutes(SESSION_TTL_MINUTES)); // 방 참여자 목록에서 제거 String roomUsersKey = ROOM_USERS_KEY.replace("{}", roomId.toString()); @@ -223,14 +217,14 @@ public long getTotalOnlineUserCount() { public Long getUserCurrentRoomId(Long userId) { try { WebSocketSessionInfo sessionInfo = getSessionInfo(userId); - return sessionInfo != null ? sessionInfo.getCurrentRoomId() : null; + return sessionInfo != null ? sessionInfo.currentRoomId() : null; } catch (CustomException e) { log.error("사용자 현재 방 조회 실패 - 사용자: {}", userId, e); return null; // 조회용이므로 예외 대신 null 반환 } } - // 내부적으로 세션 제거 처리 + // 내부적으로 세션 제거 처리 private void removeSessionInternal(String sessionId) { String sessionKey = SESSION_USER_KEY.replace("{}", sessionId); Long userId = (Long) redisTemplate.opsForValue().get(sessionKey); @@ -239,8 +233,8 @@ private void removeSessionInternal(String sessionId) { WebSocketSessionInfo sessionInfo = getSessionInfo(userId); // 방에서 퇴장 처리 - if (sessionInfo != null && sessionInfo.getCurrentRoomId() != null) { - leaveRoom(userId, sessionInfo.getCurrentRoomId()); + if (sessionInfo != null && sessionInfo.currentRoomId() != null) { + leaveRoom(userId, sessionInfo.currentRoomId()); } // 세션 데이터 삭제 @@ -249,4 +243,4 @@ private void removeSessionInternal(String sessionId) { redisTemplate.delete(sessionKey); } } -} +} \ No newline at end of file diff --git a/src/test/java/com/back/domain/chat/controller/ChatApiControllerTest.java b/src/test/java/com/back/domain/chat/controller/ChatApiControllerTest.java deleted file mode 100644 index c6be136e..00000000 --- a/src/test/java/com/back/domain/chat/controller/ChatApiControllerTest.java +++ /dev/null @@ -1,73 +0,0 @@ -package com.back.domain.chat.controller; - -import com.back.domain.chat.dto.ChatPageResponse; -import com.back.domain.chat.service.ChatService; -import com.back.global.security.JwtTokenProvider; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; - -import org.springframework.http.MediaType; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.test.context.bean.override.mockito.MockitoBean; -import org.springframework.test.web.servlet.MockMvc; - -import static org.mockito.ArgumentMatchers.*; -import static org.mockito.BDDMockito.given; - -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -import java.util.List; - -@SpringBootTest -@AutoConfigureMockMvc -class ChatApiControllerTest { - - @Autowired - private MockMvc mockMvc; - - @MockitoBean - private JwtTokenProvider jwtTokenProvider; - - @MockitoBean - private ChatService chatService; - - @Test - @DisplayName("채팅 기록 조회 성공") - void t1() throws Exception { - ChatPageResponse mockResponse = ChatPageResponse.builder() - .content(List.of()) - .pageable(ChatPageResponse.PageableDto.builder() - .page(0) - .size(20) - .hasNext(false) - .build()) - .totalElements(0) - .build(); - - given(chatService.getRoomChatHistory(anyLong(), anyInt(), anyInt(), any())) - .willReturn(mockResponse); - - // JWT 관련 스텁 - given(jwtTokenProvider.validateToken(anyString())).willReturn(true); - given(jwtTokenProvider.getAuthentication(anyString())) - .willReturn(new UsernamePasswordAuthenticationToken( - "mockUser", null, List.of()) - ); - - mockMvc.perform(get("/api/rooms/1/messages") - .param("page", "0") - .param("size", "20") - .header("Authorization", "Bearer faketoken") // 가짜 토큰 넣기 - .accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.data.content").isArray()) - .andExpect(jsonPath("$.data.totalElements").value(0)); - } -} - diff --git a/src/test/java/com/back/domain/chat/room/controller/RoomChatApiControllerTest.java b/src/test/java/com/back/domain/chat/room/controller/RoomChatApiControllerTest.java new file mode 100644 index 00000000..5db88825 --- /dev/null +++ b/src/test/java/com/back/domain/chat/room/controller/RoomChatApiControllerTest.java @@ -0,0 +1,150 @@ +package com.back.domain.chat.room.controller; + +import com.back.domain.chat.room.dto.RoomChatPageResponse; +import com.back.domain.chat.room.service.RoomChatService; +import com.back.global.security.CustomUserDetails; +import com.back.global.security.JwtTokenProvider; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; + +import org.springframework.http.MediaType; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.BDDMockito.given; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.List; + +@SpringBootTest +@AutoConfigureMockMvc +class RoomChatApiControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockitoBean + private JwtTokenProvider jwtTokenProvider; + + @MockitoBean + private RoomChatService roomChatService; + + @Test + @DisplayName("채팅 기록 조회 성공 - JWT 토큰 있음") + void t1() throws Exception { + RoomChatPageResponse mockResponse = RoomChatPageResponse.of( + List.of(), // content + 0, // page + 20, // size + false, // hasNext + 0L // totalElements + ); + + given(roomChatService.getRoomChatHistory(anyLong(), anyInt(), anyInt(), any())) + .willReturn(mockResponse); + + // JWT 관련 스텁 + given(jwtTokenProvider.validateToken("faketoken")).willReturn(true); + + CustomUserDetails mockUser = CustomUserDetails.builder() + .userId(1L) + .username("testuser") + .build(); + + given(jwtTokenProvider.getAuthentication("faketoken")) + .willReturn(new UsernamePasswordAuthenticationToken( + mockUser, + null, + List.of(new SimpleGrantedAuthority("ROLE_USER"))) + ); + + mockMvc.perform(get("/api/rooms/1/messages") + .param("page", "0") + .param("size", "20") + .header("Authorization", "Bearer faketoken") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.content").isArray()) + .andExpect(jsonPath("$.data.totalElements").value(0)); + } + + @Test + @DisplayName("채팅 기록 조회 성공 - 빈 페이지") + void t2() throws Exception { + RoomChatPageResponse mockResponse = RoomChatPageResponse.empty(0, 20); + + given(roomChatService.getRoomChatHistory(anyLong(), anyInt(), anyInt(), any())) + .willReturn(mockResponse); + + mockMvc.perform(get("/api/rooms/1/messages") + .param("page", "0") + .param("size", "20") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.content").isArray()) + .andExpect(jsonPath("$.data.content").isEmpty()) + .andExpect(jsonPath("$.data.totalElements").value(0)) + .andExpect(jsonPath("$.data.pageable.page").value(0)) + .andExpect(jsonPath("$.data.pageable.size").value(20)) + .andExpect(jsonPath("$.data.pageable.hasNext").value(false)); + } + + // Security 설정을 authenticated()로 변경한 후에 다시 활성화 + /* + @Test + @DisplayName("JWT 토큰 없이 요청 - 401 Unauthorized") + void t3() throws Exception { + mockMvc.perform(get("/api/rooms/1/messages") + .param("page", "0") + .param("size", "20") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isUnauthorized()); + } + + @Test + @DisplayName("잘못된 JWT 토큰으로 요청 - 401 Unauthorized") + void t4() throws Exception { + given(jwtTokenProvider.validateToken("invalidtoken")).willReturn(false); + + mockMvc.perform(get("/api/rooms/1/messages") + .param("page", "0") + .param("size", "20") + .header("Authorization", "Bearer invalidtoken") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isUnauthorized()); + } + */ + + @Test + @DisplayName("페이징 파라미터 테스트 - 큰 size 요청") + void t5() throws Exception { + // size가 큰 경우의 응답 + RoomChatPageResponse mockResponse = RoomChatPageResponse.of( + List.of(), // content + 0, // page + 100, // size (최대값으로 제한됨) + false, // hasNext + 0L // totalElements + ); + + given(roomChatService.getRoomChatHistory(anyLong(), anyInt(), anyInt(), any())) + .willReturn(mockResponse); + + mockMvc.perform(get("/api/rooms/1/messages") + .param("page", "0") + .param("size", "150") // 큰 값 요청 + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.pageable.size").value(100)); // 100으로 제한됨 + } +} \ No newline at end of file diff --git a/src/test/java/com/back/domain/chat/controller/ChatWebSocketApiControllerTest.java b/src/test/java/com/back/domain/chat/room/controller/RoomChatWebSocketControllerTest.java similarity index 66% rename from src/test/java/com/back/domain/chat/controller/ChatWebSocketApiControllerTest.java rename to src/test/java/com/back/domain/chat/room/controller/RoomChatWebSocketControllerTest.java index 7d235be9..d3d5f4f6 100644 --- a/src/test/java/com/back/domain/chat/controller/ChatWebSocketApiControllerTest.java +++ b/src/test/java/com/back/domain/chat/room/controller/RoomChatWebSocketControllerTest.java @@ -1,7 +1,7 @@ -package com.back.domain.chat.controller; +package com.back.domain.chat.room.controller; -import com.back.domain.chat.dto.ChatMessageDto; -import com.back.domain.chat.service.ChatService; +import com.back.domain.chat.room.dto.RoomChatMessageDto; +import com.back.domain.chat.room.service.RoomChatService; import com.back.domain.studyroom.entity.RoomChatMessage; import com.back.domain.user.entity.User; import com.back.domain.user.entity.UserProfile; @@ -22,18 +22,16 @@ import java.lang.reflect.Field; import java.security.Principal; import java.time.LocalDateTime; -import java.util.Arrays; -import java.util.List; import static org.mockito.ArgumentMatchers.*; import static org.mockito.BDDMockito.*; @ExtendWith(MockitoExtension.class) -@DisplayName("ChatWebSocketController 테스트") -class ChatWebSocketApiControllerTest { +@DisplayName("RoomChatWebSocketController 테스트") +class RoomChatWebSocketControllerTest { @Mock - private ChatService chatService; + private RoomChatService roomChatService; @Mock private SimpMessagingTemplate messagingTemplate; @@ -42,7 +40,7 @@ class ChatWebSocketApiControllerTest { private SimpMessageHeaderAccessor headerAccessor; @InjectMocks - private ChatWebSocketController chatWebSocketController; + private RoomChatWebSocketController roomChatWebSocketController; private CustomUserDetails testUser; private Principal testPrincipal; @@ -115,34 +113,35 @@ private void setField(Object target, String fieldName, Object value) throws Exce void t1() { // Given Long roomId = 1L; - ChatMessageDto inputMessage = ChatMessageDto.builder() - .content("테스트 메시지") - .messageType("TEXT") - .build(); + + RoomChatMessageDto inputMessage = RoomChatMessageDto.createRequest( + "테스트 메시지", + "TEXT" + ); // 실제로 필요한 stubbing만 설정 - given(chatService.saveRoomChatMessage(any(ChatMessageDto.class))).willReturn(mockSavedMessage); + given(roomChatService.saveRoomChatMessage(any(RoomChatMessageDto.class))).willReturn(mockSavedMessage); // When - chatWebSocketController.handleRoomChat(roomId, inputMessage, headerAccessor, testPrincipal); + roomChatWebSocketController.handleRoomChat(roomId, inputMessage, headerAccessor, testPrincipal); // Then - verify(chatService).saveRoomChatMessage(argThat(dto -> - dto.getRoomId().equals(roomId) && - dto.getUserId().equals(1L) && - dto.getContent().equals("테스트 메시지") + verify(roomChatService).saveRoomChatMessage(argThat(dto -> + dto.roomId().equals(roomId) && + dto.userId().equals(1L) && + dto.content().equals("테스트 메시지") )); verify(messagingTemplate).convertAndSend( eq("/topic/room/" + roomId), - argThat((ChatMessageDto responseDto) -> - responseDto.getMessageId().equals(100L) && - responseDto.getRoomId().equals(roomId) && - responseDto.getUserId().equals(1L) && - responseDto.getNickname().equals("테스터") && - responseDto.getProfileImageUrl().equals("https://example.com/profile.jpg") && - responseDto.getContent().equals("테스트 메시지") && - responseDto.getMessageType().equals("TEXT") + argThat((RoomChatMessageDto responseDto) -> + responseDto.messageId().equals(100L) && + responseDto.roomId().equals(roomId) && + responseDto.userId().equals(1L) && + responseDto.nickname().equals("테스터") && + responseDto.profileImageUrl().equals("https://example.com/profile.jpg") && + responseDto.content().equals("테스트 메시지") && + responseDto.messageType().equals("TEXT") ) ); @@ -154,19 +153,22 @@ void t1() { @DisplayName("인증되지 않은 사용자의 메시지 처리 - 에러 전송") void t2() { Long roomId = 1L; - ChatMessageDto inputMessage = ChatMessageDto.builder() - .content("테스트 메시지") - .messageType("TEXT") - .build(); + + RoomChatMessageDto inputMessage = RoomChatMessageDto.createRequest( + "테스트 메시지", + "TEXT" + ); Principal invalidPrincipal = null; // 인증 정보 없음 // 에러 응답을 위해 sessionId가 필요 given(headerAccessor.getSessionId()).willReturn("test-session-123"); - chatWebSocketController.handleRoomChat(roomId, inputMessage, headerAccessor, invalidPrincipal); + // When + roomChatWebSocketController.handleRoomChat(roomId, inputMessage, headerAccessor, invalidPrincipal); - verify(chatService, never()).saveRoomChatMessage(any()); + // Then + verify(roomChatService, never()).saveRoomChatMessage(any()); verify(messagingTemplate, never()).convertAndSend(anyString(), any(Object.class)); // 에러 메시지가 해당 사용자에게만 전송되는지 확인 @@ -174,8 +176,8 @@ void t2() { eq("test-session-123"), eq("/queue/errors"), argThat((WebSocketErrorResponse errorResponse) -> - errorResponse.getError().getCode().equals("WS_UNAUTHORIZED") && - errorResponse.getError().getMessage().equals("인증이 필요합니다") + errorResponse.error().code().equals("WS_UNAUTHORIZED") && + errorResponse.error().message().equals("인증이 필요합니다") ) ); } @@ -184,25 +186,28 @@ void t2() { @DisplayName("서비스 계층 예외 발생 시 에러 처리") void t3() { Long roomId = 1L; - ChatMessageDto inputMessage = ChatMessageDto.builder() - .content("테스트 메시지") - .messageType("TEXT") - .build(); + + RoomChatMessageDto inputMessage = RoomChatMessageDto.createRequest( + "테스트 메시지", + "TEXT" + ); // 예외 발생 시 sessionId와 서비스 예외 설정 given(headerAccessor.getSessionId()).willReturn("test-session-123"); - given(chatService.saveRoomChatMessage(any())).willThrow(new RuntimeException("DB 오류")); + given(roomChatService.saveRoomChatMessage(any())).willThrow(new RuntimeException("DB 오류")); - chatWebSocketController.handleRoomChat(roomId, inputMessage, headerAccessor, testPrincipal); + // When + roomChatWebSocketController.handleRoomChat(roomId, inputMessage, headerAccessor, testPrincipal); + // Then verify(messagingTemplate, never()).convertAndSend(anyString(), any(Object.class)); verify(messagingTemplate).convertAndSendToUser( eq("test-session-123"), eq("/queue/errors"), argThat((WebSocketErrorResponse errorResponse) -> - errorResponse.getError().getCode().equals("WS_ROOM_NOT_FOUND") && - errorResponse.getError().getMessage().equals("존재하지 않는 방입니다") + errorResponse.error().code().equals("WS_ROOM_NOT_FOUND") && + errorResponse.error().message().equals("존재하지 않는 방입니다") ) ); } @@ -211,19 +216,22 @@ void t3() { @DisplayName("잘못된 Principal 타입 처리") void t4() { Long roomId = 1L; - ChatMessageDto inputMessage = ChatMessageDto.builder() - .content("테스트 메시지") - .messageType("TEXT") - .build(); + + RoomChatMessageDto inputMessage = RoomChatMessageDto.createRequest( + "테스트 메시지", + "TEXT" + ); // Authentication이 아닌 다른 Principal Principal invalidTypePrincipal = () -> "some-principal-name"; given(headerAccessor.getSessionId()).willReturn("test-session-123"); - chatWebSocketController.handleRoomChat(roomId, inputMessage, headerAccessor, invalidTypePrincipal); + // When + roomChatWebSocketController.handleRoomChat(roomId, inputMessage, headerAccessor, invalidTypePrincipal); - verify(chatService, never()).saveRoomChatMessage(any()); + // Then + verify(roomChatService, never()).saveRoomChatMessage(any()); verify(messagingTemplate, never()).convertAndSend(anyString(), any(Object.class)); verify(messagingTemplate).convertAndSendToUser( @@ -237,10 +245,11 @@ void t4() { @DisplayName("CustomUserDetails가 아닌 Principal 객체 처리") void t5() { Long roomId = 1L; - ChatMessageDto inputMessage = ChatMessageDto.builder() - .content("테스트 메시지") - .messageType("TEXT") - .build(); + + RoomChatMessageDto inputMessage = RoomChatMessageDto.createRequest( + "테스트 메시지", + "TEXT" + ); Authentication authWithWrongPrincipal = new UsernamePasswordAuthenticationToken( "string-principal", null, null @@ -248,9 +257,11 @@ void t5() { given(headerAccessor.getSessionId()).willReturn("test-session-123"); - chatWebSocketController.handleRoomChat(roomId, inputMessage, headerAccessor, authWithWrongPrincipal); + // When + roomChatWebSocketController.handleRoomChat(roomId, inputMessage, headerAccessor, authWithWrongPrincipal); - verify(chatService, never()).saveRoomChatMessage(any()); + // Then + verify(roomChatService, never()).saveRoomChatMessage(any()); verify(messagingTemplate, never()).convertAndSend(anyString(), any(Object.class)); verify(messagingTemplate).convertAndSendToUser( diff --git a/src/test/java/com/back/domain/chat/service/ChatServiceTest.java b/src/test/java/com/back/domain/chat/room/service/RoomChatServiceTest.java similarity index 76% rename from src/test/java/com/back/domain/chat/service/ChatServiceTest.java rename to src/test/java/com/back/domain/chat/room/service/RoomChatServiceTest.java index 06101285..9854e1f0 100644 --- a/src/test/java/com/back/domain/chat/service/ChatServiceTest.java +++ b/src/test/java/com/back/domain/chat/room/service/RoomChatServiceTest.java @@ -1,7 +1,7 @@ -package com.back.domain.chat.service; +package com.back.domain.chat.room.service; -import com.back.domain.chat.dto.ChatMessageDto; -import com.back.domain.chat.dto.ChatPageResponse; +import com.back.domain.chat.room.dto.RoomChatMessageDto; +import com.back.domain.chat.room.dto.RoomChatPageResponse; import com.back.domain.studyroom.entity.Room; import com.back.domain.studyroom.entity.RoomChatMessage; import com.back.domain.studyroom.repository.RoomChatMessageRepository; @@ -36,8 +36,8 @@ @ExtendWith(MockitoExtension.class) @ActiveProfiles("test") -@DisplayName("ChatService 테스트") -class ChatServiceTest { +@DisplayName("RoomChatService 테스트") +class RoomChatServiceTest { @Mock private RoomChatMessageRepository roomChatMessageRepository; @@ -49,7 +49,7 @@ class ChatServiceTest { private UserRepository userRepository; @InjectMocks - private ChatService chatService; + private RoomChatService roomChatService; private Room testRoom; private User testUser; @@ -116,18 +116,18 @@ private void setField(Object target, String fieldName, Object value) throws Exce @DisplayName("채팅 메시지 저장 성공") void t1() { // Given - ChatMessageDto chatMessageDto = ChatMessageDto.builder() - .roomId(1L) - .userId(1L) - .content("안녕하세요!") - .build(); + // createRequest 사용 후 필드 업데이트 + RoomChatMessageDto roomChatMessageDto = RoomChatMessageDto + .createRequest("안녕하세요!", "TEXT") + .withRoomId(1L) + .withUserId(1L); given(roomRepository.findById(1L)).willReturn(Optional.of(testRoom)); given(userRepository.findById(1L)).willReturn(Optional.of(testUser)); given(roomChatMessageRepository.save(any(RoomChatMessage.class))).willReturn(testMessage); // When - RoomChatMessage result = chatService.saveRoomChatMessage(chatMessageDto); + RoomChatMessage result = roomChatService.saveRoomChatMessage(roomChatMessageDto); // Then assertThat(result).isNotNull(); @@ -142,15 +142,14 @@ void t1() { @Test @DisplayName("채팅 메시지 저장 실패 - 존재하지 않는 방") void t2() { - ChatMessageDto chatMessageDto = ChatMessageDto.builder() - .roomId(999L) - .userId(1L) - .content("메시지") - .build(); + RoomChatMessageDto roomChatMessageDto = RoomChatMessageDto + .createRequest("메시지", "TEXT") + .withRoomId(999L) + .withUserId(1L); given(roomRepository.findById(999L)).willReturn(Optional.empty()); - assertThatThrownBy(() -> chatService.saveRoomChatMessage(chatMessageDto)) + assertThatThrownBy(() -> roomChatService.saveRoomChatMessage(roomChatMessageDto)) .isInstanceOf(CustomException.class) .hasFieldOrPropertyWithValue("errorCode", ErrorCode.ROOM_NOT_FOUND); @@ -162,16 +161,15 @@ void t2() { @Test @DisplayName("채팅 메시지 저장 실패 - 존재하지 않는 사용자") void t3() { - ChatMessageDto chatMessageDto = ChatMessageDto.builder() - .roomId(1L) - .userId(999L) - .content("메시지") - .build(); + RoomChatMessageDto roomChatMessageDto = RoomChatMessageDto + .createRequest("메시지", "TEXT") + .withRoomId(1L) + .withUserId(999L); given(roomRepository.findById(1L)).willReturn(Optional.of(testRoom)); given(userRepository.findById(999L)).willReturn(Optional.empty()); - assertThatThrownBy(() -> chatService.saveRoomChatMessage(chatMessageDto)) + assertThatThrownBy(() -> roomChatService.saveRoomChatMessage(roomChatMessageDto)) .isInstanceOf(CustomException.class) .hasFieldOrPropertyWithValue("errorCode", ErrorCode.USER_NOT_FOUND); @@ -195,19 +193,20 @@ void t4() { given(roomChatMessageRepository.findMessagesByRoomId(eq(roomId), any(Pageable.class))) .willReturn(messagePage); - ChatPageResponse result = chatService.getRoomChatHistory(roomId, page, size, before); + RoomChatPageResponse result = roomChatService.getRoomChatHistory(roomId, page, size, before); assertThat(result).isNotNull(); - assertThat(result.getContent()).hasSize(1); - assertThat(result.getTotalElements()).isEqualTo(1); + assertThat(result.content()).hasSize(1); + assertThat(result.totalElements()).isEqualTo(1); + + RoomChatMessageDto messageDto = result.content().get(0); - ChatMessageDto messageDto = result.getContent().get(0); - assertThat(messageDto.getMessageId()).isEqualTo(1L); - assertThat(messageDto.getRoomId()).isEqualTo(1L); - assertThat(messageDto.getUserId()).isEqualTo(1L); - assertThat(messageDto.getNickname()).isEqualTo("테스터"); - assertThat(messageDto.getContent()).isEqualTo("테스트 메시지"); - assertThat(messageDto.getMessageType()).isEqualTo("TEXT"); + assertThat(messageDto.messageId()).isEqualTo(1L); + assertThat(messageDto.roomId()).isEqualTo(1L); + assertThat(messageDto.userId()).isEqualTo(1L); + assertThat(messageDto.nickname()).isEqualTo("테스터"); + assertThat(messageDto.content()).isEqualTo("테스트 메시지"); + assertThat(messageDto.messageType()).isEqualTo("TEXT"); verify(roomChatMessageRepository).findMessagesByRoomId(eq(roomId), any(Pageable.class)); verify(roomChatMessageRepository, never()).findMessagesByRoomIdBefore(any(), any(), any()); @@ -228,14 +227,15 @@ void t5() { given(roomChatMessageRepository.findMessagesByRoomIdBefore(eq(roomId), eq(before), any(Pageable.class))) .willReturn(messagePage); - ChatPageResponse result = chatService.getRoomChatHistory(roomId, page, size, before); + RoomChatPageResponse result = roomChatService.getRoomChatHistory(roomId, page, size, before); assertThat(result).isNotNull(); - assertThat(result.getContent()).hasSize(1); + assertThat(result.content()).hasSize(1); - ChatMessageDto messageDto = result.getContent().get(0); - assertThat(messageDto.getNickname()).isEqualTo("테스터"); - assertThat(messageDto.getProfileImageUrl()).isEqualTo("https://example.com/profile.jpg"); + RoomChatMessageDto messageDto = result.content().get(0); + + assertThat(messageDto.nickname()).isEqualTo("테스터"); + assertThat(messageDto.profileImageUrl()).isEqualTo("https://example.com/profile.jpg"); verify(roomChatMessageRepository).findMessagesByRoomIdBefore(eq(roomId), eq(before), any(Pageable.class)); verify(roomChatMessageRepository, never()).findMessagesByRoomId(any(), any()); @@ -248,7 +248,7 @@ void t6() { given(roomRepository.findById(nonExistentRoomId)).willReturn(Optional.empty()); - assertThatThrownBy(() -> chatService.getRoomChatHistory(nonExistentRoomId, 0, 10, null)) + assertThatThrownBy(() -> roomChatService.getRoomChatHistory(nonExistentRoomId, 0, 10, null)) .isInstanceOf(CustomException.class) .hasFieldOrPropertyWithValue("errorCode", ErrorCode.ROOM_NOT_FOUND); @@ -272,7 +272,7 @@ void t7() { given(roomChatMessageRepository.findMessagesByRoomId(eq(roomId), any(Pageable.class))) .willReturn(messagePage); - ChatPageResponse result = chatService.getRoomChatHistory(roomId, page, requestedSize, before); + RoomChatPageResponse result = roomChatService.getRoomChatHistory(roomId, page, requestedSize, before); assertThat(result).isNotNull(); @@ -297,7 +297,7 @@ void t8() { given(roomChatMessageRepository.findMessagesByRoomId(eq(roomId), any(Pageable.class))) .willReturn(messagePage); - ChatPageResponse result = chatService.getRoomChatHistory(roomId, page, requestedSize, before); + RoomChatPageResponse result = roomChatService.getRoomChatHistory(roomId, page, requestedSize, before); assertThat(result).isNotNull(); @@ -322,7 +322,7 @@ void t9() { given(roomChatMessageRepository.findMessagesByRoomId(eq(roomId), any(Pageable.class))) .willReturn(messagePage); - ChatPageResponse result = chatService.getRoomChatHistory(roomId, page, requestedSize, before); + RoomChatPageResponse result = roomChatService.getRoomChatHistory(roomId, page, requestedSize, before); assertThat(result).isNotNull(); @@ -347,7 +347,7 @@ void t10() { given(roomChatMessageRepository.findMessagesByRoomId(eq(roomId), any(Pageable.class))) .willReturn(messagePage); - ChatPageResponse result = chatService.getRoomChatHistory(roomId, page, requestedSize, before); + RoomChatPageResponse result = roomChatService.getRoomChatHistory(roomId, page, requestedSize, before); assertThat(result).isNotNull(); @@ -367,31 +367,32 @@ void t11() { given(roomChatMessageRepository.findMessagesByRoomId(eq(roomId), any(Pageable.class))) .willReturn(emptyPage); - ChatPageResponse result = chatService.getRoomChatHistory(roomId, 0, 10, null); + RoomChatPageResponse result = roomChatService.getRoomChatHistory(roomId, 0, 10, null); assertThat(result).isNotNull(); - assertThat(result.getContent()).isEmpty(); - assertThat(result.getTotalElements()).isEqualTo(0); + assertThat(result.content()).isEmpty(); + assertThat(result.totalElements()).isEqualTo(0); } @Test @DisplayName("convertToDto 메소드 테스트") void t12() throws Exception { // ChatService의 private 메소드에 접근하기 위해 리플렉션 사용 - java.lang.reflect.Method convertToDtoMethod = ChatService.class.getDeclaredMethod("convertToDto", RoomChatMessage.class); + java.lang.reflect.Method convertToDtoMethod = RoomChatService.class.getDeclaredMethod("convertToDto", RoomChatMessage.class); convertToDtoMethod.setAccessible(true); - ChatMessageDto result = (ChatMessageDto) convertToDtoMethod.invoke(chatService, testMessage); + RoomChatMessageDto result = (RoomChatMessageDto) convertToDtoMethod.invoke(roomChatService, testMessage); assertThat(result).isNotNull(); - assertThat(result.getMessageId()).isEqualTo(1L); - assertThat(result.getRoomId()).isEqualTo(1L); - assertThat(result.getUserId()).isEqualTo(1L); - assertThat(result.getNickname()).isEqualTo("테스터"); - assertThat(result.getProfileImageUrl()).isEqualTo("https://example.com/profile.jpg"); - assertThat(result.getContent()).isEqualTo("테스트 메시지"); - assertThat(result.getMessageType()).isEqualTo("TEXT"); - assertThat(result.getAttachment()).isNull(); - assertThat(result.getCreatedAt()).isNotNull(); + + assertThat(result.messageId()).isEqualTo(1L); + assertThat(result.roomId()).isEqualTo(1L); + assertThat(result.userId()).isEqualTo(1L); + assertThat(result.nickname()).isEqualTo("테스터"); + assertThat(result.profileImageUrl()).isEqualTo("https://example.com/profile.jpg"); + assertThat(result.content()).isEqualTo("테스트 메시지"); + assertThat(result.messageType()).isEqualTo("TEXT"); + assertThat(result.attachment()).isNull(); + assertThat(result.createdAt()).isNotNull(); } } \ No newline at end of file diff --git a/src/test/java/com/back/global/websocket/service/WebSocketSessionManagerTest.java b/src/test/java/com/back/global/websocket/service/WebSocketSessionManagerTest.java index 582ed249..10f7aad0 100644 --- a/src/test/java/com/back/global/websocket/service/WebSocketSessionManagerTest.java +++ b/src/test/java/com/back/global/websocket/service/WebSocketSessionManagerTest.java @@ -15,7 +15,6 @@ import org.springframework.data.redis.core.ValueOperations; import java.time.Duration; -import java.time.LocalDateTime; import java.util.Set; import static org.assertj.core.api.Assertions.*; @@ -68,12 +67,9 @@ void t1() { void t2() { // given when(redisTemplate.opsForValue()).thenReturn(valueOperations); - WebSocketSessionInfo existingSession = WebSocketSessionInfo.builder() - .userId(TEST_USER_ID) - .sessionId("old-session-123") - .connectedAt(LocalDateTime.now().minusMinutes(5)) - .lastActiveAt(LocalDateTime.now().minusMinutes(1)) - .build(); + + WebSocketSessionInfo existingSession = WebSocketSessionInfo.createNewSession(TEST_USER_ID, "old-session-123") + .withUpdatedActivity(); // 활동 시간 업데이트 // when when(valueOperations.get("ws:user:123")).thenReturn(existingSession); @@ -146,13 +142,11 @@ void t6() { void t7() { // given when(redisTemplate.opsForValue()).thenReturn(valueOperations); - WebSocketSessionInfo expectedSessionInfo = WebSocketSessionInfo.builder() - .userId(TEST_USER_ID) - .sessionId(TEST_SESSION_ID) - .connectedAt(LocalDateTime.now()) - .lastActiveAt(LocalDateTime.now()) - .currentRoomId(TEST_ROOM_ID) - .build(); + + // 체이닝으로 세션 정보 생성 + WebSocketSessionInfo expectedSessionInfo = WebSocketSessionInfo + .createNewSession(TEST_USER_ID, TEST_SESSION_ID) + .withRoomId(TEST_ROOM_ID); when(valueOperations.get("ws:user:123")).thenReturn(expectedSessionInfo); @@ -161,9 +155,9 @@ void t7() { // then assertThat(result).isNotNull(); - assertThat(result.getUserId()).isEqualTo(TEST_USER_ID); - assertThat(result.getSessionId()).isEqualTo(TEST_SESSION_ID); - assertThat(result.getCurrentRoomId()).isEqualTo(TEST_ROOM_ID); + assertThat(result.userId()).isEqualTo(TEST_USER_ID); + assertThat(result.sessionId()).isEqualTo(TEST_SESSION_ID); + assertThat(result.currentRoomId()).isEqualTo(TEST_ROOM_ID); } @Test @@ -185,12 +179,9 @@ void t8() { void t9() { // given when(redisTemplate.opsForValue()).thenReturn(valueOperations); - WebSocketSessionInfo sessionInfo = WebSocketSessionInfo.builder() - .userId(TEST_USER_ID) - .sessionId(TEST_SESSION_ID) - .connectedAt(LocalDateTime.now().minusMinutes(10)) - .lastActiveAt(LocalDateTime.now().minusMinutes(5)) - .build(); + + WebSocketSessionInfo sessionInfo = WebSocketSessionInfo + .createNewSession(TEST_USER_ID, TEST_SESSION_ID); when(valueOperations.get("ws:user:123")).thenReturn(sessionInfo); @@ -225,12 +216,9 @@ void t11() { // given when(redisTemplate.opsForValue()).thenReturn(valueOperations); when(redisTemplate.opsForSet()).thenReturn(setOperations); - WebSocketSessionInfo sessionInfo = WebSocketSessionInfo.builder() - .userId(TEST_USER_ID) - .sessionId(TEST_SESSION_ID) - .connectedAt(LocalDateTime.now()) - .lastActiveAt(LocalDateTime.now()) - .build(); + + WebSocketSessionInfo sessionInfo = WebSocketSessionInfo + .createNewSession(TEST_USER_ID, TEST_SESSION_ID); when(valueOperations.get("ws:user:123")).thenReturn(sessionInfo); @@ -252,13 +240,10 @@ void t12() { when(redisTemplate.opsForValue()).thenReturn(valueOperations); when(redisTemplate.opsForSet()).thenReturn(setOperations); Long previousRoomId = 999L; - WebSocketSessionInfo sessionInfo = WebSocketSessionInfo.builder() - .userId(TEST_USER_ID) - .sessionId(TEST_SESSION_ID) - .connectedAt(LocalDateTime.now()) - .lastActiveAt(LocalDateTime.now()) - .currentRoomId(previousRoomId) - .build(); + + WebSocketSessionInfo sessionInfo = WebSocketSessionInfo + .createNewSession(TEST_USER_ID, TEST_SESSION_ID) + .withRoomId(previousRoomId); when(valueOperations.get("ws:user:123")).thenReturn(sessionInfo); @@ -281,13 +266,10 @@ void t13() { // given when(redisTemplate.opsForValue()).thenReturn(valueOperations); when(redisTemplate.opsForSet()).thenReturn(setOperations); - WebSocketSessionInfo sessionInfo = WebSocketSessionInfo.builder() - .userId(TEST_USER_ID) - .sessionId(TEST_SESSION_ID) - .connectedAt(LocalDateTime.now()) - .lastActiveAt(LocalDateTime.now()) - .currentRoomId(TEST_ROOM_ID) - .build(); + + WebSocketSessionInfo sessionInfo = WebSocketSessionInfo + .createNewSession(TEST_USER_ID, TEST_SESSION_ID) + .withRoomId(TEST_ROOM_ID); when(valueOperations.get("ws:user:123")).thenReturn(sessionInfo); @@ -392,11 +374,10 @@ void t19() { void t20() { // given when(redisTemplate.opsForValue()).thenReturn(valueOperations); - WebSocketSessionInfo sessionInfo = WebSocketSessionInfo.builder() - .userId(TEST_USER_ID) - .sessionId(TEST_SESSION_ID) - .currentRoomId(TEST_ROOM_ID) - .build(); + + WebSocketSessionInfo sessionInfo = WebSocketSessionInfo + .createNewSession(TEST_USER_ID, TEST_SESSION_ID) + .withRoomId(TEST_ROOM_ID); when(valueOperations.get("ws:user:123")).thenReturn(sessionInfo); @@ -412,11 +393,10 @@ void t20() { void t21() { // given when(redisTemplate.opsForValue()).thenReturn(valueOperations); - WebSocketSessionInfo sessionInfo = WebSocketSessionInfo.builder() - .userId(TEST_USER_ID) - .sessionId(TEST_SESSION_ID) - .currentRoomId(null) - .build(); + + // 방 정보 없는 세션 + WebSocketSessionInfo sessionInfo = WebSocketSessionInfo + .createNewSession(TEST_USER_ID, TEST_SESSION_ID); // currentRoomId는 null when(valueOperations.get("ws:user:123")).thenReturn(sessionInfo); @@ -449,11 +429,9 @@ void t23() { when(redisTemplate.opsForSet()).thenReturn(setOperations); when(valueOperations.get("ws:session:session-123")).thenReturn(TEST_USER_ID); - WebSocketSessionInfo sessionInfo = WebSocketSessionInfo.builder() - .userId(TEST_USER_ID) - .sessionId(TEST_SESSION_ID) - .currentRoomId(TEST_ROOM_ID) - .build(); + WebSocketSessionInfo sessionInfo = WebSocketSessionInfo + .createNewSession(TEST_USER_ID, TEST_SESSION_ID) + .withRoomId(TEST_ROOM_ID); when(valueOperations.get("ws:user:123")).thenReturn(sessionInfo); // when @@ -481,5 +459,4 @@ void t24() { // 아무것도 삭제하지 않음 verify(redisTemplate, never()).delete(anyString()); } - } \ No newline at end of file