From 0ed2300e4f66bd7047658f251ce0d1cb968b680b Mon Sep 17 00:00:00 2001 From: dev-ant Date: Mon, 26 Jan 2026 21:58:13 +0900 Subject: [PATCH 1/8] =?UTF-8?q?feat:=20auction=20realtime=20flyway=20scrip?= =?UTF-8?q?t=20=EB=B0=8F=20=EC=8A=A4=EC=BC=80=EC=A5=B4=EB=9F=AC=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../scheduler/AuctionHistoryScheduler.java | 5 +- .../service/AuctionHistoryService.java | 3 +- .../fetcher/AuctionHistoryFetcher.java | 7 +- .../persister/AuctionHistoryPersister.java | 3 +- .../domain/entity/AuctionHistory.java | 11 +- .../event/AuctionHistorySavedEvent.java | 3 +- .../domain/mapper/AuctionHistoryMapper.java | 16 +- .../mapper/OpenApiAuctionHistoryMapper.java | 9 +- .../mapper/OpenApiItemOptionMapper.java | 5 +- .../AuctionHistoryRepositoryPort.java | 9 +- .../AuctionHistoryDuplicateChecker.java | 9 +- .../fetcher/AuctionHistoryFetcherPort.java | 3 +- .../AuctionHistoryPersisterPort.java | 3 +- .../AuctionHistoryJpaRepository.java | 7 +- .../AuctionHistoryQueryDslRepository.java | 30 ++- .../AuctionHistoryRepositoryPortImpl.java | 9 +- .../OpenApiAuctionHistoryListResponse.java | 1 - .../dto/OpenApiAuctionHistoryResponse.java | 3 +- .../rest/dto/enums/SearchStandard.java | 1 - .../rest/dto/enums/SortDirection.java | 1 - .../AuctionHistoryDetailResponse.java | 1 - .../domain/entity/AuctionItem.java | 40 --- .../domain/entity/AuctionRealtimeItem.java | 57 ++++ .../entity/AuctionRealtimeItemOption.java | 63 +++++ ...ion.java => AuctionHistoryItemOption.java} | 23 +- .../scheduler/AuctionRealtimeScheduler.java | 151 +++++++++++ .../service/AuctionRealtimeService.java | 75 ++++++ .../fetcher/AuctionRealtimeFetcher.java | 100 +++++++ .../persister/AuctionRealtimePersister.java | 53 ++++ .../mapper/OpenApiAuctionRealtimeMapper.java | 41 +++ .../OpenApiRealtimeItemOptionMapper.java | 15 ++ .../AuctionRealtimeItemRepositoryPort.java | 43 +++ .../AuctionRealtimeDuplicateChecker.java | 131 ++++++++++ .../fetcher/AuctionRealtimeFetcherPort.java | 33 +++ .../AuctionRealtimePersisterPort.java | 24 ++ .../client/AuctionRealtimeClient.java | 59 +++++ .../AuctionRealtimeItemRepository.java | 34 +++ ...AuctionRealtimeItemRepositoryPortImpl.java | 63 +++++ .../OpenApiAuctionRealtimeListResponse.java | 9 + .../dto/OpenApiAuctionRealtimeResponse.java | 18 ++ .../service/AuctionSearchOptionService.java | 5 +- .../entity/AuctionSearchOptionMetadata.java | 3 +- .../AuctionSearchOptionRepositoryPort.java | 3 +- .../AuctionSearchOptionJpaRepository.java | 3 +- ...AuctionSearchOptionRepositoryPortImpl.java | 3 +- .../rest/AuctionSearchOptionController.java | 3 +- .../rest/dto/response/FieldMetadata.java | 1 - .../SearchOptionMetadataResponse.java | 1 - .../eternity/common/enums/ItemCategory.java | 5 +- .../eternity/common/enums/SortDirection.java | 3 +- .../the/eternity/common/enums/SortField.java | 1 - .../common/exception/GlobalExceptionCode.java | 4 +- .../exception/GlobalExceptionHandler.java | 4 +- .../common/filter/GatewayAuthFilter.java | 7 +- .../eternity/common/response/ApiResponse.java | 3 +- .../common/response/PageResponseDto.java | 1 - .../config/openapi/OpenApiFilters.java | 3 +- .../config/openapi/OpenApiRetryPolicy.java | 3 +- .../openapi/OpenApiWebClientProperties.java | 3 +- .../scheduler/HornBugleScheduler.java | 5 +- .../application/service/HornBugleService.java | 5 +- .../domain/entity/HornBugleWorldHistory.java | 3 +- .../domain/enums/HornBugleServer.java | 5 +- .../domain/mapper/HornBugleMapper.java | 3 +- .../repository/HornBugleRepositoryPort.java | 7 +- .../service/HornBugleDuplicateChecker.java | 11 +- .../persistence/HornBugleJpaRepository.java | 7 +- .../HornBugleRepositoryPortImpl.java | 7 +- .../OpenApiHornBugleHistoryListResponse.java | 1 - .../dto/OpenApiHornBugleHistoryResponse.java | 1 - .../response/HornBugleHistoryResponse.java | 1 - .../application/service/ItemInfoService.java | 9 +- .../iteminfo/domain/entity/ItemInfoId.java | 3 +- .../exception/ItemInfoExceptionCode.java | 4 +- .../repository/ItemInfoRepositoryPort.java | 3 +- .../persistence/ItemInfoJpaRepository.java | 3 +- .../ItemInfoQueryDslRepository.java | 3 +- .../ItemInfoRepositoryPortImpl.java | 3 +- .../rest/controller/ItemInfoController.java | 3 +- .../dto/response/ItemCategoryResponse.java | 7 +- .../rest/dto/response/ItemInfoResponse.java | 5 +- .../dto/response/ItemInfoSummaryResponse.java | 5 +- .../dto/response/ItemInfoSyncResponse.java | 3 +- .../service/ItemOptionInfoService.java | 3 +- .../domain/entity/ItemOptionInfoId.java | 5 +- .../ItemOptionInfoRepositoryPort.java | 3 +- .../ItemOptionInfoRepositoryPortImpl.java | 3 +- .../controller/ItemOptionInfoController.java | 5 +- .../service/MetalwareInfoService.java | 3 +- .../MetalwareInfoJpaRepository.java | 3 +- .../MetalwareInfoRepositoryPortImpl.java | 3 +- .../controller/MetalwareInfoController.java | 3 +- .../dto/response/MetalwareInfoResponse.java | 3 +- .../entity/daily/ItemDailyStatistics.java | 3 +- .../daily/SubcategoryDailyStatistics.java | 3 +- .../daily/TopCategoryDailyStatistics.java | 3 +- .../entity/weekly/ItemWeeklyStatistics.java | 3 +- .../weekly/SubcategoryWeeklyStatistics.java | 3 +- .../weekly/TopCategoryWeeklyStatistics.java | 3 +- .../request/DailyStatisticsSearchRequest.java | 3 +- .../ItemDailyStatisticsSearchRequest.java | 3 +- .../ItemWeeklyStatisticsSearchRequest.java | 3 +- ...bcategoryDailyStatisticsSearchRequest.java | 3 +- ...categoryWeeklyStatisticsSearchRequest.java | 3 +- ...pCategoryDailyStatisticsSearchRequest.java | 3 +- ...CategoryWeeklyStatisticsSearchRequest.java | 3 +- .../response/ItemDailyStatisticsResponse.java | 1 - .../ItemWeeklyStatisticsResponse.java | 1 - .../SubcategoryDailyStatisticsResponse.java | 1 - .../SubcategoryWeeklyStatisticsResponse.java | 1 - .../TopCategoryDailyStatisticsResponse.java | 1 - .../TopCategoryWeeklyStatisticsResponse.java | 1 - .../daily/ItemDailyStatisticsRepository.java | 5 +- .../SubcategoryDailyStatisticsRepository.java | 5 +- .../TopCategoryDailyStatisticsRepository.java | 5 +- .../ItemWeeklyStatisticsRepository.java | 3 +- ...SubcategoryWeeklyStatisticsRepository.java | 3 +- ...TopCategoryWeeklyStatisticsRepository.java | 3 +- src/main/resources/application.yml | 5 + ...tables_for_realtime_history_separation.sql | 52 ++++ .../service/AuctionHistoryServiceTest.java | 13 +- .../fetcher/AuctionHistoryFetcherTest.java | 17 +- .../AuctionHistoryPersisterTest.java | 13 +- .../AuctionHistoryDuplicateCheckerTest.java | 17 +- .../fetcher/AuctionRealtimeFetcherTest.java | 246 ++++++++++++++++++ .../AuctionRealtimeDuplicateCheckerTest.java | 237 +++++++++++++++++ .../AuctionSearchOptionServiceTest.java | 11 +- .../service/ItemInfoServiceTest.java | 13 +- .../service/MetalwareInfoServiceTest.java | 11 +- 129 files changed, 1721 insertions(+), 354 deletions(-) delete mode 100644 src/main/java/until/the/eternity/auctionitem/domain/entity/AuctionItem.java create mode 100644 src/main/java/until/the/eternity/auctionitem/domain/entity/AuctionRealtimeItem.java create mode 100644 src/main/java/until/the/eternity/auctionitem/domain/entity/AuctionRealtimeItemOption.java rename src/main/java/until/the/eternity/auctionitemoption/domain/entity/{AuctionItemOption.java => AuctionHistoryItemOption.java} (72%) create mode 100644 src/main/java/until/the/eternity/auctionrealtime/application/scheduler/AuctionRealtimeScheduler.java create mode 100644 src/main/java/until/the/eternity/auctionrealtime/application/service/AuctionRealtimeService.java create mode 100644 src/main/java/until/the/eternity/auctionrealtime/application/service/fetcher/AuctionRealtimeFetcher.java create mode 100644 src/main/java/until/the/eternity/auctionrealtime/application/service/persister/AuctionRealtimePersister.java create mode 100644 src/main/java/until/the/eternity/auctionrealtime/domain/mapper/OpenApiAuctionRealtimeMapper.java create mode 100644 src/main/java/until/the/eternity/auctionrealtime/domain/mapper/OpenApiRealtimeItemOptionMapper.java create mode 100644 src/main/java/until/the/eternity/auctionrealtime/domain/repository/AuctionRealtimeItemRepositoryPort.java create mode 100644 src/main/java/until/the/eternity/auctionrealtime/domain/service/AuctionRealtimeDuplicateChecker.java create mode 100644 src/main/java/until/the/eternity/auctionrealtime/domain/service/fetcher/AuctionRealtimeFetcherPort.java create mode 100644 src/main/java/until/the/eternity/auctionrealtime/domain/service/persister/AuctionRealtimePersisterPort.java create mode 100644 src/main/java/until/the/eternity/auctionrealtime/infrastructure/client/AuctionRealtimeClient.java create mode 100644 src/main/java/until/the/eternity/auctionrealtime/infrastructure/persistence/AuctionRealtimeItemRepository.java create mode 100644 src/main/java/until/the/eternity/auctionrealtime/infrastructure/persistence/AuctionRealtimeItemRepositoryPortImpl.java create mode 100644 src/main/java/until/the/eternity/auctionrealtime/interfaces/external/dto/OpenApiAuctionRealtimeListResponse.java create mode 100644 src/main/java/until/the/eternity/auctionrealtime/interfaces/external/dto/OpenApiAuctionRealtimeResponse.java create mode 100644 src/main/resources/db/migration/V15__refactor_auction_tables_for_realtime_history_separation.sql create mode 100644 src/test/java/until/the/eternity/auctionrealtime/application/service/fetcher/AuctionRealtimeFetcherTest.java create mode 100644 src/test/java/until/the/eternity/auctionrealtime/domain/service/AuctionRealtimeDuplicateCheckerTest.java diff --git a/src/main/java/until/the/eternity/auctionhistory/application/scheduler/AuctionHistoryScheduler.java b/src/main/java/until/the/eternity/auctionhistory/application/scheduler/AuctionHistoryScheduler.java index 2ef0398a..3d29417e 100644 --- a/src/main/java/until/the/eternity/auctionhistory/application/scheduler/AuctionHistoryScheduler.java +++ b/src/main/java/until/the/eternity/auctionhistory/application/scheduler/AuctionHistoryScheduler.java @@ -1,5 +1,7 @@ package until.the.eternity.auctionhistory.application.scheduler; +import java.util.*; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; @@ -14,9 +16,6 @@ import until.the.eternity.auctionhistory.interfaces.external.dto.OpenApiAuctionHistoryResponse; import until.the.eternity.common.enums.ItemCategory; -import java.util.*; -import java.util.stream.Collectors; - @Slf4j @Component @RequiredArgsConstructor diff --git a/src/main/java/until/the/eternity/auctionhistory/application/service/AuctionHistoryService.java b/src/main/java/until/the/eternity/auctionhistory/application/service/AuctionHistoryService.java index 07a7ed6f..cc15bfd1 100644 --- a/src/main/java/until/the/eternity/auctionhistory/application/service/AuctionHistoryService.java +++ b/src/main/java/until/the/eternity/auctionhistory/application/service/AuctionHistoryService.java @@ -1,6 +1,7 @@ package until.the.eternity.auctionhistory.application.service; import jakarta.persistence.EntityManager; +import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Page; @@ -15,8 +16,6 @@ import until.the.eternity.common.request.PageRequestDto; import until.the.eternity.common.response.PageResponseDto; -import java.util.List; - @Service @RequiredArgsConstructor @Slf4j diff --git a/src/main/java/until/the/eternity/auctionhistory/application/service/fetcher/AuctionHistoryFetcher.java b/src/main/java/until/the/eternity/auctionhistory/application/service/fetcher/AuctionHistoryFetcher.java index 3aa40844..4c0e05ad 100644 --- a/src/main/java/until/the/eternity/auctionhistory/application/service/fetcher/AuctionHistoryFetcher.java +++ b/src/main/java/until/the/eternity/auctionhistory/application/service/fetcher/AuctionHistoryFetcher.java @@ -1,5 +1,8 @@ package until.the.eternity.auctionhistory.application.service.fetcher; +import java.util.ArrayList; +import java.util.List; +import java.util.OptionalInt; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; @@ -9,10 +12,6 @@ import until.the.eternity.auctionhistory.interfaces.external.dto.OpenApiAuctionHistoryResponse; import until.the.eternity.common.enums.ItemCategory; -import java.util.ArrayList; -import java.util.List; -import java.util.OptionalInt; - @Slf4j @Component @RequiredArgsConstructor diff --git a/src/main/java/until/the/eternity/auctionhistory/application/service/persister/AuctionHistoryPersister.java b/src/main/java/until/the/eternity/auctionhistory/application/service/persister/AuctionHistoryPersister.java index ee426dd5..8112b804 100644 --- a/src/main/java/until/the/eternity/auctionhistory/application/service/persister/AuctionHistoryPersister.java +++ b/src/main/java/until/the/eternity/auctionhistory/application/service/persister/AuctionHistoryPersister.java @@ -1,5 +1,6 @@ package until.the.eternity.auctionhistory.application.service.persister; +import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; @@ -10,8 +11,6 @@ import until.the.eternity.auctionhistory.interfaces.external.dto.OpenApiAuctionHistoryResponse; import until.the.eternity.common.enums.ItemCategory; -import java.util.List; - @Slf4j @RequiredArgsConstructor @Component diff --git a/src/main/java/until/the/eternity/auctionhistory/domain/entity/AuctionHistory.java b/src/main/java/until/the/eternity/auctionhistory/domain/entity/AuctionHistory.java index 201e09a3..af355e14 100644 --- a/src/main/java/until/the/eternity/auctionhistory/domain/entity/AuctionHistory.java +++ b/src/main/java/until/the/eternity/auctionhistory/domain/entity/AuctionHistory.java @@ -1,11 +1,10 @@ package until.the.eternity.auctionhistory.domain.entity; import jakarta.persistence.*; -import lombok.*; -import until.the.eternity.auctionitemoption.domain.entity.AuctionItemOption; - import java.time.Instant; import java.util.List; +import lombok.*; +import until.the.eternity.auctionitemoption.domain.entity.AuctionHistoryItemOption; @Entity @Table(name = "auction_history") @@ -36,7 +35,7 @@ public class AuctionHistory { private Instant dateAuctionBuy; @OneToMany(mappedBy = "auctionHistory", cascade = CascadeType.ALL, orphanRemoval = true) - private List auctionItemOptions; + private List auctionHistoryItemOptions; @Column(name = "item_sub_category", nullable = false) private String itemSubCategory; @@ -45,8 +44,8 @@ public class AuctionHistory { private String itemTopCategory; public AuctionHistory linkItemOptions() { - if (this.auctionItemOptions != null) { - for (AuctionItemOption o : this.auctionItemOptions) { + if (this.auctionHistoryItemOptions != null) { + for (AuctionHistoryItemOption o : this.auctionHistoryItemOptions) { o.setAuctionHistory(this); } } diff --git a/src/main/java/until/the/eternity/auctionhistory/domain/event/AuctionHistorySavedEvent.java b/src/main/java/until/the/eternity/auctionhistory/domain/event/AuctionHistorySavedEvent.java index fe8f771f..213706e6 100644 --- a/src/main/java/until/the/eternity/auctionhistory/domain/event/AuctionHistorySavedEvent.java +++ b/src/main/java/until/the/eternity/auctionhistory/domain/event/AuctionHistorySavedEvent.java @@ -1,8 +1,7 @@ package until.the.eternity.auctionhistory.domain.event; -import lombok.Getter; - import java.time.LocalDateTime; +import lombok.Getter; /** 경매장 거래 내역 저장 완료 이벤트 AuctionHistoryScheduler가 거래 내역을 성공적으로 저장한 후 발행됩니다. */ @Getter diff --git a/src/main/java/until/the/eternity/auctionhistory/domain/mapper/AuctionHistoryMapper.java b/src/main/java/until/the/eternity/auctionhistory/domain/mapper/AuctionHistoryMapper.java index 2fd9fb25..93a6fcaa 100644 --- a/src/main/java/until/the/eternity/auctionhistory/domain/mapper/AuctionHistoryMapper.java +++ b/src/main/java/until/the/eternity/auctionhistory/domain/mapper/AuctionHistoryMapper.java @@ -1,13 +1,12 @@ package until.the.eternity.auctionhistory.domain.mapper; +import java.util.List; import org.mapstruct.Mapper; import org.mapstruct.Mapping; import until.the.eternity.auctionhistory.domain.entity.AuctionHistory; import until.the.eternity.auctionhistory.interfaces.rest.dto.response.AuctionHistoryDetailResponse; import until.the.eternity.auctionhistory.interfaces.rest.dto.response.ItemOptionResponse; -import until.the.eternity.auctionitemoption.domain.entity.AuctionItemOption; - -import java.util.List; +import until.the.eternity.auctionitemoption.domain.entity.AuctionHistoryItemOption; /** * AuctionHistory Entity to internal.responseDto transfer mapper class 데이터 흐름은 external.responseDto @@ -17,17 +16,16 @@ public interface AuctionHistoryMapper { // Entity → DTO - @Mapping(target = "itemOptions", source = "auctionItemOptions") + @Mapping(target = "itemOptions", source = "auctionHistoryItemOptions") AuctionHistoryDetailResponse toDto(AuctionHistory entity); // 하위 매핑 @Mapping(target = "auctionHistory", ignore = true) - @Mapping(target = "auctionItem", ignore = true) - AuctionItemOption toEntity(ItemOptionResponse dto); + AuctionHistoryItemOption toEntity(ItemOptionResponse dto); - ItemOptionResponse toDto(AuctionItemOption entity); + ItemOptionResponse toDto(AuctionHistoryItemOption entity); - List toEntityList(List dtoList); + List toEntityList(List dtoList); - List toDtoList(List entityList); + List toDtoList(List entityList); } diff --git a/src/main/java/until/the/eternity/auctionhistory/domain/mapper/OpenApiAuctionHistoryMapper.java b/src/main/java/until/the/eternity/auctionhistory/domain/mapper/OpenApiAuctionHistoryMapper.java index c0065857..8322920b 100644 --- a/src/main/java/until/the/eternity/auctionhistory/domain/mapper/OpenApiAuctionHistoryMapper.java +++ b/src/main/java/until/the/eternity/auctionhistory/domain/mapper/OpenApiAuctionHistoryMapper.java @@ -1,20 +1,19 @@ package until.the.eternity.auctionhistory.domain.mapper; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.List; import org.mapstruct.*; import until.the.eternity.auctionhistory.domain.entity.AuctionHistory; import until.the.eternity.auctionhistory.interfaces.external.dto.OpenApiAuctionHistoryResponse; import until.the.eternity.common.enums.ItemCategory; -import java.time.Instant; -import java.time.temporal.ChronoUnit; -import java.util.List; - @Mapper(componentModel = "spring", uses = OpenApiItemOptionMapper.class) public interface OpenApiAuctionHistoryMapper { @Named("toEntity(OpenApiAuctionHistoryResponse, ItemCategory)") @Mapping(source = "dateAuctionBuy", target = "dateAuctionBuy", qualifiedByName = "utcToKst") - @Mapping(source = "openApiAuctionItemOptionResponse", target = "auctionItemOptions") + @Mapping(source = "openApiAuctionItemOptionResponse", target = "auctionHistoryItemOptions") @Mapping( target = "itemTopCategory", expression = "java(ItemCategory.findTopCategory(dto.itemSubCategory()))") diff --git a/src/main/java/until/the/eternity/auctionhistory/domain/mapper/OpenApiItemOptionMapper.java b/src/main/java/until/the/eternity/auctionhistory/domain/mapper/OpenApiItemOptionMapper.java index c5ddce3d..c3fddad9 100644 --- a/src/main/java/until/the/eternity/auctionhistory/domain/mapper/OpenApiItemOptionMapper.java +++ b/src/main/java/until/the/eternity/auctionhistory/domain/mapper/OpenApiItemOptionMapper.java @@ -3,13 +3,12 @@ import org.mapstruct.Mapper; import org.mapstruct.Mapping; import until.the.eternity.auctionitemoption.domain.dto.external.OpenApiAuctionItemOptionResponse; -import until.the.eternity.auctionitemoption.domain.entity.AuctionItemOption; +import until.the.eternity.auctionitemoption.domain.entity.AuctionHistoryItemOption; @Mapper(componentModel = "spring") public interface OpenApiItemOptionMapper { @Mapping(target = "id", ignore = true) // PK 자동 생성 @Mapping(target = "auctionHistory", ignore = true) - @Mapping(target = "auctionItem", ignore = true) - AuctionItemOption toEntity(OpenApiAuctionItemOptionResponse itemOption); + AuctionHistoryItemOption toEntity(OpenApiAuctionItemOptionResponse itemOption); } diff --git a/src/main/java/until/the/eternity/auctionhistory/domain/repository/AuctionHistoryRepositoryPort.java b/src/main/java/until/the/eternity/auctionhistory/domain/repository/AuctionHistoryRepositoryPort.java index f8a19073..ca490fa1 100644 --- a/src/main/java/until/the/eternity/auctionhistory/domain/repository/AuctionHistoryRepositoryPort.java +++ b/src/main/java/until/the/eternity/auctionhistory/domain/repository/AuctionHistoryRepositoryPort.java @@ -1,16 +1,15 @@ package until.the.eternity.auctionhistory.domain.repository; +import java.time.Instant; +import java.util.List; +import java.util.Optional; +import java.util.Set; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import until.the.eternity.auctionhistory.domain.entity.AuctionHistory; import until.the.eternity.auctionhistory.interfaces.rest.dto.request.AuctionHistorySearchRequest; import until.the.eternity.common.enums.ItemCategory; -import java.time.Instant; -import java.util.List; -import java.util.Optional; -import java.util.Set; - /** 경매장 거래 내역 POJO Repository - Mock 또는 Stub 으로 대체해 단위 테스트 용이성 확보 */ public interface AuctionHistoryRepositoryPort { diff --git a/src/main/java/until/the/eternity/auctionhistory/domain/service/AuctionHistoryDuplicateChecker.java b/src/main/java/until/the/eternity/auctionhistory/domain/service/AuctionHistoryDuplicateChecker.java index bdb29aaf..70b50dc2 100644 --- a/src/main/java/until/the/eternity/auctionhistory/domain/service/AuctionHistoryDuplicateChecker.java +++ b/src/main/java/until/the/eternity/auctionhistory/domain/service/AuctionHistoryDuplicateChecker.java @@ -1,5 +1,9 @@ package until.the.eternity.auctionhistory.domain.service; +import java.time.Instant; +import java.util.List; +import java.util.OptionalInt; +import java.util.Set; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; @@ -8,11 +12,6 @@ import until.the.eternity.auctionhistory.interfaces.external.dto.OpenApiAuctionHistoryResponse; import until.the.eternity.common.enums.ItemCategory; -import java.time.Instant; -import java.util.List; -import java.util.OptionalInt; -import java.util.Set; - @Slf4j @Component @RequiredArgsConstructor diff --git a/src/main/java/until/the/eternity/auctionhistory/domain/service/fetcher/AuctionHistoryFetcherPort.java b/src/main/java/until/the/eternity/auctionhistory/domain/service/fetcher/AuctionHistoryFetcherPort.java index cf00412a..5b649be4 100644 --- a/src/main/java/until/the/eternity/auctionhistory/domain/service/fetcher/AuctionHistoryFetcherPort.java +++ b/src/main/java/until/the/eternity/auctionhistory/domain/service/fetcher/AuctionHistoryFetcherPort.java @@ -1,10 +1,9 @@ package until.the.eternity.auctionhistory.domain.service.fetcher; +import java.util.List; import until.the.eternity.auctionhistory.interfaces.external.dto.OpenApiAuctionHistoryResponse; import until.the.eternity.common.enums.ItemCategory; -import java.util.List; - public interface AuctionHistoryFetcherPort { List fetch(ItemCategory category); } diff --git a/src/main/java/until/the/eternity/auctionhistory/domain/service/persister/AuctionHistoryPersisterPort.java b/src/main/java/until/the/eternity/auctionhistory/domain/service/persister/AuctionHistoryPersisterPort.java index 44f59205..636f4c2d 100644 --- a/src/main/java/until/the/eternity/auctionhistory/domain/service/persister/AuctionHistoryPersisterPort.java +++ b/src/main/java/until/the/eternity/auctionhistory/domain/service/persister/AuctionHistoryPersisterPort.java @@ -1,11 +1,10 @@ package until.the.eternity.auctionhistory.domain.service.persister; +import java.util.List; import until.the.eternity.auctionhistory.domain.entity.AuctionHistory; import until.the.eternity.auctionhistory.interfaces.external.dto.OpenApiAuctionHistoryResponse; import until.the.eternity.common.enums.ItemCategory; -import java.util.List; - public interface AuctionHistoryPersisterPort { List filterOutExisting( diff --git a/src/main/java/until/the/eternity/auctionhistory/infrastructure/persistence/AuctionHistoryJpaRepository.java b/src/main/java/until/the/eternity/auctionhistory/infrastructure/persistence/AuctionHistoryJpaRepository.java index bdb982ab..4215b1f2 100644 --- a/src/main/java/until/the/eternity/auctionhistory/infrastructure/persistence/AuctionHistoryJpaRepository.java +++ b/src/main/java/until/the/eternity/auctionhistory/infrastructure/persistence/AuctionHistoryJpaRepository.java @@ -1,5 +1,8 @@ package until.the.eternity.auctionhistory.infrastructure.persistence; +import java.time.Instant; +import java.util.List; +import java.util.Optional; import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; @@ -7,10 +10,6 @@ import org.springframework.stereotype.Repository; import until.the.eternity.auctionhistory.domain.entity.AuctionHistory; -import java.time.Instant; -import java.util.List; -import java.util.Optional; - @Repository public interface AuctionHistoryJpaRepository extends JpaRepository, JpaSpecificationExecutor { diff --git a/src/main/java/until/the/eternity/auctionhistory/infrastructure/persistence/AuctionHistoryQueryDslRepository.java b/src/main/java/until/the/eternity/auctionhistory/infrastructure/persistence/AuctionHistoryQueryDslRepository.java index adc69bbe..cdb13289 100644 --- a/src/main/java/until/the/eternity/auctionhistory/infrastructure/persistence/AuctionHistoryQueryDslRepository.java +++ b/src/main/java/until/the/eternity/auctionhistory/infrastructure/persistence/AuctionHistoryQueryDslRepository.java @@ -8,6 +8,11 @@ import com.querydsl.core.types.dsl.NumberTemplate; import com.querydsl.jpa.JPAExpressions; import com.querydsl.jpa.impl.JPAQueryFactory; +import java.time.Instant; +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; @@ -21,13 +26,7 @@ import until.the.eternity.auctionhistory.interfaces.rest.dto.request.DateAuctionBuyRequest; import until.the.eternity.auctionhistory.interfaces.rest.dto.request.ItemOptionSearchRequest; import until.the.eternity.auctionhistory.interfaces.rest.dto.request.PriceSearchRequest; -import until.the.eternity.auctionitemoption.domain.entity.QAuctionItemOption; - -import java.time.Instant; -import java.time.LocalDate; -import java.time.ZoneId; -import java.util.ArrayList; -import java.util.List; +import until.the.eternity.auctionitemoption.domain.entity.QAuctionHistoryItemOption; @Component @RequiredArgsConstructor @@ -41,15 +40,15 @@ record OptionConditionResult(BooleanBuilder builder, int count) {} /** 경매 거래내역 검색 (옵션 조건 포함) */ public Page search(AuctionHistorySearchRequest condition, Pageable pageable) { QAuctionHistory ah = QAuctionHistory.auctionHistory; - QAuctionItemOption aio = QAuctionItemOption.auctionItemOption; + QAuctionHistoryItemOption aio = QAuctionHistoryItemOption.auctionHistoryItemOption; // 1단계: 거래내역 조건 빌드 BooleanBuilder historyBuilder = buildHistoryPredicate(condition, ah); // 2단계: 옵션 조건이 있으면 서브쿼리 추가 if (condition.itemOptionSearchRequest() != null) { - // 서브쿼리용 별도 QAuctionItemOption 인스턴스 - QAuctionItemOption subOption = new QAuctionItemOption("subOption"); + // 서브쿼리용 별도 QAuctionHistoryItemOption 인스턴스 + QAuctionHistoryItemOption subOption = new QAuctionHistoryItemOption("subOption"); OptionConditionResult optionResult = buildItemOptionConditions(condition.itemOptionSearchRequest(), subOption); @@ -93,7 +92,7 @@ public Page search(AuctionHistorySearchRequest condition, Pageab List content = queryFactory .selectFrom(ah) - .leftJoin(ah.auctionItemOptions, aio) + .leftJoin(ah.auctionHistoryItemOptions, aio) .fetchJoin() .where(ah.auctionBuyId.in(ids)) .orderBy(orderSpecifiers.toArray(new OrderSpecifier[0])) @@ -159,7 +158,7 @@ private BooleanBuilder buildHistoryPredicate( /** 옵션 검색 조건 빌드 (서브쿼리용) */ private OptionConditionResult buildItemOptionConditions( - ItemOptionSearchRequest opt, QAuctionItemOption aio) { + ItemOptionSearchRequest opt, QAuctionHistoryItemOption aio) { BooleanBuilder builder = new BooleanBuilder(); int conditionCount = 0; boolean ergConditionAdded = false; // 에르그 조건 추가 여부 (레벨/랭크 통합) @@ -400,7 +399,10 @@ private OptionConditionResult buildItemOptionConditions( /** 옵션 조건 빌드 헬퍼 (option_type + 숫자 비교 + SearchStandard) */ private BooleanExpression buildOptionCondition( - QAuctionItemOption aio, String optionType, Integer value, SearchStandard standard) { + QAuctionHistoryItemOption aio, + String optionType, + Integer value, + SearchStandard standard) { BooleanExpression optionTypeCondition = aio.optionType.eq(optionType); NumberTemplate numValue = castOptionValueToInt(aio); @@ -422,7 +424,7 @@ private BooleanExpression buildOptionCondition( } /** option_value2 또는 option_value를 Integer로 변환하는 NumberTemplate */ - private NumberTemplate castOptionValueToInt(QAuctionItemOption aio) { + private NumberTemplate castOptionValueToInt(QAuctionHistoryItemOption aio) { return Expressions.numberTemplate( Integer.class, "COALESCE({0}, {1}, 0)", aio.optionValue2, aio.optionValue); } diff --git a/src/main/java/until/the/eternity/auctionhistory/infrastructure/persistence/AuctionHistoryRepositoryPortImpl.java b/src/main/java/until/the/eternity/auctionhistory/infrastructure/persistence/AuctionHistoryRepositoryPortImpl.java index 65428ed2..2b0cdf4d 100644 --- a/src/main/java/until/the/eternity/auctionhistory/infrastructure/persistence/AuctionHistoryRepositoryPortImpl.java +++ b/src/main/java/until/the/eternity/auctionhistory/infrastructure/persistence/AuctionHistoryRepositoryPortImpl.java @@ -1,6 +1,10 @@ package until.the.eternity.auctionhistory.infrastructure.persistence; import jakarta.persistence.EntityManager; +import java.time.Instant; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.domain.Page; @@ -12,11 +16,6 @@ import until.the.eternity.auctionhistory.interfaces.rest.dto.request.AuctionHistorySearchRequest; import until.the.eternity.common.enums.ItemCategory; -import java.time.Instant; -import java.util.HashSet; -import java.util.List; -import java.util.Optional; - /** AuctionHistoryRepository Interface 구현체 */ @Repository @RequiredArgsConstructor diff --git a/src/main/java/until/the/eternity/auctionhistory/interfaces/external/dto/OpenApiAuctionHistoryListResponse.java b/src/main/java/until/the/eternity/auctionhistory/interfaces/external/dto/OpenApiAuctionHistoryListResponse.java index c49a6b2b..e194aeb7 100644 --- a/src/main/java/until/the/eternity/auctionhistory/interfaces/external/dto/OpenApiAuctionHistoryListResponse.java +++ b/src/main/java/until/the/eternity/auctionhistory/interfaces/external/dto/OpenApiAuctionHistoryListResponse.java @@ -1,7 +1,6 @@ package until.the.eternity.auctionhistory.interfaces.external.dto; import com.fasterxml.jackson.annotation.JsonProperty; - import java.util.List; public record OpenApiAuctionHistoryListResponse( diff --git a/src/main/java/until/the/eternity/auctionhistory/interfaces/external/dto/OpenApiAuctionHistoryResponse.java b/src/main/java/until/the/eternity/auctionhistory/interfaces/external/dto/OpenApiAuctionHistoryResponse.java index 65b73ca5..f41eb460 100644 --- a/src/main/java/until/the/eternity/auctionhistory/interfaces/external/dto/OpenApiAuctionHistoryResponse.java +++ b/src/main/java/until/the/eternity/auctionhistory/interfaces/external/dto/OpenApiAuctionHistoryResponse.java @@ -2,10 +2,9 @@ import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonProperty; -import until.the.eternity.auctionitemoption.domain.dto.external.OpenApiAuctionItemOptionResponse; - import java.time.Instant; import java.util.List; +import until.the.eternity.auctionitemoption.domain.dto.external.OpenApiAuctionItemOptionResponse; public record OpenApiAuctionHistoryResponse( @JsonProperty("item_name") String itemName, diff --git a/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/enums/SearchStandard.java b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/enums/SearchStandard.java index feb2c35e..ef005676 100644 --- a/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/enums/SearchStandard.java +++ b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/enums/SearchStandard.java @@ -3,7 +3,6 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonValue; import io.swagger.v3.oas.annotations.media.Schema; - import java.util.Arrays; /** 검색 기준 (이상/이하/같음) */ diff --git a/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/enums/SortDirection.java b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/enums/SortDirection.java index e6aacf98..82666061 100644 --- a/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/enums/SortDirection.java +++ b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/enums/SortDirection.java @@ -3,7 +3,6 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonValue; import io.swagger.v3.oas.annotations.media.Schema; - import java.util.Arrays; /** 정렬 방향 (오름차순/내림차순) */ diff --git a/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/response/AuctionHistoryDetailResponse.java b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/response/AuctionHistoryDetailResponse.java index d694c60c..082c562f 100644 --- a/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/response/AuctionHistoryDetailResponse.java +++ b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/response/AuctionHistoryDetailResponse.java @@ -1,7 +1,6 @@ package until.the.eternity.auctionhistory.interfaces.rest.dto.response; import com.fasterxml.jackson.annotation.JsonFormat; - import java.time.Instant; import java.util.List; diff --git a/src/main/java/until/the/eternity/auctionitem/domain/entity/AuctionItem.java b/src/main/java/until/the/eternity/auctionitem/domain/entity/AuctionItem.java deleted file mode 100644 index 2e9264da..00000000 --- a/src/main/java/until/the/eternity/auctionitem/domain/entity/AuctionItem.java +++ /dev/null @@ -1,40 +0,0 @@ -package until.the.eternity.auctionitem.domain.entity; - -import jakarta.persistence.*; -import lombok.*; -import until.the.eternity.auctionitemoption.domain.entity.AuctionItemOption; - -import java.time.LocalDateTime; -import java.util.List; - -@Entity -@Table(name = "auction_item") -@Getter -@Setter -@NoArgsConstructor -@AllArgsConstructor -@Builder -public class AuctionItem { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(name = "item_name", nullable = false) - private String itemName; - - @Column(name = "item_display_name", nullable = false) - private String itemDisplayName; - - @Column(name = "item_count", nullable = false) - private Long itemCount; - - @Column(name = "auction_price_per_unit", nullable = false) - private Long auctionPricePerUnit; - - @Column(name = "date_auction_expire", nullable = false) - private LocalDateTime dateAuctionExpire; - - @OneToMany(mappedBy = "auctionItem", cascade = CascadeType.ALL, orphanRemoval = true) - private List auctionItemOptions; -} diff --git a/src/main/java/until/the/eternity/auctionitem/domain/entity/AuctionRealtimeItem.java b/src/main/java/until/the/eternity/auctionitem/domain/entity/AuctionRealtimeItem.java new file mode 100644 index 00000000..ff498a80 --- /dev/null +++ b/src/main/java/until/the/eternity/auctionitem/domain/entity/AuctionRealtimeItem.java @@ -0,0 +1,57 @@ +package until.the.eternity.auctionitem.domain.entity; + +import jakarta.persistence.*; +import java.time.Instant; +import java.util.List; +import lombok.*; + +/** 실시간 경매장에서 판매 중인 아이템 정보. V15 마이그레이션에서 auction_item → auction_realtime_item으로 변경됨. */ +@Entity +@Table(name = "auction_realtime_item") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class AuctionRealtimeItem { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "item_name", nullable = false) + private String itemName; + + @Column(name = "item_display_name", nullable = false) + private String itemDisplayName; + + @Column(name = "item_count", nullable = false) + private Long itemCount; + + @Column(name = "auction_price_per_unit", nullable = false) + private Long auctionPricePerUnit; + + @Column(name = "date_auction_expire", nullable = false) + private Instant dateAuctionExpire; + + @Column(name = "date_register", nullable = false) + private Instant dateRegister; + + @Column(name = "item_sub_category", nullable = false) + private String itemSubCategory; + + @Column(name = "item_top_category", nullable = false) + private String itemTopCategory; + + @OneToMany(mappedBy = "auctionRealtimeItem", cascade = CascadeType.ALL, orphanRemoval = true) + private List auctionRealtimeItemOptions; + + public AuctionRealtimeItem linkItemOptions() { + if (this.auctionRealtimeItemOptions != null) { + for (AuctionRealtimeItemOption o : this.auctionRealtimeItemOptions) { + o.setAuctionRealtimeItem(this); + } + } + return this; + } +} diff --git a/src/main/java/until/the/eternity/auctionitem/domain/entity/AuctionRealtimeItemOption.java b/src/main/java/until/the/eternity/auctionitem/domain/entity/AuctionRealtimeItemOption.java new file mode 100644 index 00000000..14943d44 --- /dev/null +++ b/src/main/java/until/the/eternity/auctionitem/domain/entity/AuctionRealtimeItemOption.java @@ -0,0 +1,63 @@ +package until.the.eternity.auctionitem.domain.entity; + +import jakarta.persistence.*; +import java.util.UUID; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +/** 실시간 경매장 아이템(auction_realtime_item)에 연결된 아이템 옵션 정보. */ +@Entity +@Table(name = "auction_realtime_item_option") +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class AuctionRealtimeItemOption { + + @Id + @Column(name = "id") + private String id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "auction_realtime_item_id", nullable = false) + private AuctionRealtimeItem auctionRealtimeItem; + + @Column(name = "option_type") + private String optionType; + + @Column(name = "option_sub_type") + private String optionSubType; + + @Column(name = "option_value") + private String optionValue; + + @Column(name = "option_value2") + private String optionValue2; + + @Column(name = "option_desc", columnDefinition = "TEXT") + private String optionDesc; + + @PrePersist + public void createId() { + this.id = UUID.randomUUID().toString(); + } + + public void setAuctionRealtimeItem(AuctionRealtimeItem auctionRealtimeItem) { + + // 이전 연관관계 정리 + if (this.auctionRealtimeItem != null) { + this.auctionRealtimeItem.getAuctionRealtimeItemOptions().remove(this); + } + + // 새 연관관계 설정 + this.auctionRealtimeItem = auctionRealtimeItem; + + // 반대 쪽 컬렉션 동기화 + if (auctionRealtimeItem != null + && !auctionRealtimeItem.getAuctionRealtimeItemOptions().contains(this)) { + auctionRealtimeItem.getAuctionRealtimeItemOptions().add(this); + } + } +} diff --git a/src/main/java/until/the/eternity/auctionitemoption/domain/entity/AuctionItemOption.java b/src/main/java/until/the/eternity/auctionitemoption/domain/entity/AuctionHistoryItemOption.java similarity index 72% rename from src/main/java/until/the/eternity/auctionitemoption/domain/entity/AuctionItemOption.java rename to src/main/java/until/the/eternity/auctionitemoption/domain/entity/AuctionHistoryItemOption.java index 34a42dea..4e326923 100644 --- a/src/main/java/until/the/eternity/auctionitemoption/domain/entity/AuctionItemOption.java +++ b/src/main/java/until/the/eternity/auctionitemoption/domain/entity/AuctionHistoryItemOption.java @@ -1,22 +1,24 @@ package until.the.eternity.auctionitemoption.domain.entity; import jakarta.persistence.*; +import java.util.UUID; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import until.the.eternity.auctionhistory.domain.entity.AuctionHistory; -import until.the.eternity.auctionitem.domain.entity.AuctionItem; - -import java.util.UUID; +/** + * 경매장 거래 내역(auction_history)에 연결된 아이템 옵션 정보. V15 마이그레이션에서 auction_item_option → + * auction_history_item_option으로 변경됨. + */ @Entity -@Table(name = "auction_item_option") +@Table(name = "auction_history_item_option") @Getter @NoArgsConstructor @AllArgsConstructor @Builder -public class AuctionItemOption { +public class AuctionHistoryItemOption { @Id @Column(name = "id") @@ -29,10 +31,6 @@ public class AuctionItemOption { nullable = false) private AuctionHistory auctionHistory; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "auction_item_id", nullable = true) - private AuctionItem auctionItem; - @Column(name = "option_type") private String optionType; @@ -57,15 +55,16 @@ public void setAuctionHistory(AuctionHistory auctionHistory) { // 이전 연관관계 정리 if (this.auctionHistory != null) { - this.auctionHistory.getAuctionItemOptions().remove(this); + this.auctionHistory.getAuctionHistoryItemOptions().remove(this); } // 새 연관관계 설정 this.auctionHistory = auctionHistory; // 반대 쪽 컬렉션 동기화 - if (auctionHistory != null && !auctionHistory.getAuctionItemOptions().contains(this)) { - auctionHistory.getAuctionItemOptions().add(this); + if (auctionHistory != null + && !auctionHistory.getAuctionHistoryItemOptions().contains(this)) { + auctionHistory.getAuctionHistoryItemOptions().add(this); } } } diff --git a/src/main/java/until/the/eternity/auctionrealtime/application/scheduler/AuctionRealtimeScheduler.java b/src/main/java/until/the/eternity/auctionrealtime/application/scheduler/AuctionRealtimeScheduler.java new file mode 100644 index 00000000..1857f917 --- /dev/null +++ b/src/main/java/until/the/eternity/auctionrealtime/application/scheduler/AuctionRealtimeScheduler.java @@ -0,0 +1,151 @@ +package until.the.eternity.auctionrealtime.application.scheduler; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import until.the.eternity.auctionitem.domain.entity.AuctionRealtimeItem; +import until.the.eternity.auctionrealtime.application.service.AuctionRealtimeService; +import until.the.eternity.auctionrealtime.application.service.fetcher.AuctionRealtimeFetcher; +import until.the.eternity.auctionrealtime.application.service.persister.AuctionRealtimePersister; +import until.the.eternity.auctionrealtime.domain.service.fetcher.AuctionRealtimeFetcherPort.FetchResult; +import until.the.eternity.common.enums.ItemCategory; + +/** + * 실시간 경매장 데이터 수집 스케줄러. + * + *

10분 간격으로 Nexon Open API /auction/list를 호출하여 현재 판매 중인 아이템 정보를 수집한다. + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class AuctionRealtimeScheduler { + + private final AuctionRealtimeService service; + private final AuctionRealtimeFetcher fetcher; + private final AuctionRealtimePersister persister; + + @Value("${openapi.auction-realtime.delay-ms:500}") + private long delayMs; + + /** 10분 간격으로 실행: 0, 10, 20, 30, 40, 50분. */ + @Scheduled(cron = "${openapi.auction-realtime.cron:0 0/10 * * * *}", zone = "Asia/Seoul") + public void fetchAndSaveAuctionRealtimeAll() { + log.info("[REALTIME] Starting Auction Realtime scheduler"); + + // ItemCategory를 topCategory별로 그룹화 + Map> categoriesByTopCategory = + Arrays.stream(ItemCategory.values()) + .collect( + Collectors.groupingBy( + ItemCategory::getTopCategory, + LinkedHashMap::new, + Collectors.toList())); + + int totalSavedCount = 0; + int totalFailedCount = 0; + List topCategories = new ArrayList<>(categoriesByTopCategory.keySet()); + + for (int topIndex = 0; topIndex < topCategories.size(); topIndex++) { + String topCategory = topCategories.get(topIndex); + List subCategories = categoriesByTopCategory.get(topCategory); + List newEntities = new ArrayList<>(); + + log.debug("[REALTIME] Processing top category [{}]", topCategory); + + for (int subIndex = 0; subIndex < subCategories.size(); subIndex++) { + ItemCategory category = subCategories.get(subIndex); + try { + log.debug("[REALTIME] Processing category [{}]", category.getSubCategory()); + + // API 호출 및 데이터 수집 + FetchResult fetchResult = fetcher.fetch(category); + + if (fetchResult.items().isEmpty()) { + log.debug("[REALTIME] [{}] No data fetched", category.getSubCategory()); + continue; + } + + // 엔티티 변환 및 필터링 + List entities = + persister.prepareEntities( + fetchResult.items(), category, fetchResult.latestDate()); + + if (entities.isEmpty()) { + continue; + } + + // 동일 날짜 데이터가 있으면 삭제 후 저장 + if (fetchResult.hasEqualDate() && fetchResult.latestDate() != null) { + service.deleteAndSave(category, fetchResult.latestDate(), entities); + } else { + newEntities.addAll(entities); + } + + // 마지막 서브 카테고리가 아닌 경우에만 delay 적용 + if (subIndex < subCategories.size() - 1) { + log.debug( + "[REALTIME] Waiting {}ms before processing next category", delayMs); + Thread.sleep(delayMs); + } + } catch (InterruptedException e) { + log.error( + "[REALTIME] Thread interrupted during delay for category [{}]", + category.getSubCategory(), + e); + Thread.currentThread().interrupt(); + break; + } catch (Exception e) { + log.error( + "[REALTIME] Error during processing category [{}]", + category.getSubCategory(), + e); + totalFailedCount++; + } + } + + // Top Category별로 저장 (동일 날짜가 아닌 신규 데이터) + if (!newEntities.isEmpty()) { + service.saveAll(newEntities); + totalSavedCount += newEntities.size(); + log.info( + "[REALTIME] Saved [{}] new auction realtime items for top category [{}]", + newEntities.size(), + topCategory); + } + + // 마지막 탑 카테고리가 아닌 경우에만 delay 적용 + if (topIndex < topCategories.size() - 1) { + try { + log.debug( + "[REALTIME] Waiting {}ms before processing next top category", delayMs); + Thread.sleep(delayMs); + } catch (InterruptedException e) { + log.error( + "[REALTIME] Thread interrupted during delay for top category [{}]", + topCategory, + e); + Thread.currentThread().interrupt(); + break; + } + } + } + + // 만료된 아이템 삭제 + int deletedExpired = service.deleteExpiredItems(Instant.now()); + + log.info( + "[REALTIME] Auction Realtime scheduler completed. Total saved: {}, Failed categories: {}, Expired deleted: {}", + totalSavedCount, + totalFailedCount, + deletedExpired); + } +} diff --git a/src/main/java/until/the/eternity/auctionrealtime/application/service/AuctionRealtimeService.java b/src/main/java/until/the/eternity/auctionrealtime/application/service/AuctionRealtimeService.java new file mode 100644 index 00000000..466fe6a4 --- /dev/null +++ b/src/main/java/until/the/eternity/auctionrealtime/application/service/AuctionRealtimeService.java @@ -0,0 +1,75 @@ +package until.the.eternity.auctionrealtime.application.service; + +import java.time.Instant; +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import until.the.eternity.auctionitem.domain.entity.AuctionRealtimeItem; +import until.the.eternity.auctionrealtime.domain.repository.AuctionRealtimeItemRepositoryPort; +import until.the.eternity.common.enums.ItemCategory; + +/** 실시간 경매장 데이터 Service. */ +@Slf4j +@Service +@RequiredArgsConstructor +public class AuctionRealtimeService { + + private final AuctionRealtimeItemRepositoryPort repository; + + /** + * 해당 카테고리 & 동일 date_auction_expire 레코드 삭제 후 새 엔티티들을 저장한다. + * + * @param category 아이템 카테고리 + * @param dateAuctionExpire 삭제할 date_auction_expire + * @param entities 저장할 엔티티 리스트 + */ + @Transactional + public void deleteAndSave( + ItemCategory category, Instant dateAuctionExpire, List entities) { + + // 동일 date_auction_expire 레코드 삭제 + int deleted = + repository.deleteBySubCategoryAndDateAuctionExpire(category, dateAuctionExpire); + log.info( + "[REALTIME] [{}] Deleted {} records with date_auction_expire={}", + category.getSubCategory(), + deleted, + dateAuctionExpire); + + // 새 엔티티 저장 + repository.saveAll(entities); + log.info( + "[REALTIME] [{}] Saved {} new auction realtime items", + category.getSubCategory(), + entities.size()); + } + + /** + * 엔티티들을 저장한다. (삭제 없이) + * + * @param entities 저장할 엔티티 리스트 + */ + @Transactional + public void saveAll(List entities) { + if (entities == null || entities.isEmpty()) { + return; + } + repository.saveAll(entities); + log.debug("[REALTIME] Saved {} auction realtime items", entities.size()); + } + + /** + * 만료된 아이템을 삭제한다. + * + * @param now 현재 시각 + * @return 삭제된 레코드 수 + */ + @Transactional + public int deleteExpiredItems(Instant now) { + int deleted = repository.deleteExpiredItems(now); + log.info("[REALTIME] Deleted {} expired auction realtime items", deleted); + return deleted; + } +} diff --git a/src/main/java/until/the/eternity/auctionrealtime/application/service/fetcher/AuctionRealtimeFetcher.java b/src/main/java/until/the/eternity/auctionrealtime/application/service/fetcher/AuctionRealtimeFetcher.java new file mode 100644 index 00000000..1bd26535 --- /dev/null +++ b/src/main/java/until/the/eternity/auctionrealtime/application/service/fetcher/AuctionRealtimeFetcher.java @@ -0,0 +1,100 @@ +package until.the.eternity.auctionrealtime.application.service.fetcher; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import until.the.eternity.auctionrealtime.domain.service.AuctionRealtimeDuplicateChecker; +import until.the.eternity.auctionrealtime.domain.service.AuctionRealtimeDuplicateChecker.DuplicateCheckResult; +import until.the.eternity.auctionrealtime.domain.service.fetcher.AuctionRealtimeFetcherPort; +import until.the.eternity.auctionrealtime.infrastructure.client.AuctionRealtimeClient; +import until.the.eternity.auctionrealtime.interfaces.external.dto.OpenApiAuctionRealtimeListResponse; +import until.the.eternity.auctionrealtime.interfaces.external.dto.OpenApiAuctionRealtimeResponse; +import until.the.eternity.common.enums.ItemCategory; + +/** 실시간 경매장 데이터 Fetcher 구현체. Cursor 기반 페이징으로 API를 호출하고, 중복 감지 시 호출을 중단한다. */ +@Slf4j +@Component +@RequiredArgsConstructor +public class AuctionRealtimeFetcher implements AuctionRealtimeFetcherPort { + + private final AuctionRealtimeClient client; + private final AuctionRealtimeDuplicateChecker duplicateChecker; + + @Override + public FetchResult fetch(ItemCategory category) { + List result = new ArrayList<>(); + String cursor = ""; + boolean hasEqualDate = false; + Instant latestDate = null; + + while (true) { + OpenApiAuctionRealtimeListResponse response = + client.fetchAuctionList(category, cursor).block(); + + if (response == null || response.auctionItems() == null) { + log.warn( + "[REALTIME] [{}] response or its items is null, something is wrong with open api call", + category.getSubCategory()); + break; + } + + log.debug( + "[REALTIME] [{}] fetched '{}' data", + category.getSubCategory(), + response.auctionItems().size()); + + if (response.auctionItems().isEmpty()) { + log.debug("[REALTIME] [{}] fetched no data", category.getSubCategory()); + break; + } + + List batch = response.auctionItems(); + + // 중복 체크 + DuplicateCheckResult checkResult = + duplicateChecker.checkDuplicateInBatch(batch, category); + + if (checkResult.isDuplicate()) { + int index = checkResult.duplicateIndex(); + latestDate = checkResult.latestDate(); + hasEqualDate = checkResult.hasEqualDate(); + + if (index > 0) { + result.addAll(batch.subList(0, index)); + } + + if (hasEqualDate) { + log.debug( + "[REALTIME] [{}] equal date found at index {}, need to delete and re-save, added {} items", + category.getSubCategory(), + index, + index); + } else { + log.debug( + "[REALTIME] [{}] duplicate found at index {}, added {} items before duplicate", + category.getSubCategory(), + index, + index); + } + break; + } + + latestDate = checkResult.latestDate(); + result.addAll(batch); + + cursor = response.nextCursor(); + + if (cursor == null || cursor.isEmpty()) { + log.debug( + "[REALTIME] [{}] response cursor is null, fetched end", + category.getSubCategory()); + break; + } + } + + return new FetchResult(result, hasEqualDate, latestDate); + } +} diff --git a/src/main/java/until/the/eternity/auctionrealtime/application/service/persister/AuctionRealtimePersister.java b/src/main/java/until/the/eternity/auctionrealtime/application/service/persister/AuctionRealtimePersister.java new file mode 100644 index 00000000..8132c675 --- /dev/null +++ b/src/main/java/until/the/eternity/auctionrealtime/application/service/persister/AuctionRealtimePersister.java @@ -0,0 +1,53 @@ +package until.the.eternity.auctionrealtime.application.service.persister; + +import java.time.Instant; +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import until.the.eternity.auctionitem.domain.entity.AuctionRealtimeItem; +import until.the.eternity.auctionrealtime.domain.mapper.OpenApiAuctionRealtimeMapper; +import until.the.eternity.auctionrealtime.domain.service.AuctionRealtimeDuplicateChecker; +import until.the.eternity.auctionrealtime.domain.service.persister.AuctionRealtimePersisterPort; +import until.the.eternity.auctionrealtime.interfaces.external.dto.OpenApiAuctionRealtimeResponse; +import until.the.eternity.common.enums.ItemCategory; + +/** 실시간 경매장 데이터 Persister 구현체. */ +@Slf4j +@RequiredArgsConstructor +@Component +public class AuctionRealtimePersister implements AuctionRealtimePersisterPort { + + private final OpenApiAuctionRealtimeMapper mapper; + private final AuctionRealtimeDuplicateChecker duplicateChecker; + + @Override + public List prepareEntities( + List dtoList, + ItemCategory category, + Instant latestDate) { + + // 저장 가능한 데이터만 필터링 + List filtered = + duplicateChecker.filterForSave(dtoList, latestDate); + + // Entity 변환 + List entities = mapper.toEntityList(filtered, category); + + // 아이템 옵션 링크 설정 + entities.forEach(AuctionRealtimeItem::linkItemOptions); + + if (entities.isEmpty()) { + log.info( + "[REALTIME] [{}] No new auction realtime items to save", + category.getSubCategory()); + } else { + log.info( + "[REALTIME] [{}] Prepared {} auction realtime items for save", + category.getSubCategory(), + entities.size()); + } + + return entities; + } +} diff --git a/src/main/java/until/the/eternity/auctionrealtime/domain/mapper/OpenApiAuctionRealtimeMapper.java b/src/main/java/until/the/eternity/auctionrealtime/domain/mapper/OpenApiAuctionRealtimeMapper.java new file mode 100644 index 00000000..24c1ea07 --- /dev/null +++ b/src/main/java/until/the/eternity/auctionrealtime/domain/mapper/OpenApiAuctionRealtimeMapper.java @@ -0,0 +1,41 @@ +package until.the.eternity.auctionrealtime.domain.mapper; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.List; +import org.mapstruct.Context; +import org.mapstruct.IterableMapping; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Named; +import until.the.eternity.auctionitem.domain.entity.AuctionRealtimeItem; +import until.the.eternity.auctionrealtime.interfaces.external.dto.OpenApiAuctionRealtimeResponse; +import until.the.eternity.common.enums.ItemCategory; + +/** OpenApiAuctionRealtimeResponse → AuctionRealtimeItem Entity 변환 Mapper. */ +@Mapper(componentModel = "spring", uses = OpenApiRealtimeItemOptionMapper.class) +public interface OpenApiAuctionRealtimeMapper { + + @Named("toEntity(OpenApiAuctionRealtimeResponse, ItemCategory)") + @Mapping( + source = "dateAuctionExpire", + target = "dateAuctionExpire", + qualifiedByName = "utcToKst") + @Mapping(source = "itemOptions", target = "auctionRealtimeItemOptions") + @Mapping(target = "itemSubCategory", expression = "java(itemCategory.getSubCategory())") + @Mapping(target = "itemTopCategory", expression = "java(itemCategory.getTopCategory())") + @Mapping(target = "id", ignore = true) + @Mapping(target = "dateRegister", expression = "java(java.time.Instant.now())") + AuctionRealtimeItem toEntity( + OpenApiAuctionRealtimeResponse dto, @Context ItemCategory itemCategory); + + @IterableMapping(qualifiedByName = "toEntity(OpenApiAuctionRealtimeResponse, ItemCategory)") + List toEntityList( + List dtoList, @Context ItemCategory itemCategory); + + @Named("utcToKst") + default Instant utcToKst(Instant utcTime) { + // API에서 받은 UTC 시간에 9시간을 더하여 KST로 변환 + return utcTime != null ? utcTime.plus(9, ChronoUnit.HOURS) : null; + } +} diff --git a/src/main/java/until/the/eternity/auctionrealtime/domain/mapper/OpenApiRealtimeItemOptionMapper.java b/src/main/java/until/the/eternity/auctionrealtime/domain/mapper/OpenApiRealtimeItemOptionMapper.java new file mode 100644 index 00000000..e33cac6c --- /dev/null +++ b/src/main/java/until/the/eternity/auctionrealtime/domain/mapper/OpenApiRealtimeItemOptionMapper.java @@ -0,0 +1,15 @@ +package until.the.eternity.auctionrealtime.domain.mapper; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import until.the.eternity.auctionitem.domain.entity.AuctionRealtimeItemOption; +import until.the.eternity.auctionitemoption.domain.dto.external.OpenApiAuctionItemOptionResponse; + +/** OpenApiAuctionItemOptionResponse → AuctionRealtimeItemOption Entity 변환 Mapper. */ +@Mapper(componentModel = "spring") +public interface OpenApiRealtimeItemOptionMapper { + + @Mapping(target = "id", ignore = true) // PK 자동 생성 + @Mapping(target = "auctionRealtimeItem", ignore = true) + AuctionRealtimeItemOption toEntity(OpenApiAuctionItemOptionResponse itemOption); +} diff --git a/src/main/java/until/the/eternity/auctionrealtime/domain/repository/AuctionRealtimeItemRepositoryPort.java b/src/main/java/until/the/eternity/auctionrealtime/domain/repository/AuctionRealtimeItemRepositoryPort.java new file mode 100644 index 00000000..72b9b129 --- /dev/null +++ b/src/main/java/until/the/eternity/auctionrealtime/domain/repository/AuctionRealtimeItemRepositoryPort.java @@ -0,0 +1,43 @@ +package until.the.eternity.auctionrealtime.domain.repository; + +import java.time.Instant; +import java.util.List; +import java.util.Optional; +import until.the.eternity.auctionitem.domain.entity.AuctionRealtimeItem; +import until.the.eternity.common.enums.ItemCategory; + +/** AuctionRealtimeItem Repository Port (Hexagonal Architecture). */ +public interface AuctionRealtimeItemRepositoryPort { + + /** + * 해당 subcategory의 최신 date_auction_expire를 조회한다. + * + * @param category 아이템 카테고리 + * @return 최신 date_auction_expire (없으면 Optional.empty()) + */ + Optional findLatestDateAuctionExpireBySubCategory(ItemCategory category); + + /** + * 해당 subcategory & date_auction_expire에 해당하는 모든 레코드를 삭제한다. + * + * @param category 아이템 카테고리 + * @param dateAuctionExpire 만료 시각 + * @return 삭제된 레코드 수 + */ + int deleteBySubCategoryAndDateAuctionExpire(ItemCategory category, Instant dateAuctionExpire); + + /** + * date_auction_expire가 현재 시각보다 이전인 모든 레코드를 삭제한다. + * + * @param now 현재 시각 + * @return 삭제된 레코드 수 + */ + int deleteExpiredItems(Instant now); + + /** + * 엔티티들을 일괄 저장한다. + * + * @param entities 저장할 엔티티 리스트 + */ + void saveAll(List entities); +} diff --git a/src/main/java/until/the/eternity/auctionrealtime/domain/service/AuctionRealtimeDuplicateChecker.java b/src/main/java/until/the/eternity/auctionrealtime/domain/service/AuctionRealtimeDuplicateChecker.java new file mode 100644 index 00000000..54c24f6d --- /dev/null +++ b/src/main/java/until/the/eternity/auctionrealtime/domain/service/AuctionRealtimeDuplicateChecker.java @@ -0,0 +1,131 @@ +package until.the.eternity.auctionrealtime.domain.service; + +import java.time.Instant; +import java.util.List; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import until.the.eternity.auctionrealtime.domain.repository.AuctionRealtimeItemRepositoryPort; +import until.the.eternity.auctionrealtime.interfaces.external.dto.OpenApiAuctionRealtimeResponse; +import until.the.eternity.common.enums.ItemCategory; + +/** + * 실시간 경매장 데이터의 중복 체크 로직. + * + *

중복 판단 기준: + * + *

    + *
  • date_auction_expire < latestDate → 중복 (과거 데이터) + *
  • date_auction_expire == latestDate → 해당 시간대 데이터 삭제 후 재저장 필요 + *
  • date_auction_expire > latestDate → 신규 데이터 + *
+ */ +@Slf4j +@Component +@RequiredArgsConstructor +public class AuctionRealtimeDuplicateChecker { + + private final AuctionRealtimeItemRepositoryPort repository; + + /** + * 중복 체크 결과. + * + * @param isDuplicate 중복 여부 + * @param hasEqualDate 동일 날짜 데이터 존재 여부 (삭제 후 재저장 필요) + * @param duplicateIndex 중복이 발생한 첫 번째 인덱스 (-1이면 중복 없음) + * @param latestDate DB의 최신 date_auction_expire + */ + public record DuplicateCheckResult( + boolean isDuplicate, boolean hasEqualDate, int duplicateIndex, Instant latestDate) { + + public static DuplicateCheckResult noDuplicate() { + return new DuplicateCheckResult(false, false, -1, null); + } + + public static DuplicateCheckResult noDuplicateWithLatestDate(Instant latestDate) { + return new DuplicateCheckResult(false, false, -1, latestDate); + } + + public static DuplicateCheckResult duplicateFound(int index, Instant latestDate) { + return new DuplicateCheckResult(true, false, index, latestDate); + } + + public static DuplicateCheckResult equalDateFound(int index, Instant latestDate) { + return new DuplicateCheckResult(true, true, index, latestDate); + } + } + + /** + * 배치에서 중복 또는 동일 날짜 데이터를 체크한다. + * + * @param batch 검사할 배치 데이터 + * @param category 아이템 카테고리 + * @return 중복 체크 결과 + */ + public DuplicateCheckResult checkDuplicateInBatch( + List batch, ItemCategory category) { + if (batch == null || batch.isEmpty()) { + return DuplicateCheckResult.noDuplicate(); + } + + Optional latestDateOpt = + repository.findLatestDateAuctionExpireBySubCategory(category); + + if (latestDateOpt.isEmpty()) { + // DB에 데이터가 없으면 모두 신규 + return DuplicateCheckResult.noDuplicate(); + } + + Instant latestDate = latestDateOpt.get(); + + for (int i = 0; i < batch.size(); i++) { + OpenApiAuctionRealtimeResponse dto = batch.get(i); + Instant dtoDate = dto.dateAuctionExpire(); + + if (dtoDate == null) { + continue; + } + + if (dtoDate.isBefore(latestDate)) { + // 과거 데이터 → 중복 + return DuplicateCheckResult.duplicateFound(i, latestDate); + } + + if (dtoDate.equals(latestDate)) { + // 동일 날짜 → 삭제 후 재저장 필요 + return DuplicateCheckResult.equalDateFound(i, latestDate); + } + } + + // 모든 데이터가 latestDate보다 이후 + return DuplicateCheckResult.noDuplicateWithLatestDate(latestDate); + } + + /** + * API 응답 데이터를 필터링하여 저장 가능한 데이터만 반환한다. + * + * @param dtos 필터링할 DTO 리스트 + * @param latestDate DB의 최신 date_auction_expire (null이면 모두 저장) + * @return 저장할 데이터 리스트 + */ + public List filterForSave( + List dtos, Instant latestDate) { + if (dtos == null || dtos.isEmpty()) { + return List.of(); + } + + if (latestDate == null) { + return dtos; + } + + return dtos.stream() + .filter( + dto -> { + Instant dtoDate = dto.dateAuctionExpire(); + // latestDate 이후 또는 동일한 날짜만 저장 + return dtoDate != null && !dtoDate.isBefore(latestDate); + }) + .toList(); + } +} diff --git a/src/main/java/until/the/eternity/auctionrealtime/domain/service/fetcher/AuctionRealtimeFetcherPort.java b/src/main/java/until/the/eternity/auctionrealtime/domain/service/fetcher/AuctionRealtimeFetcherPort.java new file mode 100644 index 00000000..8f31c645 --- /dev/null +++ b/src/main/java/until/the/eternity/auctionrealtime/domain/service/fetcher/AuctionRealtimeFetcherPort.java @@ -0,0 +1,33 @@ +package until.the.eternity.auctionrealtime.domain.service.fetcher; + +import java.time.Instant; +import java.util.List; +import until.the.eternity.auctionrealtime.interfaces.external.dto.OpenApiAuctionRealtimeResponse; +import until.the.eternity.common.enums.ItemCategory; + +/** 실시간 경매장 데이터 Fetcher Port. */ +public interface AuctionRealtimeFetcherPort { + + /** + * 페칭 결과. + * + * @param items 수집된 아이템 리스트 + * @param hasEqualDate 동일 날짜 데이터 존재 여부 (삭제 후 재저장 필요) + * @param latestDate DB의 최신 date_auction_expire + */ + record FetchResult( + List items, boolean hasEqualDate, Instant latestDate) { + + public static FetchResult empty() { + return new FetchResult(List.of(), false, null); + } + } + + /** + * 해당 카테고리의 실시간 경매장 데이터를 수집한다. + * + * @param category 아이템 카테고리 + * @return 페칭 결과 + */ + FetchResult fetch(ItemCategory category); +} diff --git a/src/main/java/until/the/eternity/auctionrealtime/domain/service/persister/AuctionRealtimePersisterPort.java b/src/main/java/until/the/eternity/auctionrealtime/domain/service/persister/AuctionRealtimePersisterPort.java new file mode 100644 index 00000000..265327f7 --- /dev/null +++ b/src/main/java/until/the/eternity/auctionrealtime/domain/service/persister/AuctionRealtimePersisterPort.java @@ -0,0 +1,24 @@ +package until.the.eternity.auctionrealtime.domain.service.persister; + +import java.time.Instant; +import java.util.List; +import until.the.eternity.auctionitem.domain.entity.AuctionRealtimeItem; +import until.the.eternity.auctionrealtime.interfaces.external.dto.OpenApiAuctionRealtimeResponse; +import until.the.eternity.common.enums.ItemCategory; + +/** 실시간 경매장 데이터 Persister Port. */ +public interface AuctionRealtimePersisterPort { + + /** + * API 응답 데이터를 Entity로 변환하고 필터링한다. + * + * @param dtoList API 응답 DTO 리스트 + * @param category 아이템 카테고리 + * @param latestDate DB의 최신 date_auction_expire (null이면 모두 저장) + * @return 저장할 Entity 리스트 + */ + List prepareEntities( + List dtoList, + ItemCategory category, + Instant latestDate); +} diff --git a/src/main/java/until/the/eternity/auctionrealtime/infrastructure/client/AuctionRealtimeClient.java b/src/main/java/until/the/eternity/auctionrealtime/infrastructure/client/AuctionRealtimeClient.java new file mode 100644 index 00000000..5e92495a --- /dev/null +++ b/src/main/java/until/the/eternity/auctionrealtime/infrastructure/client/AuctionRealtimeClient.java @@ -0,0 +1,59 @@ +package until.the.eternity.auctionrealtime.infrastructure.client; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; +import until.the.eternity.auctionrealtime.interfaces.external.dto.OpenApiAuctionRealtimeListResponse; +import until.the.eternity.common.enums.ItemCategory; + +/** Nexon Open API /auction/list 엔드포인트 호출 클라이언트. */ +@Slf4j +@Component +@RequiredArgsConstructor +public class AuctionRealtimeClient { + + private final WebClient openApiWebClient; + + /** + * 카테고리·커서 기반 경매장 현재 판매 목록 조회. + * + * @param category 조회할 카테고리 + * @param cursor 다음 페이지 커서(null 가능) + * @return 응답 DTO를 담은 Mono, 호출 실패 시 Mono.empty() + */ + public Mono fetchAuctionList( + ItemCategory category, String cursor) { + + log.info( + "[REALTIME] [{}] Calling Nexon Open API Auction List API with cursor='{}'", + category.getSubCategory(), + cursor == null ? "" : cursor); + + return openApiWebClient + .get() + .uri( + uriBuilder -> { + uriBuilder + .path("/auction/list") + .queryParam("auction_item_category", category.getSubCategory()); + if (cursor != null && !cursor.isEmpty()) { + uriBuilder.queryParam("cursor", cursor); + } + return uriBuilder.build(); + }) + .retrieve() + .bodyToMono(OpenApiAuctionRealtimeListResponse.class) + .onErrorResume( + throwable -> { + log.warn( + "[REALTIME] [{}] Failed to fetch Nexon Open API Auction List API with cursor='{}': error='{}', message='{}'", + category.getSubCategory(), + cursor, + throwable.toString(), + throwable.getMessage()); + return Mono.empty(); + }); + } +} diff --git a/src/main/java/until/the/eternity/auctionrealtime/infrastructure/persistence/AuctionRealtimeItemRepository.java b/src/main/java/until/the/eternity/auctionrealtime/infrastructure/persistence/AuctionRealtimeItemRepository.java new file mode 100644 index 00000000..399678c1 --- /dev/null +++ b/src/main/java/until/the/eternity/auctionrealtime/infrastructure/persistence/AuctionRealtimeItemRepository.java @@ -0,0 +1,34 @@ +package until.the.eternity.auctionrealtime.infrastructure.persistence; + +import java.time.Instant; +import java.util.Optional; +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 until.the.eternity.auctionitem.domain.entity.AuctionRealtimeItem; + +/** AuctionRealtimeItem JPA Repository. */ +public interface AuctionRealtimeItemRepository extends JpaRepository { + + @Query( + value = + "SELECT date_auction_expire FROM auction_realtime_item " + + "WHERE item_sub_category = :subCategory " + + "ORDER BY date_auction_expire DESC LIMIT 1", + nativeQuery = true) + Optional findLatestDateAuctionExpireBySubCategory( + @Param("subCategory") String subCategory); + + @Modifying + @Query( + "DELETE FROM AuctionRealtimeItem a " + + "WHERE a.itemSubCategory = :subCategory AND a.dateAuctionExpire = :dateAuctionExpire") + int deleteBySubCategoryAndDateAuctionExpire( + @Param("subCategory") String subCategory, + @Param("dateAuctionExpire") Instant dateAuctionExpire); + + @Modifying + @Query("DELETE FROM AuctionRealtimeItem a WHERE a.dateAuctionExpire < :now") + int deleteExpiredItems(@Param("now") Instant now); +} diff --git a/src/main/java/until/the/eternity/auctionrealtime/infrastructure/persistence/AuctionRealtimeItemRepositoryPortImpl.java b/src/main/java/until/the/eternity/auctionrealtime/infrastructure/persistence/AuctionRealtimeItemRepositoryPortImpl.java new file mode 100644 index 00000000..e1665c7f --- /dev/null +++ b/src/main/java/until/the/eternity/auctionrealtime/infrastructure/persistence/AuctionRealtimeItemRepositoryPortImpl.java @@ -0,0 +1,63 @@ +package until.the.eternity.auctionrealtime.infrastructure.persistence; + +import jakarta.persistence.EntityManager; +import java.time.Instant; +import java.util.List; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Repository; +import until.the.eternity.auctionitem.domain.entity.AuctionRealtimeItem; +import until.the.eternity.auctionrealtime.domain.repository.AuctionRealtimeItemRepositoryPort; +import until.the.eternity.common.enums.ItemCategory; + +/** AuctionRealtimeItemRepositoryPort 구현체. */ +@Slf4j +@Repository +@RequiredArgsConstructor +public class AuctionRealtimeItemRepositoryPortImpl implements AuctionRealtimeItemRepositoryPort { + + private static final int BATCH_SIZE = 500; + + private final AuctionRealtimeItemRepository jpaRepository; + private final EntityManager entityManager; + + @Override + public Optional findLatestDateAuctionExpireBySubCategory(ItemCategory category) { + return jpaRepository.findLatestDateAuctionExpireBySubCategory(category.getSubCategory()); + } + + @Override + public int deleteBySubCategoryAndDateAuctionExpire( + ItemCategory category, Instant dateAuctionExpire) { + return jpaRepository.deleteBySubCategoryAndDateAuctionExpire( + category.getSubCategory(), dateAuctionExpire); + } + + @Override + public int deleteExpiredItems(Instant now) { + return jpaRepository.deleteExpiredItems(now); + } + + @Override + public void saveAll(List entities) { + if (entities == null || entities.isEmpty()) { + return; + } + + for (int i = 0; i < entities.size(); i++) { + entityManager.persist(entities.get(i)); + + if ((i + 1) % BATCH_SIZE == 0) { + entityManager.flush(); + entityManager.clear(); + } + } + + // 마지막 배치 처리 + entityManager.flush(); + entityManager.clear(); + + log.debug("[REALTIME] Saved {} auction realtime items", entities.size()); + } +} diff --git a/src/main/java/until/the/eternity/auctionrealtime/interfaces/external/dto/OpenApiAuctionRealtimeListResponse.java b/src/main/java/until/the/eternity/auctionrealtime/interfaces/external/dto/OpenApiAuctionRealtimeListResponse.java new file mode 100644 index 00000000..980fc9e3 --- /dev/null +++ b/src/main/java/until/the/eternity/auctionrealtime/interfaces/external/dto/OpenApiAuctionRealtimeListResponse.java @@ -0,0 +1,9 @@ +package until.the.eternity.auctionrealtime.interfaces.external.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; + +/** Nexon Open API /auction/list 응답 리스트 DTO. */ +public record OpenApiAuctionRealtimeListResponse( + @JsonProperty("auction_item") List auctionItems, + @JsonProperty("next_cursor") String nextCursor) {} diff --git a/src/main/java/until/the/eternity/auctionrealtime/interfaces/external/dto/OpenApiAuctionRealtimeResponse.java b/src/main/java/until/the/eternity/auctionrealtime/interfaces/external/dto/OpenApiAuctionRealtimeResponse.java new file mode 100644 index 00000000..28d59235 --- /dev/null +++ b/src/main/java/until/the/eternity/auctionrealtime/interfaces/external/dto/OpenApiAuctionRealtimeResponse.java @@ -0,0 +1,18 @@ +package until.the.eternity.auctionrealtime.interfaces.external.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.time.Instant; +import java.util.List; +import until.the.eternity.auctionitemoption.domain.dto.external.OpenApiAuctionItemOptionResponse; + +/** Nexon Open API /auction/list 응답 DTO. 현재 경매장에서 판매 중인 아이템 정보. */ +public record OpenApiAuctionRealtimeResponse( + @JsonProperty("item_name") String itemName, + @JsonProperty("item_display_name") String itemDisplayName, + @JsonProperty("item_count") long itemCount, + @JsonProperty("auction_price_per_unit") long auctionPricePerUnit, + @JsonProperty("date_auction_expire") + @JsonFormat(shape = JsonFormat.Shape.STRING, timezone = "Asia/Seoul") + Instant dateAuctionExpire, + @JsonProperty("item_option") List itemOptions) {} diff --git a/src/main/java/until/the/eternity/auctionsearchoption/application/service/AuctionSearchOptionService.java b/src/main/java/until/the/eternity/auctionsearchoption/application/service/AuctionSearchOptionService.java index 28e70a27..aec4e464 100644 --- a/src/main/java/until/the/eternity/auctionsearchoption/application/service/AuctionSearchOptionService.java +++ b/src/main/java/until/the/eternity/auctionsearchoption/application/service/AuctionSearchOptionService.java @@ -2,6 +2,8 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.List; +import java.util.Map; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -11,9 +13,6 @@ import until.the.eternity.auctionsearchoption.interfaces.rest.dto.response.FieldMetadata; import until.the.eternity.auctionsearchoption.interfaces.rest.dto.response.SearchOptionMetadataResponse; -import java.util.List; -import java.util.Map; - @Slf4j @Service @RequiredArgsConstructor diff --git a/src/main/java/until/the/eternity/auctionsearchoption/domain/entity/AuctionSearchOptionMetadata.java b/src/main/java/until/the/eternity/auctionsearchoption/domain/entity/AuctionSearchOptionMetadata.java index 5356db73..21a9e82b 100644 --- a/src/main/java/until/the/eternity/auctionsearchoption/domain/entity/AuctionSearchOptionMetadata.java +++ b/src/main/java/until/the/eternity/auctionsearchoption/domain/entity/AuctionSearchOptionMetadata.java @@ -1,14 +1,13 @@ package until.the.eternity.auctionsearchoption.domain.entity; import jakarta.persistence.*; +import java.time.LocalDateTime; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; import org.hibernate.annotations.JdbcTypeCode; import org.hibernate.type.SqlTypes; -import java.time.LocalDateTime; - @Entity @Table(name = "auction_search_option_metadata") @Getter diff --git a/src/main/java/until/the/eternity/auctionsearchoption/domain/repository/AuctionSearchOptionRepositoryPort.java b/src/main/java/until/the/eternity/auctionsearchoption/domain/repository/AuctionSearchOptionRepositoryPort.java index fe4e89b0..971be29e 100644 --- a/src/main/java/until/the/eternity/auctionsearchoption/domain/repository/AuctionSearchOptionRepositoryPort.java +++ b/src/main/java/until/the/eternity/auctionsearchoption/domain/repository/AuctionSearchOptionRepositoryPort.java @@ -1,8 +1,7 @@ package until.the.eternity.auctionsearchoption.domain.repository; -import until.the.eternity.auctionsearchoption.domain.entity.AuctionSearchOptionMetadata; - import java.util.List; +import until.the.eternity.auctionsearchoption.domain.entity.AuctionSearchOptionMetadata; public interface AuctionSearchOptionRepositoryPort { diff --git a/src/main/java/until/the/eternity/auctionsearchoption/infrastructure/persistence/AuctionSearchOptionJpaRepository.java b/src/main/java/until/the/eternity/auctionsearchoption/infrastructure/persistence/AuctionSearchOptionJpaRepository.java index c18fd9e5..e7074578 100644 --- a/src/main/java/until/the/eternity/auctionsearchoption/infrastructure/persistence/AuctionSearchOptionJpaRepository.java +++ b/src/main/java/until/the/eternity/auctionsearchoption/infrastructure/persistence/AuctionSearchOptionJpaRepository.java @@ -1,11 +1,10 @@ package until.the.eternity.auctionsearchoption.infrastructure.persistence; +import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import until.the.eternity.auctionsearchoption.domain.entity.AuctionSearchOptionMetadata; -import java.util.List; - @Repository interface AuctionSearchOptionJpaRepository extends JpaRepository { diff --git a/src/main/java/until/the/eternity/auctionsearchoption/infrastructure/persistence/AuctionSearchOptionRepositoryPortImpl.java b/src/main/java/until/the/eternity/auctionsearchoption/infrastructure/persistence/AuctionSearchOptionRepositoryPortImpl.java index 2440e821..850f43f4 100644 --- a/src/main/java/until/the/eternity/auctionsearchoption/infrastructure/persistence/AuctionSearchOptionRepositoryPortImpl.java +++ b/src/main/java/until/the/eternity/auctionsearchoption/infrastructure/persistence/AuctionSearchOptionRepositoryPortImpl.java @@ -1,12 +1,11 @@ package until.the.eternity.auctionsearchoption.infrastructure.persistence; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; import until.the.eternity.auctionsearchoption.domain.entity.AuctionSearchOptionMetadata; import until.the.eternity.auctionsearchoption.domain.repository.AuctionSearchOptionRepositoryPort; -import java.util.List; - @Component @RequiredArgsConstructor class AuctionSearchOptionRepositoryPortImpl implements AuctionSearchOptionRepositoryPort { diff --git a/src/main/java/until/the/eternity/auctionsearchoption/interfaces/rest/AuctionSearchOptionController.java b/src/main/java/until/the/eternity/auctionsearchoption/interfaces/rest/AuctionSearchOptionController.java index 0231a36a..8ad6484b 100644 --- a/src/main/java/until/the/eternity/auctionsearchoption/interfaces/rest/AuctionSearchOptionController.java +++ b/src/main/java/until/the/eternity/auctionsearchoption/interfaces/rest/AuctionSearchOptionController.java @@ -2,6 +2,7 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -11,8 +12,6 @@ import until.the.eternity.auctionsearchoption.interfaces.rest.dto.response.SearchOptionMetadataResponse; import until.the.eternity.common.response.ApiResponse; -import java.util.List; - @Tag(name = "Auction Search Option", description = "경매 검색 옵션 API") @RestController @RequestMapping("/api/search-option") diff --git a/src/main/java/until/the/eternity/auctionsearchoption/interfaces/rest/dto/response/FieldMetadata.java b/src/main/java/until/the/eternity/auctionsearchoption/interfaces/rest/dto/response/FieldMetadata.java index 43e5d916..8b8b6ca1 100644 --- a/src/main/java/until/the/eternity/auctionsearchoption/interfaces/rest/dto/response/FieldMetadata.java +++ b/src/main/java/until/the/eternity/auctionsearchoption/interfaces/rest/dto/response/FieldMetadata.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.annotation.JsonInclude; import io.swagger.v3.oas.annotations.media.Schema; - import java.util.List; @Schema(description = "검색 조건 필드 메타데이터") diff --git a/src/main/java/until/the/eternity/auctionsearchoption/interfaces/rest/dto/response/SearchOptionMetadataResponse.java b/src/main/java/until/the/eternity/auctionsearchoption/interfaces/rest/dto/response/SearchOptionMetadataResponse.java index 02c318fb..04581ef7 100644 --- a/src/main/java/until/the/eternity/auctionsearchoption/interfaces/rest/dto/response/SearchOptionMetadataResponse.java +++ b/src/main/java/until/the/eternity/auctionsearchoption/interfaces/rest/dto/response/SearchOptionMetadataResponse.java @@ -1,7 +1,6 @@ package until.the.eternity.auctionsearchoption.interfaces.rest.dto.response; import io.swagger.v3.oas.annotations.media.Schema; - import java.util.Map; @Schema(description = "검색 옵션 메타데이터 응답") diff --git a/src/main/java/until/the/eternity/common/enums/ItemCategory.java b/src/main/java/until/the/eternity/common/enums/ItemCategory.java index ca9ee313..6b8ab078 100644 --- a/src/main/java/until/the/eternity/common/enums/ItemCategory.java +++ b/src/main/java/until/the/eternity/common/enums/ItemCategory.java @@ -1,12 +1,11 @@ package until.the.eternity.common.enums; -import lombok.Getter; -import lombok.RequiredArgsConstructor; - import java.util.Arrays; import java.util.Map; import java.util.function.Function; import java.util.stream.Collectors; +import lombok.Getter; +import lombok.RequiredArgsConstructor; @Getter @RequiredArgsConstructor diff --git a/src/main/java/until/the/eternity/common/enums/SortDirection.java b/src/main/java/until/the/eternity/common/enums/SortDirection.java index 4a797556..5e688da2 100644 --- a/src/main/java/until/the/eternity/common/enums/SortDirection.java +++ b/src/main/java/until/the/eternity/common/enums/SortDirection.java @@ -3,9 +3,8 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonValue; import io.swagger.v3.oas.annotations.media.Schema; -import org.springframework.data.domain.Sort; - import java.util.Arrays; +import org.springframework.data.domain.Sort; /** 정렬 방향 (오름차순/내림차순) */ @Schema(description = "정렬 방향", enumAsRef = true) diff --git a/src/main/java/until/the/eternity/common/enums/SortField.java b/src/main/java/until/the/eternity/common/enums/SortField.java index 3c26de2b..704d79d5 100644 --- a/src/main/java/until/the/eternity/common/enums/SortField.java +++ b/src/main/java/until/the/eternity/common/enums/SortField.java @@ -3,7 +3,6 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonValue; import io.swagger.v3.oas.annotations.media.Schema; - import java.util.Arrays; /** 정렬 필드 */ diff --git a/src/main/java/until/the/eternity/common/exception/GlobalExceptionCode.java b/src/main/java/until/the/eternity/common/exception/GlobalExceptionCode.java index 1169e930..21c217b0 100644 --- a/src/main/java/until/the/eternity/common/exception/GlobalExceptionCode.java +++ b/src/main/java/until/the/eternity/common/exception/GlobalExceptionCode.java @@ -1,11 +1,11 @@ package until.the.eternity.common.exception; +import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR; + import lombok.Getter; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; -import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR; - @Getter @RequiredArgsConstructor public enum GlobalExceptionCode implements ExceptionCode { diff --git a/src/main/java/until/the/eternity/common/exception/GlobalExceptionHandler.java b/src/main/java/until/the/eternity/common/exception/GlobalExceptionHandler.java index bc069d00..4cf5af10 100644 --- a/src/main/java/until/the/eternity/common/exception/GlobalExceptionHandler.java +++ b/src/main/java/until/the/eternity/common/exception/GlobalExceptionHandler.java @@ -1,5 +1,7 @@ package until.the.eternity.common.exception; +import static until.the.eternity.common.exception.GlobalExceptionCode.SERVER_ERROR; + import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; @@ -7,8 +9,6 @@ import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; import until.the.eternity.common.response.ApiResponse; -import static until.the.eternity.common.exception.GlobalExceptionCode.SERVER_ERROR; - @Slf4j @RestControllerAdvice public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { diff --git a/src/main/java/until/the/eternity/common/filter/GatewayAuthFilter.java b/src/main/java/until/the/eternity/common/filter/GatewayAuthFilter.java index ef255e95..b293dc46 100644 --- a/src/main/java/until/the/eternity/common/filter/GatewayAuthFilter.java +++ b/src/main/java/until/the/eternity/common/filter/GatewayAuthFilter.java @@ -4,6 +4,9 @@ import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; import lombok.NonNull; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -16,10 +19,6 @@ import until.the.eternity.common.entity.CustomWebAuthenticationDetails; import until.the.eternity.common.util.IpAddressUtil; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - /** * Gateway에서 전달한 인증 헤더(X-Auth-*)를 기반으로 Spring Security의 Authentication을 생성하는 필터 * diff --git a/src/main/java/until/the/eternity/common/response/ApiResponse.java b/src/main/java/until/the/eternity/common/response/ApiResponse.java index c7b99b00..c7f83b13 100644 --- a/src/main/java/until/the/eternity/common/response/ApiResponse.java +++ b/src/main/java/until/the/eternity/common/response/ApiResponse.java @@ -1,10 +1,9 @@ package until.the.eternity.common.response; +import java.time.Instant; import lombok.Builder; import lombok.Getter; -import java.time.Instant; - @Getter public class ApiResponse { diff --git a/src/main/java/until/the/eternity/common/response/PageResponseDto.java b/src/main/java/until/the/eternity/common/response/PageResponseDto.java index 0919ce20..c142faac 100644 --- a/src/main/java/until/the/eternity/common/response/PageResponseDto.java +++ b/src/main/java/until/the/eternity/common/response/PageResponseDto.java @@ -1,7 +1,6 @@ package until.the.eternity.common.response; import io.swagger.v3.oas.annotations.media.Schema; - import java.util.List; @Schema(description = "페이지 응답 객체") diff --git a/src/main/java/until/the/eternity/config/openapi/OpenApiFilters.java b/src/main/java/until/the/eternity/config/openapi/OpenApiFilters.java index 29ca38fe..af0147a8 100644 --- a/src/main/java/until/the/eternity/config/openapi/OpenApiFilters.java +++ b/src/main/java/until/the/eternity/config/openapi/OpenApiFilters.java @@ -1,13 +1,12 @@ package until.the.eternity.config.openapi; +import java.time.Duration; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.springframework.web.reactive.function.client.ExchangeFilterFunction; import reactor.core.publisher.Mono; -import java.time.Duration; - @Slf4j @Component @RequiredArgsConstructor diff --git a/src/main/java/until/the/eternity/config/openapi/OpenApiRetryPolicy.java b/src/main/java/until/the/eternity/config/openapi/OpenApiRetryPolicy.java index a419df7c..8c9d0cff 100644 --- a/src/main/java/until/the/eternity/config/openapi/OpenApiRetryPolicy.java +++ b/src/main/java/until/the/eternity/config/openapi/OpenApiRetryPolicy.java @@ -1,12 +1,11 @@ package until.the.eternity.config.openapi; +import java.time.Duration; import org.springframework.stereotype.Component; import org.springframework.web.reactive.function.client.WebClientResponseException; import reactor.util.retry.Retry; import reactor.util.retry.RetryBackoffSpec; -import java.time.Duration; - /** Nexon OPEN API 전용 재시도(Back-off) 정책. */ @Component public class OpenApiRetryPolicy { diff --git a/src/main/java/until/the/eternity/config/openapi/OpenApiWebClientProperties.java b/src/main/java/until/the/eternity/config/openapi/OpenApiWebClientProperties.java index f8d79043..9d51746d 100644 --- a/src/main/java/until/the/eternity/config/openapi/OpenApiWebClientProperties.java +++ b/src/main/java/until/the/eternity/config/openapi/OpenApiWebClientProperties.java @@ -2,11 +2,10 @@ import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Positive; +import java.time.Duration; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.validation.annotation.Validated; -import java.time.Duration; - /** 외부 API용 WebClient 설정값 홀더 application.yml 사용) */ @Validated @ConfigurationProperties(prefix = "openapi.nexon") diff --git a/src/main/java/until/the/eternity/hornBugle/application/scheduler/HornBugleScheduler.java b/src/main/java/until/the/eternity/hornBugle/application/scheduler/HornBugleScheduler.java index 94180550..bc50e1d4 100644 --- a/src/main/java/until/the/eternity/hornBugle/application/scheduler/HornBugleScheduler.java +++ b/src/main/java/until/the/eternity/hornBugle/application/scheduler/HornBugleScheduler.java @@ -1,5 +1,7 @@ package until.the.eternity.hornBugle.application.scheduler; +import java.util.ArrayList; +import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; @@ -11,9 +13,6 @@ import until.the.eternity.hornBugle.interfaces.external.dto.OpenApiHornBugleHistoryListResponse; import until.the.eternity.hornBugle.interfaces.external.dto.OpenApiHornBugleHistoryResponse; -import java.util.ArrayList; -import java.util.List; - @Slf4j @Component @RequiredArgsConstructor diff --git a/src/main/java/until/the/eternity/hornBugle/application/service/HornBugleService.java b/src/main/java/until/the/eternity/hornBugle/application/service/HornBugleService.java index 70db2e65..7253b467 100644 --- a/src/main/java/until/the/eternity/hornBugle/application/service/HornBugleService.java +++ b/src/main/java/until/the/eternity/hornBugle/application/service/HornBugleService.java @@ -1,5 +1,7 @@ package until.the.eternity.hornBugle.application.service; +import java.time.Instant; +import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Page; @@ -15,9 +17,6 @@ import until.the.eternity.hornBugle.interfaces.rest.dto.request.HornBuglePageRequestDto; import until.the.eternity.hornBugle.interfaces.rest.dto.response.HornBugleHistoryResponse; -import java.time.Instant; -import java.util.List; - @Slf4j @Service @RequiredArgsConstructor diff --git a/src/main/java/until/the/eternity/hornBugle/domain/entity/HornBugleWorldHistory.java b/src/main/java/until/the/eternity/hornBugle/domain/entity/HornBugleWorldHistory.java index 05ca8dff..482c8572 100644 --- a/src/main/java/until/the/eternity/hornBugle/domain/entity/HornBugleWorldHistory.java +++ b/src/main/java/until/the/eternity/hornBugle/domain/entity/HornBugleWorldHistory.java @@ -1,9 +1,8 @@ package until.the.eternity.hornBugle.domain.entity; import jakarta.persistence.*; -import lombok.*; - import java.time.Instant; +import lombok.*; @Entity @Table( diff --git a/src/main/java/until/the/eternity/hornBugle/domain/enums/HornBugleServer.java b/src/main/java/until/the/eternity/hornBugle/domain/enums/HornBugleServer.java index 51bd034f..fe361d09 100644 --- a/src/main/java/until/the/eternity/hornBugle/domain/enums/HornBugleServer.java +++ b/src/main/java/until/the/eternity/hornBugle/domain/enums/HornBugleServer.java @@ -1,11 +1,10 @@ package until.the.eternity.hornBugle.domain.enums; -import lombok.Getter; -import lombok.RequiredArgsConstructor; - import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.Arrays; +import lombok.Getter; +import lombok.RequiredArgsConstructor; @Getter @RequiredArgsConstructor diff --git a/src/main/java/until/the/eternity/hornBugle/domain/mapper/HornBugleMapper.java b/src/main/java/until/the/eternity/hornBugle/domain/mapper/HornBugleMapper.java index c9340bf3..173f1191 100644 --- a/src/main/java/until/the/eternity/hornBugle/domain/mapper/HornBugleMapper.java +++ b/src/main/java/until/the/eternity/hornBugle/domain/mapper/HornBugleMapper.java @@ -1,5 +1,6 @@ package until.the.eternity.hornBugle.domain.mapper; +import java.time.Instant; import org.mapstruct.Mapper; import org.mapstruct.Mapping; import until.the.eternity.hornBugle.domain.entity.HornBugleWorldHistory; @@ -7,8 +8,6 @@ import until.the.eternity.hornBugle.interfaces.external.dto.OpenApiHornBugleHistoryResponse; import until.the.eternity.hornBugle.interfaces.rest.dto.response.HornBugleHistoryResponse; -import java.time.Instant; - @Mapper(componentModel = "spring") public interface HornBugleMapper { diff --git a/src/main/java/until/the/eternity/hornBugle/domain/repository/HornBugleRepositoryPort.java b/src/main/java/until/the/eternity/hornBugle/domain/repository/HornBugleRepositoryPort.java index 0162f664..33667068 100644 --- a/src/main/java/until/the/eternity/hornBugle/domain/repository/HornBugleRepositoryPort.java +++ b/src/main/java/until/the/eternity/hornBugle/domain/repository/HornBugleRepositoryPort.java @@ -1,12 +1,11 @@ package until.the.eternity.hornBugle.domain.repository; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import until.the.eternity.hornBugle.domain.entity.HornBugleWorldHistory; - import java.time.Instant; import java.util.List; import java.util.Optional; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import until.the.eternity.hornBugle.domain.entity.HornBugleWorldHistory; public interface HornBugleRepositoryPort { diff --git a/src/main/java/until/the/eternity/hornBugle/domain/service/HornBugleDuplicateChecker.java b/src/main/java/until/the/eternity/hornBugle/domain/service/HornBugleDuplicateChecker.java index 267b975e..011986ca 100644 --- a/src/main/java/until/the/eternity/hornBugle/domain/service/HornBugleDuplicateChecker.java +++ b/src/main/java/until/the/eternity/hornBugle/domain/service/HornBugleDuplicateChecker.java @@ -1,5 +1,10 @@ package until.the.eternity.hornBugle.domain.service; +import java.time.Instant; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; @@ -8,12 +13,6 @@ import until.the.eternity.hornBugle.domain.repository.HornBugleRepositoryPort; import until.the.eternity.hornBugle.interfaces.external.dto.OpenApiHornBugleHistoryResponse; -import java.time.Instant; -import java.util.HashSet; -import java.util.List; -import java.util.Optional; -import java.util.Set; - @Slf4j @Component @RequiredArgsConstructor diff --git a/src/main/java/until/the/eternity/hornBugle/infrastructure/persistence/HornBugleJpaRepository.java b/src/main/java/until/the/eternity/hornBugle/infrastructure/persistence/HornBugleJpaRepository.java index 2fd3bb51..65585cf6 100644 --- a/src/main/java/until/the/eternity/hornBugle/infrastructure/persistence/HornBugleJpaRepository.java +++ b/src/main/java/until/the/eternity/hornBugle/infrastructure/persistence/HornBugleJpaRepository.java @@ -1,5 +1,8 @@ package until.the.eternity.hornBugle.infrastructure.persistence; +import java.time.Instant; +import java.util.List; +import java.util.Optional; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; @@ -7,10 +10,6 @@ import org.springframework.stereotype.Repository; import until.the.eternity.hornBugle.domain.entity.HornBugleWorldHistory; -import java.time.Instant; -import java.util.List; -import java.util.Optional; - @Repository public interface HornBugleJpaRepository extends JpaRepository { diff --git a/src/main/java/until/the/eternity/hornBugle/infrastructure/persistence/HornBugleRepositoryPortImpl.java b/src/main/java/until/the/eternity/hornBugle/infrastructure/persistence/HornBugleRepositoryPortImpl.java index 469b3760..8f7319fc 100644 --- a/src/main/java/until/the/eternity/hornBugle/infrastructure/persistence/HornBugleRepositoryPortImpl.java +++ b/src/main/java/until/the/eternity/hornBugle/infrastructure/persistence/HornBugleRepositoryPortImpl.java @@ -1,6 +1,9 @@ package until.the.eternity.hornBugle.infrastructure.persistence; import jakarta.persistence.EntityManager; +import java.time.Instant; +import java.util.List; +import java.util.Optional; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.domain.Page; @@ -10,10 +13,6 @@ import until.the.eternity.hornBugle.domain.entity.HornBugleWorldHistory; import until.the.eternity.hornBugle.domain.repository.HornBugleRepositoryPort; -import java.time.Instant; -import java.util.List; -import java.util.Optional; - @Repository @RequiredArgsConstructor public class HornBugleRepositoryPortImpl implements HornBugleRepositoryPort { diff --git a/src/main/java/until/the/eternity/hornBugle/interfaces/external/dto/OpenApiHornBugleHistoryListResponse.java b/src/main/java/until/the/eternity/hornBugle/interfaces/external/dto/OpenApiHornBugleHistoryListResponse.java index adea275a..4090a2d4 100644 --- a/src/main/java/until/the/eternity/hornBugle/interfaces/external/dto/OpenApiHornBugleHistoryListResponse.java +++ b/src/main/java/until/the/eternity/hornBugle/interfaces/external/dto/OpenApiHornBugleHistoryListResponse.java @@ -1,7 +1,6 @@ package until.the.eternity.hornBugle.interfaces.external.dto; import com.fasterxml.jackson.annotation.JsonProperty; - import java.util.List; public record OpenApiHornBugleHistoryListResponse( diff --git a/src/main/java/until/the/eternity/hornBugle/interfaces/external/dto/OpenApiHornBugleHistoryResponse.java b/src/main/java/until/the/eternity/hornBugle/interfaces/external/dto/OpenApiHornBugleHistoryResponse.java index 78f67772..1a11eb0f 100644 --- a/src/main/java/until/the/eternity/hornBugle/interfaces/external/dto/OpenApiHornBugleHistoryResponse.java +++ b/src/main/java/until/the/eternity/hornBugle/interfaces/external/dto/OpenApiHornBugleHistoryResponse.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonProperty; - import java.time.Instant; public record OpenApiHornBugleHistoryResponse( diff --git a/src/main/java/until/the/eternity/hornBugle/interfaces/rest/dto/response/HornBugleHistoryResponse.java b/src/main/java/until/the/eternity/hornBugle/interfaces/rest/dto/response/HornBugleHistoryResponse.java index 7660ff1a..cd47b1c0 100644 --- a/src/main/java/until/the/eternity/hornBugle/interfaces/rest/dto/response/HornBugleHistoryResponse.java +++ b/src/main/java/until/the/eternity/hornBugle/interfaces/rest/dto/response/HornBugleHistoryResponse.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.annotation.JsonFormat; import io.swagger.v3.oas.annotations.media.Schema; - import java.time.Instant; @Schema(description = "뿔피리 히스토리 응답") diff --git a/src/main/java/until/the/eternity/iteminfo/application/service/ItemInfoService.java b/src/main/java/until/the/eternity/iteminfo/application/service/ItemInfoService.java index 75ba6584..1c80a412 100644 --- a/src/main/java/until/the/eternity/iteminfo/application/service/ItemInfoService.java +++ b/src/main/java/until/the/eternity/iteminfo/application/service/ItemInfoService.java @@ -1,5 +1,9 @@ package until.the.eternity.iteminfo.application.service; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Page; @@ -18,11 +22,6 @@ import until.the.eternity.iteminfo.interfaces.rest.dto.response.ItemInfoSummaryResponse; import until.the.eternity.iteminfo.interfaces.rest.dto.response.ItemInfoSyncResponse; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - @Slf4j @Service @Transactional(readOnly = true) diff --git a/src/main/java/until/the/eternity/iteminfo/domain/entity/ItemInfoId.java b/src/main/java/until/the/eternity/iteminfo/domain/entity/ItemInfoId.java index 6293e4ee..d53cd3b3 100644 --- a/src/main/java/until/the/eternity/iteminfo/domain/entity/ItemInfoId.java +++ b/src/main/java/until/the/eternity/iteminfo/domain/entity/ItemInfoId.java @@ -2,9 +2,8 @@ import jakarta.persistence.Column; import jakarta.persistence.Embeddable; -import lombok.*; - import java.io.Serializable; +import lombok.*; @Embeddable @Getter diff --git a/src/main/java/until/the/eternity/iteminfo/domain/exception/ItemInfoExceptionCode.java b/src/main/java/until/the/eternity/iteminfo/domain/exception/ItemInfoExceptionCode.java index f870031a..a7ffd4c2 100644 --- a/src/main/java/until/the/eternity/iteminfo/domain/exception/ItemInfoExceptionCode.java +++ b/src/main/java/until/the/eternity/iteminfo/domain/exception/ItemInfoExceptionCode.java @@ -1,12 +1,12 @@ package until.the.eternity.iteminfo.domain.exception; +import static org.springframework.http.HttpStatus.BAD_REQUEST; + import lombok.Getter; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import until.the.eternity.common.exception.ExceptionCode; -import static org.springframework.http.HttpStatus.BAD_REQUEST; - @Getter @RequiredArgsConstructor public enum ItemInfoExceptionCode implements ExceptionCode { diff --git a/src/main/java/until/the/eternity/iteminfo/domain/repository/ItemInfoRepositoryPort.java b/src/main/java/until/the/eternity/iteminfo/domain/repository/ItemInfoRepositoryPort.java index c9f85ea3..b540fc66 100644 --- a/src/main/java/until/the/eternity/iteminfo/domain/repository/ItemInfoRepositoryPort.java +++ b/src/main/java/until/the/eternity/iteminfo/domain/repository/ItemInfoRepositoryPort.java @@ -1,13 +1,12 @@ package until.the.eternity.iteminfo.domain.repository; +import java.util.List; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import until.the.eternity.iteminfo.domain.entity.ItemInfo; import until.the.eternity.iteminfo.domain.entity.ItemInfoId; import until.the.eternity.iteminfo.interfaces.rest.dto.request.ItemInfoSearchRequest; -import java.util.List; - public interface ItemInfoRepositoryPort { List findAll(); diff --git a/src/main/java/until/the/eternity/iteminfo/infrastructure/persistence/ItemInfoJpaRepository.java b/src/main/java/until/the/eternity/iteminfo/infrastructure/persistence/ItemInfoJpaRepository.java index c6838467..2be98732 100644 --- a/src/main/java/until/the/eternity/iteminfo/infrastructure/persistence/ItemInfoJpaRepository.java +++ b/src/main/java/until/the/eternity/iteminfo/infrastructure/persistence/ItemInfoJpaRepository.java @@ -1,13 +1,12 @@ package until.the.eternity.iteminfo.infrastructure.persistence; +import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.data.jpa.repository.Query; import until.the.eternity.iteminfo.domain.entity.ItemInfo; import until.the.eternity.iteminfo.domain.entity.ItemInfoId; -import java.util.List; - public interface ItemInfoJpaRepository extends JpaRepository, JpaSpecificationExecutor { diff --git a/src/main/java/until/the/eternity/iteminfo/infrastructure/persistence/ItemInfoQueryDslRepository.java b/src/main/java/until/the/eternity/iteminfo/infrastructure/persistence/ItemInfoQueryDslRepository.java index e13e45f9..266b2303 100644 --- a/src/main/java/until/the/eternity/iteminfo/infrastructure/persistence/ItemInfoQueryDslRepository.java +++ b/src/main/java/until/the/eternity/iteminfo/infrastructure/persistence/ItemInfoQueryDslRepository.java @@ -3,6 +3,7 @@ import com.querydsl.core.BooleanBuilder; import com.querydsl.jpa.impl.JPAQuery; import com.querydsl.jpa.impl.JPAQueryFactory; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; @@ -12,8 +13,6 @@ import until.the.eternity.iteminfo.domain.entity.QItemInfo; import until.the.eternity.iteminfo.interfaces.rest.dto.request.ItemInfoSearchRequest; -import java.util.List; - @Repository @RequiredArgsConstructor public class ItemInfoQueryDslRepository { diff --git a/src/main/java/until/the/eternity/iteminfo/infrastructure/persistence/ItemInfoRepositoryPortImpl.java b/src/main/java/until/the/eternity/iteminfo/infrastructure/persistence/ItemInfoRepositoryPortImpl.java index 497ad7d1..66481cac 100644 --- a/src/main/java/until/the/eternity/iteminfo/infrastructure/persistence/ItemInfoRepositoryPortImpl.java +++ b/src/main/java/until/the/eternity/iteminfo/infrastructure/persistence/ItemInfoRepositoryPortImpl.java @@ -1,5 +1,6 @@ package until.the.eternity.iteminfo.infrastructure.persistence; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -9,8 +10,6 @@ import until.the.eternity.iteminfo.domain.repository.ItemInfoRepositoryPort; import until.the.eternity.iteminfo.interfaces.rest.dto.request.ItemInfoSearchRequest; -import java.util.List; - @Repository @RequiredArgsConstructor public class ItemInfoRepositoryPortImpl implements ItemInfoRepositoryPort { diff --git a/src/main/java/until/the/eternity/iteminfo/interfaces/rest/controller/ItemInfoController.java b/src/main/java/until/the/eternity/iteminfo/interfaces/rest/controller/ItemInfoController.java index ddb1c49a..755b5a27 100644 --- a/src/main/java/until/the/eternity/iteminfo/interfaces/rest/controller/ItemInfoController.java +++ b/src/main/java/until/the/eternity/iteminfo/interfaces/rest/controller/ItemInfoController.java @@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.http.ResponseEntity; @@ -18,8 +19,6 @@ import until.the.eternity.iteminfo.interfaces.rest.dto.response.ItemInfoSummaryResponse; import until.the.eternity.iteminfo.interfaces.rest.dto.response.ItemInfoSyncResponse; -import java.util.List; - @RestController @RequestMapping("/api/item-infos") @RequiredArgsConstructor diff --git a/src/main/java/until/the/eternity/iteminfo/interfaces/rest/dto/response/ItemCategoryResponse.java b/src/main/java/until/the/eternity/iteminfo/interfaces/rest/dto/response/ItemCategoryResponse.java index 0b1e6817..065d209e 100644 --- a/src/main/java/until/the/eternity/iteminfo/interfaces/rest/dto/response/ItemCategoryResponse.java +++ b/src/main/java/until/the/eternity/iteminfo/interfaces/rest/dto/response/ItemCategoryResponse.java @@ -1,12 +1,11 @@ package until.the.eternity.iteminfo.interfaces.rest.dto.response; -import lombok.Builder; -import lombok.Getter; -import until.the.eternity.common.enums.ItemCategory; - import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; +import lombok.Builder; +import lombok.Getter; +import until.the.eternity.common.enums.ItemCategory; @Getter @Builder diff --git a/src/main/java/until/the/eternity/iteminfo/interfaces/rest/dto/response/ItemInfoResponse.java b/src/main/java/until/the/eternity/iteminfo/interfaces/rest/dto/response/ItemInfoResponse.java index 70897793..be9d890b 100644 --- a/src/main/java/until/the/eternity/iteminfo/interfaces/rest/dto/response/ItemInfoResponse.java +++ b/src/main/java/until/the/eternity/iteminfo/interfaces/rest/dto/response/ItemInfoResponse.java @@ -1,11 +1,10 @@ package until.the.eternity.iteminfo.interfaces.rest.dto.response; import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Builder; -import until.the.eternity.iteminfo.domain.entity.ItemInfo; - import java.util.List; import java.util.stream.Collectors; +import lombok.Builder; +import until.the.eternity.iteminfo.domain.entity.ItemInfo; @Builder @Schema(description = "아이템 정보 응답 DTO") diff --git a/src/main/java/until/the/eternity/iteminfo/interfaces/rest/dto/response/ItemInfoSummaryResponse.java b/src/main/java/until/the/eternity/iteminfo/interfaces/rest/dto/response/ItemInfoSummaryResponse.java index 4b9da91b..98e5cfd5 100644 --- a/src/main/java/until/the/eternity/iteminfo/interfaces/rest/dto/response/ItemInfoSummaryResponse.java +++ b/src/main/java/until/the/eternity/iteminfo/interfaces/rest/dto/response/ItemInfoSummaryResponse.java @@ -1,11 +1,10 @@ package until.the.eternity.iteminfo.interfaces.rest.dto.response; import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Builder; -import until.the.eternity.iteminfo.domain.entity.ItemInfo; - import java.util.List; import java.util.stream.Collectors; +import lombok.Builder; +import until.the.eternity.iteminfo.domain.entity.ItemInfo; @Builder @Schema(description = "아이템 정보 요약 응답 DTO") diff --git a/src/main/java/until/the/eternity/iteminfo/interfaces/rest/dto/response/ItemInfoSyncResponse.java b/src/main/java/until/the/eternity/iteminfo/interfaces/rest/dto/response/ItemInfoSyncResponse.java index 02016f52..e98fce37 100644 --- a/src/main/java/until/the/eternity/iteminfo/interfaces/rest/dto/response/ItemInfoSyncResponse.java +++ b/src/main/java/until/the/eternity/iteminfo/interfaces/rest/dto/response/ItemInfoSyncResponse.java @@ -1,9 +1,8 @@ package until.the.eternity.iteminfo.interfaces.rest.dto.response; import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Builder; - import java.util.List; +import lombok.Builder; @Builder @Schema(description = "아이템 정보 동기화 응답 DTO") diff --git a/src/main/java/until/the/eternity/itemoptioninfo/application/service/ItemOptionInfoService.java b/src/main/java/until/the/eternity/itemoptioninfo/application/service/ItemOptionInfoService.java index fb86c034..e199d4ba 100644 --- a/src/main/java/until/the/eternity/itemoptioninfo/application/service/ItemOptionInfoService.java +++ b/src/main/java/until/the/eternity/itemoptioninfo/application/service/ItemOptionInfoService.java @@ -1,13 +1,12 @@ package until.the.eternity.itemoptioninfo.application.service; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import until.the.eternity.itemoptioninfo.domain.entity.ItemOptionInfo; import until.the.eternity.itemoptioninfo.domain.repository.ItemOptionInfoRepositoryPort; -import java.util.List; - @Service @RequiredArgsConstructor @Transactional(readOnly = true) diff --git a/src/main/java/until/the/eternity/itemoptioninfo/domain/entity/ItemOptionInfoId.java b/src/main/java/until/the/eternity/itemoptioninfo/domain/entity/ItemOptionInfoId.java index 5bbcdaf9..17634464 100644 --- a/src/main/java/until/the/eternity/itemoptioninfo/domain/entity/ItemOptionInfoId.java +++ b/src/main/java/until/the/eternity/itemoptioninfo/domain/entity/ItemOptionInfoId.java @@ -2,13 +2,12 @@ import jakarta.persistence.Column; import jakarta.persistence.Embeddable; +import java.io.Serializable; +import java.util.Objects; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; -import java.io.Serializable; -import java.util.Objects; - @Embeddable @Getter @NoArgsConstructor diff --git a/src/main/java/until/the/eternity/itemoptioninfo/domain/repository/ItemOptionInfoRepositoryPort.java b/src/main/java/until/the/eternity/itemoptioninfo/domain/repository/ItemOptionInfoRepositoryPort.java index 01e49348..86bb898b 100644 --- a/src/main/java/until/the/eternity/itemoptioninfo/domain/repository/ItemOptionInfoRepositoryPort.java +++ b/src/main/java/until/the/eternity/itemoptioninfo/domain/repository/ItemOptionInfoRepositoryPort.java @@ -1,8 +1,7 @@ package until.the.eternity.itemoptioninfo.domain.repository; -import until.the.eternity.itemoptioninfo.domain.entity.ItemOptionInfo; - import java.util.List; +import until.the.eternity.itemoptioninfo.domain.entity.ItemOptionInfo; public interface ItemOptionInfoRepositoryPort { List findAll(); diff --git a/src/main/java/until/the/eternity/itemoptioninfo/infrastructure/persistence/ItemOptionInfoRepositoryPortImpl.java b/src/main/java/until/the/eternity/itemoptioninfo/infrastructure/persistence/ItemOptionInfoRepositoryPortImpl.java index b523642a..d01cc9e1 100644 --- a/src/main/java/until/the/eternity/itemoptioninfo/infrastructure/persistence/ItemOptionInfoRepositoryPortImpl.java +++ b/src/main/java/until/the/eternity/itemoptioninfo/infrastructure/persistence/ItemOptionInfoRepositoryPortImpl.java @@ -1,12 +1,11 @@ package until.the.eternity.itemoptioninfo.infrastructure.persistence; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; import until.the.eternity.itemoptioninfo.domain.entity.ItemOptionInfo; import until.the.eternity.itemoptioninfo.domain.repository.ItemOptionInfoRepositoryPort; -import java.util.List; - @Repository @RequiredArgsConstructor public class ItemOptionInfoRepositoryPortImpl implements ItemOptionInfoRepositoryPort { diff --git a/src/main/java/until/the/eternity/itemoptioninfo/interfaces/rest/controller/ItemOptionInfoController.java b/src/main/java/until/the/eternity/itemoptioninfo/interfaces/rest/controller/ItemOptionInfoController.java index 97bafca3..d74ab49f 100644 --- a/src/main/java/until/the/eternity/itemoptioninfo/interfaces/rest/controller/ItemOptionInfoController.java +++ b/src/main/java/until/the/eternity/itemoptioninfo/interfaces/rest/controller/ItemOptionInfoController.java @@ -2,6 +2,8 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.List; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; @@ -10,9 +12,6 @@ import until.the.eternity.itemoptioninfo.domain.mapper.ItemOptionInfoMapper; import until.the.eternity.itemoptioninfo.interfaces.rest.dto.response.ItemOptionInfoResponse; -import java.util.List; -import java.util.stream.Collectors; - @RestController @RequestMapping("/api/v1/item-option-infos") @RequiredArgsConstructor diff --git a/src/main/java/until/the/eternity/metalwareinfo/application/service/MetalwareInfoService.java b/src/main/java/until/the/eternity/metalwareinfo/application/service/MetalwareInfoService.java index 8135be62..499de1a0 100644 --- a/src/main/java/until/the/eternity/metalwareinfo/application/service/MetalwareInfoService.java +++ b/src/main/java/until/the/eternity/metalwareinfo/application/service/MetalwareInfoService.java @@ -1,13 +1,12 @@ package until.the.eternity.metalwareinfo.application.service; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import until.the.eternity.metalwareinfo.domain.repository.MetalwareInfoRepositoryPort; import until.the.eternity.metalwareinfo.interfaces.rest.dto.response.MetalwareInfoResponse; -import java.util.List; - @Service @Transactional(readOnly = true) @RequiredArgsConstructor diff --git a/src/main/java/until/the/eternity/metalwareinfo/infrastructure/persistence/MetalwareInfoJpaRepository.java b/src/main/java/until/the/eternity/metalwareinfo/infrastructure/persistence/MetalwareInfoJpaRepository.java index 202802f5..61481a24 100644 --- a/src/main/java/until/the/eternity/metalwareinfo/infrastructure/persistence/MetalwareInfoJpaRepository.java +++ b/src/main/java/until/the/eternity/metalwareinfo/infrastructure/persistence/MetalwareInfoJpaRepository.java @@ -1,10 +1,9 @@ package until.the.eternity.metalwareinfo.infrastructure.persistence; +import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; -import java.util.List; - public interface MetalwareInfoJpaRepository extends JpaRepository { @Query("SELECT m.metalware FROM MetalwareInfoEntity m") diff --git a/src/main/java/until/the/eternity/metalwareinfo/infrastructure/persistence/MetalwareInfoRepositoryPortImpl.java b/src/main/java/until/the/eternity/metalwareinfo/infrastructure/persistence/MetalwareInfoRepositoryPortImpl.java index 3ee9a1a3..9121dd1b 100644 --- a/src/main/java/until/the/eternity/metalwareinfo/infrastructure/persistence/MetalwareInfoRepositoryPortImpl.java +++ b/src/main/java/until/the/eternity/metalwareinfo/infrastructure/persistence/MetalwareInfoRepositoryPortImpl.java @@ -1,11 +1,10 @@ package until.the.eternity.metalwareinfo.infrastructure.persistence; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; import until.the.eternity.metalwareinfo.domain.repository.MetalwareInfoRepositoryPort; -import java.util.List; - @Repository @RequiredArgsConstructor public class MetalwareInfoRepositoryPortImpl implements MetalwareInfoRepositoryPort { diff --git a/src/main/java/until/the/eternity/metalwareinfo/interfaces/rest/controller/MetalwareInfoController.java b/src/main/java/until/the/eternity/metalwareinfo/interfaces/rest/controller/MetalwareInfoController.java index 271a7e35..fa264595 100644 --- a/src/main/java/until/the/eternity/metalwareinfo/interfaces/rest/controller/MetalwareInfoController.java +++ b/src/main/java/until/the/eternity/metalwareinfo/interfaces/rest/controller/MetalwareInfoController.java @@ -2,6 +2,7 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; @@ -9,8 +10,6 @@ import until.the.eternity.metalwareinfo.application.service.MetalwareInfoService; import until.the.eternity.metalwareinfo.interfaces.rest.dto.response.MetalwareInfoResponse; -import java.util.List; - @RestController @RequestMapping("/api/metalware-infos") @RequiredArgsConstructor diff --git a/src/main/java/until/the/eternity/metalwareinfo/interfaces/rest/dto/response/MetalwareInfoResponse.java b/src/main/java/until/the/eternity/metalwareinfo/interfaces/rest/dto/response/MetalwareInfoResponse.java index 4e8d9d1e..a3122710 100644 --- a/src/main/java/until/the/eternity/metalwareinfo/interfaces/rest/dto/response/MetalwareInfoResponse.java +++ b/src/main/java/until/the/eternity/metalwareinfo/interfaces/rest/dto/response/MetalwareInfoResponse.java @@ -1,10 +1,9 @@ package until.the.eternity.metalwareinfo.interfaces.rest.dto.response; import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Builder; - import java.util.List; import java.util.stream.Collectors; +import lombok.Builder; @Builder @Schema(description = "세공 정보 응답 DTO") 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 f6217f44..361df9c1 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 @@ -2,11 +2,10 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.persistence.*; -import lombok.*; - import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; +import lombok.*; @Entity @Table( diff --git a/src/main/java/until/the/eternity/statistics/domain/entity/daily/SubcategoryDailyStatistics.java b/src/main/java/until/the/eternity/statistics/domain/entity/daily/SubcategoryDailyStatistics.java index 887807d9..66e5382d 100644 --- a/src/main/java/until/the/eternity/statistics/domain/entity/daily/SubcategoryDailyStatistics.java +++ b/src/main/java/until/the/eternity/statistics/domain/entity/daily/SubcategoryDailyStatistics.java @@ -2,11 +2,10 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.persistence.*; -import lombok.*; - import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; +import lombok.*; @Entity @Table( diff --git a/src/main/java/until/the/eternity/statistics/domain/entity/daily/TopCategoryDailyStatistics.java b/src/main/java/until/the/eternity/statistics/domain/entity/daily/TopCategoryDailyStatistics.java index bfcb536b..fb3ab7a8 100644 --- a/src/main/java/until/the/eternity/statistics/domain/entity/daily/TopCategoryDailyStatistics.java +++ b/src/main/java/until/the/eternity/statistics/domain/entity/daily/TopCategoryDailyStatistics.java @@ -2,11 +2,10 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.persistence.*; -import lombok.*; - import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; +import lombok.*; @Entity @Table( 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 44c20386..6970f18e 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 @@ -2,11 +2,10 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.persistence.*; -import lombok.*; - import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; +import lombok.*; @Entity @Table( diff --git a/src/main/java/until/the/eternity/statistics/domain/entity/weekly/SubcategoryWeeklyStatistics.java b/src/main/java/until/the/eternity/statistics/domain/entity/weekly/SubcategoryWeeklyStatistics.java index a05c528e..5756b091 100644 --- a/src/main/java/until/the/eternity/statistics/domain/entity/weekly/SubcategoryWeeklyStatistics.java +++ b/src/main/java/until/the/eternity/statistics/domain/entity/weekly/SubcategoryWeeklyStatistics.java @@ -2,11 +2,10 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.persistence.*; -import lombok.*; - import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; +import lombok.*; @Entity @Table( diff --git a/src/main/java/until/the/eternity/statistics/domain/entity/weekly/TopCategoryWeeklyStatistics.java b/src/main/java/until/the/eternity/statistics/domain/entity/weekly/TopCategoryWeeklyStatistics.java index 22e4406d..9201faaf 100644 --- a/src/main/java/until/the/eternity/statistics/domain/entity/weekly/TopCategoryWeeklyStatistics.java +++ b/src/main/java/until/the/eternity/statistics/domain/entity/weekly/TopCategoryWeeklyStatistics.java @@ -2,11 +2,10 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.persistence.*; -import lombok.*; - import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; +import lombok.*; @Entity @Table( diff --git a/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/request/DailyStatisticsSearchRequest.java b/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/request/DailyStatisticsSearchRequest.java index 59954a6b..7908da49 100644 --- a/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/request/DailyStatisticsSearchRequest.java +++ b/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/request/DailyStatisticsSearchRequest.java @@ -1,9 +1,8 @@ package until.the.eternity.statistics.interfaces.rest.dto.request; import io.swagger.v3.oas.annotations.media.Schema; -import org.springframework.format.annotation.DateTimeFormat; - import java.time.LocalDate; +import org.springframework.format.annotation.DateTimeFormat; @Schema(description = "일간 통계 검색 요청") public record DailyStatisticsSearchRequest( 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 index 46b2cf78..f6f91521 100644 --- 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 @@ -2,9 +2,8 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; -import org.springframework.format.annotation.DateTimeFormat; - import java.time.LocalDate; +import org.springframework.format.annotation.DateTimeFormat; @Schema(description = "아이템별 일간 통계 검색 요청") public record ItemDailyStatisticsSearchRequest( 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 index d132153d..1360de10 100644 --- 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 @@ -2,9 +2,8 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; -import org.springframework.format.annotation.DateTimeFormat; - import java.time.LocalDate; +import org.springframework.format.annotation.DateTimeFormat; @Schema(description = "아이템별 주간 통계 검색 요청") public record ItemWeeklyStatisticsSearchRequest( 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 index 4ff95e0d..47c2c966 100644 --- 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 @@ -2,9 +2,8 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; -import org.springframework.format.annotation.DateTimeFormat; - import java.time.LocalDate; +import org.springframework.format.annotation.DateTimeFormat; @Schema(description = "서브카테고리별 일간 통계 검색 요청") public record SubcategoryDailyStatisticsSearchRequest( 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 index e0247871..9d7bc716 100644 --- 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 @@ -2,9 +2,8 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; -import org.springframework.format.annotation.DateTimeFormat; - import java.time.LocalDate; +import org.springframework.format.annotation.DateTimeFormat; @Schema(description = "서브카테고리별 주간 통계 검색 요청") public record SubcategoryWeeklyStatisticsSearchRequest( 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 index 382bfefb..4b8e25d7 100644 --- 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 @@ -2,9 +2,8 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; -import org.springframework.format.annotation.DateTimeFormat; - import java.time.LocalDate; +import org.springframework.format.annotation.DateTimeFormat; @Schema(description = "탑카테고리별 일간 통계 검색 요청") public record TopCategoryDailyStatisticsSearchRequest( 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 index 2986275f..ad1c6b41 100644 --- 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 @@ -2,9 +2,8 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; -import org.springframework.format.annotation.DateTimeFormat; - import java.time.LocalDate; +import org.springframework.format.annotation.DateTimeFormat; @Schema(description = "탑카테고리별 주간 통계 검색 요청") public record TopCategoryWeeklyStatisticsSearchRequest( diff --git a/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/response/ItemDailyStatisticsResponse.java b/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/response/ItemDailyStatisticsResponse.java index e2485f4d..a9611010 100644 --- a/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/response/ItemDailyStatisticsResponse.java +++ b/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/response/ItemDailyStatisticsResponse.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.annotation.JsonFormat; import io.swagger.v3.oas.annotations.media.Schema; - import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; diff --git a/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/response/ItemWeeklyStatisticsResponse.java b/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/response/ItemWeeklyStatisticsResponse.java index 7d499d85..05e86f70 100644 --- a/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/response/ItemWeeklyStatisticsResponse.java +++ b/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/response/ItemWeeklyStatisticsResponse.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.annotation.JsonFormat; import io.swagger.v3.oas.annotations.media.Schema; - import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; diff --git a/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/response/SubcategoryDailyStatisticsResponse.java b/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/response/SubcategoryDailyStatisticsResponse.java index b167b92c..e27f7f3a 100644 --- a/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/response/SubcategoryDailyStatisticsResponse.java +++ b/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/response/SubcategoryDailyStatisticsResponse.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.annotation.JsonFormat; import io.swagger.v3.oas.annotations.media.Schema; - import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; diff --git a/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/response/SubcategoryWeeklyStatisticsResponse.java b/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/response/SubcategoryWeeklyStatisticsResponse.java index 5f67f69e..bb826866 100644 --- a/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/response/SubcategoryWeeklyStatisticsResponse.java +++ b/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/response/SubcategoryWeeklyStatisticsResponse.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.annotation.JsonFormat; import io.swagger.v3.oas.annotations.media.Schema; - import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; diff --git a/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/response/TopCategoryDailyStatisticsResponse.java b/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/response/TopCategoryDailyStatisticsResponse.java index df6c9f8e..fd49c2f2 100644 --- a/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/response/TopCategoryDailyStatisticsResponse.java +++ b/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/response/TopCategoryDailyStatisticsResponse.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.annotation.JsonFormat; import io.swagger.v3.oas.annotations.media.Schema; - import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; diff --git a/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/response/TopCategoryWeeklyStatisticsResponse.java b/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/response/TopCategoryWeeklyStatisticsResponse.java index 01782f02..ad81f768 100644 --- a/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/response/TopCategoryWeeklyStatisticsResponse.java +++ b/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/response/TopCategoryWeeklyStatisticsResponse.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.annotation.JsonFormat; import io.swagger.v3.oas.annotations.media.Schema; - import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; 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 253bcac2..447c2425 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,5 +1,7 @@ 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; @@ -7,9 +9,6 @@ import org.springframework.transaction.annotation.Transactional; import until.the.eternity.statistics.domain.entity.daily.ItemDailyStatistics; -import java.time.LocalDate; -import java.util.List; - public interface ItemDailyStatisticsRepository extends JpaRepository { /** 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 e8f72fc4..3bed0b1a 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,5 +1,7 @@ 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; @@ -7,9 +9,6 @@ import org.springframework.transaction.annotation.Transactional; import until.the.eternity.statistics.domain.entity.daily.SubcategoryDailyStatistics; -import java.time.LocalDate; -import java.util.List; - public interface SubcategoryDailyStatisticsRepository extends JpaRepository { 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 0fff60a8..31749482 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,5 +1,7 @@ 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; @@ -7,9 +9,6 @@ import org.springframework.transaction.annotation.Transactional; import until.the.eternity.statistics.domain.entity.daily.TopCategoryDailyStatistics; -import java.time.LocalDate; -import java.util.List; - public interface TopCategoryDailyStatisticsRepository extends JpaRepository { 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 24f37b00..dc7a301a 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,5 +1,6 @@ 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; @@ -7,8 +8,6 @@ import org.springframework.transaction.annotation.Transactional; import until.the.eternity.statistics.domain.entity.weekly.ItemWeeklyStatistics; -import java.util.List; - public interface ItemWeeklyStatisticsRepository extends JpaRepository { /** 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 ea3542c1..fb0986af 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,5 +1,6 @@ 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; @@ -7,8 +8,6 @@ import org.springframework.transaction.annotation.Transactional; import until.the.eternity.statistics.domain.entity.weekly.SubcategoryWeeklyStatistics; -import java.util.List; - public interface SubcategoryWeeklyStatisticsRepository extends JpaRepository { 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 c2140d57..45a1dad4 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,5 +1,6 @@ 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; @@ -7,8 +8,6 @@ import org.springframework.transaction.annotation.Transactional; import until.the.eternity.statistics.domain.entity.weekly.TopCategoryWeeklyStatistics; -import java.util.List; - public interface TopCategoryWeeklyStatisticsRepository extends JpaRepository { diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 5892cbf8..4c644416 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -93,6 +93,11 @@ openapi: cron: ${HORN_BUGLE_CRON:0 */5 * * * *} max-retries: ${HORN_BUGLE_MAX_RETRIES:3} retry-delay-ms: ${HORN_BUGLE_RETRY_DELAY_MS:2000} + auction-realtime: + # 실시간 경매장 데이터 수집 스케줄러 + # 10분 간격: 0, 10, 20, 30, 40, 50분 + cron: ${AUCTION_REALTIME_CRON:0 0/10 * * * *} + delay-ms: ${AUCTION_REALTIME_DELAY_MS:500} statistics: previous-day: diff --git a/src/main/resources/db/migration/V15__refactor_auction_tables_for_realtime_history_separation.sql b/src/main/resources/db/migration/V15__refactor_auction_tables_for_realtime_history_separation.sql new file mode 100644 index 00000000..adc9b021 --- /dev/null +++ b/src/main/resources/db/migration/V15__refactor_auction_tables_for_realtime_history_separation.sql @@ -0,0 +1,52 @@ +-- V15: Refactor auction tables for realtime/history separation +-- 1. auction_item_option -> auction_history_item_option (rename, remove auction_item_id) +-- 2. auction_item -> auction_realtime_item (rename, add category columns) +-- 3. Create auction_realtime_item_option table + +-- ============================================================ +-- Step 1: Modify auction_item_option table +-- ============================================================ + +-- 1-1. Drop foreign key constraint for auction_item_id +ALTER TABLE auction_item_option DROP FOREIGN KEY fk_option_item; + +-- 1-2. Drop auction_item_id column +ALTER TABLE auction_item_option DROP COLUMN auction_item_id; + +-- 1-3. Rename table to auction_history_item_option +RENAME TABLE auction_item_option TO auction_history_item_option; + +-- ============================================================ +-- Step 2: Modify auction_item table +-- ============================================================ + +-- 2-1. Add category columns +ALTER TABLE auction_item ADD COLUMN item_sub_category VARCHAR(25) NOT NULL DEFAULT '' COMMENT '아이템 하위 카테고리'; +ALTER TABLE auction_item ADD COLUMN item_top_category VARCHAR(25) NOT NULL DEFAULT '' COMMENT '아이템 상위 카테고리'; + +-- 2-2. Rename table to auction_realtime_item +RENAME TABLE auction_item TO auction_realtime_item; + +-- 2-3. Create indexes for auction_realtime_item +CREATE INDEX idx_realtime_top_sub_item + ON auction_realtime_item (item_top_category, item_sub_category, item_name); + +CREATE INDEX idx_realtime_sub_category_expire + ON auction_realtime_item (item_sub_category, date_auction_expire DESC); + +-- ============================================================ +-- Step 3: Create auction_realtime_item_option table +-- ============================================================ + +CREATE TABLE auction_realtime_item_option ( + id VARCHAR(36) NOT NULL COMMENT 'ItemOption의 고유 식별자 (UUID)', + auction_realtime_item_id BIGINT NOT NULL COMMENT 'auction_realtime_item 테이블의 외래 키', + option_type VARCHAR(100) COMMENT '아이템 옵션 유형', + option_sub_type VARCHAR(100) COMMENT '아이템 옵션 하위 유형', + option_value VARCHAR(255) COMMENT '아이템 옵션 값', + option_value2 VARCHAR(255) COMMENT '아이템 옵션 값 2', + option_desc TEXT COMMENT '아이템 옵션 부가 정보', + PRIMARY KEY (id), + CONSTRAINT fk_realtime_option_item FOREIGN KEY (auction_realtime_item_id) + REFERENCES auction_realtime_item(id) ON DELETE CASCADE +) COMMENT='실시간 경매장 아이템 옵션 정보 테이블'; diff --git a/src/test/java/until/the/eternity/auctionhistory/application/service/AuctionHistoryServiceTest.java b/src/test/java/until/the/eternity/auctionhistory/application/service/AuctionHistoryServiceTest.java index 7ae6dd98..ed2fea8a 100644 --- a/src/test/java/until/the/eternity/auctionhistory/application/service/AuctionHistoryServiceTest.java +++ b/src/test/java/until/the/eternity/auctionhistory/application/service/AuctionHistoryServiceTest.java @@ -1,5 +1,11 @@ package until.the.eternity.auctionhistory.application.service; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.*; + +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; @@ -21,13 +27,6 @@ import until.the.eternity.common.request.PageRequestDto; import until.the.eternity.common.response.PageResponseDto; -import java.util.List; -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.Mockito.*; - @ExtendWith(MockitoExtension.class) class AuctionHistoryServiceTest { diff --git a/src/test/java/until/the/eternity/auctionhistory/application/service/fetcher/AuctionHistoryFetcherTest.java b/src/test/java/until/the/eternity/auctionhistory/application/service/fetcher/AuctionHistoryFetcherTest.java index d1187257..e6e76af4 100644 --- a/src/test/java/until/the/eternity/auctionhistory/application/service/fetcher/AuctionHistoryFetcherTest.java +++ b/src/test/java/until/the/eternity/auctionhistory/application/service/fetcher/AuctionHistoryFetcherTest.java @@ -1,5 +1,13 @@ package until.the.eternity.auctionhistory.application.service.fetcher; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +import java.time.Instant; +import java.util.List; +import java.util.OptionalInt; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -14,15 +22,6 @@ import until.the.eternity.auctionhistory.interfaces.external.dto.OpenApiAuctionHistoryResponse; import until.the.eternity.common.enums.ItemCategory; -import java.time.Instant; -import java.util.List; -import java.util.OptionalInt; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.*; - @ExtendWith(MockitoExtension.class) class AuctionHistoryFetcherTest { diff --git a/src/test/java/until/the/eternity/auctionhistory/application/service/persister/AuctionHistoryPersisterTest.java b/src/test/java/until/the/eternity/auctionhistory/application/service/persister/AuctionHistoryPersisterTest.java index 7764a911..0d9b8d2d 100644 --- a/src/test/java/until/the/eternity/auctionhistory/application/service/persister/AuctionHistoryPersisterTest.java +++ b/src/test/java/until/the/eternity/auctionhistory/application/service/persister/AuctionHistoryPersisterTest.java @@ -1,5 +1,11 @@ package until.the.eternity.auctionhistory.application.service.persister; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.*; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -13,13 +19,6 @@ import until.the.eternity.auctionhistory.interfaces.external.dto.OpenApiAuctionHistoryResponse; import until.the.eternity.common.enums.ItemCategory; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.*; - @ExtendWith(MockitoExtension.class) class AuctionHistoryPersisterTest { diff --git a/src/test/java/until/the/eternity/auctionhistory/domain/service/AuctionHistoryDuplicateCheckerTest.java b/src/test/java/until/the/eternity/auctionhistory/domain/service/AuctionHistoryDuplicateCheckerTest.java index 6cfdc813..3c6bf7b9 100644 --- a/src/test/java/until/the/eternity/auctionhistory/domain/service/AuctionHistoryDuplicateCheckerTest.java +++ b/src/test/java/until/the/eternity/auctionhistory/domain/service/AuctionHistoryDuplicateCheckerTest.java @@ -1,5 +1,13 @@ package until.the.eternity.auctionhistory.domain.service; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +import java.time.Instant; +import java.util.List; +import java.util.Optional; +import java.util.OptionalInt; +import java.util.Set; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -12,15 +20,6 @@ import until.the.eternity.auctionhistory.interfaces.external.dto.OpenApiAuctionHistoryResponse; import until.the.eternity.common.enums.ItemCategory; -import java.time.Instant; -import java.util.List; -import java.util.Optional; -import java.util.OptionalInt; -import java.util.Set; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.when; - @ExtendWith(MockitoExtension.class) class AuctionHistoryDuplicateCheckerTest { diff --git a/src/test/java/until/the/eternity/auctionrealtime/application/service/fetcher/AuctionRealtimeFetcherTest.java b/src/test/java/until/the/eternity/auctionrealtime/application/service/fetcher/AuctionRealtimeFetcherTest.java new file mode 100644 index 00000000..3c2c7bb3 --- /dev/null +++ b/src/test/java/until/the/eternity/auctionrealtime/application/service/fetcher/AuctionRealtimeFetcherTest.java @@ -0,0 +1,246 @@ +package until.the.eternity.auctionrealtime.application.service.fetcher; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +import java.time.Instant; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +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 reactor.core.publisher.Mono; +import until.the.eternity.auctionrealtime.domain.service.AuctionRealtimeDuplicateChecker; +import until.the.eternity.auctionrealtime.domain.service.AuctionRealtimeDuplicateChecker.DuplicateCheckResult; +import until.the.eternity.auctionrealtime.domain.service.fetcher.AuctionRealtimeFetcherPort.FetchResult; +import until.the.eternity.auctionrealtime.infrastructure.client.AuctionRealtimeClient; +import until.the.eternity.auctionrealtime.interfaces.external.dto.OpenApiAuctionRealtimeListResponse; +import until.the.eternity.auctionrealtime.interfaces.external.dto.OpenApiAuctionRealtimeResponse; +import until.the.eternity.common.enums.ItemCategory; + +@ExtendWith(MockitoExtension.class) +class AuctionRealtimeFetcherTest { + + @Mock AuctionRealtimeClient client; + + @Mock AuctionRealtimeDuplicateChecker duplicateChecker; + + @InjectMocks AuctionRealtimeFetcher fetcher; + + private OpenApiAuctionRealtimeResponse dummy() { + return new OpenApiAuctionRealtimeResponse( + "페러시우스 타이탄 블레이드", "신성한 페러시우스 타이탄 블레이드", 1L, 100L, Instant.now(), null); + } + + @Nested + @DisplayName("OPEN API 끝까지 호출 시나리오") + class NormalFlow { + + @Test + @DisplayName("모든 페이지를 수집하고 cursor가 null이면 종료한다") + void fetchAllPages() { + // given + var page1 = + new OpenApiAuctionRealtimeListResponse(List.of(dummy(), dummy()), "cursor-1"); + var page2 = new OpenApiAuctionRealtimeListResponse(List.of(dummy()), null); + + when(client.fetchAuctionList(ItemCategory.SWORD, "")).thenReturn(Mono.just(page1)); + when(client.fetchAuctionList(ItemCategory.SWORD, "cursor-1")) + .thenReturn(Mono.just(page2)); + when(duplicateChecker.checkDuplicateInBatch(any(), eq(ItemCategory.SWORD))) + .thenReturn(DuplicateCheckResult.noDuplicate()); + + // when + FetchResult result = fetcher.fetch(ItemCategory.SWORD); + + // then + assertThat(result.items()).hasSize(3); + assertThat(result.hasEqualDate()).isFalse(); + + verify(client, times(2)).fetchAuctionList(eq(ItemCategory.SWORD), any()); + verify(duplicateChecker, times(2)).checkDuplicateInBatch(any(), eq(ItemCategory.SWORD)); + } + } + + @Nested + @DisplayName("OPEN API 호출 중단 시나리오") + class EarlyBreakFlow { + + @Test + @DisplayName("첫 배치 첫 항목에서 중복이면 빈 리스트를 반환한다") + void stopOnDuplicateAtFirstItem() { + // given + var page1 = + new OpenApiAuctionRealtimeListResponse(List.of(dummy(), dummy()), "cursor-1"); + Instant latestDate = Instant.parse("2024-01-01T00:00:00Z"); + + when(client.fetchAuctionList(ItemCategory.SWORD, "")).thenReturn(Mono.just(page1)); + when(duplicateChecker.checkDuplicateInBatch(page1.auctionItems(), ItemCategory.SWORD)) + .thenReturn(DuplicateCheckResult.duplicateFound(0, latestDate)); + + // when + FetchResult result = fetcher.fetch(ItemCategory.SWORD); + + // then + assertThat(result.items()).isEmpty(); + assertThat(result.latestDate()).isEqualTo(latestDate); + verify(client, times(1)).fetchAuctionList(ItemCategory.SWORD, ""); + verifyNoMoreInteractions(client); + } + + @Test + @DisplayName("첫 배치 중간에서 중복이면 중복 전까지만 반환한다") + void stopOnDuplicateAtMiddle() { + // given + var batch = List.of(dummy(), dummy(), dummy()); + var page1 = new OpenApiAuctionRealtimeListResponse(batch, "cursor-1"); + Instant latestDate = Instant.parse("2024-01-01T00:00:00Z"); + + when(client.fetchAuctionList(ItemCategory.SWORD, "")).thenReturn(Mono.just(page1)); + when(duplicateChecker.checkDuplicateInBatch(batch, ItemCategory.SWORD)) + .thenReturn(DuplicateCheckResult.duplicateFound(2, latestDate)); + + // when + FetchResult result = fetcher.fetch(ItemCategory.SWORD); + + // then + assertThat(result.items()).hasSize(2); + verify(client, times(1)).fetchAuctionList(ItemCategory.SWORD, ""); + verifyNoMoreInteractions(client); + } + + @Test + @DisplayName("동일 날짜 데이터가 감지되면 hasEqualDate가 true로 반환된다") + void stopOnEqualDate() { + // given + var batch = List.of(dummy(), dummy()); + var page1 = new OpenApiAuctionRealtimeListResponse(batch, "cursor-1"); + Instant latestDate = Instant.parse("2024-01-01T00:00:00Z"); + + when(client.fetchAuctionList(ItemCategory.SWORD, "")).thenReturn(Mono.just(page1)); + when(duplicateChecker.checkDuplicateInBatch(batch, ItemCategory.SWORD)) + .thenReturn(DuplicateCheckResult.equalDateFound(1, latestDate)); + + // when + FetchResult result = fetcher.fetch(ItemCategory.SWORD); + + // then + assertThat(result.items()).hasSize(1); + assertThat(result.hasEqualDate()).isTrue(); + assertThat(result.latestDate()).isEqualTo(latestDate); + } + + @Test + @DisplayName("두 번째 배치에서 중복이면 첫 배치 전체 + 중복 전까지만 반환한다") + void stopOnDuplicateAtSecondBatch() { + // given + var batch1 = List.of(dummy(), dummy()); + var batch2 = List.of(dummy(), dummy(), dummy()); + var page1 = new OpenApiAuctionRealtimeListResponse(batch1, "cursor-1"); + var page2 = new OpenApiAuctionRealtimeListResponse(batch2, "cursor-2"); + Instant latestDate = Instant.parse("2024-01-01T00:00:00Z"); + + when(client.fetchAuctionList(ItemCategory.SWORD, "")).thenReturn(Mono.just(page1)); + when(client.fetchAuctionList(ItemCategory.SWORD, "cursor-1")) + .thenReturn(Mono.just(page2)); + when(duplicateChecker.checkDuplicateInBatch(batch1, ItemCategory.SWORD)) + .thenReturn(DuplicateCheckResult.noDuplicate()); + when(duplicateChecker.checkDuplicateInBatch(batch2, ItemCategory.SWORD)) + .thenReturn(DuplicateCheckResult.duplicateFound(1, latestDate)); + + // when + FetchResult result = fetcher.fetch(ItemCategory.SWORD); + + // then + assertThat(result.items()).hasSize(3); // 2 from batch1 + 1 from batch2 + verify(client, times(2)).fetchAuctionList(eq(ItemCategory.SWORD), any()); + } + + @Test + @DisplayName("첫 응답이 null(Mono.empty)이면 빈 리스트를 반환한다") + void responseNull() { + when(client.fetchAuctionList(ItemCategory.SWORD, "")).thenReturn(Mono.empty()); + + FetchResult result = fetcher.fetch(ItemCategory.SWORD); + + assertThat(result.items()).isEmpty(); + verify(duplicateChecker, never()).checkDuplicateInBatch(any(), any()); + } + + @Test + @DisplayName("auctionItems()가 비어있으면 빈 리스트를 반환한다") + void auctionItemsEmpty() { + var page = new OpenApiAuctionRealtimeListResponse(List.of(), "ignored"); + + when(client.fetchAuctionList(ItemCategory.SWORD, "")).thenReturn(Mono.just(page)); + + FetchResult result = fetcher.fetch(ItemCategory.SWORD); + + assertThat(result.items()).isEmpty(); + verify(duplicateChecker, never()).checkDuplicateInBatch(any(), any()); + } + + @Test + @DisplayName("nextCursor가 빈 문자열이면 수집을 중단한다") + void stopWhenNextCursorIsEmptyString() { + // given + var page1 = new OpenApiAuctionRealtimeListResponse(List.of(dummy()), ""); + when(client.fetchAuctionList(ItemCategory.SWORD, "")).thenReturn(Mono.just(page1)); + when(duplicateChecker.checkDuplicateInBatch(any(), eq(ItemCategory.SWORD))) + .thenReturn(DuplicateCheckResult.noDuplicate()); + + // when + FetchResult result = fetcher.fetch(ItemCategory.SWORD); + + // then + assertThat(result.items()).hasSize(1); + verify(client, times(1)).fetchAuctionList(eq(ItemCategory.SWORD), any()); + verifyNoMoreInteractions(client); + } + + @Test + @DisplayName("중간 페이지의 auctionItems가 비어있으면 수집을 중단한다") + void stopWhenMiddlePageIsEmpty() { + // given + var page1 = new OpenApiAuctionRealtimeListResponse(List.of(dummy()), "cursor-1"); + var emptyPage = new OpenApiAuctionRealtimeListResponse(List.of(), "cursor-2"); + + when(client.fetchAuctionList(ItemCategory.SWORD, "")).thenReturn(Mono.just(page1)); + when(client.fetchAuctionList(ItemCategory.SWORD, "cursor-1")) + .thenReturn(Mono.just(emptyPage)); + when(duplicateChecker.checkDuplicateInBatch(any(), eq(ItemCategory.SWORD))) + .thenReturn(DuplicateCheckResult.noDuplicate()); + + // when + FetchResult result = fetcher.fetch(ItemCategory.SWORD); + + // then + assertThat(result.items()).hasSize(1); + verify(client, times(2)).fetchAuctionList(eq(ItemCategory.SWORD), any()); + verifyNoMoreInteractions(client); + } + + @Test + @DisplayName("응답 내 auctionItems 리스트가 null이면 수집을 중단한다") + void stopWhenAuctionItemsListIsNull() { + // given + var pageWithNullList = new OpenApiAuctionRealtimeListResponse(null, "cursor-1"); + when(client.fetchAuctionList(ItemCategory.SWORD, "")) + .thenReturn(Mono.just(pageWithNullList)); + + // when + FetchResult result = fetcher.fetch(ItemCategory.SWORD); + + // then + assertThat(result.items()).isEmpty(); + verify(client, times(1)).fetchAuctionList(eq(ItemCategory.SWORD), any()); + verifyNoMoreInteractions(client); + verify(duplicateChecker, never()).checkDuplicateInBatch(any(), any()); + } + } +} diff --git a/src/test/java/until/the/eternity/auctionrealtime/domain/service/AuctionRealtimeDuplicateCheckerTest.java b/src/test/java/until/the/eternity/auctionrealtime/domain/service/AuctionRealtimeDuplicateCheckerTest.java new file mode 100644 index 00000000..b731130b --- /dev/null +++ b/src/test/java/until/the/eternity/auctionrealtime/domain/service/AuctionRealtimeDuplicateCheckerTest.java @@ -0,0 +1,237 @@ +package until.the.eternity.auctionrealtime.domain.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +import java.time.Instant; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +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.auctionrealtime.domain.repository.AuctionRealtimeItemRepositoryPort; +import until.the.eternity.auctionrealtime.domain.service.AuctionRealtimeDuplicateChecker.DuplicateCheckResult; +import until.the.eternity.auctionrealtime.interfaces.external.dto.OpenApiAuctionRealtimeResponse; +import until.the.eternity.common.enums.ItemCategory; + +@ExtendWith(MockitoExtension.class) +class AuctionRealtimeDuplicateCheckerTest { + + @Mock AuctionRealtimeItemRepositoryPort repository; + + @InjectMocks AuctionRealtimeDuplicateChecker checker; + + private static final ItemCategory CATEGORY = ItemCategory.SWORD; + + private OpenApiAuctionRealtimeResponse dto(Instant dateAuctionExpire) { + return new OpenApiAuctionRealtimeResponse( + "페러시우스 타이탄 블레이드", "신성한 페러시우스 타이탄 블레이드", 1L, 100L, dateAuctionExpire, null); + } + + @Nested + @DisplayName("checkDuplicateInBatch 테스트") + class CheckDuplicateInBatchTest { + + @Test + @DisplayName("DB에 데이터가 없으면 중복 없음으로 판정") + void noDuplicateWhenNoDataInDb() { + // given + Instant now = Instant.now(); + var batch = List.of(dto(now), dto(now.minusSeconds(10))); + when(repository.findLatestDateAuctionExpireBySubCategory(CATEGORY)) + .thenReturn(Optional.empty()); + + // when + DuplicateCheckResult result = checker.checkDuplicateInBatch(batch, CATEGORY); + + // then + assertThat(result.isDuplicate()).isFalse(); + assertThat(result.hasEqualDate()).isFalse(); + assertThat(result.latestDate()).isNull(); + } + + @Test + @DisplayName("빈 배치이면 중복 없음으로 판정") + void noDuplicateWhenEmptyBatch() { + // given + List batch = List.of(); + + // when + DuplicateCheckResult result = checker.checkDuplicateInBatch(batch, CATEGORY); + + // then + assertThat(result.isDuplicate()).isFalse(); + assertThat(result.hasEqualDate()).isFalse(); + } + + @Test + @DisplayName("모든 데이터가 latestDate 이후면 중복 없음") + void noDuplicateWhenAllDataAfterLatestDate() { + // given + Instant latestDate = Instant.parse("2024-01-01T00:00:00Z"); + Instant afterLatest = latestDate.plusSeconds(100); + var batch = List.of(dto(afterLatest), dto(afterLatest.plusSeconds(10))); + + when(repository.findLatestDateAuctionExpireBySubCategory(CATEGORY)) + .thenReturn(Optional.of(latestDate)); + + // when + DuplicateCheckResult result = checker.checkDuplicateInBatch(batch, CATEGORY); + + // then + assertThat(result.isDuplicate()).isFalse(); + assertThat(result.latestDate()).isEqualTo(latestDate); + } + + @Test + @DisplayName("중간에 latestDate 이전 데이터가 있으면 해당 인덱스와 중복 반환") + void duplicateFoundWhenDataBeforeLatestDate() { + // given + Instant latestDate = Instant.parse("2024-01-01T00:00:00Z"); + Instant afterLatest = latestDate.plusSeconds(100); + Instant beforeLatest = latestDate.minusSeconds(100); + var batch = List.of(dto(afterLatest), dto(afterLatest), dto(beforeLatest)); + + when(repository.findLatestDateAuctionExpireBySubCategory(CATEGORY)) + .thenReturn(Optional.of(latestDate)); + + // when + DuplicateCheckResult result = checker.checkDuplicateInBatch(batch, CATEGORY); + + // then + assertThat(result.isDuplicate()).isTrue(); + assertThat(result.hasEqualDate()).isFalse(); + assertThat(result.duplicateIndex()).isEqualTo(2); + assertThat(result.latestDate()).isEqualTo(latestDate); + } + + @Test + @DisplayName("동일 날짜 데이터가 있으면 equalDateFound 반환") + void equalDateFoundWhenSameDate() { + // given + Instant latestDate = Instant.parse("2024-01-01T00:00:00Z"); + Instant afterLatest = latestDate.plusSeconds(100); + var batch = List.of(dto(afterLatest), dto(latestDate)); + + when(repository.findLatestDateAuctionExpireBySubCategory(CATEGORY)) + .thenReturn(Optional.of(latestDate)); + + // when + DuplicateCheckResult result = checker.checkDuplicateInBatch(batch, CATEGORY); + + // then + assertThat(result.isDuplicate()).isTrue(); + assertThat(result.hasEqualDate()).isTrue(); + assertThat(result.duplicateIndex()).isEqualTo(1); + assertThat(result.latestDate()).isEqualTo(latestDate); + } + + @Test + @DisplayName("첫 번째 항목이 과거면 인덱스 0 반환") + void duplicateAtFirstIndex() { + // given + Instant latestDate = Instant.parse("2024-01-01T00:00:00Z"); + Instant beforeLatest = latestDate.minusSeconds(100); + var batch = List.of(dto(beforeLatest), dto(latestDate)); + + when(repository.findLatestDateAuctionExpireBySubCategory(CATEGORY)) + .thenReturn(Optional.of(latestDate)); + + // when + DuplicateCheckResult result = checker.checkDuplicateInBatch(batch, CATEGORY); + + // then + assertThat(result.isDuplicate()).isTrue(); + assertThat(result.duplicateIndex()).isEqualTo(0); + } + } + + @Nested + @DisplayName("filterForSave 테스트") + class FilterForSaveTest { + + @Test + @DisplayName("latestDate가 null이면 모든 데이터 반환") + void returnAllWhenLatestDateNull() { + // given + Instant now = Instant.now(); + var dtos = List.of(dto(now), dto(now.minusSeconds(10))); + + // when + var result = checker.filterForSave(dtos, null); + + // then + assertThat(result).hasSize(2); + } + + @Test + @DisplayName("빈 리스트이면 빈 리스트 반환") + void returnEmptyWhenEmptyList() { + // given + List dtos = List.of(); + + // when + var result = checker.filterForSave(dtos, Instant.now()); + + // then + assertThat(result).isEmpty(); + } + + @Test + @DisplayName("latestDate 이전 데이터는 필터링됨") + void filterOutDataBeforeLatestDate() { + // given + Instant latestDate = Instant.parse("2024-01-01T00:00:00Z"); + Instant afterLatest = latestDate.plusSeconds(100); + Instant beforeLatest = latestDate.minusSeconds(100); + var dtos = List.of(dto(afterLatest), dto(beforeLatest), dto(afterLatest)); + + // when + var result = checker.filterForSave(dtos, latestDate); + + // then + assertThat(result).hasSize(2); + } + + @Test + @DisplayName("동일 날짜 데이터는 포함됨") + void includeSameDateData() { + // given + Instant latestDate = Instant.parse("2024-01-01T00:00:00Z"); + var dtos = List.of(dto(latestDate), dto(latestDate)); + + // when + var result = checker.filterForSave(dtos, latestDate); + + // then + assertThat(result).hasSize(2); + } + + @Test + @DisplayName("복합 시나리오: 이전/동일/이후 데이터") + void complexScenario() { + // given + Instant latestDate = Instant.parse("2024-01-01T00:00:00Z"); + Instant afterLatest = latestDate.plusSeconds(100); + Instant beforeLatest = latestDate.minusSeconds(100); + + var dtos = + List.of( + dto(afterLatest), // 신규: 포함 + dto(beforeLatest), // 과거: 제외 + dto(latestDate), // 동일 날짜: 포함 + dto(afterLatest) // 신규: 포함 + ); + + // when + var result = checker.filterForSave(dtos, latestDate); + + // then + assertThat(result).hasSize(3); + } + } +} diff --git a/src/test/java/until/the/eternity/auctionsearchoption/application/service/AuctionSearchOptionServiceTest.java b/src/test/java/until/the/eternity/auctionsearchoption/application/service/AuctionSearchOptionServiceTest.java index cbd42577..d92cf748 100644 --- a/src/test/java/until/the/eternity/auctionsearchoption/application/service/AuctionSearchOptionServiceTest.java +++ b/src/test/java/until/the/eternity/auctionsearchoption/application/service/AuctionSearchOptionServiceTest.java @@ -1,6 +1,11 @@ package until.the.eternity.auctionsearchoption.application.service; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.*; + import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.List; +import java.util.Map; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -12,12 +17,6 @@ import until.the.eternity.auctionsearchoption.interfaces.rest.dto.response.FieldMetadata; import until.the.eternity.auctionsearchoption.interfaces.rest.dto.response.SearchOptionMetadataResponse; -import java.util.List; -import java.util.Map; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.*; - @ExtendWith(MockitoExtension.class) class AuctionSearchOptionServiceTest { diff --git a/src/test/java/until/the/eternity/iteminfo/application/service/ItemInfoServiceTest.java b/src/test/java/until/the/eternity/iteminfo/application/service/ItemInfoServiceTest.java index 6e66a736..15a7a7d5 100644 --- a/src/test/java/until/the/eternity/iteminfo/application/service/ItemInfoServiceTest.java +++ b/src/test/java/until/the/eternity/iteminfo/application/service/ItemInfoServiceTest.java @@ -1,5 +1,11 @@ package until.the.eternity.iteminfo.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.util.ArrayList; +import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -19,13 +25,6 @@ import until.the.eternity.iteminfo.interfaces.rest.dto.response.ItemInfoSummaryResponse; import until.the.eternity.iteminfo.interfaces.rest.dto.response.ItemInfoSyncResponse; -import java.util.ArrayList; -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Mockito.*; - @ExtendWith(MockitoExtension.class) class ItemInfoServiceTest { diff --git a/src/test/java/until/the/eternity/metalwareinfo/application/service/MetalwareInfoServiceTest.java b/src/test/java/until/the/eternity/metalwareinfo/application/service/MetalwareInfoServiceTest.java index 6c56c0d6..7de499a5 100644 --- a/src/test/java/until/the/eternity/metalwareinfo/application/service/MetalwareInfoServiceTest.java +++ b/src/test/java/until/the/eternity/metalwareinfo/application/service/MetalwareInfoServiceTest.java @@ -1,5 +1,10 @@ package until.the.eternity.metalwareinfo.application.service; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -9,12 +14,6 @@ import until.the.eternity.metalwareinfo.domain.repository.MetalwareInfoRepositoryPort; import until.the.eternity.metalwareinfo.interfaces.rest.dto.response.MetalwareInfoResponse; -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - @ExtendWith(MockitoExtension.class) class MetalwareInfoServiceTest { From 372b501f1553e04e1dc8f6530ba31782495b80d9 Mon Sep 17 00:00:00 2001 From: dev-ant Date: Tue, 27 Jan 2026 12:40:46 +0900 Subject: [PATCH 2/8] =?UTF-8?q?feat:=20item=5Foption=20=EC=84=B8=EA=B3=B5?= =?UTF-8?q?=20=EC=98=B5=EC=85=98=20=ED=8C=8C=EC=8B=B1=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mapper/OpenApiAuctionHistoryMapper.java | 11 +++++ .../mapper/OpenApiItemOptionMapper.java | 43 +++++++++++++++++ .../entity/AuctionRealtimeItemOption.java | 12 +++++ .../entity/AuctionHistoryItemOption.java | 12 +++++ .../mapper/OpenApiAuctionRealtimeMapper.java | 13 ++++++ .../OpenApiRealtimeItemOptionMapper.java | 43 +++++++++++++++++ ...nown_item_name_and_segong_option_level.sql | 46 +++++++++++++++++++ 7 files changed, 180 insertions(+) create mode 100644 src/main/resources/db/migration/V16__update_unknown_item_name_and_segong_option_level.sql diff --git a/src/main/java/until/the/eternity/auctionhistory/domain/mapper/OpenApiAuctionHistoryMapper.java b/src/main/java/until/the/eternity/auctionhistory/domain/mapper/OpenApiAuctionHistoryMapper.java index 8322920b..f728374e 100644 --- a/src/main/java/until/the/eternity/auctionhistory/domain/mapper/OpenApiAuctionHistoryMapper.java +++ b/src/main/java/until/the/eternity/auctionhistory/domain/mapper/OpenApiAuctionHistoryMapper.java @@ -11,6 +11,8 @@ @Mapper(componentModel = "spring", uses = OpenApiItemOptionMapper.class) public interface OpenApiAuctionHistoryMapper { + String UNKNOWN_ITEM_NAME = "(Unknown)"; + @Named("toEntity(OpenApiAuctionHistoryResponse, ItemCategory)") @Mapping(source = "dateAuctionBuy", target = "dateAuctionBuy", qualifiedByName = "utcToKst") @Mapping(source = "openApiAuctionItemOptionResponse", target = "auctionHistoryItemOptions") @@ -19,6 +21,15 @@ public interface OpenApiAuctionHistoryMapper { expression = "java(ItemCategory.findTopCategory(dto.itemSubCategory()))") AuctionHistory toEntity(OpenApiAuctionHistoryResponse dto, @Context ItemCategory itemCategory); + @AfterMapping + default void afterMapping( + OpenApiAuctionHistoryResponse dto, @MappingTarget AuctionHistory entity) { + // item_name이 "(Unknown)"인 경우 item_display_name으로 대체 + if (UNKNOWN_ITEM_NAME.equals(entity.getItemName())) { + entity.setItemName(dto.itemDisplayName()); + } + } + @IterableMapping(qualifiedByName = "toEntity(OpenApiAuctionHistoryResponse, ItemCategory)") List toEntityList( List dtoList, @Context ItemCategory itemCategory); diff --git a/src/main/java/until/the/eternity/auctionhistory/domain/mapper/OpenApiItemOptionMapper.java b/src/main/java/until/the/eternity/auctionhistory/domain/mapper/OpenApiItemOptionMapper.java index c3fddad9..09f98a14 100644 --- a/src/main/java/until/the/eternity/auctionhistory/domain/mapper/OpenApiItemOptionMapper.java +++ b/src/main/java/until/the/eternity/auctionhistory/domain/mapper/OpenApiItemOptionMapper.java @@ -1,14 +1,57 @@ package until.the.eternity.auctionhistory.domain.mapper; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.mapstruct.AfterMapping; import org.mapstruct.Mapper; import org.mapstruct.Mapping; +import org.mapstruct.MappingTarget; import until.the.eternity.auctionitemoption.domain.dto.external.OpenApiAuctionItemOptionResponse; import until.the.eternity.auctionitemoption.domain.entity.AuctionHistoryItemOption; @Mapper(componentModel = "spring") public interface OpenApiItemOptionMapper { + String SEGONG_OPTION_TYPE = "세공 옵션"; + + // 패턴 1: "스킬명 숫자 레벨" 또는 "스킬명 숫자레벨" 형식 (예: "천옷만들기 품질 보너스 3 레벨", "지력 2레벨") + // 그룹1: 스킬명, 그룹2: "숫자 레벨" 또는 "숫자레벨" (option_desc), 그룹3: 숫자 (option_value2) + Pattern PATTERN_LEVEL_SUFFIX = Pattern.compile("^(.+?) ((\\d+) ?레벨)$"); + + // 패턴 2: "스킬명(숫자레벨:효과)" 형식 (예: "매그넘 샷 대미지(20레벨:200 % 증가)") + // 그룹1: 스킬명, 그룹2: 괄호 전체 (option_desc), 그룹3: 숫자 (option_value2) + Pattern PATTERN_LEVEL_PARENTHESIS = Pattern.compile("^(.+?)(\\((\\d+)레벨:.+\\))$"); + @Mapping(target = "id", ignore = true) // PK 자동 생성 @Mapping(target = "auctionHistory", ignore = true) AuctionHistoryItemOption toEntity(OpenApiAuctionItemOptionResponse itemOption); + + @AfterMapping + default void afterMapping( + OpenApiAuctionItemOptionResponse dto, @MappingTarget AuctionHistoryItemOption entity) { + // option_type이 "세공 옵션"인 경우에만 파싱 수행 + if (!SEGONG_OPTION_TYPE.equals(entity.getOptionType()) || entity.getOptionValue() == null) { + return; + } + + String originalValue = entity.getOptionValue(); + + // 패턴 1: "스킬명 숫자 레벨" 또는 "스킬명 숫자레벨" 형식 + Matcher matcher1 = PATTERN_LEVEL_SUFFIX.matcher(originalValue); + if (matcher1.matches()) { + entity.setOptionValue(matcher1.group(1)); + entity.setOptionValue2(matcher1.group(3)); + entity.setOptionDesc(matcher1.group(2)); + return; + } + + // 패턴 2: "스킬명(숫자레벨:효과)" 형식 + Matcher matcher2 = PATTERN_LEVEL_PARENTHESIS.matcher(originalValue); + if (matcher2.matches()) { + entity.setOptionValue(matcher2.group(1)); + entity.setOptionValue2(matcher2.group(3)); + entity.setOptionDesc(matcher2.group(2)); + } + // 두 패턴 모두 매칭되지 않으면 원본 값 유지 + } } diff --git a/src/main/java/until/the/eternity/auctionitem/domain/entity/AuctionRealtimeItemOption.java b/src/main/java/until/the/eternity/auctionitem/domain/entity/AuctionRealtimeItemOption.java index 14943d44..b0a651bf 100644 --- a/src/main/java/until/the/eternity/auctionitem/domain/entity/AuctionRealtimeItemOption.java +++ b/src/main/java/until/the/eternity/auctionitem/domain/entity/AuctionRealtimeItemOption.java @@ -60,4 +60,16 @@ public void setAuctionRealtimeItem(AuctionRealtimeItem auctionRealtimeItem) { auctionRealtimeItem.getAuctionRealtimeItemOptions().add(this); } } + + public void setOptionValue(String optionValue) { + this.optionValue = optionValue; + } + + public void setOptionValue2(String optionValue2) { + this.optionValue2 = optionValue2; + } + + public void setOptionDesc(String optionDesc) { + this.optionDesc = optionDesc; + } } diff --git a/src/main/java/until/the/eternity/auctionitemoption/domain/entity/AuctionHistoryItemOption.java b/src/main/java/until/the/eternity/auctionitemoption/domain/entity/AuctionHistoryItemOption.java index 4e326923..dddac912 100644 --- a/src/main/java/until/the/eternity/auctionitemoption/domain/entity/AuctionHistoryItemOption.java +++ b/src/main/java/until/the/eternity/auctionitemoption/domain/entity/AuctionHistoryItemOption.java @@ -67,4 +67,16 @@ public void setAuctionHistory(AuctionHistory auctionHistory) { auctionHistory.getAuctionHistoryItemOptions().add(this); } } + + public void setOptionValue(String optionValue) { + this.optionValue = optionValue; + } + + public void setOptionValue2(String optionValue2) { + this.optionValue2 = optionValue2; + } + + public void setOptionDesc(String optionDesc) { + this.optionDesc = optionDesc; + } } diff --git a/src/main/java/until/the/eternity/auctionrealtime/domain/mapper/OpenApiAuctionRealtimeMapper.java b/src/main/java/until/the/eternity/auctionrealtime/domain/mapper/OpenApiAuctionRealtimeMapper.java index 24c1ea07..5e005637 100644 --- a/src/main/java/until/the/eternity/auctionrealtime/domain/mapper/OpenApiAuctionRealtimeMapper.java +++ b/src/main/java/until/the/eternity/auctionrealtime/domain/mapper/OpenApiAuctionRealtimeMapper.java @@ -3,10 +3,12 @@ import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.List; +import org.mapstruct.AfterMapping; import org.mapstruct.Context; import org.mapstruct.IterableMapping; import org.mapstruct.Mapper; import org.mapstruct.Mapping; +import org.mapstruct.MappingTarget; import org.mapstruct.Named; import until.the.eternity.auctionitem.domain.entity.AuctionRealtimeItem; import until.the.eternity.auctionrealtime.interfaces.external.dto.OpenApiAuctionRealtimeResponse; @@ -16,6 +18,8 @@ @Mapper(componentModel = "spring", uses = OpenApiRealtimeItemOptionMapper.class) public interface OpenApiAuctionRealtimeMapper { + String UNKNOWN_ITEM_NAME = "(Unknown)"; + @Named("toEntity(OpenApiAuctionRealtimeResponse, ItemCategory)") @Mapping( source = "dateAuctionExpire", @@ -29,6 +33,15 @@ public interface OpenApiAuctionRealtimeMapper { AuctionRealtimeItem toEntity( OpenApiAuctionRealtimeResponse dto, @Context ItemCategory itemCategory); + @AfterMapping + default void afterMapping( + OpenApiAuctionRealtimeResponse dto, @MappingTarget AuctionRealtimeItem entity) { + // item_name이 "(Unknown)"인 경우 item_display_name으로 대체 + if (UNKNOWN_ITEM_NAME.equals(entity.getItemName())) { + entity.setItemName(dto.itemDisplayName()); + } + } + @IterableMapping(qualifiedByName = "toEntity(OpenApiAuctionRealtimeResponse, ItemCategory)") List toEntityList( List dtoList, @Context ItemCategory itemCategory); diff --git a/src/main/java/until/the/eternity/auctionrealtime/domain/mapper/OpenApiRealtimeItemOptionMapper.java b/src/main/java/until/the/eternity/auctionrealtime/domain/mapper/OpenApiRealtimeItemOptionMapper.java index e33cac6c..91bbb183 100644 --- a/src/main/java/until/the/eternity/auctionrealtime/domain/mapper/OpenApiRealtimeItemOptionMapper.java +++ b/src/main/java/until/the/eternity/auctionrealtime/domain/mapper/OpenApiRealtimeItemOptionMapper.java @@ -1,7 +1,11 @@ package until.the.eternity.auctionrealtime.domain.mapper; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.mapstruct.AfterMapping; import org.mapstruct.Mapper; import org.mapstruct.Mapping; +import org.mapstruct.MappingTarget; import until.the.eternity.auctionitem.domain.entity.AuctionRealtimeItemOption; import until.the.eternity.auctionitemoption.domain.dto.external.OpenApiAuctionItemOptionResponse; @@ -9,7 +13,46 @@ @Mapper(componentModel = "spring") public interface OpenApiRealtimeItemOptionMapper { + String SEGONG_OPTION_TYPE = "세공 옵션"; + + // 패턴 1: "스킬명 숫자 레벨" 또는 "스킬명 숫자레벨" 형식 (예: "천옷만들기 품질 보너스 3 레벨", "지력 2레벨") + // 그룹1: 스킬명, 그룹2: "숫자 레벨" 또는 "숫자레벨" (option_desc), 그룹3: 숫자 (option_value2) + Pattern PATTERN_LEVEL_SUFFIX = Pattern.compile("^(.+?) ((\\d+) ?레벨)$"); + + // 패턴 2: "스킬명(숫자레벨:효과)" 형식 (예: "매그넘 샷 대미지(20레벨:200 % 증가)") + // 그룹1: 스킬명, 그룹2: 괄호 전체 (option_desc), 그룹3: 숫자 (option_value2) + Pattern PATTERN_LEVEL_PARENTHESIS = Pattern.compile("^(.+?)(\\((\\d+)레벨:.+\\))$"); + @Mapping(target = "id", ignore = true) // PK 자동 생성 @Mapping(target = "auctionRealtimeItem", ignore = true) AuctionRealtimeItemOption toEntity(OpenApiAuctionItemOptionResponse itemOption); + + @AfterMapping + default void afterMapping( + OpenApiAuctionItemOptionResponse dto, @MappingTarget AuctionRealtimeItemOption entity) { + // option_type이 "세공 옵션"인 경우에만 파싱 수행 + if (!SEGONG_OPTION_TYPE.equals(entity.getOptionType()) || entity.getOptionValue() == null) { + return; + } + + String originalValue = entity.getOptionValue(); + + // 패턴 1: "스킬명 숫자 레벨" 또는 "스킬명 숫자레벨" 형식 + Matcher matcher1 = PATTERN_LEVEL_SUFFIX.matcher(originalValue); + if (matcher1.matches()) { + entity.setOptionValue(matcher1.group(1)); + entity.setOptionValue2(matcher1.group(3)); + entity.setOptionDesc(matcher1.group(2)); + return; + } + + // 패턴 2: "스킬명(숫자레벨:효과)" 형식 + Matcher matcher2 = PATTERN_LEVEL_PARENTHESIS.matcher(originalValue); + if (matcher2.matches()) { + entity.setOptionValue(matcher2.group(1)); + entity.setOptionValue2(matcher2.group(3)); + entity.setOptionDesc(matcher2.group(2)); + } + // 두 패턴 모두 매칭되지 않으면 원본 값 유지 + } } diff --git a/src/main/resources/db/migration/V16__update_unknown_item_name_and_segong_option_level.sql b/src/main/resources/db/migration/V16__update_unknown_item_name_and_segong_option_level.sql new file mode 100644 index 00000000..c26f1ebc --- /dev/null +++ b/src/main/resources/db/migration/V16__update_unknown_item_name_and_segong_option_level.sql @@ -0,0 +1,46 @@ +-- V16: item_name이 '(Unknown)'인 경우 item_display_name으로 업데이트, +-- 세공 옵션 파싱 (option_value, option_value2, option_desc) + +-- 1. auction_history 테이블: item_name이 '(Unknown)'인 경우 item_display_name으로 업데이트 +UPDATE auction_history +SET item_name = item_display_name +WHERE item_name = '(Unknown)'; + +-- 2. auction_realtime_item 테이블: item_name이 '(Unknown)'인 경우 item_display_name으로 업데이트 +UPDATE auction_realtime_item +SET item_name = item_display_name +WHERE item_name = '(Unknown)'; + +-- 3. auction_history_item_option 테이블: 세공 옵션 파싱 +-- 패턴 1: "스킬명 숫자 레벨" 또는 "스킬명 숫자레벨" 형식 (예: "천옷만들기 품질 보너스 3 레벨", "지력 2레벨") +UPDATE auction_history_item_option +SET option_desc = REGEXP_SUBSTR(option_value, '[0-9]+ ?레벨$'), + option_value2 = REGEXP_SUBSTR(option_value, '[0-9]+(?= ?레벨$)'), + option_value = REGEXP_REPLACE(option_value, ' [0-9]+ ?레벨$', '') +WHERE option_type = '세공 옵션' + AND option_value REGEXP '^.+ [0-9]+ ?레벨$'; + +-- 패턴 2: "스킬명(숫자레벨:효과)" 형식 (예: "매그넘 샷 대미지(20레벨:200 % 증가)") +UPDATE auction_history_item_option +SET option_desc = REGEXP_SUBSTR(option_value, '\\([0-9]+레벨:.+\\)$'), + option_value2 = REGEXP_SUBSTR(option_value, '[0-9]+(?=레벨:)'), + option_value = REGEXP_REPLACE(option_value, '\\([0-9]+레벨:.+\\)$', '') +WHERE option_type = '세공 옵션' + AND option_value REGEXP '^.+\\([0-9]+레벨:.+\\)$'; + +-- 4. auction_realtime_item_option 테이블: 세공 옵션 파싱 +-- 패턴 1: "스킬명 숫자 레벨" 또는 "스킬명 숫자레벨" 형식 (예: "천옷만들기 품질 보너스 3 레벨", "지력 2레벨") +UPDATE auction_realtime_item_option +SET option_desc = REGEXP_SUBSTR(option_value, '[0-9]+ ?레벨$'), + option_value2 = REGEXP_SUBSTR(option_value, '[0-9]+(?= ?레벨$)'), + option_value = REGEXP_REPLACE(option_value, ' [0-9]+ ?레벨$', '') +WHERE option_type = '세공 옵션' + AND option_value REGEXP '^.+ [0-9]+ ?레벨$'; + +-- 패턴 2: "스킬명(숫자레벨:효과)" 형식 (예: "매그넘 샷 대미지(20레벨:200 % 증가)") +UPDATE auction_realtime_item_option +SET option_desc = REGEXP_SUBSTR(option_value, '\\([0-9]+레벨:.+\\)$'), + option_value2 = REGEXP_SUBSTR(option_value, '[0-9]+(?=레벨:)'), + option_value = REGEXP_REPLACE(option_value, '\\([0-9]+레벨:.+\\)$', '') +WHERE option_type = '세공 옵션' + AND option_value REGEXP '^.+\\([0-9]+레벨:.+\\)$'; From c5381d69f10804c8cd50705e694474edcd154554 Mon Sep 17 00:00:00 2001 From: dev-ant Date: Wed, 28 Jan 2026 22:13:22 +0900 Subject: [PATCH 3/8] =?UTF-8?q?feat:=20horn=20bugle=20eleastic=20search=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 3 + docker-compose-local.yml | 45 +- .../service/AuctionRealtimeService.java | 43 ++ .../domain/mapper/AuctionRealtimeMapper.java | 21 + .../AuctionRealtimeItemRepositoryPort.java | 20 + ...AuctionRealtimeItemRepositoryPortImpl.java | 15 + .../AuctionRealtimeQueryDslRepository.java | 422 ++++++++++++++++++ .../controller/AuctionRealtimeController.java | 46 ++ .../request/AuctionRealtimeSearchRequest.java | 14 + .../dto/request/RealtimePageRequestDto.java | 39 ++ .../rest/dto/request/RealtimeSortField.java | 43 ++ .../AuctionRealtimeDetailResponse.java | 17 + .../response/RealtimeItemOptionResponse.java | 9 + .../runner/HornBugleIndexRunner.java | 88 ++++ .../application/service/HornBugleService.java | 91 +++- .../domain/mapper/HornBugleMapper.java | 4 + .../repository/HornBugleRepositoryPort.java | 7 + .../elasticsearch/HornBugleDocument.java | 50 +++ .../HornBugleElasticsearchConfig.java | 12 + .../HornBugleElasticsearchRepository.java | 6 + .../elasticsearch/HornBugleIndexService.java | 182 ++++++++ .../persistence/HornBugleJpaRepository.java | 35 ++ .../HornBugleRepositoryPortImpl.java | 11 + .../rest/controller/HornBugleController.java | 16 +- src/main/resources/application.yml | 13 +- ...text_index_to_horn_bugle_world_history.sql | 8 + .../elasticsearch/horn-bugle-settings.json | 9 + .../runner/HornBugleIndexRunnerTest.java | 97 ++++ .../service/HornBugleServiceTest.java | 335 ++++++++++++++ .../HornBugleIndexServiceTest.java | 237 ++++++++++ 30 files changed, 1931 insertions(+), 7 deletions(-) create mode 100644 src/main/java/until/the/eternity/auctionrealtime/domain/mapper/AuctionRealtimeMapper.java create mode 100644 src/main/java/until/the/eternity/auctionrealtime/infrastructure/persistence/AuctionRealtimeQueryDslRepository.java create mode 100644 src/main/java/until/the/eternity/auctionrealtime/interfaces/rest/controller/AuctionRealtimeController.java create mode 100644 src/main/java/until/the/eternity/auctionrealtime/interfaces/rest/dto/request/AuctionRealtimeSearchRequest.java create mode 100644 src/main/java/until/the/eternity/auctionrealtime/interfaces/rest/dto/request/RealtimePageRequestDto.java create mode 100644 src/main/java/until/the/eternity/auctionrealtime/interfaces/rest/dto/request/RealtimeSortField.java create mode 100644 src/main/java/until/the/eternity/auctionrealtime/interfaces/rest/dto/response/AuctionRealtimeDetailResponse.java create mode 100644 src/main/java/until/the/eternity/auctionrealtime/interfaces/rest/dto/response/RealtimeItemOptionResponse.java create mode 100644 src/main/java/until/the/eternity/hornBugle/application/runner/HornBugleIndexRunner.java create mode 100644 src/main/java/until/the/eternity/hornBugle/infrastructure/elasticsearch/HornBugleDocument.java create mode 100644 src/main/java/until/the/eternity/hornBugle/infrastructure/elasticsearch/HornBugleElasticsearchConfig.java create mode 100644 src/main/java/until/the/eternity/hornBugle/infrastructure/elasticsearch/HornBugleElasticsearchRepository.java create mode 100644 src/main/java/until/the/eternity/hornBugle/infrastructure/elasticsearch/HornBugleIndexService.java create mode 100644 src/main/resources/db/migration/V17__add_fulltext_index_to_horn_bugle_world_history.sql create mode 100644 src/main/resources/elasticsearch/horn-bugle-settings.json create mode 100644 src/test/java/until/the/eternity/hornBugle/application/runner/HornBugleIndexRunnerTest.java create mode 100644 src/test/java/until/the/eternity/hornBugle/application/service/HornBugleServiceTest.java create mode 100644 src/test/java/until/the/eternity/hornBugle/infrastructure/elasticsearch/HornBugleIndexServiceTest.java diff --git a/build.gradle.kts b/build.gradle.kts index 14249adc..573cde83 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -64,6 +64,9 @@ dependencies { // P6Spy implementation("com.github.gavlyukovskiy:p6spy-spring-boot-starter:${property("p6spyVersion")}") + // Elasticsearch + implementation("org.springframework.boot:spring-boot-starter-data-elasticsearch") + // QueryDSL (with Jakarta API) implementation("com.querydsl:querydsl-core:5.1.0") implementation("com.querydsl:querydsl-jpa:5.1.0:jakarta") diff --git a/docker-compose-local.yml b/docker-compose-local.yml index a79cadb9..e2289713 100644 --- a/docker-compose-local.yml +++ b/docker-compose-local.yml @@ -47,6 +47,11 @@ services: AUCTION_HISTORY_CRON: "${AUCTION_HISTORY_CRON:-0 0 * * * *}" STATISTICS_PREVIOUS_DAY_CRON: "${STATISTICS_PREVIOUS_DAY_CRON:-0 0 * * * *}" + # === Elasticsearch Configuration === + ELASTICSEARCH_ENABLED: ${ELASTICSEARCH_ENABLED:-true} + ELASTICSEARCH_INDEX_ENABLED: ${ELASTICSEARCH_INDEX_ENABLED:-true} + SPRING_ELASTICSEARCH_URIS: http://elasticsearch:9200 + # === JVM Configuration (Local - 경량 개발용) === # Heap: 256m~512m, Non-Heap: 256m, Total: ~768m JAVA_OPTS: >- @@ -85,10 +90,12 @@ services: - app-network - my-network # MySQL 컨테이너와 통신을 위해 추가 - # MySQL이 준비될 때까지 대기 + # MySQL, Elasticsearch가 준비될 때까지 대기 depends_on: mysql: condition: service_healthy + elasticsearch: + condition: service_healthy # === Health Check (Local - 표준) === healthcheck: @@ -144,6 +151,41 @@ services: - --default-time-zone=+09:00 # MySQL 레벨 타임존 설정 - --explicit_defaults_for_timestamp=1 # TIMESTAMP 기본값 명시 허용 + # Elasticsearch + # Nori 플러그인 사용 시: build 섹션 주석 해제, image 주석 처리 + # build: + # context: ./docker/elasticsearch + # dockerfile: Dockerfile + # image: open-api-batch-elasticsearch:local + elasticsearch: + image: docker.elastic.co/elasticsearch/elasticsearch:8.11.0 + container_name: open-api-batch-elasticsearch + restart: unless-stopped + environment: + - discovery.type=single-node + - xpack.security.enabled=false + - "ES_JAVA_OPTS=-Xms512m -Xmx512m" + - cluster.name=devnogi-es-cluster + ports: + - "${ELASTICSEARCH_PORT:-9200}:9200" + - "${ELASTICSEARCH_TRANSPORT_PORT:-9300}:9300" + volumes: + - elasticsearch_data:/usr/share/elasticsearch/data + networks: + - app-network + healthcheck: + test: ["CMD-SHELL", "curl -s http://localhost:9200/_cluster/health | grep -q '\"status\":\"green\"\\|\"status\":\"yellow\"'"] + interval: 10s + timeout: 10s + retries: 10 + start_period: 60s + deploy: + resources: + limits: + memory: 1g + reservations: + memory: 512m + # === Autoheal (Local - 표준) === # unhealthy 컨테이너 자동 재시작 서비스 autoheal: @@ -168,6 +210,7 @@ services: volumes: mysql_data: + elasticsearch_data: app-logs: driver: local diff --git a/src/main/java/until/the/eternity/auctionrealtime/application/service/AuctionRealtimeService.java b/src/main/java/until/the/eternity/auctionrealtime/application/service/AuctionRealtimeService.java index 466fe6a4..8aacbe22 100644 --- a/src/main/java/until/the/eternity/auctionrealtime/application/service/AuctionRealtimeService.java +++ b/src/main/java/until/the/eternity/auctionrealtime/application/service/AuctionRealtimeService.java @@ -4,11 +4,18 @@ import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import until.the.eternity.auctionitem.domain.entity.AuctionRealtimeItem; +import until.the.eternity.auctionrealtime.domain.mapper.AuctionRealtimeMapper; import until.the.eternity.auctionrealtime.domain.repository.AuctionRealtimeItemRepositoryPort; +import until.the.eternity.auctionrealtime.interfaces.rest.dto.request.AuctionRealtimeSearchRequest; +import until.the.eternity.auctionrealtime.interfaces.rest.dto.response.AuctionRealtimeDetailResponse; +import until.the.eternity.auctionrealtime.interfaces.rest.dto.response.RealtimeItemOptionResponse; import until.the.eternity.common.enums.ItemCategory; +import until.the.eternity.common.response.PageResponseDto; /** 실시간 경매장 데이터 Service. */ @Slf4j @@ -17,6 +24,42 @@ public class AuctionRealtimeService { private final AuctionRealtimeItemRepositoryPort repository; + private final AuctionRealtimeMapper mapper; + + /** + * 실시간 경매장 아이템을 검색한다. + * + * @param requestDto 검색 조건 + * @param pageable 페이지 정보 + * @return 검색 결과 + */ + @Transactional(readOnly = true) + public PageResponseDto> search( + AuctionRealtimeSearchRequest requestDto, Pageable pageable) { + + Page page = repository.search(requestDto, pageable); + Page> dtoPage = + page.map(mapper::toDto); + return PageResponseDto.of(dtoPage); + } + + /** + * ID로 실시간 경매장 아이템을 조회한다. + * + * @param id 아이템 ID + * @return 아이템 상세 정보 + */ + @Transactional(readOnly = true) + public AuctionRealtimeDetailResponse findByIdOrElseThrow(Long id) { + AuctionRealtimeItem item = + repository + .findById(id) + .orElseThrow( + () -> + new IllegalArgumentException( + "AuctionRealtimeItem not found: " + id)); + return mapper.toDto(item); + } /** * 해당 카테고리 & 동일 date_auction_expire 레코드 삭제 후 새 엔티티들을 저장한다. diff --git a/src/main/java/until/the/eternity/auctionrealtime/domain/mapper/AuctionRealtimeMapper.java b/src/main/java/until/the/eternity/auctionrealtime/domain/mapper/AuctionRealtimeMapper.java new file mode 100644 index 00000000..e373ae9c --- /dev/null +++ b/src/main/java/until/the/eternity/auctionrealtime/domain/mapper/AuctionRealtimeMapper.java @@ -0,0 +1,21 @@ +package until.the.eternity.auctionrealtime.domain.mapper; + +import java.util.List; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import until.the.eternity.auctionitem.domain.entity.AuctionRealtimeItem; +import until.the.eternity.auctionitem.domain.entity.AuctionRealtimeItemOption; +import until.the.eternity.auctionrealtime.interfaces.rest.dto.response.AuctionRealtimeDetailResponse; +import until.the.eternity.auctionrealtime.interfaces.rest.dto.response.RealtimeItemOptionResponse; + +/** AuctionRealtimeItem Entity to DTO mapper class. */ +@Mapper(componentModel = "spring") +public interface AuctionRealtimeMapper { + + @Mapping(target = "itemOptions", source = "auctionRealtimeItemOptions") + AuctionRealtimeDetailResponse toDto(AuctionRealtimeItem entity); + + RealtimeItemOptionResponse toDto(AuctionRealtimeItemOption entity); + + List toDtoList(List entityList); +} diff --git a/src/main/java/until/the/eternity/auctionrealtime/domain/repository/AuctionRealtimeItemRepositoryPort.java b/src/main/java/until/the/eternity/auctionrealtime/domain/repository/AuctionRealtimeItemRepositoryPort.java index 72b9b129..56513424 100644 --- a/src/main/java/until/the/eternity/auctionrealtime/domain/repository/AuctionRealtimeItemRepositoryPort.java +++ b/src/main/java/until/the/eternity/auctionrealtime/domain/repository/AuctionRealtimeItemRepositoryPort.java @@ -3,12 +3,32 @@ import java.time.Instant; import java.util.List; import java.util.Optional; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import until.the.eternity.auctionitem.domain.entity.AuctionRealtimeItem; +import until.the.eternity.auctionrealtime.interfaces.rest.dto.request.AuctionRealtimeSearchRequest; import until.the.eternity.common.enums.ItemCategory; /** AuctionRealtimeItem Repository Port (Hexagonal Architecture). */ public interface AuctionRealtimeItemRepositoryPort { + /** + * 실시간 경매장 아이템을 검색한다. + * + * @param condition 검색 조건 + * @param pageable 페이지 정보 + * @return 검색 결과 + */ + Page search(AuctionRealtimeSearchRequest condition, Pageable pageable); + + /** + * ID로 실시간 경매장 아이템을 조회한다. + * + * @param id 아이템 ID + * @return 아이템 (없으면 Optional.empty()) + */ + Optional findById(Long id); + /** * 해당 subcategory의 최신 date_auction_expire를 조회한다. * diff --git a/src/main/java/until/the/eternity/auctionrealtime/infrastructure/persistence/AuctionRealtimeItemRepositoryPortImpl.java b/src/main/java/until/the/eternity/auctionrealtime/infrastructure/persistence/AuctionRealtimeItemRepositoryPortImpl.java index e1665c7f..4e9fca60 100644 --- a/src/main/java/until/the/eternity/auctionrealtime/infrastructure/persistence/AuctionRealtimeItemRepositoryPortImpl.java +++ b/src/main/java/until/the/eternity/auctionrealtime/infrastructure/persistence/AuctionRealtimeItemRepositoryPortImpl.java @@ -6,9 +6,12 @@ import java.util.Optional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Repository; import until.the.eternity.auctionitem.domain.entity.AuctionRealtimeItem; import until.the.eternity.auctionrealtime.domain.repository.AuctionRealtimeItemRepositoryPort; +import until.the.eternity.auctionrealtime.interfaces.rest.dto.request.AuctionRealtimeSearchRequest; import until.the.eternity.common.enums.ItemCategory; /** AuctionRealtimeItemRepositoryPort 구현체. */ @@ -20,8 +23,20 @@ public class AuctionRealtimeItemRepositoryPortImpl implements AuctionRealtimeIte private static final int BATCH_SIZE = 500; private final AuctionRealtimeItemRepository jpaRepository; + private final AuctionRealtimeQueryDslRepository queryDslRepository; private final EntityManager entityManager; + @Override + public Page search( + AuctionRealtimeSearchRequest condition, Pageable pageable) { + return queryDslRepository.search(condition, pageable); + } + + @Override + public Optional findById(Long id) { + return jpaRepository.findById(id); + } + @Override public Optional findLatestDateAuctionExpireBySubCategory(ItemCategory category) { return jpaRepository.findLatestDateAuctionExpireBySubCategory(category.getSubCategory()); diff --git a/src/main/java/until/the/eternity/auctionrealtime/infrastructure/persistence/AuctionRealtimeQueryDslRepository.java b/src/main/java/until/the/eternity/auctionrealtime/infrastructure/persistence/AuctionRealtimeQueryDslRepository.java new file mode 100644 index 00000000..1e136f8a --- /dev/null +++ b/src/main/java/until/the/eternity/auctionrealtime/infrastructure/persistence/AuctionRealtimeQueryDslRepository.java @@ -0,0 +1,422 @@ +package until.the.eternity.auctionrealtime.infrastructure.persistence; + +import com.querydsl.core.BooleanBuilder; +import com.querydsl.core.types.Order; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.core.types.dsl.NumberTemplate; +import com.querydsl.jpa.JPAExpressions; +import com.querydsl.jpa.impl.JPAQueryFactory; +import java.util.ArrayList; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Component; +import until.the.eternity.auctionhistory.interfaces.rest.dto.enums.SearchStandard; +import until.the.eternity.auctionhistory.interfaces.rest.dto.request.ItemOptionSearchRequest; +import until.the.eternity.auctionhistory.interfaces.rest.dto.request.PriceSearchRequest; +import until.the.eternity.auctionitem.domain.entity.AuctionRealtimeItem; +import until.the.eternity.auctionitem.domain.entity.QAuctionRealtimeItem; +import until.the.eternity.auctionitem.domain.entity.QAuctionRealtimeItemOption; +import until.the.eternity.auctionrealtime.interfaces.rest.dto.request.AuctionRealtimeSearchRequest; + +@Component +@RequiredArgsConstructor +class AuctionRealtimeQueryDslRepository { + + private final JPAQueryFactory queryFactory; + + /** 옵션 조건 빌드 결과 (조건 BooleanBuilder + 추가된 조건 개수) */ + record OptionConditionResult(BooleanBuilder builder, int count) {} + + /** 실시간 경매장 검색 (옵션 조건 포함) */ + public Page search( + AuctionRealtimeSearchRequest condition, Pageable pageable) { + QAuctionRealtimeItem ar = QAuctionRealtimeItem.auctionRealtimeItem; + QAuctionRealtimeItemOption aro = QAuctionRealtimeItemOption.auctionRealtimeItemOption; + + // 1단계: 기본 조건 빌드 + BooleanBuilder itemBuilder = buildItemPredicate(condition, ar); + + // 2단계: 옵션 조건이 있으면 서브쿼리 추가 + if (condition.itemOptionSearchRequest() != null) { + QAuctionRealtimeItemOption subOption = new QAuctionRealtimeItemOption("subOption"); + OptionConditionResult optionResult = + buildItemOptionConditions(condition.itemOptionSearchRequest(), subOption); + + if (optionResult.builder().hasValue() && optionResult.count() > 0) { + var subQuery = + JPAExpressions.select(subOption.auctionRealtimeItem.id) + .from(subOption) + .where(optionResult.builder()) + .groupBy(subOption.auctionRealtimeItem.id) + .having(subOption.count().eq((long) optionResult.count())); + + itemBuilder.and(ar.id.in(subQuery)); + } + } + + // 3단계: 정렬 조건 빌드 + List> orderSpecifiers = buildOrderSpecifiers(pageable, ar); + + // 4단계: Deferred Join 패턴 적용 + // 4-1단계: ID만 먼저 조회 + List ids = + queryFactory + .select(ar.id) + .from(ar) + .where(itemBuilder) + .orderBy(orderSpecifiers.toArray(new OrderSpecifier[0])) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + if (ids.isEmpty()) { + return new PageImpl<>(List.of(), pageable, 0L); + } + + // 4-2단계: ID로 상세 조회 + List content = + queryFactory + .selectFrom(ar) + .leftJoin(ar.auctionRealtimeItemOptions, aro) + .fetchJoin() + .where(ar.id.in(ids)) + .orderBy(orderSpecifiers.toArray(new OrderSpecifier[0])) + .distinct() + .fetch(); + + // Count 쿼리 + Long total = queryFactory.select(ar.countDistinct()).from(ar).where(itemBuilder).fetchOne(); + + return new PageImpl<>(content, pageable, total == null ? 0L : total); + } + + /** 기본 조건 빌드 (카테고리, 아이템명, 가격) */ + private BooleanBuilder buildItemPredicate( + AuctionRealtimeSearchRequest c, QAuctionRealtimeItem ar) { + BooleanBuilder builder = new BooleanBuilder(); + + if (c.itemTopCategory() != null && !c.itemTopCategory().isBlank()) { + builder.and(ar.itemTopCategory.eq(c.itemTopCategory())); + } + if (c.itemSubCategory() != null && !c.itemSubCategory().isBlank()) { + builder.and(ar.itemSubCategory.eq(c.itemSubCategory())); + } + if (c.itemName() != null && !c.itemName().isBlank()) { + builder.and(ar.itemName.containsIgnoreCase(c.itemName())); + } + + if (c.priceSearchRequest() != null) { + PriceSearchRequest price = c.priceSearchRequest(); + if (price.priceFrom() != null) { + builder.and(ar.auctionPricePerUnit.goe(price.priceFrom())); + } + if (price.priceTo() != null) { + builder.and(ar.auctionPricePerUnit.loe(price.priceTo())); + } + } + + return builder; + } + + /** 옵션 검색 조건 빌드 (서브쿼리용) */ + private OptionConditionResult buildItemOptionConditions( + ItemOptionSearchRequest opt, QAuctionRealtimeItemOption aro) { + BooleanBuilder builder = new BooleanBuilder(); + int conditionCount = 0; + boolean ergConditionAdded = false; + + // 1. Balance (밸런스) + if (opt.balanceSearch() != null && opt.balanceSearch().balance() != null) { + builder.or( + buildOptionCondition( + aro, + "밸런스", + opt.balanceSearch().balance(), + opt.balanceSearch().balanceStandard())); + conditionCount++; + } + + // 2. Critical (크리티컬) + if (opt.criticalSearch() != null && opt.criticalSearch().critical() != null) { + builder.or( + buildOptionCondition( + aro, + "크리티컬", + opt.criticalSearch().critical(), + opt.criticalSearch().criticalStandard())); + conditionCount++; + } + + // 3. Defense (방어력) + if (opt.defenseSearch() != null && opt.defenseSearch().defense() != null) { + builder.or( + buildOptionCondition( + aro, + "방어력", + opt.defenseSearch().defense(), + opt.defenseSearch().defenseStandard())); + conditionCount++; + } + + // 4. Erg (에르그) - 범위 검색 + if (opt.ergSearch() != null) { + BooleanExpression ergTypeCondition = aro.optionType.eq("에르그"); + BooleanExpression ergValueCondition = null; + + if (opt.ergSearch().ergFrom() != null && opt.ergSearch().ergTo() != null) { + ergValueCondition = + castOptionValueToInt(aro) + .between(opt.ergSearch().ergFrom(), opt.ergSearch().ergTo()); + } else if (opt.ergSearch().ergFrom() != null) { + ergValueCondition = castOptionValueToInt(aro).goe(opt.ergSearch().ergFrom()); + } else if (opt.ergSearch().ergTo() != null) { + ergValueCondition = castOptionValueToInt(aro).loe(opt.ergSearch().ergTo()); + } + + if (ergValueCondition != null) { + BooleanExpression combined = ergTypeCondition.and(ergValueCondition); + builder.or(Expressions.booleanTemplate("({0})", combined)); + if (!ergConditionAdded) { + conditionCount++; + ergConditionAdded = true; + } + } + } + + // 5. ErgRank (에르그 등급) + if (opt.ergRankSearch() != null && opt.ergRankSearch().ergRank() != null) { + BooleanExpression combined = + aro.optionType.eq("에르그").and(aro.optionValue.eq(opt.ergRankSearch().ergRank())); + builder.or(Expressions.booleanTemplate("({0})", combined)); + if (!ergConditionAdded) { + conditionCount++; + ergConditionAdded = true; + } + } + + // 6. MagicDefense (마법 방어력) + if (opt.magicDefenseSearch() != null && opt.magicDefenseSearch().magicDefense() != null) { + builder.or( + buildOptionCondition( + aro, + "마법 방어력", + opt.magicDefenseSearch().magicDefense(), + opt.magicDefenseSearch().magicDefenseStandard())); + conditionCount++; + } + + // 7. MagicProtect (마법 보호) + if (opt.magicProtectSearch() != null && opt.magicProtectSearch().magicProtect() != null) { + builder.or( + buildOptionCondition( + aro, + "마법 보호", + opt.magicProtectSearch().magicProtect(), + opt.magicProtectSearch().magicProtectStandard())); + conditionCount++; + } + + // 8. MaxAttack (공격) - 범위 검색 + if (opt.maxAttackSearch() != null) { + BooleanExpression attackTypeCondition = aro.optionType.eq("공격"); + BooleanExpression attackValueCondition = null; + + if (opt.maxAttackSearch().maxAttackFrom() != null + && opt.maxAttackSearch().maxAttackTo() != null) { + attackValueCondition = + castOptionValueToInt(aro) + .between( + opt.maxAttackSearch().maxAttackFrom(), + opt.maxAttackSearch().maxAttackTo()); + } else if (opt.maxAttackSearch().maxAttackFrom() != null) { + attackValueCondition = + castOptionValueToInt(aro).goe(opt.maxAttackSearch().maxAttackFrom()); + } else if (opt.maxAttackSearch().maxAttackTo() != null) { + attackValueCondition = + castOptionValueToInt(aro).loe(opt.maxAttackSearch().maxAttackTo()); + } + + if (attackValueCondition != null) { + BooleanExpression combined = attackTypeCondition.and(attackValueCondition); + builder.or(Expressions.booleanTemplate("({0})", combined)); + conditionCount++; + } + } + + // 9. MaximumDurability (내구력) + if (opt.maximumDurabilitySearch() != null + && opt.maximumDurabilitySearch().maximumDurability() != null) { + builder.or( + buildOptionCondition( + aro, + "내구력", + opt.maximumDurabilitySearch().maximumDurability(), + opt.maximumDurabilitySearch().maximumDurabilityStandard())); + conditionCount++; + } + + // 10. MaxInjuryRate (부상률) - 범위 검색 + if (opt.maxInjuryRateSearch() != null) { + BooleanExpression injuryTypeCondition = aro.optionType.eq("부상률"); + BooleanExpression injuryValueCondition = null; + + if (opt.maxInjuryRateSearch().maxInjuryRateFrom() != null + && opt.maxInjuryRateSearch().maxInjuryRateTo() != null) { + injuryValueCondition = + castOptionValueToInt(aro) + .between( + opt.maxInjuryRateSearch().maxInjuryRateFrom(), + opt.maxInjuryRateSearch().maxInjuryRateTo()); + } else if (opt.maxInjuryRateSearch().maxInjuryRateFrom() != null) { + injuryValueCondition = + castOptionValueToInt(aro) + .goe(opt.maxInjuryRateSearch().maxInjuryRateFrom()); + } else if (opt.maxInjuryRateSearch().maxInjuryRateTo() != null) { + injuryValueCondition = + castOptionValueToInt(aro).loe(opt.maxInjuryRateSearch().maxInjuryRateTo()); + } + + if (injuryValueCondition != null) { + BooleanExpression combined = injuryTypeCondition.and(injuryValueCondition); + builder.or(Expressions.booleanTemplate("({0})", combined)); + conditionCount++; + } + } + + // 11. Proficiency (숙련) + if (opt.proficiencySearch() != null && opt.proficiencySearch().proficiency() != null) { + builder.or( + buildOptionCondition( + aro, + "숙련", + opt.proficiencySearch().proficiency(), + opt.proficiencySearch().proficiencyStandard())); + conditionCount++; + } + + // 12. Protect (보호) + if (opt.protectSearch() != null && opt.protectSearch().protect() != null) { + builder.or( + buildOptionCondition( + aro, + "보호", + opt.protectSearch().protect(), + opt.protectSearch().protectStandard())); + conditionCount++; + } + + // 13. RemainingTransactionCount (남은 거래 횟수) + if (opt.remainingTransactionCountSearch() != null + && opt.remainingTransactionCountSearch().remainingTransactionCount() != null) { + builder.or( + buildOptionCondition( + aro, + "남은 거래 횟수", + opt.remainingTransactionCountSearch().remainingTransactionCount(), + opt.remainingTransactionCountSearch() + .remainingTransactionCountStandard())); + conditionCount++; + } + + // 14. RemainingUnsealCount (남은 전용 해제 가능 횟수) + if (opt.remainingUnsealCountSearch() != null + && opt.remainingUnsealCountSearch().remainingUnsealCount() != null) { + builder.or( + buildOptionCondition( + aro, + "남은 전용 해제 가능 횟수", + opt.remainingUnsealCountSearch().remainingUnsealCount(), + opt.remainingUnsealCountSearch().remainingUnsealCountStandard())); + conditionCount++; + } + + // 15. RemainingUseCount (남은 사용 횟수) + if (opt.remainingUseCountSearch() != null + && opt.remainingUseCountSearch().remainingUseCount() != null) { + builder.or( + buildOptionCondition( + aro, + "남은 사용 횟수", + opt.remainingUseCountSearch().remainingUseCount(), + opt.remainingUseCountSearch().remainingUseCountStandard())); + conditionCount++; + } + + // 16. WearingRestrictions (착용 제한) + if (opt.wearingRestrictionsSearch() != null + && opt.wearingRestrictionsSearch().wearingRestrictions() != null) { + BooleanExpression condition = + aro.optionValue.contains(opt.wearingRestrictionsSearch().wearingRestrictions()); + builder.or(Expressions.booleanTemplate("({0})", condition)); + conditionCount++; + } + + return new OptionConditionResult(builder, conditionCount); + } + + /** 옵션 조건 빌드 헬퍼 */ + private BooleanExpression buildOptionCondition( + QAuctionRealtimeItemOption aro, + String optionType, + Integer value, + SearchStandard standard) { + BooleanExpression optionTypeCondition = aro.optionType.eq(optionType); + NumberTemplate numValue = castOptionValueToInt(aro); + + BooleanExpression valueCondition; + if (standard == null || standard.isEqual()) { + valueCondition = numValue.eq(value); + } else if (standard.isUp()) { + valueCondition = numValue.goe(value); + } else if (standard.isDown()) { + valueCondition = numValue.loe(value); + } else { + valueCondition = numValue.eq(value); + } + + BooleanExpression combined = optionTypeCondition.and(valueCondition); + return Expressions.booleanTemplate("({0})", combined); + } + + /** option_value2 또는 option_value를 Integer로 변환 */ + private NumberTemplate castOptionValueToInt(QAuctionRealtimeItemOption aro) { + return Expressions.numberTemplate( + Integer.class, "COALESCE({0}, {1}, 0)", aro.optionValue2, aro.optionValue); + } + + /** Pageable의 Sort를 QueryDSL OrderSpecifier로 변환 */ + private List> buildOrderSpecifiers( + Pageable pageable, QAuctionRealtimeItem ar) { + List> orders = new ArrayList<>(); + + if (pageable.getSort().isSorted()) { + for (Sort.Order order : pageable.getSort()) { + Order direction = order.isAscending() ? Order.ASC : Order.DESC; + String property = order.getProperty(); + + OrderSpecifier orderSpecifier = + switch (property) { + case "dateAuctionExpire" -> + new OrderSpecifier<>(direction, ar.dateAuctionExpire); + case "dateRegister" -> new OrderSpecifier<>(direction, ar.dateRegister); + case "auctionPricePerUnit" -> + new OrderSpecifier<>(direction, ar.auctionPricePerUnit); + case "itemName" -> new OrderSpecifier<>(direction, ar.itemName); + default -> new OrderSpecifier<>(Order.ASC, ar.dateAuctionExpire); + }; + + orders.add(orderSpecifier); + } + } else { + orders.add(new OrderSpecifier<>(Order.ASC, ar.dateAuctionExpire)); + } + + return orders; + } +} diff --git a/src/main/java/until/the/eternity/auctionrealtime/interfaces/rest/controller/AuctionRealtimeController.java b/src/main/java/until/the/eternity/auctionrealtime/interfaces/rest/controller/AuctionRealtimeController.java new file mode 100644 index 00000000..3742f819 --- /dev/null +++ b/src/main/java/until/the/eternity/auctionrealtime/interfaces/rest/controller/AuctionRealtimeController.java @@ -0,0 +1,46 @@ +package until.the.eternity.auctionrealtime.interfaces.rest.controller; + +import io.swagger.v3.oas.annotations.Operation; +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; +import org.springframework.web.bind.annotation.*; +import until.the.eternity.auctionrealtime.application.service.AuctionRealtimeService; +import until.the.eternity.auctionrealtime.interfaces.rest.dto.request.AuctionRealtimeSearchRequest; +import until.the.eternity.auctionrealtime.interfaces.rest.dto.request.RealtimePageRequestDto; +import until.the.eternity.auctionrealtime.interfaces.rest.dto.response.AuctionRealtimeDetailResponse; +import until.the.eternity.auctionrealtime.interfaces.rest.dto.response.RealtimeItemOptionResponse; +import until.the.eternity.common.response.PageResponseDto; + +@RequestMapping("/auction-realtime") +@RestController +@RequiredArgsConstructor +@Tag(name = "실시간 경매장 API", description = "실시간 경매장 아이템 조회 API") +public class AuctionRealtimeController { + + private final AuctionRealtimeService service; + + @GetMapping("/search") + @Operation(summary = "실시간 경매장 아이템 검색", description = "실시간 경매장에 등록된 아이템 검색") + public ResponseEntity< + PageResponseDto>> + search( + @ParameterObject @ModelAttribute @Valid RealtimePageRequestDto pageDto, + @ParameterObject @ModelAttribute @Valid + AuctionRealtimeSearchRequest requestDto) { + PageResponseDto> result = + service.search(requestDto, pageDto.toPageable()); + return ResponseEntity.ok(result); + } + + @GetMapping("/{id}") + @Operation(summary = "실시간 경매장 아이템 단건 조회", description = "실시간 경매장 아이템 ID로 단건 조회") + public ResponseEntity> findById( + @PathVariable Long id) { + AuctionRealtimeDetailResponse result = + service.findByIdOrElseThrow(id); + return ResponseEntity.ok(result); + } +} diff --git a/src/main/java/until/the/eternity/auctionrealtime/interfaces/rest/dto/request/AuctionRealtimeSearchRequest.java b/src/main/java/until/the/eternity/auctionrealtime/interfaces/rest/dto/request/AuctionRealtimeSearchRequest.java new file mode 100644 index 00000000..19d06834 --- /dev/null +++ b/src/main/java/until/the/eternity/auctionrealtime/interfaces/rest/dto/request/AuctionRealtimeSearchRequest.java @@ -0,0 +1,14 @@ +package until.the.eternity.auctionrealtime.interfaces.rest.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import until.the.eternity.auctionhistory.interfaces.rest.dto.request.ItemOptionSearchRequest; +import until.the.eternity.auctionhistory.interfaces.rest.dto.request.PriceSearchRequest; + +/** 실시간 경매장 검색 조건 DTO */ +@Schema(description = "실시간 경매장 검색 조건") +public record AuctionRealtimeSearchRequest( + @Schema(description = "아이템 이름 (like 검색)", example = "페러시우스 타이탄 블레이드") String itemName, + @Schema(description = "대분류 카테고리", example = "근거리 장비") String itemTopCategory, + @Schema(description = "소분류 카테고리", example = "검") String itemSubCategory, + @Schema(description = "가격 검색 조건") PriceSearchRequest priceSearchRequest, + @Schema(description = "아이템 옵션 검색 조건") ItemOptionSearchRequest itemOptionSearchRequest) {} diff --git a/src/main/java/until/the/eternity/auctionrealtime/interfaces/rest/dto/request/RealtimePageRequestDto.java b/src/main/java/until/the/eternity/auctionrealtime/interfaces/rest/dto/request/RealtimePageRequestDto.java new file mode 100644 index 00000000..9719ba72 --- /dev/null +++ b/src/main/java/until/the/eternity/auctionrealtime/interfaces/rest/dto/request/RealtimePageRequestDto.java @@ -0,0 +1,39 @@ +package until.the.eternity.auctionrealtime.interfaces.rest.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import until.the.eternity.common.enums.SortDirection; + +@Schema(description = "실시간 경매장 페이지 요청 파라미터") +public record RealtimePageRequestDto( + @Schema(description = "요청할 페이지 번호 (1부터 시작)", example = "1") @Min(1) Integer page, + @Schema(description = "페이지당 항목 수", example = "20") @Min(1) @Max(100) Integer size, + @Schema( + description = + "정렬 필드 (dateAuctionExpire, dateRegister, auctionPricePerUnit, itemName)", + example = "dateAuctionExpire") + RealtimeSortField sortBy, + @Schema(description = "정렬 방향 (ASC, DESC)", example = "ASC") SortDirection direction) { + + private static final int DEFAULT_PAGE = 1; + private static final int DEFAULT_SIZE = 20; + private static final RealtimeSortField DEFAULT_SORT_BY = RealtimeSortField.DATE_AUCTION_EXPIRE; + private static final SortDirection DEFAULT_DIRECTION = SortDirection.ASC; + + public Pageable toPageable() { + int resolvedPage = this.page != null ? this.page - 1 : DEFAULT_PAGE - 1; + int resolvedSize = this.size != null ? this.size : DEFAULT_SIZE; + RealtimeSortField resolvedSortBy = this.sortBy != null ? this.sortBy : DEFAULT_SORT_BY; + SortDirection resolvedDirection = + this.direction != null ? this.direction : DEFAULT_DIRECTION; + + return PageRequest.of( + resolvedPage, + resolvedSize, + Sort.by(resolvedDirection.toSpringDirection(), resolvedSortBy.getFieldName())); + } +} diff --git a/src/main/java/until/the/eternity/auctionrealtime/interfaces/rest/dto/request/RealtimeSortField.java b/src/main/java/until/the/eternity/auctionrealtime/interfaces/rest/dto/request/RealtimeSortField.java new file mode 100644 index 00000000..5151f8a4 --- /dev/null +++ b/src/main/java/until/the/eternity/auctionrealtime/interfaces/rest/dto/request/RealtimeSortField.java @@ -0,0 +1,43 @@ +package until.the.eternity.auctionrealtime.interfaces.rest.dto.request; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.Arrays; + +/** 실시간 경매장 정렬 필드 */ +@Schema(description = "실시간 경매장 정렬 필드", enumAsRef = true) +public enum RealtimeSortField { + DATE_AUCTION_EXPIRE("dateAuctionExpire", "경매 만료 일시"), + DATE_REGISTER("dateRegister", "등록 일시"), + AUCTION_PRICE_PER_UNIT("auctionPricePerUnit", "개당 가격"), + ITEM_NAME("itemName", "아이템 이름"); + + private final String fieldName; + private final String description; + + RealtimeSortField(String fieldName, String description) { + this.fieldName = fieldName; + this.description = description; + } + + @JsonValue + public String getFieldName() { + return fieldName; + } + + public String getDescription() { + return description; + } + + @JsonCreator + public static RealtimeSortField from(String fieldName) { + if (fieldName == null) { + return DATE_AUCTION_EXPIRE; + } + return Arrays.stream(RealtimeSortField.values()) + .filter(field -> field.fieldName.equalsIgnoreCase(fieldName)) + .findFirst() + .orElse(DATE_AUCTION_EXPIRE); + } +} diff --git a/src/main/java/until/the/eternity/auctionrealtime/interfaces/rest/dto/response/AuctionRealtimeDetailResponse.java b/src/main/java/until/the/eternity/auctionrealtime/interfaces/rest/dto/response/AuctionRealtimeDetailResponse.java new file mode 100644 index 00000000..ba184764 --- /dev/null +++ b/src/main/java/until/the/eternity/auctionrealtime/interfaces/rest/dto/response/AuctionRealtimeDetailResponse.java @@ -0,0 +1,17 @@ +package until.the.eternity.auctionrealtime.interfaces.rest.dto.response; + +import com.fasterxml.jackson.annotation.JsonFormat; +import java.time.Instant; +import java.util.List; + +public record AuctionRealtimeDetailResponse( + Long id, + String itemName, + String itemDisplayName, + Long itemCount, + Long auctionPricePerUnit, + @JsonFormat(shape = JsonFormat.Shape.STRING) Instant dateAuctionExpire, + @JsonFormat(shape = JsonFormat.Shape.STRING) Instant dateRegister, + String itemSubCategory, + String itemTopCategory, + List itemOptions) {} diff --git a/src/main/java/until/the/eternity/auctionrealtime/interfaces/rest/dto/response/RealtimeItemOptionResponse.java b/src/main/java/until/the/eternity/auctionrealtime/interfaces/rest/dto/response/RealtimeItemOptionResponse.java new file mode 100644 index 00000000..c72b8504 --- /dev/null +++ b/src/main/java/until/the/eternity/auctionrealtime/interfaces/rest/dto/response/RealtimeItemOptionResponse.java @@ -0,0 +1,9 @@ +package until.the.eternity.auctionrealtime.interfaces.rest.dto.response; + +public record RealtimeItemOptionResponse( + String id, + String optionType, + String optionSubType, + String optionValue, + String optionValue2, + String optionDesc) {} diff --git a/src/main/java/until/the/eternity/hornBugle/application/runner/HornBugleIndexRunner.java b/src/main/java/until/the/eternity/hornBugle/application/runner/HornBugleIndexRunner.java new file mode 100644 index 00000000..c26c781e --- /dev/null +++ b/src/main/java/until/the/eternity/hornBugle/application/runner/HornBugleIndexRunner.java @@ -0,0 +1,88 @@ +package until.the.eternity.hornBugle.application.runner; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Component; +import until.the.eternity.hornBugle.domain.entity.HornBugleWorldHistory; +import until.the.eternity.hornBugle.domain.repository.HornBugleRepositoryPort; +import until.the.eternity.hornBugle.infrastructure.elasticsearch.HornBugleIndexService; + +/** + * 서버 재기동 시 DB 데이터를 Elasticsearch에 일괄 색인하는 Runner. application.yml에서 + * elasticsearch.index.enabled=true로 설정 시 활성화됩니다. + */ +@Slf4j +@Component +@RequiredArgsConstructor +@ConditionalOnProperty( + name = "elasticsearch.index.enabled", + havingValue = "true", + matchIfMissing = false) +public class HornBugleIndexRunner implements ApplicationRunner { + + private static final int BATCH_SIZE = 500; + + private final HornBugleRepositoryPort repository; + private final HornBugleIndexService indexService; + + @Override + public void run(ApplicationArguments args) { + log.info("[ES] Starting batch indexing on application startup..."); + + try { + // 인덱스 삭제 후 재생성 + indexService.recreateIndex(); + + // DB 전체 데이터를 배치로 조회하여 색인 + long totalIndexed = indexAllFromDatabase(); + + log.info("[ES] Batch indexing completed. Total indexed: {} documents", totalIndexed); + } catch (Exception e) { + log.error("[ES] Batch indexing failed: {}", e.getMessage(), e); + } + } + + /** + * DB의 모든 데이터를 배치로 조회하여 Elasticsearch에 색인한다. + * + * @return 색인된 총 문서 수 + */ + private long indexAllFromDatabase() { + long totalIndexed = 0; + int pageNumber = 0; + + while (true) { + Pageable pageable = PageRequest.of(pageNumber, BATCH_SIZE); + Page page = repository.findAll(pageable); + + if (!page.hasContent()) { + break; + } + + List entities = page.getContent(); + indexService.indexAll(entities); + + totalIndexed += entities.size(); + log.debug( + "[ES] Indexed page {}: {} documents (total: {})", + pageNumber, + entities.size(), + totalIndexed); + + if (!page.hasNext()) { + break; + } + + pageNumber++; + } + + return totalIndexed; + } +} diff --git a/src/main/java/until/the/eternity/hornBugle/application/service/HornBugleService.java b/src/main/java/until/the/eternity/hornBugle/application/service/HornBugleService.java index 7253b467..bf9413b3 100644 --- a/src/main/java/until/the/eternity/hornBugle/application/service/HornBugleService.java +++ b/src/main/java/until/the/eternity/hornBugle/application/service/HornBugleService.java @@ -2,7 +2,7 @@ import java.time.Instant; import java.util.List; -import lombok.RequiredArgsConstructor; +import java.util.Optional; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Page; import org.springframework.stereotype.Service; @@ -13,18 +13,31 @@ import until.the.eternity.hornBugle.domain.mapper.HornBugleMapper; import until.the.eternity.hornBugle.domain.repository.HornBugleRepositoryPort; import until.the.eternity.hornBugle.domain.service.HornBugleDuplicateChecker; +import until.the.eternity.hornBugle.infrastructure.elasticsearch.HornBugleDocument; +import until.the.eternity.hornBugle.infrastructure.elasticsearch.HornBugleIndexService; import until.the.eternity.hornBugle.interfaces.external.dto.OpenApiHornBugleHistoryResponse; import until.the.eternity.hornBugle.interfaces.rest.dto.request.HornBuglePageRequestDto; import until.the.eternity.hornBugle.interfaces.rest.dto.response.HornBugleHistoryResponse; @Slf4j @Service -@RequiredArgsConstructor public class HornBugleService { private final HornBugleRepositoryPort repository; private final HornBugleDuplicateChecker duplicateChecker; private final HornBugleMapper mapper; + private final Optional indexService; + + public HornBugleService( + HornBugleRepositoryPort repository, + HornBugleDuplicateChecker duplicateChecker, + HornBugleMapper mapper, + Optional indexService) { + this.repository = repository; + this.duplicateChecker = duplicateChecker; + this.mapper = mapper; + this.indexService = indexService; + } /** * API 응답 데이터를 중복 제거 후 저장한다. @@ -58,20 +71,73 @@ public int saveAll(HornBugleServer server, List repository.saveAll(entities); + // Elasticsearch 실시간 색인 (DB 저장 성공 후, ES가 활성화된 경우에만) + indexService.ifPresent(service -> service.indexAll(entities)); + log.info("[HornBugle] [{}] Saved {} new records.", server.getServerName(), entities.size()); return entities.size(); } /** - * 서버별 최신 N건 조회 (페이징) + * 서버별 최신 N건 조회 (페이징). keyword가 있고 ES가 활성화되어 있으면 Elasticsearch 검색을 수행한다. ES가 비활성화되어 있거나 사용 불가능한 + * 경우 MySQL FULLTEXT 검색으로 fallback한다. * * @param serverName 서버 이름 (선택 사항, null이면 전체 조회) + * @param keyword 검색 키워드 (선택 사항, null이면 DB 검색) * @param pageRequest 페이지 요청 정보 * @return 페이징 응답 */ @Transactional(readOnly = true) public PageResponseDto search( + String serverName, String keyword, HornBuglePageRequestDto pageRequest) { + + // keyword가 없으면 단순 DB 조회 + if (keyword == null || keyword.isBlank()) { + return searchByDatabase(serverName, pageRequest); + } + + // keyword가 있고 ES가 활성화되어 있고 사용 가능하면 Elasticsearch 검색 + if (indexService.isPresent() && indexService.get().isAvailable()) { + try { + return searchByElasticsearch(serverName, keyword, pageRequest); + } catch (Exception e) { + log.warn( + "[HornBugle] Elasticsearch search failed, falling back to MySQL FULLTEXT search. keyword={}, error={}", + keyword, + e.getMessage()); + return searchByDatabaseWithKeyword(serverName, keyword, pageRequest); + } + } + + // ES가 비활성화되어 있거나 사용 불가능한 경우 MySQL FULLTEXT 검색 + if (indexService.isEmpty()) { + log.info( + "[HornBugle] Elasticsearch is not enabled. Using MySQL FULLTEXT search. keyword={}", + keyword); + } else { + log.warn( + "[HornBugle] Elasticsearch is not available. Falling back to MySQL FULLTEXT search. keyword={}", + keyword); + } + + return searchByDatabaseWithKeyword(serverName, keyword, pageRequest); + } + + /** Elasticsearch로 검색한다. */ + private PageResponseDto searchByElasticsearch( + String serverName, String keyword, HornBuglePageRequestDto pageRequest) { + + Page page = + indexService.get().search(keyword, serverName, pageRequest.toPageable()); + + Page responsePage = page.map(mapper::toResponse); + + return PageResponseDto.of(responsePage); + } + + /** DB로 검색한다 (keyword 없이). */ + private PageResponseDto searchByDatabase( String serverName, HornBuglePageRequestDto pageRequest) { Page page; @@ -86,4 +152,23 @@ public PageResponseDto search( return PageResponseDto.of(responsePage); } + + /** MySQL FULLTEXT 검색으로 keyword 검색을 수행한다. */ + private PageResponseDto searchByDatabaseWithKeyword( + String serverName, String keyword, HornBuglePageRequestDto pageRequest) { + + Page page; + + if (serverName != null && !serverName.isBlank()) { + page = + repository.searchByKeywordAndServerName( + keyword, serverName, pageRequest.toPageable()); + } else { + page = repository.searchByKeyword(keyword, pageRequest.toPageable()); + } + + Page responsePage = page.map(mapper::toResponse); + + return PageResponseDto.of(responsePage); + } } diff --git a/src/main/java/until/the/eternity/hornBugle/domain/mapper/HornBugleMapper.java b/src/main/java/until/the/eternity/hornBugle/domain/mapper/HornBugleMapper.java index 173f1191..a24aee75 100644 --- a/src/main/java/until/the/eternity/hornBugle/domain/mapper/HornBugleMapper.java +++ b/src/main/java/until/the/eternity/hornBugle/domain/mapper/HornBugleMapper.java @@ -5,6 +5,7 @@ import org.mapstruct.Mapping; import until.the.eternity.hornBugle.domain.entity.HornBugleWorldHistory; import until.the.eternity.hornBugle.domain.enums.HornBugleServer; +import until.the.eternity.hornBugle.infrastructure.elasticsearch.HornBugleDocument; import until.the.eternity.hornBugle.interfaces.external.dto.OpenApiHornBugleHistoryResponse; import until.the.eternity.hornBugle.interfaces.rest.dto.response.HornBugleHistoryResponse; @@ -19,4 +20,7 @@ HornBugleWorldHistory toEntity( OpenApiHornBugleHistoryResponse dto, HornBugleServer server, Instant registerTime); HornBugleHistoryResponse toResponse(HornBugleWorldHistory entity); + + @Mapping(target = "id", expression = "java(Long.parseLong(document.getId()))") + HornBugleHistoryResponse toResponse(HornBugleDocument document); } diff --git a/src/main/java/until/the/eternity/hornBugle/domain/repository/HornBugleRepositoryPort.java b/src/main/java/until/the/eternity/hornBugle/domain/repository/HornBugleRepositoryPort.java index 33667068..8e85b756 100644 --- a/src/main/java/until/the/eternity/hornBugle/domain/repository/HornBugleRepositoryPort.java +++ b/src/main/java/until/the/eternity/hornBugle/domain/repository/HornBugleRepositoryPort.java @@ -18,4 +18,11 @@ public interface HornBugleRepositoryPort { Page findAll(Pageable pageable); List findByServerNameAndDateSend(String serverName, Instant dateSend); + + /** FULLTEXT 인덱스를 사용한 키워드 검색 (전체 서버) */ + Page searchByKeyword(String keyword, Pageable pageable); + + /** FULLTEXT 인덱스를 사용한 키워드 검색 (서버 필터 포함) */ + Page searchByKeywordAndServerName( + String keyword, String serverName, Pageable pageable); } diff --git a/src/main/java/until/the/eternity/hornBugle/infrastructure/elasticsearch/HornBugleDocument.java b/src/main/java/until/the/eternity/hornBugle/infrastructure/elasticsearch/HornBugleDocument.java new file mode 100644 index 00000000..7c0ceb37 --- /dev/null +++ b/src/main/java/until/the/eternity/hornBugle/infrastructure/elasticsearch/HornBugleDocument.java @@ -0,0 +1,50 @@ +package until.the.eternity.hornBugle.infrastructure.elasticsearch; + +import java.time.Instant; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.Id; +import org.springframework.data.elasticsearch.annotations.Document; +import org.springframework.data.elasticsearch.annotations.Field; +import org.springframework.data.elasticsearch.annotations.FieldType; +import org.springframework.data.elasticsearch.annotations.Setting; +import until.the.eternity.hornBugle.domain.entity.HornBugleWorldHistory; + +@Document(indexName = "horn_bugle_world_history") +@Setting(settingPath = "elasticsearch/horn-bugle-settings.json") +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class HornBugleDocument { + + @Id private String id; + + @Field(type = FieldType.Keyword, name = "server_name") + private String serverName; + + @Field(type = FieldType.Text, name = "character_name", analyzer = "nori_analyzer") + private String characterName; + + @Field(type = FieldType.Text, name = "message", analyzer = "nori_analyzer") + private String message; + + @Field(type = FieldType.Date, name = "date_send") + private Instant dateSend; + + @Field(type = FieldType.Date, name = "date_register") + private Instant dateRegister; + + public static HornBugleDocument from(HornBugleWorldHistory entity) { + return HornBugleDocument.builder() + .id(String.valueOf(entity.getId())) + .serverName(entity.getServerName()) + .characterName(entity.getCharacterName()) + .message(entity.getMessage()) + .dateSend(entity.getDateSend()) + .dateRegister(entity.getDateRegister()) + .build(); + } +} diff --git a/src/main/java/until/the/eternity/hornBugle/infrastructure/elasticsearch/HornBugleElasticsearchConfig.java b/src/main/java/until/the/eternity/hornBugle/infrastructure/elasticsearch/HornBugleElasticsearchConfig.java new file mode 100644 index 00000000..9df5c5cd --- /dev/null +++ b/src/main/java/until/the/eternity/hornBugle/infrastructure/elasticsearch/HornBugleElasticsearchConfig.java @@ -0,0 +1,12 @@ +package until.the.eternity.hornBugle.infrastructure.elasticsearch; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories; + +/** Elasticsearch 설정 클래스. elasticsearch.enabled=true일 때만 Elasticsearch Repository를 활성화한다. */ +@Configuration +@ConditionalOnProperty(name = "elasticsearch.enabled", havingValue = "true", matchIfMissing = false) +@EnableElasticsearchRepositories( + basePackages = "until.the.eternity.hornBugle.infrastructure.elasticsearch") +public class HornBugleElasticsearchConfig {} diff --git a/src/main/java/until/the/eternity/hornBugle/infrastructure/elasticsearch/HornBugleElasticsearchRepository.java b/src/main/java/until/the/eternity/hornBugle/infrastructure/elasticsearch/HornBugleElasticsearchRepository.java new file mode 100644 index 00000000..6539c23e --- /dev/null +++ b/src/main/java/until/the/eternity/hornBugle/infrastructure/elasticsearch/HornBugleElasticsearchRepository.java @@ -0,0 +1,6 @@ +package until.the.eternity.hornBugle.infrastructure.elasticsearch; + +import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; + +public interface HornBugleElasticsearchRepository + extends ElasticsearchRepository {} diff --git a/src/main/java/until/the/eternity/hornBugle/infrastructure/elasticsearch/HornBugleIndexService.java b/src/main/java/until/the/eternity/hornBugle/infrastructure/elasticsearch/HornBugleIndexService.java new file mode 100644 index 00000000..cebe7ea4 --- /dev/null +++ b/src/main/java/until/the/eternity/hornBugle/infrastructure/elasticsearch/HornBugleIndexService.java @@ -0,0 +1,182 @@ +package until.the.eternity.hornBugle.infrastructure.elasticsearch; + +import co.elastic.clients.elasticsearch._types.query_dsl.BoolQuery; +import co.elastic.clients.elasticsearch._types.query_dsl.Query; +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.data.elasticsearch.client.elc.NativeQuery; +import org.springframework.data.elasticsearch.core.ElasticsearchOperations; +import org.springframework.data.elasticsearch.core.IndexOperations; +import org.springframework.data.elasticsearch.core.SearchHit; +import org.springframework.data.elasticsearch.core.SearchHits; +import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; +import org.springframework.stereotype.Service; +import until.the.eternity.hornBugle.domain.entity.HornBugleWorldHistory; + +@Slf4j +@Service +@RequiredArgsConstructor +@ConditionalOnProperty(name = "elasticsearch.enabled", havingValue = "true", matchIfMissing = false) +public class HornBugleIndexService { + + private static final String INDEX_NAME = "horn_bugle_world_history"; + private static final int BATCH_SIZE = 500; + + private final ElasticsearchOperations elasticsearchOperations; + private final HornBugleElasticsearchRepository repository; + + /** + * 단일 엔티티를 Elasticsearch에 색인한다. + * + * @param entity 색인할 엔티티 + */ + public void index(HornBugleWorldHistory entity) { + try { + HornBugleDocument document = HornBugleDocument.from(entity); + repository.save(document); + log.debug("[ES] Indexed document: id={}", entity.getId()); + } catch (Exception e) { + log.error( + "[ES] Failed to index document: id={}, error={}", + entity.getId(), + e.getMessage(), + e); + } + } + + /** + * 여러 엔티티를 Elasticsearch에 일괄 색인한다. + * + * @param entities 색인할 엔티티 목록 + */ + public void indexAll(List entities) { + if (entities == null || entities.isEmpty()) { + return; + } + + try { + List documents = + entities.stream().map(HornBugleDocument::from).toList(); + + for (int i = 0; i < documents.size(); i += BATCH_SIZE) { + int toIndex = Math.min(i + BATCH_SIZE, documents.size()); + List batch = documents.subList(i, toIndex); + repository.saveAll(batch); + log.debug("[ES] Indexed batch: {} documents", batch.size()); + } + + log.info("[ES] Successfully indexed {} documents", entities.size()); + } catch (Exception e) { + log.error("[ES] Failed to index {} documents: {}", entities.size(), e.getMessage(), e); + } + } + + /** 인덱스를 삭제하고 재생성한다. */ + public void recreateIndex() { + try { + IndexOperations indexOps = elasticsearchOperations.indexOps(HornBugleDocument.class); + + if (indexOps.exists()) { + indexOps.delete(); + log.info("[ES] Deleted existing index: {}", INDEX_NAME); + } + + indexOps.createWithMapping(); + log.info("[ES] Created index with mapping: {}", INDEX_NAME); + } catch (Exception e) { + log.error("[ES] Failed to recreate index: {}", e.getMessage(), e); + throw new RuntimeException("Failed to recreate Elasticsearch index", e); + } + } + + /** + * keyword로 검색한다. serverName이 있으면 필터로 추가한다. + * + * @param keyword 검색 키워드 + * @param serverName 서버명 필터 (nullable) + * @param pageable 페이징 정보 + * @return 검색 결과 + */ + public Page search(String keyword, String serverName, Pageable pageable) { + try { + // Multi-match query for keyword search + Query multiMatchQuery = + Query.of( + q -> + q.multiMatch( + mm -> + mm.query(keyword) + .fields( + "character_name", + "message", + "server_name", + "date_send"))); + + // Build bool query + BoolQuery.Builder boolQueryBuilder = new BoolQuery.Builder().must(multiMatchQuery); + + // Add serverName filter if present + if (serverName != null && !serverName.isBlank()) { + Query serverFilter = + Query.of(q -> q.term(t -> t.field("server_name").value(serverName))); + boolQueryBuilder.filter(serverFilter); + } + + Query finalQuery = Query.of(q -> q.bool(boolQueryBuilder.build())); + + NativeQuery searchQuery = + NativeQuery.builder().withQuery(finalQuery).withPageable(pageable).build(); + + SearchHits searchHits = + elasticsearchOperations.search( + searchQuery, HornBugleDocument.class, IndexCoordinates.of(INDEX_NAME)); + + List content = + searchHits.getSearchHits().stream().map(SearchHit::getContent).toList(); + + return new PageImpl<>(content, pageable, searchHits.getTotalHits()); + } catch (Exception e) { + log.error( + "[ES] Search failed: keyword={}, serverName={}, error={}", + keyword, + serverName, + e.getMessage(), + e); + throw new RuntimeException("Elasticsearch search failed", e); + } + } + + /** + * 인덱스 존재 여부를 확인한다. + * + * @return 인덱스 존재 여부 + */ + public boolean indexExists() { + try { + return elasticsearchOperations.indexOps(HornBugleDocument.class).exists(); + } catch (Exception e) { + log.error("[ES] Failed to check index existence: {}", e.getMessage(), e); + return false; + } + } + + /** + * Elasticsearch 서버가 사용 가능한 상태인지 확인한다. + * + * @return ES 서버 사용 가능 여부 + */ + public boolean isAvailable() { + try { + return elasticsearchOperations.indexOps(HornBugleDocument.class).exists() + || elasticsearchOperations.indexOps(HornBugleDocument.class).create(); + } catch (Exception e) { + log.warn("[ES] Elasticsearch is not available: {}", e.getMessage()); + return false; + } + } +} diff --git a/src/main/java/until/the/eternity/hornBugle/infrastructure/persistence/HornBugleJpaRepository.java b/src/main/java/until/the/eternity/hornBugle/infrastructure/persistence/HornBugleJpaRepository.java index 65585cf6..2e6acde1 100644 --- a/src/main/java/until/the/eternity/hornBugle/infrastructure/persistence/HornBugleJpaRepository.java +++ b/src/main/java/until/the/eternity/hornBugle/infrastructure/persistence/HornBugleJpaRepository.java @@ -25,4 +25,39 @@ public interface HornBugleJpaRepository extends JpaRepository findByServerName(String serverName, Pageable pageable); List findByServerNameAndDateSend(String serverName, Instant dateSend); + + /** FULLTEXT 인덱스를 사용한 키워드 검색 (전체 서버) */ + @Query( + value = + """ + SELECT * FROM horn_bugle_world_history + WHERE MATCH(character_name, message, server_name, date_send_text) AGAINST(:keyword IN NATURAL LANGUAGE MODE) + ORDER BY date_send DESC + """, + countQuery = + """ + SELECT COUNT(*) FROM horn_bugle_world_history + WHERE MATCH(character_name, message, server_name, date_send_text) AGAINST(:keyword IN NATURAL LANGUAGE MODE) + """, + nativeQuery = true) + Page searchByKeyword(String keyword, Pageable pageable); + + /** FULLTEXT 인덱스를 사용한 키워드 검색 (서버 필터 포함) */ + @Query( + value = + """ + SELECT * FROM horn_bugle_world_history + WHERE MATCH(character_name, message, server_name, date_send_text) AGAINST(:keyword IN NATURAL LANGUAGE MODE) + AND server_name = :serverName + ORDER BY date_send DESC + """, + countQuery = + """ + SELECT COUNT(*) FROM horn_bugle_world_history + WHERE MATCH(character_name, message, server_name, date_send_text) AGAINST(:keyword IN NATURAL LANGUAGE MODE) + AND server_name = :serverName + """, + nativeQuery = true) + Page searchByKeywordAndServerName( + String keyword, String serverName, Pageable pageable); } diff --git a/src/main/java/until/the/eternity/hornBugle/infrastructure/persistence/HornBugleRepositoryPortImpl.java b/src/main/java/until/the/eternity/hornBugle/infrastructure/persistence/HornBugleRepositoryPortImpl.java index 8f7319fc..52366b65 100644 --- a/src/main/java/until/the/eternity/hornBugle/infrastructure/persistence/HornBugleRepositoryPortImpl.java +++ b/src/main/java/until/the/eternity/hornBugle/infrastructure/persistence/HornBugleRepositoryPortImpl.java @@ -59,4 +59,15 @@ public List findByServerNameAndDateSend( String serverName, Instant dateSend) { return jpaRepository.findByServerNameAndDateSend(serverName, dateSend); } + + @Override + public Page searchByKeyword(String keyword, Pageable pageable) { + return jpaRepository.searchByKeyword(keyword, pageable); + } + + @Override + public Page searchByKeywordAndServerName( + String keyword, String serverName, Pageable pageable) { + return jpaRepository.searchByKeywordAndServerName(keyword, serverName, pageable); + } } diff --git a/src/main/java/until/the/eternity/hornBugle/interfaces/rest/controller/HornBugleController.java b/src/main/java/until/the/eternity/hornBugle/interfaces/rest/controller/HornBugleController.java index 60d7efdb..e8c1fca4 100644 --- a/src/main/java/until/the/eternity/hornBugle/interfaces/rest/controller/HornBugleController.java +++ b/src/main/java/until/the/eternity/hornBugle/interfaces/rest/controller/HornBugleController.java @@ -4,10 +4,12 @@ import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; +import jakarta.validation.constraints.Size; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springdoc.core.annotations.ParameterObject; import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import until.the.eternity.common.response.PageResponseDto; import until.the.eternity.hornBugle.application.scheduler.HornBugleScheduler; @@ -19,6 +21,7 @@ @RequestMapping("/horn-bugle") @RestController @RequiredArgsConstructor +@Validated @Tag(name = "뿔피리 히스토리 API", description = "거대한 외침의 뿔피리 내역 API") public class HornBugleController { @@ -26,13 +29,22 @@ public class HornBugleController { private final HornBugleScheduler scheduler; @GetMapping - @Operation(summary = "뿔피리 히스토리 조회", description = "거대한 외침의 뿔피리 내역을 조회합니다. 서버별 또는 전체 조회가 가능합니다.") + @Operation( + summary = "뿔피리 히스토리 조회", + description = + "거대한 외침의 뿔피리 내역을 조회합니다. 서버별 또는 전체 조회가 가능합니다. " + + "keyword 입력 시 Elasticsearch 기반 전문 검색을 수행합니다.") public ResponseEntity> search( @Parameter(description = "서버 이름 (류트, 만돌린, 하프, 울프). 미입력시 전체 조회") @RequestParam(required = false) String serverName, + @Parameter(description = "검색 키워드 (캐릭터명, 메시지, 서버명, 발화시각 검색). 최대 50자") + @RequestParam(required = false) + @Size(max = 50, message = "검색 키워드는 최대 50자까지 입력 가능합니다.") + String keyword, @ParameterObject @ModelAttribute @Valid HornBuglePageRequestDto pageRequest) { - PageResponseDto result = service.search(serverName, pageRequest); + PageResponseDto result = + service.search(serverName, keyword, pageRequest); return ResponseEntity.ok(result); } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 4c644416..755f859a 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -18,6 +18,10 @@ management: spring: application: name: open-api-batch-server + elasticsearch: + uris: ${ELASTICSEARCH_URIS:http://localhost:9200} + username: ${ELASTICSEARCH_USERNAME:} + password: ${ELASTICSEARCH_PASSWORD:} web: resources: static-locations: classpath:/static/ @@ -104,4 +108,11 @@ statistics: # 전날 통계 최종 확정 스케줄러 # AuctionHistoryScheduler 실행 이후 전날 23시대 거래까지 포함한 통계를 확정 # 기본값: 매일 00:10 (AuctionHistory가 00:05에 실행되므로 그 이후) - cron: ${STATISTICS_PREVIOUS_DAY_CRON:0 10 0 * * *} \ No newline at end of file + cron: ${STATISTICS_PREVIOUS_DAY_CRON:0 10 0 * * *} + +elasticsearch: + # Elasticsearch 기능 활성화 여부 (false면 ES 관련 기능 비활성화, DB 검색만 사용) + enabled: ${ELASTICSEARCH_ENABLED:false} + index: + # 서버 재기동 시 DB 데이터 일괄 색인 활성화 여부 + enabled: ${ELASTICSEARCH_INDEX_ENABLED:false} \ No newline at end of file diff --git a/src/main/resources/db/migration/V17__add_fulltext_index_to_horn_bugle_world_history.sql b/src/main/resources/db/migration/V17__add_fulltext_index_to_horn_bugle_world_history.sql new file mode 100644 index 00000000..039a1896 --- /dev/null +++ b/src/main/resources/db/migration/V17__add_fulltext_index_to_horn_bugle_world_history.sql @@ -0,0 +1,8 @@ +-- FULLTEXT 인덱스를 위한 date_send 문자열 컬럼 추가 (Generated Column) +ALTER TABLE horn_bugle_world_history + ADD COLUMN date_send_text VARCHAR(30) GENERATED ALWAYS AS (DATE_FORMAT(date_send, '%Y-%m-%d %H:%i:%s')) STORED; + +-- character_name, message, server_name, date_send_text에 FULLTEXT 인덱스 추가 +-- MySQL 8.0+ 에서 ngram parser를 사용하여 한글 검색 지원 +ALTER TABLE horn_bugle_world_history + ADD FULLTEXT INDEX ft_horn_bugle_search (character_name, message, server_name, date_send_text) WITH PARSER ngram; diff --git a/src/main/resources/elasticsearch/horn-bugle-settings.json b/src/main/resources/elasticsearch/horn-bugle-settings.json new file mode 100644 index 00000000..f04948ef --- /dev/null +++ b/src/main/resources/elasticsearch/horn-bugle-settings.json @@ -0,0 +1,9 @@ +{ + "analysis": { + "analyzer": { + "nori_analyzer": { + "type": "standard" + } + } + } +} diff --git a/src/test/java/until/the/eternity/hornBugle/application/runner/HornBugleIndexRunnerTest.java b/src/test/java/until/the/eternity/hornBugle/application/runner/HornBugleIndexRunnerTest.java new file mode 100644 index 00000000..0669753b --- /dev/null +++ b/src/test/java/until/the/eternity/hornBugle/application/runner/HornBugleIndexRunnerTest.java @@ -0,0 +1,97 @@ +package until.the.eternity.hornBugle.application.runner; + +import static org.mockito.Mockito.*; + +import java.time.Instant; +import java.util.List; +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.boot.ApplicationArguments; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import until.the.eternity.hornBugle.domain.entity.HornBugleWorldHistory; +import until.the.eternity.hornBugle.domain.repository.HornBugleRepositoryPort; +import until.the.eternity.hornBugle.infrastructure.elasticsearch.HornBugleIndexService; + +@ExtendWith(MockitoExtension.class) +class HornBugleIndexRunnerTest { + + @Mock private HornBugleRepositoryPort repository; + @Mock private HornBugleIndexService indexService; + + @InjectMocks private HornBugleIndexRunner runner; + + @Test + @DisplayName("서버 재기동 시 인덱스를 재생성하고 DB 데이터를 일괄 색인한다") + void run_shouldRecreateIndexAndIndexAllData() { + // given + ApplicationArguments args = mock(ApplicationArguments.class); + + HornBugleWorldHistory entity1 = createEntity(1L, "류트", "테스트1", "안녕하세요1"); + HornBugleWorldHistory entity2 = createEntity(2L, "만돌린", "테스트2", "안녕하세요2"); + + // 첫 페이지: 데이터 있고 다음 페이지 없음 (hasNext = false) + Page firstPage = + new PageImpl<>(List.of(entity1, entity2), PageRequest.of(0, 500), 2); + + when(repository.findAll(PageRequest.of(0, 500))).thenReturn(firstPage); + + // when + runner.run(args); + + // then + verify(indexService).recreateIndex(); + verify(indexService).indexAll(List.of(entity1, entity2)); + } + + @Test + @DisplayName("DB에 데이터가 없으면 인덱스만 재생성한다") + void run_withNoData_shouldOnlyRecreateIndex() { + // given + ApplicationArguments args = mock(ApplicationArguments.class); + + Page emptyPage = + new PageImpl<>(List.of(), PageRequest.of(0, 500), 0); + + when(repository.findAll(PageRequest.of(0, 500))).thenReturn(emptyPage); + + // when + runner.run(args); + + // then + verify(indexService).recreateIndex(); + verify(indexService, never()).indexAll(anyList()); + } + + @Test + @DisplayName("인덱스 재생성 실패 시 예외를 로깅하고 계속 진행하지 않는다") + void run_onRecreateIndexFailure_shouldLogAndStop() { + // given + ApplicationArguments args = mock(ApplicationArguments.class); + doThrow(new RuntimeException("ES error")).when(indexService).recreateIndex(); + + // when + runner.run(args); + + // then + verify(indexService).recreateIndex(); + verifyNoInteractions(repository); + } + + private HornBugleWorldHistory createEntity( + Long id, String serverName, String characterName, String message) { + return HornBugleWorldHistory.builder() + .id(id) + .serverName(serverName) + .characterName(characterName) + .message(message) + .dateSend(Instant.now()) + .dateRegister(Instant.now()) + .build(); + } +} diff --git a/src/test/java/until/the/eternity/hornBugle/application/service/HornBugleServiceTest.java b/src/test/java/until/the/eternity/hornBugle/application/service/HornBugleServiceTest.java new file mode 100644 index 00000000..92f37faa --- /dev/null +++ b/src/test/java/until/the/eternity/hornBugle/application/service/HornBugleServiceTest.java @@ -0,0 +1,335 @@ +package until.the.eternity.hornBugle.application.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.*; + +import java.time.Instant; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +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.Pageable; +import until.the.eternity.common.response.PageResponseDto; +import until.the.eternity.hornBugle.domain.entity.HornBugleWorldHistory; +import until.the.eternity.hornBugle.domain.enums.HornBugleServer; +import until.the.eternity.hornBugle.domain.mapper.HornBugleMapper; +import until.the.eternity.hornBugle.domain.repository.HornBugleRepositoryPort; +import until.the.eternity.hornBugle.domain.service.HornBugleDuplicateChecker; +import until.the.eternity.hornBugle.infrastructure.elasticsearch.HornBugleDocument; +import until.the.eternity.hornBugle.infrastructure.elasticsearch.HornBugleIndexService; +import until.the.eternity.hornBugle.interfaces.external.dto.OpenApiHornBugleHistoryResponse; +import until.the.eternity.hornBugle.interfaces.rest.dto.request.HornBuglePageRequestDto; +import until.the.eternity.hornBugle.interfaces.rest.dto.response.HornBugleHistoryResponse; + +@ExtendWith(MockitoExtension.class) +class HornBugleServiceTest { + + @Mock private HornBugleRepositoryPort repository; + @Mock private HornBugleDuplicateChecker duplicateChecker; + @Mock private HornBugleMapper mapper; + @Mock private HornBugleIndexService indexService; + + @Nested + @DisplayName("Elasticsearch 활성화 상태에서 search 메서드 테스트") + class SearchWithElasticsearchTest { + + private HornBugleService service; + + @BeforeEach + void setUp() { + service = + new HornBugleService( + repository, duplicateChecker, mapper, Optional.of(indexService)); + } + + @Test + @DisplayName("keyword가 null이면 DB 검색을 수행한다") + void search_withNullKeyword_shouldSearchDatabase() { + // given + String serverName = "류트"; + String keyword = null; + HornBuglePageRequestDto pageRequest = new HornBuglePageRequestDto(1, 20); + Pageable pageable = pageRequest.toPageable(); + + HornBugleWorldHistory entity = createEntity(1L, "류트", "테스트", "안녕하세요"); + HornBugleHistoryResponse response = createResponse(1L, "류트", "테스트", "안녕하세요"); + Page entityPage = new PageImpl<>(List.of(entity), pageable, 1); + + when(repository.findByServerName(serverName, pageable)).thenReturn(entityPage); + when(mapper.toResponse(entity)).thenReturn(response); + + // when + PageResponseDto result = + service.search(serverName, keyword, pageRequest); + + // then + assertThat(result.items()).hasSize(1); + assertThat(result.items().get(0).characterName()).isEqualTo("테스트"); + verify(repository).findByServerName(serverName, pageable); + verifyNoInteractions(indexService); + } + + @Test + @DisplayName("keyword가 빈 문자열이면 DB 검색을 수행한다") + void search_withEmptyKeyword_shouldSearchDatabase() { + // given + String serverName = null; + String keyword = ""; + HornBuglePageRequestDto pageRequest = new HornBuglePageRequestDto(1, 20); + Pageable pageable = pageRequest.toPageable(); + + HornBugleWorldHistory entity = createEntity(1L, "류트", "테스트", "안녕하세요"); + HornBugleHistoryResponse response = createResponse(1L, "류트", "테스트", "안녕하세요"); + Page entityPage = new PageImpl<>(List.of(entity), pageable, 1); + + when(repository.findAll(pageable)).thenReturn(entityPage); + when(mapper.toResponse(entity)).thenReturn(response); + + // when + PageResponseDto result = + service.search(serverName, keyword, pageRequest); + + // then + assertThat(result.items()).hasSize(1); + verify(repository).findAll(pageable); + verifyNoInteractions(indexService); + } + + @Test + @DisplayName("keyword가 있으면 Elasticsearch 검색을 수행한다") + void search_withKeyword_shouldSearchElasticsearch() { + // given + String serverName = "류트"; + String keyword = "테스트"; + HornBuglePageRequestDto pageRequest = new HornBuglePageRequestDto(1, 20); + Pageable pageable = pageRequest.toPageable(); + + HornBugleDocument document = createDocument("1", "류트", "테스트", "안녕하세요"); + HornBugleHistoryResponse response = createResponse(1L, "류트", "테스트", "안녕하세요"); + Page documentPage = new PageImpl<>(List.of(document), pageable, 1); + + when(indexService.search(keyword, serverName, pageable)).thenReturn(documentPage); + when(mapper.toResponse(document)).thenReturn(response); + + // when + PageResponseDto result = + service.search(serverName, keyword, pageRequest); + + // then + assertThat(result.items()).hasSize(1); + assertThat(result.items().get(0).characterName()).isEqualTo("테스트"); + verify(indexService).search(keyword, serverName, pageable); + verifyNoInteractions(repository); + } + + @Test + @DisplayName("serverName이 null이고 keyword가 있으면 Elasticsearch 전체 검색을 수행한다") + void search_withKeywordAndNoServerName_shouldSearchElasticsearchWithoutFilter() { + // given + String serverName = null; + String keyword = "테스트"; + HornBuglePageRequestDto pageRequest = new HornBuglePageRequestDto(1, 20); + Pageable pageable = pageRequest.toPageable(); + + HornBugleDocument document = createDocument("1", "류트", "테스트", "안녕하세요"); + HornBugleHistoryResponse response = createResponse(1L, "류트", "테스트", "안녕하세요"); + Page documentPage = new PageImpl<>(List.of(document), pageable, 1); + + when(indexService.search(keyword, serverName, pageable)).thenReturn(documentPage); + when(mapper.toResponse(document)).thenReturn(response); + + // when + PageResponseDto result = + service.search(serverName, keyword, pageRequest); + + // then + assertThat(result.items()).hasSize(1); + verify(indexService).search(keyword, null, pageable); + } + } + + @Nested + @DisplayName("Elasticsearch 비활성화 상태에서 search 메서드 테스트") + class SearchWithoutElasticsearchTest { + + private HornBugleService service; + + @BeforeEach + void setUp() { + service = new HornBugleService(repository, duplicateChecker, mapper, Optional.empty()); + } + + @Test + @DisplayName("keyword가 있어도 ES가 비활성화되어 있으면 DB 검색을 수행한다") + void search_withKeywordButNoEs_shouldFallbackToDatabase() { + // given + String serverName = "류트"; + String keyword = "테스트"; + HornBuglePageRequestDto pageRequest = new HornBuglePageRequestDto(1, 20); + Pageable pageable = pageRequest.toPageable(); + + HornBugleWorldHistory entity = createEntity(1L, "류트", "테스트", "안녕하세요"); + HornBugleHistoryResponse response = createResponse(1L, "류트", "테스트", "안녕하세요"); + Page entityPage = new PageImpl<>(List.of(entity), pageable, 1); + + when(repository.findByServerName(serverName, pageable)).thenReturn(entityPage); + when(mapper.toResponse(entity)).thenReturn(response); + + // when + PageResponseDto result = + service.search(serverName, keyword, pageRequest); + + // then + assertThat(result.items()).hasSize(1); + verify(repository).findByServerName(serverName, pageable); + } + } + + @Nested + @DisplayName("saveAll 메서드 테스트") + class SaveAllTest { + + private HornBugleService service; + + @BeforeEach + void setUp() { + service = + new HornBugleService( + repository, duplicateChecker, mapper, Optional.of(indexService)); + } + + @Test + @DisplayName("새 데이터 저장 시 Elasticsearch에도 색인한다") + void saveAll_shouldIndexToElasticsearch() { + // given + HornBugleServer server = HornBugleServer.LUTE; + OpenApiHornBugleHistoryResponse apiResponse = + new OpenApiHornBugleHistoryResponse("테스트", "안녕하세요", Instant.now()); + List responses = List.of(apiResponse); + + HornBugleWorldHistory entity = createEntity(1L, "류트", "테스트", "안녕하세요"); + + when(duplicateChecker.filterDuplicates(server, responses)).thenReturn(responses); + when(mapper.toEntity(eq(apiResponse), eq(server), any(Instant.class))) + .thenReturn(entity); + + // when + int savedCount = service.saveAll(server, responses); + + // then + assertThat(savedCount).isEqualTo(1); + verify(repository).saveAll(anyList()); + verify(indexService).indexAll(anyList()); + } + + @Test + @DisplayName("모든 데이터가 중복이면 저장하지 않는다") + void saveAll_withAllDuplicates_shouldNotSave() { + // given + HornBugleServer server = HornBugleServer.LUTE; + OpenApiHornBugleHistoryResponse apiResponse = + new OpenApiHornBugleHistoryResponse("테스트", "안녕하세요", Instant.now()); + List responses = List.of(apiResponse); + + when(duplicateChecker.filterDuplicates(server, responses)).thenReturn(List.of()); + + // when + int savedCount = service.saveAll(server, responses); + + // then + assertThat(savedCount).isEqualTo(0); + verifyNoInteractions(repository); + verifyNoInteractions(indexService); + } + + @Test + @DisplayName("빈 응답이면 저장하지 않는다") + void saveAll_withEmptyResponses_shouldNotSave() { + // given + HornBugleServer server = HornBugleServer.LUTE; + List responses = List.of(); + + // when + int savedCount = service.saveAll(server, responses); + + // then + assertThat(savedCount).isEqualTo(0); + verifyNoInteractions(duplicateChecker); + verifyNoInteractions(repository); + verifyNoInteractions(indexService); + } + } + + @Nested + @DisplayName("ES 비활성화 상태에서 saveAll 메서드 테스트") + class SaveAllWithoutElasticsearchTest { + + private HornBugleService service; + + @BeforeEach + void setUp() { + service = new HornBugleService(repository, duplicateChecker, mapper, Optional.empty()); + } + + @Test + @DisplayName("ES가 비활성화되어 있으면 DB에만 저장한다") + void saveAll_withoutEs_shouldOnlySaveToDatabase() { + // given + HornBugleServer server = HornBugleServer.LUTE; + OpenApiHornBugleHistoryResponse apiResponse = + new OpenApiHornBugleHistoryResponse("테스트", "안녕하세요", Instant.now()); + List responses = List.of(apiResponse); + + HornBugleWorldHistory entity = createEntity(1L, "류트", "테스트", "안녕하세요"); + + when(duplicateChecker.filterDuplicates(server, responses)).thenReturn(responses); + when(mapper.toEntity(eq(apiResponse), eq(server), any(Instant.class))) + .thenReturn(entity); + + // when + int savedCount = service.saveAll(server, responses); + + // then + assertThat(savedCount).isEqualTo(1); + verify(repository).saveAll(anyList()); + // indexService는 Optional.empty()이므로 호출되지 않음 + } + } + + private HornBugleWorldHistory createEntity( + Long id, String serverName, String characterName, String message) { + return HornBugleWorldHistory.builder() + .id(id) + .serverName(serverName) + .characterName(characterName) + .message(message) + .dateSend(Instant.now()) + .dateRegister(Instant.now()) + .build(); + } + + private HornBugleDocument createDocument( + String id, String serverName, String characterName, String message) { + return HornBugleDocument.builder() + .id(id) + .serverName(serverName) + .characterName(characterName) + .message(message) + .dateSend(Instant.now()) + .dateRegister(Instant.now()) + .build(); + } + + private HornBugleHistoryResponse createResponse( + Long id, String serverName, String characterName, String message) { + return new HornBugleHistoryResponse( + id, serverName, characterName, message, Instant.now(), Instant.now()); + } +} diff --git a/src/test/java/until/the/eternity/hornBugle/infrastructure/elasticsearch/HornBugleIndexServiceTest.java b/src/test/java/until/the/eternity/hornBugle/infrastructure/elasticsearch/HornBugleIndexServiceTest.java new file mode 100644 index 00000000..7a8d1071 --- /dev/null +++ b/src/test/java/until/the/eternity/hornBugle/infrastructure/elasticsearch/HornBugleIndexServiceTest.java @@ -0,0 +1,237 @@ +package until.the.eternity.hornBugle.infrastructure.elasticsearch; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.*; + +import java.time.Instant; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +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.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.elasticsearch.client.elc.NativeQuery; +import org.springframework.data.elasticsearch.core.ElasticsearchOperations; +import org.springframework.data.elasticsearch.core.IndexOperations; +import org.springframework.data.elasticsearch.core.SearchHit; +import org.springframework.data.elasticsearch.core.SearchHits; +import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; +import until.the.eternity.hornBugle.domain.entity.HornBugleWorldHistory; + +@ExtendWith(MockitoExtension.class) +class HornBugleIndexServiceTest { + + @Mock private ElasticsearchOperations elasticsearchOperations; + @Mock private HornBugleElasticsearchRepository repository; + + @InjectMocks private HornBugleIndexService indexService; + + @Nested + @DisplayName("index 메서드 테스트") + class IndexTest { + + @Test + @DisplayName("단일 엔티티를 색인한다") + void index_shouldSaveDocument() { + // given + HornBugleWorldHistory entity = createEntity(1L, "류트", "테스트", "안녕하세요"); + + // when + indexService.index(entity); + + // then + verify(repository).save(any(HornBugleDocument.class)); + } + + @Test + @DisplayName("색인 실패 시 예외를 던지지 않고 로그만 기록한다") + void index_onFailure_shouldNotThrowException() { + // given + HornBugleWorldHistory entity = createEntity(1L, "류트", "테스트", "안녕하세요"); + when(repository.save(any(HornBugleDocument.class))) + .thenThrow(new RuntimeException("ES error")); + + // when & then (no exception) + indexService.index(entity); + verify(repository).save(any(HornBugleDocument.class)); + } + } + + @Nested + @DisplayName("indexAll 메서드 테스트") + class IndexAllTest { + + @Test + @DisplayName("여러 엔티티를 일괄 색인한다") + void indexAll_shouldSaveAllDocuments() { + // given + List entities = + List.of( + createEntity(1L, "류트", "테스트1", "안녕하세요1"), + createEntity(2L, "만돌린", "테스트2", "안녕하세요2")); + + // when + indexService.indexAll(entities); + + // then + verify(repository).saveAll(anyList()); + } + + @Test + @DisplayName("빈 리스트면 저장하지 않는다") + void indexAll_withEmptyList_shouldNotSave() { + // when + indexService.indexAll(List.of()); + + // then + verifyNoInteractions(repository); + } + + @Test + @DisplayName("null이면 저장하지 않는다") + void indexAll_withNull_shouldNotSave() { + // when + indexService.indexAll(null); + + // then + verifyNoInteractions(repository); + } + } + + @Nested + @DisplayName("recreateIndex 메서드 테스트") + class RecreateIndexTest { + + @Test + @DisplayName("기존 인덱스가 있으면 삭제 후 재생성한다") + void recreateIndex_withExistingIndex_shouldDeleteAndCreate() { + // given + IndexOperations indexOps = mock(IndexOperations.class); + when(elasticsearchOperations.indexOps(HornBugleDocument.class)).thenReturn(indexOps); + when(indexOps.exists()).thenReturn(true); + when(indexOps.createWithMapping()).thenReturn(true); + + // when + indexService.recreateIndex(); + + // then + verify(indexOps).delete(); + verify(indexOps).createWithMapping(); + } + + @Test + @DisplayName("기존 인덱스가 없으면 생성만 한다") + void recreateIndex_withoutExistingIndex_shouldOnlyCreate() { + // given + IndexOperations indexOps = mock(IndexOperations.class); + when(elasticsearchOperations.indexOps(HornBugleDocument.class)).thenReturn(indexOps); + when(indexOps.exists()).thenReturn(false); + when(indexOps.createWithMapping()).thenReturn(true); + + // when + indexService.recreateIndex(); + + // then + verify(indexOps, never()).delete(); + verify(indexOps).createWithMapping(); + } + + @Test + @DisplayName("인덱스 생성 실패 시 예외를 던진다") + void recreateIndex_onFailure_shouldThrowException() { + // given + IndexOperations indexOps = mock(IndexOperations.class); + when(elasticsearchOperations.indexOps(HornBugleDocument.class)).thenReturn(indexOps); + when(indexOps.exists()).thenThrow(new RuntimeException("ES error")); + + // when & then + assertThatThrownBy(() -> indexService.recreateIndex()) + .isInstanceOf(RuntimeException.class) + .hasMessageContaining("Failed to recreate Elasticsearch index"); + } + } + + @Nested + @DisplayName("search 메서드 테스트") + class SearchTest { + + @Test + @DisplayName("검색 결과를 반환한다") + @SuppressWarnings("unchecked") + void search_shouldReturnResults() { + // given + String keyword = "테스트"; + String serverName = "류트"; + Pageable pageable = PageRequest.of(0, 20); + + HornBugleDocument document = + HornBugleDocument.builder() + .id("1") + .serverName("류트") + .characterName("테스트") + .message("안녕하세요") + .dateSend(Instant.now()) + .dateRegister(Instant.now()) + .build(); + + SearchHit searchHit = mock(SearchHit.class); + when(searchHit.getContent()).thenReturn(document); + + SearchHits searchHits = mock(SearchHits.class); + when(searchHits.getSearchHits()).thenReturn(List.of(searchHit)); + when(searchHits.getTotalHits()).thenReturn(1L); + + when(elasticsearchOperations.search( + any(NativeQuery.class), + eq(HornBugleDocument.class), + any(IndexCoordinates.class))) + .thenReturn(searchHits); + + // when + Page result = indexService.search(keyword, serverName, pageable); + + // then + assertThat(result.getContent()).hasSize(1); + assertThat(result.getContent().get(0).getCharacterName()).isEqualTo("테스트"); + assertThat(result.getTotalElements()).isEqualTo(1L); + } + + @Test + @DisplayName("검색 실패 시 예외를 던진다") + void search_onFailure_shouldThrowException() { + // given + String keyword = "테스트"; + String serverName = "류트"; + Pageable pageable = PageRequest.of(0, 20); + + when(elasticsearchOperations.search( + any(NativeQuery.class), + eq(HornBugleDocument.class), + any(IndexCoordinates.class))) + .thenThrow(new RuntimeException("ES error")); + + // when & then + assertThatThrownBy(() -> indexService.search(keyword, serverName, pageable)) + .isInstanceOf(RuntimeException.class) + .hasMessageContaining("Elasticsearch search failed"); + } + } + + private HornBugleWorldHistory createEntity( + Long id, String serverName, String characterName, String message) { + return HornBugleWorldHistory.builder() + .id(id) + .serverName(serverName) + .characterName(characterName) + .message(message) + .dateSend(Instant.now()) + .dateRegister(Instant.now()) + .build(); + } +} From 5ff52ae4927d49fdc5341baefe4ed6b1ae7000a7 Mon Sep 17 00:00:00 2001 From: dev-ant Date: Wed, 28 Jan 2026 23:13:35 +0900 Subject: [PATCH 4/8] =?UTF-8?q?feat:=20auction=20realtime=20api=20permital?= =?UTF-8?q?l=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/until/the/eternity/config/SecurityConfig.java | 3 ++- src/main/java/until/the/eternity/config/WebConfig.java | 2 ++ src/main/resources/application.yml | 4 ++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/main/java/until/the/eternity/config/SecurityConfig.java b/src/main/java/until/the/eternity/config/SecurityConfig.java index 52f6b9b4..13363f0a 100644 --- a/src/main/java/until/the/eternity/config/SecurityConfig.java +++ b/src/main/java/until/the/eternity/config/SecurityConfig.java @@ -45,7 +45,8 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti "/api/**", "/auction-history/**", "/statistics/**", - "/horn-bugle/**") + "/horn-bugle/**", + "/auction-realtime/**") .permitAll() // 나머지 요청은 인증 필요 .anyRequest() diff --git a/src/main/java/until/the/eternity/config/WebConfig.java b/src/main/java/until/the/eternity/config/WebConfig.java index b3ebbc84..3f577f0d 100644 --- a/src/main/java/until/the/eternity/config/WebConfig.java +++ b/src/main/java/until/the/eternity/config/WebConfig.java @@ -3,6 +3,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.format.FormatterRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import until.the.eternity.auctionrealtime.interfaces.rest.dto.request.RealtimeSortField; import until.the.eternity.common.enums.SortDirection; import until.the.eternity.common.enums.SortField; @@ -13,5 +14,6 @@ public class WebConfig implements WebMvcConfigurer { public void addFormatters(FormatterRegistry registry) { registry.addConverter(String.class, SortField.class, SortField::from); registry.addConverter(String.class, SortDirection.class, SortDirection::from); + registry.addConverter(String.class, RealtimeSortField.class, RealtimeSortField::from); } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 755f859a..4d7ac1c9 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -112,7 +112,7 @@ statistics: elasticsearch: # Elasticsearch 기능 활성화 여부 (false면 ES 관련 기능 비활성화, DB 검색만 사용) - enabled: ${ELASTICSEARCH_ENABLED:false} + enabled: ${ELASTICSEARCH_ENABLED:true} index: # 서버 재기동 시 DB 데이터 일괄 색인 활성화 여부 - enabled: ${ELASTICSEARCH_INDEX_ENABLED:false} \ No newline at end of file + enabled: ${ELASTICSEARCH_INDEX_ENABLED:true} \ No newline at end of file From b49f8038b6450994bc8905fd4ad0937d9bce6fb9 Mon Sep 17 00:00:00 2001 From: dev-ant Date: Wed, 28 Jan 2026 23:13:55 +0900 Subject: [PATCH 5/8] =?UTF-8?q?docs:=20README=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 89 ++++++++++++++++++++++++++++++++++++++---- docker-compose-dev.yml | 1 + 2 files changed, 83 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index a6c79585..ba022487 100644 --- a/README.md +++ b/README.md @@ -10,15 +10,21 @@ ## 주요 기능 -### 경매장 데이터 수집 +### 경매장 거래 내역 수집 - 매 1시간마다 마비노기 경매장 거래 내역을 Nexon Open API를 통해 수집 - 약 70개의 아이템 카테고리에 대한 커서 기반 페이지네이션으로 데이터 수집 - 카테고리별 요청 간 딜레이 설정으로 API Rate Limit 준수 +### 실시간 경매장 데이터 수집 +- 10분 간격으로 현재 판매 중인 아이템 정보 수집 +- 만료된 아이템 자동 삭제 +- 거래 내역과 분리된 별도 테이블로 관리 + ### 뿔피리 데이터 수집 - 5분마다 거대한 외침의 뿔피리 내역 수집 - 4개 서버 지원 (류트, 만돌린, 하프, 울프) - 지수 백오프 기반 재시도 로직 구현 +- **Elasticsearch 연동**: 한글 전문 검색 지원 (Ngram Parser) ### 통계 분석 - **일간 통계**: 경매 데이터 수집 완료 시 자동 트리거 (이벤트 기반) @@ -28,7 +34,7 @@ ### 데이터 조회 - 아이템별 최저가, 최고가, 평균가, 거래량 등 시세 조회 - 서버별/전체 뿔피리 내역 조회 -- 아이템 옵션 필터링 (무기 공격력, 방어구 방어력 등) +- 아이템 옵션 필터링 (무기 공격력, 방어구 방어력, 세공 옵션 등)
@@ -39,9 +45,11 @@ | **Backend** | Java 21, Spring Boot 3.5.0, Spring Data JPA, QueryDSL | | **HTTP Client** | Spring WebFlux (WebClient) | | **Database** | MySQL 8, Flyway | +| **Search Engine** | Elasticsearch (한글 전문 검색) | +| **Security** | Spring Security, JWT | | **Test** | JUnit5, Mockito, AssertJ, Testcontainers | | **Code Quality** | Spotless (Google Java Format AOSP), Jacoco | -| **Documentation** | Swagger, Spring REST Docs | +| **Documentation** | Swagger (Springdoc OpenAPI), Spring REST Docs | | **DevOps** | Docker Compose, GitHub Actions | | **Deployment** | Oracle Cloud | @@ -52,7 +60,10 @@ ``` src/main/java/until/the/eternity/ ├── auctionhistory/ # 경매장 거래 내역 수집 및 검색 -├── hornBugle/ # 뿔피리 내역 수집 및 검색 +├── auctionrealtime/ # 실시간 경매장 데이터 수집 (10분 간격) +├── auctionitem/ # 경매장 아이템 엔티티 +├── auctionitemoption/ # 아이템 옵션 정보 (세공 옵션 포함) +├── hornBugle/ # 뿔피리 내역 수집 및 검색 (Elasticsearch 연동) ├── statistics/ # 일간/주간 통계 집계 ├── iteminfo/ # 아이템 메타데이터 ├── itemoptioninfo/ # 아이템 옵션 정보 @@ -76,19 +87,45 @@ infrastructure/ # Repository 구현체, JPA ## API 엔드포인트 +### 경매장 거래 내역 | Endpoint | Method | 설명 | |----------|--------|------| | `/auction-history/search` | GET | 경매 내역 검색 (필터 및 페이징) | | `/auction-history/{id}` | GET | 단일 거래 내역 조회 | | `/auction-history/batch` | POST | 배치 수동 실행 | + +### 실시간 경매장 +| Endpoint | Method | 설명 | +|----------|--------|------| +| `/auction-realtime/search` | GET | 현재 판매 중인 아이템 검색 | +| `/auction-realtime/{id}` | GET | 단일 아이템 조회 | + +### 뿔피리 +| Endpoint | Method | 설명 | +|----------|--------|------| | `/horn-bugle` | GET | 뿔피리 내역 검색 (서버별/전체) | | `/horn-bugle/batch` | POST | 뿔피리 배치 수동 실행 | + +### 통계 +| Endpoint | Method | 설명 | +|----------|--------|------| | `/statistics/daily/items` | GET | 일간 아이템 통계 | | `/statistics/daily/subcategories` | GET | 일간 서브카테고리 통계 | | `/statistics/daily/top-categories` | GET | 일간 상위카테고리 통계 | | `/statistics/weekly/items` | GET | 주간 아이템 통계 | +| `/statistics/weekly/subcategories` | GET | 주간 서브카테고리 통계 | +| `/statistics/weekly/top-categories` | GET | 주간 상위카테고리 통계 | + +### 메타데이터 +| Endpoint | Method | 설명 | +|----------|--------|------| | `/api/item-infos` | GET | 아이템 메타데이터 | | `/api/v1/item-option-infos` | GET | 아이템 옵션 정보 | +| `/api/auction-search-options` | GET | 검색 옵션 메타데이터 | + +### 시스템 +| Endpoint | Method | 설명 | +|----------|--------|------| | `/actuator/health` | GET | 헬스체크 | | `/swagger-ui/index.html` | - | API 문서 | @@ -99,6 +136,7 @@ infrastructure/ # Repository 구현체, JPA | 스케줄러 | Cron 표현식 | 설명 | |----------|-------------|------| | 경매 내역 수집 | `0 0 * * * *` | 매 시 정각 | +| 실시간 경매장 수집 | `0 0/10 * * * *` | 10분마다 | | 뿔피리 수집 | `0 */5 * * * *` | 5분마다 | | 전일 통계 확정 | `0 10 0 * * *` | 매일 00:10 | | 주간 통계 집계 | `5 0 4 * * MON` | 매주 월요일 04:00 | @@ -126,6 +164,11 @@ JWT_REFRESH_TOKEN_VALIDITY=86400000 # Nexon Open API NEXON_OPEN_API_KEY=your-api-key + +# Elasticsearch +ELASTICSEARCH_URIS=http://localhost:9200 +ELASTICSEARCH_USERNAME= +ELASTICSEARCH_PASSWORD= ``` ### 선택 환경 변수 @@ -134,11 +177,19 @@ NEXON_OPEN_API_KEY=your-api-key AUCTION_HISTORY_CRON=0 0 * * * * AUCTION_HISTORY_DELAY_MS=1000 +# 실시간 경매장 배치 +AUCTION_REALTIME_CRON=0 0/10 * * * * +AUCTION_REALTIME_DELAY_MS=500 + # 뿔피리 배치 HORN_BUGLE_CRON=0 */5 * * * * HORN_BUGLE_MAX_RETRIES=3 HORN_BUGLE_RETRY_DELAY_MS=2000 +# Elasticsearch 기능 +ELASTICSEARCH_ENABLED=true +ELASTICSEARCH_INDEX_ENABLED=true + # 통계 STATISTICS_PREVIOUS_DAY_CRON=0 10 0 * * * STATISTICS_WEEKLY_CRON=5 0 4 * * MON @@ -179,9 +230,9 @@ docker-compose -f docker-compose-local.yml down | 환경 | 파일 | 설명 | |------|------|------| -| 로컬 개발 | `docker-compose-local.yml` | 로컬 빌드, 낮은 리소스 | -| 개발 서버 | `docker-compose-dev.yml` | 개발 환경 배포 | -| 운영 서버 | `docker-compose-prod.yml` | 운영 환경 배포 | +| 로컬 개발 | `docker-compose-local.yml` | 로컬 빌드, MySQL, Elasticsearch 포함 | +| 개발 서버 | `docker-compose-dev.yml` | 개발 환경 배포, Autoheal 컨테이너 포함 | +| 운영 서버 | `docker-compose-prod.yml` | 운영 환경 배포, 높은 리소스 할당 |
@@ -209,6 +260,30 @@ docker-compose -f docker-compose-local.yml down
+## 데이터베이스 스키마 + +Flyway를 통한 마이그레이션 관리 (17개 버전) + +### 주요 테이블 +| 테이블 | 설명 | +|--------|------| +| `auction_history` | 경매장 거래 내역 | +| `auction_realtime_item` | 현재 판매 중인 아이템 | +| `auction_history_item_option` | 거래 아이템 옵션 (세공 포함) | +| `auction_realtime_item_option` | 실시간 아이템 옵션 | +| `horn_bugle_world_history` | 뿔피리 내역 (FULLTEXT 인덱스) | +| `item_daily_statistics` | 일간 아이템 통계 | +| `item_weekly_statistics` | 주간 아이템 통계 | +| `subcategory_daily_statistics` | 일간 서브카테고리 통계 | +| `subcategory_weekly_statistics` | 주간 서브카테고리 통계 | +| `top_category_daily_statistics` | 일간 상위카테고리 통계 | +| `top_category_weekly_statistics` | 주간 상위카테고리 통계 | +| `item_info` | 아이템 메타데이터 | +| `item_option_value_info` | 아이템 옵션 정보 | +| `metalware_info` | 금속류 정보 | + +
+ ## API 응답 형식 ```json diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index 88871b78..07952045 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -57,6 +57,7 @@ services: -Xmx${JAVA_OPTS_XMX:-512m} -XX:MaxMetaspaceSize=${JAVA_OPTS_MAX_METASPACE_SIZE:-150m} -XX:ReservedCodeCacheSize=${JAVA_OPTS_RESERVED_CODE_CACHE_SIZE:-48m} + -XX:ReservedCodeCacheSize=${JAVA_OPTS_RESERVED_CODE_CACHE_SIZE:-48m} -XX:MaxDirectMemorySize=${JAVA_OPTS_MAX_DIRECT_MEMORY_SIZE:-64m} -Xss${JAVA_OPTS_XSS:-512k} -XX:+UseG1GC From 4ca27321450e0cdad36ddd7e66b90fb2cea1d36a Mon Sep 17 00:00:00 2001 From: dev-ant Date: Wed, 28 Jan 2026 23:37:07 +0900 Subject: [PATCH 6/8] =?UTF-8?q?infra:=20docker-compose-prod=EC=97=90=20ela?= =?UTF-8?q?stic=20search=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.prod.sample | 81 +++++++++++++++++++ docker-compose-prod.yml | 55 +++++++++++++ .../application/service/HornBugleService.java | 6 +- .../elasticsearch/HornBugleIndexService.java | 8 +- .../dto/request/HornBuglePageRequestDto.java | 10 +++ 5 files changed, 152 insertions(+), 8 deletions(-) create mode 100644 .env.prod.sample diff --git a/.env.prod.sample b/.env.prod.sample new file mode 100644 index 00000000..2355df86 --- /dev/null +++ b/.env.prod.sample @@ -0,0 +1,81 @@ +# ============================================================================= +# Production Environment Configuration +# ============================================================================= +# 운영 환경용 설정 파일 +# +# 사용법: +# 1. 이 파일을 .env.prod로 복사하세요 +# 2. 필요한 값들을 채워넣으세요 +# 3. docker-compose -f docker-compose-prod.yml --env-file .env.prod up -d 로 실행하세요 +# +# 주의: .env.prod 파일은 민감한 정보를 포함하므로 절대 Git에 커밋하지 마세요! +# ============================================================================= + +# ============================================================================= +# Application Configuration +# ============================================================================= +SERVER_PORT=8080 + +# ============================================================================= +# Docker Configuration +# ============================================================================= +DOCKER_USERNAME=your_docker_username +DOCKER_REPO=your_docker_repo +DOCKER_IMAGE_TAG=prod + +# ============================================================================= +# Database Configuration +# ============================================================================= +DB_IP=your_db_host +DB_PORT=3306 +DB_SCHEMA=your_db_schema +DB_USER=your_db_user +DB_PASSWORD=your_db_password + +# ============================================================================= +# Security Configuration +# ============================================================================= +# JWT 시크릿 키 (최소 256비트 권장, 운영 환경에서는 반드시 강력한 키 사용) +JWT_SECRET_KEY=your_production_secret_key_minimum_32_characters_long +JWT_ACCESS_TOKEN_VALIDITY=3600000 # 1시간 (밀리초) +JWT_REFRESH_TOKEN_VALIDITY=86400000 # 24시간 (밀리초) + +# ============================================================================= +# External API Configuration +# ============================================================================= +# Nexon Open API 키 (https://openapi.nexon.com/에서 발급) +NEXON_OPEN_API_KEY=your_nexon_api_key_here + +# 경매 데이터 수집 설정 +AUCTION_HISTORY_DELAY_MS=1000 # API 호출 간 딜레이 (1초) +AUCTION_HISTORY_CRON=0 0 * * * * # 매시간 정각에 실행 + +# 통계 스케줄러 설정 +STATISTICS_PREVIOUS_DAY_CRON=0 10 0 * * * # 매일 00:10에 전일 통계 확정 + +# ============================================================================= +# Elasticsearch Configuration +# ============================================================================= +# Elasticsearch 기능 활성화 여부 +ELASTICSEARCH_ENABLED=true +ELASTICSEARCH_INDEX_ENABLED=true + +# Elasticsearch 버전 (docker-compose-prod.yml에서 사용) +ELASTICSEARCH_VERSION=8.11.0 + +# Elasticsearch 포트 (기본값: 9200, 9300) +ELASTICSEARCH_PORT=9200 +ELASTICSEARCH_TRANSPORT_PORT=9300 + +# Elasticsearch JVM Heap 크기 (최대 3GB 권장) +# 주의: 시스템 메모리의 50%를 넘지 않도록 설정 +ELASTICSEARCH_HEAP_SIZE=1536m + +# ============================================================================= +# JVM Configuration (선택사항 - docker-compose에 기본값 있음) +# ============================================================================= +# JAVA_OPTS_XMS=768m +# JAVA_OPTS_XMX=1536m +# JAVA_OPTS_MAX_METASPACE_SIZE=450m +# JAVA_OPTS_RESERVED_CODE_CACHE_SIZE=144m +# JAVA_OPTS_MAX_DIRECT_MEMORY_SIZE=192m diff --git a/docker-compose-prod.yml b/docker-compose-prod.yml index 4b5eac7e..cc6c47e7 100644 --- a/docker-compose-prod.yml +++ b/docker-compose-prod.yml @@ -17,6 +17,11 @@ services: labels: autoheal: "true" + + depends_on: + devnogi-elastic-search: + condition: service_healthy + environment: # === Application Configuration === SPRING_PROFILES_ACTIVE: prod @@ -42,6 +47,11 @@ services: AUCTION_HISTORY_CRON: "${AUCTION_HISTORY_CRON}" STATISTICS_PREVIOUS_DAY_CRON: "${STATISTICS_PREVIOUS_DAY_CRON:-0 0 * * * *}" + # === Elasticsearch Configuration === + ELASTICSEARCH_ENABLED: ${ELASTICSEARCH_ENABLED:-true} + ELASTICSEARCH_INDEX_ENABLED: ${ELASTICSEARCH_INDEX_ENABLED:-true} + SPRING_ELASTICSEARCH_URIS: http://devnogi-elastic-search:9200 + # === Docker Configuration === DOCKER_USERNAME: ${DOCKER_USERNAME} DOCKER_REPO: ${DOCKER_REPO} @@ -97,6 +107,49 @@ services: max-size: "50m" max-file: "5" + # === Elasticsearch (Prod - 고성능, 최대 3GB) === + devnogi-elastic-search: + image: docker.elastic.co/elasticsearch/elasticsearch:${ELASTICSEARCH_VERSION:-8.11.0} + container_name: devnogi-elastic-search + restart: unless-stopped + environment: + - discovery.type=single-node + - xpack.security.enabled=false + - "ES_JAVA_OPTS=-Xms${ELASTICSEARCH_HEAP_SIZE:-1536m} -Xmx${ELASTICSEARCH_HEAP_SIZE:-1536m}" + - cluster.name=devnogi-es-cluster-prod + - bootstrap.memory_lock=true + ulimits: + memlock: + soft: -1 + hard: -1 + nofile: + soft: 65536 + hard: 65536 + ports: + - "${ELASTICSEARCH_PORT:-9200}:9200" + - "${ELASTICSEARCH_TRANSPORT_PORT:-9300}:9300" + volumes: + - elasticsearch_data:/usr/share/elasticsearch/data + networks: + - app-network + healthcheck: + test: ["CMD-SHELL", "curl -s http://localhost:9200/_cluster/health | grep -q '\"status\":\"green\"\\|\"status\":\"yellow\"'"] + interval: 15s + timeout: 10s + retries: 10 + start_period: 90s + deploy: + resources: + limits: + memory: 3g + reservations: + memory: 1536m + logging: + driver: "json-file" + options: + max-size: "50m" + max-file: "3" + # === Autoheal (Prod - 빈번한 체크, 긴 graceful shutdown) === autoheal: image: willfarrell/autoheal:latest @@ -121,6 +174,8 @@ services: volumes: app-logs: driver: local + elasticsearch_data: + driver: local networks: app-network: diff --git a/src/main/java/until/the/eternity/hornBugle/application/service/HornBugleService.java b/src/main/java/until/the/eternity/hornBugle/application/service/HornBugleService.java index bf9413b3..6c72a759 100644 --- a/src/main/java/until/the/eternity/hornBugle/application/service/HornBugleService.java +++ b/src/main/java/until/the/eternity/hornBugle/application/service/HornBugleService.java @@ -153,7 +153,7 @@ private PageResponseDto searchByDatabase( return PageResponseDto.of(responsePage); } - /** MySQL FULLTEXT 검색으로 keyword 검색을 수행한다. */ + /** MySQL FULLTEXT 검색으로 keyword 검색을 수행한다. Native Query에서 ORDER BY를 지정하므로 Sort 없이 Pageable을 전달한다. */ private PageResponseDto searchByDatabaseWithKeyword( String serverName, String keyword, HornBuglePageRequestDto pageRequest) { @@ -162,9 +162,9 @@ private PageResponseDto searchByDatabaseWithKeyword( if (serverName != null && !serverName.isBlank()) { page = repository.searchByKeywordAndServerName( - keyword, serverName, pageRequest.toPageable()); + keyword, serverName, pageRequest.toPageableWithoutSort()); } else { - page = repository.searchByKeyword(keyword, pageRequest.toPageable()); + page = repository.searchByKeyword(keyword, pageRequest.toPageableWithoutSort()); } Page responsePage = page.map(mapper::toResponse); diff --git a/src/main/java/until/the/eternity/hornBugle/infrastructure/elasticsearch/HornBugleIndexService.java b/src/main/java/until/the/eternity/hornBugle/infrastructure/elasticsearch/HornBugleIndexService.java index cebe7ea4..67ba3fb7 100644 --- a/src/main/java/until/the/eternity/hornBugle/infrastructure/elasticsearch/HornBugleIndexService.java +++ b/src/main/java/until/the/eternity/hornBugle/infrastructure/elasticsearch/HornBugleIndexService.java @@ -105,17 +105,15 @@ public void recreateIndex() { public Page search(String keyword, String serverName, Pageable pageable) { try { // Multi-match query for keyword search + // Note: date_send is Date type, excluded from text search + // server_name is Keyword type, included for exact match Query multiMatchQuery = Query.of( q -> q.multiMatch( mm -> mm.query(keyword) - .fields( - "character_name", - "message", - "server_name", - "date_send"))); + .fields("character_name", "message"))); // Build bool query BoolQuery.Builder boolQueryBuilder = new BoolQuery.Builder().must(multiMatchQuery); diff --git a/src/main/java/until/the/eternity/hornBugle/interfaces/rest/dto/request/HornBuglePageRequestDto.java b/src/main/java/until/the/eternity/hornBugle/interfaces/rest/dto/request/HornBuglePageRequestDto.java index 06adcf3d..c3648f68 100644 --- a/src/main/java/until/the/eternity/hornBugle/interfaces/rest/dto/request/HornBuglePageRequestDto.java +++ b/src/main/java/until/the/eternity/hornBugle/interfaces/rest/dto/request/HornBuglePageRequestDto.java @@ -25,6 +25,16 @@ public Pageable toPageable() { resolvedPage, resolvedSize, Sort.by(Sort.Direction.DESC, SORT_BY_DATE_SEND)); } + /** + * Native Query용 Pageable (정렬 없음). Native Query에서 ORDER BY를 직접 지정하므로 Sort를 제외한다. + */ + public Pageable toPageableWithoutSort() { + int resolvedPage = this.page != null ? this.page - 1 : DEFAULT_PAGE - 1; + int resolvedSize = this.size != null ? this.size : DEFAULT_SIZE; + + return PageRequest.of(resolvedPage, resolvedSize); + } + public int getResolvedPage() { return this.page != null ? this.page : DEFAULT_PAGE; } From 8050a300e01e40f927eb56b10bae3139c6ff5a7f Mon Sep 17 00:00:00 2001 From: Sanghyun Yi Date: Thu, 29 Jan 2026 00:01:45 +0900 Subject: [PATCH 7/8] fix: optimize imports --- .../scheduler/AuctionHistoryScheduler.java | 5 +++-- .../service/AuctionHistoryService.java | 3 ++- .../service/fetcher/AuctionHistoryFetcher.java | 7 ++++--- .../persister/AuctionHistoryPersister.java | 3 ++- .../domain/entity/AuctionHistory.java | 5 +++-- .../domain/event/AuctionHistorySavedEvent.java | 3 ++- .../domain/mapper/AuctionHistoryMapper.java | 3 ++- .../mapper/OpenApiAuctionHistoryMapper.java | 7 ++++--- .../domain/mapper/OpenApiItemOptionMapper.java | 5 +++-- .../AuctionHistoryRepositoryPort.java | 9 +++++---- .../service/AuctionHistoryDuplicateChecker.java | 9 +++++---- .../fetcher/AuctionHistoryFetcherPort.java | 3 ++- .../persister/AuctionHistoryPersisterPort.java | 3 ++- .../AuctionHistoryJpaRepository.java | 7 ++++--- .../AuctionHistoryQueryDslRepository.java | 11 ++++++----- .../AuctionHistoryRepositoryPortImpl.java | 9 +++++---- .../dto/OpenApiAuctionHistoryListResponse.java | 1 + .../dto/OpenApiAuctionHistoryResponse.java | 3 ++- .../rest/dto/enums/SearchStandard.java | 1 + .../rest/dto/enums/SortDirection.java | 1 + .../response/AuctionHistoryDetailResponse.java | 1 + .../domain/entity/AuctionRealtimeItem.java | 3 ++- .../entity/AuctionRealtimeItemOption.java | 3 ++- .../domain/entity/AuctionHistoryItemOption.java | 3 ++- .../scheduler/AuctionRealtimeScheduler.java | 11 ++++------- .../service/AuctionRealtimeService.java | 5 +++-- .../service/fetcher/AuctionRealtimeFetcher.java | 7 ++++--- .../persister/AuctionRealtimePersister.java | 5 +++-- .../domain/mapper/AuctionRealtimeMapper.java | 3 ++- .../mapper/OpenApiAuctionRealtimeMapper.java | 15 +++++---------- .../mapper/OpenApiRealtimeItemOptionMapper.java | 5 +++-- .../AuctionRealtimeItemRepositoryPort.java | 7 ++++--- .../AuctionRealtimeDuplicateChecker.java | 7 ++++--- .../fetcher/AuctionRealtimeFetcherPort.java | 5 +++-- .../persister/AuctionRealtimePersisterPort.java | 5 +++-- .../AuctionRealtimeItemRepository.java | 5 +++-- .../AuctionRealtimeItemRepositoryPortImpl.java | 7 ++++--- .../AuctionRealtimeQueryDslRepository.java | 5 +++-- .../dto/OpenApiAuctionRealtimeListResponse.java | 1 + .../dto/OpenApiAuctionRealtimeResponse.java | 3 ++- .../rest/dto/request/RealtimeSortField.java | 1 + .../response/AuctionRealtimeDetailResponse.java | 1 + .../service/AuctionSearchOptionService.java | 5 +++-- .../entity/AuctionSearchOptionMetadata.java | 3 ++- .../AuctionSearchOptionRepositoryPort.java | 3 ++- .../AuctionSearchOptionJpaRepository.java | 3 ++- .../AuctionSearchOptionRepositoryPortImpl.java | 3 ++- .../rest/AuctionSearchOptionController.java | 3 ++- .../rest/dto/response/FieldMetadata.java | 1 + .../response/SearchOptionMetadataResponse.java | 1 + .../the/eternity/common/enums/ItemCategory.java | 5 +++-- .../eternity/common/enums/SortDirection.java | 3 ++- .../the/eternity/common/enums/SortField.java | 1 + .../common/exception/GlobalExceptionCode.java | 4 ++-- .../exception/GlobalExceptionHandler.java | 4 ++-- .../common/filter/GatewayAuthFilter.java | 7 ++++--- .../eternity/common/response/ApiResponse.java | 3 ++- .../common/response/PageResponseDto.java | 1 + .../eternity/config/openapi/OpenApiFilters.java | 3 ++- .../config/openapi/OpenApiRetryPolicy.java | 3 ++- .../openapi/OpenApiWebClientProperties.java | 3 ++- .../runner/HornBugleIndexRunner.java | 3 ++- .../scheduler/HornBugleScheduler.java | 5 +++-- .../application/service/HornBugleService.java | 7 ++++--- .../domain/entity/HornBugleWorldHistory.java | 3 ++- .../hornBugle/domain/enums/HornBugleServer.java | 5 +++-- .../domain/mapper/HornBugleMapper.java | 3 ++- .../repository/HornBugleRepositoryPort.java | 7 ++++--- .../service/HornBugleDuplicateChecker.java | 11 ++++++----- .../elasticsearch/HornBugleDocument.java | 3 ++- .../elasticsearch/HornBugleIndexService.java | 3 ++- .../persistence/HornBugleJpaRepository.java | 7 ++++--- .../HornBugleRepositoryPortImpl.java | 7 ++++--- .../OpenApiHornBugleHistoryListResponse.java | 1 + .../dto/OpenApiHornBugleHistoryResponse.java | 1 + .../dto/response/HornBugleHistoryResponse.java | 1 + .../application/service/ItemInfoService.java | 9 +++++---- .../iteminfo/domain/entity/ItemInfoId.java | 3 ++- .../domain/exception/ItemInfoExceptionCode.java | 4 ++-- .../repository/ItemInfoRepositoryPort.java | 3 ++- .../persistence/ItemInfoJpaRepository.java | 3 ++- .../persistence/ItemInfoQueryDslRepository.java | 3 ++- .../persistence/ItemInfoRepositoryPortImpl.java | 3 ++- .../rest/controller/ItemInfoController.java | 3 ++- .../rest/dto/response/ItemCategoryResponse.java | 7 ++++--- .../rest/dto/response/ItemInfoResponse.java | 5 +++-- .../dto/response/ItemInfoSummaryResponse.java | 5 +++-- .../rest/dto/response/ItemInfoSyncResponse.java | 3 ++- .../service/ItemOptionInfoService.java | 3 ++- .../domain/entity/ItemOptionInfoId.java | 5 +++-- .../ItemOptionInfoRepositoryPort.java | 3 ++- .../ItemOptionInfoRepositoryPortImpl.java | 3 ++- .../controller/ItemOptionInfoController.java | 5 +++-- .../service/MetalwareInfoService.java | 3 ++- .../persistence/MetalwareInfoJpaRepository.java | 3 ++- .../MetalwareInfoRepositoryPortImpl.java | 3 ++- .../controller/MetalwareInfoController.java | 3 ++- .../dto/response/MetalwareInfoResponse.java | 3 ++- .../entity/daily/ItemDailyStatistics.java | 3 ++- .../daily/SubcategoryDailyStatistics.java | 3 ++- .../daily/TopCategoryDailyStatistics.java | 3 ++- .../entity/weekly/ItemWeeklyStatistics.java | 3 ++- .../weekly/SubcategoryWeeklyStatistics.java | 3 ++- .../weekly/TopCategoryWeeklyStatistics.java | 3 ++- .../request/DailyStatisticsSearchRequest.java | 3 ++- .../ItemDailyStatisticsSearchRequest.java | 3 ++- .../ItemWeeklyStatisticsSearchRequest.java | 3 ++- ...SubcategoryDailyStatisticsSearchRequest.java | 3 ++- ...ubcategoryWeeklyStatisticsSearchRequest.java | 3 ++- ...TopCategoryDailyStatisticsSearchRequest.java | 3 ++- ...opCategoryWeeklyStatisticsSearchRequest.java | 3 ++- .../response/ItemDailyStatisticsResponse.java | 1 + .../response/ItemWeeklyStatisticsResponse.java | 1 + .../SubcategoryDailyStatisticsResponse.java | 1 + .../SubcategoryWeeklyStatisticsResponse.java | 1 + .../TopCategoryDailyStatisticsResponse.java | 1 + .../TopCategoryWeeklyStatisticsResponse.java | 1 + .../daily/ItemDailyStatisticsRepository.java | 5 +++-- .../SubcategoryDailyStatisticsRepository.java | 5 +++-- .../TopCategoryDailyStatisticsRepository.java | 5 +++-- .../weekly/ItemWeeklyStatisticsRepository.java | 3 ++- .../SubcategoryWeeklyStatisticsRepository.java | 3 ++- .../TopCategoryWeeklyStatisticsRepository.java | 3 ++- .../service/AuctionHistoryServiceTest.java | 13 +++++++------ .../fetcher/AuctionHistoryFetcherTest.java | 17 +++++++++-------- .../persister/AuctionHistoryPersisterTest.java | 13 +++++++------ .../AuctionHistoryDuplicateCheckerTest.java | 17 +++++++++-------- .../fetcher/AuctionRealtimeFetcherTest.java | 15 ++++++++------- .../AuctionRealtimeDuplicateCheckerTest.java | 13 +++++++------ .../service/AuctionSearchOptionServiceTest.java | 11 ++++++----- .../runner/HornBugleIndexRunnerTest.java | 9 +++++---- .../service/HornBugleServiceTest.java | 13 +++++++------ .../HornBugleIndexServiceTest.java | 13 +++++++------ .../service/ItemInfoServiceTest.java | 13 +++++++------ .../service/MetalwareInfoServiceTest.java | 11 ++++++----- 135 files changed, 383 insertions(+), 261 deletions(-) diff --git a/src/main/java/until/the/eternity/auctionhistory/application/scheduler/AuctionHistoryScheduler.java b/src/main/java/until/the/eternity/auctionhistory/application/scheduler/AuctionHistoryScheduler.java index 3d29417e..2ef0398a 100644 --- a/src/main/java/until/the/eternity/auctionhistory/application/scheduler/AuctionHistoryScheduler.java +++ b/src/main/java/until/the/eternity/auctionhistory/application/scheduler/AuctionHistoryScheduler.java @@ -1,7 +1,5 @@ package until.the.eternity.auctionhistory.application.scheduler; -import java.util.*; -import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; @@ -16,6 +14,9 @@ import until.the.eternity.auctionhistory.interfaces.external.dto.OpenApiAuctionHistoryResponse; import until.the.eternity.common.enums.ItemCategory; +import java.util.*; +import java.util.stream.Collectors; + @Slf4j @Component @RequiredArgsConstructor diff --git a/src/main/java/until/the/eternity/auctionhistory/application/service/AuctionHistoryService.java b/src/main/java/until/the/eternity/auctionhistory/application/service/AuctionHistoryService.java index cc15bfd1..07a7ed6f 100644 --- a/src/main/java/until/the/eternity/auctionhistory/application/service/AuctionHistoryService.java +++ b/src/main/java/until/the/eternity/auctionhistory/application/service/AuctionHistoryService.java @@ -1,7 +1,6 @@ package until.the.eternity.auctionhistory.application.service; import jakarta.persistence.EntityManager; -import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Page; @@ -16,6 +15,8 @@ import until.the.eternity.common.request.PageRequestDto; import until.the.eternity.common.response.PageResponseDto; +import java.util.List; + @Service @RequiredArgsConstructor @Slf4j diff --git a/src/main/java/until/the/eternity/auctionhistory/application/service/fetcher/AuctionHistoryFetcher.java b/src/main/java/until/the/eternity/auctionhistory/application/service/fetcher/AuctionHistoryFetcher.java index 4c0e05ad..3aa40844 100644 --- a/src/main/java/until/the/eternity/auctionhistory/application/service/fetcher/AuctionHistoryFetcher.java +++ b/src/main/java/until/the/eternity/auctionhistory/application/service/fetcher/AuctionHistoryFetcher.java @@ -1,8 +1,5 @@ package until.the.eternity.auctionhistory.application.service.fetcher; -import java.util.ArrayList; -import java.util.List; -import java.util.OptionalInt; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; @@ -12,6 +9,10 @@ import until.the.eternity.auctionhistory.interfaces.external.dto.OpenApiAuctionHistoryResponse; import until.the.eternity.common.enums.ItemCategory; +import java.util.ArrayList; +import java.util.List; +import java.util.OptionalInt; + @Slf4j @Component @RequiredArgsConstructor diff --git a/src/main/java/until/the/eternity/auctionhistory/application/service/persister/AuctionHistoryPersister.java b/src/main/java/until/the/eternity/auctionhistory/application/service/persister/AuctionHistoryPersister.java index 8112b804..ee426dd5 100644 --- a/src/main/java/until/the/eternity/auctionhistory/application/service/persister/AuctionHistoryPersister.java +++ b/src/main/java/until/the/eternity/auctionhistory/application/service/persister/AuctionHistoryPersister.java @@ -1,6 +1,5 @@ package until.the.eternity.auctionhistory.application.service.persister; -import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; @@ -11,6 +10,8 @@ import until.the.eternity.auctionhistory.interfaces.external.dto.OpenApiAuctionHistoryResponse; import until.the.eternity.common.enums.ItemCategory; +import java.util.List; + @Slf4j @RequiredArgsConstructor @Component diff --git a/src/main/java/until/the/eternity/auctionhistory/domain/entity/AuctionHistory.java b/src/main/java/until/the/eternity/auctionhistory/domain/entity/AuctionHistory.java index af355e14..6f4cd7d8 100644 --- a/src/main/java/until/the/eternity/auctionhistory/domain/entity/AuctionHistory.java +++ b/src/main/java/until/the/eternity/auctionhistory/domain/entity/AuctionHistory.java @@ -1,11 +1,12 @@ package until.the.eternity.auctionhistory.domain.entity; import jakarta.persistence.*; -import java.time.Instant; -import java.util.List; import lombok.*; import until.the.eternity.auctionitemoption.domain.entity.AuctionHistoryItemOption; +import java.time.Instant; +import java.util.List; + @Entity @Table(name = "auction_history") @Getter diff --git a/src/main/java/until/the/eternity/auctionhistory/domain/event/AuctionHistorySavedEvent.java b/src/main/java/until/the/eternity/auctionhistory/domain/event/AuctionHistorySavedEvent.java index 213706e6..fe8f771f 100644 --- a/src/main/java/until/the/eternity/auctionhistory/domain/event/AuctionHistorySavedEvent.java +++ b/src/main/java/until/the/eternity/auctionhistory/domain/event/AuctionHistorySavedEvent.java @@ -1,8 +1,9 @@ package until.the.eternity.auctionhistory.domain.event; -import java.time.LocalDateTime; import lombok.Getter; +import java.time.LocalDateTime; + /** 경매장 거래 내역 저장 완료 이벤트 AuctionHistoryScheduler가 거래 내역을 성공적으로 저장한 후 발행됩니다. */ @Getter public class AuctionHistorySavedEvent { diff --git a/src/main/java/until/the/eternity/auctionhistory/domain/mapper/AuctionHistoryMapper.java b/src/main/java/until/the/eternity/auctionhistory/domain/mapper/AuctionHistoryMapper.java index 93a6fcaa..083ea00f 100644 --- a/src/main/java/until/the/eternity/auctionhistory/domain/mapper/AuctionHistoryMapper.java +++ b/src/main/java/until/the/eternity/auctionhistory/domain/mapper/AuctionHistoryMapper.java @@ -1,6 +1,5 @@ package until.the.eternity.auctionhistory.domain.mapper; -import java.util.List; import org.mapstruct.Mapper; import org.mapstruct.Mapping; import until.the.eternity.auctionhistory.domain.entity.AuctionHistory; @@ -8,6 +7,8 @@ import until.the.eternity.auctionhistory.interfaces.rest.dto.response.ItemOptionResponse; import until.the.eternity.auctionitemoption.domain.entity.AuctionHistoryItemOption; +import java.util.List; + /** * AuctionHistory Entity to internal.responseDto transfer mapper class 데이터 흐름은 external.responseDto * -> entity -> internal.responseDto 단방향으로 흐름 diff --git a/src/main/java/until/the/eternity/auctionhistory/domain/mapper/OpenApiAuctionHistoryMapper.java b/src/main/java/until/the/eternity/auctionhistory/domain/mapper/OpenApiAuctionHistoryMapper.java index f728374e..7df5d25b 100644 --- a/src/main/java/until/the/eternity/auctionhistory/domain/mapper/OpenApiAuctionHistoryMapper.java +++ b/src/main/java/until/the/eternity/auctionhistory/domain/mapper/OpenApiAuctionHistoryMapper.java @@ -1,13 +1,14 @@ package until.the.eternity.auctionhistory.domain.mapper; -import java.time.Instant; -import java.time.temporal.ChronoUnit; -import java.util.List; import org.mapstruct.*; import until.the.eternity.auctionhistory.domain.entity.AuctionHistory; import until.the.eternity.auctionhistory.interfaces.external.dto.OpenApiAuctionHistoryResponse; import until.the.eternity.common.enums.ItemCategory; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.List; + @Mapper(componentModel = "spring", uses = OpenApiItemOptionMapper.class) public interface OpenApiAuctionHistoryMapper { diff --git a/src/main/java/until/the/eternity/auctionhistory/domain/mapper/OpenApiItemOptionMapper.java b/src/main/java/until/the/eternity/auctionhistory/domain/mapper/OpenApiItemOptionMapper.java index 09f98a14..43776379 100644 --- a/src/main/java/until/the/eternity/auctionhistory/domain/mapper/OpenApiItemOptionMapper.java +++ b/src/main/java/until/the/eternity/auctionhistory/domain/mapper/OpenApiItemOptionMapper.java @@ -1,7 +1,5 @@ package until.the.eternity.auctionhistory.domain.mapper; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import org.mapstruct.AfterMapping; import org.mapstruct.Mapper; import org.mapstruct.Mapping; @@ -9,6 +7,9 @@ import until.the.eternity.auctionitemoption.domain.dto.external.OpenApiAuctionItemOptionResponse; import until.the.eternity.auctionitemoption.domain.entity.AuctionHistoryItemOption; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + @Mapper(componentModel = "spring") public interface OpenApiItemOptionMapper { diff --git a/src/main/java/until/the/eternity/auctionhistory/domain/repository/AuctionHistoryRepositoryPort.java b/src/main/java/until/the/eternity/auctionhistory/domain/repository/AuctionHistoryRepositoryPort.java index ca490fa1..f8a19073 100644 --- a/src/main/java/until/the/eternity/auctionhistory/domain/repository/AuctionHistoryRepositoryPort.java +++ b/src/main/java/until/the/eternity/auctionhistory/domain/repository/AuctionHistoryRepositoryPort.java @@ -1,15 +1,16 @@ package until.the.eternity.auctionhistory.domain.repository; -import java.time.Instant; -import java.util.List; -import java.util.Optional; -import java.util.Set; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import until.the.eternity.auctionhistory.domain.entity.AuctionHistory; import until.the.eternity.auctionhistory.interfaces.rest.dto.request.AuctionHistorySearchRequest; import until.the.eternity.common.enums.ItemCategory; +import java.time.Instant; +import java.util.List; +import java.util.Optional; +import java.util.Set; + /** 경매장 거래 내역 POJO Repository - Mock 또는 Stub 으로 대체해 단위 테스트 용이성 확보 */ public interface AuctionHistoryRepositoryPort { diff --git a/src/main/java/until/the/eternity/auctionhistory/domain/service/AuctionHistoryDuplicateChecker.java b/src/main/java/until/the/eternity/auctionhistory/domain/service/AuctionHistoryDuplicateChecker.java index 70b50dc2..bdb29aaf 100644 --- a/src/main/java/until/the/eternity/auctionhistory/domain/service/AuctionHistoryDuplicateChecker.java +++ b/src/main/java/until/the/eternity/auctionhistory/domain/service/AuctionHistoryDuplicateChecker.java @@ -1,9 +1,5 @@ package until.the.eternity.auctionhistory.domain.service; -import java.time.Instant; -import java.util.List; -import java.util.OptionalInt; -import java.util.Set; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; @@ -12,6 +8,11 @@ import until.the.eternity.auctionhistory.interfaces.external.dto.OpenApiAuctionHistoryResponse; import until.the.eternity.common.enums.ItemCategory; +import java.time.Instant; +import java.util.List; +import java.util.OptionalInt; +import java.util.Set; + @Slf4j @Component @RequiredArgsConstructor diff --git a/src/main/java/until/the/eternity/auctionhistory/domain/service/fetcher/AuctionHistoryFetcherPort.java b/src/main/java/until/the/eternity/auctionhistory/domain/service/fetcher/AuctionHistoryFetcherPort.java index 5b649be4..cf00412a 100644 --- a/src/main/java/until/the/eternity/auctionhistory/domain/service/fetcher/AuctionHistoryFetcherPort.java +++ b/src/main/java/until/the/eternity/auctionhistory/domain/service/fetcher/AuctionHistoryFetcherPort.java @@ -1,9 +1,10 @@ package until.the.eternity.auctionhistory.domain.service.fetcher; -import java.util.List; import until.the.eternity.auctionhistory.interfaces.external.dto.OpenApiAuctionHistoryResponse; import until.the.eternity.common.enums.ItemCategory; +import java.util.List; + public interface AuctionHistoryFetcherPort { List fetch(ItemCategory category); } diff --git a/src/main/java/until/the/eternity/auctionhistory/domain/service/persister/AuctionHistoryPersisterPort.java b/src/main/java/until/the/eternity/auctionhistory/domain/service/persister/AuctionHistoryPersisterPort.java index 636f4c2d..44f59205 100644 --- a/src/main/java/until/the/eternity/auctionhistory/domain/service/persister/AuctionHistoryPersisterPort.java +++ b/src/main/java/until/the/eternity/auctionhistory/domain/service/persister/AuctionHistoryPersisterPort.java @@ -1,10 +1,11 @@ package until.the.eternity.auctionhistory.domain.service.persister; -import java.util.List; import until.the.eternity.auctionhistory.domain.entity.AuctionHistory; import until.the.eternity.auctionhistory.interfaces.external.dto.OpenApiAuctionHistoryResponse; import until.the.eternity.common.enums.ItemCategory; +import java.util.List; + public interface AuctionHistoryPersisterPort { List filterOutExisting( diff --git a/src/main/java/until/the/eternity/auctionhistory/infrastructure/persistence/AuctionHistoryJpaRepository.java b/src/main/java/until/the/eternity/auctionhistory/infrastructure/persistence/AuctionHistoryJpaRepository.java index 4215b1f2..bdb982ab 100644 --- a/src/main/java/until/the/eternity/auctionhistory/infrastructure/persistence/AuctionHistoryJpaRepository.java +++ b/src/main/java/until/the/eternity/auctionhistory/infrastructure/persistence/AuctionHistoryJpaRepository.java @@ -1,8 +1,5 @@ package until.the.eternity.auctionhistory.infrastructure.persistence; -import java.time.Instant; -import java.util.List; -import java.util.Optional; import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; @@ -10,6 +7,10 @@ import org.springframework.stereotype.Repository; import until.the.eternity.auctionhistory.domain.entity.AuctionHistory; +import java.time.Instant; +import java.util.List; +import java.util.Optional; + @Repository public interface AuctionHistoryJpaRepository extends JpaRepository, JpaSpecificationExecutor { diff --git a/src/main/java/until/the/eternity/auctionhistory/infrastructure/persistence/AuctionHistoryQueryDslRepository.java b/src/main/java/until/the/eternity/auctionhistory/infrastructure/persistence/AuctionHistoryQueryDslRepository.java index cdb13289..d5a86889 100644 --- a/src/main/java/until/the/eternity/auctionhistory/infrastructure/persistence/AuctionHistoryQueryDslRepository.java +++ b/src/main/java/until/the/eternity/auctionhistory/infrastructure/persistence/AuctionHistoryQueryDslRepository.java @@ -8,11 +8,6 @@ import com.querydsl.core.types.dsl.NumberTemplate; import com.querydsl.jpa.JPAExpressions; import com.querydsl.jpa.impl.JPAQueryFactory; -import java.time.Instant; -import java.time.LocalDate; -import java.time.ZoneId; -import java.util.ArrayList; -import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; @@ -28,6 +23,12 @@ import until.the.eternity.auctionhistory.interfaces.rest.dto.request.PriceSearchRequest; import until.the.eternity.auctionitemoption.domain.entity.QAuctionHistoryItemOption; +import java.time.Instant; +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.List; + @Component @RequiredArgsConstructor class AuctionHistoryQueryDslRepository { diff --git a/src/main/java/until/the/eternity/auctionhistory/infrastructure/persistence/AuctionHistoryRepositoryPortImpl.java b/src/main/java/until/the/eternity/auctionhistory/infrastructure/persistence/AuctionHistoryRepositoryPortImpl.java index 2b0cdf4d..65428ed2 100644 --- a/src/main/java/until/the/eternity/auctionhistory/infrastructure/persistence/AuctionHistoryRepositoryPortImpl.java +++ b/src/main/java/until/the/eternity/auctionhistory/infrastructure/persistence/AuctionHistoryRepositoryPortImpl.java @@ -1,10 +1,6 @@ package until.the.eternity.auctionhistory.infrastructure.persistence; import jakarta.persistence.EntityManager; -import java.time.Instant; -import java.util.HashSet; -import java.util.List; -import java.util.Optional; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.domain.Page; @@ -16,6 +12,11 @@ import until.the.eternity.auctionhistory.interfaces.rest.dto.request.AuctionHistorySearchRequest; import until.the.eternity.common.enums.ItemCategory; +import java.time.Instant; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; + /** AuctionHistoryRepository Interface 구현체 */ @Repository @RequiredArgsConstructor diff --git a/src/main/java/until/the/eternity/auctionhistory/interfaces/external/dto/OpenApiAuctionHistoryListResponse.java b/src/main/java/until/the/eternity/auctionhistory/interfaces/external/dto/OpenApiAuctionHistoryListResponse.java index e194aeb7..c49a6b2b 100644 --- a/src/main/java/until/the/eternity/auctionhistory/interfaces/external/dto/OpenApiAuctionHistoryListResponse.java +++ b/src/main/java/until/the/eternity/auctionhistory/interfaces/external/dto/OpenApiAuctionHistoryListResponse.java @@ -1,6 +1,7 @@ package until.the.eternity.auctionhistory.interfaces.external.dto; import com.fasterxml.jackson.annotation.JsonProperty; + import java.util.List; public record OpenApiAuctionHistoryListResponse( diff --git a/src/main/java/until/the/eternity/auctionhistory/interfaces/external/dto/OpenApiAuctionHistoryResponse.java b/src/main/java/until/the/eternity/auctionhistory/interfaces/external/dto/OpenApiAuctionHistoryResponse.java index f41eb460..65b73ca5 100644 --- a/src/main/java/until/the/eternity/auctionhistory/interfaces/external/dto/OpenApiAuctionHistoryResponse.java +++ b/src/main/java/until/the/eternity/auctionhistory/interfaces/external/dto/OpenApiAuctionHistoryResponse.java @@ -2,9 +2,10 @@ import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonProperty; +import until.the.eternity.auctionitemoption.domain.dto.external.OpenApiAuctionItemOptionResponse; + import java.time.Instant; import java.util.List; -import until.the.eternity.auctionitemoption.domain.dto.external.OpenApiAuctionItemOptionResponse; public record OpenApiAuctionHistoryResponse( @JsonProperty("item_name") String itemName, diff --git a/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/enums/SearchStandard.java b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/enums/SearchStandard.java index ef005676..feb2c35e 100644 --- a/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/enums/SearchStandard.java +++ b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/enums/SearchStandard.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonValue; import io.swagger.v3.oas.annotations.media.Schema; + import java.util.Arrays; /** 검색 기준 (이상/이하/같음) */ diff --git a/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/enums/SortDirection.java b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/enums/SortDirection.java index 82666061..e6aacf98 100644 --- a/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/enums/SortDirection.java +++ b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/enums/SortDirection.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonValue; import io.swagger.v3.oas.annotations.media.Schema; + import java.util.Arrays; /** 정렬 방향 (오름차순/내림차순) */ diff --git a/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/response/AuctionHistoryDetailResponse.java b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/response/AuctionHistoryDetailResponse.java index 082c562f..d694c60c 100644 --- a/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/response/AuctionHistoryDetailResponse.java +++ b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/response/AuctionHistoryDetailResponse.java @@ -1,6 +1,7 @@ package until.the.eternity.auctionhistory.interfaces.rest.dto.response; import com.fasterxml.jackson.annotation.JsonFormat; + import java.time.Instant; import java.util.List; diff --git a/src/main/java/until/the/eternity/auctionitem/domain/entity/AuctionRealtimeItem.java b/src/main/java/until/the/eternity/auctionitem/domain/entity/AuctionRealtimeItem.java index ff498a80..d93c2eab 100644 --- a/src/main/java/until/the/eternity/auctionitem/domain/entity/AuctionRealtimeItem.java +++ b/src/main/java/until/the/eternity/auctionitem/domain/entity/AuctionRealtimeItem.java @@ -1,9 +1,10 @@ package until.the.eternity.auctionitem.domain.entity; import jakarta.persistence.*; +import lombok.*; + import java.time.Instant; import java.util.List; -import lombok.*; /** 실시간 경매장에서 판매 중인 아이템 정보. V15 마이그레이션에서 auction_item → auction_realtime_item으로 변경됨. */ @Entity diff --git a/src/main/java/until/the/eternity/auctionitem/domain/entity/AuctionRealtimeItemOption.java b/src/main/java/until/the/eternity/auctionitem/domain/entity/AuctionRealtimeItemOption.java index b0a651bf..28ba2905 100644 --- a/src/main/java/until/the/eternity/auctionitem/domain/entity/AuctionRealtimeItemOption.java +++ b/src/main/java/until/the/eternity/auctionitem/domain/entity/AuctionRealtimeItemOption.java @@ -1,12 +1,13 @@ package until.the.eternity.auctionitem.domain.entity; import jakarta.persistence.*; -import java.util.UUID; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import java.util.UUID; + /** 실시간 경매장 아이템(auction_realtime_item)에 연결된 아이템 옵션 정보. */ @Entity @Table(name = "auction_realtime_item_option") diff --git a/src/main/java/until/the/eternity/auctionitemoption/domain/entity/AuctionHistoryItemOption.java b/src/main/java/until/the/eternity/auctionitemoption/domain/entity/AuctionHistoryItemOption.java index dddac912..332605de 100644 --- a/src/main/java/until/the/eternity/auctionitemoption/domain/entity/AuctionHistoryItemOption.java +++ b/src/main/java/until/the/eternity/auctionitemoption/domain/entity/AuctionHistoryItemOption.java @@ -1,13 +1,14 @@ package until.the.eternity.auctionitemoption.domain.entity; import jakarta.persistence.*; -import java.util.UUID; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import until.the.eternity.auctionhistory.domain.entity.AuctionHistory; +import java.util.UUID; + /** * 경매장 거래 내역(auction_history)에 연결된 아이템 옵션 정보. V15 마이그레이션에서 auction_item_option → * auction_history_item_option으로 변경됨. diff --git a/src/main/java/until/the/eternity/auctionrealtime/application/scheduler/AuctionRealtimeScheduler.java b/src/main/java/until/the/eternity/auctionrealtime/application/scheduler/AuctionRealtimeScheduler.java index 1857f917..a854ddd8 100644 --- a/src/main/java/until/the/eternity/auctionrealtime/application/scheduler/AuctionRealtimeScheduler.java +++ b/src/main/java/until/the/eternity/auctionrealtime/application/scheduler/AuctionRealtimeScheduler.java @@ -1,12 +1,5 @@ package until.the.eternity.auctionrealtime.application.scheduler; -import java.time.Instant; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; @@ -19,6 +12,10 @@ import until.the.eternity.auctionrealtime.domain.service.fetcher.AuctionRealtimeFetcherPort.FetchResult; import until.the.eternity.common.enums.ItemCategory; +import java.time.Instant; +import java.util.*; +import java.util.stream.Collectors; + /** * 실시간 경매장 데이터 수집 스케줄러. * diff --git a/src/main/java/until/the/eternity/auctionrealtime/application/service/AuctionRealtimeService.java b/src/main/java/until/the/eternity/auctionrealtime/application/service/AuctionRealtimeService.java index 8aacbe22..1042d372 100644 --- a/src/main/java/until/the/eternity/auctionrealtime/application/service/AuctionRealtimeService.java +++ b/src/main/java/until/the/eternity/auctionrealtime/application/service/AuctionRealtimeService.java @@ -1,7 +1,5 @@ package until.the.eternity.auctionrealtime.application.service; -import java.time.Instant; -import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Page; @@ -17,6 +15,9 @@ import until.the.eternity.common.enums.ItemCategory; import until.the.eternity.common.response.PageResponseDto; +import java.time.Instant; +import java.util.List; + /** 실시간 경매장 데이터 Service. */ @Slf4j @Service diff --git a/src/main/java/until/the/eternity/auctionrealtime/application/service/fetcher/AuctionRealtimeFetcher.java b/src/main/java/until/the/eternity/auctionrealtime/application/service/fetcher/AuctionRealtimeFetcher.java index 1bd26535..b14b73b3 100644 --- a/src/main/java/until/the/eternity/auctionrealtime/application/service/fetcher/AuctionRealtimeFetcher.java +++ b/src/main/java/until/the/eternity/auctionrealtime/application/service/fetcher/AuctionRealtimeFetcher.java @@ -1,8 +1,5 @@ package until.the.eternity.auctionrealtime.application.service.fetcher; -import java.time.Instant; -import java.util.ArrayList; -import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; @@ -14,6 +11,10 @@ import until.the.eternity.auctionrealtime.interfaces.external.dto.OpenApiAuctionRealtimeResponse; import until.the.eternity.common.enums.ItemCategory; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; + /** 실시간 경매장 데이터 Fetcher 구현체. Cursor 기반 페이징으로 API를 호출하고, 중복 감지 시 호출을 중단한다. */ @Slf4j @Component diff --git a/src/main/java/until/the/eternity/auctionrealtime/application/service/persister/AuctionRealtimePersister.java b/src/main/java/until/the/eternity/auctionrealtime/application/service/persister/AuctionRealtimePersister.java index 8132c675..41731e35 100644 --- a/src/main/java/until/the/eternity/auctionrealtime/application/service/persister/AuctionRealtimePersister.java +++ b/src/main/java/until/the/eternity/auctionrealtime/application/service/persister/AuctionRealtimePersister.java @@ -1,7 +1,5 @@ package until.the.eternity.auctionrealtime.application.service.persister; -import java.time.Instant; -import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; @@ -12,6 +10,9 @@ import until.the.eternity.auctionrealtime.interfaces.external.dto.OpenApiAuctionRealtimeResponse; import until.the.eternity.common.enums.ItemCategory; +import java.time.Instant; +import java.util.List; + /** 실시간 경매장 데이터 Persister 구현체. */ @Slf4j @RequiredArgsConstructor diff --git a/src/main/java/until/the/eternity/auctionrealtime/domain/mapper/AuctionRealtimeMapper.java b/src/main/java/until/the/eternity/auctionrealtime/domain/mapper/AuctionRealtimeMapper.java index e373ae9c..f292d803 100644 --- a/src/main/java/until/the/eternity/auctionrealtime/domain/mapper/AuctionRealtimeMapper.java +++ b/src/main/java/until/the/eternity/auctionrealtime/domain/mapper/AuctionRealtimeMapper.java @@ -1,6 +1,5 @@ package until.the.eternity.auctionrealtime.domain.mapper; -import java.util.List; import org.mapstruct.Mapper; import org.mapstruct.Mapping; import until.the.eternity.auctionitem.domain.entity.AuctionRealtimeItem; @@ -8,6 +7,8 @@ import until.the.eternity.auctionrealtime.interfaces.rest.dto.response.AuctionRealtimeDetailResponse; import until.the.eternity.auctionrealtime.interfaces.rest.dto.response.RealtimeItemOptionResponse; +import java.util.List; + /** AuctionRealtimeItem Entity to DTO mapper class. */ @Mapper(componentModel = "spring") public interface AuctionRealtimeMapper { diff --git a/src/main/java/until/the/eternity/auctionrealtime/domain/mapper/OpenApiAuctionRealtimeMapper.java b/src/main/java/until/the/eternity/auctionrealtime/domain/mapper/OpenApiAuctionRealtimeMapper.java index 5e005637..8f65de6b 100644 --- a/src/main/java/until/the/eternity/auctionrealtime/domain/mapper/OpenApiAuctionRealtimeMapper.java +++ b/src/main/java/until/the/eternity/auctionrealtime/domain/mapper/OpenApiAuctionRealtimeMapper.java @@ -1,19 +1,14 @@ package until.the.eternity.auctionrealtime.domain.mapper; -import java.time.Instant; -import java.time.temporal.ChronoUnit; -import java.util.List; -import org.mapstruct.AfterMapping; -import org.mapstruct.Context; -import org.mapstruct.IterableMapping; -import org.mapstruct.Mapper; -import org.mapstruct.Mapping; -import org.mapstruct.MappingTarget; -import org.mapstruct.Named; +import org.mapstruct.*; import until.the.eternity.auctionitem.domain.entity.AuctionRealtimeItem; import until.the.eternity.auctionrealtime.interfaces.external.dto.OpenApiAuctionRealtimeResponse; import until.the.eternity.common.enums.ItemCategory; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.List; + /** OpenApiAuctionRealtimeResponse → AuctionRealtimeItem Entity 변환 Mapper. */ @Mapper(componentModel = "spring", uses = OpenApiRealtimeItemOptionMapper.class) public interface OpenApiAuctionRealtimeMapper { diff --git a/src/main/java/until/the/eternity/auctionrealtime/domain/mapper/OpenApiRealtimeItemOptionMapper.java b/src/main/java/until/the/eternity/auctionrealtime/domain/mapper/OpenApiRealtimeItemOptionMapper.java index 91bbb183..ae30e87f 100644 --- a/src/main/java/until/the/eternity/auctionrealtime/domain/mapper/OpenApiRealtimeItemOptionMapper.java +++ b/src/main/java/until/the/eternity/auctionrealtime/domain/mapper/OpenApiRealtimeItemOptionMapper.java @@ -1,7 +1,5 @@ package until.the.eternity.auctionrealtime.domain.mapper; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import org.mapstruct.AfterMapping; import org.mapstruct.Mapper; import org.mapstruct.Mapping; @@ -9,6 +7,9 @@ import until.the.eternity.auctionitem.domain.entity.AuctionRealtimeItemOption; import until.the.eternity.auctionitemoption.domain.dto.external.OpenApiAuctionItemOptionResponse; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + /** OpenApiAuctionItemOptionResponse → AuctionRealtimeItemOption Entity 변환 Mapper. */ @Mapper(componentModel = "spring") public interface OpenApiRealtimeItemOptionMapper { diff --git a/src/main/java/until/the/eternity/auctionrealtime/domain/repository/AuctionRealtimeItemRepositoryPort.java b/src/main/java/until/the/eternity/auctionrealtime/domain/repository/AuctionRealtimeItemRepositoryPort.java index 56513424..d11c7408 100644 --- a/src/main/java/until/the/eternity/auctionrealtime/domain/repository/AuctionRealtimeItemRepositoryPort.java +++ b/src/main/java/until/the/eternity/auctionrealtime/domain/repository/AuctionRealtimeItemRepositoryPort.java @@ -1,14 +1,15 @@ package until.the.eternity.auctionrealtime.domain.repository; -import java.time.Instant; -import java.util.List; -import java.util.Optional; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import until.the.eternity.auctionitem.domain.entity.AuctionRealtimeItem; import until.the.eternity.auctionrealtime.interfaces.rest.dto.request.AuctionRealtimeSearchRequest; import until.the.eternity.common.enums.ItemCategory; +import java.time.Instant; +import java.util.List; +import java.util.Optional; + /** AuctionRealtimeItem Repository Port (Hexagonal Architecture). */ public interface AuctionRealtimeItemRepositoryPort { diff --git a/src/main/java/until/the/eternity/auctionrealtime/domain/service/AuctionRealtimeDuplicateChecker.java b/src/main/java/until/the/eternity/auctionrealtime/domain/service/AuctionRealtimeDuplicateChecker.java index 54c24f6d..09637f55 100644 --- a/src/main/java/until/the/eternity/auctionrealtime/domain/service/AuctionRealtimeDuplicateChecker.java +++ b/src/main/java/until/the/eternity/auctionrealtime/domain/service/AuctionRealtimeDuplicateChecker.java @@ -1,8 +1,5 @@ package until.the.eternity.auctionrealtime.domain.service; -import java.time.Instant; -import java.util.List; -import java.util.Optional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; @@ -10,6 +7,10 @@ import until.the.eternity.auctionrealtime.interfaces.external.dto.OpenApiAuctionRealtimeResponse; import until.the.eternity.common.enums.ItemCategory; +import java.time.Instant; +import java.util.List; +import java.util.Optional; + /** * 실시간 경매장 데이터의 중복 체크 로직. * diff --git a/src/main/java/until/the/eternity/auctionrealtime/domain/service/fetcher/AuctionRealtimeFetcherPort.java b/src/main/java/until/the/eternity/auctionrealtime/domain/service/fetcher/AuctionRealtimeFetcherPort.java index 8f31c645..da3a9c02 100644 --- a/src/main/java/until/the/eternity/auctionrealtime/domain/service/fetcher/AuctionRealtimeFetcherPort.java +++ b/src/main/java/until/the/eternity/auctionrealtime/domain/service/fetcher/AuctionRealtimeFetcherPort.java @@ -1,10 +1,11 @@ package until.the.eternity.auctionrealtime.domain.service.fetcher; -import java.time.Instant; -import java.util.List; import until.the.eternity.auctionrealtime.interfaces.external.dto.OpenApiAuctionRealtimeResponse; import until.the.eternity.common.enums.ItemCategory; +import java.time.Instant; +import java.util.List; + /** 실시간 경매장 데이터 Fetcher Port. */ public interface AuctionRealtimeFetcherPort { diff --git a/src/main/java/until/the/eternity/auctionrealtime/domain/service/persister/AuctionRealtimePersisterPort.java b/src/main/java/until/the/eternity/auctionrealtime/domain/service/persister/AuctionRealtimePersisterPort.java index 265327f7..cadc56f1 100644 --- a/src/main/java/until/the/eternity/auctionrealtime/domain/service/persister/AuctionRealtimePersisterPort.java +++ b/src/main/java/until/the/eternity/auctionrealtime/domain/service/persister/AuctionRealtimePersisterPort.java @@ -1,11 +1,12 @@ package until.the.eternity.auctionrealtime.domain.service.persister; -import java.time.Instant; -import java.util.List; import until.the.eternity.auctionitem.domain.entity.AuctionRealtimeItem; import until.the.eternity.auctionrealtime.interfaces.external.dto.OpenApiAuctionRealtimeResponse; import until.the.eternity.common.enums.ItemCategory; +import java.time.Instant; +import java.util.List; + /** 실시간 경매장 데이터 Persister Port. */ public interface AuctionRealtimePersisterPort { diff --git a/src/main/java/until/the/eternity/auctionrealtime/infrastructure/persistence/AuctionRealtimeItemRepository.java b/src/main/java/until/the/eternity/auctionrealtime/infrastructure/persistence/AuctionRealtimeItemRepository.java index 399678c1..d247cd44 100644 --- a/src/main/java/until/the/eternity/auctionrealtime/infrastructure/persistence/AuctionRealtimeItemRepository.java +++ b/src/main/java/until/the/eternity/auctionrealtime/infrastructure/persistence/AuctionRealtimeItemRepository.java @@ -1,13 +1,14 @@ package until.the.eternity.auctionrealtime.infrastructure.persistence; -import java.time.Instant; -import java.util.Optional; 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 until.the.eternity.auctionitem.domain.entity.AuctionRealtimeItem; +import java.time.Instant; +import java.util.Optional; + /** AuctionRealtimeItem JPA Repository. */ public interface AuctionRealtimeItemRepository extends JpaRepository { diff --git a/src/main/java/until/the/eternity/auctionrealtime/infrastructure/persistence/AuctionRealtimeItemRepositoryPortImpl.java b/src/main/java/until/the/eternity/auctionrealtime/infrastructure/persistence/AuctionRealtimeItemRepositoryPortImpl.java index 4e9fca60..3d88f07b 100644 --- a/src/main/java/until/the/eternity/auctionrealtime/infrastructure/persistence/AuctionRealtimeItemRepositoryPortImpl.java +++ b/src/main/java/until/the/eternity/auctionrealtime/infrastructure/persistence/AuctionRealtimeItemRepositoryPortImpl.java @@ -1,9 +1,6 @@ package until.the.eternity.auctionrealtime.infrastructure.persistence; import jakarta.persistence.EntityManager; -import java.time.Instant; -import java.util.List; -import java.util.Optional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Page; @@ -14,6 +11,10 @@ import until.the.eternity.auctionrealtime.interfaces.rest.dto.request.AuctionRealtimeSearchRequest; import until.the.eternity.common.enums.ItemCategory; +import java.time.Instant; +import java.util.List; +import java.util.Optional; + /** AuctionRealtimeItemRepositoryPort 구현체. */ @Slf4j @Repository diff --git a/src/main/java/until/the/eternity/auctionrealtime/infrastructure/persistence/AuctionRealtimeQueryDslRepository.java b/src/main/java/until/the/eternity/auctionrealtime/infrastructure/persistence/AuctionRealtimeQueryDslRepository.java index 1e136f8a..d3e25e59 100644 --- a/src/main/java/until/the/eternity/auctionrealtime/infrastructure/persistence/AuctionRealtimeQueryDslRepository.java +++ b/src/main/java/until/the/eternity/auctionrealtime/infrastructure/persistence/AuctionRealtimeQueryDslRepository.java @@ -8,8 +8,6 @@ import com.querydsl.core.types.dsl.NumberTemplate; import com.querydsl.jpa.JPAExpressions; import com.querydsl.jpa.impl.JPAQueryFactory; -import java.util.ArrayList; -import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; @@ -24,6 +22,9 @@ import until.the.eternity.auctionitem.domain.entity.QAuctionRealtimeItemOption; import until.the.eternity.auctionrealtime.interfaces.rest.dto.request.AuctionRealtimeSearchRequest; +import java.util.ArrayList; +import java.util.List; + @Component @RequiredArgsConstructor class AuctionRealtimeQueryDslRepository { diff --git a/src/main/java/until/the/eternity/auctionrealtime/interfaces/external/dto/OpenApiAuctionRealtimeListResponse.java b/src/main/java/until/the/eternity/auctionrealtime/interfaces/external/dto/OpenApiAuctionRealtimeListResponse.java index 980fc9e3..114de80e 100644 --- a/src/main/java/until/the/eternity/auctionrealtime/interfaces/external/dto/OpenApiAuctionRealtimeListResponse.java +++ b/src/main/java/until/the/eternity/auctionrealtime/interfaces/external/dto/OpenApiAuctionRealtimeListResponse.java @@ -1,6 +1,7 @@ package until.the.eternity.auctionrealtime.interfaces.external.dto; import com.fasterxml.jackson.annotation.JsonProperty; + import java.util.List; /** Nexon Open API /auction/list 응답 리스트 DTO. */ diff --git a/src/main/java/until/the/eternity/auctionrealtime/interfaces/external/dto/OpenApiAuctionRealtimeResponse.java b/src/main/java/until/the/eternity/auctionrealtime/interfaces/external/dto/OpenApiAuctionRealtimeResponse.java index 28d59235..2b337bfe 100644 --- a/src/main/java/until/the/eternity/auctionrealtime/interfaces/external/dto/OpenApiAuctionRealtimeResponse.java +++ b/src/main/java/until/the/eternity/auctionrealtime/interfaces/external/dto/OpenApiAuctionRealtimeResponse.java @@ -2,9 +2,10 @@ import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonProperty; +import until.the.eternity.auctionitemoption.domain.dto.external.OpenApiAuctionItemOptionResponse; + import java.time.Instant; import java.util.List; -import until.the.eternity.auctionitemoption.domain.dto.external.OpenApiAuctionItemOptionResponse; /** Nexon Open API /auction/list 응답 DTO. 현재 경매장에서 판매 중인 아이템 정보. */ public record OpenApiAuctionRealtimeResponse( diff --git a/src/main/java/until/the/eternity/auctionrealtime/interfaces/rest/dto/request/RealtimeSortField.java b/src/main/java/until/the/eternity/auctionrealtime/interfaces/rest/dto/request/RealtimeSortField.java index 5151f8a4..7d56e88d 100644 --- a/src/main/java/until/the/eternity/auctionrealtime/interfaces/rest/dto/request/RealtimeSortField.java +++ b/src/main/java/until/the/eternity/auctionrealtime/interfaces/rest/dto/request/RealtimeSortField.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonValue; import io.swagger.v3.oas.annotations.media.Schema; + import java.util.Arrays; /** 실시간 경매장 정렬 필드 */ diff --git a/src/main/java/until/the/eternity/auctionrealtime/interfaces/rest/dto/response/AuctionRealtimeDetailResponse.java b/src/main/java/until/the/eternity/auctionrealtime/interfaces/rest/dto/response/AuctionRealtimeDetailResponse.java index ba184764..a3d5a9e0 100644 --- a/src/main/java/until/the/eternity/auctionrealtime/interfaces/rest/dto/response/AuctionRealtimeDetailResponse.java +++ b/src/main/java/until/the/eternity/auctionrealtime/interfaces/rest/dto/response/AuctionRealtimeDetailResponse.java @@ -1,6 +1,7 @@ package until.the.eternity.auctionrealtime.interfaces.rest.dto.response; import com.fasterxml.jackson.annotation.JsonFormat; + import java.time.Instant; import java.util.List; diff --git a/src/main/java/until/the/eternity/auctionsearchoption/application/service/AuctionSearchOptionService.java b/src/main/java/until/the/eternity/auctionsearchoption/application/service/AuctionSearchOptionService.java index aec4e464..28e70a27 100644 --- a/src/main/java/until/the/eternity/auctionsearchoption/application/service/AuctionSearchOptionService.java +++ b/src/main/java/until/the/eternity/auctionsearchoption/application/service/AuctionSearchOptionService.java @@ -2,8 +2,6 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; -import java.util.List; -import java.util.Map; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -13,6 +11,9 @@ import until.the.eternity.auctionsearchoption.interfaces.rest.dto.response.FieldMetadata; import until.the.eternity.auctionsearchoption.interfaces.rest.dto.response.SearchOptionMetadataResponse; +import java.util.List; +import java.util.Map; + @Slf4j @Service @RequiredArgsConstructor diff --git a/src/main/java/until/the/eternity/auctionsearchoption/domain/entity/AuctionSearchOptionMetadata.java b/src/main/java/until/the/eternity/auctionsearchoption/domain/entity/AuctionSearchOptionMetadata.java index 21a9e82b..5356db73 100644 --- a/src/main/java/until/the/eternity/auctionsearchoption/domain/entity/AuctionSearchOptionMetadata.java +++ b/src/main/java/until/the/eternity/auctionsearchoption/domain/entity/AuctionSearchOptionMetadata.java @@ -1,13 +1,14 @@ package until.the.eternity.auctionsearchoption.domain.entity; import jakarta.persistence.*; -import java.time.LocalDateTime; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; import org.hibernate.annotations.JdbcTypeCode; import org.hibernate.type.SqlTypes; +import java.time.LocalDateTime; + @Entity @Table(name = "auction_search_option_metadata") @Getter diff --git a/src/main/java/until/the/eternity/auctionsearchoption/domain/repository/AuctionSearchOptionRepositoryPort.java b/src/main/java/until/the/eternity/auctionsearchoption/domain/repository/AuctionSearchOptionRepositoryPort.java index 971be29e..fe4e89b0 100644 --- a/src/main/java/until/the/eternity/auctionsearchoption/domain/repository/AuctionSearchOptionRepositoryPort.java +++ b/src/main/java/until/the/eternity/auctionsearchoption/domain/repository/AuctionSearchOptionRepositoryPort.java @@ -1,8 +1,9 @@ package until.the.eternity.auctionsearchoption.domain.repository; -import java.util.List; import until.the.eternity.auctionsearchoption.domain.entity.AuctionSearchOptionMetadata; +import java.util.List; + public interface AuctionSearchOptionRepositoryPort { /** diff --git a/src/main/java/until/the/eternity/auctionsearchoption/infrastructure/persistence/AuctionSearchOptionJpaRepository.java b/src/main/java/until/the/eternity/auctionsearchoption/infrastructure/persistence/AuctionSearchOptionJpaRepository.java index e7074578..c18fd9e5 100644 --- a/src/main/java/until/the/eternity/auctionsearchoption/infrastructure/persistence/AuctionSearchOptionJpaRepository.java +++ b/src/main/java/until/the/eternity/auctionsearchoption/infrastructure/persistence/AuctionSearchOptionJpaRepository.java @@ -1,10 +1,11 @@ package until.the.eternity.auctionsearchoption.infrastructure.persistence; -import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import until.the.eternity.auctionsearchoption.domain.entity.AuctionSearchOptionMetadata; +import java.util.List; + @Repository interface AuctionSearchOptionJpaRepository extends JpaRepository { diff --git a/src/main/java/until/the/eternity/auctionsearchoption/infrastructure/persistence/AuctionSearchOptionRepositoryPortImpl.java b/src/main/java/until/the/eternity/auctionsearchoption/infrastructure/persistence/AuctionSearchOptionRepositoryPortImpl.java index 850f43f4..2440e821 100644 --- a/src/main/java/until/the/eternity/auctionsearchoption/infrastructure/persistence/AuctionSearchOptionRepositoryPortImpl.java +++ b/src/main/java/until/the/eternity/auctionsearchoption/infrastructure/persistence/AuctionSearchOptionRepositoryPortImpl.java @@ -1,11 +1,12 @@ package until.the.eternity.auctionsearchoption.infrastructure.persistence; -import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; import until.the.eternity.auctionsearchoption.domain.entity.AuctionSearchOptionMetadata; import until.the.eternity.auctionsearchoption.domain.repository.AuctionSearchOptionRepositoryPort; +import java.util.List; + @Component @RequiredArgsConstructor class AuctionSearchOptionRepositoryPortImpl implements AuctionSearchOptionRepositoryPort { diff --git a/src/main/java/until/the/eternity/auctionsearchoption/interfaces/rest/AuctionSearchOptionController.java b/src/main/java/until/the/eternity/auctionsearchoption/interfaces/rest/AuctionSearchOptionController.java index 8ad6484b..0231a36a 100644 --- a/src/main/java/until/the/eternity/auctionsearchoption/interfaces/rest/AuctionSearchOptionController.java +++ b/src/main/java/until/the/eternity/auctionsearchoption/interfaces/rest/AuctionSearchOptionController.java @@ -2,7 +2,6 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; -import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -12,6 +11,8 @@ import until.the.eternity.auctionsearchoption.interfaces.rest.dto.response.SearchOptionMetadataResponse; import until.the.eternity.common.response.ApiResponse; +import java.util.List; + @Tag(name = "Auction Search Option", description = "경매 검색 옵션 API") @RestController @RequestMapping("/api/search-option") diff --git a/src/main/java/until/the/eternity/auctionsearchoption/interfaces/rest/dto/response/FieldMetadata.java b/src/main/java/until/the/eternity/auctionsearchoption/interfaces/rest/dto/response/FieldMetadata.java index 8b8b6ca1..43e5d916 100644 --- a/src/main/java/until/the/eternity/auctionsearchoption/interfaces/rest/dto/response/FieldMetadata.java +++ b/src/main/java/until/the/eternity/auctionsearchoption/interfaces/rest/dto/response/FieldMetadata.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.annotation.JsonInclude; import io.swagger.v3.oas.annotations.media.Schema; + import java.util.List; @Schema(description = "검색 조건 필드 메타데이터") diff --git a/src/main/java/until/the/eternity/auctionsearchoption/interfaces/rest/dto/response/SearchOptionMetadataResponse.java b/src/main/java/until/the/eternity/auctionsearchoption/interfaces/rest/dto/response/SearchOptionMetadataResponse.java index 04581ef7..02c318fb 100644 --- a/src/main/java/until/the/eternity/auctionsearchoption/interfaces/rest/dto/response/SearchOptionMetadataResponse.java +++ b/src/main/java/until/the/eternity/auctionsearchoption/interfaces/rest/dto/response/SearchOptionMetadataResponse.java @@ -1,6 +1,7 @@ package until.the.eternity.auctionsearchoption.interfaces.rest.dto.response; import io.swagger.v3.oas.annotations.media.Schema; + import java.util.Map; @Schema(description = "검색 옵션 메타데이터 응답") diff --git a/src/main/java/until/the/eternity/common/enums/ItemCategory.java b/src/main/java/until/the/eternity/common/enums/ItemCategory.java index 6b8ab078..ca9ee313 100644 --- a/src/main/java/until/the/eternity/common/enums/ItemCategory.java +++ b/src/main/java/until/the/eternity/common/enums/ItemCategory.java @@ -1,11 +1,12 @@ package until.the.eternity.common.enums; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + import java.util.Arrays; import java.util.Map; import java.util.function.Function; import java.util.stream.Collectors; -import lombok.Getter; -import lombok.RequiredArgsConstructor; @Getter @RequiredArgsConstructor diff --git a/src/main/java/until/the/eternity/common/enums/SortDirection.java b/src/main/java/until/the/eternity/common/enums/SortDirection.java index 5e688da2..4a797556 100644 --- a/src/main/java/until/the/eternity/common/enums/SortDirection.java +++ b/src/main/java/until/the/eternity/common/enums/SortDirection.java @@ -3,9 +3,10 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonValue; import io.swagger.v3.oas.annotations.media.Schema; -import java.util.Arrays; import org.springframework.data.domain.Sort; +import java.util.Arrays; + /** 정렬 방향 (오름차순/내림차순) */ @Schema(description = "정렬 방향", enumAsRef = true) public enum SortDirection { diff --git a/src/main/java/until/the/eternity/common/enums/SortField.java b/src/main/java/until/the/eternity/common/enums/SortField.java index 704d79d5..3c26de2b 100644 --- a/src/main/java/until/the/eternity/common/enums/SortField.java +++ b/src/main/java/until/the/eternity/common/enums/SortField.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonValue; import io.swagger.v3.oas.annotations.media.Schema; + import java.util.Arrays; /** 정렬 필드 */ diff --git a/src/main/java/until/the/eternity/common/exception/GlobalExceptionCode.java b/src/main/java/until/the/eternity/common/exception/GlobalExceptionCode.java index 21c217b0..1169e930 100644 --- a/src/main/java/until/the/eternity/common/exception/GlobalExceptionCode.java +++ b/src/main/java/until/the/eternity/common/exception/GlobalExceptionCode.java @@ -1,11 +1,11 @@ package until.the.eternity.common.exception; -import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR; - import lombok.Getter; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; +import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR; + @Getter @RequiredArgsConstructor public enum GlobalExceptionCode implements ExceptionCode { diff --git a/src/main/java/until/the/eternity/common/exception/GlobalExceptionHandler.java b/src/main/java/until/the/eternity/common/exception/GlobalExceptionHandler.java index 4cf5af10..bc069d00 100644 --- a/src/main/java/until/the/eternity/common/exception/GlobalExceptionHandler.java +++ b/src/main/java/until/the/eternity/common/exception/GlobalExceptionHandler.java @@ -1,7 +1,5 @@ package until.the.eternity.common.exception; -import static until.the.eternity.common.exception.GlobalExceptionCode.SERVER_ERROR; - import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; @@ -9,6 +7,8 @@ import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; import until.the.eternity.common.response.ApiResponse; +import static until.the.eternity.common.exception.GlobalExceptionCode.SERVER_ERROR; + @Slf4j @RestControllerAdvice public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { diff --git a/src/main/java/until/the/eternity/common/filter/GatewayAuthFilter.java b/src/main/java/until/the/eternity/common/filter/GatewayAuthFilter.java index b293dc46..ef255e95 100644 --- a/src/main/java/until/the/eternity/common/filter/GatewayAuthFilter.java +++ b/src/main/java/until/the/eternity/common/filter/GatewayAuthFilter.java @@ -4,9 +4,6 @@ import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; import lombok.NonNull; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -19,6 +16,10 @@ import until.the.eternity.common.entity.CustomWebAuthenticationDetails; import until.the.eternity.common.util.IpAddressUtil; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + /** * Gateway에서 전달한 인증 헤더(X-Auth-*)를 기반으로 Spring Security의 Authentication을 생성하는 필터 * diff --git a/src/main/java/until/the/eternity/common/response/ApiResponse.java b/src/main/java/until/the/eternity/common/response/ApiResponse.java index c7f83b13..c7b99b00 100644 --- a/src/main/java/until/the/eternity/common/response/ApiResponse.java +++ b/src/main/java/until/the/eternity/common/response/ApiResponse.java @@ -1,9 +1,10 @@ package until.the.eternity.common.response; -import java.time.Instant; import lombok.Builder; import lombok.Getter; +import java.time.Instant; + @Getter public class ApiResponse { diff --git a/src/main/java/until/the/eternity/common/response/PageResponseDto.java b/src/main/java/until/the/eternity/common/response/PageResponseDto.java index c142faac..0919ce20 100644 --- a/src/main/java/until/the/eternity/common/response/PageResponseDto.java +++ b/src/main/java/until/the/eternity/common/response/PageResponseDto.java @@ -1,6 +1,7 @@ package until.the.eternity.common.response; import io.swagger.v3.oas.annotations.media.Schema; + import java.util.List; @Schema(description = "페이지 응답 객체") diff --git a/src/main/java/until/the/eternity/config/openapi/OpenApiFilters.java b/src/main/java/until/the/eternity/config/openapi/OpenApiFilters.java index af0147a8..29ca38fe 100644 --- a/src/main/java/until/the/eternity/config/openapi/OpenApiFilters.java +++ b/src/main/java/until/the/eternity/config/openapi/OpenApiFilters.java @@ -1,12 +1,13 @@ package until.the.eternity.config.openapi; -import java.time.Duration; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.springframework.web.reactive.function.client.ExchangeFilterFunction; import reactor.core.publisher.Mono; +import java.time.Duration; + @Slf4j @Component @RequiredArgsConstructor diff --git a/src/main/java/until/the/eternity/config/openapi/OpenApiRetryPolicy.java b/src/main/java/until/the/eternity/config/openapi/OpenApiRetryPolicy.java index 8c9d0cff..a419df7c 100644 --- a/src/main/java/until/the/eternity/config/openapi/OpenApiRetryPolicy.java +++ b/src/main/java/until/the/eternity/config/openapi/OpenApiRetryPolicy.java @@ -1,11 +1,12 @@ package until.the.eternity.config.openapi; -import java.time.Duration; import org.springframework.stereotype.Component; import org.springframework.web.reactive.function.client.WebClientResponseException; import reactor.util.retry.Retry; import reactor.util.retry.RetryBackoffSpec; +import java.time.Duration; + /** Nexon OPEN API 전용 재시도(Back-off) 정책. */ @Component public class OpenApiRetryPolicy { diff --git a/src/main/java/until/the/eternity/config/openapi/OpenApiWebClientProperties.java b/src/main/java/until/the/eternity/config/openapi/OpenApiWebClientProperties.java index 9d51746d..f8d79043 100644 --- a/src/main/java/until/the/eternity/config/openapi/OpenApiWebClientProperties.java +++ b/src/main/java/until/the/eternity/config/openapi/OpenApiWebClientProperties.java @@ -2,10 +2,11 @@ import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Positive; -import java.time.Duration; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.validation.annotation.Validated; +import java.time.Duration; + /** 외부 API용 WebClient 설정값 홀더 application.yml 사용) */ @Validated @ConfigurationProperties(prefix = "openapi.nexon") diff --git a/src/main/java/until/the/eternity/hornBugle/application/runner/HornBugleIndexRunner.java b/src/main/java/until/the/eternity/hornBugle/application/runner/HornBugleIndexRunner.java index c26c781e..663122b1 100644 --- a/src/main/java/until/the/eternity/hornBugle/application/runner/HornBugleIndexRunner.java +++ b/src/main/java/until/the/eternity/hornBugle/application/runner/HornBugleIndexRunner.java @@ -1,6 +1,5 @@ package until.the.eternity.hornBugle.application.runner; -import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.ApplicationArguments; @@ -14,6 +13,8 @@ import until.the.eternity.hornBugle.domain.repository.HornBugleRepositoryPort; import until.the.eternity.hornBugle.infrastructure.elasticsearch.HornBugleIndexService; +import java.util.List; + /** * 서버 재기동 시 DB 데이터를 Elasticsearch에 일괄 색인하는 Runner. application.yml에서 * elasticsearch.index.enabled=true로 설정 시 활성화됩니다. diff --git a/src/main/java/until/the/eternity/hornBugle/application/scheduler/HornBugleScheduler.java b/src/main/java/until/the/eternity/hornBugle/application/scheduler/HornBugleScheduler.java index bc50e1d4..94180550 100644 --- a/src/main/java/until/the/eternity/hornBugle/application/scheduler/HornBugleScheduler.java +++ b/src/main/java/until/the/eternity/hornBugle/application/scheduler/HornBugleScheduler.java @@ -1,7 +1,5 @@ package until.the.eternity.hornBugle.application.scheduler; -import java.util.ArrayList; -import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; @@ -13,6 +11,9 @@ import until.the.eternity.hornBugle.interfaces.external.dto.OpenApiHornBugleHistoryListResponse; import until.the.eternity.hornBugle.interfaces.external.dto.OpenApiHornBugleHistoryResponse; +import java.util.ArrayList; +import java.util.List; + @Slf4j @Component @RequiredArgsConstructor diff --git a/src/main/java/until/the/eternity/hornBugle/application/service/HornBugleService.java b/src/main/java/until/the/eternity/hornBugle/application/service/HornBugleService.java index 6c72a759..c6e3fdc1 100644 --- a/src/main/java/until/the/eternity/hornBugle/application/service/HornBugleService.java +++ b/src/main/java/until/the/eternity/hornBugle/application/service/HornBugleService.java @@ -1,8 +1,5 @@ package until.the.eternity.hornBugle.application.service; -import java.time.Instant; -import java.util.List; -import java.util.Optional; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Page; import org.springframework.stereotype.Service; @@ -19,6 +16,10 @@ import until.the.eternity.hornBugle.interfaces.rest.dto.request.HornBuglePageRequestDto; import until.the.eternity.hornBugle.interfaces.rest.dto.response.HornBugleHistoryResponse; +import java.time.Instant; +import java.util.List; +import java.util.Optional; + @Slf4j @Service public class HornBugleService { diff --git a/src/main/java/until/the/eternity/hornBugle/domain/entity/HornBugleWorldHistory.java b/src/main/java/until/the/eternity/hornBugle/domain/entity/HornBugleWorldHistory.java index 482c8572..05ca8dff 100644 --- a/src/main/java/until/the/eternity/hornBugle/domain/entity/HornBugleWorldHistory.java +++ b/src/main/java/until/the/eternity/hornBugle/domain/entity/HornBugleWorldHistory.java @@ -1,9 +1,10 @@ package until.the.eternity.hornBugle.domain.entity; import jakarta.persistence.*; -import java.time.Instant; import lombok.*; +import java.time.Instant; + @Entity @Table( name = "horn_bugle_world_history", diff --git a/src/main/java/until/the/eternity/hornBugle/domain/enums/HornBugleServer.java b/src/main/java/until/the/eternity/hornBugle/domain/enums/HornBugleServer.java index fe361d09..51bd034f 100644 --- a/src/main/java/until/the/eternity/hornBugle/domain/enums/HornBugleServer.java +++ b/src/main/java/until/the/eternity/hornBugle/domain/enums/HornBugleServer.java @@ -1,10 +1,11 @@ package until.the.eternity.hornBugle.domain.enums; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.Arrays; -import lombok.Getter; -import lombok.RequiredArgsConstructor; @Getter @RequiredArgsConstructor diff --git a/src/main/java/until/the/eternity/hornBugle/domain/mapper/HornBugleMapper.java b/src/main/java/until/the/eternity/hornBugle/domain/mapper/HornBugleMapper.java index a24aee75..c2bb9e73 100644 --- a/src/main/java/until/the/eternity/hornBugle/domain/mapper/HornBugleMapper.java +++ b/src/main/java/until/the/eternity/hornBugle/domain/mapper/HornBugleMapper.java @@ -1,6 +1,5 @@ package until.the.eternity.hornBugle.domain.mapper; -import java.time.Instant; import org.mapstruct.Mapper; import org.mapstruct.Mapping; import until.the.eternity.hornBugle.domain.entity.HornBugleWorldHistory; @@ -9,6 +8,8 @@ import until.the.eternity.hornBugle.interfaces.external.dto.OpenApiHornBugleHistoryResponse; import until.the.eternity.hornBugle.interfaces.rest.dto.response.HornBugleHistoryResponse; +import java.time.Instant; + @Mapper(componentModel = "spring") public interface HornBugleMapper { diff --git a/src/main/java/until/the/eternity/hornBugle/domain/repository/HornBugleRepositoryPort.java b/src/main/java/until/the/eternity/hornBugle/domain/repository/HornBugleRepositoryPort.java index 8e85b756..75dfd277 100644 --- a/src/main/java/until/the/eternity/hornBugle/domain/repository/HornBugleRepositoryPort.java +++ b/src/main/java/until/the/eternity/hornBugle/domain/repository/HornBugleRepositoryPort.java @@ -1,12 +1,13 @@ package until.the.eternity.hornBugle.domain.repository; -import java.time.Instant; -import java.util.List; -import java.util.Optional; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import until.the.eternity.hornBugle.domain.entity.HornBugleWorldHistory; +import java.time.Instant; +import java.util.List; +import java.util.Optional; + public interface HornBugleRepositoryPort { void saveAll(List entities); diff --git a/src/main/java/until/the/eternity/hornBugle/domain/service/HornBugleDuplicateChecker.java b/src/main/java/until/the/eternity/hornBugle/domain/service/HornBugleDuplicateChecker.java index 011986ca..267b975e 100644 --- a/src/main/java/until/the/eternity/hornBugle/domain/service/HornBugleDuplicateChecker.java +++ b/src/main/java/until/the/eternity/hornBugle/domain/service/HornBugleDuplicateChecker.java @@ -1,10 +1,5 @@ package until.the.eternity.hornBugle.domain.service; -import java.time.Instant; -import java.util.HashSet; -import java.util.List; -import java.util.Optional; -import java.util.Set; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; @@ -13,6 +8,12 @@ import until.the.eternity.hornBugle.domain.repository.HornBugleRepositoryPort; import until.the.eternity.hornBugle.interfaces.external.dto.OpenApiHornBugleHistoryResponse; +import java.time.Instant; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; + @Slf4j @Component @RequiredArgsConstructor diff --git a/src/main/java/until/the/eternity/hornBugle/infrastructure/elasticsearch/HornBugleDocument.java b/src/main/java/until/the/eternity/hornBugle/infrastructure/elasticsearch/HornBugleDocument.java index 7c0ceb37..98bb7864 100644 --- a/src/main/java/until/the/eternity/hornBugle/infrastructure/elasticsearch/HornBugleDocument.java +++ b/src/main/java/until/the/eternity/hornBugle/infrastructure/elasticsearch/HornBugleDocument.java @@ -1,6 +1,5 @@ package until.the.eternity.hornBugle.infrastructure.elasticsearch; -import java.time.Instant; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; @@ -12,6 +11,8 @@ import org.springframework.data.elasticsearch.annotations.Setting; import until.the.eternity.hornBugle.domain.entity.HornBugleWorldHistory; +import java.time.Instant; + @Document(indexName = "horn_bugle_world_history") @Setting(settingPath = "elasticsearch/horn-bugle-settings.json") @Getter diff --git a/src/main/java/until/the/eternity/hornBugle/infrastructure/elasticsearch/HornBugleIndexService.java b/src/main/java/until/the/eternity/hornBugle/infrastructure/elasticsearch/HornBugleIndexService.java index 67ba3fb7..0b6b8bd9 100644 --- a/src/main/java/until/the/eternity/hornBugle/infrastructure/elasticsearch/HornBugleIndexService.java +++ b/src/main/java/until/the/eternity/hornBugle/infrastructure/elasticsearch/HornBugleIndexService.java @@ -2,7 +2,6 @@ import co.elastic.clients.elasticsearch._types.query_dsl.BoolQuery; import co.elastic.clients.elasticsearch._types.query_dsl.Query; -import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; @@ -18,6 +17,8 @@ import org.springframework.stereotype.Service; import until.the.eternity.hornBugle.domain.entity.HornBugleWorldHistory; +import java.util.List; + @Slf4j @Service @RequiredArgsConstructor diff --git a/src/main/java/until/the/eternity/hornBugle/infrastructure/persistence/HornBugleJpaRepository.java b/src/main/java/until/the/eternity/hornBugle/infrastructure/persistence/HornBugleJpaRepository.java index 2e6acde1..e68b01eb 100644 --- a/src/main/java/until/the/eternity/hornBugle/infrastructure/persistence/HornBugleJpaRepository.java +++ b/src/main/java/until/the/eternity/hornBugle/infrastructure/persistence/HornBugleJpaRepository.java @@ -1,8 +1,5 @@ package until.the.eternity.hornBugle.infrastructure.persistence; -import java.time.Instant; -import java.util.List; -import java.util.Optional; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; @@ -10,6 +7,10 @@ import org.springframework.stereotype.Repository; import until.the.eternity.hornBugle.domain.entity.HornBugleWorldHistory; +import java.time.Instant; +import java.util.List; +import java.util.Optional; + @Repository public interface HornBugleJpaRepository extends JpaRepository { diff --git a/src/main/java/until/the/eternity/hornBugle/infrastructure/persistence/HornBugleRepositoryPortImpl.java b/src/main/java/until/the/eternity/hornBugle/infrastructure/persistence/HornBugleRepositoryPortImpl.java index 52366b65..706bc05b 100644 --- a/src/main/java/until/the/eternity/hornBugle/infrastructure/persistence/HornBugleRepositoryPortImpl.java +++ b/src/main/java/until/the/eternity/hornBugle/infrastructure/persistence/HornBugleRepositoryPortImpl.java @@ -1,9 +1,6 @@ package until.the.eternity.hornBugle.infrastructure.persistence; import jakarta.persistence.EntityManager; -import java.time.Instant; -import java.util.List; -import java.util.Optional; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.domain.Page; @@ -13,6 +10,10 @@ import until.the.eternity.hornBugle.domain.entity.HornBugleWorldHistory; import until.the.eternity.hornBugle.domain.repository.HornBugleRepositoryPort; +import java.time.Instant; +import java.util.List; +import java.util.Optional; + @Repository @RequiredArgsConstructor public class HornBugleRepositoryPortImpl implements HornBugleRepositoryPort { diff --git a/src/main/java/until/the/eternity/hornBugle/interfaces/external/dto/OpenApiHornBugleHistoryListResponse.java b/src/main/java/until/the/eternity/hornBugle/interfaces/external/dto/OpenApiHornBugleHistoryListResponse.java index 4090a2d4..adea275a 100644 --- a/src/main/java/until/the/eternity/hornBugle/interfaces/external/dto/OpenApiHornBugleHistoryListResponse.java +++ b/src/main/java/until/the/eternity/hornBugle/interfaces/external/dto/OpenApiHornBugleHistoryListResponse.java @@ -1,6 +1,7 @@ package until.the.eternity.hornBugle.interfaces.external.dto; import com.fasterxml.jackson.annotation.JsonProperty; + import java.util.List; public record OpenApiHornBugleHistoryListResponse( diff --git a/src/main/java/until/the/eternity/hornBugle/interfaces/external/dto/OpenApiHornBugleHistoryResponse.java b/src/main/java/until/the/eternity/hornBugle/interfaces/external/dto/OpenApiHornBugleHistoryResponse.java index 1a11eb0f..78f67772 100644 --- a/src/main/java/until/the/eternity/hornBugle/interfaces/external/dto/OpenApiHornBugleHistoryResponse.java +++ b/src/main/java/until/the/eternity/hornBugle/interfaces/external/dto/OpenApiHornBugleHistoryResponse.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonProperty; + import java.time.Instant; public record OpenApiHornBugleHistoryResponse( diff --git a/src/main/java/until/the/eternity/hornBugle/interfaces/rest/dto/response/HornBugleHistoryResponse.java b/src/main/java/until/the/eternity/hornBugle/interfaces/rest/dto/response/HornBugleHistoryResponse.java index cd47b1c0..7660ff1a 100644 --- a/src/main/java/until/the/eternity/hornBugle/interfaces/rest/dto/response/HornBugleHistoryResponse.java +++ b/src/main/java/until/the/eternity/hornBugle/interfaces/rest/dto/response/HornBugleHistoryResponse.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.annotation.JsonFormat; import io.swagger.v3.oas.annotations.media.Schema; + import java.time.Instant; @Schema(description = "뿔피리 히스토리 응답") diff --git a/src/main/java/until/the/eternity/iteminfo/application/service/ItemInfoService.java b/src/main/java/until/the/eternity/iteminfo/application/service/ItemInfoService.java index 1c80a412..75ba6584 100644 --- a/src/main/java/until/the/eternity/iteminfo/application/service/ItemInfoService.java +++ b/src/main/java/until/the/eternity/iteminfo/application/service/ItemInfoService.java @@ -1,9 +1,5 @@ package until.the.eternity.iteminfo.application.service; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Page; @@ -22,6 +18,11 @@ import until.the.eternity.iteminfo.interfaces.rest.dto.response.ItemInfoSummaryResponse; import until.the.eternity.iteminfo.interfaces.rest.dto.response.ItemInfoSyncResponse; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + @Slf4j @Service @Transactional(readOnly = true) diff --git a/src/main/java/until/the/eternity/iteminfo/domain/entity/ItemInfoId.java b/src/main/java/until/the/eternity/iteminfo/domain/entity/ItemInfoId.java index d53cd3b3..6293e4ee 100644 --- a/src/main/java/until/the/eternity/iteminfo/domain/entity/ItemInfoId.java +++ b/src/main/java/until/the/eternity/iteminfo/domain/entity/ItemInfoId.java @@ -2,9 +2,10 @@ import jakarta.persistence.Column; import jakarta.persistence.Embeddable; -import java.io.Serializable; import lombok.*; +import java.io.Serializable; + @Embeddable @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) diff --git a/src/main/java/until/the/eternity/iteminfo/domain/exception/ItemInfoExceptionCode.java b/src/main/java/until/the/eternity/iteminfo/domain/exception/ItemInfoExceptionCode.java index a7ffd4c2..f870031a 100644 --- a/src/main/java/until/the/eternity/iteminfo/domain/exception/ItemInfoExceptionCode.java +++ b/src/main/java/until/the/eternity/iteminfo/domain/exception/ItemInfoExceptionCode.java @@ -1,12 +1,12 @@ package until.the.eternity.iteminfo.domain.exception; -import static org.springframework.http.HttpStatus.BAD_REQUEST; - import lombok.Getter; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import until.the.eternity.common.exception.ExceptionCode; +import static org.springframework.http.HttpStatus.BAD_REQUEST; + @Getter @RequiredArgsConstructor public enum ItemInfoExceptionCode implements ExceptionCode { diff --git a/src/main/java/until/the/eternity/iteminfo/domain/repository/ItemInfoRepositoryPort.java b/src/main/java/until/the/eternity/iteminfo/domain/repository/ItemInfoRepositoryPort.java index b540fc66..c9f85ea3 100644 --- a/src/main/java/until/the/eternity/iteminfo/domain/repository/ItemInfoRepositoryPort.java +++ b/src/main/java/until/the/eternity/iteminfo/domain/repository/ItemInfoRepositoryPort.java @@ -1,12 +1,13 @@ package until.the.eternity.iteminfo.domain.repository; -import java.util.List; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import until.the.eternity.iteminfo.domain.entity.ItemInfo; import until.the.eternity.iteminfo.domain.entity.ItemInfoId; import until.the.eternity.iteminfo.interfaces.rest.dto.request.ItemInfoSearchRequest; +import java.util.List; + public interface ItemInfoRepositoryPort { List findAll(); diff --git a/src/main/java/until/the/eternity/iteminfo/infrastructure/persistence/ItemInfoJpaRepository.java b/src/main/java/until/the/eternity/iteminfo/infrastructure/persistence/ItemInfoJpaRepository.java index 2be98732..c6838467 100644 --- a/src/main/java/until/the/eternity/iteminfo/infrastructure/persistence/ItemInfoJpaRepository.java +++ b/src/main/java/until/the/eternity/iteminfo/infrastructure/persistence/ItemInfoJpaRepository.java @@ -1,12 +1,13 @@ package until.the.eternity.iteminfo.infrastructure.persistence; -import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.data.jpa.repository.Query; import until.the.eternity.iteminfo.domain.entity.ItemInfo; import until.the.eternity.iteminfo.domain.entity.ItemInfoId; +import java.util.List; + public interface ItemInfoJpaRepository extends JpaRepository, JpaSpecificationExecutor { diff --git a/src/main/java/until/the/eternity/iteminfo/infrastructure/persistence/ItemInfoQueryDslRepository.java b/src/main/java/until/the/eternity/iteminfo/infrastructure/persistence/ItemInfoQueryDslRepository.java index 266b2303..e13e45f9 100644 --- a/src/main/java/until/the/eternity/iteminfo/infrastructure/persistence/ItemInfoQueryDslRepository.java +++ b/src/main/java/until/the/eternity/iteminfo/infrastructure/persistence/ItemInfoQueryDslRepository.java @@ -3,7 +3,6 @@ import com.querydsl.core.BooleanBuilder; import com.querydsl.jpa.impl.JPAQuery; import com.querydsl.jpa.impl.JPAQueryFactory; -import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; @@ -13,6 +12,8 @@ import until.the.eternity.iteminfo.domain.entity.QItemInfo; import until.the.eternity.iteminfo.interfaces.rest.dto.request.ItemInfoSearchRequest; +import java.util.List; + @Repository @RequiredArgsConstructor public class ItemInfoQueryDslRepository { diff --git a/src/main/java/until/the/eternity/iteminfo/infrastructure/persistence/ItemInfoRepositoryPortImpl.java b/src/main/java/until/the/eternity/iteminfo/infrastructure/persistence/ItemInfoRepositoryPortImpl.java index 66481cac..497ad7d1 100644 --- a/src/main/java/until/the/eternity/iteminfo/infrastructure/persistence/ItemInfoRepositoryPortImpl.java +++ b/src/main/java/until/the/eternity/iteminfo/infrastructure/persistence/ItemInfoRepositoryPortImpl.java @@ -1,6 +1,5 @@ package until.the.eternity.iteminfo.infrastructure.persistence; -import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -10,6 +9,8 @@ import until.the.eternity.iteminfo.domain.repository.ItemInfoRepositoryPort; import until.the.eternity.iteminfo.interfaces.rest.dto.request.ItemInfoSearchRequest; +import java.util.List; + @Repository @RequiredArgsConstructor public class ItemInfoRepositoryPortImpl implements ItemInfoRepositoryPort { diff --git a/src/main/java/until/the/eternity/iteminfo/interfaces/rest/controller/ItemInfoController.java b/src/main/java/until/the/eternity/iteminfo/interfaces/rest/controller/ItemInfoController.java index 755b5a27..ddb1c49a 100644 --- a/src/main/java/until/the/eternity/iteminfo/interfaces/rest/controller/ItemInfoController.java +++ b/src/main/java/until/the/eternity/iteminfo/interfaces/rest/controller/ItemInfoController.java @@ -4,7 +4,6 @@ import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; -import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.http.ResponseEntity; @@ -19,6 +18,8 @@ import until.the.eternity.iteminfo.interfaces.rest.dto.response.ItemInfoSummaryResponse; import until.the.eternity.iteminfo.interfaces.rest.dto.response.ItemInfoSyncResponse; +import java.util.List; + @RestController @RequestMapping("/api/item-infos") @RequiredArgsConstructor diff --git a/src/main/java/until/the/eternity/iteminfo/interfaces/rest/dto/response/ItemCategoryResponse.java b/src/main/java/until/the/eternity/iteminfo/interfaces/rest/dto/response/ItemCategoryResponse.java index 065d209e..0b1e6817 100644 --- a/src/main/java/until/the/eternity/iteminfo/interfaces/rest/dto/response/ItemCategoryResponse.java +++ b/src/main/java/until/the/eternity/iteminfo/interfaces/rest/dto/response/ItemCategoryResponse.java @@ -1,12 +1,13 @@ package until.the.eternity.iteminfo.interfaces.rest.dto.response; -import java.util.Arrays; -import java.util.List; -import java.util.stream.Collectors; import lombok.Builder; import lombok.Getter; import until.the.eternity.common.enums.ItemCategory; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + @Getter @Builder public class ItemCategoryResponse { diff --git a/src/main/java/until/the/eternity/iteminfo/interfaces/rest/dto/response/ItemInfoResponse.java b/src/main/java/until/the/eternity/iteminfo/interfaces/rest/dto/response/ItemInfoResponse.java index be9d890b..70897793 100644 --- a/src/main/java/until/the/eternity/iteminfo/interfaces/rest/dto/response/ItemInfoResponse.java +++ b/src/main/java/until/the/eternity/iteminfo/interfaces/rest/dto/response/ItemInfoResponse.java @@ -1,11 +1,12 @@ package until.the.eternity.iteminfo.interfaces.rest.dto.response; import io.swagger.v3.oas.annotations.media.Schema; -import java.util.List; -import java.util.stream.Collectors; import lombok.Builder; import until.the.eternity.iteminfo.domain.entity.ItemInfo; +import java.util.List; +import java.util.stream.Collectors; + @Builder @Schema(description = "아이템 정보 응답 DTO") public record ItemInfoResponse( diff --git a/src/main/java/until/the/eternity/iteminfo/interfaces/rest/dto/response/ItemInfoSummaryResponse.java b/src/main/java/until/the/eternity/iteminfo/interfaces/rest/dto/response/ItemInfoSummaryResponse.java index 98e5cfd5..4b9da91b 100644 --- a/src/main/java/until/the/eternity/iteminfo/interfaces/rest/dto/response/ItemInfoSummaryResponse.java +++ b/src/main/java/until/the/eternity/iteminfo/interfaces/rest/dto/response/ItemInfoSummaryResponse.java @@ -1,11 +1,12 @@ package until.the.eternity.iteminfo.interfaces.rest.dto.response; import io.swagger.v3.oas.annotations.media.Schema; -import java.util.List; -import java.util.stream.Collectors; import lombok.Builder; import until.the.eternity.iteminfo.domain.entity.ItemInfo; +import java.util.List; +import java.util.stream.Collectors; + @Builder @Schema(description = "아이템 정보 요약 응답 DTO") public record ItemInfoSummaryResponse( diff --git a/src/main/java/until/the/eternity/iteminfo/interfaces/rest/dto/response/ItemInfoSyncResponse.java b/src/main/java/until/the/eternity/iteminfo/interfaces/rest/dto/response/ItemInfoSyncResponse.java index e98fce37..02016f52 100644 --- a/src/main/java/until/the/eternity/iteminfo/interfaces/rest/dto/response/ItemInfoSyncResponse.java +++ b/src/main/java/until/the/eternity/iteminfo/interfaces/rest/dto/response/ItemInfoSyncResponse.java @@ -1,9 +1,10 @@ package until.the.eternity.iteminfo.interfaces.rest.dto.response; import io.swagger.v3.oas.annotations.media.Schema; -import java.util.List; import lombok.Builder; +import java.util.List; + @Builder @Schema(description = "아이템 정보 동기화 응답 DTO") public record ItemInfoSyncResponse( diff --git a/src/main/java/until/the/eternity/itemoptioninfo/application/service/ItemOptionInfoService.java b/src/main/java/until/the/eternity/itemoptioninfo/application/service/ItemOptionInfoService.java index e199d4ba..fb86c034 100644 --- a/src/main/java/until/the/eternity/itemoptioninfo/application/service/ItemOptionInfoService.java +++ b/src/main/java/until/the/eternity/itemoptioninfo/application/service/ItemOptionInfoService.java @@ -1,12 +1,13 @@ package until.the.eternity.itemoptioninfo.application.service; -import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import until.the.eternity.itemoptioninfo.domain.entity.ItemOptionInfo; import until.the.eternity.itemoptioninfo.domain.repository.ItemOptionInfoRepositoryPort; +import java.util.List; + @Service @RequiredArgsConstructor @Transactional(readOnly = true) diff --git a/src/main/java/until/the/eternity/itemoptioninfo/domain/entity/ItemOptionInfoId.java b/src/main/java/until/the/eternity/itemoptioninfo/domain/entity/ItemOptionInfoId.java index 17634464..5bbcdaf9 100644 --- a/src/main/java/until/the/eternity/itemoptioninfo/domain/entity/ItemOptionInfoId.java +++ b/src/main/java/until/the/eternity/itemoptioninfo/domain/entity/ItemOptionInfoId.java @@ -2,12 +2,13 @@ import jakarta.persistence.Column; import jakarta.persistence.Embeddable; -import java.io.Serializable; -import java.util.Objects; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; +import java.io.Serializable; +import java.util.Objects; + @Embeddable @Getter @NoArgsConstructor diff --git a/src/main/java/until/the/eternity/itemoptioninfo/domain/repository/ItemOptionInfoRepositoryPort.java b/src/main/java/until/the/eternity/itemoptioninfo/domain/repository/ItemOptionInfoRepositoryPort.java index 86bb898b..01e49348 100644 --- a/src/main/java/until/the/eternity/itemoptioninfo/domain/repository/ItemOptionInfoRepositoryPort.java +++ b/src/main/java/until/the/eternity/itemoptioninfo/domain/repository/ItemOptionInfoRepositoryPort.java @@ -1,8 +1,9 @@ package until.the.eternity.itemoptioninfo.domain.repository; -import java.util.List; import until.the.eternity.itemoptioninfo.domain.entity.ItemOptionInfo; +import java.util.List; + public interface ItemOptionInfoRepositoryPort { List findAll(); } diff --git a/src/main/java/until/the/eternity/itemoptioninfo/infrastructure/persistence/ItemOptionInfoRepositoryPortImpl.java b/src/main/java/until/the/eternity/itemoptioninfo/infrastructure/persistence/ItemOptionInfoRepositoryPortImpl.java index d01cc9e1..b523642a 100644 --- a/src/main/java/until/the/eternity/itemoptioninfo/infrastructure/persistence/ItemOptionInfoRepositoryPortImpl.java +++ b/src/main/java/until/the/eternity/itemoptioninfo/infrastructure/persistence/ItemOptionInfoRepositoryPortImpl.java @@ -1,11 +1,12 @@ package until.the.eternity.itemoptioninfo.infrastructure.persistence; -import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; import until.the.eternity.itemoptioninfo.domain.entity.ItemOptionInfo; import until.the.eternity.itemoptioninfo.domain.repository.ItemOptionInfoRepositoryPort; +import java.util.List; + @Repository @RequiredArgsConstructor public class ItemOptionInfoRepositoryPortImpl implements ItemOptionInfoRepositoryPort { diff --git a/src/main/java/until/the/eternity/itemoptioninfo/interfaces/rest/controller/ItemOptionInfoController.java b/src/main/java/until/the/eternity/itemoptioninfo/interfaces/rest/controller/ItemOptionInfoController.java index d74ab49f..97bafca3 100644 --- a/src/main/java/until/the/eternity/itemoptioninfo/interfaces/rest/controller/ItemOptionInfoController.java +++ b/src/main/java/until/the/eternity/itemoptioninfo/interfaces/rest/controller/ItemOptionInfoController.java @@ -2,8 +2,6 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; -import java.util.List; -import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; @@ -12,6 +10,9 @@ import until.the.eternity.itemoptioninfo.domain.mapper.ItemOptionInfoMapper; import until.the.eternity.itemoptioninfo.interfaces.rest.dto.response.ItemOptionInfoResponse; +import java.util.List; +import java.util.stream.Collectors; + @RestController @RequestMapping("/api/v1/item-option-infos") @RequiredArgsConstructor diff --git a/src/main/java/until/the/eternity/metalwareinfo/application/service/MetalwareInfoService.java b/src/main/java/until/the/eternity/metalwareinfo/application/service/MetalwareInfoService.java index 499de1a0..8135be62 100644 --- a/src/main/java/until/the/eternity/metalwareinfo/application/service/MetalwareInfoService.java +++ b/src/main/java/until/the/eternity/metalwareinfo/application/service/MetalwareInfoService.java @@ -1,12 +1,13 @@ package until.the.eternity.metalwareinfo.application.service; -import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import until.the.eternity.metalwareinfo.domain.repository.MetalwareInfoRepositoryPort; import until.the.eternity.metalwareinfo.interfaces.rest.dto.response.MetalwareInfoResponse; +import java.util.List; + @Service @Transactional(readOnly = true) @RequiredArgsConstructor diff --git a/src/main/java/until/the/eternity/metalwareinfo/infrastructure/persistence/MetalwareInfoJpaRepository.java b/src/main/java/until/the/eternity/metalwareinfo/infrastructure/persistence/MetalwareInfoJpaRepository.java index 61481a24..202802f5 100644 --- a/src/main/java/until/the/eternity/metalwareinfo/infrastructure/persistence/MetalwareInfoJpaRepository.java +++ b/src/main/java/until/the/eternity/metalwareinfo/infrastructure/persistence/MetalwareInfoJpaRepository.java @@ -1,9 +1,10 @@ package until.the.eternity.metalwareinfo.infrastructure.persistence; -import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; +import java.util.List; + public interface MetalwareInfoJpaRepository extends JpaRepository { @Query("SELECT m.metalware FROM MetalwareInfoEntity m") diff --git a/src/main/java/until/the/eternity/metalwareinfo/infrastructure/persistence/MetalwareInfoRepositoryPortImpl.java b/src/main/java/until/the/eternity/metalwareinfo/infrastructure/persistence/MetalwareInfoRepositoryPortImpl.java index 9121dd1b..3ee9a1a3 100644 --- a/src/main/java/until/the/eternity/metalwareinfo/infrastructure/persistence/MetalwareInfoRepositoryPortImpl.java +++ b/src/main/java/until/the/eternity/metalwareinfo/infrastructure/persistence/MetalwareInfoRepositoryPortImpl.java @@ -1,10 +1,11 @@ package until.the.eternity.metalwareinfo.infrastructure.persistence; -import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; import until.the.eternity.metalwareinfo.domain.repository.MetalwareInfoRepositoryPort; +import java.util.List; + @Repository @RequiredArgsConstructor public class MetalwareInfoRepositoryPortImpl implements MetalwareInfoRepositoryPort { diff --git a/src/main/java/until/the/eternity/metalwareinfo/interfaces/rest/controller/MetalwareInfoController.java b/src/main/java/until/the/eternity/metalwareinfo/interfaces/rest/controller/MetalwareInfoController.java index fa264595..271a7e35 100644 --- a/src/main/java/until/the/eternity/metalwareinfo/interfaces/rest/controller/MetalwareInfoController.java +++ b/src/main/java/until/the/eternity/metalwareinfo/interfaces/rest/controller/MetalwareInfoController.java @@ -2,7 +2,6 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; -import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; @@ -10,6 +9,8 @@ import until.the.eternity.metalwareinfo.application.service.MetalwareInfoService; import until.the.eternity.metalwareinfo.interfaces.rest.dto.response.MetalwareInfoResponse; +import java.util.List; + @RestController @RequestMapping("/api/metalware-infos") @RequiredArgsConstructor diff --git a/src/main/java/until/the/eternity/metalwareinfo/interfaces/rest/dto/response/MetalwareInfoResponse.java b/src/main/java/until/the/eternity/metalwareinfo/interfaces/rest/dto/response/MetalwareInfoResponse.java index a3122710..4e8d9d1e 100644 --- a/src/main/java/until/the/eternity/metalwareinfo/interfaces/rest/dto/response/MetalwareInfoResponse.java +++ b/src/main/java/until/the/eternity/metalwareinfo/interfaces/rest/dto/response/MetalwareInfoResponse.java @@ -1,9 +1,10 @@ package until.the.eternity.metalwareinfo.interfaces.rest.dto.response; import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; + import java.util.List; import java.util.stream.Collectors; -import lombok.Builder; @Builder @Schema(description = "세공 정보 응답 DTO") 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 361df9c1..f6217f44 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 @@ -2,10 +2,11 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.persistence.*; +import lombok.*; + import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; -import lombok.*; @Entity @Table( diff --git a/src/main/java/until/the/eternity/statistics/domain/entity/daily/SubcategoryDailyStatistics.java b/src/main/java/until/the/eternity/statistics/domain/entity/daily/SubcategoryDailyStatistics.java index 66e5382d..887807d9 100644 --- a/src/main/java/until/the/eternity/statistics/domain/entity/daily/SubcategoryDailyStatistics.java +++ b/src/main/java/until/the/eternity/statistics/domain/entity/daily/SubcategoryDailyStatistics.java @@ -2,10 +2,11 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.persistence.*; +import lombok.*; + import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; -import lombok.*; @Entity @Table( diff --git a/src/main/java/until/the/eternity/statistics/domain/entity/daily/TopCategoryDailyStatistics.java b/src/main/java/until/the/eternity/statistics/domain/entity/daily/TopCategoryDailyStatistics.java index fb3ab7a8..bfcb536b 100644 --- a/src/main/java/until/the/eternity/statistics/domain/entity/daily/TopCategoryDailyStatistics.java +++ b/src/main/java/until/the/eternity/statistics/domain/entity/daily/TopCategoryDailyStatistics.java @@ -2,10 +2,11 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.persistence.*; +import lombok.*; + import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; -import lombok.*; @Entity @Table( 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 6970f18e..44c20386 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 @@ -2,10 +2,11 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.persistence.*; +import lombok.*; + import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; -import lombok.*; @Entity @Table( diff --git a/src/main/java/until/the/eternity/statistics/domain/entity/weekly/SubcategoryWeeklyStatistics.java b/src/main/java/until/the/eternity/statistics/domain/entity/weekly/SubcategoryWeeklyStatistics.java index 5756b091..a05c528e 100644 --- a/src/main/java/until/the/eternity/statistics/domain/entity/weekly/SubcategoryWeeklyStatistics.java +++ b/src/main/java/until/the/eternity/statistics/domain/entity/weekly/SubcategoryWeeklyStatistics.java @@ -2,10 +2,11 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.persistence.*; +import lombok.*; + import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; -import lombok.*; @Entity @Table( diff --git a/src/main/java/until/the/eternity/statistics/domain/entity/weekly/TopCategoryWeeklyStatistics.java b/src/main/java/until/the/eternity/statistics/domain/entity/weekly/TopCategoryWeeklyStatistics.java index 9201faaf..22e4406d 100644 --- a/src/main/java/until/the/eternity/statistics/domain/entity/weekly/TopCategoryWeeklyStatistics.java +++ b/src/main/java/until/the/eternity/statistics/domain/entity/weekly/TopCategoryWeeklyStatistics.java @@ -2,10 +2,11 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.persistence.*; +import lombok.*; + import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; -import lombok.*; @Entity @Table( diff --git a/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/request/DailyStatisticsSearchRequest.java b/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/request/DailyStatisticsSearchRequest.java index 7908da49..59954a6b 100644 --- a/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/request/DailyStatisticsSearchRequest.java +++ b/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/request/DailyStatisticsSearchRequest.java @@ -1,9 +1,10 @@ package until.the.eternity.statistics.interfaces.rest.dto.request; import io.swagger.v3.oas.annotations.media.Schema; -import java.time.LocalDate; import org.springframework.format.annotation.DateTimeFormat; +import java.time.LocalDate; + @Schema(description = "일간 통계 검색 요청") public record DailyStatisticsSearchRequest( @Schema(description = "아이템 이름 (부분 일치)", example = "켈틱") String itemName, 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 index f6f91521..46b2cf78 100644 --- 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 @@ -2,9 +2,10 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; -import java.time.LocalDate; import org.springframework.format.annotation.DateTimeFormat; +import java.time.LocalDate; + @Schema(description = "아이템별 일간 통계 검색 요청") public record ItemDailyStatisticsSearchRequest( @NotBlank(message = "아이템 이름은 필수입니다") 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 index 1360de10..d132153d 100644 --- 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 @@ -2,9 +2,10 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; -import java.time.LocalDate; import org.springframework.format.annotation.DateTimeFormat; +import java.time.LocalDate; + @Schema(description = "아이템별 주간 통계 검색 요청") public record ItemWeeklyStatisticsSearchRequest( @NotBlank(message = "아이템 이름은 필수입니다") 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 index 47c2c966..4ff95e0d 100644 --- 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 @@ -2,9 +2,10 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; -import java.time.LocalDate; import org.springframework.format.annotation.DateTimeFormat; +import java.time.LocalDate; + @Schema(description = "서브카테고리별 일간 통계 검색 요청") public record SubcategoryDailyStatisticsSearchRequest( @NotBlank(message = "탑 카테고리는 필수입니다") 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 index 9d7bc716..e0247871 100644 --- 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 @@ -2,9 +2,10 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; -import java.time.LocalDate; import org.springframework.format.annotation.DateTimeFormat; +import java.time.LocalDate; + @Schema(description = "서브카테고리별 주간 통계 검색 요청") public record SubcategoryWeeklyStatisticsSearchRequest( @NotBlank(message = "탑 카테고리는 필수입니다") 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 index 4b8e25d7..382bfefb 100644 --- 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 @@ -2,9 +2,10 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; -import java.time.LocalDate; import org.springframework.format.annotation.DateTimeFormat; +import java.time.LocalDate; + @Schema(description = "탑카테고리별 일간 통계 검색 요청") public record TopCategoryDailyStatisticsSearchRequest( @NotBlank(message = "탑 카테고리는 필수입니다") 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 index ad1c6b41..2986275f 100644 --- 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 @@ -2,9 +2,10 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; -import java.time.LocalDate; import org.springframework.format.annotation.DateTimeFormat; +import java.time.LocalDate; + @Schema(description = "탑카테고리별 주간 통계 검색 요청") public record TopCategoryWeeklyStatisticsSearchRequest( @NotBlank(message = "탑 카테고리는 필수입니다") diff --git a/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/response/ItemDailyStatisticsResponse.java b/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/response/ItemDailyStatisticsResponse.java index a9611010..e2485f4d 100644 --- a/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/response/ItemDailyStatisticsResponse.java +++ b/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/response/ItemDailyStatisticsResponse.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.annotation.JsonFormat; import io.swagger.v3.oas.annotations.media.Schema; + import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; diff --git a/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/response/ItemWeeklyStatisticsResponse.java b/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/response/ItemWeeklyStatisticsResponse.java index 05e86f70..7d499d85 100644 --- a/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/response/ItemWeeklyStatisticsResponse.java +++ b/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/response/ItemWeeklyStatisticsResponse.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.annotation.JsonFormat; import io.swagger.v3.oas.annotations.media.Schema; + import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; diff --git a/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/response/SubcategoryDailyStatisticsResponse.java b/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/response/SubcategoryDailyStatisticsResponse.java index e27f7f3a..b167b92c 100644 --- a/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/response/SubcategoryDailyStatisticsResponse.java +++ b/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/response/SubcategoryDailyStatisticsResponse.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.annotation.JsonFormat; import io.swagger.v3.oas.annotations.media.Schema; + import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; diff --git a/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/response/SubcategoryWeeklyStatisticsResponse.java b/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/response/SubcategoryWeeklyStatisticsResponse.java index bb826866..5f67f69e 100644 --- a/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/response/SubcategoryWeeklyStatisticsResponse.java +++ b/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/response/SubcategoryWeeklyStatisticsResponse.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.annotation.JsonFormat; import io.swagger.v3.oas.annotations.media.Schema; + import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; diff --git a/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/response/TopCategoryDailyStatisticsResponse.java b/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/response/TopCategoryDailyStatisticsResponse.java index fd49c2f2..df6c9f8e 100644 --- a/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/response/TopCategoryDailyStatisticsResponse.java +++ b/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/response/TopCategoryDailyStatisticsResponse.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.annotation.JsonFormat; import io.swagger.v3.oas.annotations.media.Schema; + import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; diff --git a/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/response/TopCategoryWeeklyStatisticsResponse.java b/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/response/TopCategoryWeeklyStatisticsResponse.java index ad81f768..01782f02 100644 --- a/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/response/TopCategoryWeeklyStatisticsResponse.java +++ b/src/main/java/until/the/eternity/statistics/interfaces/rest/dto/response/TopCategoryWeeklyStatisticsResponse.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.annotation.JsonFormat; import io.swagger.v3.oas.annotations.media.Schema; + import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; 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 447c2425..253bcac2 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,7 +1,5 @@ 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; @@ -9,6 +7,9 @@ import org.springframework.transaction.annotation.Transactional; import until.the.eternity.statistics.domain.entity.daily.ItemDailyStatistics; +import java.time.LocalDate; +import java.util.List; + public interface ItemDailyStatisticsRepository extends JpaRepository { /** 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 3bed0b1a..e8f72fc4 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,7 +1,5 @@ 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; @@ -9,6 +7,9 @@ import org.springframework.transaction.annotation.Transactional; import until.the.eternity.statistics.domain.entity.daily.SubcategoryDailyStatistics; +import java.time.LocalDate; +import java.util.List; + public interface SubcategoryDailyStatisticsRepository extends JpaRepository { 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 31749482..0fff60a8 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,7 +1,5 @@ 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; @@ -9,6 +7,9 @@ import org.springframework.transaction.annotation.Transactional; import until.the.eternity.statistics.domain.entity.daily.TopCategoryDailyStatistics; +import java.time.LocalDate; +import java.util.List; + public interface TopCategoryDailyStatisticsRepository extends JpaRepository { 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 dc7a301a..24f37b00 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,6 +1,5 @@ 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; @@ -8,6 +7,8 @@ import org.springframework.transaction.annotation.Transactional; import until.the.eternity.statistics.domain.entity.weekly.ItemWeeklyStatistics; +import java.util.List; + public interface ItemWeeklyStatisticsRepository extends JpaRepository { /** 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 fb0986af..ea3542c1 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,6 +1,5 @@ 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; @@ -8,6 +7,8 @@ import org.springframework.transaction.annotation.Transactional; import until.the.eternity.statistics.domain.entity.weekly.SubcategoryWeeklyStatistics; +import java.util.List; + public interface SubcategoryWeeklyStatisticsRepository extends JpaRepository { 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 45a1dad4..c2140d57 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,6 +1,5 @@ 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; @@ -8,6 +7,8 @@ import org.springframework.transaction.annotation.Transactional; import until.the.eternity.statistics.domain.entity.weekly.TopCategoryWeeklyStatistics; +import java.util.List; + public interface TopCategoryWeeklyStatisticsRepository extends JpaRepository { diff --git a/src/test/java/until/the/eternity/auctionhistory/application/service/AuctionHistoryServiceTest.java b/src/test/java/until/the/eternity/auctionhistory/application/service/AuctionHistoryServiceTest.java index ed2fea8a..7ae6dd98 100644 --- a/src/test/java/until/the/eternity/auctionhistory/application/service/AuctionHistoryServiceTest.java +++ b/src/test/java/until/the/eternity/auctionhistory/application/service/AuctionHistoryServiceTest.java @@ -1,11 +1,5 @@ package until.the.eternity.auctionhistory.application.service; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.Mockito.*; - -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; @@ -27,6 +21,13 @@ import until.the.eternity.common.request.PageRequestDto; import until.the.eternity.common.response.PageResponseDto; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.*; + @ExtendWith(MockitoExtension.class) class AuctionHistoryServiceTest { diff --git a/src/test/java/until/the/eternity/auctionhistory/application/service/fetcher/AuctionHistoryFetcherTest.java b/src/test/java/until/the/eternity/auctionhistory/application/service/fetcher/AuctionHistoryFetcherTest.java index e6e76af4..d1187257 100644 --- a/src/test/java/until/the/eternity/auctionhistory/application/service/fetcher/AuctionHistoryFetcherTest.java +++ b/src/test/java/until/the/eternity/auctionhistory/application/service/fetcher/AuctionHistoryFetcherTest.java @@ -1,13 +1,5 @@ package until.the.eternity.auctionhistory.application.service.fetcher; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.*; - -import java.time.Instant; -import java.util.List; -import java.util.OptionalInt; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -22,6 +14,15 @@ import until.the.eternity.auctionhistory.interfaces.external.dto.OpenApiAuctionHistoryResponse; import until.the.eternity.common.enums.ItemCategory; +import java.time.Instant; +import java.util.List; +import java.util.OptionalInt; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + @ExtendWith(MockitoExtension.class) class AuctionHistoryFetcherTest { diff --git a/src/test/java/until/the/eternity/auctionhistory/application/service/persister/AuctionHistoryPersisterTest.java b/src/test/java/until/the/eternity/auctionhistory/application/service/persister/AuctionHistoryPersisterTest.java index 0d9b8d2d..7764a911 100644 --- a/src/test/java/until/the/eternity/auctionhistory/application/service/persister/AuctionHistoryPersisterTest.java +++ b/src/test/java/until/the/eternity/auctionhistory/application/service/persister/AuctionHistoryPersisterTest.java @@ -1,11 +1,5 @@ package until.the.eternity.auctionhistory.application.service.persister; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.*; - -import java.util.Arrays; -import java.util.Collections; -import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -19,6 +13,13 @@ import until.the.eternity.auctionhistory.interfaces.external.dto.OpenApiAuctionHistoryResponse; import until.the.eternity.common.enums.ItemCategory; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.*; + @ExtendWith(MockitoExtension.class) class AuctionHistoryPersisterTest { diff --git a/src/test/java/until/the/eternity/auctionhistory/domain/service/AuctionHistoryDuplicateCheckerTest.java b/src/test/java/until/the/eternity/auctionhistory/domain/service/AuctionHistoryDuplicateCheckerTest.java index 3c6bf7b9..6cfdc813 100644 --- a/src/test/java/until/the/eternity/auctionhistory/domain/service/AuctionHistoryDuplicateCheckerTest.java +++ b/src/test/java/until/the/eternity/auctionhistory/domain/service/AuctionHistoryDuplicateCheckerTest.java @@ -1,13 +1,5 @@ package until.the.eternity.auctionhistory.domain.service; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.when; - -import java.time.Instant; -import java.util.List; -import java.util.Optional; -import java.util.OptionalInt; -import java.util.Set; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -20,6 +12,15 @@ import until.the.eternity.auctionhistory.interfaces.external.dto.OpenApiAuctionHistoryResponse; import until.the.eternity.common.enums.ItemCategory; +import java.time.Instant; +import java.util.List; +import java.util.Optional; +import java.util.OptionalInt; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + @ExtendWith(MockitoExtension.class) class AuctionHistoryDuplicateCheckerTest { diff --git a/src/test/java/until/the/eternity/auctionrealtime/application/service/fetcher/AuctionRealtimeFetcherTest.java b/src/test/java/until/the/eternity/auctionrealtime/application/service/fetcher/AuctionRealtimeFetcherTest.java index 3c2c7bb3..7f543ac5 100644 --- a/src/test/java/until/the/eternity/auctionrealtime/application/service/fetcher/AuctionRealtimeFetcherTest.java +++ b/src/test/java/until/the/eternity/auctionrealtime/application/service/fetcher/AuctionRealtimeFetcherTest.java @@ -1,12 +1,5 @@ package until.the.eternity.auctionrealtime.application.service.fetcher; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.*; - -import java.time.Instant; -import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -23,6 +16,14 @@ import until.the.eternity.auctionrealtime.interfaces.external.dto.OpenApiAuctionRealtimeResponse; import until.the.eternity.common.enums.ItemCategory; +import java.time.Instant; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + @ExtendWith(MockitoExtension.class) class AuctionRealtimeFetcherTest { diff --git a/src/test/java/until/the/eternity/auctionrealtime/domain/service/AuctionRealtimeDuplicateCheckerTest.java b/src/test/java/until/the/eternity/auctionrealtime/domain/service/AuctionRealtimeDuplicateCheckerTest.java index b731130b..a095f3b3 100644 --- a/src/test/java/until/the/eternity/auctionrealtime/domain/service/AuctionRealtimeDuplicateCheckerTest.java +++ b/src/test/java/until/the/eternity/auctionrealtime/domain/service/AuctionRealtimeDuplicateCheckerTest.java @@ -1,11 +1,5 @@ package until.the.eternity.auctionrealtime.domain.service; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.when; - -import java.time.Instant; -import java.util.List; -import java.util.Optional; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -18,6 +12,13 @@ import until.the.eternity.auctionrealtime.interfaces.external.dto.OpenApiAuctionRealtimeResponse; import until.the.eternity.common.enums.ItemCategory; +import java.time.Instant; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + @ExtendWith(MockitoExtension.class) class AuctionRealtimeDuplicateCheckerTest { diff --git a/src/test/java/until/the/eternity/auctionsearchoption/application/service/AuctionSearchOptionServiceTest.java b/src/test/java/until/the/eternity/auctionsearchoption/application/service/AuctionSearchOptionServiceTest.java index d92cf748..cbd42577 100644 --- a/src/test/java/until/the/eternity/auctionsearchoption/application/service/AuctionSearchOptionServiceTest.java +++ b/src/test/java/until/the/eternity/auctionsearchoption/application/service/AuctionSearchOptionServiceTest.java @@ -1,11 +1,6 @@ package until.the.eternity.auctionsearchoption.application.service; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.*; - import com.fasterxml.jackson.databind.ObjectMapper; -import java.util.List; -import java.util.Map; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -17,6 +12,12 @@ import until.the.eternity.auctionsearchoption.interfaces.rest.dto.response.FieldMetadata; import until.the.eternity.auctionsearchoption.interfaces.rest.dto.response.SearchOptionMetadataResponse; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.*; + @ExtendWith(MockitoExtension.class) class AuctionSearchOptionServiceTest { diff --git a/src/test/java/until/the/eternity/hornBugle/application/runner/HornBugleIndexRunnerTest.java b/src/test/java/until/the/eternity/hornBugle/application/runner/HornBugleIndexRunnerTest.java index 0669753b..1571a151 100644 --- a/src/test/java/until/the/eternity/hornBugle/application/runner/HornBugleIndexRunnerTest.java +++ b/src/test/java/until/the/eternity/hornBugle/application/runner/HornBugleIndexRunnerTest.java @@ -1,9 +1,5 @@ package until.the.eternity.hornBugle.application.runner; -import static org.mockito.Mockito.*; - -import java.time.Instant; -import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -18,6 +14,11 @@ import until.the.eternity.hornBugle.domain.repository.HornBugleRepositoryPort; import until.the.eternity.hornBugle.infrastructure.elasticsearch.HornBugleIndexService; +import java.time.Instant; +import java.util.List; + +import static org.mockito.Mockito.*; + @ExtendWith(MockitoExtension.class) class HornBugleIndexRunnerTest { diff --git a/src/test/java/until/the/eternity/hornBugle/application/service/HornBugleServiceTest.java b/src/test/java/until/the/eternity/hornBugle/application/service/HornBugleServiceTest.java index 92f37faa..7f167407 100644 --- a/src/test/java/until/the/eternity/hornBugle/application/service/HornBugleServiceTest.java +++ b/src/test/java/until/the/eternity/hornBugle/application/service/HornBugleServiceTest.java @@ -1,11 +1,5 @@ package until.the.eternity.hornBugle.application.service; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.*; - -import java.time.Instant; -import java.util.List; -import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -28,6 +22,13 @@ import until.the.eternity.hornBugle.interfaces.rest.dto.request.HornBuglePageRequestDto; import until.the.eternity.hornBugle.interfaces.rest.dto.response.HornBugleHistoryResponse; +import java.time.Instant; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.*; + @ExtendWith(MockitoExtension.class) class HornBugleServiceTest { diff --git a/src/test/java/until/the/eternity/hornBugle/infrastructure/elasticsearch/HornBugleIndexServiceTest.java b/src/test/java/until/the/eternity/hornBugle/infrastructure/elasticsearch/HornBugleIndexServiceTest.java index 7a8d1071..051744b5 100644 --- a/src/test/java/until/the/eternity/hornBugle/infrastructure/elasticsearch/HornBugleIndexServiceTest.java +++ b/src/test/java/until/the/eternity/hornBugle/infrastructure/elasticsearch/HornBugleIndexServiceTest.java @@ -1,11 +1,5 @@ package until.the.eternity.hornBugle.infrastructure.elasticsearch; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Mockito.*; - -import java.time.Instant; -import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -24,6 +18,13 @@ import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; import until.the.eternity.hornBugle.domain.entity.HornBugleWorldHistory; +import java.time.Instant; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.*; + @ExtendWith(MockitoExtension.class) class HornBugleIndexServiceTest { diff --git a/src/test/java/until/the/eternity/iteminfo/application/service/ItemInfoServiceTest.java b/src/test/java/until/the/eternity/iteminfo/application/service/ItemInfoServiceTest.java index 15a7a7d5..6e66a736 100644 --- a/src/test/java/until/the/eternity/iteminfo/application/service/ItemInfoServiceTest.java +++ b/src/test/java/until/the/eternity/iteminfo/application/service/ItemInfoServiceTest.java @@ -1,11 +1,5 @@ package until.the.eternity.iteminfo.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.util.ArrayList; -import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -25,6 +19,13 @@ import until.the.eternity.iteminfo.interfaces.rest.dto.response.ItemInfoSummaryResponse; import until.the.eternity.iteminfo.interfaces.rest.dto.response.ItemInfoSyncResponse; +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.*; + @ExtendWith(MockitoExtension.class) class ItemInfoServiceTest { diff --git a/src/test/java/until/the/eternity/metalwareinfo/application/service/MetalwareInfoServiceTest.java b/src/test/java/until/the/eternity/metalwareinfo/application/service/MetalwareInfoServiceTest.java index 7de499a5..6c56c0d6 100644 --- a/src/test/java/until/the/eternity/metalwareinfo/application/service/MetalwareInfoServiceTest.java +++ b/src/test/java/until/the/eternity/metalwareinfo/application/service/MetalwareInfoServiceTest.java @@ -1,10 +1,5 @@ package until.the.eternity.metalwareinfo.application.service; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -14,6 +9,12 @@ import until.the.eternity.metalwareinfo.domain.repository.MetalwareInfoRepositoryPort; import until.the.eternity.metalwareinfo.interfaces.rest.dto.response.MetalwareInfoResponse; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + @ExtendWith(MockitoExtension.class) class MetalwareInfoServiceTest { From d0c9228ae300b7cbe6a195857f82bbd7828a46b1 Mon Sep 17 00:00:00 2001 From: Sanghyun Yi Date: Thu, 29 Jan 2026 00:02:14 +0900 Subject: [PATCH 8/8] =?UTF-8?q?fix:=20hornbugle=20test=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../runner/HornBugleIndexRunnerTest.java | 98 ----- .../service/HornBugleServiceTest.java | 336 ------------------ .../HornBugleIndexServiceTest.java | 238 ------------- 3 files changed, 672 deletions(-) delete mode 100644 src/test/java/until/the/eternity/hornBugle/application/runner/HornBugleIndexRunnerTest.java delete mode 100644 src/test/java/until/the/eternity/hornBugle/application/service/HornBugleServiceTest.java delete mode 100644 src/test/java/until/the/eternity/hornBugle/infrastructure/elasticsearch/HornBugleIndexServiceTest.java diff --git a/src/test/java/until/the/eternity/hornBugle/application/runner/HornBugleIndexRunnerTest.java b/src/test/java/until/the/eternity/hornBugle/application/runner/HornBugleIndexRunnerTest.java deleted file mode 100644 index 1571a151..00000000 --- a/src/test/java/until/the/eternity/hornBugle/application/runner/HornBugleIndexRunnerTest.java +++ /dev/null @@ -1,98 +0,0 @@ -package until.the.eternity.hornBugle.application.runner; - -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.boot.ApplicationArguments; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageImpl; -import org.springframework.data.domain.PageRequest; -import until.the.eternity.hornBugle.domain.entity.HornBugleWorldHistory; -import until.the.eternity.hornBugle.domain.repository.HornBugleRepositoryPort; -import until.the.eternity.hornBugle.infrastructure.elasticsearch.HornBugleIndexService; - -import java.time.Instant; -import java.util.List; - -import static org.mockito.Mockito.*; - -@ExtendWith(MockitoExtension.class) -class HornBugleIndexRunnerTest { - - @Mock private HornBugleRepositoryPort repository; - @Mock private HornBugleIndexService indexService; - - @InjectMocks private HornBugleIndexRunner runner; - - @Test - @DisplayName("서버 재기동 시 인덱스를 재생성하고 DB 데이터를 일괄 색인한다") - void run_shouldRecreateIndexAndIndexAllData() { - // given - ApplicationArguments args = mock(ApplicationArguments.class); - - HornBugleWorldHistory entity1 = createEntity(1L, "류트", "테스트1", "안녕하세요1"); - HornBugleWorldHistory entity2 = createEntity(2L, "만돌린", "테스트2", "안녕하세요2"); - - // 첫 페이지: 데이터 있고 다음 페이지 없음 (hasNext = false) - Page firstPage = - new PageImpl<>(List.of(entity1, entity2), PageRequest.of(0, 500), 2); - - when(repository.findAll(PageRequest.of(0, 500))).thenReturn(firstPage); - - // when - runner.run(args); - - // then - verify(indexService).recreateIndex(); - verify(indexService).indexAll(List.of(entity1, entity2)); - } - - @Test - @DisplayName("DB에 데이터가 없으면 인덱스만 재생성한다") - void run_withNoData_shouldOnlyRecreateIndex() { - // given - ApplicationArguments args = mock(ApplicationArguments.class); - - Page emptyPage = - new PageImpl<>(List.of(), PageRequest.of(0, 500), 0); - - when(repository.findAll(PageRequest.of(0, 500))).thenReturn(emptyPage); - - // when - runner.run(args); - - // then - verify(indexService).recreateIndex(); - verify(indexService, never()).indexAll(anyList()); - } - - @Test - @DisplayName("인덱스 재생성 실패 시 예외를 로깅하고 계속 진행하지 않는다") - void run_onRecreateIndexFailure_shouldLogAndStop() { - // given - ApplicationArguments args = mock(ApplicationArguments.class); - doThrow(new RuntimeException("ES error")).when(indexService).recreateIndex(); - - // when - runner.run(args); - - // then - verify(indexService).recreateIndex(); - verifyNoInteractions(repository); - } - - private HornBugleWorldHistory createEntity( - Long id, String serverName, String characterName, String message) { - return HornBugleWorldHistory.builder() - .id(id) - .serverName(serverName) - .characterName(characterName) - .message(message) - .dateSend(Instant.now()) - .dateRegister(Instant.now()) - .build(); - } -} diff --git a/src/test/java/until/the/eternity/hornBugle/application/service/HornBugleServiceTest.java b/src/test/java/until/the/eternity/hornBugle/application/service/HornBugleServiceTest.java deleted file mode 100644 index 7f167407..00000000 --- a/src/test/java/until/the/eternity/hornBugle/application/service/HornBugleServiceTest.java +++ /dev/null @@ -1,336 +0,0 @@ -package until.the.eternity.hornBugle.application.service; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -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.Pageable; -import until.the.eternity.common.response.PageResponseDto; -import until.the.eternity.hornBugle.domain.entity.HornBugleWorldHistory; -import until.the.eternity.hornBugle.domain.enums.HornBugleServer; -import until.the.eternity.hornBugle.domain.mapper.HornBugleMapper; -import until.the.eternity.hornBugle.domain.repository.HornBugleRepositoryPort; -import until.the.eternity.hornBugle.domain.service.HornBugleDuplicateChecker; -import until.the.eternity.hornBugle.infrastructure.elasticsearch.HornBugleDocument; -import until.the.eternity.hornBugle.infrastructure.elasticsearch.HornBugleIndexService; -import until.the.eternity.hornBugle.interfaces.external.dto.OpenApiHornBugleHistoryResponse; -import until.the.eternity.hornBugle.interfaces.rest.dto.request.HornBuglePageRequestDto; -import until.the.eternity.hornBugle.interfaces.rest.dto.response.HornBugleHistoryResponse; - -import java.time.Instant; -import java.util.List; -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.*; - -@ExtendWith(MockitoExtension.class) -class HornBugleServiceTest { - - @Mock private HornBugleRepositoryPort repository; - @Mock private HornBugleDuplicateChecker duplicateChecker; - @Mock private HornBugleMapper mapper; - @Mock private HornBugleIndexService indexService; - - @Nested - @DisplayName("Elasticsearch 활성화 상태에서 search 메서드 테스트") - class SearchWithElasticsearchTest { - - private HornBugleService service; - - @BeforeEach - void setUp() { - service = - new HornBugleService( - repository, duplicateChecker, mapper, Optional.of(indexService)); - } - - @Test - @DisplayName("keyword가 null이면 DB 검색을 수행한다") - void search_withNullKeyword_shouldSearchDatabase() { - // given - String serverName = "류트"; - String keyword = null; - HornBuglePageRequestDto pageRequest = new HornBuglePageRequestDto(1, 20); - Pageable pageable = pageRequest.toPageable(); - - HornBugleWorldHistory entity = createEntity(1L, "류트", "테스트", "안녕하세요"); - HornBugleHistoryResponse response = createResponse(1L, "류트", "테스트", "안녕하세요"); - Page entityPage = new PageImpl<>(List.of(entity), pageable, 1); - - when(repository.findByServerName(serverName, pageable)).thenReturn(entityPage); - when(mapper.toResponse(entity)).thenReturn(response); - - // when - PageResponseDto result = - service.search(serverName, keyword, pageRequest); - - // then - assertThat(result.items()).hasSize(1); - assertThat(result.items().get(0).characterName()).isEqualTo("테스트"); - verify(repository).findByServerName(serverName, pageable); - verifyNoInteractions(indexService); - } - - @Test - @DisplayName("keyword가 빈 문자열이면 DB 검색을 수행한다") - void search_withEmptyKeyword_shouldSearchDatabase() { - // given - String serverName = null; - String keyword = ""; - HornBuglePageRequestDto pageRequest = new HornBuglePageRequestDto(1, 20); - Pageable pageable = pageRequest.toPageable(); - - HornBugleWorldHistory entity = createEntity(1L, "류트", "테스트", "안녕하세요"); - HornBugleHistoryResponse response = createResponse(1L, "류트", "테스트", "안녕하세요"); - Page entityPage = new PageImpl<>(List.of(entity), pageable, 1); - - when(repository.findAll(pageable)).thenReturn(entityPage); - when(mapper.toResponse(entity)).thenReturn(response); - - // when - PageResponseDto result = - service.search(serverName, keyword, pageRequest); - - // then - assertThat(result.items()).hasSize(1); - verify(repository).findAll(pageable); - verifyNoInteractions(indexService); - } - - @Test - @DisplayName("keyword가 있으면 Elasticsearch 검색을 수행한다") - void search_withKeyword_shouldSearchElasticsearch() { - // given - String serverName = "류트"; - String keyword = "테스트"; - HornBuglePageRequestDto pageRequest = new HornBuglePageRequestDto(1, 20); - Pageable pageable = pageRequest.toPageable(); - - HornBugleDocument document = createDocument("1", "류트", "테스트", "안녕하세요"); - HornBugleHistoryResponse response = createResponse(1L, "류트", "테스트", "안녕하세요"); - Page documentPage = new PageImpl<>(List.of(document), pageable, 1); - - when(indexService.search(keyword, serverName, pageable)).thenReturn(documentPage); - when(mapper.toResponse(document)).thenReturn(response); - - // when - PageResponseDto result = - service.search(serverName, keyword, pageRequest); - - // then - assertThat(result.items()).hasSize(1); - assertThat(result.items().get(0).characterName()).isEqualTo("테스트"); - verify(indexService).search(keyword, serverName, pageable); - verifyNoInteractions(repository); - } - - @Test - @DisplayName("serverName이 null이고 keyword가 있으면 Elasticsearch 전체 검색을 수행한다") - void search_withKeywordAndNoServerName_shouldSearchElasticsearchWithoutFilter() { - // given - String serverName = null; - String keyword = "테스트"; - HornBuglePageRequestDto pageRequest = new HornBuglePageRequestDto(1, 20); - Pageable pageable = pageRequest.toPageable(); - - HornBugleDocument document = createDocument("1", "류트", "테스트", "안녕하세요"); - HornBugleHistoryResponse response = createResponse(1L, "류트", "테스트", "안녕하세요"); - Page documentPage = new PageImpl<>(List.of(document), pageable, 1); - - when(indexService.search(keyword, serverName, pageable)).thenReturn(documentPage); - when(mapper.toResponse(document)).thenReturn(response); - - // when - PageResponseDto result = - service.search(serverName, keyword, pageRequest); - - // then - assertThat(result.items()).hasSize(1); - verify(indexService).search(keyword, null, pageable); - } - } - - @Nested - @DisplayName("Elasticsearch 비활성화 상태에서 search 메서드 테스트") - class SearchWithoutElasticsearchTest { - - private HornBugleService service; - - @BeforeEach - void setUp() { - service = new HornBugleService(repository, duplicateChecker, mapper, Optional.empty()); - } - - @Test - @DisplayName("keyword가 있어도 ES가 비활성화되어 있으면 DB 검색을 수행한다") - void search_withKeywordButNoEs_shouldFallbackToDatabase() { - // given - String serverName = "류트"; - String keyword = "테스트"; - HornBuglePageRequestDto pageRequest = new HornBuglePageRequestDto(1, 20); - Pageable pageable = pageRequest.toPageable(); - - HornBugleWorldHistory entity = createEntity(1L, "류트", "테스트", "안녕하세요"); - HornBugleHistoryResponse response = createResponse(1L, "류트", "테스트", "안녕하세요"); - Page entityPage = new PageImpl<>(List.of(entity), pageable, 1); - - when(repository.findByServerName(serverName, pageable)).thenReturn(entityPage); - when(mapper.toResponse(entity)).thenReturn(response); - - // when - PageResponseDto result = - service.search(serverName, keyword, pageRequest); - - // then - assertThat(result.items()).hasSize(1); - verify(repository).findByServerName(serverName, pageable); - } - } - - @Nested - @DisplayName("saveAll 메서드 테스트") - class SaveAllTest { - - private HornBugleService service; - - @BeforeEach - void setUp() { - service = - new HornBugleService( - repository, duplicateChecker, mapper, Optional.of(indexService)); - } - - @Test - @DisplayName("새 데이터 저장 시 Elasticsearch에도 색인한다") - void saveAll_shouldIndexToElasticsearch() { - // given - HornBugleServer server = HornBugleServer.LUTE; - OpenApiHornBugleHistoryResponse apiResponse = - new OpenApiHornBugleHistoryResponse("테스트", "안녕하세요", Instant.now()); - List responses = List.of(apiResponse); - - HornBugleWorldHistory entity = createEntity(1L, "류트", "테스트", "안녕하세요"); - - when(duplicateChecker.filterDuplicates(server, responses)).thenReturn(responses); - when(mapper.toEntity(eq(apiResponse), eq(server), any(Instant.class))) - .thenReturn(entity); - - // when - int savedCount = service.saveAll(server, responses); - - // then - assertThat(savedCount).isEqualTo(1); - verify(repository).saveAll(anyList()); - verify(indexService).indexAll(anyList()); - } - - @Test - @DisplayName("모든 데이터가 중복이면 저장하지 않는다") - void saveAll_withAllDuplicates_shouldNotSave() { - // given - HornBugleServer server = HornBugleServer.LUTE; - OpenApiHornBugleHistoryResponse apiResponse = - new OpenApiHornBugleHistoryResponse("테스트", "안녕하세요", Instant.now()); - List responses = List.of(apiResponse); - - when(duplicateChecker.filterDuplicates(server, responses)).thenReturn(List.of()); - - // when - int savedCount = service.saveAll(server, responses); - - // then - assertThat(savedCount).isEqualTo(0); - verifyNoInteractions(repository); - verifyNoInteractions(indexService); - } - - @Test - @DisplayName("빈 응답이면 저장하지 않는다") - void saveAll_withEmptyResponses_shouldNotSave() { - // given - HornBugleServer server = HornBugleServer.LUTE; - List responses = List.of(); - - // when - int savedCount = service.saveAll(server, responses); - - // then - assertThat(savedCount).isEqualTo(0); - verifyNoInteractions(duplicateChecker); - verifyNoInteractions(repository); - verifyNoInteractions(indexService); - } - } - - @Nested - @DisplayName("ES 비활성화 상태에서 saveAll 메서드 테스트") - class SaveAllWithoutElasticsearchTest { - - private HornBugleService service; - - @BeforeEach - void setUp() { - service = new HornBugleService(repository, duplicateChecker, mapper, Optional.empty()); - } - - @Test - @DisplayName("ES가 비활성화되어 있으면 DB에만 저장한다") - void saveAll_withoutEs_shouldOnlySaveToDatabase() { - // given - HornBugleServer server = HornBugleServer.LUTE; - OpenApiHornBugleHistoryResponse apiResponse = - new OpenApiHornBugleHistoryResponse("테스트", "안녕하세요", Instant.now()); - List responses = List.of(apiResponse); - - HornBugleWorldHistory entity = createEntity(1L, "류트", "테스트", "안녕하세요"); - - when(duplicateChecker.filterDuplicates(server, responses)).thenReturn(responses); - when(mapper.toEntity(eq(apiResponse), eq(server), any(Instant.class))) - .thenReturn(entity); - - // when - int savedCount = service.saveAll(server, responses); - - // then - assertThat(savedCount).isEqualTo(1); - verify(repository).saveAll(anyList()); - // indexService는 Optional.empty()이므로 호출되지 않음 - } - } - - private HornBugleWorldHistory createEntity( - Long id, String serverName, String characterName, String message) { - return HornBugleWorldHistory.builder() - .id(id) - .serverName(serverName) - .characterName(characterName) - .message(message) - .dateSend(Instant.now()) - .dateRegister(Instant.now()) - .build(); - } - - private HornBugleDocument createDocument( - String id, String serverName, String characterName, String message) { - return HornBugleDocument.builder() - .id(id) - .serverName(serverName) - .characterName(characterName) - .message(message) - .dateSend(Instant.now()) - .dateRegister(Instant.now()) - .build(); - } - - private HornBugleHistoryResponse createResponse( - Long id, String serverName, String characterName, String message) { - return new HornBugleHistoryResponse( - id, serverName, characterName, message, Instant.now(), Instant.now()); - } -} diff --git a/src/test/java/until/the/eternity/hornBugle/infrastructure/elasticsearch/HornBugleIndexServiceTest.java b/src/test/java/until/the/eternity/hornBugle/infrastructure/elasticsearch/HornBugleIndexServiceTest.java deleted file mode 100644 index 051744b5..00000000 --- a/src/test/java/until/the/eternity/hornBugle/infrastructure/elasticsearch/HornBugleIndexServiceTest.java +++ /dev/null @@ -1,238 +0,0 @@ -package until.the.eternity.hornBugle.infrastructure.elasticsearch; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -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.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.elasticsearch.client.elc.NativeQuery; -import org.springframework.data.elasticsearch.core.ElasticsearchOperations; -import org.springframework.data.elasticsearch.core.IndexOperations; -import org.springframework.data.elasticsearch.core.SearchHit; -import org.springframework.data.elasticsearch.core.SearchHits; -import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; -import until.the.eternity.hornBugle.domain.entity.HornBugleWorldHistory; - -import java.time.Instant; -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Mockito.*; - -@ExtendWith(MockitoExtension.class) -class HornBugleIndexServiceTest { - - @Mock private ElasticsearchOperations elasticsearchOperations; - @Mock private HornBugleElasticsearchRepository repository; - - @InjectMocks private HornBugleIndexService indexService; - - @Nested - @DisplayName("index 메서드 테스트") - class IndexTest { - - @Test - @DisplayName("단일 엔티티를 색인한다") - void index_shouldSaveDocument() { - // given - HornBugleWorldHistory entity = createEntity(1L, "류트", "테스트", "안녕하세요"); - - // when - indexService.index(entity); - - // then - verify(repository).save(any(HornBugleDocument.class)); - } - - @Test - @DisplayName("색인 실패 시 예외를 던지지 않고 로그만 기록한다") - void index_onFailure_shouldNotThrowException() { - // given - HornBugleWorldHistory entity = createEntity(1L, "류트", "테스트", "안녕하세요"); - when(repository.save(any(HornBugleDocument.class))) - .thenThrow(new RuntimeException("ES error")); - - // when & then (no exception) - indexService.index(entity); - verify(repository).save(any(HornBugleDocument.class)); - } - } - - @Nested - @DisplayName("indexAll 메서드 테스트") - class IndexAllTest { - - @Test - @DisplayName("여러 엔티티를 일괄 색인한다") - void indexAll_shouldSaveAllDocuments() { - // given - List entities = - List.of( - createEntity(1L, "류트", "테스트1", "안녕하세요1"), - createEntity(2L, "만돌린", "테스트2", "안녕하세요2")); - - // when - indexService.indexAll(entities); - - // then - verify(repository).saveAll(anyList()); - } - - @Test - @DisplayName("빈 리스트면 저장하지 않는다") - void indexAll_withEmptyList_shouldNotSave() { - // when - indexService.indexAll(List.of()); - - // then - verifyNoInteractions(repository); - } - - @Test - @DisplayName("null이면 저장하지 않는다") - void indexAll_withNull_shouldNotSave() { - // when - indexService.indexAll(null); - - // then - verifyNoInteractions(repository); - } - } - - @Nested - @DisplayName("recreateIndex 메서드 테스트") - class RecreateIndexTest { - - @Test - @DisplayName("기존 인덱스가 있으면 삭제 후 재생성한다") - void recreateIndex_withExistingIndex_shouldDeleteAndCreate() { - // given - IndexOperations indexOps = mock(IndexOperations.class); - when(elasticsearchOperations.indexOps(HornBugleDocument.class)).thenReturn(indexOps); - when(indexOps.exists()).thenReturn(true); - when(indexOps.createWithMapping()).thenReturn(true); - - // when - indexService.recreateIndex(); - - // then - verify(indexOps).delete(); - verify(indexOps).createWithMapping(); - } - - @Test - @DisplayName("기존 인덱스가 없으면 생성만 한다") - void recreateIndex_withoutExistingIndex_shouldOnlyCreate() { - // given - IndexOperations indexOps = mock(IndexOperations.class); - when(elasticsearchOperations.indexOps(HornBugleDocument.class)).thenReturn(indexOps); - when(indexOps.exists()).thenReturn(false); - when(indexOps.createWithMapping()).thenReturn(true); - - // when - indexService.recreateIndex(); - - // then - verify(indexOps, never()).delete(); - verify(indexOps).createWithMapping(); - } - - @Test - @DisplayName("인덱스 생성 실패 시 예외를 던진다") - void recreateIndex_onFailure_shouldThrowException() { - // given - IndexOperations indexOps = mock(IndexOperations.class); - when(elasticsearchOperations.indexOps(HornBugleDocument.class)).thenReturn(indexOps); - when(indexOps.exists()).thenThrow(new RuntimeException("ES error")); - - // when & then - assertThatThrownBy(() -> indexService.recreateIndex()) - .isInstanceOf(RuntimeException.class) - .hasMessageContaining("Failed to recreate Elasticsearch index"); - } - } - - @Nested - @DisplayName("search 메서드 테스트") - class SearchTest { - - @Test - @DisplayName("검색 결과를 반환한다") - @SuppressWarnings("unchecked") - void search_shouldReturnResults() { - // given - String keyword = "테스트"; - String serverName = "류트"; - Pageable pageable = PageRequest.of(0, 20); - - HornBugleDocument document = - HornBugleDocument.builder() - .id("1") - .serverName("류트") - .characterName("테스트") - .message("안녕하세요") - .dateSend(Instant.now()) - .dateRegister(Instant.now()) - .build(); - - SearchHit searchHit = mock(SearchHit.class); - when(searchHit.getContent()).thenReturn(document); - - SearchHits searchHits = mock(SearchHits.class); - when(searchHits.getSearchHits()).thenReturn(List.of(searchHit)); - when(searchHits.getTotalHits()).thenReturn(1L); - - when(elasticsearchOperations.search( - any(NativeQuery.class), - eq(HornBugleDocument.class), - any(IndexCoordinates.class))) - .thenReturn(searchHits); - - // when - Page result = indexService.search(keyword, serverName, pageable); - - // then - assertThat(result.getContent()).hasSize(1); - assertThat(result.getContent().get(0).getCharacterName()).isEqualTo("테스트"); - assertThat(result.getTotalElements()).isEqualTo(1L); - } - - @Test - @DisplayName("검색 실패 시 예외를 던진다") - void search_onFailure_shouldThrowException() { - // given - String keyword = "테스트"; - String serverName = "류트"; - Pageable pageable = PageRequest.of(0, 20); - - when(elasticsearchOperations.search( - any(NativeQuery.class), - eq(HornBugleDocument.class), - any(IndexCoordinates.class))) - .thenThrow(new RuntimeException("ES error")); - - // when & then - assertThatThrownBy(() -> indexService.search(keyword, serverName, pageable)) - .isInstanceOf(RuntimeException.class) - .hasMessageContaining("Elasticsearch search failed"); - } - } - - private HornBugleWorldHistory createEntity( - Long id, String serverName, String characterName, String message) { - return HornBugleWorldHistory.builder() - .id(id) - .serverName(serverName) - .characterName(characterName) - .message(message) - .dateSend(Instant.now()) - .dateRegister(Instant.now()) - .build(); - } -}