Skip to content

Commit 3c89ab6

Browse files
authored
✨ feat: 전체 랭킹 조회 + DBRider 테스트 적용 (#73)
1 parent bc83994 commit 3c89ab6

File tree

20 files changed

+416
-1
lines changed

20 files changed

+416
-1
lines changed

backend/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ dependencies {
4242
testAnnotationProcessor 'org.projectlombok:lombok'
4343
testRuntimeOnly 'com.h2database:h2'
4444
testImplementation 'org.springframework.security:spring-security-test'
45+
testImplementation 'com.github.database-rider:rider-spring:1.44.0'
4546

4647
/* ETC */
4748
implementation 'org.apache.commons:commons-lang3:3.12.0'
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package io.f1.backend.domain.stat.api;
2+
3+
import io.f1.backend.domain.stat.app.StatService;
4+
import io.f1.backend.domain.stat.dto.StatPageResponse;
5+
import io.f1.backend.global.validation.LimitPageSize;
6+
7+
import lombok.RequiredArgsConstructor;
8+
9+
import org.springframework.data.domain.Pageable;
10+
import org.springframework.data.domain.Sort.Direction;
11+
import org.springframework.data.web.PageableDefault;
12+
import org.springframework.http.ResponseEntity;
13+
import org.springframework.web.bind.annotation.GetMapping;
14+
import org.springframework.web.bind.annotation.RequestMapping;
15+
import org.springframework.web.bind.annotation.RestController;
16+
17+
@RestController
18+
@RequiredArgsConstructor
19+
@RequestMapping("/stats")
20+
public class StatController {
21+
22+
private final StatService statService;
23+
24+
@LimitPageSize
25+
@GetMapping("/rankings")
26+
public ResponseEntity<StatPageResponse> getRankings(
27+
@PageableDefault(sort = "score", direction = Direction.DESC) Pageable pageable) {
28+
StatPageResponse response = statService.getRanks(pageable);
29+
30+
return ResponseEntity.ok().body(response);
31+
}
32+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package io.f1.backend.domain.stat.app;
2+
3+
import static io.f1.backend.domain.stat.mapper.StatMapper.toStatListPageResponse;
4+
5+
import io.f1.backend.domain.stat.dao.StatRepository;
6+
import io.f1.backend.domain.stat.dto.StatPageResponse;
7+
import io.f1.backend.domain.stat.dto.StatWithNickname;
8+
9+
import lombok.RequiredArgsConstructor;
10+
11+
import org.springframework.data.domain.Page;
12+
import org.springframework.data.domain.Pageable;
13+
import org.springframework.stereotype.Service;
14+
15+
@Service
16+
@RequiredArgsConstructor
17+
public class StatService {
18+
19+
private final StatRepository statRepository;
20+
21+
public StatPageResponse getRanks(Pageable pageable) {
22+
Page<StatWithNickname> stats = statRepository.findWithUser(pageable);
23+
return toStatListPageResponse(stats);
24+
}
25+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package io.f1.backend.domain.stat.dao;
2+
3+
import io.f1.backend.domain.stat.dto.StatWithNickname;
4+
import io.f1.backend.domain.stat.entity.Stat;
5+
6+
import org.springframework.data.domain.Page;
7+
import org.springframework.data.domain.Pageable;
8+
import org.springframework.data.jpa.repository.JpaRepository;
9+
import org.springframework.data.jpa.repository.Query;
10+
11+
public interface StatRepository extends JpaRepository<Stat, Long> {
12+
13+
@Query(
14+
"""
15+
SELECT
16+
new io.f1.backend.domain.stat.dto.StatWithNickname
17+
(u.nickname, s.totalGames, s.winningGames, s.score)
18+
FROM
19+
Stat s JOIN s.user u
20+
""")
21+
Page<StatWithNickname> findWithUser(Pageable pageable);
22+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package io.f1.backend.domain.stat.dto;
2+
3+
import java.util.List;
4+
5+
public record StatPageResponse(
6+
int totalPages, int currentPage, int totalElements, List<StatResponse> ranks) {}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package io.f1.backend.domain.stat.dto;
2+
3+
public record StatResponse(
4+
long rank, String nickname, long totalGames, long winningGames, long score) {}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package io.f1.backend.domain.stat.dto;
2+
3+
public record StatWithNickname(String nickname, long totalGames, long winningGames, long score) {}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package io.f1.backend.domain.stat.mapper;
2+
3+
import io.f1.backend.domain.stat.dto.StatPageResponse;
4+
import io.f1.backend.domain.stat.dto.StatResponse;
5+
import io.f1.backend.domain.stat.dto.StatWithNickname;
6+
7+
import org.springframework.data.domain.Page;
8+
9+
public class StatMapper {
10+
11+
public static StatPageResponse toStatListPageResponse(Page<StatWithNickname> statPage) {
12+
int curPage = statPage.getNumber() + 1;
13+
14+
return new StatPageResponse(
15+
statPage.getTotalPages(),
16+
curPage,
17+
statPage.getNumberOfElements(),
18+
statPage.stream()
19+
.map(
20+
stat -> {
21+
long rank =
22+
getRankFromPage(
23+
curPage,
24+
statPage.getSize(),
25+
statPage.getContent().indexOf(stat));
26+
return toStatResponse(stat, rank);
27+
})
28+
.toList());
29+
}
30+
31+
private static StatResponse toStatResponse(StatWithNickname stat, long rank) {
32+
return new StatResponse(
33+
rank, stat.nickname(), stat.totalGames(), stat.winningGames(), stat.score());
34+
}
35+
36+
private static long getRankFromPage(int curPage, int pageSize, int index) {
37+
int startRank = (curPage - 1) * pageSize + 1;
38+
return startRank + index;
39+
}
40+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package io.f1.backend.global.config;
2+
3+
import io.f1.backend.global.exception.CustomException;
4+
import io.f1.backend.global.exception.errorcode.CommonErrorCode;
5+
import io.f1.backend.global.validation.LimitPageSize;
6+
7+
import org.springframework.core.MethodParameter;
8+
import org.springframework.data.domain.PageRequest;
9+
import org.springframework.data.domain.Pageable;
10+
import org.springframework.data.web.PageableHandlerMethodArgumentResolver;
11+
import org.springframework.web.bind.support.WebDataBinderFactory;
12+
import org.springframework.web.context.request.NativeWebRequest;
13+
import org.springframework.web.method.support.ModelAndViewContainer;
14+
15+
public class CustomPageableHandlerArgumentResolver extends PageableHandlerMethodArgumentResolver {
16+
17+
@Override
18+
public Pageable resolveArgument(
19+
MethodParameter methodParameter,
20+
ModelAndViewContainer mavContainer,
21+
NativeWebRequest webRequest,
22+
WebDataBinderFactory binderFactory) {
23+
24+
Pageable pageable =
25+
super.resolveArgument(methodParameter, mavContainer, webRequest, binderFactory);
26+
27+
if (methodParameter.hasMethodAnnotation(LimitPageSize.class)) {
28+
LimitPageSize limitPageSize = methodParameter.getMethodAnnotation(LimitPageSize.class);
29+
validatePageable(limitPageSize, pageable);
30+
}
31+
32+
return PageRequest.of(
33+
oneIndexedPageNumber(pageable.getPageNumber()), pageable.getPageSize());
34+
}
35+
36+
private int oneIndexedPageNumber(int pageNumber) {
37+
return pageNumber <= 0 ? 0 : pageNumber - 1;
38+
}
39+
40+
private void validatePageable(LimitPageSize limitPageSize, Pageable pageable) {
41+
if (pageable.getPageSize() > limitPageSize.max()) {
42+
throw new CustomException(CommonErrorCode.INVALID_PAGINATION);
43+
}
44+
}
45+
}

backend/src/main/java/io/f1/backend/global/config/WebConfig.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@
33
import org.springframework.context.annotation.Bean;
44
import org.springframework.context.annotation.Configuration;
55
import org.springframework.web.filter.HiddenHttpMethodFilter;
6+
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
67
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
78
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
89

10+
import java.util.List;
11+
912
@Configuration
1013
public class WebConfig implements WebMvcConfigurer {
1114

@@ -17,6 +20,11 @@ public void addResourceHandlers(ResourceHandlerRegistry registry) {
1720
.addResourceLocations("file:images/thumbnail/");
1821
}
1922

23+
@Override
24+
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
25+
resolvers.add(new CustomPageableHandlerArgumentResolver());
26+
}
27+
2028
@Bean
2129
public HiddenHttpMethodFilter hiddenHttpMethodFilter() {
2230
return new HiddenHttpMethodFilter();

0 commit comments

Comments
 (0)