Skip to content
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

feat: ranking system 구현 #189

Merged
merged 12 commits into from
Nov 30, 2023
Merged
Show file tree
Hide file tree
Changes from 10 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
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);
}
Comment on lines +137 to +145
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

C: RankingService에 있는건 어떤가요?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

의존성 순환 생겨서 memberSearchService로 옮겨야하는데 좀 바뀌는게 많아서 불편해질 때 변경하겠습니다!


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.TopRankingResponses;
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 TopRankingResponses topRankingResponses(TopRankingInfo myRanking,
List<TopRankingInfo> topRankings) {
return TopRankingResponses.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.TopRankingResponses;
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 TopRankingResponses 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/TopRankingResponses.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 TopRankingResponses(
parksey marked this conversation as resolved.
Show resolved Hide resolved
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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기서 직렬화하는 이유가 궁금합니다!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현재 직렬화를 하지 않으면 redis로 들어가는 데이터가 string으로 되어있는데,
이때 "{}" 이 값을 처리하지 못하여 에러가 발생했습니다.
그래서 객체로 직렬화할 수 있도록 하고 다시 기존 String 직렬화로 변경하였습니다

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