Skip to content

Commit

Permalink
완료된 리뷰가 많은 서포터 순대로 조회하는 기능 구현 (#712)
Browse files Browse the repository at this point in the history
* feat: 리뷰가 많은 사람 랭킹 구하는 기능 구현

* test: 랭킹 인수테스트와 restdocs 테스트 구현

* feat: 랭킹에 company 항목 추가

* refactor: 다른 랭킹도 추가할 수 있도록 Rankable 마커 인터페이스 구현
  • Loading branch information
cookienc authored Feb 17, 2024
1 parent 7c5c38a commit be6041a
Show file tree
Hide file tree
Showing 22 changed files with 547 additions and 6 deletions.
2 changes: 1 addition & 1 deletion backend/baton/secret
29 changes: 29 additions & 0 deletions backend/baton/src/docs/asciidoc/RankReadAPI.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
ifndef::snippets[]
:snippets: ../../../build/generated-snippets
endif::[]
:doctype: book
:icons: font
:source-highlighter: highlight.js
:toc: left
:toclevels: 3
:sectlinks:
:operation-http-request-title: Example Request
:operation-http-response-title: Example Response

==== *완료된 리뷰가 많은 서포터 랭킹 조회 API*

===== *Http Request*

include::{snippets}/../../build/generated-snippets/rank-read-api-test/read-most-review-supporter/http-request.adoc[]

===== *Http Request Query Parameters*

include::{snippets}/../../build/generated-snippets/rank-read-api-test/read-most-review-supporter/query-parameters.adoc[]

===== *Http Response*

include::{snippets}/../../build/generated-snippets/rank-read-api-test/read-most-review-supporter/http-response.adoc[]

===== *Http Response Fields*

include::{snippets}/../../build/generated-snippets/rank-read-api-test/read-most-review-supporter/response-fields.adoc[]
5 changes: 5 additions & 0 deletions backend/baton/src/docs/asciidoc/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,8 @@ include::NotificationUpdateApi.adoc[]
=== *알림 삭제*

include::NotificationDeleteApi.adoc[]

== *[ 랭킹 ]*

=== *랭킹 조회*
include::RankReadAPI.adoc[]
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package touch.baton.domain.member.query.controller;

import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import touch.baton.domain.member.query.controller.response.RankResponses;
import touch.baton.domain.member.query.service.RankQueryService;

@RequiredArgsConstructor
@RequestMapping("/api/v1/rank")
@RestController
public class RankQueryController {

private final RankQueryService rankQueryService;

@GetMapping("/supporter")
public ResponseEntity<RankResponses> readMostReviewSupporter(@RequestParam(defaultValue = "5") final int limit) {
return ResponseEntity.ok(rankQueryService.readMostReviewSupporter(limit));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package touch.baton.domain.member.query.controller.response;

import touch.baton.domain.member.command.Supporter;

import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

public record RankResponses<T extends Rankable>(List<T> data) {

public static RankResponses<SupporterRank> from(final List<Supporter> supporters) {
final AtomicInteger rank = new AtomicInteger(1);
final List<SupporterRank> responses = supporters.stream()
.map(supporter -> new SupporterRank(
rank.getAndIncrement(),
supporter.getMember().getMemberName().getValue(),
supporter.getId(),
supporter.getReviewCount().getValue(),
supporter.getMember().getImageUrl().getValue(),
supporter.getMember().getGithubUrl().getValue(),
supporter.getMember().getCompany().getValue(),
supporter.getSupporterTechnicalTags().getSupporterTechnicalTags().stream()
.map(supporterTechnicalTag -> supporterTechnicalTag.getTechnicalTag().getTagName().getValue())
.toList()))
.toList();

return new RankResponses<>(responses);
}

public record SupporterRank(
int rank,
String name,
long supporterId,
int reviewedCount,
String imageUrl,
String githubUrl,
String company,
List<String> technicalTags
) implements Rankable {

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package touch.baton.domain.member.query.controller.response;

public interface Rankable {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package touch.baton.domain.member.query.repository;

import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
import touch.baton.domain.member.command.Supporter;

import java.util.List;

import static touch.baton.domain.member.command.QMember.member;
import static touch.baton.domain.member.command.QSupporter.supporter;

@RequiredArgsConstructor
@Repository
public class RankQuerydslRepository {

private final JPAQueryFactory jpaQueryFactory;

public List<Supporter> findMostReviewSupporterByCount(final int count) {
return jpaQueryFactory.selectFrom(supporter)
.join(supporter.member, member).fetchJoin()
.orderBy(supporter.reviewCount.value.desc())
.limit(count)
.fetch();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package touch.baton.domain.member.query.service;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import touch.baton.domain.member.query.controller.response.RankResponses;
import touch.baton.domain.member.query.repository.RankQuerydslRepository;

@RequiredArgsConstructor
@Transactional(readOnly = true)
@Service
public class RankQueryService {

private final RankQuerydslRepository rankQueryDslRepository;

public RankResponses<RankResponses.SupporterRank> readMostReviewSupporter(final int maxCount) {
return RankResponses.from(rankQueryDslRepository.findMostReviewSupporterByCount(maxCount));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package touch.baton.assure.member.query;

import org.junit.jupiter.api.Test;
import touch.baton.assure.member.support.query.RankQueryAssuredSupport;
import touch.baton.config.AssuredTestConfig;
import touch.baton.config.infra.auth.oauth.authcode.FakeAuthCodes;
import touch.baton.domain.member.command.Supporter;
import touch.baton.domain.member.command.vo.SocialId;

import java.util.List;

import static touch.baton.assure.member.support.query.RankQueryAssuredSupport.RankQueryResponseBuilder.서포터_리뷰_랭킹_응답;

@SuppressWarnings("NonAsciiCharacters")
class RankQueryRestAssuredTest extends AssuredTestConfig {

@Test
void 코드_리뷰를_가장_많이_한_5명을_리뷰_숫자를_기준으로_내림차순으로_반환한다() {
// given
final String 에단_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.ethanAuthCode());
final SocialId 에단_소셜_아이디 = jwtTestManager.parseToSocialId(에단_액세스_토큰);
final Supporter 서포터_에단 = supporterRepository.getBySocialId(에단_소셜_아이디);
rankQueryRepository.updateReviewCount(서포터_에단.getId(), 10);

final String 디투_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.ditooAuthCode());
final SocialId 디투_소셜_아이디 = jwtTestManager.parseToSocialId(디투_액세스_토큰);
final Supporter 서포터_디투 = supporterRepository.getBySocialId(디투_소셜_아이디);
rankQueryRepository.updateReviewCount(서포터_디투.getId(), 5);

final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.hyenaAuthCode());
final SocialId 헤나_소셜_아이디 = jwtTestManager.parseToSocialId(헤나_액세스_토큰);
final Supporter 서포터_헤나 = supporterRepository.getBySocialId(헤나_소셜_아이디);
rankQueryRepository.updateReviewCount(서포터_헤나.getId(), 20);

// when, then
RankQueryAssuredSupport.클라이언트_요청()
.서포터_리뷰_랭킹을_조회한다(2)

.서버_응답()
.서포터_리뷰_랭킹_조회_성공을_검증한다(List.of(
서포터_리뷰_랭킹_응답(1, 서포터_헤나, 20),
서포터_리뷰_랭킹_응답(2, 서포터_에단, 10)
));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package touch.baton.assure.member.support.query;

import io.restassured.common.mapper.TypeRef;
import io.restassured.response.ExtractableResponse;
import io.restassured.response.Response;
import touch.baton.assure.common.AssuredSupport;
import touch.baton.assure.common.QueryParams;
import touch.baton.domain.member.command.Member;
import touch.baton.domain.member.command.Supporter;
import touch.baton.domain.member.query.controller.response.RankResponses;

import java.util.Collections;
import java.util.List;
import java.util.Map;

import static org.assertj.core.api.SoftAssertions.assertSoftly;

@SuppressWarnings("NonAsciiCharacters")
public class RankQueryAssuredSupport {

private RankQueryAssuredSupport() {
}

public static RankQueryBuilder 클라이언트_요청() {
return new RankQueryBuilder();
}

public static class RankQueryBuilder {

private ExtractableResponse<Response> response;

public RankQueryBuilder 서포터_리뷰_랭킹을_조회한다(final int limit) {
response = AssuredSupport.get("/api/v1/rank/supporter", new QueryParams(Map.of("limit", limit)));
return this;
}

public RankQueryResponseBuilder 서버_응답() {
return new RankQueryResponseBuilder(response);
}
}

public static class RankQueryResponseBuilder {

private final ExtractableResponse<Response> response;

public RankQueryResponseBuilder(final ExtractableResponse<Response> response) {
this.response = response;
}

public void 서포터_리뷰_랭킹_조회_성공을_검증한다(final List<RankResponses.SupporterRank> 랭킹_응답) {
final RankResponses<RankResponses.SupporterRank> actual = this.response.as(new TypeRef<>(){});

assertSoftly(softly -> {
softly.assertThat(actual.data()).hasSize(랭킹_응답.size());
softly.assertThat(actual.data()).isEqualTo(랭킹_응답);
});
}

public static RankResponses.SupporterRank 서포터_리뷰_랭킹_응답(final int 순위, final Supporter 서포터, final int 리뷰수) {
final Member member = 서포터.getMember();
return new RankResponses.SupporterRank(순위,
member.getMemberName().getValue(),
서포터.getId(),
리뷰수,
member.getImageUrl().getValue(),
member.getGithubUrl().getValue(),
member.getCompany().getValue(),
Collections.emptyList());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package touch.baton.assure.repository;

import com.querydsl.jpa.impl.JPAQueryFactory;
import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import touch.baton.domain.member.query.repository.RankQuerydslRepository;

import static touch.baton.domain.member.command.QSupporter.supporter;

@Profile("test")
@Primary
@Repository
public class TestRankQueryRepository extends RankQuerydslRepository {

private JPAQueryFactory jpaQueryFactory;

public TestRankQueryRepository(final JPAQueryFactory jpaQueryFactory) {
super(jpaQueryFactory);
this.jpaQueryFactory = jpaQueryFactory;
}

@Transactional
public void updateReviewCount(final long supporterId, final int reviewCount) {
jpaQueryFactory.update(supporter)
.set(supporter.reviewCount.value, supporter.reviewCount.value.add(reviewCount))
.where(supporter.id.eq(supporterId))
.execute();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@ default Supporter getBySocialId(final SocialId socialId) {
}

@Query("""
select s, m
select s
from Supporter s
join fetch Member m on m.id = s.member.id
join fetch s.member m
join fetch s.supporterTechnicalTags stt
where m.socialId = :socialId
""")
Optional<Supporter> joinMemberBySocialId(@Param("socialId") final SocialId socialId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import touch.baton.assure.common.OauthLoginTestManager;
import touch.baton.assure.repository.TestMemberQueryRepository;
import touch.baton.assure.repository.TestNotificationCommandRepository;
import touch.baton.assure.repository.TestRankQueryRepository;
import touch.baton.assure.repository.TestRefreshTokenRepository;
import touch.baton.assure.repository.TestRunnerPostQueryRepository;
import touch.baton.assure.repository.TestRunnerQueryRepository;
Expand Down Expand Up @@ -61,6 +62,9 @@ public abstract class AssuredTestConfig {
@Autowired
protected JwtTestManager jwtTestManager;

@Autowired
protected TestRankQueryRepository rankQueryRepository;

protected OauthLoginTestManager oauthLoginTestManager = new OauthLoginTestManager();

@BeforeEach
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import jakarta.persistence.PersistenceContext;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import touch.baton.domain.member.query.repository.RankQuerydslRepository;
import touch.baton.domain.notification.query.repository.NotificationQuerydslRepository;
import touch.baton.domain.runnerpost.query.repository.RunnerPostPageRepository;
import touch.baton.domain.tag.query.repository.TagQuerydslRepository;
Expand Down Expand Up @@ -34,4 +35,9 @@ public NotificationQuerydslRepository notificationQuerydslRepository() {
public TagQuerydslRepository tagQuerydslRepository() {
return new TagQuerydslRepository(jpaQueryFactory());
}

@Bean
public RankQuerydslRepository queryDslRepository() {
return new RankQuerydslRepository(jpaQueryFactory());
}
}
Loading

0 comments on commit be6041a

Please sign in to comment.