Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@
@RestController
@Slf4j
@RequiredArgsConstructor
@RequestMapping("/api/profile")
@Tag(name = "GET Volunteer", description = "봉사자 조회")
@RequestMapping("/api/volunteer/profile")
@Tag(name = "GET Volunteer Profile", description = "봉사자 조회")
public class VolunteerProfileQueryController {

private final VolunteerQueryUseCase volunteerQueryUseCase;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.somemore.volunteer.controller;

import com.somemore.global.common.response.ApiResponse;
import com.somemore.volunteer.dto.response.VolunteerRankingResponseDto;
import com.somemore.volunteer.usecase.VolunteerQueryUseCase;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Slf4j
@RequiredArgsConstructor
@RequestMapping("/api/volunteer/ranking")
@Tag(name = "GET Volunteer ranking", description = "봉사자 랭킹 조회")
public class VolunteerRankingQueryController {

private final VolunteerQueryUseCase volunteerQueryUseCase;

@Operation(summary = "봉사 시간 랭킹 조회", description = "봉사 시간 내림차순 4명 조회")
@GetMapping("/hours")
public ApiResponse<VolunteerRankingResponseDto> getRankingByHours() {

return ApiResponse.ok(
200,
volunteerQueryUseCase.getRankingByHours(),
"랭킹(시간) 조회 성공");
}
}
5 changes: 5 additions & 0 deletions src/main/java/com/somemore/volunteer/domain/Volunteer.java
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ public void updateWith(VolunteerProfileUpdateRequestDto dto, String imgUrl) {
this.imgUrl = imgUrl;
}

public void updateVolunteerStats(int hours, int count) {
this.totalVolunteerHours += hours;
this.totalVolunteerCount += count;
}

@Builder
private Volunteer(
OAuthProvider oauthProvider,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.somemore.volunteer.dto.response;

import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import com.somemore.volunteer.repository.mapper.VolunteerOverviewForRankingByHours;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;

import java.util.List;

@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
@Builder
public record VolunteerRankingResponseDto(
@Schema(description = "랭킹에 포함된 봉사자 리스트")
List<VolunteerOverview> rankings
) {
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
@Builder
public record VolunteerOverview(
@Schema(description = "봉사자 ID", example = "uuid-uuid-uuid-uuid")
String volunteerId,

@Schema(description = "봉사자 닉네임", example = "길동이")
String nickname,

@Schema(description = "봉사자 이미지 URL", example = "http://example.com/image.jpg")
String imgUrl,

@Schema(description = "봉사자 소개글", example = "안녕하세요! 저는 자원봉사자 홍길동입니다.")
String introduce,

@Schema(description = "봉사자 티어", example = "red")
String tier,

@Schema(description = "봉사자의 총 봉사 시간", example = "120")
Integer totalVolunteerHours
) {
private static VolunteerOverview from(VolunteerOverviewForRankingByHours source) {
return VolunteerOverview.builder()
.volunteerId(source.volunteerId().toString())
.nickname(source.nickname())
.imgUrl(source.imgUrl())
.introduce(source.introduce())
.tier(source.tier().name())
.totalVolunteerHours(source.totalVolunteerHours())
.build();
}
}

public static VolunteerRankingResponseDto from(List<VolunteerOverviewForRankingByHours> sources) {
return VolunteerRankingResponseDto.builder()
.rankings(sources.stream()
.map(VolunteerOverview::from)
.toList())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package com.somemore.volunteer.repository;

import com.somemore.volunteer.domain.Volunteer;
import com.somemore.volunteer.dto.response.VolunteerRankingResponseDto;
import com.somemore.volunteer.repository.mapper.VolunteerOverviewForRankingByHours;
import org.springframework.stereotype.Repository;

import java.util.List;
import java.util.Optional;
import java.util.UUID;

Expand All @@ -12,5 +15,7 @@ public interface VolunteerRepository {
Optional<Volunteer> findById(UUID id);
Optional<Volunteer> findByOauthId(String oauthId);
String findNicknameById(UUID id);
List<VolunteerOverviewForRankingByHours> findRankingByVolunteerHours();

void deleteAllInBatch();
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
package com.somemore.volunteer.repository;

import com.querydsl.core.types.Path;
import com.querydsl.core.types.Projections;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.impl.JPAQueryFactory;
import com.somemore.volunteer.domain.QVolunteer;
import com.somemore.volunteer.domain.Volunteer;
import com.somemore.volunteer.repository.mapper.VolunteerOverviewForRankingByHours;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

import java.util.List;
import java.util.Optional;
import java.util.UUID;

Expand Down Expand Up @@ -41,6 +44,24 @@ public String findNicknameById(UUID id) {
.orElse(null);
}

@Override
public List<VolunteerOverviewForRankingByHours> findRankingByVolunteerHours() {
return queryFactory
.select(Projections.constructor(VolunteerOverviewForRankingByHours.class,
volunteer.id,
volunteer.nickname,
volunteer.imgUrl,
volunteer.introduce,
volunteer.tier,
volunteer.totalVolunteerHours
))
.from(volunteer)
.where(isNotDeleted())
.orderBy(volunteer.totalVolunteerHours.desc())
.limit(4)
.fetch();
}

@Override
public void deleteAllInBatch() {
volunteerJpaRepository.deleteAllInBatch();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.somemore.volunteer.repository.mapper;

import com.somemore.volunteer.domain.Tier;

import java.util.UUID;

public record VolunteerOverviewForRankingByHours(
UUID volunteerId,
String nickname,
String imgUrl,
String introduce,
Tier tier,
Integer totalVolunteerHours
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,17 @@
import com.somemore.volunteer.domain.Volunteer;
import com.somemore.volunteer.domain.VolunteerDetail;
import com.somemore.volunteer.dto.response.VolunteerProfileResponseDto;
import com.somemore.volunteer.dto.response.VolunteerRankingResponseDto;
import com.somemore.volunteer.repository.VolunteerDetailRepository;
import com.somemore.volunteer.repository.VolunteerRepository;
import com.somemore.volunteer.repository.mapper.VolunteerOverviewForRankingByHours;
import com.somemore.volunteer.usecase.VolunteerQueryUseCase;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.UUID;

import static com.somemore.global.exception.ExceptionMessage.NOT_EXISTS_VOLUNTEER;
Expand Down Expand Up @@ -72,10 +75,15 @@ public String getNicknameById(UUID id) {
return nickname;
}

@Override
public VolunteerRankingResponseDto getRankingByHours() {
List<VolunteerOverviewForRankingByHours> rankingByVolunteerHours = volunteerRepository.findRankingByVolunteerHours();
return VolunteerRankingResponseDto.from(rankingByVolunteerHours);
}

private Volunteer findVolunteer(UUID volunteerId) {
return volunteerRepository.findById(volunteerId)
.orElseThrow(() -> new BadRequestException(NOT_EXISTS_VOLUNTEER));

}

private VolunteerDetail findVolunteerDetail(UUID volunteerId) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.somemore.volunteer.usecase;

import com.somemore.volunteer.dto.response.VolunteerProfileResponseDto;
import com.somemore.volunteer.dto.response.VolunteerRankingResponseDto;

import java.util.UUID;

Expand All @@ -15,4 +16,6 @@ public interface VolunteerQueryUseCase {
UUID getVolunteerIdByOAuthId(String oAuthId);

String getNicknameById(UUID id);

VolunteerRankingResponseDto getRankingByHours();
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
import com.somemore.IntegrationTestSupport;
import com.somemore.auth.oauth.OAuthProvider;
import com.somemore.volunteer.domain.Volunteer;
import com.somemore.volunteer.repository.mapper.VolunteerOverviewForRankingByHours;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.Optional;
import java.util.UUID;

Expand Down Expand Up @@ -76,4 +78,57 @@ void findByOauthId() {
assertThat(foundVolunteer.get().getOauthId()).isEqualTo(oAuthId);
assertThat(foundVolunteer.get().getNickname()).isEqualTo(volunteer.getNickname());
}

@DisplayName("봉사 시간 기준 상위 4명을 조회한다.")
@Test
void findRankingByVolunteerHours_top4() {
// given
for (int i = 1; i <= 5; i++) {
createVolunteerAndUpdateVolunteerStats(i);
}

// when
List<VolunteerOverviewForRankingByHours> rankings = volunteerRepository.findRankingByVolunteerHours();

// then
assertThat(rankings).hasSize(4);
assertThat(rankings.get(0).totalVolunteerHours()).isGreaterThan(rankings.get(1).totalVolunteerHours());
}

@DisplayName("등록된 봉사자가 없는 경우 빈 리스트를 반환한다.")
@Test
void findRankingByVolunteerHours_noVolunteers() {
// given
volunteerRepository.deleteAllInBatch();

// when
List<VolunteerOverviewForRankingByHours> rankings = volunteerRepository.findRankingByVolunteerHours();

// then
assertThat(rankings).isEmpty();
}

@DisplayName("등록된 봉사자가 4명 이하인 경우 전체 봉사자를 반환한다.")
@Test
void findRankingByVolunteerHours_lessThan4Volunteers() {
// given
volunteerRepository.deleteAllInBatch();

for (int i = 1; i <= 3; i++) {
createVolunteerAndUpdateVolunteerStats(i);
}

// when
List<VolunteerOverviewForRankingByHours> rankings = volunteerRepository.findRankingByVolunteerHours();

// then
assertThat(rankings).hasSize(3);
assertThat(rankings.get(0).totalVolunteerHours()).isGreaterThan(rankings.get(1).totalVolunteerHours());
}

private void createVolunteerAndUpdateVolunteerStats(int i) {
Volunteer volunteer = Volunteer.createDefault(OAuthProvider.NAVER, "oauth-id-" + i);
volunteer.updateVolunteerStats(i * 10, i);
volunteerRepository.save(volunteer);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@
import com.somemore.volunteer.domain.VolunteerDetail;
import com.somemore.volunteer.dto.request.VolunteerRegisterRequestDto;
import com.somemore.volunteer.dto.response.VolunteerProfileResponseDto;
import com.somemore.volunteer.dto.response.VolunteerRankingResponseDto;
import com.somemore.volunteer.repository.VolunteerDetailRepository;
import com.somemore.volunteer.repository.VolunteerRepository;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.UUID;

import static com.somemore.global.exception.ExceptionMessage.NOT_EXISTS_VOLUNTEER;
Expand Down Expand Up @@ -152,6 +154,40 @@ void getVolunteerDetailedProfile() {
.hasMessage(UNAUTHORIZED_VOLUNTEER_DETAIL.getMessage());
}

@DisplayName("봉사 시간 기준 상위 4명의 랭킹을 조회한다.")
@Test
void getRankingByHours() {
// given
for (int i = 1; i <= 5; i++) {
Volunteer volunteer = Volunteer.createDefault(oAuthProvider, "oauth-id-" + i);
volunteer.updateVolunteerStats(i * 10, i);
volunteerRepository.save(volunteer);
}

// when
VolunteerRankingResponseDto response = volunteerQueryService.getRankingByHours();

// then
assertThat(response).isNotNull();
assertThat(response.rankings()).hasSize(4);

List<Integer> hours = response.rankings().stream()
.map(VolunteerRankingResponseDto.VolunteerOverview::totalVolunteerHours)
.toList();
assertThat(hours).isSortedAccordingTo((a, b) -> b - a);
}

@DisplayName("등록된 봉사자가 없는 경우 빈 랭킹 리스트를 반환한다.")
@Test
void getRankingByHours_noVolunteers() {
// when
VolunteerRankingResponseDto response = volunteerQueryService.getRankingByHours();

// then
assertThat(response).isNotNull();
assertThat(response.rankings()).isEmpty();
}

private static VolunteerDetail createVolunteerDetail(UUID volunteerId) {

VolunteerRegisterRequestDto volunteerRegisterRequestDto =
Expand Down
Loading