From 6470d23adc2f23aacde998bbcac59db9aa67bc61 Mon Sep 17 00:00:00 2001 From: Sanghyun Yi Date: Mon, 20 Oct 2025 22:48:25 +0900 Subject: [PATCH 1/8] =?UTF-8?q?refactor:=20auction=20history=20search=20re?= =?UTF-8?q?quest=20dto=20record=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../request/AuctionHistorySearchRequest.java | 25 ++++++------------- .../request/PricePerUnitSearchRequest.java | 4 +++ 2 files changed, 11 insertions(+), 18 deletions(-) create mode 100644 src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/PricePerUnitSearchRequest.java 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 49450b7..415c315 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 @@ -1,23 +1,12 @@ package until.the.eternity.auctionhistory.interfaces.rest.dto.request; import io.swagger.v3.oas.annotations.media.Schema; -import lombok.*; /** 경매 히스토리 검색 조건 DTO - 페이지네이션 포함 */ -@Getter -@Setter -@ToString(callSuper = true) -@NoArgsConstructor -@AllArgsConstructor -@Builder -public class AuctionHistorySearchRequest { - - @Schema(description = "아이템 이름 (like 검색)", example = "페러시우스 타이탄 블레이드") - private String itemName; - - @Schema(description = "대분류 카테고리", example = "근거리 장비") - private String itemTopCategory; - - @Schema(description = "소분류 카테고리", example = "검") - private String itemSubCategory; -} +public record AuctionHistorySearchRequest( + @Schema(description = "아이템 이름 (like 검색)", example = "페러시우스 타이탄 블레이드") String itemName, + @Schema(description = "대분류 카테고리", example = "근거리 장비") String itemTopCategory, + @Schema(description = "소분류 카테고리", example = "검") String itemSubCategory, + @Schema(description = "거래 가격", example = "10000000") String auction_price_per_unit, + @Schema(description = "거래 일자", example = "검") String date_auction_buy, + @Schema(description = "거래 금액") PricePerUnitSearchRequest pricePerUnitSearchRequest) {} diff --git a/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/PricePerUnitSearchRequest.java b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/PricePerUnitSearchRequest.java new file mode 100644 index 0000000..4b5c458 --- /dev/null +++ b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/PricePerUnitSearchRequest.java @@ -0,0 +1,4 @@ +package until.the.eternity.auctionhistory.interfaces.rest.dto.request; + +public record PricePerUnitSearchRequest() { +} From 8f28559f664d6a4a7a0c1fdd0e1954849acc804b Mon Sep 17 00:00:00 2001 From: Sanghyun Yi Date: Mon, 20 Oct 2025 22:48:58 +0900 Subject: [PATCH 2/8] =?UTF-8?q?feat:=20:=20price=20=EA=B2=80=EC=83=89=20dt?= =?UTF-8?q?o=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../rest/dto/request/PricePerUnitSearchRequest.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/PricePerUnitSearchRequest.java b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/PricePerUnitSearchRequest.java index 4b5c458..1aed2a1 100644 --- a/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/PricePerUnitSearchRequest.java +++ b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/PricePerUnitSearchRequest.java @@ -1,4 +1,8 @@ package until.the.eternity.auctionhistory.interfaces.rest.dto.request; -public record PricePerUnitSearchRequest() { -} +import io.swagger.v3.oas.annotations.media.Schema; + +public record PricePerUnitSearchRequest( + @Schema(description = "가격 최소값", example = "0", defaultValue = "0") long PriceTo, + @Schema(description = "가격 최대값", example = "9999999999", defaultValue = "9999999999") + long PriceFrom) {} From 86a5ed9189f483d78e37c270f25fcd10ceaad37c Mon Sep 17 00:00:00 2001 From: Sanghyun Yi Date: Mon, 20 Oct 2025 22:50:03 +0900 Subject: [PATCH 3/8] =?UTF-8?q?fix:=20auction=20history=20search=20request?= =?UTF-8?q?=20dto=20class=20=EB=B3=80=EA=B2=BD=20=EC=82=AC=ED=95=AD=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AuctionHistoryQueryDslRepository.java | 12 ++++++------ .../dto/request/AuctionHistorySearchRequest.java | 2 +- ...nitSearchRequest.java => PriceSearchRequest.java} | 2 +- .../service/AuctionHistoryServiceTest.java | 3 ++- 4 files changed, 10 insertions(+), 9 deletions(-) rename src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/{PricePerUnitSearchRequest.java => PriceSearchRequest.java} (89%) 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 a2cacf8..c4473a4 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 @@ -39,14 +39,14 @@ public Page search(AuctionHistorySearchRequest condition, Pageab private BooleanBuilder buildPredicate(AuctionHistorySearchRequest c, QAuctionHistory ah) { BooleanBuilder builder = new BooleanBuilder(); - if (c.getItemTopCategory() != null && !c.getItemTopCategory().isBlank()) { - builder.and(ah.itemTopCategory.eq(c.getItemTopCategory())); + if (c.itemTopCategory() != null && !c.itemTopCategory().isBlank()) { + builder.and(ah.itemTopCategory.eq(c.itemTopCategory())); } - if (c.getItemSubCategory() != null && !c.getItemSubCategory().isBlank()) { - builder.and(ah.itemSubCategory.eq(c.getItemSubCategory())); + if (c.itemSubCategory() != null && !c.itemSubCategory().isBlank()) { + builder.and(ah.itemSubCategory.eq(c.itemSubCategory())); } - if (c.getItemName() != null && !c.getItemName().isBlank()) { - builder.and(ah.itemName.containsIgnoreCase(c.getItemName())); + if (c.itemName() != null && !c.itemName().isBlank()) { + builder.and(ah.itemName.containsIgnoreCase(c.itemName())); } 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 415c315..fce2675 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 @@ -9,4 +9,4 @@ public record AuctionHistorySearchRequest( @Schema(description = "소분류 카테고리", example = "검") String itemSubCategory, @Schema(description = "거래 가격", example = "10000000") String auction_price_per_unit, @Schema(description = "거래 일자", example = "검") String date_auction_buy, - @Schema(description = "거래 금액") PricePerUnitSearchRequest pricePerUnitSearchRequest) {} + @Schema(description = "거래 금액") PriceSearchRequest priceSearchRequest) {} diff --git a/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/PricePerUnitSearchRequest.java b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/PriceSearchRequest.java similarity index 89% rename from src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/PricePerUnitSearchRequest.java rename to src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/PriceSearchRequest.java index 1aed2a1..9c82c49 100644 --- a/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/PricePerUnitSearchRequest.java +++ b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/PriceSearchRequest.java @@ -2,7 +2,7 @@ import io.swagger.v3.oas.annotations.media.Schema; -public record PricePerUnitSearchRequest( +public record PriceSearchRequest( @Schema(description = "가격 최소값", example = "0", defaultValue = "0") long PriceTo, @Schema(description = "가격 최대값", example = "9999999999", defaultValue = "9999999999") long PriceFrom) {} 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 92c9bc6..ed2fea8 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 @@ -41,7 +41,8 @@ class AuctionHistoryServiceTest { @DisplayName("검색은 무조건 페이지를 반환한다") void search_should_return_paged_response() { // given - AuctionHistorySearchRequest searchRequest = new AuctionHistorySearchRequest(); + AuctionHistorySearchRequest searchRequest = + new AuctionHistorySearchRequest(null, null, null, null, null, null); PageRequestDto pageRequestDto = mock(PageRequestDto.class); Pageable pageable = PageRequest.of(0, 10); when(pageRequestDto.toPageable()).thenReturn(pageable); From a8915e78c9d801dd8f0f494a7a51c094056b4abe Mon Sep 17 00:00:00 2001 From: Sanghyun Yi Date: Mon, 20 Oct 2025 23:49:39 +0900 Subject: [PATCH 4/8] =?UTF-8?q?feat:=20auction=20search=20option=20meta=20?= =?UTF-8?q?data=20flyway=20script=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ..._insert_auction_search_option_metadata.sql | 211 ++++++++++++++++++ ..._create_auction_search_option_metadata.sql | 13 ++ 2 files changed, 224 insertions(+) create mode 100644 src/main/resources/db/migration/R__insert_auction_search_option_metadata.sql create mode 100644 src/main/resources/db/migration/V11__create_auction_search_option_metadata.sql diff --git a/src/main/resources/db/migration/R__insert_auction_search_option_metadata.sql b/src/main/resources/db/migration/R__insert_auction_search_option_metadata.sql new file mode 100644 index 0000000..759cecd --- /dev/null +++ b/src/main/resources/db/migration/R__insert_auction_search_option_metadata.sql @@ -0,0 +1,211 @@ +-- 경매 검색 옵션 메타데이터 초기 데이터 +-- Repeatable: 데이터 변경 시 자동 재실행 + +-- 기존 데이터 삭제 (Repeatable 스크립트이므로) +DELETE FROM `auction_search_option_metadata`; + +-- 1. 밸런스 +INSERT INTO `auction_search_option_metadata` +(`search_option_name`, `search_condition_json`, `display_order`, `is_active`) +VALUES ( + '밸런스', + JSON_OBJECT( + 'Balance', JSON_OBJECT('type', 'tinyint', 'required', false), + 'BalanceStandard', JSON_OBJECT('type', 'string', 'allowedValues', JSON_ARRAY('UP', 'DOWN'), 'required', false) + ), + 1, + true +); + +-- 2. 크리티컬 +INSERT INTO `auction_search_option_metadata` +(`search_option_name`, `search_condition_json`, `display_order`, `is_active`) +VALUES ( + '크리티컬', + JSON_OBJECT( + 'Critical', JSON_OBJECT('type', 'tinyint', 'required', false), + 'CriticalStandard', JSON_OBJECT('type', 'string', 'allowedValues', JSON_ARRAY('UP', 'DOWN'), 'required', false) + ), + 2, + true +); + +-- 3. 방어력 +INSERT INTO `auction_search_option_metadata` +(`search_option_name`, `search_condition_json`, `display_order`, `is_active`) +VALUES ( + '방어력', + JSON_OBJECT( + 'Defense', JSON_OBJECT('type', 'tinyint', 'required', false), + 'DefenseStandard', JSON_OBJECT('type', 'string', 'allowedValues', JSON_ARRAY('UP', 'DOWN'), 'required', false) + ), + 3, + true +); + +-- 4. 에르그 (범위) +INSERT INTO `auction_search_option_metadata` +(`search_option_name`, `search_condition_json`, `display_order`, `is_active`) +VALUES ( + '에르그', + JSON_OBJECT( + 'ErgFrom', JSON_OBJECT('type', 'tinyint', 'required', false), + 'ErgTo', JSON_OBJECT('type', 'tinyint', 'required', false) + ), + 4, + true +); + +-- 5. 에르그 등급 +INSERT INTO `auction_search_option_metadata` +(`search_option_name`, `search_condition_json`, `display_order`, `is_active`) +VALUES ( + '에르그 등급', + JSON_OBJECT( + 'ErgRank', JSON_OBJECT('type', 'string', 'allowedValues', JSON_ARRAY('S등급', 'A등급', 'B등급'), 'required', false) + ), + 5, + true +); + +-- 6. 마법 방어력 +INSERT INTO `auction_search_option_metadata` +(`search_option_name`, `search_condition_json`, `display_order`, `is_active`) +VALUES ( + '마법 방어력', + JSON_OBJECT( + 'MagicDefense', JSON_OBJECT('type', 'tinyint', 'required', false), + 'MagicDefenseStandard', JSON_OBJECT('type', 'string', 'allowedValues', JSON_ARRAY('UP', 'DOWN'), 'required', false) + ), + 6, + true +); + +-- 7. 마법 보호 +INSERT INTO `auction_search_option_metadata` +(`search_option_name`, `search_condition_json`, `display_order`, `is_active`) +VALUES ( + '마법 보호', + JSON_OBJECT( + 'MagicProtect', JSON_OBJECT('type', 'tinyint', 'required', false), + 'MagicProtectStandard', JSON_OBJECT('type', 'string', 'allowedValues', JSON_ARRAY('UP', 'DOWN'), 'required', false) + ), + 7, + true +); + +-- 8. 최대 공격력 +INSERT INTO `auction_search_option_metadata` +(`search_option_name`, `search_condition_json`, `display_order`, `is_active`) +VALUES ( + '최대 공격력', + JSON_OBJECT( + 'MaxAttackFrom', JSON_OBJECT('type', 'int', 'required', false), + 'MaxAttackTo', JSON_OBJECT('type', 'int', 'required', false) + ), + 8, + true +); + +-- 9. 최대 내구력 +INSERT INTO `auction_search_option_metadata` +(`search_option_name`, `search_condition_json`, `display_order`, `is_active`) +VALUES ( + '최대 내구력', + JSON_OBJECT( + 'MaximumDurability', JSON_OBJECT('type', 'tinyint', 'required', false), + 'MaximumDurabilityStandard', JSON_OBJECT('type', 'string', 'allowedValues', JSON_ARRAY('UP', 'DOWN'), 'required', false) + ), + 9, + true +); + +-- 10. 최대 부상률 +INSERT INTO `auction_search_option_metadata` +(`search_option_name`, `search_condition_json`, `display_order`, `is_active`) +VALUES ( + '최대 부상률', + JSON_OBJECT( + 'MaxInjuryRateFrom', JSON_OBJECT('type', 'tinyint', 'required', false), + 'MaxInjuryRateTo', JSON_OBJECT('type', 'tinyint', 'required', false) + ), + 10, + true +); + +-- 11. 숙련도 +INSERT INTO `auction_search_option_metadata` +(`search_option_name`, `search_condition_json`, `display_order`, `is_active`) +VALUES ( + '숙련도', + JSON_OBJECT( + 'Proficiency', JSON_OBJECT('type', 'tinyint', 'required', false), + 'ProficiencyStandard', JSON_OBJECT('type', 'string', 'allowedValues', JSON_ARRAY('UP', 'DOWN'), 'required', false) + ), + 11, + true +); + +-- 12. 보호 +INSERT INTO `auction_search_option_metadata` +(`search_option_name`, `search_condition_json`, `display_order`, `is_active`) +VALUES ( + '보호', + JSON_OBJECT( + 'Protect', JSON_OBJECT('type', 'tinyint', 'required', false), + 'ProtectStandard', JSON_OBJECT('type', 'string', 'allowedValues', JSON_ARRAY('UP', 'DOWN'), 'required', false) + ), + 12, + true +); + +-- 13. 남은 거래 횟수 +INSERT INTO `auction_search_option_metadata` +(`search_option_name`, `search_condition_json`, `display_order`, `is_active`) +VALUES ( + '남은 거래 횟수', + JSON_OBJECT( + 'RemainingTransactionCount', JSON_OBJECT('type', 'tinyint', 'required', false), + 'RemainingTransactionCountStandard', JSON_OBJECT('type', 'string', 'allowedValues', JSON_ARRAY('UP', 'DOWN'), 'required', false) + ), + 13, + true +); + +-- 14. 남은 전용 해제 가능 횟수 +INSERT INTO `auction_search_option_metadata` +(`search_option_name`, `search_condition_json`, `display_order`, `is_active`) +VALUES ( + '남은 전용 해제 가능 횟수', + JSON_OBJECT( + 'RemainingUnsealCount', JSON_OBJECT('type', 'tinyint', 'required', false), + 'RemainingUnsealCountStandard', JSON_OBJECT('type', 'string', 'allowedValues', JSON_ARRAY('UP', 'DOWN'), 'required', false) + ), + 14, + true +); + +-- 15. 남은 사용 횟수 +INSERT INTO `auction_search_option_metadata` +(`search_option_name`, `search_condition_json`, `display_order`, `is_active`) +VALUES ( + '남은 사용 횟수', + JSON_OBJECT( + 'RemainingUseCount', JSON_OBJECT('type', 'tinyint', 'required', false), + 'RemainingUseCountStandard', JSON_OBJECT('type', 'string', 'allowedValues', JSON_ARRAY('UP', 'DOWN'), 'required', false) + ), + 15, + true +); + +-- 16. 착용 제한 +INSERT INTO `auction_search_option_metadata` +(`search_option_name`, `search_condition_json`, `display_order`, `is_active`) +VALUES ( + '착용 제한', + JSON_OBJECT( + 'WearingRestrictions', JSON_OBJECT('type', 'string', 'required', false) + ), + 16, + true +); diff --git a/src/main/resources/db/migration/V11__create_auction_search_option_metadata.sql b/src/main/resources/db/migration/V11__create_auction_search_option_metadata.sql new file mode 100644 index 0000000..210462c --- /dev/null +++ b/src/main/resources/db/migration/V11__create_auction_search_option_metadata.sql @@ -0,0 +1,13 @@ +-- 경매 검색 옵션 메타데이터 테이블 생성 +CREATE TABLE `auction_search_option_metadata` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '고유 ID', + `search_option_name` VARCHAR(100) NOT NULL COMMENT '검색 옵션명 (한글)', + `search_condition_json` JSON NOT NULL COMMENT '검색 조건 (파라미터명:타입)', + `display_order` INT NOT NULL UNIQUE COMMENT '정렬 순서 (고유값)', + `is_active` BOOLEAN NOT NULL DEFAULT TRUE COMMENT '활성화 여부', + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '생성일시', + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '수정일시', + PRIMARY KEY (`id`), + INDEX `idx_display_order` (`display_order`), + INDEX `idx_is_active` (`is_active`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='경매 검색 옵션 메타데이터'; From 2cb4be2b84a966ef6e8551d2ed5cd22bc02a9d93 Mon Sep 17 00:00:00 2001 From: Sanghyun Yi Date: Tue, 21 Oct 2025 00:07:49 +0900 Subject: [PATCH 5/8] =?UTF-8?q?feat:=20auction=20search=20option=20meta=20?= =?UTF-8?q?=C3=AC=20=EC=A1=B0=ED=9A=8C=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/AuctionSearchOptionService.java | 56 +++++++++++++++++++ .../entity/AuctionSearchOptionMetadata.java | 50 +++++++++++++++++ .../AuctionSearchOptionRepositoryPort.java | 21 +++++++ .../AuctionSearchOptionJpaRepository.java | 15 +++++ ...AuctionSearchOptionRepositoryPortImpl.java | 24 ++++++++ .../rest/AuctionSearchOptionController.java | 31 ++++++++++ .../rest/dto/response/FieldMetadata.java | 13 +++++ .../SearchOptionMetadataResponse.java | 11 ++++ 8 files changed, 221 insertions(+) create mode 100644 src/main/java/until/the/eternity/auctionsearchoption/application/service/AuctionSearchOptionService.java create mode 100644 src/main/java/until/the/eternity/auctionsearchoption/domain/entity/AuctionSearchOptionMetadata.java create mode 100644 src/main/java/until/the/eternity/auctionsearchoption/domain/repository/AuctionSearchOptionRepositoryPort.java create mode 100644 src/main/java/until/the/eternity/auctionsearchoption/infrastructure/persistence/AuctionSearchOptionJpaRepository.java create mode 100644 src/main/java/until/the/eternity/auctionsearchoption/infrastructure/persistence/AuctionSearchOptionRepositoryPortImpl.java create mode 100644 src/main/java/until/the/eternity/auctionsearchoption/interfaces/rest/AuctionSearchOptionController.java create mode 100644 src/main/java/until/the/eternity/auctionsearchoption/interfaces/rest/dto/response/FieldMetadata.java create mode 100644 src/main/java/until/the/eternity/auctionsearchoption/interfaces/rest/dto/response/SearchOptionMetadataResponse.java 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 new file mode 100644 index 0000000..aec4e46 --- /dev/null +++ b/src/main/java/until/the/eternity/auctionsearchoption/application/service/AuctionSearchOptionService.java @@ -0,0 +1,56 @@ +package until.the.eternity.auctionsearchoption.application.service; + +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; +import org.springframework.transaction.annotation.Transactional; +import until.the.eternity.auctionsearchoption.domain.entity.AuctionSearchOptionMetadata; +import until.the.eternity.auctionsearchoption.domain.repository.AuctionSearchOptionRepositoryPort; +import until.the.eternity.auctionsearchoption.interfaces.rest.dto.response.FieldMetadata; +import until.the.eternity.auctionsearchoption.interfaces.rest.dto.response.SearchOptionMetadataResponse; + +@Slf4j +@Service +@RequiredArgsConstructor +public class AuctionSearchOptionService { + + private final AuctionSearchOptionRepositoryPort repositoryPort; + private final ObjectMapper objectMapper; + + /** + * 모든 활성화된 검색 옵션 조회 + * + * @return 검색 옵션 메타데이터 리스트 + */ + @Transactional(readOnly = true) + public List getAllActiveSearchOptions() { + List entities = repositoryPort.findAllActive(); + + return entities.stream().map(this::toResponse).toList(); + } + + private SearchOptionMetadataResponse toResponse(AuctionSearchOptionMetadata entity) { + Map searchCondition = + parseJsonToFieldMetadata(entity.getSearchConditionJson()); + + return new SearchOptionMetadataResponse( + entity.getId(), + entity.getSearchOptionName(), + searchCondition, + entity.getDisplayOrder()); + } + + private Map parseJsonToFieldMetadata(String json) { + try { + TypeReference> typeRef = new TypeReference<>() {}; + return objectMapper.readValue(json, typeRef); + } catch (Exception e) { + log.error("Failed to parse JSON to FieldMetadata: {}", json, e); + throw new IllegalStateException("JSON 파싱 실패", e); + } + } +} 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 new file mode 100644 index 0000000..21a9e82 --- /dev/null +++ b/src/main/java/until/the/eternity/auctionsearchoption/domain/entity/AuctionSearchOptionMetadata.java @@ -0,0 +1,50 @@ +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; + +@Entity +@Table(name = "auction_search_option_metadata") +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class AuctionSearchOptionMetadata { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "search_option_name", nullable = false, length = 100) + private String searchOptionName; + + @JdbcTypeCode(SqlTypes.JSON) + @Column(name = "search_condition_json", nullable = false, columnDefinition = "JSON") + private String searchConditionJson; + + @Column(name = "display_order", nullable = false, unique = true) + private Integer displayOrder; + + @Column(name = "is_active", nullable = false) + private Boolean isActive = true; + + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; + + @Column(name = "updated_at", nullable = false) + private LocalDateTime updatedAt; + + @PrePersist + protected void onCreate() { + createdAt = LocalDateTime.now(); + updatedAt = LocalDateTime.now(); + } + + @PreUpdate + protected void onUpdate() { + updatedAt = LocalDateTime.now(); + } +} 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 new file mode 100644 index 0000000..971be29 --- /dev/null +++ b/src/main/java/until/the/eternity/auctionsearchoption/domain/repository/AuctionSearchOptionRepositoryPort.java @@ -0,0 +1,21 @@ +package until.the.eternity.auctionsearchoption.domain.repository; + +import java.util.List; +import until.the.eternity.auctionsearchoption.domain.entity.AuctionSearchOptionMetadata; + +public interface AuctionSearchOptionRepositoryPort { + + /** + * 모든 활성화된 검색 옵션 조회 (정렬 순서대로) + * + * @return 검색 옵션 메타데이터 리스트 + */ + List findAllActive(); + + /** + * 모든 검색 옵션 조회 (정렬 순서대로) + * + * @return 검색 옵션 메타데이터 리스트 + */ + List findAll(); +} 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 new file mode 100644 index 0000000..e707457 --- /dev/null +++ b/src/main/java/until/the/eternity/auctionsearchoption/infrastructure/persistence/AuctionSearchOptionJpaRepository.java @@ -0,0 +1,15 @@ +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; + +@Repository +interface AuctionSearchOptionJpaRepository + extends JpaRepository { + + List findByIsActiveTrueOrderByDisplayOrderAsc(); + + List findAllByOrderByDisplayOrderAsc(); +} 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 new file mode 100644 index 0000000..850f43f --- /dev/null +++ b/src/main/java/until/the/eternity/auctionsearchoption/infrastructure/persistence/AuctionSearchOptionRepositoryPortImpl.java @@ -0,0 +1,24 @@ +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; + +@Component +@RequiredArgsConstructor +class AuctionSearchOptionRepositoryPortImpl implements AuctionSearchOptionRepositoryPort { + + private final AuctionSearchOptionJpaRepository jpaRepository; + + @Override + public List findAllActive() { + return jpaRepository.findByIsActiveTrueOrderByDisplayOrderAsc(); + } + + @Override + public List findAll() { + return jpaRepository.findAllByOrderByDisplayOrderAsc(); + } +} 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 new file mode 100644 index 0000000..8ad6484 --- /dev/null +++ b/src/main/java/until/the/eternity/auctionsearchoption/interfaces/rest/AuctionSearchOptionController.java @@ -0,0 +1,31 @@ +package until.the.eternity.auctionsearchoption.interfaces.rest; + +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; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import until.the.eternity.auctionsearchoption.application.service.AuctionSearchOptionService; +import until.the.eternity.auctionsearchoption.interfaces.rest.dto.response.SearchOptionMetadataResponse; +import until.the.eternity.common.response.ApiResponse; + +@Tag(name = "Auction Search Option", description = "경매 검색 옵션 API") +@RestController +@RequestMapping("/api/search-option") +@RequiredArgsConstructor +public class AuctionSearchOptionController { + + private final AuctionSearchOptionService service; + + @Operation(summary = "검색 옵션 메타데이터 조회", description = "경매 검색에 사용 가능한 모든 옵션 메타데이터를 조회합니다.") + @GetMapping + public ResponseEntity>> getSearchOptions() { + List searchOptions = service.getAllActiveSearchOptions(); + + return ResponseEntity.ok( + ApiResponse.success("SEARCH_OPTION_SUCCESS", "검색 옵션 조회 성공", searchOptions)); + } +} 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 new file mode 100644 index 0000000..8b8b6ca --- /dev/null +++ b/src/main/java/until/the/eternity/auctionsearchoption/interfaces/rest/dto/response/FieldMetadata.java @@ -0,0 +1,13 @@ +package until.the.eternity.auctionsearchoption.interfaces.rest.dto.response; + +import com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; + +@Schema(description = "검색 조건 필드 메타데이터") +@JsonInclude(JsonInclude.Include.NON_NULL) +public record FieldMetadata( + @Schema(description = "필드 타입", example = "tinyint") String type, + @Schema(description = "필수 여부", example = "false") Boolean required, + @Schema(description = "허용된 값 목록 (Enum인 경우)", example = "[\"UP\", \"DOWN\"]") + List allowedValues) {} 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 new file mode 100644 index 0000000..04581ef --- /dev/null +++ b/src/main/java/until/the/eternity/auctionsearchoption/interfaces/rest/dto/response/SearchOptionMetadataResponse.java @@ -0,0 +1,11 @@ +package until.the.eternity.auctionsearchoption.interfaces.rest.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.Map; + +@Schema(description = "검색 옵션 메타데이터 응답") +public record SearchOptionMetadataResponse( + @Schema(description = "검색 옵션 ID", example = "1") Long id, + @Schema(description = "검색 옵션명", example = "밸런스") String searchOptionName, + @Schema(description = "검색 조건 상세") Map searchCondition, + @Schema(description = "정렬 순서", example = "1") Integer displayOrder) {} From b2eab16d010e5203f4b796d89d61f40b5cbe8aec Mon Sep 17 00:00:00 2001 From: Sanghyun Yi Date: Tue, 21 Oct 2025 00:23:55 +0900 Subject: [PATCH 6/8] =?UTF-8?q?feat:=20auction=20history=20search=EC=97=90?= =?UTF-8?q?=EC=84=9C=20price=20request=20dto=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../rest/dto/request/AuctionHistorySearchRequest.java | 4 ++-- .../application/service/AuctionHistoryServiceTest.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) 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 fce2675..89d08cc 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 @@ -7,6 +7,6 @@ public record AuctionHistorySearchRequest( @Schema(description = "아이템 이름 (like 검색)", example = "페러시우스 타이탄 블레이드") String itemName, @Schema(description = "대분류 카테고리", example = "근거리 장비") String itemTopCategory, @Schema(description = "소분류 카테고리", example = "검") String itemSubCategory, + // TODO: 거래 가격, 거래 일자를 범위 검색으로 변경, 옵션은 별도의 RequestDTO 구현git @Schema(description = "거래 가격", example = "10000000") String auction_price_per_unit, - @Schema(description = "거래 일자", example = "검") String date_auction_buy, - @Schema(description = "거래 금액") PriceSearchRequest priceSearchRequest) {} + @Schema(description = "거래 일자", example = "검") String date_auction_buy) {} 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 ed2fea8..8c438c2 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); + new AuctionHistorySearchRequest(null, null, null, null, null); PageRequestDto pageRequestDto = mock(PageRequestDto.class); Pageable pageable = PageRequest.of(0, 10); when(pageRequestDto.toPageable()).thenReturn(pageable); From d57a0097f8da41a86ed371476b8f37dbd69d5f21 Mon Sep 17 00:00:00 2001 From: Sanghyun Yi Date: Wed, 22 Oct 2025 22:09:45 +0900 Subject: [PATCH 7/8] =?UTF-8?q?feat:=20auction=20search=20meta=20service?= =?UTF-8?q?=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gradle.properties | 2 + .../AuctionSearchOptionServiceTest.java | 144 ++++++++++++++++++ 2 files changed, 146 insertions(+) create mode 100644 src/test/java/until/the/eternity/auctionsearchoption/application/service/AuctionSearchOptionServiceTest.java diff --git a/gradle.properties b/gradle.properties index f2aaf33..b06e0a8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,6 +2,8 @@ applicationVersion=0.0.1 ### Project Config ### projectGroup=until.the.eternity +### Gradle Configuration ### +org.gradle.java.installations.auto-download=true ### Project Dependency Versions ### javaVersion=21 ### Spring Dependency Versions ### 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 new file mode 100644 index 0000000..d92cf74 --- /dev/null +++ b/src/test/java/until/the/eternity/auctionsearchoption/application/service/AuctionSearchOptionServiceTest.java @@ -0,0 +1,144 @@ +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; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import until.the.eternity.auctionsearchoption.domain.entity.AuctionSearchOptionMetadata; +import until.the.eternity.auctionsearchoption.domain.repository.AuctionSearchOptionRepositoryPort; +import until.the.eternity.auctionsearchoption.interfaces.rest.dto.response.FieldMetadata; +import until.the.eternity.auctionsearchoption.interfaces.rest.dto.response.SearchOptionMetadataResponse; + +@ExtendWith(MockitoExtension.class) +class AuctionSearchOptionServiceTest { + + @Mock private AuctionSearchOptionRepositoryPort repositoryPort; + @Mock private ObjectMapper objectMapper; + + @InjectMocks private AuctionSearchOptionService service; + + @Test + @DisplayName("모든 활성화된 검색 옵션을 조회하면 응답 리스트를 반환한다") + void getAllActiveSearchOptions_should_return_response_list() throws Exception { + // given + AuctionSearchOptionMetadata entity1 = + createEntity(1L, "밸런스", "{\"balance\":{\"type\":\"tinyint\"}}", 1); + AuctionSearchOptionMetadata entity2 = + createEntity(2L, "공격력", "{\"attack\":{\"type\":\"int\"}}", 2); + Map fieldMetadataMap = + Map.of("balance", new FieldMetadata("tinyint", false, null)); + + when(repositoryPort.findAllActive()).thenReturn(List.of(entity1, entity2)); + when(objectMapper.readValue( + anyString(), any(com.fasterxml.jackson.core.type.TypeReference.class))) + .thenReturn(fieldMetadataMap); + + // when + List result = service.getAllActiveSearchOptions(); + + // then + assertThat(result).hasSize(2); + assertThat(result.get(0).searchOptionName()).isEqualTo("밸런스"); + assertThat(result.get(1).searchOptionName()).isEqualTo("공격력"); + assertThat(result.get(0).displayOrder()).isEqualTo(1); + assertThat(result.get(1).displayOrder()).isEqualTo(2); + + verify(repositoryPort).findAllActive(); + verify(objectMapper, times(2)) + .readValue(anyString(), any(com.fasterxml.jackson.core.type.TypeReference.class)); + } + + @Test + @DisplayName("활성화된 검색 옵션이 없으면 빈 리스트를 반환한다") + void getAllActiveSearchOptions_should_return_empty_list_when_no_active_options() { + // given + when(repositoryPort.findAllActive()).thenReturn(List.of()); + + // when + List result = service.getAllActiveSearchOptions(); + + // then + assertThat(result).isEmpty(); + verify(repositoryPort).findAllActive(); + verifyNoInteractions(objectMapper); + } + + @Test + @DisplayName("검색 조건 JSON을 올바르게 파싱하여 응답에 포함한다") + void getAllActiveSearchOptions_should_parse_json_correctly() throws Exception { + // given + AuctionSearchOptionMetadata entity1 = + createEntity(1L, "밸런스", "{\"balance\":{\"type\":\"tinyint\"}}", 1); + FieldMetadata balanceField = new FieldMetadata("tinyint", false, null); + Map parsedMap = Map.of("balance", balanceField); + + when(repositoryPort.findAllActive()).thenReturn(List.of(entity1)); + when(objectMapper.readValue( + eq(entity1.getSearchConditionJson()), + any(com.fasterxml.jackson.core.type.TypeReference.class))) + .thenReturn(parsedMap); + + // when + List result = service.getAllActiveSearchOptions(); + + // then + assertThat(result).hasSize(1); + SearchOptionMetadataResponse response = result.get(0); + assertThat(response.searchCondition()).containsKey("balance"); + assertThat(response.searchCondition().get("balance")).isEqualTo(balanceField); + + verify(objectMapper) + .readValue( + eq(entity1.getSearchConditionJson()), + any(com.fasterxml.jackson.core.type.TypeReference.class)); + } + + @Test + @DisplayName("응답은 display_order 순서대로 정렬되어 있다") + void getAllActiveSearchOptions_should_maintain_display_order() throws Exception { + // given + AuctionSearchOptionMetadata entity1 = + createEntity(1L, "밸런스", "{\"balance\":{\"type\":\"tinyint\"}}", 1); + AuctionSearchOptionMetadata entity2 = + createEntity(2L, "공격력", "{\"attack\":{\"type\":\"int\"}}", 2); + AuctionSearchOptionMetadata entity3 = + createEntity(3L, "크리티컬", "{\"critical\":{\"type\":\"int\"}}", 3); + Map fieldMetadataMap = + Map.of("balance", new FieldMetadata("tinyint", false, null)); + + when(repositoryPort.findAllActive()).thenReturn(List.of(entity1, entity2, entity3)); + when(objectMapper.readValue( + anyString(), any(com.fasterxml.jackson.core.type.TypeReference.class))) + .thenReturn(fieldMetadataMap); + + // when + List result = service.getAllActiveSearchOptions(); + + // then + assertThat(result).hasSize(3); + assertThat(result.get(0).displayOrder()).isEqualTo(1); + assertThat(result.get(1).displayOrder()).isEqualTo(2); + assertThat(result.get(2).displayOrder()).isEqualTo(3); + assertThat(result).extracting("searchOptionName").containsExactly("밸런스", "공격력", "크리티컬"); + } + + private AuctionSearchOptionMetadata createEntity( + Long id, String searchOptionName, String searchConditionJson, Integer displayOrder) { + AuctionSearchOptionMetadata entity = mock(AuctionSearchOptionMetadata.class); + + when(entity.getId()).thenReturn(id); + when(entity.getSearchOptionName()).thenReturn(searchOptionName); + when(entity.getSearchConditionJson()).thenReturn(searchConditionJson); + when(entity.getDisplayOrder()).thenReturn(displayOrder); + + return entity; + } +} From aa1ec1d77a9f0f457b734b73d8733532782ca845 Mon Sep 17 00:00:00 2001 From: Lee Sanghyun <59863112+dev-ant@users.noreply.github.com> Date: Wed, 22 Oct 2025 22:22:40 +0900 Subject: [PATCH 8/8] =?UTF-8?q?fix;=20auction=20history=20search=20request?= =?UTF-8?q?=20=EC=A3=BC=EC=84=9D=20=EC=98=A4=ED=83=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../rest/dto/request/AuctionHistorySearchRequest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 89d08cc..4d96f84 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 @@ -7,6 +7,6 @@ public record AuctionHistorySearchRequest( @Schema(description = "아이템 이름 (like 검색)", example = "페러시우스 타이탄 블레이드") String itemName, @Schema(description = "대분류 카테고리", example = "근거리 장비") String itemTopCategory, @Schema(description = "소분류 카테고리", example = "검") String itemSubCategory, - // TODO: 거래 가격, 거래 일자를 범위 검색으로 변경, 옵션은 별도의 RequestDTO 구현git + // TODO: 거래 가격, 거래 일자를 범위 검색으로 변경, 옵션은 별도의 RequestDTO 구현 @Schema(description = "거래 가격", example = "10000000") String auction_price_per_unit, @Schema(description = "거래 일자", example = "검") String date_auction_buy) {}