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
Expand Up @@ -3,7 +3,9 @@
import com.back.domain.studyroom.dto.*;
import com.back.domain.studyroom.entity.Room;
import com.back.domain.studyroom.entity.RoomMember;
import com.back.domain.studyroom.entity.RoomRole;
import com.back.domain.studyroom.service.RoomService;
import com.back.domain.user.entity.User;
import com.back.global.common.dto.RsData;
import com.back.global.security.user.CurrentUser;
import io.swagger.v3.oas.annotations.Operation;
Expand Down Expand Up @@ -317,4 +319,44 @@ public ResponseEntity<RsData<Map<String, Object>>> getPopularRooms(
.status(HttpStatus.OK)
.body(RsData.success("์ธ๊ธฐ ๋ฐฉ ๋ชฉ๋ก ์กฐํšŒ ์™„๋ฃŒ", response));
}

@PutMapping("/{roomId}/members/{userId}/role")
@Operation(
summary = "๋ฉค๋ฒ„ ์—ญํ•  ๋ณ€๊ฒฝ",
description = "๋ฐฉ ๋ฉค๋ฒ„์˜ ์—ญํ• ์„ ๋ณ€๊ฒฝํ•ฉ๋‹ˆ๋‹ค. ๋ฐฉ์žฅ๋งŒ ์‹คํ–‰ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. VISITOR๋ฅผ ํฌํ•จํ•œ ๋ชจ๋“  ์‚ฌ์šฉ์ž์˜ ์—ญํ• ์„ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, HOST๋กœ ๋ณ€๊ฒฝ ์‹œ ๊ธฐ์กด ๋ฐฉ์žฅ์€ ์ž๋™์œผ๋กœ MEMBER๋กœ ๊ฐ•๋“ฑ๋ฉ๋‹ˆ๋‹ค."
)
@ApiResponses({
@ApiResponse(responseCode = "200", description = "์—ญํ•  ๋ณ€๊ฒฝ ์„ฑ๊ณต"),
@ApiResponse(responseCode = "400", description = "์ž์‹ ์˜ ์—ญํ• ์€ ๋ณ€๊ฒฝ ๋ถˆ๊ฐ€"),
@ApiResponse(responseCode = "403", description = "๋ฐฉ์žฅ ๊ถŒํ•œ ์—†์Œ"),
@ApiResponse(responseCode = "404", description = "์กด์žฌํ•˜์ง€ ์•Š๋Š” ๋ฐฉ ๋˜๋Š” ์‚ฌ์šฉ์ž"),
@ApiResponse(responseCode = "401", description = "์ธ์ฆ ์‹คํŒจ")
})
public ResponseEntity<RsData<ChangeRoleResponse>> changeUserRole(
@Parameter(description = "๋ฐฉ ID", required = true) @PathVariable Long roomId,
@Parameter(description = "๋Œ€์ƒ ์‚ฌ์šฉ์ž ID", required = true) @PathVariable Long userId,
@Valid @RequestBody ChangeRoleRequest request) {

Long currentUserId = currentUser.getUserId();

// ๋ณ€๊ฒฝ ์ „ ์—ญํ•  ์กฐํšŒ
RoomRole oldRole = roomService.getUserRoomRole(roomId, userId);

// ์—ญํ•  ๋ณ€๊ฒฝ
roomService.changeUserRole(roomId, userId, request.getNewRole(), currentUserId);

// ์‚ฌ์šฉ์ž ์ •๋ณด ์กฐํšŒ
User targetUser = roomService.getUserById(userId);

ChangeRoleResponse response = ChangeRoleResponse.of(
userId,
targetUser.getNickname(),
oldRole,
request.getNewRole()
);

return ResponseEntity
.status(HttpStatus.OK)
.body(RsData.success("์—ญํ•  ๋ณ€๊ฒฝ ์™„๋ฃŒ", response));
}
}
21 changes: 21 additions & 0 deletions src/main/java/com/back/domain/studyroom/dto/ChangeRoleRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.back.domain.studyroom.dto;

import com.back.domain.studyroom.entity.RoomRole;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

/**
* ๋ฉค๋ฒ„ ์—ญํ•  ๋ณ€๊ฒฝ ์š”์ฒญ DTO
* - VISITOR โ†’ MEMBER/SUB_HOST/HOST ๋ชจ๋‘ ๊ฐ€๋Šฅ
* - HOST๋กœ ๋ณ€๊ฒฝ ์‹œ ๊ธฐ์กด ๋ฐฉ์žฅ์€ ์ž๋™์œผ๋กœ MEMBER๋กœ ๊ฐ•๋“ฑ
*/
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class ChangeRoleRequest {

@NotNull(message = "์—ญํ• ์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค")
private RoomRole newRole;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.back.domain.studyroom.dto;

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

/**
* ์—ญํ•  ๋ณ€๊ฒฝ ์‘๋‹ต DTO
*/
@Getter
@Builder
public class ChangeRoleResponse {

private Long userId;
private String nickname;
private RoomRole oldRole;
private RoomRole newRole;
private String message;

public static ChangeRoleResponse of(Long userId, String nickname,
RoomRole oldRole, RoomRole newRole) {
String message = buildMessage(oldRole, newRole);

return ChangeRoleResponse.builder()
.userId(userId)
.nickname(nickname)
.oldRole(oldRole)
.newRole(newRole)
.message(message)
.build();
}

private static String buildMessage(RoomRole oldRole, RoomRole newRole) {
if (newRole == RoomRole.HOST) {
return "๋ฐฉ์žฅ์œผ๋กœ ์ž„๋ช…๋˜์—ˆ์Šต๋‹ˆ๋‹ค.";
} else if (oldRole == RoomRole.HOST) {
return "๋ฐฉ์žฅ ๊ถŒํ•œ์ด ํ•ด์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.";
} else if (newRole == RoomRole.SUB_HOST) {
return "๋ถ€๋ฐฉ์žฅ์œผ๋กœ ์Šน๊ฒฉ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.";
} else if (newRole == RoomRole.MEMBER && oldRole == RoomRole.VISITOR) {
return "์ •์‹ ๋ฉค๋ฒ„๋กœ ์Šน๊ฒฉ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.";
} else if (newRole == RoomRole.MEMBER) {
return "์ผ๋ฐ˜ ๋ฉค๋ฒ„๋กœ ๊ฐ•๋“ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.";
}
return "์—ญํ• ์ด ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,15 @@ public interface RoomMemberRepositoryCustom {
*/
boolean existsByRoomIdAndUserId(Long roomId, Long userId);

/**
* ์—ฌ๋Ÿฌ ์‚ฌ์šฉ์ž์˜ ๋ฉค๋ฒ„์‹ญ ์ผ๊ด„ ์กฐํšŒ (IN ์ ˆ)
* Redis์—์„œ ์˜จ๋ผ์ธ ์‚ฌ์šฉ์ž ๋ชฉ๋ก์„ ๋ฐ›์•„์„œ DB ๋ฉค๋ฒ„์‹ญ ์กฐํšŒ ์‹œ ์‚ฌ์šฉ
* @param roomId ๋ฐฉ ID
* @param userIds ์‚ฌ์šฉ์ž ID ๋ชฉ๋ก
* @return ๋ฉค๋ฒ„์‹ญ ๋ชฉ๋ก (MEMBER ์ด์ƒ๋งŒ DB์— ์žˆ์Œ)
*/
List<RoomMember> findByRoomIdAndUserIdIn(Long roomId, java.util.Set<Long> userIds);

/**
* ํŠน์ • ์—ญํ• ์˜ ๋ฉค๋ฒ„ ์ˆ˜ ์กฐํšŒ
* TODO: Redis ๊ธฐ๋ฐ˜์œผ๋กœ ๋ณ€๊ฒฝ ์˜ˆ์ •
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,35 @@ public boolean existsByRoomIdAndUserId(Long roomId, Long userId) {
return count != null && count > 0;
}

/**
* ์—ฌ๋Ÿฌ ์‚ฌ์šฉ์ž์˜ ๋ฉค๋ฒ„์‹ญ ์ผ๊ด„ ์กฐํšŒ (IN ์ ˆ)
* - Redis ์˜จ๋ผ์ธ ๋ชฉ๋ก์œผ๋กœ DB ๋ฉค๋ฒ„์‹ญ ์กฐํšŒ
* - N+1 ๋ฌธ์ œ ํ•ด๊ฒฐ
* - VISITOR๋Š” DB์— ์—†์œผ๋ฏ€๋กœ ๊ฒฐ๊ณผ์— ํฌํ•จ ์•ˆ๋จ
* @param roomId ๋ฐฉ ID
* @param userIds ์‚ฌ์šฉ์ž ID Set
* @return DB์— ์ €์žฅ๋œ ๋ฉค๋ฒ„ ๋ชฉ๋ก (MEMBER ์ด์ƒ)
*/
@Override
public List<RoomMember> findByRoomIdAndUserIdIn(Long roomId, java.util.Set<Long> userIds) {
if (userIds == null || userIds.isEmpty()) {
return List.of();
}

return queryFactory
.selectFrom(roomMember)
.leftJoin(roomMember.user, user).fetchJoin() // N+1 ๋ฐฉ์ง€
.where(
roomMember.room.id.eq(roomId),
roomMember.user.id.in(userIds)
)
.orderBy(
roomMember.role.asc(), // ์—ญํ• ์ˆœ
roomMember.joinedAt.asc() // ์ž…์žฅ ์‹œ๊ฐ„์ˆœ
)
.fetch();
}

/**
* ํŠน์ • ์—ญํ• ์˜ ๋ฉค๋ฒ„ ์ˆ˜ ์กฐํšŒ
* TODO: Redis ๊ธฐ๋ฐ˜์œผ๋กœ ๋ณ€๊ฒฝ ์˜ˆ์ •
Expand Down
116 changes: 116 additions & 0 deletions src/main/java/com/back/domain/studyroom/service/RoomRedisService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package com.back.domain.studyroom.service;

import com.back.global.websocket.service.WebSocketSessionManager;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.util.Map;
import java.util.Set;

/**
* ๋ฐฉ ์ƒํƒœ ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•œ Redis ์ „์šฉ ์„œ๋น„์Šค
* ๋ฐฉ์˜ ์˜จ๋ผ์ธ ์‚ฌ์šฉ์ž ๊ด€๋ฆฌ (์ž…์žฅ/ํ‡ด์žฅ)
* ์‹ค์‹œ๊ฐ„ ์ฐธ๊ฐ€์ž ์ˆ˜ ์กฐํšŒ
* ์˜จ๋ผ์ธ ์‚ฌ์šฉ์ž ๋ชฉ๋ก ์กฐํšŒ
* Redis: ์‹ค์‹œ๊ฐ„ ์˜จ๋ผ์ธ ์ƒํƒœ๋งŒ ๊ด€๋ฆฌ (ํœ˜๋ฐœ์„ฑ ๋ฐ์ดํ„ฐ)
* DB: ์˜๊ตฌ ๋ฉค๋ฒ„์‹ญ + ์—ญํ•  ์ •๋ณด (MEMBER ์ด์ƒ๋งŒ ์ €์žฅ)
* ์—ญํ• (Role)์€ Redis์— ์ €์žฅํ•˜์ง€ ์•Š์Œ!
* ์ด์œ  1: DB๊ฐ€ Single Source of Truth (๋ฐ์ดํ„ฐ ์ผ๊ด€์„ฑ)
* ์ด์œ  2: Redis-DB ๋™๊ธฐํ™” ๋ณต์žก๋„ ์ œ๊ฑฐ
* ์ด์œ  3: ๋ฉค๋ฒ„ ๋ชฉ๋ก ์กฐํšŒ ์‹œ IN ์ ˆ๋กœ ํšจ์œจ์  ์กฐํšŒ ๊ฐ€๋Šฅ
* @see com.back.global.websocket.service.WebSocketSessionManager WebSocket ์„ธ์…˜ ๊ด€๋ฆฌ
* @see com.back.domain.studyroom.repository.RoomMemberRepository DB ๋ฉค๋ฒ„์‹ญ ์กฐํšŒ
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class RoomRedisService {

private final WebSocketSessionManager sessionManager;

// ==================== ๋ฐฉ ์ž…์žฅ/ํ‡ด์žฅ ====================

/**
* ์‚ฌ์šฉ์ž๊ฐ€ ๋ฐฉ์— ์ž…์žฅ (Redis ์˜จ๋ผ์ธ ์ƒํƒœ ์—…๋ฐ์ดํŠธ)
* - Redis Set์— userId ์ถ”๊ฐ€
* - ์—ญํ• (Role)์€ DB์—์„œ๋งŒ ๊ด€๋ฆฌ
*
* @param userId ์‚ฌ์šฉ์ž ID
* @param roomId ๋ฐฉ ID
*/
public void enterRoom(Long userId, Long roomId) {
sessionManager.joinRoom(userId, roomId);
log.info("๋ฐฉ ์ž…์žฅ ์™„๋ฃŒ (Redis) - ์‚ฌ์šฉ์ž: {}, ๋ฐฉ: {}", userId, roomId);
}

/**
* ์‚ฌ์šฉ์ž๊ฐ€ ๋ฐฉ์—์„œ ํ‡ด์žฅ (Redis ์˜จ๋ผ์ธ ์ƒํƒœ ์—…๋ฐ์ดํŠธ)
* - Redis Set์—์„œ userId ์ œ๊ฑฐ
* - DB ๋ฉค๋ฒ„์‹ญ์€ ์œ ์ง€๋จ (์žฌ์ž…์žฅ ์‹œ ์—ญํ•  ์œ ์ง€)
*
* @param userId ์‚ฌ์šฉ์ž ID
* @param roomId ๋ฐฉ ID
*/
public void exitRoom(Long userId, Long roomId) {
sessionManager.leaveRoom(userId, roomId);
log.info("๋ฐฉ ํ‡ด์žฅ ์™„๋ฃŒ (Redis) - ์‚ฌ์šฉ์ž: {}, ๋ฐฉ: {}", userId, roomId);
}

// ==================== ์กฐํšŒ ====================

/**
* ๋ฐฉ์˜ ํ˜„์žฌ ์˜จ๋ผ์ธ ์‚ฌ์šฉ์ž ์ˆ˜ ์กฐํšŒ
* - ์‹ค์‹œ๊ฐ„ ์ฐธ๊ฐ€์ž ์ˆ˜ (DB currentParticipants์™€ ๋ฌด๊ด€)
*
* @param roomId ๋ฐฉ ID
* @return ์˜จ๋ผ์ธ ์‚ฌ์šฉ์ž ์ˆ˜
*/
public long getRoomUserCount(Long roomId) {
return sessionManager.getRoomOnlineUserCount(roomId);
}

/**
* ๋ฐฉ์˜ ์˜จ๋ผ์ธ ์‚ฌ์šฉ์ž ID ๋ชฉ๋ก ์กฐํšŒ
* - ๋ฉค๋ฒ„ ๋ชฉ๋ก ์กฐํšŒ ์‹œ ์ด ID๋กœ DB ์กฐํšŒ
* - DB์— ์—†๋Š” ID = VISITOR
*
* @param roomId ๋ฐฉ ID
* @return ์˜จ๋ผ์ธ ์‚ฌ์šฉ์ž ID Set
*/
public Set<Long> getRoomUsers(Long roomId) {
return sessionManager.getOnlineUsersInRoom(roomId);
}

/**
* ์‚ฌ์šฉ์ž๊ฐ€ ํ˜„์žฌ ํŠน์ • ๋ฐฉ์— ์žˆ๋Š”์ง€ ํ™•์ธ
*
* @param userId ์‚ฌ์šฉ์ž ID
* @param roomId ๋ฐฉ ID
* @return ์˜จ๋ผ์ธ ์—ฌ๋ถ€
*/
public boolean isUserInRoom(Long userId, Long roomId) {
return sessionManager.isUserInRoom(userId, roomId);
}

/**
* ์‚ฌ์šฉ์ž์˜ ํ˜„์žฌ ๋ฐฉ ID ์กฐํšŒ
*
* @param userId ์‚ฌ์šฉ์ž ID
* @return ๋ฐฉ ID (์—†์œผ๋ฉด null)
*/
public Long getCurrentRoomId(Long userId) {
return sessionManager.getUserCurrentRoomId(userId);
}

/**
* ์—ฌ๋Ÿฌ ๋ฐฉ์˜ ์˜จ๋ผ์ธ ์‚ฌ์šฉ์ž ์ˆ˜ ์ผ๊ด„ ์กฐํšŒ (N+1 ๋ฐฉ์ง€)
* - ๋ฐฉ ๋ชฉ๋ก ์กฐํšŒ ์‹œ ์‚ฌ์šฉ
*
* @param roomIds ๋ฐฉ ID ๋ชฉ๋ก
* @return Map<RoomId, OnlineCount>
*/
public Map<Long, Long> getBulkRoomOnlineUserCounts(java.util.List<Long> roomIds) {
return sessionManager.getBulkRoomOnlineUserCounts(roomIds);
}
}
Loading