diff --git a/src/main/java/kr/mayb/controller/OrderController.java b/src/main/java/kr/mayb/controller/OrderController.java new file mode 100644 index 0000000..c8d3245 --- /dev/null +++ b/src/main/java/kr/mayb/controller/OrderController.java @@ -0,0 +1,77 @@ +package kr.mayb.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import kr.mayb.dto.OrderInfo; +import kr.mayb.dto.OrderRequest; +import kr.mayb.dto.ProductSimple; +import kr.mayb.enums.PaymentStatus; +import kr.mayb.facade.OrderFacade; +import kr.mayb.security.DenyAll; +import kr.mayb.security.PermitAdmin; +import kr.mayb.security.PermitAuthenticated; +import kr.mayb.util.request.PageRequest; +import kr.mayb.util.response.ApiResponse; +import kr.mayb.util.response.PageResponse; +import kr.mayb.util.response.Responses; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@Tag(name = "Order", description = "주문 관련 API") +@DenyAll +@RestController +@RequiredArgsConstructor +public class OrderController { + + private final OrderFacade orderFacade; + + @Operation(summary = "주문하기") + @PermitAuthenticated + @PostMapping("/orders") + public ResponseEntity> makeOrder(@RequestBody OrderRequest request) { + OrderInfo response = orderFacade.makeOrder(request); + return Responses.ok(response); + } + + @Operation(summary = "내 주문 목록 조회") + @PermitAuthenticated + @GetMapping("/orders/me") + public ResponseEntity>> getMyOrders(PageRequest pageRequest) { + PageResponse response = orderFacade.getMyOrders(pageRequest); + return Responses.ok(response); + } + + @Operation(summary = "관리자 전체 주문 목록 조회") + @PermitAdmin + @GetMapping("/orders") + public ResponseEntity>>> getOrders( + @RequestParam(value = "pid", required = false) Long productId, + @RequestParam(value = "p_status", required = false) PaymentStatus paymentStatus, + PageRequest pageRequest + ) { + var response = orderFacade.getOrders(productId, paymentStatus, pageRequest); + return Responses.ok(response); + } + + @Operation(summary = "관리자 결제 상태 변경") + @PermitAdmin + @PatchMapping("orders/{orderId}/members/{memberId}/payment-status") + public ResponseEntity> updatePaymentStatus(@PathVariable long orderId, + @PathVariable long memberId, + @RequestBody @Valid OrderController.PaymentStatusUpdateRequest request) { + OrderInfo response = orderFacade.updatePaymentStatus(orderId, memberId, request.status()); + return Responses.ok(response); + + } + + private record PaymentStatusUpdateRequest( + @NotNull + PaymentStatus status + ) { + } +} diff --git a/src/main/java/kr/mayb/controller/SecurityController.java b/src/main/java/kr/mayb/controller/SecurityController.java index 7d9b3b1..21865a1 100644 --- a/src/main/java/kr/mayb/controller/SecurityController.java +++ b/src/main/java/kr/mayb/controller/SecurityController.java @@ -1,6 +1,7 @@ package kr.mayb.controller; import io.swagger.v3.oas.annotations.Hidden; +import io.swagger.v3.oas.annotations.tags.Tag; import kr.mayb.security.DenyAll; import kr.mayb.security.PermitAll; import kr.mayb.security.PermitAuthenticated; @@ -10,6 +11,7 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; +@Tag(name = "Security", description = "security 관련 Test API. Swagger 에 표시 안됨") @Hidden @DenyAll @RestController diff --git a/src/main/java/kr/mayb/data/model/Order.java b/src/main/java/kr/mayb/data/model/Order.java new file mode 100644 index 0000000..c0f5a19 --- /dev/null +++ b/src/main/java/kr/mayb/data/model/Order.java @@ -0,0 +1,44 @@ +package kr.mayb.data.model; + +import jakarta.persistence.*; +import kr.mayb.enums.PaymentMethod; +import kr.mayb.enums.PaymentStatus; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Table(schema = "mayb") +@Entity +public class Order extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @Column + private int totalPrice; + + @Column + @Enumerated(EnumType.STRING) + private PaymentMethod paymentMethod; + + @Column + @Enumerated(EnumType.STRING) + private PaymentStatus paymentStatus; + + @Column(nullable = false) + private long productId; + + @Column + private long productGenderPriceId; + + @Column + private long productScheduleId; + + @Column(nullable = false) + private long memberId; + + @Column(nullable = false) + private boolean hasReviewed; +} diff --git a/src/main/java/kr/mayb/data/model/Product.java b/src/main/java/kr/mayb/data/model/Product.java index 2438fe4..0908fa9 100644 --- a/src/main/java/kr/mayb/data/model/Product.java +++ b/src/main/java/kr/mayb/data/model/Product.java @@ -27,7 +27,7 @@ public class Product extends BaseEntity { private int originalPrice; @Column - private int salePrice; + private int discountPrice; @Column private String profileImageUrl; @@ -48,15 +48,14 @@ public class Product extends BaseEntity { @Enumerated(EnumType.STRING) private ProductStatus status; - @BatchSize(size = 20) - @OneToMany(mappedBy = "product", cascade = CascadeType.ALL) - private List productTags = new ArrayList<>(); + @Column + private String tags; @BatchSize(size = 20) @OneToMany(mappedBy = "product", cascade = CascadeType.ALL) - private List productGenders = new ArrayList<>(); + private List productGenderPrices = new ArrayList<>(); @BatchSize(size = 20) @OneToMany(mappedBy = "product", cascade = CascadeType.ALL) - private List productDateTimes = new ArrayList<>(); + private List productSchedules = new ArrayList<>(); } diff --git a/src/main/java/kr/mayb/data/model/ProductGender.java b/src/main/java/kr/mayb/data/model/ProductGenderPrice.java similarity index 89% rename from src/main/java/kr/mayb/data/model/ProductGender.java rename to src/main/java/kr/mayb/data/model/ProductGenderPrice.java index 26193c4..53a750b 100644 --- a/src/main/java/kr/mayb/data/model/ProductGender.java +++ b/src/main/java/kr/mayb/data/model/ProductGenderPrice.java @@ -8,7 +8,7 @@ @Setter @Table(schema = "mayb") @Entity -public class ProductGender extends BaseEntity { +public class ProductGenderPrice extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/src/main/java/kr/mayb/data/model/ProductDateTime.java b/src/main/java/kr/mayb/data/model/ProductSchedule.java similarity index 83% rename from src/main/java/kr/mayb/data/model/ProductDateTime.java rename to src/main/java/kr/mayb/data/model/ProductSchedule.java index 2da83f1..5296749 100644 --- a/src/main/java/kr/mayb/data/model/ProductDateTime.java +++ b/src/main/java/kr/mayb/data/model/ProductSchedule.java @@ -10,14 +10,14 @@ @Setter @Table(schema = "mayb") @Entity -public class ProductDateTime extends BaseEntity { +public class ProductSchedule extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private long id; @Column(nullable = false) - private LocalDateTime dateTime; + private LocalDateTime timeSlot; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "product_id", nullable = false) diff --git a/src/main/java/kr/mayb/data/model/ProductTag.java b/src/main/java/kr/mayb/data/model/ProductTag.java deleted file mode 100644 index cea4c8d..0000000 --- a/src/main/java/kr/mayb/data/model/ProductTag.java +++ /dev/null @@ -1,23 +0,0 @@ -package kr.mayb.data.model; - -import jakarta.persistence.*; -import lombok.Getter; -import lombok.Setter; - -@Getter -@Setter -@Table(schema = "mayb") -@Entity -public class ProductTag { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private long id; - - @Column(nullable = false) - private String name; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "product_id", nullable = false) - private Product product; -} diff --git a/src/main/java/kr/mayb/data/repository/MemberRepository.java b/src/main/java/kr/mayb/data/repository/MemberRepository.java index 5f530e9..dc45fa3 100644 --- a/src/main/java/kr/mayb/data/repository/MemberRepository.java +++ b/src/main/java/kr/mayb/data/repository/MemberRepository.java @@ -4,11 +4,14 @@ import kr.mayb.data.model.Member; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Collection; +import java.util.List; import java.util.Optional; public interface MemberRepository extends JpaRepository { - boolean existsByEmail(String email); Optional findByEmail(String email); + + List findAllByIdIn(Collection memberIds); } diff --git a/src/main/java/kr/mayb/data/repository/OrderRepository.java b/src/main/java/kr/mayb/data/repository/OrderRepository.java new file mode 100644 index 0000000..cacf79e --- /dev/null +++ b/src/main/java/kr/mayb/data/repository/OrderRepository.java @@ -0,0 +1,15 @@ +package kr.mayb.data.repository; + +import kr.mayb.data.model.Order; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +import java.util.Optional; + +public interface OrderRepository extends JpaRepository, JpaSpecificationExecutor { + Page findAllByMemberId(long memberId, Pageable pageable); + + Optional findByIdAndMemberId(long orderId, long memberId); +} diff --git a/src/main/java/kr/mayb/data/repository/ProductDateTimeRepository.java b/src/main/java/kr/mayb/data/repository/ProductDateTimeRepository.java deleted file mode 100644 index 49aceed..0000000 --- a/src/main/java/kr/mayb/data/repository/ProductDateTimeRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package kr.mayb.data.repository; - -import kr.mayb.data.model.Product; -import kr.mayb.data.model.ProductDateTime; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface ProductDateTimeRepository extends JpaRepository { - void deleteByProduct(Product product); -} diff --git a/src/main/java/kr/mayb/data/repository/ProductGenderPriceRepository.java b/src/main/java/kr/mayb/data/repository/ProductGenderPriceRepository.java new file mode 100644 index 0000000..9dcba57 --- /dev/null +++ b/src/main/java/kr/mayb/data/repository/ProductGenderPriceRepository.java @@ -0,0 +1,17 @@ +package kr.mayb.data.repository; + +import kr.mayb.data.model.Product; +import kr.mayb.data.model.ProductGenderPrice; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +public interface ProductGenderPriceRepository extends JpaRepository { + void deleteByProduct(Product product); + + Optional findByIdAndProduct(long id, Product product); + + List findAllByIdIn(Collection priceIds); +} diff --git a/src/main/java/kr/mayb/data/repository/ProductGenderRepository.java b/src/main/java/kr/mayb/data/repository/ProductGenderRepository.java deleted file mode 100644 index 993f459..0000000 --- a/src/main/java/kr/mayb/data/repository/ProductGenderRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package kr.mayb.data.repository; - -import kr.mayb.data.model.Product; -import kr.mayb.data.model.ProductGender; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface ProductGenderRepository extends JpaRepository { - void deleteByProduct(Product product); -} diff --git a/src/main/java/kr/mayb/data/repository/ProductRepository.java b/src/main/java/kr/mayb/data/repository/ProductRepository.java index e297cf2..40e86cd 100644 --- a/src/main/java/kr/mayb/data/repository/ProductRepository.java +++ b/src/main/java/kr/mayb/data/repository/ProductRepository.java @@ -3,5 +3,9 @@ import kr.mayb.data.model.Product; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Collection; +import java.util.List; + public interface ProductRepository extends JpaRepository { + List findAllByIdIn(Collection productIds); } diff --git a/src/main/java/kr/mayb/data/repository/ProductScheduleRepository.java b/src/main/java/kr/mayb/data/repository/ProductScheduleRepository.java new file mode 100644 index 0000000..3c27077 --- /dev/null +++ b/src/main/java/kr/mayb/data/repository/ProductScheduleRepository.java @@ -0,0 +1,17 @@ +package kr.mayb.data.repository; + +import kr.mayb.data.model.Product; +import kr.mayb.data.model.ProductSchedule; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +public interface ProductScheduleRepository extends JpaRepository { + void deleteByProduct(Product product); + + Optional findByIdAndProduct(long id, Product product); + + List findAllByIdIn(Collection scheduleIds); +} diff --git a/src/main/java/kr/mayb/data/repository/ProductTagRepository.java b/src/main/java/kr/mayb/data/repository/ProductTagRepository.java deleted file mode 100644 index cb71a96..0000000 --- a/src/main/java/kr/mayb/data/repository/ProductTagRepository.java +++ /dev/null @@ -1,10 +0,0 @@ -package kr.mayb.data.repository; - -import kr.mayb.data.model.Product; -import kr.mayb.data.model.ProductTag; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface ProductTagRepository extends JpaRepository { - - void deleteByProduct(Product product); -} diff --git a/src/main/java/kr/mayb/data/repository/specification/OrderSpecification.java b/src/main/java/kr/mayb/data/repository/specification/OrderSpecification.java new file mode 100644 index 0000000..0c2cfea --- /dev/null +++ b/src/main/java/kr/mayb/data/repository/specification/OrderSpecification.java @@ -0,0 +1,18 @@ +package kr.mayb.data.repository.specification; + +import kr.mayb.data.model.Order; +import kr.mayb.enums.PaymentStatus; +import org.springframework.data.jpa.domain.Specification; + +public class OrderSpecification { + + public static Specification withProductId(Long productId) { + return (root, query, criteriaBuilder) -> + productId != null ? criteriaBuilder.equal(root.get("productId"), productId) : criteriaBuilder.conjunction(); + } + + public static Specification withPaymentStatus(PaymentStatus paymentStatus) { + return (root, query, criteriaBuilder) -> + paymentStatus != null ? criteriaBuilder.equal(root.get("paymentStatus"), paymentStatus) : criteriaBuilder.conjunction(); + } +} diff --git a/src/main/java/kr/mayb/dto/DateTimeInfo.java b/src/main/java/kr/mayb/dto/DateTimeInfo.java deleted file mode 100644 index 2bbf552..0000000 --- a/src/main/java/kr/mayb/dto/DateTimeInfo.java +++ /dev/null @@ -1,14 +0,0 @@ -package kr.mayb.dto; - -import kr.mayb.data.model.ProductDateTime; - -import java.time.LocalDateTime; - -public record DateTimeInfo( - long id, - LocalDateTime dateTime -) { - public static DateTimeInfo of(ProductDateTime dateTime) { - return new DateTimeInfo(dateTime.getId(), dateTime.getDateTime()); - } -} diff --git a/src/main/java/kr/mayb/dto/GenderPrice.java b/src/main/java/kr/mayb/dto/GenderPrice.java index 0e72ee9..2016e14 100644 --- a/src/main/java/kr/mayb/dto/GenderPrice.java +++ b/src/main/java/kr/mayb/dto/GenderPrice.java @@ -1,14 +1,15 @@ package kr.mayb.dto; import jakarta.validation.constraints.NotBlank; -import kr.mayb.data.model.ProductGender; +import kr.mayb.data.model.ProductGenderPrice; public record GenderPrice( + long priceId, @NotBlank String gender, int price ) { - public static GenderPrice of(ProductGender gender) { - return new GenderPrice(gender.getGender(), gender.getPrice()); + public static GenderPrice of(ProductGenderPrice gender) { + return new GenderPrice(gender.getId(), gender.getGender(), gender.getPrice()); } } diff --git a/src/main/java/kr/mayb/dto/OrderInfo.java b/src/main/java/kr/mayb/dto/OrderInfo.java new file mode 100644 index 0000000..4d9676c --- /dev/null +++ b/src/main/java/kr/mayb/dto/OrderInfo.java @@ -0,0 +1,60 @@ +package kr.mayb.dto; + +import kr.mayb.data.model.Order; +import kr.mayb.data.model.Product; +import kr.mayb.data.model.ProductGenderPrice; +import kr.mayb.data.model.ProductSchedule; +import kr.mayb.enums.PaymentMethod; +import kr.mayb.enums.PaymentStatus; + +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.util.Optional; + +public record OrderInfo( + long orderId, + + int totalPrice, + PaymentMethod paymentMethod, + PaymentStatus paymentStatus, + boolean hasReviewed, + + String productName, + String productProfileImage, + + String gender, + LocalDateTime scheduledAt, + + String customerName, + + OffsetDateTime createdAt +) { + public static OrderInfo of(Order order, OrderedProductItem productItem, String customerName) { + String productName = Optional.ofNullable(productItem.product()) + .map(Product::getName) + .orElse(null); + String productProfileImageUrl = Optional.ofNullable(productItem.product()) + .map(Product::getProfileImageUrl) + .orElse(null); + String gender = Optional.of(productItem.genderPrice()) + .map(ProductGenderPrice::getGender) + .orElse(null); + LocalDateTime scheduledAt = Optional.ofNullable(productItem.schedule()) + .map(ProductSchedule::getTimeSlot) + .orElse(null); + + return new OrderInfo( + order.getId(), + order.getTotalPrice(), + order.getPaymentMethod(), + order.getPaymentStatus(), + order.isHasReviewed(), + productName, + productProfileImageUrl, + gender, + scheduledAt, + customerName, + order.getCreatedAt() + ); + } +} diff --git a/src/main/java/kr/mayb/dto/OrderRequest.java b/src/main/java/kr/mayb/dto/OrderRequest.java new file mode 100644 index 0000000..1b5d843 --- /dev/null +++ b/src/main/java/kr/mayb/dto/OrderRequest.java @@ -0,0 +1,19 @@ +package kr.mayb.dto; + +import jakarta.validation.constraints.NotNull; +import kr.mayb.enums.PaymentMethod; + +public record OrderRequest( + @NotNull + long productId, + + @NotNull + long priceId, + + @NotNull + long scheduleId, + + @NotNull + PaymentMethod paymentMethod +) { +} diff --git a/src/main/java/kr/mayb/dto/OrderedProductItem.java b/src/main/java/kr/mayb/dto/OrderedProductItem.java new file mode 100644 index 0000000..25225fc --- /dev/null +++ b/src/main/java/kr/mayb/dto/OrderedProductItem.java @@ -0,0 +1,15 @@ +package kr.mayb.dto; + +import kr.mayb.data.model.Product; +import kr.mayb.data.model.ProductGenderPrice; +import kr.mayb.data.model.ProductSchedule; + +public record OrderedProductItem( + Product product, + ProductGenderPrice genderPrice, + ProductSchedule schedule +) { + public static OrderedProductItem of(Product product, ProductGenderPrice genderPrice, ProductSchedule schedule) { + return new OrderedProductItem(product, genderPrice, schedule); + } +} diff --git a/src/main/java/kr/mayb/dto/ProductDto.java b/src/main/java/kr/mayb/dto/ProductDto.java index 069b08f..b1f868a 100644 --- a/src/main/java/kr/mayb/dto/ProductDto.java +++ b/src/main/java/kr/mayb/dto/ProductDto.java @@ -20,11 +20,11 @@ public class ProductDto { private final String description; private final int originalPrice; - private final int salePrice; + private final int discountPrice; - private final List tags; - private final List genderPrices; - private final List dateTimes; + private final List tags; + private final List prices; + private final List schedules; private final ProductStatus status; @@ -38,10 +38,10 @@ public ProductDto(Product product, boolean isAdmin) { this.detailImageUrl = product.getDetailImageUrl(); this.description = product.getDescription(); this.originalPrice = product.getOriginalPrice(); - this.salePrice = product.getSalePrice(); - this.tags = product.getProductTags().stream().map(TagInfo::of).toList(); - this.genderPrices = product.getProductGenders().stream().map(GenderPrice::of).toList(); - this.dateTimes = product.getProductDateTimes().stream().map(DateTimeInfo::of).toList(); + this.discountPrice = product.getDiscountPrice(); + this.tags = List.of(product.getTags().split("\\|")); + this.prices = product.getProductGenderPrices().stream().map(GenderPrice::of).toList(); + this.schedules = product.getProductSchedules().stream().map(ScheduleInfo::of).toList(); this.status = product.getStatus(); if (isAdmin) { diff --git a/src/main/java/kr/mayb/dto/ProductRegistrationRequest.java b/src/main/java/kr/mayb/dto/ProductRegistrationRequest.java index becc997..4122d7b 100644 --- a/src/main/java/kr/mayb/dto/ProductRegistrationRequest.java +++ b/src/main/java/kr/mayb/dto/ProductRegistrationRequest.java @@ -25,7 +25,7 @@ public record ProductRegistrationRequest( @NotNull @Size(min = 1) - List dateTimes, + List schedules, @Valid @NotNull diff --git a/src/main/java/kr/mayb/dto/ProductSimple.java b/src/main/java/kr/mayb/dto/ProductSimple.java new file mode 100644 index 0000000..602af64 --- /dev/null +++ b/src/main/java/kr/mayb/dto/ProductSimple.java @@ -0,0 +1,12 @@ +package kr.mayb.dto; + +import kr.mayb.data.model.Product; + +public record ProductSimple( + long productId, + String name +) { + public static ProductSimple of(Product product) { + return new ProductSimple(product.getId(), product.getName()); + } +} diff --git a/src/main/java/kr/mayb/dto/ProductUpdateRequest.java b/src/main/java/kr/mayb/dto/ProductUpdateRequest.java index c1e98dc..9a84479 100644 --- a/src/main/java/kr/mayb/dto/ProductUpdateRequest.java +++ b/src/main/java/kr/mayb/dto/ProductUpdateRequest.java @@ -25,7 +25,7 @@ public record ProductUpdateRequest( @NotNull @Size(min = 1) - List dateTimes, + List schedules, @Valid @NotNull diff --git a/src/main/java/kr/mayb/dto/ScheduleInfo.java b/src/main/java/kr/mayb/dto/ScheduleInfo.java new file mode 100644 index 0000000..b7c4c56 --- /dev/null +++ b/src/main/java/kr/mayb/dto/ScheduleInfo.java @@ -0,0 +1,14 @@ +package kr.mayb.dto; + +import kr.mayb.data.model.ProductSchedule; + +import java.time.LocalDateTime; + +public record ScheduleInfo( + long scheduleId, + LocalDateTime time +) { + public static ScheduleInfo of(ProductSchedule schedule) { + return new ScheduleInfo(schedule.getId(), schedule.getTimeSlot()); + } +} diff --git a/src/main/java/kr/mayb/dto/TagInfo.java b/src/main/java/kr/mayb/dto/TagInfo.java deleted file mode 100644 index da342ca..0000000 --- a/src/main/java/kr/mayb/dto/TagInfo.java +++ /dev/null @@ -1,12 +0,0 @@ -package kr.mayb.dto; - -import kr.mayb.data.model.ProductTag; - -public record TagInfo( - long id, - String name -) { - public static TagInfo of(ProductTag tag) { - return new TagInfo(tag.getId(), tag.getName()); - } -} diff --git a/src/main/java/kr/mayb/enums/OrderSort.java b/src/main/java/kr/mayb/enums/OrderSort.java new file mode 100644 index 0000000..82339a9 --- /dev/null +++ b/src/main/java/kr/mayb/enums/OrderSort.java @@ -0,0 +1,18 @@ +package kr.mayb.enums; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Sort; + +@Getter +@RequiredArgsConstructor +public enum OrderSort { + NEWEST_FIRST("createdAt"), + ; + + private final String value; + + public Sort toSortOption() { + return Sort.by(Sort.Direction.DESC, NEWEST_FIRST.getValue()); + } +} diff --git a/src/main/java/kr/mayb/enums/PaymentMethod.java b/src/main/java/kr/mayb/enums/PaymentMethod.java new file mode 100644 index 0000000..650db79 --- /dev/null +++ b/src/main/java/kr/mayb/enums/PaymentMethod.java @@ -0,0 +1,5 @@ +package kr.mayb.enums; + +public enum PaymentMethod { + BANK_TRANSFER, +} diff --git a/src/main/java/kr/mayb/enums/PaymentStatus.java b/src/main/java/kr/mayb/enums/PaymentStatus.java new file mode 100644 index 0000000..c0b7c86 --- /dev/null +++ b/src/main/java/kr/mayb/enums/PaymentStatus.java @@ -0,0 +1,8 @@ +package kr.mayb.enums; + +public enum PaymentStatus { + PENDING, + COMPLETED, + REFUNDED, + ; +} diff --git a/src/main/java/kr/mayb/facade/OrderFacade.java b/src/main/java/kr/mayb/facade/OrderFacade.java new file mode 100644 index 0000000..3159610 --- /dev/null +++ b/src/main/java/kr/mayb/facade/OrderFacade.java @@ -0,0 +1,94 @@ +package kr.mayb.facade; + +import kr.mayb.data.model.Member; +import kr.mayb.data.model.Order; +import kr.mayb.dto.*; +import kr.mayb.enums.PaymentStatus; +import kr.mayb.service.MemberService; +import kr.mayb.service.OrderService; +import kr.mayb.service.ProductService; +import kr.mayb.util.ContextUtils; +import kr.mayb.util.request.PageRequest; +import kr.mayb.util.response.PageResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +@Component +@RequiredArgsConstructor +public class OrderFacade { + private final MemberService memberService; + private final OrderService orderService; + private final ProductService productService; + + private static final String DEFAULT_UNKNOWN_MEMBER_NAME = "탈퇴하거나 찾을 수 없는 회원입니다."; + + public OrderInfo makeOrder(OrderRequest request) { + MemberDto member = ContextUtils.loadMember(); + OrderedProductItem productItem = productService.findOrderedProductItem(request.productId(), request.priceId(), request.scheduleId()); + + Order saved = orderService.makeOrder(productItem, request.paymentMethod(), member.getMemberId()); + return convertToOrderInfo(saved); + } + + public PageResponse getMyOrders(PageRequest pageRequest) { + MemberDto member = ContextUtils.loadMember(); + Page orders = orderService.getMyOrders(member.getMemberId(), pageRequest); + + List converted = convertToOrderInfos(orders.getContent()); + return PageResponse.of(new PageImpl<>(converted, orders.getPageable(), orders.getTotalElements())); + } + + public PageResponse> getOrders(Long productId, PaymentStatus paymentStatus, PageRequest pageRequest) { + Page orders = orderService.getOrders(productId, paymentStatus, pageRequest); + + List converted = convertToOrderInfos(orders.getContent()); + PageImpl orderInfoPage = new PageImpl<>(converted, orders.getPageable(), orders.getTotalElements()); + return PageResponse.of(orderInfoPage, getProductMetaData()); + } + + public OrderInfo updatePaymentStatus(long orderId, long memberId, PaymentStatus status) { + Order updated = orderService.updatePaymentStatus(orderId, memberId, status); + return convertToOrderInfo(updated); + } + + private List getProductMetaData() { + return productService.findAll() + .stream() + .map(ProductSimple::of) + .toList(); + } + + private List convertToOrderInfos(List orders) { + Map memberMap = memberService.findAllByIdIn(orders.stream().map(Order::getMemberId).collect(Collectors.toSet())); + Set productIds = orders.stream().map(Order::getProductId).collect(Collectors.toSet()); + Set priceIds = orders.stream().map(Order::getProductGenderPriceId).collect(Collectors.toSet()); + Set scheduleIds = orders.stream().map(Order::getProductScheduleId).collect(Collectors.toSet()); + + var orderedProductItemMap = productService.findOrderedProductItems(productIds, priceIds, scheduleIds, orders); + return orders.stream() + .map(order -> { + String customerName = Optional.ofNullable(memberMap.get(order.getMemberId())) + .map(Member::getName) + .orElse(DEFAULT_UNKNOWN_MEMBER_NAME); + OrderedProductItem productItem = orderedProductItemMap.get(order.getId()); + + return OrderInfo.of(order, productItem, customerName); + }) + .toList(); + } + + private OrderInfo convertToOrderInfo(Order saved) { + Member member = memberService.getMember(saved.getMemberId()); + OrderedProductItem productItem = productService.findOrderedProductItem(saved.getProductId(), saved.getProductGenderPriceId(), saved.getProductScheduleId()); + + return OrderInfo.of(saved, productItem, member.getName()); + } +} diff --git a/src/main/java/kr/mayb/service/MemberService.java b/src/main/java/kr/mayb/service/MemberService.java index f1b58b0..13cdad2 100644 --- a/src/main/java/kr/mayb/service/MemberService.java +++ b/src/main/java/kr/mayb/service/MemberService.java @@ -17,7 +17,11 @@ import org.springframework.stereotype.Service; import java.util.Collections; +import java.util.Map; import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; @Service @RequiredArgsConstructor @@ -83,6 +87,12 @@ public MemberDto updateMember(long memberId, MemberUpdateRequest request) { return convertToMemberDto(updated); } + public Map findAllByIdIn(Set memberIds) { + return memberRepository.findAllByIdIn(memberIds) + .stream() + .collect(Collectors.toMap(Member::getId, Function.identity())); + } + private MemberDto convertToMemberDto(Member member) { String contact = aesgcmEncoder.decrypt(member.getContact()); return MemberDto.of(member, contact); diff --git a/src/main/java/kr/mayb/service/OrderService.java b/src/main/java/kr/mayb/service/OrderService.java new file mode 100644 index 0000000..526f4a7 --- /dev/null +++ b/src/main/java/kr/mayb/service/OrderService.java @@ -0,0 +1,63 @@ +package kr.mayb.service; + +import jakarta.transaction.Transactional; +import kr.mayb.data.model.Order; +import kr.mayb.data.repository.OrderRepository; +import kr.mayb.data.repository.specification.OrderSpecification; +import kr.mayb.dto.OrderedProductItem; +import kr.mayb.enums.OrderSort; +import kr.mayb.enums.PaymentMethod; +import kr.mayb.enums.PaymentStatus; +import kr.mayb.error.ResourceNotFoundException; +import kr.mayb.util.request.PageRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class OrderService { + + private final OrderRepository orderRepository; + + @Transactional + public Order makeOrder(OrderedProductItem productItem, PaymentMethod paymentMethod, long memberId) { + Order order = new Order(); + order.setPaymentMethod(paymentMethod); + order.setTotalPrice(productItem.genderPrice().getPrice()); + order.setPaymentStatus(PaymentStatus.PENDING); + order.setProductId(productItem.product().getId()); + order.setProductGenderPriceId(productItem.genderPrice().getId()); + order.setProductScheduleId(productItem.schedule().getId()); + order.setMemberId(memberId); + order.setHasReviewed(false); + + return orderRepository.save(order); + } + + public Page getMyOrders(long memberId, PageRequest pageRequest) { + Pageable pageable = pageRequest.toPageable(OrderSort.NEWEST_FIRST.toSortOption()); + return orderRepository.findAllByMemberId(memberId, pageable); + } + + public Page getOrders(Long productId, PaymentStatus paymentStatus, PageRequest pageRequest) { + Pageable pageable = pageRequest.toPageable(OrderSort.NEWEST_FIRST.toSortOption()); + Specification spec = Specification + .where(OrderSpecification.withProductId(productId)) + .and(OrderSpecification.withPaymentStatus(paymentStatus)); + + return orderRepository.findAll(spec, pageable); + } + + @Transactional + public Order updatePaymentStatus(long orderId, long memberId, PaymentStatus paymentStatus) { + Order order = orderRepository.findByIdAndMemberId(orderId, memberId) + .orElseThrow(() -> new ResourceNotFoundException("There is no Order with orderId and memberId." + orderId + ", " + memberId)); + + order.setPaymentStatus(paymentStatus); + + return orderRepository.save(order); + } +} diff --git a/src/main/java/kr/mayb/service/ProductService.java b/src/main/java/kr/mayb/service/ProductService.java index 43cac4a..816c26b 100644 --- a/src/main/java/kr/mayb/service/ProductService.java +++ b/src/main/java/kr/mayb/service/ProductService.java @@ -1,30 +1,25 @@ package kr.mayb.service; -import io.micrometer.common.util.StringUtils; import jakarta.transaction.Transactional; +import kr.mayb.data.model.Order; import kr.mayb.data.model.Product; -import kr.mayb.data.model.ProductDateTime; -import kr.mayb.data.model.ProductGender; -import kr.mayb.data.model.ProductTag; -import kr.mayb.data.repository.ProductDateTimeRepository; -import kr.mayb.data.repository.ProductGenderRepository; +import kr.mayb.data.model.ProductGenderPrice; +import kr.mayb.data.model.ProductSchedule; +import kr.mayb.data.repository.ProductGenderPriceRepository; import kr.mayb.data.repository.ProductRepository; -import kr.mayb.data.repository.ProductTagRepository; -import kr.mayb.dto.GenderPrice; -import kr.mayb.dto.ProductDto; -import kr.mayb.dto.ProductRegistrationRequest; -import kr.mayb.dto.ProductUpdateRequest; +import kr.mayb.data.repository.ProductScheduleRepository; +import kr.mayb.dto.*; import kr.mayb.enums.GcsBucketPath; import kr.mayb.enums.ProductStatus; import kr.mayb.error.ResourceNotFoundException; import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.tuple.Pair; import org.springframework.security.access.AccessDeniedException; import org.springframework.stereotype.Service; import java.time.LocalDateTime; -import java.util.List; -import java.util.Objects; -import java.util.Optional; +import java.util.*; +import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -35,63 +30,52 @@ public class ProductService { private final ImageService imageService; private final ProductRepository productRepository; - private final ProductTagRepository productTagRepository; - private final ProductGenderRepository productGenderRepository; - private final ProductDateTimeRepository productDateTimeRepository; + private final ProductGenderPriceRepository productGenderPriceRepository; + private final ProductScheduleRepository productScheduleRepository; @Transactional public ProductDto registerProduct(ProductRegistrationRequest request, String profileUrl, String detailUrl, long creatorId) { Product product = new Product(); product.setName(request.name()); product.setOriginalPrice(request.originalPrice()); - product.setSalePrice(request.salePrice()); + product.setDiscountPrice(request.salePrice()); product.setProfileImageUrl(profileUrl); product.setDetailImageUrl(detailUrl); product.setDescription(request.description()); product.setCreatorId(creatorId); product.setLastModifierId(creatorId); product.setStatus(ProductStatus.ACTIVE); + product.setTags(String.join("|", request.tags())); - saveAdditionalInfo(request.tags(), request.dateTimes(), request.genderPrices(), product); + saveAdditionalInfo(request.schedules(), request.genderPrices(), product); Product saved = productRepository.save(product); return ProductDto.of(saved, true); } - private void saveAdditionalInfo(List tags, List dateTimes, List genderPrices, Product product) { - List productTags = tags.stream() - .filter(StringUtils::isNotBlank) - .map(tag -> { - ProductTag productTag = new ProductTag(); - productTag.setName(tag); - productTag.setProduct(product); - return productTag; - }) - .collect(Collectors.toList()); - - List productDateTimes = dateTimes.stream() + private void saveAdditionalInfo(List schedules, List genderPrices, Product product) { + List productSchedules = schedules.stream() .filter(Objects::nonNull) - .map(dateTime -> { - ProductDateTime productDateTime = new ProductDateTime(); - productDateTime.setDateTime(dateTime); - productDateTime.setProduct(product); - return productDateTime; + .map(time -> { + ProductSchedule productSchedule = new ProductSchedule(); + productSchedule.setTimeSlot(time); + productSchedule.setProduct(product); + return productSchedule; }) .collect(Collectors.toList()); - List productGenders = genderPrices.stream() + List productGenderPrices = genderPrices.stream() .map(genderPrice -> { - ProductGender productGender = new ProductGender(); - productGender.setGender(genderPrice.gender()); - productGender.setPrice(genderPrice.price()); - productGender.setProduct(product); - return productGender; + ProductGenderPrice productGenderPrice = new ProductGenderPrice(); + productGenderPrice.setGender(genderPrice.gender()); + productGenderPrice.setPrice(genderPrice.price()); + productGenderPrice.setProduct(product); + return productGenderPrice; }) .collect(Collectors.toList()); - product.setProductTags(productTags); - product.setProductDateTimes(productDateTimes); - product.setProductGenders(productGenders); + product.setProductSchedules(productSchedules); + product.setProductGenderPrices(productGenderPrices); } public List getProducts(boolean isAdmin) { @@ -113,8 +97,7 @@ public List getProducts(boolean isAdmin) { } public ProductDto getProduct(long productId, boolean isAdmin) { - Product product = productRepository.findById(productId) - .orElseThrow(() -> new ResourceNotFoundException("Product not found.: " + productId)); + Product product = getProduct(productId); if (isAdmin) { return ProductDto.of(product, true); @@ -129,13 +112,13 @@ public ProductDto getProduct(long productId, boolean isAdmin) { @Transactional public ProductDto updateProduct(long productId, Optional profileUrl, Optional detailUrl, ProductUpdateRequest request, long modifierId) { - Product product = productRepository.findById(productId) - .orElseThrow(() -> new ResourceNotFoundException("Product not found.: " + productId)); + Product product = getProduct(productId); product.setName(request.name()); product.setOriginalPrice(request.originalPrice()); - product.setSalePrice(request.salePrice()); + product.setDiscountPrice(request.salePrice()); product.setDescription(request.description()); + product.setTags(String.join("|", request.tags())); updateProductImage(profileUrl, detailUrl, product); clearAndUpdateAdditionalInfo(request, product); @@ -152,8 +135,7 @@ public void delete(long productId) { @Transactional public void changeStatus(long productId, boolean active, long memberId) { - Product product = productRepository.findById(productId) - .orElseThrow(() -> new ResourceNotFoundException("Product not found.: " + productId)); + Product product = getProduct(productId); if (active) { product.setStatus(ProductStatus.ACTIVE); @@ -164,6 +146,38 @@ public void changeStatus(long productId, boolean active, long memberId) { product.setLastModifierId(memberId); } + public OrderedProductItem findOrderedProductItem(long productId, long priceId, long scheduleId) { + Product product = getProduct(productId); + ProductGenderPrice genderPrice = getGenderPrice(priceId, product); + ProductSchedule schedule = getSchedule(scheduleId, product); + + return OrderedProductItem.of(product, genderPrice, schedule); + } + + public Map findOrderedProductItems(Set productIds, Set priceIds, Set scheduleIds, List orders) { + Map productMap = productRepository.findAllByIdIn(productIds) + .stream() + .collect(Collectors.toMap(Product::getId, Function.identity())); + Map genderPriceMap = productGenderPriceRepository.findAllByIdIn(priceIds) + .stream() + .collect(Collectors.toMap(ProductGenderPrice::getId, Function.identity())); + Map scheduleMap = productScheduleRepository.findAllByIdIn(scheduleIds) + .stream() + .collect(Collectors.toMap(ProductSchedule::getId, Function.identity())); + + return orders + .stream() + .map(order -> { + Product product = productMap.get(order.getProductId()); + ProductGenderPrice genderPrice = genderPriceMap.get(order.getProductGenderPriceId()); + ProductSchedule schedule = scheduleMap.get(order.getProductScheduleId()); + + OrderedProductItem orderedProductItem = OrderedProductItem.of(product, genderPrice, schedule); + return Pair.of(order.getId(), orderedProductItem); + }) + .collect(Collectors.toMap(Pair::getLeft, Pair::getRight)); + } + private void updateProductImage(Optional profileUrl, Optional detailUrl, Product product) { profileUrl.ifPresent(url -> { imageService.delete(product.getProfileImageUrl(), GcsBucketPath.PRODUCT_PROFILE); @@ -176,9 +190,27 @@ private void updateProductImage(Optional profileUrl, Optional de } private void clearAndUpdateAdditionalInfo(ProductUpdateRequest request, Product product) { - productTagRepository.deleteByProduct(product); - productGenderRepository.deleteByProduct(product); - productDateTimeRepository.deleteByProduct(product); - saveAdditionalInfo(request.tags(), request.dateTimes(), request.genderPrices(), product); + productGenderPriceRepository.deleteByProduct(product); + productScheduleRepository.deleteByProduct(product); + saveAdditionalInfo(request.schedules(), request.genderPrices(), product); + } + + private ProductSchedule getSchedule(long scheduleId, Product product) { + return productScheduleRepository.findByIdAndProduct(scheduleId, product) + .orElseThrow(() -> new ResourceNotFoundException("Schedule not found: " + scheduleId)); + } + + private ProductGenderPrice getGenderPrice(long priceId, Product product) { + return productGenderPriceRepository.findByIdAndProduct(priceId, product) + .orElseThrow(() -> new ResourceNotFoundException("Price not found: " + priceId)); + } + + private Product getProduct(long productId) { + return productRepository.findById(productId) + .orElseThrow(() -> new ResourceNotFoundException("Product not found: " + productId)); + } + + public List findAll() { + return productRepository.findAll(); } } diff --git a/src/main/java/kr/mayb/util/request/PageRequest.java b/src/main/java/kr/mayb/util/request/PageRequest.java index a0ef9bf..db9b283 100644 --- a/src/main/java/kr/mayb/util/request/PageRequest.java +++ b/src/main/java/kr/mayb/util/request/PageRequest.java @@ -21,11 +21,11 @@ public class PageRequest { private final String sort; public static PageRequest getDefault() { - return new PageRequest(0, 10, null); + return new PageRequest(0, 5, null); } public static Pageable defaultPageable() { - return defaultPageable(10); + return defaultPageable(5); } public static Pageable defaultPageable(int size) { diff --git a/src/main/java/kr/mayb/util/request/PageRequestResolver.java b/src/main/java/kr/mayb/util/request/PageRequestResolver.java index cb73761..f5f6742 100644 --- a/src/main/java/kr/mayb/util/request/PageRequestResolver.java +++ b/src/main/java/kr/mayb/util/request/PageRequestResolver.java @@ -9,7 +9,7 @@ public class PageRequestResolver implements HandlerMethodArgumentResolver { - private static final int DEFAULT_PAGE_SIZE = 10; + private static final int DEFAULT_PAGE_SIZE = 5; private static final int DEFAULT_PAGE_NUMBER = 0; private static final int MAX_PAGE_SIZE = 100; private static final int MAX_ELEMENT_SIZE = 5000; @@ -35,7 +35,7 @@ public kr.mayb.util.request.PageRequest resolveArgument(MethodParameter paramete private int parseAndApplyBoundaries(String parameter, int defaultValue, int upper) { try { int parsed = Integer.parseInt(parameter); - return parsed < 0 ? defaultValue : parsed > upper ? upper : parsed; + return parsed < 0 ? defaultValue : Math.min(parsed, upper); } catch (NumberFormatException e) { return defaultValue; }