Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.back.domain.studyroom.dto;

import com.back.domain.studyroom.entity.RoomRole;
import lombok.Builder;
import lombok.Getter;

import java.time.LocalDateTime;

/**
* 역할 변경 WebSocket 알림 DTO
* - 방 멤버의 역할이 변경되었을 때 실시간 브로드캐스트
*/
@Getter
@Builder
public class RoleChangedNotification {

private Long roomId;
private Long userId;
private String nickname;
private String profileImageUrl;
private RoomRole oldRole;
private RoomRole newRole;
private String message;
private LocalDateTime timestamp;

public static RoleChangedNotification of(
Long roomId,
Long userId,
String nickname,
String profileImageUrl,
RoomRole oldRole,
RoomRole newRole) {

String message = buildMessage(nickname, oldRole, newRole);

return RoleChangedNotification.builder()
.roomId(roomId)
.userId(userId)
.nickname(nickname)
.profileImageUrl(profileImageUrl)
.oldRole(oldRole)
.newRole(newRole)
.message(message)
.timestamp(LocalDateTime.now())
.build();
}

private static String buildMessage(String nickname, RoomRole oldRole, RoomRole newRole) {
if (newRole == RoomRole.HOST) {
return String.format("%s님이 방장으로 임명되었습니다.", nickname);
} else if (oldRole == RoomRole.HOST) {
return String.format("%s님이 일반 멤버로 변경되었습니다.", nickname);
} else if (newRole == RoomRole.SUB_HOST) {
return String.format("%s님이 부방장으로 승격되었습니다.", nickname);
} else if (newRole == RoomRole.MEMBER && oldRole == RoomRole.VISITOR) {
return String.format("%s님이 정식 멤버로 승격되었습니다.", nickname);
} else if (newRole == RoomRole.MEMBER) {
return String.format("%s님이 일반 멤버로 변경되었습니다.", nickname);
}
return String.format("%s님의 역할이 변경되었습니다.", nickname);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,16 @@
import java.util.Set;

/**
* 방 상태 관리를 위한 Redis 전용 서비스
* 방의 온라인 사용자 관리 (입장/퇴장)
* 실시간 참가자 수 조회
* 온라인 사용자 목록 조회
* Redis: 실시간 온라인 상태만 관리 (휘발성 데이터)
* DB: 영구 멤버십 + 역할 정보 (MEMBER 이상만 저장)
* 역할(Role)은 Redis에 저장하지 않음!
* 이유 1: DB가 Single Source of Truth (데이터 일관성)
* 이유 2: Redis-DB 동기화 복잡도 제거
* 이유 3: 멤버 목록 조회 시 IN 절로 효율적 조회 가능
* 방 상태 관리를 위한 Redis 전용 서비스 (곧 사라질 예정인 파일)
* (현재는 일단 유지 시킨 상황, 에러 방지용)
* @deprecated RoomParticipantService를 사용.
* 현재는 WebSocketSessionManager의 Wrapper일 뿐이며,
* RoomParticipantService에 원래 로직이 옮겨졋습니다.
*
* @see com.back.global.websocket.service.RoomParticipantService 실제 사용 서비스
* @see com.back.global.websocket.service.WebSocketSessionManager WebSocket 세션 관리
* @see com.back.domain.studyroom.repository.RoomMemberRepository DB 멤버십 조회
*/
@Deprecated
@Slf4j
@Service
@RequiredArgsConstructor
Expand Down
69 changes: 52 additions & 17 deletions src/main/java/com/back/domain/studyroom/service/RoomService.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import com.back.domain.user.repository.UserRepository;
import com.back.global.exception.CustomException;
import com.back.global.exception.ErrorCode;
import com.back.global.websocket.service.RoomParticipantService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
Expand Down Expand Up @@ -41,7 +42,8 @@ public class RoomService {
private final RoomMemberRepository roomMemberRepository;
private final UserRepository userRepository;
private final StudyRoomProperties properties;
private final RoomRedisService roomRedisService;
private final RoomParticipantService roomParticipantService;
private final org.springframework.messaging.simp.SimpMessagingTemplate messagingTemplate;

/**
* 방 생성 메서드
Expand Down Expand Up @@ -109,7 +111,7 @@ public RoomMember joinRoom(Long roomId, String password, Long userId) {
}

// Redis에서 현재 온라인 사용자 수 조회
long currentOnlineCount = roomRedisService.getRoomUserCount(roomId);
long currentOnlineCount = roomParticipantService.getParticipantCount(roomId);

// 정원 확인 (Redis 기반)
if (currentOnlineCount >= room.getMaxParticipants()) {
Expand All @@ -133,7 +135,7 @@ public RoomMember joinRoom(Long roomId, String password, Long userId) {
RoomMember member = existingMember.get();

// Redis에 온라인 등록
roomRedisService.enterRoom(userId, roomId);
roomParticipantService.enterRoom(userId, roomId);

log.info("기존 멤버 재입장 - RoomId: {}, UserId: {}, Role: {}",
roomId, userId, member.getRole());
Expand All @@ -145,7 +147,7 @@ public RoomMember joinRoom(Long roomId, String password, Long userId) {
RoomMember visitorMember = RoomMember.createVisitor(room, user);

// Redis에만 온라인 등록
roomRedisService.enterRoom(userId, roomId);
roomParticipantService.enterRoom(userId, roomId);

log.info("신규 입장 (VISITOR) - RoomId: {}, UserId: {}, DB 저장 안함", roomId, userId);

Expand All @@ -172,7 +174,7 @@ public void leaveRoom(Long roomId, Long userId) {
.orElseThrow(() -> new CustomException(ErrorCode.ROOM_NOT_FOUND));

// Redis에서 퇴장 처리 (모든 사용자)
roomRedisService.exitRoom(userId, roomId);
roomParticipantService.exitRoom(userId, roomId);

log.info("방 퇴장 완료 - RoomId: {}, UserId: {}", roomId, userId);
}
Expand Down Expand Up @@ -234,9 +236,9 @@ public void terminateRoom(Long roomId, Long userId) {
room.terminate();

// Redis에서 모든 온라인 사용자 제거
Set<Long> onlineUserIds = roomRedisService.getRoomUsers(roomId);
Set<Long> onlineUserIds = roomParticipantService.getParticipants(roomId);
for (Long onlineUserId : onlineUserIds) {
roomRedisService.exitRoom(onlineUserId, roomId);
roomParticipantService.exitRoom(onlineUserId, roomId);
}

log.info("방 종료 완료 - RoomId: {}, UserId: {}, 퇴장 처리: {}명",
Expand Down Expand Up @@ -275,7 +277,10 @@ public void changeUserRole(Long roomId, Long targetUserId, RoomRole newRole, Lon

// 3. 대상자 확인 (DB 조회 - VISITOR는 DB에 없을 수 있음)
Optional<RoomMember> targetMemberOpt = roomMemberRepository.findByRoomIdAndUserId(roomId, targetUserId);


// 변경 전 역할 저장 (알림용)
RoomRole oldRole = targetMemberOpt.map(RoomMember::getRole).orElse(RoomRole.VISITOR);

// 4. HOST로 변경하는 경우 - 기존 방장 강등
if (newRole == RoomRole.HOST) {
// 기존 방장을 MEMBER로 강등
Expand Down Expand Up @@ -306,6 +311,28 @@ public void changeUserRole(Long roomId, Long targetUserId, RoomRole newRole, Lon
log.info("VISITOR 승격 (DB 저장) - RoomId: {}, UserId: {}, NewRole: {}",
roomId, targetUserId, newRole);
}

// 6. WebSocket으로 역할 변경 알림 브로드캐스트
User targetUser = userRepository.findById(targetUserId)
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));

com.back.domain.studyroom.dto.RoleChangedNotification notification =
com.back.domain.studyroom.dto.RoleChangedNotification.of(
roomId,
targetUserId,
targetUser.getNickname(),
targetUser.getProfileImageUrl(),
oldRole,
newRole
);

messagingTemplate.convertAndSend(
"/topic/room/" + roomId + "/role-changed",
notification
);

log.info("역할 변경 알림 전송 완료 - RoomId: {}, UserId: {}, {} → {}",
roomId, targetUserId, oldRole, newRole);
}

/**
Expand All @@ -332,7 +359,7 @@ public List<RoomMember> getRoomMembers(Long roomId, Long userId) {
}

// 1. Redis에서 온라인 사용자 ID 조회
Set<Long> onlineUserIds = roomRedisService.getRoomUsers(roomId);
Set<Long> onlineUserIds = roomParticipantService.getParticipants(roomId);

if (onlineUserIds.isEmpty()) {
return List.of();
Expand Down Expand Up @@ -412,7 +439,7 @@ public void kickMember(Long roomId, Long targetUserId, Long requesterId) {
}

// Redis에서 제거 (강제 퇴장)
roomRedisService.exitRoom(targetUserId, roomId);
roomParticipantService.exitRoom(targetUserId, roomId);

log.info("멤버 추방 완료 - RoomId: {}, TargetUserId: {}, RequesterId: {}",
roomId, targetUserId, requesterId);
Expand All @@ -424,7 +451,7 @@ public void kickMember(Long roomId, Long targetUserId, Long requesterId) {
* RoomResponse 생성 (Redis에서 실시간 참가자 수 조회)
*/
public com.back.domain.studyroom.dto.RoomResponse toRoomResponse(Room room) {
long onlineCount = roomRedisService.getRoomUserCount(room.getId());
long onlineCount = roomParticipantService.getParticipantCount(room.getId());
return com.back.domain.studyroom.dto.RoomResponse.from(room, onlineCount);
}

Expand All @@ -435,8 +462,12 @@ public java.util.List<com.back.domain.studyroom.dto.RoomResponse> toRoomResponse
java.util.List<Long> roomIds = rooms.stream()
.map(Room::getId)
.collect(java.util.stream.Collectors.toList());

java.util.Map<Long, Long> participantCounts = roomRedisService.getBulkRoomOnlineUserCounts(roomIds);

java.util.Map<Long, Long> participantCounts = roomIds.stream()
.collect(java.util.stream.Collectors.toMap(
roomId -> roomId,
roomId -> roomParticipantService.getParticipantCount(roomId)
));

return rooms.stream()
.map(room -> com.back.domain.studyroom.dto.RoomResponse.from(
Expand All @@ -452,7 +483,7 @@ public java.util.List<com.back.domain.studyroom.dto.RoomResponse> toRoomResponse
public com.back.domain.studyroom.dto.RoomDetailResponse toRoomDetailResponse(
Room room,
java.util.List<com.back.domain.studyroom.entity.RoomMember> members) {
long onlineCount = roomRedisService.getRoomUserCount(room.getId());
long onlineCount = roomParticipantService.getParticipantCount(room.getId());

java.util.List<com.back.domain.studyroom.dto.RoomMemberResponse> memberResponses = members.stream()
.map(com.back.domain.studyroom.dto.RoomMemberResponse::from)
Expand All @@ -465,7 +496,7 @@ public com.back.domain.studyroom.dto.RoomDetailResponse toRoomDetailResponse(
* MyRoomResponse 생성 (Redis에서 실시간 참가자 수 조회)
*/
public com.back.domain.studyroom.dto.MyRoomResponse toMyRoomResponse(Room room, RoomRole myRole) {
long onlineCount = roomRedisService.getRoomUserCount(room.getId());
long onlineCount = roomParticipantService.getParticipantCount(room.getId());
return com.back.domain.studyroom.dto.MyRoomResponse.of(room, onlineCount, myRole);
}

Expand All @@ -478,8 +509,12 @@ public java.util.List<com.back.domain.studyroom.dto.MyRoomResponse> toMyRoomResp
java.util.List<Long> roomIds = rooms.stream()
.map(Room::getId)
.collect(java.util.stream.Collectors.toList());

java.util.Map<Long, Long> participantCounts = roomRedisService.getBulkRoomOnlineUserCounts(roomIds);

java.util.Map<Long, Long> participantCounts = roomIds.stream()
.collect(java.util.stream.Collectors.toMap(
roomId -> roomId,
roomId -> roomParticipantService.getParticipantCount(roomId)
));

return rooms.stream()
.map(room -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import com.back.domain.user.repository.UserRepository;
import com.back.global.exception.CustomException;
import com.back.global.exception.ErrorCode;
import com.back.global.websocket.service.RoomParticipantService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -48,7 +49,7 @@ class RoomServiceTest {
private StudyRoomProperties properties;

@Mock
private RoomRedisService roomRedisService;
private RoomParticipantService roomParticipantService;

@InjectMocks
private RoomService roomService;
Expand Down Expand Up @@ -144,15 +145,15 @@ void joinRoom_Success() {
given(roomRepository.findByIdWithLock(1L)).willReturn(Optional.of(testRoom));
given(userRepository.findById(2L)).willReturn(Optional.of(testUser));
given(roomMemberRepository.findByRoomIdAndUserId(1L, 2L)).willReturn(Optional.empty());
given(roomRedisService.getRoomUserCount(1L)).willReturn(0L); // Redis 카운트
given(roomParticipantService.getParticipantCount(1L)).willReturn(0L); // Redis 카운트

// when
RoomMember joinedMember = roomService.joinRoom(1L, null, 2L);

// then
assertThat(joinedMember).isNotNull();
assertThat(joinedMember.getRole()).isEqualTo(RoomRole.VISITOR);
verify(roomRedisService, times(1)).enterRoom(2L, 1L); // Redis 입장 확인
verify(roomParticipantService, times(1)).enterRoom(2L, 1L); // Redis 입장 확인
verify(roomMemberRepository, never()).save(any(RoomMember.class)); // DB 저장 안됨!
}

Expand Down Expand Up @@ -183,7 +184,7 @@ void joinRoom_WrongPassword() {
true // useWebRTC
);
given(roomRepository.findByIdWithLock(1L)).willReturn(Optional.of(privateRoom));
given(roomRedisService.getRoomUserCount(1L)).willReturn(0L); // Redis 카운트
given(roomParticipantService.getParticipantCount(1L)).willReturn(0L); // Redis 카운트

// when & then
assertThatThrownBy(() -> roomService.joinRoom(1L, "wrong", 1L))
Expand All @@ -201,7 +202,7 @@ void leaveRoom_Success() {
roomService.leaveRoom(1L, 1L);

// then
verify(roomRedisService, times(1)).exitRoom(1L, 1L); // Redis 퇴장 확인
verify(roomParticipantService, times(1)).exitRoom(1L, 1L); // Redis 퇴장 확인
}

@Test
Expand Down Expand Up @@ -312,7 +313,7 @@ void updateRoomSettings_NotOwner() {
void terminateRoom_Success() {
// given
given(roomRepository.findById(1L)).willReturn(Optional.of(testRoom));
given(roomRedisService.getRoomUsers(1L)).willReturn(java.util.Set.of()); // 온라인 사용자 없음
given(roomParticipantService.getParticipants(1L)).willReturn(java.util.Set.of()); // 온라인 사용자 없음

// when
roomService.terminateRoom(1L, 1L);
Expand Down Expand Up @@ -378,7 +379,7 @@ void kickMember_Success() {
roomService.kickMember(1L, 2L, 1L);

// then
verify(roomRedisService, times(1)).exitRoom(2L, 1L); // Redis 퇴장 확인
verify(roomParticipantService, times(1)).exitRoom(2L, 1L); // Redis 퇴장 확인
}

@Test
Expand Down