Skip to content

Commit

Permalink
feat: 보드판 정보 API 구현 (#36)
Browse files Browse the repository at this point in the history
* feat: 보드판 정보 조회 구현

* test: 보드판 정보 테스트 작성

* fix: 미션 인증에서 누락된 유효성 검사 추가

* feat: 보드판 이벤트 enum 정의

* rename: fixture 추가에 따른 이름 수정

* rename: enum 이름 수정

* refactor: dto로 한 겹 감싸기

* feat: 보드쪽 dto 작성

* rename: enum 이름 변경

* refactor: dto 및 enum 변경
  • Loading branch information
kimyu0218 authored Aug 9, 2024
1 parent ae5d3a9 commit 6d69468
Show file tree
Hide file tree
Showing 19 changed files with 428 additions and 51 deletions.
Original file line number Diff line number Diff line change
@@ -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<Integer, List<Member>> groupByVerificationCount(final Mission mission) {
Map<Integer, List<Member>> board = initializeBoard(mission.getBoardCount());

missionMemberRepository.findAllByMissionId(mission.getId())
.forEach(m -> board.get(m.getVerificationCount()).add(m.getMember()));
return board;
}

private Map<Integer, List<Member>> sortByVerifiedAt(final Map<Integer, List<Member>> 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<MissionBoardResponse> convertToBoardResponse(final Map<Integer, List<Member>> groupedAndSortedMembers) {
return groupedAndSortedMembers.entrySet().stream()
.map(entry -> MissionBoardResponse.of(entry.getKey(), entry.getValue()))
.collect(Collectors.toList());
}

private Map<Integer, List<Member>> initializeBoard(final Integer boardCount) {
return IntStream.range(0, boardCount + 1)
.boxed()
.collect(Collectors.toMap(i -> i, i -> new ArrayList<>()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -37,19 +38,20 @@ public class MissionVerificationService {
private final ObjectStorageClient objectStorageClient;

@Transactional(readOnly = true)
public List<MissionVerificationResponse> 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<MissionVerification> verifications = missionVerificationRepository.findAllByMissionIdAndDate(query.missionId(), date);

Map<Long, MissionVerification> 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) {
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.nexters.goalpanzi.application.mission.dto.request;

public record MissionBoardQuery(
Long missionId
) {
}
Original file line number Diff line number Diff line change
@@ -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
) {
}
Original file line number Diff line number Diff line change
@@ -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<MissionBoardMemberResponse> missionBoardMembers
) {

public static MissionBoardResponse of(final Integer number, final List<Member> members) {
return new MissionBoardResponse(
number,
Reward.of(number),
members.stream().
map(m -> new MissionBoardMemberResponse(m.getNickname(), m.getCharacterType()))
.collect(Collectors.toList())
);
}
}
Original file line number Diff line number Diff line change
@@ -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<MissionBoardResponse> missionBoards
) {
}
Original file line number Diff line number Diff line change
@@ -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<MissionVerificationResponse> missionVerifications
) {
}
17 changes: 14 additions & 3 deletions src/main/java/com/nexters/goalpanzi/domain/mission/Mission.java
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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;
}
}
38 changes: 38 additions & 0 deletions src/main/java/com/nexters/goalpanzi/domain/mission/Reward.java
Original file line number Diff line number Diff line change
@@ -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<Integer, Reward> 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);
}
}
2 changes: 2 additions & 0 deletions src/main/java/com/nexters/goalpanzi/exception/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -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("유효하지 않은 파일입니다."),
Expand Down
Original file line number Diff line number Diff line change
@@ -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<MissionBoardsResponse> getBoard(
@PathVariable(name = "missionId") final Long missionId
) {
MissionBoardsResponse response = missionBoardService.getBoard(new MissionBoardQuery(missionId));

return ResponseEntity.ok(response);
}
}
Original file line number Diff line number Diff line change
@@ -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<MissionBoardsResponse> getBoard(
@Schema(description = "미션 아이디", type = "integer", format = "int64", requiredMode = Schema.RequiredMode.REQUIRED)
@PathVariable(name = "missionId") final Long missionId
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -13,7 +14,6 @@
import org.springframework.web.multipart.MultipartFile;

import java.time.LocalDate;
import java.util.List;

@RequiredArgsConstructor
@RequestMapping("/api/missions")
Expand All @@ -23,12 +23,12 @@ public class MissionVerificationController implements MissionVerificationControl
private final MissionVerificationService missionVerificationService;

@GetMapping("/{missionId}/verifications")
public ResponseEntity<List<MissionVerificationResponse>> getVerifications(
public ResponseEntity<MissionVerificationsResponse> 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<MissionVerificationResponse> response = missionVerificationService.getVerifications(new MissionVerificationQuery(memberId, missionId, date));
MissionVerificationsResponse response = missionVerificationService.getVerifications(new MissionVerificationQuery(memberId, missionId, date));

return ResponseEntity.ok(response);
}
Expand Down
Loading

0 comments on commit 6d69468

Please sign in to comment.