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 2bcfcad..88f4d4a 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 @@ -157,6 +157,42 @@ private BooleanBuilder buildHistoryPredicate( } } + // 인챈트 검색 조건 + if (c.enchantSearchRequest() != null) { + String enchantPrefix = c.enchantSearchRequest().enchantPrefix(); + String enchantSuffix = c.enchantSearchRequest().enchantSuffix(); + + if (enchantPrefix != null && !enchantPrefix.isBlank()) { + QAuctionHistoryItemOption optPrefix = + new QAuctionHistoryItemOption("enchantPrefix"); + var subPrefix = + JPAExpressions.select(optPrefix.auctionHistory.auctionBuyId) + .from(optPrefix) + .where( + optPrefix + .optionType + .eq("인챈트") + .and(optPrefix.optionSubType.eq("접두")) + .and(optPrefix.optionValue.eq(enchantPrefix))); + builder.and(ah.auctionBuyId.in(subPrefix)); + } + + if (enchantSuffix != null && !enchantSuffix.isBlank()) { + QAuctionHistoryItemOption optSuffix = + new QAuctionHistoryItemOption("enchantSuffix"); + var subSuffix = + JPAExpressions.select(optSuffix.auctionHistory.auctionBuyId) + .from(optSuffix) + .where( + optSuffix + .optionType + .eq("인챈트") + .and(optSuffix.optionSubType.eq("접미")) + .and(optSuffix.optionValue.eq(enchantSuffix))); + builder.and(ah.auctionBuyId.in(subSuffix)); + } + } + return builder; } diff --git a/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/AuctionHistorySearchRequest.java b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/AuctionHistorySearchRequest.java index 72b2bb7..02dd616 100644 --- a/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/AuctionHistorySearchRequest.java +++ b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/AuctionHistorySearchRequest.java @@ -16,4 +16,5 @@ public record AuctionHistorySearchRequest( @Schema(description = "소분류 카테고리", example = "검") String itemSubCategory, @Schema(description = "거래 일자 조건") DateAuctionBuyRequest dateAuctionBuyRequest, @Schema(description = "가격 검색 조건") PriceSearchRequest priceSearchRequest, - @Schema(description = "아이템 옵션 검색 조건") ItemOptionSearchRequest itemOptionSearchRequest) {} + @Schema(description = "아이템 옵션 검색 조건") ItemOptionSearchRequest itemOptionSearchRequest, + @Schema(description = "인챈트 검색 조건") EnchantSearchRequest enchantSearchRequest) {} diff --git a/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/EnchantSearchRequest.java b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/EnchantSearchRequest.java new file mode 100644 index 0000000..5111883 --- /dev/null +++ b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/EnchantSearchRequest.java @@ -0,0 +1,9 @@ +package until.the.eternity.auctionhistory.interfaces.rest.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; + +/** 인챈트 검색 조건 DTO */ +@Schema(description = "인챈트 검색 조건") +public record EnchantSearchRequest( + @Schema(description = "접두 인챈트 fullname (예: 당당한 (6랭크))") String enchantPrefix, + @Schema(description = "접미 인챈트 fullname (예: 강한 (5랭크))") String enchantSuffix) {} 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 14e39bb..a60f726 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 @@ -125,6 +125,42 @@ private BooleanBuilder buildItemPredicate( } } + // 인챈트 검색 조건 + if (c.enchantSearchRequest() != null) { + String enchantPrefix = c.enchantSearchRequest().enchantPrefix(); + String enchantSuffix = c.enchantSearchRequest().enchantSuffix(); + + if (enchantPrefix != null && !enchantPrefix.isBlank()) { + QAuctionRealtimeItemOption optPrefix = + new QAuctionRealtimeItemOption("enchantPrefix"); + var subPrefix = + JPAExpressions.select(optPrefix.auctionRealtimeItem.id) + .from(optPrefix) + .where( + optPrefix + .optionType + .eq("인챈트") + .and(optPrefix.optionSubType.eq("접두")) + .and(optPrefix.optionValue.eq(enchantPrefix))); + builder.and(ar.id.in(subPrefix)); + } + + if (enchantSuffix != null && !enchantSuffix.isBlank()) { + QAuctionRealtimeItemOption optSuffix = + new QAuctionRealtimeItemOption("enchantSuffix"); + var subSuffix = + JPAExpressions.select(optSuffix.auctionRealtimeItem.id) + .from(optSuffix) + .where( + optSuffix + .optionType + .eq("인챈트") + .and(optSuffix.optionSubType.eq("접미")) + .and(optSuffix.optionValue.eq(enchantSuffix))); + builder.and(ar.id.in(subSuffix)); + } + } + return builder; } 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 index dc8396d..258ca1e 100644 --- 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 @@ -1,6 +1,7 @@ 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.EnchantSearchRequest; import until.the.eternity.auctionhistory.interfaces.rest.dto.request.ItemOptionSearchRequest; import until.the.eternity.auctionhistory.interfaces.rest.dto.request.PriceSearchRequest; @@ -17,4 +18,5 @@ public record AuctionRealtimeSearchRequest( @Schema(description = "대분류 카테고리", example = "근거리 장비") String itemTopCategory, @Schema(description = "소분류 카테고리", example = "검") String itemSubCategory, @Schema(description = "가격 검색 조건") PriceSearchRequest priceSearchRequest, - @Schema(description = "아이템 옵션 검색 조건") ItemOptionSearchRequest itemOptionSearchRequest) {} + @Schema(description = "아이템 옵션 검색 조건") ItemOptionSearchRequest itemOptionSearchRequest, + @Schema(description = "인챈트 검색 조건") EnchantSearchRequest enchantSearchRequest) {} diff --git a/src/main/java/until/the/eternity/enchantinfo/application/service/EnchantInfoService.java b/src/main/java/until/the/eternity/enchantinfo/application/service/EnchantInfoService.java index c39ca8f..396b881 100644 --- a/src/main/java/until/the/eternity/enchantinfo/application/service/EnchantInfoService.java +++ b/src/main/java/until/the/eternity/enchantinfo/application/service/EnchantInfoService.java @@ -1,11 +1,14 @@ package until.the.eternity.enchantinfo.application.service; import java.util.List; +import java.util.Set; import lombok.RequiredArgsConstructor; 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.common.exception.CustomException; +import until.the.eternity.enchantinfo.domain.exception.EnchantInfoExceptionCode; import until.the.eternity.enchantinfo.domain.repository.EnchantInfoRepositoryPort; import until.the.eternity.enchantinfo.interfaces.rest.dto.response.EnchantInfoResponse; import until.the.eternity.enchantinfo.interfaces.rest.dto.response.EnchantInfoSyncResponse; @@ -15,13 +18,21 @@ @RequiredArgsConstructor public class EnchantInfoService { + private static final Set ALLOWED_AFFIX_POSITIONS = Set.of("접두", "접미"); + private final EnchantInfoRepositoryPort enchantInfoRepository; public Page findAll(Pageable pageable) { return enchantInfoRepository.findAll(pageable).map(EnchantInfoResponse::from); } - public List findAllFullnames() { + public List findAllFullnames(String affixPosition) { + if (affixPosition != null) { + if (!ALLOWED_AFFIX_POSITIONS.contains(affixPosition)) { + throw new CustomException(EnchantInfoExceptionCode.INVALID_AFFIX_POSITION); + } + return enchantInfoRepository.findAllFullnamesByAffixPosition(affixPosition); + } return enchantInfoRepository.findAllFullnames(); } diff --git a/src/main/java/until/the/eternity/enchantinfo/domain/exception/EnchantInfoExceptionCode.java b/src/main/java/until/the/eternity/enchantinfo/domain/exception/EnchantInfoExceptionCode.java new file mode 100644 index 0000000..ab88e52 --- /dev/null +++ b/src/main/java/until/the/eternity/enchantinfo/domain/exception/EnchantInfoExceptionCode.java @@ -0,0 +1,23 @@ +package until.the.eternity.enchantinfo.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; + +@Getter +@RequiredArgsConstructor +public enum EnchantInfoExceptionCode implements ExceptionCode { + INVALID_AFFIX_POSITION(BAD_REQUEST, "affix_position은 '접두' 또는 '접미'만 허용됩니다."), + ; + + private final HttpStatus status; + private final String message; + + @Override + public String getCode() { + return this.name(); + } +} diff --git a/src/main/java/until/the/eternity/enchantinfo/domain/repository/EnchantInfoRepositoryPort.java b/src/main/java/until/the/eternity/enchantinfo/domain/repository/EnchantInfoRepositoryPort.java index 937ca12..753bf6a 100644 --- a/src/main/java/until/the/eternity/enchantinfo/domain/repository/EnchantInfoRepositoryPort.java +++ b/src/main/java/until/the/eternity/enchantinfo/domain/repository/EnchantInfoRepositoryPort.java @@ -11,5 +11,7 @@ public interface EnchantInfoRepositoryPort { List findAllFullnames(); + List findAllFullnamesByAffixPosition(String affixPosition); + int upsertFromAuctionHistory(); } diff --git a/src/main/java/until/the/eternity/enchantinfo/infrastructure/persistence/EnchantInfoJpaRepository.java b/src/main/java/until/the/eternity/enchantinfo/infrastructure/persistence/EnchantInfoJpaRepository.java index 18fe98d..392c1fe 100644 --- a/src/main/java/until/the/eternity/enchantinfo/infrastructure/persistence/EnchantInfoJpaRepository.java +++ b/src/main/java/until/the/eternity/enchantinfo/infrastructure/persistence/EnchantInfoJpaRepository.java @@ -10,6 +10,11 @@ public interface EnchantInfoJpaRepository extends JpaRepository findAllFullnames(); + @Query( + "SELECT e.fullname FROM EnchantInfoEntity e WHERE e.affixPosition = :affixPosition ORDER BY e.id ASC") + List findAllFullnamesByAffixPosition( + @org.springframework.data.repository.query.Param("affixPosition") String affixPosition); + @Modifying @Query( value = diff --git a/src/main/java/until/the/eternity/enchantinfo/infrastructure/persistence/EnchantInfoRepositoryPortImpl.java b/src/main/java/until/the/eternity/enchantinfo/infrastructure/persistence/EnchantInfoRepositoryPortImpl.java index 6417f72..e0c2731 100644 --- a/src/main/java/until/the/eternity/enchantinfo/infrastructure/persistence/EnchantInfoRepositoryPortImpl.java +++ b/src/main/java/until/the/eternity/enchantinfo/infrastructure/persistence/EnchantInfoRepositoryPortImpl.java @@ -23,6 +23,11 @@ public List findAllFullnames() { return jpaRepository.findAllFullnames(); } + @Override + public List findAllFullnamesByAffixPosition(String affixPosition) { + return jpaRepository.findAllFullnamesByAffixPosition(affixPosition); + } + @Override public int upsertFromAuctionHistory() { return jpaRepository.upsertFromAuctionHistory(); diff --git a/src/main/java/until/the/eternity/enchantinfo/interfaces/rest/controller/EnchantInfoController.java b/src/main/java/until/the/eternity/enchantinfo/interfaces/rest/controller/EnchantInfoController.java index 0a63690..14222db 100644 --- a/src/main/java/until/the/eternity/enchantinfo/interfaces/rest/controller/EnchantInfoController.java +++ b/src/main/java/until/the/eternity/enchantinfo/interfaces/rest/controller/EnchantInfoController.java @@ -1,6 +1,7 @@ package until.the.eternity.enchantinfo.interfaces.rest.controller; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; @@ -13,6 +14,7 @@ import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import until.the.eternity.enchantinfo.application.service.EnchantInfoService; import until.the.eternity.enchantinfo.interfaces.rest.dto.request.EnchantInfoPageRequestDto; @@ -41,10 +43,16 @@ public ResponseEntity> getEnchantInfos( @Operation( summary = "모든 인챈트 fullname 조회", - description = "페이지네이션 없이 저장된 모든 인챈트의 fullname(이름 및 랭크)을 한 번에 조회합니다.") + description = + "페이지네이션 없이 저장된 모든 인챈트의 fullname(이름 및 랭크)을 한 번에 조회합니다. " + + "affix_position을 지정하면 해당 위치(접두/접미)의 인챈트만 필터링합니다.") + @ApiResponse(responseCode = "400", description = "잘못된 affix_position 값 (접두 또는 접미만 허용)") @GetMapping("/fullnames") - public List getAllEnchantFullnames() { - return enchantInfoService.findAllFullnames(); + public List getAllEnchantFullnames( + @Parameter(description = "접두/접미 구분 필터 (허용값: 접두, 접미)", example = "접두") + @RequestParam(name = "affix_position", required = false) + String affixPosition) { + return enchantInfoService.findAllFullnames(affixPosition); } @Operation( diff --git a/src/main/java/until/the/eternity/enchantinfo/interfaces/rest/dto/request/EnchantInfoPageRequestDto.java b/src/main/java/until/the/eternity/enchantinfo/interfaces/rest/dto/request/EnchantInfoPageRequestDto.java index 508e694..0ad2a67 100644 --- a/src/main/java/until/the/eternity/enchantinfo/interfaces/rest/dto/request/EnchantInfoPageRequestDto.java +++ b/src/main/java/until/the/eternity/enchantinfo/interfaces/rest/dto/request/EnchantInfoPageRequestDto.java @@ -28,6 +28,8 @@ public Pageable toPageable() { this.direction != null ? this.direction : DEFAULT_DIRECTION; return PageRequest.of( - resolvedPage, resolvedSize, Sort.by(resolvedDirection.toSpringDirection(), SORT_FIELD)); + resolvedPage, + resolvedSize, + Sort.by(resolvedDirection.toSpringDirection(), SORT_FIELD)); } } 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 c9ad8af..df502fd 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 @@ -42,7 +42,7 @@ class AuctionHistoryServiceTest { void search_should_return_paged_response() { // given AuctionHistorySearchRequest searchRequest = - new AuctionHistorySearchRequest(null, null, null, null, null, null, null); + new AuctionHistorySearchRequest(null, null, null, null, null, null, null, null); PageRequestDto pageRequestDto = mock(PageRequestDto.class); Pageable pageable = PageRequest.of(0, 10); when(pageRequestDto.toPageable()).thenReturn(pageable);