Spring으로 3 Tier Architecture 구조 만들기(MyBatis)
✒️ 2025-06-23 13:46 내용 수정
주문 추가 및 조회하는 페이지 만들기
1. DB 연결 및 테이블 구성
- DB 연결을 위한 Mybatis 설정을 참고하여 DB 연결에 필요한 MyBatis 설정을 진행한다.
- DB에 상품 테이블과 주문 테이블을 생성한다.
- SQL문은 DDL(Data Definition Language)#1. CREATE 참고.
- Oracle DB(관계형 데이터베이스 관리 시스템(RDBMS), Oracle 설치)를 사용했다.
- ORDER는 예약어기 때문에 "ORDER"로 작성해줘야 한다.
- ORDER 테이블은 PRODUCT 테이블과 PRODUCT_ID라는 PK-FK 관계(N:M)로 연결되어 있다.
-- 상품 시퀀스
CREATE SEQUENCE SEQ_PRODUCT;
-- 상품 테이블
CREATE TABLE PRODUCT(
PRODUCT_ID NUMBER PRIMARY KEY,
PRODUCT_NAME VARCHAR2(500) NOT NULL,
PRODUCT_STOCK NUMBER DEFAULT 0,
PRODUCT_PRICE NUMBER DEFAULT 0,
REGISTER_DATE DATE DEFAULT SYSDATE,
UPDATE_DATE DATE DEFAULT SYSDATE
);
-- 주문 시퀀스
CREATE SEQUENCE SEQ_ORDER;
-- 주문 테이블
CREATE TABLE "ORDER"(
ORDER_ID NUMBER PRIMARY KEY,
PRODUCT_ID NUMBER NOT NULL,
PRODUCT_COUNT NUMBER DEFAULT 1,
ORDER_DATE DATE DEFAULT SYSDATE,
CONSTRAINT FK_ORDER_PRODUCT FOREIGN KEY(PRODUCT_ID) REFERENCES PRODUCT(PRODUCT_ID)
);
2. 전체 코드 흐름
- 이전 Spring때와 조금 다르게 DB와 DAO 사이에 Mapper 인터페이스, DAO와 Controller 사이에 Service를 추가하여 사용한다.
- 물론 Spring에서도 쓸 수 있다. 실습에선 자주 사용하지 않았다.
- DB -> Mapper -> DAO -> Service -> Controller 순으로 데이터를 거쳐오며, 최종적으로 Controller를 통해 View(HTML)에 정보를 표시한다.
- 나중에 여러 개의 DAO를 하나의 Controller의 메소드에서 호출하게 될 경우 서비스에 대한 목적이 두드러지기 힘들고, DAO의 수많은 Bean을 모두 Controller에 등록해야 하기에 이를 묶어서 처리할 메소드가 필요하기에 Service 인터페이스가 그 역할을 담당한다.
- Service도 인터페이스로 만드는 이유는 공통적인 부분을 추상화시킨 후 나중에 각기 다른 세부 사항을 구현한 클래스들을 사용하기 위함이다.
@Qualifier로 구분하여 사용하면 쉽게 변경할 수 있다. - Spring 팀 project를 진행했을 때도 하나의 Controller에서 5개 정도의 DAO를 사용해 동작을 진행하게 되어 이 부분을 Service가 처리하도록 만들고, Controller에서는 Service 객체만 가져와 메소드를 호출하는 방식으로 처리한 적이 있었다.
- 이런 방법을 사용하면 Controller에서 DAO을 여러 번 호출해서 DB에 여러 번 접근하는 것이 아닌, Service의 메소드를 한 번 호출해서 DB에 여러 번 접근하도록 설정할 수 있다.
- Service도 인터페이스로 만드는 이유는 공통적인 부분을 추상화시킨 후 나중에 각기 다른 세부 사항을 구현한 클래스들을 사용하기 위함이다.
3. DTO와 Mapper 인터페이스, mapper.xml 생성
- src/main/java 폴더의 com.example.tier처럼 group id와 artifact로 된 패키지의 하위 패키지로 dto 패키지와 mapper 패키지를 만든다.
- dto 패키지에 ProductDTO와 OrderDTO를 만든다.
- Lombok(lombok)의 @Data를 사용하여 getter와 setter를 포함한 필요 메소드들을 추가했다.
- DB에 표기 규칙#4. Snake 표기법로 작성한 컬럼명은 Java에서 표기 규칙#2. Camel 표기법로 수정되기에 DTO를 작성할 때도 형식에 맞춰야 한다.
package com.example.tier.dto;
import lombok.Data;
@Data
public class ProductDTO {
private int productId;
private String productName;
private int productStock;
private int productPrice;
private String registerDate;
private String updateDate;
}
package com.example.tier.dto;
import lombok.Data;
@Data
public class OrderDTO {
private int orderId;
private int productId;
private int productCount;
private String orderDate;
}
- FK관계로 이어진 Product 테이블과 Order 테이블의 정보를 모두 담을 수 있는 OrderVO 클래스를 vo 패키지에 만든다.
- 데이터 객체에서 DTO와 VO는 다른 항목이다.
- 편의상 Lombok의 @Data를 사용해 모든 getter와 setter를 추가했지만 데이터를 수정하기보단 데이터를 가져오는데에 더 특화된 클래스다.
package com.example.tier.vo;
import lombok.Data;
@Data
public class OrderVO {
private int productId;
private String productName;
private int productStock;
private int productPrice;
private String registerDate;
private String updateDate;
private int orderId;
private int productCount;
private String orderDate;
private int orderPrice; // DB에서 PRODUCT_PRICE와 PRODUCT_COUNT로 만들 예정
}
- mapper 패키지에 ProductMapper와 OrderMapper를 인터페이스로 만든다.
- Mapper 클래스에는
@MapperAnnotation을 추가한다. - 인터페이스로 만드는 이유는 확장성을 위해서다.
- Mapper에 작성된 메소드 이름은 SQL문의 id와 동일해야 하며, 이는 Spring에서 DAO에서
sqlSession.selectAll("a.select")로 작성했던"select"가 mapper.xml에 있는 SQL문의 id인 것과 같다. - Mapper 인터페이스를 분리하는 이유에 대해 강사님께 질문했을 때 Spring boot에선 점점 mapper와 DAO를 분리하는 것이 추세라고 하셨다.
- Mapper 클래스에는
package com.example.tier.mapper;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import com.example.tier.dto.OrderDTO;
import com.example.tier.dto.ProductDTO;
@Mapper
public interface ProductMapper {
// 상품 추가
// INSERT INTO PRODUCT VALUES(?,?,?,?,?)
public void insert(ProductDTO productDTO);
// 상품 조회
public List<ProductDTO> selectAll();
// 상품 재고 수정
public void updateStock(OrderDTO orderDTO);
}
package com.example.tier.mapper;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import com.example.tier.dto.OrderDTO;
import com.example.tier.vo.OrderVO;
@Mapper
public interface OrderMapper {
// 주문 추가
public void insert(OrderDTO orderDTO);
// 주문 조회
public List<OrderVO> selectAll(String sort);
}
- src/main/resources 폴더에 mapper 패키지를 만들고, Mapper 인터페이스와 연결할 mapper.xml(product.xml과 order.xml)의 SQL문을 작성한다.
- SELECT 시 컬럼을 전체 조회할 때
SELECT * FROM TABLENAME으로 작성하는 대신SELECT ID, NAME FROM TABLENAME처럼 컬럼명을 모두 작성하는 것이 조회 속도가 더 빠르다. - mapper의 namespace는 Mapper 인터페이스의 패키지 이름을 포함한 전체 이름으로 작성해야 한다.
- Spring boot에선 parameterType과 resultType을 생략해도 작동한다.
- SELECT 시 컬럼을 전체 조회할 때
- 미리 config.xml 파일에 alias를 등록해둔다.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0/EN" "https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<typeAliases>
<typeAlias type="com.example.tier.dto.ProductDTO" alias="productDTO"/>
<typeAlias type="com.example.tier.dto.OrderDTO" alias="orderDTO"/>
<typeAlias type="com.example.tier.vo.OrderVO" alias="orderVO"/>
</typeAliases>
</configuration>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.tier.mapper.ProductMapper">
<insert id="insert">
INSERT INTO PRODUCT (
PRODUCT_ID,
PRODUCT_NAME,
PRODUCT_STOCK,
PRODUCT_PRICE
) VALUES(
SEQ_PRODUCT.nextVal,
#{productName},
#{productStock},
#{productPrice}
)
</insert>
<select id="selectAll">
<!-- column 이름을 명시하는게 조회 시 더 빠르다 -->
SELECT PRODUCT_ID, PRODUCT_NAME, PRODUCT_STOCK,
PRODUCT_PRICE, REGISTER_DATE, UPDATE_DATE
FROM PRODUCT
</select>
<!-- 주문이 들어오면 자동으로 재고량을 차감 -->
<update id="updateStock">
UPDATE PRODUCT
SET PRODUCT_STOCK = PRODUCT_STOCK - #{productCount}
WHERE PRODUCT_ID = #{productId}
</update>
</mapper>
- Mybatis에는 동적 query문을 사용해 특정 분기에 따라 유동적으로 처리하는 SQL문을 작성할 수 있다.
- 공통적인 코드를 줄이고 유용하게 이용할 수 있다.
- SQL문의 조건절인 WHERE 절에서 많이 사용하고, 아래처럼 조건에 따라 정렬 방법을 달리 하는 경우에도 사용할 수 있다.
- JSTL과 XML 기반으로 작성하며,
<if>,<choose>와<when>, <otherwise>,<trim>과<where>, <set>, 그리고<foreach>가 있다. <foreach>의 경우 DB에 ArrayList나 HashMap을 전달할 때 사용했었다.
- 동적 query문을 이용해 Controller로부터 "sort"라는 이름의 매개변수를 받고, 해당 매개 변수에 있는 값에 따라 주문 id를 기준으로 정렬할지, 주문 가격을 기준으로 정렬할지 결정한다.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.tier.mapper.OrderMapper">
<insert id="insert">
INSERT INTO "ORDER"(
ORDER_ID,
PRODUCT_ID,
PRODUCT_COUNT
) VALUES(
SEQ_ORDER.nextVal,
#{productId},
#{productCount}
)
</insert>
<!-- mybatis 동적 쿼리문을 사용해 특정 분기에 따라 유동적으로 처리하는 sql문을 작성 -->
<select id="selectAll" resultType="orderVO">
<!-- column 이름을 명시하는게 조회 시 더 빠르다 -->
SELECT P.PRODUCT_NAME, P.PRODUCT_STOCK, P.PRODUCT_PRICE, P.REGISTER_DATE, P.UPDATE_DATE,
O.ORDER_ID, O.PRODUCT_COUNT, O.ORDER_DATE, (P.PRODUCT_PRICE * O.PRODUCT_COUNT) AS ORDER_PRICE
FROM PRODUCT P JOIN "ORDER" O
ON P.PRODUCT_ID = O.PRODUCT_ID
<choose>
<when test="sort == 'recent'.toString()">
ORDER BY O.ORDER_ID DESC
</when>
<otherwise>
ORDER BY ORDER_PRICE DESC
</otherwise>
</choose>
</select>
</mapper>
4. DAO와 Service
- 이제 src/main/java의 하위 dao패키지를 만들어 Mapper 인터페이스를 사용할 ProductDAO와 OrderDAO를 만든다.
- DAO 클래스에는
@RepositoryAnnotation을 추가한다. - DAO는 Mapper 인터페이스를 생성자 주입하고, Mapper 인터페이스의 메소드를 호출한다.
- DAO 클래스에는
package com.example.tier.dao;
import java.util.List;
import org.springframework.stereotype.Repository;
import com.example.tier.dto.OrderDTO;
import com.example.tier.dto.ProductDTO;
import com.example.tier.mapper.ProductMapper;
import lombok.RequiredArgsConstructor;
@Repository
@RequiredArgsConstructor
public class ProductDAO {
private final ProductMapper productMapper;
// 상품 추가
public void save(ProductDTO productDTO) {
productMapper.insert(productDTO);
}
// 상품 조회
public List<ProductDTO> findAll() {
return productMapper.selectAll();
}
// 상품 재고 수정
public void setProductStock(OrderDTO orderDTO) {
productMapper.updateStock(orderDTO);
}
}
package com.example.tier.dao;
import java.util.List;
import org.springframework.stereotype.Repository;
import com.example.tier.dto.OrderDTO;
import com.example.tier.mapper.OrderMapper;
import com.example.tier.vo.OrderVO;
import lombok.RequiredArgsConstructor;
@Repository
@RequiredArgsConstructor
public class OrderDAO {
private final OrderMapper orderMapper;
// 주문 추가
public void save(OrderDTO orderDTO) {
orderMapper.insert(orderDTO);
}
// 주문 내역
public List<OrderVO> findAll(String sort) {
return orderMapper.selectAll(sort);
}
}
- 서비스 기능을 처리할 Service 인터페이스와 그 구현 클래스들을 만든다.
- Service 구현 클래스에는
@ServiceAnnotation을 추가한다. - Service 구현 클래스에선 DAO를 생성자 주입하고, DAO의 메소드를 호출하여 서비스 기능을 수행한다.
- Service 구현 클래스에는
package com.example.tier.service;
import java.util.List;
import com.example.tier.dto.ProductDTO;
public interface ProductService {
// 상품 추가
public void register(ProductDTO productDTO);
// 상품 조회
public List<ProductDTO> getList();
}
package com.example.tier.service;
import java.util.List;
import org.springframework.stereotype.Service;
import com.example.tier.dao.ProductDAO;
import com.example.tier.dto.ProductDTO;
import lombok.RequiredArgsConstructor;
@Service
@RequiredArgsConstructor
public class ProductServiceImpl implements ProductService{
final ProductDAO productDAO;
@Override
public void register(ProductDTO productDTO) {
productDAO.save(productDTO);
}
@Override
public List<ProductDTO> getList() {
return productDAO.findAll();
}
}
package com.example.tier.service;
import java.util.List;
import com.example.tier.dto.OrderDTO;
import com.example.tier.vo.OrderVO;
public interface OrderService {
// 인터페이스로 만드는 이유
// FoodOrder, ToolOrder, ClotheOrder 등 여러 상품에 대한 Order를 구현하기 위함
// 주문 추가
public void order(OrderDTO orderDTO);
// 주문 조회
public List<OrderVO> getList(String sort);
}
package com.example.tier.service;
import java.util.List;
import org.springframework.stereotype.Service;
import com.example.tier.dao.OrderDAO;
import com.example.tier.dao.ProductDAO;
import com.example.tier.dto.OrderDTO;
import com.example.tier.vo.OrderVO;
import lombok.RequiredArgsConstructor;
@Service
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService{
private final OrderDAO orderDAO;
private final ProductDAO productDAO;
// 주문하기
@Override
public void order(OrderDTO orderDTO) {
productDAO.setProductStock(orderDTO); // 재고량을 차감한다
orderDAO.save(orderDTO); // 주문을 저장한다
}
@Override
public List<OrderVO> getList(String sort) {
return orderDAO.findAll(sort);
}
}
5. Controller
- src/main/java의 하위 패키지로 controller 패키지를 만들고, ProductController와 OrderController 클래스를 만든다.
- Controller 클래스는
@ControllerAnnotation과@RequestMapping("/상위경로")Annotation(선택사항)을 추가한다. @RequestMapping()이 없다면 URI에 "/매핑이름"만 입력해도 되지만, 컨트롤러가 여러 개 존재하거나 항목별로 구분하고 싶다면 "/상위경로/매핑이름" 형태로 작성하기 위해 Annotation을 추가해도 된다.- RedirectView : 절대 경로, context 상대경로, 또는 현재 요청의 상대 URL로 redirect하는 view 클래스
- Spring에서
return "redirect:경로명는 String을 반환하는 리다이렉션 형태고, RedirectView는 클래스 인스턴스를 사용한 리다이렉션 형태다.
- Spring에서
- HTML을 반환할 경우 파일의 src/main/resources/template 내의 경로와 파일 이름까지만 작성한다.
- Controller 클래스는
package com.example.tier.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.view.RedirectView;
import com.example.tier.dto.ProductDTO;
import com.example.tier.service.ProductService;
import lombok.RequiredArgsConstructor;
@Controller
@RequestMapping("/product/*")
@RequiredArgsConstructor
public class ProductController {
// 서비스 하나 당 query문 1개라면 Mapper를 직접 주입했으나
// Mapper를 여러 개 주입하기보다는 Service를 주입해서 해결한다
private final ProductService productService; // ProductServiceImpl 객체가 주입됨
@GetMapping("register") // "/product/register"로 요청
public String register(Model model) {
model.addAttribute("productDTO", new ProductDTO()); // model을 사용하여 페이지에 데이터 포워딩
return "/product/product-insert"; // html의 경로와 이름까지만 작성
}
@PostMapping("register")
public RedirectView register(ProductDTO productDTO) {
productService.register(productDTO);
return new RedirectView("list");
}
@GetMapping(value={"/", "list"})
public String list(Model model) {
model.addAttribute("list", productService.getList());
return "/product/product-list";
}
}
package com.example.tier.controller;
import java.util.List;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.view.RedirectView;
import com.example.tier.dto.OrderDTO;
import com.example.tier.service.OrderService;
import com.example.tier.vo.OrderVO;
import lombok.RequiredArgsConstructor;
@Controller
@RequiredArgsConstructor
@RequestMapping("/order/*")
public class OrderController {
private final OrderService orderService;
@GetMapping("done")
public RedirectView order(OrderDTO orderDTO) {
System.out.println("주문 개수 : " + orderDTO.getProductCount());
orderService.order(orderDTO);
return new RedirectView("/product/list");
}
@GetMapping("list")
public String list(Model model, @RequestParam(required=false, defaultValue="recent") String sort) {
// if (sort==null) { // 이렇게 작성해도 된다
// sort="recent";
// }
List<OrderVO> list = orderService.getList(sort); // DB에 sort를 넘겨주어 정렬 방법을 설정한다
model.addAttribute("orders", list);
model.addAttribute("sort", sort);
return "order/order-list";
}
}
6. HTML
- 이제 src/main/resources/templates 패키지 하위에 product 폴더와 order 폴더를 만들고, 정보를 주고받을 HTML을 만든다.
- 실습에서 script에 JQuery를 사용했는데, 아직 JQuery에 대해 공부하지 못해 나중에 상세 내용을 추가할 예정이다.
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>상품 목록</title>
<style>
div{margin:0 auto; width:1000px;}
table{width:100%; text-align:center;}
button{width:50%;}
</style>
</head>
<body>
<a th:href="@{/product/register}">상품 추가하기</a>
<div class="container">
<table border="1">
<tr>
<th>단일 선택</th>
<th>주문 개수</th>
<th>번호</th>
<th>이름</th>
<th>재고</th>
<th>가격</th>
<th>등록 날짜</th>
<th>수정 날짜</th>
</tr>
<th:block th:each="product : ${list}">
<tr th:object="${product}">
<td><input type="radio" name="productId" th:value="*{productId}"></td>
<td><input type="text" class="productCount" readOnly></td>
<td th:text="*{productId}"></td>
<td th:text="*{productName}"></td>
<td th:text="*{productStock}"></td>
<td th:text="*{productPrice}"></td>
<td th:text="*{registerDate}"></td>
<td th:text="*{updateDate}"></td>
</tr>
</th:block>
</table>
<button type="button" id="order-done">주문완료</button><button type="button" onclick="location.href='/order/list'">주문내역</button>
</div>
<form th:action="@{/order/done}" method="get" name="order-form">
<input type="hidden" name="productId">
<input type="hidden" name="productCount">
</form>
<!-- JQuery 사용 -->
<script src="https://code.jquery.com/jquery-3.6.4.min.js"></script>
<script>
const $radios = $("input[type='radio']");
const $inputs = $("input[class='productCount']");
const $done = $("#order-done");
const $form = $("form[name='order-form']");
let $temp, i;
$radios.on("click", function() {
i = $radios.index(this); // 변수 i에 선택한 라디오 버튼의 index값 저장
/* console.log(i) */
if($temp) {
$temp.prop("readOnly", true);
$temp.value = "";
}
// input 태그가 i번인 태그를 선택하고, readOnly를 false로 설정
$inputs.eq(i).prop("readOnly", false);
// $temp에 선택된 input 태그를 저장
$temp = $inputs.eq(i);
});
$done.on("click", function() {
if(i+1) {
console.log('누른 라디오버튼 : ', i);
// form 태그에서 name이 productId인 input 태그에다가 radio 버튼의 제품 번호를 넣음
$form.find("input[name='productId']").val($radios.eq(i).val());
// form 태그에서 name이 productCount인 input 태그에다가 input의 제품 개수를 넣음
$form.find("input[name='productCount']").val($inputs.eq(i).val());
//console.log($form.find("input[name='productId']").val());
//console.log($form.find("input[name='productCount']").val());
$form.submit();
}
})
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>상품 추가</title>
</head>
<body>
<div>
<form name="f" th:action="@{/product/register}" th:object="${productDTO}" method="post">
<table>
<tr>
<!-- th:field : id, name, value 속성을 자동으로 처리함 -->
<th>이름</th>
<td><input th:type="text" th:field="*{productName}" placeholder="상품 이름"></td>
</tr>
<tr>
<th>수량</th>
<td><input th:type="text" th:field="*{productStock}" placeholder="재고량"></td>
</tr>
<tr>
<th>가격</th>
<td><input th:type="text" th:field="*{productPrice}" placeholder="상품 가격"></td>
</tr>
</table>
<button type="submit">등록</button>
<!-- a태그의 href는 th:href=@{"경로명"}으로 설정한다. -->
<a th:href="@{/product/list}">취소</a>
</form>
</div>
</body>
</html>
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
<style>
#container{margin:0 auto; width:1000px;}
span{cursor:pointer;}
span.on{font-weight:bold;}
table{width:100%; border:1px soild black;}
button{width:100%;}
</style>
</head>
<body>
<div id="container">
<div class="sort">
<span class="on" id="recent" data-sort="recent">최신순</span>
<span class="" id="price" data-sort="price">결제 금액순</span>
</div>
<table>
<tr>
<th>상품 이름</th>
<th>상품 가격</th>
<th>주문 개수</th>
<th>결제 금액</th>
<th>주문 날짜</th>
</tr>
<th:block th:each="order : ${orders}">
<tr th:object="${order}">
<td th:text="*{productName}"></td>
<td th:text="*{productPrice}"></td>
<td th:text="*{productCount}"></td>
<td th:text="*{orderPrice}"></td>
<td th:text="*{orderDate}"></td>
</tr>
</th:block>
</table>
<button type="button" onclick="location.href='/product/list'">상품목록</button>
</div>
<script src="https://code.jquery.com/jquery-3.6.4.min.js"></script>
<script th:inline="javascript">
let sort = [[${sort}]];
const $spans = $('div.sort span');
// span의 class를 전부 비우고, sort id를 가진 span 태그의 class 값을 on으로 변경
$("span").attr("class", "");
$("span#"+sort).attr("class", "on");
$spans.on("click", function() {
location.href = `/order/list?sort=${$(this).data("sort")}`;
});
</script>
</body>
</html>
- 첫 페이지에선 DB에 아무 항목이 없으므로 기본 표 빼곤 아무것도 없다.
- URL을 잘 보면 /product/list로 되어 있다.
- 상품 추가 링크로 상품을 추가하고 등록을 누르면 상품이 추가된다.
- 주문을 위해 여러 상품을 더 추가한다.
- 주문할 항목을 라디오 버튼으로 선택하면 주문 개수의 input이 사용 가능하게 바뀌고, 주문 수량을 눌러 완료 버튼을 누르면 바나나의 재고량이 차감된다.
- 상품을 더 주문하고 주문 내역으로 이동하면 주문한 내역을 최신순으로 확인할 수 있다.
- 주문 id 내림차순을 기준으로 정렬했기에 주문날짜 최신순 정렬과 동일하다.
- 결제 금액순으로 정렬하면 결제 금액이 큰 순서대로 내림차순 정렬된다.