Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor: 달성 기록 API v1 리팩터링 #309

Merged
merged 2 commits into from
Nov 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 = "코스")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

application 계층에 있는 dto 클래스는 스웨거(컨트롤러 계층)이 없어야 할 것 같아요!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

V2 만드는 PR에 해당 부분 수정해서 올릴게요!

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