- 고객이 상품을 주문하면 현재 상품의 재고에서 주문 수량만큼 재고를 감소시켜야 한다.
1. 주문만큼 재고를 감소
2. 주문 수량보다 재고의 수가 적을 때 발생시킬 exception(RuntimeException) 정의
- 예외 클래스 작성
- OutofStockException.java
package com.shop.exception;
public class OutOfStockException extends RuntimeException{
// 상품의 주문 수량보다 재고의 수가 적을 때 발생시킬 exception
public OutOfStockException(String message) {
super(message);
}
}
- 재고 감소 로직
- Item.java
...
//남은 재고 수량 확인
public void removeStock(int stockNumber) {
int restStock = this.stockNumber - stockNumber;
//1. 상품의 재고 수량에서 주문 후 남은 재고 수량을 구한다.
if (restStock < 0) {
throw new OutOfStockException("상품의 재고가 부족 합니다." +
"(현재 재고 수량 : " + this.stockNumber + ")");
//2. 상품의 재고가 주문 수량보다 작을 경우 재고 부족 예외를 발생시킨다.
}
this.stockNumber = restStock;
//3. 주문 후 남은 재고 수량을 상품의 현재 재고 값으로 할당한다.
}
- this.stockNumber : DB에서 DTO로 넘어온 해당 Item 엔티티의 남은 재고에서 stockNumber : 주문 수량
- Item 엔티티에서 메서드 파라미터로 넘어온 주문 수량만큼 빼주는 메서드이다.
- 주문 수량만큼 뺀 다음 해당 결과가 0보다 적으면 재고가 부족한 것이기 때문에 예외를 발생시킨다.
- 주문할 상품과 주문 수량을 통해 OrderITem 객체를 만드는 메소드를 작성한다.
- OrderItem.java
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;
}
- 주문 제품 객체를 새롭게 생성하고 item 엔티티에 저장된 해당 제품에 대한 정보들을 저장한다.
- 또, 주문된 수량만큼 재고를 줄이는 위에서 작성한 item 클래스의 removeStock을 실행 시킨다.
- 생성한 주문 상품 객체를 이용하여 주문 객체를 만드는 메소드를 작성한다.
- Order.java
.....
public void addOrderItem(OrderItem orderItem) {
//1. orderItems에는 주문 상품 정보들을 담아준다.
// orderItem 객체를 order 객체의 orderItems에 추가한다.
orderItems.add(orderItem);
orderItem.setOrder(this);
//2. Order 엔티티와 OrderItem 엔티티가 양방향 참조 관계 이므로,
//orderItem 객체에도 order 객체를 세팅한다.
}
public static Order createOrder(Member member, List<OrderItem> orderItemList) {
Order order = new Order();
order.setMember(member);
//3. 상품을 주문한 회원의 정보를 세팅한다.
for (OrderItem orderItem : orderItemList) {
order.addOrderItem(orderItem);
//4. 상품 페이지에서는 1개의 상품을 주문하지만, 장바구니 페이지에서는
//한 번에 여러 개의 상품을 주문 할 수 있다.
//따라서 여러 개의 주문 상품을 담을 수 있도록 리스트 형태로
//파라미터 값을 받으며 주문 객체에 orderItem 객체를 추가한다.
}
order.setOrderStatus(OrderStatus.ORDER);
//5. 주문 상태를 "ORDER"로 세팅한다.
order.setOrderDate(LocalDateTime.now());
//6. 현재 시간을 주문 시간으로 세팅한다.
return order;
}
public int getTotalPrice() {
//7. 총 주문 금액을 구하는 메소드이다.
int totalPrice = 0;
for (OrderItem orderItem : orderItems) {
totalPrice += orderItem.getTotalPrice();
}
return totalPrice;
}
public void addOrderItem(OrderItem orderItem) {
//1. orderItems에는 주문 상품 정보들을 담아준다.
// orderItem 객체를 order 객체의 orderItems에 추가한다.
orderItems.add(orderItem);
orderItem.setOrder(this);
//2. Order 엔티티와 OrderItem 엔티티가 양방향 참조 관계 이므로,
//orderItem 객체에도 order 객체를 세팅한다.
}
- 일대다 @OneToMany 관계 있는 orderItems 리스트에 주문이 들어온 제품을 추가한다.
- 또한 양방향 관계에 있기 때문에 orderItem의 주문에도 현재 객체를 넣어 준다.
public static Order createOrder(Member member, List<OrderItem> orderItemList) {
Order order = new Order();
order.setMember(member);
//3. 상품을 주문한 회원의 정보를 세팅한다.
for (OrderItem orderItem : orderItemList) {
order.addOrderItem(orderItem);
//4. 상품 페이지에서는 1개의 상품을 주문하지만, 장바구니 페이지에서는
//한 번에 여러 개의 상품을 주문 할 수 있다.
//따라서 여러 개의 주문 상품을 담을 수 있도록 리스트 형태로
//파라미터 값을 받으며 주문 객체에 orderItem 객체를 추가한다.
}
order.setOrderStatus(OrderStatus.ORDER);
//5. 주문 상태를 "ORDER"로 세팅한다.
order.setOrderDate(LocalDateTime.now());
//6. 현재 시간을 주문 시간으로 세팅한다.
return order;
}
- createOrder 메서드는 주문을 생성하는 메서드이고, 어떤 회원이 시켰는지(member)를 넣고, 주문 상품 리스트를 받아오고 위에서 작성한 addOrderItem 메서드를 실행함으로서 주문 상품을 리스트에 집어넣는다.
- 주문이 들어온 것이기 때문에 주문 상태를 ORDER를 바꾼다.
- 해당 메서드가 실행된 시간을 주문한 시간으로 지정한다.
- Order 객체에 데이터를 입력한 다음 해당 객체를 반환한다.
public int getTotalPrice() {
//7. 총 주문 금액을 구하는 메소드이다.
int totalPrice = 0;
for (OrderItem orderItem : orderItems) {
totalPrice += orderItem.getTotalPrice();
}
return totalPrice;
}
- 주문의 총 가격 : (각 제품의 가격 * 수랑)들을 전부 더한 결과이다.
- orderItems 리스트를 돌면서 각 주문 제품의 TotalPrice를 가져오고 모든 제품에 대한 가격을 더한다.
- 상품 상세 페이지에서 주문할 상품의 아이디와 주문 수량을 전달받을 OrderDto 클래스를 생성한다.
- OrderDto.java
@Getter
@Setter
public class OrderDto {
@NotNull(message = "상품 아이디는 필수 입력 값입니다.")
private Long itemId;
@Min(value = 1, message = "최소 주문 수량은 1개 입니다.")
@Max(value = 999, message = "최대 주문 수량은 999개 입니다.")
private int count;
}
- OrderService 클래스에 주문 로직을 작성한다.
- OrderService.java
@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);
//1. 주문할 상품을 조회한다.
Member member = memberRepository.findByEmail(email);
//2. 현재 로그인한 회원의 이메일 정보를 이용해서 회원 정보를 조회한다.
List<OrderItem> orderItemList = new ArrayList<>();
OrderItem orderItem = OrderItem.createOrderItem(item, orderDto.getCount());
//3. 주문할 상품 엔티티와 주문 수량을 이용하여 주문 상품 엔티티를 생성한다.
orderItemList.add(orderItem);
Order order = Order.createOrder(member, orderItemList);
//4. 회원 정보와 주문할 상품 리스트 정보를 이용하여 주문 엔티티를 생성한다.
orderRepository.save(order);
//5. 생성한 주문 엔티티를 저장한다.
return order.getId();
}
- itemRepository에서 Dto로 넘어온 id값을 통해 item을 찾아온다.
- memberRepository에서 email을 기반으로 member를 찾아온다.
- 찾아온 item과 주문 수량을 통해 orderItemList에 orderItem을 엔티티를 추가한다.
- Order 엔티티의 createOrder 메서드를 통해 주문을 생성하고, orderRepository에 저장한다.
- order 메서드의 반환값으로 해당 orderId를 반환한다.
- 주문 관련 요청들을 처리하기 위해 OrderController 클래스를 생성한다.
- 상품 주문에서 웹 페이지의 새로 고침 없이 서버에 주문을 요청하기 위해서 비동기 방식을 이용한다.
- OrderController.java
@Controller
@RequiredArgsConstructor
public class OrderController {
private final OrderService orderService;
@PostMapping(value = "/order")
public @ResponseBody ResponseEntity order(@RequestBody @Valid OrderDto orderDto,
BindingResult bindingResult, Principal principal) {
//1. 스프링에서 비동기 처리를 할 때 @RequestBody와 @ResponseBody 어노테이션을 사용한다.
//@RequestBody : HTTP 요청의 본문 body에 담긴 내용을 자바 객체로 전달
//@ResponseBody : 자바 객체를 HTTP 요청의 body로 전달
if (bindingResult.hasErrors()) {
//2. 주문 정보를 받는 orderDto 객체에 데이터 바인딩 시 에러가 있는지 검사한다.
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);
//3. 에러 정보를 ResponseEntity 객체에 담아서 반환한다.
}
String email = principal.getName();
//4. 현재 로그인 유저의 정보를 얻기 위해서 @Controller 어노테이션이 선언된 클래스에서
// 메소드 인자로 principal 객체를 넘겨 줄 경우 해당 객체에 직접 접근할 수 있다.
// principal 객체에서 현재 로그인한 회원의 이메일 정보를 조회한다.
Long orderId;
try {
orderId = orderService.order(orderDto, email);
//5. 화면으로부터 넘어오는 주문 정보와 회원의 이메일 정보를 이용하여 주문 로직을 호출한다.
} catch (Exception e) {
return new ResponseEntity<String>(e.getMessage(),
HttpStatus.BAD_REQUEST);
}
return new ResponseEntity<Long>(orderId, HttpStatus.OK);
//6. 결과값으로 생성된 주문 번호와 요청이 성공했다는 HTTP 응답 상태 코드를 반환한다.
}
- @PostMapping임과 동시에 @ResponseBody ResponseEntity를 통해 body로 넘어온 데이터를 받을 수 있고, 또한 HTTP 응답의 상태 코드, 헤더 및 본문 데이터를 정의하는데 사용된다.
- Principal
1. 현재 인증된 사용자의 정보를 나타낸다.
2. Spring Security와 함께 Spring Web 요청에서 해당 인자를 포함시키면 자동으로 현재 인증된 사용자 정보가 주입된다.
- bindingResult를 통해 에러가 발생하면 Builder 패턴을 통해 에러에 대한 객체를 생성한다.
- orderService의 order에서드는 Dto, email을 기반으로 orderitem을 찾아서 order를 생성하고 해당 orderId를 반환한다.
- 각 상황에 맞게 ResponseEntity 객체에 HTTP 상태 코드를 주입해서 객체를 생성해준다.
- 상품 상세 페이지에서 구현한 주문 로직을 호출하는 코드를 작성해야 한다.
- form 태그를 사용하여 submit 방식으로 서버에 요청하게 되면 페이지가 새로 고침 된다는 단점이 있다.
- Ajax를 이용하여 주문 로직을 비동기 방식으로 호출한다. 비동기 방식을 사용하면 웹 페이지의 새로 고침 없이 필요한 부분만 불러와 사용할 수 있다는 장점이 있다.
- itemDtl.html
function order(){
var token = $("meta[name='_csrf']").attr("content");
var header = $("meta[name='_csrf_header']").attr("content");
//1,2. 스프링 시큐리티를 사용할 경우 기본적으로 POST 방식의 데이터 전송에는
//CSRF 토큰 값이 필요하므로 해당 값들을 조회한다.
var url = "/order";
var paramData = {
//3. 주문할 상품의 아이디와 주문 수량 데이터를 전달할 객체를 생성한다.
itemId : $("#itemId").val(),
count : $("#count").val()
};
var param = JSON.stringify(paramData);
//4. 서버에 보낼 주문 데이터를 json으로 변경한다.
$.ajax({
url : url,
type : "POST",
contentType : "application/json",
//5. 서버에 데이터를 보낼 형식을 json으로 지정한다.
data : param,
beforeSend : function(xhr){
/* 데이터를 전송하기 전에 헤더에 csrf값을 설정 */
xhr.setRequestHeader(header, token);
},
dataType : "json",
//6. 서버에서 결과값으로 받을 데이터의 타입을 json으로 설정한다.
cache : false,
success : function(result, status){
//7. 주문 로직 호출이 성공하면 "주문이 완료되었습니다"라는
//메시지를 보여주고 메인 페이지로 이동
alert("주문이 완료 되었습니다.");
location.href='/';
},
error : function(jqXHR, status, error){
if(jqXHR.status == '401'){
//8. 현재 로그인 상태가 아니라면, "로그인 후 이용해주세요"
//라는 메시지를 보여주고 로그인 페이지로 이동
alert('로그인 후 이용해주세요');
location.href='/members/login';
} else{
alert(jqXHR.responseText);
//9. 주문 시 에러가 발생하면 해당 메시지를 보여준다.
}
}
});
}
var token = $("meta[name='_csrf']").attr("content");
var header = $("meta[name='_csrf_header']").attr("content");
- 스프링 시큐리티를 사용할 경우 기본적으로 POST 방식의 데이터 전송에는 CSRF 토큰 값이 필요하므로 해당 값들을 조회한다.
var url = "/order";
var paramData = {
//3. 주문할 상품의 아이디와 주문 수량 데이터를 전달할 객체를 생성한다.
itemId : $("#itemId").val(),
count : $("#count").val()
};
...
<input type="hidden" id="itemId" th:value="${item.id}">
...
<input type="number" name="count" id="count" class="form-control" value="1" min="1">
- id가 각 itemId, count에 해당하는 값들을 가져온다.
var param = JSON.stringify(paramData);
//4. 서버에 보낼 주문 데이터를 json으로 변경한다.
- Post 방식으로 body에 json 방식으로 보내기 위해 var 변수에 작성된 데이터들을 json 방식으로 변환시켜준다.
$.ajax({
url : url,
type : "POST",
contentType : "application/json",
//5. 서버에 데이터를 보낼 형식을 json으로 지정한다.
data : param,
beforeSend : function(xhr){
/* 데이터를 전송하기 전에 헤더에 csrf값을 설정 */
xhr.setRequestHeader(header, token);
},
dataType : "json",
//6. 서버에서 결과값으로 받을 데이터의 타입을 json으로 설정한다.
cache : false,
success : function(result, status){
//7. 주문 로직 호출이 성공하면 "주문이 완료되었습니다"라는
//메시지를 보여주고 메인 페이지로 이동
alert("주문이 완료 되었습니다.");
location.href='/';
},
error : function(jqXHR, status, error){
if(jqXHR.status == '401'){
//8. 현재 로그인 상태가 아니라면, "로그인 후 이용해주세요"
//라는 메시지를 보여주고 로그인 페이지로 이동
alert('로그인 후 이용해주세요');
location.href='/members/login';
} else{
alert(jqXHR.responseText);
//9. 주문 시 에러가 발생하면 해당 메시지를 보여준다.
}
}
});
- 비동기 방식을 이용하기 위해 ajax를 이용한다.
- ajax는 XMLHttpRequest객체를 통해 서버에 request한다. 위의 코드와 같이 헤더에 입력해야 하는 정보들을 입력하여 필요한 정보만 서버에 보내고 받을 수 있다.
- url, type, contentType 등 정보를 입력하고, 성공과 실패시 어떤 함수를 호출할지에 대한 코드들을 작성했다.
- 위 코드의 경우에는 성공 시 /(메인 페이지)로 이동하고 실패하게 되면 (로그인을 하지 않은 상태) 로그인 페이지로 이동 시킨다.
'Portfolio, Project > Project(Programming)' 카테고리의 다른 글
Project (6-3) 주문 취소하기 (0) | 2023.08.16 |
---|---|
Project (6-2) 주문 이력 조회하기 (0) | 2023.08.15 |
Project (5-3) 메인 화면, 상품 상세 페이지 (1) (추가 내용 작성 필요할 듯...) (0) | 2023.08.11 |
Project (5-2) 상품 관리 (1) (0) | 2023.08.11 |
Project (5-1) 상품 등록 및 수정 (2) (0) | 2023.08.09 |