diff --git a/build.gradle b/build.gradle
index 23488ef..2b46879 100644
--- a/build.gradle
+++ b/build.gradle
@@ -6,6 +6,12 @@ plugins {
id 'checkstyle'
}
+configurations {
+ all {
+ exclude group: 'commons-logging', module: 'commons-logging'
+ }
+}
+
editorconfig {
excludes = ['build']
}
diff --git a/config/naver-checkstyle-rules.xml b/config/naver-checkstyle-rules.xml
index 1d93b2f..06d3541 100644
--- a/config/naver-checkstyle-rules.xml
+++ b/config/naver-checkstyle-rules.xml
@@ -221,11 +221,11 @@ The following rules in the Naver coding convention cannot be checked by this con
value="[sub-flow-after-brace] ''{0}'' at column {1} should have line break before."/>
-
-
-
-
+
+
+
+
+
diff --git a/src/main/java/capstone/relation/api/auth/jwt/SecurityUser.java b/src/main/java/capstone/relation/api/auth/jwt/SecurityUser.java
index 022c337..84be3de 100644
--- a/src/main/java/capstone/relation/api/auth/jwt/SecurityUser.java
+++ b/src/main/java/capstone/relation/api/auth/jwt/SecurityUser.java
@@ -72,7 +72,7 @@ public Long getUserId() {
return userId;
}
- private SecurityUser(Long userId, Role role) {
+ public SecurityUser(Long userId, Role role) {
this.userId = userId;
this.role = role;
}
diff --git a/src/main/java/capstone/relation/api/auth/jwt/TokenProvider.java b/src/main/java/capstone/relation/api/auth/jwt/TokenProvider.java
index 7b62a81..411fb64 100644
--- a/src/main/java/capstone/relation/api/auth/jwt/TokenProvider.java
+++ b/src/main/java/capstone/relation/api/auth/jwt/TokenProvider.java
@@ -122,8 +122,7 @@ public String getWorkSpaceIdByInviteCode(String inviteCode) {
}
}
- private String generateAccessToken(User user, Date accessTokenExpiredDate) {
-
+ public String generateAccessToken(User user, Date accessTokenExpiredDate) {
return Jwts.builder()
.setSubject(String.valueOf(user.getId()))
.claim(jwtProperties.getAuthorityKey(), user.getRole().toString())
diff --git a/src/main/java/capstone/relation/common/error/GlobalExceptionHandler.java b/src/main/java/capstone/relation/common/error/GlobalExceptionHandler.java
index cdcbe7c..e0cb62f 100644
--- a/src/main/java/capstone/relation/common/error/GlobalExceptionHandler.java
+++ b/src/main/java/capstone/relation/common/error/GlobalExceptionHandler.java
@@ -18,6 +18,17 @@ public ResponseEntity handleResponseStatusException(ResponseStatusExcept
return new ResponseEntity<>(ex.getReason(), ex.getStatusCode());
}
+ @ExceptionHandler(IllegalArgumentException.class)
+ public ResponseEntity handleIllegalArgumentException(IllegalArgumentException ex,
+ HttpServletRequest request) {
+ ProblemDetail problemDetail = ProblemDetailCreator.create(ex, request, HttpStatus.BAD_REQUEST);
+ problemDetail.setTitle("Invalid Argument");
+ problemDetail.setStatus(HttpStatus.BAD_REQUEST.value());
+ problemDetail.setDetail(ex.getMessage());
+
+ return ResponseEntity.badRequest().body(problemDetail);
+ }
+
@ExceptionHandler(Exception.class)
public ResponseEntity handleAllException(Exception ex, HttpServletRequest request) {
ProblemDetail problemDetail = ProblemDetailCreator.create(ex, request, HttpStatus.BAD_REQUEST);
diff --git a/src/main/java/capstone/relation/common/initializer/CustomInitEvent.java b/src/main/java/capstone/relation/common/initializer/CustomInitEvent.java
index 3bbdaee..57cca16 100644
--- a/src/main/java/capstone/relation/common/initializer/CustomInitEvent.java
+++ b/src/main/java/capstone/relation/common/initializer/CustomInitEvent.java
@@ -1,7 +1,9 @@
package capstone.relation.common.initializer;
import org.springframework.context.ApplicationEvent;
+import org.springframework.context.annotation.Profile;
+@Profile("dev")
public class CustomInitEvent extends ApplicationEvent {
// 여기에 추가 데이터 필드나 메서드를 정의할 수 있습니다.
diff --git a/src/main/java/capstone/relation/common/initializer/DatabaseInitializer.java b/src/main/java/capstone/relation/common/initializer/DatabaseInitializer.java
index 501226a..e21e504 100644
--- a/src/main/java/capstone/relation/common/initializer/DatabaseInitializer.java
+++ b/src/main/java/capstone/relation/common/initializer/DatabaseInitializer.java
@@ -2,6 +2,7 @@
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationListener;
+import org.springframework.context.annotation.Profile;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Component;
@@ -9,6 +10,7 @@
@Component
@RequiredArgsConstructor
+@Profile("dev")
public class DatabaseInitializer implements ApplicationListener {
private final ApplicationEventPublisher eventPublisher;
diff --git a/src/main/java/capstone/relation/common/util/SecurityUtil.java b/src/main/java/capstone/relation/common/util/SecurityUtil.java
new file mode 100644
index 0000000..3ff8392
--- /dev/null
+++ b/src/main/java/capstone/relation/common/util/SecurityUtil.java
@@ -0,0 +1,30 @@
+package capstone.relation.common.util;
+
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.userdetails.UserDetails;
+
+import capstone.relation.api.auth.jwt.SecurityUser;
+
+public class SecurityUtil {
+
+ private SecurityUtil() {
+ throw new IllegalStateException("Utility class");
+ }
+
+ public static UserDetails getCurrentUserDetails() {
+ Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+ if (authentication != null && authentication.getPrincipal() instanceof UserDetails) {
+ return (UserDetails)authentication.getPrincipal();
+ }
+ throw new IllegalStateException("User not authenticated");
+ }
+
+ public static Long getCurrentUserId() {
+ UserDetails userDetails = getCurrentUserDetails();
+ if (userDetails instanceof SecurityUser) { // Assuming SecurityUser extends UserDetails
+ return ((SecurityUser)userDetails).getUserId();
+ }
+ throw new IllegalStateException("UserDetails does not contain userId");
+ }
+}
diff --git a/src/main/java/capstone/relation/meeting/controller/MeetRoomController.java b/src/main/java/capstone/relation/meeting/controller/MeetRoomController.java
index fc1d7c3..f1e3dba 100644
--- a/src/main/java/capstone/relation/meeting/controller/MeetRoomController.java
+++ b/src/main/java/capstone/relation/meeting/controller/MeetRoomController.java
@@ -1,20 +1,17 @@
package capstone.relation.meeting.controller;
-import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
-import org.springframework.web.server.ResponseStatusException;
+import capstone.relation.common.util.SecurityUtil;
import capstone.relation.meeting.dto.request.CreateRoomDto;
import capstone.relation.meeting.dto.response.JoinResponseDto;
import capstone.relation.meeting.dto.response.MeetingRoomListDto;
import capstone.relation.meeting.service.MeetRoomService;
-import capstone.relation.user.UserService;
-import capstone.relation.user.domain.User;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
@@ -25,31 +22,20 @@
@RequiredArgsConstructor
public class MeetRoomController {
private final MeetRoomService meetRoomService;
- private final UserService userService;
@PostMapping("/create")
@Operation(summary = "회의방 생성", description =
"새로운 회의방을 생성합니다. 생성된 회의방은 `/topic/{workSpaceId}/meetingRoomList`로 생성된 방에 대한 목록 발송이 이루어집니다.\n"
+ "방 생성자는 자동으로 방에 참여합니다.\n"
)
- public JoinResponseDto createRoom(@RequestBody CreateRoomDto createRoomDto) throws Exception {
- User user = userService.getUserEntity();
- try {
- return meetRoomService.createAndJoinRoom(createRoomDto, user.getId(), user.getWorkSpace().getId());
- } catch (Exception e) {
- throw new ResponseStatusException(HttpStatus.BAD_REQUEST, e.getMessage());
- }
+ public JoinResponseDto createRoom(@RequestBody CreateRoomDto createRoomDto) {
+ return meetRoomService.createAndJoinRoom(createRoomDto);
}
@PostMapping("/join/{roomId}")
@Operation(summary = "회의방 참여", description = "회의방에 참여합니다.")
public JoinResponseDto joinRoom(@PathVariable Long roomId) {
- User user = userService.getUserEntity();
- try {
- return meetRoomService.joinRoom(user.getId(), user.getWorkSpace().getId(), roomId);
- } catch (Exception e) {
- throw new ResponseStatusException(HttpStatus.BAD_REQUEST, e.getMessage());
- }
+ return meetRoomService.joinRoom(roomId);
}
@GetMapping("/list")
@@ -57,14 +43,12 @@ public JoinResponseDto joinRoom(@PathVariable Long roomId) {
+ "이것에 대한 응답은 `/topic/{workSpaceId}/meetingRoomList`로 이루어집니다.\n"
)
public MeetingRoomListDto requestRoomList() {
- User user = userService.getUserEntity();
- return meetRoomService.sendRoomList(user.getWorkSpace().getId());
+ return meetRoomService.sendRoomList();
}
@PostMapping("/leave")
@Operation(summary = "회의방 나가기", description = "현재 참여중인 회의방을 나갑니다.")
public void leaveRoom() {
- User user = userService.getUserEntity();
- meetRoomService.leaveRoom(user.getId(), user.getWorkSpace().getId());
+ meetRoomService.leaveRoom(SecurityUtil.getCurrentUserId());
}
}
diff --git a/src/main/java/capstone/relation/meeting/dto/response/JoinResponseDto.java b/src/main/java/capstone/relation/meeting/dto/response/JoinResponseDto.java
index 0c78c71..a83523b 100644
--- a/src/main/java/capstone/relation/meeting/dto/response/JoinResponseDto.java
+++ b/src/main/java/capstone/relation/meeting/dto/response/JoinResponseDto.java
@@ -5,10 +5,12 @@
import capstone.relation.user.dto.UserInfoDto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
+import lombok.Builder;
import lombok.Data;
@Data
@AllArgsConstructor
+@Builder
public class JoinResponseDto {
@Schema(description = "회의실 ID", example = "123")
private Long roomId;
diff --git a/src/main/java/capstone/relation/meeting/repository/RedisRepository.java b/src/main/java/capstone/relation/meeting/repository/RedisRepository.java
new file mode 100644
index 0000000..6df6288
--- /dev/null
+++ b/src/main/java/capstone/relation/meeting/repository/RedisRepository.java
@@ -0,0 +1,83 @@
+package capstone.relation.meeting.repository;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.springframework.data.redis.core.HashOperations;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Repository;
+
+import jakarta.annotation.PostConstruct;
+import lombok.RequiredArgsConstructor;
+
+@Repository
+@RequiredArgsConstructor
+public class RedisRepository {
+ private static final String WORK_KEY = "WORKSPACE_ROOM_PARTICIPANTS";
+ private static final String USER_KEY = "USER_ROOM_MAPPING";
+ private final RedisTemplate redisTemplate;
+ private HashOperations>> workspaceRoomParticipants;
+ private HashOperations userRoomMapping;
+
+ @PostConstruct
+ protected void init() {
+ workspaceRoomParticipants = redisTemplate.opsForHash();
+ userRoomMapping = redisTemplate.opsForHash();
+ }
+
+ public boolean isUserInRoom(Long userId) {
+ return userRoomMapping.get(USER_KEY, userId.toString()) != null;
+ }
+
+ public String getUserRoomId(Long userId) {
+ return userRoomMapping.get(USER_KEY, userId.toString());
+ }
+
+ public void addUserToRoom(String workspaceId, Long roomId, String userId) {
+ HashMap> roomParticipants = workspaceRoomParticipants.get(WORK_KEY, workspaceId);
+ if (roomParticipants == null)
+ roomParticipants = new HashMap<>();
+ Set users = roomParticipants.get(roomId.toString());
+ if (users == null)
+ users = new HashSet<>();
+
+ users.add(userId);
+ roomParticipants.put(roomId.toString(), users);
+ workspaceRoomParticipants.put(WORK_KEY, workspaceId, roomParticipants);
+ userRoomMapping.put(USER_KEY, userId, roomId.toString());
+ }
+
+ public void removeUserFromRoom(String workspaceId, Long roomId, String userId) {
+ userRoomMapping.delete(USER_KEY, userId);
+ HashMap> roomParticipants = workspaceRoomParticipants.get(WORK_KEY, workspaceId);
+ if (roomParticipants == null)
+ return;
+ Set userIds = roomParticipants.get(roomId.toString());
+ userIds.remove(userId);
+
+ if (userIds.isEmpty())
+ roomParticipants.remove(roomId.toString());
+ else
+ roomParticipants.put(roomId.toString(), userIds);
+
+ if (roomParticipants.isEmpty())
+ workspaceRoomParticipants.delete(WORK_KEY, workspaceId);
+ else
+ workspaceRoomParticipants.put(WORK_KEY, workspaceId, roomParticipants);
+
+ }
+
+ public Set getRoomMemberIds(String workspaceId, Long roomId) {
+ HashMap> roomParticipants = workspaceRoomParticipants.get(WORK_KEY, workspaceId);
+ if (roomParticipants == null) {
+ return new HashSet<>();
+ }
+ return roomParticipants.get(roomId.toString());
+ }
+
+ public void deleteAll() {
+ redisTemplate.delete(WORK_KEY);
+ redisTemplate.delete(USER_KEY);
+ }
+}
diff --git a/src/main/java/capstone/relation/meeting/service/MeetRoomService.java b/src/main/java/capstone/relation/meeting/service/MeetRoomService.java
index 6dcff03..c609d84 100644
--- a/src/main/java/capstone/relation/meeting/service/MeetRoomService.java
+++ b/src/main/java/capstone/relation/meeting/service/MeetRoomService.java
@@ -1,244 +1,206 @@
package capstone.relation.meeting.service;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.HashSet;
import java.util.List;
import java.util.Set;
-import org.springframework.data.redis.core.HashOperations;
-import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpStatus;
-import org.springframework.http.ResponseEntity;
-import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.server.ResponseStatusException;
-import capstone.relation.api.auth.exception.AuthException;
+import capstone.relation.common.util.SecurityUtil;
import capstone.relation.meeting.domain.MeetRoom;
import capstone.relation.meeting.dto.request.CreateRoomDto;
import capstone.relation.meeting.dto.response.JoinResponseDto;
import capstone.relation.meeting.dto.response.MeetingRoomDto;
import capstone.relation.meeting.dto.response.MeetingRoomListDto;
import capstone.relation.meeting.repository.MeetRoomRepository;
+import capstone.relation.meeting.repository.RedisRepository;
+import capstone.relation.user.UserService;
import capstone.relation.user.dto.RoomInfoDto;
import capstone.relation.user.dto.UserInfoDto;
-import capstone.relation.user.repository.UserRepository;
-import capstone.relation.websocket.SocketRegistry;
-import capstone.relation.workspace.WorkSpace;
-import capstone.relation.workspace.repository.WorkSpaceRepository;
-import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class MeetRoomService {
- private static final String WORK_KEY = "WORKSPACE_ROOM_PARTICIPANTS";
- private static final String USER_KEY = "USER_ROOM_MAPPING";
- private final UserRepository userRepository;
- private final WorkSpaceRepository workSpaceRepository;
+ private final UserService userService;
+ private final RedisRepository redisRepository;
private final MeetRoomRepository meetRoomRepository;
- private final SocketRegistry socketRegistry;
- private final RedisTemplate redisTemplate;
private final SimpMessagingTemplate simpMessagingTemplate;
- private HashOperations>> workspaceRoomParticipants;
- //workspaceId, roomId, userIds
- private HashOperations userRoomMapping;
-
- @PostConstruct
- protected void init() {
- workspaceRoomParticipants = redisTemplate.opsForHash();
- userRoomMapping = redisTemplate.opsForHash();
- }
+ /**
+ * 미팅룸을 생성하고 참여합니다.
+ * @param createRoomDto 생성할 미팅룸 정보 DTO
+ * @return 참여 응답 DTO
+ */
@Transactional(readOnly = false)
- public JoinResponseDto createAndJoinRoom(CreateRoomDto createRoomDto, Long userId, String workSpaceId) {
+ public JoinResponseDto createAndJoinRoom(CreateRoomDto createRoomDto) {
String roomName = createRoomDto.getRoomName();
- if (roomName == null || roomName.isEmpty()) {
+ if (roomName == null || roomName.isEmpty())
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "회의실 이름을 입력해주세요.");
- }
- try {
- JoinResponseDto joinResponseDto = createAndJoin(workSpaceId, userId.toString(), roomName);
- sendRoomList(workSpaceId);
- return joinResponseDto;
- } catch (AuthException e) {
- throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, e.getMessage());
- } catch (Exception e) {
- throw new ResponseStatusException(HttpStatus.BAD_REQUEST, e.getMessage());
- }
- }
-
- public boolean isUserInRoom(String userId) {
- return userRoomMapping.get(USER_KEY, userId) != null;
- }
+ Long userId = SecurityUtil.getCurrentUserId();
+ String workSpaceId = userService.getUserWorkSpaceId(userId);
- public RoomInfoDto getRoomInfo(String workspaceId, Long userId) {
- if (!isUserInRoom(userId.toString())) {
- return new RoomInfoDto();
- }
- String roomId = userRoomMapping.get(USER_KEY, userId.toString());
- System.out.println("roomId = " + roomId);
- System.out.println("userId = " + userId);
- Set userIds = getRoomMembers(workspaceId, Long.parseLong(roomId));
- List userInfoList = new ArrayList<>();
- for (String id : userIds) {
- UserInfoDto userInfoDto = new UserInfoDto();
- userRepository.findById(Long.parseLong(id)).ifPresent(userInfoDto::setByUserEntity);
- userInfoList.add(userInfoDto);
- }
- return new RoomInfoDto(true, Long.parseLong(roomId),
- meetRoomRepository.findById(Long.parseLong(roomId)).get().getRoomName(), userInfoList);
+ // 미팅룸을 생성, 참여, 미팅룸 목록을 전송합니다.
+ Long roomId = createRoom(userId, roomName);
+ JoinResponseDto joinResponseDto = joinWorkspaceRoom(workSpaceId, userId, roomId);
+ sendRoomList(workSpaceId);
+ return joinResponseDto;
}
- private JoinResponseDto createAndJoin(String workSpaceId, String userId, String roomName) {
- WorkSpace workSpace = workSpaceRepository.findById(workSpaceId)
- .orElseThrow(() -> new IllegalArgumentException("Invalid workspace ID"));
- if (isUserInRoom(userId)) {
+ /**
+ * 미팅룸을 생성합니다.
+ * @param userId 사용자 ID
+ * @param roomName 미팅룸 이름
+ * @return 생성된 미팅룸 ID
+ */
+ private Long createRoom(Long userId, String roomName) {
+ if (redisRepository.isUserInRoom(userId))
throw new IllegalArgumentException("User is already in the room: " + userId);
- }
-
MeetRoom meetRoom = MeetRoom.builder()
.roomName(roomName)
.deleted(false)
.build();
meetRoomRepository.save(meetRoom);
- workSpace.addMeetRoom(meetRoom);
- Long roomId = meetRoom.getRoomId();
- return joinWorkspaceRoom(workSpaceId, userId, roomId);
+ return meetRoom.getRoomId();
+ }
+
+ /**
+ * 미팅룸 정보를 조회합니다.
+ * @param workspaceId 워크스페이스 ID
+ * @param userId 사용자 ID
+ * @return 미팅룸 정보 DTO
+ */
+ public RoomInfoDto getRoomInfo(String workspaceId, Long userId) {
+ if (!redisRepository.isUserInRoom(userId))
+ throw new IllegalArgumentException("User is not in any room: " + userId);
+
+ Long roomId = Long.parseLong(redisRepository.getUserRoomId(userId));
+ Set userIds = redisRepository.getRoomMemberIds(workspaceId, roomId);
+ List userInfoList = userService.getUserInfoList(userIds);
+ return new RoomInfoDto(true, roomId, getRoomName(roomId), userInfoList);
}
- public JoinResponseDto joinRoom(Long userId, String workspaceId, Long roomId) {
+ /**
+ * 미팅룸 이름을 조회합니다.
+ * @param roomId 미팅룸 ID
+ * @return 미팅룸 이름
+ */
+ private String getRoomName(Long roomId) {
+ return meetRoomRepository.findById(roomId)
+ .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Invalid room ID : " + roomId))
+ .getRoomName();
+ }
- //유저가 이미 회의 방에 있는지 확인
- String meetRoom = userRoomMapping.get(USER_KEY, userId.toString());
- if (meetRoom != null) {
+ /**
+ * 미팅룸에 참여합니다. 컨트롤러 연결 메서드
+ * @param roomId 가입하고자 하는 미팅룸 ID
+ * @return 참여 응답 DTO
+ */
+ public JoinResponseDto joinRoom(Long roomId) {
+ Long userId = SecurityUtil.getCurrentUserId();
+ String workspaceId = userService.getUserWorkSpaceId(userId);
+ String meetRoomId = redisRepository.getUserRoomId(userId);
+ if (meetRoomId != null)
throw new IllegalArgumentException("User is already in the room: " + userId);
- }
- JoinResponseDto joinResponseDto = joinWorkspaceRoom(workspaceId, userId.toString(), roomId);
+
+ JoinResponseDto joinResponseDto = joinWorkspaceRoom(workspaceId, userId, roomId);
sendRoomList(workspaceId);
return joinResponseDto;
}
- private JoinResponseDto joinWorkspaceRoom(String workSpaceId, String userId, Long roomId) {
+ /**
+ * 미팅룸에 참여합니다.
+ * 웹소켓으로 변경된 사용자 목록을 전송합니다.
+ * @param workSpaceId 워크스페이스 ID
+ * @param userId 사용자 ID
+ * @param roomId 미팅룸 ID
+ * @return 참여 응답 DTO
+ */
+ private JoinResponseDto joinWorkspaceRoom(String workSpaceId, Long userId, Long roomId) {
MeetRoom meetRoom = meetRoomRepository.findById(roomId)
- .orElseThrow(() -> new IllegalArgumentException("Invalid room ID"));
- Set userIds = addUserToRoom(workSpaceId, roomId, userId);
- List userInfoList = new ArrayList<>();
- for (String id : userIds) {
- UserInfoDto userInfoDto = new UserInfoDto();
- userRepository.findById(Long.parseLong(id)).ifPresent(userInfoDto::setByUserEntity);
- userInfoList.add(userInfoDto);
- }
+ .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Invalid room ID"));
+ redisRepository.addUserToRoom(workSpaceId, roomId, userId.toString());
+ Set userIds = redisRepository.getRoomMemberIds(workSpaceId, roomId);
+ List userInfoList = userService.getUserInfoList(userIds);
sendUserList(workSpaceId, roomId);
return new JoinResponseDto(roomId, meetRoom.getRoomName(), userInfoList, (long)userIds.size());
}
+ /**
+ * 미팅룸에서 나갑니다.
+ * 웹소켓으로 변경된 사용자 목록을 전송합니다.
+ * @param userId 사용자 ID
+ */
@Transactional(readOnly = false)
- public void leaveRoom(Long userId, String workspaceId) {
- String meetRoom = userRoomMapping.get(USER_KEY, userId.toString());
- if (meetRoom == null) {
- return;
- }
- removeUserFromRoom(workspaceId, Long.parseLong(meetRoom), userId.toString());
- sendUserList(workspaceId, Long.parseLong(meetRoom));
+ public void leaveRoom(Long userId) {
+ String workspaceId = userService.getUserWorkSpaceId(userId);
+ String meetRoomId = redisRepository.getUserRoomId(userId);
+ if (meetRoomId == null)
+ throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "User is not in any room: " + userId);
+ redisRepository.removeUserFromRoom(workspaceId, Long.parseLong(meetRoomId), userId.toString());
+ sendUserList(workspaceId, Long.parseLong(meetRoomId));
sendRoomList(workspaceId);
}
- private Set addUserToRoom(String workspaceId, Long roomId, String userId) {
- HashMap> roomParticipants = workspaceRoomParticipants.get(WORK_KEY, workspaceId);
- if (roomParticipants == null) {
- roomParticipants = new HashMap<>();
- }
- Set users = roomParticipants.get(roomId.toString());
- if (users == null) {
- users = new HashSet<>();
- }
- users.add(userId);
- roomParticipants.put(roomId.toString(), users);
- workspaceRoomParticipants.put(WORK_KEY, workspaceId, roomParticipants);
- userRoomMapping.put(USER_KEY, userId, roomId.toString());
- return users;
- }
-
- private void removeUserFromRoom(String workspaceId, Long roomId, String userId) {
- userRoomMapping.delete(USER_KEY, userId);
- HashMap> roomParticipants = workspaceRoomParticipants.get(WORK_KEY, workspaceId);
- if (roomParticipants == null) {
- return;
- }
- Set userIds = roomParticipants.get(roomId.toString());
- userIds.remove(userId);
-
- if (userIds.isEmpty()) {
- MeetRoom meetRoom = meetRoomRepository.findById(roomId).orElse(null);
- if (meetRoom != null) {
- meetRoom.setDeleted(true);
- meetRoomRepository.save(meetRoom);
- }
- roomParticipants.remove(roomId.toString());
- } else {
- roomParticipants.put(roomId.toString(), userIds);
- }
- if (roomParticipants.isEmpty()) {
- workspaceRoomParticipants.delete(WORK_KEY, workspaceId);
- } else {
- workspaceRoomParticipants.put(WORK_KEY, workspaceId, roomParticipants);
- }
- }
-
- public Set getRoomMembers(String workspaceId, Long roomId) {
- HashMap> roomParticipants = workspaceRoomParticipants.get(WORK_KEY, workspaceId.toString());
- if (roomParticipants == null) {
- return new HashSet<>();
- }
- return roomParticipants.get(roomId.toString());
- }
-
+ /**
+ * 미팅룸 목록을 웹소켓을 통해 전송합니다.
+ * 이벤트 : /topic/{workSpaceId}/meetingRoomList
+ * 전송 데이터 : 워크스페이스에 있는 미팅룸 목록
+ * @param workSpaceId 참여중인 워크스페이스 ID
+ * @return 미팅룸 목록
+ */
public MeetingRoomListDto sendRoomList(String workSpaceId) {
+ // 미팅룸 정보를 조회합니다.
Set meetRooms = meetRoomRepository.findAllByWorkSpaceId(workSpaceId);
MeetingRoomListDto meetingRoomListDto = new MeetingRoomListDto();
+ // 미팅룸 정보를 DTO로 변환합니다.
for (MeetRoom meetRoom : meetRooms) {
- Long roomId = meetRoom.getRoomId();
MeetingRoomDto meetingRoomDto = new MeetingRoomDto();
- meetingRoomDto.setRoomId(roomId);
+ meetingRoomDto.setRoomId(meetRoom.getRoomId());
meetingRoomDto.setRoomName(meetRoom.getRoomName());
- Set roomMembers = getRoomMembers(workSpaceId, roomId);
- if (roomMembers == null) {
- return meetingRoomListDto;
- }
+
+ // 미팅룸에 참여한 사용자 수를 조회합니다.
+ Set roomMembers = redisRepository.getRoomMemberIds(workSpaceId, meetRoom.getRoomId());
meetingRoomDto.setUserCount(roomMembers.size());
- List userInfoList = new ArrayList<>();
- for (String userId : roomMembers) {
- UserInfoDto userInfoDto = new UserInfoDto();
- userRepository.findById(Long.parseLong(userId)).ifPresent(userInfoDto::setByUserEntity);
- userInfoList.add(userInfoDto);
- }
+
+ // 사용자 정보 목록을 저장합니다.
+ List userInfoList = userService.getUserInfoList(roomMembers);
meetingRoomDto.setUserInfoList(userInfoList);
+
meetingRoomListDto.getMeetingRoomList().add(meetingRoomDto);
}
+
+ // 미팅룸 정보를 전송합니다.(웹소켓)
simpMessagingTemplate.convertAndSend("/topic/" + workSpaceId + "/meetingRoomList", meetingRoomListDto);
return meetingRoomListDto;
}
- private void sendUserList(String workSpaceId, Long roomId) {
- Set userIds = getRoomMembers(workSpaceId, roomId);
- List userInfoList = new ArrayList<>();
- for (String userId : userIds) {
- UserInfoDto userInfoDto = new UserInfoDto();
- userRepository.findById(Long.parseLong(userId)).ifPresent(userInfoDto::setByUserEntity);
- userInfoList.add(userInfoDto);
- }
- simpMessagingTemplate.convertAndSend("/topic/meetingRoom/" + roomId + "/users", userInfoList);
+ /**
+ * 미팅룸 목록을 웹소켓을 통해서 전송합니다.(컨트롤러 연결 메서드)
+ * 이벤트 : /topic/meetingRoom/{roomId}/users
+ * 전송 데이터 : 사용자 정보 리스트
+ * @return 미팅룸 목록
+ */
+ public MeetingRoomListDto sendRoomList() {
+ Long userId = SecurityUtil.getCurrentUserId();
+ String workSpaceId = userService.getUserWorkSpaceId(userId);
+ return sendRoomList(workSpaceId);
}
- public void sendErrorMessage(SimpMessageHeaderAccessor headerAccessor, String message, String destination,
- int status) {
- Long userId = (Long)headerAccessor.getSessionAttributes().get("userId");
- String socketId = socketRegistry.getSocketId(userId.toString());
- simpMessagingTemplate.convertAndSendToUser(socketId, destination,
- ResponseEntity.status(status).body(message));
+ /**
+ * 미팅룸에 참여한 사용자 목록을 웹소켓을 통해서 전송합니다.
+ * 이벤트 : /topic/meetingRoom/{roomId}/users
+ * 전송 데이터 : 사용자 정보 리스트
+ * @param workSpaceId 워크스페이스 ID
+ * @param roomId 미팅룸 ID
+ */
+ private void sendUserList(String workSpaceId, Long roomId) {
+ Set userIds = redisRepository.getRoomMemberIds(workSpaceId, roomId);
+ List userInfoList = userService.getUserInfoList(userIds);
+ simpMessagingTemplate.convertAndSend("/topic/meetingRoom/" + roomId + "/users", userInfoList);
}
}
diff --git a/src/main/java/capstone/relation/user/UserService.java b/src/main/java/capstone/relation/user/UserService.java
index fe8ff03..d1f4570 100644
--- a/src/main/java/capstone/relation/user/UserService.java
+++ b/src/main/java/capstone/relation/user/UserService.java
@@ -1,6 +1,9 @@
package capstone.relation.user;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Optional;
+import java.util.Set;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication;
@@ -72,4 +75,26 @@ public void leaveWorkspace() {
user.setWorkSpace(null);
userRepository.save(user);
}
+
+ public String getUserWorkSpaceId(Long userId) {
+ return userRepository.findById(userId)
+ .orElseThrow(() -> new IllegalArgumentException("Invalid user ID"))
+ .getWorkSpace()
+ .getId();
+ }
+
+ /**
+ * 사용자 Id 목록을 받아서 사용자 정보 목록을 반환합니다.
+ * @param userIds 사용자 Id 목록
+ * @return 사용자 정보 목록
+ */
+ public List getUserInfoList(Set userIds) {
+ List userInfoList = new ArrayList<>();
+ for (String id : userIds) {
+ UserInfoDto userInfoDto = new UserInfoDto();
+ userRepository.findById(Long.parseLong(id)).ifPresent(userInfoDto::setByUserEntity);
+ userInfoList.add(userInfoDto);
+ }
+ return userInfoList;
+ }
}
diff --git a/src/main/java/capstone/relation/user/controller/UserController.java b/src/main/java/capstone/relation/user/controller/UserController.java
index 85fd717..2362aec 100644
--- a/src/main/java/capstone/relation/user/controller/UserController.java
+++ b/src/main/java/capstone/relation/user/controller/UserController.java
@@ -28,7 +28,7 @@ public UserInfoDto getInfo() {
}
@GetMapping("/room/info")
- @Operation(summary = "사용자 방 정보 조회", description = "현재 로그인한 사용자의 방 정보를 조회합니다.")
+ @Operation(summary = "사용자 회의 방 정보 조회", description = "현재 로그인한 사용자의 회의 방 정보를 조회합니다.")
public RoomInfoDto getRoomInfo() {
User user = userService.getUserEntity();
Long userId = user.getId();
diff --git a/src/main/java/capstone/relation/user/domain/User.java b/src/main/java/capstone/relation/user/domain/User.java
index 32d1034..b9d837a 100644
--- a/src/main/java/capstone/relation/user/domain/User.java
+++ b/src/main/java/capstone/relation/user/domain/User.java
@@ -36,6 +36,7 @@ public User(Long id, String profileImage, String userName, String email, String
this.provider = provider;
this.email = email;
this.role = role;
+ this.id = id;
}
@Id
diff --git a/src/main/java/capstone/relation/user/dto/UserInfoDto.java b/src/main/java/capstone/relation/user/dto/UserInfoDto.java
index f469b2e..11e52d7 100644
--- a/src/main/java/capstone/relation/user/dto/UserInfoDto.java
+++ b/src/main/java/capstone/relation/user/dto/UserInfoDto.java
@@ -4,8 +4,10 @@
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
+import lombok.NoArgsConstructor;
@Data
+@NoArgsConstructor
public class UserInfoDto {
@Schema(description = "사용자 ID", example = "1234567890")
private Long userId;
@@ -16,6 +18,13 @@ public class UserInfoDto {
@Schema(description = "사용자 이메일", example = "wnddms12345@naver.com")
private String email;
+ public UserInfoDto(Long userId, String userName, String email, String userImage) {
+ this.userId = userId;
+ this.userName = userName;
+ this.email = email;
+ this.userImage = userImage;
+ }
+
@Hidden
public void setByUserEntity(User user) {
this.userId = user.getId();
diff --git a/src/main/java/capstone/relation/websocket/WebSocketEventListener.java b/src/main/java/capstone/relation/websocket/WebSocketEventListener.java
index 9c70a5a..cbf119e 100644
--- a/src/main/java/capstone/relation/websocket/WebSocketEventListener.java
+++ b/src/main/java/capstone/relation/websocket/WebSocketEventListener.java
@@ -18,19 +18,15 @@ public class WebSocketEventListener {
@EventListener
public void handleWebSocketDisconnectListener(SessionDisconnectEvent event) {
StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(event.getMessage());
- // String sessionId = headerAccessor.getSessionId();
String socketId = headerAccessor.getUser().getName();
Long userId = (Long)headerAccessor.getSessionAttributes().get("userId");
- String workSpaceId = (String)headerAccessor.getSessionAttributes().get("workSpaceId");
System.out.println("User Disconnected : " + userId);
- System.out.println("socketId : " + socketId);
if (userId == null) {
return;
}
- meetRoomService.leaveRoom(userId, workSpaceId);
+ meetRoomService.leaveRoom(userId);
System.out.println("register SocketId" + socketRegistry.getSocketId(userId.toString()));
- if (socketId == socketRegistry.getSocketId(userId.toString())) {
+ if (socketId == socketRegistry.getSocketId(userId.toString()))
socketRegistry.unregisterSession(userId.toString());
- }
}
}
diff --git a/src/main/java/capstone/relation/websocket/signaling/controller/SignalingController.java b/src/main/java/capstone/relation/websocket/signaling/SignalingController.java
similarity index 94%
rename from src/main/java/capstone/relation/websocket/signaling/controller/SignalingController.java
rename to src/main/java/capstone/relation/websocket/signaling/SignalingController.java
index 3d0b987..9d3f1a1 100644
--- a/src/main/java/capstone/relation/websocket/signaling/controller/SignalingController.java
+++ b/src/main/java/capstone/relation/websocket/signaling/SignalingController.java
@@ -1,4 +1,4 @@
-package capstone.relation.websocket.signaling.controller;
+package capstone.relation.websocket.signaling;
import org.springframework.messaging.handler.annotation.DestinationVariable;
import org.springframework.messaging.handler.annotation.MessageMapping;
@@ -18,7 +18,6 @@ public class SignalingController {
@MessageMapping("/ice/{roomId}")
public void ice(@DestinationVariable String roomId, IceDto iceDto, SimpMessageHeaderAccessor headerAccessor) {
- System.out.println("ice");
Long userId = (Long)headerAccessor.getSessionAttributes().get("userId");
signalingService.sendIce(roomId, iceDto, userId);
}
diff --git a/src/main/java/capstone/relation/websocket/signaling/controller/SignalingDocumentController.java b/src/main/java/capstone/relation/websocket/signaling/SignalingDocumentController.java
similarity index 99%
rename from src/main/java/capstone/relation/websocket/signaling/controller/SignalingDocumentController.java
rename to src/main/java/capstone/relation/websocket/signaling/SignalingDocumentController.java
index d09626e..b929866 100644
--- a/src/main/java/capstone/relation/websocket/signaling/controller/SignalingDocumentController.java
+++ b/src/main/java/capstone/relation/websocket/signaling/SignalingDocumentController.java
@@ -1,4 +1,4 @@
-package capstone.relation.websocket.signaling.controller;
+package capstone.relation.websocket.signaling;
import java.util.List;
diff --git a/src/main/java/capstone/relation/websocket/signaling/dto/SdpResponseDto.java b/src/main/java/capstone/relation/websocket/signaling/dto/SdpResponseDto.java
index 915e8a4..1df17e0 100644
--- a/src/main/java/capstone/relation/websocket/signaling/dto/SdpResponseDto.java
+++ b/src/main/java/capstone/relation/websocket/signaling/dto/SdpResponseDto.java
@@ -2,9 +2,12 @@
import capstone.relation.user.dto.UserInfoDto;
import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Builder;
import lombok.Data;
+import lombok.NoArgsConstructor;
@Data
+@NoArgsConstructor
@Schema(description = "offer 또는 answer를 받을 때 사용하는 DTO")
public class SdpResponseDto {
@Schema(description = "메시지를 보낸 사용자 정보(즉 자신이 아닌 통신하고 있는 유저 id)")
@@ -15,4 +18,11 @@ public class SdpResponseDto {
@Schema(description = "메시지 타입 \n"
+ "ScreenShare | Video", example = "ScreenShare")
private SignalMessageType type;
+
+ @Builder
+ public SdpResponseDto(UserInfoDto userInfo, SdpDto sessionDescription, SignalMessageType type) {
+ this.userInfo = userInfo;
+ this.sessionDescription = sessionDescription;
+ this.type = type;
+ }
}
diff --git a/src/main/java/capstone/relation/websocket/signaling/service/SignalingService.java b/src/main/java/capstone/relation/websocket/signaling/service/SignalingService.java
index 68a9474..e12cce8 100644
--- a/src/main/java/capstone/relation/websocket/signaling/service/SignalingService.java
+++ b/src/main/java/capstone/relation/websocket/signaling/service/SignalingService.java
@@ -13,40 +13,58 @@
@Service
@RequiredArgsConstructor
public class SignalingService {
+ private final UserService userService;
private final SocketRegistry socketRegistry;
-
private final SimpMessagingTemplate simpMessagingTemplate;
- private final UserService userService;
- public void sendOffer(String roomId, SdpMessageDto sdpMessageDto, Long myId) {
- System.out.println("sendOffer");
+ /**
+ * Offer 메시지를 상대방에게 전송합니다.(목적지는 메시지를 확인해서 소켓 ID를 찾아서 전송합니다.)
+ * /user/queue/offer/{roomId} 로 전송합니다.
+ * @param roomId 참여중인 화상회의방 ID
+ * @param sdpMessageDto Offer 메시지 DTO
+ * @param senderId 보내는 사람 ID (내 Id)
+ */
+ public void sendOffer(String roomId, SdpMessageDto sdpMessageDto, Long senderId) {
String destId = sdpMessageDto.getUserId();
String socketId = socketRegistry.getSocketId(destId);
- SdpResponseDto sdpResponseDto = new SdpResponseDto();
- sdpResponseDto.setUserInfo(userService.getUserInfo(myId));
- sdpResponseDto.setSessionDescription(sdpMessageDto.getSessionDescription());
- sdpResponseDto.setType(sdpMessageDto.getType());
+
+ SdpResponseDto sdpResponseDto = SdpResponseDto.builder()
+ .userInfo(userService.getUserInfo(senderId))
+ .sessionDescription(sdpMessageDto.getSessionDescription())
+ .type(sdpMessageDto.getType())
+ .build();
simpMessagingTemplate.convertAndSendToUser(socketId, "/queue/offer/" + roomId, sdpResponseDto);
}
- public void sendAnswer(String roomId, SdpMessageDto sdpMessageDto, Long myId) {
- System.out.println("sendAnswer");
+ /**
+ * Answer 메시지를 상대방에게 전송합니다.(목적지는 메시지를 확인해서 소켓 ID를 찾아서 전송합니다.)
+ * /user/queue/answer/{roomId} 로 전송합니다.
+ * @param roomId 참여중인 화상회의방 ID
+ * @param sdpMessageDto Answer 메시지 DTO
+ * @param senderId 보내는 사람 ID (내 Id)
+ */
+ public void sendAnswer(String roomId, SdpMessageDto sdpMessageDto, Long senderId) {
String socketId = socketRegistry.getSocketId(sdpMessageDto.getUserId());
- SdpResponseDto sdpResponseDto = new SdpResponseDto();
- sdpResponseDto.setUserInfo(userService.getUserInfo(myId));
- sdpResponseDto.setSessionDescription(sdpMessageDto.getSessionDescription());
- sdpResponseDto.setType(sdpMessageDto.getType());
+ SdpResponseDto sdpResponseDto = SdpResponseDto.builder()
+ .userInfo(userService.getUserInfo(senderId))
+ .sessionDescription(sdpMessageDto.getSessionDescription())
+ .type(sdpMessageDto.getType())
+ .build();
simpMessagingTemplate.convertAndSendToUser(socketId, "/queue/answer/" + roomId, sdpResponseDto);
}
- public void sendIce(String roomId, IceDto iceDto, Long myId) {
- System.out.println("sendIce");
+ /**
+ * Ice 메시지를 상대방에게 전송합니다.(목적지는 메시지를 확인해서 소켓 ID를 찾아서 전송합니다.)
+ * @param roomId 참여중인 화상회의방 ID
+ * @param iceDto Ice 메시지 DTO
+ * @param senderId 보내는 사람 ID (내 Id)
+ */
+ public void sendIce(String roomId, IceDto iceDto, Long senderId) {
String destId = iceDto.getUserId();
String socketId = socketRegistry.getSocketId(destId);
- iceDto.setUserId(myId.toString()); // 보내는 사람 ID로 갈아 껴줌.
+ iceDto.setUserId(senderId.toString()); // 보내는 사람 ID로 갈아 껴줌.
iceDto.setType(iceDto.getType());
simpMessagingTemplate.convertAndSendToUser(socketId, "/queue/ice/" + roomId, iceDto);
}
-
}
diff --git a/src/main/java/capstone/relation/workspace/WorkSpace.java b/src/main/java/capstone/relation/workspace/WorkSpace.java
index 05f4099..c23842b 100644
--- a/src/main/java/capstone/relation/workspace/WorkSpace.java
+++ b/src/main/java/capstone/relation/workspace/WorkSpace.java
@@ -45,10 +45,6 @@ public void addUser(User user) {
user.setInvitedWorkspaceId("");
}
- public void setId(String id) {
- this.id = id;
- }
-
public void setName(String name) {
this.name = name;
}
diff --git a/src/test/java/capstone/relation/CustomInitListenerTest.java b/src/test/java/capstone/relation/CustomInitListenerTest.java
new file mode 100644
index 0000000..67a6c00
--- /dev/null
+++ b/src/test/java/capstone/relation/CustomInitListenerTest.java
@@ -0,0 +1,26 @@
+package capstone.relation;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+
+import capstone.relation.workspace.school.service.SchoolService;
+
+@ExtendWith(SpringExtension.class)
+@SpringBootTest(classes = {TestCustomInitConfig.class}) // 테스트 전용 설정 클래스 추가
+@ActiveProfiles("test")
+public class CustomInitListenerTest {
+
+ @Autowired
+ private SchoolService schoolService;
+
+ @Test
+ public void testCustomInitListener() {
+ // 테스트 로직
+ // CustomInitListener가 호출되어 schoolService.initSchool() 메서드가 실행되는지 확인
+ schoolService.initSchool();
+ }
+}
diff --git a/src/test/java/capstone/relation/TestCustomInitConfig.java b/src/test/java/capstone/relation/TestCustomInitConfig.java
new file mode 100644
index 0000000..92b0ca4
--- /dev/null
+++ b/src/test/java/capstone/relation/TestCustomInitConfig.java
@@ -0,0 +1,18 @@
+package capstone.relation;
+
+import org.springframework.boot.test.context.TestConfiguration;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Profile;
+
+import capstone.relation.common.initializer.CustomInitListener;
+import capstone.relation.workspace.school.service.SchoolService;
+
+@TestConfiguration
+@Profile("test")
+public class TestCustomInitConfig {
+
+ @Bean
+ public CustomInitListener customInitListener(SchoolService schoolService) {
+ return new CustomInitListener(schoolService);
+ }
+}
diff --git a/src/test/java/capstone/relation/meeting/controller/MeetRoomControllerTest.java b/src/test/java/capstone/relation/meeting/controller/MeetRoomControllerTest.java
new file mode 100644
index 0000000..fc12bf7
--- /dev/null
+++ b/src/test/java/capstone/relation/meeting/controller/MeetRoomControllerTest.java
@@ -0,0 +1,79 @@
+package capstone.relation.meeting.controller;
+
+import static org.mockito.Mockito.*;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import capstone.relation.meeting.dto.request.CreateRoomDto;
+import capstone.relation.meeting.dto.response.JoinResponseDto;
+import capstone.relation.meeting.service.MeetRoomService;
+import capstone.relation.security.WithMockCustomUser;
+import capstone.relation.user.dto.UserInfoDto;
+
+@ExtendWith(SpringExtension.class)
+@DisplayName("회의방 컨트롤러 테스트")
+class MeetRoomControllerTest {
+
+ @InjectMocks
+ private MeetRoomController meetRoomController;
+
+ @Mock
+ private MeetRoomService meetRoomService;
+
+ private MockMvc mockMvc;
+ private ObjectMapper objectMapper;
+
+ @BeforeEach
+ void setUp() {
+ mockMvc = MockMvcBuilders.standaloneSetup(meetRoomController).build();
+ objectMapper = new ObjectMapper();
+ }
+
+ @Test
+ @DisplayName("방 생성")
+ void testCreateRoom() throws Exception {
+ CreateRoomDto createRoomDto = new CreateRoomDto("회의방");
+ List userInfoList = new ArrayList<>();
+ UserInfoDto userInfoDto = new UserInfoDto(1L, "이름", "이메일", "사진");
+ userInfoList.add(userInfoDto);
+ JoinResponseDto joinResponseDto = new JoinResponseDto(1L, "회의방", userInfoList, 1L);
+
+ when(meetRoomService.createAndJoinRoom(any(CreateRoomDto.class)))
+ .thenReturn(joinResponseDto);
+
+ mockMvc.perform(post("/meet/room/create")
+ .contentType("application/json")
+ .content(objectMapper.writeValueAsString(createRoomDto))) // CreateRoomDto 객체를 JSON 문자열로 변환
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.roomId").value(joinResponseDto.getRoomId()))
+ .andExpect(jsonPath("$.roomName").value(joinResponseDto.getRoomName()))
+ .andExpect(jsonPath("$.userInfoList[0].userId").value(userInfoDto.getUserId()))
+ .andExpect(jsonPath("$.userInfoList[0].userName").value(userInfoDto.getUserName()))
+ .andExpect(jsonPath("$.userInfoList[0].email").value(userInfoDto.getEmail()))
+ .andExpect(jsonPath("$.userInfoList[0].userImage").value(userInfoDto.getUserImage()))
+ .andExpect(jsonPath("$.userCount").value(joinResponseDto.getUserCount()));
+ }
+
+ @Test
+ @DisplayName("방 나가기 테스트")
+ @WithMockCustomUser
+ void testLeaveRoom() throws Exception {
+ mockMvc.perform(post("/meet/room/leave"))
+ .andExpect(status().isOk());
+ }
+}
diff --git a/src/test/java/capstone/relation/meeting/integration/MeetRoomIntegrationTest.java b/src/test/java/capstone/relation/meeting/integration/MeetRoomIntegrationTest.java
new file mode 100644
index 0000000..c4a09f5
--- /dev/null
+++ b/src/test/java/capstone/relation/meeting/integration/MeetRoomIntegrationTest.java
@@ -0,0 +1,224 @@
+package capstone.relation.meeting.integration;
+
+import static org.mockito.Mockito.*;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+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.boot.test.mock.mockito.MockBean;
+import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+import org.springframework.test.web.servlet.MockMvc;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import capstone.relation.meeting.domain.MeetRoom;
+import capstone.relation.meeting.dto.request.CreateRoomDto;
+import capstone.relation.meeting.repository.MeetRoomRepository;
+import capstone.relation.meeting.repository.RedisRepository;
+import capstone.relation.security.WithMockCustomUser;
+import capstone.relation.user.domain.Role;
+import capstone.relation.user.domain.User;
+import capstone.relation.user.repository.UserRepository;
+import capstone.relation.workspace.WorkSpace;
+import capstone.relation.workspace.repository.WorkSpaceRepository;
+import capstone.relation.workspace.school.domain.School;
+import capstone.relation.workspace.school.respository.SchoolRepository;
+
+@ExtendWith(SpringExtension.class)
+@SpringBootTest
+@AutoConfigureMockMvc
+@ActiveProfiles("test")
+@DisplayName("회의방 통합 테스트")
+public class MeetRoomIntegrationTest {
+
+ @Autowired
+ private MockMvc mockMvc;
+
+ @Autowired
+ private ObjectMapper objectMapper;
+
+ @Autowired
+ UserRepository userRepository;
+
+ @Autowired
+ WorkSpaceRepository workSpaceRepository;
+
+ @Autowired
+ SchoolRepository schoolRepository;
+
+ @Autowired
+ MeetRoomRepository meetRoomRepository;
+
+ @MockBean
+ private RedisRepository redisRepository;
+
+ private WorkSpace testWorkSpace;
+ private School testSchool;
+
+ @BeforeEach
+ void setup() {
+ redisRepository.deleteAll(); // Redis 저장소 초기화
+ testSchool = new School("서울캠", "과기대", "4년제", "링크", "학교소개", "학교위치", "학교지역", "학교종류", new HashSet<>());
+ schoolRepository.save(testSchool);
+
+ testWorkSpace = new WorkSpace();
+ testWorkSpace.setName("testWorkSpace");
+ testWorkSpace.setSchool(testSchool);
+ workSpaceRepository.save(testWorkSpace);
+
+ addTestUser("testUser1");
+ addTestUser("testUser2");
+ addTestUser("testUser3");
+
+ // RedisRepository 모킹
+ when(redisRepository.isUserInRoom(any())).thenReturn(false);
+ when(redisRepository.getUserRoomId(1L)).thenReturn(null);
+ when(redisRepository.getRoomMemberIds(testWorkSpace.getId(), 1L)).thenReturn(new HashSet<>(Set.of("1")));
+ when(redisRepository.getRoomMemberIds(testWorkSpace.getId(), 2L)).thenReturn(new HashSet<>(Set.of("1")));
+ }
+
+ private void addTestUser(String username) {
+ User user = User.builder().userName(username)
+ .email(username + "@naver.com")
+ .profileImage(
+ "https://lh3.googleusercontent.com/ogw/AF2bZyhqowurXq6imx61oPHn5G_c6OIEnucOyJanitxYGFUI498=s32-c-mo")
+ .role(Role.USER)
+ .provider("naver")
+ .build();
+ workSpaceRepository.save(testWorkSpace);
+ user.setWorkSpace(testWorkSpace);
+ userRepository.save(user);
+ }
+
+ @Test
+ @DisplayName("통합 테스트 : 새로운 방 생성을 할 수 있다.")
+ @WithMockCustomUser
+ @DirtiesContext
+ void createRoom() throws Exception {
+ // given
+ CreateRoomDto createRoomDto = new CreateRoomDto("New Room");
+ String requestJson = objectMapper.writeValueAsString(createRoomDto);
+
+ // when & then
+ mockMvc.perform(post("/meet/room/create")
+ .contentType("application/json")
+ .content(requestJson))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.roomId").isNumber())
+ .andExpect(jsonPath("$.roomName").value("New Room"))
+ .andExpect(jsonPath("$.userInfoList").isArray())
+ .andExpect(jsonPath("$.userCount").value(1));
+ }
+
+ @Test
+ @DisplayName("통합 테스트 : 존재하는 회의방에 참여할 수 있다.")
+ @WithMockCustomUser
+ @DirtiesContext
+ void joinRoom() throws Exception {
+ // given
+ MeetRoom meetRoom = MeetRoom.builder()
+ .roomName("채팅방")
+ .deleted(false)
+ .build();
+ meetRoomRepository.save(meetRoom);
+ // when & then
+ mockMvc.perform(post("/meet/room/join/1"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.roomId").isNumber())
+ .andExpect(jsonPath("$.roomName").value("채팅방"))
+ .andExpect(jsonPath("$.userInfoList").isArray())
+ .andExpect(jsonPath("$.userCount").value(1));
+ }
+
+ @Test
+ @DisplayName("통합 테스트 : 존재하지 않는 회의방에 참여할 수 없다.")
+ @DirtiesContext
+ @WithMockCustomUser
+ void joinRoomFail() throws Exception {
+ // when & then
+ mockMvc.perform(post("/meet/room/join/1"))
+ .andExpect(status().isNotFound());
+ }
+
+ @Test
+ @DisplayName("통합 테스트 : 회의방에서 나갈 수 있다.")
+ @WithMockCustomUser(id = 2L)
+ @DirtiesContext
+ void leaveRoom() throws Exception {
+ // given
+ MeetRoom meetRoom = MeetRoom.builder()
+ .roomName("채팅방1")
+ .deleted(false)
+ .build();
+ meetRoomRepository.save(meetRoom);
+ MeetRoom meetRoom2 = MeetRoom.builder()
+ .roomName("채팅방2")
+ .deleted(false)
+ .build();
+ meetRoomRepository.save(meetRoom2);
+ // join 시에 필요한 모킹 설정
+ when(redisRepository.getUserRoomId(anyLong())).thenReturn(null);
+ when(redisRepository.getRoomMemberIds(any(), anyLong())).thenReturn(new HashSet<>(Set.of("2")));
+ // when & then
+ mockMvc.perform(post("/meet/room/join/2"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.roomId").isNumber())
+ .andExpect(jsonPath("$.roomName").value("채팅방2"))
+ .andExpect(jsonPath("$.userInfoList").isArray())
+ .andExpect(jsonPath("$.userCount").value(1));
+ // leave 시에 필요한 모킹 설정
+ when(redisRepository.getUserRoomId(anyLong())).thenReturn(meetRoom2.getRoomId().toString());
+
+ mockMvc.perform(post("/meet/room/leave"))
+ .andExpect(status().isOk());
+ }
+
+ @Test
+ @DisplayName("통합 테스트 : 회의방 목록을 요청할 수 있다.")
+ @WithMockCustomUser
+ @DirtiesContext
+ void requestRoomList() throws Exception {
+ // given
+ MeetRoom meetRoom = MeetRoom.builder()
+ .roomName("채팅방1")
+ .deleted(false)
+ .build();
+ meetRoomRepository.save(meetRoom);
+ MeetRoom meetRoom2 = MeetRoom.builder()
+ .roomName("채팅방2")
+ .deleted(false)
+ .build();
+ meetRoomRepository.save(meetRoom2);
+
+ testWorkSpace.addMeetRoom(meetRoom);
+ testWorkSpace.addMeetRoom(meetRoom2);
+ workSpaceRepository.save(testWorkSpace);
+
+ // join 시에 필요한 모킹 설정
+ when(redisRepository.getUserRoomId(anyLong())).thenReturn(null);
+ when(redisRepository.getRoomMemberIds(any(), anyLong())).thenReturn(new HashSet<>(Set.of("1")));
+ // when & then
+ mockMvc.perform(get("/meet/room/list"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.meetingRoomList").isArray())
+ .andExpect(jsonPath("$.meetingRoomList[0].roomId").isNumber())
+ .andExpect(jsonPath("$.meetingRoomList[0].roomName").value("채팅방1"))
+ .andExpect(jsonPath("$.meetingRoomList[0].userCount").value(1))
+ .andExpect(jsonPath("$.meetingRoomList[0].userInfoList").isArray())
+ .andExpect(jsonPath("$.meetingRoomList[1].roomId").isNumber())
+ .andExpect(jsonPath("$.meetingRoomList[1].roomName").value("채팅방2"))
+ .andExpect(jsonPath("$.meetingRoomList[1].userCount").value(1))
+ .andExpect(jsonPath("$.meetingRoomList[1].userInfoList").isArray());
+ }
+}
diff --git a/src/test/java/capstone/relation/meeting/service/MeetRoomServiceTest.java b/src/test/java/capstone/relation/meeting/service/MeetRoomServiceTest.java
index 2f000a1..cf14c6f 100644
--- a/src/test/java/capstone/relation/meeting/service/MeetRoomServiceTest.java
+++ b/src/test/java/capstone/relation/meeting/service/MeetRoomServiceTest.java
@@ -3,176 +3,137 @@
import static org.assertj.core.api.Assertions.*;
import static org.mockito.BDDMockito.*;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Map;
import java.util.Optional;
-import java.util.Set;
-import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
-import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
-import org.mockito.junit.jupiter.MockitoExtension;
-import org.springframework.data.redis.core.HashOperations;
-import org.springframework.data.redis.core.RedisTemplate;
-import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
+import org.springframework.http.HttpStatus;
import org.springframework.messaging.simp.SimpMessagingTemplate;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+import org.springframework.web.server.ResponseStatusException;
import capstone.relation.meeting.domain.MeetRoom;
import capstone.relation.meeting.dto.request.CreateRoomDto;
import capstone.relation.meeting.dto.response.JoinResponseDto;
import capstone.relation.meeting.dto.response.MeetingRoomListDto;
import capstone.relation.meeting.repository.MeetRoomRepository;
-import capstone.relation.user.domain.Role;
-import capstone.relation.user.domain.User;
-import capstone.relation.user.repository.UserRepository;
-import capstone.relation.websocket.SocketRegistry;
+import capstone.relation.meeting.repository.RedisRepository;
+import capstone.relation.security.WithMockCustomUser;
+import capstone.relation.user.UserService;
import capstone.relation.workspace.WorkSpace;
import capstone.relation.workspace.repository.WorkSpaceRepository;
-@ExtendWith(MockitoExtension.class)
+@ExtendWith(SpringExtension.class)
class MeetRoomServiceTest {
@InjectMocks
private MeetRoomService meetRoomService;
@Mock
- private SimpMessagingTemplate simpMessagingTemplate;
+ private UserService mockUserService;
@Mock
- private SocketRegistry socketRegistry;
+ private SimpMessagingTemplate mockSimpMessagingTemplate;
@Mock
- private WorkSpaceRepository workSpaceRepository;
+ private WorkSpaceRepository mockWorkSpaceRepository;
@Mock
- private MeetRoomRepository meetRoomRepository;
-
- @Mock
- private RedisTemplate redisTemplate;
-
+ private MeetRoomRepository mockMeetRoomRepository;
+
@Mock
- private HashOperations>> workspaceRoomParticipants;
- //workspaceId, roomId, userIds
- @Mock
- private HashOperations mockUserRoomMapping;
+ private RedisRepository mockRedisRepository;
- @Mock
- private UserRepository userRepository;
-
- @BeforeEach
- void setUp() {
- // RedisTemplate의 opsForHash를 모킹하여 hashOperations를 반환하도록 설정합니다.
- when(redisTemplate.opsForHash()).thenReturn((HashOperations)workspaceRoomParticipants)
- .thenReturn(mockUserRoomMapping);
- // MeetingService의 init() 메서드를 명시적으로 호출합니다.
- meetRoomService.init();
- }
-
- @DisplayName("회의 방을 생성할 수 있다.")
+ @DisplayName("회의 방을 생성하고 가입할 수 있다.")
+ @WithMockCustomUser
@Test
- void createRoom() {
+ void createAndJoinRoom() {
// given
CreateRoomDto createRoomDto = new CreateRoomDto("테스트 방이름");
- SimpMessageHeaderAccessor headerAccessor = SimpMessageHeaderAccessor.create();
- Map sessionAttributes = new HashMap<>();
- sessionAttributes.put("userId", 1L);
- sessionAttributes.put("workSpaceId", "workspace-1");
- headerAccessor.setSessionAttributes(sessionAttributes);
-
- given(workSpaceRepository.findById("workspace-1")).willReturn(Optional.of(new WorkSpace()));
- given(meetRoomRepository.save(any(MeetRoom.class))).willAnswer(invocation -> {
+
+ given(mockUserService.getUserWorkSpaceId(1L)).willReturn("workspace-1");
+ given(mockWorkSpaceRepository.findById("workspace-1")).willReturn(Optional.of(new WorkSpace()));
+ given(mockMeetRoomRepository.save(any(MeetRoom.class))).willAnswer(invocation -> {
MeetRoom meetRoom = invocation.getArgument(0);
meetRoom.setRoomId(1L);
return meetRoom;
});
- given(workspaceRoomParticipants.get(anyString(), anyString())).willReturn(new HashMap<>());
- given(meetRoomRepository.findById(1L)).willReturn(
+ given(mockMeetRoomRepository.findById(1L)).willReturn(
Optional.of(MeetRoom.builder().roomId(1L).roomName("테스트 방이름").build()));
// when
- JoinResponseDto joinResponse = meetRoomService.createAndJoinRoom(createRoomDto, 1L, "workspace-1");
+ JoinResponseDto joinResponse = meetRoomService.createAndJoinRoom(createRoomDto);
// then
- verify(simpMessagingTemplate, times(1)).convertAndSend(eq("/topic/workspace-1/meetingRoomList"),
+ verify(mockSimpMessagingTemplate, times(1)).convertAndSend(eq("/topic/workspace-1/meetingRoomList"),
any(MeetingRoomListDto.class));
assertThat(joinResponse).isNotNull();
assertThat(joinResponse.getRoomName()).isEqualTo("테스트 방이름");
assertThat(joinResponse.getRoomId()).isEqualTo(1L);
}
- @DisplayName("회의 방 목록을 받아올 수 있다.")
+ @DisplayName("잘못된 회의실 이름으로 회의 방을 생성하려고 할 때 예외가 발생한다.")
@Test
- void getRoomList() {
+ public void createAndJoinRoomWithInvalidRoomName() {
// given
- WorkSpace workSpace = new WorkSpace();
- workSpace.setId("workspace-1");
- String workSpaceId = workSpace.getId();
- MeetRoom meetRoom1 = MeetRoom.builder().roomId(1L).roomName("회의실1").deleted(false).workSpace(workSpace).build();
- MeetRoom meetRoom2 = MeetRoom.builder().roomId(2L).roomName("회의실2").deleted(false).workSpace(workSpace).build();
-
- Set meetRooms = new HashSet<>();
- meetRooms.add(meetRoom1);
- meetRooms.add(meetRoom2);
- Set userIds = new HashSet<>();
- userIds.add("1");
- userIds.add("2");
- HashMap> mockParti = new HashMap<>();
- mockParti.put("1", userIds);
- mockParti.put("2", userIds);
- given(meetRoomRepository.findAllByWorkSpaceId(workSpaceId)).willReturn(meetRooms);
- given(workspaceRoomParticipants.get(anyString(), anyString())).willReturn(mockParti);
+ CreateRoomDto createRoomDto = new CreateRoomDto("");
+
+ // when & then
+ assertThatThrownBy(() -> meetRoomService.createAndJoinRoom(createRoomDto))
+ .isInstanceOf(ResponseStatusException.class)
+ .satisfies(exception -> {
+ ResponseStatusException ex = (ResponseStatusException)exception;
+ assertThat(ex.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
+ assertThat(ex.getReason()).isEqualTo("회의실 이름을 입력해주세요.");
+ });
+ }
+
+ @DisplayName("이미 회의실에 참여한 사용자가 다시 참여하려고 할 때 예외가 발생한다.")
+ @WithMockCustomUser
+ @Test
+ public void createAndJoinRoomWithAlreadyJoinedUser() {
+ // given
+ CreateRoomDto createRoomDto = new CreateRoomDto("테스트 방이름");
+
+ given(mockUserService.getUserWorkSpaceId(1L)).willReturn("workspace-1");
+ given(mockWorkSpaceRepository.findById("workspace-1")).willReturn(Optional.of(new WorkSpace()));
+ given(mockRedisRepository.isUserInRoom(1L)).willReturn(true);
+
+ // when & then
+ assertThatThrownBy(() -> meetRoomService.createAndJoinRoom(createRoomDto))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("User is already in the room: 1");
+ }
+
+ @DisplayName("워크스페이스에 참여한 모든 유저에게 회의실 목록을 전송할 수 있다.")
+ @Test
+ public void sendRoomList() {
+ // given
+ given(mockWorkSpaceRepository.findById("workspace-1")).willReturn(Optional.of(new WorkSpace()));
// when
- meetRoomService.sendRoomList(workSpaceId);
+ meetRoomService.sendRoomList("workspace-1");
// then
- ArgumentCaptor roomListCaptor = ArgumentCaptor.forClass(MeetingRoomListDto.class);
- verify(simpMessagingTemplate, times(1)).convertAndSend(eq("/topic/workspace-1/meetingRoomList"),
- roomListCaptor.capture());
-
- MeetingRoomListDto roomList = roomListCaptor.getValue();
- assertThat(roomList).isNotNull();
- assertThat(roomList.getMeetingRoomList()).hasSize(2);
- assertThat(roomList.getMeetingRoomList()).anyMatch(
- room -> room.getRoomId().equals(1L) && room.getRoomName().equals("회의실1"));
- assertThat(roomList.getMeetingRoomList()).anyMatch(
- room -> room.getRoomId().equals(2L) && room.getRoomName().equals("회의실2"));
+ verify(mockSimpMessagingTemplate, times(1)).convertAndSend(eq("/topic/workspace-1/meetingRoomList"),
+ any(MeetingRoomListDto.class));
}
- @DisplayName("회의 방에 참여할 수 있다.")
+ @DisplayName("회의실을 나갈 수 있다.")
+ @WithMockCustomUser
@Test
- void joinRoom() {
+ public void leaveRoom() {
// given
- SimpMessageHeaderAccessor headerAccessor = SimpMessageHeaderAccessor.create();
- Map sessionAttributes = new HashMap<>();
- sessionAttributes.put("userId", 1L);
- sessionAttributes.put("workSpaceId", "workspace-1");
- headerAccessor.setSessionAttributes(sessionAttributes);
-
- WorkSpace workSpace = new WorkSpace();
- workSpace.setId("workspace-1");
- MeetRoom meetRoom1 = MeetRoom.builder().roomId(1L).roomName("회의실1").deleted(false).workSpace(workSpace).build();
- given(meetRoomRepository.findById(1L)).willReturn(Optional.of(meetRoom1));
- given(userRepository.findById(1L)).willReturn(Optional.of(
- User.builder()
- .id(1L)
- .email("wnddms12345@naver.com")
- .profileImage("https://avatars.githubusercontent.com/u/77449538?v=4")
- .userName("김민수")
- .provider("github")
- .role(Role.USER)
- .build()));
- given(workspaceRoomParticipants.get(anyString(), anyString())).willReturn(new HashMap<>());
+ given(mockUserService.getUserWorkSpaceId(1L)).willReturn("workspace-1");
+ given(mockRedisRepository.isUserInRoom(1L)).willReturn(true);
+ given(mockRedisRepository.getUserRoomId(1L)).willReturn("1");
+
// when
- JoinResponseDto joinResponseDto = meetRoomService.joinRoom(1L, "workspace-1", 1L);
+ meetRoomService.leaveRoom(1L);
// then
- assertThat(joinResponseDto).isNotNull();
- assertThat(joinResponseDto.getRoomId()).isEqualTo(1L);
- assertThat(joinResponseDto.getRoomName()).isEqualTo("회의실1");
+ verify(mockRedisRepository, times(1)).removeUserFromRoom("workspace-1", 1L, "1");
}
}
diff --git a/src/test/java/capstone/relation/security/WithMockCustomUser.java b/src/test/java/capstone/relation/security/WithMockCustomUser.java
new file mode 100644
index 0000000..dd1b207
--- /dev/null
+++ b/src/test/java/capstone/relation/security/WithMockCustomUser.java
@@ -0,0 +1,14 @@
+package capstone.relation.security;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+import org.springframework.security.test.context.support.WithSecurityContext;
+
+@Retention(RetentionPolicy.RUNTIME)
+@WithSecurityContext(factory = WithMockCustomUserSecurityContextFactory.class)
+public @interface WithMockCustomUser {
+ long id() default 1L;
+
+ String role() default "USER";
+}
\ No newline at end of file
diff --git a/src/test/java/capstone/relation/security/WithMockCustomUserSecurityContextFactory.java b/src/test/java/capstone/relation/security/WithMockCustomUserSecurityContextFactory.java
new file mode 100644
index 0000000..699087b
--- /dev/null
+++ b/src/test/java/capstone/relation/security/WithMockCustomUserSecurityContextFactory.java
@@ -0,0 +1,22 @@
+package capstone.relation.security;
+
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.test.context.support.WithSecurityContextFactory;
+
+import capstone.relation.api.auth.jwt.SecurityUser;
+
+public class WithMockCustomUserSecurityContextFactory
+ implements WithSecurityContextFactory {
+
+ @Override
+ public SecurityContext createSecurityContext(WithMockCustomUser customUser) {
+ SecurityContext context = SecurityContextHolder.createEmptyContext();
+ SecurityUser securityUser = SecurityUser.of(customUser.id(), customUser.role());
+ UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(securityUser, null,
+ securityUser.getAuthorities());
+ context.setAuthentication(auth);
+ return context;
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/capstone/relation/websocket/signaling/SignalingIntegrationTest.java b/src/test/java/capstone/relation/websocket/signaling/SignalingIntegrationTest.java
new file mode 100644
index 0000000..8a3ea3c
--- /dev/null
+++ b/src/test/java/capstone/relation/websocket/signaling/SignalingIntegrationTest.java
@@ -0,0 +1,244 @@
+package capstone.relation.websocket.signaling;
+
+import static capstone.relation.user.domain.Role.*;
+import static capstone.relation.websocket.signaling.dto.SignalMessageType.*;
+import static org.assertj.core.api.AssertionsForClassTypes.*;
+
+import java.lang.reflect.Type;
+import java.util.Date;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.MockitoAnnotations;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.web.server.LocalServerPort;
+import org.springframework.messaging.converter.MappingJackson2MessageConverter;
+import org.springframework.messaging.simp.stomp.StompFrameHandler;
+import org.springframework.messaging.simp.stomp.StompHeaders;
+import org.springframework.messaging.simp.stomp.StompSession;
+import org.springframework.messaging.simp.stomp.StompSessionHandlerAdapter;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+import org.springframework.web.socket.WebSocketHttpHeaders;
+import org.springframework.web.socket.client.standard.StandardWebSocketClient;
+import org.springframework.web.socket.messaging.WebSocketStompClient;
+
+import capstone.relation.api.auth.jwt.TokenProvider;
+import capstone.relation.user.domain.User;
+import capstone.relation.user.repository.UserRepository;
+import capstone.relation.websocket.signaling.dto.IceDto;
+import capstone.relation.websocket.signaling.dto.SdpDto;
+import capstone.relation.websocket.signaling.dto.SdpMessageDto;
+import capstone.relation.websocket.signaling.dto.SdpResponseDto;
+import capstone.relation.workspace.WorkSpace;
+import capstone.relation.workspace.repository.WorkSpaceRepository;
+
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+@ActiveProfiles("test") //테스트 프로필 활성화.
+@ExtendWith(SpringExtension.class) //단위 테스트에 공통적으로 사용할 확장 기능을 선언
+public class SignalingIntegrationTest {
+ @LocalServerPort
+ private int port;
+
+ private String WEBSOCKET_URI;
+ private String WEBSOCKET_TOPIC;
+
+ private WebSocketStompClient stompClient;
+ private User sender;
+ private User receiver;
+
+ private Date accessTokenExpiredDate;
+
+ @Autowired
+ private TokenProvider tokenProvider;
+
+ @Autowired
+ private WorkSpaceRepository workSpaceRepository;
+
+ @Autowired
+ private UserRepository userRepository;
+
+ @BeforeEach
+ public void setup() {
+ MockitoAnnotations.openMocks(this);
+ this.WEBSOCKET_URI = "ws://localhost:" + port + "/ws-chat";
+ this.WEBSOCKET_TOPIC = "/app";
+ this.stompClient = new WebSocketStompClient(new StandardWebSocketClient());
+ this.stompClient.setMessageConverter(new MappingJackson2MessageConverter());
+ sender = User.builder()
+ .email("wnddms12345@gmail.com")
+ .profileImage("profileImage")
+ .provider("local")
+ .userName("senderName")
+ .role(USER)
+ .build();
+ WorkSpace workSpace = new WorkSpace();
+ workSpace.setName("workSpaceName");
+ workSpaceRepository.save(workSpace);
+ sender.setWorkSpace(workSpace);
+ userRepository.save(sender);
+
+ receiver = User.builder()
+ .email("wnddms12345@gmail.com")
+ .profileImage("profileImage")
+ .provider("local")
+ .userName("receiverName")
+ .role(USER)
+ .build();
+ receiver.setWorkSpace(workSpace);
+ userRepository.save(receiver);
+
+ accessTokenExpiredDate = new Date(new Date().getTime() + 10000000L);
+ }
+
+ private StompSession connectWebSocket(User user) throws
+ InterruptedException,
+ ExecutionException {
+
+ System.out.println("유저 ID: " + user.getId());
+ String token = tokenProvider.generateAccessToken(user, accessTokenExpiredDate);
+ System.out.println("임시 엑세스 토큰 생성: " + token);
+
+ //연결 시 사용하는 JWT 설정
+ WebSocketHttpHeaders headers = new WebSocketHttpHeaders();
+ headers.add("Authorization", "Bearer " + token);
+ //STOMP 사용시 사용하는 JWT 설정
+ StompHeaders stompHeaders = new StompHeaders();
+ stompHeaders.add("Authorization", "Bearer " + token);
+
+ //STOMP 연결
+ StompSession session = stompClient.connectAsync(WEBSOCKET_URI, headers, stompHeaders,
+ new StompSessionHandlerAdapter() {
+ @Override
+ public void afterConnected(StompSession session, StompHeaders connectedHeaders) {
+ System.out.println("STOMP 연결 성공");
+ }
+ }).get();
+
+ return session;
+ }
+
+ @Test
+ @DisplayName("소켓으로 연결된 유저에게 ice 메시지 전송할 수 있다.")
+ public void testIce() throws InterruptedException, ExecutionException, TimeoutException {
+ BlockingQueue messageQueue = new LinkedBlockingQueue<>();
+
+ IceDto iceDto = new IceDto();
+ iceDto.setType(ScreenShare);
+ iceDto.setCandidate("candidate");
+ iceDto.setSdpMid("sdpMid");
+ iceDto.setSdpMLineIndex("sdpMLineIndex");
+ iceDto.setUserId(receiver.getId().toString());
+ //유저 2명 연결 1이 송신 2가 수신.
+ StompSession senderSession = connectWebSocket(sender);
+ StompSession receiverSession = connectWebSocket(receiver);
+ receiverSession.subscribe("/user/queue/ice/123", new StompFrameHandler() {
+ @Override
+ public Type getPayloadType(StompHeaders headers) {
+ System.out.println("구독 시작: /user/queue/ice/123");
+ return IceDto.class;
+ }
+
+ @Override
+ public void handleFrame(StompHeaders headers, Object payload) {
+ System.out.println("ice 응답값 : " + payload);
+ messageQueue.offer((IceDto)payload);
+ }
+ });
+ StompHeaders stompHeaders = new StompHeaders();
+ stompHeaders.setDestination(WEBSOCKET_TOPIC + "/ice/123");
+
+ senderSession.send(stompHeaders, iceDto);
+
+ IceDto receivedMessage = messageQueue.poll(5, TimeUnit.SECONDS);
+ assertThat(receivedMessage).isNotNull();
+ assertThat(receivedMessage.getUserId()).isEqualTo(sender.getId().toString());
+ }
+
+ @Test
+ @DisplayName("offer 테스트")
+ public void testOffer() throws InterruptedException, ExecutionException, TimeoutException {
+ BlockingQueue messageQueue = new LinkedBlockingQueue<>();
+
+ SdpMessageDto sdpMessageDto = new SdpMessageDto();
+ sdpMessageDto.setUserId(receiver.getId().toString());
+ sdpMessageDto.setType(ScreenShare);
+ SdpDto sdpDto = new SdpDto();
+ sdpDto.setSdp("sdp");
+ sdpDto.setType("offer");
+ sdpMessageDto.setSessionDescription(sdpDto);
+
+ //유저 2명 연결 1이 송신 2가 수신.
+ StompSession senderSession = connectWebSocket(sender);
+ StompSession receiverSession = connectWebSocket(receiver);
+ receiverSession.subscribe("/user/queue/offer/123", new StompFrameHandler() {
+ @Override
+ public Type getPayloadType(StompHeaders headers) {
+ System.out.println("구독 시작: /user/queue/offer/123");
+ return SdpResponseDto.class;
+ }
+
+ @Override
+ public void handleFrame(StompHeaders headers, Object payload) {
+ System.out.println("offer 응답값 : " + payload);
+ messageQueue.offer((SdpResponseDto)payload);
+ }
+ });
+
+ StompHeaders stompHeaders = new StompHeaders();
+ stompHeaders.setDestination(WEBSOCKET_TOPIC + "/offer/123");
+
+ senderSession.send(stompHeaders, sdpMessageDto);
+
+ SdpResponseDto receivedMessage = messageQueue.poll(5, TimeUnit.SECONDS);
+ assertThat(receivedMessage).isNotNull();
+ assertThat(receivedMessage.getUserInfo().getUserId()).isEqualTo(sender.getId());
+ }
+
+ @Test
+ @DisplayName("소켓으로 연결된 유저에게 answer 메시지 전송할 수 있다.")
+ public void answerTest() throws InterruptedException, ExecutionException, TimeoutException {
+ BlockingQueue messageQueue = new LinkedBlockingQueue<>();
+
+ SdpMessageDto sdpMessageDto = new SdpMessageDto();
+ SdpDto sdpDto = new SdpDto();
+ sdpDto.setSdp("sdp");
+ sdpDto.setType("answer");
+ sdpMessageDto.setSessionDescription(sdpDto);
+ sdpMessageDto.setUserId(receiver.getId().toString());
+ sdpMessageDto.setType(ScreenShare);
+
+ //유저 2명 연결 1이 송신 2가 수신.
+ StompSession senderSession = connectWebSocket(sender);
+ StompSession receiverSession = connectWebSocket(receiver);
+ receiverSession.subscribe("/user/queue/answer/123", new StompFrameHandler() {
+ @Override
+ public Type getPayloadType(StompHeaders headers) {
+ System.out.println("구독 시작: /user/queue/answer/123");
+ return SdpResponseDto.class;
+ }
+
+ @Override
+ public void handleFrame(StompHeaders headers, Object payload) {
+ System.out.println("ice 응답값 : " + payload);
+ messageQueue.offer((SdpResponseDto)payload);
+ }
+ });
+ StompHeaders stompHeaders = new StompHeaders();
+ stompHeaders.setDestination(WEBSOCKET_TOPIC + "/answer/123");
+
+ senderSession.send(stompHeaders, sdpMessageDto);
+
+ SdpResponseDto receivedMessage = messageQueue.poll(5, TimeUnit.SECONDS);
+ assertThat(receivedMessage).isNotNull();
+ // assertThat(receivedMessage.getUserId()).isEqualTo(sender.getId().toString());
+ }
+}
diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml
new file mode 100644
index 0000000..fcff3dc
--- /dev/null
+++ b/src/test/resources/application-test.yml
@@ -0,0 +1,48 @@
+spring:
+ datasource:
+ url: jdbc:h2:mem:test
+ driverClassName: org.h2.Driver
+ username: sa
+ password:
+
+ jpa:
+ hibernate:
+ ddl-auto: create
+
+oauth2:
+ jwt:
+ authorityKey: "roles"
+ bearerPrefix: "Bearer "
+ tokenSecret: "testSecretKeyShouldBeKeptSecretAndHasAtLeast32Bytes"
+ accessTokenHeader: "Authorization"
+ access-token-expire-day: 30
+ refresh-token-expire-day: 30
+ naver:
+ client-id: "test-client-id"
+ client-secret: "test-client-secret"
+ redirectUri: "http://localhost:8080/login/oauth2/code/naver"
+
+ base-url: "https://nid.naver.com"
+ token-path: "/oauth2.0/token"
+ user-info-path: "/oauth2.0/profile"
+ authorizationUri: "https://nid.naver.com/oauth2.0/authorize"
+ tokenUri: "https://nid.naver.com/oauth2.0/token"
+ userInfoUri: "https://openapi.naver.com/v1/nid/me"
+ userNameAttributeName: response
+
+ kakao:
+ name: Kakao
+ client-id: "test-client-id"
+ client-secret: "test-client-secret"
+ redirectUri: "http://localhost:8080/login/kakao/index.html"
+ authorizationUri: "https://kauth.kakao.com/oauth/authorize"
+ tokenUri: "https://kauth.kakao.com/oauth/token"
+ userInfoUri: "https://kapi.kakao.com/v2/user/me"
+ userNameAttributeName: id
+
+school:
+ info:
+ url: https://www.career.go.kr/cnet/openapi/getOpenApi
+ api:
+ key:
+ test-api-key
\ No newline at end of file