-
Notifications
You must be signed in to change notification settings - Fork 1
Feature/283 봉사 시간 랭킹 기능 구현 #310
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 36 commits
24ca061
3d3bb57
627720f
9a0702f
902d44c
a754527
92a6ce4
0be813a
e65c87d
c2c8e0d
33c7151
756d27e
26da3b0
366523b
4abb947
482ddfa
b753096
b627ba1
d32c9e0
81a1e1c
98fa3af
e2ec50d
529ec4f
eeaf438
5587c96
c27cfbc
9f4953d
b6d7ac5
78e3cf1
c91552e
f2ff999
cb62123
450d909
0914343
03126bd
e625296
e6e3077
9a9331c
4557b6d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| package com.somemore.domains.volunteerrecord.controller; | ||
|
|
||
| import com.somemore.domains.volunteerrecord.dto.response.VolunteerRankingResponseDto; | ||
| import com.somemore.domains.volunteerrecord.usecase.GetVolunteerRankingUseCase; | ||
| import com.somemore.global.common.response.ApiResponse; | ||
| import io.swagger.v3.oas.annotations.Operation; | ||
| import io.swagger.v3.oas.annotations.tags.Tag; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.web.bind.annotation.*; | ||
|
|
||
| @Tag(name = "Volunteer Ranking API", description = "봉사 시간 랭킹 API") | ||
| @RequiredArgsConstructor | ||
| @RequestMapping("/api/volunteerrecord") | ||
| @RestController | ||
| public class VolunteerRankingApiController { | ||
|
|
||
| private final GetVolunteerRankingUseCase getVolunteerRankingUseCase; | ||
|
|
||
| @Operation(summary = "봉사 시간 랭킹", description = "봉사 시간 랭킹을 반환합니다.") | ||
| @GetMapping("/ranking") | ||
| public ApiResponse<VolunteerRankingResponseDto> updateCenterProfile() { | ||
|
|
||
| VolunteerRankingResponseDto volunteerRankings = getVolunteerRankingUseCase.getVolunteerRanking(); | ||
|
|
||
| return ApiResponse.ok(volunteerRankings,"봉사 시간 랭킹 반환 성공"); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| package com.somemore.domains.volunteerrecord.dto.response; | ||
|
|
||
| import java.util.UUID; | ||
|
|
||
| public record VolunteerMonthlyRankingResponseDto( | ||
| UUID volunteerId, | ||
| int totalHours, | ||
| long ranking, | ||
| String nickname | ||
| ) { | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| package com.somemore.domains.volunteerrecord.dto.response; | ||
|
|
||
| import com.fasterxml.jackson.databind.PropertyNamingStrategies; | ||
| import com.fasterxml.jackson.databind.annotation.JsonNaming; | ||
| import com.somemore.domains.center.dto.response.PreferItemResponseDto; | ||
| import lombok.Builder; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) | ||
| @Builder | ||
| public record VolunteerRankingResponseDto( | ||
| List<VolunteerTotalRankingResponseDto> volunteerTotalRankingResponse, | ||
| List<VolunteerMonthlyRankingResponseDto> volunteerMonthlyResponse, | ||
|
||
| List<VolunteerWeeklyRankingResponseDto> volunteerWeeklyRankingResponse | ||
| ) { | ||
| public static VolunteerRankingResponseDto of( | ||
| List<VolunteerTotalRankingResponseDto> totalRanking, | ||
| List<VolunteerMonthlyRankingResponseDto> monthlyRanking, | ||
| List<VolunteerWeeklyRankingResponseDto> weeklyRanking){ | ||
|
|
||
| return VolunteerRankingResponseDto.builder() | ||
| .volunteerTotalRankingResponse(totalRanking) | ||
| .volunteerMonthlyResponse(monthlyRanking) | ||
| .volunteerWeeklyRankingResponse(weeklyRanking) | ||
| .build(); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| package com.somemore.domains.volunteerrecord.dto.response; | ||
|
|
||
| import java.util.UUID; | ||
|
|
||
| public record VolunteerTotalRankingResponseDto( | ||
| UUID volunteerId, | ||
| int totalHours, | ||
| long ranking, | ||
| String nickname | ||
| ) { | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| package com.somemore.domains.volunteerrecord.dto.response; | ||
|
|
||
| import java.util.UUID; | ||
|
|
||
| public record VolunteerWeeklyRankingResponseDto( | ||
| UUID volunteerId, | ||
| int totalHours, | ||
| long ranking, | ||
| String nickname | ||
| ) { | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| package com.somemore.domains.volunteerrecord.repository; | ||
|
|
||
| import com.somemore.domains.volunteerrecord.dto.response.VolunteerMonthlyRankingResponseDto; | ||
| import com.somemore.domains.volunteerrecord.dto.response.VolunteerRankingResponseDto; | ||
| import com.somemore.domains.volunteerrecord.dto.response.VolunteerTotalRankingResponseDto; | ||
| import com.somemore.domains.volunteerrecord.dto.response.VolunteerWeeklyRankingResponseDto; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.data.redis.core.RedisTemplate; | ||
| import org.springframework.stereotype.Repository; | ||
|
|
||
| import java.time.Duration; | ||
| import java.util.List; | ||
| import java.util.Optional; | ||
|
|
||
| @RequiredArgsConstructor | ||
| @Repository | ||
| public class VolunteerRankingRedisRepository { | ||
|
||
|
|
||
| private final RedisTemplate<String, Object> redisTemplate; | ||
|
|
||
| private static final Duration CACHE_TTL = Duration.ofMinutes(60); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (끄덕)
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (끄덕) |
||
| private static final String TOTAL_RANKING_KEY = "volunteer:total_ranking"; | ||
| private static final String MONTHLY_RANKING_KEY = "volunteer:monthly_ranking"; | ||
| private static final String WEEKLY_RANKING_KEY = "volunteer:weekly_ranking"; | ||
|
|
||
| public void saveRanking(VolunteerRankingResponseDto rankings) { | ||
| redisTemplate.opsForValue().set(TOTAL_RANKING_KEY, rankings.volunteerTotalRankingResponse(), CACHE_TTL); | ||
| redisTemplate.opsForValue().set(MONTHLY_RANKING_KEY, rankings.volunteerMonthlyResponse(), CACHE_TTL); | ||
| redisTemplate.opsForValue().set(WEEKLY_RANKING_KEY, rankings.volunteerWeeklyRankingResponse(), CACHE_TTL); | ||
| } | ||
|
|
||
| @SuppressWarnings("unchecked") | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 어떤 오류가 있어서 설정하셨나요?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Json 직렬화시 나오는 무시해도 되는 경고 문구를 제거하려고 어노테이션을 추가했습니다!
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 넵 확인했습니다~ |
||
| public Optional<VolunteerRankingResponseDto> getRankings() { | ||
|
|
||
| List<VolunteerTotalRankingResponseDto> totalRanking = | ||
| (List<VolunteerTotalRankingResponseDto>) redisTemplate.opsForValue().get(TOTAL_RANKING_KEY); | ||
| List<VolunteerMonthlyRankingResponseDto> monthlyRanking = | ||
| (List<VolunteerMonthlyRankingResponseDto>) redisTemplate.opsForValue().get(MONTHLY_RANKING_KEY); | ||
| List<VolunteerWeeklyRankingResponseDto> weeklyRanking = | ||
| (List<VolunteerWeeklyRankingResponseDto>) redisTemplate.opsForValue().get(WEEKLY_RANKING_KEY); | ||
|
|
||
| if (totalRanking == null || monthlyRanking == null || weeklyRanking == null) { | ||
| return Optional.empty(); | ||
| } | ||
|
Comment on lines
+42
to
+44
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 하나라도 비어있으면 보내지 않으시는 이유가 있으신가요 ?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 프론트에서 탭으로 랭킹을 구분해서 보여주기때문에 월, 주간 같이 다른 탭을 클릭해야
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 여러 해결법이 있겠지만 좋다고 생각합니다~ 확인했습니다 |
||
|
|
||
| return Optional.of(VolunteerRankingResponseDto.of( | ||
| totalRanking, | ||
| monthlyRanking, | ||
| weeklyRanking | ||
| )); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,54 @@ | |
|
|
||
| import com.somemore.domains.volunteerrecord.domain.VolunteerRecord; | ||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
| import org.springframework.data.jpa.repository.Query; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| public interface VolunteerRecordJpaRepository extends JpaRepository<VolunteerRecord, Long> { | ||
| } | ||
|
|
||
| @Query(value = """ | ||
| SELECT volunteerId, totalHours, ranking | ||
| FROM ( | ||
| SELECT | ||
| vr.volunteer_id AS volunteerId, | ||
| SUM(vr.volunteer_hours) AS totalHours, | ||
| DENSE_RANK() OVER (ORDER BY SUM(vr.volunteer_hours) DESC) AS ranking | ||
| FROM volunteer_record vr | ||
| GROUP BY vr.volunteer_id | ||
| ) ranked | ||
| WHERE ranking <= 4 | ||
| """, nativeQuery = true) | ||
| List<Object[]> findTotalTopRankingWithTies(); | ||
|
Comment on lines
+11
to
+23
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. dense rank 와 같은 기능을 위해서 네이티브 쿼리를 활용하는 예시를 잘 확인했습니다! 혹시 일반적인 조회 기능정도의 역할을 데이터베이스에게 부여하고, 나머지는 자바로 처리하는 것은 어떤지 여쭤보고 싶습니다.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 일반적인 조회 기능정도의 역할을 데이터베이스에게 부여 DB에서 뷰 테이블을 만들고 뷰 테이블을 조회하는 방식도 생각을 했으나
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 네, 맞습니다. 저는 "네이티브 쿼리를 사용하지 않는 방식"에 대해서도 고민해보았습니다. 이 접근 방식은 일반적으로 충분히 효율적일 수 있지만, 최악의 경우, 예를 들어 공동 1등이 10,000명인 상황에서는 비효율적일 수 있다는 문제가 있습니다. |
||
|
|
||
| @Query(value = """ | ||
| SELECT volunteerId, totalHours, ranking | ||
| FROM ( | ||
| SELECT | ||
| vr.volunteer_id AS volunteerId, | ||
| SUM(vr.volunteer_hours) AS totalHours, | ||
| DENSE_RANK() OVER (ORDER BY SUM(vr.volunteer_hours) DESC) AS ranking | ||
| FROM volunteer_record vr | ||
| WHERE YEARWEEK(vr.volunteer_date, 1) = YEARWEEK(CURDATE(), 1) | ||
| GROUP BY vr.volunteer_id | ||
| ) ranked | ||
| WHERE ranking <= 4 | ||
| """, nativeQuery = true) | ||
| List<Object[]> findWeeklyTopRankingWithTies(); | ||
|
|
||
| @Query(value = """ | ||
| SELECT volunteerId, totalHours, ranking | ||
| FROM ( | ||
| SELECT | ||
| vr.volunteer_id AS volunteerId, | ||
| SUM(vr.volunteer_hours) AS totalHours, | ||
| DENSE_RANK() OVER (ORDER BY SUM(vr.volunteer_hours) DESC) AS ranking | ||
| FROM volunteer_record vr | ||
| WHERE MONTH(vr.volunteer_date) = MONTH(CURDATE()) | ||
| AND YEAR(vr.volunteer_date) = YEAR(CURDATE()) | ||
| GROUP BY vr.volunteer_id | ||
| ) ranked | ||
| WHERE ranking <= 4 | ||
| """, nativeQuery = true) | ||
| List<Object[]> findMonthlyTopRankingWithTies(); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,12 @@ | |
|
|
||
| import com.somemore.domains.volunteerrecord.domain.VolunteerRecord; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| public interface VolunteerRecordRepository { | ||
| void save(VolunteerRecord volunteerRecord); | ||
| List<Object[]> findTotalTopRankingWithTies(); | ||
| List<Object[]> findWeeklyTopRankingWithTies(); | ||
| List<Object[]> findMonthlyTopRankingWithTies(); | ||
|
Comment on lines
+9
to
+11
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 쿼리 결과 반환값을 명시적인 DTO로 지정할 수 없었는지 여쭤보고 싶습니다!
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 처음엔 DTO 타입을 넣어줬으나
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 넵! 아무래도 레디스 직렬화가 설정이 애매한 부분이 많더라구요. 추후에 수정해도 될 것 같습니다~ |
||
|
|
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
push 는 왜 지우셨는지 여쭤보고 싶습니다~
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
같은 로직을 두번 반복하는게 효율적이지 못하다는 생각이 들었고
이미 pr에서 CI를 하며 검증이 되었다고 생각해서 지웠습니다!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
좋아요~ 확인했습니다