Skip to content

Commit 67c7c2b

Browse files
loseminhonamgigun
andauthored
hotfix: 누락된 참여자 추방 컨트롤러단 추가 (#287) (#301)
* refactor: 스더티룸 권한에 대한 로직 개선 * fix: ci에서 통과 못한 테스트코드 수정 * fix:rest api와 웹소켓 중간 경로 통합 * fix:rest api와 웹소켓 중간 경로 통합 * fix: 에러 확인을 위한 통합테스트 추가, Room.create()메서드 수정 * refactor, feat : 조회 분할 * refactor: redis 로직 최적화 및 중복 검증 로직 제거 * fix: 에러 번호 수정 * feat: 스터디룸 방 비밀번호 변경 및 삭제 기능 구현 * fix:app-dev 제거 * feat: 웹소켓 기반 소극적 하트비트 * feat: 스터디룸 썸네일 기능 추가 및 webrtc 설정 변경에서 주석처리 * fix:소극적 하트비트 사용 주석처리 * Feat: 스터디 룸 내에 고양이 아바타 시스템과 프로필 이미지 url 연동 * fix: 기존 작성되어있던 test 코드 수정 * test: 아바타 테스트 코드 완료 * refactor: 프론트엔드 요청 사항에 따른 스터디룸 조회 마스킹 제거 * feat: 스터디룸 방 초대 코드 시스템 * Infra: main branch 로컬 환경과 운영 환경 동기화 * Infra: docker-compose 파일 수정 - Redis 버전 업그레이드 기존: 6.2 -> 변경: 7.0 * Fix: 백엔드 CD 파일 수정 - 자동화 시, 잘못된 도메인으로 호스트 ID 검증하는 오류 해결 * Infra: EC2 환경변수 수정 - 잘못 표기한 도메인 네임 변경 * Chore: CD 파일 수정 - Github Actions commandLine 인식 문제로 인해 set -Eeuo pipefail 줄바꿈 * Chore: 백엔드 CD 파일 수정 - 인스턴스 ID 체크 삭제 * Infra: 백엔드 CD 파일 수정 - .env 파일 추가시, $DOT_ENV_PROD -> $DOT_ENV 로 변경 * Infra: 도커 컴포즈 수정 - mysql 사용자 정보 변경 * Infra: 운영환경 설정 - application-prod.yml 과 application.yml 동기화 * Fix: SecurityConfig 수정 - H2 DB 허용 X * test,fix: 방 초대에 대한 테스트 코드 작성 및 에러 수정 * fix: 스터디룸 파일 업로드 맵핑 형식으로 변환 * fix: 병합충돌 제어 수정 * fix: 병합충돌 제어 * fix: 스터디 룸 내 프론트엔드 요구 사항 및 오류사항 수정 * feat: 방 즐겨찾기, 방 공지사항 구현 * fix: mockbean 수정 * fix: 테스트에서 빠진 비로그인 사용자 추가 * hotfix: 누락된 사용자 추방에 대한 컨트롤러 추가 * hotfix: VISITOR도 추방 가능하도록 수정 * fix: 누락된 테스트코드 추가 및 테스트코드 로직 수정 --------- Co-authored-by: namgigun <[email protected]>
1 parent fe1d26a commit 67c7c2b

File tree

3 files changed

+132
-8
lines changed

3 files changed

+132
-8
lines changed

src/main/java/com/back/domain/studyroom/controller/RoomController.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -615,4 +615,29 @@ public ResponseEntity<RsData<ChangeRoleResponse>> changeUserRole(
615615
.status(HttpStatus.OK)
616616
.body(RsData.success("역할 변경 완료", response));
617617
}
618+
619+
@DeleteMapping("/{roomId}/members/{userId}")
620+
@Operation(
621+
summary = "멤버 추방",
622+
description = "방에서 특정 멤버를 강제로 퇴장시킵니다. 방장과 부방장만 실행 가능하며, 방장은 추방할 수 없습니다. 추방된 사용자는 Redis에서 즉시 제거되고 알림을 받습니다."
623+
)
624+
@ApiResponses({
625+
@ApiResponse(responseCode = "200", description = "추방 성공"),
626+
@ApiResponse(responseCode = "400", description = "방장은 추방할 수 없음"),
627+
@ApiResponse(responseCode = "403", description = "추방 권한 없음 (방장 또는 부방장만 가능)"),
628+
@ApiResponse(responseCode = "404", description = "존재하지 않는 방 또는 멤버"),
629+
@ApiResponse(responseCode = "401", description = "인증 실패")
630+
})
631+
public ResponseEntity<RsData<Void>> kickMember(
632+
@Parameter(description = "방 ID", required = true) @PathVariable Long roomId,
633+
@Parameter(description = "추방할 사용자 ID", required = true) @PathVariable Long userId) {
634+
635+
Long currentUserId = currentUser.getUserId();
636+
637+
roomService.kickMember(roomId, userId, currentUserId);
638+
639+
return ResponseEntity
640+
.status(HttpStatus.OK)
641+
.body(RsData.success("멤버 추방 완료", null));
642+
}
618643
}

src/main/java/com/back/domain/studyroom/service/RoomService.java

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -676,28 +676,37 @@ public Page<Room> getPopularRooms(Pageable pageable) {
676676

677677
/**
678678
* 멤버 추방 (방장, 부방장만 가능)
679+
* VISITOR도 추방 가능
679680
*/
680681
@Transactional
681682
public void kickMember(Long roomId, Long targetUserId, Long requesterId) {
682683

684+
// 1. 요청자 권한 확인
683685
RoomMember requester = roomMemberRepository.findByRoomIdAndUserId(roomId, requesterId)
684686
.orElseThrow(() -> new CustomException(ErrorCode.NOT_ROOM_MEMBER));
685687

686688
if (!requester.canKickMember()) {
687689
throw new CustomException(ErrorCode.NOT_ROOM_MANAGER);
688690
}
689691

690-
RoomMember targetMember = roomMemberRepository.findByRoomIdAndUserId(roomId, targetUserId)
691-
.orElseThrow(() -> new CustomException(ErrorCode.NOT_ROOM_MEMBER));
692+
// 2. 대상자가 온라인 상태인지 확인 (Redis 체크)
693+
Set<Long> onlineUserIds = roomParticipantService.getParticipants(roomId);
694+
if (!onlineUserIds.contains(targetUserId)) {
695+
throw new CustomException(ErrorCode.NOT_ROOM_MEMBER);
696+
}
692697

693-
if (targetMember.isHost()) {
698+
// 3. 대상자 역할 확인 (DB 조회 - VISITOR는 없을 수 있음)
699+
Optional<RoomMember> targetMemberOpt = roomMemberRepository.findByRoomIdAndUserId(roomId, targetUserId);
700+
701+
// 방장은 추방 불가
702+
if (targetMemberOpt.isPresent() && targetMemberOpt.get().isHost()) {
694703
throw new CustomException(ErrorCode.CANNOT_KICK_HOST);
695704
}
696705

697-
// Redis에서 제거 (강제 퇴장)
706+
// 4. Redis에서 제거 (강제 퇴장) - VISITOR 포함 모든 사용자
698707
roomParticipantService.exitRoom(targetUserId, roomId);
699708

700-
// 멤버 추방 이벤트 발행
709+
// 5. 멤버 추방 이벤트 발행
701710
Room room = roomRepository.findById(roomId)
702711
.orElseThrow(() -> new CustomException(ErrorCode.ROOM_NOT_FOUND));
703712

@@ -710,8 +719,9 @@ public void kickMember(Long roomId, Long targetUserId, Long requesterId) {
710719
)
711720
);
712721

713-
log.info("멤버 추방 완료 - RoomId: {}, TargetUserId: {}, RequesterId: {}",
714-
roomId, targetUserId, requesterId);
722+
String targetRole = targetMemberOpt.map(m -> m.getRole().name()).orElse("VISITOR");
723+
log.info("멤버 추방 완료 - RoomId: {}, TargetUserId: {}, Role: {}, RequesterId: {}",
724+
roomId, targetUserId, targetRole, requesterId);
715725
}
716726

717727
// ==================== DTO 생성 헬퍼 메서드 ====================

src/test/java/com/back/domain/studyroom/service/RoomServiceTest.java

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,8 @@ void kickMember_Success() {
398398
RoomMember targetMember = RoomMember.createVisitor(testRoom, targetUser);
399399

400400
given(roomMemberRepository.findByRoomIdAndUserId(1L, 1L)).willReturn(Optional.of(hostMember));
401-
given(roomMemberRepository.findByRoomIdAndUserId(1L, 2L)).willReturn(Optional.of(targetMember));
401+
given(roomParticipantService.getParticipants(1L)).willReturn(java.util.Set.of(2L)); // 온라인 사용자 목록
402+
given(roomMemberRepository.findByRoomIdAndUserId(1L, 2L)).willReturn(Optional.empty()); // VISITOR는 DB에 없음
402403
given(roomRepository.findById(1L)).willReturn(Optional.of(testRoom));
403404

404405
// when
@@ -408,6 +409,94 @@ void kickMember_Success() {
408409
verify(roomParticipantService, times(1)).exitRoom(2L, 1L); // Redis 퇴장 확인
409410
}
410411

412+
@Test
413+
@DisplayName("멤버 추방 - VISITOR 추방 성공")
414+
void kickMember_VisitorSuccess() {
415+
// given
416+
RoomMember hostMember = RoomMember.createHost(testRoom, testUser);
417+
418+
given(roomMemberRepository.findByRoomIdAndUserId(1L, 1L)).willReturn(Optional.of(hostMember));
419+
given(roomParticipantService.getParticipants(1L)).willReturn(java.util.Set.of(2L)); // VISITOR가 온라인 상태
420+
given(roomMemberRepository.findByRoomIdAndUserId(1L, 2L)).willReturn(Optional.empty()); // DB에 없음
421+
given(roomRepository.findById(1L)).willReturn(Optional.of(testRoom));
422+
423+
// when
424+
roomService.kickMember(1L, 2L, 1L);
425+
426+
// then
427+
verify(roomParticipantService, times(1)).exitRoom(2L, 1L);
428+
}
429+
430+
@Test
431+
@DisplayName("멤버 추방 - MEMBER 추방 성공")
432+
void kickMember_MemberSuccess() {
433+
// given
434+
RoomMember hostMember = RoomMember.createHost(testRoom, testUser);
435+
436+
User targetUser = User.builder()
437+
.id(2L)
438+
.username("target")
439+
440+
.role(Role.USER)
441+
.build();
442+
UserProfile targetProfile = new UserProfile();
443+
targetProfile.setNickname("대상유저");
444+
targetUser.setUserProfile(targetProfile);
445+
446+
RoomMember targetMember = RoomMember.create(testRoom, targetUser, RoomRole.MEMBER);
447+
448+
given(roomMemberRepository.findByRoomIdAndUserId(1L, 1L)).willReturn(Optional.of(hostMember));
449+
given(roomParticipantService.getParticipants(1L)).willReturn(java.util.Set.of(2L)); // MEMBER가 온라인 상태
450+
given(roomMemberRepository.findByRoomIdAndUserId(1L, 2L)).willReturn(Optional.of(targetMember)); // DB에 있음
451+
given(roomRepository.findById(1L)).willReturn(Optional.of(testRoom));
452+
453+
// when
454+
roomService.kickMember(1L, 2L, 1L);
455+
456+
// then
457+
verify(roomParticipantService, times(1)).exitRoom(2L, 1L);
458+
}
459+
460+
@Test
461+
@DisplayName("멤버 추방 - 방장 추방 실패")
462+
void kickMember_CannotKickHost() {
463+
// given
464+
RoomMember hostMember = RoomMember.createHost(testRoom, testUser);
465+
466+
User anotherHost = User.builder()
467+
.id(2L)
468+
.username("host2")
469+
470+
.role(Role.USER)
471+
.build();
472+
473+
RoomMember targetHostMember = RoomMember.createHost(testRoom, anotherHost);
474+
475+
given(roomMemberRepository.findByRoomIdAndUserId(1L, 1L)).willReturn(Optional.of(hostMember));
476+
given(roomParticipantService.getParticipants(1L)).willReturn(java.util.Set.of(2L));
477+
given(roomMemberRepository.findByRoomIdAndUserId(1L, 2L)).willReturn(Optional.of(targetHostMember));
478+
479+
// when & then
480+
assertThatThrownBy(() -> roomService.kickMember(1L, 2L, 1L))
481+
.isInstanceOf(CustomException.class)
482+
.hasFieldOrPropertyWithValue("errorCode", ErrorCode.CANNOT_KICK_HOST);
483+
}
484+
485+
@Test
486+
@DisplayName("멤버 추방 - 오프라인 사용자 추방 실패")
487+
void kickMember_OfflineUser() {
488+
// given
489+
RoomMember hostMember = RoomMember.createHost(testRoom, testUser);
490+
491+
given(roomMemberRepository.findByRoomIdAndUserId(1L, 1L)).willReturn(Optional.of(hostMember));
492+
given(roomParticipantService.getParticipants(1L)).willReturn(java.util.Set.of()); // 온라인 사용자 없음
493+
494+
// when & then
495+
assertThatThrownBy(() -> roomService.kickMember(1L, 2L, 1L))
496+
.isInstanceOf(CustomException.class)
497+
.hasFieldOrPropertyWithValue("errorCode", ErrorCode.NOT_ROOM_MEMBER);
498+
}
499+
411500
@Test
412501
@DisplayName("멤버 추방 - 권한 없음")
413502
void kickMember_NoPermission() {

0 commit comments

Comments
 (0)