diff --git a/src/main/java/com/dnd/runus/application/running/RunningRecordService.java b/src/main/java/com/dnd/runus/application/running/RunningRecordService.java index d43b9537..d72a3094 100644 --- a/src/main/java/com/dnd/runus/application/running/RunningRecordService.java +++ b/src/main/java/com/dnd/runus/application/running/RunningRecordService.java @@ -3,7 +3,9 @@ import com.dnd.runus.application.running.dto.RunningResultDto; import com.dnd.runus.application.running.event.RunningRecordAddedEvent; import com.dnd.runus.domain.challenge.Challenge; +import com.dnd.runus.domain.challenge.ChallengeCondition; import com.dnd.runus.domain.challenge.ChallengeRepository; +import com.dnd.runus.domain.challenge.ChallengeType; import com.dnd.runus.domain.challenge.ChallengeWithCondition; import com.dnd.runus.domain.challenge.GoalMetricType; import com.dnd.runus.domain.challenge.achievement.ChallengeAchievement; @@ -19,13 +21,15 @@ import com.dnd.runus.domain.running.DailyRunningRecordSummary; import com.dnd.runus.domain.running.RunningRecord; import com.dnd.runus.domain.running.RunningRecordRepository; +import com.dnd.runus.global.exception.BusinessException; import com.dnd.runus.global.exception.NotFoundException; +import com.dnd.runus.global.exception.type.ErrorType; import com.dnd.runus.presentation.v1.running.dto.WeeklyRunningRatingDto; +import com.dnd.runus.presentation.v1.running.dto.request.RunningAchievementMode; import com.dnd.runus.presentation.v1.running.dto.request.RunningRecordRequestV1; import com.dnd.runus.presentation.v1.running.dto.request.RunningRecordWeeklySummaryType; import com.dnd.runus.presentation.v1.running.dto.response.*; import com.dnd.runus.presentation.v2.running.dto.request.RunningRecordRequestV2; -import com.dnd.runus.presentation.v2.running.dto.request.RunningRecordRequestV2.ChallengeAchievedDto; import com.dnd.runus.presentation.v2.running.dto.request.RunningRecordRequestV2.GoalAchievedDto; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationEventPublisher; @@ -79,7 +83,7 @@ public RunningRecordService( } @Transactional(readOnly = true) - public RunningRecordQueryResponse getRunningRecord(long memberId, long runningRecordId) { + public RunningResultDto getRunningRecord(long memberId, long runningRecordId) { RunningRecord runningRecord = runningRecordRepository .findById(runningRecordId) .filter(r -> r.member().memberId() == memberId) @@ -97,7 +101,27 @@ public RunningRecordQueryResponse getRunningRecord(long memberId, long runningRe .orElse(null) : null; - return RunningRecordQueryResponse.of(runningRecord, challengeAchievement, goalAchievement); + RunningAchievementMode runningAchievementMode = (challengeAchievement != null) + ? RunningAchievementMode.CHALLENGE + : (goalAchievement != null) ? RunningAchievementMode.GOAL : RunningAchievementMode.NORMAL; + + switch (runningAchievementMode) { + case CHALLENGE -> { + return RunningResultDto.of( + runningRecord, + challengeAchievement, + calChallengeAchievementPercentage(memberId, challengeAchievement)); + } + case GOAL -> { + int achievedGoalValue = goalAchievement.goalMetricType().getActualValue(runningRecord); + return RunningResultDto.of( + runningRecord, + goalAchievement, + calPercentage(achievedGoalValue, goalAchievement.achievementValue())); + } + } + + return RunningResultDto.from(runningRecord); } @Transactional(readOnly = true) @@ -223,16 +247,22 @@ public RunningResultDto addRunningRecordV2(long memberId, RunningRecordRequestV2 switch (request.achievementMode()) { case CHALLENGE -> { - ChallengeAchievedDto challengeAchievedForAdd = request.challengeValues(); Challenge challenge = challengeRepository - .findById(challengeAchievedForAdd.challengeId()) - .orElseThrow( - () -> new NotFoundException(Challenge.class, challengeAchievedForAdd.challengeId())); + .findById(request.challengeValues().challengeId()) + .orElseThrow(() -> new NotFoundException( + Challenge.class, request.challengeValues().challengeId())); + if (!challenge.isActive()) { + throw new BusinessException(ErrorType.CHALLENGE_NOT_ACTIVE); + } - ChallengeAchievement challengeAchievement = challengeAchievementRepository.save( - new ChallengeAchievement(challenge, record, challengeAchievedForAdd.isSuccess())); + ChallengeAchievement challengeAchievement = + challengeAchievementRepository.save(new ChallengeAchievement( + challenge, record, request.challengeValues().isSuccess())); - return RunningResultDto.of(record, challengeAchievement); + return RunningResultDto.of( + record, + challengeAchievement, + calChallengeAchievementPercentage(memberId, challengeAchievement)); } case GOAL -> { GoalAchievedDto goalAchievedForAdd = request.goalValues(); @@ -244,7 +274,10 @@ public RunningResultDto addRunningRecordV2(long memberId, RunningRecordRequestV2 : goalAchievedForAdd.goalTime(), goalAchievedForAdd.isSuccess())); - return RunningResultDto.of(record, goalAchievement); + int achievedGoalValue = goalAchievement.goalMetricType().getActualValue(record); + + return RunningResultDto.of( + record, goalAchievement, calPercentage(achievedGoalValue, goalAchievement.achievementValue())); } } return RunningResultDto.from(record); @@ -337,4 +370,54 @@ private GoalAchievement handleGoalMode(RunningRecord runningRecord, Integer goal GoalAchievement goalAchievement = new GoalAchievement(runningRecord, goalMetricType, goalValue, isAchieved); return goalAchievementRepository.save(goalAchievement); } + + /** + * 챌린지 퍼센테이지 계산(V2 이후에서 사용) + * 어제 러닝 기록 이기기 관련 챌린지면 챌린지 목표값 재등록(기본 목표 값 + 어제 러닝 기록)한 후 계산 + * @return Double(퍼센테이지 계산하기 힘든, 2 이전에 저장된 챌린지같은 경우 null로 리턴) + */ + private Double calChallengeAchievementPercentage(long memberId, ChallengeAchievement challengeAchievement) { + + Challenge challenge = challengeAchievement.challenge(); + ChallengeWithCondition challengeWithCondition = challengeRepository + .findChallengeWithConditionsByChallengeId(challenge.challengeId()) + .orElseThrow(() -> new NotFoundException(Challenge.class, challenge.challengeId())); + + // DISTANCE_IN_TIME, PACE 챌린지인지 확인 + // (v2이전에 퍼센테이지를 표현할 수 없는 애들일 경우, 퍼센테이지 null로 리턴함) + if (challenge.challengeType() == ChallengeType.DISTANCE_IN_TIME + || challengeWithCondition.conditions().stream() + .anyMatch(condition -> !condition.goalMetricType().hasPercentage())) { + return null; + } + + ChallengeCondition condition = challengeWithCondition.conditions().getFirst(); + RunningRecord runningRecord = challengeAchievement.runningRecord(); + + if (challenge.isDefeatYesterdayChallenge()) { + // 어제 러닝 기록 이기기 관련 챌린지면, 챌린지 목표값 재등록(어제 기록 + 목표값) + OffsetDateTime runningDate = runningRecord + .startAt() + .toLocalDate() + .atStartOfDay(runningRecord.startAt().getZone()) + .toOffsetDateTime(); + + RunningRecord preRunningRecord = + runningRecordRepository + .findByMemberIdAndStartAtBetween(memberId, runningDate.minusDays(1), runningDate) + .stream() + .findFirst() + .orElseThrow(() -> new NotFoundException("이전 러닝 기록을 가져올 수 없습니다.")); + condition.registerComparisonValue(condition.goalMetricType().getActualValue(preRunningRecord)); + } + + int achievedValue = condition.goalMetricType().getActualValue(runningRecord); + return calPercentage(achievedValue, condition.comparisonValue()); + } + + private double calPercentage(double part, double total) { + if (total <= 0) return 0; + double percentage = part / total; + return percentage > 1 ? 1 : percentage; + } } diff --git a/src/main/java/com/dnd/runus/application/running/dto/RunningResultDto.java b/src/main/java/com/dnd/runus/application/running/dto/RunningResultDto.java index d410507b..c66c66b4 100644 --- a/src/main/java/com/dnd/runus/application/running/dto/RunningResultDto.java +++ b/src/main/java/com/dnd/runus/application/running/dto/RunningResultDto.java @@ -9,33 +9,38 @@ public record RunningResultDto( RunningRecord runningRecord, com.dnd.runus.presentation.v1.running.dto.request.RunningAchievementMode runningAchievementMode, ChallengeAchievement challengeAchievement, - GoalAchievement goalAchievement + GoalAchievement goalAchievement, + Double percentage ) { public static RunningResultDto from(RunningRecord runningRecord) { return new RunningResultDto( runningRecord, com.dnd.runus.presentation.v1.running.dto.request.RunningAchievementMode.NORMAL, null, + null, null ); } public static RunningResultDto of(RunningRecord runningRecord, - ChallengeAchievement challengeAchievement) { + ChallengeAchievement challengeAchievement, Double percentage) { return new RunningResultDto( runningRecord, com.dnd.runus.presentation.v1.running.dto.request.RunningAchievementMode.CHALLENGE, challengeAchievement, - null + null, + percentage ); } - public static RunningResultDto of(RunningRecord runningRecord, GoalAchievement goalAchievement) { + public static RunningResultDto of(RunningRecord runningRecord, + GoalAchievement goalAchievement, Double percentage) { return new RunningResultDto( runningRecord, com.dnd.runus.presentation.v1.running.dto.request.RunningAchievementMode.GOAL, null, - goalAchievement + goalAchievement, + percentage ); } } diff --git a/src/main/java/com/dnd/runus/global/exception/type/ErrorType.java b/src/main/java/com/dnd/runus/global/exception/type/ErrorType.java index 310ebf4b..d896949a 100644 --- a/src/main/java/com/dnd/runus/global/exception/type/ErrorType.java +++ b/src/main/java/com/dnd/runus/global/exception/type/ErrorType.java @@ -50,6 +50,9 @@ public enum ErrorType { GOAL_VALUES_REQUIRED_IN_GOAL_MODE(BAD_REQUEST, DEBUG, "RUNNING_005", "개인 목표 모드에서, 개인 목표 달성값은 필수 잆니다."), CHALLENGE_VALUES_REQUIRED_IN_CHALLENGE_MODE(BAD_REQUEST, DEBUG, "RUNNING_006", "챌린지 모드에서, 챌린지 달성값은 필수 입니다."), + // ChallengeErrorType + CHALLENGE_NOT_ACTIVE(CONFLICT, DEBUG, "CHALLENGE_001", "해당 챌린지는 현재 활성화된 챌린지가 아닙니다."), + // WeatherErrorType WEATHER_API_ERROR(SERVICE_UNAVAILABLE, WARN, "WEATHER_001", "날씨 API 호출 중 오류가 발생했습니다"), ; diff --git a/src/main/java/com/dnd/runus/presentation/v1/running/RunningRecordController.java b/src/main/java/com/dnd/runus/presentation/v1/running/RunningRecordController.java index a323050f..87a2cc87 100644 --- a/src/main/java/com/dnd/runus/presentation/v1/running/RunningRecordController.java +++ b/src/main/java/com/dnd/runus/presentation/v1/running/RunningRecordController.java @@ -31,7 +31,7 @@ public class RunningRecordController { @GetMapping("/{runningRecordId}") @Operation(summary = "러닝 기록 상세 조회", description = "RunngingRecord id로 러닝 상세 기록을 조회합니다.") public RunningRecordQueryResponse getRunningRecord(@MemberId long memberId, @PathVariable long runningRecordId) { - return runningRecordService.getRunningRecord(memberId, runningRecordId); + return RunningRecordQueryResponse.from(runningRecordService.getRunningRecord(memberId, runningRecordId)); } @GetMapping("monthly-dates") diff --git a/src/main/java/com/dnd/runus/presentation/v1/running/dto/ChallengeDto.java b/src/main/java/com/dnd/runus/presentation/v1/running/dto/ChallengeDto.java index 055bd9a7..ccea6cd8 100644 --- a/src/main/java/com/dnd/runus/presentation/v1/running/dto/ChallengeDto.java +++ b/src/main/java/com/dnd/runus/presentation/v1/running/dto/ChallengeDto.java @@ -1,21 +1,31 @@ package com.dnd.runus.presentation.v1.running.dto; +import com.dnd.runus.domain.challenge.achievement.ChallengeAchievement; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; public record ChallengeDto( - @Schema(description = "챌린지 ID") - long challengeId, - @NotBlank - @Schema(description = "챌린지 이름", example = "오늘 30분 동안 뛰기") - String title, - @NotBlank - @Schema(description = "챌린지 결과 문구", example = "성공했어요!") - String subTitle, - @NotBlank - @Schema(description = "챌린지 아이콘 이미지 URL") - String iconUrl, - @Schema(description = "챌린지 성공 여부") - boolean isSuccess + @Schema(description = "챌린지 ID") + long challengeId, + @NotBlank + @Schema(description = "챌린지 이름", example = "오늘 30분 동안 뛰기") + String title, + @NotBlank + @Schema(description = "챌린지 결과 문구", example = "성공했어요!") + String subTitle, + @NotBlank + @Schema(description = "챌린지 아이콘 이미지 URL") + String iconUrl, + @Schema(description = "챌린지 성공 여부") + boolean isSuccess ) { + public static ChallengeDto from(ChallengeAchievement achievement) { + return new ChallengeDto( + achievement.challenge().challengeId(), + achievement.challenge().name(), + achievement.description(), + achievement.challenge().imageUrl(), + achievement.isSuccess() + ); + } } diff --git a/src/main/java/com/dnd/runus/presentation/v1/running/dto/GoalResultDto.java b/src/main/java/com/dnd/runus/presentation/v1/running/dto/GoalResultDto.java index a72a052a..6d42a5da 100644 --- a/src/main/java/com/dnd/runus/presentation/v1/running/dto/GoalResultDto.java +++ b/src/main/java/com/dnd/runus/presentation/v1/running/dto/GoalResultDto.java @@ -1,17 +1,26 @@ package com.dnd.runus.presentation.v1.running.dto; +import com.dnd.runus.domain.goalAchievement.GoalAchievement; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; public record GoalResultDto( - @Schema(description = "설정된 목표 제목", example = "2.5km 달성") - String title, - @Schema(description = "설정된 목표 결과 문구", example = "성공했어요!") - String subTitle, - @NotBlank - @Schema(description = "챌린지 아이콘 이미지 URL") - String iconUrl, - @Schema(description = "설정된 목표 성공 여부") - boolean isSuccess + @Schema(description = "설정된 목표 제목", example = "2.5km 달성") + String title, + @Schema(description = "설정된 목표 결과 문구", example = "성공했어요!") + String subTitle, + @NotBlank + @Schema(description = "챌린지 아이콘 이미지 URL") + String iconUrl, + @Schema(description = "설정된 목표 성공 여부") + boolean isSuccess ) { + public static GoalResultDto from(GoalAchievement achievement) { + return new GoalResultDto( + achievement.getTitle(), + achievement.getDescription(), + achievement.getIconUrl(), + achievement.isAchieved() + ); + } } diff --git a/src/main/java/com/dnd/runus/presentation/v1/running/dto/response/RunningRecordQueryResponse.java b/src/main/java/com/dnd/runus/presentation/v1/running/dto/response/RunningRecordQueryResponse.java index dbef30d5..091a28fd 100644 --- a/src/main/java/com/dnd/runus/presentation/v1/running/dto/response/RunningRecordQueryResponse.java +++ b/src/main/java/com/dnd/runus/presentation/v1/running/dto/response/RunningRecordQueryResponse.java @@ -1,7 +1,6 @@ package com.dnd.runus.presentation.v1.running.dto.response; -import com.dnd.runus.domain.challenge.achievement.ChallengeAchievement; -import com.dnd.runus.domain.goalAchievement.GoalAchievement; +import com.dnd.runus.application.running.dto.RunningResultDto; import com.dnd.runus.domain.running.RunningRecord; import com.dnd.runus.global.constant.RunningEmoji; import com.dnd.runus.presentation.v1.running.dto.ChallengeDto; @@ -32,45 +31,18 @@ public record RunningRecordQueryResponse( @NotNull RunningRecordMetricsDto runningData ) { - public static RunningRecordQueryResponse from(RunningRecord runningRecord) { - return buildResponse(runningRecord, null, null, RunningAchievementMode.NORMAL); - } - - public static RunningRecordQueryResponse of(RunningRecord runningRecord, ChallengeAchievement achievement) { - return buildResponse(runningRecord, - new ChallengeDto( - achievement.challenge().challengeId(), - achievement.challenge().name(), - achievement.description(), - achievement.challenge().imageUrl(), - achievement.isSuccess() - ), - null, - RunningAchievementMode.CHALLENGE - ); - } - public static RunningRecordQueryResponse of(RunningRecord runningRecord, GoalAchievement achievement) { - return buildResponse(runningRecord, - null, - new GoalResultDto( - achievement.getTitle(), - achievement.getDescription(), - achievement.getIconUrl(), - achievement.isAchieved() - ), - RunningAchievementMode.GOAL + public static RunningRecordQueryResponse from(RunningResultDto runningResult) { + return buildResponse( + runningResult.runningRecord(), + runningResult.challengeAchievement() == null ? null + : ChallengeDto.from(runningResult.challengeAchievement()), + runningResult.goalAchievement() == null ? null + : GoalResultDto.from(runningResult.goalAchievement()), + runningResult.runningAchievementMode() ); } - public static RunningRecordQueryResponse of(RunningRecord runningRecord, ChallengeAchievement challengeAchievement, GoalAchievement goalAchievement) { - if (challengeAchievement != null) { - return of(runningRecord, challengeAchievement); - } else if (goalAchievement != null) { - return of(runningRecord, goalAchievement); - } - return from(runningRecord); - } private static RunningRecordQueryResponse buildResponse(RunningRecord runningRecord, ChallengeDto challenge, GoalResultDto goal, RunningAchievementMode achievementMode) { return new RunningRecordQueryResponse( @@ -89,4 +61,5 @@ private static RunningRecordQueryResponse buildResponse(RunningRecord runningRec ) ); } + } diff --git a/src/main/java/com/dnd/runus/presentation/v2/running/RunningRecordControllerV2.java b/src/main/java/com/dnd/runus/presentation/v2/running/RunningRecordControllerV2.java index 5caf2f7f..db47200e 100644 --- a/src/main/java/com/dnd/runus/presentation/v2/running/RunningRecordControllerV2.java +++ b/src/main/java/com/dnd/runus/presentation/v2/running/RunningRecordControllerV2.java @@ -14,6 +14,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -27,6 +28,12 @@ public class RunningRecordControllerV2 { private final RunningRecordServiceV2 runningRecordService2; private final RunningRecordService runningRecordService; + @GetMapping("/{runningRecordId}") + @Operation(summary = "러닝 기록 상세 조회", description = "RunngingRecord id로 러닝 상세 기록을 조회합니다.") + public RunningRecordResultResponseV2 getRunningRecord(@MemberId long memberId, @PathVariable long runningRecordId) { + return RunningRecordResultResponseV2.from(runningRecordService.getRunningRecord(memberId, runningRecordId)); + } + @Operation(summary = "이번 달 러닝 기록 조회(홈화면) V2", description = """ 홈화면의 이번 달 러닝 기록을 조회 합니다.
""") @@ -47,9 +54,9 @@ public RunningRecordMonthlySummaryResponseV2 getMonthlyRunningSummary(@MemberId """ 러닝 기록을 추가합니다.
러닝 기록은 시작 시간, 종료 시간, 러닝 평가(emotion), 러닝 데이터 등으로 구성됩니다.
- 챌린지 모드가 normal : challengeValues, goalValues 둘다 null
- 챌린지 모드가 challenge : challengeValues 필수 값
- 챌린지 모드가 goal : goalValues 필수 값
+ normal : challengeValues, goalValues 둘다 null
+ challenge : challengeValues 필수 값
+ goal : goalValues 필수 값
러닝 데이터는 위치, 거리, 시간, 칼로리, 평균 페이스, 러닝 경로로 구성됩니다.
러닝 기록 추가에 성공하면 러닝 기록 ID, 기록 정보를 반환합니다.
""") @@ -58,7 +65,8 @@ public RunningRecordMonthlySummaryResponseV2 getMonthlyRunningSummary(@MemberId ErrorType.CHALLENGE_VALUES_REQUIRED_IN_CHALLENGE_MODE, ErrorType.GOAL_VALUES_REQUIRED_IN_GOAL_MODE, ErrorType.GOAL_TIME_AND_DISTANCE_BOTH_EXIST, - ErrorType.ROUTE_MUST_HAVE_AT_LEAST_TWO_COORDINATES + ErrorType.ROUTE_MUST_HAVE_AT_LEAST_TWO_COORDINATES, + ErrorType.CHALLENGE_NOT_ACTIVE }) @PostMapping @ResponseStatus(HttpStatus.OK) diff --git a/src/main/java/com/dnd/runus/presentation/v2/running/dto/AchievementResultDto.java b/src/main/java/com/dnd/runus/presentation/v2/running/dto/AchievementResultDto.java new file mode 100644 index 00000000..a2fb9c27 --- /dev/null +++ b/src/main/java/com/dnd/runus/presentation/v2/running/dto/AchievementResultDto.java @@ -0,0 +1,23 @@ +package com.dnd.runus.presentation.v2.running.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import org.springframework.lang.Nullable; + +/** + * 챌린지, 목표 달성 결과 통합 DTO + */ +public record AchievementResultDto( + @Schema(description = "챌린지 이름 또는 설정된 목표 제목", examples = {"오늘 30분 동안 뛰기", "2.5km 달성"}) + String title, + @Schema(description = "결과 문구", examples = {"정말 대단해요! 잘하셨어요", "아쉬워요. 내일 다시 도전해보세요!"}) + String subTitle, + @Schema(description = "아이콘 이미지 URL") + String iconUrl, + @Schema(description = "성공 여부") + boolean isSuccess, + @Nullable + @Schema(description = "퍼센테이지 값, V2 이전에 퍼센테이지를 나타낼 수 없는 챌린지일 경우 null값을 리턴합니다.") + Double percentage +) { + +} diff --git a/src/main/java/com/dnd/runus/presentation/v2/running/dto/RouteDtoV2.java b/src/main/java/com/dnd/runus/presentation/v2/running/dto/RouteDtoV2.java index c4ce3256..1ac8bf8f 100644 --- a/src/main/java/com/dnd/runus/presentation/v2/running/dto/RouteDtoV2.java +++ b/src/main/java/com/dnd/runus/presentation/v2/running/dto/RouteDtoV2.java @@ -14,7 +14,7 @@ public record RouteDtoV2( ) { public record Point(double longitude, double latitude) { public static Point from(CoordinatePoint point) { - return new Point(point.longitude(), point.longitude()); + return new Point(point.longitude(), point.latitude()); } } } diff --git a/src/main/java/com/dnd/runus/presentation/v2/running/dto/request/RunningRecordRequestV2.java b/src/main/java/com/dnd/runus/presentation/v2/running/dto/request/RunningRecordRequestV2.java index 7265748a..c6100820 100644 --- a/src/main/java/com/dnd/runus/presentation/v2/running/dto/request/RunningRecordRequestV2.java +++ b/src/main/java/com/dnd/runus/presentation/v2/running/dto/request/RunningRecordRequestV2.java @@ -31,7 +31,8 @@ public record RunningRecordRequestV2( com.dnd.runus.presentation.v1.running.dto.request.RunningAchievementMode achievementMode, @Schema(description = "챌린지 데이터, 챌린지를 하지 않은 경우 null이나 필드 없이 보내주세요") ChallengeAchievedDto challengeValues, - @Schema(description = "목표 데이터, 목표를 설정하지 않은 경우 null이나 필드 없이 보내주세요") + @Schema(description = "목표 데이터, 목표를 설정하지 않은 경우 null이나 필드 없이 보내주세요. " + + "goalDistance(거리) 또는 goalTime(시간)값 둘 중 하나는 null이어야 합니다.") GoalAchievedDto goalValues, @NotNull RunningRecordMetrics runningData @@ -52,7 +53,8 @@ public record RunningRecordRequestV2( if(goalValues == null) { throw new BusinessException(ErrorType.GOAL_VALUES_REQUIRED_IN_GOAL_MODE); } - if (goalValues.goalDistance() == null && goalValues.goalTime() == null) { + if ((goalValues.goalDistance() == null && goalValues.goalTime() == null) + || (goalValues.goalDistance() != null && goalValues.goalTime() != null)) { throw new BusinessException(ErrorType.GOAL_TIME_AND_DISTANCE_BOTH_EXIST); } } diff --git a/src/main/java/com/dnd/runus/presentation/v2/running/dto/response/RunningRecordResultResponseV2.java b/src/main/java/com/dnd/runus/presentation/v2/running/dto/response/RunningRecordResultResponseV2.java index 57fd0841..6390b7ff 100644 --- a/src/main/java/com/dnd/runus/presentation/v2/running/dto/response/RunningRecordResultResponseV2.java +++ b/src/main/java/com/dnd/runus/presentation/v2/running/dto/response/RunningRecordResultResponseV2.java @@ -7,8 +7,8 @@ import com.dnd.runus.domain.goalAchievement.GoalAchievement; import com.dnd.runus.domain.running.RunningRecord; import com.dnd.runus.global.constant.RunningEmoji; -import com.dnd.runus.presentation.v1.running.dto.ChallengeDto; -import com.dnd.runus.presentation.v1.running.dto.GoalResultDto; +import com.dnd.runus.presentation.v1.running.dto.request.RunningAchievementMode; +import com.dnd.runus.presentation.v2.running.dto.AchievementResultDto; import com.dnd.runus.presentation.v2.running.dto.RouteDtoV2; import com.dnd.runus.presentation.v2.running.dto.RouteDtoV2.Point; import io.swagger.v3.oas.annotations.media.Schema; @@ -29,12 +29,9 @@ public record RunningRecordResultResponseV2( RunningEmoji emotion, @NotNull @Schema(description = "달성 모드, normal: 일반(목표 설정 X), challenge: 챌린지, goal: 목표") - com.dnd.runus.presentation.v1.running.dto.request.RunningAchievementMode achievementMode, - //todo 챌린지, 및 goal 관련해서 다시 구성 - @Schema(description = "챌린지 정보, achievementMode가 challenge인 경우에만 값이 존재합니다.") - ChallengeDto challenge, - @Schema(description = "목표 결과 정보, achievementMode가 goal인 경우에만 값이 존재합니다.") - GoalResultDto goal, + RunningAchievementMode achievementMode, + @Schema(description = "달성 값(챌린지 또는 목표), achievementMode가 challenge 또는 goal인 경우에만 값이 존재합니다.") + AchievementResultDto achievementResult, @NotNull RunningRecordMetrics runningData ) { @@ -61,10 +58,7 @@ public static RunningRecordResultResponseV2 from(RunningResultDto runningRecordR runningRecord.endAt().toLocalDateTime(), runningRecord.emoji(), runningRecordResult.runningAchievementMode(), - runningRecordResult.runningAchievementMode() != com.dnd.runus.presentation.v1.running.dto.request.RunningAchievementMode.CHALLENGE ? null - : buildChallengeDto(runningRecordResult.challengeAchievement()), - runningRecordResult.runningAchievementMode() != com.dnd.runus.presentation.v1.running.dto.request.RunningAchievementMode.GOAL ? null - : buildGoalResultDto(runningRecordResult.goalAchievement()), + buildAchievementResultOf(runningRecordResult), new RunningRecordMetrics( runningRecord.averagePace(), runningRecord.duration(), @@ -75,32 +69,38 @@ public static RunningRecordResultResponseV2 from(RunningResultDto runningRecordR ); } - private static ChallengeDto buildChallengeDto(ChallengeAchievement achievement) { - if (achievement == null) { - return null; - } - return new ChallengeDto( - achievement.challenge().challengeId(), - achievement.challenge().name(), - achievement.description(), - achievement.challenge().imageUrl(), - achievement.isSuccess() - ); - } + private static AchievementResultDto buildAchievementResultOf(RunningResultDto runningRecordResult) { + if (runningRecordResult.runningAchievementMode() == RunningAchievementMode.NORMAL) return null; + switch (runningRecordResult.runningAchievementMode()) { + case GOAL -> { + GoalAchievement goalAchievement = runningRecordResult.goalAchievement(); + if (goalAchievement == null) return null; + return new AchievementResultDto( + goalAchievement.getTitle(), + goalAchievement.getDescription(), + goalAchievement.getIconUrl(), + goalAchievement.isAchieved(), + runningRecordResult.percentage() + ); + } + case CHALLENGE -> { + ChallengeAchievement challengeAchievement = runningRecordResult.challengeAchievement(); + if (challengeAchievement == null) return null; + return new AchievementResultDto( + challengeAchievement.challenge().name(), + challengeAchievement.description(), + challengeAchievement.challenge().imageUrl(), + challengeAchievement.isSuccess(), + runningRecordResult.percentage() + ); - private static GoalResultDto buildGoalResultDto(GoalAchievement achievement) { - if (achievement == null) { - return null; + } + default -> { + return null; + } } - return new GoalResultDto( - achievement.getTitle(), - achievement.getDescription(), - achievement.getIconUrl(), - achievement.isAchieved() - ); } - private static List convertRouteDtoListFrom( List runningRecordRoute) { // route가 null, empty, 또는 경로데이터를 사용하지 않았을 버전의 데이터 값 인경우 null를 리턴 diff --git a/src/test/java/com/dnd/runus/application/running/RunningRecordServiceTest.java b/src/test/java/com/dnd/runus/application/running/RunningRecordServiceTest.java index 1e11e21b..a57ed6a7 100644 --- a/src/test/java/com/dnd/runus/application/running/RunningRecordServiceTest.java +++ b/src/test/java/com/dnd/runus/application/running/RunningRecordServiceTest.java @@ -24,7 +24,6 @@ import com.dnd.runus.presentation.v1.running.dto.request.RunningRecordWeeklySummaryType; import com.dnd.runus.presentation.v1.running.dto.response.RunningRecordAddResultResponseV1; import com.dnd.runus.presentation.v1.running.dto.response.RunningRecordMonthlySummaryResponse; -import com.dnd.runus.presentation.v1.running.dto.response.RunningRecordQueryResponse; import com.dnd.runus.presentation.v1.running.dto.response.RunningRecordWeeklySummaryResponse; import com.dnd.runus.presentation.v2.running.dto.RouteDtoV2; import com.dnd.runus.presentation.v2.running.dto.request.RunningRecordRequestV2; @@ -47,6 +46,7 @@ import static com.dnd.runus.global.constant.TimeConstant.SERVER_TIMEZONE; import static java.time.temporal.ChronoField.DAY_OF_WEEK; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; @@ -117,11 +117,11 @@ void getRunningRecord() { given(runningRecordRepository.findById(runningRecordId)).willReturn(Optional.of(runningRecord)); // when - RunningRecordQueryResponse result = runningRecordService.getRunningRecord(memberId, runningRecordId); + RunningResultDto result = runningRecordService.getRunningRecord(memberId, runningRecordId); // then - assertEquals(runningRecordId, result.runningRecordId()); - assertEquals(runningRecord.emoji(), result.emotion()); + assertEquals(runningRecordId, result.runningRecord().runningId()); + assertEquals(runningRecord.emoji(), result.runningRecord().emoji()); } @Test @@ -156,6 +156,7 @@ void getRunningRecord_challenge() { // given long memberId = 1; long runningRecordId = 1; + Challenge challenge = new Challenge(1L, "challenge", "image", true, ChallengeType.TODAY); Member member = new Member(memberId, MemberRole.USER, "nickname1", OffsetDateTime.now(), OffsetDateTime.now()); RunningRecord runningRecord = new RunningRecord( 1L, @@ -171,23 +172,126 @@ void getRunningRecord_challenge() { "end location", RunningEmoji.VERY_GOOD); - ChallengeAchievement.Status challengeAchievementStatus = new ChallengeAchievement.Status( - 1L, new Challenge(1L, "challenge", "image", true, ChallengeType.TODAY), true); + ChallengeAchievement.Status challengeAchievementStatus = new ChallengeAchievement.Status(1L, challenge, true); given(runningRecordRepository.findById(runningRecordId)).willReturn(Optional.of(runningRecord)); given(challengeAchievementRepository.findByRunningRecordId(runningRecordId)) .willReturn(Optional.of(challengeAchievementStatus)); + given(challengeRepository.findChallengeWithConditionsByChallengeId(challenge.challengeId())) + .willReturn(Optional.of(new ChallengeWithCondition( + challenge, + List.of(new ChallengeCondition( + GoalMetricType.DISTANCE, ComparisonType.GREATER_THAN_OR_EQUAL_TO, 1_000))))); // when - RunningRecordQueryResponse result = runningRecordService.getRunningRecord(memberId, runningRecordId); + RunningResultDto result = runningRecordService.getRunningRecord(memberId, runningRecordId); // then - assertEquals(runningRecordId, result.runningRecordId()); - assertEquals(runningRecord.emoji(), result.emotion()); + assertEquals(runningRecordId, result.runningRecord().runningId()); + assertEquals(runningRecord.emoji(), result.runningRecord().emoji()); + assertNotNull(result.challengeAchievement()); + assertEquals(RunningAchievementMode.CHALLENGE, result.runningAchievementMode()); assertEquals( challengeAchievementStatus.challenge().name(), - result.challenge().title()); - assertEquals(RunningAchievementMode.CHALLENGE, result.achievementMode()); + result.challengeAchievement().challenge().name()); + assertEquals(1, result.percentage()); + } + + @Test + @DisplayName("Challenge 모드의 러닝 기록 조회 : DISTANCE_IN_TIME 타입일 경우 퍼센테이지 null 리턴 ") + void getRunningRecord_challenge_DISTANCE_IN_TIME_Type() { + // given + long memberId = 1; + long runningRecordId = 1; + Challenge challenge = new Challenge(1L, "challenge", "image", true, ChallengeType.DISTANCE_IN_TIME); + Member member = new Member(memberId, MemberRole.USER, "nickname1", OffsetDateTime.now(), OffsetDateTime.now()); + RunningRecord runningRecord = new RunningRecord( + 1L, + member, + 10_000, + Duration.ofSeconds(10_000), + 500, + new Pace(5, 30), + ZonedDateTime.now(), + ZonedDateTime.now(), + List.of(new CoordinatePoint(0, 0, 0), new CoordinatePoint(0, 0, 0)), + "start location", + "end location", + RunningEmoji.VERY_GOOD); + + ChallengeAchievement.Status challengeAchievementStatus = new ChallengeAchievement.Status(1L, challenge, true); + + given(runningRecordRepository.findById(runningRecordId)).willReturn(Optional.of(runningRecord)); + given(challengeAchievementRepository.findByRunningRecordId(runningRecordId)) + .willReturn(Optional.of(challengeAchievementStatus)); + given(challengeRepository.findChallengeWithConditionsByChallengeId(challenge.challengeId())) + .willReturn(Optional.of(new ChallengeWithCondition( + challenge, + List.of(new ChallengeCondition( + GoalMetricType.DISTANCE, ComparisonType.GREATER_THAN_OR_EQUAL_TO, 1_000))))); + + // when + RunningResultDto result = runningRecordService.getRunningRecord(memberId, runningRecordId); + + // then + assertEquals(runningRecordId, result.runningRecord().runningId()); + assertEquals(runningRecord.emoji(), result.runningRecord().emoji()); + assertNotNull(result.challengeAchievement()); + assertEquals(RunningAchievementMode.CHALLENGE, result.runningAchievementMode()); + assertEquals( + challengeAchievementStatus.challenge().name(), + result.challengeAchievement().challenge().name()); + assertNull(result.percentage()); + } + + @Test + @DisplayName("Challenge 모드의 러닝 기록 조회 : 퍼센테이지를 표시 할 수 없는 챌린지가 하나라도 있을 경우 퍼센테이지 null 리턴 ") + void getRunningRecord_challenge_hasNotPercentage() { + // given + long memberId = 1; + long runningRecordId = 1; + Challenge challenge = new Challenge(1L, "challenge", "image", true, ChallengeType.TODAY); + Member member = new Member(memberId, MemberRole.USER, "nickname1", OffsetDateTime.now(), OffsetDateTime.now()); + RunningRecord runningRecord = new RunningRecord( + 1L, + member, + 10_000, + Duration.ofSeconds(10_000), + 500, + new Pace(5, 30), + ZonedDateTime.now(), + ZonedDateTime.now(), + List.of(new CoordinatePoint(0, 0, 0), new CoordinatePoint(0, 0, 0)), + "start location", + "end location", + RunningEmoji.VERY_GOOD); + + ChallengeAchievement.Status challengeAchievementStatus = new ChallengeAchievement.Status(1L, challenge, true); + + given(runningRecordRepository.findById(runningRecordId)).willReturn(Optional.of(runningRecord)); + given(challengeAchievementRepository.findByRunningRecordId(runningRecordId)) + .willReturn(Optional.of(challengeAchievementStatus)); + given(challengeRepository.findChallengeWithConditionsByChallengeId(challenge.challengeId())) + .willReturn(Optional.of(new ChallengeWithCondition( + challenge, + List.of( + new ChallengeCondition( + GoalMetricType.DISTANCE, ComparisonType.GREATER_THAN_OR_EQUAL_TO, 1_000), + new ChallengeCondition( + GoalMetricType.PACE, ComparisonType.GREATER_THAN_OR_EQUAL_TO, 1_000))))); + + // when + RunningResultDto result = runningRecordService.getRunningRecord(memberId, runningRecordId); + + // then + assertEquals(runningRecordId, result.runningRecord().runningId()); + assertEquals(runningRecord.emoji(), result.runningRecord().emoji()); + assertNotNull(result.challengeAchievement()); + assertEquals(RunningAchievementMode.CHALLENGE, result.runningAchievementMode()); + assertEquals( + challengeAchievementStatus.challenge().name(), + result.challengeAchievement().challenge().name()); + assertNull(result.percentage()); } @Test @@ -531,10 +635,25 @@ private RunningRecord createRunningRecord(RunningRecordRequestV1 request, Member @DisplayName("러닝 결과 저장 V2") class RunningRecordAddV2 { + private RunningRecordRequestV2.RunningRecordMetrics runningRecordMetrics; + + @BeforeEach + void beforeEach() { + runningRecordMetrics = new RunningRecordRequestV2.RunningRecordMetrics( + Duration.ofSeconds(600), + 1000, + 500.0, + List.of( + new RouteDtoV2(new RouteDtoV2.Point(0, 0), new RouteDtoV2.Point(1, 1)), + new RouteDtoV2(new RouteDtoV2.Point(2, 2), new RouteDtoV2.Point(3, 3)), + new RouteDtoV2(new RouteDtoV2.Point(4, 4), new RouteDtoV2.Point(5, 5)))); + } + @Test @DisplayName("러닝 결과 저장 : 루트가 순서대로 들어갔는지 확인") void addRunningRecordV2_CheckRoute() { // given + Member member = new Member(MemberRole.USER, "nickname1"); RunningRecordRequestV2 request = new RunningRecordRequestV2( LocalDateTime.of(2021, 1, 1, 12, 10, 30), LocalDateTime.of(2021, 1, 1, 13, 12, 10), @@ -544,16 +663,251 @@ void addRunningRecordV2_CheckRoute() { com.dnd.runus.presentation.v1.running.dto.request.RunningAchievementMode.NORMAL, null, null, - new RunningRecordRequestV2.RunningRecordMetrics( - Duration.ofSeconds(10_100), - 10_000, - 500.0, - List.of( - new RouteDtoV2(new RouteDtoV2.Point(0, 0), new RouteDtoV2.Point(1, 1)), - new RouteDtoV2(new RouteDtoV2.Point(2, 2), new RouteDtoV2.Point(3, 3)), - new RouteDtoV2(new RouteDtoV2.Point(4, 4), new RouteDtoV2.Point(5, 5))))); - - List route = request.runningData().route().stream() + runningRecordMetrics); + + RunningRecord expectedRecord = createRecordFrom(member, request); + + given(memberRepository.findById(member.memberId())).willReturn(Optional.of(member)); + given(runningRecordRepository.save(expectedRecord)).willReturn(expectedRecord); + + // when + RunningResultDto response = runningRecordService.addRunningRecordV2(member.memberId(), request); + + // then + assertEquals( + com.dnd.runus.presentation.v1.running.dto.request.RunningAchievementMode.NORMAL, + response.runningAchievementMode()); + assertNull(response.challengeAchievement()); + assertNull(response.goalAchievement()); + assertNull(response.percentage()); + + RunningRecord resultRunning = response.runningRecord(); + for (int i = 0; i < resultRunning.route().size(); i++) { + assertEquals(i, resultRunning.route().get(i).longitude()); + assertEquals(i, resultRunning.route().get(i).latitude()); + } + } + + @Test + @DisplayName("러닝 모드가가 챌린지(어제보다 ~) : 기준이 되는 러닝 기록의 어제 러닝 기록이 없으면 NotFoundException를 발생한다.") + void addRunningRecordV2_ChallengeMode_DefeatYesterday_RunningRecordNotFoundError() { + // given + Member member = new Member(MemberRole.USER, "nickname1"); + Challenge challenge = + new Challenge(1L, "어제보다 1km 더 달리기", 360, "image url", true, ChallengeType.DEFEAT_YESTERDAY); + RunningRecordRequestV2 request = new RunningRecordRequestV2( + LocalDateTime.of(2021, 1, 1, 12, 10, 30), + LocalDateTime.of(2021, 1, 1, 13, 12, 10), + "start location", + "end location", + RunningEmoji.VERY_GOOD, + com.dnd.runus.presentation.v1.running.dto.request.RunningAchievementMode.CHALLENGE, + new RunningRecordRequestV2.ChallengeAchievedDto(challenge.challengeId(), true), + null, + runningRecordMetrics); + RunningRecord expectedRecord = createRecordFrom(member, request); + ChallengeAchievement expectedChallengeAchievement = new ChallengeAchievement( + challenge, expectedRecord, request.challengeValues().isSuccess()); + given(memberRepository.findById(member.memberId())).willReturn(Optional.of(member)); + given(runningRecordRepository.save(expectedRecord)).willReturn(expectedRecord); + given(challengeRepository.findById(request.challengeValues().challengeId())) + .willReturn(Optional.of(challenge)); + given(challengeAchievementRepository.save(expectedChallengeAchievement)) + .willReturn(expectedChallengeAchievement); + given(challengeRepository.findChallengeWithConditionsByChallengeId(challenge.challengeId())) + .willReturn(Optional.of(new ChallengeWithCondition( + challenge, + List.of(new ChallengeCondition( + GoalMetricType.DISTANCE, ComparisonType.GREATER_THAN_OR_EQUAL_TO, 1000))))); + OffsetDateTime runningDate = expectedRecord + .startAt() + .toLocalDate() + .atStartOfDay(expectedRecord.startAt().getZone()) + .toOffsetDateTime(); + given(runningRecordRepository.findByMemberIdAndStartAtBetween( + member.memberId(), runningDate.minusDays(1), runningDate)) + .willReturn(List.of()); + + // when, then + assertThrows( + NotFoundException.class, () -> runningRecordService.addRunningRecordV2(member.memberId(), request)); + } + + @Test + @DisplayName("러닝 모드가 챌린지(어제보다 ~km더 뛰기) : 챌린지 관련 값이 정상적으로 저장되었는지 확인한다.") + void addRunningRecordV2_ChallengeMode_DefeatYesterday_Distance() { + // given + Member member = new Member(MemberRole.USER, "nickname1"); + Challenge challenge = + new Challenge(1L, "어제보다 1km 더 달리기", 360, "image url", true, ChallengeType.DEFEAT_YESTERDAY); + RunningRecordRequestV2 request = new RunningRecordRequestV2( + LocalDateTime.of(2021, 1, 1, 12, 10, 30), + LocalDateTime.of(2021, 1, 1, 13, 12, 10), + "start location", + "end location", + RunningEmoji.VERY_GOOD, + com.dnd.runus.presentation.v1.running.dto.request.RunningAchievementMode.CHALLENGE, + new RunningRecordRequestV2.ChallengeAchievedDto(challenge.challengeId(), false), + null, + runningRecordMetrics); + RunningRecord expectedRecord = createRecordFrom(member, request); + ChallengeAchievement expectedChallengeAchievement = new ChallengeAchievement( + challenge, expectedRecord, request.challengeValues().isSuccess()); + OffsetDateTime runningDate = expectedRecord + .startAt() + .toLocalDate() + .atStartOfDay(expectedRecord.startAt().getZone()) + .toOffsetDateTime(); + RunningRecord yesterdayRunning = RunningRecord.builder() + .member(member) + .startAt(expectedRecord.startAt().minusDays(1)) + .endAt(expectedRecord.endAt().minusDays(1)) + .emoji(expectedRecord.emoji()) + .startLocation(expectedRecord.startLocation()) + .endLocation(expectedRecord.endLocation()) + .distanceMeter(expectedRecord.distanceMeter()) + .duration(expectedRecord.duration()) + .calorie(expectedRecord.calorie()) + .averagePace(expectedRecord.averagePace()) + .route(expectedRecord.route()) + .build(); + given(memberRepository.findById(member.memberId())).willReturn(Optional.of(member)); + given(runningRecordRepository.save(expectedRecord)).willReturn(expectedRecord); + given(challengeRepository.findById(request.challengeValues().challengeId())) + .willReturn(Optional.of(challenge)); + given(challengeAchievementRepository.save(expectedChallengeAchievement)) + .willReturn(expectedChallengeAchievement); + given(challengeRepository.findChallengeWithConditionsByChallengeId(challenge.challengeId())) + .willReturn(Optional.of(new ChallengeWithCondition( + challenge, + List.of(new ChallengeCondition( + GoalMetricType.DISTANCE, ComparisonType.GREATER_THAN_OR_EQUAL_TO, 1000))))); + given(runningRecordRepository.findByMemberIdAndStartAtBetween( + member.memberId(), runningDate.minusDays(1), runningDate)) + .willReturn(List.of(yesterdayRunning)); + + // when + RunningResultDto result = runningRecordService.addRunningRecordV2(member.memberId(), request); + + // then + assertEquals( + result.runningAchievementMode(), + com.dnd.runus.presentation.v1.running.dto.request.RunningAchievementMode.CHALLENGE); + assertNotNull(result.challengeAchievement()); + assertNotNull(result.percentage()); + assertEquals(0.5, result.percentage()); + } + + @Test + @DisplayName("러닝 모드가가 챌린지(어제보다 ~분 더 뛰기) : 챌린지 관련 값이 정상적으로 저장되었는지 확인한다.") + void addRunningRecordV2_ChallengeMode_DefeatYesterday_Time() { + // given + Member member = new Member(MemberRole.USER, "nickname1"); + Challenge challenge = + new Challenge(1L, "어제보다 30분 더 달리기", 360, "image url", true, ChallengeType.DEFEAT_YESTERDAY); + RunningRecordRequestV2 request = new RunningRecordRequestV2( + LocalDateTime.of(2021, 1, 1, 12, 10, 30), + LocalDateTime.of(2021, 1, 1, 13, 12, 10), + "start location", + "end location", + RunningEmoji.VERY_GOOD, + com.dnd.runus.presentation.v1.running.dto.request.RunningAchievementMode.CHALLENGE, + new RunningRecordRequestV2.ChallengeAchievedDto(challenge.challengeId(), false), + null, + runningRecordMetrics); + RunningRecord expectedRecord = createRecordFrom(member, request); + ChallengeAchievement expectedChallengeAchievement = new ChallengeAchievement( + challenge, expectedRecord, request.challengeValues().isSuccess()); + OffsetDateTime runningDate = expectedRecord + .startAt() + .toLocalDate() + .atStartOfDay(expectedRecord.startAt().getZone()) + .toOffsetDateTime(); + RunningRecord yesterdayRunning = RunningRecord.builder() + .member(member) + .startAt(expectedRecord.startAt().minusDays(1)) + .endAt(expectedRecord.endAt().minusDays(1)) + .emoji(expectedRecord.emoji()) + .startLocation(expectedRecord.startLocation()) + .endLocation(expectedRecord.endLocation()) + .distanceMeter(expectedRecord.distanceMeter()) + .duration(expectedRecord.duration()) + .calorie(expectedRecord.calorie()) + .averagePace(expectedRecord.averagePace()) + .route(expectedRecord.route()) + .build(); + given(memberRepository.findById(member.memberId())).willReturn(Optional.of(member)); + given(runningRecordRepository.save(expectedRecord)).willReturn(expectedRecord); + given(challengeRepository.findById(request.challengeValues().challengeId())) + .willReturn(Optional.of(challenge)); + given(challengeAchievementRepository.save(expectedChallengeAchievement)) + .willReturn(expectedChallengeAchievement); + given(challengeRepository.findChallengeWithConditionsByChallengeId(challenge.challengeId())) + .willReturn(Optional.of(new ChallengeWithCondition( + challenge, + List.of(new ChallengeCondition( + GoalMetricType.TIME, ComparisonType.GREATER_THAN_OR_EQUAL_TO, 1800))))); + given(runningRecordRepository.findByMemberIdAndStartAtBetween( + member.memberId(), runningDate.minusDays(1), runningDate)) + .willReturn(List.of(yesterdayRunning)); + + // when + RunningResultDto result = runningRecordService.addRunningRecordV2(member.memberId(), request); + + // then + assertEquals( + result.runningAchievementMode(), + com.dnd.runus.presentation.v1.running.dto.request.RunningAchievementMode.CHALLENGE); + assertNotNull(result.challengeAchievement()); + assertNotNull(result.percentage()); + assertEquals(0.25, result.percentage()); + } + + @Test + @DisplayName("러닝 모드가가 챌린지이고 챌린지가 성공일 경우 퍼센티이지 값은 1를 리턴한다.") + void addRunningRecordV2_ChallengeMode_CheckPercentage() { + // given + Member member = new Member(MemberRole.USER, "nickname1"); + Challenge challenge = new Challenge(1L, "500m 달리기", 360, "image url", true, ChallengeType.TODAY); + RunningRecordRequestV2 request = new RunningRecordRequestV2( + LocalDateTime.of(2021, 1, 1, 12, 10, 30), + LocalDateTime.of(2021, 1, 1, 13, 12, 10), + "start location", + "end location", + RunningEmoji.VERY_GOOD, + com.dnd.runus.presentation.v1.running.dto.request.RunningAchievementMode.CHALLENGE, + new RunningRecordRequestV2.ChallengeAchievedDto(challenge.challengeId(), true), + null, + runningRecordMetrics); + RunningRecord expectedRecord = createRecordFrom(member, request); + ChallengeAchievement expectedChallengeAchievement = new ChallengeAchievement( + challenge, expectedRecord, request.challengeValues().isSuccess()); + given(memberRepository.findById(member.memberId())).willReturn(Optional.of(member)); + given(runningRecordRepository.save(expectedRecord)).willReturn(expectedRecord); + given(challengeRepository.findById(request.challengeValues().challengeId())) + .willReturn(Optional.of(challenge)); + given(challengeAchievementRepository.save(expectedChallengeAchievement)) + .willReturn(expectedChallengeAchievement); + given(challengeRepository.findChallengeWithConditionsByChallengeId(challenge.challengeId())) + .willReturn(Optional.of(new ChallengeWithCondition( + challenge, + List.of(new ChallengeCondition( + GoalMetricType.DISTANCE, ComparisonType.GREATER_THAN_OR_EQUAL_TO, 500))))); + + // when + RunningResultDto result = runningRecordService.addRunningRecordV2(member.memberId(), request); + + // then + assertEquals( + result.runningAchievementMode(), + com.dnd.runus.presentation.v1.running.dto.request.RunningAchievementMode.CHALLENGE); + assertNotNull(result.challengeAchievement()); + assertNotNull(result.percentage()); + assertEquals(1, result.percentage()); + } + + private RunningRecord createRecordFrom(Member member, RunningRecordRequestV2 request) { + List coordinatePoints = request.runningData().route().stream() .flatMap(point -> Stream.of( new CoordinatePoint( point.start().longitude(), point.start().latitude()), @@ -561,9 +915,7 @@ void addRunningRecordV2_CheckRoute() { point.end().longitude(), point.end().latitude()))) .collect(Collectors.toList()); - Member member = new Member(MemberRole.USER, "nickname1"); - - RunningRecord expectedRecord = RunningRecord.builder() + return RunningRecord.builder() .member(member) .startAt(request.startAt().atZone(defaultZoneOffset)) .endAt(request.endAt().atZone(defaultZoneOffset)) @@ -576,27 +928,8 @@ void addRunningRecordV2_CheckRoute() { .averagePace(Pace.from( request.runningData().distanceMeter(), request.runningData().runningTime())) - .route(route) + .route(coordinatePoints) .build(); - - given(memberRepository.findById(1L)).willReturn(Optional.of(member)); - given(runningRecordRepository.save(expectedRecord)).willReturn(expectedRecord); - - // when - RunningResultDto response = runningRecordService.addRunningRecordV2(1L, request); - - // then - assertEquals( - com.dnd.runus.presentation.v1.running.dto.request.RunningAchievementMode.NORMAL, - response.runningAchievementMode()); - assertNull(response.challengeAchievement()); - assertNull(response.goalAchievement()); - - RunningRecord resultRunning = response.runningRecord(); - for (int i = 0; i < resultRunning.route().size(); i++) { - assertEquals(i, resultRunning.route().get(i).longitude()); - assertEquals(i, resultRunning.route().get(i).latitude()); - } } } } diff --git a/src/test/java/com/dnd/runus/presentation/v2/running/RunningRecordControllerV2Test.java b/src/test/java/com/dnd/runus/presentation/v2/running/RunningRecordControllerV2Test.java index 7dc00cd8..9bda3117 100644 --- a/src/test/java/com/dnd/runus/presentation/v2/running/RunningRecordControllerV2Test.java +++ b/src/test/java/com/dnd/runus/presentation/v2/running/RunningRecordControllerV2Test.java @@ -110,6 +110,7 @@ void addRunningRecord_Normal_CheckRunningPath() throws Exception { createRunningRecordFrom(request), com.dnd.runus.presentation.v1.running.dto.request.RunningAchievementMode.NORMAL, null, + null, null)); // when @@ -121,8 +122,7 @@ void addRunningRecord_Normal_CheckRunningPath() throws Exception { result.andExpect(status().isOk()) .andExpect(jsonPath("$.data.emotion").value("very-good")) .andExpect(jsonPath("$.data.achievementMode").value("normal")) - .andExpect(jsonPath("$.data.challenge").doesNotExist()) - .andExpect(jsonPath("$.data.goal").doesNotExist()) + .andExpect(jsonPath("$.data.achievementResult").doesNotExist()) .andExpect(jsonPath("$.data.runningData.averagePace").value("5’30”")) .andExpect( jsonPath("$.data.runningData.route[0].start.longitude").value("1.0"))