diff --git a/src/main/java/com/moabam/api/application/member/MemberMapper.java b/src/main/java/com/moabam/api/application/member/MemberMapper.java index 148a0b8d..67fd8a92 100644 --- a/src/main/java/com/moabam/api/application/member/MemberMapper.java +++ b/src/main/java/com/moabam/api/application/member/MemberMapper.java @@ -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; @@ -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 memberInfos) { MemberInfo infos = memberInfos.get(0); List badgeTypes = memberInfos.stream() @@ -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 badgedNames(Set badgeTypes) { return BadgeType.memberBadgeMap(badgeTypes); } diff --git a/src/main/java/com/moabam/api/application/member/MemberService.java b/src/main/java/com/moabam/api/application/member/MemberService.java index 8ed67cc5..358cf2df 100644 --- a/src/main/java/com/moabam/api/application/member/MemberService.java +++ b/src/main/java/com/moabam/api/application/member/MemberService.java @@ -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; @@ -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; @@ -42,6 +46,7 @@ @RequiredArgsConstructor public class MemberService { + private final RankingService rankingService; private final FcmService fcmService; private final MemberRepository memberRepository; private final InventoryRepository inventoryRepository; @@ -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()); } @@ -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 members = memberSearchRepository.findAllMembers(); + List updateRankings = members.stream() + .map(MemberMapper::toUpdateRanking) + .toList(); + + rankingService.updateScores(updateRankings); + } + private void changeNickname(Long memberId, String changedName) { List participants = participantSearchRepository.findAllRoomMangerByMemberId(memberId); @@ -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; } diff --git a/src/main/java/com/moabam/api/application/ranking/RankingMapper.java b/src/main/java/com/moabam/api/application/ranking/RankingMapper.java new file mode 100644 index 00000000..06ad3389 --- /dev/null +++ b/src/main/java/com/moabam/api/application/ranking/RankingMapper.java @@ -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 topRankings) { + return TopRankingResponse.builder() + .topRankings(topRankings) + .myRanking(myRanking) + .build(); + } +} diff --git a/src/main/java/com/moabam/api/application/ranking/RankingService.java b/src/main/java/com/moabam/api/application/ranking/RankingService.java new file mode 100644 index 00000000..aa48370d --- /dev/null +++ b/src/main/java/com/moabam/api/application/ranking/RankingService.java @@ -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 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 topRankings = getTopRankings(); + Long myRanking = zSetRedisRepository.reverseRank(RANKING, myRankingInfo.rankingInfo()); + TopRankingInfo myRankingInfoResponse = + RankingMapper.topRankingResponse(myRanking.intValue(), myRankingInfo); + + return RankingMapper.topRankingResponses(myRankingInfoResponse, topRankings); + } + + private List getTopRankings() { + Set> topRankings = + zSetRedisRepository.rangeJson(RANKING, START_INDEX, LIMIT_INDEX); + + Set scoreSet = new HashSet<>(); + List topRankingInfo = new ArrayList<>(); + + for (ZSetOperations.TypedTuple 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; + } +} diff --git a/src/main/java/com/moabam/api/domain/member/repository/MemberSearchRepository.java b/src/main/java/com/moabam/api/domain/member/repository/MemberSearchRepository.java index b853b871..82a43a01 100644 --- a/src/main/java/com/moabam/api/domain/member/repository/MemberSearchRepository.java +++ b/src/main/java/com/moabam/api/domain/member/repository/MemberSearchRepository.java @@ -29,6 +29,15 @@ public Optional findMember(Long memberId) { return findMember(memberId, true); } + public List findAllMembers() { + return jpaQueryFactory + .selectFrom(member) + .where( + member.deletedAt.isNotNull() + ) + .fetch(); + } + public Optional findMember(Long memberId, boolean isNotDeleted) { return Optional.ofNullable(jpaQueryFactory .selectFrom(member) diff --git a/src/main/java/com/moabam/api/dto/ranking/RankingInfo.java b/src/main/java/com/moabam/api/dto/ranking/RankingInfo.java new file mode 100644 index 00000000..46645389 --- /dev/null +++ b/src/main/java/com/moabam/api/dto/ranking/RankingInfo.java @@ -0,0 +1,12 @@ +package com.moabam.api.dto.ranking; + +import lombok.Builder; + +@Builder +public record RankingInfo( + Long memberId, + String nickname, + String image +) { + +} diff --git a/src/main/java/com/moabam/api/dto/ranking/TopRankingInfo.java b/src/main/java/com/moabam/api/dto/ranking/TopRankingInfo.java new file mode 100644 index 00000000..bcd56ff2 --- /dev/null +++ b/src/main/java/com/moabam/api/dto/ranking/TopRankingInfo.java @@ -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 +) { + +} diff --git a/src/main/java/com/moabam/api/dto/ranking/TopRankingResponse.java b/src/main/java/com/moabam/api/dto/ranking/TopRankingResponse.java new file mode 100644 index 00000000..38663842 --- /dev/null +++ b/src/main/java/com/moabam/api/dto/ranking/TopRankingResponse.java @@ -0,0 +1,13 @@ +package com.moabam.api.dto.ranking; + +import java.util.List; + +import lombok.Builder; + +@Builder +public record TopRankingResponse( + List topRankings, + TopRankingInfo myRanking +) { + +} diff --git a/src/main/java/com/moabam/api/dto/ranking/UpdateRanking.java b/src/main/java/com/moabam/api/dto/ranking/UpdateRanking.java new file mode 100644 index 00000000..6b5218ee --- /dev/null +++ b/src/main/java/com/moabam/api/dto/ranking/UpdateRanking.java @@ -0,0 +1,11 @@ +package com.moabam.api.dto.ranking; + +import lombok.Builder; + +@Builder +public record UpdateRanking( + RankingInfo rankingInfo, + Long score +) { + +} diff --git a/src/main/java/com/moabam/api/infrastructure/redis/ZSetRedisRepository.java b/src/main/java/com/moabam/api/infrastructure/redis/ZSetRedisRepository.java index e116d764..d68e43b7 100644 --- a/src/main/java/com/moabam/api/infrastructure/redis/ZSetRedisRepository.java +++ b/src/main/java/com/moabam/api/infrastructure/redis/ZSetRedisRepository.java @@ -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; @@ -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> rangeJson(String key, int startIndex, int limitIndex) { + setSerialize(Object.class); + Set> 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)); + } } diff --git a/src/main/java/com/moabam/api/presentation/RankingController.java b/src/main/java/com/moabam/api/presentation/RankingController.java new file mode 100644 index 00000000..d218ca5d --- /dev/null +++ b/src/main/java/com/moabam/api/presentation/RankingController.java @@ -0,0 +1,33 @@ +package com.moabam.api.presentation; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +import com.moabam.api.application.member.MemberService; +import com.moabam.api.application.ranking.RankingService; +import com.moabam.api.dto.ranking.TopRankingResponse; +import com.moabam.api.dto.ranking.UpdateRanking; +import com.moabam.global.auth.annotation.Auth; +import com.moabam.global.auth.model.AuthMember; + +import lombok.RequiredArgsConstructor; + +@RequestMapping("/rankings") +@RestController +@RequiredArgsConstructor +public class RankingController { + + private final RankingService rankingService; + private final MemberService memberService; + + @GetMapping + @ResponseStatus(HttpStatus.OK) + public TopRankingResponse getRanking(@Auth AuthMember authMember) { + UpdateRanking rankingInfo = memberService.getRankingInfo(authMember); + + return rankingService.getMemberRanking(rankingInfo); + } +} diff --git a/src/main/java/com/moabam/global/config/EmbeddedRedisConfig.java b/src/main/java/com/moabam/global/config/EmbeddedRedisConfig.java index 32ad60ec..a823fd26 100644 --- a/src/main/java/com/moabam/global/config/EmbeddedRedisConfig.java +++ b/src/main/java/com/moabam/global/config/EmbeddedRedisConfig.java @@ -17,6 +17,8 @@ import org.springframework.data.redis.serializer.StringRedisSerializer; import org.springframework.util.StringUtils; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.moabam.global.error.exception.MoabamException; import com.moabam.global.error.model.ErrorMessage; @@ -62,6 +64,14 @@ public RedisTemplate redisTemplate(RedisConnectionFactory redisC return redisTemplate; } + @Bean + public ObjectMapper objectMapper() { + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.registerModules(new JavaTimeModule()); + + return objectMapper; + } + public void startRedis() { Os os = Os.createOs(); availablePort = findPort(os); diff --git a/src/main/java/com/moabam/global/config/RedisConfig.java b/src/main/java/com/moabam/global/config/RedisConfig.java index 9d017cce..cc412271 100644 --- a/src/main/java/com/moabam/global/config/RedisConfig.java +++ b/src/main/java/com/moabam/global/config/RedisConfig.java @@ -10,6 +10,9 @@ import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; + @Configuration @Profile("!test") public class RedisConfig { @@ -35,4 +38,12 @@ public RedisTemplate redisTemplate(RedisConnectionFactory redisC return redisTemplate; } + + @Bean + public ObjectMapper objectMapper() { + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.registerModules(new JavaTimeModule()); + + return objectMapper; + } } diff --git a/src/test/java/com/moabam/api/application/member/MemberServiceTest.java b/src/test/java/com/moabam/api/application/member/MemberServiceTest.java index bdf62154..5bb68977 100644 --- a/src/test/java/com/moabam/api/application/member/MemberServiceTest.java +++ b/src/test/java/com/moabam/api/application/member/MemberServiceTest.java @@ -16,6 +16,7 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +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; @@ -69,6 +70,9 @@ class MemberServiceTest { @Mock InventoryRepository inventoryRepository; + @Mock + RankingService rankingService; + @Mock FcmService fcmService; @@ -229,4 +233,17 @@ void modify_success_test(@WithMember AuthMember authMember) { () -> assertThat(member.getProfileImage()).isEqualTo("/main") ); } + + @DisplayName("모든 랭킹 업데이트") + @Test + void update_all_ranking() { + // given + Member member1 = MemberFixture.member("1"); + Member member2 = MemberFixture.member("2"); + given(memberSearchRepository.findAllMembers()) + .willReturn(List.of(member1, member2)); + + // when + Then + assertThatNoException().isThrownBy(() -> memberService.updateAllRanking()); + } } diff --git a/src/test/java/com/moabam/api/application/ranking/RankingServiceTest.java b/src/test/java/com/moabam/api/application/ranking/RankingServiceTest.java new file mode 100644 index 00000000..d246c5df --- /dev/null +++ b/src/test/java/com/moabam/api/application/ranking/RankingServiceTest.java @@ -0,0 +1,228 @@ +package com.moabam.api.application.ranking; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; +import java.util.Set; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ZSetOperations; +import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; + +import com.moabam.api.application.member.MemberMapper; +import com.moabam.api.domain.member.Member; +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 com.moabam.global.config.EmbeddedRedisConfig; +import com.moabam.support.fixture.BugFixture; +import com.moabam.support.fixture.MemberFixture; + +@SpringBootTest(classes = {EmbeddedRedisConfig.class, RankingService.class, ZSetRedisRepository.class}) +public class RankingServiceTest { + + @Autowired + ZSetRedisRepository zSetRedisRepository; + + @Autowired + RedisTemplate redisTemplate; + + @Autowired + RankingService rankingService; + + @DisplayName("redis에 추가") + @Nested + class Add { + + @DisplayName("성공") + @Test + void add_success() { + // given + Long totalCertifyCount = 0L; + RankingInfo rankingInfo = RankingInfo.builder() + .image("https://image.moabam.com/test") + .memberId(1L) + .nickname("nickname") + .build(); + + // when + rankingService.addRanking(rankingInfo, totalCertifyCount); + + // then + Double resultDouble = redisTemplate.opsForZSet().score("Ranking", rankingInfo); + + assertAll(() -> assertThat(resultDouble).isNotNull(), + () -> assertThat(resultDouble).isEqualTo(Double.valueOf(totalCertifyCount))); + } + } + + @DisplayName("스코어 업데이트") + @Nested + class Update { + + @DisplayName("성공") + @Test + void update_success() { + // given + Member member = MemberFixture.member("1"); + member.increaseTotalCertifyCount(); + member.increaseTotalCertifyCount(); + + Member member1 = MemberFixture.member("2"); + member1.increaseTotalCertifyCount(); + member1.increaseTotalCertifyCount(); + + List members = List.of(member, member1); + List updateRankings = members.stream().map(MemberMapper::toUpdateRanking).toList(); + + // when + rankingService.updateScores(updateRankings); + + Double resultDouble = redisTemplate.opsForZSet().score("Ranking", updateRankings.get(0).rankingInfo()); + + // then + assertAll(() -> assertThat(resultDouble).isNotNull(), + () -> assertThat(resultDouble).isEqualTo(Double.valueOf(member.getTotalCertifyCount()))); + } + } + + @DisplayName("사용자 정보 변경") + @Nested + class Change { + + @DisplayName("성공") + @Test + void update_success() { + // given + Member member = Member.builder().socialId("1").bug(BugFixture.bug()).build(); + member.increaseTotalCertifyCount(); + member.increaseTotalCertifyCount(); + + long expect = member.getTotalCertifyCount(); + RankingInfo before = MemberMapper.toRankingInfo(member); + rankingService.addRanking(before, member.getTotalCertifyCount()); + + // when + member.changeIntro("밥세공기"); + RankingInfo changeInfo = MemberMapper.toRankingInfo(member); + rankingService.changeInfos(before, changeInfo); + + Double resultDouble = redisTemplate.opsForZSet().score("Ranking", changeInfo); + + // then + assertAll(() -> assertThat(resultDouble).isNotNull(), + () -> assertThat(resultDouble).isEqualTo(Double.valueOf(expect))); + } + } + + @DisplayName("랭킹 삭제") + @Nested + class Delete { + + @DisplayName("성공") + @Test + void update_success() { + // given + Long totalCertify = 5L; + Member member = Member.builder().socialId("1").bug(BugFixture.bug()).build(); + member.increaseTotalCertifyCount(); + member.increaseTotalCertifyCount(); + RankingInfo rankingInfo = MemberMapper.toRankingInfo(member); + + rankingService.addRanking(rankingInfo, totalCertify); + + // when + rankingService.removeRanking(rankingInfo); + + Double resultDouble = redisTemplate.opsForZSet().score("Ranking", rankingInfo); + + // then + assertThat(resultDouble).isNull(); + } + } + + @DisplayName("조회") + @Nested + class Select { + + @DisplayName("성공") + @Test + void test() { + // given + redisTemplate.opsForZSet().add("Ranking", new RankingInfo(1L, "Hello1", "123"), 1); + redisTemplate.opsForZSet().add("Ranking", new RankingInfo(2L, "Hello2", "123"), 2); + redisTemplate.opsForZSet().add("Ranking", new RankingInfo(3L, "Hello3", "123"), 3); + redisTemplate.opsForZSet().add("Ranking", new RankingInfo(4L, "Hello4", "123"), 4); + + // when + setSerialize(Object.class); + Set> rankings = redisTemplate.opsForZSet() + .reverseRangeWithScores("Ranking", 0, 2); + setSerialize(String.class); + + // then + assertThat(rankings).hasSize(3); + } + + @DisplayName("일부만 조회 성공") + @Test + void search_part() { + // given + redisTemplate.opsForZSet().add("Ranking", new RankingInfo(1L, "Hello1", "123"), 1); + redisTemplate.opsForZSet().add("Ranking", new RankingInfo(2L, "Hello2", "123"), 2); + + // when + setSerialize(Object.class); + Set> rankings = redisTemplate.opsForZSet() + .reverseRangeWithScores("Ranking", 0, 10); + setSerialize(String.class); + + // then + assertThat(rankings).hasSize(2); + } + + private void setSerialize(Class classes) { + redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(classes)); + } + + @DisplayName("랭킹 조회 성공") + @Test + void getTopRankings() { + // given + for (int i = 0; i < 20; i++) { + RankingInfo rankingInfo = new RankingInfo((long)(i + 1), "Hello" + (i + 1), "123"); + redisTemplate.opsForZSet().add("Ranking", rankingInfo, i + 1); + } + RankingInfo rankingInfo = new RankingInfo(21L, "Hello22", "123"); + redisTemplate.opsForZSet().add("Ranking", rankingInfo, 20); + RankingInfo rankingInfo2 = new RankingInfo(22L, "Hello23", "123"); + redisTemplate.opsForZSet().add("Ranking", rankingInfo2, 19); + + UpdateRanking myRanking = UpdateRanking.builder() + .score(1L) + .rankingInfo(RankingInfo.builder().nickname("Hello1").memberId(1L).image("123").build()) + .build(); + // When + TopRankingResponse topRankingResponse = rankingService.getMemberRanking(myRanking); + + // Then + List topRankings = topRankingResponse.topRankings(); + TopRankingInfo myRank = topRankingResponse.myRanking(); + assertAll(() -> assertThat(topRankings).hasSize(10), () -> assertThat(myRank.score()).isEqualTo(1), + () -> assertThat(topRankings.get(0).rank()).isEqualTo(1), + () -> assertThat(topRankings.get(1).rank()).isEqualTo(1), + () -> assertThat(topRankings.get(2).rank()).isEqualTo(2), + () -> assertThat(topRankings.get(3).rank()).isEqualTo(2), + () -> assertThat(topRankings.get(4).rank()).isEqualTo(3)); + + } + } +} diff --git a/src/test/java/com/moabam/api/application/room/CertificationServiceConcurrencyTest.java b/src/test/java/com/moabam/api/application/room/CertificationServiceConcurrencyTest.java index fd1fdc1e..3edc5543 100644 --- a/src/test/java/com/moabam/api/application/room/CertificationServiceConcurrencyTest.java +++ b/src/test/java/com/moabam/api/application/room/CertificationServiceConcurrencyTest.java @@ -61,11 +61,11 @@ void certify_room_success() throws InterruptedException { } Room savedRoom = roomRepository.save(room); - Member member1 = MemberFixture.member("0000", "닉네임1"); - Member member2 = MemberFixture.member("1234", "닉네임2"); - Member member3 = MemberFixture.member("5678", "닉네임3"); - Member member4 = MemberFixture.member("3333", "닉네임4"); - Member member5 = MemberFixture.member("5555", "닉네임5"); + Member member1 = MemberFixture.member("0000"); + Member member2 = MemberFixture.member("1234"); + Member member3 = MemberFixture.member("5678"); + Member member4 = MemberFixture.member("3333"); + Member member5 = MemberFixture.member("5555"); List members = memberRepository.saveAll(List.of(member1, member2, member3, member4, member5)); diff --git a/src/test/java/com/moabam/api/application/room/CertificationServiceTest.java b/src/test/java/com/moabam/api/application/room/CertificationServiceTest.java index d15ecdf1..32b9eb9b 100644 --- a/src/test/java/com/moabam/api/application/room/CertificationServiceTest.java +++ b/src/test/java/com/moabam/api/application/room/CertificationServiceTest.java @@ -102,9 +102,9 @@ class CertificationServiceTest { void init() { room = spy(RoomFixture.room()); participant = spy(RoomFixture.participant(room, 1L)); - member1 = MemberFixture.member("1", "회원1"); - member2 = MemberFixture.member("2", "회원2"); - member3 = MemberFixture.member("3", "회원3"); + member1 = MemberFixture.member("1"); + member2 = MemberFixture.member("2"); + member3 = MemberFixture.member("3"); lenient().when(room.getId()).thenReturn(1L); lenient().when(participant.getRoom()).thenReturn(room); diff --git a/src/test/java/com/moabam/api/application/room/RoomServiceConcurrencyTest.java b/src/test/java/com/moabam/api/application/room/RoomServiceConcurrencyTest.java index 53a190f9..7e1c0f41 100644 --- a/src/test/java/com/moabam/api/application/room/RoomServiceConcurrencyTest.java +++ b/src/test/java/com/moabam/api/application/room/RoomServiceConcurrencyTest.java @@ -60,9 +60,9 @@ void enter_room_concurrency_test() throws InterruptedException { Room savedRoom = roomRepository.save(room); - Member member1 = MemberFixture.member("qwe", "닉네임1"); - Member member2 = MemberFixture.member("qwfe", "닉네임2"); - Member member3 = MemberFixture.member("qff", "닉네임3"); + Member member1 = MemberFixture.member("qwe"); + Member member2 = MemberFixture.member("qwfe"); + Member member3 = MemberFixture.member("qff"); memberRepository.saveAll(List.of(member1, member2, member3)); Participant participant1 = RoomFixture.participant(savedRoom, member1.getId()); @@ -79,7 +79,7 @@ void enter_room_concurrency_test() throws InterruptedException { // when for (int i = 0; i < threadCount; i++) { - Member member = MemberFixture.member(String.valueOf(i + 100), "test"); + Member member = MemberFixture.member(String.valueOf(i + 100)); newMembers.add(member); memberRepository.save(member); final Long memberId = member.getId(); diff --git a/src/test/java/com/moabam/api/application/room/RoomServiceTest.java b/src/test/java/com/moabam/api/application/room/RoomServiceTest.java index 667508f0..2a37acfc 100644 --- a/src/test/java/com/moabam/api/application/room/RoomServiceTest.java +++ b/src/test/java/com/moabam/api/application/room/RoomServiceTest.java @@ -115,7 +115,7 @@ void room_manager_mandate_success() { Long managerId = 1L; Long memberId = 2L; - Member member = MemberFixture.member("1234", "닉네임"); + Member member = MemberFixture.member("1234"); Room room = spy(RoomFixture.room()); given(room.getId()).willReturn(1L); diff --git a/src/test/java/com/moabam/api/domain/item/repository/InventorySearchRepositoryTest.java b/src/test/java/com/moabam/api/domain/item/repository/InventorySearchRepositoryTest.java index 649a73e7..b609e309 100644 --- a/src/test/java/com/moabam/api/domain/item/repository/InventorySearchRepositoryTest.java +++ b/src/test/java/com/moabam/api/domain/item/repository/InventorySearchRepositoryTest.java @@ -85,7 +85,7 @@ void empty_success() { @Test void find_one_success() { // given - Member member = memberRepository.save(member("999", "test")); + Member member = memberRepository.save(member("999")); Item item = itemRepository.save(nightMageSkin()); Inventory inventory = inventoryRepository.save(inventory(member.getId(), item)); @@ -100,7 +100,7 @@ void find_one_success() { @Test void find_default_success() { // given - Member member = memberRepository.save(member("11314", "test")); + Member member = memberRepository.save(member("11314")); Item item = itemRepository.save(nightMageSkin()); Inventory inventory = inventoryRepository.save(inventory(member.getId(), item)); inventory.select(member); @@ -116,8 +116,8 @@ void find_default_success() { @Test void find_all_default_type_night_success() { // given - Member member1 = memberRepository.save(member("625", "회원1")); - Member member2 = memberRepository.save(member("255", "회원2")); + Member member1 = memberRepository.save(member("625")); + Member member2 = memberRepository.save(member("255")); Item item = itemRepository.save(nightMageSkin()); Inventory inventory1 = inventoryRepository.save(inventory(member1.getId(), item)); Inventory inventory2 = inventoryRepository.save(inventory(member2.getId(), item)); @@ -141,7 +141,7 @@ class FindDefaultBird { @Test void bird_find_success() { // given - Member member = MemberFixture.member("fffdd", "test"); + Member member = MemberFixture.member("fffdd"); member.exitRoom(RoomType.MORNING); memberRepository.save(member); diff --git a/src/test/java/com/moabam/api/domain/member/MemberRepositoryTest.java b/src/test/java/com/moabam/api/domain/member/MemberRepositoryTest.java index 8f600444..90729351 100644 --- a/src/test/java/com/moabam/api/domain/member/MemberRepositoryTest.java +++ b/src/test/java/com/moabam/api/domain/member/MemberRepositoryTest.java @@ -58,7 +58,7 @@ class MemberRepositoryTest { @Test void test() { // given - Member member = MemberFixture.member("313", "test"); + Member member = MemberFixture.member("313"); memberRepository.save(member); // when @@ -76,7 +76,7 @@ class FindMemberTest { @Test void room_exist_and_manager_error() { // given - Member member = MemberFixture.member("1111", "test"); + Member member = MemberFixture.member("1111"); memberRepository.save(member); Room room = RoomFixture.room(); @@ -102,7 +102,7 @@ void room_exist_and_not_manager_success() { room.changeManagerNickname("test"); roomRepository.save(room); - Member member = MemberFixture.member("44", "test"); + Member member = MemberFixture.member("44"); member.changeNickName("not"); memberRepository.save(member); @@ -133,7 +133,7 @@ void member_not_found() { @Test void search_info_success() { // given - Member member = MemberFixture.member("hhhh", "test"); + Member member = MemberFixture.member("hhhh"); member.enterRoom(RoomType.MORNING); memberRepository.save(member); @@ -158,7 +158,7 @@ void search_info_success() { @Test void no_badges_search_success() { // given - Member member = MemberFixture.member("ttttt", "test"); + Member member = MemberFixture.member("ttttt"); member.enterRoom(RoomType.MORNING); memberRepository.save(member); diff --git a/src/test/java/com/moabam/api/presentation/MemberControllerTest.java b/src/test/java/com/moabam/api/presentation/MemberControllerTest.java index f9ce684a..6297567c 100644 --- a/src/test/java/com/moabam/api/presentation/MemberControllerTest.java +++ b/src/test/java/com/moabam/api/presentation/MemberControllerTest.java @@ -12,6 +12,7 @@ import java.nio.charset.StandardCharsets; import java.util.List; +import java.util.Objects; import java.util.Optional; import org.assertj.core.api.Assertions; @@ -29,6 +30,8 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.context.annotation.Import; +import org.springframework.data.redis.core.RedisTemplate; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatusCode; @@ -45,6 +48,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.moabam.api.application.auth.OAuth2AuthorizationServerRequestService; import com.moabam.api.application.image.ImageService; +import com.moabam.api.application.member.MemberMapper; import com.moabam.api.domain.auth.repository.TokenRepository; import com.moabam.api.domain.image.ImageType; import com.moabam.api.domain.item.Inventory; @@ -63,6 +67,8 @@ import com.moabam.api.domain.room.repository.RoomRepository; import com.moabam.api.dto.auth.TokenSaveValue; import com.moabam.api.dto.member.ModifyMemberRequest; +import com.moabam.api.dto.ranking.RankingInfo; +import com.moabam.global.config.EmbeddedRedisConfig; import com.moabam.global.config.OAuthConfig; import com.moabam.global.error.exception.UnauthorizedException; import com.moabam.global.error.handler.RestTemplateResponseHandler; @@ -83,6 +89,7 @@ @AutoConfigureMockMvc @AutoConfigureMockRestServiceServer @TestInstance(TestInstance.Lifecycle.PER_CLASS) +@Import(EmbeddedRedisConfig.class) class MemberControllerTest extends WithoutFilterSupporter { @Autowired @@ -133,12 +140,15 @@ class MemberControllerTest extends WithoutFilterSupporter { @Autowired EntityManager entityManager; + @Autowired + RedisTemplate redisTemplate; + @BeforeAll void allSetUp() { restTemplateBuilder = new RestTemplateBuilder() .errorHandler(new RestTemplateResponseHandler()); - member = MemberFixture.member("1234567890987654", "nickname"); + member = MemberFixture.member("1234567890987654"); member.increaseTotalCertifyCount(); memberRepository.save(member); } @@ -361,7 +371,7 @@ void search_my_info_with_no_badge_success() throws Exception { @Test void search_friend_info_success() throws Exception { // given - Member friend = MemberFixture.member("123456789", "nick"); + Member friend = MemberFixture.member("123456789"); memberRepository.save(friend); Badge morningBirth = BadgeFixture.badge(friend.getId(), BadgeType.MORNING_BIRTH); @@ -481,6 +491,7 @@ void member_modify_request_success(String intro, String nickname) throws Excepti .characterEncoding("UTF-8")) .andExpect(status().is2xxSuccessful()) .andDo(print()); + } @DisplayName("회원 프로필없이 성공 ") @@ -499,6 +510,8 @@ void member_modify_no_image_request_success(String intro, String nickname) throw willThrow(NullPointerException.class) .given(imageService).uploadImages(any(), any()); + RankingInfo rankingInfo = MemberMapper.toRankingInfo(member); + redisTemplate.opsForZSet().add("Ranking", rankingInfo, member.getTotalCertifyCount()); // expected mockMvc.perform(multipart(HttpMethod.POST, "/members/modify") @@ -508,5 +521,15 @@ void member_modify_no_image_request_success(String intro, String nickname) throw .characterEncoding("UTF-8")) .andExpect(status().is2xxSuccessful()) .andDo(print()); + + String updateNick = member.getNickname(); + + if (Objects.nonNull(nickname)) { + updateNick = nickname; + } + + Double result = redisTemplate.opsForZSet() + .score("Ranking", new RankingInfo(member.getId(), updateNick, member.getProfileImage())); + assertThat(result).isEqualTo(member.getTotalCertifyCount()); } } diff --git a/src/test/java/com/moabam/api/presentation/NotificationControllerTest.java b/src/test/java/com/moabam/api/presentation/NotificationControllerTest.java index 3df0a842..400ce3ee 100644 --- a/src/test/java/com/moabam/api/presentation/NotificationControllerTest.java +++ b/src/test/java/com/moabam/api/presentation/NotificationControllerTest.java @@ -73,7 +73,7 @@ class NotificationControllerTest extends WithoutFilterSupporter { @BeforeEach void setUp() { - target = memberRepository.save(MemberFixture.member("123", "targetName")); + target = memberRepository.save(MemberFixture.member("123")); room = roomRepository.save(RoomFixture.room()); knockKey = String.format("room_%s_member_%s_knocks_%s", room.getId(), 1, target.getId()); diff --git a/src/test/java/com/moabam/api/presentation/RankingControllerTest.java b/src/test/java/com/moabam/api/presentation/RankingControllerTest.java new file mode 100644 index 00000000..9e124cb2 --- /dev/null +++ b/src/test/java/com/moabam/api/presentation/RankingControllerTest.java @@ -0,0 +1,82 @@ +package com.moabam.api.presentation; + +import static org.hamcrest.Matchers.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.transaction.annotation.Transactional; + +import com.moabam.api.domain.member.Member; +import com.moabam.api.domain.member.repository.MemberRepository; +import com.moabam.api.dto.ranking.RankingInfo; +import com.moabam.api.dto.ranking.UpdateRanking; +import com.moabam.support.annotation.WithMember; +import com.moabam.support.common.WithoutFilterSupporter; +import com.moabam.support.fixture.MemberFixture; + +@Transactional +@SpringBootTest +@AutoConfigureMockMvc +@AutoConfigureRestDocs +class RankingControllerTest extends WithoutFilterSupporter { + + @Autowired + MockMvc mockMvc; + + @Autowired + MemberRepository memberRepository; + + @Autowired + RedisTemplate redisTemplate; + + @DisplayName("") + @WithMember + @Test + void top_ranking() throws Exception { + // given + List members = new ArrayList<>(); + for (int i = 0; i < 20; i++) { + Member member = MemberFixture.member(String.valueOf(i + 1)); + members.add(member); + + RankingInfo rankingInfo = new RankingInfo((long)(i + 1), member.getNickname(), member.getProfileImage()); + redisTemplate.opsForZSet().add("Ranking", rankingInfo, i + 1); + } + memberRepository.saveAll(members); + + RankingInfo rankingInfo = new RankingInfo(21L, "Hello22", "123"); + redisTemplate.opsForZSet().add("Ranking", rankingInfo, 20); + RankingInfo rankingInfo2 = new RankingInfo(22L, "Hello23", "123"); + redisTemplate.opsForZSet().add("Ranking", rankingInfo2, 19); + + UpdateRanking myRanking = UpdateRanking.builder() + .score(1L) + .rankingInfo(RankingInfo.builder() + .nickname(members.get(0).getNickname()) + .memberId(members.get(0).getId()) + .image(members.get(0).getProfileImage()).build()) + .build(); + + // when + mockMvc.perform(MockMvcRequestBuilders.get("/rankings")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.topRankings", hasSize(10))) + .andExpect(jsonPath("$.myRanking.nickname", is(members.get(0).getNickname()))) + .andExpect(jsonPath("$.myRanking.rank", is(21))); + + // then + + } + +} diff --git a/src/test/java/com/moabam/api/presentation/ReportControllerTest.java b/src/test/java/com/moabam/api/presentation/ReportControllerTest.java index 006eb10a..4f5c688d 100644 --- a/src/test/java/com/moabam/api/presentation/ReportControllerTest.java +++ b/src/test/java/com/moabam/api/presentation/ReportControllerTest.java @@ -110,7 +110,7 @@ void reports_success(boolean roomFilter, boolean certificationFilter) throws Exc @Test void reports_failBy_subject_null() throws Exception { // given - Member member = MemberFixture.member("2", "ji"); + Member member = MemberFixture.member("2"); memberRepository.save(member); ReportRequest reportRequest = ReportFixture.reportRequest(member.getId(), null, null); @@ -128,7 +128,7 @@ void reports_failBy_subject_null() throws Exception { @Test void reports_failBy_member() throws Exception { // given - Member newMember = MemberFixture.member("9999", "n"); + Member newMember = MemberFixture.member("9999"); memberRepository.save(newMember); newMember.delete(LocalDateTime.now()); diff --git a/src/test/java/com/moabam/api/presentation/RoomControllerTest.java b/src/test/java/com/moabam/api/presentation/RoomControllerTest.java index c922bc93..a1c850e7 100644 --- a/src/test/java/com/moabam/api/presentation/RoomControllerTest.java +++ b/src/test/java/com/moabam/api/presentation/RoomControllerTest.java @@ -799,8 +799,8 @@ void get_room_details_test() throws Exception { Participant participant1 = RoomFixture.participant(room, 1L); participant1.enableManager(); - Member member2 = MemberFixture.member("2", "NICK2"); - Member member3 = MemberFixture.member("3", "NICK3"); + Member member2 = MemberFixture.member("2"); + Member member3 = MemberFixture.member("3"); roomRepository.save(room); routineRepository.saveAll(routines); @@ -865,7 +865,7 @@ void get_room_details_test() throws Exception { void deport_member_success() throws Exception { // given Room room = RoomFixture.room(); - Member member = MemberFixture.member("1234", "참여자"); + Member member = MemberFixture.member("1234"); memberRepository.save(member); Participant memberParticipant = RoomFixture.participant(room, member.getId()); @@ -916,7 +916,7 @@ void deport_self_fail() throws Exception { @Test void mandate_manager_success() throws Exception { // given - Member member2 = MemberFixture.member("1234", "방장될 멤버"); + Member member2 = MemberFixture.member("1234"); memberRepository.save(member2); Room room = RoomFixture.room(); @@ -1016,7 +1016,7 @@ void get_un_joined_room_details() throws Exception { Room room = RoomFixture.room("테스트 방", NIGHT, 21); Room savedRoom = roomRepository.save(room); - Member member1 = MemberFixture.member("901010", "testtest"); + Member member1 = MemberFixture.member("901010"); member1 = memberRepository.save(member1); Item item = ItemFixture.nightMageSkin(); @@ -1536,8 +1536,8 @@ void search_first_page_all_rooms_by_keyword_roomType_roomId_success() throws Exc @Test void get_room_details_before_modification_success() throws Exception { // given - Member member2 = MemberFixture.member("123", "참여자1"); - Member member3 = MemberFixture.member("456", "참여자2"); + Member member2 = MemberFixture.member("123"); + Member member3 = MemberFixture.member("456"); member2 = memberRepository.save(member2); member3 = memberRepository.save(member3); diff --git a/src/test/java/com/moabam/support/fixture/MemberFixture.java b/src/test/java/com/moabam/support/fixture/MemberFixture.java index 3bf73d15..c0ed7551 100644 --- a/src/test/java/com/moabam/support/fixture/MemberFixture.java +++ b/src/test/java/com/moabam/support/fixture/MemberFixture.java @@ -30,7 +30,7 @@ public static Member member(Bug bug) { .build(); } - public static Member member(String socialId, String nickname) { + public static Member member(String socialId) { return Member.builder() .socialId(socialId) .bug(BugFixture.bug()) diff --git a/src/test/java/com/moabam/support/fixture/RankingInfoFixture.java b/src/test/java/com/moabam/support/fixture/RankingInfoFixture.java new file mode 100644 index 00000000..9058ebcc --- /dev/null +++ b/src/test/java/com/moabam/support/fixture/RankingInfoFixture.java @@ -0,0 +1,5 @@ +package com.moabam.support.fixture; + +public class RankingInfoFixture { + +}