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..467606c --- /dev/null +++ b/src/main/java/kr/mayb/controller/ProductController.java @@ -0,0 +1,85 @@ +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; +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; +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.*; +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; + + @Operation(summary = "제품 등록(관리자)") + @PermitAdmin + @PostMapping("/products") + 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); + } + + @Operation(summary = "상품 전체 조회", description = "관지자는 모든상품, 일반 사용자는 활성화 상품만 조회 가능") + @PermitAll + @GetMapping("/products") + public ResponseEntity>> getProducts() { + List response = productFacade.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}") + 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/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..2438fe4 --- /dev/null +++ b/src/main/java/kr/mayb/data/model/Product.java @@ -0,0 +1,62 @@ +package kr.mayb.data.model; + +import jakarta.persistence.*; +import kr.mayb.enums.ProductStatus; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.BatchSize; + +import java.util.ArrayList; +import java.util.List; + +@Getter +@Setter +@Table(schema = "mayb") +@Entity +public class Product extends BaseEntity { + + @Id + @Column(name = "product_id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @Column + private String name; + + @Column + private int originalPrice; + + @Column + private int salePrice; + + @Column + private String profileImageUrl; + + @Column + private String detailImageUrl; + + @Column + private String description; + + @Column + private long creatorId; + + @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<>(); + + @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 new file mode 100644 index 0000000..2da83f1 --- /dev/null +++ b/src/main/java/kr/mayb/data/model/ProductDateTime.java @@ -0,0 +1,25 @@ +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(nullable = false) + private LocalDateTime dateTime; + + @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 new file mode 100644 index 0000000..26193c4 --- /dev/null +++ b/src/main/java/kr/mayb/data/model/ProductGender.java @@ -0,0 +1,26 @@ +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(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/data/model/ProductTag.java b/src/main/java/kr/mayb/data/model/ProductTag.java new file mode 100644 index 0000000..cea4c8d --- /dev/null +++ b/src/main/java/kr/mayb/data/model/ProductTag.java @@ -0,0 +1,23 @@ +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/ProductDateTimeRepository.java b/src/main/java/kr/mayb/data/repository/ProductDateTimeRepository.java new file mode 100644 index 0000000..49aceed --- /dev/null +++ b/src/main/java/kr/mayb/data/repository/ProductDateTimeRepository.java @@ -0,0 +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 new file mode 100644 index 0000000..993f459 --- /dev/null +++ b/src/main/java/kr/mayb/data/repository/ProductGenderRepository.java @@ -0,0 +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/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/data/repository/ProductTagRepository.java b/src/main/java/kr/mayb/data/repository/ProductTagRepository.java new file mode 100644 index 0000000..cb71a96 --- /dev/null +++ b/src/main/java/kr/mayb/data/repository/ProductTagRepository.java @@ -0,0 +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/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/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..069b08f --- /dev/null +++ b/src/main/java/kr/mayb/dto/ProductDto.java @@ -0,0 +1,60 @@ +package kr.mayb.dto; + +import kr.mayb.data.model.Product; +import kr.mayb.enums.ProductStatus; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +@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(); + } + } + + public static ProductDto of(Product product) { + 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/dto/ProductRegistrationRequest.java b/src/main/java/kr/mayb/dto/ProductRegistrationRequest.java new file mode 100644 index 0000000..becc997 --- /dev/null +++ b/src/main/java/kr/mayb/dto/ProductRegistrationRequest.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 ProductRegistrationRequest( + @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/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/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 new file mode 100644 index 0000000..e8f0dfd --- /dev/null +++ b/src/main/java/kr/mayb/facade/ProductFacade.java @@ -0,0 +1,74 @@ +package kr.mayb.facade; + +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; +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.Collection; +import java.util.List; +import java.util.Optional; + +@Component +@RequiredArgsConstructor +public class ProductFacade { + + private final ImageService imageService; + private final ProductService productService; + + 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); + + 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); + } + + 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(); + + 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/security/PermitAdmin.java b/src/main/java/kr/mayb/security/PermitAdmin.java new file mode 100644 index 0000000..ab44999 --- /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("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..43cac4a --- /dev/null +++ b/src/main/java/kr/mayb/service/ProductService.java @@ -0,0 +1,184 @@ +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.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 kr.mayb.error.ResourceNotFoundException; +import lombok.RequiredArgsConstructor; +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.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) { + 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); + product.setStatus(ProductStatus.ACTIVE); + + saveAdditionalInfo(request.tags(), request.dateTimes(), 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() + .filter(Objects::nonNull) + .map(dateTime -> { + ProductDateTime productDateTime = new ProductDateTime(); + productDateTime.setDateTime(dateTime); + productDateTime.setProduct(product); + return productDateTime; + }) + .collect(Collectors.toList()); + + List productGenders = genderPrices.stream() + .map(genderPrice -> { + ProductGender productGender = new ProductGender(); + productGender.setGender(genderPrice.gender()); + productGender.setPrice(genderPrice.price()); + productGender.setProduct(product); + return productGender; + }) + .collect(Collectors.toList()); + + product.setProductTags(productTags); + 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(product -> ProductDto.of(product, true)) + .toList(); + } + + return stream + .filter(product -> product.getStatus() == ProductStatus.ACTIVE) + .map(ProductDto::of) + .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 ResourceNotFoundException("Product not found.: " + 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, true); + } + + @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 ResourceNotFoundException("Product not found.: " + 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(); } - }