일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- 멋쟁이사자처럼대학
- 멋쟁이사자처럼 서류
- 멋사12
- 멋사10기
- IT동아리
- API
- 파이썬
- 알림봇
- 멋쟁이사자처럼10기
- 웹동아리
- 크롤링
- ㅏㄴ
- 깃허브
- 디스코드봇
- 파이썬 크롤링
- 멋사 면접
- 백엔드
- discord
- 멋사 10기
- 멋사
- 멋쟁이 사자처럼
- 멋사 서류평가
- 멋사 서류
- 기사 제목 크롤링
- 코딩동아리
- 멋사 합격
- 멋쟁이사자처럼11기
- 멋사11기
- django
- 멋쟁이사자처럼
- Today
- Total
ACHO.pk devlog
[Springboot] 상품 주문 기능 구현하기 본문
1. 주문 기능 구현하기
고객이 상품을 주문하면 현재 상품의 재고에서 주문 수량만큼 재고를 감소시켜야한다. 고객이 주문을 했는데 실제 재고가 없다면 배송을 하지 못하고 결품 처리가 되기 때문에 주문 수량만큼 상품의 재고를 감소시켜야한다.
상품의 주문 수량보다 재고의 수가 적을 때 발생시킬 exxeption을 정의해보자.
com.shop.shoop > exception패키지 생성 > OutOfStockException
package com.shop.shop.exception;
public class OutOfStockException extends RuntimeException{
public OutOfStockException(String message) {
super(message);
}
}
상품을 주문할 경우 상품의 재고를 감소시키는 로직을 작성해보자. 엔티티 클래스 안에 비즈니스 로직을 메소드로 작성하면 코드의 재사용과 데이터의 변경 포인트를 한군데로 모을 수 있다는 장점이 있다.
entity > Item
package com.shop.shop.entity;
import com.shop.shop.constant.ItemSellStatus;
import com.shop.shop.dto.ItemFormDto;
import com.shop.shop.exception.OutOfStockException;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import javax.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table(name="item")
@Getter
@Setter
@ToString
public class Item extends BaseEntity{
...생략...
public void removeStock(int stockNumber){
int restStock = this.stockNumber - stockNumber;
if(restStock<0){
throw new OutOfStockException("상품의 재고가 부족 합니다. (현재 재고 수량: " + this.stockNumber + ")");
}
this.stockNumber = restStock;
}
}
▹상품의 재고 수량에서 주문 후 남은 재고 수량을 구한다.
int restStock = this.stockNumber - stockNumber;
▹상품의 재고가 주문 수량보다 작을 경우 재고 부족 예외를 발생시킨다.
throw new OutOfStockException("상품의 재고가 부족 합니다. (현재 재고 수량: " + this.stockNumber + ")");
▹주문 후 남은 재고 수량을 상품의 현재 재고 값으로 할당한다.
this.stockNumber = restStock;
주문할 상품과 주문 수량을 통해 OrderItem 객체를 만드는 메소드를 작성해보자
entity > OrderItem
package com.shop.shop.entity;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
import java.time.LocalDateTime;
@Entity
@Getter
@Setter
public class OrderItem extends BaseEntity{
...생략...
public static OrderItem createOrderItem(Item item, int count){
OrderItem orderItem = new OrderItem();
orderItem.setItem(item);
orderItem.setCount(count);
orderItem.setOrderPrice(item.getPrice());
item.removeStock(count);
return orderItem;
}
public int getTotalPrice(){
return orderPrice*count;
}
}
▹주문할 상품과 주문 수량을 세팅한다.
orderItem.setItem(item);
orderItem.setCount(count);
▹현재 시간 기준으로 상품 가격을 주문 가격으로 세팅한다. 상품 가격은 시간에 따라서 달라질 수 있다. 또한, 쿠폰이나 할인을 적용하는 케이스들도 있지만 여기서는 고려하지 않았다.
orderItem.setOrderPrice(item.getPrice());
▹주문 수량만큼 상품의 재고 수량을 감소시킨다.
item.removeStock(count);
▹주문 가격과 주문 수량을 곱해서 해당 상품을 주문한 총 가격을 계산하는 메소드이다.
public int getTotalPrice(){
생성한 주문 상품 객체를 이용하여 주문 객체를 만드는 메소드를 작성해보자.
entity > Order
package com.shop.shop.entity;
import com.shop.shop.constant.OrderStatus;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Entity
@Table(name = "orders")
@Getter
@Setter
public class Order extends BaseEntity{
...생략...
public void addOrderItem(OrderItem orderItem) {
orderItems.add(orderItem);
orderItem.setOrder(this);
}
public static Order createOrder(Member member, List<OrderItem> orderItemList) {
Order order = new Order();
order.setMember(member);
for(OrderItem orderItem : orderItemList) {
order.addOrderItem(orderItem);
}
order.setOrderStatus(OrderStatus.ORDER);
order.setOrderDate(LocalDateTime.now());
return order;
}
public int getTotalPrice() {
int totalPrice = 0;
for(OrderItem orderItem : orderItems){
totalPrice += orderItem.getTotalPrice();
}
return totalPrice;
}
}
▹orederitems에는 주문 상품 정보들을 담아준다. orderItem 객체를 order 객체의 orderItems에 추가한다.
public void addOrderItem(OrderItem orderItem) {
▹Order 엔티티와 OrderItem 엔티티가 양방향 참조 관계이므로, orderItem 객체에도 order 객체를 세팅한다.
orderItem.setOrder(this);
▹상품을 주문한 회원의 정보를 세팅한다.
order.setMember(member);
▹상품 페이지에서는 1개의 상품을 주문하지만, 장바구니 페이지에서는 한 번에 여러 개의 상품을 주문할 수 있다. 따라서 여러 개의 주문 상품을 담을 수 있도록 리스트 형태로 파라미터 값을 받으며 주문 객체에 orderitem 객체를 추가한다.
for(OrderItem orderItem : orderItemList) {
▹주문 상태를 "ORDER"로 세팅한다.
order.setOrderStatus(OrderStatus.ORDER);
▹현재 시간을 주문 시간으로 세팅한다.
order.setOrderDate(LocalDateTime.now());
▹총 주문 금액을 구하는 메소드이다.
public int getTotalPrice() {
상품과 주문, 주문 상품 엔티티에 주문과 관련된 비즈니스 로직들을 추가한다.
상품 상세 페이지에서 주문할 상품의 아이디와 주문 수량을 전달받을 OrderDto 클래스를 만들자.
주문 최소 수량은 1개, 주문 최대 수량은 999개로 제한한다.
dto > OrderDto
package com.shop.shop.dto;
import lombok.Getter;
import lombok.Setter;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
@Getter
@Setter
public class OrderDto {
@NotNull(message = "상품 아이디는 필수 입력 값입니다.")
private Long itemId;
@Min(value = 1, message = "최소 주문 수량은 1개 입니다.")
@Max(value = 999, message = "최대 주문 수량은 999개 입니다.")
private int count;
}
주문 로직을 작성하기 위해 OrderService 클래스를 만든다.
service > OrderService
package com.shop.shop.service;
import com.shop.shop.dto.OrderDto;
import com.shop.shop.entity.Item;
import com.shop.shop.entity.Member;
import com.shop.shop.entity.Order;
import com.shop.shop.entity.OrderItem;
import com.shop.shop.repository.ItemImgRepository;
import com.shop.shop.repository.ItemRepository;
import com.shop.shop.repository.MemberRepository;
import com.shop.shop.repository.OrderRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import javax.persistence.EntityNotFoundException;
import javax.transaction.Transactional;
import java.util.ArrayList;
import java.util.List;
@Service
@Transactional
@RequiredArgsConstructor
public class OrderService {
private final ItemRepository itemRepository;
private final MemberRepository memberRepository;
private final OrderRepository orderRepository;
private final ItemImgRepository itemImgRepository;
public Long order(OrderDto orderDto, String email){
Item item = itemRepository.findById(orderDto.getItemId())
.orElseThrow(EntityNotFoundException::new);
Member member = memberRepository.findByEmail(email);
List<OrderItem> orderItemList = new ArrayList<>();
OrderItem orderItem = OrderItem.createOrderItem(item, orderDto.getCount());
orderItemList.add(orderItem);
Order order = Order.createOrder(member, orderItemList);
orderRepository.save(order);
return order.getId();
}
}
▹주문할 상품을 조회한다.
Item item = itemRepository.findById(orderDto.getItemId())
▹현재 로그인한 회원의 이메일 정보를 이용해서 회원 정보를 조회한다.
Member member = memberRepository.findByEmail(email);
▹주문할 상품 엔티티와 주문 수량을 이용하여 주문 상품 엔티티를 생성한다.
OrderItem orderItem = OrderItem.createOrderItem(item, orderDto.getCount());
▹회원 정보와 주문할 상품 리스트 정보를 이용하여 주문 엔티티를 생성한다.
Order order = Order.createOrder(member, orderItemList);
▹생성한 주문 엔티티를 저장한다.
orderRepository.save(order);
주문 관련 요청들을 처리하기 위해서 OrderController 클래스를 만들어보자. 상품 주문에서 웹페이지의 새로 고침 없이 서버에 주문을 요청하기 위해서 비동기 방식을 사용한다.
controller > OrderController
package com.shop.shop.controller;
import com.shop.shop.dto.OrderDto;
import com.shop.shop.service.OrderService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.validation.Valid;
import java.security.Principal;
import java.util.List;
@Controller
@RequiredArgsConstructor
public class OrderController {
private final OrderService orderService;
@PostMapping(value = "/order")
public @ResponseBody ResponseEntity order(@RequestBody @Valid OrderDto orderDto
, BindingResult bindingResult, Principal principal){
if(bindingResult.hasErrors()){
StringBuilder sb = new StringBuilder();
List<FieldError> fieldErrors = bindingResult.getFieldErrors();
for (FieldError fieldError : fieldErrors) {
sb.append(fieldError.getDefaultMessage());
}
return new ResponseEntity<String>(sb.toString(), HttpStatus.BAD_REQUEST);
}
String email = principal.getName();
Long orderId;
try {
orderId = orderService.order(orderDto, email);
} catch(Exception e){
return new ResponseEntity<String>(e.getMessage(), HttpStatus.BAD_REQUEST);
}
return new ResponseEntity<Long>(orderId, HttpStatus.OK);
}
}
▹스프링에서 비동기 처리를 할 때 @RequestBody와 @ResponseBody 어노테이션을 사용한다.
- @RequestBody : HTTP 요청의 본문 body에 담긴 내용을 자바 객체로 전달
- @ResponseBody : 자바 객체를 HTTP 요청의 body로 전달
public @ResponseBody ResponseEntity order(@RequestBody @Valid OrderDto orderDto
, BindingResult bindingResult, Principal principal){
▹주문 정보를 받는 orderDto 객체에 데이터 바인딩 시 에러가 있는지 검사한다.
if(bindingResult.hasErrors()){
▹에러 정보를 ResponseEntity 객체에 담아서 반환한다.
return new ResponseEntity<String>(sb.toString(), HttpStatus.BAD_REQUEST);
▹현재 로그인 유저의 정보를 얻기 위해서 @Controller 어노테이션이 선언된 클래스에서 메소드 인자로 principal 객체를 넘겨 줄 경우 해당 객체에 직접 접근할 수 있다. principal 객체에서 현재 로그인한 회원의 이메일 정보를 조회한다.
String email = principal.getName();
▹화면으로부터 넘어오는 주문 정보와 회원의 이메일 정보를 이용하여 주문 로직을 호출한다.
orderId = orderService.order(orderDto, email);
▹결과값으로 생성된 주문 번호와 요청이 성공했다는 HTTP 응답 상태 코드를 반환한다.
return new ResponseEntity<Long>(orderId, HttpStatus.OK);
1-1. 테스트 코드 작성하기
package com.shop.shop.service;
import com.shop.shop.constant.ItemSellStatus;
import com.shop.shop.dto.OrderDto;
import com.shop.shop.entity.Item;
import com.shop.shop.entity.Member;
import com.shop.shop.entity.Order;
import com.shop.shop.entity.OrderItem;
import com.shop.shop.repository.ItemRepository;
import com.shop.shop.repository.MemberRepository;
import com.shop.shop.repository.OrderRepository;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.TestPropertySource;
import javax.persistence.EntityNotFoundException;
import javax.transaction.Transactional;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
@Transactional
@TestPropertySource(locations="classpath:application-test.properties")
class OrderServiceTest {
@Autowired
private OrderService orderService;
@Autowired
private OrderRepository orderRepository;
@Autowired
ItemRepository itemRepository;
@Autowired
MemberRepository memberRepository;
public Item saveItem(){
Item item = new Item();
item.setItemNm("테스트 상품");
item.setPrice(10000);
item.setItemDetail("테스트 상품 상세 설명");
item.setItemSellStatus(ItemSellStatus.SELL);
item.setStockNumber(100);
return itemRepository.save(item);
}
public Member saveMember(){
Member member = new Member();
member.setEmail("test@test.com");
return memberRepository.save(member);
}
@Test
@DisplayName("주문 테스트")
public void order(){
Item item = saveItem();
Member member = saveMember();
OrderDto orderDto = new OrderDto();
orderDto.setCount(10);
orderDto.setItemId(item.getId());
Long orderId = orderService.order(orderDto, member.getEmail());
Order order = orderRepository.findById(orderId)
.orElseThrow(EntityNotFoundException::new);
List<OrderItem> orderItems = order.getOrderItems();
int totalPrice = orderDto.getCount()*item.getPrice();
assertEquals(totalPrice, order.getTotalPrice());
}
}
▹테스트를 위해서 주문할 상품과 회원 정보를 저장하는 메소드를 생성한다.
public Item saveItem(){
public Member saveMember(){
▹주문할 상품과 상품 수량을 orderDto 객체에 세팅한다.
orderDto.setCount(10);
orderDto.setItemId(item.getId());
▹주문 로직 호출 결과 생성된 주문 번호를 orderId 변수에 저장한다.
Long orderId = orderService.order(orderDto, member.getEmail());
▹주문 번호를 이용하여 저장된 주문 정보를 조회한다.
Order order = orderRepository.findById(orderId)
▹주문한 상품의 총 가격을 구한다.
int totalPrice = orderDto.getCount()*item.getPrice();
▹주문한 상품의 총 가격과 데이터베이스에 저장된 상품의 가격을 비교하여 같으면 테스트가 성공적으로 종료된다.
assertEquals(totalPrice, order.getTotalPrice());
실행 결과 주문 로직이 정상 동작함을 확인할 수 있다.
1-2. 상품 상세 페이지에서 주문 로직 호출
form 태그를 사용하여 submit 방식으로 서버에 요청하게 되면 페이지가 새로 고침이 된다는 단점이 있다. Ajax를 이용하여 주문 로직을 비동기 방식으로 호출해보자. 비동기 방식을 사용하면 웹 페이지의 새로 고침 없이 필요한 부분만 불러와 사용할 수 있다는 장점이 있다.
templates > item > itemDtl.html
https://github.com/roadbook2/shop/blob/master/src/main/resources/templates/item/itemDtl.html
▹스프링 시큐리티를 사용할 경우 기본적으로 POST 방식의 데이터 전송에는 CSRF 토큰 값이 필요하므로 해당 값들을 조회한다.
var token = $("meta[name='_csrf']").attr("content");
var header = $("meta[name='_csrf_header']").attr("content");
▹주문할 상품의 아이디와 주문 수량 데이터를 전달할 객체를 생성한다.
var paramData = {
▹서버에 보낼 주문 데이터를 json 으로 변경한다.
var param = JSON.stringify(paramData);
▹서버에 데이터를 보낼 형식을 json 으로 지정한다.
contentType : "application/json",
▹서버에서 결과값으로 받을 데이터의 타입을 json으로 설정한다.
dataType : "json",
▹주문 로직 호출이 성공하면 "주문이 완료되었습니다." 라는 메시지를 보여주고 메인 페이지로 이동한다.
success : function(result, status){
▹현재 로그인 상태가 아니라면 "로그인 후 이용해주세요." 메시지를 보여주고 로그인 페이지로 이동한다.
if(jqXHR.status == '401'){
▹주문 시 에러가 발생하면 해당 메시지를 보여준다.
alert(jqXHR.responseText);
주문하기 버튼을 클릭하면 주문이 완료됐다는 메시지가 나타나고 확인을 클릭하면 메인 페이지로 이동한다.
'프레임워크 > Springboot' 카테고리의 다른 글
[Springboot] 장바구니 기능 (0) | 2023.03.27 |
---|---|
[Springboot] 주문 이력 조회 및 주문 취소하기 (0) | 2023.03.21 |
[Springboot] 등록 상품 메인 화면에서 보기 / 상품 상세 페이지 (0) | 2023.03.15 |
[Springboot] 상품 관리하기 (2) | 2023.03.14 |
[Springboot] 상품 수정하기 (0) | 2023.03.13 |