๐Ÿ’ท์ œํ’ˆ ์ƒ์„ธ, ์ฃผ๋ฌธ , ์ฃผ๋ฌธ ๋‚ด์—ญ, ์ฃผ๋ฌธ ์ทจ์†Œ ํŽ˜์ด์ง€ ๋งŒ๋“ค๊ธฐ

gdhiยท2023๋…„ 12์›” 18์ผ
post-thumbnail

๐Ÿ’ท์ œํ’ˆ ์ƒ์„ธ ํŽ˜์ด์ง€


๐Ÿ“ŒItemController ์ˆ˜์ •

...

@GetMapping(value = "/item/{itemId}")
    public String itemDtl(Model model, @PathVariable("itemId") Long itemId){

        // ์ƒํ’ˆ ์ˆ˜์ • ํŽ˜์ด์ง€์—์„œ ์‚ฌ์šฉํ–ˆ๋˜ ItemDtl ๋ฉ”์†Œ๋“œ๋ฅผ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉํ•˜์—ฌ ์ƒํ’ˆ ์กฐํšŒ
        // ๋ฉ”์†Œ๋“œ ์˜ค๋ฒ„๋กœ๋”ฉ์€ ํŒŒ๋ผ๋ฏธํ„ฐ์˜ ์ˆœ์„œ๊ฐ€ ๋‹ฌ๋ผ๋„ ์ ์šฉ ๋จ
        ItemFormDto itemFormDto = itemService.getItemDtl(itemId);
        model.addAttribute("item", itemFormDto);
        return "item/itemDtl";

    }

}



๐Ÿ“ŒitemDtl.html ์ƒ์„ฑ

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="~{layouts/layout1}">

<head>
  	<!-- ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ๋กœ ์ธํ•ด ์ž๋™์œผ๋กœ ๋ณด๋‚ด์ง€๋Š” html meta ํƒœ๊ทธ -->
    <meta name="_csrf" th:content="${_csrf.token}"/>
    <meta name="_csrf_header" th:content="${_csrf.headerName}"/>
    <title>์ƒํ’ˆ ์ƒ์„ธ ์ •๋ณด</title>
</head>

<th:block layout:fragment="script">
    <script th:inline="javascript">
        //totalPrice ๋ฅผ ๊ณ„์‚ฐํ•ด์„œ ์ถœ๋ ฅํ•˜๋Š” calculateTotalPrice() ๋ฉ”์†Œ๋“œ
        //์ฒ˜์Œ ํ™”๋ฉด ์ถœ๋ ฅํ•  ๋•Œ ์ˆ˜ํ–‰๋˜๊ณ , ๊ทธ ํ›„์— ์ˆ˜๋Ÿ‰ ๋ณ€๋™ ์‹œ ์ˆ˜ํ–‰ ๋จ
        $(document).ready(function(){

            calculateTotalPrice();

            $("#count").change( function(){
                calculateTotalPrice();
            });
        });

        function calculateTotalPrice(){
            var count = $("#count").val();
            var price = $("#price").val();
            var totalPrice = price * count;
            $("#totalPrice").html(totalPrice + '์›');
        }

    </script>
</th:block>

<!-- ์‚ฌ์šฉ์ž CSS ์ถ”๊ฐ€ -->
<th:block layout:fragment="css">
    <style>
        .mgb-15{
            margin-bottom:15px;
        }
        .mgt-30{
            margin-top:30px;
        }
        .mgt-50{
            margin-top:50px;
        }
        .repImgDiv{
            margin-right:15px;
            height:auto;
            width:50%;
        }
        .repImg{
            width:100%;
            height:400px;
        }
        .wd50{
            height:auto;
            width:50%;
        }
    </style>
</th:block>

<div layout:fragment="content" style="margin-left:25%; margin-right:25%">

    <input type="hidden" id="itemId" th:value="${item.id}">

    <div class="d-flex">
        <div class="repImgDiv">
            <img th:src="${item.itemImgDtoList[0].imgUrl}" class="img-fluid img-thumbnail repImg" th:alt="${item.itemNm}">
        </div>
        <div class="wd50">
            <span th:if="${item.itemSellStatus == T(com.shop.constant.ItemSellStatus).SELL}"
                  class="badge bg-primary mgb-15">ํŒ๋งค์ค‘</span>
            <span th:unless="${item.itemSellStatus == T(com.shop.constant.ItemSellStatus).SELL}"
                  class="badge bg-danger mgb-15">ํ’ˆ์ ˆ</span>

            <div class="h4" th:text="${item.itemNm}"></div>

            <hr class="my-4">

            <div class="text-right">
                <div class="h4 text-danger text-left">
                    <input type="hidden" th:value="${item.price}" id="price" name="price">
                    <span th:text="${item.price}"></span>์›
                </div>
                <div class="input-group w-50">
                    <div class="input-group-prepend">
                        <span class="input-group-text">์ˆ˜๋Ÿ‰</span>
                    </div>
                    <input type="number" name="count" id="count" class="form-control" value="1" min="1">
                </div>
            </div>

            <hr class="my-4">

            <div class="text-right mgt-50">
                <h5>๊ฒฐ์ œ ๊ธˆ์•ก</h5>
                <h3 name="totalPrice" id="totalPrice" class="font-weight-bold"></h3>
            </div>
            <div th:if="${item.itemSellStatus == T(com.shop.constant.ItemSellStatus).SELL}" class="text-right">
                <button type="button" class="btn btn-light border border-primary btn-lg"
                        onclick="addCart()">์žฅ๋ฐ”๊ตฌ๋‹ˆ ๋‹ด๊ธฐ</button>
                <button type="button" class="btn btn-primary btn-lg" onclick="order()">์ฃผ๋ฌธํ•˜๊ธฐ</button>
            </div>
            <div th:unless="${item.itemSellStatus == T(com.shop.constant.ItemSellStatus).SELL}" class="text-right">
                <button type="button" class="btn btn-danger btn-lg">ํ’ˆ์ ˆ</button>
            </div>
        </div>
    </div>

    <div class="p-3 bg-warning bg-opacity-10 border border-warning rounded">
        <div class="container">
            <h4 class="display-5">์ƒํ’ˆ ์ƒ์„ธ ์„ค๋ช…</h4>
            <hr class="my-4">
            <p class="lead" th:text="${item.itemDetail}"></p>
        </div>
    </div>
    <div th:each="itemImg : ${item.itemImgDtoList}" class="text-center">
        <img th:if="${not #strings.isEmpty(itemImg.imgUrl)}" th:src="${itemImg.imgUrl}"
             class="rounded mgb-15" width="800">
    </div>
</div>
</html>



๐Ÿ“Œ๊ฒฐ๊ณผ









๐Ÿ’ท์ƒํ’ˆ ์ฃผ๋ฌธ ํŽ˜์ด์ง€

๐Ÿ‘‰ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ํ•  ํŒจํ‚ค์ง€ ์ƒ์„ฑ


๐Ÿ“ŒOutOfStockException ํด๋ž˜์Šค ์ƒ์„ฑ

...

package com.shop.exception;

// ์ƒํ’ˆ ์ฃผ๋ฌธ ์ˆ˜๋Ÿ‰ ๋ณด๋‹ค ํ˜„์žฌ ์žฌ๊ณ ์˜ ์ˆ˜๊ฐ€ ์ ์„ ๋•Œ ๋ฐœ์ƒ ์‹œํ‚ฌ Exception ์ •์˜
public class OutOfStockException extends RuntimeException{ // ์—๋Ÿฌ message ๋ฅผ ์ง€์ •ํ•  ์ˆ˜ ์žˆ๋Š” RuntimeException ํด๋ž˜์Šค

    public OutOfStockException(String message) {
        super(message);
    }

}



๐Ÿ“ŒItem ์—”ํ‹ฐํ‹ฐ ํด๋ž˜์Šค ์ˆ˜์ •

...

public void removeStock(int stockNumber){

        // ๊ธฐ์กด ์žฌ๊ณ  - ์ฃผ๋ฌธ ์ˆ˜๋Ÿ‰ ์žฌ๊ณ 
        int restStock = this.stockNumber - stockNumber; // 10, 5 / 10, 20

        // ์ฃผ๋ฌธ ์ˆ˜๋Ÿ‰ ์žฌ๊ณ ๊ฐ€ ๋” ๋งŽ์œผ๋ฉด ์žฌ๊ตฌ ๋ถ€์กฑ ์—๋Ÿฌ ๋ฐœ์ƒ
        if (restStock < 0){
            throw new OutOfStockException("์ƒํ’ˆ์˜ ์žฌ๊ณ ๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค. (ํ˜„์žฌ ์žฌ๊ณ  ์ˆ˜๋Ÿ‰: " + this.stockNumber + ")");
        }

        this.stockNumber = restStock; // 5
    }
}



๐Ÿ“ŒOrderItem ์—”ํ‹ฐํ‹ฐ ํด๋ž˜์Šค ์ˆ˜์ •

...

// ์ฃผ๋ฌธ ์ƒํ’ˆ๊ณผ ์ฃผ๋ฌธ ์ˆ˜๋Ÿ‰ ์ •๋ณด๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๋Š” OrderItem Entity ์— ๊ฐ์ฒด ์ƒ์„ฑ ๋ฉ”์†Œ๋“œ ์ถ”๊ฐ€
// Item(์ƒํ’ˆ) ๐Ÿ‘‰ OrderItem(์ฃผ๋ฌธ ์ƒํ’ˆ) ๋‘ ๊ฐœ๋Š” ๋‹ค๋ฅธ ๊ฒƒ์ž„
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;
    }

}



๐Ÿ“ŒOrder ์—”ํ‹ฐํ‹ฐ ํด๋ž˜์Šค ์ถ”๊ฐ€

// OrderItem ๊ฐ์ฒด๋ฅผ ์—ฐ๊ฒฐํ•˜๊ณ  OrderItem ๊ฐ์ฒด์— ์ž์‹ ์„ ์—ฐ๊ฒฐํ•˜๋Š” ๋ฉ”์†Œ๋“œ
    // ๋‚ด ์•„์ดํ…œ ๋ฆฌ์ŠคํŠธ์— ์ถ”๊ฐ€ํ•˜๊ณ  ์ฃผ๋ฌธ์— ๋ฆฌ์ŠคํŠธ๋ฅผ ์ถ”๊ฐ€ํ•œ๋‹ค.
    public void addOrderItem(OrderItem orderItem){
        orderItems.add(orderItem); // ์ฃผ๋ฌธ ๊ฐ์ฒด์— ์ฃผ๋ฌธ ์ƒํ’ˆ ๊ฐ์ฒด ์—ฐ๊ฒฐ
        orderItem.setOrder(this);  // ์ฃผ๋ฌธ ์ƒํ’ˆ ๊ฐ์ฒด์— ์ฃผ๋ฌธ ๊ฐ์ฒด ์—ฐ๊ฒฐ (์—ฐ๊ด€ ๊ด€๊ณ„ ์ฃผ์ธ)
    }

    // 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๋กœ ์„ค์ • ๐Ÿ‘‰ ์ฃผ๋ฌธ ์™„๋ฃŒ
        order.setOrderDate(LocalDateTime.now());

        return order; // ์ฃผ๋ฌธ์„œ ๋ฐ˜ํ™˜

    }

    // ๊ฐ ์ฃผ๋ฌธ ์ƒํ’ˆ์˜ TotalPrice ๋ฅผ ๊ตฌํ•œ๋’ค ๋ชจ๋‘ ๋”ํ•˜๋Š” ๋ฉ”์†Œ๋“œ ์ถ”๊ฐ€
    public int getTotalPrice(){

        int totalPrice = 0;

        // ์ฃผ๋ฌธ์„œ์— ์žˆ๋Š” ์ฃผ๋ฌธ ์•„์ดํ…œ ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ฐ˜๋ณต
        // ์ฃผ๋ฌธ ์•„์ดํ…œ๋งˆ๋‹ค ์ด ๊ฐ๊ฒฉ์„ totalPrice์— ์ถ”๊ฐ€
        for (OrderItem orderItem : orderItems){
            totalPrice += orderItem.getTotalPrice();
        }

        return totalPrice; // ์žฅ๋ฐ”๊ตฌ๋‹ˆ ์ด์•ก

    }

}



๐Ÿ“ŒOrderDto ํด๋ž˜์Šค ์ƒ์„ฑ

package com.shop.dto;

import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
// ์ œํ’ˆ ์ƒ์„ธ ํŽ˜์ด์ง€ ํ™”๋ฉด์—์„œ ๋ณด๋‚ด๋Š” ์ฃผ๋ฌธ ์ •๋ณด(์ƒํ’ˆ, ์ˆ˜๋Ÿ‰)๋ฅผ ์œ„ํ•œ Dto ๊ฐ์ฒด ์ƒ์„ฑ
public class OrderDto {
    
    @NotNull(message = "์ƒํ’ˆ ์•„์ด๋””๋Š” ํ•„์ˆ˜ ์ž…๋ ฅ ๊ฐ’์ž…๋‹ˆ๋‹ค.")
    private Long itemId;
    
    @Min(value = 1, message = "์ตœ์†Œ ์ฃผ๋ฌธ ์ˆ˜๋Ÿ‰์€ 1๊ฐœ ์ž…๋‹ˆ๋‹ค.")
    @Max(value = 999, message = "์ตœ๋Œ€ ์ฃผ๋ฌธ ์ˆ˜๋Ÿ‰์€ 999๊ฐœ ์ž…๋‹ˆ๋‹ค.")
    private int count;
    
}



๐Ÿ“ŒOrderService ํด๋ž˜์Šค ์ƒ์„ฑ

package com.shop.service;

import com.shop.dto.OrderDto;
import com.shop.dto.OrderItemDto;
import com.shop.entity.Item;
import com.shop.entity.Member;
import com.shop.entity.Order;
import com.shop.entity.OrderItem;
import com.shop.repository.ItemRepository;
import com.shop.repository.MemberRepository;
import com.shop.repository.OrderRepository;
import jakarta.persistence.EntityNotFoundException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.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; // ์ฃผ๋ฌธ ๊ฐ์ฒด๋ฅผ ์ €์žฅ

    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.createOrderItem ๐Ÿ‘‰ static ๋ฉ”์†Œ๋“œ
        OrderItem orderItem = OrderItem.createOrderItem(item, orderDto.getCount());
        orderItemList.add(orderItem);

        // Order.createOrder ๐Ÿ‘‰ static ๋ฉ”์†Œ๋“œ
        Order order = Order.createOrder(member, orderItemList);
        orderRepository.save(order);

        return order.getId();

    }

}



๐Ÿ“ŒโšกOrderController ํด๋ž˜์Šค ์ƒ์„ฑโšก

AJAX๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.

package com.shop.controller;

import com.shop.dto.OrderDto;
import com.shop.service.OrderService;
import jakarta.validation.Valid;
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 java.security.Principal;
import java.util.List;

@Controller
@RequiredArgsConstructor
public class OrderController {

    private final OrderService orderService;

    @PostMapping(value = "/order")
    // ๋น„๋™๊ธฐ ๋ฐฉ์‹์œผ๋กœ Json ๋ฐ์ดํ„ฐ๋ฅผ ์ฃผ๊ณ  ๋ฐ›์Œ ๐Ÿ‘‰ AJAX
    // @ResponseBody : ์ž๋ฐ” ๊ฐ์ฒด๋ฅผ http ์‘๋‹ต body ๋ถ€๋ถ„์œผ๋กœ ๋ณด๋ƒ„ (ResponseEntity ์ž๋ฃŒํ˜•)
    // @RequestBody : http ์š”์ฒญ์˜ body ๋ถ€๋ถ„ ๋ฐ์ดํ„ฐ๋ฅผ ์ž๋ฐ” ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜ํ•ด์„œ ๋ฐ›์Œ
    public @ResponseBody ResponseEntity order(@RequestBody @Valid OrderDto orderDto,
                                              BindingResult bindingResult,
                                              Principal principal){

        // ์ž…๋ ฅ๊ฐ’์— ๋ฌธ์ œ๊ฐ€ ์žˆ์„ ์‹œ ํ•„๋“œ ์—๋Ÿฌ ์ •๋ณด๋“ค์„ ResponseEntity ๊ฐ์ฒด์— ๋‹ด์•„์„œ ๋ฐ˜ํ™˜
        if (bindingResult.hasErrors()){

            // String์€ "ABC" + "123" ์œผ๋กœ ๋ถ™์—ฌ "ABC123"์œผ๋กœ ๋งŒ๋“ค์ง€๋งŒ
            // StringBuilder ๋Š” a = append.("ABC");, b = append.("123"); ์ด๋ ‡๊ฒŒ ๋ถ™์—ฌ ๋งŒ๋“ ๋‹ค
            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);

        }

        // ํ˜„์žฌ ๋กœ๊ทธ์ธํ•œ ์œ ์ €์˜ ์ •๋ณด(principal.getName();)๋ฅผ ๋‹ด๊ณ  ์žˆ๋Š” Principal ๊ฐ์ฒด์—์„œ ์œ ์ €์˜ email ์ถ”์ถœ
        String email = principal.getName();

        Long orderId;

        try {

            orderId = orderService.order(orderDto, email);

        }catch (Exception e){

            // OutOfStockNumber ์ด ๋ฐ›๋Š”๋‹ค
            return new ResponseEntity<String>(e.getMessage(), HttpStatus.BAD_REQUEST);

        }

        // ์ •์ƒ์ ์œผ๋กœ ๋™์ž‘ ์‹œ ์ƒ์„ฑ๋œ ์ฃผ๋ฌธ ๊ฐ์ฒด์˜ Id ์™€ ์ƒํƒœ์ฝ”๋“œ 200์„ ๋ณด๋ƒ„
        // ๐Ÿ‘‰ AJAX๋กœ ๋ณด๋‚ธ๋‹ค
        return new ResponseEntity<Long>(orderId, HttpStatus.OK);

    }

}



๐Ÿคฆโ€โ™€๏ธOrderService ์ƒํ’ˆ ์ฃผ๋ฌธ ํ…Œ์ŠคํŠธ ํ•˜๊ธฐ

package com.shop.service;

import com.shop.constant.ItemSellStatus;
import com.shop.dto.OrderDto;
import com.shop.entity.Item;
import com.shop.entity.Member;
import com.shop.entity.Order;
import com.shop.entity.OrderItem;
import com.shop.repository.ItemRepository;
import com.shop.repository.MemberRepository;
import com.shop.repository.OrderRepository;
import jakarta.persistence.EntityNotFoundException;
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 org.springframework.transaction.annotation.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(); // JPA DB์— ์™”๋‹ค๊ฐ”๋‹คํ•˜๋Š” ๊ฐ์ฒด ๊ฐ์ฒด
        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());

        // ์ฃผ๋ฌธ ๊ฐ์ฒด DB์— ์ €์žฅ
        Long orderId = orderService.order(orderDto, member.getEmail());

        // ์ง€์ •๋œ ์ฃผ๋ฌธ ๊ฐ์ฒด ์กฐํšŒ
        Order order = orderRepository.findById(orderId)
                .orElseThrow(EntityNotFoundException::new);

        // 1. DB์— ์ €์žฅ๋œ ์ฃผ๋ฌธ ๊ฐ์ฒด์—์„œ ์ฃผ๋ฌธ ์ƒํ’ˆ ์ถ”์ถœ (1๊ฐœ)
        List<OrderItem> orderItems = order.getOrderItems();

        // 2. ์œ„์—์„œ ๋งŒ๋“  ์ฃผ๋ฌธ ์ƒํ’ˆ ์ด ๊ฐ€๊ฒฉ (1๊ฐœ)
        int totalPrice = orderDto.getCount() * item.getPrice();

        // 1์˜ ๊ฐ€๊ฒฉ๊ณผ 2๊ฐ€ ๊ฐ™์€์ง€ ํ…Œ์ŠคํŠธ
        assertEquals(totalPrice, order.getTotalPrice());
        
        System.out.println(totalPrice);

    }


}

๐Ÿ“๊ฒฐ๊ณผ

โ—

create ๋กœ ๋ณ€๊ฒฝํ•ด์•ผํ•จ cart ํ…Œ์ด๋ธ”์„ ์•„์ง ๋งŒ๋“ค์ง€ ์•Š์•˜์Œ



๐Ÿ“ŒitemDtl.html ์ˆ˜์ •


...

        // ์ฃผ๋ฌธํ•˜๊ธฐ ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด ์‹คํ–‰
        function order(){

            // Ajax ํ†ต์‹ ํ•  ๋•Œ, csrf ํ† ํฐ ๊ฐ’์„ ์กฐํšŒํ•ด์„œ ์ง์ ‘ ๋ณด๋‚ด์•ผํ•จ
            var token = $("meta[name = '_csrf']").attr("content");
            var header = $("meta[name = '_csrf_header']").attr("content");

            var url = "/order";
            var paramData = {
                itemId : $("#itemId").val(),
                count : $("#count").val()
            };

            // JSON ๐Ÿ‘‰ String
            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){
                    if(jqXHR.status == '401'){
                        alert('๋กœ๊ทธ์ธ ํ›„ ์ด์šฉํ•ด์ฃผ์„ธ์š”');
                        location.href = '/members/login';
                    }
                    else{
                        alert(jqXHR.responseText);
                    }
                }

            });

        }

    </script>



๐Ÿ“Œ๊ฒฐ๊ณผ

๐Ÿ‘‰ ๋กœ๊ทธ์ธ ์•ˆํ•˜๊ณ  ์ฃผ๋ฌธ

๐Ÿ‘‰ ๋ฐ”๋กœ ๋„˜์–ด์˜จ๋‹ค

๐Ÿ‘‰ ์žฌ๊ณ ๊ฐ€ ์—†์„ ๋•Œ

๐Ÿ‘‰ ์ฃผ๋ฌธ ์„ฑ๊ณต



โ“์–ด๋–ค ๊ตฌ์กฐ๋กœ ์‹คํ–‰?









๐Ÿ’ท๊ตฌ๋งค ๋‚ด์—ญ ํŽ˜์ด์ง€


๐Ÿ“ŒOrderItemDto ํด๋ž˜์Šค ์ƒ์„ฑ

package com.shop.dto;

// ์ฃผ๋ฌธ ์ƒํ’ˆ ์ •๋ณด

import com.shop.entity.OrderItem;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class OrderItemDto {

    private String itemNm;
    private int count;
    private int orderPrice;
    private String imgUrl;

    public OrderItemDto(OrderItem orderItem, String imgUrl) {
        this.itemNm = orderItem.getItem().getItemNm();
        this.count = orderItem.getCount();
        this.orderPrice = orderItem.getOrderPrice();
        this.imgUrl = imgUrl;
    }
}



๐Ÿ“ŒOrderHistDto ํด๋ž˜์Šค ์ƒ์„ฑ

package com.shop.dto;

import com.shop.constant.OrderStatus;
import com.shop.entity.Order;
import lombok.Getter;
import lombok.Setter;

import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;

@Getter
@Setter
public class OrderHistDto {
    
    private Long orderId;
    private String orderDate;
    private OrderStatus orderStatus;
    private List<OrderItemDto> orderItemDtoList = new ArrayList<>();
    
    public OrderHistDto(Order order){
        
        this.orderId = order.getId();
        this.orderDate = order.getOrderDate()
                .format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"));
        this.orderStatus = order.getOrderStatus();
        
    }
    
    public void addOrderItemDto(OrderItemDto orderItemDto){
        orderItemDtoList.add(orderItemDto);
    }
    
}



๐Ÿ“ŒOrderRepository ์ธํ„ฐํŽ˜์ด์Šค ์ˆ˜์ •

package com.shop.repository;

import com.shop.entity.Order;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.awt.print.Pageable;
import java.util.List;

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);

}



๐Ÿ“ŒItemImgRepository ์ธํ„ฐํŽ˜์ด์Šค ์ˆ˜์ •

package com.shop.repository;

import com.shop.entity.ItemImg;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

// ์ƒํ’ˆ ์ด๋ฏธ์ง€ ์ •๋ณด ์ฟผ๋ฆฌ๋ฌธ ๋‚ ๋ฆฌ๋Š” Repository
public interface ItemImgRepository extends JpaRepository<ItemImg,Long> {
    // ์ด๋ฏธ์ง€๊ฐ€ ์ž˜ ์ €์žฅ๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•ด ์ฟผ๋ฆฌ ์กฐํšŒ๋ฌธ ์ถ”๊ฐ€
    List<ItemImg> findByItemIdOrderByIdAsc(Long itemId);

    // ๊ตฌ๋งค ๋‚ด์—ญ ํŽ˜์ด์ง€์—์„œ ์ฃผ๋ฌธ ์ƒํ’ˆ์˜ ๋Œ€ํ‘œ ์ด๋ฏธ์ง€๋ฅผ ์œ„ํ•œ ์กฐํšŒ
    ItemImg findByItemIdAndRepImgYn(Long itemId, String repImgYn);
}



๐Ÿ“ŒOrderService ํด๋ž˜์Šค ์ˆ˜์ •

...
    private final ItemImgRepository itemImgRepository;
...

// ์ฃผ๋ฌธ ๋ชฉ๋ก ์กฐํšŒ ๋ฉ”์†Œ๋“œ
    @Transactional(readOnly = true)
    public Page<OrderHistDto> getOrderList(String email, Pageable pageable){

        // ์œ ์ € email๊ณผ ํŽ˜์ด์ง• ์กฐ๊ฑด์„ ์ด์šฉํ•˜์—ฌ ์ฃผ๋ฌธ ๋ชฉ๋ก์„ ์กฐํšŒ
        List<Order> orders = orderRepository.findOrders(email, pageable);
        // ์œ ์ €์˜ ์ฃผ๋ฌธ ์ด ๊ฐœ์ˆ˜
        Long totalCount = orderRepository.countOrder(email);

        List<OrderHistDto> orderHistDtos = new ArrayList<>();

        // ์ฃผ๋ฌธ ๋ฆฌ์ŠคํŠธ๋ฅผ ์ˆœํšŒํ•˜๋ฉด์„œ ๊ตฌ๋งค ์ด๋ ฅ ํŽ˜์ด์ง€์— ์ „๋‹ฌํ•  DTO๋ฅผ ์ƒ์„ฑ
        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);

        }
        
        // ํŽ˜์ด์ง€ ๊ตฌํ˜„ ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•˜์—ฌ ๋ฐ˜ํ™˜
        // ๐Ÿ‘‰ order, orderItem Entity ๊ฐ์ฒด๋ฅผ ๊ฐ๊ฐ OrderHistDto, OrderItemDto ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜
        return new PageImpl<OrderHistDto>(orderHistDtos, pageable, totalCount);

    }

}



๐Ÿ“ŒOrderController ํด๋ž˜์Šค ์ˆ˜์ •

...

    // ๊ตฌ๋งค ๋‚ด์—ญ ์กฐํšŒ
    @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, 5);

        // ํ˜„์žฌ ๋กœ๊ทธ์ธํ•œ ํšŒ์›์€ ์ด๋ฉ”์ผ๊ณผ ํŽ˜์ด์ง• ๊ฐ์ฒด๋ฅผ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ์ „๋‹ฌํ•˜์—ฌ ํ™”๋ฉด์— ์ „๋‹ฌํ•œ ์ฃผ๋ฌธ ๋ชฉ๋ก ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ˜ํ™˜
        Page<OrderHistDto> orderHistDtoList = orderService.getOrderList(principal.getName(), pageable);

        model.addAttribute("orders", orderHistDtoList);
        model.addAttribute("page", pageable.getPageNumber());
        model.addAttribute("maxPage", 5);

        return "/order/orderHist";

    }

}



๐Ÿ“ŒorderHist.html ์ƒ์„ฑ

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="~{layouts/layout1}">

<head>
    <meta name="_csrf" th:content="${_csrf.token}"/>
    <meta name="_csrf_header" th:content="${_csrf.headerName}"/>
</head>
  
<th:block layout:fragment="css">
    <style>
        .content-mg{
            margin-left:30%;
            margin-right:30%;
            margin-top:2%;
            margin-bottom:100px;
        }
        .repImgDiv{
            margin-right:15px;
            margin-left:15px;
            height:auto;
        }
        .repImg{
            height:100px;
            width:100px;
        }
        .card{
            width:750px;
            height:100%;
            padding:30px;
            margin-bottom:20px;
        }
        .fs18{
            font-size:18px
        }
        .fs24{
            font-size:24px
        }
    </style>
</th:block>

<div layout:fragment="content" class="content-mg p-3 bg-info bg-opacity-10 border border-info border rounded">

    <h2 class="mb-4">์ฃผ๋ฌธ ๋‚ด์—ญ</h2>

    <div th:each="order : ${orders.getContent()}">

        <div class="d-flex mb-3 align-self-center">
            <h4 th:text="${order.orderDate} + ' ์ฃผ๋ฌธ'"></h4>
            <div class="ms-3">
                <th:block th:if="${order.orderStatus == T(com.shop.constant.OrderStatus).ORDER}">
                    <button type="button" class="btn btn-outline-success"
                            th:value="${order.orderId}" onclick="cancelOrder(this.value)">์ฃผ๋ฌธ์ทจ์†Œ</button>
                </th:block>
                <th:block th:unless="${order.orderStatus == T(com.shop.constant.OrderStatus).ORDER}">
                    <h6 style="margin-top : 6px">(์ทจ์†Œ ์™„๋ฃŒ)</h6>
                </th:block>
            </div>
        </div>
        <div class="card d-flex">
            <div th:each="orderItem : ${order.orderItemDtoList}" class="d-flex mb-3">
                <div class="repImgDiv">
                    <img th:src="${orderItem.imgUrl}" class = "rounded repImg" th:alt="${orderItem.itemNm}">
                </div>
                <div class="align-self-center w-75">
                    <span th:text="${orderItem.itemNm}" class="fs24 font-weight-bold"></span>
                    <div class="fs18 font-weight-light">
                        <span th:text="${orderItem.orderPrice} +'์›'"></span>
                        <span th:text="${orderItem.count} +'๊ฐœ'"></span>
                    </div>
                </div>
            </div>
        </div>

    </div>

    <div th:with="start=${(orders.number/maxPage)*maxPage + 1}, end=(${(orders.totalPages == 0) ? 1 : (start + (maxPage - 1) < orders.totalPages ? start + (maxPage - 1) : orders.totalPages)})" >
        <ul class="pagination justify-content-center">

            <li class="page-item" th:classappend="${orders.number eq 0}?'disabled':''">
                <a th:href="@{'/orders/' + ${orders.number-1}}" aria-label='Previous' class="page-link">
                    <span aria-hidden='true'>Previous</span>
                </a>
            </li>

            <li class="page-item" th:each="page: ${#numbers.sequence(start, end)}" th:classappend="${orders.number eq page-1}?'active':''">
                <a th:href="@{'/orders/' + ${page-1}}" th:inline="text" class="page-link">[[${page}]]</a>
            </li>

            <li class="page-item" th:classappend="${orders.number+1 ge orders.totalPages}?'disabled':''">
                <a th:href="@{'/orders/' + ${orders.number+1}}" aria-label='Next' class="page-link">
                    <span aria-hidden='true'>Next</span>
                </a>
            </li>

        </ul>
    </div>

</div>

</html>



๐Ÿ“Œ๊ฒฐ๊ณผ

๐Ÿ‘‰ 1ํŽ˜์ด์ง€

๐Ÿ‘‰ 2ํŽ˜์ด์ง€



โ“์–ด๋–ค ๊ตฌ์กฐ๋กœ ์‹คํ–‰?



๐Ÿ”ฅTodo

ํฌํŠธํด๋ฆฌ์˜ค์— ๋„ค์ด๋ฒ„ ๊ธ€์“ฐ๊ธฐ ์ฒ˜๋Ÿผ ์ด๋ฏธ์ง€๋ฅผ ์ฒจ๋ถ€ํ•˜๋ฉด ๊ธ€ ์•ˆ์— ๋“ค์–ด๊ฐ€์„œ ์ž‘์„ฑํ•˜๋Š” ๊ฒƒ์„ ๋งŒ๋“ค์–ด ๋ณด๋ฉด ์ข‹๊ฒ ๋‹ค.









๐Ÿ’ท์ฃผ๋ฌธ ์ทจ์†Œ ํŽ˜์ด์ง€


๐Ÿ“ŒItem ์—”ํ‹ฐํ‹ฐ ์ˆ˜์ •

...

public void addStock(int stockNumber){
        // ์ฃผ๋ฌธ ์ทจ์†Œ ์‹œ ์ƒํ’ˆ์˜ ์žฌ๊ณ ๋ฅผ ์ƒํ’ˆ ์ฃผ๋ฌธ ์ˆ˜๋Ÿ‰ ๋งŒํผ ๋‹ค์‹œ ๋”ํ•จ
        this.stockNumber += stockNumber;
    }
}



๐Ÿ“ŒOrderItem ์—”ํ‹ฐํ‹ฐ ์ˆ˜์ •

...

public void cancel(){
        // ์ฃผ๋ฌธ ์ƒํ’ˆ ์ˆ˜๋Ÿ‰์„ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋„˜๊น€
        this.getItem().addStock(count);
    }

}



๐Ÿ“ŒOrder ์—”ํ‹ฐํ‹ฐ ์ˆ˜์ •

...

    public void cacncelOrder(){

        // ์ฃผ๋ฌธ ์ƒํƒœ๋ฅผ CANCEL ๋กœ ๋ณ€๊ฒฝ
        this.orderStatus = OrderStatus.CANCEL;

        // ๋ชจ๋“  OrderItem ๊ฐ์ฒด๊ฐ€ cancel() ๋ฉ”์†Œ๋“œ ์‹คํ–‰
        for (OrderItem orderItem : orderItems){

            orderItem.cancel();

        }

    }

}



๐Ÿ“ŒOrderService ํด๋ž˜์Šค ์ˆ˜์ •

...

@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;

    }

    // ์ฃผ๋ฌธ ์ทจ์†Œ ๋ฉ”์†Œ๋“œ (๋ณ€๊ฒฝ ๊ฐ์ง€)
    public void cancelOrder(Long orderId){

        Order order = orderRepository.findById(orderId).orElseThrow(EntityNotFoundException::new);
        order.cacncelOrder();

    }

}



๐Ÿ“ŒOrderController ํด๋ž˜์Šค ์ˆ˜์ •

...

    @PostMapping("/order/{orderId}/cancel")
    // AJAX ํ˜•ํƒœ!!!!
    public @ResponseBody ResponseEntity cancelOrder(@PathVariable("orderId")
                                                        Long orderId, Principal principal){

        // ์ƒํ’ˆ์„ ์ฃผ๋ฌธํ•œ ์œ ์ €๊ฐ€ ๋งž๋Š” ์ง€ ๊ฒ€์ฆ ํ›„ orderService.cancelOrder() ๋ฉ”์†Œ๋“œ ์‹คํ–‰
        if (!orderService.validateOrder(orderId, principal.getName())){

            return new ResponseEntity<String>("์ฃผ๋ฌธ ์ทจ์†Œ ๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค.", HttpStatus.FORBIDDEN);

        }

        orderService.cancelOrder(orderId);
        return new ResponseEntity<Long>(orderId, HttpStatus.OK);

    }

}



๐Ÿ“ŒorderHist.html AJAX ์ถ”๊ฐ€

<th:block layout:fragment="script">
    <script th:inline="javascript">
        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);

            $.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);
                    }
                }
            });
        }
    </script>
</th:block>



๐Ÿ“Œ๊ฒฐ๊ณผ

๐Ÿ‘‰ ํ™•์ธ ๋ฒ„ํŠผ ํด๋ฆญ

๐Ÿ‘‰ AJAX ๋กœ ๋ฐ”๋€๋‹ค!

๐Ÿ‘‰ ์žฌ๊ณ ๋„ ์›๋ž˜๋Œ€๋กœ ๋Œ์•„์™”๋‹ค



๐Ÿ“Œ์–ด๋–ค ๊ตฌ์กฐ๋กœ ์‹คํ–‰?



๐Ÿ”ฅTodo

๋‚˜์ค‘์— ์žฅ๋ฐ”๊ตฌ๋‹ˆ๋กœ ์ฃผ๋ฌธ ํ–ˆ์„ ๋•Œ ์ฃผ๋ฌธ ์ทจ์†Œํ•˜๋ฉด ์ „๋ถ€ ์ทจ์†Œ๊ฐ€ ๋˜๋Š”๋ฐ, check box ๋กœ ์„ ํƒํ•˜์—ฌ ์ทจ์†Œ๊ฐ€ ๋˜๊ฒŒ๋” ๋งŒ๋“ค์ž.

0๊ฐœ์˜ ๋Œ“๊ธ€