From 7895c21d8ede93739daafac6726940071d8ae14e Mon Sep 17 00:00:00 2001 From: Songyi Kim <52441906+songyi00@users.noreply.github.com> Date: Sat, 10 Aug 2024 02:56:22 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=AF=B8=EC=85=98=20=EC=B5=9C=EC=A2=85?= =?UTF-8?q?=20=EC=88=9C=EC=9C=84=20=EC=A1=B0=ED=9A=8C=20API=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#39)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 미션 최종 순위 조회 API 구현 * feat: 동일한 인증 횟수인 경우 동일 순위로 지정 --- .../mission/MissionMemberService.java | 19 ++++-- .../dto/response/MemberRankResponse.java | 16 +++++ .../goalpanzi/domain/mission/MemberRank.java | 9 +++ .../goalpanzi/domain/mission/MemberRanks.java | 68 +++++++++++++++++++ .../mission/MissionMemberController.java | 13 ++++ .../mission/MissionMemberControllerDocs.java | 8 +++ .../domain/mission/MemberRanksTest.java | 37 ++++++++++ .../goalpanzi/fixture/MissionFixture.java | 18 +++++ 8 files changed, 184 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/nexters/goalpanzi/application/mission/dto/response/MemberRankResponse.java create mode 100644 src/main/java/com/nexters/goalpanzi/domain/mission/MemberRank.java create mode 100644 src/main/java/com/nexters/goalpanzi/domain/mission/MemberRanks.java create mode 100644 src/test/java/com/nexters/goalpanzi/domain/mission/MemberRanksTest.java diff --git a/src/main/java/com/nexters/goalpanzi/application/mission/MissionMemberService.java b/src/main/java/com/nexters/goalpanzi/application/mission/MissionMemberService.java index 268793e5..b8e2c574 100644 --- a/src/main/java/com/nexters/goalpanzi/application/mission/MissionMemberService.java +++ b/src/main/java/com/nexters/goalpanzi/application/mission/MissionMemberService.java @@ -1,5 +1,6 @@ package com.nexters.goalpanzi.application.mission; +import com.nexters.goalpanzi.application.mission.dto.response.MemberRankResponse; import com.nexters.goalpanzi.application.mission.dto.response.MissionsResponse; import com.nexters.goalpanzi.domain.common.BaseEntity; import com.nexters.goalpanzi.domain.member.Member; @@ -7,6 +8,7 @@ import com.nexters.goalpanzi.domain.mission.InvitationCode; import com.nexters.goalpanzi.domain.mission.Mission; import com.nexters.goalpanzi.domain.mission.MissionMember; +import com.nexters.goalpanzi.domain.mission.MemberRanks; import com.nexters.goalpanzi.domain.mission.repository.MissionMemberRepository; import com.nexters.goalpanzi.domain.mission.repository.MissionRepository; import com.nexters.goalpanzi.exception.AlreadyExistsException; @@ -30,11 +32,16 @@ public class MissionMemberService { @Transactional public void joinMission(final Long memberId, final InvitationCode invitationCode) { Member member = memberRepository.getMember(memberId); - Mission mission = getMission(invitationCode); + Mission mission = getMissionByCode(invitationCode); validateAlreadyJoin(member, mission); missionMemberRepository.save(MissionMember.join(member, mission)); } + private Mission getMissionByCode(final InvitationCode invitationCode) { + return missionRepository.findByInvitationCode(invitationCode) + .orElseThrow(() -> new NotFoundException(ErrorCode.NOT_FOUND_MISSION, invitationCode.getCode())); + } + private void validateAlreadyJoin(final Member member, final Mission mission) { missionMemberRepository.findByMemberIdAndMissionId(member.getId(), mission.getId()) .ifPresent(missionMember -> { @@ -60,8 +67,12 @@ public void deleteAllByMissionId(final Long missionId) { .forEach(BaseEntity::delete); } - private Mission getMission(final InvitationCode invitationCode) { - return missionRepository.findByInvitationCode(invitationCode) - .orElseThrow(() -> new NotFoundException(ErrorCode.NOT_FOUND_MISSION, invitationCode.getCode())); + public MemberRankResponse getMissionRank(final Long missionId, final Long memberId) { + Member member = memberRepository.getMember(memberId); + List missionMembers = missionMemberRepository.findAllByMissionId(missionId); + + MemberRanks memberRanks = MemberRanks.from(missionMembers); + + return MemberRankResponse.from(memberRanks.getRankByMember(member)); } } diff --git a/src/main/java/com/nexters/goalpanzi/application/mission/dto/response/MemberRankResponse.java b/src/main/java/com/nexters/goalpanzi/application/mission/dto/response/MemberRankResponse.java new file mode 100644 index 00000000..e35a199e --- /dev/null +++ b/src/main/java/com/nexters/goalpanzi/application/mission/dto/response/MemberRankResponse.java @@ -0,0 +1,16 @@ +package com.nexters.goalpanzi.application.mission.dto.response; + +import com.nexters.goalpanzi.domain.mission.MemberRank; +import io.swagger.v3.oas.annotations.media.Schema; + +public record MemberRankResponse( + @Schema(description = "미션 최종 순위") + Integer rank +) { + + public static MemberRankResponse from(final MemberRank memberRank) { + return new MemberRankResponse( + memberRank.rank() + ); + } +} diff --git a/src/main/java/com/nexters/goalpanzi/domain/mission/MemberRank.java b/src/main/java/com/nexters/goalpanzi/domain/mission/MemberRank.java new file mode 100644 index 00000000..b04b15e4 --- /dev/null +++ b/src/main/java/com/nexters/goalpanzi/domain/mission/MemberRank.java @@ -0,0 +1,9 @@ +package com.nexters.goalpanzi.domain.mission; + +import com.nexters.goalpanzi.domain.member.Member; + +public record MemberRank( + Member member, + Integer rank +) { +} diff --git a/src/main/java/com/nexters/goalpanzi/domain/mission/MemberRanks.java b/src/main/java/com/nexters/goalpanzi/domain/mission/MemberRanks.java new file mode 100644 index 00000000..eabba6b8 --- /dev/null +++ b/src/main/java/com/nexters/goalpanzi/domain/mission/MemberRanks.java @@ -0,0 +1,68 @@ +package com.nexters.goalpanzi.domain.mission; + +import com.nexters.goalpanzi.domain.member.Member; +import lombok.RequiredArgsConstructor; + +import java.util.ArrayList; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Objects; + +@RequiredArgsConstructor +public class MemberRanks { + + private final List memberRanks; + + public static MemberRanks from(final List missionMembers) { + List sortedMissionMembers = sortedMembersByVerificationCountDesc(missionMembers); + + List memberRanks = new ArrayList<>(); + int rank = 1; + int previousVerificationCount = sortedMissionMembers.getFirst().getVerificationCount(); + + for (int i = 0; i < sortedMissionMembers.size(); i++) { + MissionMember missionMember = sortedMissionMembers.get(i); + if (missionMember.getVerificationCount() < previousVerificationCount) { + rank = i + 1; + } + memberRanks.add(new MemberRank(missionMember.getMember(), rank)); + previousVerificationCount = missionMember.getVerificationCount(); + } + + return new MemberRanks(memberRanks); + } + + private static List sortedMembersByVerificationCountDesc(final List missionMembers) { + return missionMembers + .stream() + .sorted((m1, m2) -> m2.getVerificationCount() - m1.getVerificationCount()) + .toList(); + } + + public MemberRank getRankByMember(final Member member) { + return memberRanks.stream() + .filter(memberRank -> memberRank.member().equals(member)) + .findFirst() + .orElseThrow(() -> new NoSuchElementException("No rank found for member " + member.getId())); + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + MemberRanks memberRanks1 = (MemberRanks) o; + return Objects.equals(memberRanks, memberRanks1.memberRanks); + } + + @Override + public int hashCode() { + return Objects.hashCode(memberRanks); + } + + @Override + public String toString() { + return "Ranks{" + + "ranks=" + memberRanks + + '}'; + } +} diff --git a/src/main/java/com/nexters/goalpanzi/presentation/mission/MissionMemberController.java b/src/main/java/com/nexters/goalpanzi/presentation/mission/MissionMemberController.java index 2ed38bc5..7d0f5da3 100644 --- a/src/main/java/com/nexters/goalpanzi/presentation/mission/MissionMemberController.java +++ b/src/main/java/com/nexters/goalpanzi/presentation/mission/MissionMemberController.java @@ -1,6 +1,7 @@ package com.nexters.goalpanzi.presentation.mission; import com.nexters.goalpanzi.application.mission.MissionMemberService; +import com.nexters.goalpanzi.application.mission.dto.response.MemberRankResponse; import com.nexters.goalpanzi.application.mission.dto.response.MissionsResponse; import com.nexters.goalpanzi.common.argumentresolver.LoginMemberId; import com.nexters.goalpanzi.domain.mission.InvitationCode; @@ -11,6 +12,7 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RequiredArgsConstructor @@ -35,4 +37,15 @@ public ResponseEntity joinMission( return ResponseEntity.ok().build(); } + + @Override + @GetMapping("/rank") + public ResponseEntity getMissionRank( + @RequestParam final Long missionId, + @LoginMemberId final Long memberId + ) { + MemberRankResponse response = missionMemberService.getMissionRank(missionId, memberId); + + return ResponseEntity.ok(response); + } } diff --git a/src/main/java/com/nexters/goalpanzi/presentation/mission/MissionMemberControllerDocs.java b/src/main/java/com/nexters/goalpanzi/presentation/mission/MissionMemberControllerDocs.java index 76d792e4..d8e181ca 100644 --- a/src/main/java/com/nexters/goalpanzi/presentation/mission/MissionMemberControllerDocs.java +++ b/src/main/java/com/nexters/goalpanzi/presentation/mission/MissionMemberControllerDocs.java @@ -1,5 +1,6 @@ package com.nexters.goalpanzi.presentation.mission; +import com.nexters.goalpanzi.application.mission.dto.response.MemberRankResponse; import com.nexters.goalpanzi.application.mission.dto.response.MissionsResponse; import com.nexters.goalpanzi.common.argumentresolver.LoginMemberId; import com.nexters.goalpanzi.presentation.mission.dto.JoinMissionRequest; @@ -8,6 +9,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; @Tag(name = "미션/회원") public interface MissionMemberControllerDocs { @@ -20,4 +22,10 @@ ResponseEntity joinMission( @Parameter(hidden = true) @LoginMemberId final Long memberId, @RequestBody JoinMissionRequest request ); + + @Operation(summary = "내 미션 최종 순위 조회") + ResponseEntity getMissionRank( + @RequestParam final Long missionId, + @Parameter(hidden = true) @LoginMemberId final Long memberId + ); } diff --git a/src/test/java/com/nexters/goalpanzi/domain/mission/MemberRanksTest.java b/src/test/java/com/nexters/goalpanzi/domain/mission/MemberRanksTest.java new file mode 100644 index 00000000..5e51853f --- /dev/null +++ b/src/test/java/com/nexters/goalpanzi/domain/mission/MemberRanksTest.java @@ -0,0 +1,37 @@ +package com.nexters.goalpanzi.domain.mission; + +import com.nexters.goalpanzi.domain.member.Member; +import com.nexters.goalpanzi.domain.member.SocialType; +import com.nexters.goalpanzi.fixture.MissionFixture; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static com.nexters.goalpanzi.fixture.MemberFixture.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +class MemberRanksTest { + + @Test + void 미션_최종_등수를_확인할_수_있다() { + // given + Member memberHost = Member.socialLogin(SOCIAL_ID, EMAIL_HOST, SocialType.APPLE); + Member memberA = Member.socialLogin(SOCIAL_ID, EMAIL_MEMBER_A, SocialType.APPLE); + Member memberB = Member.socialLogin(SOCIAL_ID, EMAIL_MEMBER_B, SocialType.GOOGLE); + + List missionMembers = List.of( + new MissionMember(memberHost, MissionFixture.create(), 10), + new MissionMember(memberA, MissionFixture.create(), 12), + new MissionMember(memberB, MissionFixture.create(), 14) + ); + + // when, then + MemberRanks actual = MemberRanks.from(missionMembers); + assertAll( + () -> assertThat(actual.getRankByMember(memberB).rank()).isEqualTo(1), + () -> assertThat(actual.getRankByMember(memberA).rank()).isEqualTo(2), + () -> assertThat(actual.getRankByMember(memberHost).rank()).isEqualTo(3) + ); + } +} \ No newline at end of file diff --git a/src/test/java/com/nexters/goalpanzi/fixture/MissionFixture.java b/src/test/java/com/nexters/goalpanzi/fixture/MissionFixture.java index 5ccceaa5..9fb42cdc 100644 --- a/src/test/java/com/nexters/goalpanzi/fixture/MissionFixture.java +++ b/src/test/java/com/nexters/goalpanzi/fixture/MissionFixture.java @@ -1,6 +1,8 @@ package com.nexters.goalpanzi.fixture; import com.nexters.goalpanzi.domain.mission.DayOfWeek; +import com.nexters.goalpanzi.domain.mission.InvitationCode; +import com.nexters.goalpanzi.domain.mission.Mission; import org.springframework.mock.web.MockMultipartFile; import org.springframework.web.multipart.MultipartFile; @@ -8,9 +10,11 @@ import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; +import java.time.LocalDateTime; import java.util.List; import static com.nexters.goalpanzi.domain.mission.DayOfWeek.*; +import static com.nexters.goalpanzi.fixture.MemberFixture.MEMBER_ID; public class MissionFixture { public static final String DESCRIPTION = "운동하기"; @@ -20,6 +24,20 @@ public class MissionFixture { public static final MultipartFile IMAGE_FILE; public static final String UPLOADED_IMAGE_URL = "uploadedImageUrl"; + public static Mission create() { + return new Mission( + MEMBER_ID, + DESCRIPTION, + InvitationCode.generate(), + LocalDateTime.now(), + LocalDateTime.now().plusDays(10), + "10:00", + "12:00", + List.of(MONDAY), + 10 + ); + } + static { try { File tempFile = File.createTempFile("미션 인증 이미지", ".jpg");