diff --git a/api/build.gradle b/api/build.gradle index 4a9d58bc..a7d37ba1 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -1,5 +1,6 @@ dependencies { implementation project(path: ':core') + testImplementation(testFixtures(project(':core'))) implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' @@ -12,6 +13,6 @@ dependencies { } tasks.named('jar') { - enabled = false + enabled = true } } diff --git a/api/src/main/java/dev/handsup/HandsUpApplication.java b/api/src/main/java/dev/handsup/HandsUpApplication.java index 4317731c..c1be1152 100644 --- a/api/src/main/java/dev/handsup/HandsUpApplication.java +++ b/api/src/main/java/dev/handsup/HandsUpApplication.java @@ -2,13 +2,15 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +@EnableJpaAuditing @SpringBootApplication public class HandsUpApplication { public static void main(String[] args) { // 타 모듈 yml 가져오기 - System.setProperty("spring.config.name", "application,application-core"); + System.setProperty("spring.config.name", "application, application-core"); SpringApplication.run(HandsUpApplication.class, args); } } diff --git a/api/src/main/java/dev/handsup/auction/controller/AuctionController.java b/api/src/main/java/dev/handsup/auction/controller/AuctionController.java new file mode 100644 index 00000000..7da3849c --- /dev/null +++ b/api/src/main/java/dev/handsup/auction/controller/AuctionController.java @@ -0,0 +1,33 @@ +package dev.handsup.auction.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import dev.handsup.auction.dto.ApiAuctionMapper; +import dev.handsup.auction.dto.AuctionResponse; +import dev.handsup.auction.dto.RegisterAuctionApiRequest; +import dev.handsup.auction.dto.RegisterAuctionRequest; +import dev.handsup.auction.service.AuctionService; +import io.swagger.v3.oas.annotations.Operation; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/auctions") +public class AuctionController { + + private final AuctionService auctionService; + + @Operation(summary = "경매 등록 API", description = "경매를 등록한다") + @PostMapping + public ResponseEntity registerAuction(@Valid @RequestBody RegisterAuctionApiRequest request) { + RegisterAuctionRequest registerAuctionRequest = ApiAuctionMapper.toRegisterAuctionRequest(request); + AuctionResponse response = auctionService.registerAuction(registerAuctionRequest); + return ResponseEntity.ok(response); + } + +} diff --git a/api/src/main/java/dev/handsup/auction/dto/ApiAuctionMapper.java b/api/src/main/java/dev/handsup/auction/dto/ApiAuctionMapper.java new file mode 100644 index 00000000..94e88560 --- /dev/null +++ b/api/src/main/java/dev/handsup/auction/dto/ApiAuctionMapper.java @@ -0,0 +1,25 @@ +package dev.handsup.auction.dto; + +import static lombok.AccessLevel.*; + +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = PRIVATE) +public class ApiAuctionMapper { + + public static RegisterAuctionRequest toRegisterAuctionRequest(RegisterAuctionApiRequest request) { + return new RegisterAuctionRequest( + request.title(), + request.productCategory(), + request.initPrice(), + request.endDate(), + request.productStatus(), + request.purchaseTime(), + request.description(), + request.tradeMethod(), + request.si(), + request.gu(), + request.dong() + ); + } +} diff --git a/api/src/main/java/dev/handsup/auction/dto/RegisterAuctionApiRequest.java b/api/src/main/java/dev/handsup/auction/dto/RegisterAuctionApiRequest.java new file mode 100644 index 00000000..bc014bc4 --- /dev/null +++ b/api/src/main/java/dev/handsup/auction/dto/RegisterAuctionApiRequest.java @@ -0,0 +1,45 @@ +package dev.handsup.auction.dto; + +import java.time.LocalDate; + +import com.fasterxml.jackson.annotation.JsonFormat; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Builder; + +@Builder +public record RegisterAuctionApiRequest( + + @NotBlank(message = "title 값이 공백입니다.") + String title, + @NotBlank(message = "productCategory 값이 공백입니다.") + String productCategory, + @NotNull(message = "initPrice 값이 공백입니다.") + @Min(value = 1000, message = "최소 금액은 1000원입니다.") + @Max(value = 100000000, message = "최대 금액은 1억입니다.") + int initPrice, + + @NotNull(message = "endDate 값이 공백입니다.") + @JsonFormat(pattern = "yyyy-MM-dd") //localDate 형식으로 받음 + LocalDate endDate, + + @NotBlank(message = "productStatus가 공백입니다.") + String productStatus, + + @NotBlank(message = "purchaseTime이 공백입니다.") + String purchaseTime, + + @NotBlank(message = "description이 공백입니다.") + String description, + + @NotBlank(message = "tradeMethod가 공백입니다.") + String tradeMethod, + + String si, + String gu, + String dong +) { +} diff --git a/api/src/main/java/dev/handsup/common/exception/GlobalExceptionHandler.java b/api/src/main/java/dev/handsup/common/exception/GlobalExceptionHandler.java index 72edd20c..40c92d03 100644 --- a/api/src/main/java/dev/handsup/common/exception/GlobalExceptionHandler.java +++ b/api/src/main/java/dev/handsup/common/exception/GlobalExceptionHandler.java @@ -9,7 +9,6 @@ import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; -import dev.handsup.exception.ValidationException; import lombok.extern.slf4j.Slf4j; @Slf4j @@ -41,4 +40,11 @@ public ErrorResponseTemplate handleValidationException(ValidationException e) { log.error("ValidationException : ", e); return new ErrorResponseTemplate(e.getMessage(), e.getCode()); } + + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler(NotFoundException.class) + public ErrorResponseTemplate handleNotFoundException(NotFoundException e) { + log.error("NotFoundException : ", e); + return new ErrorResponseTemplate(e.getMessage(), e.getCode()); + } } diff --git a/api/src/test/java/dev/handsup/auction/controller/AuctionControllerTest.java b/api/src/test/java/dev/handsup/auction/controller/AuctionControllerTest.java new file mode 100644 index 00000000..0c1e0e06 --- /dev/null +++ b/api/src/test/java/dev/handsup/auction/controller/AuctionControllerTest.java @@ -0,0 +1,68 @@ +package dev.handsup.auction.controller; + +import static org.springframework.http.MediaType.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import java.time.LocalDate; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import dev.handsup.auction.domain.auction_field.PurchaseTime; +import dev.handsup.auction.domain.auction_field.TradeMethod; +import dev.handsup.auction.domain.product.ProductStatus; +import dev.handsup.auction.domain.product.product_category.ProductCategory; +import dev.handsup.auction.dto.RegisterAuctionApiRequest; +import dev.handsup.auction.repository.AuctionRepository; +import dev.handsup.auction.repository.ProductCategoryRepository; +import dev.handsup.common.support.ApiTestSupport; +import dev.handsup.fixture.ProductFixture; + +class AuctionControllerTest extends ApiTestSupport { + + private final String DIGITAL_DEVICE = "디지털 기기"; + @Autowired + private AuctionRepository auctionRepository; + @Autowired + private ProductCategoryRepository productCategoryRepository; + + @BeforeEach + void setUp() { + ProductCategory productCategory = ProductFixture.productCategory(DIGITAL_DEVICE); + productCategoryRepository.save(productCategory); + } + + @DisplayName("경매를 등록할 수 있다.") + @Test + void registerAuction() throws Exception { + RegisterAuctionApiRequest request = RegisterAuctionApiRequest.builder() + .title("아이패드 팔아요") + .description("아이패드 팔아요. 오래 되어서 싸게 팔아요") + .productStatus(ProductStatus.DIRTY.getLabel()) + .tradeMethod(TradeMethod.DIRECT.getLabel()) + .endDate(LocalDate.parse("2023-02-19")) + .initPrice(300000) + .purchaseTime(PurchaseTime.ABOVE_ONE_YEAR.getLabel()) + .productCategory(DIGITAL_DEVICE) + .build(); + + mockMvc.perform(post("/api/auctions") + .contentType(APPLICATION_JSON) + .content(toJson(request))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.title").value(request.title())) + .andExpect(jsonPath("$.description").value(request.description())) + .andExpect(jsonPath("$.productStatus").value(request.productStatus())) + .andExpect(jsonPath("$.tradeMethod").value(request.tradeMethod())) + .andExpect(jsonPath("$.endDate").value(request.endDate().toString())) + .andExpect(jsonPath("$.initPrice").value(request.initPrice())) + .andExpect(jsonPath("$.purchaseTime").value(request.purchaseTime())) + .andExpect(jsonPath("$.productCategory").value(request.productCategory())) + .andExpect(jsonPath("$.si").isEmpty()) + .andExpect(jsonPath("$.gu").isEmpty()) + .andExpect(jsonPath("$.dong").isEmpty()); + } +} \ No newline at end of file diff --git a/api/src/test/java/dev/handsup/common/support/ApiTestSupport.java b/api/src/test/java/dev/handsup/common/support/ApiTestSupport.java new file mode 100644 index 00000000..343eac39 --- /dev/null +++ b/api/src/test/java/dev/handsup/common/support/ApiTestSupport.java @@ -0,0 +1,26 @@ +package dev.handsup.common.support; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.web.servlet.MockMvc; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +import dev.handsup.support.TestContainerSupport; + +@SpringBootTest +@AutoConfigureMockMvc +public abstract class ApiTestSupport extends TestContainerSupport { + + @Autowired + protected MockMvc mockMvc; + + @Autowired + protected ObjectMapper objectMapper; + + protected String toJson(Object object) throws JsonProcessingException { + return objectMapper.writeValueAsString(object); + } +} diff --git a/api/src/test/resources/application.yml b/api/src/test/resources/application.yml new file mode 100644 index 00000000..3ed86d6c --- /dev/null +++ b/api/src/test/resources/application.yml @@ -0,0 +1,11 @@ +spring: + jpa: + hibernate: + ddl-auto: create + properties: + hibernate: + format_sql: true + default_batch_fetch_size: 10 +logging.level: + org.hibernate.SQL: debug + org.hibernate.orm.jdbc.bind: trace \ No newline at end of file diff --git a/build.gradle b/build.gradle index efef14d4..eae6851e 100644 --- a/build.gradle +++ b/build.gradle @@ -20,6 +20,7 @@ subprojects { apply { plugin('java') } apply { plugin('org.springframework.boot') } apply { plugin('io.spring.dependency-management') } + apply { plugin('java-test-fixtures')} dependencies { compileOnly 'org.projectlombok:lombok' diff --git a/core/build.gradle b/core/build.gradle index 1c15dbe3..0cafbdfe 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -1,11 +1,11 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' - implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' - implementation 'org.springframework.boot:spring-boot-starter-security' +// implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' +// implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-validation' runtimeOnly 'com.mysql:mysql-connector-j' - testImplementation 'org.springframework.security:spring-security-test' +// testImplementation 'org.springframework.security:spring-security-test' testImplementation 'org.junit.jupiter:junit-jupiter:5.9.2' //redis @@ -17,8 +17,13 @@ dependencies { runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5' - // test container - testImplementation "org.testcontainers:junit-jupiter:1.19.5" - testImplementation "org.testcontainers:testcontainers:1.19.5" - testImplementation "org.testcontainers:mysql:1.19.2" //mysql test container + // testFixtures 의존성 + testFixturesImplementation 'org.springframework.boot:spring-boot-starter-test' + testFixturesImplementation "org.testcontainers:junit-jupiter:1.19.5" + testFixturesImplementation "org.testcontainers:testcontainers:1.19.5" + testFixturesImplementation "org.testcontainers:mysql:1.19.2" //mysql test container + + testFixturesCompileOnly 'org.projectlombok:lombok' + testFixturesAnnotationProcessor 'org.projectlombok:lombok' + testFixturesCompileOnly 'org.springframework.boot:spring-boot-starter-security' } diff --git a/core/src/main/java/dev/handsup/auction/domain/Auction.java b/core/src/main/java/dev/handsup/auction/domain/Auction.java index faba5a4b..c2b91cd2 100644 --- a/core/src/main/java/dev/handsup/auction/domain/Auction.java +++ b/core/src/main/java/dev/handsup/auction/domain/Auction.java @@ -1,6 +1,8 @@ package dev.handsup.auction.domain; import static dev.handsup.auction.domain.auction_field.AuctionStatus.*; +import static dev.handsup.common.exception.CommonValidationError.*; +import static jakarta.persistence.CascadeType.*; import static jakarta.persistence.ConstraintMode.*; import static jakarta.persistence.EnumType.*; import static jakarta.persistence.FetchType.*; @@ -9,10 +11,15 @@ import java.time.LocalDate; +import org.springframework.util.Assert; + import dev.handsup.auction.domain.auction_field.AuctionStatus; +import dev.handsup.auction.domain.auction_field.PurchaseTime; import dev.handsup.auction.domain.auction_field.TradeMethod; import dev.handsup.auction.domain.auction_field.TradingLocation; import dev.handsup.auction.domain.product.Product; +import dev.handsup.auction.domain.product.ProductStatus; +import dev.handsup.auction.domain.product.product_category.ProductCategory; import dev.handsup.common.entity.TimeBaseEntity; import dev.handsup.user.domain.User; import jakarta.persistence.Column; @@ -34,6 +41,8 @@ @NoArgsConstructor(access = PROTECTED) public class Auction extends TimeBaseEntity { + private static final String AUCTION = "auction"; + @Id @GeneratedValue(strategy = IDENTITY) @Column(name = "auction_id") @@ -41,7 +50,7 @@ public class Auction extends TimeBaseEntity { @ManyToOne(fetch = LAZY) @JoinColumn(name = "seller_id", - nullable = false, + // nullable = false, foreignKey = @ForeignKey(NO_CONSTRAINT)) private User seller; @@ -52,7 +61,7 @@ public class Auction extends TimeBaseEntity { @Column(name = "title", nullable = false) private String title; - @OneToOne(fetch = LAZY) + @OneToOne(fetch = LAZY, cascade = ALL) @JoinColumn(name = "product_id", foreignKey = @ForeignKey(NO_CONSTRAINT)) private Product product; @@ -81,9 +90,10 @@ public class Auction extends TimeBaseEntity { private int bookmarkCount; @Builder - public Auction(User seller, String title, Product product, int initPrice, LocalDate endDate, + public Auction(String title, Product product, int initPrice, LocalDate endDate, TradingLocation tradingLocation, TradeMethod tradeMethod) { - this.seller = seller; + Assert.hasText(title, getNotEmptyMessage(AUCTION, "title")); + Assert.notNull(title, getNotEmptyMessage(AUCTION, "initPrice")); this.title = title; this.product = product; this.initPrice = initPrice; @@ -94,4 +104,36 @@ public Auction(User seller, String title, Product product, int initPrice, LocalD bookmarkCount = 0; status = PROGRESS; } + + public static Auction of( + String title, + ProductCategory productCategory, + int initPrice, + LocalDate endDate, + ProductStatus status, + PurchaseTime purchaseTime, + String description, + TradeMethod tradeMethod, + String si, + String gu, + String dong + ) { + return Auction.builder() + .title(title) + .product(Product.builder() + .productCategory(productCategory) + .status(status) + .description(description) + .purchaseTime(purchaseTime) + .build()) + .initPrice(initPrice) + .endDate(endDate) + .tradingLocation(TradingLocation.builder() + .si(si) + .gu(gu) + .dong(dong) + .build()) + .tradeMethod(tradeMethod) + .build(); + } } diff --git a/core/src/main/java/dev/handsup/auction/domain/auction_field/AuctionStatus.java b/core/src/main/java/dev/handsup/auction/domain/auction_field/AuctionStatus.java index ebd6c7a4..2a0a7297 100644 --- a/core/src/main/java/dev/handsup/auction/domain/auction_field/AuctionStatus.java +++ b/core/src/main/java/dev/handsup/auction/domain/auction_field/AuctionStatus.java @@ -12,5 +12,5 @@ public enum AuctionStatus { COMPLETED("완료"), CANCELED("취소"); - private final String description; + private final String label; } diff --git a/core/src/main/java/dev/handsup/auction/domain/auction_field/ProductStatus.java b/core/src/main/java/dev/handsup/auction/domain/auction_field/ProductStatus.java deleted file mode 100644 index c484fb5b..00000000 --- a/core/src/main/java/dev/handsup/auction/domain/auction_field/ProductStatus.java +++ /dev/null @@ -1,15 +0,0 @@ -package dev.handsup.auction.domain.auction_field; - -import lombok.Getter; -import lombok.RequiredArgsConstructor; - -@Getter -@RequiredArgsConstructor -public enum ProductStatus { - - NEW("미개봉"), - CLEAN("깨끗함"), - DIRTY("더러움"); - - private final String description; -} diff --git a/core/src/main/java/dev/handsup/auction/domain/auction_field/PurchaseTime.java b/core/src/main/java/dev/handsup/auction/domain/auction_field/PurchaseTime.java index 68ecf32c..15a8beaf 100644 --- a/core/src/main/java/dev/handsup/auction/domain/auction_field/PurchaseTime.java +++ b/core/src/main/java/dev/handsup/auction/domain/auction_field/PurchaseTime.java @@ -1,5 +1,10 @@ package dev.handsup.auction.domain.auction_field; +import static dev.handsup.auction.exception.AuctionErrorCode.*; + +import java.util.Arrays; + +import dev.handsup.common.exception.ValidationException; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -14,5 +19,16 @@ public enum PurchaseTime { ABOVE_ONE_YEAR("1년 이상"), UNKNOWN("모름"); - private final String description; + private final String label; + + public static PurchaseTime of(String input) { + return Arrays.stream(values()) + .filter(time -> time.isEqual(input)) + .findAny() + .orElseThrow(() -> new ValidationException(NOT_FOUND_PURCHASE_TIME)); + } + + private boolean isEqual(String input) { + return input.equalsIgnoreCase(this.label); + } } diff --git a/core/src/main/java/dev/handsup/auction/domain/auction_field/TradeMethod.java b/core/src/main/java/dev/handsup/auction/domain/auction_field/TradeMethod.java index 51d39d30..e55f7130 100644 --- a/core/src/main/java/dev/handsup/auction/domain/auction_field/TradeMethod.java +++ b/core/src/main/java/dev/handsup/auction/domain/auction_field/TradeMethod.java @@ -1,5 +1,10 @@ package dev.handsup.auction.domain.auction_field; +import static dev.handsup.auction.exception.AuctionErrorCode.*; + +import java.util.Arrays; + +import dev.handsup.common.exception.ValidationException; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -10,5 +15,16 @@ public enum TradeMethod { DIRECT("직거래"), DELIVER("택배"); - private final String description; + private final String label; + + public static TradeMethod of(String input) { + return Arrays.stream(values()) + .filter(method -> method.isEqual(input)) + .findAny() + .orElseThrow(() -> new ValidationException(NOT_FOUND_TADE_METHOD)); + } + + private boolean isEqual(String input) { + return input.equalsIgnoreCase(this.label); + } } diff --git a/core/src/main/java/dev/handsup/auction/domain/auction_field/TradingLocation.java b/core/src/main/java/dev/handsup/auction/domain/auction_field/TradingLocation.java index 9fa96956..01ea21d3 100644 --- a/core/src/main/java/dev/handsup/auction/domain/auction_field/TradingLocation.java +++ b/core/src/main/java/dev/handsup/auction/domain/auction_field/TradingLocation.java @@ -12,14 +12,15 @@ @Getter @NoArgsConstructor(access = PROTECTED) public class TradingLocation { + private static final String TRADING_LOCATION = "TradingLocation"; - @Column(name = "si", nullable = false) + @Column(name = "si") private String si; - @Column(name = "gu", nullable = false) + @Column(name = "gu") private String gu; - @Column(name = "dong", nullable = false) + @Column(name = "dong") private String dong; @Builder diff --git a/core/src/main/java/dev/handsup/auction/domain/product/Product.java b/core/src/main/java/dev/handsup/auction/domain/product/Product.java index 826e57e4..3c6facf0 100644 --- a/core/src/main/java/dev/handsup/auction/domain/product/Product.java +++ b/core/src/main/java/dev/handsup/auction/domain/product/Product.java @@ -1,12 +1,14 @@ package dev.handsup.auction.domain.product; +import static dev.handsup.common.exception.CommonValidationError.*; import static jakarta.persistence.ConstraintMode.*; import static jakarta.persistence.EnumType.*; import static jakarta.persistence.FetchType.*; import static jakarta.persistence.GenerationType.*; import static lombok.AccessLevel.*; -import dev.handsup.auction.domain.auction_field.ProductStatus; +import org.springframework.util.Assert; + import dev.handsup.auction.domain.auction_field.PurchaseTime; import dev.handsup.auction.domain.product.product_category.ProductCategory; import dev.handsup.common.entity.TimeBaseEntity; @@ -26,6 +28,7 @@ @Getter @NoArgsConstructor(access = PROTECTED) public class Product extends TimeBaseEntity { + private static final String PRODUCT = "product"; @Id @GeneratedValue(strategy = IDENTITY) @@ -47,13 +50,15 @@ public class Product extends TimeBaseEntity { @JoinColumn(name = "product_category_id", nullable = false, foreignKey = @ForeignKey(NO_CONSTRAINT)) - private ProductCategory category; + private ProductCategory productCategory; @Builder - public Product(ProductStatus status, String description, PurchaseTime purchaseTime, ProductCategory category) { + public Product(ProductStatus status, String description, PurchaseTime purchaseTime, + ProductCategory productCategory) { + Assert.hasText(description, getNotEmptyMessage(PRODUCT, "description")); this.status = status; this.description = description; this.purchaseTime = purchaseTime; - this.category = category; + this.productCategory = productCategory; } } \ No newline at end of file diff --git a/core/src/main/java/dev/handsup/auction/domain/product/ProductStatus.java b/core/src/main/java/dev/handsup/auction/domain/product/ProductStatus.java new file mode 100644 index 00000000..a197520c --- /dev/null +++ b/core/src/main/java/dev/handsup/auction/domain/product/ProductStatus.java @@ -0,0 +1,31 @@ +package dev.handsup.auction.domain.product; + +import static dev.handsup.auction.exception.AuctionErrorCode.*; + +import java.util.Arrays; + +import dev.handsup.common.exception.ValidationException; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum ProductStatus { + + NEW("미개봉"), + CLEAN("깨끗함"), + DIRTY("더러움"); + + private final String label; + + public static ProductStatus of(String input) { + return Arrays.stream(values()) + .filter(status -> status.isEqual(input)) + .findAny() + .orElseThrow(() -> new ValidationException(NOT_FOUND_PRODUCT_STATUS)); + } + + private boolean isEqual(String input) { + return input.equalsIgnoreCase(this.label); + } +} diff --git a/core/src/main/java/dev/handsup/auction/domain/product/product_category/ProductCategory.java b/core/src/main/java/dev/handsup/auction/domain/product/product_category/ProductCategory.java index 7170f94a..534f767a 100644 --- a/core/src/main/java/dev/handsup/auction/domain/product/product_category/ProductCategory.java +++ b/core/src/main/java/dev/handsup/auction/domain/product/product_category/ProductCategory.java @@ -7,6 +7,7 @@ import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -20,5 +21,16 @@ public class ProductCategory { private Long id; @Column(name = "product_category_value", nullable = false) - private ProductCategoryValue value; + private String categoryValue; + + @Builder(access = PRIVATE) + public ProductCategory(String categoryValue) { + this.categoryValue = categoryValue; + } + + public static ProductCategory of(String categoryValue) { + return ProductCategory.builder() + .categoryValue(categoryValue) + .build(); + } } diff --git a/core/src/main/java/dev/handsup/auction/domain/product/product_category/ProductCategoryValue.java b/core/src/main/java/dev/handsup/auction/domain/product/product_category/ProductCategoryValue.java deleted file mode 100644 index 5a9130f0..00000000 --- a/core/src/main/java/dev/handsup/auction/domain/product/product_category/ProductCategoryValue.java +++ /dev/null @@ -1,14 +0,0 @@ -package dev.handsup.auction.domain.product.product_category; - -import lombok.Getter; -import lombok.RequiredArgsConstructor; - -@Getter -@RequiredArgsConstructor -public enum ProductCategoryValue { - DIGITAL("디지털 기기"), - FURNITURE("가구"), - FASHION("패션"); - - private final String description; -} diff --git a/core/src/main/java/dev/handsup/auction/dto/AuctionResponse.java b/core/src/main/java/dev/handsup/auction/dto/AuctionResponse.java new file mode 100644 index 00000000..bcdbf955 --- /dev/null +++ b/core/src/main/java/dev/handsup/auction/dto/AuctionResponse.java @@ -0,0 +1,31 @@ +package dev.handsup.auction.dto; + +import java.time.LocalDate; + +import lombok.Builder; + +@Builder +public record AuctionResponse( + + Long auctionId, + String title, + + String productCategory, + + int initPrice, + + LocalDate endDate, + + String productStatus, + + String purchaseTime, + + String description, + + String tradeMethod, + + String si, + String gu, + String dong +) { +} diff --git a/core/src/main/java/dev/handsup/auction/dto/RegisterAuctionRequest.java b/core/src/main/java/dev/handsup/auction/dto/RegisterAuctionRequest.java new file mode 100644 index 00000000..69ec4014 --- /dev/null +++ b/core/src/main/java/dev/handsup/auction/dto/RegisterAuctionRequest.java @@ -0,0 +1,23 @@ +package dev.handsup.auction.dto; + +import java.time.LocalDate; + +import lombok.Builder; + +@Builder +public record RegisterAuctionRequest( + + String title, + String productCategory, + int initPrice, + LocalDate endDate, + String productStatus, + String purchaseTime, + String description, + String tradeMethod, + + String si, + String gu, + String dong +) { +} diff --git a/core/src/main/java/dev/handsup/auction/dto/mapper/AuctionMapper.java b/core/src/main/java/dev/handsup/auction/dto/mapper/AuctionMapper.java new file mode 100644 index 00000000..22cf96a3 --- /dev/null +++ b/core/src/main/java/dev/handsup/auction/dto/mapper/AuctionMapper.java @@ -0,0 +1,55 @@ +package dev.handsup.auction.dto.mapper; + +import static lombok.AccessLevel.*; + +import dev.handsup.auction.domain.Auction; +import dev.handsup.auction.domain.auction_field.PurchaseTime; +import dev.handsup.auction.domain.auction_field.TradeMethod; +import dev.handsup.auction.domain.product.ProductStatus; +import dev.handsup.auction.domain.product.product_category.ProductCategory; +import dev.handsup.auction.dto.AuctionResponse; +import dev.handsup.auction.dto.RegisterAuctionRequest; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = PRIVATE) +public class AuctionMapper { + + public static Auction toAuction(RegisterAuctionRequest request, ProductCategory productCategory) { + + ProductStatus productStatus = ProductStatus.of(request.productStatus()); + PurchaseTime purchaseTime = PurchaseTime.of(request.purchaseTime()); + TradeMethod tradeMethod = TradeMethod.of(request.tradeMethod()); + + return Auction.of( + request.title(), + productCategory, + request.initPrice(), + request.endDate(), + productStatus, + purchaseTime, + request.description(), + tradeMethod, + request.si(), + request.gu(), + request.dong() + ); + } + + public static AuctionResponse toAuctionResponse(Auction auction) { + return AuctionResponse.builder() + .auctionId(auction.getId()) + .title(auction.getTitle()) + .productCategory(auction.getProduct().getProductCategory().getCategoryValue()) + .initPrice(auction.getInitPrice()) + .endDate(auction.getEndDate()) + .productStatus(auction.getProduct().getStatus().getLabel()) + .purchaseTime(auction.getProduct().getPurchaseTime().getLabel()) + .description(auction.getProduct().getDescription()) + .tradeMethod(auction.getTradeMethod().getLabel()) + .si(auction.getTradingLocation().getSi()) + .gu(auction.getTradingLocation().getGu()) + .dong(auction.getTradingLocation().getDong()) + .build(); + } + +} diff --git a/core/src/main/java/dev/handsup/auction/exception/AuctionErrorCode.java b/core/src/main/java/dev/handsup/auction/exception/AuctionErrorCode.java new file mode 100644 index 00000000..f8b1effa --- /dev/null +++ b/core/src/main/java/dev/handsup/auction/exception/AuctionErrorCode.java @@ -0,0 +1,18 @@ +package dev.handsup.auction.exception; + +import dev.handsup.common.exception.ErrorCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum AuctionErrorCode implements ErrorCode { + + NOT_FOUND_PRODUCT_STATUS("올바른 상품 상태를 입력해주세요", "P_001"), + NOT_FOUND_PURCHASE_TIME("올바른 구매 시기를 입력해주세요", "P_002"), + NOT_FOUND_TADE_METHOD("올바른 거래 방법을 입력해주세요", "P_003"), + NOT_FOUND_PRODUCT_CATEGORY("DB에 해당 상품 카테고리가 존재하지 않습니다.", "P_005"); + + private final String message; + private final String code; +} diff --git a/core/src/main/java/dev/handsup/auction/repository/AuctionRepository.java b/core/src/main/java/dev/handsup/auction/repository/AuctionRepository.java new file mode 100644 index 00000000..54311014 --- /dev/null +++ b/core/src/main/java/dev/handsup/auction/repository/AuctionRepository.java @@ -0,0 +1,8 @@ +package dev.handsup.auction.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import dev.handsup.auction.domain.Auction; + +public interface AuctionRepository extends JpaRepository { +} diff --git a/core/src/main/java/dev/handsup/auction/repository/ProductCategoryRepository.java b/core/src/main/java/dev/handsup/auction/repository/ProductCategoryRepository.java new file mode 100644 index 00000000..5cc1b816 --- /dev/null +++ b/core/src/main/java/dev/handsup/auction/repository/ProductCategoryRepository.java @@ -0,0 +1,12 @@ +package dev.handsup.auction.repository; + +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; + +import dev.handsup.auction.domain.product.product_category.ProductCategory; + +public interface ProductCategoryRepository extends JpaRepository { + + Optional findByCategoryValue(String categoryValue); +} diff --git a/core/src/main/java/dev/handsup/auction/service/AuctionService.java b/core/src/main/java/dev/handsup/auction/service/AuctionService.java new file mode 100644 index 00000000..5dad29d4 --- /dev/null +++ b/core/src/main/java/dev/handsup/auction/service/AuctionService.java @@ -0,0 +1,33 @@ +package dev.handsup.auction.service; + +import org.springframework.stereotype.Service; + +import dev.handsup.auction.domain.Auction; +import dev.handsup.auction.domain.product.product_category.ProductCategory; +import dev.handsup.auction.dto.AuctionResponse; +import dev.handsup.auction.dto.RegisterAuctionRequest; +import dev.handsup.auction.dto.mapper.AuctionMapper; +import dev.handsup.auction.exception.AuctionErrorCode; +import dev.handsup.auction.repository.AuctionRepository; +import dev.handsup.auction.repository.ProductCategoryRepository; +import dev.handsup.common.exception.NotFoundException; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class AuctionService { + + private final AuctionRepository auctionRepository; + private final ProductCategoryRepository productCategoryRepository; + + public AuctionResponse registerAuction(RegisterAuctionRequest request) { + ProductCategory productCategory = findProductCategoryEntity(request); + Auction auction = AuctionMapper.toAuction(request, productCategory); + return AuctionMapper.toAuctionResponse(auctionRepository.save(auction)); + } + + private ProductCategory findProductCategoryEntity(RegisterAuctionRequest request) { + return productCategoryRepository.findByCategoryValue(request.productCategory()). + orElseThrow(() -> new NotFoundException(AuctionErrorCode.NOT_FOUND_PRODUCT_CATEGORY)); + } +} diff --git a/core/src/main/java/dev/handsup/exception/CommonValidationError.java b/core/src/main/java/dev/handsup/exception/CommonValidationError.java deleted file mode 100644 index 664696d8..00000000 --- a/core/src/main/java/dev/handsup/exception/CommonValidationError.java +++ /dev/null @@ -1,20 +0,0 @@ -package dev.handsup.exception; - -import static lombok.AccessLevel.*; - -import lombok.NoArgsConstructor; - -@NoArgsConstructor(access = PRIVATE) -public final class CommonValidationError { - - private static final String NOT_NULL_POSTFIX = " 는 Null 이 될 수 없습니다"; - private static final String NOT_EMPTY_POSTFIX = " 는 Empty 가 될 수 없습니다"; - - public static String getNotNullMessage(String object, String variable) { - return object + "_" + variable + NOT_NULL_POSTFIX; - } - - public static String getNotEmptyPostfix(String object, String variable) { - return object + "_" + variable + NOT_EMPTY_POSTFIX; - } -} diff --git a/core/src/main/java/dev/handsup/exception/ErrorCode.java b/core/src/main/java/dev/handsup/exception/ErrorCode.java deleted file mode 100644 index dff9e208..00000000 --- a/core/src/main/java/dev/handsup/exception/ErrorCode.java +++ /dev/null @@ -1,8 +0,0 @@ -package dev.handsup.exception; - -public interface ErrorCode { - - String getMessage(); - - String getCode(); -} diff --git a/core/src/main/java/dev/handsup/exception/NotFoundException.java b/core/src/main/java/dev/handsup/exception/NotFoundException.java deleted file mode 100644 index 1736b90b..00000000 --- a/core/src/main/java/dev/handsup/exception/NotFoundException.java +++ /dev/null @@ -1,14 +0,0 @@ -package dev.handsup.exception; - -import lombok.Getter; - -@Getter -public class NotFoundException extends RuntimeException { - - private final String code; - - public NotFoundException(ErrorCode errorCode) { - super(errorCode.getMessage()); - this.code = errorCode.getCode(); - } -} diff --git a/core/src/main/java/dev/handsup/exception/ValidationException.java b/core/src/main/java/dev/handsup/exception/ValidationException.java deleted file mode 100644 index dc872575..00000000 --- a/core/src/main/java/dev/handsup/exception/ValidationException.java +++ /dev/null @@ -1,14 +0,0 @@ -package dev.handsup.exception; - -import lombok.Getter; - -@Getter -public class ValidationException extends RuntimeException { - - private final String code; - - public ValidationException(ErrorCode errorCode) { - super(errorCode.getMessage()); - this.code = errorCode.getCode(); - } -} diff --git a/core/src/main/java/dev/handsup/user/dto/response/UserJoinResponse.java b/core/src/main/java/dev/handsup/user/dto/response/UserJoinResponse.java index 1f614183..6900733b 100644 --- a/core/src/main/java/dev/handsup/user/dto/response/UserJoinResponse.java +++ b/core/src/main/java/dev/handsup/user/dto/response/UserJoinResponse.java @@ -3,4 +3,5 @@ public record UserJoinResponse( Long userId -) {} +) { +} diff --git a/core/src/test/java/dev/handsup/auction/service/AuctionServiceTest.java b/core/src/test/java/dev/handsup/auction/service/AuctionServiceTest.java new file mode 100644 index 00000000..36bf934c --- /dev/null +++ b/core/src/test/java/dev/handsup/auction/service/AuctionServiceTest.java @@ -0,0 +1,78 @@ +package dev.handsup.auction.service; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.BDDMockito.*; + +import java.time.LocalDate; +import java.util.Optional; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import dev.handsup.auction.domain.Auction; +import dev.handsup.auction.domain.auction_field.PurchaseTime; +import dev.handsup.auction.domain.auction_field.TradeMethod; +import dev.handsup.auction.domain.product.ProductStatus; +import dev.handsup.auction.domain.product.product_category.ProductCategory; +import dev.handsup.auction.dto.AuctionResponse; +import dev.handsup.auction.dto.RegisterAuctionRequest; +import dev.handsup.auction.repository.AuctionRepository; +import dev.handsup.auction.repository.ProductCategoryRepository; +import dev.handsup.fixture.AuctionFixture; +import dev.handsup.fixture.ProductFixture; + +@ExtendWith(MockitoExtension.class) +class AuctionServiceTest { + + private final String DIGITAL_DEVICE = "디지털 기기"; + @Mock + private AuctionRepository auctionRepository; + @Mock + private ProductCategoryRepository productCategoryRepository; + + @InjectMocks + private AuctionService auctionService; + + @Test + @DisplayName("경매글을 등록할 수 있다.") + void registerAuction() { + // given + ProductCategory productCategory = ProductFixture.productCategory(DIGITAL_DEVICE); + Auction auction = AuctionFixture.auction(); + RegisterAuctionRequest registerAuctionRequest = + RegisterAuctionRequest.builder() + .title("거의 새상품 버즈 팔아요") + .tradeMethod(TradeMethod.DELIVER.getLabel()) + .purchaseTime(PurchaseTime.UNDER_ONE_MONTH.getLabel()) + .productCategory(DIGITAL_DEVICE) + .productStatus(ProductStatus.NEW.getLabel()) + .endDate(LocalDate.parse("2022-10-18")) + .description("거의 새상품이에요") + .initPrice(10000) + .si("서울시") + .gu("성북구") + .dong("동선동") + .build(); + + given(productCategoryRepository.findByCategoryValue(DIGITAL_DEVICE)) + .willReturn(Optional.of(productCategory)); + given(auctionRepository.save(any(Auction.class))).willReturn(auction); + + // when + AuctionResponse auctionResponse = auctionService.registerAuction(registerAuctionRequest); + + // then + assertAll( + () -> assertThat(auctionResponse.title()).isEqualTo(registerAuctionRequest.title()), + () -> assertThat(auctionResponse.tradeMethod()).isEqualTo(registerAuctionRequest.tradeMethod()), + () -> assertThat(auctionResponse.endDate()).isEqualTo(registerAuctionRequest.endDate()), + () -> assertThat(auctionResponse.purchaseTime()).isEqualTo(registerAuctionRequest.purchaseTime()), + () -> assertThat(auctionResponse.productCategory()).isEqualTo(registerAuctionRequest.productCategory()) + ); + } +} diff --git a/core/src/testFixtures/java/dev/handsup/fixture/AuctionFixture.java b/core/src/testFixtures/java/dev/handsup/fixture/AuctionFixture.java new file mode 100644 index 00000000..882b7179 --- /dev/null +++ b/core/src/testFixtures/java/dev/handsup/fixture/AuctionFixture.java @@ -0,0 +1,32 @@ +package dev.handsup.fixture; + +import static lombok.AccessLevel.*; + +import java.time.LocalDate; + +import dev.handsup.auction.domain.Auction; +import dev.handsup.auction.domain.auction_field.PurchaseTime; +import dev.handsup.auction.domain.auction_field.TradeMethod; +import dev.handsup.auction.domain.product.ProductStatus; +import dev.handsup.auction.domain.product.product_category.ProductCategory; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = PRIVATE) +public class AuctionFixture { + + public static Auction auction() { + return Auction.of( + "거의 새상품 버즈 팔아요", + ProductCategory.of("디지털 기기"), + 10000, + LocalDate.parse("2022-10-18"), + ProductStatus.NEW, + PurchaseTime.UNDER_ONE_MONTH, + "거의 새상품이에요", + TradeMethod.DELIVER, + "서울시", + "성북구", + "동선동" + ); + } +} diff --git a/core/src/testFixtures/java/dev/handsup/fixture/ProductFixture.java b/core/src/testFixtures/java/dev/handsup/fixture/ProductFixture.java new file mode 100644 index 00000000..c7f3600b --- /dev/null +++ b/core/src/testFixtures/java/dev/handsup/fixture/ProductFixture.java @@ -0,0 +1,15 @@ +package dev.handsup.fixture; + +import static lombok.AccessLevel.*; + +import dev.handsup.auction.domain.product.product_category.ProductCategory; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = PRIVATE) +public class ProductFixture { + + public static ProductCategory productCategory(String productCategory) { + return ProductCategory.of(productCategory); + } +} + diff --git a/core/src/test/java/dev/handsup/support/TestContainerSupport.java b/core/src/testFixtures/java/dev/handsup/support/TestContainerSupport.java similarity index 93% rename from core/src/test/java/dev/handsup/support/TestContainerSupport.java rename to core/src/testFixtures/java/dev/handsup/support/TestContainerSupport.java index 1f06b3d2..79990c5f 100644 --- a/core/src/test/java/dev/handsup/support/TestContainerSupport.java +++ b/core/src/testFixtures/java/dev/handsup/support/TestContainerSupport.java @@ -1,5 +1,7 @@ package dev.handsup.support; +import static lombok.AccessLevel.*; + import org.springframework.test.context.DynamicPropertyRegistry; import org.springframework.test.context.DynamicPropertySource; import org.testcontainers.containers.GenericContainer; @@ -7,6 +9,9 @@ import org.testcontainers.containers.MySQLContainer; import org.testcontainers.utility.DockerImageName; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = PROTECTED) public abstract class TestContainerSupport { private static final String REDIS_IMAGE = "redis:latest"; @@ -41,4 +46,4 @@ public static void setUp(DynamicPropertyRegistry registry) { registry.add("spring.datasource.username", MYSQL::getUsername); registry.add("spring.datasource.password", MYSQL::getPassword); } -} \ No newline at end of file +}