public class OutOfStockException extends RuntimeException {
public OutOfStockException(String message){
super(message);
}
}
public void removeStock(int stockNumber){
int restStock = this.stockNumber - stockNumber;
if(restStock <0){
throw new OutOfStockException("상품의 재고가 부족 합니다. (현재 재고 수량 : "+this.stockNumber+")");
}
this.stockNumber = restStock;
}
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;
}
public void addOrderItem(OrderItem orderItem){
orderItems.add(orderItem);
orderItem.setOrder(this); // Order와 OrderItem은 양방향 참조이므로 둘 다 세팅해줌
}
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;
}
@Getter @Setter
public class OrderDto {
@NotNull(message = "상품 아이디는 필수 입력 값입니다.")
private Long itemId;
@Min(value = 1, message = "최소 주문 수량은 1개입니다.")
@Max(value = 999, message = "최대 주문 수량은 999개 입니다.")
private int count;
}
@Service
@Transactional
@RequiredArgsConstructor
public class OrderService {
private final ItemRepository itemRepository;
private final MemberRepository memberRepository;
private final OrderRepository orderRepository;
private final ItemImgRepository itemImgRepository;
/*
1. orderItemList를 만든다.
2. orderItem을 만들어서 List에 추가
3. Order를 만들고 해당 List를 추가
4. Order의 id 반환
*/
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();
}
비동기 방식으로 만들기 위해 @RequestBody
, @ResponseBody
사용
@PostMapping(value = "/order")
public @ResponseBody ResponseEntity order(@RequestBody @Valid OrderDto orderDto, BindingResult bindingResult,
Principal principal){
// 결과의 각 필드의 에러를 StringBuilder로 모은 다음, ResponseEntity로 에러메시지와, Http의 상태를 한번에 전송한다.
if(bindingResult.hasErrors()) {
StringBuilder sb = new StringBuilder(); // StringBuilder : 변경할 수 있는 문자열이고, append를 통해 문자열을 추가하고, toString으로 String 객체를 반환
List<FieldError> fieldErrors = bindingResult.getFieldErrors();
for (FieldError fieldError : fieldErrors) {
sb.append(fieldError.getDefaultMessage());
}
return new ResponseEntity<String>(sb.toString(), HttpStatus.BAD_REQUEST); // 사용자의 HttpRequest에 대한 응답 데이터를 포함하는 클래스
}
String email = principal.getName(); // Authentication의 최상위 인터페이스로 현재 접속 유저의 정보를 가지고 있음
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
: HttpRequest의 본문 body에 담긴 내용을 자바 객체로 전달@ResponseBody
: 자바 객체를 HTTPRequest의 body로 전달
상품 상세 뷰에 코드를 수정한다.
function order(){
// POST 방식은 CSRF 토큰이 필요
var token = $("meta[name='_csrf']").attr("content");
var header = $("meta[name='_csrf_header']").attr("content");
var url = "/order";
// 전달 받은 정보를 객체로 만듬
var paramData = {
itemId :
// 서버에 보낼 주문 정보를 JSON으로 변환
var param = JSON.stringify(paramData);
$.ajax({
url : url,
type : "POST",
contentType : "application/json",
data : param,
beforeSend : function(xhr){
/* 데이터를 전송하기 전에 헤더에 csrf값을 설정 */
xhr.setRequestHeader(header, token);
},
dataType : "json",
cache : false,
success : function(result, status){
alert("주문이 완료 되었습니다.");
location.href='/';
},
error : function(jqXHR, status, error){
// http 오류 번호를 반환
if(jqXHR.status == '401'){
alert('로그인 후 이용해주세요');
location.href='/members/login';
} else{
// url의 full response를 반환
alert(jqXHR.responseText);
}
}
});
주문 목록 및 주문 취소 기능 추가
- Order Repository에서 조회하는 쿼리
- 조회한 정보를 저장할 Order 정보 객체
- Order에 있는 Order_Item을 화면에 보여줄 DTO
- Order_Service에 Order 목록을 조회해서, 페이지 구현 객체로 반환
- Order 조회 메서드를 통해, 뷰에 정보 전달
- Order 목록 페이지 생성
@Getter @Setter
public class OrderItemDto {
public OrderItemDto(OrderItem orderItem, String imgUrl){
this.itemNm = orderItem.getItem().getItemNm();
this.count = orderItem.getCount();
this.orderPrice = orderItem.getOrderPrice();
this.imgUrl = imgUrl;
}
private String itemNm; //상품명
private int count; //주문 수량
private int orderPrice; //주문 금액
private String imgUrl; //상품 이미지 경로
@Getter @Setter
public class OrderHistDto {
public OrderHistDto(Order order){
this.orderId = order.getId();
this.orderDate = order.getOrderDate().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"));
this.orderStatus = order.getOrderStatus();
}
private Long orderId; //주문아이디
private String orderDate; //주문날짜
private OrderStatus orderStatus; //주문 상태
private List<OrderItemDto> orderItemDtoList = new ArrayList<>(); //주문 상품리스트
public void addOrderItemDto(OrderItemDto orderItemDto){
orderItemDtoList.add(orderItemDto);
}
}
public interface OrderRepository extends JpaRepository<Order, Long> {
// 현재 사용자의 주문 정보를 페이징 조건에 맞춰 조회
@Query("select o from Order o " +
"where o.member.email = :email " +
"order by o.orderDate desc"
)
List<Order> findOrders(@Param("email") String email, Pageable pageable);
// 현재 사용자의 주문 개수가 몇 개인지 조회
@Query("select count(o) from Order o " +
"where o.member.email = :email"
)
Long countOrder(@Param("email") String email);
}
@Transactional(readOnly = true)
public Page<OrderHistDto> getOrderList(String email, Pageable pageable) {
List<Order> orders = orderRepository.findOrders(email, pageable);
Long totalCount = orderRepository.countOrder(email);
List<OrderHistDto> orderHistDtos = new ArrayList<>();
for (Order order : orders) {
OrderHistDto orderHistDto = new OrderHistDto(order);
List<OrderItem> orderItems = order.getOrderItems();
for (OrderItem orderItem : orderItems) {
ItemImg itemImg = itemImgRepository.findByItemIdAndRepimgYn
(orderItem.getItem().getId(), "Y");
OrderItemDto orderItemDto =
new OrderItemDto(orderItem, itemImg.getImgUrl());
orderHistDto.addOrderItemDto(orderItemDto);
}
orderHistDtos.add(orderHistDto);
}
return new PageImpl<OrderHistDto>(orderHistDtos, pageable, totalCount);
}
@GetMapping(value = {"/orders", "/orders/{page}"})
public String orderHist(@PathVariable("page") Optional<Integer> page, Principal principal, Model model){
Pageable pageable = PageRequest.of(page.isPresent() ? page.get() : 0, 4);
Page<OrderHistDto> ordersHistDtoList = orderService.getOrderList(principal.getName(), pageable);
model.addAttribute("orders", ordersHistDtoList);
model.addAttribute("page", pageable.getPageNumber());
model.addAttribute("maxPage", 5);
return "order/orderHist";
}
요청이 1개인 쿼리인데 N개의 쿼리가 발생하는 현상
for (Order order : orders) {
OrderHistDto orderHistDto = new OrderHistDto(order);
// 이 부분
List<OrderItem> orderItems = order.getOrderItems();
for (OrderItem orderItem : orderItems) {
ItemImg itemImg = itemImgRepository.findByItemIdAndRepimgYn
(orderItem.getItem().getId(), "Y");
OrderItemDto orderItemDto =
new OrderItemDto(orderItem, itemImg.getImgUrl());
orderHistDto.addOrderItemDto(orderItemDto);
}
orderHistDtos.add(orderHistDto);
}
위 코드에서 getOrderItems()
라는 부분에 주목하자.
회원 당 여러개의 Order
을 가짐
해당 회원의 모든 Order_item
을 가지고 오고 싶음.
아래 쿼리처럼 Order
별로 Order_item
을 다 받아와야한다.
orderitems0_.order_id = ?
Order
의 갯수가 많아지면 쿼리문이 많이 발생해서 부화가 생긴다.
이를 방지하기 위해 order_id
를 IN 조건을 이용해 한 번에 여러 Order_id
를 실행하면 좋을 것 같다.
default_batch_fetch_size=1000
으로 설정하면, order_id
를 in 조건으로 묶어서 해당하는 모든 order_item들을 받아옴.
조회 쿼리 한 번으로 지정한 사이즈 만큼 한 번에 조회가 가능
예시) order_id in (Order1, Order2, Order3)
orderitems0_.order_id in (?, ?)
상품 주문과 반대로 취소 시, Stock 수만 다시 증가 시키면 된다.
- 상품 재고를 다시 더해주는 메소드
- 주문상품 별 1번 메소드 호출
- 주문에서 주문 상품 별 2번의 메소드 호출
- 주문서비스에서 취소하기 (회원 맞는지 확인 및 DB에서 주문 불러와서 3번 실행)
- 주문 컨트롤러에서 4번 실행하고, 주문 번호를 담은 페이지 구현 객체 반환
- 주문 취소 기능을 실행하는 JS 함수 추가
public void addStock(int stockNumber){
this.stockNumber += stockNumber;
}
public void cancel() {
this.getItem().addStock(count);
}
public void cancelOrder() {
this.orderStatus = OrderStatus.CANCEL;
for (OrderItem orderItem : orderItems) {
orderItem.cancel();
}
}
// 주문의 저장된 회원 정보와 현재 로그인한 회원 정보가 같은지 확인
@Transactional(readOnly = true)
public boolean validateOrder(Long orderId, String email){
Member curMember = memberRepository.findByEmail(email);
Order order = orderRepository.findById(orderId)
.orElseThrow(EntityNotFoundException::new);
Member savedMember = order.getMember();
if(!StringUtils.equals(curMember.getEmail(), savedMember.getEmail())){
return false;
}
return true;
}
// 주문 불러와서 Order의 취소 메서드 실행
public void cancelOrder(Long orderId){
Order order = orderRepository.findById(orderId)
.orElseThrow(EntityNotFoundException::new);
order.cancelOrder();
}
@PostMapping("/order/{orderId}/cancel")
public @ResponseBody ResponseEntity cancelOrder(@PathVariable("orderId") Long orderId , Principal principal){
// 주문한 회원과 로그인한 회원 확인 후,
if(!orderService.validateOrder(orderId, principal.getName())){
return new ResponseEntity<String>("주문 취소 권한이 없습니다.", HttpStatus.FORBIDDEN);
}
orderService.cancelOrder(orderId);
return new ResponseEntity<Long>(orderId, HttpStatus.OK);
}
ResponseEntity
: 결과 데이터와 HTTP 상태 코드를 직접 제어할 수 있는 클래스function cancelOrder(orderId) {
var token = $("meta[name='_csrf']").attr("content");
var header = $("meta[name='_csrf_header']").attr("content");
var url = "/order/" + orderId + "/cancel";
var paramData = { // 주문 취소 번호 파라미터로 넘겨줄 예정
orderId : orderId,
};
var param = JSON.stringify(paramData); // JSON화
$.ajax({
url : url,
type : "POST",
contentType : "application/json",
data : param,
beforeSend : function(xhr){
/* 데이터를 전송하기 전에 헤더에 csrf값을 설정 */
xhr.setRequestHeader(header, token);
},
dataType : "json",
cache : false,
success : function(result, status){
alert("주문이 취소 되었습니다.");
location.href='/orders/' + [[${page}]];
},
error : function(jqXHR, status, error){
if(jqXHR.status == '401'){
alert('로그인 후 이용해주세요');
location.href='/members/login';
} else{
alert(jqXHR.responseText);
}
}
});
}
AJAX
: JS라이브러리로, JS를 이용해서 서버에 요청 (Ajax 알아보기)
xhr
: XMLHttpRequest의 약자로 AJAX를 통해 서버에 요청할 때 사용하는 객체
jqXHR
: 브라우저 고유 XMLHttpRequest 객체를 대체 / 기본 XHR 기능을 시뮬레이션