From 570162c304a49f9d4a056f3dc82944197cb5b51f Mon Sep 17 00:00:00 2001 From: dlsrks1021 Date: Sat, 19 Jul 2025 16:42:39 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20=EC=9C=A0=EC=A0=80=20=EB=8B=89?= =?UTF-8?q?=EB=84=A4=EC=9E=84=EC=9D=84=20=ED=86=B5=ED=95=B4=20=EB=9E=AD?= =?UTF-8?q?=ED=82=B9=20=EC=A1=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/stat/api/StatController.java | 11 ++++ .../backend/domain/stat/app/StatService.java | 30 +++++++++ .../domain/stat/dao/StatRepository.java | 6 ++ .../backend/domain/stat/StatBrowserTest.java | 62 ++++++++++++++++++- 4 files changed, 108 insertions(+), 1 deletion(-) 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..ee6fc9ae 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,14 @@ 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..67c4ec6b 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 @@ -6,20 +6,50 @@ 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.ErrorCode; +import io.f1.backend.global.exception.errorcode.RoomErrorCode; + +import java.util.Optional; 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 +@Transactional @RequiredArgsConstructor public class StatService { private final StatRepository statRepository; + @Transactional(readOnly = true) public StatPageResponse getRanks(Pageable pageable) { Page stats = statRepository.findWithUser(pageable); return toStatListPageResponse(stats); } + + @Transactional(readOnly = true) + public StatPageResponse getRanksByNickname(String nickname, int pageSize) { + + Page stats = statRepository.findWithUser( + 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..8ef34e70 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 @@ -3,6 +3,7 @@ import io.f1.backend.domain.stat.dto.StatWithNickname; import io.f1.backend.domain.stat.entity.Stat; +import java.util.Optional; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; @@ -19,4 +20,9 @@ public interface StatRepository extends JpaRepository { Stat s JOIN s.user u """) Page findWithUser(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..86aba256 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 @@ -2,12 +2,15 @@ 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; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.github.database.rider.core.api.dataset.DataSet; +import io.f1.backend.global.exception.errorcode.ErrorCode; +import io.f1.backend.global.exception.errorcode.RoomErrorCode; import io.f1.backend.global.template.BrowserTestTemplate; import org.junit.jupiter.api.DisplayName; @@ -35,7 +38,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 +89,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)); + } } From 102d8144d58795bc8da2b940417df3855bc8dc0b Mon Sep 17 00:00:00 2001 From: github-actions <> Date: Sat, 19 Jul 2025 07:42:56 +0000 Subject: [PATCH 2/4] =?UTF-8?q?chore:=20Java=20=EC=8A=A4=ED=83=80=EC=9D=BC?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/stat/api/StatController.java | 17 ++- .../backend/domain/stat/app/StatService.java | 33 +++-- .../domain/stat/dao/StatRepository.java | 9 +- .../backend/domain/stat/StatBrowserTest.java | 116 +++++++++--------- 4 files changed, 86 insertions(+), 89 deletions(-) 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 ee6fc9ae..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 @@ -31,13 +31,12 @@ 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); - } + @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 67c4ec6b..f9457e83 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,9 @@ 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.ErrorCode; import io.f1.backend.global.exception.errorcode.RoomErrorCode; -import java.util.Optional; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; @@ -28,28 +25,30 @@ public class StatService { private final StatRepository statRepository; - @Transactional(readOnly = true) + @Transactional(readOnly = true) public StatPageResponse getRanks(Pageable pageable) { Page stats = statRepository.findWithUser(pageable); return toStatListPageResponse(stats); } - @Transactional(readOnly = true) - public StatPageResponse getRanksByNickname(String nickname, int pageSize) { + @Transactional(readOnly = true) + public StatPageResponse getRanksByNickname(String nickname, int pageSize) { - Page stats = statRepository.findWithUser( - getPageableFromNickname(nickname, pageSize)); + Page stats = + statRepository.findWithUser(getPageableFromNickname(nickname, pageSize)); - return toStatListPageResponse(stats); - } + return toStatListPageResponse(stats); + } - private Pageable getPageableFromNickname(String nickname, int pageSize) { - long score = statRepository.findScoreByNickname(nickname) - .orElseThrow(() -> new CustomException(RoomErrorCode.PLAYER_NOT_FOUND)); + private Pageable getPageableFromNickname(String nickname, int pageSize) { + long score = + statRepository + .findScoreByNickname(nickname) + .orElseThrow(() -> new CustomException(RoomErrorCode.PLAYER_NOT_FOUND)); - long rowNum = statRepository.countByScoreGreaterThan(score); + long rowNum = statRepository.countByScoreGreaterThan(score); - int pageNumber = rowNum > 0 ? (int) (rowNum / pageSize) : 0; - return PageRequest.of(pageNumber, pageSize, Sort.by(Direction.DESC, "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 8ef34e70..16d87cec 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 @@ -3,12 +3,13 @@ import io.f1.backend.domain.stat.dto.StatWithNickname; import io.f1.backend.domain.stat.entity.Stat; -import java.util.Optional; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; +import java.util.Optional; + public interface StatRepository extends JpaRepository { @Query( @@ -21,8 +22,8 @@ public interface StatRepository extends JpaRepository { """) Page findWithUser(Pageable pageable); - @Query("SELECT s.score FROM Stat s WHERE s.user.nickname = :nickname") - Optional findScoreByNickname(String nickname); + @Query("SELECT s.score FROM Stat s WHERE s.user.nickname = :nickname") + Optional findScoreByNickname(String nickname); - long countByScoreGreaterThan(Long score); + 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 86aba256..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,16 +1,14 @@ 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; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.github.database.rider.core.api.dataset.DataSet; -import io.f1.backend.global.exception.errorcode.ErrorCode; -import io.f1.backend.global.exception.errorcode.RoomErrorCode; import io.f1.backend.global.template.BrowserTestTemplate; import org.junit.jupiter.api.DisplayName; @@ -90,60 +88,60 @@ void totalRankingForThreeUserWithPageSize2() throws Exception { 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)); - } + @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)); + } } From a72ec5f867a9239c296bd043d8afc2e5d06cfb52 Mon Sep 17 00:00:00 2001 From: dlsrks1021 Date: Sat, 19 Jul 2025 22:31:13 +0900 Subject: [PATCH 3/4] =?UTF-8?q?chore:=20=EB=B6=88=ED=95=84=EC=9A=94=20?= =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=20=EB=A0=88=EB=B2=A8=20=ED=8A=B8?= =?UTF-8?q?=EB=9E=9C=EC=9E=AD=EC=85=98=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/io/f1/backend/domain/stat/app/StatService.java | 1 - 1 file changed, 1 deletion(-) 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 f9457e83..3c6d66ed 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 @@ -19,7 +19,6 @@ import org.springframework.transaction.annotation.Transactional; @Service -@Transactional @RequiredArgsConstructor public class StatService { From 77830c8529e82d8e574e71a1e984a04e59b6e0f3 Mon Sep 17 00:00:00 2001 From: dlsrks1021 Date: Sun, 20 Jul 2025 15:03:27 +0900 Subject: [PATCH 4/4] =?UTF-8?q?chore:=20ranking=20=EC=BF=BC=EB=A6=AC=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=20=EB=AA=85=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/io/f1/backend/domain/stat/app/StatService.java | 4 ++-- .../java/io/f1/backend/domain/stat/dao/StatRepository.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) 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 3c6d66ed..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 @@ -26,7 +26,7 @@ public class StatService { @Transactional(readOnly = true) public StatPageResponse getRanks(Pageable pageable) { - Page stats = statRepository.findWithUser(pageable); + Page stats = statRepository.findAllStatsWithUser(pageable); return toStatListPageResponse(stats); } @@ -34,7 +34,7 @@ public StatPageResponse getRanks(Pageable pageable) { public StatPageResponse getRanksByNickname(String nickname, int pageSize) { Page stats = - statRepository.findWithUser(getPageableFromNickname(nickname, pageSize)); + statRepository.findAllStatsWithUser(getPageableFromNickname(nickname, pageSize)); return toStatListPageResponse(stats); } 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 16d87cec..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 @@ -20,7 +20,7 @@ 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);