Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,14 @@
import org.runimo.runimo.records.service.usecases.dtos.RecordDetailViewResponse;
import org.runimo.runimo.records.service.usecases.dtos.RecordSaveResponse;
import org.runimo.runimo.user.controller.UserId;
import org.runimo.runimo.records.service.dtos.WeeklyRecordStatResponse;
import org.runimo.runimo.records.service.dtos.WeeklyStatQuery;
import org.runimo.runimo.user.enums.UserHttpResponseCode;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.net.URI;
import java.time.LocalDate;

@Tag(name = "RECORD", description = "기록 관련 API")
@RequestMapping("/api/v1/records")
Expand Down Expand Up @@ -81,4 +85,22 @@ public ResponseEntity<Void> updateRecord(
recordUpdateUsecase.updateRecord(RecordUpdateRequest.toCommand(userId, request));
return ResponseEntity.ok().build();
}

@Operation(summary = "주간 기록 통계 조회" , description = "요청한 기간의 주간 기록 통계를 조회")
@ApiResponses(
value = {
@ApiResponse(responseCode = "200", description = "주간 기록 통계 조회 성공",
content = @Content(schema = @Schema(implementation = WeeklyRecordStatResponse.class))),
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터"),
@ApiResponse(responseCode = "401", description = "인증 실패")
}
)
@GetMapping("/stats/weekly")
public ResponseEntity<SuccessResponse<WeeklyRecordStatResponse>> queryWeeklyRecordStat(
@RequestParam LocalDate startDate,
@RequestParam LocalDate endDate,
@UserId Long userId) {
WeeklyRecordStatResponse response = recordQueryUsecase.getUserWeeklyRecordStat(new WeeklyStatQuery(userId, startDate, endDate));
return ResponseEntity.ok(SuccessResponse.of(UserHttpResponseCode.MY_PAGE_DATA_FETCHED, response));
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package org.runimo.runimo.records.domain;


import com.fasterxml.jackson.annotation.JsonUnwrapped;
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Builder;
Expand Down Expand Up @@ -93,6 +92,7 @@ public Distance getTotalDistance() {
public Duration getRunningTime() {
return Duration.between(startedAt, endAt);
}

private void validateEditor(Long editorId) {
if (editorId == null || !Objects.equals(this.userId, editorId)) {
throw new IllegalArgumentException("Invalid editor id");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@

import java.util.List;

public class SegmentPaceConverter implements AttributeConverter<List<SegmentPace>, String>
{
private ObjectMapper objectMapper = new ObjectMapper();
public class SegmentPaceConverter implements AttributeConverter<List<SegmentPace>, String> {
private final ObjectMapper objectMapper = new ObjectMapper();

@Override
public String convertToDatabaseColumn(List<SegmentPace> segmentPaces) {
try {
Expand All @@ -21,9 +21,10 @@ public String convertToDatabaseColumn(List<SegmentPace> segmentPaces) {

@Override
public List<SegmentPace> convertToEntityAttribute(String s) {
TypeReference<List<SegmentPace>> typeRef = new TypeReference<List<SegmentPace>>() {};
TypeReference<List<SegmentPace>> typeRef = new TypeReference<List<SegmentPace>>() {
};
try {
if(s == null || s.isEmpty()) {
if (s == null || s.isEmpty()) {
return List.of();
}
return objectMapper.readValue(s, typeRef);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
package org.runimo.runimo.records.enums;

import org.runimo.runimo.exceptions.code.CustomResponseCode;
import org.springframework.http.HttpStatus;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.runimo.runimo.records.repository;

import org.runimo.runimo.records.domain.RunningRecord;
import org.runimo.runimo.records.service.dtos.RunningRecordDistance;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.jpa.repository.JpaRepository;
Expand All @@ -9,6 +10,7 @@
import org.springframework.stereotype.Repository;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;

@Repository
Expand All @@ -30,4 +32,14 @@ Slice<RunningRecord> findFirstRunOfWeek(

@Query("select r from RunningRecord r where r.userId = :userId")
Slice<RunningRecord> findLatestByUserId(Long userId, Pageable pageRequest);

@Query("select new org.runimo.runimo.records.service.dtos.RunningRecordDistance(" +
"sum(r.totalDistance.amount), " +
"cast(r.startedAt as localdate)) " +
"from RunningRecord r " +
"where r.userId = :userId " +
"and r.startedAt between :startOfWeek and :now " +
"group by function('date', r.startedAt)")
List<RunningRecordDistance> findDailyDistanceByUserIdAndThisWeek(Long userId, LocalDateTime startOfWeek, LocalDateTime now);

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
import lombok.RequiredArgsConstructor;
import org.runimo.runimo.records.domain.RunningRecord;
import org.runimo.runimo.records.repository.RecordRepository;
import org.runimo.runimo.records.service.dtos.RunningRecordDistance;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;

@Component
Expand Down Expand Up @@ -45,4 +47,9 @@ public Optional<RunningRecord> findLatestRunningRecordByUserId(Long userId) {
PageRequest pageRequest = PageRequest.of(0, 1, Sort.by("startedAt").descending());
return recordRepository.findLatestByUserId(userId, pageRequest).stream().findFirst();
}

@Transactional(readOnly = true)
public List<RunningRecordDistance> findDailyStatByUserIdBetween(Long id, LocalDateTime from, LocalDateTime to) {
return recordRepository.findDailyDistanceByUserIdAndThisWeek(id, from, to);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.runimo.runimo.records.service.dtos;


import java.time.LocalDate;

public record DailyStat(
LocalDate date,
Long distance
) {
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.runimo.runimo.records.service.dtos;

import java.time.DayOfWeek;
import java.time.LocalDate;

public record RunningRecordDistance(
Long totalDistanceInMeters,
LocalDate date
) {
public DayOfWeek getDayOfWeek() {
return date.getDayOfWeek();
}
public DailyStat toDailyStat() {
return new DailyStat(date, totalDistanceInMeters);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.runimo.runimo.records.service.dtos;

import io.swagger.v3.oas.annotations.media.Schema;

import java.util.List;

@Schema(name = "주간 통계 응답")
public record WeeklyRecordStatResponse(
List<DailyStat> dailyStats
) {
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package org.runimo.runimo.records.service.dtos;

import java.time.LocalDate;

public record WeeklyStatQuery(Long userId, LocalDate startDate, LocalDate endDate) {
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package org.runimo.runimo.records.service.usecases;

import org.runimo.runimo.records.service.usecases.dtos.RecordDetailViewResponse;
import org.runimo.runimo.records.service.dtos.WeeklyRecordStatResponse;
import org.runimo.runimo.records.service.dtos.WeeklyStatQuery;

public interface RecordQueryUsecase {
RecordDetailViewResponse getRecordDetailView(Long publicId);
WeeklyRecordStatResponse getUserWeeklyRecordStat(WeeklyStatQuery query);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,16 @@
import lombok.RequiredArgsConstructor;
import org.runimo.runimo.records.domain.RunningRecord;
import org.runimo.runimo.records.service.RecordFinder;
import org.runimo.runimo.records.service.dtos.RunningRecordDistance;
import org.runimo.runimo.records.service.usecases.dtos.RecordDetailViewResponse;
import org.runimo.runimo.records.service.dtos.DailyStat;
import org.runimo.runimo.records.service.dtos.WeeklyRecordStatResponse;
import org.runimo.runimo.records.service.dtos.WeeklyStatQuery;
import org.springframework.stereotype.Service;

import java.util.NoSuchElementException;
import java.time.DayOfWeek;
import java.util.*;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
Expand All @@ -20,4 +26,31 @@ public RecordDetailViewResponse getRecordDetailView(Long recordId) {
.orElseThrow(NoSuchElementException::new);
return RecordDetailViewResponse.from(runningRecord);
}

@Override
public WeeklyRecordStatResponse getUserWeeklyRecordStat(WeeklyStatQuery query) {
// DB에서 일별로 이미 합산된 데이터 조회
List<RunningRecordDistance> dailyDistances = recordFinder.findDailyStatByUserIdBetween(
query.userId(),
query.startDate().atStartOfDay(),
query.endDate().atTime(23, 59, 59)
);

// 결과를 DayOfWeek 맵으로 변환
Map<DayOfWeek, DailyStat> dailyStatsMap = dailyDistances.stream()
.collect(Collectors.toMap(
RunningRecordDistance::getDayOfWeek,
RunningRecordDistance::toDailyStat
));

// 월요일부터 일요일까지 순서대로 일간 데이터 정리
List<DailyStat> weeklyStats = Arrays.stream(DayOfWeek.values())
.map(day -> dailyStatsMap.getOrDefault(day, new DailyStat(
query.startDate().with(day),
0L
)))
.toList();

return new WeeklyRecordStatResponse(weeklyStats);
}
}
Loading