Skip to content
2 changes: 2 additions & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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 ###
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,14 @@ public Page<AuctionHistory> 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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
// TODO: 거래 가격, 거래 일자를 범위 검색으로 변경, 옵션은 별도의 RequestDTO 구현
@Schema(description = "거래 가격", example = "10000000") String auction_price_per_unit,
@Schema(description = "거래 일자", example = "검") String date_auction_buy) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package until.the.eternity.auctionhistory.interfaces.rest.dto.request;

import io.swagger.v3.oas.annotations.media.Schema;

public record PriceSearchRequest(
@Schema(description = "가격 최소값", example = "0", defaultValue = "0") long PriceTo,
@Schema(description = "가격 최대값", example = "9999999999", defaultValue = "9999999999")
long PriceFrom) {}
Original file line number Diff line number Diff line change
@@ -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<SearchOptionMetadataResponse> getAllActiveSearchOptions() {
List<AuctionSearchOptionMetadata> entities = repositoryPort.findAllActive();

return entities.stream().map(this::toResponse).toList();
}

private SearchOptionMetadataResponse toResponse(AuctionSearchOptionMetadata entity) {
Map<String, FieldMetadata> searchCondition =
parseJsonToFieldMetadata(entity.getSearchConditionJson());

return new SearchOptionMetadataResponse(
entity.getId(),
entity.getSearchOptionName(),
searchCondition,
entity.getDisplayOrder());
}

private Map<String, FieldMetadata> parseJsonToFieldMetadata(String json) {
try {
TypeReference<Map<String, FieldMetadata>> 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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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<AuctionSearchOptionMetadata> findAllActive();

/**
* 모든 검색 옵션 조회 (정렬 순서대로)
*
* @return 검색 옵션 메타데이터 리스트
*/
List<AuctionSearchOptionMetadata> findAll();
}
Original file line number Diff line number Diff line change
@@ -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<AuctionSearchOptionMetadata, Long> {

List<AuctionSearchOptionMetadata> findByIsActiveTrueOrderByDisplayOrderAsc();

List<AuctionSearchOptionMetadata> findAllByOrderByDisplayOrderAsc();
}
Original file line number Diff line number Diff line change
@@ -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<AuctionSearchOptionMetadata> findAllActive() {
return jpaRepository.findByIsActiveTrueOrderByDisplayOrderAsc();
}

@Override
public List<AuctionSearchOptionMetadata> findAll() {
return jpaRepository.findAllByOrderByDisplayOrderAsc();
}
}
Original file line number Diff line number Diff line change
@@ -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<ApiResponse<List<SearchOptionMetadataResponse>>> getSearchOptions() {
List<SearchOptionMetadataResponse> searchOptions = service.getAllActiveSearchOptions();

return ResponseEntity.ok(
ApiResponse.success("SEARCH_OPTION_SUCCESS", "검색 옵션 조회 성공", searchOptions));
}
}
Original file line number Diff line number Diff line change
@@ -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<String> allowedValues) {}
Original file line number Diff line number Diff line change
@@ -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<String, FieldMetadata> searchCondition,
@Schema(description = "정렬 순서", example = "1") Integer displayOrder) {}
Loading