diff --git a/src/main/java/com/back/domain/studyroom/controller/RoomController.java b/src/main/java/com/back/domain/studyroom/controller/RoomController.java index d498d601..342a6d9c 100644 --- a/src/main/java/com/back/domain/studyroom/controller/RoomController.java +++ b/src/main/java/com/back/domain/studyroom/controller/RoomController.java @@ -417,6 +417,31 @@ public ResponseEntity> removeRoomPassword( .body(RsData.success("방 비밀번호 제거 완료", null)); } + @PostMapping("/{roomId}/password") + @Operation( + summary = "방 비밀번호 설정", + description = "비밀번호가 없는 방에 비밀번호를 설정합니다. 이미 비밀번호가 있는 경우 비밀번호 변경 API(PUT)를 사용. 방장만 실행 가능합니다." + ) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "비밀번호 설정 성공"), + @ApiResponse(responseCode = "400", description = "이미 비밀번호가 설정되어 있음"), + @ApiResponse(responseCode = "403", description = "방장 권한 없음"), + @ApiResponse(responseCode = "404", description = "존재하지 않는 방"), + @ApiResponse(responseCode = "401", description = "인증 실패") + }) + public ResponseEntity> setRoomPassword( + @Parameter(description = "방 ID", required = true) @PathVariable Long roomId, + @Valid @RequestBody SetRoomPasswordRequest request) { + + Long currentUserId = currentUser.getUserId(); + + roomService.setRoomPassword(roomId, request.getNewPassword(), currentUserId); + + return ResponseEntity + .status(HttpStatus.OK) + .body(RsData.success("방 비밀번호 설정 완료", null)); + } + @DeleteMapping("/{roomId}") @Operation( summary = "방 종료", diff --git a/src/main/java/com/back/domain/studyroom/controller/RoomInvitePublicController.java b/src/main/java/com/back/domain/studyroom/controller/RoomInvitePublicController.java index 27c8e743..666fdf65 100644 --- a/src/main/java/com/back/domain/studyroom/controller/RoomInvitePublicController.java +++ b/src/main/java/com/back/domain/studyroom/controller/RoomInvitePublicController.java @@ -36,12 +36,13 @@ public class RoomInvitePublicController { @SecurityRequirement(name = "Bearer Authentication") @Operation( summary = "초대 코드로 방 입장", - description = "초대 코드를 사용하여 방에 입장합니다. " + + description = "초대 코드를 사용하여 방 입장 권한을 획득합니다. " + "비밀번호가 걸린 방도 초대 코드로 입장 가능합니다. " + + "실제 온라인 등록은 WebSocket 연결 시 자동으로 처리됩니다. " + "비로그인 사용자는 401 응답을 받습니다 (프론트에서 로그인 페이지로 이동)." ) @ApiResponses({ - @ApiResponse(responseCode = "200", description = "입장 성공"), + @ApiResponse(responseCode = "200", description = "입장 권한 획득 성공 (WebSocket 연결 필요)"), @ApiResponse(responseCode = "400", description = "만료되었거나 유효하지 않은 코드"), @ApiResponse(responseCode = "404", description = "존재하지 않는 초대 코드"), @ApiResponse(responseCode = "401", description = "인증 필요 (비로그인)") @@ -56,12 +57,12 @@ public ResponseEntity> joinByInviteCode( // 초대 코드 검증 및 방 조회 Room room = inviteService.getRoomByInviteCode(inviteCode); - // 방 입장 (비밀번호 무시) - RoomMember member = roomService.joinRoom(room.getId(), null, userId); + // 방 입장 권한 체크 (비밀번호 무시, Redis 등록 건너뜀) + RoomMember member = roomService.joinRoom(room.getId(), null, userId, false); JoinRoomResponse response = JoinRoomResponse.from(member); return ResponseEntity .status(HttpStatus.OK) - .body(RsData.success("초대 코드로 입장 완료", response)); + .body(RsData.success("초대 코드로 입장 권한 획득 완료. WebSocket 연결 후 실제 입장됩니다.", response)); } } diff --git a/src/main/java/com/back/domain/studyroom/dto/MyRoomResponse.java b/src/main/java/com/back/domain/studyroom/dto/MyRoomResponse.java index 548a6f09..60745b74 100644 --- a/src/main/java/com/back/domain/studyroom/dto/MyRoomResponse.java +++ b/src/main/java/com/back/domain/studyroom/dto/MyRoomResponse.java @@ -14,7 +14,8 @@ public class MyRoomResponse { private Long roomId; private String title; private String description; - private Boolean isPrivate; // 비공개 방 여부 (UI에서 🔒 아이콘 표시용) + private Boolean isPrivate; + private String thumbnailUrl; private int currentParticipants; private int maxParticipants; private RoomStatus status; @@ -27,6 +28,7 @@ public static MyRoomResponse of(Room room, long currentParticipants, RoomRole my .title(room.getTitle()) .description(room.getDescription() != null ? room.getDescription() : "") .isPrivate(room.isPrivate()) // 비공개 방 여부 + .thumbnailUrl(room.getThumbnailUrl()) // 썸네일 URL .currentParticipants((int) currentParticipants) // Redis에서 조회한 실시간 값 .maxParticipants(room.getMaxParticipants()) .status(room.getStatus()) diff --git a/src/main/java/com/back/domain/studyroom/dto/RoomDetailResponse.java b/src/main/java/com/back/domain/studyroom/dto/RoomDetailResponse.java index 803d99f8..b2eacba8 100644 --- a/src/main/java/com/back/domain/studyroom/dto/RoomDetailResponse.java +++ b/src/main/java/com/back/domain/studyroom/dto/RoomDetailResponse.java @@ -15,6 +15,7 @@ public class RoomDetailResponse { private String title; private String description; private boolean isPrivate; + private String thumbnailUrl; // 썸네일 이미지 URL private int maxParticipants; private int currentParticipants; private RoomStatus status; @@ -31,6 +32,7 @@ public static RoomDetailResponse of(Room room, long currentParticipants, List findJoinablePublicRooms(Pageable pageable) { } /** - * 사용자가 참여 중인 방 조회 + * 사용자가 참여 중인 방 조회 (MEMBER 이상만) * 조회 조건: - * - 특정 사용자가 멤버로 등록된 방 (DB에 저장된 멤버십) - * TODO: Redis에서 온라인 상태 확인하도록 변경 + * - 특정 사용자가 MEMBER 이상으로 등록된 방 (VISITOR 제외) + * - DB에 저장된 멤버십만 조회 * @param userId 사용자 ID - * @return 참여 중인 방 목록 + * @return 참여 중인 방 목록 (VISITOR 제외) */ @Override public List findRoomsByUserId(Long userId) { @@ -87,7 +87,10 @@ public List findRoomsByUserId(Long userId) { .selectFrom(room) .leftJoin(room.createdBy, user).fetchJoin() // N+1 방지 .join(room.roomMembers, roomMember) // 멤버 조인 - .where(roomMember.user.id.eq(userId)) + .where( + roomMember.user.id.eq(userId), + roomMember.role.ne(com.back.domain.studyroom.entity.RoomRole.VISITOR) // VISITOR 제외 + ) .fetch(); } diff --git a/src/main/java/com/back/domain/studyroom/service/RoomService.java b/src/main/java/com/back/domain/studyroom/service/RoomService.java index 60588b4b..c2cfceeb 100644 --- a/src/main/java/com/back/domain/studyroom/service/RoomService.java +++ b/src/main/java/com/back/domain/studyroom/service/RoomService.java @@ -100,7 +100,7 @@ public Room createRoom(String title, String description, boolean isPrivate, } /** - * 방 입장 메서드 + * 방 입장 메서드 (WebSocket 연결과 함께 사용) * * 입장 검증 과정: * 1. 방 존재 확인 (비관적 락으로 동시성 제어) @@ -117,17 +117,33 @@ public Room createRoom(String title, String description, boolean isPrivate, */ @Transactional public RoomMember joinRoom(Long roomId, String password, Long userId) { + return joinRoom(roomId, password, userId, true); + } + + /** + * 방 입장 메서드 (오버로드 - WebSocket 등록 여부 선택 가능) + * + * @param roomId 방 ID + * @param password 비밀번호 (비공개 방인 경우) + * @param userId 사용자 ID + * @param registerOnline WebSocket 세션 등록 여부 (true: Redis 등록, false: 권한 체크만) + * @return RoomMember (메모리상 또는 DB 저장된 객체) + */ + @Transactional + public RoomMember joinRoom(Long roomId, String password, Long userId, boolean registerOnline) { // 1. 비관적 락으로 방 조회 - 동시 입장 시 정원 초과 방지 Room room = roomRepository.findByIdWithLock(roomId) .orElseThrow(() -> new CustomException(ErrorCode.ROOM_NOT_FOUND)); - // 2. Redis에서 현재 온라인 사용자 수 조회 - long currentOnlineCount = roomParticipantService.getParticipantCount(roomId); + // 2. Redis에서 현재 온라인 사용자 수 조회 (WebSocket 등록하는 경우만) + if (registerOnline) { + long currentOnlineCount = roomParticipantService.getParticipantCount(roomId); - // 3. 정원 확인 (Redis 기반) - if (currentOnlineCount >= room.getMaxParticipants()) { - throw new CustomException(ErrorCode.ROOM_FULL); + // 3. 정원 확인 (Redis 기반) + if (currentOnlineCount >= room.getMaxParticipants()) { + throw new CustomException(ErrorCode.ROOM_FULL); + } } // 4. 방 입장 가능 여부 확인 (활성화 + 입장 가능한 상태) @@ -140,8 +156,8 @@ public RoomMember joinRoom(Long roomId, String password, Long userId) { throw new CustomException(ErrorCode.ROOM_NOT_JOINABLE); } - // 5. 비밀번호 확인 - if (room.needsPassword() && !room.getPassword().equals(password)) { + // 5. 비밀번호 확인 (초대 코드 입장 시에는 password가 null일 수 있음) + if (room.needsPassword() && password != null && !room.getPassword().equals(password)) { throw new CustomException(ErrorCode.ROOM_PASSWORD_INCORRECT); } @@ -156,11 +172,15 @@ public RoomMember joinRoom(Long roomId, String password, Long userId) { // 기존 멤버 재입장: DB에 있는 역할 그대로 사용 RoomMember member = existingMember.get(); - // Redis에 온라인 등록 (아바타 포함) - roomParticipantService.enterRoom(userId, roomId, avatarId); - - log.info("기존 멤버 재입장 - RoomId: {}, UserId: {}, Role: {}, AvatarId: {}", - roomId, userId, member.getRole(), avatarId); + // WebSocket 연결과 함께 입장하는 경우에만 Redis 등록 + if (registerOnline) { + roomParticipantService.enterRoom(userId, roomId, avatarId); + log.info("기존 멤버 재입장 (Redis 등록) - RoomId: {}, UserId: {}, Role: {}, AvatarId: {}", + roomId, userId, member.getRole(), avatarId); + } else { + log.info("기존 멤버 권한 확인 (Redis 등록 건너뜀) - RoomId: {}, UserId: {}, Role: {}", + roomId, userId, member.getRole()); + } return member; } @@ -168,11 +188,15 @@ public RoomMember joinRoom(Long roomId, String password, Long userId) { // 신규 입장자: VISITOR로 입장 (DB 저장 안함!) RoomMember visitorMember = RoomMember.createVisitor(room, user); - // Redis에만 온라인 등록 (아바타 포함) - roomParticipantService.enterRoom(userId, roomId, avatarId); - - log.info("신규 입장 (VISITOR) - RoomId: {}, UserId: {}, DB 저장 안함, AvatarId: {}", - roomId, userId, avatarId); + // WebSocket 연결과 함께 입장하는 경우에만 Redis 등록 + if (registerOnline) { + roomParticipantService.enterRoom(userId, roomId, avatarId); + log.info("신규 입장 (VISITOR, Redis 등록) - RoomId: {}, UserId: {}, AvatarId: {}", + roomId, userId, avatarId); + } else { + log.info("신규 입장 권한 확인 (Redis 등록 건너뜀) - RoomId: {}, UserId: {}", + roomId, userId); + } // 메모리상 객체 반환 (DB에 저장되지 않음) return visitorMember; @@ -360,6 +384,35 @@ public void removeRoomPassword(Long roomId, Long userId) { log.info("방 비밀번호 제거 완료 - RoomId: {}, UserId: {}", roomId, userId); } + /** + * 방 비밀번호 설정 (비밀번호가 없는 방에 비밀번호 추가) + * - 방장만 설정 가능 + * - 기존 비밀번호가 없는 경우에만 사용 + * @param roomId 방 ID + * @param newPassword 새 비밀번호 + * @param userId 요청자 ID (방장) + */ + @Transactional + public void setRoomPassword(Long roomId, String newPassword, Long userId) { + Room room = roomRepository.findById(roomId) + .orElseThrow(() -> new CustomException(ErrorCode.ROOM_NOT_FOUND)); + + // 방장 권한 확인 + if (!room.isOwner(userId)) { + throw new CustomException(ErrorCode.NOT_ROOM_HOST); + } + + // 이미 비밀번호가 있는 경우 에러 + if (room.getPassword() != null && !room.getPassword().isEmpty()) { + throw new CustomException(ErrorCode.ROOM_PASSWORD_ALREADY_EXISTS); + } + + // 새 비밀번호 설정 + room.updatePassword(newPassword); + + log.info("방 비밀번호 설정 완료 - RoomId: {}, UserId: {}", roomId, userId); + } + @Transactional public void terminateRoom(Long roomId, Long userId) { diff --git a/src/main/java/com/back/global/exception/ErrorCode.java b/src/main/java/com/back/global/exception/ErrorCode.java index 6f8fb02f..cd66b22f 100644 --- a/src/main/java/com/back/global/exception/ErrorCode.java +++ b/src/main/java/com/back/global/exception/ErrorCode.java @@ -39,6 +39,7 @@ public enum ErrorCode { CHAT_DELETE_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "ROOM_016", "채팅 삭제 중 오류가 발생했습니다."), ROOM_PASSWORD_MISMATCH(HttpStatus.BAD_REQUEST, "ROOM_017", "현재 비밀번호가 일치하지 않습니다."), NOT_ROOM_HOST(HttpStatus.FORBIDDEN, "ROOM_018", "방장 권한이 필요합니다."), + ROOM_PASSWORD_ALREADY_EXISTS(HttpStatus.BAD_REQUEST, "ROOM_019", "이미 비밀번호가 설정되어 있습니다. 비밀번호 변경 API를 사용하세요."), // ======================== 초대 코드 관련 ======================== INVALID_INVITE_CODE(HttpStatus.NOT_FOUND, "INVITE_001", "유효하지 않은 초대 코드입니다."), 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 24984778..884eb4a3 100644 --- a/src/main/java/com/back/global/websocket/controller/WebSocketMessageController.java +++ b/src/main/java/com/back/global/websocket/controller/WebSocketMessageController.java @@ -1,7 +1,9 @@ package com.back.global.websocket.controller; +import com.back.domain.studyroom.service.AvatarService; import com.back.global.exception.CustomException; import com.back.global.security.user.CustomUserDetails; +import com.back.global.websocket.service.RoomParticipantService; import com.back.global.websocket.service.WebSocketSessionManager; import com.back.global.websocket.util.WebSocketAuthHelper; import com.back.global.websocket.util.WebSocketErrorHelper; @@ -25,9 +27,12 @@ public class WebSocketMessageController { private final WebSocketSessionManager sessionManager; private final WebSocketErrorHelper errorHelper; + private final RoomParticipantService roomParticipantService; + private final AvatarService avatarService; // WebSocket 방 입장 확인 메시지 - // 클라이언트가 REST API로 입장 후 WebSocket 세션 동기화 대기를 위해 전송 + // 클라이언트가 REST API로 입장 후 WebSocket 세션 동기화를 위해 전송 + // 초대 코드로 입장한 경우 Redis 등록이 안 되어 있으므로 여기서 처리 @MessageMapping("/rooms/{roomId}/join") public void handleWebSocketJoinRoom(@DestinationVariable Long roomId, @Payload Map payload, @@ -44,8 +49,23 @@ public void handleWebSocketJoinRoom(@DestinationVariable Long roomId, // 활동 시간 업데이트 sessionManager.updateLastActivity(userId); - // 실제 방 입장 로직은 REST API에서 이미 처리했으므로 - // 여기서는 단순히 WebSocket 세션이 준비되었음을 확인하는 용도 + // Redis에 이미 등록되어 있는지 확인 + Long currentRoomId = roomParticipantService.getCurrentRoomId(userId); + + if (currentRoomId == null || !currentRoomId.equals(roomId)) { + // Redis에 등록되지 않은 경우 (초대 코드 입장 등) + // 아바타 로드/생성 + Long avatarId = avatarService.loadOrCreateAvatar(roomId, userId); + + // Redis에 온라인 등록 + roomParticipantService.enterRoom(userId, roomId, avatarId); + + log.info("📥 [WebSocket] Redis 등록 완료 - roomId: {}, userId: {}, avatarId: {}", + roomId, userId, avatarId); + } else { + log.info("📥 [WebSocket] 이미 Redis에 등록된 사용자 - roomId: {}, userId: {}", + roomId, userId); + } } // Heartbeat 처리 diff --git a/src/test/java/com/back/domain/studyroom/controller/RoomInvitePublicControllerTest.java b/src/test/java/com/back/domain/studyroom/controller/RoomInvitePublicControllerTest.java index 93f365b1..889bbd93 100644 --- a/src/test/java/com/back/domain/studyroom/controller/RoomInvitePublicControllerTest.java +++ b/src/test/java/com/back/domain/studyroom/controller/RoomInvitePublicControllerTest.java @@ -142,7 +142,7 @@ void joinByInviteCode_Success_PublicRoom() { // given given(currentUser.getUserId()).willReturn(2L); given(inviteService.getRoomByInviteCode("ABC12345")).willReturn(testRoom); - given(roomService.joinRoom(eq(1L), isNull(), eq(2L))).willReturn(testMember); + given(roomService.joinRoom(eq(1L), isNull(), eq(2L), eq(false))).willReturn(testMember); // when ResponseEntity> response = @@ -152,11 +152,11 @@ void joinByInviteCode_Success_PublicRoom() { assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(response.getBody()).isNotNull(); assertThat(response.getBody().isSuccess()).isTrue(); - assertThat(response.getBody().getMessage()).isEqualTo("초대 코드로 입장 완료"); + assertThat(response.getBody().getMessage()).contains("초대 코드"); verify(currentUser, times(1)).getUserId(); verify(inviteService, times(1)).getRoomByInviteCode("ABC12345"); - verify(roomService, times(1)).joinRoom(eq(1L), isNull(), eq(2L)); + verify(roomService, times(1)).joinRoom(eq(1L), isNull(), eq(2L), eq(false)); } @Test @@ -165,7 +165,7 @@ void joinByInviteCode_Success_PrivateRoom_PasswordIgnored() { // given given(currentUser.getUserId()).willReturn(2L); given(inviteService.getRoomByInviteCode("PRIVATE1")).willReturn(privateRoom); - given(roomService.joinRoom(eq(2L), isNull(), eq(2L))).willReturn(privateMember); + given(roomService.joinRoom(eq(2L), isNull(), eq(2L), eq(false))).willReturn(privateMember); // when ResponseEntity> response = @@ -176,8 +176,8 @@ void joinByInviteCode_Success_PrivateRoom_PasswordIgnored() { assertThat(response.getBody()).isNotNull(); assertThat(response.getBody().isSuccess()).isTrue(); - // 비밀번호 null로 전달되는지 확인 (비밀번호 무시) - verify(roomService, times(1)).joinRoom(eq(2L), isNull(), eq(2L)); + // 비밀번호 null로 전달되는지 확인 (비밀번호 무시), registerOnline=false + verify(roomService, times(1)).joinRoom(eq(2L), isNull(), eq(2L), eq(false)); } @Test @@ -186,7 +186,7 @@ void joinByInviteCode_ResponseContainsRoomInfo() { // given given(currentUser.getUserId()).willReturn(2L); given(inviteService.getRoomByInviteCode("ABC12345")).willReturn(testRoom); - given(roomService.joinRoom(eq(1L), isNull(), eq(2L))).willReturn(testMember); + given(roomService.joinRoom(eq(1L), isNull(), eq(2L), eq(false))).willReturn(testMember); // when ResponseEntity> response = @@ -207,7 +207,7 @@ void joinByInviteCode_ResponseContainsUserInfo() { // given given(currentUser.getUserId()).willReturn(2L); given(inviteService.getRoomByInviteCode("ABC12345")).willReturn(testRoom); - given(roomService.joinRoom(eq(1L), isNull(), eq(2L))).willReturn(testMember); + given(roomService.joinRoom(eq(1L), isNull(), eq(2L), eq(false))).willReturn(testMember); // when ResponseEntity> response = @@ -263,14 +263,14 @@ void joinByInviteCode_PasswordAlwaysNull() { // given given(currentUser.getUserId()).willReturn(2L); given(inviteService.getRoomByInviteCode("ABC12345")).willReturn(testRoom); - given(roomService.joinRoom(eq(1L), isNull(), eq(2L))).willReturn(testMember); + given(roomService.joinRoom(eq(1L), isNull(), eq(2L), eq(false))).willReturn(testMember); // when invitePublicController.joinByInviteCode("ABC12345"); // then - // 비밀번호가 항상 null로 전달되는지 확인 - verify(roomService, times(1)).joinRoom(eq(1L), isNull(), eq(2L)); + // 비밀번호가 항상 null로 전달되는지 확인, registerOnline=false + verify(roomService, times(1)).joinRoom(eq(1L), isNull(), eq(2L), eq(false)); } @Test @@ -279,7 +279,7 @@ void joinByInviteCode_PrivateRoomAccessible() { // given given(currentUser.getUserId()).willReturn(2L); given(inviteService.getRoomByInviteCode("PRIVATE1")).willReturn(privateRoom); - given(roomService.joinRoom(eq(2L), isNull(), eq(2L))).willReturn(privateMember); + given(roomService.joinRoom(eq(2L), isNull(), eq(2L), eq(false))).willReturn(privateMember); // when ResponseEntity> response = @@ -290,8 +290,8 @@ void joinByInviteCode_PrivateRoomAccessible() { assertThat(response.getBody()).isNotNull(); assertThat(response.getBody().isSuccess()).isTrue(); - // 비공개 방인데도 비밀번호 없이 입장 성공 - verify(roomService, times(1)).joinRoom(eq(2L), isNull(), eq(2L)); + // 비공개 방인데도 비밀번호 없이 입장 성공, registerOnline=false + verify(roomService, times(1)).joinRoom(eq(2L), isNull(), eq(2L), eq(false)); } // ====================== HTTP 응답 테스트 ====================== @@ -302,7 +302,7 @@ void joinByInviteCode_HttpStatus() { // given given(currentUser.getUserId()).willReturn(2L); given(inviteService.getRoomByInviteCode("ABC12345")).willReturn(testRoom); - given(roomService.joinRoom(eq(1L), isNull(), eq(2L))).willReturn(testMember); + given(roomService.joinRoom(eq(1L), isNull(), eq(2L), eq(false))).willReturn(testMember); // when ResponseEntity> response = @@ -319,7 +319,7 @@ void joinByInviteCode_ResponseMessage() { // given given(currentUser.getUserId()).willReturn(2L); given(inviteService.getRoomByInviteCode("ABC12345")).willReturn(testRoom); - given(roomService.joinRoom(eq(1L), isNull(), eq(2L))).willReturn(testMember); + given(roomService.joinRoom(eq(1L), isNull(), eq(2L), eq(false))).willReturn(testMember); // when ResponseEntity> response = @@ -340,7 +340,7 @@ void joinByInviteCode_MultipleAttempts_SameUser() { // given given(currentUser.getUserId()).willReturn(2L); given(inviteService.getRoomByInviteCode("ABC12345")).willReturn(testRoom); - given(roomService.joinRoom(eq(1L), isNull(), eq(2L))).willReturn(testMember); + given(roomService.joinRoom(eq(1L), isNull(), eq(2L), eq(false))).willReturn(testMember); // when ResponseEntity> response1 = @@ -353,7 +353,7 @@ void joinByInviteCode_MultipleAttempts_SameUser() { assertThat(response2.getStatusCode()).isEqualTo(HttpStatus.OK); verify(inviteService, times(2)).getRoomByInviteCode("ABC12345"); - verify(roomService, times(2)).joinRoom(eq(1L), isNull(), eq(2L)); + verify(roomService, times(2)).joinRoom(eq(1L), isNull(), eq(2L), eq(false)); } @Test @@ -364,7 +364,7 @@ void joinByInviteCode_DifferentUsers_SameCode() { given(inviteService.getRoomByInviteCode("ABC12345")).willReturn(testRoom); RoomMember member2 = RoomMember.createVisitor(testRoom, testUser2); - given(roomService.joinRoom(eq(1L), isNull(), eq(2L))).willReturn(member2); + given(roomService.joinRoom(eq(1L), isNull(), eq(2L), eq(false))).willReturn(member2); // when - User2 입장 ResponseEntity> response1 = @@ -372,7 +372,7 @@ void joinByInviteCode_DifferentUsers_SameCode() { // given - User1 (코드 생성자도 입장 가능) given(currentUser.getUserId()).willReturn(1L); - given(roomService.joinRoom(eq(1L), isNull(), eq(1L))).willReturn(testMember); + given(roomService.joinRoom(eq(1L), isNull(), eq(1L), eq(false))).willReturn(testMember); // when - User1 입장 ResponseEntity> response2 = @@ -383,8 +383,8 @@ void joinByInviteCode_DifferentUsers_SameCode() { assertThat(response2.getStatusCode()).isEqualTo(HttpStatus.OK); verify(inviteService, times(2)).getRoomByInviteCode("ABC12345"); - verify(roomService, times(1)).joinRoom(eq(1L), isNull(), eq(2L)); - verify(roomService, times(1)).joinRoom(eq(1L), isNull(), eq(1L)); + verify(roomService, times(1)).joinRoom(eq(1L), isNull(), eq(2L), eq(false)); + verify(roomService, times(1)).joinRoom(eq(1L), isNull(), eq(1L), eq(false)); } @Test @@ -393,7 +393,7 @@ void joinByInviteCode_Fail_RoomFull() { // given given(currentUser.getUserId()).willReturn(2L); given(inviteService.getRoomByInviteCode("ABC12345")).willReturn(testRoom); - given(roomService.joinRoom(eq(1L), isNull(), eq(2L))) + given(roomService.joinRoom(eq(1L), isNull(), eq(2L), eq(false))) .willThrow(new CustomException(ErrorCode.ROOM_FULL)); // when & then @@ -402,7 +402,7 @@ void joinByInviteCode_Fail_RoomFull() { .hasFieldOrPropertyWithValue("errorCode", ErrorCode.ROOM_FULL); verify(inviteService, times(1)).getRoomByInviteCode("ABC12345"); - verify(roomService, times(1)).joinRoom(eq(1L), isNull(), eq(2L)); + verify(roomService, times(1)).joinRoom(eq(1L), isNull(), eq(2L), eq(false)); } @Test @@ -411,7 +411,7 @@ void joinByInviteCode_Fail_AlreadyJoined() { // given given(currentUser.getUserId()).willReturn(2L); given(inviteService.getRoomByInviteCode("ABC12345")).willReturn(testRoom); - given(roomService.joinRoom(eq(1L), isNull(), eq(2L))) + given(roomService.joinRoom(eq(1L), isNull(), eq(2L), eq(false))) .willThrow(new CustomException(ErrorCode.ALREADY_JOINED_ROOM)); // when & then @@ -420,7 +420,7 @@ void joinByInviteCode_Fail_AlreadyJoined() { .hasFieldOrPropertyWithValue("errorCode", ErrorCode.ALREADY_JOINED_ROOM); verify(inviteService, times(1)).getRoomByInviteCode("ABC12345"); - verify(roomService, times(1)).joinRoom(eq(1L), isNull(), eq(2L)); + verify(roomService, times(1)).joinRoom(eq(1L), isNull(), eq(2L), eq(false)); } @Test @@ -429,7 +429,7 @@ void joinByInviteCode_ResponseDataCompleteness() { // given given(currentUser.getUserId()).willReturn(2L); given(inviteService.getRoomByInviteCode("ABC12345")).willReturn(testRoom); - given(roomService.joinRoom(eq(1L), isNull(), eq(2L))).willReturn(testMember); + given(roomService.joinRoom(eq(1L), isNull(), eq(2L), eq(false))).willReturn(testMember); // when ResponseEntity> response = @@ -452,7 +452,7 @@ void joinByInviteCode_CaseSensitive() { // given given(currentUser.getUserId()).willReturn(2L); given(inviteService.getRoomByInviteCode("ABC12345")).willReturn(testRoom); - given(roomService.joinRoom(eq(1L), isNull(), eq(2L))).willReturn(testMember); + given(roomService.joinRoom(eq(1L), isNull(), eq(2L), eq(false))).willReturn(testMember); // when ResponseEntity> response = @@ -471,7 +471,7 @@ void joinByInviteCode_RoleIsVisitor() { // given given(currentUser.getUserId()).willReturn(2L); given(inviteService.getRoomByInviteCode("ABC12345")).willReturn(testRoom); - given(roomService.joinRoom(eq(1L), isNull(), eq(2L))).willReturn(testMember); + given(roomService.joinRoom(eq(1L), isNull(), eq(2L), eq(false))).willReturn(testMember); // when ResponseEntity> response = diff --git a/src/test/java/com/back/domain/studyroom/integration/RoomCreateIntegrationTest.java b/src/test/java/com/back/domain/studyroom/integration/RoomCreateIntegrationTest.java index 980ef9ce..48b318bc 100644 --- a/src/test/java/com/back/domain/studyroom/integration/RoomCreateIntegrationTest.java +++ b/src/test/java/com/back/domain/studyroom/integration/RoomCreateIntegrationTest.java @@ -89,7 +89,7 @@ void createRoom_RealSave() { maxParticipants, testUser.getId(), useWebRTC, - null // thumbnailUrl + null // thumbnailAttachmentId ); // then @@ -139,7 +139,7 @@ void createRoom_CheckCollections() { 10, testUser.getId(), true, // useWebRTC - null // thumbnailUrl + null // thumbnailAttachmentId ); // then - 컬렉션 필드들이 null이 아니어야 함 diff --git a/src/test/java/com/back/global/websocket/controller/WebSocketMessageControllerTest.java b/src/test/java/com/back/global/websocket/controller/WebSocketMessageControllerTest.java index ae55026d..415d5120 100644 --- a/src/test/java/com/back/global/websocket/controller/WebSocketMessageControllerTest.java +++ b/src/test/java/com/back/global/websocket/controller/WebSocketMessageControllerTest.java @@ -1,8 +1,10 @@ package com.back.global.websocket.controller; +import com.back.domain.studyroom.service.AvatarService; import com.back.global.exception.CustomException; import com.back.global.exception.ErrorCode; import com.back.global.security.user.CustomUserDetails; +import com.back.global.websocket.service.RoomParticipantService; import com.back.global.websocket.service.WebSocketSessionManager; import com.back.global.websocket.util.WebSocketErrorHelper; import org.junit.jupiter.api.BeforeEach; @@ -34,6 +36,12 @@ class WebSocketMessageControllerTest { @Mock private WebSocketErrorHelper errorHelper; + @Mock + private RoomParticipantService roomParticipantService; + + @Mock + private AvatarService avatarService; + @InjectMocks private WebSocketMessageController controller; @@ -67,15 +75,24 @@ class HandleWebSocketJoinRoomTest { void t1() { // given Long roomId = 1L; + Long avatarId = 5L; Map payload = new HashMap<>(); Principal mockPrincipal = createMockPrincipal(userId); + + // Redis에 등록되지 않은 상태 (초대 코드 입장 등) + when(roomParticipantService.getCurrentRoomId(userId)).thenReturn(null); + when(avatarService.loadOrCreateAvatar(roomId, userId)).thenReturn(avatarId); doNothing().when(sessionManager).updateLastActivity(userId); + doNothing().when(roomParticipantService).enterRoom(userId, roomId, avatarId); // when controller.handleWebSocketJoinRoom(roomId, payload, mockPrincipal); // then verify(sessionManager).updateLastActivity(userId); + verify(roomParticipantService).getCurrentRoomId(userId); + verify(avatarService).loadOrCreateAvatar(roomId, userId); + verify(roomParticipantService).enterRoom(userId, roomId, avatarId); } @Test @@ -91,24 +108,29 @@ void t2() { // then verify(sessionManager, never()).updateLastActivity(any(Long.class)); + verify(roomParticipantService, never()).getCurrentRoomId(any()); } @Test - @DisplayName("실패 - CustomException 발생 시 예외를 그대로 던짐") + @DisplayName("성공 - 이미 Redis에 등록된 사용자는 중복 등록 안 함") void t3() { // given Long roomId = 1L; Map payload = new HashMap<>(); Principal mockPrincipal = createMockPrincipal(userId); - CustomException expectedException = new CustomException(ErrorCode.BAD_REQUEST); - doThrow(expectedException).when(sessionManager).updateLastActivity(userId); + + // Redis에 이미 등록된 상태 + when(roomParticipantService.getCurrentRoomId(userId)).thenReturn(roomId); + doNothing().when(sessionManager).updateLastActivity(userId); - // when & then - assertThrows(CustomException.class, () -> { - controller.handleWebSocketJoinRoom(roomId, payload, mockPrincipal); - }); + // when + controller.handleWebSocketJoinRoom(roomId, payload, mockPrincipal); + // then verify(sessionManager).updateLastActivity(userId); + verify(roomParticipantService).getCurrentRoomId(userId); + verify(avatarService, never()).loadOrCreateAvatar(any(), any()); + verify(roomParticipantService, never()).enterRoom(any(), any(), any()); } }