Skip to content

Commit

Permalink
feat: 미션 최종 순위 조회 API 구현 (#39)
Browse files Browse the repository at this point in the history
* feat: 미션 최종 순위 조회 API 구현

* feat: 동일한 인증 횟수인 경우 동일 순위로 지정
  • Loading branch information
songyi00 authored Aug 9, 2024
1 parent 6d69468 commit 7895c21
Show file tree
Hide file tree
Showing 8 changed files with 184 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
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;
import com.nexters.goalpanzi.domain.member.repository.MemberRepository;
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;
Expand All @@ -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 -> {
Expand All @@ -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<MissionMember> missionMembers = missionMemberRepository.findAllByMissionId(missionId);

MemberRanks memberRanks = MemberRanks.from(missionMembers);

return MemberRankResponse.from(memberRanks.getRankByMember(member));
}
}
Original file line number Diff line number Diff line change
@@ -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()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.nexters.goalpanzi.domain.mission;

import com.nexters.goalpanzi.domain.member.Member;

public record MemberRank(
Member member,
Integer rank
) {
}
Original file line number Diff line number Diff line change
@@ -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<MemberRank> memberRanks;

public static MemberRanks from(final List<MissionMember> missionMembers) {
List<MissionMember> sortedMissionMembers = sortedMembersByVerificationCountDesc(missionMembers);

List<MemberRank> 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<MissionMember> sortedMembersByVerificationCountDesc(final List<MissionMember> 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 +
'}';
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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
Expand All @@ -35,4 +37,15 @@ public ResponseEntity<Void> joinMission(

return ResponseEntity.ok().build();
}

@Override
@GetMapping("/rank")
public ResponseEntity<MemberRankResponse> getMissionRank(
@RequestParam final Long missionId,
@LoginMemberId final Long memberId
) {
MemberRankResponse response = missionMemberService.getMissionRank(missionId, memberId);

return ResponseEntity.ok(response);
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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 {
Expand All @@ -20,4 +22,10 @@ ResponseEntity<Void> joinMission(
@Parameter(hidden = true) @LoginMemberId final Long memberId,
@RequestBody JoinMissionRequest request
);

@Operation(summary = "내 미션 최종 순위 조회")
ResponseEntity<MemberRankResponse> getMissionRank(
@RequestParam final Long missionId,
@Parameter(hidden = true) @LoginMemberId final Long memberId
);
}
Original file line number Diff line number Diff line change
@@ -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<MissionMember> 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)
);
}
}
18 changes: 18 additions & 0 deletions src/test/java/com/nexters/goalpanzi/fixture/MissionFixture.java
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
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;

import java.io.File;
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 = "운동하기";
Expand All @@ -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");
Expand Down

0 comments on commit 7895c21

Please sign in to comment.