Skip to content

Commit

Permalink
Refactor: 달성 기록 API v1 리팩터링 (#309)
Browse files Browse the repository at this point in the history
* Refactor: API 버저닝을 위한 달성 기록 리팩터링

- 서비스 단에서 전체 코스 리스트(달성한, 달성하지 않은, 현재) 및 달성 기록의 정보를 리턴하도록 구현(CoursesDto 반환)
- 바뀐 서비스 단에 맞게 서비스 재 구성
- ScaleCoursesResponse 및 ScaleControllerV1에서 데이터 가공

* Test: /api/v1/scale/courses Response가 맞게 반환되었는지 확인
  • Loading branch information
hee9841 authored Nov 19, 2024
1 parent f3cfadb commit ee185e1
Show file tree
Hide file tree
Showing 6 changed files with 424 additions and 102 deletions.
109 changes: 54 additions & 55 deletions src/main/java/com/dnd/runus/application/scale/ScaleService.java
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
package com.dnd.runus.application.scale;

import com.dnd.runus.application.scale.dto.CoursesDto;
import com.dnd.runus.domain.member.Member;
import com.dnd.runus.domain.running.RunningRecordRepository;
import com.dnd.runus.domain.scale.*;
import com.dnd.runus.presentation.v1.scale.dto.ScaleCoursesResponse;
import com.dnd.runus.domain.scale.Scale;
import com.dnd.runus.domain.scale.ScaleAchievement;
import com.dnd.runus.domain.scale.ScaleAchievementLog;
import com.dnd.runus.domain.scale.ScaleAchievementRepository;
import com.dnd.runus.domain.scale.ScaleRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.List;

import static com.dnd.runus.global.constant.MetricsConversionFactor.METERS_IN_A_KILOMETER;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
Expand All @@ -36,65 +40,60 @@ public void saveScaleAchievements(Member member) {
}

@Transactional(readOnly = true)
public ScaleCoursesResponse getAchievements(long memberId) {
List<ScaleAchievementLog> scaleAchievementLogs = scaleAchievementRepository.findScaleAchievementLogs(memberId);

ScaleCoursesResponse.Info info = new ScaleCoursesResponse.Info(
scaleAchievementLogs.size(),
scaleAchievementLogs.stream()
.mapToInt(log -> log.scale().sizeMeter())
.sum());

return new ScaleCoursesResponse(
info,
getAchievedCourses(scaleAchievementLogs),
calculateCurrentScaleLeftMeter(scaleAchievementLogs, memberId));
}

private List<ScaleCoursesResponse.AchievedCourse> getAchievedCourses(
List<ScaleAchievementLog> scaleAchievementLogs) {
boolean hasAchievedCourse = scaleAchievementLogs.stream().anyMatch(log -> log.achievedDate() != null);
if (!hasAchievedCourse) {
return List.of();
public CoursesDto getAchievements(long memberId) {
int totalRunningDistanceMeter = runningRecordRepository.findTotalDistanceMeterByMemberId(memberId);
// 지구 한바퀴 전체 코스 조회
List<CoursesDto.Course> courses = convertToCoursesDto(
scaleAchievementRepository.findScaleAchievementLogs(memberId), totalRunningDistanceMeter);

List<CoursesDto.Course> achievedCourses =
courses.stream().filter(course -> course.achievedAt() != null).toList();

// 완주하지 못한 코스(현재 + 다음 코스)
List<CoursesDto.Course> notAchievedCourses =
courses.stream().filter(course -> course.achievedAt() == null).collect(Collectors.toList());

CoursesDto.Course currentCourse = null;
CoursesDto.Course lastCourse;

if (notAchievedCourses.isEmpty()) {
// 전체 코스를 완주했을 경우, 현재 코스에
lastCourse = achievedCourses.getLast();
} else {
currentCourse = notAchievedCourses.getFirst();
lastCourse = notAchievedCourses.getLast();

// nextCourses 값을 구하기 위해 현재 코스 삭제
notAchievedCourses.removeFirst();
}

return scaleAchievementLogs.stream()
.filter(log -> log.achievedDate() != null)
.map(log -> new ScaleCoursesResponse.AchievedCourse(
log.scale().name(),
log.scale().sizeMeter(),
log.achievedDate().toLocalDate()))
.toList();
return CoursesDto.builder()
.totalCoursesCount(lastCourse.order())
.totalCoursesDistanceMeter(lastCourse.requiredMeterForAchieve())
.myTotalRunningMeter(totalRunningDistanceMeter)
.achievedCourses(achievedCourses)
.currentCourse(currentCourse)
.nextCourses(notAchievedCourses)
.build();
}

private ScaleCoursesResponse.CurrentCourse calculateCurrentScaleLeftMeter(
List<ScaleAchievementLog> scaleAchievementLogs, long memberId) {
private List<CoursesDto.Course> convertToCoursesDto(
List<ScaleAchievementLog> achievementLogs, int totalRunningDistanceMeter) {

int memberRunMeterSum = runningRecordRepository.findTotalDistanceMeterByMemberId(memberId);
List<CoursesDto.Course> result = new ArrayList<>();
int requiredForAchieveSum = 0;

ScaleAchievementLog currentScale = scaleAchievementLogs.stream()
.filter(log -> log.achievedDate() == null)
.findFirst()
.orElse(null);
for (ScaleAchievementLog achievementLog : achievementLogs) {
// 해당 코스에서 사용자가 달성한 미터값 계산(아직 달성 못한 코스는 0으로, 달성한 코스는 sizeMeter로 들어감)
int achievedMeter = Math.min(
Math.max(0, totalRunningDistanceMeter - requiredForAchieveSum),
achievementLog.scale().sizeMeter());
// 해당 코스에서 달성하기 위해 필요한 전체 미터값 계산
requiredForAchieveSum += achievementLog.scale().sizeMeter();

if (currentScale == null) {
return new ScaleCoursesResponse.CurrentCourse("지구 한바퀴", 0, 0, "축하합니다! 지구 한바퀴 완주하셨네요!");
result.add(CoursesDto.Course.of(achievementLog, requiredForAchieveSum, achievedMeter));
}

int achievedCourseMeterSum = scaleAchievementLogs.stream()
.filter(log -> log.achievedDate() != null)
.mapToInt(log -> log.scale().sizeMeter())
.sum();

double remainingKm =
(currentScale.scale().sizeMeter() + achievedCourseMeterSum - memberRunMeterSum) / METERS_IN_A_KILOMETER;

String message = String.format("%s까지 %.1fkm 남았어요!", currentScale.scale().endName(), remainingKm);

return new ScaleCoursesResponse.CurrentCourse(
currentScale.scale().name(),
currentScale.scale().sizeMeter(),
memberRunMeterSum - achievedCourseMeterSum,
message);
return result;
}
}
61 changes: 61 additions & 0 deletions src/main/java/com/dnd/runus/application/scale/dto/CoursesDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.dnd.runus.application.scale.dto;


import com.dnd.runus.domain.scale.ScaleAchievementLog;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.OffsetDateTime;
import java.util.List;
import lombok.Builder;

@Builder
public record CoursesDto(
int totalCoursesCount,
int totalCoursesDistanceMeter,
int myTotalRunningMeter,
List<Course> achievedCourses,
Course currentCourse,
List<Course> nextCourses
) {

/**
* 코스 DTO
* @param order 코스 순서
* @param achievedMeter 사용자의 코스에 대한 달성 미터 값
* (ex. totalMeter가 1000, requiredMeterForAchieve가 3000,
* 사용자가 전체 달린 거리가 2600이라면
* achievedMeter는 600
* )
* @param sizeMeter 코스의 거리 미터 값
* @param requiredMeterForAchieve 코스를 달성하기 위한 미터값
* (ex. 2코스라면, 1코스 totalMeter + 2코스 totalMeter의 값)
* @param achievedAt 달성한 날짜, 달성하지 않으면 null
*/
@Schema(name = "course", description = "코스")
public record Course(
String name,
String StartName,
String endName,
int order,
int achievedMeter,
int sizeMeter,
int requiredMeterForAchieve,
OffsetDateTime achievedAt
) {
public static Course of(
ScaleAchievementLog scaleAchievementLog,
int requiredTotalMeterForAchieve,
int achievedMeter
) {
return new Course(
scaleAchievementLog.scale().name(),
scaleAchievementLog.scale().startName(),
scaleAchievementLog.scale().endName(),
scaleAchievementLog.scale().index(),
achievedMeter,
scaleAchievementLog.scale().sizeMeter(),
requiredTotalMeterForAchieve,
scaleAchievementLog.achievedDate()
);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.dnd.runus.presentation.v1.scale;

import com.dnd.runus.application.scale.ScaleService;
import com.dnd.runus.application.scale.dto.CoursesDto;
import com.dnd.runus.presentation.annotation.MemberId;
import com.dnd.runus.presentation.v1.scale.dto.ScaleCoursesResponse;
import io.swagger.v3.oas.annotations.Operation;
Expand Down Expand Up @@ -31,6 +32,22 @@ public class ScaleController {
- 달성한 코스가 있다면, 달성한 코스 목록과 현재 진행중인 코스 정보를 반환합니다.
""")
public ScaleCoursesResponse getCourses(@MemberId long memberId) {
return scaleService.getAchievements(memberId);
CoursesDto courses = scaleService.getAchievements(memberId);
boolean isCompleteAll = courses.currentCourse() == null;

return ScaleCoursesResponse.builder()
.info(new ScaleCoursesResponse.Info(courses.totalCoursesCount(), courses.totalCoursesDistanceMeter()))
.achievedCourses(courses.achievedCourses().stream()
.map(ScaleCoursesResponse.AchievedCourse::from)
.toList())
.currentCourse(
isCompleteAll
? new ScaleCoursesResponse.CurrentCourse(
"지구 한바퀴",
courses.totalCoursesDistanceMeter(),
courses.myTotalRunningMeter(),
"축하합니다! 지구 한바퀴 완주하셨네요!")
: ScaleCoursesResponse.CurrentCourse.from(courses.currentCourse()))
.build();
}
}
Original file line number Diff line number Diff line change
@@ -1,77 +1,113 @@
package com.dnd.runus.presentation.v1.scale.dto;

import com.dnd.runus.application.scale.dto.CoursesDto;
import io.swagger.v3.oas.annotations.media.Schema;

import java.text.DecimalFormat;
import java.time.LocalDate;
import java.util.List;
import lombok.Builder;

import static com.dnd.runus.global.constant.MetricsConversionFactor.METERS_IN_A_KILOMETER;

@Builder
public record ScaleCoursesResponse(
Info info,
List<AchievedCourse> achievedCourses,
CurrentCourse currentCourse
) {

private static final DecimalFormat KILO_METER_FORMATTER = new DecimalFormat("0.#km");
private static final DecimalFormat KILO_METER_FORMATTER = new DecimalFormat("#,###.#km");

@Schema(name = "course Info", description = "코스 정보")
/**
* 거리 formater
*
* @return distanceMeter가 1km보다 작을 경우 "m"로 아니면 "#,###.#km"형식으로 반환합니다.
*/
private static String formaterForDistance(int distanceMeter) {
if (distanceMeter < METERS_IN_A_KILOMETER) {
return distanceMeter + "m";
}
return KILO_METER_FORMATTER.format(distanceMeter / METERS_IN_A_KILOMETER);
}


private static String makeMessageAboutLeftKm(String goalPoint, int leftMeter) {
//소수점 둘째자리에서 반올림
double leftKm = Math.round((leftMeter / METERS_IN_A_KILOMETER) * 10.0) / 10.0;
return goalPoint + "까지 " + KILO_METER_FORMATTER.format(leftKm) + " 남았어요!";
}



@Schema(name = "courseResponseV1 Info", description = "코스 정보")
public record Info(
@Schema(description = "총 코스 수 (공개되지 않은 코스 포함)", example = "18")
int totalCourses,
@Schema(description = "총 코스 거리 (공개되지 않은 코스 포함)", example = "1000km")
String totalDistance
@Schema(description = "총 코스 수 (공개되지 않은 코스 포함)", example = "18")
int totalCourses,
@Schema(description = "총 코스 거리 (공개되지 않은 코스 포함)", example = "1000km")
String totalDistance
) {
public Info(
int totalCourses,
int totalMeter
int totalCourses,
int totalMeter
) {
this(totalCourses, KILO_METER_FORMATTER.format(totalMeter / METERS_IN_A_KILOMETER));
this(totalCourses, formaterForDistance(totalMeter));
}
}

@Schema(name = "courseResponseV1 AchievedCourse", description = "달성한 코스")
public record AchievedCourse(
@Schema(description = "코스 이름", example = "서울에서 인천")
String name,
@Schema(description = "코스 총 거리", example = "30km")
String totalDistance,
@Schema(description = "달성 일자")
LocalDate achievedAt
@Schema(description = "코스 이름", example = "서울에서 인천")
String name,
@Schema(description = "코스 총 거리", example = "30km")
String totalDistance,
@Schema(description = "달성 일자")
LocalDate achievedAt
) {
public AchievedCourse(
String name,
int totalMeter,
LocalDate achievedAt
) {
this(name, KILO_METER_FORMATTER.format(totalMeter / METERS_IN_A_KILOMETER), achievedAt);
public static ScaleCoursesResponse.AchievedCourse from(CoursesDto.Course achievedCourse) {
return new ScaleCoursesResponse.AchievedCourse(
achievedCourse.name(),
formaterForDistance(achievedCourse.sizeMeter()),
achievedCourse.achievedAt().toLocalDate()
);
}
}

@Schema(name = "courseResponseV1 CurrentCourse", description = "현재 코스")
public record CurrentCourse(
@Schema(description = "현재 코스 이름", example = "서울에서 부산")
String name,
@Schema(description = "현재 코스 총 거리", example = "200km")
String totalDistance,
@Schema(description = "현재 달성한 거리, 현재 32.3km 달성", example = "32.3km")
String achievedDistance,
@Schema(description = "현재 코스 설명 메시지", example = "대전까지 100km 남았어요!")
String message
@Schema(description = "현재 코스 이름", example = "서울에서 부산")
String name,
@Schema(description = "현재 코스 총 거리", example = "200km")
String totalDistance,
@Schema(description = "현재 달성한 거리, 현재 32.3km 달성", example = "32.3km")
String achievedDistance,
@Schema(description = "현재 코스 설명 메시지", example = "대전까지 100km 남았어요!")
String message
) {
public CurrentCourse(
String name,
int totalMeter,
int achievedMeter,
String message
String name,
int totalDistanceMeter,
int achievedDistanceMeter,
String message
) {
this(name, KILO_METER_FORMATTER.format(totalMeter / METERS_IN_A_KILOMETER), formatAchievedDistance(achievedMeter), message);
this(
name,
formaterForDistance(totalDistanceMeter),
formaterForDistance(achievedDistanceMeter),
message
);
}

private static String formatAchievedDistance(int achievedMeter) {
if (achievedMeter < METERS_IN_A_KILOMETER) {
return achievedMeter + "m";
}
return KILO_METER_FORMATTER.format(achievedMeter / METERS_IN_A_KILOMETER);
public static ScaleCoursesResponse.CurrentCourse from(CoursesDto.Course course) {
return new ScaleCoursesResponse.CurrentCourse(
course.name(),
formaterForDistance(course.sizeMeter()),
formaterForDistance(course.achievedMeter()),
makeMessageAboutLeftKm(
course.endName(),
Math.max(0, course.sizeMeter() - course.achievedMeter())
)
);
}
}
}
Loading

0 comments on commit ee185e1

Please sign in to comment.