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 @@ -81,6 +81,7 @@ private boolean processToken(String jwtToken, HttpServletResponse response) thro
}

private void setErrorResponse(UserErrorCode errorCode, HttpServletResponse response) throws IOException {
log.warn(errorCode.name(), errorCode.getClientMessage());
response.setStatus(errorCode.getHttpStatusCode().value());
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,7 @@
import org.runimo.runimo.records.service.usecases.RecordCreateUsecase;
import org.runimo.runimo.records.service.usecases.RecordQueryUsecase;
import org.runimo.runimo.records.service.usecases.RecordUpdateUsecase;
import org.runimo.runimo.records.service.usecases.dtos.RecordCreateCommand;
import org.runimo.runimo.records.service.usecases.dtos.RecordDetailViewResponse;
import org.runimo.runimo.records.service.usecases.dtos.RecordSaveResponse;
import org.runimo.runimo.records.service.usecases.dtos.*;
import org.runimo.runimo.user.controller.UserId;
import org.runimo.runimo.records.service.dtos.WeeklyRecordStatResponse;
import org.runimo.runimo.records.service.dtos.WeeklyStatQuery;
Expand Down Expand Up @@ -103,4 +101,25 @@ public ResponseEntity<SuccessResponse<WeeklyRecordStatResponse>> queryWeeklyReco
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<SuccessResponse<MonthlyRecordStatResponse>> 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));
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +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.runimo.runimo.records.service.dtos.DailyStat;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.jpa.repository.JpaRepository;
Expand Down Expand Up @@ -33,13 +33,13 @@ 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)) " +
@Query("select new org.runimo.runimo.records.service.dtos.DailyStat(" +
"cast(r.startedAt as localdate), " +
"sum(r.totalDistance.amount)) " +
"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);
List<DailyStat> findDailyDistanceByUserIdAndThisWeek(Long userId, LocalDateTime startOfWeek, LocalDateTime now);

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
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.runimo.runimo.records.service.dtos.DailyStat;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Component;
Expand Down Expand Up @@ -49,7 +49,7 @@ public Optional<RunningRecord> findLatestRunningRecordByUserId(Long userId) {
}

@Transactional(readOnly = true)
public List<RunningRecordDistance> findDailyStatByUserIdBetween(Long id, LocalDateTime from, LocalDateTime to) {
public List<DailyStat> findDailyStatByUserIdBetween(Long id, LocalDateTime from, LocalDateTime to) {
return recordRepository.findDailyDistanceByUserIdAndThisWeek(id, from, to);
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package org.runimo.runimo.records.service.usecases;

import org.runimo.runimo.records.service.usecases.dtos.MonthlyRecordStatResponse;
import org.runimo.runimo.records.service.usecases.dtos.MonthlyStatQuery;
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);
MonthlyRecordStatResponse getUserMonthlyRecordStat(MonthlyStatQuery query);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,17 @@
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.usecases.dtos.MonthlyRecordStatResponse;
import org.runimo.runimo.records.service.usecases.dtos.MonthlyStatQuery;
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;
import org.springframework.stereotype.Service;

import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.temporal.TemporalAdjusters;
import java.util.*;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
Expand All @@ -30,27 +31,23 @@ public RecordDetailViewResponse getRecordDetailView(Long recordId) {
@Override
public WeeklyRecordStatResponse getUserWeeklyRecordStat(WeeklyStatQuery query) {
// DB에서 일별로 이미 합산된 데이터 조회
List<RunningRecordDistance> dailyDistances = recordFinder.findDailyStatByUserIdBetween(
List<DailyStat> dailyDistances = recordFinder.findDailyStatByUserIdBetween(
query.userId(),
query.startDate().atStartOfDay(),
query.endDate().atTime(23, 59, 59)
);
return new WeeklyRecordStatResponse(dailyDistances);
}

// 결과를 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);
@Override
public MonthlyRecordStatResponse getUserMonthlyRecordStat(MonthlyStatQuery query) {
LocalDate from = LocalDate.of(query.year(), query.month(), 1);
LocalDate to = from.with(TemporalAdjusters.lastDayOfMonth());
List<DailyStat> dailyDistances = recordFinder.findDailyStatByUserIdBetween(
query.userId(),
from.atStartOfDay(),
to.atTime(23, 59, 59)
);
return new MonthlyRecordStatResponse(dailyDistances);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package org.runimo.runimo.records.service.usecases.dtos;

import org.runimo.runimo.records.service.dtos.DailyStat;

import java.util.List;

public record MonthlyRecordStatResponse(
List<DailyStat> dailyStats
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.runimo.runimo.records.service.usecases.dtos;

public record MonthlyStatQuery(
Long userId,
Integer year,
Integer month
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -190,21 +190,77 @@ void tearDown() {
.log().ifValidationFails()
.statusCode(200)
.body("code", equalTo("MY_PAGE_DATA_FETCHED"))
.body("payload.daily_stats.size()", equalTo(7))
.body("payload.daily_stats.size()", equalTo(5))
.body("payload.daily_stats[0].date", equalTo("2025-03-31"))
.body("payload.daily_stats[0].distance", equalTo(1000))
.body("payload.daily_stats[1].date", equalTo("2025-04-01"))
.body("payload.daily_stats[1].distance", equalTo(2000))
.body("payload.daily_stats[2].date", equalTo("2025-04-02"))
.body("payload.daily_stats[2].distance", equalTo(0))
.body("payload.daily_stats[3].date", equalTo("2025-04-03"))
.body("payload.daily_stats[3].distance", equalTo(0))
.body("payload.daily_stats[4].date", equalTo("2025-04-04"))
.body("payload.daily_stats[4].distance", equalTo(5000))
.body("payload.daily_stats[5].date", equalTo("2025-04-05"))
.body("payload.daily_stats[5].distance", equalTo(6000))
.body("payload.daily_stats[6].date", equalTo("2025-04-06"))
.body("payload.daily_stats[6].distance", equalTo(7000));
.body("payload.daily_stats[2].date", equalTo("2025-04-04"))
.body("payload.daily_stats[2].distance", equalTo(5000))
.body("payload.daily_stats[3].date", equalTo("2025-04-05"))
.body("payload.daily_stats[3].distance", equalTo(6000))
.body("payload.daily_stats[4].date", equalTo("2025-04-06"))
.body("payload.daily_stats[4].distance", equalTo(7000));
}

@Test
@WithMockUser(username = USER_UUID)
@Sql(scripts = "/sql/weekly_record_data.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
void 월간_기록_통계_조회_성공시_정확한_정보를_반환한다() {
// given
String token = AUTH_HEADER_PREFIX + jwtTokenFactory.generateAccessToken(USER_UUID);

// when & then
given()
.header("Authorization", token)
.contentType(ContentType.JSON)
.param("year", 2025)
.param("month", 4)
.when()
.get("/api/v1/records/stats/monthly")
.then()
.log().all()
.statusCode(200)
.body("code", equalTo("MY_PAGE_DATA_FETCHED"))
.body("payload.daily_stats.size()", equalTo(6))
.body("payload.daily_stats[0].date", equalTo("2025-04-01"))
.body("payload.daily_stats[0].distance", equalTo(2000))
.body("payload.daily_stats[1].date", equalTo("2025-04-02"))
.body("payload.daily_stats[1].distance", equalTo(3000));
}

@Test
@WithMockUser(username = USER_UUID)
@Sql(scripts = "/sql/weekly_record_partial_data.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
void 월간_기록_통계_조회_잘못된_요청_데이터() {
// given
String token = AUTH_HEADER_PREFIX + jwtTokenFactory.generateAccessToken(USER_UUID);

// when & then
given()
.header("Authorization", token)
.contentType(ContentType.JSON)
.param("year", 2025)
.param("month", 13) // 잘못된 월
.when()
.get("/api/v1/records/stats/monthly")
.then()
.log().all()
.statusCode(400);
}

@Test
void 월간_기록_통계_조회_인증_실패() {
// when & then
given()
.contentType(ContentType.JSON)
.param("year", 2025)
.param("month", 4)
.when()
.get("/api/v1/records/stats/monthly")
.then()
.log().all()
.statusCode(401);
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.runimo.runimo.records.service.RecordFinder;
import org.runimo.runimo.records.service.dtos.RunningRecordDistance;
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.runimo.runimo.records.service.usecases.dtos.MonthlyRecordStatResponse;
import org.runimo.runimo.records.service.usecases.dtos.MonthlyStatQuery;

import java.time.DayOfWeek;
import java.time.LocalDate;
Expand Down Expand Up @@ -39,14 +40,14 @@ void setUp() {
LocalDateTime now = LocalDateTime.of(2025, 4, 6, 10, 0);
LocalDateTime startOfWeek = now.with(DayOfWeek.MONDAY).withHour(0).withMinute(0).withSecond(0);

List<RunningRecordDistance> savedData = List.of(
new RunningRecordDistance(1000L, LocalDate.of(2025, 3, 31)), // 월
new RunningRecordDistance(2000L, LocalDate.of(2025, 4, 1)), // 화
new RunningRecordDistance(3000L, LocalDate.of(2025, 4, 2)), // 수
new RunningRecordDistance(4000L, LocalDate.of(2025, 4, 3)), // 목
new RunningRecordDistance(5000L, LocalDate.of(2025, 4, 4)), // 금
new RunningRecordDistance(6000L, LocalDate.of(2025, 4, 5)), // 토
new RunningRecordDistance(7000L, LocalDate.of(2025, 4, 6)) // 일
List<DailyStat> savedData = List.of(
new DailyStat(LocalDate.of(2025, 3, 31), 1000L), // 월
new DailyStat(LocalDate.of(2025, 4, 1), 2000L), // 화
new DailyStat(LocalDate.of(2025, 4, 2), 3000L), // 수
new DailyStat(LocalDate.of(2025, 4, 3), 4000L), // 목
new DailyStat(LocalDate.of(2025, 4, 4), 5000L), // 금
new DailyStat(LocalDate.of(2025, 4, 5), 6000L), // 토
new DailyStat(LocalDate.of(2025, 4, 6), 7000L) // 일
);

when(recordFinder.findDailyStatByUserIdBetween(eq(1L), any(LocalDateTime.class), any(LocalDateTime.class)))
Expand Down Expand Up @@ -79,13 +80,12 @@ void setUp() {
LocalDateTime now = LocalDateTime.of(2025, 4, 6, 10, 0);
LocalDateTime startOfWeek = now.with(DayOfWeek.MONDAY).withHour(0).withMinute(0).withSecond(0);

List<RunningRecordDistance> savedData = List.of(
new RunningRecordDistance(1000L, LocalDate.of(2025, 3, 31)), // 월
new RunningRecordDistance(9000L, LocalDate.of(2025, 4, 1)), // 화
// 수, 목 데이터 없음
new RunningRecordDistance(5000L, LocalDate.of(2025, 4, 4)), // 금
new RunningRecordDistance(6000L, LocalDate.of(2025, 4, 5)), // 토
new RunningRecordDistance(7000L, LocalDate.of(2025, 4, 6)) // 일
List<DailyStat> savedData = List.of(
new DailyStat(LocalDate.of(2025, 3, 31), 1000L), // 월
new DailyStat(LocalDate.of(2025, 4, 1), 9000L), // 화
new DailyStat(LocalDate.of(2025, 4, 4), 5000L), // 금
new DailyStat(LocalDate.of(2025, 4, 5), 6000L), // 토
new DailyStat(LocalDate.of(2025, 4, 6), 7000L) // 일
);

when(recordFinder.findDailyStatByUserIdBetween(eq(1L), any(LocalDateTime.class), any(LocalDateTime.class)))
Expand All @@ -102,13 +102,36 @@ void setUp() {
List<DailyStat> expectedDistances = List.of(
new DailyStat(LocalDate.of(2025, 3, 31), 1000L),
new DailyStat(LocalDate.of(2025, 4, 1), 9000L),
new DailyStat(LocalDate.of(2025, 4, 2), 0L), // 데이터 없음
new DailyStat(LocalDate.of(2025, 4, 3), 0L), // 데이터 없음
new DailyStat(LocalDate.of(2025, 4, 4), 5000L),
new DailyStat(LocalDate.of(2025, 4, 5), 6000L),
new DailyStat(LocalDate.of(2025, 4, 6), 7000L)
);

assertEquals(expectedDistances, response.dailyStats());
}

@Test
void 월간_일별_달리기_거리_합() {
// given
MonthlyStatQuery query = new MonthlyStatQuery(1L, 2025, 4);

List<DailyStat> savedData = List.of(
new DailyStat(LocalDate.of(2025, 4, 1), 2000L), // 화
new DailyStat(LocalDate.of(2025, 4, 2), 3000L), // 수
new DailyStat(LocalDate.of(2025, 4, 3), 4000L), // 목
new DailyStat(LocalDate.of(2025, 4, 4), 5000L), // 금
new DailyStat(LocalDate.of(2025, 4, 5), 6000L), // 토
new DailyStat(LocalDate.of(2025, 4, 6), 7000L) // 일
);

when(recordFinder.findDailyStatByUserIdBetween(eq(1L), any(LocalDateTime.class), any(LocalDateTime.class)))
.thenReturn(savedData);

// when
MonthlyRecordStatResponse res = queryUsecase.getUserMonthlyRecordStat(query);

// then
assertEquals(savedData.size(), res.dailyStats().size());
assertEquals(2000L, res.dailyStats().getFirst().distance());
}
}