diff --git a/src/main/java/org/runimo/runimo/common/response/PageData.java b/src/main/java/org/runimo/runimo/common/response/PageData.java new file mode 100644 index 00000000..5d354728 --- /dev/null +++ b/src/main/java/org/runimo/runimo/common/response/PageData.java @@ -0,0 +1,20 @@ +package org.runimo.runimo.common.response; + +import java.util.List; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class PageData { + + private List items; + private PageInfo pagination; + + public static PageData of(List items, PageInfo pagination) { + return new PageData<>(items, pagination); + } +} diff --git a/src/main/java/org/runimo/runimo/common/response/PageInfo.java b/src/main/java/org/runimo/runimo/common/response/PageInfo.java new file mode 100644 index 00000000..6d982dff --- /dev/null +++ b/src/main/java/org/runimo/runimo/common/response/PageInfo.java @@ -0,0 +1,29 @@ +package org.runimo.runimo.common.response; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.domain.Page; + +@Getter +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class PageInfo { + + + private Long totalItems; + private Integer totalPages; + private Integer currentPage; + private Integer perPage; + + public static PageInfo empty(Integer currentPage, Integer perPage) { + return new PageInfo(0L, 0, currentPage, perPage); + } + + + public static PageInfo of(Page pages) { + + return new PageInfo(pages.getTotalElements(), pages.getTotalPages(), pages.getNumber(), pages.getSize()); + } +} diff --git a/src/main/java/org/runimo/runimo/common/response/SuccessPageResponse.java b/src/main/java/org/runimo/runimo/common/response/SuccessPageResponse.java new file mode 100644 index 00000000..1b4539cc --- /dev/null +++ b/src/main/java/org/runimo/runimo/common/response/SuccessPageResponse.java @@ -0,0 +1,30 @@ +package org.runimo.runimo.common.response; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.runimo.runimo.exceptions.code.CustomResponseCode; + +@Getter +@NoArgsConstructor(access = lombok.AccessLevel.PROTECTED) +public class SuccessPageResponse extends Response { + + private PageData payload; + + public SuccessPageResponse(boolean success, String message, String code, + PageData payload) { + super(success, message, code); + this.payload = payload; + } + + public SuccessPageResponse(boolean success, CustomResponseCode code, + PageData payload) { + super(success, code); + this.payload = payload; + } + + public static SuccessPageResponse of(final CustomResponseCode responseCode, + PageData data) { + return new SuccessPageResponse<>(true, responseCode, data); + } + +} diff --git a/src/main/java/org/runimo/runimo/records/controller/RecordController.java b/src/main/java/org/runimo/runimo/records/controller/RecordController.java index fd66e323..a79490bd 100644 --- a/src/main/java/org/runimo/runimo/records/controller/RecordController.java +++ b/src/main/java/org/runimo/runimo/records/controller/RecordController.java @@ -10,10 +10,13 @@ import java.net.URI; import java.time.LocalDate; import lombok.RequiredArgsConstructor; +import org.runimo.runimo.common.response.SuccessPageResponse; import org.runimo.runimo.common.response.SuccessResponse; +import org.runimo.runimo.records.controller.request.MyRecordPageRequest; import org.runimo.runimo.records.controller.request.RecordSaveRequest; import org.runimo.runimo.records.controller.request.RecordUpdateRequest; import org.runimo.runimo.records.enums.RecordHttpResponse; +import org.runimo.runimo.records.service.dto.RecordSimpleView; import org.runimo.runimo.records.service.dto.RecordSimpleViewResponse; import org.runimo.runimo.records.service.dto.WeeklyRecordStatResponse; import org.runimo.runimo.records.service.dto.WeeklyStatQuery; @@ -29,6 +32,7 @@ import org.runimo.runimo.user.enums.UserHttpResponseCode; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -43,118 +47,118 @@ @RequiredArgsConstructor public class RecordController { - private final RecordCreateUsecase recordCreateUsecase; - private final RecordUpdateUsecase recordUpdateUsecase; - private final RecordQueryUsecase recordQueryUsecase; + private final RecordCreateUsecase recordCreateUsecase; + private final RecordUpdateUsecase recordUpdateUsecase; + private final RecordQueryUsecase recordQueryUsecase; - @Operation(summary = "기록 저장", description = "기록을 저장합니다.") - @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "기록 저장 성공", - content = @Content(schema = @Schema(implementation = RecordSaveResponse.class))), - @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터"), - @ApiResponse(responseCode = "401", description = "인증 실패") - }) - @PostMapping - public ResponseEntity> saveRecord( - @RequestBody RecordSaveRequest request, - @UserId Long userId - ) { - RecordSaveResponse response = recordCreateUsecase.execute( - RecordCreateCommand.from(request, userId)); - return ResponseEntity.created(URI.create("/api/v1/records/" + response.savedId())) - .body(SuccessResponse.of(RecordHttpResponse.RECORD_SAVED, response)); - } + @Operation(summary = "기록 저장", description = "기록을 저장합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "기록 저장 성공", + content = @Content(schema = @Schema(implementation = RecordSaveResponse.class))), + @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터"), + @ApiResponse(responseCode = "401", description = "인증 실패") + }) + @PostMapping + public ResponseEntity> saveRecord( + @RequestBody RecordSaveRequest request, + @UserId Long userId + ) { + RecordSaveResponse response = recordCreateUsecase.execute( + RecordCreateCommand.from(request, userId)); + return ResponseEntity.created(URI.create("/api/v1/records/" + response.savedId())) + .body(SuccessResponse.of(RecordHttpResponse.RECORD_SAVED, response)); + } - @Operation(summary = "기록 조회", description = "기록을 조회합니다.") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "기록 조회 성공", - content = @Content(schema = @Schema(implementation = RecordDetailViewResponse.class))), - @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터"), - @ApiResponse(responseCode = "401", description = "인증 실패") - }) - @GetMapping("/{recordId}") - public ResponseEntity> viewRecord( - @PathVariable String recordId - ) { - RecordDetailViewResponse response = recordQueryUsecase.getRecordDetailView(recordId); - return ResponseEntity.ok(SuccessResponse.of(RecordHttpResponse.RECORD_FETCHED, response)); - } + @Operation(summary = "기록 조회", description = "기록을 조회합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "기록 조회 성공", + content = @Content(schema = @Schema(implementation = RecordDetailViewResponse.class))), + @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터"), + @ApiResponse(responseCode = "401", description = "인증 실패") + }) + @GetMapping("/{recordId}") + public ResponseEntity> viewRecord( + @PathVariable String recordId + ) { + RecordDetailViewResponse response = recordQueryUsecase.getRecordDetailView(recordId); + return ResponseEntity.ok(SuccessResponse.of(RecordHttpResponse.RECORD_FETCHED, response)); + } - @Operation(summary = "기록 수정", description = "기록을 수정합니다.") - @ApiResponses( - value = { - @ApiResponse(responseCode = "200", description = "기록 수정 성공"), - @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터"), - @ApiResponse(responseCode = "401", description = "인증 실패") - } - ) - @PatchMapping("/{recordId}") - public ResponseEntity updateRecord( - @RequestBody RecordUpdateRequest request, - @NotNull @PathVariable String recordId, - @UserId Long userId - ) { - recordUpdateUsecase.updateRecord(RecordUpdateRequest.toCommand(userId, recordId, request)); - return ResponseEntity.ok().build(); - } + @Operation(summary = "기록 수정", description = "기록을 수정합니다.") + @ApiResponses( + value = { + @ApiResponse(responseCode = "200", description = "기록 수정 성공"), + @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터"), + @ApiResponse(responseCode = "401", description = "인증 실패") + } + ) + @PatchMapping("/{recordId}") + public ResponseEntity updateRecord( + @RequestBody RecordUpdateRequest request, + @NotNull @PathVariable String recordId, + @UserId Long userId + ) { + recordUpdateUsecase.updateRecord(RecordUpdateRequest.toCommand(userId, recordId, 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> 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)); - } + @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> 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)); + } - @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/monthly") - public ResponseEntity> queryMonthlyRecordStat( - @RequestParam int year, - @RequestParam int month, - @UserId Long userId) { - if (month < 1 || month > 12) { - throw new IllegalArgumentException("잘못된 월입니다."); - } - MonthlyRecordStatResponse response = recordQueryUsecase.getUserMonthlyRecordStat( - new MonthlyStatQuery(userId, year, month)); - return ResponseEntity.ok( - SuccessResponse.of(UserHttpResponseCode.MY_PAGE_DATA_FETCHED, response)); + @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/monthly") + public ResponseEntity> queryMonthlyRecordStat( + @RequestParam int year, + @RequestParam int month, + @UserId Long userId) { + if (month < 1 || month > 12) { + throw new IllegalArgumentException("잘못된 월입니다."); } + MonthlyRecordStatResponse response = recordQueryUsecase.getUserMonthlyRecordStat( + new MonthlyStatQuery(userId, year, month)); + return ResponseEntity.ok( + SuccessResponse.of(UserHttpResponseCode.MY_PAGE_DATA_FETCHED, response)); + } - @Operation(summary = "개인 기록 페이지네이션 전체 조회", description = "개인 기록 페이지네이션 조회") - @ApiResponse(responseCode = "200", description = "기록 조회 성공", - content = @Content(schema = @Schema(implementation = RecordSimpleViewResponse.class))) - @GetMapping("/me") - public ResponseEntity> getMyRecordList( - @RequestParam(defaultValue = "0") int page, - @RequestParam(defaultValue = "10") int size, - @UserId Long userId - ) { - return ResponseEntity.ok( - SuccessResponse.of( - RecordHttpResponse.RECORD_FETCHED, - recordQueryUsecase.getUserRecordSimpleView(userId, page, size) - ) - ); - } + @Operation(summary = "개인 기록 페이지네이션 전체 조회", description = "개인 기록 페이지네이션 조회") + @ApiResponse(responseCode = "200", description = "기록 조회 성공", + content = @Content(schema = @Schema(implementation = RecordSimpleViewResponse.class))) + @GetMapping("/me") + public ResponseEntity> getMyRecordList( + @ModelAttribute MyRecordPageRequest request, + @UserId Long userId + ) { + return ResponseEntity.ok( + SuccessPageResponse.of( + RecordHttpResponse.RECORD_FETCHED, + recordQueryUsecase.getUserRecordSimpleViewByMonth( + MyRecordPageRequest.toQuery(request, userId)) + ) + ); + } } diff --git a/src/main/java/org/runimo/runimo/records/controller/request/MyRecordPageRequest.java b/src/main/java/org/runimo/runimo/records/controller/request/MyRecordPageRequest.java new file mode 100644 index 00000000..9a61897e --- /dev/null +++ b/src/main/java/org/runimo/runimo/records/controller/request/MyRecordPageRequest.java @@ -0,0 +1,26 @@ +package org.runimo.runimo.records.controller.request; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import java.time.LocalDate; +import lombok.Getter; +import lombok.Setter; +import org.runimo.runimo.records.service.dto.RecordQuery; + +@Getter +@Setter +public class MyRecordPageRequest { + + @Min(0) + private Integer page = 0; + @Min(1) + @Max(20) + private Integer size = 10; + private LocalDate startDate = LocalDate.now(); + private LocalDate endDate = LocalDate.now(); + + public static RecordQuery toQuery(MyRecordPageRequest request, Long userId) { + return new RecordQuery(userId, request.getPage(), request.getSize(), + request.getStartDate(), request.getEndDate()); + } +} diff --git a/src/main/java/org/runimo/runimo/records/repository/RecordRepository.java b/src/main/java/org/runimo/runimo/records/repository/RecordRepository.java index 151f2068..89716dae 100644 --- a/src/main/java/org/runimo/runimo/records/repository/RecordRepository.java +++ b/src/main/java/org/runimo/runimo/records/repository/RecordRepository.java @@ -5,6 +5,7 @@ import java.util.Optional; import org.runimo.runimo.records.domain.RunningRecord; import org.runimo.runimo.records.service.dto.RecordStatDto; +import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.data.jpa.repository.JpaRepository; @@ -15,34 +16,43 @@ @Repository public interface RecordRepository extends JpaRepository { - Optional findByRecordPublicId(String id); - - @Query("SELECT r FROM RunningRecord r " + - "WHERE r.userId = :userId " + - "AND r.startedAt BETWEEN :startOfWeek AND :now") - Slice findFirstRunOfWeek( - @Param("userId") Long userId, - @Param("startOfWeek") LocalDateTime startOfWeek, - @Param("now") LocalDateTime now, - Pageable pageable - ); - - @Query("SELECT COUNT(r.id) FROM RunningRecord r WHERE r.userId = :id") - Long countByUserId(Long id); - - @Query("select r from RunningRecord r where r.userId = :userId") - Slice findLatestByUserId(Long userId, Pageable pageRequest); - - @Query("select new org.runimo.runimo.records.service.dto.RecordStatDto(" + - "r.startedAt, " + - "r.endAt, " + - "r.totalDistance.amount) " + - "from RunningRecord r " + - "where r.userId = :userId " + - "and r.startedAt between :startOfWeek and :now " + - "order by r.startedAt asc") - List findRecordStatByUserIdAndBetween(Long userId, LocalDateTime startOfWeek, - LocalDateTime now); - - List findRecordByUserIdOrderByStartedAtDesc(Long id, Pageable pageable); + Optional findByRecordPublicId(String id); + + @Query("SELECT r FROM RunningRecord r " + + "WHERE r.userId = :userId " + + "AND r.startedAt BETWEEN :startOfWeek AND :now") + Slice findFirstRunOfWeek( + @Param("userId") Long userId, + @Param("startOfWeek") LocalDateTime startOfWeek, + @Param("now") LocalDateTime now, + Pageable pageable + ); + + @Query("SELECT COUNT(r.id) FROM RunningRecord r WHERE r.userId = :id") + Long countByUserId(Long id); + + @Query("select r from RunningRecord r where r.userId = :userId") + Slice findLatestByUserId(Long userId, Pageable pageRequest); + + @Query("select new org.runimo.runimo.records.service.dto.RecordStatDto(" + + "r.startedAt, " + + "r.endAt, " + + "r.totalDistance.amount) " + + "from RunningRecord r " + + "where r.userId = :userId " + + "and r.startedAt between :startOfWeek and :now " + + "order by r.startedAt asc") + List findRecordStatByUserIdAndBetween(Long userId, LocalDateTime startOfWeek, + LocalDateTime now); + + Page findRecordByUserIdOrderByStartedAtDesc(Long id, Pageable pageable); + + + @Query("select r " + + "from RunningRecord r " + + "where r.userId = :userId " + + "and r.startedAt between :from and :to " + + "order by r.startedAt asc") + Page findRecordByUserIdAndBetween(Long userId, LocalDateTime from, + LocalDateTime to, Pageable pageRequest); } diff --git a/src/main/java/org/runimo/runimo/records/service/RecordFinder.java b/src/main/java/org/runimo/runimo/records/service/RecordFinder.java index b30645d2..13f0e2db 100644 --- a/src/main/java/org/runimo/runimo/records/service/RecordFinder.java +++ b/src/main/java/org/runimo/runimo/records/service/RecordFinder.java @@ -1,15 +1,19 @@ package org.runimo.runimo.records.service; +import java.time.LocalDate; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; import java.util.Optional; import lombok.RequiredArgsConstructor; +import org.runimo.runimo.common.response.PageData; +import org.runimo.runimo.common.response.PageInfo; import org.runimo.runimo.records.domain.RunningRecord; import org.runimo.runimo.records.repository.RecordRepository; import org.runimo.runimo.records.service.dto.DailyStat; import org.runimo.runimo.records.service.dto.RecordSimpleView; import org.runimo.runimo.records.service.dto.RecordStatDto; +import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; @@ -20,71 +24,78 @@ @RequiredArgsConstructor public class RecordFinder { - private final RecordRepository recordRepository; + private final RecordRepository recordRepository; - @Transactional(readOnly = true) - public Optional findById(Long id) { - return recordRepository.findById(id); - } + @Transactional(readOnly = true) + public Optional findById(Long id) { + return recordRepository.findById(id); + } - @Transactional(readOnly = true) - public Optional findByPublicId(String id) { - return recordRepository.findByRecordPublicId(id); - } + @Transactional(readOnly = true) + public Optional findByPublicId(String id) { + return recordRepository.findByRecordPublicId(id); + } - @Transactional(readOnly = true) - public Optional findFirstRunOfCurrentWeek(Long userId) { - LocalDateTime now = LocalDateTime.now(); - LocalDateTime startOfWeek = now.minusDays(now.getDayOfWeek().getValue() - 1L).withHour(0) - .withMinute(0).withSecond(0); - PageRequest pageRequest = PageRequest.of(0, 1, Sort.by("startedAt").ascending()); - return recordRepository.findFirstRunOfWeek(userId, startOfWeek, now, pageRequest).stream() - .findFirst(); - } + @Transactional(readOnly = true) + public Optional findFirstRunOfCurrentWeek(Long userId) { + LocalDateTime now = LocalDateTime.now(); + LocalDateTime startOfWeek = now.minusDays(now.getDayOfWeek().getValue() - 1L).withHour(0) + .withMinute(0).withSecond(0); + PageRequest pageRequest = PageRequest.of(0, 1, Sort.by("startedAt").ascending()); + return recordRepository.findFirstRunOfWeek(userId, startOfWeek, now, pageRequest).stream() + .findFirst(); + } - @Transactional(readOnly = true) - public Long countByUserId(Long userId) { - return recordRepository.countByUserId(userId); - } + @Transactional(readOnly = true) + public Optional findLatestRunningRecordByUserId(Long userId) { + PageRequest pageRequest = PageRequest.of(0, 1, Sort.by("startedAt").descending()); + return recordRepository.findLatestByUserId(userId, pageRequest).stream().findFirst(); + } - @Transactional(readOnly = true) - public Optional 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 findDailyStatByUserIdBetween(Long id, LocalDateTime from, + LocalDateTime to) { + return mapToDailyStatList( + recordRepository.findRecordStatByUserIdAndBetween(id, from, to)); + } - @Transactional(readOnly = true) - public List findDailyStatByUserIdBetween(Long id, LocalDateTime from, - LocalDateTime to) { - return mapToDailyStatList( - recordRepository.findRecordStatByUserIdAndBetween(id, from, to)); - } + @Transactional(readOnly = true) + public PageData findRecordSimpleViewByUserIdBetween(Long userId, LocalDate from, + LocalDate to, Pageable pageRequest) { + Page pages = recordRepository.findRecordByUserIdAndBetween( + userId, from.atStartOfDay(), to.atTime(23, 59, 59), pageRequest); + return mapToPageData(pages, pageRequest); + } - @Transactional(readOnly = true) - public List findRecordSimpleViewByUserId(Long id, int page, int size) { - Pageable pageable = PageRequest.of(page, size); - return recordRepository - .findRecordByUserIdOrderByStartedAtDesc(id, pageable) - .stream() - .map(RecordSimpleView::from) - .toList(); + private PageData mapToPageData(Page pages, + Pageable pageRequest) { + if (pages.isEmpty()) { + return new PageData<>(List.of(), + PageInfo.empty(pageRequest.getPageNumber(), pageRequest.getPageSize())); } + return new PageData<>( + pages.getContent().stream() + .map(RecordSimpleView::from) + .toList(), + PageInfo.of(pages) + ); + } - private List mapToDailyStatList(List recordStatDtos) { - List list = new ArrayList<>(); - for (RecordStatDto recordStatDto : recordStatDtos) { - if (list.isEmpty() || !list.getLast().getDate().equals(recordStatDto.getStartedAt() - .toLocalDate())) { - DailyStat dailyStat = DailyStat.empty(recordStatDto.getStartedAt().toLocalDate()); - dailyStat.addData(recordStatDto); - list.add(dailyStat); - continue; - } - DailyStat last = list.getLast(); - last.addData(recordStatDto); - } - return list; + private List mapToDailyStatList(List recordStatDtos) { + List list = new ArrayList<>(); + for (RecordStatDto recordStatDto : recordStatDtos) { + if (list.isEmpty() || !list.getLast().getDate().equals(recordStatDto.getStartedAt() + .toLocalDate())) { + DailyStat dailyStat = DailyStat.empty(recordStatDto.getStartedAt().toLocalDate()); + dailyStat.addData(recordStatDto); + list.add(dailyStat); + continue; + } + DailyStat last = list.getLast(); + last.addData(recordStatDto); } + return list; + } } diff --git a/src/main/java/org/runimo/runimo/records/service/dto/RecordQuery.java b/src/main/java/org/runimo/runimo/records/service/dto/RecordQuery.java new file mode 100644 index 00000000..28301d45 --- /dev/null +++ b/src/main/java/org/runimo/runimo/records/service/dto/RecordQuery.java @@ -0,0 +1,22 @@ +package org.runimo.runimo.records.service.dto; + +import java.time.LocalDate; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; + +@Getter +@AllArgsConstructor +public class RecordQuery { + + private final Long userId; + private final Integer page; + private final Integer size; + private final LocalDate startDate; + private final LocalDate endDate; + + public Pageable toPageable() { + return PageRequest.of(page, size); + } +} diff --git a/src/main/java/org/runimo/runimo/records/service/usecases/RecordQueryUsecase.java b/src/main/java/org/runimo/runimo/records/service/usecases/RecordQueryUsecase.java index 028fa713..fb72f43f 100644 --- a/src/main/java/org/runimo/runimo/records/service/usecases/RecordQueryUsecase.java +++ b/src/main/java/org/runimo/runimo/records/service/usecases/RecordQueryUsecase.java @@ -1,6 +1,8 @@ package org.runimo.runimo.records.service.usecases; -import org.runimo.runimo.records.service.dto.RecordSimpleViewResponse; +import org.runimo.runimo.common.response.PageData; +import org.runimo.runimo.records.service.dto.RecordQuery; +import org.runimo.runimo.records.service.dto.RecordSimpleView; import org.runimo.runimo.records.service.dto.WeeklyRecordStatResponse; import org.runimo.runimo.records.service.dto.WeeklyStatQuery; import org.runimo.runimo.records.service.usecases.dtos.MonthlyRecordStatResponse; @@ -9,11 +11,11 @@ public interface RecordQueryUsecase { - RecordDetailViewResponse getRecordDetailView(String publicId); + RecordDetailViewResponse getRecordDetailView(String publicId); - WeeklyRecordStatResponse getUserWeeklyRecordStat(WeeklyStatQuery query); + WeeklyRecordStatResponse getUserWeeklyRecordStat(WeeklyStatQuery query); - MonthlyRecordStatResponse getUserMonthlyRecordStat(MonthlyStatQuery query); + MonthlyRecordStatResponse getUserMonthlyRecordStat(MonthlyStatQuery query); - RecordSimpleViewResponse getUserRecordSimpleView(Long id, int page, int size); + PageData getUserRecordSimpleViewByMonth(RecordQuery query); } diff --git a/src/main/java/org/runimo/runimo/records/service/usecases/RecordQueryUsecaseImpl.java b/src/main/java/org/runimo/runimo/records/service/usecases/RecordQueryUsecaseImpl.java index 7b4bcaad..d3a8c4cf 100644 --- a/src/main/java/org/runimo/runimo/records/service/usecases/RecordQueryUsecaseImpl.java +++ b/src/main/java/org/runimo/runimo/records/service/usecases/RecordQueryUsecaseImpl.java @@ -5,11 +5,12 @@ import java.util.List; import java.util.NoSuchElementException; import lombok.RequiredArgsConstructor; +import org.runimo.runimo.common.response.PageData; import org.runimo.runimo.records.domain.RunningRecord; import org.runimo.runimo.records.service.RecordFinder; import org.runimo.runimo.records.service.dto.DailyStat; +import org.runimo.runimo.records.service.dto.RecordQuery; import org.runimo.runimo.records.service.dto.RecordSimpleView; -import org.runimo.runimo.records.service.dto.RecordSimpleViewResponse; import org.runimo.runimo.records.service.dto.SimpleStat; import org.runimo.runimo.records.service.dto.WeeklyRecordStatResponse; import org.runimo.runimo.records.service.dto.WeeklyStatQuery; @@ -22,45 +23,51 @@ @RequiredArgsConstructor public class RecordQueryUsecaseImpl implements RecordQueryUsecase { - private final RecordFinder recordFinder; + private final RecordFinder recordFinder; - @Override - public RecordDetailViewResponse getRecordDetailView(String recordId) { - RunningRecord runningRecord = recordFinder.findByPublicId(recordId) - .orElseThrow(NoSuchElementException::new); - return RecordDetailViewResponse.from(runningRecord); - } + @Override + public RecordDetailViewResponse getRecordDetailView(String recordId) { + RunningRecord runningRecord = recordFinder.findByPublicId(recordId) + .orElseThrow(NoSuchElementException::new); + return RecordDetailViewResponse.from(runningRecord); + } - @Override - public WeeklyRecordStatResponse getUserWeeklyRecordStat(WeeklyStatQuery query) { - List dailyDistances = recordFinder.findDailyStatByUserIdBetween( - query.userId(), - query.startDate().atStartOfDay(), - query.endDate().atTime(23, 59, 59) - ); + @Override + public WeeklyRecordStatResponse getUserWeeklyRecordStat(WeeklyStatQuery query) { + List dailyDistances = recordFinder.findDailyStatByUserIdBetween( + query.userId(), + query.startDate().atStartOfDay(), + query.endDate().atTime(23, 59, 59) + ); - SimpleStat weeklyStat = SimpleStat.from(dailyDistances); + SimpleStat weeklyStat = SimpleStat.from(dailyDistances); - return new WeeklyRecordStatResponse(weeklyStat, dailyDistances); - } + return new WeeklyRecordStatResponse(weeklyStat, dailyDistances); + } - @Override - public MonthlyRecordStatResponse getUserMonthlyRecordStat(MonthlyStatQuery query) { - LocalDate from = LocalDate.of(query.year(), query.month(), 1); - LocalDate to = from.with(TemporalAdjusters.lastDayOfMonth()); - List dailyDistances = recordFinder.findDailyStatByUserIdBetween( - query.userId(), - from.atStartOfDay(), - to.atTime(23, 59, 59) - ); - SimpleStat monthlyStat = SimpleStat.from(dailyDistances); - return new MonthlyRecordStatResponse(monthlyStat, dailyDistances); - } + @Override + public MonthlyRecordStatResponse getUserMonthlyRecordStat(MonthlyStatQuery query) { + LocalDate from = LocalDate.of(query.year(), query.month(), 1); + LocalDate to = from.with(TemporalAdjusters.lastDayOfMonth()); + List dailyDistances = recordFinder.findDailyStatByUserIdBetween( + query.userId(), + from.atStartOfDay(), + to.atTime(23, 59, 59) + ); + SimpleStat monthlyStat = SimpleStat.from(dailyDistances); + return new MonthlyRecordStatResponse(monthlyStat, dailyDistances); + } - @Override - public RecordSimpleViewResponse getUserRecordSimpleView(Long id, int page, int size) { - List recordSimpleViews = recordFinder.findRecordSimpleViewByUserId(id, - page, size); - return new RecordSimpleViewResponse(recordSimpleViews); - } + @Override + public PageData getUserRecordSimpleViewByMonth(RecordQuery query) { + LocalDate from = query.getStartDate().withDayOfMonth(1); + LocalDate to = query.getEndDate().with(TemporalAdjusters.lastDayOfMonth()); + + return recordFinder.findRecordSimpleViewByUserIdBetween( + query.getUserId(), + from, + to, + query.toPageable() + ); + } } diff --git a/src/test/java/org/runimo/runimo/records/api/RecordAcceptanceTest.java b/src/test/java/org/runimo/runimo/records/api/RecordAcceptanceTest.java index 21624c7e..97c321a1 100644 --- a/src/test/java/org/runimo/runimo/records/api/RecordAcceptanceTest.java +++ b/src/test/java/org/runimo/runimo/records/api/RecordAcceptanceTest.java @@ -286,6 +286,8 @@ void tearDown() { given() .contentType(ContentType.JSON) .header("Authorization", token) + .param("startDate", "2025-04-01") + .param("endDate", "2025-04-30") .param("page", 0) .param("size", 5) .when() @@ -293,7 +295,35 @@ void tearDown() { .then() .log().all() .statusCode(HttpStatus.OK.value()) - .body("payload.record_list.size()", equalTo(5)); + .body("payload.pagination.total_pages", equalTo(2)) + .body("payload.pagination.per_page", equalTo(5)) + .body("payload.pagination.current_page", equalTo(0)) + .body("payload.pagination.total_items", equalTo(7)) + .body("payload.items.size()", equalTo(5)); + } + + @Test + @Sql(scripts = "/sql/weekly_record_data.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + void 사용자_기록_페이지네이션_조회_결과_없음() { + + String token = AUTH_HEADER_PREFIX + jwtTokenFactory.generateAccessToken(USER_UUID); + + given() + .contentType(ContentType.JSON) + .header("Authorization", token) + .param("startDate", "2010-01-01") + .param("endDate", "2010-01-30") + .param("size", 5) + .when() + .get("/api/v1/records/me") + .then() + .log().all() + .statusCode(HttpStatus.OK.value()) + .body("payload.pagination.total_pages", equalTo(0)) + .body("payload.pagination.per_page", equalTo(5)) + .body("payload.pagination.current_page", equalTo(0)) + .body("payload.pagination.total_items", equalTo(0)) + .body("payload.items.size()", equalTo(0)); } @Test