From 648959ed1f5200d416d685b8de71522e49fbcab4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=95=9C=EA=B4=80=ED=9D=AC?= <85067003+limehee@users.noreply.github.com> Date: Thu, 2 Jan 2025 18:01:53 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=B3=B4=ED=96=89=20=ED=9B=88=EB=A0=A8?= =?UTF-8?q?=20=EA=B8=B0=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20API=EC=97=90=20?= =?UTF-8?q?=EC=A0=95=ED=99=95=EB=8F=84=20=ED=8F=89=EA=B7=A0=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EC=B6=94=EA=B0=80=20=EC=99=84=EB=A3=8C=20(#95)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../stempo/controller/RecordController.java | 20 +-- .../controller/RecordControllerTest.java | 144 +++++++++--------- .../stempo/dto/response/RecordItemDto.java | 23 +++ .../dto/response/RecordResponseDto.java | 25 +-- .../com/stempo/mapper/RecordDtoMapper.java | 31 ++-- .../stempo/service/AuthenticationService.java | 10 +- .../com/stempo/service/RecordService.java | 5 +- .../com/stempo/service/RecordServiceImpl.java | 47 +++--- .../stempo/mapper/RecordDtoMapperTest.java | 29 +++- .../stempo/service/RecordServiceImplTest.java | 87 +++++++++-- 10 files changed, 278 insertions(+), 143 deletions(-) create mode 100644 stempo-application/src/main/java/com/stempo/dto/response/RecordItemDto.java diff --git a/stempo-api/src/main/java/com/stempo/controller/RecordController.java b/stempo-api/src/main/java/com/stempo/controller/RecordController.java index 427c0f60..b3987f0f 100644 --- a/stempo-api/src/main/java/com/stempo/controller/RecordController.java +++ b/stempo-api/src/main/java/com/stempo/controller/RecordController.java @@ -10,7 +10,6 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import java.time.LocalDate; -import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.GetMapping; @@ -30,25 +29,26 @@ public class RecordController { @SuccessApiResponse(data = "deviceTag", dataType = String.class, dataDescription = "사용자의 디바이스 식별자") @PreAuthorize("hasRole('USER')") @PostMapping("/api/v1/records") - public ApiResponse record( - @Valid @RequestBody RecordRequestDto requestDto + public ApiResponse recordTrainingData( + @Valid @RequestBody RecordRequestDto requestDto ) { - String deviceTag = recordService.record(requestDto); + String deviceTag = recordService.recordTrainingData(requestDto); return ApiResponse.success(deviceTag); } @Operation(summary = "[U] 내 보행 훈련 기록 조회", description = "ROLE_USER 이상의 권한이 필요함
" + "startDate와 endDate는 yyyy-MM-dd 형식으로 입력해야 함
" + "startDate 이전의 가장 최신 데이터와 startDate부터 endDate까지의 데이터를 가져옴
" - + "데이터가 없을 경우 startDate 이전 날짜, 정확도 0으로 설정하여 반환") + + "데이터가 없을 경우 startDate 이전 날짜, 정확도 0으로 설정하여 반환" + + "정확도 평균은 소수점 첫째 자리에서 반올림된 값으로 반환") @PreAuthorize("hasRole('USER')") @GetMapping("/api/v1/records") - public ApiResponse> getRecords( - @RequestParam(name = "startDate") LocalDate startDate, - @RequestParam(name = "endDate") LocalDate endDate + public ApiResponse getRecords( + @RequestParam(name = "startDate") LocalDate startDate, + @RequestParam(name = "endDate") LocalDate endDate ) { - List records = recordService.getRecordsByDateRange(startDate, endDate); - return ApiResponse.success(records); + RecordResponseDto recordsByDateRange = recordService.getRecordsByDateRange(startDate, endDate); + return ApiResponse.success(recordsByDateRange); } @Operation(summary = "[U] 내 보행 훈련 기록 통계", description = "ROLE_USER 이상의 권한이 필요함") diff --git a/stempo-api/src/test/java/com/stempo/controller/RecordControllerTest.java b/stempo-api/src/test/java/com/stempo/controller/RecordControllerTest.java index 61bf1a03..0e240f8e 100644 --- a/stempo-api/src/test/java/com/stempo/controller/RecordControllerTest.java +++ b/stempo-api/src/test/java/com/stempo/controller/RecordControllerTest.java @@ -9,6 +9,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.stempo.dto.request.RecordRequestDto; +import com.stempo.dto.response.RecordItemDto; import com.stempo.dto.response.RecordResponseDto; import com.stempo.dto.response.RecordStatisticsResponseDto; import com.stempo.service.RecordService; @@ -28,7 +29,7 @@ @WebMvcTest(controllers = RecordController.class) @ContextConfiguration(classes = TestApplication.class) @ActiveProfiles("test") -public class RecordControllerTest { +class RecordControllerTest { @Autowired private MockMvc mockMvc; @@ -50,17 +51,17 @@ public class RecordControllerTest { String expectedDeviceTag = "device123"; - when(recordService.record(any(RecordRequestDto.class))) - .thenReturn(expectedDeviceTag); + when(recordService.recordTrainingData(any(RecordRequestDto.class))) + .thenReturn(expectedDeviceTag); // when mockMvc.perform(post("/api/v1/records") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(requestDto))) - // then - .andExpect(status().isOk()) - .andExpect(jsonPath("$.success").value(true)) - .andExpect(jsonPath("$.data").value(expectedDeviceTag)); + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(requestDto))) + // then + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.data").value(expectedDeviceTag)); } @Test @@ -74,12 +75,12 @@ public class RecordControllerTest { // when mockMvc.perform(post("/api/v1/records") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(requestDto))) - // then - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect(jsonPath("$.data").isEmpty()); + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(requestDto))) + // then + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.data").isEmpty()); } @Test @@ -92,10 +93,10 @@ public class RecordControllerTest { // when mockMvc.perform(post("/api/v1/records") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(requestDto))) - // then - .andExpect(status().isUnauthorized()); + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(requestDto))) + // then + .andExpect(status().isUnauthorized()); } @Test @@ -105,52 +106,59 @@ public class RecordControllerTest { LocalDate startDate = LocalDate.of(2024, 10, 1); LocalDate endDate = LocalDate.of(2024, 10, 31); - List expectedRecords = List.of( - RecordResponseDto.builder() - .accuracy(95.5) - .duration(30) - .steps(5000) - .date(LocalDate.of(2024, 10, 15)) - .build(), - RecordResponseDto.builder() - .accuracy(90.0) - .duration(25) - .steps(4500) - .date(LocalDate.of(2024, 10, 20)) - .build() + List recordItems = List.of( + RecordItemDto.builder() + .accuracy(95.5) + .duration(30) + .steps(5000) + .date(LocalDate.of(2024, 10, 15)) + .build(), + RecordItemDto.builder() + .accuracy(90.0) + .duration(25) + .steps(4500) + .date(LocalDate.of(2024, 10, 20)) + .build() ); + int accuracyAverage = 93; + + RecordResponseDto responseDto = RecordResponseDto.builder() + .accuracyAverage(accuracyAverage) + .records(recordItems) + .build(); + when(recordService.getRecordsByDateRange(startDate, endDate)) - .thenReturn(expectedRecords); + .thenReturn(responseDto); // when mockMvc.perform(get("/api/v1/records") - .param("startDate", "2024-10-01") - .param("endDate", "2024-10-31") - .contentType(MediaType.APPLICATION_JSON)) - // then - .andExpect(status().isOk()) - .andExpect(jsonPath("$.success").value(true)) - .andExpect(jsonPath("$.data").isArray()) - .andExpect(jsonPath("$.data[0].accuracy").value(95.5)) - .andExpect(jsonPath("$.data[0].duration").value(30)) - .andExpect(jsonPath("$.data[0].steps").value(5000)) - .andExpect(jsonPath("$.data[0].date").value("2024-10-15")) - .andExpect(jsonPath("$.data[1].accuracy").value(90.0)) - .andExpect(jsonPath("$.data[1].duration").value(25)) - .andExpect(jsonPath("$.data[1].steps").value(4500)) - .andExpect(jsonPath("$.data[1].date").value("2024-10-20")); + .param("startDate", "2024-10-01") + .param("endDate", "2024-10-31") + .contentType(MediaType.APPLICATION_JSON)) + // then + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.data.accuracyAverage").value(93)) + .andExpect(jsonPath("$.data.records[0].accuracy").value(95.5)) + .andExpect(jsonPath("$.data.records[0].duration").value(30)) + .andExpect(jsonPath("$.data.records[0].steps").value(5000)) + .andExpect(jsonPath("$.data.records[0].date").value("2024-10-15")) + .andExpect(jsonPath("$.data.records[1].accuracy").value(90.0)) + .andExpect(jsonPath("$.data.records[1].duration").value(25)) + .andExpect(jsonPath("$.data.records[1].steps").value(4500)) + .andExpect(jsonPath("$.data.records[1].date").value("2024-10-20")); } @Test void 인증되지_않은_사용자가_보행_훈련_기록을_조회시_권한에러가_발생한다() throws Exception { // when mockMvc.perform(get("/api/v1/records") - .param("startDate", "2024-10-01") - .param("endDate", "2024-10-31") - .contentType(MediaType.APPLICATION_JSON)) - // then - .andExpect(status().isUnauthorized()); + .param("startDate", "2024-10-01") + .param("endDate", "2024-10-31") + .contentType(MediaType.APPLICATION_JSON)) + // then + .andExpect(status().isUnauthorized()); } @Test @@ -158,31 +166,31 @@ public class RecordControllerTest { void 정상적으로_보행_훈련_기록_통계를_조회한다() throws Exception { // given RecordStatisticsResponseDto expectedStatistics = RecordStatisticsResponseDto.builder() - .todayWalkTrainingCount(10) - .weeklyWalkTrainingCount(50) - .consecutiveWalkTrainingDays(5) - .build(); + .todayWalkTrainingCount(10) + .weeklyWalkTrainingCount(50) + .consecutiveWalkTrainingDays(5) + .build(); when(recordService.getRecordStatistics()) - .thenReturn(expectedStatistics); + .thenReturn(expectedStatistics); // when mockMvc.perform(get("/api/v1/records/statistics") - .contentType(MediaType.APPLICATION_JSON)) - // then - .andExpect(status().isOk()) - .andExpect(jsonPath("$.success").value(true)) - .andExpect(jsonPath("$.data.todayWalkTrainingCount").value(10)) - .andExpect(jsonPath("$.data.weeklyWalkTrainingCount").value(50)) - .andExpect(jsonPath("$.data.consecutiveWalkTrainingDays").value(5)); + .contentType(MediaType.APPLICATION_JSON)) + // then + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.data.todayWalkTrainingCount").value(10)) + .andExpect(jsonPath("$.data.weeklyWalkTrainingCount").value(50)) + .andExpect(jsonPath("$.data.consecutiveWalkTrainingDays").value(5)); } @Test void 인증되지_않은_사용자가_보행_훈련_기록_통계를_조회시_권한에러가_발생한다() throws Exception { // when mockMvc.perform(get("/api/v1/records/statistics") - .contentType(MediaType.APPLICATION_JSON)) - // then - .andExpect(status().isUnauthorized()); + .contentType(MediaType.APPLICATION_JSON)) + // then + .andExpect(status().isUnauthorized()); } } diff --git a/stempo-application/src/main/java/com/stempo/dto/response/RecordItemDto.java b/stempo-application/src/main/java/com/stempo/dto/response/RecordItemDto.java new file mode 100644 index 00000000..d8557ed3 --- /dev/null +++ b/stempo-application/src/main/java/com/stempo/dto/response/RecordItemDto.java @@ -0,0 +1,23 @@ +package com.stempo.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalDate; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class RecordItemDto { + + @Schema(description = "정확도", example = "0.0") + private Double accuracy; + + @Schema(description = "재활 운동 시간(초)", example = "0") + private Integer duration; + + @Schema(description = "걸음 수", example = "0") + private Integer steps; + + @Schema(description = "날짜", example = "2024-01-01") + private LocalDate date; +} diff --git a/stempo-application/src/main/java/com/stempo/dto/response/RecordResponseDto.java b/stempo-application/src/main/java/com/stempo/dto/response/RecordResponseDto.java index 04eec73f..f0d44ff7 100644 --- a/stempo-application/src/main/java/com/stempo/dto/response/RecordResponseDto.java +++ b/stempo-application/src/main/java/com/stempo/dto/response/RecordResponseDto.java @@ -1,7 +1,7 @@ package com.stempo.dto.response; import io.swagger.v3.oas.annotations.media.Schema; -import java.time.LocalDate; +import java.util.List; import lombok.Builder; import lombok.Getter; @@ -9,15 +9,18 @@ @Builder public class RecordResponseDto { - @Schema(description = "정확도", example = "0.0") - private Double accuracy; + @Schema(description = "정확도 평균", example = "0") + private Integer accuracyAverage; - @Schema(description = "재활 운동 시간(초)", example = "0") - private Integer duration; - - @Schema(description = "걸음 수", example = "0") - private Integer steps; - - @Schema(description = "날짜", example = "2024-01-01") - private LocalDate date; + @Schema(description = "보행 훈련 기록", example = """ + [ + { + "accuracy": 0.0, + "duration": 0, + "steps": 0, + "date": "2025-01-01" + } + ] + """) + private List records; } diff --git a/stempo-application/src/main/java/com/stempo/mapper/RecordDtoMapper.java b/stempo-application/src/main/java/com/stempo/mapper/RecordDtoMapper.java index 45adf429..a2b2e9d1 100644 --- a/stempo-application/src/main/java/com/stempo/mapper/RecordDtoMapper.java +++ b/stempo-application/src/main/java/com/stempo/mapper/RecordDtoMapper.java @@ -1,28 +1,37 @@ package com.stempo.mapper; +import com.stempo.dto.response.RecordItemDto; import com.stempo.dto.response.RecordResponseDto; import com.stempo.dto.response.RecordStatisticsResponseDto; import java.time.LocalDate; +import java.util.List; import org.springframework.stereotype.Component; @Component public class RecordDtoMapper { - public RecordResponseDto toDto(Double accuracy, Integer duration, Integer steps, LocalDate date) { + public RecordResponseDto toDto(int accuracyAverage, List records) { return RecordResponseDto.builder() - .accuracy(accuracy) - .duration(duration) - .steps(steps) - .date(date) - .build(); + .accuracyAverage(accuracyAverage) + .records(records) + .build(); + } + + public RecordItemDto toDto(Double accuracy, Integer duration, Integer steps, LocalDate date) { + return RecordItemDto.builder() + .accuracy(accuracy) + .duration(duration) + .steps(steps) + .date(date) + .build(); } public RecordStatisticsResponseDto toDto(int todayWalkTrainingCount, int weeklyWalkTrainingCount, - int consecutiveWalkTrainingDays) { + int consecutiveWalkTrainingDays) { return RecordStatisticsResponseDto.builder() - .todayWalkTrainingCount(todayWalkTrainingCount) - .weeklyWalkTrainingCount(weeklyWalkTrainingCount) - .consecutiveWalkTrainingDays(consecutiveWalkTrainingDays) - .build(); + .todayWalkTrainingCount(todayWalkTrainingCount) + .weeklyWalkTrainingCount(weeklyWalkTrainingCount) + .consecutiveWalkTrainingDays(consecutiveWalkTrainingDays) + .build(); } } diff --git a/stempo-application/src/main/java/com/stempo/service/AuthenticationService.java b/stempo-application/src/main/java/com/stempo/service/AuthenticationService.java index 8b1134a9..dfe3fd9e 100644 --- a/stempo-application/src/main/java/com/stempo/service/AuthenticationService.java +++ b/stempo-application/src/main/java/com/stempo/service/AuthenticationService.java @@ -10,7 +10,6 @@ import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor @@ -21,9 +20,8 @@ public class AuthenticationService { private final EncryptionUtils encryptionUtils; private final AesConfig aesConfig; - @Transactional public Object login(AuthRequestDto requestDto, JwtTokenService tokenService, - TotpAuthenticatorService authenticatorService) { + TotpAuthenticatorService authenticatorService) { String deviceTag = encryptDeviceTag(requestDto.getDeviceTag()); userService.handleAccountLock(deviceTag); @@ -31,7 +29,7 @@ public Object login(AuthRequestDto requestDto, JwtTokenService tokenService, } private Object attemptAuthentication(String deviceTag, String password, JwtTokenService tokenService, - TotpAuthenticatorService authenticatorService) { + TotpAuthenticatorService authenticatorService) { try { Authentication authentication = performAuthentication(deviceTag, password); userService.resetFailedAttempts(deviceTag); @@ -45,12 +43,12 @@ private Object attemptAuthentication(String deviceTag, String password, JwtToken private Authentication performAuthentication(String deviceTag, String password) { UsernamePasswordAuthenticationToken authenticationToken = - new UsernamePasswordAuthenticationToken(deviceTag, password); + new UsernamePasswordAuthenticationToken(deviceTag, password); return authenticationManager.authenticate(authenticationToken); } private Object handleTwoFactorIfRequired(String deviceTag, Authentication authentication, - TotpAuthenticatorService authenticatorService, JwtTokenService tokenService) { + TotpAuthenticatorService authenticatorService, JwtTokenService tokenService) { boolean isAdmin = userService.getById(deviceTag).isAdmin(); boolean hasAuthenticator = authenticatorService.isAuthenticatorExist(deviceTag); diff --git a/stempo-application/src/main/java/com/stempo/service/RecordService.java b/stempo-application/src/main/java/com/stempo/service/RecordService.java index 4611c697..a3d9ab5e 100644 --- a/stempo-application/src/main/java/com/stempo/service/RecordService.java +++ b/stempo-application/src/main/java/com/stempo/service/RecordService.java @@ -4,13 +4,12 @@ import com.stempo.dto.response.RecordResponseDto; import com.stempo.dto.response.RecordStatisticsResponseDto; import java.time.LocalDate; -import java.util.List; public interface RecordService { - String record(RecordRequestDto requestDto); + String recordTrainingData(RecordRequestDto requestDto); - List getRecordsByDateRange(LocalDate startDate, LocalDate endDate); + RecordResponseDto getRecordsByDateRange(LocalDate startDate, LocalDate endDate); RecordStatisticsResponseDto getRecordStatistics(); } diff --git a/stempo-application/src/main/java/com/stempo/service/RecordServiceImpl.java b/stempo-application/src/main/java/com/stempo/service/RecordServiceImpl.java index f63452cf..0de1badd 100644 --- a/stempo-application/src/main/java/com/stempo/service/RecordServiceImpl.java +++ b/stempo-application/src/main/java/com/stempo/service/RecordServiceImpl.java @@ -1,6 +1,7 @@ package com.stempo.service; import com.stempo.dto.request.RecordRequestDto; +import com.stempo.dto.response.RecordItemDto; import com.stempo.dto.response.RecordResponseDto; import com.stempo.dto.response.RecordStatisticsResponseDto; import com.stempo.mapper.RecordDtoMapper; @@ -29,19 +30,19 @@ public class RecordServiceImpl implements RecordService { @Override @Transactional - public String record(RecordRequestDto requestDto) { + public String recordTrainingData(RecordRequestDto requestDto) { String deviceTag = userService.getCurrentDeviceTag(); String encryptedAccuracy = encryptionUtils.encrypt(requestDto.getAccuracy().toString()); String encryptedDuration = encryptionUtils.encrypt(requestDto.getDuration().toString()); String encryptedSteps = encryptionUtils.encrypt(requestDto.getSteps().toString()); - Record record = Record.create(deviceTag, encryptedAccuracy, encryptedDuration, encryptedSteps); - return recordRepository.save(record).getDeviceTag(); + Record newRecord = Record.create(deviceTag, encryptedAccuracy, encryptedDuration, encryptedSteps); + return recordRepository.save(newRecord).getDeviceTag(); } @Override @Transactional(readOnly = true) - public List getRecordsByDateRange(LocalDate startDate, LocalDate endDate) { + public RecordResponseDto getRecordsByDateRange(LocalDate startDate, LocalDate endDate) { String deviceTag = userService.getCurrentDeviceTag(); LocalDateTime startDateTime = startDate.atStartOfDay(); LocalDateTime endDateTime = endDate.atStartOfDay().plusDays(1); @@ -51,19 +52,25 @@ public List getRecordsByDateRange(LocalDate startDate, LocalD // startDateTime과 endDateTime 사이의 데이터 가져오기 List records = recordRepository.findByDateBetween(deviceTag, startDateTime, endDateTime); + List decryptedRecords = records.stream() + .map(this::convertToDecryptedRecordItemDto) + .toList(); // 결과 합치기 - List combinedRecords = new ArrayList<>(); + List combinedRecords = new ArrayList<>(); latestBeforeStartDate.ifPresentOrElse( - record -> combinedRecords.add(convertToDecryptedDto(record)), - () -> combinedRecords.add(mapper.toDto(0.0, 0, 0, startDate.minusDays(1))) + latestTrainingRecord -> combinedRecords.add(convertToDecryptedRecordItemDto(latestTrainingRecord)), + () -> combinedRecords.add(mapper.toDto(0.0, 0, 0, startDate.minusDays(1))) ); + combinedRecords.addAll(decryptedRecords); - combinedRecords.addAll(records.stream() - .map(this::convertToDecryptedDto) - .toList()); + // 정확도 평균 계산 + int accuracyAverage = (int) Math.ceil(decryptedRecords.stream() + .mapToDouble(RecordItemDto::getAccuracy) + .average() + .orElse(0.0)); - return combinedRecords; + return mapper.toDto(accuracyAverage, combinedRecords); } @Override @@ -74,16 +81,16 @@ public RecordStatisticsResponseDto getRecordStatistics() { LocalDateTime todayStartDateTime = LocalDate.now().atStartOfDay(); LocalDateTime todayEndDateTime = todayStartDateTime.plusDays(1); LocalDateTime weekStartDateTime = LocalDate.now() - .with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)) - .atStartOfDay(); + .with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)) + .atStartOfDay(); // 오늘의 훈련 횟수 계산 int todayWalkTrainingCount = recordRepository.countByDeviceTagAndCreatedAtBetween( - deviceTag, todayStartDateTime, todayEndDateTime); + deviceTag, todayStartDateTime, todayEndDateTime); // 이번 주 훈련 횟수 계산 (월요일부터 오늘까지) int weeklyWalkTrainingCount = recordRepository.countByDeviceTagAndCreatedAtBetween( - deviceTag, weekStartDateTime, todayEndDateTime); + deviceTag, weekStartDateTime, todayEndDateTime); // 연속된 훈련 일수 계산 int consecutiveWalkTrainingDays = calculateConsecutiveTrainingDays(deviceTag); @@ -91,11 +98,11 @@ public RecordStatisticsResponseDto getRecordStatistics() { return mapper.toDto(todayWalkTrainingCount, weeklyWalkTrainingCount, consecutiveWalkTrainingDays); } - private RecordResponseDto convertToDecryptedDto(Record record) { - Double decryptedAccuracy = Double.parseDouble(encryptionUtils.decrypt(record.getAccuracy())); - Integer decryptedDuration = Integer.parseInt(encryptionUtils.decrypt(record.getDuration())); - Integer decryptedSteps = Integer.parseInt(encryptionUtils.decrypt(record.getSteps())); - LocalDate date = record.getCreatedAt().toLocalDate(); + private RecordItemDto convertToDecryptedRecordItemDto(Record trainingRecord) { + Double decryptedAccuracy = Double.parseDouble(encryptionUtils.decrypt(trainingRecord.getAccuracy())); + Integer decryptedDuration = Integer.parseInt(encryptionUtils.decrypt(trainingRecord.getDuration())); + Integer decryptedSteps = Integer.parseInt(encryptionUtils.decrypt(trainingRecord.getSteps())); + LocalDate date = trainingRecord.getCreatedAt().toLocalDate(); return mapper.toDto(decryptedAccuracy, decryptedDuration, decryptedSteps, date); } diff --git a/stempo-application/src/test/java/com/stempo/mapper/RecordDtoMapperTest.java b/stempo-application/src/test/java/com/stempo/mapper/RecordDtoMapperTest.java index 972b785b..9d95524a 100644 --- a/stempo-application/src/test/java/com/stempo/mapper/RecordDtoMapperTest.java +++ b/stempo-application/src/test/java/com/stempo/mapper/RecordDtoMapperTest.java @@ -2,9 +2,11 @@ import static org.assertj.core.api.Assertions.assertThat; +import com.stempo.dto.response.RecordItemDto; import com.stempo.dto.response.RecordResponseDto; import com.stempo.dto.response.RecordStatisticsResponseDto; import java.time.LocalDate; +import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -18,7 +20,26 @@ void setUp() { } @Test - void accuracy와_duration과_steps와_date로_RecordResponseDto로_매핑된다() { + void accuracyAverage와_records가_RecordResponseDto로_매핑된다() { + // given + int accuracyAverage = 95; + RecordItemDto recordItemDto = RecordItemDto.builder() + .accuracy(95.5) + .duration(1200) + .steps(1500) + .date(LocalDate.of(2023, 10, 24)) + .build(); + + // when + RecordResponseDto responseDto = recordDtoMapper.toDto(accuracyAverage, List.of(recordItemDto)); + + // then + assertThat(responseDto.getAccuracyAverage()).isEqualTo(accuracyAverage); + assertThat(responseDto.getRecords()).containsExactly(recordItemDto); + } + + @Test + void accuracy_duration_steps_date가_RecordItemDto로_매핑된다() { // given Double accuracy = 95.5; Integer duration = 1200; @@ -26,7 +47,7 @@ void setUp() { LocalDate date = LocalDate.of(2023, 10, 24); // when - RecordResponseDto responseDto = recordDtoMapper.toDto(accuracy, duration, steps, date); + RecordItemDto responseDto = recordDtoMapper.toDto(accuracy, duration, steps, date); // then assertThat(responseDto.getAccuracy()).isEqualTo(accuracy); @@ -36,7 +57,7 @@ void setUp() { } @Test - void 오늘_보행_훈련_횟수와_주간_보행_훈련_횟수와_연속_보행_훈련_일수로_RecordStatisticsResponseDto로_매핑된다() { + void 오늘_보행_훈련_횟수와_주간_보행_훈련_횟수와_연속_보행_훈련_일수가_RecordStatisticsResponseDto로_매핑된다() { // given int todayWalkTrainingCount = 3; int weeklyWalkTrainingCount = 10; @@ -44,7 +65,7 @@ void setUp() { // when RecordStatisticsResponseDto responseDto = recordDtoMapper.toDto(todayWalkTrainingCount, weeklyWalkTrainingCount, - consecutiveWalkTrainingDays); + consecutiveWalkTrainingDays); // then assertThat(responseDto.getTodayWalkTrainingCount()).isEqualTo(todayWalkTrainingCount); diff --git a/stempo-application/src/test/java/com/stempo/service/RecordServiceImplTest.java b/stempo-application/src/test/java/com/stempo/service/RecordServiceImplTest.java index b92610e1..94c33a40 100644 --- a/stempo-application/src/test/java/com/stempo/service/RecordServiceImplTest.java +++ b/stempo-application/src/test/java/com/stempo/service/RecordServiceImplTest.java @@ -5,11 +5,13 @@ import static org.mockito.ArgumentMatchers.anyDouble; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.stempo.dto.request.RecordRequestDto; +import com.stempo.dto.response.RecordItemDto; import com.stempo.dto.response.RecordResponseDto; import com.stempo.dto.response.RecordStatisticsResponseDto; import com.stempo.mapper.RecordDtoMapper; @@ -46,7 +48,7 @@ class RecordServiceImplTest { private RecordServiceImpl recordService; private RecordRequestDto recordRequestDto; - private Record record; + private Record trainingRecord; private String deviceTag; @BeforeEach @@ -57,7 +59,7 @@ void setUp() { recordRequestDto.setDuration(120); recordRequestDto.setSteps(1000); - record = Record.builder() + trainingRecord = Record.builder() .id(1L) .deviceTag(deviceTag) .accuracy("encrypted-accuracy") @@ -72,10 +74,10 @@ record = Record.builder() // given when(userService.getCurrentDeviceTag()).thenReturn(deviceTag); when(encryptionUtils.encrypt(anyString())).thenReturn("encrypted-value"); - when(recordRepository.save(any(Record.class))).thenReturn(record); + when(recordRepository.save(any(Record.class))).thenReturn(trainingRecord); // when - String result = recordService.record(recordRequestDto); + String result = recordService.recordTrainingData(recordRequestDto); // then assertThat(result).isEqualTo(deviceTag); @@ -90,7 +92,7 @@ record = Record.builder() LocalDate startDate = LocalDate.of(2024, 10, 21); LocalDate endDate = LocalDate.of(2024, 10, 27); LocalDateTime startDateTime = startDate.atStartOfDay(); - LocalDateTime endDateTime = endDate.atStartOfDay().plusDays(1); + LocalDateTime endDateTime = endDate.plusDays(1).atStartOfDay(); Record latestRecord = Record.builder() .id(2L) @@ -101,7 +103,7 @@ record = Record.builder() .createdAt(startDateTime.minusDays(1)) .build(); - List recordsBetweenDates = List.of(record); + List recordsBetweenDates = List.of(trainingRecord); when(userService.getCurrentDeviceTag()).thenReturn(deviceTag); when(recordRepository.findLatestBeforeStartDate(deviceTag, startDateTime)) @@ -111,28 +113,93 @@ record = Record.builder() when(encryptionUtils.decrypt(anyString())).thenReturn("95.5", "120", "1000"); when(mapper.toDto(anyDouble(), anyInt(), anyInt(), any(LocalDate.class))) .thenReturn( - RecordResponseDto.builder() + RecordItemDto.builder() .accuracy(95.5) .duration(120) .steps(1000) .build(), - RecordResponseDto.builder() + RecordItemDto.builder() .accuracy(95.5) .duration(120) .steps(1000) .build() ); + when(mapper.toDto(anyInt(), any(List.class))) + .thenReturn(RecordResponseDto.builder() + .accuracyAverage(96) + .records(List.of( + RecordItemDto.builder() + .accuracy(95.5) + .duration(120) + .steps(1000) + .build(), + RecordItemDto.builder() + .accuracy(95.5) + .duration(120) + .steps(1000) + .build() + )) + .build()); // when - List result = recordService.getRecordsByDateRange(startDate, endDate); + RecordResponseDto result = recordService.getRecordsByDateRange(startDate, endDate); // then - assertThat(result).hasSize(2); + assertThat(result.getAccuracyAverage()).isEqualTo(96); + assertThat(result.getRecords()).hasSize(2); verify(userService).getCurrentDeviceTag(); verify(recordRepository).findLatestBeforeStartDate(deviceTag, startDateTime); verify(recordRepository).findByDateBetween(deviceTag, startDateTime, endDateTime); verify(encryptionUtils, times(6)).decrypt(anyString()); // accuracy, duration, steps * 2 records verify(mapper, times(2)).toDto(anyDouble(), anyInt(), anyInt(), any(LocalDate.class)); + verify(mapper, times(1)).toDto(anyInt(), any(List.class)); + } + + @Test + void startDate_이전의_최신_데이터가_없는_경우_startDate_이전_날짜로_0으로_초기화된_값을_생성한다() { + // given + LocalDate startDate = LocalDate.of(2024, 10, 21); + LocalDate endDate = LocalDate.of(2024, 10, 27); + LocalDateTime startDateTime = startDate.atStartOfDay(); + LocalDateTime endDateTime = endDate.plusDays(1).atStartOfDay(); + + when(userService.getCurrentDeviceTag()).thenReturn(deviceTag); + when(recordRepository.findLatestBeforeStartDate(deviceTag, startDateTime)) + .thenReturn(Optional.empty()); + when(recordRepository.findByDateBetween(deviceTag, startDateTime, endDateTime)) + .thenReturn(List.of()); + when(mapper.toDto(anyDouble(), anyInt(), anyInt(), eq(startDate.minusDays(1)))) + .thenReturn(RecordItemDto.builder() + .accuracy(0.0) + .duration(0) + .steps(0) + .build()); + when(mapper.toDto(anyInt(), any(List.class))) + .thenReturn(RecordResponseDto.builder() + .accuracyAverage(0) + .records(List.of( + RecordItemDto.builder() + .accuracy(0.0) + .duration(0) + .steps(0) + .build() + )) + .build()); + + // when + RecordResponseDto result = recordService.getRecordsByDateRange(startDate, endDate); + + // then + assertThat(result.getAccuracyAverage()).isZero(); + assertThat(result.getRecords()).hasSize(1); + assertThat(result.getRecords().getFirst().getAccuracy()).isEqualTo(0.0); + assertThat(result.getRecords().getFirst().getDuration()).isZero(); + assertThat(result.getRecords().getFirst().getSteps()).isZero(); + verify(userService).getCurrentDeviceTag(); + verify(recordRepository).findLatestBeforeStartDate(deviceTag, startDateTime); + verify(recordRepository).findByDateBetween(deviceTag, startDateTime, endDateTime); + verify(mapper).toDto(anyDouble(), anyInt(), anyInt(), eq(startDate.minusDays(1))); + verify(mapper).toDto(anyInt(), any(List.class)); } @Test