From 6d69468d328fa0ea110cd72bfc130338b0d8af2a Mon Sep 17 00:00:00 2001 From: KimYujeong Date: Fri, 9 Aug 2024 23:51:00 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=B3=B4=EB=93=9C=ED=8C=90=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20API=20=EA=B5=AC=ED=98=84=20(#36)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 보드판 정보 조회 구현 * test: 보드판 정보 테스트 작성 * fix: 미션 인증에서 누락된 유효성 검사 추가 * feat: 보드판 이벤트 enum 정의 * rename: fixture 추가에 따른 이름 수정 * rename: enum 이름 수정 * refactor: dto로 한 겹 감싸기 * feat: 보드쪽 dto 작성 * rename: enum 이름 변경 * refactor: dto 및 enum 변경 --- .../mission/MissionBoardService.java | 66 +++++++++++++++ .../mission/MissionVerificationService.java | 29 ++++--- .../dto/request/MissionBoardQuery.java | 6 ++ .../response/MissionBoardMemberResponse.java | 12 +++ .../dto/response/MissionBoardResponse.java | 27 +++++++ .../dto/response/MissionBoardsResponse.java | 11 +++ .../MissionVerificationsResponse.java | 11 +++ .../goalpanzi/domain/mission/Mission.java | 17 +++- .../goalpanzi/domain/mission/Reward.java | 38 +++++++++ .../goalpanzi/exception/ErrorCode.java | 2 + .../mission/MissionBoardController.java | 28 +++++++ .../mission/MissionBoardControllerDocs.java | 33 ++++++++ .../MissionVerificationController.java | 6 +- .../MissionVerificationControllerDocs.java | 4 +- .../goalpanzi/acceptance/AcceptanceStep.java | 22 +++++ .../MissionBoardAcceptanceTest.java | 63 +++++++++++++++ .../MissionVerificationAcceptanceTest.java | 80 +++++++++++++------ .../goalpanzi/domain/member/MemberTest.java | 14 ++-- .../goalpanzi/fixture/MemberFixture.java | 10 ++- 19 files changed, 428 insertions(+), 51 deletions(-) create mode 100644 src/main/java/com/nexters/goalpanzi/application/mission/MissionBoardService.java create mode 100644 src/main/java/com/nexters/goalpanzi/application/mission/dto/request/MissionBoardQuery.java create mode 100644 src/main/java/com/nexters/goalpanzi/application/mission/dto/response/MissionBoardMemberResponse.java create mode 100644 src/main/java/com/nexters/goalpanzi/application/mission/dto/response/MissionBoardResponse.java create mode 100644 src/main/java/com/nexters/goalpanzi/application/mission/dto/response/MissionBoardsResponse.java create mode 100644 src/main/java/com/nexters/goalpanzi/application/mission/dto/response/MissionVerificationsResponse.java create mode 100644 src/main/java/com/nexters/goalpanzi/domain/mission/Reward.java create mode 100644 src/main/java/com/nexters/goalpanzi/presentation/mission/MissionBoardController.java create mode 100644 src/main/java/com/nexters/goalpanzi/presentation/mission/MissionBoardControllerDocs.java create mode 100644 src/test/java/com/nexters/goalpanzi/acceptance/MissionBoardAcceptanceTest.java diff --git a/src/main/java/com/nexters/goalpanzi/application/mission/MissionBoardService.java b/src/main/java/com/nexters/goalpanzi/application/mission/MissionBoardService.java new file mode 100644 index 00000000..cf0fb6b6 --- /dev/null +++ b/src/main/java/com/nexters/goalpanzi/application/mission/MissionBoardService.java @@ -0,0 +1,66 @@ +package com.nexters.goalpanzi.application.mission; + +import com.nexters.goalpanzi.application.mission.dto.request.MissionBoardQuery; +import com.nexters.goalpanzi.application.mission.dto.response.MissionBoardResponse; +import com.nexters.goalpanzi.application.mission.dto.response.MissionBoardsResponse; +import com.nexters.goalpanzi.domain.common.BaseEntity; +import com.nexters.goalpanzi.domain.member.Member; +import com.nexters.goalpanzi.domain.mission.Mission; +import com.nexters.goalpanzi.domain.mission.repository.MissionMemberRepository; +import com.nexters.goalpanzi.domain.mission.repository.MissionRepository; +import com.nexters.goalpanzi.exception.ErrorCode; +import com.nexters.goalpanzi.exception.NotFoundException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +@RequiredArgsConstructor +@Service +public class MissionBoardService { + + private final MissionRepository missionRepository; + private final MissionMemberRepository missionMemberRepository; + + @Transactional(readOnly = true) + public MissionBoardsResponse getBoard(final MissionBoardQuery query) { + return new MissionBoardsResponse( + missionRepository.findById(query.missionId()) + .map(this::groupByVerificationCount) + .map(this::sortByVerifiedAt) + .map(this::convertToBoardResponse) + .orElseThrow(() -> new NotFoundException(ErrorCode.NOT_FOUND_MISSION, query.missionId()))); + } + + private Map> groupByVerificationCount(final Mission mission) { + Map> board = initializeBoard(mission.getBoardCount()); + + missionMemberRepository.findAllByMissionId(mission.getId()) + .forEach(m -> board.get(m.getVerificationCount()).add(m.getMember())); + return board; + } + + private Map> sortByVerifiedAt(final Map> groupedMembers) { + return groupedMembers.entrySet().stream() + .peek(entry -> entry.getValue().sort(Comparator.comparing(BaseEntity::getCreatedAt))) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + private List convertToBoardResponse(final Map> groupedAndSortedMembers) { + return groupedAndSortedMembers.entrySet().stream() + .map(entry -> MissionBoardResponse.of(entry.getKey(), entry.getValue())) + .collect(Collectors.toList()); + } + + private Map> initializeBoard(final Integer boardCount) { + return IntStream.range(0, boardCount + 1) + .boxed() + .collect(Collectors.toMap(i -> i, i -> new ArrayList<>())); + } +} \ No newline at end of file diff --git a/src/main/java/com/nexters/goalpanzi/application/mission/MissionVerificationService.java b/src/main/java/com/nexters/goalpanzi/application/mission/MissionVerificationService.java index 3a008707..861c1e3c 100644 --- a/src/main/java/com/nexters/goalpanzi/application/mission/MissionVerificationService.java +++ b/src/main/java/com/nexters/goalpanzi/application/mission/MissionVerificationService.java @@ -4,6 +4,7 @@ import com.nexters.goalpanzi.application.mission.dto.request.MissionVerificationQuery; import com.nexters.goalpanzi.application.mission.dto.request.MyMissionVerificationQuery; import com.nexters.goalpanzi.application.mission.dto.response.MissionVerificationResponse; +import com.nexters.goalpanzi.application.mission.dto.response.MissionVerificationsResponse; import com.nexters.goalpanzi.application.upload.ObjectStorageClient; import com.nexters.goalpanzi.domain.common.BaseEntity; import com.nexters.goalpanzi.domain.member.Member; @@ -37,7 +38,7 @@ public class MissionVerificationService { private final ObjectStorageClient objectStorageClient; @Transactional(readOnly = true) - public List getVerifications(final MissionVerificationQuery query) { + public MissionVerificationsResponse getVerifications(final MissionVerificationQuery query) { LocalDate date = query.date() != null ? query.date() : LocalDate.now(); Member member = memberRepository.getMember(query.memberId()); List verifications = missionVerificationRepository.findAllByMissionIdAndDate(query.missionId(), date); @@ -45,11 +46,12 @@ public List getVerifications(final MissionVerificat Map verificationMap = verifications.stream() .collect(Collectors.toMap(v -> v.getMember().getId(), v -> v)); - return missionMemberRepository.findAllByMissionId(query.missionId()).stream() - .map(m -> convertToVerificationResponse(m, verificationMap.get(m.getMember().getId()))) - .sorted(Comparator.comparing((MissionVerificationResponse r) -> r.nickname().equals(member.getNickname())).reversed() - .thenComparing(MissionVerificationResponse::verifiedAt, Comparator.nullsLast(Comparator.reverseOrder()))) - .collect(Collectors.toList()); + return new MissionVerificationsResponse( + missionMemberRepository.findAllByMissionId(query.missionId()).stream() + .map(m -> convertToVerificationResponse(m, verificationMap.get(m.getMember().getId()))) + .sorted(Comparator.comparing((MissionVerificationResponse r) -> r.nickname().equals(member.getNickname())).reversed() + .thenComparing(MissionVerificationResponse::verifiedAt, Comparator.nullsLast(Comparator.reverseOrder()))) + .collect(Collectors.toList())); } public MissionVerificationResponse getMyVerification(final MyMissionVerificationQuery query) { @@ -80,21 +82,26 @@ private void checkVerificationValidation(final Long memberId, final Mission miss if (isCompletedMission(mission, missionMember)) { throw new BadRequestException(ErrorCode.ALREADY_COMPLETED_MISSION); } - LocalDate today = LocalDate.now(); - if (isDuplicatedVerification(memberId, mission.getId(), today)) { + if (isDuplicatedVerification(memberId, mission.getId())) { throw new BadRequestException(ErrorCode.DUPLICATE_VERIFICATION); } - if (!mission.isMissionDay(today)) { + if (!mission.isMissionPeriod()) { + throw new BadRequestException(ErrorCode.NOT_VERIFICATION_PERIOD); + } + if (!mission.isMissionDay()) { throw new BadRequestException(ErrorCode.NOT_VERIFICATION_DAY); } + if (!mission.isMissionTime()) { + throw new BadRequestException(ErrorCode.NOT_VERIFICATION_TIME); + } } private boolean isCompletedMission(final Mission mission, final MissionMember missionMember) { return missionMember.getVerificationCount() >= mission.getBoardCount(); } - private boolean isDuplicatedVerification(final Long memberId, final Long missionId, final LocalDate today) { - return missionVerificationRepository.findByMemberIdAndMissionIdAndDate(memberId, missionId, today).isPresent(); + private boolean isDuplicatedVerification(final Long memberId, final Long missionId) { + return missionVerificationRepository.findByMemberIdAndMissionIdAndDate(memberId, missionId, LocalDate.now()).isPresent(); } @Transactional diff --git a/src/main/java/com/nexters/goalpanzi/application/mission/dto/request/MissionBoardQuery.java b/src/main/java/com/nexters/goalpanzi/application/mission/dto/request/MissionBoardQuery.java new file mode 100644 index 00000000..e8fda298 --- /dev/null +++ b/src/main/java/com/nexters/goalpanzi/application/mission/dto/request/MissionBoardQuery.java @@ -0,0 +1,6 @@ +package com.nexters.goalpanzi.application.mission.dto.request; + +public record MissionBoardQuery( + Long missionId +) { +} diff --git a/src/main/java/com/nexters/goalpanzi/application/mission/dto/response/MissionBoardMemberResponse.java b/src/main/java/com/nexters/goalpanzi/application/mission/dto/response/MissionBoardMemberResponse.java new file mode 100644 index 00000000..8299a020 --- /dev/null +++ b/src/main/java/com/nexters/goalpanzi/application/mission/dto/response/MissionBoardMemberResponse.java @@ -0,0 +1,12 @@ +package com.nexters.goalpanzi.application.mission.dto.response; + +import com.nexters.goalpanzi.domain.member.CharacterType; +import io.swagger.v3.oas.annotations.media.Schema; + +public record MissionBoardMemberResponse( + @Schema(description = "닉네임", requiredMode = Schema.RequiredMode.REQUIRED) + String nickname, + @Schema(description = "캐릭터 타입", requiredMode = Schema.RequiredMode.REQUIRED) + CharacterType characterType +) { +} diff --git a/src/main/java/com/nexters/goalpanzi/application/mission/dto/response/MissionBoardResponse.java b/src/main/java/com/nexters/goalpanzi/application/mission/dto/response/MissionBoardResponse.java new file mode 100644 index 00000000..ef119ff1 --- /dev/null +++ b/src/main/java/com/nexters/goalpanzi/application/mission/dto/response/MissionBoardResponse.java @@ -0,0 +1,27 @@ +package com.nexters.goalpanzi.application.mission.dto.response; + +import com.nexters.goalpanzi.domain.member.Member; +import com.nexters.goalpanzi.domain.mission.Reward; +import io.swagger.v3.oas.annotations.media.Schema; + +import java.util.List; +import java.util.stream.Collectors; + +public record MissionBoardResponse( + @Schema(description = "보드칸 번호", type = "integer", format = "int32", requiredMode = Schema.RequiredMode.REQUIRED) + Integer number, + @Schema(description = "보드칸 보상", requiredMode = Schema.RequiredMode.REQUIRED) + Reward reward, + List missionBoardMembers +) { + + public static MissionBoardResponse of(final Integer number, final List members) { + return new MissionBoardResponse( + number, + Reward.of(number), + members.stream(). + map(m -> new MissionBoardMemberResponse(m.getNickname(), m.getCharacterType())) + .collect(Collectors.toList()) + ); + } +} diff --git a/src/main/java/com/nexters/goalpanzi/application/mission/dto/response/MissionBoardsResponse.java b/src/main/java/com/nexters/goalpanzi/application/mission/dto/response/MissionBoardsResponse.java new file mode 100644 index 00000000..f71f32d1 --- /dev/null +++ b/src/main/java/com/nexters/goalpanzi/application/mission/dto/response/MissionBoardsResponse.java @@ -0,0 +1,11 @@ +package com.nexters.goalpanzi.application.mission.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; + +import java.util.List; + +public record MissionBoardsResponse( + @Schema(description = "미션 보드칸 정보 리스트", requiredMode = Schema.RequiredMode.REQUIRED) + List missionBoards +) { +} diff --git a/src/main/java/com/nexters/goalpanzi/application/mission/dto/response/MissionVerificationsResponse.java b/src/main/java/com/nexters/goalpanzi/application/mission/dto/response/MissionVerificationsResponse.java new file mode 100644 index 00000000..2ab88d9f --- /dev/null +++ b/src/main/java/com/nexters/goalpanzi/application/mission/dto/response/MissionVerificationsResponse.java @@ -0,0 +1,11 @@ +package com.nexters.goalpanzi.application.mission.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; + +import java.util.List; + +public record MissionVerificationsResponse( + @Schema(description = "미션 인증 정보 리스트", requiredMode = Schema.RequiredMode.REQUIRED) + List missionVerifications +) { +} diff --git a/src/main/java/com/nexters/goalpanzi/domain/mission/Mission.java b/src/main/java/com/nexters/goalpanzi/domain/mission/Mission.java index 2959d514..d9aed216 100644 --- a/src/main/java/com/nexters/goalpanzi/domain/mission/Mission.java +++ b/src/main/java/com/nexters/goalpanzi/domain/mission/Mission.java @@ -1,12 +1,13 @@ package com.nexters.goalpanzi.domain.mission; -import com.nexters.goalpanzi.infrastructure.jpa.DaysOfWeekConverter; import com.nexters.goalpanzi.domain.common.BaseEntity; +import com.nexters.goalpanzi.infrastructure.jpa.DaysOfWeekConverter; import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; import org.hibernate.annotations.SQLRestriction; +import org.joda.time.LocalTime; import java.time.LocalDate; import java.time.LocalDateTime; @@ -108,7 +109,17 @@ private void validateMission() { } } - public boolean isMissionDay(final LocalDate date) { - return this.missionDays.contains(DayOfWeek.valueOf(date.getDayOfWeek().name())); + public boolean isMissionPeriod() { + LocalDate today = LocalDate.now(); + return !today.isBefore(this.missionStartDate.toLocalDate()) && !today.isAfter(missionEndDate.toLocalDate()); + } + + public boolean isMissionDay() { + return this.missionDays.contains(DayOfWeek.valueOf(LocalDate.now().getDayOfWeek().name())); + } + + public boolean isMissionTime() { + String now = LocalTime.now().toString().substring(0, 5); + return now.compareTo(uploadStartTime) >= 0 && now.compareTo(uploadEndTime) <= 0; } } diff --git a/src/main/java/com/nexters/goalpanzi/domain/mission/Reward.java b/src/main/java/com/nexters/goalpanzi/domain/mission/Reward.java new file mode 100644 index 00000000..0eea1e30 --- /dev/null +++ b/src/main/java/com/nexters/goalpanzi/domain/mission/Reward.java @@ -0,0 +1,38 @@ +package com.nexters.goalpanzi.domain.mission; + +import lombok.Getter; + +import java.util.HashMap; +import java.util.Map; + +@Getter +public enum Reward { + ORANGE(1), + CANOLA_FLOWER(3), + DOLHARUBANG(6), + HORSE_RIDING(9), + HALLA_MOUNTAIN(13), + WATERFALL(17), + BLACK_PIG(21), + SUNRISE(25), + GREEN_TEA_FIELD(29), + BEACH(31); + + private final int number; + + private static final Map REWARD_MAP = new HashMap<>(); + + static { + for (Reward item : values()) { + REWARD_MAP.put(item.getNumber(), item); + } + } + + Reward(int number) { + this.number = number; + } + + public static Reward of(final int number) { + return REWARD_MAP.get(number); + } +} diff --git a/src/main/java/com/nexters/goalpanzi/exception/ErrorCode.java b/src/main/java/com/nexters/goalpanzi/exception/ErrorCode.java index f8b3003c..7a5d19a7 100644 --- a/src/main/java/com/nexters/goalpanzi/exception/ErrorCode.java +++ b/src/main/java/com/nexters/goalpanzi/exception/ErrorCode.java @@ -34,7 +34,9 @@ public enum ErrorCode { NOT_FOUND_VERIFICATION("존재하지 않는 미션 인증입니다."), DUPLICATE_VERIFICATION("이미 인증한 미션이므로 더 이상 인증할 수 없습니다."), ALREADY_COMPLETED_MISSION("이미 완료된 미션이므로 더 이상 인증할 수 없습니다."), + NOT_VERIFICATION_PERIOD("인증 기간이 아니므로 인증할 수 없습니다."), NOT_VERIFICATION_DAY("인증 일자가 아니므로 인증할 수 없습니다."), + NOT_VERIFICATION_TIME("인증 시간대가 아니므로 인증할 수 없습니다."), // FILE UPLOAD INVALID_FILE("유효하지 않은 파일입니다."), diff --git a/src/main/java/com/nexters/goalpanzi/presentation/mission/MissionBoardController.java b/src/main/java/com/nexters/goalpanzi/presentation/mission/MissionBoardController.java new file mode 100644 index 00000000..e8f5337e --- /dev/null +++ b/src/main/java/com/nexters/goalpanzi/presentation/mission/MissionBoardController.java @@ -0,0 +1,28 @@ +package com.nexters.goalpanzi.presentation.mission; + +import com.nexters.goalpanzi.application.mission.MissionBoardService; +import com.nexters.goalpanzi.application.mission.dto.request.MissionBoardQuery; +import com.nexters.goalpanzi.application.mission.dto.response.MissionBoardsResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RequiredArgsConstructor +@RequestMapping("/api/missions") +@RestController +public class MissionBoardController implements MissionBoardControllerDocs { + + private final MissionBoardService missionBoardService; + + @GetMapping("/{missionId}/board") + public ResponseEntity getBoard( + @PathVariable(name = "missionId") final Long missionId + ) { + MissionBoardsResponse response = missionBoardService.getBoard(new MissionBoardQuery(missionId)); + + return ResponseEntity.ok(response); + } +} diff --git a/src/main/java/com/nexters/goalpanzi/presentation/mission/MissionBoardControllerDocs.java b/src/main/java/com/nexters/goalpanzi/presentation/mission/MissionBoardControllerDocs.java new file mode 100644 index 00000000..94cf95f3 --- /dev/null +++ b/src/main/java/com/nexters/goalpanzi/presentation/mission/MissionBoardControllerDocs.java @@ -0,0 +1,33 @@ +package com.nexters.goalpanzi.presentation.mission; + +import com.nexters.goalpanzi.application.mission.dto.response.MissionBoardsResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; + +@Tag( + name = "미션 보드", + description = """ + 미션 보드와 관련된 그룹입니다. + + 미션 보드판 정보을 제공합니다. + """ +) +public interface MissionBoardControllerDocs { + @Operation(summary = "미션 보드판 조회", description = "해당 미션의 보드판 현황을 조회합니다.") + @ApiResponses({ + @ApiResponse(responseCode = "200"), + @ApiResponse(responseCode = "401", content = @Content(schema = @Schema(hidden = true))), + }) + @GetMapping("/{missionId}/board") + ResponseEntity getBoard( + @Schema(description = "미션 아이디", type = "integer", format = "int64", requiredMode = Schema.RequiredMode.REQUIRED) + @PathVariable(name = "missionId") final Long missionId + ); +} diff --git a/src/main/java/com/nexters/goalpanzi/presentation/mission/MissionVerificationController.java b/src/main/java/com/nexters/goalpanzi/presentation/mission/MissionVerificationController.java index a136fbdf..f547af6e 100644 --- a/src/main/java/com/nexters/goalpanzi/presentation/mission/MissionVerificationController.java +++ b/src/main/java/com/nexters/goalpanzi/presentation/mission/MissionVerificationController.java @@ -5,6 +5,7 @@ import com.nexters.goalpanzi.application.mission.dto.request.MissionVerificationQuery; import com.nexters.goalpanzi.application.mission.dto.request.MyMissionVerificationQuery; import com.nexters.goalpanzi.application.mission.dto.response.MissionVerificationResponse; +import com.nexters.goalpanzi.application.mission.dto.response.MissionVerificationsResponse; import com.nexters.goalpanzi.common.argumentresolver.LoginMemberId; import lombok.RequiredArgsConstructor; import org.springframework.format.annotation.DateTimeFormat; @@ -13,7 +14,6 @@ import org.springframework.web.multipart.MultipartFile; import java.time.LocalDate; -import java.util.List; @RequiredArgsConstructor @RequestMapping("/api/missions") @@ -23,12 +23,12 @@ public class MissionVerificationController implements MissionVerificationControl private final MissionVerificationService missionVerificationService; @GetMapping("/{missionId}/verifications") - public ResponseEntity> getVerifications( + public ResponseEntity getVerifications( @LoginMemberId final Long memberId, @PathVariable(name = "missionId") final Long missionId, @RequestParam(name = "date", required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) final LocalDate date ) { - List response = missionVerificationService.getVerifications(new MissionVerificationQuery(memberId, missionId, date)); + MissionVerificationsResponse response = missionVerificationService.getVerifications(new MissionVerificationQuery(memberId, missionId, date)); return ResponseEntity.ok(response); } diff --git a/src/main/java/com/nexters/goalpanzi/presentation/mission/MissionVerificationControllerDocs.java b/src/main/java/com/nexters/goalpanzi/presentation/mission/MissionVerificationControllerDocs.java index da8ed7bd..80b4adb2 100644 --- a/src/main/java/com/nexters/goalpanzi/presentation/mission/MissionVerificationControllerDocs.java +++ b/src/main/java/com/nexters/goalpanzi/presentation/mission/MissionVerificationControllerDocs.java @@ -1,6 +1,7 @@ package com.nexters.goalpanzi.presentation.mission; import com.nexters.goalpanzi.application.mission.dto.response.MissionVerificationResponse; +import com.nexters.goalpanzi.application.mission.dto.response.MissionVerificationsResponse; import com.nexters.goalpanzi.common.argumentresolver.LoginMemberId; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -15,7 +16,6 @@ import org.springframework.web.multipart.MultipartFile; import java.time.LocalDate; -import java.util.List; @Tag( name = "미션 인증", @@ -33,7 +33,7 @@ public interface MissionVerificationControllerDocs { @ApiResponse(responseCode = "401", content = @Content(schema = @Schema(hidden = true))), }) @GetMapping("/{missionId}/verifications") - ResponseEntity> getVerifications( + ResponseEntity getVerifications( @Parameter(hidden = true) @LoginMemberId final Long memberId, @Schema(description = "미션 아이디", type = "integer", format = "int64", requiredMode = Schema.RequiredMode.REQUIRED) @PathVariable(name = "missionId") final Long missionId, diff --git a/src/test/java/com/nexters/goalpanzi/acceptance/AcceptanceStep.java b/src/test/java/com/nexters/goalpanzi/acceptance/AcceptanceStep.java index 5981b30d..ccae1f09 100644 --- a/src/test/java/com/nexters/goalpanzi/acceptance/AcceptanceStep.java +++ b/src/test/java/com/nexters/goalpanzi/acceptance/AcceptanceStep.java @@ -3,6 +3,7 @@ import com.nexters.goalpanzi.application.auth.dto.request.GoogleLoginCommand; import com.nexters.goalpanzi.domain.mission.DayOfWeek; import com.nexters.goalpanzi.domain.mission.TimeOfDay; +import com.nexters.goalpanzi.presentation.member.dto.UpdateProfileRequest; import com.nexters.goalpanzi.presentation.mission.dto.CreateMissionRequest; import com.nexters.goalpanzi.presentation.mission.dto.JoinMissionRequest; import io.restassured.RestAssured; @@ -33,6 +34,17 @@ public class AcceptanceStep { .extract(); } + public static ExtractableResponse 프로필_설정(UpdateProfileRequest request, String accessToken) { + return RestAssured.given().log().all() + .contentType(MediaType.APPLICATION_JSON_VALUE) + .header(HttpHeaders.AUTHORIZATION, BEARER + accessToken) + .body(request) + .when().patch("/api/member/profile") + .then().log().all() + .statusCode(HttpStatus.OK.value()) + .extract(); + } + public static ExtractableResponse 미션_생성(CreateMissionRequest request, String accessToken) { return RestAssured.given().log().all() .contentType(MediaType.APPLICATION_JSON_VALUE) @@ -112,4 +124,14 @@ public class AcceptanceStep { .statusCode(HttpStatus.OK.value()) .extract(); } + + public static ExtractableResponse 보드판_조회(Long missionId, String accessToken) { + return RestAssured.given().log().all() + .contentType(MediaType.APPLICATION_JSON_VALUE) + .header(HttpHeaders.AUTHORIZATION, BEARER + accessToken) + .when().get("/api/missions/" + missionId + "/board") + .then().log().all() + .statusCode(HttpStatus.OK.value()) + .extract(); + } } diff --git a/src/test/java/com/nexters/goalpanzi/acceptance/MissionBoardAcceptanceTest.java b/src/test/java/com/nexters/goalpanzi/acceptance/MissionBoardAcceptanceTest.java new file mode 100644 index 00000000..ea2a0e83 --- /dev/null +++ b/src/test/java/com/nexters/goalpanzi/acceptance/MissionBoardAcceptanceTest.java @@ -0,0 +1,63 @@ +package com.nexters.goalpanzi.acceptance; + +import com.nexters.goalpanzi.application.auth.dto.request.GoogleLoginCommand; +import com.nexters.goalpanzi.application.auth.dto.response.LoginResponse; +import com.nexters.goalpanzi.application.mission.dto.response.MissionBoardsResponse; +import com.nexters.goalpanzi.application.mission.dto.response.MissionDetailResponse; +import com.nexters.goalpanzi.application.upload.ObjectStorageClient; +import com.nexters.goalpanzi.domain.mission.TimeOfDay; +import com.nexters.goalpanzi.presentation.member.dto.UpdateProfileRequest; +import com.nexters.goalpanzi.presentation.mission.dto.CreateMissionRequest; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.web.multipart.MultipartFile; + +import java.time.LocalDateTime; + +import static com.nexters.goalpanzi.acceptance.AcceptanceStep.*; +import static com.nexters.goalpanzi.fixture.MemberFixture.*; +import static com.nexters.goalpanzi.fixture.MissionFixture.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +public class MissionBoardAcceptanceTest extends AcceptanceTest { + + @MockBean + private ObjectStorageClient objectStorageClient; + + @Test + void 보드판_정보를_조회한다() { + when(objectStorageClient.uploadFile(any(MultipartFile.class))).thenReturn(UPLOADED_IMAGE_URL); + + LoginResponse hostLogin = 구글_로그인(new GoogleLoginCommand(EMAIL_HOST)).as(LoginResponse.class); + CreateMissionRequest missionRequest = new CreateMissionRequest(DESCRIPTION, LocalDateTime.now(), LocalDateTime.now().plusDays(1), TimeOfDay.EVERYDAY, WEEK, 1); + 프로필_설정(new UpdateProfileRequest(NICKNAME_HOST, CHARACTER_HOST), hostLogin.accessToken()); + MissionDetailResponse mission = 미션_생성(missionRequest, hostLogin.accessToken()).as(MissionDetailResponse.class); + 미션_인증(IMAGE_FILE, mission.missionId(), hostLogin.accessToken()); + + LoginResponse memberALogin = 구글_로그인(new GoogleLoginCommand(EMAIL_MEMBER_A)).as(LoginResponse.class); + 프로필_설정(new UpdateProfileRequest(NICKNAME_MEMBER_A, CHARACTER_MEMBER_A), memberALogin.accessToken()); + 미션_참여(mission.invitationCode(), memberALogin.accessToken()); + 미션_인증(IMAGE_FILE, mission.missionId(), memberALogin.accessToken()); + + LoginResponse memberBLogin = 구글_로그인(new GoogleLoginCommand(EMAIL_MEMBER_B)).as(LoginResponse.class); + 프로필_설정(new UpdateProfileRequest(NICKNAME_MEMBER_B, CHARACTER_MEMBER_B), memberBLogin.accessToken()); + 미션_참여(mission.invitationCode(), memberBLogin.accessToken()); + + MissionBoardsResponse boards = 보드판_조회(mission.missionId(), hostLogin.accessToken()).as(MissionBoardsResponse.class); + + assertAll( + () -> assertThat(boards.missionBoards().size()).isEqualTo(mission.boardCount() + 1), + () -> assertThat(boards.missionBoards().get(0).reward()).isNull(), + () -> assertThat(boards.missionBoards().get(0).number()).isEqualTo(0), + () -> assertThat(boards.missionBoards().get(0).missionBoardMembers().size()).isEqualTo(1), + () -> assertThat(boards.missionBoards().get(1).reward()).isNotNull(), + () -> assertThat(boards.missionBoards().get(1).number()).isEqualTo(1), + () -> assertThat(boards.missionBoards().get(1).missionBoardMembers().size()).isEqualTo(2), + () -> assertThat(boards.missionBoards().get(1).missionBoardMembers().get(0).nickname()).isEqualTo(NICKNAME_HOST), + () -> assertThat(boards.missionBoards().get(1).missionBoardMembers().get(1).nickname()).isEqualTo(NICKNAME_MEMBER_A) + ); + } +} diff --git a/src/test/java/com/nexters/goalpanzi/acceptance/MissionVerificationAcceptanceTest.java b/src/test/java/com/nexters/goalpanzi/acceptance/MissionVerificationAcceptanceTest.java index 2967031d..bbf0ad33 100644 --- a/src/test/java/com/nexters/goalpanzi/acceptance/MissionVerificationAcceptanceTest.java +++ b/src/test/java/com/nexters/goalpanzi/acceptance/MissionVerificationAcceptanceTest.java @@ -4,10 +4,12 @@ import com.nexters.goalpanzi.application.auth.dto.response.LoginResponse; import com.nexters.goalpanzi.application.mission.dto.response.MissionDetailResponse; import com.nexters.goalpanzi.application.mission.dto.response.MissionVerificationResponse; +import com.nexters.goalpanzi.application.mission.dto.response.MissionVerificationsResponse; import com.nexters.goalpanzi.application.upload.ObjectStorageClient; import com.nexters.goalpanzi.domain.mission.DayOfWeek; import com.nexters.goalpanzi.domain.mission.TimeOfDay; import com.nexters.goalpanzi.exception.ErrorCode; +import com.nexters.goalpanzi.presentation.member.dto.UpdateProfileRequest; import com.nexters.goalpanzi.presentation.mission.dto.CreateMissionRequest; import io.restassured.response.ExtractableResponse; import io.restassured.response.Response; @@ -20,7 +22,7 @@ import java.util.List; import static com.nexters.goalpanzi.acceptance.AcceptanceStep.*; -import static com.nexters.goalpanzi.fixture.MemberFixture.EMAIL_HOST; +import static com.nexters.goalpanzi.fixture.MemberFixture.*; import static com.nexters.goalpanzi.fixture.MissionFixture.*; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; @@ -45,6 +47,23 @@ public class MissionVerificationAcceptanceTest extends AcceptanceTest { assertThat(response.statusCode()).isEqualTo(200); } + + @Test + void 미션_기간이_아니므로_인증에_실패한다() { + when(objectStorageClient.uploadFile(any(MultipartFile.class))).thenReturn(UPLOADED_IMAGE_URL); + + LoginResponse login = 구글_로그인(new GoogleLoginCommand(EMAIL_HOST)).as(LoginResponse.class); + CreateMissionRequest missionRequest = new CreateMissionRequest(DESCRIPTION, LocalDateTime.now().plusDays(1), LocalDateTime.now().plusDays(2), TimeOfDay.EVERYDAY, WEEK, 1); + MissionDetailResponse mission = 미션_생성(missionRequest, login.accessToken()).as(MissionDetailResponse.class); + + ExtractableResponse response = 미션_인증(IMAGE_FILE, mission.missionId(), login.accessToken()); + + assertAll( + () -> assertThat(response.statusCode()).isEqualTo(400), + () -> assertThat(response.jsonPath().getString("message")).isEqualTo(ErrorCode.NOT_VERIFICATION_PERIOD.getMessage()) + ); + } + @Test void 지정한_인증_일자가_아니므로_인증에_실패한다() { when(objectStorageClient.uploadFile(any(MultipartFile.class))).thenReturn(UPLOADED_IMAGE_URL); @@ -99,34 +118,46 @@ public class MissionVerificationAcceptanceTest extends AcceptanceTest { ); } -// TODO 프로필 생성 후 확인 필요 -// @Test -// void 특정_일자의_미션_인증_현황을_조회한다() { -// when(objectStorageClient.uploadFile(any(MultipartFile.class))).thenReturn(UPLOADED_IMAGE_URL); -// -// LoginResponse login1 = 구글_로그인(new GoogleLoginCommand(EMAIL_HOST)).as(LoginResponse.class); -// -// CreateMissionRequest missionRequest = new CreateMissionRequest(DESCRIPTION, LocalDateTime.now(), LocalDateTime.now().plusDays(1), TimeOfDay.EVERYDAY, WEEK, 1); -// MissionDetailResponse mission = 미션_생성(missionRequest, login1.accessToken()).as(MissionDetailResponse.class); -// 미션_참여(mission.invitationCode(), login1.accessToken()); -// 미션_인증(IMAGE_FILE, mission.missionId(), login1.accessToken()); -// -// LoginResponse login2 = 구글_로그인(new GoogleLoginCommand(EMAIL_HOST2)).as(LoginResponse.class); -// 미션_참여(mission.invitationCode(), login2.accessToken()); -// 미션_인증(IMAGE_FILE, mission.missionId(), login2.accessToken()); -// -// List verifications = 일자별_미션_인증_조회(mission.missionId(), LocalDate.now(), login1.accessToken()).as(List.class); -// -// assertAll( -// () -> assertThat(verifications.size()).isEqualTo(2) -// ); -// } + @Test + void 특정_일자의_미션_인증_현황을_조회한다() { + when(objectStorageClient.uploadFile(any(MultipartFile.class))).thenReturn(UPLOADED_IMAGE_URL); + + LoginResponse hostLogin = 구글_로그인(new GoogleLoginCommand(EMAIL_HOST)).as(LoginResponse.class); + CreateMissionRequest missionRequest = new CreateMissionRequest(DESCRIPTION, LocalDateTime.now(), LocalDateTime.now().plusDays(1), TimeOfDay.EVERYDAY, WEEK, 1); + 프로필_설정(new UpdateProfileRequest(NICKNAME_HOST, CHARACTER_HOST), hostLogin.accessToken()); + MissionDetailResponse mission = 미션_생성(missionRequest, hostLogin.accessToken()).as(MissionDetailResponse.class); + 미션_인증(IMAGE_FILE, mission.missionId(), hostLogin.accessToken()); + + LoginResponse memberALogin = 구글_로그인(new GoogleLoginCommand(EMAIL_MEMBER_A)).as(LoginResponse.class); + 프로필_설정(new UpdateProfileRequest(NICKNAME_MEMBER_A, CHARACTER_MEMBER_A), memberALogin.accessToken()); + 미션_참여(mission.invitationCode(), memberALogin.accessToken()); + 미션_인증(IMAGE_FILE, mission.missionId(), memberALogin.accessToken()); + + LoginResponse memberBLogin = 구글_로그인(new GoogleLoginCommand(EMAIL_MEMBER_B)).as(LoginResponse.class); + 프로필_설정(new UpdateProfileRequest(NICKNAME_MEMBER_B, CHARACTER_MEMBER_B), memberBLogin.accessToken()); + 미션_참여(mission.invitationCode(), memberBLogin.accessToken()); + 미션_인증(IMAGE_FILE, mission.missionId(), memberBLogin.accessToken()); + + MissionVerificationsResponse verifications = 일자별_미션_인증_조회(mission.missionId(), LocalDate.now(), hostLogin.accessToken()).as(MissionVerificationsResponse.class); + + assertAll( + () -> assertThat(verifications.missionVerifications().size()).isEqualTo(3), + () -> assertThat(verifications.missionVerifications().get(0).nickname()).isEqualTo(NICKNAME_HOST), + () -> assertThat(verifications.missionVerifications().get(0).characterType()).isEqualTo(CHARACTER_HOST), + () -> assertThat(verifications.missionVerifications().get(1).nickname()).isEqualTo(NICKNAME_MEMBER_B), + () -> assertThat(verifications.missionVerifications().get(1).characterType()).isEqualTo(CHARACTER_MEMBER_B), + () -> assertThat(verifications.missionVerifications().get(2).nickname()).isEqualTo(NICKNAME_MEMBER_A), + () -> assertThat(verifications.missionVerifications().get(2).characterType()).isEqualTo(CHARACTER_MEMBER_A) + ); + } @Test void 보드칸_번호에_해당하는_나의_미션_인증_내역을_조회한다() { when(objectStorageClient.uploadFile(any(MultipartFile.class))).thenReturn(UPLOADED_IMAGE_URL); LoginResponse login = 구글_로그인(new GoogleLoginCommand(EMAIL_HOST)).as(LoginResponse.class); + 프로필_설정(new UpdateProfileRequest(NICKNAME_HOST, CHARACTER_HOST), login.accessToken()); + CreateMissionRequest missionRequest = new CreateMissionRequest(DESCRIPTION, LocalDateTime.now(), LocalDateTime.now().plusDays(1), TimeOfDay.EVERYDAY, WEEK, 1); MissionDetailResponse mission = 미션_생성(missionRequest, login.accessToken()).as(MissionDetailResponse.class); 미션_인증(IMAGE_FILE, mission.missionId(), login.accessToken()); @@ -134,7 +165,8 @@ public class MissionVerificationAcceptanceTest extends AcceptanceTest { MissionVerificationResponse verification = 내_미션_인증_조회(1, mission.missionId(), login.accessToken()).as(MissionVerificationResponse.class); assertAll( - // TODO 추후 닉네임, 장기말 타입 검증도 추가 + () -> assertThat(verification.nickname()).isEqualTo(NICKNAME_HOST), + () -> assertThat(verification.characterType()).isEqualTo(CHARACTER_HOST), () -> assertThat(verification.imageUrl()).isEqualTo(UPLOADED_IMAGE_URL) ); } diff --git a/src/test/java/com/nexters/goalpanzi/domain/member/MemberTest.java b/src/test/java/com/nexters/goalpanzi/domain/member/MemberTest.java index 08e37226..2713c0d2 100644 --- a/src/test/java/com/nexters/goalpanzi/domain/member/MemberTest.java +++ b/src/test/java/com/nexters/goalpanzi/domain/member/MemberTest.java @@ -11,11 +11,11 @@ class MemberTest { @Test void 프로필_생성이_가능하다() { Member member = Member.socialLogin(SOCIAL_ID, EMAIL_HOST, SocialType.APPLE); - member.updateProfile(NICKNAME, CharacterType.CAT); + member.updateProfile(NICKNAME_HOST, CharacterType.CAT); assertAll( () -> assertThat(member.isProfileSet()).isTrue(), - () -> assertThat(member.getNickname()).isEqualTo(NICKNAME), + () -> assertThat(member.getNickname()).isEqualTo(NICKNAME_HOST), () -> assertThat(member.getCharacterType()).isEqualTo(CharacterType.CAT) ); } @@ -23,12 +23,12 @@ class MemberTest { @Test void 프로필_생성후_변경이_가능하다() { Member member = Member.socialLogin(SOCIAL_ID, EMAIL_HOST, SocialType.APPLE); - member.updateProfile(NICKNAME, CharacterType.CAT); - member.updateProfile(NICKNAME,null); + member.updateProfile(NICKNAME_HOST, CharacterType.CAT); + member.updateProfile(NICKNAME_HOST, null); assertAll( () -> assertThat(member.isProfileSet()).isTrue(), - () -> assertThat(member.getNickname()).isEqualTo(NICKNAME), + () -> assertThat(member.getNickname()).isEqualTo(NICKNAME_HOST), () -> assertThat(member.getCharacterType()).isEqualTo(CharacterType.CAT) ); } @@ -36,11 +36,11 @@ class MemberTest { @Test void 닉네임만_설정이_가능하다() { Member member = Member.socialLogin(SOCIAL_ID, EMAIL_HOST, SocialType.APPLE); - member.updateProfile(NICKNAME, null); + member.updateProfile(NICKNAME_HOST, null); assertAll( () -> assertThat(member.isProfileSet()).isFalse(), - () -> assertThat(member.getNickname()).isEqualTo(NICKNAME) + () -> assertThat(member.getNickname()).isEqualTo(NICKNAME_HOST) ); } diff --git a/src/test/java/com/nexters/goalpanzi/fixture/MemberFixture.java b/src/test/java/com/nexters/goalpanzi/fixture/MemberFixture.java index f8b251de..cf99edfb 100644 --- a/src/test/java/com/nexters/goalpanzi/fixture/MemberFixture.java +++ b/src/test/java/com/nexters/goalpanzi/fixture/MemberFixture.java @@ -1,11 +1,19 @@ package com.nexters.goalpanzi.fixture; +import com.nexters.goalpanzi.domain.member.CharacterType; + public class MemberFixture { public static final Long MEMBER_ID = 1L; public static final String ID_TOKEN_HOST = "token_host"; public static final String ID_TOKEN_MEMBER_A = "token_member_A"; public static final String EMAIL_HOST = "host@gmail.com"; public static final String EMAIL_MEMBER_A = "a@gmail.com"; + public static final String EMAIL_MEMBER_B = "b@gmail.com"; public static final String SOCIAL_ID = "12345"; - public static final String NICKNAME = "song2"; + public static final String NICKNAME_HOST = "goalpanzi"; + public static final String NICKNAME_MEMBER_A = "song2"; + public static final String NICKNAME_MEMBER_B = "kimyu0218"; + public static final CharacterType CHARACTER_HOST = CharacterType.BEAR; + public static final CharacterType CHARACTER_MEMBER_A = CharacterType.BIRD; + public static final CharacterType CHARACTER_MEMBER_B = CharacterType.CAT; }