From 2f17429a36ae531630bcf4b46116efa0620aa5e9 Mon Sep 17 00:00:00 2001 From: 0AndWild Date: Thu, 1 May 2025 21:01:44 +0900 Subject: [PATCH 1/6] feat: add product models --- src/main/java/kr/mayb/data/model/Member.java | 1 - src/main/java/kr/mayb/data/model/Product.java | 34 +++++++++++++++++++ .../kr/mayb/data/model/ProductDateTime.java | 24 +++++++++++++ .../kr/mayb/data/model/ProductGender.java | 22 ++++++++++++ .../java/kr/mayb/data/model/ProductTag.java | 22 ++++++++++++ .../repository/ProductDateTimeRepository.java | 7 ++++ .../repository/ProductGenderRepository.java | 7 ++++ .../data/repository/ProductTagRepository.java | 7 ++++ .../java/kr/mayb/security/PermitAdmin.java | 15 ++++++++ 9 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 src/main/java/kr/mayb/data/model/Product.java create mode 100644 src/main/java/kr/mayb/data/model/ProductDateTime.java create mode 100644 src/main/java/kr/mayb/data/model/ProductGender.java create mode 100644 src/main/java/kr/mayb/data/model/ProductTag.java create mode 100644 src/main/java/kr/mayb/data/repository/ProductDateTimeRepository.java create mode 100644 src/main/java/kr/mayb/data/repository/ProductGenderRepository.java create mode 100644 src/main/java/kr/mayb/data/repository/ProductTagRepository.java create mode 100644 src/main/java/kr/mayb/security/PermitAdmin.java diff --git a/src/main/java/kr/mayb/data/model/Member.java b/src/main/java/kr/mayb/data/model/Member.java index 72e072a..50e3efc 100644 --- a/src/main/java/kr/mayb/data/model/Member.java +++ b/src/main/java/kr/mayb/data/model/Member.java @@ -53,7 +53,6 @@ public class Member extends BaseEntity{ @Column private String idealType; - @JsonIgnore @BatchSize(size = 10) @ManyToMany(fetch = FetchType.LAZY) diff --git a/src/main/java/kr/mayb/data/model/Product.java b/src/main/java/kr/mayb/data/model/Product.java new file mode 100644 index 0000000..d941e1f --- /dev/null +++ b/src/main/java/kr/mayb/data/model/Product.java @@ -0,0 +1,34 @@ +package kr.mayb.data.model; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Table(schema = "mayb") +@Entity +public class Product extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @Column + private String name; + + @Column + private int originalPrice; + + @Column + private int salePrice; + + @Column + private String productImageUrl; + + @Column + private String productDetailImageUrl; + + @Column + private String description; +} diff --git a/src/main/java/kr/mayb/data/model/ProductDateTime.java b/src/main/java/kr/mayb/data/model/ProductDateTime.java new file mode 100644 index 0000000..0ea0e00 --- /dev/null +++ b/src/main/java/kr/mayb/data/model/ProductDateTime.java @@ -0,0 +1,24 @@ +package kr.mayb.data.model; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDateTime; + +@Getter +@Setter +@Table(schema = "mayb") +@Entity +public class ProductDateTime extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @Column + private LocalDateTime dateTime; + + @Column + private long productId; +} diff --git a/src/main/java/kr/mayb/data/model/ProductGender.java b/src/main/java/kr/mayb/data/model/ProductGender.java new file mode 100644 index 0000000..f009f56 --- /dev/null +++ b/src/main/java/kr/mayb/data/model/ProductGender.java @@ -0,0 +1,22 @@ +package kr.mayb.data.model; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Table(schema = "mayb") +@Entity +public class ProductGender extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @Column + private String gender; + + @Column + private long productId; +} diff --git a/src/main/java/kr/mayb/data/model/ProductTag.java b/src/main/java/kr/mayb/data/model/ProductTag.java new file mode 100644 index 0000000..2abccb5 --- /dev/null +++ b/src/main/java/kr/mayb/data/model/ProductTag.java @@ -0,0 +1,22 @@ +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, unique = true) + private String name; + + @Column + private long productId; +} diff --git a/src/main/java/kr/mayb/data/repository/ProductDateTimeRepository.java b/src/main/java/kr/mayb/data/repository/ProductDateTimeRepository.java new file mode 100644 index 0000000..da70d1e --- /dev/null +++ b/src/main/java/kr/mayb/data/repository/ProductDateTimeRepository.java @@ -0,0 +1,7 @@ +package kr.mayb.data.repository; + +import kr.mayb.data.model.ProductDateTime; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ProductDateTimeRepository extends JpaRepository { +} diff --git a/src/main/java/kr/mayb/data/repository/ProductGenderRepository.java b/src/main/java/kr/mayb/data/repository/ProductGenderRepository.java new file mode 100644 index 0000000..d18c78d --- /dev/null +++ b/src/main/java/kr/mayb/data/repository/ProductGenderRepository.java @@ -0,0 +1,7 @@ +package kr.mayb.data.repository; + +import kr.mayb.data.model.ProductGender; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ProductGenderRepository extends JpaRepository { +} diff --git a/src/main/java/kr/mayb/data/repository/ProductTagRepository.java b/src/main/java/kr/mayb/data/repository/ProductTagRepository.java new file mode 100644 index 0000000..86e29f8 --- /dev/null +++ b/src/main/java/kr/mayb/data/repository/ProductTagRepository.java @@ -0,0 +1,7 @@ +package kr.mayb.data.repository; + +import kr.mayb.data.model.ProductTag; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ProductTagRepository extends JpaRepository { +} diff --git a/src/main/java/kr/mayb/security/PermitAdmin.java b/src/main/java/kr/mayb/security/PermitAdmin.java new file mode 100644 index 0000000..ef8c7af --- /dev/null +++ b/src/main/java/kr/mayb/security/PermitAdmin.java @@ -0,0 +1,15 @@ +package kr.mayb.security; + +import org.springframework.security.access.prepost.PreAuthorize; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@PreAuthorize("hasAuthority('ADMIN')") +public @interface PermitAdmin { +} + From 01d382e86946dedea1414ae6b7aa6c49346872a5 Mon Sep 17 00:00:00 2001 From: 0AndWild Date: Thu, 1 May 2025 23:34:18 +0900 Subject: [PATCH 2/6] feat: add registerProduct API --- .../kr/mayb/controller/ProductController.java | 39 +++++++++ src/main/java/kr/mayb/data/model/Product.java | 27 ++++++- .../kr/mayb/data/model/ProductDateTime.java | 7 +- .../kr/mayb/data/model/ProductGender.java | 7 +- .../java/kr/mayb/data/model/ProductTag.java | 7 +- .../data/repository/ProductRepository.java | 7 ++ src/main/java/kr/mayb/dto/MemberDto.java | 13 ++- src/main/java/kr/mayb/dto/ProductDto.java | 45 +++++++++++ .../mayb/dto/ProductRegistrationRequest.java | 33 ++++++++ .../java/kr/mayb/facade/ProductFacade.java | 38 +++++++++ .../java/kr/mayb/security/PermitAdmin.java | 2 +- .../java/kr/mayb/service/ProductService.java | 81 +++++++++++++++++++ 12 files changed, 293 insertions(+), 13 deletions(-) create mode 100644 src/main/java/kr/mayb/controller/ProductController.java create mode 100644 src/main/java/kr/mayb/data/repository/ProductRepository.java create mode 100644 src/main/java/kr/mayb/dto/ProductDto.java create mode 100644 src/main/java/kr/mayb/dto/ProductRegistrationRequest.java create mode 100644 src/main/java/kr/mayb/facade/ProductFacade.java create mode 100644 src/main/java/kr/mayb/service/ProductService.java diff --git a/src/main/java/kr/mayb/controller/ProductController.java b/src/main/java/kr/mayb/controller/ProductController.java new file mode 100644 index 0000000..52a3437 --- /dev/null +++ b/src/main/java/kr/mayb/controller/ProductController.java @@ -0,0 +1,39 @@ +package kr.mayb.controller; + + +import io.swagger.v3.oas.annotations.tags.Tag; +import kr.mayb.dto.ProductDto; +import kr.mayb.dto.ProductRegistrationRequest; +import kr.mayb.facade.ProductFacade; +import kr.mayb.security.DenyAll; +import kr.mayb.security.PermitAdmin; +import kr.mayb.util.response.ApiResponse; +import kr.mayb.util.response.ListResponse; +import kr.mayb.util.response.Responses; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + + +@Tag(name = "Product", description = "상품 관련 API") +@DenyAll +@RestController +@RequiredArgsConstructor +public class ProductController { + + private final ProductFacade productFacade; + + @PermitAdmin + @PostMapping("/products") + public ResponseEntity>> registerProduct(@RequestPart(value = "profile") MultipartFile profileImage, + @RequestPart(value = "detail") MultipartFile detailImage, + @RequestPart(value = "product") ProductRegistrationRequest request) { + List response = productFacade.registerProduct(profileImage, detailImage, request); + return Responses.ok(response); + } +} diff --git a/src/main/java/kr/mayb/data/model/Product.java b/src/main/java/kr/mayb/data/model/Product.java index d941e1f..131d44b 100644 --- a/src/main/java/kr/mayb/data/model/Product.java +++ b/src/main/java/kr/mayb/data/model/Product.java @@ -3,6 +3,10 @@ import jakarta.persistence.*; import lombok.Getter; import lombok.Setter; +import org.hibernate.annotations.BatchSize; + +import java.util.ArrayList; +import java.util.List; @Getter @Setter @@ -11,6 +15,7 @@ public class Product extends BaseEntity { @Id + @Column(name = "product_id") @GeneratedValue(strategy = GenerationType.IDENTITY) private long id; @@ -24,11 +29,29 @@ public class Product extends BaseEntity { private int salePrice; @Column - private String productImageUrl; + private String profileImageUrl; @Column - private String productDetailImageUrl; + private String detailImageUrl; @Column private String description; + + @Column + private long creatorId; + + @Column + private long lastModifierId; + + @BatchSize(size = 20) + @OneToMany(mappedBy = "product", cascade = CascadeType.ALL) + private List productTags = new ArrayList<>(); + + @BatchSize(size = 20) + @OneToMany(mappedBy = "product", cascade = CascadeType.ALL) + private List productGenders = new ArrayList<>(); + + @BatchSize(size = 20) + @OneToMany(mappedBy = "product", cascade = CascadeType.ALL) + private List productDateTimes = new ArrayList<>(); } diff --git a/src/main/java/kr/mayb/data/model/ProductDateTime.java b/src/main/java/kr/mayb/data/model/ProductDateTime.java index 0ea0e00..2da83f1 100644 --- a/src/main/java/kr/mayb/data/model/ProductDateTime.java +++ b/src/main/java/kr/mayb/data/model/ProductDateTime.java @@ -16,9 +16,10 @@ public class ProductDateTime extends BaseEntity { @GeneratedValue(strategy = GenerationType.IDENTITY) private long id; - @Column + @Column(nullable = false) private LocalDateTime dateTime; - @Column - private long productId; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "product_id", nullable = false) + private Product product; } diff --git a/src/main/java/kr/mayb/data/model/ProductGender.java b/src/main/java/kr/mayb/data/model/ProductGender.java index f009f56..0cda298 100644 --- a/src/main/java/kr/mayb/data/model/ProductGender.java +++ b/src/main/java/kr/mayb/data/model/ProductGender.java @@ -14,9 +14,10 @@ public class ProductGender extends BaseEntity { @GeneratedValue(strategy = GenerationType.IDENTITY) private long id; - @Column + @Column(nullable = false) private String gender; - @Column - private long productId; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "product_id", nullable = false) + private Product product; } diff --git a/src/main/java/kr/mayb/data/model/ProductTag.java b/src/main/java/kr/mayb/data/model/ProductTag.java index 2abccb5..cea4c8d 100644 --- a/src/main/java/kr/mayb/data/model/ProductTag.java +++ b/src/main/java/kr/mayb/data/model/ProductTag.java @@ -14,9 +14,10 @@ public class ProductTag { @GeneratedValue(strategy = GenerationType.IDENTITY) private long id; - @Column(nullable = false, unique = true) + @Column(nullable = false) private String name; - @Column - private long productId; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "product_id", nullable = false) + private Product product; } diff --git a/src/main/java/kr/mayb/data/repository/ProductRepository.java b/src/main/java/kr/mayb/data/repository/ProductRepository.java new file mode 100644 index 0000000..e297cf2 --- /dev/null +++ b/src/main/java/kr/mayb/data/repository/ProductRepository.java @@ -0,0 +1,7 @@ +package kr.mayb.data.repository; + +import kr.mayb.data.model.Product; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ProductRepository extends JpaRepository { +} diff --git a/src/main/java/kr/mayb/dto/MemberDto.java b/src/main/java/kr/mayb/dto/MemberDto.java index 9a94206..6a0eb5c 100644 --- a/src/main/java/kr/mayb/dto/MemberDto.java +++ b/src/main/java/kr/mayb/dto/MemberDto.java @@ -1,12 +1,15 @@ package kr.mayb.dto; +import kr.mayb.data.model.Authority; import kr.mayb.data.model.Member; import kr.mayb.enums.AccountStatus; +import kr.mayb.enums.AuthorityName; import kr.mayb.enums.Gender; import lombok.AllArgsConstructor; import lombok.Getter; import java.time.LocalDate; +import java.util.List; @Getter @AllArgsConstructor @@ -34,7 +37,14 @@ public class MemberDto { private AccountStatus status; + private List authorities; + public static MemberDto of(Member member, String contact) { + List authorityNames = member.getAuthorities() + .stream() + .map(Authority::getName) + .toList(); + return new MemberDto( member.getId(), member.getEmail(), @@ -46,7 +56,8 @@ public static MemberDto of(Member member, String contact) { contact, member.getIntroduction(), member.getIdealType(), - member.getStatus() + member.getStatus(), + authorityNames ); } } diff --git a/src/main/java/kr/mayb/dto/ProductDto.java b/src/main/java/kr/mayb/dto/ProductDto.java new file mode 100644 index 0000000..df9b54f --- /dev/null +++ b/src/main/java/kr/mayb/dto/ProductDto.java @@ -0,0 +1,45 @@ +package kr.mayb.dto; + +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 java.time.LocalDateTime; +import java.util.List; + +public record ProductDto( + long productId, + + String name, + String profileImageUrl, + String detailImageUrl, + String description, + + int originalPrice, + int salePrice, + + List productTags, + List productGenders, + List productDateTimes, + + long creatorId, + long lastModifierId +) { + public static ProductDto of(Product product) { + return new ProductDto( + product.getId(), + product.getName(), + product.getProfileImageUrl(), + product.getDetailImageUrl(), + product.getDescription(), + product.getOriginalPrice(), + product.getSalePrice(), + product.getProductTags().stream().map(ProductTag::getName).toList(), + product.getProductGenders().stream().map(ProductGender::getGender).toList(), + product.getProductDateTimes().stream().map(ProductDateTime::getDateTime).toList(), + product.getCreatorId(), + product.getLastModifierId() + ); + } +} diff --git a/src/main/java/kr/mayb/dto/ProductRegistrationRequest.java b/src/main/java/kr/mayb/dto/ProductRegistrationRequest.java new file mode 100644 index 0000000..cde46b1 --- /dev/null +++ b/src/main/java/kr/mayb/dto/ProductRegistrationRequest.java @@ -0,0 +1,33 @@ +package kr.mayb.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.Size; + +import java.time.LocalDateTime; +import java.util.List; + +public record ProductRegistrationRequest( + @NotBlank + String name, + + int originalPrice, + + int salePrice, + + @NotBlank + String description, + + @NotEmpty + @Size(min = 1) + List tags, + + @NotEmpty + @Size(min = 1) + List dateTimes, + + @NotEmpty + @Size(min = 1) + List genders +) { +} diff --git a/src/main/java/kr/mayb/facade/ProductFacade.java b/src/main/java/kr/mayb/facade/ProductFacade.java new file mode 100644 index 0000000..954dabf --- /dev/null +++ b/src/main/java/kr/mayb/facade/ProductFacade.java @@ -0,0 +1,38 @@ +package kr.mayb.facade; + +import kr.mayb.data.model.Product; +import kr.mayb.dto.MemberDto; +import kr.mayb.dto.ProductDto; +import kr.mayb.dto.ProductRegistrationRequest; +import kr.mayb.enums.GcsBucketPath; +import kr.mayb.service.ImageService; +import kr.mayb.service.ProductService; +import kr.mayb.util.ContextUtils; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; +import java.util.Objects; + +@Component +@RequiredArgsConstructor +public class ProductFacade { + + private final ImageService imageService; + private final ProductService productService; + + public List registerProduct(MultipartFile profileImage, MultipartFile detailImage, ProductRegistrationRequest request) { + MemberDto admin = ContextUtils.loadMember(); + String profileUrl = imageService.upload(profileImage, GcsBucketPath.PRODUCT_PROFILE); + String detailUrl = imageService.upload(detailImage, GcsBucketPath.PRODUCT_DETAIL); + + List products = productService.registerProduct(request, profileUrl, detailUrl, admin.getMemberId()); + + return products + .stream() + .filter(Objects::nonNull) + .map(ProductDto::of) + .toList(); + } +} diff --git a/src/main/java/kr/mayb/security/PermitAdmin.java b/src/main/java/kr/mayb/security/PermitAdmin.java index ef8c7af..ab44999 100644 --- a/src/main/java/kr/mayb/security/PermitAdmin.java +++ b/src/main/java/kr/mayb/security/PermitAdmin.java @@ -9,7 +9,7 @@ @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) -@PreAuthorize("hasAuthority('ADMIN')") +@PreAuthorize("hasAnyRole('ROLE_ADMIN')") public @interface PermitAdmin { } diff --git a/src/main/java/kr/mayb/service/ProductService.java b/src/main/java/kr/mayb/service/ProductService.java new file mode 100644 index 0000000..a225df3 --- /dev/null +++ b/src/main/java/kr/mayb/service/ProductService.java @@ -0,0 +1,81 @@ +package kr.mayb.service; + +import io.micrometer.common.util.StringUtils; +import jakarta.transaction.Transactional; +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.ProductRepository; +import kr.mayb.dto.ProductRegistrationRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Objects; + +@Service +@RequiredArgsConstructor +public class ProductService { + + private final ProductRepository productRepository; + + @Transactional + public List registerProduct(ProductRegistrationRequest request, String profileUrl, String detailUrl, long creatorId) { + createProduct(request, profileUrl, detailUrl, creatorId); + return productRepository.findAll(); + } + + private void createProduct(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.setProfileImageUrl(profileUrl); + product.setDetailImageUrl(detailUrl); + product.setDescription(request.description()); + product.setCreatorId(creatorId); + product.setLastModifierId(creatorId); + + saveAdditionalInfo(request.tags(), request.dateTimes(), request.genders(), product); + + productRepository.save(product); + } + + private void saveAdditionalInfo(List tags, List dateTimes, List genders, Product product) { + List productTags = tags.stream() + .filter(StringUtils::isNotBlank) + .map(tag -> { + ProductTag productTag = new ProductTag(); + productTag.setName(tag); + productTag.setProduct(product); + return productTag; + }) + .toList(); + + List productDateTimes = dateTimes.stream() + .filter(Objects::nonNull) + .map(dateTime -> { + ProductDateTime productDateTime = new ProductDateTime(); + productDateTime.setDateTime(dateTime); + productDateTime.setProduct(product); + return productDateTime; + }) + .toList(); + + List productGenders = genders.stream() + .filter(StringUtils::isNotBlank) + .map(gender -> { + ProductGender productGender = new ProductGender(); + productGender.setGender(gender); + productGender.setProduct(product); + return productGender; + }) + .toList(); + + product.setProductTags(productTags); + product.setProductDateTimes(productDateTimes); + product.setProductGenders(productGenders); + } +} From 5dcfc7f4b801461fa05e87df3b127dd4f6ec48d0 Mon Sep 17 00:00:00 2001 From: 0AndWild Date: Thu, 8 May 2025 12:05:09 +0900 Subject: [PATCH 3/6] update: update Product register API --- .../kr/mayb/controller/ProductController.java | 12 ++++------ .../kr/mayb/data/model/ProductGender.java | 3 +++ src/main/java/kr/mayb/dto/GenderPrice.java | 14 +++++++++++ src/main/java/kr/mayb/dto/ProductDto.java | 9 ++++--- .../mayb/dto/ProductRegistrationRequest.java | 13 +++++----- .../java/kr/mayb/facade/ProductFacade.java | 14 ++--------- .../java/kr/mayb/service/ProductService.java | 24 +++++++++---------- 7 files changed, 46 insertions(+), 43 deletions(-) create mode 100644 src/main/java/kr/mayb/dto/GenderPrice.java diff --git a/src/main/java/kr/mayb/controller/ProductController.java b/src/main/java/kr/mayb/controller/ProductController.java index 52a3437..982b8fc 100644 --- a/src/main/java/kr/mayb/controller/ProductController.java +++ b/src/main/java/kr/mayb/controller/ProductController.java @@ -2,13 +2,13 @@ import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; import kr.mayb.dto.ProductDto; import kr.mayb.dto.ProductRegistrationRequest; import kr.mayb.facade.ProductFacade; import kr.mayb.security.DenyAll; import kr.mayb.security.PermitAdmin; import kr.mayb.util.response.ApiResponse; -import kr.mayb.util.response.ListResponse; import kr.mayb.util.response.Responses; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -17,8 +17,6 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; -import java.util.List; - @Tag(name = "Product", description = "상품 관련 API") @DenyAll @@ -30,10 +28,10 @@ public class ProductController { @PermitAdmin @PostMapping("/products") - public ResponseEntity>> registerProduct(@RequestPart(value = "profile") MultipartFile profileImage, - @RequestPart(value = "detail") MultipartFile detailImage, - @RequestPart(value = "product") ProductRegistrationRequest request) { - List response = productFacade.registerProduct(profileImage, detailImage, request); + public ResponseEntity> registerProduct(@RequestPart(value = "profile") MultipartFile profileImage, + @RequestPart(value = "detail") MultipartFile detailImage, + @RequestPart(value = "product") @Valid ProductRegistrationRequest request) { + ProductDto response = productFacade.registerProduct(profileImage, detailImage, request); return Responses.ok(response); } } diff --git a/src/main/java/kr/mayb/data/model/ProductGender.java b/src/main/java/kr/mayb/data/model/ProductGender.java index 0cda298..26193c4 100644 --- a/src/main/java/kr/mayb/data/model/ProductGender.java +++ b/src/main/java/kr/mayb/data/model/ProductGender.java @@ -17,6 +17,9 @@ public class ProductGender extends BaseEntity { @Column(nullable = false) private String gender; + @Column(nullable = false) + private int price; + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "product_id", nullable = false) private Product product; diff --git a/src/main/java/kr/mayb/dto/GenderPrice.java b/src/main/java/kr/mayb/dto/GenderPrice.java new file mode 100644 index 0000000..0e72ee9 --- /dev/null +++ b/src/main/java/kr/mayb/dto/GenderPrice.java @@ -0,0 +1,14 @@ +package kr.mayb.dto; + +import jakarta.validation.constraints.NotBlank; +import kr.mayb.data.model.ProductGender; + +public record GenderPrice( + @NotBlank + String gender, + int price +) { + public static GenderPrice of(ProductGender gender) { + return new GenderPrice(gender.getGender(), gender.getPrice()); + } +} diff --git a/src/main/java/kr/mayb/dto/ProductDto.java b/src/main/java/kr/mayb/dto/ProductDto.java index df9b54f..4b94c7d 100644 --- a/src/main/java/kr/mayb/dto/ProductDto.java +++ b/src/main/java/kr/mayb/dto/ProductDto.java @@ -2,7 +2,6 @@ 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 java.time.LocalDateTime; @@ -19,9 +18,9 @@ public record ProductDto( int originalPrice, int salePrice, - List productTags, - List productGenders, - List productDateTimes, + List tags, + List genderPrices, + List dateTimes, long creatorId, long lastModifierId @@ -36,7 +35,7 @@ public static ProductDto of(Product product) { product.getOriginalPrice(), product.getSalePrice(), product.getProductTags().stream().map(ProductTag::getName).toList(), - product.getProductGenders().stream().map(ProductGender::getGender).toList(), + product.getProductGenders().stream().map(GenderPrice::of).toList(), product.getProductDateTimes().stream().map(ProductDateTime::getDateTime).toList(), product.getCreatorId(), product.getLastModifierId() diff --git a/src/main/java/kr/mayb/dto/ProductRegistrationRequest.java b/src/main/java/kr/mayb/dto/ProductRegistrationRequest.java index cde46b1..becc997 100644 --- a/src/main/java/kr/mayb/dto/ProductRegistrationRequest.java +++ b/src/main/java/kr/mayb/dto/ProductRegistrationRequest.java @@ -1,7 +1,8 @@ package kr.mayb.dto; +import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; import java.time.LocalDateTime; @@ -18,16 +19,16 @@ public record ProductRegistrationRequest( @NotBlank String description, - @NotEmpty + @NotNull @Size(min = 1) List tags, - @NotEmpty + @NotNull @Size(min = 1) List dateTimes, - @NotEmpty - @Size(min = 1) - List genders + @Valid + @NotNull + List genderPrices ) { } diff --git a/src/main/java/kr/mayb/facade/ProductFacade.java b/src/main/java/kr/mayb/facade/ProductFacade.java index 954dabf..5426ec9 100644 --- a/src/main/java/kr/mayb/facade/ProductFacade.java +++ b/src/main/java/kr/mayb/facade/ProductFacade.java @@ -1,6 +1,5 @@ package kr.mayb.facade; -import kr.mayb.data.model.Product; import kr.mayb.dto.MemberDto; import kr.mayb.dto.ProductDto; import kr.mayb.dto.ProductRegistrationRequest; @@ -12,9 +11,6 @@ import org.springframework.stereotype.Component; import org.springframework.web.multipart.MultipartFile; -import java.util.List; -import java.util.Objects; - @Component @RequiredArgsConstructor public class ProductFacade { @@ -22,17 +18,11 @@ public class ProductFacade { private final ImageService imageService; private final ProductService productService; - public List registerProduct(MultipartFile profileImage, MultipartFile detailImage, ProductRegistrationRequest request) { + public ProductDto registerProduct(MultipartFile profileImage, MultipartFile detailImage, ProductRegistrationRequest request) { MemberDto admin = ContextUtils.loadMember(); String profileUrl = imageService.upload(profileImage, GcsBucketPath.PRODUCT_PROFILE); String detailUrl = imageService.upload(detailImage, GcsBucketPath.PRODUCT_DETAIL); - List products = productService.registerProduct(request, profileUrl, detailUrl, admin.getMemberId()); - - return products - .stream() - .filter(Objects::nonNull) - .map(ProductDto::of) - .toList(); + return productService.registerProduct(request, profileUrl, detailUrl, admin.getMemberId()); } } diff --git a/src/main/java/kr/mayb/service/ProductService.java b/src/main/java/kr/mayb/service/ProductService.java index a225df3..0f7026d 100644 --- a/src/main/java/kr/mayb/service/ProductService.java +++ b/src/main/java/kr/mayb/service/ProductService.java @@ -7,6 +7,8 @@ import kr.mayb.data.model.ProductGender; import kr.mayb.data.model.ProductTag; import kr.mayb.data.repository.ProductRepository; +import kr.mayb.dto.GenderPrice; +import kr.mayb.dto.ProductDto; import kr.mayb.dto.ProductRegistrationRequest; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -22,12 +24,7 @@ public class ProductService { private final ProductRepository productRepository; @Transactional - public List registerProduct(ProductRegistrationRequest request, String profileUrl, String detailUrl, long creatorId) { - createProduct(request, profileUrl, detailUrl, creatorId); - return productRepository.findAll(); - } - - private void createProduct(ProductRegistrationRequest request, String profileUrl, String detailUrl, long creatorId) { + public ProductDto registerProduct(ProductRegistrationRequest request, String profileUrl, String detailUrl, long creatorId) { Product product = new Product(); product.setName(request.name()); product.setOriginalPrice(request.originalPrice()); @@ -38,12 +35,13 @@ private void createProduct(ProductRegistrationRequest request, String profileUrl product.setCreatorId(creatorId); product.setLastModifierId(creatorId); - saveAdditionalInfo(request.tags(), request.dateTimes(), request.genders(), product); + saveAdditionalInfo(request.tags(), request.dateTimes(), request.genderPrices(), product); - productRepository.save(product); + Product saved = productRepository.save(product); + return ProductDto.of(saved); } - private void saveAdditionalInfo(List tags, List dateTimes, List genders, Product product) { + private void saveAdditionalInfo(List tags, List dateTimes, List genderPrices, Product product) { List productTags = tags.stream() .filter(StringUtils::isNotBlank) .map(tag -> { @@ -64,11 +62,11 @@ private void saveAdditionalInfo(List tags, List dateTimes }) .toList(); - List productGenders = genders.stream() - .filter(StringUtils::isNotBlank) - .map(gender -> { + List productGenders = genderPrices.stream() + .map(genderPrice -> { ProductGender productGender = new ProductGender(); - productGender.setGender(gender); + productGender.setGender(genderPrice.gender()); + productGender.setPrice(genderPrice.price()); productGender.setProduct(product); return productGender; }) From 107c42fa3839c9eafe4d5612020da9011acd95b4 Mon Sep 17 00:00:00 2001 From: 0AndWild Date: Thu, 8 May 2025 19:24:18 +0900 Subject: [PATCH 4/6] feat: add getProducts API --- .../kr/mayb/controller/ProductController.java | 15 +++++++++++++ src/main/java/kr/mayb/data/model/Product.java | 5 +++++ src/main/java/kr/mayb/dto/ProductDto.java | 8 +++++-- .../java/kr/mayb/enums/ProductStatus.java | 6 ++++++ .../java/kr/mayb/facade/ProductFacade.java | 14 +++++++++++++ .../java/kr/mayb/service/ProductService.java | 21 +++++++++++++++++++ 6 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 src/main/java/kr/mayb/enums/ProductStatus.java diff --git a/src/main/java/kr/mayb/controller/ProductController.java b/src/main/java/kr/mayb/controller/ProductController.java index 982b8fc..6565c9a 100644 --- a/src/main/java/kr/mayb/controller/ProductController.java +++ b/src/main/java/kr/mayb/controller/ProductController.java @@ -1,6 +1,7 @@ package kr.mayb.controller; +import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import kr.mayb.dto.ProductDto; @@ -8,15 +9,20 @@ import kr.mayb.facade.ProductFacade; import kr.mayb.security.DenyAll; import kr.mayb.security.PermitAdmin; +import kr.mayb.security.PermitAll; import kr.mayb.util.response.ApiResponse; +import kr.mayb.util.response.ListResponse; import kr.mayb.util.response.Responses; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; +import java.util.List; + @Tag(name = "Product", description = "상품 관련 API") @DenyAll @@ -26,6 +32,7 @@ public class ProductController { private final ProductFacade productFacade; + @Operation(summary = "제품 등록(관리자)") @PermitAdmin @PostMapping("/products") public ResponseEntity> registerProduct(@RequestPart(value = "profile") MultipartFile profileImage, @@ -34,4 +41,12 @@ public ResponseEntity> registerProduct(@RequestPart(valu ProductDto response = productFacade.registerProduct(profileImage, detailImage, request); return Responses.ok(response); } + + @Operation(summary = "상품 전체 조회", description = "관지자는 모든상품, 일반 사용자는 활성화 상품만 조회 가능") + @PermitAll + @GetMapping("/products") + public ResponseEntity>> getProducts() { + List response = productFacade.getProducts(); + return Responses.ok(response); + } } diff --git a/src/main/java/kr/mayb/data/model/Product.java b/src/main/java/kr/mayb/data/model/Product.java index 131d44b..2438fe4 100644 --- a/src/main/java/kr/mayb/data/model/Product.java +++ b/src/main/java/kr/mayb/data/model/Product.java @@ -1,6 +1,7 @@ package kr.mayb.data.model; import jakarta.persistence.*; +import kr.mayb.enums.ProductStatus; import lombok.Getter; import lombok.Setter; import org.hibernate.annotations.BatchSize; @@ -43,6 +44,10 @@ public class Product extends BaseEntity { @Column private long lastModifierId; + @Column + @Enumerated(EnumType.STRING) + private ProductStatus status; + @BatchSize(size = 20) @OneToMany(mappedBy = "product", cascade = CascadeType.ALL) private List productTags = new ArrayList<>(); diff --git a/src/main/java/kr/mayb/dto/ProductDto.java b/src/main/java/kr/mayb/dto/ProductDto.java index 4b94c7d..002d213 100644 --- a/src/main/java/kr/mayb/dto/ProductDto.java +++ b/src/main/java/kr/mayb/dto/ProductDto.java @@ -3,6 +3,7 @@ import kr.mayb.data.model.Product; import kr.mayb.data.model.ProductDateTime; import kr.mayb.data.model.ProductTag; +import kr.mayb.enums.ProductStatus; import java.time.LocalDateTime; import java.util.List; @@ -23,7 +24,9 @@ public record ProductDto( List dateTimes, long creatorId, - long lastModifierId + long lastModifierId, + + ProductStatus status ) { public static ProductDto of(Product product) { return new ProductDto( @@ -38,7 +41,8 @@ public static ProductDto of(Product product) { product.getProductGenders().stream().map(GenderPrice::of).toList(), product.getProductDateTimes().stream().map(ProductDateTime::getDateTime).toList(), product.getCreatorId(), - product.getLastModifierId() + product.getLastModifierId(), + product.getStatus() ); } } diff --git a/src/main/java/kr/mayb/enums/ProductStatus.java b/src/main/java/kr/mayb/enums/ProductStatus.java new file mode 100644 index 0000000..a2680f2 --- /dev/null +++ b/src/main/java/kr/mayb/enums/ProductStatus.java @@ -0,0 +1,6 @@ +package kr.mayb.enums; + +public enum ProductStatus { + ACTIVE, + INACTIVE +} diff --git a/src/main/java/kr/mayb/facade/ProductFacade.java b/src/main/java/kr/mayb/facade/ProductFacade.java index 5426ec9..fc61d79 100644 --- a/src/main/java/kr/mayb/facade/ProductFacade.java +++ b/src/main/java/kr/mayb/facade/ProductFacade.java @@ -3,6 +3,7 @@ import kr.mayb.dto.MemberDto; import kr.mayb.dto.ProductDto; import kr.mayb.dto.ProductRegistrationRequest; +import kr.mayb.enums.AuthorityName; import kr.mayb.enums.GcsBucketPath; import kr.mayb.service.ImageService; import kr.mayb.service.ProductService; @@ -11,6 +12,9 @@ import org.springframework.stereotype.Component; import org.springframework.web.multipart.MultipartFile; +import java.util.Collection; +import java.util.List; + @Component @RequiredArgsConstructor public class ProductFacade { @@ -25,4 +29,14 @@ public ProductDto registerProduct(MultipartFile profileImage, MultipartFile deta return productService.registerProduct(request, profileUrl, detailUrl, admin.getMemberId()); } + + public List getProducts() { + boolean isAdmin = ContextUtils.getCurrentMember() + .map(MemberDto::getAuthorities) + .stream() + .flatMap(Collection::stream) + .anyMatch(name -> name == AuthorityName.ROLE_ADMIN); + + return productService.getProducts(isAdmin); + } } diff --git a/src/main/java/kr/mayb/service/ProductService.java b/src/main/java/kr/mayb/service/ProductService.java index 0f7026d..b67f837 100644 --- a/src/main/java/kr/mayb/service/ProductService.java +++ b/src/main/java/kr/mayb/service/ProductService.java @@ -10,12 +10,14 @@ import kr.mayb.dto.GenderPrice; import kr.mayb.dto.ProductDto; import kr.mayb.dto.ProductRegistrationRequest; +import kr.mayb.enums.ProductStatus; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import java.time.LocalDateTime; import java.util.List; import java.util.Objects; +import java.util.stream.Stream; @Service @RequiredArgsConstructor @@ -34,6 +36,7 @@ public ProductDto registerProduct(ProductRegistrationRequest request, String pro product.setDescription(request.description()); product.setCreatorId(creatorId); product.setLastModifierId(creatorId); + product.setStatus(ProductStatus.ACTIVE); saveAdditionalInfo(request.tags(), request.dateTimes(), request.genderPrices(), product); @@ -76,4 +79,22 @@ private void saveAdditionalInfo(List tags, List dateTimes product.setProductDateTimes(productDateTimes); product.setProductGenders(productGenders); } + + public List getProducts(boolean isAdmin) { + List products = productRepository.findAll(); + Stream stream = products + .stream() + .filter(Objects::nonNull); + + if (isAdmin) { + return stream + .map(ProductDto::of) + .toList(); + } + + return stream + .filter(product -> product.getStatus() == ProductStatus.ACTIVE) + .map(ProductDto::of) + .toList(); + } } From f692ae60e28897699d23035e6f0ca78961aa6f27 Mon Sep 17 00:00:00 2001 From: 0AndWild Date: Thu, 8 May 2025 21:42:27 +0900 Subject: [PATCH 5/6] feat: add Product update, delete, hide APIs --- .../kr/mayb/controller/ProductController.java | 33 ++++++++- .../repository/ProductDateTimeRepository.java | 2 + .../repository/ProductGenderRepository.java | 2 + .../data/repository/ProductTagRepository.java | 3 + src/main/java/kr/mayb/dto/DateTimeInfo.java | 14 ++++ src/main/java/kr/mayb/dto/ProductDto.java | 11 +-- .../kr/mayb/dto/ProductUpdateRequest.java | 34 +++++++++ src/main/java/kr/mayb/dto/TagInfo.java | 12 +++ .../java/kr/mayb/facade/ProductFacade.java | 22 ++++++ .../java/kr/mayb/service/ProductService.java | 73 ++++++++++++++++++- .../java/kr/mayb/util/response/Responses.java | 1 - 11 files changed, 192 insertions(+), 15 deletions(-) create mode 100644 src/main/java/kr/mayb/dto/DateTimeInfo.java create mode 100644 src/main/java/kr/mayb/dto/ProductUpdateRequest.java create mode 100644 src/main/java/kr/mayb/dto/TagInfo.java diff --git a/src/main/java/kr/mayb/controller/ProductController.java b/src/main/java/kr/mayb/controller/ProductController.java index 6565c9a..1604810 100644 --- a/src/main/java/kr/mayb/controller/ProductController.java +++ b/src/main/java/kr/mayb/controller/ProductController.java @@ -6,6 +6,7 @@ import jakarta.validation.Valid; import kr.mayb.dto.ProductDto; import kr.mayb.dto.ProductRegistrationRequest; +import kr.mayb.dto.ProductUpdateRequest; import kr.mayb.facade.ProductFacade; import kr.mayb.security.DenyAll; import kr.mayb.security.PermitAdmin; @@ -15,10 +16,7 @@ import kr.mayb.util.response.Responses; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestPart; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import java.util.List; @@ -49,4 +47,31 @@ public ResponseEntity>> getProducts() List response = productFacade.getProducts(); return Responses.ok(response); } + + @Operation(summary = "상품 수정") + @PermitAdmin + @PutMapping("/products/{productId}") + public ResponseEntity> updateProduct(@PathVariable long productId, + @RequestPart(value = "profile", required = false) MultipartFile profileImage, + @RequestPart(value = "detail", required = false) MultipartFile detailImage, + @RequestPart(value = "product") @Valid ProductUpdateRequest request) { + ProductDto response = productFacade.updateProduct(productId, profileImage, detailImage, request); + return Responses.ok(response); + } + + @Operation(summary = "상품 삭제") + @PermitAdmin + @DeleteMapping("/products/{productId}") + public ResponseEntity deleteProduct(@PathVariable long productId) { + productFacade.deleteProduct(productId); + return Responses.noContent(); + } + + @Operation(summary = "상품 목록에서 숨기기 확성화, 비활성화") + @PermitAdmin + @PutMapping("/products/{productId}/{status}") + public ResponseEntity changeStatus(@PathVariable long productId, @PathVariable boolean status) { + productFacade.changeStatus(productId, status); + return Responses.ok(); + } } diff --git a/src/main/java/kr/mayb/data/repository/ProductDateTimeRepository.java b/src/main/java/kr/mayb/data/repository/ProductDateTimeRepository.java index da70d1e..49aceed 100644 --- a/src/main/java/kr/mayb/data/repository/ProductDateTimeRepository.java +++ b/src/main/java/kr/mayb/data/repository/ProductDateTimeRepository.java @@ -1,7 +1,9 @@ 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/ProductGenderRepository.java b/src/main/java/kr/mayb/data/repository/ProductGenderRepository.java index d18c78d..993f459 100644 --- a/src/main/java/kr/mayb/data/repository/ProductGenderRepository.java +++ b/src/main/java/kr/mayb/data/repository/ProductGenderRepository.java @@ -1,7 +1,9 @@ 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/ProductTagRepository.java b/src/main/java/kr/mayb/data/repository/ProductTagRepository.java index 86e29f8..cb71a96 100644 --- a/src/main/java/kr/mayb/data/repository/ProductTagRepository.java +++ b/src/main/java/kr/mayb/data/repository/ProductTagRepository.java @@ -1,7 +1,10 @@ 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/dto/DateTimeInfo.java b/src/main/java/kr/mayb/dto/DateTimeInfo.java new file mode 100644 index 0000000..2bbf552 --- /dev/null +++ b/src/main/java/kr/mayb/dto/DateTimeInfo.java @@ -0,0 +1,14 @@ +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/ProductDto.java b/src/main/java/kr/mayb/dto/ProductDto.java index 002d213..6818327 100644 --- a/src/main/java/kr/mayb/dto/ProductDto.java +++ b/src/main/java/kr/mayb/dto/ProductDto.java @@ -1,11 +1,8 @@ package kr.mayb.dto; import kr.mayb.data.model.Product; -import kr.mayb.data.model.ProductDateTime; -import kr.mayb.data.model.ProductTag; import kr.mayb.enums.ProductStatus; -import java.time.LocalDateTime; import java.util.List; public record ProductDto( @@ -19,9 +16,9 @@ public record ProductDto( int originalPrice, int salePrice, - List tags, + List tags, List genderPrices, - List dateTimes, + List dateTimes, long creatorId, long lastModifierId, @@ -37,9 +34,9 @@ public static ProductDto of(Product product) { product.getDescription(), product.getOriginalPrice(), product.getSalePrice(), - product.getProductTags().stream().map(ProductTag::getName).toList(), + product.getProductTags().stream().map(TagInfo::of).toList(), product.getProductGenders().stream().map(GenderPrice::of).toList(), - product.getProductDateTimes().stream().map(ProductDateTime::getDateTime).toList(), + product.getProductDateTimes().stream().map(DateTimeInfo::of).toList(), product.getCreatorId(), product.getLastModifierId(), product.getStatus() diff --git a/src/main/java/kr/mayb/dto/ProductUpdateRequest.java b/src/main/java/kr/mayb/dto/ProductUpdateRequest.java new file mode 100644 index 0000000..c1e98dc --- /dev/null +++ b/src/main/java/kr/mayb/dto/ProductUpdateRequest.java @@ -0,0 +1,34 @@ +package kr.mayb.dto; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +import java.time.LocalDateTime; +import java.util.List; + +public record ProductUpdateRequest( + @NotBlank + String name, + + int originalPrice, + + int salePrice, + + @NotBlank + String description, + + @NotNull + @Size(min = 1) + List tags, + + @NotNull + @Size(min = 1) + List dateTimes, + + @Valid + @NotNull + List genderPrices +) { +} diff --git a/src/main/java/kr/mayb/dto/TagInfo.java b/src/main/java/kr/mayb/dto/TagInfo.java new file mode 100644 index 0000000..da342ca --- /dev/null +++ b/src/main/java/kr/mayb/dto/TagInfo.java @@ -0,0 +1,12 @@ +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/facade/ProductFacade.java b/src/main/java/kr/mayb/facade/ProductFacade.java index fc61d79..f249a0d 100644 --- a/src/main/java/kr/mayb/facade/ProductFacade.java +++ b/src/main/java/kr/mayb/facade/ProductFacade.java @@ -3,6 +3,7 @@ import kr.mayb.dto.MemberDto; import kr.mayb.dto.ProductDto; import kr.mayb.dto.ProductRegistrationRequest; +import kr.mayb.dto.ProductUpdateRequest; import kr.mayb.enums.AuthorityName; import kr.mayb.enums.GcsBucketPath; import kr.mayb.service.ImageService; @@ -14,6 +15,7 @@ import java.util.Collection; import java.util.List; +import java.util.Optional; @Component @RequiredArgsConstructor @@ -39,4 +41,24 @@ public List getProducts() { return productService.getProducts(isAdmin); } + + public ProductDto updateProduct(long productId, MultipartFile profileImage, MultipartFile detailImage, ProductUpdateRequest request) { + MemberDto admin = ContextUtils.loadMember(); + + Optional profileUrl = Optional.ofNullable(profileImage) + .map(image -> imageService.upload(image, GcsBucketPath.PRODUCT_PROFILE)); + Optional detailUrl = Optional.ofNullable(detailImage) + .map(image -> imageService.upload(image, GcsBucketPath.PRODUCT_DETAIL)); + + return productService.updateProduct(productId, profileUrl, detailUrl, request, admin.getMemberId()); + } + + public void deleteProduct(long productId) { + productService.delete(productId); + } + + public void changeStatus(long productId, boolean active) { + MemberDto admin = ContextUtils.loadMember(); + productService.changeStatus(productId, active, admin.getMemberId()); + } } diff --git a/src/main/java/kr/mayb/service/ProductService.java b/src/main/java/kr/mayb/service/ProductService.java index b67f837..852ff9d 100644 --- a/src/main/java/kr/mayb/service/ProductService.java +++ b/src/main/java/kr/mayb/service/ProductService.java @@ -6,10 +6,15 @@ 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.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.enums.GcsBucketPath; import kr.mayb.enums.ProductStatus; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -17,13 +22,20 @@ import java.time.LocalDateTime; import java.util.List; import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; import java.util.stream.Stream; @Service @RequiredArgsConstructor public class ProductService { + private final ImageService imageService; + private final ProductRepository productRepository; + private final ProductTagRepository productTagRepository; + private final ProductGenderRepository productGenderRepository; + private final ProductDateTimeRepository productDateTimeRepository; @Transactional public ProductDto registerProduct(ProductRegistrationRequest request, String profileUrl, String detailUrl, long creatorId) { @@ -53,7 +65,7 @@ private void saveAdditionalInfo(List tags, List dateTimes productTag.setProduct(product); return productTag; }) - .toList(); + .collect(Collectors.toList()); List productDateTimes = dateTimes.stream() .filter(Objects::nonNull) @@ -63,7 +75,7 @@ private void saveAdditionalInfo(List tags, List dateTimes productDateTime.setProduct(product); return productDateTime; }) - .toList(); + .collect(Collectors.toList()); List productGenders = genderPrices.stream() .map(genderPrice -> { @@ -73,7 +85,7 @@ private void saveAdditionalInfo(List tags, List dateTimes productGender.setProduct(product); return productGender; }) - .toList(); + .collect(Collectors.toList()); product.setProductTags(productTags); product.setProductDateTimes(productDateTimes); @@ -97,4 +109,59 @@ public List getProducts(boolean isAdmin) { .map(ProductDto::of) .toList(); } + + @Transactional + public ProductDto updateProduct(long productId, Optional profileUrl, Optional detailUrl, ProductUpdateRequest request, long modifierId) { + Product product = productRepository.findById(productId) + .orElseThrow(() -> new IllegalArgumentException("상품을 찾을 수 없습니다.: " + productId)); + + product.setName(request.name()); + product.setOriginalPrice(request.originalPrice()); + product.setSalePrice(request.salePrice()); + product.setDescription(request.description()); + + updateProductImage(profileUrl, detailUrl, product); + clearAndUpdateAdditionalInfo(request, product); + + product.setLastModifierId(modifierId); + Product updated = productRepository.save(product); + return ProductDto.of(updated); + } + + @Transactional + public void delete(long productId) { + productRepository.deleteById(productId); + } + + @Transactional + public void changeStatus(long productId, boolean active, long memberId) { + Product product = productRepository.findById(productId) + .orElseThrow(() -> new IllegalArgumentException("상품을 찾을 수 없습니다.: " + productId)); + + if (active) { + product.setStatus(ProductStatus.ACTIVE); + } else { + product.setStatus(ProductStatus.INACTIVE); + } + + product.setLastModifierId(memberId); + } + + private void updateProductImage(Optional profileUrl, Optional detailUrl, Product product) { + profileUrl.ifPresent(url -> { + imageService.delete(product.getProfileImageUrl(), GcsBucketPath.PRODUCT_PROFILE); + product.setProfileImageUrl(url); + }); + detailUrl.ifPresent(url -> { + imageService.delete(product.getDetailImageUrl(), GcsBucketPath.PRODUCT_DETAIL); + product.setDetailImageUrl(url); + }); + } + + private void clearAndUpdateAdditionalInfo(ProductUpdateRequest request, Product product) { + productTagRepository.deleteByProduct(product); + productGenderRepository.deleteByProduct(product); + productDateTimeRepository.deleteByProduct(product); + saveAdditionalInfo(request.tags(), request.dateTimes(), request.genderPrices(), product); + } } diff --git a/src/main/java/kr/mayb/util/response/Responses.java b/src/main/java/kr/mayb/util/response/Responses.java index 14ef5f2..0ef9121 100644 --- a/src/main/java/kr/mayb/util/response/Responses.java +++ b/src/main/java/kr/mayb/util/response/Responses.java @@ -33,5 +33,4 @@ public static ResponseEntity ok() { public static ResponseEntity noContent() { return ResponseEntity.noContent().build(); } - } From b832da316f2749f4d201d7757e5d7fae4ec0ce37 Mon Sep 17 00:00:00 2001 From: 0AndWild Date: Fri, 9 May 2025 12:10:55 +0900 Subject: [PATCH 6/6] feat: add get product API --- .../kr/mayb/controller/ProductController.java | 8 ++ src/main/java/kr/mayb/dto/ProductDto.java | 83 +++++++++++-------- .../java/kr/mayb/facade/ProductFacade.java | 10 +++ .../java/kr/mayb/service/ProductService.java | 27 ++++-- 4 files changed, 89 insertions(+), 39 deletions(-) diff --git a/src/main/java/kr/mayb/controller/ProductController.java b/src/main/java/kr/mayb/controller/ProductController.java index 1604810..467606c 100644 --- a/src/main/java/kr/mayb/controller/ProductController.java +++ b/src/main/java/kr/mayb/controller/ProductController.java @@ -48,6 +48,14 @@ public ResponseEntity>> getProducts() return Responses.ok(response); } + @Operation(summary = "상품 단건 조회") + @PermitAll + @GetMapping("/products/{productId}") + public ResponseEntity> getProduct(@PathVariable long productId) { + ProductDto response = productFacade.getProduct(productId); + return Responses.ok(response); + } + @Operation(summary = "상품 수정") @PermitAdmin @PutMapping("/products/{productId}") diff --git a/src/main/java/kr/mayb/dto/ProductDto.java b/src/main/java/kr/mayb/dto/ProductDto.java index 6818327..069b08f 100644 --- a/src/main/java/kr/mayb/dto/ProductDto.java +++ b/src/main/java/kr/mayb/dto/ProductDto.java @@ -2,44 +2,59 @@ import kr.mayb.data.model.Product; import kr.mayb.enums.ProductStatus; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; import java.util.List; -public record ProductDto( - long productId, - - String name, - String profileImageUrl, - String detailImageUrl, - String description, - - int originalPrice, - int salePrice, - - List tags, - List genderPrices, - List dateTimes, - - long creatorId, - long lastModifierId, +@Getter +@Setter +@AllArgsConstructor +public class ProductDto { + private final long productId; + + private final String name; + private final String profileImageUrl; + private final String detailImageUrl; + private final String description; + + private final int originalPrice; + private final int salePrice; + + private final List tags; + private final List genderPrices; + private final List dateTimes; + + private final ProductStatus status; + + private Long creatorId; + private Long lastModifierId; + + public ProductDto(Product product, boolean isAdmin) { + this.productId = product.getId(); + this.name = product.getName(); + this.profileImageUrl = product.getProfileImageUrl(); + 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.status = product.getStatus(); + + if (isAdmin) { + this.creatorId = product.getCreatorId(); + this.lastModifierId = product.getLastModifierId(); + } + } - ProductStatus status -) { public static ProductDto of(Product product) { - return new ProductDto( - product.getId(), - product.getName(), - product.getProfileImageUrl(), - product.getDetailImageUrl(), - product.getDescription(), - product.getOriginalPrice(), - product.getSalePrice(), - product.getProductTags().stream().map(TagInfo::of).toList(), - product.getProductGenders().stream().map(GenderPrice::of).toList(), - product.getProductDateTimes().stream().map(DateTimeInfo::of).toList(), - product.getCreatorId(), - product.getLastModifierId(), - product.getStatus() - ); + return new ProductDto(product, false); + } + + public static ProductDto of(Product product, boolean isAdmin) { + return new ProductDto(product, isAdmin); } } diff --git a/src/main/java/kr/mayb/facade/ProductFacade.java b/src/main/java/kr/mayb/facade/ProductFacade.java index f249a0d..e8f0dfd 100644 --- a/src/main/java/kr/mayb/facade/ProductFacade.java +++ b/src/main/java/kr/mayb/facade/ProductFacade.java @@ -42,6 +42,16 @@ public List getProducts() { return productService.getProducts(isAdmin); } + public ProductDto getProduct(long productId) { + boolean isAdmin = ContextUtils.getCurrentMember() + .map(MemberDto::getAuthorities) + .stream() + .flatMap(Collection::stream) + .anyMatch(name -> name == AuthorityName.ROLE_ADMIN); + + return productService.getProduct(productId, isAdmin); + } + public ProductDto updateProduct(long productId, MultipartFile profileImage, MultipartFile detailImage, ProductUpdateRequest request) { MemberDto admin = ContextUtils.loadMember(); diff --git a/src/main/java/kr/mayb/service/ProductService.java b/src/main/java/kr/mayb/service/ProductService.java index 852ff9d..43cac4a 100644 --- a/src/main/java/kr/mayb/service/ProductService.java +++ b/src/main/java/kr/mayb/service/ProductService.java @@ -16,7 +16,9 @@ import kr.mayb.dto.ProductUpdateRequest; import kr.mayb.enums.GcsBucketPath; import kr.mayb.enums.ProductStatus; +import kr.mayb.error.ResourceNotFoundException; import lombok.RequiredArgsConstructor; +import org.springframework.security.access.AccessDeniedException; import org.springframework.stereotype.Service; import java.time.LocalDateTime; @@ -53,7 +55,7 @@ public ProductDto registerProduct(ProductRegistrationRequest request, String pro saveAdditionalInfo(request.tags(), request.dateTimes(), request.genderPrices(), product); Product saved = productRepository.save(product); - return ProductDto.of(saved); + return ProductDto.of(saved, true); } private void saveAdditionalInfo(List tags, List dateTimes, List genderPrices, Product product) { @@ -100,7 +102,7 @@ public List getProducts(boolean isAdmin) { if (isAdmin) { return stream - .map(ProductDto::of) + .map(product -> ProductDto.of(product, true)) .toList(); } @@ -110,10 +112,25 @@ public List getProducts(boolean isAdmin) { .toList(); } + public ProductDto getProduct(long productId, boolean isAdmin) { + Product product = productRepository.findById(productId) + .orElseThrow(() -> new ResourceNotFoundException("Product not found.: " + productId)); + + if (isAdmin) { + return ProductDto.of(product, true); + } + + if (product.getStatus() == ProductStatus.INACTIVE) { + throw new AccessDeniedException("Product is inactive.: " + productId); + } + + return ProductDto.of(product); + } + @Transactional public ProductDto updateProduct(long productId, Optional profileUrl, Optional detailUrl, ProductUpdateRequest request, long modifierId) { Product product = productRepository.findById(productId) - .orElseThrow(() -> new IllegalArgumentException("상품을 찾을 수 없습니다.: " + productId)); + .orElseThrow(() -> new ResourceNotFoundException("Product not found.: " + productId)); product.setName(request.name()); product.setOriginalPrice(request.originalPrice()); @@ -125,7 +142,7 @@ public ProductDto updateProduct(long productId, Optional profileUrl, Opt product.setLastModifierId(modifierId); Product updated = productRepository.save(product); - return ProductDto.of(updated); + return ProductDto.of(updated, true); } @Transactional @@ -136,7 +153,7 @@ public void delete(long productId) { @Transactional public void changeStatus(long productId, boolean active, long memberId) { Product product = productRepository.findById(productId) - .orElseThrow(() -> new IllegalArgumentException("상품을 찾을 수 없습니다.: " + productId)); + .orElseThrow(() -> new ResourceNotFoundException("Product not found.: " + productId)); if (active) { product.setStatus(ProductStatus.ACTIVE);