diff --git a/build.gradle b/build.gradle index 86ba4c57..755c5a68 100644 --- a/build.gradle +++ b/build.gradle @@ -15,6 +15,9 @@ java { repositories { mavenCentral() + maven { + url 'https://jitpack.io' + } } dependencies { @@ -34,8 +37,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-actuator' - -// H2 Database + // H2 Database runtimeOnly 'com.h2database:h2' // JSON @@ -62,9 +64,13 @@ dependencies { implementation(platform("software.amazon.awssdk:bom:2.27.21")) implementation("software.amazon.awssdk:s3") + // 아임포트 및 아임포트 웹훅 + implementation 'com.github.iamport:iamport-rest-client-java:0.2.23' + } tasks.named('test') { useJUnitPlatform() } + diff --git a/src/main/java/poomasi/domain/auth/config/SecurityConfig.java b/src/main/java/poomasi/domain/auth/config/SecurityConfig.java index 4bef2180..7c67b99b 100644 --- a/src/main/java/poomasi/domain/auth/config/SecurityConfig.java +++ b/src/main/java/poomasi/domain/auth/config/SecurityConfig.java @@ -71,26 +71,29 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti //기본 로그아웃 해제 http.logout(AbstractHttpConfigurer::disable); - /* + // 기본 경로 및 테스트 경로 http.authorizeHttpRequests((authorize) -> authorize .requestMatchers(HttpMethod.GET, "/api/farm/**").permitAll() .requestMatchers(HttpMethod.GET, "/api/product/**").permitAll() .requestMatchers(HttpMethod.GET, "/api/review/**").permitAll() - .requestMatchers("/api/sign-up", "/api/login", "api/reissue").permitAll() + .requestMatchers("/api/sign-up", "/api/login", "api/reissue", "api/payment/**", "api/order/**").permitAll() .requestMatchers("/api/need-auth/**").authenticated() .anyRequest(). authenticated() - );*/ - + ); + /* http.authorizeHttpRequests((authorize) -> authorize .requestMatchers("/**").permitAll() - .requestMatchers("/api/auth-test/**", "/api/cart/**").authenticated() + .requestMatchers("/api/auth-test/**", + "/api/cart/**", + "/api/order/**", + "/api/payment/**").authenticated() .anyRequest() .authenticated() ); - + */ /* 로그아웃 필터 등록하기 LogoutHandler[] handlers = { @@ -114,14 +117,14 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti oauth2 인증은 현재 해제해놨습니다 -> 차후 code를 front에서 어떤 경로로 받을 것인지 아니면 kakao에서 바로 redirect를 백엔드로 할 지 정해지면 processing url 작성하겠습니다 - + */ http .oauth2Login((oauth2) -> oauth2 .userInfoEndpoint((userInfoEndpointConfig) -> userInfoEndpointConfig .userService(oAuth2UserDetailServiceImpl)) .successHandler(customSuccessHandler) ); - */ + http.oauth2Login(AbstractHttpConfigurer::disable); CustomUsernamePasswordAuthenticationFilter customUsernameFilter = diff --git a/src/main/java/poomasi/domain/farm/entity/Farm.java b/src/main/java/poomasi/domain/farm/entity/Farm.java index 62335214..9fd76ef9 100644 --- a/src/main/java/poomasi/domain/farm/entity/Farm.java +++ b/src/main/java/poomasi/domain/farm/entity/Farm.java @@ -2,6 +2,8 @@ import jakarta.annotation.Nullable; import jakarta.persistence.*; +import java.util.ArrayList; +import java.util.List; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; @@ -13,6 +15,7 @@ import poomasi.domain.farm.dto.FarmUpdateRequest; import java.time.LocalDateTime; +import poomasi.domain.review.entity.Review; @Entity @Getter @@ -64,6 +67,10 @@ public class Farm { @UpdateTimestamp private LocalDateTime updatedAt = LocalDateTime.now(); + @OneToMany(cascade = CascadeType.REMOVE, orphanRemoval = true) + @JoinColumn(name = "entityId") + List reviewList = new ArrayList<>(); + @Builder public Farm(String name, Long ownerId, String address, String addressDetail, Double latitude, Double longitude, String description, Long experiencePrice) { this.name = name; diff --git a/src/main/java/poomasi/domain/member/entity/Member.java b/src/main/java/poomasi/domain/member/entity/Member.java index 3ca82998..318f3cb2 100644 --- a/src/main/java/poomasi/domain/member/entity/Member.java +++ b/src/main/java/poomasi/domain/member/entity/Member.java @@ -6,6 +6,7 @@ import lombok.NoArgsConstructor; import lombok.Setter; import org.hibernate.annotations.SQLDelete; +import poomasi.domain.order.entity.Order; import poomasi.domain.wishlist.entity.WishList; import java.time.LocalDateTime; @@ -49,6 +50,13 @@ public class Member { private LocalDateTime deletedAt; + @OneToMany(mappedBy = "member", cascade = CascadeType.ALL, orphanRemoval = true) + private List orderLists; + + @Setter + @Column(nullable = true) + private String farmerTierCode; + public Member(String email, String password, LoginType loginType, Role role) { this.email = email; this.password = password; diff --git a/src/main/java/poomasi/domain/order/_payment/config/IamportConfig.java b/src/main/java/poomasi/domain/order/_payment/config/IamportConfig.java new file mode 100644 index 00000000..8f5dd755 --- /dev/null +++ b/src/main/java/poomasi/domain/order/_payment/config/IamportConfig.java @@ -0,0 +1,23 @@ +package poomasi.domain.order._payment.config; + + +import com.siot.IamportRestClient.IamportClient; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class IamportConfig { + + @Value("${IMP_API_KEY}") + private String apiKey; + + @Value("${imp.api.secretKey}") + private String secretKey; + + @Bean + public IamportClient iamportClient() { + return new IamportClient(apiKey, secretKey); + } + +} diff --git a/src/main/java/poomasi/domain/order/_payment/controller/PaymentController.java b/src/main/java/poomasi/domain/order/_payment/controller/PaymentController.java new file mode 100644 index 00000000..84acdd44 --- /dev/null +++ b/src/main/java/poomasi/domain/order/_payment/controller/PaymentController.java @@ -0,0 +1,59 @@ +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.dto.response.PaymentResponse; +import poomasi.domain.order._payment.service.PaymentService; + +import java.io.IOException; + +@RestController +@RequestMapping("/api/payment") +@RequiredArgsConstructor +public class PaymentController { + + private final PaymentService paymentService; + + @Description("사전 결제 api") + @Secured({"ROLE_CUSTOMER", "ROLE_FARMER"}) + @PostMapping("/pre-payment") + public void postPrepare(PaymentPreRegisterRequest paymentPreRegisterRequest) throws IamportResponseException, IOException { + paymentService.portonePrePaymentRegister(paymentPreRegisterRequest); + } + + @Description("사후 결제(검증 api)") + @PostMapping("/validate") + public void validatePayment(PaymentWebHookRequest paymentWebHookRequest) throws IamportResponseException, IOException { + paymentService.portoneVerifyPostPayment(paymentWebHookRequest); + } + + /* + *@Description("포트원 webhook + 동기화") + * */ + + + + @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); + } + + +} 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 new file mode 100644 index 00000000..c2df4c06 --- /dev/null +++ b/src/main/java/poomasi/domain/order/_payment/dto/request/PaymentPreRegisterRequest.java @@ -0,0 +1,7 @@ +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/request/PaymentWebHookRequest.java b/src/main/java/poomasi/domain/order/_payment/dto/request/PaymentWebHookRequest.java new file mode 100644 index 00000000..a735aa9b --- /dev/null +++ b/src/main/java/poomasi/domain/order/_payment/dto/request/PaymentWebHookRequest.java @@ -0,0 +1,5 @@ +package poomasi.domain.order._payment.dto.request; + +public record PaymentWebHookRequest(String imp_uid, + String merchant_uid) { +} diff --git a/src/main/java/poomasi/domain/order/_payment/dto/response/PaymentPreRegisterResponse.java b/src/main/java/poomasi/domain/order/_payment/dto/response/PaymentPreRegisterResponse.java new file mode 100644 index 00000000..639ed46c --- /dev/null +++ b/src/main/java/poomasi/domain/order/_payment/dto/response/PaymentPreRegisterResponse.java @@ -0,0 +1,8 @@ +package poomasi.domain.order._payment.dto.response; + +public record PaymentPreRegisterResponse(String merchantUid) { + + public static PaymentPreRegisterResponse from(String merchantUid){ + return new PaymentPreRegisterResponse(merchantUid); + } +} 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 new file mode 100644 index 00000000..9f6d0fa6 --- /dev/null +++ b/src/main/java/poomasi/domain/order/_payment/dto/response/PaymentResponse.java @@ -0,0 +1,25 @@ +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, + String merchantUid, + BigDecimal totalPrice, + BigDecimal discountPrice, + BigDecimal finalPrice, + PaymentMethod paymentMethod +) { + public static PaymentResponse fromEntity(Payment payment){ + return new PaymentResponse( + payment.getId(), + payment.getMerchantUid(), + 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 new file mode 100644 index 00000000..321faa88 --- /dev/null +++ b/src/main/java/poomasi/domain/order/_payment/entity/Payment.java @@ -0,0 +1,39 @@ +package poomasi.domain.order._payment.entity; + + +import jakarta.persistence.*; +import jdk.jfr.Description; +import lombok.Getter; +import poomasi.domain.order.entity.Order; + +import java.math.BigDecimal; + +@Entity +@Getter +public class Payment { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Description("상품 총 가격") + private BigDecimal totalPrice; + + @Description("할인 가격") + private BigDecimal discountPrice; + + @Description("최종 가격") + private BigDecimal finalPrice; + + @Description("결제 방식") + @Enumerated(EnumType.STRING) + private PaymentMethod paymentMethod; + + @OneToOne + private Order order; + + @Description("포트원에서 결제 식별을 위한 merchant_uid") + @Column(name = "merchant_uid" , updatable = false) + private String merchantUid; + +} diff --git a/src/main/java/poomasi/domain/order/_payment/entity/PaymentMethod.java b/src/main/java/poomasi/domain/order/_payment/entity/PaymentMethod.java new file mode 100644 index 00000000..b7611e86 --- /dev/null +++ b/src/main/java/poomasi/domain/order/_payment/entity/PaymentMethod.java @@ -0,0 +1,7 @@ +package poomasi.domain.order._payment.entity; + +public enum PaymentMethod { + KAKAO_PAY, + TOSS_PAYMENTS + ; +} diff --git a/src/main/java/poomasi/domain/order/_payment/entity/PaymentState.java b/src/main/java/poomasi/domain/order/_payment/entity/PaymentState.java new file mode 100644 index 00000000..77d04d38 --- /dev/null +++ b/src/main/java/poomasi/domain/order/_payment/entity/PaymentState.java @@ -0,0 +1,31 @@ +package poomasi.domain.order._payment.entity; + +public enum PaymentState { + PENDING, // 결제 대기 중 + COMPLETED, // 결제 완료 + FAILED, // 결제 실패 + CANCELLED, // 결제 취소됨 + REFUNDED, // 환불 완료 + DECLINED; // 결제 거부됨 + + @Override + public String toString() { + // 사용자 친화적인 문자열로 반환할 수 있도록 오버라이딩 + switch (this) { + case PENDING: + return "Payment Pending"; + case COMPLETED: + return "Payment Completed"; + case FAILED: + return "Payment Failed"; + case CANCELLED: + return "Payment Cancelled"; + case REFUNDED: + return "Payment Refunded"; + case DECLINED: + return "Payment Declined"; + default: + return super.toString(); + } + } +} diff --git a/src/main/java/poomasi/domain/order/_payment/repository/PaymentRepository.java b/src/main/java/poomasi/domain/order/_payment/repository/PaymentRepository.java new file mode 100644 index 00000000..e98964ba --- /dev/null +++ b/src/main/java/poomasi/domain/order/_payment/repository/PaymentRepository.java @@ -0,0 +1,13 @@ +package poomasi.domain.order._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; + +@Repository +public interface PaymentRepository extends JpaRepository { +// public Long countByImpuidContainsIgnoreCase(String impuid); + //List findByOrderId(Long orderId); +} diff --git a/src/main/java/poomasi/domain/order/_payment/service/PaymentService.java b/src/main/java/poomasi/domain/order/_payment/service/PaymentService.java new file mode 100644 index 00000000..e5d33028 --- /dev/null +++ b/src/main/java/poomasi/domain/order/_payment/service/PaymentService.java @@ -0,0 +1,149 @@ +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.request.PrepareData; +import com.siot.IamportRestClient.response.AccessToken; +import com.siot.IamportRestClient.response.IamportResponse; +import com.siot.IamportRestClient.response.Prepare; +import jdk.jfr.Description; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +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._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.entity.Order; +import poomasi.domain.order.repository.OrderRepository; +import poomasi.domain.product._cart.service.CartService; +import poomasi.global.error.BusinessError; +import poomasi.global.error.BusinessException; + +import java.io.IOException; +import java.math.BigDecimal; +import java.util.List; + +import static poomasi.domain.order.entity.OrderStatus.AWAITING_SELLER_CONFIRMATION; +import static poomasi.domain.order.entity.OrderStatus.PENDING; +import static poomasi.global.error.BusinessError.*; + +@Service +@RequiredArgsConstructor +@Slf4j +public class PaymentService { + + @Autowired + private final PaymentRepository paymentRepository; + private final IamportClient iamportClient; + private final OrderRepository orderRepository; + private final CartService cartService; + + @Description("포트원 api 호출을 위한 accessToken 발급 메서드") + private String getPortOneAccessToken() throws IOException, IamportResponseException { + IamportResponse authResponse = iamportClient.getAuth(); + String accessToken = authResponse.getResponse().getToken(); + return accessToken; + } + + @Description("사전 결제 등록") + public PaymentPreRegisterResponse portonePrePaymentRegister(PaymentPreRegisterRequest paymentPreRegisterRequest) throws IOException, IamportResponseException { + PrepareData prepareData = new PrepareData(paymentPreRegisterRequest.merchantUid(), + paymentPreRegisterRequest.amount() + ); + iamportClient.postPrepare(prepareData); + return PaymentPreRegisterResponse.from( + paymentPreRegisterRequest.merchantUid() + ); + } + + @Transactional + @Description("프론트에서 받아온 결과를 validate하는 메서드") + public void portoneVerifyPostPayment(PaymentWebHookRequest paymentWebHookRequest) throws IOException, IamportResponseException { + String impUid = paymentWebHookRequest.imp_uid(); + String merchantUid = paymentWebHookRequest.merchant_uid(); + IamportResponse iamportResponse = iamportClient.paymentByImpUid(impUid); + BigDecimal amount = iamportResponse.getResponse() + .getAmount(); + + Order order = orderRepository.findByMerchantUid(merchantUid) + .orElseThrow(() -> new BusinessException(ORDER_NOT_FOUND)); + + if(order.getOrderStatus()!=PENDING){ //이미 처리한 주문이라면 + throw new BusinessException(ORDER_ALREADY_PROCESSED); + } + + if(validatePaymentConsistency(order.getTotalAmount(), amount)){ //결제 금액이 맞지 않다면 -> 주문 취소 api 호출 + cancelPayment(iamportResponse); + throw new BusinessException(PAYMENT_AMOUNT_MISMATCH); + } + order.setOrderStatus(AWAITING_SELLER_CONFIRMATION); // 상태 변경 + cartService.removeSelected(); //장바구니 삭제 + } + + private boolean validatePaymentConsistency(BigDecimal prepaymentAmount, BigDecimal postPaymentAmount){ + if (prepaymentAmount.compareTo(postPaymentAmount) != 0) { + return false; + } + return true; + } + + @Description("payment 상세 내역 조회를 위한 단건 api 호출") + public void getPaymentDetails(String merchantUid, Long orderId) throws IOException, IamportResponseException { + + } + + @Description("결제 취소 api") + public void cancelPayment(IamportResponse response) throws IOException, IamportResponseException{ + //true면 Uid, false면 merchantUid로 판단 + CancelData cancelData = new CancelData(response.getResponse().getMerchantUid(), false); + iamportClient.cancelPaymentByImpUid(cancelData); + } + + @Description("결제 환불 api") + public void processRefund() throws IOException, IamportResponseException{ + + } + + + @Description("결제 부분 환불 api 호출") + public void partialRefund() throws IOException, IamportResponseException { + + } + + 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/_refund/entity/Refund.java b/src/main/java/poomasi/domain/order/_refund/entity/Refund.java new file mode 100644 index 00000000..8b51294d --- /dev/null +++ b/src/main/java/poomasi/domain/order/_refund/entity/Refund.java @@ -0,0 +1,20 @@ +package poomasi.domain.order._refund.entity; + + +import jakarta.persistence.*; +import poomasi.domain.order.entity.OrderProductDetails; + +@Entity +@Table(name="refund") +public class Refund { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + +/* @OneToOne(fetch = FetchType.LAZY) + private OrderProductDetails orderProductDetails;*/ + + private String refundReason; + +} diff --git a/src/main/java/poomasi/domain/order/_refund/entity/RefundStatus.java b/src/main/java/poomasi/domain/order/_refund/entity/RefundStatus.java new file mode 100644 index 00000000..69a08f7c --- /dev/null +++ b/src/main/java/poomasi/domain/order/_refund/entity/RefundStatus.java @@ -0,0 +1,29 @@ +package poomasi.domain.order._refund.entity; + +public enum RefundStatus { + REQUESTED("환불 요청됨"), + PROCESSING("환불 처리 중"), + COMPLETED("환불 완료됨"), + REJECTED("환불 거절됨"); + + private final String description; + + RefundStatus(String description) { + this.description = description; + } + + public String getDescription() { + return description; + } +} + +/* +* REQUESTED, // 반품 요청됨 + APPROVED, // 반품 승인됨 + REJECTED, // 반품 거부됨 + RETURNED, // 상품이 반품됨 + REFUNDED, // 환불 완료됨 + CANCELLED, // 반품 요청이 취소됨 + IN_TRANSIT // 반품이 배송 중 + ; +* */ diff --git a/src/main/java/poomasi/domain/order/controller/OrderController.java b/src/main/java/poomasi/domain/order/controller/OrderController.java new file mode 100644 index 00000000..8a8c7a81 --- /dev/null +++ b/src/main/java/poomasi/domain/order/controller/OrderController.java @@ -0,0 +1,83 @@ +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.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; +import poomasi.domain.auth.security.userdetail.UserDetailsImpl; +import poomasi.domain.order._payment.dto.request.PaymentPreRegisterRequest; +import poomasi.domain.order._payment.service.PaymentService; +import poomasi.domain.order.service.OrderService; + +import java.io.IOException; + + +@Slf4j +@RestController +@RequestMapping("api/order") +@RequiredArgsConstructor +public class OrderController { + + private final OrderService orderService; + private final PaymentService paymentService; + + @Secured({"ROLE_CUSTOMER", "ROLE_FARMER"}) + @PostMapping("/pre-order") + public ResponseEntity createPreOrder(@AuthenticationPrincipal UserDetailsImpl user) throws IOException, IamportResponseException { + PaymentPreRegisterRequest paymentPreRegisterRequest = orderService.preOrderRegister(); + return ResponseEntity.ok( + paymentService.portonePrePaymentRegister(paymentPreRegisterRequest) + ); + } + + @Description("멤버의 결제 완료가 된 단건 주문 조회") + @GetMapping("/{orderId}") + public ResponseEntity getAllOrdersByMember(@PathVariable Long orderId) { + return ResponseEntity.ok( + orderService.findOrderByMemberId(orderId) + ); + } + + @Description("멤버의 결제 완료가 된 전체 주문 목록 조회") + @GetMapping("/") + public ResponseEntity getOrdersByMember() { + return ResponseEntity.ok( + orderService.findAllOrdersByMemberId() + ); + } + + @Description("어떤 주문 대한 디테일 조회. ex) 주소, 상세주소, 배송 요청 사항 등") + @GetMapping("/{orderId}/details") + public ResponseEntity getOrderDetailsByMember(@PathVariable Long orderId) { + return ResponseEntity.ok( + orderService.findOrderDetailsByOrderId(orderId) + ); + } + + + @Description("어떤 주문에 대한 모든 품목 디테일 조회.ex) 품목 가격, 이름, 등등..") + @GetMapping("/{orderId}/product/details") + public ResponseEntity getOrderProductDetailsByOrderId(@PathVariable Long orderId) { + return ResponseEntity.ok( + orderService.findAllOrderProductDetails(orderId) + ); + } + + @Description("어떤 주문에 대한 품목의 디테일 단건 조회. ex) 품목 가격, 이름, 등등..") + @GetMapping("/{orderId}/product/details/{detailsId}") + public ResponseEntity getOrderProductDetailsByDetailsId(@PathVariable Long orderId, @PathVariable Long detailsId) { + return ResponseEntity.ok( + orderService.findOrderProductDetailsById(orderId, detailsId) + ); + } + +} + + + + diff --git a/src/main/java/poomasi/domain/order/dto/response/OrderDetailsResponse.java b/src/main/java/poomasi/domain/order/dto/response/OrderDetailsResponse.java new file mode 100644 index 00000000..c93a0358 --- /dev/null +++ b/src/main/java/poomasi/domain/order/dto/response/OrderDetailsResponse.java @@ -0,0 +1,17 @@ +package poomasi.domain.order.dto.response; + +import poomasi.domain.order.entity.OrderDetails; + +public record OrderDetailsResponse( + String address, + String addressDetails, + String deliveryRequest +) { + public static OrderDetailsResponse fromEntity(OrderDetails orderDetails) { + return new OrderDetailsResponse( + orderDetails.getAddress(), + orderDetails.getAddressDetail(), + orderDetails.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 new file mode 100644 index 00000000..9e2ae945 --- /dev/null +++ b/src/main/java/poomasi/domain/order/dto/response/OrderProductDetailsResponse.java @@ -0,0 +1,29 @@ +package poomasi.domain.order.dto.response; + +import poomasi.domain.order.entity.OrderProductDetails; +import poomasi.domain.product.dto.ProductResponse; + +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(OrderProductDetails orderProductDetails) { + return new OrderProductDetailsResponse( + orderProductDetails.getOrder().getId(), + orderProductDetails.getId(), + orderProductDetails.getProduct().getId(), + orderProductDetails.getProductName(), + orderProductDetails.getCount(), + orderProductDetails.getPrice(), + orderProductDetails.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 new file mode 100644 index 00000000..77f8a793 --- /dev/null +++ b/src/main/java/poomasi/domain/order/dto/response/OrderResponse.java @@ -0,0 +1,26 @@ +package poomasi.domain.order.dto.response; + +import poomasi.domain.order._payment.dto.response.PaymentResponse; +import poomasi.domain.order.entity.Order; +import poomasi.domain.order.entity.OrderStatus; + +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(Order order) { + return new OrderResponse( + order.getId(), + order.getMerchantUid(), + order.getCreatedAt(), + order.getOrderProductDetails() + .stream() + .map(OrderProductDetailsResponse::fromEntity) + .collect(Collectors.toList()) + ); + } +} diff --git a/src/main/java/poomasi/domain/order/entity/AbstractOrder.java b/src/main/java/poomasi/domain/order/entity/AbstractOrder.java new file mode 100644 index 00000000..82a7c4ff --- /dev/null +++ b/src/main/java/poomasi/domain/order/entity/AbstractOrder.java @@ -0,0 +1,33 @@ +package poomasi.domain.order.entity; + +import jakarta.persistence.*; +import jdk.jfr.Description; +import jdk.jfr.Timestamp; +import lombok.Getter; +import org.hibernate.annotations.UpdateTimestamp; +import poomasi.domain.member.entity.Member; + +import java.time.LocalDateTime; +import java.util.Date; + +@MappedSuperclass +@Getter +public abstract class AbstractOrder { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @JoinColumn(name = "member_id") + @ManyToOne(fetch = FetchType.LAZY) + private Member member; + + @Column(name = "merchant_uid") + @Description("상품당 결제 id(아임포트 id)") + private String merchantUid = "p" + new Date().getTime(); + + @Column(name = "created_at") + @Timestamp + private LocalDateTime createdAt = LocalDateTime.now(); + +} 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..a830bed2 --- /dev/null +++ b/src/main/java/poomasi/domain/order/entity/Order.java @@ -0,0 +1,49 @@ +package poomasi.domain.order.entity; + + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; +import java.util.List; + +import static poomasi.domain.order.entity.OrderStatus.PENDING; + +@Entity +@Table(name = "orders") +@Getter +@NoArgsConstructor +public class Order extends AbstractOrder{ + + @Column(name = "order_product_details_id") + @OneToMany(mappedBy = "order", cascade = CascadeType.ALL) + private List orderProductDetails; + + @OneToOne + @JoinColumn(name = "order_details_id") // 여기서 JoinColumn 사용 + private OrderDetails orderDetails; + + @Column(name = "total_amount") + private BigDecimal totalAmount; + + @Enumerated(EnumType.STRING) + private OrderStatus orderStatus = OrderStatus.PENDING; + + public Order(OrderDetails orderDetails) { + this.orderDetails = orderDetails; + } + + public void addOrderDetail(OrderProductDetails orderProductDetails) { + this.orderProductDetails.add(orderProductDetails); + } + public void setOrderStatus(OrderStatus orderStatus) { + this.orderStatus = orderStatus; + } + + public void setTotalAmount(BigDecimal totalAmount) { + this.totalAmount = totalAmount; + } + + +} diff --git a/src/main/java/poomasi/domain/order/entity/OrderDetails.java b/src/main/java/poomasi/domain/order/entity/OrderDetails.java new file mode 100644 index 00000000..5bb7ed09 --- /dev/null +++ b/src/main/java/poomasi/domain/order/entity/OrderDetails.java @@ -0,0 +1,38 @@ +package poomasi.domain.order.entity; + +import jakarta.persistence.*; +import jdk.jfr.Description; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table(name="order_details") +@Getter +@NoArgsConstructor +public class OrderDetails { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @OneToOne(mappedBy = "orderDetails") + private Order order; + + @Column(name = "address") + private String address; + + @Column(name = "address_detail") + private String addressDetail; + + @Description("배송 요청 사항") + @Column(name = "delivery_request", length = 255) + private String deliveryRequest; + + public OrderDetails(String address, String addressDetail, String deliveryRequest) { + this.address = address; + this.addressDetail = addressDetail; + this.deliveryRequest = deliveryRequest; + } + + +} diff --git a/src/main/java/poomasi/domain/order/entity/OrderProductDetails.java b/src/main/java/poomasi/domain/order/entity/OrderProductDetails.java new file mode 100644 index 00000000..5be2e668 --- /dev/null +++ b/src/main/java/poomasi/domain/order/entity/OrderProductDetails.java @@ -0,0 +1,71 @@ +package poomasi.domain.order.entity; + + +import jakarta.persistence.*; +import jdk.jfr.Description; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import poomasi.domain.order._refund.entity.Refund; +import poomasi.domain.product.entity.Product; + +import java.io.Serializable; +import java.math.BigDecimal; + +@Entity +@Table(name = "order_product_details") +@Getter +@NoArgsConstructor +public class OrderProductDetails implements Serializable { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "order_product_details_id") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "product_id") + private Product product; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "order_id") + private Order order; + + /* @OneToOne(fetch = FetchType.LAZY) + private Refund refund;*/ + + @Column(name = "product_description", nullable = true) + private String productDescription; + + @Column(name = "product_name", length = 255) + private String productName; + + @Description("구매 당시 1개당 가격") + private BigDecimal price; + + @Column(name="count") + private Integer count; + + @Description("송장 번호") + @Column(name = "invoice_number") + private String invoiceNumber; + + //private String sellerName; + //private OneToMany Review; + //refund.. + + @Builder + public OrderProductDetails(Product product, Order order, String productDescription, String productName, BigDecimal price, Integer count) { + this.product = product; + this.order = order; + this.productDescription = productDescription; + this.productName = productName; + this.price = price; + this.count = count; + } + + public void setInvoiceNumber(String invoiceNumber) { + this.invoiceNumber = invoiceNumber; + } +} + diff --git a/src/main/java/poomasi/domain/order/entity/OrderStatus.java b/src/main/java/poomasi/domain/order/entity/OrderStatus.java new file mode 100644 index 00000000..6a3b07a8 --- /dev/null +++ b/src/main/java/poomasi/domain/order/entity/OrderStatus.java @@ -0,0 +1,11 @@ +package poomasi.domain.order.entity; + +public enum OrderStatus { + PENDING, // 결제 대기 중 + AWAITING_SELLER_CONFIRMATION, // 판매자 확인 대기 중 + READY_FOR_SHIPMENT, // 배송 대기 중 + IN_TRANSIT, // 배송 중 + DELIVERED, // 배송 완료 + ORDER_COMPLETE // 주문 완료 + ; +} \ No newline at end of file diff --git a/src/main/java/poomasi/domain/order/repository/OrderProductDetailsRepository.java b/src/main/java/poomasi/domain/order/repository/OrderProductDetailsRepository.java new file mode 100644 index 00000000..61432033 --- /dev/null +++ b/src/main/java/poomasi/domain/order/repository/OrderProductDetailsRepository.java @@ -0,0 +1,10 @@ +package poomasi.domain.order.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import poomasi.domain.order.entity.OrderProductDetails; + +import java.util.List; + +public interface OrderProductDetailsRepository extends JpaRepository { + List findByOrderId(Long orderId); +} 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..f2476ad0 --- /dev/null +++ b/src/main/java/poomasi/domain/order/repository/OrderRepository.java @@ -0,0 +1,14 @@ +package poomasi.domain.order.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import poomasi.domain.order.entity.Order; +import poomasi.domain.order.entity.OrderProductDetails; + +import java.util.List; +import java.util.Optional; + +public interface OrderRepository extends JpaRepository { + List findByMemberId(Long memberId); + //List findById(Long id); + Optional findByMerchantUid(String merchantUid); +} diff --git a/src/main/java/poomasi/domain/order/service/OrderService.java b/src/main/java/poomasi/domain/order/service/OrderService.java new file mode 100644 index 00000000..24175672 --- /dev/null +++ b/src/main/java/poomasi/domain/order/service/OrderService.java @@ -0,0 +1,198 @@ +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._payment.repository.PaymentRepository; +import poomasi.domain.order._payment.service.PaymentService; +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.Order; +import poomasi.domain.order.entity.OrderDetails; +import poomasi.domain.order.entity.OrderProductDetails; +import poomasi.domain.order.entity.OrderStatus; +import poomasi.domain.order.repository.OrderProductDetailsRepository; +import poomasi.domain.order.repository.OrderRepository; +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.domain.order.entity.OrderStatus.AWAITING_SELLER_CONFIRMATION; +import static poomasi.global.error.BusinessError.*; + +@RequiredArgsConstructor +@Service +@Slf4j +public class OrderService { + + private final OrderRepository orderRepository; + private final CartRepository cartRepository; + private final ProductRepository productRepository; + private final PaymentRepository paymentRepository; + private final PaymentService paymentService; + private final OrderProductDetailsRepository orderProductDetailsRepository; + + @Transactional + public PaymentPreRegisterRequest preOrderRegister(){ + Member member = getMember(); + Long memberId = member.getId(); + List cartList = cartRepository.findByMemberIdAndSelected(memberId); + + //TODO : dto에서 address, address detail 꺼내와야 함. -> dto 확정이 안 나서 임시로 넣음 + String address = "금정구"; + String addressDetails = "수림로"; + String deliveryRequest = "조심히 다뤄 주세요"; + + Order order = new Order( + new OrderDetails(address, + addressDetails, + deliveryRequest) + ); + + //cart에 있는 총 가격 계산하기 + BigDecimal totalPrice = BigDecimal.ZERO; + + // cart 돌면서 order details 추가 + for (Cart cart : cartList) { + Long productId = cart.getProductId(); + Product product = productRepository.findById(productId) + .orElseThrow(() -> new BusinessException(PRODUCT_NOT_FOUND)); + String productDescription = product.getDescription(); + Integer count = cart.getCount(); + String productName = product.getName(); + BigDecimal price = BigDecimal.valueOf(product.getPrice()); + OrderProductDetails orderProductDetails = OrderProductDetails + .builder() + .product(product) + .order(order) + .productDescription(productDescription) + .productName(productName) + .price(price) + .count(count) + .build(); + order.addOrderDetail(orderProductDetails); + totalPrice = totalPrice.add(price); + } + order.setTotalAmount(totalPrice); + orderRepository.save(order); + + String merchantUid = order.getMerchantUid(); + return new PaymentPreRegisterRequest(merchantUid, totalPrice); + } + + @Description("멤버 ID 기반으로 모든 order 다 들고 오는 메서드") + public List findAllOrdersByMemberId(){ + Member member = getMember(); + Long memberId = member.getId(); + List orderList = orderRepository.findByMemberId(memberId); + return orderList + .stream() + .map(OrderResponse::fromEntity) + .collect(Collectors.toList() + ); + } + + @Description("멤버 id 기반으로 특정 orderId 들고오는 메서드") + public OrderResponse findOrderByMemberId(Long orderId){ + Member member = getMember(); + Order order = orderRepository.findById(orderId) + .orElseThrow(()-> new BusinessException(ORDER_NOT_FOUND)); + + validateOrderOwnership(order, member); + return OrderResponse.fromEntity(order); + } + + + @Description("orderId 기반으로 order details(주소, 상세주소, 배송 요청 사항 ..등) 들고오는 메서드") + public OrderDetailsResponse findOrderDetailsByOrderId(Long orderId){ + Order order = orderRepository.findById(orderId) + .orElseThrow(()-> new BusinessException(ORDER_NOT_FOUND)); + OrderDetails orderDetails = order.getOrderDetails(); + + return OrderDetailsResponse.fromEntity(orderDetails); + } + + @Description("orderId에 해당하는 order product details 가져오는 메서드") + public List findAllOrderProductDetails(Long orderId){ + Member member = getMember(); + Order order = orderRepository.findById(orderId) + .orElseThrow(()-> new BusinessException(ORDER_NOT_FOUND)); + validateOrderOwnership(order, member); + return order.getOrderProductDetails() + .stream() + .map(OrderProductDetailsResponse::fromEntity) + .collect(Collectors.toList() + ); + } + + + @Description("orderId에 해당하는 order product Details의 단건 조회") + public OrderProductDetailsResponse findOrderProductDetailsById(Long orderId, Long orderProductDetailsId){ + Member member = getMember(); + OrderProductDetails orderProductDetails = orderProductDetailsRepository.findById(orderProductDetailsId) + .orElseThrow(()-> new BusinessException(ORDER_PRODUCT_DETAILS_NOT_FOUND)); + Order order = orderProductDetails.getOrder(); + + // order product details의 주인 order 검사 그리고 , orderId의 주인 member 검사 + validateOrderProductDetailsByOrderId(order, orderId); + validateOrderOwnership(order, member); + + return OrderProductDetailsResponse.fromEntity(orderProductDetails); + } + + + @Description("member의 order인지 검사하는 메서드") + private void validateOrderOwnership(Order order, Member member) { + if (!order.getMember().getId().equals(member.getId())) { + throw new BusinessException(ORDER_NOT_OWNED_EXCEPTION); + } + } + + @Description("orderId에 해당하는 order Product Details인지 조회하는 메서드") + private void validateOrderProductDetailsByOrderId(Order order, Long orderId) { + if(order.getId()!=orderId){ + throw new BusinessException(ORDER_PRODUCT_DETAILS_NOT_OWNED_EXCEPTION); + } + } + + @Description("결제가 완료 된 후, 주문 상태 변경하는 메서드. 굳이 없어도 되긴 함.") + private void completePaymentAndUpdateStatus(Long orderId){ + Order order = orderRepository.findById(orderId) + .orElseThrow(()-> new BusinessException(ORDER_NOT_FOUND)); + order.setOrderStatus(AWAITING_SELLER_CONFIRMATION); + } + + @Description("주문 상태를 변경하는 메서드") + private void changeOrderStatus(Long orderId, OrderStatus orderStatus){ + Order order = orderRepository.findById(orderId) + .orElseThrow(()-> new BusinessException(ORDER_NOT_FOUND)); + order.setOrderStatus(orderStatus); + } + + + @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 new file mode 100644 index 00000000..fb8a187c --- /dev/null +++ b/src/main/java/poomasi/domain/product/_cart/controller/CartController.java @@ -0,0 +1,86 @@ +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.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 poomasi.domain.product._cart.dto.CartResponse; +import poomasi.domain.product._cart.service.CartService; + +@Controller +@RequiredArgsConstructor +public class CartController { + + private final CartService cartService; + + //장바구니 정보 + @GetMapping("/api/cart") + public ResponseEntity getCart() { + List cart = cartService.getCart(); + 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); + 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(); + 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); + 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 new file mode 100644 index 00000000..90189d7d --- /dev/null +++ b/src/main/java/poomasi/domain/product/_cart/dto/CartRegisterRequest.java @@ -0,0 +1,21 @@ +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 new file mode 100644 index 00000000..98148a41 --- /dev/null +++ b/src/main/java/poomasi/domain/product/_cart/dto/CartRequest.java @@ -0,0 +1,7 @@ +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 new file mode 100644 index 00000000..4710c67e --- /dev/null +++ b/src/main/java/poomasi/domain/product/_cart/dto/CartResponse.java @@ -0,0 +1,12 @@ +package poomasi.domain.product._cart.dto; + +public record CartResponse( + Long cartId, + String productName, + Long productPrice, + Integer productCount, + Boolean isSelected, + String farmName +) { + +} diff --git a/src/main/java/poomasi/domain/product/_cart/entity/Cart.java b/src/main/java/poomasi/domain/product/_cart/entity/Cart.java new file mode 100644 index 00000000..bf474106 --- /dev/null +++ b/src/main/java/poomasi/domain/product/_cart/entity/Cart.java @@ -0,0 +1,47 @@ +package poomasi.domain.product._cart.entity; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@NoArgsConstructor +@Getter +public class Cart { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private Boolean selected; + + private Long memberId; + + private Long productId; + + private Integer count; + + @Builder + public Cart(Long memberId, Long productId, Boolean selected, Integer count) { + this.memberId = memberId; + this.productId = productId; + this.selected = selected; + this.count = count; + } + + public void addCount() { + this.count += 1; + } + + 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 new file mode 100644 index 00000000..726c0b87 --- /dev/null +++ b/src/main/java/poomasi/domain/product/_cart/repository/CartRepository.java @@ -0,0 +1,42 @@ +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.product._cart.dto.CartResponse; +import poomasi.domain.product._cart.entity.Cart; + +@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 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); + + @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); +} diff --git a/src/main/java/poomasi/domain/product/_cart/service/CartService.java b/src/main/java/poomasi/domain/product/_cart/service/CartService.java new file mode 100644 index 00000000..ae9262f3 --- /dev/null +++ b/src/main/java/poomasi/domain/product/_cart/service/CartService.java @@ -0,0 +1,131 @@ +package poomasi.domain.product._cart.service; + +import java.util.List; +import java.util.Optional; +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.global.error.BusinessError; +import poomasi.global.error.BusinessException; + +@Service +@RequiredArgsConstructor +public class CartService { + + private final CartRepository cartRepository; + private final ProductRepository productRepository; + + @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(); + } + + 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()); + 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(); + } + + @Transactional + public void subCount(CartRequest cartRequest) { + Member member = getMember(); + Cart cart = getCartById(cartRequest.cartId()); + checkAuth(member, cart); + cart.subCount(); + } + + private 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() { + 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; + } + + @Transactional + public void changeSelect(CartRequest cartRequest) { + Member member = getMember(); + Cart cart = getCartById(cartRequest.cartId()); + checkAuth(member, cart); + cart.changeSelect(); + } + + public Integer getPrice() { + Member member = getMember(); + return cartRepository.getPrice(member.getId()); + } + + @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 46bd6e05..18f37077 100644 --- a/src/main/java/poomasi/domain/product/_category/controller/CategoryAdminController.java +++ b/src/main/java/poomasi/domain/product/_category/controller/CategoryAdminController.java @@ -3,12 +3,15 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; 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 poomasi.domain.auth.security.userdetail.UserDetailsImpl; import poomasi.domain.product._category.dto.CategoryRequest; import poomasi.domain.product._category.service.CategoryAdminService; @@ -18,21 +21,30 @@ public class CategoryAdminController { private final CategoryAdminService categoryAdminService; + @Secured("ROLE_ADMIN") @PostMapping("/api/categories") - public ResponseEntity registerCategory(@RequestBody CategoryRequest categoryRequest) { + public ResponseEntity registerCategory( + @AuthenticationPrincipal UserDetailsImpl userDetails, + @RequestBody CategoryRequest categoryRequest) { Long categoryId = categoryAdminService.registerCategory(categoryRequest); return new ResponseEntity<>(categoryId, HttpStatus.CREATED); } + @Secured("ROLE_ADMIN") @PutMapping("/api/categories/{categoryId}") - public ResponseEntity modifyCategory(@PathVariable Long categoryId, + public ResponseEntity modifyCategory( + @AuthenticationPrincipal UserDetailsImpl userDetails, + @PathVariable Long categoryId, @RequestBody CategoryRequest categoryRequest) { categoryAdminService.modifyCategory(categoryId, categoryRequest); return new ResponseEntity<>(categoryId, HttpStatus.OK); } + @Secured("ROLE_ADMIN") @DeleteMapping("/api/categories/{categoryId}") - public ResponseEntity deleteCategory(@PathVariable Long categoryId) { + public ResponseEntity deleteCategory( + @AuthenticationPrincipal UserDetailsImpl userDetails, + @PathVariable Long categoryId) { categoryAdminService.deleteCategory(categoryId); return new ResponseEntity<>(categoryId, HttpStatus.OK); } 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 1979cd47..6d316648 100644 --- a/src/main/java/poomasi/domain/product/_category/controller/CategoryController.java +++ b/src/main/java/poomasi/domain/product/_category/controller/CategoryController.java @@ -25,8 +25,8 @@ public ResponseEntity getAllCategories() { @GetMapping("/api/categories/{categoryId}") public ResponseEntity getCategoryById(@PathVariable Long categoryId) { - List productList = categoryService.getProductInCategory( - categoryId); + List productList + = categoryService.getProductInCategory(categoryId); return new ResponseEntity<>(productList, HttpStatus.OK); } } diff --git a/src/main/java/poomasi/domain/product/controller/ProductFarmerController.java b/src/main/java/poomasi/domain/product/controller/ProductFarmerController.java index cb97f6a1..4497f380 100644 --- a/src/main/java/poomasi/domain/product/controller/ProductFarmerController.java +++ b/src/main/java/poomasi/domain/product/controller/ProductFarmerController.java @@ -4,6 +4,8 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; 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; @@ -12,6 +14,8 @@ 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.entity.Member; import poomasi.domain.product.dto.ProductRegisterRequest; import poomasi.domain.product.dto.UpdateProductQuantityRequest; import poomasi.domain.product.service.ProductFarmerService; @@ -24,34 +28,48 @@ public class ProductFarmerController { private final ProductFarmerService productFarmerService; + @Secured({"ROLE_FARMER","ROLE_ADMIN"}) @PostMapping("") - public ResponseEntity registerProduct(@RequestBody ProductRegisterRequest product) { - Long productId = productFarmerService.registerProduct(product); + 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); } + @Secured({"ROLE_FARMER","ROLE_ADMIN"}) @PutMapping("/{productId}") - public ResponseEntity modifyProduct(@RequestBody ProductRegisterRequest product, + public ResponseEntity modifyProduct( + @AuthenticationPrincipal UserDetailsImpl userDetails, + @RequestBody ProductRegisterRequest product, @PathVariable Long productId) { - productFarmerService.modifyProduct(product, productId); + Member member = userDetails.getMember(); + productFarmerService.modifyProduct(member, product, productId); return new ResponseEntity<>(productId, HttpStatus.OK); } + @Secured("ROLE_FARMER") @DeleteMapping("/{productId}") - public ResponseEntity deleteProduct(@PathVariable Long productId) { - // TODO: farmerId를 SecurityContextHolder에서 가져와서 비교해야함. - - productFarmerService.deleteProduct(productId); + public ResponseEntity deleteProduct( + @AuthenticationPrincipal UserDetailsImpl userDetails, + @PathVariable Long productId) { + Member member = userDetails.getMember(); + productFarmerService.deleteProduct(member, productId); return new ResponseEntity<>(HttpStatus.OK); } + @Secured("ROLE_FARMER") @PatchMapping("/{productId}") - public ResponseEntity updateProductQuantity(@PathVariable Long productId, + public ResponseEntity updateProductQuantity( + @AuthenticationPrincipal UserDetailsImpl userDetails, + @PathVariable Long productId, @RequestBody UpdateProductQuantityRequest request) { log.debug("Product ID: {}", productId); log.debug("Update Request: {}", request); - productFarmerService.addQuantity(productId, request); + Member member = userDetails.getMember(); + productFarmerService.addQuantity(member, productId, request); return new ResponseEntity<>(HttpStatus.OK); } } diff --git a/src/main/java/poomasi/domain/product/dto/ProductRegisterRequest.java b/src/main/java/poomasi/domain/product/dto/ProductRegisterRequest.java index 96278af7..57828406 100644 --- a/src/main/java/poomasi/domain/product/dto/ProductRegisterRequest.java +++ b/src/main/java/poomasi/domain/product/dto/ProductRegisterRequest.java @@ -1,10 +1,10 @@ package poomasi.domain.product.dto; +import poomasi.domain.member.entity.Member; import poomasi.domain.product.entity.Product; public record ProductRegisterRequest( Long categoryId, - Long farmerId, //등록한 사람 String name, String description, String imageUrl, @@ -12,10 +12,10 @@ public record ProductRegisterRequest( Long price ) { - public Product toEntity() { + public Product toEntity(Member member) { return Product.builder() .categoryId(categoryId) - .farmerId(farmerId) + .farmerId(member.getId()) .name(name) .stock(stock) .description(description) diff --git a/src/main/java/poomasi/domain/product/entity/Product.java b/src/main/java/poomasi/domain/product/entity/Product.java index f39e5cb0..f7e7bc36 100644 --- a/src/main/java/poomasi/domain/product/entity/Product.java +++ b/src/main/java/poomasi/domain/product/entity/Product.java @@ -17,6 +17,7 @@ import org.hibernate.annotations.CreationTimestamp; import org.hibernate.annotations.SQLDelete; import org.hibernate.annotations.UpdateTimestamp; +import poomasi.domain.order.entity.OrderProductDetails; import poomasi.domain.product.dto.ProductRegisterRequest; import poomasi.domain.review.entity.Review; @@ -67,6 +68,11 @@ public class Product { @Comment("평균 평점") private double averageRating = 0.0; + @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) + @JoinColumn(name = "order_product_details_id") + private List orderProductDetails; + + @Builder public Product(Long productId, Long categoryId, @@ -107,4 +113,6 @@ public void addReview(Review pReview) { .orElse(0.0); } + + } diff --git a/src/main/java/poomasi/domain/product/service/ProductFarmerService.java b/src/main/java/poomasi/domain/product/service/ProductFarmerService.java index b37d4605..a225cfb7 100644 --- a/src/main/java/poomasi/domain/product/service/ProductFarmerService.java +++ b/src/main/java/poomasi/domain/product/service/ProductFarmerService.java @@ -3,6 +3,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import poomasi.domain.member.entity.Member; import poomasi.domain.product._category.entity.Category; import poomasi.domain.product._category.repository.CategoryRepository; import poomasi.domain.product.dto.ProductRegisterRequest; @@ -18,22 +19,21 @@ public class ProductFarmerService { private final ProductRepository productRepository; private final CategoryRepository categoryRepository; - //private final MemberService memberService; - public Long registerProduct(ProductRegisterRequest request) { - //memberService.isFarmer(request.farmerId()); + @Transactional + public Long registerProduct(Member member, ProductRegisterRequest request) { Category category = getCategory(request.categoryId()); - - Product saveProduct = productRepository.save(request.toEntity()); + Product saveProduct = productRepository.save(request.toEntity(member)); category.addProduct(saveProduct); return saveProduct.getId(); } - @Transactional - public void modifyProduct(ProductRegisterRequest productRequest, Long productId) { - // TODO: 주인인지 알아보기 + public void modifyProduct(Member member, ProductRegisterRequest productRequest, + Long productId) { Product product = getProductByProductId(productId); + checkAuth(member, product); + Long categoryId = product.getCategoryId(); Category oldCategory = getCategory(categoryId); @@ -46,9 +46,11 @@ public void modifyProduct(ProductRegisterRequest productRequest, Long productId) } @Transactional - public void deleteProduct(Long productId) { + public void deleteProduct(Member member, Long productId) { //TODO: 주인인지 알아보기 Product product = getProductByProductId(productId); + checkAuth(member, product); + Long categoryId = product.getCategoryId(); Category category = getCategory(categoryId); @@ -57,11 +59,10 @@ public void deleteProduct(Long productId) { } @Transactional - public void addQuantity(Long productId, UpdateProductQuantityRequest request) { - Product productByProductId = getProductByProductId(productId); - productByProductId.addStock(request.quantity()); - - productRepository.save(productByProductId); + public void addQuantity(Member member, Long productId, UpdateProductQuantityRequest request) { + Product product = getProductByProductId(productId); + checkAuth(member, product); + product.addStock(request.quantity()); } private Product getProductByProductId(Long productId) { @@ -73,4 +74,10 @@ private Category getCategory(Long categoryId) { return categoryRepository.findById(categoryId) .orElseThrow(() -> new BusinessException(BusinessError.CATEGORY_NOT_FOUND)); } + + private void checkAuth(Member member, Product product) { + if (!product.getFarmerId().equals(member.getId())) { + throw new BusinessException(BusinessError.MEMBER_ID_MISMATCH); + } + } } diff --git a/src/main/java/poomasi/domain/product/service/ProductService.java b/src/main/java/poomasi/domain/product/service/ProductService.java index ca7b482c..79b73dca 100644 --- a/src/main/java/poomasi/domain/product/service/ProductService.java +++ b/src/main/java/poomasi/domain/product/service/ProductService.java @@ -1,7 +1,6 @@ package poomasi.domain.product.service; import java.util.List; - import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import poomasi.domain.product.dto.ProductResponse; diff --git a/src/main/java/poomasi/domain/review/controller/ReviewController.java b/src/main/java/poomasi/domain/review/controller/ReviewController.java index a9c2f580..3e8629e0 100644 --- a/src/main/java/poomasi/domain/review/controller/ReviewController.java +++ b/src/main/java/poomasi/domain/review/controller/ReviewController.java @@ -3,11 +3,14 @@ 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.PathVariable; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; +import poomasi.domain.auth.security.userdetail.UserDetailsImpl; +import poomasi.domain.member.entity.Member; import poomasi.domain.review.dto.ReviewRequest; import poomasi.domain.review.service.ReviewService; @@ -18,15 +21,21 @@ public class ReviewController { private final ReviewService reviewService; @DeleteMapping("/api/reviews/{reviewId}") - public ResponseEntity deleteProductReview(@PathVariable Long reviewId) { - reviewService.deleteReview(reviewId); + public ResponseEntity deleteProductReview( + @AuthenticationPrincipal UserDetailsImpl userDetails, + @PathVariable Long reviewId) { + Member member = userDetails.getMember(); + reviewService.deleteReview(member, reviewId); return new ResponseEntity<>(HttpStatus.OK); } @PutMapping("/api/reviews/{reviewId}") - public ResponseEntity modifyProductReview(@PathVariable Long reviewId, + public ResponseEntity modifyProductReview( + @AuthenticationPrincipal UserDetailsImpl userDetails, + @PathVariable Long reviewId, @RequestBody ReviewRequest reviewRequest) { - reviewService.modifyReview(reviewId, reviewRequest); + Member member = userDetails.getMember(); + reviewService.modifyReview(member, reviewId, reviewRequest); return new ResponseEntity<>(HttpStatus.OK); } } 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 8c78705a..ba200cf9 100644 --- a/src/main/java/poomasi/domain/review/controller/farm/FarmReviewController.java +++ b/src/main/java/poomasi/domain/review/controller/farm/FarmReviewController.java @@ -4,11 +4,14 @@ 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.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; +import poomasi.domain.auth.security.userdetail.UserDetailsImpl; +import poomasi.domain.member.entity.Member; import poomasi.domain.review.dto.ReviewRequest; import poomasi.domain.review.dto.ReviewResponse; import poomasi.domain.review.service.farm.FarmReviewService; @@ -26,10 +29,13 @@ public ResponseEntity getProductReviews(@PathVariable Long farmId) { } @PostMapping("/api/farm/{farmId}/reviews") - public ResponseEntity registerProductReview(@PathVariable Long farmId, + public ResponseEntity registerProductReview( + @AuthenticationPrincipal UserDetailsImpl userDetails, + @PathVariable Long farmId, @RequestBody ReviewRequest reviewRequest) { - Long reviewId = farmReviewService.registerFarmReview(farmId, - reviewRequest); + Member member = userDetails.getMember(); + Long reviewId = farmReviewService.registerFarmReview( + member, farmId, 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 98fd6249..0bcea444 100644 --- a/src/main/java/poomasi/domain/review/controller/product/ProductReviewController.java +++ b/src/main/java/poomasi/domain/review/controller/product/ProductReviewController.java @@ -4,11 +4,14 @@ 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.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; +import poomasi.domain.auth.security.userdetail.UserDetailsImpl; +import poomasi.domain.member.entity.Member; import poomasi.domain.review.dto.ReviewRequest; import poomasi.domain.review.dto.ReviewResponse; import poomasi.domain.review.service.product.ProductReviewService; @@ -26,10 +29,13 @@ public ResponseEntity getProductReviews(@PathVariable Long productId) { } @PostMapping("/api/products/{productId}/reviews") - public ResponseEntity registerProductReview(@PathVariable Long productId, + public ResponseEntity registerProductReview( + @AuthenticationPrincipal UserDetailsImpl userDetails, + @PathVariable Long productId, @RequestBody ReviewRequest reviewRequest) { - Long reviewId = productReviewService.registerProductReview(productId, - reviewRequest); + Member member = userDetails.getMember(); + Long reviewId = productReviewService.registerProductReview( + member, productId, reviewRequest); return new ResponseEntity<>(reviewId, HttpStatus.CREATED); } } diff --git a/src/main/java/poomasi/domain/review/dto/ReviewRequest.java b/src/main/java/poomasi/domain/review/dto/ReviewRequest.java index 0e61fa12..01b3b9ff 100644 --- a/src/main/java/poomasi/domain/review/dto/ReviewRequest.java +++ b/src/main/java/poomasi/domain/review/dto/ReviewRequest.java @@ -1,5 +1,7 @@ package poomasi.domain.review.dto; +import poomasi.domain.member.entity.Member; +import poomasi.domain.review.entity.EntityType; import poomasi.domain.review.entity.Review; public record ReviewRequest( @@ -7,7 +9,13 @@ public record ReviewRequest( String content ) { - public Review toEntity(Long entityId) { - return new Review(this.rating, this.content, entityId); + public Review toEntity(Long entityId, EntityType entityType, Member member) { + return Review.builder() + .rating(rating) + .content(content) + .entityId(entityId) + .entityType(entityType) + .reviewer(member) + .build(); } } diff --git a/src/main/java/poomasi/domain/review/dto/ReviewResponse.java b/src/main/java/poomasi/domain/review/dto/ReviewResponse.java index dfd362b2..6acfc060 100644 --- a/src/main/java/poomasi/domain/review/dto/ReviewResponse.java +++ b/src/main/java/poomasi/domain/review/dto/ReviewResponse.java @@ -5,7 +5,7 @@ public record ReviewResponse (Long id, Long productId, - //Long reviewerId, + String reviewerName, Float rating, String content //List imageUrls @@ -15,7 +15,7 @@ public static ReviewResponse fromEntity(Review review) { return new ReviewResponse( review.getId(), review.getEntityId(), - //productReview.getReviewer().getId(), + review.getReviewer().getMemberProfile().getName(), review.getRating(), review.getContent() ); diff --git a/src/main/java/poomasi/domain/review/entity/Review.java b/src/main/java/poomasi/domain/review/entity/Review.java index b72da487..9cf457cf 100644 --- a/src/main/java/poomasi/domain/review/entity/Review.java +++ b/src/main/java/poomasi/domain/review/entity/Review.java @@ -3,15 +3,19 @@ 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 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.member.entity.Member; import poomasi.domain.review.dto.ReviewRequest; @Entity @@ -42,14 +46,18 @@ public class Review { @Enumerated(EnumType.STRING) private EntityType entityType; -// @Comment("작성자") -// @ManyToOne -// private Member reviewer; + @Comment("작성자") + @ManyToOne(fetch = FetchType.LAZY) + private Member reviewer; - public Review(Float rating, String content, Long entityId) { + @Builder + public Review(Float rating, String content, Long entityId, EntityType entityType, + Member reviewer) { this.rating = rating; this.content = content; this.entityId = entityId; + this.entityType = entityType; + this.reviewer = reviewer; } public void modifyReview(ReviewRequest reviewRequest) { @@ -57,7 +65,7 @@ public void modifyReview(ReviewRequest reviewRequest) { this.content = reviewRequest.content(); } - public void setReviewType(EntityType entityType) { + public void setEntityType(EntityType entityType) { this.entityType = entityType; } } diff --git a/src/main/java/poomasi/domain/review/service/ReviewService.java b/src/main/java/poomasi/domain/review/service/ReviewService.java index 3115163c..02933282 100644 --- a/src/main/java/poomasi/domain/review/service/ReviewService.java +++ b/src/main/java/poomasi/domain/review/service/ReviewService.java @@ -3,6 +3,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import poomasi.domain.member.entity.Member; import poomasi.domain.review.dto.ReviewRequest; import poomasi.domain.review.entity.Review; import poomasi.domain.review.repository.ReviewRepository; @@ -16,17 +17,27 @@ public class ReviewService { private final ReviewRepository reviewRepository; @Transactional - public void modifyReview(Long reviewId, ReviewRequest reviewRequest) { - Review pReview = getReviewById(reviewId); - pReview.modifyReview(reviewRequest); + public void modifyReview(Member member, Long reviewId, ReviewRequest reviewRequest) { + Review review = getReviewById(reviewId); + checkAuth(member, review); + review.modifyReview(reviewRequest); } @Transactional - public void deleteReview(Long reviewId) { + public void deleteReview(Member member, Long reviewId) { Review review = getReviewById(reviewId); + if (!member.isAdmin()) { + checkAuth(member, review); + } reviewRepository.delete(review); } + private void checkAuth(Member member, Review review) { + if (!review.getReviewer().getId().equals(member.getId())) { + throw new BusinessException(BusinessError.MEMBER_ID_MISMATCH); + } + } + private Review getReviewById(Long reviewId) { return reviewRepository.findById(reviewId) .orElseThrow(() -> new BusinessException(BusinessError.REVIEW_NOT_FOUND)); 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 131668a3..cdee33b6 100644 --- a/src/main/java/poomasi/domain/review/service/farm/FarmReviewService.java +++ b/src/main/java/poomasi/domain/review/service/farm/FarmReviewService.java @@ -6,6 +6,7 @@ import org.springframework.transaction.annotation.Transactional; import poomasi.domain.farm.entity.Farm; import poomasi.domain.farm.repository.FarmRepository; +import poomasi.domain.member.entity.Member; import poomasi.domain.review.dto.ReviewRequest; import poomasi.domain.review.dto.ReviewResponse; import poomasi.domain.review.entity.EntityType; @@ -29,11 +30,11 @@ public List getFarmReview(Long farmId) { } @Transactional - public Long registerFarmReview(Long entityId, ReviewRequest reviewRequest) { + public Long registerFarmReview(Member member, Long entityId, ReviewRequest reviewRequest) { // s3 이미지 저장하고 주소 받아와서 review에 추가해주기 - Review pReview = reviewRequest.toEntity(entityId); - pReview.setReviewType(EntityType.FARM); + Review pReview = reviewRequest.toEntity(entityId, EntityType.FARM, member); + pReview.setEntityType(EntityType.FARM); pReview = reviewRepository.save(pReview); return pReview.getId(); 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 cadec05c..dd499662 100644 --- a/src/main/java/poomasi/domain/review/service/product/ProductReviewService.java +++ b/src/main/java/poomasi/domain/review/service/product/ProductReviewService.java @@ -4,6 +4,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import poomasi.domain.member.entity.Member; import poomasi.domain.product.entity.Product; import poomasi.domain.product.repository.ProductRepository; import poomasi.domain.review.dto.ReviewRequest; @@ -29,13 +30,10 @@ public List getProductReview(Long productId) { } @Transactional - public Long registerProductReview(Long entityId, ReviewRequest reviewRequest) { + public Long registerProductReview(Member member, Long entityId, ReviewRequest reviewRequest) { // s3 이미지 저장하고 주소 받아와서 review에 추가해주기 - - Review pReview = reviewRequest.toEntity(entityId); - pReview.setReviewType(EntityType.PRODUCT); + Review pReview = reviewRequest.toEntity(entityId, EntityType.PRODUCT, member); pReview = reviewRepository.save(pReview); - return pReview.getId(); } diff --git a/src/main/java/poomasi/global/error/BusinessError.java b/src/main/java/poomasi/global/error/BusinessError.java index d9ce696a..b0944e04 100644 --- a/src/main/java/poomasi/global/error/BusinessError.java +++ b/src/main/java/poomasi/global/error/BusinessError.java @@ -22,6 +22,7 @@ public enum BusinessError { MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "회원이 존재하지 않습니다."), DUPLICATE_MEMBER_EMAIL(HttpStatus.CONFLICT, "중복된 이메일입니다."), INVALID_FARMER_QUALIFICATION(HttpStatus.BAD_REQUEST, "농부 자격 증명이 필요합니다."), + MEMBER_ID_MISMATCH(HttpStatus.FORBIDDEN, "권한이 없는 요청입니다."), // Auth INVALID_CREDENTIAL(HttpStatus.UNAUTHORIZED, "잘못된 비밀번호 입니다."), @@ -48,8 +49,27 @@ public enum BusinessError { RESERVATION_ALREADY_CANCELED(HttpStatus.BAD_REQUEST, "이미 취소된 예약입니다."), RESERVATION_CANCELLATION_PERIOD_EXPIRED(HttpStatus.BAD_REQUEST, "예약 취소 기간이 지났습니다."), + //Cart + CART_NOT_FOUND(HttpStatus.NOT_FOUND, "장바구니를 찾을 수 없습니다."), + // ETC - START_DATE_SHOULD_BE_BEFORE_END_DATE(HttpStatus.BAD_REQUEST, "시작 날짜는 종료 날짜보다 이전이어야 합니다."); + START_DATE_SHOULD_BE_BEFORE_END_DATE(HttpStatus.BAD_REQUEST, "시작 날짜는 종료 날짜보다 이전이어야 합니다."), + + + // ORDER + ORDER_NOT_FOUND(HttpStatus.NOT_FOUND, "주문을 찾을 수 없습니다."), + INVALID_ORDER_REQUEST(HttpStatus.BAD_REQUEST, "잘못된 주문 요청입니다."), + ORDER_VALIDATION_FAILED(HttpStatus.BAD_REQUEST, "주문 검증에 실패했습니다."), + ORDER_ALREADY_PROCESSED(HttpStatus.CONFLICT, "해당 주문은 이미 처리되었습니다."), + ORDER_VERIFICATION_FAILED(HttpStatus.UNPROCESSABLE_ENTITY, "주문 검증에 실패했습니다."), + ORDER_NOT_OWNED_EXCEPTION(HttpStatus.UNAUTHORIZED, "허가되지 않은 주문입니다."), + ORDER_PRODUCT_DETAILS_NOT_FOUND(HttpStatus.NOT_FOUND, "주문을 찾을 수 없습니다."), + ORDER_PRODUCT_DETAILS_NOT_OWNED_EXCEPTION(HttpStatus.UNAUTHORIZED, "허가되지 않은 주문입니다."), + + // PAYMENT + PAYMENT_NOT_FOUND(HttpStatus.NOT_FOUND , "결제를 찾을 수 없습니다."), + PAYMENT_AMOUNT_MISMATCH(HttpStatus.BAD_REQUEST, "사전 결제 금액과 사후 결제 금액이 일치하지 않습니다.") + ; private final HttpStatus httpStatus; diff --git a/src/test/java/poomasi/domain/payment/iamportTest/IamportTest.java b/src/test/java/poomasi/domain/payment/iamportTest/IamportTest.java new file mode 100644 index 00000000..2d3df7bd --- /dev/null +++ b/src/test/java/poomasi/domain/payment/iamportTest/IamportTest.java @@ -0,0 +1,59 @@ +package poomasi.domain.payment.iamportTest; + +import com.siot.IamportRestClient.IamportClient; +import com.siot.IamportRestClient.exception.IamportResponseException; +import com.siot.IamportRestClient.request.PrepareData; +import com.siot.IamportRestClient.response.AccessToken; +import com.siot.IamportRestClient.response.IamportResponse; +import com.siot.IamportRestClient.response.Payment; +import com.siot.IamportRestClient.response.Prepare; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.web.client.RestTemplate; +import poomasi.domain.order._payment.dto.request.PaymentPreRegisterRequest; +import poomasi.domain.order._payment.dto.response.PaymentPreRegisterResponse; + + +import java.io.IOException; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +import static com.fasterxml.jackson.databind.type.LogicalType.DateTime; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class IamportTest { + + private IamportClient iamportClient; + + private String apiKey="~"; + private String secretKey="~"; + private String accessToken; + + @BeforeEach + public void setUp() throws Exception { + this.iamportClient = new IamportClient(apiKey, secretKey); + IamportResponse auth_response = iamportClient.getAuth(); + this.accessToken=auth_response.getResponse().getToken(); + } + + @Test + public void portonePrePaymentRegister_Test() throws IamportResponseException ,IOException{ + String merchantUid = "poomasi_" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")); + BigDecimal amount = new BigDecimal("100"); + PrepareData prepareData = new PrepareData(merchantUid, amount); + + System.out.println(merchantUid); + IamportResponse prepareIamportResponse = iamportClient.postPrepare(prepareData); + System.out.println("Response Code: " + prepareIamportResponse.getCode()); + System.out.println("Response Message: " + prepareIamportResponse.getMessage()); + + } + +} +