diff --git a/backend/src/main/java/io/f1/backend/domain/stat/api/StatController.java b/backend/src/main/java/io/f1/backend/domain/stat/api/StatController.java index 38c14de8..2918612a 100644 --- a/backend/src/main/java/io/f1/backend/domain/stat/api/StatController.java +++ b/backend/src/main/java/io/f1/backend/domain/stat/api/StatController.java @@ -11,6 +11,7 @@ import org.springframework.data.web.PageableDefault; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -29,4 +30,13 @@ public ResponseEntity getRankings( return ResponseEntity.ok().body(response); } + + @LimitPageSize + @GetMapping("/rankings/{nickname}") + public ResponseEntity getRankingsByNickname( + @PathVariable String nickname, @PageableDefault Pageable pageable) { + StatPageResponse response = + statService.getRanksByNickname(nickname, pageable.getPageSize()); + return ResponseEntity.ok().body(response); + } } diff --git a/backend/src/main/java/io/f1/backend/domain/stat/app/StatService.java b/backend/src/main/java/io/f1/backend/domain/stat/app/StatService.java index b123c80d..53668555 100644 --- a/backend/src/main/java/io/f1/backend/domain/stat/app/StatService.java +++ b/backend/src/main/java/io/f1/backend/domain/stat/app/StatService.java @@ -5,12 +5,18 @@ import io.f1.backend.domain.stat.dao.StatRepository; import io.f1.backend.domain.stat.dto.StatPageResponse; import io.f1.backend.domain.stat.dto.StatWithNickname; +import io.f1.backend.global.exception.CustomException; +import io.f1.backend.global.exception.errorcode.RoomErrorCode; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Direction; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor @@ -18,8 +24,30 @@ public class StatService { private final StatRepository statRepository; + @Transactional(readOnly = true) public StatPageResponse getRanks(Pageable pageable) { - Page stats = statRepository.findWithUser(pageable); + Page stats = statRepository.findAllStatsWithUser(pageable); return toStatListPageResponse(stats); } + + @Transactional(readOnly = true) + public StatPageResponse getRanksByNickname(String nickname, int pageSize) { + + Page stats = + statRepository.findAllStatsWithUser(getPageableFromNickname(nickname, pageSize)); + + return toStatListPageResponse(stats); + } + + private Pageable getPageableFromNickname(String nickname, int pageSize) { + long score = + statRepository + .findScoreByNickname(nickname) + .orElseThrow(() -> new CustomException(RoomErrorCode.PLAYER_NOT_FOUND)); + + long rowNum = statRepository.countByScoreGreaterThan(score); + + int pageNumber = rowNum > 0 ? (int) (rowNum / pageSize) : 0; + return PageRequest.of(pageNumber, pageSize, Sort.by(Direction.DESC, "score")); + } } diff --git a/backend/src/main/java/io/f1/backend/domain/stat/dao/StatRepository.java b/backend/src/main/java/io/f1/backend/domain/stat/dao/StatRepository.java index 912e735c..445abc37 100644 --- a/backend/src/main/java/io/f1/backend/domain/stat/dao/StatRepository.java +++ b/backend/src/main/java/io/f1/backend/domain/stat/dao/StatRepository.java @@ -8,6 +8,8 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; +import java.util.Optional; + public interface StatRepository extends JpaRepository { @Query( @@ -18,5 +20,10 @@ public interface StatRepository extends JpaRepository { FROM Stat s JOIN s.user u """) - Page findWithUser(Pageable pageable); + Page findAllStatsWithUser(Pageable pageable); + + @Query("SELECT s.score FROM Stat s WHERE s.user.nickname = :nickname") + Optional findScoreByNickname(String nickname); + + long countByScoreGreaterThan(Long score); } diff --git a/backend/src/test/java/io/f1/backend/domain/stat/StatBrowserTest.java b/backend/src/test/java/io/f1/backend/domain/stat/StatBrowserTest.java index e3af6120..1120f5ed 100644 --- a/backend/src/test/java/io/f1/backend/domain/stat/StatBrowserTest.java +++ b/backend/src/test/java/io/f1/backend/domain/stat/StatBrowserTest.java @@ -1,6 +1,7 @@ package io.f1.backend.domain.stat; import static io.f1.backend.global.exception.errorcode.CommonErrorCode.INVALID_PAGINATION; +import static io.f1.backend.global.exception.errorcode.RoomErrorCode.PLAYER_NOT_FOUND; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; @@ -35,7 +36,7 @@ void totalRankingForSingleUser() throws Exception { @Test @DisplayName("100을 넘는 페이지 크기 요청이 오면 예외를 발생시킨다") - void totalRankingForSingleUserWithInvalidPageSize() throws Exception { + void totalRankingWithInvalidPageSize() throws Exception { // when ResultActions result = mockMvc.perform(get("/stats/rankings").param("size", "101")); @@ -86,4 +87,61 @@ void totalRankingForThreeUserWithPageSize2() throws Exception { jsonPath("$.totalElements").value(1), jsonPath("$.ranks.length()").value(1)); } + + @Test + @DataSet("datasets/stat/three-user-stat.yml") + @DisplayName("랭킹 페이지에서 존재하지 않는 닉네임을 검색하면 예외를 발생시킨다.") + void totalRankingWithUnregisteredNickname() throws Exception { + // given + String nickname = "UNREGISTERED"; + + // when + ResultActions result = mockMvc.perform(get("/stats/rankings/" + nickname)); + + // then + result.andExpectAll( + status().isNotFound(), jsonPath("$.code").value(PLAYER_NOT_FOUND.getCode())); + } + + @Test + @DataSet("datasets/stat/three-user-stat.yml") + @DisplayName("총 유저 수가 3명이고 페이지 크기가 2일 때 1위 유저의 닉네임을 검색하면 첫 번째 페이지에 2개의 결과를 반환한다") + void totalRankingForThreeUserWithFirstRankedNickname() throws Exception { + // given + String nickname = "USER3"; + + // when + ResultActions result = + mockMvc.perform(get("/stats/rankings/" + nickname).param("size", "2")); + + // then + result.andExpectAll( + status().isOk(), + jsonPath("$.totalPages").value(2), + jsonPath("$.currentPage").value(1), + jsonPath("$.totalElements").value(2), + jsonPath("$.ranks.length()").value(2), + jsonPath("$.ranks[0].nickname").value(nickname)); + } + + @Test + @DataSet("datasets/stat/three-user-stat.yml") + @DisplayName("총 유저 수가 3명이고 페이지 크기가 2일 때 3위 유저의 닉네임을 검색하면 두 번째 페이지에 1개의 결과를 반환한다") + void totalRankingForThreeUserWithLastRankedNickname() throws Exception { + // given + String nickname = "USER1"; + + // when + ResultActions result = + mockMvc.perform(get("/stats/rankings/" + nickname).param("size", "2")); + + // then + result.andExpectAll( + status().isOk(), + jsonPath("$.totalPages").value(2), + jsonPath("$.currentPage").value(2), + jsonPath("$.totalElements").value(1), + jsonPath("$.ranks.length()").value(1), + jsonPath("$.ranks[0].nickname").value(nickname)); + } }