diff --git a/.github/workflows/deploy-ecs.yml b/.github/workflows/deploy-ecs.yml index 233e8472..2c8c8cca 100644 --- a/.github/workflows/deploy-ecs.yml +++ b/.github/workflows/deploy-ecs.yml @@ -118,6 +118,11 @@ jobs: AWS_SECRET_KEY=${{ secrets.AWS_SECRET_KEY }} IMP_API_KEY=${{ secrets.IMP_API_KEY }} IMP_SECRET_KEY=${{ secrets.IMP_SECRET_KEY }} + KAKAO_CLIENT_ID=${{ secrets.KAKAO_CLIENT_ID }} + KAKAO_CLIENT_SECRET=${{ secrets.KAKAO_CLIENT_SECRET }} + NAVER_OCR_SECRET=${{ secrets.NAVER_OCR_SECRET }} + NAVER_OCR_INVOKE=${{ secrets.NAVER_OCR_INVOKE }} + NAVER_OCR_TEMPLATE=${{ secrets.NAVER_OCR_TEMPLATE }} cache-from: type=gha cache-to: type=gha,mode=min,ignore-error=true diff --git a/Dockerfile b/Dockerfile index 7584f88a..685caf52 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,6 +15,11 @@ ARG AWS_ACCESS_KEY ARG AWS_SECRET_KEY ARG IMP_API_KEY ARG IMP_SECRET_KEY +ARG KAKAO_CLIENT_ID +ARG KAKAO_CLIENT_SECRET +ARG NAVER_OCR_SECRET +ARG NAVER_OCR_INVOKE +ARG NAVER_OCR_TEMPLATE # 환경 변수 설정 ENV SPRING_DATASOURCE_URL=$SPRING_DATASOURCE_URL @@ -31,6 +36,12 @@ ENV AWS_ACCESS_KEY=$AWS_ACCESS_KEY ENV AWS_SECRET_KEY=$AWS_SECRET_KEY ENV IMP_API_KEY=$IMP_API_KEY ENV IMP_SECRET_KEY=$IMP_SECRET_KEY +ENV KAKAO_CLIENT_ID=$KAKAO_CLIENT_ID +ENV KAKAO_CLIENT_SECRET=$KAKAO_CLIENT_SECRET +ENV NAVER_OCR_SECRET=$NAVER_OCR_SECRET +ENV NAVER_OCR_INVOKE=$NAVER_OCR_INVOKE +ENV NAVER_OCR_TEMPLATE=$NAVER_OCR_TEMPLATE + COPY build/libs/*.jar app.jar @@ -49,4 +60,9 @@ ENTRYPOINT ["java", "-Duser.timezone=Asia/Seoul", \ "-Daws.secret=${AWS_SECRET_KEY}", \ "-Dimp.api.key=${IMP_API_KEY}", \ "-Dimp.api.secretKey=${IMP_SECRET_KEY}", \ + "-Dkakao.client.id=${KAKAO_CLIENT_ID}", \ + "-Dkakao.client.secret=${KAKAO_CLIENT_SECRET}", \ + "-Dnaver.ocr.secret=${NAVER_OCR_SECRET}", \ + "-Dnaver.ocr.invoke=${NAVER_OCR_INVOKE}", \ + "-Dnaver.ocr.template=${NAVER_OCR_TEMPLATE}", \ "-jar", "/app.jar"] diff --git a/build.gradle b/build.gradle index 1008b24e..5973267a 100644 --- a/build.gradle +++ b/build.gradle @@ -45,7 +45,7 @@ dependencies { // JSON implementation 'org.json:json:20210307' implementation 'com.google.code.gson:gson:2.10.1' - + implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.2' // JWT implementation 'io.jsonwebtoken:jjwt-api:0.11.5' runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' @@ -66,6 +66,9 @@ dependencies { implementation(platform("software.amazon.awssdk:bom:2.27.21")) implementation("software.amazon.awssdk:s3") + implementation platform("io.awspring.cloud:spring-cloud-aws-dependencies:3.0.1") + implementation 'io.awspring.cloud:spring-cloud-aws-starter-sqs' + // 아임포트 및 아임포트 웹훅 implementation 'com.github.iamport:iamport-rest-client-java:0.2.23' diff --git a/src/main/java/poomasi/domain/aftersales/controller/AfterSalesController.java b/src/main/java/poomasi/domain/aftersales/controller/AfterSalesController.java new file mode 100644 index 00000000..d5c0fe6f --- /dev/null +++ b/src/main/java/poomasi/domain/aftersales/controller/AfterSalesController.java @@ -0,0 +1,61 @@ +package poomasi.domain.aftersales.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.annotation.Secured; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import poomasi.domain.aftersales.dto.cancel.request.FarmCancelRequest; +import poomasi.domain.aftersales.dto.cancel.request.ProductCancelRequest; +import poomasi.domain.aftersales.dto.refund.request.ProductRefundRequest; +import poomasi.domain.aftersales.dto.refund.request.ProductRefundRequestApprovalRequest; +import poomasi.domain.aftersales.service.FarmAfterSalesService; +import poomasi.domain.aftersales.service.ProductAfterSalesService; + +@RestController +@RequestMapping("/api/aftersales") +@RequiredArgsConstructor +public class AfterSalesController { + + private final ProductAfterSalesService productAfterSalesService; + private final FarmAfterSalesService farmAfterSalesService; + + @Secured({"ROLE_CUSTOMER", "ROLE_FARMER"}) + @PostMapping("/products/cancel") + public ResponseEntity productCancel(@RequestBody ProductCancelRequest productCancelRequest) { + return ResponseEntity.ok( + productAfterSalesService.cancel(productCancelRequest) + ); + } + + @Secured({"ROLE_CUSTOMER", "ROLE_FARMER"}) + @PostMapping("/products/refund-request") + public ResponseEntity requestRefund(@RequestBody ProductRefundRequest productRefundRequest) { + return ResponseEntity.ok( + productAfterSalesService. + refund(productRefundRequest) + ); + } + + @Secured({"ROLE_FARMER"}) + @PostMapping("/approve-products-refund") + public ResponseEntity approveRefund(@RequestBody ProductRefundRequestApprovalRequest productRefundRequestApprovalRequest) { + return ResponseEntity.ok( + productAfterSalesService.processRefundApproval(productRefundRequestApprovalRequest) + ); + } + + @Secured({"ROLE_CUSTOMER", "ROLE_FARMER"}) + @PostMapping("/farms/cancel") + public ResponseEntity farmCancel(@RequestBody FarmCancelRequest farmCancelRequest){ + return ResponseEntity.ok( + farmAfterSalesService.cancel(farmCancelRequest) + ); + } + + + + +} diff --git a/src/main/java/poomasi/domain/aftersales/dto/cancel/request/FarmCancelRequest.java b/src/main/java/poomasi/domain/aftersales/dto/cancel/request/FarmCancelRequest.java new file mode 100644 index 00000000..f708ab8d --- /dev/null +++ b/src/main/java/poomasi/domain/aftersales/dto/cancel/request/FarmCancelRequest.java @@ -0,0 +1,4 @@ +package poomasi.domain.aftersales.dto.cancel.request; + +public record FarmCancelRequest(Long reservationId) { +} diff --git a/src/main/java/poomasi/domain/aftersales/dto/cancel/request/ProductCancelRequest.java b/src/main/java/poomasi/domain/aftersales/dto/cancel/request/ProductCancelRequest.java new file mode 100644 index 00000000..95d05ea9 --- /dev/null +++ b/src/main/java/poomasi/domain/aftersales/dto/cancel/request/ProductCancelRequest.java @@ -0,0 +1,4 @@ +package poomasi.domain.aftersales.dto.cancel.request; + +public record ProductCancelRequest(Long orderedProductId) { +} diff --git a/src/main/java/poomasi/domain/aftersales/dto/cancel/response/FarmCancelResponse.java b/src/main/java/poomasi/domain/aftersales/dto/cancel/response/FarmCancelResponse.java new file mode 100644 index 00000000..4720c323 --- /dev/null +++ b/src/main/java/poomasi/domain/aftersales/dto/cancel/response/FarmCancelResponse.java @@ -0,0 +1,10 @@ +package poomasi.domain.aftersales.dto.cancel.response; + +import poomasi.domain.reservation.entity.ReservationStatus; + +import java.math.BigDecimal; + +public record FarmCancelResponse(Long reservationId, + ReservationStatus reservationStatus, + BigDecimal finalCancelAmount) { +} diff --git a/src/main/java/poomasi/domain/aftersales/dto/cancel/response/ProductCancelResponse.java b/src/main/java/poomasi/domain/aftersales/dto/cancel/response/ProductCancelResponse.java new file mode 100644 index 00000000..d7072a2a --- /dev/null +++ b/src/main/java/poomasi/domain/aftersales/dto/cancel/response/ProductCancelResponse.java @@ -0,0 +1,11 @@ +package poomasi.domain.aftersales.dto.cancel.response; + +import poomasi.domain.order.entity.OrderedProductStatus; + +import java.math.BigDecimal; + +public record ProductCancelResponse( + Long orderedProductId, + OrderedProductStatus orderedProductStatus, + BigDecimal finalCancelAmount) { +} diff --git a/src/main/java/poomasi/domain/aftersales/dto/refund/request/ProductRefundRequest.java b/src/main/java/poomasi/domain/aftersales/dto/refund/request/ProductRefundRequest.java new file mode 100644 index 00000000..ba3210e6 --- /dev/null +++ b/src/main/java/poomasi/domain/aftersales/dto/refund/request/ProductRefundRequest.java @@ -0,0 +1,8 @@ +package poomasi.domain.aftersales.dto.refund.request; + +import jakarta.validation.constraints.NotNull; + +public record ProductRefundRequest( + @NotNull Long orderedProductId +) { +} diff --git a/src/main/java/poomasi/domain/aftersales/dto/refund/request/ProductRefundRequestApprovalRequest.java b/src/main/java/poomasi/domain/aftersales/dto/refund/request/ProductRefundRequestApprovalRequest.java new file mode 100644 index 00000000..dcdf28e6 --- /dev/null +++ b/src/main/java/poomasi/domain/aftersales/dto/refund/request/ProductRefundRequestApprovalRequest.java @@ -0,0 +1,4 @@ +package poomasi.domain.aftersales.dto.refund.request; + +public record ProductRefundRequestApprovalRequest(Long productAfterSalesId) { +} diff --git a/src/main/java/poomasi/domain/aftersales/dto/refund/response/ProductRefundRequestApprovalResponse.java b/src/main/java/poomasi/domain/aftersales/dto/refund/response/ProductRefundRequestApprovalResponse.java new file mode 100644 index 00000000..21fbe6b6 --- /dev/null +++ b/src/main/java/poomasi/domain/aftersales/dto/refund/response/ProductRefundRequestApprovalResponse.java @@ -0,0 +1,12 @@ +package poomasi.domain.aftersales.dto.refund.response; + +import poomasi.domain.order.entity.OrderedProductStatus; + +import java.math.BigDecimal; + +public record ProductRefundRequestApprovalResponse( + Long orderedProductId, + OrderedProductStatus orderedProductStatus, + BigDecimal refundAmount +) { +} diff --git a/src/main/java/poomasi/domain/aftersales/dto/refund/response/ProductRefundResponse.java b/src/main/java/poomasi/domain/aftersales/dto/refund/response/ProductRefundResponse.java new file mode 100644 index 00000000..45688a71 --- /dev/null +++ b/src/main/java/poomasi/domain/aftersales/dto/refund/response/ProductRefundResponse.java @@ -0,0 +1,14 @@ +package poomasi.domain.aftersales.dto.refund.response; + +import poomasi.domain.order.entity.OrderedProductStatus; + +import java.math.BigDecimal; + +public record ProductRefundResponse( + Long orderedProductId, + OrderedProductStatus orderedProductStatus, + BigDecimal finalRefundAmount +) { +} + + diff --git a/src/main/java/poomasi/domain/aftersales/entity/AfterSalesType.java b/src/main/java/poomasi/domain/aftersales/entity/AfterSalesType.java new file mode 100644 index 00000000..79594169 --- /dev/null +++ b/src/main/java/poomasi/domain/aftersales/entity/AfterSalesType.java @@ -0,0 +1,8 @@ +package poomasi.domain.aftersales.entity; + +public enum AfterSalesType { + REFUND, + CANCEL +} + + diff --git a/src/main/java/poomasi/domain/aftersales/entity/FarmAfterSales.java b/src/main/java/poomasi/domain/aftersales/entity/FarmAfterSales.java new file mode 100644 index 00000000..0b7318cd --- /dev/null +++ b/src/main/java/poomasi/domain/aftersales/entity/FarmAfterSales.java @@ -0,0 +1,51 @@ +package poomasi.domain.aftersales.entity; + + +import jakarta.persistence.*; +import jdk.jfr.Description; +import jdk.jfr.Timestamp; +import lombok.Builder; +import lombok.Data; +import org.hibernate.annotations.UpdateTimestamp; +import poomasi.domain.reservation.entity.Reservation; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Entity +@Table +@Data +public class FarmAfterSales { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + Long id; + + @Description("금액") + private BigDecimal afterSalesAmount; + + @Column(name = "created_at") + @UpdateTimestamp + private LocalDateTime createdAt = LocalDateTime.now(); + + @Column(name = "updated_at") + @UpdateTimestamp + private LocalDateTime updateAt = LocalDateTime.now(); + + @Column(name = "deleted_at") + @Timestamp + private LocalDateTime deletedAt; + + @OneToOne + private Reservation reservation; + + public FarmAfterSales(){ + + } + + @Builder + public FarmAfterSales(BigDecimal afterSalesAmount, Reservation reservation) { + this.afterSalesAmount = afterSalesAmount; + this.reservation = reservation; + } +} diff --git a/src/main/java/poomasi/domain/aftersales/entity/ProductAfterSales.java b/src/main/java/poomasi/domain/aftersales/entity/ProductAfterSales.java new file mode 100644 index 00000000..3d90be1f --- /dev/null +++ b/src/main/java/poomasi/domain/aftersales/entity/ProductAfterSales.java @@ -0,0 +1,47 @@ +package poomasi.domain.aftersales.entity; + +import jakarta.persistence.*; +import jdk.jfr.Description; +import jdk.jfr.Timestamp; +import lombok.Builder; +import lombok.Data; +import org.hibernate.annotations.UpdateTimestamp; +import poomasi.domain.order.entity.OrderedProduct; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Data +@Entity +@Table(name="product_after_sales") +public class ProductAfterSales { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Description("금액") + private BigDecimal afterSalesAmount; + + @Description("타입") + private AfterSalesType afterSalesType; + + @Column(name = "created_at") + @UpdateTimestamp + private LocalDateTime createdAt = LocalDateTime.now(); + + @Column(name = "updated_at") + @UpdateTimestamp + private LocalDateTime updateAt = LocalDateTime.now(); + + @Column(name = "deleted_at") + @Timestamp + private LocalDateTime deletedAt; + + @OneToOne + private OrderedProduct orderedProduct; + + public ProductAfterSales(){ + } + + +} diff --git a/src/main/java/poomasi/domain/aftersales/repository/FarmAfterSalesRepository.java b/src/main/java/poomasi/domain/aftersales/repository/FarmAfterSalesRepository.java new file mode 100644 index 00000000..ceb36e83 --- /dev/null +++ b/src/main/java/poomasi/domain/aftersales/repository/FarmAfterSalesRepository.java @@ -0,0 +1,7 @@ +package poomasi.domain.aftersales.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import poomasi.domain.aftersales.entity.FarmAfterSales; + +public interface FarmAfterSalesRepository extends JpaRepository { +} diff --git a/src/main/java/poomasi/domain/aftersales/repository/ProductAfterSalesRepository.java b/src/main/java/poomasi/domain/aftersales/repository/ProductAfterSalesRepository.java new file mode 100644 index 00000000..849ff767 --- /dev/null +++ b/src/main/java/poomasi/domain/aftersales/repository/ProductAfterSalesRepository.java @@ -0,0 +1,9 @@ +package poomasi.domain.aftersales.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import poomasi.domain.aftersales.entity.ProductAfterSales; + +@Repository +public interface ProductAfterSalesRepository extends JpaRepository { +} diff --git a/src/main/java/poomasi/domain/aftersales/service/CancelService.java b/src/main/java/poomasi/domain/aftersales/service/CancelService.java new file mode 100644 index 00000000..292509c3 --- /dev/null +++ b/src/main/java/poomasi/domain/aftersales/service/CancelService.java @@ -0,0 +1,5 @@ +package poomasi.domain.aftersales.service; + +public interface CancelService { + T cancel(P parameter); +} diff --git a/src/main/java/poomasi/domain/aftersales/service/FarmAfterSalesService.java b/src/main/java/poomasi/domain/aftersales/service/FarmAfterSalesService.java new file mode 100644 index 00000000..8323c95b --- /dev/null +++ b/src/main/java/poomasi/domain/aftersales/service/FarmAfterSalesService.java @@ -0,0 +1,76 @@ +package poomasi.domain.aftersales.service; + + +import jdk.jfr.Description; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; +import poomasi.domain.aftersales.dto.cancel.request.FarmCancelRequest; +import poomasi.domain.aftersales.dto.cancel.response.FarmCancelResponse; +import poomasi.domain.aftersales.entity.FarmAfterSales; +import poomasi.domain.aftersales.repository.FarmAfterSalesRepository; +import poomasi.domain.auth.security.userdetail.UserDetailsImpl; +import poomasi.domain.member.entity.Member; +import poomasi.domain.reservation.entity.Reservation; +import poomasi.domain.reservation.service.ReservationService; +import poomasi.global.error.ApplicationException; +import poomasi.payment.entity.Payment; +import poomasi.payment.util.PaymentUtil; + +import java.math.BigDecimal; + +import static poomasi.domain.reservation.entity.ReservationStatus.CANCELED; +import static poomasi.global.error.ApplicationError.PAYMENT_CHECKSUM_EXCESSIVE_REFUND_AMOUNT; + +@Service +@RequiredArgsConstructor +public class FarmAfterSalesService implements CancelService{ + + private final FarmAfterSalesRepository farmAfterSalesRepository; + private final ReservationService reservationService; + private final PaymentUtil paymentUtil; + + + @Override + public FarmCancelResponse cancel(FarmCancelRequest farmCancelRequest){ + Member member = getMember(); + //1. 조회 + Long reservationId = farmCancelRequest.reservationId(); + Reservation reservation = reservationService.getReservation(reservationId); + BigDecimal cancelAmount = reservationService.calculateRefundAmount(reservation); + //2. farmaftersales 만들기 + FarmAfterSales farmAfterSales = FarmAfterSales + .builder() + .afterSalesAmount(cancelAmount) + .reservation(reservation) + .build(); + //checksum 검사 및 차감 + String merchantUid = reservation.getMerchantUid(); + Payment payment = reservation.getPayment(); + if(payment.isCheckSumValid(cancelAmount)){ + throw new ApplicationException(PAYMENT_CHECKSUM_EXCESSIVE_REFUND_AMOUNT); + } + //환불 + paymentUtil.refundByMerchantUid(merchantUid, payment.getCheckSum(), cancelAmount); + //절차 등록 + reservation.setFarmAfterSales(farmAfterSales); + reservation.cancel(); + farmAfterSalesRepository.save(farmAfterSales); + + return new FarmCancelResponse(reservationId, CANCELED, cancelAmount); + } + + + + + @Description("security context에서 member 객체 가져오는 메서드") + private Member getMember() { + Authentication authentication = SecurityContextHolder + .getContext().getAuthentication(); + Object impl = authentication.getPrincipal(); + Member member = ((UserDetailsImpl) impl).getMember(); + return member; + } + +} diff --git a/src/main/java/poomasi/domain/aftersales/service/ProductAfterSalesService.java b/src/main/java/poomasi/domain/aftersales/service/ProductAfterSalesService.java new file mode 100644 index 00000000..6a36bfd8 --- /dev/null +++ b/src/main/java/poomasi/domain/aftersales/service/ProductAfterSalesService.java @@ -0,0 +1,160 @@ +package poomasi.domain.aftersales.service; + + +import jdk.jfr.Description; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import poomasi.domain.aftersales.dto.cancel.request.ProductCancelRequest; +import poomasi.domain.aftersales.dto.cancel.response.ProductCancelResponse; +import poomasi.domain.aftersales.dto.refund.request.ProductRefundRequest; +import poomasi.domain.aftersales.dto.refund.request.ProductRefundRequestApprovalRequest; +import poomasi.domain.aftersales.dto.refund.response.ProductRefundRequestApprovalResponse; +import poomasi.domain.aftersales.dto.refund.response.ProductRefundResponse; +import poomasi.domain.aftersales.entity.ProductAfterSales; +import poomasi.domain.aftersales.repository.ProductAfterSalesRepository; +import poomasi.domain.auth.security.userdetail.UserDetailsImpl; +import poomasi.domain.member.entity.Member; +import poomasi.domain.order.entity.OrderedProduct; +import poomasi.domain.order.entity.OrderedProductStatus; +import poomasi.domain.order.repository.OrderedProductRepository; +import poomasi.domain.order.service.OrderService; +import poomasi.domain.reservation.service.ReservationService; +import poomasi.global.error.ApplicationException; +import poomasi.global.error.BusinessException; +import poomasi.payment.entity.Payment; +import poomasi.payment.util.PaymentUtil; + +import java.math.BigDecimal; + +import static poomasi.domain.order.entity.OrderedProductStatus.CANCEL_PENDING; +import static poomasi.domain.order.entity.OrderedProductStatus.REFUND_APPROVED; +import static poomasi.global.error.ApplicationError.PAYMENT_CHECKSUM_EXCESSIVE_REFUND_AMOUNT; +import static poomasi.global.error.BusinessError.REFUND_AFTER_SALES_NOT_FOUND; +import static poomasi.global.error.BusinessError.REFUND_AFTER_SALES_REQUEST_INVALID_OWNER; + +@Service +@RequiredArgsConstructor +public class ProductAfterSalesService implements CancelService{ + + private final OrderedProductRepository orderedProductRepository; + private final PaymentUtil paymentUtil; + private final OrderService orderService; + private final ReservationService reservationService; + private final ProductAfterSalesRepository productAfterSalesDetail; + private final ProductAfterSalesRepository productAfterSalesRepository; + + @Override + @Transactional + public ProductCancelResponse cancel(ProductCancelRequest productCancelRequest){ + + Long orderedProductId = productCancelRequest.orderedProductId(); + Long memberId = getMember().getId(); + + //취소 가능한지 체크 + OrderedProduct orderedProduct = orderService.validateProductCancelable(memberId, orderedProductId); + + //포트원 취소를 위한 주문 Id 찾기 + Payment payment = orderedProduct.getPayment(); + String impUid = payment.getImpUid(); + + BigDecimal finalCancelAmount = orderedProduct.calculateCancelAmount(); + + // 체크섬 검증 + BigDecimal checkSum = payment.getCheckSum(); + if(!payment.isCheckSumValid(finalCancelAmount)){ + throw new ApplicationException(PAYMENT_CHECKSUM_EXCESSIVE_REFUND_AMOUNT); + } + + //취소 요청 후, 주문 취소 상태로 변경 + paymentUtil.partialRefundByImpUid(impUid, checkSum, finalCancelAmount); + + // 취소 수량 증가 + Integer cancelRequestQuantity = orderedProduct.getCount(); + orderedProduct.getProduct().addStock(cancelRequestQuantity); + orderedProduct.setOrderedProductStatus(CANCEL_PENDING); + + orderedProductRepository.save(orderedProduct); + + return new ProductCancelResponse( + orderedProductId, + orderedProduct.getOrderedProductStatus(), + finalCancelAmount + ); + } + + //-------------------------refund---------------------// + @Description("환불 요청하는 메서드") + @Transactional + public ProductRefundResponse refund(ProductRefundRequest productRefundRequest) { + Long orderedProductId = productRefundRequest.orderedProductId(); + Long memberId = getMember().getId(); + + // 주인 검증 - 유저의 orderedProductId가 맞는지 검증 + OrderedProduct orderedProduct = orderService.validateProductRefundable(memberId, orderedProductId); + + Payment payment = orderedProduct.getPayment(); + String impUid = payment.getImpUid(); + + OrderedProductStatus orderedProductStatus = orderedProduct.getOrderedProductStatus(); + BigDecimal finalRefundAmount = orderedProduct.calculateRefundAmount(); + + // 체크섬 검증 + BigDecimal checkSum = payment.getCheckSum(); + if(payment.isCheckSumValid(finalRefundAmount)){ + throw new ApplicationException(PAYMENT_CHECKSUM_EXCESSIVE_REFUND_AMOUNT); + } + + //취소 요청 후, 주문 취소 상태로 변경 + paymentUtil.partialRefundByImpUid(impUid, checkSum, finalRefundAmount); + + //응답 반환 + return new ProductRefundResponse( + orderedProductId, + orderedProductStatus, + finalRefundAmount + ); + } + + @Description("판매자 환불 확인 메서드") + @Transactional + public ProductRefundRequestApprovalResponse processRefundApproval(ProductRefundRequestApprovalRequest productRefundRequestApprovalRequest){ + Long productAfterSalesId = productRefundRequestApprovalRequest.productAfterSalesId(); + + //환불 요청이 존재하는지 그리고 자신의 환불 요청인지 검증하고 + ProductAfterSales productAfterSales= validateProductRefundRequestByFarmerId(productAfterSalesId); + productAfterSales.getOrderedProduct().setOrderedProductStatus(REFUND_APPROVED); + + //전달한다 + return new ProductRefundRequestApprovalResponse( + productAfterSales.getId(), + productAfterSales.getOrderedProduct().getOrderedProductStatus(), + productAfterSales.getAfterSalesAmount() + ); + } + + @Description("환불 요청이 존재하고, 판매자 소유인지 확인하는 메서드") + private ProductAfterSales validateProductRefundRequestByFarmerId(Long productAfterSalesDetailId){ + ProductAfterSales productAfterSales = productAfterSalesRepository.findById(productAfterSalesDetailId) + .orElseThrow(()-> new BusinessException(REFUND_AFTER_SALES_NOT_FOUND)); + Long farmerId = getMember().getId(); + + if(farmerId != productAfterSales.getOrderedProduct().getStoreOwner().getId()){ + throw new BusinessException(REFUND_AFTER_SALES_REQUEST_INVALID_OWNER); + } + + return productAfterSales; + } + + @Description("security context에서 member 객체 가져오는 메서드") + private Member getMember() { + Authentication authentication = SecurityContextHolder + .getContext().getAuthentication(); + Object impl = authentication.getPrincipal(); + Member member = ((UserDetailsImpl) impl).getMember(); + return member; + } + +} diff --git a/src/main/java/poomasi/domain/auth/config/SecurityConfig.java b/src/main/java/poomasi/domain/auth/config/SecurityConfig.java index 653f0d68..13a5cc35 100644 --- a/src/main/java/poomasi/domain/auth/config/SecurityConfig.java +++ b/src/main/java/poomasi/domain/auth/config/SecurityConfig.java @@ -1,8 +1,6 @@ package poomasi.domain.auth.config; -import jakarta.servlet.http.HttpServletRequest; import lombok.AllArgsConstructor; -import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -15,21 +13,22 @@ import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher; -import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; -import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import poomasi.domain.auth.security.filter.CustomUsernamePasswordAuthenticationFilter; import poomasi.domain.auth.security.filter.JwtAuthenticationFilter; -import poomasi.domain.auth.security.handler.CustomSuccessHandler; +import poomasi.domain.auth.security.filter.JwtLogoutFilter; +import poomasi.domain.auth.security.handler.OAuth2FailureHandler; +import poomasi.domain.auth.security.handler.OAuth2SuccessHandler; import poomasi.domain.auth.security.userdetail.OAuth2UserDetailServiceImpl; import poomasi.domain.auth.security.userdetail.UserDetailsServiceImpl; +import poomasi.domain.auth.token.blacklist.service.AccessTokenBlacklistService; +import poomasi.domain.auth.token.blacklist.service.BlacklistJpaService; import poomasi.domain.auth.token.util.JwtUtil; - -import java.util.Arrays; -import java.util.Collections; +import poomasi.domain.auth.token.whitelist.service.RefreshTokenWhitelistService; @AllArgsConstructor @@ -41,9 +40,12 @@ public class SecurityConfig { private final AuthenticationConfiguration authenticationConfiguration; private final JwtUtil jwtUtil; private final MvcRequestMatcher.Builder mvc; - private final CustomSuccessHandler customSuccessHandler; + private final OAuth2SuccessHandler oAuth2SuccessHandler; + private final OAuth2FailureHandler oAuth2FailureHandler; private final UserDetailsServiceImpl userDetailsService; private final CorsConfigurationSource corsConfigurationSource; + private final AccessTokenBlacklistService accessTokenBlacklistService; + private final RefreshTokenWhitelistService refreshTokenWhitelistService; @Autowired private OAuth2UserDetailServiceImpl oAuth2UserDetailServiceImpl; @@ -53,14 +55,14 @@ public AuthenticationManager authenticationManager(AuthenticationConfiguration c return configuration.getAuthenticationManager(); } - @Description("순서 : Oauth2 -> jwt -> login -> logout") + @Description("순서 : logout -> Oauth2 -> jwt -> login ") @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - //form login disable + //기본 폼로그인 비활성화 http.formLogin(AbstractHttpConfigurer::disable); - //basic login disable + //http basic 비활성화 http.httpBasic(AbstractHttpConfigurer::disable); //csrf 해제 @@ -74,59 +76,85 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti http.sessionManagement((session) -> session .sessionCreationPolicy(SessionCreationPolicy.STATELESS)); - //기본 로그아웃 해제 + //기본 로그아웃 비활성화 http.logout(AbstractHttpConfigurer::disable); - - // 기본 경로 및 테스트 경로 http.authorizeHttpRequests((authorize) -> authorize - .requestMatchers(HttpMethod.POST, "/api/farm/**").permitAll() - .requestMatchers(HttpMethod.GET, "/api/product/**").permitAll() - .requestMatchers(HttpMethod.GET, "/api/review/**").permitAll() - .requestMatchers(HttpMethod.GET, "/health").permitAll() - .requestMatchers(HttpMethod.GET, "/api/image/**").permitAll() - .requestMatchers("/api/member/sign-up", "/api/login", "api/reissue", "api/payment/**", "api/order/**", "api/reservation/**", "/api/v1/farmer/reservations").permitAll() - .requestMatchers("/api/need-auth/**").authenticated() - .anyRequest(). - authenticated() - ); + // 기본 경로 및 테스트 경로 + // 인증 및 인가가 필요한 부분을 "authenticated"로 표시해주세요 + + //건호 api + .requestMatchers("/oauth2/authentication/kakao").authenticated() + .requestMatchers("api/orders/**").authenticated() + .requestMatchers(HttpMethod.POST, "api/logout").authenticated() + + //진택 api + .requestMatchers(HttpMethod.POST, "/api/reissue").authenticated() + .requestMatchers(HttpMethod.PUT, "/api/members/update/customer").authenticated() + .requestMatchers(HttpMethod.PUT, "/api/members/update/farmer").authenticated() + .requestMatchers(HttpMethod.GET, "/api/members/**").authenticated() + .requestMatchers(HttpMethod.PUT, "/api/members/to-farmer").authenticated() + .requestMatchers(HttpMethod.GET, "/api/members/summary").authenticated() + .requestMatchers(HttpMethod.PUT, "/api/members/to-customer/**").authenticated() + .requestMatchers(HttpMethod.GET, "/api/members/self").authenticated() + .requestMatchers(HttpMethod.PUT, "/api/members/update/customer/address").authenticated() + .requestMatchers(HttpMethod.DELETE, "/api/members/delete").authenticated() + .requestMatchers(HttpMethod.PUT, "/api/members/restore/**").authenticated() + .requestMatchers(HttpMethod.PUT, "/api/members/suspend/**").authenticated() + .requestMatchers(HttpMethod.GET, "/api/s3/presigned-url-get").authenticated() + .requestMatchers(HttpMethod.GET, "/api/s3/presigned-url-put").authenticated() + .requestMatchers(HttpMethod.POST, "/api/images").authenticated() + .requestMatchers(HttpMethod.POST, "api/images/multiple").authenticated() + .requestMatchers(HttpMethod.DELETE, "/api/images/delete/**").authenticated() + .requestMatchers(HttpMethod.PUT, "/api/images/update/**").authenticated() + .requestMatchers(HttpMethod.PUT, "/api/images/recover/**").authenticated() + .requestMatchers(HttpMethod.PUT, "/api/members/to-customer/**").authenticated() + .requestMatchers(HttpMethod.PUT, "/api/members/to-customer/**").authenticated() + .requestMatchers(HttpMethod.PUT, "/api/members/to-customer/**").authenticated() + + + //지민 api + .requestMatchers( "/api/v1/wishlist/**").authenticated() + .requestMatchers("/api/farmer/farms/**").authenticated() + .requestMatchers("/api/farmer/farms/info").authenticated() + .requestMatchers("/api/v1/farmer/reservations").authenticated() + + + //풍헌 api + .requestMatchers("/api/cart/**").authenticated() + .requestMatchers("/api/categories").authenticated() + .requestMatchers("/api/reviews/**").authenticated() + .requestMatchers("/api/products/**").authenticated() + .requestMatchers("/api/store/**").authenticated() + + + .anyRequest().permitAll() + ); - //endpoint : {domain}/oauth2/authentication/kakao + //oauth2 filter http .oauth2Login((oauth2) -> oauth2 .userInfoEndpoint((userInfoEndpointConfig) -> userInfoEndpointConfig - .userService(oAuth2UserDetailServiceImpl)) - .successHandler(customSuccessHandler) + .userService(oAuth2UserDetailServiceImpl) + ) + .successHandler(oAuth2SuccessHandler) + .failureHandler(oAuth2FailureHandler) ); + //username password filter CustomUsernamePasswordAuthenticationFilter customUsernameFilter = - new CustomUsernamePasswordAuthenticationFilter(authenticationManager(authenticationConfiguration), jwtUtil); + new CustomUsernamePasswordAuthenticationFilter(authenticationManager(authenticationConfiguration), jwtUtil, refreshTokenWhitelistService); customUsernameFilter.setFilterProcessesUrl("/api/login"); - http.addFilterAt(customUsernameFilter, UsernamePasswordAuthenticationFilter.class); - http.addFilterBefore(new JwtAuthenticationFilter(jwtUtil, userDetailsService), UsernamePasswordAuthenticationFilter.class); - - /* - 로그아웃 필터 등록하기 - LogoutHandler[] handlers = { - new CookieClearingLogoutHandler(), - new ClearAuthenticationHandler() - }; - CustomLogoutFilter customLogoutFilter = new CustomLogoutFilter(jwtUtil, new CustomLogoutSuccessHandler(), handlers); - customLogoutFilter.setFilterProcessesUrl("/api/logout"); - customLogoutFilter. - http.addFilterAt(customLogoutFilter, LogoutFilter.class); - - http.logout( (logout) -> - logout. - logoutSuccessHandler(new CustomLogoutSuccessHandler()) - .addLogoutHandler(new CookieClearingLogoutHandler()) - .addLogoutHandler(new ClearAuthenticationHandler()) - ); - */ - //http.addFilterAfter(customLogoutFilter, JwtAuthenticationFilter.class); + //jwt filter + http.addFilterAfter(new JwtAuthenticationFilter(jwtUtil, userDetailsService, accessTokenBlacklistService), + OAuth2LoginAuthenticationFilter.class); + + //logout filter + JwtLogoutFilter customLogoutFilter = new JwtLogoutFilter(jwtUtil, accessTokenBlacklistService, refreshTokenWhitelistService); + http.addFilterAfter(customLogoutFilter, JwtAuthenticationFilter.class); return http.build(); } diff --git a/src/main/java/poomasi/domain/auth/security/AuthTestController.java b/src/main/java/poomasi/domain/auth/security/AuthTestController.java deleted file mode 100644 index 3694523e..00000000 --- a/src/main/java/poomasi/domain/auth/security/AuthTestController.java +++ /dev/null @@ -1,53 +0,0 @@ -package poomasi.domain.auth.security; - - -import jdk.jfr.Description; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.access.annotation.Secured; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RestController; -import poomasi.domain.auth.security.userdetail.UserDetailsImpl; -import poomasi.domain.member.entity.Member; - -@Slf4j -@Description("접근 제어 확인 controller") -@RestController -public class AuthTestController { - - @Autowired - private AuthTestService authTestService; - - @Secured("ROLE_CUSTOMER") - @GetMapping("/api/auth-test/customer") - public String customer() { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - Object impl = authentication.getPrincipal(); - Member member = ((UserDetailsImpl) impl).getMember(); - - log.info("email : " + member.getEmail()); - log.info("member : " + member.getId()); - - return "hi. customer"; - } - - @Secured("ROLE_FARMER") - @GetMapping("/api/auth-test/farmer") - public String farmer() { - return "hi. farmer"; - } - - @GetMapping("/api/auth-test") - public String needAuth() { - return "auth"; - } - - @GetMapping("/api/auth-test/test") - public String Test(){ - authTestService.Test(); - return "Success"; - } - -} diff --git a/src/main/java/poomasi/domain/auth/security/AuthTestService.java b/src/main/java/poomasi/domain/auth/security/AuthTestService.java deleted file mode 100644 index 7a237b38..00000000 --- a/src/main/java/poomasi/domain/auth/security/AuthTestService.java +++ /dev/null @@ -1,27 +0,0 @@ -package poomasi.domain.auth.security; - -import lombok.extern.slf4j.Slf4j; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.stereotype.Service; -import poomasi.domain.auth.security.userdetail.UserDetailsImpl; -import poomasi.domain.member.entity.Member; - -@Slf4j -@Service -public class AuthTestService { - - //제가 테스트하려고 만든 건데 다음 pr때 지우겠습니다 - - public String Test(){ - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - Object impl = authentication.getPrincipal(); - Member member = ((UserDetailsImpl) impl).getMember(); - - log.info("member : " + member.getEmail()); - log.info("member : " + member.getId().toString()); - - return "SUCCESS"; - } - -} diff --git a/src/main/java/poomasi/domain/auth/security/filter/CustomLogoutFilter.java b/src/main/java/poomasi/domain/auth/security/filter/CustomLogoutFilter.java deleted file mode 100644 index 5843d8f6..00000000 --- a/src/main/java/poomasi/domain/auth/security/filter/CustomLogoutFilter.java +++ /dev/null @@ -1,70 +0,0 @@ - - -package poomasi.domain.auth.security.filter; - -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.ServletRequest; -import jakarta.servlet.ServletResponse; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.HttpHeaders; -import org.springframework.security.web.authentication.logout.LogoutFilter; -import org.springframework.security.web.authentication.logout.LogoutHandler; -import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; -import org.springframework.web.filter.OncePerRequestFilter; -import poomasi.domain.auth.token.util.JwtUtil; - -import java.io.IOException; -import java.io.PrintWriter; - -@Slf4j -public class CustomLogoutFilter extends LogoutFilter { - - private JwtUtil jwtUtil; - - public CustomLogoutFilter(JwtUtil jwtUtil, LogoutSuccessHandler logoutSuccessHandler, LogoutHandler... handlers) { - super(logoutSuccessHandler, handlers); - this.jwtUtil=jwtUtil; - } - - @Override - public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { - doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain); - } - - public void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException { - - log.info("[logout filter] - 로그아웃 진행합니다."); - - // POST : /api/logout 아니라면 넘기기 - String requestURI = request.getRequestURI(); - String requestMethod = request.getMethod(); - if (!"/api/logout".equals(requestURI) || !requestMethod.equals("POST")) { - log.info("[logout url not matching] "); - filterChain.doFilter(request, response); - return; - } - - - boolean isLogoutSuccess = true; - - if(isLogoutSuccess){ - PrintWriter out = response.getWriter(); - out.println("logout success~. "); - return; - } - - /* - * 로그아웃 로직 - * access token , refresh token 관리하기 - * */ - PrintWriter out = response.getWriter(); - out.println("logout success~. "); - //return; - //filterChain.doFilter(request, response); - } -} - diff --git a/src/main/java/poomasi/domain/auth/security/filter/CustomUsernamePasswordAuthenticationFilter.java b/src/main/java/poomasi/domain/auth/security/filter/CustomUsernamePasswordAuthenticationFilter.java index 27b81b61..2e642dbf 100644 --- a/src/main/java/poomasi/domain/auth/security/filter/CustomUsernamePasswordAuthenticationFilter.java +++ b/src/main/java/poomasi/domain/auth/security/filter/CustomUsernamePasswordAuthenticationFilter.java @@ -17,11 +17,10 @@ import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import poomasi.domain.auth.security.userdetail.UserDetailsImpl; import poomasi.domain.auth.token.util.JwtUtil; - +import poomasi.domain.auth.token.whitelist.service.RefreshTokenWhitelistService; import java.io.BufferedReader; import java.io.IOException; -import java.io.PrintWriter; import java.util.Map; @Slf4j @@ -30,10 +29,11 @@ public class CustomUsernamePasswordAuthenticationFilter extends UsernamePassword private final AuthenticationManager authenticationManager; private final JwtUtil jwtUtil; + private final RefreshTokenWhitelistService refreshTokenWhitelistService; @Description("인증 시도 메서드") @Override - public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { + public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException{ log.info("email - password 기반으로 인증을 시도 합니다 : CustomUsernamePasswordAuthenticationFilter"); ObjectMapper loginRequestMapper = new ObjectMapper(); @@ -63,18 +63,17 @@ protected void successfulAuthentication(HttpServletRequest request, HttpServletR String role = customUserDetails.getAuthority(); Long memberId = customUserDetails.getMember().getId(); - String accessToken = jwtUtil.generateTokenInFilter(username, role, "access", memberId); - String refreshToken = jwtUtil.generateTokenInFilter(username, role, "refresh", memberId); + String accessToken = jwtUtil.generateAccessTokenById(memberId); + String refreshToken = jwtUtil.generateRefreshTokenById(memberId); - log.info("usename password 기반 로그인 성공 . cookie에 토큰을 넣어 발급합니다."); - response.setHeader("access", accessToken); + log.info("username password 기반 로그인 성공 . cookie에 토큰을 넣어 발급합니다."); response.addCookie(createCookie("refresh", refreshToken)); response.setStatus(HttpStatus.OK.value()); - // 나중에 주석 해야 함 - PrintWriter out = response.getWriter(); - out.println("access : " + accessToken + ", refresh : " + refreshToken); - out.close(); + refreshTokenWhitelistService.putRefreshToken(refreshToken, memberId); + + response.setContentType("application/json"); // Content-Type 설정 + response.getWriter().write("{\"access\": \"" + accessToken + "\", \"refresh\": \"" + refreshToken + "\"}"); } @Override @@ -87,6 +86,7 @@ private Cookie createCookie(String key, String value) { Cookie cookie = new Cookie(key, value); cookie.setMaxAge(24*60*60); cookie.setHttpOnly(true); + cookie.setDomain("https://poomasi.shop"); return cookie; } diff --git a/src/main/java/poomasi/domain/auth/security/filter/JwtAuthenticationFilter.java b/src/main/java/poomasi/domain/auth/security/filter/JwtAuthenticationFilter.java index 122d31af..db271a82 100644 --- a/src/main/java/poomasi/domain/auth/security/filter/JwtAuthenticationFilter.java +++ b/src/main/java/poomasi/domain/auth/security/filter/JwtAuthenticationFilter.java @@ -10,19 +10,16 @@ import org.springframework.http.HttpHeaders; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; -import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.web.filter.OncePerRequestFilter; import poomasi.domain.auth.security.userdetail.UserDetailsImpl; +import poomasi.domain.auth.token.blacklist.service.BlacklistJpaService; +import poomasi.domain.auth.token.blacklist.service.AccessTokenBlacklistService; import poomasi.domain.auth.token.util.JwtUtil; -import poomasi.domain.member.entity.Member; -import poomasi.domain.member.entity.Role; import java.io.IOException; import java.io.PrintWriter; -import java.util.Collection; @Description("access token을 검증하는 필터") @AllArgsConstructor @@ -31,42 +28,57 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtUtil jwtUtil; private final UserDetailsService userDetailsService; + private final AccessTokenBlacklistService accessTokenBlacklistService; + @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { - - log.info("jwt 인증 필터입니다"); + log.info("[JwtAuthenticationFilter] - jwt 인증 필터입니다"); String requestHeader = request.getHeader(HttpHeaders.AUTHORIZATION); String accessToken = null; + String requestUri = request.getRequestURI(); + if ("/oauth2/authentication/kakao".equals(requestUri)) { + log.info("[JwtAuthenticationFilter] - 카카오 인증 요청이므로 필터를 통과합니다."); + // 요청을 그대로 다음 필터로 넘김 + filterChain.doFilter(request, response); + return; + } + + if("/api/login".equals(requestUri)) { + log.info("[JwtAuthenticationFilter] - 로그인 요청이므로 필터를 통과합니다."); + // 요청을 그대로 다음 필터로 넘김 + filterChain.doFilter(request, response); + return; + } + + if (requestHeader == null || !requestHeader.startsWith("Bearer ")) { - log.info("access token을 header로 갖지 않았으므로 다음 usernamepassword 필터로 이동합니다"); + log.info("[JwtAuthenticationFilter] : access token을 header로 갖지 않았으므로 다음 필터로 이동합니다"); filterChain.doFilter(request, response); - }else{ - //access 추출하기 - log.info("access token 추출하기"); + return; + }else{ //존재하고 Bearer로 존재한다면 + log.info("[JwtAuthenticationFilter] : access token 추출하기"); accessToken = requestHeader.substring(7); } - log.info("access token 추출 완료: " + accessToken); + log.info("[JwtAuthenticationFilter] : access token 추출 완료: " + accessToken); - if (accessToken == null) { + if (accessToken == null) { log.info("access token이 존재하지 않아서 다음 filter로 넘어갑니다."); filterChain.doFilter(request, response); return; } - // 만료 검사 - if(jwtUtil.isTokenExpired(accessToken)){ - log.warn("[인증 실패] - 토큰이 만료되었습니다."); - PrintWriter writer = response.getWriter(); - writer.print("만료된 토큰입니다."); + if(accessTokenBlacklistService.hasAccessToken(accessToken)){ + log.info("블랙리스트에 있는 토큰입니다."); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.getWriter().write("{\"message\": \"" + "token is in Blacklist"); return; } // 유효성 검사 - if(!jwtUtil.validateTokenInFilter(accessToken)) { + if(!jwtUtil.validateAccessToken(accessToken)) { log.warn("JWT 필터 - [인증 실패] - 위조된 토큰입니다."); PrintWriter writer = response.getWriter(); writer.print("위조된 토큰입니다."); @@ -74,8 +86,16 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse return; } + // 만료 검사 + if(jwtUtil.isTokenExpired(accessToken)){ + response.setStatus(HttpServletResponse.SC_BAD_REQUEST); + response.getWriter().write("{\"message\": \"" + "token is expired"); + filterChain.doFilter(request, response); + return; + } + log.info("토큰 검증 완료"); - String username = jwtUtil.getEmailFromTokenInFilter(accessToken); + String username = jwtUtil.getEmailFromToken(accessToken); UserDetailsImpl userDetailsImpl = (UserDetailsImpl) userDetailsService.loadUserByUsername(username); // (ID, password, auth) @@ -83,9 +103,10 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse SecurityContextHolder.getContext().setAuthentication(authToken); filterChain.doFilter(request, response); - + return; } + } diff --git a/src/main/java/poomasi/domain/auth/security/filter/JwtLogoutFilter.java b/src/main/java/poomasi/domain/auth/security/filter/JwtLogoutFilter.java new file mode 100644 index 00000000..209aa68d --- /dev/null +++ b/src/main/java/poomasi/domain/auth/security/filter/JwtLogoutFilter.java @@ -0,0 +1,121 @@ + + +package poomasi.domain.auth.security.filter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.filter.OncePerRequestFilter; +import poomasi.domain.auth.security.userdetail.UserDetailsImpl; +import poomasi.domain.auth.token.blacklist.service.AccessTokenBlacklistService; +import poomasi.domain.auth.token.util.JwtUtil; +import poomasi.domain.auth.token.whitelist.service.RefreshTokenWhitelistService; +import poomasi.domain.member.entity.Member; + +import java.io.IOException; + +@RequiredArgsConstructor +@Slf4j +public class JwtLogoutFilter extends OncePerRequestFilter { + + private final JwtUtil jwtUtil; + private final AccessTokenBlacklistService accessTokenBlacklistService; + private final RefreshTokenWhitelistService refreshTokenWhitelistService; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + + // 로그아웃 URL이 아니면 필터 실행하지 않음 + if (!"/api/logout".equals(request.getRequestURI())) { + filterChain.doFilter(request, response); + return; + } + + // 토큰 추출 + String accessToken = extractJwtFromRequest(request); + if (accessToken == null || !jwtUtil.validateToken(accessToken)) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.getWriter().write("{\"error\": \"Invalid token\"}"); + return; + } + + // if(만료되었으면) -> response 만료됨 + if(jwtUtil.isTokenExpired(accessToken)){ + response.setStatus(HttpServletResponse.SC_OK); + response.getWriter().write("{\"message\": \"logout completes\"}"); + return; + } + + // 블랙리스팅 + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + Object impl = authentication.getPrincipal(); + Member member = ((UserDetailsImpl) impl).getMember(); + Long memberId = member.getId(); + + if(accessTokenBlacklistService.hasAccessToken(accessToken)){ + log.info("jwt logout filter - access token이 블랙리스트에 있어요."); + }else{ + log.info("jwt logout filter - access token이 블랙리스트에 없어요. 저장할게요"); + accessTokenBlacklistService.putAccessToken(accessToken, memberId); + } + + //db refresh token 제거 + Cookie[] cookies = request.getCookies(); + String refreshToken = getRefreshToken(cookies); + + if(refreshToken!=null) { + log.info("jwt logout filter - refresh token을 지웁니다"); + refreshTokenWhitelistService.removeMemberRefreshToken(memberId); + } + + // 쿠키 삭제 + clearAuthCookie(response); + // 세션 무효화 + request.getSession().invalidate(); + // 4. SecurityContext를 비워 인증 정보 해제 + SecurityContextHolder.clearContext(); + + // 로그아웃 성공 응답 설정 + response.setStatus(HttpServletResponse.SC_OK); + response.getWriter().write("{\"message\": \"Logout successful\"}"); + return; + } + + private String extractJwtFromRequest(HttpServletRequest request) { + String bearerToken = request.getHeader("Authorization"); + if (bearerToken != null && bearerToken.startsWith("Bearer ")) { + return bearerToken.substring(7); + } + return null; + } + + private void clearAuthCookie(HttpServletResponse response) { + Cookie cookie = new Cookie("refresh", null); + cookie.setPath("/"); + cookie.setHttpOnly(true); + cookie.setMaxAge(0); + response.addCookie(cookie); + } + + private String getRefreshToken(Cookie[] cookies) { + + if (cookies != null) { + // 쿠키 목록을 순회하면서 "refresh" 쿠키를 찾습니다. + for (Cookie cookie : cookies) { + if ("refresh".equals(cookie.getName())) { + return cookie.getValue(); // 쿠키의 값 반환 + } + } + } + return null; // "refresh" 쿠키가 없다면 null 반환 + } + +} diff --git a/src/main/java/poomasi/domain/auth/security/handler/ClearAuthenticationHandler.java b/src/main/java/poomasi/domain/auth/security/handler/ClearAuthenticationHandler.java deleted file mode 100644 index f4ceee19..00000000 --- a/src/main/java/poomasi/domain/auth/security/handler/ClearAuthenticationHandler.java +++ /dev/null @@ -1,17 +0,0 @@ -package poomasi.domain.auth.security.handler; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import lombok.extern.slf4j.Slf4j; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.web.authentication.logout.LogoutHandler; - -@Slf4j -public class ClearAuthenticationHandler implements LogoutHandler { - @Override - public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { - log.info("[logout handler] - security context 제거"); - SecurityContextHolder.clearContext(); - } -} \ No newline at end of file diff --git a/src/main/java/poomasi/domain/auth/security/handler/CookieClearingLogoutHandler.java b/src/main/java/poomasi/domain/auth/security/handler/CookieClearingLogoutHandler.java deleted file mode 100644 index 4a9dd8a9..00000000 --- a/src/main/java/poomasi/domain/auth/security/handler/CookieClearingLogoutHandler.java +++ /dev/null @@ -1,27 +0,0 @@ -package poomasi.domain.auth.security.handler; - -import jakarta.servlet.http.Cookie; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import lombok.extern.slf4j.Slf4j; -import org.springframework.security.core.Authentication; -import org.springframework.security.web.authentication.logout.LogoutHandler; - -@Slf4j -public class CookieClearingLogoutHandler implements LogoutHandler { - - @Override - public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { - Cookie[] cookies = request.getCookies(); - if (cookies != null) { - for (Cookie cookie : cookies) { - cookie.setValue(null); - cookie.setMaxAge(0); // 쿠키 제거 - cookie.setPath("/"); // 적용할 경로 설정 - response.addCookie(cookie); - } - log.info("Cookies cleared"); - } - log.info("[logout handler] - cookie 제거"); - } -} \ No newline at end of file diff --git a/src/main/java/poomasi/domain/auth/security/handler/CustomLogoutSuccessHandler.java b/src/main/java/poomasi/domain/auth/security/handler/CustomLogoutSuccessHandler.java deleted file mode 100644 index 351bd24a..00000000 --- a/src/main/java/poomasi/domain/auth/security/handler/CustomLogoutSuccessHandler.java +++ /dev/null @@ -1,29 +0,0 @@ -package poomasi.domain.auth.security.handler; - -import jakarta.servlet.http.Cookie; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import lombok.extern.slf4j.Slf4j; -import org.springframework.security.core.Authentication; -import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; - -import java.io.IOException; - -@Slf4j -public class CustomLogoutSuccessHandler implements LogoutSuccessHandler { - - @Override - public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { - - log.info("[logout success handler] - cookie 제거"); - expireCookie(response, "access"); - expireCookie(response, "refresh"); - } - - private void expireCookie(HttpServletResponse response, String key) { - Cookie cookie = new Cookie(key, null); // 쿠키를 null로 설정 - cookie.setMaxAge(0); // 쿠키의 최대 생명 주기를 0으로 설정 - cookie.setPath("/"); // 쿠키의 경로를 설정 (원래 설정한 경로와 동일하게) - response.addCookie(cookie); // 응답에 쿠키 추가 - } -} diff --git a/src/main/java/poomasi/domain/auth/security/handler/OAuth2FailureHandler.java b/src/main/java/poomasi/domain/auth/security/handler/OAuth2FailureHandler.java new file mode 100644 index 00000000..e7cb5584 --- /dev/null +++ b/src/main/java/poomasi/domain/auth/security/handler/OAuth2FailureHandler.java @@ -0,0 +1,22 @@ +package poomasi.domain.auth.security.handler; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +public class OAuth2FailureHandler implements AuthenticationFailureHandler { + + + @Override + public void onAuthenticationFailure(final HttpServletRequest request, final HttpServletResponse response, final AuthenticationException exception) throws IOException, ServletException { + String redirectUrl = "https://poomasi.shop"; + response.sendRedirect(redirectUrl); + } +} diff --git a/src/main/java/poomasi/domain/auth/security/handler/CustomSuccessHandler.java b/src/main/java/poomasi/domain/auth/security/handler/OAuth2SuccessHandler.java similarity index 76% rename from src/main/java/poomasi/domain/auth/security/handler/CustomSuccessHandler.java rename to src/main/java/poomasi/domain/auth/security/handler/OAuth2SuccessHandler.java index c4f0c483..2dc9519f 100644 --- a/src/main/java/poomasi/domain/auth/security/handler/CustomSuccessHandler.java +++ b/src/main/java/poomasi/domain/auth/security/handler/OAuth2SuccessHandler.java @@ -1,40 +1,32 @@ package poomasi.domain.auth.security.handler; -/* - * TODO : Oauth2.0 로그인이 성공하면 access, refresh를 발급해야 함. - * - * */ import jakarta.servlet.ServletException; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jdk.jfr.Description; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.http.HttpHeaders; +import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; import org.springframework.security.core.Authentication; -import org.springframework.security.core.GrantedAuthority; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; -import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; import org.springframework.stereotype.Component; import poomasi.domain.auth.security.userdetail.UserDetailsImpl; import poomasi.domain.auth.token.util.JwtUtil; +import poomasi.domain.auth.token.whitelist.service.RefreshTokenWhitelistService; import poomasi.domain.member.entity.Member; import java.io.IOException; -import java.util.Collection; -import java.util.Iterator; @Slf4j @Component -public class CustomSuccessHandler implements AuthenticationSuccessHandler { +@RequiredArgsConstructor +public class OAuth2SuccessHandler implements AuthenticationSuccessHandler { private final JwtUtil jwtUtil; - - public CustomSuccessHandler(JwtUtil jwtUtil) { - this.jwtUtil = jwtUtil; - } + private final RefreshTokenWhitelistService refreshTokenWhitelistService; @Description("TODO : Oauth2.0 로그인이 성공하면 server access, refresh token을 발급하는 메서드") @Override @@ -51,11 +43,12 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo String refreshToken = jwtUtil.generateRefreshTokenById(memberId); response.addCookie(createCookie("refresh", refreshToken)); - response.addCookie(createCookie("access", accessToken)); - response.setStatus(HttpStatus.OK.value()); - response.getWriter(); + //refresh token db에 저장 + + refreshTokenWhitelistService.putRefreshToken(refreshToken, memberId); + response.sendRedirect("https://poomasi.shop/callback/kakao"+"?access=" + accessToken); } private Cookie createCookie(String key, String value) { @@ -65,7 +58,11 @@ private Cookie createCookie(String key, String value) { cookie.setSecure(true); cookie.setPath("/"); cookie.setHttpOnly(true); + cookie.setDomain("https://poomasi.shop"); + return cookie; } } + + diff --git a/src/main/java/poomasi/domain/auth/security/oauth2/dto/response/OAuth2KakaoResponse.java b/src/main/java/poomasi/domain/auth/security/oauth2/dto/response/OAuth2KakaoResponse.java index 12739679..410efab3 100644 --- a/src/main/java/poomasi/domain/auth/security/oauth2/dto/response/OAuth2KakaoResponse.java +++ b/src/main/java/poomasi/domain/auth/security/oauth2/dto/response/OAuth2KakaoResponse.java @@ -33,4 +33,10 @@ public LoginType getLoginType(){ return LoginType.KAKAO; } + @Override + public String getNickname(){ + String nickname = String.valueOf(((Map) attribute.get("profile")).get("nickname")); + return nickname; + } + } diff --git a/src/main/java/poomasi/domain/auth/security/oauth2/dto/response/OAuth2Response.java b/src/main/java/poomasi/domain/auth/security/oauth2/dto/response/OAuth2Response.java index 56497ac7..87331040 100644 --- a/src/main/java/poomasi/domain/auth/security/oauth2/dto/response/OAuth2Response.java +++ b/src/main/java/poomasi/domain/auth/security/oauth2/dto/response/OAuth2Response.java @@ -7,4 +7,5 @@ public interface OAuth2Response { String getProviderId(); String getEmail(); String getName(); + String getNickname(); } diff --git a/src/main/java/poomasi/domain/auth/security/userdetail/OAuth2UserDetailServiceImpl.java b/src/main/java/poomasi/domain/auth/security/userdetail/OAuth2UserDetailServiceImpl.java index e3923435..8dc479a1 100644 --- a/src/main/java/poomasi/domain/auth/security/userdetail/OAuth2UserDetailServiceImpl.java +++ b/src/main/java/poomasi/domain/auth/security/userdetail/OAuth2UserDetailServiceImpl.java @@ -1,6 +1,5 @@ package poomasi.domain.auth.security.userdetail; -import jdk.jfr.Description; import lombok.extern.slf4j.Slf4j; import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; @@ -9,9 +8,9 @@ import org.springframework.stereotype.Service; import poomasi.domain.auth.security.oauth2.dto.response.OAuth2KakaoResponse; import poomasi.domain.auth.security.oauth2.dto.response.OAuth2Response; +import poomasi.domain.member._profile.entity.MemberProfile; import poomasi.domain.member.entity.LoginType; import poomasi.domain.member.entity.Member; -import poomasi.domain.member._profile.entity.MemberProfile; import poomasi.domain.member.entity.Role; import poomasi.domain.member.repository.MemberRepository; @@ -46,7 +45,9 @@ public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2Authentic // 정보 추출 String providerId = oAuth2UserInfo.getProviderId(); + String nickName = oAuth2UserInfo.getNickname(); String email = oAuth2UserInfo.getEmail(); + Role role = Role.ROLE_CUSTOMER; LoginType loginType = oAuth2UserInfo.getLoginType(); @@ -59,6 +60,7 @@ public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2Authentic .loginType(loginType) .provideId(providerId) .memberProfile(new MemberProfile()) + .name(nickName) .build(); memberRepository.save(member); diff --git a/src/main/java/poomasi/domain/auth/security/userdetail/UserDetailsServiceImpl.java b/src/main/java/poomasi/domain/auth/security/userdetail/UserDetailsServiceImpl.java index 86e2ff7b..2408468a 100644 --- a/src/main/java/poomasi/domain/auth/security/userdetail/UserDetailsServiceImpl.java +++ b/src/main/java/poomasi/domain/auth/security/userdetail/UserDetailsServiceImpl.java @@ -13,6 +13,7 @@ public class UserDetailsServiceImpl implements UserDetailsService { private final MemberRepository memberRepository; + public UserDetailsServiceImpl(MemberRepository memberRepository) { this.memberRepository = memberRepository; } diff --git a/src/main/java/poomasi/domain/auth/token/blacklist/config/TokenBlacklistServiceConfig.java b/src/main/java/poomasi/domain/auth/token/blacklist/config/TokenBlacklistServiceConfig.java index 458c3702..9358a882 100644 --- a/src/main/java/poomasi/domain/auth/token/blacklist/config/TokenBlacklistServiceConfig.java +++ b/src/main/java/poomasi/domain/auth/token/blacklist/config/TokenBlacklistServiceConfig.java @@ -4,8 +4,8 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import poomasi.domain.auth.token.blacklist.service.BlacklistJpaService; -import poomasi.domain.auth.token.blacklist.service.TokenBlacklistService; import poomasi.domain.auth.token.blacklist.service.BlacklistRedisService; +import poomasi.domain.auth.token.blacklist.service.TokenBlacklistService; @Configuration public class TokenBlacklistServiceConfig { diff --git a/src/main/java/poomasi/domain/auth/token/blacklist/repository/BlacklistRepository.java b/src/main/java/poomasi/domain/auth/token/blacklist/repository/BlacklistRepository.java index 2d3684f8..505c6735 100644 --- a/src/main/java/poomasi/domain/auth/token/blacklist/repository/BlacklistRepository.java +++ b/src/main/java/poomasi/domain/auth/token/blacklist/repository/BlacklistRepository.java @@ -13,5 +13,4 @@ public interface BlacklistRepository extends JpaRepository { Optional findByTokenKeyAndExpireAtAfter(String key, LocalDateTime now); boolean existsByTokenKeyAndExpireAtAfter(String key, LocalDateTime now); void deleteAllByExpireAtBefore(LocalDateTime now); - } \ No newline at end of file diff --git a/src/main/java/poomasi/domain/auth/token/blacklist/service/AccessTokenBlacklistService.java b/src/main/java/poomasi/domain/auth/token/blacklist/service/AccessTokenBlacklistService.java new file mode 100644 index 00000000..00131ea6 --- /dev/null +++ b/src/main/java/poomasi/domain/auth/token/blacklist/service/AccessTokenBlacklistService.java @@ -0,0 +1,46 @@ +package poomasi.domain.auth.token.blacklist.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.Duration; +import java.util.Optional; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class AccessTokenBlacklistService { + + private final TokenBlacklistService tokenBlacklistService; + + @Value("${jwt.access-token-expiration-time}") + private long ACCESS_TOKEN_EXPIRE_TIME; + + @Transactional + public void putAccessToken(final String accessToken, Long memberId) { + tokenBlacklistService.setBlackList(accessToken, memberId.toString(), Duration.ofSeconds(ACCESS_TOKEN_EXPIRE_TIME)); + } + + public Optional getMemberIdByAccessToken(final String accessToken) { + Optional memberIdInBlacklist = tokenBlacklistService.getBlackList(accessToken); + return memberIdInBlacklist.map(id -> { + try { + return Long.valueOf(id); + } catch (NumberFormatException e) { + return null; + } + }); + } + + @Transactional + public void deleteAccessToken(String accessToken) { + tokenBlacklistService.deleteBlackList(accessToken); + } + + public boolean hasAccessToken(String accessToken) { + return tokenBlacklistService.hasKeyBlackList(accessToken); + } + +} \ No newline at end of file diff --git a/src/main/java/poomasi/domain/auth/token/blacklist/service/BlacklistRedisService.java b/src/main/java/poomasi/domain/auth/token/blacklist/service/BlacklistRedisService.java index 369bcbf5..9845462d 100644 --- a/src/main/java/poomasi/domain/auth/token/blacklist/service/BlacklistRedisService.java +++ b/src/main/java/poomasi/domain/auth/token/blacklist/service/BlacklistRedisService.java @@ -1,18 +1,16 @@ package poomasi.domain.auth.token.blacklist.service; import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.ValueOperations; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.Duration; -import java.util.*; +import java.util.Optional; import static poomasi.global.config.redis.error.RedisExceptionHandler.handleRedisException; -@Slf4j @Service @RequiredArgsConstructor @Transactional(readOnly = true) diff --git a/src/main/java/poomasi/domain/auth/token/refreshtoken/config/TokenStorageServiceConfig.java b/src/main/java/poomasi/domain/auth/token/refreshtoken/config/TokenStorageServiceConfig.java deleted file mode 100644 index 75458385..00000000 --- a/src/main/java/poomasi/domain/auth/token/refreshtoken/config/TokenStorageServiceConfig.java +++ /dev/null @@ -1,24 +0,0 @@ -package poomasi.domain.auth.token.refreshtoken.config; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import poomasi.domain.auth.token.refreshtoken.service.TokenJpaService; -import poomasi.domain.auth.token.refreshtoken.service.TokenRedisService; -import poomasi.domain.auth.token.refreshtoken.service.TokenStorageService; - -@Configuration -public class TokenStorageServiceConfig { - - @Value("${spring.token.storage.type}") - private String tokenStorageType; - - @Bean - public TokenStorageService tokenStorageService(TokenRedisService tokenRedisService, TokenJpaService tokenJpaService) { - if ("redis".equals(tokenStorageType)) { - return tokenRedisService; - } else { - return tokenJpaService; - } - } -} \ No newline at end of file diff --git a/src/main/java/poomasi/domain/auth/token/refreshtoken/repository/TokenRepository.java b/src/main/java/poomasi/domain/auth/token/refreshtoken/repository/TokenRepository.java deleted file mode 100644 index 86062443..00000000 --- a/src/main/java/poomasi/domain/auth/token/refreshtoken/repository/TokenRepository.java +++ /dev/null @@ -1,15 +0,0 @@ -package poomasi.domain.auth.token.refreshtoken.repository; - -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; -import poomasi.domain.auth.token.refreshtoken.entity.RefreshToken; - -import java.time.LocalDateTime; -import java.util.Optional; - -@Repository -public interface TokenRepository extends JpaRepository { - void deleteAllByData(String Data); - void deleteAllByExpireAtBefore(LocalDateTime now); - Optional findByTokenKeyAndExpireAtAfter(String key, LocalDateTime now); -} \ No newline at end of file diff --git a/src/main/java/poomasi/domain/auth/token/refreshtoken/service/RefreshTokenService.java b/src/main/java/poomasi/domain/auth/token/refreshtoken/service/RefreshTokenService.java deleted file mode 100644 index 5673353a..00000000 --- a/src/main/java/poomasi/domain/auth/token/refreshtoken/service/RefreshTokenService.java +++ /dev/null @@ -1,38 +0,0 @@ -package poomasi.domain.auth.token.refreshtoken.service; - -import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import poomasi.global.error.BusinessException; - -import java.time.Duration; - -import static poomasi.global.error.BusinessError.REFRESH_TOKEN_NOT_FOUND; - -@Service -@Transactional(readOnly = true) -@RequiredArgsConstructor -public class RefreshTokenService { - - private final TokenStorageService tokenStorageService; - - @Value("${jwt.refresh-token-expiration-time}") - private long REFRESH_TOKEN_EXPIRE_TIME; - - @Transactional - public void putRefreshToken(final String refreshToken, Long memberId) { - tokenStorageService.setValues(refreshToken, memberId.toString(), Duration.ofSeconds(REFRESH_TOKEN_EXPIRE_TIME)); - } - - public Long getRefreshToken(final String refreshToken, Long memberId) { - String result = tokenStorageService.getValues(refreshToken, memberId.toString()) - .orElseThrow(() -> new BusinessException(REFRESH_TOKEN_NOT_FOUND)); - return Long.parseLong(result); - } - - @Transactional - public void removeMemberRefreshToken(final Long memberId) { - tokenStorageService.removeRefreshTokenById(memberId); - } -} \ No newline at end of file diff --git a/src/main/java/poomasi/domain/auth/token/reissue/controller/ReissueTokenController.java b/src/main/java/poomasi/domain/auth/token/reissue/controller/ReissueTokenController.java index 24f80ba0..451c7385 100644 --- a/src/main/java/poomasi/domain/auth/token/reissue/controller/ReissueTokenController.java +++ b/src/main/java/poomasi/domain/auth/token/reissue/controller/ReissueTokenController.java @@ -1,11 +1,9 @@ package poomasi.domain.auth.token.reissue.controller; import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; import poomasi.domain.auth.token.reissue.dto.ReissueRequest; import poomasi.domain.auth.token.reissue.dto.ReissueResponse; @@ -18,12 +16,8 @@ public class ReissueTokenController { private final ReissueTokenService reissueTokenService; @PostMapping("/api/reissue") - public ResponseEntity reissue(@RequestHeader(HttpHeaders.AUTHORIZATION) String authorizationHeader, - @RequestBody ReissueRequest reissueRequest){ - - String accessToken = authorizationHeader.replace("Bearer ", ""); - - return ResponseEntity.ok(reissueTokenService.reissueToken(accessToken, reissueRequest)); + public ResponseEntity reissue(@RequestBody ReissueRequest reissueRequest){ + return ResponseEntity.ok(reissueTokenService.reissueToken(reissueRequest)); } } diff --git a/src/main/java/poomasi/domain/auth/token/reissue/dto/ReissueRequest.java b/src/main/java/poomasi/domain/auth/token/reissue/dto/ReissueRequest.java index c18fb929..66d8944a 100644 --- a/src/main/java/poomasi/domain/auth/token/reissue/dto/ReissueRequest.java +++ b/src/main/java/poomasi/domain/auth/token/reissue/dto/ReissueRequest.java @@ -1,4 +1,4 @@ package poomasi.domain.auth.token.reissue.dto; public record ReissueRequest(String refreshToken) { -} +} \ No newline at end of file diff --git a/src/main/java/poomasi/domain/auth/token/reissue/dto/ReissueResponse.java b/src/main/java/poomasi/domain/auth/token/reissue/dto/ReissueResponse.java index 258ce50d..ebb7caaa 100644 --- a/src/main/java/poomasi/domain/auth/token/reissue/dto/ReissueResponse.java +++ b/src/main/java/poomasi/domain/auth/token/reissue/dto/ReissueResponse.java @@ -1,4 +1,4 @@ package poomasi.domain.auth.token.reissue.dto; public record ReissueResponse(String accessToken, String refreshToken) { -} +} \ No newline at end of file diff --git a/src/main/java/poomasi/domain/auth/token/reissue/service/ReissueTokenService.java b/src/main/java/poomasi/domain/auth/token/reissue/service/ReissueTokenService.java index b0b21182..99020a2f 100644 --- a/src/main/java/poomasi/domain/auth/token/reissue/service/ReissueTokenService.java +++ b/src/main/java/poomasi/domain/auth/token/reissue/service/ReissueTokenService.java @@ -3,42 +3,38 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import poomasi.domain.auth.token.reissue.dto.ReissueRequest; -import poomasi.domain.auth.token.refreshtoken.service.RefreshTokenService; +import poomasi.domain.auth.token.reissue.dto.ReissueRequest; +import poomasi.domain.auth.token.whitelist.service.RefreshTokenWhitelistService; import poomasi.domain.auth.token.reissue.dto.ReissueResponse; -import poomasi.global.error.BusinessException; import poomasi.domain.auth.token.util.JwtUtil; +import poomasi.global.error.BusinessException; -import static poomasi.global.error.BusinessError.*; +import static poomasi.global.error.BusinessError.REFRESH_TOKEN_NOT_VALID; @Service @RequiredArgsConstructor public class ReissueTokenService { private final JwtUtil jwtUtil; - private final RefreshTokenService refreshTokenService; + private final RefreshTokenWhitelistService refreshTokenWhitelistService; // 토큰 재발급 - public ReissueResponse reissueToken(String accessToken, ReissueRequest reissueRequest) { - Long memberId = jwtUtil.getIdFromToken(accessToken); + public ReissueResponse reissueToken(ReissueRequest reissueRequest) { String refreshToken = reissueRequest.refreshToken(); Long requestMemberId = jwtUtil.getIdFromToken(refreshToken); - if (!requestMemberId.equals(memberId)) { - throw new BusinessException(REFRESH_TOKEN_NOT_VALID); - } - - checkRefreshToken(refreshToken, memberId); + checkRefreshToken(refreshToken, requestMemberId); - return getTokenResponse(memberId); + return getTokenResponse(requestMemberId); } public ReissueResponse getTokenResponse(Long memberId) { String newAccessToken = jwtUtil.generateAccessTokenById(memberId); - refreshTokenService.removeMemberRefreshToken(memberId); + refreshTokenWhitelistService.removeMemberRefreshToken(memberId); String newRefreshToken = jwtUtil.generateRefreshTokenById(memberId); - refreshTokenService.putRefreshToken(newRefreshToken, memberId); + refreshTokenWhitelistService.putRefreshToken(newRefreshToken, memberId); return new ReissueResponse(newAccessToken, newRefreshToken); } @@ -47,4 +43,4 @@ private void checkRefreshToken(final String refreshToken, Long memberId) { if(!jwtUtil.validateRefreshToken(refreshToken, memberId)) throw new BusinessException(REFRESH_TOKEN_NOT_VALID); } -} +} \ No newline at end of file diff --git a/src/main/java/poomasi/domain/auth/token/util/JwtUtil.java b/src/main/java/poomasi/domain/auth/token/util/JwtUtil.java index 397046d6..d22d6afc 100644 --- a/src/main/java/poomasi/domain/auth/token/util/JwtUtil.java +++ b/src/main/java/poomasi/domain/auth/token/util/JwtUtil.java @@ -7,8 +7,8 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; -import poomasi.domain.auth.token.blacklist.service.TokenBlacklistService; -import poomasi.domain.auth.token.refreshtoken.service.TokenStorageService; +import poomasi.domain.auth.token.blacklist.service.AccessTokenBlacklistService; +import poomasi.domain.auth.token.whitelist.service.RefreshTokenWhitelistService; import poomasi.domain.member.entity.Member; import poomasi.domain.member.service.MemberService; @@ -37,8 +37,8 @@ public class JwtUtil { @Value("${jwt.refresh-token-expiration-time}") private long REFRESH_TOKEN_EXPIRATION_TIME; - private final TokenBlacklistService tokenBlacklistService; - private final TokenStorageService tokenStorageService; + private final AccessTokenBlacklistService accessTokenBlacklistService; + private final RefreshTokenWhitelistService refreshTokenWhitelistService; private final MemberService memberService; @PostConstruct @@ -46,54 +46,8 @@ public void init() { secretKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); } - - public String generateTokenInFilter(String email, String role , String tokenType, Long memberId){ - Map claims = this.createClaimsInFilter(email, role, tokenType); - String memberIdString = memberId.toString(); - - return Jwts.builder() - .setClaims(claims) - .setSubject(memberIdString) - .setIssuedAt(new Date(System.currentTimeMillis())) - .setExpiration(new Date(System.currentTimeMillis() + ACCESS_TOKEN_EXPIRATION_TIME)) - .signWith(secretKey, SignatureAlgorithm.HS256) - .compact(); - } - - private Map createClaimsInFilter(String email, String role, String tokenType) { - Map claims = new HashMap<>(); - claims.put("email", email); - claims.put("role", role); - claims.put("tokenType" , tokenType); - return claims; - } - - public Boolean validateTokenInFilter(String token){ - - log.info("jwt util에서 토큰 검증을 진행합니다 . ."); - - try { - Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token); - return true; - } catch (Exception e) { - log.info("jwt util에서 토큰 검증 하다가 exception 터졌습니다."); - log.info(e.getMessage()); - return false; - } - - } - - public String getRoleFromTokenInFilter(final String token) { - return getClaimFromToken(token, "role", String.class); - } - - public String getEmailFromTokenInFilter(final String token) { - return getClaimFromToken(token, "email", String.class); - } - - public String generateAccessTokenById(final Long memberId) { - Map claims = createClaims(memberId); + Map claims = createClaims(memberId); //id, email, role claims.put("type", ACCESS); return Jwts.builder() .setClaims(claims) @@ -132,6 +86,11 @@ public Long getIdFromToken(final String token) { return getClaimFromToken(token, "id", Long.class); } + public String getEmailFromToken(final String token){ + return getClaimFromToken(token, "email", String.class); + + } + public Date getExpirationDateFromToken(final String token) { return getAllClaimsFromToken(token).getExpiration(); } @@ -154,10 +113,11 @@ public Boolean validateRefreshToken(final String refreshToken, final Long member if (!validateToken(refreshToken)) { return false; } - String storedMemberId = tokenStorageService.getValues(refreshToken, memberId.toString()) + + Long storedMemberId = refreshTokenWhitelistService.getMemberIdByRefreshToken(refreshToken, memberId) .orElse(null); - if (storedMemberId == null || !storedMemberId.equals(memberId.toString())) { + if (storedMemberId == null || !storedMemberId.equals(memberId)) { log.warn("리프레시 토큰과 멤버 ID가 일치하지 않습니다."); return false; } @@ -169,7 +129,8 @@ public Boolean validateAccessToken(final String accessToken){ if (!validateToken(accessToken)) { return false; } - if ( tokenBlacklistService.hasKeyBlackList(accessToken)){ + + if(accessTokenBlacklistService.hasAccessToken(accessToken)) { log.warn("로그아웃한 JWT token입니다."); return false; } @@ -208,7 +169,4 @@ public boolean isTokenExpired(String token) { } } - public long getAccessTokenExpiration() { - return ACCESS_TOKEN_EXPIRATION_TIME; - } } \ No newline at end of file diff --git a/src/main/java/poomasi/domain/auth/token/util/TokenCleanupScheduler.java b/src/main/java/poomasi/domain/auth/token/util/TokenCleanupScheduler.java index fb918879..0db34c8b 100644 --- a/src/main/java/poomasi/domain/auth/token/util/TokenCleanupScheduler.java +++ b/src/main/java/poomasi/domain/auth/token/util/TokenCleanupScheduler.java @@ -5,13 +5,13 @@ import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import poomasi.domain.auth.token.blacklist.service.BlacklistJpaService; -import poomasi.domain.auth.token.refreshtoken.service.TokenJpaService; +import poomasi.domain.auth.token.whitelist.service.WhitelistJpaService; @Component @RequiredArgsConstructor public class TokenCleanupScheduler { private final BlacklistJpaService blacklistJpaService; - private final TokenJpaService tokenJpaService; + private final WhitelistJpaService whitelistJpaService; // spring.token.blacklist.type이 "jpa"일 때만 실행 @Scheduled(fixedRate = 3600000) // 한 시간마다 실행 (1시간 = 3600000 밀리초) @@ -24,6 +24,6 @@ public void cleanUpBlacklistExpiredTokens() { @Scheduled(fixedRate = 86400000) // 하루마다 실행 (24시간 = 86400000 밀리초) @ConditionalOnProperty(name = "spring.token.storage.type", havingValue = "jpa") public void cleanUpTokenExpiredTokens() { - tokenJpaService.removeExpiredTokens(); + whitelistJpaService.removeExpiredTokens(); } } diff --git a/src/main/java/poomasi/domain/auth/token/whitelist/config/TokenWhitelistServiceConfig.java b/src/main/java/poomasi/domain/auth/token/whitelist/config/TokenWhitelistServiceConfig.java new file mode 100644 index 00000000..c6d1c769 --- /dev/null +++ b/src/main/java/poomasi/domain/auth/token/whitelist/config/TokenWhitelistServiceConfig.java @@ -0,0 +1,24 @@ +package poomasi.domain.auth.token.whitelist.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import poomasi.domain.auth.token.whitelist.service.WhitelistJpaService; +import poomasi.domain.auth.token.whitelist.service.WhitelistRedisService; +import poomasi.domain.auth.token.whitelist.service.TokenWhitelistService; + +@Configuration +public class TokenWhitelistServiceConfig { + + @Value("${spring.token.storage.type}") + private String tokenStorageType; + + @Bean + public TokenWhitelistService tokenWhitelistService(WhitelistRedisService whitelistRedisService, WhitelistJpaService whitelistJpaService) { + if ("redis".equals(tokenStorageType)) { + return whitelistRedisService; + } else { + return whitelistJpaService; + } + } +} \ No newline at end of file diff --git a/src/main/java/poomasi/domain/auth/token/refreshtoken/entity/RefreshToken.java b/src/main/java/poomasi/domain/auth/token/whitelist/entity/Whitelist.java similarity index 70% rename from src/main/java/poomasi/domain/auth/token/refreshtoken/entity/RefreshToken.java rename to src/main/java/poomasi/domain/auth/token/whitelist/entity/Whitelist.java index 211f178e..60860efb 100644 --- a/src/main/java/poomasi/domain/auth/token/refreshtoken/entity/RefreshToken.java +++ b/src/main/java/poomasi/domain/auth/token/whitelist/entity/Whitelist.java @@ -1,7 +1,10 @@ -package poomasi.domain.auth.token.refreshtoken.entity; +package poomasi.domain.auth.token.whitelist.entity; import jakarta.persistence.*; -import lombok.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; import java.time.LocalDateTime; @@ -11,7 +14,7 @@ @Setter @NoArgsConstructor @AllArgsConstructor -public class RefreshToken { +public class Whitelist { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; diff --git a/src/main/java/poomasi/domain/auth/token/whitelist/repository/WhitelistRepository.java b/src/main/java/poomasi/domain/auth/token/whitelist/repository/WhitelistRepository.java new file mode 100644 index 00000000..5f160b0d --- /dev/null +++ b/src/main/java/poomasi/domain/auth/token/whitelist/repository/WhitelistRepository.java @@ -0,0 +1,15 @@ +package poomasi.domain.auth.token.whitelist.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import poomasi.domain.auth.token.whitelist.entity.Whitelist; + +import java.time.LocalDateTime; +import java.util.Optional; + +@Repository +public interface WhitelistRepository extends JpaRepository { + void deleteAllByData(String Data); + void deleteAllByExpireAtBefore(LocalDateTime now); + Optional findByTokenKeyAndExpireAtAfter(String key, LocalDateTime now); +} \ No newline at end of file diff --git a/src/main/java/poomasi/domain/auth/token/whitelist/service/RefreshTokenWhitelistService.java b/src/main/java/poomasi/domain/auth/token/whitelist/service/RefreshTokenWhitelistService.java new file mode 100644 index 00000000..c0afeac5 --- /dev/null +++ b/src/main/java/poomasi/domain/auth/token/whitelist/service/RefreshTokenWhitelistService.java @@ -0,0 +1,41 @@ +package poomasi.domain.auth.token.whitelist.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.Duration; +import java.util.Optional; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class RefreshTokenWhitelistService { + + private final TokenWhitelistService tokenWhitelistService; + + @Value("${jwt.refresh-token-expiration-time}") + private long REFRESH_TOKEN_EXPIRE_TIME; + + @Transactional + public void putRefreshToken(final String refreshToken, Long memberId) { + tokenWhitelistService.setValues(refreshToken, memberId.toString(), Duration.ofSeconds(REFRESH_TOKEN_EXPIRE_TIME)); + } + + public Optional getMemberIdByRefreshToken(final String refreshToken, Long memberId) { + Optional memberIdInWhitelist = tokenWhitelistService.getValues(refreshToken, memberId.toString()); + return memberIdInWhitelist.map(id -> { + try { + return Long.valueOf(id); + } catch (NumberFormatException e) { + return null; + } + }); + } + + @Transactional + public void removeMemberRefreshToken(final Long memberId) { + tokenWhitelistService.removeRefreshTokenById(memberId); + } +} \ No newline at end of file diff --git a/src/main/java/poomasi/domain/auth/token/refreshtoken/service/TokenStorageService.java b/src/main/java/poomasi/domain/auth/token/whitelist/service/TokenWhitelistService.java similarity index 75% rename from src/main/java/poomasi/domain/auth/token/refreshtoken/service/TokenStorageService.java rename to src/main/java/poomasi/domain/auth/token/whitelist/service/TokenWhitelistService.java index 1b170b72..f444b186 100644 --- a/src/main/java/poomasi/domain/auth/token/refreshtoken/service/TokenStorageService.java +++ b/src/main/java/poomasi/domain/auth/token/whitelist/service/TokenWhitelistService.java @@ -1,4 +1,4 @@ -package poomasi.domain.auth.token.refreshtoken.service; +package poomasi.domain.auth.token.whitelist.service; import org.springframework.stereotype.Service; @@ -6,7 +6,7 @@ import java.util.Optional; @Service -public interface TokenStorageService { +public interface TokenWhitelistService { void setValues(String key, String data, Duration duration); Optional getValues(String key, String data); void removeRefreshTokenById(final Long memberId); diff --git a/src/main/java/poomasi/domain/auth/token/refreshtoken/service/TokenJpaService.java b/src/main/java/poomasi/domain/auth/token/whitelist/service/WhitelistJpaService.java similarity index 54% rename from src/main/java/poomasi/domain/auth/token/refreshtoken/service/TokenJpaService.java rename to src/main/java/poomasi/domain/auth/token/whitelist/service/WhitelistJpaService.java index 5e5e628f..119fd15c 100644 --- a/src/main/java/poomasi/domain/auth/token/refreshtoken/service/TokenJpaService.java +++ b/src/main/java/poomasi/domain/auth/token/whitelist/service/WhitelistJpaService.java @@ -1,10 +1,10 @@ -package poomasi.domain.auth.token.refreshtoken.service; +package poomasi.domain.auth.token.whitelist.service; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import poomasi.domain.auth.token.refreshtoken.entity.RefreshToken; -import poomasi.domain.auth.token.refreshtoken.repository.TokenRepository; +import poomasi.domain.auth.token.whitelist.entity.Whitelist; +import poomasi.domain.auth.token.whitelist.repository.WhitelistRepository; import java.time.Duration; import java.time.LocalDateTime; @@ -13,34 +13,34 @@ @Service @RequiredArgsConstructor @Transactional(readOnly = true) -public class TokenJpaService implements TokenStorageService { +public class WhitelistJpaService implements TokenWhitelistService { - private final TokenRepository tokenRepository; + private final WhitelistRepository whitelistRepository; @Override @Transactional public void setValues(String key, String data, Duration duration) { - RefreshToken tokenEntity = new RefreshToken(); + Whitelist tokenEntity = new Whitelist(); tokenEntity.setTokenKey(key); tokenEntity.setData(data); tokenEntity.setExpireAt(LocalDateTime.now().plusSeconds(duration.getSeconds())); - tokenRepository.save(tokenEntity); + whitelistRepository.save(tokenEntity); } @Override public Optional getValues(String key, String data) { - return tokenRepository.findByTokenKeyAndExpireAtAfter(key, LocalDateTime.now()) - .map(RefreshToken::getData); + return whitelistRepository.findByTokenKeyAndExpireAtAfter(key, LocalDateTime.now()) + .map(Whitelist::getData); } @Override @Transactional public void removeRefreshTokenById(final Long memberId) { - tokenRepository.deleteAllByData(String.valueOf(memberId)); + whitelistRepository.deleteAllByData(String.valueOf(memberId)); } @Transactional public void removeExpiredTokens() { - tokenRepository.deleteAllByExpireAtBefore(LocalDateTime.now()); + whitelistRepository.deleteAllByExpireAtBefore(LocalDateTime.now()); } } diff --git a/src/main/java/poomasi/domain/auth/token/refreshtoken/service/TokenRedisService.java b/src/main/java/poomasi/domain/auth/token/whitelist/service/WhitelistRedisService.java similarity index 96% rename from src/main/java/poomasi/domain/auth/token/refreshtoken/service/TokenRedisService.java rename to src/main/java/poomasi/domain/auth/token/whitelist/service/WhitelistRedisService.java index 96916a4d..e0f91690 100644 --- a/src/main/java/poomasi/domain/auth/token/refreshtoken/service/TokenRedisService.java +++ b/src/main/java/poomasi/domain/auth/token/whitelist/service/WhitelistRedisService.java @@ -1,4 +1,4 @@ -package poomasi.domain.auth.token.refreshtoken.service; +package poomasi.domain.auth.token.whitelist.service; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -21,7 +21,7 @@ @Service @RequiredArgsConstructor @Transactional(readOnly = true) -public class TokenRedisService implements TokenStorageService { +public class WhitelistRedisService implements TokenWhitelistService { private final RedisTemplate redisTemplate; private final RedisConnectionFactory redisConnectionFactory; diff --git a/src/main/java/poomasi/domain/farm/_category/controller/FarmCategoryController.java b/src/main/java/poomasi/domain/farm/_category/controller/FarmCategoryController.java new file mode 100644 index 00000000..7d0a18ed --- /dev/null +++ b/src/main/java/poomasi/domain/farm/_category/controller/FarmCategoryController.java @@ -0,0 +1,27 @@ +package poomasi.domain.farm._category.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import poomasi.domain.farm._category.dto.response.FarmCategoryResponse; +import poomasi.domain.farm._category.service.FarmCategoryService; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/farm/category") +public class FarmCategoryController { + private final FarmCategoryService farmCategoryService; + + @GetMapping + public ResponseEntity> getFarmCategories() { + return ResponseEntity.ok( + farmCategoryService.getFarmCategories().stream() + .map(FarmCategoryResponse::toResponse) + .toList() + ); + } +} diff --git a/src/main/java/poomasi/domain/farm/_category/domain/FarmCategory.java b/src/main/java/poomasi/domain/farm/_category/domain/FarmCategory.java new file mode 100644 index 00000000..90afd8d8 --- /dev/null +++ b/src/main/java/poomasi/domain/farm/_category/domain/FarmCategory.java @@ -0,0 +1,22 @@ +package poomasi.domain.farm._category.domain; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.Getter; +import org.hibernate.annotations.Comment; + +@Entity +@Getter +public class FarmCategory { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Comment("농장 카테고리 이름") + private String name; + + @Comment("농장 카테고리 이미지 URL") + private String imageUrl; +} diff --git a/src/main/java/poomasi/domain/farm/_category/dto/response/FarmCategoryResponse.java b/src/main/java/poomasi/domain/farm/_category/dto/response/FarmCategoryResponse.java new file mode 100644 index 00000000..d0411d15 --- /dev/null +++ b/src/main/java/poomasi/domain/farm/_category/dto/response/FarmCategoryResponse.java @@ -0,0 +1,19 @@ +package poomasi.domain.farm._category.dto.response; + +import lombok.Builder; +import poomasi.domain.farm._category.domain.FarmCategory; + +@Builder +public record FarmCategoryResponse( + Long id, + String name, + String imageUrl +) { + public static FarmCategoryResponse toResponse(FarmCategory farmCategory) { + return FarmCategoryResponse.builder() + .id(farmCategory.getId()) + .name(farmCategory.getName()) + .imageUrl(farmCategory.getImageUrl()) + .build(); + } +} diff --git a/src/main/java/poomasi/domain/farm/_category/repository/FarmCategoryRepository.java b/src/main/java/poomasi/domain/farm/_category/repository/FarmCategoryRepository.java new file mode 100644 index 00000000..593620a6 --- /dev/null +++ b/src/main/java/poomasi/domain/farm/_category/repository/FarmCategoryRepository.java @@ -0,0 +1,9 @@ +package poomasi.domain.farm._category.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import poomasi.domain.farm._category.domain.FarmCategory; + +@Repository +public interface FarmCategoryRepository extends JpaRepository { +} diff --git a/src/main/java/poomasi/domain/farm/_category/service/FarmCategoryService.java b/src/main/java/poomasi/domain/farm/_category/service/FarmCategoryService.java new file mode 100644 index 00000000..745aafc6 --- /dev/null +++ b/src/main/java/poomasi/domain/farm/_category/service/FarmCategoryService.java @@ -0,0 +1,18 @@ +package poomasi.domain.farm._category.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import poomasi.domain.farm._category.domain.FarmCategory; +import poomasi.domain.farm._category.repository.FarmCategoryRepository; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class FarmCategoryService { + private final FarmCategoryRepository farmCategoryRepository; + + public List getFarmCategories() { + return farmCategoryRepository.findAll(); + } +} diff --git a/src/main/java/poomasi/domain/farm/_schedule/controller/FarmScheduleFarmerController.java b/src/main/java/poomasi/domain/farm/_schedule/controller/FarmScheduleFarmerController.java index b9b0fe8d..d503f34e 100644 --- a/src/main/java/poomasi/domain/farm/_schedule/controller/FarmScheduleFarmerController.java +++ b/src/main/java/poomasi/domain/farm/_schedule/controller/FarmScheduleFarmerController.java @@ -16,7 +16,7 @@ @RestController @RequiredArgsConstructor -@RequestMapping("/api/farm") +@RequestMapping("/api/farmer/farms") public class FarmScheduleFarmerController { private final FarmScheduleService farmScheduleService; diff --git a/src/main/java/poomasi/domain/farm/_schedule/dto/FarmScheduleResponse.java b/src/main/java/poomasi/domain/farm/_schedule/dto/FarmScheduleResponse.java index 0c3dc8e3..7c8d55ae 100644 --- a/src/main/java/poomasi/domain/farm/_schedule/dto/FarmScheduleResponse.java +++ b/src/main/java/poomasi/domain/farm/_schedule/dto/FarmScheduleResponse.java @@ -1,11 +1,9 @@ package poomasi.domain.farm._schedule.dto; -import java.time.LocalDate; - import lombok.Builder; import poomasi.domain.farm._schedule.entity.FarmSchedule; -import poomasi.domain.farm._schedule.entity.ScheduleStatus; +import java.time.LocalDate; import java.time.LocalTime; @Builder @@ -13,8 +11,7 @@ public record FarmScheduleResponse( Long scheduleId, LocalDate date, LocalTime startTime, - LocalTime endTime, - ScheduleStatus status + LocalTime endTime ) { public static FarmScheduleResponse fromEntity(FarmSchedule farmSchedule) { diff --git a/src/main/java/poomasi/domain/farm/_schedule/dto/FarmScheduleUpdateRequest.java b/src/main/java/poomasi/domain/farm/_schedule/dto/FarmScheduleUpdateRequest.java index a754b305..b50c9a91 100644 --- a/src/main/java/poomasi/domain/farm/_schedule/dto/FarmScheduleUpdateRequest.java +++ b/src/main/java/poomasi/domain/farm/_schedule/dto/FarmScheduleUpdateRequest.java @@ -2,7 +2,6 @@ import jakarta.validation.constraints.NotNull; import poomasi.domain.farm._schedule.entity.FarmSchedule; -import poomasi.domain.farm._schedule.entity.ScheduleStatus; import java.time.LocalDate; import java.time.LocalTime; @@ -22,7 +21,6 @@ public FarmSchedule toEntity() { .startTime(startTime) .endTime(endTime) .date(date) - .status(ScheduleStatus.PENDING) .build(); } } diff --git a/src/main/java/poomasi/domain/farm/_schedule/entity/FarmSchedule.java b/src/main/java/poomasi/domain/farm/_schedule/entity/FarmSchedule.java index 19d1a497..d3de57e0 100644 --- a/src/main/java/poomasi/domain/farm/_schedule/entity/FarmSchedule.java +++ b/src/main/java/poomasi/domain/farm/_schedule/entity/FarmSchedule.java @@ -32,7 +32,8 @@ public class FarmSchedule { private LocalTime endTime; @Builder - public FarmSchedule(Long farmId, LocalDate date, LocalTime startTime, LocalTime endTime, ScheduleStatus status) { + public FarmSchedule(Long id, Long farmId, LocalDate date, LocalTime startTime, LocalTime endTime) { + this.id = id; this.farmId = farmId; this.date = date; this.startTime = startTime; diff --git a/src/main/java/poomasi/domain/farm/_schedule/entity/ScheduleStatus.java b/src/main/java/poomasi/domain/farm/_schedule/entity/ScheduleStatus.java deleted file mode 100644 index dae5b8ae..00000000 --- a/src/main/java/poomasi/domain/farm/_schedule/entity/ScheduleStatus.java +++ /dev/null @@ -1,15 +0,0 @@ -package poomasi.domain.farm._schedule.entity; - -public enum ScheduleStatus { - PENDING, - RESERVED, - ; - - public boolean isAvailable() { - return this == PENDING; - } - - public boolean isReserved() { - return this == RESERVED; - } -} diff --git a/src/main/java/poomasi/domain/farm/_schedule/repository/FarmScheduleRepository.java b/src/main/java/poomasi/domain/farm/_schedule/repository/FarmScheduleRepository.java index 77238e1c..a65b9487 100644 --- a/src/main/java/poomasi/domain/farm/_schedule/repository/FarmScheduleRepository.java +++ b/src/main/java/poomasi/domain/farm/_schedule/repository/FarmScheduleRepository.java @@ -15,4 +15,5 @@ public interface FarmScheduleRepository extends JpaRepository findByFarmIdAndDate(Long aLong, LocalDate date); + List findByFarmId(Long farmId); } diff --git a/src/main/java/poomasi/domain/farm/_schedule/service/FarmScheduleService.java b/src/main/java/poomasi/domain/farm/_schedule/service/FarmScheduleService.java index dd4fa041..322e9da4 100644 --- a/src/main/java/poomasi/domain/farm/_schedule/service/FarmScheduleService.java +++ b/src/main/java/poomasi/domain/farm/_schedule/service/FarmScheduleService.java @@ -10,6 +10,8 @@ import poomasi.global.error.BusinessException; import java.time.LocalDate; +import java.time.LocalTime; +import java.util.Collection; import java.util.List; import static poomasi.global.error.BusinessError.*; @@ -19,6 +21,28 @@ public class FarmScheduleService { private final FarmScheduleRepository farmScheduleRepository; + public void addFarmSchedule(LocalDate startDate, LocalDate endDate, LocalTime startTime, LocalTime endTime, Long farmId) { + List farmSchedules = farmScheduleRepository.findByFarmIdAndDateRange(farmId, startDate, endDate); + + // 같은 날짜에 동일한 시간에 스케줄이 있는지 확인 + for (FarmSchedule farmSchedule : farmSchedules) { + if (startTime.isBefore(farmSchedule.getEndTime()) && endTime.isAfter(farmSchedule.getStartTime())) { + throw new BusinessException(FARM_SCHEDULE_ALREADY_EXISTS); + } + } + // 등록 + for (LocalDate date = startDate; date.isBefore(endDate.plusDays(1)); date = date.plusDays(1)) { + FarmSchedule farmSchedule = FarmSchedule.builder() + .farmId(farmId) + .date(date) + .startTime(startTime) + .endTime(endTime) + .build(); + + farmScheduleRepository.save(farmSchedule); + } + } + public void addFarmSchedule(FarmScheduleUpdateRequest request) { if (request.startTime().isAfter(request.endTime())) { throw new BusinessException(START_TIME_SHOULD_BE_BEFORE_END_TIME); @@ -55,4 +79,8 @@ public FarmSchedule getFarmScheduleByScheduleId(Long id) { () -> new BusinessException(FARM_SCHEDULE_NOT_FOUND) ); } + + public List getFarmScheduleByFarmId(Long farmId) { + return farmScheduleRepository.findByFarmId(farmId); + } } diff --git a/src/main/java/poomasi/domain/farm/controller/FarmController.java b/src/main/java/poomasi/domain/farm/controller/FarmController.java index f1763f5f..191c645b 100644 --- a/src/main/java/poomasi/domain/farm/controller/FarmController.java +++ b/src/main/java/poomasi/domain/farm/controller/FarmController.java @@ -1,5 +1,6 @@ package poomasi.domain.farm.controller; +import jdk.jfr.Description; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Pageable; import org.springframework.http.ResponseEntity; @@ -7,21 +8,31 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import poomasi.domain.farm.dto.response.FarmDetailResponse; +import poomasi.domain.farm.dto.response.FarmResponse; import poomasi.domain.farm.service.FarmPlatformService; @RestController @RequiredArgsConstructor -@RequestMapping("/api/farm") +@RequestMapping("/api/farms") +@Description("인증이 필요 없는 Farm 메소드") public class FarmController { private final FarmPlatformService farmPlatformService; + @Description("Farm 단건 조회") @GetMapping("/{farmId}") - public ResponseEntity getFarm(@PathVariable Long farmId) { + public ResponseEntity getFarm(@PathVariable Long farmId) { return ResponseEntity.ok(farmPlatformService.getFarmByFarmId(farmId)); } - @GetMapping("") + @Description("Farm 상세 조회") + @GetMapping("/{farmId}/detail") + public ResponseEntity getFarmDetail(@PathVariable Long farmId) { + return ResponseEntity.ok(farmPlatformService.getFarmDetailByFarmId(farmId)); + } + + @GetMapping("Farm 다건 조회") public ResponseEntity getFarmList(Pageable pageable) { return ResponseEntity.ok(farmPlatformService.getFarmList(pageable)); } diff --git a/src/main/java/poomasi/domain/farm/controller/FarmFarmerController.java b/src/main/java/poomasi/domain/farm/controller/FarmFarmerController.java index 753bb064..5d3b2082 100644 --- a/src/main/java/poomasi/domain/farm/controller/FarmFarmerController.java +++ b/src/main/java/poomasi/domain/farm/controller/FarmFarmerController.java @@ -7,30 +7,37 @@ import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import poomasi.domain.auth.security.userdetail.UserDetailsImpl; -import poomasi.domain.farm.dto.FarmRegisterRequest; -import poomasi.domain.farm.dto.FarmUpdateRequest; +import poomasi.domain.farm.dto.request.FarmInfoUpdateRequest; +import poomasi.domain.farm.dto.request.FarmRegisterRequest; +import poomasi.domain.farm.dto.request.FarmUpdateRequest; import poomasi.domain.farm.service.FarmFarmerService; -import poomasi.domain.farm._schedule.service.FarmScheduleService; import poomasi.domain.member.entity.Member; @RestController @RequiredArgsConstructor -@RequestMapping("/api/farm") +@RequestMapping("/api/farmer/farms") public class FarmFarmerController { private final FarmFarmerService farmFarmerService; - private final FarmScheduleService farmScheduleService; - @Secured("ROLE_FARMER") @PostMapping("") public ResponseEntity registerFarm( @AuthenticationPrincipal UserDetailsImpl userDetails, - @RequestBody FarmRegisterRequest request) { + @Valid @RequestBody FarmRegisterRequest request) { Member member = userDetails.getMember(); return ResponseEntity.ok(farmFarmerService.registerFarm(member, request)); + } + @Secured("ROLE_FARMER") + @PostMapping("/info/update") + public ResponseEntity updateFarmInfo( + @AuthenticationPrincipal UserDetailsImpl userDetails, + @Valid @RequestBody FarmInfoUpdateRequest request) { + Member member = userDetails.getMember(); + return ResponseEntity.ok(farmFarmerService.updateFarmInfo(member, request)); } + @Secured("ROLE_FARMER") @PostMapping("/update") public ResponseEntity updateFarm( diff --git a/src/main/java/poomasi/domain/farm/dto/FarmRegisterRequest.java b/src/main/java/poomasi/domain/farm/dto/FarmRegisterRequest.java deleted file mode 100644 index 2a046509..00000000 --- a/src/main/java/poomasi/domain/farm/dto/FarmRegisterRequest.java +++ /dev/null @@ -1,31 +0,0 @@ -package poomasi.domain.farm.dto; - -import poomasi.domain.farm.entity.Farm; - -public record FarmRegisterRequest( - String name, - String address, - String addressDetail, - Double latitude, - Double longitude, - String phoneNumber, - String description, - int experiencePrice, - Integer maxCapacity, - Integer maxReservation -) { - public Farm toEntity(Long memberId) { - return Farm.builder() - .name(name) - .ownerId(memberId) - .address(address) - .addressDetail(addressDetail) - .latitude(latitude) - .longitude(longitude) - .description(description) - .experiencePrice(experiencePrice) - .maxCapacity(maxCapacity) - .maxReservation(maxReservation) - .build(); - } -} diff --git a/src/main/java/poomasi/domain/farm/dto/FarmResponse.java b/src/main/java/poomasi/domain/farm/dto/FarmResponse.java deleted file mode 100644 index 7b7db5cb..00000000 --- a/src/main/java/poomasi/domain/farm/dto/FarmResponse.java +++ /dev/null @@ -1,28 +0,0 @@ -package poomasi.domain.farm.dto; - -import poomasi.domain.farm.entity.Farm; - - -public record FarmResponse( - Long id, - String name, - String address, - String addressDetail, - Double latitude, - Double longitude, - String description, - int experiencePrice -) { - public static FarmResponse fromEntity(Farm farm) { - return new FarmResponse( - farm.getId(), - farm.getName(), - farm.getAddress(), - farm.getAddressDetail(), - farm.getLatitude(), - farm.getLongitude(), - farm.getDescription(), - farm.getExperiencePrice() - ); - } -} diff --git a/src/main/java/poomasi/domain/farm/dto/request/FarmInfoAggregateRequest.java b/src/main/java/poomasi/domain/farm/dto/request/FarmInfoAggregateRequest.java new file mode 100644 index 00000000..bc8865fb --- /dev/null +++ b/src/main/java/poomasi/domain/farm/dto/request/FarmInfoAggregateRequest.java @@ -0,0 +1,69 @@ +package poomasi.domain.farm.dto.request; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Builder; +import poomasi.global.error.BusinessException; + +import java.util.ArrayList; +import java.util.List; + +import static poomasi.global.error.BusinessError.FARM_INFO_DETAIL_SIZE_MISMATCH; + +@Builder +public record FarmInfoAggregateRequest( + @NotBlank(message = "제목은 필수 입력값입니다.") + String title, + + @NotBlank(message = "대표 이미지는 필수 입력값입니다.") + String mainImage, + + @NotNull(message = "세부 제목 리스트는 null일 수 없습니다.") + @Size(min = 3, max = 3, message = "세부 제목은 3개여야 합니다.") + List detailTitles, + + @NotNull(message = "세부 설명 리스트는 null일 수 없습니다.") + List detailDescriptions, + + @NotNull(message = "세부 이미지 리스트는 null일 수 없습니다.") + List detailImages +) { + public List toRequest() { + validateListsSize(); + + List requests = new ArrayList<>(); + + requests.add(new FarmInfoRegisterRequest( + true, + title, + null, + mainImage + )); + + // Add detailed farm info requests + for (int i = 0; i < detailTitles.size(); i++) { + String detailTitle = detailTitles.get(i); + String detailDescription = (i < detailDescriptions.size()) ? detailDescriptions.get(i) : null; + String detailImage = (i < detailImages.size()) ? detailImages.get(i) : null; + + requests.add(new FarmInfoRegisterRequest( + false, + detailTitle, + detailDescription, + detailImage + )); + } + + return requests; + } + + private void validateListsSize() { + if (detailDescriptions.size() != detailTitles.size()) { + throw new BusinessException(FARM_INFO_DETAIL_SIZE_MISMATCH); + } + if (detailImages.size() != detailTitles.size()) { + throw new BusinessException(FARM_INFO_DETAIL_SIZE_MISMATCH); + } + } +} diff --git a/src/main/java/poomasi/domain/farm/dto/request/FarmInfoRegisterRequest.java b/src/main/java/poomasi/domain/farm/dto/request/FarmInfoRegisterRequest.java new file mode 100644 index 00000000..41818473 --- /dev/null +++ b/src/main/java/poomasi/domain/farm/dto/request/FarmInfoRegisterRequest.java @@ -0,0 +1,22 @@ +package poomasi.domain.farm.dto.request; + +import jakarta.validation.constraints.NotNull; +import poomasi.domain.farm.entity.FarmInfo; + +public record FarmInfoRegisterRequest( + @NotNull + boolean isMain, + String title, + String content, + String imageUrl +) { + public FarmInfo toEntity(Long id) { + return FarmInfo.builder() + .farmId(id) + .isMain(isMain) + .title(title) + .content(content) + .imageUrl(imageUrl) + .build(); + } +} diff --git a/src/main/java/poomasi/domain/farm/dto/request/FarmInfoUpdateRequest.java b/src/main/java/poomasi/domain/farm/dto/request/FarmInfoUpdateRequest.java new file mode 100644 index 00000000..11641705 --- /dev/null +++ b/src/main/java/poomasi/domain/farm/dto/request/FarmInfoUpdateRequest.java @@ -0,0 +1,9 @@ +package poomasi.domain.farm.dto.request; + +public record FarmInfoUpdateRequest( + Long farmId, + String title, + String content, + String imageUrl +) { +} diff --git a/src/main/java/poomasi/domain/farm/dto/request/FarmRegisterRequest.java b/src/main/java/poomasi/domain/farm/dto/request/FarmRegisterRequest.java new file mode 100644 index 00000000..cb58a974 --- /dev/null +++ b/src/main/java/poomasi/domain/farm/dto/request/FarmRegisterRequest.java @@ -0,0 +1,77 @@ +package poomasi.domain.farm.dto.request; + +import jakarta.validation.constraints.NotNull; +import lombok.Builder; +import poomasi.domain.farm.entity.Farm; + +import java.time.LocalDate; +import java.time.LocalTime; + +@Builder +public record FarmRegisterRequest( + @NotNull + String name, + @NotNull + String address, + @NotNull + String growEnv, + String addressDetail, + @NotNull + Double latitude, + @NotNull + Double longitude, + @NotNull + String phoneNumber, + + String description, + @NotNull + int experiencePrice, + @NotNull + Integer maxPeople, + @NotNull + Integer maxTeam, + @NotNull + Long categoryId, + @NotNull + String imageUrl, + @NotNull + int price, + + @NotNull + LocalDate startDate, + + @NotNull + LocalDate endDate, + + @NotNull + LocalTime startTime, + + @NotNull + LocalTime endTime, + + @NotNull + String businessNumber, + + FarmInfoAggregateRequest info +) { + public Farm toEntity(Long memberId) { + return Farm.builder() + .name(name) + .ownerId(memberId) + .address(address) + .addressDetail(addressDetail) + .latitude(latitude) + .longitude(longitude) + .description(description) + .experiencePrice(experiencePrice) + .maxCapacity(maxPeople) + .maxReservation(maxTeam) + .categoryId(categoryId) + .phoneNumber(phoneNumber) + .mainImage(imageUrl) + .growEnv(growEnv) + .experiencePrice(price) + .businessNumber(businessNumber) + .build(); + } +} diff --git a/src/main/java/poomasi/domain/farm/dto/FarmUpdateRequest.java b/src/main/java/poomasi/domain/farm/dto/request/FarmUpdateRequest.java similarity index 67% rename from src/main/java/poomasi/domain/farm/dto/FarmUpdateRequest.java rename to src/main/java/poomasi/domain/farm/dto/request/FarmUpdateRequest.java index 23e5ebbe..f1a64fed 100644 --- a/src/main/java/poomasi/domain/farm/dto/FarmUpdateRequest.java +++ b/src/main/java/poomasi/domain/farm/dto/request/FarmUpdateRequest.java @@ -1,10 +1,8 @@ -package poomasi.domain.farm.dto; +package poomasi.domain.farm.dto.request; -import jakarta.validation.constraints.NotNull; import poomasi.domain.farm.entity.Farm; public record FarmUpdateRequest( - @NotNull(message = "Farm ID는 필수 값입니다.") Long farmId, String name, String description, String address, diff --git a/src/main/java/poomasi/domain/farm/dto/response/FarmDetailResponse.java b/src/main/java/poomasi/domain/farm/dto/response/FarmDetailResponse.java new file mode 100644 index 00000000..0efa8770 --- /dev/null +++ b/src/main/java/poomasi/domain/farm/dto/response/FarmDetailResponse.java @@ -0,0 +1,16 @@ +package poomasi.domain.farm.dto.response; + +import lombok.Builder; +import poomasi.domain.farm._schedule.dto.FarmScheduleResponse; + +import java.util.List; + +@Builder +public record FarmDetailResponse( + FarmResponse farmResponse, + FarmInfoAggregateResponse info, + List schedules +) { + + +} diff --git a/src/main/java/poomasi/domain/farm/dto/response/FarmInfoAggregateResponse.java b/src/main/java/poomasi/domain/farm/dto/response/FarmInfoAggregateResponse.java new file mode 100644 index 00000000..e2a33e3d --- /dev/null +++ b/src/main/java/poomasi/domain/farm/dto/response/FarmInfoAggregateResponse.java @@ -0,0 +1,36 @@ +package poomasi.domain.farm.dto.response; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import poomasi.domain.farm.entity.FarmInfo; + +import java.util.List; + +public record FarmInfoAggregateResponse( + @NotBlank(message = "제목은 필수 입력값입니다.") + String title, + + @NotBlank(message = "대표 이미지는 필수 입력값입니다.") + String mainImage, + + @NotNull(message = "세부 제목 리스트는 null일 수 없습니다.") + @Size(min = 3, max = 3, message = "세부 제목은 3개여야 합니다.") + List detailTitles, + + @NotNull(message = "세부 설명 리스트는 null일 수 없습니다.") + List detailDescriptions, + + @NotNull(message = "세부 이미지 리스트는 null일 수 없습니다.") + List detailImages +) { + public static FarmInfoAggregateResponse fromEntity(List farmInfos) { + return new FarmInfoAggregateResponse( + farmInfos.get(0).getTitle(), + farmInfos.get(0).getImageUrl(), + List.of(farmInfos.get(1).getTitle(), farmInfos.get(2).getTitle(), farmInfos.get(3).getTitle()), + List.of(farmInfos.get(1).getContent(), farmInfos.get(2).getContent(), farmInfos.get(3).getContent()), + List.of(farmInfos.get(1).getImageUrl(), farmInfos.get(2).getImageUrl(), farmInfos.get(3).getImageUrl()) + ); + } +} diff --git a/src/main/java/poomasi/domain/farm/dto/response/FarmInfoResponse.java b/src/main/java/poomasi/domain/farm/dto/response/FarmInfoResponse.java new file mode 100644 index 00000000..70d0b54d --- /dev/null +++ b/src/main/java/poomasi/domain/farm/dto/response/FarmInfoResponse.java @@ -0,0 +1,22 @@ +package poomasi.domain.farm.dto.response; + +import poomasi.domain.farm.entity.FarmInfo; + +public record FarmInfoResponse( + Long id, + String imageUrl, + String title, + String content, + boolean isMain +) { + + public static FarmInfoResponse fromEntity(FarmInfo farmInfo) { + return new FarmInfoResponse( + farmInfo.getId(), + farmInfo.getImageUrl(), + farmInfo.getTitle(), + farmInfo.getContent(), + farmInfo.isMain() + ); + } +} diff --git a/src/main/java/poomasi/domain/farm/dto/response/FarmResponse.java b/src/main/java/poomasi/domain/farm/dto/response/FarmResponse.java new file mode 100644 index 00000000..4155e245 --- /dev/null +++ b/src/main/java/poomasi/domain/farm/dto/response/FarmResponse.java @@ -0,0 +1,32 @@ +package poomasi.domain.farm.dto.response; + +import lombok.Builder; +import poomasi.domain.farm.entity.Farm; + +@Builder +public record FarmResponse( + Long id, + String name, + String address, + String addressDetail, + Double latitude, + Double longitude, + String description, + int experiencePrice, + double averageRating +) { + + public static FarmResponse fromEntity(Farm farm) { + return FarmResponse + .builder() + .name(farm.getName()) + .address(farm.getAddress()) + .addressDetail(farm.getAddressDetail()) + .latitude(farm.getLatitude()) + .longitude(farm.getLongitude()) + .description(farm.getDescription()) + .experiencePrice(farm.getExperiencePrice().intValue()) + .averageRating(farm.getAverageRating()) + .build(); + } +} diff --git a/src/main/java/poomasi/domain/farm/entity/Farm.java b/src/main/java/poomasi/domain/farm/entity/Farm.java index 9e278c2d..fcc561ce 100644 --- a/src/main/java/poomasi/domain/farm/entity/Farm.java +++ b/src/main/java/poomasi/domain/farm/entity/Farm.java @@ -1,7 +1,22 @@ package poomasi.domain.farm.entity; import jakarta.persistence.*; - +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; + +import java.math.BigDecimal; +import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; @@ -13,12 +28,12 @@ import org.hibernate.annotations.CreationTimestamp; import org.hibernate.annotations.SQLDelete; import org.hibernate.annotations.UpdateTimestamp; -import poomasi.domain.farm.dto.FarmUpdateRequest; +import poomasi.domain.farm.dto.request.FarmUpdateRequest; +import poomasi.domain.review.entity.Review; import java.time.LocalDateTime; - -import poomasi.domain.order.entity._farm.OrderedFarm; -import poomasi.domain.review.entity.Review; +import java.util.ArrayList; +import java.util.List; @Entity @Getter @@ -30,39 +45,60 @@ public class Farm { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @Column(nullable = false) private String name; - // FIXME: owner_id는 Member의 id를 참조해야 합니다. @Comment("농장 소유자 ID") - @Column(name = "owner_id") + @Column(name = "owner_id", nullable = false) private Long ownerId; + @Comment("사업자 등록 번호") + private String businessNumber; + @Comment("농장 간단 설명") private String description; @Comment("도로명 주소") + @Column(nullable = false) private String address; @Comment("상세 주소") private String addressDetail; @Comment("위도") + @Column(nullable = false) private Double latitude; @Comment("경도") + @Column(nullable = false) private Double longitude; + @Comment("농장 대표 이미지") + @Column(nullable = false) + private String mainImage; + + @Comment("재배 환경") + @Column(nullable = false) + private String growEnv; + @Comment("농장 상태") @Enumerated(EnumType.STRING) private FarmStatus status = FarmStatus.OPEN; + @Comment("카테고리 ID") + @Column(name = "category_id", nullable = false) + private Long categoryId; + @Comment("체험 비용") - private int experiencePrice; + @Column(nullable = false) + private BigDecimal experiencePrice; @Comment("팀 최대 인원") + @Column(nullable = false) private Integer maxCapacity; @Comment("동일 시간대 최대 예약 가능 팀 수") + @Column(nullable = false) private Integer maxReservation; @Comment("삭제 일시") @@ -76,16 +112,17 @@ public class Farm { @UpdateTimestamp private LocalDateTime updatedAt = LocalDateTime.now(); + @Column(name = "phone_number") + private String phoneNumber; + @OneToMany(cascade = CascadeType.REMOVE, orphanRemoval = true) @JoinColumn(name = "entityId") private List reviewList = new ArrayList<>(); - @OneToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "ordered_farm_id") - private OrderedFarm orderedFarm; + private double averageRating; @Builder - public Farm(Long id, String name, Long ownerId, String address, String addressDetail, Double latitude, Double longitude, String description, int experiencePrice, Integer maxCapacity, Integer maxReservation, LocalDateTime deletedAt) { + public Farm(Long id, String name, Long ownerId, String address, String addressDetail, Double latitude, Double longitude, String description, int experiencePrice, Integer maxCapacity, Integer maxReservation, String businessNumber, LocalDateTime deletedAt, Long categoryId, String phoneNumber, String mainImage, String growEnv) { this.id = id; this.name = name; this.ownerId = ownerId; @@ -94,10 +131,16 @@ public Farm(Long id, String name, Long ownerId, String address, String addressDe this.latitude = latitude; this.longitude = longitude; this.description = description; - this.experiencePrice = experiencePrice; + this.experiencePrice = new BigDecimal(experiencePrice); this.maxCapacity = maxCapacity; this.maxReservation = maxReservation; + this.businessNumber = businessNumber; this.deletedAt = deletedAt; + this.categoryId = categoryId; + this.phoneNumber = phoneNumber; + averageRating = 0.0f; + this.mainImage = mainImage; + this.growEnv = growEnv; } public Farm updateFarm(FarmUpdateRequest farmUpdateRequest) { @@ -111,7 +154,7 @@ public Farm updateFarm(FarmUpdateRequest farmUpdateRequest) { } public void updateExpPrice(int expPrice) { - this.experiencePrice = expPrice; + this.experiencePrice = new BigDecimal(expPrice); } public void updateMaxCapacity(Integer maxCapacity) { @@ -125,4 +168,12 @@ public void updateMaxReservation(Integer maxReservation) { public void delete() { this.deletedAt = LocalDateTime.now(); } + + public void addReview(Review review) { + this.reviewList.add(review); + this.averageRating = reviewList.stream() + .mapToDouble(Review::getRating) + .average() + .orElse(0.0f); + } } diff --git a/src/main/java/poomasi/domain/farm/entity/FarmInfo.java b/src/main/java/poomasi/domain/farm/entity/FarmInfo.java new file mode 100644 index 00000000..657ed124 --- /dev/null +++ b/src/main/java/poomasi/domain/farm/entity/FarmInfo.java @@ -0,0 +1,77 @@ +package poomasi.domain.farm.entity; + +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.Comment; +import org.hibernate.annotations.CurrentTimestamp; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.SQLSelect; +import poomasi.domain.farm.dto.request.FarmInfoUpdateRequest; + +import java.time.LocalDateTime; + +@Entity +@Getter +@Table(name = "farm_info") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@SQLDelete(sql = "UPDATE farm_info SET deleted_at = CURRENT_TIMESTAMP WHERE id = ?") +@SQLSelect(sql = "SELECT * FROM farm_info WHERE deleted_at IS NULL") +public class FarmInfo { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + Long id; + + @Comment("농장 ID") + @Column(nullable = false) + Long farmId; + + @Comment("이미지") + @Column(nullable = false) + String imageUrl; + + @Comment(value = "제목") + String title; + + @Comment("설명") + @Column(nullable = true) + String content; + + @Comment("메인 이미지 여부") + @Column(nullable = false) + boolean isMain; + + @Comment("생성일") + @Column(nullable = false) + @CurrentTimestamp + LocalDateTime createdAt; + + @Comment("삭제 일시") + LocalDateTime deletedAt; + + @Builder + public FarmInfo(Long farmId, String imageUrl, String title, String content, boolean isMain) { + this.farmId = farmId; + this.imageUrl = imageUrl; + this.title = title; + this.content = content; + this.isMain = isMain; + } + + public boolean isValid() { + return imageUrl != null && content != null && deletedAt == null && title != null; + } + + public boolean hasContent() { + return content != null && !content.isBlank() && !content.isEmpty() && + title != null && !title.isBlank() && !title.isEmpty(); + } + + public void update(FarmInfoUpdateRequest request) { + this.imageUrl = request.imageUrl(); + this.title = request.title(); + this.content = request.content(); + } +} diff --git a/src/main/java/poomasi/domain/farm/repository/FarmInfoRepository.java b/src/main/java/poomasi/domain/farm/repository/FarmInfoRepository.java new file mode 100644 index 00000000..092f3168 --- /dev/null +++ b/src/main/java/poomasi/domain/farm/repository/FarmInfoRepository.java @@ -0,0 +1,14 @@ +package poomasi.domain.farm.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import poomasi.domain.farm.entity.FarmInfo; + +import java.util.List; + +@Repository +public interface FarmInfoRepository extends JpaRepository { + List findAllByFarmId(Long farmId); + + void deleteAllByFarmId(Long farmId); +} diff --git a/src/main/java/poomasi/domain/farm/service/FarmFarmerService.java b/src/main/java/poomasi/domain/farm/service/FarmFarmerService.java index fe12e1b3..32e73108 100644 --- a/src/main/java/poomasi/domain/farm/service/FarmFarmerService.java +++ b/src/main/java/poomasi/domain/farm/service/FarmFarmerService.java @@ -2,32 +2,76 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import poomasi.domain.farm.dto.FarmRegisterRequest; -import poomasi.domain.farm.dto.FarmUpdateRequest; +import org.springframework.transaction.annotation.Transactional; +import poomasi.domain.farm._schedule.service.FarmScheduleService; +import poomasi.domain.farm.dto.request.FarmInfoRegisterRequest; +import poomasi.domain.farm.dto.request.FarmInfoUpdateRequest; +import poomasi.domain.farm.dto.request.FarmRegisterRequest; +import poomasi.domain.farm.dto.request.FarmUpdateRequest; import poomasi.domain.farm.entity.Farm; +import poomasi.domain.farm.entity.FarmInfo; import poomasi.domain.farm.repository.FarmRepository; import poomasi.domain.member.entity.Member; import poomasi.global.error.BusinessException; +import java.util.List; + import static poomasi.global.error.BusinessError.*; @Service @RequiredArgsConstructor +@Transactional(readOnly = true) public class FarmFarmerService { private final FarmRepository farmRepository; + private final FarmService farmService; + private final FarmInfoService farmInfoService; + private final FarmScheduleService farmScheduleService; - public Long registerFarm(Member member, FarmRegisterRequest request) { + private final int MAX_FARM_INFO_COUNT = 4; + @Transactional + public Long registerFarm(Member member, FarmRegisterRequest request) { farmRepository.getFarmByOwnerIdAndDeletedAtIsNull(member.getId()).ifPresent(farm -> { throw new BusinessException(FARM_ALREADY_EXISTS); }); - return farmRepository.save(request.toEntity(member.getId())).getId(); + Long id = farmRepository.save(request.toEntity(member.getId())).getId(); + + List farmInfoRegisterRequests = request.info().toRequest(); + farmInfoRegisterRequests.forEach(farmInfoRegisterRequest -> { + registerFarmInfo(member, farmInfoRegisterRequest); + }); + + farmScheduleService.addFarmSchedule(request.startDate(), request.endDate(), request.startTime(), request.endTime(), id); + + return id; + } + + + @Transactional + public void registerFarmInfo(Member member, FarmInfoRegisterRequest request) { + Farm farm = farmService.getFarmByFarmerId(member.getId()); + + List farmInfos = farmInfoService.getFarmInfoByFarmId(farm.getId()).stream() + .filter(FarmInfo::isValid) + .toList(); + + if (farmInfos.size() > MAX_FARM_INFO_COUNT) { + throw new BusinessException(FARM_INFO_LIMIT_EXCEEDED); + } + + if (request.isMain() && farmInfos.stream().anyMatch(FarmInfo::isMain)) { + throw new BusinessException(FARM_INFO_MAIN_ALREADY_EXISTS); + } else if (!request.isMain() && farmInfos.stream().noneMatch(FarmInfo::isMain) && farmInfos.size() == MAX_FARM_INFO_COUNT - 1) { + throw new BusinessException(FARM_INFO_MAIN_REQUIRED); + } + farmInfoService.saveFarmInfo(request.toEntity(farm.getId())); } + @Transactional public Long updateFarm(Long farmerId, FarmUpdateRequest request) { - Farm farm = this.getFarmByFarmId(request.farmId()); + Farm farm = farmService.getFarmByFarmerId(farmerId); if (!farm.getOwnerId().equals(farmerId)) { throw new BusinessException(FARM_OWNER_MISMATCH); @@ -36,12 +80,9 @@ public Long updateFarm(Long farmerId, FarmUpdateRequest request) { return farmRepository.save(request.toEntity(farm)).getId(); } - private Farm getFarmByFarmId(Long farmId) { - return farmRepository.findByIdAndDeletedAtIsNull(farmId).orElseThrow(() -> new BusinessException(FARM_NOT_FOUND)); - } public void updateFarmExpPrice(Long farmerId, Long farmId, int expPrice) { - Farm farm = this.getFarmByFarmId(farmId); + Farm farm = farmService.getFarmByFarmId(farmId); if (!farm.getOwnerId().equals(farmerId)) { throw new BusinessException(FARM_OWNER_MISMATCH); } @@ -49,7 +90,7 @@ public void updateFarmExpPrice(Long farmerId, Long farmId, int expPrice) { } public void updateFarmMaxCapacity(Long farmerId, Long farmId, Integer maxCapacity) { - Farm farm = this.getFarmByFarmId(farmId); + Farm farm = farmService.getFarmByFarmId(farmId); if (!farm.getOwnerId().equals(farmerId)) { throw new BusinessException(FARM_OWNER_MISMATCH); } @@ -57,7 +98,7 @@ public void updateFarmMaxCapacity(Long farmerId, Long farmId, Integer maxCapacit } public void updateFarmMaxReservation(Long farmerId, Long farmId, Integer maxReservation) { - Farm farm = this.getFarmByFarmId(farmId); + Farm farm = farmService.getFarmByFarmId(farmId); if (!farm.getOwnerId().equals(farmerId)) { throw new BusinessException(FARM_OWNER_MISMATCH); } @@ -65,10 +106,34 @@ public void updateFarmMaxReservation(Long farmerId, Long farmId, Integer maxRese } public void deleteFarm(Long farmerId, Long farmId) { - Farm farm = this.getFarmByFarmId(farmId); + Farm farm = farmService.getFarmByFarmId(farmId); + + // farm이 null인 경우 예외 발생 + if (farm == null) { + throw new BusinessException(FARM_NOT_FOUND); + } + if (!farm.getOwnerId().equals(farmerId)) { throw new BusinessException(FARM_OWNER_MISMATCH); } - farmRepository.delete(farm); + + if (farm.getDeletedAt() != null) { + throw new BusinessException(FARM_ALREADY_DELETED); + } + + farmService.delete(farm); + farmInfoService.deleteFarmInfo(farmId); + } + + public Long updateFarmInfo(Member member, FarmInfoUpdateRequest request) { + Farm farm = farmService.getFarmByFarmerId(member.getId()); + + if (!farm.getOwnerId().equals(member.getId())) { + throw new BusinessException(FARM_OWNER_MISMATCH); + } + + FarmInfo farmInfo = farmInfoService.getFarmInfo(request.farmId()); + farmInfo.update(request); + return farmInfoService.saveFarmInfo(farmInfo); } } diff --git a/src/main/java/poomasi/domain/farm/service/FarmInfoService.java b/src/main/java/poomasi/domain/farm/service/FarmInfoService.java new file mode 100644 index 00000000..6a1840a6 --- /dev/null +++ b/src/main/java/poomasi/domain/farm/service/FarmInfoService.java @@ -0,0 +1,50 @@ +package poomasi.domain.farm.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import poomasi.domain.farm.entity.FarmInfo; +import poomasi.domain.farm.repository.FarmInfoRepository; +import poomasi.global.error.BusinessException; + +import java.util.Collection; +import java.util.List; + +import static poomasi.global.error.BusinessError.FARM_INFO_MAIN_REQUIRED_NO_CONTENT; +import static poomasi.global.error.BusinessError.FARM_INFO_NON_MAIN_REQUIRED_CONTENT; + +@Service +@RequiredArgsConstructor +public class FarmInfoService { + private final FarmInfoRepository farmInfoRepository; + + public Long saveFarmInfo(FarmInfo farmInfo) { + // 메인 사진인데 내용이 있을 경우 예외 처리 + if (farmInfo.isMain() && farmInfo.hasContent()) { + throw new BusinessException(FARM_INFO_MAIN_REQUIRED_NO_CONTENT); + } else if (!farmInfo.isMain() && !farmInfo.hasContent()) { + throw new BusinessException(FARM_INFO_NON_MAIN_REQUIRED_CONTENT); + } + + return farmInfoRepository.save(farmInfo).getId(); + } + + public FarmInfo getFarmInfo(Long farmId) { + return farmInfoRepository.findById(farmId).orElseThrow(); + } + + public List getFarmInfoByFarmId(Long farmId) { + return farmInfoRepository.findAllByFarmId(farmId); + } + + public void deleteFarmInfoById(Long farmInfoId) { + farmInfoRepository.deleteById(farmInfoId); + } + + public void deleteFarmInfo(Long farmId) { + farmInfoRepository.deleteAllByFarmId(farmId); + } + + public List getFarmSchedulesByFarmId(Long farmId) { + return farmInfoRepository.findAllByFarmId(farmId); + } +} diff --git a/src/main/java/poomasi/domain/farm/service/FarmPlatformService.java b/src/main/java/poomasi/domain/farm/service/FarmPlatformService.java index 2ace44df..754549d2 100644 --- a/src/main/java/poomasi/domain/farm/service/FarmPlatformService.java +++ b/src/main/java/poomasi/domain/farm/service/FarmPlatformService.java @@ -3,7 +3,11 @@ import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; -import poomasi.domain.farm.dto.FarmResponse; +import poomasi.domain.farm._schedule.dto.FarmScheduleResponse; +import poomasi.domain.farm._schedule.service.FarmScheduleService; +import poomasi.domain.farm.dto.response.FarmDetailResponse; +import poomasi.domain.farm.dto.response.FarmInfoAggregateResponse; +import poomasi.domain.farm.dto.response.FarmResponse; import java.util.List; import java.util.stream.Collectors; @@ -12,6 +16,18 @@ @RequiredArgsConstructor public class FarmPlatformService { private final FarmService farmService; + private final FarmInfoService farmInfoService; + private final FarmScheduleService farmScheduleService; + + public FarmDetailResponse getFarmDetailByFarmId(Long farmId) { + return FarmDetailResponse.builder() + .farmResponse(FarmResponse.fromEntity(farmService.getFarmByFarmId(farmId))) + .info(FarmInfoAggregateResponse.fromEntity(farmInfoService.getFarmInfoByFarmId(farmId))) + .schedules(farmScheduleService.getFarmScheduleByFarmId(farmId).stream() + .map(FarmScheduleResponse::fromEntity) + .collect(Collectors.toList())) + .build(); + } public FarmResponse getFarmByFarmId(Long farmId) { return FarmResponse.fromEntity(farmService.getFarmByFarmId(farmId)); @@ -24,7 +40,7 @@ public List getFarmList(Pageable pageable) { } public List getFarmsByFarmerId(Long farmerId) { - return farmService.getFarmListByOwnerId(farmerId).stream() + return farmService.getFarmListByOwnerId(farmerId).stream() .map(FarmResponse::fromEntity) .collect(Collectors.toList()); } diff --git a/src/main/java/poomasi/domain/farm/service/FarmService.java b/src/main/java/poomasi/domain/farm/service/FarmService.java index 73464751..361fa408 100644 --- a/src/main/java/poomasi/domain/farm/service/FarmService.java +++ b/src/main/java/poomasi/domain/farm/service/FarmService.java @@ -12,6 +12,8 @@ import java.util.List; import java.util.stream.Collectors; +import static poomasi.global.error.BusinessError.FARM_NOT_FOUND; + @Service @RequiredArgsConstructor public class FarmService { @@ -20,6 +22,7 @@ public class FarmService { public List getFarmListByOwnerId(Long farmerId) { return farmRepository.findAllByOwnerIdAndDeletedAtIsNull(farmerId); } + public Farm getValidFarmByFarmId(Long farmId) { Farm farm = getFarmByFarmId(farmId); if (farm.getStatus() != FarmStatus.OPEN) { @@ -28,6 +31,10 @@ public Farm getValidFarmByFarmId(Long farmId) { return farm; } + public Farm getFarmByFarmerId(Long farmerId) { + return farmRepository.getFarmByOwnerIdAndDeletedAtIsNull(farmerId).orElseThrow(() -> new BusinessException(FARM_NOT_FOUND)); + } + public Farm getFarmByFarmId(Long farmId) { return farmRepository.findByIdAndDeletedAtIsNull(farmId) .orElseThrow(() -> new BusinessException(BusinessError.FARM_NOT_FOUND)); @@ -37,4 +44,9 @@ public List getFarmList(Pageable pageable) { return farmRepository.findByDeletedAtIsNull(pageable).stream() .collect(Collectors.toList()); } + + public void delete(Farm farm) { + farm.delete(); + farmRepository.save(farm); + } } diff --git a/src/main/java/poomasi/domain/image/controller/ImageController.java b/src/main/java/poomasi/domain/image/controller/ImageController.java index 26000941..08c52a29 100644 --- a/src/main/java/poomasi/domain/image/controller/ImageController.java +++ b/src/main/java/poomasi/domain/image/controller/ImageController.java @@ -1,13 +1,14 @@ package poomasi.domain.image.controller; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.access.annotation.Secured; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import poomasi.domain.auth.security.userdetail.UserDetailsImpl; -import poomasi.domain.image.dto.ImageRequest; -import poomasi.domain.image.entity.Image; +import poomasi.domain.image.dto.request.ImageRequest; +import poomasi.domain.image.dto.response.ImageResponse; import poomasi.domain.image.entity.ImageType; import poomasi.domain.image.service.ImageService; import poomasi.domain.member.entity.Member; @@ -23,25 +24,28 @@ public class ImageController { // 이미지 정보 저장 @PostMapping @Secured({"ROLE_CUSTOMER", "ROLE_FARMER", "ROLE_ADMIN"}) - public ResponseEntity saveImageInfo(@AuthenticationPrincipal UserDetailsImpl userDetails, @RequestBody ImageRequest imageRequest) { + public ResponseEntity saveImageInfo(@AuthenticationPrincipal UserDetailsImpl userDetails, + @Valid @RequestBody ImageRequest imageRequest) { Member member = userDetails.getMember(); - Image savedImage = imageService.saveImage(member.getId(), imageRequest); - return ResponseEntity.ok(savedImage); + ImageResponse imageResponse = imageService.saveImage(member.getId(), imageRequest); + return ResponseEntity.ok(imageResponse); } // 여러 이미지 정보 저장 @PostMapping("/multiple") @Secured({"ROLE_CUSTOMER", "ROLE_FARMER", "ROLE_ADMIN"}) - public ResponseEntity> saveMultipleImages(@AuthenticationPrincipal UserDetailsImpl userDetails, @RequestBody List imageRequests) { + public ResponseEntity> saveMultipleImages(@AuthenticationPrincipal UserDetailsImpl userDetails, + @Valid @RequestBody List imageRequests) { Member member = userDetails.getMember(); - List savedImages = imageService.saveMultipleImages(member.getId(), imageRequests); - return ResponseEntity.ok(savedImages); + List imageResponses = imageService.saveMultipleImages(member.getId(), imageRequests); + return ResponseEntity.ok(imageResponses); } // 특정 이미지 삭제 @DeleteMapping("/delete/{id}") @Secured({"ROLE_CUSTOMER", "ROLE_FARMER", "ROLE_ADMIN"}) - public ResponseEntity deleteImage(@AuthenticationPrincipal UserDetailsImpl userDetails, @PathVariable Long id) { + public ResponseEntity deleteImage(@AuthenticationPrincipal UserDetailsImpl userDetails, + @PathVariable Long id) { Member member = userDetails.getMember(); imageService.deleteImage(member.getId(), id); return ResponseEntity.noContent().build(); @@ -49,31 +53,33 @@ public ResponseEntity deleteImage(@AuthenticationPrincipal UserDetailsImpl // 특정 이미지 조회 @GetMapping("/{id}") - public ResponseEntity getImage(@PathVariable Long id) { + public ResponseEntity getImage(@PathVariable Long id) { return ResponseEntity.ok(imageService.getImageById(id)); } // 모든 이미지 조회 (특정 referenceId에 따라) @GetMapping("/reference/{type}/{referenceId}") - public ResponseEntity> getImagesByTypeAndReference(@PathVariable ImageType type, @PathVariable Long referenceId) { - List images = imageService.getImagesByTypeAndReferenceId(type, referenceId); + public ResponseEntity> getImagesByTypeAndReference(@PathVariable ImageType type, + @PathVariable Long referenceId) { + List images = imageService.getImagesByTypeAndReferenceId(type, referenceId); return ResponseEntity.ok(images); } // 이미지 정보 수정 - @PutMapping("update/{id}") + @PutMapping("/update/{id}") @Secured({"ROLE_CUSTOMER", "ROLE_FARMER", "ROLE_ADMIN"}) public ResponseEntity updateImageInfo(@AuthenticationPrincipal UserDetailsImpl userDetails, @PathVariable Long id, - @RequestBody ImageRequest imageRequest) { + @Valid @RequestBody ImageRequest imageRequest) { Member member = userDetails.getMember(); - Image updatedImage = imageService.updateImage(member.getId(), id, imageRequest); + ImageResponse updatedImage = imageService.updateImage(member.getId(), id, imageRequest); return ResponseEntity.ok(updatedImage); } @PutMapping("/recover/{id}") @Secured({"ROLE_CUSTOMER", "ROLE_FARMER", "ROLE_ADMIN"}) - public ResponseEntity recoverImage(@AuthenticationPrincipal UserDetailsImpl userDetails, @PathVariable Long id) { + public ResponseEntity recoverImage(@AuthenticationPrincipal UserDetailsImpl userDetails, + @PathVariable Long id) { Member member = userDetails.getMember(); imageService.recoverImage(member.getId(), id); return ResponseEntity.noContent().build(); diff --git a/src/main/java/poomasi/domain/image/deleteLinker/ImageDeleteFactory.java b/src/main/java/poomasi/domain/image/deleteLinker/ImageDeleteFactory.java index 821b7353..d3f5a0b7 100644 --- a/src/main/java/poomasi/domain/image/deleteLinker/ImageDeleteFactory.java +++ b/src/main/java/poomasi/domain/image/deleteLinker/ImageDeleteFactory.java @@ -3,23 +3,21 @@ import org.springframework.stereotype.Component; import poomasi.domain.image.entity.ImageType; -import java.util.HashMap; -import java.util.Map; +import java.util.List; @Component public class ImageDeleteFactory { - private final Map handlerMap; + private final List deleteLinkers; - public ImageDeleteFactory( - ProductDeleteLinker productDeleteLinker, - MemberProfileDeleteLinker memberProfileDeleteLinker) { - this.handlerMap = new HashMap<>(); - handlerMap.put(ImageType.PRODUCT, productDeleteLinker); - handlerMap.put(ImageType.MEMBER_PROFILE, memberProfileDeleteLinker); + public ImageDeleteFactory(List deleteLinkers) { + this.deleteLinkers = deleteLinkers; } public ImageDeleteLinker getDeleteLinker(ImageType type) { - return handlerMap.get(type); + return deleteLinkers.stream() + .filter(linker -> linker.supports(type)) + .findFirst() + .orElse(null); } } diff --git a/src/main/java/poomasi/domain/image/deleteLinker/ImageDeleteLinker.java b/src/main/java/poomasi/domain/image/deleteLinker/ImageDeleteLinker.java index c5820c90..80f1c1cb 100644 --- a/src/main/java/poomasi/domain/image/deleteLinker/ImageDeleteLinker.java +++ b/src/main/java/poomasi/domain/image/deleteLinker/ImageDeleteLinker.java @@ -1,7 +1,9 @@ package poomasi.domain.image.deleteLinker; import poomasi.domain.image.entity.Image; +import poomasi.domain.image.entity.ImageType; public interface ImageDeleteLinker { + boolean supports(ImageType type); void handleImageDeletion(Image image); } \ No newline at end of file diff --git a/src/main/java/poomasi/domain/image/deleteLinker/MemberProfileDeleteLinker.java b/src/main/java/poomasi/domain/image/deleteLinker/MemberProfileDeleteLinker.java index 696bed0d..c5bf3f6f 100644 --- a/src/main/java/poomasi/domain/image/deleteLinker/MemberProfileDeleteLinker.java +++ b/src/main/java/poomasi/domain/image/deleteLinker/MemberProfileDeleteLinker.java @@ -2,6 +2,7 @@ import org.springframework.stereotype.Component; import poomasi.domain.image.entity.Image; +import poomasi.domain.image.entity.ImageType; import poomasi.domain.member._profile.entity.MemberProfile; import poomasi.domain.member._profile.service.MemberProfileService; @@ -14,6 +15,11 @@ public MemberProfileDeleteLinker(MemberProfileService memberProfileService) { this.memberProfileService = memberProfileService; } + @Override + public boolean supports(ImageType type) { + return type == ImageType.MEMBER_PROFILE; + } + @Override public void handleImageDeletion(Image image) { MemberProfile memberProfile = memberProfileService.getMemberProfileById(image.getReferenceId()); diff --git a/src/main/java/poomasi/domain/image/deleteLinker/ProductDeleteLinker.java b/src/main/java/poomasi/domain/image/deleteLinker/ProductDeleteLinker.java index 9453cbce..bd259c1c 100644 --- a/src/main/java/poomasi/domain/image/deleteLinker/ProductDeleteLinker.java +++ b/src/main/java/poomasi/domain/image/deleteLinker/ProductDeleteLinker.java @@ -2,6 +2,7 @@ import org.springframework.stereotype.Component; import poomasi.domain.image.entity.Image; +import poomasi.domain.image.entity.ImageType; import poomasi.domain.product.entity.Product; import poomasi.domain.product.service.ProductService; @@ -14,10 +15,15 @@ public ProductDeleteLinker(ProductService productService) { this.productService = productService; } + @Override + public boolean supports(ImageType type) { + return type == ImageType.PRODUCT; + } + @Override public void handleImageDeletion(Image image) { Product product = productService.findProductById(image.getReferenceId()); - product.setImageUrl(null); + product.getImages().remove(image); productService.saveExistedProduct(product); } } diff --git a/src/main/java/poomasi/domain/image/deleteLinker/ProductIntroDeleteLinker.java b/src/main/java/poomasi/domain/image/deleteLinker/ProductIntroDeleteLinker.java new file mode 100644 index 00000000..7694fded --- /dev/null +++ b/src/main/java/poomasi/domain/image/deleteLinker/ProductIntroDeleteLinker.java @@ -0,0 +1,45 @@ +package poomasi.domain.image.deleteLinker; + +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; +import poomasi.domain.image.entity.Image; +import poomasi.domain.image.entity.ImageType; +import poomasi.domain.product._intro.entity.ProductIntro; +import poomasi.domain.product._intro.service.ProductIntroService; + +import java.util.Arrays; +import java.util.List; + +@Component +public class ProductIntroDeleteLinker implements ImageDeleteLinker { + + private final ProductIntroService productIntroService; + + public ProductIntroDeleteLinker(ProductIntroService productIntroService) { + this.productIntroService = productIntroService; + } + + @Override + public boolean supports(ImageType type) { + return type == ImageType.PRODUCT_INTRO; + } + + @Transactional + @Override + public void handleImageDeletion(Image image) { + ProductIntro productIntro = productIntroService.getIntroByIntroId(image.getReferenceId()); + + List images = Arrays.asList(productIntro.getMainImage(), productIntro.getSubImage1(), productIntro.getSubImage2(), productIntro.getSubImage3()); + for (int i = 0; i < images.size(); i++) { + if (images.get(i) == image) { + switch (i) { + case 0 -> productIntro.setMainImage(null); + case 1 -> productIntro.setSubImage1(null); + case 2 -> productIntro.setSubImage2(null); + case 3 -> productIntro.setSubImage3(null); + } + } + } + productIntroService.saveExistedProductIntro(productIntro); + } +} \ No newline at end of file diff --git a/src/main/java/poomasi/domain/image/dto/ImageRequest.java b/src/main/java/poomasi/domain/image/dto/ImageRequest.java deleted file mode 100644 index 7589c9a0..00000000 --- a/src/main/java/poomasi/domain/image/dto/ImageRequest.java +++ /dev/null @@ -1,16 +0,0 @@ -package poomasi.domain.image.dto; - -import poomasi.domain.image.entity.Image; -import poomasi.domain.image.entity.ImageType; - -public record ImageRequest(String objectKey, String imageUrl, ImageType type, Long referenceId) { - public Image toEntity(ImageRequest imageRequest){ - return new Image( - imageRequest.objectKey, - imageRequest.imageUrl, - imageRequest.type, - imageRequest.referenceId // 타입이 멤버 프로필일 경우 멤버 id가 아닌 멤버 프로필 id를 넣습니다. - ); - } -} - diff --git a/src/main/java/poomasi/domain/image/dto/request/ImageRequest.java b/src/main/java/poomasi/domain/image/dto/request/ImageRequest.java new file mode 100644 index 00000000..d2421428 --- /dev/null +++ b/src/main/java/poomasi/domain/image/dto/request/ImageRequest.java @@ -0,0 +1,32 @@ +package poomasi.domain.image.dto.request; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; +import poomasi.domain.image.entity.Image; +import poomasi.domain.image.entity.ImageType; + +public record ImageRequest( + @NotBlank(message = "Object key이 blank입니다.") + String objectKey, + + @NotBlank(message = "Image URL이 blank입니다.") + String imageUrl, + + @NotNull(message = "Image type이 null입니다.") + ImageType type, + + @NotNull(message = "Reference ID가 null입니다.") + @Positive(message = "Reference ID는 양의 정수이어야 합니다.") + Long referenceId +) { + public Image toEntity(ImageRequest imageRequest){ + return new Image( + imageRequest.objectKey, + imageRequest.imageUrl, + imageRequest.type, + imageRequest.referenceId // 타입이 멤버 프로필일 경우 멤버 id가 아닌 멤버 프로필 id를 넣습니다. + ); + } +} + diff --git a/src/main/java/poomasi/domain/image/dto/response/ImageResponse.java b/src/main/java/poomasi/domain/image/dto/response/ImageResponse.java new file mode 100644 index 00000000..9d6b829c --- /dev/null +++ b/src/main/java/poomasi/domain/image/dto/response/ImageResponse.java @@ -0,0 +1,28 @@ +package poomasi.domain.image.dto.response; + +import poomasi.domain.image.entity.Image; +import poomasi.domain.image.entity.ImageType; + +import java.time.LocalDateTime; + +public record ImageResponse( + Long id, + String objectKey, + String imageUrl, + ImageType type, + Long referenceId, + LocalDateTime createdAt, + LocalDateTime deletedAt +) { + public static ImageResponse fromEntity(Image image) { + return new ImageResponse( + image.getId(), + image.getObjectKey(), + image.getImageUrl(), + image.getType(), + image.getReferenceId(), + image.getCreatedAt(), + image.getDeletedAt() + ); + } +} \ No newline at end of file diff --git a/src/main/java/poomasi/domain/image/entity/Image.java b/src/main/java/poomasi/domain/image/entity/Image.java index 38d70ebb..1b0d2318 100644 --- a/src/main/java/poomasi/domain/image/entity/Image.java +++ b/src/main/java/poomasi/domain/image/entity/Image.java @@ -1,16 +1,18 @@ package poomasi.domain.image.entity; import jakarta.persistence.*; -import lombok.*; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; import org.hibernate.annotations.SQLDelete; -import poomasi.domain.image.dto.ImageRequest; +import poomasi.domain.image.dto.request.ImageRequest; +import poomasi.domain.image.dto.response.ImageResponse; import java.time.LocalDateTime; @Entity -@Table(name = "image", uniqueConstraints = { - @UniqueConstraint(columnNames = {"type", "reference_id"}) -}) +@Table(name = "image") @Getter @Setter @NoArgsConstructor @@ -48,6 +50,18 @@ public Image(String objectKey, String imageUrl, ImageType type, Long referenceId this.referenceId = referenceId; } + @Builder + public Image(Long id, String objectKey, String imageUrl, ImageType type, Long referenceId, LocalDateTime createdAt, LocalDateTime deletedAt) { + this.id = id; + this.objectKey = objectKey; + this.imageUrl = imageUrl; + this.type = type; + this.referenceId = referenceId; + this.createdAt = createdAt; + this.deletedAt = deletedAt; + } + + public void update(ImageRequest request) { this.objectKey = request.objectKey(); this.imageUrl = request.imageUrl(); @@ -63,4 +77,16 @@ public ImageRequest toRequest(Image image){ image.referenceId ); } + + public static Image fromResponse(ImageResponse imageResponse) { + return new Image( + imageResponse.id(), + imageResponse.objectKey(), + imageResponse.imageUrl(), + imageResponse.type(), + imageResponse.referenceId(), + imageResponse.createdAt(), + imageResponse.deletedAt() + ); + } } \ No newline at end of file diff --git a/src/main/java/poomasi/domain/image/entity/ImageType.java b/src/main/java/poomasi/domain/image/entity/ImageType.java index 1b2b30fd..28e681d6 100644 --- a/src/main/java/poomasi/domain/image/entity/ImageType.java +++ b/src/main/java/poomasi/domain/image/entity/ImageType.java @@ -1,5 +1,5 @@ package poomasi.domain.image.entity; public enum ImageType { - FARM, FARM_REVIEW, PRODUCT, PRODUCT_REVIEW, MEMBER_PROFILE + FARM, FARM_REVIEW, PRODUCT, PRODUCT_REVIEW, MEMBER_PROFILE, PRODUCT_INTRO } \ No newline at end of file diff --git a/src/main/java/poomasi/domain/image/linker/ProductImageLinker.java b/src/main/java/poomasi/domain/image/linker/ProductImageLinker.java index fc75dac6..9fd7dcd3 100644 --- a/src/main/java/poomasi/domain/image/linker/ProductImageLinker.java +++ b/src/main/java/poomasi/domain/image/linker/ProductImageLinker.java @@ -23,7 +23,7 @@ public boolean supports(ImageType type) { @Override public void link(Long referenceId, Image savedImage) { Product product = productService.findProductById(referenceId); - product.setImageUrl(savedImage.getImageUrl()); + product.getImages().add(savedImage); productService.saveExistedProduct(product); } } \ No newline at end of file diff --git a/src/main/java/poomasi/domain/image/linker/ProductIntroImageLinker.java b/src/main/java/poomasi/domain/image/linker/ProductIntroImageLinker.java new file mode 100644 index 00000000..c5a2d086 --- /dev/null +++ b/src/main/java/poomasi/domain/image/linker/ProductIntroImageLinker.java @@ -0,0 +1,64 @@ +package poomasi.domain.image.linker; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import poomasi.domain.image.entity.Image; +import poomasi.domain.image.entity.ImageType; +import poomasi.domain.product._intro.entity.ProductIntro; +import poomasi.domain.product._intro.service.ProductIntroService; +import poomasi.global.error.BusinessException; + +import java.util.Arrays; +import java.util.List; + +import static poomasi.global.error.BusinessError.IMAGE_LIMIT_EXCEED; + +@Service +public class ProductIntroImageLinker implements ImageLinker { + + private final ProductIntroService productIntroService; + + public ProductIntroImageLinker(ProductIntroService productIntroService) { + this.productIntroService = productIntroService; + } + + @Override + public boolean supports(ImageType type) { + return type == ImageType.PRODUCT_INTRO; + } + + @Transactional + @Override + public void link(Long referenceId, Image savedImage) { + ProductIntro productIntro = productIntroService.getIntroByIntroId(referenceId); + addImageToProductIntro(productIntro, savedImage); + productIntroService.saveExistedProductIntro(productIntro); + } + + private void addImageToProductIntro(ProductIntro productIntro, Image savedImage) { + List images = Arrays.asList( + productIntro.getMainImage(), + productIntro.getSubImage1(), + productIntro.getSubImage2(), + productIntro.getSubImage3() + ); + + for (int i = 0; i < images.size(); i++) { + if (images.get(i) == null) { + setImageByIndex(productIntro, i, savedImage); + return; + } + } + + throw new BusinessException(IMAGE_LIMIT_EXCEED); + } + + private void setImageByIndex(ProductIntro productIntro, int index, Image image) { + switch (index) { + case 0 -> productIntro.setMainImage(image); + case 1 -> productIntro.setSubImage1(image); + case 2 -> productIntro.setSubImage2(image); + case 3 -> productIntro.setSubImage3(image); + } + } +} \ No newline at end of file diff --git a/src/main/java/poomasi/domain/image/service/ImageService.java b/src/main/java/poomasi/domain/image/service/ImageService.java index b0cabc50..71d4f031 100644 --- a/src/main/java/poomasi/domain/image/service/ImageService.java +++ b/src/main/java/poomasi/domain/image/service/ImageService.java @@ -5,7 +5,8 @@ import org.springframework.transaction.annotation.Transactional; import poomasi.domain.image.deleteLinker.ImageDeleteFactory; import poomasi.domain.image.deleteLinker.ImageDeleteLinker; -import poomasi.domain.image.dto.ImageRequest; +import poomasi.domain.image.dto.request.ImageRequest; +import poomasi.domain.image.dto.response.ImageResponse; import poomasi.domain.image.entity.Image; import poomasi.domain.image.entity.ImageType; import poomasi.domain.image.linker.ImageLinker; @@ -30,7 +31,8 @@ public class ImageService { private static final int DEFAULT_IMAGE_LIMIT = 5; - private static final int IMAGE_ONE_LIMIT = 1; + private static final int MEMBER_PROFILE_IMAGE_LIMIT = 1; + private static final int PRODUCT_INTRO_IMAGE_LIMIT = 4; private final ImageRepository imageRepository; private final MemberService memberService; @@ -41,7 +43,7 @@ public class ImageService { // 이미지 타입에 맞게 link, deleteLink, 개수 제한, ownerValidate @Transactional - public Image saveImage(Long memberId, ImageRequest imageRequest) { + public ImageResponse saveImage(Long memberId, ImageRequest imageRequest) { // 기존 이미지가 있는 경우 복구 또는 예외 처리 (실제 복구 로직과는 차이가 있음) validateImageOwner(memberId, imageRequest.type(), imageRequest.referenceId()); validateImageLimit(imageRequest); @@ -52,7 +54,7 @@ public Image saveImage(Long memberId, ImageRequest imageRequest) { imageLink(image); - return imageRepository.save(image); + return ImageResponse.fromEntity(imageRepository.save(image)); } // 이미지 주인이 맞는지 검증 @@ -88,19 +90,26 @@ private Image recoverImageOrThrow(Image existingImage, ImageRequest imageRequest } private void validateImageLimit(ImageRequest imageRequest) { - int imageLimit = DEFAULT_IMAGE_LIMIT; - if (imageRequest.type() == ImageType.MEMBER_PROFILE || imageRequest.type() == ImageType.PRODUCT) { - imageLimit = IMAGE_ONE_LIMIT; // 멤버 프로필, 상품 이미지는 한 장으로 제한 - } + int imageLimit = determineImageLimit(imageRequest.type()); if (imageRepository.countByTypeAndReferenceIdAndDeletedAtIsNull(imageRequest.type(), imageRequest.referenceId()) >= imageLimit) { throw new BusinessException(IMAGE_LIMIT_EXCEED); } } + private int determineImageLimit(ImageType imageType) { + if (imageType == ImageType.MEMBER_PROFILE) { + return MEMBER_PROFILE_IMAGE_LIMIT; + } + if (imageType == ImageType.PRODUCT_INTRO) { + return PRODUCT_INTRO_IMAGE_LIMIT; + } + return DEFAULT_IMAGE_LIMIT; + } + // 여러 이미지 저장 @Transactional - public List saveMultipleImages(Long memberId, List imageRequests) { + public List saveMultipleImages(Long memberId, List imageRequests) { return imageRequests.stream() .map(imageRequest -> saveImage(memberId, imageRequest)) .collect(Collectors.toList()); @@ -108,27 +117,36 @@ public List saveMultipleImages(Long memberId, List imageReq @Transactional public void deleteImage(Long memberId, Long id) { - Image image = getImageById(id); - validateImageOwner(memberId, image.getType(), image.getReferenceId()); + ImageResponse imageResponse = getImageById(id); + validateImageOwner(memberId, imageResponse.type(), imageResponse.referenceId()); + + Image image = Image.fromResponse(imageResponse); imageRepository.delete(image); imageDeleteLink(image); } - public Image getImageById(Long id) { - return imageRepository.findByIdAndDeletedAtIsNull(id) + public ImageResponse getImageById(Long id) { + Image image = imageRepository.findByIdAndDeletedAtIsNull(id) .orElseThrow(() -> new BusinessException(IMAGE_NOT_FOUND)); + return ImageResponse.fromEntity(image); } - public List getImagesByTypeAndReferenceId(ImageType type, Long referenceId) { - return imageRepository.findByTypeAndReferenceIdAndDeletedAtIsNull(type, referenceId); + public List getImagesByTypeAndReferenceId(ImageType type, Long referenceId) { + return imageRepository.findByTypeAndReferenceIdAndDeletedAtIsNull(type, referenceId) + .stream() + .map(ImageResponse::fromEntity) + .collect(Collectors.toList()); } // 이미지 수정 @Transactional - public Image updateImage(Long memberId, Long id, ImageRequest imageRequest) { - Image image = getImageById(id); + public ImageResponse updateImage(Long memberId, Long id, ImageRequest imageRequest) { + ImageResponse imageResponse = getImageById(id); + Image image = Image.fromResponse(imageResponse); + validateImageOwner(memberId, image.getType(), image.getReferenceId()); + validateImageOwner(memberId, imageRequest.type(), imageRequest.referenceId()); if (!image.getType().equals(imageRequest.type()) || !image.getReferenceId().equals(imageRequest.referenceId())) { @@ -146,12 +164,14 @@ public Image updateImage(Long memberId, Long id, ImageRequest imageRequest) { } - return imageRepository.save(image); + return ImageResponse.fromEntity(imageRepository.save(image)); } @Transactional public void recoverImage(Long memberId, Long id) { - Image image = getImageById(id); + ImageResponse imageResponse = getImageById(id); + Image image = Image.fromResponse(imageResponse); + validateImageOwner(memberId, image.getType(), image.getReferenceId()); if (image.getDeletedAt() == null) { diff --git a/src/main/java/poomasi/domain/image/validator/FarmOwnerValidator.java b/src/main/java/poomasi/domain/image/validator/FarmOwnerValidator.java index 193aa1a0..6a36cea0 100644 --- a/src/main/java/poomasi/domain/image/validator/FarmOwnerValidator.java +++ b/src/main/java/poomasi/domain/image/validator/FarmOwnerValidator.java @@ -3,16 +3,21 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; import poomasi.domain.farm.repository.FarmRepository; - +import poomasi.domain.image.entity.ImageType; @Component @RequiredArgsConstructor public class FarmOwnerValidator implements ImageOwnerValidator { private final FarmRepository farmRepository; + @Override + public boolean supports(ImageType type) { + return type == ImageType.FARM; + } + @Override public boolean validateOwner(Long memberId, Long referenceId) { - return farmRepository.findById(referenceId) + return farmRepository.findByIdAndDeletedAtIsNull(referenceId) .filter(farm -> farm.getOwnerId().equals(memberId)) .isPresent(); } diff --git a/src/main/java/poomasi/domain/image/validator/ImageOwnerValidator.java b/src/main/java/poomasi/domain/image/validator/ImageOwnerValidator.java index 1faa853f..63de3180 100644 --- a/src/main/java/poomasi/domain/image/validator/ImageOwnerValidator.java +++ b/src/main/java/poomasi/domain/image/validator/ImageOwnerValidator.java @@ -1,5 +1,9 @@ package poomasi.domain.image.validator; +import poomasi.domain.image.entity.ImageType; + public interface ImageOwnerValidator { boolean validateOwner(Long memberId, Long referenceId); + boolean supports(ImageType type); + } \ No newline at end of file diff --git a/src/main/java/poomasi/domain/image/validator/ImageOwnerValidatorFactory.java b/src/main/java/poomasi/domain/image/validator/ImageOwnerValidatorFactory.java index 2de7c789..4e99c876 100644 --- a/src/main/java/poomasi/domain/image/validator/ImageOwnerValidatorFactory.java +++ b/src/main/java/poomasi/domain/image/validator/ImageOwnerValidatorFactory.java @@ -3,25 +3,21 @@ import org.springframework.stereotype.Component; import poomasi.domain.image.entity.ImageType; -import java.util.EnumMap; -import java.util.Map; +import java.util.List; @Component public class ImageOwnerValidatorFactory { - private final Map validators = new EnumMap<>(ImageType.class); - public ImageOwnerValidatorFactory(FarmOwnerValidator farmOwnerValidator, - ProductOwnerValidator productOwnerValidator, - ReviewOwnerValidator reviewOwnerValidator, - MemberProfileOwnerValidator memberProfileOwnerValidator) { - validators.put(ImageType.FARM, farmOwnerValidator); - validators.put(ImageType.PRODUCT, productOwnerValidator); - validators.put(ImageType.FARM_REVIEW, reviewOwnerValidator); - validators.put(ImageType.PRODUCT_REVIEW, reviewOwnerValidator); - validators.put(ImageType.MEMBER_PROFILE, memberProfileOwnerValidator); + private final List validators; + + public ImageOwnerValidatorFactory(List validators) { + this.validators = validators; } public ImageOwnerValidator getValidator(ImageType type) { - return validators.get(type); + return validators.stream() + .filter(validator -> validator.supports(type)) + .findFirst() + .orElse(null); } } diff --git a/src/main/java/poomasi/domain/image/validator/MemberProfileOwnerValidator.java b/src/main/java/poomasi/domain/image/validator/MemberProfileOwnerValidator.java index f49c12ea..7b8f2622 100644 --- a/src/main/java/poomasi/domain/image/validator/MemberProfileOwnerValidator.java +++ b/src/main/java/poomasi/domain/image/validator/MemberProfileOwnerValidator.java @@ -2,6 +2,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; +import poomasi.domain.image.entity.ImageType; import poomasi.domain.member.repository.MemberRepository; @Component @@ -9,9 +10,14 @@ public class MemberProfileOwnerValidator implements ImageOwnerValidator{ private final MemberRepository memberRepository; + @Override + public boolean supports(ImageType type) { + return type == ImageType.MEMBER_PROFILE; + } + @Override public boolean validateOwner(Long memberId, Long referenceId) { - return memberRepository.findById(memberId) + return memberRepository.findByIdAndDeletedAtIsNull(memberId) .filter(member -> member.getMemberProfile().getId().equals(referenceId)) .isPresent(); } diff --git a/src/main/java/poomasi/domain/image/validator/ProductIntroOwnerValidator.java b/src/main/java/poomasi/domain/image/validator/ProductIntroOwnerValidator.java new file mode 100644 index 00000000..dbc7b911 --- /dev/null +++ b/src/main/java/poomasi/domain/image/validator/ProductIntroOwnerValidator.java @@ -0,0 +1,24 @@ +package poomasi.domain.image.validator; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import poomasi.domain.image.entity.ImageType; +import poomasi.domain.product._intro.repository.ProductIntroRepository; + +@Component +@RequiredArgsConstructor +public class ProductIntroOwnerValidator implements ImageOwnerValidator{ + private final ProductIntroRepository productIntroRepository; + + @Override + public boolean supports(ImageType type) { + return type == ImageType.PRODUCT_INTRO; + } + + @Override + public boolean validateOwner(Long memberId, Long referenceId) { + return productIntroRepository.findById(referenceId) + .filter(productIntro -> productIntro.getFarmerId().equals(memberId)) + .isPresent(); + } +} diff --git a/src/main/java/poomasi/domain/image/validator/ProductOwnerValidator.java b/src/main/java/poomasi/domain/image/validator/ProductOwnerValidator.java index 43175fa2..2d9d3776 100644 --- a/src/main/java/poomasi/domain/image/validator/ProductOwnerValidator.java +++ b/src/main/java/poomasi/domain/image/validator/ProductOwnerValidator.java @@ -2,6 +2,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; +import poomasi.domain.image.entity.ImageType; import poomasi.domain.product.repository.ProductRepository; @Component @@ -9,6 +10,11 @@ public class ProductOwnerValidator implements ImageOwnerValidator{ private final ProductRepository productRepository; + @Override + public boolean supports(ImageType type) { + return type == ImageType.PRODUCT; + } + @Override public boolean validateOwner(Long memberId, Long referenceId) { return productRepository.findById(referenceId) diff --git a/src/main/java/poomasi/domain/image/validator/ReviewOwnerValidator.java b/src/main/java/poomasi/domain/image/validator/ReviewOwnerValidator.java index 5f8d8d6b..ea9a006a 100644 --- a/src/main/java/poomasi/domain/image/validator/ReviewOwnerValidator.java +++ b/src/main/java/poomasi/domain/image/validator/ReviewOwnerValidator.java @@ -2,6 +2,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; +import poomasi.domain.image.entity.ImageType; import poomasi.domain.review.repository.ReviewRepository; @Component @@ -9,6 +10,11 @@ public class ReviewOwnerValidator implements ImageOwnerValidator{ private final ReviewRepository reviewRepository; + @Override + public boolean supports(ImageType type) { + return type == ImageType.FARM_REVIEW || type == ImageType.PRODUCT_REVIEW; + } + @Override public boolean validateOwner(Long memberId, Long referenceId) { return reviewRepository.findById(referenceId) diff --git a/src/main/java/poomasi/domain/member/_biz/controller/BizProfileApproveRequest.java b/src/main/java/poomasi/domain/member/_biz/controller/BizProfileApproveRequest.java new file mode 100644 index 00000000..9468904c --- /dev/null +++ b/src/main/java/poomasi/domain/member/_biz/controller/BizProfileApproveRequest.java @@ -0,0 +1,6 @@ +package poomasi.domain.member._biz.controller; + +public record BizProfileApproveRequest( + Long memberId +) { +} diff --git a/src/main/java/poomasi/domain/member/_biz/controller/BizProfileFarmerController.java b/src/main/java/poomasi/domain/member/_biz/controller/BizProfileFarmerController.java new file mode 100644 index 00000000..37dafd44 --- /dev/null +++ b/src/main/java/poomasi/domain/member/_biz/controller/BizProfileFarmerController.java @@ -0,0 +1,45 @@ +package poomasi.domain.member._biz.controller; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.annotation.Secured; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; +import poomasi.domain.auth.security.userdetail.UserDetailsImpl; +import poomasi.domain.member._biz.dto.request.BizProfileCreateRequest; +import poomasi.domain.member._biz.service.MemberBizProfileFarmerService; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/farmer/biz-profile") +public class BizProfileFarmerController { + private final MemberBizProfileFarmerService memberBizProfileFarmerService; + + @PostMapping + @Secured("ROLE_FARMER") + public ResponseEntity saveBizProfile( + @AuthenticationPrincipal UserDetailsImpl userDetails, + @Valid @RequestBody BizProfileCreateRequest request + ) { + return ResponseEntity.ok(memberBizProfileFarmerService.updateBizProfile(userDetails.getMember(), request)); + + } + + @GetMapping + @Secured("ROLE_FARMER") + public ResponseEntity getBizProfile( + @AuthenticationPrincipal UserDetailsImpl userDetails + ) { + return ResponseEntity.ok(memberBizProfileFarmerService.getBizProfile(userDetails.getMember())); + } + + @PostMapping("/approve") + @Secured("ROLE_ADMIN") + public ResponseEntity approveBizProfile( + @Valid @RequestBody BizProfileApproveRequest request + ) { + return ResponseEntity.ok(memberBizProfileFarmerService.approveBizProfile(request)); + } + +} diff --git a/src/main/java/poomasi/domain/member/_biz/dto/request/BizProfileCreateRequest.java b/src/main/java/poomasi/domain/member/_biz/dto/request/BizProfileCreateRequest.java new file mode 100644 index 00000000..38930ee8 --- /dev/null +++ b/src/main/java/poomasi/domain/member/_biz/dto/request/BizProfileCreateRequest.java @@ -0,0 +1,24 @@ +package poomasi.domain.member._biz.dto.request; + +import jakarta.validation.constraints.NotNull; +import org.hibernate.annotations.Comment; +import poomasi.domain.member._biz.entity.MemberBizProfile; + +public record BizProfileCreateRequest( + @NotNull + @Comment("사업자 번호") + String number, + @NotNull + @Comment("사업자 등록증 이미지") + String imageUrl + +) { + public MemberBizProfile toEntity(Long id, boolean needsAdminApproval) { + return MemberBizProfile.builder() + .bizNumber(number) + .bizRegImage(imageUrl) + .memberId(id) + .needsAdminApproval(needsAdminApproval) + .build(); + } +} diff --git a/src/main/java/poomasi/domain/member/_biz/dto/response/BizProfileProfileResponse.java b/src/main/java/poomasi/domain/member/_biz/dto/response/BizProfileProfileResponse.java new file mode 100644 index 00000000..dd6598cf --- /dev/null +++ b/src/main/java/poomasi/domain/member/_biz/dto/response/BizProfileProfileResponse.java @@ -0,0 +1,10 @@ +package poomasi.domain.member._biz.dto.response; + +import lombok.Builder; + +@Builder +public record BizProfileProfileResponse( + String number, + String imageUrl +) { +} diff --git a/src/main/java/poomasi/domain/member/_biz/entity/MemberBizProfile.java b/src/main/java/poomasi/domain/member/_biz/entity/MemberBizProfile.java new file mode 100644 index 00000000..6a18f29d --- /dev/null +++ b/src/main/java/poomasi/domain/member/_biz/entity/MemberBizProfile.java @@ -0,0 +1,47 @@ +package poomasi.domain.member._biz.entity; + +import jakarta.persistence.*; +import lombok.*; +import org.hibernate.annotations.Comment; +import poomasi.domain.member._biz.dto.response.BizProfileProfileResponse; + + +@Entity +@Getter +@Comment("사업자 회원 프로필") +@Table(name = "member_biz_profile") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class MemberBizProfile { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Comment("사업자 번호") + @Column(nullable = false) + private String bizNumber; + + @Comment("사업자 등록증 이미지") + @Column(nullable = false) + private String bizRegImage; + + @Setter + @Comment("관리자 승인 필요 여부") + @Column(nullable = false) + private boolean needsAdminApproval; + + @Comment("회원 ID") + @Column(nullable = false) + private Long memberId; + + @Builder + public MemberBizProfile(String bizNumber, String bizRegImage, Long memberId, boolean needsAdminApproval) { + this.bizNumber = bizNumber; + this.bizRegImage = bizRegImage; + this.memberId = memberId; + this.needsAdminApproval = needsAdminApproval; + } + + public BizProfileProfileResponse toResponse() { + return new BizProfileProfileResponse(bizNumber, bizRegImage); + } +} diff --git a/src/main/java/poomasi/domain/member/_biz/repository/MemberBizProfileRepository.java b/src/main/java/poomasi/domain/member/_biz/repository/MemberBizProfileRepository.java new file mode 100644 index 00000000..328ea8b1 --- /dev/null +++ b/src/main/java/poomasi/domain/member/_biz/repository/MemberBizProfileRepository.java @@ -0,0 +1,12 @@ +package poomasi.domain.member._biz.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import poomasi.domain.member._biz.entity.MemberBizProfile; + +import java.util.Optional; + +@Repository +public interface MemberBizProfileRepository extends JpaRepository { + Optional findByMemberId(Long memberId); +} diff --git a/src/main/java/poomasi/domain/member/_biz/service/MemberBizProfileFarmerService.java b/src/main/java/poomasi/domain/member/_biz/service/MemberBizProfileFarmerService.java new file mode 100644 index 00000000..afb0bbfe --- /dev/null +++ b/src/main/java/poomasi/domain/member/_biz/service/MemberBizProfileFarmerService.java @@ -0,0 +1,43 @@ +package poomasi.domain.member._biz.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import poomasi.domain.member._biz.controller.BizProfileApproveRequest; +import poomasi.domain.member._biz.dto.request.BizProfileCreateRequest; +import poomasi.domain.member._biz.dto.response.BizProfileProfileResponse; +import poomasi.domain.member._biz.entity.MemberBizProfile; +import poomasi.domain.member.entity.Member; +import poomasi.global.ocr.OcrService; +import poomasi.global.ocr.dto.response.NaverOcrResponse; +import poomasi.global.ocr.dto.response.OcrResponse; + +import java.util.Objects; + +@Service +@RequiredArgsConstructor +public class MemberBizProfileFarmerService { + private final MemberBizProfileService memberBizProfileService; + private final OcrService ocrService; + + public Long updateBizProfile(Member member, BizProfileCreateRequest request) { + OcrResponse ocrResponse = ocrService.extractTextFromImage(ocrService.createRequest(request.imageUrl())); + MemberBizProfile bizProfile = request.toEntity(member.getId(), false); + + if (ocrResponse instanceof NaverOcrResponse naverOcrResponse) { + if (naverOcrResponse.getImages().isEmpty() || Objects.equals(naverOcrResponse.getImages().get(0).getInferResult(), "FAILURE")) { + bizProfile.setNeedsAdminApproval(true); + } + } + return memberBizProfileService.save(bizProfile).getId(); + } + + public Long approveBizProfile(BizProfileApproveRequest request) { + MemberBizProfile bizProfile = memberBizProfileService.findByMemberId(request.memberId()); + bizProfile.setNeedsAdminApproval(false); + return memberBizProfileService.save(bizProfile).getId(); + } + + public BizProfileProfileResponse getBizProfile(Member member) { + return memberBizProfileService.findByMemberId(member.getId()).toResponse(); + } +} diff --git a/src/main/java/poomasi/domain/member/_biz/service/MemberBizProfileService.java b/src/main/java/poomasi/domain/member/_biz/service/MemberBizProfileService.java new file mode 100644 index 00000000..0b975ea2 --- /dev/null +++ b/src/main/java/poomasi/domain/member/_biz/service/MemberBizProfileService.java @@ -0,0 +1,24 @@ +package poomasi.domain.member._biz.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import poomasi.domain.member._biz.entity.MemberBizProfile; +import poomasi.domain.member._biz.repository.MemberBizProfileRepository; +import poomasi.global.error.BusinessError; +import poomasi.global.error.BusinessException; + +@Service +@RequiredArgsConstructor +public class MemberBizProfileService { + private final MemberBizProfileRepository memberBizProfileRepository; + + public MemberBizProfile save(MemberBizProfile memberBizProfile) { + return memberBizProfileRepository.save(memberBizProfile); + } + + public MemberBizProfile findByMemberId(Long memberId) { + return memberBizProfileRepository.findByMemberId(memberId).orElseThrow( + () -> new BusinessException(BusinessError.MEMBER_BIZ_PROFILE_NOT_FOUND) + ); + } +} diff --git a/src/main/java/poomasi/domain/member/_profile/dto/request/AddressUpdateRequest.java b/src/main/java/poomasi/domain/member/_profile/dto/request/AddressUpdateRequest.java index 8a8ac1d0..8ddfee96 100644 --- a/src/main/java/poomasi/domain/member/_profile/dto/request/AddressUpdateRequest.java +++ b/src/main/java/poomasi/domain/member/_profile/dto/request/AddressUpdateRequest.java @@ -3,6 +3,6 @@ public record AddressUpdateRequest( String defaultAddress, String addressDetail, - Long coordinateX, - Long coordinateY) { + Double coordinateX, + Double coordinateY) { } diff --git a/src/main/java/poomasi/domain/member/_profile/dto/response/MemberProfileResponse.java b/src/main/java/poomasi/domain/member/_profile/dto/response/MemberProfileResponse.java index 691d8673..a88e3d86 100644 --- a/src/main/java/poomasi/domain/member/_profile/dto/response/MemberProfileResponse.java +++ b/src/main/java/poomasi/domain/member/_profile/dto/response/MemberProfileResponse.java @@ -6,11 +6,12 @@ import java.time.LocalDateTime; public record MemberProfileResponse( + Long id, String phoneNumber, String defaultAddress, String addressDetail, - Long coordinateX, - Long coordinateY, + Double coordinateX, + Double coordinateY, boolean isBanned, LocalDateTime createdAt, Image profileImage){ @@ -18,6 +19,7 @@ public record MemberProfileResponse( public static MemberProfileResponse fromEntity(MemberProfile profile) { return new MemberProfileResponse( + profile.getId(), profile.getPhoneNumber(), profile.getDefaultAddress(), profile.getAddressDetail(), diff --git a/src/main/java/poomasi/domain/member/_profile/entity/MemberProfile.java b/src/main/java/poomasi/domain/member/_profile/entity/MemberProfile.java index 05a969c1..6c93bad8 100644 --- a/src/main/java/poomasi/domain/member/_profile/entity/MemberProfile.java +++ b/src/main/java/poomasi/domain/member/_profile/entity/MemberProfile.java @@ -1,7 +1,10 @@ package poomasi.domain.member._profile.entity; import jakarta.persistence.*; -import lombok.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; import org.hibernate.annotations.SQLDelete; import poomasi.domain.image.entity.Image; @@ -24,6 +27,7 @@ public class MemberProfile { private String phoneNumber; @Column(nullable = false) + @Setter @Builder.Default private boolean isBanned = false; @@ -46,11 +50,11 @@ public class MemberProfile { @Column(nullable = true, length = 255) private String addressDetail; - @Column(nullable=true, length=255) - private Long coordinateX; + @Column(nullable = true, length = 255) + private Double coordinateX; - @Column(nullable=true, length=255) - private Long coordinateY; + @Column(nullable = true, length = 255) + private Double coordinateY; @PrePersist public void prePersist() { @@ -71,14 +75,12 @@ public MemberProfile() { public void setAddress( String defaultAddress, String addressDetail, - Long coordinateX, - Long coordinateY) { + Double coordinateX, + Double coordinateY) { if (defaultAddress != null) this.defaultAddress = defaultAddress; if (addressDetail != null) this.addressDetail = addressDetail; if (coordinateX != null) this.coordinateX = coordinateX; if (coordinateY != null) this.coordinateY = coordinateY; } - - } diff --git a/src/main/java/poomasi/domain/member/_profile/repository/MemberProfileRepository.java b/src/main/java/poomasi/domain/member/_profile/repository/MemberProfileRepository.java index 433f671c..266b3791 100644 --- a/src/main/java/poomasi/domain/member/_profile/repository/MemberProfileRepository.java +++ b/src/main/java/poomasi/domain/member/_profile/repository/MemberProfileRepository.java @@ -4,6 +4,10 @@ import org.springframework.stereotype.Repository; import poomasi.domain.member._profile.entity.MemberProfile; +import java.util.Optional; + @Repository public interface MemberProfileRepository extends JpaRepository { + Optional findByIdAndDeletedAtIsNull(Long id); + } \ No newline at end of file diff --git a/src/main/java/poomasi/domain/member/_profile/service/MemberProfileService.java b/src/main/java/poomasi/domain/member/_profile/service/MemberProfileService.java index 3f88b3c4..0955c7c5 100644 --- a/src/main/java/poomasi/domain/member/_profile/service/MemberProfileService.java +++ b/src/main/java/poomasi/domain/member/_profile/service/MemberProfileService.java @@ -17,7 +17,7 @@ public class MemberProfileService { private final MemberProfileRepository memberProfileRepository; public MemberProfile getMemberProfileById(Long id){ - return memberProfileRepository.findById(id) + return memberProfileRepository.findByIdAndDeletedAtIsNull(id) .orElseThrow(() -> new BusinessException(MEMBER_PROFILE_NOT_FOUND)); } diff --git a/src/main/java/poomasi/domain/member/controller/MemberController.java b/src/main/java/poomasi/domain/member/controller/MemberController.java index 19de271e..8b3365f3 100644 --- a/src/main/java/poomasi/domain/member/controller/MemberController.java +++ b/src/main/java/poomasi/domain/member/controller/MemberController.java @@ -1,5 +1,6 @@ package poomasi.domain.member.controller; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -9,42 +10,32 @@ import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import poomasi.domain.auth.security.userdetail.UserDetailsImpl; +import poomasi.domain.member.dto.response.*; import poomasi.domain.member._profile.dto.request.AddressUpdateRequest; import poomasi.domain.member.dto.request.CustomerUpdateRequest; import poomasi.domain.member.dto.request.FarmerUpdateRequest; -import poomasi.domain.member.dto.response.*; +import poomasi.domain.member.dto.request.SignupRequest; +import poomasi.domain.member.dto.response.FarmerResponse; +import poomasi.domain.member.dto.response.MemberResponse; +import poomasi.domain.member.dto.response.MemberSummaryResponse; +import poomasi.domain.member.dto.response.SignUpResponse; import poomasi.domain.member.entity.Member; import poomasi.domain.member.service.MemberService; import poomasi.domain.member.dto.request.SignupRequest; @RestController @RequiredArgsConstructor -@RequestMapping("api/member") +@RequestMapping("/api/members") public class MemberController { private final MemberService memberService; @PostMapping("/sign-up") - public ResponseEntity signUp(@RequestBody SignupRequest signupRequest) { + public ResponseEntity signUp(@Valid @RequestBody SignupRequest signupRequest) { return ResponseEntity.ok(memberService .signUp(signupRequest)); } - @PutMapping("/toFarmer") - @Secured("ROLE_CUSTOMER") - public ResponseEntity convertToFarmer(@AuthenticationPrincipal UserDetailsImpl userDetails) { - Member member = userDetails.getMember(); - memberService.convertToFarmer(member); - return ResponseEntity.noContent().build(); - } - - @PutMapping("/toCustomer/{memberId}") - @Secured("ROLE_ADMIN") - public ResponseEntity convertToCustomer(@PathVariable Long memberId) { - memberService.convertToCustomer(memberId); - return ResponseEntity.noContent().build(); - } - @GetMapping("/{memberId}") @Secured("ROLE_ADMIN") public ResponseEntity getMemberById(@PathVariable Long memberId) { @@ -68,58 +59,33 @@ public ResponseEntity getSelfMember(@AuthenticationPrincipal Use } @GetMapping("/summary/{memberId}") - @Secured({"ROLE_CUSTOMER", "ROLE_FARMER", "ROLE_ADMIN"}) public ResponseEntity getMemberSummaryById(@PathVariable Long memberId) { MemberSummaryResponse memberSummaryResponse = memberService.getMemberSummary(memberId); return ResponseEntity.ok(memberSummaryResponse); } - @PutMapping("/customer/update") - @Secured("ROLE_CUSTOMER") - public ResponseEntity updateCustomer( - @AuthenticationPrincipal UserDetailsImpl userDetails, - @RequestBody CustomerUpdateRequest customerUpdateRequest) { - + // 회원 탈퇴 + @DeleteMapping("/delete") + @Secured({"ROLE_CUSTOMER", "ROLE_FARMER", "ROLE_ADMIN"}) + public ResponseEntity deleteAccount(@AuthenticationPrincipal UserDetailsImpl userDetails) { Member member = userDetails.getMember(); - Member updatedMember = memberService.updateCustomer(member, customerUpdateRequest); - - MemberResponse memberResponse = MemberResponse.fromEntity(updatedMember); - return ResponseEntity.ok(memberResponse); + memberService.deleteAccount(member); + return ResponseEntity.noContent().build(); } - @PutMapping("/farmer/update") - @Secured("ROLE_FARMER") - public ResponseEntity updateFarmer( - @AuthenticationPrincipal UserDetailsImpl userDetails, - @RequestBody FarmerUpdateRequest farmerUpdateRequest) { - - Member member = userDetails.getMember(); - Member updatedMember = memberService.updateFarmer(member, farmerUpdateRequest); - - FarmerResponse memberResponse = FarmerResponse.fromEntity(updatedMember); - return ResponseEntity.ok(memberResponse); + // 계정 복구 + @PutMapping("/restore/{memberId}") + @Secured("ROLE_ADMIN") + public ResponseEntity restoreAccount(@PathVariable Long memberId) { + memberService.restoreAccount(memberId); + return ResponseEntity.ok().build(); } - @PutMapping("/customer/update/address") - @Secured("ROLE_CUSTOMER") - public ResponseEntity updateAddress( - @AuthenticationPrincipal UserDetailsImpl userDetails, - @RequestBody AddressUpdateRequest addressUpdateRequest - ) { - Member member = userDetails.getMember(); - memberService.updateAddress(member, addressUpdateRequest); + // 계정 정지 + @PutMapping("/suspend/{memberId}") + @Secured("ROLE_ADMIN") + public ResponseEntity suspendAccount(@PathVariable Long memberId) { + memberService.suspendAccount(memberId); return ResponseEntity.ok().build(); } - - // 회원 탈퇴, 복구, 금지 - // s3스케줄러 구현하긴해야함 - - // 이미지 validator 타입 추가 - - // 이미지 업로드 실패할시 처리 - - - - - } diff --git a/src/main/java/poomasi/domain/member/controller/MemberCustomerController.java b/src/main/java/poomasi/domain/member/controller/MemberCustomerController.java new file mode 100644 index 00000000..205e5fb5 --- /dev/null +++ b/src/main/java/poomasi/domain/member/controller/MemberCustomerController.java @@ -0,0 +1,52 @@ +package poomasi.domain.member.controller; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.annotation.Secured; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; +import poomasi.domain.auth.security.userdetail.UserDetailsImpl; +import poomasi.domain.member._profile.dto.request.AddressUpdateRequest; +import poomasi.domain.member.dto.request.CustomerUpdateRequest; +import poomasi.domain.member.dto.response.MemberResponse; +import poomasi.domain.member.entity.Member; +import poomasi.domain.member.service.MemberCustomerService; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/members") +public class MemberCustomerController { + private final MemberCustomerService memberCustomerService; + + @PutMapping("/to-customer/{memberId}") + @Secured("ROLE_ADMIN") + public ResponseEntity convertToCustomer(@PathVariable Long memberId) { + memberCustomerService.convertToCustomer(memberId); + return ResponseEntity.noContent().build(); + } + + @PutMapping("/update/customer") + @Secured("ROLE_CUSTOMER") + public ResponseEntity updateCustomer( + @AuthenticationPrincipal UserDetailsImpl userDetails, + @Valid @RequestBody CustomerUpdateRequest customerUpdateRequest) { + + Member member = userDetails.getMember(); + Member updatedMember = memberCustomerService.updateCustomer(member, customerUpdateRequest); + + MemberResponse memberResponse = MemberResponse.fromEntity(updatedMember); + return ResponseEntity.ok(memberResponse); + } + + @PutMapping("/update/customer/address") + @Secured("ROLE_CUSTOMER") + public ResponseEntity updateAddress( + @AuthenticationPrincipal UserDetailsImpl userDetails, + @RequestBody AddressUpdateRequest addressUpdateRequest + ) { + Member member = userDetails.getMember(); + memberCustomerService.updateAddress(member, addressUpdateRequest); + return ResponseEntity.ok().build(); + } +} diff --git a/src/main/java/poomasi/domain/member/controller/MemberFarmerController.java b/src/main/java/poomasi/domain/member/controller/MemberFarmerController.java new file mode 100644 index 00000000..4e1d5014 --- /dev/null +++ b/src/main/java/poomasi/domain/member/controller/MemberFarmerController.java @@ -0,0 +1,46 @@ +package poomasi.domain.member.controller; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.annotation.Secured; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import poomasi.domain.auth.security.userdetail.UserDetailsImpl; +import poomasi.domain.member.dto.request.ConvertToFarmerRequest; +import poomasi.domain.member.dto.request.FarmerUpdateRequest; +import poomasi.domain.member.dto.response.FarmerResponse; +import poomasi.domain.member.entity.Member; +import poomasi.domain.member.service.MemberFarmerService; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/members") +public class MemberFarmerController { + private final MemberFarmerService memberFarmerService; + + @PutMapping("/to-farmer") + @Secured("ROLE_CUSTOMER") + public ResponseEntity convertToFarmer(@AuthenticationPrincipal UserDetailsImpl userDetails, + @RequestBody ConvertToFarmerRequest convertToFarmerRequest) { + Member member = userDetails.getMember(); + memberFarmerService.convertToFarmer(member, convertToFarmerRequest); + return ResponseEntity.noContent().build(); + } + + @PutMapping("/update/farmer") + @Secured("ROLE_FARMER") + public ResponseEntity updateFarmer( + @AuthenticationPrincipal UserDetailsImpl userDetails, + @Valid @RequestBody FarmerUpdateRequest farmerUpdateRequest) { + + Member member = userDetails.getMember(); + Member updatedMember = memberFarmerService.updateFarmer(member, farmerUpdateRequest); + + FarmerResponse memberResponse = FarmerResponse.fromEntity(updatedMember); + return ResponseEntity.ok(memberResponse); + } +} diff --git a/src/main/java/poomasi/domain/member/dto/request/ConvertToFarmerRequest.java b/src/main/java/poomasi/domain/member/dto/request/ConvertToFarmerRequest.java new file mode 100644 index 00000000..edfce701 --- /dev/null +++ b/src/main/java/poomasi/domain/member/dto/request/ConvertToFarmerRequest.java @@ -0,0 +1,17 @@ +package poomasi.domain.member.dto.request; + +import poomasi.domain.store.dto.StoreRegisterRequest; + +public record ConvertToFarmerRequest( + String name, + String address, + String phone +) { + public StoreRegisterRequest toStoreRegisterRequest() { + return new StoreRegisterRequest( + name, + address, + phone + ); + } +} \ No newline at end of file diff --git a/src/main/java/poomasi/domain/member/dto/request/CustomerUpdateRequest.java b/src/main/java/poomasi/domain/member/dto/request/CustomerUpdateRequest.java index 693da308..46bfd764 100644 --- a/src/main/java/poomasi/domain/member/dto/request/CustomerUpdateRequest.java +++ b/src/main/java/poomasi/domain/member/dto/request/CustomerUpdateRequest.java @@ -1,8 +1,21 @@ package poomasi.domain.member.dto.request; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; + public record CustomerUpdateRequest( String name, + + @Email(message = "잘못된 이메일 형식입니다.") String email, + + @Size(min = 8, max = 20, message = "비밀번호는 8자 ~ 20자 사이로 설정해주세요.") String password, + + @Pattern( + regexp = "^[0-9]{10,15}$", + message = "전화번호 숫자는 10 ~ 15개여야 합니다." + ) String phoneNumber) { } diff --git a/src/main/java/poomasi/domain/member/dto/request/FarmerUpdateRequest.java b/src/main/java/poomasi/domain/member/dto/request/FarmerUpdateRequest.java index 3e16d19a..b0d24d7a 100644 --- a/src/main/java/poomasi/domain/member/dto/request/FarmerUpdateRequest.java +++ b/src/main/java/poomasi/domain/member/dto/request/FarmerUpdateRequest.java @@ -1,9 +1,22 @@ package poomasi.domain.member.dto.request; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; + public record FarmerUpdateRequest( String name, + + @Email(message = "잘못된 이메일 형식입니다.") String email, + + @Size(min = 8, max = 20, message = "비밀번호는 8자 ~ 20자 사이로 설정해주세요.") String password, + + @Pattern( + regexp = "^[0-9]{10,15}$", + message = "전화번호 숫자는 10 ~ 15개여야 합니다." + ) String phoneNumber, String storeName, String storeAddress) { diff --git a/src/main/java/poomasi/domain/member/dto/request/SignupRequest.java b/src/main/java/poomasi/domain/member/dto/request/SignupRequest.java index 85bbe7ab..a8f89528 100644 --- a/src/main/java/poomasi/domain/member/dto/request/SignupRequest.java +++ b/src/main/java/poomasi/domain/member/dto/request/SignupRequest.java @@ -1,4 +1,18 @@ package poomasi.domain.member.dto.request; -public record SignupRequest(String name, String email, String password) { +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +public record SignupRequest( + @NotBlank(message = "이름을 작성해주세요.") + String name, + + @NotBlank(message = "이메일을 작성해주세요.") + @Email(message = "잘못된 이메일 형식입니다.") + String email, + + @NotBlank(message = "비밀번호를 작성해주세요.") + @Size(min = 8, max = 20, message = "비밀번호는 8자 ~ 20자 사이로 설정해주세요.") + String password) { } diff --git a/src/main/java/poomasi/domain/member/entity/Member.java b/src/main/java/poomasi/domain/member/entity/Member.java index 5849d00d..70f55729 100644 --- a/src/main/java/poomasi/domain/member/entity/Member.java +++ b/src/main/java/poomasi/domain/member/entity/Member.java @@ -1,19 +1,18 @@ package poomasi.domain.member.entity; import jakarta.persistence.*; -import java.time.LocalDateTime; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import org.hibernate.annotations.SQLDelete; -import poomasi.domain.store.entity.Store; import poomasi.domain.member._profile.entity.MemberProfile; -import poomasi.domain.order.entity._product.ProductOrder; +import poomasi.domain.order.entity.Order; +import poomasi.domain.store.entity.Store; import poomasi.domain.wishlist.entity.WishList; -import poomasi.global.error.BusinessError; -import poomasi.global.error.BusinessException; -import java.util.*; + +import java.time.LocalDateTime; +import java.util.List; @Getter @Entity @@ -27,7 +26,7 @@ public class Member { private Long id; @Setter - @Column(nullable = true, length = 50) + @Column(length = 50) private String name; @Setter @@ -51,24 +50,27 @@ public class Member { @Column(nullable = true) private String provideId; - @OneToOne(fetch = FetchType.LAZY) + @Setter + @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL) private MemberProfile memberProfile; @OneToMany(mappedBy = "member", cascade = CascadeType.ALL, fetch = FetchType.LAZY) private List wishLists; + @Setter @Column(name = "deleted_at") private LocalDateTime deletedAt; @OneToMany(mappedBy = "member", cascade = CascadeType.ALL, orphanRemoval = true) - private List productOrderLists; + private List orderLists; @Setter @Column(nullable = true) private String farmerTierCode; + @Getter @Setter - @OneToOne(mappedBy="owner", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + @OneToOne(mappedBy = "owner", cascade = CascadeType.ALL, fetch = FetchType.LAZY) private Store store; public Member(String name, String email, String password, LoginType loginType, Role role) { @@ -81,14 +83,15 @@ public Member(String name, String email, String password, LoginType loginType, R } @Builder - public Member(Long id, String email, String password, Role role, LoginType loginType, String provideId, MemberProfile memberProfile) { + public Member(Long id, String email, String password, Role role, LoginType loginType, String provideId, MemberProfile memberProfile, String name) { this.id = id; this.password = password; this.email = email; this.role = role; this.loginType = loginType; this.provideId = provideId; - this.memberProfile = memberProfile; + this.memberProfile = (memberProfile != null) ? memberProfile : getOrCreateProfile(); + this.name = name; } public boolean isCustomer() { @@ -103,12 +106,6 @@ public boolean isAdmin() { return role == Role.ROLE_ADMIN; } - public Store getStore() { - if(store == null) - throw new BusinessException(BusinessError.STORE_NOT_FOUND); - return store; - } - public MemberProfile getOrCreateProfile() { if (this.memberProfile == null) { this.memberProfile = new MemberProfile(); @@ -124,6 +121,8 @@ public Store getOrCreateStore() { return store; } - - + public void setAddress(String defaultAddress, String addressDetail, Double coordinateX, Double coordinateY) { + MemberProfile memberProfile = getOrCreateProfile(); + memberProfile.setAddress(defaultAddress, addressDetail, coordinateX, coordinateY); + } } diff --git a/src/main/java/poomasi/domain/member/repository/MemberRepository.java b/src/main/java/poomasi/domain/member/repository/MemberRepository.java index 4e3cea7e..41476910 100644 --- a/src/main/java/poomasi/domain/member/repository/MemberRepository.java +++ b/src/main/java/poomasi/domain/member/repository/MemberRepository.java @@ -9,4 +9,5 @@ public interface MemberRepository extends JpaRepository { Optional findByEmailAndDeletedAtIsNull(String email); Optional findByIdAndDeletedAtIsNull(Long id); + Optional findByIdAndDeletedAtIsNotNull(Long memberId); } \ No newline at end of file diff --git a/src/main/java/poomasi/domain/member/service/MemberCustomerService.java b/src/main/java/poomasi/domain/member/service/MemberCustomerService.java new file mode 100644 index 00000000..d31656ca --- /dev/null +++ b/src/main/java/poomasi/domain/member/service/MemberCustomerService.java @@ -0,0 +1,56 @@ +package poomasi.domain.member.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import poomasi.domain.member._profile.dto.request.AddressUpdateRequest; +import poomasi.domain.member._profile.entity.MemberProfile; +import poomasi.domain.member.dto.request.CustomerUpdateRequest; +import poomasi.domain.member.entity.Member; +import poomasi.domain.member.repository.MemberRepository; +import poomasi.global.error.BusinessException; + +import static poomasi.domain.member.entity.Role.ROLE_CUSTOMER; +import static poomasi.global.error.BusinessError.INVALID_ROLE; +import static poomasi.global.error.BusinessError.MEMBER_ALREADY_CUSTOMER; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class MemberCustomerService { + private final MemberRepository memberRepository; + private final MemberService memberService; + + @Transactional + public void convertToCustomer(Long memberId) { + Member member = memberService.findMemberById(memberId); + + if (member.isCustomer()) { + throw new BusinessException(MEMBER_ALREADY_CUSTOMER); + } + + member.setRole(ROLE_CUSTOMER); + memberRepository.save(member); + } + + @Transactional + public Member updateCustomer(Member member, CustomerUpdateRequest customerUpdateRequest) + { + if (!member.isCustomer()) { + throw new BusinessException(INVALID_ROLE); + } + + memberService.updateCommonAttributes(member, customerUpdateRequest.name(),customerUpdateRequest.email(), customerUpdateRequest.password(), customerUpdateRequest.phoneNumber()); + + return memberRepository.save(member); + } + + @Transactional + public void updateAddress(Member member, AddressUpdateRequest request) { + MemberProfile profile = member.getOrCreateProfile(); + + profile.setAddress(request.defaultAddress(), request.addressDetail(), request.coordinateX(), request.coordinateY()); + + memberRepository.save(member); + } +} diff --git a/src/main/java/poomasi/domain/member/service/MemberFarmerService.java b/src/main/java/poomasi/domain/member/service/MemberFarmerService.java new file mode 100644 index 00000000..4da437bf --- /dev/null +++ b/src/main/java/poomasi/domain/member/service/MemberFarmerService.java @@ -0,0 +1,63 @@ +package poomasi.domain.member.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import poomasi.domain.member.dto.request.ConvertToFarmerRequest; +import poomasi.domain.member.dto.request.FarmerUpdateRequest; +import poomasi.domain.member.entity.Member; +import poomasi.domain.member.repository.MemberRepository; +import poomasi.domain.store.dto.StoreRegisterRequest; +import poomasi.domain.store.entity.Store; +import poomasi.domain.store.service.StoreService; +import poomasi.global.error.BusinessException; + +import static poomasi.domain.member.entity.Role.ROLE_FARMER; +import static poomasi.global.error.BusinessError.INVALID_ROLE; +import static poomasi.global.error.BusinessError.MEMBER_ALREADY_FARMER; + + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class MemberFarmerService { + private final MemberRepository memberRepository; + private final MemberService memberService; + private final StoreService storeService; + + @Transactional + public void convertToFarmer(Member member, ConvertToFarmerRequest convertToFarmerRequest) { + if (member.isFarmer()) { + throw new BusinessException(MEMBER_ALREADY_FARMER); + } + + StoreRegisterRequest storeRegisterRequest = convertToFarmerRequest.toStoreRegisterRequest(); + + storeService.addStore(storeRegisterRequest,member); + + member.setAddress(null, null, null, null); + member.setRole(ROLE_FARMER); + memberRepository.save(member); + } + + @Transactional + public Member updateFarmer(Member member, FarmerUpdateRequest farmerUpdateRequest) + { + if (!member.isFarmer()) { + throw new BusinessException(INVALID_ROLE); + } + + memberService.updateCommonAttributes(member, farmerUpdateRequest.name(), farmerUpdateRequest.email(), farmerUpdateRequest.password(), farmerUpdateRequest.phoneNumber()); + + Store store = member.getOrCreateStore(); + + if (farmerUpdateRequest.storeName() != null) { + store.setName(farmerUpdateRequest.storeName()); + } + if (farmerUpdateRequest.storeAddress() != null) { + store.setAddress(farmerUpdateRequest.storeAddress()); + } + + return memberRepository.save(member); + } +} diff --git a/src/main/java/poomasi/domain/member/service/MemberService.java b/src/main/java/poomasi/domain/member/service/MemberService.java index 8d48c257..a8ae07ca 100644 --- a/src/main/java/poomasi/domain/member/service/MemberService.java +++ b/src/main/java/poomasi/domain/member/service/MemberService.java @@ -7,12 +7,13 @@ import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import poomasi.domain.member._profile.dto.request.AddressUpdateRequest; import poomasi.domain.member._profile.entity.MemberProfile; import poomasi.domain.member.dto.request.CustomerUpdateRequest; import poomasi.domain.member.dto.request.FarmerUpdateRequest; +import poomasi.domain.member.dto.request.SignupRequest; import poomasi.domain.member.dto.response.MemberResponse; import poomasi.domain.member.dto.response.MemberSummaryResponse; +import poomasi.domain.member.dto.response.SignUpResponse; import poomasi.domain.member.entity.LoginType; import poomasi.domain.member.entity.Member; import poomasi.domain.member.repository.MemberRepository; @@ -21,8 +22,6 @@ import poomasi.domain.store.entity.Store; import poomasi.global.error.BusinessException; -import java.util.Optional; - import static poomasi.domain.member.entity.Role.ROLE_CUSTOMER; import static poomasi.domain.member.entity.Role.ROLE_FARMER; import static poomasi.global.error.BusinessError.*; @@ -69,67 +68,17 @@ public Page getAllMembersSummary(Pageable pageable) { return members.map(MemberSummaryResponse::fromEntity); } - @Transactional - public void convertToFarmer(Member member) { - if (member.isFarmer()) { - throw new BusinessException(MEMBER_ALREADY_FARMER); - } - - member.setRole(ROLE_FARMER); - memberRepository.save(member); - } - - @Transactional - public void convertToCustomer(Long memberId) { - Member member = findMemberById(memberId); - - if (member.isCustomer()) { - throw new BusinessException(MEMBER_ALREADY_CUSTOMER); - } - - member.setRole(ROLE_CUSTOMER); - memberRepository.save(member); - } - public Member findMemberById(Long memberId) { return memberRepository.findByIdAndDeletedAtIsNull(memberId) .orElseThrow(() -> new BusinessException(MEMBER_NOT_FOUND)); } - @Transactional - public Member updateCustomer(Member member, CustomerUpdateRequest customerUpdateRequest) - { - if (!member.isCustomer()) { - throw new BusinessException(INVALID_ROLE); - } - - updateCommonAttributes(member, customerUpdateRequest.name(),customerUpdateRequest.email(), customerUpdateRequest.password(), customerUpdateRequest.phoneNumber()); - - return memberRepository.save(member); + public Member findDeletedMemberById(Long memberId) { + return memberRepository.findByIdAndDeletedAtIsNotNull(memberId) + .orElseThrow(() -> new BusinessException(MEMBER_NOT_DELETED)); } - @Transactional - public Member updateFarmer(Member member, FarmerUpdateRequest farmerUpdateRequest) - { - if (!member.isFarmer()) { - throw new BusinessException(INVALID_ROLE); - } - - updateCommonAttributes(member, farmerUpdateRequest.name(), farmerUpdateRequest.email(), farmerUpdateRequest.password(), farmerUpdateRequest.phoneNumber()); - - Store store = member.getOrCreateStore(); - - if (farmerUpdateRequest.storeName() != null) { - store.setName(farmerUpdateRequest.storeName()); - } - if (farmerUpdateRequest.storeAddress() != null) { - store.setAddress(farmerUpdateRequest.storeAddress()); - } - - return memberRepository.save(member); - } - - private void updateCommonAttributes(Member member, String name, String email, String password, String phoneNumber) { + public void updateCommonAttributes(Member member, String name, String email, String password, String phoneNumber) { if (name != null) member.setName(name); if (email != null) member.setEmail(email); if (password != null) member.setPassword(passwordEncoder.encode(password)); @@ -141,11 +90,22 @@ private void updateCommonAttributes(Member member, String name, String email, St } @Transactional - public void updateAddress(Member member, AddressUpdateRequest request) { - MemberProfile profile = member.getOrCreateProfile(); + public void deleteAccount(Member member) { + memberRepository.delete(member); + } - profile.setAddress(request.defaultAddress(), request.addressDetail(), request.coordinateX(), request.coordinateY()); + @Transactional + public void restoreAccount(Long memberId) { + Member member = findDeletedMemberById(memberId); + member.setDeletedAt(null); + memberRepository.save(member); + } + @Transactional + public void suspendAccount(Long memberId) { + Member member = findMemberById(memberId); + MemberProfile profile = member.getOrCreateProfile(); + profile.setBanned(true); memberRepository.save(member); } diff --git a/src/main/java/poomasi/domain/order/_aftersales/controller/AfterSalesController.java b/src/main/java/poomasi/domain/order/_aftersales/controller/AfterSalesController.java deleted file mode 100644 index 4d4d8c2e..00000000 --- a/src/main/java/poomasi/domain/order/_aftersales/controller/AfterSalesController.java +++ /dev/null @@ -1,80 +0,0 @@ -package poomasi.domain.order._aftersales.controller; - - -import com.siot.IamportRestClient.exception.IamportResponseException; -import lombok.RequiredArgsConstructor; -import org.springframework.http.ResponseEntity; -import org.springframework.security.access.annotation.Secured; -import org.springframework.web.bind.annotation.*; -import poomasi.domain.order._aftersales.dto.cancel.request.FarmCancelRequest; -import poomasi.domain.order._aftersales.dto.cancel.request.ProductCancelRequest; -import poomasi.domain.order._aftersales.dto.refund.request.ProductRefundRequest; -import poomasi.domain.order._aftersales.dto.refund.request.ProductRefundRequestApprovalRequest; -import poomasi.domain.order._aftersales.dto.refund.request.ProductRefundRequestDeniedRequest; -import poomasi.domain.order._aftersales.service.FarmAfterSalesService; -import poomasi.domain.order._aftersales.service.ProductAfterSalesService; - -import java.io.IOException; - -@RestController -@RequestMapping("/api/aftersales") -@RequiredArgsConstructor -public class AfterSalesController { - - private final ProductAfterSalesService productAfterSalesService; - private final FarmAfterSalesService farmAfterSalesService; - - //-------------------------product cancel---------------------// - @Secured({"ROLE_CUSTOMER", "ROLE_FARMER"}) - @PostMapping("/product/cancel") - public ResponseEntity productCancel(@RequestBody ProductCancelRequest productCancelRequest) throws IOException, IamportResponseException { - return ResponseEntity.ok( - productAfterSalesService.cancel(productCancelRequest) - ); - } - - //-------------------------product refund---------------------// - @Secured({"ROLE_CUSTOMER", "ROLE_FARMER"}) - @PostMapping("/refund-request") - public ResponseEntity requestRefund(@RequestBody ProductRefundRequest productRefundRequest) { - return ResponseEntity.ok( - productAfterSalesService. - createRefundRequest(productRefundRequest) - ); - } - - @Secured({"ROLE_FARMER"}) - @PostMapping("/approve-refund-request") - public ResponseEntity approveRefundRequest(@RequestBody ProductRefundRequestApprovalRequest productRefundRequestApprovalRequest) throws IOException, IamportResponseException { - return ResponseEntity.ok( - productAfterSalesService.processRefundApproval(productRefundRequestApprovalRequest) - ); - } - - - @Secured({"ROLE_FARMER"}) - @PostMapping("/deniedrefund-request") - public ResponseEntity deniedRefundRequest(@RequestBody ProductRefundRequestDeniedRequest productRefundRequestDeniedRequest) { - return ResponseEntity.ok( - productAfterSalesService.processRefundDenied(productRefundRequestDeniedRequest) - ); - } - - - //-------------------------farm cancel---------------------// - @Secured({"ROLE_CUSTOMER", "ROLE_FARMER"}) - @PostMapping("/farm/cancel") - public ResponseEntity farmCancel(@RequestBody FarmCancelRequest farmCancelRequest) throws IOException, IamportResponseException { - return ResponseEntity.ok( - farmAfterSalesService.farmCancel(farmCancelRequest) - ); - } - - - //------------웹훅 api 받아서 해야 함---------// - - - - - -} diff --git a/src/main/java/poomasi/domain/order/_aftersales/dto/cancel/request/FarmCancelRequest.java b/src/main/java/poomasi/domain/order/_aftersales/dto/cancel/request/FarmCancelRequest.java deleted file mode 100644 index 2034f7dd..00000000 --- a/src/main/java/poomasi/domain/order/_aftersales/dto/cancel/request/FarmCancelRequest.java +++ /dev/null @@ -1,4 +0,0 @@ -package poomasi.domain.order._aftersales.dto.cancel.request; - -public record FarmCancelRequest(Long reservationId) { -} diff --git a/src/main/java/poomasi/domain/order/_aftersales/dto/cancel/request/ProductCancelRequest.java b/src/main/java/poomasi/domain/order/_aftersales/dto/cancel/request/ProductCancelRequest.java deleted file mode 100644 index 20a9a326..00000000 --- a/src/main/java/poomasi/domain/order/_aftersales/dto/cancel/request/ProductCancelRequest.java +++ /dev/null @@ -1,4 +0,0 @@ -package poomasi.domain.order._aftersales.dto.cancel.request; - -public record ProductCancelRequest(Long orderedProductId, Integer cancelRequestQuantity, String cancelReason) { -} diff --git a/src/main/java/poomasi/domain/order/_aftersales/dto/cancel/response/FarmCancelResponse.java b/src/main/java/poomasi/domain/order/_aftersales/dto/cancel/response/FarmCancelResponse.java deleted file mode 100644 index 8158ee58..00000000 --- a/src/main/java/poomasi/domain/order/_aftersales/dto/cancel/response/FarmCancelResponse.java +++ /dev/null @@ -1,4 +0,0 @@ -package poomasi.domain.order._aftersales.dto.cancel.response; - -public record FarmCancelResponse(Long reservationId, String status) { -} diff --git a/src/main/java/poomasi/domain/order/_aftersales/dto/cancel/response/ProductCancelResponse.java b/src/main/java/poomasi/domain/order/_aftersales/dto/cancel/response/ProductCancelResponse.java deleted file mode 100644 index 6b11a4b8..00000000 --- a/src/main/java/poomasi/domain/order/_aftersales/dto/cancel/response/ProductCancelResponse.java +++ /dev/null @@ -1,16 +0,0 @@ -package poomasi.domain.order._aftersales.dto.cancel.response; - -import poomasi.domain.order._aftersales.entity._product.ProductAfterSalesStatus; -import poomasi.domain.order.entity._product.OrderedProductStatus; - -import java.math.BigDecimal; - -public record ProductCancelResponse( - Long orderedProductId, - OrderedProductStatus orderedProductStatus, - - Long productAfterSalesDetailId, - Integer cancelQuantity, - ProductAfterSalesStatus productAfterSalesStatus, - BigDecimal finalCancelAmount) { -} diff --git a/src/main/java/poomasi/domain/order/_aftersales/dto/exchange/request/ProductExchangeRequest.java b/src/main/java/poomasi/domain/order/_aftersales/dto/exchange/request/ProductExchangeRequest.java deleted file mode 100644 index 8b8c60ee..00000000 --- a/src/main/java/poomasi/domain/order/_aftersales/dto/exchange/request/ProductExchangeRequest.java +++ /dev/null @@ -1,4 +0,0 @@ -package poomasi.domain.order._aftersales.dto.exchange.request; - -public record ProductExchangeRequest(Long orderedProductId, String exchangeReason) { -} diff --git a/src/main/java/poomasi/domain/order/_aftersales/dto/exchange/response/ProductExchangeResponse.java b/src/main/java/poomasi/domain/order/_aftersales/dto/exchange/response/ProductExchangeResponse.java deleted file mode 100644 index 4a8083a8..00000000 --- a/src/main/java/poomasi/domain/order/_aftersales/dto/exchange/response/ProductExchangeResponse.java +++ /dev/null @@ -1,4 +0,0 @@ -package poomasi.domain.order._aftersales.dto.exchange.response; - -public record ProductExchangeResponse(Long orderedProductId, String message) { -} diff --git a/src/main/java/poomasi/domain/order/_aftersales/dto/refund/request/ProductRefundRequest.java b/src/main/java/poomasi/domain/order/_aftersales/dto/refund/request/ProductRefundRequest.java deleted file mode 100644 index 940b0d4f..00000000 --- a/src/main/java/poomasi/domain/order/_aftersales/dto/refund/request/ProductRefundRequest.java +++ /dev/null @@ -1,13 +0,0 @@ -package poomasi.domain.order._aftersales.dto.refund.request; - -import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Positive; -import jakarta.validation.constraints.Size; - -public record ProductRefundRequest( - @NotNull Long orderedProductId, // 필수 필드 - @Positive Integer refundRequestQuantity, //필수 - @Size(max = 500) String refundReason, // 필수 필드 - @Size(max = 20) String request // nullable 필드 -) { -} diff --git a/src/main/java/poomasi/domain/order/_aftersales/dto/refund/request/ProductRefundRequestApprovalRequest.java b/src/main/java/poomasi/domain/order/_aftersales/dto/refund/request/ProductRefundRequestApprovalRequest.java deleted file mode 100644 index 2bef6976..00000000 --- a/src/main/java/poomasi/domain/order/_aftersales/dto/refund/request/ProductRefundRequestApprovalRequest.java +++ /dev/null @@ -1,5 +0,0 @@ -package poomasi.domain.order._aftersales.dto.refund.request; - -public record ProductRefundRequestApprovalRequest(Long productAfterSalesDetailId, - String invoiceNumber) { -} diff --git a/src/main/java/poomasi/domain/order/_aftersales/dto/refund/request/ProductRefundRequestDeniedRequest.java b/src/main/java/poomasi/domain/order/_aftersales/dto/refund/request/ProductRefundRequestDeniedRequest.java deleted file mode 100644 index 88f91f2f..00000000 --- a/src/main/java/poomasi/domain/order/_aftersales/dto/refund/request/ProductRefundRequestDeniedRequest.java +++ /dev/null @@ -1,5 +0,0 @@ -package poomasi.domain.order._aftersales.dto.refund.request; - -public record ProductRefundRequestDeniedRequest(Long productAfterSalesDetailId, - String refundDeinedReason) { -} diff --git a/src/main/java/poomasi/domain/order/_aftersales/dto/refund/response/ProductRefundRequestApprovalResponse.java b/src/main/java/poomasi/domain/order/_aftersales/dto/refund/response/ProductRefundRequestApprovalResponse.java deleted file mode 100644 index 1e1bc6bd..00000000 --- a/src/main/java/poomasi/domain/order/_aftersales/dto/refund/response/ProductRefundRequestApprovalResponse.java +++ /dev/null @@ -1,12 +0,0 @@ -package poomasi.domain.order._aftersales.dto.refund.response; - -import java.math.BigDecimal; - -public record ProductRefundRequestApprovalResponse( - Long orderedProductId, - Integer count, - BigDecimal refundAmount, - Long productAfterSalesDetailId, - String invoiceNumber -) { -} diff --git a/src/main/java/poomasi/domain/order/_aftersales/dto/refund/response/ProductRefundRequestDeniedResponse.java b/src/main/java/poomasi/domain/order/_aftersales/dto/refund/response/ProductRefundRequestDeniedResponse.java deleted file mode 100644 index 109d320f..00000000 --- a/src/main/java/poomasi/domain/order/_aftersales/dto/refund/response/ProductRefundRequestDeniedResponse.java +++ /dev/null @@ -1,8 +0,0 @@ -package poomasi.domain.order._aftersales.dto.refund.response; - -import poomasi.domain.order._aftersales.entity._product.ProductAfterSalesStatus; - -public record ProductRefundRequestDeniedResponse(Long productAfterSalesDetailId, - ProductAfterSalesStatus productAfterSalesStatus, - String productRefundDeniedReason) { -} diff --git a/src/main/java/poomasi/domain/order/_aftersales/dto/refund/response/ProductRefundRequestResponse.java b/src/main/java/poomasi/domain/order/_aftersales/dto/refund/response/ProductRefundRequestResponse.java deleted file mode 100644 index d5c3e26a..00000000 --- a/src/main/java/poomasi/domain/order/_aftersales/dto/refund/response/ProductRefundRequestResponse.java +++ /dev/null @@ -1,19 +0,0 @@ -package poomasi.domain.order._aftersales.dto.refund.response; - -import poomasi.domain.order._aftersales.entity._product.ProductAfterSalesStatus; -import poomasi.domain.order.entity._product.OrderedProductStatus; - -import java.math.BigDecimal; - -public record ProductRefundRequestResponse( - Long orderedProductId, - OrderedProductStatus orderedProductStatus, - - Long productAfterSalesDetailId, - Integer refundQuantity, - ProductAfterSalesStatus productAfterSalesTypem, - BigDecimal finalRefundAmount -) { -} - - diff --git a/src/main/java/poomasi/domain/order/_aftersales/entity/_farm/FarmAfterSales.java b/src/main/java/poomasi/domain/order/_aftersales/entity/_farm/FarmAfterSales.java deleted file mode 100644 index c573c2ca..00000000 --- a/src/main/java/poomasi/domain/order/_aftersales/entity/_farm/FarmAfterSales.java +++ /dev/null @@ -1,6 +0,0 @@ -package poomasi.domain.order._aftersales.entity._farm; - - - -public class FarmAfterSales { -} diff --git a/src/main/java/poomasi/domain/order/_aftersales/entity/_product/ProductAfterSalesDetail.java b/src/main/java/poomasi/domain/order/_aftersales/entity/_product/ProductAfterSalesDetail.java deleted file mode 100644 index 41a2fe2a..00000000 --- a/src/main/java/poomasi/domain/order/_aftersales/entity/_product/ProductAfterSalesDetail.java +++ /dev/null @@ -1,101 +0,0 @@ -package poomasi.domain.order._aftersales.entity._product; - -import jakarta.persistence.*; -import jdk.jfr.Description; -import jdk.jfr.Timestamp; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import org.hibernate.annotations.CreationTimestamp; -import org.hibernate.annotations.UpdateTimestamp; -import poomasi.domain.order.entity._product.OrderedProduct; - -import java.math.BigDecimal; -import java.time.LocalDateTime; - -@Description("상품 판매 후 교환/환불/추소 history") -@Entity -@Getter -@Table(name="product_after_sales_detail") -@NoArgsConstructor -public class ProductAfterSalesDetail { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(name = "created_at") - @CreationTimestamp - private LocalDateTime createdAt = LocalDateTime.now(); - - @Column(name = "updated_at") - @UpdateTimestamp - private LocalDateTime updateAt = LocalDateTime.now(); - - @Column(name = "deleted_at") - @Timestamp - private LocalDateTime deletedAt; - - @ManyToOne - private OrderedProduct orderedProduct; - - @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL) - @JoinColumn(name = "product_refund_detail_id", nullable = true) // 외래 키 설정 - private ProductRefundDetail productRefundDetail; - - //TODO : payment에 있는 것을 변경해야 함 - private String impUid; - - @Description("환불/교환/취소 금액") - private BigDecimal adjustAmount; - - @Description("취소/교환/환불 수량") - private Integer adjustmentQuantity; - - @Description("환불/교환/취소 요청 사유") - private String reason; - - @Enumerated(EnumType.STRING) - private ProductAfterSalesStatus productAfterSalesStatus; - - @Builder - public ProductAfterSalesDetail(OrderedProduct orderedProduct, - BigDecimal adjustAmount, - String reason, - Integer adjustmentQuantity, - ProductAfterSalesStatus productAfterSalesStatus) { - this.orderedProduct = orderedProduct; - this.adjustAmount = adjustAmount; - this.reason = reason; - this.adjustmentQuantity = adjustmentQuantity; - this.productAfterSalesStatus = productAfterSalesStatus; - } - - public void setOrderedProduct(OrderedProduct orderedProduct) { - this.orderedProduct = orderedProduct; - } - - public void setProductAfterSalesStatus(ProductAfterSalesStatus productAfterSalesStatus){ - this.productAfterSalesStatus = productAfterSalesStatus; - } - - public String getProductRefundDeniedReason(){ - return this.productRefundDetail.getProductRefundDeniedReason(); - } - - public void setProductRefundDeniedReason(String productRefundDeniedReason){ - this.productRefundDetail.setProductRefundDeniedReason(productRefundDeniedReason); - } - - public void setProductRefundDetail(ProductRefundDetail productRefundDetail) { - this.productRefundDetail = productRefundDetail; - productRefundDetail.setProductAfterSalesDetail(this); - } - - public void changeRefundApproveStatus(String invoiceNumber){ - this.productAfterSalesStatus = ProductAfterSalesStatus.REFUND_APPROVED; - this.productRefundDetail.setInvoiceNumber(invoiceNumber); - } - -} - diff --git a/src/main/java/poomasi/domain/order/_aftersales/entity/_product/ProductAfterSalesStatus.java b/src/main/java/poomasi/domain/order/_aftersales/entity/_product/ProductAfterSalesStatus.java deleted file mode 100644 index 5a0ab449..00000000 --- a/src/main/java/poomasi/domain/order/_aftersales/entity/_product/ProductAfterSalesStatus.java +++ /dev/null @@ -1,22 +0,0 @@ -package poomasi.domain.order._aftersales.entity._product; - -public enum ProductAfterSalesStatus { - EXCHANGE, - CANCEL, - REFUND, - - - //환불 - REFUND_REQUESTED, // 환불 요청됨 - REFUND_APPROVED, // 환불 승인됨 - REFUND_SHIPMENT_STARTED, // 환불 배송 시작 (반품 물품의 배송 시작) - REFUND_IN_TRANSIT, // 환불 배송 중 (반품 물품이 배송 중) - REFUND_DELIVERED, // 환불 배송 완료 (반품 물품이 도착함) - REFUND_IN_PROGRESS, // 환불 처리 중 (반품 수거 중이거나 처리 대기 중) - REFUND_COMPLETED, // 환불 완료 - REFUND_DENIED // 환불 요청 거절됨 - - - ; - -} diff --git a/src/main/java/poomasi/domain/order/_aftersales/entity/_product/ProductRefundDetail.java b/src/main/java/poomasi/domain/order/_aftersales/entity/_product/ProductRefundDetail.java deleted file mode 100644 index 954c3fe0..00000000 --- a/src/main/java/poomasi/domain/order/_aftersales/entity/_product/ProductRefundDetail.java +++ /dev/null @@ -1,76 +0,0 @@ -package poomasi.domain.order._aftersales.entity._product; - - -import jakarta.persistence.*; -import jdk.jfr.Description; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import poomasi.domain.product.entity.Product; - -@Entity -@Table(name= "product_refund_detail") -@Getter -@NoArgsConstructor -public class ProductRefundDetail { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @OneToOne(mappedBy = "productRefundDetail") // 주인이 아닌 쪽에 mappedBy 설정 - private ProductAfterSalesDetail productAfterSalesDetail; - - @Description("반품 회수지. 기본 값은 보낸 주소") - private String pickupLocationAddress; - - @Description("반품 회수지 상세 정보") - private String pickupLocationAddressDetail; - - @Description("반송지. 기본 값은 받은 주소") - private String returnAddress; - - @Description("반송지. 기본 값은 받은 주소") - private String returnAddressDetail; - - @Description("반품/교환 시 운송장 번호") - private String invoiceNumber; - - @Description("반품/교환 시 요청 사항") - private String request; - - @Description("환불 거절 사유") - private String productRefundDeniedReason; - - @Builder - public ProductRefundDetail(ProductAfterSalesDetail productAfterSalesDetail, - String pickupLocationAddress, - String pickupLocationAddressDetail, - String returnAddress, - String returnAddressDetail, - String invoiceNumber, - String request, - String productRefundDeniedReason){ - this.productAfterSalesDetail = productAfterSalesDetail; - this.pickupLocationAddress = pickupLocationAddress; - this.pickupLocationAddressDetail = pickupLocationAddressDetail; - this.returnAddress = returnAddress; - this.returnAddressDetail = returnAddressDetail; - this.invoiceNumber = invoiceNumber; - this.request = request; - this.productRefundDeniedReason = productRefundDeniedReason; - } - - public void setProductRefundDeniedReason(String productRefundDeniedReason) { - this.productRefundDeniedReason = productRefundDeniedReason; - } - - public void setProductAfterSalesDetail(ProductAfterSalesDetail productAfterSalesDetail) { - this.productAfterSalesDetail = productAfterSalesDetail; - } - - public void setInvoiceNumber(String invoiceNumber){ - this.invoiceNumber = invoiceNumber; - } - -} diff --git a/src/main/java/poomasi/domain/order/_aftersales/repository/ProductAfterSalesDetailRepository.java b/src/main/java/poomasi/domain/order/_aftersales/repository/ProductAfterSalesDetailRepository.java deleted file mode 100644 index e7b53e82..00000000 --- a/src/main/java/poomasi/domain/order/_aftersales/repository/ProductAfterSalesDetailRepository.java +++ /dev/null @@ -1,11 +0,0 @@ -package poomasi.domain.order._aftersales.repository; - -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; -import poomasi.domain.order._aftersales.entity._product.ProductAfterSalesDetail; - -@Repository -public interface ProductAfterSalesDetailRepository extends JpaRepository { - - -} diff --git a/src/main/java/poomasi/domain/order/_aftersales/repository/ProductRefundDetailRepository.java b/src/main/java/poomasi/domain/order/_aftersales/repository/ProductRefundDetailRepository.java deleted file mode 100644 index 3bd40c3d..00000000 --- a/src/main/java/poomasi/domain/order/_aftersales/repository/ProductRefundDetailRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package poomasi.domain.order._aftersales.repository; - -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; -import poomasi.domain.order._aftersales.entity._product.ProductRefundDetail; - -@Repository -public interface ProductRefundDetailRepository extends JpaRepository { -} diff --git a/src/main/java/poomasi/domain/order/_aftersales/service/CancelService.java b/src/main/java/poomasi/domain/order/_aftersales/service/CancelService.java deleted file mode 100644 index 5238467c..00000000 --- a/src/main/java/poomasi/domain/order/_aftersales/service/CancelService.java +++ /dev/null @@ -1,9 +0,0 @@ -package poomasi.domain.order._aftersales.service; - -import com.siot.IamportRestClient.exception.IamportResponseException; - -import java.io.IOException; - -public interface CancelService { - T cancel(P parameter) throws IOException, IamportResponseException; -} diff --git a/src/main/java/poomasi/domain/order/_aftersales/service/FarmAfterSalesService.java b/src/main/java/poomasi/domain/order/_aftersales/service/FarmAfterSalesService.java deleted file mode 100644 index df44b4bf..00000000 --- a/src/main/java/poomasi/domain/order/_aftersales/service/FarmAfterSalesService.java +++ /dev/null @@ -1,14 +0,0 @@ -package poomasi.domain.order._aftersales.service; - - -import org.springframework.stereotype.Service; -import poomasi.domain.order._aftersales.dto.cancel.request.FarmCancelRequest; - -@Service -public class FarmAfterSalesService { - - public String farmCancel(FarmCancelRequest farmCancelRequest){ - return "success!"; - } - -} diff --git a/src/main/java/poomasi/domain/order/_aftersales/service/ProductAfterSalesService.java b/src/main/java/poomasi/domain/order/_aftersales/service/ProductAfterSalesService.java deleted file mode 100644 index 2e938c56..00000000 --- a/src/main/java/poomasi/domain/order/_aftersales/service/ProductAfterSalesService.java +++ /dev/null @@ -1,371 +0,0 @@ -package poomasi.domain.order._aftersales.service; - - -import com.siot.IamportRestClient.exception.IamportResponseException; -import jdk.jfr.Description; -import lombok.RequiredArgsConstructor; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import poomasi.domain.auth.security.userdetail.UserDetailsImpl; -import poomasi.domain.member.entity.Member; -import poomasi.domain.order._aftersales.dto.cancel.request.ProductCancelRequest; -import poomasi.domain.order._aftersales.dto.cancel.response.ProductCancelResponse; -import poomasi.domain.order._aftersales.dto.refund.request.ProductRefundRequest; -import poomasi.domain.order._aftersales.dto.refund.request.ProductRefundRequestApprovalRequest; -import poomasi.domain.order._aftersales.dto.refund.request.ProductRefundRequestDeniedRequest; -import poomasi.domain.order._aftersales.dto.refund.response.ProductRefundRequestApprovalResponse; -import poomasi.domain.order._aftersales.dto.refund.response.ProductRefundRequestDeniedResponse; -import poomasi.domain.order._aftersales.dto.refund.response.ProductRefundRequestResponse; -import poomasi.domain.order._aftersales.entity._product.ProductAfterSalesDetail; -import poomasi.domain.order._aftersales.entity._product.ProductAfterSalesStatus; -import poomasi.domain.order._aftersales.entity._product.ProductRefundDetail; -import poomasi.domain.order._aftersales.repository.ProductAfterSalesDetailRepository; -import poomasi.domain.order._aftersales.repository.ProductRefundDetailRepository; -import poomasi.domain.order._payment.util.PaymentUtil; -import poomasi.domain.order.entity._product.OrderedProduct; -import poomasi.domain.order.entity._product.ProductOrder; -import poomasi.domain.order.entity._product.OrderedProductStatus; -import poomasi.domain.order.entity._product.ProductOrderDetails; -import poomasi.domain.order.repository.OrderedProductRepository; -import poomasi.global.error.BusinessException; - -import java.io.IOException; -import java.math.BigDecimal; - -import static poomasi.domain.order._aftersales.entity._product.ProductAfterSalesStatus.CANCEL; -import static poomasi.domain.order.entity._product.OrderedProductStatus.*; -import static poomasi.global.error.BusinessError.*; - -@Service -@RequiredArgsConstructor -public class ProductAfterSalesService implements CancelService{ - - private final OrderedProductRepository orderedProductRepository; - private final PaymentUtil paymentUtil; - private final ProductAfterSalesDetailRepository productAfterSalesDetailRepository; - private final ProductRefundDetailRepository productRefundDetailRepository; - - //-------------------------cancel---------------------// - @Description("판매자 확인 전 취소하는 메서드. 판매자 확인 대기 전 경우만 취소 할 수 있음") - @Override - @Transactional - public ProductCancelResponse cancel(ProductCancelRequest productCancelRequest) throws IOException, IamportResponseException { - - Long orderedProductId = productCancelRequest.orderedProductId(); - String cancelReason = productCancelRequest.cancelReason(); - - //주인 검증 - 유저의 orderedProductId가 맞는지 검증 - OrderedProduct orderedProduct = validateProductCancelRequestByMemberId(orderedProductId); - - //수량 검증 - Integer cancelRequestQuantity = productCancelRequest.cancelRequestQuantity(); - Integer adjustableQuantity = orderedProduct.getAdjustableQuantity(); - if(cancelRequestQuantity > adjustableQuantity){ - throw new BusinessException(CANCEL_QUANTITY_EXCEEDED); - } - - //포트원 취소를 위한 주문 Id 찾기 - ProductOrder productOrder = orderedProduct.getProductOrder(); - String impUid = productOrder.getImpUid(); - - //판매자 확인 대기 전이 아니라면 주문 취소를 할 수 없다 - OrderedProductStatus orderedProductStatus = orderedProduct.getOrderedProductStatus(); - if(orderedProductStatus != PENDING_SELLER_APPROVAL){ - throw new BusinessException(SHIPPING_ALREADY_IN_PROGRESS); - } - - //최종 취소 될 금액 계산 -> 배송비는 처음 한 번 환불 - BigDecimal finalCancelAmount = calculateCancelAmount(orderedProduct, cancelRequestQuantity); - - //배송비 환불 플래그 설정 - - // checksum 검증 - BigDecimal checkSum = productOrder.getCheckSum(); - - //취소하려는 금액이 남은 환불 가능한 금액보다 크다면 - if(finalCancelAmount.compareTo(checkSum) > 0){ - throw new BusinessException(CHECKSUM_EXCESSIVE_REFUND_AMOUNT); - } - - //취소 요청 후, 주문 취소 상태로 변경 - paymentUtil.partialRefundByImpUid(impUid, checkSum, finalCancelAmount, cancelReason); - //orderedProduct.setOrderedProductStatus(CANCEL_PENDING); - - //checksum 뺴기 : 주문 취소가 정상적으로 완료가 되었다면 동기화 - productOrder.subtractChecksum(finalCancelAmount); - - //취소/환불/교환 가능 수량 변경 및 플래그 설정 - orderedProduct.subtractRefundableCount(cancelRequestQuantity); - orderedProduct.addCancelQuantity(cancelRequestQuantity); - //모두 취소 해버렸다면 - orderedProductStatus = orderedProduct.changeOrderedProductStatusToCancel(); - - //TODO : 취소 된 수량도 추가해야 하나? 오늘 회의에서 결정함 - //취소 된 상품 수량 증가 - orderedProduct.getProduct().addStock(cancelRequestQuantity); - - //취소 내역 저장 - ProductAfterSalesDetail productAfterSalesDetail = new ProductAfterSalesDetail() - .builder() - .orderedProduct(orderedProduct) - .adjustAmount(finalCancelAmount) - .reason(cancelReason) - .adjustmentQuantity(cancelRequestQuantity) - .productAfterSalesStatus(CANCEL) - .build(); - orderedProduct.addProductAfterSalesDetail(productAfterSalesDetail); - productAfterSalesDetailRepository.save(productAfterSalesDetail); - - //응답 반환 - return new ProductCancelResponse(orderedProductId, - orderedProductStatus, - productAfterSalesDetail.getId(), - cancelRequestQuantity, - productAfterSalesDetail.getProductAfterSalesStatus(), - finalCancelAmount - ); - - } - - @Description("요청이 구매자 소유인지 확인하는 메서드") - private OrderedProduct validateProductCancelRequestByMemberId(Long orderedProductId){ - Member member = getMember(); - Long memberId = getMember().getId(); - OrderedProduct orderedProduct = orderedProductRepository.findById(orderedProductId) - .orElseThrow(()-> new BusinessException(ORDERED_PRODUCT_NOT_FOUND)); - - if(orderedProduct.getProductOrder().getMember().getId() != memberId){ - new BusinessException(ORDERED_PRODUCT_NOT_FOUND); // TODO : 메서드 추출 이후, error enum 변경 - } - - return orderedProduct; - } - - - @Description("취소 요청에서 취소 금액 계산하는 메서드. 취소 전적이 한 번이라도 있으면 배송비 환불 x.") - private BigDecimal calculateCancelAmount(OrderedProduct orderedProduct, Integer cancelRequestQuantity){ - - boolean isCanceled = orderedProduct.isCanceled(); - - BigDecimal cancelAmount = orderedProduct.getPrice() - .multiply(new BigDecimal(cancelRequestQuantity) - ); - - if(!isCanceled){ - //배송비 붙여야 한다 - BigDecimal deliveryFee = orderedProduct.getDeliveryFee(); - cancelAmount = cancelAmount.add(deliveryFee); - } - return cancelAmount; - } - - - //-------------------------refund---------------------// - @Description("환불 요청하는 메서드") - @Transactional - public ProductRefundRequestResponse createRefundRequest(ProductRefundRequest productRefundRequest) { - Long orderedProductId = productRefundRequest.orderedProductId(); - String refundReason = productRefundRequest.refundReason(); - - // 주인 검증 - 유저의 orderedProductId가 맞는지 검증 - OrderedProduct orderedProduct = validateProductCancelRequestByMemberId(orderedProductId); - - // 수량 검증 - 조정 가능한 수량보다 환불 수량이 많으면 exception - Integer refundRequestQuantity = productRefundRequest.refundRequestQuantity(); - Integer adjustableQuantity = orderedProduct.getAdjustableQuantity(); - if(refundRequestQuantity > adjustableQuantity){ - throw new BusinessException(REFUND_QUANTITY_EXCEEDED); - } - - //포트원 취소를 위한 주문 Id 찾기 - ProductOrder productOrder = orderedProduct.getProductOrder(); - String impUid = productOrder.getImpUid(); - - //구매 확정 상태라면 환불을 할 수 없다 - OrderedProductStatus orderedProductStatus = orderedProduct.getOrderedProductStatus(); - if(orderedProductStatus == DELIVERED){ - throw new BusinessException(PURCHASE_ALREADY_CONFIRMED); - } - - // 배송 대기 전 상태라면 환불을 할 수 없다. - if(orderedProductStatus == PENDING_SELLER_APPROVAL){ - throw new BusinessException(REFUND_NOT_ALLOWED_BEFORE_SHIPPING); - } - - //최종 환불 금액 계산 - BigDecimal finalRefundAmount = calculateRefundAmount(orderedProduct, refundRequestQuantity); - - // checksum 검증 - BigDecimal checkSum = productOrder.getCheckSum(); - - //취소하려는 금액이 남은 환불 가능한 금액보다 크다면 - if(finalRefundAmount.compareTo(checkSum) > 0){ - throw new BusinessException(CHECKSUM_EXCESSIVE_REFUND_AMOUNT); - } - - //취소/환불/교환 가능 수량 변경 - orderedProduct.subtractRefundableCount(refundRequestQuantity); - - //환불 내역 저장 - ProductAfterSalesDetail productAfterSalesDetail = new ProductAfterSalesDetail() - .builder() - .orderedProduct(orderedProduct) - .adjustAmount(finalRefundAmount) - .reason(refundReason) - .adjustmentQuantity(refundRequestQuantity) - .productAfterSalesStatus(ProductAfterSalesStatus.REFUND_REQUESTED) - .build(); - - //환불 상세 저장 - ProductRefundDetail productRefundDetail = createProductRefundDetail(orderedProduct, productOrder, productRefundRequest); - productAfterSalesDetail.setProductRefundDetail(productRefundDetail); - orderedProduct.addProductAfterSalesDetail(productAfterSalesDetail); - - productAfterSalesDetailRepository.save(productAfterSalesDetail); - productRefundDetailRepository.save(productRefundDetail); - - //응답 반환 - return new ProductRefundRequestResponse( - orderedProductId, - orderedProductStatus, - productAfterSalesDetail.getId(), - refundRequestQuantity, - productAfterSalesDetail.getProductAfterSalesStatus(), - finalRefundAmount - ); - } - - @Description("반품 상세 요청사항 만드는 메서드") - private ProductRefundDetail createProductRefundDetail(OrderedProduct orderedProduct, - ProductOrder productOrder, - ProductRefundRequest productRefundRequest) { - - ProductOrderDetails productOrderDetails = productOrder.getProductOrderDetails(); - String pickupLocationAddress = productOrderDetails.getDestinationAddress(); - String pickUpLocationAddressDetail = productOrderDetails.getDestinationAddressDetail(); - - String returnAddress = orderedProduct.getStoreAddress(); - String returnAddressDetail = orderedProduct.getStoreAddressDetail(); - - String request = productRefundRequest.request(); //nullable field - - ProductRefundDetail productRefundDetail = new ProductRefundDetail() - .builder() - .pickupLocationAddress(pickupLocationAddress) - .pickupLocationAddressDetail(pickUpLocationAddressDetail) - .returnAddress(returnAddress) - .returnAddressDetail(returnAddressDetail) - .request(request) - .build(); - - return productRefundDetail; - } - - - @Description("환불 요청에서 환불 금액 계산하는 메서드") - private BigDecimal calculateRefundAmount(OrderedProduct orderedProduct, Integer refundRequestQuantity){ - - BigDecimal refundAmount = orderedProduct.getPrice() - .multiply(new BigDecimal(refundRequestQuantity)); - - return refundAmount; - } - - - @Description("판매자 환불 거절 메서드") - @Transactional - public ProductRefundRequestDeniedResponse processRefundDenied(ProductRefundRequestDeniedRequest productRefundRequestDeniedRequest){ - - Long productAfterSalesDetailId = productRefundRequestDeniedRequest.productAfterSalesDetailId(); - String refundDeinedReason = productRefundRequestDeniedRequest.refundDeinedReason(); - - //환불 요청이 존재하는지 그리고 자신의 환불 요청인지 검증하고 - ProductAfterSalesDetail productAfterSalesDetail = validateProductRefundRequestByFarmerId(productAfterSalesDetailId); - - //refund detail 만든 후 - ProductRefundDetail productRefundDetail = productAfterSalesDetail.getProductRefundDetail(); - productRefundDetail.setProductRefundDeniedReason(refundDeinedReason); - - //환불 거부 상태로 등록 후 - productAfterSalesDetail.setProductAfterSalesStatus(ProductAfterSalesStatus.REFUND_DENIED); - - productAfterSalesDetail.setProductRefundDetail(productRefundDetail); - //db에 저장한다 - productRefundDetailRepository.save(productRefundDetail); - - //전달한다 - return new ProductRefundRequestDeniedResponse( - productAfterSalesDetail.getId(), - productAfterSalesDetail.getProductAfterSalesStatus(), - productAfterSalesDetail.getProductRefundDeniedReason() - ); - } - - @Description("판매자 환불 확인 메서드") - public ProductRefundRequestApprovalResponse processRefundApproval(ProductRefundRequestApprovalRequest productRefundRequestApprovalRequest) throws IOException, IamportResponseException { - - Long productAfterSalesDetailId = productRefundRequestApprovalRequest.productAfterSalesDetailId(); - String invoiceNumber = productRefundRequestApprovalRequest.invoiceNumber(); - - //환불 요청이 존재하는지 그리고 자신의 환불 요청인지 검증하고 - ProductAfterSalesDetail productAfterSalesDetail = validateProductRefundRequestByFarmerId(productAfterSalesDetailId); - - //환불 결제 금액 찾고 - BigDecimal finalRefundAmount = productAfterSalesDetail.getAdjustAmount(); - - //결제를 찾는다 - OrderedProduct orderedProduct = productAfterSalesDetail.getOrderedProduct(); - ProductOrder productOrder = orderedProduct.getProductOrder(); - - //환불에 필요한 parameter - String refundReason = productAfterSalesDetail.getReason(); - String impUid = productOrder.getImpUid(); - BigDecimal checkSum = productOrder.getCheckSum(); - - //환불 처리 - paymentUtil.partialRefundByImpUid(impUid, checkSum, finalRefundAmount, refundReason); - - //성공하면 checksum 포트원 서버와 동기화 - productOrder.subtractChecksum(finalRefundAmount); - - //운송장 번호와 상태 등록 - productAfterSalesDetail.changeRefundApproveStatus(invoiceNumber); - - return new ProductRefundRequestApprovalResponse( - orderedProduct.getId(), - productAfterSalesDetail.getAdjustmentQuantity(), - finalRefundAmount, - productAfterSalesDetailId, - invoiceNumber - ); - - } - - - @Description("환불 요청이 존재하고, 판매자 소유인지 확인하는 메서드 ") - private ProductAfterSalesDetail validateProductRefundRequestByFarmerId(Long productAfterSalesDetailId){ - ProductAfterSalesDetail productAfterSalesDetail = productAfterSalesDetailRepository.findById(productAfterSalesDetailId) - .orElseThrow(()-> new BusinessException(REFUND_AFTER_SALES_NOT_FOUND) - ); - Long farmerId = getMember().getId(); - /* - if(farmerId != productAfterSalesDetail.getOrderedProduct().getStoreId().getMember()){ - throw new BusinessException(REFUND_AFTER_SALES_REQUEST_INVALID_OWNER); - } - */ - return productAfterSalesDetail; - } - - // ------------------------------// - @Description("security context에서 member 객체 가져오는 메서드") - private Member getMember() { - Authentication authentication = SecurityContextHolder - .getContext().getAuthentication(); - Object impl = authentication.getPrincipal(); - Member member = ((UserDetailsImpl) impl).getMember(); - return member; - } - -} diff --git a/src/main/java/poomasi/domain/order/_payment/controller/PaymentController.java b/src/main/java/poomasi/domain/order/_payment/controller/PaymentController.java deleted file mode 100644 index 58777551..00000000 --- a/src/main/java/poomasi/domain/order/_payment/controller/PaymentController.java +++ /dev/null @@ -1,76 +0,0 @@ -package poomasi.domain.order._payment.controller; - -import com.siot.IamportRestClient.exception.IamportResponseException; -import jdk.jfr.Description; -import lombok.RequiredArgsConstructor; -import org.springframework.http.ResponseEntity; -import org.springframework.security.access.annotation.Secured; -import org.springframework.web.bind.annotation.*; -import poomasi.domain.order._payment.dto.request.PaymentPreRegisterRequest; -import poomasi.domain.order._payment.dto.request.PaymentWebHookRequest; -import poomasi.domain.order._payment.service.ProductPaymentService; - -import java.io.IOException; - -@RestController -@RequestMapping("/api/payment") -@RequiredArgsConstructor -public class PaymentController { - - private final ProductPaymentService productPaymentService; - - @Description("사전 결제 api") - @Secured({"ROLE_CUSTOMER", "ROLE_FARMER"}) - @PostMapping("/pre-payment") - public ResponseEntity postPrepare(PaymentPreRegisterRequest paymentPreRegisterRequest) throws IamportResponseException, IOException { - return ResponseEntity.ok( - productPaymentService.portonePrePaymentRegister(paymentPreRegisterRequest) - ); - } - - @Description("결제 바로 직전 포트원에서 보내는 confirm 요청" + " 결제를 진행하려면 HTTP Status 200 응답, 그렇지 않으면 500 응답 보내기" ) - @PostMapping("/confirm/") - public ResponseEntity confirmProductStock(@RequestParam String merchantUid, @RequestParam String impUid) throws IamportResponseException, IOException { - productPaymentService.confirmBeforePayment(merchantUid, impUid); - return ResponseEntity.ok().build(); - } - - - @Description("포트원 웹훅 수신 api") - @PostMapping("/portone-webhook") - public void handleIamportWebhook(@RequestBody PaymentWebHookRequest paymentWebHookRequest) throws IamportResponseException, IOException { - productPaymentService.handlePortOneProductWebhookEvent(paymentWebHookRequest); - } - - - - - - - /* - - @GetMapping("/") - @Secured("ROLE_CUSTOMER") - @Description("결제 내역 단건 조회") - public ResponseEntity getPaymentById(Long paymentId){ - PaymentResponse paymentResponse = paymentService.getPayment(paymentId); - return ResponseEntity.ok(paymentResponse); - } - - - @Secured("ROLE_CUSTOMER") - @Description("order에 해당하는 결제 내역 조회") - public ResponseEntity getPaymentByOrderId(@RequestParam Long orderId){ - PaymentResponse paymentResponse = paymentService.getPayment(orderId); - return ResponseEntity.ok(paymentResponse); - } - - */ - - -} - -/**TODO : filter 만들어서 webhook URL에 대해 IP 검증해야 함 - *@Description("포트원 webhook + 동기화") - @PostMapping("/portone-webhook") - * */ \ No newline at end of file diff --git a/src/main/java/poomasi/domain/order/_payment/dto/request/PaymentPreRegisterRequest.java b/src/main/java/poomasi/domain/order/_payment/dto/request/PaymentPreRegisterRequest.java deleted file mode 100644 index 24fb5aae..00000000 --- a/src/main/java/poomasi/domain/order/_payment/dto/request/PaymentPreRegisterRequest.java +++ /dev/null @@ -1,6 +0,0 @@ -package poomasi.domain.order._payment.dto.request; - -import java.math.BigDecimal; - -public record PaymentPreRegisterRequest(String merchantUid, BigDecimal amount) { -} diff --git a/src/main/java/poomasi/domain/order/_payment/dto/response/PaymentResponse.java b/src/main/java/poomasi/domain/order/_payment/dto/response/PaymentResponse.java deleted file mode 100644 index 14f8ff20..00000000 --- a/src/main/java/poomasi/domain/order/_payment/dto/response/PaymentResponse.java +++ /dev/null @@ -1,23 +0,0 @@ -package poomasi.domain.order._payment.dto.response; - -import poomasi.domain.order._payment.entity.Payment; -import poomasi.domain.order._payment.entity.PaymentMethod; - -import java.math.BigDecimal; - -public record PaymentResponse(Long paymentId, - BigDecimal totalPrice, - BigDecimal discountPrice, - BigDecimal finalPrice, - PaymentMethod paymentMethod -) { - public static PaymentResponse fromEntity(Payment payment){ - return new PaymentResponse( - payment.getId(), - payment.getTotalPrice(), - payment.getDiscountPrice(), - payment.getFinalPrice(), - payment.getPaymentMethod() - ); - } -} diff --git a/src/main/java/poomasi/domain/order/_payment/entity/Payment.java b/src/main/java/poomasi/domain/order/_payment/entity/Payment.java deleted file mode 100644 index 0983c68f..00000000 --- a/src/main/java/poomasi/domain/order/_payment/entity/Payment.java +++ /dev/null @@ -1,66 +0,0 @@ -package poomasi.domain.order._payment.entity; - -import jakarta.persistence.*; -import jdk.jfr.Description; -import lombok.Getter; -import poomasi.domain.order.entity.PaymentStatus; -import poomasi.domain.order.entity._farm.FarmOrder; -import poomasi.domain.order.entity._product.ProductOrder; - -import java.math.BigDecimal; -import java.util.List; - -@Entity -@Getter -public class Payment { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(name = "imp_uid") - @Description("아임포트 결제 imp_uid") - private String impUid; - - @OneToOne(mappedBy = "payment") - private ProductOrder productOrder; - - @Description("포트원 결제 금액") - private BigDecimal totalPrice; - - @Description("할인 가격") - private BigDecimal discountPrice; - - @Description("사용 포인트") - private BigDecimal usedPoint; - - @Description("배송비") - private BigDecimal deliveryFee; - - @Description("최종 가격") - private BigDecimal finalPrice; - - @Description("결제 방식") - @Enumerated(EnumType.STRING) - private PaymentMethod paymentMethod; - - @Description("checksum") - private BigDecimal checkSum; - - @Enumerated(EnumType.STRING) - private PaymentStatus paymentStatus = PaymentStatus.PAYMENT_PENDING; - - public void setCheckSum(BigDecimal checksum) { - this.checkSum = checksum; - } - - public void subtractCheckSum(BigDecimal checksum) { - this.checkSum = this.checkSum.subtract(checksum); - } - - public void setPaymentStatus(PaymentStatus paymentStatus) { - this.paymentStatus = paymentStatus; - } - - -} diff --git a/src/main/java/poomasi/domain/order/_payment/service/FarmPaymentService.java b/src/main/java/poomasi/domain/order/_payment/service/FarmPaymentService.java deleted file mode 100644 index ff842ae9..00000000 --- a/src/main/java/poomasi/domain/order/_payment/service/FarmPaymentService.java +++ /dev/null @@ -1,15 +0,0 @@ -package poomasi.domain.order._payment.service; - - -import com.siot.IamportRestClient.exception.IamportResponseException; -import org.springframework.stereotype.Service; - -import java.io.IOException; - -@Service -public class FarmPaymentService { - - - - -} diff --git a/src/main/java/poomasi/domain/order/_payment/service/ProductPaymentService.java b/src/main/java/poomasi/domain/order/_payment/service/ProductPaymentService.java deleted file mode 100644 index 45751886..00000000 --- a/src/main/java/poomasi/domain/order/_payment/service/ProductPaymentService.java +++ /dev/null @@ -1,191 +0,0 @@ -package poomasi.domain.order._payment.service; - -import com.siot.IamportRestClient.IamportClient; -import com.siot.IamportRestClient.exception.IamportResponseException; -import com.siot.IamportRestClient.request.CancelData; -import com.siot.IamportRestClient.response.IamportResponse; -import jdk.jfr.Description; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Isolation; -import org.springframework.transaction.annotation.Transactional; -import poomasi.domain.auth.security.userdetail.UserDetailsImpl; -import poomasi.domain.member.entity.Member; -import poomasi.domain.order._payment.config.IamportConfig; -import poomasi.domain.order._payment.dto.request.PaymentPreRegisterRequest; -import poomasi.domain.order._payment.dto.request.PaymentWebHookRequest; -import poomasi.domain.order._payment.dto.response.PaymentPreRegisterResponse; -import poomasi.domain.order._payment.dto.response.PaymentResponse; -import poomasi.domain.order._payment.entity.Payment; -import poomasi.domain.order._payment.repository.PaymentRepository; -import poomasi.domain.order._payment.util.PaymentUtil; -import poomasi.domain.order.entity._product.OrderedProduct; -import poomasi.domain.order.entity._product.ProductOrder; -import poomasi.domain.order.repository.ProductOrderRepository; -import poomasi.domain.product._cart.service.CartService; -import poomasi.domain.product.entity.Product; -import poomasi.global.error.BusinessException; -import poomasi.global.error.PaymentConfirmError; -import poomasi.global.error.PaymentConfirmException; - -import java.io.IOException; -import java.math.BigDecimal; -import java.util.List; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; - -import static poomasi.domain.order.entity.PaymentStatus.*; -import static poomasi.global.error.BusinessError.*; - -@Service -@RequiredArgsConstructor -@Slf4j -public class ProductPaymentService{ - - @Autowired - private final PaymentRepository paymentRepository; - private final ProductOrderRepository productOrderRepository; - private final PaymentUtil paymentUtil; - - private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); - private final AtomicBoolean isWebhookReceived = new AtomicBoolean(false); // 웹훅 수신 여부 체크 - //private final ThreadLocal isWebhookReceived = ThreadLocal.withInitial(AtomicBoolean::new); -> thread local로 제어 - - - @Description("사전 결제 등록. 프론트엔드에게 서버 merchant uid를 return 해야 함") - public PaymentPreRegisterResponse portonePrePaymentRegister(PaymentPreRegisterRequest paymentPreRegisterRequest) throws IOException, IamportResponseException { - - String merchantUid = paymentPreRegisterRequest.merchantUid(); - BigDecimal amount = paymentPreRegisterRequest.amount(); - - paymentUtil.sendPrepareData(merchantUid, amount); - return PaymentPreRegisterResponse.from( - paymentPreRegisterRequest.merchantUid() - ); - } - - @Transactional(isolation = Isolation.SERIALIZABLE) - @Description("포트원 결제 직전 바로 받는 confirm 요청. 40초 대기") - public void confirmBeforePayment(String impUid, String merchantUid) throws IOException, IamportResponseException { - - ProductOrder productOrder = productOrderRepository.findByMerchantUid(merchantUid) - .orElseThrow(() -> new BusinessException(PAYMENT_NOT_FOUND)); - - List orderedProductList = productOrder.getOrderedProducts(); - //수량 검증 - for(OrderedProduct orderedProduct : orderedProductList) { - Product product = orderedProduct.getProduct(); - Integer remainQuantity = product.getStock(); - Integer orderQuantity = orderedProduct.getCount(); - - //주문 재고가 남은 재고보다 많다면 500 + cancelReason 보내야 함 - if(orderQuantity > remainQuantity){ - throw new PaymentConfirmException(PaymentConfirmError.PAYMENT_PROUCT_CONFIRM_EXCEPTION); - } - } - - //결제 되어야 할 금액 - BigDecimal amountToBePaid = productOrder.getTotalAmount(); - - /* - * 1. 200ok 보내기 - * 2. 타이머 세팅 후 - * 타이머 타임 아웃 되면(웹훅을 받지 못하면) 결제 단건 api 호출 - * 만약 웹훅을 받으면 받은 데이터에서 getImpUid후, 결제 단건 호출 및 타이머 초기화 - * */ - - //재고 검증 완료 -> 200 OK 보내야 함 + 웹훅 수신 여부에 따라 분기 - scheduler.schedule(() -> { - try { - if (!isWebhookReceived.get()) { // 웹훅 수신 못 받으면 다시 보내기 - if(paymentUtil.validatePaymentAmount(impUid, amountToBePaid)){ - productOrder.setPaymentStatus(PAYMENT_COMPLETE); - decreaseStock(productOrder); //재고 차감 - }else{ - paymentUtil.cancelPaymentByImpUid(impUid); //실제 결제 된 금액과 결제 되어야 할 금액이 다르다면 -> 결제 취소 api를 호출해야 한다. - productOrder.setPaymentStatus(PAYMENT_DECLINED); - throw new BusinessException(PAYMENT_AMOUNT_MISMATCH); - } - } - } catch (IOException | IamportResponseException e) { - log.error(e.getMessage(), e); - throw new BusinessException(PAYMENT_BAD_REQUEST); - } - }, 40, TimeUnit.SECONDS); - - } - - @Description("웹훅 처리 service -> 결제 정상적으로 성공됨을 보장") - public void handlePortOneProductWebhookEvent(PaymentWebHookRequest paymentWebHookRequest) throws IOException, IamportResponseException { - - isWebhookReceived.set(true); //웹훅 수신 플래그 설정하기 - - String impUid = paymentWebHookRequest.impUid(); - String merchantUid = paymentWebHookRequest.merchantUid(); - ProductOrder productOrder = productOrderRepository.findByMerchantUid(merchantUid) - .orElseThrow(() -> new BusinessException(PAYMENT_NOT_FOUND)); - BigDecimal amountToBePaid = productOrder.getTotalAmount(); - - //결제 되어야 할 금액과 결제 된 금액이 같다면 - if(paymentUtil.validatePaymentAmount(impUid, amountToBePaid)){ - try{ - decreaseStock(productOrder); - productOrder.setPaymentStatus(PAYMENT_COMPLETE); - }catch(BusinessException businessException){ - productOrder.setPaymentStatus(PAYMENT_INSUFFICIENT_QUANTITY); - throw new BusinessException(PAYMENT_BAD_REQUEST); - } - }else{ - paymentUtil.cancelPaymentByImpUid(impUid); //실제 결제 된 금액과 결제 되어야 할 금액이 다르다면 -> 결제 취소 api를 호출해야 한다. - productOrder.setPaymentStatus(PAYMENT_DECLINED); - throw new BusinessException(PAYMENT_AMOUNT_MISMATCH); - } - } - - @Description("재고 차감 메서드. 감소하다 exception이 일어나면 rollback하고 결제 취소 해야 함") - @Transactional(isolation = Isolation.SERIALIZABLE) - public void decreaseStock(ProductOrder productOrder){ - List orderedProductList = productOrder.getOrderedProducts(); - for (OrderedProduct orderedProduct : orderedProductList){ - Product product = orderedProduct.getProduct(); - Integer remainQuantity = product.getStock(); //남은 수량 - Integer subtractQuantity = orderedProduct.getCount();//빼야 할 수량 - if(subtractQuantity > remainQuantity){ - throw new BusinessException(STOCK_QUANTITY_EXCEEDED); - } - product.subtractStock(subtractQuantity); - } - } - - - public PaymentResponse getPayment(Long paymentId) { - Payment payment = paymentRepository.findById(paymentId) - .orElseThrow(() -> new BusinessException(PAYMENT_NOT_FOUND)); - return PaymentResponse.fromEntity(payment); - } - - @Description("orderID로 결제 방법 찾는 메서드") - public PaymentResponse getPaymentByOrderId(Long orderId) { - Member member = getMember(); - Payment payment = paymentRepository.findById(orderId) - .orElseThrow(() -> new BusinessException(PAYMENT_NOT_FOUND)); - return PaymentResponse.fromEntity(payment); - } - - private Member getMember() { - Authentication authentication = SecurityContextHolder - .getContext().getAuthentication(); - Object impl = authentication.getPrincipal(); - Member member = ((UserDetailsImpl) impl).getMember(); - return member; - } - -} - - diff --git a/src/main/java/poomasi/domain/order/_payment/util/PaymentUtil.java b/src/main/java/poomasi/domain/order/_payment/util/PaymentUtil.java deleted file mode 100644 index c5c571d8..00000000 --- a/src/main/java/poomasi/domain/order/_payment/util/PaymentUtil.java +++ /dev/null @@ -1,88 +0,0 @@ -package poomasi.domain.order._payment.util; - - -import com.siot.IamportRestClient.IamportClient; -import com.siot.IamportRestClient.exception.IamportResponseException; -import com.siot.IamportRestClient.request.CancelData; -import com.siot.IamportRestClient.request.PrepareData; -import com.siot.IamportRestClient.response.IamportResponse; -import com.siot.IamportRestClient.response.Payment; -import jdk.jfr.Description; -import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Transactional; -import poomasi.global.error.BusinessError; -import poomasi.global.error.BusinessException; - -import java.io.IOException; -import java.math.BigDecimal; - -@Component -public class PaymentUtil { - - private final IamportClient iamportClient; - - @Autowired - public PaymentUtil(IamportClient iamportClient) { - this.iamportClient = iamportClient; - } - - @Description("포트원에서 결제 금액 조회하는 메서드") - public BigDecimal getPaymentAmount(String impUid) throws IOException, IamportResponseException { - IamportResponse iamportResponse = getSingleTransaction(impUid); - return iamportResponse.getResponse().getAmount(); - } - - @Description("단건 결제 조회 API") - public IamportResponse getSingleTransaction(String impUid) throws IOException, IamportResponseException { - IamportResponse iamportResponse = iamportClient.paymentByImpUid(impUid); - return iamportResponse; - } - - @Description("결제 취소 api") - public void cancelPaymentByImpUid(String impUid) throws IOException, IamportResponseException { - CancelData cancelDate = new CancelData(impUid, false); - iamportClient.cancelPaymentByImpUid(cancelDate); - } - - @Transactional - @Description("imp uid로 결제 부분 환불 api 호출") - public void partialRefundByImpUid(String impUid, BigDecimal checkSum, BigDecimal amount, String reason) throws IOException, IamportResponseException { - CancelData cancelData = new CancelData(impUid, true, amount); - cancelData.setChecksum(checkSum); - cancelData.setReason(reason); - iamportClient.cancelPaymentByImpUid(cancelData); - } - - @Transactional - @Description("merchant Uid로 결제 부분 환불 api 호출") - public void partialRefundByMerchantUid(String merchantUid, BigDecimal checkSum, BigDecimal amount, String reason) throws IOException, IamportResponseException { - CancelData cancelData = new CancelData(merchantUid, false, amount); - cancelData.setChecksum(checkSum); - cancelData.setReason(reason); - iamportClient.cancelPaymentByImpUid(cancelData); - } - - - @Description("사전 결제 데이터 전송") - public void sendPrepareData(String merchantUid, BigDecimal amount) throws IOException, IamportResponseException { - PrepareData prepareData = this.generatePrepareData(merchantUid, amount); - iamportClient.postPrepare(prepareData); - } - - @Description("단건 조회 후, 결제 되어야 할 금액과 결제 된 금액이 같은지 확인하는 메서드") - public boolean validatePaymentAmount(String impUid, BigDecimal amountToBePaid) throws IOException, IamportResponseException{ - IamportResponse iamportResponse = getSingleTransaction(impUid); //내가 보냄 - BigDecimal amount = iamportResponse.getResponse().getAmount(); - if(amountToBePaid.compareTo(amount)!=0){ - return false; - } - return true; - } - - @Description("사전 결제를 위한 Prepare Data를 만드는 메서드") - private PrepareData generatePrepareData(String merchantUid, BigDecimal amount) { - return new PrepareData(merchantUid, amount); - } -} diff --git a/src/main/java/poomasi/domain/order/controller/OrderController.java b/src/main/java/poomasi/domain/order/controller/OrderController.java index d36111e6..0fafc2e8 100644 --- a/src/main/java/poomasi/domain/order/controller/OrderController.java +++ b/src/main/java/poomasi/domain/order/controller/OrderController.java @@ -1,93 +1,44 @@ package poomasi.domain.order.controller; -import com.siot.IamportRestClient.exception.IamportResponseException; import jdk.jfr.Description; import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; import org.springframework.security.access.annotation.Secured; import org.springframework.web.bind.annotation.*; -import poomasi.domain.farm.service.FarmService; -import poomasi.domain.order._payment.dto.request.PaymentPreRegisterRequest; -import poomasi.domain.order._payment.service.ProductPaymentService; -import poomasi.domain.order.dto.request.ProductOrderRegisterRequest; -import poomasi.domain.order.service.FarmOrderService; -import poomasi.domain.order.service.ProductOrderService; +import poomasi.domain.order.dto.request.PreOrderRequest; +import poomasi.domain.order.dto.response.OrderResponse; +import poomasi.domain.order.dto.response.PreOrderResponse; +import poomasi.domain.order.service.OrderService; -import java.io.IOException; +import java.util.List; -@Slf4j @RestController -@RequestMapping("api/order") +@RequestMapping("api/orders") @RequiredArgsConstructor public class OrderController { - private final ProductOrderService productOrderService; - private final FarmOrderService farmOrderService; - private final ProductPaymentService productPaymentService; + private final OrderService orderService; + @GetMapping("") @Secured({"ROLE_CUSTOMER", "ROLE_FARMER"}) - @PostMapping("/product/pre-order") - @Description("product 사전 결제") - public ResponseEntity createProductPreOrder(@RequestBody ProductOrderRegisterRequest productOrderRegisterRequest) throws IOException, IamportResponseException { - PaymentPreRegisterRequest paymentPreRegisterRequest = productOrderService.productPreOrderRegister(productOrderRegisterRequest); - return ResponseEntity.ok( - productPaymentService.portonePrePaymentRegister(paymentPreRegisterRequest) - ); + public ResponseEntity getAllOrders(@RequestParam(defaultValue = "1") int page, + @RequestParam(defaultValue = "10") int size) { + List orders = orderService.getOrders(page, size); + return ResponseEntity.ok(orders); } @Secured({"ROLE_CUSTOMER", "ROLE_FARMER"}) - @PostMapping("/farm/pre-order") - @Description("farm 사전 결제") - public ResponseEntity createFarmPreOrder() throws IOException, IamportResponseException { - PaymentPreRegisterRequest paymentPreRegisterRequest = productOrderService.farmPreOrderRegister(); - return ResponseEntity.ok( - productPaymentService.portonePrePaymentRegister(paymentPreRegisterRequest) - ); - } - - @Description("멤버의 결제 완료가 된 단건 주문 조회. 특정 건만 조회") - @GetMapping("/{orderId}") - public ResponseEntity getAllOrdersByMember(@PathVariable Long orderId) { - return ResponseEntity.ok( - productOrderService.findOrderByMemberId(orderId) - ); - } - - @Description("멤버의 결제 완료가 된 전체 주문 목록 조회. 전체 주문 목록 조회") - @GetMapping("/") - public ResponseEntity getOrdersByMember() { - return ResponseEntity.ok( - productOrderService.findAllOrdersByMemberId() - ); - } - - @Description("어떤 주문 대한 디테일 조회. ex) 주소, 상세주소, 배송 요청 사항 등") - @GetMapping("/{orderId}/details") - public ResponseEntity getOrderDetailsByMember(@PathVariable Long orderId) { - return ResponseEntity.ok( - productOrderService.findOrderDetailsByOrderId(orderId) - ); - } + @PostMapping("/pre-order") + @Description("product 사전 주문 등록") + public ResponseEntity createProductPreOrder(@RequestBody PreOrderRequest preOrderRequest) { + PreOrderResponse preOrderResponse = orderService.productPreOrderRegister(preOrderRequest); + return ResponseEntity.ok(preOrderResponse); - @Description("어떤 주문에 대한 모든 품목 디테일 조회.ex) 품목 가격, 이름, 등등..") - @GetMapping("/{orderId}/product/details") - public ResponseEntity getOrderProductDetailsByOrderId(@PathVariable Long orderId) { - return ResponseEntity.ok( - productOrderService.findAllOrderProductDetails(orderId) - ); } - @Description("어떤 주문에 대한 품목의 디테일 단건 조회. ex) 품목 가격, 이름, 등등..") - @GetMapping("/{orderId}/product/details/{detailsId}") - public ResponseEntity getOrderProductDetailsByDetailsId(@PathVariable Long orderId, @PathVariable Long detailsId) { - return ResponseEntity.ok( - productOrderService.findOrderProductDetailsById(orderId, detailsId) - ); - } } diff --git a/src/main/java/poomasi/domain/order/dto/request/PreOrderRequest.java b/src/main/java/poomasi/domain/order/dto/request/PreOrderRequest.java new file mode 100644 index 00000000..13ac538b --- /dev/null +++ b/src/main/java/poomasi/domain/order/dto/request/PreOrderRequest.java @@ -0,0 +1,16 @@ +package poomasi.domain.order.dto.request; + +import java.util.List; + +public record PreOrderRequest( + List productOrderRequest, + String destinationAddress, + String destinationAddressDetail, + String deliveryRequest) +{ + public PreOrderRequest { + if (deliveryRequest == null) { + deliveryRequest = "조심히 다뤄 주세요"; + } + } +} diff --git a/src/main/java/poomasi/domain/order/dto/request/ProductOrderRegisterRequest.java b/src/main/java/poomasi/domain/order/dto/request/ProductOrderRegisterRequest.java deleted file mode 100644 index 55b5d106..00000000 --- a/src/main/java/poomasi/domain/order/dto/request/ProductOrderRegisterRequest.java +++ /dev/null @@ -1,6 +0,0 @@ -package poomasi.domain.order.dto.request; - -public record ProductOrderRegisterRequest(String destinationAddress, - String destinationAddressDetail, - String deliveryRequest) { -} diff --git a/src/main/java/poomasi/domain/order/dto/request/ProductOrderRequest.java b/src/main/java/poomasi/domain/order/dto/request/ProductOrderRequest.java new file mode 100644 index 00000000..c3e98d8f --- /dev/null +++ b/src/main/java/poomasi/domain/order/dto/request/ProductOrderRequest.java @@ -0,0 +1,11 @@ +package poomasi.domain.order.dto.request; + +import jdk.jfr.Description; + +@Description("cart에서 상품 정보 넘어오는 정보") +public record ProductOrderRequest( + Long productId, + Integer count +) { + +} diff --git a/src/main/java/poomasi/domain/order/dto/response/OrderDetailsResponse.java b/src/main/java/poomasi/domain/order/dto/response/OrderDetailsResponse.java deleted file mode 100644 index 749f9dab..00000000 --- a/src/main/java/poomasi/domain/order/dto/response/OrderDetailsResponse.java +++ /dev/null @@ -1,17 +0,0 @@ -package poomasi.domain.order.dto.response; - -import poomasi.domain.order.entity._product.ProductOrderDetails; - -public record OrderDetailsResponse( - String address, - String addressDetails, - String deliveryRequest -) { - public static OrderDetailsResponse fromEntity(ProductOrderDetails productOrderDetails) { - return new OrderDetailsResponse( - productOrderDetails.getDestinationAddress(), - productOrderDetails.getDestinationAddressDetail(), - productOrderDetails.getDeliveryRequest() - ); - } -} diff --git a/src/main/java/poomasi/domain/order/dto/response/OrderProductDetailsResponse.java b/src/main/java/poomasi/domain/order/dto/response/OrderProductDetailsResponse.java deleted file mode 100644 index 1726d646..00000000 --- a/src/main/java/poomasi/domain/order/dto/response/OrderProductDetailsResponse.java +++ /dev/null @@ -1,28 +0,0 @@ -package poomasi.domain.order.dto.response; - -import poomasi.domain.order.entity._product.OrderedProduct; - -import java.math.BigDecimal; - -public record OrderProductDetailsResponse( - Long orderId, - Long orderProductDetailsId, - Long productId, - String productName, - Integer count, - BigDecimal price, //총 결제 금액 - String invoiceNumber - ) { - public static OrderProductDetailsResponse fromEntity(OrderedProduct orderedProduct) { - return new OrderProductDetailsResponse( - orderedProduct.getOrderId(), - orderedProduct.getId(), - orderedProduct.getProduct().getId(), - orderedProduct.getProductName(), - orderedProduct.getCount(), - orderedProduct.getPrice(), - orderedProduct.getInvoiceNumber() - ); - } - -} diff --git a/src/main/java/poomasi/domain/order/dto/response/OrderResponse.java b/src/main/java/poomasi/domain/order/dto/response/OrderResponse.java index e5621032..48683100 100644 --- a/src/main/java/poomasi/domain/order/dto/response/OrderResponse.java +++ b/src/main/java/poomasi/domain/order/dto/response/OrderResponse.java @@ -1,24 +1,34 @@ package poomasi.domain.order.dto.response; -import poomasi.domain.order.entity._product.ProductOrder; +import poomasi.domain.order.entity.Order; +import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.List; import java.util.stream.Collectors; public record OrderResponse(Long orderId, - String merchantUid, LocalDateTime createdAt, - List orderProductDetailsResponseList) { - public static OrderResponse fromEntity(ProductOrder productOrder) { + BigDecimal totalAmount, + String address, + String addressDetail, + String deliveryRequest, + String paymentMethod, + List orderedProductResponse) { + public static OrderResponse fromEntity(Order order) { return new OrderResponse( - productOrder.getId(), - productOrder.getMerchantUid(), - productOrder.getCreatedAt(), - productOrder.getOrderedProducts() + order.getId(), + order.getCreatedAt(), + order.getTotalAmount(), + order.getAddress(), + order.getAddressDetail(), + order.getDeliveryRequest(), + order.getPaymentMethod(), + order.getOrderedProducts() .stream() - .map(OrderProductDetailsResponse::fromEntity) + .map(OrderedProductResponse::fromEntity) .collect(Collectors.toList()) - ); + + ); } } diff --git a/src/main/java/poomasi/domain/order/dto/response/OrderedProductResponse.java b/src/main/java/poomasi/domain/order/dto/response/OrderedProductResponse.java new file mode 100644 index 00000000..69095fb3 --- /dev/null +++ b/src/main/java/poomasi/domain/order/dto/response/OrderedProductResponse.java @@ -0,0 +1,34 @@ +package poomasi.domain.order.dto.response; + +import poomasi.domain.order.entity.OrderedProduct; + +import java.math.BigDecimal; + +public record OrderedProductResponse( + Long productId, + String productName, + Integer count, + String productDescription, + BigDecimal price, //총 결제 금액 + String storeName, + String invoiceStatus, + String deliveryService, + String invoiceNumber, + boolean isReviewed +) { + public static OrderedProductResponse fromEntity(OrderedProduct orderedProduct) { + return new OrderedProductResponse( + orderedProduct.getProductId(), + orderedProduct.getProductName(), + orderedProduct.getCount(), + orderedProduct.getProductDescription(), + orderedProduct.getPrice(), + orderedProduct.getStoreName(), + orderedProduct.getOrderedProductStatus().toString(), + orderedProduct.getDeliveryService(), + orderedProduct.getInvoiceNumber(), + orderedProduct.getReview() != null + ); + } + +} diff --git a/src/main/java/poomasi/domain/order/dto/response/PreOrderResponse.java b/src/main/java/poomasi/domain/order/dto/response/PreOrderResponse.java new file mode 100644 index 00000000..31709a63 --- /dev/null +++ b/src/main/java/poomasi/domain/order/dto/response/PreOrderResponse.java @@ -0,0 +1,4 @@ +package poomasi.domain.order.dto.response; + +public record PreOrderResponse(String merchantUid) { +} diff --git a/src/main/java/poomasi/domain/order/entity/Order.java b/src/main/java/poomasi/domain/order/entity/Order.java new file mode 100644 index 00000000..65e78661 --- /dev/null +++ b/src/main/java/poomasi/domain/order/entity/Order.java @@ -0,0 +1,112 @@ +package poomasi.domain.order.entity; + + +import jakarta.persistence.*; +import jdk.jfr.Description; +import jdk.jfr.Timestamp; +import lombok.Builder; +import lombok.Getter; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.UpdateTimestamp; +import poomasi.domain.member.entity.Member; +import poomasi.payment.entity.Payment; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@Entity +@Table(name = "product_order") +@Getter +@SQLDelete(sql = "UPDATE product_order SET deleted_at = current_timestamp WHERE id = ?") +public class Order { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @JoinColumn(name = "member_id") + @ManyToOne(fetch = FetchType.LAZY) + @Description("주문 한 사람을 참조한다.") + private Member member; + + @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY) + private Payment payment; + + @Column(name = "created_at") + @CreationTimestamp + private LocalDateTime createdAt = LocalDateTime.now(); + + @Column(name = "updated_at") + @UpdateTimestamp + private LocalDateTime updateAt = LocalDateTime.now(); + + @Column(name = "deleted_at") + @Timestamp + private LocalDateTime deletedAt; + + @Column(name = "total_amount") + @Description("총 결제 금액 : 모든 품목들의 배송비 + 가격") + private BigDecimal totalAmount; + + @Column(name = "merchant_uid") + @Description("서버 내부 주문 id(아임포트 id)") + private String merchantUid; + + @Column(name = "ordered_products_id") + @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) + private List orderedProducts = new ArrayList<>(); + + @Column(name = "address") + @Description("도착 주소") + private String address; + + @Column(name = "address_detail") + @Description("도착 상세 주소") + private String addressDetail; + + @Description("배송 요청 사항") + @Column(name = "delivery_request", length = 255) + private String deliveryRequest; + + public Order(){} + + @Builder + public Order(Member member, + Payment payment, + BigDecimal totalAmount, + String address, + String addressDetail, + String deliveryRequest ) { + this.member = member; + this.payment = payment; + this.totalAmount = totalAmount; + this.address = address; + this.addressDetail = addressDetail; + this.deliveryRequest = deliveryRequest; + } + + public String getPaymentMethod(){ + return this.payment.getPaymentMethod().toString(); + } + + public void addTotalAmount(BigDecimal amount){ + this.totalAmount = this.totalAmount.add(amount); + } + + public void addOrderedProduct(OrderedProduct orderedProduct){ + this.orderedProducts.add(orderedProduct); + orderedProduct.setOrder(this); + } + + public void setMerchantUid(String merchantUid){ + this.merchantUid = merchantUid; + } + + public void setPayment(Payment payment){ + this.payment=payment; + payment.setOrder(this); + } +} diff --git a/src/main/java/poomasi/domain/order/entity/OrderedProduct.java b/src/main/java/poomasi/domain/order/entity/OrderedProduct.java new file mode 100644 index 00000000..a3e456c5 --- /dev/null +++ b/src/main/java/poomasi/domain/order/entity/OrderedProduct.java @@ -0,0 +1,163 @@ +package poomasi.domain.order.entity; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Objects; +import jakarta.persistence.*; +import jdk.jfr.Description; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import poomasi.domain.aftersales.entity.ProductAfterSales; +import poomasi.domain.member.entity.Member; +import poomasi.domain.product.entity.Product; +import poomasi.domain.review.entity.Review; +import poomasi.payment.entity.Payment; + +import java.math.BigDecimal; + +import static poomasi.domain.order.entity.OrderedProductStatus.PENDING_SELLER_APPROVAL; + +@Entity +@Table(name = "ordered_products") +@Getter +@NoArgsConstructor +public class OrderedProduct { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "ordered_product_id") + private Long id; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(nullable = true, name = "product_after_sales_id") + private ProductAfterSales productAfterSales; + + @Setter + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "product_order_id") + private Order order; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "product_id") + private Product product; + + @Column(name = "product_description", nullable = true) + private String productDescription; + + @Column(name = "product_name", length = 255) + private String productName; + + @Column(name = "image_url") + private String imageUrl; + + @Column(name = "grow_env") + private String growEnv; + + @Description("구매 당시 1개당 가격") + private BigDecimal price; + + @Description("구매 수량") + @Column(name = "count") + private Integer count; + + @Description("택배 회사") + @Column(name = "delivery_service", nullable = true) + private String deliveryService; + + @Description("송장 번호") + @Column(name = "invoice_number", nullable = true) + private String invoiceNumber; + + @Enumerated(EnumType.STRING) + private OrderedProductStatus orderedProductStatus = PENDING_SELLER_APPROVAL; + + @Description("TODO : product의 delivery fee를 참조해야 한다.") + private BigDecimal deliveryFee; + + @Description("flag가 설정되어 있으면 배송비 환불하지 않아도 된다") + private boolean isCanceled = false; + + @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY) + @Setter + private Review review; + + + @Builder + public OrderedProduct(Product product, Order order, String productDescription, + String productName, BigDecimal price, Integer count, BigDecimal deliveryFee, String imageUrl, String growEnv) { + this.product = product; + this.order = order; + this.productDescription = productDescription; + this.productName = productName; + this.price = price; + this.count = count; + this.deliveryFee = deliveryFee; + this.imageUrl = imageUrl; + this.growEnv = growEnv; + } + + public void setInvoiceNumber(String invoiceNumber) { + this.invoiceNumber = invoiceNumber; + } + + public void setOrderedProductStatus(OrderedProductStatus orderedProductStatus) { + this.orderedProductStatus = orderedProductStatus; + } + + public void addProductAfterSalesDetail(ProductAfterSales productAfterSales) { + this.productAfterSales = productAfterSales; + productAfterSales.setOrderedProduct(this); + } + + public Long getOrderId() { + return this.order.getId(); + } + + public Member getStoreOwner(){ + return this.product.getStore().getOwner(); + } + + public String getStoreName(){ + return this.product.getStore().getName(); + } + + public String getStoreAddress() { + return this.product.getStore().getAddress(); + } + + public Long getProductId(){ + return this.product.getId(); + } + + public Payment getPayment(){ + return this.order.getPayment(); + } + + public BigDecimal calculateCancelAmount(){ + BigDecimal count = new BigDecimal(this.count); + return this.price.multiply(count).add(deliveryFee); + } + + public BigDecimal calculateRefundAmount(){ + BigDecimal count = new BigDecimal(this.count); + return this.price.multiply(count); + } +} + diff --git a/src/main/java/poomasi/domain/order/entity/OrderedProductStatus.java b/src/main/java/poomasi/domain/order/entity/OrderedProductStatus.java new file mode 100644 index 00000000..5c230a9f --- /dev/null +++ b/src/main/java/poomasi/domain/order/entity/OrderedProductStatus.java @@ -0,0 +1,17 @@ +package poomasi.domain.order.entity; + +public enum OrderedProductStatus { + + PENDING_SELLER_APPROVAL, // 판매자 수락 전 (주문 완료 후 대기 상태) + SHIPMENT_STARTED, // 배송 시작 (판매자 수락을 하면 바뀌는 상태) + IN_TRANSIT, // 배송 중 + DELIVERED, // 배송 완료 + + //환불 + REFUND_REQUESTED, // 환불 요청됨 + REFUND_APPROVED, // 환불 승인됨 + + //주문 취소 + CANCEL_PENDING // 주문 취소 상태 + ; +} \ No newline at end of file diff --git a/src/main/java/poomasi/domain/order/entity/PaymentStatus.java b/src/main/java/poomasi/domain/order/entity/PaymentStatus.java deleted file mode 100644 index b8b4dc21..00000000 --- a/src/main/java/poomasi/domain/order/entity/PaymentStatus.java +++ /dev/null @@ -1,10 +0,0 @@ -package poomasi.domain.order.entity; - -public enum PaymentStatus { - PAYMENT_PENDING, // 결제 대기 중 - PAYMENT_COMPLETE, // 결제 성공 - PAYMENT_DECLINED, // 결제 거부 - PAYMENT_INSUFFICIENT_QUANTITY - ; -} - diff --git a/src/main/java/poomasi/domain/order/entity/_abstract/AbstractOrder.java b/src/main/java/poomasi/domain/order/entity/_abstract/AbstractOrder.java deleted file mode 100644 index d7a6ae7d..00000000 --- a/src/main/java/poomasi/domain/order/entity/_abstract/AbstractOrder.java +++ /dev/null @@ -1,72 +0,0 @@ -package poomasi.domain.order.entity._abstract; - -import jakarta.persistence.*; -import jdk.jfr.Description; -import jdk.jfr.Timestamp; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.experimental.SuperBuilder; -import org.hibernate.annotations.CreationTimestamp; -import org.hibernate.annotations.UpdateTimestamp; -import poomasi.domain.member.entity.Member; -import poomasi.domain.order._payment.entity.Payment; - -import java.math.BigDecimal; -import java.time.LocalDateTime; - - -@MappedSuperclass -@Getter -@Setter -@SuperBuilder // 빌더 패턴을 사용하도록 설정 -@NoArgsConstructor -public abstract class AbstractOrder { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @JoinColumn(name = "member_id") - @ManyToOne(fetch = FetchType.LAZY) - @Description("주문 한 사람을 참조한다.") - private Member member; - - @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY) - private Payment payment; - - @Column(name = "created_at") - @CreationTimestamp - private LocalDateTime createdAt = LocalDateTime.now(); - - @Column(name = "updated_at") - @UpdateTimestamp - private LocalDateTime updateAt = LocalDateTime.now(); - - @Column(name = "deleted_at") - @Timestamp - private LocalDateTime deletedAt; - - @Column(name = "total_amount") - @Description("총 결제 금액") - private BigDecimal totalAmount; - - public void setCheckSum(BigDecimal checkSum) { - this.payment.setCheckSum(checkSum); - } - - public void subtractChecksum(BigDecimal checkSum) { - this.payment.subtractCheckSum(checkSum); - } - - public BigDecimal getCheckSum(){ - return this.payment.getCheckSum(); - } - - public void setTotalAmount(BigDecimal totalAmount) { - this.totalAmount = totalAmount; - } - - -} - diff --git a/src/main/java/poomasi/domain/order/entity/_farm/FarmOrder.java b/src/main/java/poomasi/domain/order/entity/_farm/FarmOrder.java deleted file mode 100644 index 46501720..00000000 --- a/src/main/java/poomasi/domain/order/entity/_farm/FarmOrder.java +++ /dev/null @@ -1,43 +0,0 @@ -package poomasi.domain.order.entity._farm; - -import jakarta.persistence.*; -import jdk.jfr.Description; -import org.hibernate.annotations.Comment; -import poomasi.domain.order._payment.entity.Payment; -import poomasi.domain.order.entity._abstract.AbstractOrder; - -import java.util.Date; - -//@Entity -//@Table(name = "farm_order") -public class FarmOrder extends AbstractOrder { - /* - @OneToOne(fetch=FetchType.LAZY) - private FarmOrderDetails farmOrderDetails; - - @Column(name = "owner_id") - private Long ownerId; - - @Comment("농장 간단 설명") - private String description; - - @Comment("도로명 주소") - private String destinationAddress; - - @Comment("상세 주소") - private String addressDetail; - - @Comment("위도") - private Double latitude; - - @Comment("경도") - private Double longitude; - */ - - @Column(name = "merchant_uid") - @Description("서버 내부 주문 id(아임포트 id)") - private String merchantUid = "f" + new Date().getTime(); - - -} - diff --git a/src/main/java/poomasi/domain/order/entity/_farm/FarmOrderDetails.java b/src/main/java/poomasi/domain/order/entity/_farm/FarmOrderDetails.java deleted file mode 100644 index 7b6e194c..00000000 --- a/src/main/java/poomasi/domain/order/entity/_farm/FarmOrderDetails.java +++ /dev/null @@ -1,28 +0,0 @@ -package poomasi.domain.order.entity._farm; - -import jakarta.persistence.*; -import poomasi.domain.farm.entity.Farm; - -//@Entity -//@Table(name="farm_order_details") -public class FarmOrderDetails { - /* - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "farm_order_details_id") - private Long id; - - @OneToOne - private FarmOrder farmOrder; - - - @Column(name="farm_name") - private String farmName; - - @Column(name="farm_address") - private String farmAddress; - - -*/ - -} diff --git a/src/main/java/poomasi/domain/order/entity/_farm/OrderedFarm.java b/src/main/java/poomasi/domain/order/entity/_farm/OrderedFarm.java deleted file mode 100644 index 1c785e39..00000000 --- a/src/main/java/poomasi/domain/order/entity/_farm/OrderedFarm.java +++ /dev/null @@ -1,20 +0,0 @@ -package poomasi.domain.order.entity._farm; - - -import jakarta.persistence.*; -import poomasi.domain.farm.entity.Farm; - -@Entity -@Table(name = "ordered_farm") -public class OrderedFarm { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "ordered_farm_id") - private Long id; - - @OneToOne - private Farm farm; - - -} diff --git a/src/main/java/poomasi/domain/order/entity/_product/OrderedProduct.java b/src/main/java/poomasi/domain/order/entity/_product/OrderedProduct.java deleted file mode 100644 index 13c988c0..00000000 --- a/src/main/java/poomasi/domain/order/entity/_product/OrderedProduct.java +++ /dev/null @@ -1,137 +0,0 @@ -package poomasi.domain.order.entity._product; - - -import jakarta.persistence.*; -import jdk.jfr.Description; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import poomasi.domain.order._aftersales.entity._product.ProductAfterSalesDetail; -import poomasi.domain.product.entity.Product; - -import java.io.Serializable; -import java.math.BigDecimal; -import java.util.List; - -@Entity -@Table(name = "ordered_products") -@Getter -@NoArgsConstructor -public class OrderedProduct implements Serializable { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "ordered_product_id") - private Long id; - - @OneToMany(fetch = FetchType.LAZY) - @JoinColumn(nullable = true, name = "product_after_sales_detail_id") - private List productAfterSalesDetails; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "product_order_id") - private ProductOrder productOrder; - - //FIXME : store Id를 참조해야 한다. - //나중에 store Id로 변경해야 한다 - //private Store store; - //private Long storeId; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "product_id") - private Product product; - - @Column(name = "product_description", nullable = true) - private String productDescription; - - @Column(name = "product_name", length = 255) - private String productName; - - @Description("구매 당시 1개당 가격") - private BigDecimal price; - - @Description("구매 수량") - @Column(name="count") - private Integer count; - - @Description("송장 번호") - @Column(name = "invoice_number", nullable = true) - private String invoiceNumber; - - private OrderedProductStatus orderedProductStatus = OrderedProductStatus.PENDING_SELLER_APPROVAL; - - @Description("TODO : product의 delivery fee를 참조해야 한다.") - private BigDecimal deliveryFee; - - @Description("환불 가능한 남은 수량") - @Column(name = "refundable_count") - private Integer adjustableQuantity; - - @Description("취소 된 수량") - @Column(name = "cacnel_quantity") - private Integer cancelQuantity; - - @Description("flag가 설정되어 있으면 배송비 환불하지 않아도 된다") - private boolean isCanceled = false; - - // 웹훅 받아서 조회해야 함. - // findByInvoiceNumber 후 - // web hook controller 만들어서 - // 배송 상태 적절히 변경해야 함 - - @Builder - public OrderedProduct(Product product, ProductOrder productOrder, String productDescription, String productName, BigDecimal price, Integer count) { - this.product = product; - this.productOrder = productOrder; - this.productDescription = productDescription; - this.productName = productName; - this.price = price; - this.count = count; - } - - public void setInvoiceNumber(String invoiceNumber) { - this.invoiceNumber = invoiceNumber; - } - - public void setOrderedProductStatus(OrderedProductStatus orderedProductStatus) { - this.orderedProductStatus = orderedProductStatus; - } - - public void addProductAfterSalesDetail(ProductAfterSalesDetail productAfterSalesDetail) { - this.productAfterSalesDetails.add(productAfterSalesDetail); - productAfterSalesDetail.setOrderedProduct(this); - } - - public Long getOrderId(){ - return this.productOrder.getId(); - } - - public void subtractRefundableCount(Integer refundableCount) { - this.adjustableQuantity -= refundableCount; - } - - public void addCancelQuantity(Integer cancelQuantity) { - this.isCanceled = true; - this.cancelQuantity += cancelQuantity; - } - - public OrderedProductStatus changeOrderedProductStatusToCancel() { - if (this.count == this.cancelQuantity) { - this.orderedProductStatus = OrderedProductStatus.CANCELLED; - } - return this.orderedProductStatus; - } - - public String getStoreAddress(){ - //return this.store.getStoreAddress() - return "TODO : store의 address를 참조해야 함"; - } - - public String getStoreAddressDetail(){ - //return this.store.getStoreAddressDetail() - return "TODO: store의 address detail을 참조해야 함"; - } - - -} - diff --git a/src/main/java/poomasi/domain/order/entity/_product/OrderedProductStatus.java b/src/main/java/poomasi/domain/order/entity/_product/OrderedProductStatus.java deleted file mode 100644 index 407a9a9f..00000000 --- a/src/main/java/poomasi/domain/order/entity/_product/OrderedProductStatus.java +++ /dev/null @@ -1,31 +0,0 @@ -package poomasi.domain.order.entity._product; - -public enum OrderedProductStatus { - - PENDING_SELLER_APPROVAL, // 판매자 수락 전 (주문 완료 후 대기 상태) - SHIPMENT_STARTED, // 배송 시작 (판매자 수락을 하면 바뀌는 상태) - IN_TRANSIT, // 배송 중 - DELIVERED, // 배송 완료 - CANCELLED, // 주문 취소 완료 (취소가 최종적으로 완료된 상태) - - //교환 - EXCHANGE_PENDING, // 교환 요청 대기중 - EXCHANGE_APPROVED, // 교환 요청 승인됨 - EXCHANGE_IN_PROGRESS, // 교환 처리 중 (배송 중이거나 준비 중) - EXCHANGE_COMPLETED, // 교환 완료 - EXCHANGE_DENIED, // 교환 요청 거절됨 - - //환불 - REFUND_REQUESTED, // 환불 요청됨 - REFUND_APPROVED, // 환불 승인됨 - REFUND_SHIPMENT_STARTED, // 환불 배송 시작 (반품 물품의 배송 시작) - REFUND_IN_TRANSIT, // 환불 배송 중 (반품 물품이 배송 중) - REFUND_DELIVERED, // 환불 배송 완료 (반품 물품이 도착함) - REFUND_IN_PROGRESS, // 환불 처리 중 (반품 수거 중이거나 처리 대기 중) - REFUND_COMPLETED, // 환불 완료 - REFUND_DENIED, // 환불 요청 거절됨 - - //주문 취소 - CANCEL_PENDING, // 주문 취소 대기 중 (취소 요청을 받은 상태) - ; -} \ No newline at end of file diff --git a/src/main/java/poomasi/domain/order/entity/_product/ProductOrder.java b/src/main/java/poomasi/domain/order/entity/_product/ProductOrder.java deleted file mode 100644 index 90c237e2..00000000 --- a/src/main/java/poomasi/domain/order/entity/_product/ProductOrder.java +++ /dev/null @@ -1,68 +0,0 @@ -package poomasi.domain.order.entity._product; - - -import jakarta.persistence.*; -import jdk.jfr.Description; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.experimental.SuperBuilder; -import org.hibernate.annotations.SQLDelete; -import poomasi.domain.member.entity.Member; -import poomasi.domain.order.entity.PaymentStatus; -import poomasi.domain.order.entity._abstract.AbstractOrder; - -import java.math.BigDecimal; -import java.util.Date; -import java.util.List; - -@Entity -@Table(name = "product_order") -@Getter -@SuperBuilder -@SQLDelete(sql = "UPDATE product_order SET deleted_at = current_timestamp WHERE id = ?") -public class ProductOrder extends AbstractOrder { - - @Column(name = "merchant_uid") - @Description("서버 내부 주문 id(아임포트 id)") - private String merchantUid = "p" + new Date().getTime(); - - @Column(name = "ordered_products_id") - @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) - private List orderedProducts; - - @OneToOne - @JoinColumn(name = "product_order_details_id") // 외래 키 지정 - @Description("상품 배송지, 요청 사항") - private ProductOrderDetails productOrderDetails; - - public ProductOrder(){ - - } - - public void addOrderedProduct(OrderedProduct orderedProduct) { - this.orderedProducts.add(orderedProduct); - } - - public void setMerchantUid(String merchantUid) { - this.merchantUid = merchantUid; - } - - public String getImpUid(){ - return this.getPayment().getImpUid(); - } - - public PaymentStatus getPaymentStatus(){ - return this.getPayment().getPaymentStatus(); - } - - public void setPaymentStatus(PaymentStatus paymentStatus){ - this.getPayment().setPaymentStatus(paymentStatus); - } - - public void setProductOrderDetails(ProductOrderDetails productOrderDetails){ - this.productOrderDetails = productOrderDetails; - productOrderDetails.setProductOrder(this); - } - -} diff --git a/src/main/java/poomasi/domain/order/entity/_product/ProductOrderDetails.java b/src/main/java/poomasi/domain/order/entity/_product/ProductOrderDetails.java deleted file mode 100644 index 0cf2e983..00000000 --- a/src/main/java/poomasi/domain/order/entity/_product/ProductOrderDetails.java +++ /dev/null @@ -1,47 +0,0 @@ -package poomasi.domain.order.entity._product; - -import jakarta.persistence.*; -import jdk.jfr.Description; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Entity -@Table(name="product_order_details") -@Getter -@NoArgsConstructor -public class ProductOrderDetails { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @OneToOne(mappedBy = "productOrderDetails", cascade = CascadeType.ALL) // 필드명으로 지정 - private ProductOrder productOrder; - - @Column(name = "return_address") - @Description("도착 주소") - private String destinationAddress; - - @Column(name = "destination_address_detail") - @Description("도착 상세 주소") - private String destinationAddressDetail; - - @Description("배송 요청 사항") - @Column(name = "delivery_request", length = 255) - private String deliveryRequest; - - - @Builder - public ProductOrderDetails(ProductOrder productOrder, String destinationAddress, String destinationAddressDetail, String deliveryRequest) { - this.productOrder = productOrder; - this.destinationAddress = destinationAddress; - this.destinationAddressDetail = destinationAddressDetail; - this.deliveryRequest = deliveryRequest; - } - - public void setProductOrder(ProductOrder productOrder) { - this.productOrder = productOrder; - } - -} diff --git a/src/main/java/poomasi/domain/order/entity/_product/ProductsOrderDetailsStatus.java b/src/main/java/poomasi/domain/order/entity/_product/ProductsOrderDetailsStatus.java deleted file mode 100644 index 53a53fab..00000000 --- a/src/main/java/poomasi/domain/order/entity/_product/ProductsOrderDetailsStatus.java +++ /dev/null @@ -1,11 +0,0 @@ -package poomasi.domain.order.entity._product; - -public enum ProductsOrderDetailsStatus { - WAITING_SHIPPING, - IN_SHIPPING, // 배송 중 - DELIVERED_COMPLETE, // 배송 완료 - CANCELLED, // 취소 - RETURNED, // 환불 - EXCHANGED // 교환 - ; -} diff --git a/src/main/java/poomasi/domain/order/repository/OrderRepository.java b/src/main/java/poomasi/domain/order/repository/OrderRepository.java new file mode 100644 index 00000000..77b84fe7 --- /dev/null +++ b/src/main/java/poomasi/domain/order/repository/OrderRepository.java @@ -0,0 +1,17 @@ +package poomasi.domain.order.repository; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import poomasi.domain.order.entity.Order; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface OrderRepository extends JpaRepository { + Optional findByMerchantUid(String merchantUid); + Page findById(Long id, Pageable pageable); + Page findByMemberId(Long memberId, Pageable pageable); +} diff --git a/src/main/java/poomasi/domain/order/repository/OrderedProductRepository.java b/src/main/java/poomasi/domain/order/repository/OrderedProductRepository.java index 4827c4de..c920dcdf 100644 --- a/src/main/java/poomasi/domain/order/repository/OrderedProductRepository.java +++ b/src/main/java/poomasi/domain/order/repository/OrderedProductRepository.java @@ -1,9 +1,9 @@ package poomasi.domain.order.repository; import org.springframework.data.jpa.repository.JpaRepository; -import poomasi.domain.order.entity._product.OrderedProduct; - -import java.util.List; +import org.springframework.stereotype.Repository; +import poomasi.domain.order.entity.OrderedProduct; +@Repository public interface OrderedProductRepository extends JpaRepository { } diff --git a/src/main/java/poomasi/domain/order/repository/ProductOrderRepository.java b/src/main/java/poomasi/domain/order/repository/ProductOrderRepository.java deleted file mode 100644 index c439df8a..00000000 --- a/src/main/java/poomasi/domain/order/repository/ProductOrderRepository.java +++ /dev/null @@ -1,14 +0,0 @@ -package poomasi.domain.order.repository; - -import org.springframework.data.jpa.repository.JpaRepository; -import poomasi.domain.order.entity._product.ProductOrder; - -import java.util.List; -import java.util.Optional; - -public interface ProductOrderRepository extends JpaRepository { - List findByMemberId(Long memberId); - Optional findByMerchantUid(String merchantUid); - //Optional findByImpUid(String impUid); - //Optional findByMerchantUidAndImpUid(String merchantUid, String impUid); -} diff --git a/src/main/java/poomasi/domain/order/service/FarmOrderService.java b/src/main/java/poomasi/domain/order/service/FarmOrderService.java deleted file mode 100644 index 825369cc..00000000 --- a/src/main/java/poomasi/domain/order/service/FarmOrderService.java +++ /dev/null @@ -1,8 +0,0 @@ -package poomasi.domain.order.service; - - -import org.springframework.stereotype.Service; - -@Service -public class FarmOrderService implements OrderService { -} diff --git a/src/main/java/poomasi/domain/order/service/OrderService.java b/src/main/java/poomasi/domain/order/service/OrderService.java index 1f89f786..7fec2874 100644 --- a/src/main/java/poomasi/domain/order/service/OrderService.java +++ b/src/main/java/poomasi/domain/order/service/OrderService.java @@ -1,5 +1,219 @@ package poomasi.domain.order.service; -public interface OrderService { +import jdk.jfr.Description; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import poomasi.domain.auth.security.userdetail.UserDetailsImpl; +import poomasi.domain.member.entity.Member; +import poomasi.domain.order.dto.request.PreOrderRequest; +import poomasi.domain.order.dto.request.ProductOrderRequest; +import poomasi.domain.order.dto.response.OrderResponse; +import poomasi.domain.order.dto.response.PreOrderResponse; +import poomasi.domain.order.entity.Order; +import poomasi.domain.order.entity.OrderedProduct; +import poomasi.domain.order.entity.OrderedProductStatus; +import poomasi.domain.order.repository.OrderRepository; +import poomasi.domain.order.repository.OrderedProductRepository; +import poomasi.domain.product._cart.entity.Cart; +import poomasi.domain.product._cart.repository.CartRepository; +import poomasi.domain.product._cart.service.CartService; +import poomasi.domain.product.entity.Product; +import poomasi.domain.product.service.ProductService; +import poomasi.global.error.ApplicationException; +import poomasi.global.error.BusinessException; +import poomasi.payment.entity.ItemType; +import poomasi.payment.entity.Payment; +import poomasi.payment.util.PaymentUtil; + +import java.math.BigDecimal; +import java.util.List; +import java.util.stream.Collectors; + +import static poomasi.domain.order.entity.OrderedProductStatus.DELIVERED; +import static poomasi.domain.order.entity.OrderedProductStatus.PENDING_SELLER_APPROVAL; +import static poomasi.global.error.ApplicationError.PAYMENT_NOT_FOUND; +import static poomasi.global.error.BusinessError.*; + +@RequiredArgsConstructor +@Service +@Slf4j +@Transactional(readOnly = true) +public class OrderService { + + private final OrderRepository orderRepository; + private final CartRepository cartRepository; + + private final CartService cartService; + private final ProductService productService; + private final PaymentUtil paymentUtil; + private final OrderedProductRepository orderedProductRepository; + + public List getOrders(int page, int size){ + Member member = getMember(); + Long memberId = member.getId(); + Page orders = orderRepository.findByMemberId( + memberId, PageRequest. + of(page, size, Sort.by("createdAt") + .descending() + ) + ); + return orders.stream() + .map(OrderResponse::fromEntity) + .collect(Collectors.toList()); + } + @Description("사전 주문 생성 메서드") + @Transactional + public PreOrderResponse productPreOrderRegister(PreOrderRequest preOrderRequest) { + + Member member = getMember(); + Long memberId = member.getId(); + + List productOrderRequest = preOrderRequest.productOrderRequest(); + List cartList = cartService.getCart(memberId); + + // 예외처리 : 요청의 productId와 cart의 productId를 뽑아내고 값을 비교한다. + List productIdRequest = productOrderRequest.stream().map(ProductOrderRequest::productId).collect(Collectors.toList()); + List productIdInCart = cartService.extractProductIds(cartList); + + if (productIdRequest.size() != productIdInCart.size() || !productIdRequest.containsAll(productIdInCart)) { + throw new BusinessException(CART_PRODUCT_MISMATCHING); + } + + String destinationAddress = preOrderRequest.destinationAddress(); + String destinationAddressDetail = preOrderRequest.destinationAddressDetail(); + String deliveryRequest = preOrderRequest.deliveryRequest(); + + // 검증 완료 + Order order = Order + .builder() + .member(member) + .totalAmount(BigDecimal.ZERO) + .address(destinationAddress) + .addressDetail(destinationAddressDetail) + .deliveryRequest(deliveryRequest) + .build(); + + orderRepository.save(order); + + for (ProductOrderRequest productOrderRequest1 : productOrderRequest) { + Long productId = productOrderRequest1.productId(); + Product product = productService.findValidProductById(productId); + Integer productStock = product.getStock(); + Integer quantityInOrderRequest = productOrderRequest1.count(); + // 1. 주문 상품이 재고보다 더 많으면 + if (quantityInOrderRequest > productStock) { + throw new BusinessException(PRODUCT_STOCK_ZERO); + } + + product.subtractStock(quantityInOrderRequest); + + // 2. 자신의 장바구니 아이템이 아니라면 + Cart cart = cartRepository.findByMemberIdAndProductId(member.getId(), productId) + .orElseThrow(()-> new BusinessException(CART_PRODUCT_MISMATCHING)); + + cartRepository.delete(cart); + + // 3. 검증이 완료 되었다면 orderedProduct를 추가한다. + String productDescription = product.getDescription(); + String productName = product.getName(); + BigDecimal price = product.getPrice(); //1개당 가격 + + OrderedProduct orderedProduct = OrderedProduct + .builder() + .product(product) + .order(order) + .productDescription(productDescription) + .productName(productName) + .price(price) + .count(quantityInOrderRequest) + .deliveryFee(product.getShippingFee()) + .growEnv(product.getGrowEnv()) + .build(); + + order.addTotalAmount(price.multiply(BigDecimal.valueOf(quantityInOrderRequest.longValue()))); + order.addOrderedProduct(orderedProduct); + } + + String merchantUid = paymentUtil.createMerchantUid(ItemType.PRODUCT); + order.setMerchantUid(merchantUid); + + + Payment payment = Payment + .builder() + .totalAmount(order.getTotalAmount()) + .checkSum(order.getTotalAmount()) + .itemType(ItemType.PRODUCT) + .build(); + + order.setPayment(payment); + + orderRepository.save(order); + + paymentUtil.sendPrepareData(merchantUid, order.getTotalAmount()); + return new PreOrderResponse(order.getMerchantUid()); + } + + + public Order findByMerchantUid(String merchantUid) { + return orderRepository.findByMerchantUid(merchantUid) + .orElseThrow(() -> new ApplicationException(PAYMENT_NOT_FOUND)); + } + + + @Description("요청이 구매자 소유인지 확인하고 판매자 대기중인지 체크하는 메서드") + public OrderedProduct validateProductCancelable(Long memberId, Long orderedProductId){ + + OrderedProduct orderedProduct = orderedProductRepository.findById(orderedProductId) + .orElseThrow(()-> new BusinessException(ORDERED_PRODUCT_NOT_FOUND)); + if(orderedProduct.getOrder().getMember().getId() != memberId){ + new BusinessException(ORDERED_PRODUCT_NOT_FOUND); + } + OrderedProductStatus orderedProductStatus = orderedProduct.getOrderedProductStatus(); + if(orderedProductStatus != PENDING_SELLER_APPROVAL){ + throw new BusinessException(SHIPPING_ALREADY_IN_PROGRESS); + } + return orderedProduct; + } + + + @Description("요청이 구매자 소유인지 확인하고 환불 가능한지 확인하는 메섣,") + public OrderedProduct validateProductRefundable(Long memberId, Long orderedProductId){ + + OrderedProduct orderedProduct = orderedProductRepository.findById(orderedProductId) + .orElseThrow(()-> new BusinessException(ORDERED_PRODUCT_NOT_FOUND)); + + if(orderedProduct.getOrder().getMember().getId() != memberId){ + new BusinessException(ORDERED_PRODUCT_NOT_FOUND); + } + OrderedProductStatus orderedProductStatus = orderedProduct.getOrderedProductStatus(); + if(orderedProductStatus != DELIVERED){ + throw new BusinessException(REFUND_BAD_REQUEST); + } + return orderedProduct; + } + + + + + @Description("security context에서 member 객체 가져오는 메서드") + private Member getMember() { + Authentication authentication = SecurityContextHolder + .getContext().getAuthentication(); + Object impl = authentication.getPrincipal(); + Member member = ((UserDetailsImpl) impl).getMember(); + return member; + } + + } + + + diff --git a/src/main/java/poomasi/domain/order/service/ProductOrderService.java b/src/main/java/poomasi/domain/order/service/ProductOrderService.java deleted file mode 100644 index 5edd4359..00000000 --- a/src/main/java/poomasi/domain/order/service/ProductOrderService.java +++ /dev/null @@ -1,224 +0,0 @@ -package poomasi.domain.order.service; - -import jdk.jfr.Description; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import poomasi.domain.auth.security.userdetail.UserDetailsImpl; -import poomasi.domain.member.entity.Member; -import poomasi.domain.order._payment.dto.request.PaymentPreRegisterRequest; -import poomasi.domain.order.dto.request.ProductOrderRegisterRequest; -import poomasi.domain.order.dto.response.OrderDetailsResponse; -import poomasi.domain.order.dto.response.OrderProductDetailsResponse; -import poomasi.domain.order.dto.response.OrderResponse; -import poomasi.domain.order.entity._product.OrderedProduct; -import poomasi.domain.order.entity._product.ProductOrder; -import poomasi.domain.order.entity._product.ProductOrderDetails; -import poomasi.domain.order.entity.PaymentStatus; -import poomasi.domain.order.repository.OrderedProductRepository; -import poomasi.domain.order.repository.ProductOrderRepository; -import poomasi.domain.product._cart.entity.Cart; -import poomasi.domain.product._cart.repository.CartRepository; -import poomasi.domain.product.entity.Product; -import poomasi.domain.product.repository.ProductRepository; -import poomasi.global.error.BusinessException; - -import java.math.BigDecimal; -import java.util.List; -import java.util.stream.Collectors; - -import static poomasi.global.error.BusinessError.*; - -@RequiredArgsConstructor -@Service -@Slf4j -public class ProductOrderService { - - private final ProductOrderRepository productOrderRepository; - private final CartRepository cartRepository; - private final ProductRepository productRepository; - private final OrderedProductRepository orderedProductRepository; - - @Transactional - public PaymentPreRegisterRequest productPreOrderRegister(ProductOrderRegisterRequest productOrderRegisterRequest){ - Member member = getMember(); - Long memberId = member.getId(); - List cartList = cartRepository.findByMemberIdAndSelected(memberId); - - String destinationAddress = productOrderRegisterRequest.destinationAddress(); - String destinationAddressDetail = productOrderRegisterRequest.destinationAddressDetail(); - String deliveryRequest = productOrderRegisterRequest.deliveryRequest(); - - ProductOrder productOrder = new ProductOrder() - .builder() - .member(member) - .build(); - - ProductOrderDetails productOrderDetails = new ProductOrderDetails() - .builder() - .destinationAddress(destinationAddress) - .destinationAddressDetail(destinationAddressDetail) - .deliveryRequest(deliveryRequest) - .build(); - - productOrder.setProductOrderDetails(productOrderDetails); - - //cart에 있는 총 가격 계산하기 - BigDecimal totalPrice = BigDecimal.ZERO; - - // cart 돌면서 productOrder details 추가 - for (Cart cart : cartList) { - Long productId = cart.getProductId(); - Product product = productRepository.findById(productId) - .orElseThrow(() -> new BusinessException(PRODUCT_NOT_FOUND)); - - Integer productStock = product.getStock(); - Integer quantityInCart = cart.getCount(); - - // 현재 남아있는 재고보다 더 많이 요청하면 - // pending 상태로 저장이 안 됨. - if(quantityInCart > productStock){ - throw new BusinessException(PRODUCT_STOCK_ZERO); - } - - String productDescription = product.getDescription(); - Integer count = cart.getCount(); - String productName = product.getName(); - BigDecimal price = BigDecimal.valueOf(product.getPrice()); - - //TODO : Store store = product.getStore(); - - OrderedProduct orderedProduct = OrderedProduct - .builder() - .product(product) - .productOrder(productOrder) - //.store(store) - .productDescription(productDescription) - .productName(productName) - .price(price) - .count(count) - .build(); - - productOrder.addOrderedProduct(orderedProduct); - totalPrice = totalPrice.add(price); - } - productOrder.setTotalAmount(totalPrice); - productOrder.setCheckSum(totalPrice); - productOrderRepository.save(productOrder); - - String merchantUid = productOrder.getMerchantUid(); - return new PaymentPreRegisterRequest(merchantUid, totalPrice); - } - - @Transactional - //TODO : 만들어야 합니다 ~ - public PaymentPreRegisterRequest farmPreOrderRegister(){ - Member member = getMember(); - String merchantUid = ""; - BigDecimal totalPrice = BigDecimal.ZERO; - - return new PaymentPreRegisterRequest(merchantUid, totalPrice); - } - - - @Description("멤버 ID 기반으로 모든 order 다 들고 오는 메서드") - public List findAllOrdersByMemberId(){ - Member member = getMember(); - Long memberId = member.getId(); - List productOrderList = productOrderRepository.findByMemberId(memberId); - return productOrderList - .stream() - .map(OrderResponse::fromEntity) - .collect(Collectors.toList() - ); - } - - @Description("멤버 id 기반으로 특정 orderId 들고오는 메서드") - public OrderResponse findOrderByMemberId(Long orderId){ - Member member = getMember(); - ProductOrder productOrder = productOrderRepository.findById(orderId) - .orElseThrow(()-> new BusinessException(ORDER_NOT_FOUND)); - - validateOrderOwnership(productOrder, member); - return OrderResponse.fromEntity(productOrder); - } - - - @Description("orderId 기반으로 order details(주소, 상세주소, 배송 요청 사항 ..등) 들고오는 메서드") - public OrderDetailsResponse findOrderDetailsByOrderId(Long orderId){ - ProductOrder productOrder = productOrderRepository.findById(orderId) - .orElseThrow(()-> new BusinessException(ORDER_NOT_FOUND)); - ProductOrderDetails productOrderDetails = productOrder.getProductOrderDetails(); - - return OrderDetailsResponse.fromEntity(productOrderDetails); - } - - - - @Description("orderId에 해당하는 order product details 가져오는 메서드") - public List findAllOrderProductDetails(Long orderId){ - Member member = getMember(); - ProductOrder productOrder = productOrderRepository.findById(orderId) - .orElseThrow(()-> new BusinessException(ORDER_NOT_FOUND)); - validateOrderOwnership(productOrder, member); - return productOrder.getOrderedProducts() - .stream() - .map(OrderProductDetailsResponse::fromEntity) - .collect(Collectors.toList() - ); - } - - - @Description("orderId에 해당하는 order product Details의 단건 조회") - public OrderProductDetailsResponse findOrderProductDetailsById(Long orderId, Long orderProductDetailsId){ - Member member = getMember(); - OrderedProduct orderedProduct = orderedProductRepository.findById(orderProductDetailsId) - .orElseThrow(()-> new BusinessException(ORDER_PRODUCT_DETAILS_NOT_FOUND)); - ProductOrder productOrder = orderedProduct.getProductOrder(); - - // productOrder product details의 주인 productOrder 검사 그리고 , orderId의 주인 member 검사 - validateOrderProductDetailsByOrderId(productOrder, orderId); - validateOrderOwnership(productOrder, member); - - return OrderProductDetailsResponse.fromEntity(orderedProduct); - } - - - @Description("member의 order인지 검사하는 메서드") - private void validateOrderOwnership(ProductOrder productOrder, Member member) { - if (!productOrder.getMember().getId().equals(member.getId())) { - throw new BusinessException(ORDER_NOT_OWNED_EXCEPTION); - } - } - - @Description("orderId에 해당하는 productOrder Product Details인지 조회하는 메서드") - private void validateOrderProductDetailsByOrderId(ProductOrder productOrder, Long orderId) { - if(productOrder.getId()!=orderId){ - throw new BusinessException(ORDER_PRODUCT_DETAILS_NOT_OWNED_EXCEPTION); - } - } - - - @Description("주문 상태를 변경하는 메서드") - private void changeOrderStatus(Long orderId, PaymentStatus paymentStatus){ - ProductOrder productOrder = productOrderRepository.findById(orderId) - .orElseThrow(()-> new BusinessException(ORDER_NOT_FOUND)); - productOrder.setPaymentStatus(paymentStatus); - } - - - @Description("security context에서 member 객체 가져오는 메서드") - private Member getMember() { - Authentication authentication = SecurityContextHolder - .getContext().getAuthentication(); - Object impl = authentication.getPrincipal(); - Member member = ((UserDetailsImpl) impl).getMember(); - return member; - } - -} - - diff --git a/src/main/java/poomasi/domain/product/_cart/controller/CartController.java b/src/main/java/poomasi/domain/product/_cart/controller/CartController.java index fb8a187c..85c56550 100644 --- a/src/main/java/poomasi/domain/product/_cart/controller/CartController.java +++ b/src/main/java/poomasi/domain/product/_cart/controller/CartController.java @@ -1,86 +1,59 @@ package poomasi.domain.product._cart.controller; -import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PatchMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import poomasi.domain.product._cart.dto.CartRegisterRequest; -import poomasi.domain.product._cart.dto.CartRequest; +import org.springframework.web.bind.annotation.*; +import poomasi.domain.auth.security.userdetail.UserDetailsImpl; +import poomasi.domain.member.entity.Member; import poomasi.domain.product._cart.dto.CartResponse; import poomasi.domain.product._cart.service.CartService; +import java.util.List; + @Controller @RequiredArgsConstructor +@RequestMapping("/api/carts") public class CartController { private final CartService cartService; - //장바구니 정보 - @GetMapping("/api/cart") - public ResponseEntity getCart() { - List cart = cartService.getCart(); + //장바구니 모든 정보 + @GetMapping("") + public ResponseEntity getCart(@AuthenticationPrincipal UserDetailsImpl userDetails) { + Member member = userDetails.getMember(); + List cart = cartService.getCart(member); return ResponseEntity.ok().body(cart); } - //장바구니 선택한거만 가격 - @GetMapping("/api/cart/price") - public ResponseEntity getPrice() { - Integer price = cartService.getPrice(); - return ResponseEntity.ok().body(price); - } - //장바구니 추가 - @PostMapping("/api/cart") - public ResponseEntity addCart(@RequestBody CartRegisterRequest cartRequest) { - Long cartId = cartService.addCart(cartRequest); + @PostMapping("/{productId}") + public ResponseEntity addCart( + @AuthenticationPrincipal UserDetailsImpl userDetails, + @PathVariable Long productId) { + Member member = userDetails.getMember(); + Long cartId = cartService.addCart(member, productId); return new ResponseEntity<>(cartId, HttpStatus.CREATED); } - //장바구니 선택/해제 - @PostMapping("/api/cart/select") - public ResponseEntity changeSelect(@RequestBody CartRequest cartRequest) { - cartService.changeSelect(cartRequest); - return ResponseEntity.ok().build(); - } - //장바구니 삭제 - @DeleteMapping("/api/cart") - public ResponseEntity removeCart(@RequestBody CartRequest cartRequest) { - cartService.deleteCart(cartRequest); - return ResponseEntity.ok().build(); - } - - //장바구니 선택된거 삭제 - @DeleteMapping("/api/cart/selected") - public ResponseEntity removeSelected() { - cartService.removeSelected(); + @DeleteMapping("/{cartId}") + public ResponseEntity removeCart( + @AuthenticationPrincipal UserDetailsImpl userDetails, + @PathVariable Long cartId) { + Member member = userDetails.getMember(); + cartService.deleteCart(member, cartId); return ResponseEntity.ok().build(); } //장바구니 전부 삭제 - @DeleteMapping("/api/cart/all") - public ResponseEntity removeAllCart() { - cartService.deleteAll(); - return ResponseEntity.ok().build(); - } - - //장바구니 물건 개수 추가 - @PatchMapping("/api/cart/add") - public ResponseEntity addCount(@RequestBody CartRequest cartRequest) { - cartService.addCount(cartRequest); + @DeleteMapping("/all") + public ResponseEntity removeAllCart(@AuthenticationPrincipal UserDetailsImpl userDetails) { + Member member = userDetails.getMember(); + cartService.deleteAll(member); return ResponseEntity.ok().build(); } - //장바구니 물건 개수 감소 - @PatchMapping("/api/cart/sub") - public ResponseEntity subCount(@RequestBody CartRequest cartRequest) { - cartService.subCount(cartRequest); - return ResponseEntity.ok().build(); - } } diff --git a/src/main/java/poomasi/domain/product/_cart/dto/CartRegisterRequest.java b/src/main/java/poomasi/domain/product/_cart/dto/CartRegisterRequest.java deleted file mode 100644 index 90189d7d..00000000 --- a/src/main/java/poomasi/domain/product/_cart/dto/CartRegisterRequest.java +++ /dev/null @@ -1,21 +0,0 @@ -package poomasi.domain.product._cart.dto; - -import lombok.extern.slf4j.Slf4j; -import poomasi.domain.member.entity.Member; -import poomasi.domain.product._cart.entity.Cart; - -@Slf4j -public record CartRegisterRequest( - Long productId, - Integer count -) { - - public Cart toEntity(Member member) { - return Cart.builder() - .memberId(member.getId()) - .productId(productId) - .selected(Boolean.TRUE) - .count(count != null ? count : 1) - .build(); - } -} diff --git a/src/main/java/poomasi/domain/product/_cart/dto/CartRequest.java b/src/main/java/poomasi/domain/product/_cart/dto/CartRequest.java deleted file mode 100644 index 98148a41..00000000 --- a/src/main/java/poomasi/domain/product/_cart/dto/CartRequest.java +++ /dev/null @@ -1,7 +0,0 @@ -package poomasi.domain.product._cart.dto; - -public record CartRequest( - Long cartId -) { - -} diff --git a/src/main/java/poomasi/domain/product/_cart/dto/CartResponse.java b/src/main/java/poomasi/domain/product/_cart/dto/CartResponse.java index 4710c67e..09db1b78 100644 --- a/src/main/java/poomasi/domain/product/_cart/dto/CartResponse.java +++ b/src/main/java/poomasi/domain/product/_cart/dto/CartResponse.java @@ -1,12 +1,12 @@ package poomasi.domain.product._cart.dto; +import java.math.BigDecimal; + public record CartResponse( Long cartId, String productName, - Long productPrice, - Integer productCount, - Boolean isSelected, - String farmName + BigDecimal productPrice, + String storeName ) { } diff --git a/src/main/java/poomasi/domain/product/_cart/entity/Cart.java b/src/main/java/poomasi/domain/product/_cart/entity/Cart.java index 7a50b0bb..71985cad 100644 --- a/src/main/java/poomasi/domain/product/_cart/entity/Cart.java +++ b/src/main/java/poomasi/domain/product/_cart/entity/Cart.java @@ -1,12 +1,11 @@ package poomasi.domain.product._cart.entity; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; +import jakarta.persistence.*; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import poomasi.domain.member.entity.Member; +import poomasi.domain.product.entity.Product; @Entity @NoArgsConstructor @@ -17,32 +16,24 @@ public class Cart { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - private Boolean selected; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT)) + private Member member; - private Long memberId; - - private Long productId; - - private Integer count; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "product_id", foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT)) + private Product product; @Builder - public Cart(Long id, Long memberId, Long productId, Boolean selected, Integer count) { + public Cart(Long id, Member member, Product product) { this.id = id; - this.memberId = memberId; - this.productId = productId; - this.selected = selected; - this.count = count; + this.member = member; + this.product = product; } - public void addCount() { - this.count += 1; + public boolean containsProduct(){ + return product != null; } - public void subCount() { - this.count -= 1; - } - public void changeSelect() { - this.selected = !this.selected; - } } diff --git a/src/main/java/poomasi/domain/product/_cart/repository/CartRepository.java b/src/main/java/poomasi/domain/product/_cart/repository/CartRepository.java index 726c0b87..3bf2893f 100644 --- a/src/main/java/poomasi/domain/product/_cart/repository/CartRepository.java +++ b/src/main/java/poomasi/domain/product/_cart/repository/CartRepository.java @@ -1,42 +1,31 @@ package poomasi.domain.product._cart.repository; -import java.util.List; -import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; +import poomasi.domain.member.entity.Member; import poomasi.domain.product._cart.dto.CartResponse; import poomasi.domain.product._cart.entity.Cart; +import java.util.List; +import java.util.Optional; + @Repository public interface CartRepository extends JpaRepository { - @Query("SELECT new poomasi.domain.product._cart.dto.CartResponse(c.id, p.name, p.price, c.count, c.selected,f.name) " - + - "FROM Cart c " + - "INNER JOIN Product p ON c.productId = p.id " + - "INNER JOIN Farm f ON f.ownerId = :memberId") - List findByMemberId(Long memberId); - - @Query("select sum(p.price * c.count) from Cart c inner join Product p on c.productId = p.id where c.memberId = :memberId and c.selected = true") - Integer getPrice(Long memberId); + @Query("SELECT new poomasi.domain.product._cart.dto.CartResponse(c.id, c.product.name, c.product.price, c.member.store.name) from Cart c where c.member = :member") + List findByMember(Member member); - @Query("select c from Cart c where c.memberId = :memberId and c.productId = :productId") Optional findByMemberIdAndProductId(Long memberId, Long productId); - @Modifying - @Transactional - @Query("delete from Cart c where c.memberId = :memberId") - void deleteAllByMemberId(Long memberId); + List findByMemberId(Long memberId); + + @Query("SELECT e FROM Cart e WHERE e.id IN :ids") + List getCartsByIdList(List ids); @Modifying @Transactional - @Query("delete from Cart c where c.memberId = :memberId and c.selected = true") - void deleteByMemberIdAndSelected(Long memberId); - - // order 만들 때 사용할 거 - @Query("select c from Cart c where c.memberId = :memberId and c.selected = true") - List findByMemberIdAndSelected(Long memberId); + void deleteAllByMemberId(Long memberId); } diff --git a/src/main/java/poomasi/domain/product/_cart/service/CartService.java b/src/main/java/poomasi/domain/product/_cart/service/CartService.java index ae9262f3..fcacab54 100644 --- a/src/main/java/poomasi/domain/product/_cart/service/CartService.java +++ b/src/main/java/poomasi/domain/product/_cart/service/CartService.java @@ -1,131 +1,101 @@ package poomasi.domain.product._cart.service; -import java.util.List; -import java.util.Optional; +import jdk.jfr.Description; import lombok.RequiredArgsConstructor; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import poomasi.domain.auth.security.userdetail.UserDetailsImpl; import poomasi.domain.member.entity.Member; -import poomasi.domain.product._cart.dto.CartRegisterRequest; -import poomasi.domain.product._cart.dto.CartRequest; import poomasi.domain.product._cart.dto.CartResponse; import poomasi.domain.product._cart.entity.Cart; import poomasi.domain.product._cart.repository.CartRepository; import poomasi.domain.product.entity.Product; -import poomasi.domain.product.repository.ProductRepository; +import poomasi.domain.product.service.ProductService; import poomasi.global.error.BusinessError; import poomasi.global.error.BusinessException; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + @Service @RequiredArgsConstructor public class CartService { private final CartRepository cartRepository; - private final ProductRepository productRepository; + private final ProductService productService; + + public List getCart(Member member) { + return cartRepository.findByMember(member); + } @Transactional - public Long addCart(CartRegisterRequest cartRequest) { - Member member = getMember(); - Product product = getProductById(cartRequest.productId()); - - Optional cartOptional = cartRepository.findByMemberIdAndProductId(member.getId(), - product.getId()); - if (cartOptional.isPresent()) { - Cart cart = cartOptional.get(); - return cart.getId(); - } + public Long addCart(Member member, Long productId) { + Product product = getProductById(productId); + + Optional cartOptional = + cartRepository.findByMemberIdAndProductId(member.getId(), product.getId()); + + //이미 담은 상품임 + if (cartOptional.isPresent()) + return cartOptional.get().getId(); + + Cart cart = Cart.builder() + .member(member) + .product(product) + .build(); - Cart cart = cartRequest.toEntity(member); - if (product.getStock() < cart.getCount()) { - throw new BusinessException(BusinessError.PRODUCT_STOCK_ZERO); - } cart = cartRepository.save(cart); return cart.getId(); - } @Transactional - public void deleteCart(CartRequest cartRequest) { - Member member = getMember(); - Cart cart = getCartById(cartRequest.cartId()); + @Description("카트 한 개 삭제") + public void deleteCart(Member member, Long cartId) { + Cart cart = getCartById(cartId); checkAuth(member, cart); cartRepository.delete(cart); } - private void checkAuth(Member member, Cart cart) { - if (!member.getId().equals(cart.getMemberId())) { - throw new BusinessException(BusinessError.MEMBER_ID_MISMATCH); - } - } - @Transactional - public void addCount(CartRequest cartRequest) { - Member member = getMember(); - Cart cart = getCartById(cartRequest.cartId()); - Product product = getProductById(cart.getProductId()); - if (product.getStock().equals(cart.getCount())) { - throw new BusinessException(BusinessError.PRODUCT_STOCK_ZERO); - } - checkAuth(member, cart); - cart.addCount(); + @Description("전부 삭제") + public void deleteAll(Member member) { + cartRepository.deleteAllByMemberId(member.getId()); } - @Transactional - public void subCount(CartRequest cartRequest) { - Member member = getMember(); - Cart cart = getCartById(cartRequest.cartId()); - checkAuth(member, cart); - cart.subCount(); + + @Description("요청한 사람이랑 카트 주인이랑 같은지 확인") + private void checkAuth(Member member, Cart cart) { + if (!member.getId().equals(cart.getMember().getId())) { + throw new BusinessException(BusinessError.MEMBER_ID_MISMATCH); + } } - private Cart getCartById(Long cartId) { + public Cart getCartById(Long cartId) { return cartRepository.findById(cartId) .orElseThrow(() -> new BusinessException(BusinessError.CART_NOT_FOUND)); } - private Product getProductById(Long productId) { - return productRepository.findById(productId) - .orElseThrow(() -> new BusinessException(BusinessError.PRODUCT_NOT_FOUND)); + public List getCart(Long memberId){ + return cartRepository.findByMemberId(memberId); } - public List getCart() { - Member member = getMember(); - return cartRepository.findByMemberId(member.getId()); - } - - private Member getMember() { - Authentication authentication = SecurityContextHolder - .getContext().getAuthentication(); - Object impl = authentication.getPrincipal(); - Member member = ((UserDetailsImpl) impl).getMember(); - return member; + private Product getProductById(Long productId) { + return productService.findProductById(productId); } - @Transactional - public void changeSelect(CartRequest cartRequest) { - Member member = getMember(); - Cart cart = getCartById(cartRequest.cartId()); - checkAuth(member, cart); - cart.changeSelect(); + @Description("order 만들 때 사용할 거") + public List getCartsByIdList(List ids) { + List orderList = cartRepository.getCartsByIdList(ids); + cartRepository.deleteAll(orderList); + return orderList; } - public Integer getPrice() { - Member member = getMember(); - return cartRepository.getPrice(member.getId()); - } + public List extractProductIds(List cartList) { + return cartList.stream() + .filter(Cart::containsProduct) // product가 존재하는지 확인 + .map(cart -> cart.getProduct().getId()) // product의 id를 추출 + .collect(Collectors.toList()); // List으로 수집 - @Transactional - public void deleteAll() { - Member member = getMember(); - cartRepository.deleteAllByMemberId(member.getId()); - } - @Transactional - public void removeSelected() { - Member member = getMember(); - cartRepository.deleteByMemberIdAndSelected(member.getId()); } } diff --git a/src/main/java/poomasi/domain/product/_category/controller/CategoryAdminController.java b/src/main/java/poomasi/domain/product/_category/controller/CategoryAdminController.java index 18f37077..3ea96504 100644 --- a/src/main/java/poomasi/domain/product/_category/controller/CategoryAdminController.java +++ b/src/main/java/poomasi/domain/product/_category/controller/CategoryAdminController.java @@ -5,12 +5,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.security.access.annotation.Secured; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import poomasi.domain.auth.security.userdetail.UserDetailsImpl; import poomasi.domain.product._category.dto.CategoryRequest; import poomasi.domain.product._category.service.CategoryAdminService; diff --git a/src/main/java/poomasi/domain/product/_category/controller/CategoryController.java b/src/main/java/poomasi/domain/product/_category/controller/CategoryController.java index 6d316648..a27c1e63 100644 --- a/src/main/java/poomasi/domain/product/_category/controller/CategoryController.java +++ b/src/main/java/poomasi/domain/product/_category/controller/CategoryController.java @@ -1,6 +1,5 @@ package poomasi.domain.product._category.controller; -import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -11,6 +10,8 @@ import poomasi.domain.product._category.dto.ProductListInCategoryResponse; import poomasi.domain.product._category.service.CategoryService; +import java.util.List; + @RestController @RequiredArgsConstructor public class CategoryController { diff --git a/src/main/java/poomasi/domain/product/_category/dto/ProductListInCategoryResponse.java b/src/main/java/poomasi/domain/product/_category/dto/ProductListInCategoryResponse.java index 12eff02f..3984611b 100644 --- a/src/main/java/poomasi/domain/product/_category/dto/ProductListInCategoryResponse.java +++ b/src/main/java/poomasi/domain/product/_category/dto/ProductListInCategoryResponse.java @@ -1,16 +1,21 @@ package poomasi.domain.product._category.dto; +import java.math.BigDecimal; +import java.util.List; import lombok.Builder; +import poomasi.domain.image.entity.Image; import poomasi.domain.product.entity.Product; +import java.math.BigDecimal; + @Builder public record ProductListInCategoryResponse( Long categoryId, String name, String description, - String imageUrl, + List images, Integer quantity, - Long price + BigDecimal price ) { public static ProductListInCategoryResponse fromEntity(Product product) { @@ -18,7 +23,7 @@ public static ProductListInCategoryResponse fromEntity(Product product) { .categoryId(product.getCategoryId()) .name(product.getName()) .description(product.getDescription()) - .imageUrl(product.getImageUrl()) + .images(product.getImages()) .quantity(product.getStock()) .price(product.getPrice()) .build(); diff --git a/src/main/java/poomasi/domain/product/_category/entity/Category.java b/src/main/java/poomasi/domain/product/_category/entity/Category.java index 2e5e2b78..18220860 100644 --- a/src/main/java/poomasi/domain/product/_category/entity/Category.java +++ b/src/main/java/poomasi/domain/product/_category/entity/Category.java @@ -1,20 +1,15 @@ package poomasi.domain.product._category.entity; -import jakarta.persistence.CascadeType; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.OneToMany; -import java.util.ArrayList; -import java.util.List; +import jakarta.persistence.*; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import poomasi.domain.product._category.dto.CategoryRequest; import poomasi.domain.product.entity.Product; +import java.util.ArrayList; +import java.util.List; + @Entity @Getter @NoArgsConstructor @@ -50,5 +45,6 @@ public void deleteProduct(Product product) { public void addProduct(Product saveProduct) { this.products.add(saveProduct); + saveProduct.setCategory(this); } } diff --git a/src/main/java/poomasi/domain/product/_category/service/CategoryService.java b/src/main/java/poomasi/domain/product/_category/service/CategoryService.java index 51594960..99637dd5 100644 --- a/src/main/java/poomasi/domain/product/_category/service/CategoryService.java +++ b/src/main/java/poomasi/domain/product/_category/service/CategoryService.java @@ -1,6 +1,5 @@ package poomasi.domain.product._category.service; -import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import poomasi.domain.product._category.dto.CategoryResponse; @@ -10,6 +9,8 @@ import poomasi.global.error.BusinessError; import poomasi.global.error.BusinessException; +import java.util.List; + @Service @RequiredArgsConstructor public class CategoryService { diff --git a/src/main/java/poomasi/domain/product/_intro/controller/ProductIntroController.java b/src/main/java/poomasi/domain/product/_intro/controller/ProductIntroController.java new file mode 100644 index 00000000..c23a26e2 --- /dev/null +++ b/src/main/java/poomasi/domain/product/_intro/controller/ProductIntroController.java @@ -0,0 +1,38 @@ +package poomasi.domain.product._intro.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.annotation.Secured; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.*; +import poomasi.domain.auth.security.userdetail.UserDetailsImpl; +import poomasi.domain.member.entity.Member; +import poomasi.domain.product._intro.dto.ProductIntroResponse; +import poomasi.domain.product._intro.dto.ProductIntroUpdateRequest; +import poomasi.domain.product._intro.service.ProductIntroService; + +@Controller +@RequiredArgsConstructor +@RequestMapping("/api/products/{productId}/intro") +public class ProductIntroController { + + private final ProductIntroService productIntroService; + + @GetMapping("") + public ResponseEntity getIntro(@PathVariable Long productId) { + ProductIntroResponse productIntro = productIntroService.getIntro(productId); + return ResponseEntity.ok().body(productIntro); + } + + @Secured("ROLE_FARMER") + @PutMapping("") + public ResponseEntity updateIntro( + @AuthenticationPrincipal UserDetailsImpl userDetails, + @RequestBody ProductIntroUpdateRequest productIntroUpdateRequest, + @PathVariable Long productId) { + Member member = userDetails.getMember(); + productIntroService.updateIntro(member, productIntroUpdateRequest, productId); + return ResponseEntity.ok().build(); + } +} diff --git a/src/main/java/poomasi/domain/product/_intro/dto/ProductIntroResponse.java b/src/main/java/poomasi/domain/product/_intro/dto/ProductIntroResponse.java new file mode 100644 index 00000000..d1eee4a7 --- /dev/null +++ b/src/main/java/poomasi/domain/product/_intro/dto/ProductIntroResponse.java @@ -0,0 +1,46 @@ +package poomasi.domain.product._intro.dto; + +import lombok.Builder; +import poomasi.domain.product._intro.entity.ProductIntro; + +@Builder +public record ProductIntroResponse( + Long productIntroId, + + String mainTitle, + String mainImageUrl, + + String subTitle1, + String subDesc1, + String subImage1Url, + + String subTitle2, + String subDesc2, + String subImage2Url, + + String subTitle3, + String subDesc3, + String subImage3Url +) { + + public static ProductIntroResponse fromEntity(ProductIntro product) { + return ProductIntroResponse.builder() + .productIntroId(product.getId()) + + .mainImageUrl(product.getMainImage() == null?"":product.getMainImage().getImageUrl()) + .mainTitle(product.getMainTitle()) + + .subTitle1(product.getSubTitle1()) + .subDesc1(product.getSubDesc1()) + .subImage1Url(product.getSubImage1() == null?"":product.getSubImage1().getImageUrl()) + + .subTitle2(product.getSubTitle2()) + .subDesc2(product.getSubDesc2()) + .subImage2Url(product.getSubImage2() == null?"":product.getSubImage2().getImageUrl()) + + .subTitle3(product.getSubTitle3()) + .subDesc3(product.getSubDesc3()) + .subImage3Url(product.getSubImage3() == null?"":product.getSubImage3().getImageUrl()) + .build(); + } +} diff --git a/src/main/java/poomasi/domain/product/_intro/dto/ProductIntroUpdateRequest.java b/src/main/java/poomasi/domain/product/_intro/dto/ProductIntroUpdateRequest.java new file mode 100644 index 00000000..33ba818a --- /dev/null +++ b/src/main/java/poomasi/domain/product/_intro/dto/ProductIntroUpdateRequest.java @@ -0,0 +1,39 @@ +package poomasi.domain.product._intro.dto; + +import poomasi.domain.product._intro.entity.ProductIntro; +import poomasi.domain.product.entity.Product; + +public record ProductIntroUpdateRequest( + String mainTitle, + + String subTitle1, + String subDesc1, + + String subTitle2, + String subDesc2, + + String subTitle3, + String subDesc3 +) { + + public ProductIntro toEntity(Product product) { + return ProductIntro.builder() + .product(product) + + .mainTitle(mainTitle) + //.mainImage(mainImage) + + .subTitle1(subTitle1) + .subDesc1(subDesc1) + //.subImage1(subImage1) + + .subTitle2(subTitle2) + .subDesc2(subDesc2) + //.subImage2(subImage2) + + .subTitle3(subTitle3) + .subDesc3(subDesc3) + //.subImage3(subImage3) + .build(); + } +} diff --git a/src/main/java/poomasi/domain/product/_intro/entity/ProductIntro.java b/src/main/java/poomasi/domain/product/_intro/entity/ProductIntro.java new file mode 100644 index 00000000..971354b8 --- /dev/null +++ b/src/main/java/poomasi/domain/product/_intro/entity/ProductIntro.java @@ -0,0 +1,90 @@ +package poomasi.domain.product._intro.entity; + +import jakarta.persistence.*; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import poomasi.domain.image.entity.Image; +import poomasi.domain.product._intro.dto.ProductIntroUpdateRequest; +import poomasi.domain.product.entity.Product; + +@Entity +@Getter +@NoArgsConstructor +public class ProductIntro { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @OneToOne + @JoinColumn(name = "farmId") + @Setter + private Product product; + + private String mainTitle; + @OneToOne(cascade = CascadeType.ALL) + @Setter + private Image mainImage; + + private String subTitle1; + private String subDesc1; + @OneToOne(cascade = CascadeType.ALL) + @Setter + private Image subImage1; + + private String subTitle2; + private String subDesc2; + @OneToOne(cascade = CascadeType.ALL) + @Setter + private Image subImage2; + + private String subTitle3; + private String subDesc3; + @OneToOne(cascade = CascadeType.ALL) + @Setter + private Image subImage3; + + @Builder + public ProductIntro(Product product, String mainTitle, Image mainImage, String subTitle1, + String subDesc1, Image subImage1, String subTitle2, String subDesc2, Image subImage2, + String subTitle3, String subDesc3, Image subImage3) { + this.product = product; + this.mainTitle = mainTitle; + this.mainImage = mainImage; + this.subTitle1 = subTitle1; + this.subDesc1 = subDesc1; + this.subImage1 = subImage1; + this.subTitle2 = subTitle2; + this.subDesc2 = subDesc2; + this.subImage2 = subImage2; + this.subTitle3 = subTitle3; + this.subDesc3 = subDesc3; + this.subImage3 = subImage3; + } + + public void update(ProductIntroUpdateRequest productIntroUpdateRequest) { + this.mainTitle = productIntroUpdateRequest.mainTitle(); + + this.subTitle1 = productIntroUpdateRequest.subTitle1(); + this.subDesc1 = productIntroUpdateRequest.subDesc1(); + + this.subTitle2 = productIntroUpdateRequest.subTitle2(); + this.subDesc2 = productIntroUpdateRequest.subDesc2(); + + this.subTitle3 = productIntroUpdateRequest.subTitle3(); + this.subDesc3 = productIntroUpdateRequest.subDesc3(); + } + + public void updateImage(Image mainImage, Image subImage1, Image subImage2, Image subImage3){ + this.mainImage = mainImage; + this.subImage1 = subImage1; + this.subImage2 = subImage2; + this.subImage3 = subImage3; + } + + public Long getFarmerId() { + return product.getFarmerId(); + } +} diff --git a/src/main/java/poomasi/domain/product/_intro/repository/ProductIntroRepository.java b/src/main/java/poomasi/domain/product/_intro/repository/ProductIntroRepository.java new file mode 100644 index 00000000..ee14ea74 --- /dev/null +++ b/src/main/java/poomasi/domain/product/_intro/repository/ProductIntroRepository.java @@ -0,0 +1,13 @@ +package poomasi.domain.product._intro.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import poomasi.domain.product._intro.entity.ProductIntro; + +import java.util.Optional; + +@Repository +public interface ProductIntroRepository extends JpaRepository { + + Optional findByProductId(Long productId); +} diff --git a/src/main/java/poomasi/domain/product/_intro/service/ProductIntroService.java b/src/main/java/poomasi/domain/product/_intro/service/ProductIntroService.java new file mode 100644 index 00000000..c3b1cdf0 --- /dev/null +++ b/src/main/java/poomasi/domain/product/_intro/service/ProductIntroService.java @@ -0,0 +1,56 @@ +package poomasi.domain.product._intro.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import poomasi.domain.member.entity.Member; +import poomasi.domain.product._intro.dto.ProductIntroResponse; +import poomasi.domain.product._intro.dto.ProductIntroUpdateRequest; +import poomasi.domain.product._intro.entity.ProductIntro; +import poomasi.domain.product._intro.repository.ProductIntroRepository; +import poomasi.domain.product.entity.Product; +import poomasi.domain.product.service.ProductService; +import poomasi.global.error.BusinessError; +import poomasi.global.error.BusinessException; + +@Service +@RequiredArgsConstructor +public class ProductIntroService { + + private final ProductService productService; + private final ProductIntroRepository productIntroRepository; + + public ProductIntroResponse getIntro(Long productId) { + Product product = getProduct(productId); + return ProductIntroResponse.fromEntity(product.getProductIntro()); + } + + @Transactional + public void updateIntro(Member member, ProductIntroUpdateRequest productIntroUpdateRequest, Long productId) { + Product product = getProduct(productId); + if (!member.getId().equals(product.getFarmerId())) { + throw new BusinessException(BusinessError.MEMBER_ID_MISMATCH); + } + +// Image mainImage = getImage(productIntroUpdateRequest.mainImageId()); +// Image subImage1 = getImage(productIntroUpdateRequest.subImage1Id()); +// Image subImage2 = getImage(productIntroUpdateRequest.subImage2Id()); +// Image subImage3 = getImage(productIntroUpdateRequest.subImage3Id()); + + product.getProductIntro().update(productIntroUpdateRequest); + } + + private Product getProduct(Long productId) { + return productService.findProductById(productId); + } + + public ProductIntro getIntroByIntroId(Long productIntroId) { + return productIntroRepository.findById(productIntroId) + .orElseThrow(() -> new BusinessException(BusinessError.INTRO_NOT_FOUND)); + } + + @Transactional + public void saveExistedProductIntro(ProductIntro productIntro){ + productIntroRepository.save(productIntro); + } +} diff --git a/src/main/java/poomasi/domain/product/controller/ProductController.java b/src/main/java/poomasi/domain/product/controller/ProductController.java index 5d9b4444..64f48004 100644 --- a/src/main/java/poomasi/domain/product/controller/ProductController.java +++ b/src/main/java/poomasi/domain/product/controller/ProductController.java @@ -1,6 +1,5 @@ package poomasi.domain.product.controller; -import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -11,9 +10,11 @@ import poomasi.domain.product.dto.ProductResponse; import poomasi.domain.product.service.ProductService; +import java.util.List; + @RestController @RequiredArgsConstructor -@RequestMapping("/api/product") +@RequestMapping("/api/products") public class ProductController { private final ProductService productService; diff --git a/src/main/java/poomasi/domain/product/controller/ProductFarmerController.java b/src/main/java/poomasi/domain/product/controller/ProductFarmerController.java index 4497f380..7e783835 100644 --- a/src/main/java/poomasi/domain/product/controller/ProductFarmerController.java +++ b/src/main/java/poomasi/domain/product/controller/ProductFarmerController.java @@ -6,39 +6,33 @@ import org.springframework.http.ResponseEntity; import org.springframework.security.access.annotation.Secured; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.PatchMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import poomasi.domain.auth.security.userdetail.UserDetailsImpl; import poomasi.domain.member.entity.Member; import poomasi.domain.product.dto.ProductRegisterRequest; +import poomasi.domain.product.dto.ProductRegisterResponse; import poomasi.domain.product.dto.UpdateProductQuantityRequest; import poomasi.domain.product.service.ProductFarmerService; @RestController @RequiredArgsConstructor -@RequestMapping("/api/product") +@RequestMapping("/api/products") @Slf4j public class ProductFarmerController { private final ProductFarmerService productFarmerService; - @Secured({"ROLE_FARMER","ROLE_ADMIN"}) + @Secured({"ROLE_FARMER", "ROLE_ADMIN"}) @PostMapping("") public ResponseEntity registerProduct (@AuthenticationPrincipal UserDetailsImpl userDetails, @RequestBody ProductRegisterRequest product) { Member member = userDetails.getMember(); - Long productId = productFarmerService.registerProduct(member, product); - return new ResponseEntity<>(productId, HttpStatus.CREATED); + ProductRegisterResponse response = productFarmerService.registerProduct(member, product); + return new ResponseEntity<>(response, HttpStatus.CREATED); } - @Secured({"ROLE_FARMER","ROLE_ADMIN"}) + @Secured({"ROLE_FARMER", "ROLE_ADMIN"}) @PutMapping("/{productId}") public ResponseEntity modifyProduct( @AuthenticationPrincipal UserDetailsImpl userDetails, diff --git a/src/main/java/poomasi/domain/product/controller/ProductTagController.java b/src/main/java/poomasi/domain/product/controller/ProductTagController.java index b49a0b59..536fb5b4 100644 --- a/src/main/java/poomasi/domain/product/controller/ProductTagController.java +++ b/src/main/java/poomasi/domain/product/controller/ProductTagController.java @@ -8,24 +8,26 @@ import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; import poomasi.domain.product.dto.ProductTagRequest; import poomasi.domain.product.service.ProductTagService; @Controller @RequiredArgsConstructor +@RequestMapping("/api/products") public class ProductTagController { private final ProductTagService productTagService; @Secured("ROLE_ADMIN") - @PostMapping("/api/products/tag") + @PostMapping("/tag") public ResponseEntity addTag(@RequestBody ProductTagRequest productTagRequest) { productTagService.addTag(productTagRequest); return new ResponseEntity<>(HttpStatus.CREATED); } @Secured("ROLE_ADMIN") - @DeleteMapping("/api/products/tag") + @DeleteMapping("/tag") public ResponseEntity deleteTag(@RequestBody ProductTagRequest productTagRequest) { productTagService.deleteTag(productTagRequest); return ResponseEntity.ok().build(); diff --git a/src/main/java/poomasi/domain/product/dto/ProductRegisterRequest.java b/src/main/java/poomasi/domain/product/dto/ProductRegisterRequest.java index ffbae25b..658cf7db 100644 --- a/src/main/java/poomasi/domain/product/dto/ProductRegisterRequest.java +++ b/src/main/java/poomasi/domain/product/dto/ProductRegisterRequest.java @@ -1,19 +1,56 @@ package poomasi.domain.product.dto; +import org.hibernate.annotations.Comment; import poomasi.domain.member.entity.Member; -import poomasi.domain.store.entity.Store; +import poomasi.domain.product._intro.entity.ProductIntro; import poomasi.domain.product.entity.Product; +import poomasi.domain.store.entity.Store; + +import java.math.BigDecimal; public record ProductRegisterRequest( + //product Long categoryId, String name, String description, String imageUrl, Integer stock, - Long price + BigDecimal price, + @Comment("재배 환경") + String growEnv, + BigDecimal shippingFee, + + //product intro + String mainTitle, + + String subTitle1, + String subDesc1, + + String subTitle2, + String subDesc2, + + String subTitle3, + String subDesc3, + + String oneLineDescription, + Integer orderLimit + ) { public Product toEntity(Member member, Store store) { + ProductIntro productIntro = ProductIntro.builder() + .mainTitle(mainTitle) + + .subTitle1(subTitle1) + .subDesc1(subDesc1) + + .subTitle2(subTitle2) + .subDesc2(subDesc2) + + .subTitle3(subTitle3) + .subDesc3(subDesc3) + .build(); + return Product.builder() .categoryId(categoryId) .farmerId(member.getId()) @@ -24,6 +61,11 @@ public Product toEntity(Member member, Store store) { .stock(stock) .price(price) .store(store) + .growEnv(growEnv) + .shippingFee(shippingFee) + .productIntro(productIntro) + .oneLineDescription(oneLineDescription) + .orderLimit(orderLimit) .build(); } } diff --git a/src/main/java/poomasi/domain/product/dto/ProductRegisterResponse.java b/src/main/java/poomasi/domain/product/dto/ProductRegisterResponse.java new file mode 100644 index 00000000..e24b0937 --- /dev/null +++ b/src/main/java/poomasi/domain/product/dto/ProductRegisterResponse.java @@ -0,0 +1,8 @@ +package poomasi.domain.product.dto; + +public record ProductRegisterResponse( + Long productId, + Long productIntroId +) { + +} diff --git a/src/main/java/poomasi/domain/product/dto/ProductResponse.java b/src/main/java/poomasi/domain/product/dto/ProductResponse.java index a759c3f4..058c7d94 100644 --- a/src/main/java/poomasi/domain/product/dto/ProductResponse.java +++ b/src/main/java/poomasi/domain/product/dto/ProductResponse.java @@ -1,21 +1,30 @@ package poomasi.domain.product.dto; -import java.util.List; import lombok.Builder; +import poomasi.domain.image.entity.Image; +import poomasi.domain.product._intro.dto.ProductIntroResponse; import poomasi.domain.product.entity.Product; import poomasi.domain.product.entity.ProductTagEnum; +import java.math.BigDecimal; +import java.util.List; + @Builder public record ProductResponse( Long id, String name, - Long price, + BigDecimal price, Integer stock, String description, - String imageUrl, + List images, Long categoryId, String storeName, - List tags + List tags, + ProductIntroResponse productIntro, + String growEnv, + BigDecimal shippingFee, + String oneLineDescription, + Integer orderLimit ) { public static ProductResponse fromEntity(Product product) { @@ -27,10 +36,15 @@ public static ProductResponse fromEntity(Product product) { .price(product.getPrice()) .stock(product.getStock()) .description(product.getDescription()) - .imageUrl(product.getImageUrl()) + .images(product.getImages()) .storeName(product.getStore().getName()) .categoryId(product.getCategoryId()) + .growEnv(product.getGrowEnv()) + .shippingFee(product.getShippingFee()) + .oneLineDescription(product.getOneLineDescription()) + .orderLimit(product.getOrderLimit()) .tags(tags) + .productIntro(ProductIntroResponse.fromEntity(product.getProductIntro())) .build(); } } diff --git a/src/main/java/poomasi/domain/product/entity/Product.java b/src/main/java/poomasi/domain/product/entity/Product.java index fcb8d69e..6a622278 100644 --- a/src/main/java/poomasi/domain/product/entity/Product.java +++ b/src/main/java/poomasi/domain/product/entity/Product.java @@ -1,10 +1,6 @@ package poomasi.domain.product.entity; import jakarta.persistence.*; - -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -12,15 +8,24 @@ import org.hibernate.annotations.Comment; import org.hibernate.annotations.CreationTimestamp; import org.hibernate.annotations.UpdateTimestamp; -import poomasi.domain.order.entity._product.OrderedProduct; -import poomasi.domain.store.entity.Store; +import poomasi.domain.image.entity.Image; +import poomasi.domain.product._category.entity.Category; +import poomasi.domain.product._intro.entity.ProductIntro; +import poomasi.domain.order.entity.OrderedProduct; +import poomasi.domain.product._intro.entity.ProductIntro; import poomasi.domain.product.dto.ProductRegisterRequest; import poomasi.domain.review.entity.Review; +import poomasi.domain.store.entity.Store; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; @Entity @Getter @NoArgsConstructor -//@SQLDelete(sql = "UPDATE product SET deleted_at = current_timestamp WHERE id = ?") +//@SQLDelete(sql = "UPDATE product SET deleted_at = current_timestamp WHERE farmId = ?") public class Product { @Id @@ -28,6 +33,7 @@ public class Product { private Long id; @Comment("카테고리 ID") + @Setter private Long categoryId; @Comment("등록한 사람") @@ -40,14 +46,27 @@ public class Product { private String description; @Setter - @Comment("이미지 URL") - private String imageUrl; + @Comment("이미지") + @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true) + private List images; @Comment("재고") private Integer stock; @Comment("가격") - private Long price; + private BigDecimal price; + + @Comment("재배 환경") + private String growEnv; + + @Comment("배송비") + BigDecimal shippingFee; + + @Comment("한줄 소개") + private String oneLineDescription; + + @Comment("인당 최대 개수 제한") + private Integer orderLimit; @Comment("삭제 일시") private LocalDateTime deletedAt; @@ -60,7 +79,7 @@ public class Product { @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) @JoinColumn(name = "entityId") - List reviewList = new ArrayList<>(); + List reviewList; @ManyToOne @JoinColumn(name = "store_id") // 외래 키 컬럼 지정 @@ -77,15 +96,18 @@ public class Product { @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) @JoinColumn(name = "order_product_details_id") - private List orderProductDetails; + private List orderedProducts; + + @OneToOne(mappedBy = "product", cascade = CascadeType.ALL, orphanRemoval = true) + private ProductIntro productIntro; -// @PreRemove -// public void preRemove() { -// // Product가 삭제되기 전에 연관된 이미지를 삭제 -// for (Image image : images) { -// image.setDeletedAt(LocalDateTime.now()); -// } -// } + @PreRemove + public void preRemove() { + // Product가 삭제되기 전에 연관된 이미지를 삭제 + for (Image image : images) { + image.setDeletedAt(LocalDateTime.now()); + } + } @Builder public Product(Long productId, @@ -95,26 +117,40 @@ public Product(Long productId, String description, String imageUrl, Integer stock, - Long price, - Store store) { + BigDecimal price, + Store store, + String growEnv, + BigDecimal shippingFee, + ProductIntro productIntro, + String oneLineDescription, + Integer orderLimit) { this.id = productId; this.categoryId = categoryId; this.farmerId = farmerId; this.name = name; this.description = description; - this.imageUrl = imageUrl; + this.images = new ArrayList<>(); this.stock = stock; this.price = price; this.store = store; + this.productIntro = productIntro; + this.growEnv = growEnv; + this.shippingFee = shippingFee; + this.reviewList = new ArrayList<>(); + this.oneLineDescription = oneLineDescription; + this.orderLimit = orderLimit; } public Product modify(ProductRegisterRequest productRegisterRequest) { this.categoryId = productRegisterRequest.categoryId(); this.name = productRegisterRequest.name(); this.description = productRegisterRequest.description(); - this.imageUrl = productRegisterRequest.imageUrl(); this.stock = productRegisterRequest.stock(); this.price = productRegisterRequest.price(); + this.growEnv = productRegisterRequest.growEnv(); + this.shippingFee = productRegisterRequest.shippingFee(); + this.oneLineDescription = productRegisterRequest.oneLineDescription(); + this.orderLimit = productRegisterRequest.orderLimit(); return this; } @@ -135,4 +171,7 @@ public void subtractStock(Integer stock) { } + public void setCategory(Category category) { + this.categoryId = category.getId(); + } } diff --git a/src/main/java/poomasi/domain/product/repository/ProductRepository.java b/src/main/java/poomasi/domain/product/repository/ProductRepository.java index 17ce4ed0..e09e7f2d 100644 --- a/src/main/java/poomasi/domain/product/repository/ProductRepository.java +++ b/src/main/java/poomasi/domain/product/repository/ProductRepository.java @@ -1,16 +1,16 @@ package poomasi.domain.product.repository; -import java.util.List; -import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import poomasi.domain.product.entity.Product; +import java.util.List; +import java.util.Optional; + @Repository public interface ProductRepository extends JpaRepository { Optional findByIdAndDeletedAtIsNull(Long id); - List findAllByDeletedAtIsNull(); } diff --git a/src/main/java/poomasi/domain/product/service/ProductFarmerService.java b/src/main/java/poomasi/domain/product/service/ProductFarmerService.java index 52140d85..98c1a6d2 100644 --- a/src/main/java/poomasi/domain/product/service/ProductFarmerService.java +++ b/src/main/java/poomasi/domain/product/service/ProductFarmerService.java @@ -3,15 +3,22 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import poomasi.domain.image.entity.Image; +import poomasi.domain.image.repository.ImageRepository; import poomasi.domain.member.entity.Member; import poomasi.domain.product._category.entity.Category; -import poomasi.domain.product._category.repository.CategoryRepository; +import poomasi.domain.product._category.service.CategoryService; +import poomasi.domain.product._intro.entity.ProductIntro; +import poomasi.domain.product._intro.repository.ProductIntroRepository; +import poomasi.domain.product.dto.ProductRegisterResponse; import poomasi.domain.store.entity.Store; import poomasi.domain.store.repository.StoreRepository; import poomasi.domain.product.dto.ProductRegisterRequest; import poomasi.domain.product.dto.UpdateProductQuantityRequest; import poomasi.domain.product.entity.Product; import poomasi.domain.product.repository.ProductRepository; +import poomasi.domain.store.entity.Store; +import poomasi.domain.store.service.StoreService; import poomasi.global.error.BusinessError; import poomasi.global.error.BusinessException; @@ -20,24 +27,35 @@ public class ProductFarmerService { private final ProductRepository productRepository; - private final CategoryRepository categoryRepository; - private final StoreRepository storeRepository; + private final CategoryService categoryService; + private final StoreService storeService; + private final ImageRepository imageRepository; + private final ProductIntroRepository productIntroRepository; @Transactional - public Long registerProduct(Member member, ProductRegisterRequest request) { + public ProductRegisterResponse registerProduct(Member member, ProductRegisterRequest request) { Category category = getCategory(request.categoryId()); Store store = member.getStore(); + Product saveProduct = productRepository.save(request.toEntity(member,store)); category.addProduct(saveProduct); store.addProduct(saveProduct); - return saveProduct.getId(); + saveProduct.getProductIntro().setProduct(saveProduct); + return new ProductRegisterResponse(saveProduct.getId(), saveProduct.getProductIntro().getId()); + } + + private Image getImage(Long imageId) { + if(imageId == null) + return null; + return imageRepository.findById(imageId) + .orElseThrow(() -> new BusinessException(BusinessError.IMAGE_NOT_FOUND)); } @Transactional public void modifyProduct(Member member, ProductRegisterRequest productRequest, Long productId) { - Product product = getProductByProductId(productId); + Product product = getProduct(productId); checkAuth(member, product); Long categoryId = product.getCategoryId(); @@ -53,7 +71,7 @@ public void modifyProduct(Member member, ProductRegisterRequest productRequest, @Transactional public void deleteProduct(Member member, Long productId) { - Product product = getProductByProductId(productId); + Product product = getProduct(productId); checkAuth(member, product); Long categoryId = product.getCategoryId(); @@ -65,19 +83,18 @@ public void deleteProduct(Member member, Long productId) { @Transactional public void addQuantity(Member member, Long productId, UpdateProductQuantityRequest request) { - Product product = getProductByProductId(productId); + Product product = getProduct(productId); checkAuth(member, product); product.addStock(request.quantity()); } - private Product getProductByProductId(Long productId) { + private Product getProduct(Long productId) { return productRepository.findById(productId) - .orElseThrow(() -> new BusinessException(BusinessError.PRODUCT_NOT_FOUND)); + .orElseThrow(()->new BusinessException(BusinessError.PRODUCT_NOT_FOUND)); } private Category getCategory(Long categoryId) { - return categoryRepository.findById(categoryId) - .orElseThrow(() -> new BusinessException(BusinessError.CATEGORY_NOT_FOUND)); + return categoryService.getCategory(categoryId); } private void checkAuth(Member member, Product product) { diff --git a/src/main/java/poomasi/domain/product/service/ProductService.java b/src/main/java/poomasi/domain/product/service/ProductService.java index f2b75ce1..72a9dfca 100644 --- a/src/main/java/poomasi/domain/product/service/ProductService.java +++ b/src/main/java/poomasi/domain/product/service/ProductService.java @@ -1,19 +1,30 @@ package poomasi.domain.product.service; -import java.util.List; +import jdk.jfr.Description; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Isolation; +import org.springframework.transaction.annotation.Transactional; +import poomasi.domain.order.entity.Order; +import poomasi.domain.order.entity.OrderedProduct; +import poomasi.domain.order.repository.OrderRepository; import poomasi.domain.product.dto.ProductResponse; import poomasi.domain.product.entity.Product; import poomasi.domain.product.repository.ProductRepository; import poomasi.global.error.BusinessError; import poomasi.global.error.BusinessException; +import java.util.List; + +import static poomasi.global.error.BusinessError.PRODUCT_STOCK_QUANTITY_EXCEEDED; + @Service @RequiredArgsConstructor +@Transactional(readOnly = true) public class ProductService { private final ProductRepository productRepository; + private final OrderRepository orderRepository; public List getAllProducts() { return productRepository.findAllByDeletedAtIsNull() @@ -26,7 +37,6 @@ public ProductResponse getProductByProductId(Long productId) { return ProductResponse.fromEntity(findProductById(productId)); } - public Product findValidProductById(Long productId) { Product product = findProductById(productId); if (product.getStock() == 0) { @@ -40,7 +50,23 @@ public Product findProductById(Long productId) { .orElseThrow(() -> new BusinessException(BusinessError.PRODUCT_NOT_FOUND)); } + @Transactional public void saveExistedProduct(Product product) { productRepository.save(product); } + + @Description("재고 차감 메서드. 감소하다 exception이 일어나면 rollback하고 결제 취소 해야 함") + @Transactional(isolation = Isolation.SERIALIZABLE) + public void decreaseStock(Order order) { + List orderedProductList = order.getOrderedProducts(); + for (OrderedProduct orderedProduct : orderedProductList) { + Product product = orderedProduct.getProduct(); + Integer remainQuantity = product.getStock(); //남은 수량 + Integer subtractQuantity = orderedProduct.getCount();//빼야 할 수량 + if (subtractQuantity > remainQuantity) { + throw new BusinessException(PRODUCT_STOCK_QUANTITY_EXCEEDED); + } + product.subtractStock(subtractQuantity); + } + } } diff --git a/src/main/java/poomasi/domain/reservation/controller/ReservationPlatformController.java b/src/main/java/poomasi/domain/reservation/controller/ReservationPlatformController.java index e8acca1e..edad410b 100644 --- a/src/main/java/poomasi/domain/reservation/controller/ReservationPlatformController.java +++ b/src/main/java/poomasi/domain/reservation/controller/ReservationPlatformController.java @@ -1,5 +1,6 @@ package poomasi.domain.reservation.controller; +import jdk.jfr.Description; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.access.annotation.Secured; @@ -17,14 +18,16 @@ public class ReservationPlatformController { private final ReservationPlatformService reservationPlatformService; - @PostMapping("/create") + @PostMapping("/pre-reservation") @Secured("ROLE_CUSTOMER") + @Description("FARM 사전 주문") public ResponseEntity createReservation( @AuthenticationPrincipal UserDetailsImpl userDetails, @RequestBody ReservationRequest request ) { Member member = userDetails.getMember(); ReservationResponse reservation = reservationPlatformService.createReservation(member, request); + return ResponseEntity.ok(reservation); } diff --git a/src/main/java/poomasi/domain/reservation/dto/request/ReservationRequest.java b/src/main/java/poomasi/domain/reservation/dto/request/ReservationRequest.java index 412391a8..93ca7557 100644 --- a/src/main/java/poomasi/domain/reservation/dto/request/ReservationRequest.java +++ b/src/main/java/poomasi/domain/reservation/dto/request/ReservationRequest.java @@ -7,6 +7,8 @@ import poomasi.domain.reservation.entity.Reservation; import poomasi.domain.reservation.entity.ReservationStatus; +import java.math.BigDecimal; + @Builder public record ReservationRequest( Long farmId, @@ -15,7 +17,7 @@ public record ReservationRequest( int memberCount, String request ) { - public Reservation toEntity(Member member, Farm farm, FarmSchedule farmSchedule) { + public Reservation toEntity(Member member, Farm farm, FarmSchedule farmSchedule, String merchantUid) { return Reservation.builder() .member(member) .farm(farm) @@ -23,8 +25,9 @@ public Reservation toEntity(Member member, Farm farm, FarmSchedule farmSchedule) .reservationDate(farmSchedule.getDate()) .memberCount(memberCount) .request(request) - .price(farm.getExperiencePrice() * memberCount) + .price(farm.getExperiencePrice().multiply(BigDecimal.valueOf(memberCount))) .status(ReservationStatus.ACCEPTED) + .merchantUid(merchantUid) .build(); } } diff --git a/src/main/java/poomasi/domain/reservation/dto/response/ReservationResponse.java b/src/main/java/poomasi/domain/reservation/dto/response/ReservationResponse.java index c54c3116..59542484 100644 --- a/src/main/java/poomasi/domain/reservation/dto/response/ReservationResponse.java +++ b/src/main/java/poomasi/domain/reservation/dto/response/ReservationResponse.java @@ -1,13 +1,26 @@ package poomasi.domain.reservation.dto.response; +import java.time.LocalDate; + import lombok.Builder; +import lombok.Getter; import poomasi.domain.reservation.entity.ReservationStatus; import java.time.LocalDate; @Builder -public record ReservationResponse(Long farmId, Long memberId, Long scheduleId, LocalDate reservationDate, - int memberCount, ReservationStatus status, String request, int price - +public record ReservationResponse( + Long id, + Long farmId, + Long memberId, + Long scheduleId, + LocalDate reservationDate, + int memberCount, + ReservationStatus status, + String request, + int price, + String merchantUid, + boolean isReviewed ) { + } diff --git a/src/main/java/poomasi/domain/reservation/entity/Reservation.java b/src/main/java/poomasi/domain/reservation/entity/Reservation.java index 6249157e..604a35dd 100644 --- a/src/main/java/poomasi/domain/reservation/entity/Reservation.java +++ b/src/main/java/poomasi/domain/reservation/entity/Reservation.java @@ -1,21 +1,24 @@ package poomasi.domain.reservation.entity; import jakarta.persistence.*; -import lombok.AccessLevel; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; +import lombok.*; import org.hibernate.annotations.Comment; import org.hibernate.annotations.CreationTimestamp; import org.hibernate.annotations.UpdateTimestamp; +import poomasi.domain.aftersales.entity.FarmAfterSales; import poomasi.domain.farm._schedule.entity.FarmSchedule; import poomasi.domain.farm.entity.Farm; import poomasi.domain.member.entity.Member; import poomasi.domain.reservation.dto.response.ReservationResponse; +import poomasi.domain.review.entity.Review; +import poomasi.payment.entity.Payment; +import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; +import poomasi.domain.review.entity.Review; + @Entity @Getter @Table(name = "reservation", indexes = { @@ -60,9 +63,12 @@ public class Reservation { @Column(nullable = false) private String request; + @Column(nullable = false) + private String merchantUid; + @Comment("결제 예정 금액") @Column(nullable = false) - private int price; + private BigDecimal price; @CreationTimestamp private LocalDateTime createdAt; @@ -73,9 +79,22 @@ public class Reservation { @Comment("예약 취소 일자") private LocalDateTime canceledAt; + @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY) + @Setter + private Review review; + + @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY) + private Payment payment; + + @OneToOne + @Setter + @Getter + private FarmAfterSales farmAfterSales; @Builder - public Reservation(Farm farm, Member member, FarmSchedule scheduleId, LocalDate reservationDate, int memberCount, ReservationStatus status, String request, int price) { + public Reservation(Long id, Farm farm, Member member, FarmSchedule scheduleId, LocalDate reservationDate, + int memberCount, ReservationStatus status, String request, BigDecimal price, String merchantUid) { + this.id = id; this.farm = farm; this.member = member; this.scheduleId = scheduleId; @@ -84,10 +103,14 @@ public Reservation(Farm farm, Member member, FarmSchedule scheduleId, LocalDate this.status = status; this.request = request; this.price = price; + this.review = null; + this.merchantUid = merchantUid; + this.payment = payment; } public ReservationResponse toResponse() { return ReservationResponse.builder() + .id(id) .farmId(farm.getId()) .memberId(member.getId()) .scheduleId(scheduleId.getId()) @@ -95,7 +118,10 @@ public ReservationResponse toResponse() { .memberCount(memberCount) .status(status) .request(request) - .price(price) + .price(price.intValue()) + .isReviewed(review != null) + .price(price.intValue()) + .merchantUid(merchantUid) .build(); } @@ -103,12 +129,18 @@ public boolean isCanceled() { return status == ReservationStatus.CANCELED; } + public boolean isNotCancelled() { + return !isCanceled(); + } + + public void completePayment() { + this.status = ReservationStatus.ACCEPTED; + } + public void cancel() { this.status = ReservationStatus.CANCELED; this.canceledAt = LocalDateTime.now(); } - public boolean isNotCancelled() { - return !isCanceled(); - } + } diff --git a/src/main/java/poomasi/domain/reservation/entity/ReservationStatus.java b/src/main/java/poomasi/domain/reservation/entity/ReservationStatus.java index dc6f6e4e..842b9b94 100644 --- a/src/main/java/poomasi/domain/reservation/entity/ReservationStatus.java +++ b/src/main/java/poomasi/domain/reservation/entity/ReservationStatus.java @@ -1,8 +1,8 @@ package poomasi.domain.reservation.entity; public enum ReservationStatus { - WAITING, // 대기 - ACCEPTED, // 수락 + PENDING, // 대기 + ACCEPTED, // 결제 완료 REJECTED, // 거절 CANCELED, // 취소 DONE // 완료 diff --git a/src/main/java/poomasi/domain/reservation/repository/ReservationRepository.java b/src/main/java/poomasi/domain/reservation/repository/ReservationRepository.java index 3aa013ca..ec2f6e91 100644 --- a/src/main/java/poomasi/domain/reservation/repository/ReservationRepository.java +++ b/src/main/java/poomasi/domain/reservation/repository/ReservationRepository.java @@ -7,6 +7,7 @@ import poomasi.domain.reservation.entity.Reservation; import java.util.List; +import java.util.Optional; @Repository public interface ReservationRepository extends JpaRepository { @@ -17,4 +18,6 @@ public interface ReservationRepository extends JpaRepository List findAllByFarmIdAndScheduleId(Long farm_id, FarmSchedule scheduleId); List findAllByFarmIn(List farms); + + Optional findByMerchantUid(String merchantUid); } diff --git a/src/main/java/poomasi/domain/reservation/service/ReservationPlatformService.java b/src/main/java/poomasi/domain/reservation/service/ReservationPlatformService.java index d43dc3e9..b3117ceb 100644 --- a/src/main/java/poomasi/domain/reservation/service/ReservationPlatformService.java +++ b/src/main/java/poomasi/domain/reservation/service/ReservationPlatformService.java @@ -13,6 +13,10 @@ import poomasi.domain.reservation.entity.Reservation; import poomasi.global.error.BusinessError; import poomasi.global.error.BusinessException; +import poomasi.payment.entity.ItemType; +import poomasi.payment.entity.Payment; +import poomasi.payment.service.PaymentPortoneService; +import poomasi.payment.util.PaymentUtil; @Service @RequiredArgsConstructor @@ -21,6 +25,8 @@ public class ReservationPlatformService { private final ReservationService reservationService; private final FarmService farmService; private final FarmScheduleService farmScheduleService; + private final PaymentUtil paymentUtil; + private final PaymentPortoneService paymentPortoneService; private final int RESERVATION_CANCELLATION_PERIOD = 3; @@ -35,13 +41,23 @@ public ReservationResponse createReservation(Member member, ReservationRequest r throw new BusinessException(BusinessError.RESERVATION_FULL); } - // 2. 농장에서 최대 수용 가능 인원 확인 if (request.memberCount() > farm.getMaxCapacity()) { throw new BusinessException(BusinessError.RESERVATION_MEMBER_EXCEED); } - Reservation reservation = reservationService.createReservation(request.toEntity(member, farm, farmSchedule)); + // 3. 사전 결제 생성 + String merchantUid = paymentUtil.createMerchantUid(ItemType.PRODUCT); + Reservation reservation = reservationService.createReservation(request.toEntity(member, farm, farmSchedule, merchantUid)); + Payment payment = Payment + .builder() + .reservation(reservation) + .totalAmount(reservation.getPrice()) + .checkSum(reservation.getPrice()) + .itemType(ItemType.FARM) + .build(); + + paymentPortoneService.prepaymentRegister(merchantUid, reservation.getPrice()); return reservation.toResponse(); } @@ -50,7 +66,6 @@ public ReservationResponse getReservation(Member member, Long reservationId) { if (!reservation.getMember().getId().equals(member.getId()) && !member.isAdmin() && !reservation.getFarm().getOwnerId().equals(member.getId())) { throw new BusinessException(BusinessError.RESERVATION_NOT_ACCESSIBLE); } - return reservation.toResponse(); } @@ -72,5 +87,7 @@ public void cancelReservation(Member member, Long reservationId) { } reservationService.cancelReservation(reservation); + + } } diff --git a/src/main/java/poomasi/domain/reservation/service/ReservationService.java b/src/main/java/poomasi/domain/reservation/service/ReservationService.java index 160ab8e5..ba38a12b 100644 --- a/src/main/java/poomasi/domain/reservation/service/ReservationService.java +++ b/src/main/java/poomasi/domain/reservation/service/ReservationService.java @@ -1,5 +1,6 @@ package poomasi.domain.reservation.service; +import jdk.jfr.Description; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import poomasi.domain.farm._schedule.entity.FarmSchedule; @@ -10,6 +11,9 @@ import poomasi.global.error.BusinessError; import poomasi.global.error.BusinessException; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.temporal.ChronoUnit; import java.util.List; @Service @@ -57,4 +61,30 @@ public List getValidReservationsByFarmIdAndScheduleId(Long farmId, public List getReservationByFarmIds(List farms) { return reservationRepository.findAllByFarmIn(farms); } + + public Reservation findByMerchantUid(String merchantUid) { + return reservationRepository.findByMerchantUid(merchantUid).orElseThrow(() -> new BusinessException(BusinessError.RESERVATION_NOT_FOUND)); + } + + public Reservation save(Reservation reservation) { + return reservationRepository.save(reservation); + } + + + @Description("3일 이내 취소면 50%. 그 이후는 100%") + public BigDecimal calculateRefundAmount(Reservation reservation) { + LocalDate reservationDate = reservation.getReservationDate(); + LocalDate today = LocalDate.now(); + long daysUntilReservation = ChronoUnit.DAYS.between(today, reservationDate); + BigDecimal price = reservation.getPrice(); + + if (daysUntilReservation <= 3) { + return price.divide(BigDecimal.valueOf(2)); + } else { + return price; + } + + } + + } diff --git a/src/main/java/poomasi/domain/review/controller/farm/FarmReviewController.java b/src/main/java/poomasi/domain/review/controller/farm/FarmReviewController.java index ba200cf9..5bb96040 100644 --- a/src/main/java/poomasi/domain/review/controller/farm/FarmReviewController.java +++ b/src/main/java/poomasi/domain/review/controller/farm/FarmReviewController.java @@ -1,6 +1,5 @@ package poomasi.domain.review.controller.farm; -import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -16,26 +15,28 @@ import poomasi.domain.review.dto.ReviewResponse; import poomasi.domain.review.service.farm.FarmReviewService; +import java.util.List; + @Controller @RequiredArgsConstructor public class FarmReviewController { private final FarmReviewService farmReviewService; - @GetMapping("/api/farm/{farmId}/reviews") + @GetMapping("/api/farms/{farmId}/reviews") public ResponseEntity getProductReviews(@PathVariable Long farmId) { List response = farmReviewService.getFarmReview(farmId); return new ResponseEntity<>(response, HttpStatus.OK); } - @PostMapping("/api/farm/{farmId}/reviews") + @PostMapping("/api/farms/{reservationId}/reviews") public ResponseEntity registerProductReview( @AuthenticationPrincipal UserDetailsImpl userDetails, - @PathVariable Long farmId, + @PathVariable Long reservationId, @RequestBody ReviewRequest reviewRequest) { Member member = userDetails.getMember(); Long reviewId = farmReviewService.registerFarmReview( - member, farmId, reviewRequest); + member, reservationId, reviewRequest); return new ResponseEntity<>(reviewId, HttpStatus.CREATED); } } diff --git a/src/main/java/poomasi/domain/review/controller/product/ProductReviewController.java b/src/main/java/poomasi/domain/review/controller/product/ProductReviewController.java index 0bcea444..027a1734 100644 --- a/src/main/java/poomasi/domain/review/controller/product/ProductReviewController.java +++ b/src/main/java/poomasi/domain/review/controller/product/ProductReviewController.java @@ -1,6 +1,5 @@ package poomasi.domain.review.controller.product; -import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -16,6 +15,8 @@ import poomasi.domain.review.dto.ReviewResponse; import poomasi.domain.review.service.product.ProductReviewService; +import java.util.List; + @Controller @RequiredArgsConstructor public class ProductReviewController { @@ -28,14 +29,14 @@ public ResponseEntity getProductReviews(@PathVariable Long productId) { return new ResponseEntity<>(response, HttpStatus.OK); } - @PostMapping("/api/products/{productId}/reviews") + @PostMapping("/api/products/{orderedProductId}/reviews") public ResponseEntity registerProductReview( @AuthenticationPrincipal UserDetailsImpl userDetails, - @PathVariable Long productId, + @PathVariable Long orderedProductId, @RequestBody ReviewRequest reviewRequest) { Member member = userDetails.getMember(); Long reviewId = productReviewService.registerProductReview( - member, productId, reviewRequest); + member, orderedProductId, reviewRequest); return new ResponseEntity<>(reviewId, HttpStatus.CREATED); } } diff --git a/src/main/java/poomasi/domain/review/dto/ReviewResponse.java b/src/main/java/poomasi/domain/review/dto/ReviewResponse.java index 1b6ac560..6566ae21 100644 --- a/src/main/java/poomasi/domain/review/dto/ReviewResponse.java +++ b/src/main/java/poomasi/domain/review/dto/ReviewResponse.java @@ -1,23 +1,27 @@ package poomasi.domain.review.dto; +import poomasi.domain.image.entity.Image; import poomasi.domain.review.entity.Review; +import java.util.List; + public record ReviewResponse (Long id, Long entityId, String reviewerName, Float rating, - String content - //List imageUrls + String content, + List imageUrls ) { public static ReviewResponse fromEntity(Review review) { return new ReviewResponse( review.getId(), review.getEntityId(), - review.getReviewer().getName(), + review.getReviewer().getName() == null ? "" : review.getReviewer().getName(), review.getRating(), - review.getContent() + review.getContent(), + review.getImages().stream().map(Image::getImageUrl).toList() ); } } diff --git a/src/main/java/poomasi/domain/review/entity/Review.java b/src/main/java/poomasi/domain/review/entity/Review.java index dda0c932..5ea0512b 100644 --- a/src/main/java/poomasi/domain/review/entity/Review.java +++ b/src/main/java/poomasi/domain/review/entity/Review.java @@ -1,23 +1,20 @@ package poomasi.domain.review.entity; -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.ManyToOne; -import java.time.LocalDateTime; +import jakarta.persistence.*; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import org.hibernate.annotations.Comment; import org.hibernate.annotations.CreationTimestamp; import org.hibernate.annotations.UpdateTimestamp; +import poomasi.domain.image.entity.Image; import poomasi.domain.member.entity.Member; import poomasi.domain.review.dto.ReviewRequest; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + @Entity @Getter @NoArgsConstructor @@ -50,6 +47,9 @@ public class Review { @ManyToOne(fetch = FetchType.LAZY) private Member reviewer; + @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY) + private final List images = new ArrayList<>(); + @Builder public Review(Long id, Float rating, String content, Long entityId, EntityType entityType, Member reviewer) { diff --git a/src/main/java/poomasi/domain/review/repository/ReviewRepository.java b/src/main/java/poomasi/domain/review/repository/ReviewRepository.java index 6a506d4f..238344be 100644 --- a/src/main/java/poomasi/domain/review/repository/ReviewRepository.java +++ b/src/main/java/poomasi/domain/review/repository/ReviewRepository.java @@ -1,11 +1,12 @@ package poomasi.domain.review.repository; -import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; import poomasi.domain.review.entity.Review; +import java.util.List; + @Repository public interface ReviewRepository extends JpaRepository { diff --git a/src/main/java/poomasi/domain/review/service/farm/FarmReviewService.java b/src/main/java/poomasi/domain/review/service/farm/FarmReviewService.java index 567dfbed..1c765bd1 100644 --- a/src/main/java/poomasi/domain/review/service/farm/FarmReviewService.java +++ b/src/main/java/poomasi/domain/review/service/farm/FarmReviewService.java @@ -1,12 +1,14 @@ package poomasi.domain.review.service.farm; -import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import poomasi.domain.farm.entity.Farm; -import poomasi.domain.farm.repository.FarmRepository; +import poomasi.domain.farm.service.FarmService; import poomasi.domain.member.entity.Member; +import poomasi.domain.reservation.entity.Reservation; +import poomasi.domain.reservation.entity.ReservationStatus; +import poomasi.domain.reservation.service.ReservationService; import poomasi.domain.review.dto.ReviewRequest; import poomasi.domain.review.dto.ReviewResponse; import poomasi.domain.review.entity.EntityType; @@ -15,32 +17,52 @@ import poomasi.global.error.BusinessError; import poomasi.global.error.BusinessException; +import java.time.LocalDate; +import java.util.List; + @Service @RequiredArgsConstructor public class FarmReviewService { private final ReviewRepository reviewRepository; - private final FarmRepository farmRepository; + private final FarmService farmService; + private final ReservationService reservationService; public List getFarmReview(Long farmId) { - getFarmByFarmId(farmId); //상품이 존재하는지 체크 + Farm farm = getFarmByFarmId(farmId); //상품이 존재하는지 체크 - return reviewRepository.findByFarmId(farmId).stream() + return farm.getReviewList().stream() .map(ReviewResponse::fromEntity).toList(); } @Transactional - public Long registerFarmReview(Member member, Long entityId, ReviewRequest reviewRequest) { + public Long registerFarmReview(Member member, Long reservationId, ReviewRequest reviewRequest) { // s3 이미지 저장하고 주소 받아와서 review에 추가해주기 + Reservation reservation = reservationService.getReservationById(reservationId); + + checkStatusAndDate(reservation); + + Review review = reviewRequest.toEntity(reservation.getFarm().getId(), EntityType.FARM, + member); + review = reviewRepository.save(review); + + reservation.getFarm().addReview(review); + reservation.setReview(review); + + return review.getId(); + } - Review pReview = reviewRequest.toEntity(entityId, EntityType.FARM, member); - pReview = reviewRepository.save(pReview); + private void checkStatusAndDate(Reservation reservation) { + if (reservation.getStatus() != ReservationStatus.DONE) { + throw new BusinessException(BusinessError.RESERVATION_NOT_DONE); + } - return pReview.getId(); + if (reservation.getScheduleId().getDate().isBefore(LocalDate.now())) { + throw new BusinessException(BusinessError.DATE_BEFORE_RESERVATION); + } } private Farm getFarmByFarmId(Long farmId) { - return farmRepository.findById(farmId) - .orElseThrow(() -> new BusinessException(BusinessError.FARM_NOT_FOUND)); + return farmService.getFarmByFarmId(farmId); } } diff --git a/src/main/java/poomasi/domain/review/service/product/ProductReviewService.java b/src/main/java/poomasi/domain/review/service/product/ProductReviewService.java index dd499662..6b03ada7 100644 --- a/src/main/java/poomasi/domain/review/service/product/ProductReviewService.java +++ b/src/main/java/poomasi/domain/review/service/product/ProductReviewService.java @@ -1,12 +1,14 @@ package poomasi.domain.review.service.product; -import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import poomasi.domain.member.entity.Member; +import poomasi.domain.order.entity.OrderedProduct; +import poomasi.domain.order.entity.OrderedProductStatus; +import poomasi.domain.order.repository.OrderedProductRepository; import poomasi.domain.product.entity.Product; -import poomasi.domain.product.repository.ProductRepository; +import poomasi.domain.product.service.ProductService; import poomasi.domain.review.dto.ReviewRequest; import poomasi.domain.review.dto.ReviewResponse; import poomasi.domain.review.entity.EntityType; @@ -15,31 +17,51 @@ import poomasi.global.error.BusinessError; import poomasi.global.error.BusinessException; +import java.util.List; + @Service @RequiredArgsConstructor public class ProductReviewService { private final ReviewRepository reviewRepository; - private final ProductRepository productRepository; + private final ProductService productService; + private final OrderedProductRepository orderedProductRepository; public List getProductReview(Long productId) { - getProductByProductId(productId); //상품이 존재하는지 체크 - - return reviewRepository.findByProductId(productId).stream() - .map(ReviewResponse::fromEntity).toList(); + Product product = getProduct(productId); + return product.getReviewList().stream().map(ReviewResponse::fromEntity).toList(); } @Transactional - public Long registerProductReview(Member member, Long entityId, ReviewRequest reviewRequest) { + public Long registerProductReview(Member member, Long orderedProductId, ReviewRequest reviewRequest) { // s3 이미지 저장하고 주소 받아와서 review에 추가해주기 - Review pReview = reviewRequest.toEntity(entityId, EntityType.PRODUCT, member); + OrderedProduct orderedProduct = findOrderedProduct(orderedProductId); + + if(orderedProduct.getReview() != null) + throw new BusinessException(BusinessError.REVIEW_ALREADY_EXIST); + + checkOrderStatus(orderedProduct); + + Review pReview = reviewRequest.toEntity(orderedProduct.getProduct().getId(), EntityType.PRODUCT, member); pReview = reviewRepository.save(pReview); + orderedProduct.setReview(pReview); + orderedProduct.getProduct().addReview(pReview); return pReview.getId(); } - private Product getProductByProductId(Long productId) { - return productRepository.findById(productId) - .orElseThrow(() -> new BusinessException(BusinessError.PRODUCT_NOT_FOUND)); + private void checkOrderStatus(OrderedProduct orderedProduct) { + if( orderedProduct.getOrderedProductStatus() == OrderedProductStatus.DELIVERED) + throw new BusinessException(BusinessError.ORDER_NOT_DELIVERED); + } + + private OrderedProduct findOrderedProduct(Long orderedProductId){ + return orderedProductRepository.findById(orderedProductId) + .orElseThrow(()->new BusinessException(BusinessError.ORDERED_PRODUCT_NOT_FOUND)); } + private Product getProduct(Long productId){ + return productService.findProductById(productId); + } } + + diff --git a/src/main/java/poomasi/domain/store/controller/StoreController.java b/src/main/java/poomasi/domain/store/controller/StoreController.java index 5101e889..c6e2192f 100644 --- a/src/main/java/poomasi/domain/store/controller/StoreController.java +++ b/src/main/java/poomasi/domain/store/controller/StoreController.java @@ -3,39 +3,42 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.access.annotation.Secured; -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; +import poomasi.domain.auth.security.userdetail.UserDetailsImpl; +import poomasi.domain.member.entity.Member; import poomasi.domain.store.dto.StoreRegisterRequest; import poomasi.domain.store.service.StoreService; -@Controller +@RestController @RequiredArgsConstructor -@RequestMapping("/api/store") +@RequestMapping("/api/stores") public class StoreController { - private final StoreService storeService; @Secured("ROLE_FARMER") @PostMapping("") - public ResponseEntity addStore(@RequestBody StoreRegisterRequest storeRegisterRequest) { - storeService.addStore(storeRegisterRequest); + public ResponseEntity addStore( + @RequestBody StoreRegisterRequest storeRegisterRequest, + @AuthenticationPrincipal UserDetailsImpl userDetails + ) { + Member member = userDetails.getMember(); + storeService.addStore(storeRegisterRequest, member); return ResponseEntity.ok().build(); } - @Secured("ROLE_FARMER") - @GetMapping("") - public ResponseEntity getStore() { - return ResponseEntity.ok(storeService.getStore()); + @GetMapping("/{memberId}") + public ResponseEntity getStore(@PathVariable Long memberId) { + return ResponseEntity.ok(storeService.getStore(memberId)); } @Secured("ROLE_FARMER") @PutMapping("") - public ResponseEntity updateStore(@RequestBody StoreRegisterRequest storeRegisterRequest) { - storeService.updateStore(storeRegisterRequest); + public ResponseEntity updateStore( + @RequestBody StoreRegisterRequest storeRegisterRequest, + @AuthenticationPrincipal UserDetailsImpl userDetails + ) { + storeService.updateStore(storeRegisterRequest, userDetails.getMember()); return ResponseEntity.ok().build(); } } diff --git a/src/main/java/poomasi/domain/store/dto/StoreFeeRequest.java b/src/main/java/poomasi/domain/store/dto/StoreFeeRequest.java deleted file mode 100644 index 57d332cd..00000000 --- a/src/main/java/poomasi/domain/store/dto/StoreFeeRequest.java +++ /dev/null @@ -1,7 +0,0 @@ -package poomasi.domain.store.dto; - -public record StoreFeeRequest( - Integer fee -) { - -} diff --git a/src/main/java/poomasi/domain/store/dto/StoreRegisterRequest.java b/src/main/java/poomasi/domain/store/dto/StoreRegisterRequest.java index 08e975d3..101d444f 100644 --- a/src/main/java/poomasi/domain/store/dto/StoreRegisterRequest.java +++ b/src/main/java/poomasi/domain/store/dto/StoreRegisterRequest.java @@ -1,18 +1,12 @@ package poomasi.domain.store.dto; -import org.hibernate.annotations.Comment; import poomasi.domain.member.entity.Member; import poomasi.domain.store.entity.Store; public record StoreRegisterRequest( String name, String address, - String phone, - String ownerPhone, - @Comment("사업자 번호") - String businessNumber, - @Comment("배송비") - Integer shipingFee + String phone ) { public Store toEntity(Member member) { @@ -20,9 +14,6 @@ public Store toEntity(Member member) { .name(name) .address(address) .phone(phone) - .ownerPhone(ownerPhone) - .businessNumber(businessNumber) - .shipingFee(shipingFee) .owner(member) .build(); } diff --git a/src/main/java/poomasi/domain/store/dto/StoreResponse.java b/src/main/java/poomasi/domain/store/dto/StoreResponse.java index 3a821c63..a18fba1f 100644 --- a/src/main/java/poomasi/domain/store/dto/StoreResponse.java +++ b/src/main/java/poomasi/domain/store/dto/StoreResponse.java @@ -1,12 +1,12 @@ package poomasi.domain.store.dto; -import jakarta.validation.constraints.Positive; -import java.util.List; import lombok.Builder; import org.hibernate.annotations.Comment; import org.jetbrains.annotations.NotNull; -import poomasi.domain.store.entity.Store; import poomasi.domain.product.dto.ProductResponse; +import poomasi.domain.store.entity.Store; + +import java.util.List; @Builder public record StoreResponse( @@ -18,18 +18,10 @@ public record StoreResponse( String phone, - @NotNull - String ownerPhone, - @Comment("사업자 번호") @NotNull String businessNumber, - @Comment("배송비") - @NotNull - @Positive - Integer shipingFee, - @NotNull String ownerName, @@ -37,15 +29,13 @@ public record StoreResponse( ) { public static StoreResponse fromEntity(Store store) { - return StoreResponse.builder() + return StoreResponse.builder() .name(store.getName()) .address(store.getAddress()) .phone(store.getPhone()) - .ownerPhone(store.getOwnerPhone()) .businessNumber(store.getBusinessNumber()) - .shipingFee(store.getShipingFee()) //TODO 나중에 삼항연산자 삭제 - .ownerName( store.getOwner().getMemberProfile() == null ? "" + .ownerName( store.getOwner().getName() == null ? "" : store.getOwner().getName()) .products(store.getProducts().stream().map(ProductResponse::fromEntity).toList()) .build(); diff --git a/src/main/java/poomasi/domain/store/entity/Store.java b/src/main/java/poomasi/domain/store/entity/Store.java index 84d24c95..109530ee 100644 --- a/src/main/java/poomasi/domain/store/entity/Store.java +++ b/src/main/java/poomasi/domain/store/entity/Store.java @@ -1,23 +1,17 @@ package poomasi.domain.store.entity; -import jakarta.persistence.CascadeType; -import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.OneToMany; -import jakarta.persistence.OneToOne; -import java.util.ArrayList; -import java.util.List; +import jakarta.persistence.*; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import org.hibernate.annotations.Comment; import poomasi.domain.member.entity.Member; -import poomasi.domain.store.dto.StoreRegisterRequest; import poomasi.domain.product.entity.Product; +import poomasi.domain.store.dto.StoreRegisterRequest; + +import java.util.ArrayList; +import java.util.List; @Entity @NoArgsConstructor @@ -39,35 +33,27 @@ public class Store { @OneToOne(fetch = FetchType.LAZY) private Member owner; - private String ownerPhone; @Comment("사업자 번호") private String businessNumber; - @Comment("배송비") - private Integer shipingFee; @OneToMany(mappedBy = "store", cascade = CascadeType.ALL) List products = new ArrayList<>(); @Builder public Store(Long id, String name, String address, String phone, Member owner, - String ownerPhone, String businessNumber, Integer shipingFee) { + String businessNumber) { this.id = id; this.name = name; this.address = address; this.phone = phone; this.owner = owner; - this.ownerPhone = ownerPhone; this.businessNumber = businessNumber; - this.shipingFee = shipingFee; } public void updateStore(StoreRegisterRequest storeRegisterRequest) { this.name = storeRegisterRequest.name(); this.address = storeRegisterRequest.address(); this.phone = storeRegisterRequest.phone(); - this.ownerPhone = storeRegisterRequest.ownerPhone(); - this.businessNumber = storeRegisterRequest.businessNumber(); - this.shipingFee = storeRegisterRequest.shipingFee(); } public void addProduct(Product saveProduct) { diff --git a/src/main/java/poomasi/domain/store/repository/StoreRepository.java b/src/main/java/poomasi/domain/store/repository/StoreRepository.java index 397b1b58..708301ea 100644 --- a/src/main/java/poomasi/domain/store/repository/StoreRepository.java +++ b/src/main/java/poomasi/domain/store/repository/StoreRepository.java @@ -1,13 +1,14 @@ package poomasi.domain.store.repository; -import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import poomasi.domain.store.entity.Store; +import java.util.Optional; + @Repository public interface StoreRepository extends JpaRepository { - //@Query("select s from Store s where s.owner.id = :id") + //@Query("select s from Store s where s.owner.farmId = :farmId") Optional findByOwnerId(Long id); } diff --git a/src/main/java/poomasi/domain/store/service/StoreService.java b/src/main/java/poomasi/domain/store/service/StoreService.java index 5920f908..d0063f1e 100644 --- a/src/main/java/poomasi/domain/store/service/StoreService.java +++ b/src/main/java/poomasi/domain/store/service/StoreService.java @@ -1,12 +1,10 @@ package poomasi.domain.store.service; import lombok.RequiredArgsConstructor; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import poomasi.domain.auth.security.userdetail.UserDetailsImpl; import poomasi.domain.member.entity.Member; +import poomasi.domain.member.service.MemberService; import poomasi.domain.store.dto.StoreRegisterRequest; import poomasi.domain.store.dto.StoreResponse; import poomasi.domain.store.entity.Store; @@ -20,19 +18,22 @@ public class StoreService { private final StoreRepository storeRepository; + private final MemberService memberService; @Transactional - public void addStore(StoreRegisterRequest storeRegisterRequest) { - Member member = getMember(); + public void addStore(StoreRegisterRequest storeRegisterRequest, Member member) { + if (member.getStore() != null) { + throw new BusinessException(BusinessError.STORE_ALREADY_EXISTS); + } + Store store = storeRegisterRequest.toEntity(member); + store = storeRepository.save(store); member.setStore(store); - storeRepository.save(store); } - public StoreResponse getStore() { - Member member = getMember(); - Store store = getStore(member); - return StoreResponse.fromEntity(store); + public StoreResponse getStore(Long memberId) { + Member member = memberService.findMemberById(memberId); + return StoreResponse.fromEntity(member.getStore()); } private Store getStore(Member member) { @@ -40,16 +41,8 @@ private Store getStore(Member member) { .orElseThrow(() -> new BusinessException(BusinessError.STORE_NOT_FOUND)); } - private Member getMember() { - Authentication authentication = SecurityContextHolder - .getContext().getAuthentication(); - Object impl = authentication.getPrincipal(); - return ((UserDetailsImpl) impl).getMember(); - } - @Transactional - public void updateStore(StoreRegisterRequest storeRegisterRequest) { - Member member = getMember(); + public void updateStore(StoreRegisterRequest storeRegisterRequest, Member member) { Store store = getStore(member); store.updateStore(storeRegisterRequest); } diff --git a/src/main/java/poomasi/domain/wishlist/controller/WishListPlatformController.java b/src/main/java/poomasi/domain/wishlist/controller/WishListPlatformController.java index f7b228e6..2adcdbae 100644 --- a/src/main/java/poomasi/domain/wishlist/controller/WishListPlatformController.java +++ b/src/main/java/poomasi/domain/wishlist/controller/WishListPlatformController.java @@ -2,32 +2,42 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.annotation.Secured; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; +import poomasi.domain.auth.security.userdetail.UserDetailsImpl; import poomasi.domain.wishlist.dto.WishListDeleteRequest; import poomasi.domain.wishlist.dto.request.WishListAddRequest; import poomasi.domain.wishlist.service.WishListPlatformService; +import poomasi.global.common.ServiceType; -@RequestMapping("/api/v1/wish-list") +@RequestMapping("/api/v1/wishlist") @RestController @RequiredArgsConstructor public class WishListPlatformController { private final WishListPlatformService wishListPlatformService; @PostMapping("/add") - public ResponseEntity addWishList(@RequestBody WishListAddRequest request) { - wishListPlatformService.addWishList(request); + @Secured("ROLE_CUSTOMER") + public ResponseEntity addWishList( + @AuthenticationPrincipal UserDetailsImpl userDetails, + @RequestBody WishListAddRequest request) { + wishListPlatformService.addWishList(userDetails.getMember(), request); return ResponseEntity.ok().build(); } - @PostMapping("/delete") - public ResponseEntity deleteWishList(@RequestBody WishListDeleteRequest request) { - wishListPlatformService.deleteWishList(request); + @DeleteMapping("/delete") + @Secured("ROLE_CUSTOMER") + public ResponseEntity deleteWishList( + @AuthenticationPrincipal UserDetailsImpl userDetails, + @RequestBody WishListDeleteRequest request) { + wishListPlatformService.deleteWishList(userDetails.getMember(), request); return ResponseEntity.ok().build(); } - @GetMapping("/find") - public ResponseEntity findWishListByMemberId(@RequestBody Long memberId) { - // FIXME : memberID는 SecurityContextHolder에서 가져오도록 수정 - return ResponseEntity.ok(wishListPlatformService.findWishListByMemberId(memberId)); + @GetMapping + @Secured("ROLE_CUSTOMER") + public ResponseEntity findWishListByMemberId(@AuthenticationPrincipal UserDetailsImpl userDetails, @RequestParam String type) { + return ResponseEntity.ok(wishListPlatformService.findWishListByMemberIdAndServiceType(userDetails.getMember().getId(), ServiceType.of(type))); } } diff --git a/src/main/java/poomasi/domain/wishlist/dto/WishListDeleteRequest.java b/src/main/java/poomasi/domain/wishlist/dto/WishListDeleteRequest.java index 9651bd52..b35cdb65 100644 --- a/src/main/java/poomasi/domain/wishlist/dto/WishListDeleteRequest.java +++ b/src/main/java/poomasi/domain/wishlist/dto/WishListDeleteRequest.java @@ -1,7 +1,9 @@ package poomasi.domain.wishlist.dto; +import poomasi.global.common.ServiceType; + public record WishListDeleteRequest( - Long memberId, - Long productId + Long objectId, + ServiceType type ) { } diff --git a/src/main/java/poomasi/domain/wishlist/dto/WishListResponse.java b/src/main/java/poomasi/domain/wishlist/dto/WishListResponse.java index 954b50de..d25b5a34 100644 --- a/src/main/java/poomasi/domain/wishlist/dto/WishListResponse.java +++ b/src/main/java/poomasi/domain/wishlist/dto/WishListResponse.java @@ -1,21 +1,28 @@ package poomasi.domain.wishlist.dto; +import java.math.BigDecimal; +import java.util.List; + +import poomasi.domain.image.entity.Image; import poomasi.domain.wishlist.entity.WishList; +import poomasi.global.common.ServiceType; + +import java.math.BigDecimal; public record WishListResponse( - Long productId, - String productName, - Long price, + Long objectId, + ServiceType type, + BigDecimal price, String imageUrl, String description ) { - public static WishListResponse fromEntity(WishList wishList) { + public static WishListResponse fromEntity(WishList wishList, BigDecimal price, String imageUrl, String description) { return new WishListResponse( - wishList.getProduct().getId(), - wishList.getProduct().getName(), - wishList.getProduct().getPrice(), - wishList.getProduct().getImageUrl(), - wishList.getProduct().getDescription() + wishList.getObjectId(), + wishList.getType(), + price, + imageUrl, + description ); } } diff --git a/src/main/java/poomasi/domain/wishlist/dto/request/WishListAddRequest.java b/src/main/java/poomasi/domain/wishlist/dto/request/WishListAddRequest.java index 2783a879..e0780970 100644 --- a/src/main/java/poomasi/domain/wishlist/dto/request/WishListAddRequest.java +++ b/src/main/java/poomasi/domain/wishlist/dto/request/WishListAddRequest.java @@ -1,17 +1,18 @@ package poomasi.domain.wishlist.dto.request; import poomasi.domain.member.entity.Member; -import poomasi.domain.product.entity.Product; import poomasi.domain.wishlist.entity.WishList; +import poomasi.global.common.ServiceType; public record WishListAddRequest( - Long memberId, - Long productId + Long objectId, + ServiceType type ) { - public WishList toEntity(Member member, Product product) { + public WishList toEntity(Member member) { return WishList.builder() .member(member) - .product(product) + .objectId(objectId) + .type(type) .build(); } } diff --git a/src/main/java/poomasi/domain/wishlist/entity/WishList.java b/src/main/java/poomasi/domain/wishlist/entity/WishList.java index 8b0ce31b..8b8c7d81 100644 --- a/src/main/java/poomasi/domain/wishlist/entity/WishList.java +++ b/src/main/java/poomasi/domain/wishlist/entity/WishList.java @@ -7,9 +7,8 @@ import org.hibernate.annotations.Comment; import org.hibernate.annotations.CurrentTimestamp; import org.hibernate.annotations.SQLDelete; -import org.hibernate.annotations.Where; import poomasi.domain.member.entity.Member; -import poomasi.domain.product.entity.Product; +import poomasi.global.common.ServiceType; import java.time.LocalDateTime; @@ -27,10 +26,13 @@ public class WishList { @JoinColumn(name = "member_id") private Member member; - @Comment("상품") - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "product_id") - private Product product; + @Comment("상품 혹은 농장 아이디") + private Long objectId; + + @Comment("상품 혹은 농장 구분") + @Column(nullable = false) + @Enumerated(EnumType.STRING) + private ServiceType type; @Comment("등록일시") @CurrentTimestamp @@ -40,9 +42,10 @@ public class WishList { private LocalDateTime deletedAt; @Builder - public WishList(Member member, Product product) { + public WishList(Member member, Long objectId, ServiceType type) { this.member = member; - this.product = product; + this.objectId = objectId; + this.type = type; } } diff --git a/src/main/java/poomasi/domain/wishlist/repository/WishListRepository.java b/src/main/java/poomasi/domain/wishlist/repository/WishListRepository.java index 1a7584b6..222b74cf 100644 --- a/src/main/java/poomasi/domain/wishlist/repository/WishListRepository.java +++ b/src/main/java/poomasi/domain/wishlist/repository/WishListRepository.java @@ -2,11 +2,12 @@ import org.springframework.data.jpa.repository.JpaRepository; import poomasi.domain.wishlist.entity.WishList; +import poomasi.global.common.ServiceType; import java.util.List; public interface WishListRepository extends JpaRepository { List findByMemberId(Long memberId); - void deleteByMemberIdAndProductId(Long memberId, Long productId); + void deleteByMemberIdAndObjectIdAndType(Long member_id, Long objectId, ServiceType type); } diff --git a/src/main/java/poomasi/domain/wishlist/service/WishListPlatformService.java b/src/main/java/poomasi/domain/wishlist/service/WishListPlatformService.java index fc22b570..cb8181e5 100644 --- a/src/main/java/poomasi/domain/wishlist/service/WishListPlatformService.java +++ b/src/main/java/poomasi/domain/wishlist/service/WishListPlatformService.java @@ -3,10 +3,15 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import poomasi.domain.farm.service.FarmService; +import poomasi.domain.member.entity.Member; +import poomasi.domain.product.service.ProductService; import poomasi.domain.wishlist.dto.WishListDeleteRequest; import poomasi.domain.wishlist.dto.WishListResponse; import poomasi.domain.wishlist.dto.request.WishListAddRequest; -import poomasi.domain.wishlist.entity.WishList; +import poomasi.global.common.ServiceType; +import poomasi.global.error.ApplicationError; +import poomasi.global.error.ApplicationException; import java.util.List; @@ -14,21 +19,32 @@ @Service public class WishListPlatformService { private final WishListService wishListService; + private final ProductService productService; + private final FarmService farmService; @Transactional - public void addWishList(WishListAddRequest request) { - wishListService.addWishList(request); + public void addWishList(Member member, WishListAddRequest request) { + wishListService.addWishList(member, request); } @Transactional - public void deleteWishList(WishListDeleteRequest request) { - wishListService.deleteWishList(request); + public void deleteWishList(Member member, WishListDeleteRequest request) { + wishListService.deleteWishList(member, request); } @Transactional(readOnly = true) - public List findWishListByMemberId(Long memberId) { - return wishListService.findWishListByMemberId(memberId).stream() - .map(WishListResponse::fromEntity) - .toList(); + public List findWishListByMemberIdAndServiceType(Long memberId, ServiceType type) { + if (type.equals(ServiceType.FARM)) { + return wishListService.findWishListByMemberId(memberId).stream() + .map(wishList -> WishListResponse.fromEntity(wishList, productService.findProductById(wishList.getObjectId()).getPrice(), productService.findProductById(wishList.getObjectId()).getImages().getFirst().getImageUrl(), productService.findProductById(wishList.getObjectId()).getDescription())) + .toList(); + + } else if (type.equals(ServiceType.PRODUCT)) { + return wishListService.findWishListByMemberId(memberId).stream() + .map(wishList -> WishListResponse.fromEntity(wishList, farmService.getFarmByFarmId(wishList.getObjectId()).getExperiencePrice(), farmService.getFarmByFarmId(wishList.getObjectId()).getMainImage(), farmService.getFarmByFarmId(wishList.getObjectId()).getDescription())) + .toList(); + } + + throw new ApplicationException(ApplicationError.ENUM_TYPE_ERROR); } } diff --git a/src/main/java/poomasi/domain/wishlist/service/WishListService.java b/src/main/java/poomasi/domain/wishlist/service/WishListService.java index a9cf39fa..6de0470d 100644 --- a/src/main/java/poomasi/domain/wishlist/service/WishListService.java +++ b/src/main/java/poomasi/domain/wishlist/service/WishListService.java @@ -4,36 +4,39 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import poomasi.domain.member.entity.Member; -import poomasi.domain.member.service.MemberService; import poomasi.domain.product.entity.Product; import poomasi.domain.product.service.ProductService; import poomasi.domain.wishlist.dto.WishListDeleteRequest; import poomasi.domain.wishlist.dto.request.WishListAddRequest; import poomasi.domain.wishlist.entity.WishList; import poomasi.domain.wishlist.repository.WishListRepository; +import poomasi.global.common.ServiceType; import java.util.List; @Service @RequiredArgsConstructor +@Transactional(readOnly = true) public class WishListService { private final WishListRepository wishListRepository; - private final MemberService memberService; private final ProductService productService; @Transactional - public void addWishList(WishListAddRequest request) { - Member member = memberService.findMemberById(request.memberId()); - Product product = productService.findProductById(request.productId()); - wishListRepository.save(request.toEntity(member, product)); + public void addWishList(Member member, WishListAddRequest request) { + Product product = productService.findProductById(request.objectId()); + wishListRepository.save(request.toEntity(member)); } @Transactional - public void deleteWishList(WishListDeleteRequest request) { - wishListRepository.deleteByMemberIdAndProductId(request.memberId(), request.productId()); + public void deleteWishList(Member member, WishListDeleteRequest request) { + wishListRepository.deleteByMemberIdAndObjectIdAndType(member.getId(), request.objectId(), request.type()); + } + + + public List findWishListByMemberIdAndServiceType(Long memberId, ServiceType type) { + return wishListRepository.findByMemberId(memberId); } - @Transactional(readOnly = true) public List findWishListByMemberId(Long memberId) { return wishListRepository.findByMemberId(memberId); } diff --git a/src/main/java/poomasi/global/common/ImageFormat.java b/src/main/java/poomasi/global/common/ImageFormat.java new file mode 100644 index 00000000..5b5e67b6 --- /dev/null +++ b/src/main/java/poomasi/global/common/ImageFormat.java @@ -0,0 +1,28 @@ +package poomasi.global.common; + +public enum ImageFormat { + JPEG, + PNG, + GIF, + BMP, + TIFF, + UNKNOWN; + + public static ImageFormat from(String format) { + switch (format) { + case "jpeg": + case "jpg": + return JPEG; + case "png": + return PNG; + case "gif": + return GIF; + case "bmp": + return BMP; + case "tiff": + return TIFF; + default: + return UNKNOWN; + } + } +} diff --git a/src/main/java/poomasi/global/common/ProduceType.java b/src/main/java/poomasi/global/common/ProduceType.java new file mode 100644 index 00000000..bdf0dc5b --- /dev/null +++ b/src/main/java/poomasi/global/common/ProduceType.java @@ -0,0 +1,5 @@ +package poomasi.global.common; + +public enum ProduceType { + FRUIT, VEGETABLE +} diff --git a/src/main/java/poomasi/global/common/ServiceType.java b/src/main/java/poomasi/global/common/ServiceType.java new file mode 100644 index 00000000..012d8730 --- /dev/null +++ b/src/main/java/poomasi/global/common/ServiceType.java @@ -0,0 +1,19 @@ +package poomasi.global.common; + +import poomasi.global.error.ApplicationException; + +import static poomasi.global.error.ApplicationError.ENUM_TYPE_ERROR; + +public enum ServiceType { + PRODUCT, FARM; + + public static ServiceType of(String type) { + if (type.equals("product")) { + return PRODUCT; + } else if (type.equals("farm")) { + return FARM; + } else { + throw new ApplicationException(ENUM_TYPE_ERROR); + } + } +} diff --git a/src/main/java/poomasi/global/config/client/RestClientConfig.java b/src/main/java/poomasi/global/config/client/RestClientConfig.java new file mode 100644 index 00000000..ea702e50 --- /dev/null +++ b/src/main/java/poomasi/global/config/client/RestClientConfig.java @@ -0,0 +1,14 @@ +package poomasi.global.config.client; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestClient; + +@Configuration +public class RestClientConfig { + + @Bean + public RestClient.Builder restClient() { + return RestClient.builder(); + } +} diff --git a/src/main/java/poomasi/global/config/s3/S3PresignedUrlController.java b/src/main/java/poomasi/global/config/s3/S3PresignedUrlController.java index 46b49f7b..64192b73 100644 --- a/src/main/java/poomasi/global/config/s3/S3PresignedUrlController.java +++ b/src/main/java/poomasi/global/config/s3/S3PresignedUrlController.java @@ -1,5 +1,6 @@ package poomasi.global.config.s3; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.access.annotation.Secured; @@ -8,6 +9,8 @@ import poomasi.global.config.s3.dto.request.PresignedUrlPutRequest; import poomasi.global.config.s3.dto.response.PresignedPutUrlResponse; +import java.util.Map; + @RestController @RequiredArgsConstructor @RequestMapping("/api/s3") @@ -22,13 +25,15 @@ public ResponseEntity presignedUrlGet(@RequestParam String keyname) { return ResponseEntity.ok(presignedGetUrl); } - @PostMapping("/presigned-url-put") + @GetMapping("/presigned-url-put") @Secured({"ROLE_CUSTOMER", "ROLE_FARMER", "ROLE_ADMIN"}) - public ResponseEntity presignedUrlPut(@RequestBody PresignedUrlPutRequest request) { + public ResponseEntity presignedUrlPut(@RequestParam String keyPrefix, + @RequestParam Map metadata) { PresignedPutUrlResponse presignedPutUrl = s3PresignedUrlService.createPresignedPutUrl( awsProperties.getS3().getBucket(), awsProperties.getS3().getRegion(), - request.keyPrefix(), request.metadata()); + keyPrefix, + metadata); return ResponseEntity.ok(presignedPutUrl); } -} +} \ No newline at end of file diff --git a/src/main/java/poomasi/global/config/s3/S3PresignedUrlService.java b/src/main/java/poomasi/global/config/s3/S3PresignedUrlService.java index b8541f7d..b06c3b98 100644 --- a/src/main/java/poomasi/global/config/s3/S3PresignedUrlService.java +++ b/src/main/java/poomasi/global/config/s3/S3PresignedUrlService.java @@ -52,9 +52,6 @@ public PresignedPutUrlResponse createPresignedPutUrl(String bucketName, String r String date = now.format(DATE_FORMATTER); String encodedTime = encryptionUtil.encodeTime(now).substring(0, 10); - // jpg 말고 다른 형식 파일 들어오는 경우에 대해서도 따로 처리 필요 - // 사진 갯수 5개로 제한하기 - String uniqueIdentifier = UUID.randomUUID().toString(); String keyName = String.format("%s/%s/%s_%s.jpg", keyPrefix, date, uniqueIdentifier, encodedTime); @@ -79,5 +76,3 @@ public PresignedPutUrlResponse createPresignedPutUrl(String bucketName, String r return new PresignedPutUrlResponse(presignedRequest.url().toExternalForm(), keyName, objectUrl); } } - -// reference: https://docs.aws.amazon.com/ko_kr/AmazonS3/latest/userguide/example_s3_Scenario_PresignedUrl_section.html diff --git a/src/main/java/poomasi/global/config/s3/dto/request/PresignedUrlPutRequest.java b/src/main/java/poomasi/global/config/s3/dto/request/PresignedUrlPutRequest.java index af8926c6..ffc326a2 100644 --- a/src/main/java/poomasi/global/config/s3/dto/request/PresignedUrlPutRequest.java +++ b/src/main/java/poomasi/global/config/s3/dto/request/PresignedUrlPutRequest.java @@ -1,6 +1,12 @@ package poomasi.global.config.s3.dto.request; +import jakarta.validation.constraints.NotBlank; + import java.util.Map; -public record PresignedUrlPutRequest(String keyPrefix, Map metadata) { +public record PresignedUrlPutRequest( + @NotBlank(message = "키 접두어를 입력해주세요") + String keyPrefix, + + Map metadata) { } diff --git a/src/main/java/poomasi/global/error/ApplicationError.java b/src/main/java/poomasi/global/error/ApplicationError.java index f9b7a253..228653aa 100644 --- a/src/main/java/poomasi/global/error/ApplicationError.java +++ b/src/main/java/poomasi/global/error/ApplicationError.java @@ -6,7 +6,20 @@ @Getter @AllArgsConstructor public enum ApplicationError { - ENCRYPT_ERROR("암호화 에러입니다."); + ENCRYPT_ERROR("암호화 에러입니다."), + ENUM_TYPE_ERROR("지원하지 않는 타입입니다."), + + // Payment + PAYMENT_INVALID_REQUEST("결제 요청이 올바르지 않습니다."), + PAYMENT_NOT_FOUND("결제를 찾을 수 없습니다."), + PAYMENT_AMOUNT_MISMATCH("사전 결제 금액과 사후 결제 금액이 일치하지 않습니다."), + PAYMENT_BAD_REQUEST("잘못 된 결제 요청입니다."), + PAYMENT_CHECKSUM_EXCESSIVE_REFUND_AMOUNT("환불 요청 금액이 환불 가능한 금액보다 더 많습니다"), + + // OCR + OCR_SUPPORT_ERROR("지원하지 않는 OCR 서비스입니다."), + OCR_RESULT_FAILURE("OCR 요청이 올바르지 않습니다."); + private final String message; diff --git a/src/main/java/poomasi/global/error/BusinessError.java b/src/main/java/poomasi/global/error/BusinessError.java index 86cba565..ad2dfba1 100644 --- a/src/main/java/poomasi/global/error/BusinessError.java +++ b/src/main/java/poomasi/global/error/BusinessError.java @@ -1,9 +1,8 @@ package poomasi.global.error; -import org.springframework.http.HttpStatus; - import lombok.AllArgsConstructor; import lombok.Getter; +import org.springframework.boot.actuate.autoconfigure.observation.ObservationProperties.Http; import org.springframework.http.HttpStatus; @Getter @@ -12,13 +11,17 @@ public enum BusinessError { // Product PRODUCT_NOT_FOUND(HttpStatus.NOT_FOUND, "상품을 찾을 수 없습니다."), PRODUCT_STOCK_ZERO(HttpStatus.BAD_REQUEST, "재고가 없습니다."), - STOCK_QUANTITY_EXCEEDED(HttpStatus.BAD_REQUEST, "장바구나 수량이 남은 재고를 초과하였습니다"), + PRODUCT_STOCK_QUANTITY_EXCEEDED(HttpStatus.BAD_REQUEST, "장바구나 수량이 남은 재고를 초과하였습니다"), // Category CATEGORY_NOT_FOUND(HttpStatus.NOT_FOUND, "카테고리를 찾을 수 없습니다."), // Review REVIEW_NOT_FOUND(HttpStatus.NOT_FOUND, "리뷰를 찾을 수 없습니다."), + REVIEW_ALREADY_EXIST(HttpStatus.BAD_REQUEST, "이미 작성한 리뷰가 있습니다"), + ORDER_NOT_DELIVERED(HttpStatus.BAD_REQUEST, "아직 배송이 완료되지 않았습니다."), + RESERVATION_NOT_DONE(HttpStatus.BAD_REQUEST, "예약이 확정되지 않았습니다"), + DATE_BEFORE_RESERVATION(HttpStatus.BAD_REQUEST, "체험 다음 날부터 리뷰를 작성할 수 있습니다."), // Member MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "회원이 존재하지 않습니다."), @@ -28,6 +31,8 @@ public enum BusinessError { MEMBER_ALREADY_FARMER(HttpStatus.BAD_REQUEST, "이미 농부인 회원입니다."), MEMBER_ID_MISMATCH(HttpStatus.FORBIDDEN, "권한이 없는 요청입니다."), INVALID_ROLE(HttpStatus.FORBIDDEN, "권한이 없는 요청입니다."), + MEMBER_BIZ_PROFILE_NOT_FOUND(HttpStatus.NOT_FOUND, "사업자 프로필이 존재하지 않습니다."), + MEMBER_NOT_DELETED(HttpStatus.NOT_FOUND, "회원이 탈퇴한 이력이 없습니다."), // MemberProfile MEMBER_PROFILE_NOT_FOUND(HttpStatus.NOT_FOUND, "회원 세부 정보가 존재하지 않습니다."), @@ -42,6 +47,16 @@ public enum BusinessError { FARM_OWNER_MISMATCH(HttpStatus.FORBIDDEN, "해당 농장의 소유자가 아닙니다."), FARM_ALREADY_EXISTS(HttpStatus.CONFLICT, "이미 농장이 존재합니다."), FARM_NOT_OPEN(HttpStatus.BAD_REQUEST, "오픈되지 않은 농장입니다."), + FARM_ALREADY_DELETED(HttpStatus.BAD_REQUEST, "이미 삭제된 농장입니다."), + + // FarmInfo + FARM_INFO_LIMIT_EXCEEDED(HttpStatus.BAD_REQUEST, "농장 소개는 최대 3개까지 등록 가능합니다."), + FARM_INFO_MAIN_ALREADY_EXISTS(HttpStatus.BAD_REQUEST, "이미 메인 소개가 존재합니다."), + FARM_INFO_MAIN_REQUIRED(HttpStatus.BAD_REQUEST, "메인 소개가 필요합니다."), + FARM_INFO_MAIN_REQUIRED_NO_CONTENT(HttpStatus.BAD_REQUEST, "메인 소개가 필요합니다."), + FARM_INFO_NON_MAIN_REQUIRED_CONTENT(HttpStatus.BAD_REQUEST, "메인이 아닌 이미지는 내용이 필요합니다."), + FARM_INFO_NOT_VALID(HttpStatus.BAD_REQUEST, "농장 소개가 유효하지 않습니다."), + FARM_INFO_DETAIL_SIZE_MISMATCH(HttpStatus.BAD_REQUEST, "세부 소개의 크기가 일치하지 않습니다."), // FarmSchedule FARM_SCHEDULE_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 날짜의 스케줄을 찾을 수 없습니다."), @@ -62,7 +77,6 @@ public enum BusinessError { //Cart CART_NOT_FOUND(HttpStatus.NOT_FOUND, "장바구니를 찾을 수 없습니다."), - //ProductTag INVALID_TAG_NAME(HttpStatus.BAD_REQUEST, "존재하지 않는 태그명입니다."), TAG_NOT_FOUND(HttpStatus.NOT_FOUND, "태그가 존재하지 않습니다."), @@ -86,16 +100,13 @@ public enum BusinessError { ORDER_PRODUCT_DETAILS_NOT_FOUND(HttpStatus.NOT_FOUND, "주문을 찾을 수 없습니다."), ORDER_PRODUCT_DETAILS_NOT_OWNED_EXCEPTION(HttpStatus.UNAUTHORIZED, "허가되지 않은 주문입니다."), ORDERED_PRODUCT_NOT_FOUND(HttpStatus.NOT_FOUND, "찾을 수 없는 주문입니다."), - - - // PAYMENT - PAYMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "결제를 찾을 수 없습니다."), - PAYMENT_AMOUNT_MISMATCH(HttpStatus.BAD_REQUEST, "사전 결제 금액과 사후 결제 금액이 일치하지 않습니다."), - PAYMENT_BAD_REQUEST(HttpStatus.BAD_REQUEST, "잘못 된 결제 요청입니다."), - CHECKSUM_EXCESSIVE_REFUND_AMOUNT(HttpStatus.BAD_REQUEST, "환불 요청 금액이 환불 가능한 금액보다 더 많습니다"), + COUNT_LIMIT_EXCEEDED(HttpStatus.BAD_REQUEST, "주문 가능 최대 개수 보다 많이 요청했습니다."), + PORTONE_NOT_WORKING(HttpStatus.BAD_GATEWAY, "포트원 서버가 동작하지 않습니다."), + CART_PRODUCT_MISMATCHING(HttpStatus.BAD_REQUEST, "장바구니에 담겨 있지 않는 상품을 주문하려 합니다"), //Store STORE_NOT_FOUND(HttpStatus.NOT_FOUND, "등록된 상점이 없습니다."), + STORE_ALREADY_EXISTS(HttpStatus.CONFLICT, "이미 상점이 존재합니다."), // After sales SHIPPING_ALREADY_IN_PROGRESS(HttpStatus.BAD_REQUEST, "배송 준비 중이거나 배송 중인 주문입니다."), @@ -105,12 +116,21 @@ public enum BusinessError { REFUND_QUANTITY_EXCEEDED(HttpStatus.BAD_REQUEST, "환불 가능한 수량을 초과한 요청입니다."), PURCHASE_ALREADY_CONFIRMED(HttpStatus.BAD_REQUEST, "이미 구매 확정이 된 상태입니다."), REFUND_NOT_ALLOWED_BEFORE_SHIPPING(HttpStatus.BAD_REQUEST, "배송 대기 전 상태에서는 환불을 요청할 수 없습니다."), - REFUND_AFTER_SALES_NOT_FOUND(HttpStatus.NOT_FOUND , "찾을 수 없는 환불 요청입니다."), + REFUND_AFTER_SALES_NOT_FOUND(HttpStatus.NOT_FOUND, "찾을 수 없는 환불 요청입니다."), REFUND_AFTER_SALES_REQUEST_INVALID_OWNER(HttpStatus.BAD_REQUEST, "판매자의 환불 요청이 아닙니다."), - ; + REFUND_BAD_REQUEST(HttpStatus.BAD_REQUEST, "잘못된 환불 요청입니다."), + + + //Product intro + INTRO_NOT_FOUND(HttpStatus.NOT_FOUND, "제품 소개가 생성되지 않았습니다."), + + //SQS + SQS_ERROR(HttpStatus.INTERNAL_SERVER_ERROR,"SQS생성에 실패했습니다"), + SQS_EVENT_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "SQS EVENT 실행에 실패했습니다.") + ; private final HttpStatus httpStatus; private final String message; -} +} diff --git a/src/main/java/poomasi/global/health/HealthcheckController.java b/src/main/java/poomasi/global/health/HealthcheckController.java index a5a3c8a4..a71e00f8 100644 --- a/src/main/java/poomasi/global/health/HealthcheckController.java +++ b/src/main/java/poomasi/global/health/HealthcheckController.java @@ -1,8 +1,8 @@ package poomasi.global.health; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; -import org.springframework.http.ResponseEntity; @RestController public class HealthcheckController { diff --git a/src/main/java/poomasi/global/ocr/NaverOcrService.java b/src/main/java/poomasi/global/ocr/NaverOcrService.java new file mode 100644 index 00000000..61564de5 --- /dev/null +++ b/src/main/java/poomasi/global/ocr/NaverOcrService.java @@ -0,0 +1,58 @@ +package poomasi.global.ocr; + +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestClient; +import poomasi.global.common.ImageFormat; +import poomasi.global.error.ApplicationException; +import poomasi.global.ocr.dto.request.NaverOcrImage; +import poomasi.global.ocr.dto.request.NaverOcrRequest; +import poomasi.global.ocr.dto.request.OcrRequest; +import poomasi.global.ocr.dto.response.NaverOcrImageResponse; +import poomasi.global.ocr.dto.response.NaverOcrResponse; +import poomasi.global.ocr.dto.response.OcrResponse; + +import java.util.List; + +import static poomasi.global.error.ApplicationError.OCR_SUPPORT_ERROR; + + +@Service +@RequiredArgsConstructor +public class NaverOcrService implements OcrService { + @Value("${naver.ocr.secret}") + private String ocrSecret; + + @Value("${naver.ocr.invoke}") + private String ocrInvoke; + + @Value("${naver.ocr.template}") + private String ocrTemplate; + + private final RestClient.Builder restClient; + + @Override + public OcrResponse extractTextFromImage(OcrRequest request) { + if (!(request instanceof NaverOcrRequest ocrRequest)) { + throw new ApplicationException(OCR_SUPPORT_ERROR); + } + try { + NaverOcrResponse response = restClient.build().post().uri(ocrInvoke).header("Content-Type", MediaType.APPLICATION_JSON_VALUE).header("X-OCR-SECRET", ocrSecret).body(ocrRequest).retrieve().body(NaverOcrResponse.class); + + return response; + } catch (Exception e) { + new NaverOcrImageResponse(); + return new NaverOcrResponse("1.0", "requestId", System.currentTimeMillis(), List.of(NaverOcrImageResponse.builder().inferResult("FAILURE").build())); + } + } + + @Override + public OcrRequest createRequest(String url) { + return NaverOcrRequest + .builder() + .images(List.of(NaverOcrImage.builder().format(ImageFormat.PNG).url(url).templateIds(List.of(Integer.parseInt(ocrTemplate))).build())) + .build(); + } +} diff --git a/src/main/java/poomasi/global/ocr/OcrService.java b/src/main/java/poomasi/global/ocr/OcrService.java new file mode 100644 index 00000000..0cadf6af --- /dev/null +++ b/src/main/java/poomasi/global/ocr/OcrService.java @@ -0,0 +1,10 @@ +package poomasi.global.ocr; + +import poomasi.global.ocr.dto.request.OcrRequest; +import poomasi.global.ocr.dto.response.OcrResponse; + +public interface OcrService { + OcrResponse extractTextFromImage(OcrRequest request); + + OcrRequest createRequest(String url); +} diff --git a/src/main/java/poomasi/global/ocr/dto/request/NaverOcrImage.java b/src/main/java/poomasi/global/ocr/dto/request/NaverOcrImage.java new file mode 100644 index 00000000..0eb0fb8c --- /dev/null +++ b/src/main/java/poomasi/global/ocr/dto/request/NaverOcrImage.java @@ -0,0 +1,27 @@ +package poomasi.global.ocr.dto.request; + +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import poomasi.global.common.ImageFormat; + +import java.util.List; +import java.util.Locale; + +@Data +@NoArgsConstructor +public class NaverOcrImage { + private String format; + private String name; + private String data; + private String url; + private List templateIds; + + @Builder + public NaverOcrImage(ImageFormat format, String url, List templateIds) { + this.format = format.name().toLowerCase(Locale.ROOT);// png로 설정 + this.name = "medium"; + this.url = url; + this.templateIds = templateIds; + } +} diff --git a/src/main/java/poomasi/global/ocr/dto/request/NaverOcrRequest.java b/src/main/java/poomasi/global/ocr/dto/request/NaverOcrRequest.java new file mode 100644 index 00000000..3b7ce8f4 --- /dev/null +++ b/src/main/java/poomasi/global/ocr/dto/request/NaverOcrRequest.java @@ -0,0 +1,26 @@ +package poomasi.global.ocr.dto.request; + +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@NoArgsConstructor +public class NaverOcrRequest extends OcrRequest { + private String version; + private String requestId; + private long timestamp; + private String lang; + private List images; + + @Builder + public NaverOcrRequest(List images) { + this.version = "V2"; + this.requestId = "string"; + this.timestamp = System.currentTimeMillis(); + this.lang = "ko"; + this.images = images; + } +} diff --git a/src/main/java/poomasi/global/ocr/dto/request/OcrRequest.java b/src/main/java/poomasi/global/ocr/dto/request/OcrRequest.java new file mode 100644 index 00000000..f67bfcb6 --- /dev/null +++ b/src/main/java/poomasi/global/ocr/dto/request/OcrRequest.java @@ -0,0 +1,4 @@ +package poomasi.global.ocr.dto.request; + +public abstract class OcrRequest { +} diff --git a/src/main/java/poomasi/global/ocr/dto/response/ConvertedImageInfo.java b/src/main/java/poomasi/global/ocr/dto/response/ConvertedImageInfo.java new file mode 100644 index 00000000..c41f961f --- /dev/null +++ b/src/main/java/poomasi/global/ocr/dto/response/ConvertedImageInfo.java @@ -0,0 +1,11 @@ +package poomasi.global.ocr.dto.response; + +import lombok.Data; + +@Data +public class ConvertedImageInfo { + private int width; + private int height; + private int pageIndex; + private boolean longImage; +} diff --git a/src/main/java/poomasi/global/ocr/dto/response/NaverOcrImageResponse.java b/src/main/java/poomasi/global/ocr/dto/response/NaverOcrImageResponse.java new file mode 100644 index 00000000..6537fc61 --- /dev/null +++ b/src/main/java/poomasi/global/ocr/dto/response/NaverOcrImageResponse.java @@ -0,0 +1,26 @@ +package poomasi.global.ocr.dto.response; + +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +public class NaverOcrImageResponse { + private String uid; + private String name; + private String inferResult; + private String message; + private ValidationResult validationResult; + private ConvertedImageInfo convertedImageInfo; + + @Builder + public NaverOcrImageResponse(String uid, String name, String inferResult, String message, ValidationResult validationResult, ConvertedImageInfo convertedImageInfo) { + this.uid = uid; + this.name = name; + this.inferResult = inferResult; + this.message = message; + this.validationResult = validationResult; + this.convertedImageInfo = convertedImageInfo; + } +} diff --git a/src/main/java/poomasi/global/ocr/dto/response/NaverOcrResponse.java b/src/main/java/poomasi/global/ocr/dto/response/NaverOcrResponse.java new file mode 100644 index 00000000..fa9383aa --- /dev/null +++ b/src/main/java/poomasi/global/ocr/dto/response/NaverOcrResponse.java @@ -0,0 +1,24 @@ +package poomasi.global.ocr.dto.response; + +import lombok.Builder; +import lombok.Data; +import lombok.RequiredArgsConstructor; + +import java.util.List; + +@Data +@RequiredArgsConstructor +public class NaverOcrResponse extends OcrResponse { + private String version; + private String requestId; + private long timestamp; + private List images; + + @Builder + public NaverOcrResponse(String version, String requestId, long timestamp, List images) { + this.version = version; + this.requestId = requestId; + this.timestamp = timestamp; + this.images = images; + } +} diff --git a/src/main/java/poomasi/global/ocr/dto/response/OcrResponse.java b/src/main/java/poomasi/global/ocr/dto/response/OcrResponse.java new file mode 100644 index 00000000..a53985da --- /dev/null +++ b/src/main/java/poomasi/global/ocr/dto/response/OcrResponse.java @@ -0,0 +1,4 @@ +package poomasi.global.ocr.dto.response; + +public abstract class OcrResponse { +} diff --git a/src/main/java/poomasi/global/ocr/dto/response/ValidationResult.java b/src/main/java/poomasi/global/ocr/dto/response/ValidationResult.java new file mode 100644 index 00000000..74a1231a --- /dev/null +++ b/src/main/java/poomasi/global/ocr/dto/response/ValidationResult.java @@ -0,0 +1,9 @@ +package poomasi.global.ocr.dto.response; + + +import lombok.Data; + +@Data +public class ValidationResult { + private String result; +} diff --git a/src/main/java/poomasi/domain/order/_payment/config/IamportConfig.java b/src/main/java/poomasi/payment/config/IamportConfig.java similarity index 91% rename from src/main/java/poomasi/domain/order/_payment/config/IamportConfig.java rename to src/main/java/poomasi/payment/config/IamportConfig.java index 758f0f92..3b2f2079 100644 --- a/src/main/java/poomasi/domain/order/_payment/config/IamportConfig.java +++ b/src/main/java/poomasi/payment/config/IamportConfig.java @@ -1,4 +1,4 @@ -package poomasi.domain.order._payment.config; +package poomasi.payment.config; import com.siot.IamportRestClient.IamportClient; diff --git a/src/main/java/poomasi/payment/controller/PaymentController.java b/src/main/java/poomasi/payment/controller/PaymentController.java new file mode 100644 index 00000000..0d9fda1e --- /dev/null +++ b/src/main/java/poomasi/payment/controller/PaymentController.java @@ -0,0 +1,42 @@ +package poomasi.payment.controller; + +import com.siot.IamportRestClient.exception.IamportResponseException; +import jdk.jfr.Description; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.annotation.Secured; +import org.springframework.web.bind.annotation.*; + +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import poomasi.payment.service.PaymentPortoneService; +import poomasi.payment.dto.request.PaymentWebHookRequest; +import poomasi.payment.service.PaymentPortoneService; + +import java.io.IOException; + +@RestController +@RequestMapping("/api/payments") +@RequiredArgsConstructor +public class PaymentController { + + private final PaymentPortoneService paymentService; + + + @Description("포트원 웹훅 수신 api") + @PostMapping("/portone-webhook") + public void handleIamportWebhook(@RequestBody PaymentWebHookRequest paymentWebHookRequest) + throws IamportResponseException, IOException { + paymentService.handlePortOneProductWebhookEvent(paymentWebHookRequest); + } + + +} + + diff --git a/src/main/java/poomasi/domain/order/_payment/dto/request/PaymentValidateRequest.java b/src/main/java/poomasi/payment/dto/request/PaymentValidateRequest.java similarity index 60% rename from src/main/java/poomasi/domain/order/_payment/dto/request/PaymentValidateRequest.java rename to src/main/java/poomasi/payment/dto/request/PaymentValidateRequest.java index 51639b27..e7454847 100644 --- a/src/main/java/poomasi/domain/order/_payment/dto/request/PaymentValidateRequest.java +++ b/src/main/java/poomasi/payment/dto/request/PaymentValidateRequest.java @@ -1,4 +1,4 @@ -package poomasi.domain.order._payment.dto.request; +package poomasi.payment.dto.request; public record PaymentValidateRequest(String merchantUid, String amount) { } diff --git a/src/main/java/poomasi/domain/order/_payment/dto/request/PaymentWebHookRequest.java b/src/main/java/poomasi/payment/dto/request/PaymentWebHookRequest.java similarity index 84% rename from src/main/java/poomasi/domain/order/_payment/dto/request/PaymentWebHookRequest.java rename to src/main/java/poomasi/payment/dto/request/PaymentWebHookRequest.java index b9f68f75..35a34a91 100644 --- a/src/main/java/poomasi/domain/order/_payment/dto/request/PaymentWebHookRequest.java +++ b/src/main/java/poomasi/payment/dto/request/PaymentWebHookRequest.java @@ -1,4 +1,4 @@ -package poomasi.domain.order._payment.dto.request; +package poomasi.payment.dto.request; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/src/main/java/poomasi/domain/order/_payment/dto/response/PaymentPreRegisterResponse.java b/src/main/java/poomasi/payment/dto/response/PaymentPreRegisterResponse.java similarity index 79% rename from src/main/java/poomasi/domain/order/_payment/dto/response/PaymentPreRegisterResponse.java rename to src/main/java/poomasi/payment/dto/response/PaymentPreRegisterResponse.java index 639ed46c..3a7bef01 100644 --- a/src/main/java/poomasi/domain/order/_payment/dto/response/PaymentPreRegisterResponse.java +++ b/src/main/java/poomasi/payment/dto/response/PaymentPreRegisterResponse.java @@ -1,4 +1,4 @@ -package poomasi.domain.order._payment.dto.response; +package poomasi.payment.dto.response; public record PaymentPreRegisterResponse(String merchantUid) { diff --git a/src/main/java/poomasi/payment/entity/ItemType.java b/src/main/java/poomasi/payment/entity/ItemType.java new file mode 100644 index 00000000..f74f4e18 --- /dev/null +++ b/src/main/java/poomasi/payment/entity/ItemType.java @@ -0,0 +1,6 @@ +package poomasi.payment.entity; + +public enum ItemType { + FARM, + PRODUCT +} diff --git a/src/main/java/poomasi/payment/entity/Payment.java b/src/main/java/poomasi/payment/entity/Payment.java new file mode 100644 index 00000000..ddac6aef --- /dev/null +++ b/src/main/java/poomasi/payment/entity/Payment.java @@ -0,0 +1,90 @@ +package poomasi.payment.entity; + +import jakarta.persistence.*; +import jdk.jfr.Description; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; +import lombok.Setter; +import poomasi.domain.order.entity.Order; +import poomasi.domain.reservation.entity.Reservation; + +import java.math.BigDecimal; +import poomasi.domain.reservation.entity.Reservation; + +@Entity +@Getter + +public class Payment { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "imp_uid") + @Description("아임포트 결제 imp_uid") + private String impUid; + + @Setter + @OneToOne(mappedBy = "payment") + private Order order; + + @Setter + @OneToOne(mappedBy = "payment") + private Reservation reservation; + + @Description("포트원 결제 금액") + private BigDecimal totalAmount; + + @Description("결제 방식") + @Enumerated(EnumType.STRING) + private PaymentMethod paymentMethod = PaymentMethod.TOSS_PAYMENTS; + + @Description("checksum") + private BigDecimal checkSum; + + @Enumerated(EnumType.STRING) + private PaymentStatus paymentStatus = PaymentStatus.PAYMENT_PENDING; + + @Enumerated(EnumType.STRING) + private ItemType itemType; + + public Payment(){ + } + + + @Builder + public Payment(String impUid, Order order, + Reservation reservation, BigDecimal totalAmount, BigDecimal checkSum, ItemType itemType) { + this.impUid = impUid; + this.order = order; + this.reservation = reservation; + this.totalAmount = totalAmount; + this.checkSum = checkSum; + this.itemType = itemType; + } + + + public void setCheckSum(BigDecimal checksum) { + this.checkSum = checksum; + } + + public void subtractCheckSum(BigDecimal checksum) { + this.checkSum = this.checkSum.subtract(checksum); + } + + public void setPaymentStatus(PaymentStatus paymentStatus) { + this.paymentStatus = paymentStatus; + } + + @Description("체크섬보다 크면 true 후 체크섬 빼기, 아니면 false") + public boolean isCheckSumValid(BigDecimal amount){ + if(checkSum.compareTo(amount) >= 0){ + checkSum = checkSum.subtract(amount); + return true; + } + return false; + } + + +} diff --git a/src/main/java/poomasi/domain/order/_payment/entity/PaymentMethod.java b/src/main/java/poomasi/payment/entity/PaymentMethod.java similarity index 60% rename from src/main/java/poomasi/domain/order/_payment/entity/PaymentMethod.java rename to src/main/java/poomasi/payment/entity/PaymentMethod.java index b7611e86..0d4d0d8e 100644 --- a/src/main/java/poomasi/domain/order/_payment/entity/PaymentMethod.java +++ b/src/main/java/poomasi/payment/entity/PaymentMethod.java @@ -1,4 +1,4 @@ -package poomasi.domain.order._payment.entity; +package poomasi.payment.entity; public enum PaymentMethod { KAKAO_PAY, diff --git a/src/main/java/poomasi/payment/entity/PaymentStatus.java b/src/main/java/poomasi/payment/entity/PaymentStatus.java new file mode 100644 index 00000000..55ac9659 --- /dev/null +++ b/src/main/java/poomasi/payment/entity/PaymentStatus.java @@ -0,0 +1,9 @@ +package poomasi.payment.entity; + +public enum PaymentStatus { + PAYMENT_PENDING, // 결제 대기 중 + PAYMENT_COMPLETE, + PAYMENT_DECLINED// 결제 성공 + ; +} + diff --git a/src/main/java/poomasi/domain/order/_payment/repository/PaymentRepository.java b/src/main/java/poomasi/payment/repository/PaymentRepository.java similarity index 71% rename from src/main/java/poomasi/domain/order/_payment/repository/PaymentRepository.java rename to src/main/java/poomasi/payment/repository/PaymentRepository.java index e98964ba..d5bfd7a0 100644 --- a/src/main/java/poomasi/domain/order/_payment/repository/PaymentRepository.java +++ b/src/main/java/poomasi/payment/repository/PaymentRepository.java @@ -1,13 +1,12 @@ -package poomasi.domain.order._payment.repository; +package poomasi.payment.repository; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; -import poomasi.domain.order._payment.entity.Payment; - -import java.util.List; +import poomasi.payment.entity.Payment; @Repository public interface PaymentRepository extends JpaRepository { // public Long countByImpuidContainsIgnoreCase(String impuid); //List findByOrderId(Long orderId); + Payment findByImpUid(String impUid); } diff --git a/src/main/java/poomasi/payment/service/PaymentPortoneService.java b/src/main/java/poomasi/payment/service/PaymentPortoneService.java new file mode 100644 index 00000000..f01bff9f --- /dev/null +++ b/src/main/java/poomasi/payment/service/PaymentPortoneService.java @@ -0,0 +1,179 @@ +package poomasi.payment.service; + +import com.siot.IamportRestClient.IamportClient; +import com.siot.IamportRestClient.exception.IamportResponseException; +import com.siot.IamportRestClient.response.IamportResponse; +import com.siot.IamportRestClient.response.Payment; +import java.io.IOException; +import java.util.Objects; +import jdk.jfr.Description; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Isolation; +import org.springframework.transaction.annotation.Transactional; +import poomasi.domain.order.entity.Order; +import poomasi.domain.order.entity.OrderedProduct; +import poomasi.domain.order.service.OrderService; +import poomasi.domain.product.entity.Product; +import poomasi.domain.product.service.ProductService; +import poomasi.domain.reservation.entity.Reservation; +import poomasi.domain.reservation.service.ReservationService; +import poomasi.global.error.ApplicationException; +import poomasi.global.error.BusinessError; +import poomasi.global.error.BusinessException; +import poomasi.global.error.PaymentConfirmError; +import poomasi.global.error.PaymentConfirmException; +import poomasi.payment.dto.request.PaymentWebHookRequest; +import poomasi.payment.entity.ItemType; +import poomasi.payment.util.PaymentUtil; + +import java.math.BigDecimal; +import java.util.List; + +import static poomasi.global.error.ApplicationError.PAYMENT_AMOUNT_MISMATCH; +import static poomasi.global.error.ApplicationError.PAYMENT_BAD_REQUEST; +import static poomasi.payment.entity.PaymentStatus.*; + +@Service +@RequiredArgsConstructor +@Slf4j +public class PaymentPortoneService implements PaymentService { + private final PaymentUtil paymentUtil; + private final ProductService productService; + private final OrderService orderService; + private final ReservationService reservationService; + private final IamportClient iamportClient; + + @Override + @Description("사전 결제 등록. 프론트엔드에게 서버 merchant uid를 return 해야 함") + public void prepaymentRegister(String merchantUid, BigDecimal amount) { + paymentUtil.sendPrepareData(merchantUid, amount); + //return PaymentPreRegisterResponse.from(paymentPreRegisterRequest.merchantUid()); + } + + @Override + @Transactional(isolation = Isolation.SERIALIZABLE) + @Description("포트원 결제 직전 바로 받는 confirm 요청. 40초 대기: 결제 전에 재고 확인") + public void confirmBeforePayment(String impUid, String merchantUid) { + if (paymentUtil.checkItemType(merchantUid).equals(ItemType.PRODUCT)) { + confirmProductStock(impUid, merchantUid); + } else { + confirmFarmStock(impUid, merchantUid); + } + } + + + @Override + @Description("웹훅 처리 service -> 결제 정상적으로 성공됨을 보장: 결제 금액 확인") + public void handlePortOneProductWebhookEvent(PaymentWebHookRequest paymentWebHookRequest) { + String impUid = paymentWebHookRequest.impUid(); + String merchantUid = paymentWebHookRequest.merchantUid(); + + if (paymentUtil.checkItemType(merchantUid).equals(ItemType.PRODUCT)) { + handleProductPayment(impUid, merchantUid); + } else { + handleFarmPayment(impUid, merchantUid); + } + } + + private void handleProductPayment(String impUid, String merchantUid) { + Order order = orderService.findByMerchantUid(merchantUid); + List orderedProductList = order.getOrderedProducts(); + //수량 검증 + for (OrderedProduct orderedProduct : orderedProductList) { + Product product = orderedProduct.getProduct(); + Integer remainQuantity = product.getStock(); + Integer orderQuantity = orderedProduct.getCount(); + + //주문 재고가 남은 재고보다 많다면 500 + cancelReason 보내야 함 + if (orderQuantity > remainQuantity) { + throw new PaymentConfirmException(PaymentConfirmError.PAYMENT_PROUCT_CONFIRM_EXCEPTION); + } + } + //결제 되어야 할 금액 + BigDecimal amountToBePaid = order.getTotalAmount(); + } + + private void handleFarmPayment(String impUid, String merchantUid) { + Reservation reservation = reservationService.findByMerchantUid(merchantUid); + + BigDecimal amountToBePaid = reservation.getPrice(); + + if (paymentUtil.validatePaymentAmount(impUid, amountToBePaid)) { + try { + reservation.completePayment(); + reservationService.save(reservation); + } catch (BusinessException businessException) { + throw new ApplicationException(PAYMENT_BAD_REQUEST); + } + } else { + //실제 결제 된 금액과 결제 되어야 할 금액이 다르다면 -> 결제 취소 api를 호출해야 한다. + paymentUtil.cancelPaymentByImpUid(impUid); + reservation.cancel(); + reservationService.save(reservation); + throw new ApplicationException(PAYMENT_AMOUNT_MISMATCH); + } + } + + private void confirmProductStock(String impUid, String merchantUid) { + Order order = orderService.findByMerchantUid(merchantUid); + List orderedProductList = order.getOrderedProducts(); + //수량 검증 + for (OrderedProduct orderedProduct : orderedProductList) { + Product product = orderedProduct.getProduct(); + Integer remainQuantity = product.getStock(); + Integer orderQuantity = orderedProduct.getCount(); + + //주문 재고가 남은 재고보다 많다면 500 + cancelReason 보내야 함 + if (orderQuantity > remainQuantity) { + throw new PaymentConfirmException(PaymentConfirmError.PAYMENT_PROUCT_CONFIRM_EXCEPTION); + } + } + } + + private void confirmFarmStock(String impUid, String merchantUid) { + Reservation reservation = reservationService.findByMerchantUid(merchantUid); + + // FIXME: SQS로 웹훅 수신 여부 체크하는 로직으로 변경 필요 2024-11-13 + } + + @Description("결제 내역 단건 조회") + public String getPayment(String impUid) { + IamportResponse response = null; + try { + response = iamportClient.paymentByImpUid(impUid); + } catch (IamportResponseException | IOException e) { + throw new BusinessException(BusinessError.SQS_ERROR); + }; + if(response.getCode() != 200) + throw new BusinessException(BusinessError.SQS_ERROR); + + return response.getResponse().getStatus(); + } + + public void confirmProductPayment(Order productOrder, String status) { + if(status.equals("paid") && + productOrder.getPayment().getPaymentStatus()== PAYMENT_PENDING){ + productOrder.getPayment().setPaymentStatus(PAYMENT_COMPLETE); + }else if(status.equals("cancelled") || status.equals("failed")){ + productOrder.getPayment().setPaymentStatus(PAYMENT_DECLINED); + List products = productOrder.getOrderedProducts(); + + products.forEach(orderedProduct-> + orderedProduct.getProduct() + .addStock(orderedProduct.getCount())); + } + } + + public void confirmFarmPayment(Reservation reservation, String status) { + if(status.equals("paid") && + reservation.getPayment().getPaymentStatus()== PAYMENT_PENDING){ + reservation.getPayment().setPaymentStatus(PAYMENT_COMPLETE); + }else if(status.equals("cancelled") || status.equals("failed")){ + reservation.getPayment().setPaymentStatus(PAYMENT_DECLINED); + } + } +} + + diff --git a/src/main/java/poomasi/payment/service/PaymentService.java b/src/main/java/poomasi/payment/service/PaymentService.java new file mode 100644 index 00000000..d90cb70b --- /dev/null +++ b/src/main/java/poomasi/payment/service/PaymentService.java @@ -0,0 +1,14 @@ +package poomasi.payment.service; + +import poomasi.payment.dto.request.PaymentWebHookRequest; + +import java.math.BigDecimal; + +public interface PaymentService { + + void prepaymentRegister(String merchantUid, BigDecimal amount); + + void confirmBeforePayment(String impUid, String merchantUid); + + void handlePortOneProductWebhookEvent(PaymentWebHookRequest paymentWebHookRequest); +} diff --git a/src/main/java/poomasi/payment/util/PaymentUtil.java b/src/main/java/poomasi/payment/util/PaymentUtil.java new file mode 100644 index 00000000..91d5c502 --- /dev/null +++ b/src/main/java/poomasi/payment/util/PaymentUtil.java @@ -0,0 +1,138 @@ +package poomasi.payment.util; + + +import com.siot.IamportRestClient.IamportClient; +import com.siot.IamportRestClient.exception.IamportResponseException; +import com.siot.IamportRestClient.request.CancelData; +import com.siot.IamportRestClient.request.PrepareData; +import com.siot.IamportRestClient.response.IamportResponse; +import com.siot.IamportRestClient.response.Payment; +import jdk.jfr.Description; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; +import poomasi.global.error.ApplicationException; +import poomasi.payment.entity.ItemType; + +import java.io.IOException; +import java.math.BigDecimal; +import java.util.Date; + +import static org.hibernate.query.sqm.tree.SqmNode.log; +import static poomasi.global.error.ApplicationError.PAYMENT_INVALID_REQUEST; + +@Component +public class PaymentUtil { + + private final IamportClient iamportClient; + + @Autowired + public PaymentUtil(IamportClient iamportClient) { + this.iamportClient = iamportClient; + } + + @Description("merchantUid 생성") + public String createMerchantUid(ItemType type) { + if (type == ItemType.PRODUCT) { + return "p" + new Date().getTime(); + } + return "f" + new Date().getTime(); + } + + @Description("Product인지 Farm인지 확인") + public ItemType checkItemType(String merchantUid) { + if (merchantUid.startsWith("p")) { + return ItemType.PRODUCT; + } + return ItemType.FARM; + } + + @Description("포트원에서 결제 금액 조회하는 메서드") + public BigDecimal getPaymentAmount(String impUid) { + IamportResponse iamportResponse = getSingleTransaction(impUid); + return iamportResponse.getResponse().getAmount(); + } + + @Description("단건 결제 조회 API") + public IamportResponse getSingleTransaction(String impUid) { + try { + IamportResponse iamportResponse = iamportClient.paymentByImpUid(impUid); + return iamportResponse; + } catch (IOException e) { + log.error("iamport response exception : " + e.getMessage(), e); + } catch (IamportResponseException e) { + log.error("iamport exception : " + e.getMessage(), e); + } + throw new ApplicationException(PAYMENT_INVALID_REQUEST); + } + + @Description("결제 취소 api") + public void cancelPaymentByImpUid(String impUid) { + CancelData cancelDate = new CancelData(impUid, false); + try { + iamportClient.cancelPaymentByImpUid(cancelDate); + } catch (IOException e) { + log.error("iamport response exception : " + e.getMessage(), e); + } catch (IamportResponseException e) { + log.error("iamport exception : " + e.getMessage(), e); + } + throw new ApplicationException(PAYMENT_INVALID_REQUEST); + } + + @Transactional + @Description("imp uid로 결제 부분 환불 api 호출") + public void partialRefundByImpUid(String impUid, BigDecimal checkSum, BigDecimal amount) { + CancelData cancelData = new CancelData(impUid, true, amount); + cancelData.setChecksum(checkSum); + try { + iamportClient.cancelPaymentByImpUid(cancelData); + } catch (IOException e) { + log.error("iamport response exception : " + e.getMessage(), e); + } catch (IamportResponseException e) { + log.error("iamport exception : " + e.getMessage(), e); + } + throw new ApplicationException(PAYMENT_INVALID_REQUEST); + } + + + @Transactional + @Description("merchant Uid로 결제 부분 환불 api 호출") + public void refundByMerchantUid(String merchantUid, BigDecimal checkSum, BigDecimal amount) { + + CancelData cancelData = new CancelData(merchantUid, false, amount); + cancelData.setChecksum(checkSum); + try { + iamportClient.cancelPaymentByImpUid(cancelData); + } catch (IOException e) { + log.error("iamport response exception : " + e.getMessage(), e); + } catch (IamportResponseException e) { + log.error("iamport exception : " + e.getMessage(), e); + } + throw new ApplicationException(PAYMENT_INVALID_REQUEST); + } + + @Description("사전 결제 데이터 전송") + public void sendPrepareData(String merchantUid, BigDecimal amount) { + PrepareData prepareData = this.generatePrepareData(merchantUid, amount); + try { + iamportClient.postPrepare(prepareData); + } catch (IOException e) { + log.error("iamport response exception : " + e.getMessage(), e); + } catch (IamportResponseException e) { + log.error("iamport exception : " + e.getMessage(), e); + } + throw new ApplicationException(PAYMENT_INVALID_REQUEST); + } + + @Description("단건 조회 후, 결제 되어야 할 금액과 결제 된 금액이 같은지 확인하는 메서드") + public boolean validatePaymentAmount(String impUid, BigDecimal amountToBePaid) { + IamportResponse iamportResponse = getSingleTransaction(impUid); //내가 보냄 + BigDecimal amount = iamportResponse.getResponse().getAmount(); + return amountToBePaid.equals(amount); + } + + @Description("사전 결제를 위한 Prepare Data를 만드는 메서드") + private PrepareData generatePrepareData(String merchantUid, BigDecimal amount) { + return new PrepareData(merchantUid, amount); + } +} diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 751fc892..5ef15333 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -22,11 +22,30 @@ spring: port: ${REDIS_PORT} host: ${REDIS_HOST} + security: + redirect_url: https://api.poomasi.shop/callback/kakao + oauth2: + client: + registration: + kakao: + client-id: ${KAKAO_CLIENT_ID} + client-secret: ${KAKAO_CLIENT_SECRET} + scope: account_email, profile_nickname + client-name: Kakao + authorization-grant-type: authorization_code + client-authentication-method: client_secret_post + redirect-uri: https://api.poomasi.shop/login/oauth2/code/kakao + provider: + kakao: + authorization-uri: https://kauth.kakao.com/oauth/authorize #카카오 로그인 화면 + token-uri: https://kauth.kakao.com/oauth/token + user-info-uri: https://kapi.kakao.com/v2/user/me + user-name-attribute: kakao_account, profile_nickname token: storage: - type: blacklist - blacklist: - type: redis + type: redis + blacklist: + type: redis logging: level: @@ -50,3 +69,10 @@ imp: api: key: ${IMP_API_KEY} secretKey: ${IMP_SECRET_KEY} + + +naver: + ocr: + secret: ${NAVER_OCR_SECRET} + invoke: ${NAVER_OCR_INVOKE} + template: ${NAVER_OCR_TEMPLATE} diff --git a/src/test/java/poomasi/domain/auth/token/blacklist/service/AccessTokenBlacklistServiceTest.java b/src/test/java/poomasi/domain/auth/token/blacklist/service/AccessTokenBlacklistServiceTest.java new file mode 100644 index 00000000..00bdf5fb --- /dev/null +++ b/src/test/java/poomasi/domain/auth/token/blacklist/service/AccessTokenBlacklistServiceTest.java @@ -0,0 +1,96 @@ +package poomasi.domain.auth.token.blacklist.service; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Optional; + +import static org.mockito.Mockito.*; + +import static org.junit.jupiter.api.Assertions.*; + +@ExtendWith(MockitoExtension.class) +class AccessTokenBlacklistServiceTest { + + @Mock + private TokenBlacklistService tokenBlacklistService; + + @InjectMocks + private AccessTokenBlacklistService accessTokenBlacklistService; + + private final String accessToken = "test-access-token"; + private final Long memberId = 1L; + + @Test + @DisplayName("getMemberIdByAccessToken 성공 테스트") + void getMemberIdByAccessToken_Success() { + // Given + when(tokenBlacklistService.getBlackList(accessToken)).thenReturn(Optional.of(memberId.toString())); + + // When + Optional result = accessTokenBlacklistService.getMemberIdByAccessToken(accessToken); + + // Then + assertTrue(result.isPresent()); + assertEquals(memberId, result.get()); + + verify(tokenBlacklistService).getBlackList(accessToken); + } + + @Test + @DisplayName("getMemberIdByAccessToken 실패 테스트 - 블랙리스트에 토큰이 없는 경우") + void getMemberIdByAccessToken_NotFound() { + // Given + when(tokenBlacklistService.getBlackList(accessToken)).thenReturn(Optional.empty()); + + // When + Optional result = accessTokenBlacklistService.getMemberIdByAccessToken(accessToken); + + // Then + assertFalse(result.isPresent()); + + verify(tokenBlacklistService).getBlackList(accessToken); + } + + @Test + @DisplayName("deleteAccessToken 성공 테스트") + void deleteAccessToken_Success() { + // When + accessTokenBlacklistService.deleteAccessToken(accessToken); + + // Then + verify(tokenBlacklistService).deleteBlackList(accessToken); + } + + @Test + @DisplayName("hasAccessToken 존재하는 토큰 확인 테스트") + void hasAccessToken_Exists() { + // Given + when(tokenBlacklistService.hasKeyBlackList(accessToken)).thenReturn(true); + + // When + boolean result = accessTokenBlacklistService.hasAccessToken(accessToken); + + // Then + assertTrue(result); + verify(tokenBlacklistService).hasKeyBlackList(accessToken); + } + + @Test + @DisplayName("hasAccessToken 존재하지 않는 토큰 확인 테스트") + void hasAccessToken_NotExists() { + // Given + when(tokenBlacklistService.hasKeyBlackList(accessToken)).thenReturn(false); + + // When + boolean result = accessTokenBlacklistService.hasAccessToken(accessToken); + + // Then + assertFalse(result); + verify(tokenBlacklistService).hasKeyBlackList(accessToken); + } +} diff --git a/src/test/java/poomasi/domain/auth/token/whitelist/service/RefreshTokenWhitelistServiceTest.java b/src/test/java/poomasi/domain/auth/token/whitelist/service/RefreshTokenWhitelistServiceTest.java new file mode 100644 index 00000000..82f61380 --- /dev/null +++ b/src/test/java/poomasi/domain/auth/token/whitelist/service/RefreshTokenWhitelistServiceTest.java @@ -0,0 +1,68 @@ +package poomasi.domain.auth.token.whitelist.service; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Optional; + +import static org.mockito.Mockito.*; + +import static org.junit.jupiter.api.Assertions.*; + +@ExtendWith(MockitoExtension.class) +class RefreshTokenWhitelistServiceTest { + + @Mock + private TokenWhitelistService tokenWhitelistService; + + @InjectMocks + private RefreshTokenWhitelistService refreshTokenWhitelistService; + + private final String refreshToken = "test-refresh-token"; + private final Long memberId = 1L; + + @Test + @DisplayName("getMemberIdByRefreshToken 성공 테스트") + void getMemberIdByRefreshToken_Success() { + // Given + when(tokenWhitelistService.getValues(refreshToken, memberId.toString())).thenReturn(Optional.of(memberId.toString())); + + // When + Optional result = refreshTokenWhitelistService.getMemberIdByRefreshToken(refreshToken, memberId); + + // Then + assertTrue(result.isPresent()); + assertEquals(memberId, result.get()); + + verify(tokenWhitelistService).getValues(refreshToken, memberId.toString()); + } + + @Test + @DisplayName("getMemberIdByRefreshToken 실패 테스트 - 화이트리스트에 토큰이 없는 경우") + void getMemberIdByRefreshToken_NotFound() { + // Given + when(tokenWhitelistService.getValues(refreshToken, memberId.toString())).thenReturn(Optional.empty()); + + // When + Optional result = refreshTokenWhitelistService.getMemberIdByRefreshToken(refreshToken, memberId); + + // Then + assertFalse(result.isPresent()); + + verify(tokenWhitelistService).getValues(refreshToken, memberId.toString()); + } + + @Test + @DisplayName("removeMemberRefreshToken 성공 테스트") + void removeMemberRefreshToken_Success() { + // When + refreshTokenWhitelistService.removeMemberRefreshToken(memberId); + + // Then + verify(tokenWhitelistService).removeRefreshTokenById(memberId); + } +} diff --git a/src/test/java/poomasi/domain/farm/service/FarmFarmerServiceTest.java b/src/test/java/poomasi/domain/farm/service/FarmFarmerServiceTest.java index b6d3ac62..b35d41fb 100644 --- a/src/test/java/poomasi/domain/farm/service/FarmFarmerServiceTest.java +++ b/src/test/java/poomasi/domain/farm/service/FarmFarmerServiceTest.java @@ -7,18 +7,20 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import poomasi.domain.farm.dto.FarmRegisterRequest; -import poomasi.domain.farm.dto.FarmUpdateRequest; +import poomasi.domain.farm.FarmTestHelper; +import poomasi.domain.farm.dto.request.FarmRegisterRequest; import poomasi.domain.farm.entity.Farm; import poomasi.domain.farm.repository.FarmRepository; import poomasi.domain.member.entity.Member; import poomasi.global.error.BusinessError; import poomasi.global.error.BusinessException; +import java.time.LocalDateTime; import java.util.Optional; import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @ExtendWith(MockitoExtension.class) @@ -26,10 +28,14 @@ class FarmFarmerServiceTest { @InjectMocks private FarmFarmerService farmFarmerService; - + @Mock + private FarmInfoService farmInfoService; + @Mock + private FarmService farmService; @Mock private FarmRepository farmRepository; + @Nested @DisplayName("농장 등록") class RegisterFarm { @@ -48,7 +54,21 @@ void should_throwException_when_farmAlreadyExists() { given(farmRepository.getFarmByOwnerIdAndDeletedAtIsNull(member.getId())).willReturn(Optional.of(existingFarm)); - FarmRegisterRequest request = new FarmRegisterRequest("New Farm", "Address", "Detail", 1.0, 1.0, "010-1234-5678", "Description", 10000, 10, 5); + FarmRegisterRequest request = FarmRegisterRequest + .builder() + .name("New Farm") + .address("Address") + .addressDetail("Detail") + .phoneNumber("010-1234-5678") + .latitude(1.0) + .longitude(1.0) + .phoneNumber("010-123-123") + .experiencePrice(10000) + .maxPeople(10) + .maxTeam(10) + .categoryId(1L) + .imageUrl("10") + .price(10).build(); // when & then assertThatThrownBy(() -> farmFarmerService.registerFarm(member, request)) @@ -57,44 +77,6 @@ void should_throwException_when_farmAlreadyExists() { } } - @Nested - @DisplayName("농장 정보 업데이트") - class UpdateFarm { - @Test - @DisplayName("농장이 존재하지 않는 경우 예외를 발생시킨다") - void should_throwException_when_farmNotExist() { - // given - Long farmId = 1L; - FarmUpdateRequest request = new FarmUpdateRequest(farmId, "Updated Farm", "Description", "Address", "Detail", 1.0, 1.0); - given(farmRepository.findByIdAndDeletedAtIsNull(farmId)).willReturn(Optional.empty()); - - // when & then - assertThatThrownBy(() -> farmFarmerService.updateFarm(1L, request)) - .isInstanceOf(BusinessException.class) - .hasFieldOrPropertyWithValue("businessError", BusinessError.FARM_NOT_FOUND); - } - - @Test - @DisplayName("농장 소유자가 아닌 경우 예외를 발생시킨다") - void should_throwException_when_ownerMismatch() { - // given - Long farmId = 1L; - Long farmerId = 2L; - Farm farm = Farm.builder() - .id(farmId) - .name("Farm") - .ownerId(3L) - .build(); - given(farmRepository.findByIdAndDeletedAtIsNull(farmId)).willReturn(Optional.of(farm)); - - FarmUpdateRequest request = new FarmUpdateRequest(farmId, "Updated Farm", "Description", "Address", "Detail", 1.0, 1.0); - - // when & then - assertThatThrownBy(() -> farmFarmerService.updateFarm(farmerId, request)) - .isInstanceOf(BusinessException.class) - .hasFieldOrPropertyWithValue("businessError", BusinessError.FARM_OWNER_MISMATCH); - } - } @Nested @DisplayName("농장 삭제") @@ -110,7 +92,7 @@ void should_throwException_when_ownerMismatchOnDelete() { .name("Farm") .ownerId(3L) .build(); - given(farmRepository.findByIdAndDeletedAtIsNull(farmId)).willReturn(Optional.of(farm)); + given(farmService.getFarmByFarmId(farmId)).willReturn(farm); // when & then assertThatThrownBy(() -> farmFarmerService.deleteFarm(farmerId, farmId)) @@ -129,22 +111,26 @@ void should_deleteFarm_when_ownerMatches() { .name("Farm") .ownerId(farmerId) .build(); - given(farmRepository.findByIdAndDeletedAtIsNull(farmId)).willReturn(Optional.of(farm)); + + given(farmService.getFarmByFarmId(farmId)).willReturn(farm); // when farmFarmerService.deleteFarm(farmerId, farmId); // then - verify(farmRepository).delete(farm); + verify(farmService).delete(farm); + verify(farmInfoService).deleteFarmInfo(farmId); } @Test @DisplayName("농장이 존재하지 않는 경우 예외를 발생시킨다") void should_throwException_when_farmNotExistOnDelete() { // given - Long farmId = 1L; + Long farmId = 3L; Long farmerId = 1L; - given(farmRepository.findByIdAndDeletedAtIsNull(farmId)).willReturn(Optional.empty()); + + // farmService에서 농장이 없을 때 null을 반환하도록 설정 + given(farmService.getFarmByFarmId(farmId)).willReturn(null); // when & then assertThatThrownBy(() -> farmFarmerService.deleteFarm(farmerId, farmId)) @@ -162,16 +148,19 @@ void should_throwException_when_farmAlreadyDeleted() { .id(farmId) .name("Farm") .ownerId(farmerId) - .deletedAt(null) + .deletedAt(LocalDateTime.now()) // 이미 삭제된 상태 .build(); - given(farmRepository.findByIdAndDeletedAtIsNull(farmId)).willReturn(Optional.of(farm)); + given(farmService.getFarmByFarmId(farmId)).willReturn(farm); - // when - farmFarmerService.deleteFarm(farmerId, farmId); + // when & then + assertThatThrownBy(() -> farmFarmerService.deleteFarm(farmerId, farmId)) + .isInstanceOf(BusinessException.class) + .hasFieldOrPropertyWithValue("businessError", BusinessError.FARM_ALREADY_DELETED); - // then - verify(farmRepository).delete(farm); + // delete 메서드가 호출되지 않았는지 확인 + verify(farmService, never()).delete(farm); + verify(farmInfoService, never()).deleteFarmInfo(farmId); } } } diff --git a/src/test/java/poomasi/domain/farm/service/FarmPlatformServiceTest.java b/src/test/java/poomasi/domain/farm/service/FarmPlatformServiceTest.java index 60759722..35ccc0ac 100644 --- a/src/test/java/poomasi/domain/farm/service/FarmPlatformServiceTest.java +++ b/src/test/java/poomasi/domain/farm/service/FarmPlatformServiceTest.java @@ -9,7 +9,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; -import poomasi.domain.farm.dto.FarmResponse; +import poomasi.domain.farm.dto.response.FarmResponse; import poomasi.domain.farm.entity.Farm; import poomasi.global.error.BusinessError; import poomasi.global.error.BusinessException; @@ -42,16 +42,26 @@ void should_returnFarmResponse_when_farmExists() { .id(farmId) .name("Test Farm") .ownerId(1L) + .experiencePrice(10000) + .address("Address") + .phoneNumber("010-1234-5678") + .latitude(1.0) + .longitude(1.0) + .mainImage("Main Image") + .description("Description") + .growEnv("Grow Env") + .maxCapacity(10) + .maxReservation(5) + .categoryId(1L) .build(); + given(farmService.getFarmByFarmId(farmId)).willReturn(farm); // when FarmResponse response = farmPlatformService.getFarmByFarmId(farmId); // then - assertThat(response.id()).isEqualTo(farmId); assertThat(response.name()).isEqualTo("Test Farm"); - verify(farmService).getFarmByFarmId(farmId); // farmService 호출 확인 } @Test diff --git a/src/test/java/poomasi/domain/image/service/ImageServiceTest.java b/src/test/java/poomasi/domain/image/service/ImageServiceTest.java new file mode 100644 index 00000000..d9cd1a99 --- /dev/null +++ b/src/test/java/poomasi/domain/image/service/ImageServiceTest.java @@ -0,0 +1,217 @@ +package poomasi.domain.image.service; + +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.*; +import poomasi.domain.image.dto.request.ImageRequest; +import poomasi.domain.image.dto.response.ImageResponse; +import poomasi.domain.image.entity.Image; +import poomasi.domain.image.entity.ImageType; +import poomasi.domain.image.repository.ImageRepository; +import poomasi.domain.image.validator.ImageOwnerValidator; +import poomasi.domain.image.validator.ImageOwnerValidatorFactory; +import poomasi.domain.image.linker.ImageLinkerFactory; +import poomasi.domain.image.deleteLinker.ImageDeleteFactory; +import poomasi.domain.member.entity.Member; +import poomasi.domain.member.service.MemberService; +import poomasi.global.error.BusinessError; +import poomasi.global.error.BusinessException; + +import java.util.Optional; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import static org.mockito.Mockito.*; +import static org.junit.jupiter.api.Assertions.*; + +class ImageServiceTest { + + @Mock + private ImageRepository imageRepository; + + @Mock + private MemberService memberService; + + @Mock + private ImageOwnerValidatorFactory validatorFactory; + + @Mock + private ImageLinkerFactory imageLinkerFactory; + + @Mock + private ImageDeleteFactory imageDeleteFactory; + + @InjectMocks + private ImageService imageService; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + @Test + @DisplayName("이미지 저장 성공 테스트") + void saveImage_success() { + // Given + Long memberId = 1L; + ImageRequest imageRequest = new ImageRequest("key", "imageUrl", ImageType.MEMBER_PROFILE, 1L); + Image image = new Image("key", "imageUrl", ImageType.MEMBER_PROFILE, 1L); + + when(imageRepository.findByObjectKeyAndTypeAndReferenceId(any(), any(), any())) + .thenReturn(Optional.empty()); + when(imageRepository.save(any(Image.class))) + .thenReturn(image); + + Member member = mock(Member.class); + when(memberService.findMemberById(memberId)).thenReturn(member); + when(member.isAdmin()).thenReturn(true); + + // When + ImageResponse imageResponse = imageService.saveImage(memberId, imageRequest); + + // Then + assertNotNull(imageResponse); + assertEquals(image.getObjectKey(), imageResponse.objectKey()); + verify(imageRepository, times(1)).save(any(Image.class)); + } + + @Test + @DisplayName("이미지 저장 실패 테스트 - 개수 초과") + void saveImage_IMAGE_LIMIT_EXCEED() { + // Given + Long memberId = 1L; + ImageRequest imageRequest = new ImageRequest("key", "imageUrl", ImageType.MEMBER_PROFILE, 1L); + + when(imageRepository.countByTypeAndReferenceIdAndDeletedAtIsNull(any(), any())) + .thenReturn(1L); + + Member member = mock(Member.class); + when(memberService.findMemberById(memberId)).thenReturn(member); + + when(imageRepository.countByTypeAndReferenceIdAndDeletedAtIsNull(any(), any())) + .thenReturn(1L); + + // When & Then + assertThatThrownBy(() -> imageService.saveImage(memberId, imageRequest)) + .isInstanceOf(BusinessException.class) + .hasFieldOrPropertyWithValue("businessError", BusinessError.IMAGE_LIMIT_EXCEED); + } + + @Test + @DisplayName("이미지 소유자 검증 실패 테스트") + void validateImageOwner_IMAGE_OWNER_MISMATCH() { + // Given + Long memberId = 1L; + ImageRequest imageRequest = new ImageRequest("key", "imageUrl", ImageType.MEMBER_PROFILE, 1L); + + Member member = mock(Member.class); + when(memberService.findMemberById(memberId)).thenReturn(member); + when(member.isAdmin()).thenReturn(false); + + ImageOwnerValidator validator = mock(ImageOwnerValidator.class); + when(validatorFactory.getValidator(any())).thenReturn(validator); + when(validator.validateOwner(any(), any())).thenReturn(false); + + // When & Then + assertThatThrownBy(() -> imageService.saveImage(memberId, imageRequest)) + .isInstanceOf(BusinessException.class) + .hasFieldOrPropertyWithValue("businessError", BusinessError.IMAGE_OWNER_MISMATCH); + } + + @Test + @DisplayName("이미지 삭제 성공 테스트") + void deleteImage_success() { + // Given + Long memberId = 1L; + Long imageId = 1L; + Image image = new Image("key", "imageUrl", ImageType.MEMBER_PROFILE, 1L); + + Member member = mock(Member.class); + when(memberService.findMemberById(memberId)).thenReturn(member); + when(member.isAdmin()).thenReturn(false); + + when(imageRepository.findByIdAndDeletedAtIsNull(imageId)) + .thenReturn(Optional.of(image)); + + // When + imageService.deleteImage(memberId, imageId); + + // Then + verify(imageRepository, times(1)).delete(any(Image.class)); + } + + @Test + @DisplayName("이미지 삭제 실패 테스트 - 이미지 없음") + void deleteImage_IMAGE_NOT_FOUND() { + // Given + Long memberId = 1L; + Long imageId = 1L; + + when(imageRepository.findByIdAndDeletedAtIsNull(imageId)) + .thenReturn(Optional.empty()); + + // When & Then + assertThatThrownBy(() -> imageService.deleteImage(memberId, imageId)) + .isInstanceOf(BusinessException.class) + .hasFieldOrPropertyWithValue("businessError", BusinessError.IMAGE_NOT_FOUND); + } + + @Test + @DisplayName("이미지 업데이트 성공 테스트") + void updateImage_success() { + // Given + Long memberId = 1L; + Long imageId = 1L; + ImageRequest imageRequest = new ImageRequest("newKey", "newImageUrl", ImageType.MEMBER_PROFILE, 1L); + + Member member = mock(Member.class); + when(memberService.findMemberById(memberId)).thenReturn(member); + when(member.isAdmin()).thenReturn(false); + + Image existingImage = new Image("key", "imageUrl", ImageType.MEMBER_PROFILE, 1L); + when(imageRepository.findByIdAndDeletedAtIsNull(imageId)) + .thenReturn(Optional.of(existingImage)); + + when(imageRepository.save(any(Image.class))) + .thenReturn(existingImage); + + // When + ImageResponse imageResponse = imageService.updateImage(memberId, imageId, imageRequest); + + // Then + assertNotNull(imageResponse); + assertEquals(imageRequest.objectKey(), "newKey"); + assertEquals(imageResponse.objectKey(), "key"); + verify(imageRepository, times(1)).save(any(Image.class)); + } + + @Test + @DisplayName("이미지 수정 실패 - 자신의 이미지가 아님") + void updateImage_IMAGE_OWNER_MISMATCH() { + // Given + Long memberId = 1L; + Long imageId = 1L; + ImageRequest imageRequest = new ImageRequest("newKey", "newImageUrl", ImageType.PRODUCT, 4L); + + Member member = mock(Member.class); + when(memberService.findMemberById(memberId)).thenReturn(member); + when(member.isAdmin()).thenReturn(false); + + ImageOwnerValidator validator = mock(ImageOwnerValidator.class); + when(validatorFactory.getValidator(any())).thenReturn(validator); + when(validator.validateOwner(any(), any())).thenReturn(false); + + Image existingImage = new Image("key", "imageUrl", ImageType.MEMBER_PROFILE, 1L); + when(imageRepository.findByIdAndDeletedAtIsNull(imageId)) + .thenReturn(Optional.of(existingImage)); + + when(imageRepository.save(any(Image.class))) + .thenReturn(existingImage); + + assertThatThrownBy(() -> imageService.updateImage(memberId, imageId, imageRequest)) + .isInstanceOf(BusinessException.class) + .hasFieldOrPropertyWithValue("businessError", BusinessError.IMAGE_OWNER_MISMATCH); + } +} diff --git a/src/test/java/poomasi/domain/member/_profile/service/MemberProfileServiceTest.java b/src/test/java/poomasi/domain/member/_profile/service/MemberProfileServiceTest.java new file mode 100644 index 00000000..0bd38f8b --- /dev/null +++ b/src/test/java/poomasi/domain/member/_profile/service/MemberProfileServiceTest.java @@ -0,0 +1,89 @@ +package poomasi.domain.member._profile.service; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import poomasi.domain.member._profile.entity.MemberProfile; +import poomasi.domain.member._profile.repository.MemberProfileRepository; +import poomasi.global.error.BusinessError; +import poomasi.global.error.BusinessException; + +import java.util.Optional; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; +import static org.junit.jupiter.api.Assertions.*; + +@ExtendWith(MockitoExtension.class) +class MemberProfileServiceTest { + + @Mock + private MemberProfileRepository memberProfileRepository; + + @InjectMocks + private MemberProfileService memberProfileService; + + private MemberProfile memberProfile; + + @BeforeEach + void setup() { + memberProfile = mock(MemberProfile.class); + } + + @Test + @DisplayName("getMemberProfileById 성공 테스트") + void getMemberProfileById_Success() { + // Given + when(memberProfile.getId()).thenReturn(1L); + when(memberProfile.getPhoneNumber()).thenReturn("123456789"); + when(memberProfileRepository.findByIdAndDeletedAtIsNull(1L)).thenReturn(Optional.of(memberProfile)); + + // When + MemberProfile result = memberProfileService.getMemberProfileById(1L); + + // Then + assertEquals(1L, result.getId()); + assertEquals("123456789", result.getPhoneNumber()); + + verify(memberProfileRepository).findByIdAndDeletedAtIsNull(1L); + } + + @Test + @DisplayName("getMemberProfileById 실패 테스트 - 프로필을 찾을 수 없는 경우") + void getMemberProfileById_NotFound() { + // Given + when(memberProfileRepository.findByIdAndDeletedAtIsNull(1L)).thenReturn(Optional.empty()); + + // When & Then + assertThatThrownBy(() -> memberProfileService.getMemberProfileById(1L)) + .isInstanceOf(BusinessException.class) + .hasFieldOrPropertyWithValue("businessError", BusinessError.MEMBER_PROFILE_NOT_FOUND); + + + verify(memberProfileRepository).findByIdAndDeletedAtIsNull(1L); + } + + @Test + @DisplayName("saveMemberProfile 성공 테스트") + void saveMemberProfile_Success() { + // Given + when(memberProfile.getId()).thenReturn(1L); + when(memberProfile.getPhoneNumber()).thenReturn("123456789"); + when(memberProfileRepository.save(any(MemberProfile.class))).thenReturn(memberProfile); + + // When + MemberProfile result = memberProfileService.saveMemberProfile(memberProfile); + + // Then + assertNotNull(result); + assertEquals(1L, result.getId()); + assertEquals("123456789", result.getPhoneNumber()); + + verify(memberProfileRepository).save(memberProfile); + } +} diff --git a/src/test/java/poomasi/domain/member/service/MemberCustomerServiceTest.java b/src/test/java/poomasi/domain/member/service/MemberCustomerServiceTest.java new file mode 100644 index 00000000..b426fe09 --- /dev/null +++ b/src/test/java/poomasi/domain/member/service/MemberCustomerServiceTest.java @@ -0,0 +1,142 @@ +package poomasi.domain.member.service; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import poomasi.domain.member._profile.dto.request.AddressUpdateRequest; +import poomasi.domain.member._profile.entity.MemberProfile; +import poomasi.domain.member.dto.request.CustomerUpdateRequest; +import poomasi.domain.member.entity.Member; +import poomasi.domain.member.repository.MemberRepository; +import poomasi.global.error.BusinessException; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.*; +import static poomasi.domain.member.entity.Role.ROLE_CUSTOMER; +import static poomasi.domain.member.entity.Role.ROLE_FARMER; +import static poomasi.global.error.BusinessError.INVALID_ROLE; +import static poomasi.global.error.BusinessError.MEMBER_ALREADY_CUSTOMER; + +@ExtendWith(MockitoExtension.class) +class MemberCustomerServiceTest { + + @Mock + private MemberRepository memberRepository; + + @Mock + private MemberService memberService; + + @InjectMocks + private MemberCustomerService memberCustomerService; + + private Member member; + + @BeforeEach + void setUp() { + member = Member.builder() + .id(1L) + .name("testName") + .email("test@example.com") + .password("password") + .role(ROLE_CUSTOMER) + .build(); + } + + @Test + @DisplayName("농부 -> 고객 전환 성공 테스트") + void convertToCustomer_success() { + // given + member.setRole(ROLE_FARMER); + given(memberService.findMemberById(member.getId())).willReturn(member); + + // when + memberCustomerService.convertToCustomer(member.getId()); + + // then + assertEquals(ROLE_CUSTOMER, member.getRole()); + verify(memberRepository, times(1)).save(member); + } + + @Test + @DisplayName("이미 고객일 때 고객 전환 실패 테스트") + void convertToCustomer_alreadyCustomer() { + // given + member.setRole(ROLE_CUSTOMER); + given(memberService.findMemberById(member.getId())).willReturn(member); + + // when & then + assertThatThrownBy(() -> memberCustomerService.convertToCustomer(member.getId())) + .isInstanceOf(BusinessException.class) + .hasFieldOrPropertyWithValue("businessError", MEMBER_ALREADY_CUSTOMER); + } + + @Test + @DisplayName("회원 정보 업데이트 성공 테스트") + void updateCustomer_success() { + // given + CustomerUpdateRequest customerUpdateRequest = new CustomerUpdateRequest( + "UpdatedName", + "updated@example.com", + "newPassword", + "9876543210"); + + doAnswer(invocation -> { + Member member = invocation.getArgument(0); + member.setName(customerUpdateRequest.name()); + member.setEmail(customerUpdateRequest.email()); + member.setPassword(customerUpdateRequest.password()); + member.getOrCreateProfile().setPhoneNumber(customerUpdateRequest.phoneNumber()); + return null; + }).when(memberService).updateCommonAttributes(any(Member.class), anyString(), anyString(), anyString(), anyString()); + + given(memberRepository.save(member)).willReturn(member); + + // when + Member updatedMember = memberCustomerService.updateCustomer(member, customerUpdateRequest); + + // then + assertEquals("UpdatedName", updatedMember.getName()); + assertEquals("updated@example.com", updatedMember.getEmail()); + verify(memberService, times(1)).updateCommonAttributes(member, customerUpdateRequest.name(), customerUpdateRequest.email(), customerUpdateRequest.password(), customerUpdateRequest.phoneNumber()); + verify(memberRepository, times(1)).save(member); + } + + @Test + @DisplayName("회원 정보 업데이트 실패 테스트 - Role이 다름") + void updateCustomer_invalidRole() { + // given + member.setRole(ROLE_FARMER); + CustomerUpdateRequest customerUpdateRequest = new CustomerUpdateRequest("UpdatedName", "updated@example.com", "newPassword", "9876543210"); + + // when & then + assertThatThrownBy(() -> memberCustomerService.updateCustomer(member, customerUpdateRequest)) + .isInstanceOf(BusinessException.class) + .hasFieldOrPropertyWithValue("businessError", INVALID_ROLE); + } + + @Test + @DisplayName("회원 주소 업데이트 성공 테스트") + void updateAddress_success() { + // given + AddressUpdateRequest addressUpdateRequest = new AddressUpdateRequest("defaultAddress", "addressDetail", 10.0, 20.0); + MemberProfile profile = new MemberProfile(); + member.setMemberProfile(profile); + + // when + memberCustomerService.updateAddress(member, addressUpdateRequest); + + // then + assertEquals("defaultAddress", profile.getDefaultAddress()); + assertEquals("addressDetail", profile.getAddressDetail()); + assertEquals(10.0, profile.getCoordinateX()); + assertEquals(20.0, profile.getCoordinateY()); + verify(memberRepository, times(1)).save(member); + } +} diff --git a/src/test/java/poomasi/domain/member/service/MemberFarmerServiceTest.java b/src/test/java/poomasi/domain/member/service/MemberFarmerServiceTest.java new file mode 100644 index 00000000..4a8c88d0 --- /dev/null +++ b/src/test/java/poomasi/domain/member/service/MemberFarmerServiceTest.java @@ -0,0 +1,146 @@ +package poomasi.domain.member.service; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import poomasi.domain.member.dto.request.ConvertToFarmerRequest; +import poomasi.domain.member.dto.request.FarmerUpdateRequest; +import poomasi.domain.member.entity.Member; +import poomasi.domain.member.entity.Role; +import poomasi.domain.member.repository.MemberRepository; +import poomasi.domain.store.entity.Store; +import poomasi.domain.store.service.StoreService; +import poomasi.global.error.BusinessError; +import poomasi.global.error.BusinessException; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class MemberFarmerServiceTest { + + @Mock + private MemberRepository memberRepository; + + @Mock + private MemberService memberService; + + @Mock + private StoreService storeService; + + @InjectMocks + private MemberFarmerService memberFarmerService; + + private Member customerMember; + private Member farmerMember; + + @BeforeEach + void setUp() { + customerMember = Member.builder() + .id(1L) + .name("Customer") + .email("customer@example.com") + .password("password") + .role(Role.ROLE_CUSTOMER) + .build(); + + farmerMember = Member.builder() + .id(2L) + .name("Farmer") + .email("farmer@example.com") + .password("password") + .role(Role.ROLE_FARMER) + .build(); + } + + @Test + @DisplayName("회원 -> 농부로 변환 성공 테스트") + void convertToFarmer_success() { + // given + ConvertToFarmerRequest convertToFarmerRequest = new ConvertToFarmerRequest("name", "address", "phone"); + given(memberRepository.save(customerMember)).willReturn(customerMember); + + // when + memberFarmerService.convertToFarmer(customerMember, convertToFarmerRequest); + + // then + assertEquals(Role.ROLE_FARMER, customerMember.getRole()); + verify(memberRepository, times(1)).save(customerMember); + } + + @Test + @DisplayName("이미 농부인 회원 변환 시도 시 예외 발생 테스트") + void convertToFarmer_alreadyFarmer() { + ConvertToFarmerRequest convertToFarmerRequest = new ConvertToFarmerRequest("name", "address", "phone"); + // when & then + assertThatThrownBy(() -> memberFarmerService.convertToFarmer(farmerMember, convertToFarmerRequest)) + .isInstanceOf(BusinessException.class) + .hasFieldOrPropertyWithValue("businessError", BusinessError.MEMBER_ALREADY_FARMER); + } + + @Test + @DisplayName("농부 정보 업데이트 성공 테스트") + void updateFarmer_success() { + // given + FarmerUpdateRequest farmerUpdateRequest = new FarmerUpdateRequest( + "UpdatedFarmer", + "farmer@newemail.com", + "newPassword", + "1234567890", + "NewStoreName", + "NewStoreAddress"); + Store store = new Store(); + farmerMember.setStore(store); + + doAnswer(invocation -> { + Member member = invocation.getArgument(0); + member.setName(farmerUpdateRequest.name()); + member.setEmail(farmerUpdateRequest.email()); + member.setPassword(farmerUpdateRequest.password()); + member.getOrCreateProfile().setPhoneNumber(farmerUpdateRequest.phoneNumber()); + return null; + }).when(memberService).updateCommonAttributes(any(Member.class), anyString(), anyString(), anyString(), anyString()); + + + given(memberRepository.save(farmerMember)).willReturn(farmerMember); + + // when + Member updatedMember = memberFarmerService.updateFarmer(farmerMember, farmerUpdateRequest); + + // then + assertEquals("UpdatedFarmer", updatedMember.getName()); + assertEquals("farmer@newemail.com", updatedMember.getEmail()); + assertEquals("NewStoreName", updatedMember.getStore().getName()); + assertEquals("NewStoreAddress", updatedMember.getStore().getAddress()); + + verify(memberService, times(1)).updateCommonAttributes(farmerMember, farmerUpdateRequest.name(), farmerUpdateRequest.email(), farmerUpdateRequest.password(), farmerUpdateRequest.phoneNumber()); + verify(memberRepository, times(1)).save(farmerMember); + } + + @Test + @DisplayName("농부가 아닌 회원이 농부 정보 업데이트 시도 시 예외 발생 테스트") + void updateFarmer_invalidRole() { + // given + FarmerUpdateRequest farmerUpdateRequest = new FarmerUpdateRequest( + "UpdatedFarmer", + "farmer@newemail.com", + "newPassword", + "1234567890", + "NewStoreName", + "NewStoreAddress"); + + // when & then + assertThatThrownBy(() -> memberFarmerService.updateFarmer(customerMember, farmerUpdateRequest)) + .isInstanceOf(BusinessException.class) + .hasFieldOrPropertyWithValue("businessError", BusinessError.INVALID_ROLE); + + verify(memberRepository, never()).save(any(Member.class)); + } +} diff --git a/src/test/java/poomasi/domain/member/service/MemberServiceTest.java b/src/test/java/poomasi/domain/member/service/MemberServiceTest.java new file mode 100644 index 00000000..b7b0d029 --- /dev/null +++ b/src/test/java/poomasi/domain/member/service/MemberServiceTest.java @@ -0,0 +1,170 @@ +package poomasi.domain.member.service; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.crypto.password.PasswordEncoder; +import poomasi.domain.member.dto.request.SignupRequest; +import poomasi.domain.member.dto.response.SignUpResponse; +import poomasi.domain.member.entity.Member; +import poomasi.domain.member.repository.MemberRepository; +import poomasi.global.error.BusinessError; +import poomasi.global.error.BusinessException; + +import java.util.Optional; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.*; +import static poomasi.domain.member.entity.Role.ROLE_CUSTOMER; + +@ExtendWith(MockitoExtension.class) +class MemberServiceTest { + + @Mock + private MemberRepository memberRepository; + + @Mock + private PasswordEncoder passwordEncoder; + + @InjectMocks + private MemberService memberService; + + private Member member; + private Long memberId; + + @BeforeEach + void setUp() { + memberId = 1L; + member = Member.builder() + .id(memberId) + .name("testName") + .email("test@example.com") + .password("password") + .role(ROLE_CUSTOMER) + .build(); + } + + @Test + @DisplayName("회원가입 성공 테스트") + void signUp_success() { + // given + SignupRequest signupRequest = new SignupRequest("testName", "test@example.com", "testPassword"); + Member mockMember = mock(Member.class); + given(memberRepository.save(any(Member.class))).willReturn(mockMember); + + // when + SignUpResponse response = memberService.signUp(signupRequest); + + // then + assertNotNull(response); + assertEquals("회원 가입 성공", response.message()); + verify(memberRepository, times(1)).save(any(Member.class)); + } + + @Test + @DisplayName("이메일 중복 시 회원가입 실패 테스트") + void signUp_DuplicationMember() { + // given + SignupRequest signupRequest = new SignupRequest("testName", "test@example.com", "testPassword"); + given(memberRepository.findByEmailAndDeletedAtIsNull(anyString())).willReturn(Optional.of(mock(Member.class))); + + // when & then + assertThatThrownBy(() -> memberService.signUp(signupRequest)) + .isInstanceOf(BusinessException.class) + .hasFieldOrPropertyWithValue("businessError", BusinessError.DUPLICATE_MEMBER_EMAIL); + + verify(memberRepository, times(1)).findByEmailAndDeletedAtIsNull(signupRequest.email()); + } + + @Test + @DisplayName("id로 멤버 조회 성공 테스트") + void getMemberById_success() { + // given + when(memberRepository.findByIdAndDeletedAtIsNull(memberId)).thenReturn(Optional.of(member)); + + // when + var memberResponse = memberService.getMemberById(memberId); + + // then + assertNotNull(memberResponse); + assertEquals("testName", memberResponse.name()); + assertEquals("test@example.com", memberResponse.email()); + } + + @Test + @DisplayName("회원 정보 없을 때 조회 실패 테스트") + void getMemberById_memberNotFound() { + // given + when(memberRepository.findByIdAndDeletedAtIsNull(memberId)).thenReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> memberService.getMemberById(memberId)) + .isInstanceOf(BusinessException.class) + .hasFieldOrPropertyWithValue("businessError", poomasi.global.error.BusinessError.MEMBER_NOT_FOUND); + } + + @Test + @DisplayName("회원 정보 업데이트 테스트") + void updateCommonAttributes_success() { + // given + String newName = "Updated Name"; + String newEmail = "updated@example.com"; + String newPassword = "newPassword"; + String newPhoneNumber = "1234567890"; + + // when + memberService.updateCommonAttributes(member, newName, newEmail, newPassword, newPhoneNumber); + + // then + assertEquals(member.getName(), newName); + assertEquals(member.getEmail(), newEmail); + assertNotEquals(member.getPassword(), "password"); + assertEquals(member.getOrCreateProfile().getPhoneNumber(), newPhoneNumber); + } + + @Test + @DisplayName("회원 탈퇴 테스트") + void deleteAccount_success() { + // when + memberService.deleteAccount(member); + + // then + verify(memberRepository, times(1)).delete(member); + } + + @Test + @DisplayName("회원 복구 테스트") + void restoreAccount_success() { + // given + member.setDeletedAt(java.time.LocalDateTime.now()); + when(memberRepository.findByIdAndDeletedAtIsNotNull(memberId)).thenReturn(Optional.of(member)); + + // when + memberService.restoreAccount(memberId); + + // then + assertThat(member.getDeletedAt()).isNull(); + verify(memberRepository, times(1)).save(member); + } + + @DisplayName("회원 정지 테스트") + @Test + void suspendAccount_success() { + when(memberRepository.findByIdAndDeletedAtIsNull(memberId)).thenReturn(Optional.of(member)); + + // when + memberService.suspendAccount(memberId); + + // then + assertThat(member.getOrCreateProfile().isBanned()).isTrue(); + verify(memberRepository, times(1)).save(member); + } +} \ No newline at end of file diff --git a/src/test/java/poomasi/domain/product/cart/CartServiceTest.java b/src/test/java/poomasi/domain/product/cart/CartServiceTest.java new file mode 100644 index 00000000..51a6fc92 --- /dev/null +++ b/src/test/java/poomasi/domain/product/cart/CartServiceTest.java @@ -0,0 +1,235 @@ +package poomasi.domain.product.cart; + +import java.math.BigDecimal; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import poomasi.domain.member.entity.Member; +import poomasi.domain.product._cart.dto.CartResponse; +import poomasi.domain.product._cart.entity.Cart; +import poomasi.domain.product._cart.repository.CartRepository; +import poomasi.domain.product._cart.service.CartService; +import poomasi.domain.product.entity.Product; +import poomasi.domain.product.service.ProductService; +import poomasi.global.error.BusinessError; +import poomasi.global.error.BusinessException; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class CartServiceTest { + + @InjectMocks + private CartService cartService; + + @Mock + private CartRepository cartRepository; + + @Mock + private ProductService productService; + + private Member member; + private Product product; + private Cart cart; + + @BeforeEach + void setUp() { + member = Member.builder() + .id(1L) + .email("test@test.com") + .build(); + + product = Product.builder() + .productId(1L) + .name("테스트 상품") + .price(BigDecimal.valueOf(10000)) + .build(); + + cart = Cart.builder() + .id(1L) + .member(member) + .product(product) + .build(); + } + + @Nested + @DisplayName("장바구니 조회") + class GetCart { + @Test + @DisplayName("성공") + void success() { + // given + List expectedResponses = Arrays.asList( + new CartResponse(1L, "상품1",BigDecimal.valueOf(10000),"asd"), + new CartResponse(2L, "상품2",BigDecimal.valueOf(20000),"asdf") + ); + given(cartRepository.findByMember(member)).willReturn(expectedResponses); + + // when + List responses = cartService.getCart(member); + + // then + assertThat(responses).hasSize(2); + verify(cartRepository).findByMember(member); + } + } + + @Nested + @DisplayName("장바구니 추가") + class AddCart { + @Test + @DisplayName("성공 - 새로운 상품") + void success_NewProduct() { + // given + given(productService.findProductById(product.getId())).willReturn(product); + given(cartRepository.findByMemberIdAndProductId(member.getId(), product.getId())) + .willReturn(Optional.empty()); + given(cartRepository.save(any(Cart.class))).willReturn(cart); + + // when + Long cartId = cartService.addCart(member, product.getId()); + + // then + assertThat(cartId).isEqualTo(cart.getId()); + verify(cartRepository).save(any(Cart.class)); + } + + @Test + @DisplayName("성공 - 이미 존재하는 상품") + void success_ExistingProduct() { + // given + given(productService.findProductById(product.getId())).willReturn(product); + given(cartRepository.findByMemberIdAndProductId(member.getId(), product.getId())) + .willReturn(Optional.of(cart)); + + // when + Long cartId = cartService.addCart(member, product.getId()); + + // then + assertThat(cartId).isEqualTo(cart.getId()); + verify(cartRepository, never()).save(any(Cart.class)); + } + } + + @Nested + @DisplayName("장바구니 삭제") + class DeleteCart { + @Test + @DisplayName("성공") + void success() { + // given + given(cartRepository.findById(cart.getId())).willReturn(Optional.of(cart)); + + // when + cartService.deleteCart(member, cart.getId()); + + // then + verify(cartRepository).delete(cart); + } + + @Test + @DisplayName("실패 - 존재하지 않는 장바구니") + void fail_CartNotFound() { + // given + given(cartRepository.findById(cart.getId())).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> cartService.deleteCart(member, cart.getId())) + .isInstanceOf(BusinessException.class) + .hasFieldOrPropertyWithValue("businessError", BusinessError.CART_NOT_FOUND); + } + + @Test + @DisplayName("실패 - 권한 없음") + void fail_UnauthorizedAccess() { + // given + Member otherMember = Member.builder().id(2L).build(); + given(cartRepository.findById(cart.getId())).willReturn(Optional.of(cart)); + + // when & then + assertThatThrownBy(() -> cartService.deleteCart(otherMember, cart.getId())) + .isInstanceOf(BusinessException.class) + .hasFieldOrPropertyWithValue("businessError", BusinessError.MEMBER_ID_MISMATCH); + } + } + + @Nested + @DisplayName("장바구니 전체 삭제") + class DeleteAll { + @Test + @DisplayName("성공") + void success() { + // when + cartService.deleteAll(member); + + // then + verify(cartRepository).deleteAllByMemberId(member.getId()); + } + } + + @Nested + @DisplayName("ID 리스트로 장바구니 조회") + class GetCartsByIdList { + @Test + @DisplayName("성공") + void success() { + // given + List cartIds = Arrays.asList(1L, 2L); + List carts = Arrays.asList( + Cart.builder().id(1L).build(), + Cart.builder().id(2L).build() + ); + given(cartRepository.getCartsByIdList(cartIds)).willReturn(carts); + + // when + List result = cartService.getCartsByIdList(cartIds); + + // then + assertThat(result).hasSize(2); + verify(cartRepository).deleteAll(carts); + } + } + + @Nested + @DisplayName("장바구니 ID로 조회") + class GetCartById { + @Test + @DisplayName("성공") + void success() { + // given + given(cartRepository.findById(cart.getId())).willReturn(Optional.of(cart)); + + // when + Cart result = cartService.getCartById(cart.getId()); + + // then + assertThat(result).isNotNull(); + assertThat(result.getId()).isEqualTo(cart.getId()); + } + + @Test + @DisplayName("실패 - 존재하지 않는 장바구니") + void fail_CartNotFound() { + // given + given(cartRepository.findById(cart.getId())).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> cartService.getCartById(cart.getId())) + .isInstanceOf(BusinessException.class) + .hasFieldOrPropertyWithValue("businessError", BusinessError.CART_NOT_FOUND); + } + } +} diff --git a/src/test/java/poomasi/domain/product/category/CategoryAdminServiceTest.java b/src/test/java/poomasi/domain/product/category/CategoryAdminServiceTest.java new file mode 100644 index 00000000..3afa8ab6 --- /dev/null +++ b/src/test/java/poomasi/domain/product/category/CategoryAdminServiceTest.java @@ -0,0 +1,152 @@ +package poomasi.domain.product.category; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import poomasi.domain.product._category.dto.CategoryRequest; +import poomasi.domain.product._category.entity.Category; +import poomasi.domain.product._category.repository.CategoryRepository; +import poomasi.domain.product._category.service.CategoryAdminService; +import poomasi.global.error.BusinessError; +import poomasi.global.error.BusinessException; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class CategoryAdminServiceTest { + + @InjectMocks + private CategoryAdminService categoryAdminService; + + @Mock + private CategoryRepository categoryRepository; + + @Nested + @DisplayName("카테고리 등록") + class RegisterCategory { + private CategoryRequest request; + private Category category; + + @BeforeEach + void setUp() { + request = new CategoryRequest("테스트 카테고리"); + category = request.toEntity(); + category = Category.builder() + .id(1L) + .name("테스트 카테고리") + .build(); + } + + @Test + @DisplayName("성공") + void success() { + // given + given(categoryRepository.save(any(Category.class))).willReturn(category); + + // when + Long savedId = categoryAdminService.registerCategory(request); + + // then + assertThat(savedId).isEqualTo(1L); + verify(categoryRepository, times(1)).save(any(Category.class)); + } + } + + @Nested + @DisplayName("카테고리 수정") + class ModifyCategory { + private Long categoryId; + private CategoryRequest request; + private Category category; + + @BeforeEach + void setUp() { + categoryId = 1L; + request = new CategoryRequest("수정된 카테고리"); + category = Category.builder() + .id(categoryId) + .name("기존 카테고리") + .build(); + } + + @Test + @DisplayName("성공") + void success() { + // given + given(categoryRepository.findById(categoryId)).willReturn(Optional.of(category)); + + // when + categoryAdminService.modifyCategory(categoryId, request); + + // then + assertThat(category.getName()).isEqualTo(request.name()); + verify(categoryRepository, times(1)).findById(categoryId); + } + + @Test + @DisplayName("실패 - 존재하지 않는 카테고리") + void fail_CategoryNotFound() { + // given + given(categoryRepository.findById(categoryId)).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> categoryAdminService.modifyCategory(categoryId, request)) + .isInstanceOf(BusinessException.class); + verify(categoryRepository, times(1)).findById(categoryId); + } + } + + @Nested + @DisplayName("카테고리 삭제") + class DeleteCategory { + private Long categoryId; + private Category category; + + @BeforeEach + void setUp() { + categoryId = 1L; + category = Category.builder() + .id(categoryId) + .name("삭제할 카테고리") + .build(); + } + + @Test + @DisplayName("성공") + void success() { + // given + given(categoryRepository.findById(categoryId)).willReturn(Optional.of(category)); + + // when + categoryAdminService.deleteCategory(categoryId); + + // then + verify(categoryRepository, times(1)).findById(categoryId); + verify(categoryRepository, times(1)).delete(category); + } + + @Test + @DisplayName("실패 - 존재하지 않는 카테고리") + void fail_CategoryNotFound() { + // given + given(categoryRepository.findById(categoryId)).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> categoryAdminService.deleteCategory(categoryId)) + .isInstanceOf(BusinessException.class); + verify(categoryRepository, times(1)).findById(categoryId); + verify(categoryRepository, never()).delete(any()); + } + } +} diff --git a/src/test/java/poomasi/domain/product/category/CategoryServiceTest.java b/src/test/java/poomasi/domain/product/category/CategoryServiceTest.java new file mode 100644 index 00000000..fa07ad2f --- /dev/null +++ b/src/test/java/poomasi/domain/product/category/CategoryServiceTest.java @@ -0,0 +1,203 @@ +package poomasi.domain.product.category; + +import java.math.BigDecimal; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import poomasi.domain.product._category.dto.CategoryResponse; +import poomasi.domain.product._category.dto.ProductListInCategoryResponse; +import poomasi.domain.product._category.entity.Category; +import poomasi.domain.product._category.repository.CategoryRepository; +import poomasi.domain.product._category.service.CategoryService; +import poomasi.domain.product.entity.Product; +import poomasi.global.error.BusinessError; +import poomasi.global.error.BusinessException; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class CategoryServiceTest { + + @InjectMocks + private CategoryService categoryService; + + @Mock + private CategoryRepository categoryRepository; + + @Nested + @DisplayName("전체 카테고리 조회") + class GetAllCategories { + private List categories; + + @BeforeEach + void setUp() { + categories = Arrays.asList( + Category.builder().id(1L).name("카테고리1").build(), + Category.builder().id(2L).name("카테고리2").build(), + Category.builder().id(3L).name("카테고리3").build() + ); + } + + @Test + @DisplayName("성공 - 카테고리 목록 존재") + void success_WithCategories() { + // given + given(categoryRepository.findAll()).willReturn(categories); + + // when + List responses = categoryService.getAllCategories(); + + // then + assertThat(responses).hasSize(3); + assertThat(responses.getFirst().id()).isEqualTo(1L); + assertThat(responses.getFirst().name()).isEqualTo("카테고리1"); + verify(categoryRepository, times(1)).findAll(); + } + + @Test + @DisplayName("성공 - 카테고리 목록 비어있음") + void success_WithEmptyCategories() { + // given + given(categoryRepository.findAll()).willReturn(Collections.emptyList()); + + // when + List responses = categoryService.getAllCategories(); + + // then + assertThat(responses).isEmpty(); + verify(categoryRepository, times(1)).findAll(); + } + } + + @Nested + @DisplayName("카테고리 내 상품 조회") + class GetProductInCategory { + private Long categoryId; + private Category category; + private List products; + + @BeforeEach + void setUp() { + categoryId = 1L; + products = Arrays.asList( + Product.builder().productId(1L).name("상품1").price(BigDecimal.valueOf(1000)).build(), + Product.builder().productId(2L).name("상품2").price(BigDecimal.valueOf(2000)).build() + ); + category = Category.builder() + .id(categoryId) + .name("테스트 카테고리") + .build(); + + category.addProduct(products.get(0)); + category.addProduct(products.get(1)); + } + + @Test + @DisplayName("성공 - 상품 존재") + void success_WithProducts() { + // given + given(categoryRepository.findById(categoryId)).willReturn(Optional.of(category)); + + // when + List responses = categoryService.getProductInCategory(categoryId); + + // then + assertThat(responses).hasSize(2); + assertThat(responses.get(0).categoryId()).isEqualTo(1L); + assertThat(responses.get(0).name()).isEqualTo("상품1"); + assertThat(responses.get(1).categoryId()).isEqualTo(1L); + assertThat(responses.get(1).name()).isEqualTo("상품2"); + verify(categoryRepository, times(1)).findById(categoryId); + } + + @Test + @DisplayName("성공 - 상품 없음") + void success_WithNoProducts() { + // given + Category emptyCategory = Category.builder() + .id(categoryId) + .name("테스트 카테고리") + .build(); + given(categoryRepository.findById(categoryId)).willReturn(Optional.of(emptyCategory)); + + // when + List responses = categoryService.getProductInCategory(categoryId); + + // then + assertThat(responses).isEmpty(); + verify(categoryRepository, times(1)).findById(categoryId); + } + + @Test + @DisplayName("실패 - 존재하지 않는 카테고리") + void fail_CategoryNotFound() { + // given + given(categoryRepository.findById(categoryId)).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> categoryService.getProductInCategory(categoryId)) + .isInstanceOf(BusinessException.class) + .hasFieldOrPropertyWithValue("businessError", BusinessError.CATEGORY_NOT_FOUND); + verify(categoryRepository, times(1)).findById(categoryId); + } + } + + @Nested + @DisplayName("카테고리 조회") + class GetCategory { + private Long categoryId; + private Category category; + + @BeforeEach + void setUp() { + categoryId = 1L; + category = Category.builder() + .id(categoryId) + .name("테스트 카테고리") + .build(); + } + + @Test + @DisplayName("성공") + void success() { + // given + given(categoryRepository.findById(categoryId)).willReturn(Optional.of(category)); + + // when + Category foundCategory = categoryService.getCategory(categoryId); + + // then + assertThat(foundCategory).isNotNull(); + assertThat(foundCategory.getId()).isEqualTo(categoryId); + assertThat(foundCategory.getName()).isEqualTo("테스트 카테고리"); + verify(categoryRepository, times(1)).findById(categoryId); + } + + @Test + @DisplayName("실패 - 존재하지 않는 카테고리") + void fail_CategoryNotFound() { + // given + given(categoryRepository.findById(categoryId)).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> categoryService.getCategory(categoryId)) + .isInstanceOf(BusinessException.class) + .hasFieldOrPropertyWithValue("businessError", BusinessError.CATEGORY_NOT_FOUND); + verify(categoryRepository, times(1)).findById(categoryId); + } + } +} \ No newline at end of file diff --git a/src/test/java/poomasi/domain/reservation/service/ReservationFarmerServiceTest.java b/src/test/java/poomasi/domain/reservation/service/ReservationFarmerServiceTest.java new file mode 100644 index 00000000..3bc8eafc --- /dev/null +++ b/src/test/java/poomasi/domain/reservation/service/ReservationFarmerServiceTest.java @@ -0,0 +1,90 @@ +package poomasi.domain.reservation.service; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import poomasi.domain.farm._schedule.entity.FarmSchedule; +import poomasi.domain.farm.entity.Farm; +import poomasi.domain.farm.service.FarmService; +import poomasi.domain.member.entity.Member; +import poomasi.domain.reservation.dto.response.ReservationResponse; +import poomasi.domain.reservation.entity.Reservation; +import poomasi.global.error.BusinessException; + +import java.math.BigDecimal; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.BDDMockito.given; +import static poomasi.global.error.BusinessError.RESERVATION_NOT_ACCESSIBLE; + +@ExtendWith(MockitoExtension.class) +class ReservationFarmerServiceTest { + + @Mock + private Member member; + + @Mock + private FarmService farmService; + + @Mock + private ReservationService reservationService; + + @InjectMocks + private ReservationFarmerService reservationFarmerService; + + @Nested + @DisplayName("농장 예약 조회") + class GetReservationsByFarmerId { + + @Test + @DisplayName("농장 예약을 조회한다") + void should_getReservationsByFarmerId() { + // given + Farm farm = Farm.builder().id(1L).ownerId(1L).experiencePrice(1000).build(); // Farm 생성 + FarmSchedule farmSchedule = FarmSchedule.builder().id(1L).build(); // FarmSchedule 생성 + List farms = List.of(farm); + + Reservation reservation = Reservation.builder() + .id(1L) + .farm(farm) + .scheduleId(farmSchedule) // FarmSchedule 설정 + .member(member) + .price(new BigDecimal(1000)) + .build(); + + List reservations = List.of(reservation); + + given(member.isFarmer()).willReturn(true); + given(farmService.getFarmListByOwnerId(anyLong())).willReturn(farms); + given(reservationService.getReservationByFarmIds(farms)).willReturn(reservations); // 추가된 스터빙 + + // when + List result = reservationFarmerService.getReservationsByFarmerId(member); + + // then + assertEquals(1, result.size()); + } + + @Test + @DisplayName("농장 예약 조회 권한이 없는 경우") + void should_throwException_when_notAccessible() { + // given + given(member.isFarmer()).willReturn(false); + given(member.isAdmin()).willReturn(false); + + // when + BusinessException exception = assertThrows(BusinessException.class, + () -> reservationFarmerService.getReservationsByFarmerId(member)); + + // then + assertEquals(RESERVATION_NOT_ACCESSIBLE, exception.getBusinessError()); + } + } +} diff --git a/src/test/java/poomasi/domain/reservation/service/ReservationPlatformServiceTest.java b/src/test/java/poomasi/domain/reservation/service/ReservationPlatformServiceTest.java new file mode 100644 index 00000000..27c11525 --- /dev/null +++ b/src/test/java/poomasi/domain/reservation/service/ReservationPlatformServiceTest.java @@ -0,0 +1,187 @@ +package poomasi.domain.reservation.service; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import poomasi.domain.farm._schedule.entity.FarmSchedule; +import poomasi.domain.farm._schedule.service.FarmScheduleService; +import poomasi.domain.farm.entity.Farm; +import poomasi.domain.farm.service.FarmService; +import poomasi.domain.member.entity.Member; +import poomasi.domain.reservation.dto.request.ReservationRequest; +import poomasi.domain.reservation.dto.response.ReservationResponse; +import poomasi.domain.reservation.entity.Reservation; +import poomasi.global.error.BusinessException; +import poomasi.payment.entity.ItemType; +import poomasi.payment.util.PaymentUtil; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class ReservationPlatformServiceTest { + + @Mock + private ReservationService reservationService; + + @Mock + private FarmService farmService; + + @Mock + private FarmScheduleService farmScheduleService; + + @Mock + private PaymentUtil paymentUtil; + + @InjectMocks + private ReservationPlatformService reservationPlatformService; + + private Member member; + private Farm farm; + private FarmSchedule farmSchedule; + private ReservationRequest request; + + @BeforeEach + void setUp() { + member = Member.builder().id(1L).build(); + farm = Farm.builder().id(1L).ownerId(1L).maxReservation(5).maxCapacity(10).build(); + farmSchedule = FarmSchedule + .builder() + .id(1L) + .startTime(LocalTime.from(LocalDate.now().atStartOfDay())) + .endTime(LocalTime.from(LocalDate.now().atStartOfDay().plusHours(3))) + .date(LocalDate.now()) + .farmId(1L) + .build(); + + request = ReservationRequest + .builder() + .farmId(1L) + .scheduleId(1L) + .memberCount(5) + .build(); + } + + @Nested + @DisplayName("예약 생성 테스트") + class CreateReservation { + + + @Test + @DisplayName("최대 예약 수용 초과로 예약 생성 실패") + void shouldFailWhenMaxReservationExceeded() { + // given + given(farmService.getValidFarmByFarmId(anyLong())).willReturn(farm); + given(farmScheduleService.getFarmScheduleByScheduleId(anyLong())).willReturn(farmSchedule); + given(reservationService.getValidReservationsByFarmIdAndScheduleId(anyLong(), any())).willReturn(List.of(Reservation.builder().build(), Reservation.builder().build(), Reservation.builder().build(), Reservation.builder().build(), Reservation.builder().build())); + + // when & then + assertThrows(BusinessException.class, () -> reservationPlatformService.createReservation(member, request)); + } + } + + @Nested + @DisplayName("예약 조회 테스트") + class GetReservation { + + @Test + @DisplayName("예약 조회 성공") + void shouldGetReservationSuccessfully() { + // given + Reservation reservation = Reservation.builder() + .id(1L) + .farm(farm) + .scheduleId(farmSchedule) + .member(member) + .price(new BigDecimal(10000)) + .reservationDate(farmSchedule.getDate()) + .build(); + given(reservationService.getReservationById(1L)).willReturn(reservation); + + // when + ReservationResponse response = reservationPlatformService.getReservation(member, 1L); + + // then + assertNotNull(response); + assertEquals(1L, response.id()); + } + + @Test + @DisplayName("권한이 없어 예약 조회 실패") + void shouldFailWhenNoAccessRights() { + // given + Member otherMember = Member.builder().id(2L).build(); + Farm farm = Farm.builder().id(1L).ownerId(3L).build(); // farm의 ownerId는 3L + Member member = Member.builder().id(1L).build(); // 요청하는 member의 id는 1L + Reservation reservation = Reservation.builder() + .id(1L) + .farm(farm) + .scheduleId(farmSchedule) + .member(otherMember) // 예약자는 otherMember + .price(new BigDecimal(10000)) + .reservationDate(farmSchedule.getDate()) + .build(); + + given(reservationService.getReservationById(1L)).willReturn(reservation); + + // when & then + assertThrows(BusinessException.class, () -> reservationPlatformService.getReservation(member, 1L)); + } + } + + @Nested + @DisplayName("예약 취소 테스트") + class CancelReservation { + + @Test + @DisplayName("예약 취소 성공") + void shouldCancelReservationSuccessfully() { + // given + Reservation reservation = Reservation.builder() + .id(1L) + .farm(farm) + .scheduleId(farmSchedule) + .member(member) + .reservationDate(farmSchedule.getDate()) + .build(); + + given(reservationService.getReservationById(1L)).willReturn(reservation); + + // when + reservationPlatformService.cancelReservation(member, 1L); + + // then + verify(reservationService).cancelReservation(reservation); + } + + @Test + @DisplayName("권한이 없어 예약 취소 실패") + void shouldFailToCancelWhenNoAccessRights() { + // given + Member otherMember = Member.builder().id(2L).build(); + Reservation reservation = Reservation.builder() + .id(1L) + .farm(farm) + .scheduleId(farmSchedule) + .member(otherMember) + .build(); + given(reservationService.getReservationById(1L)).willReturn(reservation); + + // when & then + assertThrows(BusinessException.class, () -> reservationPlatformService.cancelReservation(member, 1L)); + } + } +} diff --git a/src/test/java/poomasi/domain/store/StoreServiceTest.java b/src/test/java/poomasi/domain/store/StoreServiceTest.java index c1327a03..6b933cc4 100644 --- a/src/test/java/poomasi/domain/store/StoreServiceTest.java +++ b/src/test/java/poomasi/domain/store/StoreServiceTest.java @@ -17,6 +17,7 @@ import java.util.Optional; import poomasi.domain.auth.security.userdetail.UserDetailsImpl; import poomasi.domain.member.entity.Member; +import poomasi.domain.member.service.MemberService; import poomasi.domain.store.dto.StoreRegisterRequest; import poomasi.domain.store.dto.StoreResponse; import poomasi.domain.store.entity.Store; @@ -37,6 +38,9 @@ class StoreServiceTest { @Mock private SecurityContext securityContext; + @Mock + private MemberService memberService; + @Mock private Authentication authentication; @@ -45,23 +49,22 @@ class StoreServiceTest { @BeforeEach void setUp() { // 테스트 멤버와 Authentication 설정 - testMember = Member.builder().build(); - - SecurityContextHolder.setContext(securityContext); - when(securityContext.getAuthentication()).thenReturn(authentication); - when(authentication.getPrincipal()).thenReturn(new UserDetailsImpl(testMember)); + testMember = Member.builder() + .id(1L) + .build(); } @Test void addStore_StoreAddedSuccessfully() { // given + StoreRegisterRequest request = mock(StoreRegisterRequest.class); Store store = mock(Store.class); when(request.toEntity(any(Member.class))).thenReturn(store); // when - storeService.addStore(request); + storeService.addStore(request,testMember); // then verify(storeRepository, times(1)).save(store); @@ -70,15 +73,16 @@ void addStore_StoreAddedSuccessfully() { @Test void getStore_StoreExists_ReturnStoreResponse() { // given - Store store = new Store(1L, "test","test","test",testMember,"test","test",100); - when(storeRepository.findByOwnerId(testMember.getId())).thenReturn(Optional.of(store)); + Store store = new Store(1L, "test","test","test",testMember,"test"); + testMember.setStore(store); + when(memberService.findMemberById(testMember.getId())).thenReturn(testMember); // when - StoreResponse response = storeService.getStore(); + StoreResponse response = storeService.getStore(testMember.getId()); // then assertNotNull(response); - verify(storeRepository, times(1)).findByOwnerId(testMember.getId()); + verify(memberService, times(1)).findMemberById(testMember.getId()); } @Test @@ -90,7 +94,7 @@ void updateStore_StoreExists_StoreUpdatedSuccessfully() { when(storeRepository.findByOwnerId(testMember.getId())).thenReturn(Optional.of(store)); // when - storeService.updateStore(request); + storeService.updateStore(request, testMember); // then verify(store, times(1)).updateStore(request); @@ -103,7 +107,7 @@ void updateStore_StoreDoesNotExist_ThrowsBusinessException() { when(storeRepository.findByOwnerId(testMember.getId())).thenReturn(Optional.empty()); // when & then - BusinessException exception = assertThrows(BusinessException.class, () -> storeService.updateStore(request)); + BusinessException exception = assertThrows(BusinessException.class, () -> storeService.updateStore(request, testMember)); assertEquals(BusinessError.STORE_NOT_FOUND, exception.getBusinessError()); } diff --git a/src/test/java/poomasi/global/config/s3/S3PresignedUrlServiceTest.java b/src/test/java/poomasi/global/config/s3/S3PresignedUrlServiceTest.java index aa41f765..11412726 100644 --- a/src/test/java/poomasi/global/config/s3/S3PresignedUrlServiceTest.java +++ b/src/test/java/poomasi/global/config/s3/S3PresignedUrlServiceTest.java @@ -1,76 +1,100 @@ package poomasi.global.config.s3; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.*; + import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import poomasi.global.config.aws.AwsProperties; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.junit.jupiter.api.extension.ExtendWith; +import poomasi.global.config.s3.dto.response.PresignedPutUrlResponse; import poomasi.global.util.EncryptionUtil; -import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; -import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; -import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.http.SdkHttpMethod; +import software.amazon.awssdk.http.SdkHttpRequest; import software.amazon.awssdk.services.s3.presigner.S3Presigner; +import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest; +import software.amazon.awssdk.services.s3.presigner.model.PresignedGetObjectRequest; +import software.amazon.awssdk.services.s3.presigner.model.PresignedPutObjectRequest; +import software.amazon.awssdk.services.s3.presigner.model.PutObjectPresignRequest; +import java.net.MalformedURLException; +import java.net.URI; import java.util.HashMap; import java.util.Map; -import static org.junit.jupiter.api.Assertions.*; +@ExtendWith(MockitoExtension.class) +class S3PresignedUrlServiceTest { + + @Mock + private S3Presigner s3Presigner; + + @Mock + private EncryptionUtil encryptionUtil; -@SpringBootTest -public class S3PresignedUrlServiceTest { + @InjectMocks private S3PresignedUrlService s3PresignedUrlService; - @Autowired - private AwsProperties awsProperties; - -// @BeforeEach -// public void setUp() { -// String accessKey = awsProperties.getAccess(); -// String secretKey = awsProperties.getSecret(); -// String region = awsProperties.getS3().getRegion(); -// -// // 자격 증명 설정 -// AwsBasicCredentials awsCreds = AwsBasicCredentials.create( -// accessKey, -// secretKey -// ); -// -// // S3Presigner 인스턴스 생성 -// S3Presigner presigner = S3Presigner.builder() -// .credentialsProvider(StaticCredentialsProvider.create(awsCreds)) -// .region(Region.of(region)) -// .build(); -// -// // S3PresignedUrlService 초기화 -// s3PresignedUrlService = new S3PresignedUrlService(presigner, new EncryptionUtil()); -// } -// -// @Test -// public void testCreatePresignedGetUrl() { -// String objectKey = "object_key"; -// String bucketName = awsProperties.getS3().getBucket(); -// -// String presignedUrl = s3PresignedUrlService.createPresignedGetUrl(bucketName, objectKey); -// -// assertNotNull(presignedUrl); -// System.out.println("Presigned GET URL: " + presignedUrl); -// } -// -// @Test -// public void testCreatePresignedPutUrl() { -// String keyPrefix = "uploads"; -// String bucketName = awsProperties.getS3().getBucket(); -// -// // 메타데이터 생성 -// Map metadata = new HashMap<>(); -// metadata.put("Content-Type", "image/jpg"); -// metadata.put("x-amz-meta-title", "Test Image"); -// -// // presigned PUT URL 생성 -// String presignedUrl = s3PresignedUrlService.createPresignedPutUrl(bucketName, keyPrefix, metadata); -// -// assertNotNull(presignedUrl); -// System.out.println("Presigned PUT URL: " + presignedUrl); -// } + private static final String BUCKET_NAME = "test-bucket"; + private static final String REGION = "us-west-2"; + private static final String KEY_PREFIX = "test-prefix"; + private static final String KEY_NAME = "test-key"; + + @Test + @DisplayName("Presigned Put URL 생성 성공 테스트") + void createPresignedPutUrl_Success() throws MalformedURLException { + // Given + Map metadata = new HashMap<>(); + metadata.put("key1", "value1"); + + given(encryptionUtil.encodeTime(any())).willReturn("encodedTimeString"); + + PresignedPutObjectRequest mockPresignedRequest = mock(PresignedPutObjectRequest.class); + SdkHttpRequest mockHttpRequest = mock(SdkHttpRequest.class); + + // Mock httpRequest method() to return PUT + when(mockPresignedRequest.url()).thenReturn(URI.create("https://test-bucket.s3.us-west-2.amazonaws.com/test-key").toURL()); + when(mockPresignedRequest.httpRequest()).thenReturn(mockHttpRequest); + when(mockHttpRequest.method()).thenReturn(SdkHttpMethod.PUT); + + given(s3Presigner.presignPutObject(any(PutObjectPresignRequest.class))) + .willReturn(mockPresignedRequest); + + // When + PresignedPutUrlResponse response = s3PresignedUrlService.createPresignedPutUrl(BUCKET_NAME, REGION, KEY_PREFIX, metadata); + + // Then + assertEquals("https://test-bucket.s3.us-west-2.amazonaws.com/test-key", response.presignedPutUrl()); + assertEquals("https://test-bucket.s3.us-west-2.amazonaws.com/" + response.keyName(), response.objectUrl()); + + verify(s3Presigner).presignPutObject(any(PutObjectPresignRequest.class)); + } + + @Test + @DisplayName("Presigned Get URL 생성 성공 테스트") + void createPresignedGetUrl_Success() throws MalformedURLException { + // Given + PresignedGetObjectRequest mockPresignedRequest = mock(PresignedGetObjectRequest.class); + SdkHttpRequest mockHttpRequest = mock(SdkHttpRequest.class); + + // Mock httpRequest method() to return GET + when(mockPresignedRequest.url()).thenReturn(URI.create("https://test-bucket.s3.us-west-2.amazonaws.com/test-key").toURL()); + when(mockPresignedRequest.httpRequest()).thenReturn(mockHttpRequest); + when(mockHttpRequest.method()).thenReturn(SdkHttpMethod.GET); + + given(s3Presigner.presignGetObject(any(GetObjectPresignRequest.class))) + .willReturn(mockPresignedRequest); + + // When + String presignedUrl = s3PresignedUrlService.createPresignedGetUrl(BUCKET_NAME, KEY_NAME); + + // Then + assertEquals("https://test-bucket.s3.us-west-2.amazonaws.com/test-key", presignedUrl); + + verify(s3Presigner).presignGetObject(any(GetObjectPresignRequest.class)); + } } diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml index 51b36f18..53f3ac19 100644 --- a/src/test/resources/application-test.yml +++ b/src/test/resources/application-test.yml @@ -12,5 +12,4 @@ spring: imp: api: key: ${IMP_API_KEY} - secretKey: ${IMP_SECRET_KEY} - + secretKey: ${IMP_SECRET_KEY} \ No newline at end of file