Skip to content

Commit

Permalink
feat: ranking system 구현 (#189)
Browse files Browse the repository at this point in the history
* feat: 회원의 랭킹 redis에 추가 및 삭제, 업데이트 기능 추가

* test: 회원 정보 변경 및 삭제 추가에 따른 랭킹 참여, 제외 테스트 코드 추가

* feat: 랭킹시스템 API 추가 및 랭킹 조회 기능 추가

* feat: 랭킹 조회 테스트 코드 추가 및 랭킹 업데이트 로직 각 업데이트 -> 스케쥴러

* style: checkstyle 에러 fix

* refactor: 응답 객체명 변경 TopRankingInfoResponse -> TopRankingInfo

* fix: 랭킹 업데이트 시간 15분 매초마다 동작하는 방식 -> 15분에 한 번만 실행되도록 변경

* refactor: 랭킹 응답 반환 객체 변수면 s 제거

Co-authored-by: Kim Heebin <[email protected]>

* refactor: ToprankingResponses 응답 객체 반환명 TopRankingResponse로 변경

---------

Co-authored-by: Kim Heebin <[email protected]>
  • Loading branch information
parksey and kmebin authored Nov 30, 2023
1 parent 5f07beb commit 70de15b
Show file tree
Hide file tree
Showing 28 changed files with 710 additions and 38 deletions.
17 changes: 17 additions & 0 deletions src/main/java/com/moabam/api/application/member/MemberMapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
import com.moabam.api.dto.member.MemberInfo;
import com.moabam.api.dto.member.MemberInfoResponse;
import com.moabam.api.dto.member.MemberInfoSearchResponse;
import com.moabam.api.dto.ranking.RankingInfo;
import com.moabam.api.dto.ranking.UpdateRanking;

import lombok.AccessLevel;
import lombok.NoArgsConstructor;
Expand All @@ -33,6 +35,13 @@ public static Member toMember(Long socialId) {
.build();
}

public static UpdateRanking toUpdateRanking(Member member) {
return UpdateRanking.builder()
.rankingInfo(toRankingInfo(member))
.score(member.getTotalCertifyCount())
.build();
}

public static MemberInfoSearchResponse toMemberInfoSearchResponse(List<MemberInfo> memberInfos) {
MemberInfo infos = memberInfos.get(0);
List<BadgeType> badgeTypes = memberInfos.stream()
Expand Down Expand Up @@ -79,6 +88,14 @@ public static Inventory toInventory(Long memberId, Item item) {
.build();
}

public static RankingInfo toRankingInfo(Member member) {
return RankingInfo.builder()
.memberId(member.getId())
.nickname(member.getNickname())
.image(member.getProfileImage())
.build();
}

private static List<BadgeResponse> badgedNames(Set<BadgeType> badgeTypes) {
return BadgeType.memberBadgeMap(badgeTypes);
}
Expand Down
32 changes: 30 additions & 2 deletions src/main/java/com/moabam/api/application/member/MemberService.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@
import java.util.Objects;
import java.util.Optional;

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.moabam.api.application.auth.mapper.AuthMapper;
import com.moabam.api.application.ranking.RankingService;
import com.moabam.api.domain.item.Inventory;
import com.moabam.api.domain.item.Item;
import com.moabam.api.domain.item.repository.InventoryRepository;
Expand All @@ -26,6 +28,8 @@
import com.moabam.api.dto.member.MemberInfoResponse;
import com.moabam.api.dto.member.MemberInfoSearchResponse;
import com.moabam.api.dto.member.ModifyMemberRequest;
import com.moabam.api.dto.ranking.RankingInfo;
import com.moabam.api.dto.ranking.UpdateRanking;
import com.moabam.api.infrastructure.fcm.FcmService;
import com.moabam.global.auth.model.AuthMember;
import com.moabam.global.common.util.BaseDataCode;
Expand All @@ -42,6 +46,7 @@
@RequiredArgsConstructor
public class MemberService {

private final RankingService rankingService;
private final FcmService fcmService;
private final MemberRepository memberRepository;
private final InventoryRepository inventoryRepository;
Expand Down Expand Up @@ -85,6 +90,7 @@ public void delete(Member member) {
member.delete(clockHolder.times());
memberRepository.flush();
memberRepository.delete(member);
rankingService.removeRanking(MemberMapper.toRankingInfo(member));
fcmService.deleteTokenByMemberId(member.getId());
}

Expand All @@ -96,27 +102,48 @@ public MemberInfoResponse searchInfo(AuthMember authMember, Long memberId) {
searchId = memberId;
}
MemberInfoSearchResponse memberInfoSearchResponse = findMemberInfo(searchId, isMe);

return MemberMapper.toMemberInfoResponse(memberInfoSearchResponse);
}

@Transactional
public void modifyInfo(AuthMember authMember, ModifyMemberRequest modifyMemberRequest, String newProfileUri) {
validateNickname(modifyMemberRequest.nickname());

Member member = memberSearchRepository.findMember(authMember.id())
.orElseThrow(() -> new NotFoundException(MEMBER_NOT_FOUND));

RankingInfo beforeInfo = MemberMapper.toRankingInfo(member);
member.changeNickName(modifyMemberRequest.nickname());

boolean nickNameChanged = member.changeNickName(modifyMemberRequest.nickname());
member.changeIntro(modifyMemberRequest.intro());
member.changeProfileUri(newProfileUri);

memberRepository.save(member);

RankingInfo afterInfo = MemberMapper.toRankingInfo(member);
rankingService.changeInfos(beforeInfo, afterInfo);

if (nickNameChanged) {
changeNickname(authMember.id(), modifyMemberRequest.nickname());
}
}

public UpdateRanking getRankingInfo(AuthMember authMember) {
Member member = findMember(authMember.id());

return MemberMapper.toUpdateRanking(member);
}

@Scheduled(cron = "0 15 * * * *")
public void updateAllRanking() {
List<Member> members = memberSearchRepository.findAllMembers();
List<UpdateRanking> updateRankings = members.stream()
.map(MemberMapper::toUpdateRanking)
.toList();

rankingService.updateScores(updateRankings);
}

private void changeNickname(Long memberId, String changedName) {
List<Participant> participants = participantSearchRepository.findAllRoomMangerByMemberId(memberId);

Expand All @@ -138,6 +165,7 @@ private Member signUp(Long socialId) {
Member member = MemberMapper.toMember(socialId);
Member savedMember = memberRepository.save(member);
saveMyEgg(savedMember);
rankingService.addRanking(MemberMapper.toRankingInfo(member), member.getTotalCertifyCount());

return savedMember;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.moabam.api.application.ranking;

import java.util.List;

import com.moabam.api.dto.ranking.RankingInfo;
import com.moabam.api.dto.ranking.TopRankingInfo;
import com.moabam.api.dto.ranking.TopRankingResponse;
import com.moabam.api.dto.ranking.UpdateRanking;

import lombok.AccessLevel;
import lombok.NoArgsConstructor;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class RankingMapper {

public static TopRankingInfo topRankingResponse(int rank, long score, RankingInfo rankInfo) {
return TopRankingInfo.builder()
.rank(rank)
.score(score)
.nickname(rankInfo.nickname())
.image(rankInfo.image())
.memberId(rankInfo.memberId())
.build();
}

public static TopRankingInfo topRankingResponse(int rank, UpdateRanking updateRanking) {
return TopRankingInfo.builder()
.rank(rank)
.score(updateRanking.score())
.nickname(updateRanking.rankingInfo().nickname())
.image(updateRanking.rankingInfo().image())
.memberId(updateRanking.rankingInfo().memberId())
.build();
}

public static TopRankingResponse topRankingResponses(TopRankingInfo myRanking,
List<TopRankingInfo> topRankings) {
return TopRankingResponse.builder()
.topRankings(topRankings)
.myRanking(myRanking)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package com.moabam.api.application.ranking;

import static java.util.Objects.*;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Service;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.moabam.api.dto.ranking.RankingInfo;
import com.moabam.api.dto.ranking.TopRankingInfo;
import com.moabam.api.dto.ranking.TopRankingResponse;
import com.moabam.api.dto.ranking.UpdateRanking;
import com.moabam.api.infrastructure.redis.ZSetRedisRepository;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class RankingService {

private static final String RANKING = "Ranking";
private static final int START_INDEX = 0;
private static final int LIMIT_INDEX = 9;

private final ObjectMapper objectMapper;
private final ZSetRedisRepository zSetRedisRepository;

public void addRanking(RankingInfo rankingInfo, Long totalCertifyCount) {
zSetRedisRepository.add(RANKING, rankingInfo, totalCertifyCount);
}

public void updateScores(List<UpdateRanking> updateRankings) {
updateRankings.forEach(updateRanking ->
zSetRedisRepository.add(RANKING, updateRanking.rankingInfo(), updateRanking.score()));
}

public void changeInfos(RankingInfo before, RankingInfo after) {
zSetRedisRepository.changeMember(RANKING, before, after);
}

public void removeRanking(RankingInfo rankingInfo) {
zSetRedisRepository.delete(RANKING, rankingInfo);
}

public TopRankingResponse getMemberRanking(UpdateRanking myRankingInfo) {
List<TopRankingInfo> topRankings = getTopRankings();
Long myRanking = zSetRedisRepository.reverseRank(RANKING, myRankingInfo.rankingInfo());
TopRankingInfo myRankingInfoResponse =
RankingMapper.topRankingResponse(myRanking.intValue(), myRankingInfo);

return RankingMapper.topRankingResponses(myRankingInfoResponse, topRankings);
}

private List<TopRankingInfo> getTopRankings() {
Set<ZSetOperations.TypedTuple<Object>> topRankings =
zSetRedisRepository.rangeJson(RANKING, START_INDEX, LIMIT_INDEX);

Set<Long> scoreSet = new HashSet<>();
List<TopRankingInfo> topRankingInfo = new ArrayList<>();

for (ZSetOperations.TypedTuple<Object> topRanking : topRankings) {
long score = requireNonNull(topRanking.getScore()).longValue();
scoreSet.add(score);

RankingInfo rankingInfo = objectMapper.convertValue(topRanking.getValue(), RankingInfo.class);
topRankingInfo.add(RankingMapper.topRankingResponse(scoreSet.size(), score, rankingInfo));
}

return topRankingInfo;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,15 @@ public Optional<Member> findMember(Long memberId) {
return findMember(memberId, true);
}

public List<Member> findAllMembers() {
return jpaQueryFactory
.selectFrom(member)
.where(
member.deletedAt.isNotNull()
)
.fetch();
}

public Optional<Member> findMember(Long memberId, boolean isNotDeleted) {
return Optional.ofNullable(jpaQueryFactory
.selectFrom(member)
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/com/moabam/api/dto/ranking/RankingInfo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.moabam.api.dto.ranking;

import lombok.Builder;

@Builder
public record RankingInfo(
Long memberId,
String nickname,
String image
) {

}
14 changes: 14 additions & 0 deletions src/main/java/com/moabam/api/dto/ranking/TopRankingInfo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.moabam.api.dto.ranking;

import lombok.Builder;

@Builder
public record TopRankingInfo(
int rank,
Long memberId,
Long score,
String nickname,
String image
) {

}
13 changes: 13 additions & 0 deletions src/main/java/com/moabam/api/dto/ranking/TopRankingResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.moabam.api.dto.ranking;

import java.util.List;

import lombok.Builder;

@Builder
public record TopRankingResponse(
List<TopRankingInfo> topRankings,
TopRankingInfo myRanking
) {

}
11 changes: 11 additions & 0 deletions src/main/java/com/moabam/api/dto/ranking/UpdateRanking.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.moabam.api.dto.ranking;

import lombok.Builder;

@Builder
public record UpdateRanking(
RankingInfo rankingInfo,
Long score
) {

}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
import java.util.Set;

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.data.redis.core.ZSetOperations.TypedTuple;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.stereotype.Repository;

import lombok.RequiredArgsConstructor;
Expand Down Expand Up @@ -35,4 +38,41 @@ public Long rank(String key, Object value) {
.opsForZSet()
.rank(key, value);
}

public void add(String key, Object value, double score) {
redisTemplate
.opsForZSet()
.add(requireNonNull(key), requireNonNull(value), score);
}

public void changeMember(String key, Object before, Object after) {
Double score = redisTemplate.opsForZSet().score(key, before);

if (score == null) {
return;
}

delete(key, before);
add(key, after, score);
}

public void delete(String key, Object value) {
redisTemplate.opsForZSet().remove(key, value);
}

public Set<TypedTuple<Object>> rangeJson(String key, int startIndex, int limitIndex) {
setSerialize(Object.class);
Set<ZSetOperations.TypedTuple<Object>> rankings = redisTemplate.opsForZSet()
.reverseRangeWithScores(key, startIndex, limitIndex);
setSerialize(String.class);
return rankings;
}

public Long reverseRank(String key, Object myRankingInfo) {
return redisTemplate.opsForZSet().reverseRank(key, myRankingInfo);
}

private void setSerialize(Class classes) {
redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(classes));
}
}
Loading

0 comments on commit 70de15b

Please sign in to comment.