diff --git a/src/main/java/com/sajang/devracebackend/controller/AuthController.java b/src/main/java/com/sajang/devracebackend/controller/AuthController.java index 224569e..664ff24 100644 --- a/src/main/java/com/sajang/devracebackend/controller/AuthController.java +++ b/src/main/java/com/sajang/devracebackend/controller/AuthController.java @@ -37,17 +37,17 @@ public ResponseEntity> signup( return ResponseData.toResponseEntity(ResponseCode.CREATED_USER, signupResponseDto); // 이 reponseDto 내에 새로운 JWT Access 토큰이 들어있음. 이후 앞으로는 이걸로 헤더에 장착해야함. } - @PostMapping("/reissue") - @Operation(summary = "JWT Access Token 재발급 [JWT X]") - public ResponseEntity> reissue(@RequestBody AuthDto.ReissueRequest reissueRequestDto) { - AuthDto.TokenResponse tokenResponseDto = authService.reissue(reissueRequestDto); - return ResponseData.toResponseEntity(ResponseCode.REISSUE_SUCCESS, tokenResponseDto); - } - @DeleteMapping("/users") @Operation(summary = "마이 Page - 회원탈퇴 [JWT O]") public ResponseEntity withdrawal() { authService.withdrawal(); return ResponseData.toResponseEntity(ResponseCode.DELETE_USER); } + + @PostMapping("/reissue") + @Operation(summary = "로그인 유지 - JWT Access Token 재발급 [JWT X]") + public ResponseEntity> reissue(@RequestBody AuthDto.ReissueRequest reissueRequestDto) { + AuthDto.TokenResponse tokenResponseDto = authService.reissue(reissueRequestDto); + return ResponseData.toResponseEntity(ResponseCode.REISSUE_SUCCESS, tokenResponseDto); + } } diff --git a/src/main/java/com/sajang/devracebackend/controller/RoomController.java b/src/main/java/com/sajang/devracebackend/controller/RoomController.java index be5d077..7a18ad2 100644 --- a/src/main/java/com/sajang/devracebackend/controller/RoomController.java +++ b/src/main/java/com/sajang/devracebackend/controller/RoomController.java @@ -37,24 +37,34 @@ public ResponseEntity> createRoom(@RequestBody Pr } @GetMapping("/rooms") - @Operation(summary = "초대링크 방 조회 [JWT O]", description = "URI : /rooms?link={방링크 string}") + @Operation(summary = "초대 접속 - 초대링크 방 조회 [JWT O]", description = "URI : /rooms?link={방링크 string}") public ResponseEntity> findRoomByLink(@RequestParam(value = "link", required = true) String link) { RoomDto.Response roomResponseDto = roomService.findRoomByLink(link); return ResponseData.toResponseEntity(ResponseCode.READ_ROOM, roomResponseDto); } + @PutMapping("/rooms/{roomId}") + @Operation(summary = "입장 Page - 방 입장 대기열 나가기 [JWT O]") + public ResponseEntity userStopWaitRoom(@PathVariable(value = "roomId") Long roomId) { + userRoomService.userStopWaitRoom(roomId); + return ResponseData.toResponseEntity(ResponseCode.UPDATE_ROOM); + } + @GetMapping("/rooms/{roomId}") @Operation(summary = "문제풀이 Page - 문제풀이 페이지 정보 조회 [JWT O]") - public ResponseEntity> loadSolvingPage(@PathVariable(value = "roomId") Long roomId) { // value=""를 작성해주어야만, Swagger에서 api테스트할때 이름값이 뜸. - UserRoomDto.SolvePageResponse solvePageResponseDto = userRoomService.loadSolvingPage(roomId); + public ResponseEntity> loadSolvePage(@PathVariable(value = "roomId") Long roomId) { // value=""를 작성해주어야만, Swagger에서 api테스트할때 이름값이 뜸. + UserRoomDto.SolvePageResponse solvePageResponseDto = userRoomService.loadSolvePage(roomId); return ResponseData.toResponseEntity(ResponseCode.READ_USERROOM, solvePageResponseDto); } - @GetMapping("/rooms/{roomId}/access-check") - @Operation(summary = "문제풀이 Page - 문제풀이 페이지 접근허용 검사 [JWT O]", description = "isLeave == 0 or 1 or null") - public ResponseEntity> checkAccess(@PathVariable(value = "roomId") Long roomId) { - UserRoomDto.CheckAccessResponse checkAccessResponseDto = userRoomService.checkAccess(roomId); - return ResponseData.toResponseEntity(ResponseCode.READ_USERROOM, checkAccessResponseDto); + @PostMapping("/rooms/{roomId}") // 의미상 PUT보단 POST가 더 적합하다고 판단했음. + @Operation(summary = "문제풀이 Page - 문제풀이 성공 및 실패/퇴장 [JWT O]", description = "isRetry==0 : 첫풀이 경우 / isRetry==1 : 재풀이 경우") + public ResponseEntity solveProblem( + @PathVariable(value = "roomId") Long roomId, + @RequestBody UserRoomDto.SolveRequest solveRequest) { + + userRoomService.solveProblem(roomId, solveRequest); + return ResponseData.toResponseEntity(ResponseCode.UPDATE_USERROOM); // 비록 POST이지만, 비즈니스 로직은 update임. } @GetMapping("/rooms/{roomId}/state-check") @@ -64,21 +74,11 @@ public ResponseEntity> checkState(@Path return ResponseData.toResponseEntity(ResponseCode.READ_USERROOM, checkStateResponseDto); } - @PostMapping("/rooms/{roomId}") // 의미상 PUT보단 POST가 더 적합하다고 판단했음. - @Operation(summary = "문제풀이 Page - 문제풀이 성공 및 실패/퇴장 [JWT O]", description = "isRetry==0 : 첫풀이 경우 / isRetry==1 : 재풀이 경우") - public ResponseEntity passSolvingProblem( - @PathVariable(value = "roomId") Long roomId, - @RequestBody UserRoomDto.SolveRequest solveRequest) { - - userRoomService.passSolvingProblem(roomId, solveRequest); - return ResponseData.toResponseEntity(ResponseCode.UPDATE_USERROOM); // 비록 POST이지만, 비즈니스 로직은 update임. - } - - @PutMapping("/rooms/{roomId}") - @Operation(summary = "입장 Page - 방 입장 대기열 나가기 [JWT O]") - public ResponseEntity userStopWaitRoom(@PathVariable(value = "roomId") Long roomId) { - userRoomService.userStopWaitRoom(roomId); - return ResponseData.toResponseEntity(ResponseCode.UPDATE_ROOM); + @GetMapping("/rooms/{roomId}/access-check") + @Operation(summary = "문제풀이 Page - 문제풀이 페이지 접근허용 검사 [JWT O]", description = "isLeave == 0 or 1 or null") + public ResponseEntity> checkAccess(@PathVariable(value = "roomId") Long roomId) { + UserRoomDto.CheckAccessResponse checkAccessResponseDto = userRoomService.checkAccess(roomId); + return ResponseData.toResponseEntity(ResponseCode.READ_USERROOM, checkAccessResponseDto); } @MessageMapping("wait.enter") // 웹소켓 메시지 처리 (백엔드로 '/pub/wait.enter'를 호출시 이 브로커에서 처리) diff --git a/src/main/java/com/sajang/devracebackend/controller/TestController.java b/src/main/java/com/sajang/devracebackend/controller/TestController.java index d082bb8..5dd187d 100644 --- a/src/main/java/com/sajang/devracebackend/controller/TestController.java +++ b/src/main/java/com/sajang/devracebackend/controller/TestController.java @@ -16,7 +16,7 @@ public class TestController { @GetMapping("/health") - @Operation(summary = "서버 헬스체크 [JWT X]") + @Operation(summary = "AWS - 서버 헬스체크 [JWT X]") public ResponseEntity healthCheck() { return ResponseData.toResponseEntity(ResponseCode.HEALTHY_SUCCESS); } diff --git a/src/main/java/com/sajang/devracebackend/controller/UserController.java b/src/main/java/com/sajang/devracebackend/controller/UserController.java index b580086..be54ee8 100644 --- a/src/main/java/com/sajang/devracebackend/controller/UserController.java +++ b/src/main/java/com/sajang/devracebackend/controller/UserController.java @@ -51,6 +51,13 @@ public ResponseEntity updateUserProfile( return ResponseData.toResponseEntity(ResponseCode.UPDATE_USER); } + @GetMapping("/users/solved-count") + @Operation(summary = "문제풀이 Page - 사용자 백준 solvedCount값 조회 [JWT O]") + public ResponseEntity> findUserSolvedCount() { + UserDto.SolvedCountResponse solvedCountResponseDto = userService.findUserSolvedCount(); + return ResponseData.toResponseEntity(ResponseCode.READ_SOLVEDCOUNT, solvedCountResponseDto); + } + @GetMapping("/users/rooms") @Operation(summary = "내 코드 Page - 코드방 목록 조회/정렬/검색 [JWT O]", description = """ @@ -69,15 +76,8 @@ public ResponseEntity>> findCode @GetMapping("/users/rooms-check") @Operation(summary = "메인 Page - 참여중인 방 여부 검사 [JWT O]", description = "roomId == Long or null") - public ResponseEntity> checkCurrentRoom() { - UserDto.CheckRoomResponse checkRoomResponseDto = userService.checkCurrentRoom(); + public ResponseEntity> checkRoom() { + UserDto.CheckRoomResponse checkRoomResponseDto = userService.checkRoom(); return ResponseData.toResponseEntity(ResponseCode.READ_ROOM, checkRoomResponseDto); } - - @GetMapping("/users/solved-count") - @Operation(summary = "사용자 백준 solvedCount값 조회 [JWT O]") - public ResponseEntity> checkUserSolvedCount() { - UserDto.SolvedCountResponse solvedCountResponseDto = userService.checkUserSolvedCount(); - return ResponseData.toResponseEntity(ResponseCode.READ_SOLVEDCOUNT, solvedCountResponseDto); - } } diff --git a/src/main/java/com/sajang/devracebackend/dto/AuthDto.java b/src/main/java/com/sajang/devracebackend/dto/AuthDto.java index aae2c41..2a69d2b 100644 --- a/src/main/java/com/sajang/devracebackend/dto/AuthDto.java +++ b/src/main/java/com/sajang/devracebackend/dto/AuthDto.java @@ -11,19 +11,19 @@ public class AuthDto { @Getter @NoArgsConstructor - public static class ReissueRequest { + public static class SignupRequest { - private String accessToken; - private String refreshToken; + private String nickname; + private String bojId; + private Integer isImageChange; } @Getter @NoArgsConstructor - public static class SignupRequest { + public static class ReissueRequest { - private String nickname; - private String bojId; - private Integer isImageChange; + private String accessToken; + private String refreshToken; } @@ -33,21 +33,21 @@ public static class SignupRequest { @Getter @NoArgsConstructor @AllArgsConstructor - public static class TokenResponse { + public static class SignupResponse { - private String grantType; - private String accessToken; - private Long accessTokenExpiresIn; - private String refreshToken; + private UserDto.Response userResponseDto; + private AuthDto.TokenResponse tokenResponseDto; } @Builder @Getter @NoArgsConstructor @AllArgsConstructor - public static class SignupResponse { + public static class TokenResponse { - private UserDto.Response userResponseDto; - private AuthDto.TokenResponse tokenResponseDto; + private String grantType; + private String accessToken; + private Long accessTokenExpiresIn; + private String refreshToken; } } diff --git a/src/main/java/com/sajang/devracebackend/dto/UserRoomDto.java b/src/main/java/com/sajang/devracebackend/dto/UserRoomDto.java index 786d172..a1c34b0 100644 --- a/src/main/java/com/sajang/devracebackend/dto/UserRoomDto.java +++ b/src/main/java/com/sajang/devracebackend/dto/UserRoomDto.java @@ -3,8 +3,6 @@ import com.sajang.devracebackend.domain.enums.Language; import com.sajang.devracebackend.domain.enums.RoomState; import com.sajang.devracebackend.domain.mapping.UserRoom; -import com.sajang.devracebackend.dto.ProblemDto; -import com.sajang.devracebackend.dto.UserDto; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; diff --git a/src/main/java/com/sajang/devracebackend/repository/UserRepository.java b/src/main/java/com/sajang/devracebackend/repository/UserRepository.java index 7de886d..8f9b314 100644 --- a/src/main/java/com/sajang/devracebackend/repository/UserRepository.java +++ b/src/main/java/com/sajang/devracebackend/repository/UserRepository.java @@ -21,6 +21,6 @@ public interface UserRepository extends JpaRepository { // ===================== // Optional findBySocialTypeAndSocialId(SocialType socialType, String socialId); // '소셜 타입, 식별자'로 해당 회원을 찾기 위한 메소드 (차후 추가정보 입력 회원가입 가능) - boolean existsByBojId(String bojId); List findByIdIn(List userIdList); + boolean existsByBojId(String bojId); } \ No newline at end of file diff --git a/src/main/java/com/sajang/devracebackend/service/AuthService.java b/src/main/java/com/sajang/devracebackend/service/AuthService.java index 20f8cac..6e793a1 100644 --- a/src/main/java/com/sajang/devracebackend/service/AuthService.java +++ b/src/main/java/com/sajang/devracebackend/service/AuthService.java @@ -7,6 +7,6 @@ public interface AuthService { AuthDto.SignupResponse signup(MultipartFile imageFile, AuthDto.SignupRequest SignupRequestDto) throws IOException; - AuthDto.TokenResponse reissue(AuthDto.ReissueRequest reissueRequestDto); void withdrawal(); + AuthDto.TokenResponse reissue(AuthDto.ReissueRequest reissueRequestDto); } \ No newline at end of file diff --git a/src/main/java/com/sajang/devracebackend/service/ChatService.java b/src/main/java/com/sajang/devracebackend/service/ChatService.java index dc262dd..27c1def 100644 --- a/src/main/java/com/sajang/devracebackend/service/ChatService.java +++ b/src/main/java/com/sajang/devracebackend/service/ChatService.java @@ -5,6 +5,6 @@ import org.springframework.data.domain.Slice; public interface ChatService { - ChatDto.Response createChat(ChatDto.SaveRequest saveRequestDto); Slice findChatsByRoom(Long roomId, Pageable pageable); + ChatDto.Response createChat(ChatDto.SaveRequest saveRequestDto); } diff --git a/src/main/java/com/sajang/devracebackend/service/UserRoomService.java b/src/main/java/com/sajang/devracebackend/service/UserRoomService.java index b7dc026..8961712 100644 --- a/src/main/java/com/sajang/devracebackend/service/UserRoomService.java +++ b/src/main/java/com/sajang/devracebackend/service/UserRoomService.java @@ -9,10 +9,10 @@ public interface UserRoomService { UserRoom findUserRoomWithEagerRoom(Long userId, Long roomId, boolean isIncludeUserRoomList, boolean isIncludeProblem); RoomDto.WaitResponse userWaitRoom(RoomDto.WaitRequest waitRequestDto); - void usersEnterRoom(Long roomId); void userStopWaitRoom(Long roomId); - UserRoomDto.SolvePageResponse loadSolvingPage(Long roomId); - UserRoomDto.CheckAccessResponse checkAccess(Long roomId); - void passSolvingProblem(Long roomId, UserRoomDto.SolveRequest solveRequest); + void usersEnterRoom(Long roomId); + void solveProblem(Long roomId, UserRoomDto.SolveRequest solveRequest); + UserRoomDto.SolvePageResponse loadSolvePage(Long roomId); Page findCodeRooms(Integer isPass, Integer number, Pageable pageable); + UserRoomDto.CheckAccessResponse checkAccess(Long roomId); } diff --git a/src/main/java/com/sajang/devracebackend/service/UserService.java b/src/main/java/com/sajang/devracebackend/service/UserService.java index b2f2077..4af56ad 100644 --- a/src/main/java/com/sajang/devracebackend/service/UserService.java +++ b/src/main/java/com/sajang/devracebackend/service/UserService.java @@ -13,6 +13,6 @@ public interface UserService { List findUsersOriginal(List userIdList, boolean isDto); UserDto.Response findUserProfile(); void updateUserProfile(MultipartFile imageFile, UserDto.UpdateRequest updateRequestDto) throws IOException; - UserDto.SolvedCountResponse checkUserSolvedCount(); - UserDto.CheckRoomResponse checkCurrentRoom(); + UserDto.SolvedCountResponse findUserSolvedCount(); + UserDto.CheckRoomResponse checkRoom(); } \ No newline at end of file diff --git a/src/main/java/com/sajang/devracebackend/service/impl/AuthServiceImpl.java b/src/main/java/com/sajang/devracebackend/service/impl/AuthServiceImpl.java index 2d1fed8..69891b1 100644 --- a/src/main/java/com/sajang/devracebackend/service/impl/AuthServiceImpl.java +++ b/src/main/java/com/sajang/devracebackend/service/impl/AuthServiceImpl.java @@ -7,7 +7,6 @@ import com.sajang.devracebackend.dto.UserDto; import com.sajang.devracebackend.repository.UserRepository; import com.sajang.devracebackend.repository.UserRoomBatchRepository; -import com.sajang.devracebackend.repository.UserRoomRepository; import com.sajang.devracebackend.response.exception.Exception400; import com.sajang.devracebackend.security.jwt.TokenProvider; import com.sajang.devracebackend.service.AuthService; @@ -30,7 +29,6 @@ public class AuthServiceImpl implements AuthService { private final AwsS3Service awsS3Service; private final UserService userService; private final UserRepository userRepository; - private final UserRoomRepository userRoomRepository; private final UserRoomBatchRepository userRoomBatchRepository; private final TokenProvider tokenProvider; @@ -86,6 +84,23 @@ else if(imageFile == null && signupRequestDto.getIsImageChange() == 1) { // 기 return signupResponseDto; } + @Transactional + @Override + public void withdrawal() { + User user = userService.findLoginUser(); + List userRoomList = user.getUserRoomList(); + + // 자식 UserRoom 삭제 - hard delete + // userRoomRepository.deleteAll(userRoomList); // JPA의 deleteAll()은 SQL 쿼리가 UserRoom 개수만큼 날아가는 문제가 있음. + // userRoomRepository.deleteAllInBatch(userRoomList); // JPA의 deleteAllInBatch()은 10000개 이상의 데이터 처리시 stackoverflow 에러가 발생함. + userRoomBatchRepository.batchDelete(userRoomList); // JDBC의 batch delete를 활용하여, 대용량 Batch 삭제 처리가 가능함. -> DB 여러번 접근 방지 & 성능 향상 + + // 부모 User 삭제 - soft delete + user.deleteAccount(); + awsS3Service.deleteImage(user.getImageUrl()); // 이전의 사진은 AWS S3에서 삭제. + user.updateImage(null); // 탈퇴했기에 기본사진임을 명시하고자 null값으로 imageUrl 업데이트. + } + @Transactional @Override public AuthDto.TokenResponse reissue(AuthDto.ReissueRequest reissueRequestDto) { // Refresh Token으로 Access Token 재발급 메소드 @@ -115,21 +130,4 @@ public AuthDto.TokenResponse reissue(AuthDto.ReissueRequest reissueRequestDto) { AuthDto.TokenResponse tokenResponseDto = tokenProvider.generateAccessTokenByRefreshToken(userId, role, refreshToken); return tokenResponseDto; } - - @Transactional - @Override - public void withdrawal() { - User user = userService.findLoginUser(); - List userRoomList = user.getUserRoomList(); - - // 자식 UserRoom 삭제 - hard delete - // userRoomRepository.deleteAll(userRoomList); // JPA의 deleteAll()은 SQL 쿼리가 UserRoom 개수만큼 날아가는 문제가 있음. - // userRoomRepository.deleteAllInBatch(userRoomList); // JPA의 deleteAllInBatch()은 10000개 이상의 데이터 처리시 stackoverflow 에러가 발생함. - userRoomBatchRepository.batchDelete(userRoomList); // JDBC의 batch delete를 활용하여, 대용량 Batch 삭제 처리가 가능함. -> DB 여러번 접근 방지 & 성능 향상 - - // 부모 User 삭제 - soft delete - user.deleteAccount(); - awsS3Service.deleteImage(user.getImageUrl()); // 이전의 사진은 AWS S3에서 삭제. - user.updateImage(null); // 탈퇴했기에 기본사진임을 명시하고자 null값으로 imageUrl 업데이트. - } } diff --git a/src/main/java/com/sajang/devracebackend/service/impl/ChatServiceImpl.java b/src/main/java/com/sajang/devracebackend/service/impl/ChatServiceImpl.java index d877bc8..520e286 100644 --- a/src/main/java/com/sajang/devracebackend/service/impl/ChatServiceImpl.java +++ b/src/main/java/com/sajang/devracebackend/service/impl/ChatServiceImpl.java @@ -36,6 +36,40 @@ public class ChatServiceImpl implements ChatService { private final ChatRepository chatRepository; + @Transactional(readOnly = true) + @Override + public Slice findChatsByRoom(Long roomId, Pageable pageable) { + // < 채팅방 페이징 정렬 기준 > + // - 페이징을 할 때, 최신 날짜인 마지막 페이지를 0으로 설정하여 역순으로 페이징. + // - 한 페이지 내의 채팅은 날짜 오름차순으로 정렬. + // - 만약 퇴장한 경우에는, 본인 퇴장시각 이하까지의 채팅 내역을 조회. + // => 알고리즘: DESC로 DB에서 페이징 조회후, 각 페이지별로 날짜 오름차순 정렬. + + // 'UserRoom.room' Eager 로딩 (N+1 문제 해결) + UserRoom userRoom = userRoomService.findUserRoomWithEagerRoom(SecurityUtil.getCurrentMemberId(), roomId, false, false); + LocalDateTime leaveTime = userRoom.getLeaveTime(); + + // 퇴장한 경우, 본인 퇴장시각 이하까지의 채팅내역 페이징 조회 + Slice chatSlice; + if(userRoom.getIsLeave() == 1) chatSlice = chatRepository.findAllByRoomIdAndCreatedTimeLessThanEqual(roomId, leaveTime, pageable); + else chatSlice = chatRepository.findAllByRoomId(roomId, pageable); + + // DESC로 가져온 데이터를 다시 오름차순으로 페이지별 정렬 + List reversedChatList = new ArrayList<>(chatSlice.getContent()); // 새로운 리스트로 만듦으로써 불변성 해제. + Collections.reverse(reversedChatList); + + // 사용자 캐싱을 위한 맵 생성 (이미 검색한것은 다시 검색하지않도록 성능 향상) + Map cacheUserMap = new HashMap<>(); + + return new SliceImpl<>(reversedChatList.stream() + .map(chat -> { + Long senderId = chat.getSenderId(); + User senderUser = cacheUserMap.computeIfAbsent(senderId, id -> userService.findUser(id)); // 만약 캐시에 senderId키의 데이터가 없다면, DB조회하고 캐시에 추가. + return new ChatDto.Response(chat, senderUser.getNickname(), senderUser.getImageUrl()); + }) + .collect(Collectors.toList()), pageable, chatSlice.hasNext()); + } + @Transactional @Override public ChatDto.Response createChat(ChatDto.SaveRequest saveRequestDto) { @@ -77,38 +111,4 @@ else if(saveRequestDto.getMessageType().equals(MessageType.RANK)) { return new ChatDto.Response(chat, user.getNickname(), user.getImageUrl()); } - - @Transactional(readOnly = true) - @Override - public Slice findChatsByRoom(Long roomId, Pageable pageable) { - // < 채팅방 페이징 정렬 기준 > - // - 페이징을 할 때, 최신 날짜인 마지막 페이지를 0으로 설정하여 역순으로 페이징. - // - 한 페이지 내의 채팅은 날짜 오름차순으로 정렬. - // - 만약 퇴장한 경우에는, 본인 퇴장시각 이하까지의 채팅 내역을 조회. - // => 알고리즘: DESC로 DB에서 페이징 조회후, 각 페이지별로 날짜 오름차순 정렬. - - // 'UserRoom.room' Eager 로딩 (N+1 문제 해결) - UserRoom userRoom = userRoomService.findUserRoomWithEagerRoom(SecurityUtil.getCurrentMemberId(), roomId, false, false); - LocalDateTime leaveTime = userRoom.getLeaveTime(); - - // 퇴장한 경우, 본인 퇴장시각 이하까지의 채팅내역 페이징 조회 - Slice chatSlice; - if(userRoom.getIsLeave() == 1) chatSlice = chatRepository.findAllByRoomIdAndCreatedTimeLessThanEqual(roomId, leaveTime, pageable); - else chatSlice = chatRepository.findAllByRoomId(roomId, pageable); - - // DESC로 가져온 데이터를 다시 오름차순으로 페이지별 정렬 - List reversedChatList = new ArrayList<>(chatSlice.getContent()); // 새로운 리스트로 만듦으로써 불변성 해제. - Collections.reverse(reversedChatList); - - // 사용자 캐싱을 위한 맵 생성 (이미 검색한것은 다시 검색하지않도록 성능 향상) - Map cacheUserMap = new HashMap<>(); - - return new SliceImpl<>(reversedChatList.stream() - .map(chat -> { - Long senderId = chat.getSenderId(); - User senderUser = cacheUserMap.computeIfAbsent(senderId, id -> userService.findUser(id)); // 만약 캐시에 senderId키의 데이터가 없다면, DB조회하고 캐시에 추가. - return new ChatDto.Response(chat, senderUser.getNickname(), senderUser.getImageUrl()); - }) - .collect(Collectors.toList()), pageable, chatSlice.hasNext()); - } } diff --git a/src/main/java/com/sajang/devracebackend/service/impl/UserRoomServiceImpl.java b/src/main/java/com/sajang/devracebackend/service/impl/UserRoomServiceImpl.java index 1037453..38b0b31 100644 --- a/src/main/java/com/sajang/devracebackend/service/impl/UserRoomServiceImpl.java +++ b/src/main/java/com/sajang/devracebackend/service/impl/UserRoomServiceImpl.java @@ -90,6 +90,13 @@ public RoomDto.WaitResponse userWaitRoom(RoomDto.WaitRequest waitRequestDto) { return waitResponseDto; } + @Transactional + @Override + public void userStopWaitRoom(Long roomId) { + Room room = roomService.findRoom(roomId); + room.deleteWaiting(SecurityUtil.getCurrentMemberId(), false); // 대기자 목록에서 해당 사용자 제거. + } + @Transactional @Override public void usersEnterRoom(Long roomId) { @@ -133,51 +140,7 @@ public void usersEnterRoom(Long roomId) { @Transactional @Override - public void userStopWaitRoom(Long roomId) { - Room room = roomService.findRoom(roomId); - room.deleteWaiting(SecurityUtil.getCurrentMemberId(), false); // 대기자 목록에서 해당 사용자 제거. - } - - @Transactional(readOnly = true) - @Override - public UserRoomDto.SolvePageResponse loadSolvingPage(Long roomId) { - - // 'UserRoom.room & UserRoom.room.problem' Eager 로딩 (N+1 문제 해결) - UserRoom userRoom = findUserRoomWithEagerRoom(SecurityUtil.getCurrentMemberId(), roomId, false, true); // 어차피 문제풀이 페이지는 입장 이후이기에, 부모 Room을 갖고있는 자식 UserRoom은 반드시 존재함. - - List rankUserIdList = userRoom.getRoom().getWaiting(); - List rankUserDtoList = userService.findUsersOriginal(rankUserIdList, true); - - return new UserRoomDto.SolvePageResponse(userRoom, rankUserDtoList); // Fetch Join으로 UserRoom 내부의 UserRoom.room과 UserRoom.room.problem은 Eager 로딩 처리되어, N+1 문제가 발생하지 않음. - } - - @Transactional(readOnly = true) - @Override - public UserRoomDto.CheckAccessResponse checkAccess(Long roomId) { - - // 'UserRoom.room' Eager 로딩 (N+1 문제 해결) - Optional optionalUserRoom = userRoomRepository.findByUser_IdAndRoom_Id(SecurityUtil.getCurrentMemberId(), roomId); - - // UserRoom이 존재하면 해당 정보 사용. 그렇지 않다면 DB에 Room 조회 쿼리 날림. - Room room = optionalUserRoom - .map(UserRoom::getRoom) // 이 시점에는 아직 @EntityGraph의 영향을 받지않아, 아직 조회 쿼리가 1번으로 유지됨. - .orElseGet(() -> roomService.findRoom(roomId)); - - Boolean isExistUserRoom = optionalUserRoom.isPresent(); - Integer isLeave = optionalUserRoom.map(UserRoom::getIsLeave).orElse(null); // UserRoom이 없다면 isLeave는 null - - UserRoomDto.CheckAccessResponse checkAccessResponseDto = UserRoomDto.CheckAccessResponse.builder() - .isExistUserRoom(isExistUserRoom) - .roomState(room.getRoomState()) // @EntityGraph로 UserRoom 내부의 Room은 Eager 로딩 처리되어, N+1 문제가 발생하지 않음. - .isLeave(isLeave) - .build(); - - return checkAccessResponseDto; - } - - @Transactional - @Override - public void passSolvingProblem(Long roomId, UserRoomDto.SolveRequest solveRequestDto) { + public void solveProblem(Long roomId, UserRoomDto.SolveRequest solveRequestDto) { // 'UserRoom.room & UserRoom.userRoomList' Eager 로딩 (N+1 문제 해결) UserRoom userRoom = findUserRoomWithEagerRoom(SecurityUtil.getCurrentMemberId(), roomId, true, false); // 어차피 문제풀이 페이지는 입장 이후이기에, 부모 Room을 갖고있는 자식 UserRoom은 반드시 존재함. @@ -198,6 +161,19 @@ public void passSolvingProblem(Long roomId, UserRoomDto.SolveRequest solveReques if(isLeaveAllUsers == true) room.updateRoomState(RoomState.FINISH); } + @Transactional(readOnly = true) + @Override + public UserRoomDto.SolvePageResponse loadSolvePage(Long roomId) { + + // 'UserRoom.room & UserRoom.room.problem' Eager 로딩 (N+1 문제 해결) + UserRoom userRoom = findUserRoomWithEagerRoom(SecurityUtil.getCurrentMemberId(), roomId, false, true); // 어차피 문제풀이 페이지는 입장 이후이기에, 부모 Room을 갖고있는 자식 UserRoom은 반드시 존재함. + + List rankUserIdList = userRoom.getRoom().getWaiting(); + List rankUserDtoList = userService.findUsersOriginal(rankUserIdList, true); + + return new UserRoomDto.SolvePageResponse(userRoom, rankUserDtoList); // Fetch Join으로 UserRoom 내부의 UserRoom.room과 UserRoom.room.problem은 Eager 로딩 처리되어, N+1 문제가 발생하지 않음. + } + @Transactional(readOnly = true) @Override public Page findCodeRooms(Integer isPass, Integer number, Pageable pageable) { @@ -221,6 +197,30 @@ else if(isPass == null && number != null) { // 문제번호 검색 조회의 return userRoomPage.map(userRoom -> new UserRoomDto.CodePageResponse(userRoom)); } + @Transactional(readOnly = true) + @Override + public UserRoomDto.CheckAccessResponse checkAccess(Long roomId) { + + // 'UserRoom.room' Eager 로딩 (N+1 문제 해결) + Optional optionalUserRoom = userRoomRepository.findByUser_IdAndRoom_Id(SecurityUtil.getCurrentMemberId(), roomId); + + // UserRoom이 존재하면 해당 정보 사용. 그렇지 않다면 DB에 Room 조회 쿼리 날림. + Room room = optionalUserRoom + .map(UserRoom::getRoom) // 이 시점에는 아직 @EntityGraph의 영향을 받지않아, 아직 조회 쿼리가 1번으로 유지됨. + .orElseGet(() -> roomService.findRoom(roomId)); + + Boolean isExistUserRoom = optionalUserRoom.isPresent(); + Integer isLeave = optionalUserRoom.map(UserRoom::getIsLeave).orElse(null); // UserRoom이 없다면 isLeave는 null + + UserRoomDto.CheckAccessResponse checkAccessResponseDto = UserRoomDto.CheckAccessResponse.builder() + .isExistUserRoom(isExistUserRoom) + .roomState(room.getRoomState()) // @EntityGraph로 UserRoom 내부의 Room은 Eager 로딩 처리되어, N+1 문제가 발생하지 않음. + .isLeave(isLeave) + .build(); + + return checkAccessResponseDto; + } + // ========== 유틸성 메소드 ========== // diff --git a/src/main/java/com/sajang/devracebackend/service/impl/UserServiceImpl.java b/src/main/java/com/sajang/devracebackend/service/impl/UserServiceImpl.java index 53a78d3..b34dc4f 100644 --- a/src/main/java/com/sajang/devracebackend/service/impl/UserServiceImpl.java +++ b/src/main/java/com/sajang/devracebackend/service/impl/UserServiceImpl.java @@ -91,7 +91,7 @@ else if(imageFile == null && updateRequestDto.getIsImageChange() == 1) { // 기 @Transactional(readOnly = true) @Override - public UserDto.SolvedCountResponse checkUserSolvedCount() { + public UserDto.SolvedCountResponse findUserSolvedCount() { User user = findLoginUser(); UserDto.SolvedCountResponse solvedCountResponseDto = getSolvedCount(user.getBojId()); @@ -100,7 +100,7 @@ public UserDto.SolvedCountResponse checkUserSolvedCount() { @Transactional(readOnly = true) @Override - public UserDto.CheckRoomResponse checkCurrentRoom() { + public UserDto.CheckRoomResponse checkRoom() { // 'User.userRoomList' Eager 로딩 (N+1 문제 해결) User user = userRepository.findByIdWithEagerUserRoomList(SecurityUtil.getCurrentMemberId()).orElseThrow(