diff --git a/src/main/java/until/the/eternity/statistics/application/service/ItemDailyStatisticsService.java b/src/main/java/until/the/eternity/statistics/application/service/ItemDailyStatisticsService.java index 923325b..ab63326 100644 --- a/src/main/java/until/the/eternity/statistics/application/service/ItemDailyStatisticsService.java +++ b/src/main/java/until/the/eternity/statistics/application/service/ItemDailyStatisticsService.java @@ -20,24 +20,24 @@ public class ItemDailyStatisticsService { private final ItemDailyStatisticsRepository repository; private final ItemDailyStatisticsMapper mapper; - /** 아이템별 일간 통계 전체 조회 (페이징) */ + /** 아이템별 일간 통계 조회 (itemName, subCategory, topCategory, 날짜 범위) */ @Transactional(readOnly = true) - public PageResponseDto findAll(Pageable pageable) { - Page page = repository.findAll(pageable); - Page dtoPage = page.map(mapper::toDto); - return PageResponseDto.of(dtoPage); - } + public java.util.List search( + String itemName, + String subCategory, + String topCategory, + java.time.LocalDate startDate, + java.time.LocalDate endDate) { + // 날짜 범위 검증 (최대 30일) + until.the.eternity.statistics.util.DateRangeValidator.validateDailyDateRange( + startDate, endDate); - /** 아이템별 일간 통계 ID로 단건 조회 */ - @Transactional(readOnly = true) - public ItemDailyStatisticsResponse findById(Long id) { - ItemDailyStatistics entity = - repository - .findById(id) - .orElseThrow( - () -> - new IllegalArgumentException( - "ItemDailyStatistics not found: " + id)); - return mapper.toDto(entity); + // 조회 + java.util.List results = + repository.findByItemAndDateRange( + itemName, subCategory, topCategory, startDate, endDate); + + // DTO 변환 + return results.stream().map(mapper::toDto).collect(java.util.stream.Collectors.toList()); } } diff --git a/src/main/java/until/the/eternity/statistics/application/service/ItemWeeklyStatisticsService.java b/src/main/java/until/the/eternity/statistics/application/service/ItemWeeklyStatisticsService.java index 9d3099f..75502a7 100644 --- a/src/main/java/until/the/eternity/statistics/application/service/ItemWeeklyStatisticsService.java +++ b/src/main/java/until/the/eternity/statistics/application/service/ItemWeeklyStatisticsService.java @@ -20,24 +20,24 @@ public class ItemWeeklyStatisticsService { private final ItemWeeklyStatisticsRepository repository; private final ItemWeeklyStatisticsMapper mapper; - /** 아이템별 주간 통계 전체 조회 (페이징) */ + /** 아이템별 주간 통계 조회 (itemName, subCategory, topCategory, 날짜 범위) */ @Transactional(readOnly = true) - public PageResponseDto findAll(Pageable pageable) { - Page page = repository.findAll(pageable); - Page dtoPage = page.map(mapper::toDto); - return PageResponseDto.of(dtoPage); - } + public java.util.List search( + String itemName, + String subCategory, + String topCategory, + java.time.LocalDate startDate, + java.time.LocalDate endDate) { + // 날짜 범위 검증 (최대 4개월) + until.the.eternity.statistics.util.DateRangeValidator.validateWeeklyDateRange( + startDate, endDate); - /** 아이템별 주간 통계 ID로 단건 조회 */ - @Transactional(readOnly = true) - public ItemWeeklyStatisticsResponse findById(Long id) { - ItemWeeklyStatistics entity = - repository - .findById(id) - .orElseThrow( - () -> - new IllegalArgumentException( - "ItemWeeklyStatistics not found: " + id)); - return mapper.toDto(entity); + // 조회 + java.util.List results = + repository.findByItemAndDateRange( + itemName, subCategory, topCategory, startDate, endDate); + + // DTO 변환 + return results.stream().map(mapper::toDto).collect(java.util.stream.Collectors.toList()); } } diff --git a/src/main/java/until/the/eternity/statistics/application/service/SubcategoryDailyStatisticsService.java b/src/main/java/until/the/eternity/statistics/application/service/SubcategoryDailyStatisticsService.java index 9d8638d..50f865b 100644 --- a/src/main/java/until/the/eternity/statistics/application/service/SubcategoryDailyStatisticsService.java +++ b/src/main/java/until/the/eternity/statistics/application/service/SubcategoryDailyStatisticsService.java @@ -20,24 +20,22 @@ public class SubcategoryDailyStatisticsService { private final SubcategoryDailyStatisticsRepository repository; private final SubcategoryDailyStatisticsMapper mapper; - /** 서브카테고리별 일간 통계 전체 조회 (페이징) */ + /** 서브카테고리별 일간 통계 조회 (subCategory, 날짜 범위) */ @Transactional(readOnly = true) - public PageResponseDto findAll(Pageable pageable) { - Page page = repository.findAll(pageable); - Page dtoPage = page.map(mapper::toDto); - return PageResponseDto.of(dtoPage); - } + public java.util.List search( + String topCategory, // topCategory는 파라미터로 받지만 조회에는 사용하지 않음 (DB 구조상) + String subCategory, + java.time.LocalDate startDate, + java.time.LocalDate endDate) { + // 날짜 범위 검증 (최대 30일) + until.the.eternity.statistics.util.DateRangeValidator.validateDailyDateRange( + startDate, endDate); - /** 서브카테고리별 일간 통계 ID로 단건 조회 */ - @Transactional(readOnly = true) - public SubcategoryDailyStatisticsResponse findById(Long id) { - SubcategoryDailyStatistics entity = - repository - .findById(id) - .orElseThrow( - () -> - new IllegalArgumentException( - "SubcategoryDailyStatistics not found: " + id)); - return mapper.toDto(entity); + // 조회 + java.util.List results = + repository.findBySubcategoryAndDateRange(subCategory, startDate, endDate); + + // DTO 변환 + return results.stream().map(mapper::toDto).collect(java.util.stream.Collectors.toList()); } } diff --git a/src/main/java/until/the/eternity/statistics/application/service/SubcategoryWeeklyStatisticsService.java b/src/main/java/until/the/eternity/statistics/application/service/SubcategoryWeeklyStatisticsService.java index 676c1c9..ea9df09 100644 --- a/src/main/java/until/the/eternity/statistics/application/service/SubcategoryWeeklyStatisticsService.java +++ b/src/main/java/until/the/eternity/statistics/application/service/SubcategoryWeeklyStatisticsService.java @@ -20,24 +20,22 @@ public class SubcategoryWeeklyStatisticsService { private final SubcategoryWeeklyStatisticsRepository repository; private final SubcategoryWeeklyStatisticsMapper mapper; - /** 서브카테고리별 주간 통계 전체 조회 (페이징) */ + /** 서브카테고리별 주간 통계 조회 (subCategory, 날짜 범위) */ @Transactional(readOnly = true) - public PageResponseDto findAll(Pageable pageable) { - Page page = repository.findAll(pageable); - Page dtoPage = page.map(mapper::toDto); - return PageResponseDto.of(dtoPage); - } + public java.util.List search( + String topCategory, // topCategory는 파라미터로 받지만 조회에는 사용하지 않음 (DB 구조상) + String subCategory, + java.time.LocalDate startDate, + java.time.LocalDate endDate) { + // 날짜 범위 검증 (최대 4개월) + until.the.eternity.statistics.util.DateRangeValidator.validateWeeklyDateRange( + startDate, endDate); - /** 서브카테고리별 주간 통계 ID로 단건 조회 */ - @Transactional(readOnly = true) - public SubcategoryWeeklyStatisticsResponse findById(Long id) { - SubcategoryWeeklyStatistics entity = - repository - .findById(id) - .orElseThrow( - () -> - new IllegalArgumentException( - "SubcategoryWeeklyStatistics not found: " + id)); - return mapper.toDto(entity); + // 조회 + java.util.List results = + repository.findBySubcategoryAndDateRange(subCategory, startDate, endDate); + + // DTO 변환 + return results.stream().map(mapper::toDto).collect(java.util.stream.Collectors.toList()); } } diff --git a/src/main/java/until/the/eternity/statistics/application/service/TopCategoryDailyStatisticsService.java b/src/main/java/until/the/eternity/statistics/application/service/TopCategoryDailyStatisticsService.java index f51801f..eea5eaf 100644 --- a/src/main/java/until/the/eternity/statistics/application/service/TopCategoryDailyStatisticsService.java +++ b/src/main/java/until/the/eternity/statistics/application/service/TopCategoryDailyStatisticsService.java @@ -20,24 +20,19 @@ public class TopCategoryDailyStatisticsService { private final TopCategoryDailyStatisticsRepository repository; private final TopCategoryDailyStatisticsMapper mapper; - /** 탑카테고리별 일간 통계 전체 조회 (페이징) */ + /** 탑카테고리별 일간 통계 조회 (topCategory, 날짜 범위) */ @Transactional(readOnly = true) - public PageResponseDto findAll(Pageable pageable) { - Page page = repository.findAll(pageable); - Page dtoPage = page.map(mapper::toDto); - return PageResponseDto.of(dtoPage); - } + public java.util.List search( + String topCategory, java.time.LocalDate startDate, java.time.LocalDate endDate) { + // 날짜 범위 검증 (최대 30일) + until.the.eternity.statistics.util.DateRangeValidator.validateDailyDateRange( + startDate, endDate); - /** 탑카테고리별 일간 통계 ID로 단건 조회 */ - @Transactional(readOnly = true) - public TopCategoryDailyStatisticsResponse findById(Long id) { - TopCategoryDailyStatistics entity = - repository - .findById(id) - .orElseThrow( - () -> - new IllegalArgumentException( - "TopCategoryDailyStatistics not found: " + id)); - return mapper.toDto(entity); + // 조회 + java.util.List results = + repository.findByTopCategoryAndDateRange(topCategory, startDate, endDate); + + // DTO 변환 + return results.stream().map(mapper::toDto).collect(java.util.stream.Collectors.toList()); } } diff --git a/src/main/java/until/the/eternity/statistics/application/service/TopCategoryWeeklyStatisticsService.java b/src/main/java/until/the/eternity/statistics/application/service/TopCategoryWeeklyStatisticsService.java index a586ea5..de9dbe1 100644 --- a/src/main/java/until/the/eternity/statistics/application/service/TopCategoryWeeklyStatisticsService.java +++ b/src/main/java/until/the/eternity/statistics/application/service/TopCategoryWeeklyStatisticsService.java @@ -20,24 +20,19 @@ public class TopCategoryWeeklyStatisticsService { private final TopCategoryWeeklyStatisticsRepository repository; private final TopCategoryWeeklyStatisticsMapper mapper; - /** 탑카테고리별 주간 통계 전체 조회 (페이징) */ + /** 탑카테고리별 주간 통계 조회 (topCategory, 날짜 범위) */ @Transactional(readOnly = true) - public PageResponseDto findAll(Pageable pageable) { - Page page = repository.findAll(pageable); - Page dtoPage = page.map(mapper::toDto); - return PageResponseDto.of(dtoPage); - } + public java.util.List search( + String topCategory, java.time.LocalDate startDate, java.time.LocalDate endDate) { + // 날짜 범위 검증 (최대 4개월) + until.the.eternity.statistics.util.DateRangeValidator.validateWeeklyDateRange( + startDate, endDate); - /** 탑카테고리별 주간 통계 ID로 단건 조회 */ - @Transactional(readOnly = true) - public TopCategoryWeeklyStatisticsResponse findById(Long id) { - TopCategoryWeeklyStatistics entity = - repository - .findById(id) - .orElseThrow( - () -> - new IllegalArgumentException( - "TopCategoryWeeklyStatistics not found: " + id)); - return mapper.toDto(entity); + // 조회 + java.util.List results = + repository.findByTopCategoryAndDateRange(topCategory, startDate, endDate); + + // DTO 변환 + return results.stream().map(mapper::toDto).collect(java.util.stream.Collectors.toList()); } } diff --git a/src/main/java/until/the/eternity/statistics/domain/entity/daily/ItemDailyStatistics.java b/src/main/java/until/the/eternity/statistics/domain/entity/daily/ItemDailyStatistics.java index 49f5b17..361df9c 100644 --- a/src/main/java/until/the/eternity/statistics/domain/entity/daily/ItemDailyStatistics.java +++ b/src/main/java/until/the/eternity/statistics/domain/entity/daily/ItemDailyStatistics.java @@ -36,6 +36,14 @@ public class ItemDailyStatistics { @Schema(description = "아이템 이름", example = "켈틱 로열 나이트 소드") private String itemName; + @Column(name = "item_top_category", nullable = false, length = 255) + @Schema(description = "아이템 탑 카테고리", example = "무기") + private String itemTopCategory; + + @Column(name = "item_sub_category", nullable = false, length = 255) + @Schema(description = "아이템 서브 카테고리", example = "한손검") + private String itemSubCategory; + @Column(name = "date_auction_buy", nullable = false) @Schema(description = "거래 일자", example = "2025-07-01") private LocalDate dateAuctionBuy; diff --git a/src/main/java/until/the/eternity/statistics/domain/entity/weekly/ItemWeeklyStatistics.java b/src/main/java/until/the/eternity/statistics/domain/entity/weekly/ItemWeeklyStatistics.java index b1ae76b..6970f18 100644 --- a/src/main/java/until/the/eternity/statistics/domain/entity/weekly/ItemWeeklyStatistics.java +++ b/src/main/java/until/the/eternity/statistics/domain/entity/weekly/ItemWeeklyStatistics.java @@ -36,6 +36,14 @@ public class ItemWeeklyStatistics { @Schema(description = "아이템 이름", example = "켈틱 로열 나이트 소드") private String itemName; + @Column(name = "item_top_category", nullable = false, length = 255) + @Schema(description = "아이템 탑 카테고리", example = "무기") + private String itemTopCategory; + + @Column(name = "item_sub_category", nullable = false, length = 255) + @Schema(description = "아이템 서브 카테고리", example = "한손검") + private String itemSubCategory; + @Column(name = "year", nullable = false) @Schema(description = "연도", example = "2025") private Integer year; diff --git a/src/main/java/until/the/eternity/statistics/interfaces/rest/controller/ItemDailyStatisticsController.java b/src/main/java/until/the/eternity/statistics/interfaces/rest/controller/ItemDailyStatisticsController.java index 892ad3b..377f6cd 100644 --- a/src/main/java/until/the/eternity/statistics/interfaces/rest/controller/ItemDailyStatisticsController.java +++ b/src/main/java/until/the/eternity/statistics/interfaces/rest/controller/ItemDailyStatisticsController.java @@ -3,6 +3,7 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springdoc.core.annotations.ParameterObject; import org.springframework.http.ResponseEntity; @@ -10,6 +11,7 @@ import until.the.eternity.common.request.PageRequestDto; import until.the.eternity.common.response.PageResponseDto; import until.the.eternity.statistics.application.service.ItemDailyStatisticsService; +import until.the.eternity.statistics.interfaces.rest.dto.request.ItemDailyStatisticsSearchRequest; import until.the.eternity.statistics.interfaces.rest.dto.response.ItemDailyStatisticsResponse; @RestController @@ -22,19 +24,20 @@ public class ItemDailyStatisticsController { @GetMapping @Operation( - summary = "아이템별 일간 통계 목록 조회", - description = "아이템별 일간 거래 통계 목록을 페이징하여 조회합니다. 최저가, 최고가, 평균가, 거래 총량, 거래 수량 정보를 포함합니다.") - public ResponseEntity> getItemDailyStatistics( - @ParameterObject @ModelAttribute PageRequestDto pageDto) { - PageResponseDto result = service.findAll(pageDto.toPageable()); - return ResponseEntity.ok(result); - } - - @GetMapping("/{id}") - @Operation(summary = "아이템별 일간 통계 단건 조회", description = "ID를 통해 특정 아이템의 일간 거래 통계를 조회합니다.") - public ResponseEntity getItemDailyStatisticsById( - @Parameter(description = "통계 ID", example = "1") @PathVariable Long id) { - ItemDailyStatisticsResponse result = service.findById(id); - return ResponseEntity.ok(result); + summary = "아이템별 일간 통계 조회", + description = "아이템 이름, 서브 카테고리, 탑 카테고리로 일간 통계를 조회합니다. 최대 30일까지 조회 가능합니다.") + public ResponseEntity> searchItemDailyStatistics( + @ParameterObject @ModelAttribute + @Valid + ItemDailyStatisticsSearchRequest + request) { + java.util.List results = + service.search( + request.itemName(), + request.subCategory(), + request.topCategory(), + request.getStartDateWithDefault(), + request.getEndDateWithDefault()); + return ResponseEntity.ok(results); } } diff --git a/src/main/java/until/the/eternity/statistics/interfaces/rest/controller/ItemWeeklyStatisticsController.java b/src/main/java/until/the/eternity/statistics/interfaces/rest/controller/ItemWeeklyStatisticsController.java index af8cad2..2c8a5a9 100644 --- a/src/main/java/until/the/eternity/statistics/interfaces/rest/controller/ItemWeeklyStatisticsController.java +++ b/src/main/java/until/the/eternity/statistics/interfaces/rest/controller/ItemWeeklyStatisticsController.java @@ -22,21 +22,20 @@ public class ItemWeeklyStatisticsController { @GetMapping @Operation( - summary = "아이템별 주간 통계 목록 조회", - description = - "아이템별 주간 거래 통계 목록을 페이징하여 조회합니다. 최저가, 최고가, 평균가, 거래 총량, 거래 수량, 연도, 주차 정보를 포함합니다.") - public ResponseEntity> getItemWeeklyStatistics( - @ParameterObject @ModelAttribute PageRequestDto pageDto) { - PageResponseDto result = - service.findAll(pageDto.toPageable()); - return ResponseEntity.ok(result); - } - - @GetMapping("/{id}") - @Operation(summary = "아이템별 주간 통계 단건 조회", description = "ID를 통해 특정 아이템의 주간 거래 통계를 조회합니다.") - public ResponseEntity getItemWeeklyStatisticsById( - @Parameter(description = "통계 ID", example = "1") @PathVariable Long id) { - ItemWeeklyStatisticsResponse result = service.findById(id); - return ResponseEntity.ok(result); + summary = "아이템별 주간 통계 조회", + description = "아이템 이름, 서브 카테고리, 탑 카테고리로 주간 통계를 조회합니다. 최대 4개월까지 조회 가능합니다.") + public ResponseEntity> searchItemWeeklyStatistics( + @ParameterObject @ModelAttribute + @jakarta.validation.Valid + until.the.eternity.statistics.interfaces.rest.dto.request.ItemWeeklyStatisticsSearchRequest + request) { + java.util.List results = + service.search( + request.itemName(), + request.subCategory(), + request.topCategory(), + request.getStartDateWithDefault(), + request.getEndDateWithDefault()); + return ResponseEntity.ok(results); } } diff --git a/src/main/java/until/the/eternity/statistics/interfaces/rest/controller/SubcategoryDailyStatisticsController.java b/src/main/java/until/the/eternity/statistics/interfaces/rest/controller/SubcategoryDailyStatisticsController.java index 2f8df45..f1d0703 100644 --- a/src/main/java/until/the/eternity/statistics/interfaces/rest/controller/SubcategoryDailyStatisticsController.java +++ b/src/main/java/until/the/eternity/statistics/interfaces/rest/controller/SubcategoryDailyStatisticsController.java @@ -22,21 +22,20 @@ public class SubcategoryDailyStatisticsController { @GetMapping @Operation( - summary = "서브카테고리별 일간 통계 목록 조회", - description = - "서브카테고리별 일간 거래 통계 목록을 페이징하여 조회합니다. 최저가, 최고가, 평균가, 거래 총량, 거래 수량 정보를 포함합니다.") - public ResponseEntity> - getSubcategoryDailyStatistics(@ParameterObject @ModelAttribute PageRequestDto pageDto) { - PageResponseDto result = - service.findAll(pageDto.toPageable()); - return ResponseEntity.ok(result); - } - - @GetMapping("/{id}") - @Operation(summary = "서브카테고리별 일간 통계 단건 조회", description = "ID를 통해 특정 서브카테고리의 일간 거래 통계를 조회합니다.") - public ResponseEntity getSubcategoryDailyStatisticsById( - @Parameter(description = "통계 ID", example = "1") @PathVariable Long id) { - SubcategoryDailyStatisticsResponse result = service.findById(id); - return ResponseEntity.ok(result); + summary = "서브카테고리별 일간 통계 조회", + description = "탑 카테고리와 서브 카테고리로 일간 통계를 조회합니다. 최대 30일까지 조회 가능합니다.") + public ResponseEntity> + searchSubcategoryDailyStatistics( + @ParameterObject @ModelAttribute + @jakarta.validation.Valid + until.the.eternity.statistics.interfaces.rest.dto.request.SubcategoryDailyStatisticsSearchRequest + request) { + java.util.List results = + service.search( + request.topCategory(), + request.subCategory(), + request.getStartDateWithDefault(), + request.getEndDateWithDefault()); + return ResponseEntity.ok(results); } } diff --git a/src/main/java/until/the/eternity/statistics/interfaces/rest/controller/SubcategoryWeeklyStatisticsController.java b/src/main/java/until/the/eternity/statistics/interfaces/rest/controller/SubcategoryWeeklyStatisticsController.java index 43c7039..d607cae 100644 --- a/src/main/java/until/the/eternity/statistics/interfaces/rest/controller/SubcategoryWeeklyStatisticsController.java +++ b/src/main/java/until/the/eternity/statistics/interfaces/rest/controller/SubcategoryWeeklyStatisticsController.java @@ -22,22 +22,20 @@ public class SubcategoryWeeklyStatisticsController { @GetMapping @Operation( - summary = "서브카테고리별 주간 통계 목록 조회", - description = - "서브카테고리별 주간 거래 통계 목록을 페이징하여 조회합니다. 최저가, 최고가, 평균가, 거래 총량, 거래 수량, 연도, 주차 정보를 포함합니다.") - public ResponseEntity> - getSubcategoryWeeklyStatistics( - @ParameterObject @ModelAttribute PageRequestDto pageDto) { - PageResponseDto result = - service.findAll(pageDto.toPageable()); - return ResponseEntity.ok(result); - } - - @GetMapping("/{id}") - @Operation(summary = "서브카테고리별 주간 통계 단건 조회", description = "ID를 통해 특정 서브카테고리의 주간 거래 통계를 조회합니다.") - public ResponseEntity getSubcategoryWeeklyStatisticsById( - @Parameter(description = "통계 ID", example = "1") @PathVariable Long id) { - SubcategoryWeeklyStatisticsResponse result = service.findById(id); - return ResponseEntity.ok(result); + summary = "서브카테고리별 주간 통계 조회", + description = "탑 카테고리와 서브 카테고리로 주간 통계를 조회합니다. 최대 4개월까지 조회 가능합니다.") + public ResponseEntity> + searchSubcategoryWeeklyStatistics( + @ParameterObject @ModelAttribute + @jakarta.validation.Valid + until.the.eternity.statistics.interfaces.rest.dto.request.SubcategoryWeeklyStatisticsSearchRequest + request) { + java.util.List results = + service.search( + request.topCategory(), + request.subCategory(), + request.getStartDateWithDefault(), + request.getEndDateWithDefault()); + return ResponseEntity.ok(results); } } diff --git a/src/main/java/until/the/eternity/statistics/interfaces/rest/controller/TopCategoryDailyStatisticsController.java b/src/main/java/until/the/eternity/statistics/interfaces/rest/controller/TopCategoryDailyStatisticsController.java index f0c9c59..718996d 100644 --- a/src/main/java/until/the/eternity/statistics/interfaces/rest/controller/TopCategoryDailyStatisticsController.java +++ b/src/main/java/until/the/eternity/statistics/interfaces/rest/controller/TopCategoryDailyStatisticsController.java @@ -22,20 +22,16 @@ public class TopCategoryDailyStatisticsController { @GetMapping @Operation( - summary = "탑카테고리별 일간 통계 목록 조회", - description = "탑카테고리별 일간 거래 통계 목록을 페이징하여 조회합니다. 최저가, 최고가, 평균가, 거래 총량, 거래 수량 정보를 포함합니다.") - public ResponseEntity> - getTopCategoryDailyStatistics(@ParameterObject @ModelAttribute PageRequestDto pageDto) { - PageResponseDto result = - service.findAll(pageDto.toPageable()); - return ResponseEntity.ok(result); - } - - @GetMapping("/{id}") - @Operation(summary = "탑카테고리별 일간 통계 단건 조회", description = "ID를 통해 특정 탑카테고리의 일간 거래 통계를 조회합니다.") - public ResponseEntity getTopCategoryDailyStatisticsById( - @Parameter(description = "통계 ID", example = "1") @PathVariable Long id) { - TopCategoryDailyStatisticsResponse result = service.findById(id); - return ResponseEntity.ok(result); + summary = "탑카테고리별 일간 통계 조회", + description = "탑 카테고리로 일간 통계를 조회합니다. 최대 30일까지 조회 가능합니다.") + public ResponseEntity> + searchTopCategoryDailyStatistics( + @ParameterObject @ModelAttribute + @jakarta.validation.Valid + until.the.eternity.statistics.interfaces.rest.dto.request.TopCategoryDailyStatisticsSearchRequest + request) { + java.util.List results = + service.search(request.topCategory(), request.getStartDateWithDefault(), request.getEndDateWithDefault()); + return ResponseEntity.ok(results); } } diff --git a/src/main/java/until/the/eternity/statistics/interfaces/rest/controller/TopCategoryWeeklyStatisticsController.java b/src/main/java/until/the/eternity/statistics/interfaces/rest/controller/TopCategoryWeeklyStatisticsController.java index e63033e..b37a128 100644 --- a/src/main/java/until/the/eternity/statistics/interfaces/rest/controller/TopCategoryWeeklyStatisticsController.java +++ b/src/main/java/until/the/eternity/statistics/interfaces/rest/controller/TopCategoryWeeklyStatisticsController.java @@ -22,22 +22,16 @@ public class TopCategoryWeeklyStatisticsController { @GetMapping @Operation( - summary = "탑카테고리별 주간 통계 목록 조회", - description = - "탑카테고리별 주간 거래 통계 목록을 페이징하여 조회합니다. 최저가, 최고가, 평균가, 거래 총량, 거래 수량, 연도, 주차 정보를 포함합니다.") - public ResponseEntity> - getTopCategoryWeeklyStatistics( - @ParameterObject @ModelAttribute PageRequestDto pageDto) { - PageResponseDto result = - service.findAll(pageDto.toPageable()); - return ResponseEntity.ok(result); - } - - @GetMapping("/{id}") - @Operation(summary = "탑카테고리별 주간 통계 단건 조회", description = "ID를 통해 특정 탑카테고리의 주간 거래 통계를 조회합니다.") - public ResponseEntity getTopCategoryWeeklyStatisticsById( - @Parameter(description = "통계 ID", example = "1") @PathVariable Long id) { - TopCategoryWeeklyStatisticsResponse result = service.findById(id); - return ResponseEntity.ok(result); + summary = "탑카테고리별 주간 통계 조회", + description = "탑 카테고리로 주간 통계를 조회합니다. 최대 4개월까지 조회 가능합니다.") + public ResponseEntity> + searchTopCategoryWeeklyStatistics( + @ParameterObject @ModelAttribute + @jakarta.validation.Valid + until.the.eternity.statistics.interfaces.rest.dto.request.TopCategoryWeeklyStatisticsSearchRequest + request) { + java.util.List results = + service.search(request.topCategory(), request.getStartDateWithDefault(), request.getEndDateWithDefault()); + return ResponseEntity.ok(results); } } diff --git a/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/request/ItemDailyStatisticsSearchRequest.java b/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/request/ItemDailyStatisticsSearchRequest.java new file mode 100644 index 0000000..f6f9152 --- /dev/null +++ b/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/request/ItemDailyStatisticsSearchRequest.java @@ -0,0 +1,33 @@ +package until.the.eternity.statistics.interfaces.rest.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import java.time.LocalDate; +import org.springframework.format.annotation.DateTimeFormat; + +@Schema(description = "아이템별 일간 통계 검색 요청") +public record ItemDailyStatisticsSearchRequest( + @NotBlank(message = "아이템 이름은 필수입니다") + @Schema(description = "아이템 이름", example = "축복의 포션", required = true) + String itemName, + @NotBlank(message = "서브 카테고리는 필수입니다") + @Schema(description = "아이템 서브 카테고리", example = "포션", required = true) + String subCategory, + @NotBlank(message = "탑 카테고리는 필수입니다") + @Schema(description = "아이템 탑 카테고리", example = "소모품", required = true) + String topCategory, + @Schema(description = "검색 시작 일자 (미입력 시 오늘로부터 2주 전)", example = "2026-01-01") + @DateTimeFormat(pattern = "yyyy-MM-dd") + LocalDate startDate, + @Schema(description = "검색 종료 일자 (미입력 시 오늘)", example = "2026-01-31") + @DateTimeFormat(pattern = "yyyy-MM-dd") + LocalDate endDate) { + + public LocalDate getStartDateWithDefault() { + return startDate != null ? startDate : LocalDate.now().minusDays(14); + } + + public LocalDate getEndDateWithDefault() { + return endDate != null ? endDate : LocalDate.now(); + } +} diff --git a/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/request/ItemWeeklyStatisticsSearchRequest.java b/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/request/ItemWeeklyStatisticsSearchRequest.java new file mode 100644 index 0000000..1360de1 --- /dev/null +++ b/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/request/ItemWeeklyStatisticsSearchRequest.java @@ -0,0 +1,33 @@ +package until.the.eternity.statistics.interfaces.rest.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import java.time.LocalDate; +import org.springframework.format.annotation.DateTimeFormat; + +@Schema(description = "아이템별 주간 통계 검색 요청") +public record ItemWeeklyStatisticsSearchRequest( + @NotBlank(message = "아이템 이름은 필수입니다") + @Schema(description = "아이템 이름", example = "축복의 포션", required = true) + String itemName, + @NotBlank(message = "서브 카테고리는 필수입니다") + @Schema(description = "아이템 서브 카테고리", example = "포션", required = true) + String subCategory, + @NotBlank(message = "탑 카테고리는 필수입니다") + @Schema(description = "아이템 탑 카테고리", example = "소모품", required = true) + String topCategory, + @Schema(description = "검색 시작 일자 (미입력 시 오늘로부터 2달 전)", example = "2026-01-01") + @DateTimeFormat(pattern = "yyyy-MM-dd") + LocalDate startDate, + @Schema(description = "검색 종료 일자 (미입력 시 오늘)", example = "2026-04-30") + @DateTimeFormat(pattern = "yyyy-MM-dd") + LocalDate endDate) { + + public LocalDate getStartDateWithDefault() { + return startDate != null ? startDate : LocalDate.now().minusMonths(2); + } + + public LocalDate getEndDateWithDefault() { + return endDate != null ? endDate : LocalDate.now(); + } +} diff --git a/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/request/SubcategoryDailyStatisticsSearchRequest.java b/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/request/SubcategoryDailyStatisticsSearchRequest.java new file mode 100644 index 0000000..47c2c96 --- /dev/null +++ b/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/request/SubcategoryDailyStatisticsSearchRequest.java @@ -0,0 +1,30 @@ +package until.the.eternity.statistics.interfaces.rest.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import java.time.LocalDate; +import org.springframework.format.annotation.DateTimeFormat; + +@Schema(description = "서브카테고리별 일간 통계 검색 요청") +public record SubcategoryDailyStatisticsSearchRequest( + @NotBlank(message = "탑 카테고리는 필수입니다") + @Schema(description = "아이템 탑 카테고리", example = "소모품", required = true) + String topCategory, + @NotBlank(message = "서브 카테고리는 필수입니다") + @Schema(description = "아이템 서브 카테고리", example = "포션", required = true) + String subCategory, + @Schema(description = "검색 시작 일자 (미입력 시 오늘로부터 2주 전)", example = "2026-01-01") + @DateTimeFormat(pattern = "yyyy-MM-dd") + LocalDate startDate, + @Schema(description = "검색 종료 일자 (미입력 시 오늘)", example = "2026-01-31") + @DateTimeFormat(pattern = "yyyy-MM-dd") + LocalDate endDate) { + + public LocalDate getStartDateWithDefault() { + return startDate != null ? startDate : LocalDate.now().minusDays(14); + } + + public LocalDate getEndDateWithDefault() { + return endDate != null ? endDate : LocalDate.now(); + } +} diff --git a/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/request/SubcategoryWeeklyStatisticsSearchRequest.java b/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/request/SubcategoryWeeklyStatisticsSearchRequest.java new file mode 100644 index 0000000..9d7bc71 --- /dev/null +++ b/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/request/SubcategoryWeeklyStatisticsSearchRequest.java @@ -0,0 +1,30 @@ +package until.the.eternity.statistics.interfaces.rest.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import java.time.LocalDate; +import org.springframework.format.annotation.DateTimeFormat; + +@Schema(description = "서브카테고리별 주간 통계 검색 요청") +public record SubcategoryWeeklyStatisticsSearchRequest( + @NotBlank(message = "탑 카테고리는 필수입니다") + @Schema(description = "아이템 탑 카테고리", example = "소모품", required = true) + String topCategory, + @NotBlank(message = "서브 카테고리는 필수입니다") + @Schema(description = "아이템 서브 카테고리", example = "포션", required = true) + String subCategory, + @Schema(description = "검색 시작 일자 (미입력 시 오늘로부터 2달 전)", example = "2026-01-01") + @DateTimeFormat(pattern = "yyyy-MM-dd") + LocalDate startDate, + @Schema(description = "검색 종료 일자 (미입력 시 오늘)", example = "2026-04-30") + @DateTimeFormat(pattern = "yyyy-MM-dd") + LocalDate endDate) { + + public LocalDate getStartDateWithDefault() { + return startDate != null ? startDate : LocalDate.now().minusMonths(2); + } + + public LocalDate getEndDateWithDefault() { + return endDate != null ? endDate : LocalDate.now(); + } +} diff --git a/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/request/TopCategoryDailyStatisticsSearchRequest.java b/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/request/TopCategoryDailyStatisticsSearchRequest.java new file mode 100644 index 0000000..4b8e25d --- /dev/null +++ b/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/request/TopCategoryDailyStatisticsSearchRequest.java @@ -0,0 +1,27 @@ +package until.the.eternity.statistics.interfaces.rest.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import java.time.LocalDate; +import org.springframework.format.annotation.DateTimeFormat; + +@Schema(description = "탑카테고리별 일간 통계 검색 요청") +public record TopCategoryDailyStatisticsSearchRequest( + @NotBlank(message = "탑 카테고리는 필수입니다") + @Schema(description = "아이템 탑 카테고리", example = "소모품", required = true) + String topCategory, + @Schema(description = "검색 시작 일자 (미입력 시 오늘로부터 2주 전)", example = "2026-01-01") + @DateTimeFormat(pattern = "yyyy-MM-dd") + LocalDate startDate, + @Schema(description = "검색 종료 일자 (미입력 시 오늘)", example = "2026-01-31") + @DateTimeFormat(pattern = "yyyy-MM-dd") + LocalDate endDate) { + + public LocalDate getStartDateWithDefault() { + return startDate != null ? startDate : LocalDate.now().minusDays(14); + } + + public LocalDate getEndDateWithDefault() { + return endDate != null ? endDate : LocalDate.now(); + } +} diff --git a/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/request/TopCategoryWeeklyStatisticsSearchRequest.java b/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/request/TopCategoryWeeklyStatisticsSearchRequest.java new file mode 100644 index 0000000..ad1c6b4 --- /dev/null +++ b/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/request/TopCategoryWeeklyStatisticsSearchRequest.java @@ -0,0 +1,27 @@ +package until.the.eternity.statistics.interfaces.rest.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import java.time.LocalDate; +import org.springframework.format.annotation.DateTimeFormat; + +@Schema(description = "탑카테고리별 주간 통계 검색 요청") +public record TopCategoryWeeklyStatisticsSearchRequest( + @NotBlank(message = "탑 카테고리는 필수입니다") + @Schema(description = "아이템 탑 카테고리", example = "소모품", required = true) + String topCategory, + @Schema(description = "검색 시작 일자 (미입력 시 오늘로부터 2달 전)", example = "2026-01-01") + @DateTimeFormat(pattern = "yyyy-MM-dd") + LocalDate startDate, + @Schema(description = "검색 종료 일자 (미입력 시 오늘)", example = "2026-04-30") + @DateTimeFormat(pattern = "yyyy-MM-dd") + LocalDate endDate) { + + public LocalDate getStartDateWithDefault() { + return startDate != null ? startDate : LocalDate.now().minusMonths(2); + } + + public LocalDate getEndDateWithDefault() { + return endDate != null ? endDate : LocalDate.now(); + } +} diff --git a/src/main/java/until/the/eternity/statistics/repository/daily/ItemDailyStatisticsRepository.java b/src/main/java/until/the/eternity/statistics/repository/daily/ItemDailyStatisticsRepository.java index 96894e0..d78b583 100644 --- a/src/main/java/until/the/eternity/statistics/repository/daily/ItemDailyStatisticsRepository.java +++ b/src/main/java/until/the/eternity/statistics/repository/daily/ItemDailyStatisticsRepository.java @@ -1,13 +1,38 @@ package until.the.eternity.statistics.repository.daily; +import java.time.LocalDate; +import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.transaction.annotation.Transactional; import until.the.eternity.statistics.domain.entity.daily.ItemDailyStatistics; public interface ItemDailyStatisticsRepository extends JpaRepository { + /** + * 아이템별 일간 통계 조회 + * + * @param itemName 아이템 이름 + * @param subCategory 서브 카테고리 + * @param topCategory 탑 카테고리 + * @param startDate 시작 일자 + * @param endDate 종료 일자 + * @return 해당 조건의 일간 통계 리스트 + */ + @Query( + "SELECT i FROM ItemDailyStatistics i WHERE i.itemName = :itemName " + + "AND i.itemSubCategory = :subCategory AND i.itemTopCategory = :topCategory " + + "AND i.dateAuctionBuy BETWEEN :startDate AND :endDate " + + "ORDER BY i.dateAuctionBuy ASC") + List findByItemAndDateRange( + @Param("itemName") String itemName, + @Param("subCategory") String subCategory, + @Param("topCategory") String topCategory, + @Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate); + /** * 당일 거래된 각 아이템의 통계를 item_daily_statistics 테이블에 upsert * AuctionHistoryScheduler가 실행될 때마다 당일 통계만 업데이트 diff --git a/src/main/java/until/the/eternity/statistics/repository/daily/SubcategoryDailyStatisticsRepository.java b/src/main/java/until/the/eternity/statistics/repository/daily/SubcategoryDailyStatisticsRepository.java index fdb790d..207547a 100644 --- a/src/main/java/until/the/eternity/statistics/repository/daily/SubcategoryDailyStatisticsRepository.java +++ b/src/main/java/until/the/eternity/statistics/repository/daily/SubcategoryDailyStatisticsRepository.java @@ -1,14 +1,34 @@ package until.the.eternity.statistics.repository.daily; +import java.time.LocalDate; +import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.transaction.annotation.Transactional; import until.the.eternity.statistics.domain.entity.daily.SubcategoryDailyStatistics; public interface SubcategoryDailyStatisticsRepository extends JpaRepository { + /** + * 서브카테고리별 일간 통계 조회 + * + * @param subCategory 서브 카테고리 + * @param startDate 시작 일자 + * @param endDate 종료 일자 + * @return 해당 조건의 일간 통계 리스트 + */ + @Query( + "SELECT s FROM SubcategoryDailyStatistics s WHERE s.itemSubCategory = :subCategory " + + "AND s.dateAuctionBuy BETWEEN :startDate AND :endDate " + + "ORDER BY s.dateAuctionBuy ASC") + List findBySubcategoryAndDateRange( + @Param("subCategory") String subCategory, + @Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate); + /** * 당일의 ItemDailyStatistics 데이터를 기반으로 서브카테고리별 통계를 집계하여 upsert * item_daily_statistics 테이블만 사용하여 효율적으로 집계 diff --git a/src/main/java/until/the/eternity/statistics/repository/daily/TopCategoryDailyStatisticsRepository.java b/src/main/java/until/the/eternity/statistics/repository/daily/TopCategoryDailyStatisticsRepository.java index 555ca79..148fad0 100644 --- a/src/main/java/until/the/eternity/statistics/repository/daily/TopCategoryDailyStatisticsRepository.java +++ b/src/main/java/until/the/eternity/statistics/repository/daily/TopCategoryDailyStatisticsRepository.java @@ -1,14 +1,34 @@ package until.the.eternity.statistics.repository.daily; +import java.time.LocalDate; +import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.transaction.annotation.Transactional; import until.the.eternity.statistics.domain.entity.daily.TopCategoryDailyStatistics; public interface TopCategoryDailyStatisticsRepository extends JpaRepository { + /** + * 탑카테고리별 일간 통계 조회 + * + * @param topCategory 탑 카테고리 + * @param startDate 시작 일자 + * @param endDate 종료 일자 + * @return 해당 조건의 일간 통계 리스트 + */ + @Query( + "SELECT t FROM TopCategoryDailyStatistics t WHERE t.itemTopCategory = :topCategory " + + "AND t.dateAuctionBuy BETWEEN :startDate AND :endDate " + + "ORDER BY t.dateAuctionBuy ASC") + List findByTopCategoryAndDateRange( + @Param("topCategory") String topCategory, + @Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate); + /** * 당일의 SubcategoryDailyStatistics 데이터를 기반으로 탑카테고리별 통계를 집계하여 upsert * item_daily_statistics 테이블을 사용하여 top_category 정보를 가져옴 diff --git a/src/main/java/until/the/eternity/statistics/repository/weekly/ItemWeeklyStatisticsRepository.java b/src/main/java/until/the/eternity/statistics/repository/weekly/ItemWeeklyStatisticsRepository.java index b77af6e..cfb3e18 100644 --- a/src/main/java/until/the/eternity/statistics/repository/weekly/ItemWeeklyStatisticsRepository.java +++ b/src/main/java/until/the/eternity/statistics/repository/weekly/ItemWeeklyStatisticsRepository.java @@ -1,13 +1,38 @@ package until.the.eternity.statistics.repository.weekly; +import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.transaction.annotation.Transactional; import until.the.eternity.statistics.domain.entity.weekly.ItemWeeklyStatistics; +import until.the.eternity.statistics.util.WeekConverter; public interface ItemWeeklyStatisticsRepository extends JpaRepository { + /** + * 아이템별 주간 통계 조회 + * + * @param itemName 아이템 이름 + * @param subCategory 서브 카테고리 + * @param topCategory 탑 카테고리 + * @param startDate 시작 날짜 + * @param endDate 종료 날짜 + * @return 해당 조건의 주간 통계 리스트 + */ + @Query( + "SELECT i FROM ItemWeeklyStatistics i WHERE i.itemName = :itemName " + + "AND i.itemSubCategory = :subCategory AND i.itemTopCategory = :topCategory " + + "AND i.weekStartDate BETWEEN :startDate AND :endDate " + + "ORDER BY i.year ASC, i.weekNumber ASC") + List findByItemAndDateRange( + @Param("itemName") String itemName, + @Param("subCategory") String subCategory, + @Param("topCategory") String topCategory, + @Param("startDate") java.time.LocalDate startDate, + @Param("endDate") java.time.LocalDate endDate); + /** 전주(지난 주 월~일)의 ItemDailyStatistics 데이터를 기반으로 아이템별 주간 통계를 집계하여 upsert */ @Modifying(clearAutomatically = true, flushAutomatically = true) @Transactional diff --git a/src/main/java/until/the/eternity/statistics/repository/weekly/SubcategoryWeeklyStatisticsRepository.java b/src/main/java/until/the/eternity/statistics/repository/weekly/SubcategoryWeeklyStatisticsRepository.java index d2b7559..8fee807 100644 --- a/src/main/java/until/the/eternity/statistics/repository/weekly/SubcategoryWeeklyStatisticsRepository.java +++ b/src/main/java/until/the/eternity/statistics/repository/weekly/SubcategoryWeeklyStatisticsRepository.java @@ -1,14 +1,34 @@ package until.the.eternity.statistics.repository.weekly; +import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.transaction.annotation.Transactional; import until.the.eternity.statistics.domain.entity.weekly.SubcategoryWeeklyStatistics; +import until.the.eternity.statistics.util.WeekConverter; public interface SubcategoryWeeklyStatisticsRepository extends JpaRepository { + /** + * 서브카테고리별 주간 통계 조회 + * + * @param subCategory 서브 카테고리 + * @param startDate 시작 날짜 + * @param endDate 종료 날짜 + * @return 해당 조건의 주간 통계 리스트 + */ + @Query( + "SELECT s FROM SubcategoryWeeklyStatistics s WHERE s.itemSubCategory = :subCategory " + + "AND s.weekStartDate BETWEEN :startDate AND :endDate " + + "ORDER BY s.year ASC, s.weekNumber ASC") + List findBySubcategoryAndDateRange( + @Param("subCategory") String subCategory, + @Param("startDate") java.time.LocalDate startDate, + @Param("endDate") java.time.LocalDate endDate); + /** 전주의 ItemWeeklyStatistics 데이터를 기반으로 서브카테고리별 주간 통계를 집계하여 upsert */ @Modifying(clearAutomatically = true, flushAutomatically = true) @Transactional diff --git a/src/main/java/until/the/eternity/statistics/repository/weekly/TopCategoryWeeklyStatisticsRepository.java b/src/main/java/until/the/eternity/statistics/repository/weekly/TopCategoryWeeklyStatisticsRepository.java index b6bb0dc..a13c242 100644 --- a/src/main/java/until/the/eternity/statistics/repository/weekly/TopCategoryWeeklyStatisticsRepository.java +++ b/src/main/java/until/the/eternity/statistics/repository/weekly/TopCategoryWeeklyStatisticsRepository.java @@ -1,14 +1,34 @@ package until.the.eternity.statistics.repository.weekly; +import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.transaction.annotation.Transactional; import until.the.eternity.statistics.domain.entity.weekly.TopCategoryWeeklyStatistics; +import until.the.eternity.statistics.util.WeekConverter; public interface TopCategoryWeeklyStatisticsRepository extends JpaRepository { + /** + * 탑카테고리별 주간 통계 조회 + * + * @param topCategory 탑 카테고리 + * @param startDate 시작 날짜 + * @param endDate 종료 날짜 + * @return 해당 조건의 주간 통계 리스트 + */ + @Query( + "SELECT t FROM TopCategoryWeeklyStatistics t WHERE t.itemTopCategory = :topCategory " + + "AND t.weekStartDate BETWEEN :startDate AND :endDate " + + "ORDER BY t.year ASC, t.weekNumber ASC") + List findByTopCategoryAndDateRange( + @Param("topCategory") String topCategory, + @Param("startDate") java.time.LocalDate startDate, + @Param("endDate") java.time.LocalDate endDate); + /** 전주의 SubcategoryWeeklyStatistics 데이터를 기반으로 탑카테고리별 주간 통계를 집계하여 upsert */ @Modifying(clearAutomatically = true, flushAutomatically = true) @Transactional diff --git a/src/main/java/until/the/eternity/statistics/util/DateRangeValidator.java b/src/main/java/until/the/eternity/statistics/util/DateRangeValidator.java new file mode 100644 index 0000000..4aa75f7 --- /dev/null +++ b/src/main/java/until/the/eternity/statistics/util/DateRangeValidator.java @@ -0,0 +1,61 @@ +package until.the.eternity.statistics.util; + +import java.time.LocalDate; +import java.time.temporal.ChronoUnit; + +public class DateRangeValidator { + + private static final long DAILY_MAX_DAYS = 30; + private static final long WEEKLY_MAX_DAYS = 120; // 4개월 (약 120일) + + /** + * Daily 통계 조회 날짜 범위 검증 (최대 30일) + * + * @param startDate 시작 일자 + * @param endDate 종료 일자 + * @throws IllegalArgumentException 날짜 범위가 유효하지 않은 경우 + */ + public static void validateDailyDateRange(LocalDate startDate, LocalDate endDate) { + validateBasicDateRange(startDate, endDate); + + long daysBetween = ChronoUnit.DAYS.between(startDate, endDate); + if (daysBetween > DAILY_MAX_DAYS) { + throw new IllegalArgumentException( + String.format( + "일간 통계 조회는 최대 %d일까지만 가능합니다. 요청 기간: %d일", + DAILY_MAX_DAYS, daysBetween)); + } + } + + /** + * Weekly 통계 조회 날짜 범위 검증 (최대 4개월, 약 120일) + * + * @param startDate 시작 일자 + * @param endDate 종료 일자 + * @throws IllegalArgumentException 날짜 범위가 유효하지 않은 경우 + */ + public static void validateWeeklyDateRange(LocalDate startDate, LocalDate endDate) { + validateBasicDateRange(startDate, endDate); + + long daysBetween = ChronoUnit.DAYS.between(startDate, endDate); + if (daysBetween > WEEKLY_MAX_DAYS) { + throw new IllegalArgumentException( + String.format( + "주간 통계 조회는 최대 4개월(%d일)까지만 가능합니다. 요청 기간: %d일", + WEEKLY_MAX_DAYS, daysBetween)); + } + } + + /** + * 기본 날짜 범위 검증 + * + * @param startDate 시작 일자 + * @param endDate 종료 일자 + * @throws IllegalArgumentException 시작일이 종료일보다 늦은 경우 + */ + private static void validateBasicDateRange(LocalDate startDate, LocalDate endDate) { + if (startDate.isAfter(endDate)) { + throw new IllegalArgumentException("시작 일자는 종료 일자보다 이전이어야 합니다."); + } + } +} diff --git a/src/main/java/until/the/eternity/statistics/util/WeekConverter.java b/src/main/java/until/the/eternity/statistics/util/WeekConverter.java new file mode 100644 index 0000000..a90adda --- /dev/null +++ b/src/main/java/until/the/eternity/statistics/util/WeekConverter.java @@ -0,0 +1,42 @@ +package until.the.eternity.statistics.util; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.temporal.IsoFields; +import java.time.temporal.TemporalAdjusters; +import java.util.ArrayList; +import java.util.List; + +public class WeekConverter { + + /** + * 날짜 범위를 Year-Week 리스트로 변환 ISO 8601 주 번호 체계 사용 (월요일 시작) + * + * @param startDate 시작 일자 + * @param endDate 종료 일자 + * @return Year-Week 조합 리스트 + */ + public static List convertToYearWeekList(LocalDate startDate, LocalDate endDate) { + List yearWeeks = new ArrayList<>(); + + // 시작 날짜가 속한 주의 월요일 + LocalDate current = startDate.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)); + + // 종료 날짜가 속한 주의 월요일 + LocalDate endWeekStart = endDate.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)); + + while (!current.isAfter(endWeekStart)) { + int year = current.get(IsoFields.WEEK_BASED_YEAR); + int weekNumber = current.get(IsoFields.WEEK_OF_WEEK_BASED_YEAR); + yearWeeks.add(new YearWeek(year, weekNumber)); + + // 다음 주로 이동 + current = current.plusWeeks(1); + } + + return yearWeeks; + } + + /** Year-Week 조합을 나타내는 record */ + public record YearWeek(int year, int weekNumber) {} +} diff --git a/src/test/java/until/the/eternity/statistics/application/service/ItemDailyStatisticsServiceTest.java b/src/test/java/until/the/eternity/statistics/application/service/ItemDailyStatisticsServiceTest.java deleted file mode 100644 index d9c3cbc..0000000 --- a/src/test/java/until/the/eternity/statistics/application/service/ItemDailyStatisticsServiceTest.java +++ /dev/null @@ -1,122 +0,0 @@ -package until.the.eternity.statistics.application.service; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Mockito.*; - -import java.math.BigDecimal; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.util.List; -import java.util.Optional; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageImpl; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import until.the.eternity.common.response.PageResponseDto; -import until.the.eternity.statistics.domain.entity.daily.ItemDailyStatistics; -import until.the.eternity.statistics.domain.mapper.ItemDailyStatisticsMapper; -import until.the.eternity.statistics.interfaces.rest.dto.response.ItemDailyStatisticsResponse; -import until.the.eternity.statistics.repository.daily.ItemDailyStatisticsRepository; - -@ExtendWith(MockitoExtension.class) -class ItemDailyStatisticsServiceTest { - - @Mock private ItemDailyStatisticsRepository repository; - @Mock private ItemDailyStatisticsMapper mapper; - - @InjectMocks private ItemDailyStatisticsService service; - - @Test - @DisplayName("findAll은 페이징된 통계 목록을 반환한다") - void findAll_should_return_paged_statistics() { - // given - Pageable pageable = PageRequest.of(0, 10); - ItemDailyStatistics entity = createMockEntity(); - ItemDailyStatisticsResponse response = createMockResponse(); - Page entityPage = new PageImpl<>(List.of(entity), pageable, 1); - - when(repository.findAll(pageable)).thenReturn(entityPage); - when(mapper.toDto(entity)).thenReturn(response); - - // when - PageResponseDto result = service.findAll(pageable); - - // then - assertThat(result.items()).hasSize(1).contains(response); - assertThat(result.meta().totalElements()).isEqualTo(1); - verify(repository).findAll(pageable); - verify(mapper).toDto(entity); - } - - @Test - @DisplayName("findById는 ID에 해당하는 통계를 반환한다") - void findById_should_return_statistics_when_exists() { - // given - Long id = 1L; - ItemDailyStatistics entity = createMockEntity(); - ItemDailyStatisticsResponse response = createMockResponse(); - - when(repository.findById(id)).thenReturn(Optional.of(entity)); - when(mapper.toDto(entity)).thenReturn(response); - - // when - ItemDailyStatisticsResponse result = service.findById(id); - - // then - assertThat(result).isEqualTo(response); - verify(repository).findById(id); - verify(mapper).toDto(entity); - } - - @Test - @DisplayName("findById는 데이터가 없으면 예외를 발생시킨다") - void findById_should_throw_exception_when_not_exists() { - // given - Long id = 999L; - when(repository.findById(id)).thenReturn(Optional.empty()); - - // when & then - assertThatThrownBy(() -> service.findById(id)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("ItemDailyStatistics not found"); - - verify(repository).findById(id); - verify(mapper, never()).toDto(any()); - } - - private ItemDailyStatistics createMockEntity() { - return ItemDailyStatistics.builder() - .id(1L) - .itemName("Test Item") - .dateAuctionBuy(LocalDate.of(2025, 7, 1)) - .minPrice(100000L) - .maxPrice(150000L) - .avgPrice(new BigDecimal("125000.00")) - .totalVolume(5000000L) - .totalQuantity(100L) - .createdAt(LocalDateTime.now()) - .updatedAt(LocalDateTime.now()) - .build(); - } - - private ItemDailyStatisticsResponse createMockResponse() { - return new ItemDailyStatisticsResponse( - 1L, - "Test Item", - LocalDate.of(2025, 7, 1), - 100000L, - 150000L, - new BigDecimal("125000.00"), - 5000000L, - 100L, - LocalDateTime.now(), - LocalDateTime.now()); - } -} diff --git a/src/test/java/until/the/eternity/statistics/application/service/ItemWeeklyStatisticsServiceTest.java b/src/test/java/until/the/eternity/statistics/application/service/ItemWeeklyStatisticsServiceTest.java deleted file mode 100644 index b7067b2..0000000 --- a/src/test/java/until/the/eternity/statistics/application/service/ItemWeeklyStatisticsServiceTest.java +++ /dev/null @@ -1,98 +0,0 @@ -package until.the.eternity.statistics.application.service; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.math.BigDecimal; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.util.List; -import java.util.Optional; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageImpl; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import until.the.eternity.common.response.PageResponseDto; -import until.the.eternity.statistics.domain.entity.weekly.ItemWeeklyStatistics; -import until.the.eternity.statistics.domain.mapper.ItemWeeklyStatisticsMapper; -import until.the.eternity.statistics.interfaces.rest.dto.response.ItemWeeklyStatisticsResponse; -import until.the.eternity.statistics.repository.weekly.ItemWeeklyStatisticsRepository; - -@ExtendWith(MockitoExtension.class) -class ItemWeeklyStatisticsServiceTest { - - @Mock private ItemWeeklyStatisticsRepository repository; - @Mock private ItemWeeklyStatisticsMapper mapper; - - @InjectMocks private ItemWeeklyStatisticsService service; - - @Test - @DisplayName("findAll은 페이징된 주간 통계 목록을 반환한다") - void findAll_should_return_paged_statistics() { - // given - Pageable pageable = PageRequest.of(0, 10); - ItemWeeklyStatistics entity = - ItemWeeklyStatistics.builder() - .id(1L) - .itemName("Test Item") - .year(2025) - .weekNumber(27) - .weekStartDate(LocalDate.of(2025, 7, 1)) - .minPrice(100000L) - .maxPrice(150000L) - .avgPrice(new BigDecimal("125000.00")) - .totalVolume(35000000L) - .totalQuantity(700L) - .createdAt(LocalDateTime.now()) - .updatedAt(LocalDateTime.now()) - .build(); - - ItemWeeklyStatisticsResponse response = - new ItemWeeklyStatisticsResponse( - 1L, - "Test Item", - 2025, - 27, - LocalDate.of(2025, 7, 1), - 100000L, - 150000L, - new BigDecimal("125000.00"), - 35000000L, - 700L, - LocalDateTime.now(), - LocalDateTime.now()); - - Page entityPage = new PageImpl<>(List.of(entity), pageable, 1); - - when(repository.findAll(pageable)).thenReturn(entityPage); - when(mapper.toDto(entity)).thenReturn(response); - - // when - PageResponseDto result = service.findAll(pageable); - - // then - assertThat(result.items()).hasSize(1).contains(response); - verify(repository).findAll(pageable); - } - - @Test - @DisplayName("findById는 데이터가 없으면 예외를 발생시킨다") - void findById_should_throw_exception_when_not_exists() { - // given - Long id = 999L; - when(repository.findById(id)).thenReturn(Optional.empty()); - - // when & then - assertThatThrownBy(() -> service.findById(id)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("ItemWeeklyStatistics not found"); - } -} diff --git a/src/test/java/until/the/eternity/statistics/application/service/SubcategoryDailyStatisticsServiceTest.java b/src/test/java/until/the/eternity/statistics/application/service/SubcategoryDailyStatisticsServiceTest.java deleted file mode 100644 index 9eff9c8..0000000 --- a/src/test/java/until/the/eternity/statistics/application/service/SubcategoryDailyStatisticsServiceTest.java +++ /dev/null @@ -1,94 +0,0 @@ -package until.the.eternity.statistics.application.service; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.math.BigDecimal; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.util.List; -import java.util.Optional; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageImpl; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import until.the.eternity.common.response.PageResponseDto; -import until.the.eternity.statistics.domain.entity.daily.SubcategoryDailyStatistics; -import until.the.eternity.statistics.domain.mapper.SubcategoryDailyStatisticsMapper; -import until.the.eternity.statistics.interfaces.rest.dto.response.SubcategoryDailyStatisticsResponse; -import until.the.eternity.statistics.repository.daily.SubcategoryDailyStatisticsRepository; - -@ExtendWith(MockitoExtension.class) -class SubcategoryDailyStatisticsServiceTest { - - @Mock private SubcategoryDailyStatisticsRepository repository; - @Mock private SubcategoryDailyStatisticsMapper mapper; - - @InjectMocks private SubcategoryDailyStatisticsService service; - - @Test - @DisplayName("findAll은 페이징된 통계 목록을 반환한다") - void findAll_should_return_paged_statistics() { - // given - Pageable pageable = PageRequest.of(0, 10); - SubcategoryDailyStatistics entity = - SubcategoryDailyStatistics.builder() - .id(1L) - .itemSubCategory("한손검") - .dateAuctionBuy(LocalDate.of(2025, 7, 1)) - .minPrice(100000L) - .maxPrice(150000L) - .avgPrice(new BigDecimal("125000.00")) - .totalVolume(50000000L) - .totalQuantity(1000L) - .createdAt(LocalDateTime.now()) - .updatedAt(LocalDateTime.now()) - .build(); - - SubcategoryDailyStatisticsResponse response = - new SubcategoryDailyStatisticsResponse( - 1L, - "한손검", - LocalDate.of(2025, 7, 1), - 100000L, - 150000L, - new BigDecimal("125000.00"), - 50000000L, - 1000L, - LocalDateTime.now(), - LocalDateTime.now()); - - Page entityPage = new PageImpl<>(List.of(entity), pageable, 1); - - when(repository.findAll(pageable)).thenReturn(entityPage); - when(mapper.toDto(entity)).thenReturn(response); - - // when - PageResponseDto result = service.findAll(pageable); - - // then - assertThat(result.items()).hasSize(1).contains(response); - verify(repository).findAll(pageable); - } - - @Test - @DisplayName("findById는 데이터가 없으면 예외를 발생시킨다") - void findById_should_throw_exception_when_not_exists() { - // given - Long id = 999L; - when(repository.findById(id)).thenReturn(Optional.empty()); - - // when & then - assertThatThrownBy(() -> service.findById(id)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("SubcategoryDailyStatistics not found"); - } -} diff --git a/src/test/java/until/the/eternity/statistics/application/service/SubcategoryWeeklyStatisticsServiceTest.java b/src/test/java/until/the/eternity/statistics/application/service/SubcategoryWeeklyStatisticsServiceTest.java deleted file mode 100644 index 889913b..0000000 --- a/src/test/java/until/the/eternity/statistics/application/service/SubcategoryWeeklyStatisticsServiceTest.java +++ /dev/null @@ -1,36 +0,0 @@ -package until.the.eternity.statistics.application.service; - -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Mockito.when; - -import java.util.Optional; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import until.the.eternity.statistics.domain.mapper.SubcategoryWeeklyStatisticsMapper; -import until.the.eternity.statistics.repository.weekly.SubcategoryWeeklyStatisticsRepository; - -@ExtendWith(MockitoExtension.class) -class SubcategoryWeeklyStatisticsServiceTest { - - @Mock private SubcategoryWeeklyStatisticsRepository repository; - @Mock private SubcategoryWeeklyStatisticsMapper mapper; - - @InjectMocks private SubcategoryWeeklyStatisticsService service; - - @Test - @DisplayName("findById는 데이터가 없으면 예외를 발생시킨다") - void findById_should_throw_exception_when_not_exists() { - // given - Long id = 999L; - when(repository.findById(id)).thenReturn(Optional.empty()); - - // when & then - assertThatThrownBy(() -> service.findById(id)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("SubcategoryWeeklyStatistics not found"); - } -} diff --git a/src/test/java/until/the/eternity/statistics/application/service/TopCategoryDailyStatisticsServiceTest.java b/src/test/java/until/the/eternity/statistics/application/service/TopCategoryDailyStatisticsServiceTest.java deleted file mode 100644 index 1e1130c..0000000 --- a/src/test/java/until/the/eternity/statistics/application/service/TopCategoryDailyStatisticsServiceTest.java +++ /dev/null @@ -1,36 +0,0 @@ -package until.the.eternity.statistics.application.service; - -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Mockito.when; - -import java.util.Optional; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import until.the.eternity.statistics.domain.mapper.TopCategoryDailyStatisticsMapper; -import until.the.eternity.statistics.repository.daily.TopCategoryDailyStatisticsRepository; - -@ExtendWith(MockitoExtension.class) -class TopCategoryDailyStatisticsServiceTest { - - @Mock private TopCategoryDailyStatisticsRepository repository; - @Mock private TopCategoryDailyStatisticsMapper mapper; - - @InjectMocks private TopCategoryDailyStatisticsService service; - - @Test - @DisplayName("findById는 데이터가 없으면 예외를 발생시킨다") - void findById_should_throw_exception_when_not_exists() { - // given - Long id = 999L; - when(repository.findById(id)).thenReturn(Optional.empty()); - - // when & then - assertThatThrownBy(() -> service.findById(id)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("TopCategoryDailyStatistics not found"); - } -} diff --git a/src/test/java/until/the/eternity/statistics/application/service/TopCategoryWeeklyStatisticsServiceTest.java b/src/test/java/until/the/eternity/statistics/application/service/TopCategoryWeeklyStatisticsServiceTest.java deleted file mode 100644 index 1a425c5..0000000 --- a/src/test/java/until/the/eternity/statistics/application/service/TopCategoryWeeklyStatisticsServiceTest.java +++ /dev/null @@ -1,36 +0,0 @@ -package until.the.eternity.statistics.application.service; - -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Mockito.when; - -import java.util.Optional; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import until.the.eternity.statistics.domain.mapper.TopCategoryWeeklyStatisticsMapper; -import until.the.eternity.statistics.repository.weekly.TopCategoryWeeklyStatisticsRepository; - -@ExtendWith(MockitoExtension.class) -class TopCategoryWeeklyStatisticsServiceTest { - - @Mock private TopCategoryWeeklyStatisticsRepository repository; - @Mock private TopCategoryWeeklyStatisticsMapper mapper; - - @InjectMocks private TopCategoryWeeklyStatisticsService service; - - @Test - @DisplayName("findById는 데이터가 없으면 예외를 발생시킨다") - void findById_should_throw_exception_when_not_exists() { - // given - Long id = 999L; - when(repository.findById(id)).thenReturn(Optional.empty()); - - // when & then - assertThatThrownBy(() -> service.findById(id)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("TopCategoryWeeklyStatistics not found"); - } -}