diff --git a/src/main/java/com/t3t/frontserver/FrontServerApplication.java b/src/main/java/com/t3t/frontserver/FrontServerApplication.java index 00300d78..c5c725c5 100644 --- a/src/main/java/com/t3t/frontserver/FrontServerApplication.java +++ b/src/main/java/com/t3t/frontserver/FrontServerApplication.java @@ -2,16 +2,14 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.cloud.client.discovery.EnableDiscoveryClient; -import org.springframework.cloud.netflix.eureka.EnableEurekaClient; -import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.boot.context.properties.ConfigurationPropertiesScan; -@EnableDiscoveryClient +@ConfigurationPropertiesScan @SpringBootApplication public class FrontServerApplication { - public static void main(String[] args) { - SpringApplication.run(FrontServerApplication.class, args); - } + public static void main(String[] args) { + SpringApplication.run(FrontServerApplication.class, args); + } } diff --git a/src/main/java/com/t3t/frontserver/book/exception/BookApiClientException.java b/src/main/java/com/t3t/frontserver/book/exception/BookApiClientException.java new file mode 100644 index 00000000..5239c1b4 --- /dev/null +++ b/src/main/java/com/t3t/frontserver/book/exception/BookApiClientException.java @@ -0,0 +1,12 @@ +package com.t3t.frontserver.book.exception; + +/** + * BookApiClient 로 도서 관련 API 호출 실패시 발생하는 예외 + * + * @author woody35545(구건모) + */ +public class BookApiClientException extends RuntimeException { + public BookApiClientException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/src/main/java/com/t3t/frontserver/common/exception/GlobalExceptionHandler.java b/src/main/java/com/t3t/frontserver/common/exception/GlobalExceptionHandler.java index 4878003c..02392dd6 100644 --- a/src/main/java/com/t3t/frontserver/common/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/t3t/frontserver/common/exception/GlobalExceptionHandler.java @@ -10,26 +10,25 @@ import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletResponse; +import java.util.Optional; @Slf4j @ControllerAdvice public class GlobalExceptionHandler { + /** + * 예외 발생 시 예외에 대한 정보를 제공하는 핸들러 + * + * @author 구건모(woody35545) + */ @ExceptionHandler(Exception.class) public String handleException(RedirectAttributes redirectAttributes, Exception e) { - redirectAttributes.addAttribute("message", e.getMessage()); + + redirectAttributes.addAttribute("message", + Optional.ofNullable(e.getMessage()).orElse("오류가 발생하였습니다. 잠시 후 다시 시도해주세요.")); + return "redirect:/message"; } -/* @ExceptionHandler(RestApiClientException.class) - public String handleUnAuthorizedException(Model model, RestApiClientException e, HttpServletResponse response) { - Cookie cookie = new Cookie("t3t", null); - cookie.setMaxAge(0); - cookie.setPath("/"); - SecurityContextHolder.clearContext(); - response.addCookie(cookie); - return "redirect:/login"; - }*/ - @ExceptionHandler(FeignException.Unauthorized.class) public String handleLogoutException(Model model, FeignException.Unauthorized e, HttpServletResponse response) { Cookie cookie = new Cookie("t3t", null); diff --git a/src/main/java/com/t3t/frontserver/config/RedisConfig.java b/src/main/java/com/t3t/frontserver/config/RedisConfig.java new file mode 100644 index 00000000..387cd9f3 --- /dev/null +++ b/src/main/java/com/t3t/frontserver/config/RedisConfig.java @@ -0,0 +1,48 @@ +package com.t3t.frontserver.config; + +import com.t3t.frontserver.property.RedisProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.RedisStandaloneConfiguration; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.repository.configuration.EnableRedisRepositories; +import org.springframework.data.redis.serializer.StringRedisSerializer; + + +@Configuration +@EnableRedisRepositories +public class RedisConfig { + + /** + * RedisConnectionFactory 빈 설정 + * + * @author woody35545(구건모) + */ + @Bean + public RedisConnectionFactory redisConnectionFactory(RedisProperties redisProperties) { + RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration(); + redisStandaloneConfiguration.setHostName(redisProperties.getRedisServerIpAddress()); + redisStandaloneConfiguration.setPort(Integer.parseInt(redisProperties.getRedisServerPort())); + redisStandaloneConfiguration.setPassword(redisProperties.getRedisServerPassword()); + redisStandaloneConfiguration.setDatabase(redisProperties.getRedisDatabase()); + System.out.println("redisProperties.getRedisServerIpAddress() = " + redisProperties.getRedisServerIpAddress()); + return new LettuceConnectionFactory(redisStandaloneConfiguration); + } + + /** + * RedisTemplate 빈 설정 + * + * @author woody35545(구건모) + */ + @Bean + public RedisTemplate redisTemplate(RedisProperties redisProperties) { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(redisConnectionFactory(redisProperties)); + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setValueSerializer(new StringRedisSerializer()); + return redisTemplate; + } + +} diff --git a/src/main/java/com/t3t/frontserver/member/controller/MyPageController.java b/src/main/java/com/t3t/frontserver/member/controller/MyPageController.java index 7aca1395..7b9d5d8e 100644 --- a/src/main/java/com/t3t/frontserver/member/controller/MyPageController.java +++ b/src/main/java/com/t3t/frontserver/member/controller/MyPageController.java @@ -6,19 +6,29 @@ import com.t3t.frontserver.member.model.dto.MyPageInfoViewDto; import com.t3t.frontserver.member.model.response.MemberInfoResponse; import com.t3t.frontserver.member.service.MemberService; +import com.t3t.frontserver.model.response.PageResponse; +import com.t3t.frontserver.order.model.response.OrderInfoResponse; +import com.t3t.frontserver.order.service.OrderService; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; +@Slf4j @Controller @RequiredArgsConstructor public class MyPageController { private final MemberService memberService; + private final OrderService orderService; /** * 마이페이지 - 회원 기본 정보 관리 뷰 @@ -155,11 +165,39 @@ public String gradeView(Model model) { /** * 마이페이지 - 회원 주문 페이지 뷰 + * * @author woody35545(구건모) */ @GetMapping("/mypage/order") - public String orderView() { + public String orderView(Model model, + @RequestParam(value = "pageNo", defaultValue = "0", required = false) int pageNo, + @RequestParam(value = "pageSize", defaultValue = "3", required = false) int pageSize) { + + if (!SecurityContextUtils.isLoggedIn()) { + return "redirect:/login"; + } + + PageResponse orderInfoResponsePageResponse = + orderService.getMemberOrderInfoListByMemberId(SecurityContextUtils.getMemberId(), + Pageable.ofSize(pageSize).withPage(pageNo)); + + List orderInfoResponseList = new ArrayList<>(); + + if (orderInfoResponsePageResponse != null) { + orderInfoResponseList = orderInfoResponsePageResponse.getContent(); + + int blockLimit = 5; + int nextPage = orderInfoResponsePageResponse.getPageNo() + 1; + int startPage = Math.max(nextPage - blockLimit, 1); + int endPage = Math.min(nextPage + blockLimit, orderInfoResponsePageResponse.getTotalPages()); + + model.addAttribute("nextPage", nextPage); + model.addAttribute("startPage", startPage); + model.addAttribute("endPage", endPage); + } + + model.addAttribute("orderInfoResponseList", orderInfoResponseList); + return "main/page/mypageOrder"; } - } diff --git a/src/main/java/com/t3t/frontserver/order/adaptor/OrderAdaptor.java b/src/main/java/com/t3t/frontserver/order/adaptor/OrderAdaptor.java new file mode 100644 index 00000000..574cab65 --- /dev/null +++ b/src/main/java/com/t3t/frontserver/order/adaptor/OrderAdaptor.java @@ -0,0 +1,69 @@ +package com.t3t.frontserver.order.adaptor; + +import com.t3t.frontserver.model.response.BaseResponse; +import com.t3t.frontserver.model.response.PageResponse; +import com.t3t.frontserver.order.client.OrderApiClient; +import com.t3t.frontserver.order.exception.OrderApiClientException; +import com.t3t.frontserver.order.model.request.MemberOrderPreparationRequest; +import com.t3t.frontserver.order.model.request.OrderConfirmRequest; +import com.t3t.frontserver.order.model.response.MemberOrderPreparationResponse; +import com.t3t.frontserver.order.model.response.OrderInfoResponse; +import com.t3t.frontserver.util.FeignClientUtils; +import feign.FeignException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Component; + +import java.util.Optional; + +@Slf4j +@Component +@RequiredArgsConstructor +public class OrderAdaptor { + private final OrderApiClient orderApiClient; + + /** + * 회원 주문 생성 + * 결제 대기 상태의 주문을 생성한다. + * + * @author woody35545(구건모) + */ + public MemberOrderPreparationResponse createMemberOrder(MemberOrderPreparationRequest request) { + try { + return Optional.ofNullable(orderApiClient.createMemberOrder(request).getBody()) + .map(BaseResponse::getData) + .orElseThrow(RuntimeException::new); + } catch (FeignException e) { + throw new OrderApiClientException("주문 생성에 실패하였습니다. " + FeignClientUtils.getMessageFromFeignException(e)); + } + } + + /** + * 주문에 대한 결제 검증 및 주문 승인 + * + * @author woody35545(구건모) + */ + public void confirmOrder(OrderConfirmRequest request) { + try { + orderApiClient.confirmOrder(request); + } catch (FeignException e) { + throw new OrderApiClientException("주문에 대한 결제 검증에 실패하였습니다. " + FeignClientUtils.getMessageFromFeignException(e)); + } + } + + /** + * 특정 회원의 모든 주문 관련 정보를 페이징을 통해 조회 + * + * @author woody35545(구건모) + */ + public PageResponse getMemberOrderInfoListByMemberId(Long memberId, Pageable pageable) { + try { + return Optional.ofNullable(orderApiClient.getMemberOrderInfoListByMemberId(memberId, pageable).getBody()) + .map(BaseResponse::getData) + .orElseThrow(RuntimeException::new); + } catch (FeignException e) { + throw new OrderApiClientException("주문 정보 조회에 실패하였습니다. " + FeignClientUtils.getMessageFromFeignException(e)); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/t3t/frontserver/order/client/OrderApiClient.java b/src/main/java/com/t3t/frontserver/order/client/OrderApiClient.java new file mode 100644 index 00000000..b4929c16 --- /dev/null +++ b/src/main/java/com/t3t/frontserver/order/client/OrderApiClient.java @@ -0,0 +1,44 @@ +package com.t3t.frontserver.order.client; + +import com.t3t.frontserver.model.response.BaseResponse; +import com.t3t.frontserver.model.response.PageResponse; +import com.t3t.frontserver.order.model.request.MemberOrderPreparationRequest; +import com.t3t.frontserver.order.model.request.OrderConfirmRequest; +import com.t3t.frontserver.order.model.response.MemberOrderPreparationResponse; +import com.t3t.frontserver.order.model.response.OrderInfoResponse; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@FeignClient(name = "OrderApiClient", url = "${t3t.feignClient.url}") +public interface OrderApiClient { + + /** + * 회원 주문 생성 API
+ * 결제 대기 상태의 주문을 생성한다. + * + * @author woody35545(구건모) + */ + @PostMapping(value = "/t3t/bookstore/orders/member") + ResponseEntity> createMemberOrder(@RequestBody MemberOrderPreparationRequest memberOrderPreparationRequest); + + + /** + * 주문에 대한 결제 검증 및 주문 승인 API + * + * @author woody35545(구건모) + */ + @PostMapping("/t3t/bookstore/orders/confirm") + ResponseEntity> confirmOrder(@RequestBody OrderConfirmRequest orderConfirmRequest); + + /** + * 특정 회원의 모든 주문 관련 정보를 페이징을 통해 조회 + * + * @author woody35545(구건모) + */ + @GetMapping("/t3t/bookstore/members/{memberId}/orders") + ResponseEntity>> getMemberOrderInfoListByMemberId(@PathVariable("memberId") Long memberId, Pageable pageable); +} \ No newline at end of file diff --git a/src/main/java/com/t3t/frontserver/order/controller/OrderController.java b/src/main/java/com/t3t/frontserver/order/controller/OrderController.java new file mode 100644 index 00000000..90e035fc --- /dev/null +++ b/src/main/java/com/t3t/frontserver/order/controller/OrderController.java @@ -0,0 +1,191 @@ +package com.t3t.frontserver.order.controller; + +import com.t3t.frontserver.auth.util.SecurityContextUtils; +import com.t3t.frontserver.book.client.BookApiClient; +import com.t3t.frontserver.book.exception.BookApiClientException; +import com.t3t.frontserver.book.model.response.BookDetailResponse; +import com.t3t.frontserver.index.OrderFormRequest; +import com.t3t.frontserver.member.model.dto.MemberAddressDto; +import com.t3t.frontserver.member.model.dto.MemberDto; +import com.t3t.frontserver.member.model.response.MemberInfoResponse; +import com.t3t.frontserver.member.service.MemberService; +import com.t3t.frontserver.model.response.BaseResponse; +import com.t3t.frontserver.order.model.dto.OrderCheckoutViewDto; +import com.t3t.frontserver.order.model.request.MemberOrderPreparationRequest; +import com.t3t.frontserver.order.model.request.ShoppingCartOrderRequest; +import com.t3t.frontserver.shoppingcart.service.ShoppingCartService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PostMapping; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +@Slf4j +@Controller +@RequiredArgsConstructor +public class OrderController { + + private final MemberService memberService; + private final BookApiClient bookApiClient; + private final ShoppingCartService shoppingCartService; + + /** + * 회원 바로 주문 + * + * @author woody35545 + */ + @PostMapping("/order/checkout") + public String orderCheckoutView(@ModelAttribute OrderFormRequest request, Model model) { + + if (!SecurityContextUtils.isLoggedIn()) { + return "redirect:/login"; + } + + BigDecimal deliveryPrice = new BigDecimal("3000");// 배송료 (추후 변경 예정) + BigDecimal totalOrderPrice = BigDecimal.ZERO; // 상품 총 주문 금액 + BigDecimal totalPackagingPrice = BigDecimal.ZERO; // 포장 금액 + BigDecimal totalDiscountPrice = BigDecimal.ZERO; // 총 할인 금액 + BigDecimal totalPaymentPrice = BigDecimal.ZERO; // 결제 총액(남은 결제 금액) + + MemberInfoResponse memberInfoResponse = memberService.getMemberInfoResponseById(SecurityContextUtils.getMemberId()); + List memberAddressInfoList = memberService.getMemberAddressDtoListByMemberId(SecurityContextUtils.getMemberId()) + .stream() + .map(addressDto -> OrderCheckoutViewDto.MemberAddressInfo.builder() + .id(addressDto.getId()) + .roadNameAddress(addressDto.getRoadNameAddress()) + .addressDetail(addressDto.getAddressDetail()) + .nickName(addressDto.getAddressNickname()) + .isDefault(addressDto.getIsDefaultAddress()) + .build()).collect(Collectors.toList()); + + BookDetailResponse bookDetailResponse = Optional.ofNullable(bookApiClient.getBook(request.getBookId()).getBody()) + .map(BaseResponse::getData) + .orElseThrow(() -> new BookApiClientException("책 정보 조회 실패")); + + totalOrderPrice = totalOrderPrice.add( + bookDetailResponse.getPrice().multiply(BigDecimal.valueOf(request.getOrderQuantity()))); + + totalDiscountPrice = totalDiscountPrice.add( + bookDetailResponse.getPrice().subtract(bookDetailResponse.getDiscountedPrice()).multiply(BigDecimal.valueOf(request.getOrderQuantity()))); + + totalPaymentPrice = totalPaymentPrice.add( + totalOrderPrice.add(totalPackagingPrice).add(deliveryPrice).subtract(totalDiscountPrice)); + + OrderCheckoutViewDto.OrderDetailInfo orderDetailInfo = OrderCheckoutViewDto.OrderDetailInfo.builder() + .bookId(bookDetailResponse.getId()) + .bookName(bookDetailResponse.getBookName()) + .publisher(bookDetailResponse.getPublisherName()) + .price(bookDetailResponse.getPrice()) + .discountedPrice(bookDetailResponse.getDiscountedPrice()) + .discountRate(bookDetailResponse.getDiscountRate()) + .quantity(request.getOrderQuantity()) + .totalPrice(bookDetailResponse.getDiscountedPrice().multiply(BigDecimal.valueOf(request.getOrderQuantity()))) + .build(); + + OrderCheckoutViewDto orderCheckoutViewDto = OrderCheckoutViewDto.builder() + .memberId(memberInfoResponse.getMemberId()) + .memberName(memberInfoResponse.getName()) + .memberPhoneNumber(memberInfoResponse.getPhone()) + .addressInfoList(memberAddressInfoList) + .orderDetailInfoList(List.of(orderDetailInfo)) + .totalPackagingPrice(totalPackagingPrice) + .totalOrderPrice(totalOrderPrice) + .totalPaymentPrice(totalPaymentPrice) + .totalDiscountPrice(totalDiscountPrice) + .deliveryPrice(deliveryPrice) + .build(); + + model.addAttribute("orderCheckoutViewDto", orderCheckoutViewDto); + model.addAttribute("memberOrderPreparationRequest", new MemberOrderPreparationRequest()); + + return "main/page/orderCheckout"; + } + + /** + * 회원 장바구니 주문 + * @author woody35545(구건모) + */ + @PostMapping("/shoppingcart/order/checkout") + public String shoppingCartOrderCheckoutView(@ModelAttribute ShoppingCartOrderRequest request, Model model) { + + if (!SecurityContextUtils.isLoggedIn()) { + return "redirect:/login"; + } + + BigDecimal deliveryPrice = new BigDecimal("3000"); + BigDecimal totalOrderPrice = BigDecimal.ZERO; + BigDecimal totalPackagingPrice = BigDecimal.ZERO; + BigDecimal totalDiscountPrice = BigDecimal.ZERO; + BigDecimal totalPaymentPrice = BigDecimal.ZERO; + + + MemberInfoResponse memberInfoResponse = memberService.getMemberInfoResponseById(SecurityContextUtils.getMemberId()); + + List memberAddressInfoList = memberService.getMemberAddressDtoListByMemberId(SecurityContextUtils.getMemberId()) + .stream() + .map(addressDto -> OrderCheckoutViewDto.MemberAddressInfo.builder() + .id(addressDto.getId()) + .roadNameAddress(addressDto.getRoadNameAddress()) + .addressDetail(addressDto.getAddressDetail()) + .nickName(addressDto.getAddressNickname()) + .isDefault(addressDto.getIsDefaultAddress()) + .build()).collect(Collectors.toList()); + + List orderedBookDetailResponseList = request.getOrderDetailInfoList().stream() + .map(orderDetailInfo -> Optional.ofNullable(bookApiClient.getBook(orderDetailInfo.getBookId()).getBody()) + .map(BaseResponse::getData) + .orElseThrow(() -> new BookApiClientException("책 정보 조회 실패"))).collect(Collectors.toList()); + + List orderedOrderDetailInfoList = new ArrayList(); + + for (int i = 0; i < request.getOrderDetailInfoList().size(); i++) { + ShoppingCartOrderRequest.OrderDetailInfo orderDetailInfo = request.getOrderDetailInfoList().get(i); + + BookDetailResponse bookDetailResponse = orderedBookDetailResponseList.get(i); + + OrderCheckoutViewDto.OrderDetailInfo orderedOrderDetailInfo = OrderCheckoutViewDto.OrderDetailInfo.builder() + .bookId(bookDetailResponse.getId()) + .bookName(bookDetailResponse.getBookName()) + .publisher(bookDetailResponse.getPublisherName()) + .price(bookDetailResponse.getPrice()) + .discountedPrice(bookDetailResponse.getDiscountedPrice()) + .discountRate(bookDetailResponse.getDiscountRate()) + .quantity(orderDetailInfo.getQuantity()) + .totalPrice(bookDetailResponse.getDiscountedPrice().multiply(BigDecimal.valueOf(orderDetailInfo.getQuantity()))) + .build(); + + totalOrderPrice = totalOrderPrice.add(bookDetailResponse.getPrice().multiply(BigDecimal.valueOf(orderDetailInfo.getQuantity()))); + totalDiscountPrice = totalDiscountPrice.add(bookDetailResponse.getPrice().subtract(bookDetailResponse.getDiscountedPrice()).multiply(BigDecimal.valueOf(orderDetailInfo.getQuantity()))); + totalPaymentPrice = totalPaymentPrice.add(bookDetailResponse.getDiscountedPrice().multiply(BigDecimal.valueOf(orderDetailInfo.getQuantity()))); + + orderedOrderDetailInfoList.add(orderedOrderDetailInfo); + } + + OrderCheckoutViewDto orderCheckoutViewDto = OrderCheckoutViewDto.builder() + .memberId(memberInfoResponse.getMemberId()) + .memberName(memberInfoResponse.getName()) + .memberPhoneNumber(memberInfoResponse.getPhone()) + .addressInfoList(memberAddressInfoList) + .orderDetailInfoList(orderedOrderDetailInfoList) + .totalPackagingPrice(totalPackagingPrice) + .totalOrderPrice(totalOrderPrice) + .totalPaymentPrice(totalPaymentPrice) + .totalDiscountPrice(totalDiscountPrice) + .deliveryPrice(deliveryPrice) + .build(); + + model.addAttribute("orderCheckoutViewDto", orderCheckoutViewDto); + model.addAttribute("memberOrderPreparationRequest", new MemberOrderPreparationRequest()); + + shoppingCartService.deleteShoppingCart(SecurityContextUtils.getMemberId().toString()); + + return "main/page/orderCheckout"; + } +} \ No newline at end of file diff --git a/src/main/java/com/t3t/frontserver/order/exception/OrderApiClientException.java b/src/main/java/com/t3t/frontserver/order/exception/OrderApiClientException.java new file mode 100644 index 00000000..9504eeed --- /dev/null +++ b/src/main/java/com/t3t/frontserver/order/exception/OrderApiClientException.java @@ -0,0 +1,18 @@ +package com.t3t.frontserver.order.exception; + +/** + * 주문 API 클라이언트 호출 중 발생하는 예외 + * + * @auhtor woody35545(구건모) + */ +public class OrderApiClientException extends RuntimeException { + private static final String DEFAULT_MESSAGE = "주문 API 호출에 실패하였습니다."; + + public OrderApiClientException() { + super(DEFAULT_MESSAGE); + } + + public OrderApiClientException(String reason) { + super(DEFAULT_MESSAGE + " " + reason); + } +} \ No newline at end of file diff --git a/src/main/java/com/t3t/frontserver/order/model/dto/OrderCheckoutViewDto.java b/src/main/java/com/t3t/frontserver/order/model/dto/OrderCheckoutViewDto.java new file mode 100644 index 00000000..f676d1ec --- /dev/null +++ b/src/main/java/com/t3t/frontserver/order/model/dto/OrderCheckoutViewDto.java @@ -0,0 +1,126 @@ +package com.t3t.frontserver.order.model.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.lang.Nullable; + +import java.math.BigDecimal; +import java.util.List; + +/** + * 주문 페이지에 필요한 정보를 담는 DTO + * + * @author woody35545(구건모) + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class OrderCheckoutViewDto { + + /** + * 주문인 정보 + * 비회원의 경우 null 이 될 수 있다. + */ + @Nullable + private Long memberId; // 주문인의 회원 식별자 + @Nullable + private String memberName; // 주문인 이름 + @Nullable + private String memberPhoneNumber; // 주문인 휴대전화 번호 + + /** + * 사용자 배송 주소록 정보 + * 사용자가 어떠한 주소도 등록하지 않은 경우 null 또는 empty 가 될 수 있다. + */ + @Nullable + private List addressInfoList; // 회원이 등록한 주소 리스트 + + /** + * 주문 관련 정보 + * 단건의 상품을 구매하더라도 List 로 받아서 처리한다. + */ + private List orderDetailInfoList; // 주문 상세 정보 + + + /** + * 지불 금액 관련 정보 + * 사용자가 선택한 상품, 포장, 쿠폰 등에 따라 적절히 계산되어 설정된다. + */ + private BigDecimal totalOrderPrice; // 상품 총 주문 금액 + private BigDecimal totalPackagingPrice; // 포장 금액 + private BigDecimal deliveryPrice; // 배송료 + private BigDecimal totalDiscountPrice; // 총 할인 금액 + private BigDecimal totalPaymentPrice; // 결제 총액(남은 결제 금액) + + /** + * 회원 주소 정보 + */ + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class MemberAddressInfo { + private Long id; // MemberAddress 식별자 + private String roadNameAddress; // 도로명 주소 + private String addressDetail; // 상세 주소 + private String nickName; // 주소 별칭 + private Boolean isDefault; // 기본 주소 여부 + } + + /** + * 주문 상세 정보 + */ + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class OrderDetailInfo { + + /** + * 상품 정보 + */ + private Long bookId; // 책 식별자 + private String bookName; // 책 이름 + private String publisher; // 출판사 이름 + private Integer quantity; // 책 수량 + private BigDecimal price; // 책 가격 + private BigDecimal discountedPrice; // 확인 필요) + private BigDecimal discountRate; // 책 할인율 + private BigDecimal totalPrice; // 책 총 가격 + + /** + * 포장 정보 + * 포장지를 사용하지 않을 경우 포장지 관련 필드는 null 이 될 수 있다. + */ + @Nullable + private Long packagingId; // 포장지 식별자 + @Nullable + private String packagingName; // 포장지 이름 + @Nullable + private BigDecimal packagingPrice; // 포장지 가격 + } + + /** + * 쿠폰 정보 + * 아직 쿠폰이 구현되지 않은 상태이므로 임의로 구성하였다. + * 따라서 추후에 변경될 수 있다. + * 쿠폰을 사용하지 않을 경우 null 이 될 수 있다. + */ + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class CouponInfo { + @Nullable + private Long couponId; // 쿠폰 식별자 + @Nullable + private String couponName; // 쿠폰 이름 + @Nullable + private BigDecimal discountPrice; // 할인 금액 + @Nullable + private BigDecimal discountRate; // 할인율 + } +} \ No newline at end of file diff --git a/src/main/java/com/t3t/frontserver/order/model/request/GuestOrderPreparationRequest.java b/src/main/java/com/t3t/frontserver/order/model/request/GuestOrderPreparationRequest.java new file mode 100644 index 00000000..0183c718 --- /dev/null +++ b/src/main/java/com/t3t/frontserver/order/model/request/GuestOrderPreparationRequest.java @@ -0,0 +1,88 @@ +package com.t3t.frontserver.order.model.request; + +import com.t3t.frontserver.payment.constant.PaymentProviderType; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.lang.Nullable; + +import javax.validation.constraints.*; +import java.time.LocalDate; +import java.util.List; + +/** + * 비회원 임시 주문 생성 요청 객체 + * + * @auhtor woody35545(구건모) + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class GuestOrderPreparationRequest { + /** + * 비회원 정보 + */ + private String guestOrderPassword; // 비회원 주문 비밀번호 + + /** + * 결제 정보 + */ + + @NotNull(message = "결제 제공자가 누락되었습니다.") + private PaymentProviderType paymentProviderType; // 결제 제공자 + + /** + * 주문 상세 정보 (상품, 수량, 포장 정보) + */ + @NotEmpty(message = "주문 상세 정보가 누락되었습니다.") + private List orderDetailInfoList; // 주문 상세 정보 + + /** + * 배송 정보 + */ + @Nullable + private Integer addressNumber; // 배송 우편 주소 + + @Nullable + private String roadnameAddress; // 배송 도로명 주소 + + @NotNull(message = "배송 상세 주소가 누락되었습니다.") + private String detailAddress; // 배송 상세 주소 + + @NotNull(message = "희망 배송 일자가 누락되었습니다.") + @Future(message = "희망 배송 일자는 현재 날짜보다 이후 날짜여야 합니다.") + private LocalDate deliveryDate; // 희망 배송 일자 + + @NotBlank(message = "수령인 이름이 누락되었습니다.") + private String recipientName; // 배송 수령인 이름 + + @NotBlank(message = "수령인 전화번호가 누락되었습니다.") + @Pattern(regexp = "^\\d{3}-\\d{4}-\\d{4}$", message = "올바른 전화번호 형식이 아닙니다. (예시: 010-1234-5678)") + private String recipientPhoneNumber; // 배송 수령인 전화번호 + + @AssertTrue(message = "우편 주소와 도로명 주소 중 하나는 반드시 입력되어야 합니다.") + private boolean isEitherAddressNotNull() { + return addressNumber != null || roadnameAddress != null; + } + + /** + * 주문 상세 생성에 필요한 정보
+ * 주문 상품, 수량, 포장 정보를 가지고 있다. + * + * @auhtor woody35545(구건모) + */ + @Data + @Builder + @AllArgsConstructor + @NoArgsConstructor + public static class OrderDetailInfo { + @NotNull(message = "책 식별자가 누락되었습니다.") + private Long bookId; // 책 식별자 + @NotNull(message = "수량이 누락되었습니다.") + private Integer quantity; // 주문 수량 + @Nullable + private Long packagingId; // 포장 식별자 + } +} diff --git a/src/main/java/com/t3t/frontserver/order/model/request/MemberOrderPreparationRequest.java b/src/main/java/com/t3t/frontserver/order/model/request/MemberOrderPreparationRequest.java new file mode 100644 index 00000000..0c642d8e --- /dev/null +++ b/src/main/java/com/t3t/frontserver/order/model/request/MemberOrderPreparationRequest.java @@ -0,0 +1,92 @@ +package com.t3t.frontserver.order.model.request; + +import com.t3t.frontserver.payment.constant.PaymentProviderType; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.lang.Nullable; + +import javax.validation.constraints.*; +import java.time.LocalDate; +import java.util.List; + +/** + * 결제 대기 상태의 주문 생성 요청에 대한 DTO 클래스 + * + * @auhtor woody35545(구건모) + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class MemberOrderPreparationRequest { + + /** + * 회원 정보 + */ + @NotNull(message = "주문인의 회원 식별자가 누락되었습니다.") + private Long memberId; // 주문인 + + /** + * 결제 정보 + */ + @NotNull(message = "결제 제공자가 누락되었습니다.") + private PaymentProviderType paymentProviderType; // 결제 제공자 + + /** + * 주문 상세 정보 (상품, 수량, 포장 정보) + */ + @NotEmpty(message = "주문 상세 정보가 누락되었습니다.") + private List orderDetailInfoList; // 주문 상세 정보 + + /** + * 배송 정보 + */ + @Nullable + private Long memberAddressId; // 회원 주소록에서 주소를 선택한 경우 회원 주소 식별자 + + @Nullable + private Integer addressNumber; // 배송 우편 주소 + + @Nullable + private String roadnameAddress; // 배송 도로명 주소 + + @NotNull(message = "배송 상세 주소가 누락되었습니다.") + private String detailAddress; // 배송 상세 주소 + + @NotNull(message = "희망 배송 일자가 누락되었습니다.") + @Future(message = "희망 배송 일자는 현재 날짜보다 이후 날짜여야 합니다.") + private LocalDate deliveryDate; // 희망 배송 일자 + + @NotBlank(message = "수령인 이름이 누락되었습니다.") + private String recipientName; // 배송 수령인 이름 + + @NotBlank(message = "수령인 전화번호가 누락되었습니다.") + @Pattern(regexp = "^\\d{3}-\\d{4}-\\d{4}$", message = "올바른 전화번호 형식이 아닙니다. (예시: 010-1234-5678)") + private String recipientPhoneNumber; // 배송 수령인 전화번호 + + @AssertTrue(message = "우편 주소와 도로명 주소 중 하나는 반드시 입력되어야 합니다.") + private boolean isEitherAddressNotNull() { + return memberAddressId != null || addressNumber != null || roadnameAddress != null; + } + + /** + * 주문 상세 생성에 필요한 정보
+ * 주문 상품, 수량, 포장 정보를 가지고 있다. + * + * @auhtor woody35545(구건모) + */ + @Data + @Builder + @AllArgsConstructor + @NoArgsConstructor + public static class OrderDetailInfo { + @NotNull(message = "책 식별자가 누락되었습니다.") + private Long bookId; // 책 식별자 + @NotNull(message = "수량이 누락되었습니다.") + private Integer quantity; // 주문 수량 + @Nullable + private Long packagingId; // 포장 식별자 + } +} \ No newline at end of file diff --git a/src/main/java/com/t3t/frontserver/order/model/request/OrderConfirmRequest.java b/src/main/java/com/t3t/frontserver/order/model/request/OrderConfirmRequest.java new file mode 100644 index 00000000..68c79e3f --- /dev/null +++ b/src/main/java/com/t3t/frontserver/order/model/request/OrderConfirmRequest.java @@ -0,0 +1,33 @@ +package com.t3t.frontserver.order.model.request; + +import com.t3t.frontserver.payment.constant.PaymentProviderType; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.math.BigDecimal; + +/** + * 주문 승인 요청 객체 + * + * @Author woody35545(구건모) + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class OrderConfirmRequest { + @NotNull(message = "주문 식별자가 누락되었습니다.") + private Long orderId; // 주문 식별자 + @NotNull(message = "결제 제공처가 누락되었습니다.") + private PaymentProviderType paymentProviderType; // 결제 제공처 + @NotBlank(message = "결제 제공처의 결제 키가 누락되었습니다.") + private String paymentKey; // 결제 제공처의 결제 키 + @NotBlank(message = "결제 제공처의 주문 식별자가 누락되었습니다.") + private String paymentOrderId; // 결제 제공처의 주문 식별자 + @NotBlank(message = "결제 금액 정보가 누락되었습니다.") + private BigDecimal paidAmount; // 사용자가 결제한 금액 +} diff --git a/src/main/java/com/t3t/frontserver/order/model/request/ShoppingCartOrderRequest.java b/src/main/java/com/t3t/frontserver/order/model/request/ShoppingCartOrderRequest.java new file mode 100644 index 00000000..4ebe22d8 --- /dev/null +++ b/src/main/java/com/t3t/frontserver/order/model/request/ShoppingCartOrderRequest.java @@ -0,0 +1,32 @@ +package com.t3t.frontserver.order.model.request; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +/** + * 장바구니 주문 요청 객체 + * + * @author woody35545(구건모) + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class ShoppingCartOrderRequest { + List orderDetailInfoList = new ArrayList<>(); + + @Data + @AllArgsConstructor + @NoArgsConstructor + @Builder + public static class OrderDetailInfo { + private Long bookId; + private Integer quantity; + private String packagingName; + } +} diff --git a/src/main/java/com/t3t/frontserver/order/model/response/GuestOrderPreparationResponse.java b/src/main/java/com/t3t/frontserver/order/model/response/GuestOrderPreparationResponse.java new file mode 100644 index 00000000..f3d38993 --- /dev/null +++ b/src/main/java/com/t3t/frontserver/order/model/response/GuestOrderPreparationResponse.java @@ -0,0 +1,38 @@ +package com.t3t.frontserver.order.model.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +/** + * 비회원 임시 주문 생성에 대한 응답 객체 + * + * @author woody35545(구건모) + */ + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class GuestOrderPreparationResponse { + + /** + * 생성된 주문 정보 + */ + private String guestOrderId; // 비회원에게 제공하는 주문 식별자 + private Long orderId; // 주문 식별자 + + /** + * 결제 관련 정보 + */ + private String providerOrderId; // 결제 제공자측에서 사용할 주문 식별자 + private BigDecimal totalPrice; // 총 결제해야 할 금액 + + /** + * 배송 정보 + */ + private Long deliveryId; // 배송 정보 식별자 +} diff --git a/src/main/java/com/t3t/frontserver/order/model/response/MemberOrderPreparationResponse.java b/src/main/java/com/t3t/frontserver/order/model/response/MemberOrderPreparationResponse.java new file mode 100644 index 00000000..23e5907d --- /dev/null +++ b/src/main/java/com/t3t/frontserver/order/model/response/MemberOrderPreparationResponse.java @@ -0,0 +1,41 @@ +package com.t3t.frontserver.order.model.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +/** + * 회원 임시 주문 생성에 대한 응답 객체 + * + * @auhtor woody35545(구건모) + */ + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MemberOrderPreparationResponse { + /** + * 회원 정보 + */ + private Long memberId; // 회원 식별자 + + /** + * 생성된 주문 정보 + */ + private Long orderId; // 주문 식별자 + + /** + * 결제 관련 정보 + */ + private String providerOrderId; // 결제 제공자측에서 사용할 주문 식별자 + private BigDecimal totalPrice; // 총 결제해야 할 금액 + + /** + * 배송 정보 + */ + private Long deliveryId; // 배송 정보 식별자 +} \ No newline at end of file diff --git a/src/main/java/com/t3t/frontserver/order/model/response/OrderInfoResponse.java b/src/main/java/com/t3t/frontserver/order/model/response/OrderInfoResponse.java new file mode 100644 index 00000000..e5c4f051 --- /dev/null +++ b/src/main/java/com/t3t/frontserver/order/model/response/OrderInfoResponse.java @@ -0,0 +1,105 @@ +package com.t3t.frontserver.order.model.response; + +import com.t3t.frontserver.payment.constant.PaymentProviderType; +import lombok.*; +import org.springframework.lang.Nullable; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; + +/** + * 주문 관련 정보에 대한 응답 객체 + * + * @author woody35545(구건모) + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class OrderInfoResponse { + + /** + * 주문 정보 + */ + private Long orderId; + private LocalDateTime orderCreatedAt; + + /** + * 주문에 포함된 주문 상세 정보 + */ + private List orderDetailInfoList; + + + /** + * 주문 회원 정보
+ * 비회원의 경우 null 이 될 수 있다. + */ + @Nullable + private Long memberId; + + /** + * 결제 정보 + */ + private Long paymentId; + private Long paymentProviderId; + private PaymentProviderType paymentProviderType; + private BigDecimal paymentTotalAmount; + private LocalDateTime paymentCreatedAt; + private String paymentProviderOrderId; + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor(access = AccessLevel.PRIVATE) + public static class OrderDetailInfo { + private Long orderDetailId; // 주문 상세 식별자 + private LocalDateTime createdAt; // 주문 상세 생성 일시 + + private Integer quantity; // 주문 수량 + /** + * price
+ * 주문 상세 항목 단건에 대한 최종 가격 + * (= 책 가격 * 할인율 + 포장지 가격) + */ + private BigDecimal price; // 상품 최종 결제 금액 + + /** + * order + */ + private Long orderId; // 주문 상세 항목이 속한 주문 정보 식별자 + + /** + * book + */ + private Long bookId; // 주문한 책 식별자 + private String bookName; // 주문한 책 이름 + private String bookPublisherName; // 주문한 책 출판사 이름 + private String bookImageUrl; // 책 이미지 URL + + /** + * packaging + */ + private Long packagingId; // 주문 상세 항목에 사용된 포장지 식별자 + private String packagingName; // 주문 상세 항목에 사용된 포장지 이름 + private BigDecimal packagingPrice; // 주문 상세 항목에 사용된 포장지 가격 + + /** + * orderStatus + */ + private String orderStatusName; // 주문 상태 이름 + + /** + * delivery + */ + private Long deliveryId; // 배송 식별자 + private BigDecimal deliveryPrice; // 배송비 + private int addressNumber; // 우편 주소 + private String roadnameAddress; // 도로명 주소 + private String detailAddress; // 상세 주소 + private LocalDate deliveryDate; // 배송 예정 일자(희망 배송 일자) + private String recipientName; // 수령인 이름 + private String recipientPhoneNumber; // 수령인 연락처 + } +} \ No newline at end of file diff --git a/src/main/java/com/t3t/frontserver/order/service/OrderService.java b/src/main/java/com/t3t/frontserver/order/service/OrderService.java new file mode 100644 index 00000000..1e8cf61c --- /dev/null +++ b/src/main/java/com/t3t/frontserver/order/service/OrderService.java @@ -0,0 +1,47 @@ +package com.t3t.frontserver.order.service; + + +import com.t3t.frontserver.model.response.PageResponse; +import com.t3t.frontserver.order.adaptor.OrderAdaptor; +import com.t3t.frontserver.order.model.request.MemberOrderPreparationRequest; +import com.t3t.frontserver.order.model.request.OrderConfirmRequest; +import com.t3t.frontserver.order.model.response.MemberOrderPreparationResponse; +import com.t3t.frontserver.order.model.response.OrderInfoResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class OrderService { + private final OrderAdaptor orderAdaptor; + + /** + * 회원 주문 생성 + * 결제 대기 상태의 주문을 생성한다. + * + * @author woody35545(구건모) + */ + public MemberOrderPreparationResponse createMemberOrder(MemberOrderPreparationRequest memberOrderPreparationRequest) { + return orderAdaptor.createMemberOrder(memberOrderPreparationRequest); + } + + /** + * 주문에 대한 결제 검증 및 주문 승인 + * + * @author woody35545(구건모) + */ + public void confirmOrder(OrderConfirmRequest request) { + orderAdaptor.confirmOrder(request); + } + + /** + * 특정 회원의 모든 주문 관련 정보를 페이징을 통해 조회 + * + * @author woody35545(구건모) + */ + public PageResponse getMemberOrderInfoListByMemberId(Long memberId, Pageable pageable) { + return orderAdaptor.getMemberOrderInfoListByMemberId(memberId, pageable); + } +} \ No newline at end of file diff --git a/src/main/java/com/t3t/frontserver/payments/config/MvcConfig.java b/src/main/java/com/t3t/frontserver/payment/config/MvcConfig.java similarity index 93% rename from src/main/java/com/t3t/frontserver/payments/config/MvcConfig.java rename to src/main/java/com/t3t/frontserver/payment/config/MvcConfig.java index b1e3d20a..c123dfae 100644 --- a/src/main/java/com/t3t/frontserver/payments/config/MvcConfig.java +++ b/src/main/java/com/t3t/frontserver/payment/config/MvcConfig.java @@ -1,4 +1,4 @@ -package com.t3t.frontserver.payments.config; +package com.t3t.frontserver.payment.config; import org.springframework.context.annotation.Configuration; import org.springframework.http.CacheControl; diff --git a/src/main/java/com/t3t/frontserver/payment/constant/PaymentProviderType.java b/src/main/java/com/t3t/frontserver/payment/constant/PaymentProviderType.java new file mode 100644 index 00000000..693a78b2 --- /dev/null +++ b/src/main/java/com/t3t/frontserver/payment/constant/PaymentProviderType.java @@ -0,0 +1,10 @@ +package com.t3t.frontserver.payment.constant; + +/** + * 결제 서비스 제공자의 종류를 나타내는 enum
+ * + * @author woody35545(구건모) + */ +public enum PaymentProviderType { + TOSS, NAVER +} diff --git a/src/main/java/com/t3t/frontserver/payment/controller/PaymentController.java b/src/main/java/com/t3t/frontserver/payment/controller/PaymentController.java new file mode 100644 index 00000000..856aa6c8 --- /dev/null +++ b/src/main/java/com/t3t/frontserver/payment/controller/PaymentController.java @@ -0,0 +1,116 @@ +package com.t3t.frontserver.payment.controller; + +import com.t3t.frontserver.auth.util.SecurityContextUtils; +import com.t3t.frontserver.order.model.request.MemberOrderPreparationRequest; +import com.t3t.frontserver.order.model.request.OrderConfirmRequest; +import com.t3t.frontserver.order.model.response.MemberOrderPreparationResponse; +import com.t3t.frontserver.order.service.OrderService; +import com.t3t.frontserver.payment.constant.PaymentProviderType; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; + +import java.math.BigDecimal; +import java.time.LocalDate; + +@Slf4j +@Controller +@RequiredArgsConstructor +public class PaymentController { + + @Value("${t3t.feignClient.url}") + private String feignClientUrl; + + private final OrderService orderService; + + /** + * 결제 대기상태의 주문을 생성하고, 사용자가 결제를 진행할 수 있도록 결제 페이지를 반환한다. + * + * @author woody35545(구건모) + */ + @PostMapping("/payment") + public String paymentCheckoutView(Model model, @ModelAttribute MemberOrderPreparationRequest memberOrderPreparationRequest) { + + if (!SecurityContextUtils.isLoggedIn()) { + return "redirect:/login"; + } + + // 날짜 선택 관련 UI 적용 전까지 배송날짜 임시로 설정 + memberOrderPreparationRequest.setPaymentProviderType(PaymentProviderType.TOSS); + memberOrderPreparationRequest.setDeliveryDate(LocalDate.now().plusDays(3)); + log.info("memberOrderPreparationRequest => {}", memberOrderPreparationRequest); + + // 결제 대기 상태의 주문 생성 + MemberOrderPreparationResponse memberOrderPreparationResponse = + orderService.createMemberOrder(memberOrderPreparationRequest); + log.info("memberOrderPreparationResponse => {}", memberOrderPreparationResponse); + + // 총 결제해야할 금액 + model.addAttribute("amount", memberOrderPreparationResponse.getTotalPrice()); + + // 북스토에서 사용하는 주문 식별자 + model.addAttribute("orderId", memberOrderPreparationResponse.getOrderId()); + + // 결제 제공처에서 사용할 주문 식별자 + model.addAttribute("providerOrderId", memberOrderPreparationResponse.getProviderOrderId()); + + return "main/payment/checkout"; + } + + + /** + * 결제 페이지에서 결제 과정이 성공적으로 수행된 경우 이동되는 페이지로, + * 북스토어 백엔드측에 결제가 유효한지 검증을 요청하고 결제 대기 상태의 주문을 확정한다. + * + * @author woody35545(구건모) + */ + @GetMapping("/payment/success") + public String paymentRequest(Model model, + @RequestParam Long serviceOrderId, + @RequestParam String paymentKey, + @RequestParam("orderId") String providerOrderId, + @RequestParam String amount) { + + log.info("serviceOrderId => {}", serviceOrderId); + + OrderConfirmRequest orderConfirmRequest = OrderConfirmRequest.builder() + .paymentOrderId(providerOrderId) + .orderId(serviceOrderId) + .paymentKey(paymentKey) + .paymentProviderType(PaymentProviderType.TOSS) + .paidAmount(new BigDecimal(amount)).build(); + + log.info("[*] orderConfirmRequest => {}", orderConfirmRequest); + + log.info("[*] confirmOrder request ... "); + orderService.confirmOrder(orderConfirmRequest); + + model.addAttribute("feignClientUrl", feignClientUrl); + model.addAttribute("paymentKey", paymentKey); + model.addAttribute("orderId", providerOrderId); + model.addAttribute("amount", amount); + + return "main/payment/success"; + } + + /** + * 결제 실패시 이동하는 페이지로 사용자에게 결제 실패 사유에 대한 메시지를 제공한다. + * + * @author woody35545(구건모) + */ + @GetMapping("/payment/fail") + public String failPayment(Model model, @RequestParam String failCode, @RequestParam String failMessage) { + + model.addAttribute("code", failCode); + model.addAttribute("message", failMessage); + + return "main/payment/fail"; + } +} + diff --git a/src/main/java/com/t3t/frontserver/payments/controller/PaymentController.java b/src/main/java/com/t3t/frontserver/payments/controller/PaymentController.java deleted file mode 100644 index 420c5692..00000000 --- a/src/main/java/com/t3t/frontserver/payments/controller/PaymentController.java +++ /dev/null @@ -1,51 +0,0 @@ - package com.t3t.frontserver.payments.controller; - - import org.springframework.beans.factory.annotation.Value; - import org.springframework.stereotype.Controller; - import org.springframework.ui.Model; - import org.springframework.web.bind.annotation.*; - - import javax.servlet.http.HttpServletRequest; - - - - @Controller - public class PaymentController { - - @Value("${t3t.feignClient.url}") - private String feignClientUrl; - - @GetMapping("/payments") - public String index(HttpServletRequest request , - Model model) throws Exception { - - model.addAttribute("amount", 50000); - return "main/payments/checkout"; - } - - @GetMapping("payments/success") - public String paymentRequest(@RequestParam String paymentKey, - @RequestParam String orderId, - @RequestParam String amount, - Model model) throws Exception { - model.addAttribute("feignClientUrl", feignClientUrl); - model.addAttribute("paymentKey", paymentKey); - model.addAttribute("orderId", orderId); - model.addAttribute("amount", amount); - return "main/payments/success"; - } - - - @GetMapping("payments/fail") - public String failPayment(@RequestParam String failCode, - @RequestParam String failMessage, - Model model) throws Exception { - model.addAttribute("code", failCode); - model.addAttribute("message", failMessage); - - return "main/payments/fail"; - } - - - } - diff --git a/src/main/java/com/t3t/frontserver/property/RedisProperties.java b/src/main/java/com/t3t/frontserver/property/RedisProperties.java new file mode 100644 index 00000000..2bda4b89 --- /dev/null +++ b/src/main/java/com/t3t/frontserver/property/RedisProperties.java @@ -0,0 +1,18 @@ +package com.t3t.frontserver.property; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Redis 서버 접속 정보를 담는 프로퍼티 클래스 + * + * @author woody35545(구건모) + */ +@Data +@ConfigurationProperties(prefix = "t3t.redis") +public class RedisProperties { + private String redisServerIpAddress; + private String redisServerPassword; + private String redisServerPort; + private Integer redisDatabase; +} diff --git a/src/main/java/com/t3t/frontserver/shoppingcart/controller/ShoppingCartController.java b/src/main/java/com/t3t/frontserver/shoppingcart/controller/ShoppingCartController.java new file mode 100644 index 00000000..86a277db --- /dev/null +++ b/src/main/java/com/t3t/frontserver/shoppingcart/controller/ShoppingCartController.java @@ -0,0 +1,111 @@ +package com.t3t.frontserver.shoppingcart.controller; + +import com.t3t.frontserver.auth.util.SecurityContextUtils; +import com.t3t.frontserver.order.model.request.ShoppingCartOrderRequest; +import com.t3t.frontserver.shoppingcart.model.request.AddShoppingCartItemRequest; +import com.t3t.frontserver.shoppingcart.service.ShoppingCartService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.CookieValue; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletResponse; +import java.math.BigDecimal; +import java.util.UUID; + +@Slf4j +@Controller +@RequiredArgsConstructor +public class ShoppingCartController { + + private final ShoppingCartService shoppingCartService; + + /** + * 장바구니 페이지 조회 + * + * @author woody35545(구건모) + */ + @GetMapping("/shoppingcart") + public String shoppingCartView(Model model, + @CookieValue(value = "shoppingCartId", required = false) String shoppingCartId, + HttpServletResponse response) { + + if (SecurityContextUtils.isLoggedIn()) { + shoppingCartId = SecurityContextUtils.getMemberId().toString(); + + } else { + if (shoppingCartId == null || shoppingCartId.isEmpty()) { + shoppingCartId = UUID.randomUUID().toString().replace("-", ""); + Cookie cookie = new Cookie("shoppingCartId", shoppingCartId); + cookie.setHttpOnly(true); + cookie.setMaxAge(60 * 60 * 24); + response.addCookie(cookie); + } + } + + model.addAttribute("shoppingCartItemList", shoppingCartService.getShoppingCartItemList(shoppingCartId)); + model.addAttribute("shoppingCartOrderRequest", new ShoppingCartOrderRequest()); + + return "main/page/shoppingcart"; + } + + /** + * 장바구니 항목 추가 요청 처리 + * + * @author woody35545(구건모) + */ + @PostMapping("/shoppingcart") + public String addShoppingCartItem(@CookieValue(value = "shoppingCartId", required = false) String shoppingCartId, + @RequestParam("bookId") String bookId, + @RequestParam("orderQuantity") Integer orderQuantity, + @RequestParam("bookName") String bookName, + @RequestParam(value = "bookImageUrl", required = false) String bookImageUrl, + @RequestParam(value = "bookPublisherName") String bookPublisherName, + @RequestParam(value = "packageId", required = false) Long packageId, + @RequestParam(value = "price") BigDecimal price) { + + if (SecurityContextUtils.isLoggedIn()) { + shoppingCartId = SecurityContextUtils.getMemberId().toString(); + } + + shoppingCartService.addShoppingCartItem( + AddShoppingCartItemRequest.builder() + .shoppingCartId(shoppingCartId) + .bookId(bookId) + .bookName(bookName) + .bookImageUrl(bookImageUrl) + .bookPublisherName(bookPublisherName) + .quantity(orderQuantity) + .packagingId(packageId) + .packagingName("포장") + .price(price) + .build()); + + return "redirect:/shoppingcart"; + } + + + /** + * 장바구니 상품 삭제 요청 처리 + * + * @author woody35545(구건모) + */ + @PostMapping("/shoppingcart/delete") + // hidden method로 delete 호출기 바로 주문하기 버튼에서도 delete 요청이 보내져서 임시로 메서드를 POST 로 변경하고 구분을 위해 url 변경 + public String deleteShoppingCartItem(@CookieValue(value = "shoppingCartId", required = false) String shoppingCartId, + @RequestParam("bookId") String bookId) { + + if (SecurityContextUtils.isLoggedIn()) { + shoppingCartId = SecurityContextUtils.getMemberId().toString(); + } + + shoppingCartService.deleteShoppingCartItem(shoppingCartId, bookId); + + return "redirect:/shoppingcart"; + } +} diff --git a/src/main/java/com/t3t/frontserver/shoppingcart/model/entity/ShoppingCart.java b/src/main/java/com/t3t/frontserver/shoppingcart/model/entity/ShoppingCart.java new file mode 100644 index 00000000..55b79165 --- /dev/null +++ b/src/main/java/com/t3t/frontserver/shoppingcart/model/entity/ShoppingCart.java @@ -0,0 +1,54 @@ +package com.t3t.frontserver.shoppingcart.model.entity; + +import lombok.*; +import org.springframework.data.annotation.Id; +import org.springframework.data.redis.core.RedisHash; + +import java.math.BigDecimal; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * 레디스에 저장될 장바구니 정보 + * @author woody35545(구건모) + */ +@Getter +@Builder +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@RedisHash(value = "shoppingCart", timeToLive = 60 * 60 * 24) +public class ShoppingCart { + + /** + * 회원의 경우 memberId, 비회원의 경우 임의의 UUID + */ + @Id + private String id; + private Map shoppingCartItemMap = new LinkedHashMap<>(); + + @Getter + @Builder + @AllArgsConstructor(access = AccessLevel.PRIVATE) + @NoArgsConstructor(access = AccessLevel.PROTECTED) + public static class ShoppingCartItem { + private String bookId; + private String bookName; + private String bookImageUrl; + private String bookPublisherName; + private Integer quantity; + private Long packagingId; + private String packagingName; + private BigDecimal price; + } + + public void updateQuantity(String bookId, int quantityToUpdate) { + + if(!shoppingCartItemMap.containsKey(bookId)) { + return ; + } + + ShoppingCartItem shoppingCartItem = shoppingCartItemMap.get(bookId); + shoppingCartItem.quantity = quantityToUpdate; + } + +} diff --git a/src/main/java/com/t3t/frontserver/shoppingcart/model/request/AddShoppingCartItemRequest.java b/src/main/java/com/t3t/frontserver/shoppingcart/model/request/AddShoppingCartItemRequest.java new file mode 100644 index 00000000..ca52ddeb --- /dev/null +++ b/src/main/java/com/t3t/frontserver/shoppingcart/model/request/AddShoppingCartItemRequest.java @@ -0,0 +1,25 @@ +package com.t3t.frontserver.shoppingcart.model.request; + +import lombok.*; + +import java.math.BigDecimal; + +/** + * 장바구니 항목 추가 요청 객체 + * @author woody35545(구건모) + */ +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class AddShoppingCartItemRequest { + private String shoppingCartId; + private String bookId; + private String bookName; + private String bookImageUrl; + private String bookPublisherName; + private Integer quantity; + private Long packagingId; + private String packagingName; + private BigDecimal price; +} diff --git a/src/main/java/com/t3t/frontserver/shoppingcart/repository/ShoppingCartRepository.java b/src/main/java/com/t3t/frontserver/shoppingcart/repository/ShoppingCartRepository.java new file mode 100644 index 00000000..8061575e --- /dev/null +++ b/src/main/java/com/t3t/frontserver/shoppingcart/repository/ShoppingCartRepository.java @@ -0,0 +1,7 @@ +package com.t3t.frontserver.shoppingcart.repository; + +import com.t3t.frontserver.shoppingcart.model.entity.ShoppingCart; +import org.springframework.data.repository.CrudRepository; + +public interface ShoppingCartRepository extends CrudRepository { +} diff --git a/src/main/java/com/t3t/frontserver/shoppingcart/service/ShoppingCartService.java b/src/main/java/com/t3t/frontserver/shoppingcart/service/ShoppingCartService.java new file mode 100644 index 00000000..11465e42 --- /dev/null +++ b/src/main/java/com/t3t/frontserver/shoppingcart/service/ShoppingCartService.java @@ -0,0 +1,98 @@ +package com.t3t.frontserver.shoppingcart.service; + +import com.t3t.frontserver.shoppingcart.model.entity.ShoppingCart; +import com.t3t.frontserver.shoppingcart.model.request.AddShoppingCartItemRequest; +import com.t3t.frontserver.shoppingcart.repository.ShoppingCartRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +@Service +@RequiredArgsConstructor +public class ShoppingCartService { + + private final ShoppingCartRepository shoppingCartRepository; + + /** + * 장바구니에 담긴 항목 리스트을 조회한다. + * + * @auhtor woody35545(구건모) + */ + public List getShoppingCartItemList(String shoppingCartId) { + + if (!shoppingCartRepository.existsById(shoppingCartId)) { + shoppingCartRepository.save(ShoppingCart.builder() + .id(shoppingCartId) + .build()); + } + + return shoppingCartRepository.findById(shoppingCartId) + .map(ShoppingCart::getShoppingCartItemMap) + .map(Map::values) + .map(List::copyOf) + .orElseThrow(() -> new IllegalArgumentException("장바구니를 찾을 수 없습니다.")); + } + + /** + * 장바구니에 항목을 추가한다. + * + * @author woody35545(구건모) + */ + public void addShoppingCartItem(AddShoppingCartItemRequest request) { + + if (!shoppingCartRepository.existsById(request.getShoppingCartId())) { + shoppingCartRepository.save(ShoppingCart.builder() + .id(request.getShoppingCartId()) + .build()); + } + + ShoppingCart shoppingCart = shoppingCartRepository.findById(request.getShoppingCartId()) + .orElseThrow(() -> new IllegalArgumentException("장바구니를 찾을 수 없습니다.")); + + if (shoppingCart.getShoppingCartItemMap().containsKey(request.getBookId())) { + shoppingCart.updateQuantity(request.getBookId(), + shoppingCart.getShoppingCartItemMap().get(request.getBookId()).getQuantity() + request.getQuantity()); + } else { + shoppingCart.getShoppingCartItemMap().put(request.getBookId(), ShoppingCart.ShoppingCartItem.builder() + .bookId(request.getBookId()) + .quantity(request.getQuantity()) + .bookImageUrl(request.getBookImageUrl()) + .bookName(request.getBookName()) + .bookPublisherName(request.getBookPublisherName()) + .packagingId(request.getPackagingId()) + .packagingName(request.getPackagingName()) + .price(request.getPrice()) + .build()); + } + + shoppingCartRepository.save(shoppingCart); + + } + + + /** + * 장바구니에서 특정 항목을 삭제한다. + * + * @author woody35545(구건모) + */ + public void deleteShoppingCartItem(String shoppingCartId, String bookId) { + Optional shoppingCart = shoppingCartRepository.findById(shoppingCartId); + + if (shoppingCart.isPresent()) { + shoppingCart.get().getShoppingCartItemMap().remove(bookId); + shoppingCartRepository.save(shoppingCart.get()); + } + } + + /** + * 장바구니를 삭제한다. + * @author woody35545(구건모) + */ + public void deleteShoppingCart(String shoppingCartId) { + shoppingCartRepository.deleteById(shoppingCartId); + } + +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 5314b307..4cb20fc6 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -34,4 +34,10 @@ eureka: t3t: feignClient: - url: http://localhost:9090 \ No newline at end of file + url: http://localhost:9090 + + redis: + redisServerIpAddress: ${redisServerIpAddress} + redisServerPassword: ${redisServerPassword} + redisServerPort: ${redisServerPort} + redisDatabase: ${redisDatabase} \ No newline at end of file diff --git a/src/main/resources/templates/main/fragment/topbar.html b/src/main/resources/templates/main/fragment/topbar.html index 532d036d..ba05285c 100644 --- a/src/main/resources/templates/main/fragment/topbar.html +++ b/src/main/resources/templates/main/fragment/topbar.html @@ -13,7 +13,7 @@
- + diff --git a/src/main/resources/templates/main/page/detail.html b/src/main/resources/templates/main/page/detail.html index e8183886..87aa9006 100644 --- a/src/main/resources/templates/main/page/detail.html +++ b/src/main/resources/templates/main/page/detail.html @@ -19,10 +19,13 @@