Skip to content

Commit 60e4d69

Browse files
committed
feat: 전체 랭킹 조회 구현
1 parent d7911fe commit 60e4d69

File tree

21 files changed

+458
-2
lines changed

21 files changed

+458
-2
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+
import lombok.RequiredArgsConstructor;
7+
import org.springframework.data.domain.Pageable;
8+
import org.springframework.data.domain.Sort.Direction;
9+
import org.springframework.data.web.PageableDefault;
10+
import org.springframework.http.ResponseEntity;
11+
import org.springframework.web.bind.annotation.GetMapping;
12+
import org.springframework.web.bind.annotation.RequestMapping;
13+
import org.springframework.web.bind.annotation.RestController;
14+
15+
@RestController
16+
@RequiredArgsConstructor
17+
@RequestMapping("/stats")
18+
public class StatController {
19+
20+
private final StatService statService;
21+
22+
@LimitPageSize
23+
@GetMapping("/rankings")
24+
public ResponseEntity<StatPageResponse> getRankings(
25+
@PageableDefault(sort = "score", direction = Direction.DESC)
26+
Pageable pageable
27+
) {
28+
StatPageResponse response = statService.getRanks(pageable);
29+
30+
return ResponseEntity.ok().body(response);
31+
}
32+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
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.dto.StatWithNickname;
6+
import io.f1.backend.domain.stat.dao.StatRepository;
7+
import io.f1.backend.domain.stat.dto.StatPageResponse;
8+
import lombok.RequiredArgsConstructor;
9+
import org.springframework.data.domain.Page;
10+
import org.springframework.data.domain.Pageable;
11+
import org.springframework.stereotype.Service;
12+
13+
@Service
14+
@RequiredArgsConstructor
15+
public class StatService {
16+
17+
private final StatRepository statRepository;
18+
19+
public StatPageResponse getRanks(Pageable pageable) {
20+
Page<StatWithNickname> stats = statRepository.findWithUser(pageable);
21+
return toStatListPageResponse(stats);
22+
}
23+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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+
import org.springframework.data.domain.Page;
6+
import org.springframework.data.domain.Pageable;
7+
import org.springframework.data.jpa.repository.JpaRepository;
8+
import org.springframework.data.jpa.repository.Query;
9+
10+
public interface StatRepository extends JpaRepository<Stat, Long> {
11+
12+
@Query(
13+
"""
14+
SELECT
15+
new io.f1.backend.domain.stat.dto.StatWithNickname
16+
(u.nickname, s.totalGames, s.winningGames, s.score)
17+
FROM
18+
Stat s JOIN s.user u
19+
""")
20+
Page<StatWithNickname> findWithUser(Pageable pageable);
21+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package io.f1.backend.domain.stat.dto;
2+
3+
import java.util.List;
4+
5+
public record StatPageResponse(
6+
int totalPages,
7+
int currentPage,
8+
int totalElements,
9+
List<StatResponse> ranks
10+
) { }
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package io.f1.backend.domain.stat.dto;
2+
3+
public record StatResponse(
4+
long rank,
5+
String nickname,
6+
long totalGames,
7+
long winningGames,
8+
long score
9+
) { }
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package io.f1.backend.domain.stat.dto;
2+
3+
public record StatWithNickname(
4+
String nickname,
5+
long totalGames,
6+
long winningGames,
7+
long score
8+
) { }

backend/src/main/java/io/f1/backend/domain/stat/entity/Stat.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@
1414

1515
import lombok.AccessLevel;
1616
import lombok.Builder;
17+
import lombok.Getter;
1718
import lombok.NoArgsConstructor;
1819

1920
@Entity
21+
@Getter
2022
@NoArgsConstructor(access = AccessLevel.PROTECTED)
2123
public class Stat extends BaseEntity {
2224

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package io.f1.backend.domain.stat.mapper;
2+
3+
import io.f1.backend.domain.stat.dto.StatWithNickname;
4+
import io.f1.backend.domain.stat.dto.StatPageResponse;
5+
import io.f1.backend.domain.stat.dto.StatResponse;
6+
import org.springframework.data.domain.Page;
7+
8+
public class StatMapper {
9+
10+
public static StatPageResponse toStatListPageResponse(Page<StatWithNickname> statPage) {
11+
int curPage = statPage.getNumber() + 1;
12+
13+
return new StatPageResponse(
14+
statPage.getTotalPages(),
15+
curPage,
16+
statPage.getNumberOfElements(),
17+
statPage.stream().map(
18+
stat -> {
19+
long rank = getRankFromPage(
20+
curPage,
21+
statPage.getSize(),
22+
statPage.getContent().indexOf(stat)
23+
);
24+
return toStatResponse(stat, rank);
25+
}
26+
).toList()
27+
);
28+
}
29+
30+
private static StatResponse toStatResponse(StatWithNickname stat, long rank) {
31+
return new StatResponse(
32+
rank,
33+
stat.nickname(),
34+
stat.totalGames(),
35+
stat.winningGames(),
36+
stat.score()
37+
);
38+
}
39+
40+
private static long getRankFromPage(int curPage, int pageSize, int index) {
41+
int startRank = (curPage - 1) * pageSize + 1;
42+
return startRank + index;
43+
}
44+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
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+
import org.springframework.core.MethodParameter;
7+
import org.springframework.data.domain.PageRequest;
8+
import org.springframework.data.domain.Pageable;
9+
import org.springframework.data.web.PageableHandlerMethodArgumentResolver;
10+
import org.springframework.web.bind.support.WebDataBinderFactory;
11+
import org.springframework.web.context.request.NativeWebRequest;
12+
import org.springframework.web.method.support.ModelAndViewContainer;
13+
14+
public class CustomPageableHandlerArgumentResolver extends PageableHandlerMethodArgumentResolver {
15+
16+
@Override
17+
public Pageable resolveArgument(
18+
MethodParameter methodParameter, ModelAndViewContainer mavContainer,
19+
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
20+
21+
Pageable pageable = super.resolveArgument(methodParameter, mavContainer, webRequest, binderFactory);
22+
23+
if (methodParameter.hasMethodAnnotation(LimitPageSize.class)) {
24+
LimitPageSize limitPageSize = methodParameter.getMethodAnnotation(LimitPageSize.class);
25+
validatePageable(limitPageSize, pageable);
26+
}
27+
28+
return PageRequest.of(oneIndexedPageNumber(pageable.getPageNumber()), pageable.getPageSize());
29+
}
30+
31+
private int oneIndexedPageNumber(int pageNumber) {
32+
return pageNumber <= 0 ? 0 : pageNumber - 1;
33+
}
34+
35+
private void validatePageable(LimitPageSize limitPageSize, Pageable pageable) {
36+
if (pageable.getPageSize() > limitPageSize.max()) {
37+
throw new CustomException(CommonErrorCode.INVALID_PAGINATION);
38+
}
39+
}
40+
}

0 commit comments

Comments
 (0)